guardia-messenger/app/(tabs)/voice.tsx
2026-06-03 09:17:03 +09:00

184 lines
8.2 KiB
TypeScript

import { useState, useRef } from 'react'
import {
View, Text, Pressable, StyleSheet, ScrollView,
ActivityIndicator, Alert,
} from 'react-native'
import { COLORS, API_BASE } from '../../constants/Config'
import { getToken } from '../../utils/auth'
import { VoiceInput } from '../../components/VoiceInput'
// 음성 명령 → ITSM 매핑
const VOICE_SHORTCUTS = [
{ label: '서버 상태', command: '/server status', icon: '🖥️' },
{ label: 'SR 만들기', command: '/sr create', icon: '📋' },
{ label: '배포 시작', command: '/deploy', icon: '🚀' },
{ label: '장애 보고', command: '/incident create',icon: '🚨' },
{ label: '대시보드', command: '/dashboard', icon: '📊' },
{ label: '경보 목록', command: '/alert list', icon: '🔔' },
]
export default function VoiceTab() {
const [isListening, setListening] = useState(false)
const [transcript, setTranscript] = useState('')
const [result, setResult] = useState<{ cmd: string; response: string } | null>(null)
const [loading, setLoading] = useState(false)
const [history, setHistory] = useState<{ text: string; cmd: string; time: string }[]>([])
async function processVoice(text: string) {
if (!text.trim()) return
setTranscript(text)
setLoading(true)
try {
const token = await getToken()
const res = await fetch(`${API_BASE}/api/ux/voice-process`, {
method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ text, context: 'voice-tab' }),
})
const data = await res.json()
if (data.mapped_command) {
setResult({ cmd: data.mapped_command, response: `실행: ${data.mapped_command}` })
setHistory(prev => [{
text, cmd: data.mapped_command,
time: new Date().toLocaleTimeString('ko-KR', { hour: '2-digit', minute: '2-digit' }),
}, ...prev.slice(0, 9)])
} else {
setResult({ cmd: '', response: '명령을 인식하지 못했습니다. 다시 시도하거나 아래 빠른 명령을 사용하세요.' })
}
} catch (e) {
setResult({ cmd: '', response: '서버 연결 오류' })
} finally {
setLoading(false)
}
}
async function runShortcut(command: string, label: string) {
setTranscript(label)
setLoading(true)
setResult({ cmd: command, response: `실행 중: ${command}` })
setHistory(prev => [{
text: label, cmd: command,
time: new Date().toLocaleTimeString('ko-KR', { hour: '2-digit', minute: '2-digit' }),
}, ...prev.slice(0, 9)])
setLoading(false)
}
return (
<ScrollView style={S.root} contentContainerStyle={{ paddingBottom: 40 }}>
{/* 헤더 */}
<View style={S.header}>
<Text style={S.title}>🎤 </Text>
<Text style={S.subtitle}> </Text>
</View>
{/* 메인 마이크 버튼 */}
<View style={S.micSection}>
<View style={S.micWrap}>
<VoiceInput onTranscript={processVoice} size="normal" />
</View>
<Text style={S.micHint}>
{isListening ? '듣고 있어요... 명령을 말씀하세요' : '마이크를 탭하여 시작'}
</Text>
</View>
{/* 인식 결과 */}
{loading && (
<View style={S.resultBox}>
<ActivityIndicator color={COLORS.gnbBg} />
<Text style={{ marginTop: 8, color: '#64748b' }}> ...</Text>
</View>
)}
{!loading && result && (
<View style={[S.resultBox, result.cmd ? S.resultSuccess : S.resultFail]}>
{transcript ? <Text style={S.transcriptText}>💬 "{transcript}"</Text> : null}
{result.cmd ? (
<View style={S.cmdBadge}>
<Text style={S.cmdText}>{result.cmd}</Text>
</View>
) : null}
<Text style={S.responseText}>{result.response}</Text>
</View>
)}
{/* 빠른 명령 버튼 */}
<Text style={S.sectionTitle}> </Text>
<View style={S.shortcuts}>
{VOICE_SHORTCUTS.map((s, i) => (
<Pressable key={i} onPress={() => runShortcut(s.command, s.label)} style={S.shortBtn}>
<Text style={S.shortIcon}>{s.icon}</Text>
<Text style={S.shortLabel}>{s.label}</Text>
</Pressable>
))}
</View>
{/* 사용 이력 */}
{history.length > 0 && (
<>
<Text style={S.sectionTitle}>📋 </Text>
<View style={S.historyBox}>
{history.map((h, i) => (
<View key={i} style={S.historyRow}>
<Text style={S.historyTime}>{h.time}</Text>
<Text style={S.historyText} numberOfLines={1}>{h.text}</Text>
<Text style={S.historyCmd}>{h.cmd}</Text>
</View>
))}
</View>
</>
)}
{/* 도움말 */}
<View style={S.helpBox}>
<Text style={S.helpTitle}>💡 </Text>
{['"서버 상태 확인해줘"', '"SR 만들어줘"', '"배포 시작해"', '"장애 보고해줘"'].map((e, i) => (
<Text key={i} style={S.helpItem}> {e}</Text>
))}
</View>
</ScrollView>
)
}
const S = StyleSheet.create({
root: { flex: 1, backgroundColor: '#f8fafc' },
header: { padding: 20, paddingBottom: 12, backgroundColor: COLORS.gnbBg },
title: { fontSize: 20, fontWeight: '800', color: '#fff' },
subtitle: { fontSize: 12, color: 'rgba(255,255,255,0.7)', marginTop: 4 },
micSection: { alignItems: 'center', paddingVertical: 32, backgroundColor: '#fff',
borderBottomWidth: 1, borderBottomColor: COLORS.border },
micWrap: { width: 80, height: 80, borderRadius: 40, backgroundColor: '#eff6ff',
alignItems: 'center', justifyContent: 'center',
shadowColor: COLORS.gnbBg, shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.2, shadowRadius: 12, elevation: 6 },
micHint: { marginTop: 12, fontSize: 13, color: '#64748b' },
resultBox: { margin: 12, borderRadius: 12, padding: 16, alignItems: 'center' },
resultSuccess:{ backgroundColor: '#eff6ff', borderWidth: 1, borderColor: '#bfdbfe' },
resultFail: { backgroundColor: '#fff5f5', borderWidth: 1, borderColor: '#fca5a5' },
transcriptText:{ fontSize: 13, color: '#64748b', marginBottom: 8, fontStyle: 'italic' },
cmdBadge: { backgroundColor: COLORS.gnbBg, borderRadius: 8,
paddingHorizontal: 14, paddingVertical: 6, marginBottom: 8 },
cmdText: { color: '#fff', fontWeight: '700', fontSize: 14, fontFamily: 'monospace' },
responseText: { fontSize: 13, color: '#374151', textAlign: 'center' },
sectionTitle: { fontSize: 14, fontWeight: '700', color: '#374151',
paddingHorizontal: 16, paddingVertical: 12 },
shortcuts: { flexDirection: 'row', flexWrap: 'wrap', paddingHorizontal: 12, gap: 8 },
shortBtn: { width: '30%', backgroundColor: '#fff', borderRadius: 12, padding: 12,
alignItems: 'center', borderWidth: 1, borderColor: COLORS.border,
shadowColor: '#000', shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.05, shadowRadius: 3, elevation: 1 },
shortIcon: { fontSize: 24, marginBottom: 4 },
shortLabel: { fontSize: 12, fontWeight: '600', color: COLORS.gnbBg, textAlign: 'center' },
historyBox: { marginHorizontal: 12, backgroundColor: '#fff', borderRadius: 12,
borderWidth: 1, borderColor: COLORS.border, overflow: 'hidden' },
historyRow: { flexDirection: 'row', alignItems: 'center', padding: 10,
borderBottomWidth: 1, borderBottomColor: '#f1f5f9' },
historyTime: { fontSize: 11, color: '#94a3b8', width: 40 },
historyText: { flex: 1, fontSize: 12, color: '#374151', marginHorizontal: 8 },
historyCmd: { fontSize: 11, color: COLORS.gnbBg, fontFamily: 'monospace' },
helpBox: { margin: 12, backgroundColor: '#fff', borderRadius: 12, padding: 16,
borderWidth: 1, borderColor: COLORS.border },
helpTitle: { fontSize: 13, fontWeight: '700', color: '#374151', marginBottom: 8 },
helpItem: { fontSize: 12, color: '#64748b', marginBottom: 4 },
})