작업계획서대로 진행

This commit is contained in:
2026-01-11 01:29:46 +09:00
parent 1b8cd8577e
commit 01bd66d524
51 changed files with 11124 additions and 273 deletions

View File

@@ -0,0 +1,30 @@
import { execute, queryOne } from '../../../utils/db'
/**
* 사업 주간보고 확정
* PUT /api/business-report/[id]/confirm
*/
export default defineEventHandler(async (event) => {
const businessReportId = Number(getRouterParam(event, 'id'))
const existing = await queryOne(`
SELECT * FROM wr_business_weekly_report WHERE business_report_id = $1
`, [businessReportId])
if (!existing) {
throw createError({ statusCode: 404, message: '보고서를 찾을 수 없습니다.' })
}
if (existing.status === 'confirmed') {
throw createError({ statusCode: 400, message: '이미 확정된 보고서입니다.' })
}
await execute(`
UPDATE wr_business_weekly_report SET
status = 'confirmed',
updated_at = NOW()
WHERE business_report_id = $1
`, [businessReportId])
return { success: true, message: '보고서가 확정되었습니다.' }
})

View File

@@ -0,0 +1,80 @@
import { query, queryOne } from '../../../utils/db'
/**
* 사업 주간보고 상세 조회
* GET /api/business-report/[id]/detail
*/
export default defineEventHandler(async (event) => {
const businessReportId = Number(getRouterParam(event, 'id'))
const report = await queryOne(`
SELECT
br.*,
b.business_name,
e.employee_name as created_by_name
FROM wr_business_weekly_report br
JOIN wr_business b ON br.business_id = b.business_id
LEFT JOIN wr_employee_info e ON br.created_by = e.employee_id
WHERE br.business_report_id = $1
`, [businessReportId])
if (!report) {
throw createError({ statusCode: 404, message: '보고서를 찾을 수 없습니다.' })
}
// 소속 프로젝트 목록
const projects = await query(`
SELECT project_id, project_name FROM wr_project_info WHERE business_id = $1
`, [report.business_id])
const projectIds = projects.map((p: any) => p.project_id)
// 해당 주차 실적 목록
const tasks = await query(`
SELECT
t.task_id,
t.task_description,
t.task_type,
t.task_hours,
p.project_id,
p.project_name,
e.employee_id,
e.employee_name
FROM wr_weekly_report_task t
JOIN wr_weekly_report r ON t.report_id = r.report_id
JOIN wr_project_info p ON t.project_id = p.project_id
JOIN wr_employee_info e ON r.author_id = e.employee_id
WHERE t.project_id = ANY($1)
AND r.report_year = $2
AND r.report_week = $3
ORDER BY p.project_name, e.employee_name
`, [projectIds, report.report_year, report.report_week])
return {
report: {
businessReportId: report.business_report_id,
businessId: report.business_id,
businessName: report.business_name,
reportYear: report.report_year,
reportWeek: report.report_week,
weekStartDate: report.week_start_date,
weekEndDate: report.week_end_date,
aiSummary: report.ai_summary,
manualSummary: report.manual_summary,
status: report.status,
createdByName: report.created_by_name,
createdAt: report.created_at,
updatedAt: report.updated_at
},
tasks: tasks.map((t: any) => ({
taskId: t.task_id,
taskDescription: t.task_description,
taskType: t.task_type,
taskHours: t.task_hours,
projectId: t.project_id,
projectName: t.project_name,
employeeId: t.employee_id,
employeeName: t.employee_name
}))
}
})

View File

@@ -0,0 +1,31 @@
import { execute, queryOne } from '../../../utils/db'
/**
* 사업 주간보고 수정
* PUT /api/business-report/[id]/update
*/
export default defineEventHandler(async (event) => {
const businessReportId = Number(getRouterParam(event, 'id'))
const body = await readBody<{ manualSummary: string }>(event)
const existing = await queryOne(`
SELECT * FROM wr_business_weekly_report WHERE business_report_id = $1
`, [businessReportId])
if (!existing) {
throw createError({ statusCode: 404, message: '보고서를 찾을 수 없습니다.' })
}
if (existing.status === 'confirmed') {
throw createError({ statusCode: 400, message: '확정된 보고서는 수정할 수 없습니다.' })
}
await execute(`
UPDATE wr_business_weekly_report SET
manual_summary = $1,
updated_at = NOW()
WHERE business_report_id = $2
`, [body.manualSummary, businessReportId])
return { success: true }
})

View File

