Skip to content

feat: 공지사항, 자주 묻는 질문 기능 추가#241

Merged
X1n9fU merged 18 commits intomainfrom
develop
Aug 20, 2025
Merged

feat: 공지사항, 자주 묻는 질문 기능 추가#241
X1n9fU merged 18 commits intomainfrom
develop

Conversation

@X1n9fU
Copy link
Contributor

@X1n9fU X1n9fU commented Aug 20, 2025

#️⃣ 연관된 이슈

ex) #이슈번호, #이슈번호
#237 #239

📝 작업 내용

이번 PR에서 작업한 내용을 간략히 설명해주세요(이미지 첨부 가능)

  • 리디자인 학과 게시판 중 공지사항 기능 구현
  • 리디자인 학과 게시판 중 자주 묻는 질문 기능 구현

스크린샷 (선택)

💬 리뷰 요구사항(선택)

리뷰어가 특별히 봐주었으면 하는 부분이 있다면 작성해주세요

ex) 메서드 XXX의 이름을 더 잘 짓고 싶은데 혹시 좋은 명칭이 있을까요?

Summary by CodeRabbit

  • New Features

    • 학과 공지 게시판 읽기/작성/수정/삭제 및 이미지 업로드 지원
    • FAQ(자주 묻는 질문) 조회/작성/수정/삭제 엔드포인트 추가
    • 제휴업체 생성 시 메인/서브 이미지 업로드 지원
    • 공지 카테고리(학과 공지) 추가 및 학과 약어 표기 반영
  • Improvements

    • 알림 목록 날짜 형식 통일(Asia/Seoul, yyyy-MM-dd HH:mm:ss)
  • Bug Fixes

    • 권한 체크 표현 수정으로 MANAGER/ADMIN 접근 제어 정상화
    • 예외 처리 개선: 공지/FAQ 도메인별 오류 코드 적용, 알 수 없는 오류는 500 반환
  • Documentation

    • Notification API 태그 및 설명 추가
    • 제휴업체 관련 API 문구 정리

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 20, 2025

📝 Walkthrough

Walkthrough

공지/FAQ 기능 추가 및 예외 처리 확장. Department enum에 약어 필드 도입. Notice/Question 도메인에 Controller/Service/Repository/DTO/Exception 추가. Voice 모듈 패키지 이동. 일부 컨트롤러의 권한 표현 변경 및 Partner 생성 API에 이미지 파라미터 추가. Notification 문서화/응답 포맷 보강. Post 카테고리/엔티티 보강.

Changes

Cohort / File(s) Summary
Department 약어 추가
src/main/java/inu/codin/codin/common/dto/Department.java
enum 각 상수에 약어 인자 추가, abbreviation 필드/게터 도입, 생성자 2-인자화.
글로벌 예외 처리 확장
src/main/java/inu/codin/codin/common/exception/GlobalExceptionHandler.java
QuestionException/NoticeException 전용 핸들러 추가, 일반 예외 500 반환 및 로깅 강화.
Notice 도메인 신규
.../domain/board/notice/controller/NoticeController.java, .../notice/dto/request/NoticeCreateUpdateRequestDTO.java, .../notice/dto/response/NoticeDetailResponseDto.java, .../notice/dto/response/NoticeListResponseDto.java, .../notice/dto/response/NoticePageResponse.java, .../notice/exception/NoticeErrorCode.java, .../notice/exception/NoticeException.java, .../notice/repository/NoticeRepository.java, .../notice/service/NoticeService.java
공지 목록/상세/생성/수정/삭제 API, 페이지 응답/리스트/상세 DTO, 오류코드/예외, Mongo 리포지토리, 서비스 로직 추가.
Question 도메인 신규
.../domain/board/question/controller/QuestionController.java, .../question/dto/request/QuestionCreateUpdateRequestDto.java, .../question/dto/response/QuestionResponseDto.java, .../question/entity/QuestionEntity.java, .../question/exception/QuestionErrorCode.java, .../question/exception/QuestionException.java, .../question/repository/QuestionRepository.java, .../question/service/QuestionService.java
FAQ 목록/생성/수정/삭제 API, 요청/응답 DTO, 문서 엔티티, 오류코드/예외, Mongo 리포지토리, 서비스 로직 추가.
Voice 모듈 패키지 이동
.../domain/board/voice/controller/VoiceController.java, .../board/voice/dto/VoiceBoxAnswerRequest.java, .../board/voice/dto/VoiceBoxCreateRequest.java, .../board/voice/dto/VoiceBoxDetailResponse.java, .../board/voice/dto/VoiceBoxPageResponse.java, .../board/voice/entity/VoiceEntity.java, .../board/voice/repository/VoiceRepository.java, .../board/voice/service/VoiceService.java
패키지/임포트 경로를 post.domain.voice → board.voice로 변경. 기능 동일.
권한 표현 업데이트
.../domain/info/controller/LabController.java, .../domain/info/controller/OfficeController.java, .../domain/info/controller/ProfessorController.java, .../domain/lecture/controller/LectureUploadController.java
@PreAuthorize hasAnyRole 인자에서 'ROLE_' 접두어 제거(‘MANAGER’, ‘ADMIN’).
Partner 생성 API 변경
src/main/java/inu/codin/codin/domain/info/controller/PartnerController.java
권한 표현 변경, createPartner에 mainImage/subImages 멀티파트 파라미터 추가 및 서비스 호출 시그니처 변경.
Notification 보강
.../domain/notification/controller/NotificationController.java, .../domain/notification/dto/response/NotificationListResponseDto.java
컨트롤러에 Swagger Tag 추가, 응답 DTO의 dateTime JSON 포맷 지정 및 id의 @id 제거.
Post 엔티티/카테고리 확장
src/main/java/inu/codin/codin/domain/post/entity/PostCategory.java, src/main/java/inu/codin/codin/domain/post/entity/PostEntity.java
카테고리에 DEPARTMENT_NOTICE 추가, PostEntity에 updateNotice(추가 병합형) 메서드 추가.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Manager as Manager(관리자/매니저)
  participant API as NoticeController
  participant SVC as NoticeService
  participant SEC as SecurityUtils
  participant S3 as S3Service
  participant Repo as NoticeRepository
  participant UserRepo as UserRepository

  Manager->>API: POST /notice (noticeContent, noticeImages?)
  API->>SVC: createNotice(dto, images)
  SVC->>SEC: 현재 사용자 조회/권한 확인
  alt 이미지 존재
    SVC->>S3: 이미지 업로드(배치)
    S3-->>SVC: S3 URL 목록
  else 없음
    note over SVC: 빈 목록 사용
  end
  SVC->>UserRepo: 사용자 엔티티 조회
  SVC->>SVC: 학과 유효성 검사 및 제목 접두어(학과 약어) 구성
  SVC->>Repo: PostEntity 저장(카테고리=DEPARTMENT_NOTICE)
  Repo-->>SVC: 저장 결과(게시글 ID)
  SVC-->>API: {postId}
  API-->>Manager: 201 Created + SingleResponse
Loading
sequenceDiagram
  autonumber
  actor User as 사용자
  participant API as NoticeController
  participant SVC as NoticeService
  participant Repo as NoticeRepository
  participant Scrap as ScrapService
  participant Hits as HitsService
  participant UserRepo as UserRepository

  User->>API: GET /notice/category?department=&page=
  API->>SVC: getAllNotices(dept, page)
  SVC->>Repo: getNoticesByCategory(regex, [EXTRACURRICULAR_INNER, DEPARTMENT_NOTICE], pageable)
  Repo-->>SVC: Page<PostEntity>
  SVC->>UserRepo: 작성자 닉네임 조회(배치/개별)
  SVC-->>API: NoticePageResponse
  API-->>User: 200 OK

  User->>API: GET /notice/{postId}
  API->>SVC: getNoticesWithDetail(postId)
  SVC->>Repo: findByIdAndNotDeleted(id)
  Repo-->>SVC: PostEntity or empty
  alt 없음
    SVC-->>API: throw NoticeException(NOTICE_NOT_FOUND)
  else 있음
    SVC->>Scrap: 스크랩 카운트 조회
    SVC->>Hits: 조회수 조회
    SVC-->>API: NoticeDetailResponseDto
    API-->>User: 200 OK
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60–90 minutes

Possibly related PRs

Suggested labels

✨ Feature, Priority : High, Status: Reviewing

Suggested reviewers

  • BHC-Chicken
  • doma17

Poem

(\_/) 깡충깡충—새 공지와 FAQ가 펴졌네!
(•ᴗ•)و 약어를 달고 제목도 반짝,
⛅ 예외는 길을 안내하고,
📦 보이스는 새 집으로 이사 완료,
🧭 권한 표지도 또렷해졌지.
오늘도 코드는 당근처럼 바삭! 🥕

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch develop

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@X1n9fU X1n9fU changed the title Develop feat: 공지사항, 자주 묻는 질문 기능 추가 Aug 20, 2025
@coderabbitai coderabbitai bot requested review from BHC-Chicken and doma17 August 20, 2025 14:06
@coderabbitai coderabbitai bot added Priority : High 우선순위 상 Status: Reviewing 처리하고 리뷰 중인 이슈 ✨ Feature 기능 개발 labels Aug 20, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
src/main/java/inu/codin/codin/domain/board/voice/repository/VoiceRepository.java (1)

