diff --git a/.gitignore b/.gitignore index 227c6ad..7c43360 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ build/ !**/src/main/**/build/ !**/src/test/**/build/ .env +local.env ### STS ### .apt_generated 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 682a2f3..66f71a3 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 @@ -18,4 +18,13 @@ default void existsByIdElseThrow(Set ids) { throw new NotFoundException(ErrorCode.CONCEPT_TAG_NOT_FOUND_IN_LIST); } } + + + default List findAllByIdsElseThrow(Set ids) { + List conceptTags = findAllById(ids); + if (conceptTags.size() != ids.size()) { + throw new NotFoundException(ErrorCode.CONCEPT_TAG_NOT_FOUND_IN_LIST); + } + return conceptTags; + } } diff --git a/src/main/java/com/moplus/moplus_server/domain/problemset/controller/ProblemSetController.java b/src/main/java/com/moplus/moplus_server/domain/problemset/controller/ProblemSetController.java index d101b45..8b13e35 100644 --- a/src/main/java/com/moplus/moplus_server/domain/problemset/controller/ProblemSetController.java +++ b/src/main/java/com/moplus/moplus_server/domain/problemset/controller/ProblemSetController.java @@ -4,13 +4,17 @@ import com.moplus.moplus_server.domain.problemset.dto.request.ProblemReorderRequest; import com.moplus.moplus_server.domain.problemset.dto.request.ProblemSetPostRequest; import com.moplus.moplus_server.domain.problemset.dto.request.ProblemSetUpdateRequest; +import com.moplus.moplus_server.domain.problemset.dto.response.ProblemSetGetResponse; import com.moplus.moplus_server.domain.problemset.service.ProblemSetDeleteService; +import com.moplus.moplus_server.domain.problemset.service.ProblemSetGetService; import com.moplus.moplus_server.domain.problemset.service.ProblemSetSaveService; import com.moplus.moplus_server.domain.problemset.service.ProblemSetUpdateService; +import io.swagger.v3.oas.annotations.Hidden; 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.PutMapping; @@ -26,6 +30,7 @@ public class ProblemSetController { private final ProblemSetSaveService problemSetSaveService; private final ProblemSetUpdateService problemSetUpdateService; private final ProblemSetDeleteService problemSetDeleteService; + private final ProblemSetGetService problemSetGetService; @PostMapping("") @Operation(summary = "문항세트 생성", description = "문항세트를 생성합니다. 문항은 요청 순서대로 저장합니다.") @@ -35,6 +40,7 @@ public ResponseEntity createProblemSet( return ResponseEntity.ok(problemSetSaveService.createProblemSet(request)); } + @Hidden @PutMapping("/{problemSetId}/sequence") @Operation(summary = "세트 문항순서 변경", description = "문항세트 내의 문항 리스트의 순서를 변경합니다.") public ResponseEntity reorderProblems( @@ -65,7 +71,17 @@ public ResponseEntity deleteProblemSet( @PutMapping("/{problemSetId}/confirm") @Operation(summary = "문항세트 컨펌 토글", description = "문항세트의 컨펌 상태를 토글합니다.") - public ResponseEntity toggleConfirmProblemSet(@PathVariable Long problemSetId) { + public ResponseEntity toggleConfirmProblemSet( + @PathVariable Long problemSetId + ) { return ResponseEntity.ok(problemSetUpdateService.toggleConfirmProblemSet(problemSetId)); } -} + + @GetMapping("/{problemSetId}") + @Operation(summary = "문항세트 개별 조회", description = "문항세트를 조회합니다.") + public ResponseEntity getProblemSet( + @PathVariable Long problemSetId + ) { + return ResponseEntity.ok(problemSetGetService.getProblemSet(problemSetId)); + } +} \ No newline at end of file diff --git a/src/main/java/com/moplus/moplus_server/domain/problemset/controller/ProblemSetSearchController.java b/src/main/java/com/moplus/moplus_server/domain/problemset/controller/ProblemSetSearchController.java new file mode 100644 index 0000000..405a66f --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/domain/problemset/controller/ProblemSetSearchController.java @@ -0,0 +1,35 @@ +package com.moplus.moplus_server.domain.problemset.controller; + + +import com.moplus.moplus_server.domain.problemset.dto.response.ProblemSetSearchGetResponse; +import com.moplus.moplus_server.domain.problemset.repository.ProblemSetSearchRepositoryCustom; +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.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v1/problemSet") +@RequiredArgsConstructor +public class ProblemSetSearchController { + + private final ProblemSetSearchRepositoryCustom problemSetSearchRepository; + + @GetMapping("/search") + @Operation( + summary = "문항세트 검색", + description = "문항세트 타이틀, 문항세트 내 포함된 개념태그, 문항세트 내 포함된 문항 타이틀로 검색합니다." + ) + public ResponseEntity> search( + @RequestParam(value = "problemSetTitle", required = false) String problemSetTitle, + @RequestParam(value = "problemTitle", required = false) String problemTitle, + @RequestParam(value = "conceptTagNames", required = false) List conceptTagNames + ) { + List problemSets = problemSetSearchRepository.search(problemSetTitle, problemTitle, conceptTagNames); + return ResponseEntity.ok(problemSets); + } +} diff --git a/src/main/java/com/moplus/moplus_server/domain/problemset/domain/ProblemSet.java b/src/main/java/com/moplus/moplus_server/domain/problemset/domain/ProblemSet.java index 4b2e7d9..cfa041e 100644 --- a/src/main/java/com/moplus/moplus_server/domain/problemset/domain/ProblemSet.java +++ b/src/main/java/com/moplus/moplus_server/domain/problemset/domain/ProblemSet.java @@ -8,6 +8,7 @@ import jakarta.persistence.CollectionTable; import jakarta.persistence.Column; import jakarta.persistence.ElementCollection; +import jakarta.persistence.Embedded; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; @@ -33,7 +34,8 @@ public class ProblemSet extends BaseEntity { @Column(name = "problem_set_id") Long id; - private String name; + @Embedded + private Title title; private boolean isDeleted; @Enumerated(EnumType.STRING) @@ -46,8 +48,8 @@ public class ProblemSet extends BaseEntity { private List problemIds = new ArrayList<>(); @Builder - public ProblemSet(String name, List problemIds) { - this.name = name; + public ProblemSet(String title, List problemIds) { + this.title = new Title(title); this.isDeleted = false; this.confirmStatus = ProblemSetConfirmStatus.NOT_CONFIRMED; this.problemIds = problemIds; @@ -73,8 +75,8 @@ public void toggleConfirm(List problems) { this.confirmStatus = this.confirmStatus.toggle(); } - public void updateProblemSet(String name, List newProblems) { - this.name = name; + public void updateProblemSet(String title, List newProblems) { + this.title = new Title(title); this.problemIds = newProblems; } } diff --git a/src/main/java/com/moplus/moplus_server/domain/problemset/domain/Title.java b/src/main/java/com/moplus/moplus_server/domain/problemset/domain/Title.java new file mode 100644 index 0000000..aa91e1c --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/domain/problemset/domain/Title.java @@ -0,0 +1,25 @@ +package com.moplus.moplus_server.domain.problemset.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Embeddable +@NoArgsConstructor +public class Title { + + private static final String DEFAULT_TITLE = "제목 없음"; + + @Column(name = "title", nullable = false) + private String value; + + public Title(String title) { + this.value = verifyTitle(title); + } + + private static String verifyTitle(String title) { + return (title == null || title.trim().isEmpty()) ? DEFAULT_TITLE : title; + } +} diff --git a/src/main/java/com/moplus/moplus_server/domain/problemset/dto/request/ProblemSetPostRequest.java b/src/main/java/com/moplus/moplus_server/domain/problemset/dto/request/ProblemSetPostRequest.java index 0401915..eb50f4d 100644 --- a/src/main/java/com/moplus/moplus_server/domain/problemset/dto/request/ProblemSetPostRequest.java +++ b/src/main/java/com/moplus/moplus_server/domain/problemset/dto/request/ProblemSetPostRequest.java @@ -5,12 +5,12 @@ import java.util.List; public record ProblemSetPostRequest( - String problemSetName, + String problemSetTitle, List problems ) { public ProblemSet toEntity(List problemIdList) { return ProblemSet.builder() - .name(this.problemSetName) + .title(this.problemSetTitle) .problemIds(problemIdList) .build(); } diff --git a/src/main/java/com/moplus/moplus_server/domain/problemset/dto/request/ProblemSetUpdateRequest.java b/src/main/java/com/moplus/moplus_server/domain/problemset/dto/request/ProblemSetUpdateRequest.java index a0e4745..6752f0e 100644 --- a/src/main/java/com/moplus/moplus_server/domain/problemset/dto/request/ProblemSetUpdateRequest.java +++ b/src/main/java/com/moplus/moplus_server/domain/problemset/dto/request/ProblemSetUpdateRequest.java @@ -3,7 +3,7 @@ import java.util.List; public record ProblemSetUpdateRequest( - String problemSetName, + String problemSetTitle, List problems ) { } diff --git a/src/main/java/com/moplus/moplus_server/domain/problemset/dto/response/ProblemSetGetResponse.java b/src/main/java/com/moplus/moplus_server/domain/problemset/dto/response/ProblemSetGetResponse.java new file mode 100644 index 0000000..704d9af --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/domain/problemset/dto/response/ProblemSetGetResponse.java @@ -0,0 +1,24 @@ +package com.moplus.moplus_server.domain.problemset.dto.response; + +import com.moplus.moplus_server.domain.problemset.domain.ProblemSet; +import com.moplus.moplus_server.domain.problemset.domain.ProblemSetConfirmStatus; +import java.util.List; +import lombok.Builder; + +@Builder +public record ProblemSetGetResponse( + Long id, + String title, + ProblemSetConfirmStatus confirmStatus, + List problemSummaries +) { + public static ProblemSetGetResponse of(ProblemSet problemSet, List problemSummaries) { + + return ProblemSetGetResponse.builder() + .id(problemSet.getId()) + .title(problemSet.getTitle().getValue()) + .confirmStatus(problemSet.getConfirmStatus()) + .problemSummaries(problemSummaries) + .build(); + } +} diff --git a/src/main/java/com/moplus/moplus_server/domain/problemset/dto/response/ProblemSetSearchGetResponse.java b/src/main/java/com/moplus/moplus_server/domain/problemset/dto/response/ProblemSetSearchGetResponse.java new file mode 100644 index 0000000..140e1c3 --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/domain/problemset/dto/response/ProblemSetSearchGetResponse.java @@ -0,0 +1,19 @@ +package com.moplus.moplus_server.domain.problemset.dto.response; + +import java.util.List; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class ProblemSetSearchGetResponse { + private String problemSetTitle; + private List problemThumbnailResponses; + + public ProblemSetSearchGetResponse( + String problemSetTitle, List problemThumbnailResponses + ) { + this.problemSetTitle = problemSetTitle; + this.problemThumbnailResponses = problemThumbnailResponses; + } +} diff --git a/src/main/java/com/moplus/moplus_server/domain/problemset/dto/response/ProblemSummaryResponse.java b/src/main/java/com/moplus/moplus_server/domain/problemset/dto/response/ProblemSummaryResponse.java new file mode 100644 index 0000000..b563593 --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/domain/problemset/dto/response/ProblemSummaryResponse.java @@ -0,0 +1,27 @@ +package com.moplus.moplus_server.domain.problemset.dto.response; + +import com.moplus.moplus_server.domain.problem.domain.problem.Problem; +import java.util.List; +import lombok.Builder; + +@Builder +public record ProblemSummaryResponse( + String problemId, + int number, + String practiceTestName, + String comment, + String mainProblemImageUrl, + List tagNames +) { + public static ProblemSummaryResponse of(Problem problem, String practiceTestName, List tagNames) { + + return ProblemSummaryResponse.builder() + .problemId(problem.getId().toString()) + .number(problem.getNumber()) + .comment(problem.getComment()) + .mainProblemImageUrl(problem.getMainProblemImageUrl()) + .practiceTestName(practiceTestName) + .tagNames(tagNames) + .build(); + } +} diff --git a/src/main/java/com/moplus/moplus_server/domain/problemset/dto/response/ProblemThumbnailResponse.java b/src/main/java/com/moplus/moplus_server/domain/problemset/dto/response/ProblemThumbnailResponse.java new file mode 100644 index 0000000..0848eba --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/domain/problemset/dto/response/ProblemThumbnailResponse.java @@ -0,0 +1,14 @@ +package com.moplus.moplus_server.domain.problemset.dto.response; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class ProblemThumbnailResponse { + private String mainProblemImageUrl; + + public ProblemThumbnailResponse(String mainProblemImageUrl) { + this.mainProblemImageUrl = mainProblemImageUrl; + } +} diff --git a/src/main/java/com/moplus/moplus_server/domain/problemset/repository/ProblemSetSearchRepositoryCustom.java b/src/main/java/com/moplus/moplus_server/domain/problemset/repository/ProblemSetSearchRepositoryCustom.java new file mode 100644 index 0000000..52474a1 --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/domain/problemset/repository/ProblemSetSearchRepositoryCustom.java @@ -0,0 +1,57 @@ +package com.moplus.moplus_server.domain.problemset.repository; + +import static com.moplus.moplus_server.domain.concept.domain.QConceptTag.conceptTag; +import static com.moplus.moplus_server.domain.problem.domain.problem.QProblem.problem; +import static com.moplus.moplus_server.domain.problemset.domain.QProblemSet.problemSet; + +import com.moplus.moplus_server.domain.problemset.dto.response.ProblemSetSearchGetResponse; +import com.moplus.moplus_server.domain.problemset.dto.response.ProblemThumbnailResponse; +import com.querydsl.core.group.GroupBy; +import com.querydsl.core.types.Projections; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.jpa.impl.JPAQueryFactory; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class ProblemSetSearchRepositoryCustom { + + private final JPAQueryFactory queryFactory; + + public List search(String problemSetTitle, String problemTitle, List conceptTagNames) { + return queryFactory + .from(problemSet) + .leftJoin(problem).on(problem.id.in(problemSet.problemIds)) // 문제 세트 내 포함된 문항과 조인 + .leftJoin(conceptTag).on(conceptTag.id.in(problem.conceptTagIds)) // 문제의 개념 태그 조인 + .where( + containsProblemSetTitle(problemSetTitle), + containsProblemTitle(problemTitle), + containsConceptTagNames(conceptTagNames) + ) + .distinct() + .transform(GroupBy.groupBy(problemSet.id).list( + Projections.constructor(ProblemSetSearchGetResponse.class, + problemSet.title.value, + GroupBy.list( + Projections.constructor(ProblemThumbnailResponse.class, + problem.mainProblemImageUrl + ) + ) + ) + )); + } + + private BooleanExpression containsProblemSetTitle(String problemSetTitle) { + return (problemSetTitle == null || problemSetTitle.isEmpty()) ? null : problemSet.title.value.containsIgnoreCase(problemSetTitle); + } + + private BooleanExpression containsProblemTitle(String problemTitle) { + return (problemTitle == null || problemTitle.isEmpty()) ? null : problem.comment.containsIgnoreCase(problemTitle); + } + + private BooleanExpression containsConceptTagNames(List conceptTagNames) { + return (conceptTagNames == null || conceptTagNames.isEmpty()) ? null : conceptTag.name.in(conceptTagNames); + } +} \ No newline at end of file diff --git a/src/main/java/com/moplus/moplus_server/domain/problemset/service/ProblemSetGetService.java b/src/main/java/com/moplus/moplus_server/domain/problemset/service/ProblemSetGetService.java new file mode 100644 index 0000000..20aae25 --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/domain/problemset/service/ProblemSetGetService.java @@ -0,0 +1,46 @@ +package com.moplus.moplus_server.domain.problemset.service; + +import com.moplus.moplus_server.domain.concept.domain.ConceptTag; +import com.moplus.moplus_server.domain.concept.repository.ConceptTagRepository; +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.repository.PracticeTestTagRepository; +import com.moplus.moplus_server.domain.problem.repository.ProblemRepository; +import com.moplus.moplus_server.domain.problemset.domain.ProblemSet; +import com.moplus.moplus_server.domain.problemset.dto.response.ProblemSetGetResponse; +import com.moplus.moplus_server.domain.problemset.dto.response.ProblemSummaryResponse; +import com.moplus.moplus_server.domain.problemset.repository.ProblemSetRepository; +import java.util.ArrayList; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class ProblemSetGetService { + + private final ProblemSetRepository problemSetRepository; + private final ProblemRepository problemRepository; + private final PracticeTestTagRepository practiceTestTagRepository; + private final ConceptTagRepository conceptTagRepository; + + @Transactional(readOnly = true) + public ProblemSetGetResponse getProblemSet(Long problemSetId) { + + ProblemSet problemSet = problemSetRepository.findByIdElseThrow(problemSetId); + + List problemSummaries = new ArrayList<>(); + for (ProblemId problemId : problemSet.getProblemIds()) { + Problem problem = problemRepository.findByIdElseThrow(problemId); + PracticeTestTag practiceTestTag = practiceTestTagRepository.findByIdElseThrow(problem.getPracticeTestId()); + List tagNames = conceptTagRepository.findAllByIdsElseThrow(problem.getConceptTagIds()) + .stream() + .map(ConceptTag::getName) + .toList(); + problemSummaries.add(ProblemSummaryResponse.of(problem, practiceTestTag.getName(), tagNames)); + } + return ProblemSetGetResponse.of(problemSet, problemSummaries); + } +} diff --git a/src/main/java/com/moplus/moplus_server/domain/problemset/service/ProblemSetSaveService.java b/src/main/java/com/moplus/moplus_server/domain/problemset/service/ProblemSetSaveService.java index 3805f6e..c363815 100644 --- a/src/main/java/com/moplus/moplus_server/domain/problemset/service/ProblemSetSaveService.java +++ b/src/main/java/com/moplus/moplus_server/domain/problemset/service/ProblemSetSaveService.java @@ -3,12 +3,11 @@ import com.moplus.moplus_server.domain.problem.domain.problem.ProblemId; import com.moplus.moplus_server.domain.problem.repository.ProblemRepository; import com.moplus.moplus_server.domain.problemset.domain.ProblemSet; -import com.moplus.moplus_server.domain.problemset.dto.request.ProblemReorderRequest; import com.moplus.moplus_server.domain.problemset.dto.request.ProblemSetPostRequest; -import com.moplus.moplus_server.domain.problemset.dto.request.ProblemSetUpdateRequest; import com.moplus.moplus_server.domain.problemset.repository.ProblemSetRepository; +import com.moplus.moplus_server.global.error.exception.ErrorCode; +import com.moplus.moplus_server.global.error.exception.InvalidValueException; import java.util.List; -import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -22,6 +21,11 @@ public class ProblemSetSaveService { @Transactional public Long createProblemSet(ProblemSetPostRequest request) { + // 빈 문항 유효성 검증 + if (request.problems().isEmpty()) { + throw new InvalidValueException(ErrorCode.EMPTY_PROBLEMS_ERROR); + } + // 문제 ID 리스트를 ProblemId 객체로 변환 List problemIdList = request.problems().stream() .map(ProblemId::new) @@ -35,4 +39,5 @@ public Long createProblemSet(ProblemSetPostRequest request) { return problemSetRepository.save(problemSet).getId(); } + } diff --git a/src/main/java/com/moplus/moplus_server/domain/problemset/service/ProblemSetUpdateService.java b/src/main/java/com/moplus/moplus_server/domain/problemset/service/ProblemSetUpdateService.java index fd42a0f..92134ac 100644 --- a/src/main/java/com/moplus/moplus_server/domain/problemset/service/ProblemSetUpdateService.java +++ b/src/main/java/com/moplus/moplus_server/domain/problemset/service/ProblemSetUpdateService.java @@ -40,13 +40,18 @@ public void reorderProblems(Long problemSetId, ProblemReorderRequest request) { public void updateProblemSet(Long problemSetId, ProblemSetUpdateRequest request) { ProblemSet problemSet = problemSetRepository.findByIdElseThrow(problemSetId); + // 빈 문항 유효성 검증 + if (request.problems().isEmpty()) { + throw new InvalidValueException(ErrorCode.EMPTY_PROBLEMS_ERROR); + } + // 문항 리스트 검증 List problemIdList = request.problems().stream() .map(ProblemId::new) .collect(Collectors.toList()); problemIdList.forEach(problemRepository::findByIdElseThrow); - problemSet.updateProblemSet(request.problemSetName(), problemIdList); + problemSet.updateProblemSet(request.problemSetTitle(), problemIdList); } @Transactional 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 93516b5..747ef27 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 @@ -56,6 +56,7 @@ public enum ErrorCode { //문항세트 PROBLEM_SET_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 문항세트를 찾을 수 없습니다"), + EMPTY_PROBLEMS_ERROR(HttpStatus.BAD_REQUEST, "적어도 1개의 문항을 등록해주세요"), ; diff --git a/src/test/java/com/moplus/moplus_server/domain/problemset/ProblemSetServiceTest.java b/src/test/java/com/moplus/moplus_server/domain/problemset/ProblemSetServiceTest.java index e7c95ca..50c5d4e 100644 --- a/src/test/java/com/moplus/moplus_server/domain/problemset/ProblemSetServiceTest.java +++ b/src/test/java/com/moplus/moplus_server/domain/problemset/ProblemSetServiceTest.java @@ -18,14 +18,13 @@ 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({"/insert-problemset.sql"}) +@Sql({"/insert-problem2.sql"}) @SpringBootTest public class ProblemSetServiceTest { @@ -59,7 +58,7 @@ void setUp() { .orElseThrow(() -> new IllegalArgumentException("문항세트를 찾을 수 없습니다.")); assertThat(savedProblemSet).isNotNull(); - assertThat(savedProblemSet.getName()).isEqualTo("초기 문항세트"); + assertThat(savedProblemSet.getTitle().getValue()).isEqualTo("초기 문항세트"); assertThat(savedProblemSet.getProblemIds()).hasSize(3); assertThat(savedProblemSet.getProblemIds().get(0).getId()).isEqualTo("24052001001"); assertThat(savedProblemSet.getProblemIds().get(1).getId()).isEqualTo("24052001002"); @@ -99,10 +98,9 @@ void setUp() { problemSetUpdateService.updateProblemSet(problemSetId, updateRequest); // then - ProblemSet updatedProblemSet = problemSetRepository.findById(problemSetId) - .orElseThrow(() -> new IllegalArgumentException("문항세트를 찾을 수 없습니다.")); + ProblemSet updatedProblemSet = problemSetRepository.findByIdElseThrow(problemSetId); - assertThat(updatedProblemSet.getName()).isEqualTo("업데이트된 문항세트"); + assertThat(updatedProblemSet.getTitle().getValue()).isEqualTo("업데이트된 문항세트"); assertThat(updatedProblemSet.getProblemIds()).hasSize(2); assertThat(updatedProblemSet.getProblemIds().get(0).getId()).isEqualTo("24052001002"); assertThat(updatedProblemSet.getProblemIds().get(1).getId()).isEqualTo("24052001003"); @@ -139,4 +137,60 @@ void setUp() { .isInstanceOf(InvalidValueException.class) .hasMessageContaining(ErrorCode.INVALID_CONFIRM_PROBLEM.getMessage()); } + + @Test + void 빈_문항리스트_문항세트_생성_실패_테스트() { + // given + ProblemSetPostRequest emptyProblemSetRequest = new ProblemSetPostRequest( + "빈 문항세트", + List.of() // 빈 리스트 + ); + + // when & then + assertThatThrownBy(() -> problemSetSaveService.createProblemSet(emptyProblemSetRequest)) + .isInstanceOf(InvalidValueException.class) + .hasMessageContaining(ErrorCode.EMPTY_PROBLEMS_ERROR.getMessage()); + } + + @Test + void 빈_제목_문항세트_생성_테스트() { + // given + ProblemSetPostRequest emptyTitleRequest = new ProblemSetPostRequest( + "", // 빈 문자열 제목 + List.of("24052001001", "24052001002", "24052001003") + ); + + ProblemSetPostRequest nullTitleRequest = new ProblemSetPostRequest( + null, // null 제목 + List.of("24052001001", "24052001002", "24052001003") + ); + + // when + Long emptyTitleProblemSetId = problemSetSaveService.createProblemSet(emptyTitleRequest); + Long nullTitleProblemSetId = problemSetSaveService.createProblemSet(nullTitleRequest); + + // then + ProblemSet emptyTitleSavedProblemSet = problemSetRepository.findByIdElseThrow(emptyTitleProblemSetId); + + ProblemSet nullTitleSavedProblemSet = problemSetRepository.findByIdElseThrow(nullTitleProblemSetId); + + assertThat(emptyTitleSavedProblemSet.getTitle().getValue()).isEqualTo("제목 없음"); // 빈 문자열 제목 테스트 + assertThat(nullTitleSavedProblemSet.getTitle().getValue()).isEqualTo("제목 없음"); // null 제목 테스트 + } + + @Test + void 문항세트_빈_제목_업데이트_테스트() { + // given + Long problemSetId = problemSetSaveService.createProblemSet(problemSetPostRequest); + + ProblemSetUpdateRequest emptyUpdateRequest = new ProblemSetUpdateRequest( + "업데이트된 빈 문항세트", + List.of() + ); + + // when & then + assertThatThrownBy(() -> problemSetUpdateService.updateProblemSet(problemSetId, emptyUpdateRequest)) + .isInstanceOf(InvalidValueException.class) + .hasMessageContaining(ErrorCode.EMPTY_PROBLEMS_ERROR.getMessage()); + } } \ No newline at end of file diff --git a/src/test/java/com/moplus/moplus_server/domain/problemset/repository/ProblemSetSearchRepositoryCustomTest.java b/src/test/java/com/moplus/moplus_server/domain/problemset/repository/ProblemSetSearchRepositoryCustomTest.java new file mode 100644 index 0000000..9e05157 --- /dev/null +++ b/src/test/java/com/moplus/moplus_server/domain/problemset/repository/ProblemSetSearchRepositoryCustomTest.java @@ -0,0 +1,91 @@ +package com.moplus.moplus_server.domain.problemset.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.moplus.moplus_server.domain.problemset.dto.response.ProblemSetSearchGetResponse; +import com.moplus.moplus_server.domain.problemset.dto.response.ProblemThumbnailResponse; +import java.util.List; +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 +@SpringBootTest +@ActiveProfiles("h2test") +@Sql({"/practice-test-tag.sql", "/concept-tag.sql", "/insert-problem.sql", "/insert-problem-set.sql"}) +public class ProblemSetSearchRepositoryCustomTest { + + @Autowired + private ProblemSetSearchRepositoryCustom problemSetSearchRepository; + + @Test + void 문항세트_타이틀_일부_포함_검색() { + // when + List result = problemSetSearchRepository.search("고2 모의고사", null, null); + + // then + assertThat(result).hasSize(1); + assertThat(result.get(0).getProblemSetTitle()).isEqualTo("2025년 5월 고2 모의고사 문제 세트"); + } + + @Test + void 문항타이틀_포함_검색() { + // when + List result = problemSetSearchRepository.search(null, "설명 1", null); + + // then + assertThat(result).hasSize(1); + assertThat(result.get(0).getProblemSetTitle()).isEqualTo("2025년 5월 고2 모의고사 문제 세트"); + } + + @Test + void 개념태그_하나라도_포함되면_조회() { + // when + List result = problemSetSearchRepository.search(null, null, List.of("미분 개념")); + + // then + assertThat(result).hasSize(1); + assertThat(result.get(0).getProblemSetTitle()).isEqualTo("2025년 5월 고2 모의고사 문제 세트"); + } + + @Test + void 모두_적용된_검색() { + // when + List result = problemSetSearchRepository.search("고2", "설명 1", List.of("미분 개념")); + + // then + assertThat(result).hasSize(1); + assertThat(result.get(0).getProblemSetTitle()).isEqualTo("2025년 5월 고2 모의고사 문제 세트"); + } + + @Test + void 아무_조건도_없으면_모든_데이터_조회() { + // when + List result = problemSetSearchRepository.search(null, null, null); + + // then + assertThat(result).hasSize(1); + } + + @Test + void 문항_여러개_문항세트_검색_조회() { + // when + List result = problemSetSearchRepository.search("고2 모의고사", null, null); + + // then + assertThat(result).hasSize(1); + ProblemSetSearchGetResponse response = result.get(0); + assertThat(response.getProblemSetTitle()).isEqualTo("2025년 5월 고2 모의고사 문제 세트"); + + // ✅ 문항이 2개 존재하는지 확인 + List problems = response.getProblemThumbnailResponses(); + assertThat(problems).hasSize(2); + + // ✅ 문항의 이미지 URL이 올바르게 매핑되었는지 확인 + assertThat(problems.get(0).getMainProblemImageUrl()).isEqualTo("mainProblem.png1"); + assertThat(problems.get(1).getMainProblemImageUrl()).isEqualTo("mainProblem.png2"); + } +} \ No newline at end of file diff --git a/src/test/java/com/moplus/moplus_server/domain/problemset/service/ProblemSetGetServiceTest.java b/src/test/java/com/moplus/moplus_server/domain/problemset/service/ProblemSetGetServiceTest.java new file mode 100644 index 0000000..85e7e40 --- /dev/null +++ b/src/test/java/com/moplus/moplus_server/domain/problemset/service/ProblemSetGetServiceTest.java @@ -0,0 +1,151 @@ +package com.moplus.moplus_server.domain.problemset.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; + +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.repository.PracticeTestTagRepository; +import com.moplus.moplus_server.domain.problem.repository.ProblemRepository; +import com.moplus.moplus_server.domain.problemset.domain.ProblemSet; +import com.moplus.moplus_server.domain.problemset.dto.response.ProblemSetGetResponse; +import com.moplus.moplus_server.domain.problemset.repository.ProblemSetRepository; +import com.moplus.moplus_server.global.error.exception.NotFoundException; +import java.util.List; +import java.util.Set; +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"}) +@SpringBootTest +public class ProblemSetGetServiceTest { + + @Autowired + private ProblemSetGetService problemSetGetService; + + @Autowired + private ProblemSetRepository problemSetRepository; + + @Autowired + private ProblemRepository problemRepository; + + @Autowired + private PracticeTestTagRepository practiceTestTagRepository; + + private ProblemSet savedProblemSet; + private Problem savedProblem; + + @BeforeEach + void setUp() { + // 문제 생성 요청 데이터 준비 + ProblemPostRequest problemPostRequest = new ProblemPostRequest( + Set.of(1L, 2L), + 1L, + 1, + "1", + "문제 설명", + "mainProblem.png", + "mainAnalysis.png", + "readingTip.png", + "seniorTip.png", + "prescription.png", + List.of() + ); + + // 문제 저장 + ProblemId createdProblemId = problemRepository.save( + new Problem( + new ProblemId("24052001001"), + practiceTestTagRepository.findByIdElseThrow(1L), + 1, + "1", + "문제 설명", + "mainProblem.png", + "mainAnalysis.png", + "readingTip.png", + "seniorTip.png", + "prescription.png", + Set.of(1L, 2L) + ) + ).getId(); + + savedProblem = problemRepository.findByIdElseThrow(createdProblemId); + + // 문항세트 저장 + savedProblemSet = problemSetRepository.save( + new ProblemSet("테스트 문항세트", List.of(savedProblem.getId())) + ); + } + + @Test + void 문항세트_조회_성공_테스트() { + // when + ProblemSetGetResponse response = problemSetGetService.getProblemSet(savedProblemSet.getId()); + + // then + assertThat(response).isNotNull(); + assertThat(response.title()).isEqualTo("테스트 문항세트"); + assertThat(response.problemSummaries()).hasSize(1); + assertThat(response.problemSummaries().get(0).problemId()).isEqualTo(savedProblem.getId().toString()); + assertThat(response.problemSummaries().get(0).practiceTestName()).isEqualTo("2025년 5월 고2 모의고사"); + assertThat(response.problemSummaries().get(0).tagNames()).contains("미분 개념", "적분 개념"); + } + + @Test + void 존재하지_않는_문항세트_조회_실패_테스트() { + // when & then + assertThatThrownBy(() -> problemSetGetService.getProblemSet(999L)) + .isInstanceOf(NotFoundException.class) + .hasMessageContaining("해당 문항세트를 찾을 수 없습니다"); + } + + @Test + void 문항세트_조회_성공_테스트_여러개() { + // given: 여러 개의 문제를 저장 + Problem savedProblem2 = problemRepository.save( + new Problem( + new ProblemId("24052001002"), + practiceTestTagRepository.findByIdElseThrow(1L), + 2, + "2", + "문제 설명2", + "mainProblem2.png", + "mainAnalysis2.png", + "readingTip2.png", + "seniorTip2.png", + "prescription2.png", + Set.of(3L, 4L) + ) + ); + + ProblemSet multipleProblemSet = problemSetRepository.save( + new ProblemSet("여러 문항 테스트 문항세트", List.of(savedProblem.getId(), savedProblem2.getId())) + ); + + // when + ProblemSetGetResponse response = problemSetGetService.getProblemSet(multipleProblemSet.getId()); + + // then + assertThat(response).isNotNull(); + assertThat(response.title()).isEqualTo("여러 문항 테스트 문항세트"); + assertThat(response.problemSummaries()).hasSize(2); + + // 첫 번째 문제 검증 + assertThat(response.problemSummaries().get(0).problemId()).isEqualTo(savedProblem.getId().toString()); + assertThat(response.problemSummaries().get(0).practiceTestName()).isEqualTo("2025년 5월 고2 모의고사"); + assertThat(response.problemSummaries().get(0).tagNames()).contains("미분 개념", "적분 개념"); + + // 두 번째 문제 검증 + assertThat(response.problemSummaries().get(1).problemId()).isEqualTo(savedProblem2.getId().toString()); + assertThat(response.problemSummaries().get(1).practiceTestName()).isEqualTo("2025년 5월 고2 모의고사"); + assertThat(response.problemSummaries().get(1).tagNames()).contains("삼각함수 개념", "행렬 개념"); + } +} \ No newline at end of file diff --git a/src/test/resources/insert-problem-set.sql b/src/test/resources/insert-problem-set.sql new file mode 100644 index 0000000..e9c8af1 --- /dev/null +++ b/src/test/resources/insert-problem-set.sql @@ -0,0 +1,11 @@ +DELETE FROM problem_set_problems; +DELETE FROM problem_set; + +-- 문제 세트 추가 +INSERT INTO problem_set (problem_set_id, title, is_deleted, confirm_status) +VALUES (1, '2025년 5월 고2 모의고사 문제 세트', false, 'NOT_CONFIRMED'); + +-- 문제 세트에 포함된 문제 추가 +INSERT INTO problem_set_problems (problem_set_id, problem_id, sequence) +VALUES (1, '240520012001', 0), + (1, '240520012002', 1); \ No newline at end of file diff --git a/src/test/resources/insert-problem.sql b/src/test/resources/insert-problem.sql index 530736b..d9ecdfb 100644 --- a/src/test/resources/insert-problem.sql +++ b/src/test/resources/insert-problem.sql @@ -7,17 +7,17 @@ INSERT INTO problem (problem_id, practice_test_id, number, answer, comment, main main_analysis_image_url, reading_tip_image_url, senior_tip_image_url, prescription_image_url, is_published, is_variation) VALUES ('240520012001', 1, 1, '1', '기존 문제 설명 1', - 'mainProblem.png', 'mainAnalysis.png', 'readingTip.png', 'seniorTip.png', 'prescription.png', + 'mainProblem.png1', 'mainAnalysis.png1', 'readingTip.png1', 'seniorTip.png1', 'prescription.png1', false, false), ('240520012002', 1, 1, '1', '기존 문제 설명 2', - 'mainProblem.png', 'mainAnalysis.png', 'readingTip.png', 'seniorTip.png', 'prescription.png', + 'mainProblem.png2', 'mainAnalysis.png2', 'readingTip.png2', 'seniorTip.png2', 'prescription.png2', false, false); --- ✅ 기존 자식 문제(ChildProblem) 삽입 +-- 기존 자식 문제(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) +-- 문제-컨셉 태그 연결 (기존 문제의 ConceptTag) INSERT INTO problem_concept (problem_id, concept_tag_id) VALUES ('240520012001', 1), ('240520012001', 2), @@ -25,7 +25,7 @@ VALUES ('240520012001', 1), ('240520012002', 1), ('240520012002', 3); --- ✅ 자식 문제-컨셉 태그 연결 +-- 자식 문제-컨셉 태그 연결 INSERT INTO child_problem_concept (child_problem_id, concept_tag_id) VALUES (1, 3), (1, 4), diff --git a/src/test/resources/insert-problemset.sql b/src/test/resources/insert-problem2.sql similarity index 100% rename from src/test/resources/insert-problemset.sql rename to src/test/resources/insert-problem2.sql