136 lines
8.5 KiB
TypeScript
136 lines
8.5 KiB
TypeScript
import { useEffect, useState } from 'react'
|
|
import { Outlet, Navigate, NavLink, useNavigate } from 'react-router-dom'
|
|
import {
|
|
LayoutDashboard, Store, Flower2, Boxes, ShoppingBag, Users, Crown, Megaphone,
|
|
Repeat, CalendarClock, BarChart3, UserCog, ScrollText, Settings, LogOut, UserCircle, ArrowLeftRight, Smartphone,
|
|
ClipboardList, CalendarDays, Mail, PieChart, Sparkles, Cpu,
|
|
ShieldCheck, KeySquare, ListTree, Menu as MenuIcon, Building2, Briefcase,
|
|
} from 'lucide-react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import { getMe } from '../api/client'
|
|
import LanguageSwitcher from '../i18n/LanguageSwitcher'
|
|
|
|
const links = [
|
|
{ to: '/admin/dashboard', key: 'dashboard', icon: LayoutDashboard, roles: ['ADMIN', 'MANAGER'] },
|
|
{ to: '/admin/stores', key: 'stores', icon: Store, roles: ['ADMIN', 'MANAGER'] },
|
|
{ to: '/admin/products', key: 'products', icon: Flower2, roles: ['ADMIN', 'MANAGER'] },
|
|
{ to: '/admin/inventory', key: 'inventory', icon: Boxes, roles: ['ADMIN', 'MANAGER'] },
|
|
{ to: '/admin/orders', key: 'orders', icon: ShoppingBag, roles: ['ADMIN', 'MANAGER'] },
|
|
{ to: '/admin/transfers', key: 'transfers', icon: ArrowLeftRight, roles: ['ADMIN', 'MANAGER'] },
|
|
{ to: '/admin/members', key: 'members', icon: Users, roles: ['ADMIN', 'MANAGER'] },
|
|
{ to: '/admin/loyalty', key: 'loyalty', icon: Crown, roles: ['ADMIN', 'MANAGER'] },
|
|
{ to: '/admin/events', key: 'events', icon: Megaphone, roles: ['ADMIN', 'MANAGER'] },
|
|
{ to: '/admin/subscriptions', key: 'subscriptions', icon: Repeat, roles: ['ADMIN', 'MANAGER'] },
|
|
{ to: '/admin/schedule', key: 'schedule', icon: CalendarClock, roles: ['ADMIN', 'MANAGER'] },
|
|
{ to: '/admin/analytics', key: 'analytics', icon: BarChart3, roles: ['ADMIN', 'MANAGER'] },
|
|
]
|
|
const adminLinks = [
|
|
{ to: '/admin/users', key: 'users', icon: UserCog, roles: ['ADMIN'] },
|
|
{ to: '/admin/audit', key: 'audit', icon: ScrollText, roles: ['ADMIN', 'MANAGER'] },
|
|
{ to: '/admin/settings', key: 'settings', icon: Settings, roles: ['ADMIN'] },
|
|
{ to: '/admin/app', key: 'appInstall', icon: Smartphone, roles: ['ADMIN', 'MANAGER'] },
|
|
]
|
|
// 최신 AI 기법(중앙 guardia-rag) 토글 — 라벨 i18n 미의존(고정 표기), 변경은 MANAGER+(USER 차단)
|
|
const aiLinks = [
|
|
{ to: '/admin/ai-techniques', label: 'AI Techniques', icon: Sparkles, roles: ['ADMIN', 'MANAGER'] },
|
|
{ to: '/admin/ai-platform', label: 'AI 플랫폼 설정', icon: Cpu, roles: ['ADMIN'] },
|
|
]
|
|
// UIWS 이식 — 업무 모듈(관리자 영역 병합). 라벨은 i18n 미의존(고정 한/영 안전 표기).
|
|
const uiwsLinks = [
|
|
{ to: '/admin/uiws/worklog', label: '업무일지 Worklog', icon: ClipboardList, roles: ['ADMIN', 'MANAGER'] },
|
|
{ to: '/admin/uiws/schedule', label: '일정 Schedule', icon: CalendarDays, roles: ['ADMIN', 'MANAGER'] },
|
|
{ to: '/admin/uiws/message', label: '쪽지 Message', icon: Mail, roles: ['ADMIN', 'MANAGER'] },
|
|
{ to: '/admin/uiws/stats', label: '업무통계 Stats', icon: PieChart, roles: ['ADMIN', 'MANAGER'] },
|
|
]
|
|
// UIWS 시스템관리(권한 RBAC) — SuperAdmin(ADMIN) 전용. 고객앱/기존 admin 보존, 백엔드 권한과 이중 가드.
|
|
const sysLinks = [
|
|
{ to: '/admin/uiws/system/roles', label: '권한 관리', icon: ShieldCheck, roles: ['ADMIN'] },
|
|
{ to: '/admin/uiws/system/role-menus', label: '역할-메뉴 권한', icon: KeySquare, roles: ['ADMIN'] },
|
|
{ to: '/admin/uiws/system/codes', label: '공통코드', icon: ListTree, roles: ['ADMIN'] },
|
|
{ to: '/admin/uiws/system/menus', label: '메뉴 관리', icon: MenuIcon, roles: ['ADMIN'] },
|
|
{ to: '/admin/uiws/system/depts', label: '부서 관리', icon: Building2, roles: ['ADMIN'] },
|
|
{ to: '/admin/uiws/system/companies', label: '거래처 관리', icon: Briefcase, roles: ['ADMIN'] },
|
|
]
|
|
const linkClass = ({ isActive }: { isActive: boolean }) =>
|
|
`flex items-center gap-3 px-5 py-2.5 text-sm transition-colors ${isActive ? 'bg-card text-brand border-r-2 border-brand' : 'text-slate-300 hover:bg-card/60'}`
|
|
|
|
export default function AdminLayout() {
|
|
const { t } = useTranslation()
|
|
const token = localStorage.getItem('mall_admin_token')
|
|
const [role, setRole] = useState(() => localStorage.getItem('mall_role') || '')
|
|
const [who, setWho] = useState(() => localStorage.getItem('mall_admin_user') || '')
|
|
const nav = useNavigate()
|
|
|
|
useEffect(() => {
|
|
document.documentElement.classList.add('admin-shell')
|
|
return () => document.documentElement.classList.remove('admin-shell')
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
if (!token) return
|
|
getMe().then((me: any) => {
|
|
if (me?.role) { localStorage.setItem('mall_role', me.role); setRole(me.role) }
|
|
if (me?.username) { localStorage.setItem('mall_admin_user', me.username); setWho(me.username) }
|
|
}).catch(() => {})
|
|
}, [token])
|
|
|
|
if (!token) return <Navigate to="/admin/login" replace />
|
|
// Block USER role from accessing the admin area
|
|
if (role && role === 'USER') return <Navigate to="/admin/login" replace />
|
|
|
|
const visible = links.filter(l => !role || l.roles.includes(role))
|
|
const visibleAdmin = adminLinks.filter(l => l.roles.includes(role))
|
|
const visibleAi = aiLinks.filter(l => !role || l.roles.includes(role))
|
|
const visibleUiws = uiwsLinks.filter(l => !role || l.roles.includes(role))
|
|
const visibleSys = sysLinks.filter(l => l.roles.includes(role)) // ADMIN 전용(role 비어있으면 비표시)
|
|
|
|
const logout = () => {
|
|
localStorage.removeItem('mall_admin_token'); localStorage.removeItem('mall_role'); localStorage.removeItem('mall_admin_user')
|
|
nav('/admin/login')
|
|
}
|
|
|
|
return (
|
|
<div className="admin-shell flex h-screen bg-ink text-[#e6edf6]">
|
|
<aside className="w-60 bg-panel border-r border-edge flex flex-col">
|
|
<div className="h-16 flex items-center gap-2 px-5 border-b border-edge">
|
|
<Flower2 className="text-brand" size={22} />
|
|
<div><div className="font-bold text-base leading-tight">GUARDiA Mall</div><div className="text-[11px] text-slate-400">{t('admin.console')}</div></div>
|
|
</div>
|
|
<nav className="flex-1 py-2 overflow-auto">
|
|
{visible.map(({ to, key, icon: Icon }) => <NavLink key={to} to={to} className={linkClass}><Icon size={18} />{t(`admin.nav.${key}`)}</NavLink>)}
|
|
{visibleAdmin.length > 0 && (<>
|
|
<div className="px-5 pt-4 pb-1.5 text-[10px] font-semibold tracking-wider text-slate-500 uppercase border-t border-edge mt-3">{t('admin.system')}</div>
|
|
{visibleAdmin.map(({ to, key, icon: Icon }) => <NavLink key={to} to={to} className={linkClass}><Icon size={18} />{t(`admin.nav.${key}`)}</NavLink>)}
|
|
</>)}
|
|
{visibleAi.length > 0 && (<>
|
|
<div className="px-5 pt-4 pb-1.5 text-[10px] font-semibold tracking-wider text-slate-500 uppercase border-t border-edge mt-3">AI</div>
|
|
{visibleAi.map(({ to, label, icon: Icon }) => <NavLink key={to} to={to} className={linkClass}><Icon size={18} />{label}</NavLink>)}
|
|
</>)}
|
|
{visibleUiws.length > 0 && (<>
|
|
<div className="px-5 pt-4 pb-1.5 text-[10px] font-semibold tracking-wider text-slate-500 uppercase border-t border-edge mt-3">업무 (UIWS)</div>
|
|
{visibleUiws.map(({ to, label, icon: Icon }) => <NavLink key={to} to={to} className={linkClass}><Icon size={18} />{label}</NavLink>)}
|
|
</>)}
|
|
{visibleSys.length > 0 && (<>
|
|
<div className="px-5 pt-4 pb-1.5 text-[10px] font-semibold tracking-wider text-slate-500 uppercase border-t border-edge mt-3">시스템관리 (권한)</div>
|
|
{visibleSys.map(({ to, label, icon: Icon }) => <NavLink key={to} to={to} className={linkClass}><Icon size={18} />{label}</NavLink>)}
|
|
</>)}
|
|
</nav>
|
|
<div className="p-4 text-[11px] text-slate-500 border-t border-edge">{t('admin.onPremiseTag')}</div>
|
|
</aside>
|
|
<div className="flex-1 flex flex-col overflow-hidden">
|
|
<header className="h-16 bg-panel border-b border-edge flex items-center justify-between px-6">
|
|
<div className="text-sm text-slate-400 truncate">{t('admin.header')}</div>
|
|
<div className="flex items-center gap-4">
|
|
<LanguageSwitcher variant="admin" />
|
|
<span className="flex items-center gap-1.5 text-sm text-slate-300"><UserCircle size={18} /> {who || 'admin'} <span className="text-[10px] text-brand">{role}</span></span>
|
|
<button onClick={logout} className="flex items-center gap-1.5 text-sm text-slate-400 hover:text-brand"><LogOut size={16} /> {t('admin.signOut')}</button>
|
|
</div>
|
|
</header>
|
|
<main className="flex-1 overflow-auto p-6">
|
|
<Outlet />
|
|
</main>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|