기능구현중
This commit is contained in:
105
backend/api/meeting/[id]/analyze.post.ts
Normal file
105
backend/api/meeting/[id]/analyze.post.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { queryOne, execute } from '../../../utils/db'
|
||||
import { requireAuth } from '../../../utils/session'
|
||||
import { chatCompletion } from '../../../utils/openai'
|
||||
|
||||
/**
|
||||
* 회의록 AI 분석
|
||||
* POST /api/meeting/[id]/analyze
|
||||
*/
|
||||
export default defineEventHandler(async (event) => {
|
||||
await requireAuth(event)
|
||||
const meetingId = parseInt(event.context.params?.id || '0')
|
||||
|
||||
if (!meetingId) {
|
||||
throw createError({ statusCode: 400, message: '회의록 ID가 필요합니다.' })
|
||||
}
|
||||
|
||||
// 회의록 조회
|
||||
const meeting = await queryOne<any>(`
|
||||
SELECT m.*, p.project_name
|
||||
FROM wr_meeting m
|
||||
LEFT JOIN wr_project_info p ON m.project_id = p.project_id
|
||||
WHERE m.meeting_id = $1
|
||||
`, [meetingId])
|
||||
|
||||
if (!meeting) {
|
||||
throw createError({ statusCode: 404, message: '회의록을 찾을 수 없습니다.' })
|
||||
}
|
||||
|
||||
if (!meeting.raw_content) {
|
||||
throw createError({ statusCode: 400, message: '분석할 회의 내용이 없습니다.' })
|
||||
}
|
||||
|
||||
// AI 프롬프트
|
||||
const systemPrompt = `당신은 회의록 정리 전문가입니다.
|
||||
아래 회의 내용을 분석하여 JSON 형식으로 정리해주세요.
|
||||
|
||||
## 출력 형식 (JSON만 출력, 다른 텍스트 없이)
|
||||
{
|
||||
"agendas": [
|
||||
{
|
||||
"no": 1,
|
||||
"title": "안건 제목",
|
||||
"content": "상세 내용 요약",
|
||||
"status": "DECIDED | PENDING | IN_PROGRESS",
|
||||
"decision": "결정 내용 (결정된 경우만)",
|
||||
"todos": [
|
||||
{
|
||||
"title": "TODO 제목",
|
||||
"assignee": "담당자명 또는 null",
|
||||
"reason": "TODO로 추출한 이유"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"summary": "전체 회의 요약 (2-3문장)"
|
||||
}
|
||||
|
||||
## 규칙
|
||||
1. 안건은 주제별로 분리하여 넘버링
|
||||
2. 결정된 사항은 DECIDED, 추후 논의는 PENDING, 진행중은 IN_PROGRESS
|
||||
3. 미결정/진행중 사항 중 액션이 필요한 것은 todos로 추출
|
||||
4. 담당자가 언급되면 assignee에 기록 (없으면 null)
|
||||
5. JSON 외 다른 텍스트 출력 금지`
|
||||
|
||||
const userPrompt = `## 회의 정보
|
||||
- 제목: ${meeting.meeting_title}
|
||||
- 프로젝트: ${meeting.project_name || '없음 (내부업무)'}
|
||||
- 일자: ${meeting.meeting_date}
|
||||
|
||||
## 회의 내용
|
||||
${meeting.raw_content}`
|
||||
|
||||
try {
|
||||
const result = await chatCompletion([
|
||||
{ role: 'system', content: systemPrompt },
|
||||
{ role: 'user', content: userPrompt }
|
||||
], { model: 'gpt-4o-mini', temperature: 0.3 })
|
||||
|
||||
// JSON 파싱
|
||||
let aiResult: any
|
||||
try {
|
||||
// JSON 블록 추출 (```json ... ``` 형태 처리)
|
||||
let jsonStr = result.trim()
|
||||
if (jsonStr.startsWith('```')) {
|
||||
jsonStr = jsonStr.replace(/^```json?\n?/, '').replace(/\n?```$/, '')
|
||||
}
|
||||
aiResult = JSON.parse(jsonStr)
|
||||
} catch (e) {
|
||||
console.error('AI result parse error:', result)
|
||||
throw createError({ statusCode: 500, message: 'AI 응답 파싱 실패' })
|
||||
}
|
||||
|
||||
// DB 저장
|
||||
await execute(`
|
||||
UPDATE wr_meeting
|
||||
SET ai_summary = $1, ai_status = 'PENDING', ai_processed_at = NOW()
|
||||
WHERE meeting_id = $2
|
||||
`, [JSON.stringify(aiResult), meetingId])
|
||||
|
||||
return { success: true, result: aiResult }
|
||||
} catch (e: any) {
|
||||
console.error('AI analyze error:', e)
|
||||
throw createError({ statusCode: 500, message: e.message || 'AI 분석 실패' })
|
||||
}
|
||||
})
|
||||
82
backend/api/meeting/[id]/confirm.post.ts
Normal file
82
backend/api/meeting/[id]/confirm.post.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { queryOne, execute, insertReturning } from '../../../utils/db'
|
||||
import { requireAuth } from '../../../utils/session'
|
||||
import { getClientIp } from '../../../utils/ip'
|
||||
|
||||
interface ConfirmBody {
|
||||
selectedTodos?: Array<{
|
||||
agendaNo: number
|
||||
todoIndex: number
|
||||
title: string
|
||||
assignee?: string
|
||||
}>
|
||||
}
|
||||
|
||||
/**
|
||||
* AI 분석 결과 확정 + TODO 생성
|
||||
* POST /api/meeting/[id]/confirm
|
||||
*/
|
||||
export default defineEventHandler(async (event) => {
|
||||
const employeeId = await requireAuth(event)
|
||||
const meetingId = parseInt(event.context.params?.id || '0')
|
||||
const body = await readBody<ConfirmBody>(event)
|
||||
const ip = getClientIp(event)
|
||||
|
||||
if (!meetingId) {
|
||||
throw createError({ statusCode: 400, message: '회의록 ID가 필요합니다.' })
|
||||
}
|
||||
|
||||
// 회의록 조회
|
||||
const meeting = await queryOne<any>(`
|
||||
SELECT meeting_id, ai_summary, ai_status, project_id
|
||||
FROM wr_meeting WHERE meeting_id = $1
|
||||
`, [meetingId])
|
||||
|
||||
if (!meeting) {
|
||||
throw createError({ statusCode: 404, message: '회의록을 찾을 수 없습니다.' })
|
||||
}
|
||||
|
||||
if (!meeting.ai_summary) {
|
||||
throw createError({ statusCode: 400, message: 'AI 분석 결과가 없습니다.' })
|
||||
}
|
||||
|
||||
const aiResult = typeof meeting.ai_summary === 'string'
|
||||
? JSON.parse(meeting.ai_summary)
|
||||
: meeting.ai_summary
|
||||
|
||||
// 선택된 TODO 생성
|
||||
const createdTodos: any[] = []
|
||||
|
||||
if (body.selectedTodos && body.selectedTodos.length > 0) {
|
||||
for (const todo of body.selectedTodos) {
|
||||
const inserted = await insertReturning(`
|
||||
INSERT INTO wr_todo (
|
||||
source_type, meeting_id, project_id,
|
||||
todo_title, todo_description, todo_status,
|
||||
author_id, created_at, created_ip
|
||||
) VALUES ('MEETING', $1, $2, $3, $4, 'PENDING', $5, NOW(), $6)
|
||||
RETURNING todo_id
|
||||
`, [
|
||||
meetingId,
|
||||
meeting.project_id,
|
||||
todo.title,
|
||||
`안건 ${todo.agendaNo}에서 추출`,
|
||||
employeeId,
|
||||
ip
|
||||
])
|
||||
createdTodos.push({ todoId: inserted.todo_id, title: todo.title })
|
||||
}
|
||||
}
|
||||
|
||||
// 상태 업데이트
|
||||
await execute(`
|
||||
UPDATE wr_meeting
|
||||
SET ai_status = 'CONFIRMED', ai_confirmed_at = NOW()
|
||||
WHERE meeting_id = $1
|
||||
`, [meetingId])
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `확정 완료. ${createdTodos.length}개의 TODO가 생성되었습니다.`,
|
||||
createdTodos
|
||||
}
|
||||
})
|
||||
22
backend/api/repository/[id]/index.delete.ts
Normal file
22
backend/api/repository/[id]/index.delete.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { execute } from '../../../utils/db'
|
||||
import { requireAuth } from '../../../utils/session'
|
||||
|
||||
/**
|
||||
* 저장소 삭제 (비활성화)
|
||||
* DELETE /api/repository/[id]
|
||||
*/
|
||||
export default defineEventHandler(async (event) => {
|
||||
await requireAuth(event)
|
||||
const repoId = parseInt(event.context.params?.id || '0')
|
||||
|
||||
if (!repoId) {
|
||||
throw createError({ statusCode: 400, message: '저장소 ID가 필요합니다.' })
|
||||
}
|
||||
|
||||
// 실제 삭제 대신 비활성화
|
||||
await execute(`
|
||||
UPDATE wr_repository SET is_active = false, updated_at = NOW() WHERE repo_id = $1
|
||||
`, [repoId])
|
||||
|
||||
return { success: true }
|
||||
})
|
||||
64
backend/api/repository/[id]/index.put.ts
Normal file
64
backend/api/repository/[id]/index.put.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { execute } from '../../../utils/db'
|
||||
import { requireAuth } from '../../../utils/session'
|
||||
import { getClientIp } from '../../../utils/ip'
|
||||
|
||||
interface UpdateRepoBody {
|
||||
repoName?: string
|
||||
repoPath?: string
|
||||
repoUrl?: string
|
||||
defaultBranch?: string
|
||||
description?: string
|
||||
isActive?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* 저장소 수정
|
||||
* PUT /api/repository/[id]
|
||||
*/
|
||||
export default defineEventHandler(async (event) => {
|
||||
await requireAuth(event)
|
||||
const repoId = parseInt(event.context.params?.id || '0')
|
||||
const body = await readBody<UpdateRepoBody>(event)
|
||||
const ip = getClientIp(event)
|
||||
|
||||
if (!repoId) {
|
||||
throw createError({ statusCode: 400, message: '저장소 ID가 필요합니다.' })
|
||||
}
|
||||
|
||||
const updates: string[] = ['updated_at = NOW()', 'updated_ip = $1']
|
||||
const values: any[] = [ip]
|
||||
let idx = 2
|
||||
|
||||
if (body.repoName !== undefined) {
|
||||
updates.push(`repo_name = $${idx++}`)
|
||||
values.push(body.repoName)
|
||||
}
|
||||
if (body.repoPath !== undefined) {
|
||||
updates.push(`repo_path = $${idx++}`)
|
||||
values.push(body.repoPath)
|
||||
}
|
||||
if (body.repoUrl !== undefined) {
|
||||
updates.push(`repo_url = $${idx++}`)
|
||||
values.push(body.repoUrl)
|
||||
}
|
||||
if (body.defaultBranch !== undefined) {
|
||||
updates.push(`default_branch = $${idx++}`)
|
||||
values.push(body.defaultBranch)
|
||||
}
|
||||
if (body.description !== undefined) {
|
||||
updates.push(`description = $${idx++}`)
|
||||
values.push(body.description)
|
||||
}
|
||||
if (body.isActive !== undefined) {
|
||||
updates.push(`is_active = $${idx++}`)
|
||||
values.push(body.isActive)
|
||||
}
|
||||
|
||||
values.push(repoId)
|
||||
|
||||
await execute(`
|
||||
UPDATE wr_repository SET ${updates.join(', ')} WHERE repo_id = $${idx}
|
||||
`, values)
|
||||
|
||||
return { success: true }
|
||||
})
|
||||
47
backend/api/repository/create.post.ts
Normal file
47
backend/api/repository/create.post.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { insertReturning } from '../../utils/db'
|
||||
import { requireAuth } from '../../utils/session'
|
||||
import { getClientIp } from '../../utils/ip'
|
||||
|
||||
interface CreateRepoBody {
|
||||
projectId: number
|
||||
serverId: number
|
||||
repoName: string
|
||||
repoPath: string
|
||||
repoUrl?: string
|
||||
defaultBranch?: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 저장소 추가
|
||||
* POST /api/repository/create
|
||||
*/
|
||||
export default defineEventHandler(async (event) => {
|
||||
const employeeId = await requireAuth(event)
|
||||
const body = await readBody<CreateRepoBody>(event)
|
||||
const ip = getClientIp(event)
|
||||
|
||||
if (!body.projectId || !body.serverId || !body.repoPath) {
|
||||
throw createError({ statusCode: 400, message: '필수 항목을 입력해주세요.' })
|
||||
}
|
||||
|
||||
const repo = await insertReturning(`
|
||||
INSERT INTO wr_repository (
|
||||
project_id, server_id, repo_name, repo_path, repo_url,
|
||||
default_branch, description, created_by, created_at, created_ip
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, NOW(), $9)
|
||||
RETURNING repo_id
|
||||
`, [
|
||||
body.projectId,
|
||||
body.serverId,
|
||||
body.repoName || body.repoPath,
|
||||
body.repoPath,
|
||||
body.repoUrl || null,
|
||||
body.defaultBranch || 'main',
|
||||
body.description || null,
|
||||
employeeId,
|
||||
ip
|
||||
])
|
||||
|
||||
return { success: true, repoId: repo.repo_id }
|
||||
})
|
||||
23
backend/api/todo/[id]/complete.put.ts
Normal file
23
backend/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: '완료 처리되었습니다.' }
|
||||
})
|
||||
28
backend/api/todo/[id]/discard.put.ts
Normal file
28
backend/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: '폐기 처리되었습니다.' }
|
||||
})
|
||||
Reference in New Issue
Block a user