/** * OpenAI API 유틸리티 */ const OPENAI_API_URL = 'https://api.openai.com/v1/chat/completions' // 모델별 파라미터 설정 const MODEL_CONFIG: Record = { // 최신 모델 (max_completion_tokens 사용) 'gpt-5.1': { maxTokensParam: 'max_completion_tokens', defaultMaxTokens: 4096 }, 'gpt-5': { maxTokensParam: 'max_completion_tokens', defaultMaxTokens: 4096 }, 'gpt-4.1': { maxTokensParam: 'max_completion_tokens', defaultMaxTokens: 4096 }, 'gpt-4.1-mini': { maxTokensParam: 'max_completion_tokens', defaultMaxTokens: 4096 }, 'gpt-4.1-nano': { maxTokensParam: 'max_completion_tokens', defaultMaxTokens: 4096 }, 'o1': { maxTokensParam: 'max_completion_tokens', defaultMaxTokens: 4096 }, 'o1-mini': { maxTokensParam: 'max_completion_tokens', defaultMaxTokens: 4096 }, 'o1-pro': { maxTokensParam: 'max_completion_tokens', defaultMaxTokens: 4096 }, 'o3-mini': { maxTokensParam: 'max_completion_tokens', defaultMaxTokens: 4096 }, // 이전 모델 (max_tokens 사용) 'gpt-4o': { maxTokensParam: 'max_tokens', defaultMaxTokens: 4096 }, 'gpt-4o-mini': { maxTokensParam: 'max_tokens', defaultMaxTokens: 4096 }, 'gpt-4-turbo': { maxTokensParam: 'max_tokens', defaultMaxTokens: 4096 }, 'gpt-4': { maxTokensParam: 'max_tokens', defaultMaxTokens: 4096 }, 'gpt-3.5-turbo': { maxTokensParam: 'max_tokens', defaultMaxTokens: 4096 }, } // 기본 모델 설정 const DEFAULT_MODEL = 'gpt-5.1' function getModelConfig(model: string) { if (MODEL_CONFIG[model]) { return MODEL_CONFIG[model] } for (const key of Object.keys(MODEL_CONFIG)) { if (model.startsWith(key)) { return MODEL_CONFIG[key] } } return { maxTokensParam: 'max_completion_tokens', defaultMaxTokens: 4096 } } interface ChatMessage { role: 'system' | 'user' | 'assistant' content: string | Array<{ type: string; text?: string; image_url?: { url: string } }> } interface OpenAIResponse { choices: { message: { content: string } }[] } export async function callOpenAI( messages: ChatMessage[], jsonMode = true, model = DEFAULT_MODEL ): Promise { const apiKey = process.env.OPENAI_API_KEY if (!apiKey || apiKey === 'your-openai-api-key-here') { throw new Error('OPENAI_API_KEY가 설정되지 않았습니다.') } const config = getModelConfig(model) const requestBody: any = { model, messages, temperature: 0.1, [config.maxTokensParam]: config.defaultMaxTokens, } if (jsonMode) { requestBody.response_format = { type: 'json_object' } } const response = await fetch(OPENAI_API_URL, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }, body: JSON.stringify(requestBody) }) if (!response.ok) { const error = await response.text() throw new Error(`OpenAI API 오류: ${response.status} - ${error}`) } const data = await response.json() as OpenAIResponse return data.choices[0].message.content } /** * 이미지 분석용 OpenAI 호출 (Vision) */ export async function callOpenAIVision( systemPrompt: string, imageBase64List: string[], model = DEFAULT_MODEL ): Promise { const apiKey = process.env.OPENAI_API_KEY if (!apiKey || apiKey === 'your-openai-api-key-here') { throw new Error('OPENAI_API_KEY가 설정되지 않았습니다.') } const config = getModelConfig(model) const imageContents = imageBase64List.map(base64 => ({ type: 'image_url' as const, image_url: { url: base64.startsWith('data:') ? base64 : `data:image/png;base64,${base64}` } })) const requestBody: any = { model, messages: [ { role: 'system', content: systemPrompt }, { role: 'user', content: [ { type: 'text', text: '이 이미지들에서 주간보고 내용을 추출해주세요.' }, ...imageContents ] } ], temperature: 0.1, [config.maxTokensParam]: config.defaultMaxTokens, response_format: { type: 'json_object' } } const response = await fetch(OPENAI_API_URL, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }, body: JSON.stringify(requestBody) }) if (!response.ok) { const error = await response.text() throw new Error(`OpenAI Vision API 오류: ${response.status} - ${error}`) } const data = await response.json() as OpenAIResponse return data.choices[0].message.content } /** * 주간보고 분석 시스템 프롬프트 (Task 기반) */ export const REPORT_PARSE_SYSTEM_PROMPT = `당신은 주간업무보고 텍스트를 분석하여 구조화된 JSON으로 변환하는 전문가입니다. ## 핵심 원칙 - **원문의 내용을 그대로 유지하세요!** - **Task는 적당히 묶어서 정리하세요. 너무 세분화하지 마세요!** - 하나의 Task에 여러 줄이 들어갈 수 있습니다. ## Task 분리 규칙 (중요!) ❌ 잘못된 예 (너무 세분화): - Task 1: "API 개발" - Task 2: "API 테스트" ✅ 올바른 예 (적절히 묶기): - Task 1: "API 개발 및 테스트 완료" ❌ 잘못된 예 (프로젝트명 반복): - "PIMS 고도화 - 사용자 인증 개발" ✅ 올바른 예 (프로젝트명 제외): - "사용자 인증 개발" ## 완료여부(isCompleted) 판단 규칙 ★중요★ 금주 실적(workTasks)의 완료여부를 판단합니다: - 기본값: true (완료) - false (진행중): 차주 계획(planTasks)에 비슷한/연관된 작업이 있는 경우 예시: - 실적: "로그인 API 개발" + 계획: "로그인 API 테스트" → isCompleted: false (연관 작업 있음) - 실적: "DB 백업 완료" + 계획에 관련 없음 → isCompleted: true ## 수행시간 예측 기준 - **0시간**: "없음", "특이사항 없음", "해당없음", "한 게 없다", "작업 없음" 등 실제 작업이 없는 경우 - 단순 작업: 2~4시간 - 일반 작업: 8시간 (1일) - 복잡한 작업: 16~24시간 (2~3일) ## JSON 출력 형식 { "reportYear": 2025, "reportWeek": 2, "weekStartDate": "2025-01-06", "weekEndDate": "2025-01-12", "reports": [ { "employeeName": "홍길동", "employeeEmail": "hong@example.com", "projects": [ { "projectName": "PIMS 고도화", "workTasks": [ { "description": "사용자 인증 모듈 개발", "hours": 16, "isCompleted": false }, { "description": "DB 백업 스크립트 작성", "hours": 4, "isCompleted": true } ], "planTasks": [ { "description": "사용자 인증 테스트 및 배포", "hours": 8 } ] } ], "issueDescription": "개발서버 메모리 부족", "vacationDescription": null, "remarkDescription": null } ] } ## 주의사항 - Task description에 프로젝트명을 포함하지 마세요 - 비슷한 작업은 하나의 Task로 묶으세요 - 한 Task 내 여러 항목은 \\n으로 줄바꿈 - 이메일이 없으면 employeeEmail은 null` /** * 주간보고 텍스트 분석 프롬프트 */ export function buildParseReportPrompt(rawText: string): ChatMessage[] { return [ { role: 'system', content: REPORT_PARSE_SYSTEM_PROMPT }, { role: 'user', content: rawText } ] }