작업계획서대로 진행

This commit is contained in:
2026-01-11 01:29:46 +09:00
parent 1b8cd8577e
commit 01bd66d524
51 changed files with 11124 additions and 273 deletions

View 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: '회의록이 삭제되었습니다.'
}
})

View 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
}))
}
})

View 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: '회의록이 수정되었습니다.'
}
})

View File

@@ -0,0 +1,92 @@
import { insertReturning, query, execute } from '../../utils/db'
import { getClientIp } from '../../utils/ip'
import { getCurrentUserEmail, getCurrentUserId } from '../../utils/user'
interface Attendee {
employeeId?: number
externalName?: string
externalCompany?: string
}
interface CreateMeetingBody {
meetingTitle: string
meetingType: 'PROJECT' | 'INTERNAL'
projectId?: number
meetingDate: string
startTime?: string
endTime?: string
location?: string
rawContent?: string
attendees?: Attendee[]
}
/**
* 회의록 작성
* POST /api/meeting/create
*/
export default defineEventHandler(async (event) => {
const body = await readBody<CreateMeetingBody>(event)
const clientIp = getClientIp(event)
const userEmail = await getCurrentUserEmail(event)
const userId = await getCurrentUserId(event)
// 필수값 검증
if (!body.meetingTitle) {
throw createError({ statusCode: 400, message: '회의 제목은 필수입니다.' })
}
if (!body.meetingType) {
throw createError({ statusCode: 400, message: '회의 유형은 필수입니다.' })
}
if (!body.meetingDate) {
throw createError({ statusCode: 400, message: '회의 일자는 필수입니다.' })
}
if (body.meetingType === 'PROJECT' && !body.projectId) {
throw createError({ statusCode: 400, message: '프로젝트 회의는 프로젝트 선택이 필수입니다.' })
}
// 회의록 INSERT
const meeting = await insertReturning(`
INSERT INTO wr_meeting (
meeting_title, meeting_type, project_id,
meeting_date, start_time, end_time, location,
raw_content, ai_status, author_id,
created_ip, created_email, updated_ip, updated_email
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, 'NONE', $9, $10, $11, $10, $11)
RETURNING *
`, [
body.meetingTitle,
body.meetingType,
body.meetingType === 'PROJECT' ? body.projectId : null,
body.meetingDate,
body.startTime || null,
body.endTime || null,
body.location || null,
body.rawContent || null,
userId,
clientIp,
userEmail
])
// 참석자 INSERT
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)
`, [meeting.meeting_id, att.employeeId])
} else if (att.externalName) {
await execute(`
INSERT INTO wr_meeting_attendee (meeting_id, external_name, external_company)
VALUES ($1, $2, $3)
`, [meeting.meeting_id, att.externalName, att.externalCompany || null])
}
}
}
return {
success: true,
meetingId: meeting.meeting_id,
message: '회의록이 등록되었습니다.'
}
})

View File

@@ -0,0 +1,122 @@
import { query } from '../../utils/db'
/**
* 회의록 목록 조회
* GET /api/meeting/list
*
* Query params:
* - projectId: 프로젝트 필터 (선택)
* - meetingType: PROJECT | INTERNAL (선택)
* - startDate: 시작일 (선택)
* - endDate: 종료일 (선택)
* - keyword: 검색어 (선택)
* - page: 페이지 번호 (기본 1)
* - pageSize: 페이지 크기 (기본 20)
*/
export default defineEventHandler(async (event) => {
const params = getQuery(event)
const projectId = params.projectId ? Number(params.projectId) : null
const meetingType = params.meetingType as string | null
const startDate = params.startDate as string | null
const endDate = params.endDate as string | null
const keyword = params.keyword as string | null
const page = Number(params.page) || 1
const pageSize = Number(params.pageSize) || 20
const offset = (page - 1) * pageSize
// WHERE 조건 구성
const conditions: string[] = []
const values: any[] = []
let paramIndex = 1
if (projectId) {
conditions.push(`m.project_id = $${paramIndex++}`)
values.push(projectId)
}
if (meetingType) {
conditions.push(`m.meeting_type = $${paramIndex++}`)
values.push(meetingType)
}
if (startDate) {
conditions.push(`m.meeting_date >= $${paramIndex++}`)
values.push(startDate)
}
if (endDate) {
conditions.push(`m.meeting_date <= $${paramIndex++}`)
values.push(endDate)
}
if (keyword) {
conditions.push(`(m.meeting_title ILIKE $${paramIndex} OR m.raw_content ILIKE $${paramIndex})`)
values.push(`%${keyword}%`)
paramIndex++
}
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''
// 전체 건수 조회
const countSql = `
SELECT COUNT(*) as total
FROM wr_meeting m
${whereClause}
`
const countResult = await query(countSql, values)
const total = Number(countResult[0]?.total || 0)
// 목록 조회
const listSql = `
SELECT
m.meeting_id,
m.meeting_title,
m.meeting_type,
m.project_id,
p.project_name,
m.meeting_date,
m.start_time,
m.end_time,
m.location,
m.ai_status,
m.author_id,
e.employee_name as author_name,
m.created_at,
(SELECT COUNT(*) FROM wr_meeting_attendee WHERE meeting_id = m.meeting_id) as attendee_count
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
${whereClause}
ORDER BY m.meeting_date DESC, m.created_at DESC
LIMIT $${paramIndex++} OFFSET $${paramIndex++}
`
values.push(pageSize, offset)
const meetings = await query(listSql, values)
return {
meetings: meetings.map((m: any) => ({
meetingId: m.meeting_id,
meetingTitle: m.meeting_title,
meetingType: m.meeting_type,
projectId: m.project_id,
projectName: m.project_name,
meetingDate: m.meeting_date,
startTime: m.start_time,
endTime: m.end_time,
location: m.location,
aiStatus: m.ai_status,
authorId: m.author_id,
authorName: m.author_name,
attendeeCount: Number(m.attendee_count),
createdAt: m.created_at
})),
pagination: {
page,
pageSize,
total,
totalPages: Math.ceil(total / pageSize)
}
}
})