mirror of
https://github.com/nad4-su/meeting-minutes.git
synced 2026-08-12 22:33:25 +09:00
* feat(phase-1): Prisma 연동 + 회의록 CRUD API Phase 1 foundation (ROADMAP #1~#3): - src/lib/db.ts: Prisma 클라이언트 싱글톤 (dev hot-reload 대응) - Meeting 스키마 확장: attendees/tags (String[]), template/depth/ summaryMode/customPrompt (향후 참조 복원용), createdAt 인덱스 - 첫 마이그레이션 `20260421104750_init` 생성 - /api/meetings (GET 목록 + POST 생성) - q 파라미터로 제목/transcript/markdown 부분 검색 - limit (max 100), offset 페이지네이션 - attendees/tags 배열 타입 정규화 - /api/meetings/[id] (GET/PUT/DELETE) - PUT은 markdownMinutes/title/attendees/tags만 편집 허용 - 존재하지 않는 id에 대해 404 - docker-compose.yml: tools 프로필에 migrate 서비스 추가 (app 컨테이너 bloat 없이 마이그레이션 실행) - Prisma mock 기반 API 테스트 14개 신규 (총 71 tests) * docs: migrate 실행 단계 추가, ROADMAP의 #1/#2 완료 체크 * docs: PR 본문 드래프트 추가
120 lines
3.2 KiB
TypeScript
120 lines
3.2 KiB
TypeScript
import { NextRequest } from 'next/server'
|
|
import { prisma } from '@/lib/db'
|
|
|
|
const DEFAULT_LIMIT = 20
|
|
const MAX_LIMIT = 100
|
|
|
|
export async function GET(request: NextRequest) {
|
|
try {
|
|
const url = new URL(request.url)
|
|
const q = url.searchParams.get('q')?.trim() ?? ''
|
|
const limit = Math.min(
|
|
MAX_LIMIT,
|
|
Math.max(1, Number(url.searchParams.get('limit')) || DEFAULT_LIMIT),
|
|
)
|
|
const offset = Math.max(0, Number(url.searchParams.get('offset')) || 0)
|
|
|
|
const where = q
|
|
? {
|
|
OR: [
|
|
{ title: { contains: q, mode: 'insensitive' as const } },
|
|
{ rawTranscript: { contains: q, mode: 'insensitive' as const } },
|
|
{ markdownMinutes: { contains: q, mode: 'insensitive' as const } },
|
|
],
|
|
}
|
|
: undefined
|
|
|
|
const [meetings, total] = await Promise.all([
|
|
prisma.meeting.findMany({
|
|
where,
|
|
orderBy: { createdAt: 'desc' },
|
|
take: limit,
|
|
skip: offset,
|
|
select: {
|
|
id: true,
|
|
title: true,
|
|
createdAt: true,
|
|
updatedAt: true,
|
|
tags: true,
|
|
attendees: true,
|
|
template: true,
|
|
summaryMode: true,
|
|
status: true,
|
|
},
|
|
}),
|
|
prisma.meeting.count({ where }),
|
|
])
|
|
|
|
return Response.json({
|
|
meetings,
|
|
total,
|
|
limit,
|
|
offset,
|
|
})
|
|
} catch (err) {
|
|
const message = err instanceof Error ? err.message : '알 수 없는 오류'
|
|
return Response.json(
|
|
{ error: `회의록 목록 조회 실패: ${message}` },
|
|
{ status: 500 },
|
|
)
|
|
}
|
|
}
|
|
|
|
export async function POST(request: NextRequest) {
|
|
try {
|
|
const body = await request.json()
|
|
const {
|
|
title,
|
|
rawTranscript,
|
|
markdownMinutes,
|
|
summaryMode,
|
|
template,
|
|
depth,
|
|
customPrompt,
|
|
attendees,
|
|
tags,
|
|
} = body
|
|
|
|
if (!title || typeof title !== 'string' || title.trim().length === 0) {
|
|
return Response.json(
|
|
{ error: '제목은 필수입니다.' },
|
|
{ status: 400 },
|
|
)
|
|
}
|
|
|
|
if (!markdownMinutes || typeof markdownMinutes !== 'string') {
|
|
return Response.json(
|
|
{ error: '회의록 본문은 필수입니다.' },
|
|
{ status: 400 },
|
|
)
|
|
}
|
|
|
|
const meeting = await prisma.meeting.create({
|
|
data: {
|
|
title: title.trim(),
|
|
rawTranscript: typeof rawTranscript === 'string' ? rawTranscript : null,
|
|
markdownMinutes,
|
|
summaryMode: typeof summaryMode === 'string' ? summaryMode : null,
|
|
template: typeof template === 'string' ? template : null,
|
|
depth: typeof depth === 'string' ? depth : null,
|
|
customPrompt: typeof customPrompt === 'string' ? customPrompt : null,
|
|
attendees: Array.isArray(attendees)
|
|
? attendees.filter((a) => typeof a === 'string')
|
|
: [],
|
|
tags: Array.isArray(tags)
|
|
? tags.filter((t) => typeof t === 'string')
|
|
: [],
|
|
status: 'COMPLETED',
|
|
},
|
|
})
|
|
|
|
return Response.json(meeting, { status: 201 })
|
|
} catch (err) {
|
|
const message = err instanceof Error ? err.message : '알 수 없는 오류'
|
|
return Response.json(
|
|
{ error: `회의록 생성 실패: ${message}` },
|
|
{ status: 500 },
|
|
)
|
|
}
|
|
}
|