diff --git a/src/docs/asciidoc/grooming-test-api.adoc b/src/docs/asciidoc/grooming-test-api.adoc index 15747b4..6ee128a 100644 --- a/src/docs/asciidoc/grooming-test-api.adoc +++ b/src/docs/asciidoc/grooming-test-api.adoc @@ -17,6 +17,9 @@ include::{snippetsDir}/loadGroomingTests/1/response-fields.adoc[] ==== Request include::{snippetsDir}/submitGroomingTests/1/http-request.adoc[] +==== Request Body Fields +include::{snippetsDir}/submitGroomingTests/1/request-fields.adoc[] + ==== 성공 Response include::{snippetsDir}/submitGroomingTests/1/http-response.adoc[] @@ -49,4 +52,30 @@ include::{snippetsDir}/checkGroomingTestAvailability/3/http-response.adoc[] include::{snippetsDir}/checkGroomingTestAvailability/1/response-fields.adoc[] ==== 실패 Response -include::{snippetsDir}/checkGroomingTestAvailability/4/http-response.adoc[] \ No newline at end of file +include::{snippetsDir}/checkGroomingTestAvailability/4/http-response.adoc[] + +--- + +=== **4. 그루밍 테스트 결과 저장** + +그루밍 테스트 결과를 저장하는 api 입니다. (비로그인 그루밍 테스트 이후 회원가입 할 때) + +==== Request +include::{snippetsDir}/saveGroomingTestResult/1/http-request.adoc[] + +==== Request Body Fields +include::{snippetsDir}/saveGroomingTestResult/1/request-fields.adoc[] + +==== 성공 Response +include::{snippetsDir}/saveGroomingTestResult/1/http-response.adoc[] + +==== Response Body Fields +include::{snippetsDir}/saveGroomingTestResult/1/response-fields.adoc[] + +==== 실패 Response +실패 1. 존재하지 않는 유저인 경우 + +include::{snippetsDir}/saveGroomingTestResult/2/http-response.adoc[] + +실패 2. 존재하지 않는 그루밍 레벨인 경우 +include::{snippetsDir}/saveGroomingTestResult/3/http-response.adoc[] diff --git a/src/main/java/com/ftm/server/adapter/in/web/grooming/controller/SaveGroomingTestResultController.java b/src/main/java/com/ftm/server/adapter/in/web/grooming/controller/SaveGroomingTestResultController.java new file mode 100644 index 0000000..cd43e46 --- /dev/null +++ b/src/main/java/com/ftm/server/adapter/in/web/grooming/controller/SaveGroomingTestResultController.java @@ -0,0 +1,29 @@ +package com.ftm.server.adapter.in.web.grooming.controller; + +import com.ftm.server.adapter.in.web.grooming.dto.request.SaveGroomingTestResultRequest; +import com.ftm.server.application.command.grooming.SaveGroomingTestResultCommand; +import com.ftm.server.application.port.in.grooming.SaveGroomingTestResultUseCase; +import com.ftm.server.common.response.ApiResponse; +import com.ftm.server.common.response.enums.SuccessResponseCode; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +public class SaveGroomingTestResultController { + + private final SaveGroomingTestResultUseCase saveGroomingTestResultUseCase; + + @PostMapping("/api/grooming/tests") + public ResponseEntity> saveGroomingTestResult( + @RequestBody SaveGroomingTestResultRequest request) { + saveGroomingTestResultUseCase.execute(SaveGroomingTestResultCommand.from(request)); + + return ResponseEntity.status(HttpStatus.CREATED) + .body(ApiResponse.success(SuccessResponseCode.CREATED)); + } +} diff --git a/src/main/java/com/ftm/server/adapter/in/web/grooming/dto/request/SaveGroomingTestResultRequest.java b/src/main/java/com/ftm/server/adapter/in/web/grooming/dto/request/SaveGroomingTestResultRequest.java new file mode 100644 index 0000000..7b10227 --- /dev/null +++ b/src/main/java/com/ftm/server/adapter/in/web/grooming/dto/request/SaveGroomingTestResultRequest.java @@ -0,0 +1,23 @@ +package com.ftm.server.adapter.in.web.grooming.dto.request; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class SaveGroomingTestResultRequest { + + private Long userId; + private Long groomingLevelId; + private Integer totalScore; + private List results; + + @Getter + @AllArgsConstructor + public static class GroomingTestResult { + + private Long questionId; + private List answerIds; + } +} diff --git a/src/main/java/com/ftm/server/application/command/grooming/SaveGroomingTestResultCommand.java b/src/main/java/com/ftm/server/application/command/grooming/SaveGroomingTestResultCommand.java new file mode 100644 index 0000000..1e18f8d --- /dev/null +++ b/src/main/java/com/ftm/server/application/command/grooming/SaveGroomingTestResultCommand.java @@ -0,0 +1,46 @@ +package com.ftm.server.application.command.grooming; + +import com.ftm.server.adapter.in.web.grooming.dto.request.SaveGroomingTestResultRequest; +import java.util.List; +import lombok.Getter; + +@Getter +public class SaveGroomingTestResultCommand { + + private final Long userId; + private final Long groomingLevelId; + private final Integer totalScore; + private final List results; + + @Getter + public static class GroomingTestResult { + private final Long questionId; + private final List answerIds; + + private GroomingTestResult(Long questionId, List answerIds) { + this.questionId = questionId; + this.answerIds = answerIds; + } + + public static GroomingTestResult of(Long questionId, List answerIds) { + return new GroomingTestResult(questionId, answerIds); + } + } + + private SaveGroomingTestResultCommand(SaveGroomingTestResultRequest request) { + this.userId = request.getUserId(); + this.groomingLevelId = request.getGroomingLevelId(); + this.totalScore = request.getTotalScore(); + this.results = + request.getResults().stream() + .map( + result -> + GroomingTestResult.of( + result.getQuestionId(), result.getAnswerIds())) + .toList(); + } + + public static SaveGroomingTestResultCommand from(SaveGroomingTestResultRequest request) { + return new SaveGroomingTestResultCommand(request); + } +} diff --git a/src/main/java/com/ftm/server/application/port/in/grooming/SaveGroomingTestResultUseCase.java b/src/main/java/com/ftm/server/application/port/in/grooming/SaveGroomingTestResultUseCase.java new file mode 100644 index 0000000..87ae287 --- /dev/null +++ b/src/main/java/com/ftm/server/application/port/in/grooming/SaveGroomingTestResultUseCase.java @@ -0,0 +1,10 @@ +package com.ftm.server.application.port.in.grooming; + +import com.ftm.server.application.command.grooming.SaveGroomingTestResultCommand; +import com.ftm.server.common.annotation.UseCase; + +@UseCase +public interface SaveGroomingTestResultUseCase { + + void execute(SaveGroomingTestResultCommand command); +} 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 new file mode 100644 index 0000000..d0f0d98 --- /dev/null +++ b/src/main/java/com/ftm/server/application/service/grooming/SaveGroomingTestResultService.java @@ -0,0 +1,65 @@ +package com.ftm.server.application.service.grooming; + +import com.ftm.server.application.command.grooming.SaveGroomingTestResultCommand; +import com.ftm.server.application.port.in.grooming.SaveGroomingTestResultUseCase; +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.common.exception.CustomException; +import com.ftm.server.domain.entity.GroomingTestResult; +import com.ftm.server.domain.entity.User; +import java.time.LocalDateTime; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class SaveGroomingTestResultService implements SaveGroomingTestResultUseCase { + + private final SaveGroomingTestResultPort saveGroomingTestResultPort; + private final UpdateUserForGroomingPort updateUserForGroomingPort; + private final LoadUserForGroomingPort loadUserForGroomingPort; + + @Override + @Transactional + public void execute(SaveGroomingTestResultCommand command) { + User user = + loadUserForGroomingPort + .loadUser(FindByIdQuery.of(command.getUserId())) + .orElseThrow(() -> CustomException.USER_NOT_FOUND); + + // 그루밍 테스트 결과 저장 + saveUserGroomingTestResult(command); + + // 유저 그루밍 정보 업데이트 + updateUserGroomingInfo(user, command); + } + + private void saveUserGroomingTestResult(SaveGroomingTestResultCommand command) { + // 그루밍 테스트 결과 등록 + LocalDateTime testedAt = LocalDateTime.now(); + List groomingTestResults = + command.getResults().stream() + .flatMap( + result -> + result.getAnswerIds().stream() + .map( + answerId -> + GroomingTestResult.create( + command.getUserId(), + result.getQuestionId(), + answerId, + testedAt))) + .toList(); + saveGroomingTestResultPort.saveGroomingTestResults(groomingTestResults); + } + + private void updateUserGroomingInfo(User user, SaveGroomingTestResultCommand command) { + user.updateGroomingInfo(command.getTotalScore(), command.getGroomingLevelId()); + + updateUserForGroomingPort.updateUserGroomingStatus(user); + } +} diff --git a/src/main/java/com/ftm/server/infrastructure/security/SecurityConfig.java b/src/main/java/com/ftm/server/infrastructure/security/SecurityConfig.java index 3ba5720..8d7b643 100644 --- a/src/main/java/com/ftm/server/infrastructure/security/SecurityConfig.java +++ b/src/main/java/com/ftm/server/infrastructure/security/SecurityConfig.java @@ -60,7 +60,8 @@ public class SecurityConfig { "/api/auth/login/**", "/api/users", "/api/users/social", - "/api/grooming/tests/submission" + "/api/grooming/tests/submission", + "/api/grooming/tests" }; private static final String[] ANONYMOUS_MATCHERS = {"/docs/**"}; diff --git a/src/test/java/com/ftm/server/grooming/SaveGroomingTestResultTest.java b/src/test/java/com/ftm/server/grooming/SaveGroomingTestResultTest.java new file mode 100644 index 0000000..b096619 --- /dev/null +++ b/src/test/java/com/ftm/server/grooming/SaveGroomingTestResultTest.java @@ -0,0 +1,168 @@ +package com.ftm.server.grooming; + +import static com.epages.restdocs.apispec.ResourceDocumentation.resource; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +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.JsonFieldType.NUMBER; +import static org.springframework.restdocs.payload.PayloadDocumentation.*; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import com.epages.restdocs.apispec.ResourceSnippetParameters; +import com.ftm.server.BaseTest; +import com.ftm.server.adapter.in.web.grooming.dto.request.SaveGroomingTestResultRequest; +import com.ftm.server.application.command.user.GeneralUserCreationCommand; +import com.ftm.server.application.port.out.persistence.auth.LoadUserForAuthPort; +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.application.query.FindByEmailQuery; +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.ArrayList; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; +import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; +import org.springframework.restdocs.payload.FieldDescriptor; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.transaction.annotation.Transactional; + +public class SaveGroomingTestResultTest extends BaseTest { + + @Autowired private SecurityAuthenticationPort securityAuthenticationPort; + @Autowired private SaveUserPort saveUserPort; + @Autowired private SaveUserImagePort saveUserImagePort; + @Autowired private LoadUserForAuthPort loadUserForAuthPort; + + private final List requestFieldSaveGroomingTestResult = + List.of( + fieldWithPath("userId").type(NUMBER).description("유저 ID"), + fieldWithPath("groomingLevelId").type(NUMBER).description("그루밍 레벨 ID"), + fieldWithPath("totalScore").type(NUMBER).description("그루밍 테스트 총 점수"), + fieldWithPath("results[]").type(ARRAY).optional().description("그루밍 테스트 결과 목록"), + fieldWithPath("results[].questionId").type(NUMBER).description("질문 ID"), + fieldWithPath("results[].answerIds[]").type(ARRAY).description("선택한 답변 ID 목록")); + + private final List responseFieldSaveGroomingTestResult = + List.of( + fieldWithPath("status").type(NUMBER).description("응답 상태"), + fieldWithPath("code").type(STRING).description("상태 코드"), + fieldWithPath("message").type(STRING).description("메시지"), + fieldWithPath("data").type(OBJECT).optional().description("응답 데이터")); + + private ResultActions getResultActions(SaveGroomingTestResultRequest request) throws Exception { + return mockMvc.perform( + RestDocumentationRequestBuilders.post("/api/grooming/tests") + .contentType(APPLICATION_JSON_VALUE) + .content(mapper.writeValueAsString(request))); + } + + private RestDocumentationResultHandler getDocument(Integer identifier) { + return document( + "saveGroomingTestResult/" + identifier, + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint(), getModifiedHeader()), + requestFields(requestFieldSaveGroomingTestResult), + responseFields(responseFieldSaveGroomingTestResult), + resource( + ResourceSnippetParameters.builder() + .tag("그루밍 테스트") + .summary("그루밍 테스트 결과 저장 api") + .description("그루밍 테스트 결과 저장 api 입니다.") + .responseFields(responseFieldSaveGroomingTestResult) + .build())); + } + + @BeforeEach + void setUp() { + GeneralUserCreationCommand command = + new GeneralUserCreationCommand( + "test@gmail.com", + securityAuthenticationPort.passwordEncode("test1234!"), + "test", + null, + null); + User testUser = saveUserPort.saveUser(User.createGeneralUser(command)); + saveUserImagePort.saveUserDefaultImage(UserImage.createUserImage(testUser.getId())); + } + + @Test + @Transactional + void 그루밍_테스트_결과_저장_성공() throws Exception { + // given + User user = + loadUserForAuthPort.loadUserByEmail(FindByEmailQuery.of("test@gmail.com")).get(); + List results = + List.of( + new SaveGroomingTestResultRequest.GroomingTestResult( + 1L, List.of(1L, 2L, 3L))); + SaveGroomingTestResultRequest request = + new SaveGroomingTestResultRequest(user.getId(), 1L, 10, results); + + // when + ResultActions resultActions = getResultActions(request); + + // then + resultActions.andExpect(status().isCreated()).andDo(print()); + + // documentation + resultActions.andDo(getDocument(1)); + } + + @Test + @Transactional + void 그루밍_테스트_결과_저장_실패1() throws Exception { + // given + List results = new ArrayList<>(); + SaveGroomingTestResultRequest request = + new SaveGroomingTestResultRequest(100L, 1L, 10, results); + + // when + ResultActions resultActions = getResultActions(request); + + // then + resultActions + .andExpect(status().is(ErrorResponseCode.USER_NOT_FOUND.getHttpStatus().value())) + .andExpect(jsonPath("code").value(ErrorResponseCode.USER_NOT_FOUND.getCode())) + .andDo(print()); + + // documentation + resultActions.andDo(getDocument(2)); + } + + @Test + @Transactional + void 그루밍_테스트_결과_저장_실패2() throws Exception { + // given + User user = + loadUserForAuthPort.loadUserByEmail(FindByEmailQuery.of("test@gmail.com")).get(); + List results = new ArrayList<>(); + SaveGroomingTestResultRequest request = + new SaveGroomingTestResultRequest(user.getId(), 100L, 10, results); + + // when + ResultActions resultActions = getResultActions(request); + + // then + resultActions + .andExpect( + status().is( + ErrorResponseCode.GROOMING_LEVEL_NOT_FOUND + .getHttpStatus() + .value())) + .andExpect( + jsonPath("code") + .value(ErrorResponseCode.GROOMING_LEVEL_NOT_FOUND.getCode())) + .andDo(print()); + + // documentation + resultActions.andDo(getDocument(3)); + } +} diff --git a/src/test/java/com/ftm/server/grooming/SubmitGroomingTestsTest.java b/src/test/java/com/ftm/server/grooming/SubmitGroomingTestsTest.java index 5ddd39b..2b7edfe 100644 --- a/src/test/java/com/ftm/server/grooming/SubmitGroomingTestsTest.java +++ b/src/test/java/com/ftm/server/grooming/SubmitGroomingTestsTest.java @@ -47,13 +47,13 @@ public class SubmitGroomingTestsTest extends BaseTest { .description("질문 그루밍 카테고리"), fieldWithPath("submissions[].answers[]") .type(ARRAY) - .description("질문에 답한 응답 목록"), + .description("질문에 답한 답변 목록"), fieldWithPath("submissions[].answers[].answerId") .type(NUMBER) - .description("응답 ID"), + .description("답변 ID"), fieldWithPath("submissions[].answers[].score") .type(NUMBER) - .description("응답 점수")); + .description("답변 점수")); private final List responseFieldSubmitGroomingTests = List.of(