guardia-messenger/app/(tabs)/meeting.tsx

116 lines
5.0 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* meeting.tsx (#17) — 회의 녹음 → STT → 회의록
*
* meeting-recorder-dev 협업 화면. 녹음 제어 UI + 음성 입력(온디바이스 STT)으로
* 회의 내용을 텍스트로 누적하고, Ollama로 회의록/액션아이템을 요약한다.
* 결과는 meeting_sr.tsx(액션아이템→SR)로 전달하기 위해 SecureStore에 저장.
*
* EAS 안전: android/ios 네이티브 녹음 라이브러리 없이 음성 인식 누적 방식.
*/
import { useState } from 'react'
import { View, Text, Pressable, StyleSheet, ScrollView, ActivityIndicator } from 'react-native'
import { router } from 'expo-router'
import * as SecureStore from 'expo-secure-store'
import { COLORS } from '../../constants/Config'
import { VoiceInput } from '../../components/VoiceInput'
import { generate, DEFAULT_TEXT_MODEL } from '../../lib/ollama'
export const MEETING_CACHE_KEY = 'grd_meeting_minutes'
export default function MeetingScreen() {
const [recording, setRecording] = useState(false)
const [transcript, setTranscript] = useState<string[]>([])
const [minutes, setMinutes] = useState('')
const [loading, setLoading] = useState(false)
function onTranscript(text: string) {
if (text.trim()) setTranscript(prev => [...prev, text.trim()])
}
async function summarize() {
const full = transcript.join(' ')
if (!full.trim()) return
setLoading(true)
const prompt =
`다음은 IT 운영 회의 녹취록입니다: "${full}". ` +
`한국어로 (1) 회의 요약 3줄, (2) 결정 사항, (3) 액션 아이템(담당/할일 형식)으로 정리하세요.`
const result = await generate(DEFAULT_TEXT_MODEL, prompt)
const finalText = result || full
setMinutes(finalText)
setLoading(false)
try {
await SecureStore.setItemAsync(
MEETING_CACHE_KEY,
JSON.stringify({ at: Date.now(), transcript: full, minutes: finalText }),
)
} catch {
/* 무시 */
}
}
return (
<ScrollView style={S.root} contentContainerStyle={{ paddingBottom: 40 }}>
<View style={S.header}>
<Text style={S.title}>🎙 </Text>
<Text style={S.sub}> AI .</Text>
</View>
<View style={S.recordCard}>
<VoiceInput onTranscript={onTranscript} size="normal" />
<Pressable
style={[S.recBtn, recording && S.recBtnOn]}
onPress={() => setRecording(r => !r)}
>
<Text style={S.recBtnText}>{recording ? '⏸ 녹음 표시 중지' : '▶ 녹음 표시 시작'}</Text>
</Pressable>
<Text style={S.count}> : {transcript.length}</Text>
</View>
{transcript.length > 0 ? (
<View style={S.card}>
<Text style={S.cardTitle}> </Text>
{transcript.map((t, i) => (
<Text key={i} style={S.line}>
{t}
</Text>
))}
</View>
) : null}
<Pressable style={S.sumBtn} onPress={summarize} disabled={loading || transcript.length === 0}>
{loading ? <ActivityIndicator color="#fff" /> : <Text style={S.sumBtnText}>🤖 AI </Text>}
</Pressable>
{minutes ? (
<View style={S.card}>
<Text style={S.cardTitle}>AI </Text>
<Text style={S.minutes}>{minutes}</Text>
<Pressable style={S.nextBtn} onPress={() => router.push('/meeting_sr')}>
<Text style={S.nextBtnText}> SR </Text>
</Pressable>
</View>
) : null}
</ScrollView>
)
}
const S = StyleSheet.create({
root: { flex: 1, backgroundColor: COLORS.bg },
header: { padding: 20, paddingBottom: 12, backgroundColor: COLORS.gnbBg },
title: { fontSize: 20, fontWeight: '800', color: '#fff' },
sub: { fontSize: 12, color: 'rgba(255,255,255,0.7)', marginTop: 4 },
recordCard: { backgroundColor: COLORS.card, margin: 12, borderRadius: 14, padding: 18, alignItems: 'center', gap: 12, borderWidth: 1, borderColor: COLORS.border },
recBtn: { backgroundColor: COLORS.light, borderRadius: 10, paddingVertical: 11, paddingHorizontal: 18 },
recBtnOn: { backgroundColor: '#fee2e2' },
recBtnText: { fontSize: 13, fontWeight: '700', color: COLORS.blue },
count: { fontSize: 12, color: COLORS.muted },
card: { backgroundColor: COLORS.card, marginHorizontal: 12, marginBottom: 12, borderRadius: 14, padding: 14, borderWidth: 1, borderColor: COLORS.border },
cardTitle: { fontSize: 14, fontWeight: '700', color: COLORS.text, marginBottom: 8 },
line: { fontSize: 13, color: COLORS.text, lineHeight: 20 },
sumBtn: { backgroundColor: COLORS.accent, marginHorizontal: 12, borderRadius: 12, paddingVertical: 14, alignItems: 'center', marginBottom: 12 },
sumBtnText: { color: '#fff', fontWeight: '700', fontSize: 14 },
minutes: { fontSize: 13, color: COLORS.text, lineHeight: 20 },
nextBtn: { marginTop: 12, backgroundColor: COLORS.gnbBg, borderRadius: 10, paddingVertical: 12, alignItems: 'center' },
nextBtnText: { color: '#fff', fontWeight: '700', fontSize: 13 },
})