diff --git a/src/main/generated/com/moplus/moplus_server/domain/problem/domain/problem/QProblem.java b/src/main/generated/com/moplus/moplus_server/domain/problem/domain/problem/QProblem.java index 4850572..c06ac88 100644 --- a/src/main/generated/com/moplus/moplus_server/domain/problem/domain/problem/QProblem.java +++ b/src/main/generated/com/moplus/moplus_server/domain/problem/domain/problem/QProblem.java @@ -61,6 +61,8 @@ public class QProblem extends EntityPathBase { public final StringPath readingTipImageUrl = createString("readingTipImageUrl"); + public final QRecommendedTime recommendedTime; + public final StringPath seniorTipImageUrl = createString("seniorTipImageUrl"); public final QTitle title; @@ -89,6 +91,7 @@ public QProblem(Class type, PathMetadata metadata, PathInits this.answer = inits.isInitialized("answer") ? new com.moplus.moplus_server.domain.problem.domain.QAnswer(forProperty("answer")) : null; this.difficulty = inits.isInitialized("difficulty") ? new QDifficulty(forProperty("difficulty")) : null; this.problemCustomId = inits.isInitialized("problemCustomId") ? new QProblemCustomId(forProperty("problemCustomId")) : null; + this.recommendedTime = inits.isInitialized("recommendedTime") ? new QRecommendedTime(forProperty("recommendedTime")) : null; this.title = inits.isInitialized("title") ? new QTitle(forProperty("title")) : null; } diff --git a/src/main/generated/com/moplus/moplus_server/domain/problem/domain/problem/QRecommendedTime.java b/src/main/generated/com/moplus/moplus_server/domain/problem/domain/problem/QRecommendedTime.java new file mode 100644 index 0000000..2ef9140 --- /dev/null +++ b/src/main/generated/com/moplus/moplus_server/domain/problem/domain/problem/QRecommendedTime.java @@ -0,0 +1,39 @@ +package com.moplus.moplus_server.domain.problem.domain.problem; + +import static com.querydsl.core.types.PathMetadataFactory.*; + +import com.querydsl.core.types.dsl.*; + +import com.querydsl.core.types.PathMetadata; +import javax.annotation.processing.Generated; +import com.querydsl.core.types.Path; + + +/** + * QRecommendedTime is a Querydsl query type for RecommendedTime + */ +@Generated("com.querydsl.codegen.DefaultEmbeddableSerializer") +public class QRecommendedTime extends BeanPath { + + private static final long serialVersionUID = -1102611877L; + + public static final QRecommendedTime recommendedTime = new QRecommendedTime("recommendedTime"); + + public final NumberPath minute = createNumber("minute", Integer.class); + + public final NumberPath second = createNumber("second", Integer.class); + + public QRecommendedTime(String variable) { + super(RecommendedTime.class, forVariable(variable)); + } + + public QRecommendedTime(Path path) { + super(path.getType(), path.getMetadata()); + } + + public QRecommendedTime(PathMetadata metadata) { + super(RecommendedTime.class, metadata); + } + +} + 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 index ab47b56..f3bf9ff 100644 --- 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 @@ -15,7 +15,7 @@ @Generated( value = "org.mapstruct.ap.MappingProcessor", - date = "2025-02-18T19:54:34+0900", + date = "2025-02-18T21:41:47+0900", comments = "version: 1.6.3, compiler: javac, environment: Java 17.0.10 (JetBrains s.r.o.)" ) @Component @@ -48,6 +48,8 @@ public Problem from(ProblemUpdateRequest request, ProblemCustomId problemCustomI Problem.ProblemBuilder problem = Problem.builder(); if ( request != null ) { + problem.recommendedMinute( request.recommendedMinute() ); + problem.recommendedSecond( request.recommendedSecond() ); problem.answerType( request.answerType() ); Set set = request.conceptTagIds(); if ( set != null ) { diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/controller/ProblemSearchController.java b/src/main/java/com/moplus/moplus_server/domain/problem/controller/ProblemSearchController.java index fab72f2..7deed3b 100644 --- a/src/main/java/com/moplus/moplus_server/domain/problem/controller/ProblemSearchController.java +++ b/src/main/java/com/moplus/moplus_server/domain/problem/controller/ProblemSearchController.java @@ -27,10 +27,10 @@ public class ProblemSearchController { ) public ResponseEntity> search( @RequestParam(value = "problemCustomId", required = false) String problemCustomId, - @RequestParam(value = "comment", required = false) String comment, + @RequestParam(value = "memo", required = false) String memo, @RequestParam(value = "conceptTagIds", required = false) List conceptTagIds ) { - List problems = problemSearchRepository.search(problemCustomId, comment, + List problems = problemSearchRepository.search(problemCustomId, memo, conceptTagIds); return ResponseEntity.ok(problems); } 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 9ca4b79..f4257c2 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 @@ -80,13 +80,16 @@ public class Problem extends BaseEntity { @OrderColumn(name = "sequence") private List childProblems = new ArrayList<>(); + @Embedded + private RecommendedTime recommendedTime; + @Builder public Problem(List childProblems, boolean isConfirmed, AnswerType answerType, Set conceptTagIds, Integer difficulty, String mainHandwritingExplanationImageUrl, List prescriptionImageUrls, String seniorTipImageUrl, String readingTipImageUrl, String mainAnalysisImageUrl, String mainProblemImageUrl, String memo, String answer, String title, ProblemType problemType, int number, PracticeTestTag practiceTestTag, - ProblemCustomId problemCustomId) { + ProblemCustomId problemCustomId, Integer recommendedMinute, Integer recommendedSecond) { this.childProblems = childProblems; this.isConfirmed = isConfirmed; this.answerType = answerType; @@ -105,6 +108,7 @@ public Problem(List childProblems, boolean isConfirmed, AnswerType this.number = number; this.practiceTestId = practiceTestTag != null ? practiceTestTag.getId() : null; this.problemCustomId = problemCustomId; + this.recommendedTime = new RecommendedTime(recommendedMinute, recommendedSecond); } public String getAnswer() { @@ -123,11 +127,15 @@ public void update(Problem inputProblem) { this.memo = inputProblem.getMemo(); this.mainProblemImageUrl = inputProblem.getMainProblemImageUrl(); this.mainAnalysisImageUrl = inputProblem.getMainAnalysisImageUrl(); - this.mainHandwritingExplanationImageUrl = inputProblem.getMainHandwritingExplanationImageUrl(); // 추가 + this.mainHandwritingExplanationImageUrl = inputProblem.getMainHandwritingExplanationImageUrl(); this.readingTipImageUrl = inputProblem.getReadingTipImageUrl(); this.seniorTipImageUrl = inputProblem.getSeniorTipImageUrl(); this.prescriptionImageUrls = inputProblem.getPrescriptionImageUrls(); this.answerType = inputProblem.getAnswerType(); + this.recommendedTime = new RecommendedTime( + inputProblem.getRecommendedTime() != null ? inputProblem.getRecommendedTime().getMinute() : null, + inputProblem.getRecommendedTime() != null ? inputProblem.getRecommendedTime().getSecond() : null + ); } public void updateChildProblem(List inputChildProblems) { @@ -156,7 +164,8 @@ public boolean isValid() { && prescriptionImageUrls != null && !prescriptionImageUrls.isEmpty() && prescriptionImageUrls.stream().allMatch(url -> url != null && !url.isEmpty()) && answerType != null - && conceptTagIds != null && !conceptTagIds.isEmpty(); + && conceptTagIds != null && !conceptTagIds.isEmpty() + && recommendedTime != null; } public String getTitle() { diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/domain/problem/RecommendedTime.java b/src/main/java/com/moplus/moplus_server/domain/problem/domain/problem/RecommendedTime.java new file mode 100644 index 0000000..8c0b8f7 --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/domain/problem/domain/problem/RecommendedTime.java @@ -0,0 +1,34 @@ +package com.moplus.moplus_server.domain.problem.domain.problem; + +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 RecommendedTime { + @Column(name = "recommended_minute") + private Integer minute; + @Column(name = "recommended_second") + private Integer second; + + public RecommendedTime(Integer minute, Integer second) { + validateTime(minute, second); + this.minute = minute; + this.second = second; + } + + private void validateTime(Integer minute, Integer second) { + if (minute != null && (minute < 0 || minute > 60)) { + throw new InvalidValueException(ErrorCode.INVALID_INPUT_VALUE); + } + if (second != null && (second < 0 || second > 60)) { + throw new InvalidValueException(ErrorCode.INVALID_INPUT_VALUE); + } + } +} \ No newline at end of file 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 a4d0971..1603133 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 @@ -23,6 +23,8 @@ public record ProblemUpdateRequest( String seniorTipImageUrl, List prescriptionImageUrls, AnswerType answerType, - List updateChildProblems + List updateChildProblems, + Integer recommendedMinute, + Integer recommendedSecond ) { } 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 index eadc866..2d4fd4b 100644 --- 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 @@ -29,7 +29,9 @@ public record ProblemGetResponse( String readingTipImageUrl, String seniorTipImageUrl, List prescriptionImageUrls, - List childProblems + List childProblems, + Integer recommendedMinute, + Integer recommendedSecond ) { public static ProblemGetResponse of(Problem problem) { @@ -55,6 +57,8 @@ public static ProblemGetResponse of(Problem problem) { .childProblems(problem.getChildProblems().stream() .map(ChildProblemGetResponse::of) .toList()) + .recommendedMinute(problem.getRecommendedTime().getMinute()) + .recommendedSecond(problem.getRecommendedTime().getSecond()) .build(); } } diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/repository/ProblemSearchRepositoryCustom.java b/src/main/java/com/moplus/moplus_server/domain/problem/repository/ProblemSearchRepositoryCustom.java index 9c57fd1..bec194e 100644 --- a/src/main/java/com/moplus/moplus_server/domain/problem/repository/ProblemSearchRepositoryCustom.java +++ b/src/main/java/com/moplus/moplus_server/domain/problem/repository/ProblemSearchRepositoryCustom.java @@ -19,13 +19,13 @@ public class ProblemSearchRepositoryCustom { private final JPAQueryFactory queryFactory; - public List search(String problemId, String comment, List conceptTagIds) { + public List search(String problemId, String memo, List conceptTagIds) { return queryFactory .select(problem.problemCustomId.id, problem.memo, problem.mainProblemImageUrl) .from(problem) .where( containsProblemId(problemId), - containsName(comment), + containsName(memo), inConceptTagIds(conceptTagIds) ) .leftJoin(conceptTag).on(conceptTag.id.in(problem.conceptTagIds)).fetchJoin() 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 0ff784c..2f67b1d 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 @@ -22,6 +22,8 @@ public interface ProblemMapper { @Mappings({ @Mapping(target = "problemCustomId", source = "problemCustomId"), @Mapping(target = "practiceTestTag", source = "practiceTestTag"), + @Mapping(target = "recommendedMinute", source = "request.recommendedMinute"), + @Mapping(target = "recommendedSecond", source = "request.recommendedSecond") }) Problem from(ProblemUpdateRequest request, ProblemCustomId problemCustomId, PracticeTestTag practiceTestTag); diff --git a/src/test/java/com/moplus/moplus_server/domain/problem/domain/problem/ProblemCustomIdServiceTest.java b/src/test/java/com/moplus/moplus_server/domain/problem/domain/problem/ProblemCustomIdServiceTest.java index 00c0f57..c55b27b 100644 --- a/src/test/java/com/moplus/moplus_server/domain/problem/domain/problem/ProblemCustomIdServiceTest.java +++ b/src/test/java/com/moplus/moplus_server/domain/problem/domain/problem/ProblemCustomIdServiceTest.java @@ -17,7 +17,9 @@ import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.transaction.annotation.Transactional; +@Transactional @ExtendWith(MockitoExtension.class) class ProblemCustomIdServiceTest { 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 9b9145d..a9d2679 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 @@ -71,12 +71,14 @@ void setUp() { "업데이트된 메모", "updatedMainProblem.png", "updatedMainAnalysis.png", - "updatedMainHandwriting.png", // 추가 + "updatedMainHandwriting.png", "updatedReadingTip.png", "updatedSeniorTip.png", - List.of("prescription1.png", "prescription2.png"), // List으로 변경 + List.of("prescription1.png", "prescription2.png"), AnswerType.SHORT_STRING_ANSWER, - List.of(updateChildProblem1, updateChildProblem2) + List.of(updateChildProblem1, updateChildProblem2), + 30, + 45 ); } @@ -133,6 +135,10 @@ class 문제_업데이트_정상_동작 { assertThat(newChild.getAnswerType()).isEqualTo(AnswerType.SHORT_STRING_ANSWER); assertThat(newChild.getAnswer()).isEqualTo("23"); assertThat(newChild.getConceptTagIds()).containsExactlyInAnyOrderElementsOf(Set.of(3L, 4L)); + + // 추가된 검증 + assertThat(response.recommendedMinute()).isEqualTo(30); + assertThat(response.recommendedSecond()).isEqualTo(45); } } @@ -163,12 +169,14 @@ class 문제_업데이트_예외_처리 { "잘못된 메모", "updatedMainProblem.png", "updatedMainAnalysis.png", - "updatedMainHandwriting.png", // 추가 + "updatedMainHandwriting.png", "updatedReadingTip.png", "updatedSeniorTip.png", - List.of("prescription1.png"), // List으로 변경 + List.of("prescription1.png"), AnswerType.SHORT_STRING_ANSWER, - List.of() + List.of(), + 30, + 45 ); // when & then diff --git a/src/test/resources/concept-tag.sql b/src/test/resources/concept-tag.sql index 1d06a9a..3485a8b 100644 --- a/src/test/resources/concept-tag.sql +++ b/src/test/resources/concept-tag.sql @@ -1,3 +1,5 @@ +DELETE FROM concept_tag; + INSERT INTO concept_tag (concept_tag_id, name) VALUES (1, '미분 개념'), (2, '적분 개념'), diff --git a/src/test/resources/insert-problem.sql b/src/test/resources/insert-problem.sql index 03bc152..0e6a2d4 100644 --- a/src/test/resources/insert-problem.sql +++ b/src/test/resources/insert-problem.sql @@ -1,3 +1,9 @@ +DELETE FROM child_problem_concept; +DELETE FROM problem_concept; +DELETE FROM child_problem; +DELETE FROM problem; + +-- 데이터 삽입 INSERT INTO problem (problem_id, problem_custom_id, practice_test_id, @@ -13,16 +19,29 @@ INSERT INTO problem (problem_id, senior_tip_image_url, prescription_image_urls, answer_type, - is_confirmed) + is_confirmed, + recommended_minute, + recommended_second) VALUES (1, '1224052001', 1, 1, 'GICHUL_PROBLEM', '제목1', '1', 5, '기존 문제 설명 1', 'mainProblem.png1', 'mainAnalysis.png1', 'readingTip.png1', 'seniorTip.png1', - 'prescription.png1', 'MULTIPLE_CHOICE', false), + 'prescription.png1', 'MULTIPLE_CHOICE', false, 30, 45), (2, '1224052002', 1, 1, 'GICHUL_PROBLEM', '제목2', '1', 5, '기존 문제 설명 2', 'mainProblem.png2', 'mainAnalysis.png2', 'readingTip.png2', 'seniorTip.png2', - 'prescription.png2', 'MULTIPLE_CHOICE', false); + 'prescription.png2', 'MULTIPLE_CHOICE', false, 25, 30); + +-- 자식 문제 테이블 생성 +CREATE TABLE IF NOT EXISTS child_problem ( + child_problem_id BIGINT PRIMARY KEY, + problem_id BIGINT, + image_url VARCHAR(255), + answer_type VARCHAR(50), + answer VARCHAR(255), + sequence INT +); +-- 자식 문제 데이터 삽입 INSERT INTO child_problem (child_problem_id, - problem_id, -- Long 타입으로 변경 (부모 Problem의 id 참조) + problem_id, image_url, answer_type, answer, @@ -30,18 +49,30 @@ INSERT INTO child_problem (child_problem_id, VALUES (1, 1, 'child1.png', 'MULTIPLE_CHOICE', '1', 0), (2, 1, 'child2.png', 'SHORT_STRING_ANSWER', '정답2', 1); --- 문제-컨셉 태그 연결 (기존 문제의 ConceptTag) -INSERT INTO problem_concept (problem_id, -- Long 타입으로 변경 (Problem의 id 참조) - concept_tag_id) +-- 문제-컨셉 태그 연결 테이블 생성 +CREATE TABLE IF NOT EXISTS problem_concept ( + problem_id BIGINT, + concept_tag_id BIGINT, + PRIMARY KEY (problem_id, concept_tag_id) +); + +-- 문제-컨셉 태그 데이터 삽입 +INSERT INTO problem_concept (problem_id, concept_tag_id) VALUES (1, 1), (1, 2), (1, 3), (2, 1), (2, 3); --- 자식 문제-컨셉 태그 연결 -INSERT INTO child_problem_concept (child_problem_id, - concept_tag_id) +-- 자식 문제-컨셉 태그 연결 테이블 생성 +CREATE TABLE IF NOT EXISTS child_problem_concept ( + child_problem_id BIGINT, + concept_tag_id BIGINT, + PRIMARY KEY (child_problem_id, concept_tag_id) +); + +-- 자식 문제-컨셉 태그 데이터 삽입 +INSERT INTO child_problem_concept (child_problem_id, concept_tag_id) VALUES (1, 3), (1, 4), (2, 5), diff --git a/src/test/resources/insert-problem2.sql b/src/test/resources/insert-problem2.sql index 75f6573..ee909a8 100644 --- a/src/test/resources/insert-problem2.sql +++ b/src/test/resources/insert-problem2.sql @@ -1,3 +1,8 @@ +DELETE FROM child_problem_concept; +DELETE FROM problem_concept; +DELETE FROM child_problem; +DELETE FROM problem; + -- problem 데이터 삽입 INSERT INTO problem (problem_id, problem_custom_id, @@ -15,18 +20,23 @@ INSERT INTO problem (problem_id, senior_tip_image_url, prescription_image_urls, answer_type, - is_confirmed) + is_confirmed, + recommended_minute, + recommended_second) VALUES (1, '24052001001', 1, 1, 'GICHUL_PROBLEM', '제목1', '1', 5, '기존 문제 설명', 'mainProblem.png', 'mainAnalysis.png', 'mainHandwriting1.png', 'readingTip.png', 'seniorTip.png', - 'prescription1.png, prescription2.png', 'MULTIPLE_CHOICE', false), + 'prescription1.png, prescription2.png', 'MULTIPLE_CHOICE', false, + 30, 0), (2, '24052001002', 1, 2, 'GICHUL_PROBLEM', '제목2', '2', 4, '문제 2 설명', 'mainProblem2.png', 'mainAnalysis2.png', 'mainHandwriting2.png', 'readingTip2.png', 'seniorTip2.png', - 'prescription3.png, prescription4.png', 'MULTIPLE_CHOICE', false), + 'prescription3.png, prescription4.png', 'MULTIPLE_CHOICE', false, + 20, 30), (3, '24052001003', 1, 3, 'GICHUL_PROBLEM', '제목3', '3', 3, '문제 3 설명', 'mainProblem3.png', 'mainAnalysis3.png', 'mainHandwriting3.png', 'readingTip3.png', 'seniorTip3.png', - 'prescription5.png, prescription6.png', 'SHORT_STRING_ANSWER', true); + 'prescription5.png, prescription6.png', 'SHORT_STRING_ANSWER', true, + 15, 45); -- 자식 문제 데이터 삽입 INSERT INTO child_problem (child_problem_id, @@ -77,7 +87,10 @@ INSERT INTO problem (problem_id, senior_tip_image_url, prescription_image_urls, answer_type, - is_confirmed) + is_confirmed, + recommended_minute, + recommended_second) VALUES (4, '24052001004', 1, 4, 'GICHUL_PROBLEM', '', '', 1, '유효하지 않은 문제 설명', '', 'mainAnalysis4.png', '', 'readingTip4.png', 'seniorTip4.png', - '', 'MULTIPLE_CHOICE', false); \ No newline at end of file + '', 'MULTIPLE_CHOICE', false, + null, null); \ No newline at end of file diff --git a/src/test/resources/practice-test-tag.sql b/src/test/resources/practice-test-tag.sql index b1f5b17..60c45c7 100644 --- a/src/test/resources/practice-test-tag.sql +++ b/src/test/resources/practice-test-tag.sql @@ -1,3 +1,5 @@ +DELETE FROM practice_test_tag; + INSERT INTO practice_test_tag (practice_test_tag_id, name, test_year, test_month, subject, area) VALUES (1, '2025년 5월 고2 모의고사', 2024, 5, '고2', '수학'), (2, '2023년 3월 고2 모의고사', 2023, 3, '고2', '수학'); \ No newline at end of file