116 lines
5.0 KiB
TypeScript
116 lines
5.0 KiB
TypeScript
/**
|
||
* 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 },
|
||
})
|