기능구현중
This commit is contained in:
23
server/api/todo/[id]/complete.put.ts
Normal file
23
server/api/todo/[id]/complete.put.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { execute } from '../../../utils/db'
|
||||
import { requireAuth } from '../../../utils/session'
|
||||
|
||||
/**
|
||||
* TODO 완료 처리
|
||||
* PUT /api/todo/[id]/complete
|
||||
*/
|
||||
export default defineEventHandler(async (event) => {
|
||||
await requireAuth(event)
|
||||
const todoId = parseInt(event.context.params?.id || '0')
|
||||
|
||||
if (!todoId) {
|
||||
throw createError({ statusCode: 400, message: 'TODO ID가 필요합니다.' })
|
||||
}
|
||||
|
||||
await execute(`
|
||||
UPDATE wr_todo
|
||||
SET status = 'COMPLETED', completed_at = NOW(), updated_at = NOW()
|
||||
WHERE todo_id = $1
|
||||
`, [todoId])
|
||||
|
||||
return { success: true, message: '완료 처리되었습니다.' }
|
||||
})
|
||||
18
server/api/todo/[id]/delete.delete.ts
Normal file
18
server/api/todo/[id]/delete.delete.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { execute, queryOne } from '../../../utils/db'
|
||||
|
||||
/**
|
||||
* TODO 삭제
|
||||
* DELETE /api/todo/[id]/delete
|
||||
*/
|
||||
export default defineEventHandler(async (event) => {
|
||||
const todoId = Number(getRouterParam(event, 'id'))
|
||||
|
||||
const existing = await queryOne('SELECT * FROM wr_todo WHERE todo_id = $1', [todoId])
|
||||
if (!existing) {
|
||||
throw createError({ statusCode: 404, message: 'TODO를 찾을 수 없습니다.' })
|
||||
}
|
||||
|
||||
await execute('DELETE FROM wr_todo WHERE todo_id = $1', [todoId])
|
||||
|
||||
return { success: true, message: 'TODO가 삭제되었습니다.' }
|
||||
})
|
||||
53
server/api/todo/[id]/detail.get.ts
Normal file
53
server/api/todo/[id]/detail.get.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { queryOne } from '../../../utils/db'
|
||||
|
||||
/**
|
||||
* TODO 상세 조회
|
||||
* GET /api/todo/[id]/detail
|
||||
*/
|
||||
export default defineEventHandler(async (event) => {
|
||||
const todoId = Number(getRouterParam(event, 'id'))
|
||||
|
||||
const todo = await queryOne(`
|
||||
SELECT
|
||||
t.*,
|
||||
p.project_name,
|
||||
m.meeting_title,
|
||||
a.employee_name as assignee_name,
|
||||
r.employee_name as reporter_name
|
||||
FROM wr_todo t
|
||||
LEFT JOIN wr_project_info p ON t.project_id = p.project_id
|
||||
LEFT JOIN wr_meeting m ON t.meeting_id = m.meeting_id
|
||||
LEFT JOIN wr_employee_info a ON t.assignee_id = a.employee_id
|
||||
LEFT JOIN wr_employee_info r ON t.reporter_id = r.employee_id
|
||||
WHERE t.todo_id = $1
|
||||
`, [todoId])
|
||||
|
||||
if (!todo) {
|
||||
throw createError({ statusCode: 404, message: 'TODO를 찾을 수 없습니다.' })
|
||||
}
|
||||
|
||||
return {
|
||||
todo: {
|
||||
todoId: todo.todo_id,
|
||||
todoTitle: todo.todo_title,
|
||||
todoContent: todo.todo_content,
|
||||
sourceType: todo.source_type,
|
||||
sourceId: todo.source_id,
|
||||
meetingId: todo.meeting_id,
|
||||
meetingTitle: todo.meeting_title,
|
||||
projectId: todo.project_id,
|
||||
projectName: todo.project_name,
|
||||
assigneeId: todo.assignee_id,
|
||||
assigneeName: todo.assignee_name,
|
||||
reporterId: todo.reporter_id,
|
||||
reporterName: todo.reporter_name,
|
||||
dueDate: todo.due_date,
|
||||
status: todo.status,
|
||||
priority: todo.priority,
|
||||
completedAt: todo.completed_at,
|
||||
weeklyReportId: todo.weekly_report_id,
|
||||
createdAt: todo.created_at,
|
||||
updatedAt: todo.updated_at
|
||||
}
|
||||
}
|
||||
})
|
||||
28
server/api/todo/[id]/discard.put.ts
Normal file
28
server/api/todo/[id]/discard.put.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { execute } from '../../../utils/db'
|
||||
import { requireAuth } from '../../../utils/session'
|
||||
|
||||
interface DiscardBody {
|
||||
reason?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO 폐기 처리
|
||||
* PUT /api/todo/[id]/discard
|
||||
*/
|
||||
export default defineEventHandler(async (event) => {
|
||||
await requireAuth(event)
|
||||
const todoId = parseInt(event.context.params?.id || '0')
|
||||
const body = await readBody<DiscardBody>(event)
|
||||
|
||||
if (!todoId) {
|
||||
throw createError({ statusCode: 400, message: 'TODO ID가 필요합니다.' })
|
||||
}
|
||||
|
||||
await execute(`
|
||||
UPDATE wr_todo
|
||||
SET status = 'DISCARDED', discard_reason = $1, updated_at = NOW()
|
||||
WHERE todo_id = $2
|
||||
`, [body.reason || null, todoId])
|
||||
|
||||
return { success: true, message: '폐기 처리되었습니다.' }
|
||||
})
|
||||
84
server/api/todo/[id]/update.put.ts
Normal file
84
server/api/todo/[id]/update.put.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { execute, queryOne } from '../../../utils/db'
|
||||
|
||||
interface UpdateTodoBody {
|
||||
todoTitle?: string
|
||||
todoContent?: string
|
||||
projectId?: number | null
|
||||
assigneeId?: number | null
|
||||
dueDate?: string | null
|
||||
status?: string
|
||||
priority?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO 수정
|
||||
* PUT /api/todo/[id]/update
|
||||
*/
|
||||
export default defineEventHandler(async (event) => {
|
||||
const todoId = Number(getRouterParam(event, 'id'))
|
||||
const body = await readBody<UpdateTodoBody>(event)
|
||||
|
||||
const existing = await queryOne('SELECT * FROM wr_todo WHERE todo_id = $1', [todoId])
|
||||
if (!existing) {
|
||||
throw createError({ statusCode: 404, message: 'TODO를 찾을 수 없습니다.' })
|
||||
}
|
||||
|
||||
const updates: string[] = []
|
||||
const values: any[] = []
|
||||
let paramIndex = 1
|
||||
|
||||
if (body.todoTitle !== undefined) {
|
||||
updates.push(`todo_title = $${paramIndex++}`)
|
||||
values.push(body.todoTitle)
|
||||
}
|
||||
|
||||
if (body.todoContent !== undefined) {
|
||||
updates.push(`todo_content = $${paramIndex++}`)
|
||||
values.push(body.todoContent)
|
||||
}
|
||||
|
||||
if (body.projectId !== undefined) {
|
||||
updates.push(`project_id = $${paramIndex++}`)
|
||||
values.push(body.projectId)
|
||||
}
|
||||
|
||||
if (body.assigneeId !== undefined) {
|
||||
updates.push(`assignee_id = $${paramIndex++}`)
|
||||
values.push(body.assigneeId)
|
||||
}
|
||||
|
||||
if (body.dueDate !== undefined) {
|
||||
updates.push(`due_date = $${paramIndex++}`)
|
||||
values.push(body.dueDate)
|
||||
}
|
||||
|
||||
if (body.status !== undefined) {
|
||||
updates.push(`status = $${paramIndex++}`)
|
||||
values.push(body.status)
|
||||
|
||||
// 완료 상태로 변경 시 완료일시 기록
|
||||
if (body.status === 'COMPLETED') {
|
||||
updates.push(`completed_at = NOW()`)
|
||||
} else if (existing.status === 'COMPLETED') {
|
||||
updates.push(`completed_at = NULL`)
|
||||
}
|
||||
}
|
||||
|
||||
if (body.priority !== undefined) {
|
||||
updates.push(`priority = $${paramIndex++}`)
|
||||
values.push(body.priority)
|
||||
}
|
||||
|
||||
if (updates.length === 0) {
|
||||
return { success: true, message: '변경된 내용이 없습니다.' }
|
||||
}
|
||||
|
||||
updates.push('updated_at = NOW()')
|
||||
values.push(todoId)
|
||||
|
||||
await execute(`
|
||||
UPDATE wr_todo SET ${updates.join(', ')} WHERE todo_id = $${paramIndex}
|
||||
`, values)
|
||||
|
||||
return { success: true }
|
||||
})
|
||||
55
server/api/todo/create.post.ts
Normal file
55
server/api/todo/create.post.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { insertReturning } from '../../utils/db'
|
||||
import { getCurrentUserId } from '../../utils/user'
|
||||
|
||||
interface CreateTodoBody {
|
||||
todoTitle: string
|
||||
todoContent?: string
|
||||
sourceType?: string
|
||||
sourceId?: number
|
||||
meetingId?: number
|
||||
projectId?: number
|
||||
assigneeId?: number
|
||||
dueDate?: string
|
||||
priority?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO 생성
|
||||
* POST /api/todo/create
|
||||
*/
|
||||
export default defineEventHandler(async (event) => {
|
||||
const body = await readBody<CreateTodoBody>(event)
|
||||
const userId = await getCurrentUserId(event)
|
||||
|
||||
if (!body.todoTitle?.trim()) {
|
||||
throw createError({ statusCode: 400, message: '제목을 입력해주세요.' })
|
||||
}
|
||||
|
||||
const result = await insertReturning(`
|
||||
INSERT INTO wr_todo (
|
||||
todo_title, todo_content, source_type, source_id, meeting_id,
|
||||
project_id, assignee_id, reporter_id, due_date, status, priority, created_by
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, 'PENDING', $10, $8)
|
||||
RETURNING *
|
||||
`, [
|
||||
body.todoTitle.trim(),
|
||||
body.todoContent || null,
|
||||
body.sourceType || null,
|
||||
body.sourceId || null,
|
||||
body.meetingId || null,
|
||||
body.projectId || null,
|
||||
body.assigneeId || null,
|
||||
userId,
|
||||
body.dueDate || null,
|
||||
body.priority || 1
|
||||
])
|
||||
|
||||
return {
|
||||
success: true,
|
||||
todo: {
|
||||
todoId: result.todo_id,
|
||||
todoTitle: result.todo_title,
|
||||
status: result.status
|
||||
}
|
||||
}
|
||||
})
|
||||
97
server/api/todo/list.get.ts
Normal file
97
server/api/todo/list.get.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { query } from '../../utils/db'
|
||||
import { getCurrentUserId } from '../../utils/user'
|
||||
|
||||
/**
|
||||
* TODO 목록 조회
|
||||
* GET /api/todo/list
|
||||
*/
|
||||
export default defineEventHandler(async (event) => {
|
||||
const params = getQuery(event)
|
||||
const userId = await getCurrentUserId(event)
|
||||
|
||||
const status = params.status as string | null
|
||||
const assigneeId = params.assigneeId ? Number(params.assigneeId) : null
|
||||
const projectId = params.projectId ? Number(params.projectId) : null
|
||||
const meetingId = params.meetingId ? Number(params.meetingId) : null
|
||||
const myOnly = params.myOnly === 'true'
|
||||
|
||||
const conditions: string[] = []
|
||||
const values: any[] = []
|
||||
let paramIndex = 1
|
||||
|
||||
if (status) {
|
||||
conditions.push(`t.status = $${paramIndex++}`)
|
||||
values.push(status)
|
||||
}
|
||||
|
||||
if (assigneeId) {
|
||||
conditions.push(`t.assignee_id = $${paramIndex++}`)
|
||||
values.push(assigneeId)
|
||||
}
|
||||
|
||||
if (projectId) {
|
||||
conditions.push(`t.project_id = $${paramIndex++}`)
|
||||
values.push(projectId)
|
||||
}
|
||||
|
||||
if (meetingId) {
|
||||
conditions.push(`t.meeting_id = $${paramIndex++}`)
|
||||
values.push(meetingId)
|
||||
}
|
||||
|
||||
if (myOnly && userId) {
|
||||
conditions.push(`(t.assignee_id = $${paramIndex} OR t.reporter_id = $${paramIndex})`)
|
||||
values.push(userId)
|
||||
paramIndex++
|
||||
}
|
||||
|
||||
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''
|
||||
|
||||
const sql = `
|
||||
SELECT
|
||||
t.*,
|
||||
p.project_name,
|
||||
m.meeting_title,
|
||||
a.employee_name as assignee_name,
|
||||
r.employee_name as reporter_name
|
||||
FROM wr_todo t
|
||||
LEFT JOIN wr_project_info p ON t.project_id = p.project_id
|
||||
LEFT JOIN wr_meeting m ON t.meeting_id = m.meeting_id
|
||||
LEFT JOIN wr_employee_info a ON t.assignee_id = a.employee_id
|
||||
LEFT JOIN wr_employee_info r ON t.reporter_id = r.employee_id
|
||||
${whereClause}
|
||||
ORDER BY
|
||||
CASE t.status WHEN 'PENDING' THEN 1 WHEN 'IN_PROGRESS' THEN 2 WHEN 'COMPLETED' THEN 3 ELSE 4 END,
|
||||
t.priority DESC,
|
||||
t.due_date ASC NULLS LAST,
|
||||
t.created_at DESC
|
||||
LIMIT 100
|
||||
`
|
||||
|
||||
const todos = await query(sql, values)
|
||||
|
||||
return {
|
||||
todos: todos.map((t: any) => ({
|
||||
todoId: t.todo_id,
|
||||
todoTitle: t.todo_title,
|
||||
todoContent: t.todo_content,
|
||||
sourceType: t.source_type,
|
||||
sourceId: t.source_id,
|
||||
meetingId: t.meeting_id,
|
||||
meetingTitle: t.meeting_title,
|
||||
projectId: t.project_id,
|
||||
projectName: t.project_name,
|
||||
assigneeId: t.assignee_id,
|
||||
assigneeName: t.assignee_name,
|
||||
reporterId: t.reporter_id,
|
||||
reporterName: t.reporter_name,
|
||||
dueDate: t.due_date,
|
||||
status: t.status,
|
||||
priority: t.priority,
|
||||
completedAt: t.completed_at,
|
||||
weeklyReportId: t.weekly_report_id,
|
||||
createdAt: t.created_at,
|
||||
updatedAt: t.updated_at
|
||||
}))
|
||||
}
|
||||
})
|
||||
43
server/api/todo/report/link.post.ts
Normal file
43
server/api/todo/report/link.post.ts
Normal 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가 주간보고와 연계되었습니다.'
|
||||
}
|
||||
})
|
||||
108
server/api/todo/report/similar.post.ts
Normal file
108
server/api/todo/report/similar.post.ts
Normal 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
|
||||
}
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user