작업계획서대로 진행
This commit is contained in:
30
backend/api/business-report/[id]/confirm.put.ts
Normal file
30
backend/api/business-report/[id]/confirm.put.ts
Normal 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: '보고서가 확정되었습니다.' }
|
||||
})
|
||||
80
backend/api/business-report/[id]/detail.get.ts
Normal file
80
backend/api/business-report/[id]/detail.get.ts
Normal 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
|
||||
}))
|
||||
}
|
||||
})
|
||||
31
backend/api/business-report/[id]/update.put.ts
Normal file
31
backend/api/business-report/[id]/update.put.ts
Normal 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 }
|
||||
})
|
||||
148
backend/api/business-report/generate.post.ts
Normal file
148
backend/api/business-report/generate.post.ts
Normal 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'
|
||||
}
|
||||
}
|
||||
})
|
||||
51
backend/api/business-report/list.get.ts
Normal file
51
backend/api/business-report/list.get.ts
Normal 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
|
||||
}))
|
||||
}
|
||||
})
|
||||
44
backend/api/business/[id]/delete.delete.ts
Normal file
44
backend/api/business/[id]/delete.delete.ts
Normal 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: '사업이 삭제(중단) 처리되었습니다.'
|
||||
}
|
||||
})
|
||||
71
backend/api/business/[id]/detail.get.ts
Normal file
71
backend/api/business/[id]/detail.get.ts
Normal 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
|
||||
}))
|
||||
}
|
||||
})
|
||||
68
backend/api/business/[id]/update.put.ts
Normal file
68
backend/api/business/[id]/update.put.ts
Normal 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: '사업이 수정되었습니다.'
|
||||
}
|
||||
})
|
||||
47
backend/api/business/create.post.ts
Normal file
47
backend/api/business/create.post.ts
Normal 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: '사업이 등록되었습니다.'
|
||||
}
|
||||
})
|
||||
77
backend/api/business/list.get.ts
Normal file
77
backend/api/business/list.get.ts
Normal 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
|
||||
}))
|
||||
}
|
||||
})
|
||||
36
backend/api/maintenance/[id]/delete.delete.ts
Normal file
36
backend/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
backend/api/maintenance/[id]/detail.get.ts
Normal file
64
backend/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
backend/api/maintenance/[id]/status.put.ts
Normal file
48
backend/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
backend/api/maintenance/[id]/update.put.ts
Normal file
85
backend/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
backend/api/maintenance/bulk-create.post.ts
Normal file
84
backend/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
backend/api/maintenance/create.post.ts
Normal file
57
backend/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
backend/api/maintenance/list.get.ts
Normal file
114
backend/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)
|
||||
}
|
||||
}
|
||||
})
|
||||
175
backend/api/maintenance/upload.post.ts
Normal file
175
backend/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
|
||||
}
|
||||
})
|
||||
30
backend/api/meeting/[id]/delete.delete.ts
Normal file
30
backend/api/meeting/[id]/delete.delete.ts
Normal 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: '회의록이 삭제되었습니다.'
|
||||
}
|
||||
})
|
||||
96
backend/api/meeting/[id]/detail.get.ts
Normal file
96
backend/api/meeting/[id]/detail.get.ts
Normal 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
|
||||
}))
|
||||
}
|
||||
})
|
||||
107
backend/api/meeting/[id]/update.put.ts
Normal file
107
backend/api/meeting/[id]/update.put.ts
Normal 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: '회의록이 수정되었습니다.'
|
||||
}
|
||||
})
|
||||
92
backend/api/meeting/create.post.ts
Normal file
92
backend/api/meeting/create.post.ts
Normal 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: '회의록이 등록되었습니다.'
|
||||
}
|
||||
})
|
||||
122
backend/api/meeting/list.get.ts
Normal file
122
backend/api/meeting/list.get.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 로그인한 사용자의 이메일 조회
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user