116 lines
4.6 KiB
TypeScript
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 },
|
|
})
|