diff --git a/src/docs/asciidoc/grooming-test-api.adoc b/src/docs/asciidoc/grooming-test-api.adoc index 18f5213..fd7392f 100644 --- a/src/docs/asciidoc/grooming-test-api.adoc +++ b/src/docs/asciidoc/grooming-test-api.adoc @@ -8,4 +8,19 @@ include::{snippetsDir}/loadGroomingTests/1/http-response.adoc[] ==== Response Body Fields include::{snippetsDir}/loadGroomingTests/1/response-fields.adoc[] ---- \ No newline at end of file +--- + +=== **2. 그루밍 테스트 제출** + +그루밍 테스트 제출(완료) api 입니다. + +==== Request +include::{snippetsDir}/submitGroomingTests/1/http-request.adoc[] + +==== 성공 Response +include::{snippetsDir}/submitGroomingTests/1/http-response.adoc[] + +==== Response Body Fields +include::{snippetsDir}/submitGroomingTests/1/response-fields.adoc[] + +--- diff --git a/src/main/java/com/ftm/server/adapter/in/web/grooming/controller/SubmitGroomingTestController.java b/src/main/java/com/ftm/server/adapter/in/web/grooming/controller/SubmitGroomingTestController.java new file mode 100644 index 0000000..977107a --- /dev/null +++ b/src/main/java/com/ftm/server/adapter/in/web/grooming/controller/SubmitGroomingTestController.java @@ -0,0 +1,39 @@ +package com.ftm.server.adapter.in.web.grooming.controller; + +import com.ftm.server.adapter.in.web.grooming.dto.request.GroomingTestSubmissionRequest; +import com.ftm.server.adapter.in.web.grooming.dto.response.GroomingTestResultResponse; +import com.ftm.server.adapter.in.web.grooming.mapper.GroomingTestCommandMapper; +import com.ftm.server.application.port.in.grooming.SubmitGroomingTestUseCase; +import com.ftm.server.application.vo.grooming.GroomingTestResultVo; +import com.ftm.server.common.response.ApiResponse; +import com.ftm.server.common.response.enums.SuccessResponseCode; +import com.ftm.server.infrastructure.security.UserPrincipal; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +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 SubmitGroomingTestController { + + private final SubmitGroomingTestUseCase submitGroomingTestUseCase; + + @PostMapping("/api/grooming/tests/submission") + public ResponseEntity> completeGroomingTests( + @RequestBody GroomingTestSubmissionRequest request, + @AuthenticationPrincipal UserPrincipal userPrincipal) { + GroomingTestResultVo results = + submitGroomingTestUseCase.execute( + GroomingTestCommandMapper.toSubmitGroomingTestCommand( + userPrincipal, request)); + + return ResponseEntity.status(HttpStatus.OK) + .body( + ApiResponse.success( + SuccessResponseCode.OK, GroomingTestResultResponse.from(results))); + } +} 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 new file mode 100644 index 0000000..53038fe --- /dev/null +++ b/src/main/java/com/ftm/server/adapter/in/web/grooming/dto/request/GroomingTestSubmissionRequest.java @@ -0,0 +1,28 @@ +package com.ftm.server.adapter.in.web.grooming.dto.request; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class GroomingTestSubmissionRequest { + + private List submissions; + + @Getter + @AllArgsConstructor + 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; + } + } +} 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 new file mode 100644 index 0000000..71decd8 --- /dev/null +++ b/src/main/java/com/ftm/server/adapter/in/web/grooming/dto/response/GroomingTestResultResponse.java @@ -0,0 +1,25 @@ +package com.ftm.server.adapter.in.web.grooming.dto.response; + +import com.ftm.server.application.vo.grooming.GroomingLevelVo; +import com.ftm.server.application.vo.grooming.GroomingTestResultGradesVo; +import com.ftm.server.application.vo.grooming.GroomingTestResultScoresVo; +import com.ftm.server.application.vo.grooming.GroomingTestResultVo; +import lombok.Getter; + +@Getter +public class GroomingTestResultResponse { + + private final GroomingTestResultScoresVo scores; + private final GroomingTestResultGradesVo grades; + private final GroomingLevelVo level; + + private GroomingTestResultResponse(GroomingTestResultVo result) { + this.scores = result.getScores(); + this.grades = result.getGrades(); + this.level = result.getLevel(); + } + + public static GroomingTestResultResponse from(GroomingTestResultVo result) { + return new GroomingTestResultResponse(result); + } +} 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 new file mode 100644 index 0000000..a25733b --- /dev/null +++ b/src/main/java/com/ftm/server/adapter/in/web/grooming/mapper/GroomingTestCommandMapper.java @@ -0,0 +1,34 @@ +package com.ftm.server.adapter.in.web.grooming.mapper; + +import com.ftm.server.adapter.in.web.grooming.dto.request.GroomingTestSubmissionRequest; +import com.ftm.server.application.command.grooming.SubmitGroomingTestCommand; +import com.ftm.server.infrastructure.security.UserPrincipal; +import java.util.List; + +public class GroomingTestCommandMapper { + + public static SubmitGroomingTestCommand toSubmitGroomingTestCommand( + UserPrincipal userPrincipal, GroomingTestSubmissionRequest request) { + Long userId = (userPrincipal != null) ? userPrincipal.getId() : null; + List submissions = + request.getSubmissions().stream() + .map( + item -> + SubmitGroomingTestCommand.SubmittedQuestion.of( + item.getQuestionId(), + item.getGroomingCategory(), + item.getAnswers().stream() + .map( + answer -> + SubmitGroomingTestCommand + .SubmittedQuestion + .SelectedAnswer.of( + answer + .getAnswerId(), + answer.getScore())) + .toList())) + .toList(); + + return SubmitGroomingTestCommand.from(userId, submissions); + } +} diff --git a/src/main/java/com/ftm/server/adapter/out/persistence/adapter/grooming/GroomingDomainPersistenceAdapter.java b/src/main/java/com/ftm/server/adapter/out/persistence/adapter/grooming/GroomingDomainPersistenceAdapter.java index 0bab35f..43784d9 100644 --- a/src/main/java/com/ftm/server/adapter/out/persistence/adapter/grooming/GroomingDomainPersistenceAdapter.java +++ b/src/main/java/com/ftm/server/adapter/out/persistence/adapter/grooming/GroomingDomainPersistenceAdapter.java @@ -1,17 +1,17 @@ package com.ftm.server.adapter.out.persistence.adapter.grooming; -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.model.GroomingTestAnswerJpaEntity; -import com.ftm.server.adapter.out.persistence.model.GroomingTestQuestionJpaEntity; -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.persistence.grooming.LoadGroomingTestAnswerPort; -import com.ftm.server.application.port.out.persistence.grooming.LoadGroomingTestQuestionPort; +import com.ftm.server.adapter.out.persistence.mapper.*; +import com.ftm.server.adapter.out.persistence.model.*; +import com.ftm.server.adapter.out.persistence.repository.*; +import com.ftm.server.application.port.out.persistence.grooming.*; +import com.ftm.server.application.query.FIndGroomingLevelByScoreQuery; +import com.ftm.server.application.query.FindByIdQuery; import com.ftm.server.common.annotation.Adapter; -import com.ftm.server.domain.entity.GroomingTestAnswer; -import com.ftm.server.domain.entity.GroomingTestQuestion; +import com.ftm.server.common.exception.CustomException; +import com.ftm.server.common.response.enums.ErrorResponseCode; +import com.ftm.server.domain.entity.*; import java.util.List; +import java.util.Optional; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -20,15 +20,26 @@ @RequiredArgsConstructor @Slf4j public class GroomingDomainPersistenceAdapter - implements LoadGroomingTestQuestionPort, LoadGroomingTestAnswerPort { + implements LoadGroomingTestQuestionPort, + LoadGroomingTestAnswerPort, + LoadUserForGroomingPort, + SaveGroomingTestResultPort, + LoadGroomingLevelPort, + UpdateUserForGroomingPort { // Repository private final GroomingTestQuestionRepository groomingTestQuestionRepository; private final GroomingTestAnswerRepository groomingTestAnswerRepository; + private final GroomingLevelRepository groomingLevelRepository; + private final GroomingTestResultRepository groomingTestResultRepository; + private final UserRepository userRepository; // Mapper private final GroomingTestQuestionMapper groomingTestQuestionMapper; private final GroomingTestAnswerMapper groomingTestAnswerMapper; + private final GroomingLevelMapper groomingLevelMapper; + private final GroomingTestResultMapper groomingTestResultMapper; + private final UserMapper userMapper; @Override public List loadGroomingTestQuestions() { @@ -45,4 +56,61 @@ public List loadGroomingTestAnswers() { .map(groomingTestAnswerMapper::toDomain) .collect(Collectors.toList()); } + + @Override + public Optional loadUser(FindByIdQuery query) { + return userRepository.findById(query.getId()).map(userMapper::toDomainEntity); + } + + @Override + public void saveGroomingTestResults(List results) { + List entities = + results.stream() + .map( + result -> { + UserJpaEntity user = + userRepository.getReferenceById(result.getUserId()); + GroomingTestQuestionJpaEntity question = + groomingTestQuestionRepository.getReferenceById( + result.getGroomingTestQuestionId()); + GroomingTestAnswerJpaEntity answer = + groomingTestAnswerRepository.getReferenceById( + result.getGroomingTestAnswerId()); + + return groomingTestResultMapper.toJpaEntity( + user, question, answer, result); + }) + .toList(); + + groomingTestResultRepository.saveAll(entities); + } + + @Override + public Optional loadGroomingLevelByScore(FIndGroomingLevelByScoreQuery query) { + return groomingLevelRepository + .findByScoreInRange(query.getScore()) + .map(groomingLevelMapper::toDomainEntity); + } + + @Override + public void updateUserGroomingStatus(User user) { + UserJpaEntity userJpaEntity = + userRepository + .findById(user.getId()) + .orElseThrow(() -> CustomException.USER_NOT_FOUND); + + // 유저 점수 업데이트 + userJpaEntity.updateGroomingScore(user); + + GroomingLevelJpaEntity groomingLevelJpaEntity = + groomingLevelRepository + .findById(user.getGroomingLevelId()) + .orElseThrow( + () -> + new CustomException( + ErrorResponseCode.GROOMING_LEVEL_NOT_FOUND)); + + // 유저 그루밍 레벨 업데이트 + userJpaEntity.updateGroomingLevel(groomingLevelJpaEntity); + } } diff --git a/src/main/java/com/ftm/server/adapter/out/persistence/mapper/GroomingTestResultMapper.java b/src/main/java/com/ftm/server/adapter/out/persistence/mapper/GroomingTestResultMapper.java index aa369a8..d4da921 100644 --- a/src/main/java/com/ftm/server/adapter/out/persistence/mapper/GroomingTestResultMapper.java +++ b/src/main/java/com/ftm/server/adapter/out/persistence/mapper/GroomingTestResultMapper.java @@ -17,6 +17,7 @@ public GroomingTestResult toDomainEntity(GroomingTestResultJpaEntity jpaEntity) jpaEntity.getUser().getId(), jpaEntity.getGroomingTestQuestion().getId(), jpaEntity.getGroomingTestAnswer().getId(), + jpaEntity.getTestedAt(), jpaEntity.getCreatedAt(), jpaEntity.getUpdatedAt()); } @@ -24,8 +25,12 @@ public GroomingTestResult toDomainEntity(GroomingTestResultJpaEntity jpaEntity) public GroomingTestResultJpaEntity toJpaEntity( UserJpaEntity userJpaEntity, GroomingTestQuestionJpaEntity groomingTestQuestionJpaEntity, - GroomingTestAnswerJpaEntity groomingTestAnswerJpaEntity) { + GroomingTestAnswerJpaEntity groomingTestAnswerJpaEntity, + GroomingTestResult domainEntity) { return GroomingTestResultJpaEntity.from( - userJpaEntity, groomingTestQuestionJpaEntity, groomingTestAnswerJpaEntity); + userJpaEntity, + groomingTestQuestionJpaEntity, + groomingTestAnswerJpaEntity, + domainEntity); } } diff --git a/src/main/java/com/ftm/server/adapter/out/persistence/model/GroomingTestResultJpaEntity.java b/src/main/java/com/ftm/server/adapter/out/persistence/model/GroomingTestResultJpaEntity.java index c00f15f..0df25cd 100644 --- a/src/main/java/com/ftm/server/adapter/out/persistence/model/GroomingTestResultJpaEntity.java +++ b/src/main/java/com/ftm/server/adapter/out/persistence/model/GroomingTestResultJpaEntity.java @@ -1,6 +1,8 @@ package com.ftm.server.adapter.out.persistence.model; +import com.ftm.server.domain.entity.GroomingTestResult; import jakarta.persistence.*; +import java.time.LocalDateTime; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; @@ -28,24 +30,30 @@ public class GroomingTestResultJpaEntity extends BaseTimeJpaEntity { @JoinColumn(name = "grooming_test_answer_id") private GroomingTestAnswerJpaEntity groomingTestAnswer; + private LocalDateTime testedAt; + @Builder(access = AccessLevel.PRIVATE) private GroomingTestResultJpaEntity( UserJpaEntity user, GroomingTestQuestionJpaEntity groomingTestQuestion, - GroomingTestAnswerJpaEntity groomingTestAnswer) { + GroomingTestAnswerJpaEntity groomingTestAnswer, + LocalDateTime testedAt) { this.user = user; this.groomingTestQuestion = groomingTestQuestion; this.groomingTestAnswer = groomingTestAnswer; + this.testedAt = testedAt; } public static GroomingTestResultJpaEntity from( UserJpaEntity userJpaEntity, GroomingTestQuestionJpaEntity groomingTestQuestionJpaEntity, - GroomingTestAnswerJpaEntity groomingTestAnswerJpaEntity) { + GroomingTestAnswerJpaEntity groomingTestAnswerJpaEntity, + GroomingTestResult groomingTestResult) { return GroomingTestResultJpaEntity.builder() .user(userJpaEntity) .groomingTestQuestion(groomingTestQuestionJpaEntity) .groomingTestAnswer(groomingTestAnswerJpaEntity) + .testedAt(groomingTestResult.getTestedAt()) .build(); } } diff --git a/src/main/java/com/ftm/server/adapter/out/persistence/model/UserJpaEntity.java b/src/main/java/com/ftm/server/adapter/out/persistence/model/UserJpaEntity.java index d6923c1..e7cf243 100644 --- a/src/main/java/com/ftm/server/adapter/out/persistence/model/UserJpaEntity.java +++ b/src/main/java/com/ftm/server/adapter/out/persistence/model/UserJpaEntity.java @@ -122,4 +122,12 @@ public static UserJpaEntity from(User user, GroomingLevelJpaEntity groomingLevel .deletedAt(user.getDeletedAt()) .build(); } + + public void updateGroomingScore(User user) { + this.groomingScore = user.getGroomingScore(); + } + + public void updateGroomingLevel(GroomingLevelJpaEntity groomingLevelJpaEntity) { + this.groomingLevel = groomingLevelJpaEntity; + } } diff --git a/src/main/java/com/ftm/server/adapter/out/persistence/repository/GroomingLevelCustomRepository.java b/src/main/java/com/ftm/server/adapter/out/persistence/repository/GroomingLevelCustomRepository.java new file mode 100644 index 0000000..b7e0259 --- /dev/null +++ b/src/main/java/com/ftm/server/adapter/out/persistence/repository/GroomingLevelCustomRepository.java @@ -0,0 +1,9 @@ +package com.ftm.server.adapter.out.persistence.repository; + +import com.ftm.server.adapter.out.persistence.model.GroomingLevelJpaEntity; +import java.util.Optional; + +public interface GroomingLevelCustomRepository { + + Optional findByScoreInRange(int score); +} diff --git a/src/main/java/com/ftm/server/adapter/out/persistence/repository/GroomingLevelCustomRepositoryImpl.java b/src/main/java/com/ftm/server/adapter/out/persistence/repository/GroomingLevelCustomRepositoryImpl.java new file mode 100644 index 0000000..99002b1 --- /dev/null +++ b/src/main/java/com/ftm/server/adapter/out/persistence/repository/GroomingLevelCustomRepositoryImpl.java @@ -0,0 +1,27 @@ +package com.ftm.server.adapter.out.persistence.repository; + +import static com.ftm.server.adapter.out.persistence.model.QGroomingLevelJpaEntity.groomingLevelJpaEntity; + +import com.ftm.server.adapter.out.persistence.model.GroomingLevelJpaEntity; +import com.querydsl.jpa.impl.JPAQueryFactory; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class GroomingLevelCustomRepositoryImpl implements GroomingLevelCustomRepository { + + private final JPAQueryFactory queryFactory; + + @Override + public Optional findByScoreInRange(int score) { + return Optional.ofNullable( + queryFactory + .selectFrom(groomingLevelJpaEntity) + .where( + groomingLevelJpaEntity.minScore.loe(score), + groomingLevelJpaEntity.maxScore.goe(score)) + .fetchOne()); + } +} diff --git a/src/main/java/com/ftm/server/adapter/out/persistence/repository/GroomingLevelRepository.java b/src/main/java/com/ftm/server/adapter/out/persistence/repository/GroomingLevelRepository.java index ef562f9..848140a 100644 --- a/src/main/java/com/ftm/server/adapter/out/persistence/repository/GroomingLevelRepository.java +++ b/src/main/java/com/ftm/server/adapter/out/persistence/repository/GroomingLevelRepository.java @@ -3,4 +3,5 @@ import com.ftm.server.adapter.out.persistence.model.GroomingLevelJpaEntity; import org.springframework.data.jpa.repository.JpaRepository; -public interface GroomingLevelRepository extends JpaRepository {} +public interface GroomingLevelRepository + extends JpaRepository, GroomingLevelCustomRepository {} 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 new file mode 100644 index 0000000..09b7614 --- /dev/null +++ b/src/main/java/com/ftm/server/application/command/grooming/SubmitGroomingTestCommand.java @@ -0,0 +1,56 @@ +package com.ftm.server.application.command.grooming; + +import java.util.List; +import lombok.Getter; + +@Getter +public class SubmitGroomingTestCommand { + + private final Long userId; + private final List submissions; + + protected SubmitGroomingTestCommand(Long userId, List submissions) { + this.userId = userId; + this.submissions = submissions; + } + + public static SubmitGroomingTestCommand from(Long userId, List submissions) { + return new SubmitGroomingTestCommand(userId, submissions); + } + + @Getter + public static class SubmittedQuestion { + + private final Long questionId; + private final String groomingCategory; + private final List answers; + + private SubmittedQuestion( + Long questionId, String groomingCategory, List answers) { + this.questionId = questionId; + this.groomingCategory = groomingCategory; + this.answers = answers; + } + + 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); + } + } + } +} diff --git a/src/main/java/com/ftm/server/application/port/in/grooming/SubmitGroomingTestUseCase.java b/src/main/java/com/ftm/server/application/port/in/grooming/SubmitGroomingTestUseCase.java new file mode 100644 index 0000000..d6e4a00 --- /dev/null +++ b/src/main/java/com/ftm/server/application/port/in/grooming/SubmitGroomingTestUseCase.java @@ -0,0 +1,11 @@ +package com.ftm.server.application.port.in.grooming; + +import com.ftm.server.application.command.grooming.SubmitGroomingTestCommand; +import com.ftm.server.application.vo.grooming.GroomingTestResultVo; +import com.ftm.server.common.annotation.UseCase; + +@UseCase +public interface SubmitGroomingTestUseCase { + + GroomingTestResultVo execute(SubmitGroomingTestCommand command); +} diff --git a/src/main/java/com/ftm/server/application/port/out/persistence/grooming/LoadGroomingLevelPort.java b/src/main/java/com/ftm/server/application/port/out/persistence/grooming/LoadGroomingLevelPort.java new file mode 100644 index 0000000..2333107 --- /dev/null +++ b/src/main/java/com/ftm/server/application/port/out/persistence/grooming/LoadGroomingLevelPort.java @@ -0,0 +1,12 @@ +package com.ftm.server.application.port.out.persistence.grooming; + +import com.ftm.server.application.query.FIndGroomingLevelByScoreQuery; +import com.ftm.server.common.annotation.Port; +import com.ftm.server.domain.entity.GroomingLevel; +import java.util.Optional; + +@Port +public interface LoadGroomingLevelPort { + + Optional loadGroomingLevelByScore(FIndGroomingLevelByScoreQuery query); +} diff --git a/src/main/java/com/ftm/server/application/port/out/persistence/grooming/LoadUserForGroomingPort.java b/src/main/java/com/ftm/server/application/port/out/persistence/grooming/LoadUserForGroomingPort.java new file mode 100644 index 0000000..3718df3 --- /dev/null +++ b/src/main/java/com/ftm/server/application/port/out/persistence/grooming/LoadUserForGroomingPort.java @@ -0,0 +1,12 @@ +package com.ftm.server.application.port.out.persistence.grooming; + +import com.ftm.server.application.query.FindByIdQuery; +import com.ftm.server.common.annotation.Port; +import com.ftm.server.domain.entity.User; +import java.util.Optional; + +@Port +public interface LoadUserForGroomingPort { + + Optional loadUser(FindByIdQuery query); +} diff --git a/src/main/java/com/ftm/server/application/port/out/persistence/grooming/SaveGroomingTestResultPort.java b/src/main/java/com/ftm/server/application/port/out/persistence/grooming/SaveGroomingTestResultPort.java new file mode 100644 index 0000000..80cbf4a --- /dev/null +++ b/src/main/java/com/ftm/server/application/port/out/persistence/grooming/SaveGroomingTestResultPort.java @@ -0,0 +1,11 @@ +package com.ftm.server.application.port.out.persistence.grooming; + +import com.ftm.server.common.annotation.Port; +import com.ftm.server.domain.entity.GroomingTestResult; +import java.util.List; + +@Port +public interface SaveGroomingTestResultPort { + + void saveGroomingTestResults(List results); +} diff --git a/src/main/java/com/ftm/server/application/port/out/persistence/grooming/UpdateUserForGroomingPort.java b/src/main/java/com/ftm/server/application/port/out/persistence/grooming/UpdateUserForGroomingPort.java new file mode 100644 index 0000000..7ab5973 --- /dev/null +++ b/src/main/java/com/ftm/server/application/port/out/persistence/grooming/UpdateUserForGroomingPort.java @@ -0,0 +1,10 @@ +package com.ftm.server.application.port.out.persistence.grooming; + +import com.ftm.server.common.annotation.Port; +import com.ftm.server.domain.entity.User; + +@Port +public interface UpdateUserForGroomingPort { + + void updateUserGroomingStatus(User user); +} diff --git a/src/main/java/com/ftm/server/application/query/FIndGroomingLevelByScoreQuery.java b/src/main/java/com/ftm/server/application/query/FIndGroomingLevelByScoreQuery.java new file mode 100644 index 0000000..5085019 --- /dev/null +++ b/src/main/java/com/ftm/server/application/query/FIndGroomingLevelByScoreQuery.java @@ -0,0 +1,17 @@ +package com.ftm.server.application.query; + +import lombok.Getter; + +@Getter +public class FIndGroomingLevelByScoreQuery { + + private final int score; + + private FIndGroomingLevelByScoreQuery(int score) { + this.score = score; + } + + public static FIndGroomingLevelByScoreQuery of(int score) { + return new FIndGroomingLevelByScoreQuery(score); + } +} 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 new file mode 100644 index 0000000..617311f --- /dev/null +++ b/src/main/java/com/ftm/server/application/service/grooming/GroomingTestResultCalculateService.java @@ -0,0 +1,97 @@ +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 new file mode 100644 index 0000000..12f0a0b --- /dev/null +++ b/src/main/java/com/ftm/server/application/service/grooming/SubmitGroomingTestService.java @@ -0,0 +1,83 @@ +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.in.grooming.SubmitGroomingTestUseCase; +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.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 java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@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; + + @Override + public GroomingTestResultVo execute(SubmitGroomingTestCommand command) { + // 그루밍 테스트 결과 + GroomingTestResultVo results = groomingTestResultCalculateService.process(command); + + // 비로그인 유저인 경우 + if (command.getUserId() == null) { + return results; + } + + User user = + loadUserForGroomingPort + .loadUser(FindByIdQuery.of(command.getUserId())) + .orElseThrow(() -> CustomException.USER_NOT_FOUND); + + // 유저 그루밍 레벨 업데이트 + updateUserGroomingInfo(user, results.getScores(), results.getLevel()); + + // 유저 그루밍 테스트 결과 등록 + saveUserGroomingTestResult(user.getId(), command.getSubmissions(), results); + + return results; + } + + private void updateUserGroomingInfo( + User user, GroomingTestResultScoresVo scores, GroomingLevelVo level) { + user.updateGroomingInfo(scores.getTotalScore(), level.getGroomingLevelId()); + + updateUserForGroomingPort.updateUserGroomingStatus(user); + } + + 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); + } +} diff --git a/src/main/java/com/ftm/server/application/vo/grooming/GroomingCategoryGradeInfoVo.java b/src/main/java/com/ftm/server/application/vo/grooming/GroomingCategoryGradeInfoVo.java new file mode 100644 index 0000000..b221a12 --- /dev/null +++ b/src/main/java/com/ftm/server/application/vo/grooming/GroomingCategoryGradeInfoVo.java @@ -0,0 +1,20 @@ +package com.ftm.server.application.vo.grooming; + +import com.ftm.server.domain.enums.GroomingCategoryGrade; +import lombok.Getter; + +@Getter +public class GroomingCategoryGradeInfoVo { + + private final String grade; + private final int level; + + private GroomingCategoryGradeInfoVo(String grade, int level) { + this.grade = grade; + this.level = level; + } + + public static GroomingCategoryGradeInfoVo from(GroomingCategoryGrade categoryGrade) { + return new GroomingCategoryGradeInfoVo(categoryGrade.getGrade(), categoryGrade.getLevel()); + } +} diff --git a/src/main/java/com/ftm/server/application/vo/grooming/GroomingLevelVo.java b/src/main/java/com/ftm/server/application/vo/grooming/GroomingLevelVo.java new file mode 100644 index 0000000..426e5d2 --- /dev/null +++ b/src/main/java/com/ftm/server/application/vo/grooming/GroomingLevelVo.java @@ -0,0 +1,22 @@ +package com.ftm.server.application.vo.grooming; + +import com.ftm.server.domain.entity.GroomingLevel; +import lombok.Getter; + +@Getter +public class GroomingLevelVo { + + private final Long groomingLevelId; + private final String mildLevelName; + private final String spicyLevelName; + + private GroomingLevelVo(GroomingLevel groomingLevel) { + this.groomingLevelId = groomingLevel.getId(); + this.mildLevelName = groomingLevel.getMildLevelName(); + this.spicyLevelName = groomingLevel.getSpicyLevelName(); + } + + public static GroomingLevelVo from(GroomingLevel groomingLevel) { + return new GroomingLevelVo(groomingLevel); + } +} diff --git a/src/main/java/com/ftm/server/application/vo/grooming/GroomingTestResultGradesVo.java b/src/main/java/com/ftm/server/application/vo/grooming/GroomingTestResultGradesVo.java new file mode 100644 index 0000000..6526c23 --- /dev/null +++ b/src/main/java/com/ftm/server/application/vo/grooming/GroomingTestResultGradesVo.java @@ -0,0 +1,35 @@ +package com.ftm.server.application.vo.grooming; + +import lombok.Getter; + +@Getter +public class GroomingTestResultGradesVo { + + private final GroomingCategoryGradeInfoVo beauty; + private final GroomingCategoryGradeInfoVo hygiene; + private final GroomingCategoryGradeInfoVo hair; + private final GroomingCategoryGradeInfoVo workout; + private final GroomingCategoryGradeInfoVo fashion; + + private GroomingTestResultGradesVo( + GroomingCategoryGradeInfoVo beauty, + GroomingCategoryGradeInfoVo hygiene, + GroomingCategoryGradeInfoVo hair, + GroomingCategoryGradeInfoVo workout, + GroomingCategoryGradeInfoVo fashion) { + this.beauty = beauty; + this.hygiene = hygiene; + this.hair = hair; + this.workout = workout; + this.fashion = fashion; + } + + public static GroomingTestResultGradesVo from( + GroomingCategoryGradeInfoVo beauty, + GroomingCategoryGradeInfoVo hygiene, + GroomingCategoryGradeInfoVo hair, + GroomingCategoryGradeInfoVo workout, + GroomingCategoryGradeInfoVo fashion) { + return new GroomingTestResultGradesVo(beauty, hygiene, hair, workout, fashion); + } +} diff --git a/src/main/java/com/ftm/server/application/vo/grooming/GroomingTestResultScoresVo.java b/src/main/java/com/ftm/server/application/vo/grooming/GroomingTestResultScoresVo.java new file mode 100644 index 0000000..2a02a23 --- /dev/null +++ b/src/main/java/com/ftm/server/application/vo/grooming/GroomingTestResultScoresVo.java @@ -0,0 +1,40 @@ +package com.ftm.server.application.vo.grooming; + +import lombok.Getter; + +@Getter +public class GroomingTestResultScoresVo { + + private final int beautyScore; + private final int hygieneScore; + private final int hairScore; + private final int workoutScore; + private final int fashionScore; + private final int totalScore; + + private GroomingTestResultScoresVo( + int beautyScore, + int hygieneScore, + int hairScore, + int workoutScore, + int fashionScore, + int totalScore) { + this.beautyScore = beautyScore; + this.hygieneScore = hygieneScore; + this.hairScore = hairScore; + this.workoutScore = workoutScore; + this.fashionScore = fashionScore; + this.totalScore = totalScore; + } + + public static GroomingTestResultScoresVo of( + int beautyScore, + int hygieneScore, + int hairScore, + int workoutScore, + int fashionScore, + int totalScore) { + return new GroomingTestResultScoresVo( + beautyScore, hygieneScore, hairScore, workoutScore, fashionScore, totalScore); + } +} diff --git a/src/main/java/com/ftm/server/application/vo/grooming/GroomingTestResultVo.java b/src/main/java/com/ftm/server/application/vo/grooming/GroomingTestResultVo.java new file mode 100644 index 0000000..6e63ed6 --- /dev/null +++ b/src/main/java/com/ftm/server/application/vo/grooming/GroomingTestResultVo.java @@ -0,0 +1,32 @@ +package com.ftm.server.application.vo.grooming; + +import lombok.Getter; + +@Getter +public class GroomingTestResultVo { + + private final boolean isAuthenticated; + private final GroomingTestResultScoresVo scores; + private final GroomingTestResultGradesVo grades; + private final GroomingLevelVo level; + + private GroomingTestResultVo( + boolean isAuthenticated, + GroomingTestResultScoresVo scores, + GroomingTestResultGradesVo grades, + GroomingLevelVo level) { + this.isAuthenticated = isAuthenticated; + this.scores = scores; + this.grades = grades; + this.level = level; + } + + public static GroomingTestResultVo from( + Long userId, + GroomingTestResultScoresVo scores, + GroomingTestResultGradesVo grades, + GroomingLevelVo level) { + boolean isAuthenticated = userId != null; + return new GroomingTestResultVo(isAuthenticated, scores, grades, level); + } +} diff --git a/src/main/java/com/ftm/server/common/consts/StaticConsts.java b/src/main/java/com/ftm/server/common/consts/StaticConsts.java index 1160cc2..8eeb150 100644 --- a/src/main/java/com/ftm/server/common/consts/StaticConsts.java +++ b/src/main/java/com/ftm/server/common/consts/StaticConsts.java @@ -2,6 +2,12 @@ public class StaticConsts { + public static final int BEAUTY_CATEGORY_MAX_SCORE = 20; + public static final int HYGIENE_CATEGORY_MAX_SCORE = 14; + public static final int HAIR_CATEGORY_MAX_SCORE = 11; + public static final int WORKOUT_CATEGORY_MAX_SCORE = 40; + public static final int FASHION_CATEGORY_MAX_SCORE = 14; + public static final String AUTHORIZATION_HEADER_PREFIX = "Bearer "; public static final String AUTHORIZATION_GRANT_TYPE = "authorization_code"; public static final String PENDING_SOCIAL_USER_SESSION_KEY = "PENDING_SOCIAL_USER_INFO"; 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 a976a1e..4fbf75e 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 @@ -11,6 +11,8 @@ public enum ErrorResponseCode { HttpStatus.BAD_REQUEST, "E400_001", "클라이언트 요청값의 일부가 잘못된 형식이거나, 필수 데이터가 누락되었습니다."), INVALID_SEESION_FOR_SOCIAL_USER_SIGNUP( 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 값입니다."), // 401번 NOT_AUTHENTICATED(HttpStatus.UNAUTHORIZED, "E401_001", "인증되지 않은 사용자입니다."), @@ -22,6 +24,8 @@ public enum ErrorResponseCode { // 404번 USER_NOT_FOUND(HttpStatus.NOT_FOUND, "E404_001", "요청된 사용자를 찾을 수 없습니다."), USER_IMAGE_NOT_FOUND(HttpStatus.NOT_FOUND, "E404_002", "요청한 유저 이미지를 찾을 수 없습니다."), + GROOMING_LEVEL_NOT_FOUND(HttpStatus.NOT_FOUND, "E404_003", "요청한 그루밍 레벨 정보를 찾을 수 없습니다."), + GROOMING_CATEGORY_NOT_FOUND(HttpStatus.NOT_FOUND, "E404_004", "요창한 그루밍 카테고리 정보를 찾을 수 없습니다."), // 409번 USER_ALREADY_EXISTS(HttpStatus.CONFLICT, "E409_001", "이미 존재하는 사용자입니다."), diff --git a/src/main/java/com/ftm/server/domain/entity/GroomingTestResult.java b/src/main/java/com/ftm/server/domain/entity/GroomingTestResult.java index 339d007..0df18a7 100644 --- a/src/main/java/com/ftm/server/domain/entity/GroomingTestResult.java +++ b/src/main/java/com/ftm/server/domain/entity/GroomingTestResult.java @@ -14,6 +14,7 @@ public class GroomingTestResult extends BaseTime { private Long userId; private Long groomingTestQuestionId; private Long groomingTestAnswerId; + private LocalDateTime testedAt; @Builder(access = AccessLevel.PRIVATE) private GroomingTestResult( @@ -21,12 +22,14 @@ private GroomingTestResult( Long userId, Long groomingTestAnswerId, Long groomingTestQuestionId, + LocalDateTime testedAt, LocalDateTime createdAt, LocalDateTime updatedAt) { this.id = id; this.userId = userId; this.groomingTestQuestionId = groomingTestQuestionId; this.groomingTestAnswerId = groomingTestAnswerId; + this.testedAt = testedAt; this.createdAt = createdAt; this.updatedAt = updatedAt; } @@ -36,6 +39,7 @@ public static GroomingTestResult of( Long userId, Long groomingTestQuestionId, Long groomingTestAnswerId, + LocalDateTime testedAt, LocalDateTime createdAt, LocalDateTime updatedAt) { return GroomingTestResult.builder() @@ -43,8 +47,22 @@ public static GroomingTestResult of( .userId(userId) .groomingTestQuestionId(groomingTestQuestionId) .groomingTestAnswerId(groomingTestAnswerId) + .testedAt(testedAt) .createdAt(createdAt) .updatedAt(updatedAt) .build(); } + + public static GroomingTestResult create( + Long userId, + Long groomingTestQuestionId, + Long groomingTestAnswerId, + LocalDateTime testedAt) { + return GroomingTestResult.builder() + .userId(userId) + .groomingTestQuestionId(groomingTestQuestionId) + .groomingTestAnswerId(groomingTestAnswerId) + .testedAt(testedAt) + .build(); + } } diff --git a/src/main/java/com/ftm/server/domain/entity/User.java b/src/main/java/com/ftm/server/domain/entity/User.java index 209cccf..ed2614a 100644 --- a/src/main/java/com/ftm/server/domain/entity/User.java +++ b/src/main/java/com/ftm/server/domain/entity/User.java @@ -148,4 +148,9 @@ public static User createTestKakaoUser() { .role(UserRole.USER) .build(); } + + public void updateGroomingInfo(Integer groomingScore, Long groomingLevelId) { + this.groomingScore = groomingScore; + this.groomingLevelId = groomingLevelId; + } } diff --git a/src/main/java/com/ftm/server/domain/enums/GroomingCategory.java b/src/main/java/com/ftm/server/domain/enums/GroomingCategory.java index 7022514..f5dbce1 100644 --- a/src/main/java/com/ftm/server/domain/enums/GroomingCategory.java +++ b/src/main/java/com/ftm/server/domain/enums/GroomingCategory.java @@ -1,5 +1,8 @@ package com.ftm.server.domain.enums; +import com.ftm.server.common.exception.CustomException; +import com.ftm.server.common.response.enums.ErrorResponseCode; +import java.util.Arrays; import lombok.Getter; @Getter @@ -15,4 +18,12 @@ public enum GroomingCategory { GroomingCategory(String value) { this.value = value; } + + public static GroomingCategory from(String value) { + return Arrays.stream(GroomingCategory.values()) + .filter(category -> category.name().equals(value.toUpperCase())) + .findFirst() + .orElseThrow( + () -> new CustomException(ErrorResponseCode.GROOMING_CATEGORY_NOT_FOUND)); + } } diff --git a/src/main/java/com/ftm/server/domain/enums/GroomingCategoryGrade.java b/src/main/java/com/ftm/server/domain/enums/GroomingCategoryGrade.java new file mode 100644 index 0000000..efcbc26 --- /dev/null +++ b/src/main/java/com/ftm/server/domain/enums/GroomingCategoryGrade.java @@ -0,0 +1,36 @@ +package com.ftm.server.domain.enums; + +import com.ftm.server.common.exception.CustomException; +import com.ftm.server.common.response.enums.ErrorResponseCode; +import java.util.Arrays; +import lombok.Getter; + +@Getter +public enum GroomingCategoryGrade { + LOW("무심", 1, 0.0, 0.33), + MIDDLE("단정", 2, 0.34, 0.66), + HIGH("완벽", 3, 0.67, 1.0); + + private final String grade; + private final int level; + private final double minRatio; + private final double maxRatio; + + GroomingCategoryGrade(String grade, int level, double minRatio, double maxRatio) { + this.grade = grade; + this.level = level; + this.minRatio = minRatio; + this.maxRatio = maxRatio; + } + + public static GroomingCategoryGrade fromScore(int score, int maxScore) { + if (maxScore == 0) throw new CustomException(ErrorResponseCode.INVALID_MAX_SCORE); + double ratio = (double) score / maxScore; + + return Arrays.stream(values()) + .filter(g -> ratio >= g.minRatio && ratio <= g.maxRatio) + .findFirst() + .orElseThrow( + () -> new CustomException(ErrorResponseCode.INVALID_RATIO_FOR_PERCENTAGE)); + } +} diff --git a/src/main/java/com/ftm/server/infrastructure/querydsl/QueryDslConfig.java b/src/main/java/com/ftm/server/infrastructure/querydsl/QueryDslConfig.java new file mode 100644 index 0000000..d079730 --- /dev/null +++ b/src/main/java/com/ftm/server/infrastructure/querydsl/QueryDslConfig.java @@ -0,0 +1,19 @@ +package com.ftm.server.infrastructure.querydsl; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@RequiredArgsConstructor +public class QueryDslConfig { + + private final EntityManager em; + + @Bean + public JPAQueryFactory queryFactory() { + return new JPAQueryFactory(em); + } +} 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 6d6b3db..3ba5720 100644 --- a/src/main/java/com/ftm/server/infrastructure/security/SecurityConfig.java +++ b/src/main/java/com/ftm/server/infrastructure/security/SecurityConfig.java @@ -59,7 +59,8 @@ public class SecurityConfig { "/api/users/email/authentication/code", "/api/auth/login/**", "/api/users", - "/api/users/social" + "/api/users/social", + "/api/grooming/tests/submission" }; private static final String[] ANONYMOUS_MATCHERS = {"/docs/**"}; diff --git a/src/test/java/com/ftm/server/grooming/SubmitGroomingTestsTest.java b/src/test/java/com/ftm/server/grooming/SubmitGroomingTestsTest.java new file mode 100644 index 0000000..5ddd39b --- /dev/null +++ b/src/test/java/com/ftm/server/grooming/SubmitGroomingTestsTest.java @@ -0,0 +1,239 @@ +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.STRING; +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.auth.dto.request.GeneralLoginRequest; +import com.ftm.server.adapter.in.web.grooming.dto.request.GroomingTestSubmissionRequest; +import com.ftm.server.application.command.user.GeneralUserCreationCommand; +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.domain.entity.User; +import com.ftm.server.domain.entity.UserImage; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.mock.web.MockHttpSession; +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; +import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; +import org.springframework.restdocs.payload.FieldDescriptor; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.transaction.annotation.Transactional; + +public class SubmitGroomingTestsTest extends BaseTest { + + @Autowired private SecurityAuthenticationPort securityAuthenticationPort; + @Autowired private SaveUserPort saveUserPort; + @Autowired private SaveUserImagePort saveUserImagePort; + + private final List requestFieldSubmitGroomingTests = + List.of( + fieldWithPath("submissions").type(ARRAY).description("그루밍 테스트 제출 목록"), + fieldWithPath("submissions[].questionId").type(NUMBER).description("질문 ID"), + fieldWithPath("submissions[].groomingCategory") + .type(STRING) + .description("질문 그루밍 카테고리"), + fieldWithPath("submissions[].answers[]") + .type(ARRAY) + .description("질문에 답한 응답 목록"), + fieldWithPath("submissions[].answers[].answerId") + .type(NUMBER) + .description("응답 ID"), + fieldWithPath("submissions[].answers[].score") + .type(NUMBER) + .description("응답 점수")); + + private final List responseFieldSubmitGroomingTests = + List.of( + fieldWithPath("status").type(NUMBER).description("응답 상태"), + fieldWithPath("code").type(STRING).description("상태 코드"), + fieldWithPath("message").type(STRING).description("메시지"), + fieldWithPath("data").type(OBJECT).optional().description("응답 데이터"), + fieldWithPath("data.scores").type(OBJECT).description("그루밍 테스트 카테고리 별 결과 점수"), + fieldWithPath("data.scores.beautyScore").type(NUMBER).description("뷰티 영역 점수"), + fieldWithPath("data.scores.hygieneScore").type(NUMBER).description("위생 영역 점수"), + fieldWithPath("data.scores.hairScore").type(NUMBER).description("미용 영역 점수"), + fieldWithPath("data.scores.workoutScore").type(NUMBER).description("운동 영역 점수"), + fieldWithPath("data.scores.fashionScore").type(NUMBER).description("패션 영역 점수"), + fieldWithPath("data.scores.totalScore").type(NUMBER).description("총 점수"), + fieldWithPath("data.grades").type(OBJECT).description("그루밍 테스트 카테고리 별 결과 등급"), + fieldWithPath("data.grades.beauty").type(OBJECT).description("뷰티 영역 등급"), + fieldWithPath("data.grades.beauty.grade") + .type(STRING) + .description("뷰티 영역 등급 이름"), + fieldWithPath("data.grades.beauty.level") + .type(NUMBER) + .description("뷰티 영역 등급 레벨(1, 2, 3)"), + fieldWithPath("data.grades.hygiene").type(OBJECT).description("위생 영역 등급"), + fieldWithPath("data.grades.hygiene.grade") + .type(STRING) + .description("위생 영역 등급 이름"), + fieldWithPath("data.grades.hygiene.level") + .type(NUMBER) + .description("위생 영역 등급 레벨(1, 2, 3)"), + fieldWithPath("data.grades.hair").type(OBJECT).description("미용 영역 등급"), + fieldWithPath("data.grades.hair.grade").type(STRING).description("미용 영역 등급 이름"), + fieldWithPath("data.grades.hair.level") + .type(NUMBER) + .description("미용 영역 등급 레벨(1, 2, 3)"), + fieldWithPath("data.grades.workout").type(OBJECT).description("운동 영역 등급"), + fieldWithPath("data.grades.workout.grade") + .type(STRING) + .description("운동 영역 등급 이름"), + fieldWithPath("data.grades.workout.level") + .type(NUMBER) + .description("운동 영역 등급 레벨(1, 2, 3)"), + fieldWithPath("data.grades.fashion").type(OBJECT).description("패션 영역 등급"), + fieldWithPath("data.grades.fashion.grade") + .type(STRING) + .description("패션 영역 등급 이름"), + fieldWithPath("data.grades.fashion.level") + .type(NUMBER) + .description("패션 영역 등급 레벨(1, 2, 3)"), + fieldWithPath("data.level").type(OBJECT).description("그루밍 테스트 결과 레벨"), + fieldWithPath("data.level.groomingLevelId") + .type(NUMBER) + .description("그루밍 레벨 ID"), + fieldWithPath("data.level.mildLevelName") + .type(STRING) + .description("그루밍 레벨 순한맛 이름"), + fieldWithPath("data.level.spicyLevelName") + .type(STRING) + .description("그루밍 레벨 매운맛 이름")); + + 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))), + new GroomingTestSubmissionRequest.SubmittedQuestion( + 6L, + "HYGIENE", + List.of( + new GroomingTestSubmissionRequest.SubmittedQuestion + .SelectedAnswer(16L, 1))), + new GroomingTestSubmissionRequest.SubmittedQuestion( + 10L, + "HAIR", + List.of( + new GroomingTestSubmissionRequest.SubmittedQuestion + .SelectedAnswer(28L, 1))), + 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))), + new GroomingTestSubmissionRequest.SubmittedQuestion( + 18L, + "FASHION", + List.of( + new GroomingTestSubmissionRequest.SubmittedQuestion + .SelectedAnswer(61L, 4)))); + return new GroomingTestSubmissionRequest(submissions); + } + + private RestDocumentationResultHandler getDocument(Integer identifier) { + return document( + "submitGroomingTests/" + identifier, + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint(), getModifiedHeader()), + requestFields(requestFieldSubmitGroomingTests), + responseFields(responseFieldSubmitGroomingTests), + resource( + ResourceSnippetParameters.builder() + .tag("그루밍 테스트") + .summary("그루밍 테스트 제출 api") + .description("그루밍 테스트 제출 api 입니다.") + .responseFields(responseFieldSubmitGroomingTests) + .build())); + } + + @Test + @Transactional + void 그루밍_테스트_제출_성공1() throws Exception { + // given + 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())); + GroomingTestSubmissionRequest request = getRequest(); + + // when + MvcResult loginResult = + mockMvc.perform( + RestDocumentationRequestBuilders.post("/api/auth/login") + .contentType(APPLICATION_JSON_VALUE) + .content( + mapper.writeValueAsString( + new GeneralLoginRequest( + "test@gmail.com", "test1234!")))) + .andExpect(status().isOk()) + .andReturn(); + MockHttpSession session = (MockHttpSession) loginResult.getRequest().getSession(false); + + ResultActions resultActions = + mockMvc.perform( + RestDocumentationRequestBuilders.post("/api/grooming/tests/submission") + .contentType(APPLICATION_JSON_VALUE) + .content(mapper.writeValueAsString(request)) + .session(session)); + // then + resultActions.andExpect(status().isOk()).andDo(print()); + + // documentation + resultActions.andDo(getDocument(1)); + } + + @Test + @Transactional + void 그루밍_테스트_제출_성공2() throws Exception { + // given + // given + GroomingTestSubmissionRequest request = getRequest(); + + // when + ResultActions resultActions = + mockMvc.perform( + RestDocumentationRequestBuilders.post("/api/grooming/tests/submission") + .contentType(APPLICATION_JSON_VALUE) + .content(mapper.writeValueAsString(request))); + + // then + resultActions.andExpect(status().isOk()).andDo(print()); + + // documentation + resultActions.andDo(getDocument(2)); + } +}