diff --git a/src/main/java/com/jobdri/jobdri_api/domain/mockapply/controller/MockApplyController.java b/src/main/java/com/jobdri/jobdri_api/domain/mockapply/controller/MockApplyController.java index c1cb3fe..ef4e117 100644 --- a/src/main/java/com/jobdri/jobdri_api/domain/mockapply/controller/MockApplyController.java +++ b/src/main/java/com/jobdri/jobdri_api/domain/mockapply/controller/MockApplyController.java @@ -4,6 +4,7 @@ import com.jobdri.jobdri_api.domain.mockapply.dto.request.MockApplyCreateMockFromJobPostingRequest; import com.jobdri.jobdri_api.domain.mockapply.dto.request.MockApplyCreateMockRequest; import com.jobdri.jobdri_api.domain.mockapply.dto.response.MockApplyCreateResponse; +import com.jobdri.jobdri_api.domain.mockapply.dto.response.MockApplyHomeResponse; import com.jobdri.jobdri_api.domain.mockapply.dto.response.MockApplySequenceResponse; import com.jobdri.jobdri_api.domain.jobposting.dto.response.JobPostingResponse; import com.jobdri.jobdri_api.domain.mockapply.service.MockApplyService; @@ -33,6 +34,20 @@ public class MockApplyController { private final MockApplyService mockApplyService; + @Operation( + summary = "내 모의 서류 지원 홈 목록 조회", + description = "홈 화면에서 이어서 작성할 지원과 완료된 분석 결과 카드를 조회합니다." + ) + @GetMapping("/me") + public ApiResponse getMyMockApplies( + @AuthenticationPrincipal UserDetailsImpl userDetails + ) { + return ApiResponse.onSuccess( + "모의 서류 지원 목록 조회에 성공했습니다.", + mockApplyService.getMyMockApplies(userDetails.getUser()) + ); + } + @Operation( summary = "실제 공고 기반 모의 서류 지원 생성", description = "공고 텍스트/URL 추출, 공고 저장, 사용자 확인 및 수정이 선행된 뒤 저장된 채용 공고 ID를 기준으로 로그인 사용자의 ACTUAL 타입 모의 서류 지원을 생성합니다." 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 new file mode 100644 index 0000000..ec1dffc --- /dev/null +++ b/src/main/java/com/jobdri/jobdri_api/domain/mockapply/dto/response/MockApplyHomeItemResponse.java @@ -0,0 +1,50 @@ +package com.jobdri.jobdri_api.domain.mockapply.dto.response; + +import com.jobdri.jobdri_api.domain.analysis.entity.Analysis; +import com.jobdri.jobdri_api.domain.jobposting.entity.JobPosting; +import com.jobdri.jobdri_api.domain.mockapply.entity.ApplyType; +import com.jobdri.jobdri_api.domain.mockapply.entity.MockApply; +import com.jobdri.jobdri_api.domain.mockapply.entity.MockApplyStatus; + +import java.time.LocalDateTime; + +public record MockApplyHomeItemResponse( + Long mockApplyId, + String resumePath, + Long jobPostingId, + MockApplyStatus status, + String companyName, + String detailClassificationName, + String jobTitle, + LocalDateTime createdAt, + ApplyType applyType, + Integer score +) { + public static MockApplyHomeItemResponse from(MockApply mockApply) { + JobPosting jobPosting = mockApply.getJobPosting(); + String detailClassificationName = jobPosting.getDetailClassification().getDetailName(); + Analysis analysis = mockApply.getAnalysis(); + + return new MockApplyHomeItemResponse( + mockApply.getId(), + resumePath(mockApply), + jobPosting.getId(), + mockApply.getStatus(), + jobPosting.getCompany().getName(), + detailClassificationName, + detailClassificationName, + mockApply.getCreatedAt(), + mockApply.getApplyType(), + analysis == null ? null : analysis.getScore() + ); + } + + private static String resumePath(MockApply mockApply) { + return switch (mockApply.getStatus()) { + case APPLICATION_CREATED -> "/mock-applies/" + mockApply.getId() + "/job-posting"; + case QUESTION_SELECT -> "/mock-applies/" + mockApply.getId() + "/questions"; + case ANSWER_WRITE -> "/mock-applies/" + mockApply.getId() + "/answers"; + case COMPLETED -> "/mock-applies/" + mockApply.getId() + "/analysis"; + }; + } +} diff --git a/src/main/java/com/jobdri/jobdri_api/domain/mockapply/dto/response/MockApplyHomeResponse.java b/src/main/java/com/jobdri/jobdri_api/domain/mockapply/dto/response/MockApplyHomeResponse.java new file mode 100644 index 0000000..7da94c0 --- /dev/null +++ b/src/main/java/com/jobdri/jobdri_api/domain/mockapply/dto/response/MockApplyHomeResponse.java @@ -0,0 +1,9 @@ +package com.jobdri.jobdri_api.domain.mockapply.dto.response; + +import java.util.List; + +public record MockApplyHomeResponse( + List inProgress, + List completed +) { +} 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 3ddd7a0..1a273a0 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,18 @@ public interface MockApplyRepository extends JpaRepository { List findAllByJobPostingId(Long jobPostingId); long countByUserIdAndJobPostingId(Long userId, Long jobPostingId); + @Query(""" + select ma + from MockApply ma + join fetch ma.jobPosting jp + join fetch jp.company + join fetch jp.detailClassification + left join fetch ma.analysis + where ma.user.id = :userId + order by ma.createdAt desc, ma.id desc + """) + List findHomeItemsByUserId(@Param("userId") Long userId); + @Query(""" select count(ma) from MockApply ma 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 5b64b31..cd2b1b4 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 @@ -12,9 +12,12 @@ import com.jobdri.jobdri_api.domain.mockapply.dto.request.MockApplyCreateMockFromJobPostingRequest; import com.jobdri.jobdri_api.domain.mockapply.dto.request.MockApplyCreateMockRequest; import com.jobdri.jobdri_api.domain.mockapply.dto.response.MockApplyCreateResponse; +import com.jobdri.jobdri_api.domain.mockapply.dto.response.MockApplyHomeItemResponse; +import com.jobdri.jobdri_api.domain.mockapply.dto.response.MockApplyHomeResponse; import com.jobdri.jobdri_api.domain.mockapply.dto.response.MockApplySequenceResponse; import com.jobdri.jobdri_api.domain.mockapply.entity.ApplyType; import com.jobdri.jobdri_api.domain.mockapply.entity.MockApply; +import com.jobdri.jobdri_api.domain.mockapply.entity.MockApplyStatus; import com.jobdri.jobdri_api.domain.mockapply.repository.MockApplyRepository; import com.jobdri.jobdri_api.domain.user.entity.User; import com.jobdri.jobdri_api.domain.user.service.UserService; @@ -24,6 +27,9 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.List; +import java.util.stream.Collectors; + @Service @RequiredArgsConstructor @Transactional(readOnly = true) @@ -122,6 +128,27 @@ public MockApplySequenceResponse getMockApplySequence(User user, Long mockApplyI ); } + public MockApplyHomeResponse getMyMockApplies(User user) { + User validatedUser = userService.validateUser(user); + List items = mockApplyRepository.findHomeItemsByUserId(validatedUser.getId()).stream() + .map(MockApplyHomeItemResponse::from) + .toList(); + + return new MockApplyHomeResponse( + filterByCompletion(items, false), + filterByCompletion(items, true) + ); + } + + private List filterByCompletion( + List items, + boolean completed + ) { + return items.stream() + .filter(item -> completed == (item.status() == MockApplyStatus.COMPLETED)) + .collect(Collectors.toList()); + } + private MockApply getOwnedMockApply(User user, Long mockApplyId) { MockApply mockApply = mockApplyRepository.findById(mockApplyId) .orElseThrow(() -> new GeneralException( 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 388bc1d..88a9385 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,5 +1,7 @@ package com.jobdri.jobdri_api.domain.mockapply.service; +import com.jobdri.jobdri_api.domain.analysis.entity.Analysis; +import com.jobdri.jobdri_api.domain.analysis.repository.AnalysisRepository; import com.jobdri.jobdri_api.domain.classification.entity.Classification; import com.jobdri.jobdri_api.domain.classification.entity.DetailClassification; import com.jobdri.jobdri_api.domain.classification.entity.MiddleClassification; @@ -14,6 +16,7 @@ import com.jobdri.jobdri_api.domain.jobposting.service.MockJobPostingGenerationService; import com.jobdri.jobdri_api.domain.mockapply.dto.request.MockApplyCreateMockRequest; import com.jobdri.jobdri_api.domain.mockapply.dto.response.MockApplyCreateResponse; +import com.jobdri.jobdri_api.domain.mockapply.dto.response.MockApplyHomeResponse; import com.jobdri.jobdri_api.domain.mockapply.dto.response.MockApplySequenceResponse; import com.jobdri.jobdri_api.domain.mockapply.entity.ApplyType; import com.jobdri.jobdri_api.domain.mockapply.entity.MockApply; @@ -51,6 +54,9 @@ class MockApplyServiceTest { @Autowired private MockApplyRepository mockApplyRepository; + @Autowired + private AnalysisRepository analysisRepository; + @Autowired private JobPostingRepository jobPostingRepository; @@ -180,6 +186,42 @@ void getMockApplySequence() { assertThat(response.sequence()).isEqualTo(2); } + @Test + @DisplayName("홈 화면에서 이어쓰기와 완료 결과 카드 목록을 조회한다") + void getMyMockApplies() { + User user = saveUser("home-list@example.com"); + User otherUser = saveUser("home-list-other@example.com"); + JobPosting backendPosting = saveJobPosting(user, "백엔드 개발"); + JobPosting dataPosting = saveJobPosting(user, "데이터 분석"); + JobPosting otherPosting = saveJobPosting(otherUser, "프론트엔드 개발"); + + MockApply inProgress = mockApplyRepository.save(MockApply.create(user, backendPosting, ApplyType.ACTUAL)); + inProgress.updateStatus(MockApplyStatus.ANSWER_WRITE); + MockApply completed = mockApplyRepository.save(MockApply.create(user, dataPosting, ApplyType.MOCK)); + completed.updateStatus(MockApplyStatus.COMPLETED); + mockApplyRepository.save(MockApply.create(otherUser, otherPosting, ApplyType.MOCK)); + Analysis analysis = analysisRepository.save(Analysis.create(completed, 71, 72, 73, 74, "완료 분석입니다.")); + completed.assignAnalysis(analysis); + + MockApplyHomeResponse response = mockApplyService.getMyMockApplies(user); + + assertThat(response.inProgress()).hasSize(1); + assertThat(response.completed()).hasSize(1); + assertThat(response.inProgress().get(0).mockApplyId()).isEqualTo(inProgress.getId()); + assertThat(response.inProgress().get(0).jobPostingId()).isEqualTo(backendPosting.getId()); + 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).applyType()).isEqualTo(ApplyType.ACTUAL); + assertThat(response.inProgress().get(0).score()).isNull(); + assertThat(response.inProgress().get(0).resumePath()).isEqualTo("/mock-applies/" + inProgress.getId() + "/answers"); + assertThat(response.completed().get(0).mockApplyId()).isEqualTo(completed.getId()); + assertThat(response.completed().get(0).score()).isEqualTo(71); + assertThat(response.completed().get(0).applyType()).isEqualTo(ApplyType.MOCK); + assertThat(response.completed().get(0).resumePath()).isEqualTo("/mock-applies/" + completed.getId() + "/analysis"); + } + @Test @DisplayName("존재하지 않는 공고 ID로 ACTUAL 타입 지원 생성 시 예외를 던진다") void createActualApplyThrowsWhenJobPostingNotFound() {