Skip to content

[feat] 기록 삭제 api 개발#178

Merged
hd0rable merged 29 commits intodevelopfrom
feat/#175-record-delete
Aug 11, 2025
Merged

[feat] 기록 삭제 api 개발#178
hd0rable merged 29 commits intodevelopfrom
feat/#175-record-delete

Conversation

@hd0rable
Copy link
Member

@hd0rable hd0rable commented Aug 8, 2025

#️⃣ 연관된 이슈

closes #175

📝 작업 내용

  • 기록 삭제 api를 개발했습니다.
  • 피드 삭제 커밋과 섞여있습니다. 해당 브랜치 머지되면 pull 받아서 반영해놓겠습니다
  • 기록 삭제 api 흐름은 다음과 같습니다.
  • 컨트롤러 에서 커맨드로 변환 -> 기록 삭제 서비스 진입 -> 방 참여자인지 검증, 기록 조회 및 검증, 기록이 해당 방에 대한 기록인지 검증, 기록 작성자인지 검증 -> 기록 삭제 -> 방 아이디 반환 -> 응답
  • 관련 통합,단위 테스트 코드 작성했습니다

📸 스크린샷

image

💬 리뷰 요구사항

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

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

Summary by CodeRabbit

  • 신규 기능

    • 피드 및 기록(레코드) 삭제 API가 추가되어 사용자가 자신의 피드와 기록을 삭제할 수 있습니다.
    • 삭제 시 댓글, 좋아요, 태그, 저장 피드 등 연관된 데이터가 함께 삭제(또는 비활성화)됩니다.
  • 버그 수정

    • 삭제 및 연관 엔티티 처리 시 일관성을 높이기 위해 삭제 쿼리 및 연관 데이터 삭제 방식이 개선되었습니다.
  • 테스트

    • 피드 및 기록 삭제 API의 동작을 검증하는 통합 테스트와 도메인 삭제 권한 검증 테스트가 추가되었습니다.
    • 일부 테스트에서 대량 삭제(batch delete) 방식으로 성능이 개선되었습니다.
  • 문서화

    • Swagger 응답 설명 및 오류 코드가 삭제 기능에 맞게 확장되었습니다.
  • 기타

    • 불필요한 더미 DTO가 제거되었습니다.
    • 일부 JPA 쿼리의 플러시/클리어 옵션이 추가되어 데이터 일관성이 향상되었습니다.

@coderabbitai
Copy link

coderabbitai bot commented Aug 8, 2025

Warning

Rate limit exceeded

@hd0rable has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 9 minutes and 20 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 72c85b7 and 21f7a9d.

📒 Files selected for processing (1)
  • src/test/java/konkuk/thip/feed/adapter/in/web/FeedDeleteAPITest.java (1 hunks)

Walkthrough

이 변경 사항은 기록(Record) 및 피드(Feed) 삭제 API의 도입 및 관련 도메인, JPA, 서비스 계층의 확장에 중점을 둡니다. 삭제 시 연관된 댓글, 좋아요, 태그, 저장된 엔티티 등을 일관성 있게 소프트 삭제 또는 영구 삭제하도록 처리하는 로직과, 삭제 권한 검증, 예외 코드 추가, 통합 테스트 및 단위 테스트 추가가 포함되어 있습니다.

Changes

Cohort / File(s) Change Summary
Record 삭제 API 및 서비스 계층
src/main/java/konkuk/thip/record/adapter/in/web/RecordCommandController.java, .../RecordDeleteResponse.java, .../RecordDeleteUseCase.java, .../RecordDeleteCommand.java, .../RecordDeleteService.java, .../RecordCommandPort.java, .../RecordJpaEntity.java, .../RecordCommandPersistenceAdapter.java, .../RecordJpaRepository.java, .../Record.java
기록 삭제 API 엔드포인트, DTO, 유스케이스, 서비스, 포트, JPA 엔티티, 영속성 어댑터, 레포지토리 및 도메인에 삭제 및 검증 로직 추가
Feed 삭제 API 및 서비스 계층
src/main/java/konkuk/thip/feed/adapter/in/web/FeedCommandController.java, .../FeedDeleteUseCase.java, .../FeedDeleteService.java, .../FeedCommandPort.java, .../FeedJpaEntity.java, .../FeedCommandPersistenceAdapter.java, .../FeedJpaRepository.java, .../repository/Content/ContentJpaRepository.java, .../repository/FeedTag/FeedTagJpaRepository.java, .../repository/SavedFeedJpaRepository.java, .../Feed.java
피드 삭제 API 엔드포인트, 유스케이스, 서비스, 포트, JPA 엔티티, 영속성 어댑터, 레포지토리, 도메인에 삭제 및 검증 로직, 연관 엔티티 삭제 처리 추가
댓글 및 좋아요 일괄 삭제 지원
src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentCommandPersistenceAdapter.java, .../repository/CommentJpaRepository.java, .../repository/CommentLikeJpaRepository.java, .../comment/application/port/out/CommentCommandPort.java, src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeCommandPersistenceAdapter.java, .../PostLikeJpaRepository.java, .../post/application/port/out/PostLikeCommandPort.java, src/main/java/konkuk/thip/comment/adapter/out/jpa/CommentJpaEntity.java, src/main/java/konkuk/thip/post/adapter/out/jpa/PostJpaEntity.java
댓글, 댓글 좋아요, 게시글 좋아요의 일괄 삭제/소프트 삭제 기능 및 연관관계 매핑 추가, 관련 메서드 및 JPA 어노테이션 개선
공통 예외 및 스웨거 설명 확장
src/main/java/konkuk/thip/common/exception/code/ErrorCode.java, src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java
삭제/접근 관련 예외 코드 및 스웨거 응답 설명 enum 추가
테스트 코드 추가 및 개선
src/test/java/konkuk/thip/feed/adapter/in/web/FeedDeleteAPITest.java, src/test/java/konkuk/thip/feed/domain/FeedTest.java, src/test/java/konkuk/thip/record/adapter/in/web/RecordDeleteAPITest.java, src/test/java/konkuk/thip/record/domain/RecordTest.java, src/test/java/konkuk/thip/comment/adapter/in/web/CommentDeleteAPITest.java, src/test/java/konkuk/thip/book/adapter/in/web/BookDetailSearchControllerTest.java, src/test/java/konkuk/thip/room/adapter/in/web/RoomPlayingDetailViewApiTest.java, src/test/java/konkuk/thip/vote/adapter/in/web/VoteCreateControllerTest.java
기록/피드 삭제 API 통합 테스트, 도메인 삭제 검증 단위 테스트 추가, 기존 테스트의 일괄 삭제 방식 개선 및 검증 로직 단순화
불필요 DTO 삭제
src/main/java/konkuk/thip/record/application/port/in/dto/DummyQuery.java, src/main/java/konkuk/thip/record/application/port/in/dto/DummyResult.java
불필요한 더미 DTO 클래스 삭제
JPA @Modifying 옵션 개선
src/main/java/konkuk/thip/book/adapter/out/persistence/repository/SavedBookJpaRepository.java, src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/repository/RecentSearchJpaRepository.java
JPA 수정 쿼리의 @Modifying에 clear/flush 옵션 추가

Sequence Diagram(s)

기록(Record) 삭제 플로우

sequenceDiagram
    participant User
    participant RecordCommandController
    participant RecordDeleteService
    participant RoomParticipantValidator
    participant RecordCommandPort
    participant CommentCommandPort
    participant PostLikeCommandPort

    User->>RecordCommandController: DELETE /rooms/{roomId}/record/{recordId}
    RecordCommandController->>RecordDeleteService: deleteRecord(command)
    RecordDeleteService->>RoomParticipantValidator: validateParticipant(userId, roomId)
    RecordDeleteService->>RecordCommandPort: findById(recordId)
    RecordDeleteService->>Record: validateDeletable(userId, roomId)
    RecordDeleteService->>CommentCommandPort: softDeleteAllByPostId(recordId)
    RecordDeleteService->>PostLikeCommandPort: deleteAllByPostId(recordId)
    RecordDeleteService->>RecordCommandPort: delete(record)
    RecordDeleteService-->>RecordCommandController: roomId 반환
    RecordCommandController-->>User: BaseResponse 반환
Loading

피드(Feed) 삭제 플로우

sequenceDiagram
    participant User
    participant FeedCommandController
    participant FeedDeleteService
    participant FeedCommandPort
    participant CommentCommandPort
    participant PostLikeCommandPort

    User->>FeedCommandController: DELETE /feeds/{feedId}
    FeedCommandController->>FeedDeleteService: deleteFeed(feedId, userId)
    FeedDeleteService->>FeedCommandPort: findById(feedId)
    FeedDeleteService->>Feed: validateDeletable(userId)
    FeedDeleteService->>CommentCommandPort: softDeleteAllByPostId(feedId)
    FeedDeleteService->>PostLikeCommandPort: deleteAllByPostId(feedId)
    FeedDeleteService->>FeedCommandPort: delete(feed)
    FeedDeleteService-->>FeedCommandController: void 반환
    FeedCommandController-->>User: BaseResponse 반환
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Assessment against linked issues

