From 8f5d86541e5850e4eac08813355b67d4f5fa558e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=84=B8=EC=A4=80?= <74056843+sejoon00@users.noreply.github.com> Date: Thu, 30 Jan 2025 03:39:01 +0900 Subject: [PATCH 1/5] =?UTF-8?q?[feat/#23]=20=EB=AC=B8=ED=95=AD=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1,=20=EC=88=98=EC=A0=95,=20=EC=A1=B0=ED=9A=8C=20api?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ConceptTagController.java | 29 ++ .../dto/response/ConceptTagResponse.java | 15 + .../repository/ConceptTagRepository.java | 14 + .../controller/ProblemGetController.java | 27 ++ .../controller/ProblemSaveController.java | 39 ++ .../domain/problem/domain/Answer.java | 45 ++ .../domain/problem/domain/ChildProblem.java | 35 -- .../domain/problem/domain/Problem.java | 63 --- .../domain/childProblem/ChildProblem.java | 71 ++++ .../domain/practiceTest/PracticeTest.java | 34 ++ .../problem/domain/practiceTest/Subject.java | 30 ++ .../problem/domain/problem/Problem.java | 105 +++++ .../problem/domain/problem/ProblemId.java | 18 + .../domain/problem/ProblemIdService.java | 47 +++ .../problem/domain/problem/ProblemType.java | 37 ++ .../request/ChildProblemDeleteRequest.java | 6 + .../dto/request/ChildProblemPostRequest.java | 13 + .../request/ChildProblemUpdateRequest.java | 14 + .../dto/request/ProblemPostRequest.java | 37 ++ .../dto/request/ProblemUpdateRequest.java | 21 + .../dto/response/ChildProblemGetResponse.java | 26 ++ .../dto/response/ProblemGetResponse.java | 42 ++ .../repository/ChildProblemRepository.java | 2 +- .../repository/PracticeTestTagRepository.java | 14 + .../problem/repository/ProblemRepository.java | 20 +- .../problem/service/ProblemGetService.java | 22 + .../problem/service/ProblemSaveService.java | 57 +++ .../DetailResultApplicationService.java | 6 +- .../v0/practiceTest/domain/PracticeTest.java | 15 +- .../v0/practiceTest/domain/RatingTable.java | 1 + .../v0/practiceTest/domain/Subject.java | 47 --- .../admin/request/PracticeTestRequest.java | 8 +- .../repository/PracticeTestRepository.java | 4 - ...ory.java => ProblemForTestRepository.java} | 2 +- .../admin/PracticeTestAdminService.java | 8 +- .../admin/ProblemImageUploadService.java | 11 +- .../service/client/PracticeTestService.java | 2 +- .../service/client/ProblemService.java | 18 +- .../exception/AlreadyExistException.java | 7 + .../global/error/exception/ErrorCode.java | 9 +- .../resources/templates/answerInputForm.html | 91 ++-- .../resources/templates/imageUploadPage.html | 8 +- .../resources/templates/practiceTestList.html | 106 ++--- .../resources/templates/testInputForm.html | 391 +++++++++--------- .../client/PracticeTestServiceTest.java | 4 +- .../scheduler/TestResultSchedulerTest.java | 11 +- 46 files changed, 1158 insertions(+), 474 deletions(-) create mode 100644 src/main/java/com/moplus/moplus_server/domain/concept/controller/ConceptTagController.java create mode 100644 src/main/java/com/moplus/moplus_server/domain/concept/dto/response/ConceptTagResponse.java create mode 100644 src/main/java/com/moplus/moplus_server/domain/problem/controller/ProblemGetController.java create mode 100644 src/main/java/com/moplus/moplus_server/domain/problem/controller/ProblemSaveController.java create mode 100644 src/main/java/com/moplus/moplus_server/domain/problem/domain/Answer.java delete mode 100644 src/main/java/com/moplus/moplus_server/domain/problem/domain/ChildProblem.java delete mode 100644 src/main/java/com/moplus/moplus_server/domain/problem/domain/Problem.java create mode 100644 src/main/java/com/moplus/moplus_server/domain/problem/domain/childProblem/ChildProblem.java create mode 100644 src/main/java/com/moplus/moplus_server/domain/problem/domain/practiceTest/PracticeTest.java create mode 100644 src/main/java/com/moplus/moplus_server/domain/problem/domain/practiceTest/Subject.java create mode 100644 src/main/java/com/moplus/moplus_server/domain/problem/domain/problem/Problem.java create mode 100644 src/main/java/com/moplus/moplus_server/domain/problem/domain/problem/ProblemId.java create mode 100644 src/main/java/com/moplus/moplus_server/domain/problem/domain/problem/ProblemIdService.java create mode 100644 src/main/java/com/moplus/moplus_server/domain/problem/domain/problem/ProblemType.java create mode 100644 src/main/java/com/moplus/moplus_server/domain/problem/dto/request/ChildProblemDeleteRequest.java create mode 100644 src/main/java/com/moplus/moplus_server/domain/problem/dto/request/ChildProblemPostRequest.java create mode 100644 src/main/java/com/moplus/moplus_server/domain/problem/dto/request/ChildProblemUpdateRequest.java create mode 100644 src/main/java/com/moplus/moplus_server/domain/problem/dto/request/ProblemPostRequest.java create mode 100644 src/main/java/com/moplus/moplus_server/domain/problem/dto/request/ProblemUpdateRequest.java create mode 100644 src/main/java/com/moplus/moplus_server/domain/problem/dto/response/ChildProblemGetResponse.java create mode 100644 src/main/java/com/moplus/moplus_server/domain/problem/dto/response/ProblemGetResponse.java create mode 100644 src/main/java/com/moplus/moplus_server/domain/problem/repository/PracticeTestTagRepository.java create mode 100644 src/main/java/com/moplus/moplus_server/domain/problem/service/ProblemGetService.java create mode 100644 src/main/java/com/moplus/moplus_server/domain/problem/service/ProblemSaveService.java delete mode 100644 src/main/java/com/moplus/moplus_server/domain/v0/practiceTest/domain/Subject.java rename src/main/java/com/moplus/moplus_server/domain/v0/practiceTest/repository/{ProblemRepository.java => ProblemForTestRepository.java} (92%) create mode 100644 src/main/java/com/moplus/moplus_server/global/error/exception/AlreadyExistException.java diff --git a/src/main/java/com/moplus/moplus_server/domain/concept/controller/ConceptTagController.java b/src/main/java/com/moplus/moplus_server/domain/concept/controller/ConceptTagController.java new file mode 100644 index 0000000..094e6ae --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/domain/concept/controller/ConceptTagController.java @@ -0,0 +1,29 @@ +package com.moplus.moplus_server.domain.concept.controller; + +import com.moplus.moplus_server.domain.concept.dto.response.ConceptTagResponse; +import com.moplus.moplus_server.domain.concept.repository.ConceptTagRepository; +import io.swagger.v3.oas.annotations.Operation; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v1/conceptTags") +@RequiredArgsConstructor +public class ConceptTagController { + + ConceptTagRepository conceptTagRepository; + + @GetMapping("") + @Operation(summary = "모든 개념 태그 리스트 조회") + public ResponseEntity> getConceptTags( + ) { + List responses = conceptTagRepository.findAll().stream() + .map(ConceptTagResponse::of) + .toList(); + return ResponseEntity.ok(responses); + } +} diff --git a/src/main/java/com/moplus/moplus_server/domain/concept/dto/response/ConceptTagResponse.java b/src/main/java/com/moplus/moplus_server/domain/concept/dto/response/ConceptTagResponse.java new file mode 100644 index 0000000..e92b490 --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/domain/concept/dto/response/ConceptTagResponse.java @@ -0,0 +1,15 @@ +package com.moplus.moplus_server.domain.concept.dto.response; + +import com.moplus.moplus_server.domain.concept.domain.ConceptTag; + +public record ConceptTagResponse( + Long id, + String name +) { + public static ConceptTagResponse of(ConceptTag entity) { + return new ConceptTagResponse( + entity.getId(), + entity.getName() + ); + } +} diff --git a/src/main/java/com/moplus/moplus_server/domain/concept/repository/ConceptTagRepository.java b/src/main/java/com/moplus/moplus_server/domain/concept/repository/ConceptTagRepository.java index df5bc5c..682a2f3 100644 --- a/src/main/java/com/moplus/moplus_server/domain/concept/repository/ConceptTagRepository.java +++ b/src/main/java/com/moplus/moplus_server/domain/concept/repository/ConceptTagRepository.java @@ -1,7 +1,21 @@ package com.moplus.moplus_server.domain.concept.repository; import com.moplus.moplus_server.domain.concept.domain.ConceptTag; +import com.moplus.moplus_server.global.error.exception.ErrorCode; +import com.moplus.moplus_server.global.error.exception.NotFoundException; +import java.util.List; +import java.util.Set; import org.springframework.data.jpa.repository.JpaRepository; public interface ConceptTagRepository extends JpaRepository { + + default void existsByIdElseThrow(Set ids) { + List foundIds = findAllById(ids).stream() + .map(ConceptTag::getId) // 엔티티의 ID 추출 + .toList(); + + if (ids.size() != foundIds.size()) { + throw new NotFoundException(ErrorCode.CONCEPT_TAG_NOT_FOUND_IN_LIST); + } + } } diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/controller/ProblemGetController.java b/src/main/java/com/moplus/moplus_server/domain/problem/controller/ProblemGetController.java new file mode 100644 index 0000000..bf465bf --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/domain/problem/controller/ProblemGetController.java @@ -0,0 +1,27 @@ +package com.moplus.moplus_server.domain.problem.controller; + +import com.moplus.moplus_server.domain.problem.dto.response.ProblemGetResponse; +import com.moplus.moplus_server.domain.problem.service.ProblemGetService; +import io.swagger.v3.oas.annotations.Operation; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v1/problems") +@RequiredArgsConstructor +public class ProblemGetController { + + private final ProblemGetService problemGetService; + + @GetMapping("/{id}") + @Operation(summary = "문항 조회", description = "문항를 조회합니다.") + public ResponseEntity getProblem( + @PathVariable("id") String id + ) { + return ResponseEntity.ok(problemGetService.getProblem(id)); + } +} diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/controller/ProblemSaveController.java b/src/main/java/com/moplus/moplus_server/domain/problem/controller/ProblemSaveController.java new file mode 100644 index 0000000..a190bad --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/domain/problem/controller/ProblemSaveController.java @@ -0,0 +1,39 @@ +package com.moplus.moplus_server.domain.problem.controller; + +import com.moplus.moplus_server.domain.problem.dto.request.ProblemPostRequest; +import com.moplus.moplus_server.domain.problem.dto.request.ProblemUpdateRequest; +import com.moplus.moplus_server.domain.problem.dto.response.ProblemGetResponse; +import com.moplus.moplus_server.domain.problem.service.ProblemSaveService; +import io.swagger.v3.oas.annotations.Operation; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v1/problems") +@RequiredArgsConstructor +public class ProblemSaveController { + + private final ProblemSaveService problemSaveService; + + @PostMapping("") + @Operation(summary = "문항 생성", description = "문제를 생성합니다. 새끼 문항은 list 순서대로 sequence를 저장합니다.") + public ResponseEntity createProblem( + @RequestBody ProblemPostRequest request + ) { + return ResponseEntity.ok(problemSaveService.createProblem(request).toString()); + } + + @PostMapping("/{id}") + @Operation(summary = "문항 업데이트", description = "문제를 업데이트합니다. 새끼 문항은 수정된 리스트, 새로 생성된 리스트, 삭제된 리스트가 필요합니다.") + public ResponseEntity updateProblem( + @PathVariable("id") String id, + @RequestBody ProblemUpdateRequest request + ) { + return ResponseEntity.ok(problemSaveService.updateProblem(id, request)); + } +} diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/domain/Answer.java b/src/main/java/com/moplus/moplus_server/domain/problem/domain/Answer.java new file mode 100644 index 0000000..2cb733a --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/domain/problem/domain/Answer.java @@ -0,0 +1,45 @@ +package com.moplus.moplus_server.domain.problem.domain; + +import com.moplus.moplus_server.domain.problem.domain.problem.ProblemType; +import com.moplus.moplus_server.global.error.exception.ErrorCode; +import com.moplus.moplus_server.global.error.exception.InvalidValueException; +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Embeddable +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Answer { + + @Column(name = "answer") + private String value; + + public Answer(String value, ProblemType problemType) { + validateByType(value, problemType); + this.value = value; + } + + private void validateByType(String answer, ProblemType problemType) { + if (answer.isBlank()) { + throw new InvalidValueException(ErrorCode.BLANK_INPUT_VALUE); + } + if (problemType == ProblemType.MULTIPLE_CHOICE) { + if (!answer.matches("^[1-5]*$")) { + throw new InvalidValueException(ErrorCode.INVALID_MULTIPLE_CHOICE_ANSWER); + } + } + if (problemType == ProblemType.SHORT_NUMBER_ANSWER) { + try { + int numericAnswer = Integer.parseInt(answer); + if (numericAnswer < 0 || numericAnswer > 999) { + throw new InvalidValueException(ErrorCode.INVALID_SHORT_NUMBER_ANSWER); + } + } catch (NumberFormatException e) { + throw new InvalidValueException(ErrorCode.INVALID_SHORT_NUMBER_ANSWER); + } + } + } +} diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/domain/ChildProblem.java b/src/main/java/com/moplus/moplus_server/domain/problem/domain/ChildProblem.java deleted file mode 100644 index 21291ef..0000000 --- a/src/main/java/com/moplus/moplus_server/domain/problem/domain/ChildProblem.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.moplus.moplus_server.domain.problem.domain; - -import jakarta.persistence.CollectionTable; -import jakarta.persistence.Column; -import jakarta.persistence.ElementCollection; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import java.util.Set; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Getter -@Entity -@NoArgsConstructor -public class ChildProblem { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "problem_id") - Long id; - @ElementCollection - @CollectionTable(name = "child_problem_concept", joinColumns = @JoinColumn(name = "concept_tag_id")) - Set conceptTagIds; - private String problemImageUrl; - private String answer; - - public ChildProblem(String problemImageUrl, String answer, Set conceptTagIds) { - this.problemImageUrl = problemImageUrl; - this.answer = answer; - this.conceptTagIds = conceptTagIds; - } -} diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/domain/Problem.java b/src/main/java/com/moplus/moplus_server/domain/problem/domain/Problem.java deleted file mode 100644 index fcaff94..0000000 --- a/src/main/java/com/moplus/moplus_server/domain/problem/domain/Problem.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.moplus.moplus_server.domain.problem.domain; - -import com.moplus.moplus_server.global.common.BaseEntity; -import jakarta.persistence.CascadeType; -import jakarta.persistence.CollectionTable; -import jakarta.persistence.Column; -import jakarta.persistence.ElementCollection; -import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.OneToMany; -import java.util.ArrayList; -import java.util.List; -import java.util.Set; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Getter -@Entity -@NoArgsConstructor -public class Problem extends BaseEntity { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "problem_id") - Long id; - - String practiceTestId; - int number; - int answer; - String comment; - String mainProblemImageUrl; - String mainAnalysisImageUrl; - String readingTipImageUrl; - String seniorTipImageUrl; - String prescriptionImageUrl; - - @ElementCollection - @CollectionTable(name = "problem_concept", joinColumns = @JoinColumn(name = "concept_tag_id")) - Set conceptTagIds; - - @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) - @JoinColumn(name = "problem_id") - private List childProblems = new ArrayList<>(); - - public Problem(String practiceTestId, int number, int answer, String comment, String mainProblemImageUrl, - String mainAnalysisImageUrl, String readingTipImageUrl, String seniorTipImageUrl, - String prescriptionImageUrl, Set conceptTagIds) { - this.practiceTestId = practiceTestId; - this.number = number; - this.answer = answer; - this.comment = comment; - this.mainProblemImageUrl = mainProblemImageUrl; - this.mainAnalysisImageUrl = mainAnalysisImageUrl; - this.readingTipImageUrl = readingTipImageUrl; - this.seniorTipImageUrl = seniorTipImageUrl; - this.prescriptionImageUrl = prescriptionImageUrl; - this.conceptTagIds = conceptTagIds; - } -} diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/domain/childProblem/ChildProblem.java b/src/main/java/com/moplus/moplus_server/domain/problem/domain/childProblem/ChildProblem.java new file mode 100644 index 0000000..7bbede9 --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/domain/problem/domain/childProblem/ChildProblem.java @@ -0,0 +1,71 @@ +package com.moplus.moplus_server.domain.problem.domain.childProblem; + +import com.moplus.moplus_server.domain.problem.domain.Answer; +import com.moplus.moplus_server.domain.problem.domain.problem.ProblemType; +import com.moplus.moplus_server.domain.problem.dto.request.ChildProblemUpdateRequest; +import com.moplus.moplus_server.global.common.BaseEntity; +import com.moplus.moplus_server.global.error.exception.ErrorCode; +import com.moplus.moplus_server.global.error.exception.InvalidValueException; +import jakarta.persistence.CollectionTable; +import jakarta.persistence.Column; +import jakarta.persistence.ElementCollection; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import java.util.Set; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Entity +@NoArgsConstructor +public class ChildProblem extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "problem_id") + Long id; + @ElementCollection + @CollectionTable(name = "child_problem_concept", joinColumns = @JoinColumn(name = "concept_tag_id")) + Set conceptTagIds; + private String imageUrl; + @Embedded + private Answer answer; + private ProblemType problemType; + private int sequence; + + @Builder + public ChildProblem(String imageUrl, ProblemType problemType, String answer, Set conceptTagIds, + int sequence) { + validateAnswerByType(answer, problemType); + this.imageUrl = imageUrl; + this.problemType = problemType; + this.answer = new Answer(answer, problemType); + this.conceptTagIds = conceptTagIds; + this.sequence = sequence; + } + + public void validateAnswerByType(String answer, ProblemType problemType) { + if (this.problemType == ProblemType.MULTIPLE_CHOICE) { + if (!answer.matches("^[1-5]*$")) { + throw new InvalidValueException(ErrorCode.INVALID_MULTIPLE_CHOICE_ANSWER); + } + } + } + + public void update(ChildProblemUpdateRequest request) { + this.imageUrl = request.imageUrl(); + this.problemType = request.problemType(); + this.answer = new Answer(request.answer(), request.problemType()); + this.conceptTagIds = request.conceptTagIds(); + this.sequence = request.sequence(); + } + + public String getAnswer() { + return answer.getValue(); + } +} diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/domain/practiceTest/PracticeTest.java b/src/main/java/com/moplus/moplus_server/domain/problem/domain/practiceTest/PracticeTest.java new file mode 100644 index 0000000..f2f064f --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/domain/problem/domain/practiceTest/PracticeTest.java @@ -0,0 +1,34 @@ +package com.moplus.moplus_server.domain.problem.domain.practiceTest; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Entity +@Table(name = "practice_test_tag") +@NoArgsConstructor +public class PracticeTest { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String name; + private int year; + private int month; + private Subject subject; + private String area; + + public PracticeTest(String name, int year, int month, Subject subject) { + this.name = name; + this.year = year; + this.month = month; + this.subject = subject; + this.area = "수학"; + } +} diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/domain/practiceTest/Subject.java b/src/main/java/com/moplus/moplus_server/domain/problem/domain/practiceTest/Subject.java new file mode 100644 index 0000000..9dd4f16 --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/domain/problem/domain/practiceTest/Subject.java @@ -0,0 +1,30 @@ +package com.moplus.moplus_server.domain.problem.domain.practiceTest; + + +import java.util.Arrays; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum Subject { + + 고1("고1", 30, 100, 1), + 고2("고2", 30, 100, 2), + 미적분("미적분", 30, 100, 3), + 기하("기하", 30, 100, 4), + 확률과통계("확률과통계", 30, 100, 5), + ; + + private final String value; + private final int problemCount; + private final int perfectScore; + private final int idCode; + + public static Subject fromValue(String value) { + return Arrays.stream(Subject.values()) + .filter(subject -> subject.value.equals(value)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("해당 값에 맞는 Subject가 없습니다: " + value)); + } +} diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/domain/problem/Problem.java b/src/main/java/com/moplus/moplus_server/domain/problem/domain/problem/Problem.java new file mode 100644 index 0000000..b72ceb8 --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/domain/problem/domain/problem/Problem.java @@ -0,0 +1,105 @@ +package com.moplus.moplus_server.domain.problem.domain.problem; + +import com.moplus.moplus_server.domain.problem.domain.Answer; +import com.moplus.moplus_server.domain.problem.domain.childProblem.ChildProblem; +import com.moplus.moplus_server.domain.problem.domain.practiceTest.PracticeTest; +import com.moplus.moplus_server.domain.problem.dto.request.ChildProblemPostRequest; +import com.moplus.moplus_server.domain.problem.dto.request.ChildProblemUpdateRequest; +import com.moplus.moplus_server.global.common.BaseEntity; +import jakarta.persistence.CascadeType; +import jakarta.persistence.CollectionTable; +import jakarta.persistence.ElementCollection; +import jakarta.persistence.Embedded; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OrderBy; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Entity +@NoArgsConstructor +public class Problem extends BaseEntity { + + @EmbeddedId + ProblemId id; + + Long practiceTestId; + int number; + @Embedded + Answer answer; + String comment; + String mainProblemImageUrl; + String mainAnalysisImageUrl; + String readingTipImageUrl; + String seniorTipImageUrl; + String prescriptionImageUrl; + @ElementCollection + @CollectionTable(name = "problem_concept", joinColumns = @JoinColumn(name = "concept_tag_id")) + Set conceptTagIds; + private ProblemType problemType; + private boolean isPublished; + private boolean isModified; + + @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) + @JoinColumn(name = "problem_id") + @OrderBy("sequence ASC") + private List childProblems = new ArrayList<>(); + + @Builder + public Problem(ProblemId id, PracticeTest practiceTest, int number, String answer, String comment, + String mainProblemImageUrl, + String mainAnalysisImageUrl, String readingTipImageUrl, String seniorTipImageUrl, + String prescriptionImageUrl, ProblemType problemType, Set conceptTagIds, + List childProblems) { + this.id = id; + this.practiceTestId = practiceTest.getId(); + this.number = number; + this.comment = comment; + this.mainProblemImageUrl = mainProblemImageUrl; + this.mainAnalysisImageUrl = mainAnalysisImageUrl; + this.readingTipImageUrl = readingTipImageUrl; + this.seniorTipImageUrl = seniorTipImageUrl; + this.prescriptionImageUrl = prescriptionImageUrl; + this.problemType = ProblemType.getTypeForProblem(practiceTest.getSubject().getValue(), number); + this.answer = new Answer(answer, this.problemType); + this.conceptTagIds = conceptTagIds; + this.childProblems = childProblems; + this.isPublished = false; + this.isModified = false; + } + + public String getAnswer() { + return answer.getValue(); + } + + public void addChildProblem(ChildProblemPostRequest request) { + ChildProblem childProblem = ChildProblem.builder() + .imageUrl(request.imageUrl()) + .problemType(request.problemType()) + .answer(request.answer()) + .conceptTagIds(request.conceptTagIds()) + .sequence(request.sequence()) + .build(); + childProblems.add(request.sequence(), childProblem); + } + + public void updateChildProblem(ChildProblemUpdateRequest request) { + childProblems.get(request.sequence()).update(request); + } + + public void deleteChildProblem(Long childProblemId) { + childProblems.forEach(childProblem -> { + if (childProblem.getId().equals(childProblemId)) { + childProblems.remove(childProblem); + } + }); + } +} diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/domain/problem/ProblemId.java b/src/main/java/com/moplus/moplus_server/domain/problem/domain/problem/ProblemId.java new file mode 100644 index 0000000..847b283 --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/domain/problem/domain/problem/ProblemId.java @@ -0,0 +1,18 @@ +package com.moplus.moplus_server.domain.problem.domain.problem; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import java.io.Serializable; +import lombok.NoArgsConstructor; + +@Embeddable +@NoArgsConstructor +public class ProblemId implements Serializable { + + @Column(name = "problem_id") + private String id; + + public ProblemId(String id) { + this.id = id; + } +} diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/domain/problem/ProblemIdService.java b/src/main/java/com/moplus/moplus_server/domain/problem/domain/problem/ProblemIdService.java new file mode 100644 index 0000000..e5daf59 --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/domain/problem/domain/problem/ProblemIdService.java @@ -0,0 +1,47 @@ +package com.moplus.moplus_server.domain.problem.domain.problem; + +import com.moplus.moplus_server.domain.problem.domain.practiceTest.PracticeTest; +import com.moplus.moplus_server.domain.problem.repository.ProblemRepository; +import java.util.concurrent.atomic.AtomicInteger; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class ProblemIdService { + + private static final AtomicInteger SEQUENCE = new AtomicInteger(1); // XXX 값 증가를 위한 카운터 + private final ProblemRepository problemRepository; + + /* + 문제 ID 생성 로직 + AA : 과목 ( 1: 수학, 2: 영어, 3: 국어, 4: 사회, 5: 과학 ) + S : ( 1: 고1, 2: 고2, 3: 미적분, 4: 기하, 5: 확률과 통계, 6: 가형, 7: 나형 ) + YY: 년도 (두 자리) + MM: 월 (두 자리) + NN : 번호 (01~99) + C : 변형 여부 ( 0: 기본, 1: 변형 ) + XXX : 3자리 구분 숫자 + */ + public ProblemId nextId(int number, PracticeTest practiceTest) { + + int DEFAULT_AREA = 1; //현재 영역은 수학밖에 없음 + int subject = practiceTest.getSubject().getIdCode(); // AA (과목) + int year = practiceTest.getYear() % 100; // YY (두 자리 연도) + int month = practiceTest.getMonth(); // MM (두 자리 월) + int DEFAULT_MODIFIED = 0; // 변형 여부 (0: 기본, 1: 변형) + + String generatedId; + int sequence; + + // 중복되지 않는 ID 찾을 때까지 반복 + do { + sequence = SEQUENCE.getAndIncrement() % 1000; // 000~999 순환 + generatedId = String.format("%02d%d%02d%02d%02d%d%03d", + DEFAULT_AREA, subject, year, month, + number, DEFAULT_MODIFIED, sequence); + } while (problemRepository.existsById(new ProblemId(generatedId))); // ID가 이미 존재하면 재생성 + + return new ProblemId(generatedId); + } +} diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/domain/problem/ProblemType.java b/src/main/java/com/moplus/moplus_server/domain/problem/domain/problem/ProblemType.java new file mode 100644 index 0000000..2410bcf --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/domain/problem/domain/problem/ProblemType.java @@ -0,0 +1,37 @@ +package com.moplus.moplus_server.domain.problem.domain.problem; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public enum ProblemType { + MULTIPLE_CHOICE("객관식"), + SHORT_NUMBER_ANSWER("주관식_숫자"), + SHORT_STRING_ANSWER("주관식_문자"); + + + private final String name; + + public static ProblemType getTypeForProblem(String subject, int number) { + + // 미적분, 기하, 확률과 통계 + if (subject.equals("미적분") || subject.equals("기하") || subject.equals("확률과통계")) { + if ((number >= 1 && number <= 15) || (number >= 23 && number <= 28)) { + return MULTIPLE_CHOICE; + } else if ((number >= 16 && number <= 22) || number == 29 || number == 30) { + return SHORT_NUMBER_ANSWER; + } + } + + // 고1, 고2 + if (subject.equals("고1") || subject.equals("고2")) { + if (number >= 1 && number <= 21) { + return MULTIPLE_CHOICE; + } else if (number >= 22 && number <= 30) { + return SHORT_NUMBER_ANSWER; + } + } + + // 기본값: 객관식 + return MULTIPLE_CHOICE; + } +} diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/dto/request/ChildProblemDeleteRequest.java b/src/main/java/com/moplus/moplus_server/domain/problem/dto/request/ChildProblemDeleteRequest.java new file mode 100644 index 0000000..07615cb --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/domain/problem/dto/request/ChildProblemDeleteRequest.java @@ -0,0 +1,6 @@ +package com.moplus.moplus_server.domain.problem.dto.request; + +public record ChildProblemDeleteRequest( + Long childProblemId +) { +} diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/dto/request/ChildProblemPostRequest.java b/src/main/java/com/moplus/moplus_server/domain/problem/dto/request/ChildProblemPostRequest.java new file mode 100644 index 0000000..1b42975 --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/domain/problem/dto/request/ChildProblemPostRequest.java @@ -0,0 +1,13 @@ +package com.moplus.moplus_server.domain.problem.dto.request; + +import com.moplus.moplus_server.domain.problem.domain.problem.ProblemType; +import java.util.Set; + +public record ChildProblemPostRequest( + String imageUrl, + ProblemType problemType, + String answer, + Set conceptTagIds, + int sequence +) { +} diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/dto/request/ChildProblemUpdateRequest.java b/src/main/java/com/moplus/moplus_server/domain/problem/dto/request/ChildProblemUpdateRequest.java new file mode 100644 index 0000000..f8c5f35 --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/domain/problem/dto/request/ChildProblemUpdateRequest.java @@ -0,0 +1,14 @@ +package com.moplus.moplus_server.domain.problem.dto.request; + +import com.moplus.moplus_server.domain.problem.domain.problem.ProblemType; +import java.util.Set; + +public record ChildProblemUpdateRequest( + Long id, + String imageUrl, + ProblemType problemType, + String answer, + Set conceptTagIds, + int sequence +) { +} diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/dto/request/ProblemPostRequest.java b/src/main/java/com/moplus/moplus_server/domain/problem/dto/request/ProblemPostRequest.java new file mode 100644 index 0000000..3af1adc --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/domain/problem/dto/request/ProblemPostRequest.java @@ -0,0 +1,37 @@ +package com.moplus.moplus_server.domain.problem.dto.request; + +import com.moplus.moplus_server.domain.problem.domain.practiceTest.PracticeTest; +import com.moplus.moplus_server.domain.problem.domain.problem.Problem; +import com.moplus.moplus_server.domain.problem.domain.problem.ProblemId; +import java.util.List; +import java.util.Set; + +public record ProblemPostRequest( + Set conceptTagIds, + Long practiceTestId, + int number, + String answer, + String comment, + String mainProblemImageUrl, + String mainAnalysisImageUrl, + String readingTipImageUrl, + String seniorTipImageUrl, + String prescriptionImageUrl, + List childProblems +) { + public Problem toEntity(PracticeTest practiceTest, ProblemId problemId) { + return Problem.builder() + .id(problemId) + .conceptTagIds(conceptTagIds) + .practiceTest(practiceTest) + .number(number) + .answer(answer) + .comment(comment) + .mainProblemImageUrl(mainProblemImageUrl) + .mainAnalysisImageUrl(mainAnalysisImageUrl) + .readingTipImageUrl(readingTipImageUrl) + .seniorTipImageUrl(seniorTipImageUrl) + .prescriptionImageUrl(prescriptionImageUrl) + .build(); + } +} diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/dto/request/ProblemUpdateRequest.java b/src/main/java/com/moplus/moplus_server/domain/problem/dto/request/ProblemUpdateRequest.java new file mode 100644 index 0000000..b1a462b --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/domain/problem/dto/request/ProblemUpdateRequest.java @@ -0,0 +1,21 @@ +package com.moplus.moplus_server.domain.problem.dto.request; + +import java.util.List; +import java.util.Set; + +public record ProblemUpdateRequest( + Set conceptTagIds, + Long practiceTestId, + int number, + int answer, + String comment, + String mainProblemImageUrl, + String mainAnalysisImageUrl, + String readingTipImageUrl, + String seniorTipImageUrl, + String prescriptionImageUrl, + List updateChildProblems, + List createChildProblems, + List deleteChildProblems +) { +} diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/dto/response/ChildProblemGetResponse.java b/src/main/java/com/moplus/moplus_server/domain/problem/dto/response/ChildProblemGetResponse.java new file mode 100644 index 0000000..1641d2c --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/domain/problem/dto/response/ChildProblemGetResponse.java @@ -0,0 +1,26 @@ +package com.moplus.moplus_server.domain.problem.dto.response; + +import com.moplus.moplus_server.domain.problem.domain.childProblem.ChildProblem; +import com.moplus.moplus_server.domain.problem.domain.problem.ProblemType; +import java.util.Set; +import lombok.Builder; + +@Builder +public record ChildProblemGetResponse( + Long childProblemId, + String imageUrl, + ProblemType problemType, + String answer, + Set conceptTagIds +) { + + public static ChildProblemGetResponse of(ChildProblem childProblem) { + return ChildProblemGetResponse.builder() + .childProblemId(childProblem.getId()) + .imageUrl(childProblem.getImageUrl()) + .problemType(childProblem.getProblemType()) + .answer(childProblem.getAnswer()) + .conceptTagIds(childProblem.getConceptTagIds()) + .build(); + } +} diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/dto/response/ProblemGetResponse.java b/src/main/java/com/moplus/moplus_server/domain/problem/dto/response/ProblemGetResponse.java new file mode 100644 index 0000000..1947067 --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/domain/problem/dto/response/ProblemGetResponse.java @@ -0,0 +1,42 @@ +package com.moplus.moplus_server.domain.problem.dto.response; + +import com.moplus.moplus_server.domain.problem.domain.problem.Problem; +import java.util.List; +import java.util.Set; +import lombok.Builder; + +@Builder +public record ProblemGetResponse( + String problemId, + Set conceptTagIds, + Long practiceTestId, + int number, + String answer, + String comment, + String mainProblemImageUrl, + String mainAnalysisImageUrl, + String readingTipImageUrl, + String seniorTipImageUrl, + String prescriptionImageUrl, + List childProblems +) { + + public static ProblemGetResponse of(Problem problem) { + return ProblemGetResponse.builder() + .problemId(problem.getId().toString()) + .conceptTagIds(problem.getConceptTagIds()) + .practiceTestId(problem.getPracticeTestId()) + .number(problem.getNumber()) + .answer(problem.getAnswer()) + .comment(problem.getComment()) + .mainProblemImageUrl(problem.getMainProblemImageUrl()) + .mainAnalysisImageUrl(problem.getMainAnalysisImageUrl()) + .readingTipImageUrl(problem.getReadingTipImageUrl()) + .seniorTipImageUrl(problem.getSeniorTipImageUrl()) + .prescriptionImageUrl(problem.getPrescriptionImageUrl()) + .childProblems(problem.getChildProblems().stream() + .map(ChildProblemGetResponse::of) + .toList()) + .build(); + } +} diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/repository/ChildProblemRepository.java b/src/main/java/com/moplus/moplus_server/domain/problem/repository/ChildProblemRepository.java index a27fe22..be781ed 100644 --- a/src/main/java/com/moplus/moplus_server/domain/problem/repository/ChildProblemRepository.java +++ b/src/main/java/com/moplus/moplus_server/domain/problem/repository/ChildProblemRepository.java @@ -1,6 +1,6 @@ package com.moplus.moplus_server.domain.problem.repository; -import com.moplus.moplus_server.domain.problem.domain.ChildProblem; +import com.moplus.moplus_server.domain.problem.domain.childProblem.ChildProblem; import org.springframework.data.jpa.repository.JpaRepository; public interface ChildProblemRepository extends JpaRepository { diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/repository/PracticeTestTagRepository.java b/src/main/java/com/moplus/moplus_server/domain/problem/repository/PracticeTestTagRepository.java new file mode 100644 index 0000000..9e5a514 --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/domain/problem/repository/PracticeTestTagRepository.java @@ -0,0 +1,14 @@ +package com.moplus.moplus_server.domain.problem.repository; + +import com.moplus.moplus_server.domain.problem.domain.practiceTest.PracticeTest; +import com.moplus.moplus_server.global.error.exception.ErrorCode; +import com.moplus.moplus_server.global.error.exception.NotFoundException; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface PracticeTestTagRepository extends JpaRepository { + + default PracticeTest findByIdElseThrow(Long id) { + return findById(id) + .orElseThrow(() -> new NotFoundException(ErrorCode.PRACTICE_TEST_NOT_FOUND)); + } +} diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/repository/ProblemRepository.java b/src/main/java/com/moplus/moplus_server/domain/problem/repository/ProblemRepository.java index 84c1f1f..99d0d05 100644 --- a/src/main/java/com/moplus/moplus_server/domain/problem/repository/ProblemRepository.java +++ b/src/main/java/com/moplus/moplus_server/domain/problem/repository/ProblemRepository.java @@ -1,7 +1,23 @@ package com.moplus.moplus_server.domain.problem.repository; -import com.moplus.moplus_server.domain.problem.domain.Problem; +import com.moplus.moplus_server.domain.problem.domain.problem.Problem; +import com.moplus.moplus_server.domain.problem.domain.problem.ProblemId; +import com.moplus.moplus_server.global.error.exception.AlreadyExistException; +import com.moplus.moplus_server.global.error.exception.ErrorCode; +import com.moplus.moplus_server.global.error.exception.NotFoundException; import org.springframework.data.jpa.repository.JpaRepository; -public interface ProblemRepository extends JpaRepository { +public interface ProblemRepository extends JpaRepository { + + boolean existsByPracticeTestIdAndNumber(Long practiceTestId, int number); + + default void existsByPracticeTestIdAndNumberOrThrow(Long practiceTestId, int number) { + if (existsByPracticeTestIdAndNumber(practiceTestId, number)) { + throw new AlreadyExistException(ErrorCode.PROBLEM_ALREADY_EXIST); + } + } + + default Problem findByIdElseThrow(ProblemId problemId) { + return findById(problemId).orElseThrow(() -> new NotFoundException(ErrorCode.PROBLEM_NOT_FOUND)); + } } diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/service/ProblemGetService.java b/src/main/java/com/moplus/moplus_server/domain/problem/service/ProblemGetService.java new file mode 100644 index 0000000..65dccf1 --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/domain/problem/service/ProblemGetService.java @@ -0,0 +1,22 @@ +package com.moplus.moplus_server.domain.problem.service; + +import com.moplus.moplus_server.domain.problem.domain.problem.Problem; +import com.moplus.moplus_server.domain.problem.domain.problem.ProblemId; +import com.moplus.moplus_server.domain.problem.dto.response.ProblemGetResponse; +import com.moplus.moplus_server.domain.problem.repository.ProblemRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class ProblemGetService { + + private final ProblemRepository problemRepository; + + @Transactional(readOnly = true) + public ProblemGetResponse getProblem(String problemId) { + Problem problem = problemRepository.findByIdElseThrow(new ProblemId(problemId)); + return ProblemGetResponse.of(problem); + } +} diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/service/ProblemSaveService.java b/src/main/java/com/moplus/moplus_server/domain/problem/service/ProblemSaveService.java new file mode 100644 index 0000000..4182002 --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/domain/problem/service/ProblemSaveService.java @@ -0,0 +1,57 @@ +package com.moplus.moplus_server.domain.problem.service; + +import com.moplus.moplus_server.domain.concept.repository.ConceptTagRepository; +import com.moplus.moplus_server.domain.problem.domain.practiceTest.PracticeTest; +import com.moplus.moplus_server.domain.problem.domain.problem.Problem; +import com.moplus.moplus_server.domain.problem.domain.problem.ProblemId; +import com.moplus.moplus_server.domain.problem.domain.problem.ProblemIdService; +import com.moplus.moplus_server.domain.problem.dto.request.ChildProblemDeleteRequest; +import com.moplus.moplus_server.domain.problem.dto.request.ProblemPostRequest; +import com.moplus.moplus_server.domain.problem.dto.request.ProblemUpdateRequest; +import com.moplus.moplus_server.domain.problem.dto.response.ProblemGetResponse; +import com.moplus.moplus_server.domain.problem.repository.PracticeTestTagRepository; +import com.moplus.moplus_server.domain.problem.repository.ProblemRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class ProblemSaveService { + + private final ProblemRepository problemRepository; + private final PracticeTestTagRepository practiceTestRepository; + private final ConceptTagRepository conceptTagRepository; + private final ProblemIdService problemIdService; + + @Transactional + public ProblemId createProblem(ProblemPostRequest request) { + PracticeTest practiceTest = practiceTestRepository.findByIdElseThrow(request.practiceTestId()); + problemRepository.existsByPracticeTestIdAndNumberOrThrow(practiceTest.getId(), request.number()); + conceptTagRepository.existsByIdElseThrow(request.conceptTagIds()); + + ProblemId problemId = problemIdService.nextId(request.number(), practiceTest); + Problem problem = request.toEntity(practiceTest, problemId); + request.childProblems() + .forEach(problem::addChildProblem); + + return problemRepository.save(problem).getId(); + } + + @Transactional + public ProblemGetResponse updateProblem(String problemId, ProblemUpdateRequest request) { + PracticeTest practiceTest = practiceTestRepository.findByIdElseThrow(request.practiceTestId()); + problemRepository.existsByPracticeTestIdAndNumberOrThrow(practiceTest.getId(), request.number()); + conceptTagRepository.existsByIdElseThrow(request.conceptTagIds()); + + Problem problem = problemRepository.findByIdElseThrow(new ProblemId(problemId)); + request.deleteChildProblems().stream() + .map(ChildProblemDeleteRequest::childProblemId) + .forEach(problem::deleteChildProblem); + request.createChildProblems().forEach(problem::addChildProblem); + request.updateChildProblems().forEach(problem::updateChildProblem); + + Problem savedProblem = problemRepository.save(problem); + return ProblemGetResponse.of(savedProblem); + } +} diff --git a/src/main/java/com/moplus/moplus_server/domain/v0/DetailResultApplication/service/DetailResultApplicationService.java b/src/main/java/com/moplus/moplus_server/domain/v0/DetailResultApplication/service/DetailResultApplicationService.java index 085cfad..1382c1f 100644 --- a/src/main/java/com/moplus/moplus_server/domain/v0/DetailResultApplication/service/DetailResultApplicationService.java +++ b/src/main/java/com/moplus/moplus_server/domain/v0/DetailResultApplication/service/DetailResultApplicationService.java @@ -14,7 +14,7 @@ import com.moplus.moplus_server.domain.v0.practiceTest.domain.PracticeTest; import com.moplus.moplus_server.domain.v0.practiceTest.domain.ProblemForTest; import com.moplus.moplus_server.domain.v0.practiceTest.repository.PracticeTestRepository; -import com.moplus.moplus_server.domain.v0.practiceTest.repository.ProblemRepository; +import com.moplus.moplus_server.domain.v0.practiceTest.repository.ProblemForTestRepository; import com.moplus.moplus_server.global.error.exception.ErrorCode; import com.moplus.moplus_server.global.error.exception.NotFoundException; import java.time.Duration; @@ -33,7 +33,7 @@ public class DetailResultApplicationService { private final EstimatedRatingRepository estimatedRatingRepository; private final IncorrectProblemService incorrectProblemService; private final IncorrectProblemRepository incorrectProblemRepository; - private final ProblemRepository problemRepository; + private final ProblemForTestRepository problemForTestRepository; @Transactional public void saveApplication(DetailResultApplicationPostRequest request) { @@ -57,7 +57,7 @@ public ReviewNoteGetResponse getReviewNoteInfo(Long testResultId) { List incorrectProblemForTests = incorrectProblemRepository.findAllByTestResultId(testResultId) .stream() .map(IncorrectProblem::getProblemId) - .map(problemId -> problemRepository.findById(problemId).orElseThrow()) + .map(problemId -> problemForTestRepository.findById(problemId).orElseThrow()) .toList(); List forCurrentRating = incorrectProblemForTests.stream() diff --git a/src/main/java/com/moplus/moplus_server/domain/v0/practiceTest/domain/PracticeTest.java b/src/main/java/com/moplus/moplus_server/domain/v0/practiceTest/domain/PracticeTest.java index d40720a..92be481 100644 --- a/src/main/java/com/moplus/moplus_server/domain/v0/practiceTest/domain/PracticeTest.java +++ b/src/main/java/com/moplus/moplus_server/domain/v0/practiceTest/domain/PracticeTest.java @@ -1,5 +1,6 @@ package com.moplus.moplus_server.domain.v0.practiceTest.domain; +import com.moplus.moplus_server.domain.problem.domain.practiceTest.Subject; import com.moplus.moplus_server.domain.v0.practiceTest.dto.admin.request.PracticeTestRequest; import com.moplus.moplus_server.global.common.BaseEntity; import jakarta.persistence.Column; @@ -30,7 +31,8 @@ public class PracticeTest extends BaseEntity { private long viewCount = 0L; private int solvesCount = 0; - private String publicationYear; + private int publicationYear; + private int month = 0; @Enumerated(EnumType.STRING) private Subject subject; @@ -38,15 +40,18 @@ public class PracticeTest extends BaseEntity { private Duration averageSolvingTime = Duration.ZERO; @Builder - public PracticeTest(String name, String round, String provider, String publicationYear, Subject subject) { + public PracticeTest(String name, String round, String provider, long viewCount, int solvesCount, + int publicationYear, + int month, Subject subject, Duration averageSolvingTime) { this.name = name; this.round = round; this.provider = provider; - this.viewCount = 0; - this.solvesCount = 0; + this.viewCount = viewCount; + this.solvesCount = solvesCount; this.publicationYear = publicationYear; + this.month = month; this.subject = subject; - this.averageSolvingTime = Duration.ZERO; + this.averageSolvingTime = averageSolvingTime; } public void updateByPracticeTestRequest(PracticeTestRequest request) { diff --git a/src/main/java/com/moplus/moplus_server/domain/v0/practiceTest/domain/RatingTable.java b/src/main/java/com/moplus/moplus_server/domain/v0/practiceTest/domain/RatingTable.java index 611885e..5ac79c2 100644 --- a/src/main/java/com/moplus/moplus_server/domain/v0/practiceTest/domain/RatingTable.java +++ b/src/main/java/com/moplus/moplus_server/domain/v0/practiceTest/domain/RatingTable.java @@ -1,5 +1,6 @@ package com.moplus.moplus_server.domain.v0.practiceTest.domain; +import com.moplus.moplus_server.domain.problem.domain.practiceTest.Subject; import com.moplus.moplus_server.domain.v0.practiceTest.dto.admin.request.RatingTableRequest; import com.moplus.moplus_server.domain.v0.practiceTest.repository.converter.RatingRowConverter; import com.moplus.moplus_server.global.common.BaseEntity; diff --git a/src/main/java/com/moplus/moplus_server/domain/v0/practiceTest/domain/Subject.java b/src/main/java/com/moplus/moplus_server/domain/v0/practiceTest/domain/Subject.java deleted file mode 100644 index 5b460b4..0000000 --- a/src/main/java/com/moplus/moplus_server/domain/v0/practiceTest/domain/Subject.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.moplus.moplus_server.domain.v0.practiceTest.domain; - - -import java.util.Arrays; -import lombok.AllArgsConstructor; -import lombok.Getter; - -@Getter -@AllArgsConstructor -public enum Subject { - - // 화법과작문("화법과작문", 45, 100), -// 언어와매체("언어와매체", 45, 100), - 고1("고1", 30, 100), - 고2("고2", 30, 100), - 미적분("미적분", 30, 100), - 확률과통계("확률과통계", 30, 100), - 기하("기하", 30, 100); -// 영어("영어",45, 100), -// 물리I("물리I",20, 50), -// 화학I("화학I",20, 50), -// 생명과학I("생명과학I",20, 50), -// 지구과학I("지구과학I",20, 50), -// 물리II("물리II",20, 50), -// 화학II("화학II",20, 50), -// 생명과학II("생명과학II",20, 50), -// 지구과학II("지구과학II",20, 50), -// 한국지리("한국지리",20, 50), -// 세계지리("세계지리",20, 50), -// 동아시아사("동아시아사",20, 50), -// 생활과윤리("생활과윤리",20, 50), -// 윤리와사상("윤리와사상",20, 50), -// 사회문화("사회문화",20, 50), -// 정치와법("정치와법",20, 50), -// 경제("경제",20, 50); - - private final String value; - private final int problemCount; - private final int perfectScore; - - public static Subject fromValue(String value) { - return Arrays.stream(Subject.values()) - .filter(subject -> subject.value.equals(value)) - .findFirst() - .orElseThrow(() -> new IllegalArgumentException("해당 값에 맞는 Subject가 없습니다: " + value)); - } -} diff --git a/src/main/java/com/moplus/moplus_server/domain/v0/practiceTest/dto/admin/request/PracticeTestRequest.java b/src/main/java/com/moplus/moplus_server/domain/v0/practiceTest/dto/admin/request/PracticeTestRequest.java index b2c4a5e..b27fb95 100644 --- a/src/main/java/com/moplus/moplus_server/domain/v0/practiceTest/dto/admin/request/PracticeTestRequest.java +++ b/src/main/java/com/moplus/moplus_server/domain/v0/practiceTest/dto/admin/request/PracticeTestRequest.java @@ -1,7 +1,7 @@ package com.moplus.moplus_server.domain.v0.practiceTest.dto.admin.request; +import com.moplus.moplus_server.domain.problem.domain.practiceTest.Subject; import com.moplus.moplus_server.domain.v0.practiceTest.domain.PracticeTest; -import com.moplus.moplus_server.domain.v0.practiceTest.domain.Subject; import java.util.ArrayList; import java.util.List; import lombok.Getter; @@ -16,11 +16,11 @@ public class PracticeTestRequest { private String name; private String round; private String provider; - private String publicationYear; + private int publicationYear; private String subject; private List ratingTables = new ArrayList<>(); - public PracticeTestRequest(Long id, String name, String round, String provider, String publicationYear, + public PracticeTestRequest(Long id, String name, String round, String provider, int publicationYear, String subject, List ratingTables) { this.id = id; this.name = name; @@ -51,7 +51,7 @@ public static PracticeTestRequest getUpdateModelObject(PracticeTest practiceTest } public static PracticeTestRequest getCreateModelObject() { - return new PracticeTestRequest(null, "", "", "", "", null, RatingTableRequest.getDefaultRatingTableRequest()); + return new PracticeTestRequest(null, "", "", "", 0, null, RatingTableRequest.getDefaultRatingTableRequest()); } public PracticeTest toEntity() { diff --git a/src/main/java/com/moplus/moplus_server/domain/v0/practiceTest/repository/PracticeTestRepository.java b/src/main/java/com/moplus/moplus_server/domain/v0/practiceTest/repository/PracticeTestRepository.java index 948dc4e..fc3b4f9 100644 --- a/src/main/java/com/moplus/moplus_server/domain/v0/practiceTest/repository/PracticeTestRepository.java +++ b/src/main/java/com/moplus/moplus_server/domain/v0/practiceTest/repository/PracticeTestRepository.java @@ -14,8 +14,4 @@ public interface PracticeTestRepository extends JpaRepository { +public interface ProblemForTestRepository extends JpaRepository { List findAllByPracticeTestId(Long id); diff --git a/src/main/java/com/moplus/moplus_server/domain/v0/practiceTest/service/admin/PracticeTestAdminService.java b/src/main/java/com/moplus/moplus_server/domain/v0/practiceTest/service/admin/PracticeTestAdminService.java index 4260692..e2d7aab 100644 --- a/src/main/java/com/moplus/moplus_server/domain/v0/practiceTest/service/admin/PracticeTestAdminService.java +++ b/src/main/java/com/moplus/moplus_server/domain/v0/practiceTest/service/admin/PracticeTestAdminService.java @@ -1,12 +1,12 @@ package com.moplus.moplus_server.domain.v0.practiceTest.service.admin; +import com.moplus.moplus_server.domain.problem.domain.practiceTest.Subject; import com.moplus.moplus_server.domain.v0.practiceTest.domain.PracticeTest; import com.moplus.moplus_server.domain.v0.practiceTest.domain.RatingTable; -import com.moplus.moplus_server.domain.v0.practiceTest.domain.Subject; import com.moplus.moplus_server.domain.v0.practiceTest.dto.admin.request.PracticeTestRequest; import com.moplus.moplus_server.domain.v0.practiceTest.dto.admin.request.RatingTableRequest; import com.moplus.moplus_server.domain.v0.practiceTest.repository.PracticeTestRepository; -import com.moplus.moplus_server.domain.v0.practiceTest.repository.ProblemRepository; +import com.moplus.moplus_server.domain.v0.practiceTest.repository.ProblemForTestRepository; import com.moplus.moplus_server.domain.v0.practiceTest.repository.RatingTableRepository; import com.moplus.moplus_server.global.error.exception.ErrorCode; import com.moplus.moplus_server.global.error.exception.NotFoundException; @@ -22,7 +22,7 @@ public class PracticeTestAdminService { private final PracticeTestRepository practiceTestRepository; private final RatingTableRepository ratingTableRepository; - private final ProblemRepository problemRepository; + private final ProblemForTestRepository problemForTestRepository; private final RatingTableAdminService ratingTableAdminService; private static void addToPracticeTestUpdateModel(Model model, List ratingTables, @@ -63,7 +63,7 @@ public void getPracticeTestUpdateModel(Model model, Long id) { @Transactional public void deletePracticeTest(Long id) { ratingTableRepository.deleteAllByPracticeTestId(id); - problemRepository.deleteAllByPracticeTestId(id); + problemForTestRepository.deleteAllByPracticeTestId(id); practiceTestRepository.deleteById(id); } diff --git a/src/main/java/com/moplus/moplus_server/domain/v0/practiceTest/service/admin/ProblemImageUploadService.java b/src/main/java/com/moplus/moplus_server/domain/v0/practiceTest/service/admin/ProblemImageUploadService.java index affbdff..411f1e1 100644 --- a/src/main/java/com/moplus/moplus_server/domain/v0/practiceTest/service/admin/ProblemImageUploadService.java +++ b/src/main/java/com/moplus/moplus_server/domain/v0/practiceTest/service/admin/ProblemImageUploadService.java @@ -6,8 +6,8 @@ import com.moplus.moplus_server.domain.v0.practiceTest.domain.ProblemImageForTest; import com.moplus.moplus_server.domain.v0.practiceTest.dto.admin.request.ProblemImageRequest; import com.moplus.moplus_server.domain.v0.practiceTest.repository.PracticeTestRepository; +import com.moplus.moplus_server.domain.v0.practiceTest.repository.ProblemForTestRepository; import com.moplus.moplus_server.domain.v0.practiceTest.repository.ProblemImageRepository; -import com.moplus.moplus_server.domain.v0.practiceTest.repository.ProblemRepository; import com.moplus.moplus_server.global.error.exception.ErrorCode; import com.moplus.moplus_server.global.error.exception.NotFoundException; import com.moplus.moplus_server.global.utils.s3.S3Util; @@ -26,12 +26,13 @@ public class ProblemImageUploadService { private final S3Util s3Util; private final PracticeTestRepository practiceTestRepository; - private final ProblemRepository problemRepository; + private final ProblemForTestRepository problemForTestRepository; private final ProblemImageRepository problemImageRepository; @Transactional(readOnly = true) public void setProblemImagesByPracticeTestId(Long practiceTestId, Model model) { - List imageRequests = problemRepository.findAllByPracticeTestId(practiceTestId).stream() + List imageRequests = problemForTestRepository.findAllByPracticeTestId(practiceTestId) + .stream() .map(ProblemImageRequest::of) .toList(); model.addAttribute("problemImageRequests", imageRequests); @@ -41,7 +42,7 @@ public void setProblemImagesByPracticeTestId(Long practiceTestId, Model model) { public void uploadImage(Long practiceId, Long problemId, MultipartFile image) { PracticeTest practiceTest = practiceTestRepository.findById(practiceId) .orElseThrow(() -> new NotFoundException(ErrorCode.PRACTICE_TEST_NOT_FOUND)); - ProblemForTest problemForTest = problemRepository.findById(problemId) + ProblemForTest problemForTest = problemForTestRepository.findById(problemId) .orElseThrow(() -> new NotFoundException(ErrorCode.PROBLEM_NOT_FOUND)); String fileName = uploadFile(image, problemId, practiceTest.getName()); String s3ObjectUrl = s3Util.getS3ObjectUrl(fileName); @@ -52,7 +53,7 @@ public void uploadImage(Long practiceId, Long problemId, MultipartFile image) { .build(); ProblemImageForTest saved = problemImageRepository.save(problemImageForTest); problemForTest.addImage(saved); - problemRepository.save(problemForTest); + problemForTestRepository.save(problemForTest); } public String uploadFile(MultipartFile file, Long problemId, String practiceTestName) { diff --git a/src/main/java/com/moplus/moplus_server/domain/v0/practiceTest/service/client/PracticeTestService.java b/src/main/java/com/moplus/moplus_server/domain/v0/practiceTest/service/client/PracticeTestService.java index 8a3a9a6..9f99f8a 100644 --- a/src/main/java/com/moplus/moplus_server/domain/v0/practiceTest/service/client/PracticeTestService.java +++ b/src/main/java/com/moplus/moplus_server/domain/v0/practiceTest/service/client/PracticeTestService.java @@ -1,7 +1,7 @@ package com.moplus.moplus_server.domain.v0.practiceTest.service.client; -import com.moplus.moplus_server.domain.v0.practiceTest.dto.client.response.PracticeTestGetResponse; import com.moplus.moplus_server.domain.v0.practiceTest.domain.PracticeTest; +import com.moplus.moplus_server.domain.v0.practiceTest.dto.client.response.PracticeTestGetResponse; import com.moplus.moplus_server.domain.v0.practiceTest.repository.PracticeTestRepository; import com.moplus.moplus_server.global.error.exception.ErrorCode; import com.moplus.moplus_server.global.error.exception.NotFoundException; diff --git a/src/main/java/com/moplus/moplus_server/domain/v0/practiceTest/service/client/ProblemService.java b/src/main/java/com/moplus/moplus_server/domain/v0/practiceTest/service/client/ProblemService.java index b22ff8b..e5560a6 100644 --- a/src/main/java/com/moplus/moplus_server/domain/v0/practiceTest/service/client/ProblemService.java +++ b/src/main/java/com/moplus/moplus_server/domain/v0/practiceTest/service/client/ProblemService.java @@ -4,7 +4,7 @@ import com.moplus.moplus_server.domain.v0.practiceTest.domain.ProblemForTest; import com.moplus.moplus_server.domain.v0.practiceTest.dto.admin.request.ProblemCreateRequest; import com.moplus.moplus_server.domain.v0.practiceTest.dto.client.response.ProblemGetResponse; -import com.moplus.moplus_server.domain.v0.practiceTest.repository.ProblemRepository; +import com.moplus.moplus_server.domain.v0.practiceTest.repository.ProblemForTestRepository; import com.moplus.moplus_server.global.error.exception.ErrorCode; import com.moplus.moplus_server.global.error.exception.NotFoundException; import jakarta.servlet.http.HttpServletRequest; @@ -18,7 +18,7 @@ @RequiredArgsConstructor public class ProblemService { - private final ProblemRepository problemRepository; + private final ProblemForTestRepository problemForTestRepository; @Transactional public void saveProblems(PracticeTest practiceTest, HttpServletRequest request) { @@ -41,12 +41,12 @@ public void saveProblems(PracticeTest practiceTest, HttpServletRequest request) .toList(); problemsEntities .forEach(ProblemForTest::calculateProblemRating); - problemRepository.saveAll(problemsEntities); + problemForTestRepository.saveAll(problemsEntities); } @Transactional public void updateProblems(PracticeTest practiceTest, HttpServletRequest request) { - List problemForTests = problemRepository.findAllByPracticeTestId(practiceTest.getId()); + List problemForTests = problemForTestRepository.findAllByPracticeTestId(practiceTest.getId()); for (int i = 1; i <= practiceTest.getSubject().getProblemCount(); i++) { ProblemForTest problemForTest = problemForTests.get(i - 1); @@ -55,29 +55,29 @@ public void updateProblems(PracticeTest practiceTest, HttpServletRequest request problemForTest.updateCorrectRate(Double.parseDouble(request.getParameter("correctRate_" + i))); problemForTest.calculateProblemRating(); - problemRepository.save(problemForTest); + problemForTestRepository.save(problemForTest); } } public ProblemForTest getProblemByPracticeTestIdAndNumber(Long practiceId, String problemNumber) { - return problemRepository.findByProblemNumberAndPracticeTestId(problemNumber, practiceId) + return problemForTestRepository.findByProblemNumberAndPracticeTestId(problemNumber, practiceId) .orElseThrow(() -> new NotFoundException(ErrorCode.PROBLEM_NOT_FOUND)); } @Transactional public ProblemForTest updateCorrectRate(Long practiceTestId, String problemNumber, double correctRate) { - ProblemForTest problemForTest = problemRepository.findByProblemNumberAndPracticeTestIdWithPessimisticLock( + ProblemForTest problemForTest = problemForTestRepository.findByProblemNumberAndPracticeTestIdWithPessimisticLock( problemNumber, practiceTestId) .orElseThrow(() -> new NotFoundException(ErrorCode.PROBLEM_NOT_FOUND)); problemForTest.getPracticeTest(); problemForTest.updateCorrectRate(correctRate); - return problemRepository.save(problemForTest); + return problemForTestRepository.save(problemForTest); } public List getProblemsByTestId(Long testId) { - return problemRepository.findAllByPracticeTestId(testId).stream() + return problemForTestRepository.findAllByPracticeTestId(testId).stream() .map(ProblemGetResponse::from) .toList(); } diff --git a/src/main/java/com/moplus/moplus_server/global/error/exception/AlreadyExistException.java b/src/main/java/com/moplus/moplus_server/global/error/exception/AlreadyExistException.java new file mode 100644 index 0000000..31f1405 --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/global/error/exception/AlreadyExistException.java @@ -0,0 +1,7 @@ +package com.moplus.moplus_server.global.error.exception; + +public class AlreadyExistException extends BusinessException { + public AlreadyExistException(ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/src/main/java/com/moplus/moplus_server/global/error/exception/ErrorCode.java b/src/main/java/com/moplus/moplus_server/global/error/exception/ErrorCode.java index b8c892f..3968ed9 100644 --- a/src/main/java/com/moplus/moplus_server/global/error/exception/ErrorCode.java +++ b/src/main/java/com/moplus/moplus_server/global/error/exception/ErrorCode.java @@ -10,6 +10,7 @@ public enum ErrorCode { INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버 오류, 관리자에게 문의하세요"), INVALID_INPUT_VALUE(HttpStatus.BAD_REQUEST, "잘못된 입력 값입니다"), BAD_CREDENTIALS(HttpStatus.UNAUTHORIZED, "잘못된 인증 정보입니다"), + BLANK_INPUT_VALUE(HttpStatus.BAD_REQUEST, "빈 값이 입력되었습니다"), //Auth AUTH_NOT_FOUND(HttpStatus.UNAUTHORIZED, "시큐리티 인증 정보를 찾을 수 없습니다."), @@ -26,8 +27,14 @@ public enum ErrorCode { //모의고사 PRACTICE_TEST_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 모의고사를 찾을 수 없습니다"), - //문제 + //문항 PROBLEM_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 문제를 찾을 수 없습니다"), + PROBLEM_ALREADY_EXIST(HttpStatus.CONFLICT, "해당 문제는 이미 존재합니다"), + INVALID_MULTIPLE_CHOICE_ANSWER(HttpStatus.BAD_REQUEST, "객관식 문제의 정답은 1~5 사이의 숫자여야 합니다"), + INVALID_SHORT_NUMBER_ANSWER(HttpStatus.BAD_REQUEST, "주관식 문제의 정답은 0~999 사이의 숫자여야 합니다"), + + //개념태그 + CONCEPT_TAG_NOT_FOUND_IN_LIST(HttpStatus.NOT_FOUND, "해당 리스트 중 존재하지 않는 개념 태그가 있습니다."), //시험결과 TEST_RESULT_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 시험 결과지를 찾을 수 없습니다"), diff --git a/src/main/resources/templates/answerInputForm.html b/src/main/resources/templates/answerInputForm.html index bf73a3f..6fd3d02 100644 --- a/src/main/resources/templates/answerInputForm.html +++ b/src/main/resources/templates/answerInputForm.html @@ -8,7 +8,7 @@ + function confirmDeletion(event) { + event.preventDefault(); + const confirmDelete = confirm("정말 삭제하시겠습니까?"); + if (confirmDelete) { + event.target.closest("form").submit(); + } + } +
- + - + -
    -
  • -
    -
    - 모의고사 이미지 -
    - -
    - - -
    -
    -
  • -
+
    +
  • +
    +
    + 모의고사 이미지 +
    + +
    + + +
    +
    +
  • +
diff --git a/src/main/resources/templates/testInputForm.html b/src/main/resources/templates/testInputForm.html index 1e08913..1666a8a 100644 --- a/src/main/resources/templates/testInputForm.html +++ b/src/main/resources/templates/testInputForm.html @@ -1,210 +1,219 @@ - Practice Test 등록 - - + + - /* 표 스타일 */ - table { - width: 100%; - border-collapse: collapse; - margin-bottom: 20px; - } +

Practice Test 등록

- table, th, td { - border: 1px solid #ccc; - } +
+
+ + +
- th, td { - padding: 8px; - text-align: center; - } +
+ + +
- th { - background-color: #f3f3f3; - } - - - +
+ + +
-

Practice Test 등록

+
+ + +
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- -
-
- - - -
+
+ +
+
+ + + +
+
-
- - -
-
- + + +
+
+ +
-
- + diff --git a/src/test/java/com/moplus/moplus_server/domain/practiceTest/service/client/PracticeTestServiceTest.java b/src/test/java/com/moplus/moplus_server/domain/practiceTest/service/client/PracticeTestServiceTest.java index a39a9cd..5a639ed 100644 --- a/src/test/java/com/moplus/moplus_server/domain/practiceTest/service/client/PracticeTestServiceTest.java +++ b/src/test/java/com/moplus/moplus_server/domain/practiceTest/service/client/PracticeTestServiceTest.java @@ -4,7 +4,7 @@ import com.moplus.moplus_server.domain.v0.practiceTest.domain.PracticeTest; import com.moplus.moplus_server.domain.v0.practiceTest.repository.PracticeTestRepository; -import com.moplus.moplus_server.domain.v0.practiceTest.repository.ProblemRepository; +import com.moplus.moplus_server.domain.v0.practiceTest.repository.ProblemForTestRepository; import com.moplus.moplus_server.domain.v0.practiceTest.service.client.PracticeTestService; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; @@ -25,7 +25,7 @@ class PracticeTestServiceTest { @Autowired private PracticeTestRepository practiceTestRepository; @Autowired - private ProblemRepository problemRepository; + private ProblemForTestRepository problemForTestRepository; @BeforeEach void setup() { diff --git a/src/test/java/com/moplus/moplus_server/global/scheduler/TestResultSchedulerTest.java b/src/test/java/com/moplus/moplus_server/global/scheduler/TestResultSchedulerTest.java index bd942a5..67d5298 100644 --- a/src/test/java/com/moplus/moplus_server/global/scheduler/TestResultSchedulerTest.java +++ b/src/test/java/com/moplus/moplus_server/global/scheduler/TestResultSchedulerTest.java @@ -1,24 +1,23 @@ package com.moplus.moplus_server.global.scheduler; +import static org.mockito.Mockito.when; + +import com.moplus.moplus_server.domain.problem.domain.practiceTest.Subject; import com.moplus.moplus_server.domain.v0.TestResult.entity.TestResult; import com.moplus.moplus_server.domain.v0.TestResult.repository.TestResultRepository; import com.moplus.moplus_server.domain.v0.practiceTest.domain.PracticeTest; -import com.moplus.moplus_server.domain.v0.practiceTest.domain.Subject; import com.moplus.moplus_server.domain.v0.practiceTest.repository.PracticeTestRepository; import java.lang.reflect.Field; +import java.time.Duration; +import java.util.List; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; - -import java.time.Duration; -import java.util.List; import org.mockito.junit.jupiter.MockitoExtension; -import static org.mockito.Mockito.*; - @ExtendWith(MockitoExtension.class) class TestResultSchedulerTest { From 43c8501f50d6f1b5f9e2d3bdbc5026f55596ba75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=84=B8=EC=A4=80?= <74056843+sejoon00@users.noreply.github.com> Date: Thu, 30 Jan 2025 21:13:20 +0900 Subject: [PATCH 2/5] =?UTF-8?q?[feat/#23]=20mapStruct=EB=A5=BC=20=ED=86=B5?= =?UTF-8?q?=ED=95=9C=20dto=20->=20=EA=B0=9D=EC=B2=B4=20mapper=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20=EB=B0=8F=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/PracticeTestTagController.java | 2 + ...Controller.java => ProblemController.java} | 0 .../controller/ProblemGetController.java | 27 ------ ...PracticeTest.java => PracticeTestTag.java} | 0 .../dto/response/PracticeTestTagResponse.java | 2 + .../problem/service/ProblemDeleteService.java | 2 + .../problem/service/ProblemUpdateService.java | 2 + .../service/mapper/ChildProblemMapper.java | 14 +++ .../problem/service/mapper/ProblemMapper.java | 26 ++++++ .../global/scheduler/TestResultScheduler.java | 59 ------------- .../client/PracticeTestServiceTest.java | 58 ------------ .../domain/problem/ProblemIdServiceTest.java | 4 + .../service/ProblemSaveServiceTest.java | 4 + .../service/ProblemUpdateServiceTest.java | 4 + .../scheduler/TestResultSchedulerTest.java | 88 ------------------- src/test/resources/concept-tag.sql | 0 src/test/resources/insert-problem.sql | 0 src/test/resources/practice-test-tag.sql | 2 + 18 files changed, 62 insertions(+), 232 deletions(-) create mode 100644 src/main/java/com/moplus/moplus_server/domain/problem/controller/PracticeTestTagController.java rename src/main/java/com/moplus/moplus_server/domain/problem/controller/{ProblemSaveController.java => ProblemController.java} (100%) delete mode 100644 src/main/java/com/moplus/moplus_server/domain/problem/controller/ProblemGetController.java rename src/main/java/com/moplus/moplus_server/domain/problem/domain/practiceTest/{PracticeTest.java => PracticeTestTag.java} (100%) create mode 100644 src/main/java/com/moplus/moplus_server/domain/problem/dto/response/PracticeTestTagResponse.java create mode 100644 src/main/java/com/moplus/moplus_server/domain/problem/service/ProblemDeleteService.java create mode 100644 src/main/java/com/moplus/moplus_server/domain/problem/service/ProblemUpdateService.java create mode 100644 src/main/java/com/moplus/moplus_server/domain/problem/service/mapper/ChildProblemMapper.java create mode 100644 src/main/java/com/moplus/moplus_server/domain/problem/service/mapper/ProblemMapper.java delete mode 100644 src/main/java/com/moplus/moplus_server/global/scheduler/TestResultScheduler.java delete mode 100644 src/test/java/com/moplus/moplus_server/domain/practiceTest/service/client/PracticeTestServiceTest.java create mode 100644 src/test/java/com/moplus/moplus_server/domain/problem/domain/problem/ProblemIdServiceTest.java create mode 100644 src/test/java/com/moplus/moplus_server/domain/problem/service/ProblemSaveServiceTest.java create mode 100644 src/test/java/com/moplus/moplus_server/domain/problem/service/ProblemUpdateServiceTest.java delete mode 100644 src/test/java/com/moplus/moplus_server/global/scheduler/TestResultSchedulerTest.java create mode 100644 src/test/resources/concept-tag.sql create mode 100644 src/test/resources/insert-problem.sql create mode 100644 src/test/resources/practice-test-tag.sql diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/controller/PracticeTestTagController.java b/src/main/java/com/moplus/moplus_server/domain/problem/controller/PracticeTestTagController.java new file mode 100644 index 0000000..d630855 --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/domain/problem/controller/PracticeTestTagController.java @@ -0,0 +1,2 @@ +package com.moplus.moplus_server.domain.problem.controller;public class PracticeTestTagController { +} diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/controller/ProblemSaveController.java b/src/main/java/com/moplus/moplus_server/domain/problem/controller/ProblemController.java similarity index 100% rename from src/main/java/com/moplus/moplus_server/domain/problem/controller/ProblemSaveController.java rename to src/main/java/com/moplus/moplus_server/domain/problem/controller/ProblemController.java diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/controller/ProblemGetController.java b/src/main/java/com/moplus/moplus_server/domain/problem/controller/ProblemGetController.java deleted file mode 100644 index bf465bf..0000000 --- a/src/main/java/com/moplus/moplus_server/domain/problem/controller/ProblemGetController.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.moplus.moplus_server.domain.problem.controller; - -import com.moplus.moplus_server.domain.problem.dto.response.ProblemGetResponse; -import com.moplus.moplus_server.domain.problem.service.ProblemGetService; -import io.swagger.v3.oas.annotations.Operation; -import lombok.RequiredArgsConstructor; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequestMapping("/api/v1/problems") -@RequiredArgsConstructor -public class ProblemGetController { - - private final ProblemGetService problemGetService; - - @GetMapping("/{id}") - @Operation(summary = "문항 조회", description = "문항를 조회합니다.") - public ResponseEntity getProblem( - @PathVariable("id") String id - ) { - return ResponseEntity.ok(problemGetService.getProblem(id)); - } -} diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/domain/practiceTest/PracticeTest.java b/src/main/java/com/moplus/moplus_server/domain/problem/domain/practiceTest/PracticeTestTag.java similarity index 100% rename from src/main/java/com/moplus/moplus_server/domain/problem/domain/practiceTest/PracticeTest.java rename to src/main/java/com/moplus/moplus_server/domain/problem/domain/practiceTest/PracticeTestTag.java diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/dto/response/PracticeTestTagResponse.java b/src/main/java/com/moplus/moplus_server/domain/problem/dto/response/PracticeTestTagResponse.java new file mode 100644 index 0000000..a623b73 --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/domain/problem/dto/response/PracticeTestTagResponse.java @@ -0,0 +1,2 @@ +package com.moplus.moplus_server.domain.problem.dto.response;public record PracticeTestTagResponse() { +} diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/service/ProblemDeleteService.java b/src/main/java/com/moplus/moplus_server/domain/problem/service/ProblemDeleteService.java new file mode 100644 index 0000000..3d465ab --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/domain/problem/service/ProblemDeleteService.java @@ -0,0 +1,2 @@ +package com.moplus.moplus_server.domain.problem.service;public class ProblemDeleteService { +} diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/service/ProblemUpdateService.java b/src/main/java/com/moplus/moplus_server/domain/problem/service/ProblemUpdateService.java new file mode 100644 index 0000000..fc078cc --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/domain/problem/service/ProblemUpdateService.java @@ -0,0 +1,2 @@ +package com.moplus.moplus_server.domain.problem.service;public class ProblemUpdateService { +} diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/service/mapper/ChildProblemMapper.java b/src/main/java/com/moplus/moplus_server/domain/problem/service/mapper/ChildProblemMapper.java new file mode 100644 index 0000000..35da321 --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/domain/problem/service/mapper/ChildProblemMapper.java @@ -0,0 +1,14 @@ +package com.moplus.moplus_server.domain.problem.service; + +import com.moplus.moplus_server.domain.problem.domain.childProblem.ChildProblem; +import com.moplus.moplus_server.domain.problem.dto.request.ChildProblemPostRequest; +import com.moplus.moplus_server.domain.problem.dto.request.ChildProblemUpdateRequest; +import org.mapstruct.Mapper; + +@Mapper(componentModel = "spring") +public interface ChildProblemMapper { + + ChildProblem from(ChildProblemPostRequest request); + + ChildProblem from(ChildProblemUpdateRequest request); +} diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/service/mapper/ProblemMapper.java b/src/main/java/com/moplus/moplus_server/domain/problem/service/mapper/ProblemMapper.java new file mode 100644 index 0000000..882c71f --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/domain/problem/service/mapper/ProblemMapper.java @@ -0,0 +1,26 @@ +package com.moplus.moplus_server.domain.problem.service; + +import com.moplus.moplus_server.domain.problem.domain.practiceTest.PracticeTestTag; +import com.moplus.moplus_server.domain.problem.domain.problem.Problem; +import com.moplus.moplus_server.domain.problem.domain.problem.ProblemId; +import com.moplus.moplus_server.domain.problem.dto.request.ProblemPostRequest; +import com.moplus.moplus_server.domain.problem.dto.request.ProblemUpdateRequest; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Mappings; + +@Mapper(componentModel = "spring") +public interface ProblemMapper { + + @Mappings({ + @Mapping(target = "id", source = "problemId"), + @Mapping(target = "practiceTestTag", source = "practiceTestTag"), + }) + Problem from(ProblemPostRequest request, ProblemId problemId, PracticeTestTag practiceTestTag); + + @Mappings({ + @Mapping(target = "id", source = "problemId"), + @Mapping(target = "practiceTestTag", source = "practiceTestTag"), + }) + Problem from(ProblemUpdateRequest request, ProblemId problemId, PracticeTestTag practiceTestTag); +} diff --git a/src/main/java/com/moplus/moplus_server/global/scheduler/TestResultScheduler.java b/src/main/java/com/moplus/moplus_server/global/scheduler/TestResultScheduler.java deleted file mode 100644 index 4a453ae..0000000 --- a/src/main/java/com/moplus/moplus_server/global/scheduler/TestResultScheduler.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.moplus.moplus_server.global.scheduler; - -import com.moplus.moplus_server.domain.v0.TestResult.entity.TestResult; -import com.moplus.moplus_server.domain.v0.TestResult.repository.TestResultRepository; -import com.moplus.moplus_server.domain.v0.practiceTest.domain.PracticeTest; -import com.moplus.moplus_server.domain.v0.practiceTest.repository.PracticeTestRepository; -import java.time.Duration; -import java.util.List; -import lombok.RequiredArgsConstructor; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Component; - -@Component -@RequiredArgsConstructor -public class TestResultScheduler { - - private final PracticeTestRepository practiceTestRepository; - private final TestResultRepository testResultRepository; - - - // 5분마다 실행 (cron 표현식을 사용해 5분마다 스케줄링) - @Scheduled(cron = "0 */5 * * * *") - public void calculateAverageSolvingTime() { - List practiceTests = practiceTestRepository.findAll(); - - for (PracticeTest practiceTest : practiceTests) { - if (practiceTest.getSolvesCount() == 0) { - continue; - } - - Duration sum = Duration.ZERO; - List allByPracticeTestId = - testResultRepository.findAllByPracticeTestId(practiceTest.getId()); - - long validCount = 0; - - for (TestResult testResult : allByPracticeTestId) { - Duration solvingTime = testResult.getSolvingTime(); - - // solvingTime이 null이거나 0초일 경우는 제외 - if (solvingTime != null && !solvingTime.isZero()) { - sum = sum.plus(solvingTime); // Duration 객체는 불변이므로 새로운 객체로 할당 - validCount++; // 유효한 solvingTime이 있을 때만 카운트 증가 - } - } - - if (validCount > 0) { - // 유효한 solvingTime이 있는 경우만 평균 계산 - Duration average = sum.dividedBy(validCount); - - // 초 단위까지 포함한 average를 저장 - practiceTest.updateAverageSolvingTime(average); - practiceTestRepository.save(practiceTest); - } - System.out.println( - "평균 풀이 시간 계산 완료 : " + practiceTest.getId() + "L, 평균 시간 " + practiceTest.getAverageSolvingTime()); - } - } -} diff --git a/src/test/java/com/moplus/moplus_server/domain/practiceTest/service/client/PracticeTestServiceTest.java b/src/test/java/com/moplus/moplus_server/domain/practiceTest/service/client/PracticeTestServiceTest.java deleted file mode 100644 index 5a639ed..0000000 --- a/src/test/java/com/moplus/moplus_server/domain/practiceTest/service/client/PracticeTestServiceTest.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.moplus.moplus_server.domain.practiceTest.service.client; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import com.moplus.moplus_server.domain.v0.practiceTest.domain.PracticeTest; -import com.moplus.moplus_server.domain.v0.practiceTest.repository.PracticeTestRepository; -import com.moplus.moplus_server.domain.v0.practiceTest.repository.ProblemForTestRepository; -import com.moplus.moplus_server.domain.v0.practiceTest.service.client.PracticeTestService; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; - -@SpringBootTest -@ActiveProfiles("h2test") -class PracticeTestServiceTest { - - @Autowired - private PracticeTestService practiceTestService; - - @Autowired - private PracticeTestRepository practiceTestRepository; - @Autowired - private ProblemForTestRepository problemForTestRepository; - - @BeforeEach - void setup() { - PracticeTest practiceTest = new PracticeTest(); - practiceTestRepository.save(practiceTest); - } - - @Test - public void 동시에_조회수가_정상적으로_업데이트_되어야한다() throws InterruptedException { - Long practiceTestId = 1L; - int threadCount = 100; - ExecutorService executorService = Executors.newFixedThreadPool(36); - CountDownLatch countDownLatch = new CountDownLatch(threadCount); - - for (int i = 0; i < threadCount; i++) { - executorService.submit(() -> { - try { - practiceTestService.updateViewCount(practiceTestId); - } finally { - countDownLatch.countDown(); - } - }); - } - countDownLatch.await(); - - PracticeTest practiceTest = practiceTestRepository.findById(practiceTestId).orElseThrow(); - assertEquals(threadCount, practiceTest.getViewCount()); - } - -} \ No newline at end of file diff --git a/src/test/java/com/moplus/moplus_server/domain/problem/domain/problem/ProblemIdServiceTest.java b/src/test/java/com/moplus/moplus_server/domain/problem/domain/problem/ProblemIdServiceTest.java new file mode 100644 index 0000000..30e0157 --- /dev/null +++ b/src/test/java/com/moplus/moplus_server/domain/problem/domain/problem/ProblemIdServiceTest.java @@ -0,0 +1,4 @@ +import static org.junit.jupiter.api.Assertions.*; +class ProblemIdServiceTest { + +} \ No newline at end of file diff --git a/src/test/java/com/moplus/moplus_server/domain/problem/service/ProblemSaveServiceTest.java b/src/test/java/com/moplus/moplus_server/domain/problem/service/ProblemSaveServiceTest.java new file mode 100644 index 0000000..2fcce6b --- /dev/null +++ b/src/test/java/com/moplus/moplus_server/domain/problem/service/ProblemSaveServiceTest.java @@ -0,0 +1,4 @@ +import static org.junit.jupiter.api.Assertions.*; +class ProblemSaveServiceTest { + +} \ No newline at end of file diff --git a/src/test/java/com/moplus/moplus_server/domain/problem/service/ProblemUpdateServiceTest.java b/src/test/java/com/moplus/moplus_server/domain/problem/service/ProblemUpdateServiceTest.java new file mode 100644 index 0000000..3231e35 --- /dev/null +++ b/src/test/java/com/moplus/moplus_server/domain/problem/service/ProblemUpdateServiceTest.java @@ -0,0 +1,4 @@ +import static org.junit.jupiter.api.Assertions.*; +class ProblemUpdateServiceTest { + +} \ No newline at end of file diff --git a/src/test/java/com/moplus/moplus_server/global/scheduler/TestResultSchedulerTest.java b/src/test/java/com/moplus/moplus_server/global/scheduler/TestResultSchedulerTest.java deleted file mode 100644 index 67d5298..0000000 --- a/src/test/java/com/moplus/moplus_server/global/scheduler/TestResultSchedulerTest.java +++ /dev/null @@ -1,88 +0,0 @@ -package com.moplus.moplus_server.global.scheduler; - -import static org.mockito.Mockito.when; - -import com.moplus.moplus_server.domain.problem.domain.practiceTest.Subject; -import com.moplus.moplus_server.domain.v0.TestResult.entity.TestResult; -import com.moplus.moplus_server.domain.v0.TestResult.repository.TestResultRepository; -import com.moplus.moplus_server.domain.v0.practiceTest.domain.PracticeTest; -import com.moplus.moplus_server.domain.v0.practiceTest.repository.PracticeTestRepository; -import java.lang.reflect.Field; -import java.time.Duration; -import java.util.List; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -@ExtendWith(MockitoExtension.class) -class TestResultSchedulerTest { - - @Mock - private PracticeTestRepository practiceTestRepository; - - @Mock - private TestResultRepository testResultRepository; - - @InjectMocks - private TestResultScheduler testResultScheduler; // calculateAverageSolvingTime 메서드를 가진 클래스 - - private PracticeTest practiceTest; - private TestResult testResult1; - private TestResult testResult2; - - @BeforeEach - void setUp() throws NoSuchFieldException, IllegalAccessException { - // PracticeTest 객체 초기화 - practiceTest = PracticeTest.builder() - .name("Sample Test") - .round("1st Round") - .provider("Provider A") - .publicationYear("2024") - .subject(Subject.미적분) - .build(); - Field idField = PracticeTest.class.getDeclaredField("id"); - idField.setAccessible(true); // private 필드 접근 허용 - idField.set(practiceTest, 1L); // - - // TestResult 객체 초기화 (각 테스트의 풀이 시간을 설정) - testResult1 = TestResult.builder() - .score(85) - .solvingTime(Duration.ofMinutes(30)) // 30분 걸림 - .practiceTestId(practiceTest.getId()) - .build(); - practiceTest.plus1SolvesCount(); - - testResult2 = TestResult.builder() - .score(90) - .solvingTime(Duration.ofMinutes(45)) // 45분 걸림 - .practiceTestId(practiceTest.getId()) - .build(); - practiceTest.plus1SolvesCount(); - } - - @Test - void 평균시간계산() { - - when(practiceTestRepository.findAll()).thenReturn(List.of(practiceTest)); - List testResultsForPracticeTest1 = List.of(testResult1, testResult2); - when(testResultRepository.findAllByPracticeTestId(1L)).thenReturn(testResultsForPracticeTest1); - - // 메서드 실행 - testResultScheduler.calculateAverageSolvingTime(); - - // 검증 - long totalSeconds = testResult1.getSolvingTime().getSeconds() + testResult2.getSolvingTime().getSeconds(); - - long averageSeconds = totalSeconds / 2; - - Duration expectedAverage = Duration.ofSeconds(averageSeconds); - System.out.println(expectedAverage); - - Assertions.assertEquals(practiceTest.getAverageSolvingTime(), expectedAverage); - - } -} \ No newline at end of file diff --git a/src/test/resources/concept-tag.sql b/src/test/resources/concept-tag.sql new file mode 100644 index 0000000..e69de29 diff --git a/src/test/resources/insert-problem.sql b/src/test/resources/insert-problem.sql new file mode 100644 index 0000000..e69de29 diff --git a/src/test/resources/practice-test-tag.sql b/src/test/resources/practice-test-tag.sql new file mode 100644 index 0000000..a3ae1b6 --- /dev/null +++ b/src/test/resources/practice-test-tag.sql @@ -0,0 +1,2 @@ +INSERT INTO practice_test_tag (name, test_year, test_month, subject, area) +VALUES ('2025년 5월 고2 모의고사', 2024, 5, '고2', '수학'); \ No newline at end of file From 4f072605f872c79f66205c00eb72c9adcde63c55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=84=B8=EC=A4=80?= <74056843+sejoon00@users.noreply.github.com> Date: Thu, 30 Jan 2025 21:54:26 +0900 Subject: [PATCH 3/5] =?UTF-8?q?[fix/#23]=20insert-problem.sql=EC=97=90=20c?= =?UTF-8?q?hildProblem=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=82=AD=EC=A0=9C?= =?UTF-8?q?=20sql=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 5 + .../mapper/ChildProblemMapperImpl.java | 58 +++++++ .../service/mapper/ProblemMapperImpl.java | 75 +++++++++ .../controller/PracticeTestTagController.java | 26 ++- .../problem/controller/ProblemController.java | 31 +++- .../domain/childProblem/ChildProblem.java | 21 +-- .../domain/practiceTest/PracticeTestTag.java | 10 +- .../problem/domain/problem/Problem.java | 79 +++++---- .../problem/domain/problem/ProblemId.java | 2 + .../domain/problem/ProblemIdService.java | 20 +-- .../request/ChildProblemUpdateRequest.java | 2 + .../dto/request/ProblemPostRequest.java | 6 +- .../dto/request/ProblemUpdateRequest.java | 5 +- .../dto/response/PracticeTestTagResponse.java | 15 +- .../repository/ChildProblemRepository.java | 6 + .../repository/PracticeTestTagRepository.java | 6 +- .../problem/repository/ProblemRepository.java | 7 + .../problem/service/ProblemDeleteService.java | 20 ++- .../problem/service/ProblemSaveService.java | 43 ++--- .../problem/service/ProblemUpdateService.java | 61 ++++++- .../service/mapper/ChildProblemMapper.java | 2 +- .../problem/service/mapper/ProblemMapper.java | 2 +- .../v0/practiceTest/domain/PracticeTest.java | 7 +- .../repository/PracticeTestRepository.java | 2 +- .../global/common/BaseEntity.java | 6 - .../global/error/exception/ErrorCode.java | 3 + .../domain/problem/ProblemIdServiceTest.java | 78 ++++++++- .../service/ProblemSaveServiceTest.java | 150 +++++++++++++++++- .../service/ProblemUpdateServiceTest.java | 112 ++++++++++++- src/test/resources/auth-test-data.sql | 4 +- src/test/resources/concept-tag.sql | 12 ++ src/test/resources/insert-problem.sql | 29 ++++ 32 files changed, 790 insertions(+), 115 deletions(-) create mode 100644 src/main/generated/com/moplus/moplus_server/domain/problem/service/mapper/ChildProblemMapperImpl.java create mode 100644 src/main/generated/com/moplus/moplus_server/domain/problem/service/mapper/ProblemMapperImpl.java diff --git a/build.gradle b/build.gradle index 47c4de6..f4e44f9 100644 --- a/build.gradle +++ b/build.gradle @@ -56,6 +56,11 @@ dependencies { // validator implementation 'commons-validator:commons-validator:1.7' + // Map Struct + implementation 'org.mapstruct:mapstruct:1.6.3' + annotationProcessor 'org.mapstruct:mapstruct-processor:1.6.3' + annotationProcessor 'org.projectlombok:lombok-mapstruct-binding:0.2.0' + } tasks.named('test') { diff --git a/src/main/generated/com/moplus/moplus_server/domain/problem/service/mapper/ChildProblemMapperImpl.java b/src/main/generated/com/moplus/moplus_server/domain/problem/service/mapper/ChildProblemMapperImpl.java new file mode 100644 index 0000000..e76d12e --- /dev/null +++ b/src/main/generated/com/moplus/moplus_server/domain/problem/service/mapper/ChildProblemMapperImpl.java @@ -0,0 +1,58 @@ +package com.moplus.moplus_server.domain.problem.service.mapper; + +import com.moplus.moplus_server.domain.problem.domain.childProblem.ChildProblem; +import com.moplus.moplus_server.domain.problem.dto.request.ChildProblemPostRequest; +import com.moplus.moplus_server.domain.problem.dto.request.ChildProblemUpdateRequest; +import java.util.LinkedHashSet; +import java.util.Set; +import javax.annotation.processing.Generated; +import org.springframework.stereotype.Component; + +@Generated( + value = "org.mapstruct.ap.MappingProcessor", + date = "2025-01-30T21:11:35+0900", + comments = "version: 1.6.3, compiler: javac, environment: Java 17.0.10 (JetBrains s.r.o.)" +) +@Component +public class ChildProblemMapperImpl implements ChildProblemMapper { + + @Override + public ChildProblem from(ChildProblemPostRequest request) { + if ( request == null ) { + return null; + } + + ChildProblem.ChildProblemBuilder childProblem = ChildProblem.builder(); + + childProblem.imageUrl( request.imageUrl() ); + childProblem.problemType( request.problemType() ); + childProblem.answer( request.answer() ); + Set set = request.conceptTagIds(); + if ( set != null ) { + childProblem.conceptTagIds( new LinkedHashSet( set ) ); + } + childProblem.sequence( request.sequence() ); + + return childProblem.build(); + } + + @Override + public ChildProblem from(ChildProblemUpdateRequest request) { + if ( request == null ) { + return null; + } + + ChildProblem.ChildProblemBuilder childProblem = ChildProblem.builder(); + + childProblem.imageUrl( request.imageUrl() ); + childProblem.problemType( request.problemType() ); + childProblem.answer( request.answer() ); + Set set = request.conceptTagIds(); + if ( set != null ) { + childProblem.conceptTagIds( new LinkedHashSet( set ) ); + } + childProblem.sequence( request.sequence() ); + + return childProblem.build(); + } +} diff --git a/src/main/generated/com/moplus/moplus_server/domain/problem/service/mapper/ProblemMapperImpl.java b/src/main/generated/com/moplus/moplus_server/domain/problem/service/mapper/ProblemMapperImpl.java new file mode 100644 index 0000000..dc059cb --- /dev/null +++ b/src/main/generated/com/moplus/moplus_server/domain/problem/service/mapper/ProblemMapperImpl.java @@ -0,0 +1,75 @@ +package com.moplus.moplus_server.domain.problem.service.mapper; + +import com.moplus.moplus_server.domain.problem.domain.practiceTest.PracticeTestTag; +import com.moplus.moplus_server.domain.problem.domain.problem.Problem; +import com.moplus.moplus_server.domain.problem.domain.problem.ProblemId; +import com.moplus.moplus_server.domain.problem.dto.request.ProblemPostRequest; +import com.moplus.moplus_server.domain.problem.dto.request.ProblemUpdateRequest; +import java.util.LinkedHashSet; +import java.util.Set; +import javax.annotation.processing.Generated; +import org.springframework.stereotype.Component; + +@Generated( + value = "org.mapstruct.ap.MappingProcessor", + date = "2025-01-30T21:11:35+0900", + comments = "version: 1.6.3, compiler: javac, environment: Java 17.0.10 (JetBrains s.r.o.)" +) +@Component +public class ProblemMapperImpl implements ProblemMapper { + + @Override + public Problem from(ProblemPostRequest request, ProblemId problemId, PracticeTestTag practiceTestTag) { + if ( request == null && problemId == null && practiceTestTag == null ) { + return null; + } + + Problem.ProblemBuilder problem = Problem.builder(); + + if ( request != null ) { + problem.number( request.number() ); + problem.answer( request.answer() ); + problem.comment( request.comment() ); + problem.mainProblemImageUrl( request.mainProblemImageUrl() ); + problem.mainAnalysisImageUrl( request.mainAnalysisImageUrl() ); + problem.readingTipImageUrl( request.readingTipImageUrl() ); + problem.seniorTipImageUrl( request.seniorTipImageUrl() ); + problem.prescriptionImageUrl( request.prescriptionImageUrl() ); + Set set = request.conceptTagIds(); + if ( set != null ) { + problem.conceptTagIds( new LinkedHashSet( set ) ); + } + } + problem.id( problemId ); + problem.practiceTestTag( practiceTestTag ); + + return problem.build(); + } + + @Override + public Problem from(ProblemUpdateRequest request, ProblemId problemId, PracticeTestTag practiceTestTag) { + if ( request == null && problemId == null && practiceTestTag == null ) { + return null; + } + + Problem.ProblemBuilder problem = Problem.builder(); + + if ( request != null ) { + problem.answer( String.valueOf( request.answer() ) ); + problem.comment( request.comment() ); + problem.mainProblemImageUrl( request.mainProblemImageUrl() ); + problem.mainAnalysisImageUrl( request.mainAnalysisImageUrl() ); + problem.readingTipImageUrl( request.readingTipImageUrl() ); + problem.seniorTipImageUrl( request.seniorTipImageUrl() ); + problem.prescriptionImageUrl( request.prescriptionImageUrl() ); + Set set = request.conceptTagIds(); + if ( set != null ) { + problem.conceptTagIds( new LinkedHashSet( set ) ); + } + } + problem.id( problemId ); + problem.practiceTestTag( practiceTestTag ); + + return problem.build(); + } +} diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/controller/PracticeTestTagController.java b/src/main/java/com/moplus/moplus_server/domain/problem/controller/PracticeTestTagController.java index d630855..443b7bc 100644 --- a/src/main/java/com/moplus/moplus_server/domain/problem/controller/PracticeTestTagController.java +++ b/src/main/java/com/moplus/moplus_server/domain/problem/controller/PracticeTestTagController.java @@ -1,2 +1,26 @@ -package com.moplus.moplus_server.domain.problem.controller;public class PracticeTestTagController { +package com.moplus.moplus_server.domain.problem.controller; + +import com.moplus.moplus_server.domain.problem.dto.response.PracticeTestTagResponse; +import com.moplus.moplus_server.domain.problem.repository.PracticeTestTagRepository; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v1/practiceTestTags") +@RequiredArgsConstructor +public class PracticeTestTagController { + + private final PracticeTestTagRepository practiceTestTagRepository; + + @GetMapping("") + public ResponseEntity> getPracticeTestTags() { + List responses = practiceTestTagRepository.findAll().stream() + .map(PracticeTestTagResponse::of) + .toList(); + return ResponseEntity.ok(responses); + } } diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/controller/ProblemController.java b/src/main/java/com/moplus/moplus_server/domain/problem/controller/ProblemController.java index a190bad..6772804 100644 --- a/src/main/java/com/moplus/moplus_server/domain/problem/controller/ProblemController.java +++ b/src/main/java/com/moplus/moplus_server/domain/problem/controller/ProblemController.java @@ -3,10 +3,15 @@ import com.moplus.moplus_server.domain.problem.dto.request.ProblemPostRequest; import com.moplus.moplus_server.domain.problem.dto.request.ProblemUpdateRequest; import com.moplus.moplus_server.domain.problem.dto.response.ProblemGetResponse; +import com.moplus.moplus_server.domain.problem.service.ProblemDeleteService; +import com.moplus.moplus_server.domain.problem.service.ProblemGetService; import com.moplus.moplus_server.domain.problem.service.ProblemSaveService; +import com.moplus.moplus_server.domain.problem.service.ProblemUpdateService; import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -16,9 +21,20 @@ @RestController @RequestMapping("/api/v1/problems") @RequiredArgsConstructor -public class ProblemSaveController { +public class ProblemController { private final ProblemSaveService problemSaveService; + private final ProblemUpdateService problemUpdateService; + private final ProblemGetService problemGetService; + private final ProblemDeleteService problemDeleteService; + + @GetMapping("/{id}") + @Operation(summary = "문항 조회", description = "문항를 조회합니다.") + public ResponseEntity getProblem( + @PathVariable("id") String id + ) { + return ResponseEntity.ok(problemGetService.getProblem(id)); + } @PostMapping("") @Operation(summary = "문항 생성", description = "문제를 생성합니다. 새끼 문항은 list 순서대로 sequence를 저장합니다.") @@ -29,11 +45,20 @@ public ResponseEntity createProblem( } @PostMapping("/{id}") - @Operation(summary = "문항 업데이트", description = "문제를 업데이트합니다. 새끼 문항은 수정된 리스트, 새로 생성된 리스트, 삭제된 리스트가 필요합니다.") + @Operation(summary = "문항 업데이트", description = "문제를 업데이트합니다. 문항 번호, 모의고사는 수정할 수 없습니다. 새로 추가되는 새끼문항 id는 빈 값입니다.") public ResponseEntity updateProblem( @PathVariable("id") String id, @RequestBody ProblemUpdateRequest request ) { - return ResponseEntity.ok(problemSaveService.updateProblem(id, request)); + return ResponseEntity.ok(problemUpdateService.updateProblem(id, request)); + } + + @DeleteMapping("/{id}") + @Operation(summary = "문항 삭제") + public ResponseEntity updateProblem( + @PathVariable("id") String id + ) { + problemDeleteService.deleteProblem(id); + return ResponseEntity.ok().body(null); } } diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/domain/childProblem/ChildProblem.java b/src/main/java/com/moplus/moplus_server/domain/problem/domain/childProblem/ChildProblem.java index 7bbede9..2067862 100644 --- a/src/main/java/com/moplus/moplus_server/domain/problem/domain/childProblem/ChildProblem.java +++ b/src/main/java/com/moplus/moplus_server/domain/problem/domain/childProblem/ChildProblem.java @@ -2,7 +2,6 @@ import com.moplus.moplus_server.domain.problem.domain.Answer; import com.moplus.moplus_server.domain.problem.domain.problem.ProblemType; -import com.moplus.moplus_server.domain.problem.dto.request.ChildProblemUpdateRequest; import com.moplus.moplus_server.global.common.BaseEntity; import com.moplus.moplus_server.global.error.exception.ErrorCode; import com.moplus.moplus_server.global.error.exception.InvalidValueException; @@ -11,6 +10,8 @@ import jakarta.persistence.ElementCollection; import jakarta.persistence.Embedded; import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; @@ -27,14 +28,16 @@ public class ChildProblem extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "problem_id") + @Column(name = "child_problem_id") Long id; @ElementCollection - @CollectionTable(name = "child_problem_concept", joinColumns = @JoinColumn(name = "concept_tag_id")) + @CollectionTable(name = "child_problem_concept", joinColumns = @JoinColumn(name = "child_problem_id")) + @Column(name = "concept_tag_id") Set conceptTagIds; private String imageUrl; @Embedded private Answer answer; + @Enumerated(EnumType.STRING) private ProblemType problemType; private int sequence; @@ -57,12 +60,12 @@ public void validateAnswerByType(String answer, ProblemType problemType) { } } - public void update(ChildProblemUpdateRequest request) { - this.imageUrl = request.imageUrl(); - this.problemType = request.problemType(); - this.answer = new Answer(request.answer(), request.problemType()); - this.conceptTagIds = request.conceptTagIds(); - this.sequence = request.sequence(); + public void update(ChildProblem input) { + this.imageUrl = input.imageUrl; + this.problemType = input.problemType; + this.answer = input.answer; + this.conceptTagIds = input.conceptTagIds; + this.sequence = input.sequence; } public String getAnswer() { diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/domain/practiceTest/PracticeTestTag.java b/src/main/java/com/moplus/moplus_server/domain/problem/domain/practiceTest/PracticeTestTag.java index f2f064f..1782bd8 100644 --- a/src/main/java/com/moplus/moplus_server/domain/problem/domain/practiceTest/PracticeTestTag.java +++ b/src/main/java/com/moplus/moplus_server/domain/problem/domain/practiceTest/PracticeTestTag.java @@ -1,6 +1,9 @@ package com.moplus.moplus_server.domain.problem.domain.practiceTest; +import jakarta.persistence.Column; import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; @@ -12,19 +15,22 @@ @Entity @Table(name = "practice_test_tag") @NoArgsConstructor -public class PracticeTest { +public class PracticeTestTag { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; + @Column(name = "test_year") private int year; + @Column(name = "test_month") private int month; + @Enumerated(value = EnumType.STRING) private Subject subject; private String area; - public PracticeTest(String name, int year, int month, Subject subject) { + public PracticeTestTag(String name, int year, int month, Subject subject) { this.name = name; this.year = year; this.month = month; diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/domain/problem/Problem.java b/src/main/java/com/moplus/moplus_server/domain/problem/domain/problem/Problem.java index b72ceb8..b574ba4 100644 --- a/src/main/java/com/moplus/moplus_server/domain/problem/domain/problem/Problem.java +++ b/src/main/java/com/moplus/moplus_server/domain/problem/domain/problem/Problem.java @@ -2,12 +2,11 @@ import com.moplus.moplus_server.domain.problem.domain.Answer; import com.moplus.moplus_server.domain.problem.domain.childProblem.ChildProblem; -import com.moplus.moplus_server.domain.problem.domain.practiceTest.PracticeTest; -import com.moplus.moplus_server.domain.problem.dto.request.ChildProblemPostRequest; -import com.moplus.moplus_server.domain.problem.dto.request.ChildProblemUpdateRequest; +import com.moplus.moplus_server.domain.problem.domain.practiceTest.PracticeTestTag; import com.moplus.moplus_server.global.common.BaseEntity; import jakarta.persistence.CascadeType; import jakarta.persistence.CollectionTable; +import jakarta.persistence.Column; import jakarta.persistence.ElementCollection; import jakarta.persistence.Embedded; import jakarta.persistence.EmbeddedId; @@ -17,6 +16,8 @@ import jakarta.persistence.OneToMany; import jakarta.persistence.OrderBy; import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; import java.util.List; import java.util.Set; import lombok.Builder; @@ -42,11 +43,12 @@ public class Problem extends BaseEntity { String seniorTipImageUrl; String prescriptionImageUrl; @ElementCollection - @CollectionTable(name = "problem_concept", joinColumns = @JoinColumn(name = "concept_tag_id")) + @CollectionTable(name = "problem_concept", joinColumns = @JoinColumn(name = "problem_id")) + @Column(name = "concept_tag_id") Set conceptTagIds; private ProblemType problemType; private boolean isPublished; - private boolean isModified; + private boolean isVariation; @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) @JoinColumn(name = "problem_id") @@ -54,13 +56,12 @@ public class Problem extends BaseEntity { private List childProblems = new ArrayList<>(); @Builder - public Problem(ProblemId id, PracticeTest practiceTest, int number, String answer, String comment, + public Problem(ProblemId id, PracticeTestTag practiceTestTag, int number, String answer, String comment, String mainProblemImageUrl, String mainAnalysisImageUrl, String readingTipImageUrl, String seniorTipImageUrl, - String prescriptionImageUrl, ProblemType problemType, Set conceptTagIds, - List childProblems) { + String prescriptionImageUrl, Set conceptTagIds) { this.id = id; - this.practiceTestId = practiceTest.getId(); + this.practiceTestId = practiceTestTag.getId(); this.number = number; this.comment = comment; this.mainProblemImageUrl = mainProblemImageUrl; @@ -68,38 +69,58 @@ public Problem(ProblemId id, PracticeTest practiceTest, int number, String answe this.readingTipImageUrl = readingTipImageUrl; this.seniorTipImageUrl = seniorTipImageUrl; this.prescriptionImageUrl = prescriptionImageUrl; - this.problemType = ProblemType.getTypeForProblem(practiceTest.getSubject().getValue(), number); + this.problemType = ProblemType.getTypeForProblem(practiceTestTag.getSubject().getValue(), number); this.answer = new Answer(answer, this.problemType); - this.conceptTagIds = conceptTagIds; - this.childProblems = childProblems; + this.conceptTagIds = new HashSet<>(conceptTagIds); this.isPublished = false; - this.isModified = false; + this.isVariation = false; } public String getAnswer() { return answer.getValue(); } - public void addChildProblem(ChildProblemPostRequest request) { - ChildProblem childProblem = ChildProblem.builder() - .imageUrl(request.imageUrl()) - .problemType(request.problemType()) - .answer(request.answer()) - .conceptTagIds(request.conceptTagIds()) - .sequence(request.sequence()) - .build(); - childProblems.add(request.sequence(), childProblem); + public void addChildProblem(List inputChildProblems) { + List mutableChildProblems = new ArrayList<>(inputChildProblems); + mutableChildProblems.sort(Comparator.comparingInt(ChildProblem::getSequence)); + mutableChildProblems.forEach(childProblems::add); + mutableChildProblems.forEach(childProblem -> conceptTagIds.addAll(childProblem.getConceptTagIds())); } - public void updateChildProblem(ChildProblemUpdateRequest request) { - childProblems.get(request.sequence()).update(request); + public void update(Problem inputProblem) { + this.conceptTagIds = new HashSet<>(inputProblem.getConceptTagIds()); + this.number = inputProblem.getNumber(); + this.answer = new Answer(inputProblem.getAnswer(), this.problemType); + this.comment = inputProblem.getComment(); + this.mainProblemImageUrl = inputProblem.getMainProblemImageUrl(); + this.mainAnalysisImageUrl = inputProblem.getMainAnalysisImageUrl(); + this.readingTipImageUrl = inputProblem.getReadingTipImageUrl(); + this.seniorTipImageUrl = inputProblem.getSeniorTipImageUrl(); + this.prescriptionImageUrl = inputProblem.getPrescriptionImageUrl(); } - public void deleteChildProblem(Long childProblemId) { - childProblems.forEach(childProblem -> { - if (childProblem.getId().equals(childProblemId)) { - childProblems.remove(childProblem); - } + public void updateChildProblem(List inputChildProblems) { + List mutableChildProblems = new ArrayList<>(inputChildProblems); + mutableChildProblems.sort(Comparator.comparingInt(ChildProblem::getSequence)); + + inputChildProblems.forEach(childProblem -> { + childProblems.stream() + .filter(existingChildProblem -> existingChildProblem.getId().equals(childProblem.getId())) + .findFirst() + .ifPresentOrElse( + existingChildProblem -> { + existingChildProblem.update(childProblem); + conceptTagIds.addAll(existingChildProblem.getConceptTagIds()); + }, + () -> { + childProblems.add(childProblem); + conceptTagIds.addAll(childProblem.getConceptTagIds()); + } + ); }); } + + public void deleteChildProblem(List deleteChildProblems) { + childProblems.removeIf(childProblem -> deleteChildProblems.contains(childProblem.getId())); + } } diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/domain/problem/ProblemId.java b/src/main/java/com/moplus/moplus_server/domain/problem/domain/problem/ProblemId.java index 847b283..2bc4343 100644 --- a/src/main/java/com/moplus/moplus_server/domain/problem/domain/problem/ProblemId.java +++ b/src/main/java/com/moplus/moplus_server/domain/problem/domain/problem/ProblemId.java @@ -3,8 +3,10 @@ import jakarta.persistence.Column; import jakarta.persistence.Embeddable; import java.io.Serializable; +import lombok.Getter; import lombok.NoArgsConstructor; +@Getter @Embeddable @NoArgsConstructor public class ProblemId implements Serializable { diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/domain/problem/ProblemIdService.java b/src/main/java/com/moplus/moplus_server/domain/problem/domain/problem/ProblemIdService.java index e5daf59..f3d3e82 100644 --- a/src/main/java/com/moplus/moplus_server/domain/problem/domain/problem/ProblemIdService.java +++ b/src/main/java/com/moplus/moplus_server/domain/problem/domain/problem/ProblemIdService.java @@ -1,6 +1,6 @@ package com.moplus.moplus_server.domain.problem.domain.problem; -import com.moplus.moplus_server.domain.problem.domain.practiceTest.PracticeTest; +import com.moplus.moplus_server.domain.problem.domain.practiceTest.PracticeTestTag; import com.moplus.moplus_server.domain.problem.repository.ProblemRepository; import java.util.concurrent.atomic.AtomicInteger; import lombok.RequiredArgsConstructor; @@ -15,20 +15,20 @@ public class ProblemIdService { /* 문제 ID 생성 로직 - AA : 과목 ( 1: 수학, 2: 영어, 3: 국어, 4: 사회, 5: 과학 ) - S : ( 1: 고1, 2: 고2, 3: 미적분, 4: 기하, 5: 확률과 통계, 6: 가형, 7: 나형 ) YY: 년도 (두 자리) MM: 월 (두 자리) NN : 번호 (01~99) + AA : 영역 ( 01: 수학, 02: 영어, 03: 국어, 04: 사회, 05: 과학 ) + S : ( 1: 고1, 2: 고2, 3: 미적분, 4: 기하, 5: 확률과 통계, 6: 가형, 7: 나형 ) C : 변형 여부 ( 0: 기본, 1: 변형 ) XXX : 3자리 구분 숫자 */ - public ProblemId nextId(int number, PracticeTest practiceTest) { + public ProblemId nextId(int number, PracticeTestTag practiceTestTag) { int DEFAULT_AREA = 1; //현재 영역은 수학밖에 없음 - int subject = practiceTest.getSubject().getIdCode(); // AA (과목) - int year = practiceTest.getYear() % 100; // YY (두 자리 연도) - int month = practiceTest.getMonth(); // MM (두 자리 월) + int subject = practiceTestTag.getSubject().getIdCode(); // AA (과목) + int year = practiceTestTag.getYear() % 100; // YY (두 자리 연도) + int month = practiceTestTag.getMonth(); // MM (두 자리 월) int DEFAULT_MODIFIED = 0; // 변형 여부 (0: 기본, 1: 변형) String generatedId; @@ -37,9 +37,9 @@ public ProblemId nextId(int number, PracticeTest practiceTest) { // 중복되지 않는 ID 찾을 때까지 반복 do { sequence = SEQUENCE.getAndIncrement() % 1000; // 000~999 순환 - generatedId = String.format("%02d%d%02d%02d%02d%d%03d", - DEFAULT_AREA, subject, year, month, - number, DEFAULT_MODIFIED, sequence); + generatedId = String.format("%02d%02d%02d%02d%d%d%03d", + year, month, number, DEFAULT_AREA, + subject, DEFAULT_MODIFIED, sequence); } while (problemRepository.existsById(new ProblemId(generatedId))); // ID가 이미 존재하면 재생성 return new ProblemId(generatedId); diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/dto/request/ChildProblemUpdateRequest.java b/src/main/java/com/moplus/moplus_server/domain/problem/dto/request/ChildProblemUpdateRequest.java index f8c5f35..d080b17 100644 --- a/src/main/java/com/moplus/moplus_server/domain/problem/dto/request/ChildProblemUpdateRequest.java +++ b/src/main/java/com/moplus/moplus_server/domain/problem/dto/request/ChildProblemUpdateRequest.java @@ -1,9 +1,11 @@ package com.moplus.moplus_server.domain.problem.dto.request; import com.moplus.moplus_server.domain.problem.domain.problem.ProblemType; +import io.swagger.v3.oas.annotations.media.Schema; import java.util.Set; public record ChildProblemUpdateRequest( + @Schema(description = "새로 생성되는 새끼문항은 빈 값입니다.") Long id, String imageUrl, ProblemType problemType, diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/dto/request/ProblemPostRequest.java b/src/main/java/com/moplus/moplus_server/domain/problem/dto/request/ProblemPostRequest.java index 3af1adc..d6e5821 100644 --- a/src/main/java/com/moplus/moplus_server/domain/problem/dto/request/ProblemPostRequest.java +++ b/src/main/java/com/moplus/moplus_server/domain/problem/dto/request/ProblemPostRequest.java @@ -1,6 +1,6 @@ package com.moplus.moplus_server.domain.problem.dto.request; -import com.moplus.moplus_server.domain.problem.domain.practiceTest.PracticeTest; +import com.moplus.moplus_server.domain.problem.domain.practiceTest.PracticeTestTag; import com.moplus.moplus_server.domain.problem.domain.problem.Problem; import com.moplus.moplus_server.domain.problem.domain.problem.ProblemId; import java.util.List; @@ -19,11 +19,11 @@ public record ProblemPostRequest( String prescriptionImageUrl, List childProblems ) { - public Problem toEntity(PracticeTest practiceTest, ProblemId problemId) { + public Problem toEntity(PracticeTestTag practiceTestTag, ProblemId problemId) { return Problem.builder() .id(problemId) .conceptTagIds(conceptTagIds) - .practiceTest(practiceTest) + .practiceTestTag(practiceTestTag) .number(number) .answer(answer) .comment(comment) diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/dto/request/ProblemUpdateRequest.java b/src/main/java/com/moplus/moplus_server/domain/problem/dto/request/ProblemUpdateRequest.java index b1a462b..9e04f0d 100644 --- a/src/main/java/com/moplus/moplus_server/domain/problem/dto/request/ProblemUpdateRequest.java +++ b/src/main/java/com/moplus/moplus_server/domain/problem/dto/request/ProblemUpdateRequest.java @@ -5,8 +5,6 @@ public record ProblemUpdateRequest( Set conceptTagIds, - Long practiceTestId, - int number, int answer, String comment, String mainProblemImageUrl, @@ -15,7 +13,6 @@ public record ProblemUpdateRequest( String seniorTipImageUrl, String prescriptionImageUrl, List updateChildProblems, - List createChildProblems, - List deleteChildProblems + List deleteChildProblems ) { } diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/dto/response/PracticeTestTagResponse.java b/src/main/java/com/moplus/moplus_server/domain/problem/dto/response/PracticeTestTagResponse.java index a623b73..a4b0f1c 100644 --- a/src/main/java/com/moplus/moplus_server/domain/problem/dto/response/PracticeTestTagResponse.java +++ b/src/main/java/com/moplus/moplus_server/domain/problem/dto/response/PracticeTestTagResponse.java @@ -1,2 +1,15 @@ -package com.moplus.moplus_server.domain.problem.dto.response;public record PracticeTestTagResponse() { +package com.moplus.moplus_server.domain.problem.dto.response; + +import com.moplus.moplus_server.domain.problem.domain.practiceTest.PracticeTestTag; + +public record PracticeTestTagResponse( + Long id, + String name +) { + public static PracticeTestTagResponse of(PracticeTestTag practiceTestTag) { + return new PracticeTestTagResponse( + practiceTestTag.getId(), + practiceTestTag.getName() + ); + } } diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/repository/ChildProblemRepository.java b/src/main/java/com/moplus/moplus_server/domain/problem/repository/ChildProblemRepository.java index be781ed..9b28686 100644 --- a/src/main/java/com/moplus/moplus_server/domain/problem/repository/ChildProblemRepository.java +++ b/src/main/java/com/moplus/moplus_server/domain/problem/repository/ChildProblemRepository.java @@ -1,7 +1,13 @@ package com.moplus.moplus_server.domain.problem.repository; import com.moplus.moplus_server.domain.problem.domain.childProblem.ChildProblem; +import com.moplus.moplus_server.global.error.exception.ErrorCode; +import com.moplus.moplus_server.global.error.exception.NotFoundException; import org.springframework.data.jpa.repository.JpaRepository; public interface ChildProblemRepository extends JpaRepository { + + default ChildProblem findByIdElseThrow(Long childProblemId) { + return findById(childProblemId).orElseThrow(() -> new NotFoundException(ErrorCode.CHILD_PROBLEM_NOT_FOUND)); + } } diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/repository/PracticeTestTagRepository.java b/src/main/java/com/moplus/moplus_server/domain/problem/repository/PracticeTestTagRepository.java index 9e5a514..2e02d64 100644 --- a/src/main/java/com/moplus/moplus_server/domain/problem/repository/PracticeTestTagRepository.java +++ b/src/main/java/com/moplus/moplus_server/domain/problem/repository/PracticeTestTagRepository.java @@ -1,13 +1,13 @@ package com.moplus.moplus_server.domain.problem.repository; -import com.moplus.moplus_server.domain.problem.domain.practiceTest.PracticeTest; +import com.moplus.moplus_server.domain.problem.domain.practiceTest.PracticeTestTag; import com.moplus.moplus_server.global.error.exception.ErrorCode; import com.moplus.moplus_server.global.error.exception.NotFoundException; import org.springframework.data.jpa.repository.JpaRepository; -public interface PracticeTestTagRepository extends JpaRepository { +public interface PracticeTestTagRepository extends JpaRepository { - default PracticeTest findByIdElseThrow(Long id) { + default PracticeTestTag findByIdElseThrow(Long id) { return findById(id) .orElseThrow(() -> new NotFoundException(ErrorCode.PRACTICE_TEST_NOT_FOUND)); } diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/repository/ProblemRepository.java b/src/main/java/com/moplus/moplus_server/domain/problem/repository/ProblemRepository.java index 99d0d05..494f0c9 100644 --- a/src/main/java/com/moplus/moplus_server/domain/problem/repository/ProblemRepository.java +++ b/src/main/java/com/moplus/moplus_server/domain/problem/repository/ProblemRepository.java @@ -17,6 +17,13 @@ default void existsByPracticeTestIdAndNumberOrThrow(Long practiceTestId, int num } } + default void existsByIdElseThrow(ProblemId problemId) { + if (!existsById(problemId)) { + throw new NotFoundException(ErrorCode.PROBLEM_NOT_FOUND); + } + } + + default Problem findByIdElseThrow(ProblemId problemId) { return findById(problemId).orElseThrow(() -> new NotFoundException(ErrorCode.PROBLEM_NOT_FOUND)); } diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/service/ProblemDeleteService.java b/src/main/java/com/moplus/moplus_server/domain/problem/service/ProblemDeleteService.java index 3d465ab..1bb193f 100644 --- a/src/main/java/com/moplus/moplus_server/domain/problem/service/ProblemDeleteService.java +++ b/src/main/java/com/moplus/moplus_server/domain/problem/service/ProblemDeleteService.java @@ -1,2 +1,20 @@ -package com.moplus.moplus_server.domain.problem.service;public class ProblemDeleteService { +package com.moplus.moplus_server.domain.problem.service; + +import com.moplus.moplus_server.domain.problem.domain.problem.ProblemId; +import com.moplus.moplus_server.domain.problem.repository.ProblemRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class ProblemDeleteService { + + private final ProblemRepository problemRepository; + + @Transactional + public void deleteProblem(String problemId) { + problemRepository.existsByIdElseThrow(new ProblemId(problemId)); + problemRepository.deleteById(new ProblemId(problemId)); + } } diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/service/ProblemSaveService.java b/src/main/java/com/moplus/moplus_server/domain/problem/service/ProblemSaveService.java index 4182002..6db9ddc 100644 --- a/src/main/java/com/moplus/moplus_server/domain/problem/service/ProblemSaveService.java +++ b/src/main/java/com/moplus/moplus_server/domain/problem/service/ProblemSaveService.java @@ -1,16 +1,17 @@ package com.moplus.moplus_server.domain.problem.service; import com.moplus.moplus_server.domain.concept.repository.ConceptTagRepository; -import com.moplus.moplus_server.domain.problem.domain.practiceTest.PracticeTest; +import com.moplus.moplus_server.domain.problem.domain.childProblem.ChildProblem; +import com.moplus.moplus_server.domain.problem.domain.practiceTest.PracticeTestTag; import com.moplus.moplus_server.domain.problem.domain.problem.Problem; import com.moplus.moplus_server.domain.problem.domain.problem.ProblemId; import com.moplus.moplus_server.domain.problem.domain.problem.ProblemIdService; -import com.moplus.moplus_server.domain.problem.dto.request.ChildProblemDeleteRequest; import com.moplus.moplus_server.domain.problem.dto.request.ProblemPostRequest; -import com.moplus.moplus_server.domain.problem.dto.request.ProblemUpdateRequest; -import com.moplus.moplus_server.domain.problem.dto.response.ProblemGetResponse; import com.moplus.moplus_server.domain.problem.repository.PracticeTestTagRepository; import com.moplus.moplus_server.domain.problem.repository.ProblemRepository; +import com.moplus.moplus_server.domain.problem.service.mapper.ChildProblemMapper; +import com.moplus.moplus_server.domain.problem.service.mapper.ProblemMapper; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -23,35 +24,23 @@ public class ProblemSaveService { private final PracticeTestTagRepository practiceTestRepository; private final ConceptTagRepository conceptTagRepository; private final ProblemIdService problemIdService; + private final ProblemMapper problemMapper; + private final ChildProblemMapper childProblemMapper; @Transactional public ProblemId createProblem(ProblemPostRequest request) { - PracticeTest practiceTest = practiceTestRepository.findByIdElseThrow(request.practiceTestId()); - problemRepository.existsByPracticeTestIdAndNumberOrThrow(practiceTest.getId(), request.number()); + PracticeTestTag practiceTestTag = practiceTestRepository.findByIdElseThrow(request.practiceTestId()); + problemRepository.existsByPracticeTestIdAndNumberOrThrow(practiceTestTag.getId(), request.number()); conceptTagRepository.existsByIdElseThrow(request.conceptTagIds()); - ProblemId problemId = problemIdService.nextId(request.number(), practiceTest); - Problem problem = request.toEntity(practiceTest, problemId); - request.childProblems() - .forEach(problem::addChildProblem); + ProblemId problemId = problemIdService.nextId(request.number(), practiceTestTag); + Problem problem = problemMapper.from(request, problemId, practiceTestTag); - return problemRepository.save(problem).getId(); - } + List childProblems = request.childProblems().stream() + .map(childProblemMapper::from) + .toList(); + problem.addChildProblem(childProblems); - @Transactional - public ProblemGetResponse updateProblem(String problemId, ProblemUpdateRequest request) { - PracticeTest practiceTest = practiceTestRepository.findByIdElseThrow(request.practiceTestId()); - problemRepository.existsByPracticeTestIdAndNumberOrThrow(practiceTest.getId(), request.number()); - conceptTagRepository.existsByIdElseThrow(request.conceptTagIds()); - - Problem problem = problemRepository.findByIdElseThrow(new ProblemId(problemId)); - request.deleteChildProblems().stream() - .map(ChildProblemDeleteRequest::childProblemId) - .forEach(problem::deleteChildProblem); - request.createChildProblems().forEach(problem::addChildProblem); - request.updateChildProblems().forEach(problem::updateChildProblem); - - Problem savedProblem = problemRepository.save(problem); - return ProblemGetResponse.of(savedProblem); + return problemRepository.save(problem).getId(); } } diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/service/ProblemUpdateService.java b/src/main/java/com/moplus/moplus_server/domain/problem/service/ProblemUpdateService.java index fc078cc..a7f0678 100644 --- a/src/main/java/com/moplus/moplus_server/domain/problem/service/ProblemUpdateService.java +++ b/src/main/java/com/moplus/moplus_server/domain/problem/service/ProblemUpdateService.java @@ -1,2 +1,61 @@ -package com.moplus.moplus_server.domain.problem.service;public class ProblemUpdateService { +package com.moplus.moplus_server.domain.problem.service; + +import com.moplus.moplus_server.domain.concept.repository.ConceptTagRepository; +import com.moplus.moplus_server.domain.problem.domain.childProblem.ChildProblem; +import com.moplus.moplus_server.domain.problem.domain.practiceTest.PracticeTestTag; +import com.moplus.moplus_server.domain.problem.domain.problem.Problem; +import com.moplus.moplus_server.domain.problem.domain.problem.ProblemId; +import com.moplus.moplus_server.domain.problem.dto.request.ChildProblemUpdateRequest; +import com.moplus.moplus_server.domain.problem.dto.request.ProblemUpdateRequest; +import com.moplus.moplus_server.domain.problem.dto.response.ProblemGetResponse; +import com.moplus.moplus_server.domain.problem.repository.ChildProblemRepository; +import com.moplus.moplus_server.domain.problem.repository.PracticeTestTagRepository; +import com.moplus.moplus_server.domain.problem.repository.ProblemRepository; +import com.moplus.moplus_server.domain.problem.service.mapper.ChildProblemMapper; +import com.moplus.moplus_server.domain.problem.service.mapper.ProblemMapper; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class ProblemUpdateService { + + private final ProblemRepository problemRepository; + private final PracticeTestTagRepository practiceTestRepository; + private final ConceptTagRepository conceptTagRepository; + private final ChildProblemRepository childProblemRepository; + private final ChildProblemMapper childProblemMapper; + private final ProblemMapper problemMapper; + + @Transactional + public ProblemGetResponse updateProblem(String problemId, ProblemUpdateRequest request) { + conceptTagRepository.existsByIdElseThrow(request.conceptTagIds()); + Problem problem = problemRepository.findByIdElseThrow(new ProblemId(problemId)); + PracticeTestTag practiceTestTag = practiceTestRepository.findByIdElseThrow(problem.getPracticeTestId()); + Problem inputProblem = problemMapper.from(request, problem.getId(), practiceTestTag); + problem.update(inputProblem); + problem.deleteChildProblem(request.deleteChildProblems()); + + List childProblems = changeToChildProblems(request); + problem.updateChildProblem(childProblems); + + return ProblemGetResponse.of(problemRepository.save(problem)); + } + + private List changeToChildProblems(ProblemUpdateRequest request) { + return request.updateChildProblems().stream() + .map(this::getChildProblem) + .toList(); + } + + private ChildProblem getChildProblem(ChildProblemUpdateRequest updateChildProblem) { + if (updateChildProblem.id() == null) { + return childProblemMapper.from(updateChildProblem); + } + ChildProblem childProblem = childProblemRepository.findByIdElseThrow(updateChildProblem.id()); + childProblem.update(childProblemMapper.from(updateChildProblem)); + return childProblem; + } } diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/service/mapper/ChildProblemMapper.java b/src/main/java/com/moplus/moplus_server/domain/problem/service/mapper/ChildProblemMapper.java index 35da321..d8a6566 100644 --- a/src/main/java/com/moplus/moplus_server/domain/problem/service/mapper/ChildProblemMapper.java +++ b/src/main/java/com/moplus/moplus_server/domain/problem/service/mapper/ChildProblemMapper.java @@ -1,4 +1,4 @@ -package com.moplus.moplus_server.domain.problem.service; +package com.moplus.moplus_server.domain.problem.service.mapper; import com.moplus.moplus_server.domain.problem.domain.childProblem.ChildProblem; import com.moplus.moplus_server.domain.problem.dto.request.ChildProblemPostRequest; diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/service/mapper/ProblemMapper.java b/src/main/java/com/moplus/moplus_server/domain/problem/service/mapper/ProblemMapper.java index 882c71f..f1244a2 100644 --- a/src/main/java/com/moplus/moplus_server/domain/problem/service/mapper/ProblemMapper.java +++ b/src/main/java/com/moplus/moplus_server/domain/problem/service/mapper/ProblemMapper.java @@ -1,4 +1,4 @@ -package com.moplus.moplus_server.domain.problem.service; +package com.moplus.moplus_server.domain.problem.service.mapper; import com.moplus.moplus_server.domain.problem.domain.practiceTest.PracticeTestTag; import com.moplus.moplus_server.domain.problem.domain.problem.Problem; diff --git a/src/main/java/com/moplus/moplus_server/domain/v0/practiceTest/domain/PracticeTest.java b/src/main/java/com/moplus/moplus_server/domain/v0/practiceTest/domain/PracticeTest.java index 92be481..a206e63 100644 --- a/src/main/java/com/moplus/moplus_server/domain/v0/practiceTest/domain/PracticeTest.java +++ b/src/main/java/com/moplus/moplus_server/domain/v0/practiceTest/domain/PracticeTest.java @@ -10,6 +10,7 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import jakarta.persistence.Table; import java.time.Duration; import lombok.Builder; import lombok.Getter; @@ -17,6 +18,7 @@ @Getter @Entity +@Table(name = "practice_test") @NoArgsConstructor public class PracticeTest extends BaseEntity { @@ -32,7 +34,6 @@ public class PracticeTest extends BaseEntity { private long viewCount = 0L; private int solvesCount = 0; private int publicationYear; - private int month = 0; @Enumerated(EnumType.STRING) private Subject subject; @@ -41,15 +42,13 @@ public class PracticeTest extends BaseEntity { @Builder public PracticeTest(String name, String round, String provider, long viewCount, int solvesCount, - int publicationYear, - int month, Subject subject, Duration averageSolvingTime) { + int publicationYear, Subject subject, Duration averageSolvingTime) { this.name = name; this.round = round; this.provider = provider; this.viewCount = viewCount; this.solvesCount = solvesCount; this.publicationYear = publicationYear; - this.month = month; this.subject = subject; this.averageSolvingTime = averageSolvingTime; } diff --git a/src/main/java/com/moplus/moplus_server/domain/v0/practiceTest/repository/PracticeTestRepository.java b/src/main/java/com/moplus/moplus_server/domain/v0/practiceTest/repository/PracticeTestRepository.java index fc3b4f9..e1b5fb1 100644 --- a/src/main/java/com/moplus/moplus_server/domain/v0/practiceTest/repository/PracticeTestRepository.java +++ b/src/main/java/com/moplus/moplus_server/domain/v0/practiceTest/repository/PracticeTestRepository.java @@ -12,6 +12,6 @@ public interface PracticeTestRepository extends JpaRepository findAllByOrderByViewCountDesc(); @Lock(LockModeType.PESSIMISTIC_WRITE) - @Query("select s from PracticeTest s where s.id = :id") + @Query("select s from PracticeTestTag s where s.id = :id") PracticeTest findByIdWithPessimisticLock(@Param("id") Long id); } diff --git a/src/main/java/com/moplus/moplus_server/global/common/BaseEntity.java b/src/main/java/com/moplus/moplus_server/global/common/BaseEntity.java index 82a23e2..2e0b341 100644 --- a/src/main/java/com/moplus/moplus_server/global/common/BaseEntity.java +++ b/src/main/java/com/moplus/moplus_server/global/common/BaseEntity.java @@ -4,7 +4,6 @@ import jakarta.persistence.EntityListeners; import jakarta.persistence.MappedSuperclass; import java.time.LocalDateTime; -import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.experimental.SuperBuilder; @@ -26,9 +25,4 @@ public abstract class BaseEntity { @LastModifiedDate @Column(name = "update_at") private LocalDateTime updatedDate; - - @Column(name = "deleted") - @Builder.Default - private boolean deleted = false; - } diff --git a/src/main/java/com/moplus/moplus_server/global/error/exception/ErrorCode.java b/src/main/java/com/moplus/moplus_server/global/error/exception/ErrorCode.java index 3968ed9..eb740fd 100644 --- a/src/main/java/com/moplus/moplus_server/global/error/exception/ErrorCode.java +++ b/src/main/java/com/moplus/moplus_server/global/error/exception/ErrorCode.java @@ -33,6 +33,9 @@ public enum ErrorCode { INVALID_MULTIPLE_CHOICE_ANSWER(HttpStatus.BAD_REQUEST, "객관식 문제의 정답은 1~5 사이의 숫자여야 합니다"), INVALID_SHORT_NUMBER_ANSWER(HttpStatus.BAD_REQUEST, "주관식 문제의 정답은 0~999 사이의 숫자여야 합니다"), + //새끼 문항 + CHILD_PROBLEM_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 새끼 문제를 찾을 수 없습니다"), + //개념태그 CONCEPT_TAG_NOT_FOUND_IN_LIST(HttpStatus.NOT_FOUND, "해당 리스트 중 존재하지 않는 개념 태그가 있습니다."), diff --git a/src/test/java/com/moplus/moplus_server/domain/problem/domain/problem/ProblemIdServiceTest.java b/src/test/java/com/moplus/moplus_server/domain/problem/domain/problem/ProblemIdServiceTest.java index 30e0157..629e128 100644 --- a/src/test/java/com/moplus/moplus_server/domain/problem/domain/problem/ProblemIdServiceTest.java +++ b/src/test/java/com/moplus/moplus_server/domain/problem/domain/problem/ProblemIdServiceTest.java @@ -1,4 +1,78 @@ -import static org.junit.jupiter.api.Assertions.*; +package com.moplus.moplus_server.domain.problem.domain.problem; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.moplus.moplus_server.domain.problem.domain.practiceTest.PracticeTestTag; +import com.moplus.moplus_server.domain.problem.domain.practiceTest.Subject; +import com.moplus.moplus_server.domain.problem.repository.ProblemRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) class ProblemIdServiceTest { - + + @Mock + private ProblemRepository problemRepository; + + @InjectMocks + private ProblemIdService problemIdService; + + private PracticeTestTag practiceTestTag; + + @BeforeEach + void setUp() { + practiceTestTag = Mockito.mock(PracticeTestTag.class); + when(practiceTestTag.getSubject()).thenReturn(Subject.고2); + when(practiceTestTag.getYear()).thenReturn(2024); + when(practiceTestTag.getMonth()).thenReturn(5); + } + + @Test + void nextId_정상생성_및_중복확인() { + // given + int 문제번호 = 20; + when(problemRepository.existsById(any(ProblemId.class))).thenReturn(false); // 중복 없음 + + // when + ProblemId generatedId = problemIdService.nextId(문제번호, practiceTestTag); + + // then + assertThat(generatedId).isNotNull(); + assertThat(generatedId.getId()).matches("\\d{13}"); // ID 형식이 맞는지 확인 + assertThat(generatedId.getId()).startsWith("2405200120"); + + // 문제 ID 중복 확인을 위해 existsById 호출 확인 + verify(problemRepository, atLeastOnce()).existsById(any(ProblemId.class)); + + } + + @Test + void nextId_중복발생시_다시_생성() { + // given + int 문제번호 = 2; + when(problemRepository.existsById(any(ProblemId.class))) + .thenReturn(true) // 첫 번째 생성된 ID는 중복됨 + .thenReturn(false); // 두 번째는 중복 없음 + + // when + ProblemId generatedId = problemIdService.nextId(문제번호, practiceTestTag); + + // then + assertThat(generatedId).isNotNull(); + assertThat(generatedId.getId()).matches("\\d{13}"); + assertThat(generatedId.getId()).startsWith("2405020120"); + + // 중복된 ID가 나왔으므로 existsById가 최소 두 번 이상 호출되었는지 확인 + verify(problemRepository, atLeast(2)).existsById(any(ProblemId.class)); + } } \ No newline at end of file diff --git a/src/test/java/com/moplus/moplus_server/domain/problem/service/ProblemSaveServiceTest.java b/src/test/java/com/moplus/moplus_server/domain/problem/service/ProblemSaveServiceTest.java index 2fcce6b..65db7b9 100644 --- a/src/test/java/com/moplus/moplus_server/domain/problem/service/ProblemSaveServiceTest.java +++ b/src/test/java/com/moplus/moplus_server/domain/problem/service/ProblemSaveServiceTest.java @@ -1,4 +1,150 @@ -import static org.junit.jupiter.api.Assertions.*; +package com.moplus.moplus_server.domain.problem.service; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.moplus.moplus_server.domain.problem.domain.childProblem.ChildProblem; +import com.moplus.moplus_server.domain.problem.domain.problem.Problem; +import com.moplus.moplus_server.domain.problem.domain.problem.ProblemId; +import com.moplus.moplus_server.domain.problem.domain.problem.ProblemType; +import com.moplus.moplus_server.domain.problem.dto.request.ChildProblemPostRequest; +import com.moplus.moplus_server.domain.problem.dto.request.ProblemPostRequest; +import com.moplus.moplus_server.domain.problem.repository.ProblemRepository; +import java.util.List; +import java.util.Set; +import java.util.stream.IntStream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.Rollback; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.transaction.annotation.Transactional; + +@Transactional +@ActiveProfiles("h2test") +@Sql({"/practice-test-tag.sql", "/concept-tag.sql"}) +@SpringBootTest class ProblemSaveServiceTest { - + + @Autowired + private ProblemSaveService problemSaveService; + + @Autowired + private ProblemRepository problemRepository; + + private ProblemPostRequest problemPostRequestOutOfOrder; + private ProblemPostRequest problemPostRequestInOrder; + + @BeforeEach + void setUp() { + // 🔹 1. 일부러 순서를 뒤죽박죽으로 설정한 문제 + ChildProblemPostRequest childProblem1 = new ChildProblemPostRequest( + "child1.png", ProblemType.SHORT_STRING_ANSWER, "정답1", Set.of(3L, 4L), 3 + ); + ChildProblemPostRequest childProblem2 = new ChildProblemPostRequest( + "child2.png", ProblemType.MULTIPLE_CHOICE, "1", Set.of(5L, 6L), 1 + ); + ChildProblemPostRequest childProblem3 = new ChildProblemPostRequest( + "child3.png", ProblemType.MULTIPLE_CHOICE, "2", Set.of(3L, 4L), 0 + ); + ChildProblemPostRequest childProblem4 = new ChildProblemPostRequest( + "child4.png", ProblemType.SHORT_NUMBER_ANSWER, "0", Set.of(1L, 2L), 2 + ); + + problemPostRequestOutOfOrder = new ProblemPostRequest( + Set.of(1L, 2L), + 1L, + 21, + "1", + "설명", + "mainProblem.png", + "mainAnalysis.png", + "readingTip.png", + "seniorTip.png", + "prescription.png", + List.of(childProblem1, childProblem2, childProblem3, childProblem4) // 🔹 순서 뒤죽박죽 + ); + + // 🔹 2. 순서가 올바른 상태에서 입력되는 문제 + problemPostRequestInOrder = new ProblemPostRequest( + Set.of(1L, 2L), + 1L, + 20, + "2", + "다른 설명", + "mainProblem2.png", + "mainAnalysis2.png", + "readingTip2.png", + "seniorTip2.png", + "prescription2.png", + List.of(childProblem3, childProblem2, childProblem4, childProblem1) // 🔹 순서 유지 (0,1,2,3) + ); + } + + @Test + @Rollback(value = false) + void 정상동작() { + + // when + ProblemId createdProblemId = problemSaveService.createProblem(problemPostRequestInOrder); + + // then + assertThat(createdProblemId).isNotNull(); + assertThat(createdProblemId.getId()).startsWith("2405200120"); // ID 앞부분 확인 + + Problem savedProblem = problemRepository.findByIdElseThrow(createdProblemId); + + // 모든 자식 문제의 conceptTagIds가 부모 문제의 conceptTagIds에 포함되는지 검증 + Set problemTags = savedProblem.getConceptTagIds(); + problemPostRequestInOrder.childProblems().forEach(child -> { + assertThat(problemTags).containsAll(child.conceptTagIds()); + }); + + // 자식 문제의 순서 검증 + List childProblems = savedProblem.getChildProblems(); + + assertThat(childProblems).hasSize(4); // 자식 문제 개수 검증 + + // 저장된 자식 문제가 원래 요청한 `sequence` 순서와 같은지 확인 + IntStream.range(0, childProblems.size()).forEach(i -> { + assertThat(childProblems.get(i).getSequence()).isEqualTo(i); + }); + } + + @Test + @Rollback(true) + void 자식문제_올바른_순서_저장() { + // when + ProblemId createdProblemId = problemSaveService.createProblem(problemPostRequestOutOfOrder); + + // then + assertThat(createdProblemId).isNotNull(); + assertThat(createdProblemId.getId()).startsWith("2405210120"); // ID 앞부분 확인 + + // 저장된 문제 조회 + Problem savedProblem = problemRepository.findByIdElseThrow(createdProblemId); + + // ✅ 모든 자식 문제의 conceptTagIds가 부모 문제의 conceptTagIds에 포함되는지 검증 + Set problemTags = savedProblem.getConceptTagIds(); + problemPostRequestOutOfOrder.childProblems().forEach(child -> { + assertThat(problemTags).containsAll(child.conceptTagIds()); + }); + + // ✅ 자식 문제의 순서 검증 + List childProblems = savedProblem.getChildProblems(); + + assertThat(childProblems).hasSize(4); // 자식 문제 개수 검증 + + // 🔹 저장된 자식 문제들이 `sequence` 오름차순으로 정렬되었는지 확인 + IntStream.range(0, childProblems.size()).forEach(i -> { + assertThat(childProblems.get(i).getSequence()).isEqualTo(i); + }); + + // 🔹 정렬 후 올바른 문제인지 검증 + assertThat(childProblems.get(0).getImageUrl()).isEqualTo("child3.png"); // sequence 0 + assertThat(childProblems.get(1).getImageUrl()).isEqualTo("child2.png"); // sequence 1 + assertThat(childProblems.get(2).getImageUrl()).isEqualTo("child4.png"); // sequence 2 + assertThat(childProblems.get(3).getImageUrl()).isEqualTo("child1.png"); // sequence 3 + } } \ No newline at end of file diff --git a/src/test/java/com/moplus/moplus_server/domain/problem/service/ProblemUpdateServiceTest.java b/src/test/java/com/moplus/moplus_server/domain/problem/service/ProblemUpdateServiceTest.java index 3231e35..6d59ad5 100644 --- a/src/test/java/com/moplus/moplus_server/domain/problem/service/ProblemUpdateServiceTest.java +++ b/src/test/java/com/moplus/moplus_server/domain/problem/service/ProblemUpdateServiceTest.java @@ -1,4 +1,112 @@ -import static org.junit.jupiter.api.Assertions.*; +package com.moplus.moplus_server.domain.problem.service; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.moplus.moplus_server.domain.problem.domain.childProblem.ChildProblem; +import com.moplus.moplus_server.domain.problem.domain.problem.Problem; +import com.moplus.moplus_server.domain.problem.domain.problem.ProblemId; +import com.moplus.moplus_server.domain.problem.domain.problem.ProblemType; +import com.moplus.moplus_server.domain.problem.dto.request.ChildProblemUpdateRequest; +import com.moplus.moplus_server.domain.problem.dto.request.ProblemUpdateRequest; +import com.moplus.moplus_server.domain.problem.dto.response.ProblemGetResponse; +import com.moplus.moplus_server.domain.problem.repository.ProblemRepository; +import java.util.List; +import java.util.Set; +import java.util.stream.IntStream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.transaction.annotation.Transactional; + +@Transactional +@ActiveProfiles("h2test") +@Sql({"/practice-test-tag.sql", "/concept-tag.sql", "/insert-problem.sql"}) +@SpringBootTest class ProblemUpdateServiceTest { - + + @Autowired + private ProblemUpdateService problemUpdateService; + + @Autowired + private ProblemRepository problemRepository; + + private ProblemId problemId; + private ProblemUpdateRequest problemUpdateRequest; + + @BeforeEach + void setUp() { + problemId = new ProblemId("240520012001"); + + // 🔹 새 자식 문제 추가 + ChildProblemUpdateRequest newChildProblem = new ChildProblemUpdateRequest( + null, + "newChild.png", + ProblemType.SHORT_STRING_ANSWER, + "새로운 정답", + Set.of(1L, 2L), + 1 + ); + + // 🔹 기존 자식 문제 업데이트 + ChildProblemUpdateRequest updateChildProblem = new ChildProblemUpdateRequest( + 1L, // 기존 자식 문제 ID + "updatedChild.png", + ProblemType.MULTIPLE_CHOICE, + "2", + Set.of(2L, 3L), + 0 + ); + + // 🔹 기존 자식 문제 삭제 + List deleteChildProblem = List.of(2L); // 삭제할 자식 문제 ID + + problemUpdateRequest = new ProblemUpdateRequest( + Set.of(1L, 2L, 3L), // 업데이트할 부모 문제의 Concept Tags + 1, // 문제 정답 + "수정된 설명", // 새로운 설명 + "updatedMainProblem.png", + "updatedMainAnalysis.png", + "updatedReadingTip.png", + "updatedSeniorTip.png", + "updatedPrescription.png", + List.of(newChildProblem, updateChildProblem), // 업데이트할 자식 문제 + deleteChildProblem // 삭제할 자식 문제 + ); + } + + @Test + void 문제_업데이트_정상동작() { + // when + ProblemGetResponse response = problemUpdateService.updateProblem(problemId.getId(), + problemUpdateRequest); + + // then + assertThat(response).isNotNull(); + assertThat(response.comment()).isEqualTo("수정된 설명"); // ✅ 설명이 변경되었는지 검증 + assertThat(response.mainProblemImageUrl()).isEqualTo("updatedMainProblem.png"); // ✅ 이미지 URL 변경 확인 + + Problem updatedProblem = problemRepository.findByIdElseThrow(problemId); + + // ✅ 자식 문제 개수 검증 + List childProblems = updatedProblem.getChildProblems(); + assertThat(childProblems).hasSize(2); // 기존 2개 → 1개 삭제, 1개 추가 후 2개 + + // ✅ 부모 문제의 conceptTagIds가 자식 문제의 conceptTagIds를 모두 포함하는지 검증 + Set problemTags = updatedProblem.getConceptTagIds(); + updatedProblem.getChildProblems().forEach(child -> { + assertThat(problemTags).containsAll(child.getConceptTagIds()); + }); + + // ✅ 자식 문제 순서가 올바르게 정렬되었는지 확인 + IntStream.range(0, childProblems.size()).forEach(i -> { + assertThat(childProblems.get(i).getSequence()).isEqualTo(i); + }); + + // ✅ 개별 자식 문제 검증 + assertThat(childProblems.get(0).getImageUrl()).isEqualTo("updatedChild.png"); // 기존 자식 문제 업데이트 확인 + assertThat(childProblems.get(1).getImageUrl()).isEqualTo("newChild.png"); // 새 자식 문제 추가 확인 + } } \ No newline at end of file diff --git a/src/test/resources/auth-test-data.sql b/src/test/resources/auth-test-data.sql index 595162a..cb7319e 100644 --- a/src/test/resources/auth-test-data.sql +++ b/src/test/resources/auth-test-data.sql @@ -1,4 +1,4 @@ -INSERT INTO member (deleted, created_at, update_at, member_id, email, password, name, role) -VALUES (false, '2024-07-24 21:27:20.000000', '2024-07-24 21:27:21.000000', 1, 'admin@example.com', +INSERT INTO member (created_at, update_at, member_id, email, password, name, role) +VALUES ('2024-07-24 21:27:20.000000', '2024-07-24 21:27:21.000000', 1, 'admin@example.com', 'password123', '홍길동', 'ADMIN'); diff --git a/src/test/resources/concept-tag.sql b/src/test/resources/concept-tag.sql index e69de29..6b94678 100644 --- a/src/test/resources/concept-tag.sql +++ b/src/test/resources/concept-tag.sql @@ -0,0 +1,12 @@ +INSERT INTO concept_tag (name) +VALUES ('미분 개념'); +INSERT INTO concept_tag (name) +VALUES ('적분 개념'); +INSERT INTO concept_tag (name) +VALUES ('삼각함수 개념'); +INSERT INTO concept_tag (name) +VALUES ('행렬 개념'); +INSERT INTO concept_tag (name) +VALUES ('확률과 통계 개념'); +INSERT INTO concept_tag (name) +VALUES ('기하 개념'); \ No newline at end of file diff --git a/src/test/resources/insert-problem.sql b/src/test/resources/insert-problem.sql index e69de29..9f81e33 100644 --- a/src/test/resources/insert-problem.sql +++ b/src/test/resources/insert-problem.sql @@ -0,0 +1,29 @@ +DELETE +FROM child_problem_concept; +DELETE +FROM child_problem; + +INSERT INTO problem (problem_id, practice_test_id, number, answer, comment, main_problem_image_url, + main_analysis_image_url, reading_tip_image_url, senior_tip_image_url, prescription_image_url, + is_published, is_variation) +VALUES ('240520012001', 1, 1, '1', '기존 문제 설명', + 'mainProblem.png', 'mainAnalysis.png', 'readingTip.png', 'seniorTip.png', 'prescription.png', + false, false); + +-- ✅ 기존 자식 문제(ChildProblem) 삽입 +INSERT INTO child_problem (child_problem_id, problem_id, image_url, problem_type, answer, sequence) +VALUES (1, '240520012001', 'child1.png', 'MULTIPLE_CHOICE', '1', 0), + (2, '240520012001', 'child2.png', 'SHORT_STRING_ANSWER', '정답2', 0); + +-- ✅ 문제-컨셉 태그 연결 (기존 문제의 ConceptTag) +INSERT INTO problem_concept (problem_id, concept_tag_id) +VALUES ('240520012001', 1), + ('240520012001', 2), + ('240520012001', 3); + +-- ✅ 자식 문제-컨셉 태그 연결 +INSERT INTO child_problem_concept (child_problem_id, concept_tag_id) +VALUES (1, 3), + (1, 4), + (2, 5), + (2, 6); \ No newline at end of file From 22c15cf4872ba7dc4161dc7f2126ec1ae52bd5b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=84=B8=EC=A4=80?= <74056843+sejoon00@users.noreply.github.com> Date: Thu, 30 Jan 2025 22:13:22 +0900 Subject: [PATCH 4/5] =?UTF-8?q?[fix/#23]=20updateChildProblem=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=ED=95=84=EC=9A=94=EC=97=86=EB=8A=94=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../moplus_server/domain/problem/domain/problem/Problem.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/domain/problem/Problem.java b/src/main/java/com/moplus/moplus_server/domain/problem/domain/problem/Problem.java index b574ba4..b059bf3 100644 --- a/src/main/java/com/moplus/moplus_server/domain/problem/domain/problem/Problem.java +++ b/src/main/java/com/moplus/moplus_server/domain/problem/domain/problem/Problem.java @@ -100,11 +100,8 @@ public void update(Problem inputProblem) { } public void updateChildProblem(List inputChildProblems) { - List mutableChildProblems = new ArrayList<>(inputChildProblems); - mutableChildProblems.sort(Comparator.comparingInt(ChildProblem::getSequence)); - inputChildProblems.forEach(childProblem -> { - childProblems.stream() + this.childProblems.stream() .filter(existingChildProblem -> existingChildProblem.getId().equals(childProblem.getId())) .findFirst() .ifPresentOrElse( From c389d797b39b171585f9051c9da07849f720e8de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=84=B8=EC=A4=80?= <74056843+sejoon00@users.noreply.github.com> Date: Thu, 30 Jan 2025 22:45:04 +0900 Subject: [PATCH 5/5] =?UTF-8?q?[feature/#23]=20=EB=AA=A8=EC=9D=98=EA=B3=A0?= =?UTF-8?q?=EC=82=AC=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20swagger?= =?UTF-8?q?=20=EC=84=A4=EB=AA=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/problem/controller/PracticeTestTagController.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/controller/PracticeTestTagController.java b/src/main/java/com/moplus/moplus_server/domain/problem/controller/PracticeTestTagController.java index 443b7bc..1534257 100644 --- a/src/main/java/com/moplus/moplus_server/domain/problem/controller/PracticeTestTagController.java +++ b/src/main/java/com/moplus/moplus_server/domain/problem/controller/PracticeTestTagController.java @@ -2,6 +2,7 @@ import com.moplus.moplus_server.domain.problem.dto.response.PracticeTestTagResponse; import com.moplus.moplus_server.domain.problem.repository.PracticeTestTagRepository; +import io.swagger.v3.oas.annotations.Operation; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -17,6 +18,7 @@ public class PracticeTestTagController { private final PracticeTestTagRepository practiceTestTagRepository; @GetMapping("") + @Operation(summary = "모의고사 목록 조회") public ResponseEntity> getPracticeTestTags() { List responses = practiceTestTagRepository.findAll().stream() .map(PracticeTestTagResponse::of)