diff --git a/docs/RELEASE-NOTES.md b/docs/RELEASE-NOTES.md
index 6817940..c9dee1d 100644
--- a/docs/RELEASE-NOTES.md
+++ b/docs/RELEASE-NOTES.md
@@ -4,6 +4,12 @@
## [Unreleased]
+### 추가
+- 공통 UI 피드백 반영 (#121)
+ - 2단 탭 네비게이션 (섹션 탭 + 서브 탭)
+ - 섹션 간 직접 이동
+ - 메인화면 카드 높이 동기화 (CSS Grid)
+
## [2026-03-31]
### 추가
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 96c6356..416167e 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -25,26 +25,40 @@ function AppLayout() {
const isMainMenu = location.pathname === '/';
return (
-
-
-
- }>
-
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
-
-
-
+
+ {/* 메인 화면: 전체화면, 섹션 페이지: 탭 + 스크롤 콘텐츠 */}
+ {isMainMenu ? (
+
+ }>
+
+ } />
+
+
+
+ ) : (
+ <>
+
+
+
+
+ }>
+
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+
+
+
+ >
+ )}
);
diff --git a/frontend/src/components/Navbar.tsx b/frontend/src/components/Navbar.tsx
index adf72d7..564edcf 100644
--- a/frontend/src/components/Navbar.tsx
+++ b/frontend/src/components/Navbar.tsx
@@ -1,49 +1,75 @@
-import { Link, useLocation } from 'react-router-dom';
+import { Link, useLocation, useNavigate } from 'react-router-dom';
import { useThemeContext } from '../contexts/ThemeContext';
-interface NavSection {
- key: string;
- title: string;
- paths: string[];
- items: { path: string; label: string; icon: string }[];
+interface MenuItem {
+ id: string;
+ label: string;
+ path: string;
}
-const sections: NavSection[] = [
+interface MenuSection {
+ id: string;
+ label: string;
+ shortLabel: string;
+ icon: React.ReactNode;
+ defaultPath: string;
+ children: MenuItem[];
+}
+
+const MENU_STRUCTURE: MenuSection[] = [
{
- key: 'collector',
- title: 'S&P Collector',
- paths: ['/dashboard', '/jobs', '/executions', '/recollects', '/schedules', '/schedule-timeline'],
- items: [
- { path: '/dashboard', label: '대시보드', icon: '📊' },
- { path: '/executions', label: '실행 이력', icon: '📋' },
- { path: '/recollects', label: '재수집 이력', icon: '🔄' },
- { path: '/jobs', label: '작업', icon: '⚙️' },
- { path: '/schedules', label: '스케줄', icon: '🕐' },
- { path: '/schedule-timeline', label: '타임라인', icon: '📅' },
+ id: 'collector',
+ label: 'S&P Collector',
+ shortLabel: 'Collector',
+ icon: (
+
+ ),
+ defaultPath: '/dashboard',
+ children: [
+ { id: 'dashboard', label: '대시보드', path: '/dashboard' },
+ { id: 'executions', label: '실행 이력', path: '/executions' },
+ { id: 'recollects', label: '재수집 이력', path: '/recollects' },
+ { id: 'jobs', label: '작업 관리', path: '/jobs' },
+ { id: 'schedules', label: '스케줄', path: '/schedules' },
+ { id: 'timeline', label: '타임라인', path: '/schedule-timeline' },
],
},
{
- key: 'bypass',
- title: 'S&P Bypass',
- paths: ['/bypass-config'],
- items: [
- { path: '/bypass-config', label: 'Bypass API', icon: '🔗' },
+ id: 'bypass',
+ label: 'S&P Bypass',
+ shortLabel: 'Bypass',
+ icon: (
+
+ ),
+ defaultPath: '/bypass-config',
+ children: [
+ { id: 'bypass-config', label: 'Bypass API', path: '/bypass-config' },
],
},
{
- key: 'risk',
- title: 'S&P Risk & Compliance',
- paths: ['/screening-guide', '/risk-compliance-history'],
- items: [
- { path: '/screening-guide', label: 'Screening Guide', icon: '⚖️' },
- { path: '/risk-compliance-history', label: 'Change History', icon: '📜' },
+ id: 'risk',
+ label: 'S&P Risk & Compliance',
+ shortLabel: 'Risk & Compliance',
+ icon: (
+
+ ),
+ defaultPath: '/screening-guide',
+ children: [
+ { id: 'screening-guide', label: 'Screening Guide', path: '/screening-guide' },
+ { id: 'risk-compliance-history', label: 'Change History', path: '/risk-compliance-history' },
],
},
];
-function getCurrentSection(pathname: string): NavSection | null {
- for (const section of sections) {
- if (section.paths.some((p) => pathname === p || pathname.startsWith(p + '/'))) {
+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;
}
}
@@ -52,56 +78,75 @@ function getCurrentSection(pathname: string): NavSection | null {
export default function Navbar() {
const location = useLocation();
+ const navigate = useNavigate();
const { theme, toggle } = useThemeContext();
const currentSection = getCurrentSection(location.pathname);
- // 메인 화면에서는 Navbar 숨김
+ // 메인 화면에서는 숨김
if (!currentSection) return null;
- const isActive = (path: string) => {
- if (path === '/dashboard') return location.pathname === '/dashboard';
+ const isActivePath = (path: string) => {
return location.pathname === path || location.pathname.startsWith(path + '/');
};
return (
-