From a7f4ce05385bdb36a9854248d135f639926fb9ca Mon Sep 17 00:00:00 2001 From: jaekwan Date: Sun, 10 May 2026 14:07:48 +0900 Subject: [PATCH 01/16] =?UTF-8?q?feat:=20=EC=97=B4=EB=A0=A4=EC=9E=88?= =?UTF-8?q?=EB=8A=94=20=EC=84=A4=EB=AC=B8=20=EC=88=98=20=EB=B0=8F=20?= =?UTF-8?q?=EC=B5=9C=EA=B3=A0=20=EC=B0=B8=EC=97=AC=20=EB=B3=B4=EC=83=81=20?= =?UTF-8?q?=EC=BD=94=EC=9D=B8=20=EC=A1=B0=ED=9A=8C=20API=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../survey/controller/SurveyStatsController.java | 11 +++++++++++ .../domain/survey/model/dto/OpenSurveyStats.java | 10 ++++++++++ .../model/response/OpenSurveyStatsResponse.java | 12 ++++++++++++ .../survey/repository/SurveyRepository.java | 2 ++ .../survey/repository/SurveyRepositoryImpl.java | 15 +++++++++++++++ .../domain/survey/service/query/SurveyQuery.java | 2 ++ .../survey/service/query/SurveyQueryService.java | 6 ++++++ 7 files changed, 58 insertions(+) create mode 100644 src/main/java/OneQ/OnSurvey/domain/survey/model/dto/OpenSurveyStats.java create mode 100644 src/main/java/OneQ/OnSurvey/domain/survey/model/response/OpenSurveyStatsResponse.java diff --git a/src/main/java/OneQ/OnSurvey/domain/survey/controller/SurveyStatsController.java b/src/main/java/OneQ/OnSurvey/domain/survey/controller/SurveyStatsController.java index e7f12677..ebcb6e38 100644 --- a/src/main/java/OneQ/OnSurvey/domain/survey/controller/SurveyStatsController.java +++ b/src/main/java/OneQ/OnSurvey/domain/survey/controller/SurveyStatsController.java @@ -1,8 +1,11 @@ package OneQ.OnSurvey.domain.survey.controller; import OneQ.OnSurvey.domain.survey.model.dto.GlobalStats; +import OneQ.OnSurvey.domain.survey.model.dto.OpenSurveyStats; import OneQ.OnSurvey.domain.survey.model.response.GlobalStatsResponse; +import OneQ.OnSurvey.domain.survey.model.response.OpenSurveyStatsResponse; import OneQ.OnSurvey.domain.survey.service.SurveyGlobalStatsService; +import OneQ.OnSurvey.domain.survey.service.query.SurveyQuery; import OneQ.OnSurvey.global.common.response.SuccessResponse; import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; @@ -16,6 +19,7 @@ public class SurveyStatsController { private final SurveyGlobalStatsService surveyGlobalStatsService; + private final SurveyQuery surveyQuery; @GetMapping("/global-stats") @Operation(summary = "전체 설문 전역 통계 조회", description = "전체 설문에 대한 총 목표 수/참여자 수/프로모션 지급자 수/일간 활성 사용자 수를 반환합니다.") @@ -23,4 +27,11 @@ public SuccessResponse getGlobalStats() { GlobalStats stats = surveyGlobalStatsService.getStats(); return SuccessResponse.ok(GlobalStatsResponse.from(stats)); } + + @GetMapping("/open-stats") + @Operation(summary = "열린 설문 통계 조회", description = "현재 진행 중인 설문 수와 가장 높은 참여 보상 코인 금액을 반환합니다.") + public SuccessResponse getOpenStats() { + OpenSurveyStats stats = surveyQuery.getOpenSurveyStats(); + return SuccessResponse.ok(OpenSurveyStatsResponse.from(stats)); + } } diff --git a/src/main/java/OneQ/OnSurvey/domain/survey/model/dto/OpenSurveyStats.java b/src/main/java/OneQ/OnSurvey/domain/survey/model/dto/OpenSurveyStats.java new file mode 100644 index 00000000..dc3069fe --- /dev/null +++ b/src/main/java/OneQ/OnSurvey/domain/survey/model/dto/OpenSurveyStats.java @@ -0,0 +1,10 @@ +package OneQ.OnSurvey.domain.survey.model.dto; + +public record OpenSurveyStats( + Long openSurveyCount, + Integer maxRewardCoin +) { + public static OpenSurveyStats of(Long openSurveyCount, Integer maxRewardCoin) { + return new OpenSurveyStats(openSurveyCount, maxRewardCoin != null ? maxRewardCoin : 0); + } +} diff --git a/src/main/java/OneQ/OnSurvey/domain/survey/model/response/OpenSurveyStatsResponse.java b/src/main/java/OneQ/OnSurvey/domain/survey/model/response/OpenSurveyStatsResponse.java new file mode 100644 index 00000000..cf0af95f --- /dev/null +++ b/src/main/java/OneQ/OnSurvey/domain/survey/model/response/OpenSurveyStatsResponse.java @@ -0,0 +1,12 @@ +package OneQ.OnSurvey.domain.survey.model.response; + +import OneQ.OnSurvey.domain.survey.model.dto.OpenSurveyStats; + +public record OpenSurveyStatsResponse( + Long openSurveyCount, + Integer maxRewardCoin +) { + public static OpenSurveyStatsResponse from(OpenSurveyStats stats) { + return new OpenSurveyStatsResponse(stats.openSurveyCount(), stats.maxRewardCoin()); + } +} diff --git a/src/main/java/OneQ/OnSurvey/domain/survey/repository/SurveyRepository.java b/src/main/java/OneQ/OnSurvey/domain/survey/repository/SurveyRepository.java index cc83e59d..621e0cfe 100644 --- a/src/main/java/OneQ/OnSurvey/domain/survey/repository/SurveyRepository.java +++ b/src/main/java/OneQ/OnSurvey/domain/survey/repository/SurveyRepository.java @@ -5,6 +5,7 @@ import OneQ.OnSurvey.domain.survey.entity.Survey; import OneQ.OnSurvey.domain.survey.model.SurveyStatus; import OneQ.OnSurvey.domain.survey.model.dto.OngoingSurveyStats; +import OneQ.OnSurvey.domain.survey.model.dto.OpenSurveyStats; import OneQ.OnSurvey.domain.survey.model.dto.SurveyDetailData; import OneQ.OnSurvey.domain.survey.model.dto.SurveyListView; import OneQ.OnSurvey.domain.survey.model.dto.SurveySearchQuery; @@ -37,4 +38,5 @@ Slice getSurveyListWithEligibility( List closeDueSurveys(); List findOngoingSurveys(); + OpenSurveyStats findOpenSurveyStats(); } diff --git a/src/main/java/OneQ/OnSurvey/domain/survey/repository/SurveyRepositoryImpl.java b/src/main/java/OneQ/OnSurvey/domain/survey/repository/SurveyRepositoryImpl.java index fe1a13ac..21b753d0 100644 --- a/src/main/java/OneQ/OnSurvey/domain/survey/repository/SurveyRepositoryImpl.java +++ b/src/main/java/OneQ/OnSurvey/domain/survey/repository/SurveyRepositoryImpl.java @@ -9,6 +9,7 @@ import OneQ.OnSurvey.domain.survey.model.Residence; import OneQ.OnSurvey.domain.survey.model.SurveyStatus; import OneQ.OnSurvey.domain.survey.model.dto.OngoingSurveyStats; +import OneQ.OnSurvey.domain.survey.model.dto.OpenSurveyStats; import OneQ.OnSurvey.domain.survey.model.dto.SurveyDetailData; import OneQ.OnSurvey.domain.survey.model.dto.SurveyListView; import OneQ.OnSurvey.domain.survey.model.dto.SurveySearchQuery; @@ -319,4 +320,18 @@ public List findOngoingSurveys() { .orderBy(survey.id.desc()) .fetch(); } + + @Override + public OpenSurveyStats findOpenSurveyStats() { + Tuple result = jpaQueryFactory + .select(survey.count(), surveyInfo.promotionAmount.max()) + .from(survey) + .leftJoin(surveyInfo).on(survey.id.eq(surveyInfo.surveyId)) + .where(survey.status.eq(SurveyStatus.ONGOING)) + .fetchOne(); + + Long count = result != null ? result.get(survey.count()) : 0L; + Integer maxCoin = result != null ? result.get(surveyInfo.promotionAmount.max()) : null; + return OpenSurveyStats.of(count != null ? count : 0L, maxCoin); + } } diff --git a/src/main/java/OneQ/OnSurvey/domain/survey/service/query/SurveyQuery.java b/src/main/java/OneQ/OnSurvey/domain/survey/service/query/SurveyQuery.java index ea3f5614..ad53bf23 100644 --- a/src/main/java/OneQ/OnSurvey/domain/survey/service/query/SurveyQuery.java +++ b/src/main/java/OneQ/OnSurvey/domain/survey/service/query/SurveyQuery.java @@ -4,6 +4,7 @@ import OneQ.OnSurvey.domain.survey.entity.Survey; import OneQ.OnSurvey.domain.survey.model.SurveyStatus; import OneQ.OnSurvey.domain.survey.model.dto.OngoingSurveyStats; +import OneQ.OnSurvey.domain.survey.model.dto.OpenSurveyStats; import OneQ.OnSurvey.domain.survey.model.dto.ScreeningViewData; import OneQ.OnSurvey.domain.survey.model.dto.SurveyDetailData; import OneQ.OnSurvey.domain.survey.model.dto.SurveyListView; @@ -47,6 +48,7 @@ ParticipationScreeningListResponse getScreeningList( // 외부 PORT Page getPagedSurveyListViewByQuery(Pageable pageable, SurveySearchQuery query); List getOngoingSurveyStats(); + OpenSurveyStats getOpenSurveyStats(); SurveyDetailData getSurveyDetailById(Long surveyId); ScreeningViewData getScreeningIntroBySurveyId(Long surveyId); List getSectionDtoListBySurveyId(Long surveyId); diff --git a/src/main/java/OneQ/OnSurvey/domain/survey/service/query/SurveyQueryService.java b/src/main/java/OneQ/OnSurvey/domain/survey/service/query/SurveyQueryService.java index 3dec0a24..2daa19ee 100644 --- a/src/main/java/OneQ/OnSurvey/domain/survey/service/query/SurveyQueryService.java +++ b/src/main/java/OneQ/OnSurvey/domain/survey/service/query/SurveyQueryService.java @@ -17,6 +17,7 @@ import OneQ.OnSurvey.domain.survey.model.Residence; import OneQ.OnSurvey.domain.survey.model.SurveyStatus; import OneQ.OnSurvey.domain.survey.model.dto.OngoingSurveyStats; +import OneQ.OnSurvey.domain.survey.model.dto.OpenSurveyStats; import OneQ.OnSurvey.domain.survey.model.dto.ScreeningIntroData; import OneQ.OnSurvey.domain.survey.model.dto.ScreeningViewData; import OneQ.OnSurvey.domain.survey.model.dto.SurveyDetailData; @@ -531,4 +532,9 @@ public List getSectionDtoListBySurveyId(Long surveyId) { public List getOngoingSurveyStats() { return surveyRepository.findOngoingSurveys(); } + + @Override + public OpenSurveyStats getOpenSurveyStats() { + return surveyRepository.findOpenSurveyStats(); + } } From 5a2028f8d783221cfb08366c7854972335be2463 Mon Sep 17 00:00:00 2001 From: jaekwan Date: Sun, 10 May 2026 14:14:47 +0900 Subject: [PATCH 02/16] =?UTF-8?q?refactor:=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EB=A6=AC=EB=B7=B0=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/survey/model/dto/OpenSurveyStats.java | 5 ++++- .../domain/survey/repository/SurveyRepositoryImpl.java | 9 ++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/main/java/OneQ/OnSurvey/domain/survey/model/dto/OpenSurveyStats.java b/src/main/java/OneQ/OnSurvey/domain/survey/model/dto/OpenSurveyStats.java index dc3069fe..d7c83b30 100644 --- a/src/main/java/OneQ/OnSurvey/domain/survey/model/dto/OpenSurveyStats.java +++ b/src/main/java/OneQ/OnSurvey/domain/survey/model/dto/OpenSurveyStats.java @@ -5,6 +5,9 @@ public record OpenSurveyStats( Integer maxRewardCoin ) { public static OpenSurveyStats of(Long openSurveyCount, Integer maxRewardCoin) { - return new OpenSurveyStats(openSurveyCount, maxRewardCoin != null ? maxRewardCoin : 0); + return new OpenSurveyStats( + openSurveyCount != null ? openSurveyCount : 0L, + maxRewardCoin != null ? maxRewardCoin : 0 + ); } } diff --git a/src/main/java/OneQ/OnSurvey/domain/survey/repository/SurveyRepositoryImpl.java b/src/main/java/OneQ/OnSurvey/domain/survey/repository/SurveyRepositoryImpl.java index 21b753d0..42a6935c 100644 --- a/src/main/java/OneQ/OnSurvey/domain/survey/repository/SurveyRepositoryImpl.java +++ b/src/main/java/OneQ/OnSurvey/domain/survey/repository/SurveyRepositoryImpl.java @@ -330,8 +330,11 @@ public OpenSurveyStats findOpenSurveyStats() { .where(survey.status.eq(SurveyStatus.ONGOING)) .fetchOne(); - Long count = result != null ? result.get(survey.count()) : 0L; - Integer maxCoin = result != null ? result.get(surveyInfo.promotionAmount.max()) : null; - return OpenSurveyStats.of(count != null ? count : 0L, maxCoin); + if (result == null) { + return OpenSurveyStats.of(0L, null); + } + Long count = result.get(survey.count()); + Integer maxCoin = result.get(surveyInfo.promotionAmount.max()); + return OpenSurveyStats.of(count, maxCoin); } } From 813b491787442855846c24bcbb2e36cced0ca094 Mon Sep 17 00:00:00 2001 From: jaekwan Date: Sun, 10 May 2026 14:28:27 +0900 Subject: [PATCH 03/16] =?UTF-8?q?fix:=20/v1/surveys/open-stats=20=EC=9D=B8?= =?UTF-8?q?=EC=A6=9D=20=ED=95=84=ED=84=B0=20=ED=97=88=EC=9A=A9=20=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../OneQ/OnSurvey/global/common/config/SecurityConfig.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/OneQ/OnSurvey/global/common/config/SecurityConfig.java b/src/main/java/OneQ/OnSurvey/global/common/config/SecurityConfig.java index e0688dc1..cf1ecd89 100644 --- a/src/main/java/OneQ/OnSurvey/global/common/config/SecurityConfig.java +++ b/src/main/java/OneQ/OnSurvey/global/common/config/SecurityConfig.java @@ -47,7 +47,8 @@ public class SecurityConfig { "/auth/toss/login", "/auth/reissue", "/connect-out", - "/toss/promotion/recheck-pending" + "/toss/promotion/recheck-pending", + "/v1/surveys/open-stats" }; @Bean From 1ce6463f9b1797fc6797b1a58509ee6fbf7ed0bf Mon Sep 17 00:00:00 2001 From: jaekwan Date: Sun, 10 May 2026 14:39:16 +0900 Subject: [PATCH 04/16] =?UTF-8?q?chore:=20open-stats=20AuthFilter=EC=97=90?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/OneQ/OnSurvey/global/auth/filter/AuthFilter.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/OneQ/OnSurvey/global/auth/filter/AuthFilter.java b/src/main/java/OneQ/OnSurvey/global/auth/filter/AuthFilter.java index 3bcc3b8d..cb8c444e 100644 --- a/src/main/java/OneQ/OnSurvey/global/auth/filter/AuthFilter.java +++ b/src/main/java/OneQ/OnSurvey/global/auth/filter/AuthFilter.java @@ -45,6 +45,7 @@ protected boolean shouldNotFilter(HttpServletRequest req) { || path.startsWith("/swagger-ui/") || path.startsWith("/v3/api-docs") || path.equals("/toss/promotion/recheck-pending") + || path.equals("/v1/surveys/open-stats") || path.startsWith("/v1/bo"); } From cd118e8ed3a6f1807f1dec8bce5e67ea71ced176 Mon Sep 17 00:00:00 2001 From: jaekwan Date: Sun, 10 May 2026 15:06:00 +0900 Subject: [PATCH 05/16] =?UTF-8?q?test:=20=EC=88=9C=EC=88=98=20=EB=B9=84?= =?UTF-8?q?=EC=A6=88=EB=8B=88=EC=8A=A4=20=EB=A1=9C=EC=A7=81=20=EB=8B=A8?= =?UTF-8?q?=EC=9C=84=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?(PR=201/9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DefaultSurveyRefundPolicy: 환불 금액 계산 로직 8개 케이스 - QuestionConverter: DTO↔Entity 변환 로직 전 문항 타입 커버 (10개) - SurveyExportService: CSV 빌딩 로직 13개 케이스 (권한, BOM, 이스케이프 등) - DiscountCodeCommandService: 할인 코드 생성 및 중복 재시도 6개 케이스 --- gradlew | 0 .../DiscountCodeCommandServiceTest.java | 128 +++++++ .../service/QuestionConverterTest.java | 257 ++++++++++++++ .../export/SurveyExportServiceTest.java | 331 ++++++++++++++++++ .../refund/DefaultSurveyRefundPolicyTest.java | 156 +++++++++ 5 files changed, 872 insertions(+) mode change 100644 => 100755 gradlew create mode 100644 src/test/java/OneQ/OnSurvey/domain/discount/service/DiscountCodeCommandServiceTest.java create mode 100644 src/test/java/OneQ/OnSurvey/domain/question/service/QuestionConverterTest.java create mode 100644 src/test/java/OneQ/OnSurvey/domain/survey/service/export/SurveyExportServiceTest.java create mode 100644 src/test/java/OneQ/OnSurvey/domain/survey/service/refund/DefaultSurveyRefundPolicyTest.java diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/src/test/java/OneQ/OnSurvey/domain/discount/service/DiscountCodeCommandServiceTest.java b/src/test/java/OneQ/OnSurvey/domain/discount/service/DiscountCodeCommandServiceTest.java new file mode 100644 index 00000000..975e1d41 --- /dev/null +++ b/src/test/java/OneQ/OnSurvey/domain/discount/service/DiscountCodeCommandServiceTest.java @@ -0,0 +1,128 @@ +package OneQ.OnSurvey.domain.discount.service; + +import OneQ.OnSurvey.domain.discount.entity.DiscountCode; +import OneQ.OnSurvey.domain.discount.model.request.CreateDiscountCodeRequest; +import OneQ.OnSurvey.domain.discount.model.response.DiscountCodeResponse; +import OneQ.OnSurvey.domain.discount.repository.DiscountCodeRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.RepeatedTest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.LocalDate; +import java.util.HashSet; +import java.util.Set; +import java.util.regex.Pattern; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class DiscountCodeCommandServiceTest { + + private static final Pattern CODE_PATTERN = Pattern.compile("[A-Z0-9]{6}"); + + @Mock + private DiscountCodeRepository discountCodeRepository; + + @InjectMocks + private DiscountCodeCommandService discountCodeCommandService; + + @Test + @DisplayName("할인 코드 생성 성공 - 6자리 대문자+숫자 코드 반환") + void create_success() { + CreateDiscountCodeRequest request = new CreateDiscountCodeRequest( + "OnSurvey", LocalDate.of(2026, 12, 31) + ); + given(discountCodeRepository.existsByCode(anyString())).willReturn(false); + given(discountCodeRepository.save(any(DiscountCode.class))) + .willAnswer(inv -> inv.getArgument(0)); + + DiscountCodeResponse response = discountCodeCommandService.create(request); + + assertThat(response.organizationName()).isEqualTo("OnSurvey"); + assertThat(response.code()).hasSize(6); + assertThat(CODE_PATTERN.matcher(response.code()).matches()).isTrue(); + assertThat(response.expiredAt()).isEqualTo(LocalDate.of(2026, 12, 31)); + } + + @Test + @DisplayName("코드 중복 시 재생성 후 저장") + void create_codeConflict_retries() { + CreateDiscountCodeRequest request = new CreateDiscountCodeRequest( + "TestOrg", LocalDate.of(2027, 1, 1) + ); + // 처음 두 번은 중복, 세 번째에 성공 + given(discountCodeRepository.existsByCode(anyString())) + .willReturn(true, true, false); + given(discountCodeRepository.save(any(DiscountCode.class))) + .willAnswer(inv -> inv.getArgument(0)); + + DiscountCodeResponse response = discountCodeCommandService.create(request); + + assertThat(response.code()).hasSize(6); + verify(discountCodeRepository, times(3)).existsByCode(anyString()); + } + + @Test + @DisplayName("저장된 엔티티의 조직명이 요청값과 동일") + void create_savedEntity_hasCorrectOrgName() { + String orgName = "학회명"; + LocalDate expiredAt = LocalDate.of(2026, 6, 30); + CreateDiscountCodeRequest request = new CreateDiscountCodeRequest(orgName, expiredAt); + + given(discountCodeRepository.existsByCode(anyString())).willReturn(false); + ArgumentCaptor captor = ArgumentCaptor.forClass(DiscountCode.class); + given(discountCodeRepository.save(captor.capture())) + .willAnswer(inv -> inv.getArgument(0)); + + discountCodeCommandService.create(request); + + DiscountCode saved = captor.getValue(); + assertThat(saved.getOrganizationName()).isEqualTo(orgName); + assertThat(saved.getExpiredAt()).isEqualTo(expiredAt); + assertThat(CODE_PATTERN.matcher(saved.getCode()).matches()).isTrue(); + } + + @RepeatedTest(20) + @DisplayName("반복 생성 시 매번 유효한 6자리 코드 생성") + void create_repeatedCalls_alwaysValidCode() { + CreateDiscountCodeRequest request = new CreateDiscountCodeRequest( + "RepeatedOrg", LocalDate.of(2027, 12, 31) + ); + given(discountCodeRepository.existsByCode(anyString())).willReturn(false); + given(discountCodeRepository.save(any(DiscountCode.class))) + .willAnswer(inv -> inv.getArgument(0)); + + DiscountCodeResponse response = discountCodeCommandService.create(request); + + assertThat(CODE_PATTERN.matcher(response.code()).matches()).isTrue(); + } + + @Test + @DisplayName("여러 번 생성 시 코드가 무작위로 다양하게 생성됨") + void create_multipleInvocations_producesDiverseCodes() { + CreateDiscountCodeRequest request = new CreateDiscountCodeRequest( + "Org", LocalDate.of(2027, 1, 1) + ); + given(discountCodeRepository.existsByCode(anyString())).willReturn(false); + given(discountCodeRepository.save(any(DiscountCode.class))) + .willAnswer(inv -> inv.getArgument(0)); + + Set codes = new HashSet<>(); + for (int i = 0; i < 30; i++) { + codes.add(discountCodeCommandService.create(request).code()); + } + + // 30번 중 최소 10개 이상 고유한 코드가 생성되어야 함 (완전 랜덤성 확인) + assertThat(codes.size()).isGreaterThan(10); + } +} diff --git a/src/test/java/OneQ/OnSurvey/domain/question/service/QuestionConverterTest.java b/src/test/java/OneQ/OnSurvey/domain/question/service/QuestionConverterTest.java new file mode 100644 index 00000000..03071914 --- /dev/null +++ b/src/test/java/OneQ/OnSurvey/domain/question/service/QuestionConverterTest.java @@ -0,0 +1,257 @@ +package OneQ.OnSurvey.domain.question.service; + +import OneQ.OnSurvey.domain.question.entity.question.*; +import OneQ.OnSurvey.domain.question.model.QuestionType; +import OneQ.OnSurvey.domain.question.model.dto.GridOptionDto; +import OneQ.OnSurvey.domain.question.model.dto.OptionDto; +import OneQ.OnSurvey.domain.question.model.dto.QuestionUpsertDto; +import OneQ.OnSurvey.domain.question.model.dto.type.*; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class QuestionConverterTest { + + // ── toQuestionDto ───────────────────────────────────────────────────────── + + @Test + @DisplayName("Choice 엔티티 → ChoiceDto 변환") + void toQuestionDto_choice() { + Choice choice = Choice.of( + 1L, 1, "Q1", "설명", true, 1, + 3, false, true, false, + QuestionType.CHOICE, null + ); + + DefaultQuestionDto dto = QuestionConverter.toQuestionDto(choice); + + assertThat(dto).isInstanceOf(ChoiceDto.class); + ChoiceDto choiceDto = (ChoiceDto) dto; + assertThat(choiceDto.getMaxChoice()).isEqualTo(3); + assertThat(choiceDto.getHasCustomInput()).isTrue(); + assertThat(choiceDto.getHasNoneOption()).isFalse(); + assertThat(choiceDto.getIsSectionDecidable()).isFalse(); + assertThat(choiceDto.getQuestionType()).isEqualTo(QuestionType.Values.CHOICE); + assertThat(choiceDto.getTitle()).isEqualTo("Q1"); + } + + @Test + @DisplayName("Rating 엔티티 → RatingDto 변환") + void toQuestionDto_rating() { + Rating rating = Rating.of( + 1L, 2, "평가", "설명", false, 1, + "매우 좋음", "매우 나쁨", 5, QuestionType.RATING, null + ); + + DefaultQuestionDto dto = QuestionConverter.toQuestionDto(rating); + + assertThat(dto).isInstanceOf(RatingDto.class); + RatingDto ratingDto = (RatingDto) dto; + assertThat(ratingDto.getRate()).isEqualTo(5); + assertThat(ratingDto.getMaxValue()).isEqualTo("매우 좋음"); + assertThat(ratingDto.getMinValue()).isEqualTo("매우 나쁨"); + assertThat(ratingDto.getQuestionType()).isEqualTo(QuestionType.Values.RATING); + } + + @Test + @DisplayName("DateAnswer 엔티티 → DateDto 변환") + void toQuestionDto_date() { + LocalDateTime now = LocalDateTime.of(2024, 1, 15, 12, 0); + DateAnswer dateAnswer = DateAnswer.of( + 1L, 3, "날짜 질문", null, false, 1, + now, QuestionType.DATE, null + ); + + DefaultQuestionDto dto = QuestionConverter.toQuestionDto(dateAnswer); + + assertThat(dto).isInstanceOf(DateDto.class); + DateDto dateDto = (DateDto) dto; + assertThat(dateDto.getDate()).isEqualTo(now); + assertThat(dateDto.getQuestionType()).isEqualTo(QuestionType.Values.DATE); + } + + @Test + @DisplayName("Grid 엔티티 → GridDto 변환") + void toQuestionDto_grid() { + Grid grid = Grid.of( + 1L, 4, "그리드 질문", null, true, 1, + QuestionType.GRID, null, true, false, true + ); + + DefaultQuestionDto dto = QuestionConverter.toQuestionDto(grid); + + assertThat(dto).isInstanceOf(GridDto.class); + GridDto gridDto = (GridDto) dto; + assertThat(gridDto.getIsCheckbox()).isTrue(); + assertThat(gridDto.getIsChoiceMixed()).isFalse(); + assertThat(gridDto.getIsChoiceDistinct()).isTrue(); + assertThat(gridDto.getQuestionType()).isEqualTo(QuestionType.Values.GRID); + } + + @Test + @DisplayName("Time 엔티티 → TimeDto 변환") + void toQuestionDto_time() { + Time time = Time.of( + 1L, 5, "시간 질문", null, false, 1, + QuestionType.TIME, null, true + ); + + DefaultQuestionDto dto = QuestionConverter.toQuestionDto(time); + + assertThat(dto).isInstanceOf(TimeDto.class); + TimeDto timeDto = (TimeDto) dto; + assertThat(timeDto.getIsInterval()).isTrue(); + assertThat(timeDto.getQuestionType()).isEqualTo(QuestionType.Values.TIME); + } + + @Test + @DisplayName("기본 질문 엔티티 → DefaultQuestionDto 변환") + void toQuestionDto_default() { + ShortAnswer shortAnswer = ShortAnswer.of( + 1L, 6, "단답형", null, false, 1, QuestionType.SHORT, null + ); + + DefaultQuestionDto dto = QuestionConverter.toQuestionDto(shortAnswer); + + assertThat(dto.getClass()).isEqualTo(DefaultQuestionDto.class); + assertThat(dto.getQuestionType()).isEqualTo(QuestionType.Values.SHORT); + assertThat(dto.getTitle()).isEqualTo("단답형"); + } + + // ── toQuestionUpsertDto ─────────────────────────────────────────────────── + + @Test + @DisplayName("ChoiceDto 리스트 → QuestionUpsertDto 변환 (기본 필드)") + void toQuestionUpsertDto_withChoiceDto() { + OptionDto option = OptionDto.builder() + .optionId(10L).content("선택지1").nextSection(null).imageUrl(null).build(); + + ChoiceDto choiceDto = ChoiceDto.builder() + .questionId(1L) + .questionType(QuestionType.Values.CHOICE) + .title("객관식 질문") + .description("설명") + .isRequired(true) + .questionOrder(1) + .section(1) + .maxChoice(2) + .hasNoneOption(false) + .hasCustomInput(true) + .isSectionDecidable(false) + .options(List.of(option)) + .build(); + + QuestionUpsertDto result = QuestionConverter.toQuestionUpsertDto(99L, List.of(choiceDto)); + + assertThat(result.getSurveyId()).isEqualTo(99L); + assertThat(result.getUpsertInfoList()).hasSize(1); + + QuestionUpsertDto.UpsertInfo info = result.getUpsertInfoList().get(0); + assertThat(info.getQuestionId()).isEqualTo(1L); + assertThat(info.getQuestionType()).isEqualTo(QuestionType.CHOICE); + assertThat(info.getMaxChoice()).isEqualTo(2); + assertThat(info.getHasCustomInput()).isTrue(); + assertThat(info.getOptions()).hasSize(1); + assertThat(info.getOptions().get(0).getContent()).isEqualTo("선택지1"); + } + + @Test + @DisplayName("RatingDto → UpsertInfo 변환") + void toQuestionUpsertDto_withRatingDto() { + RatingDto ratingDto = RatingDto.builder() + .questionId(2L) + .questionType(QuestionType.Values.RATING) + .title("평가 질문") + .isRequired(false) + .questionOrder(2) + .section(1) + .minValue("나쁨") + .maxValue("좋음") + .rate(10) + .build(); + + QuestionUpsertDto result = QuestionConverter.toQuestionUpsertDto(1L, List.of(ratingDto)); + + QuestionUpsertDto.UpsertInfo info = result.getUpsertInfoList().get(0); + assertThat(info.getQuestionType()).isEqualTo(QuestionType.RATING); + assertThat(info.getRate()).isEqualTo(10); + assertThat(info.getMinValue()).isEqualTo("나쁨"); + assertThat(info.getMaxValue()).isEqualTo("좋음"); + } + + @Test + @DisplayName("GridDto → UpsertInfo 변환") + void toQuestionUpsertDto_withGridDto() { + GridOptionDto gridOption = GridOptionDto.builder() + .gridOptionId(1L).isRow(true).content("행1").order(1).build(); + + GridDto gridDto = GridDto.builder() + .questionId(3L) + .questionType(QuestionType.Values.GRID) + .title("그리드 질문") + .isRequired(true) + .questionOrder(3) + .section(2) + .isCheckbox(true) + .isChoiceMixed(null) + .isChoiceDistinct(null) + .gridOptions(List.of(gridOption)) + .build(); + + QuestionUpsertDto result = QuestionConverter.toQuestionUpsertDto(1L, List.of(gridDto)); + + QuestionUpsertDto.UpsertInfo info = result.getUpsertInfoList().get(0); + assertThat(info.getIsCheckbox()).isTrue(); + assertThat(info.getIsChoiceMixed()).isFalse(); + assertThat(info.getIsChoiceDistinct()).isFalse(); + assertThat(info.getGridOptions()).hasSize(1); + } + + @Test + @DisplayName("TimeDto → UpsertInfo 변환 (isInterval null이면 false)") + void toQuestionUpsertDto_withTimeDto_nullIsInterval() { + TimeDto timeDto = TimeDto.builder() + .questionId(4L) + .questionType(QuestionType.Values.TIME) + .title("시간 질문") + .isRequired(false) + .questionOrder(4) + .section(1) + .isInterval(null) + .build(); + + QuestionUpsertDto result = QuestionConverter.toQuestionUpsertDto(1L, List.of(timeDto)); + + QuestionUpsertDto.UpsertInfo info = result.getUpsertInfoList().get(0); + assertThat(info.getIsInterval()).isFalse(); + } + + @Test + @DisplayName("section이 null이면 기본값 1로 설정") + void toQuestionUpsertDto_sectionNullDefaultsToOne() { + DefaultQuestionDto dto = DefaultQuestionDto.builder() + .questionId(5L) + .questionType(QuestionType.Values.SHORT) + .title("단답형") + .isRequired(false) + .questionOrder(1) + .section(null) + .build(); + + QuestionUpsertDto result = QuestionConverter.toQuestionUpsertDto(1L, List.of(dto)); + + assertThat(result.getUpsertInfoList().get(0).getSection()).isEqualTo(1); + } + + @Test + @DisplayName("빈 리스트 입력 시 upsertInfoList도 빈 리스트") + void toQuestionUpsertDto_emptyList() { + QuestionUpsertDto result = QuestionConverter.toQuestionUpsertDto(1L, List.of()); + + assertThat(result.getUpsertInfoList()).isEmpty(); + } +} diff --git a/src/test/java/OneQ/OnSurvey/domain/survey/service/export/SurveyExportServiceTest.java b/src/test/java/OneQ/OnSurvey/domain/survey/service/export/SurveyExportServiceTest.java new file mode 100644 index 00000000..e85d7f72 --- /dev/null +++ b/src/test/java/OneQ/OnSurvey/domain/survey/service/export/SurveyExportServiceTest.java @@ -0,0 +1,331 @@ +package OneQ.OnSurvey.domain.survey.service.export; + +import OneQ.OnSurvey.domain.survey.SurveyErrorCode; +import OneQ.OnSurvey.domain.survey.entity.SurveyInfo; +import OneQ.OnSurvey.domain.survey.model.AgeRange; +import OneQ.OnSurvey.domain.survey.model.Gender; +import OneQ.OnSurvey.domain.survey.model.Residence; +import OneQ.OnSurvey.domain.survey.model.export.SurveyAnswerProjection; +import OneQ.OnSurvey.domain.survey.model.export.SurveyExportFile; +import OneQ.OnSurvey.domain.survey.model.export.SurveyMemberProjection; +import OneQ.OnSurvey.domain.survey.model.export.SurveyQuestionHeader; +import OneQ.OnSurvey.domain.survey.repository.export.SurveyExportRepository; +import OneQ.OnSurvey.domain.survey.repository.surveyInfo.SurveyInfoRepository; +import OneQ.OnSurvey.global.common.exception.CustomException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.BDDMockito.given; + +@ExtendWith(MockitoExtension.class) +class SurveyExportServiceTest { + + @Mock + private SurveyExportRepository surveyExportRepository; + + @Mock + private SurveyInfoRepository surveyInfoRepository; + + @InjectMocks + private SurveyExportService surveyExportService; + + private static final Long SURVEY_ID = 1L; + private static final Long MEMBER_ID = 10L; + + @BeforeEach + void setUp() { + } + + private SurveyInfo buildSurveyInfo(Gender gender, Set ages) { + return SurveyInfo.builder() + .surveyId(SURVEY_ID) + .dueCount(100) + .completedCount(0) + .gender(gender) + .ages(ages) + .residences(Set.of(Residence.ALL)) + .genderPrice(0) + .agePrice(0) + .residencePrice(0) + .dueCountPrice(0) + .promotionAmount(0) + .build(); + } + + // ── exportCsv 권한 체크 ──────────────────────────────────────────────────── + + @Test + @DisplayName("소유자가 아니면 SURVEY_FORBIDDEN 예외 발생") + void exportCsv_notOwner_throwsForbidden() { + given(surveyExportRepository.existsOwnedSurvey(SURVEY_ID, MEMBER_ID)).willReturn(false); + + assertThatThrownBy(() -> surveyExportService.exportCsv(SURVEY_ID, MEMBER_ID)) + .isInstanceOf(CustomException.class) + .satisfies(ex -> assertThat(((CustomException) ex).getErrorCode()) + .isEqualTo(SurveyErrorCode.SURVEY_FORBIDDEN)); + } + + @Test + @DisplayName("SurveyInfo가 없으면 SURVEY_INFO_NOT_FOUND 예외 발생") + void exportCsv_surveyInfoNotFound_throwsException() { + given(surveyExportRepository.existsOwnedSurvey(SURVEY_ID, MEMBER_ID)).willReturn(true); + given(surveyInfoRepository.findBySurveyId(SURVEY_ID)).willReturn(Optional.empty()); + + assertThatThrownBy(() -> surveyExportService.exportCsv(SURVEY_ID, MEMBER_ID)) + .isInstanceOf(CustomException.class) + .satisfies(ex -> assertThat(((CustomException) ex).getErrorCode()) + .isEqualTo(SurveyErrorCode.SURVEY_INFO_NOT_FOUND)); + } + + // ── CSV 내용 검증 ───────────────────────────────────────────────────────── + + @Test + @DisplayName("gender=ALL이면 gender 컬럼 미포함") + void exportCsv_genderAll_noGenderColumn() { + SurveyInfo info = buildSurveyInfo(Gender.ALL, Set.of(AgeRange.ALL)); + given(surveyExportRepository.existsOwnedSurvey(SURVEY_ID, MEMBER_ID)).willReturn(true); + given(surveyInfoRepository.findBySurveyId(SURVEY_ID)).willReturn(Optional.of(info)); + given(surveyExportRepository.findQuestionHeaders(SURVEY_ID)).willReturn(List.of()); + given(surveyExportRepository.findMembersWhoAnswered(SURVEY_ID)).willReturn(List.of()); + given(surveyExportRepository.findAnswers(SURVEY_ID)).willReturn(List.of()); + given(surveyExportRepository.findSurveyTitle(SURVEY_ID)).willReturn("테스트설문"); + + SurveyExportFile file = surveyExportService.exportCsv(SURVEY_ID, MEMBER_ID); + + String csv = removeBom(file.bytes()); + assertThat(csv).doesNotContain("gender"); + } + + @Test + @DisplayName("gender=MALE이면 gender 컬럼 포함") + void exportCsv_specificGender_hasGenderColumn() { + SurveyInfo info = buildSurveyInfo(Gender.MALE, Set.of(AgeRange.ALL)); + given(surveyExportRepository.existsOwnedSurvey(SURVEY_ID, MEMBER_ID)).willReturn(true); + given(surveyInfoRepository.findBySurveyId(SURVEY_ID)).willReturn(Optional.of(info)); + given(surveyExportRepository.findQuestionHeaders(SURVEY_ID)).willReturn(List.of()); + given(surveyExportRepository.findMembersWhoAnswered(SURVEY_ID)).willReturn(List.of()); + given(surveyExportRepository.findAnswers(SURVEY_ID)).willReturn(List.of()); + given(surveyExportRepository.findSurveyTitle(SURVEY_ID)).willReturn("테스트설문"); + + SurveyExportFile file = surveyExportService.exportCsv(SURVEY_ID, MEMBER_ID); + + String csv = removeBom(file.bytes()); + assertThat(csv).contains("gender"); + } + + @Test + @DisplayName("ages=ALL이면 age 컬럼 미포함") + void exportCsv_ageAll_noAgeColumn() { + SurveyInfo info = buildSurveyInfo(Gender.ALL, Set.of(AgeRange.ALL)); + given(surveyExportRepository.existsOwnedSurvey(SURVEY_ID, MEMBER_ID)).willReturn(true); + given(surveyInfoRepository.findBySurveyId(SURVEY_ID)).willReturn(Optional.of(info)); + given(surveyExportRepository.findQuestionHeaders(SURVEY_ID)).willReturn(List.of()); + given(surveyExportRepository.findMembersWhoAnswered(SURVEY_ID)).willReturn(List.of()); + given(surveyExportRepository.findAnswers(SURVEY_ID)).willReturn(List.of()); + given(surveyExportRepository.findSurveyTitle(SURVEY_ID)).willReturn("설문"); + + SurveyExportFile file = surveyExportService.exportCsv(SURVEY_ID, MEMBER_ID); + + String csv = removeBom(file.bytes()); + assertThat(csv).doesNotContain("age"); + } + + @Test + @DisplayName("특정 연령대 설정 시 age 컬럼 포함") + void exportCsv_specificAge_hasAgeColumn() { + SurveyInfo info = buildSurveyInfo(Gender.ALL, Set.of(AgeRange.TWENTY)); + given(surveyExportRepository.existsOwnedSurvey(SURVEY_ID, MEMBER_ID)).willReturn(true); + given(surveyInfoRepository.findBySurveyId(SURVEY_ID)).willReturn(Optional.of(info)); + given(surveyExportRepository.findQuestionHeaders(SURVEY_ID)).willReturn(List.of()); + given(surveyExportRepository.findMembersWhoAnswered(SURVEY_ID)).willReturn(List.of()); + given(surveyExportRepository.findAnswers(SURVEY_ID)).willReturn(List.of()); + given(surveyExportRepository.findSurveyTitle(SURVEY_ID)).willReturn("설문"); + + SurveyExportFile file = surveyExportService.exportCsv(SURVEY_ID, MEMBER_ID); + + String csv = removeBom(file.bytes()); + assertThat(csv).contains("age"); + } + + @Test + @DisplayName("질문 헤더와 응답자 데이터가 올바르게 CSV에 포함됨") + void exportCsv_withQuestionsAndMembers_csvContainsData() { + SurveyInfo info = buildSurveyInfo(Gender.ALL, Set.of(AgeRange.ALL)); + List headers = List.of( + new SurveyQuestionHeader(100L, 0, "좋아하는 음식", 0, null) + ); + List members = List.of( + new SurveyMemberProjection(MEMBER_ID, "19900101", "MALE", "서울") + ); + List answers = List.of( + new SurveyAnswerProjection(MEMBER_ID, 100L, "피자", 0) + ); + + given(surveyExportRepository.existsOwnedSurvey(SURVEY_ID, MEMBER_ID)).willReturn(true); + given(surveyInfoRepository.findBySurveyId(SURVEY_ID)).willReturn(Optional.of(info)); + given(surveyExportRepository.findQuestionHeaders(SURVEY_ID)).willReturn(headers); + given(surveyExportRepository.findMembersWhoAnswered(SURVEY_ID)).willReturn(members); + given(surveyExportRepository.findAnswers(SURVEY_ID)).willReturn(answers); + given(surveyExportRepository.findSurveyTitle(SURVEY_ID)).willReturn("음식설문"); + + SurveyExportFile file = surveyExportService.exportCsv(SURVEY_ID, MEMBER_ID); + + String csv = removeBom(file.bytes()); + assertThat(csv).contains("Q1. 좋아하는 음식"); + assertThat(csv).contains("서울"); + assertThat(csv).contains("피자"); + } + + @Test + @DisplayName("CSV 파일에 BOM(UTF-8)이 포함됨") + void exportCsv_hasBom() { + SurveyInfo info = buildSurveyInfo(Gender.ALL, Set.of(AgeRange.ALL)); + given(surveyExportRepository.existsOwnedSurvey(SURVEY_ID, MEMBER_ID)).willReturn(true); + given(surveyInfoRepository.findBySurveyId(SURVEY_ID)).willReturn(Optional.of(info)); + given(surveyExportRepository.findQuestionHeaders(SURVEY_ID)).willReturn(List.of()); + given(surveyExportRepository.findMembersWhoAnswered(SURVEY_ID)).willReturn(List.of()); + given(surveyExportRepository.findAnswers(SURVEY_ID)).willReturn(List.of()); + given(surveyExportRepository.findSurveyTitle(SURVEY_ID)).willReturn("설문"); + + SurveyExportFile file = surveyExportService.exportCsv(SURVEY_ID, MEMBER_ID); + + assertThat(file.bytes()[0]).isEqualTo((byte) 0xEF); + assertThat(file.bytes()[1]).isEqualTo((byte) 0xBB); + assertThat(file.bytes()[2]).isEqualTo((byte) 0xBF); + } + + @Test + @DisplayName("파일명에 날짜가 포함되고 확장자가 .csv") + void exportCsv_filenameHasDateAndCsvExtension() { + SurveyInfo info = buildSurveyInfo(Gender.ALL, Set.of(AgeRange.ALL)); + given(surveyExportRepository.existsOwnedSurvey(SURVEY_ID, MEMBER_ID)).willReturn(true); + given(surveyInfoRepository.findBySurveyId(SURVEY_ID)).willReturn(Optional.of(info)); + given(surveyExportRepository.findQuestionHeaders(SURVEY_ID)).willReturn(List.of()); + given(surveyExportRepository.findMembersWhoAnswered(SURVEY_ID)).willReturn(List.of()); + given(surveyExportRepository.findAnswers(SURVEY_ID)).willReturn(List.of()); + given(surveyExportRepository.findSurveyTitle(SURVEY_ID)).willReturn("음식 설문"); + + SurveyExportFile file = surveyExportService.exportCsv(SURVEY_ID, MEMBER_ID); + + assertThat(file.filename()).endsWith(".csv"); + assertThat(file.filename()).startsWith("음식 설문_"); + assertThat(file.contentType()).isEqualTo("text/csv; charset=UTF-8"); + } + + @Test + @DisplayName("파일명에 특수문자가 있으면 공백으로 치환") + void exportCsv_titleWithSpecialChars_sanitized() { + SurveyInfo info = buildSurveyInfo(Gender.ALL, Set.of(AgeRange.ALL)); + given(surveyExportRepository.existsOwnedSurvey(SURVEY_ID, MEMBER_ID)).willReturn(true); + given(surveyInfoRepository.findBySurveyId(SURVEY_ID)).willReturn(Optional.of(info)); + given(surveyExportRepository.findQuestionHeaders(SURVEY_ID)).willReturn(List.of()); + given(surveyExportRepository.findMembersWhoAnswered(SURVEY_ID)).willReturn(List.of()); + given(surveyExportRepository.findAnswers(SURVEY_ID)).willReturn(List.of()); + given(surveyExportRepository.findSurveyTitle(SURVEY_ID)).willReturn("설문/테스트:조사*파일"); + + SurveyExportFile file = surveyExportService.exportCsv(SURVEY_ID, MEMBER_ID); + + assertThat(file.filename()).doesNotContain("/", ":", "*"); + } + + @Test + @DisplayName("surveyTitle이 null이면 파일명 기본값 'survey'") + void exportCsv_nullTitle_defaultFilename() { + SurveyInfo info = buildSurveyInfo(Gender.ALL, Set.of(AgeRange.ALL)); + given(surveyExportRepository.existsOwnedSurvey(SURVEY_ID, MEMBER_ID)).willReturn(true); + given(surveyInfoRepository.findBySurveyId(SURVEY_ID)).willReturn(Optional.of(info)); + given(surveyExportRepository.findQuestionHeaders(SURVEY_ID)).willReturn(List.of()); + given(surveyExportRepository.findMembersWhoAnswered(SURVEY_ID)).willReturn(List.of()); + given(surveyExportRepository.findAnswers(SURVEY_ID)).willReturn(List.of()); + given(surveyExportRepository.findSurveyTitle(SURVEY_ID)).willReturn(null); + + SurveyExportFile file = surveyExportService.exportCsv(SURVEY_ID, MEMBER_ID); + + assertThat(file.filename()).startsWith("survey_"); + } + + @Test + @DisplayName("콤마 포함 응답은 큰따옴표로 감싸서 CSV 이스케이프") + void exportCsv_answerWithComma_escapedWithQuotes() { + SurveyInfo info = buildSurveyInfo(Gender.ALL, Set.of(AgeRange.ALL)); + List headers = List.of( + new SurveyQuestionHeader(100L, 0, "음식", 0, null) + ); + List members = List.of( + new SurveyMemberProjection(MEMBER_ID, null, null, "서울") + ); + List answers = List.of( + new SurveyAnswerProjection(MEMBER_ID, 100L, "피자,파스타", 0) + ); + + given(surveyExportRepository.existsOwnedSurvey(SURVEY_ID, MEMBER_ID)).willReturn(true); + given(surveyInfoRepository.findBySurveyId(SURVEY_ID)).willReturn(Optional.of(info)); + given(surveyExportRepository.findQuestionHeaders(SURVEY_ID)).willReturn(headers); + given(surveyExportRepository.findMembersWhoAnswered(SURVEY_ID)).willReturn(members); + given(surveyExportRepository.findAnswers(SURVEY_ID)).willReturn(answers); + given(surveyExportRepository.findSurveyTitle(SURVEY_ID)).willReturn("설문"); + + SurveyExportFile file = surveyExportService.exportCsv(SURVEY_ID, MEMBER_ID); + + String csv = removeBom(file.bytes()); + assertThat(csv).contains("\"피자,파스타\""); + } + + @Test + @DisplayName("exportCsvForAdmin은 소유자 체크 없이 CSV 생성") + void exportCsvForAdmin_noOwnerCheck() { + SurveyInfo info = buildSurveyInfo(Gender.ALL, Set.of(AgeRange.ALL)); + given(surveyInfoRepository.findBySurveyId(SURVEY_ID)).willReturn(Optional.of(info)); + given(surveyExportRepository.findQuestionHeaders(SURVEY_ID)).willReturn(List.of()); + given(surveyExportRepository.findMembersWhoAnswered(SURVEY_ID)).willReturn(List.of()); + given(surveyExportRepository.findAnswers(SURVEY_ID)).willReturn(List.of()); + given(surveyExportRepository.findSurveyTitle(SURVEY_ID)).willReturn("관리자용설문"); + + SurveyExportFile file = surveyExportService.exportCsvForAdmin(SURVEY_ID); + + assertThat(file).isNotNull(); + assertThat(file.filename()).contains("관리자용설문"); + } + + @Test + @DisplayName("응답자의 생년월일로 나이 계산") + void exportCsv_withBirthday_ageCalculated() { + SurveyInfo info = buildSurveyInfo(Gender.ALL, Set.of(AgeRange.TWENTY)); + given(surveyExportRepository.existsOwnedSurvey(SURVEY_ID, MEMBER_ID)).willReturn(true); + given(surveyInfoRepository.findBySurveyId(SURVEY_ID)).willReturn(Optional.of(info)); + given(surveyExportRepository.findQuestionHeaders(SURVEY_ID)).willReturn(List.of()); + given(surveyExportRepository.findMembersWhoAnswered(SURVEY_ID)).willReturn( + List.of(new SurveyMemberProjection(MEMBER_ID, "2000-01-01", null, "서울")) + ); + given(surveyExportRepository.findAnswers(SURVEY_ID)).willReturn(List.of()); + given(surveyExportRepository.findSurveyTitle(SURVEY_ID)).willReturn("설문"); + + SurveyExportFile file = surveyExportService.exportCsv(SURVEY_ID, MEMBER_ID); + + String csv = removeBom(file.bytes()); + // 2000년생이면 2026 - 2000 + 1 = 27 + assertThat(csv).contains("27"); + } + + private String removeBom(byte[] bytes) { + if (bytes.length >= 3 + && bytes[0] == (byte) 0xEF + && bytes[1] == (byte) 0xBB + && bytes[2] == (byte) 0xBF) { + return new String(bytes, 3, bytes.length - 3, StandardCharsets.UTF_8); + } + return new String(bytes, StandardCharsets.UTF_8); + } +} diff --git a/src/test/java/OneQ/OnSurvey/domain/survey/service/refund/DefaultSurveyRefundPolicyTest.java b/src/test/java/OneQ/OnSurvey/domain/survey/service/refund/DefaultSurveyRefundPolicyTest.java new file mode 100644 index 00000000..115c72f5 --- /dev/null +++ b/src/test/java/OneQ/OnSurvey/domain/survey/service/refund/DefaultSurveyRefundPolicyTest.java @@ -0,0 +1,156 @@ +package OneQ.OnSurvey.domain.survey.service.refund; + +import OneQ.OnSurvey.domain.survey.entity.Survey; +import OneQ.OnSurvey.domain.survey.entity.SurveyInfo; +import OneQ.OnSurvey.domain.survey.model.Gender; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; + +class DefaultSurveyRefundPolicyTest { + + private DefaultSurveyRefundPolicy policy; + + @BeforeEach + void setUp() { + policy = new DefaultSurveyRefundPolicy(); + ReflectionTestUtils.setField(policy, "rewardPerResponse", 100); + } + + private Survey surveyWithCoin(int totalCoin) { + return Survey.builder() + .memberId(1L) + .title("테스트 설문") + .description("설명") + .totalCoin(totalCoin) + .build(); + } + + private SurveyInfo surveyInfo(int dueCount, int completedCount) { + return SurveyInfo.builder() + .surveyId(1L) + .dueCount(dueCount) + .completedCount(completedCount) + .gender(Gender.ALL) + .ages(Set.of()) + .residences(Set.of()) + .genderPrice(0) + .agePrice(0) + .residencePrice(0) + .dueCountPrice(0) + .promotionAmount(0) + .build(); + } + + @Test + @DisplayName("목표 응답 수가 0 이하이면 환불 금액은 0") + void calculateRefundAmount_whenTargetCountZero_returnsZero() { + Survey survey = surveyWithCoin(1000); + SurveyInfo info = surveyInfo(0, 0); + + int result = policy.calculateRefundAmount(survey, info); + + assertThat(result).isZero(); + } + + @Test + @DisplayName("목표 응답 수가 음수이면 환불 금액은 0") + void calculateRefundAmount_whenTargetCountNegative_returnsZero() { + Survey survey = surveyWithCoin(1000); + SurveyInfo info = surveyInfo(-1, 0); + + int result = policy.calculateRefundAmount(survey, info); + + assertThat(result).isZero(); + } + + @Test + @DisplayName("완료 수가 목표 수 이상이면 환불 금액은 0") + void calculateRefundAmount_whenCompletedReachedTarget_returnsZero() { + Survey survey = surveyWithCoin(1000); + SurveyInfo info = surveyInfo(10, 10); + + int result = policy.calculateRefundAmount(survey, info); + + assertThat(result).isZero(); + } + + @Test + @DisplayName("완료 수가 목표 수를 초과해도 환불 금액은 0") + void calculateRefundAmount_whenCompletedExceedsTarget_returnsZero() { + Survey survey = surveyWithCoin(1000); + SurveyInfo info = surveyInfo(10, 15); + + int result = policy.calculateRefundAmount(survey, info); + + assertThat(result).isZero(); + } + + @Test + @DisplayName("응답이 0건일 때 전액 환불") + void calculateRefundAmount_whenNoCompletions_returnsFullRefund() { + // totalCoin=1000, rewardPerResponse=100, dueCount=10, completedCount=0 + // paidReward=0, refundBase=1000, lackRatio=1.0 → refund=1000 + Survey survey = surveyWithCoin(1000); + SurveyInfo info = surveyInfo(10, 0); + + int result = policy.calculateRefundAmount(survey, info); + + assertThat(result).isEqualTo(1000); + } + + @Test + @DisplayName("절반 응답 완료 시 남은 비율만큼 환불") + void calculateRefundAmount_whenHalfCompleted_returnsHalfRefund() { + // totalCoin=1000, rewardPerResponse=100, dueCount=10, completedCount=5 + // paidReward=500, refundBase=500, lackRatio=5/10=0.5 → refund=250 + Survey survey = surveyWithCoin(1000); + SurveyInfo info = surveyInfo(10, 5); + + int result = policy.calculateRefundAmount(survey, info); + + assertThat(result).isEqualTo(250); + } + + @Test + @DisplayName("totalCoin이 paidReward 이하이면 refundBase=0 → 환불 금액 0") + void calculateRefundAmount_whenTotalCoinBelowPaidReward_returnsZero() { + // totalCoin=300, rewardPerResponse=100, completedCount=5 → paidReward=500 > totalCoin + // refundBase=0 → refund=0 + Survey survey = surveyWithCoin(300); + SurveyInfo info = surveyInfo(10, 5); + + int result = policy.calculateRefundAmount(survey, info); + + assertThat(result).isZero(); + } + + @Test + @DisplayName("소수점 이하는 내림 처리") + void calculateRefundAmount_floorsFractionalAmount() { + // totalCoin=1000, rewardPerResponse=100, dueCount=3, completedCount=1 + // paidReward=100, refundBase=900, lackRatio=2/3≈0.6667 → floor(600)=600 + Survey survey = surveyWithCoin(1000); + SurveyInfo info = surveyInfo(3, 1); + + int result = policy.calculateRefundAmount(survey, info); + + assertThat(result).isEqualTo(600); + } + + @Test + @DisplayName("목표 1건 중 0건 완료 → 전액 환불") + void calculateRefundAmount_singleTargetNoCompletion_fullRefund() { + Survey survey = surveyWithCoin(500); + SurveyInfo info = surveyInfo(1, 0); + + int result = policy.calculateRefundAmount(survey, info); + + assertThat(result).isEqualTo(500); + } +} From 86b40c4fd5e2bd860ac320cc6a70a3c5ed425aa7 Mon Sep 17 00:00:00 2001 From: jaekwan Date: Sun, 10 May 2026 15:06:40 +0900 Subject: [PATCH 06/16] =?UTF-8?q?refactor:=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=20=EC=BD=94=EB=93=9C=20=EC=84=B9=EC=85=98=20=EA=B5=AC=EB=B6=84?= =?UTF-8?q?=20=EC=A3=BC=EC=84=9D=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/question/service/QuestionConverterTest.java | 4 ---- .../domain/survey/service/export/SurveyExportServiceTest.java | 4 ---- 2 files changed, 8 deletions(-) diff --git a/src/test/java/OneQ/OnSurvey/domain/question/service/QuestionConverterTest.java b/src/test/java/OneQ/OnSurvey/domain/question/service/QuestionConverterTest.java index 03071914..067a92c0 100644 --- a/src/test/java/OneQ/OnSurvey/domain/question/service/QuestionConverterTest.java +++ b/src/test/java/OneQ/OnSurvey/domain/question/service/QuestionConverterTest.java @@ -16,8 +16,6 @@ class QuestionConverterTest { - // ── toQuestionDto ───────────────────────────────────────────────────────── - @Test @DisplayName("Choice 엔티티 → ChoiceDto 변환") void toQuestionDto_choice() { @@ -122,8 +120,6 @@ void toQuestionDto_default() { assertThat(dto.getTitle()).isEqualTo("단답형"); } - // ── toQuestionUpsertDto ─────────────────────────────────────────────────── - @Test @DisplayName("ChoiceDto 리스트 → QuestionUpsertDto 변환 (기본 필드)") void toQuestionUpsertDto_withChoiceDto() { diff --git a/src/test/java/OneQ/OnSurvey/domain/survey/service/export/SurveyExportServiceTest.java b/src/test/java/OneQ/OnSurvey/domain/survey/service/export/SurveyExportServiceTest.java index e85d7f72..b45129f4 100644 --- a/src/test/java/OneQ/OnSurvey/domain/survey/service/export/SurveyExportServiceTest.java +++ b/src/test/java/OneQ/OnSurvey/domain/survey/service/export/SurveyExportServiceTest.java @@ -64,8 +64,6 @@ private SurveyInfo buildSurveyInfo(Gender gender, Set ages) { .build(); } - // ── exportCsv 권한 체크 ──────────────────────────────────────────────────── - @Test @DisplayName("소유자가 아니면 SURVEY_FORBIDDEN 예외 발생") void exportCsv_notOwner_throwsForbidden() { @@ -89,8 +87,6 @@ void exportCsv_surveyInfoNotFound_throwsException() { .isEqualTo(SurveyErrorCode.SURVEY_INFO_NOT_FOUND)); } - // ── CSV 내용 검증 ───────────────────────────────────────────────────────── - @Test @DisplayName("gender=ALL이면 gender 컬럼 미포함") void exportCsv_genderAll_noGenderColumn() { From b0a726938e54f1699b98fe9d6765218c037c7f4b Mon Sep 17 00:00:00 2001 From: jaekwan Date: Sun, 10 May 2026 15:26:05 +0900 Subject: [PATCH 07/16] =?UTF-8?q?test:=20Member=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EB=8B=A8=EC=9C=84=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(PR=202/9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MemberTest: 코인 증감, 포인트, 동의 정책, 온보딩, 상태 변경 등 엔티티 비즈니스 로직 (14개) - MemberQueryServiceTest: 조회, 검색, 위임 로직 (7개) - MemberModifyServiceTest: upsertMember(신규/기존), 프로필, 온보딩, 삭제 (9개) --- .../OnSurvey/domain/member/MemberTest.java | 202 ++++++++++++++++++ .../service/MemberModifyServiceTest.java | 190 ++++++++++++++++ .../service/MemberQueryServiceTest.java | 149 +++++++++++++ 3 files changed, 541 insertions(+) create mode 100644 src/test/java/OneQ/OnSurvey/domain/member/MemberTest.java create mode 100644 src/test/java/OneQ/OnSurvey/domain/member/service/MemberModifyServiceTest.java create mode 100644 src/test/java/OneQ/OnSurvey/domain/member/service/MemberQueryServiceTest.java diff --git a/src/test/java/OneQ/OnSurvey/domain/member/MemberTest.java b/src/test/java/OneQ/OnSurvey/domain/member/MemberTest.java new file mode 100644 index 00000000..73e7b515 --- /dev/null +++ b/src/test/java/OneQ/OnSurvey/domain/member/MemberTest.java @@ -0,0 +1,202 @@ +package OneQ.OnSurvey.domain.member; + +import OneQ.OnSurvey.domain.member.value.Interest; +import OneQ.OnSurvey.domain.member.value.MemberStatus; +import OneQ.OnSurvey.domain.member.value.Role; +import OneQ.OnSurvey.domain.survey.model.Gender; +import OneQ.OnSurvey.domain.survey.model.Residence; +import OneQ.OnSurvey.global.common.exception.CustomException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class MemberTest { + + private Member buildMember() { + return Member.createMember( + 1000L, "홍길동", "010-1234-5678", + "19900101", "test@test.com", + Gender.MALE, Role.ROLE_MEMBER, MemberStatus.ACTIVE + ); + } + + @Test + @DisplayName("increaseCoin - 양수 금액이면 코인 증가") + void increaseCoin_positiveAmount_increasesCoin() { + Member member = buildMember(); + + member.increaseCoin(500L); + + assertThat(member.getCoin()).isEqualTo(500L); + } + + @Test + @DisplayName("increaseCoin - 0 이하이면 COIN_NOT_POSITIVE 예외") + void increaseCoin_zero_throwsException() { + Member member = buildMember(); + + assertThatThrownBy(() -> member.increaseCoin(0L)) + .isInstanceOf(CustomException.class) + .satisfies(ex -> assertThat(((CustomException) ex).getErrorCode()) + .isEqualTo(CoinErrorCode.COIN_NOT_POSITIVE)); + } + + @Test + @DisplayName("increaseCoin - 음수이면 COIN_NOT_POSITIVE 예외") + void increaseCoin_negative_throwsException() { + Member member = buildMember(); + + assertThatThrownBy(() -> member.increaseCoin(-100L)) + .isInstanceOf(CustomException.class) + .satisfies(ex -> assertThat(((CustomException) ex).getErrorCode()) + .isEqualTo(CoinErrorCode.COIN_NOT_POSITIVE)); + } + + @Test + @DisplayName("decreaseCoin - 충분한 코인이면 차감") + void decreaseCoin_sufficientCoin_decreasesCoin() { + Member member = buildMember(); + member.increaseCoin(1000L); + + member.decreaseCoin(300L); + + assertThat(member.getCoin()).isEqualTo(700L); + } + + @Test + @DisplayName("decreaseCoin - 코인 부족이면 COIN_LACK 예외") + void decreaseCoin_insufficientCoin_throwsException() { + Member member = buildMember(); + member.increaseCoin(100L); + + assertThatThrownBy(() -> member.decreaseCoin(500L)) + .isInstanceOf(CustomException.class) + .satisfies(ex -> assertThat(((CustomException) ex).getErrorCode()) + .isEqualTo(CoinErrorCode.COIN_LACK)); + } + + @Test + @DisplayName("decreaseCoin - 0 이하이면 COIN_NOT_POSITIVE 예외") + void decreaseCoin_zero_throwsException() { + Member member = buildMember(); + + assertThatThrownBy(() -> member.decreaseCoin(0L)) + .isInstanceOf(CustomException.class) + .satisfies(ex -> assertThat(((CustomException) ex).getErrorCode()) + .isEqualTo(CoinErrorCode.COIN_NOT_POSITIVE)); + } + + @Test + @DisplayName("increasePromotionPoint - 양수 금액이면 포인트 증가") + void increasePromotionPoint_positiveAmount_increasesPoint() { + Member member = buildMember(); + + member.increasePromotionPoint(200L); + + assertThat(member.getPromotionPoint()).isEqualTo(200L); + } + + @Test + @DisplayName("increasePromotionPoint - 0 이하이면 COIN_NOT_POSITIVE 예외") + void increasePromotionPoint_zero_throwsException() { + Member member = buildMember(); + + assertThatThrownBy(() -> member.increasePromotionPoint(0L)) + .isInstanceOf(CustomException.class) + .satisfies(ex -> assertThat(((CustomException) ex).getErrorCode()) + .isEqualTo(CoinErrorCode.COIN_NOT_POSITIVE)); + } + + @Test + @DisplayName("update - null 값은 무시하고 비null 값만 업데이트") + void update_nullFieldsIgnored() { + Member member = buildMember(); + + member.update(null, "010-9999-9999", null, null, null, MemberStatus.ACTIVE); + + assertThat(member.getName()).isEqualTo("홍길동"); + assertThat(member.getPhoneNumber()).isEqualTo("010-9999-9999"); + assertThat(member.getEmail()).isEqualTo("test@test.com"); + } + + @Test + @DisplayName("updateAgreePolicy - serviceAgreed 항목 포함 시 서비스 동의 true") + void updateAgreePolicy_withServiceAgreed_setsTrue() { + Member member = buildMember(); + + member.updateAgreePolicy(List.of("serviceAgreed")); + + assertThat(member.isServiceAgreed()).isTrue(); + assertThat(member.isMarketingAgreed()).isFalse(); + } + + @Test + @DisplayName("updateAgreePolicy - marketingAgreed 항목 포함 시 마케팅 동의 true") + void updateAgreePolicy_withMarketingAgreed_setsTrue() { + Member member = buildMember(); + + member.updateAgreePolicy(List.of("marketingAgreed")); + + assertThat(member.isMarketingAgreed()).isTrue(); + assertThat(member.isServiceAgreed()).isFalse(); + } + + @Test + @DisplayName("updateAgreePolicy - null 이면 동의 상태 변경 없음") + void updateAgreePolicy_null_noChange() { + Member member = buildMember(); + + member.updateAgreePolicy(null); + + assertThat(member.isServiceAgreed()).isFalse(); + assertThat(member.isMarketingAgreed()).isFalse(); + } + + @Test + @DisplayName("memberConnectOut - 상태 TOSS_CONNECT_OUT으로 변경") + void memberConnectOut_changesStatus() { + Member member = buildMember(); + + member.memberConnectOut(); + + assertThat(member.getStatus()).isEqualTo(MemberStatus.TOSS_CONNECT_OUT); + } + + @Test + @DisplayName("changeProfileUrl - 프로필 URL 변경") + void changeProfileUrl_updatesUrl() { + Member member = buildMember(); + + member.changeProfileUrl("https://cdn.example.com/avatar.png"); + + assertThat(member.getProfileUrl()).isEqualTo("https://cdn.example.com/avatar.png"); + } + + @Test + @DisplayName("completeOnboarding - 거주지, 관심사 설정 및 완료 플래그 true") + void completeOnboarding_setsResidenceAndInterests() { + Member member = buildMember(); + + member.completeOnboarding(Residence.BUSAN, Set.of(Interest.CAREER, Interest.FINANCE)); + + assertThat(member.isOnboardingCompleted()).isTrue(); + assertThat(member.getResidence()).isEqualTo(Residence.BUSAN); + assertThat(member.getInterests()).containsExactlyInAnyOrder(Interest.CAREER, Interest.FINANCE); + } + + @Test + @DisplayName("completeOnboarding - interests가 null이면 빈 Set으로 처리") + void completeOnboarding_nullInterests_usesEmptySet() { + Member member = buildMember(); + + member.completeOnboarding(Residence.SEOUL, null); + + assertThat(member.isOnboardingCompleted()).isTrue(); + assertThat(member.getInterests()).isEmpty(); + } +} diff --git a/src/test/java/OneQ/OnSurvey/domain/member/service/MemberModifyServiceTest.java b/src/test/java/OneQ/OnSurvey/domain/member/service/MemberModifyServiceTest.java new file mode 100644 index 00000000..f13ea65e --- /dev/null +++ b/src/test/java/OneQ/OnSurvey/domain/member/service/MemberModifyServiceTest.java @@ -0,0 +1,190 @@ +package OneQ.OnSurvey.domain.member.service; + +import OneQ.OnSurvey.domain.member.Member; +import OneQ.OnSurvey.domain.member.MemberErrorCode; +import OneQ.OnSurvey.domain.member.repository.MemberRepository; +import OneQ.OnSurvey.domain.member.value.Interest; +import OneQ.OnSurvey.domain.member.value.MemberStatus; +import OneQ.OnSurvey.domain.member.value.Role; +import OneQ.OnSurvey.domain.survey.model.Gender; +import OneQ.OnSurvey.domain.survey.model.Residence; +import OneQ.OnSurvey.global.auth.dto.DecryptedLoginMeResponse; +import OneQ.OnSurvey.global.common.exception.CustomException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class MemberModifyServiceTest { + + @Mock + private MemberRepository memberRepository; + + @InjectMocks + private MemberModifyService memberModifyService; + + private Member buildMember(Long userKey) { + return Member.createMember( + userKey, "기존이름", "010-0000-0000", + "19850101", "old@test.com", + Gender.FEMALE, Role.ROLE_MEMBER, MemberStatus.ACTIVE + ); + } + + private DecryptedLoginMeResponse buildLoginResponse(long userKey) { + return new DecryptedLoginMeResponse( + userKey, "scope", List.of("serviceAgreed"), "policy", + "certTxId", "새이름", "010-9999-9999", + "19900101", Gender.MALE, "KR", "new@test.com" + ); + } + + @Test + @DisplayName("upsertMember - 기존 멤버가 있으면 정보 업데이트") + void upsertMember_existingMember_updatesFields() { + Member existing = buildMember(1000L); + DecryptedLoginMeResponse loginResponse = buildLoginResponse(1000L); + given(memberRepository.findMemberByUserKey(1000L)).willReturn(Optional.of(existing)); + + Member result = memberModifyService.upsertMember(loginResponse); + + assertThat(result.getName()).isEqualTo("새이름"); + assertThat(result.getPhoneNumber()).isEqualTo("010-9999-9999"); + assertThat(result.getEmail()).isEqualTo("new@test.com"); + assertThat(result.getStatus()).isEqualTo(MemberStatus.ACTIVE); + verify(memberRepository, never()).save(any()); + } + + @Test + @DisplayName("upsertMember - 신규 멤버면 생성 후 저장") + void upsertMember_newMember_createsAndSaves() { + DecryptedLoginMeResponse loginResponse = buildLoginResponse(2000L); + given(memberRepository.findMemberByUserKey(2000L)).willReturn(Optional.empty()); + given(memberRepository.save(any(Member.class))).willAnswer(inv -> inv.getArgument(0)); + + Member result = memberModifyService.upsertMember(loginResponse); + + assertThat(result.getUserKey()).isEqualTo(2000L); + assertThat(result.getName()).isEqualTo("새이름"); + assertThat(result.getRole()).isEqualTo(Role.ROLE_MEMBER); + verify(memberRepository).save(any(Member.class)); + } + + @Test + @DisplayName("upsertMember - agreeTerms에 serviceAgreed 포함 시 서비스 동의 true") + void upsertMember_withServiceAgreed_setsServiceAgreedTrue() { + given(memberRepository.findMemberByUserKey(3000L)).willReturn(Optional.empty()); + given(memberRepository.save(any(Member.class))).willAnswer(inv -> inv.getArgument(0)); + + DecryptedLoginMeResponse loginResponse = new DecryptedLoginMeResponse( + 3000L, "scope", List.of("serviceAgreed", "marketingAgreed"), + "policy", "certTxId", "이름", "010-1111-1111", + "20000101", Gender.MALE, "KR", "email@test.com" + ); + + Member result = memberModifyService.upsertMember(loginResponse); + + assertThat(result.isServiceAgreed()).isTrue(); + assertThat(result.isMarketingAgreed()).isTrue(); + } + + @Test + @DisplayName("upsertMember - agreeTerms가 null이면 동의 상태 변경 없음") + void upsertMember_nullAgreeTerms_noChange() { + given(memberRepository.findMemberByUserKey(4000L)).willReturn(Optional.empty()); + given(memberRepository.save(any(Member.class))).willAnswer(inv -> inv.getArgument(0)); + + DecryptedLoginMeResponse loginResponse = new DecryptedLoginMeResponse( + 4000L, "scope", null, + "policy", "certTxId", "이름", "010-2222-2222", + "19950101", Gender.FEMALE, "KR", "email@test.com" + ); + + Member result = memberModifyService.upsertMember(loginResponse); + + assertThat(result.isServiceAgreed()).isFalse(); + assertThat(result.isMarketingAgreed()).isFalse(); + } + + @Test + @DisplayName("changeMemberStatusTossConnectOut - TOSS_CONNECT_OUT으로 상태 변경 후 저장") + void changeMemberStatusTossConnectOut_updatesStatusAndSaves() { + Member member = buildMember(5000L); + ArgumentCaptor captor = ArgumentCaptor.forClass(Member.class); + + memberModifyService.changeMemberStatusTossConnectOut(member); + + verify(memberRepository).save(captor.capture()); + assertThat(captor.getValue().getStatus()).isEqualTo(MemberStatus.TOSS_CONNECT_OUT); + } + + @Test + @DisplayName("changeProfileImage - 멤버 존재 시 프로필 URL 변경") + void changeProfileImage_found_changesProfileUrl() { + Member member = buildMember(6000L); + given(memberRepository.findMemberByUserKey(6000L)).willReturn(Optional.of(member)); + + memberModifyService.changeProfileImage(6000L, "https://new-image.com/photo.jpg"); + + assertThat(member.getProfileUrl()).isEqualTo("https://new-image.com/photo.jpg"); + } + + @Test + @DisplayName("changeProfileImage - 멤버 없으면 MEMBER_NOT_FOUND 예외") + void changeProfileImage_notFound_throwsException() { + given(memberRepository.findMemberByUserKey(999L)).willReturn(Optional.empty()); + + assertThatThrownBy(() -> memberModifyService.changeProfileImage(999L, "url")) + .isInstanceOf(CustomException.class) + .satisfies(ex -> assertThat(((CustomException) ex).getErrorCode()) + .isEqualTo(MemberErrorCode.MEMBER_NOT_FOUND)); + } + + @Test + @DisplayName("completeOnboarding - 멤버 존재 시 온보딩 완료 처리") + void completeOnboarding_found_completesOnboarding() { + Member member = buildMember(7000L); + given(memberRepository.findMemberByUserKey(7000L)).willReturn(Optional.of(member)); + + memberModifyService.completeOnboarding( + 7000L, Residence.SEOUL, Set.of(Interest.HEALTH, Interest.CULTURE) + ); + + assertThat(member.isOnboardingCompleted()).isTrue(); + assertThat(member.getResidence()).isEqualTo(Residence.SEOUL); + assertThat(member.getInterests()).containsExactlyInAnyOrder(Interest.HEALTH, Interest.CULTURE); + } + + @Test + @DisplayName("completeOnboarding - 멤버 없으면 MEMBER_NOT_FOUND 예외") + void completeOnboarding_notFound_throwsException() { + given(memberRepository.findMemberByUserKey(999L)).willReturn(Optional.empty()); + + assertThatThrownBy(() -> memberModifyService.completeOnboarding(999L, Residence.SEOUL, Set.of())) + .isInstanceOf(CustomException.class) + .satisfies(ex -> assertThat(((CustomException) ex).getErrorCode()) + .isEqualTo(MemberErrorCode.MEMBER_NOT_FOUND)); + } + + @Test + @DisplayName("deleteById - repository.deleteById 호출") + void deleteById_delegatesToRepository() { + memberModifyService.deleteById(8000L); + + verify(memberRepository).deleteById(8000L); + } +} diff --git a/src/test/java/OneQ/OnSurvey/domain/member/service/MemberQueryServiceTest.java b/src/test/java/OneQ/OnSurvey/domain/member/service/MemberQueryServiceTest.java new file mode 100644 index 00000000..7840d81d --- /dev/null +++ b/src/test/java/OneQ/OnSurvey/domain/member/service/MemberQueryServiceTest.java @@ -0,0 +1,149 @@ +package OneQ.OnSurvey.domain.member.service; + +import OneQ.OnSurvey.domain.member.Member; +import OneQ.OnSurvey.domain.member.MemberErrorCode; +import OneQ.OnSurvey.domain.member.dto.MemberInfoResponse; +import OneQ.OnSurvey.domain.member.dto.MemberSearchResult; +import OneQ.OnSurvey.domain.member.repository.MemberRepository; +import OneQ.OnSurvey.domain.member.value.MemberStatus; +import OneQ.OnSurvey.domain.member.value.Role; +import OneQ.OnSurvey.domain.survey.model.Gender; +import OneQ.OnSurvey.global.common.exception.CustomException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class MemberQueryServiceTest { + + @Mock + private MemberRepository memberRepository; + + @InjectMocks + private MemberQueryService memberQueryService; + + private Member buildMember(Long userKey) { + return Member.createMember( + userKey, "홍길동", "010-1234-5678", + "19900101", "test@test.com", + Gender.MALE, Role.ROLE_MEMBER, MemberStatus.ACTIVE + ); + } + + @Test + @DisplayName("getMemberByUserKey - 멤버 존재 시 반환") + void getMemberByUserKey_found_returnsMember() { + Member member = buildMember(1000L); + given(memberRepository.findMemberByUserKey(1000L)).willReturn(Optional.of(member)); + + Member result = memberQueryService.getMemberByUserKey(1000L); + + assertThat(result).isEqualTo(member); + assertThat(result.getUserKey()).isEqualTo(1000L); + } + + @Test + @DisplayName("getMemberByUserKey - 멤버 없으면 MEMBER_NOT_FOUND 예외") + void getMemberByUserKey_notFound_throwsException() { + given(memberRepository.findMemberByUserKey(999L)).willReturn(Optional.empty()); + + assertThatThrownBy(() -> memberQueryService.getMemberByUserKey(999L)) + .isInstanceOf(CustomException.class) + .satisfies(ex -> assertThat(((CustomException) ex).getErrorCode()) + .isEqualTo(MemberErrorCode.MEMBER_NOT_FOUND)); + } + + @Test + @DisplayName("getMemberInfo - 멤버 존재 시 MemberInfoResponse 반환") + void getMemberInfo_found_returnsResponse() { + Member member = Member.builder() + .userKey(2000L) + .name("김철수") + .profileUrl("https://example.com/img.jpg") + .coin(500L) + .promotionPoint(100L) + .onboardingCompleted(true) + .role(Role.ROLE_MEMBER) + .status(MemberStatus.ACTIVE) + .build(); + given(memberRepository.findMemberByUserKey(2000L)).willReturn(Optional.of(member)); + + MemberInfoResponse response = memberQueryService.getMemberInfo(2000L); + + assertThat(response.name()).isEqualTo("김철수"); + assertThat(response.profileUrl()).isEqualTo("https://example.com/img.jpg"); + assertThat(response.coin()).isEqualTo(500L); + assertThat(response.promotionPoint()).isEqualTo(100L); + assertThat(response.isOnboardingCompleted()).isTrue(); + } + + @Test + @DisplayName("getMemberInfo - 멤버 없으면 MEMBER_NOT_FOUND 예외") + void getMemberInfo_notFound_throwsException() { + given(memberRepository.findMemberByUserKey(999L)).willReturn(Optional.empty()); + + assertThatThrownBy(() -> memberQueryService.getMemberInfo(999L)) + .isInstanceOf(CustomException.class) + .satisfies(ex -> assertThat(((CustomException) ex).getErrorCode()) + .isEqualTo(MemberErrorCode.MEMBER_NOT_FOUND)); + } + + @Test + @DisplayName("validateAdminRoleAndGetMemberIdByUserKey - repository 결과 그대로 반환") + void validateAdminRoleAndGetMemberIdByUserKey_delegatesToRepository() { + given(memberRepository.validateAdminRoleAndGetMemberIdByUserKey(3000L)).willReturn(42L); + + Long result = memberQueryService.validateAdminRoleAndGetMemberIdByUserKey(3000L); + + assertThat(result).isEqualTo(42L); + verify(memberRepository).validateAdminRoleAndGetMemberIdByUserKey(3000L); + } + + @Test + @DisplayName("searchMembers - 결과 목록을 MemberSearchResult로 변환") + void searchMembers_returnsMappedList() { + Member m1 = buildMember(1001L); + Member m2 = buildMember(1002L); + given(memberRepository.searchMembers("test@test.com", null, null, null)) + .willReturn(List.of(m1, m2)); + + List results = memberQueryService.searchMembers("test@test.com", null, null, null); + + assertThat(results).hasSize(2); + assertThat(results.get(0).userKey()).isEqualTo(1001L); + assertThat(results.get(1).userKey()).isEqualTo(1002L); + } + + @Test + @DisplayName("searchMembers - 결과 없으면 빈 리스트 반환") + void searchMembers_emptyResult_returnsEmptyList() { + given(memberRepository.searchMembers(null, null, null, "없는이름")) + .willReturn(List.of()); + + List results = memberQueryService.searchMembers(null, null, null, "없는이름"); + + assertThat(results).isEmpty(); + } + + @Test + @DisplayName("getUsernameByUserKey - repository 결과 그대로 반환") + void getUsernameByUserKey_delegatesToRepository() { + given(memberRepository.getUsernameByUserKey(5000L)).willReturn("홍길동"); + + String result = memberQueryService.getUsernameByUserKey(5000L); + + assertThat(result).isEqualTo("홍길동"); + verify(memberRepository).getUsernameByUserKey(5000L); + } +} From 77d96591055885ee8204ad489bebcfeeb7a00c19 Mon Sep 17 00:00:00 2001 From: wonjuneee Date: Sun, 10 May 2026 15:13:13 +0900 Subject: [PATCH 08/16] =?UTF-8?q?mod:=20=EC=84=A4=EB=AC=B8=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EC=A1=B0=ED=9A=8C=20=EC=8B=9C=20=EC=84=B9=EC=85=98?= =?UTF-8?q?=20=EA=B0=9C=EC=88=98=EB=A5=BC=20=EB=B0=98=ED=99=98=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95=ED=95=98=EA=B3=A0,=20?= =?UTF-8?q?=EC=BF=BC=EB=A6=AC=EB=AC=B8=20=EC=B5=9C=EC=A0=81=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../survey/model/dto/ParticipationInfoVO.java | 37 ++++++++++++++++++ .../response/ParticipationInfoResponse.java | 19 +++------ .../survey/repository/SurveyRepository.java | 2 + .../repository/SurveyRepositoryImpl.java | 39 +++++++++++++++++++ .../service/query/SurveyQueryService.java | 19 +++++---- 5 files changed, 93 insertions(+), 23 deletions(-) create mode 100644 src/main/java/OneQ/OnSurvey/domain/survey/model/dto/ParticipationInfoVO.java diff --git a/src/main/java/OneQ/OnSurvey/domain/survey/model/dto/ParticipationInfoVO.java b/src/main/java/OneQ/OnSurvey/domain/survey/model/dto/ParticipationInfoVO.java new file mode 100644 index 00000000..0ac0db40 --- /dev/null +++ b/src/main/java/OneQ/OnSurvey/domain/survey/model/dto/ParticipationInfoVO.java @@ -0,0 +1,37 @@ +package OneQ.OnSurvey.domain.survey.model.dto; + +import OneQ.OnSurvey.domain.member.value.Interest; +import OneQ.OnSurvey.domain.participation.model.dto.ParticipationStatus; + +import java.time.LocalDateTime; +import java.util.Set; + +public record ParticipationInfoVO( + Long surveyId, + String title, + String description, + Integer totalSections, + LocalDateTime deadline, + Set interests, + ParticipationStatus participationStatus, + Boolean isFree +) { + public ParticipationInfoVO( + Long surveyId, + String title, + String description, + Integer totalSections, + LocalDateTime deadline, + Set interests, + Long screeningId, + Boolean eIsScreened, + Boolean eIsResponded, + Boolean isFree + ) { + this( + surveyId, title, description, totalSections, deadline, interests, + ParticipationStatus.generateStatus(screeningId, eIsScreened, eIsResponded), + isFree + ); + } +} diff --git a/src/main/java/OneQ/OnSurvey/domain/survey/model/response/ParticipationInfoResponse.java b/src/main/java/OneQ/OnSurvey/domain/survey/model/response/ParticipationInfoResponse.java index 4d4e7bac..9c1d9f89 100644 --- a/src/main/java/OneQ/OnSurvey/domain/survey/model/response/ParticipationInfoResponse.java +++ b/src/main/java/OneQ/OnSurvey/domain/survey/model/response/ParticipationInfoResponse.java @@ -1,8 +1,7 @@ package OneQ.OnSurvey.domain.survey.model.response; import OneQ.OnSurvey.domain.member.value.Interest; -import OneQ.OnSurvey.domain.participation.model.dto.ParticipationStatus; -import OneQ.OnSurvey.domain.survey.entity.Survey; +import OneQ.OnSurvey.domain.survey.model.dto.ParticipationInfoVO; import java.time.LocalDateTime; import java.util.Set; @@ -11,6 +10,7 @@ public record ParticipationInfoResponse( Long surveyId, String title, String description, + Integer totalSections, LocalDateTime deadline, Set interests, Integer responseCount, @@ -20,19 +20,12 @@ public record ParticipationInfoResponse( Boolean isFree ) { public static ParticipationInfoResponse from( - Survey survey, int responseCount, ParticipationStatus participationStatus + ParticipationInfoVO vo, int responseCount ) { return new ParticipationInfoResponse( - survey.getId(), - survey.getTitle(), - survey.getDescription(), - survey.getDeadline(), - survey.getInterests(), - responseCount, - participationStatus.isScreenRequired(), - participationStatus.isScreened(), - participationStatus.isSurveyResponded(), - survey.getIsFree() + vo.surveyId(), vo.title(), vo.description(), vo.totalSections(), vo.deadline(), vo.interests(), responseCount, + vo.participationStatus().isScreenRequired(), vo.participationStatus().isScreened(), vo.participationStatus().isSurveyResponded(), + vo.isFree() ); } } diff --git a/src/main/java/OneQ/OnSurvey/domain/survey/repository/SurveyRepository.java b/src/main/java/OneQ/OnSurvey/domain/survey/repository/SurveyRepository.java index 621e0cfe..75ec5cb3 100644 --- a/src/main/java/OneQ/OnSurvey/domain/survey/repository/SurveyRepository.java +++ b/src/main/java/OneQ/OnSurvey/domain/survey/repository/SurveyRepository.java @@ -7,6 +7,7 @@ import OneQ.OnSurvey.domain.survey.model.dto.OngoingSurveyStats; import OneQ.OnSurvey.domain.survey.model.dto.OpenSurveyStats; import OneQ.OnSurvey.domain.survey.model.dto.SurveyDetailData; +import OneQ.OnSurvey.domain.survey.model.dto.ParticipationInfoVO; import OneQ.OnSurvey.domain.survey.model.dto.SurveyListView; import OneQ.OnSurvey.domain.survey.model.dto.SurveySearchQuery; import OneQ.OnSurvey.domain.survey.model.dto.SurveyWithEligibility; @@ -39,4 +40,5 @@ Slice getSurveyListWithEligibility( List findOngoingSurveys(); OpenSurveyStats findOpenSurveyStats(); + ParticipationInfoVO getParticipationInfoVO(Long surveyId, Long memberId); } diff --git a/src/main/java/OneQ/OnSurvey/domain/survey/repository/SurveyRepositoryImpl.java b/src/main/java/OneQ/OnSurvey/domain/survey/repository/SurveyRepositoryImpl.java index 42a6935c..b9aa87b4 100644 --- a/src/main/java/OneQ/OnSurvey/domain/survey/repository/SurveyRepositoryImpl.java +++ b/src/main/java/OneQ/OnSurvey/domain/survey/repository/SurveyRepositoryImpl.java @@ -10,6 +10,7 @@ import OneQ.OnSurvey.domain.survey.model.SurveyStatus; import OneQ.OnSurvey.domain.survey.model.dto.OngoingSurveyStats; import OneQ.OnSurvey.domain.survey.model.dto.OpenSurveyStats; +import OneQ.OnSurvey.domain.survey.model.dto.ParticipationInfoVO; import OneQ.OnSurvey.domain.survey.model.dto.SurveyDetailData; import OneQ.OnSurvey.domain.survey.model.dto.SurveyListView; import OneQ.OnSurvey.domain.survey.model.dto.SurveySearchQuery; @@ -40,6 +41,7 @@ import static OneQ.OnSurvey.domain.survey.entity.QScreening.screening; import static OneQ.OnSurvey.domain.survey.entity.QSurvey.survey; import static OneQ.OnSurvey.domain.survey.entity.QSurveyInfo.surveyInfo; +import static OneQ.OnSurvey.domain.question.entity.QSection.section; import static com.querydsl.core.group.GroupBy.groupBy; import static com.querydsl.core.group.GroupBy.set; @@ -337,4 +339,41 @@ public OpenSurveyStats findOpenSurveyStats() { Integer maxCoin = result.get(surveyInfo.promotionAmount.max()); return OpenSurveyStats.of(count, maxCoin); } + + @Override + public ParticipationInfoVO getParticipationInfoVO(Long surveyId, Long memberId) { + EnumPath interestAlias = Expressions.enumPath(Interest.class, "interestAlias"); + + return jpaQueryFactory + .from(survey) + .join(section).on(survey.id.eq(section.surveyId)) + .leftJoin(survey.interests, interestAlias) + .leftJoin(screening).on( + survey.id.eq(screening.surveyId) + ) + .leftJoin(response).on( + survey.id.eq(response.surveyId), + response.memberId.eq(memberId) + ) + .where( + survey.id.eq(surveyId), + survey.status.eq(SurveyStatus.ONGOING) + ) + .groupBy(survey.id, survey.title, survey.description, survey.deadline, interestAlias, + screening.id, response.isScreened, response.isResponded, survey.isFree + ) + .transform( + groupBy(survey.id).as(Projections.constructor(ParticipationInfoVO.class, + survey.id, + survey.title, + survey.description, + section.sectionId.countDistinct().intValue(), + survey.deadline, + set(interestAlias), + screening.id, // 스크리닝 존재 여부 + response.isScreened, // 스크리닝 응답 여부 + response.isResponded, // 설문 응답 여부 + survey.isFree + ))).get(surveyId); + } } diff --git a/src/main/java/OneQ/OnSurvey/domain/survey/service/query/SurveyQueryService.java b/src/main/java/OneQ/OnSurvey/domain/survey/service/query/SurveyQueryService.java index 2daa19ee..3946882e 100644 --- a/src/main/java/OneQ/OnSurvey/domain/survey/service/query/SurveyQueryService.java +++ b/src/main/java/OneQ/OnSurvey/domain/survey/service/query/SurveyQueryService.java @@ -228,27 +228,26 @@ public ParticipationInfoResponse getParticipationInfo(Long surveyId, Long userKe throw new CustomException(SurveyErrorCode.SURVEY_WRONG_SEGMENTATION); } - Survey survey = surveyRepository.getSurveyById(surveyId) - .orElseThrow(() -> new CustomException(SurveyErrorCode.SURVEY_NOT_FOUND)); + ParticipationInfoVO vo = surveyRepository.getParticipationInfoVO(surveyId, memberId); - if (!isSurveyAccessible(survey.getStatus())) { - log.warn("[SURVEY:QUERY] 마감된 설문 참여 불가 - surveyId: {}, status: {}", surveyId, survey.getStatus()); + // surveyId에 해당하는 설문이 없거나 상태가 ONGOING이 아닌 경우 + if (vo == null) { + log.warn("[SURVEY:QUERY] 마감된 설문 참여 불가 - surveyId: {}", surveyId); throw new CustomException(SurveyErrorCode.SURVEY_INCORRECT_STATUS); } - int completedCount = redisAgent.getIntValue(this.completedKey + surveyId); - ParticipationStatus participationStatus = surveyRepository.getParticipationStatus(surveyId, memberId); - if (participationStatus.isScreenRequired()) { + + if (vo.participationStatus().isScreenRequired()) { log.warn("[SURVEY:QUERY] 스크리닝 퀴즈 응답이 필요합니다. - surveyId: {}, memberId: {}", surveyId, memberId); } - if (participationStatus.isScreened()) { + if (vo.participationStatus().isScreened()) { log.warn("[SURVEY:QUERY] 스크리닝 퀴즈에 의해 필터링되었습니다. - surveyId: {}, memberId: {}", surveyId, memberId); } - if (participationStatus.isSurveyResponded()) { + if (vo.participationStatus().isSurveyResponded()) { log.warn("[SURVEY:QUERY] 이미 참여한 설문입니다. - surveyId: {}, memberId: {}", surveyId, memberId); } - return ParticipationInfoResponse.from(survey, completedCount, participationStatus); + return ParticipationInfoResponse.from(vo, completedCount); } @Override From 7d858115da9e49b7c6ae571bfc8e3d64e97c2d94 Mon Sep 17 00:00:00 2001 From: wonjuneee Date: Sun, 10 May 2026 15:21:29 +0900 Subject: [PATCH 09/16] =?UTF-8?q?chore:=20Deprecated=EB=90=9C=20API=20?= =?UTF-8?q?=EB=B0=8F=20=EB=AF=B8=EC=82=AC=EC=9A=A9=20=EB=A9=94=EC=84=9C?= =?UTF-8?q?=EB=93=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ParticipationController.java | 126 ------------------ .../response/DeprecatedQuestionResponse.java | 49 ------- .../survey/repository/SurveyRepository.java | 5 - .../repository/SurveyRepositoryImpl.java | 32 ----- .../survey/service/query/SurveyQuery.java | 8 -- .../service/query/SurveyQueryService.java | 49 ------- 6 files changed, 269 deletions(-) delete mode 100644 src/main/java/OneQ/OnSurvey/domain/survey/model/response/DeprecatedQuestionResponse.java diff --git a/src/main/java/OneQ/OnSurvey/domain/survey/controller/ParticipationController.java b/src/main/java/OneQ/OnSurvey/domain/survey/controller/ParticipationController.java index b2d09ff1..36eddc13 100644 --- a/src/main/java/OneQ/OnSurvey/domain/survey/controller/ParticipationController.java +++ b/src/main/java/OneQ/OnSurvey/domain/survey/controller/ParticipationController.java @@ -4,23 +4,16 @@ import OneQ.OnSurvey.domain.participation.entity.ScreeningAnswer; import OneQ.OnSurvey.domain.participation.model.dto.AnswerInsertDto; import OneQ.OnSurvey.domain.participation.model.dto.ParticipationCompletionDto; -import OneQ.OnSurvey.domain.participation.model.dto.ParticipationStatus; import OneQ.OnSurvey.domain.participation.service.answer.AnswerCommand; import OneQ.OnSurvey.domain.participation.service.response.ResponseCommand; -import OneQ.OnSurvey.domain.question.model.dto.type.DefaultQuestionDto; -import OneQ.OnSurvey.domain.question.service.QuestionQueryService; -import OneQ.OnSurvey.domain.survey.SurveyErrorCode; -import OneQ.OnSurvey.domain.survey.entity.Survey; import OneQ.OnSurvey.domain.survey.model.SurveyStatus; import OneQ.OnSurvey.domain.survey.model.request.InsertQuestionAnswerRequest; import OneQ.OnSurvey.domain.survey.model.request.InsertScreeningAnswerRequest; import OneQ.OnSurvey.domain.survey.model.request.SurveyParticipationCompletionRequest; import OneQ.OnSurvey.domain.survey.model.response.*; -import OneQ.OnSurvey.domain.survey.repository.SurveyRepository; import OneQ.OnSurvey.domain.survey.service.command.SurveyCommandService; import OneQ.OnSurvey.domain.survey.service.query.SurveyQuery; import OneQ.OnSurvey.global.auth.custom.CustomUserDetails; -import OneQ.OnSurvey.global.common.exception.CustomException; import OneQ.OnSurvey.global.common.response.SuccessResponse; import io.swagger.v3.oas.annotations.Operation; import jakarta.validation.Valid; @@ -32,9 +25,6 @@ import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; -import java.time.LocalDateTime; -import java.util.List; - @Slf4j @RestController @RequestMapping("/v1/survey-participation") @@ -47,9 +37,6 @@ public class ParticipationController { private final ResponseCommand responseCommand; private final SurveyCommandService surveyCommandService; - private final QuestionQueryService questionQueryService; - private final SurveyRepository surveyRepository; - @GetMapping("surveys/ongoing/all") @Operation(summary = "열려있는 설문을 모두 조회합니다.") public SuccessResponse getOngoingSurveyList( @@ -68,89 +55,6 @@ public SuccessResponse getOngoingSurveyList( return SuccessResponse.ok(results); } - @Deprecated(forRemoval = true) - @GetMapping("surveys/ongoing") - @Operation(summary = "노출 중인 설문을 조회합니다.") - public SuccessResponse getSurveyListOnGoing( - @AuthenticationPrincipal CustomUserDetails principal, - @RequestParam(required = false, defaultValue = "0") Long lastSurveyId, - @RequestParam(defaultValue = "15") Integer size - ) { - log.info("[PARTICIPATION] 노출 중 설문 조회 - lastSurveyId: {}, size: {}", lastSurveyId, size); - - Pageable recommendedPageable = PageRequest.of(0, size, Sort.by("id")); - Pageable impendingPageable = PageRequest.of(0, size, Sort.by( - Sort.Order.asc("deadline"), - Sort.Order.asc("id") - )); - - SurveyParticipationResponse.SliceSurveyData recommended = surveyQueryService.getParticipationSurveyList( - lastSurveyId, recommendedPageable, SurveyStatus.ONGOING, principal.getMemberId(), principal.getUserKey() - ); - SurveyParticipationResponse.SliceSurveyData impending = surveyQueryService.getParticipationSurveyList( - lastSurveyId, LocalDateTime.now(), impendingPageable, SurveyStatus.ONGOING, principal.getMemberId(), principal.getUserKey() - ); - - SurveyParticipationResponse response = SurveyParticipationResponse.builder() - .recommended(recommended.getSurveyDataList()) - .impending(impending.getSurveyDataList()) - .recommendedHasNext(recommended.getHasNext()) - .impendingHasNext(impending.getHasNext()) - .build(); - - return SuccessResponse.ok(response); - } - - @Deprecated(forRemoval = true) - @GetMapping("surveys/ongoing/recommended") - @Operation(summary = "사용자 추천 설문을 조회합니다.") - public SuccessResponse getRecommendedSurveyList( - @AuthenticationPrincipal CustomUserDetails principal, - @RequestParam(required = false, defaultValue = "0") Long lastSurveyId, - @RequestParam(defaultValue = "15") Integer size - ) { - log.info("[PARTICIPATION] 사용자 추천 설문 조회 - lastSurveyId: {}, size: {}", lastSurveyId, size); - - Pageable pageable = PageRequest.of(0, size, Sort.by("id")); - SurveyParticipationResponse.SliceSurveyData recommended = - surveyQueryService.getParticipationSurveyList(lastSurveyId, pageable, SurveyStatus.ONGOING, principal.getMemberId(), principal.getUserKey() - ); - - SurveyParticipationResponse response = SurveyParticipationResponse.builder() - .recommended(recommended.getSurveyDataList()) - .recommendedHasNext(recommended.getHasNext()) - .build(); - - return SuccessResponse.ok(response); - } - - @Deprecated(forRemoval = true) - @GetMapping("surveys/ongoing/impending") - @Operation(summary = "마감 임박 설문을 조회합니다.") - public SuccessResponse getImpendingSurveyList( - @AuthenticationPrincipal CustomUserDetails principal, - @RequestParam(required = false, defaultValue = "0") Long lastSurveyId, - @RequestParam(required = false) LocalDateTime lastDeadline, - @RequestParam(defaultValue = "15") Integer size - ) { - log.info("[PARTICIPATION] 마감 임박 설문 조회 - lastSurveyId: {}, lastDeadline: {}, size: {}", lastSurveyId, lastDeadline, size); - - Pageable pageable = PageRequest.of(0, size, Sort.by( - Sort.Order.asc("deadline"), - Sort.Order.asc("id") - )); - SurveyParticipationResponse.SliceSurveyData impending = - surveyQueryService.getParticipationSurveyList(lastSurveyId, lastDeadline, pageable, SurveyStatus.ONGOING, principal.getMemberId(), principal.getUserKey() - ); - - SurveyParticipationResponse response = SurveyParticipationResponse.builder() - .impending(impending.getSurveyDataList()) - .impendingHasNext(impending.getHasNext()) - .build(); - - return SuccessResponse.ok(response); - } - @GetMapping("surveys/info") @Operation(summary = "선택한 설문의 기본 정보를 조회합니다.") public SuccessResponse getSurveyInfo( @@ -174,36 +78,6 @@ public SuccessResponse getQuestionsOfSurveyId( return SuccessResponse.ok(surveyQueryService.getParticipationQuestionInfo(surveyId, section, principal.getUserKey())); } - /** - * @deprecated - * @code GET /surveys/info - * @code GET /surveys/questions - */ - @Deprecated(forRemoval = true) - @GetMapping("surveys") - @Operation(summary = "선택한 설문을 조회합니다.") - public SuccessResponse getTotalSurveyInfoOfSurveyId( - @RequestParam Long surveyId, - @AuthenticationPrincipal CustomUserDetails principal - ) { - log.info("[PARTICIPATION] 응답하고자 하는 설문 문항조회 - surveyId: {}", surveyId); - - Survey survey = surveyQueryService.getSurveyById(surveyId); - - if (surveyQueryService.checkValidSegmentation(surveyId, principal.getUserKey())) { - log.warn("[PARTICIPATION] 세그먼트 불일치로 인한 설문 응답 불가 - surveyId: {}, userKey: {}", surveyId, principal.getUserKey()); - throw new CustomException(SurveyErrorCode.SURVEY_WRONG_SEGMENTATION); - } - - List questionDtoList = questionQueryService.getQuestionDtoListBySurveyId(surveyId); - ParticipationStatus participationStatus = surveyRepository.getParticipationStatus(surveyId, principal.getMemberId()); - - DeprecatedQuestionResponse body = - DeprecatedQuestionResponse.of(survey, questionDtoList, participationStatus); - - return SuccessResponse.ok(body); - } - @GetMapping("surveys/screenings") @Operation(summary = "세그멘테이션에 일치하는 설문의 스크리닝 문항을 조회합니다.") public SuccessResponse getRecommendedScreenings( diff --git a/src/main/java/OneQ/OnSurvey/domain/survey/model/response/DeprecatedQuestionResponse.java b/src/main/java/OneQ/OnSurvey/domain/survey/model/response/DeprecatedQuestionResponse.java deleted file mode 100644 index 67c7d6f2..00000000 --- a/src/main/java/OneQ/OnSurvey/domain/survey/model/response/DeprecatedQuestionResponse.java +++ /dev/null @@ -1,49 +0,0 @@ -package OneQ.OnSurvey.domain.survey.model.response; - -import OneQ.OnSurvey.domain.member.value.Interest; -import OneQ.OnSurvey.domain.participation.model.dto.ParticipationStatus; -import OneQ.OnSurvey.domain.question.model.dto.type.DefaultQuestionDto; -import OneQ.OnSurvey.domain.survey.entity.Survey; - -import java.time.LocalDateTime; -import java.util.List; -import java.util.Set; - -public record DeprecatedQuestionResponse( - Long surveyId, - Long memberId, - String title, - String description, - Boolean isFree, - Set interests, - LocalDateTime deadline, - List info, - boolean isScreenRequired, - boolean isScreened, - boolean isSurveyResponded -) { - - public static DeprecatedQuestionResponse of( - Survey survey, - List info, - ParticipationStatus participationStatus - ) { - Set interestsSet = survey.getInterests() != null - ? new java.util.HashSet<>(survey.getInterests()) - : java.util.Collections.emptySet(); - - return new DeprecatedQuestionResponse( - survey.getId(), - survey.getMemberId(), - survey.getTitle(), - survey.getDescription(), - survey.getIsFree(), - interestsSet, - survey.getDeadline(), - info, - participationStatus.isScreenRequired(), - participationStatus.isScreened(), - participationStatus.isSurveyResponded() - ); - } -} diff --git a/src/main/java/OneQ/OnSurvey/domain/survey/repository/SurveyRepository.java b/src/main/java/OneQ/OnSurvey/domain/survey/repository/SurveyRepository.java index 75ec5cb3..8279fdf1 100644 --- a/src/main/java/OneQ/OnSurvey/domain/survey/repository/SurveyRepository.java +++ b/src/main/java/OneQ/OnSurvey/domain/survey/repository/SurveyRepository.java @@ -1,7 +1,6 @@ package OneQ.OnSurvey.domain.survey.repository; import OneQ.OnSurvey.domain.member.dto.MemberSegmentation; -import OneQ.OnSurvey.domain.participation.model.dto.ParticipationStatus; import OneQ.OnSurvey.domain.survey.entity.Survey; import OneQ.OnSurvey.domain.survey.model.SurveyStatus; import OneQ.OnSurvey.domain.survey.model.dto.OngoingSurveyStats; @@ -31,13 +30,9 @@ List getSurveyIdListByFilters( Slice getSurveyListWithEligibility( Long lastSurveyId, LocalDateTime lastDeadline, Pageable pageable, SurveyStatus status, Long creatorId, Collection excludedIds, MemberSegmentation memberSegmentation); - Survey save(Survey survey); - SurveyStatus getSurveyStatusById(Long surveyId); - ParticipationStatus getParticipationStatus(Long surveyId, Long memberId); List closeDueSurveys(); - List findOngoingSurveys(); OpenSurveyStats findOpenSurveyStats(); ParticipationInfoVO getParticipationInfoVO(Long surveyId, Long memberId); diff --git a/src/main/java/OneQ/OnSurvey/domain/survey/repository/SurveyRepositoryImpl.java b/src/main/java/OneQ/OnSurvey/domain/survey/repository/SurveyRepositoryImpl.java index b9aa87b4..0675b559 100644 --- a/src/main/java/OneQ/OnSurvey/domain/survey/repository/SurveyRepositoryImpl.java +++ b/src/main/java/OneQ/OnSurvey/domain/survey/repository/SurveyRepositoryImpl.java @@ -2,7 +2,6 @@ import OneQ.OnSurvey.domain.member.dto.MemberSegmentation; import OneQ.OnSurvey.domain.member.value.Interest; -import OneQ.OnSurvey.domain.participation.model.dto.ParticipationStatus; import OneQ.OnSurvey.domain.survey.entity.Survey; import OneQ.OnSurvey.domain.survey.model.AgeRange; import OneQ.OnSurvey.domain.survey.model.Gender; @@ -17,7 +16,6 @@ import OneQ.OnSurvey.domain.survey.model.dto.SurveyWithEligibility; import OneQ.OnSurvey.global.common.util.QuerydslUtils; import com.querydsl.core.BooleanBuilder; -import com.querydsl.core.Tuple; import com.querydsl.core.types.Expression; import com.querydsl.core.types.Projections; import com.querydsl.core.types.dsl.BooleanExpression; @@ -200,36 +198,6 @@ public SurveyStatus getSurveyStatusById(Long surveyId) { .fetchOne(); } - @Override - public ParticipationStatus getParticipationStatus(Long surveyId, Long memberId) { - Tuple statusResult = jpaQueryFactory - .select( - screening.id, // 스크리닝 존재 여부 - response.isScreened, // 스크리닝 응답 여부 - response.isResponded // 설문 응답 여부 - ) - .from(survey) - .leftJoin(screening).on( - survey.id.eq(screening.surveyId) - ) - .leftJoin(response).on( - survey.id.eq(response.surveyId), - response.memberId.eq(memberId) - ) - .where(survey.id.eq(surveyId)) - .fetchOne(); - - if (statusResult == null) { - return ParticipationStatus.defaultStatus(false); - } - - Long screeningId = statusResult.get(screening.id); - Boolean isScreened = statusResult.get(response.isScreened); - Boolean isResponded = statusResult.get(response.isResponded); - - return ParticipationStatus.generateStatus(screeningId, isScreened, isResponded); - } - @Override public List getSurveyIdListByFilters( Long lastSurveyId, LocalDateTime lastDeadline, Pageable pageable, diff --git a/src/main/java/OneQ/OnSurvey/domain/survey/service/query/SurveyQuery.java b/src/main/java/OneQ/OnSurvey/domain/survey/service/query/SurveyQuery.java index ad53bf23..2f530d62 100644 --- a/src/main/java/OneQ/OnSurvey/domain/survey/service/query/SurveyQuery.java +++ b/src/main/java/OneQ/OnSurvey/domain/survey/service/query/SurveyQuery.java @@ -13,7 +13,6 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; -import java.time.LocalDateTime; import java.util.List; public interface SurveyQuery { @@ -22,13 +21,6 @@ public interface SurveyQuery { SurveyParticipationResponse getParticipationSurveySlice( Long lastSurveyId, Pageable pageable, SurveyStatus status, Long memberId, Long userKey ); - - SurveyParticipationResponse.SliceSurveyData getParticipationSurveyList( - Long lastSurveyId, Pageable pageable, SurveyStatus status, Long memberId, Long userKey - ); - SurveyParticipationResponse.SliceSurveyData getParticipationSurveyList( - Long lastSurveyId, LocalDateTime lastDeadline, Pageable pageable, SurveyStatus status, Long memberId, Long userKey - ); ParticipationScreeningListResponse getScreeningList( Long lastSurveyId, Pageable pageable, Long memberId, Long userKey ); diff --git a/src/main/java/OneQ/OnSurvey/domain/survey/service/query/SurveyQueryService.java b/src/main/java/OneQ/OnSurvey/domain/survey/service/query/SurveyQueryService.java index 3946882e..efc75d0f 100644 --- a/src/main/java/OneQ/OnSurvey/domain/survey/service/query/SurveyQueryService.java +++ b/src/main/java/OneQ/OnSurvey/domain/survey/service/query/SurveyQueryService.java @@ -3,7 +3,6 @@ import OneQ.OnSurvey.domain.member.dto.MemberSegmentation; import OneQ.OnSurvey.domain.member.repository.MemberRepository; import OneQ.OnSurvey.domain.member.value.Interest; -import OneQ.OnSurvey.domain.participation.model.dto.ParticipationStatus; import OneQ.OnSurvey.domain.participation.repository.response.ResponseRepository; import OneQ.OnSurvey.domain.question.model.dto.SectionDto; import OneQ.OnSurvey.domain.question.model.dto.type.DefaultQuestionDto; @@ -143,54 +142,6 @@ public SurveyParticipationResponse getParticipationSurveySlice( .build(); } - @Override - public SurveyParticipationResponse.SliceSurveyData getParticipationSurveyList( - Long lastSurveyId, Pageable pageable, SurveyStatus status, Long memberId, Long userKey - ) { - log.info("[SURVEY:QUERY:getParticipationSurveyList] 본인 제작 제외 설문 조회 - " - + "lastSurveyId: {}, size: {}, status: {}, userKey: {}", - lastSurveyId, pageable.getPageSize(), status.name(), userKey - ); - - List excludedIdList = responseRepository.getExcludedSurveyIdList(memberId, true); - MemberSegmentation memberSegmentation = memberRepository.findMemberSegmentByUserKey(userKey); - log.info("[SURVEY:QUERY:getParticipationSurveyList] 사용자 세그멘테이션 - userKey: {}, memberSegmentation: {}, excludedIdList: {}", - userKey, memberSegmentation, excludedIdList); - - Slice recommendedList = surveyRepository.getSurveyListWithEligibility( - lastSurveyId, null, pageable, status, memberId, excludedIdList, memberSegmentation - ); - log.info("[SURVEY:QUERY:getParticipationSurveyList] 추천 설문 조회 결과 - recommended: {}", recommendedList); - - return new SurveyParticipationResponse.SliceSurveyData( - recommendedList.stream().map(SurveyParticipationResponse::from).toList(), recommendedList.hasNext() - ); - } - - @Override - public SurveyParticipationResponse.SliceSurveyData getParticipationSurveyList( - Long lastSurveyId, LocalDateTime lastDeadline, Pageable pageable, SurveyStatus status, Long memberId, Long userKey - ) { - log.info("[SURVEY:QUERY:getParticipationSurveyList] 본인 제작 제외 마감기한 기반 설문 조회 - " - + "lastSurveyId: {}, lastDateTime: {}, size: {}, status: {}, userKey: {}", - lastSurveyId, lastDeadline, pageable.getPageSize(), status.name(), userKey - ); - - List excludedIdList = responseRepository.getExcludedSurveyIdList(memberId, true); - MemberSegmentation memberSegmentation = memberRepository.findMemberSegmentByUserKey(userKey); - log.info("[SURVEY:QUERY:getParticipationSurveyList] 사용자 세그멘테이션 - userKey: {}, memberSegmentation: {}, excludedIdList: {}", - userKey, memberSegmentation, excludedIdList); - - Slice impendingList = surveyRepository.getSurveyListWithEligibility( - lastSurveyId, lastDeadline, pageable, status, memberId, excludedIdList, memberSegmentation - ); - log.info("[SURVEY:QUERY:getParticipationSurveyList] 마감임박 설문 조회 결과 - impending: {}", impendingList); - - return new SurveyParticipationResponse.SliceSurveyData( - impendingList.stream().map(SurveyParticipationResponse::from).toList(), impendingList.hasNext() - ); - } - @Override public ParticipationScreeningListResponse getScreeningList( Long lastSurveyId, Pageable pageable, Long memberId, Long userKey From 0b09e46add1f11b339781cd3eb6f325177c1c9fa Mon Sep 17 00:00:00 2001 From: wonjuneee Date: Sun, 10 May 2026 15:42:52 +0900 Subject: [PATCH 10/16] =?UTF-8?q?fix:=20section=EC=9D=B4=20=EC=97=86?= =?UTF-8?q?=EB=8A=94=20=EB=8B=A8=EC=9D=BC=20=EC=84=A4=EB=AC=B8=EC=9D=98=20?= =?UTF-8?q?=EC=84=B9=EC=85=98=20=EA=B0=9C=EC=88=98=EB=A5=BC=20=EA=B8=B0?= =?UTF-8?q?=EB=B3=B8=EA=B0=92=201=EB=A1=9C=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/survey/repository/SurveyRepositoryImpl.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/OneQ/OnSurvey/domain/survey/repository/SurveyRepositoryImpl.java b/src/main/java/OneQ/OnSurvey/domain/survey/repository/SurveyRepositoryImpl.java index 0675b559..7c4d03a9 100644 --- a/src/main/java/OneQ/OnSurvey/domain/survey/repository/SurveyRepositoryImpl.java +++ b/src/main/java/OneQ/OnSurvey/domain/survey/repository/SurveyRepositoryImpl.java @@ -16,6 +16,7 @@ import OneQ.OnSurvey.domain.survey.model.dto.SurveyWithEligibility; import OneQ.OnSurvey.global.common.util.QuerydslUtils; import com.querydsl.core.BooleanBuilder; +import com.querydsl.core.Tuple; import com.querydsl.core.types.Expression; import com.querydsl.core.types.Projections; import com.querydsl.core.types.dsl.BooleanExpression; @@ -314,7 +315,7 @@ public ParticipationInfoVO getParticipationInfoVO(Long surveyId, Long memberId) return jpaQueryFactory .from(survey) - .join(section).on(survey.id.eq(section.surveyId)) + .leftJoin(section).on(survey.id.eq(section.surveyId)) .leftJoin(survey.interests, interestAlias) .leftJoin(screening).on( survey.id.eq(screening.surveyId) @@ -335,7 +336,7 @@ public ParticipationInfoVO getParticipationInfoVO(Long surveyId, Long memberId) survey.id, survey.title, survey.description, - section.sectionId.countDistinct().intValue(), + section.sectionId.coalesce(1L).countDistinct().intValue(), // 최소 하나의 섹션 개수를 가지도록 coalesce 처리 survey.deadline, set(interestAlias), screening.id, // 스크리닝 존재 여부 From d679f6ba8c90ad430f238e524f95290d56ca4275 Mon Sep 17 00:00:00 2001 From: jaekwan Date: Sun, 10 May 2026 15:55:15 +0900 Subject: [PATCH 11/16] =?UTF-8?q?test:=20=EC=9E=94=EC=97=AC=20=EC=84=9C?= =?UTF-8?q?=EB=B9=84=EC=8A=A4=20=EB=A0=88=EC=9D=B4=EC=96=B4=20=EB=8B=A8?= =?UTF-8?q?=EC=9C=84=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=9D=BC=EA=B4=84=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DiscountCodeQueryService / SurveyQueryService / SurveyCommandService SurveyGlobalStatsService / ResponseQueryService — 총 36개 테스트 --- gradlew | 0 .../service/DiscountCodeQueryServiceTest.java | 134 +++++++++++ .../response/ResponseQueryServiceTest.java | 79 ++++++ .../service/SurveyGlobalStatsServiceTest.java | 130 ++++++++++ .../command/SurveyCommandServiceTest.java | 226 ++++++++++++++++++ .../service/query/SurveyQueryServiceTest.java | 148 ++++++++++++ 6 files changed, 717 insertions(+) mode change 100644 => 100755 gradlew create mode 100644 src/test/java/OneQ/OnSurvey/domain/discount/service/DiscountCodeQueryServiceTest.java create mode 100644 src/test/java/OneQ/OnSurvey/domain/participation/service/response/ResponseQueryServiceTest.java create mode 100644 src/test/java/OneQ/OnSurvey/domain/survey/service/SurveyGlobalStatsServiceTest.java create mode 100644 src/test/java/OneQ/OnSurvey/domain/survey/service/command/SurveyCommandServiceTest.java create mode 100644 src/test/java/OneQ/OnSurvey/domain/survey/service/query/SurveyQueryServiceTest.java diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/src/test/java/OneQ/OnSurvey/domain/discount/service/DiscountCodeQueryServiceTest.java b/src/test/java/OneQ/OnSurvey/domain/discount/service/DiscountCodeQueryServiceTest.java new file mode 100644 index 00000000..1cc5cbed --- /dev/null +++ b/src/test/java/OneQ/OnSurvey/domain/discount/service/DiscountCodeQueryServiceTest.java @@ -0,0 +1,134 @@ +package OneQ.OnSurvey.domain.discount.service; + +import OneQ.OnSurvey.domain.discount.DiscountCodeErrorCode; +import OneQ.OnSurvey.domain.discount.entity.DiscountCode; +import OneQ.OnSurvey.domain.discount.model.response.DiscountCodeResponse; +import OneQ.OnSurvey.domain.discount.model.response.ValidateDiscountCodeResponse; +import OneQ.OnSurvey.domain.discount.repository.DiscountCodeRepository; +import OneQ.OnSurvey.global.common.exception.CustomException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.LocalDate; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.BDDMockito.given; + +@ExtendWith(MockitoExtension.class) +class DiscountCodeQueryServiceTest { + + @Mock + private DiscountCodeRepository discountCodeRepository; + + @InjectMocks + private DiscountCodeQueryService discountCodeQueryService; + + private DiscountCode buildCode(String code, LocalDate expiredAt) { + return DiscountCode.of("테스트기업", code, expiredAt); + } + + @Test + @DisplayName("validate - 코드 없으면 DISCOUNT_CODE_NOT_FOUND 예외") + void validate_notFound_throwsException() { + given(discountCodeRepository.findByCode("XXXXXX")).willReturn(Optional.empty()); + + assertThatThrownBy(() -> discountCodeQueryService.validate("XXXXXX")) + .isInstanceOf(CustomException.class) + .satisfies(ex -> assertThat(((CustomException) ex).getErrorCode()) + .isEqualTo(DiscountCodeErrorCode.DISCOUNT_CODE_NOT_FOUND)); + } + + @Test + @DisplayName("validate - 만료된 코드면 DISCOUNT_CODE_EXPIRED 예외") + void validate_expired_throwsException() { + DiscountCode expired = buildCode("AAAAAA", LocalDate.now().minusDays(1)); + given(discountCodeRepository.findByCode("AAAAAA")).willReturn(Optional.of(expired)); + + assertThatThrownBy(() -> discountCodeQueryService.validate("AAAAAA")) + .isInstanceOf(CustomException.class) + .satisfies(ex -> assertThat(((CustomException) ex).getErrorCode()) + .isEqualTo(DiscountCodeErrorCode.DISCOUNT_CODE_EXPIRED)); + } + + @Test + @DisplayName("validate - 유효한 코드면 eligible=true 반환") + void validate_valid_returnsEligibleTrue() { + DiscountCode valid = buildCode("BBBBBB", LocalDate.now().plusDays(30)); + given(discountCodeRepository.findByCode("BBBBBB")).willReturn(Optional.of(valid)); + + ValidateDiscountCodeResponse response = discountCodeQueryService.validate("BBBBBB"); + + assertThat(response.eligible()).isTrue(); + } + + @Test + @DisplayName("getByCode - 코드 없으면 DISCOUNT_CODE_NOT_FOUND 예외") + void getByCode_notFound_throwsException() { + given(discountCodeRepository.findByCode("YYYYYY")).willReturn(Optional.empty()); + + assertThatThrownBy(() -> discountCodeQueryService.getByCode("YYYYYY")) + .isInstanceOf(CustomException.class) + .satisfies(ex -> assertThat(((CustomException) ex).getErrorCode()) + .isEqualTo(DiscountCodeErrorCode.DISCOUNT_CODE_NOT_FOUND)); + } + + @Test + @DisplayName("getByCode - 만료된 코드면 DISCOUNT_CODE_EXPIRED 예외") + void getByCode_expired_throwsException() { + DiscountCode expired = buildCode("CCCCCC", LocalDate.now().minusDays(1)); + given(discountCodeRepository.findByCode("CCCCCC")).willReturn(Optional.of(expired)); + + assertThatThrownBy(() -> discountCodeQueryService.getByCode("CCCCCC")) + .isInstanceOf(CustomException.class) + .satisfies(ex -> assertThat(((CustomException) ex).getErrorCode()) + .isEqualTo(DiscountCodeErrorCode.DISCOUNT_CODE_EXPIRED)); + } + + @Test + @DisplayName("getByCode - 유효한 코드면 엔티티 반환") + void getByCode_valid_returnsEntity() { + DiscountCode valid = buildCode("DDDDDD", LocalDate.now().plusDays(10)); + given(discountCodeRepository.findByCode("DDDDDD")).willReturn(Optional.of(valid)); + + DiscountCode result = discountCodeQueryService.getByCode("DDDDDD"); + + assertThat(result.getCode()).isEqualTo("DDDDDD"); + assertThat(result.getOrganizationName()).isEqualTo("테스트기업"); + } + + @Test + @DisplayName("findAll - 활성 코드가 만료 코드보다 먼저, 각 그룹 내 만료일 오름차순") + void findAll_sortedActiveFirst() { + LocalDate today = LocalDate.now(); + DiscountCode expired1 = buildCode("EXP001", today.minusDays(5)); + DiscountCode expired2 = buildCode("EXP002", today.minusDays(1)); + DiscountCode active1 = buildCode("ACT001", today.plusDays(10)); + DiscountCode active2 = buildCode("ACT002", today.plusDays(3)); + given(discountCodeRepository.findAll()).willReturn(List.of(expired1, active1, expired2, active2)); + + List results = discountCodeQueryService.findAll(); + + assertThat(results).hasSize(4); + assertThat(results.get(0).code()).isEqualTo("ACT002"); + assertThat(results.get(1).code()).isEqualTo("ACT001"); + assertThat(results.get(2).code()).isEqualTo("EXP001"); + assertThat(results.get(3).code()).isEqualTo("EXP002"); + } + + @Test + @DisplayName("findAll - 빈 목록이면 빈 리스트 반환") + void findAll_empty_returnsEmptyList() { + given(discountCodeRepository.findAll()).willReturn(List.of()); + + List results = discountCodeQueryService.findAll(); + + assertThat(results).isEmpty(); + } +} diff --git a/src/test/java/OneQ/OnSurvey/domain/participation/service/response/ResponseQueryServiceTest.java b/src/test/java/OneQ/OnSurvey/domain/participation/service/response/ResponseQueryServiceTest.java new file mode 100644 index 00000000..ed735909 --- /dev/null +++ b/src/test/java/OneQ/OnSurvey/domain/participation/service/response/ResponseQueryServiceTest.java @@ -0,0 +1,79 @@ +package OneQ.OnSurvey.domain.participation.service.response; + +import OneQ.OnSurvey.domain.participation.repository.response.ResponseRepository; +import OneQ.OnSurvey.domain.survey.model.AgeRange; +import OneQ.OnSurvey.domain.survey.model.Gender; +import OneQ.OnSurvey.domain.survey.model.Residence; +import OneQ.OnSurvey.domain.survey.model.SurveyResponseFilterCondition; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.never; + +@ExtendWith(MockitoExtension.class) +class ResponseQueryServiceTest { + + @Mock + private ResponseRepository responseRepository; + + @InjectMocks + private ResponseQueryService responseQueryService; + + @Test + @DisplayName("getResponseCountBySurveyId (필터 없음) - repository에 위임") + void getResponseCount_noFilter_delegatesToRepository() { + given(responseRepository.getResponseCountBySurveyId(1L)).willReturn(50); + + Integer result = responseQueryService.getResponseCountBySurveyId(1L); + + assertThat(result).isEqualTo(50); + verify(responseRepository).getResponseCountBySurveyId(1L); + } + + @Test + @DisplayName("getResponseCountBySurveyId - filter가 null이면 필터 없는 메서드 위임") + void getResponseCount_nullFilter_delegatesToNoFilter() { + given(responseRepository.getResponseCountBySurveyId(1L)).willReturn(30); + + Integer result = responseQueryService.getResponseCountBySurveyId(1L, null); + + assertThat(result).isEqualTo(30); + verify(responseRepository).getResponseCountBySurveyId(1L); + verify(responseRepository, never()).getResponseCountBySurveyId(1L, null); + } + + @Test + @DisplayName("getResponseCountBySurveyId - filter가 비어있으면 필터 없는 메서드 위임") + void getResponseCount_emptyFilter_delegatesToNoFilter() { + SurveyResponseFilterCondition empty = SurveyResponseFilterCondition.empty(); + given(responseRepository.getResponseCountBySurveyId(1L)).willReturn(20); + + Integer result = responseQueryService.getResponseCountBySurveyId(1L, empty); + + assertThat(result).isEqualTo(20); + verify(responseRepository).getResponseCountBySurveyId(1L); + } + + @Test + @DisplayName("getResponseCountBySurveyId - 유효한 filter면 필터 포함 메서드 위임") + void getResponseCount_withFilter_delegatesToFilteredMethod() { + SurveyResponseFilterCondition filter = new SurveyResponseFilterCondition( + List.of(AgeRange.TWENTY), List.of(Gender.MALE), List.of(Residence.SEOUL) + ); + given(responseRepository.getResponseCountBySurveyId(1L, filter)).willReturn(10); + + Integer result = responseQueryService.getResponseCountBySurveyId(1L, filter); + + assertThat(result).isEqualTo(10); + verify(responseRepository).getResponseCountBySurveyId(1L, filter); + } +} diff --git a/src/test/java/OneQ/OnSurvey/domain/survey/service/SurveyGlobalStatsServiceTest.java b/src/test/java/OneQ/OnSurvey/domain/survey/service/SurveyGlobalStatsServiceTest.java new file mode 100644 index 00000000..776fa728 --- /dev/null +++ b/src/test/java/OneQ/OnSurvey/domain/survey/service/SurveyGlobalStatsServiceTest.java @@ -0,0 +1,130 @@ +package OneQ.OnSurvey.domain.survey.service; + +import OneQ.OnSurvey.domain.survey.entity.SurveyGlobalStats; +import OneQ.OnSurvey.domain.survey.model.dto.GlobalStats; +import OneQ.OnSurvey.domain.survey.repository.SurveyGlobalStatsRepository; +import OneQ.OnSurvey.global.infra.redis.RedisAgent; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class SurveyGlobalStatsServiceTest { + + @Mock + private SurveyGlobalStatsRepository statsRepository; + + @Mock + private RedisAgent redisAgent; + + @InjectMocks + private SurveyGlobalStatsService surveyGlobalStatsService; + + @BeforeEach + void setUp() { + ReflectionTestUtils.setField(surveyGlobalStatsService, "dailyUserKey", "daily:user:"); + } + + private SurveyGlobalStats buildStats(long due, long completed, long promotion) { + return SurveyGlobalStats.builder() + .id(1L) + .totalDueCount(due) + .totalCompletedCount(completed) + .totalPromotionCount(promotion) + .build(); + } + + @Test + @DisplayName("addDueCount - 기존 stats에 delta만큼 증가") + void addDueCount_increasesExistingStats() { + SurveyGlobalStats stats = buildStats(1000L, 500L, 200L); + given(statsRepository.findById(1L)).willReturn(Optional.of(stats)); + + surveyGlobalStatsService.addDueCount(100L); + + assertThat(stats.getTotalDueCount()).isEqualTo(1100L); + } + + @Test + @DisplayName("addDueCount - stats 없으면 init 후 증가") + void addDueCount_noStats_initAndIncreases() { + SurveyGlobalStats newStats = SurveyGlobalStats.init(); + given(statsRepository.findById(1L)).willReturn(Optional.empty()); + given(statsRepository.save(any(SurveyGlobalStats.class))).willReturn(newStats); + + surveyGlobalStatsService.addDueCount(50L); + + assertThat(newStats.getTotalDueCount()).isEqualTo(1050L); + } + + @Test + @DisplayName("addCompletedCount - 기존 stats에 delta만큼 증가") + void addCompletedCount_increasesExistingStats() { + SurveyGlobalStats stats = buildStats(1000L, 500L, 200L); + given(statsRepository.findById(1L)).willReturn(Optional.of(stats)); + + surveyGlobalStatsService.addCompletedCount(30L); + + assertThat(stats.getTotalCompletedCount()).isEqualTo(530L); + } + + @Test + @DisplayName("addPromotionCount - 기존 stats에 delta만큼 증가") + void addPromotionCount_increasesExistingStats() { + SurveyGlobalStats stats = buildStats(1000L, 500L, 200L); + given(statsRepository.findById(1L)).willReturn(Optional.of(stats)); + + surveyGlobalStatsService.addPromotionCount(10L); + + assertThat(stats.getTotalPromotionCount()).isEqualTo(210L); + } + + @Test + @DisplayName("getStats - stats 존재 시 dailyUserCount와 함께 반환") + void getStats_existingStats_returnsCorrectValues() { + SurveyGlobalStats stats = buildStats(2000L, 800L, 400L); + given(statsRepository.findById(1L)).willReturn(Optional.of(stats)); + given(redisAgent.getZSetCount(any(), anyLong(), anyLong())).willReturn(42L); + + GlobalStats result = surveyGlobalStatsService.getStats(); + + assertThat(result.totalDueCount()).isEqualTo(2000L); + assertThat(result.totalCompletedCount()).isEqualTo(800L); + assertThat(result.totalPromotionCount()).isEqualTo(400L); + assertThat(result.dailyUserCount()).isEqualTo(42L); + } + + @Test + @DisplayName("getStats - stats 없으면 init 기본값 사용") + void getStats_noStats_usesInitDefaults() { + given(statsRepository.findById(1L)).willReturn(Optional.empty()); + given(redisAgent.getZSetCount(any(), anyLong(), anyLong())).willReturn(0L); + + GlobalStats result = surveyGlobalStatsService.getStats(); + + assertThat(result.totalDueCount()).isEqualTo(1000L); + assertThat(result.totalCompletedCount()).isEqualTo(1000L); + assertThat(result.totalPromotionCount()).isEqualTo(1000L); + } + + @Test + @DisplayName("removeOldDailyUsers - redisAgent.rangeRemoveFromZSet 호출") + void removeOldDailyUsers_callsRedisRangeRemove() { + surveyGlobalStatsService.removeOldDailyUsers(); + + verify(redisAgent).rangeRemoveFromZSet(any(), anyLong(), anyLong()); + } +} diff --git a/src/test/java/OneQ/OnSurvey/domain/survey/service/command/SurveyCommandServiceTest.java b/src/test/java/OneQ/OnSurvey/domain/survey/service/command/SurveyCommandServiceTest.java new file mode 100644 index 00000000..69ca233f --- /dev/null +++ b/src/test/java/OneQ/OnSurvey/domain/survey/service/command/SurveyCommandServiceTest.java @@ -0,0 +1,226 @@ +package OneQ.OnSurvey.domain.survey.service.command; + +import OneQ.OnSurvey.domain.member.Member; +import OneQ.OnSurvey.domain.member.MemberErrorCode; +import OneQ.OnSurvey.domain.member.repository.MemberRepository; +import OneQ.OnSurvey.domain.member.value.MemberStatus; +import OneQ.OnSurvey.domain.member.value.Role; +import OneQ.OnSurvey.domain.question.service.QuestionQueryService; +import OneQ.OnSurvey.domain.discount.service.DiscountCodeQueryService; +import OneQ.OnSurvey.domain.survey.SurveyErrorCode; +import OneQ.OnSurvey.domain.survey.entity.Screening; +import OneQ.OnSurvey.domain.survey.entity.Survey; +import OneQ.OnSurvey.domain.survey.entity.SurveyInfo; +import OneQ.OnSurvey.domain.survey.model.Gender; +import OneQ.OnSurvey.domain.survey.model.SurveyStatus; +import OneQ.OnSurvey.domain.survey.model.response.ScreeningResponse; +import OneQ.OnSurvey.domain.survey.repository.SurveyRepository; +import OneQ.OnSurvey.domain.survey.repository.screening.ScreeningRepository; +import OneQ.OnSurvey.domain.survey.repository.surveyInfo.SurveyInfoRepository; +import OneQ.OnSurvey.domain.survey.service.SurveyGlobalStatsService; +import OneQ.OnSurvey.domain.survey.service.refund.SurveyRefundPolicy; +import OneQ.OnSurvey.global.common.exception.CustomException; +import OneQ.OnSurvey.global.infra.discord.notifier.AlertNotifier; +import OneQ.OnSurvey.global.infra.redis.RedisAgent; +import OneQ.OnSurvey.global.infra.transaction.AfterCommitExecutor; +import OneQ.OnSurvey.global.infra.transaction.TransactionHandler; +import OneQ.OnSurvey.global.promotion.application.PromotionTierResolver; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.Optional; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class SurveyCommandServiceTest { + + @Mock private SurveyRepository surveyRepository; + @Mock private ScreeningRepository screeningRepository; + @Mock private SurveyInfoRepository surveyInfoRepository; + @Mock private MemberRepository memberRepository; + @Mock private SurveyRefundPolicy surveyRefundPolicy; + @Mock private SurveyGlobalStatsService surveyGlobalStatsService; + @Mock private QuestionQueryService questionQueryService; + @Mock private PromotionTierResolver promotionTierResolver; + @Mock private DiscountCodeQueryService discountCodeQueryService; + @Mock private AlertNotifier alertNotifier; + @Mock private AfterCommitExecutor afterCommitExecutor; + @Mock private RedisAgent redisAgent; + @Mock private TransactionHandler transactionHandler; + + @InjectMocks + private SurveyCommandService surveyCommandService; + + private Survey buildSurvey(Long id, Long memberId) { + Survey survey = Survey.of(memberId, "테스트 설문", "설명"); + ReflectionTestUtils.setField(survey, "id", id); + return survey; + } + + private SurveyInfo buildRefundableSurveyInfo(Long surveyId) { + return SurveyInfo.createSurveyInfo( + surveyId, 100, Gender.ALL, Set.of(), Set.of(), 0, 0, 0, 0, 0, null + ); + } + + private Member buildMember(Long userKey) { + Member member = Member.createMember( + userKey, "홍길동", "010-1234-5678", + "19900101", "test@test.com", + OneQ.OnSurvey.domain.survey.model.Gender.MALE, Role.ROLE_MEMBER, MemberStatus.ACTIVE + ); + member.increaseCoin(1000L); + return member; + } + + @Test + @DisplayName("upsertScreening - content가 null이면 기존 스크리닝 삭제") + void upsertScreening_nullContent_deletesExisting() { + Screening existing = Screening.of(1L, "기존 질문", true); + given(screeningRepository.getScreeningBySurveyId(1L)).willReturn(existing); + + ScreeningResponse response = surveyCommandService.upsertScreening(1L, null, null); + + verify(screeningRepository).delete(existing); + assertThat(response.screeningId()).isNull(); + } + + @Test + @DisplayName("upsertScreening - content가 blank이면 기존 스크리닝 삭제") + void upsertScreening_blankContent_deletesExisting() { + Screening existing = Screening.of(1L, "기존 질문", true); + given(screeningRepository.getScreeningBySurveyId(1L)).willReturn(existing); + + ScreeningResponse response = surveyCommandService.upsertScreening(1L, " ", true); + + verify(screeningRepository).delete(existing); + assertThat(response.screeningId()).isNull(); + } + + @Test + @DisplayName("upsertScreening - 스크리닝 없고 유효한 값이면 신규 생성") + void upsertScreening_noExisting_createsNew() { + Screening created = Screening.of(1L, "새 질문", false); + ReflectionTestUtils.setField(created, "id", 99L); + given(screeningRepository.getScreeningBySurveyId(1L)).willReturn(null); + given(screeningRepository.save(any(Screening.class))).willReturn(created); + + ScreeningResponse response = surveyCommandService.upsertScreening(1L, "새 질문", false); + + assertThat(response.content()).isEqualTo("새 질문"); + assertThat(response.answer()).isFalse(); + } + + @Test + @DisplayName("upsertScreening - 기존 스크리닝 있으면 내용 업데이트") + void upsertScreening_existingScreening_updates() { + Screening existing = Screening.of(1L, "기존 질문", true); + ReflectionTestUtils.setField(existing, "id", 5L); + given(screeningRepository.getScreeningBySurveyId(1L)).willReturn(existing); + given(screeningRepository.save(existing)).willReturn(existing); + + ScreeningResponse response = surveyCommandService.upsertScreening(1L, "수정된 질문", false); + + assertThat(response.content()).isEqualTo("수정된 질문"); + assertThat(response.answer()).isFalse(); + } + + @Test + @DisplayName("refundSurvey - 설문 없으면 SURVEY_NOT_FOUND 예외") + void refundSurvey_surveyNotFound_throwsException() { + given(surveyRepository.getSurveyById(999L)).willReturn(Optional.empty()); + + assertThatThrownBy(() -> surveyCommandService.refundSurvey(1L, 999L)) + .isInstanceOf(CustomException.class) + .satisfies(ex -> assertThat(((CustomException) ex).getErrorCode()) + .isEqualTo(SurveyErrorCode.SURVEY_NOT_FOUND)); + } + + @Test + @DisplayName("refundSurvey - surveyInfo 없으면 SURVEY_INFO_NOT_FOUND 예외") + void refundSurvey_infoNotFound_throwsException() { + Survey survey = buildSurvey(1L, 10L); + given(surveyRepository.getSurveyById(1L)).willReturn(Optional.of(survey)); + given(surveyInfoRepository.findBySurveyId(1L)).willReturn(Optional.empty()); + + assertThatThrownBy(() -> surveyCommandService.refundSurvey(1L, 1L)) + .isInstanceOf(CustomException.class) + .satisfies(ex -> assertThat(((CustomException) ex).getErrorCode()) + .isEqualTo(SurveyErrorCode.SURVEY_INFO_NOT_FOUND)); + } + + @Test + @DisplayName("refundSurvey - 환불 불가 상태면 SURVEY_NOT_REFUNDABLE 예외") + void refundSurvey_notRefundable_throwsException() { + Survey survey = buildSurvey(1L, 10L); + SurveyInfo info = buildRefundableSurveyInfo(1L); + info.markNonRefundable(); + given(surveyRepository.getSurveyById(1L)).willReturn(Optional.of(survey)); + given(surveyInfoRepository.findBySurveyId(1L)).willReturn(Optional.of(info)); + + assertThatThrownBy(() -> surveyCommandService.refundSurvey(1L, 1L)) + .isInstanceOf(CustomException.class) + .satisfies(ex -> assertThat(((CustomException) ex).getErrorCode()) + .isEqualTo(SurveyErrorCode.SURVEY_NOT_REFUNDABLE)); + } + + @Test + @DisplayName("refundSurvey - 환불 금액이 0 이하면 SURVEY_NOT_REFUNDABLE 예외") + void refundSurvey_zeroRefundAmount_throwsException() { + Survey survey = buildSurvey(1L, 10L); + SurveyInfo info = buildRefundableSurveyInfo(1L); + given(surveyRepository.getSurveyById(1L)).willReturn(Optional.of(survey)); + given(surveyInfoRepository.findBySurveyId(1L)).willReturn(Optional.of(info)); + given(surveyRefundPolicy.calculateRefundAmount(survey, info)).willReturn(0); + + assertThatThrownBy(() -> surveyCommandService.refundSurvey(1L, 1L)) + .isInstanceOf(CustomException.class) + .satisfies(ex -> assertThat(((CustomException) ex).getErrorCode()) + .isEqualTo(SurveyErrorCode.SURVEY_NOT_REFUNDABLE)); + } + + @Test + @DisplayName("refundSurvey - 환불 성공 시 코인 지급 및 설문 상태 REFUNDED") + void refundSurvey_success_increasesCoinAndUpdatesStatus() { + Survey survey = buildSurvey(1L, 10L); + SurveyInfo info = buildRefundableSurveyInfo(1L); + Member member = buildMember(1L); + given(surveyRepository.getSurveyById(1L)).willReturn(Optional.of(survey)); + given(surveyInfoRepository.findBySurveyId(1L)).willReturn(Optional.of(info)); + given(surveyRefundPolicy.calculateRefundAmount(survey, info)).willReturn(500); + given(memberRepository.findMemberByUserKey(1L)).willReturn(Optional.of(member)); + + Boolean result = surveyCommandService.refundSurvey(1L, 1L); + + assertThat(result).isTrue(); + assertThat(member.getCoin()).isEqualTo(1500L); + assertThat(survey.getStatus()).isEqualTo(SurveyStatus.REFUNDED); + } + + @Test + @DisplayName("refundSurvey - 멤버 없으면 MEMBER_NOT_FOUND 예외") + void refundSurvey_memberNotFound_throwsException() { + Survey survey = buildSurvey(1L, 10L); + SurveyInfo info = buildRefundableSurveyInfo(1L); + given(surveyRepository.getSurveyById(1L)).willReturn(Optional.of(survey)); + given(surveyInfoRepository.findBySurveyId(1L)).willReturn(Optional.of(info)); + given(surveyRefundPolicy.calculateRefundAmount(survey, info)).willReturn(300); + given(memberRepository.findMemberByUserKey(1L)).willReturn(Optional.empty()); + + assertThatThrownBy(() -> surveyCommandService.refundSurvey(1L, 1L)) + .isInstanceOf(CustomException.class) + .satisfies(ex -> assertThat(((CustomException) ex).getErrorCode()) + .isEqualTo(MemberErrorCode.MEMBER_NOT_FOUND)); + } +} diff --git a/src/test/java/OneQ/OnSurvey/domain/survey/service/query/SurveyQueryServiceTest.java b/src/test/java/OneQ/OnSurvey/domain/survey/service/query/SurveyQueryServiceTest.java new file mode 100644 index 00000000..b1808e14 --- /dev/null +++ b/src/test/java/OneQ/OnSurvey/domain/survey/service/query/SurveyQueryServiceTest.java @@ -0,0 +1,148 @@ +package OneQ.OnSurvey.domain.survey.service.query; + +import OneQ.OnSurvey.domain.member.repository.MemberRepository; +import OneQ.OnSurvey.domain.participation.repository.response.ResponseRepository; +import OneQ.OnSurvey.domain.question.repository.section.SectionRepository; +import OneQ.OnSurvey.domain.question.service.QuestionQueryService; +import OneQ.OnSurvey.domain.survey.SurveyErrorCode; +import OneQ.OnSurvey.domain.survey.entity.Survey; +import OneQ.OnSurvey.domain.survey.entity.SurveyInfo; +import OneQ.OnSurvey.domain.survey.model.Gender; +import OneQ.OnSurvey.domain.survey.model.SurveyStatus; +import OneQ.OnSurvey.domain.survey.model.response.MySurveyListResponse; +import OneQ.OnSurvey.domain.survey.repository.SurveyRepository; +import OneQ.OnSurvey.domain.survey.repository.screening.ScreeningRepository; +import OneQ.OnSurvey.domain.survey.repository.surveyInfo.SurveyInfoRepository; +import OneQ.OnSurvey.global.common.exception.CustomException; +import OneQ.OnSurvey.global.infra.redis.RedisAgent; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.BDDMockito.given; + +@ExtendWith(MockitoExtension.class) +class SurveyQueryServiceTest { + + @Mock private SurveyRepository surveyRepository; + @Mock private SurveyInfoRepository surveyInfoRepository; + @Mock private ScreeningRepository screeningRepository; + @Mock private ResponseRepository responseRepository; + @Mock private MemberRepository memberRepository; + @Mock private SectionRepository sectionRepository; + @Mock private RedisAgent redisAgent; + @Mock private QuestionQueryService questionQueryService; + + @InjectMocks + private SurveyQueryService surveyQueryService; + + private Survey buildSurvey(Long id, Long memberId, SurveyStatus status, LocalDateTime createdAt) { + Survey survey = Survey.builder() + .memberId(memberId) + .title("테스트 설문") + .totalCoin(1000) + .deadline(LocalDateTime.now().plusDays(7)) + .build(); + ReflectionTestUtils.setField(survey, "id", id); + ReflectionTestUtils.setField(survey, "status", status); + ReflectionTestUtils.setField(survey, "createdAt", createdAt); + return survey; + } + + @Test + @DisplayName("getSurveyById - 설문 존재 시 반환") + void getSurveyById_found_returnsSurvey() { + Survey survey = buildSurvey(1L, 10L, SurveyStatus.ONGOING, LocalDateTime.now()); + given(surveyRepository.getSurveyById(1L)).willReturn(Optional.of(survey)); + + Survey result = surveyQueryService.getSurveyById(1L); + + assertThat(result).isEqualTo(survey); + } + + @Test + @DisplayName("getSurveyById - 설문 없으면 SURVEY_NOT_FOUND 예외") + void getSurveyById_notFound_throwsException() { + given(surveyRepository.getSurveyById(999L)).willReturn(Optional.empty()); + + assertThatThrownBy(() -> surveyQueryService.getSurveyById(999L)) + .isInstanceOf(CustomException.class) + .satisfies(ex -> assertThat(((CustomException) ex).getErrorCode()) + .isEqualTo(SurveyErrorCode.SURVEY_NOT_FOUND)); + } + + @Test + @DisplayName("getPromotionAmountBySurveyId - surveyInfo 존재 시 promotionAmount 반환") + void getPromotionAmountBySurveyId_found_returnsAmount() { + SurveyInfo info = SurveyInfo.createSurveyInfo( + 1L, 100, Gender.ALL, Set.of(), Set.of(), 0, 0, 0, 0, 300, null); + given(surveyInfoRepository.findBySurveyId(1L)).willReturn(Optional.of(info)); + + Integer result = surveyQueryService.getPromotionAmountBySurveyId(1L); + + assertThat(result).isEqualTo(300); + } + + @Test + @DisplayName("getPromotionAmountBySurveyId - surveyInfo 없으면 null 반환") + void getPromotionAmountBySurveyId_notFound_returnsNull() { + given(surveyInfoRepository.findBySurveyId(999L)).willReturn(Optional.empty()); + + Integer result = surveyQueryService.getPromotionAmountBySurveyId(999L); + + assertThat(result).isNull(); + } + + @Test + @DisplayName("getMySurveys - ONGOING/CLOSED는 ongoing, REFUNDED는 refunded로 분리") + void getMySurveys_separatesOngoingAndRefunded() { + LocalDateTime base = LocalDateTime.of(2026, 1, 1, 0, 0); + Survey ongoing = buildSurvey(1L, 10L, SurveyStatus.ONGOING, base.plusDays(2)); + Survey closed = buildSurvey(2L, 10L, SurveyStatus.CLOSED, base.plusDays(1)); + Survey refunded = buildSurvey(3L, 10L, SurveyStatus.REFUNDED, base); + given(surveyRepository.getSurveyListByMemberId(10L)).willReturn(List.of(ongoing, closed, refunded)); + + MySurveyListResponse response = surveyQueryService.getMySurveys(10L); + + assertThat(response.totalCount()).isEqualTo(2); + assertThat(response.refundedCount()).isEqualTo(1); + assertThat(response.ongoingSurveys()).hasSize(2); + assertThat(response.refundedSurveys()).hasSize(1); + } + + @Test + @DisplayName("getMySurveys - ongoing 목록은 createdAt 내림차순 정렬") + void getMySurveys_ongoingSortedByCreatedAtDesc() { + LocalDateTime base = LocalDateTime.of(2026, 1, 1, 0, 0); + Survey older = buildSurvey(1L, 10L, SurveyStatus.ONGOING, base); + Survey newer = buildSurvey(2L, 10L, SurveyStatus.ONGOING, base.plusDays(5)); + given(surveyRepository.getSurveyListByMemberId(10L)).willReturn(List.of(older, newer)); + + MySurveyListResponse response = surveyQueryService.getMySurveys(10L); + + assertThat(response.ongoingSurveys().get(0).surveyId()).isEqualTo(2L); + assertThat(response.ongoingSurveys().get(1).surveyId()).isEqualTo(1L); + } + + @Test + @DisplayName("getMySurveys - 빈 목록이면 카운트 0") + void getMySurveys_empty_returnsZeroCounts() { + given(surveyRepository.getSurveyListByMemberId(10L)).willReturn(List.of()); + + MySurveyListResponse response = surveyQueryService.getMySurveys(10L); + + assertThat(response.totalCount()).isZero(); + assertThat(response.refundedCount()).isZero(); + } +} From d0f67fb32fbd0e4f627e45bf432c4fb0afca853d Mon Sep 17 00:00:00 2001 From: jaekwan Date: Sun, 10 May 2026 16:17:15 +0900 Subject: [PATCH 12/16] =?UTF-8?q?test:=20=EC=9E=94=EC=97=AC=20=EC=84=9C?= =?UTF-8?q?=EB=B9=84=EC=8A=A4=20=EB=A0=88=EC=9D=B4=EC=96=B4=20=EB=8B=A8?= =?UTF-8?q?=EC=9C=84=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?(question,=20participation,=20formRequest)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QuestionAnswerCommandServiceTest.java | 71 ++++++++ .../ScreeningAnswerCommandServiceTest.java | 102 +++++++++++ .../service/QuestionCommandServiceTest.java | 126 ++++++++++++++ .../service/QuestionQueryServiceTest.java | 82 +++++++++ .../formRequest/FormCommandServiceTest.java | 162 ++++++++++++++++++ .../formRequest/FormQueryServiceTest.java | 80 +++++++++ 6 files changed, 623 insertions(+) create mode 100644 src/test/java/OneQ/OnSurvey/domain/participation/service/answer/QuestionAnswerCommandServiceTest.java create mode 100644 src/test/java/OneQ/OnSurvey/domain/participation/service/answer/ScreeningAnswerCommandServiceTest.java create mode 100644 src/test/java/OneQ/OnSurvey/domain/question/service/QuestionCommandServiceTest.java create mode 100644 src/test/java/OneQ/OnSurvey/domain/question/service/QuestionQueryServiceTest.java create mode 100644 src/test/java/OneQ/OnSurvey/domain/survey/service/formRequest/FormCommandServiceTest.java create mode 100644 src/test/java/OneQ/OnSurvey/domain/survey/service/formRequest/FormQueryServiceTest.java diff --git a/src/test/java/OneQ/OnSurvey/domain/participation/service/answer/QuestionAnswerCommandServiceTest.java b/src/test/java/OneQ/OnSurvey/domain/participation/service/answer/QuestionAnswerCommandServiceTest.java new file mode 100644 index 00000000..6c099dfe --- /dev/null +++ b/src/test/java/OneQ/OnSurvey/domain/participation/service/answer/QuestionAnswerCommandServiceTest.java @@ -0,0 +1,71 @@ +package OneQ.OnSurvey.domain.participation.service.answer; + +import OneQ.OnSurvey.domain.participation.entity.QuestionAnswer; +import OneQ.OnSurvey.domain.participation.entity.Response; +import OneQ.OnSurvey.domain.participation.repository.answer.AnswerRepository; +import OneQ.OnSurvey.domain.participation.repository.response.ResponseRepository; +import OneQ.OnSurvey.domain.question.repository.question.QuestionRepository; +import OneQ.OnSurvey.global.infra.redis.RedisAgent; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class QuestionAnswerCommandServiceTest { + + @Mock private AnswerRepository answerRepository; + @Mock private ResponseRepository responseRepository; + @Mock private RedisAgent redisAgent; + @Mock private QuestionRepository questionRepository; + + @InjectMocks + private QuestionAnswerCommandService questionAnswerCommandService; + + @Test + @DisplayName("updateResponseAfterQuestionAnswers - 응답 없으면 신규 생성 후 저장") + void updateResponseAfterQuestionAnswers_notFound_createsAndSaves() { + given(responseRepository.findBySurveyIdAndMemberId(1L, 2L)).willReturn(Optional.empty()); + + questionAnswerCommandService.updateResponseAfterQuestionAnswers(1L, 2L); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Response.class); + verify(responseRepository).save(captor.capture()); + assertThat(captor.getValue().getSurveyId()).isEqualTo(1L); + assertThat(captor.getValue().getMemberId()).isEqualTo(2L); + assertThat(captor.getValue().getIsResponded()).isFalse(); + } + + @Test + @DisplayName("updateResponseAfterQuestionAnswers - 미완료 응답이면 저장") + void updateResponseAfterQuestionAnswers_notResponded_saves() { + Response response = Response.of(1L, 2L); + given(responseRepository.findBySurveyIdAndMemberId(1L, 2L)).willReturn(Optional.of(response)); + + questionAnswerCommandService.updateResponseAfterQuestionAnswers(1L, 2L); + + verify(responseRepository).save(response); + } + + @Test + @DisplayName("updateResponseAfterQuestionAnswers - 이미 완료된 응답이면 저장 안함") + void updateResponseAfterQuestionAnswers_alreadyResponded_doesNotSave() { + Response response = Response.of(1L, 2L); + response.markResponded(); + given(responseRepository.findBySurveyIdAndMemberId(1L, 2L)).willReturn(Optional.of(response)); + + questionAnswerCommandService.updateResponseAfterQuestionAnswers(1L, 2L); + + verify(responseRepository, never()).save(response); + } +} diff --git a/src/test/java/OneQ/OnSurvey/domain/participation/service/answer/ScreeningAnswerCommandServiceTest.java b/src/test/java/OneQ/OnSurvey/domain/participation/service/answer/ScreeningAnswerCommandServiceTest.java new file mode 100644 index 00000000..24667aaa --- /dev/null +++ b/src/test/java/OneQ/OnSurvey/domain/participation/service/answer/ScreeningAnswerCommandServiceTest.java @@ -0,0 +1,102 @@ +package OneQ.OnSurvey.domain.participation.service.answer; + +import OneQ.OnSurvey.domain.participation.entity.Response; +import OneQ.OnSurvey.domain.participation.entity.ScreeningAnswer; +import OneQ.OnSurvey.domain.participation.model.dto.AnswerInsertDto; +import OneQ.OnSurvey.domain.participation.repository.answer.AnswerRepository; +import OneQ.OnSurvey.domain.participation.repository.response.ResponseRepository; +import OneQ.OnSurvey.domain.survey.repository.screening.ScreeningRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class ScreeningAnswerCommandServiceTest { + + @Mock private AnswerRepository answerRepository; + @Mock private ResponseRepository responseRepository; + @Mock private ScreeningRepository screeningRepository; + + @InjectMocks + private ScreeningAnswerCommandService screeningAnswerCommandService; + + private AnswerInsertDto.AnswerInfo buildAnswerInfo(Long screeningId, Long memberId, String content) { + return AnswerInsertDto.AnswerInfo.builder() + .id(screeningId) + .memberId(memberId) + .content(content) + .build(); + } + + @Test + @DisplayName("updateResponseAfterScreening - 기댓값과 제출값 같으면 screened=false") + void updateResponseAfterScreening_sameAsExpected_notScreened() { + given(screeningRepository.getScreeningAnswer(10L)).willReturn(true); + Response response = Response.of(5L, 1L); + given(responseRepository.findBySurveyIdAndMemberId(5L, 1L)).willReturn(Optional.of(response)); + + AnswerInsertDto.AnswerInfo info = buildAnswerInfo(10L, 1L, "true"); + screeningAnswerCommandService.updateResponseAfterScreening(5L, info); + + assertThat(response.getIsScreened()).isFalse(); + verify(responseRepository).save(response); + } + + @Test + @DisplayName("updateResponseAfterScreening - 기댓값과 제출값 다르면 screened=true") + void updateResponseAfterScreening_differsFromExpected_screened() { + given(screeningRepository.getScreeningAnswer(10L)).willReturn(true); + Response response = Response.of(5L, 1L); + given(responseRepository.findBySurveyIdAndMemberId(5L, 1L)).willReturn(Optional.of(response)); + + AnswerInsertDto.AnswerInfo info = buildAnswerInfo(10L, 1L, "false"); + screeningAnswerCommandService.updateResponseAfterScreening(5L, info); + + assertThat(response.getIsScreened()).isTrue(); + verify(responseRepository).save(response); + } + + @Test + @DisplayName("updateResponseAfterScreening - 기존 응답 없으면 신규 생성 후 저장") + void updateResponseAfterScreening_noExistingResponse_createsNewAndSaves() { + given(screeningRepository.getScreeningAnswer(10L)).willReturn(false); + given(responseRepository.findBySurveyIdAndMemberId(5L, 1L)).willReturn(Optional.empty()); + + AnswerInsertDto.AnswerInfo info = buildAnswerInfo(10L, 1L, "false"); + screeningAnswerCommandService.updateResponseAfterScreening(5L, info); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Response.class); + verify(responseRepository).save(captor.capture()); + assertThat(captor.getValue().getSurveyId()).isEqualTo(5L); + assertThat(captor.getValue().getMemberId()).isEqualTo(1L); + assertThat(captor.getValue().getIsScreened()).isFalse(); + } + + @Test + @DisplayName("insertAnswer - answerRepository.save 호출 후 updateResponseAfterScreening 진행") + void insertAnswer_savesAnswerAndUpdatesResponse() { + given(screeningRepository.getSurveyId(10L)).willReturn(5L); + given(screeningRepository.getScreeningAnswer(10L)).willReturn(true); + given(answerRepository.save(any())).willReturn(null); + Response response = Response.of(5L, 1L); + given(responseRepository.findBySurveyIdAndMemberId(5L, 1L)).willReturn(Optional.of(response)); + + AnswerInsertDto.AnswerInfo info = buildAnswerInfo(10L, 1L, "true"); + Boolean result = screeningAnswerCommandService.insertAnswer(info); + + assertThat(result).isTrue(); + verify(answerRepository).save(any()); + verify(responseRepository).save(response); + } +} diff --git a/src/test/java/OneQ/OnSurvey/domain/question/service/QuestionCommandServiceTest.java b/src/test/java/OneQ/OnSurvey/domain/question/service/QuestionCommandServiceTest.java new file mode 100644 index 00000000..f5c97bd3 --- /dev/null +++ b/src/test/java/OneQ/OnSurvey/domain/question/service/QuestionCommandServiceTest.java @@ -0,0 +1,126 @@ +package OneQ.OnSurvey.domain.question.service; + +import OneQ.OnSurvey.domain.question.entity.Section; +import OneQ.OnSurvey.domain.question.model.dto.SectionDto; +import OneQ.OnSurvey.domain.question.repository.choiceOption.ChoiceOptionRepository; +import OneQ.OnSurvey.domain.question.repository.gridOption.GridOptionRepository; +import OneQ.OnSurvey.domain.question.repository.question.QuestionRepository; +import OneQ.OnSurvey.domain.question.repository.section.SectionRepository; +import OneQ.OnSurvey.domain.survey.SurveyErrorCode; +import OneQ.OnSurvey.global.common.exception.CustomException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.never; + +@ExtendWith(MockitoExtension.class) +class QuestionCommandServiceTest { + + @Mock private ChoiceOptionRepository choiceOptionRepository; + @Mock private GridOptionRepository gridOptionRepository; + @Mock private SectionRepository sectionRepository; + @Mock private QuestionRepository questionRepository; + + @InjectMocks + private QuestionCommandService questionCommandService; + + private Section buildSection(Long sectionId, Long surveyId, String title, int order, int nextSection) { + Section section = Section.builder() + .surveyId(surveyId) + .title(title) + .sectionOrder(order) + .nextSection(nextSection) + .build(); + if (sectionId != null) { + ReflectionTestUtils.setField(section, "sectionId", sectionId); + } + return section; + } + + @Test + @DisplayName("upsertSections - null 제목이면 SURVEY_FORM_INVALID_SECTION 예외") + void upsertSections_nullTitle_throwsInvalidSection() { + SectionDto invalid = new SectionDto(null, null, null, 1, 2); + + assertThatThrownBy(() -> questionCommandService.upsertSections(1L, List.of(invalid))) + .isInstanceOf(CustomException.class) + .satisfies(ex -> assertThat(((CustomException) ex).getErrorCode()) + .isEqualTo(SurveyErrorCode.SURVEY_FORM_INVALID_SECTION)); + } + + @Test + @DisplayName("upsertSections - 공백 제목이면 SURVEY_FORM_INVALID_SECTION 예외") + void upsertSections_blankTitle_throwsInvalidSection() { + SectionDto invalid = new SectionDto(null, " ", null, 1, 2); + + assertThatThrownBy(() -> questionCommandService.upsertSections(1L, List.of(invalid))) + .isInstanceOf(CustomException.class) + .satisfies(ex -> assertThat(((CustomException) ex).getErrorCode()) + .isEqualTo(SurveyErrorCode.SURVEY_FORM_INVALID_SECTION)); + } + + @Test + @DisplayName("upsertSections - null order이면 SURVEY_FORM_INVALID_SECTION 예외") + void upsertSections_nullOrder_throwsInvalidSection() { + SectionDto invalid = new SectionDto(null, "제목", null, null, 2); + + assertThatThrownBy(() -> questionCommandService.upsertSections(1L, List.of(invalid))) + .isInstanceOf(CustomException.class) + .satisfies(ex -> assertThat(((CustomException) ex).getErrorCode()) + .isEqualTo(SurveyErrorCode.SURVEY_FORM_INVALID_SECTION)); + } + + @Test + @DisplayName("upsertSections - 신규 섹션이면 새 Section 엔티티 생성 및 저장") + void upsertSections_newSection_createsAndSaves() { + SectionDto newDto = new SectionDto(null, "새 섹션", "설명", 1, 2); + Section savedSection = buildSection(100L, 1L, "새 섹션", 1, 2); + given(sectionRepository.findAllSectionBySurveyId(1L)).willReturn(List.of()); + given(sectionRepository.saveAll(any())).willReturn(List.of(savedSection)); + + List result = questionCommandService.upsertSections(1L, List.of(newDto)); + + assertThat(result).hasSize(1); + assertThat(result.get(0).sectionId()).isEqualTo(100L); + assertThat(result.get(0).title()).isEqualTo("새 섹션"); + verify(sectionRepository).saveAll(any()); + } + + @Test + @DisplayName("upsertSections - 기존 섹션이면 내용 업데이트") + void upsertSections_existingSection_updatesSection() { + Section existing = buildSection(50L, 1L, "기존 섹션", 1, 2); + SectionDto updateDto = new SectionDto(50L, "수정된 섹션", "수정 설명", 1, 3); + given(sectionRepository.findAllSectionBySurveyId(1L)).willReturn(List.of(existing)); + given(sectionRepository.saveAll(any())).willReturn(List.of(existing)); + + questionCommandService.upsertSections(1L, List.of(updateDto)); + + assertThat(existing.getTitle()).isEqualTo("수정된 섹션"); + assertThat(existing.getNextSection()).isEqualTo(3); + verify(sectionRepository).saveAll(any()); + } + + @Test + @DisplayName("upsertSections - DB에만 있는 섹션은 삭제") + void upsertSections_extraDbSections_deletesOldSections() { + Section extra = buildSection(50L, 1L, "삭제될 섹션", 1, 2); + given(sectionRepository.findAllSectionBySurveyId(1L)).willReturn(List.of(extra)); + + questionCommandService.upsertSections(1L, List.of()); + + verify(sectionRepository).deleteAll(List.of(50L)); + } +} diff --git a/src/test/java/OneQ/OnSurvey/domain/question/service/QuestionQueryServiceTest.java b/src/test/java/OneQ/OnSurvey/domain/question/service/QuestionQueryServiceTest.java new file mode 100644 index 00000000..97ebf4e7 --- /dev/null +++ b/src/test/java/OneQ/OnSurvey/domain/question/service/QuestionQueryServiceTest.java @@ -0,0 +1,82 @@ +package OneQ.OnSurvey.domain.question.service; + +import OneQ.OnSurvey.domain.question.entity.ChoiceOption; +import OneQ.OnSurvey.domain.question.entity.GridOption; +import OneQ.OnSurvey.domain.question.model.dto.GridOptionDto; +import OneQ.OnSurvey.domain.question.model.dto.OptionDto; +import OneQ.OnSurvey.domain.question.repository.choiceOption.ChoiceOptionRepository; +import OneQ.OnSurvey.domain.question.repository.gridOption.GridOptionRepository; +import OneQ.OnSurvey.domain.question.repository.question.QuestionRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; + +@ExtendWith(MockitoExtension.class) +class QuestionQueryServiceTest { + + @Mock private QuestionRepository questionRepository; + @Mock private ChoiceOptionRepository choiceOptionRepository; + @Mock private GridOptionRepository gridOptionRepository; + + @InjectMocks + private QuestionQueryService questionQueryService; + + @Test + @DisplayName("getOptionsByQuestionIdList - 보기를 OptionDto로 변환하여 반환") + void getOptionsByQuestionIdList_returnsOptionDtos() { + ChoiceOption option = ChoiceOption.of(1L, "선택지1", null, null); + ReflectionTestUtils.setField(option, "choiceOptionId", 10L); + given(choiceOptionRepository.getOptionsByQuestionIds(List.of(1L))).willReturn(List.of(option)); + + List result = questionQueryService.getOptionsByQuestionIdList(List.of(1L)); + + assertThat(result).hasSize(1); + assertThat(result.get(0).getOptionId()).isEqualTo(10L); + assertThat(result.get(0).getContent()).isEqualTo("선택지1"); + assertThat(result.get(0).getQuestionId()).isEqualTo(1L); + } + + @Test + @DisplayName("getOptionsByQuestionIdList - 보기 없으면 빈 리스트 반환") + void getOptionsByQuestionIdList_empty_returnsEmpty() { + given(choiceOptionRepository.getOptionsByQuestionIds(List.of(1L))).willReturn(List.of()); + + List result = questionQueryService.getOptionsByQuestionIdList(List.of(1L)); + + assertThat(result).isEmpty(); + } + + @Test + @DisplayName("getGridOptionsByQuestionIdList - 그리드 옵션을 GridOptionDto로 변환하여 반환") + void getGridOptionsByQuestionIdList_returnsGridOptionDtos() { + GridOption gridOption = GridOption.of(2L, true, "행1", 0); + ReflectionTestUtils.setField(gridOption, "gridOptionId", 20L); + given(gridOptionRepository.getGridOptionsByQuestionIds(List.of(2L))).willReturn(List.of(gridOption)); + + List result = questionQueryService.getGridOptionsByQuestionIdList(List.of(2L)); + + assertThat(result).hasSize(1); + assertThat(result.get(0).getGridOptionId()).isEqualTo(20L); + assertThat(result.get(0).getIsRow()).isTrue(); + assertThat(result.get(0).getContent()).isEqualTo("행1"); + } + + @Test + @DisplayName("countQuestionsBySurveyId - repository에 위임") + void countQuestionsBySurveyId_delegatesToRepository() { + given(questionRepository.countBySurveyId(1L)).willReturn(7); + + int result = questionQueryService.countQuestionsBySurveyId(1L); + + assertThat(result).isEqualTo(7); + } +} diff --git a/src/test/java/OneQ/OnSurvey/domain/survey/service/formRequest/FormCommandServiceTest.java b/src/test/java/OneQ/OnSurvey/domain/survey/service/formRequest/FormCommandServiceTest.java new file mode 100644 index 00000000..790191e7 --- /dev/null +++ b/src/test/java/OneQ/OnSurvey/domain/survey/service/formRequest/FormCommandServiceTest.java @@ -0,0 +1,162 @@ +package OneQ.OnSurvey.domain.survey.service.formRequest; + +import OneQ.OnSurvey.domain.member.dto.MemberSearchResult; +import OneQ.OnSurvey.domain.member.service.MemberFinder; +import OneQ.OnSurvey.domain.member.value.MemberStatus; +import OneQ.OnSurvey.domain.survey.SurveyErrorCode; +import OneQ.OnSurvey.domain.survey.entity.FormRequest; +import OneQ.OnSurvey.domain.survey.model.formRequest.FormPublishRequest; +import OneQ.OnSurvey.domain.survey.model.Gender; +import OneQ.OnSurvey.domain.survey.model.response.SurveyFormResponse; +import OneQ.OnSurvey.domain.survey.repository.formRequest.FormRequestRepository; +import OneQ.OnSurvey.domain.survey.service.command.SurveyCommand; +import OneQ.OnSurvey.domain.survey.service.query.SurveyQueryService; +import OneQ.OnSurvey.global.common.exception.CustomException; +import OneQ.OnSurvey.global.infra.redis.RedisCacheAction; +import OneQ.OnSurvey.global.infra.redis.RedisLockAction; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.context.ApplicationEventPublisher; + +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class FormCommandServiceTest { + + @Mock private ApplicationEventPublisher eventPublisher; + @Mock private RedisCacheAction redisCacheAction; + @Mock private RedisLockAction redisLockAction; + @Mock private FormConverter formConverter; + @Mock private FormRequestLambda formRequestLambda; + @Mock private FormRequestRepository formRequestRepository; + @Mock private SurveyQueryService surveyQueryService; + @Mock private MemberFinder memberFinder; + @Mock private SurveyCommand surveyCommand; + + @InjectMocks + private FormCommandService formCommandService; + + private FormRequest buildRegisteredRequest() { + FormRequest request = FormRequest.builder() + .formLink("https://link") + .userKey(1L) + .requesterEmail("test@test.com") + .isRegistered(true) + .registeredSurveyId(10L) + .build(); + return request; + } + + private MemberSearchResult buildMemberResult(Long userKey) { + return new MemberSearchResult(1L, userKey, "홍길동", "test@test.com", + "010-1234-5678", "19900101", Gender.MALE, MemberStatus.ACTIVE, 1000L); + } + + @Test + @DisplayName("markAsRegistered - FormRequest 없으면 FORM_REQUEST_NOT_FOUND 예외") + void markAsRegistered_notFound_throwsException() { + given(formRequestRepository.findById(999L)).willReturn(Optional.empty()); + + assertThatThrownBy(() -> formCommandService.markAsRegistered(999L, 1L, 10)) + .isInstanceOf(CustomException.class) + .satisfies(ex -> assertThat(((CustomException) ex).getErrorCode()) + .isEqualTo(SurveyErrorCode.FORM_REQUEST_NOT_FOUND)); + } + + @Test + @DisplayName("markAsRegistered - 성공 시 request에 surveyId와 questionCount 기록") + void markAsRegistered_success_marksRequest() { + FormRequest request = FormRequest.createRequest("https://link", "test@test.com", 100, 1000, 1L); + given(formRequestRepository.findById(1L)).willReturn(Optional.of(request)); + + formCommandService.markAsRegistered(1L, 10L, 5); + + assertThat(request.getIsRegistered()).isTrue(); + assertThat(request.getRegisteredSurveyId()).isEqualTo(10L); + assertThat(request.getQuestionCount()).isEqualTo(5); + } + + @Test + @DisplayName("publishFormRequest - FormRequest 없으면 FORM_REQUEST_NOT_FOUND 예외") + void publishFormRequest_requestNotFound_throwsException() { + given(formRequestRepository.findById(999L)).willReturn(Optional.empty()); + FormPublishRequest publishRequest = new FormPublishRequest(null, null, null); + + assertThatThrownBy(() -> formCommandService.publishFormRequest(999L, publishRequest)) + .isInstanceOf(CustomException.class) + .satisfies(ex -> assertThat(((CustomException) ex).getErrorCode()) + .isEqualTo(SurveyErrorCode.FORM_REQUEST_NOT_FOUND)); + } + + @Test + @DisplayName("publishFormRequest - 미등록 상태면 FORM_REQUEST_NOT_YET_REGISTERED 예외") + void publishFormRequest_notRegistered_throwsException() { + FormRequest unregistered = FormRequest.createRequest("https://link", "test@test.com", 100, 1000, 1L); + given(formRequestRepository.findById(1L)).willReturn(Optional.of(unregistered)); + FormPublishRequest publishRequest = new FormPublishRequest(null, null, null); + + assertThatThrownBy(() -> formCommandService.publishFormRequest(1L, publishRequest)) + .isInstanceOf(CustomException.class) + .satisfies(ex -> assertThat(((CustomException) ex).getErrorCode()) + .isEqualTo(SurveyErrorCode.FORM_REQUEST_NOT_YET_REGISTERED)); + } + + @Test + @DisplayName("publishFormRequest - 회원 없으면 FORM_REQUEST_MEMBER_NOT_FOUND 예외") + void publishFormRequest_memberNotFound_throwsException() { + FormRequest registered = buildRegisteredRequest(); + given(formRequestRepository.findById(1L)).willReturn(Optional.of(registered)); + given(memberFinder.searchMembers("test@test.com", null, null, null)).willReturn(List.of()); + FormPublishRequest publishRequest = new FormPublishRequest(null, null, null); + + assertThatThrownBy(() -> formCommandService.publishFormRequest(1L, publishRequest)) + .isInstanceOf(CustomException.class) + .satisfies(ex -> assertThat(((CustomException) ex).getErrorCode()) + .isEqualTo(SurveyErrorCode.FORM_REQUEST_MEMBER_NOT_FOUND)); + } + + @Test + @DisplayName("publishFormRequest - 회원이 복수면 FORM_REQUEST_MEMBER_NOT_FOUND 예외") + void publishFormRequest_multipleMembers_throwsException() { + FormRequest registered = buildRegisteredRequest(); + given(formRequestRepository.findById(1L)).willReturn(Optional.of(registered)); + given(memberFinder.searchMembers("test@test.com", null, null, null)) + .willReturn(List.of(buildMemberResult(1L), buildMemberResult(2L))); + FormPublishRequest publishRequest = new FormPublishRequest(null, null, null); + + assertThatThrownBy(() -> formCommandService.publishFormRequest(1L, publishRequest)) + .isInstanceOf(CustomException.class) + .satisfies(ex -> assertThat(((CustomException) ex).getErrorCode()) + .isEqualTo(SurveyErrorCode.FORM_REQUEST_MEMBER_NOT_FOUND)); + } + + @Test + @DisplayName("publishFormRequest - 성공 시 surveyCommand.submitSurvey 호출") + void publishFormRequest_success_callsSubmitSurvey() { + FormRequest registered = buildRegisteredRequest(); + given(formRequestRepository.findById(1L)).willReturn(Optional.of(registered)); + given(memberFinder.searchMembers("test@test.com", null, null, null)) + .willReturn(List.of(buildMemberResult(99L))); + SurveyFormResponse mockResponse = SurveyFormResponse.builder().build(); + given(surveyCommand.submitSurvey(anyLong(), anyLong(), isNull())).willReturn(mockResponse); + + FormPublishRequest publishRequest = new FormPublishRequest(null, null, null); + + SurveyFormResponse result = formCommandService.publishFormRequest(1L, publishRequest); + + assertThat(result).isNotNull(); + verify(surveyCommand).submitSurvey(99L, 10L, null); + } +} diff --git a/src/test/java/OneQ/OnSurvey/domain/survey/service/formRequest/FormQueryServiceTest.java b/src/test/java/OneQ/OnSurvey/domain/survey/service/formRequest/FormQueryServiceTest.java new file mode 100644 index 00000000..1a221411 --- /dev/null +++ b/src/test/java/OneQ/OnSurvey/domain/survey/service/formRequest/FormQueryServiceTest.java @@ -0,0 +1,80 @@ +package OneQ.OnSurvey.domain.survey.service.formRequest; + +import OneQ.OnSurvey.domain.survey.entity.FormRequest; +import OneQ.OnSurvey.domain.survey.model.formRequest.FormListResponse; +import OneQ.OnSurvey.domain.survey.model.formRequest.FormValidationEmailQuotaResponse; +import OneQ.OnSurvey.domain.survey.repository.formRequest.FormRequestRepository; +import OneQ.OnSurvey.global.infra.redis.RedisCacheAction; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; + +@ExtendWith(MockitoExtension.class) +class FormQueryServiceTest { + + @Mock private RedisCacheAction redisCacheAction; + @Mock private FormRequestRepository formRequestRepository; + + @InjectMocks + private FormQueryService formQueryService; + + @BeforeEach + void setUp() { + ReflectionTestUtils.setField(formQueryService, "emailQuota", 20); + ReflectionTestUtils.setField(formQueryService, "emailHourUsageKey", "form:email:usage:"); + } + + @Test + @DisplayName("getEmailQuota - 사용량 없으면 전체 한도 반환") + void getEmailQuota_noUsage_returnsFullQuota() { + given(redisCacheAction.getIntValue(anyString())).willReturn(0); + + FormValidationEmailQuotaResponse result = formQueryService.getEmailQuota(1L); + + assertThat(result.quota()).isEqualTo(20); + } + + @Test + @DisplayName("getEmailQuota - 사용량이 있으면 차감된 한도 반환") + void getEmailQuota_withUsage_returnsReducedQuota() { + given(redisCacheAction.getIntValue(anyString())).willReturn(5); + + FormValidationEmailQuotaResponse result = formQueryService.getEmailQuota(1L); + + assertThat(result.quota()).isEqualTo(15); + } + + @Test + @DisplayName("getAllUnregisteredRequests - 미등록 신청 목록 반환") + void getAllUnregisteredRequests_returnsFormListResponse() { + FormRequest request = FormRequest.createRequest("https://link", "email@test.com", 100, 1000, 1L); + given(formRequestRepository.findAllUnregistered()).willReturn(List.of(request)); + + FormListResponse result = formQueryService.getAllUnregisteredRequests(); + + assertThat(result.totalCount()).isEqualTo(1); + assertThat(result.requests()).hasSize(1); + } + + @Test + @DisplayName("getAllUnregisteredRequests - 빈 목록이면 totalCount=0") + void getAllUnregisteredRequests_empty_returnsZeroCount() { + given(formRequestRepository.findAllUnregistered()).willReturn(List.of()); + + FormListResponse result = formQueryService.getAllUnregisteredRequests(); + + assertThat(result.totalCount()).isEqualTo(0); + assertThat(result.requests()).isEmpty(); + } +} From 4a18c9321f52f4f76b57b14aaaeb0bb9bc0f040c Mon Sep 17 00:00:00 2001 From: jaekwan Date: Sun, 10 May 2026 16:27:56 +0900 Subject: [PATCH 13/16] =?UTF-8?q?refactor:=20@RepeatedTest=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=20=EB=B0=8F=20=ED=99=95=EB=A5=A0=EC=A0=81=20=EB=8B=A4?= =?UTF-8?q?=EC=96=91=EC=84=B1=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DiscountCodeCommandServiceTest.java | 36 ------------------- 1 file changed, 36 deletions(-) diff --git a/src/test/java/OneQ/OnSurvey/domain/discount/service/DiscountCodeCommandServiceTest.java b/src/test/java/OneQ/OnSurvey/domain/discount/service/DiscountCodeCommandServiceTest.java index 975e1d41..a67b2a9e 100644 --- a/src/test/java/OneQ/OnSurvey/domain/discount/service/DiscountCodeCommandServiceTest.java +++ b/src/test/java/OneQ/OnSurvey/domain/discount/service/DiscountCodeCommandServiceTest.java @@ -5,7 +5,6 @@ import OneQ.OnSurvey.domain.discount.model.response.DiscountCodeResponse; import OneQ.OnSurvey.domain.discount.repository.DiscountCodeRepository; import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.RepeatedTest; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; @@ -14,8 +13,6 @@ import org.mockito.junit.jupiter.MockitoExtension; import java.time.LocalDate; -import java.util.HashSet; -import java.util.Set; import java.util.regex.Pattern; import static org.assertj.core.api.Assertions.assertThat; @@ -92,37 +89,4 @@ void create_savedEntity_hasCorrectOrgName() { assertThat(CODE_PATTERN.matcher(saved.getCode()).matches()).isTrue(); } - @RepeatedTest(20) - @DisplayName("반복 생성 시 매번 유효한 6자리 코드 생성") - void create_repeatedCalls_alwaysValidCode() { - CreateDiscountCodeRequest request = new CreateDiscountCodeRequest( - "RepeatedOrg", LocalDate.of(2027, 12, 31) - ); - given(discountCodeRepository.existsByCode(anyString())).willReturn(false); - given(discountCodeRepository.save(any(DiscountCode.class))) - .willAnswer(inv -> inv.getArgument(0)); - - DiscountCodeResponse response = discountCodeCommandService.create(request); - - assertThat(CODE_PATTERN.matcher(response.code()).matches()).isTrue(); - } - - @Test - @DisplayName("여러 번 생성 시 코드가 무작위로 다양하게 생성됨") - void create_multipleInvocations_producesDiverseCodes() { - CreateDiscountCodeRequest request = new CreateDiscountCodeRequest( - "Org", LocalDate.of(2027, 1, 1) - ); - given(discountCodeRepository.existsByCode(anyString())).willReturn(false); - given(discountCodeRepository.save(any(DiscountCode.class))) - .willAnswer(inv -> inv.getArgument(0)); - - Set codes = new HashSet<>(); - for (int i = 0; i < 30; i++) { - codes.add(discountCodeCommandService.create(request).code()); - } - - // 30번 중 최소 10개 이상 고유한 코드가 생성되어야 함 (완전 랜덤성 확인) - assertThat(codes.size()).isGreaterThan(10); - } } From 5acd05f8eb1014ec98ac3f164ec9899c7b68d615 Mon Sep 17 00:00:00 2001 From: jaekwan Date: Sun, 10 May 2026 16:30:10 +0900 Subject: [PATCH 14/16] =?UTF-8?q?refactor:=20=EC=85=80=ED=94=84=20?= =?UTF-8?q?=EB=A6=AC=EB=B7=B0=20=EB=B0=98=EC=98=81=20-=20=EB=AF=B8?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=20import=20=EC=A0=95=EB=A6=AC,=20verify=20?= =?UTF-8?q?=EB=B3=B4=EC=99=84,=20DisplayName=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/DiscountCodeQueryServiceTest.java | 12 ++++++------ .../question/service/QuestionCommandServiceTest.java | 3 ++- .../service/command/SurveyCommandServiceTest.java | 2 +- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/test/java/OneQ/OnSurvey/domain/discount/service/DiscountCodeQueryServiceTest.java b/src/test/java/OneQ/OnSurvey/domain/discount/service/DiscountCodeQueryServiceTest.java index 1cc5cbed..b13a9054 100644 --- a/src/test/java/OneQ/OnSurvey/domain/discount/service/DiscountCodeQueryServiceTest.java +++ b/src/test/java/OneQ/OnSurvey/domain/discount/service/DiscountCodeQueryServiceTest.java @@ -104,14 +104,14 @@ void getByCode_valid_returnsEntity() { } @Test - @DisplayName("findAll - 활성 코드가 만료 코드보다 먼저, 각 그룹 내 만료일 오름차순") + @DisplayName("findAll - 활성 코드 우선, 만료 코드 후순; 각 그룹 내 만료일 오름차순(가까운 날짜 먼저)") void findAll_sortedActiveFirst() { LocalDate today = LocalDate.now(); - DiscountCode expired1 = buildCode("EXP001", today.minusDays(5)); - DiscountCode expired2 = buildCode("EXP002", today.minusDays(1)); - DiscountCode active1 = buildCode("ACT001", today.plusDays(10)); - DiscountCode active2 = buildCode("ACT002", today.plusDays(3)); - given(discountCodeRepository.findAll()).willReturn(List.of(expired1, active1, expired2, active2)); + DiscountCode expiredEarlier = buildCode("EXP001", today.minusDays(5)); + DiscountCode expiredLater = buildCode("EXP002", today.minusDays(1)); + DiscountCode activeNear = buildCode("ACT002", today.plusDays(3)); + DiscountCode activeFar = buildCode("ACT001", today.plusDays(10)); + given(discountCodeRepository.findAll()).willReturn(List.of(expiredEarlier, activeFar, expiredLater, activeNear)); List results = discountCodeQueryService.findAll(); diff --git a/src/test/java/OneQ/OnSurvey/domain/question/service/QuestionCommandServiceTest.java b/src/test/java/OneQ/OnSurvey/domain/question/service/QuestionCommandServiceTest.java index f5c97bd3..d64d07c6 100644 --- a/src/test/java/OneQ/OnSurvey/domain/question/service/QuestionCommandServiceTest.java +++ b/src/test/java/OneQ/OnSurvey/domain/question/service/QuestionCommandServiceTest.java @@ -114,7 +114,7 @@ void upsertSections_existingSection_updatesSection() { } @Test - @DisplayName("upsertSections - DB에만 있는 섹션은 삭제") + @DisplayName("upsertSections - DB에만 있는 섹션은 삭제, saveAll은 호출되지 않음") void upsertSections_extraDbSections_deletesOldSections() { Section extra = buildSection(50L, 1L, "삭제될 섹션", 1, 2); given(sectionRepository.findAllSectionBySurveyId(1L)).willReturn(List.of(extra)); @@ -122,5 +122,6 @@ void upsertSections_extraDbSections_deletesOldSections() { questionCommandService.upsertSections(1L, List.of()); verify(sectionRepository).deleteAll(List.of(50L)); + verify(sectionRepository, never()).saveAll(any()); } } diff --git a/src/test/java/OneQ/OnSurvey/domain/survey/service/command/SurveyCommandServiceTest.java b/src/test/java/OneQ/OnSurvey/domain/survey/service/command/SurveyCommandServiceTest.java index 69ca233f..91076b0d 100644 --- a/src/test/java/OneQ/OnSurvey/domain/survey/service/command/SurveyCommandServiceTest.java +++ b/src/test/java/OneQ/OnSurvey/domain/survey/service/command/SurveyCommandServiceTest.java @@ -78,7 +78,7 @@ private Member buildMember(Long userKey) { Member member = Member.createMember( userKey, "홍길동", "010-1234-5678", "19900101", "test@test.com", - OneQ.OnSurvey.domain.survey.model.Gender.MALE, Role.ROLE_MEMBER, MemberStatus.ACTIVE + Gender.MALE, Role.ROLE_MEMBER, MemberStatus.ACTIVE ); member.increaseCoin(1000L); return member; From 830b9d88255e0e7e69fedb87200b27d41360ca6a Mon Sep 17 00:00:00 2001 From: jaekwan Date: Tue, 12 May 2026 22:17:01 +0900 Subject: [PATCH 15/16] =?UTF-8?q?test:=20=EC=9D=8C=EC=88=98=20=EA=B2=BD?= =?UTF-8?q?=EA=B3=84=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EB=B0=8F=20serviceAg?= =?UTF-8?q?reed=20=EA=B2=A9=EB=A6=AC=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../OnSurvey/domain/member/MemberTest.java | 22 +++++++++++++++++++ .../service/MemberModifyServiceTest.java | 4 ++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/test/java/OneQ/OnSurvey/domain/member/MemberTest.java b/src/test/java/OneQ/OnSurvey/domain/member/MemberTest.java index 73e7b515..ab0fa69f 100644 --- a/src/test/java/OneQ/OnSurvey/domain/member/MemberTest.java +++ b/src/test/java/OneQ/OnSurvey/domain/member/MemberTest.java @@ -91,6 +91,17 @@ void decreaseCoin_zero_throwsException() { .isEqualTo(CoinErrorCode.COIN_NOT_POSITIVE)); } + @Test + @DisplayName("decreaseCoin - 음수이면 COIN_NOT_POSITIVE 예외") + void decreaseCoin_negative_throwsException() { + Member member = buildMember(); + + assertThatThrownBy(() -> member.decreaseCoin(-1L)) + .isInstanceOf(CustomException.class) + .satisfies(ex -> assertThat(((CustomException) ex).getErrorCode()) + .isEqualTo(CoinErrorCode.COIN_NOT_POSITIVE)); + } + @Test @DisplayName("increasePromotionPoint - 양수 금액이면 포인트 증가") void increasePromotionPoint_positiveAmount_increasesPoint() { @@ -112,6 +123,17 @@ void increasePromotionPoint_zero_throwsException() { .isEqualTo(CoinErrorCode.COIN_NOT_POSITIVE)); } + @Test + @DisplayName("increasePromotionPoint - 음수이면 COIN_NOT_POSITIVE 예외") + void increasePromotionPoint_negative_throwsException() { + Member member = buildMember(); + + assertThatThrownBy(() -> member.increasePromotionPoint(-1L)) + .isInstanceOf(CustomException.class) + .satisfies(ex -> assertThat(((CustomException) ex).getErrorCode()) + .isEqualTo(CoinErrorCode.COIN_NOT_POSITIVE)); + } + @Test @DisplayName("update - null 값은 무시하고 비null 값만 업데이트") void update_nullFieldsIgnored() { diff --git a/src/test/java/OneQ/OnSurvey/domain/member/service/MemberModifyServiceTest.java b/src/test/java/OneQ/OnSurvey/domain/member/service/MemberModifyServiceTest.java index f13ea65e..ee17cced 100644 --- a/src/test/java/OneQ/OnSurvey/domain/member/service/MemberModifyServiceTest.java +++ b/src/test/java/OneQ/OnSurvey/domain/member/service/MemberModifyServiceTest.java @@ -91,7 +91,7 @@ void upsertMember_withServiceAgreed_setsServiceAgreedTrue() { given(memberRepository.save(any(Member.class))).willAnswer(inv -> inv.getArgument(0)); DecryptedLoginMeResponse loginResponse = new DecryptedLoginMeResponse( - 3000L, "scope", List.of("serviceAgreed", "marketingAgreed"), + 3000L, "scope", List.of("serviceAgreed"), "policy", "certTxId", "이름", "010-1111-1111", "20000101", Gender.MALE, "KR", "email@test.com" ); @@ -99,7 +99,7 @@ void upsertMember_withServiceAgreed_setsServiceAgreedTrue() { Member result = memberModifyService.upsertMember(loginResponse); assertThat(result.isServiceAgreed()).isTrue(); - assertThat(result.isMarketingAgreed()).isTrue(); + assertThat(result.isMarketingAgreed()).isFalse(); } @Test From 53fc2389be291967472510852959eb3141ba6117 Mon Sep 17 00:00:00 2001 From: jaekwan Date: Tue, 12 May 2026 22:19:36 +0900 Subject: [PATCH 16/16] =?UTF-8?q?test:=20CodeRabbit=20=EB=B0=98=EC=98=81?= =?UTF-8?q?=20-=20=EA=B2=BD=EA=B3=84=20=EC=BC=80=EC=9D=B4=EC=8A=A4,=20neve?= =?UTF-8?q?r=20matcher=20=EA=B0=9C=EC=84=A0,=20ArgumentCaptor=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/DiscountCodeQueryServiceTest.java | 15 +++++++++ .../QuestionAnswerCommandServiceTest.java | 3 +- .../response/ResponseQueryServiceTest.java | 6 ++-- .../service/SurveyGlobalStatsServiceTest.java | 32 +++++++++++++++++-- 4 files changed, 50 insertions(+), 6 deletions(-) diff --git a/src/test/java/OneQ/OnSurvey/domain/discount/service/DiscountCodeQueryServiceTest.java b/src/test/java/OneQ/OnSurvey/domain/discount/service/DiscountCodeQueryServiceTest.java index b13a9054..27bcbeb3 100644 --- a/src/test/java/OneQ/OnSurvey/domain/discount/service/DiscountCodeQueryServiceTest.java +++ b/src/test/java/OneQ/OnSurvey/domain/discount/service/DiscountCodeQueryServiceTest.java @@ -122,6 +122,21 @@ void findAll_sortedActiveFirst() { assertThat(results.get(3).code()).isEqualTo("EXP002"); } + @Test + @DisplayName("findAll - 만료일이 오늘인 코드는 활성으로 분류(isBefore 기준)") + void findAll_todayExpiry_treatedAsActive() { + LocalDate today = LocalDate.now(); + DiscountCode todayCode = buildCode("TODAY1", today); + DiscountCode expiredCode = buildCode("EXP001", today.minusDays(1)); + given(discountCodeRepository.findAll()).willReturn(List.of(expiredCode, todayCode)); + + List results = discountCodeQueryService.findAll(); + + assertThat(results).hasSize(2); + assertThat(results.get(0).code()).isEqualTo("TODAY1"); + assertThat(results.get(1).code()).isEqualTo("EXP001"); + } + @Test @DisplayName("findAll - 빈 목록이면 빈 리스트 반환") void findAll_empty_returnsEmptyList() { diff --git a/src/test/java/OneQ/OnSurvey/domain/participation/service/answer/QuestionAnswerCommandServiceTest.java b/src/test/java/OneQ/OnSurvey/domain/participation/service/answer/QuestionAnswerCommandServiceTest.java index 6c099dfe..d2eabe8c 100644 --- a/src/test/java/OneQ/OnSurvey/domain/participation/service/answer/QuestionAnswerCommandServiceTest.java +++ b/src/test/java/OneQ/OnSurvey/domain/participation/service/answer/QuestionAnswerCommandServiceTest.java @@ -17,6 +17,7 @@ import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -66,6 +67,6 @@ void updateResponseAfterQuestionAnswers_alreadyResponded_doesNotSave() { questionAnswerCommandService.updateResponseAfterQuestionAnswers(1L, 2L); - verify(responseRepository, never()).save(response); + verify(responseRepository, never()).save(any(Response.class)); } } diff --git a/src/test/java/OneQ/OnSurvey/domain/participation/service/response/ResponseQueryServiceTest.java b/src/test/java/OneQ/OnSurvey/domain/participation/service/response/ResponseQueryServiceTest.java index ed735909..dfea8d51 100644 --- a/src/test/java/OneQ/OnSurvey/domain/participation/service/response/ResponseQueryServiceTest.java +++ b/src/test/java/OneQ/OnSurvey/domain/participation/service/response/ResponseQueryServiceTest.java @@ -15,9 +15,11 @@ import java.util.List; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.verify; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; @ExtendWith(MockitoExtension.class) class ResponseQueryServiceTest { @@ -48,7 +50,7 @@ void getResponseCount_nullFilter_delegatesToNoFilter() { assertThat(result).isEqualTo(30); verify(responseRepository).getResponseCountBySurveyId(1L); - verify(responseRepository, never()).getResponseCountBySurveyId(1L, null); + verify(responseRepository, never()).getResponseCountBySurveyId(anyLong(), any()); } @Test diff --git a/src/test/java/OneQ/OnSurvey/domain/survey/service/SurveyGlobalStatsServiceTest.java b/src/test/java/OneQ/OnSurvey/domain/survey/service/SurveyGlobalStatsServiceTest.java index 776fa728..ced3b4e3 100644 --- a/src/test/java/OneQ/OnSurvey/domain/survey/service/SurveyGlobalStatsServiceTest.java +++ b/src/test/java/OneQ/OnSurvey/domain/survey/service/SurveyGlobalStatsServiceTest.java @@ -8,6 +8,7 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @@ -19,6 +20,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willAnswer; import static org.mockito.Mockito.verify; @ExtendWith(MockitoExtension.class) @@ -61,13 +63,13 @@ void addDueCount_increasesExistingStats() { @Test @DisplayName("addDueCount - stats 없으면 init 후 증가") void addDueCount_noStats_initAndIncreases() { - SurveyGlobalStats newStats = SurveyGlobalStats.init(); given(statsRepository.findById(1L)).willReturn(Optional.empty()); - given(statsRepository.save(any(SurveyGlobalStats.class))).willReturn(newStats); + ArgumentCaptor captor = ArgumentCaptor.forClass(SurveyGlobalStats.class); + given(statsRepository.save(captor.capture())).willAnswer(inv -> inv.getArgument(0)); surveyGlobalStatsService.addDueCount(50L); - assertThat(newStats.getTotalDueCount()).isEqualTo(1050L); + assertThat(captor.getValue().getTotalDueCount()).isEqualTo(1050L); } @Test @@ -81,6 +83,18 @@ void addCompletedCount_increasesExistingStats() { assertThat(stats.getTotalCompletedCount()).isEqualTo(530L); } + @Test + @DisplayName("addCompletedCount - stats 없으면 init 후 증가") + void addCompletedCount_noStats_initAndIncreases() { + given(statsRepository.findById(1L)).willReturn(Optional.empty()); + ArgumentCaptor captor = ArgumentCaptor.forClass(SurveyGlobalStats.class); + given(statsRepository.save(captor.capture())).willAnswer(inv -> inv.getArgument(0)); + + surveyGlobalStatsService.addCompletedCount(30L); + + assertThat(captor.getValue().getTotalCompletedCount()).isEqualTo(1030L); + } + @Test @DisplayName("addPromotionCount - 기존 stats에 delta만큼 증가") void addPromotionCount_increasesExistingStats() { @@ -92,6 +106,18 @@ void addPromotionCount_increasesExistingStats() { assertThat(stats.getTotalPromotionCount()).isEqualTo(210L); } + @Test + @DisplayName("addPromotionCount - stats 없으면 init 후 증가") + void addPromotionCount_noStats_initAndIncreases() { + given(statsRepository.findById(1L)).willReturn(Optional.empty()); + ArgumentCaptor captor = ArgumentCaptor.forClass(SurveyGlobalStats.class); + given(statsRepository.save(captor.capture())).willAnswer(inv -> inv.getArgument(0)); + + surveyGlobalStatsService.addPromotionCount(10L); + + assertThat(captor.getValue().getTotalPromotionCount()).isEqualTo(1010L); + } + @Test @DisplayName("getStats - stats 존재 시 dailyUserCount와 함께 반환") void getStats_existingStats_returnsCorrectValues() {