기능구현중
This commit is contained in:
228
server/api/report/review.post.ts
Normal file
228
server/api/report/review.post.ts
Normal file
@@ -0,0 +1,228 @@
|
||||
import { defineEventHandler, readBody, createError } from 'h3'
|
||||
import { query } from '../../utils/db'
|
||||
import OpenAI from 'openai'
|
||||
|
||||
const openai = new OpenAI({
|
||||
apiKey: process.env.OPENAI_API_KEY
|
||||
})
|
||||
|
||||
// 문자열 해시 함수 (seed용)
|
||||
function hashCode(str: string): number {
|
||||
let hash = 0
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const char = str.charCodeAt(i)
|
||||
hash = ((hash << 5) - hash) + char
|
||||
hash = hash & hash // 32bit 정수로 변환
|
||||
}
|
||||
return Math.abs(hash)
|
||||
}
|
||||
|
||||
interface QualityScore {
|
||||
summary: string // 총평 (맨 위)
|
||||
specificity: { score: number; improvement: string } // 구체성
|
||||
completeness: { score: number; improvement: string } // 완결성
|
||||
timeEstimation: { score: number; improvement: string } // 시간산정
|
||||
planning: { score: number; improvement: string } // 계획성
|
||||
overall: number // 종합점수
|
||||
bestPractice: { // 모범 답안
|
||||
workTasks: string[] // 금주 실적 모범 답안
|
||||
planTasks: string[] // 차주 계획 모범 답안
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 주간보고 PMO AI 리뷰 - 작성 품질 점수 + 모범 답안
|
||||
* POST /api/report/review
|
||||
*/
|
||||
export default defineEventHandler(async (event) => {
|
||||
const body = await readBody(event)
|
||||
const { reportId } = body
|
||||
|
||||
if (!reportId) {
|
||||
throw createError({ statusCode: 400, message: 'reportId가 필요합니다.' })
|
||||
}
|
||||
|
||||
// 주간보고 조회
|
||||
const reports = await query(`
|
||||
SELECT
|
||||
r.report_id,
|
||||
r.report_year,
|
||||
r.report_week,
|
||||
e.employee_name as author_name
|
||||
FROM wr_weekly_report r
|
||||
JOIN wr_employee_info e ON r.author_id = e.employee_id
|
||||
WHERE r.report_id = $1
|
||||
`, [reportId])
|
||||
|
||||
if (reports.length === 0) {
|
||||
throw createError({ statusCode: 404, message: '주간보고를 찾을 수 없습니다.' })
|
||||
}
|
||||
|
||||
const report = reports[0]
|
||||
|
||||
// Task 조회
|
||||
const tasks = await query(`
|
||||
SELECT
|
||||
t.task_type,
|
||||
t.task_description,
|
||||
t.task_hours,
|
||||
t.is_completed,
|
||||
p.project_name
|
||||
FROM wr_weekly_report_task t
|
||||
JOIN wr_project_info p ON t.project_id = p.project_id
|
||||
WHERE t.report_id = $1
|
||||
ORDER BY t.task_type, p.project_name
|
||||
`, [reportId])
|
||||
|
||||
if (tasks.length === 0) {
|
||||
throw createError({ statusCode: 400, message: '등록된 Task가 없습니다.' })
|
||||
}
|
||||
|
||||
// Task를 실적/계획으로 분리
|
||||
const workTasks = tasks.filter((t: any) => t.task_type === 'WORK')
|
||||
const planTasks = tasks.filter((t: any) => t.task_type === 'PLAN')
|
||||
|
||||
// 프롬프트용 텍스트 생성
|
||||
let taskText = `[작성자] ${report.author_name}\n[기간] ${report.report_year}년 ${report.report_week}주차\n\n`
|
||||
|
||||
if (workTasks.length > 0) {
|
||||
taskText += `[금주 실적]\n`
|
||||
workTasks.forEach((t: any) => {
|
||||
const status = t.is_completed ? '완료' : '진행중'
|
||||
taskText += `- ${t.project_name} / ${t.task_description} / ${t.task_hours}h / ${status}\n`
|
||||
})
|
||||
taskText += '\n'
|
||||
}
|
||||
|
||||
if (planTasks.length > 0) {
|
||||
taskText += `[차주 계획]\n`
|
||||
planTasks.forEach((t: any) => {
|
||||
taskText += `- ${t.project_name} / ${t.task_description} / ${t.task_hours}h\n`
|
||||
})
|
||||
}
|
||||
|
||||
// OpenAI 품질 점수 + 모범 답안 요청
|
||||
const systemPrompt = `당신은 SI 프로젝트의 PMO(Project Management Officer)입니다.
|
||||
주간보고의 작성 품질을 평가하고, 모범 답안을 제시해주세요.
|
||||
|
||||
[평가 항목] (각 1~10점)
|
||||
1. 구체성 (specificity): 작업 내용이 어떤 기능/모듈인지 구체적으로 작성되었는지
|
||||
2. 완결성 (completeness): 필수 정보 포함 여부
|
||||
3. 시간산정 (timeEstimation): 작업 시간이 내용 대비 적절하게 배분되었는지
|
||||
4. 계획성 (planning): 차주 계획이 실현 가능하고 명확한 목표가 있는지
|
||||
|
||||
[완결성 상세 기준] - 엄격하게 적용
|
||||
- 진행중 작업에 진척률(%)이 없으면 -2점
|
||||
- 진행중 작업에 완료 예정일이 없으면 -2점
|
||||
- 완료 작업인데 산출물/결과 언급이 없으면 -1점
|
||||
- 상태(완료/진행중)가 명확하지 않으면 -1점
|
||||
|
||||
[계획성 상세 기준] - 엄격하게 적용
|
||||
- 차주 계획에 예상 소요시간 근거가 없으면 -1점
|
||||
- 차주 계획에 목표 완료일/산출물이 없으면 -2점
|
||||
- 단순 "~할 예정", "~진행" 만 있고 구체적 목표가 없으면 -2점
|
||||
- 실현 가능성이 낮은 과도한 계획이면 -1점
|
||||
|
||||
[점수 기준]
|
||||
- 1~3점: 매우 부족 (내용이 거의 없거나 한 단어 수준)
|
||||
- 4~5점: 부족 (진척률/예정일 누락, 모호한 표현)
|
||||
- 6~7점: 보통 (기본 내용은 있으나 구체성 부족)
|
||||
- 8~9점: 양호 (진척률, 예정일, 산출물 모두 명시)
|
||||
- 10점: 우수 (완벽한 모범 사례)
|
||||
|
||||
※ 진행중 작업에 진척률/예정일이 없으면 완결성은 6점 이하로 평가하세요.
|
||||
※ 차주 계획에 구체적 목표가 없으면 계획성은 6점 이하로 평가하세요.
|
||||
|
||||
[모범 답안 작성 규칙]
|
||||
- 사용자가 작성한 내용을 기반으로 더 구체적으로 보완
|
||||
- 같은 프로젝트명, 비슷한 작업 내용을 유지하되 구체성 추가
|
||||
- 진행중인 작업은 반드시 진척률(%)과 완료 예정일 추가
|
||||
- 시간이 긴 작업은 세부 내역 포함
|
||||
- 차주 계획은 목표 산출물과 예상 완료일 명시
|
||||
- 형식: "프로젝트명 / 작업내용 (세부사항, 진척률, 예정일) / 시간h / 상태"
|
||||
|
||||
[응답 규칙]
|
||||
- 반드시 아래 JSON 형식으로만 응답
|
||||
- summary: 전체적인 총평 (30~60자, 격려 포함)
|
||||
- improvement: 각 항목별 개선 포인트 (15~30자, 구체적으로)
|
||||
- bestPractice: 모범 답안 (workTasks, planTasks 배열)
|
||||
- JSON 외의 텍스트는 절대 포함하지 마세요`
|
||||
|
||||
const userPrompt = `다음 주간보고의 작성 품질을 평가하고, 모범 답안을 만들어주세요.
|
||||
|
||||
${taskText}
|
||||
|
||||
아래 JSON 형식으로만 응답하세요:
|
||||
{
|
||||
"summary": "총평 (격려 포함)",
|
||||
"specificity": { "score": 숫자, "improvement": "개선포인트" },
|
||||
"completeness": { "score": 숫자, "improvement": "개선포인트" },
|
||||
"timeEstimation": { "score": 숫자, "improvement": "개선포인트" },
|
||||
"planning": { "score": 숫자, "improvement": "개선포인트" },
|
||||
"overall": 종합점수(소수점1자리),
|
||||
"bestPractice": {
|
||||
"workTasks": ["모범답안1", "모범답안2", ...],
|
||||
"planTasks": ["모범답안1", "모범답안2", ...]
|
||||
}
|
||||
}`
|
||||
|
||||
try {
|
||||
// Task 내용 기반 seed 생성 (같은 내용 = 같은 점수)
|
||||
const seed = hashCode(taskText)
|
||||
|
||||
const response = await openai.chat.completions.create({
|
||||
model: 'gpt-4o-mini',
|
||||
messages: [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
{ role: 'user', content: userPrompt }
|
||||
],
|
||||
max_tokens: 1500,
|
||||
temperature: 0.2, // 낮춰서 일관성 강화
|
||||
seed: seed // 같은 내용 = 같은 seed = 같은 결과
|
||||
})
|
||||
|
||||
const content = response.choices[0]?.message?.content || ''
|
||||
|
||||
// JSON 파싱
|
||||
let qualityScore: QualityScore
|
||||
try {
|
||||
// JSON 블록 추출 (```json ... ``` 형태 처리)
|
||||
let jsonStr = content
|
||||
const jsonMatch = content.match(/```json\s*([\s\S]*?)\s*```/)
|
||||
if (jsonMatch) {
|
||||
jsonStr = jsonMatch[1]
|
||||
} else {
|
||||
// { } 사이 추출
|
||||
const braceMatch = content.match(/\{[\s\S]*\}/)
|
||||
if (braceMatch) {
|
||||
jsonStr = braceMatch[0]
|
||||
}
|
||||
}
|
||||
qualityScore = JSON.parse(jsonStr)
|
||||
} catch (parseError) {
|
||||
console.error('JSON 파싱 실패:', content)
|
||||
throw new Error('AI 응답을 파싱할 수 없습니다.')
|
||||
}
|
||||
|
||||
const reviewedAt = new Date().toISOString()
|
||||
|
||||
// DB에 저장 (ai_review에 JSON 문자열로 저장)
|
||||
await query(`
|
||||
UPDATE wr_weekly_report
|
||||
SET ai_review = $1, ai_review_at = $2
|
||||
WHERE report_id = $3
|
||||
`, [JSON.stringify(qualityScore), reviewedAt, reportId])
|
||||
|
||||
return {
|
||||
success: true,
|
||||
qualityScore,
|
||||
reviewedAt
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('OpenAI API error:', error)
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
message: 'AI 품질 평가 중 오류가 발생했습니다: ' + error.message
|
||||
})
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user