From 08e3d5bcb01a958381c7e56ac2913122846d6940 Mon Sep 17 00:00:00 2001 From: songhyeonpk Date: Fri, 11 Apr 2025 13:56:08 +0900 Subject: [PATCH 1/6] =?UTF-8?q?refactor:=20=EA=B7=B8=EB=A3=A8=EB=B0=8D=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20=EB=A6=AC=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../response/GroomingTestsInfoResponse.java | 54 ++++++++++++-- .../out/cache/CaffeineCacheAdapter.java | 71 +++++++++++++++++++ .../in/grooming/LoadGroomingTestsUseCase.java | 4 +- .../cache/LoadGroomingTestsWithCachePort.java | 11 +++ .../grooming/LoadGroomingTestsService.java | 49 ++++--------- .../vo/grooming/GroomingTestAnswerInfoVo.java | 2 - .../grooming/LoadGroomingTestsTest.java | 30 ++++---- 7 files changed, 163 insertions(+), 58 deletions(-) create mode 100644 src/main/java/com/ftm/server/adapter/out/cache/CaffeineCacheAdapter.java create mode 100644 src/main/java/com/ftm/server/application/port/out/cache/LoadGroomingTestsWithCachePort.java diff --git a/src/main/java/com/ftm/server/adapter/in/web/grooming/dto/response/GroomingTestsInfoResponse.java b/src/main/java/com/ftm/server/adapter/in/web/grooming/dto/response/GroomingTestsInfoResponse.java index d1c3e08..982843f 100644 --- a/src/main/java/com/ftm/server/adapter/in/web/grooming/dto/response/GroomingTestsInfoResponse.java +++ b/src/main/java/com/ftm/server/adapter/in/web/grooming/dto/response/GroomingTestsInfoResponse.java @@ -1,22 +1,66 @@ package com.ftm.server.adapter.in.web.grooming.dto.response; +import com.ftm.server.application.vo.grooming.GroomingTestAnswerInfoVo; import com.ftm.server.application.vo.grooming.GroomingTestQuestionWithAnswersVo; -import java.util.Set; +import com.ftm.server.domain.enums.GroomingCategory; +import java.util.List; import lombok.Getter; @Getter public class GroomingTestsInfoResponse { private final Integer totalCount; - private final Set groomingTests; + private final List groomingTests; - GroomingTestsInfoResponse(Set groomingTests) { + GroomingTestsInfoResponse(List groomingTests) { this.totalCount = groomingTests.size(); - this.groomingTests = groomingTests; + this.groomingTests = + groomingTests.stream().map(GroomingTestQuestionWithAnswersResponse::from).toList(); } public static GroomingTestsInfoResponse from( - Set groomingTests) { + List groomingTests) { return new GroomingTestsInfoResponse(groomingTests); } + + @Getter + public static class GroomingTestQuestionWithAnswersResponse { + private final Long groomingTestQuestionId; + private final String question; + private final GroomingCategory groomingCategory; + private final List answers; + + GroomingTestQuestionWithAnswersResponse( + GroomingTestQuestionWithAnswersVo groomingTestQuestionWithAnswersVo) { + this.groomingTestQuestionId = + groomingTestQuestionWithAnswersVo.getGroomingTestQuestionId(); + this.question = groomingTestQuestionWithAnswersVo.getQuestion(); + this.groomingCategory = groomingTestQuestionWithAnswersVo.getGroomingCategory(); + this.answers = + groomingTestQuestionWithAnswersVo.getAnswers().stream() + .map(GroomingTestAnswerInfoResponse::from) + .toList(); + } + + public static GroomingTestQuestionWithAnswersResponse from( + GroomingTestQuestionWithAnswersVo groomingTestQuestionWithAnswersVo) { + return new GroomingTestQuestionWithAnswersResponse(groomingTestQuestionWithAnswersVo); + } + + @Getter + public static class GroomingTestAnswerInfoResponse { + private final Long groomingTestAnswerId; + private final String answer; + + GroomingTestAnswerInfoResponse(GroomingTestAnswerInfoVo groomingTestAnswerInfoVo) { + this.groomingTestAnswerId = groomingTestAnswerInfoVo.getGroomingTestAnswerId(); + this.answer = groomingTestAnswerInfoVo.getAnswer(); + } + + public static GroomingTestAnswerInfoResponse from( + GroomingTestAnswerInfoVo groomingTestAnswerInfoVo) { + return new GroomingTestAnswerInfoResponse(groomingTestAnswerInfoVo); + } + } + } } diff --git a/src/main/java/com/ftm/server/adapter/out/cache/CaffeineCacheAdapter.java b/src/main/java/com/ftm/server/adapter/out/cache/CaffeineCacheAdapter.java new file mode 100644 index 0000000..48b9498 --- /dev/null +++ b/src/main/java/com/ftm/server/adapter/out/cache/CaffeineCacheAdapter.java @@ -0,0 +1,71 @@ +package com.ftm.server.adapter.out.cache; + +import static com.ftm.server.common.consts.StaticConsts.GROOMING_TESTS_INFO_CACHE_KEY_ALL; +import static com.ftm.server.common.consts.StaticConsts.GROOMING_TESTS_INFO_CACHE_NAME; + +import com.ftm.server.adapter.out.persistence.mapper.GroomingTestAnswerMapper; +import com.ftm.server.adapter.out.persistence.mapper.GroomingTestQuestionMapper; +import com.ftm.server.adapter.out.persistence.repository.GroomingTestAnswerRepository; +import com.ftm.server.adapter.out.persistence.repository.GroomingTestQuestionRepository; +import com.ftm.server.application.port.out.cache.LoadGroomingTestsWithCachePort; +import com.ftm.server.application.vo.grooming.GroomingTestAnswerInfoVo; +import com.ftm.server.application.vo.grooming.GroomingTestQuestionWithAnswersVo; +import com.ftm.server.common.annotation.Adapter; +import com.ftm.server.domain.entity.GroomingTestAnswer; +import com.ftm.server.domain.entity.GroomingTestQuestion; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.Cacheable; + +@Slf4j +@Adapter +@RequiredArgsConstructor +public class CaffeineCacheAdapter implements LoadGroomingTestsWithCachePort { + + private final GroomingTestQuestionRepository groomingTestQuestionRepository; + private final GroomingTestAnswerRepository groomingTestAnswerRepository; + + private final GroomingTestQuestionMapper groomingTestQuestionMapper; + private final GroomingTestAnswerMapper groomingTestAnswerMapper; + + @Cacheable(value = GROOMING_TESTS_INFO_CACHE_NAME, key = GROOMING_TESTS_INFO_CACHE_KEY_ALL) + @Override + public List loadGroomingTestsCache() { + // 모든 그루밍 테스트 질문 목록 조회 + List questions = + groomingTestQuestionRepository.findAll().stream() + .map(groomingTestQuestionMapper::toDomain) + .toList(); + + // 모든 그루밍 테스트 답변 목록 조회 + List answers = + groomingTestAnswerRepository.findAll().stream() + .map(groomingTestAnswerMapper::toDomain) + .toList(); + + // 같은 질문의 답변 목록끼리 그룹화 + Map> answersByQuestionId = + answers.stream() + .collect( + Collectors.groupingBy( + GroomingTestAnswer + ::getGroomingTestQuestionId, // 질문 ID를 기준으로 답변 그룹화 + Collectors.mapping( + GroomingTestAnswerInfoVo::from, + Collectors.toList()))); + + // 각 그루밍 테스트 질문에 속한 답변목록을 함께 담아 응답 + return questions.stream() + .map( + question -> + GroomingTestQuestionWithAnswersVo.from( + question, + answersByQuestionId.getOrDefault( + question.getId(), List.of()))) + .collect(Collectors.toCollection(ArrayList::new)); + } +} diff --git a/src/main/java/com/ftm/server/application/port/in/grooming/LoadGroomingTestsUseCase.java b/src/main/java/com/ftm/server/application/port/in/grooming/LoadGroomingTestsUseCase.java index e3ddca4..18394e6 100644 --- a/src/main/java/com/ftm/server/application/port/in/grooming/LoadGroomingTestsUseCase.java +++ b/src/main/java/com/ftm/server/application/port/in/grooming/LoadGroomingTestsUseCase.java @@ -2,10 +2,10 @@ import com.ftm.server.application.vo.grooming.GroomingTestQuestionWithAnswersVo; import com.ftm.server.common.annotation.UseCase; -import java.util.Set; +import java.util.List; @UseCase public interface LoadGroomingTestsUseCase { - Set execute(); + List execute(); } diff --git a/src/main/java/com/ftm/server/application/port/out/cache/LoadGroomingTestsWithCachePort.java b/src/main/java/com/ftm/server/application/port/out/cache/LoadGroomingTestsWithCachePort.java new file mode 100644 index 0000000..3b7be31 --- /dev/null +++ b/src/main/java/com/ftm/server/application/port/out/cache/LoadGroomingTestsWithCachePort.java @@ -0,0 +1,11 @@ +package com.ftm.server.application.port.out.cache; + +import com.ftm.server.application.vo.grooming.GroomingTestQuestionWithAnswersVo; +import com.ftm.server.common.annotation.Port; +import java.util.List; + +@Port +public interface LoadGroomingTestsWithCachePort { + + List loadGroomingTestsCache(); +} diff --git a/src/main/java/com/ftm/server/application/service/grooming/LoadGroomingTestsService.java b/src/main/java/com/ftm/server/application/service/grooming/LoadGroomingTestsService.java index 7b916a2..43d30e3 100644 --- a/src/main/java/com/ftm/server/application/service/grooming/LoadGroomingTestsService.java +++ b/src/main/java/com/ftm/server/application/service/grooming/LoadGroomingTestsService.java @@ -1,56 +1,31 @@ package com.ftm.server.application.service.grooming; -import static com.ftm.server.common.consts.StaticConsts.GROOMING_TESTS_INFO_CACHE_KEY_ALL; -import static com.ftm.server.common.consts.StaticConsts.GROOMING_TESTS_INFO_CACHE_NAME; - import com.ftm.server.application.port.in.grooming.LoadGroomingTestsUseCase; -import com.ftm.server.application.port.out.persistence.grooming.LoadGroomingTestAnswerPort; -import com.ftm.server.application.port.out.persistence.grooming.LoadGroomingTestQuestionPort; -import com.ftm.server.application.vo.grooming.GroomingTestAnswerInfoVo; +import com.ftm.server.application.port.out.cache.LoadGroomingTestsWithCachePort; import com.ftm.server.application.vo.grooming.GroomingTestQuestionWithAnswersVo; -import com.ftm.server.domain.entity.GroomingTestAnswer; -import com.ftm.server.domain.entity.GroomingTestQuestion; +import java.util.Collections; import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; -import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor public class LoadGroomingTestsService implements LoadGroomingTestsUseCase { - private final LoadGroomingTestQuestionPort loadGroomingTestQuestionPort; - private final LoadGroomingTestAnswerPort loadGroomingTestAnswerPort; + private final LoadGroomingTestsWithCachePort loadGroomingTestsWithCachePort; @Override - @Cacheable(value = GROOMING_TESTS_INFO_CACHE_NAME, key = GROOMING_TESTS_INFO_CACHE_KEY_ALL) - public Set execute() { - // 모든 그루밍 테스트 질문 목록 조회 - List questions = - loadGroomingTestQuestionPort.loadGroomingTestQuestions(); + @Transactional(readOnly = true) + public List execute() { - // 모든 그루밍 테스트 답변 목록 조회 - List answers = loadGroomingTestAnswerPort.loadGroomingTestAnswers(); + // 캐싱된 그루밍 테스트 목록 조회 + List infos = + loadGroomingTestsWithCachePort.loadGroomingTestsCache(); - // 같은 질문의 답변 목록끼리 그룹화 - Map> answersByQuestionId = - answers.stream() - .map(GroomingTestAnswerInfoVo::from) - .collect( - Collectors.groupingBy( - GroomingTestAnswerInfoVo::getGroomingTestQuestionId)); + // 그루밍 테스트 목록의 순서를 랜덤하게 섞는 작업 + Collections.shuffle(infos); - // 각 그루밍 테스트 질문에 속한 답변목록을 함께 담아 응답, 순서 랜덤 - return questions.stream() - .map( - question -> - GroomingTestQuestionWithAnswersVo.from( - question, - answersByQuestionId.getOrDefault( - question.getId(), List.of()))) - .collect(Collectors.toSet()); + return infos; } } diff --git a/src/main/java/com/ftm/server/application/vo/grooming/GroomingTestAnswerInfoVo.java b/src/main/java/com/ftm/server/application/vo/grooming/GroomingTestAnswerInfoVo.java index dd1e390..0ba74c0 100644 --- a/src/main/java/com/ftm/server/application/vo/grooming/GroomingTestAnswerInfoVo.java +++ b/src/main/java/com/ftm/server/application/vo/grooming/GroomingTestAnswerInfoVo.java @@ -9,13 +9,11 @@ public class GroomingTestAnswerInfoVo { private Long groomingTestAnswerId; - private Long groomingTestQuestionId; private String answer; private Integer score; private GroomingTestAnswerInfoVo(GroomingTestAnswer groomingTestAnswer) { this.groomingTestAnswerId = groomingTestAnswer.getId(); - this.groomingTestQuestionId = groomingTestAnswer.getGroomingTestQuestionId(); this.answer = groomingTestAnswer.getAnswer(); this.score = groomingTestAnswer.getScore(); } diff --git a/src/test/java/com/ftm/server/grooming/LoadGroomingTestsTest.java b/src/test/java/com/ftm/server/grooming/LoadGroomingTestsTest.java index 5eee818..3c319bc 100644 --- a/src/test/java/com/ftm/server/grooming/LoadGroomingTestsTest.java +++ b/src/test/java/com/ftm/server/grooming/LoadGroomingTestsTest.java @@ -1,19 +1,22 @@ package com.ftm.server.grooming; import static com.epages.restdocs.apispec.ResourceDocumentation.resource; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; import static org.springframework.restdocs.payload.JsonFieldType.*; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.epages.restdocs.apispec.ResourceSnippetParameters; import com.ftm.server.BaseTest; +import com.ftm.server.application.port.out.cache.LoadGroomingTestsWithCachePort; import java.util.List; import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; import org.springframework.restdocs.payload.FieldDescriptor; @@ -22,6 +25,10 @@ public class LoadGroomingTestsTest extends BaseTest { + @SuppressWarnings("removal") + @SpyBean + private LoadGroomingTestsWithCachePort loadGroomingTestsWithCachePort; + private final List responseFieldLoadGroomingTests = List.of( fieldWithPath("status").type(NUMBER).description("응답 상태"), @@ -47,15 +54,9 @@ public class LoadGroomingTestsTest extends BaseTest { fieldWithPath("data.groomingTests[].answers[].groomingTestAnswerId") .type(NUMBER) .description("그루밍 테스트 답변 id"), - fieldWithPath("data.groomingTests[].answers[].groomingTestQuestionId") - .type(NUMBER) - .description("그루밍 테스트 답변이 속한 질문 id"), fieldWithPath("data.groomingTests[].answers[].answer") .type(STRING) - .description("그루밍 테스트 답변 내용"), - fieldWithPath("data.groomingTests[].answers[].score") - .type(NUMBER) - .description("그루밍 테스트 답변 점수")); + .description("그루밍 테스트 답변 내용")); private RestDocumentationResultHandler getDocument(Integer identifier) { return document( @@ -75,12 +76,17 @@ private RestDocumentationResultHandler getDocument(Integer identifier) { @Test @Transactional void 그루밍_테스트_목록_조회_성공() throws Exception { - // when + // then + // 첫번째 호출 -> 캐시 miss -> 실제 메서드 실행 + mockMvc.perform(RestDocumentationRequestBuilders.get("/api/grooming/tests")) + .andExpect(status().isOk()); + + // 두번째 호출 -> 캐시 HIT -> 메소드 호출 없이 응답 ResultActions resultActions = - mockMvc.perform(RestDocumentationRequestBuilders.get("/api/grooming/tests")); + mockMvc.perform(RestDocumentationRequestBuilders.get("/api/grooming/tests")) + .andExpect(status().isOk()); - // then - resultActions.andExpect(status().isOk()).andDo(print()); + verify(loadGroomingTestsWithCachePort, times(1)).loadGroomingTestsCache(); // documentation resultActions.andDo(getDocument(1)); From f5fc91193e503d6030cb8d57d943f14ea955f858 Mon Sep 17 00:00:00 2001 From: songhyeonpk Date: Fri, 11 Apr 2025 13:58:34 +0900 Subject: [PATCH 2/6] =?UTF-8?q?feat:=20=EA=B7=B8=EB=A3=A8=EB=B0=8D=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EB=82=B4=EC=97=AD=EC=9D=98=20?= =?UTF-8?q?=EC=9C=A0=ED=9A=A8=EC=84=B1=EC=9D=84=20=EA=B2=80=EC=A6=9D?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../grooming/GroomingTestValidator.java | 72 +++++++++++++++++++ .../vo/grooming/SubmitGroomingTestVo.java | 46 ++++++++++++ .../response/enums/ErrorResponseCode.java | 4 ++ 3 files changed, 122 insertions(+) create mode 100644 src/main/java/com/ftm/server/application/service/grooming/GroomingTestValidator.java create mode 100644 src/main/java/com/ftm/server/application/vo/grooming/SubmitGroomingTestVo.java diff --git a/src/main/java/com/ftm/server/application/service/grooming/GroomingTestValidator.java b/src/main/java/com/ftm/server/application/service/grooming/GroomingTestValidator.java new file mode 100644 index 0000000..c07a129 --- /dev/null +++ b/src/main/java/com/ftm/server/application/service/grooming/GroomingTestValidator.java @@ -0,0 +1,72 @@ +package com.ftm.server.application.service.grooming; + +import com.ftm.server.application.port.out.cache.LoadGroomingTestsWithCachePort; +import com.ftm.server.application.vo.grooming.GroomingTestAnswerInfoVo; +import com.ftm.server.application.vo.grooming.GroomingTestQuestionWithAnswersVo; +import com.ftm.server.application.vo.grooming.SubmitGroomingTestVo; +import com.ftm.server.common.exception.CustomException; +import com.ftm.server.common.response.enums.ErrorResponseCode; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class GroomingTestValidator { + + private final LoadGroomingTestsWithCachePort loadGroomingTestsWithCachePort; + + public void execute(List submissions) { + // 1. 질문 ID 중복 검사 + List questionIds = + submissions.stream().map(SubmitGroomingTestVo::getQuestionId).toList(); + if (questionIds.size() != new HashSet<>(questionIds).size()) { + throw new CustomException(ErrorResponseCode.INVALID_GROOMING_TEST_QUESTION_ID); + } + + // 2. 캐시에서 유효한 질문/답변 조회 + List infos = + loadGroomingTestsWithCachePort.loadGroomingTestsCache(); + + Map> validAnswerMap = + infos.stream() + .collect( + Collectors.toMap( + GroomingTestQuestionWithAnswersVo + ::getGroomingTestQuestionId, + vo -> + vo.getAnswers().stream() + .map( + GroomingTestAnswerInfoVo + ::getGroomingTestAnswerId) + .collect(Collectors.toSet()))); + + // 3. 유효성 검사 + for (SubmitGroomingTestVo submitted : submissions) { + Long questionId = submitted.getQuestionId(); + List answerIds = submitted.getAnswerIds(); + + // 유효하지 않은 질문이 존재할 경우 + if (!validAnswerMap.containsKey(questionId)) { + throw new CustomException(ErrorResponseCode.INVALID_GROOMING_TEST_QUESTION_ID); + } + + Set validAnswerIds = validAnswerMap.get(questionId); // 해당 질문에 속한 답변 목록 + for (Long answerId : answerIds) { + // 유효하지 않은 답변이 존재할 경우, 각 질문에 속한 답변이 아닐 경우 + if (!validAnswerIds.contains(answerId)) { + throw new CustomException(ErrorResponseCode.INVALID_GROOMING_TEST_ANSWER_ID); + } + } + + // 4. 답변 ID 중복 검사 + if (answerIds.size() != new HashSet<>(answerIds).size()) { + throw new CustomException(ErrorResponseCode.INVALID_GROOMING_TEST_ANSWER_ID); + } + } + } +} diff --git a/src/main/java/com/ftm/server/application/vo/grooming/SubmitGroomingTestVo.java b/src/main/java/com/ftm/server/application/vo/grooming/SubmitGroomingTestVo.java new file mode 100644 index 0000000..dc0cd9b --- /dev/null +++ b/src/main/java/com/ftm/server/application/vo/grooming/SubmitGroomingTestVo.java @@ -0,0 +1,46 @@ +package com.ftm.server.application.vo.grooming; + +import com.ftm.server.application.command.grooming.SaveGroomingTestResultCommand; +import com.ftm.server.application.command.grooming.SubmitGroomingTestCommand; +import java.util.List; +import lombok.Getter; + +@Getter +public class SubmitGroomingTestVo { + + private final Long questionId; + private final List answerIds; + + private SubmitGroomingTestVo(Long questionId, List answerIds) { + this.questionId = questionId; + this.answerIds = answerIds; + } + + public static SubmitGroomingTestVo of(Long questionId, List answerIds) { + return new SubmitGroomingTestVo(questionId, answerIds); + } + + public static List from(SubmitGroomingTestCommand command) { + return command.getSubmissions().stream() + .map( + submission -> + SubmitGroomingTestVo.of( + submission.getQuestionId(), + submission.getAnswers().stream() + .map( + SubmitGroomingTestCommand.SubmittedQuestion + .SelectedAnswer + ::getAnswerId) + .toList())) + .toList(); + } + + public static List from(SaveGroomingTestResultCommand command) { + return command.getResults().stream() + .map( + result -> + SubmitGroomingTestVo.of( + result.getQuestionId(), result.getAnswerIds())) + .toList(); + } +} diff --git a/src/main/java/com/ftm/server/common/response/enums/ErrorResponseCode.java b/src/main/java/com/ftm/server/common/response/enums/ErrorResponseCode.java index 4fbf75e..340beaa 100644 --- a/src/main/java/com/ftm/server/common/response/enums/ErrorResponseCode.java +++ b/src/main/java/com/ftm/server/common/response/enums/ErrorResponseCode.java @@ -13,6 +13,10 @@ public enum ErrorResponseCode { HttpStatus.BAD_REQUEST, "E400_002", "소셜 회원가입을 위한 session 값이 잘못됨"), INVALID_MAX_SCORE(HttpStatus.BAD_REQUEST, "E400_003", "최대 점수 값은 0보다 커야합니다."), INVALID_RATIO_FOR_PERCENTAGE(HttpStatus.BAD_REQUEST, "E400_004", "퍼센트 계산 오류, 잘못된 ratio 값입니다."), + INVALID_GROOMING_TEST_QUESTION_ID( + HttpStatus.BAD_REQUEST, "E400_005", "유효하지 않은 그루밍 테스트 질문 정보입니다."), + INVALID_GROOMING_TEST_ANSWER_ID( + HttpStatus.BAD_REQUEST, "E400_006", "유효하지 않은 그루밍 테스트 답변 정보입니다."), // 401번 NOT_AUTHENTICATED(HttpStatus.UNAUTHORIZED, "E401_001", "인증되지 않은 사용자입니다."), From 79ebb27f69e14eb63a0c0068957fc24b784ee2d1 Mon Sep 17 00:00:00 2001 From: songhyeonpk Date: Fri, 11 Apr 2025 13:59:57 +0900 Subject: [PATCH 3/6] =?UTF-8?q?refactor:=20=EA=B7=B8=EB=A3=A8=EB=B0=8D=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=A0=9C=EC=B6=9C=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../GroomingTestSubmissionRequest.java | 9 +- .../response/GroomingTestResultResponse.java | 2 + .../mapper/GroomingTestCommandMapper.java | 11 +- .../grooming/SubmitGroomingTestCommand.java | 27 +--- .../GroomingTestResultCalculateService.java | 97 ------------ .../grooming/SubmitGroomingTestService.java | 144 +++++++++++------- .../vo/grooming/SubmitGroomingTestVo.java | 8 +- .../grooming/SubmitGroomingTestsTest.java | 119 +++++++++------ 8 files changed, 178 insertions(+), 239 deletions(-) delete mode 100644 src/main/java/com/ftm/server/application/service/grooming/GroomingTestResultCalculateService.java diff --git a/src/main/java/com/ftm/server/adapter/in/web/grooming/dto/request/GroomingTestSubmissionRequest.java b/src/main/java/com/ftm/server/adapter/in/web/grooming/dto/request/GroomingTestSubmissionRequest.java index 53038fe..f66b70d 100644 --- a/src/main/java/com/ftm/server/adapter/in/web/grooming/dto/request/GroomingTestSubmissionRequest.java +++ b/src/main/java/com/ftm/server/adapter/in/web/grooming/dto/request/GroomingTestSubmissionRequest.java @@ -16,13 +16,6 @@ public static class SubmittedQuestion { private Long questionId; private String groomingCategory; - private List answers; - - @Getter - @AllArgsConstructor - public static class SelectedAnswer { - private Long answerId; - private Integer score; - } + private List answerIds; } } diff --git a/src/main/java/com/ftm/server/adapter/in/web/grooming/dto/response/GroomingTestResultResponse.java b/src/main/java/com/ftm/server/adapter/in/web/grooming/dto/response/GroomingTestResultResponse.java index 71decd8..af48080 100644 --- a/src/main/java/com/ftm/server/adapter/in/web/grooming/dto/response/GroomingTestResultResponse.java +++ b/src/main/java/com/ftm/server/adapter/in/web/grooming/dto/response/GroomingTestResultResponse.java @@ -9,11 +9,13 @@ @Getter public class GroomingTestResultResponse { + private final boolean isAuthenticated; private final GroomingTestResultScoresVo scores; private final GroomingTestResultGradesVo grades; private final GroomingLevelVo level; private GroomingTestResultResponse(GroomingTestResultVo result) { + this.isAuthenticated = result.isAuthenticated(); this.scores = result.getScores(); this.grades = result.getGrades(); this.level = result.getLevel(); diff --git a/src/main/java/com/ftm/server/adapter/in/web/grooming/mapper/GroomingTestCommandMapper.java b/src/main/java/com/ftm/server/adapter/in/web/grooming/mapper/GroomingTestCommandMapper.java index a25733b..07b2bbd 100644 --- a/src/main/java/com/ftm/server/adapter/in/web/grooming/mapper/GroomingTestCommandMapper.java +++ b/src/main/java/com/ftm/server/adapter/in/web/grooming/mapper/GroomingTestCommandMapper.java @@ -17,16 +17,7 @@ public static SubmitGroomingTestCommand toSubmitGroomingTestCommand( SubmitGroomingTestCommand.SubmittedQuestion.of( item.getQuestionId(), item.getGroomingCategory(), - item.getAnswers().stream() - .map( - answer -> - SubmitGroomingTestCommand - .SubmittedQuestion - .SelectedAnswer.of( - answer - .getAnswerId(), - answer.getScore())) - .toList())) + item.getAnswerIds())) .toList(); return SubmitGroomingTestCommand.from(userId, submissions); diff --git a/src/main/java/com/ftm/server/application/command/grooming/SubmitGroomingTestCommand.java b/src/main/java/com/ftm/server/application/command/grooming/SubmitGroomingTestCommand.java index 09b7614..97b7b2e 100644 --- a/src/main/java/com/ftm/server/application/command/grooming/SubmitGroomingTestCommand.java +++ b/src/main/java/com/ftm/server/application/command/grooming/SubmitGroomingTestCommand.java @@ -23,34 +23,17 @@ public static class SubmittedQuestion { private final Long questionId; private final String groomingCategory; - private final List answers; + private final List answerIds; - private SubmittedQuestion( - Long questionId, String groomingCategory, List answers) { + private SubmittedQuestion(Long questionId, String groomingCategory, List answerIds) { this.questionId = questionId; this.groomingCategory = groomingCategory; - this.answers = answers; + this.answerIds = answerIds; } public static SubmittedQuestion of( - Long questionId, String groomingCategory, List answers) { - return new SubmittedQuestion(questionId, groomingCategory, answers); - } - - @Getter - public static class SelectedAnswer { - - private final Long answerId; - private final int score; - - private SelectedAnswer(Long answerId, int score) { - this.answerId = answerId; - this.score = score; - } - - public static SelectedAnswer of(Long answerId, int score) { - return new SelectedAnswer(answerId, score); - } + Long questionId, String groomingCategory, List answerIds) { + return new SubmittedQuestion(questionId, groomingCategory, answerIds); } } } diff --git a/src/main/java/com/ftm/server/application/service/grooming/GroomingTestResultCalculateService.java b/src/main/java/com/ftm/server/application/service/grooming/GroomingTestResultCalculateService.java deleted file mode 100644 index 617311f..0000000 --- a/src/main/java/com/ftm/server/application/service/grooming/GroomingTestResultCalculateService.java +++ /dev/null @@ -1,97 +0,0 @@ -package com.ftm.server.application.service.grooming; - -import static com.ftm.server.common.consts.StaticConsts.*; - -import com.ftm.server.application.command.grooming.SubmitGroomingTestCommand; -import com.ftm.server.application.port.out.persistence.grooming.LoadGroomingLevelPort; -import com.ftm.server.application.query.FIndGroomingLevelByScoreQuery; -import com.ftm.server.application.vo.grooming.*; -import com.ftm.server.common.exception.CustomException; -import com.ftm.server.common.response.enums.ErrorResponseCode; -import com.ftm.server.domain.entity.GroomingLevel; -import com.ftm.server.domain.enums.GroomingCategory; -import com.ftm.server.domain.enums.GroomingCategoryGrade; -import java.util.List; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class GroomingTestResultCalculateService { - - private final LoadGroomingLevelPort loadGroomingLevelPort; - - public GroomingTestResultVo process(SubmitGroomingTestCommand command) { - // 그루밍 테스트 결과 점수 계산 - GroomingTestResultScoresVo scores = calculateScores(command.getSubmissions()); - - // 그루밍 테스트 결과 등급 계산 - GroomingTestResultGradesVo grades = calculateGrades(scores); - - // 그루밍 레벨 조회 - GroomingLevel groomingLevel = - loadGroomingLevelPort - .loadGroomingLevelByScore( - FIndGroomingLevelByScoreQuery.of(scores.getTotalScore())) - .orElseThrow( - () -> - new CustomException( - ErrorResponseCode.GROOMING_LEVEL_NOT_FOUND)); - GroomingLevelVo level = GroomingLevelVo.from(groomingLevel); - - return GroomingTestResultVo.from(command.getUserId(), scores, grades, level); - } - - // 점수 계산 - private GroomingTestResultScoresVo calculateScores( - List submissions) { - // 그루밍 카테고리별 점수 계산 - int beautyScore = 0; - int hygieneScore = 0; - int hairScore = 0; - int workoutScore = 0; - int fashionScore = 0; - - for (SubmitGroomingTestCommand.SubmittedQuestion result : submissions) { - GroomingCategory category = GroomingCategory.from(result.getGroomingCategory()); - switch (category) { - case BEAUTY -> beautyScore += sum(result); - case HYGIENE -> hygieneScore += sum(result); - case HAIR -> hairScore += sum(result); - case WORKOUT -> workoutScore += sum(result); - case FASHION -> fashionScore += sum(result); - } - } - - int totalScore = beautyScore + hygieneScore + hairScore + workoutScore + fashionScore; - - return GroomingTestResultScoresVo.of( - beautyScore, hygieneScore, hairScore, workoutScore, fashionScore, totalScore); - } - - private int sum(SubmitGroomingTestCommand.SubmittedQuestion submissions) { - return submissions.getAnswers().stream() - .mapToInt(SubmitGroomingTestCommand.SubmittedQuestion.SelectedAnswer::getScore) - .sum(); - } - - // 등급 계산 - private GroomingTestResultGradesVo calculateGrades(GroomingTestResultScoresVo scores) { - return GroomingTestResultGradesVo.from( - GroomingCategoryGradeInfoVo.from( - GroomingCategoryGrade.fromScore( - scores.getBeautyScore(), BEAUTY_CATEGORY_MAX_SCORE)), - GroomingCategoryGradeInfoVo.from( - GroomingCategoryGrade.fromScore( - scores.getHygieneScore(), HYGIENE_CATEGORY_MAX_SCORE)), - GroomingCategoryGradeInfoVo.from( - GroomingCategoryGrade.fromScore( - scores.getHairScore(), HAIR_CATEGORY_MAX_SCORE)), - GroomingCategoryGradeInfoVo.from( - GroomingCategoryGrade.fromScore( - scores.getWorkoutScore(), WORKOUT_CATEGORY_MAX_SCORE)), - GroomingCategoryGradeInfoVo.from( - GroomingCategoryGrade.fromScore( - scores.getFashionScore(), FASHION_CATEGORY_MAX_SCORE))); - } -} diff --git a/src/main/java/com/ftm/server/application/service/grooming/SubmitGroomingTestService.java b/src/main/java/com/ftm/server/application/service/grooming/SubmitGroomingTestService.java index 12f0a0b..971b7d8 100644 --- a/src/main/java/com/ftm/server/application/service/grooming/SubmitGroomingTestService.java +++ b/src/main/java/com/ftm/server/application/service/grooming/SubmitGroomingTestService.java @@ -4,80 +4,122 @@ import com.ftm.server.application.command.grooming.SubmitGroomingTestCommand; import com.ftm.server.application.port.in.grooming.SubmitGroomingTestUseCase; +import com.ftm.server.application.port.out.cache.LoadGroomingTestsWithCachePort; import com.ftm.server.application.port.out.persistence.grooming.LoadGroomingLevelPort; -import com.ftm.server.application.port.out.persistence.grooming.LoadUserForGroomingPort; -import com.ftm.server.application.port.out.persistence.grooming.SaveGroomingTestResultPort; -import com.ftm.server.application.port.out.persistence.grooming.UpdateUserForGroomingPort; -import com.ftm.server.application.query.FindByIdQuery; +import com.ftm.server.application.query.FIndGroomingLevelByScoreQuery; import com.ftm.server.application.vo.grooming.*; import com.ftm.server.common.exception.CustomException; -import com.ftm.server.domain.entity.GroomingTestResult; -import com.ftm.server.domain.entity.User; -import java.time.LocalDateTime; +import com.ftm.server.common.response.enums.ErrorResponseCode; +import com.ftm.server.domain.entity.GroomingLevel; +import com.ftm.server.domain.enums.GroomingCategory; +import com.ftm.server.domain.enums.GroomingCategoryGrade; +import java.util.EnumMap; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor public class SubmitGroomingTestService implements SubmitGroomingTestUseCase { - private final GroomingTestResultCalculateService groomingTestResultCalculateService; private final LoadGroomingLevelPort loadGroomingLevelPort; - private final LoadUserForGroomingPort loadUserForGroomingPort; - private final UpdateUserForGroomingPort updateUserForGroomingPort; - private final SaveGroomingTestResultPort saveGroomingTestResultPort; + private final LoadGroomingTestsWithCachePort loadGroomingTestsWithCachePort; + + private final GroomingTestValidator groomingTestValidator; @Override + @Transactional(readOnly = true) public GroomingTestResultVo execute(SubmitGroomingTestCommand command) { - // 그루밍 테스트 결과 - GroomingTestResultVo results = groomingTestResultCalculateService.process(command); + // 그루밍 테스트 유효성 검증 + List submissions = SubmitGroomingTestVo.from(command); + groomingTestValidator.execute(submissions); - // 비로그인 유저인 경우 - if (command.getUserId() == null) { - return results; - } + // 그루밍 테스트 계산 (점수, 등급, 레벨) + GroomingTestResultScoresVo scores = calculateScores(command); + GroomingTestResultGradesVo grades = calculateGrades(scores); + GroomingLevel groomingLevel = + loadGroomingLevelPort + .loadGroomingLevelByScore( + FIndGroomingLevelByScoreQuery.of(scores.getTotalScore())) + .orElseThrow( + () -> + new CustomException( + ErrorResponseCode.GROOMING_LEVEL_NOT_FOUND)); + GroomingLevelVo level = GroomingLevelVo.from(groomingLevel); - User user = - loadUserForGroomingPort - .loadUser(FindByIdQuery.of(command.getUserId())) - .orElseThrow(() -> CustomException.USER_NOT_FOUND); + return GroomingTestResultVo.from(command.getUserId(), scores, grades, level); + } - // 유저 그루밍 레벨 업데이트 - updateUserGroomingInfo(user, results.getScores(), results.getLevel()); + // 점수 계산 + private GroomingTestResultScoresVo calculateScores(SubmitGroomingTestCommand command) { + // 캐싱된 그루밍 테스트 목록 조회 + List infos = + loadGroomingTestsWithCachePort.loadGroomingTestsCache(); - // 유저 그루밍 테스트 결과 등록 - saveUserGroomingTestResult(user.getId(), command.getSubmissions(), results); + // 각 질문별 답변:점수 정보를 관리하는 map + Map> scoreMap = toScoreMap(infos); - return results; - } + // 그루밍 카테고리별 점수를 관리하는 map + Map categoryScores = new EnumMap<>(GroomingCategory.class); + + // 그루밍 카테고리별 점수 계산 + for (SubmitGroomingTestCommand.SubmittedQuestion sub : command.getSubmissions()) { + GroomingCategory category = GroomingCategory.from(sub.getGroomingCategory()); + Map answersMap = scoreMap.get(sub.getQuestionId()); - private void updateUserGroomingInfo( - User user, GroomingTestResultScoresVo scores, GroomingLevelVo level) { - user.updateGroomingInfo(scores.getTotalScore(), level.getGroomingLevelId()); + int score = + sub.getAnswerIds().stream() + .mapToInt(ans -> answersMap.getOrDefault(ans, 0)) + .sum(); + + categoryScores.merge(category, score, Integer::sum); + } + + return GroomingTestResultScoresVo.of( + categoryScores.getOrDefault(GroomingCategory.BEAUTY, 0), + categoryScores.getOrDefault(GroomingCategory.HYGIENE, 0), + categoryScores.getOrDefault(GroomingCategory.HAIR, 0), + categoryScores.getOrDefault(GroomingCategory.WORKOUT, 0), + categoryScores.getOrDefault(GroomingCategory.FASHION, 0), + categoryScores.values().stream().mapToInt(Integer::intValue).sum()); + } - updateUserForGroomingPort.updateUserGroomingStatus(user); + private Map> toScoreMap( + List infos) { + return infos.stream() + .collect( + Collectors.toMap( + GroomingTestQuestionWithAnswersVo::getGroomingTestQuestionId, + question -> + question.getAnswers().stream() + .collect( + Collectors.toMap( + GroomingTestAnswerInfoVo + ::getGroomingTestAnswerId, + GroomingTestAnswerInfoVo + ::getScore)))); } - private void saveUserGroomingTestResult( - Long userId, - List submissions, - GroomingTestResultVo results) { - // 그루밍 테스트 결과 등록 - LocalDateTime testedAt = LocalDateTime.now(); - List groomingTestResults = - submissions.stream() - .flatMap( - result -> - result.getAnswers().stream() - .map( - answerInfo -> - GroomingTestResult.create( - userId, - result.getQuestionId(), - answerInfo.getAnswerId(), - testedAt))) - .toList(); - saveGroomingTestResultPort.saveGroomingTestResults(groomingTestResults); + // 등급 계산 + private GroomingTestResultGradesVo calculateGrades(GroomingTestResultScoresVo scores) { + return GroomingTestResultGradesVo.from( + GroomingCategoryGradeInfoVo.from( + GroomingCategoryGrade.fromScore( + scores.getBeautyScore(), BEAUTY_CATEGORY_MAX_SCORE)), + GroomingCategoryGradeInfoVo.from( + GroomingCategoryGrade.fromScore( + scores.getHygieneScore(), HYGIENE_CATEGORY_MAX_SCORE)), + GroomingCategoryGradeInfoVo.from( + GroomingCategoryGrade.fromScore( + scores.getHairScore(), HAIR_CATEGORY_MAX_SCORE)), + GroomingCategoryGradeInfoVo.from( + GroomingCategoryGrade.fromScore( + scores.getWorkoutScore(), WORKOUT_CATEGORY_MAX_SCORE)), + GroomingCategoryGradeInfoVo.from( + GroomingCategoryGrade.fromScore( + scores.getFashionScore(), FASHION_CATEGORY_MAX_SCORE))); } } diff --git a/src/main/java/com/ftm/server/application/vo/grooming/SubmitGroomingTestVo.java b/src/main/java/com/ftm/server/application/vo/grooming/SubmitGroomingTestVo.java index dc0cd9b..4c5a618 100644 --- a/src/main/java/com/ftm/server/application/vo/grooming/SubmitGroomingTestVo.java +++ b/src/main/java/com/ftm/server/application/vo/grooming/SubmitGroomingTestVo.java @@ -25,13 +25,7 @@ public static List from(SubmitGroomingTestCommand command) .map( submission -> SubmitGroomingTestVo.of( - submission.getQuestionId(), - submission.getAnswers().stream() - .map( - SubmitGroomingTestCommand.SubmittedQuestion - .SelectedAnswer - ::getAnswerId) - .toList())) + submission.getQuestionId(), submission.getAnswerIds())) .toList(); } diff --git a/src/test/java/com/ftm/server/grooming/SubmitGroomingTestsTest.java b/src/test/java/com/ftm/server/grooming/SubmitGroomingTestsTest.java index 2b7edfe..4c6bd7d 100644 --- a/src/test/java/com/ftm/server/grooming/SubmitGroomingTestsTest.java +++ b/src/test/java/com/ftm/server/grooming/SubmitGroomingTestsTest.java @@ -19,6 +19,7 @@ import com.ftm.server.application.port.out.persistence.user.SaveUserImagePort; import com.ftm.server.application.port.out.persistence.user.SaveUserPort; import com.ftm.server.application.port.out.security.SecurityAuthenticationPort; +import com.ftm.server.common.response.enums.ErrorResponseCode; import com.ftm.server.domain.entity.User; import com.ftm.server.domain.entity.UserImage; import java.util.List; @@ -45,15 +46,9 @@ public class SubmitGroomingTestsTest extends BaseTest { fieldWithPath("submissions[].groomingCategory") .type(STRING) .description("질문 그루밍 카테고리"), - fieldWithPath("submissions[].answers[]") + fieldWithPath("submissions[].answerIds[]") .type(ARRAY) - .description("질문에 답한 답변 목록"), - fieldWithPath("submissions[].answers[].answerId") - .type(NUMBER) - .description("답변 ID"), - fieldWithPath("submissions[].answers[].score") - .type(NUMBER) - .description("답변 점수")); + .description("질문에 답한 답변 ID 목록")); private final List responseFieldSubmitGroomingTests = List.of( @@ -61,6 +56,7 @@ public class SubmitGroomingTestsTest extends BaseTest { fieldWithPath("code").type(STRING).description("상태 코드"), fieldWithPath("message").type(STRING).description("메시지"), fieldWithPath("data").type(OBJECT).optional().description("응답 데이터"), + fieldWithPath("data.authenticated").type(BOOLEAN).description("인증 여부"), fieldWithPath("data.scores").type(OBJECT).description("그루밍 테스트 카테고리 별 결과 점수"), fieldWithPath("data.scores.beautyScore").type(NUMBER).description("뷰티 영역 점수"), fieldWithPath("data.scores.hygieneScore").type(NUMBER).description("위생 영역 점수"), @@ -117,45 +113,15 @@ private GroomingTestSubmissionRequest getRequest() { List submissions = List.of( new GroomingTestSubmissionRequest.SubmittedQuestion( - 1L, - "BEAUTY", - List.of( - new GroomingTestSubmissionRequest.SubmittedQuestion - .SelectedAnswer(1L, 1), - new GroomingTestSubmissionRequest.SubmittedQuestion - .SelectedAnswer(2L, 1), - new GroomingTestSubmissionRequest.SubmittedQuestion - .SelectedAnswer(3L, 2))), + 1L, "BEAUTY", List.of(1L, 2L, 3L)), new GroomingTestSubmissionRequest.SubmittedQuestion( - 6L, - "HYGIENE", - List.of( - new GroomingTestSubmissionRequest.SubmittedQuestion - .SelectedAnswer(16L, 1))), + 6L, "HYGIENE", List.of(16L)), new GroomingTestSubmissionRequest.SubmittedQuestion( - 10L, - "HAIR", - List.of( - new GroomingTestSubmissionRequest.SubmittedQuestion - .SelectedAnswer(28L, 1))), + 10L, "HAIR", List.of(28L)), new GroomingTestSubmissionRequest.SubmittedQuestion( - 15L, - "WORKOUT", - List.of( - new GroomingTestSubmissionRequest.SubmittedQuestion - .SelectedAnswer(41L, 1), - new GroomingTestSubmissionRequest.SubmittedQuestion - .SelectedAnswer(42L, 1), - new GroomingTestSubmissionRequest.SubmittedQuestion - .SelectedAnswer(43L, 1), - new GroomingTestSubmissionRequest.SubmittedQuestion - .SelectedAnswer(44L, 1))), + 15L, "WORKOUT", List.of(41L, 42L, 43L, 44L)), new GroomingTestSubmissionRequest.SubmittedQuestion( - 18L, - "FASHION", - List.of( - new GroomingTestSubmissionRequest.SubmittedQuestion - .SelectedAnswer(61L, 4)))); + 18L, "FASHION", List.of(61L))); return new GroomingTestSubmissionRequest(submissions); } @@ -219,7 +185,6 @@ private RestDocumentationResultHandler getDocument(Integer identifier) { @Test @Transactional void 그루밍_테스트_제출_성공2() throws Exception { - // given // given GroomingTestSubmissionRequest request = getRequest(); @@ -236,4 +201,70 @@ private RestDocumentationResultHandler getDocument(Integer identifier) { // documentation resultActions.andDo(getDocument(2)); } + + @Test + @Transactional + void 그루밍_테스트_제출_실패1() throws Exception { + // given + List submissions = + List.of( + new GroomingTestSubmissionRequest.SubmittedQuestion( + 1000L, "BEAUTY", List.of(1L))); + GroomingTestSubmissionRequest request = new GroomingTestSubmissionRequest(submissions); + + // when + ResultActions resultActions = + mockMvc.perform( + RestDocumentationRequestBuilders.post("/api/grooming/tests/submission") + .contentType(APPLICATION_JSON_VALUE) + .content(mapper.writeValueAsString(request))); + + resultActions + .andExpect( + status().is( + ErrorResponseCode.INVALID_GROOMING_TEST_QUESTION_ID + .getHttpStatus() + .value())) + .andExpect( + jsonPath("code") + .value( + ErrorResponseCode.INVALID_GROOMING_TEST_QUESTION_ID + .getCode())) + .andDo(print()); + + // documentation + resultActions.andDo(getDocument(3)); + } + + @Test + @Transactional + void 그루밍_테스트_제출_실패2() throws Exception { + // given + List submissions = + List.of( + new GroomingTestSubmissionRequest.SubmittedQuestion( + 1L, "BEAUTY", List.of(1000L))); + GroomingTestSubmissionRequest request = new GroomingTestSubmissionRequest(submissions); + + // when + ResultActions resultActions = + mockMvc.perform( + RestDocumentationRequestBuilders.post("/api/grooming/tests/submission") + .contentType(APPLICATION_JSON_VALUE) + .content(mapper.writeValueAsString(request))); + + resultActions + .andExpect( + status().is( + ErrorResponseCode.INVALID_GROOMING_TEST_ANSWER_ID + .getHttpStatus() + .value())) + .andExpect( + jsonPath("code") + .value(ErrorResponseCode.INVALID_GROOMING_TEST_ANSWER_ID.getCode())) + .andDo(print()); + + // documentation + resultActions.andDo(getDocument(4)); + } } From 315112dd6da67648e61e59a7672a6caf8a69a1ad Mon Sep 17 00:00:00 2001 From: songhyeonpk Date: Fri, 11 Apr 2025 14:00:25 +0900 Subject: [PATCH 4/6] =?UTF-8?q?refactor:=20=EA=B7=B8=EB=A3=A8=EB=B0=8D=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EA=B2=B0=EA=B3=BC=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=20=EA=B8=B0=EB=8A=A5=20=EB=A6=AC=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SaveGroomingTestResultService.java | 7 +++ .../grooming/SaveGroomingTestResultTest.java | 62 +++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/src/main/java/com/ftm/server/application/service/grooming/SaveGroomingTestResultService.java b/src/main/java/com/ftm/server/application/service/grooming/SaveGroomingTestResultService.java index d0f0d98..5b9b720 100644 --- a/src/main/java/com/ftm/server/application/service/grooming/SaveGroomingTestResultService.java +++ b/src/main/java/com/ftm/server/application/service/grooming/SaveGroomingTestResultService.java @@ -6,6 +6,7 @@ import com.ftm.server.application.port.out.persistence.grooming.SaveGroomingTestResultPort; import com.ftm.server.application.port.out.persistence.grooming.UpdateUserForGroomingPort; import com.ftm.server.application.query.FindByIdQuery; +import com.ftm.server.application.vo.grooming.SubmitGroomingTestVo; import com.ftm.server.common.exception.CustomException; import com.ftm.server.domain.entity.GroomingTestResult; import com.ftm.server.domain.entity.User; @@ -23,9 +24,15 @@ public class SaveGroomingTestResultService implements SaveGroomingTestResultUseC private final UpdateUserForGroomingPort updateUserForGroomingPort; private final LoadUserForGroomingPort loadUserForGroomingPort; + private final GroomingTestValidator groomingTestValidator; + @Override @Transactional public void execute(SaveGroomingTestResultCommand command) { + // 그루밍 테스트 유효성 검증 + List submissions = SubmitGroomingTestVo.from(command); + groomingTestValidator.execute(submissions); + User user = loadUserForGroomingPort .loadUser(FindByIdQuery.of(command.getUserId())) diff --git a/src/test/java/com/ftm/server/grooming/SaveGroomingTestResultTest.java b/src/test/java/com/ftm/server/grooming/SaveGroomingTestResultTest.java index b096619..2930226 100644 --- a/src/test/java/com/ftm/server/grooming/SaveGroomingTestResultTest.java +++ b/src/test/java/com/ftm/server/grooming/SaveGroomingTestResultTest.java @@ -165,4 +165,66 @@ void setUp() { // documentation resultActions.andDo(getDocument(3)); } + + @Test + @Transactional + void 그루밍_테스트_결과_저장_실패3() throws Exception { + // given + User user = + loadUserForAuthPort.loadUserByEmail(FindByEmailQuery.of("test@gmail.com")).get(); + List results = + List.of(new SaveGroomingTestResultRequest.GroomingTestResult(1000L, List.of())); + SaveGroomingTestResultRequest request = + new SaveGroomingTestResultRequest(user.getId(), 1L, 10, results); + + // when + ResultActions resultActions = getResultActions(request); + + // then + resultActions + .andExpect( + status().is( + ErrorResponseCode.INVALID_GROOMING_TEST_QUESTION_ID + .getHttpStatus() + .value())) + .andExpect( + jsonPath("code") + .value( + ErrorResponseCode.INVALID_GROOMING_TEST_QUESTION_ID + .getCode())) + .andDo(print()); + + // documentation + resultActions.andDo(getDocument(4)); + } + + @Test + @Transactional + void 그루밍_테스트_결과_저장_실패4() throws Exception { + // given + User user = + loadUserForAuthPort.loadUserByEmail(FindByEmailQuery.of("test@gmail.com")).get(); + List results = + List.of(new SaveGroomingTestResultRequest.GroomingTestResult(1L, List.of(100L))); + SaveGroomingTestResultRequest request = + new SaveGroomingTestResultRequest(user.getId(), 1L, 10, results); + + // when + ResultActions resultActions = getResultActions(request); + + // then + resultActions + .andExpect( + status().is( + ErrorResponseCode.INVALID_GROOMING_TEST_ANSWER_ID + .getHttpStatus() + .value())) + .andExpect( + jsonPath("code") + .value(ErrorResponseCode.INVALID_GROOMING_TEST_ANSWER_ID.getCode())) + .andDo(print()); + + // documentation + resultActions.andDo(getDocument(5)); + } } From a425eaf936345208f65e95bc0bb0eaeff2a26b3c Mon Sep 17 00:00:00 2001 From: songhyeonpk Date: Fri, 11 Apr 2025 16:53:35 +0900 Subject: [PATCH 5/6] =?UTF-8?q?refactor:=20=EA=B7=B8=EB=A3=A8=EB=B0=8D=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EA=B0=80=EB=8A=A5=20=EC=97=AC?= =?UTF-8?q?=EB=B6=80=20=EA=B8=B0=EB=8A=A5=20=EB=A6=AC=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/grooming/CheckGroomingTestAvailabilityService.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/com/ftm/server/application/service/grooming/CheckGroomingTestAvailabilityService.java b/src/main/java/com/ftm/server/application/service/grooming/CheckGroomingTestAvailabilityService.java index e1fc17e..8e89dc6 100644 --- a/src/main/java/com/ftm/server/application/service/grooming/CheckGroomingTestAvailabilityService.java +++ b/src/main/java/com/ftm/server/application/service/grooming/CheckGroomingTestAvailabilityService.java @@ -12,6 +12,7 @@ import java.time.temporal.ChronoUnit; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor @@ -20,6 +21,7 @@ public class CheckGroomingTestAvailabilityService implements CheckGroomingTestAv private final LoadGroomingTestResultPort loadGroomingTestResultPort; @Override + @Transactional(readOnly = true) public GroomingTestAvailabilityVo execute(FindByUserIdQuery query) { // 가장 최근 테스트 날짜 조회 LocalDateTime lastTestDateTime = From 6f7c62118638390bfdf9d919b4b264703428bff6 Mon Sep 17 00:00:00 2001 From: songhyeonpk Date: Fri, 11 Apr 2025 16:53:41 +0900 Subject: [PATCH 6/6] =?UTF-8?q?docs:=20=EA=B7=B8=EB=A3=A8=EB=B0=8D=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EA=B4=80=EB=A0=A8=20=EB=AC=B8?= =?UTF-8?q?=EC=84=9C=ED=99=94=20=EB=82=B4=EC=9A=A9=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/docs/asciidoc/grooming-test-api.adoc | 26 +++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/docs/asciidoc/grooming-test-api.adoc b/src/docs/asciidoc/grooming-test-api.adoc index 6ee128a..b33f6ef 100644 --- a/src/docs/asciidoc/grooming-test-api.adoc +++ b/src/docs/asciidoc/grooming-test-api.adoc @@ -21,11 +21,24 @@ include::{snippetsDir}/submitGroomingTests/1/http-request.adoc[] include::{snippetsDir}/submitGroomingTests/1/request-fields.adoc[] ==== 성공 Response +성공 1. 인증된 유저인 경우 include::{snippetsDir}/submitGroomingTests/1/http-response.adoc[] +성공 2. 인증되지 않은 유저인 경우 +include::{snippetsDir}/submitGroomingTests/2/http-response.adoc[] + ==== Response Body Fields include::{snippetsDir}/submitGroomingTests/1/response-fields.adoc[] +==== 실패 Response +실패 3. 유효하지 않은 그루밍 테스트 질문 ID일 경우 (존재하지 않는 질문 ID, 중복된 질문 ID) + +include::{snippetsDir}/submitGroomingTests/3/http-response.adoc[] + +실패 4. 유효하지 않은 그루밍 테스트 답변 ID일 경우 (존재하지 않는 답변 ID, 질문과 잘못 매핑된 답변 ID, 중복된 답변 ID) + +include::{snippetsDir}/submitGroomingTests/4/http-response.adoc[] + --- === **3. 그루밍 테스트 가능 여부 조회** @@ -58,7 +71,7 @@ include::{snippetsDir}/checkGroomingTestAvailability/4/http-response.adoc[] === **4. 그루밍 테스트 결과 저장** -그루밍 테스트 결과를 저장하는 api 입니다. (비로그인 그루밍 테스트 이후 회원가입 할 때) +그루밍 테스트 결과를 저장하는 api 입니다. ==== Request include::{snippetsDir}/saveGroomingTestResult/1/http-request.adoc[] @@ -78,4 +91,15 @@ include::{snippetsDir}/saveGroomingTestResult/1/response-fields.adoc[] include::{snippetsDir}/saveGroomingTestResult/2/http-response.adoc[] 실패 2. 존재하지 않는 그루밍 레벨인 경우 + include::{snippetsDir}/saveGroomingTestResult/3/http-response.adoc[] + +실패 3. 유효하지 않은 그루밍 테스트 질문 ID일 경우 (존재하지 않는 질문 ID, 중복된 질문 ID) + +include::{snippetsDir}/saveGroomingTestResult/4/http-response.adoc[] + +실패 4. 유효하지 않은 그루밍 테스트 답변 ID일 경우 (존재하지 않는 답변 ID, 질문과 잘못 매핑된 답변 ID, 중복된 답변 ID) + +include::{snippetsDir}/saveGroomingTestResult/5/http-response.adoc[] + +---