작업계획서대로 진행
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
|
||||
}))
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user