작업계획서대로 진행

This commit is contained in:
2026-01-11 10:50:51 +09:00
parent 5cda181cc5
commit d4620dc1fa
39 changed files with 3344 additions and 120 deletions

View File

@@ -0,0 +1,43 @@
import { execute, queryOne } from '../../../utils/db'
interface LinkBody {
todoId: number
weeklyReportId: number
markCompleted?: boolean
}
/**
* TODO와 주간보고 연계
* POST /api/todo/report/link
*/
export default defineEventHandler(async (event) => {
const body = await readBody<LinkBody>(event)
if (!body.todoId || !body.weeklyReportId) {
throw createError({ statusCode: 400, message: 'TODO ID와 주간보고 ID가 필요합니다.' })
}
const todo = await queryOne('SELECT * FROM wr_todo WHERE todo_id = $1', [body.todoId])
if (!todo) {
throw createError({ statusCode: 404, message: 'TODO를 찾을 수 없습니다.' })
}
const updates = ['weekly_report_id = $1', 'updated_at = NOW()']
const values = [body.weeklyReportId]
if (body.markCompleted) {
updates.push('status = $2', 'completed_at = NOW()')
values.push('COMPLETED')
}
values.push(body.todoId)
await execute(`
UPDATE wr_todo SET ${updates.join(', ')} WHERE todo_id = $${values.length}
`, values)
return {
success: true,
message: body.markCompleted ? 'TODO가 완료 처리되고 주간보고와 연계되었습니다.' : 'TODO가 주간보고와 연계되었습니다.'
}
})

View File

@@ -0,0 +1,108 @@
import { query } from '../../../utils/db'
import { callOpenAI } from '../../../utils/openai'
interface SearchBody {
taskDescription: string
projectId?: number
}
/**
* 주간보고 실적과 유사한 TODO 검색
* POST /api/todo/report/similar
*/
export default defineEventHandler(async (event) => {
const body = await readBody<SearchBody>(event)
if (!body.taskDescription?.trim()) {
return { todos: [] }
}
// 해당 프로젝트의 미완료 TODO 조회
const conditions = ["t.status IN ('PENDING', 'IN_PROGRESS')"]
const values: any[] = []
let paramIndex = 1
if (body.projectId) {
conditions.push(`t.project_id = $${paramIndex++}`)
values.push(body.projectId)
}
const todos = await query(`
SELECT
t.todo_id,
t.todo_title,
t.todo_content,
t.project_id,
p.project_name,
t.status,
t.due_date
FROM wr_todo t
LEFT JOIN wr_project_info p ON t.project_id = p.project_id
WHERE ${conditions.join(' AND ')}
ORDER BY t.created_at DESC
LIMIT 20
`, values)
if (todos.length === 0) {
return { todos: [] }
}
// OpenAI로 유사도 분석
const todoList = todos.map((t: any, i: number) =>
`${i + 1}. [ID:${t.todo_id}] ${t.todo_title}${t.todo_content ? ' - ' + t.todo_content.substring(0, 50) : ''}`
).join('\n')
const prompt = `다음 주간보고 실적과 가장 유사한 TODO를 찾아주세요.
실적 내용: "${body.taskDescription}"
TODO 목록:
${todoList}
유사한 TODO의 ID를 배열로 반환해주세요 (유사도 70% 이상만, 최대 3개).
JSON 형식: { "similarIds": [1, 2] }`
try {
const response = await callOpenAI([
{ role: 'system', content: '주간보고 실적과 TODO의 유사도를 분석하는 전문가입니다.' },
{ role: 'user', content: prompt }
], true)
const parsed = JSON.parse(response)
const similarIds = parsed.similarIds || []
const similarTodos = todos.filter((t: any) => similarIds.includes(t.todo_id))
return {
todos: similarTodos.map((t: any) => ({
todoId: t.todo_id,
todoTitle: t.todo_title,
todoContent: t.todo_content,
projectId: t.project_id,
projectName: t.project_name,
status: t.status,
dueDate: t.due_date
}))
}
} catch (e) {
console.error('OpenAI error:', e)
// 실패 시 키워드 기반 간단 매칭
const keywords = body.taskDescription.split(/\s+/).filter(k => k.length >= 2)
const matched = todos.filter((t: any) => {
const title = t.todo_title?.toLowerCase() || ''
return keywords.some(k => title.includes(k.toLowerCase()))
}).slice(0, 3)
return {
todos: matched.map((t: any) => ({
todoId: t.todo_id,
todoTitle: t.todo_title,
projectId: t.project_id,
projectName: t.project_name,
status: t.status,
dueDate: t.due_date
})),
fallback: true
}
}
})