import React, { useState, useCallback } from 'react'; import { View, Text, StyleSheet, TouchableOpacity, FlatList, TextInput, ActivityIndicator, RefreshControl, Modal, ScrollView, } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; import { apiClient } from '../../services/api'; interface G2BProject { bid_no: string; title: string; org: string; budget_krw: number | null; deadline: string | null; announce_date: string | null; guardia_score: number; guardia_modules: string[]; project_type: string; guardia_proposal: string; } interface CategorySummary { category: string; count: number; avg_budget_billion: number; avg_guardia_score: number; } const SCORE_COLOR = (score: number) => { if (score >= 90) return '#00D4AA'; if (score >= 75) return '#00A0C8'; if (score >= 60) return '#F59E0B'; return '#6B7280'; }; const TYPE_COLORS: Record = { ITSM: '#00D4AA', SM: '#00C896', 보안: '#EF4444', 클라우드: '#3B82F6', AI: '#8B5CF6', ERP: '#F59E0B', MES: '#10B981', SI: '#6B7280', }; function formatBudget(krw: number | null): string { if (!krw) return '미정'; if (krw >= 1_000_000_000) return `${(krw / 1_000_000_000).toFixed(1)}억`; if (krw >= 1_000_000) return `${(krw / 1_000_000).toFixed(0)}백만`; return `${krw.toLocaleString()}원`; } function ScoreBadge({ score }: { score: number }) { return ( GUARDiA {score}점 ); } function ProjectCard({ item, onPress, }: { item: G2BProject; onPress: () => void; }) { const typeColor = TYPE_COLORS[item.project_type] ?? '#6B7280'; return ( {item.project_type} {item.title} {item.org} {formatBudget(item.budget_krw)} {item.deadline && ( {item.deadline} )} {item.guardia_score >= 80 && ( GUARDiA 고적합 사업 )} ); } function DetailModal({ item, visible, onClose, }: { item: G2BProject | null; visible: boolean; onClose: () => void; }) { if (!item) return null; const typeColor = TYPE_COLORS[item.project_type] ?? '#6B7280'; return ( {item.title} 발주 기관 {item.org} 예산 규모 {formatBudget(item.budget_krw)} 마감일 {item.deadline ?? '-'} 프로젝트 유형 {item.project_type} GUARDiA 적합성 분석 적합성 점수 {item.guardia_score} 적용 모듈 {item.guardia_modules.map((m) => ( {m} ))} GUARDiA 제안 {item.guardia_proposal} ); } export default function NarasajangSW() { const [projects, setProjects] = useState([]); const [summary, setSummary] = useState([]); const [loading, setLoading] = useState(false); const [refreshing, setRefreshing] = useState(false); const [keyword, setKeyword] = useState(''); const [selected, setSelected] = useState(null); const [tab, setTab] = useState<'list' | 'summary'>('list'); const fetchProjects = useCallback(async (kw = '') => { setLoading(true); try { const params = kw ? { keyword: kw } : {}; const [pRes, sRes] = await Promise.all([ apiClient.get('/api/g2b-opportunity/projects', { params }), apiClient.get('/api/g2b-opportunity/summary/by-category'), ]); setProjects(pRes.data.projects ?? pRes.data); setSummary(sRes.data.summary ?? sRes.data); } catch { // 오류 시 빈 목록 유지 } finally { setLoading(false); setRefreshing(false); } }, []); React.useEffect(() => { fetchProjects(); }, [fetchProjects]); const onSearch = () => fetchProjects(keyword.trim()); const onRefresh = () => { setRefreshing(true); fetchProjects(keyword.trim()); }; const highScore = projects.filter((p) => p.guardia_score >= 80).length; return ( {/* 헤더 */} 나라장터 SW 공고 {highScore > 0 && ( 고적합 {highScore} )} {/* 검색바 */} 검색 {/* 탭 */} {(['list', 'summary'] as const).map((t) => ( setTab(t)} > {t === 'list' ? `공고 목록 (${projects.length})` : '카테고리 요약'} ))} {loading && !refreshing ? ( 나라장터 공고 분석 중... ) : tab === 'list' ? ( item.bid_no} renderItem={({ item }) => ( setSelected(item)} /> )} refreshControl={} contentContainerStyle={styles.list} ListEmptyComponent={ 공고가 없습니다 } /> ) : ( {summary.map((cat) => ( {cat.category} {cat.count}건 평균 예산 {cat.avg_budget_billion.toFixed(1)}억 평균 GUARDiA 점수 {cat.avg_guardia_score.toFixed(0)}점 ))} )} setSelected(null)} /> ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#0F172A' }, header: { flexDirection: 'row', alignItems: 'center', gap: 8, padding: 16, paddingTop: 20 }, headerTitle: { fontSize: 18, fontWeight: '700', color: '#F1F5F9', flex: 1 }, headerBadge: { backgroundColor: '#F59E0B22', borderRadius: 12, paddingHorizontal: 8, paddingVertical: 3, borderWidth: 1, borderColor: '#F59E0B' }, headerBadgeText: { color: '#F59E0B', fontSize: 11, fontWeight: '700' }, searchRow: { flexDirection: 'row', gap: 8, paddingHorizontal: 16, paddingBottom: 10 }, searchBox: { flex: 1, flexDirection: 'row', alignItems: 'center', gap: 8, backgroundColor: '#1E293B', borderRadius: 10, paddingHorizontal: 12, height: 40 }, searchInput: { flex: 1, color: '#F1F5F9', fontSize: 14 }, searchBtn: { backgroundColor: '#00A0C8', borderRadius: 10, paddingHorizontal: 14, height: 40, justifyContent: 'center' }, searchBtnText: { color: '#fff', fontWeight: '700', fontSize: 13 }, tabRow: { flexDirection: 'row', borderBottomWidth: 1, borderBottomColor: '#1E293B', marginHorizontal: 16 }, tab: { flex: 1, paddingVertical: 10, alignItems: 'center' }, tabActive: { borderBottomWidth: 2, borderBottomColor: '#00A0C8' }, tabText: { color: '#6B7280', fontSize: 13 }, tabTextActive: { color: '#00A0C8', fontWeight: '700' }, list: { padding: 16, paddingBottom: 80 }, center: { alignItems: 'center', justifyContent: 'center', paddingVertical: 60 }, loadingText: { color: '#9CA3AF', marginTop: 8 }, emptyText: { color: '#6B7280', marginTop: 8 }, card: { backgroundColor: '#1E293B', borderRadius: 14, padding: 14, marginBottom: 10 }, cardHeader: { flexDirection: 'row', gap: 8, marginBottom: 8 }, typeBadge: { borderRadius: 8, paddingHorizontal: 8, paddingVertical: 3, borderWidth: 1 }, typeText: { fontSize: 11, fontWeight: '700' }, scoreBadge: { borderRadius: 8, paddingHorizontal: 8, paddingVertical: 3, borderWidth: 1 }, scoreText: { fontSize: 11, fontWeight: '700' }, cardTitle: { color: '#F1F5F9', fontSize: 14, fontWeight: '600', lineHeight: 20, marginBottom: 4 }, cardOrg: { color: '#9CA3AF', fontSize: 12, marginBottom: 8 }, cardMeta: { flexDirection: 'row', gap: 12 }, metaItem: { flexDirection: 'row', alignItems: 'center', gap: 4 }, metaText: { color: '#9CA3AF', fontSize: 12 }, highlightBanner: { flexDirection: 'row', alignItems: 'center', gap: 4, marginTop: 8, backgroundColor: '#F59E0B11', borderRadius: 6, padding: 6 }, highlightText: { color: '#F59E0B', fontSize: 11, fontWeight: '600' }, divider: { height: 1, backgroundColor: '#334155', marginVertical: 14 }, sectionTitle: { color: '#9CA3AF', fontSize: 12, fontWeight: '600', marginBottom: 8, marginTop: 4, textTransform: 'uppercase', letterSpacing: 0.5 }, detailRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingVertical: 8, borderBottomWidth: 1, borderBottomColor: '#1E293B' }, detailLabel: { color: '#6B7280', fontSize: 13 }, detailValue: { color: '#F1F5F9', fontSize: 13, fontWeight: '600' }, scoreRow: { flexDirection: 'row', alignItems: 'center', gap: 10, marginBottom: 12 }, scoreLabel: { color: '#9CA3AF', fontSize: 12, width: 70 }, scoreBar: { flex: 1, height: 6, backgroundColor: '#1E293B', borderRadius: 3, overflow: 'hidden' }, scoreBarFill: { height: '100%', borderRadius: 3 }, scoreBig: { fontSize: 18, fontWeight: '800', width: 36, textAlign: 'right' }, moduleGrid: { flexDirection: 'row', flexWrap: 'wrap', gap: 6, marginBottom: 12 }, moduleChip: { backgroundColor: '#00A0C822', borderRadius: 6, paddingHorizontal: 8, paddingVertical: 4, borderWidth: 1, borderColor: '#00A0C8' }, moduleText: { color: '#00A0C8', fontSize: 11, fontWeight: '600' }, proposalText: { color: '#CBD5E1', fontSize: 13, lineHeight: 20 }, modalOverlay: { flex: 1, backgroundColor: '#00000080', justifyContent: 'flex-end' }, modalBox: { backgroundColor: '#1E293B', borderTopLeftRadius: 20, borderTopRightRadius: 20, padding: 20, maxHeight: '85%' }, modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 16 }, modalTitle: { flex: 1, color: '#F1F5F9', fontSize: 16, fontWeight: '700', lineHeight: 22, marginRight: 8 }, summaryCard: { backgroundColor: '#1E293B', borderRadius: 12, padding: 14, marginBottom: 10 }, summaryHeader: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 10 }, summaryCategory: { color: '#F1F5F9', fontWeight: '700', fontSize: 15 }, summaryCount: { color: '#00A0C8', fontWeight: '700', fontSize: 15 }, summaryRow: { flexDirection: 'row', gap: 12 }, summaryItem: { flex: 1, backgroundColor: '#0F172A', borderRadius: 8, padding: 10 }, summaryLabel: { color: '#9CA3AF', fontSize: 11, marginBottom: 4 }, summaryValue: { color: '#F1F5F9', fontWeight: '700', fontSize: 16 }, });