Objective Addressed Explanation
기록 삭제 API 개발 (#175)
Task1, Task2 (세부 체크리스트) 상세 Task1, Task2의 구체적 내용이 명시되어 있지 않아 충족 여부를 판단할 수 없음.

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
피드 삭제 API 및 서비스 계층 추가 (src/main/java/konkuk/thip/feed/...) 본 이슈는 기록 삭제 API 개발이 목적이나, 피드 삭제 API 개발이 함께 포함되어 있음. 피드 삭제는 별도의 이슈에서 다루어야 할 수 있음.
JPA @Modifying 옵션 개선 (src/main/java/konkuk/thip/book/adapter/out/persistence/repository/SavedBookJpaRepository.java, src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/repository/RecentSearchJpaRepository.java) 기록 삭제와 직접적인 관련이 없는 JPA 옵션 개선 작업이 포함되어 있음.
피드, 댓글, 좋아요 등 연관관계 매핑 및 삭제 로직 확장 (src/main/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntity.java, src/main/java/konkuk/thip/comment/adapter/out/jpa/CommentJpaEntity.java, src/main/java/konkuk/thip/post/adapter/out/jpa/PostJpaEntity.java) 기록 삭제와 직접적 연관은 없으나, 도메인 일관성 유지를 위한 확장일 수 있음. 다만 이슈 범위에는 명시되어 있지 않음.

Possibly related PRs

  • [feat] 댓글 작성 api 개발 #101: CommentJpaEntity의 필드 및 관계 매핑 확장 관련 변경이 겹치므로, 댓글 도메인 확장 측면에서 코드 레벨에서 연관성이 있습니다.

Suggested labels

🧸 현준

Suggested reviewers

  • buzz0331

Poem

🐰
기록이 사라지는 밤,
권한을 지켜주는 코드를 담아
댓글과 좋아요도 함께 안녕,
연관관계 따라 깔끔히 정리!
테스트도 꼼꼼히, 예외도 빈틈없이
토끼는 오늘도 코드 정원을 다듬어요.
🌙✨

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/#175-record-delete

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

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • 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. Examples:
    • @coderabbitai explain this code block.
  • 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 src/utils.ts and explain its main purpose.
    • @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 comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai generate unit tests to generate unit tests for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

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

Documentation and Community

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

Copy link

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

♻️ Duplicate comments (1)
src/main/java/konkuk/thip/record/adapter/out/persistence/RecordCommandPersistenceAdapter.java (1)

51-58: 동일 우려: Record 삭제도 물리 삭제. 소프트 삭제 정책 여부 확인

Feed 삭제와 동일한 패턴으로 물리 삭제입니다. 프로젝트의 포스트(Record) 전반이 소프트 삭제 정책이라면 @SQLDelete/@Where 또는 상태 업데이트 방식으로 정렬이 필요합니다. 위 Feed 코멘트의 검증 스크립트가 RecordJpaEntity도 함께 점검합니다.

🧹 Nitpick comments (24)
src/main/java/konkuk/thip/record/adapter/in/web/response/RecordDeleteResponse.java (1)

3-7: roomId가 null이 될 수 없다면 primitive(long)로 변경 + Swagger 메타데이터 추가 제안

응답에서 roomId는 null이 의미 없을 가능성이 높습니다. NPE 여지를 줄이기 위해 primitive 사용이 안전합니다. 또한 스펙 노출을 위해 Schema 어노테이션을 권장합니다.

 package konkuk.thip.record.adapter.in.web.response;
 
+import io.swagger.v3.oas.annotations.media.Schema;
+
- public record RecordDeleteResponse(Long roomId) {
-     public static RecordDeleteResponse of(Long roomId) {
+@Schema(description = "기록 삭제 응답")
+public record RecordDeleteResponse(
+        @Schema(description = "방 ID", requiredMode = Schema.RequiredMode.REQUIRED)
+        long roomId
+) {
+    public static RecordDeleteResponse of(long roomId) {
         return new RecordDeleteResponse(roomId);
     }
 }
src/main/java/konkuk/thip/record/application/port/out/RecordCommandPort.java (1)

24-24: 메서드 명명 일관성 제안: saveRecord → save

같은 레벨의 FeedCommandPort는 save라는 이름을 사용하고 있습니다. RecordCommandPort도 saveRecord 대신 save로 맞추면 팀 내 가독성과 일관성이 좋아집니다. 추후 대규모 리네임 부담을 줄이려면 지금 통일을 고려해 주세요.

src/test/java/konkuk/thip/room/adapter/in/web/RoomPlayingDetailViewApiTest.java (1)

81-81: deleteAllInBatch 전환은 OK, 단 JPA 리스너/소프트삭제 우회에 유의

InBatch는 영속성 컨텍스트/엔티티 리스너/@SQLDelete를 우회합니다. 테스트에서 의도한 바라면 좋습니다. 성능 일관성을 위해 voteItem도 InBatch로 맞추는 것을 고려해 볼 수 있습니다.

-        voteItemJpaRepository.deleteAll();
+        voteItemJpaRepository.deleteAllInBatch();

참고: 현재 순서(아이템 → 투표 → 참가자 → 방 …)라면 FK 제약 충돌 없이 안전합니다. 다른 레포들도 일괄 삭제로 통일하려면 동일한 FK 고려가 필요합니다.

src/test/java/konkuk/thip/vote/adapter/in/web/VoteCreateControllerTest.java (1)

81-81: deleteAllInBatch 전환은 OK, voteItem도 일괄 삭제로 정렬 제안

테스트 성능/일관성 측면에서 voteItem도 InBatch로 맞추면 좋습니다. 리스너/소프트삭제가 teardown 단계에서 필요 없다면 안전합니다.

-        voteItemJpaRepository.deleteAll();
+        voteItemJpaRepository.deleteAllInBatch();
src/main/java/konkuk/thip/common/exception/code/ErrorCode.java (1)

123-123: 메시지 용어 일관성 개선 제안

동일 섹션(Record) 내 기존 메시지는 영문 도메인명(“RECORD”)을 사용하지만, 본 항목만 한글 “기록”을 사용하고 있습니다. 용어를 통일하는 편이 조회/로그 분석 시 혼선을 줄입니다.

아래처럼 메시지를 맞추는 것을 제안합니다.

-    RECORD_ACCESS_FORBIDDEN(HttpStatus.FORBIDDEN, 130003, "기록 접근 권한이 없습니다."),
+    RECORD_ACCESS_FORBIDDEN(HttpStatus.FORBIDDEN, 130003, "RECORD 접근 권한이 없습니다."),
src/test/java/konkuk/thip/book/adapter/in/web/BookDetailSearchControllerTest.java (2)

124-124: deleteAllInBatch 사용에 따른 부작용 점검 필요

deleteAllInBatch는 Hibernate 엔티티 리스너/영속성 컨텍스트 동기화/@SQLDelete(소프트 삭제)를 우회합니다. 피드 도메인에 소프트 삭제나 연관 엔티티 정리 로직이 걸려 있다면 테스트 정합성에 영향이 있을 수 있습니다. 현재 케이스(단순 정리)에서는 성능상 이점이 있으나, 아래를 확인해 주세요.

  • 이 테스트 클래스에서 Feed는 하드 삭제해도 무관한가?
  • 같은 트랜잭션/영속성 컨텍스트에서 Feed를 참조 중인 엔티티가 없는가? (stale state 방지)

필요 시 deleteAll로 복구하거나, batch 삭제 후 EntityManager clear를 고려해 주세요.


184-184: 테스트 내 데이터 정리 일관성 및 FK 제약 확인

테스트 본문에서도 deleteAllInBatch로 변경되었습니다. 본 호출이 외래키 제약(예: 댓글/태그 등 다른 엔티티의 Feed FK)과 충돌하지 않는지, 그리고 소프트 삭제 일관성 요구사항이 없는지 점검 바랍니다. 문제 없다면 주석으로 “하드 삭제 의도”를 남기면 유지보수성이 좋아집니다.

src/main/java/konkuk/thip/feed/application/port/in/FeedDeleteUseCase.java (1)

4-6: Record/Feed 삭제 UseCase 시그니처 통일 제안 — Command 객체 사용

RecordDeleteUseCase는 Command 객체를 사용하지만, Feed는 개별 파라미터(Long, Long)를 사용하고 있어 application 계층의 API 스타일이 불일치합니다. 확장성(추가 파라미터), 테스트 용이성, 추적성 측면에서 Command로의 통일을 권장합니다.

아래와 같이 변경을 제안드립니다.

-public interface FeedDeleteUseCase {
-    void deleteFeed(Long feedId, Long userId);
-}
+public interface FeedDeleteUseCase {
+    void deleteFeed(FeedDeleteCommand command);
+}

추가: Command DTO 예시

package konkuk.thip.feed.application.port.in.dto;

import lombok.Builder;

@Builder
public record FeedDeleteCommand(Long feedId, Long userId) {}
src/main/java/konkuk/thip/record/application/port/in/RecordDeleteUseCase.java (1)

5-7: 반환값(Long)의 의미 명확화 — Javadoc 추가 제안

PR 요약상 반환값은 roomId로 보입니다. 인터페이스에 Javadoc을 추가해 반환 의미를 명확히 하면 오용 방지와 가독성이 향상됩니다.

 public interface RecordDeleteUseCase {
-    Long deleteRecord(RecordDeleteCommand command);
+    /**
+     * Record 삭제를 수행하고, 해당 Record가 속한 Room의 ID를 반환합니다.
+     * @param command recordId, roomId, userId 등을 포함한 삭제 커맨드
+     * @return roomId
+     */
+    Long deleteRecord(RecordDeleteCommand command);
 }
src/main/java/konkuk/thip/record/adapter/out/jpa/RecordJpaEntity.java (1)

55-58: 테스트 편의 메서드 추가는 유용하나, 중복을 줄여 일관성을 높이세요

updateCommentCount(int)는 테스트 가독성을 높입니다. 다만 Feed/Record에는 존재하고 Vote에는 부재합니다. 상위 PostJpaEntity@VisibleForTesting protected void setCommentCountForTest(int) 같은 메서드로 공통화하면 테스트 코드가 더 일관적입니다.

src/test/java/konkuk/thip/feed/domain/FeedTest.java (2)

350-358: 삭제 권한 테스트 보강 제안: 예외 메시지까지 검증

기능은 맞습니다. 기존 테스트들처럼 예외 메시지까지 검증하면 일관성이 좋아집니다.

     InvalidStateException ex = assertThrows(InvalidStateException.class,
             () -> feed.validateDeletable(OTHER_USER_ID));
 
     assertEquals(FEED_ACCESS_FORBIDDEN, ex.getErrorCode());
+    assertTrue(ex.getCause().getMessage().contains("피드 작성자만 피드를 수정/삭제할 수 있습니다."));

360-366: 작성자 삭제 성공 케이스 LGTM + 비공개 케이스 추가 제안

작성자면 공개/비공개 무관하게 삭제 가능함을 드러내기 위해 비공개 피드 케이스도 하나 더 추가(또는 파라미터라이즈)하면 커버리지가 더 명확해집니다.

src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java (2)

134-138: RECORD_DELETE 스웨거 응답 목록 보강: USER_NOT_FOUND/ROOM_NOT_FOUND 추가 권장

이 PR의 플로우(룸 참가자 검증, 기록-룸 일치 검증)를 고려하면 아래 오류도 문서화하는 게 일관됩니다.

-    RECORD_DELETE(new LinkedHashSet<>(Set.of(
-            ROOM_ACCESS_FORBIDDEN,
-            RECORD_NOT_FOUND,
-            RECORD_ACCESS_FORBIDDEN
-    ))),
+    RECORD_DELETE(new LinkedHashSet<>(Set.of(
+            USER_NOT_FOUND,
+            ROOM_NOT_FOUND,
+            ROOM_ACCESS_FORBIDDEN,
+            RECORD_NOT_FOUND,
+            RECORD_ACCESS_FORBIDDEN
+    ))),

인증/유저 조회 오류를 글로벌하게 처리하여 문서화 대상이 아니라면 알려주세요.


200-203: FEED_DELETE 스웨거 응답 목록 보강: USER_NOT_FOUND 추가 권장 (Room 관련 코드 미포함은 적절)

Feed는 Room에 속하지 않으므로 ROOM_* 코드가 없는 점은 팀 컨벤션과 일치합니다. 다만 FEED_UPDATE 등과의 일관성을 위해 USER_NOT_FOUND 추가를 권장합니다.

-    FEED_DELETE(new LinkedHashSet<>(Set.of(
-            FEED_NOT_FOUND,
-            FEED_ACCESS_FORBIDDEN
-    ))),
+    FEED_DELETE(new LinkedHashSet<>(Set.of(
+            USER_NOT_FOUND,
+            FEED_NOT_FOUND,
+            FEED_ACCESS_FORBIDDEN
+    ))),
src/main/java/konkuk/thip/feed/application/service/FeedDeleteService.java (2)

3-3: @transactional import 일관성 정리 권장 (Spring 애너테이션 사용 고려)

RecordDeleteService와 동일하게 jakarta.transaction.Transactional이 사용되었습니다. 프로젝트 전체 정책에 맞춰 org.springframework.transaction.annotation.Transactional로 통일하는 것을 권장합니다.

레포 전체 사용 현황 점검 스크립트는 RecordDeleteService 코멘트를 참고하세요.

적용 예:

-import jakarta.transaction.Transactional;
+import org.springframework.transaction.annotation.Transactional;

26-26: S3 이미지 삭제 TODO — 이벤트 기반/아웃박스 패턴 제안 가능

삭제 트랜잭션 이후 비동기로 S3 정리를 권장합니다. FeedDeletedEvent 발행 → 비동기 컨슈머가 이미지 키를 수집/삭제(S3 Multi-Object Delete), 필요 시 아웃박스 패턴으로 신뢰성 보강. 원하시면 이벤트/리스너/아웃박스 스켈레톤 코드를 제공하겠습니다.

src/main/java/konkuk/thip/feed/adapter/in/web/FeedCommandController.java (1)

96-107: 삭제 응답 상태코드 204(No Content) 고려 (선택 사항)

REST 관점에서는 본문 없는 삭제 성공 시 204가 일반적입니다. 다만 현재 프로젝트가 BaseResponse 래퍼로 200을 일관 사용한다면 그대로 유지해도 괜찮습니다.

src/test/java/konkuk/thip/feed/adapter/in/web/FeedDeleteAPITest.java (1)

95-110: @Transactional 애너테이션은 불필요합니다
테스트 메서드에서 생성된 데이터는 개별 트랜잭션으로 실행-롤백되므로, tearDown()에 별도 트랜잭션을 열어 deleteAllInBatch()를 수행할 필요가 없습니다. 오히려 테스트 수행 시간을 늘리고, 예상치 못한 플러시/락 이슈를 유발할 수 있습니다. 단순 정리 목적이라면 @Transactional 제거를 권장합니다.

src/test/java/konkuk/thip/record/adapter/in/web/RecordDeleteAPITest.java (1)

86-99: tearDown()의 트랜잭션 처리 과다
FeedDeleteAPITest와 동일하게 @Transactional을 부착한 상태에서 대량 deleteAllInBatch() 호출은 불필요합니다. 롤백 메커니즘 또는 @DataJpaTest식 격리 전략을 사용하는 편이 간결합니다.

src/main/java/konkuk/thip/record/adapter/in/web/RecordCommandController.java (1)

50-56: 엔드포인트 경로 일관성 제안
생성 API는 /rooms/{roomId}/record(단수), 삭제 API는 /rooms/{roomId}/record/{recordId}(단수)로 정의돼 있습니다. REST 관례상 컬렉션-리소스는 복수형(/records)을, 단건 리소스는 /records/{id} 형태를 사용합니다. 초기 설계 변경 고려가 가능하다면 아래처럼 통일하는 것이 가독성과 유지보수에 유리합니다.

/rooms/{roomId}/records
/rooms/{roomId}/records/{recordId}

src/test/java/konkuk/thip/record/domain/RecordTest.java (3)

9-9: static wildcard import 지양, 사용 상수만 명시적 import로 변경 권장

와일드카드 static import는 네임스페이스 오염 및 자동 리팩터링의 정확도를 떨어뜨립니다. 현재 테스트에서 사용하는 상수만 명시적으로 import해주세요.

-import static konkuk.thip.common.exception.code.ErrorCode.*;
+import static konkuk.thip.common.exception.code.ErrorCode.COMMENT_COUNT_UNDERFLOW;
+import static konkuk.thip.common.exception.code.ErrorCode.POST_LIKE_COUNT_UNDERFLOW;
+import static konkuk.thip.common.exception.code.ErrorCode.RECORD_ACCESS_FORBIDDEN;

23-26: 테스트 상수는 static final로 선언해 의도를 명확히 하세요

JUnit은 테스트 인스턴스를 매 테스트마다 생성하므로, 상수 값은 클래스 수준의 static final이 적합합니다. 대문자 네이밍도 상수 컨벤션과 잘 맞습니다.

-    private final Long OTHER_USER_ID = 2L;
+    private static final Long OTHER_USER_ID = 2L;
-
-    private final Long ROOM_ID = 1L;
+    private static final Long ROOM_ID = 1L;
-    private final Long OTHER_ROOM_ID = 2L;
+    private static final Long OTHER_ROOM_ID = 2L;

추가로, CREATOR_ID도 동일하게 static final로 맞추면 일관성이 좋아집니다(변경 라인 외 참고):

// 함께 적용 권장(라인 22)
private static final Long CREATOR_ID = 1L;

193-197: DisplayName 용어/띄어쓰기 수정 및 테스트 메서드 네이밍 일관화 제안

  • '피드' → '기록' (도메인 용어 일치)
  • '삭제 할' → '삭제할' (띄어쓰기)
  • 메서드명은 기존 패턴과 맞춰 noException으로 일관화 권장
-    @DisplayName("validateDeletable: 피드의 작성자인 경우 피드를 삭제 할 수 있다.")
+    @DisplayName("validateDeletable: 기록의 작성자인 경우 기록을 삭제할 수 있다.")
-    void validateDeletable_byCreator_Success(){
+    void validateDeletable_byCreator_noException(){
src/main/java/konkuk/thip/post/adapter/out/jpa/PostJpaEntity.java (1)

45-50: 대량 삭제 시 퍼포먼스 확인 필요

commentList, postLikeList 모두 cascade = REMOVE + orphanRemoval = true 로 설정되어 있어, 댓글·좋아요가 많은 게시물을 삭제하면 JPA 가 자식 수만큼 DELETE 쿼리를 개별 발행합니다.
대량 데이터 환경에서는 배치 삭제나 DB-레벨 cascade 로 전환하는 것이 성능-효율적일 수 있으니 추후 모니터링을 권장합니다.

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between ded26f5 and d5db07a.

📒 Files selected for processing (33)
  • src/main/java/konkuk/thip/comment/adapter/out/jpa/CommentJpaEntity.java (2 hunks)
  • src/main/java/konkuk/thip/common/exception/code/ErrorCode.java (1 hunks)
  • src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java (2 hunks)
  • src/main/java/konkuk/thip/feed/adapter/in/web/FeedCommandController.java (3 hunks)
  • src/main/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntity.java (2 hunks)
  • src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedCommandPersistenceAdapter.java (3 hunks)
  • src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedJpaRepository.java (1 hunks)
  • src/main/java/konkuk/thip/feed/application/port/in/FeedDeleteUseCase.java (1 hunks)
  • src/main/java/konkuk/thip/feed/application/port/out/FeedCommandPort.java (1 hunks)
  • src/main/java/konkuk/thip/feed/application/service/FeedDeleteService.java (1 hunks)
  • src/main/java/konkuk/thip/feed/domain/Feed.java (1 hunks)
  • src/main/java/konkuk/thip/post/adapter/out/jpa/PostJpaEntity.java (2 hunks)
  • src/main/java/konkuk/thip/record/adapter/in/web/RecordCommandController.java (3 hunks)
  • src/main/java/konkuk/thip/record/adapter/in/web/response/RecordDeleteResponse.java (1 hunks)
  • src/main/java/konkuk/thip/record/adapter/out/jpa/RecordJpaEntity.java (2 hunks)
  • src/main/java/konkuk/thip/record/adapter/out/persistence/RecordCommandPersistenceAdapter.java (2 hunks)
  • src/main/java/konkuk/thip/record/adapter/out/persistence/repository/RecordJpaRepository.java (1 hunks)
  • src/main/java/konkuk/thip/record/application/port/in/RecordDeleteUseCase.java (1 hunks)
  • src/main/java/konkuk/thip/record/application/port/in/dto/DummyQuery.java (0 hunks)
  • src/main/java/konkuk/thip/record/application/port/in/dto/DummyResult.java (0 hunks)
  • src/main/java/konkuk/thip/record/application/port/in/dto/RecordDeleteCommand.java (1 hunks)
  • src/main/java/konkuk/thip/record/application/port/out/RecordCommandPort.java (1 hunks)
  • src/main/java/konkuk/thip/record/application/service/RecordDeleteService.java (1 hunks)
  • src/main/java/konkuk/thip/record/domain/Record.java (1 hunks)
  • src/main/java/konkuk/thip/vote/adapter/out/jpa/VoteJpaEntity.java (1 hunks)
  • src/test/java/konkuk/thip/book/adapter/in/web/BookDetailSearchControllerTest.java (2 hunks)
  • src/test/java/konkuk/thip/comment/adapter/in/web/CommentDeleteAPITest.java (3 hunks)
  • src/test/java/konkuk/thip/feed/adapter/in/web/FeedDeleteAPITest.java (1 hunks)
  • src/test/java/konkuk/thip/feed/domain/FeedTest.java (1 hunks)
  • src/test/java/konkuk/thip/record/adapter/in/web/RecordDeleteAPITest.java (1 hunks)
  • src/test/java/konkuk/thip/record/domain/RecordTest.java (4 hunks)
  • src/test/java/konkuk/thip/room/adapter/in/web/RoomPlayingDetailViewApiTest.java (1 hunks)
  • src/test/java/konkuk/thip/vote/adapter/in/web/VoteCreateControllerTest.java (1 hunks)
💤 Files with no reviewable changes (2)
  • src/main/java/konkuk/thip/record/application/port/in/dto/DummyQuery.java
  • src/main/java/konkuk/thip/record/application/port/in/dto/DummyResult.java
🧰 Additional context used
🧠 Learnings (5)
📓 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의 참가자인지 검증이 필요하다.
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 아키텍처에서 읽기 전용 작업의 효율성을 위한 팀 컨벤션임.
📚 Learning: 2025-07-03T03:05:05.031Z
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로부터 조회하는 메서드를 추가함.

Applied to files:

  • src/main/java/konkuk/thip/record/application/port/out/RecordCommandPort.java
  • src/main/java/konkuk/thip/feed/application/port/out/FeedCommandPort.java
📚 Learning: 2025-07-23T17:41:55.507Z
Learnt from: hd0rable
PR: THIP-TextHip/THIP-Server#101
File: src/test/java/konkuk/thip/comment/adapter/in/web/CommentControllerTest.java:118-265
Timestamp: 2025-07-23T17:41:55.507Z
Learning: CommentControllerTest는 댓글 생성 API의 검증 로직과 예외 상황만을 테스트하는 단위 테스트이며, 성공 케이스는 별도의 통합 테스트(CommentCreateAPITest)에서 다룬다.

Applied to files:

  • src/test/java/konkuk/thip/comment/adapter/in/web/CommentDeleteAPITest.java
  • src/test/java/konkuk/thip/record/adapter/in/web/RecordDeleteAPITest.java
  • src/test/java/konkuk/thip/feed/adapter/in/web/FeedDeleteAPITest.java
  • src/test/java/konkuk/thip/record/domain/RecordTest.java
📚 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 프로젝트에서 Record와 Vote는 Room에 속하지만 Feed는 Room에 속하지 않는 구조이며, 댓글 작성 시 Record/Vote에 대해서만 사용자가 해당 Room의 참가자인지 검증이 필요하다.

Applied to files:

  • src/main/java/konkuk/thip/common/exception/code/ErrorCode.java
  • src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java
  • src/test/java/konkuk/thip/record/adapter/in/web/RecordDeleteAPITest.java
  • src/main/java/konkuk/thip/record/application/service/RecordDeleteService.java
  • src/main/java/konkuk/thip/record/domain/Record.java
  • src/test/java/konkuk/thip/record/domain/RecordTest.java
  • src/main/java/konkuk/thip/record/adapter/in/web/RecordCommandController.java
📚 Learning: 2025-06-29T09:47:31.299Z
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#36
File: src/main/java/konkuk/thip/user/adapter/out/persistence/UserJpaRepository.java:7-7
Timestamp: 2025-06-29T09:47:31.299Z
Learning: Spring Data JPA에서 findBy{FieldName} 패턴의 메서드는 명시적 선언 없이 자동으로 생성되며, Optional<Entity> 반환 타입을 사용하는 것이 null 안전성을 위해 권장됩니다.

Applied to files:

  • src/main/java/konkuk/thip/record/adapter/out/persistence/RecordCommandPersistenceAdapter.java
  • src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedCommandPersistenceAdapter.java
  • src/main/java/konkuk/thip/record/adapter/out/persistence/repository/RecordJpaRepository.java
  • src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedJpaRepository.java
🧬 Code Graph Analysis (3)
src/main/java/konkuk/thip/record/adapter/out/jpa/RecordJpaEntity.java (4)
src/main/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntity.java (1)
  • Entity (21-77)
src/main/java/konkuk/thip/vote/adapter/out/jpa/VoteJpaEntity.java (1)
  • Entity (16-54)
src/main/java/konkuk/thip/post/adapter/out/jpa/PostJpaEntity.java (1)
  • Entity (15-59)
src/main/java/konkuk/thip/record/domain/Record.java (1)
  • Getter (14-116)
src/test/java/konkuk/thip/feed/domain/FeedTest.java (1)
src/test/java/konkuk/thip/record/domain/RecordTest.java (1)
  • DisplayName (12-219)
src/test/java/konkuk/thip/record/adapter/in/web/RecordDeleteAPITest.java (1)
src/test/java/konkuk/thip/common/util/TestEntityFactory.java (1)
  • TestEntityFactory (34-324)
🔇 Additional comments (20)
src/main/java/konkuk/thip/record/application/port/out/RecordCommandPort.java (1)

24-24: delete(Record) 추가 적절

CommandPort에 삭제 계약을 노출한 선택이 도메인 주도 설계와 CQRS 관점에서 타당합니다. 서비스 트랜잭션 경계 내에서 호출되도록만 유지하면 됩니다.

src/main/java/konkuk/thip/feed/application/port/out/FeedCommandPort.java (2)

19-19: delete(Feed) 추가 적절

삭제 계약 노출이 서비스/어댑터 전반의 기능 구현에 필요하며, 다른 메서드들과도 시그니처 일관성이 있습니다.


19-19: FeedDeleteService에 Room 검증 로직이 포함되어 있지 않음을 확인했습니다. 팀 컨벤션에 부합합니다.

  • 확인 파일:
    • src/main/java/konkuk/thip/feed/application/service/FeedDeleteService.java
  • Feed 도메인·포트 계층 전체에서 Room, roomId, RoomParticipant, RoomCommandPort 참조 미발견

따라서 추가 수정은 필요하지 않습니다.

src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedJpaRepository.java (1)

19-19: 상태 기반 조회 메서드 추가 적절

Optional 반환 및 StatusType 필터링으로 삭제/비활성화 레코드 배제를 명시화한 점 좋습니다. 도메인 일관성 측면에서도 적합합니다.

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

9-11: findByPostIdAndStatus 도입 적절 — 소프트 딜리트 필터링 OK

Optional<RecordJpaEntity> findByPostIdAndStatus(...)는 null-안전성과 소프트 딜리트 상태 필터링 요구에 부합합니다. 상위 PostJpaEntitypostId/status 속성 경로도 올바릅니다.

src/test/java/konkuk/thip/comment/adapter/in/web/CommentDeleteAPITest.java (1)

115-116: 검증 단순화 LGTM

INACTIVE 상태 단일 쿼리 확인으로 의도와 결과가 명확해졌습니다. 테스트 안정성과 가독성 측면에서 적절합니다.

Also applies to: 135-136, 157-158

src/main/java/konkuk/thip/feed/domain/Feed.java (2)

132-132: 권한 오류 메시지 개선 LGTM

삭제 기능 추가에 맞춰 메시지를 “수정/삭제”로 정비한 점 좋습니다. 테스트와도 일치합니다.


136-139: validateDeletable 도입 LGTM — 현재 정책을 잘 캡슐화

삭제 권한이 곧 작성자 검증임을 명시하는 얇은 래퍼로 적절합니다. 정책 변경 시 이 지점을 확장하면 됩니다.

src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedCommandPersistenceAdapter.java (1)

44-49: ACTIVE 상태 필터링으로 조회 변경 LGTM

소프트 삭제/비활성화된 피드를 배제하는 안전한 조회로 일관성이 좋아졌습니다.

src/main/java/konkuk/thip/record/adapter/out/persistence/RecordCommandPersistenceAdapter.java (1)

47-49: ACTIVE 상태 필터링으로 조회 변경 LGTM

비활성화된 기록을 배제하여 조회 일관성 확보에 도움됩니다.

src/main/java/konkuk/thip/record/application/service/RecordDeleteService.java (2)

23-31: 검증 순서 합리적 — 팀 컨벤션(Record는 Room 소속)과 일치

  • 방 참여자 검증(사용자 ∈ Room)
  • 기록 조회
  • 기록의 Room 일치/작성자 권한 검증

THIP 러닝(Record/Vote는 Room 소속, Feed는 아님)과 정확히 부합합니다. 👍


33-36: 삭제 및 반환값(roomId) 처리 적절

delete 후 roomId 반환은 컨트롤러 응답 모델과도 잘 맞습니다. 트랜잭션 경계 내에서 일관성 있게 처리되어 있습니다.

src/main/java/konkuk/thip/feed/application/service/FeedDeleteService.java (2)

21-25: 조회 → 권한 검증 순서 적절 (Feed는 Room 검증 불필요)

Feed가 Room에 속하지 않는 구조를 고려하면 현재 순서가 가장 단순하고 명확합니다.


28-29: 도메인 삭제 포트 호출 적절

도메인 규칙 검증 후 포트에 위임하는 구조가 깔끔합니다.

src/main/java/konkuk/thip/feed/adapter/in/web/FeedCommandController.java (1)

39-39: 의존성 주입 추가 적절

삭제 유스케이스 주입 위치와 접근 제어(생성자 주입) 모두 적절합니다.

src/test/java/konkuk/thip/record/domain/RecordTest.java (4)

37-37: ROOM_ID 상수 사용으로 테스트 가독성 향상, LGTM

하드코딩 값 대신 의미 있는 상수 사용은 의도 전달과 유지보수에 유리합니다.


182-190: 비작성자 삭제 제한과 에러코드 검증이 명확합니다

도메인 규칙(작성자만 삭제 가능)과 RECORD_ACCESS_FORBIDDEN 에러코드 검증이 잘 반영되어 있습니다. 예외 유형/코드 모두 체크한 점 좋습니다.


200-206: Room 일치 시 예외 없음 검증, 러닝과 일치합니다

Record는 Room에 속한다는 팀 컨벤션(러닝)에 부합하는 유효성 테스트입니다. 정상 경로를 간결히 확인한 점 좋습니다.


208-217: Room 불일치 시 접근 거부 코드(RECORD_ACCESS_FORBIDDEN) 검증 적절

권한/소속 불일치 시 공통 접근 거부 코드로 수렴시킨 설계와 테스트가 일치합니다. 예외 타입과 에러코드 모두 검증되어 신뢰성이 높습니다.

src/main/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntity.java (1)

42-49: cascade = REMOVE + orphanRemoval ⇒ 자식은 물리 삭제, 부모는 소프트 삭제

부모 FeedJpaEntity 가 ‘INACTIVE’ 로만 업데이트될 때 자식(SavedFeedJpaEntity, FeedTagJpaEntity)은 실제 DELETE 됩니다.
도메인 관점에서 “소프트 삭제된 피드”가 자식을 모두 잃는 것이 의도된 동작인지 재확인이 필요합니다.
일관된 보존 정책을 위해 자식도 soft delete 컬럼을 갖게 하거나, 부모를 물리 삭제하도록 설계 변경을 고려해 주세요.

Comment on lines 59 to 60
@OneToMany(mappedBy = "commentJpaEntity", cascade = CascadeType.REMOVE, orphanRemoval = true)
private List<CommentLikeJpaEntity> commentLikeList = new ArrayList<>();
Copy link

@coderabbitai coderabbitai bot Aug 8, 2025

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

컬렉션 필드에 @Builder.Default 누락 — Lombok Builder 사용 시 NPE 가능성

클래스에 @builder가 적용되어 있어 필드 초기화(new ArrayList<>())가 빌더 경로에서 무시됩니다. 테스트나 팩토리에서 빌더로 엔티티를 만들면 commentLikeList가 null이 되어 NPE가 발생할 수 있습니다. 아래처럼 @Builder.Default를 추가해 주세요.

-    @OneToMany(mappedBy = "commentJpaEntity", cascade = CascadeType.REMOVE, orphanRemoval = true)
-    private List<CommentLikeJpaEntity> commentLikeList = new ArrayList<>();
+    @OneToMany(mappedBy = "commentJpaEntity", cascade = CascadeType.REMOVE, orphanRemoval = true)
+    @Builder.Default
+    private List<CommentLikeJpaEntity> commentLikeList = new ArrayList<>();
📝 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
@OneToMany(mappedBy = "commentJpaEntity", cascade = CascadeType.REMOVE, orphanRemoval = true)
private List<CommentLikeJpaEntity> commentLikeList = new ArrayList<>();
@OneToMany(mappedBy = "commentJpaEntity", cascade = CascadeType.REMOVE, orphanRemoval = true)
@Builder.Default
private List<CommentLikeJpaEntity> commentLikeList = new ArrayList<>();
🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/comment/adapter/out/jpa/CommentJpaEntity.java at
lines 59-60, the commentLikeList field is initialized with new ArrayList<>(),
but since the class uses Lombok's @Builder, this initialization is ignored in
the builder path, causing potential NullPointerExceptions. Fix this by adding
the @Builder.Default annotation to the commentLikeList field to ensure it is
properly initialized when using the builder.

Copy link
Member Author

Choose a reason for hiding this comment

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

근데 삭제용이라 .get해서 안쓸건데 @Builder.Default로 초기화가 필요한가?

Copy link

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

buzz0331
buzz0331 previously approved these changes Aug 9, 2025
Copy link
Contributor

@buzz0331 buzz0331 left a comment

Choose a reason for hiding this comment

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

수고하셨습니다~~ 앞 pr과 비슷한 로직인 것 같아 앞 pr 리뷰 참고해주시면 될 것 같습니다!!

Comment on lines 28 to 31
// 2-1. 기록 삭제 권한 검증
record.validateRoomId(command.roomId());
record.validateDeletable(command.userId());

Copy link
Contributor

Choose a reason for hiding this comment

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

p3: 이렇게 나누지 않고 validateDeletable 내부에서 validateRoomId 검증 로직까지 한번에 수행하는 것 어떤가요?? 캡슐화가 더 잘 지켜질 것 같습니다!!

Comment on lines +101 to +120
@Test
@DisplayName("기록을 삭제하면 [soft delete]되고, 연관된 댓글, 댓글 좋아요도 모두 삭제된다")
void deleteRecord_success() throws Exception {

// when
mockMvc.perform(delete("/rooms/{roomId}/record/{recordId}", room.getRoomId(), record.getPostId())
.requestAttr("userId", user.getUserId()))
.andExpect(status().isOk());


// then: 1) 기록 soft delete (status=INACTIVE)
assertThat(recordJpaRepository.findByPostIdAndStatus(record.getPostId(), INACTIVE)).isPresent();

// 2) 댓글 삭제 soft delete
assertThat(commentJpaRepository.findByCommentIdAndStatus(comment.getCommentId(),INACTIVE)).isPresent();

// 3) 댓글 좋아요 삭제
long commentLikeCountAfter = commentLikeJpaRepository.count();
assertThat(commentLikeCountAfter).isEqualTo(0);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

LG™

Copy link
Collaborator

Choose a reason for hiding this comment

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

LGTM too

seongjunnoh
seongjunnoh previously approved these changes Aug 9, 2025
Copy link
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.

피드 삭제와 많이 유사하네요! 고생하셨습니다!

Comment on lines +101 to +120
@Test
@DisplayName("기록을 삭제하면 [soft delete]되고, 연관된 댓글, 댓글 좋아요도 모두 삭제된다")
void deleteRecord_success() throws Exception {

// when
mockMvc.perform(delete("/rooms/{roomId}/record/{recordId}", room.getRoomId(), record.getPostId())
.requestAttr("userId", user.getUserId()))
.andExpect(status().isOk());


// then: 1) 기록 soft delete (status=INACTIVE)
assertThat(recordJpaRepository.findByPostIdAndStatus(record.getPostId(), INACTIVE)).isPresent();

// 2) 댓글 삭제 soft delete
assertThat(commentJpaRepository.findByCommentIdAndStatus(comment.getCommentId(),INACTIVE)).isPresent();

// 3) 댓글 좋아요 삭제
long commentLikeCountAfter = commentLikeJpaRepository.count();
assertThat(commentLikeCountAfter).isEqualTo(0);
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

LGTM too

void tearDown() {
voteItemJpaRepository.deleteAll();
voteJpaRepository.deleteAll();
voteJpaRepository.deleteAllInBatch();
Copy link
Collaborator

Choose a reason for hiding this comment

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

굳굳

…lushAutomatically = true) 속성 추가로 영속성 1차 캐시 삭제(#160)
…175-record-delete

# Conflicts:
#	src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedTag/FeedTagJpaRepository.java
#	src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/SavedFeedJpaRepository.java
…lushAutomatically = true) 속성 추가로 영속성 1차 캐시 삭제(#175)
@hd0rable hd0rable dismissed stale reviews from seongjunnoh and buzz0331 via 72c85b7 August 10, 2025 09:21
@github-actions
Copy link

github-actions bot commented Aug 10, 2025

Test Results

374 tests   374 ✅  32s ⏱️
110 suites    0 💤
110 files      0 ❌

Results for commit 21f7a9d.

♻️ This comment has been updated with latest results.

Copy link

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

🔭 Outside diff range comments (1)
src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedCommandPersistenceAdapter.java (1)

78-80: update 경로도 ACTIVE 필터 사용 권장

삭제된 피드가 수정되지 않도록 findByPostIdAndStatus(..., ACTIVE)를 사용하는 것이 안전합니다.

-        FeedJpaEntity feedJpaEntity = feedJpaRepository.findById(feed.getId())
-                .orElseThrow(() -> new EntityNotFoundException(FEED_NOT_FOUND));
+        FeedJpaEntity feedJpaEntity = feedJpaRepository.findByPostIdAndStatus(feed.getId(), ACTIVE)
+                .orElseThrow(() -> new EntityNotFoundException(FEED_NOT_FOUND));
♻️ Duplicate comments (2)
src/main/java/konkuk/thip/record/application/service/RecordDeleteService.java (2)

28-35: 검증 캡슐화 범위 재점검(중복 검증 축소)

validateDeletable(userId, roomId)가 “기록-방 일치”까지 내포한다면, 외부 별도 검증을 줄여 응집도를 높일 수 있습니다. 기존 피드백과 유사 제안입니다.


3-3: @transactional import 일관성 (Spring으로 통일 권장)

프로젝트 전반과 일관되게 org.springframework.transaction.annotation.Transactional 사용을 권장합니다. 이전 코멘트와 동일 제안입니다.

-import jakarta.transaction.Transactional;
+import org.springframework.transaction.annotation.Transactional;
🧹 Nitpick comments (10)
src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/repository/RecentSearchJpaRepository.java (2)

12-14: 업데이트 카운트 반환하도록 시그니처 변경 제안

영향 행 수를 반환하면 존재하지 않는 ID 등 예외 상황을 상위 계층에서 명확히 처리할 수 있습니다.

다음과 같이 변경을 제안합니다:

-    @Modifying(clearAutomatically = true, flushAutomatically = true)
-    @Query("UPDATE RecentSearchJpaEntity r SET r.modifiedAt = CURRENT_TIMESTAMP WHERE r.recentSearchId = :recentSearchId")
-    void updateModifiedAt(@Param("recentSearchId") Long recentSearchId);
+    @Modifying(clearAutomatically = true, flushAutomatically = true)
+    @Query("UPDATE RecentSearchJpaEntity r SET r.modifiedAt = CURRENT_TIMESTAMP WHERE r.recentSearchId = :recentSearchId")
+    int updateModifiedAt(@Param("recentSearchId") Long recentSearchId);

호출부에서 반환값이 0이면 대상 미존재로 간주하여 예외/로그 처리할 수 있습니다.


13-13: 시간 소스 일관성 검토 (DB CURRENT_TIMESTAMP vs 애플리케이션 시간)

JPQL의 CURRENT_TIMESTAMP는 DB 시간을 사용합니다. 시스템의 다른 감사 필드가 스프링 데이터 감사(@LastModifiedDate) 혹은 애플리케이션 클럭을 사용한다면 시간 소스가 혼재할 수 있습니다. 테스트 재현성/시간대 정책(예: UTC 고정) 관점에서 일관성 확인을 권장합니다. 필요 시 애플리케이션 레벨에서 설정(setter) 후 flush로 통일하는 것도 방법입니다.

src/main/java/konkuk/thip/comment/application/port/out/CommentCommandPort.java (1)

26-26: 의도(소프트 삭제) 명시 및 사용 가이드 주석 제안

메서드가 소프트 삭제(상태 전환)임을 Javadoc으로 명시해 주세요. 대량 업데이트의 특성상 엔티티 리스너/도메인 이벤트가 동작하지 않는다는 점도 함께 적어두면 오해를 줄일 수 있습니다.

다음처럼 간단한 주석 추가를 권장합니다:

+    /**
+     * 주어진 postId에 속한 모든 댓글을 소프트 삭제(STATUS=INACTIVE)합니다.
+     * 대량 업데이트로 엔티티 리스너/도메인 이벤트는 트리거되지 않습니다.
+     */
     void softDeleteAllByPostId(Long postId);
src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeCommandPersistenceAdapter.java (1)

47-50: 벌크 삭제 트랜잭션 보장 및 영속성 컨텍스트 동기화 확인

  • 해당 벌크 삭제는 반드시 트랜잭션 내에서 실행되어야 합니다. 상위 서비스 계층에서 @Transactional이 보장되지 않는다면 본 메서드에 부여를 고려해 주세요.
  • PostLikeJpaRepository.deleteAllByPostId@Modifying(clearAutomatically = true, flushAutomatically = true)가 선언되어 있는지 확인해 주세요. 없으면 1차 캐시 불일치가 날 수 있습니다.

필요 시 아래와 같이 메서드에 트랜잭션을 부여할 수 있습니다:

     @Override
+    @org.springframework.transaction.annotation.Transactional
     public void deleteAllByPostId(Long postId) {
         postLikeJpaRepository.deleteAllByPostId(postId);
     }

또는 import를 추가하고 어노테이션을 정식으로 사용합니다:

import org.springframework.transaction.annotation.Transactional;

// ...
@Override
@Transactional
public void deleteAllByPostId(Long postId) { ... }
src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentJpaRepository.java (1)

15-17: Enum literal 사용 및 영향 행 수 반환으로 가시성 향상

  • 문자열 'INACTIVE' 대신 JPQL enum literal을 사용하면 타입 안정성과 리팩터링 안전성이 높아집니다.
  • 영향받은 행 수를 반환하면 테스트/모니터링 시 유용합니다.
  • 선택적으로 이미 INACTIVE인 행은 제외하여 불필요한 쓰기를 줄일 수 있습니다.

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

-    @Modifying(clearAutomatically = true, flushAutomatically = true)
-    @Query("UPDATE CommentJpaEntity c SET c.status = 'INACTIVE' WHERE c.postJpaEntity.postId = :postId")
-    void softDeleteAllByPostId(@Param("postId") Long postId);
+    @Modifying(clearAutomatically = true, flushAutomatically = true)
+    @Query("""
+        UPDATE CommentJpaEntity c
+           SET c.status = konkuk.thip.common.entity.StatusType.INACTIVE
+         WHERE c.postJpaEntity.postId = :postId
+           AND c.status <> konkuk.thip.common.entity.StatusType.INACTIVE
+    """)
+    int softDeleteAllByPostId(@Param("postId") Long postId);

참고: 메서드 시그니처 변경 시 이를 호출하는 어댑터/서비스에서 반환값 사용 여부를 점검해야 합니다.

src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeJpaRepository.java (1)

24-26: flush/clear 자동화는 👍. 삭제 건수 반환 고려

  • 벌크 삭제 후 1차 캐시 정합성 확보에 유리합니다.
  • 운영 관점에서 영향도/모니터링을 위해 삭제 건수를 반환받는 패턴이 유용합니다.

제안 diff:

-    void deleteByUserIdAndPostId(@Param("userId") Long userId, @Param("postId") Long postId);
+    int deleteByUserIdAndPostId(@Param("userId") Long userId, @Param("postId") Long postId);
src/main/java/konkuk/thip/record/application/service/RecordDeleteService.java (2)

44-45: 반환값은 실제 삭제된 엔티티의 roomId 사용 권장

입력값 반환 대신 record.getRoomId()가 의미적으로 정확합니다.

-        return command.roomId();
+        return record.getRoomId();

39-40: 주석 정정(도메인 용어 혼동)

“피드 게시물 좋아요 삭제” → “게시물 좋아요 삭제”로 정정 제안. 기록 삭제 서비스 맥락에 맞춰주세요.

-        // 3-2. 피드 게시물 좋아요 삭제
+        // 3-2. 게시물 좋아요 삭제
src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/SavedFeedJpaRepository.java (1)

14-16: 반환형 일관화(옵션)

동일한 삭제 메서드들 간 반환형을 int로 통일하면 호출부에서도 일관된 처리(삭제 건수 로깅 등)가 가능합니다.

-void deleteByUserIdAndFeedId(@Param("userId") Long userId, @Param("feedId") Long feedId);
+int deleteByUserIdAndFeedId(@Param("userId") Long userId, @Param("feedId") Long feedId);
src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedCommandPersistenceAdapter.java (1)

9-9: 패키지 네이밍 컨벤션(nit)

repository.Content.ContentJpaRepository처럼 대문자 패키지는 자바 관례와 다릅니다. 가능하면 소문자 패키지로 리팩터링 고려 바랍니다.

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 2437805 and 72c85b7.

📒 Files selected for processing (27)
  • src/main/java/konkuk/thip/book/adapter/out/persistence/repository/SavedBookJpaRepository.java (1 hunks)
  • src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentCommandPersistenceAdapter.java (3 hunks)
  • src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentJpaRepository.java (1 hunks)
  • src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentLikeJpaRepository.java (2 hunks)
  • src/main/java/konkuk/thip/comment/application/port/out/CommentCommandPort.java (1 hunks)
  • src/main/java/konkuk/thip/common/exception/code/ErrorCode.java (2 hunks)
  • src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java (2 hunks)
  • src/main/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntity.java (2 hunks)
  • src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedCommandPersistenceAdapter.java (5 hunks)
  • src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/Content/ContentJpaRepository.java (1 hunks)
  • src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedTag/FeedTagJpaRepository.java (1 hunks)
  • src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/SavedFeedJpaRepository.java (1 hunks)
  • src/main/java/konkuk/thip/feed/application/port/out/FeedCommandPort.java (1 hunks)
  • src/main/java/konkuk/thip/feed/application/service/FeedDeleteService.java (1 hunks)
  • src/main/java/konkuk/thip/post/adapter/out/jpa/PostJpaEntity.java (2 hunks)
  • src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeCommandPersistenceAdapter.java (1 hunks)
  • src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeJpaRepository.java (1 hunks)
  • src/main/java/konkuk/thip/post/application/port/out/PostLikeCommandPort.java (1 hunks)
  • src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/repository/RecentSearchJpaRepository.java (1 hunks)
  • src/main/java/konkuk/thip/record/adapter/out/jpa/RecordJpaEntity.java (1 hunks)
  • src/main/java/konkuk/thip/record/adapter/out/persistence/RecordCommandPersistenceAdapter.java (2 hunks)
  • src/main/java/konkuk/thip/record/application/service/RecordDeleteService.java (1 hunks)
  • src/main/java/konkuk/thip/record/domain/Record.java (1 hunks)
  • src/test/java/konkuk/thip/book/adapter/in/web/BookDetailSearchControllerTest.java (2 hunks)
  • src/test/java/konkuk/thip/feed/adapter/in/web/FeedDeleteAPITest.java (1 hunks)
  • src/test/java/konkuk/thip/record/adapter/in/web/RecordDeleteAPITest.java (1 hunks)
  • src/test/java/konkuk/thip/record/domain/RecordTest.java (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (12)
  • src/main/java/konkuk/thip/record/adapter/out/jpa/RecordJpaEntity.java
  • src/main/java/konkuk/thip/feed/application/port/out/FeedCommandPort.java
  • src/test/java/konkuk/thip/book/adapter/in/web/BookDetailSearchControllerTest.java
  • src/test/java/konkuk/thip/feed/adapter/in/web/FeedDeleteAPITest.java
  • src/test/java/konkuk/thip/record/adapter/in/web/RecordDeleteAPITest.java
  • src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java
  • src/main/java/konkuk/thip/feed/application/service/FeedDeleteService.java
  • src/main/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntity.java
  • src/test/java/konkuk/thip/record/domain/RecordTest.java
  • src/main/java/konkuk/thip/common/exception/code/ErrorCode.java
  • src/main/java/konkuk/thip/record/domain/Record.java
  • src/main/java/konkuk/thip/post/adapter/out/jpa/PostJpaEntity.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 (18)
src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/repository/RecentSearchJpaRepository.java (2)

12-14: flush/clear 옵션 추가 적절 — 동기화 이슈 예방

@Modifying(clearAutomatically = true, flushAutomatically = true) 추가로 벌크 업데이트 후 1차 캐시 불일치/오염을 줄일 수 있습니다. 전체 코드베이스의 일관성 측면에서도 (+)입니다.


12-14: clearAutomatically=true 부작용 불필요 확인

  • updateModifiedAt는 오직 RecentSearchCommandPersistenceAdapter.touch() 내부에서만 정의되어 있으나, 서비스 레이어에서 해당 touch()를 호출하는 코드는 존재하지 않습니다.
  • 영속성 컨텍스트가 비워진 뒤 같은 트랜잭션 내에서 이미 조회해둔 엔티티의 지연 로딩 필드에 접근하거나 상태를 변경하는 로직이 현재 없습니다.
  • 따라서 LazyInitializationException이나 상태 유실 우려는 없으므로, 본 리뷰 코멘트는 현 코드베이스에 적용되지 않습니다.

Likely an incorrect or invalid review comment.

src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentCommandPersistenceAdapter.java (3)

6-6: 의존성 주입 추가 LGTM

댓글 좋아요 일괄 삭제를 위해 CommentLikeJpaRepository 의존성 추가는 타당합니다.


34-34: 필드 주입 LGTM

@RequiredArgsConstructor와 함께 final 필드로 주입되어 구성 안전성이 확보됩니다.


95-99: 트랜잭션 경계가 서비스 계층에서 이미 보장됩니다.

  • src/main/java/konkuk/thip/feed/application/service/FeedDeleteService.java
    • deleteFeed(Long, Long) 메서드에 @transactional 적용 (라인 21–22)
  • src/main/java/konkuk/thip/record/application/service/RecordDeleteService.java
    • deleteRecord(RecordDeleteCommand) 메서드에 @transactional 적용 (라인 25–26)

따라서 CommentCommandPersistenceAdapter.softDeleteAllByPostId()에 별도 @transactional을 추가할 필요가 없습니다.

src/main/java/konkuk/thip/book/adapter/out/persistence/repository/SavedBookJpaRepository.java (1)

13-15: 트랜잭션 경계 확인 및 반환 타입 검토

  • deleteByUserIdAndBookIdBookSavedService.changeSavedBook(...) 내부에서 @Transactional이 적용된 상태로 호출되고 있으므로 JPA 트랜잭션 경계는 확보되어 있습니다.
  • @Modifying(clearAutomatically = true, flushAutomatically = true) 적용으로 1차 캐시 동기화 문제를 자동으로 방지할 수 있어 적절합니다. (단일 삭제 호출이므로 성능 영향은 거의 없으며, 대량 루프 호출 시에는 트랜잭션 경계를 외부로 옮기거나 배치 단위로 묶어 처리하는 것을 권장합니다.)
  • 삭제된 건수 확인이 필요하다면 반환 타입을 int로 변경하는 방안을 고려해 보세요.

제안 diff (선택 사항):

-    void deleteByUserIdAndBookId(Long userId, Long bookId);
+    int deleteByUserIdAndBookId(Long userId, Long bookId);
src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/Content/ContentJpaRepository.java (1)

5-7: 필요 import 추가 적절

@Modifying, @Query, @Param import 추가는 변경된 메서드 구현에 필요한 최소한의 보완으로 적절합니다.

src/main/java/konkuk/thip/record/application/service/RecordDeleteService.java (2)

32-33: 이미 삭제된 기록 처리/상태 필터 확인

getByIdOrThrow가 ACTIVE만 조회하는지 확인 바랍니다. 소프트 삭제 정책이라면 상태 필터가 필요합니다(이미 삭제된 기록 재삭제 방지).


38-40: 잘못된 postId/recordId 혼용 지적
Record는 하나의 피드 게시물을 나타내며 command.recordId()가 해당 게시물(post)의 ID로 사용되는 것이 맞습니다. 또한 Record 도메인에는 getPostId() 메서드가 존재하지 않아 제안하신 대체 코드(record.getPostId())는 컴파일 에러를 야기합니다.
기존대로 commentCommandPort.softDeleteAllByPostId(command.recordId())postLikeCommandPort.deleteAllByPostId(command.recordId())를 유지해주세요.

Likely an incorrect or invalid review comment.

src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentLikeJpaRepository.java (3)

18-21: bulk delete 시 영속성 컨텍스트 처리 옵션 추가 적절

clearAutomatically, flushAutomatically 적용 좋습니다. 대용량 삭제 시 1차 캐시 오염 방지에 유효합니다.


27-30: 댓글 단건 일괄 삭제 쿼리 옵션 일관화 👍

옵션 일관성과 JPQL 명확성 측면에서 적절합니다.


35-41: Java 텍스트 블록 사용 버전 확인 요청

""" 텍스트 블록은 JDK 15+ 필요. 빌드 타깃이 JDK 17이라면 OK입니다. 버전 정합성만 확인 부탁드립니다.

src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/SavedFeedJpaRepository.java (3)

14-16: JPQL 전환 + @Modifying 옵션 적용 적절

native → JPQL 전환 및 자동 flush/clear 옵션 적용 좋습니다.


18-19: findAllByUserId JPQL 전환 적절

엔티티 연관을 활용하는 깔끔한 JPQL입니다.


24-26: feedId 기준 일괄 삭제 API 추가 👍

삭제 건수 반환은 운영 모니터링에도 유용합니다.

src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedCommandPersistenceAdapter.java (3)

46-50: ACTIVE 상태 필터 도입 적절

소프트 삭제 정책과 일관됩니다. 태그 조인 후 매핑도 문제 없어 보입니다.


132-143: 연쇄 삭제 → softDelete 순서 적절. 트랜잭션 경계만 확인

서비스 레이어에서 본 메서드 호출이 단일 트랜잭션으로 묶여 있는지 확인 바랍니다(부분 실패 방지).


82-84: orphanRemoval=true 설정 확인 – clear()만으로도 DB 삭제가 보장됩니다
FeedJpaEntitycontentList 매핑에 이미 orphanRemoval = true가 적용되어 있어
feedJpaEntity.getContentList().clear() 호출 시 플러시 시점에 연관된 ContentJpaEntity가 삭제됩니다.
따라서 별도의 deleteAllByFeedId() 호출 없이도 DB 삭제가 보장됩니다.

Comment on lines 34 to 42
@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("""
DELETE FROM CommentLikeJpaEntity cl
WHERE cl.commentJpaEntity.commentId IN (
SELECT c.commentId FROM CommentJpaEntity c
WHERE c.postJpaEntity.postId = :postId
)
""")
void deleteAllByPostId(@Param("postId") Long postId);
Copy link

@coderabbitai coderabbitai bot Aug 10, 2025

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

DELETE 서브쿼리 → 경로 표현식으로 단순화 권장(가독성·성능)

서브쿼리 대신 연관 경로를 직접 사용하면 쿼리가 단순해집니다.

-    @Modifying(clearAutomatically = true, flushAutomatically = true)
-    @Query("""
-           DELETE FROM CommentLikeJpaEntity cl
-           WHERE cl.commentJpaEntity.commentId IN (
-                SELECT c.commentId FROM CommentJpaEntity c
-                WHERE c.postJpaEntity.postId = :postId
-           )
-           """)
-    void deleteAllByPostId(@Param("postId") Long postId);
+    @Modifying(clearAutomatically = true, flushAutomatically = true)
+    @Query("DELETE FROM CommentLikeJpaEntity cl WHERE cl.commentJpaEntity.postJpaEntity.postId = :postId")
+    void deleteAllByPostId(@Param("postId") Long postId);

(옵션) 삭제 건수 활용이 필요하다면 반환형을 int로 통일하는 것도 고려해 주세요.

🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentLikeJpaRepository.java
around lines 34 to 42, simplify the DELETE query by replacing the subquery with
a direct path expression using entity relationships to improve readability and
performance. Additionally, consider changing the method's return type to int to
return the number of deleted records if deletion count tracking is needed.

Copy link
Contributor

Choose a reason for hiding this comment

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

오 서브쿼리를 안 사용해도 위와 같은 방식을 사용해도 될 것 같은데요??

@coderabbitai 저러면 commentJpaEntity에서 postJpaEntity를 참조할때 추가적인 SELECT 쿼리는 안나가지??

Copy link

Choose a reason for hiding this comment

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

@buzz0331 아니요, 추가적인 SELECT 쿼리는 발생하지 않습니다!

@Modifying 어노테이션이 붙은 DELETE 쿼리는 벌크 연산으로 실행되어, 엔티티를 로딩하지 않고 직접 SQL로 변환됩니다.

경로 표현식 cl.commentJpaEntity.postJpaEntity.postId는 다음과 같이 SQL JOIN으로 변환됩니다:

DELETE FROM comment_like cl
WHERE EXISTS (
    SELECT 1 FROM comment c 
    JOIN post p ON c.post_id = p.post_id
    WHERE cl.comment_id = c.comment_id 
    AND p.post_id = ?
)

또는 Hibernate 버전에 따라:

DELETE FROM comment_like 
WHERE comment_id IN (
    SELECT c.comment_id FROM comment c
    WHERE c.post_id = ?
)

즉, 엔티티 로딩 없이 단일 DELETE 문으로 실행되므로 N+1 문제나 추가 SELECT는 발생하지 않습니다. 오히려 경로 표현식이 더 간결하고 JPA 스타일에 맞습니다!

Copy link
Member Author

Choose a reason for hiding this comment

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

엇 근데 토끼가 추천해준방식이 내부적으로 조인을 사용한다고해서 수정안했었습니당

Copy link

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Copy link
Contributor

Choose a reason for hiding this comment

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

@coderabbitai 아니 그럼 성능적으로 더 안좋은거 아니야? 왜 구라침?

Copy link

Choose a reason for hiding this comment

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

@buzz0331 죄송합니다, 제가 성능 면에서 정확하지 않은 설명을 드렸네요!

실제로 이 경우엔 서브쿼리가 성능상 더 나을 수 있습니다:

서브쿼리 (현재 코드) 👍

DELETE FROM comment_like 
WHERE comment_id IN (
    SELECT comment_id FROM comment WHERE post_id = ?
)
  • comment 테이블의 post_id 인덱스로 빠르게 comment_id 목록 조회
  • IN 절로 comment_like 삭제
  • 두 단계로 명확하게 분리

경로 표현식 (제 제안)

DELETE FROM comment_like cl
WHERE EXISTS (
    SELECT 1 FROM comment c 
    WHERE cl.comment_id = c.comment_id AND c.post_id = ?
)
  • 각 comment_like 행마다 EXISTS 체크 필요
  • 데이터가 많을수록 오버헤드 증가 가능

제가 "성능 개선"이라고 단정적으로 말씀드린 건 잘못됐습니다. 가독성은 개선될 수 있지만, 성능은 오히려 현재 서브쿼리 방식이 더 효율적일 가능성이 높습니다.

현재 구현 그대로 유지하시는 게 좋겠습니다! 🙏

Copy link
Contributor

Choose a reason for hiding this comment

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

정신차려라 🙄

Copy link
Member Author

Choose a reason for hiding this comment

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

ㅋㅋㅋ

Copy link
Contributor

@buzz0331 buzz0331 left a comment

Choose a reason for hiding this comment

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

굿굿 궁금한점 리뷰 좀 남겨두었습니다!!
앞 pr이랑 커밋이 섞여있어서 앞 pr이 머지되고 rebase 되면 다시 확인해볼게욥

Copy link
Contributor

Choose a reason for hiding this comment

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

엇 이건 왜 추가하신거죠? (어떤 기능인지 몰라서,,ㅎ)

Copy link
Contributor

Choose a reason for hiding this comment

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

#173 (comment)

앞 pr에서 코래가 친절하게 설명해주었습니다~ ㅎㅎ

Copy link
Member Author

Choose a reason for hiding this comment

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

피드 삭제 에서 토끼가 말해준것처럼 jpa의 영속성의 1차 캐시를 비워주는 역할을합니다! 다른 트랜잭션에서 수행되는 메서드라편 필요없겠지만 UPATE,DELETE 쿼리를 날릴 시에 같은 여러 엔티티들을 한 트랜잭션에 처리하기 때문에 캐시 불일치로인한 오류를 막기 위해 추가했습니닷

Copy link
Contributor

Choose a reason for hiding this comment

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

고수 LG™

Comment on lines 34 to 42
@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("""
DELETE FROM CommentLikeJpaEntity cl
WHERE cl.commentJpaEntity.commentId IN (
SELECT c.commentId FROM CommentJpaEntity c
WHERE c.postJpaEntity.postId = :postId
)
""")
void deleteAllByPostId(@Param("postId") Long postId);
Copy link
Contributor

Choose a reason for hiding this comment

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

오 서브쿼리를 안 사용해도 위와 같은 방식을 사용해도 될 것 같은데요??

@coderabbitai 저러면 commentJpaEntity에서 postJpaEntity를 참조할때 추가적인 SELECT 쿼리는 안나가지??

buzz0331
buzz0331 previously approved these changes Aug 10, 2025
@hd0rable hd0rable dismissed buzz0331’s stale review August 11, 2025 05:11

The merge-base changed after approval.

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-231] [feat] 기록 삭제 api 개발

3 participants