15-23: 인덱스 선언 필수 추가 요청 (department, answer, deletedAt)

VoiceEntity에 department, answer, deletedAt 조합에 대한 인덱스 애노테이션이 없습니다. 컬렉션 스캔으로 인한 페이징 쿼리 성능 저하를 방지하려면 해당 필드들에 @indexed 혹은 @CompoundIndex를 반드시 선언해주세요.

• 대상 파일
src/main/java/inu/codin/codin/domain/board/voice/entity/VoiceEntity.java

• 추가가 필요한 위치

  • private Department department;
  • private String answer;
  • private LocalDateTime deletedAt;

• 예시 (@CompoundIndex)

@Document(collection = "voice_boxes")
@CompoundIndexes({
  @CompoundIndex(
    name = "voice_dept_answer_deleted_idx",
    def = "{'department': 1, 'answer': 1, 'deletedAt': 1}"
  )
})
public class VoiceEntity extends BaseTimeEntity {
    // …
}
src/main/java/inu/codin/codin/domain/board/voice/dto/VoiceBoxDetailResponse.java (1)

33-45: 인증 컨텍스트 부재 시 DTO 매핑 실패 가능 + 중복 호출 최적화

of() 내부에서 SecurityUtils.getCurrentUserId()를 두 번 호출하고, 비인증 요청에서는 JwtException이 발생해 응답 직렬화가 실패할 수 있습니다. 한 번만 조회하고 예외는 무시(익명인 경우 null 처리)하는 방식으로 방어 코드를 추가하는 것을 권장합니다.

아래와 같이 수정 제안드립니다:

-    public static VoiceBoxDetailResponse of(VoiceEntity voiceEntity) {
-        return VoiceBoxDetailResponse.builder()
-                .boxId(voiceEntity.getId().toHexString())
-                .department(voiceEntity.getDepartment())
-                .question(voiceEntity.getQuestion())
-                .answer(voiceEntity.getAnswer())
-                .isUserInPositive(voiceEntity.getPositiveVoteIds() == null ? null : voiceEntity.getPositiveVoteIds().contains(SecurityUtils.getCurrentUserId()))
-                .isUserInOpposite(voiceEntity.getOppositeVoteIds() == null ? null : voiceEntity.getOppositeVoteIds().contains(SecurityUtils.getCurrentUserId()))
-                .userCountPositive(voiceEntity.getPositiveVoteIds() == null ? null : voiceEntity.getPositiveVoteIds().size())
-                .userCountOpposite(voiceEntity.getOppositeVoteIds() == null ? null : voiceEntity.getOppositeVoteIds().size())
-                .createdAt(voiceEntity.getCreatedAt())
-                .build();
-    }
+    public static VoiceBoxDetailResponse of(VoiceEntity voiceEntity) {
+        org.bson.types.ObjectId currentUserId = null;
+        try {
+            currentUserId = SecurityUtils.getCurrentUserId();
+        } catch (JwtException ignored) {
+            // 비인증 요청인 경우 null 유지
+        }
+
+        var positiveIds = voiceEntity.getPositiveVoteIds();
+        var oppositeIds = voiceEntity.getOppositeVoteIds();
+
+        return VoiceBoxDetailResponse.builder()
+                .boxId(voiceEntity.getId().toHexString())
+                .department(voiceEntity.getDepartment())
+                .question(voiceEntity.getQuestion())
+                .answer(voiceEntity.getAnswer())
+                .isUserInPositive(positiveIds == null ? null : (currentUserId != null && positiveIds.contains(currentUserId)))
+                .isUserInOpposite(oppositeIds == null ? null : (currentUserId != null && oppositeIds.contains(currentUserId)))
+                .userCountPositive(positiveIds == null ? null : positiveIds.size())
+                .userCountOpposite(oppositeIds == null ? null : oppositeIds.size())
+                .createdAt(voiceEntity.getCreatedAt())
+                .build();
+    }

추가로 필요한 import (메서드 본문 외부 변경):

import inu.codin.codin.common.exception.JwtException;
src/main/java/inu/codin/codin/domain/board/voice/service/VoiceService.java (2)

45-63: NPE 위험: toggleVoiceBox의 Boolean 매개변수 null 처리 필요

positive가 null인 경우 if (positive)에서 NPE가 발생합니다. 컨트롤러 바인딩 누락 시 실제로 발생할 수 있으므로 즉시 방어 코드를 추가하세요.

 public void toggleVoiceBox(String boxId, Boolean positive) {
     if (!ObjectIdUtil.isValid(boxId)) {
         throw new IllegalArgumentException("유효하지 않은 ID 형식입니다.");
     }
 
+    if (positive == null) {
+        throw new IllegalArgumentException("positive 파라미터는 null일 수 없습니다.");
+    }
+
     ObjectId objectId = ObjectIdUtil.toObjectId(boxId);
     VoiceEntity voiceEntity = voiceRepository.findByIdAndNotDeleted(objectId)
             .orElseThrow(() -> new IllegalArgumentException("익명의 소리함 질문을 찾을 수 없습니다."));
 
     ObjectId currentUserId = SecurityUtils.getCurrentUserId();
 
     if (positive) {
         voiceEntity.votePositiveToggle(currentUserId);
     } else {
         voiceEntity.voteOppositeToggle(currentUserId);
     }
 
     voiceRepository.save(voiceEntity);
 }

54-63: 경합 시 갱신 손실 가능성: 투표 토글은 원자적 업데이트/낙관적 락 도입 권장

현재 read-modify-write 패턴(save)으로는 동시 요청 시 한쪽 변경이 소실될 수 있습니다(예: 서로 다른 사용자의 동시 토글). MongoDB에서는 $addToSet/$pull 기반 원자적 업데이트 또는 @Version을 이용한 낙관적 락을 권장합니다.

옵션:

  • VoiceEntity에 @Version Long version 필드를 추가하고 OptimisticLockingFailureException 발생 시 재시도.
  • MongoTemplate를 사용해 filter(_id, deletedAt=null, voteIds 포함여부)에 따라 $addToSet/$pull를 한 번의 update로 수행.

원자적 업데이트 예시(개념 코드):

// filter: _id, deletedAt=null
// update: positive==true ? $addToSet: {positiveVoteIds: userId} / $pull: {oppositeVoteIds: userId}
// 반대의 경우도 대칭 처리

필요 시 구현 스니펫 제공 가능합니다.

src/main/java/inu/codin/codin/domain/info/controller/PartnerController.java (1)

31-37: 오탈자(“내열”→“내역”) 및 용어 일관성(“Partner”→“제휴업체”) 정정 제안

API 문서/응답 메시지의 사용자 노출 텍스트는 일관성과 정확성이 중요합니다. 아래 두 곳 수정 권장합니다.

적용 diff:

-                .body(new ListResponse<>(200, "Partner 썸네일 리스트 반환 성공", partnerService.getPartnerList()));
+                .body(new ListResponse<>(200, "제휴업체 썸네일 리스트 반환 성공", partnerService.getPartnerList()));

-                .body(new SingleResponse<>(200, "Partner 상세 내열 반환 성공", partnerService.getPartnerDetails(partnerId)));
+                .body(new SingleResponse<>(200, "제휴업체 상세 내역 반환 성공", partnerService.getPartnerDetails(partnerId)));

Also applies to: 39-46

♻️ Duplicate comments (1)
src/main/java/inu/codin/codin/domain/info/controller/PartnerController.java (1)

48-48: 권한 표현 변경(hasAnyRole('MANAGER','ADMIN')) — SecurityConfig 설정과의 정합성 재확인

LabController와 동일한 변경입니다. 역할 접두어(ROLE_) 설정과 실제 권한 문자열이 일치하는지 확인이 필요합니다. 상위 코멘트( LabController )의 검증 스크립트를 참고해 주세요.

Also applies to: 61-61

🧹 Nitpick comments (55)
src/main/java/inu/codin/codin/domain/notification/dto/response/NotificationListResponseDto.java (1)

21-22: LocalDateTime에 timezone 속성은 효과가 없습니다 — 직렬화 시간대 의도 확인 필요

Jackson에서 LocalDateTime은 시간대 정보를 갖지 않으므로 @jsonformat의 timezone="Asia/Seoul"은 변환에 사용되지 않습니다. 결국 현재 값 그대로 패턴만 적용되어 직렬화됩니다. KST 고정 표현이 목적이라면 아래 중 하나를 고려해주세요.

  • 최소 변경: timezone 속성을 제거해 혼동을 줄입니다.
  • 표준화: OffsetDateTime/ZonedDateTime로 전환하여 명시적 오프셋(+09:00) 또는 타임존을 포함한 ISO-8601로 반환합니다.

아래는 최소 변경(혼동 제거) 제안입니다.

-    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
     private LocalDateTime dateTime;

아키텍처적으로, 동일 패턴을 여러 DTO에서 반복 중이라면 전역 Serializer를 통해 일괄 적용하는 것을 권장합니다(아래 참고).

전역 Serializer 예시(Spring Boot):

// 예: src/main/java/.../config/JacksonConfig.java
import java.time.format.DateTimeFormatter;
import java.time.LocalDateTime;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Bean;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;

