From 8b74f455df460d1587dc4e660b0048f85a37721b Mon Sep 17 00:00:00 2001 From: Nan Kyung Lee Date: Tue, 24 Mar 2026 15:54:43 +0900 Subject: [PATCH] =?UTF-8?q?feat(korea):=20=EC=A4=91=EA=B5=AD=EC=96=B4=20?= =?UTF-8?q?=EA=B2=BD=EA=B3=A0=EB=AC=B8=20TTS=20=EC=9D=8C=EC=84=B1=20?= =?UTF-8?q?=EC=9E=AC=EC=83=9D=20(Web=20Speech=20API)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 경고문 옆 🔊 버튼 클릭 → 중국어(zh-CN) 음성 재생 - SpeechSynthesis API 사용 (브라우저 내장, API 키 불필요) - 재생 중 버튼 애니메이션 (pulse) + 배경 하이라이트 - 재생 속도 0.85x (확성기 방송용 느린 발화) - 클릭: 클립보드 복사 / 🔊: 음성 재생 분리 Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/components/korea/OpsGuideModal.tsx | 54 ++++++++++++++++--- 1 file changed, 46 insertions(+), 8 deletions(-) diff --git a/frontend/src/components/korea/OpsGuideModal.tsx b/frontend/src/components/korea/OpsGuideModal.tsx index d928e94..559a763 100644 --- a/frontend/src/components/korea/OpsGuideModal.tsx +++ b/frontend/src/components/korea/OpsGuideModal.tsx @@ -137,10 +137,29 @@ export function OpsGuideModal({ ships, onClose, onFlyTo, onRouteSelect }: Props) const criticalCount = suspects.filter(s => s.riskLevel === 'CRITICAL').length; const highCount = suspects.filter(s => s.riskLevel === 'HIGH').length; + const [speakingIdx, setSpeakingIdx] = useState(null); + const copyToClipboard = (text: string, idx: number) => { navigator.clipboard.writeText(text).then(() => { setCopiedIdx(idx); setTimeout(() => setCopiedIdx(null), 1500); }); }; + const speakChinese = useCallback((text: string, idx: number) => { + if (typeof window === 'undefined' || !window.speechSynthesis) return; + window.speechSynthesis.cancel(); + const utter = new SpeechSynthesisUtterance(text); + utter.lang = 'zh-CN'; + utter.rate = 0.85; + utter.volume = 1; + // Try to find a Chinese voice + const voices = window.speechSynthesis.getVoices(); + const zhVoice = voices.find(v => v.lang.startsWith('zh')) || voices.find(v => v.lang.includes('CN')); + if (zhVoice) utter.voice = zhVoice; + utter.onstart = () => setSpeakingIdx(idx); + utter.onend = () => setSpeakingIdx(null); + utter.onerror = () => setSpeakingIdx(null); + window.speechSynthesis.speak(utter); + }, []); + const handleSuspectClick = (s: SuspectVessel) => { setSelectedSuspect(s); setTab('procedure'); @@ -244,16 +263,35 @@ export function OpsGuideModal({ ships, onClose, onFlyTo, onRouteSelect }: Props) {/* 중국어 경고문 */}
-
📢 중국어 경고문 (클릭하여 복사)
+
📢 중국어 경고문 (클릭: 복사 | 🔊: 음성)
{(CN_WARNINGS[selectedSuspect.estimatedType] || CN_WARNINGS.UNKNOWN).map((w, i) => ( -
copyToClipboard(w.zh, i)} style={{ - background: copiedIdx === i ? 'rgba(34,197,94,0.15)' : '#111827', - border: copiedIdx === i ? '1px solid rgba(34,197,94,0.4)' : '1px solid #1e293b', - borderRadius: 4, padding: '6px 10px', marginBottom: 4, cursor: 'pointer', transition: 'all 0.2s', +
-
{w.zh}
-
{w.ko}
-
사용: {w.usage} {copiedIdx === i && ✓ 복사됨}
+
copyToClipboard(w.zh, i)}> +
{w.zh}
+
{w.ko}
+
+ 사용: {w.usage} + {copiedIdx === i && ✓ 복사됨} +
+
+
))}