Files
weeklyreport/backend/api/ai/parse-my-report.post.ts

153 lines
4.8 KiB
TypeScript

import { query } from '../../utils/db'
import { callOpenAI } from '../../utils/openai'
interface ParsedTask {
description: string
hours: number
isCompleted?: boolean
}
interface ParsedProject {
projectName: string
matchedProjectId: number | null
workTasks: ParsedTask[]
planTasks: ParsedTask[]
}
interface ParsedResult {
projects: ParsedProject[]
issueDescription: string | null
vacationDescription: string | null
remarkDescription: string | null
}
/**
* 개인 주간보고 텍스트 분석 (OpenAI)
* POST /api/ai/parse-my-report
*/
export default defineEventHandler(async (event) => {
const userId = getCookie(event, 'user_id')
if (!userId) {
throw createError({ statusCode: 401, message: '로그인이 필요합니다.' })
}
const body = await readBody<{ rawText: string }>(event)
if (!body.rawText || body.rawText.trim().length < 5) {
throw createError({ statusCode: 400, message: '분석할 텍스트를 입력해주세요.' })
}
// 기존 프로젝트 목록 조회
const projects = await query<any>(`
SELECT project_id, project_code, project_name
FROM wr_project_info
WHERE project_status = 'IN_PROGRESS'
`)
// 프로젝트 목록을 ID 포함해서 전달
const projectList = projects.map(p => `[ID:${p.project_id}] ${p.project_code}: ${p.project_name}`).join('\n')
// OpenAI 분석
const prompt = buildMyReportPrompt(body.rawText, projectList)
const aiResponse = await callOpenAI(prompt, true)
let parsed: ParsedResult
try {
parsed = JSON.parse(aiResponse)
} catch (e) {
throw createError({ statusCode: 500, message: 'AI 응답 파싱 실패' })
}
// 프로젝트 매칭
if (parsed.projects) {
for (const proj of parsed.projects) {
if (!proj.matchedProjectId && proj.projectName) {
const matched = projects.find((p: any) =>
p.project_name.toLowerCase().includes(proj.projectName.toLowerCase()) ||
proj.projectName.toLowerCase().includes(p.project_name.toLowerCase()) ||
p.project_code.toLowerCase() === proj.projectName.toLowerCase()
)
if (matched) {
proj.matchedProjectId = matched.project_id
proj.projectName = matched.project_name
}
}
// workTasks 기본값
proj.workTasks = (proj.workTasks || []).map((t: any) => ({
description: t.description || '',
hours: t.hours || 0,
isCompleted: t.isCompleted !== false
}))
// planTasks 기본값
proj.planTasks = (proj.planTasks || []).map((t: any) => ({
description: t.description || '',
hours: t.hours || 0
}))
}
// 내용 없는 프로젝트 제외 (workTasks, planTasks 모두 비어있으면 제외)
parsed.projects = parsed.projects.filter((proj: any) =>
(proj.workTasks && proj.workTasks.length > 0) ||
(proj.planTasks && proj.planTasks.length > 0)
)
}
return {
success: true,
parsed,
projects
}
})
function buildMyReportPrompt(rawText: string, projectList: string): any[] {
return [
{
role: 'system',
content: `당신은 주간보고 내용을 분석하는 AI입니다.
사용자가 입력한 텍스트를 분석하여 프로젝트별 업무 내용을 추출해주세요.
현재 등록된 프로젝트 목록 (형식: [ID:숫자] 코드: 이름):
${projectList}
⚠️ 중요: 입력 텍스트에서 추출한 프로젝트명과 위 목록을 비교하여 가장 유사한 프로젝트의 ID를 matchedProjectId에 반환하세요.
- 유사도 판단: 키워드 일치, 약어, 부분 문자열 등 고려
- 예: "한우 유전체" → "보은 한우 온라인 유전체 분석" 매칭 가능
- 예: "HEIS" → "보건환경연구원 HEIS" 매칭 가능
- 매칭되는 프로젝트가 없으면 matchedProjectId는 null
응답은 반드시 아래 JSON 형식으로만 출력하세요:
{
"projects": [
{
"projectName": "입력에서 추출한 원본 프로젝트명",
"matchedProjectId": 5,
"workTasks": [
{"description": "작업내용", "hours": 8, "isCompleted": true}
],
"planTasks": [
{"description": "계획내용", "hours": 8}
]
}
],
"issueDescription": "이슈/리스크 내용 또는 null",
"vacationDescription": "휴가일정 내용 또는 null",
"remarkDescription": "기타사항 내용 또는 null"
}
규칙:
1. projectName은 입력 텍스트에서 추출한 원본 그대로
2. matchedProjectId는 위 프로젝트 목록에서 가장 유사한 프로젝트의 ID (숫자)
3. "금주 실적", "이번주", "완료" 등은 workTasks로 분류
4. "차주 계획", "다음주", "예정" 등은 planTasks로 분류
5. 시간이 명시되지 않은 경우 hours는 0으로
6. JSON 외의 텍스트는 절대 출력하지 마세요`
},
{
role: 'user',
content: rawText
}
]
}