[feat] 피드 작성을 위한 화면 조회 api 및 기록을 피드에 핀하기 api 개발#195
Conversation
Walkthrough새 API 두 개가 추가됨: 피드 작성 화면 정보 조회(GET /feeds/write-info)와 기록 핀하기 정보 조회(GET /rooms/{roomId}/records/{recordId}/pin). 관련 포트·서비스·리포지토리·매퍼·응답 DTO·도메인 검증 메서드가 추가되었고 일부 서비스의 @transactional import가 Spring으로 교체됨. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant FeedQueryController
participant FeedShowWriteInfoUseCase
participant FeedQueryPort
participant Repository
participant Mapper
Client->>FeedQueryController: GET /feeds/write-info
FeedQueryController->>FeedShowWriteInfoUseCase: showFeedWriteInfo()
FeedShowWriteInfoUseCase->>FeedQueryPort: findAllTags()
FeedQueryPort->>Repository: findAllTags()
Repository-->>FeedQueryPort: List<TagCategoryQueryDto>
FeedShowWriteInfoUseCase->>Mapper: toTagsWithCategoryResult(rows)
Mapper-->>FeedShowWriteInfoUseCase: List<TagsWithCategoryResult>
FeedShowWriteInfoUseCase-->>FeedQueryController: FeedShowWriteInfoResponse
FeedQueryController-->>Client: 200 OK + BaseResponse
sequenceDiagram
participant Client
participant RecordQueryController
participant RecordPinUseCase
participant RoomParticipantValidator
participant RecordCommandPort
participant BookCommandPort
participant BookQueryMapper
Client->>RecordQueryController: GET /rooms/{roomId}/records/{recordId}/pin
RecordQueryController->>RecordPinUseCase: pinRecord(RecordPinQuery)
RecordPinUseCase->>RoomParticipantValidator: validateUserIsRoomMember(roomId, userId)
RecordPinUseCase->>RecordCommandPort: getByIdOrThrow(recordId)
RecordCommandPort-->>RecordPinUseCase: Record
RecordPinUseCase->>Record: validatePin(userId, roomId)
RecordPinUseCase->>BookCommandPort: findBookByRoomId(roomId)
BookCommandPort-->>RecordPinUseCase: Book
RecordPinUseCase->>BookQueryMapper: toBookSelectableResult(Book)
BookQueryMapper-->>RecordPinUseCase: BookSelectableResult
RecordPinUseCase-->>RecordQueryController: RecordPinResponse
RecordQueryController-->>Client: 200 OK + BaseResponse
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Assessment against linked issues
Out-of-scope changes
Possibly related PRs
Suggested reviewers
Poem
📜 Recent review detailsConfiguration used: CodeRabbit UI 📒 Files selected for processing (1)
🧰 Additional context used🧠 Learnings (1)📚 Learning: 2025-08-13T05:22:32.258ZApplied to files:
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
🔇 Additional comments (4)
✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 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 (
|
…-info # Conflicts: # src/main/java/konkuk/thip/feed/application/mapper/FeedQueryMapper.java
There was a problem hiding this comment.
Actionable comments posted: 12
🔭 Outside diff range comments (8)
src/main/java/konkuk/thip/book/application/service/BookSavedService.java (2)
26-28: 외부 API 호출을 트랜잭션 경계 밖으로 이동 권장changeSavedBook 내부에서 Naver API(bookApiQueryPort.findDetailBookByIsbn)를 호출하고 있어 트랜잭션이 불필요하게 길어지고 DB 리소스 점유/락 시간이 늘어날 수 있습니다. 외부 I/O는 실패/지연 가능성이 높으므로 트랜잭션 시작 전에 호출하고, 저장 단계만 짧은 쓰기 트랜잭션으로 감싸는 구조로 리팩터링을 권장합니다.
예시 접근:
- changeSavedBook 메서드에서 조건 분기 후, API 호출을 먼저 수행
- 실제 저장/연결만 별도 private 메서드에 @transactional 부여
Also applies to: 50-53
38-45: 경합 상황 대비:saved_books테이블에 복합 유니크 제약 및 예외 매핑 필요
현재SavedBookJpaEntity에 (user_id, book_id) 복합 유니크 제약이 없어, 동시 요청 시 중복 레코드가 생성될 수 있습니다. 아래와 같이 개선을 권장합니다.• 엔티티/DDL에 복합 유니크 제약 추가
SavedBookJpaEntity에와 같이 어노테이션 또는 마이그레이션 스크립트로 인덱스를 추가합니다.@Table( name = "saved_books", uniqueConstraints = @UniqueConstraint(columnNames = {"user_id", "book_id"}) )• 중복 저장 시 예외 처리 로직 추가
BookCommandPersistenceAdapter.saveSavedBook호출부에서와 같이 제약 위반 예외를 비즈니스 예외로 매핑하세요.try { savedBookJpaRepository.save(entity); } catch (DataIntegrityViolationException e) { throw new DuplicateSaveBookException("이미 저장된 책입니다."); }• (선택) 서비스 계층에서의 idempotent 처리
BookSavedService내save흐름을 idempotent하게 변경하거나, 위 비즈니스 예외를 그대로 핸들링해 적절한 응답을 반환하도록 합니다.src/main/java/konkuk/thip/comment/application/service/CommentDeleteService.java (1)
42-48: 댓글 수 동시성 제어 구현 필요 (TODO 처리 권장)댓글 수 감소는 경쟁 조건의 대표 포인트입니다. 현재 TODO만 존재하므로 낙관적 락(@Version) 또는 DB 원자 연산(update ... set comment_count = comment_count - 1 where id=? and comment_count > 0) 같은 확정적 동시성 제어를 구현하세요. 이벤트 기반 집계로 일관성을 유지하는 방안도 고려 가능합니다.
원하시면 PostHandler/CountUpdatable 경유가 아닌 저장소 레벨 원자 감소 메서드 시그니처와 예외 매핑까지 포함한 패치를 제안하겠습니다.
src/main/java/konkuk/thip/feed/application/service/FeedDeleteService.java (1)
30-31: S3 이미지 삭제는 트랜잭션 외부에서 이벤트 기반 처리 권장외부 I/O(S3)는 트랜잭션 밖에서 처리하세요. 도메인 이벤트 발행 → @TransactionalEventListener(phase = AFTER_COMMIT) 또는 아웃박스 패턴으로 비동기 삭제를 적용하면 일관성과 내고장성을 확보할 수 있습니다.
필요 시 Feed 삭제 이벤트/리스너, 아웃박스 엔티티, 퍼블리셔 견본 코드를 제공하겠습니다.
src/main/java/konkuk/thip/comment/application/service/CommentLikeService.java (1)
38-48: 좋아요 토글 동시성 및 카운트 정합성 강화 필요현재 댓글 좋아요 로직은 다음과 같은 경쟁 조건과 비정합 위험을 안고 있습니다.
- DB 레벨의 (user_id, comment_id) 유니크 제약이 없어 동시 중복 insert/delete 발생 가능
Comment.updateLikeCount도메인 기반 증감 후 전체 엔티티 저장 방식(commentCommandPort.update)은 비원자적 업데이트로 동시 요청 시 증감 손실아래 항목을 반영해 수정이 필요합니다.
• CommentLikeJpaEntity에 유니크 제약 추가
@Table( name = "comment_likes", uniqueConstraints = @UniqueConstraint(columnNames = {"user_id","comment_id"}) )• 댓글 좋아요 카운트는 DB 원자 연산 또는 낙관적 락 적용
예시:public interface CommentJpaRepository extends JpaRepository<CommentJpaEntity, Long> { @Modifying @Query("UPDATE CommentJpaEntity c SET c.likeCount = c.likeCount + :delta WHERE c.commentId = :id") void adjustLikeCount(@Param("id") Long id, @Param("delta") int delta); }• CommentLikeService에서
save/delete후 도메인 증감 대신 위 원자 메서드 호출점검 위치:
- src/main/java/konkuk/thip/comment/adapter/out/jpa/CommentLikeJpaEntity.java (
@Table설정)- src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentCommandPersistenceAdapter.java (update 로직)
- src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentJpaRepository.java (원자 증감 쿼리 추가)
- src/main/java/konkuk/thip/comment/domain/Comment.java (
updateLikeCount도메인 증감 로직)위 변경으로 동시성 안전성과 카운트 정합성을 확보해주세요.
src/main/java/konkuk/thip/feed/application/service/FeedSavedService.java (1)
12-41: (userId, feedId)에 대한 DB 유니크 제약 및 중복키 처리 보강 필요
현재 SavedFeed 테이블에(userId, feedId)조합에 대한 고유 제약이 없어 동시성 경쟁 시 중복 저장이 발생할 수 있습니다.
다음 사항을 반영해주세요:
SavedFeedJpaEntity에@Table(uniqueConstraints = @UniqueConstraint(columnNames = {"user_id","feed_id"}))추가하거나, 별도 DB 마이그레이션 스크립트로 UNIQUE 인덱스 생성FeedCommandPersistenceAdapter.saveSavedFeed에서 중복키 예외(예:DataIntegrityViolationException또는DuplicateKeyException)를 잡아 idempotent 하게 처리하는 방어 로직 구현src/main/java/konkuk/thip/post/application/service/PostLikeService.java (2)
31-47: isLiked 조회시 postType 미반영 — 교차 타입 충돌 가능성Line 37에서 좋아요 여부 조회가 userId+postId만으로 수행됩니다. 여러 PostType이 공존하고 동일한 postId가 다른 타입에도 존재할 경우 오탐/누락이 발생할 수 있습니다. 저장(save)은 postType을 전달하지만 조회는 전달하지 않아 불일치가 있습니다. 포트 시그니처를 postType까지 포함하도록 확장하세요.
다음 수정안을 적용해 주세요.
- 본 파일 (호출부) 수정:
- boolean alreadyLiked = postLikeQueryPort.isLikedPostByUser(command.userId(), command.postId()); + boolean alreadyLiked = postLikeQueryPort.isLikedPostByUser(command.userId(), command.postId(), command.postType());
- 포트/어댑터 시그니처 예시(다른 파일 수정 필요):
// PostLikeQueryPort.java boolean isLikedPostByUser(Long userId, Long postId, PostType postType); // JPA/Query 어댑터 예시 @Override public boolean isLikedPostByUser(Long userId, Long postId, PostType postType) { return repository.existsByUserIdAndPostIdAndPostType(userId, postId, postType); }
39-51: 좋아요 카운트 동시성 제어 보강 필요Line 40의 TODO대로, 동시 요청 시 카운트 꼬임이 발생할 수 있습니다. 아래 중 하나를 적용 권장합니다.
- 좋아요 테이블에 (userId, postType, postId) 유니크 인덱스 + DB 레벨 upsert/삭제 결과를 기반으로 카운트 증감
- 게시물 카운트 컬럼에 대한 낙관적 락(Version) + 재시도 정책
- 별도 카운트 테이블을 두고 원자적 UPDATE ... SET like_count = like_count ± 1
현재 구현에서는 updateLikeCount가 애플리케이션 레벨 계산이어서 레이스에 취약합니다.
🧹 Nitpick comments (12)
src/main/java/konkuk/thip/book/application/service/BookSavedService.java (1)
33-35: 에러 코드 불일치 가능성 확인삭제 요청 시 책 미저장 케이스에 대해 두 곳에서 서로 다른 에러 코드를 던집니다: BOOK_NOT_SAVED_DB_CANNOT_DELETE(라인 33) vs BOOK_NOT_SAVED_CANNOT_DELETE(라인 73). 의도된 구분인지, 도메인 정책상 동일 코드로 통일해야 하는지 확인 부탁드립니다.
Also applies to: 71-74
src/main/java/konkuk/thip/record/application/service/RecordDeleteService.java (1)
39-41: 주석 오타 수정 제안"피드 게시물 좋아요 삭제" → "기록 게시물 좋아요 삭제"로 의미를 명확히 해주세요.
적용 diff:
- // 3-2. 피드 게시물 좋아요 삭제 + // 3-2. 기록 게시물 좋아요 삭제src/main/java/konkuk/thip/record/domain/Record.java (2)
100-103: 오류 메시지 맞춤법/띄어쓰기 보정"핀 할"은 "핀할"로 붙여 쓰는 것이 자연스럽습니다.
다음처럼 수정하세요.
- throw new InvalidStateException(RECORD_ACCESS_FORBIDDEN, new IllegalArgumentException("기록 작성자만 기록을 수정/삭제/핀 할 수 있습니다.")); + throw new InvalidStateException(RECORD_ACCESS_FORBIDDEN, new IllegalArgumentException("기록 작성자만 기록을 수정/삭제/핀할 수 있습니다."));
117-120: validatePin이 validateDeletable과 동일 로직 — 중복 제거 권장두 메서드가 동일하게 roomId/creator를 검증합니다. 공통 헬퍼로 추출하여 중복을 제거하면 변경 비용이 줄어듭니다.
아래처럼 공통 메서드로 정리하세요.
- public void validatePin(Long userId,Long roomId) { - validateRoomId(roomId); - validateCreator(userId); - } + public void validatePin(Long userId, Long roomId) { + validateCreatorAndRoom(userId, roomId); + }이 변경을 지원하기 위해 클래스 내부에 다음 헬퍼를 추가하고, validateDeletable에서도 이를 사용하도록 변경을 제안합니다.
// 클래스 내부에 추가 (다른 메서드들과 동일 접근제어자 컨벤션 유지) private void validateCreatorAndRoom(Long userId, Long roomId) { validateRoomId(roomId); validateCreator(userId); } // 선택: 기존 메서드도 공통 헬퍼 사용 public void validateDeletable(Long userId, Long roomId) { validateCreatorAndRoom(userId, roomId); }src/main/java/konkuk/thip/feed/application/port/out/FeedQueryPort.java (1)
43-44: 포트에서 DB 프로젝션 DTO 노출 최소화 검토현재 포트가 List (QueryDSL 프로젝션 성격)를 그대로 노출합니다. 선택지는 두 가지입니다:
- 유지: 현 구조 유지 시, 구현에서 정렬/중복 제거를 보장하고, 애플리케이션 서비스가 그룹핑(FeedQueryMapper)을 담당합니다.
- 개선(권장): 그룹핑(카테고리별 태그 리스트)을 Persistence Adapter/Repository 레이어로 이동하고, 포트는 List를 반환하도록 변경해 애플리케이션 서비스가 영속성 세부에 덜 의존하도록 분리합니다.
둘 중 어떤 방식을 택하든, 메서드 계약(정렬, 중복, 비어있는 카테고리 처리)을 JavaDoc로 명확히 해 주세요.
src/test/java/konkuk/thip/record/domain/RecordTest.java (1)
233-237: 테스트 메서드 네이밍 일관성 유지 제안동일 파일 내에서 Success/SUCCESS 표기 혼재가 있습니다. 기존 validateDeletable_byCreator_byRoomId_Success와 맞추어 대문자 S로 통일하는 편이 좋습니다.
아래와 같이 메서드명을 통일해 주세요:
- void validatePin_byCreator_byRoomId_success() { + void validatePin_byCreator_byRoomId_Success() {src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedQueryPersistenceAdapter.java (1)
152-155: findAllTags 위임은 적절하나, 메서드 명시성과 캐싱 고려 필요
- 명확성: 반환이 “카테고리-태그 매핑용 행”이라면 메서드명을 findAllTagsWithCategory 정도로 더 구체화하면 의도가 분명합니다. 현재 명명은 태그 엔터티 전체 조회로 오해될 수 있습니다.
- 성능: 화면 렌더링용으로 전 태그/카테고리를 매 요청마다 로드한다면 캐싱(예: Spring Cache) 혹은 변경 감지 기반 무효화 전략 고려가 필요합니다. 데이터 규모가 커지면 API 레이턴시가 증가할 수 있습니다.
필수 변경은 아니나, API 호출 빈도가 높을 경우 캐시 도입을 권장합니다.
src/main/java/konkuk/thip/feed/adapter/in/web/response/FeedShowWriteInfoResponse.java (1)
10-12: 응답 리스트를 불변/널-세이프로 감싸기외부에서 전달된 리스트가 변경되어도 안전하도록 방어적 복사를 권장합니다. 또한 null 입력 시 빈 리스트로 대체하면 클라이언트 처리 부담이 줄어듭니다.
- public static FeedShowWriteInfoResponse of(List<TagsWithCategoryResult> categoryList) { - return new FeedShowWriteInfoResponse(categoryList); - } + public static FeedShowWriteInfoResponse of(List<TagsWithCategoryResult> categoryList) { + return new FeedShowWriteInfoResponse( + categoryList == null ? java.util.List.of() : java.util.List.copyOf(categoryList) + ); + }src/test/java/konkuk/thip/feed/adapter/in/web/FeedShowWriteInfoAPITest.java (1)
71-84: 테스트 견고성 강화: 태그 개수 검증 추가 및 순서 의존성 완화 고려현재 테스트는 태그 순서를 인덱스로 단정하지만(비결정적 순서가 될 수 있음), 저장소/매퍼에서 정렬·순서를 보장하지 않으면 플래키해질 수 있습니다. 저장소/매퍼에 정렬/LinkedHashMap 적용을 병행하면서, 테스트에도 태그 개수 검증을 추가해 의도를 명확히 하세요.
아래 diff로 태그 리스트 크기 검증을 추가할 수 있습니다.
.andExpect(jsonPath("$.data.categoryList", hasSize(2))) .andExpect(jsonPath("$.data.categoryList[0].category", is(literatureCategory.getValue()))) + .andExpect(jsonPath("$.data.categoryList[0].tagList", hasSize(3))) .andExpect(jsonPath("$.data.categoryList[0].tagList[0]", is(KOREAN_NOVEL.getValue()))) .andExpect(jsonPath("$.data.categoryList[0].tagList[1]", is(FOREIGN_NOVEL.getValue()))) .andExpect(jsonPath("$.data.categoryList[0].tagList[2]", is(CLASSIC_LITERATURE.getValue()))) .andExpect(jsonPath("$.data.categoryList[1].category", is(scienceCategory.getValue()))) + .andExpect(jsonPath("$.data.categoryList[1].tagList", hasSize(3))) .andExpect(jsonPath("$.data.categoryList[1].tagList[0]", is(GENERAL_SCIENCE.getValue()))) .andExpect(jsonPath("$.data.categoryList[1].tagList[1]", is(PHYSICS.getValue()))) .andExpect(jsonPath("$.data.categoryList[1].tagList[2]", is(CHEMISTRY.getValue())));만약 저장소/매퍼에서 순서 보장을 하지 않기로 결정한다면, containsInAnyOrder 매처 사용으로 순서 의존성을 제거하는 것도 대안입니다.
src/main/java/konkuk/thip/feed/adapter/in/web/FeedQueryController.java (1)
101-108: 새 엔드포인트 추가 적절. 캐시 고려 제안정적에 가까운 카테고리/태그 매핑 조회 API 특성상, 서비스 레이어에 캐시(@Cacheable) 도입을 고려하면 DB 부하를 크게 줄일 수 있습니다. 변경 빈도가 낮다면 TTL/수동 무효화 전략으로 충분합니다.
src/main/java/konkuk/thip/record/adapter/in/web/RecordQueryController.java (1)
70-81: 엔드포인트 설계 타당. 메서드 명칭을 의도에 맞게 구체화 제안GET은 상태 변경이 없고 검증/조회 기능만 수행하므로 적절합니다. 다만 메서드명이 pinRecord라서 실제로 핀을 수행한다고 오해될 수 있습니다. 의미 전달을 위해 메서드명을
getRecordPinInfo정도로 변경하는 것을 제안합니다(경로는 유지).다음과 같이 메서드명만 수정 가능합니다:
- public BaseResponse<RecordPinResponse> pinRecord( + public BaseResponse<RecordPinResponse> getRecordPinInfo(src/main/java/konkuk/thip/record/application/service/RecordPinService.java (1)
36-36: 사소한 스타일: 인자 간 공백가독성을 위해 콤마 뒤 공백을 권장합니다.
- record.validatePin(query.userId(),query.roomId()); + record.validatePin(query.userId(), query.roomId());
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (29)
src/main/java/konkuk/thip/book/application/service/BookSavedService.java(1 hunks)src/main/java/konkuk/thip/comment/application/service/CommentDeleteService.java(1 hunks)src/main/java/konkuk/thip/comment/application/service/CommentLikeService.java(1 hunks)src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java(1 hunks)src/main/java/konkuk/thip/feed/adapter/in/web/FeedQueryController.java(3 hunks)src/main/java/konkuk/thip/feed/adapter/in/web/response/FeedShowWriteInfoResponse.java(1 hunks)src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedQueryPersistenceAdapter.java(2 hunks)src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepository.java(2 hunks)src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepositoryImpl.java(2 hunks)src/main/java/konkuk/thip/feed/application/mapper/FeedQueryMapper.java(2 hunks)src/main/java/konkuk/thip/feed/application/port/in/FeedShowWriteInfoUseCase.java(1 hunks)src/main/java/konkuk/thip/feed/application/port/in/dto/TagsWithCategoryResult.java(1 hunks)src/main/java/konkuk/thip/feed/application/port/out/FeedQueryPort.java(2 hunks)src/main/java/konkuk/thip/feed/application/port/out/dto/TagCategoryQueryDto.java(1 hunks)src/main/java/konkuk/thip/feed/application/service/FeedDeleteService.java(1 hunks)src/main/java/konkuk/thip/feed/application/service/FeedSavedService.java(1 hunks)src/main/java/konkuk/thip/feed/application/service/FeedShowWriteInfoService.java(1 hunks)src/main/java/konkuk/thip/post/application/service/PostLikeService.java(1 hunks)src/main/java/konkuk/thip/record/adapter/in/web/RecordQueryController.java(3 hunks)src/main/java/konkuk/thip/record/adapter/in/web/response/RecordPinResponse.java(1 hunks)src/main/java/konkuk/thip/record/application/port/in/RecordPinUseCase.java(1 hunks)src/main/java/konkuk/thip/record/application/port/in/dto/RecordPinQuery.java(1 hunks)src/main/java/konkuk/thip/record/application/service/RecordCreateService.java(2 hunks)src/main/java/konkuk/thip/record/application/service/RecordDeleteService.java(1 hunks)src/main/java/konkuk/thip/record/application/service/RecordPinService.java(1 hunks)src/main/java/konkuk/thip/record/domain/Record.java(2 hunks)src/test/java/konkuk/thip/feed/adapter/in/web/FeedShowWriteInfoAPITest.java(1 hunks)src/test/java/konkuk/thip/record/adapter/in/web/RecordPinAPITest.java(1 hunks)src/test/java/konkuk/thip/record/domain/RecordTest.java(1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#101
File: src/main/java/konkuk/thip/comment/application/service/CommentCreateService.java:36-39
Timestamp: 2025-07-26T06:09:00.850Z
Learning: THIP 프로젝트에서 Record와 Vote는 Room에 속하지만 Feed는 Room에 속하지 않는 구조이며, 댓글 작성 시 Record/Vote에 대해서만 사용자가 해당 Room의 참가자인지 검증이 필요하다.
📚 Learning: 2025-07-26T06:09:00.850Z
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#101
File: src/main/java/konkuk/thip/comment/application/service/CommentCreateService.java:36-39
Timestamp: 2025-07-26T06:09:00.850Z
Learning: THIP 프로젝트에서는 "사용자가 방에 속하는지 검증" 로직을 RoomParticipantPolicy 도메인 서비스로 캡슐화하여 재사용성을 높이고 비즈니스 로직의 중복을 방지하는 방식을 선호한다.
Applied to files:
src/main/java/konkuk/thip/record/application/service/RecordCreateService.java
🧬 Code Graph Analysis (3)
src/test/java/konkuk/thip/record/adapter/in/web/RecordPinAPITest.java (1)
src/test/java/konkuk/thip/common/util/TestEntityFactory.java (1)
TestEntityFactory(35-361)
src/test/java/konkuk/thip/feed/adapter/in/web/FeedShowWriteInfoAPITest.java (1)
src/test/java/konkuk/thip/common/util/TestEntityFactory.java (1)
TestEntityFactory(35-361)
src/main/java/konkuk/thip/record/adapter/in/web/RecordQueryController.java (1)
src/main/java/konkuk/thip/record/adapter/in/web/RecordCommandController.java (1)
Tag(21-58)
🔇 Additional comments (26)
src/main/java/konkuk/thip/book/application/service/BookSavedService.java (1)
13-13: Spring @transactional 전환 적절jakarta → Spring 트랜잭션 애노테이션 전환은 일관성 측면과 readOnly 등 Spring 확장 옵션 활용 측면에서 적절합니다.
src/main/java/konkuk/thip/comment/application/service/CommentDeleteService.java (1)
12-12: Spring @transactional 전환 적절트랜잭션 애노테이션 전환 자체는 문제 없습니다. deleteComment는 쓰기 트랜잭션이어야 하므로 기본 설정(readOnly=false)도 적절합니다.
src/main/java/konkuk/thip/record/application/service/RecordDeleteService.java (2)
12-12: Spring @transactional 전환 적절deleteRecord는 쓰기 작업이므로 기본 트랜잭션 설정으로 적절합니다.
28-35: 도메인/권한 검증 흐름 적절Room 참여자 검증 후 기록 삭제 권한 검증 흐름은 프로젝트 학습사항(Record/Vote는 Room 소속, Feed는 아님)에 부합합니다.
src/main/java/konkuk/thip/feed/application/service/FeedDeleteService.java (2)
10-10: Spring @transactional 전환 적절삭제 플로우에 쓰기 트랜잭션 적용 적절합니다.
10-10: jakarta.transaction.Transactional 사용 검토 완료rg를 통해
jakarta.transaction.Transactional패키지 사용 여부를 전역 검색한 결과, 잔여 사용이 발견되지 않았습니다. 추가 조치가 필요 없습니다.src/main/java/konkuk/thip/comment/application/service/CommentLikeService.java (1)
15-15: Spring @transactional 전환 적절좋아요 토글은 쓰기 트랜잭션 필요하므로 전환이 적절합니다.
src/main/java/konkuk/thip/feed/application/service/FeedSavedService.java (1)
12-12: Spring @transactional로의 마이그레이션 적합jakarta → Spring의 트랜잭션 애노테이션 변경은 일관성 및 세밀한 옵션 활용 측면에서 적절합니다. 다른 서비스들과 동일 패턴 유지도 👍
src/main/java/konkuk/thip/post/application/service/PostLikeService.java (1)
14-14: Spring @transactional 사용으로의 통일 👍Spring 트랜잭션 애노테이션 사용으로 옵션 호환성이 좋아집니다. 다른 모듈과의 일관성도 확보되었습니다.
src/main/java/konkuk/thip/record/application/service/RecordCreateService.java (3)
18-18: Spring @transactional로 변경된 점 확인트랜잭션 애노테이션을 Spring으로 통일한 변경, 문제 없습니다.
36-41: 트랜잭션 경계 지정 및 룸 멤버십 검증 호출 방식 적절메서드 레벨 @transactional 적용은 본 유즈케이스에 적합합니다. 또한 RoomParticipantValidator를 통해 룸 멤버십을 검증하는 패턴은 팀 선호(학습 노트: RoomParticipantPolicy로의 캡슐화)와 부합합니다.
50-55: Book 조회 안전성은 이미 보장됩니다
BookCommandPort.findById(...)의 구현(BookCommandPersistenceAdapter)에서bookJpaRepository.findById(id).orElseThrow(...)을 호출하므로, 조회 실패 시 예외가 발생하고null이 반환되지 않습니다. 또한getByIdOrThrow스타일의 메서드는BookCommandPort에 존재하지 않습니다.
– 해당 제안은 불필요하므로 무시하셔도 좋습니다.Likely an incorrect or invalid review comment.
src/main/java/konkuk/thip/feed/application/port/out/dto/TagCategoryQueryDto.java (1)
1-15: QueryDSL 5.0.0 + APT 설정 검증 완료
- build.gradle에 이미 다음 의존성과 APT 설정이 적용되어 있습니다.
- implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
- annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta"
- build/generated/querydsl 디렉터리가 main.java.srcDirs에 포함되어 있어 QType 생성 경로가 올바르게 설정되어 있습니다.
- QueryDSL 5.x부터 record의 canonical constructor에 @QueryProjection 사용을 공식적으로 지원하므로, 추가 조치 없이 정상적으로 QTagCategoryQueryDto가 생성됩니다.
src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepository.java (1)
3-3: 새 DTO import 추가 적절findAllTags 도입에 따른 DTO import가 올바르게 반영되었습니다.
src/main/java/konkuk/thip/feed/application/port/out/FeedQueryPort.java (1)
5-5: 포트 의존성 import 추가 적절TagCategoryQueryDto, List import 추가가 변경 사항과 일치하며 문제 없습니다.
Also applies to: 9-9
src/test/java/konkuk/thip/record/domain/RecordTest.java (1)
209-237: validatePin 도메인 검증 케이스 3종(작성자 아님/roomId 불일치/성공) 커버 양호핵심 제약을 간결히 검증하고 있으며, 에러 코드(RECORD_ACCESS_FORBIDDEN)도 일관되게 점검하고 있어 좋습니다.
src/main/java/konkuk/thip/record/application/port/in/RecordPinUseCase.java (1)
3-7: Query API의 직접 DTO 참조는 팀 컨벤션에 부합합니다.
THIP 프로젝트의 CQRS 아키텍처에서는 조회 전용 application 계층(use case)이 adapter.in.web.response 패키지의 DTO를 직접 반환하는 것을 허용하고 있으므로, RecordPinUseCase의 RecordPinResponse 반환은 의도된 설계입니다. 해당 포트의 반환형 변경 제안은 불필요합니다.Likely an incorrect or invalid review comment.
src/main/java/konkuk/thip/feed/application/port/in/FeedShowWriteInfoUseCase.java (1)
3-7: 계층 분리 위반: Application 포트 레이어가 웹 어댑터 타입에 의존함
application.port.in 및 port.out 모듈에서adapter.in.web.response패키지의 DTO를 import/반환하고 있습니다. 포트 레이어는 웹 어댑터를 몰라야 하며, 애플리케이션 전용 DTO(application.port.in.dto)로 반환형을 변경한 뒤, 컨트롤러에서 웹 응답 DTO로 감싸는 리팩터링이 필요합니다.확인된 주요 위치 (rg 스크립트 결과):
- feed/application/port/in/*.java
- feed/application/port/out/*.java
- room/application/port/in/*.java
- room/application/port/out/*.java
- user, vote, record, comment, recentSearch 등 다수의 모듈
예시:
FeedShowWriteInfoUseCase-import konkuk.thip.feed.adapter.in.web.response.FeedShowWriteInfoResponse; +import konkuk.thip.feed.application.port.in.dto.TagsWithCategoryResult; public interface FeedShowWriteInfoUseCase { - FeedShowWriteInfoResponse showFeedWriteInfo(); + java.util.List<TagsWithCategoryResult> showFeedWriteInfo(); }컨트롤러에서는 반환된 DTO 리스트를
FeedShowWriteInfoResponse.of(tagsWithCategoryResultList)처럼 변환해 사용해주세요.
포트 전반에 걸친 어댑터 의존 제거 및 application DTO 활용으로 구조를 개선해야 합니다.
⛔ Skipped due to learnings
Learnt from: buzz0331 PR: THIP-TextHip/THIP-Server#78 File: src/main/java/konkuk/thip/user/application/port/out/FollowingQueryPort.java:3-3 Timestamp: 2025-07-14T18:22:56.538Z Learning: THIP 프로젝트에서는 Query API(조회 API)에 한해서는 application 계층에서 adapter.in.web.response 패키지의 response DTO를 직접 참조하는 것을 허용함. 이는 CQRS 아키텍처에서 읽기 전용 작업의 효율성을 위한 팀 컨벤션임.src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java (1)
139-144: RECORD_PIN API에서 ROOM_NOT_FOUND/USER_NOT_FOUND 미발생으로 Swagger 수정 불필요
현재RecordPinService#pinRecord흐름을 보면:
- 방 존재 여부를 별도로 조회하지 않고
RoomParticipantValidator.validateUserIsRoomMember→ 존재하지 않거나 미참여 시 ROOM_ACCESS_FORBIDDEN- 기록 조회
getByIdOrThrow→ RECORD_NOT_FOUND- 권한 검사
record.validatePin→ RECORD_ACCESS_FORBIDDEN
따라서RECORD_PIN응답 정의에ROOM_NOT_FOUND혹은USER_NOT_FOUND를 추가할 이유가 없습니다.Likely an incorrect or invalid review comment.
src/main/java/konkuk/thip/feed/application/service/FeedShowWriteInfoService.java (1)
14-16: 간결한 유스케이스 구현 및 트랜잭션 설정이 적절합니다의존성 주입과 @transactional(readOnly = true) 설정으로 조회 유스케이스에 맞게 잘 구성되었습니다.
src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepositoryImpl.java (1)
11-16: 임포트 변경 사항 문제 없음기능적 영향이 없는 임포트 변경으로 보입니다.
src/main/java/konkuk/thip/record/adapter/in/web/response/RecordPinResponse.java (1)
5-17: 단순하고 명확한 DTO 변환 팩토리 메서드 설계 좋습니다레코드 기반 응답과 정적 팩토리로의 매핑이 간결합니다. 현재 요구사항에 적합해 보입니다.
src/main/java/konkuk/thip/feed/adapter/in/web/FeedQueryController.java (2)
28-28: use case 주입 추가 적절@requiredargsconstructor로 안전하게 주입되며 책임 분리가 명확합니다.
10-10: 와일드카드 import 사용 규칙 확인 필요프로젝트의 Checkstyle/Spotless 설정에서
import ...*허용 여부가 다를 수 있습니다. 빌드 파이프라인에서 실패하지 않는지 확인해주세요. 필요 시 명시적 import로 되돌리는 편이 안전합니다.src/main/java/konkuk/thip/record/application/service/RecordPinService.java (2)
27-41: 검증 순서 적절하며 정보 노출 최소화에 유리방 참여자 검증 → 기록 존재/권한 검증 → 책 조회 순으로 구성돼 비참여자에게 리소스 존재 여부를 누설하지 않습니다. 프로젝트 러닝(Record/Vote는 Room 소속이고 관련 작업 시 Room 참가자 검증 필요)에 부합합니다.
3-5: CQRS 관점에서 QueryPort 사용 권장 (CommandPort 오남용 개선)조회 용도의
findBookByRoomId를BookCommandPort에서 호출하고 있습니다. 읽기 전용 유스케이스이므로BookQueryPort사용이 더 명확합니다. 포트가 존재한다면 아래와 같이 교체를 권장합니다.-import konkuk.thip.book.application.port.out.BookCommandPort; +import konkuk.thip.book.application.port.out.BookQueryPort; @@ - private final BookCommandPort bookCommandPort; + private final BookQueryPort bookQueryPort; @@ - Book book = bookCommandPort.findBookByRoomId(query.roomId()); + Book book = bookQueryPort.findBookByRoomId(query.roomId());포트가 없다면 생성해 분리하거나, 최소한 메서드 명/포트 명을 Query 성격에 맞게 리네이밍하여 의도를 드러내 주세요.
Also applies to: 21-22, 39-39
⛔ Skipped due to learnings
Learnt from: seongjunnoh PR: THIP-TextHip/THIP-Server#43 File: src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java:0-0 Timestamp: 2025-07-03T03:05:05.031Z Learning: THIP 프로젝트에서는 CQRS Port 분리 시 다음 컨벤션을 따름: CommandPort에는 findByXXX를 통해 도메인 엔티티를 찾아오는 메서드를 추가하고, QueryPort에는 조회 API의 response에 해당하는 데이터들을 DB로부터 조회하는 메서드를 추가함.
There was a problem hiding this comment.
Actionable comments posted: 1
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepositoryImpl.java(2 hunks)src/main/java/konkuk/thip/feed/application/mapper/FeedQueryMapper.java(2 hunks)src/main/java/konkuk/thip/feed/application/service/FeedShowWriteInfoService.java(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- src/main/java/konkuk/thip/feed/application/service/FeedShowWriteInfoService.java
- src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepositoryImpl.java
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (2)
src/main/java/konkuk/thip/feed/application/mapper/FeedQueryMapper.java (2)
108-109: LinkedHashMap 사용으로 카테고리 표시 순서 보존: 좋습니다DB에서 넘어오는 순서를 그대로 유지할 수 있도록 LinkedHashMap을 지정하신 점 적절합니다.
112-115: Stream.toList()는 Java 17에서도 지원됩니다.
build.gradle의 Gradle Toolchain 설정이JavaLanguageVersion.of(17)으로 되어 있어 Stream.toList() 사용에 호환성 이슈가 없습니다.
| public record FeedShowWriteInfoResponse( | ||
| List<TagsWithCategoryResult> categoryList | ||
| ) { |
There was a problem hiding this comment.
p3 : response 가 result를 의존하여 내부 데이터를 구성하는게 아니라, result를 내부 필드로 직접적으로 가지고 있는 것이라면, 그냥 response 내부에서 String category, List tagList 를 inner class 로 가지고 있는게 더 낫지 않나 생각합니다!
그런데 뭐 사소하니 넘어가도 좋을 것 같습니다!
| } | ||
|
|
||
| public void validatePin(Long userId,Long roomId) { | ||
| validateRoomId(roomId); |
There was a problem hiding this comment.
이런 메서드가 있는줄은 몰랐네여 좋습니다!
|
|
||
| public void validatePin(Long userId,Long roomId) { | ||
| validateRoomId(roomId); | ||
| validateCreator(userId); |
buzz0331
left a comment
There was a problem hiding this comment.
굿굿 코드가 깔끔하네여~~ 사소한 리뷰 하나 남겼습니다
| public record RecordPinResponse( | ||
| String bookTitle, | ||
| String authorName, | ||
| String bookImageUrl, | ||
| String isbn | ||
| ) { | ||
| static public RecordPinResponse of(BookSelectableResult bookSelectableResult) { | ||
| return new RecordPinResponse( | ||
| bookSelectableResult.bookTitle(), | ||
| bookSelectableResult.authorName(), | ||
| bookSelectableResult.bookImageUrl(), | ||
| bookSelectableResult.isbn()); | ||
| } |
There was a problem hiding this comment.
엇 이거 저번에 한 api당 result도 각각 나눠서 쓰기로 했던 것 같은데 RecordPinResult 같은 걸로 주입 받는게 어떨까욥
There was a problem hiding this comment.
아하 저는 재사용하려고해서 쓴거긴한데 일단 배포해두고 추후에 수정하겠습니닷
| default List<TagsWithCategoryResult> toTagsWithCategoryResult(List<TagCategoryQueryDto> rows) { | ||
| Map<String, List<String>> grouped = rows.stream() | ||
| .collect(Collectors.groupingBy( | ||
| TagCategoryQueryDto::categoryValue, | ||
| LinkedHashMap::new, // 순서 보장 | ||
| Collectors.mapping(TagCategoryQueryDto::tagValue, Collectors.toList()) | ||
| )); |
#️⃣ 연관된 이슈
📝 작업 내용
📸 스크린샷
💬 리뷰 요구사항
피드 작성을 위한 화면 조회가 사실 DB에 존재하는 태그들을 상위 카테고리에 매핑하여 보여주는 api인데 Category가 Room도메인 하위 밸류고, Tag가 feed도메인 하위 밸류라 패키지 구조라던가 클래스명이 어색할 수 있는데 제생각에 제일 최적..해서 작성했습니다 ㅎ.. 좋은의견이 있다면 의견부탁드리겠습니다.
📌 PR 진행 시 이러한 점들을 참고해 주세요
Summary by CodeRabbit