Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ public class Company {
private String name;

@Enumerated(EnumType.STRING)
@Column(nullable = false)
private CompanySize size;

@Builder.Default
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@
public record JobPostingCreateRequest(
@NotBlank(message = "회사명은 필수입니다.")
String companyName,

@NotNull(message = "회사 규모는 필수입니다.")
CompanySize companySize,

@NotNull(message = "소분류 ID는 필수입니다.")
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@
public record JobPostingGenerateRequest(
@NotBlank(message = "회사명은 필수입니다.")
String companyName,

@NotNull(message = "회사 규모는 필수입니다.")
CompanySize companySize,

@NotNull(message = "소분류 ID는 필수입니다.")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,5 @@ public class JobPostingIngestCommand {
private byte[] imageBytes;
private String imageContentType;
private CompanySize companySize;
private String tone;
private Integer candidateLimit;
}
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ public record JobPostingUpdateRequest(
@NotBlank(message = "회사명은 필수입니다.")
String companyName,

@NotNull(message = "회사 규모는 필수입니다.")
CompanySize companySize,

@NotNull(message = "소분류 ID는 필수입니다.")
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -69,8 +65,7 @@ public JobPostingGenerateResponse generateJobPosting(JobPostingGenerateRequest r
try {
StructuredResponse<JobPostingGenerateResponse> 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);
Expand All @@ -93,16 +88,15 @@ 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);
}
}

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) {
Expand Down Expand Up @@ -136,8 +130,7 @@ public JobPostingExtractResponse extractJobPosting(String rawText, byte[] imageB
try {
StructuredResponse<JobPostingExtractResponse> 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);
Expand Down Expand Up @@ -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
);
}
Expand All @@ -270,7 +263,7 @@ private <T> T extractStructuredContent(StructuredResponse<T> response, Class<T>
.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,
Expand Down Expand Up @@ -325,41 +318,30 @@ 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,
"AI 응답이 비어 있습니다."
);
}

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) {
Expand Down Expand Up @@ -439,32 +421,30 @@ 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,
"AI 생성 응답이 비어 있습니다."
);
}

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<JobPostingClassificationCandidateResponse> candidates
) {
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand Down
Loading
Loading