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

134 lines
4.8 KiB
TypeScript

import { query } from '../../utils/db'
import { callOpenAIVision } from '../../utils/openai'
/**
* 개인 주간보고 이미지 분석 (OpenAI Vision)
* POST /api/ai/parse-my-report-image
*/
export default defineEventHandler(async (event) => {
const userId = getCookie(event, 'user_id')
if (!userId) {
throw createError({ statusCode: 401, message: '로그인이 필요합니다.' })
}
const body = await readBody<{ images: string[] }>(event)
if (!body.images || body.images.length === 0) {
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 Vision 분석
const prompt = buildImagePrompt(projectList)
console.log('=== AI 이미지 분석 시작 ===')
console.log('이미지 개수:', body.images.length)
console.log('프로젝트 목록:', projectList)
const aiResponse = await callOpenAIVision(prompt, body.images)
console.log('=== AI 응답 (raw) ===')
console.log(aiResponse)
let parsed: any
try {
parsed = JSON.parse(aiResponse)
console.log('=== AI 응답 (parsed) ===')
console.log(JSON.stringify(parsed, null, 2))
} catch (e) {
console.error('=== JSON 파싱 실패 ===')
console.error(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
}
}
proj.workTasks = (proj.workTasks || []).map((t: any) => ({
description: t.description || '',
hours: t.hours || 0,
isCompleted: t.isCompleted !== false
}))
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 buildImagePrompt(projectList: string): string {
return `당신은 주간보고 내용을 분석하는 AI입니다.
이미지에서 주간보고 내용을 추출하여 JSON으로 반환해주세요.
이미지에 여러 사람의 내용이 있어도 모두 추출하여 하나의 보고서로 통합해주세요.
현재 등록된 프로젝트 목록 (형식: [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. 이미지에서 모든 주간보고 내용을 추출
2. projectName은 이미지에서 추출한 원본 텍스트 그대로
3. matchedProjectId는 위 프로젝트 목록에서 가장 유사한 프로젝트의 ID (숫자)
4. "금주 실적", "이번주", "완료" 등은 workTasks로 분류
5. "차주 계획", "다음주", "예정" 등은 planTasks로 분류
6. 시간이 명시되지 않은 경우 hours는 0으로
7. JSON 외의 텍스트는 절대 출력하지 마세요`
}