diff --git a/src/main/java/com/jobdri/jobdri_api/domain/company/entity/Company.java b/src/main/java/com/jobdri/jobdri_api/domain/company/entity/Company.java index 2365b17..ac6c3b9 100644 --- a/src/main/java/com/jobdri/jobdri_api/domain/company/entity/Company.java +++ b/src/main/java/com/jobdri/jobdri_api/domain/company/entity/Company.java @@ -23,7 +23,6 @@ public class Company { private String name; @Enumerated(EnumType.STRING) - @Column(nullable = false) private CompanySize size; @Builder.Default diff --git a/src/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/request/JobPostingCreateRequest.java b/src/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/request/JobPostingCreateRequest.java index 81dc67b..9b1e940 100644 --- a/src/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/request/JobPostingCreateRequest.java +++ b/src/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/request/JobPostingCreateRequest.java @@ -7,8 +7,6 @@ public record JobPostingCreateRequest( @NotBlank(message = "회사명은 필수입니다.") String companyName, - - @NotNull(message = "회사 규모는 필수입니다.") CompanySize companySize, @NotNull(message = "소분류 ID는 필수입니다.") diff --git a/src/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/request/JobPostingExtractMultipartRequest.java b/src/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/request/JobPostingExtractMultipartRequest.java index 82b04ce..04fc410 100644 --- a/src/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/request/JobPostingExtractMultipartRequest.java +++ b/src/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/request/JobPostingExtractMultipartRequest.java @@ -1,14 +1,7 @@ package com.jobdri.jobdri_api.domain.jobposting.dto.request; -import lombok.Getter; -import lombok.Setter; import org.springframework.web.multipart.MultipartFile; -@Getter -@Setter -public class JobPostingExtractMultipartRequest { +public record JobPostingExtractMultipartRequest(String rawText, String sourceUrl, MultipartFile image) { - private String rawText; - private String sourceUrl; - private MultipartFile image; } diff --git a/src/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/request/JobPostingGenerateRequest.java b/src/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/request/JobPostingGenerateRequest.java index 50eba08..bbd510d 100644 --- a/src/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/request/JobPostingGenerateRequest.java +++ b/src/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/request/JobPostingGenerateRequest.java @@ -7,8 +7,6 @@ public record JobPostingGenerateRequest( @NotBlank(message = "회사명은 필수입니다.") String companyName, - - @NotNull(message = "회사 규모는 필수입니다.") CompanySize companySize, @NotNull(message = "소분류 ID는 필수입니다.") 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 ef0cbf6..409041d 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 @@ -13,6 +13,5 @@ public class JobPostingIngestCommand { private byte[] imageBytes; private String imageContentType; private CompanySize companySize; - private String tone; private Integer candidateLimit; } diff --git a/src/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/request/JobPostingIngestMultipartRequest.java b/src/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/request/JobPostingIngestMultipartRequest.java index 6fb81de..ea7d828 100644 --- a/src/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/request/JobPostingIngestMultipartRequest.java +++ b/src/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/request/JobPostingIngestMultipartRequest.java @@ -1,18 +1,9 @@ package com.jobdri.jobdri_api.domain.jobposting.dto.request; import com.jobdri.jobdri_api.domain.company.entity.CompanySize; -import lombok.Getter; -import lombok.Setter; import org.springframework.web.multipart.MultipartFile; -@Getter -@Setter -public class JobPostingIngestMultipartRequest { +public record JobPostingIngestMultipartRequest(String rawText, String sourceUrl, MultipartFile image, + CompanySize companySize, Integer candidateLimit) { - private String rawText; - private String sourceUrl; - private MultipartFile image; - private CompanySize companySize; - private String tone; - private Integer candidateLimit; } diff --git a/src/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/request/JobPostingUpdateRequest.java b/src/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/request/JobPostingUpdateRequest.java index 1402891..20da478 100644 --- a/src/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/request/JobPostingUpdateRequest.java +++ b/src/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/request/JobPostingUpdateRequest.java @@ -8,7 +8,6 @@ public record JobPostingUpdateRequest( @NotBlank(message = "회사명은 필수입니다.") String companyName, - @NotNull(message = "회사 규모는 필수입니다.") CompanySize companySize, @NotNull(message = "소분류 ID는 필수입니다.") diff --git a/src/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/response/JobPostingClassificationResultResponse.java b/src/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/response/JobPostingClassificationResultResponse.java index b05c47e..9625f7b 100644 --- a/src/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/response/JobPostingClassificationResultResponse.java +++ b/src/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/response/JobPostingClassificationResultResponse.java @@ -1,20 +1,7 @@ package com.jobdri.jobdri_api.domain.jobposting.dto.response; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; +public record JobPostingClassificationResultResponse(Long detailClassificationId, String detailClassificationName, + String middleClassificationName, String bigClassificationName, + String reason, double confidence) { -@Getter -@Setter -@NoArgsConstructor -@AllArgsConstructor -public class JobPostingClassificationResultResponse { - - private Long detailClassificationId; - private String detailClassificationName; - private String middleClassificationName; - private String bigClassificationName; - private String reason; - private double confidence; } diff --git a/src/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/response/JobPostingExtractResponse.java b/src/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/response/JobPostingExtractResponse.java index fe3e132..15fad6c 100644 --- a/src/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/response/JobPostingExtractResponse.java +++ b/src/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/response/JobPostingExtractResponse.java @@ -1,21 +1,6 @@ package com.jobdri.jobdri_api.domain.jobposting.dto.response; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; +public record JobPostingExtractResponse(String companyName, String jobTitle, String task, String requirements, + String preferredQualifications, String rawText, double confidence) { -@Getter -@Setter -@NoArgsConstructor -@AllArgsConstructor -public class JobPostingExtractResponse { - - private String companyName; - private String jobTitle; - private String task; - private String requirements; - private String preferredQualifications; - private String rawText; - private double confidence; } diff --git a/src/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/response/JobPostingGenerateResponse.java b/src/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/response/JobPostingGenerateResponse.java index 653e8e8..bea59ca 100644 --- a/src/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/response/JobPostingGenerateResponse.java +++ b/src/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/response/JobPostingGenerateResponse.java @@ -1,20 +1,6 @@ package com.jobdri.jobdri_api.domain.jobposting.dto.response; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; +public record JobPostingGenerateResponse(String companyName, String jobTitle, String task, String requirements, + String preferredQualifications, String summary) { -@Getter -@Setter -@NoArgsConstructor -@AllArgsConstructor -public class JobPostingGenerateResponse { - - private String companyName; - private String jobTitle; - private String task; - private String requirements; - private String preferredQualifications; - private String summary; } diff --git a/src/main/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingAiService.java b/src/main/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingAiService.java index cb26740..b734d8c 100644 --- a/src/main/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingAiService.java +++ b/src/main/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingAiService.java @@ -11,11 +11,7 @@ import com.jobdri.jobdri_api.global.apiPayload.code.GeneralErrorCode; import com.jobdri.jobdri_api.global.apiPayload.exception.GeneralException; import com.openai.client.OpenAIClient; -import com.openai.models.responses.ResponseCreateParams; -import com.openai.models.responses.ResponseInputContent; -import com.openai.models.responses.ResponseInputImage; -import com.openai.models.responses.ResponseInputItem; -import com.openai.models.responses.StructuredResponse; +import com.openai.models.responses.*; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; @@ -69,8 +65,7 @@ public JobPostingGenerateResponse generateJobPosting(JobPostingGenerateRequest r try { StructuredResponse response = openAIClient.responses().create(params); JobPostingGenerateResponse generated = extractStructuredContent(response, JobPostingGenerateResponse.class); - normalizeGeneratedResponse(generated, request); - return generated; + return normalizeGeneratedResponse(generated, request); } catch (Exception e) { log.error("채용 공고 생성 OpenAI API 호출 오류: {}", e.getMessage(), e); return createFallbackGeneratedResponse(request); @@ -93,8 +88,7 @@ public JobPostingClassificationResultResponse classifyDetailClassification( openAIClient.responses().create(params); JobPostingClassificationResultResponse classification = extractStructuredContent(response, JobPostingClassificationResultResponse.class); - normalizeClassificationResponse(classification, candidates); - return classification; + return normalizeClassificationResponse(classification, candidates); } catch (Exception e) { log.error("채용 공고 소분류 분류 OpenAI API 호출 오류: {}", e.getMessage(), e); return fallbackClassification(candidates); @@ -102,7 +96,7 @@ public JobPostingClassificationResultResponse classifyDetailClassification( } public JobPostingExtractResponse extractJobPosting(JobPostingExtractMultipartRequest request) { - return extractJobPosting(request.getRawText(), request.getImage(), request.getSourceUrl()); + return extractJobPosting(request.rawText(), request.image(), request.sourceUrl()); } public JobPostingExtractResponse extractJobPosting(String rawText, byte[] imageBytes, String imageContentType, String sourceUrl) { @@ -136,8 +130,7 @@ public JobPostingExtractResponse extractJobPosting(String rawText, byte[] imageB try { StructuredResponse response = openAIClient.responses().create(params); JobPostingExtractResponse extracted = extractStructuredContent(response, JobPostingExtractResponse.class); - normalizeResponse(extracted, rawText); - return extracted; + return normalizeResponse(extracted, rawText); } catch (Exception e) { log.error("채용 공고 추출 OpenAI API 호출 오류: {}", e.getMessage(), e); return createFallbackResponse(rawText); @@ -240,12 +233,12 @@ private String buildClassificationPrompt( [후보 목록] %s """.formatted( - defaultString(extracted.getCompanyName()), - defaultString(extracted.getJobTitle()), - defaultString(extracted.getTask()), - defaultString(extracted.getRequirements()), - defaultString(extracted.getPreferredQualifications()), - defaultString(extracted.getRawText()), + defaultString(extracted.companyName()), + defaultString(extracted.jobTitle()), + defaultString(extracted.task()), + defaultString(extracted.requirements()), + defaultString(extracted.preferredQualifications()), + defaultString(extracted.rawText()), candidateText ); } @@ -270,7 +263,7 @@ private T extractStructuredContent(StructuredResponse response, Class .filter(item -> item.message().isPresent()) .flatMap(item -> item.asMessage().content().stream()) .filter(content -> content.outputText().isPresent()) - .map(content -> content.asOutputText()) + .map(StructuredResponseOutputMessage.Content::asOutputText) .findFirst() .orElseThrow(() -> new GeneralException( GeneralErrorCode.INTERNAL_SERVER_ERROR, @@ -325,7 +318,7 @@ private byte[] readImageBytes(MultipartFile imageFile) { } } - private void normalizeResponse(JobPostingExtractResponse response, String rawText) { + private JobPostingExtractResponse normalizeResponse(JobPostingExtractResponse response, String rawText) { if (response == null) { throw new GeneralException( GeneralErrorCode.INTERNAL_SERVER_ERROR, @@ -333,33 +326,22 @@ private void normalizeResponse(JobPostingExtractResponse response, String rawTex ); } - if (response.getCompanyName() == null) { - response.setCompanyName(""); - } - if (response.getJobTitle() == null) { - response.setJobTitle(""); - } - if (response.getTask() == null) { - response.setTask(""); - } - if (response.getRequirements() == null) { - response.setRequirements(""); - } - if (response.getPreferredQualifications() == null) { - response.setPreferredQualifications(""); - } - if (response.getRawText() == null || response.getRawText().isBlank()) { - response.setRawText(rawText == null ? "" : rawText); - } - - double confidence = response.getConfidence(); - if (Double.isNaN(confidence) || Double.isInfinite(confidence)) { - response.setConfidence(0.0); - } else if (confidence < 0.0) { - response.setConfidence(0.0); + double confidence = response.confidence(); + if (Double.isNaN(confidence) || Double.isInfinite(confidence) || confidence < 0.0) { + confidence = 0.0; } else if (confidence > 1.0) { - response.setConfidence(1.0); + confidence = 1.0; } + + return new JobPostingExtractResponse( + defaultString(response.companyName()), + defaultString(response.jobTitle()), + defaultString(response.task()), + defaultString(response.requirements()), + defaultString(response.preferredQualifications()), + response.rawText() == null || response.rawText().isBlank() ? defaultString(rawText) : response.rawText(), + confidence + ); } private JobPostingExtractResponse createFallbackResponse(String rawText) { @@ -439,7 +421,7 @@ private String buildGenerationPrompt(JobPostingGenerateRequest request, DetailCl ); } - private void normalizeGeneratedResponse(JobPostingGenerateResponse response, JobPostingGenerateRequest request) { + private JobPostingGenerateResponse normalizeGeneratedResponse(JobPostingGenerateResponse response, JobPostingGenerateRequest request) { if (response == null) { throw new GeneralException( GeneralErrorCode.INTERNAL_SERVER_ERROR, @@ -447,24 +429,22 @@ private void normalizeGeneratedResponse(JobPostingGenerateResponse response, Job ); } - if (response.getCompanyName() == null || response.getCompanyName().isBlank()) { - response.setCompanyName(request.companyName()); - } - if (response.getTask() == null) { - response.setTask(""); - } - if (response.getRequirements() == null) { - response.setRequirements(""); - } - if (response.getPreferredQualifications() == null) { - response.setPreferredQualifications(""); - } - if (response.getSummary() == null) { - response.setSummary(""); + String companyName = response.companyName(); + if (companyName == null || companyName.isBlank()) { + companyName = request.companyName(); } + + return new JobPostingGenerateResponse( + companyName, + defaultString(response.jobTitle()), + defaultString(response.task()), + defaultString(response.requirements()), + defaultString(response.preferredQualifications()), + defaultString(response.summary()) + ); } - private void normalizeClassificationResponse( + private JobPostingClassificationResultResponse normalizeClassificationResponse( JobPostingClassificationResultResponse response, List candidates ) { @@ -476,25 +456,25 @@ private void normalizeClassificationResponse( } JobPostingClassificationCandidateResponse matched = candidates.stream() - .filter(candidate -> candidate.getDetailClassificationId().equals(response.getDetailClassificationId())) + .filter(candidate -> candidate.getDetailClassificationId().equals(response.detailClassificationId())) .findFirst() .orElseGet(() -> candidates.getFirst()); - response.setDetailClassificationId(matched.getDetailClassificationId()); - response.setDetailClassificationName(matched.getDetailClassificationName()); - response.setMiddleClassificationName(matched.getMiddleClassificationName()); - response.setBigClassificationName(matched.getBigClassificationName()); - - if (response.getReason() == null) { - response.setReason(""); - } - - double confidence = response.getConfidence(); + double confidence = response.confidence(); if (Double.isNaN(confidence) || Double.isInfinite(confidence) || confidence < 0.0) { - response.setConfidence(0.0); + confidence = 0.0; } else if (confidence > 1.0) { - response.setConfidence(1.0); + confidence = 1.0; } + + return new JobPostingClassificationResultResponse( + matched.getDetailClassificationId(), + matched.getDetailClassificationName(), + matched.getMiddleClassificationName(), + matched.getBigClassificationName(), + defaultString(response.reason()), + confidence + ); } private JobPostingGenerateResponse createFallbackGeneratedResponse(JobPostingGenerateRequest request) { 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 79141d9..0763afc 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 @@ -45,13 +45,12 @@ public JobPostingAsyncStatusResponse getTask(String taskId) { private JobPostingIngestCommand snapshot(JobPostingIngestMultipartRequest request) { return JobPostingIngestCommand.builder() - .rawText(request.getRawText()) - .sourceUrl(request.getSourceUrl()) - .imageBytes(readBytes(request.getImage())) - .imageContentType(readContentType(request.getImage())) - .companySize(request.getCompanySize()) - .tone(request.getTone()) - .candidateLimit(request.getCandidateLimit()) + .rawText(request.rawText()) + .sourceUrl(request.sourceUrl()) + .imageBytes(readBytes(request.image())) + .imageContentType(readContentType(request.image())) + .companySize(request.companySize()) + .candidateLimit(request.candidateLimit()) .build(); } diff --git a/src/main/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingClassificationService.java b/src/main/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingClassificationService.java index db3fd2f..b48be9c 100644 --- a/src/main/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingClassificationService.java +++ b/src/main/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingClassificationService.java @@ -27,11 +27,11 @@ public List findCandidates(JobPosting private String buildSearchQuery(JobPostingExtractResponse extracted) { return String.join(" ", - normalize(extracted.getJobTitle()), - normalize(extracted.getTask()), - normalize(extracted.getRequirements()), - normalize(extracted.getPreferredQualifications()), - normalize(extracted.getRawText()) + normalize(extracted.jobTitle()), + normalize(extracted.task()), + normalize(extracted.requirements()), + normalize(extracted.preferredQualifications()), + normalize(extracted.rawText()) ).trim(); } 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 41baef8..a580836 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 @@ -33,19 +33,15 @@ public class JobPostingIngestService { public JobPostingIngestResponse ingestAndCreate(JobPostingIngestMultipartRequest request) { JobPostingIngestCommand command = JobPostingIngestCommand.builder() - .rawText(request.getRawText()) - .sourceUrl(request.getSourceUrl()) - .companySize(request.getCompanySize()) - .tone(request.getTone()) - .candidateLimit(request.getCandidateLimit()) + .rawText(request.rawText()) + .sourceUrl(request.sourceUrl()) + .companySize(request.companySize()) + .candidateLimit(request.candidateLimit()) .build(); return ingestAndCreate(command); } public JobPostingIngestResponse ingestAndCreate(JobPostingIngestCommand command) { - if (command.getCompanySize() == null) { - throw new GeneralException(GeneralErrorCode.INVALID_PARAMETER, "회사 규모는 필수입니다."); - } JobPostingExtractResponse extracted = jobPostingAiService.extractJobPosting( command.getRawText(), @@ -68,7 +64,7 @@ public JobPostingIngestResponse ingestAndCreate(JobPostingIngestCommand command) JobPostingClassificationResultResponse classification = jobPostingAiService.classifyDetailClassification(extracted, candidates); - if (classification.getConfidence() < classificationConfidenceThreshold) { + if (classification.confidence() < classificationConfidenceThreshold) { return new JobPostingIngestResponse( false, "소분류 분류 confidence가 낮아 저장을 보류했습니다.", @@ -82,27 +78,27 @@ public JobPostingIngestResponse ingestAndCreate(JobPostingIngestCommand command) JobPostingGenerateResponse generated = jobPostingAiService.generateJobPosting( new JobPostingGenerateRequest( - extracted.getCompanyName(), + extracted.companyName(), command.getCompanySize(), - classification.getDetailClassificationId(), - extracted.getRawText(), + classification.detailClassificationId(), + extracted.rawText(), "", - extracted.getTask(), - extracted.getRequirements(), - extracted.getPreferredQualifications(), - command.getTone(), - extracted.getJobTitle() + extracted.task(), + extracted.requirements(), + extracted.preferredQualifications(), + null, + extracted.jobTitle() ) ); JobPostingResponse saved = jobPostingService.createJobPosting( new JobPostingCreateRequest( - fallbackCompanyName(extracted.getCompanyName()), + fallbackCompanyName(extracted.companyName()), command.getCompanySize(), - classification.getDetailClassificationId(), - generated.getTask(), - generated.getRequirements(), - generated.getPreferredQualifications() + classification.detailClassificationId(), + generated.task(), + generated.requirements(), + generated.preferredQualifications() ) ); 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 cb97864..3eeb5c1 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 @@ -30,7 +30,7 @@ public class MockApplyController { @Operation( summary = "실제 공고 기반 모의 서류 지원 생성", - description = "기존 채용 공고 ID를 기준으로 로그인 사용자의 ACTUAL 타입 모의 서류 지원을 생성합니다." + description = "공고 텍스트/URL 추출, 공고 저장, 사용자 확인 및 수정이 선행된 뒤 저장된 채용 공고 ID를 기준으로 로그인 사용자의 ACTUAL 타입 모의 서류 지원을 생성합니다." ) @ApiResponses(value = { @io.swagger.v3.oas.annotations.responses.ApiResponse(