itsm_ysm/mobile/app/(tabs)/me.tsx
itms-merge-dev efc3e66635 feat(itms): 모바일 앱 신규 (Expo, 12화면, OAuth password grant)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 17:10:46 +09:00

105 lines
4.5 KiB
TypeScript

import { useCallback, useState } from 'react'
import { View, Text, ScrollView, StyleSheet, TouchableOpacity } from 'react-native'
import { router, useFocusEffect } from 'expo-router'
import { Feather } from '@expo/vector-icons'
import { useTheme } from '../../constants/ThemeContext'
import { type PrefMode } from '../../constants/ThemeContext'
import { type ThemeTokens } from '../../constants/ItmsTheme'
import { Card, Row, SectionTitle, Btn } from '../../components/ui'
import { API_BASE, ITMS_ENV } from '../../constants/Config'
import { itmsProfile, clearItmsSession, type ItmsProfile } from '../itmsApi'
/*
* 내 정보 — 토큰에서 도출한 프로필(userName·roles·email·org) + 테마 모드 + 로그아웃.
* ITMS 백엔드에는 preferences API 가 없어 테마는 로컬 저장만.
*/
export default function Me() {
const { t, mode, setMode } = useTheme()
const s = makeStyles(t)
const [me, setMe] = useState<ItmsProfile | null>(null)
useFocusEffect(useCallback(() => {
let alive = true
itmsProfile().then((p) => { if (alive) setMe(p) }).catch(() => { /* noop */ })
return () => { alive = false }
}, []))
const logout = async () => {
await clearItmsSession()
router.replace('/login')
}
const modes: { key: PrefMode; label: string; icon: keyof typeof Feather.glyphMap }[] = [
{ key: 'light', label: '라이트', icon: 'sun' },
{ key: 'dark', label: '다크', icon: 'moon' },
{ key: 'system', label: '시스템', icon: 'smartphone' },
]
return (
<ScrollView style={s.wrap} contentContainerStyle={s.content}>
<View style={s.profileHead}>
<View style={s.avatar}><Text style={s.avatarText}>{(me?.userNm || 'I').slice(0, 1)}</Text></View>
<Text style={s.name}>{me?.userNm || '-'}</Text>
<Text style={s.sub}>{me?.userId ? `@${me.userId}` : ''}</Text>
{me?.roles && me.roles.length > 0 && (
<View style={s.roleRow}>
{me.roles.map((r) => (
<View key={r} style={s.roleBadge}><Text style={s.roleText}>{r.replace(/^ROLE_/, '')}</Text></View>
))}
</View>
)}
</View>
<SectionTitle style={s.mt}> </SectionTitle>
<Card>
<Row label="아이디" value={me?.userId || '-'} />
<Row label="이메일" value={me?.email || '-'} />
<Row label="연락처" value={me?.phone || '-'} />
<Row label="조직 ID" value={me?.orgId || '-'} />
</Card>
<SectionTitle style={s.mt}> </SectionTitle>
<View style={s.modeRow}>
{modes.map((m) => {
const on = mode === m.key
return (
<TouchableOpacity key={m.key} style={[s.modeCell, on && { borderColor: t.brand, backgroundColor: t.brand + '14' }]} onPress={() => setMode(m.key)}>
<Feather name={m.icon} size={18} color={on ? t.brand : t.muted} />
<Text style={[s.modeLbl, { color: on ? t.brand : t.muted }]}>{m.label}</Text>
</TouchableOpacity>
)
})}
</View>
<SectionTitle style={s.mt}> </SectionTitle>
<Card>
<Row label="서버" value={API_BASE} />
<Row label="환경" value={ITMS_ENV} />
<Row label="버전" value="1.0.0" />
</Card>
<View style={{ marginTop: 24 }}>
<Btn label="로그아웃" kind="danger" icon="log-out" onPress={logout} />
</View>
<View style={{ height: 24 }} />
</ScrollView>
)
}
const makeStyles = (t: ThemeTokens) => StyleSheet.create({
wrap: { flex: 1, backgroundColor: t.bg },
content: { padding: 16 },
profileHead: { alignItems: 'center', paddingVertical: 20 },
avatar: { width: 72, height: 72, borderRadius: 36, backgroundColor: t.brand, justifyContent: 'center', alignItems: 'center' },
avatarText: { color: t.onBrand, fontSize: 30, fontWeight: '900' },
name: { color: t.text, fontSize: 20, fontWeight: '900', marginTop: 12 },
sub: { color: t.muted, fontSize: 13, marginTop: 2 },
roleRow: { flexDirection: 'row', flexWrap: 'wrap', gap: 6, marginTop: 10, justifyContent: 'center' },
roleBadge: { backgroundColor: t.brand + '1a', borderRadius: 8, paddingHorizontal: 10, paddingVertical: 4 },
roleText: { color: t.brand, fontSize: 11, fontWeight: '800' },
mt: { marginTop: 22 },
modeRow: { flexDirection: 'row', gap: 10 },
modeCell: { flex: 1, backgroundColor: t.card, borderRadius: 12, borderWidth: 1, borderColor: t.border, paddingVertical: 16, alignItems: 'center', gap: 6 },
modeLbl: { fontSize: 12, fontWeight: '700' },
})