@Configuration
public class JacksonConfig {
    private static final DateTimeFormatter LDT_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    @Bean
    public Jackson2ObjectMapperBuilderCustomizer jsonCustomizer() {
        return builder -> {
            builder.serializerByType(LocalDateTime.class, new LocalDateTimeSerializer(LDT_FMT));
            builder.deserializerByType(LocalDateTime.class, new LocalDateTimeDeserializer(LDT_FMT));
            // 필요 시 WRITE_DATES_AS_TIMESTAMPS 비활성화 등 추가 설정
        };
    }
}

검증 포인트:

  • 서버 시스템 타임존이 KST가 아닐 때도 응답 시간이 기대한 KST 표현인지 확인.
  • 클라이언트가 ISO-8601(+09:00) 형태를 선호한다면 OffsetDateTime로의 전환 영향 범위를 점검.
src/main/java/inu/codin/codin/domain/board/voice/dto/VoiceBoxAnswerRequest.java (1)

10-12: 답변 길이 제한 추가 제안 (@SiZe)

서비스/DB/응답 크기 보호를 위해 내용 길이 상한을 두는 것을 권장합니다.

다음과 같이 길이 제한을 추가할 수 있습니다:

     @Schema(description = "답변 내용", example = "학회비를 납부하면 다양한 혜택을 받을 수 있습니다.")
-    @NotBlank(message = "답변 내용은 필수입니다.")
+    @NotBlank(message = "답변 내용은 필수입니다.")
+    @Size(max = 1000, message = "답변은 최대 1000자까지 가능합니다.")
     private String answer;

추가로 필요한 import:

import jakarta.validation.constraints.Size;
src/main/java/inu/codin/codin/domain/board/voice/dto/VoiceBoxPageResponse.java (1)

24-30: 네이밍 잔존(post) → voice 용어로 정리 권장

메서드 파라미터명이 post/postPaging로 남아 있어 도메인 이전 의도가 드러나지 않습니다. 가독성을 위해 voice/contents로 정리하세요.

