From 315439f3a36ce126ab9125a13cc2e43efabce7f9 Mon Sep 17 00:00:00 2001 From: shinae1023 Date: Tue, 19 May 2026 15:39:10 +0900 Subject: [PATCH] =?UTF-8?q?[Fix]=20=EC=82=AC=EC=9A=A9=EC=9E=90=20=EC=9D=B8?= =?UTF-8?q?=EC=A6=9D=20=EB=B0=8F=20=EA=B2=80=EC=A6=9D=20flow,=20=EC=A7=80?= =?UTF-8?q?=EC=9B=90=EC=84=9C=20=EC=88=98=EC=A0=95=20api(#26)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/JobPostingAiController.java | 22 ++++- .../controller/JobPostingController.java | 36 ++++++-- .../dto/request/JobPostingIngestCommand.java | 1 + .../dto/response/JobPostingResponse.java | 2 + .../domain/jobposting/entity/JobPosting.java | 9 ++ .../repository/JobPostingRepository.java | 2 + .../service/JobPostingAsyncFacadeService.java | 11 ++- .../service/JobPostingIngestService.java | 16 +++- .../jobposting/service/JobPostingService.java | 52 +++++++---- .../controller/MockApplyController.java | 45 ++++++++++ ...kApplyCreateMockFromJobPostingRequest.java | 9 ++ .../mockapply/service/MockApplyService.java | 29 ++++-- .../jobdri_api/domain/user/entity/User.java | 5 ++ .../domain/user/service/UserService.java | 13 +++ .../global/config/OpenAiConfig.java | 2 +- src/main/resources/application-dev.yaml | 88 ++++++++++++++++++ src/main/resources/application.yaml | 89 +------------------ .../analysis/service/QuestionServiceTest.java | 2 + .../service/JobPostingAiServiceTest.java | 7 ++ .../service/MockApplyServiceTest.java | 38 +++++++- 20 files changed, 345 insertions(+), 133 deletions(-) create mode 100644 src/main/java/com/jobdri/jobdri_api/domain/mockapply/dto/request/MockApplyCreateMockFromJobPostingRequest.java create mode 100644 src/main/resources/application-dev.yaml diff --git a/src/main/java/com/jobdri/jobdri_api/domain/jobposting/controller/JobPostingAiController.java b/src/main/java/com/jobdri/jobdri_api/domain/jobposting/controller/JobPostingAiController.java index 33f2c1e..26a7ad2 100644 --- a/src/main/java/com/jobdri/jobdri_api/domain/jobposting/controller/JobPostingAiController.java +++ b/src/main/java/com/jobdri/jobdri_api/domain/jobposting/controller/JobPostingAiController.java @@ -10,7 +10,9 @@ import com.jobdri.jobdri_api.domain.jobposting.service.JobPostingAiService; import com.jobdri.jobdri_api.domain.jobposting.service.JobPostingAsyncFacadeService; import com.jobdri.jobdri_api.domain.jobposting.service.JobPostingIngestService; +import com.jobdri.jobdri_api.domain.user.service.UserService; import com.jobdri.jobdri_api.global.apiPayload.ApiResponse; +import com.jobdri.jobdri_api.global.security.UserDetailsImpl; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.ExampleObject; @@ -20,6 +22,7 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.MediaType; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.ModelAttribute; @@ -37,6 +40,7 @@ public class JobPostingAiController { private final JobPostingAiService jobPostingAiService; private final JobPostingIngestService jobPostingIngestService; private final JobPostingAsyncFacadeService jobPostingAsyncFacadeService; + private final UserService userService; @Operation( summary = "채용 공고 정보 추출", @@ -44,8 +48,10 @@ public class JobPostingAiController { ) @PostMapping(value = "/extract", consumes = MediaType.APPLICATION_JSON_VALUE) public ApiResponse extractJobPostingFromText( + @AuthenticationPrincipal UserDetailsImpl userDetails, @Valid @RequestBody JobPostingExtractRequest request ) { + validateAuthenticatedUser(userDetails); return ApiResponse.onSuccess( "채용 공고 추출에 성공했습니다.", jobPostingAiService.extractJobPosting(request.rawText()) @@ -58,8 +64,10 @@ public ApiResponse extractJobPostingFromText( ) @PostMapping(value = "/extract", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public ApiResponse extractJobPostingFromMultipart( + @AuthenticationPrincipal UserDetailsImpl userDetails, @ModelAttribute JobPostingExtractMultipartRequest request ) { + validateAuthenticatedUser(userDetails); return ApiResponse.onSuccess( "채용 공고 추출에 성공했습니다.", jobPostingAiService.extractJobPosting(request) @@ -194,11 +202,13 @@ public ApiResponse extractJobPostingFromMultipart( }) @PostMapping(value = "/ingest", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public ApiResponse ingestJobPosting( + @AuthenticationPrincipal UserDetailsImpl userDetails, @ModelAttribute JobPostingIngestMultipartRequest request ) { + var user = validateAuthenticatedUser(userDetails); return ApiResponse.onSuccess( "채용 공고 추출 및 저장에 성공했습니다.", - jobPostingIngestService.ingestAndCreate(request) + jobPostingIngestService.ingestAndCreate(user, request) ); } @@ -208,11 +218,13 @@ public ApiResponse ingestJobPosting( ) @PostMapping(value = "/ingest/async", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public ApiResponse submitIngestJobPostingAsync( + @AuthenticationPrincipal UserDetailsImpl userDetails, @ModelAttribute JobPostingIngestMultipartRequest request ) { + var user = validateAuthenticatedUser(userDetails); return ApiResponse.onSuccess( "채용 공고 비동기 작업 접수에 성공했습니다.", - jobPostingAsyncFacadeService.submit(request) + jobPostingAsyncFacadeService.submit(user, request) ); } @@ -222,11 +234,17 @@ public ApiResponse submitIngestJobPostingAsync( ) @GetMapping("/ingest/async/{taskId}") public ApiResponse getIngestJobPostingAsyncStatus( + @AuthenticationPrincipal UserDetailsImpl userDetails, @PathVariable String taskId ) { + validateAuthenticatedUser(userDetails); return ApiResponse.onSuccess( "채용 공고 비동기 작업 상태 조회에 성공했습니다.", jobPostingAsyncFacadeService.getTask(taskId) ); } + + private com.jobdri.jobdri_api.domain.user.entity.User validateAuthenticatedUser(UserDetailsImpl userDetails) { + return userService.validateUser(userDetails == null ? null : userDetails.getUser()); + } } diff --git a/src/main/java/com/jobdri/jobdri_api/domain/jobposting/controller/JobPostingController.java b/src/main/java/com/jobdri/jobdri_api/domain/jobposting/controller/JobPostingController.java index 9cb7639..78823a8 100644 --- a/src/main/java/com/jobdri/jobdri_api/domain/jobposting/controller/JobPostingController.java +++ b/src/main/java/com/jobdri/jobdri_api/domain/jobposting/controller/JobPostingController.java @@ -12,11 +12,14 @@ import com.jobdri.jobdri_api.domain.jobposting.service.MockQuestionCacheService; import com.jobdri.jobdri_api.domain.jobposting.service.MockJobPostingGenerationService; import com.jobdri.jobdri_api.domain.jobposting.service.JobPostingService; +import com.jobdri.jobdri_api.domain.user.service.UserService; import com.jobdri.jobdri_api.global.apiPayload.ApiResponse; +import com.jobdri.jobdri_api.global.security.UserDetailsImpl; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -38,12 +41,15 @@ public class JobPostingController { private final MockJobPostingGenerationService mockJobPostingGenerationService; private final MockQuestionCacheService mockQuestionCacheService; private final JobPostingService jobPostingService; + private final UserService userService; @Operation(summary = "채용 공고 초안 생성", description = "회사 정보와 직무 정보를 바탕으로 AI가 공고 본문 초안을 생성합니다.") @PostMapping("/generate") public ApiResponse generateJobPosting( + @AuthenticationPrincipal UserDetailsImpl userDetails, @Valid @RequestBody JobPostingGenerateRequest request ) { + validateAuthenticatedUser(userDetails); return ApiResponse.onSuccess( "채용 공고 초안 생성에 성공했습니다.", jobPostingAiService.generateJobPosting(request) @@ -56,8 +62,10 @@ public ApiResponse generateJobPosting( ) @PostMapping("/mock/generate") public ApiResponse generateMockJobPosting( + @AuthenticationPrincipal UserDetailsImpl userDetails, @Valid @RequestBody JobPostingMockGenerateRequest request ) { + validateAuthenticatedUser(userDetails); return ApiResponse.onSuccess( "모의 공고 생성에 성공했습니다.", mockJobPostingGenerationService.generate(request) @@ -70,8 +78,10 @@ public ApiResponse generateMockJobPosting( ) @PostMapping("/mock/questions") public ApiResponse getMockRecommendedQuestions( + @AuthenticationPrincipal UserDetailsImpl userDetails, @Valid @RequestBody JobPostingMockGenerateRequest request ) { + validateAuthenticatedUser(userDetails); return ApiResponse.onSuccess( "모의 공고 추천 질문 조회에 성공했습니다.", new JobPostingMockQuestionResponse(mockQuestionCacheService.getRecommendedQuestions(request)) @@ -81,44 +91,58 @@ public ApiResponse getMockRecommendedQuestions( @Operation(summary = "채용 공고 저장", description = "생성되었거나 직접 작성한 채용 공고를 DB에 저장합니다.") @PostMapping public ApiResponse createJobPosting( + @AuthenticationPrincipal UserDetailsImpl userDetails, @Valid @RequestBody JobPostingCreateRequest request ) { + var user = validateAuthenticatedUser(userDetails); return ApiResponse.onSuccess( "채용 공고 저장에 성공했습니다.", - jobPostingService.createJobPosting(request) + jobPostingService.createJobPosting(user, request) ); } @Operation(summary = "채용 공고 수정", description = "기존 채용 공고를 수정합니다. 회사명이 없으면 회사를 새로 생성합니다.") @PutMapping("/{jobPostingId}") public ApiResponse updateJobPosting( + @AuthenticationPrincipal UserDetailsImpl userDetails, @PathVariable Long jobPostingId, @Valid @RequestBody JobPostingUpdateRequest request ) { + var user = validateAuthenticatedUser(userDetails); return ApiResponse.onSuccess( "채용 공고 수정에 성공했습니다.", - jobPostingService.updateJobPosting(jobPostingId, request) + jobPostingService.updateJobPosting(user, jobPostingId, request) ); } @Operation(summary = "채용 공고 단건 조회", description = "채용 공고 ID로 단건 조회합니다.") @GetMapping("/{jobPostingId}") - public ApiResponse getJobPosting(@PathVariable Long jobPostingId) { + public ApiResponse getJobPosting( + @AuthenticationPrincipal UserDetailsImpl userDetails, + @PathVariable Long jobPostingId + ) { + var user = validateAuthenticatedUser(userDetails); return ApiResponse.onSuccess( "채용 공고 조회에 성공했습니다.", - jobPostingService.getJobPosting(jobPostingId) + jobPostingService.getJobPosting(user, jobPostingId) ); } @Operation(summary = "채용 공고 목록 조회", description = "전체 공고 또는 회사별 공고 목록을 조회합니다.") @GetMapping public ApiResponse> getJobPostings( + @AuthenticationPrincipal UserDetailsImpl userDetails, @RequestParam(required = false) Long companyId ) { + var user = validateAuthenticatedUser(userDetails); List result = companyId == null - ? jobPostingService.getAllJobPostings() - : jobPostingService.getJobPostingsByCompany(companyId); + ? jobPostingService.getAllJobPostings(user) + : jobPostingService.getJobPostingsByCompany(user, companyId); return ApiResponse.onSuccess("채용 공고 목록 조회에 성공했습니다.", result); } + + private com.jobdri.jobdri_api.domain.user.entity.User validateAuthenticatedUser(UserDetailsImpl userDetails) { + return userService.validateUser(userDetails == null ? null : userDetails.getUser()); + } } diff --git a/src/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/request/JobPostingIngestCommand.java b/src/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/request/JobPostingIngestCommand.java index 409041d..f29d0fd 100644 --- a/src/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/request/JobPostingIngestCommand.java +++ b/src/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/request/JobPostingIngestCommand.java @@ -8,6 +8,7 @@ @Builder public class JobPostingIngestCommand { + private Long userId; private String rawText; private String sourceUrl; private byte[] imageBytes; diff --git a/src/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/response/JobPostingResponse.java b/src/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/response/JobPostingResponse.java index 96f9d58..a99cdfa 100644 --- a/src/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/response/JobPostingResponse.java +++ b/src/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/response/JobPostingResponse.java @@ -9,6 +9,7 @@ public class JobPostingResponse { private Long jobPostingId; + private Long userId; private Long companyId; private String companyName; private String companySize; @@ -21,6 +22,7 @@ public class JobPostingResponse { public static JobPostingResponse from(JobPosting jobPosting) { return JobPostingResponse.builder() .jobPostingId(jobPosting.getId()) + .userId(jobPosting.getUser().getId()) .companyId(jobPosting.getCompany().getId()) .companyName(jobPosting.getCompany().getName()) .companySize(jobPosting.getCompany().getSize().name()) diff --git a/src/main/java/com/jobdri/jobdri_api/domain/jobposting/entity/JobPosting.java b/src/main/java/com/jobdri/jobdri_api/domain/jobposting/entity/JobPosting.java index aa9166c..608838b 100644 --- a/src/main/java/com/jobdri/jobdri_api/domain/jobposting/entity/JobPosting.java +++ b/src/main/java/com/jobdri/jobdri_api/domain/jobposting/entity/JobPosting.java @@ -3,6 +3,7 @@ import com.jobdri.jobdri_api.domain.classification.entity.DetailClassification; import com.jobdri.jobdri_api.domain.company.entity.Company; import com.jobdri.jobdri_api.domain.mockapply.entity.MockApply; +import com.jobdri.jobdri_api.domain.user.entity.User; import jakarta.persistence.*; import lombok.*; @@ -25,6 +26,10 @@ public class JobPosting { @JoinColumn(name = "company_id", nullable = false) private Company company; + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "user_id", nullable = false) + private User user; + @ManyToOne(fetch = FetchType.LAZY, optional = false) @JoinColumn(name = "detail_classification_id", nullable = false) private DetailClassification detailClassification; @@ -43,6 +48,7 @@ public class JobPosting { private List mockApplies = new ArrayList<>(); public static JobPosting create( + User user, Company company, DetailClassification detailClassification, String task, @@ -50,6 +56,7 @@ public static JobPosting create( String preferred ) { return JobPosting.builder() + .user(user) .company(company) .detailClassification(detailClassification) .task(task) @@ -59,12 +66,14 @@ public static JobPosting create( } public void update( + User user, Company company, DetailClassification detailClassification, String task, String requirement, String preferred ) { + this.user = user; this.company = company; this.detailClassification = detailClassification; this.task = task; diff --git a/src/main/java/com/jobdri/jobdri_api/domain/jobposting/repository/JobPostingRepository.java b/src/main/java/com/jobdri/jobdri_api/domain/jobposting/repository/JobPostingRepository.java index dfd86d6..4e4a9a6 100644 --- a/src/main/java/com/jobdri/jobdri_api/domain/jobposting/repository/JobPostingRepository.java +++ b/src/main/java/com/jobdri/jobdri_api/domain/jobposting/repository/JobPostingRepository.java @@ -9,6 +9,8 @@ public interface JobPostingRepository extends JpaRepository { List findAllByCompanyId(Long companyId); + List findAllByUserId(Long userId); + List findAllByUserIdAndCompanyId(Long userId, Long companyId); List findTop5ByDetailClassificationIdOrderByIdDesc(Long detailClassificationId); List findTop5ByCompanyIdOrderByIdDesc(Long companyId); diff --git a/src/main/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingAsyncFacadeService.java b/src/main/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingAsyncFacadeService.java index 0763afc..23aa3f6 100644 --- a/src/main/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingAsyncFacadeService.java +++ b/src/main/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingAsyncFacadeService.java @@ -4,6 +4,8 @@ import com.jobdri.jobdri_api.domain.jobposting.dto.request.JobPostingIngestMultipartRequest; import com.jobdri.jobdri_api.domain.jobposting.dto.response.JobPostingAsyncStatusResponse; import com.jobdri.jobdri_api.domain.jobposting.dto.response.JobPostingAsyncSubmitResponse; +import com.jobdri.jobdri_api.domain.user.entity.User; +import com.jobdri.jobdri_api.domain.user.service.UserService; import com.jobdri.jobdri_api.global.apiPayload.code.GeneralErrorCode; import com.jobdri.jobdri_api.global.apiPayload.exception.GeneralException; import lombok.RequiredArgsConstructor; @@ -19,10 +21,12 @@ public class JobPostingAsyncFacadeService { private final JobPostingAsyncTaskService jobPostingAsyncTaskService; private final JobPostingAsyncProcessor jobPostingAsyncProcessor; + private final UserService userService; - public JobPostingAsyncSubmitResponse submit(JobPostingIngestMultipartRequest request) { + public JobPostingAsyncSubmitResponse submit(User user, JobPostingIngestMultipartRequest request) { + User validatedUser = userService.validateUser(user); String taskId = jobPostingAsyncTaskService.createPendingTask(); - JobPostingIngestCommand command = snapshot(request); + JobPostingIngestCommand command = snapshot(validatedUser, request); try { jobPostingAsyncProcessor.process(taskId, command); @@ -43,8 +47,9 @@ public JobPostingAsyncStatusResponse getTask(String taskId) { return jobPostingAsyncTaskService.getTask(taskId); } - private JobPostingIngestCommand snapshot(JobPostingIngestMultipartRequest request) { + private JobPostingIngestCommand snapshot(User user, JobPostingIngestMultipartRequest request) { return JobPostingIngestCommand.builder() + .userId(user.getId()) .rawText(request.rawText()) .sourceUrl(request.sourceUrl()) .imageBytes(readBytes(request.image())) diff --git a/src/main/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingIngestService.java b/src/main/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingIngestService.java index a580836..f093e8f 100644 --- a/src/main/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingIngestService.java +++ b/src/main/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingIngestService.java @@ -10,6 +10,8 @@ import com.jobdri.jobdri_api.domain.jobposting.dto.response.JobPostingGenerateResponse; import com.jobdri.jobdri_api.domain.jobposting.dto.response.JobPostingIngestResponse; import com.jobdri.jobdri_api.domain.jobposting.dto.response.JobPostingResponse; +import com.jobdri.jobdri_api.domain.user.entity.User; +import com.jobdri.jobdri_api.domain.user.service.UserService; import com.jobdri.jobdri_api.global.apiPayload.code.GeneralErrorCode; import com.jobdri.jobdri_api.global.apiPayload.exception.GeneralException; import lombok.RequiredArgsConstructor; @@ -24,15 +26,17 @@ public class JobPostingIngestService { private static final int DEFAULT_CANDIDATE_LIMIT = 10; - @Value("${job-posting.ingest.classification-confidence-threshold}") + @Value("${job-posting.ingest.classification-confidence-threshold:0.65}") private double classificationConfidenceThreshold; private final JobPostingAiService jobPostingAiService; private final JobPostingClassificationService jobPostingClassificationService; private final JobPostingService jobPostingService; + private final UserService userService; - public JobPostingIngestResponse ingestAndCreate(JobPostingIngestMultipartRequest request) { + public JobPostingIngestResponse ingestAndCreate(User user, JobPostingIngestMultipartRequest request) { JobPostingIngestCommand command = JobPostingIngestCommand.builder() + .userId(user.getId()) .rawText(request.rawText()) .sourceUrl(request.sourceUrl()) .companySize(request.companySize()) @@ -92,6 +96,7 @@ public JobPostingIngestResponse ingestAndCreate(JobPostingIngestCommand command) ); JobPostingResponse saved = jobPostingService.createJobPosting( + resolveUser(command), new JobPostingCreateRequest( fallbackCompanyName(extracted.companyName()), command.getCompanySize(), @@ -119,4 +124,11 @@ private String fallbackCompanyName(String companyName) { } return companyName; } + + private User resolveUser(JobPostingIngestCommand command) { + if (command.getUserId() == null) { + throw new GeneralException(GeneralErrorCode.MISSING_AUTH_INFO, "인증 정보가 누락되었습니다."); + } + return userService.getUser(command.getUserId()); + } } diff --git a/src/main/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingService.java b/src/main/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingService.java index 65e35e2..b04a9c4 100644 --- a/src/main/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingService.java +++ b/src/main/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingService.java @@ -9,6 +9,8 @@ import com.jobdri.jobdri_api.domain.jobposting.dto.response.JobPostingResponse; import com.jobdri.jobdri_api.domain.jobposting.entity.JobPosting; import com.jobdri.jobdri_api.domain.jobposting.repository.JobPostingRepository; +import com.jobdri.jobdri_api.domain.user.entity.User; +import com.jobdri.jobdri_api.domain.user.service.UserService; import com.jobdri.jobdri_api.global.apiPayload.code.GeneralErrorCode; import com.jobdri.jobdri_api.global.apiPayload.exception.GeneralException; import lombok.RequiredArgsConstructor; @@ -25,13 +27,16 @@ public class JobPostingService { private final JobPostingRepository jobPostingRepository; private final CompanyRepository companyRepository; private final DetailClassificationRepository detailClassificationRepository; + private final UserService userService; @Transactional - public JobPostingResponse createJobPosting(JobPostingCreateRequest request) { + public JobPostingResponse createJobPosting(User user, JobPostingCreateRequest request) { + User validatedUser = userService.validateUser(user); Company company = findOrCreateCompany(request.companyName(), request.companySize()); DetailClassification detailClassification = findDetailClassification(request.detailClassificationId()); JobPosting jobPosting = JobPosting.create( + validatedUser, company, detailClassification, request.task(), @@ -43,17 +48,15 @@ public JobPostingResponse createJobPosting(JobPostingCreateRequest request) { } @Transactional - public JobPostingResponse updateJobPosting(Long jobPostingId, JobPostingUpdateRequest request) { - JobPosting jobPosting = jobPostingRepository.findById(jobPostingId) - .orElseThrow(() -> new GeneralException( - GeneralErrorCode.JOB_POSTING_NOT_FOUND, - "해당 공고를 찾을 수 없습니다. jobPostingId=" + jobPostingId - )); + public JobPostingResponse updateJobPosting(User user, Long jobPostingId, JobPostingUpdateRequest request) { + User validatedUser = userService.validateUser(user); + JobPosting jobPosting = getOwnedJobPosting(validatedUser, jobPostingId); Company company = findOrCreateCompany(request.companyName(), request.companySize()); DetailClassification detailClassification = findDetailClassification(request.detailClassificationId()); jobPosting.update( + validatedUser, company, detailClassification, request.task(), @@ -64,28 +67,39 @@ public JobPostingResponse updateJobPosting(Long jobPostingId, JobPostingUpdateRe return JobPostingResponse.from(jobPosting); } - public JobPostingResponse getJobPosting(Long jobPostingId) { - JobPosting jobPosting = jobPostingRepository.findById(jobPostingId) - .orElseThrow(() -> new GeneralException( - GeneralErrorCode.JOB_POSTING_NOT_FOUND, - "해당 공고를 찾을 수 없습니다. jobPostingId=" + jobPostingId - )); - - return JobPostingResponse.from(jobPosting); + public JobPostingResponse getJobPosting(User user, Long jobPostingId) { + User validatedUser = userService.validateUser(user); + return JobPostingResponse.from(getOwnedJobPosting(validatedUser, jobPostingId)); } - public List getAllJobPostings() { - return jobPostingRepository.findAll().stream() + public List getAllJobPostings(User user) { + User validatedUser = userService.validateUser(user); + return jobPostingRepository.findAllByUserId(validatedUser.getId()).stream() .map(JobPostingResponse::from) .toList(); } - public List getJobPostingsByCompany(Long companyId) { - return jobPostingRepository.findAllByCompanyId(companyId).stream() + public List getJobPostingsByCompany(User user, Long companyId) { + User validatedUser = userService.validateUser(user); + return jobPostingRepository.findAllByUserIdAndCompanyId(validatedUser.getId(), companyId).stream() .map(JobPostingResponse::from) .toList(); } + public JobPosting getOwnedJobPosting(User user, Long jobPostingId) { + JobPosting jobPosting = jobPostingRepository.findById(jobPostingId) + .orElseThrow(() -> new GeneralException( + GeneralErrorCode.JOB_POSTING_NOT_FOUND, + "해당 공고를 찾을 수 없습니다. jobPostingId=" + jobPostingId + )); + + if (!jobPosting.getUser().getId().equals(user.getId())) { + throw new GeneralException(GeneralErrorCode.FORBIDDEN, "해당 공고에 접근할 수 없습니다."); + } + + return jobPosting; + } + private Company findOrCreateCompany(String companyName, com.jobdri.jobdri_api.domain.company.entity.CompanySize companySize) { return companyRepository.findByName(companyName) .orElseGet(() -> companyRepository.save(Company.create(companyName, companySize))); 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 9a06b0a..90c5666 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 @@ -1,6 +1,7 @@ package com.jobdri.jobdri_api.domain.mockapply.controller; import com.jobdri.jobdri_api.domain.mockapply.dto.request.MockApplyCreateActualRequest; +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.jobposting.dto.response.JobPostingResponse; @@ -75,6 +76,50 @@ public ApiResponse createActualApply( ); } + @Operation( + summary = "저장된 공고 기반 MOCK 타입 모의 서류 지원 생성", + description = "AI 초안 생성 후 사용자가 수정하여 저장한 채용 공고 ID를 기준으로 로그인 사용자의 MOCK 타입 모의 서류 지원을 생성합니다." + ) + @ApiResponses(value = { + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "200", + description = "모의 서류 지원 생성 성공", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ApiResponse.class), + examples = @ExampleObject(value = "{\"isSuccess\":true,\"code\":\"COMMON2000\",\"message\":\"모의 서류 지원이 생성되었습니다.\",\"result\":{\"jobPostingId\":1,\"mockApplyId\":10,\"applyType\":\"MOCK\"},\"error\":null}") + ) + ), + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "401", + description = "인증 정보 누락", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ApiResponse.class), + examples = @ExampleObject(value = "{\"isSuccess\":false,\"code\":\"AUTH_4011\",\"message\":\"인증 정보가 누락되었습니다.\",\"result\":null,\"error\":\"인증 정보가 누락되었습니다.\"}") + ) + ), + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "403", + description = "다른 사용자의 공고 접근", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ApiResponse.class), + examples = @ExampleObject(value = "{\"isSuccess\":false,\"code\":\"AUTH_4031\",\"message\":\"해당 공고에 접근할 수 없습니다.\",\"result\":null,\"error\":\"해당 공고에 접근할 수 없습니다.\"}") + ) + ) + }) + @PostMapping("/mock/from-job-posting") + public ApiResponse createMockApplyFromJobPosting( + @AuthenticationPrincipal UserDetailsImpl userDetails, + @Valid @RequestBody MockApplyCreateMockFromJobPostingRequest request + ) { + return ApiResponse.onSuccess( + "모의 서류 지원이 생성되었습니다.", + mockApplyService.createMockApplyFromJobPosting(userDetails.getUser(), request.jobPostingId()) + ); + } + @Operation( summary = "가상 공고 기반 모의 서류 지원 생성", description = "선택한 소분류를 기준으로 가상 채용 공고와 MOCK 타입 모의 서류 지원을 함께 생성합니다." diff --git a/src/main/java/com/jobdri/jobdri_api/domain/mockapply/dto/request/MockApplyCreateMockFromJobPostingRequest.java b/src/main/java/com/jobdri/jobdri_api/domain/mockapply/dto/request/MockApplyCreateMockFromJobPostingRequest.java new file mode 100644 index 0000000..469355a --- /dev/null +++ b/src/main/java/com/jobdri/jobdri_api/domain/mockapply/dto/request/MockApplyCreateMockFromJobPostingRequest.java @@ -0,0 +1,9 @@ +package com.jobdri.jobdri_api.domain.mockapply.dto.request; + +import jakarta.validation.constraints.NotNull; + +public record MockApplyCreateMockFromJobPostingRequest( + @NotNull(message = "공고 ID는 필수입니다.") + Long jobPostingId +) { +} 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 2237a36..065a6c0 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 @@ -9,12 +9,14 @@ import com.jobdri.jobdri_api.domain.jobposting.repository.JobPostingRepository; import com.jobdri.jobdri_api.domain.jobposting.service.JobPostingService; import com.jobdri.jobdri_api.domain.jobposting.service.MockJobPostingGenerationService; +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.entity.ApplyType; import com.jobdri.jobdri_api.domain.mockapply.entity.MockApply; 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; import com.jobdri.jobdri_api.global.apiPayload.code.GeneralErrorCode; import com.jobdri.jobdri_api.global.apiPayload.exception.GeneralException; import lombok.RequiredArgsConstructor; @@ -30,21 +32,29 @@ public class MockApplyService { private final CompanyRepository companyRepository; private final MockJobPostingGenerationService mockJobPostingGenerationService; private final JobPostingService jobPostingService; + private final UserService userService; @Transactional public MockApplyCreateResponse createActualApply(User user, Long jobPostingId) { - JobPosting jobPosting = jobPostingRepository.findById(jobPostingId) - .orElseThrow(() -> new GeneralException( - GeneralErrorCode.JOB_POSTING_NOT_FOUND, - "해당 공고를 찾을 수 없습니다. jobPostingId=" + jobPostingId - )); + User validatedUser = userService.validateUser(user); + JobPosting jobPosting = jobPostingService.getOwnedJobPosting(validatedUser, jobPostingId); + + MockApply mockApply = MockApply.create(validatedUser, jobPosting, ApplyType.ACTUAL); + return MockApplyCreateResponse.from(mockApplyRepository.save(mockApply)); + } + + @Transactional + public MockApplyCreateResponse createMockApplyFromJobPosting(User user, Long jobPostingId) { + User validatedUser = userService.validateUser(user); + JobPosting jobPosting = jobPostingService.getOwnedJobPosting(validatedUser, jobPostingId); - MockApply mockApply = MockApply.create(user, jobPosting, ApplyType.ACTUAL); + MockApply mockApply = MockApply.create(validatedUser, jobPosting, ApplyType.MOCK); return MockApplyCreateResponse.from(mockApplyRepository.save(mockApply)); } @Transactional public MockApplyCreateResponse createMockApply(User user, MockApplyCreateMockRequest request) { + User validatedUser = userService.validateUser(user); Company company = companyRepository.findById(request.companyId()) .orElseThrow(() -> new GeneralException( GeneralErrorCode.COMPANY_NOT_FOUND, @@ -62,19 +72,20 @@ public MockApplyCreateResponse createMockApply(User user, MockApplyCreateMockReq generated.requirement(), generated.preferred() ); - Long savedJobPostingId = jobPostingService.createJobPosting(createRequest).getJobPostingId(); + Long savedJobPostingId = jobPostingService.createJobPosting(validatedUser, createRequest).getJobPostingId(); JobPosting savedJobPosting = jobPostingRepository.findById(savedJobPostingId) .orElseThrow(() -> new GeneralException( GeneralErrorCode.JOB_POSTING_NOT_FOUND, "생성된 모의 공고를 찾을 수 없습니다. jobPostingId=" + savedJobPostingId )); - MockApply mockApply = MockApply.create(user, savedJobPosting, ApplyType.MOCK); + MockApply mockApply = MockApply.create(validatedUser, savedJobPosting, ApplyType.MOCK); return MockApplyCreateResponse.from(mockApplyRepository.save(mockApply)); } public JobPostingResponse getMockApplyJobPosting(User user, Long mockApplyId) { - MockApply mockApply = getOwnedMockApply(user, mockApplyId); + User validatedUser = userService.validateUser(user); + MockApply mockApply = getOwnedMockApply(validatedUser, mockApplyId); return JobPostingResponse.from(mockApply.getJobPosting()); } diff --git a/src/main/java/com/jobdri/jobdri_api/domain/user/entity/User.java b/src/main/java/com/jobdri/jobdri_api/domain/user/entity/User.java index 94a5904..e6d4b70 100644 --- a/src/main/java/com/jobdri/jobdri_api/domain/user/entity/User.java +++ b/src/main/java/com/jobdri/jobdri_api/domain/user/entity/User.java @@ -1,6 +1,7 @@ package com.jobdri.jobdri_api.domain.user.entity; import com.jobdri.jobdri_api.domain.experience.entity.Experience; +import com.jobdri.jobdri_api.domain.jobposting.entity.JobPosting; import com.jobdri.jobdri_api.domain.mockapply.entity.MockApply; import com.jobdri.jobdri_api.domain.payment.entity.Payment; import jakarta.persistence.*; @@ -57,6 +58,10 @@ public class User { @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) private List mockApplies = new ArrayList<>(); + @Builder.Default + @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) + private List jobPostings = new ArrayList<>(); + public static User signup( String name, String email, diff --git a/src/main/java/com/jobdri/jobdri_api/domain/user/service/UserService.java b/src/main/java/com/jobdri/jobdri_api/domain/user/service/UserService.java index d41a064..7a58e58 100644 --- a/src/main/java/com/jobdri/jobdri_api/domain/user/service/UserService.java +++ b/src/main/java/com/jobdri/jobdri_api/domain/user/service/UserService.java @@ -19,4 +19,17 @@ public User getUser(Long userId) { return userRepository.findById(userId) .orElseThrow(() -> new GeneralException(GeneralErrorCode.USER_NOT_FOUND)); } + + @Transactional(readOnly = true) + public User validateUser(User user) { + if (user == null || user.getId() == null) { + throw new GeneralException(GeneralErrorCode.MISSING_AUTH_INFO, "인증 정보가 누락되었습니다."); + } + + return userRepository.findById(user.getId()) + .orElseThrow(() -> new GeneralException( + GeneralErrorCode.USER_NOT_FOUND, + "해당 유저를 찾을 수 없습니다. userId=" + user.getId() + )); + } } diff --git a/src/main/java/com/jobdri/jobdri_api/global/config/OpenAiConfig.java b/src/main/java/com/jobdri/jobdri_api/global/config/OpenAiConfig.java index 1d3e359..361d4cf 100644 --- a/src/main/java/com/jobdri/jobdri_api/global/config/OpenAiConfig.java +++ b/src/main/java/com/jobdri/jobdri_api/global/config/OpenAiConfig.java @@ -11,7 +11,7 @@ @Configuration public class OpenAiConfig { - @Value("${openai.api.key}") + @Value("${openai.api.key:}") private String openAiApiKey; @Bean diff --git a/src/main/resources/application-dev.yaml b/src/main/resources/application-dev.yaml new file mode 100644 index 0000000..23b5d28 --- /dev/null +++ b/src/main/resources/application-dev.yaml @@ -0,0 +1,88 @@ +spring: + application: + name: jobdri-api + sql: + init: + mode: always + datasource: + url: ${DB_URL:jdbc:postgresql://localhost:5432/jobdri} + username: ${DB_USERNAME:jobdri} + password: ${DB_PASSWORD:jobdri} + driver-class-name: ${DB_DRIVER:org.postgresql.Driver} + jpa: + hibernate: + ddl-auto: update + properties: + hibernate: + format_sql: true + open-in-view: false + data: + redis: + host: ${REDIS_HOST:localhost} + port: ${REDIS_PORT:6379} + password: ${REDIS_PASSWORD:} + ssl: + enabled: ${REDIS_SSL_ENABLED:false} + mail: + host: ${MAIL_HOST:smtp.gmail.com} + port: ${MAIL_PORT:587} + username: ${MAIL_USERNAME:} + password: ${MAIL_PASSWORD:} + default-encoding: UTF-8 + properties: + mail: + smtp: + auth: ${MAIL_SMTP_AUTH:true} + starttls: + enable: ${MAIL_SMTP_STARTTLS_ENABLE:true} + connectiontimeout: ${MAIL_SMTP_CONNECTION_TIMEOUT:5000} + timeout: ${MAIL_SMTP_TIMEOUT:5000} + writetimeout: ${MAIL_SMTP_WRITE_TIMEOUT:5000} + security: + oauth2: + client: + registration: + google: + client-id: ${GOOGLE_CLIENT_ID:dummy-google-client-id} + client-secret: ${GOOGLE_CLIENT_SECRET:dummy-google-client-secret} + scope: + - email + - profile + redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}" + +mail: + from: ${MAIL_FROM:${MAIL_USERNAME:}} + +app: + oauth2: + redirect-uri: ${APP_OAUTH2_REDIRECT_URI:http://localhost:3000/oauth2/redirect} + +server: + port: 8080 + +jwt: + secret: + key: ${JWT_SECRET_KEY:am9iZHJpLWxvY2FsLXNlY3JldC1rZXktZm9yLWRldmVsb3BtZW50LTIwMjYtam9iZHJp} + expiration: + access-token: ${JWT_ACCESS_TOKEN_EXPIRATION:3600000} + refresh-token: ${JWT_REFRESH_TOKEN_EXPIRATION:1209600000} + +openai: + api: + key: ${OPENAI_API_KEY:} + model: + job-posting-extractor: ${OPENAI_JOB_POSTING_MODEL:gpt-4o-mini} + +job-posting: + ingest: + classification-confidence-threshold: ${JOB_POSTING_CLASSIFICATION_CONFIDENCE_THRESHOLD:0.65} + +async: + job-posting: + core-pool-size: ${JOB_POSTING_ASYNC_CORE_POOL_SIZE:2} + max-pool-size: ${JOB_POSTING_ASYNC_MAX_POOL_SIZE:4} + queue-capacity: ${JOB_POSTING_ASYNC_QUEUE_CAPACITY:20} + mail: + core-pool-size: ${MAIL_ASYNC_CORE_POOL_SIZE:1} + max-pool-size: ${MAIL_ASYNC_MAX_POOL_SIZE:2} + queue-capacity: ${MAIL_ASYNC_QUEUE_CAPACITY:50} diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 23b5d28..f9333d9 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -1,88 +1,3 @@ spring: - application: - name: jobdri-api - sql: - init: - mode: always - datasource: - url: ${DB_URL:jdbc:postgresql://localhost:5432/jobdri} - username: ${DB_USERNAME:jobdri} - password: ${DB_PASSWORD:jobdri} - driver-class-name: ${DB_DRIVER:org.postgresql.Driver} - jpa: - hibernate: - ddl-auto: update - properties: - hibernate: - format_sql: true - open-in-view: false - data: - redis: - host: ${REDIS_HOST:localhost} - port: ${REDIS_PORT:6379} - password: ${REDIS_PASSWORD:} - ssl: - enabled: ${REDIS_SSL_ENABLED:false} - mail: - host: ${MAIL_HOST:smtp.gmail.com} - port: ${MAIL_PORT:587} - username: ${MAIL_USERNAME:} - password: ${MAIL_PASSWORD:} - default-encoding: UTF-8 - properties: - mail: - smtp: - auth: ${MAIL_SMTP_AUTH:true} - starttls: - enable: ${MAIL_SMTP_STARTTLS_ENABLE:true} - connectiontimeout: ${MAIL_SMTP_CONNECTION_TIMEOUT:5000} - timeout: ${MAIL_SMTP_TIMEOUT:5000} - writetimeout: ${MAIL_SMTP_WRITE_TIMEOUT:5000} - security: - oauth2: - client: - registration: - google: - client-id: ${GOOGLE_CLIENT_ID:dummy-google-client-id} - client-secret: ${GOOGLE_CLIENT_SECRET:dummy-google-client-secret} - scope: - - email - - profile - redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}" - -mail: - from: ${MAIL_FROM:${MAIL_USERNAME:}} - -app: - oauth2: - redirect-uri: ${APP_OAUTH2_REDIRECT_URI:http://localhost:3000/oauth2/redirect} - -server: - port: 8080 - -jwt: - secret: - key: ${JWT_SECRET_KEY:am9iZHJpLWxvY2FsLXNlY3JldC1rZXktZm9yLWRldmVsb3BtZW50LTIwMjYtam9iZHJp} - expiration: - access-token: ${JWT_ACCESS_TOKEN_EXPIRATION:3600000} - refresh-token: ${JWT_REFRESH_TOKEN_EXPIRATION:1209600000} - -openai: - api: - key: ${OPENAI_API_KEY:} - model: - job-posting-extractor: ${OPENAI_JOB_POSTING_MODEL:gpt-4o-mini} - -job-posting: - ingest: - classification-confidence-threshold: ${JOB_POSTING_CLASSIFICATION_CONFIDENCE_THRESHOLD:0.65} - -async: - job-posting: - core-pool-size: ${JOB_POSTING_ASYNC_CORE_POOL_SIZE:2} - max-pool-size: ${JOB_POSTING_ASYNC_MAX_POOL_SIZE:4} - queue-capacity: ${JOB_POSTING_ASYNC_QUEUE_CAPACITY:20} - mail: - core-pool-size: ${MAIL_ASYNC_CORE_POOL_SIZE:1} - max-pool-size: ${MAIL_ASYNC_MAX_POOL_SIZE:2} - queue-capacity: ${MAIL_ASYNC_QUEUE_CAPACITY:50} + profiles: + active: prod # 본인이 테스트할 환경에 따라서 바꾸기 \ No newline at end of file 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 8c7aeac..23d758b 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 @@ -225,9 +225,11 @@ private MockApply saveMockApply(User user) { } private JobPosting saveJobPosting() { + User user = saveUser("question-jobposting" + System.nanoTime() + "@example.com"); Company company = companyRepository.save(Company.create("테스트 기업", CompanySize.MEDIUM)); DetailClassification detailClassification = saveDetailClassification(); return jobPostingRepository.save(JobPosting.create( + user, company, detailClassification, "주요 업무", diff --git a/src/test/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingAiServiceTest.java b/src/test/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingAiServiceTest.java index 63797be..71eb4d7 100644 --- a/src/test/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingAiServiceTest.java +++ b/src/test/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingAiServiceTest.java @@ -35,6 +35,8 @@ class JobPostingAiServiceTest { private static final Company TEST_COMPANY = Company.create("선택 기업", CompanySize.MEDIUM); + private static final com.jobdri.jobdri_api.domain.user.entity.User TEST_USER = + com.jobdri.jobdri_api.domain.user.entity.User.signup("테스트 사용자", "test-user@example.com", "encoded-password"); @Mock private OpenAIClient openAIClient; @@ -55,6 +57,7 @@ void setUp() { jobPostingRepository ); ReflectionTestUtils.setField(TEST_COMPANY, "id", 1L); + ReflectionTestUtils.setField(TEST_USER, "id", 1L); ReflectionTestUtils.setField(jobPostingAiService, "extractionModel", "gpt-4o-mini"); } @@ -110,6 +113,7 @@ void generateMockJobPostingUsesFallbackWhenNoReferencePostings() { void generateMockJobPostingUsesReferencePostingFallback() { DetailClassification detailClassification = createDetailClassification(10L, 100L, "데이터", "데이터 분석"); JobPosting referencePosting = JobPosting.create( + TEST_USER, Company.create("참고 기업", CompanySize.MEDIUM), detailClassification, "기존 주요 업무", @@ -136,6 +140,7 @@ void generateMockJobPostingUsesReferencePostingFallback() { void generateMockJobPostingPrefersCompanyAndDetailReferences() { DetailClassification detailClassification = createDetailClassification(10L, 100L, "데이터", "데이터 분석"); JobPosting companySpecificPosting = JobPosting.create( + TEST_USER, Company.create("선택 기업", CompanySize.MEDIUM), detailClassification, "회사 맞춤 주요 업무", @@ -176,6 +181,7 @@ void generateMockRecommendedQuestionsUsesFallback() { void generateMockJobPostingUsesTopScoredReferenceFirst() { DetailClassification detailClassification = createDetailClassification(10L, 100L, "백엔드", "Java/Spring"); JobPosting topScoredPosting = JobPosting.create( + TEST_USER, Company.create("선택 기업", CompanySize.MEDIUM), detailClassification, "회사 기반 주요 업무", @@ -183,6 +189,7 @@ void generateMockJobPostingUsesTopScoredReferenceFirst() { "회사 기반 우대 사항" ); JobPosting lowerPriorityPosting = JobPosting.create( + TEST_USER, Company.create("다른 기업", CompanySize.MEDIUM), detailClassification, "직무 기반 주요 업무", 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 6a4593a..3d280ef 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 @@ -70,7 +70,7 @@ class MockApplyServiceTest { @DisplayName("기존 공고를 기준으로 ACTUAL 타입 모의 서류 지원을 생성한다") void createActualApply() { User user = saveUser("actual-apply@example.com"); - JobPosting jobPosting = saveJobPosting("백엔드 개발"); + JobPosting jobPosting = saveJobPosting(user, "백엔드 개발"); MockApplyCreateResponse response = mockApplyService.createActualApply(user, jobPosting.getId()); @@ -123,11 +123,27 @@ void createMockApply() { assertThat(jobPosting.getPreferred()).isEqualTo("React 경험 우대"); } + @Test + @DisplayName("저장된 공고를 기준으로 MOCK 타입 모의 서류 지원을 생성한다") + void createMockApplyFromJobPosting() { + User user = saveUser("mock-from-job-posting@example.com"); + JobPosting jobPosting = saveJobPosting(user, "백엔드 개발"); + + MockApplyCreateResponse response = mockApplyService.createMockApplyFromJobPosting(user, jobPosting.getId()); + + MockApply mockApply = mockApplyRepository.findById(response.mockApplyId()).orElseThrow(); + assertThat(response.jobPostingId()).isEqualTo(jobPosting.getId()); + assertThat(response.applyType()).isEqualTo(ApplyType.MOCK); + assertThat(mockApply.getUser().getId()).isEqualTo(user.getId()); + assertThat(mockApply.getJobPosting().getId()).isEqualTo(jobPosting.getId()); + assertThat(mockApply.getApplyType()).isEqualTo(ApplyType.MOCK); + } + @Test @DisplayName("mockApplyId로 생성된 모의 공고를 조회한다") void getMockApplyJobPosting() { User user = saveUser("mock-job-posting@example.com"); - JobPosting jobPosting = saveJobPosting("백엔드 개발"); + JobPosting jobPosting = saveJobPosting(user, "백엔드 개발"); MockApply mockApply = mockApplyRepository.save(MockApply.create(user, jobPosting, ApplyType.MOCK)); var response = mockApplyService.getMockApplyJobPosting(user, mockApply.getId()); @@ -147,6 +163,19 @@ void createActualApplyThrowsWhenJobPostingNotFound() { .isEqualTo(GeneralErrorCode.JOB_POSTING_NOT_FOUND); } + @Test + @DisplayName("다른 사용자의 공고로 MOCK 타입 지원 생성 시 예외를 던진다") + void createMockApplyFromJobPostingThrowsWhenForbidden() { + User owner = saveUser("mock-owner@example.com"); + User otherUser = saveUser("mock-other@example.com"); + JobPosting jobPosting = saveJobPosting(owner, "프론트엔드 개발"); + + assertThatThrownBy(() -> mockApplyService.createMockApplyFromJobPosting(otherUser, jobPosting.getId())) + .isInstanceOf(GeneralException.class) + .extracting("code") + .isEqualTo(GeneralErrorCode.FORBIDDEN); + } + @Test @DisplayName("존재하지 않는 소분류 ID로 MOCK 타입 지원 생성 시 예외를 던진다") void createMockApplyThrowsWhenDetailClassificationNotFound() { @@ -201,7 +230,7 @@ void createMockApplyThrowsWhenMiddleClassificationMismatched() { void getMockApplyJobPostingThrowsWhenForbidden() { User owner = saveUser("owner@example.com"); User otherUser = saveUser("other@example.com"); - JobPosting jobPosting = saveJobPosting("데이터 분석"); + JobPosting jobPosting = saveJobPosting(owner, "데이터 분석"); MockApply mockApply = mockApplyRepository.save(MockApply.create(owner, jobPosting, ApplyType.MOCK)); assertThatThrownBy(() -> mockApplyService.getMockApplyJobPosting(otherUser, mockApply.getId())) @@ -214,10 +243,11 @@ private User saveUser(String email) { return userRepository.save(User.signup("테스트 사용자", email, "encoded-password")); } - private JobPosting saveJobPosting(String detailName) { + private JobPosting saveJobPosting(User user, String detailName) { Company company = companyRepository.save(Company.create("테스트 기업", CompanySize.MEDIUM)); DetailClassification detailClassification = saveDetailClassification(detailName); return jobPostingRepository.save(JobPosting.create( + user, company, detailClassification, "주요 업무",