@@ -0,0 +1,148 @@
import { query, queryOne, insertReturning, execute } from '../../utils/db'
import { callOpenAI } from '../../utils/openai'
import { getCurrentUserId } from '../../utils/user'
interface GenerateBody {
businessId: number
reportYear: number
reportWeek: number
weekStartDate: string
weekEndDate: string
}
/**
* 사업 주간보고 취합 생성
* POST /api/business-report/generate
*/
export default defineEventHandler(async (event) => {
const body = await readBody<GenerateBody>(event)
const userId = await getCurrentUserId(event)
if (!body.businessId || !body.reportYear || !body.reportWeek) {
throw createError({ statusCode: 400, message: '필수 파라미터가 누락되었습니다.' })
}
// 기존 보고서 확인
const existing = await queryOne(`
SELECT * FROM wr_business_weekly_report
WHERE business_id = $1 AND report_year = $2 AND report_week = $3
`, [body.businessId, body.reportYear, body.reportWeek])
// 사업에 속한 프로젝트 목록
const projects = await query(`
SELECT project_id, project_name FROM wr_project_info WHERE business_id = $1
`, [body.businessId])
if (projects.length === 0) {
throw createError({ statusCode: 400, message: '해당 사업에 속한 프로젝트가 없습니다.' })
}
const projectIds = projects.map((p: any) => p.project_id)
// 해당 주차 주간보고 실적 조회
const tasks = await query(`
SELECT
t.task_description,
t.task_type,
t.task_hours,
p.project_name,
e.employee_name
FROM wr_weekly_report_task t
JOIN wr_weekly_report r ON t.report_id = r.report_id
JOIN wr_project_info p ON t.project_id = p.project_id
JOIN wr_employee_info e ON r.author_id = e.employee_id
WHERE t.project_id = ANY($1)
AND r.report_year = $2
AND r.report_week = $3
ORDER BY p.project_name, e.employee_name
`, [projectIds, body.reportYear, body.reportWeek])
if (tasks.length === 0) {
throw createError({ statusCode: 400, message: '해당 주차에 등록된 실적이 없습니다.' })
}
// 프로젝트별로 그룹화
const groupedTasks: Record<string, any[]> = {}
for (const task of tasks) {
const key = task.project_name
if (!groupedTasks[key]) groupedTasks[key] = []
groupedTasks[key].push(task)
}
// OpenAI 프롬프트 생성
let taskText = ''
for (const [projectName, projectTasks] of Object.entries(groupedTasks)) {
taskText += `\n[${projectName}]\n`
for (const t of projectTasks) {
taskText += `- ${t.employee_name}: ${t.task_description}\n`
}
}
const prompt = `다음은 사업의 주간 실적입니다. 이를 경영진에게 보고하기 위한 간결한 요약문을 작성해주세요.
${taskText}
요약 작성 가이드:
1. 프로젝트별로 구분하여 작성
2. 핵심 성과와 진행 상황 중심
3. 한국어로 작성
4. 불릿 포인트 형식
5. 200자 이내로 간결하게
JSON 형식으로 응답해주세요:
{
"summary": "요약 내용"
}`
let aiSummary = ''
try {
const response = await callOpenAI([
{ role: 'system', content: '당신은 프로젝트 관리 전문가입니다. 주간 실적을 간결하게 요약합니다.' },
{ role: 'user', content: prompt }
], true)
const parsed = JSON.parse(response)
aiSummary = parsed.summary || response
} catch (e) {
console.error('OpenAI error:', e)
aiSummary = '(AI 요약 생성 실패)'
}
let result
if (existing) {
// 업데이트
await execute(`
UPDATE wr_business_weekly_report SET
ai_summary = $1,
updated_at = NOW()
WHERE business_report_id = $2
`, [aiSummary, existing.business_report_id])
result = { ...existing, ai_summary: aiSummary }
} else {
// 신규 생성
result = await insertReturning(`
INSERT INTO wr_business_weekly_report (
business_id, report_year, report_week, week_start_date, week_end_date,
ai_summary, status, created_by
) VALUES ($1, $2, $3, $4, $5, $6, 'draft', $7)
RETURNING *
`, [
body.businessId,
body.reportYear,
body.reportWeek,
body.weekStartDate,
body.weekEndDate,
aiSummary,
userId
])
}
return {
success: true,
report: {
businessReportId: result.business_report_id,
aiSummary: result.ai_summary || aiSummary,
status: result.status || 'draft'
}
}
})

View File

