134 lines
4.8 KiB
TypeScript
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 외의 텍스트는 절대 출력하지 마세요`
|
|
}
|