170 lines
5.0 KiB
TypeScript
170 lines
5.0 KiB
TypeScript
import { query } from '../../utils/db'
|
|
import { callOpenAIVision, REPORT_PARSE_SYSTEM_PROMPT } from '../../utils/openai'
|
|
|
|
const ADMIN_EMAIL = 'coziny@gmail.com'
|
|
|
|
interface ParsedTask {
|
|
description: string
|
|
hours: number
|
|
}
|
|
|
|
interface ParsedProject {
|
|
projectName: string
|
|
workTasks: ParsedTask[]
|
|
planTasks: ParsedTask[]
|
|
}
|
|
|
|
interface ParsedReport {
|
|
employeeName: string
|
|
employeeEmail: string | null
|
|
projects: ParsedProject[]
|
|
issueDescription: string | null
|
|
vacationDescription: string | null
|
|
remarkDescription: string | null
|
|
}
|
|
|
|
interface ParsedResult {
|
|
reportYear: number
|
|
reportWeek: number
|
|
weekStartDate: string
|
|
weekEndDate: string
|
|
reports: ParsedReport[]
|
|
}
|
|
|
|
/**
|
|
* 이미지에서 주간보고 분석 (OpenAI Vision)
|
|
* POST /api/admin/parse-image
|
|
*/
|
|
export default defineEventHandler(async (event) => {
|
|
// 관리자 권한 체크
|
|
const userId = getCookie(event, 'user_id')
|
|
if (!userId) {
|
|
throw createError({ statusCode: 401, message: '로그인이 필요합니다.' })
|
|
}
|
|
|
|
const currentUser = await query<any>(`
|
|
SELECT employee_email FROM wr_employee_info WHERE employee_id = $1
|
|
`, [userId])
|
|
|
|
if (!currentUser[0] || currentUser[0].employee_email !== ADMIN_EMAIL) {
|
|
throw createError({ statusCode: 403, message: '관리자만 사용할 수 있습니다.' })
|
|
}
|
|
|
|
const body = await readBody<{ images: string[] }>(event)
|
|
|
|
if (!body.images || body.images.length === 0) {
|
|
throw createError({ statusCode: 400, message: '분석할 이미지를 업로드해주세요.' })
|
|
}
|
|
|
|
if (body.images.length > 10) {
|
|
throw createError({ statusCode: 400, message: '이미지는 최대 10장까지 업로드 가능합니다.' })
|
|
}
|
|
|
|
// OpenAI Vision 분석
|
|
const aiResponse = await callOpenAIVision(REPORT_PARSE_SYSTEM_PROMPT, body.images)
|
|
|
|
let parsed: ParsedResult
|
|
try {
|
|
parsed = JSON.parse(aiResponse)
|
|
} catch (e) {
|
|
throw createError({ statusCode: 500, message: 'AI 응답 파싱 실패' })
|
|
}
|
|
|
|
// 주차 정보 기본값 설정 (AI가 파싱 못한 경우)
|
|
const now = new Date()
|
|
if (!parsed.reportYear) {
|
|
parsed.reportYear = now.getFullYear()
|
|
}
|
|
if (!parsed.reportWeek) {
|
|
// ISO 주차 계산
|
|
const startOfYear = new Date(now.getFullYear(), 0, 1)
|
|
const days = Math.floor((now.getTime() - startOfYear.getTime()) / (24 * 60 * 60 * 1000))
|
|
parsed.reportWeek = Math.ceil((days + startOfYear.getDay() + 1) / 7)
|
|
}
|
|
if (!parsed.weekStartDate || !parsed.weekEndDate) {
|
|
// 현재 주의 월요일~일요일 계산
|
|
const day = now.getDay()
|
|
const monday = new Date(now)
|
|
monday.setDate(now.getDate() - (day === 0 ? 6 : day - 1))
|
|
const sunday = new Date(monday)
|
|
sunday.setDate(monday.getDate() + 6)
|
|
parsed.weekStartDate = monday.toISOString().split('T')[0]
|
|
parsed.weekEndDate = sunday.toISOString().split('T')[0]
|
|
}
|
|
|
|
// 기존 직원 목록 조회
|
|
const employees = await query<any>(`
|
|
SELECT employee_id, employee_name, employee_email
|
|
FROM wr_employee_info
|
|
WHERE is_active = true
|
|
`)
|
|
|
|
// 기존 프로젝트 목록 조회
|
|
const projects = await query<any>(`
|
|
SELECT project_id, project_code, project_name
|
|
FROM wr_project_info
|
|
WHERE project_status != 'COMPLETED'
|
|
`)
|
|
|
|
// 직원 및 프로젝트 매칭
|
|
const matchedReports = parsed.reports.map(report => {
|
|
let matchedEmployee = null
|
|
if (report.employeeEmail) {
|
|
matchedEmployee = employees.find(
|
|
(e: any) => e.employee_email.toLowerCase() === report.employeeEmail?.toLowerCase()
|
|
)
|
|
}
|
|
if (!matchedEmployee) {
|
|
matchedEmployee = employees.find(
|
|
(e: any) => e.employee_name === report.employeeName
|
|
)
|
|
}
|
|
|
|
const matchedProjects = report.projects.map(proj => {
|
|
const existingProject = projects.find((p: any) =>
|
|
p.project_name.includes(proj.projectName) ||
|
|
proj.projectName.includes(p.project_name)
|
|
)
|
|
|
|
return {
|
|
...proj,
|
|
matchedProjectId: existingProject?.project_id || null,
|
|
matchedProjectCode: existingProject?.project_code || null,
|
|
matchedProjectName: existingProject?.project_name || null,
|
|
isNewProject: !existingProject
|
|
}
|
|
})
|
|
|
|
return {
|
|
...report,
|
|
matchedEmployeeId: matchedEmployee?.employee_id || null,
|
|
matchedEmployeeName: matchedEmployee?.employee_name || null,
|
|
matchedEmployeeEmail: matchedEmployee?.employee_email || null,
|
|
isEmployeeMatched: !!matchedEmployee,
|
|
isNewEmployee: !matchedEmployee && !!report.employeeEmail,
|
|
projects: matchedProjects
|
|
}
|
|
})
|
|
|
|
return {
|
|
success: true,
|
|
parsed: {
|
|
reportYear: parsed.reportYear,
|
|
reportWeek: parsed.reportWeek,
|
|
weekStartDate: parsed.weekStartDate,
|
|
weekEndDate: parsed.weekEndDate,
|
|
reports: matchedReports
|
|
},
|
|
employees: employees.map((e: any) => ({
|
|
employeeId: e.employee_id,
|
|
employeeName: e.employee_name,
|
|
employeeEmail: e.employee_email
|
|
})),
|
|
projects: projects.map((p: any) => ({
|
|
projectId: p.project_id,
|
|
projectCode: p.project_code,
|
|
projectName: p.project_name
|
|
}))
|
|
}
|
|
})
|