[chore] 패키지 구조 변경 (RoomPost 도입)#229
Conversation
Walkthrough프로젝트 전반에 roompost 모듈 도입을 중심으로 record/vote/room 관련 타입·포트·컨트롤러·리포지토리의 이동·통합, PostType/CountUpdatable 패키지 재배치, RoomJoinType의 enum 전환 및 테스트/매퍼/어댑터 재배치를 수행했습니다. Changes
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<...>
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>
Estimated code review effort🎯 5 (Critical) | ⏱️ ~90 minutes Assessment against linked issues
Possibly related PRs
Suggested labels
Suggested reviewers
Poem
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 unit tests
🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
Test Results399 tests 399 ✅ 29s ⏱️ Results for commit e29f42b. ♻️ This comment has been updated with latest results. |
There was a problem hiding this comment.
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가 발생할 수 있습니다.현재
BookCommandPort에getByIdOrThrow시그니처가 존재하지 않으므로, 아래 중 하나를 선택해주세요:
- 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: 메서드명 및 호출부 변경 필요:validateUserCanUnLike→validateUserCanUnlike아래 두 곳에서 메서드명과 호출부를 일관되게 변경해야 합니다.
- 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)으로 설정하고 있어pageEnd가null일 경우 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을 인자로 사용한다면,RoomPost가CountUpdatable을 확장하고 있어야 컴파일/런타임이 일치합니다. 앞서 RoomPostLikeAccessPolicy 쪽 코멘트의 검증 스크립트를 참고해 확인 부탁드립니다.
| import konkuk.thip.post.domain.CountUpdatable; | ||
| import konkuk.thip.post.application.service.handler.PostHandler; | ||
| import konkuk.thip.post.domain.PostType; |
There was a problem hiding this comment.
💡 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)' -nLength 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"
fiLength 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 파일을 찾을 수 없습니다."
fiLength 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.
| import konkuk.thip.post.domain.CountUpdatable; | ||
| import konkuk.thip.post.application.service.handler.PostHandler; | ||
| import lombok.RequiredArgsConstructor; |
There was a problem hiding this comment.
💡 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 -C3Length 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 클래스를 찾지 못했습니다."
fiLength 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; |
There was a problem hiding this comment.
💡 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 || trueLength of output: 971
VoteQueryPort 경로 이동 및 구현체 확인 완료
- 구경로(
konkuk.thip.vote.application.port.out.VoteQueryPort) 참조 잔여 없음 - 새경로 포트 구현체
VoteQueryPersistenceAdapter가konkuk.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; |
There was a problem hiding this comment.
💡 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 || trueLength 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.
| 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(); | ||
| }); |
There was a problem hiding this comment.
커서 생성 시 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.
| 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.
| public record RoomPostSearchQuery( | ||
| Long roomId, | ||
| String type, | ||
| String sort, |
There was a problem hiding this comment.
💡 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 타입으로 교체 권장
확인 결과 RoomPostSearchTypeParams 및 RoomPostSearchSortParams 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.
| 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.
| 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); |
There was a problem hiding this comment.
🛠️ Refactor suggestion
애플리케이션 포트가 Web 어댑터의 Response 타입에 의존 — 계층 역전 발생
application.port.in이 adapter.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.
| 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; |
There was a problem hiding this comment.
💡 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"
doneLength 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"} ) )
- @table에 uniqueConstraints 추가
- 데이터베이스 마이그레이션 스크립트
- 기존 테이블에 UNIQUE 제약 추가 SQL
ALTER TABLE attendance_checks ADD CONSTRAINT uk_attendance_room_creator_date UNIQUE (room_id, creator_id, date);
- 기존 테이블에 UNIQUE 제약 추가 SQL
- 저장 로직 보강
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.
| 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); |
There was a problem hiding this comment.
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.
| 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; | |||
There was a problem hiding this comment.
💡 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 || trueLength 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
left a comment
There was a problem hiding this comment.
넘넘 좋네요!! 역시 문귀,문장,문악 답게 꼼꼼한 작업 리스펙힙니다!! 너무너무수고하셨어여!!
패키지 구조 보다가 하나 뭔가 생각이든건.. roomPost 밑에 record,vote,attendanceCheck 이런식으로 패키지를 하나 더 두는건 어떨까?라고도 생각해봣지만 그럼 어댑터,어플리케이션에서 괜히 패키지땜에 더헷갈리는거같기도하고.. roomPost 밑에 record,vote가 뭔가 많아서 잘 안읽히는 느낌이 들어서 한번 적어봣습니다 하핳
| @Target({ElementType.TYPE}) | ||
| @Retention(RetentionPolicy.RUNTIME) | ||
| @Component | ||
| public @interface DomainService { |
| package konkuk.thip.post.domain; | ||
|
|
||
| import konkuk.thip.post.domain.service.PostCountService; | ||
|
|
There was a problem hiding this comment.
오 밑에 TODO 패키지 구조 변경 주석 지워주시면 감사하겠습니다!!
| ); | ||
| } | ||
|
|
||
| public static PostType roomPostTypeFrom(String type) { |
There was a problem hiding this comment.
오 roomPostType 따로 정의한게 아니라 PostType안에서 roomPostType확인하는거 좋네여 FEED일 경우 예외처리도 넘넘좋습니다!!
seongjunnoh
left a comment
There was a problem hiding this comment.
굳굳 바뀐 구조 좋습니다!! 이전보다 훨씬 코드의 가독성이 좋아질 것 같네요!
| package konkuk.thip.post.domain; | ||
|
|
||
| import konkuk.thip.post.domain.service.PostCountService; | ||
|
|
#️⃣ 연관된 이슈
📝 작업 내용
패키지 구조를 다음과 같이 변경하였습니다.
roompost
post
추가적으로 저희가 정한 테스트 컨벤션에 맞게 모든 테스트 코드들을 한번 정리했습니다.
📸 스크린샷
💬 리뷰 요구사항
HelperService와 DomainService도 구분하기 위해 DomainService를 나타내는 어노테이션하나 추가했습니다. (ex. PostCountService)
추가적으로 변경되었으면 하는 부분 있다면 리뷰로 요청주세요!!
📌 PR 진행 시 이러한 점들을 참고해 주세요
Summary by CodeRabbit
신기능
변경/리팩터
문서/테스트