From 4326ab8fe85e6013ce7cc6eb2c2343600dc64773 Mon Sep 17 00:00:00 2001 From: wooh Date: Tue, 26 May 2026 11:36:02 +0900 Subject: [PATCH 1/2] =?UTF-8?q?[Feat]=20=EC=9E=90=EC=86=8C=EC=84=9C=20?= =?UTF-8?q?=EB=8B=B5=EB=B3=80=20=EB=B0=8F=20=EB=B6=84=EC=84=9D=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=EC=97=90=20=EC=A7=80=EC=9B=90=20=EC=88=9C=EB=B2=88=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 자소서 답변 저장/수정 응답에 sequence 필드 추가 - 자소서 분석 실행/조회 응답에 sequence 필드 추가 - 같은 사용자와 같은 공고 기준으로 현재 지원 순번 계산 - 재지원 플로우에서 프론트가 몇 차 지원인지 구분할 수 있도록 응답 보강 - 관련 서비스 테스트 추가 --- .../dto/response/AnalysisResponse.java | 3 ++ .../dto/response/QuestionAnswerResponse.java | 1 + .../analysis/service/AnalysisService.java | 7 ++++- .../analysis/service/QuestionService.java | 1 + .../repository/MockApplyRepository.java | 9 ++++++ .../analysis/service/AnalysisServiceTest.java | 30 +++++++++++++++++++ .../analysis/service/QuestionServiceTest.java | 21 +++++++++++++ 7 files changed, 71 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/jobdri/jobdri_api/domain/analysis/dto/response/AnalysisResponse.java b/src/main/java/com/jobdri/jobdri_api/domain/analysis/dto/response/AnalysisResponse.java index 077fcdb..9d841fb 100644 --- a/src/main/java/com/jobdri/jobdri_api/domain/analysis/dto/response/AnalysisResponse.java +++ b/src/main/java/com/jobdri/jobdri_api/domain/analysis/dto/response/AnalysisResponse.java @@ -9,6 +9,7 @@ public record AnalysisResponse( Long mockApplyId, Long analysisId, MockApplyStatus status, + int sequence, int score, int jobFit, int impact, @@ -19,12 +20,14 @@ public record AnalysisResponse( public static AnalysisResponse of( Analysis analysis, MockApplyStatus status, + int sequence, List questions ) { return new AnalysisResponse( analysis.getMockApply().getId(), analysis.getId(), status, + sequence, analysis.getScore(), analysis.getJobFit(), analysis.getImpact(), diff --git a/src/main/java/com/jobdri/jobdri_api/domain/analysis/dto/response/QuestionAnswerResponse.java b/src/main/java/com/jobdri/jobdri_api/domain/analysis/dto/response/QuestionAnswerResponse.java index abf0ecf..111fef9 100644 --- a/src/main/java/com/jobdri/jobdri_api/domain/analysis/dto/response/QuestionAnswerResponse.java +++ b/src/main/java/com/jobdri/jobdri_api/domain/analysis/dto/response/QuestionAnswerResponse.java @@ -7,6 +7,7 @@ public record QuestionAnswerResponse( Long mockApplyId, MockApplyStatus status, + int sequence, List questions ) { } diff --git a/src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisService.java b/src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisService.java index 4b91102..8d66f33 100644 --- a/src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisService.java +++ b/src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisService.java @@ -179,7 +179,12 @@ private AnalysisResponse toResponse( )) .toList(); - return AnalysisResponse.of(analysis, mockApply.getStatus(), questionResponses); + return AnalysisResponse.of( + analysis, + mockApply.getStatus(), + mockApplyRepository.calculateSequence(mockApply), + questionResponses + ); } private MockApply getOwnedMockApply(User user, 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 1acaad1..0bf0cb2 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 @@ -199,6 +199,7 @@ public QuestionAnswerResponse saveAnswers( return new QuestionAnswerResponse( mockApply.getId(), mockApply.getStatus(), + mockApplyRepository.calculateSequence(mockApply), questions.stream().map(QuestionResponse::from).toList() ); } diff --git a/src/main/java/com/jobdri/jobdri_api/domain/mockapply/repository/MockApplyRepository.java b/src/main/java/com/jobdri/jobdri_api/domain/mockapply/repository/MockApplyRepository.java index 1a273a0..d302947 100644 --- a/src/main/java/com/jobdri/jobdri_api/domain/mockapply/repository/MockApplyRepository.java +++ b/src/main/java/com/jobdri/jobdri_api/domain/mockapply/repository/MockApplyRepository.java @@ -12,6 +12,15 @@ public interface MockApplyRepository extends JpaRepository { List findAllByJobPostingId(Long jobPostingId); long countByUserIdAndJobPostingId(Long userId, Long jobPostingId); + default int calculateSequence(MockApply mockApply) { + return Math.toIntExact(countSequenceByUserIdAndJobPostingId( + mockApply.getUser().getId(), + mockApply.getJobPosting().getId(), + mockApply.getCreatedAt(), + mockApply.getId() + )); + } + @Query(""" select ma from MockApply ma diff --git a/src/test/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisServiceTest.java b/src/test/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisServiceTest.java index e89137b..58c88be 100644 --- a/src/test/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisServiceTest.java +++ b/src/test/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisServiceTest.java @@ -111,6 +111,7 @@ void analyzeSavesAnalysis() { AnalysisResponse response = analysisService.analyze(user, mockApply.getId()); assertThat(response.status()).isEqualTo(MockApplyStatus.COMPLETED); + assertThat(response.sequence()).isEqualTo(1); assertThat(response.score()).isEqualTo(100); assertThat(response.jobFit()).isEqualTo(82); assertThat(response.impact()).isEqualTo(71); @@ -131,6 +132,35 @@ void analyzeSavesAnalysis() { )).hasSize(1); } + @Test + @DisplayName("분석 응답은 같은 공고 기준 현재 지원 순번을 반환한다") + void analyzeReturnsSequence() { + User user = saveUser("analysis-sequence@example.com"); + JobPosting jobPosting = saveJobPosting(user); + mockApplyRepository.save(MockApply.create(user, jobPosting, ApplyType.ACTUAL)); + MockApply secondMockApply = mockApplyRepository.save(MockApply.create(user, jobPosting, ApplyType.ACTUAL)); + Question question = saveQuestion(secondMockApply, "재지원 분석 문항입니다.", "Spring Boot API를 개발했습니다."); + when(analysisAiClient.analyze(any(), any())).thenReturn(new AnalysisLlmResponse( + 80, + 81, + 82, + 83, + "재지원 분석입니다.", + List.of(new AnalysisLlmResponse.QuestionAnalysisItem( + question.getId(), + "Spring Boot API를 개발했습니다.", + "mentioned", + "성과 지표가 부족합니다.", + "Spring Boot API를 개발해 응답 시간을 개선했습니다." + )) + )); + + AnalysisResponse response = analysisService.analyze(user, secondMockApply.getId()); + + assertThat(response.mockApplyId()).isEqualTo(secondMockApply.getId()); + assertThat(response.sequence()).isEqualTo(2); + } + @Test @DisplayName("LLM 분석 실패 시 크레딧 차감과 분석 저장을 롤백한다") void analyzeRollsBackCreditWhenLlmFails() { 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 16967dc..96ce1a3 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 @@ -299,12 +299,33 @@ void saveAnswers() { ))); assertThat(response.questions()).hasSize(1); + assertThat(response.sequence()).isEqualTo(1); assertThat(response.questions().get(0).content()).isEqualTo("지원 동기를 작성해주세요."); assertThat(response.questions().get(0).answer()).isEqualTo("저는 백엔드 개발 경험을 바탕으로 지원했습니다."); assertThat(questionRepository.findById(questionId).orElseThrow().getAnswer()) .isEqualTo("저는 백엔드 개발 경험을 바탕으로 지원했습니다."); } + @Test + @DisplayName("답변 저장 응답은 같은 공고 기준 현재 지원 순번을 반환한다") + void saveAnswersReturnsSequence() { + User user = saveUser("answer-sequence@example.com"); + JobPosting jobPosting = saveJobPosting(); + mockApplyRepository.save(MockApply.create(user, jobPosting, ApplyType.ACTUAL)); + MockApply secondMockApply = mockApplyRepository.save(MockApply.create(user, jobPosting, ApplyType.ACTUAL)); + QuestionSelectionResponse selected = questionService.saveSelectedQuestions(user, secondMockApply.getId(), new QuestionSelectionSaveRequest(List.of( + new QuestionSelectionSaveRequest.QuestionItem("재지원 답변 문항입니다.", 700, false) + ))); + Long questionId = selected.questions().get(0).questionId(); + + QuestionAnswerResponse response = questionService.saveAnswers(user, secondMockApply.getId(), new QuestionAnswerSaveRequest(List.of( + new QuestionAnswerSaveRequest.AnswerItem(questionId, "두 번째 지원 답변입니다.") + ))); + + assertThat(response.mockApplyId()).isEqualTo(secondMockApply.getId()); + assertThat(response.sequence()).isEqualTo(2); + } + @Test @DisplayName("해당 지원서에 속하지 않은 문항은 답변 저장에 사용할 수 없다") void saveAnswersThrowsWhenQuestionDoesNotBelongToMockApply() { From 6f044f21a3de47840bdb105843df78b404865e7f Mon Sep 17 00:00:00 2001 From: wooh Date: Tue, 26 May 2026 11:43:06 +0900 Subject: [PATCH 2/2] =?UTF-8?q?[Fix]=20=EB=AA=A8=EC=9D=98=20=EC=84=9C?= =?UTF-8?q?=EB=A5=98=20=EC=A7=80=EC=9B=90=20=EC=88=9C=EB=B2=88=20=EA=B3=84?= =?UTF-8?q?=EC=82=B0=20=EC=95=88=EC=A0=95=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - sequence 계산 시 createdAt 비교를 제거하고 id 기준으로 계산하도록 수정 - H2/CI 환경의 timestamp 정밀도 차이로 순번 테스트가 실패하던 문제 해결 - 모의 서류 지원 순번 조회 API도 공통 sequence 계산 로직을 사용하도록 정리 - 전체 테스트 통과 확인 --- .../domain/mockapply/repository/MockApplyRepository.java | 7 +------ .../domain/mockapply/service/MockApplyService.java | 9 +-------- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/jobdri/jobdri_api/domain/mockapply/repository/MockApplyRepository.java b/src/main/java/com/jobdri/jobdri_api/domain/mockapply/repository/MockApplyRepository.java index d302947..f1bd89c 100644 --- a/src/main/java/com/jobdri/jobdri_api/domain/mockapply/repository/MockApplyRepository.java +++ b/src/main/java/com/jobdri/jobdri_api/domain/mockapply/repository/MockApplyRepository.java @@ -16,7 +16,6 @@ default int calculateSequence(MockApply mockApply) { return Math.toIntExact(countSequenceByUserIdAndJobPostingId( mockApply.getUser().getId(), mockApply.getJobPosting().getId(), - mockApply.getCreatedAt(), mockApply.getId() )); } @@ -38,15 +37,11 @@ select count(ma) from MockApply ma where ma.user.id = :userId and ma.jobPosting.id = :jobPostingId - and ( - ma.createdAt < :createdAt - or (ma.createdAt = :createdAt and ma.id <= :mockApplyId) - ) + and ma.id <= :mockApplyId """) long countSequenceByUserIdAndJobPostingId( @Param("userId") Long userId, @Param("jobPostingId") Long jobPostingId, - @Param("createdAt") java.time.LocalDateTime createdAt, @Param("mockApplyId") Long mockApplyId ); } diff --git a/src/main/java/com/jobdri/jobdri_api/domain/mockapply/service/MockApplyService.java b/src/main/java/com/jobdri/jobdri_api/domain/mockapply/service/MockApplyService.java index ef58858..23c1206 100644 --- a/src/main/java/com/jobdri/jobdri_api/domain/mockapply/service/MockApplyService.java +++ b/src/main/java/com/jobdri/jobdri_api/domain/mockapply/service/MockApplyService.java @@ -108,14 +108,7 @@ public MockApplySequenceResponse getMockApplySequence(User user, Long mockApplyI int totalCount = Math.toIntExact( mockApplyRepository.countByUserIdAndJobPostingId(validatedUser.getId(), jobPostingId) ); - int sequence = Math.toIntExact( - mockApplyRepository.countSequenceByUserIdAndJobPostingId( - validatedUser.getId(), - jobPostingId, - mockApply.getCreatedAt(), - mockApply.getId() - ) - ); + int sequence = mockApplyRepository.calculateSequence(mockApply); if (sequence < 1 || sequence > totalCount) { throw new GeneralException(