@@ -0,0 +1,51 @@
import { query } from '../../utils/db'
/**
* 사업 주간보고 목록 조회
* GET /api/business-report/list
*/
export default defineEventHandler(async (event) => {
const params = getQuery(event)
const businessId = params.businessId ? Number(params.businessId) : null
const year = params.year ? Number(params.year) : new Date().getFullYear()
let sql = `
SELECT
br.*,
b.business_name,
e.employee_name as created_by_name
FROM wr_business_weekly_report br
JOIN wr_business b ON br.business_id = b.business_id
LEFT JOIN wr_employee_info e ON br.created_by = e.employee_id
WHERE br.report_year = $1
`
const queryParams: any[] = [year]
let paramIndex = 2
if (businessId) {
sql += ` AND br.business_id = $${paramIndex++}`
queryParams.push(businessId)
}
sql += ' ORDER BY br.report_week DESC, br.business_id'
const reports = await query(sql, queryParams)
return {
reports: reports.map((r: any) => ({
businessReportId: r.business_report_id,
businessId: r.business_id,
businessName: r.business_name,
reportYear: r.report_year,
reportWeek: r.report_week,
weekStartDate: r.week_start_date,
weekEndDate: r.week_end_date,
aiSummary: r.ai_summary,
manualSummary: r.manual_summary,
status: r.status,
createdByName: r.created_by_name,
createdAt: r.created_at,
updatedAt: r.updated_at
}))
}
})

View File

@@ -0,0 +1,44 @@
import { queryOne, execute } from '../../../utils/db'
/**
* 사업 삭제 (상태를 suspended로 변경)
* DELETE /api/business/[id]/delete
*/
export default defineEventHandler(async (event) => {
const businessId = Number(getRouterParam(event, 'id'))
if (!businessId) {
throw createError({ statusCode: 400, message: '사업 ID가 필요합니다.' })
}
const existing = await queryOne(`
SELECT business_id FROM wr_business WHERE business_id = $1
`, [businessId])
if (!existing) {
throw createError({ statusCode: 404, message: '사업을 찾을 수 없습니다.' })
}
// 소속 프로젝트 체크
const projectCount = await queryOne(`
SELECT COUNT(*) as cnt FROM wr_project_info WHERE business_id = $1
`, [businessId])
if (Number(projectCount?.cnt) > 0) {
throw createError({
statusCode: 400,
message: `소속된 프로젝트가 ${projectCount.cnt}개 있습니다. 먼저 프로젝트를 해제하세요.`
})
}
// 완전 삭제 대신 상태 변경
await execute(`
UPDATE wr_business SET business_status = 'suspended', updated_at = NOW()
WHERE business_id = $1
`, [businessId])
return {
success: true,
message: '사업이 삭제(중단) 처리되었습니다.'
}
})

View File

@@ -0,0 +1,71 @@
import { query, queryOne } from '../../../utils/db'
/**
* 사업 상세 조회
* GET /api/business/[id]/detail
*/
export default defineEventHandler(async (event) => {
const businessId = Number(getRouterParam(event, 'id'))
if (!businessId) {
throw createError({ statusCode: 400, message: '사업 ID가 필요합니다.' })
}
const business = await queryOne(`
SELECT
b.*,
e1.employee_name as created_by_name,
e2.employee_name as updated_by_name
FROM wr_business b
LEFT JOIN wr_employee_info e1 ON b.created_by = e1.employee_id
LEFT JOIN wr_employee_info e2 ON b.updated_by = e2.employee_id
WHERE b.business_id = $1
`, [businessId])
if (!business) {
throw createError({ statusCode: 404, message: '사업을 찾을 수 없습니다.' })
}
// 소속 프로젝트 목록
const projects = await query(`
SELECT
p.project_id,
p.project_name,
p.project_code,
p.project_type,
p.project_status,
p.start_date,
p.end_date
FROM wr_project_info p
WHERE p.business_id = $1
ORDER BY p.project_name
`, [businessId])
return {
business: {
businessId: business.business_id,
businessName: business.business_name,
businessCode: business.business_code,
clientName: business.client_name,
contractStartDate: business.contract_start_date,
contractEndDate: business.contract_end_date,
businessStatus: business.business_status,
description: business.description,
createdBy: business.created_by,
createdByName: business.created_by_name,
updatedBy: business.updated_by,
updatedByName: business.updated_by_name,
createdAt: business.created_at,
updatedAt: business.updated_at
},
projects: projects.map((p: any) => ({
projectId: p.project_id,
projectName: p.project_name,
projectCode: p.project_code,
projectType: p.project_type,
projectStatus: p.project_status,
startDate: p.start_date,
endDate: p.end_date
}))
}
})

View File

