diff --git a/docs/RELEASE-NOTES.md b/docs/RELEASE-NOTES.md
index 9046f36..3e47e2f 100644
--- a/docs/RELEASE-NOTES.md
+++ b/docs/RELEASE-NOTES.md
@@ -5,6 +5,10 @@
## [Unreleased]
### 추가
+- 프론트엔드 UI 개편 (#115)
+ - 메인 화면 3개 섹션 카드 (Collector/Bypass/Risk&Compliance)
+ - 섹션별 Navbar 분리
+ - 플랫폼명 S&P Data Platform 변경
- Risk&Compliance 값 변경 이력 확인 페이지 개발 (#111)
- 선박 위험지표/선박 제재/회사 제재 변경 이력 조회
- 선박/회사 기본정보 및 현재 Risk&Compliance 상태 조회
diff --git a/frontend/index.html b/frontend/index.html
index b92b03d..4958206 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -8,7 +8,7 @@
-
S&P 배치 관리
+ S&P Data Platform
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 7efdcfd..96c6356 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -1,11 +1,12 @@
import { lazy, Suspense } from 'react';
-import { BrowserRouter, Routes, Route } from 'react-router-dom';
+import { BrowserRouter, Routes, Route, useLocation } from 'react-router-dom';
import { ToastProvider, useToastContext } from './contexts/ToastContext';
import { ThemeProvider } from './contexts/ThemeContext';
import Navbar from './components/Navbar';
import ToastContainer from './components/Toast';
import LoadingSpinner from './components/LoadingSpinner';
+const MainMenu = lazy(() => import('./pages/MainMenu'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Jobs = lazy(() => import('./pages/Jobs'));
const Executions = lazy(() => import('./pages/Executions'));
@@ -20,14 +21,17 @@ const RiskComplianceHistory = lazy(() => import('./pages/RiskComplianceHistory')
function AppLayout() {
const { toasts, removeToast } = useToastContext();
+ const location = useLocation();
+ const isMainMenu = location.pathname === '/';
return (
-
+
}>
- } />
+ } />
+ } />
} />
} />
} />
diff --git a/frontend/src/components/Navbar.tsx b/frontend/src/components/Navbar.tsx
index 142689f..adf72d7 100644
--- a/frontend/src/components/Navbar.tsx
+++ b/frontend/src/components/Navbar.tsx
@@ -1,58 +1,107 @@
import { Link, useLocation } from 'react-router-dom';
import { useThemeContext } from '../contexts/ThemeContext';
-const navItems = [
- { path: '/', label: '대시보드', icon: '📊' },
- { path: '/executions', label: '실행 이력', icon: '📋' },
- { path: '/recollects', label: '재수집 이력', icon: '🔄' },
- { path: '/jobs', label: '작업', icon: '⚙️' },
- { path: '/schedules', label: '스케줄', icon: '🕐' },
- { path: '/schedule-timeline', label: '타임라인', icon: '📅' },
- { path: '/bypass-config', label: 'Bypass API', icon: '🔗' },
- { path: '/screening-guide', label: 'Screening Guide', icon: '⚖️' },
- { path: '/risk-compliance-history', label: 'Change History', icon: '📜' },
+interface NavSection {
+ key: string;
+ title: string;
+ paths: string[];
+ items: { path: string; label: string; icon: string }[];
+}
+
+const sections: NavSection[] = [
+ {
+ 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: '📅' },
+ ],
+ },
+ {
+ key: 'bypass',
+ title: 'S&P Bypass',
+ paths: ['/bypass-config'],
+ items: [
+ { path: '/bypass-config', label: 'Bypass API', icon: '🔗' },
+ ],
+ },
+ {
+ 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: '📜' },
+ ],
+ },
];
-export default function Navbar() {
- const location = useLocation();
- const { theme, toggle } = useThemeContext();
-
- const isActive = (path: string) => {
- if (path === '/') return location.pathname === '/';
- return location.pathname.startsWith(path);
- };
-
- return (
-
-
-
- S&P 배치 관리
-
-
- {navItems.map((item) => (
-
- {item.icon}
- {item.label}
-
- ))}
-
- {theme === 'dark' ? '☀️' : '🌙'}
-
-
-
-
- );
+function getCurrentSection(pathname: string): NavSection | null {
+ for (const section of sections) {
+ if (section.paths.some((p) => pathname === p || pathname.startsWith(p + '/'))) {
+ return section;
+ }
+ }
+ return null;
+}
+
+export default function Navbar() {
+ const location = useLocation();
+ 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';
+ return location.pathname === path || location.pathname.startsWith(path + '/');
+ };
+
+ return (
+
+
+
+
+ ← 메인
+
+ |
+ {currentSection.title}
+
+
+ {currentSection.items.map((item) => (
+
+ {item.icon}
+ {item.label}
+
+ ))}
+
+ {theme === 'dark' ? '☀️' : '🌙'}
+
+
+
+
+ );
}
diff --git a/frontend/src/pages/MainMenu.tsx b/frontend/src/pages/MainMenu.tsx
new file mode 100644
index 0000000..60a1723
--- /dev/null
+++ b/frontend/src/pages/MainMenu.tsx
@@ -0,0 +1,69 @@
+import { Link } from 'react-router-dom';
+import { useThemeContext } from '../contexts/ThemeContext';
+
+const sections = [
+ {
+ title: 'S&P Collector',
+ description: 'S&P 배치 수집 관리',
+ detail: '대시보드, 실행 이력, 재수집 이력, 작업 관리, 스케줄, 타임라인',
+ path: '/dashboard',
+ icon: '🔄',
+ iconClass: 'gc-card-icon',
+ menuCount: 6,
+ },
+ {
+ title: 'S&P Bypass',
+ description: 'S&P Bypass API 관리',
+ detail: 'API 등록, 코드 생성 관리, 테스트',
+ path: '/bypass-config',
+ icon: '🔗',
+ iconClass: 'gc-card-icon gc-card-icon-guide',
+ menuCount: 1,
+ },
+ {
+ title: 'S&P Risk & Compliance',
+ description: 'S&P 위험 지표 및 규정 준수',
+ detail: '위험 지표 및 규정 준수 가이드, 변경 이력 조회',
+ path: '/screening-guide',
+ icon: '⚖️',
+ iconClass: 'gc-card-icon gc-card-icon-nexus',
+ menuCount: 2,
+ },
+];
+
+export default function MainMenu() {
+ const { theme, toggle } = useThemeContext();
+
+ return (
+
+ {/* 헤더 */}
+
+
S&P Data Platform
+
해양 데이터 통합 관리 플랫폼
+
+
+ {/* 섹션 카드 */}
+
+ {sections.map((section) => (
+
+
+ {section.icon}
+
+
{section.title}
+
{section.description} {section.detail}
+
+ ))}
+
+
+ {/* 테마 토글 */}
+
+ {theme === 'dark' ? '☀️ 라이트 모드' : '🌙 다크 모드'}
+
+
+ );
+}
diff --git a/frontend/src/theme/base.css b/frontend/src/theme/base.css
index 803906a..7148406 100644
--- a/frontend/src/theme/base.css
+++ b/frontend/src/theme/base.css
@@ -23,3 +23,76 @@ body {
::-webkit-scrollbar-thumb:hover {
background: var(--wing-accent);
}
+
+/* Main Menu Cards */
+.gc-cards {
+ padding: 2rem 0;
+ display: flex;
+ justify-content: center;
+ align-items: stretch;
+ gap: 2rem;
+ width: 80%;
+ margin: 0 auto;
+}
+
+.gc-cards > * {
+ flex: 1 1 0;
+}
+
+.gc-card {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ text-align: center;
+ padding: 2.5rem 2rem;
+ border: 1px solid var(--wing-border);
+ border-radius: 12px;
+ background: var(--wing-surface);
+ text-decoration: none !important;
+ color: inherit !important;
+ transition: all 0.2s ease;
+ height: 100%;
+}
+
+.gc-card:hover {
+ border-color: #4183c4;
+ box-shadow: 0 4px 16px rgba(65, 131, 196, 0.15);
+ transform: translateY(-2px);
+}
+
+.gc-card-icon {
+ color: #4183c4;
+ margin-bottom: 1rem;
+}
+
+.gc-card-icon-guide {
+ color: #21ba45;
+}
+
+.gc-card-icon-nexus {
+ color: #f2711c;
+}
+
+.gc-card h3 {
+ font-size: 1.3rem;
+ margin-bottom: 0.5rem;
+ color: var(--wing-text);
+}
+
+.gc-card p {
+ font-size: 0.95rem;
+ color: var(--wing-muted);
+ line-height: 1.5;
+ margin-bottom: 1rem;
+}
+
+.gc-card-link {
+ font-size: 0.9rem;
+ color: #4183c4;
+ font-weight: 600;
+ margin-top: auto;
+}
+
+.gc-card:hover .gc-card-link {
+ text-decoration: underline;
+}
diff --git a/src/main/java/com/snp/batch/global/controller/WebViewController.java b/src/main/java/com/snp/batch/global/controller/WebViewController.java
index cfb5928..bf2206b 100644
--- a/src/main/java/com/snp/batch/global/controller/WebViewController.java
+++ b/src/main/java/com/snp/batch/global/controller/WebViewController.java
@@ -12,11 +12,11 @@ import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class WebViewController {
- @GetMapping({"/", "/jobs", "/executions", "/executions/{id:\\d+}",
+ @GetMapping({"/", "/dashboard", "/jobs", "/executions", "/executions/{id:\\d+}",
"/recollects", "/recollects/{id:\\d+}",
"/schedules", "/schedule-timeline", "/monitoring",
"/bypass-config", "/screening-guide", "/risk-compliance-history",
- "/jobs/**", "/executions/**", "/recollects/**",
+ "/dashboard/**", "/jobs/**", "/executions/**", "/recollects/**",
"/schedules/**", "/schedule-timeline/**", "/monitoring/**",
"/bypass-config/**", "/screening-guide/**", "/risk-compliance-history/**"})
public String forward() {