From 7ba65bc8751f3c44a5519c25e0317a39cef1e833 Mon Sep 17 00:00:00 2001 From: "Choi, Minwoo" Date: Fri, 17 Oct 2025 12:45:21 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20Spring=20Batch=20=EA=B8=B0=EB=B0=98=20T?= =?UTF-8?q?ripReport=20=EC=97=B0=EA=B4=80=20=EC=97=94=ED=8B=B0=ED=8B=B0=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20=EB=B0=8F=20=ED=95=98=EB=93=9C=20=EB=94=9C?= =?UTF-8?q?=EB=A6=AC=ED=8A=B8=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80(#9?= =?UTF-8?q?2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: TripReportQueryRepository, TripReportQueryRepositoryAdapter 구현 * feat: TripReportStudyLogQueryRepository, TripReportStudyLogQueryRepositoryAdapter 구현 * feat: TripReportCommandService, TripReportStudyLogCommandService에 하드 딜리트 비즈니스 로직 추가 * feat: HardDeleteFacade에 TripReport, TripReportStudyLog에 하드 딜리트 비즈니스 로직 추가 * test: TripReportCommandServiceTest에 HardDeleteTripReportsOwnedByDeletedMember 단위 테스트 추가 * test: TripReportStudyLogCommandServiceTest에 HardDeleteTripReportStudyLogsOwnedByDeletedMember 단위 테스트 추가 --- .../application/facade/HardDeleteFacade.java | 27 ++++++++++ .../service/TripReportCommandService.java | 6 +++ .../TripReportStudyLogCommandService.java | 6 +++ .../repository/TripReportQueryRepository.java | 5 ++ .../TripReportStudyLogQueryRepository.java | 5 ++ .../TripReportQueryRepositoryAdapter.java | 29 ++++++++++ ...pReportStudyLogQueryRepositoryAdapter.java | 43 +++++++++++++++ .../service/TripReportCommandServiceTest.java | 33 ++++++++++++ .../TripReportStudyLogCommandServiceTest.java | 54 +++++++++++++++++++ 9 files changed, 208 insertions(+) create mode 100644 src/main/java/com/ject/studytrip/trip/domain/repository/TripReportQueryRepository.java create mode 100644 src/main/java/com/ject/studytrip/trip/domain/repository/TripReportStudyLogQueryRepository.java create mode 100644 src/main/java/com/ject/studytrip/trip/infra/querydsl/TripReportQueryRepositoryAdapter.java create mode 100644 src/main/java/com/ject/studytrip/trip/infra/querydsl/TripReportStudyLogQueryRepositoryAdapter.java diff --git a/src/main/java/com/ject/studytrip/cleanup/application/facade/HardDeleteFacade.java b/src/main/java/com/ject/studytrip/cleanup/application/facade/HardDeleteFacade.java index da2d0f6..bc197f3 100644 --- a/src/main/java/com/ject/studytrip/cleanup/application/facade/HardDeleteFacade.java +++ b/src/main/java/com/ject/studytrip/cleanup/application/facade/HardDeleteFacade.java @@ -10,6 +10,8 @@ import com.ject.studytrip.studylog.application.service.StudyLogDailyMissionCommandService; import com.ject.studytrip.trip.application.service.DailyGoalCommandService; import com.ject.studytrip.trip.application.service.TripCommandService; +import com.ject.studytrip.trip.application.service.TripReportCommandService; +import com.ject.studytrip.trip.application.service.TripReportStudyLogCommandService; import java.util.LinkedHashMap; import java.util.Map; import lombok.RequiredArgsConstructor; @@ -29,6 +31,8 @@ public class HardDeleteFacade { private final StudyLogDailyMissionCommandService studyLogDailyMissionCommandService; private final DailyGoalCommandService dailyGoalCommandService; private final PomodoroCommandService pomodoroCommandService; + private final TripReportCommandService tripReportCommandService; + private final TripReportStudyLogCommandService tripReportStudyLogCommandService; private final HardDeleteExecutor executor; @@ -46,6 +50,10 @@ public class HardDeleteFacade { "dailyMissionsOwnedByDeletedMission"; private static final String DAILY_MISSIONS_OWNED_BY_DELETED_DAILY_GOAL = "dailyMissionsOwnedByDeletedDailyGoal"; + private static final String TRIP_REPORT_STUDY_LOGS_OWNED_BY_DELETED_MEMBER = + "tripReportStudyLogsOwnedByDeletedMember"; + private static final String TRIP_REPORTS_OWNED_BY_DELETED_MEMBER = + "tripReportsOwnedByDeletedMember"; private static final String POMODOROS = "pomodoros"; private static final String STUDY_LOG_DAILY_MISSIONS = "studyLogDailyMissions"; @@ -68,6 +76,8 @@ public void hardDeleteAll() { deletePomodoros(phases); // 뽀모도로 삭제 deleteStudyLogDailyMissions(phases); // StudyLogDailyMission 삭제 deleteDailyMissions(phases); // 데일리 미션 삭제 + deleteTripReportStudyLogs(phases); // TripReportStudyLog 삭제 + deleteTripReports(phases); // 여행 리포트 삭제 deleteStudyLogs(phases); // 학습 로그 삭제 deleteDailyGoals(phases); // 데일리 목표 삭제 deleteMissions(phases); // 미션 삭제 @@ -122,6 +132,23 @@ private void deleteDailyMissions(Map phases) { executor.run(DAILY_MISSIONS, dailyMissionCommandService::hardDeleteDailyMissions)); } + private void deleteTripReportStudyLogs(Map phases) { + phases.put( + TRIP_REPORT_STUDY_LOGS_OWNED_BY_DELETED_MEMBER, + executor.run( + TRIP_REPORT_STUDY_LOGS_OWNED_BY_DELETED_MEMBER, + tripReportStudyLogCommandService + ::hardDeleteTripReportStudyLogsOwnedByDeletedMember)); + } + + private void deleteTripReports(Map phases) { + phases.put( + TRIP_REPORTS_OWNED_BY_DELETED_MEMBER, + executor.run( + TRIP_REPORTS_OWNED_BY_DELETED_MEMBER, + tripReportCommandService::hardDeleteTripReportsOwnedByDeletedMember)); + } + private void deleteStudyLogs(Map phases) { phases.put( STUDY_LOGS_OWNED_BY_DELETED_MEMBER, diff --git a/src/main/java/com/ject/studytrip/trip/application/service/TripReportCommandService.java b/src/main/java/com/ject/studytrip/trip/application/service/TripReportCommandService.java index 48f4869..973da01 100644 --- a/src/main/java/com/ject/studytrip/trip/application/service/TripReportCommandService.java +++ b/src/main/java/com/ject/studytrip/trip/application/service/TripReportCommandService.java @@ -3,6 +3,7 @@ import com.ject.studytrip.member.domain.model.Member; import com.ject.studytrip.trip.domain.factory.TripReportFactory; import com.ject.studytrip.trip.domain.model.TripReport; +import com.ject.studytrip.trip.domain.repository.TripReportQueryRepository; import com.ject.studytrip.trip.domain.repository.TripReportRepository; import com.ject.studytrip.trip.presentation.dto.request.CreateTripReportRequest; import lombok.RequiredArgsConstructor; @@ -12,6 +13,7 @@ @RequiredArgsConstructor public class TripReportCommandService { private final TripReportRepository tripReportRepository; + private final TripReportQueryRepository tripReportQueryRepository; public TripReport createTripReport(Member member, CreateTripReportRequest request) { TripReport tripReport = @@ -32,4 +34,8 @@ public TripReport createTripReport(Member member, CreateTripReportRequest reques public void updateImageUrl(TripReport tripReport, String imageUrl) { tripReport.updateImageUrl(imageUrl); } + + public long hardDeleteTripReportsOwnedByDeletedMember() { + return tripReportQueryRepository.deleteAllByDeletedMemberOwner(); + } } diff --git a/src/main/java/com/ject/studytrip/trip/application/service/TripReportStudyLogCommandService.java b/src/main/java/com/ject/studytrip/trip/application/service/TripReportStudyLogCommandService.java index d12ec8f..5689c4c 100644 --- a/src/main/java/com/ject/studytrip/trip/application/service/TripReportStudyLogCommandService.java +++ b/src/main/java/com/ject/studytrip/trip/application/service/TripReportStudyLogCommandService.java @@ -4,6 +4,7 @@ import com.ject.studytrip.trip.domain.factory.TripReportStudyLogFactory; import com.ject.studytrip.trip.domain.model.TripReport; import com.ject.studytrip.trip.domain.model.TripReportStudyLog; +import com.ject.studytrip.trip.domain.repository.TripReportStudyLogQueryRepository; import com.ject.studytrip.trip.domain.repository.TripReportStudyLogRepository; import java.util.List; import lombok.RequiredArgsConstructor; @@ -13,6 +14,7 @@ @RequiredArgsConstructor public class TripReportStudyLogCommandService { private final TripReportStudyLogRepository tripReportStudyLogRepository; + private final TripReportStudyLogQueryRepository tripReportStudyLogQueryRepository; public void createTripReportStudyLogs(TripReport tripReport, List studyLogs) { List tripReportStudyLogs = @@ -22,4 +24,8 @@ public void createTripReportStudyLogs(TripReport tripReport, List stud tripReportStudyLogRepository.saveAll(tripReportStudyLogs); } + + public long hardDeleteTripReportStudyLogsOwnedByDeletedMember() { + return tripReportStudyLogQueryRepository.deleteAllByDeletedMemberOwner(); + } } diff --git a/src/main/java/com/ject/studytrip/trip/domain/repository/TripReportQueryRepository.java b/src/main/java/com/ject/studytrip/trip/domain/repository/TripReportQueryRepository.java new file mode 100644 index 0000000..820d62b --- /dev/null +++ b/src/main/java/com/ject/studytrip/trip/domain/repository/TripReportQueryRepository.java @@ -0,0 +1,5 @@ +package com.ject.studytrip.trip.domain.repository; + +public interface TripReportQueryRepository { + long deleteAllByDeletedMemberOwner(); +} diff --git a/src/main/java/com/ject/studytrip/trip/domain/repository/TripReportStudyLogQueryRepository.java b/src/main/java/com/ject/studytrip/trip/domain/repository/TripReportStudyLogQueryRepository.java new file mode 100644 index 0000000..26d597e --- /dev/null +++ b/src/main/java/com/ject/studytrip/trip/domain/repository/TripReportStudyLogQueryRepository.java @@ -0,0 +1,5 @@ +package com.ject.studytrip.trip.domain.repository; + +public interface TripReportStudyLogQueryRepository { + long deleteAllByDeletedMemberOwner(); +} diff --git a/src/main/java/com/ject/studytrip/trip/infra/querydsl/TripReportQueryRepositoryAdapter.java b/src/main/java/com/ject/studytrip/trip/infra/querydsl/TripReportQueryRepositoryAdapter.java new file mode 100644 index 0000000..056ed12 --- /dev/null +++ b/src/main/java/com/ject/studytrip/trip/infra/querydsl/TripReportQueryRepositoryAdapter.java @@ -0,0 +1,29 @@ +package com.ject.studytrip.trip.infra.querydsl; + +import com.ject.studytrip.member.domain.model.QMember; +import com.ject.studytrip.trip.domain.model.QTripReport; +import com.ject.studytrip.trip.domain.repository.TripReportQueryRepository; +import com.querydsl.jpa.JPAExpressions; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class TripReportQueryRepositoryAdapter implements TripReportQueryRepository { + private final JPAQueryFactory queryFactory; + private final QTripReport tripReport = QTripReport.tripReport; + private final QMember member = QMember.member; + + @Override + public long deleteAllByDeletedMemberOwner() { + return queryFactory + .delete(tripReport) + .where( + tripReport.member.id.in( + JPAExpressions.select(member.id) + .from(member) + .where(member.deletedAt.isNotNull()))) + .execute(); + } +} diff --git a/src/main/java/com/ject/studytrip/trip/infra/querydsl/TripReportStudyLogQueryRepositoryAdapter.java b/src/main/java/com/ject/studytrip/trip/infra/querydsl/TripReportStudyLogQueryRepositoryAdapter.java new file mode 100644 index 0000000..118bf41 --- /dev/null +++ b/src/main/java/com/ject/studytrip/trip/infra/querydsl/TripReportStudyLogQueryRepositoryAdapter.java @@ -0,0 +1,43 @@ +package com.ject.studytrip.trip.infra.querydsl; + +import com.ject.studytrip.member.domain.model.QMember; +import com.ject.studytrip.studylog.domain.model.QStudyLog; +import com.ject.studytrip.trip.domain.model.QTripReport; +import com.ject.studytrip.trip.domain.model.QTripReportStudyLog; +import com.ject.studytrip.trip.domain.repository.TripReportStudyLogQueryRepository; +import com.querydsl.jpa.JPAExpressions; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class TripReportStudyLogQueryRepositoryAdapter implements TripReportStudyLogQueryRepository { + private final JPAQueryFactory queryFactory; + private final QTripReportStudyLog tripReportStudyLog = QTripReportStudyLog.tripReportStudyLog; + private final QTripReport tripReport = QTripReport.tripReport; + private final QStudyLog studyLog = QStudyLog.studyLog; + private final QMember member = QMember.member; + + @Override + public long deleteAllByDeletedMemberOwner() { + return queryFactory + .delete(tripReportStudyLog) + .where( + tripReportStudyLog + .tripReport + .id + .in( + JPAExpressions.select(tripReport.id) + .from(tripReport) + .join(tripReport.member, member) + .where(member.deletedAt.isNotNull())) + .or( + tripReportStudyLog.studyLog.id.in( + JPAExpressions.select(studyLog.id) + .from(studyLog) + .join(studyLog.member, member) + .where(member.deletedAt.isNotNull())))) + .execute(); + } +} diff --git a/src/test/java/com/ject/studytrip/trip/application/service/TripReportCommandServiceTest.java b/src/test/java/com/ject/studytrip/trip/application/service/TripReportCommandServiceTest.java index e9dcadd..371d8e1 100644 --- a/src/test/java/com/ject/studytrip/trip/application/service/TripReportCommandServiceTest.java +++ b/src/test/java/com/ject/studytrip/trip/application/service/TripReportCommandServiceTest.java @@ -8,6 +8,7 @@ import com.ject.studytrip.member.domain.model.Member; import com.ject.studytrip.member.fixture.MemberFixture; import com.ject.studytrip.trip.domain.model.TripReport; +import com.ject.studytrip.trip.domain.repository.TripReportQueryRepository; import com.ject.studytrip.trip.domain.repository.TripReportRepository; import com.ject.studytrip.trip.fixture.CreateTripReportRequestFixture; import com.ject.studytrip.trip.fixture.TripReportFixture; @@ -23,6 +24,7 @@ class TripReportCommandServiceTest extends BaseUnitTest { @InjectMocks private TripReportCommandService tripReportCommandService; @Mock private TripReportRepository tripReportRepository; + @Mock private TripReportQueryRepository tripReportQueryRepository; private Member member; private TripReport tripReport; @@ -72,4 +74,35 @@ void shouldUpdateImageUrlWhenTripReportIsValid() { assertThat(tripReport.getImageUrl()).isNotEqualTo(oldImageUrl); } } + + @Nested + @DisplayName("hardDeleteTripReportsOwnedByDeletedMember 메서드는") + class HardDeleteTripReportsOwnedByDeletedMember { + + @Test + @DisplayName("삭제된 멤버가 소유한 여행 리포트가 없으면 0을 반환한다.") + void shouldReturnZeroWhenTripReportsOwnedByDeletedMemberDoNotExist() { + // given + given(tripReportQueryRepository.deleteAllByDeletedMemberOwner()).willReturn(0L); + + // when + long result = tripReportCommandService.hardDeleteTripReportsOwnedByDeletedMember(); + + // then + assertThat(result).isEqualTo(0L); + } + + @Test + @DisplayName("삭제된 멤버가 소유한 여행 리포트가 있으면 해당 개수를 반환한다.") + void shouldReturnCountWhenTripReportsOwnedByDeletedMemberExist() { + // given + given(tripReportQueryRepository.deleteAllByDeletedMemberOwner()).willReturn(5L); + + // when + long result = tripReportCommandService.hardDeleteTripReportsOwnedByDeletedMember(); + + // then + assertThat(result).isEqualTo(5L); + } + } } diff --git a/src/test/java/com/ject/studytrip/trip/application/service/TripReportStudyLogCommandServiceTest.java b/src/test/java/com/ject/studytrip/trip/application/service/TripReportStudyLogCommandServiceTest.java index 6f32291..437e892 100644 --- a/src/test/java/com/ject/studytrip/trip/application/service/TripReportStudyLogCommandServiceTest.java +++ b/src/test/java/com/ject/studytrip/trip/application/service/TripReportStudyLogCommandServiceTest.java @@ -1,6 +1,8 @@ package com.ject.studytrip.trip.application.service; +import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.willDoNothing; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -11,6 +13,7 @@ import com.ject.studytrip.studylog.domain.model.StudyLog; import com.ject.studytrip.studylog.fixture.StudyLogFixture; import com.ject.studytrip.trip.domain.model.*; +import com.ject.studytrip.trip.domain.repository.TripReportStudyLogQueryRepository; import com.ject.studytrip.trip.domain.repository.TripReportStudyLogRepository; import com.ject.studytrip.trip.fixture.DailyGoalFixture; import com.ject.studytrip.trip.fixture.TripFixture; @@ -27,6 +30,7 @@ class TripReportStudyLogCommandServiceTest extends BaseUnitTest { @InjectMocks private TripReportStudyLogCommandService tripReportStudyLogCommandService; @Mock private TripReportStudyLogRepository tripReportStudyLogRepository; + @Mock private TripReportStudyLogQueryRepository tripReportStudyLogQueryRepository; private TripReport tripReport; private List studyLogs; @@ -59,4 +63,54 @@ void shouldCreateTripReportStudyLogs() { verify(tripReportStudyLogRepository, times(1)).saveAll(anyList()); } } + + @Nested + @DisplayName("hardDeleteTripReportStudyLogsOwnedByDeletedMember 메서드는") + class HardDeleteTripReportStudyLogsOwnedByDeletedMember { + + @Test + @DisplayName("삭제된 멤버가 소유한 여행 리포트가 없으면 0을 반환한다.") + void shouldReturnZeroWhenTripReportsOwnedByDeletedMemberDoNotExist() { + // given + given(tripReportStudyLogQueryRepository.deleteAllByDeletedMemberOwner()).willReturn(0L); + + // when + long result = + tripReportStudyLogCommandService + .hardDeleteTripReportStudyLogsOwnedByDeletedMember(); + + // then + assertThat(result).isEqualTo(0L); + } + + @Test + @DisplayName("삭제된 멤버가 소유한 학습 로그가 없으면 0을 반환한다.") + void shouldReturnZeroWhenStudyLogsOwnedByDeletedMemberDoNotExist() { + // given + given(tripReportStudyLogQueryRepository.deleteAllByDeletedMemberOwner()).willReturn(0L); + + // when + long result = + tripReportStudyLogCommandService + .hardDeleteTripReportStudyLogsOwnedByDeletedMember(); + + // then + assertThat(result).isEqualTo(0L); + } + + @Test + @DisplayName("삭제된 멤버가 소유한 여행 리포트 또는 학습 로그가 있으면 삭제된 TripReportStudyLog 개수를 반환한다.") + void shouldReturnCountWhenTripReportsOrStudyLogsOwnedByDeletedMemberExist() { + // given + given(tripReportStudyLogQueryRepository.deleteAllByDeletedMemberOwner()).willReturn(5L); + + // when + long result = + tripReportStudyLogCommandService + .hardDeleteTripReportStudyLogsOwnedByDeletedMember(); + + // then + assertThat(result).isEqualTo(5L); + } + } }