From e638d97d652e4c8137a2bb87f10d366a947e0dc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Mon, 9 Feb 2026 21:52:45 +0900 Subject: [PATCH] =?UTF-8?q?feat:=EC=9D=8C=EC=84=B1=EC=9E=85=EB=A0=A5=20?= =?UTF-8?q?=EC=8B=A4=EC=8B=9C=EA=B0=84=20=EB=AF=B8=EB=A6=AC=EB=B3=B4?= =?UTF-8?q?=EA=B8=B0=20+=20=EC=9E=AC=ED=81=B4=EB=A6=AD=20=EC=A4=91?= =?UTF-8?q?=EC=A7=80=20=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit continuous 모드로 변경, interimResults로 실시간 텍스트 표시 녹음 중 버튼 재클릭 시 중지, 아이콘 정지 모양으로 변경 Co-Authored-By: Claude Opus 4.6 --- .../views/juil/construction-photos.blade.php | 107 ++++++++++++------ 1 file changed, 73 insertions(+), 34 deletions(-) diff --git a/resources/views/juil/construction-photos.blade.php b/resources/views/juil/construction-photos.blade.php index d53125b7..52ee209d 100644 --- a/resources/views/juil/construction-photos.blade.php +++ b/resources/views/juil/construction-photos.blade.php @@ -54,63 +54,102 @@ } // --- VoiceInputButton (Web Speech API STT) --- -function VoiceInputButton({ onResult, disabled, mode = 'replace' }) { +function VoiceInputButton({ onResult, disabled }) { const [recording, setRecording] = useState(false); + const [interim, setInterim] = useState(''); const recognitionRef = useRef(null); const isSupported = typeof window !== 'undefined' && (window.SpeechRecognition || window.webkitSpeechRecognition); - const toggle = (e) => { - e.preventDefault(); - e.stopPropagation(); - if (disabled || !isSupported) return; - - if (recording) { - recognitionRef.current?.stop(); - setRecording(false); - return; - } + const stopRecording = useCallback(() => { + recognitionRef.current?.stop(); + recognitionRef.current = null; + setRecording(false); + setInterim(''); + }, []); + const startRecording = useCallback(() => { const SR = window.SpeechRecognition || window.webkitSpeechRecognition; const recognition = new SR(); recognition.lang = 'ko-KR'; - recognition.continuous = false; - recognition.interimResults = false; + recognition.continuous = true; + recognition.interimResults = true; recognition.maxAlternatives = 1; + let finalText = ''; + recognition.onresult = (event) => { - const text = event.results[0][0].transcript; - if (text) onResult(text, mode); - setRecording(false); + let interimText = ''; + for (let i = event.resultIndex; i < event.results.length; i++) { + const transcript = event.results[i][0].transcript; + if (event.results[i].isFinal) { + finalText += transcript; + if (finalText) onResult(finalText); + finalText = ''; + interimText = ''; + } else { + interimText += transcript; + } + } + setInterim(interimText); + }; + recognition.onerror = () => stopRecording(); + recognition.onend = () => { + setRecording(false); + setInterim(''); + recognitionRef.current = null; }; - recognition.onerror = () => setRecording(false); - recognition.onend = () => setRecording(false); recognitionRef.current = recognition; recognition.start(); setRecording(true); + setInterim(''); + }, [onResult, stopRecording]); + + const toggle = (e) => { + e.preventDefault(); + e.stopPropagation(); + if (disabled || !isSupported) return; + recording ? stopRecording() : startRecording(); }; + useEffect(() => { + return () => { recognitionRef.current?.stop(); }; + }, []); + if (!isSupported) return null; return ( - +
+ + {recording && interim && ( +
+ ...{interim} +
+ )} +
); }