Skip to content

[chore] 패키지 구조 변경 (RoomPost 도입)#229

Merged
seongjunnoh merged 7 commits into
developfrom
chore/#227-modifying-package
Aug 16, 2025
Merged

[chore] 패키지 구조 변경 (RoomPost 도입)#229
seongjunnoh merged 7 commits into
developfrom
chore/#227-modifying-package

Conversation

@buzz0331
Copy link
Copy Markdown
Contributor

@buzz0331 buzz0331 commented Aug 15, 2025

#️⃣ 연관된 이슈

closes #227

📝 작업 내용

패키지 구조를 다음과 같이 변경하였습니다.

roompost

  • attendancecheck, record, vote 도메인을 모두 포함하는 상위 애그리거트
  • Controller도 RoomPostController로 통합
  • 패키지 내부에서 각 도메인에 관련된 클래스들은 도메인 이름으로 시작 (ex. VoteCreateService)
  • 방 전체 게시글에 대한 책임을 갖는 클래스는 RoomPost로 시작 (ex. RoomPostSearchService)

post

  • common 패키지에 들어있던 post 관련 로직 통합
  • 내부적으로 post에 대한 공통 로직이 들어있는 헬퍼 서비스 (ex. PostHandler, PostLikeAuthorizationValidator 등등) 포함
  • 게시글 좋아요 도메인인 PostLike도 포함
  • 이때 PostLikeAccessPolicy 인터페이스를 구현하는 FeedLikeAccessPolicy와 RoomPostLikeAccessPolicy는 각각의 도메인에 구현체를 두는게 더 맞다고 생각하여 패키지를 이동하였습니다.

추가적으로 저희가 정한 테스트 컨벤션에 맞게 모든 테스트 코드들을 한번 정리했습니다.

📸 스크린샷

스크린샷 2025-08-15 오후 4 47 31

💬 리뷰 요구사항

HelperService와 DomainService도 구분하기 위해 DomainService를 나타내는 어노테이션하나 추가했습니다. (ex. PostCountService)

추가적으로 변경되었으면 하는 부분 있다면 리뷰로 요청주세요!!

📌 PR 진행 시 이러한 점들을 참고해 주세요

* P1 : 꼭 반영해 주세요 (Request Changes) - 이슈가 발생하거나 취약점이 발견되는 케이스 등
* P2 : 반영을 적극적으로 고려해 주시면 좋을 것 같아요 (Comment)
* P3 : 이런 방법도 있을 것 같아요~ 등의 사소한 의견입니다 (Chore)

Summary by CodeRabbit

  • 신기능

    • 기록/투표/오늘의한마디 관련 엔드포인트를 통합한 새 RoomPost API(생성·삭제·상태변경) 추가
  • 변경/리팩터

    • 게시글 조회 응답이 RoomPostSearchResponse/RoomPostDto로 개편
    • 방 참여 요청이 문자열에서 enum 기반 RoomJoinType으로 변경
    • 내부 모듈 재조직으로 record/vote 기능이 roompost로 통합·재배치
  • 문서/테스트

    • API 테스트 클래스명/표시명 일괄 정리(…ApiTest로 통일)

@buzz0331 buzz0331 self-assigned this Aug 15, 2025
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Aug 15, 2025

Walkthrough

프로젝트 전반에 roompost 모듈 도입을 중심으로 record/vote/room 관련 타입·포트·컨트롤러·리포지토리의 이동·통합, PostType/CountUpdatable 패키지 재배치, RoomJoinType의 enum 전환 및 테스트/매퍼/어댑터 재배치를 수행했습니다.

Changes