@@ -0,0 +1,68 @@
import { queryOne, execute } from '../../../utils/db'
import { getCurrentUserId } from '../../../utils/user'
interface UpdateBusinessBody {
businessName: string
businessCode?: string
clientName?: string
contractStartDate?: string
contractEndDate?: string
businessStatus?: string
description?: string
}
/**
* 사업 수정
* PUT /api/business/[id]/update
*/
export default defineEventHandler(async (event) => {
const businessId = Number(getRouterParam(event, 'id'))
const body = await readBody<UpdateBusinessBody>(event)
const userId = await getCurrentUserId(event)
if (!businessId) {
throw createError({ statusCode: 400, message: '사업 ID가 필요합니다.' })
}
if (!body.businessName) {
throw createError({ statusCode: 400, message: '사업명은 필수입니다.' })
}
const existing = await queryOne(`
SELECT business_id FROM wr_business WHERE business_id = $1
`, [businessId])
if (!existing) {
throw createError({ statusCode: 404, message: '사업을 찾을 수 없습니다.' })
}
await execute(`
UPDATE wr_business SET
business_name = $1,
business_code = $2,
client_name = $3,
contract_start_date = $4,
contract_end_date = $5,
business_status = $6,
description = $7,
updated_at = NOW(),
updated_by = $8
WHERE business_id = $9
`, [
body.businessName,
body.businessCode || null,
body.clientName || null,
body.contractStartDate || null,
body.contractEndDate || null,
body.businessStatus || 'active',
body.description || null,
userId,
businessId
])
return {
success: true,
businessId,
message: '사업이 수정되었습니다.'
}
})

View File

@@ -0,0 +1,47 @@
import { insertReturning } from '../../utils/db'
import { getCurrentUserId } from '../../utils/user'
interface CreateBusinessBody {
businessName: string
businessCode?: string
clientName?: string
contractStartDate?: string
contractEndDate?: string
description?: string
}
/**
* 사업 생성
* POST /api/business/create
*/
export default defineEventHandler(async (event) => {
const body = await readBody<CreateBusinessBody>(event)
const userId = await getCurrentUserId(event)
if (!body.businessName) {
throw createError({ statusCode: 400, message: '사업명은 필수입니다.' })
}
const business = await insertReturning(`
INSERT INTO wr_business (
business_name, business_code, client_name,
contract_start_date, contract_end_date, description,
business_status, created_by, updated_by
) VALUES ($1, $2, $3, $4, $5, $6, 'active', $7, $7)
RETURNING *
`, [
body.businessName,
body.businessCode || null,
body.clientName || null,
body.contractStartDate || null,
body.contractEndDate || null,
body.description || null,
userId
])
return {
success: true,
businessId: business.business_id,
message: '사업이 등록되었습니다.'
}
})

View File

@@ -0,0 +1,77 @@
import { query } from '../../utils/db'
/**
* 사업 목록 조회
* GET /api/business/list
*
* Query params:
* - status: 상태 필터 (active, completed, suspended)
* - businessName: 사업명 검색
* - businessCode: 사업코드 검색
* - clientName: 발주처 검색
*/
export default defineEventHandler(async (event) => {
const params = getQuery(event)
const status = params.status as string | null
const businessName = params.businessName as string | null
const businessCode = params.businessCode as string | null
const clientName = params.clientName as string | null
const conditions: string[] = []
const values: any[] = []
let paramIndex = 1
if (status) {
conditions.push(`b.business_status = $${paramIndex++}`)
values.push(status)
}
if (businessName) {
conditions.push(`b.business_name ILIKE $${paramIndex++}`)
values.push(`%${businessName}%`)
}
if (businessCode) {
conditions.push(`b.business_code ILIKE $${paramIndex++}`)
values.push(`%${businessCode}%`)
}
if (clientName) {
conditions.push(`b.client_name ILIKE $${paramIndex++}`)
values.push(`%${clientName}%`)
}
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''
const sql = `
SELECT
b.*,
e.employee_name as created_by_name,
(SELECT COUNT(*) FROM wr_project_info WHERE business_id = b.business_id) as project_count
FROM wr_business b
LEFT JOIN wr_employee_info e ON b.created_by = e.employee_id
${whereClause}
ORDER BY b.created_at DESC
`
const businesses = await query(sql, values)
return {
businesses: businesses.map((b: any) => ({
businessId: b.business_id,
businessName: b.business_name,
businessCode: b.business_code,
clientName: b.client_name,
contractStartDate: b.contract_start_date,
contractEndDate: b.contract_end_date,
businessStatus: b.business_status,
description: b.description,
projectCount: Number(b.project_count),
createdBy: b.created_by,
createdByName: b.created_by_name,
createdAt: b.created_at,
updatedAt: b.updated_at
}))
}
})

