- Spring Boot 3.2.1 + React 19 프로젝트 구조 - S&P Global Maritime API Bypass 및 Risk & Compliance Screening 기능 - 팀 워크플로우 v1.6.1 적용 (settings.json, hooks, workflow-version) - 프론트엔드 빌드 (Vite + TypeScript + Tailwind CSS) - 메인 카드 레이아웃 CSS Grid 전환 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
138 lines
5.8 KiB
TypeScript
138 lines
5.8 KiB
TypeScript
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
|
import { useThemeContext } from '../contexts/ThemeContext';
|
|
|
|
interface MenuItem {
|
|
id: string;
|
|
label: string;
|
|
path: string;
|
|
}
|
|
|
|
interface MenuSection {
|
|
id: string;
|
|
label: string;
|
|
shortLabel: string;
|
|
icon: React.ReactNode;
|
|
defaultPath: string;
|
|
children: MenuItem[];
|
|
}
|
|
|
|
const MENU_STRUCTURE: MenuSection[] = [
|
|
{
|
|
id: 'bypass',
|
|
label: 'S&P Global API',
|
|
shortLabel: 'S&P Global API',
|
|
icon: (
|
|
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9" />
|
|
</svg>
|
|
),
|
|
defaultPath: '/bypass-catalog',
|
|
children: [
|
|
{ id: 'bypass-catalog', label: 'API 카탈로그', path: '/bypass-catalog' },
|
|
{ id: 'bypass-config', label: 'API 관리', path: '/bypass-config' },
|
|
{ id: 'bypass-account-requests', label: '계정 신청 관리', path: '/bypass-account-requests' },
|
|
{ id: 'bypass-account-management', label: '계정 관리', path: '/bypass-account-management' },
|
|
{ id: 'bypass-access-request', label: 'API 계정 신청', path: '/bypass-access-request' },
|
|
],
|
|
},
|
|
{
|
|
id: 'risk',
|
|
label: 'S&P Risk & Compliance',
|
|
shortLabel: 'Risk & Compliance',
|
|
icon: (
|
|
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
|
</svg>
|
|
),
|
|
defaultPath: '/risk-compliance-history',
|
|
children: [
|
|
{ id: 'risk-compliance-history', label: 'Change History', path: '/risk-compliance-history' },
|
|
{ id: 'screening-guide', label: 'Screening Guide', path: '/screening-guide' },
|
|
],
|
|
},
|
|
];
|
|
|
|
function getCurrentSection(pathname: string): MenuSection | null {
|
|
for (const section of MENU_STRUCTURE) {
|
|
if (section.children.some((c) => pathname === c.path || pathname.startsWith(c.path + '/'))) {
|
|
return section;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export default function Navbar() {
|
|
const location = useLocation();
|
|
const navigate = useNavigate();
|
|
const { theme, toggle } = useThemeContext();
|
|
const currentSection = getCurrentSection(location.pathname);
|
|
|
|
// 메인 화면에서는 숨김
|
|
if (!currentSection) return null;
|
|
|
|
const isActivePath = (path: string) => {
|
|
return location.pathname === path || location.pathname.startsWith(path + '/');
|
|
};
|
|
|
|
return (
|
|
<div className="mb-6">
|
|
{/* 1단: 섹션 탭 */}
|
|
<div className="bg-slate-900 px-6 pt-3 rounded-t-xl flex items-center">
|
|
<Link
|
|
to="/"
|
|
className="flex items-center gap-1 px-3 py-2.5 text-sm text-slate-400 hover:text-white no-underline transition-colors"
|
|
title="메인 메뉴"
|
|
>
|
|
←
|
|
</Link>
|
|
<div className="flex-1 flex items-center justify-start gap-1">
|
|
{MENU_STRUCTURE.map((section) => (
|
|
<button
|
|
key={section.id}
|
|
onClick={() => {
|
|
if (currentSection?.id !== section.id) {
|
|
navigate(section.defaultPath);
|
|
}
|
|
}}
|
|
className={`flex items-center gap-2 px-5 py-2.5 rounded-t-lg text-sm font-medium transition-all ${
|
|
currentSection?.id === section.id
|
|
? 'bg-wing-bg text-wing-text shadow-sm'
|
|
: 'text-slate-400 hover:text-white hover:bg-slate-800'
|
|
}`}
|
|
>
|
|
{section.icon}
|
|
<span>{section.shortLabel}</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
<button
|
|
onClick={toggle}
|
|
className="px-2.5 py-1.5 rounded-lg text-sm text-slate-400 hover:text-white hover:bg-slate-800 transition-colors"
|
|
title={theme === 'dark' ? '라이트 모드' : '다크 모드'}
|
|
>
|
|
{theme === 'dark' ? '☀️' : '🌙'}
|
|
</button>
|
|
</div>
|
|
|
|
{/* 2단: 서브 탭 */}
|
|
<div className="bg-wing-surface border-b border-wing-border px-6 rounded-b-xl shadow-md">
|
|
<div className="flex gap-1 -mb-px justify-end">
|
|
{currentSection?.children.map((child) => (
|
|
<button
|
|
key={child.id}
|
|
onClick={() => navigate(child.path)}
|
|
className={`px-4 py-3 text-sm font-medium transition-all border-b-2 ${
|
|
isActivePath(child.path)
|
|
? 'border-blue-600 text-blue-600'
|
|
: 'border-transparent text-wing-muted hover:text-wing-text hover:border-wing-border'
|
|
}`}
|
|
>
|
|
{child.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|