기능구현중
This commit is contained in:
36
server/api/maintenance/[id]/delete.delete.ts
Normal file
36
server/api/maintenance/[id]/delete.delete.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { queryOne, execute } from '../../../utils/db'
|
||||
|
||||
/**
|
||||
* 유지보수 업무 삭제
|
||||
* DELETE /api/maintenance/[id]/delete
|
||||
*/
|
||||
export default defineEventHandler(async (event) => {
|
||||
const taskId = Number(getRouterParam(event, 'id'))
|
||||
|
||||
if (!taskId) {
|
||||
throw createError({ statusCode: 400, message: '업무 ID가 필요합니다.' })
|
||||
}
|
||||
|
||||
const existing = await queryOne(`
|
||||
SELECT task_id, weekly_report_id FROM wr_maintenance_task WHERE task_id = $1
|
||||
`, [taskId])
|
||||
|
||||
if (!existing) {
|
||||
throw createError({ statusCode: 404, message: '업무를 찾을 수 없습니다.' })
|
||||
}
|
||||
|
||||
// 주간보고에 연계된 경우 경고
|
||||
if (existing.weekly_report_id) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
message: '주간보고에 연계된 업무입니다. 연계 해제 후 삭제하세요.'
|
||||
})
|
||||
}
|
||||
|
||||
await execute(`DELETE FROM wr_maintenance_task WHERE task_id = $1`, [taskId])
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: '유지보수 업무가 삭제되었습니다.'
|
||||
}
|
||||
})
|
||||
64
server/api/maintenance/[id]/detail.get.ts
Normal file
64
server/api/maintenance/[id]/detail.get.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { queryOne } from '../../../utils/db'
|
||||
|
||||
/**
|
||||
* 유지보수 업무 상세 조회
|
||||
* GET /api/maintenance/[id]/detail
|
||||
*/
|
||||
export default defineEventHandler(async (event) => {
|
||||
const taskId = Number(getRouterParam(event, 'id'))
|
||||
|
||||
if (!taskId) {
|
||||
throw createError({ statusCode: 400, message: '업무 ID가 필요합니다.' })
|
||||
}
|
||||
|
||||
const task = await queryOne(`
|
||||
SELECT
|
||||
t.*,
|
||||
p.project_name,
|
||||
p.project_code,
|
||||
e1.employee_name as assignee_name,
|
||||
e2.employee_name as created_by_name,
|
||||
e3.employee_name as updated_by_name
|
||||
FROM wr_maintenance_task t
|
||||
LEFT JOIN wr_project_info p ON t.project_id = p.project_id
|
||||
LEFT JOIN wr_employee_info e1 ON t.assignee_id = e1.employee_id
|
||||
LEFT JOIN wr_employee_info e2 ON t.created_by = e2.employee_id
|
||||
LEFT JOIN wr_employee_info e3 ON t.updated_by = e3.employee_id
|
||||
WHERE t.task_id = $1
|
||||
`, [taskId])
|
||||
|
||||
if (!task) {
|
||||
throw createError({ statusCode: 404, message: '업무를 찾을 수 없습니다.' })
|
||||
}
|
||||
|
||||
return {
|
||||
task: {
|
||||
taskId: task.task_id,
|
||||
projectId: task.project_id,
|
||||
projectName: task.project_name,
|
||||
projectCode: task.project_code,
|
||||
batchId: task.batch_id,
|
||||
requestDate: task.request_date,
|
||||
requestTitle: task.request_title,
|
||||
requestContent: task.request_content,
|
||||
requesterName: task.requester_name,
|
||||
requesterContact: task.requester_contact,
|
||||
taskType: task.task_type,
|
||||
priority: task.priority,
|
||||
status: task.status,
|
||||
assigneeId: task.assignee_id,
|
||||
assigneeName: task.assignee_name,
|
||||
devCompletedAt: task.dev_completed_at,
|
||||
opsCompletedAt: task.ops_completed_at,
|
||||
clientConfirmedAt: task.client_confirmed_at,
|
||||
resolutionContent: task.resolution_content,
|
||||
weeklyReportId: task.weekly_report_id,
|
||||
createdBy: task.created_by,
|
||||
createdByName: task.created_by_name,
|
||||
updatedBy: task.updated_by,
|
||||
updatedByName: task.updated_by_name,
|
||||
createdAt: task.created_at,
|
||||
updatedAt: task.updated_at
|
||||
}
|
||||
}
|
||||
})
|
||||
48
server/api/maintenance/[id]/status.put.ts
Normal file
48
server/api/maintenance/[id]/status.put.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { queryOne, execute } from '../../../utils/db'
|
||||
import { getCurrentUserId } from '../../../utils/user'
|
||||
|
||||
interface StatusBody {
|
||||
status: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 유지보수 업무 상태 변경
|
||||
* PUT /api/maintenance/[id]/status
|
||||
*/
|
||||
export default defineEventHandler(async (event) => {
|
||||
const taskId = Number(getRouterParam(event, 'id'))
|
||||
const body = await readBody<StatusBody>(event)
|
||||
const userId = await getCurrentUserId(event)
|
||||
|
||||
if (!taskId) {
|
||||
throw createError({ statusCode: 400, message: '업무 ID가 필요합니다.' })
|
||||
}
|
||||
|
||||
const validStatuses = ['PENDING', 'IN_PROGRESS', 'COMPLETED']
|
||||
if (!validStatuses.includes(body.status)) {
|
||||
throw createError({ statusCode: 400, message: '유효하지 않은 상태입니다.' })
|
||||
}
|
||||
|
||||
const existing = await queryOne(`
|
||||
SELECT task_id FROM wr_maintenance_task WHERE task_id = $1
|
||||
`, [taskId])
|
||||
|
||||
if (!existing) {
|
||||
throw createError({ statusCode: 404, message: '업무를 찾을 수 없습니다.' })
|
||||
}
|
||||
|
||||
await execute(`
|
||||
UPDATE wr_maintenance_task SET
|
||||
status = $1,
|
||||
updated_at = NOW(),
|
||||
updated_by = $2
|
||||
WHERE task_id = $3
|
||||
`, [body.status, userId, taskId])
|
||||
|
||||
return {
|
||||
success: true,
|
||||
taskId,
|
||||
status: body.status,
|
||||
message: '상태가 변경되었습니다.'
|
||||
}
|
||||
})
|
||||
85
server/api/maintenance/[id]/update.put.ts
Normal file
85
server/api/maintenance/[id]/update.put.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { queryOne, execute } from '../../../utils/db'
|
||||
import { getCurrentUserId } from '../../../utils/user'
|
||||
|
||||
interface UpdateMaintenanceBody {
|
||||
projectId?: number | null
|
||||
requestDate?: string
|
||||
requestTitle?: string
|
||||
requestContent?: string
|
||||
requesterName?: string
|
||||
requesterContact?: string
|
||||
taskType?: string
|
||||
priority?: string
|
||||
status?: string
|
||||
assigneeId?: number | null
|
||||
resolutionContent?: string
|
||||
devCompletedAt?: string | null
|
||||
opsCompletedAt?: string | null
|
||||
clientConfirmedAt?: string | null
|
||||
}
|
||||
|
||||
/**
|
||||
* 유지보수 업무 수정
|
||||
* PUT /api/maintenance/[id]/update
|
||||
*/
|
||||
export default defineEventHandler(async (event) => {
|
||||
const taskId = Number(getRouterParam(event, 'id'))
|
||||
const body = await readBody<UpdateMaintenanceBody>(event)
|
||||
const userId = await getCurrentUserId(event)
|
||||
|
||||
if (!taskId) {
|
||||
throw createError({ statusCode: 400, message: '업무 ID가 필요합니다.' })
|
||||
}
|
||||
|
||||
const existing = await queryOne(`
|
||||
SELECT task_id FROM wr_maintenance_task WHERE task_id = $1
|
||||
`, [taskId])
|
||||
|
||||
if (!existing) {
|
||||
throw createError({ statusCode: 404, message: '업무를 찾을 수 없습니다.' })
|
||||
}
|
||||
|
||||
await execute(`
|
||||
UPDATE wr_maintenance_task SET
|
||||
project_id = $1,
|
||||
request_date = $2,
|
||||
request_title = $3,
|
||||
request_content = $4,
|
||||
requester_name = $5,
|
||||
requester_contact = $6,
|
||||
task_type = $7,
|
||||
priority = $8,
|
||||
status = $9,
|
||||
assignee_id = $10,
|
||||
resolution_content = $11,
|
||||
dev_completed_at = $12,
|
||||
ops_completed_at = $13,
|
||||
client_confirmed_at = $14,
|
||||
updated_at = NOW(),
|
||||
updated_by = $15
|
||||
WHERE task_id = $16
|
||||
`, [
|
||||
body.projectId ?? null,
|
||||
body.requestDate,
|
||||
body.requestTitle,
|
||||
body.requestContent || null,
|
||||
body.requesterName || null,
|
||||
body.requesterContact || null,
|
||||
body.taskType || 'GENERAL',
|
||||
body.priority || 'MEDIUM',
|
||||
body.status || 'PENDING',
|
||||
body.assigneeId ?? null,
|
||||
body.resolutionContent || null,
|
||||
body.devCompletedAt || null,
|
||||
body.opsCompletedAt || null,
|
||||
body.clientConfirmedAt || null,
|
||||
userId,
|
||||
taskId
|
||||
])
|
||||
|
||||
return {
|
||||
success: true,
|
||||
taskId,
|
||||
message: '유지보수 업무가 수정되었습니다.'
|
||||
}
|
||||
})
|
||||
84
server/api/maintenance/bulk-create.post.ts
Normal file
84
server/api/maintenance/bulk-create.post.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { insertReturning, query } from '../../utils/db'
|
||||
import { getCurrentUserId } from '../../utils/user'
|
||||
|
||||
interface TaskItem {
|
||||
requestDate: string | null
|
||||
requestTitle: string
|
||||
requestContent: string | null
|
||||
requesterName: string | null
|
||||
requesterContact: string | null
|
||||
taskType: string
|
||||
priority: string
|
||||
resolutionContent: string | null
|
||||
isDuplicate?: boolean
|
||||
selected?: boolean
|
||||
}
|
||||
|
||||
interface BulkCreateBody {
|
||||
projectId: number
|
||||
batchId: number
|
||||
tasks: TaskItem[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 유지보수 업무 일괄 등록
|
||||
* POST /api/maintenance/bulk-create
|
||||
*/
|
||||
export default defineEventHandler(async (event) => {
|
||||
const body = await readBody<BulkCreateBody>(event)
|
||||
const userId = await getCurrentUserId(event)
|
||||
|
||||
if (!body.projectId) {
|
||||
throw createError({ statusCode: 400, message: '프로젝트를 선택해주세요.' })
|
||||
}
|
||||
|
||||
if (!body.tasks || body.tasks.length === 0) {
|
||||
throw createError({ statusCode: 400, message: '등록할 업무가 없습니다.' })
|
||||
}
|
||||
|
||||
// 선택된 항목만 필터 (selected가 false가 아닌 것)
|
||||
const tasksToInsert = body.tasks.filter(t => t.selected !== false && !t.isDuplicate)
|
||||
|
||||
if (tasksToInsert.length === 0) {
|
||||
throw createError({ statusCode: 400, message: '등록할 업무가 없습니다. (모두 제외되었거나 중복)' })
|
||||
}
|
||||
|
||||
const inserted: number[] = []
|
||||
const errors: string[] = []
|
||||
|
||||
for (const task of tasksToInsert) {
|
||||
try {
|
||||
const result = await insertReturning(`
|
||||
INSERT INTO wr_maintenance_task (
|
||||
project_id, batch_id, request_date, request_title, request_content,
|
||||
requester_name, requester_contact, task_type, priority, status,
|
||||
resolution_content, created_by
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, 'PENDING', $10, $11)
|
||||
RETURNING task_id
|
||||
`, [
|
||||
body.projectId,
|
||||
body.batchId,
|
||||
task.requestDate || null,
|
||||
task.requestTitle,
|
||||
task.requestContent || null,
|
||||
task.requesterName || null,
|
||||
task.requesterContact || null,
|
||||
task.taskType || 'other',
|
||||
task.priority || 'medium',
|
||||
task.resolutionContent || null,
|
||||
userId
|
||||
])
|
||||
inserted.push(result.task_id)
|
||||
} catch (e: any) {
|
||||
errors.push(`${task.requestTitle}: ${e.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
insertedCount: inserted.length,
|
||||
errorCount: errors.length,
|
||||
errors: errors.length > 0 ? errors : undefined,
|
||||
taskIds: inserted
|
||||
}
|
||||
})
|
||||
57
server/api/maintenance/create.post.ts
Normal file
57
server/api/maintenance/create.post.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { insertReturning } from '../../utils/db'
|
||||
import { getCurrentUserId } from '../../utils/user'
|
||||
|
||||
interface CreateMaintenanceBody {
|
||||
projectId?: number
|
||||
requestDate: string
|
||||
requestTitle: string
|
||||
requestContent?: string
|
||||
requesterName?: string
|
||||
requesterContact?: string
|
||||
taskType?: string
|
||||
priority?: string
|
||||
assigneeId?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 유지보수 업무 생성
|
||||
* POST /api/maintenance/create
|
||||
*/
|
||||
export default defineEventHandler(async (event) => {
|
||||
const body = await readBody<CreateMaintenanceBody>(event)
|
||||
const userId = await getCurrentUserId(event)
|
||||
|
||||
if (!body.requestTitle) {
|
||||
throw createError({ statusCode: 400, message: '제목은 필수입니다.' })
|
||||
}
|
||||
|
||||
if (!body.requestDate) {
|
||||
throw createError({ statusCode: 400, message: '요청일은 필수입니다.' })
|
||||
}
|
||||
|
||||
const task = await insertReturning(`
|
||||
INSERT INTO wr_maintenance_task (
|
||||
project_id, request_date, request_title, request_content,
|
||||
requester_name, requester_contact, task_type, priority,
|
||||
assignee_id, status, created_by, updated_by
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, 'PENDING', $10, $10)
|
||||
RETURNING *
|
||||
`, [
|
||||
body.projectId || null,
|
||||
body.requestDate,
|
||||
body.requestTitle,
|
||||
body.requestContent || null,
|
||||
body.requesterName || null,
|
||||
body.requesterContact || null,
|
||||
body.taskType || 'GENERAL',
|
||||
body.priority || 'MEDIUM',
|
||||
body.assigneeId || null,
|
||||
userId
|
||||
])
|
||||
|
||||
return {
|
||||
success: true,
|
||||
taskId: task.task_id,
|
||||
message: '유지보수 업무가 등록되었습니다.'
|
||||
}
|
||||
})
|
||||
114
server/api/maintenance/list.get.ts
Normal file
114
server/api/maintenance/list.get.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import { query } from '../../utils/db'
|
||||
|
||||
/**
|
||||
* 유지보수 업무 목록 조회
|
||||
* GET /api/maintenance/list
|
||||
*
|
||||
* Query params:
|
||||
* - projectId: 프로젝트 ID
|
||||
* - status: 상태 (PENDING, IN_PROGRESS, COMPLETED)
|
||||
* - priority: 우선순위 (HIGH, MEDIUM, LOW)
|
||||
* - keyword: 검색어 (제목, 내용, 요청자)
|
||||
* - startDate, endDate: 요청일 범위
|
||||
* - page, pageSize: 페이지네이션
|
||||
*/
|
||||
export default defineEventHandler(async (event) => {
|
||||
const params = getQuery(event)
|
||||
|
||||
const projectId = params.projectId ? Number(params.projectId) : null
|
||||
const status = params.status as string | null
|
||||
const priority = params.priority as string | null
|
||||
const keyword = params.keyword as string | null
|
||||
const startDate = params.startDate as string | null
|
||||
const endDate = params.endDate as string | null
|
||||
const page = Number(params.page) || 1
|
||||
const pageSize = Number(params.pageSize) || 20
|
||||
|
||||
const conditions: string[] = []
|
||||
const values: any[] = []
|
||||
let paramIndex = 1
|
||||
|
||||
if (projectId) {
|
||||
conditions.push(`t.project_id = $${paramIndex++}`)
|
||||
values.push(projectId)
|
||||
}
|
||||
|
||||
if (status) {
|
||||
conditions.push(`t.status = $${paramIndex++}`)
|
||||
values.push(status)
|
||||
}
|
||||
|
||||
if (priority) {
|
||||
conditions.push(`t.priority = $${paramIndex++}`)
|
||||
values.push(priority)
|
||||
}
|
||||
|
||||
if (keyword) {
|
||||
conditions.push(`(t.request_title ILIKE $${paramIndex} OR t.request_content ILIKE $${paramIndex} OR t.requester_name ILIKE $${paramIndex})`)
|
||||
values.push(`%${keyword}%`)
|
||||
paramIndex++
|
||||
}
|
||||
|
||||
if (startDate) {
|
||||
conditions.push(`t.request_date >= $${paramIndex++}`)
|
||||
values.push(startDate)
|
||||
}
|
||||
|
||||
if (endDate) {
|
||||
conditions.push(`t.request_date <= $${paramIndex++}`)
|
||||
values.push(endDate)
|
||||
}
|
||||
|
||||
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''
|
||||
|
||||
// 전체 카운트
|
||||
const countSql = `SELECT COUNT(*) as total FROM wr_maintenance_task t ${whereClause}`
|
||||
const countResult = await query(countSql, values)
|
||||
const total = Number(countResult[0]?.total || 0)
|
||||
|
||||
// 목록 조회
|
||||
const offset = (page - 1) * pageSize
|
||||
const listSql = `
|
||||
SELECT
|
||||
t.*,
|
||||
p.project_name,
|
||||
e.employee_name as assignee_name
|
||||
FROM wr_maintenance_task t
|
||||
LEFT JOIN wr_project_info p ON t.project_id = p.project_id
|
||||
LEFT JOIN wr_employee_info e ON t.assignee_id = e.employee_id
|
||||
${whereClause}
|
||||
ORDER BY t.request_date DESC, t.task_id DESC
|
||||
LIMIT $${paramIndex++} OFFSET $${paramIndex++}
|
||||
`
|
||||
values.push(pageSize, offset)
|
||||
|
||||
const tasks = await query(listSql, values)
|
||||
|
||||
return {
|
||||
tasks: tasks.map((t: any) => ({
|
||||
taskId: t.task_id,
|
||||
projectId: t.project_id,
|
||||
projectName: t.project_name,
|
||||
requestDate: t.request_date,
|
||||
requestTitle: t.request_title,
|
||||
requestContent: t.request_content,
|
||||
requesterName: t.requester_name,
|
||||
requesterContact: t.requester_contact,
|
||||
taskType: t.task_type,
|
||||
priority: t.priority,
|
||||
status: t.status,
|
||||
assigneeId: t.assignee_id,
|
||||
assigneeName: t.assignee_name,
|
||||
devCompletedAt: t.dev_completed_at,
|
||||
opsCompletedAt: t.ops_completed_at,
|
||||
clientConfirmedAt: t.client_confirmed_at,
|
||||
createdAt: t.created_at
|
||||
})),
|
||||
pagination: {
|
||||
page,
|
||||
pageSize,
|
||||
total,
|
||||
totalPages: Math.ceil(total / pageSize)
|
||||
}
|
||||
}
|
||||
})
|
||||
64
server/api/maintenance/report/available.get.ts
Normal file
64
server/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
server/api/maintenance/report/generate-text.post.ts
Normal file
100
server/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
server/api/maintenance/report/link.post.ts
Normal file
78
server/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
|
||||
}
|
||||
})
|
||||
145
server/api/maintenance/stats.get.ts
Normal file
145
server/api/maintenance/stats.get.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import { query } from '../../utils/db'
|
||||
|
||||
/**
|
||||
* 유지보수 업무 통계
|
||||
* GET /api/maintenance/stats
|
||||
*/
|
||||
export default defineEventHandler(async (event) => {
|
||||
const params = getQuery(event)
|
||||
const projectId = params.projectId ? Number(params.projectId) : null
|
||||
const year = params.year ? Number(params.year) : new Date().getFullYear()
|
||||
const month = params.month ? Number(params.month) : null
|
||||
|
||||
const conditions: string[] = ['EXTRACT(YEAR FROM m.request_date) = $1']
|
||||
const values: any[] = [year]
|
||||
let paramIndex = 2
|
||||
|
||||
if (projectId) {
|
||||
conditions.push(`m.project_id = $${paramIndex++}`)
|
||||
values.push(projectId)
|
||||
}
|
||||
|
||||
if (month) {
|
||||
conditions.push(`EXTRACT(MONTH FROM m.request_date) = $${paramIndex++}`)
|
||||
values.push(month)
|
||||
}
|
||||
|
||||
const whereClause = conditions.join(' AND ')
|
||||
|
||||
// 상태별 통계
|
||||
const statusStats = await query(`
|
||||
SELECT
|
||||
m.status,
|
||||
COUNT(*) as count
|
||||
FROM wr_maintenance_task m
|
||||
WHERE ${whereClause}
|
||||
GROUP BY m.status
|
||||
`, values)
|
||||
|
||||
// 유형별 통계
|
||||
const typeStats = await query(`
|
||||
SELECT
|
||||
m.task_type,
|
||||
COUNT(*) as count
|
||||
FROM wr_maintenance_task m
|
||||
WHERE ${whereClause}
|
||||
GROUP BY m.task_type
|
||||
`, values)
|
||||
|
||||
// 우선순위별 통계
|
||||
const priorityStats = await query(`
|
||||
SELECT
|
||||
m.priority,
|
||||
COUNT(*) as count
|
||||
FROM wr_maintenance_task m
|
||||
WHERE ${whereClause}
|
||||
GROUP BY m.priority
|
||||
`, values)
|
||||
|
||||
// 월별 추이 (연간)
|
||||
const monthlyTrend = await query(`
|
||||
SELECT
|
||||
EXTRACT(MONTH FROM m.request_date) as month,
|
||||
COUNT(*) as total,
|
||||
COUNT(CASE WHEN m.status = 'COMPLETED' THEN 1 END) as completed
|
||||
FROM wr_maintenance_task m
|
||||
WHERE EXTRACT(YEAR FROM m.request_date) = $1
|
||||
${projectId ? 'AND m.project_id = $2' : ''}
|
||||
GROUP BY EXTRACT(MONTH FROM m.request_date)
|
||||
ORDER BY month
|
||||
`, projectId ? [year, projectId] : [year])
|
||||
|
||||
// 담당자별 통계
|
||||
const assigneeStats = await query(`
|
||||
SELECT
|
||||
e.employee_id,
|
||||
e.employee_name,
|
||||
COUNT(*) as total,
|
||||
COUNT(CASE WHEN m.status = 'COMPLETED' THEN 1 END) as completed
|
||||
FROM wr_maintenance_task m
|
||||
JOIN wr_employee_info e ON m.assignee_id = e.employee_id
|
||||
WHERE ${whereClause}
|
||||
GROUP BY e.employee_id, e.employee_name
|
||||
ORDER BY total DESC
|
||||
LIMIT 10
|
||||
`, values)
|
||||
|
||||
// 프로젝트별 통계
|
||||
const projectStats = await query(`
|
||||
SELECT
|
||||
p.project_id,
|
||||
p.project_name,
|
||||
COUNT(*) as total,
|
||||
COUNT(CASE WHEN m.status = 'COMPLETED' THEN 1 END) as completed
|
||||
FROM wr_maintenance_task m
|
||||
JOIN wr_project_info p ON m.project_id = p.project_id
|
||||
WHERE EXTRACT(YEAR FROM m.request_date) = $1
|
||||
${month ? 'AND EXTRACT(MONTH FROM m.request_date) = $2' : ''}
|
||||
GROUP BY p.project_id, p.project_name
|
||||
ORDER BY total DESC
|
||||
LIMIT 10
|
||||
`, month ? [year, month] : [year])
|
||||
|
||||
// 전체 합계
|
||||
const totalResult = await query(`
|
||||
SELECT
|
||||
COUNT(*) as total,
|
||||
COUNT(CASE WHEN m.status = 'COMPLETED' THEN 1 END) as completed,
|
||||
COUNT(CASE WHEN m.status = 'PENDING' THEN 1 END) as pending,
|
||||
COUNT(CASE WHEN m.status = 'IN_PROGRESS' THEN 1 END) as in_progress
|
||||
FROM wr_maintenance_task m
|
||||
WHERE ${whereClause}
|
||||
`, values)
|
||||
|
||||
return {
|
||||
year,
|
||||
month,
|
||||
projectId,
|
||||
summary: {
|
||||
total: Number(totalResult[0]?.total || 0),
|
||||
completed: Number(totalResult[0]?.completed || 0),
|
||||
pending: Number(totalResult[0]?.pending || 0),
|
||||
inProgress: Number(totalResult[0]?.in_progress || 0)
|
||||
},
|
||||
byStatus: statusStats.map((s: any) => ({ status: s.status, count: Number(s.count) })),
|
||||
byType: typeStats.map((t: any) => ({ taskType: t.task_type, count: Number(t.count) })),
|
||||
byPriority: priorityStats.map((p: any) => ({ priority: p.priority, count: Number(p.count) })),
|
||||
monthlyTrend: monthlyTrend.map((m: any) => ({
|
||||
month: Number(m.month),
|
||||
total: Number(m.total),
|
||||
completed: Number(m.completed)
|
||||
})),
|
||||
byAssignee: assigneeStats.map((a: any) => ({
|
||||
employeeId: a.employee_id,
|
||||
employeeName: a.employee_name,
|
||||
total: Number(a.total),
|
||||
completed: Number(a.completed)
|
||||
})),
|
||||
byProject: projectStats.map((p: any) => ({
|
||||
projectId: p.project_id,
|
||||
projectName: p.project_name,
|
||||
total: Number(p.total),
|
||||
completed: Number(p.completed)
|
||||
}))
|
||||
}
|
||||
})
|
||||
175
server/api/maintenance/upload.post.ts
Normal file
175
server/api/maintenance/upload.post.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
import { insertReturning, query, queryOne } from '../../utils/db'
|
||||
import { callOpenAI } from '../../utils/openai'
|
||||
import { getCurrentUserId } from '../../utils/user'
|
||||
import * as XLSX from 'xlsx'
|
||||
|
||||
/**
|
||||
* 유지보수 업무 엑셀/CSV 업로드 및 AI 파싱
|
||||
* POST /api/maintenance/upload
|
||||
*/
|
||||
export default defineEventHandler(async (event) => {
|
||||
const userId = await getCurrentUserId(event)
|
||||
|
||||
// multipart/form-data 처리
|
||||
const formData = await readMultipartFormData(event)
|
||||
if (!formData || formData.length === 0) {
|
||||
throw createError({ statusCode: 400, message: '파일을 업로드해주세요.' })
|
||||
}
|
||||
|
||||
const fileField = formData.find(f => f.name === 'file')
|
||||
const projectIdField = formData.find(f => f.name === 'projectId')
|
||||
|
||||
if (!fileField || !fileField.data) {
|
||||
throw createError({ statusCode: 400, message: '파일이 필요합니다.' })
|
||||
}
|
||||
|
||||
const projectId = projectIdField?.data ? Number(projectIdField.data.toString()) : null
|
||||
|
||||
// 파일 확장자 확인
|
||||
const filename = fileField.filename || ''
|
||||
const ext = filename.split('.').pop()?.toLowerCase()
|
||||
|
||||
if (!['xlsx', 'xls', 'csv'].includes(ext || '')) {
|
||||
throw createError({ statusCode: 400, message: '엑셀(.xlsx, .xls) 또는 CSV 파일만 지원합니다.' })
|
||||
}
|
||||
|
||||
// SheetJS로 파싱
|
||||
let rows: any[] = []
|
||||
try {
|
||||
const workbook = XLSX.read(fileField.data, { type: 'buffer' })
|
||||
const sheetName = workbook.SheetNames[0]
|
||||
const sheet = workbook.Sheets[sheetName]
|
||||
rows = XLSX.utils.sheet_to_json(sheet, { header: 1, defval: '' })
|
||||
} catch (e) {
|
||||
throw createError({ statusCode: 400, message: '파일 파싱에 실패했습니다.' })
|
||||
}
|
||||
|
||||
if (rows.length < 2) {
|
||||
throw createError({ statusCode: 400, message: '데이터가 없습니다. (헤더 + 최소 1행)' })
|
||||
}
|
||||
|
||||
// 헤더와 데이터 분리
|
||||
const headers = rows[0] as string[]
|
||||
const dataRows = rows.slice(1).filter((r: any[]) => r.some(cell => cell !== ''))
|
||||
|
||||
if (dataRows.length === 0) {
|
||||
throw createError({ statusCode: 400, message: '데이터 행이 없습니다.' })
|
||||
}
|
||||
|
||||
// OpenAI로 컬럼 매핑 분석
|
||||
const prompt = `다음은 유지보수 업무 목록 엑셀의 헤더입니다:
|
||||
${JSON.stringify(headers)}
|
||||
|
||||
그리고 샘플 데이터 3행입니다:
|
||||
${JSON.stringify(dataRows.slice(0, 3))}
|
||||
|
||||
각 컬럼이 다음 필드 중 어느 것에 해당하는지 매핑해주세요:
|
||||
- request_date: 요청일 (날짜)
|
||||
- request_title: 요청 제목/건명
|
||||
- request_content: 요청 내용/상세
|
||||
- requester_name: 요청자 이름
|
||||
- requester_contact: 요청자 연락처/이메일/전화
|
||||
- task_type: 유형 (bug/feature/inquiry/other)
|
||||
- priority: 우선순위 (high/medium/low)
|
||||
- resolution_content: 조치 내용/처리 결과
|
||||
|
||||
JSON 형식으로 응답 (인덱스 기반):
|
||||
{
|
||||
"mapping": {
|
||||
"request_date": 0,
|
||||
"request_title": 1,
|
||||
...
|
||||
},
|
||||
"confidence": 0.9
|
||||
}`
|
||||
|
||||
let mapping: Record<string, number> = {}
|
||||
try {
|
||||
const response = await callOpenAI([
|
||||
{ role: 'system', content: '엑셀 컬럼 매핑 전문가입니다. 정확하게 필드를 매핑합니다.' },
|
||||
{ role: 'user', content: prompt }
|
||||
], true)
|
||||
const parsed = JSON.parse(response)
|
||||
mapping = parsed.mapping || {}
|
||||
} catch (e) {
|
||||
console.error('OpenAI mapping error:', e)
|
||||
// 기본 매핑 시도 (순서대로)
|
||||
mapping = { request_date: 0, request_title: 1, request_content: 2 }
|
||||
}
|
||||
|
||||
// 배치 ID 생성
|
||||
const batchResult = await queryOne<{ nextval: string }>(`SELECT nextval('wr_maintenance_batch_seq')`)
|
||||
const batchId = Number(batchResult?.nextval || Date.now())
|
||||
|
||||
// 데이터 변환
|
||||
const parsedTasks = dataRows.map((row: any[], idx: number) => {
|
||||
const getValue = (field: string) => {
|
||||
const colIdx = mapping[field]
|
||||
if (colIdx === undefined || colIdx === null) return null
|
||||
return row[colIdx]?.toString().trim() || null
|
||||
}
|
||||
|
||||
// 날짜 파싱
|
||||
let requestDate = getValue('request_date')
|
||||
if (requestDate) {
|
||||
// 엑셀 시리얼 넘버 처리
|
||||
if (!isNaN(Number(requestDate))) {
|
||||
const excelDate = XLSX.SSF.parse_date_code(Number(requestDate))
|
||||
if (excelDate) {
|
||||
requestDate = `${excelDate.y}-${String(excelDate.m).padStart(2,'0')}-${String(excelDate.d).padStart(2,'0')}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 유형 정규화
|
||||
let taskType = getValue('task_type')?.toLowerCase() || 'other'
|
||||
if (taskType.includes('버그') || taskType.includes('오류') || taskType.includes('bug')) taskType = 'bug'
|
||||
else if (taskType.includes('기능') || taskType.includes('개선') || taskType.includes('feature')) taskType = 'feature'
|
||||
else if (taskType.includes('문의') || taskType.includes('inquiry')) taskType = 'inquiry'
|
||||
else taskType = 'other'
|
||||
|
||||
// 우선순위 정규화
|
||||
let priority = getValue('priority')?.toLowerCase() || 'medium'
|
||||
if (priority.includes('높') || priority.includes('긴급') || priority.includes('high')) priority = 'high'
|
||||
else if (priority.includes('낮') || priority.includes('low')) priority = 'low'
|
||||
else priority = 'medium'
|
||||
|
||||
return {
|
||||
rowIndex: idx + 2, // 1-based + 헤더
|
||||
requestDate,
|
||||
requestTitle: getValue('request_title') || `업무 ${idx + 1}`,
|
||||
requestContent: getValue('request_content'),
|
||||
requesterName: getValue('requester_name'),
|
||||
requesterContact: getValue('requester_contact'),
|
||||
taskType,
|
||||
priority,
|
||||
resolutionContent: getValue('resolution_content'),
|
||||
isDuplicate: false
|
||||
}
|
||||
})
|
||||
|
||||
// 중복 감지 (같은 제목 + 같은 날짜)
|
||||
if (projectId) {
|
||||
for (const task of parsedTasks) {
|
||||
if (task.requestTitle && task.requestDate) {
|
||||
const dup = await queryOne(`
|
||||
SELECT task_id FROM wr_maintenance_task
|
||||
WHERE project_id = $1 AND request_title = $2 AND request_date = $3
|
||||
`, [projectId, task.requestTitle, task.requestDate])
|
||||
if (dup) {
|
||||
task.isDuplicate = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
batchId,
|
||||
filename,
|
||||
totalRows: dataRows.length,
|
||||
mapping,
|
||||
headers,
|
||||
tasks: parsedTasks
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user