View 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: '유지보수 업무가 삭제되었습니다.'
}
})

View 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
}
}
})

View 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: '상태가 변경되었습니다.'
}
})

View 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: '유지보수 업무가 수정되었습니다.'
}
})

View 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
}
})

View 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: '유지보수 업무가 등록되었습니다.'
}
})

View 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)
}
}
})

View 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
}
})

View File

@@ -0,0 +1,30 @@
import { queryOne, execute } from '../../../utils/db'
/**
* 회의록 삭제
* DELETE /api/meeting/[id]/delete
*/
export default defineEventHandler(async (event) => {
const meetingId = Number(getRouterParam(event, 'id'))
if (!meetingId) {
throw createError({ statusCode: 400, message: '회의록 ID가 필요합니다.' })
}
// 회의록 존재 확인
const existing = await queryOne(`
SELECT meeting_id FROM wr_meeting WHERE meeting_id = $1
`, [meetingId])
if (!existing) {
throw createError({ statusCode: 404, message: '회의록을 찾을 수 없습니다.' })
}
// CASCADE 설정으로 참석자, 안건도 함께 삭제됨
await execute(`DELETE FROM wr_meeting WHERE meeting_id = $1`, [meetingId])
return {
success: true,
message: '회의록이 삭제되었습니다.'
}
})

View File

@@ -0,0 +1,96 @@
import { query, queryOne } from '../../../utils/db'
/**
* 회의록 상세 조회
* GET /api/meeting/[id]/detail
*/
export default defineEventHandler(async (event) => {
const meetingId = Number(getRouterParam(event, 'id'))
if (!meetingId) {
throw createError({ statusCode: 400, message: '회의록 ID가 필요합니다.' })
}
// 회의록 기본 정보
const meeting = await queryOne(`
SELECT
m.*,
p.project_name,
e.employee_name as author_name,
e.employee_email as author_email
FROM wr_meeting m
LEFT JOIN wr_project_info p ON m.project_id = p.project_id
LEFT JOIN wr_employee_info e ON m.author_id = e.employee_id
WHERE m.meeting_id = $1
`, [meetingId])
if (!meeting) {
throw createError({ statusCode: 404, message: '회의록을 찾을 수 없습니다.' })
}
// 참석자 목록
const attendees = await query(`
SELECT
a.attendee_id,
a.employee_id,
e.employee_name,
e.employee_email,
e.company,
a.external_name,
a.external_company
FROM wr_meeting_attendee a
LEFT JOIN wr_employee_info e ON a.employee_id = e.employee_id
WHERE a.meeting_id = $1
ORDER BY a.attendee_id
`, [meetingId])
// 안건 목록 (AI 분석 결과)
const agendas = await query(`
SELECT *
FROM wr_meeting_agenda
WHERE meeting_id = $1
ORDER BY agenda_no
`, [meetingId])
return {
meeting: {
meetingId: meeting.meeting_id,
meetingTitle: meeting.meeting_title,
meetingType: meeting.meeting_type,
projectId: meeting.project_id,
projectName: meeting.project_name,
meetingDate: meeting.meeting_date,
startTime: meeting.start_time,
endTime: meeting.end_time,
location: meeting.location,
rawContent: meeting.raw_content,
aiSummary: meeting.ai_summary,
aiStatus: meeting.ai_status,
aiProcessedAt: meeting.ai_processed_at,
aiConfirmedAt: meeting.ai_confirmed_at,
authorId: meeting.author_id,
authorName: meeting.author_name,
authorEmail: meeting.author_email,
createdAt: meeting.created_at,
updatedAt: meeting.updated_at
},
attendees: attendees.map((a: any) => ({
attendeeId: a.attendee_id,
employeeId: a.employee_id,
employeeName: a.employee_name,
employeeEmail: a.employee_email,
company: a.company,
externalName: a.external_name,
externalCompany: a.external_company,
isExternal: !a.employee_id
})),
agendas: agendas.map((a: any) => ({
agendaId: a.agenda_id,
agendaNo: a.agenda_no,
agendaTitle: a.agenda_title,
agendaContent: a.agenda_content,
decisionStatus: a.decision_status,
decisionContent: a.decision_content
}))
}
})

View File

