기능구현중
This commit is contained in:
105
server/api/meeting/[id]/analyze.post.ts
Normal file
105
server/api/meeting/[id]/analyze.post.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { queryOne, execute } from '../../../utils/db'
|
||||
import { requireAuth } from '../../../utils/session'
|
||||
import { callOpenAI } 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 callOpenAI([
|
||||
{ role: 'system', content: systemPrompt },
|
||||
{ role: 'user', content: userPrompt }
|
||||
], true, 'gpt-4o-mini')
|
||||
|
||||
// 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
server/api/meeting/[id]/confirm.post.ts
Normal file
82
server/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
|
||||
}
|
||||
})
|
||||
30
server/api/meeting/[id]/delete.delete.ts
Normal file
30
server/api/meeting/[id]/delete.delete.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { queryOne, execute } from '../../../utils/db'
|
||||
|
||||
/**
|
||||
* 회의록 삭제
|
||||
* DELETE /api/meeting/[id]/delete
|
||||
*/
|
||||
export default defineEventHandler(async (event) => {
|
||||
const meetingId = Number(getRouterParam(event, 'id'))
|
||||
|
||||
if (!meetingId) {
|
||||
throw createError({ statusCode: 400, message: '회의록 ID가 필요합니다.' })
|
||||
}
|
||||
|
||||
// 회의록 존재 확인
|
||||
const existing = await queryOne(`
|
||||
SELECT meeting_id FROM wr_meeting WHERE meeting_id = $1
|
||||
`, [meetingId])
|
||||
|
||||
if (!existing) {
|
||||
throw createError({ statusCode: 404, message: '회의록을 찾을 수 없습니다.' })
|
||||
}
|
||||
|
||||
// CASCADE 설정으로 참석자, 안건도 함께 삭제됨
|
||||
await execute(`DELETE FROM wr_meeting WHERE meeting_id = $1`, [meetingId])
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: '회의록이 삭제되었습니다.'
|
||||
}
|
||||
})
|
||||
96
server/api/meeting/[id]/detail.get.ts
Normal file
96
server/api/meeting/[id]/detail.get.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { query, queryOne } from '../../../utils/db'
|
||||
|
||||
/**
|
||||
* 회의록 상세 조회
|
||||
* GET /api/meeting/[id]/detail
|
||||
*/
|
||||
export default defineEventHandler(async (event) => {
|
||||
const meetingId = Number(getRouterParam(event, 'id'))
|
||||
|
||||
if (!meetingId) {
|
||||
throw createError({ statusCode: 400, message: '회의록 ID가 필요합니다.' })
|
||||
}
|
||||
|
||||
// 회의록 기본 정보
|
||||
const meeting = await queryOne(`
|
||||
SELECT
|
||||
m.*,
|
||||
p.project_name,
|
||||
e.employee_name as author_name,
|
||||
e.employee_email as author_email
|
||||
FROM wr_meeting m
|
||||
LEFT JOIN wr_project_info p ON m.project_id = p.project_id
|
||||
LEFT JOIN wr_employee_info e ON m.author_id = e.employee_id
|
||||
WHERE m.meeting_id = $1
|
||||
`, [meetingId])
|
||||
|
||||
if (!meeting) {
|
||||
throw createError({ statusCode: 404, message: '회의록을 찾을 수 없습니다.' })
|
||||
}
|
||||
|
||||
// 참석자 목록
|
||||
const attendees = await query(`
|
||||
SELECT
|
||||
a.attendee_id,
|
||||
a.employee_id,
|
||||
e.employee_name,
|
||||
e.employee_email,
|
||||
e.company,
|
||||
a.external_name,
|
||||
a.external_company
|
||||
FROM wr_meeting_attendee a
|
||||
LEFT JOIN wr_employee_info e ON a.employee_id = e.employee_id
|
||||
WHERE a.meeting_id = $1
|
||||
ORDER BY a.attendee_id
|
||||
`, [meetingId])
|
||||
|
||||
// 안건 목록 (AI 분석 결과)
|
||||
const agendas = await query(`
|
||||
SELECT *
|
||||
FROM wr_meeting_agenda
|
||||
WHERE meeting_id = $1
|
||||
ORDER BY agenda_no
|
||||
`, [meetingId])
|
||||
|
||||
return {
|
||||
meeting: {
|
||||
meetingId: meeting.meeting_id,
|
||||
meetingTitle: meeting.meeting_title,
|
||||
meetingType: meeting.meeting_type,
|
||||
projectId: meeting.project_id,
|
||||
projectName: meeting.project_name,
|
||||
meetingDate: meeting.meeting_date,
|
||||
startTime: meeting.start_time,
|
||||
endTime: meeting.end_time,
|
||||
location: meeting.location,
|
||||
rawContent: meeting.raw_content,
|
||||
aiSummary: meeting.ai_summary,
|
||||
aiStatus: meeting.ai_status,
|
||||
aiProcessedAt: meeting.ai_processed_at,
|
||||
aiConfirmedAt: meeting.ai_confirmed_at,
|
||||
authorId: meeting.author_id,
|
||||
authorName: meeting.author_name,
|
||||
authorEmail: meeting.author_email,
|
||||
createdAt: meeting.created_at,
|
||||
updatedAt: meeting.updated_at
|
||||
},
|
||||
attendees: attendees.map((a: any) => ({
|
||||
attendeeId: a.attendee_id,
|
||||
employeeId: a.employee_id,
|
||||
employeeName: a.employee_name,
|
||||
employeeEmail: a.employee_email,
|
||||
company: a.company,
|
||||
externalName: a.external_name,
|
||||
externalCompany: a.external_company,
|
||||
isExternal: !a.employee_id
|
||||
})),
|
||||
agendas: agendas.map((a: any) => ({
|
||||
agendaId: a.agenda_id,
|
||||
agendaNo: a.agenda_no,
|
||||
agendaTitle: a.agenda_title,
|
||||
agendaContent: a.agenda_content,
|
||||
decisionStatus: a.decision_status,
|
||||
decisionContent: a.decision_content
|
||||
}))
|
||||
}
|
||||
})
|
||||
107
server/api/meeting/[id]/update.put.ts
Normal file
107
server/api/meeting/[id]/update.put.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { queryOne, execute } from '../../../utils/db'
|
||||
import { getClientIp } from '../../../utils/ip'
|
||||
import { getCurrentUserEmail } from '../../../utils/user'
|
||||
|
||||
interface Attendee {
|
||||
employeeId?: number
|
||||
externalName?: string
|
||||
externalCompany?: string
|
||||
}
|
||||
|
||||
interface UpdateMeetingBody {
|
||||
meetingTitle: string
|
||||
meetingType: 'PROJECT' | 'INTERNAL'
|
||||
projectId?: number
|
||||
meetingDate: string
|
||||
startTime?: string
|
||||
endTime?: string
|
||||
location?: string
|
||||
rawContent?: string
|
||||
attendees?: Attendee[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 회의록 수정
|
||||
* PUT /api/meeting/[id]/update
|
||||
*/
|
||||
export default defineEventHandler(async (event) => {
|
||||
const meetingId = Number(getRouterParam(event, 'id'))
|
||||
const body = await readBody<UpdateMeetingBody>(event)
|
||||
const clientIp = getClientIp(event)
|
||||
const userEmail = await getCurrentUserEmail(event)
|
||||
|
||||
if (!meetingId) {
|
||||
throw createError({ statusCode: 400, message: '회의록 ID가 필요합니다.' })
|
||||
}
|
||||
|
||||
// 회의록 존재 확인
|
||||
const existing = await queryOne(`
|
||||
SELECT meeting_id FROM wr_meeting WHERE meeting_id = $1
|
||||
`, [meetingId])
|
||||
|
||||
if (!existing) {
|
||||
throw createError({ statusCode: 404, message: '회의록을 찾을 수 없습니다.' })
|
||||
}
|
||||
|
||||
// 필수값 검증
|
||||
if (!body.meetingTitle) {
|
||||
throw createError({ statusCode: 400, message: '회의 제목은 필수입니다.' })
|
||||
}
|
||||
if (body.meetingType === 'PROJECT' && !body.projectId) {
|
||||
throw createError({ statusCode: 400, message: '프로젝트 회의는 프로젝트 선택이 필수입니다.' })
|
||||
}
|
||||
|
||||
// 회의록 UPDATE
|
||||
await execute(`
|
||||
UPDATE wr_meeting SET
|
||||
meeting_title = $1,
|
||||
meeting_type = $2,
|
||||
project_id = $3,
|
||||
meeting_date = $4,
|
||||
start_time = $5,
|
||||
end_time = $6,
|
||||
location = $7,
|
||||
raw_content = $8,
|
||||
updated_at = NOW(),
|
||||
updated_ip = $9,
|
||||
updated_email = $10
|
||||
WHERE meeting_id = $11
|
||||
`, [
|
||||
body.meetingTitle,
|
||||
body.meetingType,
|
||||
body.meetingType === 'PROJECT' ? body.projectId : null,
|
||||
body.meetingDate,
|
||||
body.startTime || null,
|
||||
body.endTime || null,
|
||||
body.location || null,
|
||||
body.rawContent || null,
|
||||
clientIp,
|
||||
userEmail,
|
||||
meetingId
|
||||
])
|
||||
|
||||
// 참석자 갱신 (기존 삭제 후 새로 INSERT)
|
||||
await execute(`DELETE FROM wr_meeting_attendee WHERE meeting_id = $1`, [meetingId])
|
||||
|
||||
if (body.attendees && body.attendees.length > 0) {
|
||||
for (const att of body.attendees) {
|
||||
if (att.employeeId) {
|
||||
await execute(`
|
||||
INSERT INTO wr_meeting_attendee (meeting_id, employee_id)
|
||||
VALUES ($1, $2)
|
||||
`, [meetingId, att.employeeId])
|
||||
} else if (att.externalName) {
|
||||
await execute(`
|
||||
INSERT INTO wr_meeting_attendee (meeting_id, external_name, external_company)
|
||||
VALUES ($1, $2, $3)
|
||||
`, [meetingId, att.externalName, att.externalCompany || null])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
meetingId,
|
||||
message: '회의록이 수정되었습니다.'
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user