/** * #38 멀티기관 계정 전환 (feature-screen-dev와 협업) * GET /api/institutions/ — 접근 가능 기관 목록 * POST /api/auth/switch-tenant — { tenant_id } → { access_token, tenant_name } * 성공: SecureStore 'grd_token' 갱신 + 토스트 + router.replace('/(tabs)') */ import { useCallback, useEffect, useState } from 'react' import { View, Text, FlatList, TouchableOpacity, StyleSheet, RefreshControl, ActivityIndicator, ToastAndroid, Platform, Alert, } from 'react-native' import { useRouter } from 'expo-router' import * as SecureStore from 'expo-secure-store' import { COLORS } from '../../constants/Config' import { getInstitutions, switchTenant } from '../../services/api' import LineIcon from '../../components/LineIcon' interface Institution { id?: string | number inst_id?: string | number tenant_id?: string | number name?: string inst_name?: string region?: string is_current?: boolean current?: boolean } function toast(msg: string) { if (Platform.OS === 'android') ToastAndroid.show(msg, ToastAndroid.SHORT) else Alert.alert('', msg) } export default function MultiTenantScreen() { const router = useRouter() const [items, setItems] = useState([]) const [loading, setLoading] = useState(true) const [refresh, setRefresh] = useState(false) const [switching, setSwitching] = useState(null) const load = useCallback(async (isRefresh = false) => { isRefresh ? setRefresh(true) : setLoading(true) try { const r = await getInstitutions() const list: Institution[] = Array.isArray(r.data) ? r.data : r.data?.items ?? [] setItems(list) } catch { setItems([]) } finally { setLoading(false) setRefresh(false) } }, []) useEffect(() => { load() }, [load]) const idOf = (inst: Institution) => inst.tenant_id ?? inst.inst_id ?? inst.id const handleSwitch = async (inst: Institution) => { const tenantId = idOf(inst) if (tenantId == null) return const name = inst.name ?? inst.inst_name ?? '기관' setSwitching(tenantId) try { const r = await switchTenant(tenantId) const { access_token, tenant_name } = r.data ?? {} if (access_token) { await SecureStore.setItemAsync('grd_token', access_token) } toast(`${tenant_name ?? name}으로 전환됨`) router.replace('/(tabs)') } catch (e: any) { Alert.alert('전환 실패', e.response?.data?.detail ?? '기관 전환에 실패했습니다.') } finally { setSwitching(null) } } if (loading) { return ( ) } return ( String(idOf(it) ?? i)} refreshControl={ load(true)} tintColor={COLORS.accent} />} ListHeaderComponent={ 기관 전환 접근 가능한 기관 {items.length}곳 · 선택하면 해당 기관으로 전환됩니다 } ListEmptyComponent={ 접근 가능한 기관이 없습니다. } contentContainerStyle={items.length === 0 ? { flexGrow: 1 } : undefined} renderItem={({ item }) => { const isCurrent = item.is_current ?? item.current ?? false const tid = idOf(item) return ( handleSwitch(item)} > {item.name ?? item.inst_name ?? '기관'} {!!item.region && {item.region}} {isCurrent ? ( 현재 ) : switching === tid ? ( ) : ( )} ) }} /> ) } const s = StyleSheet.create({ center: { flex: 1, alignItems: 'center', justifyContent: 'center', backgroundColor: COLORS.bg }, header: { padding: 20, paddingBottom: 8 }, headerTitle: { fontSize: 18, fontWeight: '800', color: COLORS.text }, headerSub: { fontSize: 12, color: COLORS.muted, marginTop: 4 }, card: { flexDirection: 'row', alignItems: 'center', gap: 12, backgroundColor: '#fff', marginHorizontal: 16, marginTop: 10, borderRadius: 14, padding: 16, borderWidth: 1, borderColor: COLORS.border, }, cardCurrent: { borderColor: COLORS.accent, backgroundColor: '#E8F7FB' }, iconBox: { width: 42, height: 42, borderRadius: 11, backgroundColor: 'rgba(0,160,200,.08)', alignItems: 'center', justifyContent: 'center', }, name: { fontSize: 15, fontWeight: '700', color: COLORS.text }, meta: { fontSize: 12, color: COLORS.muted, marginTop: 2 }, currentBadge: { backgroundColor: COLORS.accent, paddingHorizontal: 10, paddingVertical: 3, borderRadius: 8 }, currentText: { fontSize: 11, fontWeight: '700', color: '#fff' }, chevron: { fontSize: 22, color: COLORS.muted }, empty: { flex: 1, alignItems: 'center', justifyContent: 'center' }, emptyText: { color: COLORS.muted, fontSize: 14 }, })