Merge pull request 'feat: 프론트엔드 UI 개편 - 메인 화면 및 섹션별 네비게이션 (#115)' (#116) from feature/ISSUE-115-frontend-ui-redesign into develop
This commit is contained in:
커밋
88e7fb3d12
@ -5,6 +5,10 @@
|
|||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
### 추가
|
### 추가
|
||||||
|
- 프론트엔드 UI 개편 (#115)
|
||||||
|
- 메인 화면 3개 섹션 카드 (Collector/Bypass/Risk&Compliance)
|
||||||
|
- 섹션별 Navbar 분리
|
||||||
|
- 플랫폼명 S&P Data Platform 변경
|
||||||
- Risk&Compliance 값 변경 이력 확인 페이지 개발 (#111)
|
- Risk&Compliance 값 변경 이력 확인 페이지 개발 (#111)
|
||||||
- 선박 위험지표/선박 제재/회사 제재 변경 이력 조회
|
- 선박 위험지표/선박 제재/회사 제재 변경 이력 조회
|
||||||
- 선박/회사 기본정보 및 현재 Risk&Compliance 상태 조회
|
- 선박/회사 기본정보 및 현재 Risk&Compliance 상태 조회
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
<link rel="apple-touch-icon" sizes="180x180" href="/snp-api/apple-touch-icon.png" />
|
<link rel="apple-touch-icon" sizes="180x180" href="/snp-api/apple-touch-icon.png" />
|
||||||
<link rel="manifest" href="/snp-api/site.webmanifest" />
|
<link rel="manifest" href="/snp-api/site.webmanifest" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>S&P 배치 관리</title>
|
<title>S&P Data Platform</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@ -1,11 +1,12 @@
|
|||||||
import { lazy, Suspense } from 'react';
|
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 { ToastProvider, useToastContext } from './contexts/ToastContext';
|
||||||
import { ThemeProvider } from './contexts/ThemeContext';
|
import { ThemeProvider } from './contexts/ThemeContext';
|
||||||
import Navbar from './components/Navbar';
|
import Navbar from './components/Navbar';
|
||||||
import ToastContainer from './components/Toast';
|
import ToastContainer from './components/Toast';
|
||||||
import LoadingSpinner from './components/LoadingSpinner';
|
import LoadingSpinner from './components/LoadingSpinner';
|
||||||
|
|
||||||
|
const MainMenu = lazy(() => import('./pages/MainMenu'));
|
||||||
const Dashboard = lazy(() => import('./pages/Dashboard'));
|
const Dashboard = lazy(() => import('./pages/Dashboard'));
|
||||||
const Jobs = lazy(() => import('./pages/Jobs'));
|
const Jobs = lazy(() => import('./pages/Jobs'));
|
||||||
const Executions = lazy(() => import('./pages/Executions'));
|
const Executions = lazy(() => import('./pages/Executions'));
|
||||||
@ -20,14 +21,17 @@ const RiskComplianceHistory = lazy(() => import('./pages/RiskComplianceHistory')
|
|||||||
|
|
||||||
function AppLayout() {
|
function AppLayout() {
|
||||||
const { toasts, removeToast } = useToastContext();
|
const { toasts, removeToast } = useToastContext();
|
||||||
|
const location = useLocation();
|
||||||
|
const isMainMenu = location.pathname === '/';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-wing-bg text-wing-text">
|
<div className="min-h-screen bg-wing-bg text-wing-text">
|
||||||
<div className="max-w-7xl mx-auto px-4 py-6">
|
<div className={isMainMenu ? 'px-4' : 'max-w-7xl mx-auto px-4 py-6'}>
|
||||||
<Navbar />
|
<Navbar />
|
||||||
<Suspense fallback={<LoadingSpinner />}>
|
<Suspense fallback={<LoadingSpinner />}>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Dashboard />} />
|
<Route path="/" element={<MainMenu />} />
|
||||||
|
<Route path="/dashboard" element={<Dashboard />} />
|
||||||
<Route path="/jobs" element={<Jobs />} />
|
<Route path="/jobs" element={<Jobs />} />
|
||||||
<Route path="/executions" element={<Executions />} />
|
<Route path="/executions" element={<Executions />} />
|
||||||
<Route path="/executions/:id" element={<ExecutionDetail />} />
|
<Route path="/executions/:id" element={<ExecutionDetail />} />
|
||||||
|
|||||||
@ -1,35 +1,84 @@
|
|||||||
import { Link, useLocation } from 'react-router-dom';
|
import { Link, useLocation } from 'react-router-dom';
|
||||||
import { useThemeContext } from '../contexts/ThemeContext';
|
import { useThemeContext } from '../contexts/ThemeContext';
|
||||||
|
|
||||||
const navItems = [
|
interface NavSection {
|
||||||
{ path: '/', label: '대시보드', icon: '📊' },
|
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: '/executions', label: '실행 이력', icon: '📋' },
|
||||||
{ path: '/recollects', label: '재수집 이력', icon: '🔄' },
|
{ path: '/recollects', label: '재수집 이력', icon: '🔄' },
|
||||||
{ path: '/jobs', label: '작업', icon: '⚙️' },
|
{ path: '/jobs', label: '작업', icon: '⚙️' },
|
||||||
{ path: '/schedules', label: '스케줄', icon: '🕐' },
|
{ path: '/schedules', label: '스케줄', icon: '🕐' },
|
||||||
{ path: '/schedule-timeline', label: '타임라인', icon: '📅' },
|
{ path: '/schedule-timeline', label: '타임라인', icon: '📅' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'bypass',
|
||||||
|
title: 'S&P Bypass',
|
||||||
|
paths: ['/bypass-config'],
|
||||||
|
items: [
|
||||||
{ path: '/bypass-config', label: 'Bypass API', icon: '🔗' },
|
{ 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: '/screening-guide', label: 'Screening Guide', icon: '⚖️' },
|
||||||
{ path: '/risk-compliance-history', label: 'Change History', icon: '📜' },
|
{ path: '/risk-compliance-history', label: 'Change History', icon: '📜' },
|
||||||
|
],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
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() {
|
export default function Navbar() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { theme, toggle } = useThemeContext();
|
const { theme, toggle } = useThemeContext();
|
||||||
|
const currentSection = getCurrentSection(location.pathname);
|
||||||
|
|
||||||
|
// 메인 화면에서는 Navbar 숨김
|
||||||
|
if (!currentSection) return null;
|
||||||
|
|
||||||
const isActive = (path: string) => {
|
const isActive = (path: string) => {
|
||||||
if (path === '/') return location.pathname === '/';
|
if (path === '/dashboard') return location.pathname === '/dashboard';
|
||||||
return location.pathname.startsWith(path);
|
return location.pathname === path || location.pathname.startsWith(path + '/');
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="bg-wing-glass-dense backdrop-blur-sm shadow-md rounded-xl mb-6 px-4 py-3 border border-wing-border">
|
<nav className="bg-wing-glass-dense backdrop-blur-sm shadow-md rounded-xl mb-6 px-4 py-3 border border-wing-border">
|
||||||
<div className="flex items-center justify-between flex-wrap gap-2">
|
<div className="flex items-center justify-between flex-wrap gap-2">
|
||||||
<Link to="/" className="text-lg font-bold text-wing-accent no-underline">
|
<div className="flex items-center gap-3">
|
||||||
S&P 배치 관리
|
<Link
|
||||||
|
to="/"
|
||||||
|
className="px-2.5 py-1.5 rounded-lg text-sm font-medium no-underline text-wing-muted hover:bg-wing-hover hover:text-wing-accent transition-colors"
|
||||||
|
title="메인 메뉴"
|
||||||
|
>
|
||||||
|
← 메인
|
||||||
</Link>
|
</Link>
|
||||||
|
<span className="text-wing-border">|</span>
|
||||||
|
<span className="text-sm font-bold text-wing-accent">{currentSection.title}</span>
|
||||||
|
</div>
|
||||||
<div className="flex gap-1 flex-wrap items-center">
|
<div className="flex gap-1 flex-wrap items-center">
|
||||||
{navItems.map((item) => (
|
{currentSection.items.map((item) => (
|
||||||
<Link
|
<Link
|
||||||
key={item.path}
|
key={item.path}
|
||||||
to={item.path}
|
to={item.path}
|
||||||
|
|||||||
69
frontend/src/pages/MainMenu.tsx
Normal file
69
frontend/src/pages/MainMenu.tsx
Normal file
@ -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 (
|
||||||
|
<div className="min-h-[70vh] flex flex-col items-center justify-center">
|
||||||
|
{/* 헤더 */}
|
||||||
|
<div className="text-center mb-10">
|
||||||
|
<h1 className="text-3xl font-bold text-wing-text mb-2">S&P Data Platform</h1>
|
||||||
|
<p className="text-sm text-wing-muted">해양 데이터 통합 관리 플랫폼</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 섹션 카드 */}
|
||||||
|
<div className="gc-cards">
|
||||||
|
{sections.map((section) => (
|
||||||
|
<Link key={section.path} to={section.path} className="gc-card">
|
||||||
|
<div className={section.iconClass}>
|
||||||
|
<span className="text-5xl">{section.icon}</span>
|
||||||
|
</div>
|
||||||
|
<h3>{section.title}</h3>
|
||||||
|
<p>{section.description}<br />{section.detail}</p>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 테마 토글 */}
|
||||||
|
<button
|
||||||
|
onClick={toggle}
|
||||||
|
className="mt-8 px-3 py-1.5 rounded-lg text-sm bg-wing-card text-wing-muted
|
||||||
|
hover:text-wing-text border border-wing-border transition-colors"
|
||||||
|
title={theme === 'dark' ? '라이트 모드' : '다크 모드'}
|
||||||
|
>
|
||||||
|
{theme === 'dark' ? '☀️ 라이트 모드' : '🌙 다크 모드'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -23,3 +23,76 @@ body {
|
|||||||
::-webkit-scrollbar-thumb:hover {
|
::-webkit-scrollbar-thumb:hover {
|
||||||
background: var(--wing-accent);
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@ -12,11 +12,11 @@ import org.springframework.web.bind.annotation.GetMapping;
|
|||||||
@Controller
|
@Controller
|
||||||
public class WebViewController {
|
public class WebViewController {
|
||||||
|
|
||||||
@GetMapping({"/", "/jobs", "/executions", "/executions/{id:\\d+}",
|
@GetMapping({"/", "/dashboard", "/jobs", "/executions", "/executions/{id:\\d+}",
|
||||||
"/recollects", "/recollects/{id:\\d+}",
|
"/recollects", "/recollects/{id:\\d+}",
|
||||||
"/schedules", "/schedule-timeline", "/monitoring",
|
"/schedules", "/schedule-timeline", "/monitoring",
|
||||||
"/bypass-config", "/screening-guide", "/risk-compliance-history",
|
"/bypass-config", "/screening-guide", "/risk-compliance-history",
|
||||||
"/jobs/**", "/executions/**", "/recollects/**",
|
"/dashboard/**", "/jobs/**", "/executions/**", "/recollects/**",
|
||||||
"/schedules/**", "/schedule-timeline/**", "/monitoring/**",
|
"/schedules/**", "/schedule-timeline/**", "/monitoring/**",
|
||||||
"/bypass-config/**", "/screening-guide/**", "/risk-compliance-history/**"})
|
"/bypass-config/**", "/screening-guide/**", "/risk-compliance-history/**"})
|
||||||
public String forward() {
|
public String forward() {
|
||||||
|
|||||||
불러오는 중...
Reference in New Issue
Block a user