// apps/mobile/app/(tabs)/history.tsx // 히스토리 탭 — 전사 기록 목록 // Phase M-2에서 디자인(docs/v3/designs/history.html) 정밀 적용 import { useEffect, useState } from 'react' import { View, FlatList, StyleSheet, ActivityIndicator, RefreshControl, Pressable } from 'react-native' import { useSafeAreaInsets } from 'react-native-safe-area-context' import { MetalCard, PhosphorText, Led, d3roNativePalette } from '@d3ro/ui-native' import { supabase, isSupabaseConfigured } from '../../lib/supabase' interface HistoryEntry { id: string original_text: string | null polished_text: string | null mode: string status: string created_at: string stt_model: string | null word_count: number | null } type FilterType = 'all' | 'favorites' | 'processing' export default function HistoryScreen(): React.ReactElement { const insets = useSafeAreaInsets() const [entries, setEntries] = useState([]) const [loading, setLoading] = useState(true) const [refreshing, setRefreshing] = useState(false) const [filter, setFilter] = useState('all') async function load(): Promise { if (!isSupabaseConfigured()) { setLoading(false) return } try { const query = supabase .from('history') .select('id, original_text, polished_text, mode, status, created_at, stt_model, word_count') .order('created_at', { ascending: false }) .limit(50) const { data, error } = await query if (!error && data) { setEntries(data as HistoryEntry[]) } } finally { setLoading(false) setRefreshing(false) } } useEffect(() => { void load() }, []) if (loading) { return ( ) } return ( {/* Header */} HISTORY {/* Filter Chips */} {(['all', 'favorites', 'processing'] as const).map((f) => ( setFilter(f)} > {f === 'all' ? 'ALL' : f === 'favorites' ? 'SAVED' : 'PROC'} ))} item.id} contentContainerStyle={entries.length === 0 ? styles.center : styles.listContent} refreshControl={ { setRefreshing(true); void load() }} tintColor={d3roNativePalette.accent.amber} /> } ListEmptyComponent={ No history yet. Start recording! } renderItem={({ item }) => ( {new Date(item.created_at).toLocaleTimeString('ko-KR', { hour: '2-digit', minute: '2-digit' })} {item.word_count != null && ( {item.word_count} W )} {item.polished_text ?? item.original_text ?? '(empty)'} {item.stt_model?.toUpperCase() ?? 'LOCAL'} )} /> ) } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: d3roNativePalette.bg.app }, center: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 32, backgroundColor: d3roNativePalette.bg.app }, header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingHorizontal: 20, paddingBottom: 12 }, headerLeft: { flexDirection: 'row', alignItems: 'center', gap: 8 }, filters: { flexDirection: 'row', paddingHorizontal: 20, paddingBottom: 12, gap: 12, borderBottomWidth: 1, borderBottomColor: 'rgba(46, 46, 50, 0.5)' }, chip: { backgroundColor: d3roNativePalette.bg.card, borderWidth: 1, borderColor: d3roNativePalette.border.default, borderRadius: 999, paddingHorizontal: 16, paddingVertical: 6 }, chipActive: { backgroundColor: d3roNativePalette.accent.amber, borderColor: d3roNativePalette.accent.amber }, listContent: { padding: 20, paddingBottom: 120 }, card: { marginBottom: 12 }, cardHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }, cardHeaderLeft: { flexDirection: 'row', alignItems: 'center' }, wordBadge: { backgroundColor: d3roNativePalette.accent.amberDim, paddingHorizontal: 8, paddingVertical: 2, borderRadius: 4 }, transcriptText: { marginBottom: 8 }, cardMeta: { flexDirection: 'row', gap: 12 } })