'use client' import { useEffect, useRef } from 'react' import { useSpeechRecognition } from '@/hooks/useSpeechRecognition' import { useLiveSummary } from '@/hooks/useLiveSummary' import { formatTranscriptChunks } from '@/lib/transcript-formatter' interface LiveRecorderProps { onTranscriptReady: (transcript: string) => void liveSummaryEnabled: boolean } export function LiveRecorder({ onTranscriptReady, liveSummaryEnabled, }: LiveRecorderProps) { const { isListening, isSupported, chunks, interimText, error: recognitionError, startListening, stopListening, resetChunks, } = useSpeechRecognition() const { summary, isSummarizing, error: summaryError, lastUpdatedAt, wordCount, wordsUntilNext, minWords, cooldownUntil, } = useLiveSummary(chunks, { enabled: liveSummaryEnabled && isListening, }) const transcriptEndRef = useRef(null) const summaryEndRef = useRef(null) useEffect(() => { transcriptEndRef.current?.scrollIntoView({ behavior: 'smooth', block: 'end' }) }, [chunks.length, interimText]) useEffect(() => { summaryEndRef.current?.scrollIntoView({ behavior: 'smooth', block: 'end' }) }, [summary]) if (isSupported === null) { return (
브라우저 기능 확인 중...
) } if (!isSupported) { return (

이 브라우저는 음성 인식을 지원하지 않습니다. Chrome 브라우저를 사용해주세요.

) } function handleStop() { stopListening() const transcript = formatTranscriptChunks(chunks) if (transcript.length > 0) { onTranscriptReady(transcript) } } const lastUpdatedLabel = lastUpdatedAt ? new Date(lastUpdatedAt).toLocaleTimeString('ko-KR', { hour: '2-digit', minute: '2-digit', second: '2-digit', }) : null const hasTranscript = chunks.length > 0 || interimText.length > 0 const hasSummary = summary.length > 0 const showPanels = isListening || hasTranscript || hasSummary const cooldownSeconds = cooldownUntil && cooldownUntil > Date.now() ? Math.ceil((cooldownUntil - Date.now()) / 1000) : 0 const summaryProgress = (() => { if (!liveSummaryEnabled) return null if (!isListening && !hasSummary) return null if (cooldownSeconds > 0) return `⏸️ API 쿼터 초과 — ${cooldownSeconds}초 후 재시도` if (isSummarizing) return '요약 생성 중...' if (!hasSummary) { if (wordCount < minWords) { return `첫 요약까지 ${wordsUntilNext}단어 (${wordCount}/${minWords})` } return '곧 첫 요약이 생성됩니다...' } if (wordsUntilNext > 0 && isListening) { return `다음 갱신까지 ${wordsUntilNext}단어` } return isListening ? '다음 갱신 대기 중...' : '녹음 종료됨' })() return (
{isListening ? ( ) : ( )} {chunks.length > 0 && !isListening && ( )} {isListening && (
녹음 중 · {wordCount}단어 · {chunks.length}개 구간
)}
{recognitionError && (
음성 인식 오류: {recognitionError}
)} {isListening && !hasTranscript && (

🎙️ 마이크가 활성화되었습니다. 말씀해주세요...

아무 반응이 없다면 브라우저 주소창 왼쪽에서 마이크 권한을 확인해주세요.

)} {showPanels && (

실시간 텍스트

{chunks.length}개 구간 · {wordCount}단어
{hasTranscript ? (
{chunks.map((chunk, i) => (

{chunk.text}

))} {interimText && (

{interimText}

)}
) : (

{isListening ? '발화를 기다리는 중...' : '녹음을 시작하면 여기에 텍스트가 나타납니다.'}

)}
{liveSummaryEnabled && (

실시간 회의록

{summaryProgress && {summaryProgress}} {lastUpdatedLabel && · {lastUpdatedLabel}}
{summaryError ? (

{summaryError}

) : hasSummary ? ( <>
                      {summary}
                    
) : (

발화가 쌓이면 30초 간격으로 Gemini가 중간 회의록을 작성합니다.

진행률 {Math.min(wordCount, minWords)}/{minWords}단어
)}
)}
)}
) }