플랫폼 고도화: 프리미엄 랜딩 페이지 및 사용자별 기본 설정(랜딩 페이지 선택) 기능 추가
This commit is contained in:
parent
ff45871d22
commit
9614f8d56b
@ -1,5 +1,5 @@
|
||||
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
|
||||
import { AuthProvider } from '../shared/auth/AuthContext';
|
||||
import { AuthProvider, useAuth } from '../shared/auth/AuthContext';
|
||||
import { SystemProvider } from '../shared/context/SystemContext';
|
||||
import { AuthGuard } from '../shared/auth/AuthGuard';
|
||||
import { MainLayout } from '../widgets/layout/MainLayout';
|
||||
@ -16,11 +16,19 @@ import { UserManagementPage } from './pages/UserManagementPage';
|
||||
import { BasicSettingsPage } from './pages/BasicSettingsPage';
|
||||
import { VersionPage } from './pages/VersionPage';
|
||||
import { LicensePage } from '../system/pages/LicensePage';
|
||||
import { LandingPage } from './pages/LandingPage';
|
||||
import { GeneralPreferencesPage } from './pages/GeneralPreferencesPage';
|
||||
|
||||
import './styles/global.css';
|
||||
|
||||
const modules = [assetModule, cctvModule, productionModule];
|
||||
|
||||
const DefaultRedirect = () => {
|
||||
const { user } = useAuth();
|
||||
const preferredPath = localStorage.getItem(`landing_page_${user?.id}`) || '/home';
|
||||
return <Navigate to={preferredPath} replace />;
|
||||
};
|
||||
|
||||
export const App = () => {
|
||||
return (
|
||||
<AuthProvider>
|
||||
@ -36,11 +44,17 @@ export const App = () => {
|
||||
<MainLayout modulesList={modules} />
|
||||
</AuthGuard>
|
||||
}>
|
||||
{/* Home / Platform Introduction */}
|
||||
<Route path="/home" element={<LandingPage />} />
|
||||
|
||||
{/* User Preferences (Accessible by everyone authenticated) */}
|
||||
<Route path="/preferences" element={<GeneralPreferencesPage />} />
|
||||
|
||||
{/* Dynamic Module Routes */}
|
||||
<Route path="/*" element={<ModuleLoader modules={modules} />} />
|
||||
|
||||
{/* Navigation Fallback within Layout */}
|
||||
<Route index element={<Navigate to="/asset/dashboard" replace />} />
|
||||
<Route index element={<DefaultRedirect />} />
|
||||
|
||||
{/* Platform Admin Routes (Could also be moved to an Admin module later) */}
|
||||
<Route path="/admin/users" element={<UserManagementPage />} />
|
||||
|
||||
91
src/platform/pages/GeneralPreferencesPage.tsx
Normal file
91
src/platform/pages/GeneralPreferencesPage.tsx
Normal file
@ -0,0 +1,91 @@
|
||||
import { useState } from 'react';
|
||||
import { Card } from '../../shared/ui/Card';
|
||||
import { Button } from '../../shared/ui/Button';
|
||||
import { Select } from '../../shared/ui/Select';
|
||||
import { Save, Home, User } from 'lucide-react';
|
||||
import { useAuth } from '../../shared/auth/AuthContext';
|
||||
|
||||
export function GeneralPreferencesPage() {
|
||||
const { user } = useAuth();
|
||||
const [preferences, setPreferences] = useState({
|
||||
landingPage: localStorage.getItem(`landing_page_${user?.id}`) || '/home'
|
||||
});
|
||||
const [isSaved, setIsSaved] = useState(false);
|
||||
|
||||
const landingPageOptions = [
|
||||
{ label: '플랫폼 소개 (기본)', value: '/home' },
|
||||
{ label: '자산 대시보드', value: '/asset/dashboard' },
|
||||
{ label: 'CCTV 모니터링', value: '/cctv/monitoring' },
|
||||
{ label: '생산 대시보드', value: '/production/dashboard' }
|
||||
];
|
||||
|
||||
const handleSave = () => {
|
||||
localStorage.setItem(`landing_page_${user?.id}`, preferences.landingPage);
|
||||
setIsSaved(true);
|
||||
setTimeout(() => setIsSaved(false), 3000);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="page-container p-6 max-w-4xl mx-auto">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-2xl font-bold text-slate-900">기본 설정</h1>
|
||||
<p className="text-slate-500 mt-1">사용자 개인의 플랫폼 환경을 설정합니다.</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-6">
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center gap-3 mb-6 border-b border-slate-100 pb-4">
|
||||
<Home className="text-blue-600" size={24} />
|
||||
<h2 className="text-lg font-bold">로그인 랜딩 페이지</h2>
|
||||
</div>
|
||||
|
||||
<div className="max-w-md">
|
||||
<p className="text-sm text-slate-600 mb-4">
|
||||
로그인 시 또는 최상위 메뉴 이동 시 처음으로 표시될 화면을 선택하세요.
|
||||
</p>
|
||||
<Select
|
||||
label="접속 시작 페이지"
|
||||
value={preferences.landingPage}
|
||||
onChange={(e) => setPreferences({ ...preferences, landingPage: e.target.value })}
|
||||
options={landingPageOptions}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center gap-3 mb-6 border-b border-slate-100 pb-4">
|
||||
<User className="text-indigo-600" size={24} />
|
||||
<h2 className="text-lg font-bold">사용자 정보</h2>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-6 text-sm">
|
||||
<div>
|
||||
<span className="text-slate-500 block mb-1">사용자 이름</span>
|
||||
<span className="font-bold text-slate-900">{user?.name}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-slate-500 block mb-1">계정 등급</span>
|
||||
<span className="font-bold text-indigo-600 capitalize">{user?.role}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-slate-500 block mb-1">소속 부서</span>
|
||||
<span className="font-medium text-slate-700">{user?.department || '미지정'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<div className="flex items-center justify-between bg-white p-4 rounded-xl border border-slate-200 shadow-sm mt-4">
|
||||
<div className="text-sm">
|
||||
{isSaved && <span className="text-green-600 font-bold animate-pulse">✓ 설정이 저장되었습니다.</span>}
|
||||
</div>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
icon={<Save size={18} />}
|
||||
>
|
||||
설정 저장
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
275
src/platform/pages/LandingPage.css
Normal file
275
src/platform/pages/LandingPage.css
Normal file
@ -0,0 +1,275 @@
|
||||
.landing-container {
|
||||
padding: 2rem;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4rem;
|
||||
}
|
||||
|
||||
/* Hero Section */
|
||||
.hero-section {
|
||||
display: grid;
|
||||
grid-template-cols: 1.2fr 0.8fr;
|
||||
gap: 4rem;
|
||||
align-items: center;
|
||||
padding: 2rem 0;
|
||||
}
|
||||
|
||||
.premium-badge {
|
||||
background: linear-gradient(90deg, #6366f1, #8b5cf6, #ec4899);
|
||||
color: white;
|
||||
padding: 0.4rem 1rem;
|
||||
border-radius: 99px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
display: inline-block;
|
||||
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 3.5rem;
|
||||
line-height: 1.1;
|
||||
font-weight: 800;
|
||||
color: var(--sokuree-text-main);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.gradient-text {
|
||||
background: linear-gradient(135deg, #2563eb, #7c3aed);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.hero-description {
|
||||
font-size: 1.15rem;
|
||||
color: var(--sokuree-text-muted);
|
||||
line-height: 1.6;
|
||||
margin-bottom: 2.5rem;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.hero-actions .primary-btn {
|
||||
background: #2563eb;
|
||||
color: white;
|
||||
padding: 1rem 2rem;
|
||||
border-radius: 12px;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: 0 10px 20px rgba(37, 99, 235, 0.2);
|
||||
}
|
||||
|
||||
.hero-actions .primary-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 15px 30px rgba(37, 99, 235, 0.3);
|
||||
background: #1d4ed8;
|
||||
}
|
||||
|
||||
/* Hero Visual */
|
||||
.hero-visual {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.visual-core {
|
||||
width: 280px;
|
||||
height: 280px;
|
||||
background: white;
|
||||
border-radius: 40px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
box-shadow: 0 25px 60px rgba(0, 0, 0, 0.08), inset 0 0 40px rgba(255, 255, 255, 0.8);
|
||||
transform: rotate(-10deg);
|
||||
}
|
||||
|
||||
.core-icon {
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.core-glow {
|
||||
position: absolute;
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
background: radial-gradient(circle, rgba(37, 99, 235, 0.15) 0%, transparent 70%);
|
||||
border-radius: 50%;
|
||||
z-index: 1;
|
||||
filter: blur(20px);
|
||||
}
|
||||
|
||||
.floating-card {
|
||||
position: absolute;
|
||||
background: white;
|
||||
padding: 0.75rem 1.25rem;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.05);
|
||||
z-index: 3;
|
||||
font-weight: 600;
|
||||
font-size: 0.85rem;
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.floating-card.c1 {
|
||||
top: -20px;
|
||||
right: 40px;
|
||||
animation: float 4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.floating-card.c2 {
|
||||
bottom: 20px;
|
||||
left: 40px;
|
||||
animation: float 5s ease-in-out infinite reverse;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateY(-15px);
|
||||
}
|
||||
}
|
||||
|
||||
/* Features Grid */
|
||||
.features-grid {
|
||||
display: grid;
|
||||
grid-template-cols: repeat(4, 1fr);
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
padding: 2rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.feature-card:hover {
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
|
||||
.feature-icon-box {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: var(--sokuree-bg-light);
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.feature-title {
|
||||
font-size: 1.15rem;
|
||||
font-weight: 700;
|
||||
color: var(--sokuree-text-main);
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.feature-desc {
|
||||
font-size: 0.9rem;
|
||||
color: var(--sokuree-text-muted);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Quick Access */
|
||||
.quick-access {
|
||||
background: var(--sokuree-bg-light);
|
||||
padding: 3rem;
|
||||
border-radius: 24px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 800;
|
||||
color: var(--sokuree-text-main);
|
||||
margin-bottom: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.access-cards {
|
||||
display: grid;
|
||||
grid-template-cols: repeat(3, 1fr);
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.access-card {
|
||||
background: white;
|
||||
padding: 1.75rem;
|
||||
border-radius: 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.access-card:hover {
|
||||
border-color: #2563eb;
|
||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.access-card:hover .access-arrow {
|
||||
transform: translateX(4px);
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.access-info h4 {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.access-info p {
|
||||
font-size: 0.85rem;
|
||||
color: var(--sokuree-text-muted);
|
||||
}
|
||||
|
||||
.access-arrow {
|
||||
color: #cbd5e1;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.features-grid {
|
||||
grid-template-cols: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.hero-section {
|
||||
grid-template-cols: 1fr;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.hero-visual {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.access-cards {
|
||||
grid-template-cols: 1fr;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
}
|
||||
118
src/platform/pages/LandingPage.tsx
Normal file
118
src/platform/pages/LandingPage.tsx
Normal file
@ -0,0 +1,118 @@
|
||||
import { Card } from '../../shared/ui/Card';
|
||||
import { Box, CheckCircle2, Shield, Zap, BarChart3, Globe, ArrowRight } from 'lucide-react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import './LandingPage.css';
|
||||
|
||||
export function LandingPage() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const features = [
|
||||
{
|
||||
icon: <Shield className="text-blue-500" size={24} />,
|
||||
title: "보안 인프라 관리",
|
||||
description: "강력한 암호화 엔진과 권한 계층 구조를 통해 기업의 핵심 자산을 안전하게 보호합니다."
|
||||
},
|
||||
{
|
||||
icon: <Zap className="text-amber-500" size={24} />,
|
||||
title: "실시간 모니터링",
|
||||
description: "CCTV 연동 및 자산 상태 추적을 통해 현장의 모든 상황을 실시간으로 파악합니다."
|
||||
},
|
||||
{
|
||||
icon: <BarChart3 className="text-emerald-500" size={24} />,
|
||||
title: "데이터 시각화",
|
||||
description: "복잡한 자산 및 정비 이력을 직관적인 대시보드와 보고서로 변환하여 의사결정을 지원합니다."
|
||||
},
|
||||
{
|
||||
icon: <Globe className="text-indigo-500" size={24} />,
|
||||
title: "통합 제어 플랫폼",
|
||||
description: "자산, 생산, 보안을 하나의 플랫폼에서 통합 제어하여 운영 효율성을 극대화합니다."
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="landing-container animate-fade-in">
|
||||
{/* Hero Section */}
|
||||
<div className="hero-section">
|
||||
<div className="hero-content">
|
||||
<div className="badge-wrapper">
|
||||
<span className="premium-badge">Next-Gen Agentic Platform</span>
|
||||
</div>
|
||||
<h1 className="hero-title">
|
||||
SMART IMS <br />
|
||||
<span className="gradient-text">Intelligent Management System</span>
|
||||
</h1>
|
||||
<p className="hero-description">
|
||||
차세대 지능형 통합 관리 솔루션 Smart IMS에 오신 것을 환영합니다. <br />
|
||||
우리는 데이터 중심의 스마트한 의무 자격 및 자산 관리 환경을 제공합니다.
|
||||
</p>
|
||||
<div className="hero-actions">
|
||||
<button className="primary-btn" onClick={() => navigate('/asset/dashboard')}>
|
||||
자산 대시보드 바로가기 <ArrowRight size={18} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="hero-visual">
|
||||
<div className="floating-card c1">
|
||||
<CheckCircle2 className="text-green-500" size={20} />
|
||||
<span>System Online</span>
|
||||
</div>
|
||||
<div className="floating-card c2">
|
||||
<Zap className="text-amber-500" size={20} />
|
||||
<span>Real-time Sync</span>
|
||||
</div>
|
||||
<div className="visual-core">
|
||||
<Box size={120} className="core-icon" />
|
||||
<div className="core-glow"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Features Grid */}
|
||||
<div className="features-grid">
|
||||
{features.map((feature, idx) => (
|
||||
<Card key={idx} className="feature-card">
|
||||
<div className="feature-icon-box">
|
||||
{feature.icon}
|
||||
</div>
|
||||
<h3 className="feature-title">{feature.title}</h3>
|
||||
<p className="feature-desc">{feature.description}</p>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Quick Access Section */}
|
||||
<div className="quick-access">
|
||||
<h2 className="section-title">주요 서비스 바로가기</h2>
|
||||
<div className="access-cards">
|
||||
<div className="access-card" onClick={() => navigate('/asset/list')}>
|
||||
<div className="access-info">
|
||||
<h4>자산 인벤토리</h4>
|
||||
<p>전체 자산 현황 조회 및 등록</p>
|
||||
</div>
|
||||
<div className="access-arrow">
|
||||
<ArrowRight size={20} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="access-card" onClick={() => navigate('/cctv/monitoring')}>
|
||||
<div className="access-info">
|
||||
<h4>CCTV 모니터링</h4>
|
||||
<p>실시간 스트리밍 및 보안 감시</p>
|
||||
</div>
|
||||
<div className="access-arrow">
|
||||
<ArrowRight size={20} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="access-card" onClick={() => navigate('/production/dashboard')}>
|
||||
<div className="access-info">
|
||||
<h4>생산 현황</h4>
|
||||
<p>실시간 생산실적 및 가동률 확인</p>
|
||||
</div>
|
||||
<div className="access-arrow">
|
||||
<ArrowRight size={20} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -96,4 +96,21 @@ button {
|
||||
border-radius: var(--sokuree-radius-md);
|
||||
padding: var(--sokuree-spacing-md);
|
||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
/* Utility Animations */
|
||||
.animate-fade-in {
|
||||
animation: fadeIn 0.6s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
@ -2,7 +2,7 @@ import { useState } from 'react';
|
||||
import { Outlet, Link, useLocation } from 'react-router-dom';
|
||||
import { useAuth } from '../../shared/auth/AuthContext';
|
||||
import { useSystem } from '../../shared/context/SystemContext';
|
||||
import { Settings, LogOut, Box, ChevronDown, ChevronRight, Layers, User as UserIcon, Video, Shield, Info } from 'lucide-react';
|
||||
import { Settings, LogOut, Box, ChevronDown, ChevronRight, Layers, User as UserIcon, Video, Shield, Info, Home, UserCog } from 'lucide-react';
|
||||
import type { IModuleDefinition } from '../../core/types';
|
||||
import './MainLayout.css';
|
||||
|
||||
@ -46,7 +46,15 @@ export function MainLayout({ modulesList }: MainLayoutProps) {
|
||||
</div>
|
||||
|
||||
<nav className="sidebar-nav">
|
||||
{/* Module: System Management (Platform Core) */}
|
||||
{/* Platform Home */}
|
||||
<div className="module-group mb-4">
|
||||
<Link to="/home" className={`nav-item ${location.pathname === '/home' ? 'active' : ''}`}>
|
||||
<Home size={18} />
|
||||
<span>플랫폼 홈</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* System Management (Platform Core) */}
|
||||
{isAdmin && (
|
||||
<div className="module-group">
|
||||
<button
|
||||
@ -181,6 +189,16 @@ export function MainLayout({ modulesList }: MainLayoutProps) {
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
<div className="sidebar-divider my-4 border-t border-slate-100 opacity-50"></div>
|
||||
|
||||
{/* User Preferences - Accessible to all roles */}
|
||||
<div className="module-group">
|
||||
<Link to="/preferences" className={`nav-item ${location.pathname === '/preferences' ? 'active' : ''}`}>
|
||||
<UserCog size={18} />
|
||||
<span>기본 설정</span>
|
||||
</Link>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div className="sidebar-footer">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user