-    public static VoiceBoxPageResponse of(List<VoiceBoxDetailResponse> postPaging, long lastPage, long nextPage) {
-        return VoiceBoxPageResponse.newPagingHasNext(postPaging, lastPage, nextPage);
+    public static VoiceBoxPageResponse of(List<VoiceBoxDetailResponse> contents, long lastPage, long nextPage) {
+        return VoiceBoxPageResponse.newPagingHasNext(contents, lastPage, nextPage);
     }
 
-    private static VoiceBoxPageResponse newPagingHasNext(List<VoiceBoxDetailResponse> posts, long lastPage, long nextPage) {
-        return new VoiceBoxPageResponse(posts, lastPage, nextPage);
+    private static VoiceBoxPageResponse newPagingHasNext(List<VoiceBoxDetailResponse> contents, long lastPage, long nextPage) {
+        return new VoiceBoxPageResponse(contents, lastPage, nextPage);
     }
src/main/java/inu/codin/codin/domain/board/voice/entity/VoiceEntity.java (2)

37-38: 불필요한 이중 세미콜론 제거

문법적으로 문제는 없지만 불필요한 ;;은 스타일/정적분석 경고를 유발합니다.

-        this.positiveVoteIds = positiveVoteIds != null ? positiveVoteIds : new ArrayList<>();;
-        this.oppositeVoteIds = oppositeVoteIds != null ? oppositeVoteIds : new ArrayList<>();;
+        this.positiveVoteIds = positiveVoteIds != null ? positiveVoteIds : new ArrayList<>();
+        this.oppositeVoteIds = oppositeVoteIds != null ? oppositeVoteIds : new ArrayList<>();

16-33: 쿼리 패턴에 맞춘 인덱스 고려 (MongoDB)

부서별 조회(Department)와 “답변 여부(Answer null/non-null)”로 페이징하는 패턴이 있다면, 아래와 같은 인덱스를 고려하면 성능에 도움이 됩니다.

  • 단일: { department: 1 }
  • 복합(자주 함께 필터링한다면): { department: 1, answer: 1 }
  • 미답변 먼저 조회한다면(partial index가 가능하다면): answer 필드 null 여부 기반 인덱스

Spring Data MongoDB에서는 @indexed 혹은 @CompoundIndex를 사용할 수 있습니다.

src/main/java/inu/codin/codin/domain/board/voice/controller/VoiceController.java (4)

41-47: 페이지 파라미터 유효성: 원시 타입에 @NotNull 무효, 음수 방지 추가 권장

int에는 @NotNull이 적용되지 않습니다. 음수 페이지 요청 방지를 위해 @PositiveOrZero(또는 @min(0))로 교체하세요.

-            @RequestParam("page") @NotNull int pageNumber
+            @RequestParam("page") @PositiveOrZero int pageNumber
-            @RequestParam("page") @NotNull int pageNumber
+            @RequestParam("page") @PositiveOrZero int pageNumber

이 변경을 위해 아래 import가 필요합니다:

import jakarta.validation.constraints.PositiveOrZero;

Also applies to: 64-70


10-16: 검증 애너테이션 import 보강

위 페이지 파라미터 제안을 반영하려면 PositiveOrZero import가 필요합니다.

 import io.swagger.v3.oas.annotations.tags.Tag;
 import jakarta.validation.Valid;
+import jakarta.validation.constraints.PositiveOrZero;
 import jakarta.validation.constraints.NotNull;

75-81: HTTP 메서드 의미론: 답변 추가는 POST보다 PATCH/PUT이 적합

리소스의 부분 업데이트(답변 추가/변경)라면 @PatchMapping(또는 @PutMapping)이 의도에 더 가깝습니다. API 컨트랙트 변경 영향이 작다면 교체를 고려하세요.

-    @PostMapping("/not-answered/{boxId}")
+    @PatchMapping("/not-answered/{boxId}")

87-93: 삭제 응답 코드 개선 제안

삭제 성공 시 200 OK + 바디 대신 204 No Content도 고려 가능합니다. 다만 현재 공통 응답 래퍼(SingleResponse)를 사용 중이라면 일관성을 우선해도 됩니다.

src/main/java/inu/codin/codin/domain/board/voice/dto/VoiceBoxCreateRequest.java (1)

16-18: 질문 길이 제한 추가 제안 (@SiZe)

스팸/과도한 본문을 방지하고 문서/DB 제약과 정렬되도록 길이 제한을 권장합니다.

     @Schema(description = "질문", example = "학회비 낸 사람은 얼마나 이득인가요?")
-    @NotBlank(message = "질문 내용은 필수입니다.")
+    @NotBlank(message = "질문 내용은 필수입니다.")
+    @Size(max = 1000, message = "질문은 최대 1000자까지 가능합니다.")
     private String question;

추가로 필요한 import:

import jakarta.validation.constraints.Size;
src/main/java/inu/codin/codin/domain/board/voice/repository/VoiceRepository.java (2)

18-23: 문자열 @query 대신 파생 메서드 쿼리로 전환 제안 (타입 세이프티/리팩토링 내구성 향상)

문자열 기반 @query는 런타임까지 오류가 지연됩니다. 동일 조건은 스프링 데이터 메서드 파생 쿼리로 안전하게 표현 가능합니다.

아래와 같이 대체를 제안합니다:

-    @Query("{'department': ?0, 'answer': {$ne: null}, 'deletedAt': null}")
-    Page<VoiceEntity> findAnsweredByDepartmentAndNotDeleted(Department department, Pageable pageable);
+    Page<VoiceEntity> findByDepartmentAndAnswerNotNullAndDeletedAtIsNull(Department department, Pageable pageable);
-    @Query("{'department': ?0, 'answer': null, 'deletedAt': null}")
-    Page<VoiceEntity> findByDepartmentAndAnswerIsNullAndNotDeleted(Department department, Pageable pageable);
+    Page<VoiceEntity> findByDepartmentAndAnswerIsNullAndDeletedAtIsNull(Department department, Pageable pageable);

호출부 메서드명도 함께 변경 필요합니다.


26-27: ID 조회 쿼리도 파생 메서드로 통일 제안

일관성을 위해 아래처럼 바꾸면 좋습니다.

-    @Query("{'_id': ?0, 'deletedAt': null}")
-    Optional<VoiceEntity> findByIdAndNotDeleted(ObjectId id);
+    Optional<VoiceEntity> findByIdAndDeletedAtIsNull(ObjectId id);
src/main/java/inu/codin/codin/domain/board/voice/dto/VoiceBoxDetailResponse.java (2)

24-26: 불리언 필드 네이밍/타입 개선 제안

isUserInPositive / isUserInOpposite가 Boolean(객체)이며 null을 반환할 수 있습니다. API 소비자 입장에서는 boolean 기본형으로 false를 디폴트로 주는 편이 해석이 명확합니다. 또한 필드명이 is*로 시작하면 Lombok/Jackson 접근자 생성이 혼동될 수 있으니 userInPositive 같은 네이밍을 고려해 주세요.


35-36: ObjectId 문자열 변환 유틸 일관 사용 제안

직접 toHexString() 대신 ObjectIdUtil.toString(...)을 사용하면 전역 포맷 일관성이 보장됩니다.

src/main/java/inu/codin/codin/domain/board/voice/service/VoiceService.java (4)

38-43: 페이지 번호 음수 입력 방어 로직 제안

음수 pageNumber가 들어오면 PageRequest.of(...)에서 예외가 발생합니다. 안전 가드를 추가하세요.

-        Pageable pageable = PageRequest.of(pageNumber, PAGE_SIZE, Sort.by("createdAt").descending());
-        Page<VoiceEntity> voiceEntities = voiceRepository.findAnsweredByDepartmentAndNotDeleted(department, pageable);
-
-        return getVoiceBoxPageResponse(pageNumber, voiceEntities);
+        int safePageNumber = Math.max(0, pageNumber);
+        Pageable pageable = PageRequest.of(safePageNumber, PAGE_SIZE, Sort.by("createdAt").descending());
+        Page<VoiceEntity> voiceEntities = voiceRepository.findAnsweredByDepartmentAndNotDeleted(department, pageable);
+        return getVoiceBoxPageResponse(safePageNumber, voiceEntities);

동일 가드를 getAllNotAnsweredList에도 적용하는 것을 권장합니다.


65-70: getAllNotAnsweredList에도 동일한 페이지 가드 적용 권장

-        Pageable pageable = PageRequest.of(pageNumber, PAGE_SIZE, Sort.by("createdAt").descending());
-        Page<VoiceEntity> notAnsweredVoices = voiceRepository.findByDepartmentAndAnswerIsNullAndNotDeleted(department, pageable);
-
-        return getVoiceBoxPageResponse(pageNumber, notAnsweredVoices);
+        int safePageNumber = Math.max(0, pageNumber);
+        Pageable pageable = PageRequest.of(safePageNumber, PAGE_SIZE, Sort.by("createdAt").descending());
+        Page<VoiceEntity> notAnsweredVoices = voiceRepository.findByDepartmentAndAnswerIsNullAndNotDeleted(department, pageable);
+        return getVoiceBoxPageResponse(safePageNumber, notAnsweredVoices);

100-109: lastPage/nextPage 값 의미 명확화(0/1 기반 여부) 및 네이밍

getTotalPages()는 “총 페이지 수”이며 마지막 페이지 인덱스가 아닙니다. lastPage라는 명칭은 “마지막 페이지 번호”로 오인될 수 있습니다. totalPages로 이름을 바꾸거나, 1-based 표기를 원하면 적절히 보정해 주세요.


72-99: 중복 ID 검증/조회 로직 헬퍼로 추출 제안(가독성/중복 제거)

boxId 유효성 검증과 조회 패턴이 세 곳에서 반복됩니다. private 메서드로 추출하면 가독성과 유지보수성이 좋아집니다.

예시:

private VoiceEntity getActiveVoiceOrThrow(String boxId) {
    if (!ObjectIdUtil.isValid(boxId)) {
        throw new IllegalArgumentException("유효하지 않은 ID 형식입니다.");
    }
    ObjectId objectId = ObjectIdUtil.toObjectId(boxId);
    return voiceRepository.findByIdAndNotDeleted(objectId)
            .orElseThrow(() -> new IllegalArgumentException("익명의 소리함 질문을 찾을 수 없습니다."));
}
src/main/java/inu/codin/codin/domain/info/controller/PartnerController.java (1)

52-59: PartnerService.createPartner 3-파라미터 서명 일치 확인 및 subImages null 처리 제안

  • PartnerService.createPartner(PartnerCreateRequestDto, MultipartFile, List) 서명이 컨트롤러 호출부와 일치합니다.
  • Optional: subImages가 null일 때 NPE 발생 가능성이 있으므로, 컨트롤러에서 빈 리스트로 치환해 전달하거나 서비스 레이어에서 기본값 처리하여 안전성을 높이길 권장합니다.

추천 수정:

     @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
     public ResponseEntity<?> createPartner(@RequestPart("partnerInfo") @Valid PartnerCreateRequestDto partnerCreateRequestDto,
                                            @RequestPart(value = "mainImage", required = false) MultipartFile mainImage,
                                            @RequestPart(value = "subImages", required = false) List<MultipartFile> subImages) {
-        partnerService.createPartner(partnerCreateRequestDto, mainImage, subImages);
+        List<MultipartFile> safeSubImages = (subImages == null)
+            ? Collections.emptyList()
+            : subImages;
+        partnerService.createPartner(partnerCreateRequestDto, mainImage, safeSubImages);
         return ResponseEntity.status(HttpStatus.CREATED)
                 .body(new SingleResponse<>(201, "Partner 생성 완료", null));
     }

필요 시 상단에 import 추가:

import java.util.Collections;
src/main/java/inu/codin/codin/domain/board/notice/dto/request/NoticeCreateUpdateRequestDTO.java (1)

1-17: DTO 유효성 검증은 적절합니다 — 생성자 명시로 직렬화 안정성 소폭 강화 제안

현재도 기본 생성자가 암묵적으로 제공되어 Jackson 바인딩에 문제는 없어 보입니다. 다만 팀 컨벤션 차원에서 명시적으로 NoArgsConstructor를 두면 직렬화 안정성이 조금 더 명확해집니다.

예시 diff:

 import io.swagger.v3.oas.annotations.media.Schema;
 import jakarta.validation.constraints.NotBlank;
+import lombok.AccessLevel;
 import lombok.Getter;
+import lombok.NoArgsConstructor;

 @Getter
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
 public class NoticeCreateUpdateRequestDTO {
src/main/java/inu/codin/codin/domain/board/question/repository/QuestionRepository.java (1)

11-14: 부서별 조회에 페이징/정렬 지원과 인덱스 고려

리스트 전체 반환은 데이터 증가 시 비용이 커집니다. Pageable 지원 메서드를 추가하고, department 필드에 인덱스를 두면 조회 성능이 좋아집니다.

권장 diff:

+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Page;
 @Repository
 public interface QuestionRepository extends MongoRepository<QuestionEntity, ObjectId> {
-    List<QuestionEntity> findAllByDepartment(Department department);
+    List<QuestionEntity> findAllByDepartment(Department department);
+    Page<QuestionEntity> findAllByDepartment(Department department, Pageable pageable);
 }

엔티티 쪽 인덱스 예시(참고, 다른 파일):

// src/main/java/.../QuestionEntity.java
@Indexed
private Department department;
src/main/java/inu/codin/codin/domain/board/question/dto/request/QuestionCreateUpdateRequestDto.java (1)

20-22: Swagger에서 Enum 값 표시 개선 제안

스키마에 Department enum을 직접 연결하면 허용 값이 문서에 명확히 드러납니다. 또한 팀 컨벤션상 기본 생성자 명시가 필요하다면 Notice DTO와 동일하게 적용을 검토해 주세요.

권장 diff:

-    @Schema(description = "학과", example = "COMPUTER_SCI")
+    @Schema(description = "학과", implementation = Department.class, example = "COMPUTER_SCI")
     @NotNull
     private Department department;

선택(일관성):

+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
 @Getter
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
 public class QuestionCreateUpdateRequestDto {
src/main/java/inu/codin/codin/domain/board/question/exception/QuestionException.java (1)

5-12: 부모 필드 섀도잉 제거 및 공변 반환 타입으로 getErrorCode()만 오버라이드하는 편이 안전합니다.

현재 GlobalException에도 errorCode가 있을 가능성이 높습니다. 동일 이름의 필드를 다시 선언해 저장하면 부모 필드를 섀도잉하게 되고, 유지보수 시 불필요한 이중 상태가 됩니다. typed getter가 목적이라면 필드 재선언 없이 메서드만 오버라이드하세요.

적용 예시:

-import lombok.Getter;
-@Getter
 public class QuestionException extends GlobalException {
 
-    private final QuestionErrorCode errorCode;
     public QuestionException(QuestionErrorCode errorCode) {
         super(errorCode);
-        this.errorCode = errorCode;
     }
+
+    @Override
+    public QuestionErrorCode getErrorCode() {
+        return (QuestionErrorCode) super.getErrorCode();
+    }
 }
src/main/java/inu/codin/codin/domain/board/notice/exception/NoticeException.java (1)

6-13: NoticeException도 동일하게 필드 섀도잉을 제거하고 오버라이드로 일관화하세요.

typed getter만 필요하다면, 부모의 상태만 단일 소스로 유지하고 공변 반환 타입으로 getErrorCode()를 오버라이드하는 방식이 깔끔합니다.

적용 예시:

-@Getter
 public class NoticeException extends GlobalException {
 
-    private final NoticeErrorCode errorCode;
     public NoticeException(NoticeErrorCode errorCode) {
         super(errorCode);
-        this.errorCode = errorCode;
     }
+
+    @Override
+    public NoticeErrorCode getErrorCode() {
+        return (NoticeErrorCode) super.getErrorCode();
+    }
 }
src/main/java/inu/codin/codin/common/dto/Department.java (1)

25-35: fromDescription에 약어(abbreviation) 지원 추가 제안

이제 각 학과에 약어가 도입되었으므로, 입력이 약어인 경우에도 매칭되도록 하면 사용성이 좋아집니다. 현재는 name 또는 description만 매칭합니다.

간단 적용 예시:

     public static Department fromDescription(String description) {
         for (Department department : Department.values()) {
             if (department.name().equals(description) || department.getDescription().equals(description)) {
                 return department;
             }
         }
 
         log.warn("정보대 내의 학과가 아닙니다. description : " + description);
         return OTHERS;
     }

를 다음과 같이 수정:

     public static Department fromDescription(String description) {
+        if (description == null) {
+            log.warn("정보대 내의 학과가 아닙니다. description : null");
+            return OTHERS;
+        }
+        description = description.trim();
         for (Department department : Department.values()) {
-            if (department.name().equals(description) || department.getDescription().equals(description)) {
+            if (department.name().equals(description)
+                    || department.getDescription().equals(description)
+                    || department.getAbbreviation().equals(description)) {
                 return department;
             }
         }
 
         log.warn("정보대 내의 학과가 아닙니다. description : " + description);
         return OTHERS;
     }

참고: 입력 정규화(trim)만 추가했고, 대소문자 무시는 도메인 특성상 불필요해 보여 제외했습니다.

src/main/java/inu/codin/codin/common/exception/GlobalExceptionHandler.java (3)

32-37: 스택트레이스 접근 시 방어적 처리(빈 스택 대비) 및 로그 레벨 재검토

e.getStackTrace()[0]는 비정상 생성된 예외에서 빈 배열일 수 있어 방어가 필요합니다. 또한 서버 오류(500) 레벨에서는 error 로그가 일반적입니다. 최소한 빈 스택 방어는 권장합니다.

적용 예시:

-        log.warn("[Exception] Class: {}, Error Message : {}, Stack Trace: {}",
-                e.getClass().getSimpleName(),
-                e.getMessage(),
-                e.getStackTrace()[0].toString());
+        StackTraceElement[] trace = e.getStackTrace();
+        String head = (trace != null && trace.length > 0) ? trace[0].toString() : "N/A";
+        log.warn("[Exception] Class: {}, Error Message : {}, Stack Trace: {}",
+                e.getClass().getSimpleName(),
+                e.getMessage(),
+                head);
         return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                 .body(new ExceptionResponse(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR.value()));

원하시면 warn -> error로 변경도 함께 제안드릴 수 있습니다.


120-132: GlobalException 핸들러와 중복되는 도메인별 핸들러 제거 제안

QuestionException/NoticeException은 GlobalException을 상속하고, GlobalException 전용 핸들러가 동일한 로직을 수행합니다. 현재 구현은 완전 중복으로 유지 비용만 늘립니다. 도메인별 커스터마이징 계획이 없다면 제거를 권장합니다.

적용 예시:

-    @ExceptionHandler(QuestionException.class)
-    protected ResponseEntity<ExceptionResponse> handleQuestionException(QuestionException e) {
-        QuestionErrorCode code = e.getErrorCode();
-        return ResponseEntity.status(code.httpStatus())
-                .body(new ExceptionResponse(code.message(), code.httpStatus().value()));
-    }
-
-    @ExceptionHandler(NoticeException.class)
-    protected ResponseEntity<ExceptionResponse> handleNoticeException(NoticeException e) {
-        NoticeErrorCode code = e.getErrorCode();
-        return ResponseEntity.status(code.httpStatus())
-                .body(new ExceptionResponse(code.message(), code.httpStatus().value()));
-    }

도메인별 특수 처리(추가 로깅, 메시지 포맷 보정 등)를 곧 추가하실 계획이라면 그대로 유지하셔도 됩니다.


7-10: 중복 핸들러 제거 시 불필요해지는 import도 함께 정리

위 제안(도메인별 핸들러 제거)을 수용하는 경우, 관련 import를 정리하여 깨끗하게 유지하세요.

-import inu.codin.codin.domain.board.notice.exception.NoticeErrorCode;
-import inu.codin.codin.domain.board.notice.exception.NoticeException;
-import inu.codin.codin.domain.board.question.exception.QuestionErrorCode;
-import inu.codin.codin.domain.board.question.exception.QuestionException;
src/main/java/inu/codin/codin/domain/board/question/dto/response/QuestionResponseDto.java (2)

11-13: DTO 필드를 불변으로 만들어 직렬화 안정성과 스레드 안전성을 높이세요

Response DTO는 재할당될 이유가 거의 없습니다. final 적용을 권장합니다.

-    private String id;
-    private String question;
-    private String answer;
+    private final String id;
+    private final String question;
+    private final String answer;

15-20: ObjectId 문자열 변환은 toHexString()이 더 명확합니다

org.bson.types.ObjectId는 toString() 구현이 버전에 따라 표현이 달라질 수 있어(예: 래핑 문자열) API 응답 ID로는 toHexString() 사용이 안전합니다. 또한 QuestionEntity에 department가 존재하지만 응답에는 포함되지 않습니다. 요구사항상 department 노출이 필요한지 확인 부탁드립니다.

-                questionEntity.get_id().toString(),
+                questionEntity.get_id().toHexString(),
                 questionEntity.getQuestion(),
                 questionEntity.getAnswer()
src/main/java/inu/codin/codin/domain/board/question/entity/QuestionEntity.java (2)

19-21: 필드명 _id는 자바 컨벤션과 어긋납니다 (선택 사항)

MongoDB에서는 자바 필드명이 id여도 @id로 DB의 _id에 매핑됩니다. 팀 컨벤션에 맞춰 id로 통일하면 DTO/타 도메인과의 일관성이 좋아집니다. 다만 변경 범위가 커질 수 있으니 추후 리팩터로 고려해 주세요.


35-39: 메서드 명이 실제 동작과 달라 혼란을 줄 수 있습니다

updateQuestion이 question 뿐 아니라 answer/department까지 모두 변경합니다. update 또는 updateFrom(DTO) 같은 이름이 더 명확합니다.

src/main/java/inu/codin/codin/domain/board/notice/repository/NoticeRepository.java (4)

7-7: 미사용 import 제거

PageRequest는 사용되지 않습니다. 정리해 주세요.

-import org.springframework.data.domain.PageRequest;

19-23: postStatus 단일 값 필터는 $in 대신 동등 비교가 단순합니다

단일 상태만 조회한다면 $in 배열 대신 동등 비교가 더 간단하고 의도 전달이 명확합니다.

-    @Query("{'deletedAt': null, " +
-            "'postStatus': { $in: ['ACTIVE'] }, " +
+    @Query("{'deletedAt': null, " +
+            "'postStatus': 'ACTIVE', " +
             "'title': ?0," +
             "'postCategory': { $in: ?1 }}")

25-26: 파라미터명 컨벤션 수정 (Id -> id)

메서드 시그니처/파라미터명은 카멜케이스 소문자 시작을 권장합니다.

-    Optional<PostEntity> findByIdAndNotDeleted(ObjectId Id);
+    Optional<PostEntity> findByIdAndNotDeleted(ObjectId id);

19-24: 정규식 검색 성능 및 인덱스 고려 필요

title에 Pattern을 사용한 정규식 매칭은 접두(anchor: ^)가 없는 경우 컬렉션 스캔으로 성능 이슈가 발생할 수 있습니다. 접두 패턴을 사용하는지(예: "^prefix")와 title, postCategory, postStatus, deletedAt 복합 인덱스 구성 여부 확인을 권장합니다.

src/main/java/inu/codin/codin/domain/board/notice/dto/response/NoticeListResponseDto.java (2)

16-35: Response DTO에 Bean Validation(@NotBlank/@NotNull) 사용은 실효성이 낮습니다.

검증은 일반적으로 요청 DTO에 적용됩니다. 응답 DTO에는 Swagger 스키마(requiredMode) 표기로 대체하거나, 매핑 시 널 방지 로직으로 일관성만 보장하는 편이 더 깔끔합니다.


58-68: ObjectId 직렬화 방식 일관성 유지 필요 (userId).

동일 PR의 NoticeDetailResponseDto.of에서는 post.getUserId().toString()을 사용합니다. 본 DTO에서는 String.valueOf(...)를 사용하고 있어 미세한 불일치가 있습니다. 일관되게 toString() 사용 또는 명확하게 toHexString() 사용으로 통일하는 것을 권장합니다.

다음과 같이 사소 수정 가능합니다:

-                .userId(String.valueOf(postEntity.getUserId()))
+                .userId(postEntity.getUserId().toString())
src/main/java/inu/codin/codin/domain/board/question/controller/QuestionController.java (2)

21-21: 오탈자: 태그 설명의 ‘리다지인’ → ‘리디자인’.

문서 가독성을 위해 아래처럼 수정 제안합니다.

-@Tag(name = "Question API", description = "[리다지인] 게시판 자주 묻는 질문 API")
+@Tag(name = "Question API", description = "[리디자인] 게시판 자주 묻는 질문 API")

56-57: HTTP 상태 상수 사용으로 가독성 향상 제안.

숫자 리터럴(201) 대신 HttpStatus 상수 사용을 권장합니다.

-        return ResponseEntity.status(201)
+        return ResponseEntity.status(HttpStatus.CREATED)
                 .body(new SingleResponse<>(201, "자주 묻는 질문 작성 성공", null));

추가 import 필요:

import org.springframework.http.HttpStatus;
src/main/java/inu/codin/codin/domain/board/notice/dto/response/NoticeDetailResponseDto.java (3)

43-45: 필드명 단수/복수 불일치: List 타입인데 postImageUrl(단수).

API 응답 혼동을 줄이기 위해 postImageUrls로의 리네이밍을 권장합니다. 응답 스키마(예시)도 배열로 맞춰 주세요. 다만 외부 API 호환성(브레이킹 체인지) 영향 검토가 필요합니다.

-    @Schema(description = "게시물 내 이미지 url , blank 가능", example = "example/1231")
-    private final List<String> postImageUrl;
+    @Schema(description = "게시물 내 이미지 url 목록, 빈 배열 가능", example = "[\"example/1231\"]")
+    private final List<String> postImageUrls;
-        this.postImageUrl = postImageUrls;
+        this.postImageUrls = postImageUrls;

46-49: primitive boolean에 @NotNull은 무의미합니다.

boolean은 null이 될 수 없으므로 제거 권장.

-    @Schema(description = "게시물 익명 여부 default = true (익명)", example = "true")
-    @NotNull
-    private final boolean isAnonymous;
+    @Schema(description = "게시물 익명 여부 default = true (익명)", example = "true")
+    private final boolean isAnonymous;

17-35: Response DTO에서의 Bean Validation 사용은 과도합니다.

요청 DTO에 검증을 집중하고, 응답은 스키마 문서화로 대체하는 방식을 권장합니다.

src/main/java/inu/codin/codin/domain/board/question/service/QuestionService.java (1)

55-59: 부서 검증 로직 중복(DRY 위반).

NoticeService에도 유사한 validateDepartment가 존재합니다. 공통 유틸(예: Department 유효성 체크 메서드)로 추출하여 재사용을 권장합니다.

src/main/java/inu/codin/codin/domain/board/notice/service/NoticeService.java (3)

124-129: N+1 쿼리 가능성 — 사용자 정보 조회 최적화 권장.

목록의 각 post마다 getUserEntity 호출 시 사용자 조회가 반복됩니다. userId를 모아 findAllById로 일괄 조회 후 Map 캐싱하여 매핑하는 방식으로 N+1을 제거하는 리팩터링을 권장합니다.


146-149: 예외 일관성: 사용자 미존재 시 NoticeException 사용 검토.

현재 IllegalArgumentException을 던집니다. 전역 예외 처리 및 에러 응답 포맷 일관성을 위해 NoticeException 계열로 매핑하는 것을 권장합니다.


159-164: create 경로에서의 validateUserAndPost는 부분 중복/불필요 검증.

생성 시 postUserId에 현재 사용자 ID를 그대로 전달하므로 SecurityUtils.validateUser(postUserId)는 항상 참입니다. 역할 검증과 소유자 검증을 분리하거나, 생성 경로에서는 역할만 확인하는 메서드로 단순화하는 것이 가독성에 유리합니다.

src/main/java/inu/codin/codin/domain/board/notice/controller/NoticeController.java (8)

48-49: 경로 설계: GET /notice/category 대신 쿼리 파라미터 기반 필터로 단순화 검토

  • 동일 리소스 컬렉션 조회는 통상 GET /notice?department=...&page=... 형태로 표현합니다. 라우팅을 단순화하면 API 표면적이 줄고 캐시 전략에도 유리합니다. 호환성 영향이 있다면 그대로 유지해도 무방합니다.

이 엔드포인트가 신규라면 경로 변경 고려 가치가 있습니다. 기존 클라이언트가 이미 /notice/category를 사용 중인지 확인 부탁드립니다.


49-53: 원시 타입에 @NotNull은 효과 없음 → @min(0) 등으로 검증 전환 권장

  • primitive int에는 null이 들어올 수 없어 @NotNull이 동작하지 않습니다. 음수 페이지를 방지하려면 @min(0) 또는 @PositiveOrZero를 권장합니다.

페이지 인덱스가 0 또는 1 기준인지 서비스/문서와 일치하는지 확인 바랍니다.

적용 제안(diff):

-                                                                            @RequestParam("page") @NotNull int pageNumber) {
+                                                                            @RequestParam("page") @Min(0) int pageNumber) {

추가 import:

import jakarta.validation.constraints.Min;

59-64: 단수 리소스 조회 메서드 네이밍 정교화 권장

  • 단수 리소스를 반환하므로 getNoticesWithDetail → getNoticeWithDetail이 더 명확합니다. (서비스 메서드명 변경은 별도 고려)

적용 제안(diff):

-    public ResponseEntity<SingleResponse<NoticeDetailResponseDto>> getNoticesWithDetail(@PathVariable String postId) {
+    public ResponseEntity<SingleResponse<NoticeDetailResponseDto>> getNoticeWithDetail(@PathVariable String postId) {

83-93: List.of()는 불변 리스트라 후속 로직에서 수정 시 예외 발생 가능 + 주석 변수명 오탈자

  • List.of()는 불변입니다. 서비스 계층에서 추가/변경 가능성이 조금이라도 있다면 변경 가능한 리스트로 초기화하세요.
  • 주석의 변수명이 postImages → noticeImages로 혼동되어 있습니다.

적용 제안(diff):

-        // postImages가 null이면 빈 리스트로 처리
-        if (noticeImages == null || noticeImages.isEmpty()) noticeImages = List.of();
+        // noticeImages가 null이거나 비어 있으면 빈 리스트로 처리
+        if (noticeImages == null || noticeImages.isEmpty()) noticeImages = new ArrayList<>();

추가 import:

import java.util.ArrayList;

비고:

  • 서비스에서 리스트를 수정하지 않는 것이 확실하다면 현 구현도 동작에는 문제 없습니다. 다만 방어적으로 변경 가능 리스트를 권장합니다.

85-93: 생성 API 응답의 제네릭 와일드카드(?) → 구체 타입 명시로 API 계약 강화

  • ResponseEntity<SingleResponse<?>>는 클라이언트/문서화 측면에서 불명확합니다. 생성된 게시물의 식별자나 상세 DTO 등 구체 타입을 명시해 주세요. 예: SingleResponse 또는 SingleResponse.

createNotice가 실제로 반환하는 타입(식별자/상세 DTO/없음)을 알려주시면 시그니처를 구체화하도록 제안 드리겠습니다.


102-111: 업데이트 엔드포인트: 메서드명/메시지 일관성 및 ok() 사용 제안

  • 도메인 일관성: updatePostContent → updateNoticeContent 권장.
  • 사용자 메시지: “게시물” → “공지사항”.
  • 200 OK는 ResponseEntity.ok()로 간결하게 표현 가능합니다.

적용 제안(diff):

-    public ResponseEntity<SingleResponse<?>>  updatePostContent(
+    public ResponseEntity<SingleResponse<?>> updateNoticeContent(
@@
-        return ResponseEntity.status(HttpStatus.OK)
-                .body(new SingleResponse<>(200, "게시물 내용이 수정되었습니다.", null));
+        return ResponseEntity.ok()
+                .body(new SingleResponse<>(200, "공지사항 내용이 수정되었습니다.", null));

107-111: 업데이트 API의 noticeImages도 변경 가능 리스트로 초기화

  • 생성 API와 동일한 이유로 List.of() 대신 변경 가능 리스트 사용을 권장합니다.

적용 제안(diff):

-        if (noticeImages == null || noticeImages.isEmpty()) noticeImages = List.of();
+        if (noticeImages == null || noticeImages.isEmpty()) noticeImages = new ArrayList<>();

참고: 이미 ArrayList import를 추가했다면 중복 추가는 불필요합니다.


118-121: imageUrl 입력값 검증(@notblank) 및 서버 측 안전성 확인

  • 빈 문자열/공백만 전달되는 것을 방지하려면 @notblank를 권장합니다.
  • 보안 관점: 전달된 imageUrl이 해당 postId에 실제로 속하고, 허용된 저장소/도메인만 대상으로 하는지 서비스 계층에서 검증되는지 확인해 주세요.

적용 제안(diff):

-    public ResponseEntity<SingleResponse<?>> deleteNoticeImage(
-            @PathVariable String postId,
-            @RequestParam String imageUrl) {
+    public ResponseEntity<SingleResponse<?>> deleteNoticeImage(
+            @PathVariable String postId,
+            @RequestParam @NotBlank String imageUrl) {

추가 import:

import jakarta.validation.constraints.NotBlank;

추가 제안(선택): REST 관점에서 imageUrl 대신 이미지 식별자(ID)를 경로 변수로 받는 설계(예: DELETE /{postId}/images/{imageId})가 추후 유지보수에 유리할 수 있습니다.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between b7ad6fe and e6b12a1.

📒 Files selected for processing (36)
  • src/main/java/inu/codin/codin/common/dto/Department.java (1 hunks)
  • src/main/java/inu/codin/codin/common/exception/GlobalExceptionHandler.java (3 hunks)
  • src/main/java/inu/codin/codin/domain/board/notice/controller/NoticeController.java (1 hunks)
  • src/main/java/inu/codin/codin/domain/board/notice/dto/request/NoticeCreateUpdateRequestDTO.java (1 hunks)
  • src/main/java/inu/codin/codin/domain/board/notice/dto/response/NoticeDetailResponseDto.java (1 hunks)
  • src/main/java/inu/codin/codin/domain/board/notice/dto/response/NoticeListResponseDto.java (1 hunks)
  • src/main/java/inu/codin/codin/domain/board/notice/dto/response/NoticePageResponse.java (1 hunks)
  • src/main/java/inu/codin/codin/domain/board/notice/exception/NoticeErrorCode.java (1 hunks)
  • src/main/java/inu/codin/codin/domain/board/notice/exception/NoticeException.java (1 hunks)
  • src/main/java/inu/codin/codin/domain/board/notice/repository/NoticeRepository.java (1 hunks)
  • src/main/java/inu/codin/codin/domain/board/notice/service/NoticeService.java (1 hunks)
  • src/main/java/inu/codin/codin/domain/board/question/controller/QuestionController.java (1 hunks)
  • src/main/java/inu/codin/codin/domain/board/question/dto/request/QuestionCreateUpdateRequestDto.java (1 hunks)
  • src/main/java/inu/codin/codin/domain/board/question/dto/response/QuestionResponseDto.java (1 hunks)
  • src/main/java/inu/codin/codin/domain/board/question/entity/QuestionEntity.java (1 hunks)
  • src/main/java/inu/codin/codin/domain/board/question/exception/QuestionErrorCode.java (1 hunks)
  • src/main/java/inu/codin/codin/domain/board/question/exception/QuestionException.java (1 hunks)
  • src/main/java/inu/codin/codin/domain/board/question/repository/QuestionRepository.java (1 hunks)
  • src/main/java/inu/codin/codin/domain/board/question/service/QuestionService.java (1 hunks)
  • src/main/java/inu/codin/codin/domain/board/voice/controller/VoiceController.java (1 hunks)
  • src/main/java/inu/codin/codin/domain/board/voice/dto/VoiceBoxAnswerRequest.java (1 hunks)
  • src/main/java/inu/codin/codin/domain/board/voice/dto/VoiceBoxCreateRequest.java (1 hunks)
  • src/main/java/inu/codin/codin/domain/board/voice/dto/VoiceBoxDetailResponse.java (1 hunks)
  • src/main/java/inu/codin/codin/domain/board/voice/dto/VoiceBoxPageResponse.java (1 hunks)
  • src/main/java/inu/codin/codin/domain/board/voice/entity/VoiceEntity.java (1 hunks)
  • src/main/java/inu/codin/codin/domain/board/voice/repository/VoiceRepository.java (1 hunks)
  • src/main/java/inu/codin/codin/domain/board/voice/service/VoiceService.java (1 hunks)
  • src/main/java/inu/codin/codin/domain/info/controller/LabController.java (3 hunks)
  • src/main/java/inu/codin/codin/domain/info/controller/OfficeController.java (4 hunks)
  • src/main/java/inu/codin/codin/domain/info/controller/PartnerController.java (3 hunks)
  • src/main/java/inu/codin/codin/domain/info/controller/ProfessorController.java (3 hunks)
  • src/main/java/inu/codin/codin/domain/lecture/controller/LectureUploadController.java (2 hunks)
  • src/main/java/inu/codin/codin/domain/notification/controller/NotificationController.java (2 hunks)
  • src/main/java/inu/codin/codin/domain/notification/dto/response/NotificationListResponseDto.java (2 hunks)
  • src/main/java/inu/codin/codin/domain/post/entity/PostCategory.java (1 hunks)
  • src/main/java/inu/codin/codin/domain/post/entity/PostEntity.java (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (13)
src/main/java/inu/codin/codin/domain/board/voice/service/VoiceService.java (2)
src/main/java/inu/codin/codin/common/security/util/SecurityUtils.java (1)
  • SecurityUtils (14-49)
src/main/java/inu/codin/codin/common/util/ObjectIdUtil.java (1)
  • ObjectIdUtil (5-39)
src/main/java/inu/codin/codin/domain/board/question/controller/QuestionController.java (3)
src/main/java/inu/codin/codin/domain/board/notice/controller/NoticeController.java (1)
  • RestController (24-137)
src/main/java/inu/codin/codin/domain/info/controller/LabController.java (1)
  • RestController (18-67)
src/main/java/inu/codin/codin/domain/info/controller/OfficeController.java (1)
  • RestController (18-70)
src/main/java/inu/codin/codin/domain/board/question/dto/response/QuestionResponseDto.java (1)
src/main/java/inu/codin/codin/domain/board/question/entity/QuestionEntity.java (1)
  • Getter (14-40)
src/main/java/inu/codin/codin/domain/board/notice/dto/response/NoticeDetailResponseDto.java (2)
src/main/java/inu/codin/codin/domain/board/notice/dto/response/NoticeListResponseDto.java (1)
  • Getter (14-70)
src/main/java/inu/codin/codin/domain/board/notice/dto/response/NoticePageResponse.java (1)
  • Getter (10-32)
src/main/java/inu/codin/codin/domain/board/voice/dto/VoiceBoxDetailResponse.java (1)
src/main/java/inu/codin/codin/common/security/util/SecurityUtils.java (1)
  • SecurityUtils (14-49)
src/main/java/inu/codin/codin/domain/board/question/dto/request/QuestionCreateUpdateRequestDto.java (2)
src/main/java/inu/codin/codin/domain/board/question/entity/QuestionEntity.java (1)
  • Getter (14-40)
src/main/java/inu/codin/codin/domain/board/question/dto/response/QuestionResponseDto.java (1)
  • Getter (7-22)
src/main/java/inu/codin/codin/domain/board/question/service/QuestionService.java (1)
src/main/java/inu/codin/codin/domain/board/notice/service/NoticeService.java (1)
  • Service (35-165)
src/main/java/inu/codin/codin/domain/board/question/entity/QuestionEntity.java (2)
src/main/java/inu/codin/codin/domain/board/question/dto/request/QuestionCreateUpdateRequestDto.java (1)
  • Getter (9-23)
src/main/java/inu/codin/codin/domain/board/question/dto/response/QuestionResponseDto.java (1)
  • Getter (7-22)
src/main/java/inu/codin/codin/domain/board/notice/service/NoticeService.java (2)
src/main/java/inu/codin/codin/common/security/util/SecurityUtils.java (1)
  • SecurityUtils (14-49)
src/main/java/inu/codin/codin/domain/board/question/service/QuestionService.java (1)
  • Service (16-60)
src/main/java/inu/codin/codin/domain/board/notice/dto/response/NoticePageResponse.java (1)
src/main/java/inu/codin/codin/domain/board/notice/dto/response/NoticeListResponseDto.java (1)
  • Getter (14-70)
src/main/java/inu/codin/codin/domain/board/notice/exception/NoticeErrorCode.java (1)
src/main/java/inu/codin/codin/domain/board/notice/exception/NoticeException.java (1)
  • Getter (6-14)
src/main/java/inu/codin/codin/domain/board/notice/controller/NoticeController.java (2)
src/main/java/inu/codin/codin/domain/board/question/controller/QuestionController.java (1)
  • RestController (19-84)
src/main/java/inu/codin/codin/domain/info/controller/PartnerController.java (1)
  • RestController (22-75)
src/main/java/inu/codin/codin/domain/board/notice/dto/response/NoticeListResponseDto.java (2)
src/main/java/inu/codin/codin/domain/board/notice/dto/response/NoticeDetailResponseDto.java (2)
  • Getter (15-111)
  • Getter (81-91)
src/main/java/inu/codin/codin/domain/board/notice/dto/response/NoticePageResponse.java (1)
  • Getter (10-32)
🔇 Additional comments (35)
src/main/java/inu/codin/codin/domain/notification/controller/NotificationController.java (1)

7-7: OpenAPI 태그 추가 적절 — 문서화 품질 향상

클래스 레벨의 @tag 추가와 관련 import가 적절합니다. 런타임 동작에 영향 없이 스웨거 문서 그룹화에 도움이 됩니다.

Also applies to: 18-18

src/main/java/inu/codin/codin/domain/notification/dto/response/NotificationListResponseDto.java (1)

3-3: 필요한 JsonFormat import 추가 OK

직렬화 포맷 제어를 위한 JsonFormat import 추가는 타당합니다.

src/main/java/inu/codin/codin/domain/board/voice/dto/VoiceBoxAnswerRequest.java (1)

1-1: 패키지 이동만: 기능 영향 없음 (LGTM)

네임스페이스 정리만 이뤄졌고, DTO의 검증 애너테이션 구성도 적절합니다.

src/main/java/inu/codin/codin/domain/board/voice/dto/VoiceBoxPageResponse.java (2)

15-16: nextPage 의미 확인 요청

nextPage가 “다음 페이지 번호”인지 “존재 여부(hasNext)”인지 API 계약을 재확인해주세요. 클라이언트가 hasNext를 기대한다면 불리언 필드 추가/변경이 필요합니다.


1-1: 레거시 voice 패키지 import 제거 확인 완료
전체 Java 파일에서 inu.codin.codin.domain.post(.domain)?\.voice 경로를 사용하는 import가 발견되지 않았습니다. 검토를 마칩니다.

src/main/java/inu/codin/codin/domain/board/voice/controller/VoiceController.java (2)

28-36: 생성 API의 상태 코드/메시지 일관성 (LGTM)

201 Created로 응답하는 점, 메시지와 응답 래퍼 사용이 일관적입니다.


49-58: 글로벌 보안 설정 확인: 모든 요청에 인증 요구됨

– src/main/java/inu/codin/codin/common/config/SecurityConfig.java (라인 81–88) 확인 시
.authorizeHttpRequests(...)…anyRequest().hasRole("USER") 로 모든 엔드포인트를 인증(ROLE_USER) 대상에 포함하고 있습니다.
– 따라서 /voice-box/vote/{boxId}(toggleVoiceBox) API는 별도의 @PreAuthorize 없이도 인증이 적용되어 있으며, 추가 애노테이션은 불필요합니다.

src/main/java/inu/codin/codin/domain/board/voice/dto/VoiceBoxCreateRequest.java (1)

1-1: 패키지 이동만: 기능 영향 없음 (LGTM)

src/main/java/inu/codin/codin/domain/board/voice/repository/VoiceRepository.java (1)

1-5: 패키지/임포트 경로 리팩터링 일관성 확인 (기능 영향 없음)

voice 도메인의 board 네임스페이스로의 이동이 잘 반영되었습니다. 공개 API/시그니처 변화는 없습니다.

src/main/java/inu/codin/codin/domain/board/voice/dto/VoiceBoxDetailResponse.java (1)

1-7: 패키지/임포트 경로 업데이트 적정

board.voice 네임스페이스로의 이동이 DTO에도 일관되게 반영되었습니다.

src/main/java/inu/codin/codin/domain/board/voice/service/VoiceService.java (2)

6-10: 패키지/임포트 경로 리팩터링 OK

board.voice.* 경로로의 전환이 Service에 정상 반영되었습니다.


72-99: 권한 검증 위치 확인 요청: 답변 등록/삭제는 관리자 권한 필요 여부

addAnswer, deleteVoiceBox가 서비스 레벨에서 권한 검증을 하지 않습니다. 컨트롤러/필터에서 이미 보장한다면 OK, 아니라면 최소 Role 체크를 추가해 주세요.

예시(개념 코드):

var role = SecurityUtils.getCurrentUserRole();
if (!role.isAdminLike()) { // 실제 Role Enum에 맞춰 적용
    throw new JwtException(SecurityErrorCode.ACCESS_DENIED, "권한이 없습니다.");
}

필요 시 Role Enum에 맞춘 구체 구현을 제안드리겠습니다.

src/main/java/inu/codin/codin/domain/info/controller/ProfessorController.java (3)

49-56: 접두사 제거 변경 LGTM

보안 설정이 기본 ROLE_ 접두사를 사용하는 경우 해당 변경은 권한 검사 일관성을 개선합니다.


58-65: 접두사 제거 변경 LGTM

삭제 엔드포인트의 권한 표현도 일관되게 정리되었습니다.


40-47: ROLE 접두사 일관성 확인 완료
CustomUserDetails에서 SimpleGrantedAuthority("ROLE_"+…)로 권한을 생성하고, SecurityConfig 및 모든 컨트롤러의 @PreAuthorize(hasRole/hasAnyRole) 호출이 접두사 없는 권한 이름(ADMIN, MANAGER)을 사용하여 Spring Security 기본 “ROLE_” 접두사 규칙을 따르고 있음을 확인했습니다. 변경된 hasAnyRole('MANAGER', 'ADMIN') 설정은 올바르며 추가 조치가 필요 없습니다.

src/main/java/inu/codin/codin/domain/post/entity/PostCategory.java (1)

24-25: PostCategory ordinal 의존성 검증 완료 (백엔드)
rg 스크립트 실행 결과, Java 백엔드에서는 ordinal 기반 의존성이 전혀 발견되지 않았습니다.

• PostCategory.ordinal() 호출 위치: 없음
• PostCategory.values()[index] 사용 위치: 없음

따라서 Java 쪽 런타임 동작에는 영향이 없습니다.
프론트엔드가 monorepo에 함께 포함되어 있다면 동일 스크립트로 ordinal/인덱스 의존 여부를 한 번 더 검토해주세요.

src/main/java/inu/codin/codin/domain/info/controller/OfficeController.java (4)

33-40: 역할 접두사 제거 변경 맞음 — 전역 ROLE_ 접두사 전략과의 정합성만 확인

hasAnyRole('MANAGER','ADMIN') 사용은 기본 접두사 "ROLE_" 전제로 타당합니다. SecurityConfig에서 Role 접두사를 커스터마이징하지 않았는지와 GrantedAuthority 부여 시 "ROLE_"를 사용 중인지 점검해 주세요.

전역 점검 스크립트는 ProfessorController 코멘트에 첨부한 것을 재사용해 주세요.


42-50: 접두사 제거 변경 LGTM

생성 API의 권한 표현이 일관되게 정리되었습니다.


52-60: 접두사 제거 변경 LGTM

직원 수정 API 권한 표현 정리 확인했습니다.


62-69: 접두사 제거 변경 LGTM

직원 삭제 API 권한 표현 정리 확인했습니다.

src/main/java/inu/codin/codin/domain/lecture/controller/LectureUploadController.java (2)

31-37: 권한 표현 변경 타당 — ROLE_ 접두사 일관성만 재확인 요청

hasAnyRole('MANAGER','ADMIN') 변경은 기본 접두사 "ROLE_" 전제로 적절합니다. GrantedAuthority가 "ROLE_MANAGER"/"ROLE_ADMIN"로 부여되는지 SecurityConfig/인증 로직에서 일관성 확인 바랍니다.

전역 점검 스크립트는 ProfessorController 코멘트에 첨부한 것을 재사용해 주세요.


44-50: 접두사 제거 변경 LGTM

강의실 현황 업로드 엔드포인트 권한 표현 정리 확인했습니다.

src/main/java/inu/codin/codin/domain/info/controller/LabController.java (1)

40-40: SecurityConfig에서 기본 ROLE_ 접두어 사용 확인 — @PreAuthorize 변경은 올바릅니다

SecurityConfig(src/main/java/inu/codin/codin/common/config/SecurityConfig.java)에서 GrantedAuthorityDefaultssetRolePrefix 커스터마이징이 없습니다.
• 어노테이션(@PreAuthorize("hasAnyRole('MANAGER','ADMIN')"))에 ROLE_을 직접 쓰는 패턴이 없으며, 기본 접두어 적용 시 ROLE_MANAGER, ROLE_ADMIN 권한을 요구하도록 일치합니다.
• 프로젝트 전반의 hasRole, hasAnyRole 사용도 동일 포맷으로 일관되게 적용되어 있습니다.

위 확인 결과, 해당 변경사항에 따른 권한 요구 로직은 문제 없습니다.

src/main/java/inu/codin/codin/domain/board/question/exception/QuestionErrorCode.java (1)

7-24: 에러 코드 설계 및 구현 적절합니다.

GlobalErrorCode 계약에 맞춘 httpStatus()/message() 구현과 @requiredargsconstructor 사용이 깔끔합니다. 도메인 메시지도 명확합니다.

src/main/java/inu/codin/codin/domain/board/question/entity/QuestionEntity.java (1)

14-17: MongoDB 매핑/보일러플레이트 구성 적절합니다

@document, @id(ObjectId), @NoArgsConstructor(PROTECTED) 조합이 표준에 맞고, BaseTimeEntity 상속도 일관적입니다. 빌더 생성자도 적절합니다.

src/main/java/inu/codin/codin/domain/board/notice/dto/response/NoticeListResponseDto.java (1)

42-44: createdAt 직렬화 포맷 지정 적절 (Seoul TZ, 패턴 일치). LGTM

src/main/java/inu/codin/codin/domain/board/question/controller/QuestionController.java (1)

33-41: 조회 엔드포인트 흐름 명확. LGTM

파라미터 검증은 Service 단 validateDepartment에서 처리되어 Controller가 단순하고 좋습니다.

src/main/java/inu/codin/codin/domain/board/notice/dto/response/NoticeDetailResponseDto.java (2)

57-59: createdAt 직렬화 포맷 지정 적절 (Seoul TZ, 패턴 일치). LGTM


93-108: ID 직렬화 형식 표준화 권장.

ObjectId#toString() 반환 형식은 드라이버 버전에 따라 가독성 문자열일 가능성이 있습니다. API 레벨에서 24자리 hex로 고정하려면 toHexString() 사용이 안전합니다. 프로젝트 전반의 ID 직렬화 컨벤션 확인/정합성 검토 바랍니다.

src/main/java/inu/codin/codin/domain/board/question/service/QuestionService.java (1)

22-26: 조회 플로우 간결하고 명확. LGTM

stream + DTO 매핑 간결하며 서비스 레이어 책임에 부합합니다.

src/main/java/inu/codin/codin/domain/board/notice/service/NoticeService.java (3)

60-63: 카테고리/타이틀 prefix 기반 필터링 및 페이징 구성 깔끔. LGTM

정규식 앞부분 anchoring(^)과 Pattern.quote 사용으로 안전하게 prefix 매칭됩니다.


62-63: 빈 결과 시 lastPage = -1 가능성 확인 필요.

getTotalPages() == 0이면 lastPage = totalPages - 1로 -1이 됩니다. 클라이언트가 lastPage >= 0을 기대하는지 확인하고 필요 시 0으로 보정하는 로직을 고려해 주세요.


151-157: 비인증 사용자 접근 시 동작 정의 필요 (상세조회).

SecurityUtils.getCurrentUserId() 호출은 인증이 없으면 예외를 던집니다. 상세조회가 공개 API라면(Controller에 권한 제한 없음), 비로그인 사용자의 경우 isScrap=false, isMine=false로 처리하도록 예외를 흡수하는 방식을 검토해 주세요.

src/main/java/inu/codin/codin/domain/board/notice/controller/NoticeController.java (2)

24-32: 전반 구조/보안/문서화 구성이 일관적입니다


131-136: 소프트 삭제 엔드포인트 처리 흐름 적절

  • 권한 검사, 서비스 위임, 일관된 응답 포맷이 잘 맞춰져 있습니다.

@X1n9fU X1n9fU merged commit 634f670 into main Aug 20, 2025
1 check passed
@coderabbitai coderabbitai bot mentioned this pull request Oct 7, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ Feature 기능 개발 Priority : High 우선순위 상 Status: Reviewing 처리하고 리뷰 중인 이슈

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants