작업계획서대로 진행
This commit is contained in:
30
backend/api/meeting/[id]/delete.delete.ts
Normal file
30
backend/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
backend/api/meeting/[id]/detail.get.ts
Normal file
96
backend/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
backend/api/meeting/[id]/update.put.ts
Normal file
107
backend/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: '회의록이 수정되었습니다.'
|
||||
}
|
||||
})
|
||||
92
backend/api/meeting/create.post.ts
Normal file
92
backend/api/meeting/create.post.ts
Normal 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: '회의록이 등록되었습니다.'
|
||||
}
|
||||
})
|
||||
122
backend/api/meeting/list.get.ts
Normal file
122
backend/api/meeting/list.get.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user