diff --git a/src/main/java/com/jobdri/jobdri_api/domain/analysis/dto/criteria/JobCategoryEvaluationCriteria.java b/src/main/java/com/jobdri/jobdri_api/domain/analysis/dto/criteria/JobCategoryEvaluationCriteria.java new file mode 100644 index 0000000..15733c5 --- /dev/null +++ b/src/main/java/com/jobdri/jobdri_api/domain/analysis/dto/criteria/JobCategoryEvaluationCriteria.java @@ -0,0 +1,16 @@ +package com.jobdri.jobdri_api.domain.analysis.dto.criteria; + +import java.util.List; + +public record JobCategoryEvaluationCriteria( + String jobCategoryMiddle, + List includedJobs, + List competencyTags, + List coreCompetencies, + List relatedActions, + List relatedExperiences, + List relatedKeywords, + String goodEvidenceExample, + List missingKeywordExamples +) { +} diff --git a/src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisAiClient.java b/src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisAiClient.java index d58015a..ec6dbca 100644 --- a/src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisAiClient.java +++ b/src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisAiClient.java @@ -1,6 +1,7 @@ package com.jobdri.jobdri_api.domain.analysis.service; import com.jobdri.jobdri_api.domain.analysis.dto.llm.AnalysisLlmResponse; +import com.jobdri.jobdri_api.domain.analysis.dto.criteria.JobCategoryEvaluationCriteria; import com.jobdri.jobdri_api.domain.analysis.entity.Question; import com.jobdri.jobdri_api.domain.corpus.service.CorpusRetrievalService; import com.jobdri.jobdri_api.domain.corpus.service.CorpusRetrievalService.RetrievalContext; @@ -28,6 +29,7 @@ public class AnalysisAiClient { private static final int MAX_REFERENCE_SECTION_LENGTH = 3000; private static final int MAX_REFERENCE_FIELD_LENGTH = 300; + private static final int MAX_CRITERIA_ITEMS = 5; private static final String OUTPUT_SCHEMA = """ [출력 형식] { @@ -177,10 +179,18 @@ public class AnalysisAiClient { private String analysisModel; public AnalysisLlmResponse analyze(AnalysisExecutionPayload payload) { - return analyze(payload.jobPosting(), payload.answeredQuestions()); + return analyze(payload.jobPosting(), payload.answeredQuestions(), payload.jobCategoryEvaluationCriteria()); } public AnalysisLlmResponse analyze(JobPosting jobPosting, List questions) { + return analyze(jobPosting, questions, null); + } + + public AnalysisLlmResponse analyze( + JobPosting jobPosting, + List questions, + JobCategoryEvaluationCriteria jobCategoryEvaluationCriteria + ) { RetrievalContext referenceContext = emptyContext(); try { referenceContext = corpusRetrievalService.retrieveForAnalysis(jobPosting, questions); @@ -190,7 +200,7 @@ public AnalysisLlmResponse analyze(JobPosting jobPosting, List questio } var params = ResponseCreateParams.builder() .model(analysisModel) - .input(buildPrompt(jobPosting, questions, referenceContext)) + .input(buildPrompt(jobPosting, questions, referenceContext, jobCategoryEvaluationCriteria)) .temperature(0.2) .text(AnalysisLlmResponse.class) .build(); @@ -212,10 +222,11 @@ public AnalysisLlmResponse analyze(JobPosting jobPosting, List questio } } - private String buildPrompt( + String buildPrompt( JobPosting jobPosting, List questions, - RetrievalContext referenceContext + RetrievalContext referenceContext, + JobCategoryEvaluationCriteria jobCategoryEvaluationCriteria ) { String questionText = questions.stream() .map(question -> """ @@ -231,6 +242,7 @@ private String buildPrompt( String similarJobPostingText = formatJobPostingReferences(referenceContext.jobPostingReferences()); String similarQuestionText = formatQuestionReferences(referenceContext.questionReferences()); + String jobCategoryCriteriaSection = formatJobCategoryEvaluationCriteriaSection(jobCategoryEvaluationCriteria); return """ [시스템 지시] @@ -264,6 +276,8 @@ private String buildPrompt( [유사 자소서 문항 검색 결과] %s + %s + [자소서 문항과 답변] %s @@ -291,10 +305,54 @@ private String buildPrompt( defaultString(jobPosting.getPreferred()), similarJobPostingText, similarQuestionText, + jobCategoryCriteriaSection, questionText ); } + private String formatJobCategoryEvaluationCriteriaSection(JobCategoryEvaluationCriteria criteria) { + if (criteria == null) { + return ""; + } + + return """ + [직무별 보조 평가 기준] + 중분류: %s + 주의: + - 이 직무별 기준은 실제 JD를 대체하지 않는다. + - 실제 JD의 자격요건, 우대사항, 주요업무를 우선한다. + - 직무별 기준은 JD가 모호하거나 암묵 역량 판단이 필요할 때만 보조적으로 참고한다. + - missingKeywords는 실제 JD 표현을 우선 사용하고, 직무별 missingKeywordExamples는 문구 정리와 유사 키워드 묶기에만 참고한다. + - 직무별 기준에 있는 키워드가 자소서에 없다는 이유만으로 무조건 missing 처리하지 않는다. + - 원문에 없는 수치, 도구, 경험을 만들어내지 않는다. + 핵심 역량: %s + 관련 행동: %s + 관련 키워드: %s + 좋은 근거 예시: %s + 누락 키워드 문구 예시: %s + """.formatted( + defaultString(criteria.jobCategoryMiddle()), + formatCriteriaList(criteria.coreCompetencies()), + formatCriteriaList(criteria.relatedActions()), + formatCriteriaList(criteria.relatedKeywords()), + truncate(defaultString(criteria.goodEvidenceExample()), MAX_REFERENCE_FIELD_LENGTH), + formatCriteriaList(criteria.missingKeywordExamples()) + ).trim(); + } + + private String formatCriteriaList(List values) { + if (values == null || values.isEmpty()) { + return "없음"; + } + String formatted = values.stream() + .filter(value -> value != null && !value.isBlank()) + .limit(MAX_CRITERIA_ITEMS) + .map(value -> "- " + truncate(value.trim(), MAX_REFERENCE_FIELD_LENGTH)) + .reduce("", (left, right) -> left + "\n" + right) + .trim(); + return formatted.isBlank() ? "없음" : "\n" + formatted; + } + private String formatJobPostingReferences(List references) { if (references == null || references.isEmpty()) { return "없음"; diff --git a/src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisExecutionPayload.java b/src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisExecutionPayload.java index 078c996..73c3383 100644 --- a/src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisExecutionPayload.java +++ b/src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisExecutionPayload.java @@ -1,5 +1,6 @@ package com.jobdri.jobdri_api.domain.analysis.service; +import com.jobdri.jobdri_api.domain.analysis.dto.criteria.JobCategoryEvaluationCriteria; import com.jobdri.jobdri_api.domain.analysis.entity.Question; import com.jobdri.jobdri_api.domain.jobposting.entity.JobPosting; @@ -11,6 +12,16 @@ public record AnalysisExecutionPayload( Long mockApplyId, JobPosting jobPosting, List questions, - List answeredQuestions + List answeredQuestions, + JobCategoryEvaluationCriteria jobCategoryEvaluationCriteria ) { + public AnalysisExecutionPayload( + Long userId, + Long mockApplyId, + JobPosting jobPosting, + List questions, + List answeredQuestions + ) { + this(userId, mockApplyId, jobPosting, questions, answeredQuestions, null); + } } diff --git a/src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisService.java b/src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisService.java index 01b7650..926d21c 100644 --- a/src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisService.java +++ b/src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisService.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; +import com.jobdri.jobdri_api.domain.analysis.dto.criteria.JobCategoryEvaluationCriteria; import com.jobdri.jobdri_api.domain.analysis.dto.llm.AnalysisLlmResponse; import com.jobdri.jobdri_api.domain.analysis.dto.response.AnalysisQuestionResponse; import com.jobdri.jobdri_api.domain.analysis.dto.response.AnalysisResponse; @@ -68,6 +69,7 @@ public class AnalysisService { private final AnalysisAiClient analysisAiClient; private final CreditService creditService; private final ObjectMapper objectMapper; + private final JobCategoryEvaluationCriteriaProvider jobCategoryEvaluationCriteriaProvider; @Transactional @AuditLogEvent(action = "ANALYSIS_RUN", targetType = "MOCK_APPLY", targetId = "#arg1") @@ -130,18 +132,29 @@ public AnalysisExecutionPayload prepareAnalysisExecution(User user, Long mockApp // Initialize hierarchy before leaving the read transaction so detached payload can be used safely. mockApply.getJobPosting().getDetailClassification().getMiddleClassification().getMiddleName(); mockApply.getJobPosting().getDetailClassification().getMiddleClassification().getClassification().getBigName(); + JobCategoryEvaluationCriteria evaluationCriteria = jobCategoryEvaluationCriteriaProvider + .findByMiddleName(mockApply.getJobPosting().getDetailClassification().getMiddleClassification().getMiddleName()) + .orElse(null); return new AnalysisExecutionPayload( user.getId(), mockApplyId, mockApply.getJobPosting(), List.copyOf(questions), - List.copyOf(answeredQuestions) + List.copyOf(answeredQuestions), + evaluationCriteria ); } public AnalysisLlmResponse executeAnalysis(AnalysisExecutionPayload payload) { - return analysisAiClient.analyze(payload.jobPosting(), payload.answeredQuestions()); + if (payload.jobCategoryEvaluationCriteria() == null) { + return analysisAiClient.analyze(payload.jobPosting(), payload.answeredQuestions()); + } + return analysisAiClient.analyze( + payload.jobPosting(), + payload.answeredQuestions(), + payload.jobCategoryEvaluationCriteria() + ); } @Transactional diff --git a/src/main/java/com/jobdri/jobdri_api/domain/analysis/service/JobCategoryEvaluationCriteriaProvider.java b/src/main/java/com/jobdri/jobdri_api/domain/analysis/service/JobCategoryEvaluationCriteriaProvider.java new file mode 100644 index 0000000..3c8998c --- /dev/null +++ b/src/main/java/com/jobdri/jobdri_api/domain/analysis/service/JobCategoryEvaluationCriteriaProvider.java @@ -0,0 +1,71 @@ +package com.jobdri.jobdri_api.domain.analysis.service; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.jobdri.jobdri_api.domain.analysis.dto.criteria.JobCategoryEvaluationCriteria; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.io.ClassPathResource; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +@Component +@Slf4j +public class JobCategoryEvaluationCriteriaProvider { + private static final String RESOURCE_PATH = "analysis/job-category-evaluation-criteria.json"; + private static final TypeReference> CRITERIA_TYPE = new TypeReference<>() { + }; + + private final Map criteriaByMiddleName; + + public JobCategoryEvaluationCriteriaProvider(ObjectMapper objectMapper) { + this.criteriaByMiddleName = loadCriteria(objectMapper); + log.info("Loaded job category evaluation criteria. count={}", criteriaByMiddleName.size()); + } + + public Optional findByMiddleName(String middleName) { + if (!StringUtils.hasText(middleName)) { + return Optional.empty(); + } + return Optional.ofNullable(criteriaByMiddleName.get(normalizeKey(middleName))); + } + + private Map loadCriteria(ObjectMapper objectMapper) { + ClassPathResource resource = new ClassPathResource(RESOURCE_PATH); + try (InputStream inputStream = resource.getInputStream()) { + List criteria = objectMapper.readValue(inputStream, CRITERIA_TYPE); + Map result = new LinkedHashMap<>(); + + for (JobCategoryEvaluationCriteria item : criteria) { + if (item == null || !StringUtils.hasText(item.jobCategoryMiddle())) { + log.warn("Skipped invalid job category evaluation criteria row. reason=blank_middle_name"); + continue; + } + + String key = normalizeKey(item.jobCategoryMiddle()); + if (result.containsKey(key)) { + throw new IllegalStateException("중복된 직무 중분류 평가 기준입니다. middleName=" + item.jobCategoryMiddle()); + } + result.put(key, item); + } + + return Map.copyOf(result); + } catch (IOException e) { + throw new IllegalStateException("직무 중분류 평가 기준 resource 로딩에 실패했습니다. path=" + RESOURCE_PATH, e); + } + } + + private String normalizeKey(String value) { + return value.trim() + .replace('ㆍ', '·') + .replace('・', '·') + .replace('/', '·') + .replaceAll("\\s+", ""); + } +} diff --git a/src/main/resources/analysis/job-category-evaluation-criteria.json b/src/main/resources/analysis/job-category-evaluation-criteria.json new file mode 100644 index 0000000..68b85b4 --- /dev/null +++ b/src/main/resources/analysis/job-category-evaluation-criteria.json @@ -0,0 +1,1027 @@ +[ + { + "jobCategoryMiddle": "엔지니어링·설계", + "includedJobs": [ + "공정개발", + "공정기술", + "기계설계", + "레이더 장비 설치 및 시운전", + "반도체 공정 개발", + "반도체 공정 엔지니어", + "반도체 공정기술", + "반도체공정기술", + "생산기술", + "설계", + "설비기술", + "양산기술", + "연구개발", + "장비 엔지니어", + "재료개발", + "전기기술", + "차량 시스템 개발", + "차량 아키텍처 개발", + "회로설계" + ], + "competencyTags": [ + "설계검증", + "공정최적화", + "도면해독", + "기술적문제해결", + "협업" + ], + "coreCompetencies": [ + "기계/회로/공정 설계 및 도면 분석", + "성능 시뮬레이션 및 설계 타당성 검증" + ], + "relatedActions": [ + "설계 요구 사양서 검토 및 사양에 부합하는 구조/회로 설계", + "시뮬레이션 툴을 활용한 오차 범위 최소화 및 설계 최적화" + ], + "relatedExperiences": [ + "신제품 및 신기술 설계 프로젝트 참여 경험", + "시뮬레이션 툴 활용 및 프로토타입 설계 검증 경험" + ], + "relatedKeywords": [ + "CAD", + "3D 모델링", + "시뮬레이션", + "도면 분석", + "회로/구조 설계", + "설계 최적화" + ], + "goodEvidenceExample": "설계 사양 검토 과정에서 잠재적 오류 리스크를 식별하고, 가상 시뮬레이션을 통해 구조적 취약점이나 신호 왜곡을 보완하여 설계 완성도를 높인 경험", + "missingKeywordExamples": [ + "설계 타당성 검증 경험", + "시뮬레이션 기반 설계 최적화", + "도면 분석 및 오류 식별 경험", + "공정 최적화 경험", + "CAD/3D 모델링 활용 경험" + ] + }, + { + "jobCategoryMiddle": "제조·생산", + "includedJobs": [ + "공정기술", + "생산", + "생산관리", + "생산기술", + "생산운영", + "생산직", + "설비관리", + "안전관리", + "양산기술", + "오퍼레이터", + "제조", + "제조직", + "품질관리", + "품질보증", + "플랜트 운영" + ], + "competencyTags": [ + "공정관리", + "수율개선", + "생산계획", + "안전관리", + "현장협업" + ], + "coreCompetencies": [ + "생산 공정 관리 및 수율 개선", + "제조 설비 가동 최적화 및 유지보수" + ], + "relatedActions": [ + "공정 주요 변수 모니터링 및 표준 작업 기준(SOP) 수립", + "가동 정지 리스크 방지를 위한 예방 정비 및 트러블슈팅" + ], + "relatedExperiences": [ + "양산 공정 운영 및 수율 향상 개선 활동 경험", + "현장 생산 설비 정기 점검 및 고장 대응 경험" + ], + "relatedKeywords": [ + "생산 공정 관리", + "수율 향상", + "설비 보전", + "표준운영절차(SOP)", + "트러블슈팅" + ], + "goodEvidenceExample": "양산 단계에서 발생한 공정 병목 또는 설비 알람 데이터를 분석하여, 작업 방식을 개선하거나 설비 파라미터를 최적화해 생산 수율과 안정성을 개선한 경험", + "missingKeywordExamples": [ + "공정 수율 개선 경험", + "표준작업지침(SOP) 수립 경험", + "설비 예방정비 조치 경험", + "공정 병목 원인 분석", + "생산 모니터링 체계 구축" + ] + }, + { + "jobCategoryMiddle": "기획·전략", + "includedJobs": [ + "ESG 관리", + "경영기획", + "경영지원", + "기획", + "사업개발", + "사업관리", + "사업기획", + "상품기획", + "서비스 기획", + "운영컨설팅", + "인턴", + "일반관리", + "전략기획", + "전시기획", + "카드상품기획", + "콘텐츠 기획" + ], + "competencyTags": [ + "문제정의", + "데이터기반의사결정", + "일정관리", + "커뮤니케이션", + "전략수립" + ], + "coreCompetencies": [ + "비즈니스 모델 기획 및 중장기 전략 수립", + "성과 지표(KPI) 설정 및 프로세스 혁신" + ], + "relatedActions": [ + "시장 트렌드 및 사내 운영 지표 분석을 통한 기획안 도출", + "다부서 이해관계 조율을 통한 프로젝트 관리(PM)" + ], + "relatedExperiences": [ + "신규 사업 타당성 검토 및 상품 기획 참여 경험", + "부서별 성과 관리 지표 설계 및 모니터링 경험" + ], + "relatedKeywords": [ + "사업 기획", + "지표 분석(KPI)", + "리소스 배분", + "이해관계자 조율", + "타당성 분석" + ], + "goodEvidenceExample": "사용자 지표 및 시장 조사를 기반으로 비즈니스 개선 기회를 포착하고, 세부 실행 계획 및 일정표를 조율하여 신규 서비스 출시를 총괄 기획한 경험", + "missingKeywordExamples": [ + "비즈니스 모델 기획 경험", + "성과 지표(KPI) 설계 경험", + "시장 트렌드 분석 경험", + "다부서 리소스 조율 경험", + "사업 타당성 검토 경험" + ] + }, + { + "jobCategoryMiddle": "의료·바이오", + "includedJobs": [ + "R&D", + "간호사", + "공정 엔지니어", + "공정엔지니어", + "미생물 연구", + "분석연구", + "소재개발", + "연구개발", + "연구원", + "임상병리사", + "품질관리", + "품질보증" + ], + "competencyTags": [ + "실험설계", + "데이터해석", + "분석연구", + "GMP준수", + "논리적분석" + ], + "coreCompetencies": [ + "미생물/생물학/의약품 연구 분석 및 시험 검증", + "임상·검체 데이터 관리와 GMP/GCP 등 규정 준수" + ], + "relatedActions": [ + "분석 장비를 활용한 시료/검체 분석 및 데이터 해석", + "임상/제조 현장에서 요구되는 절차와 문서 기준 준수 확인" + ], + "relatedExperiences": [ + "신소재/원료 분석 및 실험법 개발 프로젝트 경험", + "품질 검증을 위한 HPLC/GC 기기 분석, 검체 관리 또는 임상 데이터 기록 경험" + ], + "relatedKeywords": [ + "기기 분석", + "실험 설계", + "물성 해석", + "검체 관리", + "임상 데이터", + "GMP/GCP 준수", + "분석법 검증" + ], + "goodEvidenceExample": "실험실 또는 임상 현장에서 시료·검체 분석 데이터를 기반으로 유효성을 검증하고, 규정에 맞는 문서화와 품질 확인 절차를 수행하여 시험 과정의 신뢰성과 정밀도를 입증한 경험", + "missingKeywordExamples": [ + "실험 설계 및 수행 경험", + "시험 데이터 해석 경험", + "기기 분석 활용 경험", + "검체 관리 및 임상 기록 경험", + "GMP/GCP 규정 준수 경험" + ] + }, + { + "jobCategoryMiddle": "AI·개발·데이터", + "includedJobs": [ + "HW 개발", + "데이터 분석", + "백엔드 개발", + "서버 개발", + "소프트웨어 개발", + "시스템 운영", + "응용 개발", + "정보보안", + "제품개발", + "플랫폼 개발" + ], + "competencyTags": [ + "트러블슈팅", + "아키텍처설계", + "데이터분석", + "가설검증", + "논리적사고" + ], + "coreCompetencies": [ + "소프트웨어 아키텍처 설계 및 시스템 개발", + "데이터 수집/가공 및 분석 모델링" + ], + "relatedActions": [ + "요구사항 분석을 통한 효율적인 개발 및 리팩토링 수행", + "데이터 기반의 문제 진단 및 알고리즘을 통한 성능 최적화" + ], + "relatedExperiences": [ + "신규 서비스/플랫폼 개발 및 운영 프로젝트 경험", + "정형/비정형 데이터 정제 및 비즈니스 인사이트 도출 경험" + ], + "relatedKeywords": [ + "소프트웨어 아키텍처", + "데이터베이스 설계", + "API", + "데이터 모델링", + "최적화", + "트러블슈팅" + ], + "goodEvidenceExample": "데이터 처리 프로세스나 시스템 구조 상의 병목을 진단하고, 적합한 프레임워크나 최적화 기법을 도입하여 처리 속도 및 안정성을 향상시킨 경험", + "missingKeywordExamples": [ + "아키텍처 설계 및 리팩토링", + "데이터 파이프라인 구축 경험", + "API 설계 및 고도화", + "쿼리 실행 계획 최적화", + "시스템 트러블슈팅 경험" + ] + }, + { + "jobCategoryMiddle": "마케팅·광고·MD", + "includedJobs": [ + "MD", + "디지털 마케팅", + "마케팅", + "브랜드 마케팅", + "상품기획", + "언론홍보", + "온라인 마케팅", + "인플루언서 마케팅", + "전략마케팅", + "제품 마케팅", + "콘텐츠 기획", + "콘텐츠 마케팅", + "트렌드 마케팅", + "홍보" + ], + "competencyTags": [ + "성과측정", + "고객분석", + "시장조사", + "채널전략", + "트렌드분석" + ], + "coreCompetencies": [ + "디지털 광고 캠페인 설계 및 효율(ROAS) 분석", + "브랜드 비주얼 아이덴티티 수립 및 상품 소싱(MD)" + ], + "relatedActions": [ + "잠재 고객 타깃 세그먼트 분류 및 최적의 채널 광고 집행", + "브랜드 일관성을 유지하는 비주얼 가이드라인 작성 및 관리" + ], + "relatedExperiences": [ + "온라인 광고 캠페인 기획 및 퍼널별 전환율 개선 경험", + "브랜드 런칭/리뉴얼 및 신제품 MD 소싱 기획 경험" + ], + "relatedKeywords": [ + "ROAS 최적화", + "캠페인 관리", + "타깃 고객", + "상품 기획(MD)", + "브랜드 전략" + ], + "goodEvidenceExample": "고객 행동 데이터를 분석하여 전환율이 이탈하는 지점을 정의하고, 고객 맞춤형 프로모션 기획 및 광고 타겟팅 정교화를 통해 마케팅 성과(ROAS)를 향상시킨 경험", + "missingKeywordExamples": [ + "광고 캠페인 최적화 경험", + "ROAS 데이터 분석", + "브랜드 비주얼 가이드 수립", + "퍼널별 이탈률 전환 분석", + "마케팅 콘텐츠 기획" + ] + }, + { + "jobCategoryMiddle": "영업", + "includedJobs": [ + "기술지원", + "사무영업", + "영업", + "영업관리", + "영업마케팅", + "해외영업" + ], + "competencyTags": [ + "고객발굴", + "단가협상", + "수주리드", + "파트너십", + "목표달성" + ], + "coreCompetencies": [ + "신규 영업 기회 발굴 및 고객사 관리", + "영업 계약 체결 및 단가 협상(Negotiation)" + ], + "relatedActions": [ + "고객사별 요구 사양 파악 및 맞춤형 제안서 작성", + "리드 타임 및 매출 목표 관리, 거래처 이견 조율 및 수주 성공" + ], + "relatedExperiences": [ + "B2B/B2C 영업 전략 수립 및 파이프라인 관리 경험", + "국내외 바이어 비즈니스 매칭 및 수출/영업 계약 경험" + ], + "relatedKeywords": [ + "영업 제안", + "고객사 발굴", + "단가 협상", + "수주 리드", + "매출 목표 관리" + ], + "goodEvidenceExample": "고객사 원가 구조 및 구매 성향을 면밀히 파악하여 설득력 있는 맞춤형 제안과 협상을 이끌어내고, 이를 통해 장기 영업 계약 체결에 성공한 경험", + "missingKeywordExamples": [ + "신규 바이어 개척 경험", + "고객사 관리 및 네고 전략", + "영업 계약 단가 협상", + "매출 목표 파이프라인 관리", + "고객 요구사항 조율" + ] + }, + { + "jobCategoryMiddle": "법무·사무·총무", + "includedJobs": [ + "경영사무", + "비서", + "사무보조", + "사무지원", + "사무직원", + "송무직원", + "신입행원", + "일반직", + "행정", + "행정지원" + ], + "competencyTags": [ + "사무행정", + "문서표준화", + "계약검토", + "운영효율화", + "꼼꼼함" + ], + "coreCompetencies": [ + "사내 일반 행정 및 스케줄링 조율", + "계약 검토 지원 및 문서 표준화 관리" + ], + "relatedActions": [ + "우선순위 중심의 업무 스케줄 제어 및 기밀 사항 보안 통제", + "사내 표준 양식 가이드 제공 및 기획 보고 문서화" + ], + "relatedExperiences": [ + "회의록 작성 및 이사회 등 경영진 의전 행정 지원 경험", + "부서 비용 정산 총괄 및 계약서 초안 법무 검토 지원 경험" + ], + "relatedKeywords": [ + "사무 행정", + "문서 표준화", + "기밀 보안", + "일정 관리", + "전표 정산" + ], + "goodEvidenceExample": "부서별 기획 문서 및 보고 양식이 혼재해 있던 프로세스를 단일 표준 템플릿으로 통합하고, 비용 전표 처리 단계를 간소화하여 행정 소요 시간을 단축한 경험", + "missingKeywordExamples": [ + "사내 일반 행정 지원", + "회의 일정 및 스케줄 조율", + "계약서 검토 및 표준화", + "사내 자산 관리 경험", + "행정 업무 지원" + ] + }, + { + "jobCategoryMiddle": "인사·HR", + "includedJobs": [ + "HR전략수립", + "경영지원", + "인사", + "인사 운영", + "인사관리", + "인사운영", + "인사제도 기획", + "인턴" + ], + "competencyTags": [ + "인재확보", + "조직문화", + "평가보상", + "교육기획", + "인사운영" + ], + "coreCompetencies": [ + "채용 파이프라인 구축 및 인재 소싱", + "임직원 온보딩 및 교육 프로그램(HRD) 설계" + ], + "relatedActions": [ + "직무 분석을 통한 세부 요구 명세(JD) 정의", + "조직 문화 적합도 지표 정의 및 조직 진단 모니터링" + ], + "relatedExperiences": [ + "다이렉트 소싱을 통한 우수 인재 채용 프로젝트 리드 경험", + "사내 핵심 가치 전파 및 직급별 역량 교육 설계 경험" + ], + "relatedKeywords": [ + "채용 전략(TA)", + "직무 분석", + "조직 문화", + "HRD 교육", + "온보딩" + ], + "goodEvidenceExample": "사내 핵심 부서의 퇴사율 요인을 시계열 데이터로 분석하여 조직 문화적 갈등 포인트를 정의하고, 온보딩 교육 개선 및 직무 매칭 제도를 개선하여 고용 유지율을 높인 경험", + "missingKeywordExamples": [ + "채용 파이프라인 소싱 경험", + "온보딩 프로그램 기획", + "인사 제도 및 규정 정립", + "임직원 교육 과정 설계", + "조직 문화 진단 및 개선" + ] + }, + { + "jobCategoryMiddle": "금융·보험", + "includedJobs": [ + "(미분류)", + "금융상담", + "기업분석", + "신입행원", + "은행업무 지원", + "자산관리" + ], + "competencyTags": [ + "리스크관리", + "금융상품설계", + "고객상담", + "시장분석", + "수리분석" + ], + "coreCompetencies": [ + "여신/수신 금융 서비스 및 상품 제안", + "금융 리스크 스크리닝 및 투자 가치 평가(Valuation)" + ], + "relatedActions": [ + "기업 및 개인 재무 안정성 수리 분석 및 여신 적합성 판정", + "시장 거시 경제 지표 분석 리포트 작성" + ], + "relatedExperiences": [ + "금융 영업 지원 및 은행 업무 수검 준비 경험", + "기업 신용 분석 및 포트폴리오 리스크 평가 프로젝트 참여 경험" + ], + "relatedKeywords": [ + "금융 상품", + "리스크 심사", + "신용 분석", + "거시 경제 리서치", + "가치 평가" + ], + "goodEvidenceExample": "고객사 재무제표 상의 이상 지표와 산업 동향을 입체적으로 다각 분석하여, 여신 지원 여부를 결정하기 위한 정교한 리스크 진단 리포트를 작성한 경험", + "missingKeywordExamples": [ + "금융 상품 제안 경험", + "투자 타당성 리스크 평가", + "여신 수신 자산 심사", + "자금 조달 및 집행 관리", + "금융 데이터 트렌드 분석" + ] + }, + { + "jobCategoryMiddle": "물류·무역", + "includedJobs": [ + "구매", + "물류 운영", + "물류관리", + "물류운영", + "수출입통관", + "협력업체 관리" + ], + "competencyTags": [ + "공급망최적화", + "재고분석", + "수출입통관", + "운송조율", + "비용절감" + ], + "coreCompetencies": [ + "공급망(SCM) 최적화 및 수요 예측", + "무역 서류(CI, PL, B/L 등) 검토 및 관세 통관 조율" + ], + "relatedActions": [ + "적정 안전 재고 일수 산정 및 창고 공간 보관 효율화", + "통관 지연 이슈 발생 시 선사 및 관세사와 긴급 소통/중재" + ], + "relatedExperiences": [ + "글로벌 포워딩 및 복합 운송 관리 업무 경험", + "재고 분석 기법 적용을 통한 적정 재고 유지 및 SCM 안정화 경험" + ], + "relatedKeywords": [ + "SCM", + "재고 효율화", + "안전 재고", + "무역 서류", + "통관 지연 대응", + "운송 라우팅" + ], + "goodEvidenceExample": "수출입 물류 과정에서 발생한 항만 지연 이슈에 대응하기 위해 신속히 대체 운송 라우팅을 확보하고, 관세 증빙 서류를 정밀 보완해 최종 납기 지연을 방지한 경험", + "missingKeywordExamples": [ + "재고 관리 및 분석 경험", + "수출입 서류 검토 경험", + "통관 지연 대응 경험", + "납기 관리 및 조율 경험", + "물류비 절감 활동 경험" + ] + }, + { + "jobCategoryMiddle": "디자인", + "includedJobs": [ + "UI/UX 디자인", + "UX/UI 디자인", + "VMD", + "공간디자인", + "디자인", + "크리에이티브 디렉터 인턴" + ], + "competencyTags": [ + "시각화", + "사용자중심", + "컨셉도출", + "UIUX개선", + "그래픽설계" + ], + "coreCompetencies": [ + "브랜드 비주얼 아이덴티티(BI/CI) 수립", + "시각/공간/UX/UI 디자인 설계 및 3D 모델링" + ], + "relatedActions": [ + "고유 소구점(USP) 분석을 반영한 그래픽 모티프 및 디자인 콘셉트 도출", + "그래픽 툴을 활용한 정밀 설계 및 고해상도 렌더링 검증" + ], + "relatedExperiences": [ + "신규 브랜드 디자인 런칭 및 브랜드 리뉴얼 비주얼 가이드 수립 경험", + "평면 도면 기반의 3D 공간 레이아웃 모델링 참여 경험" + ], + "relatedKeywords": [ + "BI/CI", + "비주얼 커뮤니케이션", + "UX/UI", + "그래픽 툴(Photoshop, Illustrator)", + "3D 모델링" + ], + "goodEvidenceExample": "제품의 핵심 콘셉트를 분석하여 디자인 방향성을 정의하고, 온·오프라인 매체에 일관되게 적용할 수 있는 비주얼 가이드라인을 수립해 브랜드 완성도를 높인 경험", + "missingKeywordExamples": [ + "브랜드 BI/CI 수립 경험", + "공간 레이아웃 디자인", + "UX/UI 인터페이스 설계", + "디자인 가이드라인 정립", + "3D 모델링 비주얼 시각화" + ] + }, + { + "jobCategoryMiddle": "회계·세무", + "includedJobs": [ + "세무사무", + "재무회계", + "회계" + ], + "competencyTags": [ + "결산관리", + "재무제표분석", + "세무신고", + "자금계획", + "내부통제" + ], + "coreCompetencies": [ + "재무제표 작성 및 월/분기 손익 결산 통제", + "법인세/부가가치세 신고 및 내부회계관리" + ], + "relatedActions": [ + "전표 승인 프로세스 준수 및 기간별 비용 배부 오류 정정", + "회계 데이터 검증 매크로 작성을 통한 데이터 정합성 검토" + ], + "relatedExperiences": [ + "SAP FI/CO 등 ERP 회계 모듈 활용 및 결산 수검 경험", + "자금 수지 계획 수립 및 외부 감사 수검 준비 경험" + ], + "relatedKeywords": [ + "재무 회계", + "연결 결산", + "세무 조정", + "SAP FI/CO", + "자금 관리" + ], + "goodEvidenceExample": "다국적 법인 간 복잡한 전표 전산 처리 과정에서 발생한 거래 계정 오류의 원인을 추적하여 해결하고, ERP 회계 마스터를 정비하여 결산 오류 리스크를 낮춘 경험", + "missingKeywordExamples": [ + "월/분기 결산 실무 경험", + "재무제표 작성 경험", + "세무 신고 실무 경험", + "ERP 회계 모듈 활용", + "K-IFRS 기준 이해" + ] + }, + { + "jobCategoryMiddle": "미디어·문화·스포츠", + "includedJobs": [ + "공연기획", + "드라마 프로듀서", + "방송PD", + "예능PD", + "콘텐츠 모니터링 및 데이터 관리" + ], + "competencyTags": [ + "콘텐츠기획", + "제작일정조율", + "트렌드분석", + "리소스관리", + "협업" + ], + "coreCompetencies": [ + "방송/드라마/공연 콘텐츠 제작 기획", + "제작 타임라인 설계 및 외부 이해관계 조율(PM)" + ], + "relatedActions": [ + "트렌드 리서치를 바탕으로 한 크리에이티브 콘셉트 및 스토리보드 기획", + "제작 비용 및 외주 리소스 일정 준수 모니터링" + ], + "relatedExperiences": [ + "영상 콘텐츠 편집/촬영 및 편집 프로세스 참여 경험", + "무대 공간 구성 및 문화 행사 프로젝트 PM 경험" + ], + "relatedKeywords": [ + "콘텐츠 기획", + "스토리보드", + "제작 일정 조율", + "리소스 통제", + "편집/촬영" + ], + "goodEvidenceExample": "프로젝트 총괄 PM으로서 초기 시나리오/콘셉트 기획 단계부터 최종 아웃풋 편집 및 현장 연출 조율까지의 타임라인을 관리하여 콘텐츠의 퀄리티를 유지한 경험", + "missingKeywordExamples": [ + "콘텐츠 제작 기획 경험", + "제작 타임라인 마일스톤 관리", + "외부 에이전시 커뮤니케이션", + "미디어 트렌드 리서치", + "프로젝트 리소스 조율" + ] + }, + { + "jobCategoryMiddle": "건축·시설", + "includedJobs": [ + "건축", + "건축 시공", + "건축시공", + "토목공사관리" + ], + "competencyTags": [ + "도면분석", + "시공관리", + "공정관리", + "안전준수", + "현장조율" + ], + "coreCompetencies": [ + "건축/토목 시공 관리 및 감리 업무", + "안전 법령 준수 및 현장 시설물 제어" + ], + "relatedActions": [ + "공정표 기준의 작업 진척률 모니터링 및 협력사 관리", + "안전 수칙 위반 요인 교정 및 비상 대피 체계 수립" + ], + "relatedExperiences": [ + "건설/시공 현장 관리 및 자재 검수 참여 경험", + "시설 정기 점검 및 안전 수칙 준수 검증 경험" + ], + "relatedKeywords": [ + "시공 관리", + "공정표", + "자재 검수", + "현장 감리", + "안전 관리 지침", + "시설 점검" + ], + "goodEvidenceExample": "현장 시공 중 발생한 공정 지연 리스크를 조기에 식별하고, 공정표 조율 및 협력사와의 안전한 시공 방안 수정을 통해 기한 내 시공을 정상 완료한 경험", + "missingKeywordExamples": [ + "건축 시공 관리 경험", + "공사 현장 감리 업무", + "안전 법령 준수 상태 점검", + "설계 도면 정밀 검토" + ] + }, + { + "jobCategoryMiddle": "공공·복지", + "includedJobs": [ + "공공외교", + "공공행정", + "보안검색", + "사회복지사", + "항공보안", + "환경관리" + ], + "competencyTags": [ + "사례관리", + "지역사회연계", + "복지프로그램", + "행정지원", + "공감능력" + ], + "coreCompetencies": [ + "공공 행정 서비스 지원 및 복지 상담", + "복지 프로그램 기획 및 지역사회 자원 연계" + ], + "relatedActions": [ + "사회적 취약 지표 정의 및 수혜 대상자 선별을 위한 심층 면담", + "행정 법령 준수 확인 및 수작업 행정 프로세스 간소화" + ], + "relatedExperiences": [ + "복지 수혜 대상 사례 관리 및 맞춤형 연계 경험", + "정부 과제 행정 지원 및 대외 이해관계자 소통 경험" + ], + "relatedKeywords": [ + "사례 관리", + "수혜자 발굴", + "행정 지원", + "컴플라이언스", + "연계 협력" + ], + "goodEvidenceExample": "지역사회 내 취약계층의 복지 사각지대 해소를 위해 정성적 심층 면담을 수행하고, 적합한 민간·공공 지원 프로세스를 연계하여 자립을 도운 경험", + "missingKeywordExamples": [ + "공공 행정 서비스 지원", + "복지 프로그램 기획", + "지역사회 자원 조율", + "민원 상담 및 현장 대응", + "복지 정책 가이드라인 분석" + ] + }, + { + "jobCategoryMiddle": "고객서비스·리테일", + "includedJobs": [ + "객실승무원", + "고객서비스", + "프론트" + ], + "competencyTags": [ + "매장운영", + "고객경험", + "VOC분석", + "서비스품질", + "소통능력" + ], + "coreCompetencies": [ + "매장 운영 지표 관리 및 서비스 품질 제어", + "고객 경험(CX) 향상 및 VOC(고객의소리) 개선" + ], + "relatedActions": [ + "고객 만족도를 높이는 서비스 표준화 가이드라인 정립", + "불만 고객 응대 및 분쟁 요인의 신속한 조율 및 정상화" + ], + "relatedExperiences": [ + "오프라인 매장 리테일 운영 및 스태프 관리 경험", + "고객 불만 접수(VOC) 통계 분석 및 불만 예방 프로세스 도입 경험" + ], + "relatedKeywords": [ + "매장 운영 관리", + "고객 만족도(CS)", + "VOC 개선", + "서비스 가이드라인" + ], + "goodEvidenceExample": "매장 운영 중 빈번히 발생하는 불만 접수(VOC) 유형을 분석하여 서비스 절차상의 문제점을 도출하고, 응대 매뉴얼 보완을 통해 서비스 만족도를 개선한 경험", + "missingKeywordExamples": [ + "매장 운영 지표 관리", + "서비스 품질(QSC) 제어", + "고객 경험(CX) 향상 활동", + "리테일 매출 트렌드 분석", + "매장 재고 및 발주 관리" + ] + }, + { + "jobCategoryMiddle": "운전·운송·배송", + "includedJobs": [ + "선박 승무원", + "여객운송", + "운송총괄" + ], + "competencyTags": [ + "배송최적화", + "안전운행", + "운송일정관리", + "장비점검", + "성실성" + ], + "coreCompetencies": [ + "화물 및 여객 운송 일정 관리 및 배송 경로 최적화", + "차량/장비 안전 수칙 준수 및 정기 예방 점검" + ], + "relatedActions": [ + "실시간 물동량 및 교통 요인 반영 배송 일정 조율", + "배송 적재 기준 준수 및 현장 기기 안전 진단" + ], + "relatedExperiences": [ + "안전 무사고 운송 운영 및 배송 경로 맵핑 경험", + "운송 원장 실사 매칭 및 현장 배송 조율 경험" + ], + "relatedKeywords": [ + "배송 경로", + "안전 운행", + "일정 관리", + "차량 점검", + "성실성" + ], + "goodEvidenceExample": "화물 배송 최적화 프로그램을 도입하여 일일 물동량과 차량별 가동 상황을 조율하고, 최적화 경로를 재배치해 배송 완료율을 높이고 유류비를 절감한 경험", + "missingKeywordExamples": [ + "운송 경로 최적화 경험", + "화물 운송 일정 관리", + "차량 장비 안전 점검", + "물류 배송 리드타임 단축", + "물류 안전 규정 준수" + ] + }, + { + "jobCategoryMiddle": "교육", + "includedJobs": [ + "교육 기획", + "소프트웨어 교육", + "연구원" + ], + "competencyTags": [ + "교육과정설계", + "교수요목작성", + "교육효과평가", + "LMS운영", + "소통역량" + ], + "coreCompetencies": [ + "교육 과정 설계 및 강의 커리큘럼 개발", + "교육 효과 평가 분석 및 학습 관리 시스템(LMS) 운영" + ], + "relatedActions": [ + "교육 대상자 요구도 분석 및 타겟팅된 교수 매체 선정", + "교육 콘텐츠 기획안 작성 및 강의 스크립트 표준화" + ], + "relatedExperiences": [ + "연령별/대상별 교육 프로그램 강사 및 운영 경험", + "이러닝 강좌 설계 및 사후 만족도 지표 피드백 반영 경험" + ], + "relatedKeywords": [ + "교육 과정 설계", + "커리큘럼 개발", + "학습 진단", + "교육 만족도", + "LMS 운영" + ], + "goodEvidenceExample": "교육 대상자의 역량 격차 요인을 사전 진단하여 적합한 난이도별 맞춤형 교육 커리큘럼을 설계하고, 학습 몰입도를 높이기 위한 교수 매체를 적용해 교육 만족도를 향상시킨 경험", + "missingKeywordExamples": [ + "교육 과정 설계 및 기획", + "강의 커리큘럼 개발", + "교육 효과성 평가 분석", + "학습자 요구사항 진단", + "온/오프라인 교육 운영" + ] + }, + { + "jobCategoryMiddle": "고객상담·TM", + "includedJobs": [ + "고객상담", + "기술지원", + "사무영업" + ], + "competencyTags": [ + "고객응대", + "불만해결", + "상담품질", + "VOC분석", + "경청과공감" + ], + "coreCompetencies": [ + "고객 문의 대응 및 1차 고객 접점 상담 관리", + "상담 품질 관리 및 지식베이스(FAQ) 표준화" + ], + "relatedActions": [ + "고객 니즈에 부합하는 경청과 공감을 기반으로 신속하게 응대", + "반복적인 고객 요구 사항 파악을 통한 대시보드 피드백 제공" + ], + "relatedExperiences": [ + "고객 콜센터/채팅 상담 운영 및 상담 템플릿 제작 경험", + "악성 고객 민원 조율 및 상담 가동 지표 관리 경험" + ], + "relatedKeywords": [ + "고객 상담", + "상담 이력 관리", + "VOC 공유", + "경청과 공감", + "안내 템플릿" + ], + "goodEvidenceExample": "반복되는 특정 고객 문의 유형을 식별하고, 직관적으로 설명할 수 있는 안내 스크립트 템플릿을 개발하여 상담 소요 시간을 단축하고 만족도를 향상시킨 경험", + "missingKeywordExamples": [ + "고객 문의 상담 대응", + "상담 품질(QA) 모니터링", + "고객 클레임 조율 해결", + "상담 스크립트 개선 경험", + "고객 만족도(CSAT) 관리" + ] + }, + { + "jobCategoryMiddle": "식·음료", + "includedJobs": [ + "식품안전", + "품질관리", + "품질위생" + ], + "competencyTags": [ + "품질위생", + "식품안전", + "공정관리", + "메뉴기획", + "원가관리" + ], + "coreCompetencies": [ + "식음료 제조 공정 및 품질위생 관리", + "위생 법령 컴플라이언스 체크 및 위해 요소 차단" + ], + "relatedActions": [ + "현장 가공 설비 위생 점검 및 원료 유통 기한 이력 관리", + "불량 원료 선별 프로세스 준수 및 폐기 조치 통제" + ], + "relatedExperiences": [ + "해썹(HACCP) 인증 준비 및 식품안전 위생 점검 경험", + "식품 원가 분석 및 생산 수율 관리 경험" + ], + "relatedKeywords": [ + "HACCP", + "위생 관리", + "식품안전", + "유통 이력", + "원가 통제" + ], + "goodEvidenceExample": "식음료 가공 공정의 오염 방지를 위해 위해 요소를 분석하고 위생 관리 기준을 개선한 뒤, 현장 작업자에게 위생 수칙을 교육하여 안전성을 확보한 경험", + "missingKeywordExamples": [ + "식음료 제조 공정 관리", + "품질 위생 점검 수행", + "위생 법령 컴플라이언스", + "원부자재 이력 관리", + "HACCP 기준 준수 검증" + ] + }, + { + "jobCategoryMiddle": "QA·품질관리", + "includedJobs": [ + "품질관리" + ], + "competencyTags": [ + "품질보증", + "부적합관리", + "CAPA", + "품질경영시스템", + "규격인증" + ], + "coreCompetencies": [ + "품질 경영 시스템(QMS) 구축 및 관리", + "부적합품 처리 및 시정/예방조치(CAPA)" + ], + "relatedActions": [ + "원자재 및 완제품의 품질 규격 적합성 검사 및 분석", + "부적합/일탈 발생 시 원인 추적(5-Why) 및 개선안 수립" + ], + "relatedExperiences": [ + "품질 감사 및 해외 규격(ISO 등) 인증 획득/유지 경험", + "품질 일탈 사고 대응 및 시정 조치 프로세스 리드 경험" + ], + "relatedKeywords": [ + "QMS", + "품질 검사", + "일탈 분석", + "CAPA", + "ISO 인증", + "품질 신뢰성" + ], + "goodEvidenceExample": "공정 내 제품 불량률 상승 원인을 추적하여 근본 원인을 분석하고, 부적합 조치 프로세스(CAPA)를 수립하여 표준화 지침을 제정해 재발률을 낮춘 경험", + "missingKeywordExamples": [ + "품질 기준 적합성 검사", + "불량 원인 분석 경험", + "시정조치(CAPA) 수행", + "품질 문서/SOP 관리", + "ISO 등 인증 대응 경험" + ] + } +] diff --git a/src/test/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisAiClientTest.java b/src/test/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisAiClientTest.java new file mode 100644 index 0000000..3e5208d --- /dev/null +++ b/src/test/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisAiClientTest.java @@ -0,0 +1,90 @@ +package com.jobdri.jobdri_api.domain.analysis.service; + +import com.jobdri.jobdri_api.domain.analysis.dto.criteria.JobCategoryEvaluationCriteria; +import com.jobdri.jobdri_api.domain.analysis.entity.Question; +import com.jobdri.jobdri_api.domain.company.entity.Company; +import com.jobdri.jobdri_api.domain.corpus.service.CorpusRetrievalService; +import com.jobdri.jobdri_api.domain.corpus.service.CorpusRetrievalService.RetrievalContext; +import com.jobdri.jobdri_api.domain.jobposting.entity.JobPosting; +import com.jobdri.jobdri_api.global.config.LlmConcurrencyLimiter; +import com.openai.client.OpenAIClient; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class AnalysisAiClientTest { + + private final AnalysisAiClient analysisAiClient = new AnalysisAiClient( + mock(OpenAIClient.class), + mock(CorpusRetrievalService.class), + mock(LlmConcurrencyLimiter.class) + ); + + @Test + @DisplayName("직무 중분류 기준이 있으면 프롬프트에 보조 평가 기준 섹션을 포함한다") + void buildPromptIncludesJobCategoryCriteriaWhenPresent() { + String prompt = analysisAiClient.buildPrompt( + mockJobPosting(), + List.of(mockQuestion()), + new RetrievalContext(List.of(), List.of()), + new JobCategoryEvaluationCriteria( + "AI·개발·데이터", + List.of("백엔드 개발"), + List.of("트러블슈팅"), + List.of("소프트웨어 아키텍처 설계 및 시스템 개발"), + List.of("요구사항 분석을 통한 효율적인 개발 및 리팩토링 수행"), + List.of("신규 서비스/플랫폼 개발 및 운영 프로젝트 경험"), + List.of("API", "데이터베이스 설계"), + "시스템 구조 상의 병목을 진단하고 최적화해 안정성을 향상시킨 경험", + List.of("API 설계 및 고도화", "시스템 트러블슈팅 경험") + ) + ); + + assertThat(prompt).contains("[직무별 보조 평가 기준]"); + assertThat(prompt).contains("중분류: AI·개발·데이터"); + assertThat(prompt).contains("이 직무별 기준은 실제 JD를 대체하지 않는다."); + assertThat(prompt).contains("실제 JD의 자격요건, 우대사항, 주요업무를 우선한다."); + assertThat(prompt).contains("직무별 기준에 있는 키워드가 자소서에 없다는 이유만으로 무조건 missing 처리하지 않는다."); + assertThat(prompt).contains("소프트웨어 아키텍처 설계 및 시스템 개발"); + assertThat(prompt).contains("API 설계 및 고도화"); + } + + @Test + @DisplayName("직무 중분류 기준이 없으면 보조 평가 기준 섹션을 생략한다") + void buildPromptOmitsJobCategoryCriteriaSectionWhenCriteriaMissing() { + String prompt = analysisAiClient.buildPrompt( + mockJobPosting(), + List.of(mockQuestion()), + new RetrievalContext(List.of(), List.of()), + null + ); + + assertThat(prompt).doesNotContain("[직무별 보조 평가 기준]"); + assertThat(prompt).doesNotContain("이 직무별 기준은 실제 JD를 대체하지 않는다."); + } + + private JobPosting mockJobPosting() { + JobPosting jobPosting = mock(JobPosting.class, org.mockito.Answers.RETURNS_DEEP_STUBS); + Company company = mock(Company.class); + when(company.getName()).thenReturn("잡드리"); + when(jobPosting.getCompany()).thenReturn(company); + when(jobPosting.getDetailClassification().getDetailName()).thenReturn("백엔드 개발"); + when(jobPosting.getTask()).thenReturn("API 개발"); + when(jobPosting.getRequirement()).thenReturn("Spring Boot 경험"); + when(jobPosting.getPreferred()).thenReturn("대용량 트래픽 경험"); + return jobPosting; + } + + private Question mockQuestion() { + Question question = mock(Question.class); + when(question.getId()).thenReturn(1L); + when(question.getContent()).thenReturn("직무 경험을 작성해주세요."); + when(question.getAnswer()).thenReturn("Spring Boot API를 개발했습니다."); + return question; + } +} diff --git a/src/test/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisServiceTest.java b/src/test/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisServiceTest.java index 6731d53..1f779c8 100644 --- a/src/test/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisServiceTest.java +++ b/src/test/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisServiceTest.java @@ -458,6 +458,39 @@ void analyzeThrowsWhenNoAnswers() { .isEqualTo(GeneralErrorCode.INVALID_PARAMETER); } + @Test + @DisplayName("분석 실행 payload는 공고 중분류에 맞는 직무별 평가 기준을 포함한다") + void prepareAnalysisExecutionIncludesJobCategoryEvaluationCriteria() { + User user = saveUser("analysis-criteria-payload@example.com"); + JobPosting jobPosting = saveJobPosting( + user, + "AI·개발·데이터", + "백엔드 개발", + "API 개발", + "Spring Boot 경험", + "대용량 트래픽 경험" + ); + MockApply mockApply = mockApplyRepository.save(MockApply.create(user, jobPosting, ApplyType.ACTUAL)); + saveQuestion(mockApply, "지원 직무 경험", "Spring Boot API를 개발했습니다."); + + AnalysisExecutionPayload payload = analysisService.prepareAnalysisExecution(user, mockApply.getId()); + + assertThat(payload.jobCategoryEvaluationCriteria()).isNotNull(); + assertThat(payload.jobCategoryEvaluationCriteria().jobCategoryMiddle()).isEqualTo("AI·개발·데이터"); + } + + @Test + @DisplayName("공고 중분류에 맞는 직무별 평가 기준이 없으면 payload 기준은 null이다") + void prepareAnalysisExecutionKeepsCriteriaNullWhenMiddleNameIsUnknown() { + User user = saveUser("analysis-criteria-missing-payload@example.com"); + MockApply mockApply = saveMockApply(user); + saveQuestion(mockApply, "지원 직무 경험", "Spring Boot API를 개발했습니다."); + + AnalysisExecutionPayload payload = analysisService.prepareAnalysisExecution(user, mockApply.getId()); + + assertThat(payload.jobCategoryEvaluationCriteria()).isNull(); + } + @Test @DisplayName("다른 사용자의 지원서는 분석할 수 없다") void analyzeThrowsWhenUserDoesNotOwnMockApply() { @@ -958,10 +991,34 @@ private JobPosting saveJobPosting(User user, String task, String requirement, St )); } + private JobPosting saveJobPosting( + User user, + String middleName, + String detailName, + String task, + String requirement, + String preferred + ) { + Company company = companyRepository.save(Company.create("분석 테스트 기업", CompanySize.MEDIUM)); + DetailClassification detailClassification = saveDetailClassification(middleName, detailName); + return jobPostingRepository.save(JobPosting.create( + user, + company, + detailClassification, + task, + requirement, + preferred + )); + } + private DetailClassification saveDetailClassification() { + return saveDetailClassification("분석 테스트 중분류", "분석 테스트 소분류"); + } + + private DetailClassification saveDetailClassification(String middleName, String detailName) { Classification classification = Classification.create("분석 테스트 대분류 " + System.nanoTime()); - MiddleClassification middleClassification = classification.addMiddleClassification("분석 테스트 중분류"); - DetailClassification detailClassification = middleClassification.addDetailClassification("분석 테스트 소분류"); + MiddleClassification middleClassification = classification.addMiddleClassification(middleName); + DetailClassification detailClassification = middleClassification.addDetailClassification(detailName); classificationRepository.save(classification); return detailClassificationRepository.findById(detailClassification.getId()).orElseThrow(); } diff --git a/src/test/java/com/jobdri/jobdri_api/domain/analysis/service/JobCategoryEvaluationCriteriaProviderTest.java b/src/test/java/com/jobdri/jobdri_api/domain/analysis/service/JobCategoryEvaluationCriteriaProviderTest.java new file mode 100644 index 0000000..54a6eb4 --- /dev/null +++ b/src/test/java/com/jobdri/jobdri_api/domain/analysis/service/JobCategoryEvaluationCriteriaProviderTest.java @@ -0,0 +1,48 @@ +package com.jobdri.jobdri_api.domain.analysis.service; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.jobdri.jobdri_api.domain.analysis.dto.criteria.JobCategoryEvaluationCriteria; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +class JobCategoryEvaluationCriteriaProviderTest { + + private JobCategoryEvaluationCriteriaProvider provider; + + @BeforeEach + void setUp() { + provider = new JobCategoryEvaluationCriteriaProvider(new ObjectMapper()); + } + + @Test + @DisplayName("직무 중분류 평가 기준 JSON resource를 로드하고 중분류명으로 조회한다") + void findByMiddleName() { + Optional criteria = provider.findByMiddleName("AI·개발·데이터"); + + assertThat(criteria).isPresent(); + assertThat(criteria.get().jobCategoryMiddle()).isEqualTo("AI·개발·데이터"); + assertThat(criteria.get().coreCompetencies()).isNotEmpty(); + assertThat(criteria.get().missingKeywordExamples()).isNotEmpty(); + } + + @Test + @DisplayName("중분류명 매칭 시 앞뒤 공백과 구분자 주변 공백을 정규화한다") + void findByMiddleNameNormalizesWhitespaceAndSeparator() { + Optional criteria = provider.findByMiddleName(" AI / 개발 / 데이터 "); + + assertThat(criteria).isPresent(); + assertThat(criteria.get().jobCategoryMiddle()).isEqualTo("AI·개발·데이터"); + } + + @Test + @DisplayName("없는 중분류명은 Optional.empty를 반환한다") + void findByMiddleNameReturnsEmptyWhenMissing() { + assertThat(provider.findByMiddleName("없는 중분류")).isEmpty(); + assertThat(provider.findByMiddleName(" ")).isEmpty(); + } +}