From 89a012f3d892520a32e63f042b5fb438429c7520 Mon Sep 17 00:00:00 2001 From: "Choi, Minwoo" Date: Sat, 30 Aug 2025 15:26:47 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20Spring=20Batch=20=EA=B8=B0=EB=B0=98=20?= =?UTF-8?q?=EC=97=B0=EA=B4=80=20=EC=97=94=ED=8B=B0=ED=8B=B0=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20=EB=B0=8F=20=ED=95=98=EB=93=9C=20=EB=94=9C=EB=A6=AC?= =?UTF-8?q?=ED=8A=B8=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80(#64)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: PomodoroQueryRepository, PomodoroQueryRepositoryAdapter 구현 * feat: DailyGoalQueryRepository, DailyGoalQueryRepositoryAdapter 구현 * feat: HardDeleteExecutor, HardDeleteFacade 구현 * feat: MemberQueryRepository에 deleteAllByDeletedAtIsNotNull 메서드 추가 * feat: TripQueryRepository에 deleteAllByDeletedAtIsNotNull, deleteAllByDeletedMemberOwner 메서드 추가 * feat: StampQueryRepository에 deleteAllByDeletedAtIsNotNull, deleteAllByDeletedTripOwner 메서드 추가 * feat: MissionQueryRepository에 deleteAllByDeletedAtIsNotNull, deleteAllByDeletedStampOwner 메서드 추가 * feat: DailyMissionQueryRepository에 deleteAllByDeletedAtIsNotNull 메서드 추가 * feat: DailyMissionQueryRepository에 deleteAllByDeletedMissionOwner 메서드 추가 * feat: DailyMissionQueryRepository에 deleteAllByDeletedDailyGoalOwner 메서드 추가 * feat: StudyLogDailyMissionQueryRepository에 deleteAllByDeletedAtIsNotNull 메서드 추가 * feat: StudyLogDailyMissionQueryRepository에 deleteAllByDeletedDailyMissionOwner 메서드 추가 * feat: StudyLogDailyMissionQueryRepository에 deleteAllByDeletedStudyLogOwner 메서드 추가 * feat: StudyLogQueryRepository에 deleteAllByDeletedAtIsNotNull 메서드 추가 * feat: StudyLogQueryRepository에 deleteAllByDeletedMemberOwner, deleteAllByDeletedDailyGoalOwner 메서드 추가 * feat: MemberService에 hardDeleteMembers 메서드 추가 * feat: TripService에 hardDeleteTrips, hardDeleteTripsOwnedByDeletedMember 메서드 추가 * feat: StampService에 hardDeleteStamps, hardDeleteStampsOwnedByDeletedTrip 메서드 추가 * feat: MissionService에 hardDeleteMissions, hardDeleteMissionsOwnedByDeletedStamp 메서드 추가 * feat: DailyMissionService에 hardDeleteDailyMissions 메서드 추가 * feat: DailyMissionService에 hardDeleteDailyMissionsOwnedByDeletedMission 메서드 추가 * feat: DailyMissionService에 hardDeleteDailyMissionsOwnedByDeletedDailyGoal 메서드 추가 * feat: StudyLogDailyMissionService에 hardDeleteStudyLogDailyMissions 메서드 추가 * feat: StudyLogDailyMissionService에 hardDeleteStudyLogDailyMissionsOwnedByDeletedDailyMission 메서드 추가 * feat: StudyLogDailyMissionService에 hardDeleteStudyLogDailyMissionsOwnedByDeletedStudyLog 메서드 추가 * feat: StudyLogService에 hardDeleteStudyLogs 메서드 추가 * feat: StudyLogService에 hardDeleteStudyLogsOwnedByDeletedMember 메서드 추가 * feat: StudyLogService에 hardDeleteStudyLogsOwnedByDeletedDailyGoal 메서드 추가 * feat: PomodoroService에 hardDeletePomodoros, hardDeletePomodorosOwnedByDeletedDailyGoal 메서드 추가 * feat: DailyGoalService에 hardDeleteDailyGoals, hardDeleteDailyGoalsOwnedByDeletedTrip 메서드 추가 * chore: BatchJobConfig 추가 * chore: applicaiton-flyway.yml 구현 * chore: application.yml에 flyway 프로필 설정 추가 * chore: local, test, dev, prod 프로필 환경에서 flyway 설정 제거 * chore: application-batch.yml 추가 * chore: application-datasource.yml에 * chore: V2__create_batch_meta_tables.sql 추가 * chore: SchedulerConfig에 hardDeleteJob 스케줄링 추가 * test: MemberServiceTest에 HardDeleteMembers 단위 테스트 추가 * test: TripServiceTest에 HardDeleteTrips, HardDeleteTripsOwnedByDeletedMember 단위 테스트 추가 * test: StampServiceTest에 HardDeleteStamps, HardDeleteStampsOwnedByDeletedTrip 단위 테스트 추가 * test: MissionServiceTest에 HardDeleteMissions, HardDeleteMissionsOwnedByDeletedStamp 단위 테스트 추가 * test: DailyMissionServiceTest에 HardDeleteDailyMissions 단위 테스트 추가 * test: DailyMissionServiceTest에 HardDeleteDailyMissionsOwnedByDeletedMission 단위 테스트 추가 * test: DailyMissionServiceTest에 HardDeleteDailyMissionsOwnedByDeletedDailyGoal 단위 테스트 추가 * test: StudyLogDailyMissionServiceTest에 HardDeleteStudyLogDailyMissions 단위 테스트 추가 * test: StudyLogDailyMissionServiceTest에 HardDeleteStudyLogDailyMissionsOwnedByDeletedDailyMission 단위 테스트 추가 * test: StudyLogDailyMissionServiceTest에 HardDeleteStudyLogDailyMissionsOwnedByDeletedStudyLog 단위 테스트 추가 * test: StudyLogServiceTest에 HardDeleteStudyLogs 단위 테스트 추가 * test: StudyLogServiceTest에 HardDeleteStudyLogsOwnedByDeletedMember 단위 테스트 추가 * test: StudyLogServiceTest에 HardDeleteStudyLogsOwnedByDeletedDailyGoal 단위 테스트 추가 * test: PomodoroServiceTest에 HardDeletePomodoros, HardDeletePomodorosOwnedByDeletedDailyGoal 단위 테스트 추가 * test: DailyGoalServiceTest에 HardDeleteDailyGoals, HardDeleteDailyGoalsOwnedByDeletedTrip 단위 테스트 추가 --- .../executor/HardDeleteExecutor.java | 21 +++ .../application/facade/HardDeleteFacade.java | 174 ++++++++++++++++++ .../global/config/BatchJobConfig.java | 49 +++++ .../global/config/SchedulerConfig.java | 27 ++- .../application/service/MemberService.java | 6 + .../repository/MemberQueryRepository.java | 2 + .../MemberQueryRepositoryAdapter.java | 5 + .../service/DailyMissionService.java | 17 ++ .../application/service/MissionService.java | 11 ++ .../DailyMissionQueryRepository.java | 6 + .../repository/MissionQueryRepository.java | 4 + .../DailyMissionQueryRepositoryAdapter.java | 35 ++++ .../MissionQueryRepositoryAdapter.java | 18 ++ .../application/service/PomodoroService.java | 14 ++ .../repository/PomodoroQueryRepository.java | 7 + .../PomodoroQueryRepositoryAdapter.java | 34 ++++ .../application/service/StampService.java | 11 ++ .../repository/StampQueryRepository.java | 4 + .../querydsl/StampQueryRepositoryAdapter.java | 20 ++ .../service/StudyLogDailyMissionService.java | 17 ++ .../application/service/StudyLogService.java | 16 ++ .../StudyLogDailyMissionQueryRepository.java | 6 + .../repository/StudyLogQueryRepository.java | 6 + ...LogDailyMissionQueryRepositoryAdapter.java | 35 ++++ .../StudyLogQueryRepositoryAdapter.java | 32 ++++ .../application/service/DailyGoalService.java | 14 ++ .../trip/application/service/TripService.java | 11 ++ .../repository/DailyGoalQueryRepository.java | 7 + .../repository/TripQueryRepository.java | 4 + .../DailyGoalQueryRepositoryAdapter.java | 34 ++++ .../querydsl/TripQueryRepositoryAdapter.java | 20 ++ src/main/resources/application-batch.yml | 12 ++ src/main/resources/application-datasource.yml | 2 +- src/main/resources/application-dev.yml | 7 - src/main/resources/application-flyway.yml | 12 ++ src/main/resources/application-local.yml | 11 +- src/main/resources/application-prod.yml | 7 - src/main/resources/application.yml | 2 + .../V2__create_batch_meta_tables.sql | 96 ++++++++++ .../service/MemberServiceTest.java | 31 ++++ .../service/DailyMissionServiceTest.java | 93 ++++++++++ .../service/MissionServiceTest.java | 62 +++++++ .../service/PomodoroServiceTest.java | 64 +++++++ .../application/service/StampServiceTest.java | 62 +++++++ .../StudyLogDailyMissionServiceTest.java | 107 +++++++++++ .../service/StudyLogServiceTest.java | 93 ++++++++++ .../service/DailyGoalServiceTest.java | 64 +++++++ .../application/service/TripServiceTest.java | 62 +++++++ src/test/resources/application-test.yml | 9 +- 49 files changed, 1430 insertions(+), 33 deletions(-) create mode 100644 src/main/java/com/ject/studytrip/cleanup/application/executor/HardDeleteExecutor.java create mode 100644 src/main/java/com/ject/studytrip/cleanup/application/facade/HardDeleteFacade.java create mode 100644 src/main/java/com/ject/studytrip/global/config/BatchJobConfig.java create mode 100644 src/main/java/com/ject/studytrip/pomodoro/domain/repository/PomodoroQueryRepository.java create mode 100644 src/main/java/com/ject/studytrip/pomodoro/infra/querydsl/PomodoroQueryRepositoryAdapter.java create mode 100644 src/main/java/com/ject/studytrip/trip/domain/repository/DailyGoalQueryRepository.java create mode 100644 src/main/java/com/ject/studytrip/trip/infra/querydsl/DailyGoalQueryRepositoryAdapter.java create mode 100644 src/main/resources/application-batch.yml create mode 100644 src/main/resources/application-flyway.yml create mode 100644 src/main/resources/db/migration-batch/V2__create_batch_meta_tables.sql diff --git a/src/main/java/com/ject/studytrip/cleanup/application/executor/HardDeleteExecutor.java b/src/main/java/com/ject/studytrip/cleanup/application/executor/HardDeleteExecutor.java new file mode 100644 index 0000000..a338fe5 --- /dev/null +++ b/src/main/java/com/ject/studytrip/cleanup/application/executor/HardDeleteExecutor.java @@ -0,0 +1,21 @@ +package com.ject.studytrip.cleanup.application.executor; + +import java.util.function.LongSupplier; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class HardDeleteExecutor { + + public long run(String phase, LongSupplier supplier) { + try { + long deleted = supplier.getAsLong(); + log.info("[HardDelete] phase={}, deleted={}", phase, deleted); + return deleted; + } catch (Exception e) { + log.error("[HardDelete] phase={} failed: {}", phase, e.getMessage(), e); + return -1L; // 실패 표식 + } + } +} 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 new file mode 100644 index 0000000..e60c90a --- /dev/null +++ b/src/main/java/com/ject/studytrip/cleanup/application/facade/HardDeleteFacade.java @@ -0,0 +1,174 @@ +package com.ject.studytrip.cleanup.application.facade; + +import com.ject.studytrip.cleanup.application.executor.HardDeleteExecutor; +import com.ject.studytrip.member.application.service.MemberService; +import com.ject.studytrip.mission.application.service.DailyMissionService; +import com.ject.studytrip.mission.application.service.MissionService; +import com.ject.studytrip.pomodoro.application.service.PomodoroService; +import com.ject.studytrip.stamp.application.service.StampService; +import com.ject.studytrip.studylog.application.service.StudyLogDailyMissionService; +import com.ject.studytrip.studylog.application.service.StudyLogService; +import com.ject.studytrip.trip.application.service.DailyGoalService; +import com.ject.studytrip.trip.application.service.TripService; +import java.util.LinkedHashMap; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class HardDeleteFacade { + private final PomodoroService pomodoroService; + private final StudyLogDailyMissionService studyLogDailyMissionService; + private final StudyLogService studyLogService; + private final DailyMissionService dailyMissionService; + private final MissionService missionService; + private final StampService stampService; + private final TripService tripService; + private final DailyGoalService dailyGoalService; + private final MemberService memberService; + + private final HardDeleteExecutor executor; + + private static final String POMODOROS_OWNED_BY_DELETED_DAILY_GOAL = + "pomodorosOwnedByDeletedDailyGoal"; + private static final String STUDY_LOG_DAILY_MISSIONS_OWNED_BY_DELETED_DAILY_MISSION = + "studyLogDailyMissionsOwnedByDeletedDailyMission"; + private static final String STUDY_LOG_DAILY_MISSIONS_OWNED_BY_DELETED_STUDY_LOG = + "studyLogDailyMissionsOwnedByDeletedStudyLog"; + private static final String STUDY_LOGS_OWNED_BY_DELETED_MEMBER = + "studyLogsOwnedByDeletedMember"; + private static final String STUDY_LOGS_OWNED_BY_DELETED_DAILY_GOAL = + "studyLogsOwnedByDeletedDailyGoal"; + private static final String DAILY_MISSIONS_OWNED_BY_DELETED_MISSION = + "dailyMissionsOwnedByDeletedMission"; + private static final String DAILY_MISSIONS_OWNED_BY_DELETED_DAILY_GOAL = + "dailyMissionsOwnedByDeletedDailyGoal"; + + private static final String POMODOROS = "pomodoros"; + private static final String STUDY_LOG_DAILY_MISSIONS = "studyLogDailyMissions"; + private static final String STUDY_LOGS = "studyLogs"; + private static final String DAILY_MISSIONS = "dailyMissions"; + private static final String MISSIONS_OWNED_BY_DELETED_STAMP = "missionsOwnedByDeletedStamp"; + private static final String MISSIONS = "missions"; + private static final String STAMPS_OWNED_BY_DELETED_TRIP = "stampsOwnedByDeletedTrip"; + private static final String STAMPS = "stamps"; + private static final String TRIPS_OWNED_BY_DELETED_MEMBER = "tripsOwnedByDeletedMember"; + private static final String TRIPS = "trips"; + private static final String DAILY_GOALS_OWNED_BY_DELETED_TRIP = "dailyGoalsOwnedByDeletedTrip"; + private static final String DAILY_GOALS = "dailyGoals"; + private static final String MEMBERS = "members"; + + public void hardDeleteAll() { + Map phases = new LinkedHashMap<>(); + + deletePomodoros(phases); // 뽀모도로 삭제 + deleteStudyLogDailyMissions(phases); // StudyLogDailyMission 삭제 + deleteDailyMissions(phases); // 데일리 미션 삭제 + deleteStudyLogs(phases); // 학습 로그 삭제 + deleteDailyGoals(phases); // 데일리 목표 삭제 + deleteMissions(phases); // 미션 삭제 + deleteStamps(phases); // 스탬프 삭제 + deleteTrips(phases); // 여행 삭제 + deleteMembers(phases); // 멤버 삭제 + } + + private void deletePomodoros(Map phases) { + phases.put( + POMODOROS_OWNED_BY_DELETED_DAILY_GOAL, + executor.run( + POMODOROS_OWNED_BY_DELETED_DAILY_GOAL, + pomodoroService::hardDeletePomodorosOwnedByDeletedDailyGoal)); + phases.put(POMODOROS, executor.run(POMODOROS, pomodoroService::hardDeletePomodoros)); + } + + private void deleteStudyLogDailyMissions(Map phases) { + phases.put( + STUDY_LOG_DAILY_MISSIONS_OWNED_BY_DELETED_DAILY_MISSION, + executor.run( + STUDY_LOG_DAILY_MISSIONS_OWNED_BY_DELETED_DAILY_MISSION, + studyLogDailyMissionService + ::hardDeleteStudyLogDailyMissionsOwnedByDeletedDailyMission)); + phases.put( + STUDY_LOG_DAILY_MISSIONS_OWNED_BY_DELETED_STUDY_LOG, + executor.run( + STUDY_LOG_DAILY_MISSIONS_OWNED_BY_DELETED_STUDY_LOG, + studyLogDailyMissionService + ::hardDeleteStudyLogDailyMissionsOwnedByDeletedStudyLog)); + phases.put( + STUDY_LOG_DAILY_MISSIONS, + executor.run( + STUDY_LOG_DAILY_MISSIONS, + studyLogDailyMissionService::hardDeleteStudyLogDailyMissions)); + } + + private void deleteDailyMissions(Map phases) { + phases.put( + DAILY_MISSIONS_OWNED_BY_DELETED_MISSION, + executor.run( + DAILY_MISSIONS_OWNED_BY_DELETED_MISSION, + dailyMissionService::hardDeleteDailyMissionsOwnedByDeletedMission)); + phases.put( + DAILY_MISSIONS_OWNED_BY_DELETED_DAILY_GOAL, + executor.run( + DAILY_MISSIONS_OWNED_BY_DELETED_DAILY_GOAL, + dailyMissionService::hardDeleteDailyMissionsOwnedByDeletedDailyGoal)); + phases.put( + DAILY_MISSIONS, + executor.run(DAILY_MISSIONS, dailyMissionService::hardDeleteDailyMissions)); + } + + private void deleteStudyLogs(Map phases) { + phases.put( + STUDY_LOGS_OWNED_BY_DELETED_MEMBER, + executor.run( + STUDY_LOGS_OWNED_BY_DELETED_MEMBER, + studyLogService::hardDeleteStudyLogsOwnedByDeletedMember)); + phases.put( + STUDY_LOGS_OWNED_BY_DELETED_DAILY_GOAL, + executor.run( + STUDY_LOGS_OWNED_BY_DELETED_DAILY_GOAL, + studyLogService::hardDeleteStudyLogsOwnedByDeletedDailyGoal)); + phases.put(STUDY_LOGS, executor.run(STUDY_LOGS, studyLogService::hardDeleteStudyLogs)); + } + + private void deleteDailyGoals(Map phases) { + phases.put( + DAILY_GOALS_OWNED_BY_DELETED_TRIP, + executor.run( + DAILY_GOALS_OWNED_BY_DELETED_TRIP, + dailyGoalService::hardDeleteDailyGoalsOwnedByDeletedTrip)); + phases.put(DAILY_GOALS, executor.run(DAILY_GOALS, dailyGoalService::hardDeleteDailyGoals)); + } + + private void deleteMissions(Map phases) { + phases.put( + MISSIONS_OWNED_BY_DELETED_STAMP, + executor.run( + MISSIONS_OWNED_BY_DELETED_STAMP, + missionService::hardDeleteMissionsOwnedByDeletedStamp)); + phases.put(MISSIONS, executor.run(MISSIONS, missionService::hardDeleteMissions)); + } + + private void deleteStamps(Map phases) { + phases.put( + STAMPS_OWNED_BY_DELETED_TRIP, + executor.run( + STAMPS_OWNED_BY_DELETED_TRIP, + stampService::hardDeleteStampsOwnedByDeletedTrip)); + phases.put(STAMPS, executor.run(STAMPS, stampService::hardDeleteStamps)); + } + + private void deleteTrips(Map phases) { + phases.put( + TRIPS_OWNED_BY_DELETED_MEMBER, + executor.run( + TRIPS_OWNED_BY_DELETED_MEMBER, + tripService::hardDeleteTripsOwnedByDeletedMember)); + phases.put(TRIPS, executor.run(TRIPS, tripService::hardDeleteTrips)); + } + + private void deleteMembers(Map phases) { + phases.put(MEMBERS, executor.run(MEMBERS, memberService::hardDeleteMembers)); + } +} diff --git a/src/main/java/com/ject/studytrip/global/config/BatchJobConfig.java b/src/main/java/com/ject/studytrip/global/config/BatchJobConfig.java new file mode 100644 index 0000000..8b7ec05 --- /dev/null +++ b/src/main/java/com/ject/studytrip/global/config/BatchJobConfig.java @@ -0,0 +1,49 @@ +package com.ject.studytrip.global.config; + +import com.ject.studytrip.cleanup.application.facade.HardDeleteFacade; +import lombok.RequiredArgsConstructor; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.job.builder.JobBuilder; +import org.springframework.batch.core.launch.support.RunIdIncrementer; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.step.builder.StepBuilder; +import org.springframework.batch.core.step.tasklet.Tasklet; +import org.springframework.batch.repeat.RepeatStatus; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.PlatformTransactionManager; + +@Configuration +@RequiredArgsConstructor +public class BatchJobConfig { + private final JobRepository jobRepository; + private final HardDeleteFacade hardDeleteFacade; + private final PlatformTransactionManager transactionManager; + + private static final String HARD_DELETE_JOB_NAME = "hardDeleteJob"; + private static final String HARD_DELETE_STEP_NAME = "hardDeleteStep"; + + @Bean + public Job hardDeleteJob(Step hardDeleteStep) { + return new JobBuilder(HARD_DELETE_JOB_NAME, jobRepository) + .incrementer(new RunIdIncrementer()) + .start(hardDeleteStep) + .build(); + } + + @Bean + public Step hardDeleteStep(Tasklet hardDeleteTasklet) { + return new StepBuilder(HARD_DELETE_STEP_NAME, jobRepository) + .tasklet(hardDeleteTasklet, transactionManager) + .build(); + } + + @Bean + public Tasklet hardDeleteTasklet() { + return (contribution, chunkContext) -> { + hardDeleteFacade.hardDeleteAll(); + return RepeatStatus.FINISHED; + }; + } +} diff --git a/src/main/java/com/ject/studytrip/global/config/SchedulerConfig.java b/src/main/java/com/ject/studytrip/global/config/SchedulerConfig.java index 6149aed..f263479 100644 --- a/src/main/java/com/ject/studytrip/global/config/SchedulerConfig.java +++ b/src/main/java/com/ject/studytrip/global/config/SchedulerConfig.java @@ -1,8 +1,33 @@ package com.ject.studytrip.global.config; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.JobParametersBuilder; +import org.springframework.batch.core.launch.JobLauncher; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.Scheduled; +@Profile("!test") @EnableScheduling @Configuration -public class SchedulerConfig {} +public class SchedulerConfig { + private final JobLauncher jobLauncher; + private final Job hardDeleteJob; + + public SchedulerConfig(JobLauncher jobLauncher, @Qualifier("hardDeleteJob") Job hardDeleteJob) { + this.jobLauncher = jobLauncher; + this.hardDeleteJob = hardDeleteJob; + } + + // 매일 04:00 (Asia/Seoul) + @Scheduled(cron = "0 0 4 * * *", zone = "Asia/Seoul") + public void runDaily() throws Exception { + jobLauncher.run( + hardDeleteJob, + new JobParametersBuilder() + .addLong("ts", System.currentTimeMillis()) // 매 실행마다 유니크 파라미터 + .toJobParameters()); + } +} diff --git a/src/main/java/com/ject/studytrip/member/application/service/MemberService.java b/src/main/java/com/ject/studytrip/member/application/service/MemberService.java index 1257582..e87e1f1 100644 --- a/src/main/java/com/ject/studytrip/member/application/service/MemberService.java +++ b/src/main/java/com/ject/studytrip/member/application/service/MemberService.java @@ -16,6 +16,7 @@ import com.ject.studytrip.member.presentation.dto.request.UpdateMemberRequest; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; @Service @@ -90,6 +91,11 @@ public String getRoleByMemberId(String memberId) { return memberRole.name(); } + @Transactional(propagation = Propagation.REQUIRES_NEW) + public long hardDeleteMembers() { + return memberQueryRepository.deleteAllByDeletedAtIsNotNull(); + } + private void validateMemberIsUnique(SocialProvider socialProvider, String socialId) { boolean isMemberDuplicated = memberRepository.existsBySocialProviderAndSocialId(socialProvider, socialId); diff --git a/src/main/java/com/ject/studytrip/member/domain/repository/MemberQueryRepository.java b/src/main/java/com/ject/studytrip/member/domain/repository/MemberQueryRepository.java index 220ec0d..8653e14 100644 --- a/src/main/java/com/ject/studytrip/member/domain/repository/MemberQueryRepository.java +++ b/src/main/java/com/ject/studytrip/member/domain/repository/MemberQueryRepository.java @@ -4,4 +4,6 @@ public interface MemberQueryRepository { MemberRole findMemberRoleById(Long memberId); + + long deleteAllByDeletedAtIsNotNull(); } diff --git a/src/main/java/com/ject/studytrip/member/infra/querydsl/MemberQueryRepositoryAdapter.java b/src/main/java/com/ject/studytrip/member/infra/querydsl/MemberQueryRepositoryAdapter.java index b1dbfbe..6e20eea 100644 --- a/src/main/java/com/ject/studytrip/member/infra/querydsl/MemberQueryRepositoryAdapter.java +++ b/src/main/java/com/ject/studytrip/member/infra/querydsl/MemberQueryRepositoryAdapter.java @@ -21,4 +21,9 @@ public MemberRole findMemberRoleById(Long memberId) { .where(member.id.eq(memberId)) .fetchOne(); } + + @Override + public long deleteAllByDeletedAtIsNotNull() { + return queryFactory.delete(member).where(member.deletedAt.isNotNull()).execute(); + } } diff --git a/src/main/java/com/ject/studytrip/mission/application/service/DailyMissionService.java b/src/main/java/com/ject/studytrip/mission/application/service/DailyMissionService.java index 1969e2d..af57180 100644 --- a/src/main/java/com/ject/studytrip/mission/application/service/DailyMissionService.java +++ b/src/main/java/com/ject/studytrip/mission/application/service/DailyMissionService.java @@ -10,6 +10,8 @@ import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor @@ -47,4 +49,19 @@ public List getValidDailyMissionsByIds( public List getDailyMissionsByDailyGoal(Long dailyGoalId) { return dailyMissionQueryRepository.findAllByDailyGoalIdFetchJoinMission(dailyGoalId); } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + public long hardDeleteDailyMissions() { + return dailyMissionQueryRepository.deleteAllByDeletedAtIsNotNull(); + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + public long hardDeleteDailyMissionsOwnedByDeletedMission() { + return dailyMissionQueryRepository.deleteAllByDeletedMissionOwner(); + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + public long hardDeleteDailyMissionsOwnedByDeletedDailyGoal() { + return dailyMissionQueryRepository.deleteAllByDeletedDailyGoalOwner(); + } } diff --git a/src/main/java/com/ject/studytrip/mission/application/service/MissionService.java b/src/main/java/com/ject/studytrip/mission/application/service/MissionService.java index 7e8a11b..7831e63 100644 --- a/src/main/java/com/ject/studytrip/mission/application/service/MissionService.java +++ b/src/main/java/com/ject/studytrip/mission/application/service/MissionService.java @@ -13,6 +13,7 @@ import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; @Service @@ -95,6 +96,16 @@ public void validateAllMissionsCompletedByStampId(Long stampId) { MissionPolicy.validateAllCompleted(exists); } + @Transactional(propagation = Propagation.REQUIRES_NEW) + public long hardDeleteMissions() { + return missionQueryRepository.deleteAllByDeletedAtIsNotNull(); + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + public long hardDeleteMissionsOwnedByDeletedStamp() { + return missionQueryRepository.deleteAllByDeletedStampOwner(); + } + private void validateMissionIsActiveAndBelongsToStamp(Long stampId, Mission mission) { MissionPolicy.validateMissionBelongsToStamp(stampId, mission); MissionPolicy.validateNotDeleted(mission); diff --git a/src/main/java/com/ject/studytrip/mission/domain/repository/DailyMissionQueryRepository.java b/src/main/java/com/ject/studytrip/mission/domain/repository/DailyMissionQueryRepository.java index 4cbda68..ab7441d 100644 --- a/src/main/java/com/ject/studytrip/mission/domain/repository/DailyMissionQueryRepository.java +++ b/src/main/java/com/ject/studytrip/mission/domain/repository/DailyMissionQueryRepository.java @@ -5,4 +5,10 @@ public interface DailyMissionQueryRepository { List findAllByDailyGoalIdFetchJoinMission(Long dailyGoalId); + + long deleteAllByDeletedAtIsNotNull(); + + long deleteAllByDeletedMissionOwner(); + + long deleteAllByDeletedDailyGoalOwner(); } diff --git a/src/main/java/com/ject/studytrip/mission/domain/repository/MissionQueryRepository.java b/src/main/java/com/ject/studytrip/mission/domain/repository/MissionQueryRepository.java index 045dbc8..93da4aa 100644 --- a/src/main/java/com/ject/studytrip/mission/domain/repository/MissionQueryRepository.java +++ b/src/main/java/com/ject/studytrip/mission/domain/repository/MissionQueryRepository.java @@ -7,4 +7,8 @@ public interface MissionQueryRepository { List findAllByIdsInFetchJoinStamp(List ids); boolean existsByStampIdAndCompletedIsFalseAndDeletedAtIsNull(Long stampId); + + long deleteAllByDeletedAtIsNotNull(); + + long deleteAllByDeletedStampOwner(); } diff --git a/src/main/java/com/ject/studytrip/mission/infra/querydsl/DailyMissionQueryRepositoryAdapter.java b/src/main/java/com/ject/studytrip/mission/infra/querydsl/DailyMissionQueryRepositoryAdapter.java index 2f7258c..cf66ea4 100644 --- a/src/main/java/com/ject/studytrip/mission/infra/querydsl/DailyMissionQueryRepositoryAdapter.java +++ b/src/main/java/com/ject/studytrip/mission/infra/querydsl/DailyMissionQueryRepositoryAdapter.java @@ -4,6 +4,8 @@ import com.ject.studytrip.mission.domain.model.QDailyMission; import com.ject.studytrip.mission.domain.model.QMission; import com.ject.studytrip.mission.domain.repository.DailyMissionQueryRepository; +import com.ject.studytrip.trip.domain.model.QDailyGoal; +import com.querydsl.jpa.JPAExpressions; import com.querydsl.jpa.impl.JPAQueryFactory; import java.util.List; import lombok.RequiredArgsConstructor; @@ -15,6 +17,7 @@ public class DailyMissionQueryRepositoryAdapter implements DailyMissionQueryRepo private final JPAQueryFactory queryFactory; private final QDailyMission dailyMission = QDailyMission.dailyMission; private final QMission mission = QMission.mission; + private final QDailyGoal dailyGoal = QDailyGoal.dailyGoal; @Override public List findAllByDailyGoalIdFetchJoinMission(Long dailyGoalId) { @@ -25,4 +28,36 @@ public List findAllByDailyGoalIdFetchJoinMission(Long dailyGoalId) .where(dailyMission.dailyGoal.id.eq(dailyGoalId), dailyMission.deletedAt.isNull()) .fetch(); } + + @Override + public long deleteAllByDeletedAtIsNotNull() { + return queryFactory + .delete(dailyMission) + .where(dailyMission.deletedAt.isNotNull()) + .execute(); + } + + @Override + public long deleteAllByDeletedMissionOwner() { + return queryFactory + .delete(dailyMission) + .where( + dailyMission.mission.id.in( + JPAExpressions.select(mission.id) + .from(mission) + .where(mission.deletedAt.isNotNull()))) + .execute(); + } + + @Override + public long deleteAllByDeletedDailyGoalOwner() { + return queryFactory + .delete(dailyMission) + .where( + dailyMission.dailyGoal.id.in( + JPAExpressions.select(dailyGoal.id) + .from(dailyGoal) + .where(dailyGoal.deletedAt.isNotNull()))) + .execute(); + } } diff --git a/src/main/java/com/ject/studytrip/mission/infra/querydsl/MissionQueryRepositoryAdapter.java b/src/main/java/com/ject/studytrip/mission/infra/querydsl/MissionQueryRepositoryAdapter.java index 3dc4bc5..cc3ef54 100644 --- a/src/main/java/com/ject/studytrip/mission/infra/querydsl/MissionQueryRepositoryAdapter.java +++ b/src/main/java/com/ject/studytrip/mission/infra/querydsl/MissionQueryRepositoryAdapter.java @@ -4,6 +4,7 @@ import com.ject.studytrip.mission.domain.model.QMission; import com.ject.studytrip.mission.domain.repository.MissionQueryRepository; import com.ject.studytrip.stamp.domain.model.QStamp; +import com.querydsl.jpa.JPAExpressions; import com.querydsl.jpa.impl.JPAQueryFactory; import java.util.List; import lombok.RequiredArgsConstructor; @@ -41,6 +42,23 @@ public boolean existsByStampIdAndCompletedIsFalseAndDeletedAtIsNull(Long stampId return hit != null; } + @Override + public long deleteAllByDeletedAtIsNotNull() { + return queryFactory.delete(mission).where(mission.deletedAt.isNotNull()).execute(); + } + + @Override + public long deleteAllByDeletedStampOwner() { + return queryFactory + .delete(mission) + .where( + mission.stamp.id.in( + JPAExpressions.select(stamp.id) + .from(stamp) + .where(stamp.deletedAt.isNotNull()))) + .execute(); + } + // @Override // public long countByStampIdAndDeletedAtIsNull(Long stampId) { // Long count = diff --git a/src/main/java/com/ject/studytrip/pomodoro/application/service/PomodoroService.java b/src/main/java/com/ject/studytrip/pomodoro/application/service/PomodoroService.java index 01a00cb..6c7cf3b 100644 --- a/src/main/java/com/ject/studytrip/pomodoro/application/service/PomodoroService.java +++ b/src/main/java/com/ject/studytrip/pomodoro/application/service/PomodoroService.java @@ -5,16 +5,20 @@ import com.ject.studytrip.pomodoro.domain.factory.PomodoroFactory; import com.ject.studytrip.pomodoro.domain.model.Pomodoro; import com.ject.studytrip.pomodoro.domain.policy.PomodoroPolicy; +import com.ject.studytrip.pomodoro.domain.repository.PomodoroQueryRepository; import com.ject.studytrip.pomodoro.domain.repository.PomodoroRepository; import com.ject.studytrip.pomodoro.presentation.dto.request.CreatePomodoroRequest; import com.ject.studytrip.trip.domain.model.DailyGoal; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor public class PomodoroService { private final PomodoroRepository pomodoroRepository; + private final PomodoroQueryRepository pomodoroQueryRepository; public Pomodoro createPomodoro(DailyGoal dailyGoal, CreatePomodoroRequest request) { int focusDurationInSeconds = request.focusDurationInMinute() * 60; @@ -52,4 +56,14 @@ public void updateTotalFocusTime(Long dailyGoalId, int totalFocusTimeInSeconds) pomodoro.updateTotalFocusTimeInSeconds(totalFocusTimeInSeconds); } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + public long hardDeletePomodoros() { + return pomodoroQueryRepository.deleteAllByDeletedAtIsNotNull(); + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + public long hardDeletePomodorosOwnedByDeletedDailyGoal() { + return pomodoroQueryRepository.deleteAllByDeletedDailyGoalOwner(); + } } diff --git a/src/main/java/com/ject/studytrip/pomodoro/domain/repository/PomodoroQueryRepository.java b/src/main/java/com/ject/studytrip/pomodoro/domain/repository/PomodoroQueryRepository.java new file mode 100644 index 0000000..9f28301 --- /dev/null +++ b/src/main/java/com/ject/studytrip/pomodoro/domain/repository/PomodoroQueryRepository.java @@ -0,0 +1,7 @@ +package com.ject.studytrip.pomodoro.domain.repository; + +public interface PomodoroQueryRepository { + long deleteAllByDeletedAtIsNotNull(); + + long deleteAllByDeletedDailyGoalOwner(); +} diff --git a/src/main/java/com/ject/studytrip/pomodoro/infra/querydsl/PomodoroQueryRepositoryAdapter.java b/src/main/java/com/ject/studytrip/pomodoro/infra/querydsl/PomodoroQueryRepositoryAdapter.java new file mode 100644 index 0000000..1cb9447 --- /dev/null +++ b/src/main/java/com/ject/studytrip/pomodoro/infra/querydsl/PomodoroQueryRepositoryAdapter.java @@ -0,0 +1,34 @@ +package com.ject.studytrip.pomodoro.infra.querydsl; + +import com.ject.studytrip.pomodoro.domain.model.QPomodoro; +import com.ject.studytrip.pomodoro.domain.repository.PomodoroQueryRepository; +import com.ject.studytrip.trip.domain.model.QDailyGoal; +import com.querydsl.jpa.JPAExpressions; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class PomodoroQueryRepositoryAdapter implements PomodoroQueryRepository { + private final JPAQueryFactory queryFactory; + private final QPomodoro pomodoro = QPomodoro.pomodoro; + private final QDailyGoal dailyGoal = QDailyGoal.dailyGoal; + + @Override + public long deleteAllByDeletedAtIsNotNull() { + return queryFactory.delete(pomodoro).where(pomodoro.deletedAt.isNotNull()).execute(); + } + + @Override + public long deleteAllByDeletedDailyGoalOwner() { + return queryFactory + .delete(pomodoro) + .where( + pomodoro.dailyGoal.id.in( + JPAExpressions.select(dailyGoal.id) + .from(dailyGoal) + .where(dailyGoal.deletedAt.isNotNull()))) + .execute(); + } +} diff --git a/src/main/java/com/ject/studytrip/stamp/application/service/StampService.java b/src/main/java/com/ject/studytrip/stamp/application/service/StampService.java index 5b25d1f..0ed8f80 100644 --- a/src/main/java/com/ject/studytrip/stamp/application/service/StampService.java +++ b/src/main/java/com/ject/studytrip/stamp/application/service/StampService.java @@ -17,6 +17,7 @@ import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; @Service @@ -154,6 +155,16 @@ public void validateAllStampsCompletedByTripId(Long tripId) { StampPolicy.validateAllCompleted(exists); } + @Transactional(propagation = Propagation.REQUIRES_NEW) + public long hardDeleteStamps() { + return stampQueryRepository.deleteAllByDeletedAtIsNotNull(); + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + public long hardDeleteStampsOwnedByDeletedTrip() { + return stampQueryRepository.deleteAllByDeletedTripOwner(); + } + private String getExplorationStampName(List stamps) { // 스탬프별 개수 집계 Map stampCountMap = diff --git a/src/main/java/com/ject/studytrip/stamp/domain/repository/StampQueryRepository.java b/src/main/java/com/ject/studytrip/stamp/domain/repository/StampQueryRepository.java index 8b505d4..25d6069 100644 --- a/src/main/java/com/ject/studytrip/stamp/domain/repository/StampQueryRepository.java +++ b/src/main/java/com/ject/studytrip/stamp/domain/repository/StampQueryRepository.java @@ -10,4 +10,8 @@ public interface StampQueryRepository { Optional findFirstIncompleteStampByTripId(Long tripId); boolean existsByTripIdAndCompletedIsFalseAndDeletedAtIsNull(Long tripId); + + long deleteAllByDeletedAtIsNotNull(); + + long deleteAllByDeletedTripOwner(); } diff --git a/src/main/java/com/ject/studytrip/stamp/infra/querydsl/StampQueryRepositoryAdapter.java b/src/main/java/com/ject/studytrip/stamp/infra/querydsl/StampQueryRepositoryAdapter.java index 9db4d20..1d8efc5 100644 --- a/src/main/java/com/ject/studytrip/stamp/infra/querydsl/StampQueryRepositoryAdapter.java +++ b/src/main/java/com/ject/studytrip/stamp/infra/querydsl/StampQueryRepositoryAdapter.java @@ -3,6 +3,8 @@ import com.ject.studytrip.stamp.domain.model.QStamp; import com.ject.studytrip.stamp.domain.model.Stamp; import com.ject.studytrip.stamp.domain.repository.StampQueryRepository; +import com.ject.studytrip.trip.domain.model.QTrip; +import com.querydsl.jpa.JPAExpressions; import com.querydsl.jpa.impl.JPAQueryFactory; import java.util.List; import java.util.Optional; @@ -14,6 +16,7 @@ public class StampQueryRepositoryAdapter implements StampQueryRepository { private final JPAQueryFactory queryFactory; private final QStamp stamp = QStamp.stamp; + private final QTrip trip = QTrip.trip; @Override public List findStampsToShiftAfterOrder(Long tripId, int deletedOrder) { @@ -55,4 +58,21 @@ public boolean existsByTripIdAndCompletedIsFalseAndDeletedAtIsNull(Long tripId) return hit != null; } + + @Override + public long deleteAllByDeletedAtIsNotNull() { + return queryFactory.delete(stamp).where(stamp.deletedAt.isNotNull()).execute(); + } + + @Override + public long deleteAllByDeletedTripOwner() { + return queryFactory + .delete(stamp) + .where( + stamp.trip.id.in( + JPAExpressions.select(trip.id) + .from(trip) + .where(trip.deletedAt.isNotNull()))) + .execute(); + } } diff --git a/src/main/java/com/ject/studytrip/studylog/application/service/StudyLogDailyMissionService.java b/src/main/java/com/ject/studytrip/studylog/application/service/StudyLogDailyMissionService.java index a8700e3..18521f1 100644 --- a/src/main/java/com/ject/studytrip/studylog/application/service/StudyLogDailyMissionService.java +++ b/src/main/java/com/ject/studytrip/studylog/application/service/StudyLogDailyMissionService.java @@ -10,6 +10,8 @@ import java.util.Map; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor @@ -34,4 +36,19 @@ public Map> getGroupedStudyLogDailyMissionsBySt return studyLogDailyMissionQueryRepository.findStudyLogDailyMissionsGroupedByStudyLogId( studyLogIds); } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + public long hardDeleteStudyLogDailyMissions() { + return studyLogDailyMissionQueryRepository.deleteAllByDeletedAtIsNotNull(); + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + public long hardDeleteStudyLogDailyMissionsOwnedByDeletedDailyMission() { + return studyLogDailyMissionQueryRepository.deleteAllByDeletedDailyMissionOwner(); + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + public long hardDeleteStudyLogDailyMissionsOwnedByDeletedStudyLog() { + return studyLogDailyMissionQueryRepository.deleteAllByDeletedStudyLogOwner(); + } } diff --git a/src/main/java/com/ject/studytrip/studylog/application/service/StudyLogService.java b/src/main/java/com/ject/studytrip/studylog/application/service/StudyLogService.java index 60fe620..60b0032 100644 --- a/src/main/java/com/ject/studytrip/studylog/application/service/StudyLogService.java +++ b/src/main/java/com/ject/studytrip/studylog/application/service/StudyLogService.java @@ -10,6 +10,7 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Slice; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; @Service @@ -32,4 +33,19 @@ public Slice getStudyLogsSliceByTripId(Long tripId, int page, int size return studyLogQueryRepository.findSliceByTripIdOrderByCreatedAtDesc( tripId, PageRequest.of(page, size)); } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + public long hardDeleteStudyLogs() { + return studyLogQueryRepository.deleteAllByDeletedAtIsNotNull(); + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + public long hardDeleteStudyLogsOwnedByDeletedMember() { + return studyLogQueryRepository.deleteAllByDeletedMemberOwner(); + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + public long hardDeleteStudyLogsOwnedByDeletedDailyGoal() { + return studyLogQueryRepository.deleteAllByDeletedDailyGoalOwner(); + } } diff --git a/src/main/java/com/ject/studytrip/studylog/domain/repository/StudyLogDailyMissionQueryRepository.java b/src/main/java/com/ject/studytrip/studylog/domain/repository/StudyLogDailyMissionQueryRepository.java index 5987135..65f3114 100644 --- a/src/main/java/com/ject/studytrip/studylog/domain/repository/StudyLogDailyMissionQueryRepository.java +++ b/src/main/java/com/ject/studytrip/studylog/domain/repository/StudyLogDailyMissionQueryRepository.java @@ -7,4 +7,10 @@ public interface StudyLogDailyMissionQueryRepository { Map> findStudyLogDailyMissionsGroupedByStudyLogId( List studyLogIds); + + long deleteAllByDeletedAtIsNotNull(); + + long deleteAllByDeletedDailyMissionOwner(); + + long deleteAllByDeletedStudyLogOwner(); } diff --git a/src/main/java/com/ject/studytrip/studylog/domain/repository/StudyLogQueryRepository.java b/src/main/java/com/ject/studytrip/studylog/domain/repository/StudyLogQueryRepository.java index 73362b7..3753430 100644 --- a/src/main/java/com/ject/studytrip/studylog/domain/repository/StudyLogQueryRepository.java +++ b/src/main/java/com/ject/studytrip/studylog/domain/repository/StudyLogQueryRepository.java @@ -8,4 +8,10 @@ public interface StudyLogQueryRepository { long countActiveStudyLogsByMemberId(Long memberId); Slice findSliceByTripIdOrderByCreatedAtDesc(Long tripId, Pageable pageable); + + long deleteAllByDeletedAtIsNotNull(); + + long deleteAllByDeletedMemberOwner(); + + long deleteAllByDeletedDailyGoalOwner(); } diff --git a/src/main/java/com/ject/studytrip/studylog/infra/querydsl/StudyLogDailyMissionQueryRepositoryAdapter.java b/src/main/java/com/ject/studytrip/studylog/infra/querydsl/StudyLogDailyMissionQueryRepositoryAdapter.java index bda3ab7..7917582 100644 --- a/src/main/java/com/ject/studytrip/studylog/infra/querydsl/StudyLogDailyMissionQueryRepositoryAdapter.java +++ b/src/main/java/com/ject/studytrip/studylog/infra/querydsl/StudyLogDailyMissionQueryRepositoryAdapter.java @@ -2,9 +2,11 @@ import com.ject.studytrip.mission.domain.model.QDailyMission; import com.ject.studytrip.mission.domain.model.QMission; +import com.ject.studytrip.studylog.domain.model.QStudyLog; import com.ject.studytrip.studylog.domain.model.QStudyLogDailyMission; import com.ject.studytrip.studylog.domain.model.StudyLogDailyMission; import com.ject.studytrip.studylog.domain.repository.StudyLogDailyMissionQueryRepository; +import com.querydsl.jpa.JPAExpressions; import com.querydsl.jpa.impl.JPAQueryFactory; import java.util.List; import java.util.Map; @@ -21,6 +23,7 @@ public class StudyLogDailyMissionQueryRepositoryAdapter QStudyLogDailyMission.studyLogDailyMission; private final QDailyMission dailyMission = QDailyMission.dailyMission; private final QMission mission = QMission.mission; + private final QStudyLog studyLog = QStudyLog.studyLog; @Override public Map> findStudyLogDailyMissionsGroupedByStudyLogId( @@ -36,4 +39,36 @@ public Map> findStudyLogDailyMissionsGroupedByS .stream() .collect(Collectors.groupingBy(sldm -> sldm.getStudyLog().getId())); } + + @Override + public long deleteAllByDeletedAtIsNotNull() { + return queryFactory + .delete(studyLogDailyMission) + .where(studyLogDailyMission.deletedAt.isNotNull()) + .execute(); + } + + @Override + public long deleteAllByDeletedDailyMissionOwner() { + return queryFactory + .delete(studyLogDailyMission) + .where( + studyLogDailyMission.dailyMission.id.in( + JPAExpressions.select(dailyMission.id) + .from(dailyMission) + .where(dailyMission.deletedAt.isNotNull()))) + .execute(); + } + + @Override + public long deleteAllByDeletedStudyLogOwner() { + return queryFactory + .delete(studyLogDailyMission) + .where( + studyLogDailyMission.studyLog.id.in( + JPAExpressions.select(studyLog.id) + .from(studyLog) + .where(studyLog.deletedAt.isNotNull()))) + .execute(); + } } diff --git a/src/main/java/com/ject/studytrip/studylog/infra/querydsl/StudyLogQueryRepositoryAdapter.java b/src/main/java/com/ject/studytrip/studylog/infra/querydsl/StudyLogQueryRepositoryAdapter.java index 4e69efc..570d971 100644 --- a/src/main/java/com/ject/studytrip/studylog/infra/querydsl/StudyLogQueryRepositoryAdapter.java +++ b/src/main/java/com/ject/studytrip/studylog/infra/querydsl/StudyLogQueryRepositoryAdapter.java @@ -1,9 +1,11 @@ package com.ject.studytrip.studylog.infra.querydsl; +import com.ject.studytrip.member.domain.model.QMember; import com.ject.studytrip.studylog.domain.model.QStudyLog; import com.ject.studytrip.studylog.domain.model.StudyLog; import com.ject.studytrip.studylog.domain.repository.StudyLogQueryRepository; import com.ject.studytrip.trip.domain.model.QDailyGoal; +import com.querydsl.jpa.JPAExpressions; import com.querydsl.jpa.impl.JPAQueryFactory; import java.util.List; import java.util.Optional; @@ -19,6 +21,7 @@ public class StudyLogQueryRepositoryAdapter implements StudyLogQueryRepository { private final JPAQueryFactory queryFactory; private final QStudyLog studyLog = QStudyLog.studyLog; private final QDailyGoal dailyGoal = QDailyGoal.dailyGoal; + private final QMember member = QMember.member; @Override public long countActiveStudyLogsByMemberId(Long memberId) { @@ -52,4 +55,33 @@ public Slice findSliceByTripIdOrderByCreatedAtDesc(Long tripId, Pageab return new SliceImpl<>(result, pageable, hasNext); } + + @Override + public long deleteAllByDeletedAtIsNotNull() { + return queryFactory.delete(studyLog).where(studyLog.deletedAt.isNotNull()).execute(); + } + + @Override + public long deleteAllByDeletedMemberOwner() { + return queryFactory + .delete(studyLog) + .where( + studyLog.member.id.in( + JPAExpressions.select(member.id) + .from(member) + .where(member.deletedAt.isNotNull()))) + .execute(); + } + + @Override + public long deleteAllByDeletedDailyGoalOwner() { + return queryFactory + .delete(studyLog) + .where( + studyLog.dailyGoal.id.in( + JPAExpressions.select(dailyGoal.id) + .from(dailyGoal) + .where(dailyGoal.deletedAt.isNotNull()))) + .execute(); + } } diff --git a/src/main/java/com/ject/studytrip/trip/application/service/DailyGoalService.java b/src/main/java/com/ject/studytrip/trip/application/service/DailyGoalService.java index 5d22dc6..42eac44 100644 --- a/src/main/java/com/ject/studytrip/trip/application/service/DailyGoalService.java +++ b/src/main/java/com/ject/studytrip/trip/application/service/DailyGoalService.java @@ -6,15 +6,19 @@ import com.ject.studytrip.trip.domain.model.DailyGoal; import com.ject.studytrip.trip.domain.model.Trip; import com.ject.studytrip.trip.domain.policy.DailyGoalPolicy; +import com.ject.studytrip.trip.domain.repository.DailyGoalQueryRepository; import com.ject.studytrip.trip.domain.repository.DailyGoalRepository; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor public class DailyGoalService { public final DailyGoalRepository dailyGoalRepository; + public final DailyGoalQueryRepository dailyGoalQueryRepository; public DailyGoal createDailyGoal(Trip trip, String title) { DailyGoal dailyGoal = DailyGoalFactory.create(trip, title); @@ -41,4 +45,14 @@ public DailyGoal getValidDailyGoal(Long tripId, Long dailyGoalId) { return dailyGoal; } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + public long hardDeleteDailyGoals() { + return dailyGoalQueryRepository.deleteAllByDeletedAtIsNotNull(); + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + public long hardDeleteDailyGoalsOwnedByDeletedTrip() { + return dailyGoalQueryRepository.deleteAllByDeletedTripOwner(); + } } diff --git a/src/main/java/com/ject/studytrip/trip/application/service/TripService.java b/src/main/java/com/ject/studytrip/trip/application/service/TripService.java index efd1a2c..bb932e2 100644 --- a/src/main/java/com/ject/studytrip/trip/application/service/TripService.java +++ b/src/main/java/com/ject/studytrip/trip/application/service/TripService.java @@ -17,6 +17,7 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Slice; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; @Service @@ -110,4 +111,14 @@ public void completeTrip(Trip trip) { public void increaseCompletedStamps(Trip trip) { trip.increaseCompletedStamps(); } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + public long hardDeleteTrips() { + return tripQueryRepository.deleteAllByDeletedAtIsNotNull(); + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + public long hardDeleteTripsOwnedByDeletedMember() { + return tripQueryRepository.deleteAllByDeletedMemberOwner(); + } } diff --git a/src/main/java/com/ject/studytrip/trip/domain/repository/DailyGoalQueryRepository.java b/src/main/java/com/ject/studytrip/trip/domain/repository/DailyGoalQueryRepository.java new file mode 100644 index 0000000..6173c58 --- /dev/null +++ b/src/main/java/com/ject/studytrip/trip/domain/repository/DailyGoalQueryRepository.java @@ -0,0 +1,7 @@ +package com.ject.studytrip.trip.domain.repository; + +public interface DailyGoalQueryRepository { + long deleteAllByDeletedAtIsNotNull(); + + long deleteAllByDeletedTripOwner(); +} diff --git a/src/main/java/com/ject/studytrip/trip/domain/repository/TripQueryRepository.java b/src/main/java/com/ject/studytrip/trip/domain/repository/TripQueryRepository.java index 6af0e5f..23eb4eb 100644 --- a/src/main/java/com/ject/studytrip/trip/domain/repository/TripQueryRepository.java +++ b/src/main/java/com/ject/studytrip/trip/domain/repository/TripQueryRepository.java @@ -9,4 +9,8 @@ public interface TripQueryRepository { Slice findSliceByMemberId(Long memberId, Pageable pageable); long countActiveTripsByMemberIdAndCategory(Long memberId, TripCategory category); + + long deleteAllByDeletedAtIsNotNull(); + + long deleteAllByDeletedMemberOwner(); } diff --git a/src/main/java/com/ject/studytrip/trip/infra/querydsl/DailyGoalQueryRepositoryAdapter.java b/src/main/java/com/ject/studytrip/trip/infra/querydsl/DailyGoalQueryRepositoryAdapter.java new file mode 100644 index 0000000..eb4bcf5 --- /dev/null +++ b/src/main/java/com/ject/studytrip/trip/infra/querydsl/DailyGoalQueryRepositoryAdapter.java @@ -0,0 +1,34 @@ +package com.ject.studytrip.trip.infra.querydsl; + +import com.ject.studytrip.trip.domain.model.QDailyGoal; +import com.ject.studytrip.trip.domain.model.QTrip; +import com.ject.studytrip.trip.domain.repository.DailyGoalQueryRepository; +import com.querydsl.jpa.JPAExpressions; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class DailyGoalQueryRepositoryAdapter implements DailyGoalQueryRepository { + private final JPAQueryFactory queryFactory; + private final QDailyGoal dailyGoal = QDailyGoal.dailyGoal; + private final QTrip trip = QTrip.trip; + + @Override + public long deleteAllByDeletedAtIsNotNull() { + return queryFactory.delete(dailyGoal).where(dailyGoal.deletedAt.isNotNull()).execute(); + } + + @Override + public long deleteAllByDeletedTripOwner() { + return queryFactory + .delete(dailyGoal) + .where( + dailyGoal.trip.id.in( + JPAExpressions.select(trip.id) + .from(trip) + .where(trip.deletedAt.isNotNull()))) + .execute(); + } +} diff --git a/src/main/java/com/ject/studytrip/trip/infra/querydsl/TripQueryRepositoryAdapter.java b/src/main/java/com/ject/studytrip/trip/infra/querydsl/TripQueryRepositoryAdapter.java index 7edb648..3c06f95 100644 --- a/src/main/java/com/ject/studytrip/trip/infra/querydsl/TripQueryRepositoryAdapter.java +++ b/src/main/java/com/ject/studytrip/trip/infra/querydsl/TripQueryRepositoryAdapter.java @@ -1,9 +1,11 @@ package com.ject.studytrip.trip.infra.querydsl; +import com.ject.studytrip.member.domain.model.QMember; import com.ject.studytrip.trip.domain.model.QTrip; import com.ject.studytrip.trip.domain.model.Trip; import com.ject.studytrip.trip.domain.model.TripCategory; import com.ject.studytrip.trip.domain.repository.TripQueryRepository; +import com.querydsl.jpa.JPAExpressions; import com.querydsl.jpa.impl.JPAQueryFactory; import java.util.List; import java.util.Optional; @@ -18,6 +20,7 @@ public class TripQueryRepositoryAdapter implements TripQueryRepository { private final JPAQueryFactory queryFactory; private final QTrip trip = QTrip.trip; + private final QMember member = QMember.member; @Override public Slice findSliceByMemberId(Long memberId, Pageable pageable) { @@ -52,4 +55,21 @@ public long countActiveTripsByMemberIdAndCategory(Long memberId, TripCategory ca return Optional.ofNullable(count).orElse(0L); } + + @Override + public long deleteAllByDeletedAtIsNotNull() { + return queryFactory.delete(trip).where(trip.deletedAt.isNotNull()).execute(); + } + + @Override + public long deleteAllByDeletedMemberOwner() { + return queryFactory + .delete(trip) + .where( + trip.member.id.in( + JPAExpressions.select(member.id) + .from(member) + .where(member.deletedAt.isNotNull()))) + .execute(); + } } diff --git a/src/main/resources/application-batch.yml b/src/main/resources/application-batch.yml new file mode 100644 index 0000000..1b92aab --- /dev/null +++ b/src/main/resources/application-batch.yml @@ -0,0 +1,12 @@ +spring: + config: + activate: + on-profile: "batch" + + batch: + job: + enabled: false # 서버 기동 시 배치 Job 자동 실행 금지 (수동/스케줄로만) + + jdbc: + initialize-schema: never # 메타테이블은 Flyway로만 관리 + table-prefix: BATCH_ # 배치 메타테이블 접두 diff --git a/src/main/resources/application-datasource.yml b/src/main/resources/application-datasource.yml index a1ab59a..da14de1 100644 --- a/src/main/resources/application-datasource.yml +++ b/src/main/resources/application-datasource.yml @@ -7,4 +7,4 @@ spring: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://${DB_HOST:localhost}:${DB_PORT:3306}/${DB_NAME:}?useSSL=false&allowPublicKeyRetrieval=true username: ${DB_USERNAME:root} - password: ${DB_PASSWORD:} \ No newline at end of file + password: ${DB_PASSWORD:} diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 72553e9..c7496a0 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -8,13 +8,6 @@ spring: ddl-auto: none # 스키마 변경 책임은 전부 Flyway로. Hibernate는 생성/수정 금지 open-in-view: false - flyway: - enabled: true # Flyway 켜기 - locations: classpath:db/migration # 마이그레이션 SQL 파일 위치(V1, V2, V3 등 여기서 읽음) - validate-on-migrate: true # 마이그레이션 전체 검증 - fail-on-missing-locations: true # 경로 누락 시 실패 - clean-disabled: true # Flyway Clean(스키마 삭제) 금지 - logging: level: org.springframework.orm.jpa: DEBUG diff --git a/src/main/resources/application-flyway.yml b/src/main/resources/application-flyway.yml new file mode 100644 index 0000000..3727629 --- /dev/null +++ b/src/main/resources/application-flyway.yml @@ -0,0 +1,12 @@ +spring: + config: + activate: + on-profile: "flyway" + + flyway: + enabled: true # Flyway 켜기 + locations: + - classpath:db/migration # 도메인 마이그레이션 SQL 파일 위치 + - classpath:db/migration-batch # 배치 마이그레이션 SQL 파일 위치 + validate-on-migrate: true # 마이그레이션 전체 검증 + fail-on-missing-locations: true # 경로 누락 시 실패 \ No newline at end of file diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index 388a36d..c33576c 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -6,21 +6,14 @@ spring: jpa: hibernate: ddl-auto: none # 스키마 변경 책임은 전부 Flyway로. Hibernate는 생성/수정 금지 - show-sql: true open-in-view: false + show-sql: true properties: hibernate: format_sql: true dialect: org.hibernate.dialect.MySQL8Dialect - flyway: - enabled: true # Flyway 켜기 - locations: classpath:db/migration # 마이그레이션 SQL 파일 위치(V1, V2, V3 등 여기서 읽음) - validate-on-migrate: true # 마이그레이션 전체 검증 - fail-on-missing-locations: true # 경로 누락 시 실패 - clean-disabled: false - logging: level: org.springframework.orm.jpa: DEBUG - org.springframework.transaction: DEBUG \ No newline at end of file + org.springframework.transaction: DEBUG diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 32a60c7..8fbaad8 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -7,10 +7,3 @@ spring: hibernate: ddl-auto: none # 스키마 변경 책임은 전부 Flyway로. Hibernate는 생성/수정 금지 open-in-view: false - - flyway: - enabled: true # Flyway 켜기 - locations: classpath:db/migration # 마이그레이션 SQL 파일 위치(V1, V2, V3 등 여기서 읽음) - validate-on-migrate: true # 마이그레이션 전체 검증 - fail-on-missing-locations: true # 경로 누락 시 실패 - clean-disabled: true # Flyway Clean(스키마 삭제) 금지 diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index e253547..b434f49 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -4,6 +4,8 @@ spring: - datasource - redis - security + - flyway + - batch sql: init: diff --git a/src/main/resources/db/migration-batch/V2__create_batch_meta_tables.sql b/src/main/resources/db/migration-batch/V2__create_batch_meta_tables.sql new file mode 100644 index 0000000..200572c --- /dev/null +++ b/src/main/resources/db/migration-batch/V2__create_batch_meta_tables.sql @@ -0,0 +1,96 @@ +CREATE TABLE BATCH_JOB_INSTANCE ( + JOB_INSTANCE_ID BIGINT NOT NULL PRIMARY KEY , + VERSION BIGINT , + JOB_NAME VARCHAR(100) NOT NULL, + JOB_KEY VARCHAR(32) NOT NULL, + constraint JOB_INST_UN unique (JOB_NAME, JOB_KEY) +) ENGINE=InnoDB; + +CREATE TABLE BATCH_JOB_EXECUTION ( + JOB_EXECUTION_ID BIGINT NOT NULL PRIMARY KEY , + VERSION BIGINT , + JOB_INSTANCE_ID BIGINT NOT NULL, + CREATE_TIME DATETIME(6) NOT NULL, + START_TIME DATETIME(6) DEFAULT NULL , + END_TIME DATETIME(6) DEFAULT NULL , + STATUS VARCHAR(10) , + EXIT_CODE VARCHAR(2500) , + EXIT_MESSAGE VARCHAR(2500) , + LAST_UPDATED DATETIME(6), + constraint JOB_INST_EXEC_FK foreign key (JOB_INSTANCE_ID) + references BATCH_JOB_INSTANCE(JOB_INSTANCE_ID) +) ENGINE=InnoDB; + +CREATE TABLE BATCH_JOB_EXECUTION_PARAMS ( + JOB_EXECUTION_ID BIGINT NOT NULL , + PARAMETER_NAME VARCHAR(100) NOT NULL , + PARAMETER_TYPE VARCHAR(100) NOT NULL , + PARAMETER_VALUE VARCHAR(2500) , + IDENTIFYING CHAR(1) NOT NULL , + constraint JOB_EXEC_PARAMS_FK foreign key (JOB_EXECUTION_ID) + references BATCH_JOB_EXECUTION(JOB_EXECUTION_ID) +) ENGINE=InnoDB; + +CREATE TABLE BATCH_STEP_EXECUTION ( + STEP_EXECUTION_ID BIGINT NOT NULL PRIMARY KEY , + VERSION BIGINT NOT NULL, + STEP_NAME VARCHAR(100) NOT NULL, + JOB_EXECUTION_ID BIGINT NOT NULL, + CREATE_TIME DATETIME(6) NOT NULL, + START_TIME DATETIME(6) DEFAULT NULL , + END_TIME DATETIME(6) DEFAULT NULL , + STATUS VARCHAR(10) , + COMMIT_COUNT BIGINT , + READ_COUNT BIGINT , + FILTER_COUNT BIGINT , + WRITE_COUNT BIGINT , + READ_SKIP_COUNT BIGINT , + WRITE_SKIP_COUNT BIGINT , + PROCESS_SKIP_COUNT BIGINT , + ROLLBACK_COUNT BIGINT , + EXIT_CODE VARCHAR(2500) , + EXIT_MESSAGE VARCHAR(2500) , + LAST_UPDATED DATETIME(6), + constraint JOB_EXEC_STEP_FK foreign key (JOB_EXECUTION_ID) + references BATCH_JOB_EXECUTION(JOB_EXECUTION_ID) +) ENGINE=InnoDB; + +CREATE TABLE BATCH_STEP_EXECUTION_CONTEXT ( + STEP_EXECUTION_ID BIGINT NOT NULL PRIMARY KEY, + SHORT_CONTEXT VARCHAR(2500) NOT NULL, + SERIALIZED_CONTEXT TEXT , + constraint STEP_EXEC_CTX_FK foreign key (STEP_EXECUTION_ID) + references BATCH_STEP_EXECUTION(STEP_EXECUTION_ID) +) ENGINE=InnoDB; + +CREATE TABLE BATCH_JOB_EXECUTION_CONTEXT ( + JOB_EXECUTION_ID BIGINT NOT NULL PRIMARY KEY, + SHORT_CONTEXT VARCHAR(2500) NOT NULL, + SERIALIZED_CONTEXT TEXT , + constraint JOB_EXEC_CTX_FK foreign key (JOB_EXECUTION_ID) + references BATCH_JOB_EXECUTION(JOB_EXECUTION_ID) +) ENGINE=InnoDB; + +CREATE TABLE BATCH_STEP_EXECUTION_SEQ ( + ID BIGINT NOT NULL, + UNIQUE_KEY CHAR(1) NOT NULL, + constraint UNIQUE_KEY_UN unique (UNIQUE_KEY) +) ENGINE=InnoDB; + +INSERT INTO BATCH_STEP_EXECUTION_SEQ (ID, UNIQUE_KEY) select * from (select 0 as ID, '0' as UNIQUE_KEY) as tmp where not exists(select * from BATCH_STEP_EXECUTION_SEQ); + +CREATE TABLE BATCH_JOB_EXECUTION_SEQ ( + ID BIGINT NOT NULL, + UNIQUE_KEY CHAR(1) NOT NULL, + constraint UNIQUE_KEY_UN unique (UNIQUE_KEY) +) ENGINE=InnoDB; + +INSERT INTO BATCH_JOB_EXECUTION_SEQ (ID, UNIQUE_KEY) select * from (select 0 as ID, '0' as UNIQUE_KEY) as tmp where not exists(select * from BATCH_JOB_EXECUTION_SEQ); + +CREATE TABLE BATCH_JOB_SEQ ( + ID BIGINT NOT NULL, + UNIQUE_KEY CHAR(1) NOT NULL, + constraint UNIQUE_KEY_UN unique (UNIQUE_KEY) +) ENGINE=InnoDB; + +INSERT INTO BATCH_JOB_SEQ (ID, UNIQUE_KEY) select * from (select 0 as ID, '0' as UNIQUE_KEY) as tmp where not exists(select * from BATCH_JOB_SEQ); diff --git a/src/test/java/com/ject/studytrip/member/application/service/MemberServiceTest.java b/src/test/java/com/ject/studytrip/member/application/service/MemberServiceTest.java index 87d8851..ab1b7e6 100644 --- a/src/test/java/com/ject/studytrip/member/application/service/MemberServiceTest.java +++ b/src/test/java/com/ject/studytrip/member/application/service/MemberServiceTest.java @@ -378,4 +378,35 @@ void shouldReturnRoleNameWhenMemberIdIsValid() { assertThat(result).isEqualTo(MEMBER_ROLE.name()); } } + + @Nested + @DisplayName("hardDeleteMembers 메서드는") + class HardDeleteMembers { + + @Test + @DisplayName("삭제된 멤버가 없으면 0을 반환한다.") + void shouldReturnZeroWhenDeletedMembersDoNotExist() { + // given + given(memberQueryRepository.deleteAllByDeletedAtIsNotNull()).willReturn(0L); + + // when + long result = memberService.hardDeleteMembers(); + + // then + assertThat(result).isEqualTo(0L); + } + + @Test + @DisplayName("삭제된 멤버가 있으면 해당 개수를 반환한다.") + void shouldReturnCountWhenDeletedMembersExist() { + // given + given(memberQueryRepository.deleteAllByDeletedAtIsNotNull()).willReturn(5L); + + // when + long result = memberService.hardDeleteMembers(); + + // then + assertThat(result).isEqualTo(5L); + } + } } diff --git a/src/test/java/com/ject/studytrip/mission/application/service/DailyMissionServiceTest.java b/src/test/java/com/ject/studytrip/mission/application/service/DailyMissionServiceTest.java index 586c089..3e81d9f 100644 --- a/src/test/java/com/ject/studytrip/mission/application/service/DailyMissionServiceTest.java +++ b/src/test/java/com/ject/studytrip/mission/application/service/DailyMissionServiceTest.java @@ -178,4 +178,97 @@ void shouldGetDailyMissionsByDailyGoalId() { assertThat(result.isEmpty()).isFalse(); } } + + @Nested + @DisplayName("hardDeleteDailyMissions 메서드는") + class HardDeleteDailyMissions { + + @Test + @DisplayName("삭제된 데일리 미션이 없으면 0을 반환한다.") + void shouldReturnZeroWhenDeletedDailyMissionsDoNotExist() { + // given + given(dailyMissionQueryRepository.deleteAllByDeletedAtIsNotNull()).willReturn(0L); + + // when + long result = dailyMissionService.hardDeleteDailyMissions(); + + // then + assertThat(result).isEqualTo(0L); + } + + @Test + @DisplayName("삭제된 데일리 미션이 있으면 해당 개수를 반환한다.") + void shouldReturnCountWhenDeletedDailyMissionsExist() { + // given + given(dailyMissionQueryRepository.deleteAllByDeletedAtIsNotNull()).willReturn(5L); + + // when + long result = dailyMissionService.hardDeleteDailyMissions(); + + // then + assertThat(result).isEqualTo(5L); + } + } + + @Nested + @DisplayName("hardDeleteDailyMissionsOwnedByDeletedMission 메서드는") + class HardDeleteDailyMissionsOwnedByDeletedMission { + + @Test + @DisplayName("삭제된 미션이 소유한 데일리 미션이 없으면 0을 반환한다.") + void shouldReturnZeroWhenDailyMissionsOwnedByDeletedMissionDoNotExist() { + // given + given(dailyMissionQueryRepository.deleteAllByDeletedMissionOwner()).willReturn(0L); + + // when + long result = dailyMissionService.hardDeleteDailyMissionsOwnedByDeletedMission(); + + // then + assertThat(result).isEqualTo(0L); + } + + @Test + @DisplayName("삭제된 미션이 소유한 데일리 미션이 있으면 해당 개수를 반환한다.") + void shouldReturnCountWhenDailyMissionsOwnedByDeletedMissionExist() { + // given + given(dailyMissionQueryRepository.deleteAllByDeletedMissionOwner()).willReturn(5L); + + // when + long result = dailyMissionService.hardDeleteDailyMissionsOwnedByDeletedMission(); + + // then + assertThat(result).isEqualTo(5L); + } + } + + @Nested + @DisplayName("hardDeleteDailyMissionsOwnedByDeletedDailyGoal 메서드는") + class HardDeleteDailyMissionsOwnedByDeletedDailyGoal { + + @Test + @DisplayName("삭제된 데일리 목표가 소유한 데일리 미션이 없으면 0을 반환한다.") + void shouldReturnZeroWhenDailyMissionsOwnedByDeletedDailyGoalDoNotExist() { + // given + given(dailyMissionQueryRepository.deleteAllByDeletedDailyGoalOwner()).willReturn(0L); + + // when + long result = dailyMissionService.hardDeleteDailyMissionsOwnedByDeletedDailyGoal(); + + // then + assertThat(result).isEqualTo(0L); + } + + @Test + @DisplayName("삭제된 데일리 목표가 소유한 데일리 미션이 있으면 해당 개수를 반환한다.") + void shouldReturnCountWhenDailyMissionsOwnedByDeletedDailyGoalExist() { + // given + given(dailyMissionQueryRepository.deleteAllByDeletedDailyGoalOwner()).willReturn(5L); + + // when + long result = dailyMissionService.hardDeleteDailyMissionsOwnedByDeletedDailyGoal(); + + // then + assertThat(result).isEqualTo(5L); + } + } } diff --git a/src/test/java/com/ject/studytrip/mission/application/service/MissionServiceTest.java b/src/test/java/com/ject/studytrip/mission/application/service/MissionServiceTest.java index 87100e3..473d914 100644 --- a/src/test/java/com/ject/studytrip/mission/application/service/MissionServiceTest.java +++ b/src/test/java/com/ject/studytrip/mission/application/service/MissionServiceTest.java @@ -415,4 +415,66 @@ void shouldPassWhenAllMissionsAreCompleted() { assertDoesNotThrow(() -> missionService.validateAllMissionsCompletedByStampId(stampId)); } } + + @Nested + @DisplayName("hardDeleteMissions 메서드는") + class HardDeleteMissions { + + @Test + @DisplayName("삭제된 미션이 없으면 0을 반환한다.") + void shouldReturnZeroWhenDeletedMissionsDoNotExist() { + // given + given(missionQueryRepository.deleteAllByDeletedAtIsNotNull()).willReturn(0L); + + // when + long result = missionService.hardDeleteMissions(); + + // then + assertThat(result).isEqualTo(0L); + } + + @Test + @DisplayName("삭제된 미션이 있으면 해당 개수를 반환한다.") + void shouldReturnCountWhenDeletedMissionsExist() { + // given + given(missionQueryRepository.deleteAllByDeletedAtIsNotNull()).willReturn(5L); + + // when + long result = missionService.hardDeleteMissions(); + + // then + assertThat(result).isEqualTo(5L); + } + } + + @Nested + @DisplayName("hardDeleteMissionsOwnedByDeletedStamp 메서드는") + class HardDeleteMissionsOwnedByDeletedStamp { + + @Test + @DisplayName("삭제된 스탬프가 소유한 미션이 없으면 0을 반환한다.") + void shouldReturnZeroWhenMissionsOwnedByDeletedStampDoNotExist() { + // given + given(missionQueryRepository.deleteAllByDeletedStampOwner()).willReturn(0L); + + // when + long result = missionService.hardDeleteMissionsOwnedByDeletedStamp(); + + // then + assertThat(result).isEqualTo(0L); + } + + @Test + @DisplayName("삭제된 스탬프가 소유한 미션이 있으면 해당 개수를 반환한다.") + void shouldReturnCountWhenMissionsOwnedByDeletedStampExist() { + // given + given(missionQueryRepository.deleteAllByDeletedStampOwner()).willReturn(5L); + + // when + long result = missionService.hardDeleteMissionsOwnedByDeletedStamp(); + + // then + assertThat(result).isEqualTo(5L); + } + } } diff --git a/src/test/java/com/ject/studytrip/pomodoro/application/service/PomodoroServiceTest.java b/src/test/java/com/ject/studytrip/pomodoro/application/service/PomodoroServiceTest.java index 2c1e5a3..72332b4 100644 --- a/src/test/java/com/ject/studytrip/pomodoro/application/service/PomodoroServiceTest.java +++ b/src/test/java/com/ject/studytrip/pomodoro/application/service/PomodoroServiceTest.java @@ -11,6 +11,7 @@ import com.ject.studytrip.member.fixture.MemberFixture; import com.ject.studytrip.pomodoro.domain.error.PomodoroErrorCode; import com.ject.studytrip.pomodoro.domain.model.Pomodoro; +import com.ject.studytrip.pomodoro.domain.repository.PomodoroQueryRepository; import com.ject.studytrip.pomodoro.domain.repository.PomodoroRepository; import com.ject.studytrip.pomodoro.fixture.PomodoroFixture; import com.ject.studytrip.pomodoro.presentation.dto.request.CreatePomodoroRequest; @@ -32,6 +33,7 @@ public class PomodoroServiceTest extends BaseUnitTest { @InjectMocks private PomodoroService pomodoroService; @Mock private PomodoroRepository pomodoroRepository; + @Mock private PomodoroQueryRepository pomodoroQueryRepository; private DailyGoal dailyGoal; private Pomodoro pomodoro; @@ -196,4 +198,66 @@ void shouldThrowExceptionWhenPomodoroIsDeleted() { .hasMessage(PomodoroErrorCode.POMODORO_ALREADY_DELETED.getMessage()); } } + + @Nested + @DisplayName("hardDeletePomodoros 메서드는") + class HardDeletePomodoros { + + @Test + @DisplayName("삭제된 뽀모도로가 없으면 0을 반환한다.") + void shouldReturnZeroWhenDeletedPomodorosDoNotExist() { + // given + given(pomodoroQueryRepository.deleteAllByDeletedAtIsNotNull()).willReturn(0L); + + // when + long result = pomodoroService.hardDeletePomodoros(); + + // then + assertThat(result).isEqualTo(0L); + } + + @Test + @DisplayName("삭제된 뽀모도로가 있으면 해당 개수를 반환한다.") + void shouldReturnCountWhenDeletedPomodorosExist() { + // given + given(pomodoroQueryRepository.deleteAllByDeletedAtIsNotNull()).willReturn(5L); + + // when + long result = pomodoroService.hardDeletePomodoros(); + + // then + assertThat(result).isEqualTo(5L); + } + } + + @Nested + @DisplayName("hardDeletePomodorosOwnedByDeletedDailyGoal 메서드는") + class HardDeletePomodorosOwnedByDeletedDailyGoal { + + @Test + @DisplayName("삭제된 데일리 목표가 소유한 뽀모도로가 없으면 0을 반환한다.") + void shouldReturnZeroWhenPomodorosOwnedByDeletedDailyGoal() { + // given + given(pomodoroQueryRepository.deleteAllByDeletedDailyGoalOwner()).willReturn(0L); + + // when + long result = pomodoroService.hardDeletePomodorosOwnedByDeletedDailyGoal(); + + // then + assertThat(result).isEqualTo(0L); + } + + @Test + @DisplayName("삭제된 데일리 목표가 소유한 뽀모도로가 있으면 해당 개수를 반환한다.") + void shouldReturnCountWhenPomodorosOwnedByDeletedDailyGoal() { + // given + given(pomodoroQueryRepository.deleteAllByDeletedDailyGoalOwner()).willReturn(5L); + + // when + long result = pomodoroService.hardDeletePomodorosOwnedByDeletedDailyGoal(); + + // then + assertThat(result).isEqualTo(5L); + } + } } diff --git a/src/test/java/com/ject/studytrip/stamp/application/service/StampServiceTest.java b/src/test/java/com/ject/studytrip/stamp/application/service/StampServiceTest.java index 337ce3b..f5431dd 100644 --- a/src/test/java/com/ject/studytrip/stamp/application/service/StampServiceTest.java +++ b/src/test/java/com/ject/studytrip/stamp/application/service/StampServiceTest.java @@ -649,4 +649,66 @@ void shouldPassWhenAllStampsAreCompleted() { assertDoesNotThrow(() -> stampService.validateAllStampsCompletedByTripId(tripId)); } } + + @Nested + @DisplayName("hardDeleteStamps 메서드는") + class HardDeleteStamps { + + @Test + @DisplayName("삭제된 스탬프가 없으면 0을 반환한다.") + void shouldReturnZeroWhenDeletedStampsDoNotExist() { + // given + given(stampQueryRepository.deleteAllByDeletedAtIsNotNull()).willReturn(0L); + + // when + long result = stampService.hardDeleteStamps(); + + // then + assertThat(result).isEqualTo(0L); + } + + @Test + @DisplayName("삭제된 스탬프가 있으면 해당 개수를 반환한다.") + void shouldReturnCountWhenDeletedStampsExist() { + // given + given(stampQueryRepository.deleteAllByDeletedTripOwner()).willReturn(5L); + + // when + long result = stampService.hardDeleteStampsOwnedByDeletedTrip(); + + // then + assertThat(result).isEqualTo(5L); + } + } + + @Nested + @DisplayName("hardDeleteStampsOwnedByDeletedTrip 메서드는") + class HardDeleteStampsOwnedByDeletedTrip { + + @Test + @DisplayName("삭제된 여행이 소유한 스탬프가 없으면 0을 반환한다.") + void shouldReturnZeroWhenStampsOwnedByDeletedTripDoNotExist() { + // given + given(stampQueryRepository.deleteAllByDeletedTripOwner()).willReturn(0L); + + // when + long result = stampService.hardDeleteStampsOwnedByDeletedTrip(); + + // then + assertThat(result).isEqualTo(0L); + } + + @Test + @DisplayName("삭제된 여행이 소유한 스탬프가 있으면 해당 개수를 반환한다.") + void shouldReturnCountWhenStampsOwnedByDeletedTripExist() { + // given + given(stampQueryRepository.deleteAllByDeletedTripOwner()).willReturn(5L); + + // when + long result = stampService.hardDeleteStampsOwnedByDeletedTrip(); + + // then + assertThat(result).isEqualTo(5L); + } + } } diff --git a/src/test/java/com/ject/studytrip/studylog/application/service/StudyLogDailyMissionServiceTest.java b/src/test/java/com/ject/studytrip/studylog/application/service/StudyLogDailyMissionServiceTest.java index 34143f8..c3d1eb8 100644 --- a/src/test/java/com/ject/studytrip/studylog/application/service/StudyLogDailyMissionServiceTest.java +++ b/src/test/java/com/ject/studytrip/studylog/application/service/StudyLogDailyMissionServiceTest.java @@ -120,4 +120,111 @@ void shouldReturnGroupedStudyLogDailyMissionMap() { .findStudyLogDailyMissionsGroupedByStudyLogId(studyLogIds); } } + + @Nested + @DisplayName("hardDeleteStudyLogDailyMissions 메서드는") + class HardDeleteStudyLogDailyMissions { + + @Test + @DisplayName("삭제된 StudyLogDailyMission이 없으면 0을 반환한다.") + void shouldReturnZeroWhenDeletedStudyLogDailyMissionsDoNotExist() { + // given + given(studyLogDailyMissionQueryRepository.deleteAllByDeletedAtIsNotNull()) + .willReturn(0L); + + // when + long result = studyLogDailyMissionService.hardDeleteStudyLogDailyMissions(); + + // then + assertThat(result).isEqualTo(0L); + } + + @Test + @DisplayName("삭제된 StudyLogDailyMission이 있으면 해당 개수를 반환한다.") + void shouldReturnCountWhenDeletedStudyLogDailyMissionsExist() { + // given + given(studyLogDailyMissionQueryRepository.deleteAllByDeletedAtIsNotNull()) + .willReturn(5L); + + // when + long result = studyLogDailyMissionService.hardDeleteStudyLogDailyMissions(); + + // then + assertThat(result).isEqualTo(5L); + } + } + + @Nested + @DisplayName("hardDeleteStudyLogDailyMissionsOwnedByDeletedDailyMission 메서드는") + class HardDeleteStudyLogDailyMissionsOwnedByDeletedDailyMission { + + @Test + @DisplayName("삭제된 데일리 미션이 소유한 StudyLogDailyMission이 없으면 0을 반환한다.") + void shouldReturnZeroWhenStudyLogDailyMissionsOwnedByDeletedDailyMissionDoNotExist() { + // given + given(studyLogDailyMissionQueryRepository.deleteAllByDeletedDailyMissionOwner()) + .willReturn(0L); + + // when + long result = + studyLogDailyMissionService + .hardDeleteStudyLogDailyMissionsOwnedByDeletedDailyMission(); + + // then + assertThat(result).isEqualTo(0L); + } + + @Test + @DisplayName("삭제된 데일리 미션이 소유한 StudyLogDailyMission이 있으면 해당 개수를 반환한다.") + void shouldReturnCountWhenStudyLogDailyMissionsOwnedByDeletedDailyMissionExist() { + // given + given(studyLogDailyMissionQueryRepository.deleteAllByDeletedDailyMissionOwner()) + .willReturn(5L); + + // when + long result = + studyLogDailyMissionService + .hardDeleteStudyLogDailyMissionsOwnedByDeletedDailyMission(); + + // then + assertThat(result).isEqualTo(5L); + } + } + + @Nested + @DisplayName("hardDeleteStudyLogDailyMissionsOwnedByDeletedStudyLog 메서드는") + class HardDeleteStudyLogDailyMissionsOwnedByDeletedStudyLog { + + @Test + @DisplayName("삭제된 학습 로그가 소유한 StudyLogDailyMission이 없으면 0을 반환한다.") + void shouldReturnZeroWhenDailyMissionsOwnedByDeletedStudyLogDoNotExist() { + // given + given(studyLogDailyMissionQueryRepository.deleteAllByDeletedStudyLogOwner()) + .willReturn(0L); + + // when + long result = + studyLogDailyMissionService + .hardDeleteStudyLogDailyMissionsOwnedByDeletedStudyLog(); + + // then + assertThat(result).isEqualTo(0L); + } + + @Test + @DisplayName("삭제된 학습 로그가 소유한 StudyLogDailyMission이 있으면 해당 개수를 반환한다.") + void shouldReturnCountWhenStudyLogDailyMissionsOwnedByDeletedStudyLogExist() { + // given + given(studyLogDailyMissionQueryRepository.deleteAllByDeletedStudyLogOwner()) + .willReturn(5L); + + // when + long result = + studyLogDailyMissionService + .hardDeleteStudyLogDailyMissionsOwnedByDeletedStudyLog(); + + // then + assertThat(result).isEqualTo(5L); + } + } } diff --git a/src/test/java/com/ject/studytrip/studylog/application/service/StudyLogServiceTest.java b/src/test/java/com/ject/studytrip/studylog/application/service/StudyLogServiceTest.java index 1a6e1a8..d75f2a7 100644 --- a/src/test/java/com/ject/studytrip/studylog/application/service/StudyLogServiceTest.java +++ b/src/test/java/com/ject/studytrip/studylog/application/service/StudyLogServiceTest.java @@ -144,4 +144,97 @@ void shouldReturnStudyLogsByTripIdWithSlice() { assertThat(result.getContent().get(1)).isEqualTo(studyLog2); } } + + @Nested + @DisplayName("hardDeleteStudyLogs 메서드는") + class HardDeleteStudyLogs { + + @Test + @DisplayName("삭제된 학습 로그가 없으면 0을 반환한다.") + void shouldReturnZeroWhenDeletedStudyLogsDoNotExist() { + // given + given(studyLogQueryRepository.deleteAllByDeletedAtIsNotNull()).willReturn(0L); + + // when + long result = studyLogService.hardDeleteStudyLogs(); + + // then + assertThat(result).isEqualTo(0L); + } + + @Test + @DisplayName("삭제된 학습 로그가 있으면 해당 개수를 반환한다.") + void shouldReturnCountWhenDeletedStudyLogsExist() { + // given + given(studyLogQueryRepository.deleteAllByDeletedAtIsNotNull()).willReturn(5L); + + // when + long result = studyLogService.hardDeleteStudyLogs(); + + // then + assertThat(result).isEqualTo(5L); + } + } + + @Nested + @DisplayName("hardDeleteStudyLogsOwnedByDeletedMember 메서드는") + class HardDeleteStudyLogsOwnedByDeletedMember { + + @Test + @DisplayName("삭제된 멤버가 소유한 학습 로그가 없으면 0을 반환한다.") + void shouldReturnZeroWhenStudyLogsOwnedByDeletedMemberDoNotExist() { + // given + given(studyLogQueryRepository.deleteAllByDeletedMemberOwner()).willReturn(0L); + + // when + long result = studyLogService.hardDeleteStudyLogsOwnedByDeletedMember(); + + // then + assertThat(result).isEqualTo(0L); + } + + @Test + @DisplayName("삭제된 멤버가 소유한 학습 로그가 있으면 해당 개수를 반환한다.") + void shouldReturnCountWhenStudyLogsOwnedByDeletedMemberExist() { + // given + given(studyLogQueryRepository.deleteAllByDeletedMemberOwner()).willReturn(5L); + + // when + long result = studyLogService.hardDeleteStudyLogsOwnedByDeletedMember(); + + // then + assertThat(result).isEqualTo(5L); + } + } + + @Nested + @DisplayName("hardDeleteStudyLogsOwnedByDeletedDailyGoal 메서드는") + class HardDeleteStudyLogsOwnedByDeletedDailyGoal { + + @Test + @DisplayName("삭제된 데일리 목표가 소유한 학습 로그가 없으면 0을 반환한다.") + void shouldReturnZeroWhenStudyLogsOwnedByDeletedDailyGoalDoNotExist() { + // given + given(studyLogQueryRepository.deleteAllByDeletedDailyGoalOwner()).willReturn(0L); + + // when + long result = studyLogService.hardDeleteStudyLogsOwnedByDeletedDailyGoal(); + + // then + assertThat(result).isEqualTo(0L); + } + + @Test + @DisplayName("삭제된 데일리 목표가 소유한 학습 로그가 있으면 해당 개수를 반환한다.") + void shouldReturnCountWhenStudyLogsOwnedByDeletedDailyGoalExist() { + // given + given(studyLogQueryRepository.deleteAllByDeletedDailyGoalOwner()).willReturn(5L); + + // when + long result = studyLogService.hardDeleteStudyLogsOwnedByDeletedDailyGoal(); + + // then + assertThat(result).isEqualTo(5L); + } + } } diff --git a/src/test/java/com/ject/studytrip/trip/application/service/DailyGoalServiceTest.java b/src/test/java/com/ject/studytrip/trip/application/service/DailyGoalServiceTest.java index b800a80..7f34410 100644 --- a/src/test/java/com/ject/studytrip/trip/application/service/DailyGoalServiceTest.java +++ b/src/test/java/com/ject/studytrip/trip/application/service/DailyGoalServiceTest.java @@ -13,6 +13,7 @@ import com.ject.studytrip.trip.domain.model.DailyGoal; import com.ject.studytrip.trip.domain.model.Trip; import com.ject.studytrip.trip.domain.model.TripCategory; +import com.ject.studytrip.trip.domain.repository.DailyGoalQueryRepository; import com.ject.studytrip.trip.domain.repository.DailyGoalRepository; import com.ject.studytrip.trip.fixture.DailyGoalFixture; import com.ject.studytrip.trip.fixture.TripFixture; @@ -29,6 +30,7 @@ public class DailyGoalServiceTest extends BaseUnitTest { @InjectMocks private DailyGoalService dailyGoalService; @Mock private DailyGoalRepository dailyGoalRepository; + @Mock private DailyGoalQueryRepository dailyGoalQueryRepository; private Member member; private Trip trip; @@ -138,4 +140,66 @@ void shouldThrowExceptionWhenDeletedDailyGoal() { .hasMessage(DailyGoalErrorCode.DAILY_GOAL_ALREADY_DELETED.getMessage()); } } + + @Nested + @DisplayName("hardDeleteDailyGoals 메서드는") + class HardDeleteDailyGoals { + + @Test + @DisplayName("삭제된 데일리 목표가 없으면 0을 반환한다.") + void shouldReturnZeroWhenDeletedDailyGoalsDoNotExist() { + // given + given(dailyGoalQueryRepository.deleteAllByDeletedAtIsNotNull()).willReturn(0L); + + // when + long result = dailyGoalService.hardDeleteDailyGoals(); + + // then + assertThat(result).isEqualTo(0L); + } + + @Test + @DisplayName("삭제된 데일리 목표가 있으면 해당 개수를 반환한다.") + void shouldReturnCountWhenDeletedDailyGoalsExist() { + // given + given(dailyGoalQueryRepository.deleteAllByDeletedAtIsNotNull()).willReturn(5L); + + // when + long result = dailyGoalService.hardDeleteDailyGoals(); + + // then + assertThat(result).isEqualTo(5L); + } + } + + @Nested + @DisplayName("hardDeleteDailyGoalsOwnedByDeletedTrip 메서드는") + class HardDeleteDailyGoalsOwnedByDeletedTrip { + + @Test + @DisplayName("삭제된 여행이 소유한 데일리 목표가 없으면 0을 반환한다.") + void shouldReturnZeroWhenDailyGoalsOwnedByDeletedTripDoNotExist() { + // given + given(dailyGoalQueryRepository.deleteAllByDeletedTripOwner()).willReturn(0L); + + // when + long result = dailyGoalService.hardDeleteDailyGoalsOwnedByDeletedTrip(); + + // then + assertThat(result).isEqualTo(0L); + } + + @Test + @DisplayName("삭제된 여행이 소유한 데일리 목표가 있으면 해당 개수를 반환한다.") + void shouldReturnCountWhenDailyGoalsOwnedByDeletedTripExist() { + // given + given(dailyGoalQueryRepository.deleteAllByDeletedTripOwner()).willReturn(5L); + + // when + long result = dailyGoalService.hardDeleteDailyGoalsOwnedByDeletedTrip(); + + // then + assertThat(result).isEqualTo(5L); + } + } } diff --git a/src/test/java/com/ject/studytrip/trip/application/service/TripServiceTest.java b/src/test/java/com/ject/studytrip/trip/application/service/TripServiceTest.java index f1b0116..6c31a78 100644 --- a/src/test/java/com/ject/studytrip/trip/application/service/TripServiceTest.java +++ b/src/test/java/com/ject/studytrip/trip/application/service/TripServiceTest.java @@ -389,4 +389,66 @@ void shouldIncreaseCompletedStamps() { assertThat(trip.getCompletedStamps()).isEqualTo(1); } } + + @Nested + @DisplayName("hardDeleteTrips 메서드는") + class HardDeleteTrips { + + @Test + @DisplayName("삭제된 여행이 없으면 0을 반환한다.") + void shouldReturnZeroWhenDeletedTripsDoNotExist() { + // given + given(tripQueryRepository.deleteAllByDeletedAtIsNotNull()).willReturn(0L); + + // when + long result = tripService.hardDeleteTrips(); + + // then + assertThat(result).isEqualTo(0L); + } + + @Test + @DisplayName("삭제된 여행이 있으면 해당 개수를 반환한다.") + void shouldReturnCountWhenDeletedTripsExist() { + // given + given(tripQueryRepository.deleteAllByDeletedAtIsNotNull()).willReturn(5L); + + // when + long result = tripService.hardDeleteTrips(); + + // then + assertThat(result).isEqualTo(5L); + } + } + + @Nested + @DisplayName("hardDeleteTripsOwnedByDeletedMember 메서드는") + class HardDeleteTripsOwnedByDeletedMember { + + @Test + @DisplayName("삭제된 멤버가 소유한 여행이 없으면 0을 반환한다.") + void shouldReturnZeroWhenTripsOwnedByDeletedMemberDoNotExist() { + // given + given(tripQueryRepository.deleteAllByDeletedMemberOwner()).willReturn(0L); + + // when + long result = tripService.hardDeleteTripsOwnedByDeletedMember(); + + // then + assertThat(result).isEqualTo(0L); + } + + @Test + @DisplayName("삭제된 멤버가 소유한 여행이 있으면 해당 개수를 반환한다.") + void shouldReturnCountWhenTripsOwnedByDeletedMemberExist() { + // given + given(tripQueryRepository.deleteAllByDeletedMemberOwner()).willReturn(5L); + + // when + long result = tripService.hardDeleteTripsOwnedByDeletedMember(); + + // then + assertThat(result).isEqualTo(5L); + } + } } diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml index acd769a..6c744a1 100644 --- a/src/test/resources/application-test.yml +++ b/src/test/resources/application-test.yml @@ -6,16 +6,9 @@ spring: jpa: hibernate: ddl-auto: none # 스키마 변경 책임은 전부 Flyway로. Hibernate는 생성/수정 금지 - show-sql: true open-in-view: false + show-sql: true properties: hibernate: format_sql: true dialect: org.hibernate.dialect.MySQL8Dialect - - flyway: - enabled: true # Flyway 켜기 - locations: classpath:db/migration # 마이그레이션 SQL 파일 위치(V1, V2, V3 등 여기서 읽음) - validate-on-migrate: true # 마이그레이션 전체 검증 - fail-on-missing-locations: true # 경로 누락 시 실패 - clean-disabled: false