/** * #32 역할별 메뉴 제어 훅 * JWT payload에서 role을 추출해 노출 허용 탭 목록을 반환한다. * * engineer: 운영 탭 / pm: 승인+보고 / admin: 전체 */ import { useEffect, useState } from 'react' import * as SecureStore from 'expo-secure-store' export type Role = 'engineer' | 'pm' | 'admin' | string /** 실제 (tabs)에 존재하는 라우트 기준 매핑 */ export const ROLE_TABS: Record = { engineer: ['index', 'sr', 'chat', 'notifications', 'dr', 'network', 'scan', 'settings'], pm: ['index', 'sr', 'chat', 'notifications', 'insights', 'settings'], admin: ['*'], // 전체 허용 } /** 모든 탭 (admin/fallback 용) */ export const ALL_TABS = [ 'index', 'sr', 'chat', 'notifications', 'dr', 'network', 'insights', 'voice', 'scan', 'settings', ] /** RN-safe base64 디코더 (atob 미보장 환경 대응) */ const B64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' function base64UrlDecode(input: string): string { let str = input.replace(/-/g, '+').replace(/_/g, '/') while (str.length % 4) str += '=' let output = '' for (let i = 0; i < str.length; ) { const e1 = B64.indexOf(str.charAt(i++)) const e2 = B64.indexOf(str.charAt(i++)) const e3 = B64.indexOf(str.charAt(i++)) const e4 = B64.indexOf(str.charAt(i++)) const c1 = (e1 << 2) | (e2 >> 4) const c2 = ((e2 & 15) << 4) | (e3 >> 2) const c3 = ((e3 & 3) << 6) | e4 output += String.fromCharCode(c1) if (e3 !== 64 && e3 !== -1) output += String.fromCharCode(c2) if (e4 !== 64 && e4 !== -1) output += String.fromCharCode(c3) } // UTF-8 디코드 try { return decodeURIComponent( output.split('').map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)).join('') ) } catch { return output } } export function decodeJWT(token: string): { role?: string; user_id?: string; sub?: string; [k: string]: any } { try { const parts = token.split('.') if (parts.length < 2) return {} return JSON.parse(base64UrlDecode(parts[1])) } catch { return {} } } /** 토큰에서 role만 동기적으로 추출 (토큰 문자열을 이미 가진 경우) */ export function roleFromToken(token: string | null | undefined): Role { if (!token) return 'engineer' const p = decodeJWT(token) return (p.role as string) ?? 'engineer' } /** role 기준 허용 탭 목록 */ export function tabsForRole(role: Role): string[] { const allowed = ROLE_TABS[role] if (!allowed || allowed.includes('*')) return ALL_TABS return allowed } /** SecureStore 토큰을 읽어 허용 탭을 반환하는 비동기 훅 */ export function useRoleMenu(): { role: Role; tabs: string[]; loading: boolean } { const [role, setRole] = useState('engineer') const [tabs, setTabs] = useState(ALL_TABS) const [loading, setLoading] = useState(true) useEffect(() => { ;(async () => { try { const token = await SecureStore.getItemAsync('grd_token') const r = roleFromToken(token) setRole(r) setTabs(tabsForRole(r)) } catch { setRole('engineer') setTabs(tabsForRole('engineer')) } finally { setLoading(false) } })() }, []) return { role, tabs, loading } }