From 798cbaea82678b7c1d08bb3d53921aa7583a956b Mon Sep 17 00:00:00 2001 From: wooh Date: Sat, 23 May 2026 04:53:38 +0900 Subject: [PATCH 1/3] =?UTF-8?q?[Feat]=20=EC=A7=81=EC=A0=91=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=AC=B8=ED=95=AD=20=ED=9B=84=EB=B3=B4=20API=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 직접 추가 문항 후보 생성 API 추가 - 문항 후보 응답에 custom 필드 추가 - 직접 추가 문항을 선택 확정 전까지 후보로만 관리 - 후보 목록 조회 시 기본 문항과 직접 추가 문항 함께 반환 - 직접 추가 문항 후보 저장 및 조회 테스트 추가 --- .../controller/QuestionController.java | 15 +++++ .../QuestionCandidateCreateRequest.java | 13 ++++ .../response/QuestionCandidateResponse.java | 3 +- .../entity/CustomQuestionCandidate.java | 50 +++++++++++++++ .../CustomQuestionCandidateRepository.java | 12 ++++ .../analysis/service/QuestionService.java | 62 +++++++++++++++++-- .../analysis/service/QuestionServiceTest.java | 29 +++++++++ 7 files changed, 179 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/jobdri/jobdri_api/domain/analysis/dto/request/QuestionCandidateCreateRequest.java create mode 100644 src/main/java/com/jobdri/jobdri_api/domain/analysis/entity/CustomQuestionCandidate.java create mode 100644 src/main/java/com/jobdri/jobdri_api/domain/analysis/repository/CustomQuestionCandidateRepository.java diff --git a/src/main/java/com/jobdri/jobdri_api/domain/analysis/controller/QuestionController.java b/src/main/java/com/jobdri/jobdri_api/domain/analysis/controller/QuestionController.java index 0308ae7..2dabba2 100644 --- a/src/main/java/com/jobdri/jobdri_api/domain/analysis/controller/QuestionController.java +++ b/src/main/java/com/jobdri/jobdri_api/domain/analysis/controller/QuestionController.java @@ -1,5 +1,6 @@ package com.jobdri.jobdri_api.domain.analysis.controller; +import com.jobdri.jobdri_api.domain.analysis.dto.request.QuestionCandidateCreateRequest; import com.jobdri.jobdri_api.domain.analysis.dto.request.QuestionAnswerSaveRequest; import com.jobdri.jobdri_api.domain.analysis.dto.request.QuestionSelectionSaveRequest; import com.jobdri.jobdri_api.domain.analysis.dto.response.QuestionAnswerResponse; @@ -16,6 +17,7 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -43,6 +45,19 @@ public ApiResponse> getQuestionCandidates( ); } + @Operation(summary = "직접 추가 문항 후보 생성", description = "직접 입력한 문항을 선택 후보 목록에 추가합니다. 선택 문항으로 확정 저장되지는 않습니다.") + @PostMapping("/candidates") + public ApiResponse addCustomQuestionCandidate( + @AuthenticationPrincipal UserDetailsImpl userDetails, + @PathVariable Long mockApplyId, + @Valid @RequestBody QuestionCandidateCreateRequest request + ) { + return ApiResponse.onSuccess( + "직접 추가 문항 후보가 생성되었습니다.", + questionService.addCustomQuestionCandidate(userDetails.getUser(), mockApplyId, request) + ); + } + @Operation(summary = "선택 문항 조회", description = "현재 모의 서류 지원에 저장된 선택 문항 목록을 조회합니다.") @GetMapping public ApiResponse getSelectedQuestions( diff --git a/src/main/java/com/jobdri/jobdri_api/domain/analysis/dto/request/QuestionCandidateCreateRequest.java b/src/main/java/com/jobdri/jobdri_api/domain/analysis/dto/request/QuestionCandidateCreateRequest.java new file mode 100644 index 0000000..9e04847 --- /dev/null +++ b/src/main/java/com/jobdri/jobdri_api/domain/analysis/dto/request/QuestionCandidateCreateRequest.java @@ -0,0 +1,13 @@ +package com.jobdri.jobdri_api.domain.analysis.dto.request; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Positive; + +public record QuestionCandidateCreateRequest( + @NotBlank(message = "문항 내용은 필수입니다.") + String content, + + @Positive(message = "글자수 제한은 1 이상이어야 합니다.") + Integer charLimit +) { +} diff --git a/src/main/java/com/jobdri/jobdri_api/domain/analysis/dto/response/QuestionCandidateResponse.java b/src/main/java/com/jobdri/jobdri_api/domain/analysis/dto/response/QuestionCandidateResponse.java index 3c024e2..b9265d6 100644 --- a/src/main/java/com/jobdri/jobdri_api/domain/analysis/dto/response/QuestionCandidateResponse.java +++ b/src/main/java/com/jobdri/jobdri_api/domain/analysis/dto/response/QuestionCandidateResponse.java @@ -4,6 +4,7 @@ public record QuestionCandidateResponse( Long questionId, String content, int charLimit, - boolean selected + boolean selected, + boolean custom ) { } diff --git a/src/main/java/com/jobdri/jobdri_api/domain/analysis/entity/CustomQuestionCandidate.java b/src/main/java/com/jobdri/jobdri_api/domain/analysis/entity/CustomQuestionCandidate.java new file mode 100644 index 0000000..2336009 --- /dev/null +++ b/src/main/java/com/jobdri/jobdri_api/domain/analysis/entity/CustomQuestionCandidate.java @@ -0,0 +1,50 @@ +package com.jobdri.jobdri_api.domain.analysis.entity; + +import com.jobdri.jobdri_api.domain.mockapply.entity.MockApply; +import jakarta.persistence.*; +import lombok.*; + +import java.time.LocalDateTime; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Builder(access = AccessLevel.PRIVATE) +@Table( + name = "custom_question_candidates", + uniqueConstraints = { + @UniqueConstraint( + name = "uk_custom_question_candidates_mock_apply_content", + columnNames = {"mock_apply_id", "content"} + ) + } +) +public class CustomQuestionCandidate { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "mock_apply_id", nullable = false) + private MockApply mockApply; + + @Column(nullable = false, columnDefinition = "TEXT") + private String content; + + @Column(name = "char_limit", nullable = false) + private int limit; + + @Column(nullable = false) + private LocalDateTime createdAt; + + public static CustomQuestionCandidate create(MockApply mockApply, String content, int limit) { + return CustomQuestionCandidate.builder() + .mockApply(mockApply) + .content(content) + .limit(limit) + .createdAt(LocalDateTime.now()) + .build(); + } +} diff --git a/src/main/java/com/jobdri/jobdri_api/domain/analysis/repository/CustomQuestionCandidateRepository.java b/src/main/java/com/jobdri/jobdri_api/domain/analysis/repository/CustomQuestionCandidateRepository.java new file mode 100644 index 0000000..3ec89ea --- /dev/null +++ b/src/main/java/com/jobdri/jobdri_api/domain/analysis/repository/CustomQuestionCandidateRepository.java @@ -0,0 +1,12 @@ +package com.jobdri.jobdri_api.domain.analysis.repository; + +import com.jobdri.jobdri_api.domain.analysis.entity.CustomQuestionCandidate; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.Optional; + +public interface CustomQuestionCandidateRepository extends JpaRepository { + List findAllByMockApplyIdOrderByIdAsc(Long mockApplyId); + Optional findByMockApplyIdAndContent(Long mockApplyId, String content); +} diff --git a/src/main/java/com/jobdri/jobdri_api/domain/analysis/service/QuestionService.java b/src/main/java/com/jobdri/jobdri_api/domain/analysis/service/QuestionService.java index e0ebba9..8671e7a 100644 --- a/src/main/java/com/jobdri/jobdri_api/domain/analysis/service/QuestionService.java +++ b/src/main/java/com/jobdri/jobdri_api/domain/analysis/service/QuestionService.java @@ -1,12 +1,15 @@ package com.jobdri.jobdri_api.domain.analysis.service; +import com.jobdri.jobdri_api.domain.analysis.dto.request.QuestionCandidateCreateRequest; import com.jobdri.jobdri_api.domain.analysis.dto.request.QuestionAnswerSaveRequest; import com.jobdri.jobdri_api.domain.analysis.dto.request.QuestionSelectionSaveRequest; import com.jobdri.jobdri_api.domain.analysis.dto.response.QuestionAnswerResponse; import com.jobdri.jobdri_api.domain.analysis.dto.response.QuestionCandidateResponse; import com.jobdri.jobdri_api.domain.analysis.dto.response.QuestionResponse; import com.jobdri.jobdri_api.domain.analysis.dto.response.QuestionSelectionResponse; +import com.jobdri.jobdri_api.domain.analysis.entity.CustomQuestionCandidate; import com.jobdri.jobdri_api.domain.analysis.entity.Question; +import com.jobdri.jobdri_api.domain.analysis.repository.CustomQuestionCandidateRepository; import com.jobdri.jobdri_api.domain.analysis.repository.QuestionRepository; import com.jobdri.jobdri_api.domain.mockapply.entity.MockApply; import com.jobdri.jobdri_api.domain.mockapply.entity.MockApplyStatus; @@ -19,6 +22,7 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Set; @@ -44,21 +48,63 @@ public class QuestionService { private final MockApplyRepository mockApplyRepository; private final QuestionRepository questionRepository; + private final CustomQuestionCandidateRepository customQuestionCandidateRepository; public List getQuestionCandidates(User user, Long mockApplyId) { MockApply mockApply = getOwnedMockApply(user, mockApplyId); - Set selectedContents = questionRepository.findAllByMockApplyId(mockApply.getId()).stream() + List selectedQuestions = questionRepository.findAllByMockApplyIdOrderByIdAsc(mockApply.getId()); + Set selectedContents = selectedQuestions.stream() .map(Question::getContent) .collect(Collectors.toSet()); - return DEFAULT_CANDIDATES.stream() + List candidates = new ArrayList<>(DEFAULT_CANDIDATES.stream() .map(candidate -> new QuestionCandidateResponse( candidate.id(), candidate.content(), candidate.charLimit(), - selectedContents.contains(candidate.content()) + selectedContents.contains(candidate.content()), + false )) - .toList(); + .toList()); + + customQuestionCandidateRepository.findAllByMockApplyIdOrderByIdAsc(mockApply.getId()).stream() + .map(candidate -> new QuestionCandidateResponse( + candidate.getId(), + candidate.getContent(), + candidate.getLimit(), + selectedContents.contains(candidate.getContent()), + true + )) + .forEach(candidates::add); + + return candidates; + } + + @Transactional + public QuestionCandidateResponse addCustomQuestionCandidate( + User user, + Long mockApplyId, + QuestionCandidateCreateRequest request + ) { + MockApply mockApply = getOwnedMockApply(user, mockApplyId); + String content = request.content().trim(); + validateCustomCandidate(content); + + CustomQuestionCandidate candidate = customQuestionCandidateRepository + .findByMockApplyIdAndContent(mockApply.getId(), content) + .orElseGet(() -> customQuestionCandidateRepository.save(CustomQuestionCandidate.create( + mockApply, + content, + resolveCharLimit(request.charLimit()) + ))); + + return new QuestionCandidateResponse( + candidate.getId(), + candidate.getContent(), + candidate.getLimit(), + false, + true + ); } public QuestionSelectionResponse getSelectedQuestions(User user, Long mockApplyId) { @@ -159,6 +205,14 @@ private int resolveCharLimit(Integer charLimit) { return charLimit; } + private void validateCustomCandidate(String content) { + boolean existsInDefault = DEFAULT_CANDIDATES.stream() + .anyMatch(candidate -> candidate.content().equals(content)); + if (existsInDefault) { + throw new GeneralException(GeneralErrorCode.INVALID_PARAMETER, "이미 기본 후보에 존재하는 문항입니다."); + } + } + private String normalizeAnswer(String answer) { if (StringUtils.hasText(answer)) { return answer; diff --git a/src/test/java/com/jobdri/jobdri_api/domain/analysis/service/QuestionServiceTest.java b/src/test/java/com/jobdri/jobdri_api/domain/analysis/service/QuestionServiceTest.java index 3275f83..86b7fef 100644 --- a/src/test/java/com/jobdri/jobdri_api/domain/analysis/service/QuestionServiceTest.java +++ b/src/test/java/com/jobdri/jobdri_api/domain/analysis/service/QuestionServiceTest.java @@ -1,6 +1,7 @@ package com.jobdri.jobdri_api.domain.analysis.service; import com.jobdri.jobdri_api.domain.analysis.dto.request.QuestionAnswerSaveRequest; +import com.jobdri.jobdri_api.domain.analysis.dto.request.QuestionCandidateCreateRequest; import com.jobdri.jobdri_api.domain.analysis.dto.request.QuestionSelectionSaveRequest; import com.jobdri.jobdri_api.domain.analysis.dto.response.QuestionAnswerResponse; import com.jobdri.jobdri_api.domain.analysis.dto.response.QuestionCandidateResponse; @@ -110,6 +111,31 @@ void saveSelectedQuestionsReplacesExistingQuestions() { .containsExactly("새 문항"); } + @Test + @DisplayName("직접 추가 문항은 선택 문항으로 저장하지 않고 후보 목록에 추가한다") + void addCustomQuestionCandidate() { + User user = saveUser("question-custom-candidate@example.com"); + MockApply mockApply = saveMockApply(user); + + QuestionCandidateResponse response = questionService.addCustomQuestionCandidate( + user, + mockApply.getId(), + new QuestionCandidateCreateRequest("직접 추가한 후보 문항입니다.", 500) + ); + List candidates = questionService.getQuestionCandidates(user, mockApply.getId()); + + assertThat(response.content()).isEqualTo("직접 추가한 후보 문항입니다."); + assertThat(response.charLimit()).isEqualTo(500); + assertThat(response.selected()).isFalse(); + assertThat(response.custom()).isTrue(); + assertThat(questionRepository.findAllByMockApplyId(mockApply.getId())).isEmpty(); + assertThat(candidates).hasSize(6); + assertThat(candidates.get(5).questionId()).isEqualTo(response.questionId()); + assertThat(candidates.get(5).content()).isEqualTo("직접 추가한 후보 문항입니다."); + assertThat(candidates.get(5).selected()).isFalse(); + assertThat(candidates.get(5).custom()).isTrue(); + } + @Test @DisplayName("문항 후보 목록은 이미 저장된 기본 문항을 선택 상태로 반환한다") void getQuestionCandidatesMarksSelectedQuestion() { @@ -125,6 +151,9 @@ void getQuestionCandidatesMarksSelectedQuestion() { assertThat(candidates) .extracting(QuestionCandidateResponse::questionId) .containsExactly(1L, 2L, 3L, 4L, 5L); + assertThat(candidates) + .extracting(QuestionCandidateResponse::custom) + .containsOnly(false); assertThat(candidates.get(0).selected()).isTrue(); assertThat(candidates.get(1).selected()).isFalse(); } From c4161dcb8cfbe10c0945e888f80b8b3ed158cc36 Mon Sep 17 00:00:00 2001 From: wooh Date: Sat, 23 May 2026 05:04:24 +0900 Subject: [PATCH 2/3] =?UTF-8?q?[Fix]=20=EC=A7=81=EC=A0=91=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=AC=B8=ED=95=AD=20=ED=9B=84=EB=B3=B4=20=EC=A4=91?= =?UTF-8?q?=EB=B3=B5=20=EC=83=9D=EC=84=B1=20=EC=B2=98=EB=A6=AC=20=EB=B3=B4?= =?UTF-8?q?=EC=99=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 직접 추가 문항 후보 저장 시 unique 충돌 재조회 처리 - 동시 중복 요청에서도 기존 후보를 반환하도록 보완 - 기본 문항과 동일한 직접 추가 후보 등록 차단 테스트 추가 - 중복 custom 문항 후보 요청 idempotency 테스트 추가 --- .../analysis/service/QuestionService.java | 37 +++++++++++--- .../analysis/service/QuestionServiceTest.java | 50 +++++++++++++++++++ 2 files changed, 80 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/jobdri/jobdri_api/domain/analysis/service/QuestionService.java b/src/main/java/com/jobdri/jobdri_api/domain/analysis/service/QuestionService.java index 8671e7a..40e77a0 100644 --- a/src/main/java/com/jobdri/jobdri_api/domain/analysis/service/QuestionService.java +++ b/src/main/java/com/jobdri/jobdri_api/domain/analysis/service/QuestionService.java @@ -18,6 +18,7 @@ import com.jobdri.jobdri_api.global.apiPayload.code.GeneralErrorCode; import com.jobdri.jobdri_api.global.apiPayload.exception.GeneralException; import lombok.RequiredArgsConstructor; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; @@ -90,13 +91,7 @@ public QuestionCandidateResponse addCustomQuestionCandidate( String content = request.content().trim(); validateCustomCandidate(content); - CustomQuestionCandidate candidate = customQuestionCandidateRepository - .findByMockApplyIdAndContent(mockApply.getId(), content) - .orElseGet(() -> customQuestionCandidateRepository.save(CustomQuestionCandidate.create( - mockApply, - content, - resolveCharLimit(request.charLimit()) - ))); + CustomQuestionCandidate candidate = findOrCreateCustomCandidate(mockApply, content, request.charLimit()); return new QuestionCandidateResponse( candidate.getId(), @@ -107,6 +102,34 @@ public QuestionCandidateResponse addCustomQuestionCandidate( ); } + private CustomQuestionCandidate findOrCreateCustomCandidate( + MockApply mockApply, + String content, + Integer charLimit + ) { + return customQuestionCandidateRepository + .findByMockApplyIdAndContent(mockApply.getId(), content) + .orElseGet(() -> saveCustomCandidate(mockApply, content, charLimit)); + } + + private CustomQuestionCandidate saveCustomCandidate( + MockApply mockApply, + String content, + Integer charLimit + ) { + try { + return customQuestionCandidateRepository.saveAndFlush(CustomQuestionCandidate.create( + mockApply, + content, + resolveCharLimit(charLimit) + )); + } catch (DataIntegrityViolationException e) { + return customQuestionCandidateRepository + .findByMockApplyIdAndContent(mockApply.getId(), content) + .orElseThrow(() -> e); + } + } + public QuestionSelectionResponse getSelectedQuestions(User user, Long mockApplyId) { MockApply mockApply = getOwnedMockApply(user, mockApplyId); List questions = questionRepository.findAllByMockApplyIdOrderByIdAsc(mockApply.getId()).stream() diff --git a/src/test/java/com/jobdri/jobdri_api/domain/analysis/service/QuestionServiceTest.java b/src/test/java/com/jobdri/jobdri_api/domain/analysis/service/QuestionServiceTest.java index 86b7fef..2303ff0 100644 --- a/src/test/java/com/jobdri/jobdri_api/domain/analysis/service/QuestionServiceTest.java +++ b/src/test/java/com/jobdri/jobdri_api/domain/analysis/service/QuestionServiceTest.java @@ -136,6 +136,56 @@ void addCustomQuestionCandidate() { assertThat(candidates.get(5).custom()).isTrue(); } + @Test + @DisplayName("기본 후보와 같은 내용은 직접 추가 문항 후보로 등록할 수 없다") + void addCustomQuestionCandidateThrowsWhenContentExistsInDefaultCandidate() { + User user = saveUser("question-custom-default-duplicate@example.com"); + MockApply mockApply = saveMockApply(user); + + assertThatThrownBy(() -> questionService.addCustomQuestionCandidate( + user, + mockApply.getId(), + new QuestionCandidateCreateRequest("지원 동기와 입사 후 목표를 작성해주세요.", 700) + )) + .isInstanceOf(GeneralException.class) + .extracting("code") + .isEqualTo(GeneralErrorCode.INVALID_PARAMETER); + assertThat(questionRepository.findAllByMockApplyId(mockApply.getId())).isEmpty(); + assertThat(questionService.getQuestionCandidates(user, mockApply.getId())).hasSize(5); + } + + @Test + @DisplayName("같은 직접 추가 문항 후보를 여러 번 요청하면 기존 후보를 반환한다") + void addCustomQuestionCandidateReturnsExistingCandidateWhenDuplicated() { + User user = saveUser("question-custom-duplicate@example.com"); + MockApply mockApply = saveMockApply(user); + + QuestionCandidateResponse first = questionService.addCustomQuestionCandidate( + user, + mockApply.getId(), + new QuestionCandidateCreateRequest("중복 직접 추가 문항입니다.", 500) + ); + QuestionCandidateResponse second = questionService.addCustomQuestionCandidate( + user, + mockApply.getId(), + new QuestionCandidateCreateRequest("중복 직접 추가 문항입니다.", 800) + ); + List candidates = questionService.getQuestionCandidates(user, mockApply.getId()); + + assertThat(second.questionId()).isEqualTo(first.questionId()); + assertThat(second.content()).isEqualTo(first.content()); + assertThat(second.charLimit()).isEqualTo(first.charLimit()); + assertThat(second.selected()).isFalse(); + assertThat(second.custom()).isTrue(); + assertThat(questionRepository.findAllByMockApplyId(mockApply.getId())).isEmpty(); + assertThat(candidates).hasSize(6); + assertThat(candidates.stream() + .filter(QuestionCandidateResponse::custom) + .map(QuestionCandidateResponse::content) + .toList()) + .containsExactly("중복 직접 추가 문항입니다."); + } + @Test @DisplayName("문항 후보 목록은 이미 저장된 기본 문항을 선택 상태로 반환한다") void getQuestionCandidatesMarksSelectedQuestion() { From 82e609bc75529f3cefbe392e4adcb0ae5d8860a2 Mon Sep 17 00:00:00 2001 From: wooh Date: Sat, 23 May 2026 05:13:29 +0900 Subject: [PATCH 3/3] =?UTF-8?q?[Fix]=20=EC=A7=81=EC=A0=91=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=AC=B8=ED=95=AD=20=ED=9B=84=EB=B3=B4=20=EC=84=A0?= =?UTF-8?q?=ED=83=9D=20=EC=83=81=ED=83=9C=20=EC=9D=91=EB=8B=B5=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/QuestionRepository.java | 1 + .../analysis/service/QuestionService.java | 3 ++- .../analysis/service/QuestionServiceTest.java | 27 +++++++++++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/jobdri/jobdri_api/domain/analysis/repository/QuestionRepository.java b/src/main/java/com/jobdri/jobdri_api/domain/analysis/repository/QuestionRepository.java index 8ada435..de95563 100644 --- a/src/main/java/com/jobdri/jobdri_api/domain/analysis/repository/QuestionRepository.java +++ b/src/main/java/com/jobdri/jobdri_api/domain/analysis/repository/QuestionRepository.java @@ -8,5 +8,6 @@ public interface QuestionRepository extends JpaRepository { List findAllByMockApplyId(Long mockApplyId); List findAllByMockApplyIdOrderByIdAsc(Long mockApplyId); + boolean existsByMockApplyIdAndContent(Long mockApplyId, String content); void deleteAllByMockApplyId(Long mockApplyId); } diff --git a/src/main/java/com/jobdri/jobdri_api/domain/analysis/service/QuestionService.java b/src/main/java/com/jobdri/jobdri_api/domain/analysis/service/QuestionService.java index 40e77a0..7ad7fb8 100644 --- a/src/main/java/com/jobdri/jobdri_api/domain/analysis/service/QuestionService.java +++ b/src/main/java/com/jobdri/jobdri_api/domain/analysis/service/QuestionService.java @@ -92,12 +92,13 @@ public QuestionCandidateResponse addCustomQuestionCandidate( validateCustomCandidate(content); CustomQuestionCandidate candidate = findOrCreateCustomCandidate(mockApply, content, request.charLimit()); + boolean selected = questionRepository.existsByMockApplyIdAndContent(mockApply.getId(), candidate.getContent()); return new QuestionCandidateResponse( candidate.getId(), candidate.getContent(), candidate.getLimit(), - false, + selected, true ); } diff --git a/src/test/java/com/jobdri/jobdri_api/domain/analysis/service/QuestionServiceTest.java b/src/test/java/com/jobdri/jobdri_api/domain/analysis/service/QuestionServiceTest.java index 2303ff0..16967dc 100644 --- a/src/test/java/com/jobdri/jobdri_api/domain/analysis/service/QuestionServiceTest.java +++ b/src/test/java/com/jobdri/jobdri_api/domain/analysis/service/QuestionServiceTest.java @@ -186,6 +186,33 @@ void addCustomQuestionCandidateReturnsExistingCandidateWhenDuplicated() { .containsExactly("중복 직접 추가 문항입니다."); } + @Test + @DisplayName("이미 선택된 직접 추가 후보를 다시 요청하면 선택 상태로 반환한다") + void addCustomQuestionCandidateReturnsSelectedStateWhenAlreadySelected() { + User user = saveUser("question-custom-selected@example.com"); + MockApply mockApply = saveMockApply(user); + QuestionCandidateResponse first = questionService.addCustomQuestionCandidate( + user, + mockApply.getId(), + new QuestionCandidateCreateRequest("이미 선택된 직접 추가 문항입니다.", 500) + ); + questionService.saveSelectedQuestions(user, mockApply.getId(), new QuestionSelectionSaveRequest(List.of( + new QuestionSelectionSaveRequest.QuestionItem("이미 선택된 직접 추가 문항입니다.", 500, true) + ))); + + QuestionCandidateResponse second = questionService.addCustomQuestionCandidate( + user, + mockApply.getId(), + new QuestionCandidateCreateRequest("이미 선택된 직접 추가 문항입니다.", 800) + ); + + assertThat(second.questionId()).isEqualTo(first.questionId()); + assertThat(second.content()).isEqualTo(first.content()); + assertThat(second.charLimit()).isEqualTo(first.charLimit()); + assertThat(second.selected()).isTrue(); + assertThat(second.custom()).isTrue(); + } + @Test @DisplayName("문항 후보 목록은 이미 저장된 기본 문항을 선택 상태로 반환한다") void getQuestionCandidatesMarksSelectedQuestion() {