feat: 회의록 자동 작성 웹 서비스 구현

- 오디오 파일 업로드 + 유효성 검사
- 실시간 음성 인식 (Web Speech API)
- 회의록 생성 (단순 STT→MD 변환 / Gemini AI 요약)
- 마크다운/HTML 내보내기
- PostgreSQL + Prisma ORM
- Docker Compose 배포 지원
- TDD: 31개 테스트, 96% 커버리지
This commit is contained in:
2026-04-11 23:18:05 +09:00
parent b5743fe896
commit adb48c2352
31 changed files with 4288 additions and 82 deletions
+91
View File
@@ -0,0 +1,91 @@
'use client'
import { useSpeechRecognition } from '@/hooks/useSpeechRecognition'
import { formatTranscriptChunks } from '@/lib/transcript-formatter'
interface LiveRecorderProps {
onTranscriptReady: (transcript: string) => void
}
export function LiveRecorder({ onTranscriptReady }: LiveRecorderProps) {
const {
isListening,
isSupported,
chunks,
interimText,
startListening,
stopListening,
resetChunks,
} = useSpeechRecognition()
if (!isSupported) {
return (
<div className="rounded-2xl border border-amber-200 bg-amber-50 p-6 text-center">
<p className="text-amber-800">
.
Chrome .
</p>
</div>
)
}
function handleStop() {
stopListening()
const transcript = formatTranscriptChunks(chunks)
if (transcript.length > 0) {
onTranscriptReady(transcript)
}
}
return (
<div className="space-y-4">
<div className="flex items-center gap-3">
{isListening ? (
<button
onClick={handleStop}
className="flex items-center gap-2 rounded-full bg-red-500 px-6 py-3 text-white font-medium shadow-lg shadow-red-500/25 hover:bg-red-600 transition-colors"
>
<span className="relative flex h-3 w-3">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-white opacity-75" />
<span className="relative inline-flex h-3 w-3 rounded-full bg-white" />
</span>
</button>
) : (
<button
onClick={startListening}
className="flex items-center gap-2 rounded-full bg-blue-600 px-6 py-3 text-white font-medium shadow-lg shadow-blue-600/25 hover:bg-blue-700 transition-colors"
>
<span className="text-lg">🎤</span>
</button>
)}
{chunks.length > 0 && !isListening && (
<button
onClick={resetChunks}
className="rounded-full border border-neutral-300 px-4 py-2 text-sm text-neutral-600 hover:bg-neutral-50 transition-colors"
>
</button>
)}
</div>
{(chunks.length > 0 || interimText) && (
<div className="rounded-2xl border border-neutral-200 bg-neutral-50 p-6 max-h-64 overflow-y-auto">
<h3 className="text-sm font-semibold text-neutral-500 mb-3 uppercase tracking-wider">
</h3>
<div className="space-y-1 text-sm font-mono text-neutral-700">
{chunks.map((chunk, i) => (
<p key={i}>{chunk.text}</p>
))}
{interimText && (
<p className="text-neutral-400 italic">{interimText}...</p>
)}
</div>
</div>
)}
</div>
)
}