작업계획서대로 진행
This commit is contained in:
64
backend/api/maintenance/report/available.get.ts
Normal file
64
backend/api/maintenance/report/available.get.ts
Normal file
@@ -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
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
})
|
||||||
100
backend/api/maintenance/report/generate-text.post.ts
Normal file
100
backend/api/maintenance/report/generate-text.post.ts
Normal file
@@ -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<GenerateBody>(event)
|
||||||
|
|
||||||
|
if (!body.tasks || body.tasks.length === 0) {
|
||||||
|
throw createError({ statusCode: 400, message: '업무 목록이 필요합니다.' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 유형별 그룹화
|
||||||
|
const grouped: Record<string, TaskInput[]> = {}
|
||||||
|
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<string, string> = {
|
||||||
|
bug: '버그수정',
|
||||||
|
feature: '기능개선',
|
||||||
|
inquiry: '문의대응',
|
||||||
|
other: '기타'
|
||||||
|
}
|
||||||
|
return labels[type] || '기타'
|
||||||
|
}
|
||||||
78
backend/api/maintenance/report/link.post.ts
Normal file
78
backend/api/maintenance/report/link.post.ts
Normal file
@@ -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<LinkBody>(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
|
||||||
|
}
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user