@@ -0,0 +1,107 @@
import { queryOne, execute } from '../../../utils/db'
import { getClientIp } from '../../../utils/ip'
import { getCurrentUserEmail } from '../../../utils/user'
interface Attendee {
employeeId?: number
externalName?: string
externalCompany?: string
}
interface UpdateMeetingBody {
meetingTitle: string
meetingType: 'PROJECT' | 'INTERNAL'
projectId?: number
meetingDate: string
startTime?: string
endTime?: string
location?: string
rawContent?: string
attendees?: Attendee[]
}
/**
* 회의록 수정
* PUT /api/meeting/[id]/update
*/
export default defineEventHandler(async (event) => {
const meetingId = Number(getRouterParam(event, 'id'))
const body = await readBody<UpdateMeetingBody>(event)
const clientIp = getClientIp(event)
const userEmail = await getCurrentUserEmail(event)
if (!meetingId) {
throw createError({ statusCode: 400, message: '회의록 ID가 필요합니다.' })
}
// 회의록 존재 확인
const existing = await queryOne(`
SELECT meeting_id FROM wr_meeting WHERE meeting_id = $1
`, [meetingId])
if (!existing) {
throw createError({ statusCode: 404, message: '회의록을 찾을 수 없습니다.' })
}
// 필수값 검증
if (!body.meetingTitle) {
throw createError({ statusCode: 400, message: '회의 제목은 필수입니다.' })
}
if (body.meetingType === 'PROJECT' && !body.projectId) {
throw createError({ statusCode: 400, message: '프로젝트 회의는 프로젝트 선택이 필수입니다.' })
}
// 회의록 UPDATE
await execute(`
UPDATE wr_meeting SET
meeting_title = $1,
meeting_type = $2,
project_id = $3,
meeting_date = $4,
start_time = $5,
end_time = $6,
location = $7,
raw_content = $8,
updated_at = NOW(),
updated_ip = $9,
updated_email = $10
WHERE meeting_id = $11
`, [
body.meetingTitle,
body.meetingType,
body.meetingType === 'PROJECT' ? body.projectId : null,
body.meetingDate,
body.startTime || null,
body.endTime || null,
body.location || null,
body.rawContent || null,
clientIp,
userEmail,
meetingId
])
// 참석자 갱신 (기존 삭제 후 새로 INSERT)
await execute(`DELETE FROM wr_meeting_attendee WHERE meeting_id = $1`, [meetingId])
if (body.attendees && body.attendees.length > 0) {
for (const att of body.attendees) {
if (att.employeeId) {
await execute(`
INSERT INTO wr_meeting_attendee (meeting_id, employee_id)
VALUES ($1, $2)
`, [meetingId, att.employeeId])
} else if (att.externalName) {
await execute(`
INSERT INTO wr_meeting_attendee (meeting_id, external_name, external_company)
VALUES ($1, $2, $3)
`, [meetingId, att.externalName, att.externalCompany || null])
}
}
}
return {
success: true,
meetingId,
message: '회의록이 수정되었습니다.'
}
})

View File

@@ -0,0 +1,92 @@
import { insertReturning, query, execute } from '../../utils/db'
import { getClientIp } from '../../utils/ip'
import { getCurrentUserEmail, getCurrentUserId } from '../../utils/user'
interface Attendee {
employeeId?: number
externalName?: string
externalCompany?: string
}
interface CreateMeetingBody {
meetingTitle: string
meetingType: 'PROJECT' | 'INTERNAL'
projectId?: number
meetingDate: string
startTime?: string
endTime?: string
location?: string
rawContent?: string
attendees?: Attendee[]
}
/**
* 회의록 작성
* POST /api/meeting/create
*/
export default defineEventHandler(async (event) => {
const body = await readBody<CreateMeetingBody>(event)
const clientIp = getClientIp(event)
const userEmail = await getCurrentUserEmail(event)
const userId = await getCurrentUserId(event)
// 필수값 검증
if (!body.meetingTitle) {
throw createError({ statusCode: 400, message: '회의 제목은 필수입니다.' })
}
if (!body.meetingType) {
throw createError({ statusCode: 400, message: '회의 유형은 필수입니다.' })
}
if (!body.meetingDate) {
throw createError({ statusCode: 400, message: '회의 일자는 필수입니다.' })
}
if (body.meetingType === 'PROJECT' && !body.projectId) {
throw createError({ statusCode: 400, message: '프로젝트 회의는 프로젝트 선택이 필수입니다.' })
}
// 회의록 INSERT
const meeting = await insertReturning(`
INSERT INTO wr_meeting (
meeting_title, meeting_type, project_id,
meeting_date, start_time, end_time, location,
raw_content, ai_status, author_id,
created_ip, created_email, updated_ip, updated_email
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, 'NONE', $9, $10, $11, $10, $11)
RETURNING *
`, [
body.meetingTitle,
body.meetingType,
body.meetingType === 'PROJECT' ? body.projectId : null,
body.meetingDate,
body.startTime || null,
body.endTime || null,
body.location || null,
body.rawContent || null,
userId,
clientIp,
userEmail
])
// 참석자 INSERT
if (body.attendees && body.attendees.length > 0) {
for (const att of body.attendees) {
if (att.employeeId) {
await execute(`
INSERT INTO wr_meeting_attendee (meeting_id, employee_id)
VALUES ($1, $2)
`, [meeting.meeting_id, att.employeeId])
} else if (att.externalName) {
await execute(`
INSERT INTO wr_meeting_attendee (meeting_id, external_name, external_company)
VALUES ($1, $2, $3)
`, [meeting.meeting_id, att.externalName, att.externalCompany || null])
}
}
}
return {
success: true,
meetingId: meeting.meeting_id,
message: '회의록이 등록되었습니다.'
}
})

