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
@@ -0,0 +1,37 @@
package com.jobdri.jobdri_api.domain.analysis.controller;

import com.jobdri.jobdri_api.domain.analysis.dto.request.AnalysisRetrievalPreviewRequest;
import com.jobdri.jobdri_api.domain.analysis.dto.response.AnalysisRetrievalPreviewResponse;
import com.jobdri.jobdri_api.domain.analysis.service.AnalysisAdminDebugService;
import com.jobdri.jobdri_api.global.apiPayload.ApiResponse;
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.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/admin/analysis")
@Tag(name = "AnalysisAdmin", description = "관리자용 자소서 분석 디버그 API")
public class AnalysisAdminController {

private final AnalysisAdminDebugService analysisAdminDebugService;

@Operation(
summary = "분석 retrieval 미리보기",
description = "mockApplyId를 기준으로 실제 분석 전에 조회되는 유사 JD/문항 검색 결과를 반환합니다."
)
@PostMapping("/retrieval-preview")
public ApiResponse<AnalysisRetrievalPreviewResponse> previewRetrieval(
@Valid @RequestBody AnalysisRetrievalPreviewRequest request
) {
return ApiResponse.onSuccess(
"분석 retrieval 미리보기에 성공했습니다.",
analysisAdminDebugService.previewRetrieval(request.mockApplyId())
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.jobdri.jobdri_api.domain.analysis.dto.request;

import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;

public record AnalysisRetrievalPreviewRequest(
@NotNull(message = "mockApplyId는 필수입니다.")
@Positive(message = "mockApplyId는 1 이상이어야 합니다.")
Long mockApplyId
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.jobdri.jobdri_api.domain.analysis.dto.response;

import java.util.List;

public record AnalysisRetrievalPreviewResponse(
Long mockApplyId,
JobPostingSnapshot jobPosting,
List<QuestionSnapshot> questions,
List<JobPostingReference> similarJobPostings,
List<QuestionReference> similarQuestions
) {
public record JobPostingSnapshot(
Long jobPostingId,
String companyName,
String detailClassificationName,
String task,
String requirement,
String preferred
) {
}

public record QuestionSnapshot(
Long questionId,
String content,
String answer
) {
}

public record JobPostingReference(
Long corpusId,
String companyName,
String roleName,
String responsibilities,
String requirements,
String preferred,
double distance
) {
}

public record QuestionReference(
Long corpusId,
String companyName,
String roleName,
String questionType,
Integer charLimit,
String questionText,
double distance
) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package com.jobdri.jobdri_api.domain.analysis.service;

import com.jobdri.jobdri_api.domain.analysis.dto.response.AnalysisRetrievalPreviewResponse;
import com.jobdri.jobdri_api.domain.analysis.entity.Question;
import com.jobdri.jobdri_api.domain.analysis.repository.QuestionRepository;
import com.jobdri.jobdri_api.domain.analysis.service.AnalysisReferenceRetrievalService.AnalysisReferenceContext;
import com.jobdri.jobdri_api.domain.jobposting.entity.JobPosting;
import com.jobdri.jobdri_api.domain.mockapply.entity.MockApply;
import com.jobdri.jobdri_api.domain.mockapply.repository.MockApplyRepository;
import com.jobdri.jobdri_api.global.apiPayload.code.GeneralErrorCode;
import com.jobdri.jobdri_api.global.apiPayload.exception.GeneralException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class AnalysisAdminDebugService {

private final MockApplyRepository mockApplyRepository;
private final QuestionRepository questionRepository;
private final AnalysisReferenceRetrievalService analysisReferenceRetrievalService;

public AnalysisRetrievalPreviewResponse previewRetrieval(Long mockApplyId) {
MockApply mockApply = mockApplyRepository.findByIdWithJobPosting(mockApplyId)
.orElseThrow(() -> new GeneralException(
GeneralErrorCode.MOCK_APPLY_NOT_FOUND,
"해당 모의 서류 지원을 찾을 수 없습니다. mockApplyId=" + mockApplyId
));
List<Question> questions = questionRepository.findAllByMockApplyIdOrderByIdAsc(mockApplyId);
JobPosting jobPosting = mockApply.getJobPosting();

AnalysisReferenceContext referenceContext =
analysisReferenceRetrievalService.retrieve(jobPosting, questions);

return new AnalysisRetrievalPreviewResponse(
mockApply.getId(),
new AnalysisRetrievalPreviewResponse.JobPostingSnapshot(
jobPosting.getId(),
jobPosting.getCompany().getName(),
jobPosting.getDetailClassification().getDetailName(),
jobPosting.getTask(),
jobPosting.getRequirement(),
jobPosting.getPreferred()
),
questions.stream()
.map(question -> new AnalysisRetrievalPreviewResponse.QuestionSnapshot(
question.getId(),
question.getContent(),
question.getAnswer()
))
.toList(),
referenceContext.jobPostingReferences().stream()
.map(reference -> new AnalysisRetrievalPreviewResponse.JobPostingReference(
reference.corpusId(),
reference.companyName(),
reference.roleName(),
reference.responsibilities(),
reference.requirements(),
reference.preferred(),
reference.distance()
))
.toList(),
referenceContext.questionReferences().stream()
.map(reference -> new AnalysisRetrievalPreviewResponse.QuestionReference(
reference.corpusId(),
reference.companyName(),
reference.roleName(),
reference.questionType(),
reference.charLimit(),
reference.questionText(),
reference.distance()
))
.toList()
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
import com.jobdri.jobdri_api.domain.analysis.dto.llm.AnalysisLlmResponse;
import com.jobdri.jobdri_api.domain.analysis.entity.Question;
import com.jobdri.jobdri_api.domain.jobposting.entity.JobPosting;
import com.jobdri.jobdri_api.domain.analysis.service.AnalysisReferenceRetrievalService.AnalysisReferenceContext;
import com.jobdri.jobdri_api.domain.analysis.service.AnalysisReferenceRetrievalService.RetrievedJobPostingReference;
import com.jobdri.jobdri_api.domain.analysis.service.AnalysisReferenceRetrievalService.RetrievedQuestionReference;
import com.jobdri.jobdri_api.global.apiPayload.code.GeneralErrorCode;
import com.jobdri.jobdri_api.global.apiPayload.exception.GeneralException;
import com.openai.client.OpenAIClient;
Expand All @@ -22,14 +25,16 @@
public class AnalysisAiClient {

private final OpenAIClient openAIClient;
private final AnalysisReferenceRetrievalService analysisReferenceRetrievalService;

@Value("${openai.model.cover-letter-analysis:gpt-4o-mini}")
private String analysisModel;

public AnalysisLlmResponse analyze(JobPosting jobPosting, List<Question> questions) {
AnalysisReferenceContext referenceContext = analysisReferenceRetrievalService.retrieve(jobPosting, questions);
var params = ResponseCreateParams.builder()
.model(analysisModel)
.input(buildPrompt(jobPosting, questions))
.input(buildPrompt(jobPosting, questions, referenceContext))
.temperature(0.2)
.text(AnalysisLlmResponse.class)
.build();
Expand All @@ -48,7 +53,11 @@ public AnalysisLlmResponse analyze(JobPosting jobPosting, List<Question> questio
}
}

private String buildPrompt(JobPosting jobPosting, List<Question> questions) {
private String buildPrompt(
JobPosting jobPosting,
List<Question> questions,
AnalysisReferenceContext referenceContext
) {
String questionText = questions.stream()
.map(question -> """
- questionId: %d
Expand All @@ -61,6 +70,9 @@ private String buildPrompt(JobPosting jobPosting, List<Question> questions) {
))
.reduce("", (left, right) -> left + "\n" + right);

String similarJobPostingText = formatJobPostingReferences(referenceContext.jobPostingReferences());
String similarQuestionText = formatQuestionReferences(referenceContext.questionReferences());

return """
[시스템 지시]
너는 한국 채용 담당자이자 자기소개서 평가 전문가다.
Expand Down Expand Up @@ -126,6 +138,12 @@ private String buildPrompt(JobPosting jobPosting, List<Question> questions) {
우대 사항:
%s

[유사 JD 검색 결과]
%s

[유사 자소서 문항 검색 결과]
%s

[자소서 문항과 답변]
%s

Expand All @@ -148,10 +166,60 @@ private String buildPrompt(JobPosting jobPosting, List<Question> questions) {
defaultString(jobPosting.getTask()),
defaultString(jobPosting.getRequirement()),
defaultString(jobPosting.getPreferred()),
similarJobPostingText,
similarQuestionText,
questionText
);
}

private String formatJobPostingReferences(List<RetrievedJobPostingReference> references) {
if (references == null || references.isEmpty()) {
return "없음";
}
return references.stream()
.map(reference -> """
- 회사명: %s
직무명: %s
주요 업무: %s
자격 요건: %s
우대 사항: %s
거리: %.4f
""".formatted(
defaultString(reference.companyName()),
defaultString(reference.roleName()),
defaultString(reference.responsibilities()),
defaultString(reference.requirements()),
defaultString(reference.preferred()),
reference.distance()
))
.reduce("", (left, right) -> left + "\n" + right)
.trim();
}

private String formatQuestionReferences(List<RetrievedQuestionReference> references) {
if (references == null || references.isEmpty()) {
return "없음";
}
return references.stream()
.map(reference -> """
- 회사명: %s
직무명: %s
문항 유형: %s
글자 수 제한: %s
문항: %s
거리: %.4f
""".formatted(
defaultString(reference.companyName()),
defaultString(reference.roleName()),
defaultString(reference.questionType()),
reference.charLimit() == null ? "" : reference.charLimit(),
defaultString(reference.questionText()),
reference.distance()
))
.reduce("", (left, right) -> left + "\n" + right)
.trim();
}

private AnalysisLlmResponse extractStructuredContent(StructuredResponse<AnalysisLlmResponse> response) {
return response.output().stream()
.filter(item -> item.message().isPresent())
Expand Down
Loading
Loading