Cohort / File(s) Summary
모듈/패키지 리팩터링 (roompost 도입)
src/main/java/konkuk/thip/roompost/..., src/main/java/konkuk/thip/record/... (삭제/이관), src/main/java/konkuk/thip/vote/... (삭제/이관)
record/vote 관련 컨트롤러·포트·DTO·리포지토리·도메인을 roompost 네임스페이스로 이동하거나 대체(다수 파일 생성/삭제/이관).
포스트 공통 타입 재배치
src/main/java/konkuk/thip/post/domain/PostType.java, src/main/java/konkuk/thip/post/domain/CountUpdatable.java
PostType/CountUpdatable 패키지 이동, PostType에 roomPostTypeFrom 추가. 참조 경로 전역 갱신.
컨트롤러 추가/이동
src/main/java/konkuk/thip/roompost/adapter/in/web/RoomPostCommandController.java, .../RoomPostQueryController.java
roompost용 명령/조회 컨트롤러 추가(레코드/투표/출석 포함); 기존 vote/record 컨트롤러 삭제/이관.
퍼시스턴스·리포지토리·쿼리 변경
src/main/java/konkuk/thip/roompost/adapter/out/persistence/*, .../repository/...
JPA 엔티티·매퍼·퍼시스턴스 어댑터·쿼리 리포지토리(RecordQueryRepository/impl 등) 생성 및 RoomPostSortType 추가; 기존 record/vote 쿼리 삭제/대체.
애플리케이션 포트·DTO 이동/신설
src/main/java/konkuk/thip/roompost/application/port/in/*, .../port/out/*, .../port/in/dto/**
in/out 포트와 명령/검색 DTO를 roompost로 신설·이관; 기존 record/vote 포트 파일 삭제.
서비스/검증자/매퍼 리팩터링
src/main/java/konkuk/thip/roompost/application/service/*, .../validator/*, .../mapper/*
서비스·검증자·매퍼들을 roompost 네임스페이스로 이동·명칭 변경(예: RecordSearchService → RoomPostSearchService), 로직 연결을 roompost DTO/포트로 전환.
RoomJoinType 변경
src/main/java/konkuk/thip/room/adapter/in/web/request/RoomJoinRequest.java, .../RoomJoinCommand.java, .../RoomJoinService.java
RoomJoinCommand의 type을 String→RoomJoinType(enum)으로 변경 및 서비스 분기 switch로 정리.
공통 애노테이션/DI 변경
src/main/java/konkuk/thip/common/annotation/DomainService.java, .../PostCountService.java, .../PostLikeAuthorizationValidator.java
@domainservice 추가 및 일부 컴포넌트 어노테이션(@HelperService 등) 적용으로 DI 어노테이션 조정.
Comment 엔티티 소소 변경
src/main/java/konkuk/thip/comment/adapter/out/jpa/CommentJpaEntity.java
PostType import 경로 변경 및 Lombok @Builder.Default 추가(컬렉션 기본값 빌더 동작 보장).
테스트 일괄 정리
src/test/java/**
테스트 패키지·임포트·클래스명·DisplayName을 roompost 구조와 일치하도록 대규모 갱신.

Sequence Diagram(s)

sequenceDiagram
  participant C as Client
  participant W as RoomPostCommandController
  participant UC as UseCase (Record/Vote/Attendance)
  participant P as CommandPort
  participant R as Repository

  C->>W: POST /rooms/{roomId}/record|vote|daily-greeting
  W->>UC: toCommand(...), create|vote|delete(...)
  UC->>P: save/delete/execute(...)
  P->>R: JPA ops
  R-->>P: entity id/result
  P-->>UC: id/result
  UC-->>W: Result DTO
  W-->>C: BaseResponse<...>
Loading
sequenceDiagram
  participant C as Client
  participant Q as RoomPostQueryController
  participant S as RoomPostSearchUseCase
  participant RP as RecordQueryPort
  participant RQ as RecordQueryRepository

  C->>Q: GET /rooms/{roomId}/posts
  Q->>S: search(RoomPostSearchQuery)
  S->>RP: search* (cursor, sort/type)
  RP->>RQ: find* (RoomPostSortType)
  RQ-->>RP: List<RoomPostQueryDto> (+nextCursor)
  RP-->>S: CursorBasedList<RoomPostQueryDto>
  S-->>Q: RoomPostSearchResponse
  Q-->>C: BaseResponse<RoomPostSearchResponse>
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~90 minutes

Assessment against linked issues

Objective Addressed Explanation
roompost 모듈 도입 및 패키지 전면 재구성 (#227)
record/vote/attendance 관련 API·포트·리포지토리의 roompost 이관 및 기존 제거 (#227)
PostType/CountUpdatable 등 공통 타입의 패키지 변경 및 전역 참조 갱신 (#227)
테스트 코드 패키지/임포트 정리 및 신규 구조 반영 (#227)

Possibly related PRs

Suggested labels

🍀 refactor

Suggested reviewers

  • buzz0331

Poem

토끼가 껑충, 패키지 숲을 건너가요 🐇
record와 vote 모아 방 하나 만들고, roompost로 살포시 옮겼죠.
import 줄이 정리되고 enum은 깔끔히 자리잡아,
커서는 다음 페이지로 살금살금 이어지고,
나는 당근 들고 빌드 통과를 기다려요 🥕✨

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 chore/#227-modifying-package

🪧 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.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Aug 15, 2025

Test Results

399 tests   399 ✅  29s ⏱️
119 suites    0 💤
119 files      0 ❌

Results for commit e29f42b.

♻️ This comment has been updated with latest results.

Copy link
Copy Markdown

@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: 11

🔭 Outside diff range comments (15)
src/main/java/konkuk/thip/roompost/domain/VoteParticipant.java (1)

27-34: NPE 및 잘못된 상태 유입 가능성: null/동일값 체크 로직 보강 필요

this.voteItemId가 null이면 equals에서 NPE가 발생할 수 있고, 인자로 null이 들어오면 null 상태가 허용되어 도메인 불변식이 깨집니다. null 안전성과 동일 투표 방지 로직을 함께 강화해주세요.

다음 패치를 제안합니다(Objects를 FQN으로 사용해 import 불필요):

     public void changeVoteItem(Long voteItemId) {
-        // 같은 항목을 투표하려고 하는 경우 예외처리
-        if(this.voteItemId.equals(voteItemId)) {
+        final Long newVoteItemId = java.util.Objects.requireNonNull(voteItemId, "voteItemId must not be null");
+        // 같은 항목을 투표하려고 하는 경우 예외처리
+        if (java.util.Objects.equals(this.voteItemId, newVoteItemId)) {
             throw new InvalidStateException(ErrorCode.VOTE_ITEM_ALREADY_VOTED);
         }
         // 투표 항목 변경
-        this.voteItemId = voteItemId;
+        this.voteItemId = newVoteItemId;
     }
src/main/java/konkuk/thip/post/application/service/PostLikeService.java (1)

37-47: 포트 시그니처에 PostType 파라미터 추가 필요
확인 결과,

  • PostLikeQueryPort.isLikedPostByUser(Long userId, Long postId)
  • PostLikeCommandPort.delete(Long userId, Long postId)
    두 메서드는 PostType을 고려하지 않으나, save(Long userId, Long postId, PostType postType)만 PostType을 필요로 합니다.
    서로 다른 도메인(FEED, RECORD, VOTE 등) 간에 같은 postId가 존재할 수 있어, 좋아요 조회·삭제 시 교차 충돌 위험이 있습니다.

수정 대상

  • src/main/java/konkuk/thip/post/application/port/out/PostLikeQueryPort.java (메서드 정의)
  • src/main/java/konkuk/thip/post/application/port/out/PostLikeCommandPort.java (delete 정의)
  • src/main/java/konkuk/thip/post/application/service/PostLikeService.java (호출부)
  • persistence 어댑터 및 테스트 전반

예시 변경(diff 축약)

- boolean alreadyLiked = postLikeQueryPort.isLikedPostByUser(command.userId(), command.postId());
+ boolean alreadyLiked = postLikeQueryPort.isLikedPostByUser(command.userId(), command.postId(), command.postType());

...

- postLikeCommandPort.delete(command.userId(), command.postId());
+ postLikeCommandPort.delete(command.userId(), command.postId(), command.postType());

인터페이스 업데이트

public interface PostLikeQueryPort {
    boolean isLikedPostByUser(Long userId, Long postId, PostType postType);
}
public interface PostLikeCommandPort {
    void save(Long userId, Long postId, PostType postType);
    void delete(Long userId, Long postId, PostType postType);
}

어댑터·리포지토리 레이어와 테스트 코드도 동일하게 반영해 주세요.

src/main/java/konkuk/thip/comment/application/service/CommentDeleteService.java (1)

42-49: 댓글 수 감소의 동시성 이슈: 원자적 업데이트 필요

동일 게시물에서 다중 삭제 요청 시 단순 decreaseCommentCount()updatePost() 패턴은 lost update 위험이 높습니다(낙관적 락 미적용 시 마지막 쓰기 승리). 리드 없이 DB에서 원자적 감소 쿼리 혹은 버전 컬럼 기반 낙관적 락을 권장합니다.

다음과 같이 리드-모디파이-라이트를 제거하고 핸들러 단에서 원자 감소 메서드를 제공하는 방향을 제안합니다.

-        // 4. 게시글 댓글 수 감소
-        // 4-1. 도메인 게시물 댓글 수 감소
-        post.decreaseCommentCount();
-        // 4-2 Jpa엔티티 게시물 댓글 수 감소
-        postHandler.updatePost(comment.getPostType(), post);
+        // 4. 게시글 댓글 수 감소 (원자적 감소)
+        postHandler.decreaseCommentCount(comment.getPostType(), comment.getTargetPostId());

핸들러 내(new) 메서드 예시(파일 외 참고용):

// PostHandler.java
public void decreaseCommentCount(PostType postType, Long postId) {
    // 예: UPDATE ... SET comment_count = comment_count - 1 WHERE id = :id
    // 또는 버전 컬럼 기반 업데이트: WHERE id = :id AND version = :version
    // JPA: repository.decrementCommentCount(postId); // @Modifying 쿼리
}

참고로 동시성 요구가 더 크다면:

  • 낙관적 락(버전 필드) + 재시도
  • DB 제약(GREATEST(comment_count-1, 0))으로 음수 방지
  • 이벤트 기반 비동기 카운터 집계(읽기 모델 분리)도 고려 가능합니다.
src/main/java/konkuk/thip/room/application/service/RoomShowPlayingDetailViewService.java (1)

38-38: Book 조회 null 안전성 강화: getByIdOrThrow 적용 또는 null 처리 필요
bookCommandPort.findById(room.getBookId())가 null을 반환하면 NPE가 발생할 수 있습니다.

현재 BookCommandPortgetByIdOrThrow 시그니처가 존재하지 않으므로, 아래 중 하나를 선택해주세요:

  • BookCommandPort에 Book getByIdOrThrow(Long id) 메서드를 추가하고 교체
    - Book book = bookCommandPort.findById(room.getBookId());
    + Book book = bookCommandPort.getByIdOrThrow(room.getBookId());
  • 또는 findById 반환값을 Optional로 처리하거나 Objects.requireNonNull(...) 등으로 null을 방지

영향 위치:

  • src/main/java/konkuk/thip/room/application/service/RoomShowPlayingDetailViewService.java 38행
src/main/java/konkuk/thip/post/application/service/validator/PostLikeAuthorizationValidator.java (1)

30-34: 메서드명 및 호출부 변경 필요: validateUserCanUnLikevalidateUserCanUnlike

아래 두 곳에서 메서드명과 호출부를 일관되게 변경해야 합니다.

  • src/main/java/konkuk/thip/post/application/service/validator/PostLikeAuthorizationValidator.java (30행)
  • src/main/java/konkuk/thip/post/application/service/PostLikeService.java (45행)
--- a/src/main/java/konkuk/thip/post/application/service/validator/PostLikeAuthorizationValidator.java
@@ -30,7 +30,7 @@
-    public void validateUserCanUnLike(boolean alreadyLiked) {
+    public void validateUserCanUnlike(boolean alreadyLiked) {
         if (!alreadyLiked) {
             throw new InvalidStateException(POST_NOT_LIKED_CANNOT_CANCEL);
         }
--- a/src/main/java/konkuk/thip/post/application/service/PostLikeService.java
@@ -45,7 +45,7 @@
-            postLikeAuthorizationValidator.validateUserCanUnLike(alreadyLiked); // 좋아요 취소 가능 여부 검증
+            postLikeAuthorizationValidator.validateUserCanUnlike(alreadyLiked); // 좋아요 취소 가능 여부 검증
src/main/java/konkuk/thip/roompost/adapter/in/web/request/VoteCreateRequest.java (1)

42-45: 컴파일 오류: 레코드 컴포넌트 접근자 호출 누락

Java record의 컴포넌트는 접근자 메서드로 호출해야 합니다. 현재 voteItem.itemName는 컴파일되지 않습니다. voteItem.itemName()로 수정하세요.

수정 diff:

-                List<VoteCreateCommand.VoteItemCreateCommand> mappedItems = voteItemList.stream()
-                        .map(voteItem -> new VoteCreateCommand.VoteItemCreateCommand(voteItem.itemName))
-                        .toList();
+                List<VoteCreateCommand.VoteItemCreateCommand> mappedItems = voteItemList.stream()
+                        .map(voteItem -> new VoteCreateCommand.VoteItemCreateCommand(voteItem.itemName()))
+                        .toList();
src/main/java/konkuk/thip/roompost/domain/Vote.java (3)

47-56: totalPageCount가 0일 때의 분모 0/잘못된 허용 케이스 방지 필요

totalPageCount가 0이면 ratio가 Infinity/NaN 이 되어 isOverview 검증이 비정상적으로 통과할 수 있습니다. 사전에 유효 범위를 검증해 주세요.

아래처럼 가드를 추가하는 것을 제안합니다:

-    public void validateOverview(int totalPageCount) {
-        double ratio = (double) page / totalPageCount;
+    public void validateOverview(int totalPageCount) {
+        if (totalPageCount <= 0) {
+            throw new InvalidStateException(INVALID_VOTE_PAGE_RANGE,
+                new IllegalArgumentException("책 전체 page 는 1 이상이어야 합니다."));
+        }
+        double ratio = (double) page / totalPageCount;
         if (isOverview && ratio < 0.8) {
             String message = String.format(
                     "총평(isOverview)은 진행률이 80%% 이상일 때만 가능합니다. 현재 진행률 = %.2f%% (%d/%d)",
                     ratio * 100, page, totalPageCount
             );
             throw new InvalidStateException(VOTE_CANNOT_BE_OVERVIEW, new IllegalStateException(message));
         }
     }

92-96: NPE 위험: equals 호출 대상이 null일 수 있습니다

creatorId가 null일 경우 this.creatorId.equals(userId)에서 NPE가 발생합니다. Objects.equals로 안전 비교하거나, 생성 시점에 null을 허용하지 않도록 보장하세요.

-        if (!this.creatorId.equals(userId)) {
+        if (!java.util.Objects.equals(this.creatorId, userId)) {
             throw new InvalidStateException(VOTE_ACCESS_FORBIDDEN, new IllegalArgumentException("투표 작성자만 투표를 수정/삭제할 수 있습니다."));
         }

103-107: NPE 위험: RoomId 비교도 안전 비교로 교체 권장

roomId가 null이면 this.roomId.equals(roomId)가 NPE를 유발할 수 있습니다.

-        if (!this.roomId.equals(roomId)) {
+        if (!java.util.Objects.equals(this.roomId, roomId)) {
             throw new InvalidStateException(VOTE_ACCESS_FORBIDDEN, new IllegalArgumentException("투표가 해당 방에 속하지 않습니다."));
         }
src/main/java/konkuk/thip/roompost/adapter/in/web/request/AttendanceCheckCreateRequest.java (1)

12-14: 엔티티 생성 전 content 트리밍 적용 제안

AttendanceCheckCreateRequest#toCommand에서 사용자 입력의 선/후행 공백을 제거하여 도메인으로 전달하도록 변경을 권장드립니다.
Controller 계층에서는 이미 @RequestBody @Valid가 적용되어 있어 DTO 검증이 수행되고 있습니다.
또한, 도메인 규칙에 맞춰 최대 길이 제한을 위해 @Size 검증도 추가를 고려해 주세요.

수정 예시:

     public AttendanceCheckCreateCommand toCommand(Long creatorId, Long roomId) {
-        return new AttendanceCheckCreateCommand(creatorId, roomId, content);
+        return new AttendanceCheckCreateCommand(creatorId, roomId, content.strip());
     }

• 파일:

  • src/main/java/konkuk/thip/roompost/adapter/in/web/request/AttendanceCheckCreateRequest.java (toCommand 메서드)
  • src/main/java/konkuk/thip/roompost/adapter/in/web/RoomPostCommandController.java (129행: @RequestBody @Valid AttendanceCheckCreateRequest 확인됨)
src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/vote/VoteParticipantJpaRepository.java (1)

14-16: @query(named parameter) 바인딩 누락 가능성: @Param 추가 필요

findVoteParticipantByUserIdAndVoteItemId는 named parameter(:userId, :voteItemId)를 사용하지만 파라미터에 @Param이 없습니다. 컴파일 옵션에 따라 런타임 바인딩 실패가 발생할 수 있습니다. 안전하게 @Param을 명시해 주세요.

-    Optional<VoteParticipantJpaEntity> findVoteParticipantByUserIdAndVoteItemId(Long userId, Long voteItemId);
+    Optional<VoteParticipantJpaEntity> findVoteParticipantByUserIdAndVoteItemId(
+            @Param("userId") Long userId,
+            @Param("voteItemId") Long voteItemId
+    );
src/main/java/konkuk/thip/roompost/application/service/manager/RoomProgressManager.java (1)

22-29: Book 조회 시 NPE 방지 로직 추가 필요
현재 BookCommandPort에는 findById(Long)만 정의되어 있고, getByIdOrThrow 같은 보장형 메서드는 없습니다. 따라서 findById가 예외 대신 null을 반환할 경우 book.getPageCount()에서 NPE가 발생할 수 있습니다.

아래 두 가지 중 선택해 적용해 주세요.

옵션 A: 포트에 getByIdOrThrow 메서드를 추가 후 사용

// BookCommandPort.java에 추가
default Book getByIdOrThrow(Long id) {
    Book book = findById(id);
    if (book == null) {
        throw new EntityNotFoundException(ErrorCode.BOOK_NOT_FOUND);
    }
    return book;
}

// RoomProgressManager.java 변경
- Book book = bookCommandPort.findById(room.getBookId());
+ Book book = bookCommandPort.getByIdOrThrow(room.getBookId());

옵션 B: Optional 래핑 후 예외 처리

import java.util.Optional;

- Book book = bookCommandPort.findById(room.getBookId());
+ Book book = Optional.ofNullable(bookCommandPort.findById(room.getBookId()))
+    .orElseThrow(() -> new EntityNotFoundException(
+        ErrorCode.BOOK_NOT_FOUND, "Book not found for id=" + room.getBookId()));

검증 포인트

  • BookCommandPort.findById(Long)가 null을 반환할 수 있는지(구현체 확인)
  • 서비스 로직 전체에서 동일한 예외 처리 방식 일관성 유지
src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/vote/VoteQueryRepositoryImpl.java (1)

37-43: 부분 null(start/end) 처리 누락 — between에 null 전달 위험

startEndNull은 두 값이 모두 null인 경우만 처리합니다. 하나만 null인 경우 between(start, end)에 null이 전달되어 런타임 예외가 발생할 수 있습니다. 안전한 범위 Predicate로 교체해 주세요.

-                .where(
-                        vote.roomJpaEntity.roomId.eq(roomId),
-                        filterByType(type, vote, userId),
-                        (startEndNull(pageStart, pageEnd) ? vote.isOverview.isTrue() : vote.page.between(pageStart, pageEnd))
-                )
+                .where(
+                        vote.roomJpaEntity.roomId.eq(roomId),
+                        filterByType(type, vote, userId),
+                        pageRangePredicate(pageStart, pageEnd)
+                )
                 .fetch();
     }
 
-    private boolean startEndNull(Integer start, Integer end) {
-        return start == null && end == null;
-    }
+    private BooleanExpression pageRangePredicate(Integer start, Integer end) {
+        if (start == null && end == null) {
+            return vote.isOverview.isTrue();
+        } else if (start != null && end != null) {
+            return vote.page.between(start, end);
+        } else if (start != null) {
+            return vote.page.goe(start);
+        } else { // end != null
+            return vote.page.loe(end);
+        }
+    }

Also applies to: 45-47

src/main/java/konkuk/thip/roompost/adapter/in/web/RoomPostQueryController.java (1)

44-53: RoomPostSearchService의 pageEnd 기본값 보정 로직 추가 필요

현재 RoomPostSearchService에서는 !isOverview 조건 하에 pageStart만 기본값(0)으로 설정하고 있어 pageEndnull일 경우 Repository 호출 시 NPE가 발생할 수 있습니다. 또한, 기본 보정 로직이 isPageFilter 여부를 고려하지 않아 의도치 않은 동작이 발생할 수 있습니다.

수정 제안:

  • src/main/java/konkuk/thip/roompost/application/service/RoomPostSearchService.java
    • if (!isOverview) 블록을 아래처럼 변경하여 pageEnd도 기본값으로 보정합니다.
    • 필요에 따라 isPageFilter 플래그를 함께 검사하도록 분기 조건을 조정하세요.
--- a/src/main/java/konkuk/thip/roompost/application/service/RoomPostSearchService.java
+++ b/src/main/java/konkuk/thip/roompost/application/service/RoomPostSearchService.java
@@ -76,6 +76,11 @@ public CursorBasedList<RoomPostQueryDto> search(…) {
     // 총평 보기가 아닌 경우, pageStart와 pageEnd를 default 값 주입
     if (!isOverview) {
         if (pageStart == null) {
             pageStart = 0;
+        }
+        if (pageEnd == null) {
+            // 책의 마지막 페이지 수로 pageEnd 기본값 설정
+            pageEnd = book.getPageCount();
         }
     }
  • isPageFilter=false인 경우에도 기본 보정이 실행되지 않도록, 필요시 아래와 같이 조건을 강화하세요.
if (isPageFilter && !isOverview) { … }

위 변경으로 pageStart·pageEnd 모두 안전하게 기본값이 주입되며, NPE 위험을 제거할 수 있습니다.

src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/record/RecordQueryRepositoryImpl.java (1)

96-124: 페이지 필터링에서 pageStart/pageEnd가 null일 때 NPE/잘못된 쿼리 위험

isOverview=false인데 pageStart/pageEnd가 null이면 between 절에서 문제가 생길 수 있습니다. 두 값이 모두 있을 때에만 between을 적용하고, 없으면 전체 페이지를 포함하도록 방어해 주세요(또는 상위 레이어에서 기본값을 보정).

다음과 같이 수정하면 안전합니다.

-        if (isOverview) {
+        if (Boolean.TRUE.equals(isOverview)) {
             voteCondition.and(vote.isOverview.isTrue());
         } else {
-            voteCondition.and(vote.isOverview.isFalse())
-                    .and(vote.page.between(pageStart, pageEnd));
+            voteCondition.and(vote.isOverview.isFalse());
+            if (pageStart != null && pageEnd != null) {
+                voteCondition.and(vote.page.between(pageStart, pageEnd));
+            }
         }
@@
-        if (isOverview) {
+        if (Boolean.TRUE.equals(isOverview)) {
             recordCondition.and(record.isOverview.isTrue());
         } else {
-            recordCondition.and(record.isOverview.isFalse())
-                    .and(record.page.between(pageStart, pageEnd));
+            recordCondition.and(record.isOverview.isFalse());
+            if (pageStart != null && pageEnd != null) {
+                recordCondition.and(record.page.between(pageStart, pageEnd));
+            }
         }
♻️ Duplicate comments (1)
src/main/java/konkuk/thip/roompost/domain/Record.java (1)

12-15: CountUpdatable 구현(또는 상속) 관계 검증 필요

이 엔티티는 현재 RoomPost만 구현하고 있습니다. 상위 계층에서 좋아요 정책/유스케이스가 CountUpdatable을 인자로 사용한다면, RoomPostCountUpdatable을 확장하고 있어야 컴파일/런타임이 일치합니다. 앞서 RoomPostLikeAccessPolicy 쪽 코멘트의 검증 스크립트를 참고해 확인 부탁드립니다.

Comment on lines +9 to +11
import konkuk.thip.post.domain.CountUpdatable;
import konkuk.thip.post.application.service.handler.PostHandler;
import konkuk.thip.post.domain.PostType;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

PostType/CountUpdatable/PostHandler 패키지 이동 반영 OK. 타입 변환 방식 일관성 확인 권장

리팩터링된 패키지 경로로의 정리는 적절합니다. 한 가지 점검사항: 본 서비스는 PostType.from(String)을 사용합니다(라인 34). 본 PR에서 일부 컴포넌트가 room-post 전용 매핑 메서드(roomPostTypeFrom)를 사용하도록 변경되었다면, 여기서도 동일 규칙을 적용할지 확인해 주세요. 매핑 규칙이 분기되면 권한 검증/조회 핸들러에서 타입 해석 불일치가 발생할 수 있습니다.

아래 스크립트로 타입 매핑 사용 현황을 점검해 주세요.

참고: 프로젝트 학습 메모에 따르면 Record/Vote는 Room 소속, Feed는 비소속입니다. commentAuthorizationValidator가 타입별 룸 참가자 검증을 이 규칙에 맞춰 수행하는지 함께 확인하면 좋겠습니다.


🏁 Script executed:

#!/bin/bash
# PostType 매핑 메서드 사용 현황 파악
rg -nP 'PostType\.from\s*\(' -C2
rg -nP 'PostType\.roomPostTypeFrom\s*\(' -C2

# 혹시 남아 있을 수 있는 구 패키지 경로 사용 점검
rg -nP 'import\s+konkuk\.thip\.common\.post\.(PostType|CountUpdatable)' -n

Length of output: 2523


🏁 Script executed:

#!/bin/bash
# Locate PostType enum implementation
fd --max-depth 3 PostType.java

# Display content around 'from' and 'roomPostTypeFrom' methods
# Replace path with actual file if found
POST_TYPE_FILE=$(fd --max-depth 3 PostType.java | head -n1)
if [ -n "$POST_TYPE_FILE" ]; then
  echo "=== File: $POST_TYPE_FILE ==="
  sed -n '1,200p' "$POST_TYPE_FILE" | sed -n '/from/,/}/p'
  echo
  sed -n '1,200p' "$POST_TYPE_FILE" | sed -n '/roomPostTypeFrom/,/}/p'
else
  echo "PostType.java not found"
fi

Length of output: 169


🏁 Script executed:

#!/bin/bash
# PostType.java 전체 경로 검색
fd PostType.java

# 검색된 파일에서 매핑 메서드 내용 출력
POST_TYPE_FILE=$(fd PostType.java | head -n1)
if [ -n "$POST_TYPE_FILE" ]; then
  echo "=== $POST_TYPE_FILE ==="
  sed -n '1,200p' "$POST_TYPE_FILE" | sed -n '/from\s*(/,/)/p'
  echo
  sed -n '1,200p' "$POST_TYPE_FILE" | sed -n '/roomPostTypeFrom\s*(/,/)/p'
else
  echo "PostType.java 파일을 찾을 수 없습니다."
fi

Length of output: 777


PostType 매핑 방식 일관성 확인 필요

리팩터링된 패키지 경로 반영은 적절합니다. 다만, 방 게시글 전용 매핑을 위해 일부 컴포넌트(RoomPostIsLikeRequest)에서는 PostType.roomPostTypeFrom을 사용하는 반면,
CommentCreateService(src/main/java/konkuk/thip/comment/application/service/CommentCreateService.java:34)에서는 여전히 일반 매핑인 PostType.from을 사용하고 있습니다.
매핑 로직 분기 시 권한 검증·조회 핸들러에서 타입 해석이 불일치할 수 있으므로, 아래 사항을 검토해주세요:

  • CommentCreateService: PostType.from(command.postType())PostType.roomPostTypeFrom(command.postType()) 적용 여부
  • commentAuthorizationValidator: Record/Vote(룸 소속 게시글)와 Feed(비소속 게시글) 검증 로직이 프로젝트 규칙에 맞게 구현되었는지 확인
🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/comment/application/service/CommentCreateService.java
around lines 9 to 11 and 34, the service uses the general
PostType.from(command.postType()) while room-specific components use
PostType.roomPostTypeFrom; change the mapping to
PostType.roomPostTypeFrom(command.postType()) to maintain consistent room-post
type interpretation, and update any imports if necessary; after that, review
commentAuthorizationValidator implementation to ensure its Record/Vote
(room-scoped) vs Feed (non-room) validation logic aligns with the project rules
and adjust its type checks to use the same roomPostTypeFrom mapping so
authorization and type resolution are consistent across handlers.

Comment on lines +11 to 13
import konkuk.thip.post.domain.CountUpdatable;
import konkuk.thip.post.application.service.handler.PostHandler;
import lombok.RequiredArgsConstructor;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

패키지 경로 변경 반영 OK — Room 소속 리소스 접근 검증(Record/Vote) 동일 적용 여부 확인

CommentAuthorizationValidator.validateUserCanAccessPostForComment(...)가 댓글 생성과 동일하게 Record/Vote에 대해 "해당 Room 참가자 여부"를 검증하는지 확인 부탁드립니다(팀 공유 지식 기준).

아래 스크립트로 해당 메서드 구현과 룸 참가자 검증 로직(예: RoomParticipant/Room 권한 검사)이 포함되어 있는지 빠르게 점검할 수 있습니다.

동시에, 댓글 좋아요 중복 삽입 방지를 위해 (userId, commentId) 유니크 인덱스가 저장소 레벨에 구성되어 있는지도 확인해 주세요. 이는 동시성 하에서 중복 like를 방지하는 최후방어선입니다.


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 메서드 선언/구현 위치 및 주변 문맥 확인
rg -nP 'validateUserCanAccessPostForComment\s*\(' -C5

# Room 참가자 검증과 연관된 키워드가 같은 파일에 함께 있는지 힌트 검색
rg -nP 'RoomParticipant|RoomParticipants|isParticipant|validate.*Participant|validate.*Room' -n -C3

Length of output: 266650


#!/usr/bin/env bash
set -euo pipefail

# 1. CommentAccessPolicy 구현체 확인
echo "=== CommentAccessPolicy 구현체 ==="
rg -nP 'implements\s+CommentAccessPolicy' -C3 src/main/java

# 2. CommentLikeJpaEntity 위치 및 unique 제약 확인
echo
echo "=== CommentLikeJpaEntity 검색 ==="
entity_file=$(rg -lP 'class\s+CommentLikeJpaEntity' -g '*.java' src/main/java || true)
if [[ -n "$entity_file" ]]; then
  echo "Found: $entity_file"
  echo "--- @Table 어노테이션 포함 부분 ---"
  rg -nP '@Table' -C2 "$entity_file" || echo "(no @Table annotation)"
  echo "--- unique 또는 uniqueConstraints 키워드 ---"
  rg -nP 'unique|uniqueConstraints' -n "$entity_file" || echo "(no unique constraints)"
else
  echo "CommentLikeJpaEntity 클래스를 찾지 못했습니다."
fi

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== 1. CommentAccessPolicy 인터페이스 위치 ==="
rg -nP "interface\s+CommentAccessPolicy" -C3 src/main/java || echo "(인터페이스 없음)"

echo
echo "=== 2. CommentAccessPolicy 구현체 목록 ==="
rg -nP "implements\s+CommentAccessPolicy" -C3 src/main/java || echo "(구현체 없음)"

echo
echo "=== 3. CommentLikeJpaEntity 에서 (userId, commentId) 유니크 인덱스 확인 ==="
entity_file=$(rg -lP "class\s+CommentLikeJpaEntity" -g '*.java' src/main/java || true)
if [[ -n "$entity_file" ]]; then
  echo "Found: $entity_file"
  echo "--- @Table 어노테이션 ---"
  rg -nP "@Table" -C2 "$entity_file" || echo "(어노테이션 없음)"
  echo "--- uniqueConstraints 또는 unique 키워드 ---"
  rg -nP "uniqueConstraints|unique" -C2 "$entity_file" || echo "(유니크 제약 없음)"
else
  echo "CommentLikeJpaEntity 클래스를 찾지 못했습니다."
fi

Length of output: 3801


CommentAuthorizationValidator: Record/Vote Room 참가자 검증 정상 적용 확인, CommentLike DB 유니크 제약 추가 필요

  • CommentAuthorizationValidator.validateUserCanAccessPostForComment()
    – PostType.RECORD·VOTE → RoomPostCommentAccessPolicy.validateCommentAccess() 호출
    – 내부에서 RoomParticipantValidator.validateUserIsRoomMember(roomId, userId)로 “해당 방 참가자 여부” 검증이 정확히 적용됨 확인
  • CommentLikeJpaEntity.java (src/main/java/konkuk/thip/comment/adapter/out/jpa/CommentLikeJpaEntity.java)
    – 현재 @table(name="comment_likes")만 선언되어 있어 (user_id, comment_id) 조합에 대한 DB 유니크 제약이 없음
    – 동시성 하 중복 좋아요를 막기 위해 아래와 같이 uniqueConstraints 추가 필요
    @Entity
    @Table(
      name = "comment_likes",
      uniqueConstraints = @UniqueConstraint(
        name = "uk_comment_like_user_comment",
        columnNames = {"user_id", "comment_id"}
      )
    )
    
🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/comment/adapter/out/jpa/CommentLikeJpaEntity.java
around the class/@Table declaration, add a unique constraint for the (user_id,
comment_id) pair by updating the @Table annotation to include uniqueConstraints
= @UniqueConstraint(name = "uk_comment_like_user_comment", columnNames =
{"user_id", "comment_id"}); ensure javax.persistence.UniqueConstraint is
imported and the column names match the entity/JPA column mappings, then
regenerate/apply the DB migration or schema update so the unique constraint is
created.

import konkuk.thip.room.domain.RoomParticipants;
import konkuk.thip.room.domain.RoomParticipant;
import konkuk.thip.vote.application.port.out.VoteQueryPort;
import konkuk.thip.roompost.application.port.out.VoteQueryPort;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

VoteQueryPort 패키지 이동 반영 OK — 모듈 간 의존 방향 점검 권장

Room 모듈 서비스가 roompost 포트(VoteQueryPort)에 직접 의존하게 되었습니다. 설계상 Room이 더 하위(혹은 독립) 모듈이어야 한다면, 의존 역전을 방지하기 위해:

  • Room 쪽에 추상 포트를 두고 roompost가 이를 구현, 혹은
  • roompost 내에서 해당 조회를 조합한 파사드/애플리케이션 서비스를 제공하고 Room은 그 결과만 주입

같은 구조를 고려해 볼 수 있습니다. 현 단계에선 기능상 문제는 아니며 구조적 개선 제안입니다.

구 경로 참조 잔존 및 구현체 존재 여부를 아래로 확인해 주세요.


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 구 VoteQueryPort 경로 참조 잔여 여부
rg -nP 'konkuk\.thip\.vote\.application\.port\.out\.VoteQueryPort' -C2 || true

# 새 VoteQueryPort 구현체가 존재하는지
rg -nP 'implements\s+VoteQueryPort\b' -C3 || true

Length of output: 971


VoteQueryPort 경로 이동 및 구현체 확인 완료

  • 구경로(konkuk.thip.vote.application.port.out.VoteQueryPort) 참조 잔여 없음
  • 새경로 포트 구현체 VoteQueryPersistenceAdapterkonkuk.thip.roompost.adapter.out.persistence에 존재

Room 모듈이 하위(또는 독립) 모듈이라면 모듈 간 의존 방향을 재검토해 보세요:

  • Room 모듈에 추상 포트 정의 후 roompost 모듈이 이를 구현
  • 또는 roompost 모듈에 조회 전용 파사드/애플리케이션 서비스 제공 → Room은 결과만 주입
🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/room/application/service/RoomShowPlayingDetailViewService.java
around line 14, the review notes the VoteQueryPort has been moved to
konkuk.thip.roompost.application.port.out and its implementation
VoteQueryPersistenceAdapter lives in
konkuk.thip.roompost.adapter.out.persistence; fix by ensuring the Room module
imports the correct new port package (update the import to
konkuk.thip.roompost.application.port.out.VoteQueryPort if not already), then
reconcile module dependencies: either move the VoteQueryPort interface into the
room module and have roompost implement it, or (preferred if keeping current
layout) add a compile-time dependency from room -> roompost (adjust pom/gradle)
so Room can reference the port, and verify VoteQueryPersistenceAdapter is a bean
discoverable by the application context (correct package/component-scan or
@Configuration). Ensure package visibility and tests compile after the change.


import jakarta.persistence.*;
import konkuk.thip.common.entity.BaseJpaEntity;
import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

RoomJpaEntity 교차 의존 검증 필요

roompost 어댑터에서 room 어댑터의 RoomJpaEntity를 참조하는 구조는 테이블 FK 맵핑상 자연스러우나, 모듈 의존(gradle) 방향이 이를 허용하는지 점검이 필요합니다. 또한 중복 정의된 RoomJpaEntity가 없는지도 확인해 주세요.

다음 스크립트로 중복 정의 및 빌드 설정 단서를 확인할 수 있습니다.


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "RoomJpaEntity 정의 파일 위치"
fd -a 'RoomJpaEntity.java' || true

echo
echo "모듈 의존(gradle) 단서 검색"
rg -nP 'implementation.*room' -g '*build.gradle*' -C2 || true

Length of output: 306


RoomJpaEntity 중복 정의 없음, Gradle 모듈 의존성 미발견

  • RoomJpaEntity 정의 위치
    • src/main/java/konkuk/thip/room/adapter/out/jpa/RoomJpaEntity.java
  • roompost 모듈의 build.gradle에서 room 모듈(프로젝트) 의존성 미발견

조치사항:

  • roompost 모듈의 Gradle 설정(build.gradle)에
    implementation project(':room')
    또는 실제 모듈 이름에 맞는 프로젝트 의존성을 추가하여
    RoomJpaEntity 참조가 정상적으로 컴파일·실행되는지 검증하고 수정해 주세요.
🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/roompost/adapter/out/jpa/AttendanceCheckJpaEntity.java
around line 5, the import of RoomJpaEntity points to a class in the room module
but the roompost module lacks a Gradle project dependency; update the roompost
module's build.gradle to add the project dependency (e.g., implementation
project(':room') or the actual module name), then refresh Gradle, recompile to
ensure the import resolves and adjust the dependency name if your multi-module
settings use a different path.

Comment on lines +23 to +28
return CursorBasedList.of(roomPostQueryDtos, cursor.getPageSize(), postQueryDto -> {
Cursor nextCursor = new Cursor(List.of(postQueryDto.isOverview() ? "1" : "0",
postQueryDto.page().toString(),
postQueryDto.postId().toString()));
return nextCursor.toEncodedString();
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

커서 생성 시 page가 null일 경우 NPE/파싱 에러 가능성 있음

overview 항목(총평)의 경우 page가 null일 수 있습니다. 현재 toString() 호출이 null에서 NPE를 유발하거나, "null"이 인코딩되어 Cursor.getInteger(…) 파싱에 실패할 수 있습니다. 안전한 기본값(예: 0)으로 대체해 주세요.

다음과 같이 방어 로직을 추가하는 것을 제안합니다.

-        return CursorBasedList.of(roomPostQueryDtos, cursor.getPageSize(), postQueryDto -> {
-            Cursor nextCursor = new Cursor(List.of(postQueryDto.isOverview() ? "1" : "0",
-                    postQueryDto.page().toString(),
-                    postQueryDto.postId().toString()));
-            return nextCursor.toEncodedString();
-        });
+        return CursorBasedList.of(roomPostQueryDtos, cursor.getPageSize(), postQueryDto -> {
+            String pageStr = postQueryDto.page() == null ? "0" : postQueryDto.page().toString();
+            Cursor nextCursor = new Cursor(List.of(
+                    postQueryDto.isOverview() ? "1" : "0",
+                    pageStr,
+                    postQueryDto.postId().toString()
+            ));
+            return nextCursor.toEncodedString();
+        });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return CursorBasedList.of(roomPostQueryDtos, cursor.getPageSize(), postQueryDto -> {
Cursor nextCursor = new Cursor(List.of(postQueryDto.isOverview() ? "1" : "0",
postQueryDto.page().toString(),
postQueryDto.postId().toString()));
return nextCursor.toEncodedString();
});
return CursorBasedList.of(roomPostQueryDtos, cursor.getPageSize(), postQueryDto -> {
String pageStr = postQueryDto.page() == null ? "0" : postQueryDto.page().toString();
Cursor nextCursor = new Cursor(List.of(
postQueryDto.isOverview() ? "1" : "0",
pageStr,
postQueryDto.postId().toString()
));
return nextCursor.toEncodedString();
});
🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/roompost/adapter/out/persistence/RecordQueryPersistenceAdapter.java
around lines 23 to 28, the current cursor construction calls page().toString()
which can NPE or produce the string "null" for overview items where page is
null; replace this by defensively converting a possibly-null Integer page to a
safe default (e.g. 0) before toString() — e.g. compute int safePage =
(postQueryDto.page() == null ? 0 : postQueryDto.page()); then use
Integer.toString(safePage) (or String.valueOf(safePage)) when building the
Cursor list so null pages are encoded as "0" and parsing won't fail.

Comment on lines +6 to 9
public record RoomPostSearchQuery(
Long roomId,
String type,
String sort,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

String 기반 type/sort → 전용 파라미터 타입 사용 고려 (요약과 불일치 가능성)

AI 요약에서는 RoomPostSearchTypeParams/RoomPostSearchSortParams 도입을 언급했는데, 현재 DTO는 String type, String sort로 유지되어 있습니다. 문자열은 오입력 가능성이 높아 유효값을 컴파일 타임에 보장하기 어렵습니다. 전용 파라미터 타입이 실제로 존재한다면 교체를 고려해 주세요.

아래는 타입 교체 예시입니다(패키지 경로는 실제 소스 기준으로 조정 필요).

 public record RoomPostSearchQuery(
         Long roomId,
-        String type,
-        String sort,
+        RoomPostSearchTypeParams type,
+        RoomPostSearchSortParams sort,
         Integer pageStart,
         Integer pageEnd,
         Boolean isOverview,
         Boolean isPageFilter,
         String nextCursor,
         Long userId
 ) {
 }

전용 타입 존재 여부를 먼저 확인해 주세요.

아래 스크립트로 전용 파라미터 타입 정의가 있는지 확인할 수 있습니다.


🏁 Script executed:

#!/bin/bash
# RoomPostSearchTypeParams / RoomPostSearchSortParams 정의 검색
rg -nP -C2 '^(public\s+)?(class|record|enum)\s+RoomPostSearch(Type|Sort)Params\b'

Length of output: 1159


문자열 기반 type/sort → 전용 enum 타입으로 교체 권장

확인 결과 RoomPostSearchTypeParamsRoomPostSearchSortParams enum이
src/main/java/konkuk/thip/roompost/application/port/in/dto 에 이미 정의되어 있으므로,
DTO에서 컴파일 타임 유효성 보장을 위해 아래와 같이 교체를 적용해주세요.

  • 대상 파일
    src/main/java/konkuk/thip/roompost/application/port/in/dto/RoomPostSearchQuery.java
  • 변경 전
    String type, String sort
  • 변경 후
    RoomPostSearchTypeParams type, RoomPostSearchSortParams sort

예시 diff:

 public record RoomPostSearchQuery(
         Long roomId,
-        String type,
-        String sort,
+        RoomPostSearchTypeParams type,
+        RoomPostSearchSortParams sort,
         Integer pageStart,
         Integer pageEnd,
         Boolean isOverview,
         Boolean isPageFilter,
         String nextCursor,
         Long userId
 ) {
 }

위와 같이 수정하시면 타입 오입력 방지 및 코드 안정성을 높일 수 있습니다.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public record RoomPostSearchQuery(
Long roomId,
String type,
String sort,
package konkuk.thip.roompost.application.port.in.dto;
public record RoomPostSearchQuery(
Long roomId,
RoomPostSearchTypeParams type,
RoomPostSearchSortParams sort,
Integer pageStart,
Integer pageEnd,
Boolean isOverview,
Boolean isPageFilter,
String nextCursor,
Long userId
) {
}
🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/roompost/application/port/in/dto/RoomPostSearchQuery.java
around lines 6 to 9, the DTO currently declares String type and String sort;
replace them with the enum types RoomPostSearchTypeParams and
RoomPostSearchSortParams respectively, and add the necessary import statements
(or use fully-qualified names) for those enums so the record signature becomes
type-safe; ensure any callers or tests are updated to pass the enum values
instead of strings and recompile to catch any remaining usages.

Comment on lines +3 to +7
import konkuk.thip.roompost.adapter.in.web.response.RecordPinResponse;
import konkuk.thip.roompost.application.port.in.dto.record.RecordPinQuery;

public interface RecordPinUseCase {
RecordPinResponse pinRecord(RecordPinQuery query);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

애플리케이션 포트가 Web 어댑터의 Response 타입에 의존 — 계층 역전 발생

application.port.inadapter.in.web.response.RecordPinResponse에 의존하면서 레이어 경계가 붕괴되었습니다. 포트는 Web 응답을 반환하지 않고, 애플리케이션 내부 DTO(또는 도메인 값)를 반환해야 합니다. Web 어댑터에서 응답 매핑을 수행하세요.

다음과 같이 포트 반환 타입을 애플리케이션 DTO로 분리하는 것을 권장합니다.

 package konkuk.thip.roompost.application.port.in;

-import konkuk.thip.roompost.adapter.in.web.response.RecordPinResponse;
 import konkuk.thip.roompost.application.port.in.dto.record.RecordPinQuery;
+import konkuk.thip.roompost.application.port.in.dto.record.RecordPinResult;

 public interface RecordPinUseCase {
-    RecordPinResponse pinRecord(RecordPinQuery query);
+    RecordPinResult pinRecord(RecordPinQuery query);
 }

애플리케이션 DTO 예시(신규 파일):

// src/main/java/konkuk/thip/roompost/application/port/in/dto/record/RecordPinResult.java
package konkuk.thip.roompost.application.port.in.dto.record;

public record RecordPinResult(Long recordId, boolean pinned) {}

그리고 Controller(Web 어댑터)에서 RecordPinResult -> RecordPinResponse 매핑을 수행해 주십시오. 필요하시면 연쇄 수정(PR 내 적용 지점)도 함께 제안드리겠습니다.

🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/roompost/application/port/in/RecordPinUseCase.java
around lines 3-7, the port currently depends on the Web adapter response type;
change the port to return an application-layer DTO (e.g., RecordPinResult)
instead of RecordPinResponse. Create the new record DTO at
src/main/java/konkuk/thip/roompost/application/port/in/dto/record/RecordPinResult.java
with fields (Long recordId, boolean pinned), update the interface signature to
return RecordPinResult, and update the Web controller/adapter to map
RecordPinResult -> RecordPinResponse when returning HTTP responses. Ensure no
other application-layer classes import
adapter.in.web.response.RecordPinResponse.

Comment on lines +1 to 9
package konkuk.thip.roompost.application.service;

import konkuk.thip.room.application.port.in.AttendanceCheckCreateUseCase;
import konkuk.thip.room.application.port.in.dto.AttendanceCheckCreateCommand;
import konkuk.thip.room.application.port.in.dto.AttendanceCheckCreateResult;
import konkuk.thip.room.application.port.out.AttendanceCheckCommandPort;
import konkuk.thip.room.application.port.out.AttendanceCheckQueryPort;
import konkuk.thip.room.domain.AttendanceCheck;
import konkuk.thip.roompost.application.port.in.AttendanceCheckCreateUseCase;
import konkuk.thip.roompost.application.port.in.dto.attendancecheck.AttendanceCheckCreateCommand;
import konkuk.thip.roompost.application.port.in.dto.attendancecheck.AttendanceCheckCreateResult;
import konkuk.thip.roompost.application.port.out.AttendanceCheckCommandPort;
import konkuk.thip.roompost.application.port.out.AttendanceCheckQueryPort;
import konkuk.thip.roompost.domain.AttendanceCheck;
import konkuk.thip.room.application.service.validator.RoomParticipantValidator;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

roompost 모듈로 서비스 이전 OK — 동시성/중복 작성 방지 전략 확인 필요

모듈 경로 재정렬과 포트/DTO/도메인 import 재배치 모두 적절합니다. 다만 create 로직 특성상(“오늘의 한마디”) 동시성 하에서 중복 저장이 발생하지 않도록 방어가 필요합니다. 현재는 조회 → 저장의 비원자적 흐름이라, 두 요청이 동시에 들어오면 둘 다 count=0으로 보고 각각 저장할 수 있습니다.

검토 포인트:

  • DB 유니크 제약: (room_id, creator_id, date(일자)) 혹은 해당 역할의 대체 키 존재 여부
  • 저장 시 중복 키 충돌 처리(재시도/친절한 에러 변환)
  • 애플리케이션 레벨의 락(낙관적/비관적) 또는 Redis 분산락 사용 여부

스키마/엔티티 수준에서 유니크 제약 존재 여부를 다음 스크립트로 빠르게 확인해볼 수 있습니다:

중복 방지 요구사항이 없다면(하루 복수 작성 허용), 현재 구현으로 충분합니다. 요구사항에 따라 보강 필요 시 알려주세요. 보강 코드와 마이그레이션 스크립트까지 제안 드리겠습니다.


🏁 Script executed:

#!/bin/bash
# AttendanceCheck JPA 엔티티 및 테이블 제약 확인
fd -a 'AttendanceCheck*JpaEntity.java' -a | while read -r F; do
  echo ">>> $F"
  rg -n -C2 '@Table|\b@Index\b|\buniqueConstraints\b' "$F"
done

Length of output: 554


중복 저장 방지를 위한 DB 유니크 제약 및 예외 처리 추가 필요

JPA 엔티티(AttendanceCheckJpaEntity)를 확인한 결과, 현재 attendance_checks 테이블에 (room_id, creator_id, date)에 대한 유니크 제약이 설정되어 있지 않습니다. 이에 동시성 상황에서 중복 INSERT가 발생할 수 있으므로 아래 보강이 필요합니다.

검토 포인트 및 제안사항:

  • AttendanceCheckJpaEntity.java
    • @table에 uniqueConstraints 추가
      @Table(
        name = "attendance_checks",
        uniqueConstraints = @UniqueConstraint(
          name = "uk_attendance_room_creator_date",
          columnNames = {"room_id", "creator_id", "date"}
        )
      )
  • 데이터베이스 마이그레이션 스크립트
    • 기존 테이블에 UNIQUE 제약 추가 SQL
      ALTER TABLE attendance_checks
        ADD CONSTRAINT uk_attendance_room_creator_date
        UNIQUE (room_id, creator_id, date);
  • 저장 로직 보강
    • AttendanceCheckCreateService에서 DataIntegrityViolationException 등 중복 키 예외 캐치 후
      • 재시도 전략 또는 친절한 사용자 에러 반환
  • 추가 방어책 (옵션)
    • 낙관적 락(@Version) 혹은 분산 락(Redis Lock) 적용 검토

위 변경을 반영해 주시면 중복 생성 이슈를 완벽히 방어할 수 있습니다.

🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/roompost/application/service/AttendanceCheckCreateService.java
around lines 1 to 9, add handling for duplicate inserts by (1) ensuring the
AttendanceCheck JPA entity/table has a unique constraint on (room_id,
creator_id, date) via @Table(uniqueConstraints=...), (2) providing a DB
migration to add the UNIQUE constraint to the existing attendance_checks table,
and (3) updating this service's save logic to catch
DataIntegrityViolationException (or the specific constraint violation), then
either apply a retry strategy or return a clear user-facing error indicating the
attendance check already exists; optionally consider adding optimistic locking
(@Version) or a distributed lock for extra protection.

Comment on lines 90 to 93
Map<Long, List<VoteItemQueryDto>> voteItemQueryMap = voteQueryPort.findVoteItemsByVoteIds(cursorBasedList.contents().stream()
.filter(postQueryDto -> postQueryDto.postType().equals(VOTE.getType()))
.map(PostQueryDto::postId)
.map(RoomPostQueryDto::postId)
.collect(Collectors.toSet()), userId);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

VOTE 타입 비교 시 NPE 가능성 제거

postType()이 null일 경우 현재 equals 호출에서 NPE가 날 수 있습니다. 상수 기준 비교로 바꿔 null-safe하게 처리하세요.

-        Map<Long, List<VoteItemQueryDto>> voteItemQueryMap = voteQueryPort.findVoteItemsByVoteIds(cursorBasedList.contents().stream()
-                .filter(postQueryDto -> postQueryDto.postType().equals(VOTE.getType()))
+        Map<Long, List<VoteItemQueryDto>> voteItemQueryMap = voteQueryPort.findVoteItemsByVoteIds(cursorBasedList.contents().stream()
+                .filter(postQueryDto -> VOTE.getType().equals(postQueryDto.postType()))
                 .map(RoomPostQueryDto::postId)
                 .collect(Collectors.toSet()), userId);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Map<Long, List<VoteItemQueryDto>> voteItemQueryMap = voteQueryPort.findVoteItemsByVoteIds(cursorBasedList.contents().stream()
.filter(postQueryDto -> postQueryDto.postType().equals(VOTE.getType()))
.map(PostQueryDto::postId)
.map(RoomPostQueryDto::postId)
.collect(Collectors.toSet()), userId);
Map<Long, List<VoteItemQueryDto>> voteItemQueryMap = voteQueryPort.findVoteItemsByVoteIds(cursorBasedList.contents().stream()
.filter(postQueryDto -> VOTE.getType().equals(postQueryDto.postType()))
.map(RoomPostQueryDto::postId)
.collect(Collectors.toSet()), userId);
🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/roompost/application/service/RoomPostSearchService.java
around lines 90 to 93, the lambda currently calls
postQueryDto.postType().equals(VOTE.getType()) which can NPE if postType() is
null; change the comparison to be null-safe by using the constant-first pattern
(e.g., VOTE.getType().equals(postQueryDto.postType())) or
Objects.equals(VOTE.getType(), postQueryDto.postType()) so null postType values
won't throw an exception.

@@ -1,4 +1,4 @@
package konkuk.thip.room.domain;
package konkuk.thip.roompost.domain;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

roompost 도메인으로의 패키지 이동 적절함 + 구 패키지 import 잔존 여부 점검 + 일일 작성 제한 상수 외부화 제안

  • 패키지 리네임은 본 PR 방향과 일치합니다.
  • 구 패키지(konkuk.thip.room.domain.AttendanceCheck)를 import하던 코드가 남아있지 않은지 한 번 스캔해 주세요.
  • 선택사항: LIMIT_WRITE_COUNT_PER_DAY(현재 5)가 하드코딩되어 있어 환경/비즈니스에 따른 조정이 어렵습니다. 설정 또는 정책 객체로 외부화하면 테스트 용이성과 운영 유연성이 좋아집니다.

구 패키지 참조 여부 점검 스크립트:

선택적 리팩터링 예시(아이디어):

  • 정책 클래스로 상수 위임
  • 또는 팩토리 메서드 오버로드로 일일 제한치를 주입 가능하게 변경

예시 코드 스케치:

// 예: 정책 클래스
package konkuk.thip.roompost.domain.policy;
public final class AttendanceCheckPolicy {
    private AttendanceCheckPolicy() {}
    public static final int DEFAULT_DAILY_WRITE_LIMIT = 5;
}

// 예: 도메인에서 정책 사용 (현행 하드코딩 제거)
import konkuk.thip.roompost.domain.policy.AttendanceCheckPolicy;
// ...
private static void validateWriteCount(int alreadyWrittenCountTodayOfUser) {
    if (alreadyWrittenCountTodayOfUser >= AttendanceCheckPolicy.DEFAULT_DAILY_WRITE_LIMIT) {
        throw new InvalidStateException(ATTENDANCE_CHECK_WRITE_LIMIT_EXCEEDED);
    }
}

// 또는 오버로드 제공
public static AttendanceCheck withoutId(
    Long roomId, Long creatorId, String todayComment,
    int alreadyWrittenCountTodayOfUser, int dailyLimit
) {
    if (alreadyWrittenCountTodayOfUser >= dailyLimit) {
        throw new InvalidStateException(ATTENDANCE_CHECK_WRITE_LIMIT_EXCEEDED);
    }
    return AttendanceCheck.builder()
            .roomId(roomId)
            .creatorId(creatorId)
            .todayComment(todayComment)
            .build();
}

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== 구 패키지 import/참조 잔존 여부 검색 =="
rg -nP 'konkuk\.thip\.room\.domain\.AttendanceCheck' -C2 || true

Length of output: 156


구 패키지 import/참조 없음 확인 + 일일 작성 제한 상수 외부화 제안

  • 패키지 이동은 적절하며, konkuk.thip.room.domain.AttendanceCheck 참조는 모두 제거된 것을 확인했습니다.
  • 선택사항: LIMIT_WRITE_COUNT_PER_DAY(현재 5)를 환경 설정 또는 정책 객체로 외부화하면 테스트 용이성과 운영 유연성이 향상됩니다.
    예시
    • AttendanceCheckPolicy 같은 정책 클래스로 상수 위임
    • 또는 팩토리 메서드 오버로드로 일일 제한치를 주입 가능하도록 리팩터링
🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/roompost/domain/AttendanceCheck.java around lines
1-1, confirm removal of any remaining references to the old package
konkuk.thip.room.domain.AttendanceCheck and externalize the hardcoded
LIMIT_WRITE_COUNT_PER_DAY (currently 5) by injecting it from configuration or a
policy object; implement either an AttendanceCheckPolicy (holding the daily
limit and related rules) and have AttendanceCheck depend on that, or add a
factory/constructor overload that accepts the daily limit as a parameter so
tests and runtime config can supply different values.

hd0rable
hd0rable previously approved these changes Aug 16, 2025
Copy link
Copy Markdown
Member

@hd0rable hd0rable left a comment

Choose a reason for hiding this comment

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

넘넘 좋네요!! 역시 문귀,문장,문악 답게 꼼꼼한 작업 리스펙힙니다!! 너무너무수고하셨어여!!
패키지 구조 보다가 하나 뭔가 생각이든건.. roomPost 밑에 record,vote,attendanceCheck 이런식으로 패키지를 하나 더 두는건 어떨까?라고도 생각해봣지만 그럼 어댑터,어플리케이션에서 괜히 패키지땜에 더헷갈리는거같기도하고.. roomPost 밑에 record,vote가 뭔가 많아서 잘 안읽히는 느낌이 들어서 한번 적어봣습니다 하핳

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Component
public @interface DomainService {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

오 좋네여 굿굿

package konkuk.thip.post.domain;

import konkuk.thip.post.domain.service.PostCountService;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

오 밑에 TODO 패키지 구조 변경 주석 지워주시면 감사하겠습니다!!

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

넵 제가 지우겠습니다

);
}

public static PostType roomPostTypeFrom(String type) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

오 roomPostType 따로 정의한게 아니라 PostType안에서 roomPostType확인하는거 좋네여 FEED일 경우 예외처리도 넘넘좋습니다!!

seongjunnoh
seongjunnoh previously approved these changes Aug 16, 2025
Copy link
Copy Markdown
Collaborator

@seongjunnoh seongjunnoh left a comment

Choose a reason for hiding this comment

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

굳굳 바뀐 구조 좋습니다!! 이전보다 훨씬 코드의 가독성이 좋아질 것 같네요!

package konkuk.thip.post.domain;

import konkuk.thip.post.domain.service.PostCountService;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

넵 제가 지우겠습니다

@seongjunnoh seongjunnoh dismissed stale reviews from hd0rable and themself via e29f42b August 16, 2025 08:22
@seongjunnoh seongjunnoh merged commit 7e56075 into develop Aug 16, 2025
4 checks passed
@seongjunnoh seongjunnoh deleted the chore/#227-modifying-package branch August 16, 2025 08:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[THIP2025-271] [chore] 패키지 구조 변경(roompost 도입)

3 participants