1ㅊㅏ완료
This commit is contained in:
@@ -1,15 +1,21 @@
|
||||
import { query, queryOne, insertReturning, execute } from '../../../utils/db'
|
||||
import { defineEventHandler, readBody, createError, getCookie } from 'h3'
|
||||
import { query, queryOne, execute, insertReturning } from '../../../utils/db'
|
||||
import { getClientIp } from '../../../utils/ip'
|
||||
import { getCurrentUserEmail } from '../../../utils/user'
|
||||
import OpenAI from 'openai'
|
||||
|
||||
interface AggregateBody {
|
||||
projectId: number
|
||||
projectIds: number[]
|
||||
reportYear: number
|
||||
reportWeek: number
|
||||
}
|
||||
|
||||
const openai = new OpenAI({
|
||||
apiKey: process.env.OPENAI_API_KEY
|
||||
})
|
||||
|
||||
/**
|
||||
* 수동 취합 실행
|
||||
* 다중 프로젝트 취합 실행 (OpenAI 요약 포함)
|
||||
* POST /api/report/summary/aggregate
|
||||
*/
|
||||
export default defineEventHandler(async (event) => {
|
||||
@@ -22,75 +28,106 @@ export default defineEventHandler(async (event) => {
|
||||
const clientIp = getClientIp(event)
|
||||
const userEmail = await getCurrentUserEmail(event)
|
||||
|
||||
if (!body.projectId || !body.reportYear || !body.reportWeek) {
|
||||
throw createError({ statusCode: 400, message: '프로젝트, 연도, 주차를 선택해주세요.' })
|
||||
if (!body.projectIds || body.projectIds.length === 0) {
|
||||
throw createError({ statusCode: 400, message: '프로젝트를 선택해주세요.' })
|
||||
}
|
||||
if (!body.reportYear || !body.reportWeek) {
|
||||
throw createError({ statusCode: 400, message: '연도와 주차를 선택해주세요.' })
|
||||
}
|
||||
|
||||
// 해당 프로젝트/주차의 제출된 보고서 조회 (새 구조)
|
||||
const reports = await query<any>(`
|
||||
SELECT
|
||||
r.report_id,
|
||||
r.author_id,
|
||||
r.week_start_date,
|
||||
r.week_end_date,
|
||||
rp.detail_id
|
||||
FROM wr_weekly_report r
|
||||
JOIN wr_weekly_report_project rp ON r.report_id = rp.report_id
|
||||
WHERE rp.project_id = $1
|
||||
AND r.report_year = $2
|
||||
AND r.report_week = $3
|
||||
AND r.report_status IN ('SUBMITTED', 'AGGREGATED')
|
||||
ORDER BY r.report_id
|
||||
`, [body.projectId, body.reportYear, body.reportWeek])
|
||||
let summaryCount = 0
|
||||
let totalMembers = 0
|
||||
const allReportIds: number[] = []
|
||||
|
||||
if (reports.length === 0) {
|
||||
// 각 프로젝트별로 취합 생성
|
||||
for (const projectId of body.projectIds) {
|
||||
// 해당 프로젝트/주차의 Task 조회 (작성자 포함)
|
||||
const tasks = await query<any>(`
|
||||
SELECT
|
||||
t.task_id,
|
||||
t.task_type,
|
||||
t.task_description,
|
||||
t.task_hours,
|
||||
t.is_completed,
|
||||
r.report_id,
|
||||
r.author_id,
|
||||
e.employee_name as author_name,
|
||||
r.week_start_date,
|
||||
r.week_end_date
|
||||
FROM wr_weekly_report r
|
||||
JOIN wr_weekly_report_task t ON r.report_id = t.report_id
|
||||
JOIN wr_employee_info e ON r.author_id = e.employee_id
|
||||
WHERE t.project_id = $1
|
||||
AND r.report_year = $2
|
||||
AND r.report_week = $3
|
||||
AND r.report_status IN ('SUBMITTED', 'AGGREGATED')
|
||||
ORDER BY t.task_type, e.employee_name
|
||||
`, [projectId, body.reportYear, body.reportWeek])
|
||||
|
||||
if (tasks.length === 0) continue
|
||||
|
||||
const reportIds = [...new Set(tasks.map(t => t.report_id))]
|
||||
const weekStartDate = tasks[0].week_start_date
|
||||
const weekEndDate = tasks[0].week_end_date
|
||||
|
||||
// 총 시간 계산
|
||||
const totalWorkHours = tasks
|
||||
.filter(t => t.task_type === 'WORK')
|
||||
.reduce((sum, t) => sum + (parseFloat(t.task_hours) || 0), 0)
|
||||
|
||||
// OpenAI로 요약 생성 (금주 실적 / 차주 계획 분리)
|
||||
const { workSummary, planSummary } = await generateAISummary(tasks, projectId, body.reportYear, body.reportWeek)
|
||||
|
||||
// 기존 취합 보고서 확인
|
||||
const existing = await queryOne<any>(`
|
||||
SELECT summary_id FROM wr_aggregated_report_summary
|
||||
WHERE project_id = $1 AND report_year = $2 AND report_week = $3
|
||||
`, [projectId, body.reportYear, body.reportWeek])
|
||||
|
||||
if (existing) {
|
||||
// 기존 취합 업데이트
|
||||
await execute(`
|
||||
UPDATE wr_aggregated_report_summary
|
||||
SET report_ids = $1,
|
||||
member_count = $2,
|
||||
total_work_hours = $3,
|
||||
ai_work_summary = $4,
|
||||
ai_plan_summary = $5,
|
||||
ai_summary_at = NOW(),
|
||||
aggregated_at = NOW(),
|
||||
updated_at = NOW(),
|
||||
updated_ip = $6,
|
||||
updated_email = $7
|
||||
WHERE summary_id = $8
|
||||
`, [reportIds, reportIds.length, totalWorkHours, workSummary, planSummary, clientIp, userEmail, existing.summary_id])
|
||||
} else {
|
||||
// 새 취합 생성
|
||||
await insertReturning<any>(`
|
||||
INSERT INTO wr_aggregated_report_summary (
|
||||
project_id, report_year, report_week, week_start_date, week_end_date,
|
||||
report_ids, member_count, total_work_hours, ai_work_summary, ai_plan_summary, ai_summary_at,
|
||||
created_ip, created_email, updated_ip, updated_email
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, NOW(), $11, $12, $11, $12)
|
||||
RETURNING summary_id
|
||||
`, [
|
||||
projectId, body.reportYear, body.reportWeek,
|
||||
weekStartDate, weekEndDate,
|
||||
reportIds, reportIds.length, totalWorkHours, workSummary, planSummary,
|
||||
clientIp, userEmail
|
||||
])
|
||||
}
|
||||
|
||||
summaryCount++
|
||||
totalMembers += reportIds.length
|
||||
allReportIds.push(...reportIds)
|
||||
}
|
||||
|
||||
if (summaryCount === 0) {
|
||||
throw createError({ statusCode: 400, message: '취합할 보고서가 없습니다.' })
|
||||
}
|
||||
|
||||
const reportIds = [...new Set(reports.map(r => r.report_id))]
|
||||
const weekStartDate = reports[0].week_start_date
|
||||
const weekEndDate = reports[0].week_end_date
|
||||
|
||||
// 기존 취합 보고서 확인
|
||||
const existing = await queryOne<any>(`
|
||||
SELECT summary_id FROM wr_aggregated_report_summary
|
||||
WHERE project_id = $1 AND report_year = $2 AND report_week = $3
|
||||
`, [body.projectId, body.reportYear, body.reportWeek])
|
||||
|
||||
let summaryId: number
|
||||
|
||||
if (existing) {
|
||||
// 기존 취합 업데이트
|
||||
await execute(`
|
||||
UPDATE wr_aggregated_report_summary
|
||||
SET report_ids = $1,
|
||||
member_count = $2,
|
||||
aggregated_at = NOW(),
|
||||
updated_at = NOW(),
|
||||
updated_ip = $3,
|
||||
updated_email = $4
|
||||
WHERE summary_id = $5
|
||||
`, [reportIds, reportIds.length, clientIp, userEmail, existing.summary_id])
|
||||
summaryId = existing.summary_id
|
||||
} else {
|
||||
// 새 취합 생성
|
||||
const newSummary = await insertReturning<any>(`
|
||||
INSERT INTO wr_aggregated_report_summary (
|
||||
project_id, report_year, report_week, week_start_date, week_end_date,
|
||||
report_ids, member_count,
|
||||
created_ip, created_email, updated_ip, updated_email
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $8, $9)
|
||||
RETURNING summary_id
|
||||
`, [
|
||||
body.projectId, body.reportYear, body.reportWeek,
|
||||
weekStartDate, weekEndDate,
|
||||
reportIds, reportIds.length,
|
||||
clientIp, userEmail
|
||||
])
|
||||
summaryId = newSummary.summary_id
|
||||
}
|
||||
|
||||
// 개별 보고서 상태 업데이트
|
||||
const uniqueReportIds = [...new Set(allReportIds)]
|
||||
await execute(`
|
||||
UPDATE wr_weekly_report
|
||||
SET report_status = 'AGGREGATED',
|
||||
@@ -98,11 +135,88 @@ export default defineEventHandler(async (event) => {
|
||||
updated_ip = $1,
|
||||
updated_email = $2
|
||||
WHERE report_id = ANY($3)
|
||||
`, [clientIp, userEmail, reportIds])
|
||||
`, [clientIp, userEmail, uniqueReportIds])
|
||||
|
||||
return {
|
||||
success: true,
|
||||
summaryId,
|
||||
memberCount: reportIds.length
|
||||
summaryCount,
|
||||
totalMembers: uniqueReportIds.length
|
||||
}
|
||||
})
|
||||
|
||||
// OpenAI로 금주 실적/차주 계획 분리 요약 생성
|
||||
async function generateAISummary(tasks: any[], projectId: number, year: number, week: number): Promise<{ workSummary: string, planSummary: string }> {
|
||||
// 프로젝트명 조회
|
||||
const project = await queryOne<any>(`SELECT project_name FROM wr_project_info WHERE project_id = $1`, [projectId])
|
||||
const projectName = project?.project_name || '프로젝트'
|
||||
|
||||
// Task를 실적/계획으로 분류
|
||||
const workTasks = tasks.filter(t => t.task_type === 'WORK')
|
||||
const planTasks = tasks.filter(t => t.task_type === 'PLAN')
|
||||
|
||||
// 금주 실적 요약
|
||||
const workPrompt = `당신은 주간보고 취합 전문가입니다. 아래 금주 실적을 간결하게 요약해주세요.
|
||||
|
||||
## 프로젝트: ${projectName}
|
||||
## 기간: ${year}년 ${week}주차
|
||||
|
||||
## 금주 실적 (${workTasks.length}건)
|
||||
${workTasks.map(t => `- [${t.author_name}] ${t.is_completed ? '완료' : '진행중'} | ${t.task_description} (${t.task_hours}h)`).join('\n') || '없음'}
|
||||
|
||||
## 요약 규칙
|
||||
1. 완료된 작업과 진행 중인 작업을 구분하여 핵심만 요약
|
||||
2. 동일/유사한 작업은 하나로 통합
|
||||
3. 담당자 이름은 생략하고 내용 위주로 작성
|
||||
4. 3~5줄 이내로 간결하게
|
||||
5. 마크다운 리스트 형식으로 작성`
|
||||
|
||||
// 차주 계획 요약
|
||||
const planPrompt = `당신은 주간보고 취합 전문가입니다. 아래 차주 계획을 간결하게 요약해주세요.
|
||||
|
||||
## 프로젝트: ${projectName}
|
||||
## 기간: ${year}년 ${week+1}주차 계획
|
||||
|
||||
## 차주 계획 (${planTasks.length}건)
|
||||
${planTasks.map(t => `- [${t.author_name}] ${t.task_description} (${t.task_hours}h)`).join('\n') || '없음'}
|
||||
|
||||
## 요약 규칙
|
||||
1. 주요 계획을 우선순위에 따라 요약
|
||||
2. 동일/유사한 작업은 하나로 통합
|
||||
3. 담당자 이름은 생략하고 내용 위주로 작성
|
||||
4. 2~4줄 이내로 간결하게
|
||||
5. 마크다운 리스트 형식으로 작성`
|
||||
|
||||
try {
|
||||
const [workRes, planRes] = await Promise.all([
|
||||
openai.chat.completions.create({
|
||||
model: 'gpt-4o-mini',
|
||||
messages: [
|
||||
{ role: 'system', content: '주간보고 취합 전문가입니다. 간결하게 요약합니다.' },
|
||||
{ role: 'user', content: workPrompt }
|
||||
],
|
||||
temperature: 0.3,
|
||||
max_tokens: 500
|
||||
}),
|
||||
openai.chat.completions.create({
|
||||
model: 'gpt-4o-mini',
|
||||
messages: [
|
||||
{ role: 'system', content: '주간보고 취합 전문가입니다. 간결하게 요약합니다.' },
|
||||
{ role: 'user', content: planPrompt }
|
||||
],
|
||||
temperature: 0.3,
|
||||
max_tokens: 500
|
||||
})
|
||||
])
|
||||
|
||||
return {
|
||||
workSummary: workRes.choices[0]?.message?.content || '요약 없음',
|
||||
planSummary: planRes.choices[0]?.message?.content || '요약 없음'
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('OpenAI 요약 생성 실패:', error)
|
||||
return {
|
||||
workSummary: '요약 생성 실패',
|
||||
planSummary: '요약 생성 실패'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user