import { defineEventHandler, createError, getCookie } from 'h3' import { query, queryOne, execute } from '../../../utils/db' import OpenAI from 'openai' const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }) /** * 기존 취합 보고서에 AI 요약 일괄 생성 * POST /api/report/summary/regenerate-ai */ export default defineEventHandler(async (event) => { const userId = getCookie(event, 'user_id') if (!userId) { throw createError({ statusCode: 401, message: '로그인이 필요합니다.' }) } // AI 요약이 없는 취합 보고서 조회 const summaries = await query(` SELECT s.summary_id, s.project_id, s.report_year, s.report_week, p.project_name FROM wr_aggregated_report_summary s JOIN wr_project_info p ON s.project_id = p.project_id WHERE s.ai_work_summary IS NULL OR s.ai_plan_summary IS NULL ORDER BY s.summary_id `, []) console.log(`AI 요약 생성 대상: ${summaries.length}건`) let successCount = 0 let errorCount = 0 for (const summary of summaries) { try { // 해당 프로젝트/주차의 Task 조회 const tasks = await query(` SELECT t.task_type, t.task_description, t.task_hours, t.is_completed, e.employee_name as author_name FROM wr_weekly_report r JOIN wr_weekly_report_task t ON r.report_id = t.report_id JOIN wr_employee_info e ON r.author_id = e.employee_id WHERE t.project_id = $1 AND r.report_year = $2 AND r.report_week = $3 AND r.report_status IN ('SUBMITTED', 'AGGREGATED') ORDER BY t.task_type, e.employee_name `, [summary.project_id, summary.report_year, summary.report_week]) if (tasks.length === 0) { console.log(`Skip ${summary.summary_id}: no tasks`) continue } // AI 요약 생성 const { workSummary, planSummary } = await generateAISummary( tasks, summary.project_name, summary.report_year, summary.report_week ) // 업데이트 await execute(` UPDATE wr_aggregated_report_summary SET ai_work_summary = $1, ai_plan_summary = $2, ai_summary_at = NOW() WHERE summary_id = $3 `, [workSummary, planSummary, summary.summary_id]) successCount++ console.log(`Generated AI summary for ${summary.project_name} (${summary.report_year}-W${summary.report_week})`) } catch (e: any) { console.error(`Error for summary ${summary.summary_id}:`, e.message) errorCount++ } } return { success: true, total: summaries.length, successCount, errorCount } }) async function generateAISummary(tasks: any[], projectName: string, year: number, week: number) { const workTasks = tasks.filter(t => t.task_type === 'WORK') const planTasks = tasks.filter(t => t.task_type === 'PLAN') const workPrompt = `주간보고 취합 전문가입니다. 아래 금주 실적을 간결하게 요약해주세요. ## 프로젝트: ${projectName} ## 기간: ${year}년 ${week}주차 ## 금주 실적 (${workTasks.length}건) ${workTasks.map(t => `- [${t.author_name}] ${t.is_completed ? '완료' : '진행중'} | ${t.task_description} (${t.task_hours}h)`).join('\n') || '없음'} ## 요약 규칙 1. 완료된 작업과 진행 중인 작업을 구분하여 핵심만 요약 2. 동일/유사한 작업은 하나로 통합 3. 담당자 이름은 생략하고 내용 위주로 작성 4. 3~5줄 이내로 간결하게 5. 마크다운 리스트 형식으로 작성` const planPrompt = `주간보고 취합 전문가입니다. 아래 차주 계획을 간결하게 요약해주세요. ## 프로젝트: ${projectName} ## 기간: ${year}년 ${week+1}주차 계획 ## 차주 계획 (${planTasks.length}건) ${planTasks.map(t => `- [${t.author_name}] ${t.task_description} (${t.task_hours}h)`).join('\n') || '없음'} ## 요약 규칙 1. 주요 계획을 우선순위에 따라 요약 2. 동일/유사한 작업은 하나로 통합 3. 담당자 이름은 생략하고 내용 위주로 작성 4. 2~4줄 이내로 간결하게 5. 마크다운 리스트 형식으로 작성` const [workRes, planRes] = await Promise.all([ openai.chat.completions.create({ model: 'gpt-4o-mini', messages: [ { role: 'system', content: '주간보고 취합 전문가입니다. 간결하게 요약합니다.' }, { role: 'user', content: workPrompt } ], temperature: 0.3, max_tokens: 500 }), openai.chat.completions.create({ model: 'gpt-4o-mini', messages: [ { role: 'system', content: '주간보고 취합 전문가입니다. 간결하게 요약합니다.' }, { role: 'user', content: planPrompt } ], temperature: 0.3, max_tokens: 500 }) ]) return { workSummary: workRes.choices[0]?.message?.content || '요약 없음', planSummary: planRes.choices[0]?.message?.content || '요약 없음' } }