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

116 lines
4.6 KiB
TypeScript

/**
* meeting_sr.tsx (#18) — 회의 액션아이템 → SR 1-tap 등록
*
* meeting.tsx가 SecureStore에 저장한 회의록에서 Ollama로 액션아이템 배열 추출 →
* 각 항목을 1-tap으로 SR 등록(createSR).
*/
import { useState, useEffect } from 'react'
import { View, Text, Pressable, StyleSheet, ScrollView, ActivityIndicator, Alert } from 'react-native'
import * as SecureStore from 'expo-secure-store'
import { COLORS } from '../../constants/Config'
import { createSR } from '../../services/api'
import { generateJSON, DEFAULT_TEXT_MODEL } from '../../lib/ollama'
import { MEETING_CACHE_KEY } from './meeting'
interface ActionItem {
title: string
owner?: string
priority?: string
}
export default function MeetingSRScreen() {
const [items, setItems] = useState<ActionItem[]>([])
const [loading, setLoading] = useState(true)
const [registered, setRegistered] = useState<Record<number, boolean>>({})
useEffect(() => {
;(async () => {
setLoading(true)
let minutes = ''
try {
const cached = await SecureStore.getItemAsync(MEETING_CACHE_KEY)
if (cached) minutes = JSON.parse(cached).minutes ?? JSON.parse(cached).transcript ?? ''
} catch {
/* 무시 */
}
if (!minutes.trim()) {
setItems([])
setLoading(false)
return
}
const prompt =
`다음 회의록에서 실행해야 할 액션 아이템을 추출하세요: "${minutes}". ` +
`JSON 배열로만 출력: [{"title":"할 일","owner":"담당","priority":"HIGH|MEDIUM|LOW"}]`
const result = await generateJSON<ActionItem[]>(DEFAULT_TEXT_MODEL, prompt, [])
setItems(Array.isArray(result) ? result.filter(x => x?.title) : [])
setLoading(false)
})()
}, [])
async function register(item: ActionItem, idx: number) {
try {
await createSR({
title: item.title,
description: `회의 액션아이템${item.owner ? ` (담당: ${item.owner})` : ''}`,
priority: (item.priority ?? 'MEDIUM').toUpperCase(),
sr_type: 'OTHER',
})
setRegistered(r => ({ ...r, [idx]: true }))
Alert.alert('등록 완료', `SR이 접수되었습니다.\n${item.title}`)
} catch {
Alert.alert('등록 실패', '서버에 연결할 수 없습니다.')
}
}
return (
<ScrollView style={S.root} contentContainerStyle={{ paddingBottom: 40 }}>
<View style={S.header}>
<Text style={S.title}> SR</Text>
<Text style={S.sub}> 1-tap으로 SR .</Text>
</View>
{loading ? (
<View style={S.center}>
<ActivityIndicator color={COLORS.accent} />
<Text style={S.hint}>AI가 ...</Text>
</View>
) : items.length === 0 ? (
<View style={S.center}>
<Text style={S.hint}> .{'\n'} .</Text>
</View>
) : (
items.map((item, i) => (
<View key={i} style={S.card}>
<Text style={S.itemTitle}>{item.title}</Text>
<Text style={S.itemMeta}>
{item.owner ? `담당: ${item.owner} · ` : ''}: {(item.priority ?? 'MEDIUM').toUpperCase()}
</Text>
<Pressable
style={[S.btn, registered[i] && S.btnDone]}
onPress={() => register(item, i)}
disabled={registered[i]}
>
<Text style={S.btnText}>{registered[i] ? '✓ 등록됨' : 'SR 등록'}</Text>
</Pressable>
</View>
))
)}
</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 },
center: { alignItems: 'center', padding: 40, gap: 10 },
hint: { fontSize: 13, color: COLORS.muted, textAlign: 'center', lineHeight: 19 },
card: { backgroundColor: COLORS.card, margin: 12, marginBottom: 0, borderRadius: 14, padding: 14, borderWidth: 1, borderColor: COLORS.border },
itemTitle: { fontSize: 14, fontWeight: '700', color: COLORS.text },
itemMeta: { fontSize: 12, color: COLORS.muted, marginTop: 4 },
btn: { marginTop: 10, backgroundColor: COLORS.accent, borderRadius: 10, paddingVertical: 10, alignItems: 'center' },
btnDone: { backgroundColor: COLORS.success },
btnText: { color: '#fff', fontWeight: '700', fontSize: 13 },
})