From f7ed1f0fac62a4c4182e49aca64ae73199410ce9 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Fri, 29 Aug 2025 17:14:15 +0900 Subject: [PATCH 01/34] =?UTF-8?q?[feat]=20BaseJpaEntity=20=EC=97=90=20hibe?= =?UTF-8?q?rnate=20filter=20=EC=A0=95=EC=9D=98=20(#277)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - status 필드값을 필터로 정의 --- .../konkuk/thip/common/entity/BaseJpaEntity.java | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/main/java/konkuk/thip/common/entity/BaseJpaEntity.java b/src/main/java/konkuk/thip/common/entity/BaseJpaEntity.java index 93ec51963..dd18f75d9 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) From 5daea4c9b78e6dc27e47d09d4ad0b38e74b0500d Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Fri, 29 Aug 2025 17:14:52 +0900 Subject: [PATCH 02/34] =?UTF-8?q?[feat]=20spring=20boot=20aop=20=ED=99=9C?= =?UTF-8?q?=EC=9A=A9=ED=95=98=EA=B8=B0=20=EC=9C=84=ED=95=B4=20=EC=9D=98?= =?UTF-8?q?=EC=A1=B4=EC=84=B1=20=EC=B6=94=EA=B0=80=20(#277)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/build.gradle b/build.gradle index bc529f8d8..81ff76ca1 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' @@ -78,6 +81,9 @@ dependencies { // Spring Boot Actuator implementation 'org.springframework.boot:spring-boot-starter-actuator' + + // AOP + implementation 'org.springframework.boot:spring-boot-starter-aop' } def querydslDir = layout.buildDirectory.dir("generated/querydsl").get().asFile From 68c759cc46d291968e19ef319fed712bff6d92a7 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Fri, 29 Aug 2025 18:21:44 +0900 Subject: [PATCH 03/34] =?UTF-8?q?[feat]=20statusFilter=20Aspect=20?= =?UTF-8?q?=EB=8F=84=EC=9E=85=20(#277)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - hibernate @Filter 를 트랜잭션 경계 진입 시 자동 적용하는 AOP 추가 - 세션 단위에서 filter enable/disable 을 판단하여 filter 중복 적용 방지 --- .../thip/common/aop/StatusFilterAspect.java | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 src/main/java/konkuk/thip/common/aop/StatusFilterAspect.java 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..7950ea644 --- /dev/null +++ b/src/main/java/konkuk/thip/common/aop/StatusFilterAspect.java @@ -0,0 +1,118 @@ +package konkuk.thip.common.aop; + +import jakarta.persistence.EntityManager; +import konkuk.thip.common.entity.StatusType; +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 java.util.List; + +@Slf4j +@Aspect +@Component +@RequiredArgsConstructor +public class StatusFilterAspect { + + private final EntityManager em; + 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 + ")"; + + private static final String PCUT_INCLUDE_INACTIVE = + "@annotation(" + ANN_INCLUDE_INACTIVE + ")"; + private static final String PCUT_UNFILTERED = + "@annotation(" + ANN_UNFILTERED + ")"; + + // 기본: ACTIVE만 (트랜잭션 경계 진입 시) + @Around(PCUT_TX_DEFAULT) + public Object enableActiveByDefault(ProceedingJoinPoint pjp) throws Throwable { + var s = session(); + 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 모두 + @Around(PCUT_INCLUDE_INACTIVE) + public Object includeInactive(ProceedingJoinPoint pjp) throws Throwable { + var s = session(); + 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: 필터 해제 + @Around(PCUT_UNFILTERED) + public Object unfiltered(ProceedingJoinPoint pjp) throws Throwable { + var s = session(); + var wasEnabled = isFilterEnabled(s); + if (wasEnabled) { + disableFilter(s); + log.debug("statusFilter -> DISABLED (temporarily by @Unfiltered)"); + } + 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"); + } +} From 9633f59f66d53d01674e1e1da77e593c8a9fa818 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Fri, 29 Aug 2025 18:23:02 +0900 Subject: [PATCH 04/34] =?UTF-8?q?[feat]=20IncludeInactive,=20Unfiltered=20?= =?UTF-8?q?=EC=96=B4=EB=85=B8=ED=85=8C=EC=9D=B4=EC=85=98=20=EB=8F=84?= =?UTF-8?q?=EC=9E=85=20(#277)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - inactive 상태(= soft delete) 인 jpa entity 도 같이 조회 or 필터링 없이 모든 jpa entity 조회할 수 있도록 어노테이션 추가 --- .../annotation/persistence/IncludeInactive.java | 11 +++++++++++ .../common/annotation/persistence/Unfiltered.java | 11 +++++++++++ 2 files changed, 22 insertions(+) create mode 100644 src/main/java/konkuk/thip/common/annotation/persistence/IncludeInactive.java create mode 100644 src/main/java/konkuk/thip/common/annotation/persistence/Unfiltered.java 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 { +} From ee75622a95b2e9a7153c209d525a853546f3048c Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Fri, 29 Aug 2025 18:24:19 +0900 Subject: [PATCH 05/34] =?UTF-8?q?[test]=20filter=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20(#277)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 등록한 hibernate filter 가 잘 동작하는지 확인하기 위해 테스트 코드 작성 --- .../thip/common/util/StatusFilterTest.java | 230 ++++++++++++++++++ .../thip/config/StatusFilterTestConfig.java | 109 +++++++++ 2 files changed, 339 insertions(+) create mode 100644 src/test/java/konkuk/thip/common/util/StatusFilterTest.java create mode 100644 src/test/java/konkuk/thip/config/StatusFilterTestConfig.java diff --git a/src/test/java/konkuk/thip/common/util/StatusFilterTest.java b/src/test/java/konkuk/thip/common/util/StatusFilterTest.java new file mode 100644 index 000000000..408e29555 --- /dev/null +++ b/src/test/java/konkuk/thip/common/util/StatusFilterTest.java @@ -0,0 +1,230 @@ +package konkuk.thip.common.util; + +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.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 java.util.List; + +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.TestUserService testUserService; + @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("[jpa 쿼리 메서드] active 상태인 엔티티만 조회하는 것이 기본 동작이다.") + void jpa_query_method_default_find_active_entities() throws Exception { + //given + saveActiveUser(3); + saveInactiveUser(2); + + //when + List userJpaEntities = testUserService.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 = testUserService.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 모두 카운트 + } + +// @Test +// @DisplayName("LEFT JOIN + 글로벌 필터 ON: 자식이 INACTIVE뿐이면 부모가 사라져 count=0, 우회 방법은 count=1") +// void leftJoinBehavesInnerWhenAllChildrenInactive_thenBypassKeepsParent() { +// // given: Book 1권, SavedBook 1건(자식) — 자식은 INACTIVE로 강제 +// UserJpaEntity u = userJpaRepository.save(TestEntityFactory.createUser(Alias.WRITER, "u1")); +// BookJpaEntity b = bookJpaRepository.save(TestEntityFactory.createBook()); +// var sb = savedBookJpaRepository.save(TestEntityFactory.createSavedBook(u, b)); +// jdbcTemplate.update("UPDATE saved_books SET status = 'INACTIVE' WHERE saved_id = ?", sb.getSavedId()); +// +// // when +// long defaultCount = testLeftJoinQuerydslService.countBooksWithLeftJoinDefault(b.getBookId()); // 글로벌 필터 ON (WHERE에 sb.status 조건 주입) +// long onClauseCount = testLeftJoinQuerydslService.countBooksWithLeftJoinOnActive(b.getBookId()); // 필터 OFF + ON절로 ACTIVE 조건 +// +// // then +// assertThat(defaultCount).isZero(); // 부모(Book)는 있지만, WHERE에 sb 조건이 들어가면서 행이 사라짐 → 0 +// assertThat(onClauseCount).isEqualTo(1L); // 부모 보존(LEFT 의미 유지) → 1 +// } +// +// @Test +// @DisplayName("LEFT JOIN + 글로벌 필터 ON: ACTIVE 자식 2개면 count=2(중복), 우회 방법은 부모 기준으로 count=1") +// void leftJoinDuplicatesWithMultipleActiveChildren_thenBypassDedupToParent() { +// // given: Book 1권 + ACTIVE SavedBook 2건 +// UserJpaEntity u = userJpaRepository.save(TestEntityFactory.createUser(Alias.WRITER, "u1")); +// BookJpaEntity b = bookJpaRepository.save(TestEntityFactory.createBook()); +// savedBookJpaRepository.save(TestEntityFactory.createSavedBook(u, b)); +// savedBookJpaRepository.save(TestEntityFactory.createSavedBook(u, b)); +// +// // when +// long defaultCount = testLeftJoinQuerydslService.countBooksWithLeftJoinDefault(b.getBookId()); // WHERE에 sb.status=ACTIVE → 부모 중복 발생 → 2 +// long onClauseCount = testLeftJoinQuerydslService.countBooksWithLeftJoinOnActive(b.getBookId()); // ON절에 ACTIVE 조건, 부모 보존 관점에서 → 1 +// +// // then +// assertThat(defaultCount).isEqualTo(2L); // 자식 2건으로 인해 조인 곱 2건 +// assertThat(onClauseCount).isEqualTo(1L); // 부모 기준 1건 +// } +} 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..11b6c3fba --- /dev/null +++ b/src/test/java/konkuk/thip/config/StatusFilterTestConfig.java @@ -0,0 +1,109 @@ +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; + +@Configuration +public class StatusFilterTestConfig { + + // jpa query 메서드 + @Component + @RequiredArgsConstructor + public static class TestUserService { + + 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(); + } + } +} From 50bd3e7b5b17d7d18df01bfab887a792d4f878cf Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Fri, 29 Aug 2025 18:25:12 +0900 Subject: [PATCH 06/34] =?UTF-8?q?[move]=20=EA=B8=B0=EC=A1=B4=20custom=20?= =?UTF-8?q?=EC=96=B4=EB=85=B8=ED=85=8C=EC=9D=B4=EC=85=98=20=ED=8C=A8?= =?UTF-8?q?=ED=82=A4=EC=A7=80=20=EC=9D=B4=EB=8F=99=20(#277)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/common/annotation/{ => application}/DomainService.java | 2 +- .../thip/common/annotation/{ => application}/HelperService.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename src/main/java/konkuk/thip/common/annotation/{ => application}/DomainService.java (89%) rename src/main/java/konkuk/thip/common/annotation/{ => application}/HelperService.java (86%) 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; From 89af422a4a1d3433acc4d35ed396a20ca4976c3a Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Fri, 29 Aug 2025 18:27:02 +0900 Subject: [PATCH 07/34] =?UTF-8?q?[refactor]=20jpa=20entity=20=EC=9D=98=20?= =?UTF-8?q?=EC=97=B0=EA=B4=80=EA=B4=80=EA=B3=84=20nullable=20=EC=A0=9C?= =?UTF-8?q?=EC=95=BD=EC=A1=B0=EA=B1=B4=20=EC=88=98=EC=A0=95=20(#277)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 누락된 nullable = false 추가 - jpa 상속 관계인 엔티티들은 single table 전략 이슈로 인해 nullable = true 유지 --- .../konkuk/thip/book/adapter/out/jpa/SavedBookJpaEntity.java | 4 ++-- .../thip/comment/adapter/out/jpa/CommentJpaEntity.java | 3 +++ .../thip/comment/adapter/out/jpa/CommentLikeJpaEntity.java | 4 ++-- .../java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntity.java | 2 +- .../konkuk/thip/feed/adapter/out/jpa/SavedFeedJpaEntity.java | 4 ++-- .../konkuk/thip/post/adapter/out/jpa/PostLikeJpaEntity.java | 4 ++-- .../recentSearch/adapter/out/jpa/RecentSearchJpaEntity.java | 2 +- .../thip/room/adapter/out/jpa/RoomParticipantJpaEntity.java | 4 ++-- .../roompost/adapter/out/jpa/AttendanceCheckJpaEntity.java | 4 ++-- .../thip/roompost/adapter/out/jpa/RecordJpaEntity.java | 3 +-- .../thip/roompost/adapter/out/jpa/VoteItemJpaEntity.java | 2 +- .../konkuk/thip/roompost/adapter/out/jpa/VoteJpaEntity.java | 3 +-- .../roompost/adapter/out/jpa/VoteParticipantJpaEntity.java | 5 ++--- .../java/konkuk/thip/user/adapter/out/jpa/UserJpaEntity.java | 2 ++ 14 files changed, 24 insertions(+), 22 deletions(-) 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..31f8116f0 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 @@ -18,10 +18,10 @@ public class SavedBookJpaEntity extends BaseJpaEntity { private Long savedId; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id") + @JoinColumn(name = "user_id", nullable = false) private UserJpaEntity userJpaEntity; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "book_id") + @JoinColumn(name = "book_id", nullable = false) private BookJpaEntity bookJpaEntity; } \ No newline at end of file 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..84cb882a6 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 @@ -51,6 +51,9 @@ public class CommentJpaEntity extends BaseJpaEntity { @JoinColumn(name = "user_id", nullable = false) private UserJpaEntity userJpaEntity; + /** + * nullable = true : 최상위 댓글인 경우 null + */ @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "parent_id") private CommentJpaEntity parent; 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..5efe4216b 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 @@ -19,10 +19,10 @@ public class CommentLikeJpaEntity extends BaseJpaEntity { private Long likeId; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id") + @JoinColumn(name = "user_id", nullable = false) private UserJpaEntity userJpaEntity; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "comment_id") + @JoinColumn(name = "comment_id", nullable = false) private CommentJpaEntity commentJpaEntity; } \ No newline at end of file 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..00c3da74b 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 @@ -33,7 +33,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..47256889e 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 @@ -18,10 +18,10 @@ public class SavedFeedJpaEntity extends BaseJpaEntity { private Long savedId; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id") + @JoinColumn(name = "user_id", nullable = false) private UserJpaEntity userJpaEntity; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "post_id") + @JoinColumn(name = "post_id", nullable = false) private FeedJpaEntity feedJpaEntity; } \ No newline at end of file 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..5cadd1506 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 @@ -18,10 +18,10 @@ public class PostLikeJpaEntity extends BaseJpaEntity { private Long likeId; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "post_id") + @JoinColumn(name = "post_id", nullable = false) private PostJpaEntity postJpaEntity; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id") + @JoinColumn(name = "user_id", nullable = false) private UserJpaEntity userJpaEntity; } \ No newline at end of file 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..24fe2bfcd 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 @@ -27,6 +27,6 @@ public class RecentSearchJpaEntity extends BaseJpaEntity { private RecentSearchType type; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id") + @JoinColumn(name = "user_id", nullable = false) private UserJpaEntity userJpaEntity; } \ No newline at end of file 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..e0a6d714b 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 @@ -35,11 +35,11 @@ public class RoomParticipantJpaEntity extends BaseJpaEntity { private RoomParticipantRole roomParticipantRole; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id") + @JoinColumn(name = "user_id", nullable = false) private UserJpaEntity userJpaEntity; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "room_id") + @JoinColumn(name = "room_id", nullable = false) private RoomJpaEntity roomJpaEntity; @VisibleForTesting 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 3e43a40a8..6849547cd 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 @@ -23,11 +23,11 @@ public class AttendanceCheckJpaEntity extends BaseJpaEntity { private String todayComment; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "room_id") + @JoinColumn(name = "room_id", nullable = false) private RoomJpaEntity roomJpaEntity; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id") + @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..40609fa58 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 @@ -26,7 +26,7 @@ public class VoteItemJpaEntity extends BaseJpaEntity { private int count = 0; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "post_id") + @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..55ed5bef1 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 @@ -20,11 +19,11 @@ public class VoteParticipantJpaEntity extends BaseJpaEntity { private Long voteParticipantId; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id") + @JoinColumn(name = "user_id", nullable = false) private UserJpaEntity userJpaEntity; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "vote_item_id") + @JoinColumn(name = "vote_item_id", nullable = false) private VoteItemJpaEntity voteItemJpaEntity; public VoteParticipantJpaEntity updateVoteItem(VoteItemJpaEntity voteItemJpaEntity) { 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 From 64420fd85982740738b194ebcbb1a6c1d4d820ac Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Fri, 29 Aug 2025 18:43:22 +0900 Subject: [PATCH 08/34] =?UTF-8?q?[refactor]=20=EC=84=9C=EB=B9=84=EC=8A=A4?= =?UTF-8?q?=20=EB=A9=94=EC=84=9C=EB=93=9C=EC=97=90=20=EB=88=84=EB=9D=BD?= =?UTF-8?q?=EB=90=9C=20transactional=20=EC=96=B4=EB=85=B8=ED=85=8C?= =?UTF-8?q?=EC=9D=B4=EC=85=98=20=EC=B6=94=EA=B0=80=20(#277)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - status filter 를 적용하기 위한 트랜잭션을 서비스 메서드가 시작하도록 명시 --- .../comment/application/service/CommentShowAllService.java | 2 ++ .../feed/application/service/FeedRelatedWithBookService.java | 2 ++ .../thip/feed/application/service/FeedShowSingleService.java | 2 ++ .../java/konkuk/thip/post/domain/service/PostCountService.java | 2 +- .../application/service/RecentSearchDeleteService.java | 1 + .../application/service/RecentSearchGetService.java | 1 + .../thip/user/application/service/UserIsFollowingService.java | 2 ++ .../thip/user/application/service/UserMyPageService.java | 3 +++ .../user/application/service/UserVerifyNicknameService.java | 2 ++ .../user/application/service/UserViewAliasChoiceService.java | 2 ++ 10 files changed, 18 insertions(+), 1 deletion(-) 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..fe0b59017 100644 --- a/src/main/java/konkuk/thip/comment/application/service/CommentShowAllService.java +++ b/src/main/java/konkuk/thip/comment/application/service/CommentShowAllService.java @@ -11,6 +11,7 @@ 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 +26,7 @@ public class CommentShowAllService implements CommentShowAllUseCase { private final CommentQueryMapper commentQueryMapper; @Override + @Transactional(readOnly = true) public CommentForSinglePostResponse showAllCommentsOfPost(CommentShowAllQuery query) { Cursor cursor = Cursor.from(query.cursorStr(), PAGE_SIZE); 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/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/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/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..1055fe298 100644 --- a/src/main/java/konkuk/thip/user/application/service/UserVerifyNicknameService.java +++ b/src/main/java/konkuk/thip/user/application/service/UserVerifyNicknameService.java @@ -4,6 +4,7 @@ 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 +13,7 @@ public class UserVerifyNicknameService implements UserVerifyNicknameUseCase { private final UserQueryPort userQueryPort; @Override + @Transactional(readOnly = true) 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(); From f77a03b6266aebb1ead7f98eb6f587f018d763cb Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Fri, 29 Aug 2025 19:56:05 +0900 Subject: [PATCH 09/34] =?UTF-8?q?[test]=20jpa=20repository=EC=9D=98=20?= =?UTF-8?q?=EB=94=94=ED=8F=B4=ED=8A=B8=20findById=20=EB=A9=94=EC=84=9C?= =?UTF-8?q?=EB=93=9C=EC=9D=98=20=EB=8F=99=EC=9E=91=EA=B3=BC=20=EC=97=B0?= =?UTF-8?q?=EA=B3=84=ED=95=9C=20filter=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80=20(#277)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - spring data jpa repository 가 제공하는 디폴트 findById 메서드는 내부적으로 바로 PK 기준으로 em.find() 로 동작하므로 filter 적용대상이 아님 - 따라서 소프트 딜리트 적용 대상 entity 를 단건조회할 시에는 custom 메서드를 정의하여 사용해야함 --- .../thip/common/util/StatusFilterTest.java | 87 ++++++++++--------- .../thip/config/StatusFilterTestConfig.java | 23 ++++- 2 files changed, 70 insertions(+), 40 deletions(-) diff --git a/src/test/java/konkuk/thip/common/util/StatusFilterTest.java b/src/test/java/konkuk/thip/common/util/StatusFilterTest.java index 408e29555..68da16b35 100644 --- a/src/test/java/konkuk/thip/common/util/StatusFilterTest.java +++ b/src/test/java/konkuk/thip/common/util/StatusFilterTest.java @@ -14,8 +14,10 @@ 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; @@ -27,7 +29,8 @@ public class StatusFilterTest { @Autowired private BookJpaRepository bookJpaRepository; @Autowired private SavedBookJpaRepository savedBookJpaRepository; - @Autowired private StatusFilterTestConfig.TestUserService testUserService; + @Autowired private StatusFilterTestConfig.TestUserIdFindService testUserIdFindService; + @Autowired private StatusFilterTestConfig.TestUserQueryService testUserQueryService; @Autowired private StatusFilterTestConfig.TestUserJpqlService testUserJpqlService; @Autowired private StatusFilterTestConfig.TestUserQuerydslService testUserQuerydslService; @@ -59,6 +62,48 @@ private void saveInactiveUser(int count) { } } + @Test + @DisplayName("spring data jpa의 기본 findById 메서드는 PK를 기준으로만 조회하므로 status 필터링이 적용되지 않는다.") + @Transactional // filter를 활성화 하기 위한 트랜잭션 어노테이션 + 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 필터링이 적용된다.") + @Transactional // filter를 활성화 하기 위한 트랜잭션 어노테이션 + 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 { @@ -67,7 +112,7 @@ void jpa_query_method_default_find_active_entities() throws Exception { saveInactiveUser(2); //when - List userJpaEntities = testUserService.findAllActiveOnly(); + List userJpaEntities = testUserQueryService.findAllActiveOnly(); //then assertThat(userJpaEntities).hasSize(3) @@ -85,7 +130,7 @@ void jpa_query_method_specific_find_active_and_inactive_entities() throws Except saveInactiveUser(2); //when - List userJpaEntities = testUserService.findAllIncludingInactive(); + List userJpaEntities = testUserQueryService.findAllIncludingInactive(); //then assertThat(userJpaEntities).hasSize(5) @@ -191,40 +236,4 @@ void join_filter_propagation_on_user() throws Exception { assertThat(defCount).isEqualTo(1); // active user만 카운트 assertThat(incCount).isEqualTo(2); // active + inactive user 모두 카운트 } - -// @Test -// @DisplayName("LEFT JOIN + 글로벌 필터 ON: 자식이 INACTIVE뿐이면 부모가 사라져 count=0, 우회 방법은 count=1") -// void leftJoinBehavesInnerWhenAllChildrenInactive_thenBypassKeepsParent() { -// // given: Book 1권, SavedBook 1건(자식) — 자식은 INACTIVE로 강제 -// UserJpaEntity u = userJpaRepository.save(TestEntityFactory.createUser(Alias.WRITER, "u1")); -// BookJpaEntity b = bookJpaRepository.save(TestEntityFactory.createBook()); -// var sb = savedBookJpaRepository.save(TestEntityFactory.createSavedBook(u, b)); -// jdbcTemplate.update("UPDATE saved_books SET status = 'INACTIVE' WHERE saved_id = ?", sb.getSavedId()); -// -// // when -// long defaultCount = testLeftJoinQuerydslService.countBooksWithLeftJoinDefault(b.getBookId()); // 글로벌 필터 ON (WHERE에 sb.status 조건 주입) -// long onClauseCount = testLeftJoinQuerydslService.countBooksWithLeftJoinOnActive(b.getBookId()); // 필터 OFF + ON절로 ACTIVE 조건 -// -// // then -// assertThat(defaultCount).isZero(); // 부모(Book)는 있지만, WHERE에 sb 조건이 들어가면서 행이 사라짐 → 0 -// assertThat(onClauseCount).isEqualTo(1L); // 부모 보존(LEFT 의미 유지) → 1 -// } -// -// @Test -// @DisplayName("LEFT JOIN + 글로벌 필터 ON: ACTIVE 자식 2개면 count=2(중복), 우회 방법은 부모 기준으로 count=1") -// void leftJoinDuplicatesWithMultipleActiveChildren_thenBypassDedupToParent() { -// // given: Book 1권 + ACTIVE SavedBook 2건 -// UserJpaEntity u = userJpaRepository.save(TestEntityFactory.createUser(Alias.WRITER, "u1")); -// BookJpaEntity b = bookJpaRepository.save(TestEntityFactory.createBook()); -// savedBookJpaRepository.save(TestEntityFactory.createSavedBook(u, b)); -// savedBookJpaRepository.save(TestEntityFactory.createSavedBook(u, b)); -// -// // when -// long defaultCount = testLeftJoinQuerydslService.countBooksWithLeftJoinDefault(b.getBookId()); // WHERE에 sb.status=ACTIVE → 부모 중복 발생 → 2 -// long onClauseCount = testLeftJoinQuerydslService.countBooksWithLeftJoinOnActive(b.getBookId()); // ON절에 ACTIVE 조건, 부모 보존 관점에서 → 1 -// -// // then -// assertThat(defaultCount).isEqualTo(2L); // 자식 2건으로 인해 조인 곱 2건 -// assertThat(onClauseCount).isEqualTo(1L); // 부모 기준 1건 -// } } diff --git a/src/test/java/konkuk/thip/config/StatusFilterTestConfig.java b/src/test/java/konkuk/thip/config/StatusFilterTestConfig.java index 11b6c3fba..7341c8e26 100644 --- a/src/test/java/konkuk/thip/config/StatusFilterTestConfig.java +++ b/src/test/java/konkuk/thip/config/StatusFilterTestConfig.java @@ -13,14 +13,35 @@ 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 TestUserService { + public static class TestUserQueryService { private final UserJpaRepository userJpaRepository; From e6e72c4b0ddb2da6ff23d4df23edb87188b43dbe Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Fri, 29 Aug 2025 20:04:07 +0900 Subject: [PATCH 10/34] =?UTF-8?q?[feat]=20soft=20delete=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9=20=EB=8C=80=EC=83=81=20entity=20=EC=9D=98=20=EB=8B=A8?= =?UTF-8?q?=EA=B1=B4=20=EC=A1=B0=ED=9A=8C=EC=9A=A9=20=EB=A9=94=EC=84=9C?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80=20(#277)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../persistence/repository/CommentJpaRepository.java | 7 +++++-- .../out/persistence/repository/FeedJpaRepository.java | 7 +++++-- .../out/persistence/repository/RoomJpaRepository.java | 6 ++++++ .../roomparticipant/RoomParticipantJpaRepository.java | 5 +++++ .../attendancecheck/AttendanceCheckJpaRepository.java | 10 +++++++--- .../repository/record/RecordJpaRepository.java | 7 +++++-- .../persistence/repository/vote/VoteJpaRepository.java | 7 +++++-- .../out/persistence/repository/UserJpaRepository.java | 7 ++++++- 8 files changed, 44 insertions(+), 12 deletions(-) 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/feed/adapter/out/persistence/repository/FeedJpaRepository.java b/src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedJpaRepository.java index 80973ecab..7e5be6824 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 @@ -10,11 +10,14 @@ public interface FeedJpaRepository extends JpaRepository, FeedQueryRepository { + /** + * 소프트 딜리트 적용 대상 entity 단건 조회 메서드 + */ + Optional findByPostId(Long postId); + @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); @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); - - Optional findByPostIdAndStatus(Long postId, StatusType status); } 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..6a7c5a6fc 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,9 +6,15 @@ 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 " + 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..ea3760527 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,6 +10,11 @@ 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 " + 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 31aa2637a..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,21 +1,25 @@ 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; import org.springframework.data.repository.query.Param; import java.time.LocalDateTime; +import java.util.Optional; 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); + 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/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/user/adapter/out/persistence/repository/UserJpaRepository.java b/src/main/java/konkuk/thip/user/adapter/out/persistence/repository/UserJpaRepository.java index f8036c18c..288427d27 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,14 @@ 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); From 86044656a6ec59f1a819833ccf842d6596083117 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Fri, 29 Aug 2025 21:24:33 +0900 Subject: [PATCH 11/34] =?UTF-8?q?[refactor]=20user=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EC=98=81=EC=86=8D=EC=84=B1=20=EB=A9=94=EC=84=9C=EB=93=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#277)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - soft delete 적용 대상인 entity 조회 시 custom 단건 조회 메서드 호출하도록 수정 - status 조건 코드 삭제 --- .../UserCommandPersistenceAdapter.java | 7 +++--- .../repository/UserQueryRepositoryImpl.java | 22 ++++++++----------- 2 files changed, 12 insertions(+), 17 deletions(-) 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/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)); } From 4f352bc8401e41d4e6c92b9c1efe2f94151f6bfb Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Fri, 29 Aug 2025 21:27:13 +0900 Subject: [PATCH 12/34] =?UTF-8?q?[refactor]=20following=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EC=98=81=EC=86=8D=EC=84=B1=20=EB=A9=94=EC=84=9C?= =?UTF-8?q?=EB=93=9C=20=EC=88=98=EC=A0=95=20(#277)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FollowingCommandPersistenceAdapter.java | 4 ++-- .../FollowingQueryRepositoryImpl.java | 20 +++++++------------ 2 files changed, 9 insertions(+), 15 deletions(-) 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/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(); } From 3b4fcec7e030eb472915e93484572a193c2404ec Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Fri, 29 Aug 2025 21:30:19 +0900 Subject: [PATCH 13/34] =?UTF-8?q?[refactor]=20attendanceCheck=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EC=98=81=EC=86=8D=EC=84=B1=20=EB=A9=94=EC=84=9C?= =?UTF-8?q?=EB=93=9C=20=EC=88=98=EC=A0=95=20(#277)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AttendanceCheckCommandPersistenceAdapter.java | 6 +++--- .../persistence/AttendanceCheckQueryPersistenceAdapter.java | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) 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 90dcb5824..a7fcfe49d 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 @@ -28,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) ); @@ -43,7 +43,7 @@ public Long save(AttendanceCheck attendanceCheck) { @Override public Optional findById(Long id) { - return attendanceCheckJpaRepository.findById(id) + return attendanceCheckJpaRepository.findByAttendanceCheckId(id) .map(attendanceCheckMapper::toDomainEntity); } } 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 From 7926b2a7434e33387ff4375403243385b809c3e4 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Fri, 29 Aug 2025 21:33:13 +0900 Subject: [PATCH 14/34] =?UTF-8?q?[refactor]=20record=20=EA=B4=80=EB=A0=A8?= =?UTF-8?q?=20=EC=98=81=EC=86=8D=EC=84=B1=20=EB=A9=94=EC=84=9C=EB=93=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#277)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../persistence/RecordCommandPersistenceAdapter.java | 11 +++++------ .../repository/record/RecordQueryRepositoryImpl.java | 9 ++++----- 2 files changed, 9 insertions(+), 11 deletions(-) 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 015bbf797..0d6f8f029 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 saveRecord(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 saveRecord(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/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; } From 9f4e7618738c77923f15bfc8521072b13c8c3c81 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Fri, 29 Aug 2025 21:49:31 +0900 Subject: [PATCH 15/34] =?UTF-8?q?[refactor]=20vote=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EC=98=81=EC=86=8D=EC=84=B1=20=EB=A9=94=EC=84=9C=EB=93=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#277)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../VoteCommandPersistenceAdapter.java | 15 +++++++-------- .../repository/vote/VoteQueryRepositoryImpl.java | 4 ++-- 2 files changed, 9 insertions(+), 10 deletions(-) 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/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()) // 해당 투표에 참여한 총 참여자 수 기준 내림차순 정렬 From 487a4ed580adc778f7c12481495c6c8bce088bed Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Fri, 29 Aug 2025 23:44:58 +0900 Subject: [PATCH 16/34] =?UTF-8?q?[refactor]=20room=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EC=98=81=EC=86=8D=EC=84=B1=20=EB=A9=94=EC=84=9C=EB=93=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#277)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../RoomCommandPersistenceAdapter.java | 6 ++-- .../repository/RoomJpaRepository.java | 3 +- .../repository/RoomQueryRepositoryImpl.java | 28 ++++++------------- 3 files changed, 12 insertions(+), 25 deletions(-) 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/repository/RoomJpaRepository.java b/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomJpaRepository.java index 6a7c5a6fc..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 @@ -17,8 +17,7 @@ public interface RoomJpaRepository extends JpaRepository, R @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 5a0358747..7a4c0effb 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 @@ -10,7 +10,6 @@ import com.querydsl.jpa.JPAExpressions; import com.querydsl.jpa.impl.JPAQueryFactory; import konkuk.thip.book.adapter.out.jpa.QBookJpaEntity; -import konkuk.thip.common.entity.StatusType; import konkuk.thip.common.util.DateUtil; import konkuk.thip.room.adapter.in.web.response.RoomGetHomeJoinedListResponse; import konkuk.thip.room.adapter.in.web.response.RoomRecruitingDetailViewResponse; @@ -40,8 +39,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(StatusType.ACTIVE)); + where.and(room.startDate.after(today)); return where; } @@ -208,7 +206,6 @@ public Page searchHomeJoinedRooms( // 활동 기간 중인 방만: startDate ≤ today ≤ endDate BooleanBuilder where = new BooleanBuilder(); where.and(participant.userJpaEntity.userId.eq(userId)); - where.and(participant.status.eq(StatusType.ACTIVE)); where.and(room.startDate.loe(date)); where.and(room.endDate.goe(date)); @@ -269,9 +266,7 @@ public List findRecruitingRoomsUserParticipated( ) { LocalDate today = LocalDate.now(); BooleanExpression base = participant.userJpaEntity.userId.eq(userId) - .and(participant.status.eq(StatusType.ACTIVE)) - .and(room.startDate.after(today)) - .and(room.status.eq(StatusType.ACTIVE)); // 유저가 참여한 방 && 모집중인 방 + .and(room.startDate.after(today)); // 유저가 참여한 방 && 모집중인 방 DateExpression cursorExpr = room.startDate; // 커서 비교는 startDate(= 모집 마감일 - 1일) OrderSpecifier[] orders = new OrderSpecifier[]{ cursorExpr.asc(), room.roomId.asc() @@ -287,10 +282,8 @@ public List findPlayingRoomsUserParticipated( ) { LocalDate today = LocalDate.now(); BooleanExpression base = participant.userJpaEntity.userId.eq(userId) - .and(participant.status.eq(StatusType.ACTIVE)) .and(room.startDate.loe(today)) - .and(room.endDate.goe(today)) - .and(room.status.eq(StatusType.ACTIVE)); // 유저가 참여한 방 && 현재 진행중인 방 + .and(room.endDate.goe(today)); // 유저가 참여한 방 && 현재 진행중인 방 DateExpression cursorExpr = room.endDate; // 커서 비교는 endDate(= 진행 마감일) OrderSpecifier[] orders = new OrderSpecifier[]{ cursorExpr.asc(), room.roomId.asc() @@ -308,9 +301,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(StatusType.ACTIVE)) - .and(playing.or(recruiting)) - .and(room.status.eq(StatusType.ACTIVE)); // 유저가 참여한 방 && 현재 진행중인 방 + 모집중인 방 + .and(playing.or(recruiting)); // 유저가 참여한 방 && 현재 진행중인 방 + 모집중인 방 // 진행중: cursor=endDate, 모집중: cursor=startDate DateExpression cursorExpr = new CaseBuilder() @@ -338,9 +329,7 @@ public List findExpiredRoomsUserParticipated( ) { LocalDate today = LocalDate.now(); BooleanExpression base = participant.userJpaEntity.userId.eq(userId) - .and(participant.status.eq(StatusType.ACTIVE)) - .and(room.endDate.before(today)) - .and(room.status.eq(StatusType.ACTIVE)); // 유저가 참여한 방 && 만료된 방 + .and(room.endDate.before(today)); // 유저가 참여한 방 && 만료된 방 DateExpression cursorExpr = room.endDate; OrderSpecifier[] orders = new OrderSpecifier[]{ @@ -392,8 +381,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(StatusType.ACTIVE)); + .and(room.startDate.after(LocalDate.now())); // 모집 마감 시각 > 현재 시각 + if (dateCursor != null && roomIdCursor != null) { // 첫 페이지가 아닌 경우 baseCondition = baseCondition.and(cursorExpr.gt(dateCursor) @@ -422,8 +411,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(StatusType.ACTIVE)); + .and(userJoinedRoom(userId).not()); // 유저가 참여하지 않은 방만 조회 } /** From 60be983d5fe5939bd60e38ddfd933f98b402c8c0 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Fri, 29 Aug 2025 23:46:07 +0900 Subject: [PATCH 17/34] =?UTF-8?q?[refactor]=20roomParticipant=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EC=98=81=EC=86=8D=EC=84=B1=20=EB=A9=94=EC=84=9C?= =?UTF-8?q?=EB=93=9C=20=EC=88=98=EC=A0=95=20(#277)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../RoomParticipantCommandPersistenceAdapter.java | 6 +++--- .../roomparticipant/RoomParticipantJpaRepository.java | 9 +++------ 2 files changed, 6 insertions(+), 9 deletions(-) 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/roomparticipant/RoomParticipantJpaRepository.java b/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/roomparticipant/RoomParticipantJpaRepository.java index ea3760527..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 @@ -17,20 +17,17 @@ public interface RoomParticipantJpaRepository extends JpaRepository 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); } From f35b1e1008096c93472bb0504c80c9acdde2f57c Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Sat, 30 Aug 2025 00:19:14 +0900 Subject: [PATCH 18/34] =?UTF-8?q?[refactor]=20feed=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EC=98=81=EC=86=8D=EC=84=B1=20=EB=A9=94=EC=84=9C=EB=93=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#277)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FeedCommandPersistenceAdapter.java | 24 +++++-------------- .../FeedQueryPersistenceAdapter.java | 4 ++-- .../repository/FeedJpaRepository.java | 9 ++++--- .../repository/FeedQueryRepositoryImpl.java | 16 ++++--------- 4 files changed, 16 insertions(+), 37 deletions(-) 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 560170d35..13b24eed3 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 7e5be6824..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; @@ -15,9 +14,9 @@ public interface FeedJpaRepository extends JpaRepository, F */ Optional findByPostId(Long postId); - @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); + @Query("SELECT COUNT(f) FROM FeedJpaEntity f WHERE f.userJpaEntity.userId = :userId") + long countAllFeedsByUserId(@Param("userId") Long userId); - @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 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)) From 94158ca7ecf994ded11fbd9966a363194d717041 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Sat, 30 Aug 2025 00:39:42 +0900 Subject: [PATCH 19/34] =?UTF-8?q?[refactor]=20=EB=8C=93=EA=B8=80=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=EC=97=90=20status=20filter=20=EB=AF=B8=EC=A0=81?= =?UTF-8?q?=EC=9A=A9=20=EB=AA=85=EC=8B=9C=20(#277)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/comment/application/service/CommentShowAllService.java | 2 ++ 1 file changed, 2 insertions(+) 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 fe0b59017..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,6 +7,7 @@ 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; @@ -27,6 +28,7 @@ public class CommentShowAllService implements CommentShowAllUseCase { @Override @Transactional(readOnly = true) + @Unfiltered public CommentForSinglePostResponse showAllCommentsOfPost(CommentShowAllQuery query) { Cursor cursor = Cursor.from(query.cursorStr(), PAGE_SIZE); From 47947a81e3bf3a1940f6ea0358fab3e5a8dd2e8a Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Sat, 30 Aug 2025 00:40:11 +0900 Subject: [PATCH 20/34] =?UTF-8?q?[refactor]=20comment=20=EA=B4=80=EB=A0=A8?= =?UTF-8?q?=20=EC=98=81=EC=86=8D=EC=84=B1=20=EC=BD=94=EB=93=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(#277)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CommentCommandPersistenceAdapter.java | 17 ++++++------ .../CommentLikeCommandPersistenceAdapter.java | 2 +- .../CommentQueryRepositoryImpl.java | 26 +++++++++++++------ 3 files changed, 27 insertions(+), 18 deletions(-) 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/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(); From 08bcd8b6ffc3ae9747f8b30d446796cf26096f90 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Sat, 30 Aug 2025 00:43:32 +0900 Subject: [PATCH 21/34] =?UTF-8?q?[refactor]=20book=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EC=98=81=EC=86=8D=EC=84=B1=20=EC=BD=94=EB=93=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(#277)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../out/persistence/BookCommandPersistenceAdapter.java | 4 ++-- .../adapter/out/persistence/BookQueryPersistenceAdapter.java | 4 ++-- .../adapter/out/persistence/repository/BookJpaRepository.java | 3 +-- 3 files changed, 5 insertions(+), 6 deletions(-) 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 aef360221..99829efb4 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 @@ -61,7 +61,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); @@ -70,7 +70,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 1e77dc93a..b7e20740a 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 @@ -37,7 +37,7 @@ public boolean existsBookByIsbn(String isbn) { @Override public List findSavedBooksByUserId(Long userId) { - UserJpaEntity user = userJpaRepository.findById(userId) + UserJpaEntity user = userJpaRepository.findByUserId(userId) .orElseThrow(() -> new EntityNotFoundException(USER_NOT_FOUND)); return bookJpaRepository.findSavedBooksByUserId(user.getUserId()).stream() @@ -47,7 +47,7 @@ public List findSavedBooksByUserId(Long userId) { @Override public List findJoiningRoomsBooksByUserId(Long userId) { - UserJpaEntity user = userJpaRepository.findById(userId) + UserJpaEntity user = userJpaRepository.findByUserId(userId) .orElseThrow(() -> new EntityNotFoundException(USER_NOT_FOUND)); return bookJpaRepository.findJoiningRoomsBooksByUserId(user.getUserId()) 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 3ea27d835..d09c98c3c 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 @@ -21,8 +21,7 @@ public interface BookJpaRepository extends JpaRepository { "JOIN RoomJpaEntity r ON r.bookJpaEntity.bookId = b.bookId " + "JOIN RoomParticipantJpaEntity rp ON rp.roomJpaEntity.roomId = r.roomId " + "WHERE rp.userJpaEntity.userId = :userId " + - " AND r.status = 'ACTIVE' " + - " AND r.startDate <= CURRENT_TIMESTAMP " + // 진행 중인 방만 조회 (모집 중 / 만료된 방 x) + "AND r.startDate <= CURRENT_TIMESTAMP " + // 진행 중인 방만 조회 (모집 중 / 만료된 방 x) "GROUP BY b " + "ORDER BY MAX(r.roomPercentage) DESC") // 방의 진행률이 높은 순서로 정렬 List findJoiningRoomsBooksByUserId(Long userId); From 622cf9a8118cb0f506f95928aad7cd045e36d3fb Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Sat, 30 Aug 2025 00:46:58 +0900 Subject: [PATCH 22/34] =?UTF-8?q?[refactor]=20vote=20=EC=82=AD=EC=A0=9C=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=88=98=EC=A0=95=20(#277)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/roompost/adapter/in/web/VoteDeleteApiTest.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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); From ea0f36be14b916fdab7c5d605900b403c8b1ed9b Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Sat, 30 Aug 2025 00:47:40 +0900 Subject: [PATCH 23/34] =?UTF-8?q?[refactor]=20room=20participant=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20=EA=B4=80=EB=A0=A8=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95=20(#277)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../konkuk/thip/room/adapter/in/web/RoomJoinApiTest.java | 9 +++++---- .../adapter/in/web/RoomParticipantDeleteApiTest.java | 8 ++++++-- 2 files changed, 11 insertions(+), 6 deletions(-) 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(); From 7a872b486a6de46f7f96e138ecddfd50f211240e Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Sat, 30 Aug 2025 00:47:52 +0900 Subject: [PATCH 24/34] =?UTF-8?q?[refactor]=20record=20=EC=82=AD=EC=A0=9C?= =?UTF-8?q?=20=EA=B4=80=EB=A0=A8=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=88=98=EC=A0=95=20(#277)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/roompost/adapter/in/web/RecordDeleteApiTest.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) 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); From 5fc2dcf0152918664cf4255924015d8f597fbbe6 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Sat, 30 Aug 2025 00:47:58 +0900 Subject: [PATCH 25/34] =?UTF-8?q?[refactor]=20feed=20=EC=82=AD=EC=A0=9C=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=88=98=EC=A0=95=20(#277)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../konkuk/thip/feed/adapter/in/web/FeedDeleteApiTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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(); From 65c387e428479bc1ee725300c73dcaf09e67c7cc Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Sat, 30 Aug 2025 00:48:04 +0900 Subject: [PATCH 26/34] =?UTF-8?q?[refactor]=20comment=20=EC=82=AD=EC=A0=9C?= =?UTF-8?q?=20=EA=B4=80=EB=A0=A8=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=88=98=EC=A0=95=20(#277)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../comment/adapter/in/web/CommentDeleteApiTest.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) 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(); From 42ca4517a659e64e54828c95d93fc531b0d22c2f Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Sat, 30 Aug 2025 00:51:29 +0900 Subject: [PATCH 27/34] =?UTF-8?q?[fix]=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20tear=20down=20=EB=A9=94=EC=84=9C=EB=93=9C?= =?UTF-8?q?=20=EC=88=98=EC=A0=95=20(#277)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - soft delete 적용 대상인 entity 삭제 시 deleteAllInBatch 메서드 호출하도록 수정 --- .../book/adapter/in/web/BookDetailSearchApiTest.java | 11 +++++------ .../adapter/in/web/BookMostSearchedBooksApiTest.java | 2 +- .../thip/book/adapter/in/web/BookSearchApiTest.java | 4 ++-- 3 files changed, 8 insertions(+), 9 deletions(-) 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/BookMostSearchedBooksApiTest.java b/src/test/java/konkuk/thip/book/adapter/in/web/BookMostSearchedBooksApiTest.java index b332bfd5a..13e416567 100644 --- a/src/test/java/konkuk/thip/book/adapter/in/web/BookMostSearchedBooksApiTest.java +++ b/src/test/java/konkuk/thip/book/adapter/in/web/BookMostSearchedBooksApiTest.java @@ -74,7 +74,7 @@ void setUp() { @AfterEach void tearDown() { - userJpaRepository.deleteAll(); + 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 From 11425df4a030d6e3658e2738281b41be70d4e811 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Sat, 30 Aug 2025 00:51:43 +0900 Subject: [PATCH 28/34] =?UTF-8?q?[refactor]=20custom=20=EC=96=B4=EB=85=B8?= =?UTF-8?q?=ED=85=8C=EC=9D=B4=EC=85=98=20=ED=8C=A8=ED=82=A4=EC=A7=80=20?= =?UTF-8?q?=EC=9D=B4=EB=8F=99=EC=9C=BC=EB=A1=9C=20=EC=9D=B8=ED=95=9C=20imp?= =?UTF-8?q?ort=20=EB=AC=B8=20=EC=88=98=EC=A0=95=20(#277)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/post/application/service/handler/PostHandler.java | 2 +- .../service/validator/PostLikeAuthorizationValidator.java | 2 +- .../application/service/manager/RecentSearchCreateManager.java | 2 +- .../application/service/validator/RoomParticipantValidator.java | 2 +- .../application/service/manager/RoomProgressManager.java | 2 +- .../application/service/validator/RoomPostAccessValidator.java | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) 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/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/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/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; From 04bc4c5f9a67ac52621b0b8dd25777102c527b25 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Mon, 1 Sep 2025 02:51:09 +0900 Subject: [PATCH 29/34] =?UTF-8?q?[move]=20status=20filter=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=ED=8C=A8=ED=82=A4=EC=A7=80=20=EC=9D=B4=EB=8F=99=20(#277)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/common/{util => persistence}/StatusFilterTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) rename src/test/java/konkuk/thip/common/{util => persistence}/StatusFilterTest.java (99%) diff --git a/src/test/java/konkuk/thip/common/util/StatusFilterTest.java b/src/test/java/konkuk/thip/common/persistence/StatusFilterTest.java similarity index 99% rename from src/test/java/konkuk/thip/common/util/StatusFilterTest.java rename to src/test/java/konkuk/thip/common/persistence/StatusFilterTest.java index 68da16b35..46e4a4741 100644 --- a/src/test/java/konkuk/thip/common/util/StatusFilterTest.java +++ b/src/test/java/konkuk/thip/common/persistence/StatusFilterTest.java @@ -1,8 +1,9 @@ -package konkuk.thip.common.util; +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; From b9dd16f6e8db8631e61295818b89f9e405e4daf0 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Mon, 1 Sep 2025 02:51:38 +0900 Subject: [PATCH 30/34] =?UTF-8?q?[test]=20jpa=20repository=20=EB=A9=94?= =?UTF-8?q?=EC=84=9C=EB=93=9C=20=EB=82=B4=EB=B6=80=20=EB=8F=99=EC=9E=91=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=EC=9D=84=20=EC=9C=84=ED=95=9C=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1=20?= =?UTF-8?q?(#277)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../persistence/JpaRepositoryMethodTest.java | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 src/test/java/konkuk/thip/common/persistence/JpaRepositoryMethodTest.java 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(); + } +} From 2e22e6d326a1e1eb5967d46c532806c97f95efb3 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Mon, 1 Sep 2025 02:59:10 +0900 Subject: [PATCH 31/34] =?UTF-8?q?[docs]=20=EC=9E=98=EB=AA=BB=EB=90=9C=20?= =?UTF-8?q?=EC=A3=BC=EC=84=9D=20=EC=82=AD=EC=A0=9C=20(#277)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - filter는 스프링 빈이 트랜잭션을 시작할때 AOP가 이를 가로채서 세션을 획득함으로써 활성화 된다 - 하지만 스프링 테스트 메서드는 스프링 빈이 아니므로, TestConfig.TestService를 스프링 빈으로 만듦으로써 filter 활성화 테스트를 진행하였다 - 삭제된 주석 내용처럼 테스트 메서드에 트랜잭션을 붙여서 filter가 활성화 된 것이 아니라, 스프링 빈으로 등록한 TestService의 메서드를 호출함으로써 filter가 활성화 된 것 이다 --- .../java/konkuk/thip/common/persistence/StatusFilterTest.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/test/java/konkuk/thip/common/persistence/StatusFilterTest.java b/src/test/java/konkuk/thip/common/persistence/StatusFilterTest.java index 46e4a4741..238491f34 100644 --- a/src/test/java/konkuk/thip/common/persistence/StatusFilterTest.java +++ b/src/test/java/konkuk/thip/common/persistence/StatusFilterTest.java @@ -65,7 +65,6 @@ private void saveInactiveUser(int count) { @Test @DisplayName("spring data jpa의 기본 findById 메서드는 PK를 기준으로만 조회하므로 status 필터링이 적용되지 않는다.") - @Transactional // filter를 활성화 하기 위한 트랜잭션 어노테이션 void default_find_by_id_method_does_not_execute_filtering() throws Exception { //given UserJpaEntity activeUser = userJpaRepository.save(TestEntityFactory.createUser(Alias.WRITER, "activeUser")); @@ -86,7 +85,6 @@ void default_find_by_id_method_does_not_execute_filtering() throws Exception { @Test @DisplayName("jpa repository에 정의한 custom 메서드는 status 필터링이 적용된다.") - @Transactional // filter를 활성화 하기 위한 트랜잭션 어노테이션 void custom_find_active_by_id_method_does_execute_filtering() throws Exception { //given UserJpaEntity activeUser = userJpaRepository.save(TestEntityFactory.createUser(Alias.WRITER, "activeUser")); From 16756f2842b119f8efebbcbd4061647d108804e1 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Mon, 1 Sep 2025 17:36:13 +0900 Subject: [PATCH 32/34] =?UTF-8?q?[refactor]=20=ED=95=84=EC=88=98=20?= =?UTF-8?q?=EC=97=B0=EA=B4=80=EA=B4=80=EA=B3=84=EC=9D=B8=20jpa=20entity=20?= =?UTF-8?q?=EB=93=A4=EC=9D=98=20optional=20=EC=A0=9C=EC=95=BD=EC=A1=B0?= =?UTF-8?q?=EA=B1=B4=20=EC=B6=94=EA=B0=80=20(#277)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/book/adapter/out/jpa/SavedBookJpaEntity.java | 5 +++-- .../thip/comment/adapter/out/jpa/CommentJpaEntity.java | 7 +++---- .../thip/comment/adapter/out/jpa/CommentLikeJpaEntity.java | 4 ++-- .../konkuk/thip/feed/adapter/out/jpa/FeedJpaEntity.java | 1 - .../thip/feed/adapter/out/jpa/SavedFeedJpaEntity.java | 4 ++-- .../adapter/out/jpa/NotificationJpaEntity.java | 4 ++-- .../konkuk/thip/post/adapter/out/jpa/PostJpaEntity.java | 2 +- .../thip/post/adapter/out/jpa/PostLikeJpaEntity.java | 4 ++-- .../adapter/out/jpa/RecentSearchJpaEntity.java | 2 +- .../konkuk/thip/room/adapter/out/jpa/RoomJpaEntity.java | 2 +- .../room/adapter/out/jpa/RoomParticipantJpaEntity.java | 6 +++--- .../roompost/adapter/out/jpa/AttendanceCheckJpaEntity.java | 4 ++-- .../thip/roompost/adapter/out/jpa/VoteItemJpaEntity.java | 2 +- .../roompost/adapter/out/jpa/VoteParticipantJpaEntity.java | 4 ++-- .../thip/user/adapter/out/jpa/FollowingJpaEntity.java | 4 ++-- 15 files changed, 27 insertions(+), 28 deletions(-) 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 31f8116f0..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) + @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 = "book_id", nullable = false) private BookJpaEntity bookJpaEntity; } \ No newline at end of file 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 84cb882a6..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,7 +47,7 @@ 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; @@ -74,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 5efe4216b..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) + @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 = "comment_id", nullable = false) private CommentJpaEntity commentJpaEntity; } \ No newline at end of file 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 00c3da74b..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) 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 47256889e..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) + @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 = "post_id", nullable = false) private FeedJpaEntity feedJpaEntity; } \ No newline at end of file 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 5cadd1506..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) + @ManyToOne(fetch = FetchType.LAZY, optional = false) @JoinColumn(name = "post_id", nullable = false) private PostJpaEntity postJpaEntity; - @ManyToOne(fetch = FetchType.LAZY) + @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/adapter/out/jpa/RecentSearchJpaEntity.java b/src/main/java/konkuk/thip/recentSearch/adapter/out/jpa/RecentSearchJpaEntity.java index 24fe2bfcd..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) + @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/room/adapter/out/jpa/RoomJpaEntity.java b/src/main/java/konkuk/thip/room/adapter/out/jpa/RoomJpaEntity.java index ce51508d9..2ba563d55 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 @@ -50,7 +50,7 @@ public class RoomJpaEntity extends BaseJpaEntity { @Column(name = "member_count",nullable = false) private int memberCount = 1; - @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 e0a6d714b..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,14 +31,14 @@ 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) + @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 = "room_id", nullable = false) private RoomJpaEntity roomJpaEntity; 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 f9348d908..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,11 +24,11 @@ public class AttendanceCheckJpaEntity extends BaseJpaEntity { @Column(name = "today_comment",length = 100, nullable = false) private String todayComment; - @ManyToOne(fetch = FetchType.LAZY) + @ManyToOne(fetch = FetchType.LAZY, optional = false) @JoinColumn(name = "room_id", nullable = false) private RoomJpaEntity roomJpaEntity; - @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/roompost/adapter/out/jpa/VoteItemJpaEntity.java b/src/main/java/konkuk/thip/roompost/adapter/out/jpa/VoteItemJpaEntity.java index 40609fa58..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,7 +25,7 @@ public class VoteItemJpaEntity extends BaseJpaEntity { @Column(nullable = false) private int count = 0; - @ManyToOne(fetch = FetchType.LAZY) + @ManyToOne(fetch = FetchType.LAZY, optional = false) @JoinColumn(name = "post_id", nullable = false) private VoteJpaEntity voteJpaEntity; 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 55ed5bef1..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 @@ -18,11 +18,11 @@ public class VoteParticipantJpaEntity extends BaseJpaEntity { @Column(name = "vote_participant_id") private Long voteParticipantId; - @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 = "vote_item_id", nullable = false) private VoteItemJpaEntity voteItemJpaEntity; 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; // 팔로우 당하는 유저 } From 6cd777f175b53aea7c3253ee90d562b739990c56 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Mon, 1 Sep 2025 19:04:45 +0900 Subject: [PATCH 33/34] =?UTF-8?q?[feat]=20status=20filter=20aop=EC=97=90?= =?UTF-8?q?=20=ED=8A=B8=EB=9E=9C=EC=9E=AD=EC=85=98=20=EA=B0=80=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(#277)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - hibernate session 획득 시 트랜잭션 활성 여부 가드 추가 - @IncludeInactive, @Unfiltered 포인트컷을 트랜잭션 범위 내에서만 동작하도록 제한 --- .../thip/common/aop/StatusFilterAspect.java | 36 ++++++++++++++----- .../thip/common/exception/code/ErrorCode.java | 2 ++ 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/src/main/java/konkuk/thip/common/aop/StatusFilterAspect.java b/src/main/java/konkuk/thip/common/aop/StatusFilterAspect.java index 7950ea644..916165723 100644 --- a/src/main/java/konkuk/thip/common/aop/StatusFilterAspect.java +++ b/src/main/java/konkuk/thip/common/aop/StatusFilterAspect.java @@ -2,6 +2,7 @@ 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; @@ -9,9 +10,12 @@ 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 @@ -19,6 +23,18 @@ 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 세션 얻기 } @@ -38,15 +54,18 @@ private Session session() { " && !" + "@annotation(" + ANN_INCLUDE_INACTIVE + ")" + " && !" + "@annotation(" + ANN_UNFILTERED + ")"; + // @IncludeInactive: 트랜잭션 컨텍스트가 보장된 경우에만 동작 private static final String PCUT_INCLUDE_INACTIVE = - "@annotation(" + ANN_INCLUDE_INACTIVE + ")"; + "@annotation(" + ANN_INCLUDE_INACTIVE + ") && (" + "@annotation(" + ANN_TX + ") || @within(" + ANN_TX + ")" + ")"; + + // @Unfiltered: 트랜잭션 컨텍스트가 보장된 경우에만 동작 private static final String PCUT_UNFILTERED = - "@annotation(" + ANN_UNFILTERED + ")"; + "@annotation(" + ANN_UNFILTERED + ") && (" + "@annotation(" + ANN_TX + ") || @within(" + ANN_TX + ")" + ")"; - // 기본: ACTIVE만 (트랜잭션 경계 진입 시) + // 기본: ACTIVE만 @Around(PCUT_TX_DEFAULT) public Object enableActiveByDefault(ProceedingJoinPoint pjp) throws Throwable { - var s = session(); + var s = currentTxSession(); var wasEnabled = isFilterEnabled(s); if (!wasEnabled) { enableFilterWith(s, List.of(StatusType.ACTIVE.name())); @@ -60,10 +79,10 @@ public Object enableActiveByDefault(ProceedingJoinPoint pjp) throws Throwable { } } - // Include Inactive: ACTIVE, INACTIVE 모두 + // Include Inactive: ACTIVE, INACTIVE 모두 + 종료 시 active-only 로 복귀 @Around(PCUT_INCLUDE_INACTIVE) public Object includeInactive(ProceedingJoinPoint pjp) throws Throwable { - var s = session(); + var s = currentTxSession(); var prevEnabled = isFilterEnabled(s); enableFilterWith(s, List.of(StatusType.ACTIVE.name(), StatusType.INACTIVE.name())); @@ -78,14 +97,13 @@ public Object includeInactive(ProceedingJoinPoint pjp) throws Throwable { } } - // Unfiltered: 필터 해제 + // Unfiltered: 필터 해제 + 종료 시 active-only 로 복귀 @Around(PCUT_UNFILTERED) public Object unfiltered(ProceedingJoinPoint pjp) throws Throwable { - var s = session(); + var s = currentTxSession(); var wasEnabled = isFilterEnabled(s); if (wasEnabled) { disableFilter(s); - log.debug("statusFilter -> DISABLED (temporarily by @Unfiltered)"); } try { return pjp.proceed(); 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 From 8c20e2909dd6dcbe8e1461ed909930eff1ee01b9 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Mon, 1 Sep 2025 20:38:38 +0900 Subject: [PATCH 34/34] =?UTF-8?q?[feat]=20=EB=8B=89=EB=84=A4=EC=9E=84=20?= =?UTF-8?q?=EC=A4=91=EB=B3=B5=20=EA=B2=80=EC=A6=9D=20api=20service=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=EC=97=90=20@Unfiltered=20=EC=96=B4?= =?UTF-8?q?=EB=85=B8=ED=85=8C=EC=9D=B4=EC=85=98=20=EC=B6=94=EA=B0=80=20(#2?= =?UTF-8?q?77)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - soft delete 된 유저를 포함해서 닉네임 중복 검증 수행하도록 filter 비활성화 --- .../repository/UserJpaRepository.java | 1 + .../service/UserVerifyNicknameService.java | 2 ++ .../web/UserVerifyNicknameControllerTest.java | 35 +++++++++++++++++-- 3 files changed, 35 insertions(+), 3 deletions(-) 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 288427d27..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 @@ -13,6 +13,7 @@ public interface UserJpaRepository extends JpaRepository, U Optional findByUserId(Long userId); Optional findByOauth2Id(String oauth2Id); + boolean existsByNickname(String nickname); boolean existsByNicknameAndUserIdNot(String nickname, Long 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 1055fe298..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,5 +1,6 @@ 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; @@ -14,6 +15,7 @@ public class UserVerifyNicknameService implements UserVerifyNicknameUseCase { @Override @Transactional(readOnly = true) + @Unfiltered // soft delete 된 유저의 닉네임을 포함해서 중복 검증 public boolean isNicknameUnique(String nickname) { return !userQueryPort.existsByNickname(nickname); } 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 + } }