/** * 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([]) 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 ( 🎙️ 회의 녹음 음성으로 회의 내용을 받아쓰고 AI 회의록을 생성합니다. setRecording(r => !r)} > {recording ? '⏸ 녹음 표시 중지' : '▶ 녹음 표시 시작'} 받아쓴 문장: {transcript.length}개 {transcript.length > 0 ? ( 녹취 내용 {transcript.map((t, i) => ( • {t} ))} ) : null} {loading ? : 🤖 AI 회의록 생성} {minutes ? ( AI 회의록 {minutes} router.push('/meeting_sr')}> 액션아이템 → SR 등록 → ) : null} ) } 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 }, })