From 5cda181cc53eee802d0d9d47774416ad090911ec Mon Sep 17 00:00:00 2001 From: Hyoseong Jo Date: Sun, 11 Jan 2026 01:34:31 +0900 Subject: [PATCH] =?UTF-8?q?=EC=9E=91=EC=97=85=EA=B3=84=ED=9A=8D=EC=84=9C?= =?UTF-8?q?=EB=8C=80=EB=A1=9C=20=EC=A7=84=ED=96=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/maintenance/report/available.get.ts | 64 +++++++++++ .../maintenance/report/generate-text.post.ts | 100 ++++++++++++++++++ backend/api/maintenance/report/link.post.ts | 78 ++++++++++++++ 3 files changed, 242 insertions(+) create mode 100644 backend/api/maintenance/report/available.get.ts create mode 100644 backend/api/maintenance/report/generate-text.post.ts create mode 100644 backend/api/maintenance/report/link.post.ts diff --git a/backend/api/maintenance/report/available.get.ts b/backend/api/maintenance/report/available.get.ts new file mode 100644 index 0000000..d2d2ef1 --- /dev/null +++ b/backend/api/maintenance/report/available.get.ts @@ -0,0 +1,64 @@ +import { query } from '../../../utils/db' + +/** + * 주간보고 연계용 유지보수 업무 조회 + * 해당 주차에 완료된 유지보수 업무 목록 + * GET /api/maintenance/report/available + */ +export default defineEventHandler(async (event) => { + const params = getQuery(event) + const projectId = params.projectId ? Number(params.projectId) : null + const weekStartDate = params.weekStartDate as string + const weekEndDate = params.weekEndDate as string + + if (!projectId) { + throw createError({ statusCode: 400, message: '프로젝트 ID가 필요합니다.' }) + } + + // 해당 주차에 완료된 유지보수 업무 (아직 주간보고에 연결 안 된 것) + const sql = ` + SELECT + m.task_id, + m.request_date, + m.request_title, + m.request_content, + m.requester_name, + m.task_type, + m.priority, + m.status, + m.resolution_content, + m.dev_completed_at, + m.ops_completed_at, + m.client_confirmed_at, + m.weekly_report_id + FROM wr_maintenance_task m + WHERE m.project_id = $1 + AND m.status = 'COMPLETED' + AND m.weekly_report_id IS NULL + AND ( + (m.dev_completed_at >= $2 AND m.dev_completed_at <= $3) + OR (m.ops_completed_at >= $2 AND m.ops_completed_at <= $3) + OR (m.client_confirmed_at >= $2 AND m.client_confirmed_at <= $3) + ) + ORDER BY m.dev_completed_at DESC NULLS LAST, m.task_id DESC + ` + + const tasks = await query(sql, [projectId, weekStartDate, weekEndDate + ' 23:59:59']) + + return { + tasks: tasks.map((t: any) => ({ + taskId: t.task_id, + requestDate: t.request_date, + requestTitle: t.request_title, + requestContent: t.request_content, + requesterName: t.requester_name, + taskType: t.task_type, + priority: t.priority, + status: t.status, + resolutionContent: t.resolution_content, + devCompletedAt: t.dev_completed_at, + opsCompletedAt: t.ops_completed_at, + clientConfirmedAt: t.client_confirmed_at + })) + } +}) diff --git a/backend/api/maintenance/report/generate-text.post.ts b/backend/api/maintenance/report/generate-text.post.ts new file mode 100644 index 0000000..0f578ba --- /dev/null +++ b/backend/api/maintenance/report/generate-text.post.ts @@ -0,0 +1,100 @@ +import { callOpenAI } from '../../../utils/openai' + +interface TaskInput { + taskId: number + requestTitle: string + requestContent?: string + taskType: string + resolutionContent?: string +} + +interface GenerateBody { + tasks: TaskInput[] + projectName?: string +} + +/** + * 유지보수 업무를 주간보고 실적 문장으로 변환 + * POST /api/maintenance/report/generate-text + */ +export default defineEventHandler(async (event) => { + const body = await readBody(event) + + if (!body.tasks || body.tasks.length === 0) { + throw createError({ statusCode: 400, message: '업무 목록이 필요합니다.' }) + } + + // 유형별 그룹화 + const grouped: Record = {} + for (const task of body.tasks) { + const type = task.taskType || 'other' + if (!grouped[type]) grouped[type] = [] + grouped[type].push(task) + } + + // OpenAI 프롬프트 + const taskList = body.tasks.map((t, i) => + `${i+1}. [${t.taskType}] ${t.requestTitle}${t.resolutionContent ? ' → ' + t.resolutionContent : ''}` + ).join('\n') + + const prompt = `다음 유지보수 업무 목록을 주간보고 실적으로 작성해주세요. + +업무 목록: +${taskList} + +작성 가이드: +1. 유사한 업무는 하나로 병합 (예: "XX 관련 버그 수정 3건") +2. 주간보고에 적합한 간결한 문장으로 작성 +3. 기술적 용어는 유지하되 명확하게 +4. 각 실적은 한 줄로 작성 + +JSON 형식으로 응답: +{ + "tasks": [ + { + "description": "실적 문장", + "sourceTaskIds": [원본 task_id 배열], + "taskType": "bug|feature|inquiry|other" + } + ] +}` + + try { + const response = await callOpenAI([ + { role: 'system', content: '주간보고 작성 전문가입니다. 유지보수 업무를 간결하고 명확한 실적 문장으로 변환합니다.' }, + { role: 'user', content: prompt } + ], true) + + const parsed = JSON.parse(response) + + return { + success: true, + generatedTasks: parsed.tasks || [] + } + } catch (e) { + console.error('OpenAI error:', e) + + // 실패 시 기본 변환 + const defaultTasks = body.tasks.map(t => ({ + description: `[${getTypeLabel(t.taskType)}] ${t.requestTitle}`, + sourceTaskIds: [t.taskId], + taskType: t.taskType + })) + + return { + success: true, + generatedTasks: defaultTasks, + fallback: true + } + } +}) + +function getTypeLabel(type: string): string { + const labels: Record = { + bug: '버그수정', + feature: '기능개선', + inquiry: '문의대응', + other: '기타' + } + return labels[type] || '기타' +} diff --git a/backend/api/maintenance/report/link.post.ts b/backend/api/maintenance/report/link.post.ts new file mode 100644 index 0000000..98ef7cf --- /dev/null +++ b/backend/api/maintenance/report/link.post.ts @@ -0,0 +1,78 @@ +import { insertReturning, execute, query } from '../../../utils/db' +import { getClientIp } from '../../../utils/ip' +import { getCurrentUserEmail } from '../../../utils/user' + +interface TaskItem { + description: string + sourceTaskIds: number[] + taskType: string + taskHours?: number +} + +interface LinkBody { + reportId: number + projectId: number + tasks: TaskItem[] +} + +/** + * 유지보수 업무를 주간보고 실적으로 등록 + * POST /api/maintenance/report/link + */ +export default defineEventHandler(async (event) => { + const body = await readBody(event) + const clientIp = getClientIp(event) + const userEmail = await getCurrentUserEmail(event) + + if (!body.reportId || !body.projectId) { + throw createError({ statusCode: 400, message: '보고서 ID와 프로젝트 ID가 필요합니다.' }) + } + + if (!body.tasks || body.tasks.length === 0) { + throw createError({ statusCode: 400, message: '등록할 실적이 없습니다.' }) + } + + const insertedTaskIds: number[] = [] + const linkedMaintenanceIds: number[] = [] + + for (const task of body.tasks) { + // 주간보고 실적 등록 + const result = await insertReturning(` + INSERT INTO wr_weekly_report_task ( + report_id, project_id, task_type, task_description, task_hours, + is_completed, created_ip, created_email + ) VALUES ($1, $2, $3, $4, $5, true, $6, $7) + RETURNING task_id + `, [ + body.reportId, + body.projectId, + task.taskType || 'other', + task.description, + task.taskHours || null, + clientIp, + userEmail + ]) + + const newTaskId = result.task_id + insertedTaskIds.push(newTaskId) + + // 유지보수 업무와 연결 + if (task.sourceTaskIds && task.sourceTaskIds.length > 0) { + for (const maintenanceTaskId of task.sourceTaskIds) { + await execute(` + UPDATE wr_maintenance_task + SET weekly_report_id = $1, updated_at = NOW() + WHERE task_id = $2 AND weekly_report_id IS NULL + `, [body.reportId, maintenanceTaskId]) + linkedMaintenanceIds.push(maintenanceTaskId) + } + } + } + + return { + success: true, + insertedCount: insertedTaskIds.length, + linkedMaintenanceCount: linkedMaintenanceIds.length, + taskIds: insertedTaskIds + } +})