diff --git a/build.gradle b/build.gradle index f49aae725..9258d190c 100644 --- a/build.gradle +++ b/build.gradle @@ -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' @@ -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" diff --git a/src/main/java/konkuk/thip/book/adapter/out/jpa/SavedBookJpaEntity.java b/src/main/java/konkuk/thip/book/adapter/out/jpa/SavedBookJpaEntity.java index b910d2441..9a3c6ce8a 100644 --- a/src/main/java/konkuk/thip/book/adapter/out/jpa/SavedBookJpaEntity.java +++ b/src/main/java/konkuk/thip/book/adapter/out/jpa/SavedBookJpaEntity.java @@ -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.*; @@ -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) + @JoinColumn(name = "book_id", nullable = false) private BookJpaEntity bookJpaEntity; } \ No newline at end of file diff --git a/src/main/java/konkuk/thip/book/adapter/out/persistence/BookCommandPersistenceAdapter.java b/src/main/java/konkuk/thip/book/adapter/out/persistence/BookCommandPersistenceAdapter.java index 91e40f8a1..4ec356b35 100644 --- a/src/main/java/konkuk/thip/book/adapter/out/persistence/BookCommandPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/book/adapter/out/persistence/BookCommandPersistenceAdapter.java @@ -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); @@ -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)); diff --git a/src/main/java/konkuk/thip/book/adapter/out/persistence/BookQueryPersistenceAdapter.java b/src/main/java/konkuk/thip/book/adapter/out/persistence/BookQueryPersistenceAdapter.java index 8927c5453..aa9b2bd6c 100644 --- a/src/main/java/konkuk/thip/book/adapter/out/persistence/BookQueryPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/book/adapter/out/persistence/BookQueryPersistenceAdapter.java @@ -68,5 +68,4 @@ public CursorBasedList findJoiningRoomsBooksByRoomPercentage(Long public Set findUnusedBookIds() { return bookJpaRepository.findUnusedBookIds(); } - } diff --git a/src/main/java/konkuk/thip/book/adapter/out/persistence/repository/BookJpaRepository.java b/src/main/java/konkuk/thip/book/adapter/out/persistence/repository/BookJpaRepository.java index dec4dd285..726b7079d 100644 --- a/src/main/java/konkuk/thip/book/adapter/out/persistence/repository/BookJpaRepository.java +++ b/src/main/java/konkuk/thip/book/adapter/out/persistence/repository/BookJpaRepository.java @@ -7,7 +7,7 @@ import java.util.Optional; import java.util.Set; -public interface BookJpaRepository extends JpaRepository,BookQueryRepository { +public interface BookJpaRepository extends JpaRepository, BookQueryRepository { Optional findByIsbn(String isbn); boolean existsByIsbn(String isbn); diff --git a/src/main/java/konkuk/thip/comment/adapter/out/jpa/CommentJpaEntity.java b/src/main/java/konkuk/thip/comment/adapter/out/jpa/CommentJpaEntity.java index 68bde5025..760283133 100644 --- a/src/main/java/konkuk/thip/comment/adapter/out/jpa/CommentJpaEntity.java +++ b/src/main/java/konkuk/thip/comment/adapter/out/jpa/CommentJpaEntity.java @@ -39,7 +39,7 @@ 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; @@ -47,10 +47,13 @@ public class CommentJpaEntity extends BaseJpaEntity { @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 + */ @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "parent_id") private CommentJpaEntity parent; @@ -71,5 +74,4 @@ public CommentJpaEntity updateFrom(Comment comment) { public void updateLikeCount(int likeCount) { this.likeCount = likeCount; } - -} \ No newline at end of file +} diff --git a/src/main/java/konkuk/thip/comment/adapter/out/jpa/CommentLikeJpaEntity.java b/src/main/java/konkuk/thip/comment/adapter/out/jpa/CommentLikeJpaEntity.java index 15f46203c..59f664ab0 100644 --- a/src/main/java/konkuk/thip/comment/adapter/out/jpa/CommentLikeJpaEntity.java +++ b/src/main/java/konkuk/thip/comment/adapter/out/jpa/CommentLikeJpaEntity.java @@ -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; } \ No newline at end of file diff --git a/src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentCommandPersistenceAdapter.java b/src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentCommandPersistenceAdapter.java index 2cc237cc5..eacfca1a3 100644 --- a/src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentCommandPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentCommandPersistenceAdapter.java @@ -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 @@ -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) ); @@ -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)); } @@ -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 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) ); @@ -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); diff --git a/src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentLikeCommandPersistenceAdapter.java b/src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentLikeCommandPersistenceAdapter.java index 138e08c98..53c774908 100644 --- a/src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentLikeCommandPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentLikeCommandPersistenceAdapter.java @@ -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)); diff --git a/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentJpaRepository.java b/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentJpaRepository.java index c0352929b..aee0ebe3c 100644 --- a/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentJpaRepository.java +++ b/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentJpaRepository.java @@ -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; @@ -10,7 +9,11 @@ import java.util.Optional; public interface CommentJpaRepository extends JpaRepository, CommentQueryRepository { - Optional findByCommentIdAndStatus(Long commentId, StatusType status); + + /** + * 소프트 딜리트 적용 대상 entity 단건 조회 메서드 + */ + Optional findByCommentId(Long commentId); @Modifying(clearAutomatically = true, flushAutomatically = true) @Query("UPDATE CommentJpaEntity c SET c.status = 'INACTIVE' WHERE c.postJpaEntity.postId = :postId") diff --git a/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepositoryImpl.java b/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepositoryImpl.java index 89ea08f02..193ef0795 100644 --- a/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepositoryImpl.java +++ b/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepositoryImpl.java @@ -15,10 +15,16 @@ 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; @@ -26,6 +32,7 @@ public class CommentQueryRepositoryImpl implements CommentQueryRepository { private final QCommentJpaEntity parentComment = new QCommentJpaEntity("parentComment"); private final QUserJpaEntity parentCommentCreator = new QUserJpaEntity("parentCommentCreator"); + // 최상위 댓글 조회 (삭제된 댓글 포함, 최신순, 페이징) @Override public List findRootCommentsWithDeletedByCreatedAtDesc(Long postId, String postTypeStr, LocalDateTime lastCreatedAt, int size) { // 최상위 댓글(size+1) 프로젝션 생성 @@ -44,6 +51,7 @@ public List 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 @@ -53,7 +61,7 @@ public List 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 개 조회 @@ -89,10 +97,11 @@ public List 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 ) .fetch(); @@ -148,10 +157,11 @@ public Map> 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(); @@ -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(); } @@ -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(); diff --git a/src/main/java/konkuk/thip/comment/application/service/CommentShowAllService.java b/src/main/java/konkuk/thip/comment/application/service/CommentShowAllService.java index 9c3131c29..69dbc40ef 100644 --- a/src/main/java/konkuk/thip/comment/application/service/CommentShowAllService.java +++ b/src/main/java/konkuk/thip/comment/application/service/CommentShowAllService.java @@ -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; @@ -25,6 +27,8 @@ public class CommentShowAllService implements CommentShowAllUseCase { private final CommentQueryMapper commentQueryMapper; @Override + @Transactional(readOnly = true) + @Unfiltered public CommentForSinglePostResponse showAllCommentsOfPost(CommentShowAllQuery query) { Cursor cursor = Cursor.from(query.cursorStr(), PAGE_SIZE); diff --git a/src/main/java/konkuk/thip/common/annotation/DomainService.java b/src/main/java/konkuk/thip/common/annotation/application/DomainService.java similarity index 89% rename from src/main/java/konkuk/thip/common/annotation/DomainService.java rename to src/main/java/konkuk/thip/common/annotation/application/DomainService.java index 5dcaae9da..b42694cc5 100644 --- a/src/main/java/konkuk/thip/common/annotation/DomainService.java +++ b/src/main/java/konkuk/thip/common/annotation/application/DomainService.java @@ -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.Component; diff --git a/src/main/java/konkuk/thip/common/annotation/HelperService.java b/src/main/java/konkuk/thip/common/annotation/application/HelperService.java similarity index 86% rename from src/main/java/konkuk/thip/common/annotation/HelperService.java rename to src/main/java/konkuk/thip/common/annotation/application/HelperService.java index 4eacdc6b3..eeab77534 100644 --- a/src/main/java/konkuk/thip/common/annotation/HelperService.java +++ b/src/main/java/konkuk/thip/common/annotation/application/HelperService.java @@ -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; diff --git a/src/main/java/konkuk/thip/common/annotation/persistence/IncludeInactive.java b/src/main/java/konkuk/thip/common/annotation/persistence/IncludeInactive.java new file mode 100644 index 000000000..d4faebbda --- /dev/null +++ b/src/main/java/konkuk/thip/common/annotation/persistence/IncludeInactive.java @@ -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 { +} diff --git a/src/main/java/konkuk/thip/common/annotation/persistence/Unfiltered.java b/src/main/java/konkuk/thip/common/annotation/persistence/Unfiltered.java new file mode 100644 index 000000000..9dfcff5f4 --- /dev/null +++ b/src/main/java/konkuk/thip/common/annotation/persistence/Unfiltered.java @@ -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 { +} diff --git a/src/main/java/konkuk/thip/common/aop/StatusFilterAspect.java b/src/main/java/konkuk/thip/common/aop/StatusFilterAspect.java new file mode 100644 index 000000000..916165723 --- /dev/null +++ b/src/main/java/konkuk/thip/common/aop/StatusFilterAspect.java @@ -0,0 +1,136 @@ +package konkuk.thip.common.aop; + +import jakarta.persistence.EntityManager; +import konkuk.thip.common.entity.StatusType; +import konkuk.thip.common.exception.InvalidStateException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.hibernate.Session; +import org.springframework.stereotype.Component; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +import java.util.List; + +import static konkuk.thip.common.exception.code.ErrorCode.PERSISTENCE_TRANSACTION_REQUIRED; + +@Slf4j +@Aspect +@Component +@RequiredArgsConstructor +public class StatusFilterAspect { + + private final EntityManager em; + + /** + * Hibernate Session은 thread-not-safe 하므로 반드시 트랜잭션 경계 내에서만 사용해야함 + * 현재 스레드에 바인딩된 EntityManager를 통해 세션을 획득하도록 강제 + */ + private Session currentTxSession() { + if (!TransactionSynchronizationManager.isActualTransactionActive()) { + throw new InvalidStateException(PERSISTENCE_TRANSACTION_REQUIRED); + } + return session(); + } + + private Session session() { + return em.unwrap(Session.class); // 현재 스레드의 em에서 Hibernate 세션 얻기 + } + + private static final String FILTER_NAME = "statusFilter"; + private static final String PARAM_STATUSES = "statuses"; + + private static final String ANN_TX = "org.springframework.transaction.annotation.Transactional"; + private static final String ANN_INCLUDE_INACTIVE = "konkuk.thip.common.annotation.persistence.IncludeInactive"; + private static final String ANN_UNFILTERED = "konkuk.thip.common.annotation.persistence.Unfiltered"; + + // 기본: ACTIVE만 (트랜잭션 경계 진입 시) + // 1) @Transactional 이고 + // 2) @IncludeInactive, @Unfiltered 가 붙어있지 않은 경우에만 적용 + private static final String PCUT_TX_DEFAULT = + "(" + "@annotation(" + ANN_TX + ") || @within(" + ANN_TX + ")" + ")" + + " && !" + "@annotation(" + ANN_INCLUDE_INACTIVE + ")" + + " && !" + "@annotation(" + ANN_UNFILTERED + ")"; + + // @IncludeInactive: 트랜잭션 컨텍스트가 보장된 경우에만 동작 + private static final String PCUT_INCLUDE_INACTIVE = + "@annotation(" + ANN_INCLUDE_INACTIVE + ") && (" + "@annotation(" + ANN_TX + ") || @within(" + ANN_TX + ")" + ")"; + + // @Unfiltered: 트랜잭션 컨텍스트가 보장된 경우에만 동작 + private static final String PCUT_UNFILTERED = + "@annotation(" + ANN_UNFILTERED + ") && (" + "@annotation(" + ANN_TX + ") || @within(" + ANN_TX + ")" + ")"; + + // 기본: ACTIVE만 + @Around(PCUT_TX_DEFAULT) + public Object enableActiveByDefault(ProceedingJoinPoint pjp) throws Throwable { + var s = currentTxSession(); + var wasEnabled = isFilterEnabled(s); + if (!wasEnabled) { + enableFilterWith(s, List.of(StatusType.ACTIVE.name())); + } + try { + return pjp.proceed(); + } finally { + if (!wasEnabled) { + disableFilter(s); + } + } + } + + // Include Inactive: ACTIVE, INACTIVE 모두 + 종료 시 active-only 로 복귀 + @Around(PCUT_INCLUDE_INACTIVE) + public Object includeInactive(ProceedingJoinPoint pjp) throws Throwable { + var s = currentTxSession(); + var prevEnabled = isFilterEnabled(s); + + enableFilterWith(s, List.of(StatusType.ACTIVE.name(), StatusType.INACTIVE.name())); + + try { + return pjp.proceed(); + } finally { + restoreToActive(s); + if (!prevEnabled) { + disableFilter(s); + } + } + } + + // Unfiltered: 필터 해제 + 종료 시 active-only 로 복귀 + @Around(PCUT_UNFILTERED) + public Object unfiltered(ProceedingJoinPoint pjp) throws Throwable { + var s = currentTxSession(); + var wasEnabled = isFilterEnabled(s); + if (wasEnabled) { + disableFilter(s); + } + try { + return pjp.proceed(); + } finally { + if (wasEnabled) { + restoreToActive(s); + } + } + } + + private boolean isFilterEnabled(Session s) { + return s.getEnabledFilter(FILTER_NAME) != null; + } + + private void enableFilterWith(Session s, List statuses) { + s.enableFilter(FILTER_NAME).setParameterList(PARAM_STATUSES, statuses); + log.debug("statusFilter -> ENABLED [statuses={}]", statuses); + } + + private void restoreToActive(Session s) { + var restored = List.of(StatusType.ACTIVE.name()); + s.enableFilter(FILTER_NAME).setParameterList(PARAM_STATUSES, restored); + log.debug("statusFilter -> RESTORED [statuses={}]", restored); + } + + private void disableFilter(Session s) { + s.disableFilter(FILTER_NAME); + log.debug("statusFilter -> DISABLED"); + } +} diff --git a/src/main/java/konkuk/thip/common/entity/BaseJpaEntity.java b/src/main/java/konkuk/thip/common/entity/BaseJpaEntity.java index 26a6e729f..9f33e2410 100644 --- a/src/main/java/konkuk/thip/common/entity/BaseJpaEntity.java +++ b/src/main/java/konkuk/thip/common/entity/BaseJpaEntity.java @@ -7,6 +7,9 @@ import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; import jakarta.persistence.*; import lombok.Getter; +import org.hibernate.annotations.Filter; +import org.hibernate.annotations.FilterDef; +import org.hibernate.annotations.ParamDef; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; @@ -16,20 +19,28 @@ @Getter @MappedSuperclass @EntityListeners(AuditingEntityListener.class) +@FilterDef( + name = "statusFilter", + parameters = @ParamDef(name = "statuses", type = String.class) +) +@Filter( + name = "statusFilter", + condition = "status in (:statuses)" +) public abstract class BaseJpaEntity { @CreatedDate @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") @JsonSerialize(using = LocalDateSerializer.class) @JsonDeserialize(using = LocalDateDeserializer.class) - @Column(name = "created_at",nullable = false, updatable = false) + @Column(name = "created_at", nullable = false, updatable = false) private LocalDateTime createdAt; @LastModifiedDate @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") @JsonSerialize(using = LocalDateSerializer.class) @JsonDeserialize(using = LocalDateDeserializer.class) - @Column(name = "modified_at",nullable = false) + @Column(name = "modified_at", nullable = false) private LocalDateTime modifiedAt; @Enumerated(EnumType.STRING) diff --git a/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java b/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java index f51479bcc..7e7629da0 100644 --- a/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java +++ b/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java @@ -27,6 +27,8 @@ public enum ErrorCode implements ResponseCode { JSON_PROCESSING_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 50100, "JSON 직렬화/역직렬화에 실패했습니다."), AWS_BUCKET_BASE_URL_NOT_CONFIGURED(HttpStatus.INTERNAL_SERVER_ERROR, 50101, "aws s3 bucket base url 설정이 누락되었습니다."), + PERSISTENCE_TRANSACTION_REQUIRED(HttpStatus.INTERNAL_SERVER_ERROR, 50110, "@Transactional 컨텍스트가 필요합니다. 트랜잭션 범위 내에서만 사용할 수 있습니다."), + /* 60000부터 비즈니스 예외 */ /** * 60000 : alias error diff --git a/src/main/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntity.java b/src/main/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntity.java index e52ef1774..d2f2c8520 100644 --- a/src/main/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntity.java +++ b/src/main/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntity.java @@ -20,7 +20,6 @@ import java.util.List; @Entity -//@Table(name = "feeds") @DiscriminatorValue("FEED") @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @@ -33,7 +32,7 @@ public class FeedJpaEntity extends PostJpaEntity { private int reportCount = 0; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "book_id") + @JoinColumn(name = "book_id") // RECORD, VOTE 로 인해 nullable = true로 설정 private BookJpaEntity bookJpaEntity; // JSON 문자열로 저장되는 단일 컬럼 diff --git a/src/main/java/konkuk/thip/feed/adapter/out/jpa/SavedFeedJpaEntity.java b/src/main/java/konkuk/thip/feed/adapter/out/jpa/SavedFeedJpaEntity.java index 2e1f1ea7a..fe1edb0e0 100644 --- a/src/main/java/konkuk/thip/feed/adapter/out/jpa/SavedFeedJpaEntity.java +++ b/src/main/java/konkuk/thip/feed/adapter/out/jpa/SavedFeedJpaEntity.java @@ -17,11 +17,11 @@ public class SavedFeedJpaEntity 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 = "post_id") + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "post_id", nullable = false) private FeedJpaEntity feedJpaEntity; } \ No newline at end of file diff --git a/src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedCommandPersistenceAdapter.java b/src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedCommandPersistenceAdapter.java index 5d7a96a66..792b63b55 100644 --- a/src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedCommandPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedCommandPersistenceAdapter.java @@ -16,7 +16,6 @@ import java.util.Optional; -import static konkuk.thip.common.entity.StatusType.ACTIVE; import static konkuk.thip.common.exception.code.ErrorCode.*; @Repository @@ -32,7 +31,7 @@ public class FeedCommandPersistenceAdapter implements FeedCommandPort { @Override public Optional findById(Long id) { - return feedJpaRepository.findByPostIdAndStatus(id,ACTIVE) + return feedJpaRepository.findByPostId(id) .map(feedMapper::toDomainEntity); } @@ -40,7 +39,7 @@ public Optional findById(Long id) { @Override public Long save(Feed feed) { - UserJpaEntity userJpaEntity = userJpaRepository.findById(feed.getCreatorId()).orElseThrow( + UserJpaEntity userJpaEntity = userJpaRepository.findByUserId(feed.getCreatorId()).orElseThrow( () -> new EntityNotFoundException(USER_NOT_FOUND) ); BookJpaEntity bookJpaEntity = bookJpaRepository.findById(feed.getTargetBookId()).orElseThrow( @@ -51,34 +50,23 @@ public Long save(Feed feed) { // Feed 먼저 영속화 → ID 생성 FeedJpaEntity savedFeed = feedJpaRepository.save(feedJpaEntity); -// // Content가 존재하면 ContentJpaEntity 생성 및 Feed 연관관계 설정 -// applyFeedContents(feed, savedFeed); - // 태그가 존재하면 태그 피드 매핑 생성 및 저장 -// applyFeedTags(feed, savedFeed); - return savedFeed.getPostId(); } @Override public Long update(Feed feed) { - FeedJpaEntity feedJpaEntity = feedJpaRepository.findByPostIdAndStatus(feed.getId(),ACTIVE) + FeedJpaEntity feedJpaEntity = feedJpaRepository.findByPostId(feed.getId()) .orElseThrow(() -> new EntityNotFoundException(FEED_NOT_FOUND)); feedJpaEntity.updateFrom(feed); -// feedJpaEntity.replaceContentList(feed.getContentList()); // 피드 수정시 기존 영속성 컨텍스트 내 엔티티 연결 제거 -// applyFeedContents(feed, feedJpaEntity); -// -// feedTagJpaRepository.deleteAllByFeedId(feedJpaEntity.getPostId()); // 피드 수정시 기존 피드의 모든 FeedTag 매핑 row 삭제 -// applyFeedTags(feed, feedJpaEntity); - return feedJpaEntity.getPostId(); } @Override public void saveSavedFeed(Long userId, Long feedId) { - UserJpaEntity user = userJpaRepository.findById(userId) + UserJpaEntity user = userJpaRepository.findByUserId(userId) .orElseThrow(() -> new EntityNotFoundException(USER_NOT_FOUND)); - FeedJpaEntity feed = feedJpaRepository.findByPostIdAndStatus(feedId,ACTIVE) + FeedJpaEntity feed = feedJpaRepository.findByPostId(feedId) .orElseThrow(() -> new EntityNotFoundException(FEED_NOT_FOUND)); SavedFeedJpaEntity entity = SavedFeedJpaEntity.builder() .userJpaEntity(user) @@ -94,7 +82,7 @@ public void deleteSavedFeed(Long userId, Long feedId) { @Override public void delete(Feed feed) { - FeedJpaEntity feedJpaEntity = feedJpaRepository.findByPostIdAndStatus(feed.getId(),ACTIVE) + FeedJpaEntity feedJpaEntity = feedJpaRepository.findByPostId(feed.getId()) .orElseThrow(() -> new EntityNotFoundException(FEED_NOT_FOUND)); savedFeedJpaRepository.deleteAllByFeedId(feedJpaEntity.getPostId()); diff --git a/src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedQueryPersistenceAdapter.java b/src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedQueryPersistenceAdapter.java index 9a78633ff..a0ffbe84a 100644 --- a/src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedQueryPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedQueryPersistenceAdapter.java @@ -88,12 +88,12 @@ public CursorBasedList findSpecificUserFeedsByCreatedAt(Long feedO @Override public int countAllFeedsByUserId(Long userId) { - return (int) feedJpaRepository.countAllFeedsByUserId(userId, StatusType.ACTIVE); + return (int) feedJpaRepository.countAllFeedsByUserId(userId); } @Override public int countPublicFeedsByUserId(Long userId) { - return (int) feedJpaRepository.countPublicFeedsByUserId(userId, StatusType.ACTIVE); + return (int) feedJpaRepository.countPublicFeedsByUserId(userId); } @Override diff --git a/src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedJpaRepository.java b/src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedJpaRepository.java index 80973ecab..a46ee9d21 100644 --- a/src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedJpaRepository.java +++ b/src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedJpaRepository.java @@ -1,6 +1,5 @@ package konkuk.thip.feed.adapter.out.persistence.repository; -import konkuk.thip.common.entity.StatusType; import konkuk.thip.feed.adapter.out.jpa.FeedJpaEntity; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; @@ -10,11 +9,14 @@ public interface FeedJpaRepository extends JpaRepository, FeedQueryRepository { - @Query("SELECT COUNT(f) FROM FeedJpaEntity f WHERE f.userJpaEntity.userId = :userId AND f.status = :status") - long countAllFeedsByUserId(@Param("userId") Long userId, @Param("status") StatusType status); + /** + * 소프트 딜리트 적용 대상 entity 단건 조회 메서드 + */ + Optional findByPostId(Long postId); - @Query("SELECT COUNT(f) FROM FeedJpaEntity f WHERE f.userJpaEntity.userId = :userId AND f.isPublic = TRUE AND f.status = :status") - long countPublicFeedsByUserId(@Param("userId") Long userId, @Param("status") StatusType status); + @Query("SELECT COUNT(f) FROM FeedJpaEntity f WHERE f.userJpaEntity.userId = :userId") + long countAllFeedsByUserId(@Param("userId") Long userId); - Optional findByPostIdAndStatus(Long postId, StatusType status); + @Query("SELECT COUNT(f) FROM FeedJpaEntity f WHERE f.userJpaEntity.userId = :userId AND f.isPublic = TRUE") + long countPublicFeedsByUserId(@Param("userId") Long userId); } diff --git a/src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepositoryImpl.java b/src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepositoryImpl.java index d9c70715d..4951ecb2e 100644 --- a/src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepositoryImpl.java +++ b/src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepositoryImpl.java @@ -8,7 +8,6 @@ import com.querydsl.jpa.impl.JPAQueryFactory; import jakarta.annotation.Nullable; import konkuk.thip.book.adapter.out.jpa.QBookJpaEntity; -import konkuk.thip.common.entity.StatusType; import konkuk.thip.feed.adapter.out.jpa.FeedJpaEntity; import konkuk.thip.feed.adapter.out.jpa.QFeedJpaEntity; import konkuk.thip.feed.adapter.out.jpa.QSavedFeedJpaEntity; @@ -134,7 +133,6 @@ private List fetchFeedIdsAndPriorityByFollowingPriority(Long userId, Inte .and(following.followingUserJpaEntity.userId.eq(feed.userJpaEntity.userId))) .where( // ACTIVE 인 feed & (내가 작성한 글 or 다른 유저가 작성한 공개글) & cursorCondition - feed.status.eq(StatusType.ACTIVE), feed.userJpaEntity.userId.eq(userId).or(feed.isPublic.eq(true)), cursorCondition ) @@ -152,7 +150,6 @@ private List fetchFeedIdsLatest(Long userId, LocalDateTime lastCreatedAt, .from(feed) .where( // ACTIVE 인 feed & (내가 작성한 글 or 다른 유저가 작성한 공개글) & cursorCondition - feed.status.eq(StatusType.ACTIVE), feed.userJpaEntity.userId.eq(userId).or(feed.isPublic.eq(true)), lastCreatedAt != null ? feed.createdAt.lt(lastCreatedAt) : Expressions.TRUE ) @@ -168,8 +165,8 @@ private List fetchFeedEntitiesByIds(List ids) { return jpaQueryFactory .select(feed).distinct() .from(feed) - .leftJoin(feed.userJpaEntity, user).fetchJoin() - .leftJoin(feed.bookJpaEntity, book).fetchJoin() + .join(feed.userJpaEntity, user).fetchJoin() // user 필수 + .join(feed.bookJpaEntity, book).fetchJoin() // book 필수 .where(feed.postId.in(ids)) .fetch(); } @@ -218,7 +215,6 @@ private List fetchMyFeedIdsByCreatedAt(Long userId, LocalDateTime lastCrea .from(feed) .where( // ACTIVE 인 feed & 내가 작성한 글 & cursorCondition - feed.status.eq(StatusType.ACTIVE), feed.userJpaEntity.userId.eq(userId), lastCreatedAt != null ? feed.createdAt.lt(lastCreatedAt) : Expressions.TRUE ) @@ -233,7 +229,6 @@ private List fetchSpecificUserFeedIdsByCreatedAt(Long userId, LocalDateTim .from(feed) .where( // ACTIVE 인 feed & 특정 유저가 작성한 공개 글 & cursorCondition - feed.status.eq(StatusType.ACTIVE), feed.userJpaEntity.userId.eq(userId), feed.isPublic.eq(Boolean.TRUE), lastCreatedAt != null ? feed.createdAt.lt(lastCreatedAt) : Expressions.TRUE @@ -348,8 +343,7 @@ private QFeedQueryDto toQueryDto() { // 필터링 조건: 책 ISBN & 공개 피드 private BooleanExpression feedByBooksFilter(String isbn, Long userId) { - return feed.status.eq(StatusType.ACTIVE) - .and(feed.bookJpaEntity.isbn.eq(isbn)) + return feed.bookJpaEntity.isbn.eq(isbn) // .and(feed.userJpaEntity.userId.ne(userId)) .and(feed.isPublic.eq(true)); } @@ -361,8 +355,7 @@ public List findLatestPublicFeedCreatorsIn(Set userIds, int size) { .from(feed) .where( feed.userJpaEntity.userId.in(userIds), - feed.isPublic.isTrue(), - feed.status.eq(StatusType.ACTIVE) + feed.isPublic.isTrue() ) .groupBy(feed.userJpaEntity.userId) .orderBy(feed.createdAt.max().desc()) @@ -374,7 +367,6 @@ public List findLatestPublicFeedCreatorsIn(Set userIds, int size) { public List findSavedFeedsByCreatedAt(Long userId, LocalDateTime lastSavedAt, int size) { BooleanExpression where = savedFeed.userJpaEntity.userId.eq(userId) - .and(savedFeed.feedJpaEntity.status.eq(StatusType.ACTIVE)) .and( savedFeed.feedJpaEntity.userJpaEntity.userId.eq(userId) .or(savedFeed.feedJpaEntity.isPublic.eq(true)) diff --git a/src/main/java/konkuk/thip/feed/application/service/FeedRelatedWithBookService.java b/src/main/java/konkuk/thip/feed/application/service/FeedRelatedWithBookService.java index 17cb81365..73849b9a4 100644 --- a/src/main/java/konkuk/thip/feed/application/service/FeedRelatedWithBookService.java +++ b/src/main/java/konkuk/thip/feed/application/service/FeedRelatedWithBookService.java @@ -12,6 +12,7 @@ import konkuk.thip.post.application.port.out.PostLikeQueryPort; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.Set; @@ -30,6 +31,7 @@ public class FeedRelatedWithBookService implements FeedRelatedWithBookUseCase { private static final int DEFAULT_PAGE_SIZE = 10; @Override + @Transactional(readOnly = true) public FeedRelatedWithBookResponse getFeedsByBook(FeedRelatedWithBookQuery query) { // 책이 DB에 존재하는지 확인 diff --git a/src/main/java/konkuk/thip/feed/application/service/FeedShowSingleService.java b/src/main/java/konkuk/thip/feed/application/service/FeedShowSingleService.java index df1df6a32..7b20b0c3f 100644 --- a/src/main/java/konkuk/thip/feed/application/service/FeedShowSingleService.java +++ b/src/main/java/konkuk/thip/feed/application/service/FeedShowSingleService.java @@ -13,6 +13,7 @@ import konkuk.thip.user.domain.User; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.util.Set; @@ -28,6 +29,7 @@ public class FeedShowSingleService implements FeedShowSingleUseCase { private final FeedQueryMapper feedQueryMapper; @Override + @Transactional(readOnly = true) public FeedShowSingleResponse showSingleFeed(Long feedId, Long userId) { // 1. 단일 피드 조회 및 피드 조회 유효성 검증 Feed feed = feedCommandPort.getByIdOrThrow(feedId); diff --git a/src/main/java/konkuk/thip/notification/adapter/out/jpa/NotificationJpaEntity.java b/src/main/java/konkuk/thip/notification/adapter/out/jpa/NotificationJpaEntity.java index 2d7abc0f7..b28d749e5 100644 --- a/src/main/java/konkuk/thip/notification/adapter/out/jpa/NotificationJpaEntity.java +++ b/src/main/java/konkuk/thip/notification/adapter/out/jpa/NotificationJpaEntity.java @@ -26,7 +26,7 @@ public class NotificationJpaEntity extends BaseJpaEntity { @Column(name = "is_checked",nullable = false) private boolean isChecked; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id") + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "user_id", nullable = false) private UserJpaEntity userJpaEntity; } \ No newline at end of file diff --git a/src/main/java/konkuk/thip/post/adapter/out/jpa/PostJpaEntity.java b/src/main/java/konkuk/thip/post/adapter/out/jpa/PostJpaEntity.java index a29a68e88..b8074ca53 100644 --- a/src/main/java/konkuk/thip/post/adapter/out/jpa/PostJpaEntity.java +++ b/src/main/java/konkuk/thip/post/adapter/out/jpa/PostJpaEntity.java @@ -39,7 +39,7 @@ public abstract class PostJpaEntity extends BaseJpaEntity { @Column(name = "dtype", insertable = false, updatable = false) private String dtype; - @ManyToOne(fetch = FetchType.LAZY) + @ManyToOne(fetch = FetchType.LAZY, optional = false) @JoinColumn(name = "user_id", nullable = false) private UserJpaEntity userJpaEntity; diff --git a/src/main/java/konkuk/thip/post/adapter/out/jpa/PostLikeJpaEntity.java b/src/main/java/konkuk/thip/post/adapter/out/jpa/PostLikeJpaEntity.java index c50c272a4..dc483b025 100644 --- a/src/main/java/konkuk/thip/post/adapter/out/jpa/PostLikeJpaEntity.java +++ b/src/main/java/konkuk/thip/post/adapter/out/jpa/PostLikeJpaEntity.java @@ -17,11 +17,11 @@ public class PostLikeJpaEntity extends BaseJpaEntity { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long likeId; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "post_id") + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "post_id", nullable = false) private PostJpaEntity postJpaEntity; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id") + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "user_id", nullable = false) private UserJpaEntity userJpaEntity; } \ No newline at end of file diff --git a/src/main/java/konkuk/thip/post/application/service/handler/PostHandler.java b/src/main/java/konkuk/thip/post/application/service/handler/PostHandler.java index 535791c96..a2bfaa4a8 100644 --- a/src/main/java/konkuk/thip/post/application/service/handler/PostHandler.java +++ b/src/main/java/konkuk/thip/post/application/service/handler/PostHandler.java @@ -1,6 +1,6 @@ package konkuk.thip.post.application.service.handler; -import konkuk.thip.common.annotation.HelperService; +import konkuk.thip.common.annotation.application.HelperService; import konkuk.thip.post.domain.CountUpdatable; import konkuk.thip.post.domain.PostType; import konkuk.thip.feed.application.port.out.FeedCommandPort; diff --git a/src/main/java/konkuk/thip/post/application/service/validator/PostLikeAuthorizationValidator.java b/src/main/java/konkuk/thip/post/application/service/validator/PostLikeAuthorizationValidator.java index 57b65ec45..68a5bbcaf 100644 --- a/src/main/java/konkuk/thip/post/application/service/validator/PostLikeAuthorizationValidator.java +++ b/src/main/java/konkuk/thip/post/application/service/validator/PostLikeAuthorizationValidator.java @@ -1,6 +1,6 @@ package konkuk.thip.post.application.service.validator; -import konkuk.thip.common.annotation.HelperService; +import konkuk.thip.common.annotation.application.HelperService; import konkuk.thip.common.exception.InvalidStateException; import konkuk.thip.post.application.service.policy.PostLikeAccessPolicy; import konkuk.thip.post.domain.CountUpdatable; diff --git a/src/main/java/konkuk/thip/post/domain/service/PostCountService.java b/src/main/java/konkuk/thip/post/domain/service/PostCountService.java index 814c0747a..064a66c48 100644 --- a/src/main/java/konkuk/thip/post/domain/service/PostCountService.java +++ b/src/main/java/konkuk/thip/post/domain/service/PostCountService.java @@ -1,6 +1,6 @@ package konkuk.thip.post.domain.service; -import konkuk.thip.common.annotation.DomainService; +import konkuk.thip.common.annotation.application.DomainService; import konkuk.thip.common.exception.InvalidStateException; import static konkuk.thip.common.exception.code.ErrorCode.POST_LIKE_COUNT_UNDERFLOW; diff --git a/src/main/java/konkuk/thip/recentSearch/adapter/out/jpa/RecentSearchJpaEntity.java b/src/main/java/konkuk/thip/recentSearch/adapter/out/jpa/RecentSearchJpaEntity.java index adc701bcf..4218bd247 100644 --- a/src/main/java/konkuk/thip/recentSearch/adapter/out/jpa/RecentSearchJpaEntity.java +++ b/src/main/java/konkuk/thip/recentSearch/adapter/out/jpa/RecentSearchJpaEntity.java @@ -26,7 +26,7 @@ public class RecentSearchJpaEntity extends BaseJpaEntity { @Column(nullable = false) private RecentSearchType type; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id") + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "user_id", nullable = false) private UserJpaEntity userJpaEntity; } \ No newline at end of file diff --git a/src/main/java/konkuk/thip/recentSearch/application/service/RecentSearchDeleteService.java b/src/main/java/konkuk/thip/recentSearch/application/service/RecentSearchDeleteService.java index f703134c3..ed5e62136 100644 --- a/src/main/java/konkuk/thip/recentSearch/application/service/RecentSearchDeleteService.java +++ b/src/main/java/konkuk/thip/recentSearch/application/service/RecentSearchDeleteService.java @@ -15,6 +15,7 @@ public class RecentSearchDeleteService implements RecentSearchDeleteUseCase { private final RecentSearchCommandPort recentSearchCommandPort; + @Override @Transactional public Void deleteRecentSearch(Long recentSearchId, Long userId) { RecentSearch recentSearch = recentSearchCommandPort.getByIdOrThrow(recentSearchId); diff --git a/src/main/java/konkuk/thip/recentSearch/application/service/RecentSearchGetService.java b/src/main/java/konkuk/thip/recentSearch/application/service/RecentSearchGetService.java index 33e4b8962..11e013211 100644 --- a/src/main/java/konkuk/thip/recentSearch/application/service/RecentSearchGetService.java +++ b/src/main/java/konkuk/thip/recentSearch/application/service/RecentSearchGetService.java @@ -21,6 +21,7 @@ public class RecentSearchGetService implements RecentSearchGetUseCase { private static final int MAX_RECENT_SEARCHES = 5; + @Override @Transactional(readOnly = true) public RecentSearchGetResponse getRecentSearches(String typeParam, Long userId) { RecentSearchType type = RecentSearchType.from(typeParam); diff --git a/src/main/java/konkuk/thip/recentSearch/application/service/manager/RecentSearchCreateManager.java b/src/main/java/konkuk/thip/recentSearch/application/service/manager/RecentSearchCreateManager.java index 3c41bf10b..4de7be665 100644 --- a/src/main/java/konkuk/thip/recentSearch/application/service/manager/RecentSearchCreateManager.java +++ b/src/main/java/konkuk/thip/recentSearch/application/service/manager/RecentSearchCreateManager.java @@ -1,6 +1,6 @@ package konkuk.thip.recentSearch.application.service.manager; -import konkuk.thip.common.annotation.HelperService; +import konkuk.thip.common.annotation.application.HelperService; import konkuk.thip.recentSearch.adapter.out.jpa.RecentSearchType; import konkuk.thip.recentSearch.application.port.out.RecentSearchCommandPort; import konkuk.thip.recentSearch.application.port.out.RecentSearchQueryPort; diff --git a/src/main/java/konkuk/thip/room/adapter/out/jpa/RoomJpaEntity.java b/src/main/java/konkuk/thip/room/adapter/out/jpa/RoomJpaEntity.java index c3b92ac6c..0d0666352 100644 --- a/src/main/java/konkuk/thip/room/adapter/out/jpa/RoomJpaEntity.java +++ b/src/main/java/konkuk/thip/room/adapter/out/jpa/RoomJpaEntity.java @@ -55,7 +55,7 @@ public class RoomJpaEntity extends BaseJpaEntity { @Enumerated(EnumType.STRING) private RoomStatus roomStatus = RoomStatus.RECRUITING; - @ManyToOne(fetch = FetchType.LAZY) + @ManyToOne(fetch = FetchType.LAZY, optional = false) @JoinColumn(name = "book_id", nullable = false) private BookJpaEntity bookJpaEntity; diff --git a/src/main/java/konkuk/thip/room/adapter/out/jpa/RoomParticipantJpaEntity.java b/src/main/java/konkuk/thip/room/adapter/out/jpa/RoomParticipantJpaEntity.java index 90637fc8b..3c75571c7 100644 --- a/src/main/java/konkuk/thip/room/adapter/out/jpa/RoomParticipantJpaEntity.java +++ b/src/main/java/konkuk/thip/room/adapter/out/jpa/RoomParticipantJpaEntity.java @@ -31,15 +31,15 @@ public class RoomParticipantJpaEntity extends BaseJpaEntity { private double userPercentage = 0.0; @Enumerated(EnumType.STRING) - @Column(name = "room_participant_role",nullable = false) + @Column(name = "room_participant_role", nullable = false) private RoomParticipantRole roomParticipantRole; - @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 = "room_id") + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "room_id", nullable = false) private RoomJpaEntity roomJpaEntity; @VisibleForTesting diff --git a/src/main/java/konkuk/thip/room/adapter/out/persistence/RoomCommandPersistenceAdapter.java b/src/main/java/konkuk/thip/room/adapter/out/persistence/RoomCommandPersistenceAdapter.java index a9c946643..3a93b11f9 100644 --- a/src/main/java/konkuk/thip/room/adapter/out/persistence/RoomCommandPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/room/adapter/out/persistence/RoomCommandPersistenceAdapter.java @@ -26,7 +26,7 @@ public class RoomCommandPersistenceAdapter implements RoomCommandPort { @Override public Room getByIdOrThrow(Long id) { - RoomJpaEntity roomJpaEntity = roomJpaRepository.findById(id).orElseThrow( + RoomJpaEntity roomJpaEntity = roomJpaRepository.findByRoomId(id).orElseThrow( () -> new EntityNotFoundException(ROOM_NOT_FOUND) ); return roomMapper.toDomainEntity(roomJpaEntity); @@ -34,7 +34,7 @@ public Room getByIdOrThrow(Long id) { @Override public Optional findById(Long id) { - return roomJpaRepository.findById(id) + return roomJpaRepository.findByRoomId(id) .map(roomMapper::toDomainEntity); } @@ -50,7 +50,7 @@ public Long save(Room room) { @Override public void update(Room room) { - RoomJpaEntity roomJpaEntity = roomJpaRepository.findById(room.getId()).orElseThrow( + RoomJpaEntity roomJpaEntity = roomJpaRepository.findByRoomId(room.getId()).orElseThrow( () -> new EntityNotFoundException(ROOM_NOT_FOUND) ); diff --git a/src/main/java/konkuk/thip/room/adapter/out/persistence/RoomParticipantCommandPersistenceAdapter.java b/src/main/java/konkuk/thip/room/adapter/out/persistence/RoomParticipantCommandPersistenceAdapter.java index 318fc9bf5..620cbbaec 100644 --- a/src/main/java/konkuk/thip/room/adapter/out/persistence/RoomParticipantCommandPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/room/adapter/out/persistence/RoomParticipantCommandPersistenceAdapter.java @@ -39,10 +39,10 @@ public List findAllByRoomId(Long roomId) { @Override public void save(RoomParticipant roomParticipant) { - UserJpaEntity userJpaEntity = userJpaRepository.findById(roomParticipant.getUserId()).orElseThrow( + UserJpaEntity userJpaEntity = userJpaRepository.findByUserId(roomParticipant.getUserId()).orElseThrow( () -> new EntityNotFoundException(USER_NOT_FOUND)); - RoomJpaEntity roomJpaEntity = roomJpaRepository.findById(roomParticipant.getRoomId()).orElseThrow( + RoomJpaEntity roomJpaEntity = roomJpaRepository.findByRoomId(roomParticipant.getRoomId()).orElseThrow( () -> new EntityNotFoundException(ROOM_NOT_FOUND) ); @@ -62,7 +62,7 @@ public void deleteByUserIdAndRoomId(Long userId, Long roomId) { @Override public void update(RoomParticipant roomParticipant) { - RoomParticipantJpaEntity roomParticipantJpaEntity = roomParticipantJpaRepository.findById(roomParticipant.getId()).orElseThrow( + RoomParticipantJpaEntity roomParticipantJpaEntity = roomParticipantJpaRepository.findByRoomParticipantId(roomParticipant.getId()).orElseThrow( () -> new EntityNotFoundException(ErrorCode.ROOM_PARTICIPANT_NOT_FOUND) ); diff --git a/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomJpaRepository.java b/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomJpaRepository.java index 3455dcbe4..cf88790eb 100644 --- a/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomJpaRepository.java +++ b/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomJpaRepository.java @@ -6,13 +6,18 @@ import org.springframework.data.repository.query.Param; import java.time.LocalDate; +import java.util.Optional; public interface RoomJpaRepository extends JpaRepository, RoomQueryRepository { + /** + * 소프트 딜리트 적용 대상 entity 단건 조회 메서드 + */ + Optional findByRoomId(Long roomId); + @Query("SELECT COUNT(r) FROM RoomJpaEntity r " + "WHERE r.bookJpaEntity.isbn = :isbn " + - "AND r.startDate > :currentDate " + - "AND r.status = 'ACTIVE'") + "AND r.startDate > :currentDate") int countActiveRoomsByBookIdAndStartDateAfter(@Param("isbn") String isbn, @Param("currentDate") LocalDate currentDate); } diff --git a/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepositoryImpl.java b/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepositoryImpl.java index 834e9d25c..4119a2f70 100644 --- a/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepositoryImpl.java +++ b/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepositoryImpl.java @@ -25,8 +25,6 @@ import java.time.LocalDate; import java.util.List; -import static konkuk.thip.common.entity.StatusType.ACTIVE; - @Repository @RequiredArgsConstructor public class RoomQueryRepositoryImpl implements RoomQueryRepository { @@ -39,8 +37,7 @@ public class RoomQueryRepositoryImpl implements RoomQueryRepository { /** 모집중 + ACTIVE 공통 where */ private BooleanBuilder recruitingActiveWhere(LocalDate today) { BooleanBuilder where = new BooleanBuilder(); - where.and(room.startDate.after(today)) - .and(room.status.eq(ACTIVE)); + where.and(room.startDate.after(today)); return where; } @@ -210,8 +207,6 @@ public List findHomeJoinedRoomsByUserPercentage( // 활동 기간 중인 방만: startDate ≤ today ≤ endDate BooleanBuilder where = new BooleanBuilder(); where.and(participant.userJpaEntity.userId.eq(userId)); - where.and(participant.status.eq(ACTIVE)); - where.and(room.status.eq(ACTIVE)); where.and(room.startDate.loe(LocalDate.now())); where.and(room.endDate.goe(LocalDate.now())); @@ -257,9 +252,7 @@ public List findRecruitingRoomsUserParticipated( ) { LocalDate today = LocalDate.now(); BooleanExpression base = participant.userJpaEntity.userId.eq(userId) - .and(participant.status.eq(ACTIVE)) - .and(room.startDate.after(today)) - .and(room.status.eq(ACTIVE)); // 유저가 참여한 방 && 모집중인 방 + .and(room.startDate.after(today)); // 유저가 참여한 방 && 모집중인 방 DateExpression cursorExpr = room.startDate; // 커서 비교는 startDate(= 모집 마감일 - 1일) OrderSpecifier[] orders = new OrderSpecifier[]{ cursorExpr.asc(), room.roomId.asc() @@ -275,10 +268,8 @@ public List findPlayingRoomsUserParticipated( ) { LocalDate today = LocalDate.now(); BooleanExpression base = participant.userJpaEntity.userId.eq(userId) - .and(participant.status.eq(ACTIVE)) .and(room.startDate.loe(today)) - .and(room.endDate.goe(today)) - .and(room.status.eq(ACTIVE)); // 유저가 참여한 방 && 현재 진행중인 방 + .and(room.endDate.goe(today)); // 유저가 참여한 방 && 현재 진행중인 방 DateExpression cursorExpr = room.endDate; // 커서 비교는 endDate(= 진행 마감일) OrderSpecifier[] orders = new OrderSpecifier[]{ cursorExpr.asc(), room.roomId.asc() @@ -296,9 +287,7 @@ public List findPlayingAndRecruitingRoomsUserParticipated( BooleanExpression playing = room.startDate.loe(today).and(room.endDate.goe(today)); BooleanExpression recruiting = room.startDate.after(today); BooleanExpression base = participant.userJpaEntity.userId.eq(userId) - .and(participant.status.eq(ACTIVE)) - .and(playing.or(recruiting)) - .and(room.status.eq(ACTIVE)); // 유저가 참여한 방 && 현재 진행중인 방 + 모집중인 방 + .and(playing.or(recruiting)); // 유저가 참여한 방 && 현재 진행중인 방 + 모집중인 방 // 진행중: cursor=endDate, 모집중: cursor=startDate DateExpression cursorExpr = new CaseBuilder() @@ -326,9 +315,7 @@ public List findExpiredRoomsUserParticipated( ) { LocalDate today = LocalDate.now(); BooleanExpression base = participant.userJpaEntity.userId.eq(userId) - .and(participant.status.eq(ACTIVE)) - .and(room.endDate.before(today)) - .and(room.status.eq(ACTIVE)); // 유저가 참여한 방 && 만료된 방 + .and(room.endDate.before(today)); // 유저가 참여한 방 && 만료된 방 DateExpression cursorExpr = room.endDate; OrderSpecifier[] orders = new OrderSpecifier[]{ @@ -380,8 +367,8 @@ public List findRoomsByCategoryOrderByMemberCount(Category categor public List findRoomsByIsbnOrderByStartDateAsc(String isbn, LocalDate dateCursor, Long roomIdCursor, int pageSize) { DateExpression cursorExpr = room.startDate; // 커서 비교는 startDate(= 모집 마감일 - 1일) BooleanExpression baseCondition = room.bookJpaEntity.isbn.eq(isbn) - .and(room.startDate.after(LocalDate.now())) // 모집 마감 시각 > 현재 시각 - .and(room.status.eq(ACTIVE)); + .and(room.startDate.after(LocalDate.now())); // 모집 마감 시각 > 현재 시각 + if (dateCursor != null && roomIdCursor != null) { // 첫 페이지가 아닌 경우 baseCondition = baseCondition.and(cursorExpr.gt(dateCursor) @@ -410,8 +397,7 @@ private BooleanExpression findDeadlinePopularRoomCondition(Category category, Lo return room.category.eq(category) .and(room.startDate.after(LocalDate.now())) // 모집 마감 시각 > 현재 시각 .and(room.isPublic.isTrue()) // 공개 방만 조회 - .and(userJoinedRoom(userId).not()) // 유저가 참여하지 않은 방만 조회 - .and(room.status.eq(ACTIVE)); + .and(userJoinedRoom(userId).not()); // 유저가 참여하지 않은 방만 조회 } /** diff --git a/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/roomparticipant/RoomParticipantJpaRepository.java b/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/roomparticipant/RoomParticipantJpaRepository.java index 04ab7f8f5..22284a4d6 100644 --- a/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/roomparticipant/RoomParticipantJpaRepository.java +++ b/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/roomparticipant/RoomParticipantJpaRepository.java @@ -10,22 +10,24 @@ public interface RoomParticipantJpaRepository extends JpaRepository, RoomParticipantQueryRepository{ + /** + * 소프트 딜리트 적용 대상 entity 단건 조회 메서드 + */ + Optional findByRoomParticipantId(Long roomParticipantId); + @Query("SELECT rp FROM RoomParticipantJpaEntity rp " + "WHERE rp.userJpaEntity.userId = :userId " + - "AND rp.roomJpaEntity.roomId = :roomId " + - "AND rp.status = 'ACTIVE'") + "AND rp.roomJpaEntity.roomId = :roomId") Optional findByUserIdAndRoomId(@Param("userId") Long userId, @Param("roomId") Long roomId); @Query("SELECT rp FROM RoomParticipantJpaEntity rp " + - "WHERE rp.roomJpaEntity.roomId = :roomId " + - "AND rp.status = 'ACTIVE'") + "WHERE rp.roomJpaEntity.roomId = :roomId") List findAllByRoomId(@Param("roomId") Long roomId); @Query("SELECT CASE WHEN COUNT(rp) > 0 THEN true ELSE false END " + "FROM RoomParticipantJpaEntity rp " + "WHERE rp.userJpaEntity.userId = :userId " + - "AND rp.roomJpaEntity.roomId = :roomId " + - "AND rp.status = 'ACTIVE'") + "AND rp.roomJpaEntity.roomId = :roomId") boolean existsByUserIdAndRoomId(@Param("userId") Long userId, @Param("roomId") Long roomId); } diff --git a/src/main/java/konkuk/thip/room/application/service/validator/RoomParticipantValidator.java b/src/main/java/konkuk/thip/room/application/service/validator/RoomParticipantValidator.java index ec72c017c..163060674 100644 --- a/src/main/java/konkuk/thip/room/application/service/validator/RoomParticipantValidator.java +++ b/src/main/java/konkuk/thip/room/application/service/validator/RoomParticipantValidator.java @@ -1,6 +1,6 @@ package konkuk.thip.room.application.service.validator; -import konkuk.thip.common.annotation.HelperService; +import konkuk.thip.common.annotation.application.HelperService; import konkuk.thip.common.exception.InvalidStateException; import konkuk.thip.room.application.port.out.RoomParticipantQueryPort; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/konkuk/thip/roompost/adapter/out/jpa/AttendanceCheckJpaEntity.java b/src/main/java/konkuk/thip/roompost/adapter/out/jpa/AttendanceCheckJpaEntity.java index e51d78254..0421920a3 100644 --- a/src/main/java/konkuk/thip/roompost/adapter/out/jpa/AttendanceCheckJpaEntity.java +++ b/src/main/java/konkuk/thip/roompost/adapter/out/jpa/AttendanceCheckJpaEntity.java @@ -24,12 +24,12 @@ public class AttendanceCheckJpaEntity extends BaseJpaEntity { @Column(name = "today_comment",length = 100, nullable = false) private String todayComment; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "room_id") + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "room_id", nullable = false) private RoomJpaEntity roomJpaEntity; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id") + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "user_id", nullable = false) private UserJpaEntity userJpaEntity; } diff --git a/src/main/java/konkuk/thip/roompost/adapter/out/jpa/RecordJpaEntity.java b/src/main/java/konkuk/thip/roompost/adapter/out/jpa/RecordJpaEntity.java index e9d1de6af..319acf7d6 100644 --- a/src/main/java/konkuk/thip/roompost/adapter/out/jpa/RecordJpaEntity.java +++ b/src/main/java/konkuk/thip/roompost/adapter/out/jpa/RecordJpaEntity.java @@ -12,7 +12,6 @@ import lombok.NoArgsConstructor; @Entity -//@Table(name = "records") @DiscriminatorValue("RECORD") @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @@ -25,7 +24,7 @@ public class RecordJpaEntity extends PostJpaEntity { private Boolean isOverview; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "room_id") + @JoinColumn(name = "room_id") // FEED 로 인해 nullable = true로 설정 private RoomJpaEntity roomJpaEntity; @Builder diff --git a/src/main/java/konkuk/thip/roompost/adapter/out/jpa/VoteItemJpaEntity.java b/src/main/java/konkuk/thip/roompost/adapter/out/jpa/VoteItemJpaEntity.java index 9d5eaf601..5312f5e70 100644 --- a/src/main/java/konkuk/thip/roompost/adapter/out/jpa/VoteItemJpaEntity.java +++ b/src/main/java/konkuk/thip/roompost/adapter/out/jpa/VoteItemJpaEntity.java @@ -25,8 +25,8 @@ public class VoteItemJpaEntity extends BaseJpaEntity { @Column(nullable = false) private int count = 0; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "post_id") + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "post_id", nullable = false) private VoteJpaEntity voteJpaEntity; public VoteItemJpaEntity updateFrom(VoteItem voteItem) { diff --git a/src/main/java/konkuk/thip/roompost/adapter/out/jpa/VoteJpaEntity.java b/src/main/java/konkuk/thip/roompost/adapter/out/jpa/VoteJpaEntity.java index 4bfd4eb77..03a6b77ba 100644 --- a/src/main/java/konkuk/thip/roompost/adapter/out/jpa/VoteJpaEntity.java +++ b/src/main/java/konkuk/thip/roompost/adapter/out/jpa/VoteJpaEntity.java @@ -12,7 +12,6 @@ import lombok.NoArgsConstructor; @Entity -//@Table(name = "votes") @DiscriminatorValue("VOTE") @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @@ -25,7 +24,7 @@ public class VoteJpaEntity extends PostJpaEntity { private boolean isOverview; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "room_id") + @JoinColumn(name = "room_id") // FEED 로 인해 nullable = true로 설정 private RoomJpaEntity roomJpaEntity; @Builder diff --git a/src/main/java/konkuk/thip/roompost/adapter/out/jpa/VoteParticipantJpaEntity.java b/src/main/java/konkuk/thip/roompost/adapter/out/jpa/VoteParticipantJpaEntity.java index bda48ca30..06d71abf8 100644 --- a/src/main/java/konkuk/thip/roompost/adapter/out/jpa/VoteParticipantJpaEntity.java +++ b/src/main/java/konkuk/thip/roompost/adapter/out/jpa/VoteParticipantJpaEntity.java @@ -5,7 +5,6 @@ import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; import lombok.*; - @Entity @Table(name = "vote_participants") @Getter @@ -19,12 +18,12 @@ public class VoteParticipantJpaEntity extends BaseJpaEntity { @Column(name = "vote_participant_id") private Long voteParticipantId; - @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 = "vote_item_id") + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "vote_item_id", nullable = false) private VoteItemJpaEntity voteItemJpaEntity; public VoteParticipantJpaEntity updateVoteItem(VoteItemJpaEntity voteItemJpaEntity) { diff --git a/src/main/java/konkuk/thip/roompost/adapter/out/persistence/AttendanceCheckCommandPersistenceAdapter.java b/src/main/java/konkuk/thip/roompost/adapter/out/persistence/AttendanceCheckCommandPersistenceAdapter.java index 8347a8f5e..fa5ac8c4d 100644 --- a/src/main/java/konkuk/thip/roompost/adapter/out/persistence/AttendanceCheckCommandPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/roompost/adapter/out/persistence/AttendanceCheckCommandPersistenceAdapter.java @@ -1,6 +1,5 @@ package konkuk.thip.roompost.adapter.out.persistence; -import konkuk.thip.common.entity.StatusType; import konkuk.thip.roompost.adapter.out.jpa.AttendanceCheckJpaEntity; import konkuk.thip.roompost.adapter.out.mapper.AttendanceCheckMapper; import konkuk.thip.roompost.adapter.out.persistence.repository.attendancecheck.AttendanceCheckJpaRepository; @@ -29,11 +28,11 @@ public class AttendanceCheckCommandPersistenceAdapter implements AttendanceCheck @Override public Long save(AttendanceCheck attendanceCheck) { - RoomJpaEntity roomJpaEntity = roomJpaRepository.findById(attendanceCheck.getRoomId()).orElseThrow( + RoomJpaEntity roomJpaEntity = roomJpaRepository.findByRoomId(attendanceCheck.getRoomId()).orElseThrow( () -> new EntityNotFoundException(ROOM_NOT_FOUND) ); - UserJpaEntity userJpaEntity = userJpaRepository.findById(attendanceCheck.getCreatorId()).orElseThrow( + UserJpaEntity userJpaEntity = userJpaRepository.findByUserId(attendanceCheck.getCreatorId()).orElseThrow( () -> new EntityNotFoundException(USER_NOT_FOUND) ); @@ -44,13 +43,13 @@ public Long save(AttendanceCheck attendanceCheck) { @Override public Optional findById(Long id) { - return attendanceCheckJpaRepository.findByAttendanceCheckIdAndStatus(id, StatusType.ACTIVE) + return attendanceCheckJpaRepository.findByAttendanceCheckId(id) .map(attendanceCheckMapper::toDomainEntity); } @Override public void delete(AttendanceCheck attendanceCheck) { - AttendanceCheckJpaEntity attendanceCheckJpaEntity = attendanceCheckJpaRepository.findByAttendanceCheckIdAndStatus(attendanceCheck.getId(), StatusType.ACTIVE).orElseThrow( + AttendanceCheckJpaEntity attendanceCheckJpaEntity = attendanceCheckJpaRepository.findByAttendanceCheckId(attendanceCheck.getId()).orElseThrow( () -> new EntityNotFoundException(ATTENDANCE_CHECK_NOT_FOUND) ); diff --git a/src/main/java/konkuk/thip/roompost/adapter/out/persistence/AttendanceCheckQueryPersistenceAdapter.java b/src/main/java/konkuk/thip/roompost/adapter/out/persistence/AttendanceCheckQueryPersistenceAdapter.java index bd9eb4418..1b1c07e60 100644 --- a/src/main/java/konkuk/thip/roompost/adapter/out/persistence/AttendanceCheckQueryPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/roompost/adapter/out/persistence/AttendanceCheckQueryPersistenceAdapter.java @@ -26,7 +26,7 @@ public int countAttendanceChecksOnTodayByUser(Long userId, Long roomId) { LocalDateTime startOfDay = LocalDateTime.now().toLocalDate().atStartOfDay(); LocalDateTime endOfDay = startOfDay.plusDays(1); - return attendanceCheckJpaRepository.countByUserIdAndRoomIdAndCreatedAtBetween(userId, roomId, startOfDay, endOfDay, ACTIVE); + return attendanceCheckJpaRepository.countByUserIdAndRoomIdAndCreatedAtBetween(userId, roomId, startOfDay, endOfDay); } @Override diff --git a/src/main/java/konkuk/thip/roompost/adapter/out/persistence/RecordCommandPersistenceAdapter.java b/src/main/java/konkuk/thip/roompost/adapter/out/persistence/RecordCommandPersistenceAdapter.java index 0d0c20dd5..b327f9c1a 100644 --- a/src/main/java/konkuk/thip/roompost/adapter/out/persistence/RecordCommandPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/roompost/adapter/out/persistence/RecordCommandPersistenceAdapter.java @@ -15,7 +15,6 @@ import java.util.Optional; -import static konkuk.thip.common.entity.StatusType.ACTIVE; import static konkuk.thip.common.exception.code.ErrorCode.*; @Repository @@ -29,11 +28,11 @@ public class RecordCommandPersistenceAdapter implements RecordCommandPort { @Override public Long save(Record record) { - UserJpaEntity userJpaEntity = userJpaRepository.findById(record.getCreatorId()).orElseThrow( + UserJpaEntity userJpaEntity = userJpaRepository.findByUserId(record.getCreatorId()).orElseThrow( () -> new EntityNotFoundException(USER_NOT_FOUND) ); - RoomJpaEntity roomJpaEntity = roomJpaRepository.findById(record.getRoomId()).orElseThrow( + RoomJpaEntity roomJpaEntity = roomJpaRepository.findByRoomId(record.getRoomId()).orElseThrow( () -> new EntityNotFoundException(ROOM_NOT_FOUND) ); @@ -44,13 +43,13 @@ public Long save(Record record) { @Override public Optional findById(Long id) { - return recordJpaRepository.findByPostIdAndStatus(id, ACTIVE) + return recordJpaRepository.findByPostId(id) .map(recordMapper::toDomainEntity); } @Override public void delete(Record record) { - RecordJpaEntity recordJpaEntity = recordJpaRepository.findByPostIdAndStatus(record.getId(),ACTIVE).orElseThrow( + RecordJpaEntity recordJpaEntity = recordJpaRepository.findByPostId(record.getId()).orElseThrow( () -> new EntityNotFoundException(RECORD_NOT_FOUND) ); @@ -60,7 +59,7 @@ public void delete(Record record) { @Override public void update(Record record) { - RecordJpaEntity recordJpaEntity = recordJpaRepository.findByPostIdAndStatus(record.getId(),ACTIVE).orElseThrow( + RecordJpaEntity recordJpaEntity = recordJpaRepository.findByPostId(record.getId()).orElseThrow( () -> new EntityNotFoundException(RECORD_NOT_FOUND) ); diff --git a/src/main/java/konkuk/thip/roompost/adapter/out/persistence/VoteCommandPersistenceAdapter.java b/src/main/java/konkuk/thip/roompost/adapter/out/persistence/VoteCommandPersistenceAdapter.java index 3fbfea317..3a753a395 100644 --- a/src/main/java/konkuk/thip/roompost/adapter/out/persistence/VoteCommandPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/roompost/adapter/out/persistence/VoteCommandPersistenceAdapter.java @@ -24,7 +24,6 @@ import java.util.List; import java.util.Optional; -import static konkuk.thip.common.entity.StatusType.ACTIVE; import static konkuk.thip.common.exception.code.ErrorCode.*; @Repository @@ -43,11 +42,11 @@ public class VoteCommandPersistenceAdapter implements VoteCommandPort { @Override public Long saveVote(Vote vote) { - UserJpaEntity userJpaEntity = userJpaRepository.findById(vote.getCreatorId()).orElseThrow( + UserJpaEntity userJpaEntity = userJpaRepository.findByUserId(vote.getCreatorId()).orElseThrow( () -> new EntityNotFoundException(USER_NOT_FOUND) ); - RoomJpaEntity roomJpaEntity = roomJpaRepository.findById(vote.getRoomId()).orElseThrow( + RoomJpaEntity roomJpaEntity = roomJpaRepository.findByRoomId(vote.getRoomId()).orElseThrow( () -> new EntityNotFoundException(ROOM_NOT_FOUND) ); @@ -59,7 +58,7 @@ public void saveAllVoteItems(List voteItems) { if (voteItems.isEmpty()) return; Long voteId = voteItems.get(0).getVoteId(); - VoteJpaEntity voteJpaEntity = voteJpaRepository.findByPostIdAndStatus(voteId,ACTIVE).orElseThrow( + VoteJpaEntity voteJpaEntity = voteJpaRepository.findByPostId(voteId).orElseThrow( () -> new EntityNotFoundException(VOTE_NOT_FOUND) ); @@ -72,7 +71,7 @@ public void saveAllVoteItems(List voteItems) { @Override public Optional findById(Long id) { - return voteJpaRepository.findByPostIdAndStatus(id,ACTIVE) + return voteJpaRepository.findByPostId(id) .map(voteMapper::toDomainEntity); } @@ -109,7 +108,7 @@ public void updateVoteParticipant(VoteParticipant voteParticipant) { @Override public void saveVoteParticipant(VoteParticipant voteParticipant) { - UserJpaEntity userJpaEntity = userJpaRepository.findById(voteParticipant.getUserId()).orElseThrow( + UserJpaEntity userJpaEntity = userJpaRepository.findByUserId(voteParticipant.getUserId()).orElseThrow( () -> new EntityNotFoundException(USER_NOT_FOUND) ); @@ -138,7 +137,7 @@ public void updateVoteItem(VoteItem voteItem) { @Override public void delete(Vote vote) { - VoteJpaEntity voteJpaEntity = voteJpaRepository.findByPostIdAndStatus(vote.getId(),ACTIVE).orElseThrow( + VoteJpaEntity voteJpaEntity = voteJpaRepository.findByPostId(vote.getId()).orElseThrow( () -> new EntityNotFoundException(VOTE_NOT_FOUND) ); @@ -152,7 +151,7 @@ public void delete(Vote vote) { @Override public void updateVote(Vote vote) { - VoteJpaEntity voteJpaEntity = voteJpaRepository.findByPostIdAndStatus(vote.getId(),ACTIVE).orElseThrow( + VoteJpaEntity voteJpaEntity = voteJpaRepository.findByPostId(vote.getId()).orElseThrow( () -> new EntityNotFoundException(VOTE_NOT_FOUND) ); diff --git a/src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/attendancecheck/AttendanceCheckJpaRepository.java b/src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/attendancecheck/AttendanceCheckJpaRepository.java index 1c5ed8286..4d3766ac7 100644 --- a/src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/attendancecheck/AttendanceCheckJpaRepository.java +++ b/src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/attendancecheck/AttendanceCheckJpaRepository.java @@ -1,6 +1,5 @@ package konkuk.thip.roompost.adapter.out.persistence.repository.attendancecheck; -import konkuk.thip.common.entity.StatusType; import konkuk.thip.roompost.adapter.out.jpa.AttendanceCheckJpaEntity; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; @@ -11,14 +10,16 @@ public interface AttendanceCheckJpaRepository extends JpaRepository, AttendanceCheckQueryRepository { + /** + * 소프트 딜리트 적용 대상 entity 단건 조회 메서드 + */ + Optional findByAttendanceCheckId(Long attendanceCheckId); + // TODO : count 값을 매번 쿼리를 통해 계산하는게 아니라 DB에 저장 or redis 캐시에 저장하는 방법도 좋을 듯 @Query("SELECT COUNT(a) FROM AttendanceCheckJpaEntity a " + "WHERE a.userJpaEntity.userId = :userId " + "AND a.roomJpaEntity.roomId = :roomId " + - "AND a.status = :status " + "AND a.createdAt >= :startOfDay " + "AND a.createdAt < :endOfDay") - int countByUserIdAndRoomIdAndCreatedAtBetween(@Param("userId") Long userId, @Param("roomId") Long roomId, @Param("startOfDay") LocalDateTime startOfDay, @Param("endOfDay") LocalDateTime endOfDay, @Param("status")StatusType status); - - Optional findByAttendanceCheckIdAndStatus(Long attendanceCheckId, StatusType status); + int countByUserIdAndRoomIdAndCreatedAtBetween(@Param("userId") Long userId, @Param("roomId") Long roomId, @Param("startOfDay") LocalDateTime startOfDay, @Param("endOfDay") LocalDateTime endOfDay); } diff --git a/src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/record/RecordJpaRepository.java b/src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/record/RecordJpaRepository.java index 6e197c50d..7f684dca8 100644 --- a/src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/record/RecordJpaRepository.java +++ b/src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/record/RecordJpaRepository.java @@ -1,11 +1,14 @@ package konkuk.thip.roompost.adapter.out.persistence.repository.record; -import konkuk.thip.common.entity.StatusType; import konkuk.thip.roompost.adapter.out.jpa.RecordJpaEntity; import org.springframework.data.jpa.repository.JpaRepository; import java.util.Optional; public interface RecordJpaRepository extends JpaRepository, RecordQueryRepository { - Optional findByPostIdAndStatus(Long postId, StatusType status); + + /** + * 소프트 딜리트 적용 대상 entity 단건 조회 메서드 + */ + Optional findByPostId(Long postId); } diff --git a/src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/record/RecordQueryRepositoryImpl.java b/src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/record/RecordQueryRepositoryImpl.java index b7cba1500..2376fc136 100644 --- a/src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/record/RecordQueryRepositoryImpl.java +++ b/src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/record/RecordQueryRepositoryImpl.java @@ -5,7 +5,6 @@ import com.querydsl.core.types.dsl.CaseBuilder; import com.querydsl.core.types.dsl.NumberExpression; import com.querydsl.jpa.impl.JPAQueryFactory; -import konkuk.thip.common.entity.StatusType; import konkuk.thip.common.util.Cursor; import konkuk.thip.post.adapter.out.jpa.QPostJpaEntity; import konkuk.thip.roompost.adapter.out.jpa.QRecordJpaEntity; @@ -64,8 +63,8 @@ private BooleanBuilder buildMyRecordCondition(Long roomId, Long userId) { .and(treat(post, QRecordJpaEntity.class).roomJpaEntity.roomId.eq(roomId)); where.and(voteCondition.or(recordCondition)) - .and(post.userJpaEntity.userId.eq(userId)) - .and(post.status.eq(StatusType.ACTIVE)); + .and(post.userJpaEntity.userId.eq(userId)); + return where; } @@ -114,8 +113,8 @@ private BooleanBuilder buildRecordVoteCondition(Long roomId, Integer pageStart, .and(treat(post, QRecordJpaEntity.class).page.between(pageStart, pageEnd)); } - where.and(voteCondition.or(recordCondition)) - .and(post.status.eq(StatusType.ACTIVE)); + where.and(voteCondition.or(recordCondition)); + return where; } diff --git a/src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/vote/VoteJpaRepository.java b/src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/vote/VoteJpaRepository.java index 8591fde59..1ec6e367e 100644 --- a/src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/vote/VoteJpaRepository.java +++ b/src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/vote/VoteJpaRepository.java @@ -1,11 +1,14 @@ package konkuk.thip.roompost.adapter.out.persistence.repository.vote; -import konkuk.thip.common.entity.StatusType; import konkuk.thip.roompost.adapter.out.jpa.VoteJpaEntity; import org.springframework.data.jpa.repository.JpaRepository; import java.util.Optional; public interface VoteJpaRepository extends JpaRepository, VoteQueryRepository { - Optional findByPostIdAndStatus(Long postId, StatusType status); + + /** + * 소프트 딜리트 적용 대상 entity 단건 조회 메서드 + */ + Optional findByPostId(Long postId); } diff --git a/src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/vote/VoteQueryRepositoryImpl.java b/src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/vote/VoteQueryRepositoryImpl.java index 7a25d9522..adbbeca2b 100644 --- a/src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/vote/VoteQueryRepositoryImpl.java +++ b/src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/vote/VoteQueryRepositoryImpl.java @@ -33,7 +33,7 @@ public List findVotesByRoom(Long roomId, String type, Integer pag return jpaQueryFactory .select(vote) .from(vote) - .leftJoin(vote.userJpaEntity, user).fetchJoin() + .join(vote.userJpaEntity, user).fetchJoin() // vote <-> user (not null) .where( vote.roomJpaEntity.roomId.eq(roomId), filterByType(type, vote, userId), @@ -59,7 +59,7 @@ public List findTopParticipationVotes List topVotes = jpaQueryFactory .select(vote) .from(vote) - .leftJoin(voteItem).on(voteItem.voteJpaEntity.eq(vote)) + .join(voteItem).on(voteItem.voteJpaEntity.eq(vote)) // vote item이 없는 경우 포함 X .where(vote.roomJpaEntity.roomId.eq(roomId)) .groupBy(vote) .orderBy(voteItem.count.sum().desc()) // 해당 투표에 참여한 총 참여자 수 기준 내림차순 정렬 diff --git a/src/main/java/konkuk/thip/roompost/application/service/manager/RoomProgressManager.java b/src/main/java/konkuk/thip/roompost/application/service/manager/RoomProgressManager.java index 44d0be4b4..baeffa549 100644 --- a/src/main/java/konkuk/thip/roompost/application/service/manager/RoomProgressManager.java +++ b/src/main/java/konkuk/thip/roompost/application/service/manager/RoomProgressManager.java @@ -1,7 +1,7 @@ package konkuk.thip.roompost.application.service.manager; import konkuk.thip.book.domain.Book; -import konkuk.thip.common.annotation.HelperService; +import konkuk.thip.common.annotation.application.HelperService; import konkuk.thip.room.application.port.out.RoomCommandPort; import konkuk.thip.room.application.port.out.RoomParticipantCommandPort; import konkuk.thip.room.domain.Room; diff --git a/src/main/java/konkuk/thip/roompost/application/service/validator/RoomPostAccessValidator.java b/src/main/java/konkuk/thip/roompost/application/service/validator/RoomPostAccessValidator.java index 586f9fab9..705b8e8c8 100644 --- a/src/main/java/konkuk/thip/roompost/application/service/validator/RoomPostAccessValidator.java +++ b/src/main/java/konkuk/thip/roompost/application/service/validator/RoomPostAccessValidator.java @@ -1,6 +1,6 @@ package konkuk.thip.roompost.application.service.validator; -import konkuk.thip.common.annotation.HelperService; +import konkuk.thip.common.annotation.application.HelperService; import konkuk.thip.common.exception.BusinessException; import konkuk.thip.common.exception.code.ErrorCode; diff --git a/src/main/java/konkuk/thip/user/adapter/out/jpa/FollowingJpaEntity.java b/src/main/java/konkuk/thip/user/adapter/out/jpa/FollowingJpaEntity.java index 0213fc559..d7a515071 100644 --- a/src/main/java/konkuk/thip/user/adapter/out/jpa/FollowingJpaEntity.java +++ b/src/main/java/konkuk/thip/user/adapter/out/jpa/FollowingJpaEntity.java @@ -17,11 +17,11 @@ public class FollowingJpaEntity extends BaseJpaEntity { @Column(name = "following_id") private Long followingId; - @ManyToOne(fetch = FetchType.LAZY) + @ManyToOne(fetch = FetchType.LAZY, optional = false) @JoinColumn(name = "user_id", nullable = false) private UserJpaEntity userJpaEntity; // 팔로잉 하는 유저 - @ManyToOne(fetch = FetchType.LAZY) + @ManyToOne(fetch = FetchType.LAZY, optional = false) @JoinColumn(name = "following_user_id", nullable = false) private UserJpaEntity followingUserJpaEntity; // 팔로우 당하는 유저 } diff --git a/src/main/java/konkuk/thip/user/adapter/out/jpa/UserJpaEntity.java b/src/main/java/konkuk/thip/user/adapter/out/jpa/UserJpaEntity.java index 1a2c51c77..1acef21e1 100644 --- a/src/main/java/konkuk/thip/user/adapter/out/jpa/UserJpaEntity.java +++ b/src/main/java/konkuk/thip/user/adapter/out/jpa/UserJpaEntity.java @@ -6,6 +6,7 @@ import konkuk.thip.user.domain.value.Alias; import konkuk.thip.user.domain.User; import lombok.*; +import org.hibernate.annotations.SQLDelete; import java.time.LocalDate; @@ -15,6 +16,7 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor @Builder +@SQLDelete(sql = "UPDATE users SET status = 'INACTIVE' WHERE user_id = ?") public class UserJpaEntity extends BaseJpaEntity { @Id diff --git a/src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingCommandPersistenceAdapter.java b/src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingCommandPersistenceAdapter.java index 762af4f1f..2d7ce7cee 100644 --- a/src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingCommandPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingCommandPersistenceAdapter.java @@ -36,7 +36,7 @@ public Optional findByUserIdAndTargetUserId(Long userId, Long targetU @Override public void save(Following following, User targetUser) { // insert용 - UserJpaEntity userJpaEntity = userJpaRepository.findById(following.getUserId()).orElseThrow( + UserJpaEntity userJpaEntity = userJpaRepository.findByUserId(following.getUserId()).orElseThrow( () -> new EntityNotFoundException(USER_NOT_FOUND)); UserJpaEntity targetUserJpaEntity = updateUserFollowerCount(targetUser); @@ -55,7 +55,7 @@ public void deleteFollowing(Following following, User targetUser) { } private UserJpaEntity updateUserFollowerCount(User targetUser) { - UserJpaEntity userJpaEntity = userJpaRepository.findById(targetUser.getId()).orElseThrow( + UserJpaEntity userJpaEntity = userJpaRepository.findByUserId(targetUser.getId()).orElseThrow( () -> new EntityNotFoundException(USER_NOT_FOUND) ); diff --git a/src/main/java/konkuk/thip/user/adapter/out/persistence/UserCommandPersistenceAdapter.java b/src/main/java/konkuk/thip/user/adapter/out/persistence/UserCommandPersistenceAdapter.java index ee76028d1..d141c6808 100644 --- a/src/main/java/konkuk/thip/user/adapter/out/persistence/UserCommandPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/user/adapter/out/persistence/UserCommandPersistenceAdapter.java @@ -14,7 +14,6 @@ import java.util.function.Function; import java.util.stream.Collectors; -import static konkuk.thip.common.exception.code.ErrorCode.ALIAS_NOT_FOUND; import static konkuk.thip.common.exception.code.ErrorCode.USER_NOT_FOUND; @Repository @@ -33,7 +32,7 @@ public Long save(User user) { @Override public User findById(Long userId) { - UserJpaEntity userJpaEntity = userJpaRepository.findById(userId).orElseThrow( + UserJpaEntity userJpaEntity = userJpaRepository.findByUserId(userId).orElseThrow( () -> new EntityNotFoundException(USER_NOT_FOUND)); return userMapper.toDomainEntity(userJpaEntity); @@ -41,7 +40,7 @@ public User findById(Long userId) { @Override public Map findByIds(List userIds) { - List entities = userJpaRepository.findAllById(userIds); + List entities = userJpaRepository.findAllById(userIds); // 내부 구현 메서드가 jpql 기반이므로 필터 적용 대상임을 확인함 return entities.stream() .map(userMapper::toDomainEntity) .collect(Collectors.toMap(User::getId, Function.identity())); @@ -49,7 +48,7 @@ public Map findByIds(List userIds) { @Override public void update(User user) { - UserJpaEntity userJpaEntity = userJpaRepository.findById(user.getId()).orElseThrow( + UserJpaEntity userJpaEntity = userJpaRepository.findByUserId(user.getId()).orElseThrow( () -> new EntityNotFoundException(USER_NOT_FOUND) ); diff --git a/src/main/java/konkuk/thip/user/adapter/out/persistence/repository/UserJpaRepository.java b/src/main/java/konkuk/thip/user/adapter/out/persistence/repository/UserJpaRepository.java index f8036c18c..214ea6009 100644 --- a/src/main/java/konkuk/thip/user/adapter/out/persistence/repository/UserJpaRepository.java +++ b/src/main/java/konkuk/thip/user/adapter/out/persistence/repository/UserJpaRepository.java @@ -6,9 +6,15 @@ import java.util.Optional; public interface UserJpaRepository extends JpaRepository, UserQueryRepository { + + /** + * 소프트 딜리트 적용 대상 entity 단건 조회 메서드 + */ + Optional findByUserId(Long userId); + Optional findByOauth2Id(String oauth2Id); + boolean existsByNickname(String nickname); - Optional findById(Long userId); boolean existsByNicknameAndUserIdNot(String nickname, Long userId); diff --git a/src/main/java/konkuk/thip/user/adapter/out/persistence/repository/UserQueryRepositoryImpl.java b/src/main/java/konkuk/thip/user/adapter/out/persistence/repository/UserQueryRepositoryImpl.java index d796d52df..f9f44c25a 100644 --- a/src/main/java/konkuk/thip/user/adapter/out/persistence/repository/UserQueryRepositoryImpl.java +++ b/src/main/java/konkuk/thip/user/adapter/out/persistence/repository/UserQueryRepositoryImpl.java @@ -6,7 +6,6 @@ import com.querydsl.core.types.dsl.NumberExpression; import com.querydsl.jpa.impl.JPAQueryFactory; import konkuk.thip.comment.adapter.out.jpa.QCommentJpaEntity; -import konkuk.thip.common.entity.StatusType; import konkuk.thip.post.adapter.out.jpa.QPostJpaEntity; import konkuk.thip.post.adapter.out.jpa.QPostLikeJpaEntity; import konkuk.thip.room.adapter.out.jpa.QRoomJpaEntity; @@ -34,15 +33,15 @@ public class UserQueryRepositoryImpl implements UserQueryRepository { @Override public Set findUserIdsByBookId(Long bookId) { - QRoomParticipantJpaEntity userRoom = QRoomParticipantJpaEntity.roomParticipantJpaEntity; + QRoomParticipantJpaEntity roomParticipant = QRoomParticipantJpaEntity.roomParticipantJpaEntity; QRoomJpaEntity room = QRoomJpaEntity.roomJpaEntity; return new HashSet<>( queryFactory - .select(userRoom.userJpaEntity.userId) + .select(roomParticipant.userJpaEntity.userId) .distinct() - .from(userRoom) - .join(userRoom.roomJpaEntity, room) + .from(roomParticipant) + .join(roomParticipant.roomJpaEntity, room) .where(room.bookJpaEntity.bookId.eq(bookId)) .fetch() ); @@ -70,8 +69,7 @@ public List findUsersByNicknameOrderByAccuracy(String keyword, Lon )) .from(user) .where(user.nickname.like(pattern) - .and(user.userId.ne(userId)) - .and(user.status.eq(StatusType.ACTIVE))) + .and(user.userId.ne(userId))) .orderBy(priority.desc(), user.nickname.asc()) .limit(size) .fetch(); @@ -84,9 +82,8 @@ public List findLikeByUserId(Long userId, LocalDateTime cursor QPostJpaEntity post = QPostJpaEntity.postJpaEntity; BooleanBuilder where = new BooleanBuilder(); - where.and(user.userId.eq(userId)) - .and(post.status.eq(StatusType.ACTIVE)) - .and(postLike.status.eq(StatusType.ACTIVE)); + where.and(user.userId.eq(userId)); + if (cursorLocalDateTime != null) { where.and(postLike.createdAt.lt(cursorLocalDateTime)); } @@ -117,9 +114,8 @@ public List findCommentByUserId(Long userId, LocalDateTime cur QCommentJpaEntity comment = QCommentJpaEntity.commentJpaEntity; BooleanBuilder where = new BooleanBuilder(); - where.and(user.userId.eq(userId)) - .and(post.status.eq(StatusType.ACTIVE)) - .and(comment.status.eq(StatusType.ACTIVE)); + where.and(user.userId.eq(userId)); + if (cursorLocalDateTime != null) { where.and(comment.createdAt.lt(cursorLocalDateTime)); } diff --git a/src/main/java/konkuk/thip/user/adapter/out/persistence/repository/following/FollowingQueryRepositoryImpl.java b/src/main/java/konkuk/thip/user/adapter/out/persistence/repository/following/FollowingQueryRepositoryImpl.java index 28638b0fe..1a2032800 100644 --- a/src/main/java/konkuk/thip/user/adapter/out/persistence/repository/following/FollowingQueryRepositoryImpl.java +++ b/src/main/java/konkuk/thip/user/adapter/out/persistence/repository/following/FollowingQueryRepositoryImpl.java @@ -2,7 +2,6 @@ import com.querydsl.core.BooleanBuilder; import com.querydsl.jpa.impl.JPAQueryFactory; -import konkuk.thip.common.entity.StatusType; import konkuk.thip.user.adapter.out.jpa.FollowingJpaEntity; import konkuk.thip.user.adapter.out.jpa.QFollowingJpaEntity; import konkuk.thip.user.adapter.out.jpa.QUserJpaEntity; @@ -31,8 +30,7 @@ public Optional findByUserAndTargetUser(Long userId, Long ta FollowingJpaEntity followingJpaEntity = jpaQueryFactory .selectFrom(following) .where(following.userJpaEntity.userId.eq(userId) - .and(following.followingUserJpaEntity.userId.eq(targetUserId)) - .and(following.status.eq(StatusType.ACTIVE))) + .and(following.followingUserJpaEntity.userId.eq(targetUserId))) .fetchOne(); return Optional.ofNullable(followingJpaEntity); @@ -63,8 +61,9 @@ private List findFollowDtos(Long userId, LocalDateTime cursor, int QUserJpaEntity user = QUserJpaEntity.userJpaEntity; BooleanBuilder condition = new BooleanBuilder() - .and((isFollowerQuery ? following.followingUserJpaEntity.userId.eq(userId) : following.userJpaEntity.userId.eq(userId))) - .and(following.status.eq(StatusType.ACTIVE)); + .and((isFollowerQuery + ? following.followingUserJpaEntity.userId.eq(userId) // 나를 팔로우한 사람들 + : following.userJpaEntity.userId.eq(userId))); // 내가 팔로우하는 사람들 if (cursor != null) { condition.and(following.createdAt.lt(cursor)); @@ -81,7 +80,7 @@ private List findFollowDtos(Long userId, LocalDateTime cursor, int following.createdAt )) .from(following) - .leftJoin(targetUser, user) + .join(targetUser, user) .where(condition) .orderBy(following.createdAt.desc()) .limit(size + 1) @@ -97,8 +96,7 @@ public List findLatestFollowers(Long userId, int size) { .select(follower) .from(following) .join(following.userJpaEntity, follower) - .where(following.followingUserJpaEntity.userId.eq(userId) - .and(following.status.eq(StatusType.ACTIVE))) + .where(following.followingUserJpaEntity.userId.eq(userId)) .orderBy(following.createdAt.desc()) .limit(size) .fetch(); @@ -118,11 +116,7 @@ public List findAllFollowingUsersOrderByFollowedAtDesc(Long u )) .from(following) .join(following.followingUserJpaEntity, followingTargetUser) - .where( - following.userJpaEntity.userId.eq(userId) - .and(following.userJpaEntity.status.eq(StatusType.ACTIVE)) - .and(followingTargetUser.status.eq(StatusType.ACTIVE)) - ) + .where(following.userJpaEntity.userId.eq(userId)) .orderBy(following.createdAt.desc()) .fetch(); } diff --git a/src/main/java/konkuk/thip/user/application/service/UserIsFollowingService.java b/src/main/java/konkuk/thip/user/application/service/UserIsFollowingService.java index 7a036c95b..5326eb999 100644 --- a/src/main/java/konkuk/thip/user/application/service/UserIsFollowingService.java +++ b/src/main/java/konkuk/thip/user/application/service/UserIsFollowingService.java @@ -4,6 +4,7 @@ import konkuk.thip.user.application.port.out.FollowingCommandPort; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor @@ -12,6 +13,7 @@ public class UserIsFollowingService implements UserIsFollowingUsecase { private final FollowingCommandPort followingCommandPort; @Override + @Transactional(readOnly = true) public boolean isFollowing(Long userId, Long targetUserId) { return followingCommandPort.findByUserIdAndTargetUserId(userId, targetUserId) .isPresent(); diff --git a/src/main/java/konkuk/thip/user/application/service/UserMyPageService.java b/src/main/java/konkuk/thip/user/application/service/UserMyPageService.java index f270b8fe2..1fe1d46c4 100644 --- a/src/main/java/konkuk/thip/user/application/service/UserMyPageService.java +++ b/src/main/java/konkuk/thip/user/application/service/UserMyPageService.java @@ -13,6 +13,7 @@ import konkuk.thip.user.domain.User; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.util.List; @@ -26,6 +27,7 @@ public class UserMyPageService implements UserMyPageUseCase { private final ReactionQueryMapper reactionQueryMapper; @Override + @Transactional(readOnly = true) public UserReactionResponse getUserReaction(Long userId, UserReactionType userReactionType, int size, String cursorStr) { Cursor cursor = Cursor.from(cursorStr, size); @@ -45,6 +47,7 @@ public UserReactionResponse getUserReaction(Long userId, UserReactionType userRe } @Override + @Transactional(readOnly = true) public UserProfileResponse getUserProfile(Long userId) { User user = userCommandPort.findById(userId); diff --git a/src/main/java/konkuk/thip/user/application/service/UserVerifyNicknameService.java b/src/main/java/konkuk/thip/user/application/service/UserVerifyNicknameService.java index 8650171c2..3155f2a93 100644 --- a/src/main/java/konkuk/thip/user/application/service/UserVerifyNicknameService.java +++ b/src/main/java/konkuk/thip/user/application/service/UserVerifyNicknameService.java @@ -1,9 +1,11 @@ package konkuk.thip.user.application.service; +import konkuk.thip.common.annotation.persistence.Unfiltered; import konkuk.thip.user.application.port.in.UserVerifyNicknameUseCase; import konkuk.thip.user.application.port.out.UserQueryPort; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor @@ -12,6 +14,8 @@ public class UserVerifyNicknameService implements UserVerifyNicknameUseCase { private final UserQueryPort userQueryPort; @Override + @Transactional(readOnly = true) + @Unfiltered // soft delete 된 유저의 닉네임을 포함해서 중복 검증 public boolean isNicknameUnique(String nickname) { return !userQueryPort.existsByNickname(nickname); } diff --git a/src/main/java/konkuk/thip/user/application/service/UserViewAliasChoiceService.java b/src/main/java/konkuk/thip/user/application/service/UserViewAliasChoiceService.java index 3fe0fe9d8..24c3456c4 100644 --- a/src/main/java/konkuk/thip/user/application/service/UserViewAliasChoiceService.java +++ b/src/main/java/konkuk/thip/user/application/service/UserViewAliasChoiceService.java @@ -7,6 +7,7 @@ import konkuk.thip.user.domain.value.Alias; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.util.Map; @@ -15,6 +16,7 @@ public class UserViewAliasChoiceService implements UserViewAliasChoiceUseCase { @Override + @Transactional(readOnly = true) public UserViewAliasChoiceResult getAllAliasesAndCategories() { Map aliasToCategory = EnumMappings.getAliasToCategory(); diff --git a/src/test/java/konkuk/thip/book/adapter/in/web/BookDetailSearchApiTest.java b/src/test/java/konkuk/thip/book/adapter/in/web/BookDetailSearchApiTest.java index ba428423b..f3d07d72b 100644 --- a/src/test/java/konkuk/thip/book/adapter/in/web/BookDetailSearchApiTest.java +++ b/src/test/java/konkuk/thip/book/adapter/in/web/BookDetailSearchApiTest.java @@ -31,11 +31,10 @@ import static org.assertj.core.api.Assertions.assertThat; - @SpringBootTest @AutoConfigureMockMvc(addFilters = false) @ActiveProfiles("test") -@DisplayName("[통합] 방 상세보기 api 통합 테스트") +@DisplayName("[통합] 책 상세보기 api 통합 테스트") class BookDetailSearchApiTest { @Autowired @@ -114,12 +113,12 @@ void setup() { @AfterEach void tearDown() { - savedBookJpaRepository.deleteAll(); + savedBookJpaRepository.deleteAllInBatch(); feedJpaRepository.deleteAllInBatch(); roomParticipantJpaRepository.deleteAllInBatch(); - roomJpaRepository.deleteAll(); - bookJpaRepository.deleteAll(); - userJpaRepository.deleteAll(); + roomJpaRepository.deleteAllInBatch(); + bookJpaRepository.deleteAllInBatch(); + userJpaRepository.deleteAllInBatch(); } @Test diff --git a/src/test/java/konkuk/thip/book/adapter/in/web/BookSearchApiTest.java b/src/test/java/konkuk/thip/book/adapter/in/web/BookSearchApiTest.java index 7a8989bc6..d13fd4130 100644 --- a/src/test/java/konkuk/thip/book/adapter/in/web/BookSearchApiTest.java +++ b/src/test/java/konkuk/thip/book/adapter/in/web/BookSearchApiTest.java @@ -72,8 +72,8 @@ void setUp() { @AfterEach void tearDown() { - recentSearchJpaRepository.deleteAll(); - userJpaRepository.deleteAll(); + recentSearchJpaRepository.deleteAllInBatch(); + userJpaRepository.deleteAllInBatch(); } @Test diff --git a/src/test/java/konkuk/thip/comment/adapter/in/web/CommentDeleteApiTest.java b/src/test/java/konkuk/thip/comment/adapter/in/web/CommentDeleteApiTest.java index cfa56328f..b39a96877 100644 --- a/src/test/java/konkuk/thip/comment/adapter/in/web/CommentDeleteApiTest.java +++ b/src/test/java/konkuk/thip/comment/adapter/in/web/CommentDeleteApiTest.java @@ -106,7 +106,8 @@ void deleteRootComment_success() throws Exception { .andExpect(status().isOk()); // then - assertThat(commentJpaRepository.findByCommentIdAndStatus(commentId,INACTIVE)).isPresent(); + CommentJpaEntity commentJpaEntity = commentJpaRepository.findById(commentId).orElse(null); + assertThat(commentJpaEntity.getStatus()).isEqualTo(INACTIVE); } @Test @@ -126,7 +127,8 @@ void deleteReplyComment_success() throws Exception { .andExpect(status().isOk()); // then - assertThat(commentJpaRepository.findByCommentIdAndStatus(replyId,INACTIVE)).isPresent(); + CommentJpaEntity commentJpaEntity = commentJpaRepository.findById(replyId).orElse(null); + assertThat(commentJpaEntity.getStatus()).isEqualTo(INACTIVE); } @Test @@ -148,7 +150,8 @@ void deleteComment_success() throws Exception { .andExpect(status().isOk()); // then - assertThat(commentJpaRepository.findByCommentIdAndStatus(commentId,INACTIVE)).isPresent(); + CommentJpaEntity commentJpaEntity = commentJpaRepository.findById(commentId).orElse(null); + assertThat(commentJpaEntity.getStatus()).isEqualTo(INACTIVE); // Feed 댓글수 감소 확인 FeedJpaEntity updatedFeed = feedJpaRepository.findById(feed.getPostId()).get(); diff --git a/src/test/java/konkuk/thip/common/persistence/JpaRepositoryMethodTest.java b/src/test/java/konkuk/thip/common/persistence/JpaRepositoryMethodTest.java new file mode 100644 index 000000000..f3e8968f0 --- /dev/null +++ b/src/test/java/konkuk/thip/common/persistence/JpaRepositoryMethodTest.java @@ -0,0 +1,119 @@ +package konkuk.thip.common.persistence; + +import jakarta.persistence.Entity; +import jakarta.persistence.EntityManager; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.domain.EntityScan; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.test.context.ActiveProfiles; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertSame; + +@DataJpaTest +@ActiveProfiles("test") +@EnableJpaRepositories( + considerNestedRepositories = true, + basePackageClasses = JpaRepositoryMethodTest.class // 이 테스트 안의 nested repo만 스캔 +) +@EntityScan(basePackageClasses = JpaRepositoryMethodTest.class) +public class JpaRepositoryMethodTest { + + /** + * JPA 리포지토리의 PK 조회(findById)와 파생 쿼리(JPQL 기반, findByUserId)의 내부 동작 차이를 + * “영속성 컨텍스트(1차 캐시), auto flush, 동일성 해석(Identity Resolution)” + * 관점에서 검증하는 슬라이스 테스트입니다. + */ + + @Autowired EntityManager em; + @Autowired TestUserRepository testUserRepository; + + @Entity + @Getter + @Setter + @NoArgsConstructor + private static class TestUser { + @Id + @GeneratedValue + private Long userId; + + private String nickname; + + public TestUser(String nickname) { + this.nickname = nickname; + } + } + + private interface TestUserRepository extends JpaRepository { + Optional findByUserId(Long userId); + } + + @Test + @DisplayName("jpa repository가 제공하는 findById 메서드는 영속성 컨텍스트 flush 또는 쿼리 없이 1차 캐시를 먼저 바라본다.") + void findById_use_first_level_cache_without_flush_or_query() throws Exception { + //given + TestUser testUser = new TestUser("노성준"); + em.persist(testUser); // 1차 캐시에 등록 (아직 DB에 반영되지 않음, flush X) + Long id = testUser.getUserId(); + + //when + TestUser found = testUserRepository.findById(id).orElseThrow(); // findById 메서드 호출 + + //then + assertSame(testUser, found); // 동일 인스턴스임을 확인 -> 1차 캐시에 저장된 엔티티를 조회했으므로 + /** + * 추가로 로그에서 select 쿼리가 실행되지 않았음을 확인할 수 있다. + * insert 쿼리는 트랜잭션 커밋 -> flush 시점에 실행된다. + */ + } + + @Test + @DisplayName("jpa repository에 정의한 jpql 메서드는 필요시 auto flush -> DB query -> 영속성 컨텍스트에서의 동일성 해석 과정을 거친 후 엔티티를 반환한다.") + void derivedQuery_auto_flush_if_needed_then_select_and_identity_resolution() throws Exception { + //given + TestUser testUser = new TestUser("노성준"); + em.persist(testUser); // 1차 캐시에 등록 (아직 DB에 반영되지 않음, flush X) + Long id = testUser.getUserId(); + + testUser.setNickname("김희용"); // 더티(수정) 상태 + + //when + TestUser found = testUserRepository.findByUserId(id).orElseThrow(); // findByUserId 메서드 호출 + /** + * 이때 FlushMode = AUTO(디폴트) 이므로, hibernate는 쿼리 실행 전 auto flush 필요 여부를 판단함 + * (-> 변경 사항이 현재 쿼리 결과에 영향을 줄 수 있는지를 판단) + * 이후 DB에 query가 날라간다 + */ + + //then + assertSame(testUser, found); // 동일 인스턴스임을 확인 -> DB select 쿼리가 나가지만, 동일성 해석을 거쳐 1차 캐시의 인스턴스를 반환 + /** + * 이미 1차 캐시에 DB에서 로드한 엔티티와 동일한 키값을 가지는 엔티티가 존재하므로, 기존 인스턴스를 반환한다. + */ + } + + @Test + @DisplayName("영속성 컨텍스트 내부에서 remove 처리된 엔티티를 findById 메서드로 조회할 경우, null을 반환한다.") + void findById_returns_null_if_entity_removed_in_first_level_cache() throws Exception { + //given + TestUser testUser = new TestUser("노성준"); + em.persist(testUser); + Long id = testUser.getUserId(); + + em.remove(testUser); // 영속성 컨텍스트에서 removed로 마킹 + + //when //then + assertThat(testUserRepository.findById(id)).isEmpty(); + } +} diff --git a/src/test/java/konkuk/thip/common/persistence/StatusFilterTest.java b/src/test/java/konkuk/thip/common/persistence/StatusFilterTest.java new file mode 100644 index 000000000..238491f34 --- /dev/null +++ b/src/test/java/konkuk/thip/common/persistence/StatusFilterTest.java @@ -0,0 +1,238 @@ +package konkuk.thip.common.persistence; + +import konkuk.thip.book.adapter.out.jpa.BookJpaEntity; +import konkuk.thip.book.adapter.out.persistence.repository.BookJpaRepository; +import konkuk.thip.book.adapter.out.persistence.repository.SavedBookJpaRepository; +import konkuk.thip.common.util.TestEntityFactory; +import konkuk.thip.config.StatusFilterTestConfig; +import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; +import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; +import konkuk.thip.user.domain.value.Alias; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +@ActiveProfiles("test") +public class StatusFilterTest { + + @Autowired private UserJpaRepository userJpaRepository; + @Autowired private BookJpaRepository bookJpaRepository; + @Autowired private SavedBookJpaRepository savedBookJpaRepository; + + @Autowired private StatusFilterTestConfig.TestUserIdFindService testUserIdFindService; + @Autowired private StatusFilterTestConfig.TestUserQueryService testUserQueryService; + @Autowired private StatusFilterTestConfig.TestUserJpqlService testUserJpqlService; + @Autowired private StatusFilterTestConfig.TestUserQuerydslService testUserQuerydslService; + + @Autowired private JdbcTemplate jdbcTemplate; + + @AfterEach + public void tearDown() { + savedBookJpaRepository.deleteAllInBatch(); + bookJpaRepository.deleteAllInBatch(); + userJpaRepository.deleteAllInBatch(); + } + + private void saveActiveUser(int count) { + for (int i = 1; i <= count; i++) { + UserJpaEntity user = TestEntityFactory.createUser(Alias.WRITER, "activeUser" + i); + userJpaRepository.save(user); + } + } + + private void saveInactiveUser(int count) { + for (int i = 1; i <= count; i++) { + UserJpaEntity user = TestEntityFactory.createUser(Alias.WRITER, "inactiveUser" + i); + userJpaRepository.save(user); + + jdbcTemplate.update( + "UPDATE users SET status = 'INACTIVE' WHERE user_id = ?", + user.getUserId() + ); + } + } + + @Test + @DisplayName("spring data jpa의 기본 findById 메서드는 PK를 기준으로만 조회하므로 status 필터링이 적용되지 않는다.") + void default_find_by_id_method_does_not_execute_filtering() throws Exception { + //given + UserJpaEntity activeUser = userJpaRepository.save(TestEntityFactory.createUser(Alias.WRITER, "activeUser")); + UserJpaEntity inactiveUser = userJpaRepository.save(TestEntityFactory.createUser(Alias.WRITER, "inactiveUser")); + jdbcTemplate.update( + "UPDATE users SET status = 'INACTIVE' WHERE user_id = ?", + inactiveUser.getUserId() + ); + + //when + Optional findActiveUser = testUserIdFindService.defaultFindById(activeUser.getUserId()); + Optional findInactiveUser = testUserIdFindService.defaultFindById(inactiveUser.getUserId()); + + //then + assertThat(findActiveUser).isPresent(); + assertThat(findInactiveUser).isPresent(); // status 필터링이 적용되지 않아서 INACTIVE 엔티티도 조회됨 + } + + @Test + @DisplayName("jpa repository에 정의한 custom 메서드는 status 필터링이 적용된다.") + void custom_find_active_by_id_method_does_execute_filtering() throws Exception { + //given + UserJpaEntity activeUser = userJpaRepository.save(TestEntityFactory.createUser(Alias.WRITER, "activeUser")); + UserJpaEntity inactiveUser = userJpaRepository.save(TestEntityFactory.createUser(Alias.WRITER, "inactiveUser")); + jdbcTemplate.update( + "UPDATE users SET status = 'INACTIVE' WHERE user_id = ?", + inactiveUser.getUserId() + ); + + //when + Optional findActiveUser = testUserIdFindService.customFindById(activeUser.getUserId()); + Optional findInactiveUser = testUserIdFindService.customFindById(inactiveUser.getUserId()); + + //then + assertThat(findActiveUser).isPresent(); + assertThat(findInactiveUser).isNotPresent(); // status 필터링이 적용되어 INACTIVE 엔티티는 조회되지 않음 + } + + @Test + @DisplayName("[jpa 쿼리 메서드] active 상태인 엔티티만 조회하는 것이 기본 동작이다.") + void jpa_query_method_default_find_active_entities() throws Exception { + //given + saveActiveUser(3); + saveInactiveUser(2); + + //when + List userJpaEntities = testUserQueryService.findAllActiveOnly(); + + //then + assertThat(userJpaEntities).hasSize(3) + .extracting(UserJpaEntity::getNickname) + .containsExactlyInAnyOrder( + "activeUser1", "activeUser2", "activeUser3" + ); + } + + @Test + @DisplayName("[jpa 쿼리 메서드] IncludeInactive 어노테이션이 붙은 메서드는 active, inactive 상태인 모든 엔티티를 조회한다.") + void jpa_query_method_specific_find_active_and_inactive_entities() throws Exception { + //given + saveActiveUser(3); + saveInactiveUser(2); + + //when + List userJpaEntities = testUserQueryService.findAllIncludingInactive(); + + //then + assertThat(userJpaEntities).hasSize(5) + .extracting(UserJpaEntity::getNickname) + .containsExactlyInAnyOrder( + "activeUser1", "activeUser2", "activeUser3", "inactiveUser1", "inactiveUser2" + ); + } + + @Test + @DisplayName("[jpql] active 상태인 엔티티만 조회하는 것이 기본 동작이다.") + void jpql_default_find_active_entities() throws Exception { + //given + saveActiveUser(3); + saveInactiveUser(2); + + //when + List userJpaEntities = testUserJpqlService.findAllByJpql(); + + //then + assertThat(userJpaEntities).hasSize(3) + .extracting(UserJpaEntity::getNickname) + .containsExactlyInAnyOrder( + "activeUser1", "activeUser2", "activeUser3" + ); + } + + @Test + @DisplayName("[jpql] IncludeInactive 어노테이션이 붙은 메서드는 active, inactive 상태인 모든 엔티티를 조회한다.") + void jpql_specific_find_active_and_inactive_entities() throws Exception { + //given + saveActiveUser(3); + saveInactiveUser(2); + + //when + List userJpaEntities = testUserJpqlService.findAllIncludingInactiveByJpql(); + + //then + assertThat(userJpaEntities).hasSize(5) + .extracting(UserJpaEntity::getNickname) + .containsExactlyInAnyOrder( + "activeUser1", "activeUser2", "activeUser3", "inactiveUser1", "inactiveUser2" + ); + } + + @Test + @DisplayName("[querydsl] active 상태인 엔티티만 조회하는 것이 기본 동작이다.") + void query_dsl_default_find_active_entities() throws Exception { + //given + saveActiveUser(3); + saveInactiveUser(2); + + //when + List userJpaEntities = testUserQuerydslService.findAllByQuerydsl(); + + //then + assertThat(userJpaEntities).hasSize(3) + .extracting(UserJpaEntity::getNickname) + .containsExactlyInAnyOrder( + "activeUser1", "activeUser2", "activeUser3" + ); + } + + @Test + @DisplayName("[querydsl 쿼리 메서드] IncludeInactive 어노테이션이 붙은 메서드는 active, inactive 상태인 모든 엔티티를 조회한다.") + void query_dsl_specific_find_active_and_inactive_entities() throws Exception { + //given + saveActiveUser(3); + saveInactiveUser(2); + + //when + List userJpaEntities = testUserQuerydslService.findAllIncludingInactiveByQuerydsl(); + + //then + assertThat(userJpaEntities).hasSize(5) + .extracting(UserJpaEntity::getNickname) + .containsExactlyInAnyOrder( + "activeUser1", "activeUser2", "activeUser3", "inactiveUser1", "inactiveUser2" + ); + } + + @Test + @DisplayName("[join 테스트] 루트=User + SavedBook ON-조인 시: 기본은 ACTIVE만, @IncludeInactive 적용 시 ACTIVE+INACTIVE 모두 집계한다.") + void join_filter_propagation_on_user() throws Exception { + //given + UserJpaEntity activeUser = userJpaRepository.save(TestEntityFactory.createUser(Alias.WRITER, "activeUser")); + UserJpaEntity inactiveUser = userJpaRepository.save(TestEntityFactory.createUser(Alias.WRITER, "inactiveUser")); + jdbcTemplate.update( + "UPDATE users SET status = 'INACTIVE' WHERE user_id = ?", + inactiveUser.getUserId() + ); + + BookJpaEntity bookJpaEntity = bookJpaRepository.save(TestEntityFactory.createBook()); + + savedBookJpaRepository.save(TestEntityFactory.createSavedBook(activeUser, bookJpaEntity)); + savedBookJpaRepository.save(TestEntityFactory.createSavedBook(inactiveUser, bookJpaEntity)); + + //when + long defCount = testUserQuerydslService.countSaversByBook(bookJpaEntity.getBookId()); + long incCount = testUserQuerydslService.countSaversByBookIncludingInactive(bookJpaEntity.getBookId()); + + //then + assertThat(defCount).isEqualTo(1); // active user만 카운트 + assertThat(incCount).isEqualTo(2); // active + inactive user 모두 카운트 + } +} diff --git a/src/test/java/konkuk/thip/config/StatusFilterTestConfig.java b/src/test/java/konkuk/thip/config/StatusFilterTestConfig.java new file mode 100644 index 000000000..7341c8e26 --- /dev/null +++ b/src/test/java/konkuk/thip/config/StatusFilterTestConfig.java @@ -0,0 +1,130 @@ +package konkuk.thip.config; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import konkuk.thip.book.adapter.out.jpa.QSavedBookJpaEntity; +import konkuk.thip.common.annotation.persistence.IncludeInactive; +import konkuk.thip.user.adapter.out.jpa.QUserJpaEntity; +import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; +import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; +import org.springframework.context.annotation.Configuration; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; +import lombok.RequiredArgsConstructor; + +import java.util.List; +import java.util.Optional; + +@Configuration +public class StatusFilterTestConfig { + + // jpa repository PK 조회 메서드 테스트 + @Component + @RequiredArgsConstructor + public static class TestUserIdFindService { + + private final UserJpaRepository userJpaRepository; + + /** jpa repository 기본 findById 메서드 */ + @Transactional(readOnly = true) + public Optional defaultFindById(Long userId) { + return userJpaRepository.findById(userId); + } + + /** jpa repository custom 메서드 */ + @Transactional(readOnly = true) + public Optional customFindById(Long userId) { + return userJpaRepository.findByUserId(userId); + } + } + + // jpa query 메서드 + @Component + @RequiredArgsConstructor + public static class TestUserQueryService { + + private final UserJpaRepository userJpaRepository; + + /** 기본: ACTIVE만 (Aspect가 트랜잭션 경계에서 statusFilter를 ACTIVE로 enable) */ + @Transactional(readOnly = true) + public List findAllActiveOnly() { + return userJpaRepository.findAll(); + } + + /** IncludeInactive: ACTIVE + INACTIVE 모두 */ + @IncludeInactive + @Transactional(readOnly = true) + public List findAllIncludingInactive() { + return userJpaRepository.findAll(); + } + } + + // jpql + @Component + @RequiredArgsConstructor + public static class TestUserJpqlService { + private final EntityManager em; + + /** 기본: ACTIVE만 */ + @Transactional(readOnly = true) + public List findAllByJpql() { + return em.createQuery( + "select u from UserJpaEntity u", UserJpaEntity.class + ).getResultList(); + } + + /** IncludeInactive: ACTIVE + INACTIVE */ + @IncludeInactive + @Transactional(readOnly = true) + public List findAllIncludingInactiveByJpql() { + return em.createQuery( + "select u from UserJpaEntity u", UserJpaEntity.class + ).getResultList(); + } + } + + // querydsl + @Component + @RequiredArgsConstructor + public static class TestUserQuerydslService { + private final JPAQueryFactory qf; + + /** 기본: ACTIVE만 */ + @Transactional(readOnly = true) + public List findAllByQuerydsl() { + QUserJpaEntity u = QUserJpaEntity.userJpaEntity; + return qf.selectFrom(u).fetch(); + } + + /** IncludeInactive: ACTIVE + INACTIVE */ + @IncludeInactive + @Transactional(readOnly = true) + public List findAllIncludingInactiveByQuerydsl() { + QUserJpaEntity u = QUserJpaEntity.userJpaEntity; + return qf.selectFrom(u).fetch(); + } + + @Transactional(readOnly = true) + public long countSaversByBook(Long bookId) { + QSavedBookJpaEntity sb = QSavedBookJpaEntity.savedBookJpaEntity; + QUserJpaEntity u = QUserJpaEntity.userJpaEntity; + return qf.select(u.userId.countDistinct()) + .from(u) + .join(sb).on(sb.userJpaEntity.eq(u)) + .where(sb.bookJpaEntity.bookId.eq(bookId)) + .fetchOne(); + } + + @IncludeInactive + @Transactional(readOnly = true) + public long countSaversByBookIncludingInactive(Long bookId) { + QSavedBookJpaEntity sb = QSavedBookJpaEntity.savedBookJpaEntity; + QUserJpaEntity u = QUserJpaEntity.userJpaEntity; + return qf.select(u.userId.countDistinct()) + .from(u) + .join(sb).on(sb.userJpaEntity.eq(u)) + .where(sb.bookJpaEntity.bookId.eq(bookId)) + .fetchOne(); + } + } +} diff --git a/src/test/java/konkuk/thip/feed/adapter/in/web/FeedDeleteApiTest.java b/src/test/java/konkuk/thip/feed/adapter/in/web/FeedDeleteApiTest.java index 79ed5dc7c..8b39285be 100644 --- a/src/test/java/konkuk/thip/feed/adapter/in/web/FeedDeleteApiTest.java +++ b/src/test/java/konkuk/thip/feed/adapter/in/web/FeedDeleteApiTest.java @@ -81,7 +81,8 @@ void deleteFeed_success() throws Exception { // then: 1) 피드 soft delete (status=INACTIVE) - assertThat(feedJpaRepository.findByPostIdAndStatus(feed.getPostId(), INACTIVE)).isPresent(); + FeedJpaEntity feedJpaEntity = feedJpaRepository.findById(feed.getPostId()).orElse(null); + assertThat(feedJpaEntity.getStatus()).isEqualTo(INACTIVE); // 4) 댓글 삭제 soft delete assertThat(commentJpaRepository.findById(comment.getCommentId())).isPresent(); diff --git a/src/test/java/konkuk/thip/room/adapter/in/web/RoomJoinApiTest.java b/src/test/java/konkuk/thip/room/adapter/in/web/RoomJoinApiTest.java index 7f404183b..8b9164db5 100644 --- a/src/test/java/konkuk/thip/room/adapter/in/web/RoomJoinApiTest.java +++ b/src/test/java/konkuk/thip/room/adapter/in/web/RoomJoinApiTest.java @@ -29,6 +29,7 @@ import java.util.HashMap; import java.util.Map; +import static konkuk.thip.common.entity.StatusType.INACTIVE; import static konkuk.thip.room.application.port.in.dto.RoomJoinType.CANCEL; import static konkuk.thip.room.application.port.in.dto.RoomJoinType.JOIN; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; @@ -51,6 +52,7 @@ class RoomJoinApiTest { private RoomJpaEntity room; private UserJpaEntity host; private UserJpaEntity participant; + private RoomParticipantJpaEntity memberParticipation; private void setUpWithOnlyHost() { Alias alias = TestEntityFactory.createLiteratureAlias(); @@ -70,7 +72,7 @@ private void setUpWithParticipant() { Category category = TestEntityFactory.createLiteratureCategory(); createRoom(book, category,2); // 방장과 참여자 포함 roomParticipantJpaRepository.save(TestEntityFactory.createRoomParticipant(room, host, RoomParticipantRole.HOST, 0.0)); - roomParticipantJpaRepository.save(TestEntityFactory.createRoomParticipant(room, participant, RoomParticipantRole.MEMBER, 0.0)); + memberParticipation = roomParticipantJpaRepository.save(TestEntityFactory.createRoomParticipant(room, participant, RoomParticipantRole.MEMBER, 0.0)); } private void createRoom(BookJpaEntity book, Category category, int memberCount) { @@ -171,9 +173,8 @@ void cancelJoin_success() throws Exception { .andExpect(status().isOk()); // 참여자 삭제 확인 - boolean exists = roomParticipantJpaRepository - .existsByUserIdAndRoomId(participant.getUserId(), room.getRoomId()); - assertThat(exists).isFalse(); + RoomParticipantJpaEntity member = roomParticipantJpaRepository.findById(memberParticipation.getRoomParticipantId()).orElse(null); + assertThat(member.getStatus()).isEqualTo(INACTIVE); // 인원수 감소 확인 room = roomJpaRepository.findById(room.getRoomId()).orElseThrow(); diff --git a/src/test/java/konkuk/thip/room/adapter/in/web/RoomParticipantDeleteApiTest.java b/src/test/java/konkuk/thip/room/adapter/in/web/RoomParticipantDeleteApiTest.java index 001323db0..6210bb75c 100644 --- a/src/test/java/konkuk/thip/room/adapter/in/web/RoomParticipantDeleteApiTest.java +++ b/src/test/java/konkuk/thip/room/adapter/in/web/RoomParticipantDeleteApiTest.java @@ -27,6 +27,7 @@ import java.time.LocalDate; +import static konkuk.thip.common.entity.StatusType.INACTIVE; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; @@ -131,8 +132,11 @@ void leaveRoom_success() throws Exception { .andExpect(status().isOk()); // 방에서 참여자 정보가 사라졌는지 확인 - boolean exists = roomParticipantJpaRepository.existsByUserIdAndRoomId(participant.getUserId(), room.getRoomId()); - assertThat(exists).isFalse(); + em.flush(); + em.clear(); // 영속성 컨텍스트 초기화 -> 테스트 코드에 트랜잭션이 있으므로 + + RoomParticipantJpaEntity member = roomParticipantJpaRepository.findById(memberParticipation.getRoomParticipantId()).orElse(null); + assertThat(member.getStatus()).isEqualTo(INACTIVE); // 인원수 감소 확인 RoomJpaEntity updateRoom = roomJpaRepository.findById(room.getRoomId()).orElseThrow(); diff --git a/src/test/java/konkuk/thip/roompost/adapter/in/web/RecordDeleteApiTest.java b/src/test/java/konkuk/thip/roompost/adapter/in/web/RecordDeleteApiTest.java index bc1c29b67..b67553006 100644 --- a/src/test/java/konkuk/thip/roompost/adapter/in/web/RecordDeleteApiTest.java +++ b/src/test/java/konkuk/thip/roompost/adapter/in/web/RecordDeleteApiTest.java @@ -89,11 +89,13 @@ void deleteRecord_success() throws Exception { .andExpect(status().isOk()); - // then: 1) 기록 soft delete (status=INACTIVE) - assertThat(recordJpaRepository.findByPostIdAndStatus(record.getPostId(), INACTIVE)).isPresent(); + // then: 1) 기록 soft delete (status=INACTIVE -> findByPostId() 조회 안됨) + RecordJpaEntity deletedRecord = recordJpaRepository.findById(record.getPostId()).orElse(null); + assertThat(deletedRecord.getStatus()).isEqualTo(INACTIVE); // 2) 댓글 삭제 soft delete - assertThat(commentJpaRepository.findByCommentIdAndStatus(comment.getCommentId(),INACTIVE)).isPresent(); + CommentJpaEntity deleteComment = commentJpaRepository.findById(comment.getCommentId()).orElse(null); + assertThat(deleteComment.getStatus()).isEqualTo(INACTIVE); // 3) 댓글 좋아요 삭제 assertThat(commentLikeJpaRepository.count()).isEqualTo(0); diff --git a/src/test/java/konkuk/thip/roompost/adapter/in/web/VoteDeleteApiTest.java b/src/test/java/konkuk/thip/roompost/adapter/in/web/VoteDeleteApiTest.java index 8824fab09..f57614cea 100644 --- a/src/test/java/konkuk/thip/roompost/adapter/in/web/VoteDeleteApiTest.java +++ b/src/test/java/konkuk/thip/roompost/adapter/in/web/VoteDeleteApiTest.java @@ -113,10 +113,12 @@ void deleteVote_success() throws Exception { // then: 1) 투표 soft delete (status=INACTIVE) - assertThat(voteJpaRepository.findByPostIdAndStatus(vote.getPostId(), INACTIVE)).isPresent(); + VoteJpaEntity voteJpaEntity = voteJpaRepository.findById(vote.getPostId()).orElse(null); + assertThat(voteJpaEntity.getStatus()).isEqualTo(INACTIVE); // 2) 댓글 삭제 soft delete - assertThat(commentJpaRepository.findByCommentIdAndStatus(comment.getCommentId(),INACTIVE)).isPresent(); + CommentJpaEntity commentJpaEntity = commentJpaRepository.findById(comment.getCommentId()).orElse(null); + assertThat(commentJpaEntity.getStatus()).isEqualTo(INACTIVE); // 3) 댓글 좋아요 물리 삭제 assertThat(commentLikeJpaRepository.count()).isEqualTo(0); diff --git a/src/test/java/konkuk/thip/user/adapter/in/web/UserVerifyNicknameControllerTest.java b/src/test/java/konkuk/thip/user/adapter/in/web/UserVerifyNicknameControllerTest.java index 63b6f12c1..fa56158ec 100644 --- a/src/test/java/konkuk/thip/user/adapter/in/web/UserVerifyNicknameControllerTest.java +++ b/src/test/java/konkuk/thip/user/adapter/in/web/UserVerifyNicknameControllerTest.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import konkuk.thip.common.entity.StatusType; import konkuk.thip.common.util.TestEntityFactory; import konkuk.thip.user.adapter.in.web.request.UserVerifyNicknameRequest; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; @@ -14,6 +15,7 @@ import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; @@ -40,12 +42,12 @@ class UserVerifyNicknameControllerTest { @Autowired private ObjectMapper objectMapper; - @Autowired - private UserJpaRepository userJpaRepository; + @Autowired private UserJpaRepository userJpaRepository; + @Autowired private JdbcTemplate jdbcTemplate; @AfterEach void tearDown() { - userJpaRepository.deleteAll(); + userJpaRepository.deleteAllInBatch(); } @Test @@ -147,4 +149,31 @@ void nickname_too_long() throws Exception { .andExpect(jsonPath("$.code").value(API_INVALID_PARAM.getCode())) .andExpect(jsonPath("$.message", containsString("닉네임은 최대 10자 입니다."))); } + + @Test + @DisplayName("회원 탈퇴한(= soft delete 처리된) 유저의 닉네임 정보를 포함해서 중복 검증을 수행한다.") + void verify_nickname_with_soft_delete_users() throws Exception { + //given + UserJpaEntity deleteUser = userJpaRepository.save(TestEntityFactory.createUser(Alias.WRITER, "노성준")); + jdbcTemplate.update( + "UPDATE users SET status = ? WHERE user_id = ?", + StatusType.INACTIVE.name(), deleteUser.getUserId()); + + UserVerifyNicknameRequest request = new UserVerifyNicknameRequest("노성준"); + + //when + ResultActions result = mockMvc.perform(post("/users/nickname") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))); + + //then + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.data.isVerified").exists()); + + String json = result.andReturn().getResponse().getContentAsString(); + JsonNode jsonNode = objectMapper.readTree(json); + boolean isVerified = jsonNode.path("data").path("isVerified").asBoolean(); + + assertThat(isVerified).isFalse(); // 닉네임 중복으로 인해 isVerified == false + } }