157 lines
5.7 KiB
TypeScript
157 lines
5.7 KiB
TypeScript
/**
|
||
* #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<Institution[]>([])
|
||
const [loading, setLoading] = useState(true)
|
||
const [refresh, setRefresh] = useState(false)
|
||
const [switching, setSwitching] = useState<string | number | null>(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 (
|
||
<View style={s.center}>
|
||
<ActivityIndicator color={COLORS.accent} size="large" />
|
||
</View>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<FlatList
|
||
style={{ flex: 1, backgroundColor: COLORS.bg }}
|
||
data={items}
|
||
keyExtractor={(it, i) => String(idOf(it) ?? i)}
|
||
refreshControl={<RefreshControl refreshing={refresh} onRefresh={() => load(true)} tintColor={COLORS.accent} />}
|
||
ListHeaderComponent={
|
||
<View style={s.header}>
|
||
<Text style={s.headerTitle}>기관 전환</Text>
|
||
<Text style={s.headerSub}>접근 가능한 기관 {items.length}곳 · 선택하면 해당 기관으로 전환됩니다</Text>
|
||
</View>
|
||
}
|
||
ListEmptyComponent={
|
||
<View style={s.empty}><Text style={s.emptyText}>접근 가능한 기관이 없습니다.</Text></View>
|
||
}
|
||
contentContainerStyle={items.length === 0 ? { flexGrow: 1 } : undefined}
|
||
renderItem={({ item }) => {
|
||
const isCurrent = item.is_current ?? item.current ?? false
|
||
const tid = idOf(item)
|
||
return (
|
||
<TouchableOpacity
|
||
style={[s.card, isCurrent && s.cardCurrent]}
|
||
disabled={isCurrent || switching != null}
|
||
onPress={() => handleSwitch(item)}
|
||
>
|
||
<View style={s.iconBox}>
|
||
<LineIcon name="building" size={20} color={COLORS.accent} />
|
||
</View>
|
||
<View style={{ flex: 1 }}>
|
||
<Text style={s.name}>{item.name ?? item.inst_name ?? '기관'}</Text>
|
||
{!!item.region && <Text style={s.meta}>{item.region}</Text>}
|
||
</View>
|
||
{isCurrent ? (
|
||
<View style={s.currentBadge}><Text style={s.currentText}>현재</Text></View>
|
||
) : switching === tid ? (
|
||
<ActivityIndicator size="small" color={COLORS.accent} />
|
||
) : (
|
||
<Text style={s.chevron}>›</Text>
|
||
)}
|
||
</TouchableOpacity>
|
||
)
|
||
}}
|
||
/>
|
||
)
|
||
}
|
||
|
||
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 },
|
||
})
|