View File

@@ -0,0 +1,122 @@
import { query } from '../../utils/db'
/**
* 회의록 목록 조회
* GET /api/meeting/list
*
* Query params:
* - projectId: 프로젝트 필터 (선택)
* - meetingType: PROJECT | INTERNAL (선택)
* - startDate: 시작일 (선택)
* - endDate: 종료일 (선택)
* - keyword: 검색어 (선택)
* - page: 페이지 번호 (기본 1)
* - pageSize: 페이지 크기 (기본 20)
*/
export default defineEventHandler(async (event) => {
const params = getQuery(event)
const projectId = params.projectId ? Number(params.projectId) : null
const meetingType = params.meetingType as string | null
const startDate = params.startDate as string | null
const endDate = params.endDate as string | null
const keyword = params.keyword as string | null
const page = Number(params.page) || 1
const pageSize = Number(params.pageSize) || 20
const offset = (page - 1) * pageSize
// WHERE 조건 구성
const conditions: string[] = []
const values: any[] = []
let paramIndex = 1
if (projectId) {
conditions.push(`m.project_id = $${paramIndex++}`)
values.push(projectId)
}
if (meetingType) {
conditions.push(`m.meeting_type = $${paramIndex++}`)
values.push(meetingType)
}
if (startDate) {
conditions.push(`m.meeting_date >= $${paramIndex++}`)
values.push(startDate)
}
if (endDate) {
conditions.push(`m.meeting_date <= $${paramIndex++}`)
values.push(endDate)
}
if (keyword) {
conditions.push(`(m.meeting_title ILIKE $${paramIndex} OR m.raw_content ILIKE $${paramIndex})`)
values.push(`%${keyword}%`)
paramIndex++
}
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''
// 전체 건수 조회
const countSql = `
SELECT COUNT(*) as total
FROM wr_meeting m
${whereClause}
`
const countResult = await query(countSql, values)
const total = Number(countResult[0]?.total || 0)
// 목록 조회
const listSql = `
SELECT
m.meeting_id,
m.meeting_title,
m.meeting_type,
m.project_id,
p.project_name,
m.meeting_date,
m.start_time,
m.end_time,
m.location,
m.ai_status,
m.author_id,
e.employee_name as author_name,
m.created_at,
(SELECT COUNT(*) FROM wr_meeting_attendee WHERE meeting_id = m.meeting_id) as attendee_count
FROM wr_meeting m
LEFT JOIN wr_project_info p ON m.project_id = p.project_id
LEFT JOIN wr_employee_info e ON m.author_id = e.employee_id
${whereClause}
ORDER BY m.meeting_date DESC, m.created_at DESC
LIMIT $${paramIndex++} OFFSET $${paramIndex++}
`
values.push(pageSize, offset)
const meetings = await query(listSql, values)
return {
meetings: meetings.map((m: any) => ({
meetingId: m.meeting_id,
meetingTitle: m.meeting_title,
meetingType: m.meeting_type,
projectId: m.project_id,
projectName: m.project_name,
meetingDate: m.meeting_date,
startTime: m.start_time,
endTime: m.end_time,
location: m.location,
aiStatus: m.ai_status,
authorId: m.author_id,
authorName: m.author_name,
attendeeCount: Number(m.attendee_count),
createdAt: m.created_at
})),
pagination: {
page,
pageSize,
total,
totalPages: Math.ceil(total / pageSize)
}
}
})

View File

