/** * Cloudflare Pages Functions: Gemini API Proxy * 위치: /functions/api/analyze.js * * 개선 사항: * 1. JSON 추출 로직 강화: AI가 마크다운 기호를 섞어 보내도 정규식을 통해 JSON만 정확히 뽑아냅니다. * 2. 입력 데이터 검증: JD나 이력서가 비어있을 경우에 대한 예외 처리를 추가했습니다. * 3. 응답 안정성: AI에게 JSON 스키마를 더 명확히 전달하여 파싱 에러를 방지합니다. * 4. 한글 인코딩 보장: Response 헤더에 charset=UTF-8을 명시했습니다. */ export async function onRequestPost(context) { try { // 1. 환경 변수 로드 const apiKey = context.env.GEMINI_API_KEY; if (!apiKey) { return new Response(JSON.stringify({ error: "CONFIG_ERROR", message: "Cloudflare 환경 변수(GEMINI_API_KEY)가 설정되지 않았습니다. 대시보드 설정을 확인하세요." }), { status: 500, headers: { "Content-Type": "application/json;charset=UTF-8" } }); } // 2. 데이터 수신 및 유효성 검사 const body = await context.request.json().catch(() => ({})); const { jd, resume, level, customCriteria } = body; if (!jd || !resume) { return new Response(JSON.stringify({ error: "INVALID_INPUT", message: "직무 기술서(JD)와 이력서 데이터가 모두 필요합니다." }), { status: 400, headers: { "Content-Type": "application/json;charset=UTF-8" } }); } // 3. 결정론적 채점을 위한 강화된 시스템 프롬프트 const systemPrompt = `당신은 세계 최고 수준의 IT 기업 CHRO입니다. 당신의 임무는 제공된 직무 기술서(JD)와 지원자 이력서를 비교하여 "객관적이고 수학적인" 적합도 분석 리포트를 작성하는 것입니다. [반드시 준수해야 할 출력 규칙] 1. 모든 응답은 반드시 JSON 형식이어야 합니다. 텍스트 설명이나 인사말을 포함하지 마십시오. 2. 결과의 일관성: 동일한 입력에 대해 항상 동일한 점수와 내용을 반환하십시오. 3. 점수 산정: 90점 이상(Excellent), 70-89점(Good), 0-69점(Poor) 기준을 엄격히 적용하십시오. 4. 신뢰도 평가: 경력 연차는 신뢰도 점수에 영향을 주지 않습니다. 내용의 구체성과 데이터 기반 증거 유무만으로 판단하십시오. [출력 JSON 구조] { "candidate_name": "문자열 (이력서에서 추출)", "overall_score": 숫자 (0-100), "factors": { "hard_skills": 숫자, "experience": 숫자, "soft_skills": 숫자, "problem_solving": 숫자, "culture_fit": 숫자 }, "reliability": { "overall_score": 숫자, "summary": "문자열", "green_flags": ["문자열"], "red_flags": ["문자열"], "verification_questions": ["문자열"] }, "strengths": ["문자열"], "weaknesses": ["문자열"], "interview_questions": ["문자열"], "factor_details": { "hard_skills": "문자열", "experience": "문자열", "soft_skills": "문자열", "problem_solving": "문자열", "culture_fit": "문자열" } }`; // 4. Gemini API 호출 (안정적인 1.5-flash 모델 사용) const geminiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=${apiKey}`; const geminiResponse = await fetch(geminiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ contents: [{ parts: [{ text: `[채용 정보]\n직급: ${level || "미지정"}\n추가 기준: ${customCriteria || "없음"}\n\n[직무 기술서 (JD)]\n${jd}\n\n[지원자 이력서]\n${resume}` }] }], systemInstruction: { parts: [{ text: systemPrompt }] }, generationConfig: { responseMimeType: "application/json", temperature: 0, topK: 1, topP: 0 } }) }); if (!geminiResponse.ok) { const errData = await geminiResponse.json().catch(() => ({})); return new Response(JSON.stringify({ error: "AI_API_ERROR", message: errData.error?.message || "AI 서비스가 응답하지 않습니다." }), { status: geminiResponse.status, headers: { "Content-Type": "application/json;charset=UTF-8" } }); } const resJson = await geminiResponse.json(); let aiText = resJson.candidates?.[0]?.content?.parts?.[0]?.text || "{}"; // 5. 강력한 JSON 추출 로직 (Regex 사용) // AI가 간혹 마크다운 코드 블록 등을 섞어서 보내는 경우를 대비하여 { } 사이의 내용만 추출합니다. let cleanedJson; try { const jsonStart = aiText.indexOf('{'); const jsonEnd = aiText.lastIndexOf('}'); if (jsonStart !== -1 && jsonEnd !== -1) { aiText = aiText.substring(jsonStart, jsonEnd + 1); } cleanedJson = JSON.parse(aiText); } catch (parseError) { console.error("AI Response Parsing Error:", aiText); return new Response(JSON.stringify({ error: "PARSE_ERROR", message: "AI의 응답 데이터 형식이 올바르지 않습니다. 다시 시도해 주세요." }), { status: 500, headers: { "Content-Type": "application/json;charset=UTF-8" } }); } // 6. 결과 반환 return new Response(JSON.stringify({ data: cleanedJson, usage: resJson.usageMetadata }), { headers: { "Content-Type": "application/json;charset=UTF-8" } }); } catch (err) { return new Response(JSON.stringify({ error: "SERVER_ERROR", message: "분석 과정에서 예상치 못한 오류가 발생했습니다.", details: err.message }), { status: 500, headers: { "Content-Type": "application/json;charset=UTF-8" } }); } }