From d1d3446b2bf50309612b3f79a8694449b6e32c8e Mon Sep 17 00:00:00 2001 From: wooh Date: Thu, 30 Jul 2026 21:48:27 +0900 Subject: [PATCH 1/2] =?UTF-8?q?[Fix]=20=EB=AA=A8=EC=9D=98=EC=A7=80?= =?UTF-8?q?=EC=9B=90=20=ED=99=88=20=EC=9D=91=EB=8B=B5=EC=97=90=20=EC=9E=90?= =?UTF-8?q?=EC=86=8C=EC=84=9C=20=EC=B1=84=EC=A0=90=EC=A4=91=20=EC=97=AC?= =?UTF-8?q?=EB=B6=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MockApply 홈 카드 응답에 analysisInProgress 필드 추가 - PENDING/RUNNING 분석 비동기 작업 기준으로 채점중 여부 계산 - 홈/최근/검색 목록에서 동일한 채점중 플래그를 내려주도록 수정 - 홈 목록 응답의 채점중 상태 검증 테스트 추가 --- .../AnalysisAsyncTaskRepository.java | 6 ++ .../response/MockApplyHomeItemResponse.java | 8 ++- .../mockapply/service/MockApplyService.java | 59 +++++++++++++++---- .../service/MockApplyServiceTest.java | 8 +++ 4 files changed, 66 insertions(+), 15 deletions(-) diff --git a/src/main/java/com/jobdri/jobdri_api/domain/analysis/repository/AnalysisAsyncTaskRepository.java b/src/main/java/com/jobdri/jobdri_api/domain/analysis/repository/AnalysisAsyncTaskRepository.java index 44266197..4a849e68 100644 --- a/src/main/java/com/jobdri/jobdri_api/domain/analysis/repository/AnalysisAsyncTaskRepository.java +++ b/src/main/java/com/jobdri/jobdri_api/domain/analysis/repository/AnalysisAsyncTaskRepository.java @@ -17,5 +17,11 @@ Optional findFirstByUserIdAndMockApplyIdAndStatusInOrderByCre Collection statuses ); + List findByUserIdAndMockApplyIdInAndStatusIn( + Long userId, + Collection mockApplyIds, + Collection statuses + ); + List findByStatusIn(Collection statuses); } diff --git a/src/main/java/com/jobdri/jobdri_api/domain/mockapply/dto/response/MockApplyHomeItemResponse.java b/src/main/java/com/jobdri/jobdri_api/domain/mockapply/dto/response/MockApplyHomeItemResponse.java index bf46381d..23a9c236 100644 --- a/src/main/java/com/jobdri/jobdri_api/domain/mockapply/dto/response/MockApplyHomeItemResponse.java +++ b/src/main/java/com/jobdri/jobdri_api/domain/mockapply/dto/response/MockApplyHomeItemResponse.java @@ -22,9 +22,10 @@ public record MockApplyHomeItemResponse( JobPostingProfileColor profileColor, LocalDateTime createdAt, ApplyType applyType, - Integer score + Integer score, + boolean analysisInProgress ) { - public static MockApplyHomeItemResponse from(MockApply mockApply) { + public static MockApplyHomeItemResponse from(MockApply mockApply, boolean analysisInProgress) { JobPosting jobPosting = mockApply.getJobPosting(); String detailClassificationName = jobPosting.getDetailClassification().getDetailName(); Analysis analysis = mockApply.getAnalysis(); @@ -42,7 +43,8 @@ public static MockApplyHomeItemResponse from(MockApply mockApply) { jobPosting.getProfileColor(), mockApply.getCreatedAt(), mockApply.getApplyType(), - analysis == null ? null : analysis.getScore() + analysis == null ? null : analysis.getScore(), + analysisInProgress ); } 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 18f0ac0b..d2251b5d 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 @@ -1,6 +1,8 @@ package com.jobdri.jobdri_api.domain.mockapply.service; import com.jobdri.jobdri_api.domain.analysis.entity.Question; +import com.jobdri.jobdri_api.domain.analysis.entity.AnalysisAsyncTask.TaskStatus; +import com.jobdri.jobdri_api.domain.analysis.repository.AnalysisAsyncTaskRepository; import com.jobdri.jobdri_api.domain.analysis.repository.AnalysisRepository; import com.jobdri.jobdri_api.domain.analysis.repository.QuestionAnalysisRepository; import com.jobdri.jobdri_api.domain.analysis.repository.QuestionRepository; @@ -36,6 +38,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; @@ -46,6 +49,8 @@ import java.sql.SQLException; import java.util.List; import java.util.Locale; +import java.util.Set; +import java.util.stream.Collectors; @Service @RequiredArgsConstructor @@ -72,6 +77,7 @@ public class MockApplyService { private final UserService userService; private final MockApplyPersistenceService mockApplyPersistenceService; private final MockApplySequenceService mockApplySequenceService; + private final AnalysisAsyncTaskRepository analysisAsyncTaskRepository; @Transactional(propagation = Propagation.NOT_SUPPORTED) @AuditLogEvent(action = "MOCK_APPLY_CREATE", targetType = "MOCK_APPLY", targetId = "#result.mockApplyId()") @@ -222,11 +228,9 @@ public MockApplySequenceResponse getMockApplySequence(User user, Long mockApplyI public MockApplyHomeResponse getMyMockApplies(User user, int page, int size) { User validatedUser = userService.validateUser(user); - List inProgressItems = mockApplyRepository - .findAllByUserIdAndStatusNotOrderByCreatedAtDescIdDesc(validatedUser.getId(), MockApplyStatus.COMPLETED) - .stream() - .map(MockApplyHomeItemResponse::from) - .toList(); + List inProgressMockApplies = mockApplyRepository + .findAllByUserIdAndStatusNotOrderByCreatedAtDescIdDesc(validatedUser.getId(), MockApplyStatus.COMPLETED); + List inProgressItems = toHomeItems(validatedUser.getId(), inProgressMockApplies); Pageable pageable = PageRequest.of( Math.max(page, 0), Math.min(Math.max(size, 1), MAX_PAGE_SIZE), @@ -237,7 +241,7 @@ public MockApplyHomeResponse getMyMockApplies(User user, int page, int size) { ); Page completedItems = mockApplyRepository .findAllByUserIdAndStatus(validatedUser.getId(), MockApplyStatus.COMPLETED, pageable) - .map(MockApplyHomeItemResponse::from); + .map(mockApply -> MockApplyHomeItemResponse.from(mockApply, false)); return new MockApplyHomeResponse( inProgressItems, @@ -256,9 +260,8 @@ public List getRecentMockApplies(User user, int limit ) ); - return mockApplyRepository.findAllByUserId(validatedUser.getId(), pageable) - .map(MockApplyHomeItemResponse::from) - .getContent(); + List mockApplies = mockApplyRepository.findAllByUserId(validatedUser.getId(), pageable).getContent(); + return toHomeItems(validatedUser.getId(), mockApplies); } public Page getCompletedMockApplies( @@ -285,7 +288,7 @@ public Page getCompletedMockApplies( maxScoreExclusive(resolvedFilter), pageable ) - .map(MockApplyHomeItemResponse::from); + .map(mockApply -> MockApplyHomeItemResponse.from(mockApply, false)); } public Page searchMyMockApplies(User user, String query, int page, int size) { @@ -300,8 +303,40 @@ public Page searchMyMockApplies(User user, String que ) ); - return mockApplyRepository.searchByUserId(validatedUser.getId(), normalizedQuery, pageable) - .map(MockApplyHomeItemResponse::from); + Page mockApplyPage = mockApplyRepository.searchByUserId(validatedUser.getId(), normalizedQuery, pageable); + return new PageImpl<>( + toHomeItems(validatedUser.getId(), mockApplyPage.getContent()), + pageable, + mockApplyPage.getTotalElements() + ); + } + + private List toHomeItems(Long userId, List mockApplies) { + Set activeAnalysisMockApplyIds = findActiveAnalysisMockApplyIds(userId, mockApplies); + return mockApplies.stream() + .map(mockApply -> MockApplyHomeItemResponse.from( + mockApply, + activeAnalysisMockApplyIds.contains(mockApply.getId()) + )) + .toList(); + } + + private Set findActiveAnalysisMockApplyIds(Long userId, List mockApplies) { + List mockApplyIds = mockApplies.stream() + .filter(mockApply -> mockApply.getStatus() != MockApplyStatus.COMPLETED) + .map(MockApply::getId) + .toList(); + if (mockApplyIds.isEmpty()) { + return Set.of(); + } + return analysisAsyncTaskRepository.findByUserIdAndMockApplyIdInAndStatusIn( + userId, + mockApplyIds, + List.of(TaskStatus.PENDING, TaskStatus.RUNNING) + ) + .stream() + .map(task -> task.getMockApplyId()) + .collect(Collectors.toSet()); } @Transactional diff --git a/src/test/java/com/jobdri/jobdri_api/domain/mockapply/service/MockApplyServiceTest.java b/src/test/java/com/jobdri/jobdri_api/domain/mockapply/service/MockApplyServiceTest.java index 4419422e..6124cf37 100644 --- a/src/test/java/com/jobdri/jobdri_api/domain/mockapply/service/MockApplyServiceTest.java +++ b/src/test/java/com/jobdri/jobdri_api/domain/mockapply/service/MockApplyServiceTest.java @@ -1,9 +1,11 @@ package com.jobdri.jobdri_api.domain.mockapply.service; import com.jobdri.jobdri_api.domain.analysis.entity.Analysis; +import com.jobdri.jobdri_api.domain.analysis.entity.AnalysisAsyncTask; import com.jobdri.jobdri_api.domain.analysis.entity.Question; import com.jobdri.jobdri_api.domain.analysis.entity.QuestionAnalysis; import com.jobdri.jobdri_api.domain.analysis.entity.QuestionAnalysisStatus; +import com.jobdri.jobdri_api.domain.analysis.repository.AnalysisAsyncTaskRepository; import com.jobdri.jobdri_api.domain.analysis.repository.AnalysisRepository; import com.jobdri.jobdri_api.domain.analysis.repository.QuestionAnalysisRepository; import com.jobdri.jobdri_api.domain.analysis.repository.QuestionRepository; @@ -73,6 +75,9 @@ class MockApplyServiceTest { @Autowired private AnalysisRepository analysisRepository; + @Autowired + private AnalysisAsyncTaskRepository analysisAsyncTaskRepository; + @Autowired private QuestionAnalysisRepository questionAnalysisRepository; @@ -338,6 +343,7 @@ void getMyMockApplies() { MockApply inProgress = mockApplyRepository.save(MockApply.create(user, backendPosting, ApplyType.ACTUAL)); inProgress.updateStatus(MockApplyStatus.ANSWER_WRITE); inProgress.updateDisplayName("카카오 백엔드 지원 연습"); + analysisAsyncTaskRepository.save(AnalysisAsyncTask.pending(user.getId(), inProgress.getId(), 3)); MockApply completedFirst = mockApplyRepository.save(MockApply.create(user, dataPosting, ApplyType.MOCK)); completedFirst.updateStatus(MockApplyStatus.COMPLETED); MockApply completedSecond = mockApplyRepository.save(MockApply.create(user, dataPosting, ApplyType.ACTUAL)); @@ -372,6 +378,7 @@ void getMyMockApplies() { assertThat(response.inProgress().get(0).createdAt()).isEqualTo(baseTime); assertThat(response.inProgress().get(0).applyType()).isEqualTo(ApplyType.ACTUAL); assertThat(response.inProgress().get(0).score()).isNull(); + assertThat(response.inProgress().get(0).analysisInProgress()).isTrue(); assertThat(response.inProgress().get(0).resumePath()).isEqualTo("/mock-applies/" + inProgress.getId() + "/answers"); assertThat(response.completed().getContent()).extracting(MockApplyHomeItemResponse::mockApplyId) .containsExactly(completedSecond.getId(), completedFirst.getId()); @@ -383,6 +390,7 @@ void getMyMockApplies() { assertThat(response.completed().getContent().get(0).profileColor()).isEqualTo(JobPostingProfileColor.GREEN); assertThat(response.completed().getContent().get(0).displayName()).isNull(); assertThat(response.completed().getContent().get(0).score()).isEqualTo(81); + assertThat(response.completed().getContent().get(0).analysisInProgress()).isFalse(); assertThat(response.completed().getContent().get(0).applyType()).isEqualTo(ApplyType.ACTUAL); assertThat(response.completed().getContent().get(0).resumePath()).isEqualTo("/mock-applies/" + completedSecond.getId() + "/analysis"); } From 7a09c6f53bafd32c4ce24d35813c04ade1e9f582 Mon Sep 17 00:00:00 2001 From: wooh Date: Thu, 30 Jul 2026 21:57:35 +0900 Subject: [PATCH 2/2] =?UTF-8?q?[Fix]=20=EB=AA=A8=EC=9D=98=EC=A7=80?= =?UTF-8?q?=EC=9B=90=20=ED=99=88=20=EC=9D=91=EB=8B=B5=20=EC=B1=84=EC=A0=90?= =?UTF-8?q?=EC=A4=91=20=ED=94=8C=EB=9E=98=EA=B7=B8=20=EB=B3=B4=EA=B0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - analysisInProgress 조회 패턴을 위한 analysis_async_tasks 복합 인덱스 추가 - 운영 수동 migration과 schema.sql에 동일 인덱스 반영 - PENDING/RUNNING 분석 작업 모두 홈 응답에서 채점중으로 내려가도록 테스트 보강 --- ...analysis_async_tasks_home_lookup_index.sql | 2 + src/main/resources/schema.sql | 3 ++ .../service/MockApplyServiceTest.java | 42 ++++++++++++------- 3 files changed, 32 insertions(+), 15 deletions(-) create mode 100644 ops/db/migrations/20260730_analysis_async_tasks_home_lookup_index.sql diff --git a/ops/db/migrations/20260730_analysis_async_tasks_home_lookup_index.sql b/ops/db/migrations/20260730_analysis_async_tasks_home_lookup_index.sql new file mode 100644 index 00000000..c13621fc --- /dev/null +++ b/ops/db/migrations/20260730_analysis_async_tasks_home_lookup_index.sql @@ -0,0 +1,2 @@ +CREATE INDEX IF NOT EXISTS idx_analysis_async_tasks_user_mock_apply_status + ON analysis_async_tasks (user_id, mock_apply_id, status); diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 61aef4bf..8dadec32 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -55,6 +55,9 @@ CREATE INDEX IF NOT EXISTS idx_mock_question_embeddings_hnsw CREATE INDEX IF NOT EXISTS idx_job_posting_embeddings_hnsw ON job_posting_embeddings USING hnsw (embedding vector_cosine_ops); +CREATE INDEX IF NOT EXISTS idx_analysis_async_tasks_user_mock_apply_status + ON analysis_async_tasks (user_id, mock_apply_id, status); + ALTER TABLE IF EXISTS analyses ADD COLUMN IF NOT EXISTS missing_keywords TEXT NOT NULL DEFAULT '[]'; diff --git a/src/test/java/com/jobdri/jobdri_api/domain/mockapply/service/MockApplyServiceTest.java b/src/test/java/com/jobdri/jobdri_api/domain/mockapply/service/MockApplyServiceTest.java index 6124cf37..1f184bdd 100644 --- a/src/test/java/com/jobdri/jobdri_api/domain/mockapply/service/MockApplyServiceTest.java +++ b/src/test/java/com/jobdri/jobdri_api/domain/mockapply/service/MockApplyServiceTest.java @@ -51,6 +51,7 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.support.TransactionTemplate; +import java.time.Instant; import java.time.LocalDateTime; import java.util.List; import java.util.UUID; @@ -344,6 +345,11 @@ void getMyMockApplies() { inProgress.updateStatus(MockApplyStatus.ANSWER_WRITE); inProgress.updateDisplayName("카카오 백엔드 지원 연습"); analysisAsyncTaskRepository.save(AnalysisAsyncTask.pending(user.getId(), inProgress.getId(), 3)); + MockApply runningInProgress = mockApplyRepository.save(MockApply.create(user, backendPosting, ApplyType.MOCK)); + runningInProgress.updateStatus(MockApplyStatus.ANSWER_WRITE); + AnalysisAsyncTask runningTask = AnalysisAsyncTask.pending(user.getId(), runningInProgress.getId(), 3); + runningTask.markRunning("worker-1", 0, Instant.now()); + analysisAsyncTaskRepository.save(runningTask); MockApply completedFirst = mockApplyRepository.save(MockApply.create(user, dataPosting, ApplyType.MOCK)); completedFirst.updateStatus(MockApplyStatus.COMPLETED); MockApply completedSecond = mockApplyRepository.save(MockApply.create(user, dataPosting, ApplyType.ACTUAL)); @@ -356,30 +362,36 @@ void getMyMockApplies() { LocalDateTime baseTime = LocalDateTime.of(2026, 1, 1, 12, 0); ReflectionTestUtils.setField(inProgress, "createdAt", baseTime); + ReflectionTestUtils.setField(runningInProgress, "createdAt", baseTime.plusSeconds(30)); ReflectionTestUtils.setField(completedFirst, "createdAt", baseTime.plusMinutes(1)); ReflectionTestUtils.setField(completedSecond, "createdAt", baseTime.plusMinutes(2)); mockApplyRepository.saveAndFlush(inProgress); + mockApplyRepository.saveAndFlush(runningInProgress); mockApplyRepository.saveAndFlush(completedFirst); mockApplyRepository.saveAndFlush(completedSecond); MockApplyHomeResponse response = mockApplyService.getMyMockApplies(user, 0, 9); - assertThat(response.inProgress()).hasSize(1); + assertThat(response.inProgress()).hasSize(2); assertThat(response.completed().getContent()).hasSize(2); - assertThat(response.inProgress().get(0).mockApplyId()).isEqualTo(inProgress.getId()); - assertThat(response.inProgress().get(0).jobPostingId()).isEqualTo(backendPosting.getId()); - assertThat(response.inProgress().get(0).displayName()).isEqualTo("카카오 백엔드 지원 연습"); - assertThat(response.inProgress().get(0).sequence()).isEqualTo(1); - assertThat(response.inProgress().get(0).status()).isEqualTo(MockApplyStatus.ANSWER_WRITE); - assertThat(response.inProgress().get(0).companyName()).isEqualTo("테스트 기업"); - assertThat(response.inProgress().get(0).detailClassificationName()).isEqualTo("백엔드 개발"); - assertThat(response.inProgress().get(0).jobTitle()).isEqualTo("백엔드 개발"); - assertThat(response.inProgress().get(0).profileColor()).isEqualTo(JobPostingProfileColor.BLUE); - assertThat(response.inProgress().get(0).createdAt()).isEqualTo(baseTime); - assertThat(response.inProgress().get(0).applyType()).isEqualTo(ApplyType.ACTUAL); - assertThat(response.inProgress().get(0).score()).isNull(); - assertThat(response.inProgress().get(0).analysisInProgress()).isTrue(); - assertThat(response.inProgress().get(0).resumePath()).isEqualTo("/mock-applies/" + inProgress.getId() + "/answers"); + MockApplyHomeItemResponse runningItem = response.inProgress().get(0); + MockApplyHomeItemResponse pendingItem = response.inProgress().get(1); + assertThat(runningItem.mockApplyId()).isEqualTo(runningInProgress.getId()); + assertThat(runningItem.analysisInProgress()).isTrue(); + assertThat(pendingItem.mockApplyId()).isEqualTo(inProgress.getId()); + assertThat(pendingItem.jobPostingId()).isEqualTo(backendPosting.getId()); + assertThat(pendingItem.displayName()).isEqualTo("카카오 백엔드 지원 연습"); + assertThat(pendingItem.sequence()).isEqualTo(1); + assertThat(pendingItem.status()).isEqualTo(MockApplyStatus.ANSWER_WRITE); + assertThat(pendingItem.companyName()).isEqualTo("테스트 기업"); + assertThat(pendingItem.detailClassificationName()).isEqualTo("백엔드 개발"); + assertThat(pendingItem.jobTitle()).isEqualTo("백엔드 개발"); + assertThat(pendingItem.profileColor()).isEqualTo(JobPostingProfileColor.BLUE); + assertThat(pendingItem.createdAt()).isEqualTo(baseTime); + assertThat(pendingItem.applyType()).isEqualTo(ApplyType.ACTUAL); + assertThat(pendingItem.score()).isNull(); + assertThat(pendingItem.analysisInProgress()).isTrue(); + assertThat(pendingItem.resumePath()).isEqualTo("/mock-applies/" + inProgress.getId() + "/answers"); assertThat(response.completed().getContent()).extracting(MockApplyHomeItemResponse::mockApplyId) .containsExactly(completedSecond.getId(), completedFirst.getId()); assertThat(response.completed().getTotalElements()).isEqualTo(2);