@@ -8,7 +8,10 @@ export default defineEventHandler(async (event) => {
const projectId = getRouterParam(event, 'id')
const project = await queryOne<any>(`
SELECT * FROM wr_project_info WHERE project_id = $1
SELECT p.*, b.business_name, b.business_code
FROM wr_project_info p
LEFT JOIN wr_business b ON p.business_id = b.business_id
WHERE p.project_id = $1
`, [projectId])
if (!project) {
@@ -35,6 +38,9 @@ export default defineEventHandler(async (event) => {
endDate: project.end_date,
contractAmount: project.contract_amount,
projectStatus: project.project_status,
businessId: project.business_id,
businessName: project.business_name,
businessCode: project.business_code,
createdAt: project.created_at,
updatedAt: project.updated_at,
currentPm: pm ? { employeeId: pm.employee_id, employeeName: pm.employee_name } : null,

View File

@@ -11,6 +11,7 @@ interface UpdateProjectBody {
endDate?: string
contractAmount?: number
projectStatus?: string
businessId?: number | null
}
/**
@@ -46,10 +47,11 @@ export default defineEventHandler(async (event) => {
end_date = $6,
contract_amount = $7,
project_status = $8,
business_id = $9,
updated_at = NOW(),
updated_ip = $9,
updated_email = $10
WHERE project_id = $11
updated_ip = $10,
updated_email = $11
WHERE project_id = $12
`, [
body.projectName ?? existing.project_name,
body.projectType ?? existing.project_type ?? 'SI',
@@ -59,6 +61,7 @@ export default defineEventHandler(async (event) => {
body.endDate ?? existing.end_date,
body.contractAmount ?? existing.contract_amount,
body.projectStatus ?? existing.project_status,
body.businessId !== undefined ? body.businessId : existing.business_id,
clientIp,
userEmail,
projectId

View File

@@ -10,6 +10,7 @@ interface CreateProjectBody {
startDate?: string
endDate?: string
contractAmount?: number
businessId?: number | null
}
/**
@@ -62,9 +63,9 @@ export default defineEventHandler(async (event) => {
const project = await insertReturning(`
INSERT INTO wr_project_info (
project_code, project_name, project_type, client_name, project_description,
start_date, end_date, contract_amount,
start_date, end_date, contract_amount, business_id,
created_ip, created_email, updated_ip, updated_email
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $9, $10)
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $10, $11)
RETURNING *
`, [
projectCode,
@@ -75,6 +76,7 @@ export default defineEventHandler(async (event) => {
body.startDate || null,
body.endDate || null,
body.contractAmount || null,
body.businessId || null,
clientIp,
userEmail
])
@@ -85,7 +87,8 @@ export default defineEventHandler(async (event) => {
projectId: project.project_id,
projectCode: project.project_code,
projectName: project.project_name,
projectType: project.project_type
projectType: project.project_type,
businessId: project.business_id
}
}
})

View File

@@ -7,9 +7,11 @@ import { query } from '../../utils/db'
export default defineEventHandler(async (event) => {
const queryParams = getQuery(event)
const status = queryParams.status as string || null
const businessId = queryParams.businessId ? Number(queryParams.businessId) : null
let sql = `
SELECT p.*,
b.business_name,
(SELECT employee_name FROM wr_employee_info e
JOIN wr_project_manager_history pm ON e.employee_id = pm.employee_id
WHERE pm.project_id = p.project_id AND pm.role_type = 'PM'
@@ -21,14 +23,27 @@ export default defineEventHandler(async (event) => {
AND (pm.end_date IS NULL OR pm.end_date >= CURRENT_DATE)
LIMIT 1) as pl_name
FROM wr_project_info p
LEFT JOIN wr_business b ON p.business_id = b.business_id
`
const conditions: string[] = []
const params: any[] = []
let paramIndex = 1
if (status) {
sql += ' WHERE p.project_status = $1'
conditions.push(`p.project_status = $${paramIndex++}`)
params.push(status)
}
if (businessId) {
conditions.push(`p.business_id = $${paramIndex++}`)
params.push(businessId)
}
if (conditions.length > 0) {
sql += ' WHERE ' + conditions.join(' AND ')
}
sql += ' ORDER BY p.created_at DESC'
const projects = await query(sql, params)
@@ -45,6 +60,8 @@ export default defineEventHandler(async (event) => {
endDate: p.end_date,
contractAmount: p.contract_amount,
projectStatus: p.project_status,
businessId: p.business_id,
businessName: p.business_name,
pmName: p.pm_name,
plName: p.pl_name,
createdAt: p.created_at

View File

@@ -2,6 +2,13 @@ import type { H3Event } from 'h3'
import { queryOne } from './db'
import { getAuthenticatedUserId } from './session'
/**
* 현재 로그인한 사용자의 ID 조회
*/
export async function getCurrentUserId(event: H3Event): Promise<number | null> {
return await getAuthenticatedUserId(event)
}
/**
* 현재 로그인한 사용자의 이메일 조회
*/