Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
f7ed1f0
[feat] BaseJpaEntity 에 hibernate filter 정의 (#277)
seongjunnoh Aug 29, 2025
5daea4c
[feat] spring boot aop 활용하기 위해 의존성 추가 (#277)
seongjunnoh Aug 29, 2025
68c759c
[feat] statusFilter Aspect 도입 (#277)
seongjunnoh Aug 29, 2025
9633f59
[feat] IncludeInactive, Unfiltered 어노테이션 도입 (#277)
seongjunnoh Aug 29, 2025
ee75622
[test] filter 관련 테스트 코드 작성 (#277)
seongjunnoh Aug 29, 2025
50bd3e7
[move] 기존 custom 어노테이션 패키지 이동 (#277)
seongjunnoh Aug 29, 2025
89af422
[refactor] jpa entity 의 연관관계 nullable 제약조건 수정 (#277)
seongjunnoh Aug 29, 2025
64420fd
[refactor] 서비스 메서드에 누락된 transactional 어노테이션 추가 (#277)
seongjunnoh Aug 29, 2025
f77a03b
[test] jpa repository의 디폴트 findById 메서드의 동작과 연계한 filter 테스트 코드 추가 (#277)
seongjunnoh Aug 29, 2025
e6e72c4
[feat] soft delete 적용 대상 entity 의 단건 조회용 메서드 추가 (#277)
seongjunnoh Aug 29, 2025
8604465
[refactor] user 관련 영속성 메서드 수정 (#277)
seongjunnoh Aug 29, 2025
4f352bc
[refactor] following 관련 영속성 메서드 수정 (#277)
seongjunnoh Aug 29, 2025
3b4fcec
[refactor] attendanceCheck 관련 영속성 메서드 수정 (#277)
seongjunnoh Aug 29, 2025
7926b2a
[refactor] record 관련 영속성 메서드 수정 (#277)
seongjunnoh Aug 29, 2025
9f4e761
[refactor] vote 관련 영속성 메서드 수정 (#277)
seongjunnoh Aug 29, 2025
487a4ed
[refactor] room 관련 영속성 메서드 수정 (#277)
seongjunnoh Aug 29, 2025
60be983
[refactor] roomParticipant 관련 영속성 메서드 수정 (#277)
seongjunnoh Aug 29, 2025
f35b1e1
[refactor] feed 관련 영속성 메서드 수정 (#277)
seongjunnoh Aug 29, 2025
94158ca
[refactor] 댓글 조회 서비스 코드에 status filter 미적용 명시 (#277)
seongjunnoh Aug 29, 2025
47947a8
[refactor] comment 관련 영속성 코드 수정 (#277)
seongjunnoh Aug 29, 2025
08bcd8b
[refactor] book 관련 영속성 코드 수정 (#277)
seongjunnoh Aug 29, 2025
622cf9a
[refactor] vote 삭제 관련 테스트 코드 수정 (#277)
seongjunnoh Aug 29, 2025
ea0f36b
[refactor] room participant 삭제 관련 테스트 코드 수정 (#277)
seongjunnoh Aug 29, 2025
7a872b4
[refactor] record 삭제 관련 테스트 코드 수정 (#277)
seongjunnoh Aug 29, 2025
5fc2dcf
[refactor] feed 삭제 관련 테스트 코드 수정 (#277)
seongjunnoh Aug 29, 2025
65c387e
[refactor] comment 삭제 관련 테스트 코드 수정 (#277)
seongjunnoh Aug 29, 2025
42ca451
[fix] 테스트 코드 tear down 메서드 수정 (#277)
seongjunnoh Aug 29, 2025
11425df
[refactor] custom 어노테이션 패키지 이동으로 인한 import 문 수정 (#277)
seongjunnoh Aug 29, 2025
dc34e9a
develop merge
seongjunnoh Aug 29, 2025
04bc4c5
[move] status filter 관련 테스트 코드 패키지 이동 (#277)
seongjunnoh Aug 31, 2025
b9dd16f
[test] jpa repository 메서드 내부 동작 검증을 위한 테스트 코드 작성 (#277)
seongjunnoh Aug 31, 2025
2e22e6d
[docs] 잘못된 주석 삭제 (#277)
seongjunnoh Aug 31, 2025
16756f2
[refactor] 필수 연관관계인 jpa entity 들의 optional 제약조건 추가 (#277)
seongjunnoh Sep 1, 2025
6cd777f
[feat] status filter aop에 트랜잭션 가드 추가 (#277)
seongjunnoh Sep 1, 2025
f787539
Merge remote-tracking branch 'origin' into refactor/#277-jpa-entity-f…
seongjunnoh Sep 1, 2025
8c20e29
[feat] 닉네임 중복 검증 api service 메서드에 @Unfiltered 어노테이션 추가 (#277)
seongjunnoh Sep 1, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ dependencies {
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'

testCompileOnly 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'

//Security
implementation 'org.springframework.boot:spring-boot-starter-security'

Expand Down Expand Up @@ -79,6 +82,9 @@ dependencies {
// Spring Boot Actuator
implementation 'org.springframework.boot:spring-boot-starter-actuator'

// AOP
implementation 'org.springframework.boot:spring-boot-starter-aop'

// Flyway
implementation "org.flywaydb:flyway-core"
implementation "org.flywaydb:flyway-mysql"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package konkuk.thip.book.adapter.out.jpa;

import jakarta.persistence.*;
import jakarta.validation.constraints.NotNull;
import konkuk.thip.common.entity.BaseJpaEntity;
import konkuk.thip.user.adapter.out.jpa.UserJpaEntity;
import lombok.*;
Expand All @@ -17,11 +18,11 @@ public class SavedBookJpaEntity extends BaseJpaEntity {
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long savedId;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "user_id", nullable = false)
private UserJpaEntity userJpaEntity;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "book_id")
@ManyToOne(fetch = FetchType.LAZY, optional = false)
Comment on lines +21 to +25
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

굿굿 반영 감삼다

@JoinColumn(name = "book_id", nullable = false)
private BookJpaEntity bookJpaEntity;
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ public void updateForPageCount(Book book) {

@Override
public Book findBookByRoomId(Long roomId) {
BookJpaEntity bookJpaEntity = roomJpaRepository.findById(roomId).orElseThrow(
BookJpaEntity bookJpaEntity = roomJpaRepository.findByRoomId(roomId).orElseThrow(
() -> new EntityNotFoundException(ROOM_NOT_FOUND)
).getBookJpaEntity();
return bookMapper.toDomainEntity(bookJpaEntity);
Expand All @@ -71,7 +71,7 @@ public Book findBookByRoomId(Long roomId) {
// 사용자가 책을 저장
@Override
public void saveSavedBook(Long userId, Long bookId) {
UserJpaEntity user = userJpaRepository.findById(userId)
UserJpaEntity user = userJpaRepository.findByUserId(userId)
.orElseThrow(() -> new EntityNotFoundException(USER_NOT_FOUND));
BookJpaEntity book = bookJpaRepository.findById(bookId)
.orElseThrow(() -> new EntityNotFoundException(BOOK_NOT_FOUND));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,5 +68,4 @@ public CursorBasedList<BookQueryDto> findJoiningRoomsBooksByRoomPercentage(Long
public Set<Long> findUnusedBookIds() {
return bookJpaRepository.findUnusedBookIds();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import java.util.Optional;
import java.util.Set;

public interface BookJpaRepository extends JpaRepository<BookJpaEntity, Long>,BookQueryRepository {
public interface BookJpaRepository extends JpaRepository<BookJpaEntity, Long>, BookQueryRepository {
Optional<BookJpaEntity> findByIsbn(String isbn);

boolean existsByIsbn(String isbn);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,18 +39,21 @@ public class CommentJpaEntity extends BaseJpaEntity {
private int likeCount = 0;

//TODO 상속구조 해지하면서 postType만 가질지, postId + postType가질지 논의 필요
@ManyToOne(fetch = FetchType.LAZY)
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "post_id", nullable = false)
private PostJpaEntity postJpaEntity;

@Enumerated(EnumType.STRING)
@Column(name = "post_type", nullable = false, length = 10)
private PostType postType;

@ManyToOne(fetch = FetchType.LAZY)
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "user_id", nullable = false)
private UserJpaEntity userJpaEntity;

/**
* nullable = true : 최상위 댓글인 경우 null
*/
Comment on lines +54 to +56
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "parent_id")
private CommentJpaEntity parent;
Comment on lines +54 to 59
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Aug 29, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue

상위 댓글 필터링 시 ObjectNotFound 위험 — 매핑에 NotFound/nullable/optional 명시 필요

Hibernate Filter로 부모가 INACTIVE인 경우 지연로딩 시 부모 조회가 필터에 걸려 예외가 날 수 있습니다. @NotFound(IGNORE)와 명시적 nullable/optional로 방어하세요.

-    /**
-     * nullable = true : 최상위 댓글인 경우 null
-     */
-    @ManyToOne(fetch = FetchType.LAZY)
-    @JoinColumn(name = "parent_id")
+    /**
+     * nullable = true : 최상위 댓글인 경우 null
+     * 부모가 필터로 제외될 수 있으므로 NotFound IGNORE로 방어
+     */
+    @NotFound(action = NotFoundAction.IGNORE)
+    @ManyToOne(fetch = FetchType.LAZY, optional = true)
+    @JoinColumn(name = "parent_id", nullable = true)
     private CommentJpaEntity parent;

아래 import 추가가 필요합니다.

import org.hibernate.annotations.NotFound;
import org.hibernate.annotations.NotFoundAction;
🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/comment/adapter/out/jpa/CommentJpaEntity.java
around lines 54 to 59, the lazy parent relation can throw ObjectNotFound when
the parent is filtered out by a Hibernate filter; to fix this mark the
association as optional/nullable and ignore missing targets: add
@NotFound(NotFoundAction.IGNORE) on the parent field, set @ManyToOne(optional =
true, fetch = FetchType.LAZY) and @JoinColumn(name = "parent_id", nullable =
true), and add the required imports (org.hibernate.annotations.NotFound and
org.hibernate.annotations.NotFoundAction).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

현재 프로젝트는 헥사고날 아키텍처를 따르고 있어서 jpa entity와 도메인 entity를 분리해 사용하고 있으므로, Jpa entity 필드의 지연로딩이 발생하는 유스케이스는 없어.
(매번 jpa entity -> domain entity 로 매핑한 후, 어플리케이션 레어에서는 도메인 엔티티만 바라보고 있음.
추가로 comment domain entity 에서는 parent의 Long Id 값 만을 참조하고 있는데, parent가 없거나 inactive 라면 이 값은 null로 매핑됨)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@seongjunnoh, 완전히 맞는 말씀입니다! 헥사고날 아키텍처에서 JPA entity와 domain entity를 명확히 분리하고 계시는군요.

JPA entity가 항상 domain entity로 매핑된 후 어플리케이션 레이어에서는 도메인 엔티티만 사용하시니, 제가 우려했던 지연로딩 시 ObjectNotFound 예외 상황은 발생할 수 없습니다. 특히 comment domain entity에서 parent를 Long ID로만 참조하고 계시니, parent가 inactive이거나 없는 경우에도 단순히 null로 매핑되어 안전하게 처리되겠네요.

아키텍처 패턴을 고려하지 못한 제안이었습니다. 명확한 설명 감사합니다!


✏️ Learnings added
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#287
File: src/main/java/konkuk/thip/comment/adapter/out/jpa/CommentJpaEntity.java:54-59
Timestamp: 2025-09-01T08:35:56.409Z
Learning: THIP 프로젝트는 헥사고날 아키텍처를 사용하여 JPA entity와 domain entity를 분리한다. JPA entity는 어댑터 계층에서 도메인 entity로 매핑되고, 어플리케이션 계층에서는 도메인 entity만 사용한다. 따라서 JPA entity 필드의 지연로딩이 발생하는 유스케이스는 없다.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#287
File: src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepositoryImpl.java:100-105
Timestamp: 2025-09-01T07:10:50.427Z
Learning: 댓글 시스템에서 '삭제된 부모댓글이라도, 그 하위에 active 인 자식댓글이 존재하면 이를 보여줘야한다'는 비즈니스 요구사항이 있다. 따라서 findAllActiveChildCommentsByCreatedAtAsc 메서드는 부모 댓글의 status와 관계없이 모든 active 상태인 자식 댓글들을 조회하는 것이 올바른 동작이다.

Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#113
File: src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/RecentSearchCommandPersistenceAdapter.java:38-44
Timestamp: 2025-07-30T14:05:04.945Z
Learning: seongjunnoh는 코드 최적화 제안에 대해 구체적인 기술적 근거와 효율성 차이를 이해하고 싶어하며, 성능 개선 방식에 대한 상세한 설명을 선호한다.

Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#93
File: src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryPersistenceAdapter.java:49-114
Timestamp: 2025-07-28T16:44:31.224Z
Learning: seongjunnoh는 코드 중복 문제에 대한 리팩토링 제안을 적극적으로 수용하고 함수형 인터페이스를 활용한 해결책을 선호한다.

Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#285
File: src/main/java/konkuk/thip/room/adapter/out/jpa/RoomStatus.java:1-7
Timestamp: 2025-08-31T05:25:14.835Z
Learning: seongjunnoh는 enum 의존성에 대해 유연한 접근을 선호하며, 도메인→어댑터 레이어 참조와 같은 아키텍처 layering 원칙보다 실용적인 구현을 우선시한다.

Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#195
File: src/main/java/konkuk/thip/feed/application/mapper/FeedQueryMapper.java:0-0
Timestamp: 2025-08-13T05:22:32.287Z
Learning: seongjunnoh는 데이터 무결성과 중복 방지에 대한 고민이 깊으며, LinkedHashSet을 활용한 중복 제거와 순서 보장을 동시에 달성하는 솔루션을 선호한다.

Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#278
File: src/main/java/konkuk/thip/common/exception/code/ErrorCode.java:210-211
Timestamp: 2025-08-24T09:40:48.595Z
Learning: seongjunnoh는 HTTP 상태 코드 선택에 대해 기술적 근거와 코드베이스 내 일관성을 중요하게 생각하며, 구체적인 사례 분석을 통한 설명을 선호한다.

Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#180
File: src/main/java/konkuk/thip/room/application/service/AttendanceCheckCreateService.java:22-40
Timestamp: 2025-08-14T09:15:31.371Z
Learning: seongjunnoh는 기술적 완벽성보다 실제 비즈니스 시나리오와 사용자 행동 패턴을 우선 고려하며, 발생 가능성이 낮은 엣지 케이스보다는 실용적인 구현을 선호한다.

Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#278
File: src/main/java/konkuk/thip/TestTokenController.java:0-0
Timestamp: 2025-08-24T09:33:52.982Z
Learning: seongjunnoh는 Spring의 ConditionalOnProperty 동작 원리를 정확히 이해하고 있으며, 보안 이슈에 대해서도 실질적인 위험성을 기준으로 판단하는 실용적 접근을 선호한다.

Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#112
File: src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepositoryImpl.java:272-272
Timestamp: 2025-07-30T10:44:34.115Z
Learning: seongjunnoh는 피드 커서 페이지네이션에서 LocalDateTime 단일 커서 방식을 선호하며, 복합 키 기반 커서보다 구현 단순성과 성능을 우선시한다.

Expand All @@ -71,5 +74,4 @@ public CommentJpaEntity updateFrom(Comment comment) {
public void updateLikeCount(int likeCount) {
this.likeCount = likeCount;
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ public class CommentLikeJpaEntity extends BaseJpaEntity {
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long likeId;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "user_id", nullable = false)
private UserJpaEntity userJpaEntity;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "comment_id")
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "comment_id", nullable = false)
private CommentJpaEntity commentJpaEntity;
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@

import java.util.Optional;

import static konkuk.thip.common.entity.StatusType.ACTIVE;
import static konkuk.thip.common.exception.code.ErrorCode.*;

@Repository
Expand All @@ -39,7 +38,7 @@ public class CommentCommandPersistenceAdapter implements CommentCommandPort {
public Long save(Comment comment) {

// 1. 작성자(User) 조회 및 존재 검증
UserJpaEntity userJpaEntity = userJpaRepository.findById(comment.getCreatorId()).orElseThrow(
UserJpaEntity userJpaEntity = userJpaRepository.findByUserId(comment.getCreatorId()).orElseThrow(
() -> new EntityNotFoundException(USER_NOT_FOUND)
);

Expand All @@ -49,7 +48,7 @@ public Long save(Comment comment) {
// 3. 부모 댓글 조회 (있을 경우)
CommentJpaEntity parentCommentJpaEntity = null;
if (comment.getParentCommentId() != null) {
parentCommentJpaEntity = commentJpaRepository.findById(comment.getParentCommentId())
parentCommentJpaEntity = commentJpaRepository.findByCommentId(comment.getParentCommentId())
.orElseThrow(() -> new EntityNotFoundException(COMMENT_NOT_FOUND));
}

Expand All @@ -60,24 +59,24 @@ public Long save(Comment comment) {

private PostJpaEntity findPostJpaEntity(PostType postType, Long postId) {
return switch (postType) {
case FEED -> feedJpaRepository.findById(postId)
case FEED -> feedJpaRepository.findByPostId(postId)
.orElseThrow(() -> new EntityNotFoundException(FEED_NOT_FOUND));
case RECORD -> recordJpaRepository.findById(postId)
case RECORD -> recordJpaRepository.findByPostId(postId)
.orElseThrow(() -> new EntityNotFoundException(RECORD_NOT_FOUND));
case VOTE -> voteJpaRepository.findById(postId)
case VOTE -> voteJpaRepository.findByPostId(postId)
.orElseThrow(() -> new EntityNotFoundException(VOTE_NOT_FOUND));
};
}

@Override
public Optional<Comment> findById(Long id) {
return commentJpaRepository.findByCommentIdAndStatus(id, ACTIVE)
return commentJpaRepository.findByCommentId(id)
.map(commentMapper::toDomainEntity);
}

@Override
public void update(Comment comment) {
CommentJpaEntity commentJpaEntity = commentJpaRepository.findById(comment.getId()).orElseThrow(
CommentJpaEntity commentJpaEntity = commentJpaRepository.findByCommentId(comment.getId()).orElseThrow(
() -> new EntityNotFoundException(COMMENT_NOT_FOUND)
);

Expand All @@ -86,7 +85,7 @@ public void update(Comment comment) {

@Override
public void delete(Comment comment) {
CommentJpaEntity commentJpaEntity = commentJpaRepository.findById(comment.getId()).orElseThrow(
CommentJpaEntity commentJpaEntity = commentJpaRepository.findByCommentId(comment.getId()).orElseThrow(
() -> new EntityNotFoundException(COMMENT_NOT_FOUND)
);
commentJpaRepository.delete(commentJpaEntity);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public class CommentLikeCommandPersistenceAdapter implements CommentLikeCommandP
@Override
public void save(Long userId, Long commentId) {

UserJpaEntity user = userJpaRepository.findById(userId)
UserJpaEntity user = userJpaRepository.findByUserId(userId)
.orElseThrow(() -> new EntityNotFoundException(USER_NOT_FOUND));
CommentJpaEntity comment = commentJpaRepository.findById(commentId)
.orElseThrow(() -> new EntityNotFoundException(COMMENT_NOT_FOUND));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package konkuk.thip.comment.adapter.out.persistence.repository;

import konkuk.thip.comment.adapter.out.jpa.CommentJpaEntity;
import konkuk.thip.common.entity.StatusType;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
Expand All @@ -10,7 +9,11 @@
import java.util.Optional;

public interface CommentJpaRepository extends JpaRepository<CommentJpaEntity, Long>, CommentQueryRepository {
Optional<CommentJpaEntity> findByCommentIdAndStatus(Long commentId, StatusType status);

/**
* 소프트 딜리트 적용 대상 entity 단건 조회 메서드
*/
Optional<CommentJpaEntity> findByCommentId(Long commentId);

@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("UPDATE CommentJpaEntity c SET c.status = 'INACTIVE' WHERE c.postJpaEntity.postId = :postId")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,24 @@
import java.util.*;
import java.util.stream.Collectors;

import static konkuk.thip.common.entity.StatusType.ACTIVE;

@Repository
@RequiredArgsConstructor
public class CommentQueryRepositoryImpl implements CommentQueryRepository {

/**
* 댓글 관련 queryDsl 코드에서는 status 값 명시해야함 (서비스 메서드에서 status filter off가 전제)
*/

private final JPAQueryFactory queryFactory;

private final QCommentJpaEntity comment = QCommentJpaEntity.commentJpaEntity;
private final QUserJpaEntity commentCreator = QUserJpaEntity.userJpaEntity;
private final QCommentJpaEntity parentComment = new QCommentJpaEntity("parentComment");
private final QUserJpaEntity parentCommentCreator = new QUserJpaEntity("parentCommentCreator");

// 최상위 댓글 조회 (삭제된 댓글 포함, 최신순, 페이징)
@Override
public List<CommentQueryDto> findRootCommentsWithDeletedByCreatedAtDesc(Long postId, String postTypeStr, LocalDateTime lastCreatedAt, int size) {
// 최상위 댓글(size+1) 프로젝션 생성
Expand All @@ -44,6 +51,7 @@ public List<CommentQueryDto> findRootCommentsWithDeletedByCreatedAtDesc(Long pos
BooleanExpression whereClause = comment.postJpaEntity.postId.eq(postId)
.and(comment.postJpaEntity.dtype.eq(postTypeStr)) // dType 필터링 추가
.and(comment.parent.isNull()) // 게시글의 최상위 댓글 조회
.and(commentCreator.status.eq(ACTIVE)) // 댓글 작성자 ACTIVE
.and(lastCreatedAt != null // 최신순 정렬
? comment.createdAt.lt(lastCreatedAt)
: Expressions.TRUE
Expand All @@ -53,7 +61,7 @@ public List<CommentQueryDto> findRootCommentsWithDeletedByCreatedAtDesc(Long pos
return queryFactory
.select(proj)
.from(comment)
.leftJoin(comment.userJpaEntity, commentCreator)
.join(comment.userJpaEntity, commentCreator)
.where(whereClause)
.orderBy(comment.createdAt.desc())
.limit(size + 1) // size + 1 개 조회
Expand Down Expand Up @@ -89,10 +97,11 @@ public List<CommentQueryDto> findAllActiveChildCommentsByCreatedAtAsc(Long rootC
.from(comment)
.leftJoin(comment.parent, parentComment)
.leftJoin(parentComment.userJpaEntity, parentCommentCreator)
.leftJoin(comment.userJpaEntity, commentCreator)
.join(comment.userJpaEntity, commentCreator)
.where(
comment.parent.commentId.in(parentIds), // parentIds 하위의 모든 자식 댓글 조회
comment.status.eq(StatusType.ACTIVE) // 자식 댓글은 ACTIVE인 것만 조회
comment.status.eq(ACTIVE), // 자식 댓글은 ACTIVE인 것만 조회
commentCreator.status.eq(ACTIVE) // 자식 댓글 작성자 ACTIVE
)
Comment on lines +100 to 105
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Aug 29, 2025

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

자식 댓글 조회 시 부모 상태 필터 미적용 — findChildCommentId와 불일치

여기서는 자식 댓글과 작성자만 ACTIVE를 강제하지만, 아래 findChildCommentId에서는 부모도 ACTIVE를 요구합니다. “삭제된 부모 아래 자식 노출 정책”을 일관화해 주세요.

가능한 두 옵션:

  1. 부모가 INACTIVE면 자식도 제외(현행 findChildCommentId에 맞춤):
-                    .where(
-                            comment.parent.commentId.in(parentIds),
-                            comment.status.eq(ACTIVE),
-                            commentCreator.status.eq(ACTIVE)
-                    )
+                    .where(
+                            comment.parent.commentId.in(parentIds),
+                            parentComment.status.eq(ACTIVE),
+                            comment.status.eq(ACTIVE),
+                            commentCreator.status.eq(ACTIVE)
+                    )
  1. 삭제된 부모 아래 자식도 허용(루프들과 일치하도록 단건 조회를 완화):
-                        parentComment.status.eq(ACTIVE),
                         comment.status.eq(ACTIVE),

의도에 맞는 방향으로 선택 부탁드립니다.


🏁 Script executed:

#!/bin/bash
# findChildCommentId 메서드의 쿼리에서 부모 상태 필터 적용 여부 확인
rg -n "findChildCommentId" -C5 src

Length of output: 3525


🏁 Script executed:

#!/bin/bash
# findChildCommentId 메서드의 부모 상태 필터 적용 여부 확인 (join 및 where 절)
sed -n '200,260p' src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepositoryImpl.java

Length of output: 1686


자식 댓글 조회 시 부모 상태(ACTIVE) 필터 누락 — 일관성 유지 필요
findAllActiveChildCommentsByCreatedAtAsc에서는 부모 상태 필터가 빠져 있어, findChildCommentId의 동작과 불일치합니다. 삭제된(비활성) 부모 아래 자식이 노출되지 않도록 아래 중 하나를 적용하세요.

 .where(
-       comment.parent.commentId.in(parentIds),
+       comment.parent.commentId.in(parentIds),
+       parentComment.status.eq(ACTIVE),      // 부모도 ACTIVE인 경우만 조회
        comment.status.eq(ACTIVE),
        commentCreator.status.eq(ACTIVE)
 )
📝 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
.join(comment.userJpaEntity, commentCreator)
.where(
comment.parent.commentId.in(parentIds), // parentIds 하위의 모든 자식 댓글 조회
comment.status.eq(StatusType.ACTIVE) // 자식 댓글은 ACTIVE인 것만 조회
comment.status.eq(ACTIVE), // 자식 댓글은 ACTIVE인 것만 조회
commentCreator.status.eq(ACTIVE) // 자식 댓글 작성자 ACTIVE
)
.join(comment.userJpaEntity, commentCreator)
.where(
comment.parent.commentId.in(parentIds), // parentIds 하위의 모든 자식 댓글 조회
parentComment.status.eq(ACTIVE), // 부모도 ACTIVE인 경우만 조회
comment.status.eq(ACTIVE), // 자식 댓글은 ACTIVE인 것만 조회
commentCreator.status.eq(ACTIVE) // 자식 댓글 작성자 ACTIVE
)
🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepositoryImpl.java
around lines 100 to 105, the query for finding child comments is missing a
filter that ensures the parent comment is ACTIVE, causing inconsistency with
findChildCommentId; update the JPQL/QueryDSL predicate to include a check for
the parent’s status (e.g., comment.parent.status.eq(ACTIVE)) or join the parent
entity and assert its status is ACTIVE so children under deleted/inactive
parents are excluded.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

위 메서드는 부모 댓글의 status와 관계없이 모든 active 상태인 자식 댓글들을 조회하는 메서드야.
'삭제된 부모댓글이라도, 그 하위에 active 인 자식댓글이 존재하면 이를 보여줘야한다' 라는 요구사항이 있어.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@seongjunnoh 비즈니스 요구사항에 대한 명확한 설명 감사드립니다!

삭제된 부모 댓글 하위의 활성 자식 댓글도 표시해야 한다는 정책을 이해했습니다. 이 경우 현재 구현이 요구사항에 맞게 올바르게 작동하고 있네요. findChildCommentId와 다른 동작을 하는 것도 각각의 용도가 다르기 때문이군요.

향후 리뷰에서 이런 도메인별 정책을 고려하도록 하겠습니다.


✏️ Learnings added
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#287
File: src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepositoryImpl.java:100-105
Timestamp: 2025-09-01T07:10:50.427Z
Learning: 댓글 시스템에서 '삭제된 부모댓글이라도, 그 하위에 active 인 자식댓글이 존재하면 이를 보여줘야한다'는 비즈니스 요구사항이 있다. 따라서 findAllActiveChildCommentsByCreatedAtAsc 메서드는 부모 댓글의 status와 관계없이 모든 active 상태인 자식 댓글들을 조회하는 것이 올바른 동작이다.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#113
File: src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/RecentSearchCommandPersistenceAdapter.java:38-44
Timestamp: 2025-07-30T14:05:04.945Z
Learning: seongjunnoh는 코드 최적화 제안에 대해 구체적인 기술적 근거와 효율성 차이를 이해하고 싶어하며, 성능 개선 방식에 대한 상세한 설명을 선호한다.

Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#93
File: src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryPersistenceAdapter.java:49-114
Timestamp: 2025-07-28T16:44:31.224Z
Learning: seongjunnoh는 코드 중복 문제에 대한 리팩토링 제안을 적극적으로 수용하고 함수형 인터페이스를 활용한 해결책을 선호한다.

Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#285
File: src/main/java/konkuk/thip/room/adapter/out/jpa/RoomStatus.java:1-7
Timestamp: 2025-08-31T05:25:14.835Z
Learning: seongjunnoh는 enum 의존성에 대해 유연한 접근을 선호하며, 도메인→어댑터 레이어 참조와 같은 아키텍처 layering 원칙보다 실용적인 구현을 우선시한다.

Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#195
File: src/main/java/konkuk/thip/feed/application/mapper/FeedQueryMapper.java:0-0
Timestamp: 2025-08-13T05:22:32.287Z
Learning: seongjunnoh는 데이터 무결성과 중복 방지에 대한 고민이 깊으며, LinkedHashSet을 활용한 중복 제거와 순서 보장을 동시에 달성하는 솔루션을 선호한다.

Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#180
File: src/main/java/konkuk/thip/room/application/service/AttendanceCheckCreateService.java:22-40
Timestamp: 2025-08-14T09:15:31.371Z
Learning: seongjunnoh는 기술적 완벽성보다 실제 비즈니스 시나리오와 사용자 행동 패턴을 우선 고려하며, 발생 가능성이 낮은 엣지 케이스보다는 실용적인 구현을 선호한다.

Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#278
File: src/main/java/konkuk/thip/common/exception/code/ErrorCode.java:210-211
Timestamp: 2025-08-24T09:40:48.595Z
Learning: seongjunnoh는 HTTP 상태 코드 선택에 대해 기술적 근거와 코드베이스 내 일관성을 중요하게 생각하며, 구체적인 사례 분석을 통한 설명을 선호한다.

Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#278
File: src/main/java/konkuk/thip/TestTokenController.java:0-0
Timestamp: 2025-08-24T09:33:52.982Z
Learning: seongjunnoh는 Spring의 ConditionalOnProperty 동작 원리를 정확히 이해하고 있으며, 보안 이슈에 대해서도 실질적인 위험성을 기준으로 판단하는 실용적 접근을 선호한다.

Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#112
File: src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepositoryImpl.java:272-272
Timestamp: 2025-07-30T10:44:34.115Z
Learning: seongjunnoh는 피드 커서 페이지네이션에서 LocalDateTime 단일 커서 방식을 선호하며, 복합 키 기반 커서보다 구현 단순성과 성능을 우선시한다.

.fetch();

Expand Down Expand Up @@ -148,10 +157,11 @@ public Map<Long, List<CommentQueryDto>> findAllActiveChildCommentsByCreatedAtAsc
.from(comment)
.leftJoin(comment.parent, parentComment)
.leftJoin(parentComment.userJpaEntity, parentCommentCreator)
.leftJoin(comment.userJpaEntity, commentCreator)
.join(comment.userJpaEntity, commentCreator)
.where(
comment.parent.commentId.in(parentIds), // parentIds 하위의 모든 자식 댓글 조회
comment.status.eq(StatusType.ACTIVE) // 자식 댓글은 ACTIVE인 것만 조회
comment.status.eq(ACTIVE), // 자식 댓글은 ACTIVE인 것만 조회
commentCreator.status.eq(ACTIVE) // 자식 댓글 작성자 ACTIVE
)
.fetch();

Expand Down Expand Up @@ -196,7 +206,7 @@ public CommentQueryDto findRootCommentId(Long rootCommentId) {
.join(comment.userJpaEntity, commentCreator)
.where(
comment.commentId.eq(rootCommentId),
comment.status.eq(StatusType.ACTIVE)
comment.status.eq(ACTIVE)
)
.fetchOne();
}
Expand Down Expand Up @@ -225,8 +235,8 @@ public CommentQueryDto findChildCommentId(Long rootCommentId, Long replyCommentI
.join(comment.userJpaEntity, commentCreator)
.where(
comment.parent.commentId.eq(rootCommentId),
parentComment.status.eq(StatusType.ACTIVE),
comment.status.eq(StatusType.ACTIVE),
parentComment.status.eq(ACTIVE),
comment.status.eq(ACTIVE),
comment.commentId.eq(replyCommentId)
)
.fetchOne();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@
import konkuk.thip.comment.application.port.out.CommentLikeQueryPort;
import konkuk.thip.comment.application.port.out.CommentQueryPort;
import konkuk.thip.comment.application.port.out.dto.CommentQueryDto;
import konkuk.thip.common.annotation.persistence.Unfiltered;
import konkuk.thip.common.util.Cursor;
import konkuk.thip.common.util.CursorBasedList;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.*;
import java.util.stream.Collectors;
Expand All @@ -25,6 +27,8 @@ public class CommentShowAllService implements CommentShowAllUseCase {
private final CommentQueryMapper commentQueryMapper;

@Override
@Transactional(readOnly = true)
@Unfiltered
public CommentForSinglePostResponse showAllCommentsOfPost(CommentShowAllQuery query) {
Comment on lines +30 to 32
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Aug 29, 2025

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

@unfiltered로 자식 댓글까지 비활성 포함되는 부작용 발생

메서드 단위 @unfiltered는 전체 트랜잭션에서 필터를 꺼서, 이름상 ‘findAllActiveChildCommentsOldestFirst’까지 INACTIVE를 포함시킬 위험이 큽니다. 요구사항은 “루트는 삭제 포함 조회, 자식은 ACTIVE만”으로 보입니다.

서비스에서 @unfiltered를 제거하고, “루트 댓글 조회”를 수행하는 어댑터/포트 메서드에만 @unfiltered를 부여해 범위를 축소하세요.

     @Override
-    @Transactional(readOnly = true)
-    @Unfiltered
+    @Transactional(readOnly = true)
     public CommentForSinglePostResponse showAllCommentsOfPost(CommentShowAllQuery query) {

추가로, 루트 조회용 포트/리포지토리 메서드(예: findLatestRootCommentsWithDeleted)에 @unfiltered를 선언하거나, 어댑터 내부에서 일시적으로 필터 on/off를 제어하는 구현을 분리하는 것을 권장합니다(같은 빈 내 self-invocation 주의).

📝 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
@Transactional(readOnly = true)
@Unfiltered
public CommentForSinglePostResponse showAllCommentsOfPost(CommentShowAllQuery query) {
@Override
@Transactional(readOnly = true)
public CommentForSinglePostResponse showAllCommentsOfPost(CommentShowAllQuery query) {
// …
}
🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/comment/application/service/CommentShowAllService.java
around lines 30-32, remove the class/method-level @Unfiltered so the whole
transaction no longer disables the filter (which currently causes child comments
to include INACTIVE), and instead apply @Unfiltered narrowly to the adapter/port
method that loads root comments (e.g., the repository method like
findLatestRootCommentsWithDeleted or the adapter method that queries roots).
Update the service to call a dedicated port/repository method annotated with
@Unfiltered (or have the adapter temporarily enable/disable the filter around
the root-query) so roots are fetched including deleted while child-comment
queries remain filtered to ACTIVE; if you toggle filters inside the same bean,
avoid self-invocation by moving toggle logic to a separate component or the
repository/adapter implementation.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

댓글 조회 서비스 메서드의 하위 동작에서는 status filter를 끄고, 대신 하위 영속성 메서드에 명시적으로 요구사항에 맞는 status 조건을 붙이는 식으로 구현했으므로 문제될 건 없다고 생각해.

Copy link
Copy Markdown

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!

Cursor cursor = Cursor.from(query.cursorStr(), PAGE_SIZE);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package konkuk.thip.common.annotation;
package konkuk.thip.common.annotation.application;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

굿 패키지 분리 좋습니다


import org.springframework.core.annotation.AliasFor;
import org.springframework.stereotype.Component;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package konkuk.thip.common.annotation;
package konkuk.thip.common.annotation.application;

import org.springframework.core.annotation.AliasFor;
import org.springframework.stereotype.Service;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package konkuk.thip.common.annotation.persistence;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface IncludeInactive {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package konkuk.thip.common.annotation.persistence;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Unfiltered {
}
Loading