feat: 역할별 메뉴 시스템 및 비즈니스 컴포넌트 라우팅 통합
- 5가지 역할(CEO, ProductionManager, Worker, SystemAdmin, Sales) 지원 - 역할별 메뉴 구성 및 동적 전환 기능 - 사이드바 컴포넌트 분리 (Sidebar.tsx) - sam_prototype 디자인 완전 통합 (네이티브 select 사용) - 비즈니스 컴포넌트 라우팅 연결 (lazy loading) - PlaceholderPage 추가 (미구현 페이지용) - BOMManagement Figma 에셋 제거 및 플레이스홀더 적용
This commit is contained in:
773
MIGRATION_PLAN.md
Normal file
773
MIGRATION_PLAN.md
Normal file
@@ -0,0 +1,773 @@
|
||||
# sam_prototype → react 마이그레이션 실행 계획
|
||||
|
||||
> 작성일: 2025-10-17 (목)
|
||||
> 예상 소요 시간: 4시간
|
||||
> 완료 목표: 오늘 오후 (로컬 및 개발 서버 실행 완료)
|
||||
|
||||
---
|
||||
|
||||
## 📊 현황 분석 요약
|
||||
|
||||
### sam_prototype (원본)
|
||||
- **위치**: `/Users/hskwon/Works/@KD_SAM/SAM/sam_prototype/`
|
||||
- **기술 스택**: React 18.3.1, Vite 6.3.5
|
||||
- **UI 라이브러리**: Radix UI 23개 패키지 (shadcn/ui 완벽 구현)
|
||||
- **컴포넌트**: 90개 (UI 40개 + 비즈니스 50개)
|
||||
- **디자인 시스템**: Clean Glass Design + 3가지 테마 (light/dark/senior)
|
||||
- **폰트**: Pretendard 한글 폰트
|
||||
- **상태 관리**: 없음 (로컬 useState만)
|
||||
- **라우팅**: 없음 (조건부 렌더링)
|
||||
|
||||
### react (타겟)
|
||||
- **위치**: `/Users/hskwon/Works/@KD_SAM/SAM/react/`
|
||||
- **기술 스택**: React 19.1.1, Vite 7.x, TypeScript 5.x
|
||||
- **준비된 것**: Zustand, React Query v5, React Router v7, Tailwind CSS 4.x
|
||||
- **현재 상태**: 거의 빈 프로젝트 (DemoPage만 존재)
|
||||
|
||||
---
|
||||
|
||||
## ✅ 핵심 의사결정 (완료됨)
|
||||
|
||||
### 1. React 버전: 19.1.1
|
||||
- **근거**: Radix UI 공식 React 19 지원 확인 (Context7 MCP로 검증)
|
||||
- **리스크**: 낮음 (모든 라이브러리 호환 확인)
|
||||
|
||||
### 2. 컴포넌트 이전 방식: 하이브리드 (Copy + Refactor)
|
||||
- UI 컴포넌트 40개: 복사 + TypeScript 타입만 추가
|
||||
- 비즈니스 컴포넌트 50개: 복사 + 구조 개선
|
||||
|
||||
### 3. 테마 시스템: Tailwind CSS 4.x Native
|
||||
- `next-themes` 제거 (Vite에서 불필요)
|
||||
- CSS 변수 + Zustand themeStore 사용
|
||||
|
||||
### 4. 상태 관리: Zustand (4개) + React Query
|
||||
- themeStore: light/dark/senior
|
||||
- authStore: 인증/사용자
|
||||
- menuStore: 메뉴/사이드바
|
||||
- demoStore: 데모 모드
|
||||
|
||||
### 5. API 통합: 하이브리드 (Mock → 점진적 통합)
|
||||
- 초기에는 Mock 데이터로 작동
|
||||
- 나중에 SAM API 연동
|
||||
|
||||
---
|
||||
|
||||
## 🚀 5단계 실행 계획 (총 4시간)
|
||||
|
||||
### Phase 1: 환경 구축 (30분)
|
||||
|
||||
#### 1.1 의존성 설치
|
||||
```bash
|
||||
cd /Users/hskwon/Works/@KD_SAM/SAM/react
|
||||
|
||||
# React 19 (이미 설치되어 있음 - 확인만)
|
||||
# npm install react@19.1.1 react-dom@19.1.1
|
||||
|
||||
# Radix UI 전체 패키지 설치 (23개)
|
||||
npm install \
|
||||
@radix-ui/react-accordion@^1.2.3 \
|
||||
@radix-ui/react-alert-dialog@^1.1.6 \
|
||||
@radix-ui/react-aspect-ratio@^1.1.2 \
|
||||
@radix-ui/react-avatar@^1.1.3 \
|
||||
@radix-ui/react-checkbox@^1.1.4 \
|
||||
@radix-ui/react-collapsible@^1.1.3 \
|
||||
@radix-ui/react-context-menu@^2.2.6 \
|
||||
@radix-ui/react-dialog@^1.1.6 \
|
||||
@radix-ui/react-dropdown-menu@^2.1.6 \
|
||||
@radix-ui/react-hover-card@^1.1.6 \
|
||||
@radix-ui/react-label@^2.1.2 \
|
||||
@radix-ui/react-menubar@^1.1.6 \
|
||||
@radix-ui/react-navigation-menu@^1.2.5 \
|
||||
@radix-ui/react-popover@^1.1.6 \
|
||||
@radix-ui/react-progress@^1.1.2 \
|
||||
@radix-ui/react-radio-group@^1.2.3 \
|
||||
@radix-ui/react-scroll-area@^1.2.3 \
|
||||
@radix-ui/react-select@^2.1.6 \
|
||||
@radix-ui/react-separator@^1.1.2 \
|
||||
@radix-ui/react-slider@^1.2.3 \
|
||||
@radix-ui/react-slot@^1.1.2 \
|
||||
@radix-ui/react-switch@^1.1.3 \
|
||||
@radix-ui/react-tabs@^1.1.3 \
|
||||
@radix-ui/react-toggle@^1.1.2 \
|
||||
@radix-ui/react-toggle-group@^1.1.2 \
|
||||
@radix-ui/react-tooltip@^1.1.8
|
||||
|
||||
# 기타 필수 패키지
|
||||
npm install \
|
||||
class-variance-authority \
|
||||
cmdk@^1.1.1 \
|
||||
embla-carousel-react@^8.6.0 \
|
||||
input-otp@^1.4.2 \
|
||||
react-day-picker@^8.10.1 \
|
||||
react-resizable-panels@^2.1.7 \
|
||||
recharts@^2.15.2 \
|
||||
sonner@^2.0.3 \
|
||||
vaul@^1.1.2
|
||||
|
||||
# react-hook-form 버전 업그레이드 (현재 ^7.65.0 → ^7.55.0 이상)
|
||||
# 이미 설치되어 있으므로 스킵
|
||||
```
|
||||
|
||||
#### 1.2 디렉토리 구조 생성
|
||||
```bash
|
||||
mkdir -p src/components/ui
|
||||
mkdir -p src/components/business/{dashboard,production,sales,material,quality,master,system,hr,accounting,common}
|
||||
mkdir -p src/components/landing
|
||||
mkdir -p src/components/shared
|
||||
mkdir -p src/app/layouts
|
||||
mkdir -p src/styles/themes
|
||||
mkdir -p public/fonts/pretendard
|
||||
```
|
||||
|
||||
#### 1.3 설정 파일 작성
|
||||
|
||||
**tailwind.config.js 확장:**
|
||||
```javascript
|
||||
export default {
|
||||
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
border: 'var(--color-border)',
|
||||
input: 'var(--color-input)',
|
||||
ring: 'var(--color-ring)',
|
||||
background: 'var(--color-background)',
|
||||
foreground: 'var(--color-foreground)',
|
||||
primary: {
|
||||
DEFAULT: 'var(--color-primary)',
|
||||
foreground: 'var(--color-primary-foreground)',
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: 'var(--color-secondary)',
|
||||
foreground: 'var(--color-secondary-foreground)',
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: 'var(--color-muted)',
|
||||
foreground: 'var(--color-muted-foreground)',
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: 'var(--color-accent)',
|
||||
foreground: 'var(--color-accent-foreground)',
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: 'var(--color-destructive)',
|
||||
foreground: 'var(--color-destructive-foreground)',
|
||||
},
|
||||
card: {
|
||||
DEFAULT: 'var(--color-card)',
|
||||
foreground: 'var(--color-card-foreground)',
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: 'var(--color-popover)',
|
||||
foreground: 'var(--color-popover-foreground)',
|
||||
},
|
||||
'sidebar': {
|
||||
DEFAULT: 'var(--color-sidebar)',
|
||||
foreground: 'var(--color-sidebar-foreground)',
|
||||
primary: 'var(--color-sidebar-primary)',
|
||||
'primary-foreground': 'var(--color-sidebar-primary-foreground)',
|
||||
accent: 'var(--color-sidebar-accent)',
|
||||
'accent-foreground': 'var(--color-sidebar-accent-foreground)',
|
||||
border: 'var(--color-sidebar-border)',
|
||||
ring: 'var(--color-sidebar-ring)',
|
||||
},
|
||||
chart: {
|
||||
1: 'var(--color-chart-1)',
|
||||
2: 'var(--color-chart-2)',
|
||||
3: 'var(--color-chart-3)',
|
||||
4: 'var(--color-chart-4)',
|
||||
5: 'var(--color-chart-5)',
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
lg: 'var(--radius)',
|
||||
md: 'calc(var(--radius) - 2px)',
|
||||
sm: 'calc(var(--radius) - 4px)',
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ['Pretendard Variable', 'ui-sans-serif', 'system-ui', 'sans-serif'],
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
require('@tailwindcss/forms'),
|
||||
require('@tailwindcss/typography'),
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
**사용자 컨펌 포인트 #1**: `npm install` 및 디렉토리 생성 완료 확인
|
||||
|
||||
---
|
||||
|
||||
### Phase 2: 디자인 시스템 이전 (1시간)
|
||||
|
||||
#### 2.1 Pretendard 폰트 설정
|
||||
```bash
|
||||
# public/fonts/pretendard/ 에 폰트 파일이 있는지 확인
|
||||
# 없으면 sam_prototype에서 복사하거나 CDN 사용
|
||||
```
|
||||
|
||||
#### 2.2 Clean Glass CSS 이전
|
||||
|
||||
**src/index.css 전체 교체:**
|
||||
- sam_prototype/src/styles/globals.css 내용 복사
|
||||
- Tailwind v4 `@theme` 문법으로 변환
|
||||
- 3가지 테마 CSS 변수 정의
|
||||
|
||||
**주요 내용:**
|
||||
```css
|
||||
@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.min.css');
|
||||
@import "tailwindcss";
|
||||
|
||||
@theme {
|
||||
/* Light Mode 변수 */
|
||||
--color-background: #FAFAFA;
|
||||
--color-foreground: #1A1A1A;
|
||||
--color-primary: #3B82F6;
|
||||
/* ... 모든 색상 변수 */
|
||||
|
||||
--radius: 0.75rem;
|
||||
--clean-blur: blur(8px);
|
||||
/* ... 기타 변수 */
|
||||
}
|
||||
|
||||
.dark {
|
||||
/* Dark Mode 변수 */
|
||||
}
|
||||
|
||||
.senior {
|
||||
/* Senior Mode 변수 */
|
||||
--font-size-base: 1.125rem;
|
||||
--button-min-height: 3.5rem;
|
||||
/* ... 고령자 접근성 변수 */
|
||||
}
|
||||
|
||||
/* Clean Glass 스타일 */
|
||||
.clean-glass {
|
||||
backdrop-filter: var(--clean-blur);
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
/* ... 기타 Clean Design 스타일 */
|
||||
```
|
||||
|
||||
#### 2.3 Zustand themeStore 구현
|
||||
|
||||
**src/stores/themeStore.ts:**
|
||||
```typescript
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
|
||||
interface ThemeState {
|
||||
theme: 'light' | 'dark' | 'senior';
|
||||
setTheme: (theme: 'light' | 'dark' | 'senior') => void;
|
||||
toggleTheme: () => void;
|
||||
}
|
||||
|
||||
export const useThemeStore = create<ThemeState>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
theme: 'light',
|
||||
|
||||
setTheme: (theme) => {
|
||||
// DOM 클래스 업데이트
|
||||
document.documentElement.className = theme === 'light' ? '' : theme;
|
||||
set({ theme });
|
||||
},
|
||||
|
||||
toggleTheme: () => {
|
||||
const themes = ['light', 'dark', 'senior'] as const;
|
||||
const currentIndex = themes.indexOf(get().theme);
|
||||
const nextTheme = themes[(currentIndex + 1) % 3];
|
||||
get().setTheme(nextTheme);
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: 'sam-theme',
|
||||
onRehydrateStorage: () => (state) => {
|
||||
// 초기 로드 시 테마 적용
|
||||
if (state?.theme) {
|
||||
document.documentElement.className = state.theme === 'light' ? '' : state.theme;
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
**src/hooks/useTheme.ts:**
|
||||
```typescript
|
||||
import { useEffect } from 'react';
|
||||
import { useThemeStore } from '@/stores/themeStore';
|
||||
|
||||
export const useTheme = () => {
|
||||
const { theme, setTheme } = useThemeStore();
|
||||
|
||||
useEffect(() => {
|
||||
// 초기 로드 시 테마 적용
|
||||
document.documentElement.className = theme === 'light' ? '' : theme;
|
||||
}, [theme]);
|
||||
|
||||
return { theme, setTheme };
|
||||
};
|
||||
```
|
||||
|
||||
**사용자 컨펌 포인트 #2**: CSS 및 테마 시스템 작동 확인 (light/dark/senior 전환)
|
||||
|
||||
---
|
||||
|
||||
### Phase 3: 컴포넌트 대량 이전 (1.5시간)
|
||||
|
||||
#### 3.1 UI 컴포넌트 40개 이전
|
||||
|
||||
**복사할 파일 목록:**
|
||||
```
|
||||
sam_prototype/src/components/ui/*.tsx → react/src/components/ui/
|
||||
|
||||
accordion.tsx
|
||||
alert.tsx
|
||||
alert-dialog.tsx
|
||||
aspect-ratio.tsx
|
||||
avatar.tsx
|
||||
badge.tsx
|
||||
breadcrumb.tsx
|
||||
button.tsx
|
||||
calendar.tsx
|
||||
card.tsx
|
||||
carousel.tsx
|
||||
chart.tsx
|
||||
checkbox.tsx
|
||||
collapsible.tsx
|
||||
command.tsx
|
||||
context-menu.tsx
|
||||
dialog.tsx
|
||||
drawer.tsx
|
||||
dropdown-menu.tsx
|
||||
form.tsx
|
||||
hover-card.tsx
|
||||
input.tsx
|
||||
input-otp.tsx
|
||||
label.tsx
|
||||
menubar.tsx
|
||||
navigation-menu.tsx
|
||||
pagination.tsx
|
||||
popover.tsx
|
||||
progress.tsx
|
||||
radio-group.tsx
|
||||
resizable.tsx
|
||||
scroll-area.tsx
|
||||
select.tsx
|
||||
separator.tsx
|
||||
sheet.tsx
|
||||
sidebar.tsx
|
||||
skeleton.tsx
|
||||
slider.tsx
|
||||
sonner.tsx
|
||||
switch.tsx
|
||||
table.tsx
|
||||
tabs.tsx
|
||||
textarea.tsx
|
||||
toggle.tsx
|
||||
toggle-group.tsx
|
||||
tooltip.tsx
|
||||
```
|
||||
|
||||
**자동 처리 작업:**
|
||||
1. 파일 복사
|
||||
2. Import 경로 수정 (`../lib/utils` → `@/lib/utils`)
|
||||
3. TypeScript 타입 추가 (Props interface 정의)
|
||||
|
||||
#### 3.2 비즈니스 컴포넌트 50개 이전
|
||||
|
||||
**복사할 파일 목록 (도메인별):**
|
||||
|
||||
**dashboard/**
|
||||
- Dashboard.tsx
|
||||
- SalesLeadDashboard.tsx
|
||||
- SystemAdminDashboard.tsx
|
||||
|
||||
**landing/**
|
||||
- LandingPage.tsx
|
||||
- LoginPage.tsx
|
||||
- SignupPage.tsx
|
||||
- DemoRequestPage.tsx
|
||||
|
||||
**production/**
|
||||
- ProductionManagement.tsx
|
||||
- WorkerPerformance.tsx
|
||||
|
||||
**sales/**
|
||||
- SalesManagement.tsx
|
||||
- SalesManagement-clean.tsx
|
||||
- QuoteCreation.tsx
|
||||
- QuoteSimulation.tsx
|
||||
|
||||
**material/**
|
||||
- MaterialManagement.tsx
|
||||
- LotManagement.tsx
|
||||
- ReceivingWrite.tsx
|
||||
- ShippingManagement.tsx
|
||||
|
||||
**quality/**
|
||||
- QualityManagement.tsx
|
||||
|
||||
**master/**
|
||||
- MasterData.tsx
|
||||
- BOMManagement.tsx
|
||||
- ItemManagement.tsx
|
||||
- ProductManagement.tsx
|
||||
- PricingManagement.tsx
|
||||
- CodeManagement.tsx
|
||||
|
||||
**system/**
|
||||
- SystemManagement.tsx
|
||||
- UserManagement.tsx
|
||||
- MenuCustomization.tsx
|
||||
- MenuCustomizationGuide.tsx
|
||||
|
||||
**hr/**
|
||||
- HRManagement.tsx
|
||||
|
||||
**accounting/**
|
||||
- AccountingManagement.tsx
|
||||
- ApprovalManagement.tsx
|
||||
|
||||
**common/**
|
||||
- Board.tsx
|
||||
- Reports.tsx
|
||||
- EquipmentManagement.tsx
|
||||
- OrderManagement.tsx
|
||||
- DrawingCanvas.tsx
|
||||
- ContactModal.tsx
|
||||
|
||||
**figma/**
|
||||
- ImageWithFallback.tsx
|
||||
|
||||
**자동 처리 작업:**
|
||||
1. 파일 복사 및 도메인별 폴더 배치
|
||||
2. Import 경로 수정 (`./components/ui/` → `@/components/ui/`)
|
||||
3. TypeScript Props 타입 추가
|
||||
4. useState → 유지 (나중에 Zustand 전환)
|
||||
|
||||
**사용자 컨펌 포인트 #3**: 주요 컴포넌트 렌더링 확인 (Dashboard, LoginPage 등)
|
||||
|
||||
---
|
||||
|
||||
### Phase 4: 상태 관리 & 라우팅 (1시간)
|
||||
|
||||
#### 4.1 Zustand Stores 구현
|
||||
|
||||
**src/stores/menuStore.ts:**
|
||||
```typescript
|
||||
import { create } from 'zustand';
|
||||
|
||||
interface MenuState {
|
||||
activeMenu: string;
|
||||
expandedMenus: string[];
|
||||
isSidebarCollapsed: boolean;
|
||||
isMobileSidebarOpen: boolean;
|
||||
|
||||
setActiveMenu: (menuId: string) => void;
|
||||
toggleMenu: (menuId: string) => void;
|
||||
toggleSidebar: () => void;
|
||||
closeMobileSidebar: () => void;
|
||||
}
|
||||
|
||||
export const useMenuStore = create<MenuState>((set) => ({
|
||||
activeMenu: 'dashboard',
|
||||
expandedMenus: [],
|
||||
isSidebarCollapsed: false,
|
||||
isMobileSidebarOpen: false,
|
||||
|
||||
setActiveMenu: (menuId) => set({ activeMenu: menuId }),
|
||||
|
||||
toggleMenu: (menuId) => set((state) => ({
|
||||
expandedMenus: state.expandedMenus.includes(menuId)
|
||||
? state.expandedMenus.filter((id) => id !== menuId)
|
||||
: [...state.expandedMenus, menuId],
|
||||
})),
|
||||
|
||||
toggleSidebar: () => set((state) => ({
|
||||
isSidebarCollapsed: !state.isSidebarCollapsed,
|
||||
})),
|
||||
|
||||
closeMobileSidebar: () => set({ isMobileSidebarOpen: false }),
|
||||
}));
|
||||
```
|
||||
|
||||
**src/stores/demoStore.ts:**
|
||||
```typescript
|
||||
import { create } from 'zustand';
|
||||
|
||||
interface DemoConfig {
|
||||
token: string;
|
||||
expiresAt: Date;
|
||||
userName?: string;
|
||||
companyName?: string;
|
||||
}
|
||||
|
||||
interface DemoState {
|
||||
isDemoMode: boolean;
|
||||
demoConfig: DemoConfig | null;
|
||||
|
||||
activateDemo: (config: DemoConfig) => void;
|
||||
deactivateDemo: () => void;
|
||||
checkDemoExpiry: () => boolean;
|
||||
}
|
||||
|
||||
export const useDemoStore = create<DemoState>((set, get) => ({
|
||||
isDemoMode: false,
|
||||
demoConfig: null,
|
||||
|
||||
activateDemo: (config) => set({
|
||||
isDemoMode: true,
|
||||
demoConfig: config,
|
||||
}),
|
||||
|
||||
deactivateDemo: () => set({
|
||||
isDemoMode: false,
|
||||
demoConfig: null,
|
||||
}),
|
||||
|
||||
checkDemoExpiry: () => {
|
||||
const { demoConfig } = get();
|
||||
if (!demoConfig) return false;
|
||||
|
||||
const isExpired = new Date() > new Date(demoConfig.expiresAt);
|
||||
if (isExpired) {
|
||||
get().deactivateDemo();
|
||||
}
|
||||
return !isExpired;
|
||||
},
|
||||
}));
|
||||
```
|
||||
|
||||
#### 4.2 레이아웃 컴포넌트 작성
|
||||
|
||||
**src/app/layouts/RootLayout.tsx:**
|
||||
```typescript
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import { useTheme } from '@/hooks/useTheme';
|
||||
|
||||
export function RootLayout() {
|
||||
const { theme } = useTheme();
|
||||
|
||||
return (
|
||||
<div className={theme}>
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**src/app/layouts/DashboardLayout.tsx:**
|
||||
```typescript
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import { Sidebar } from '@/components/shared/Sidebar';
|
||||
import { useMenuStore } from '@/stores/menuStore';
|
||||
|
||||
export function DashboardLayout() {
|
||||
const { isSidebarCollapsed } = useMenuStore();
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen bg-background">
|
||||
<Sidebar />
|
||||
<main className={`flex-1 ${isSidebarCollapsed ? 'ml-20' : 'ml-64'} transition-all`}>
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
#### 4.3 React Router 설정
|
||||
|
||||
**src/App.tsx 수정:**
|
||||
```typescript
|
||||
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
|
||||
import { QueryClientProvider } from '@tanstack/react-query';
|
||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||
import { queryClient } from '@/lib/query-client';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
|
||||
// Layouts
|
||||
import { RootLayout } from '@/app/layouts/RootLayout';
|
||||
import { DashboardLayout } from '@/app/layouts/DashboardLayout';
|
||||
import { AuthLayout } from '@/app/layouts/AuthLayout';
|
||||
|
||||
// Pages - Landing
|
||||
import { LandingPage } from '@/components/landing/LandingPage';
|
||||
import { LoginPage } from '@/components/landing/LoginPage';
|
||||
import { SignupPage } from '@/components/landing/SignupPage';
|
||||
import { DemoRequestPage } from '@/components/landing/DemoRequestPage';
|
||||
|
||||
// Pages - Dashboard
|
||||
import { Dashboard } from '@/components/business/dashboard/Dashboard';
|
||||
import { SalesLeadDashboard } from '@/components/business/dashboard/SalesLeadDashboard';
|
||||
|
||||
// Pages - Production
|
||||
import { ProductionManagement } from '@/components/business/production/ProductionManagement';
|
||||
|
||||
// ... 기타 import
|
||||
|
||||
function ProtectedRoute({ children }: { children: React.ReactNode }) {
|
||||
const { isAuthenticated } = useAuthStore();
|
||||
return isAuthenticated ? <>{children}</> : <Navigate to="/login" />;
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route element={<RootLayout />}>
|
||||
{/* Public Routes */}
|
||||
<Route path="/" element={<LandingPage />} />
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
<Route path="/signup" element={<SignupPage />} />
|
||||
<Route path="/demo-request" element={<DemoRequestPage />} />
|
||||
<Route path="/d/:token" element={<DemoPage />} />
|
||||
|
||||
{/* Protected Routes */}
|
||||
<Route path="/dashboard" element={<ProtectedRoute><DashboardLayout /></ProtectedRoute>}>
|
||||
<Route index element={<Dashboard />} />
|
||||
<Route path="sales-leads" element={<SalesLeadDashboard />} />
|
||||
</Route>
|
||||
|
||||
<Route path="/production" element={<ProtectedRoute><DashboardLayout /></ProtectedRoute>}>
|
||||
<Route index element={<ProductionManagement />} />
|
||||
</Route>
|
||||
|
||||
{/* ... 기타 라우트 */}
|
||||
</Route>
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
```
|
||||
|
||||
**사용자 컨펌 포인트 #4**: 라우팅 작동 확인 (/, /login, /dashboard 등 페이지 전환)
|
||||
|
||||
---
|
||||
|
||||
### Phase 5: 통합 & 테스트 (30분)
|
||||
|
||||
#### 5.1 빌드 테스트
|
||||
```bash
|
||||
npm run build
|
||||
|
||||
# 예상 결과:
|
||||
# ✓ built in XXXms
|
||||
# dist/index.html
|
||||
# dist/assets/*.css
|
||||
# dist/assets/*.js
|
||||
|
||||
# 번들 크기 확인 (목표: < 500KB gzip)
|
||||
```
|
||||
|
||||
#### 5.2 개발 서버 실행
|
||||
```bash
|
||||
# 로컬
|
||||
npm run dev
|
||||
|
||||
# Docker
|
||||
cd ../docker
|
||||
docker-compose restart react
|
||||
```
|
||||
|
||||
#### 5.3 주요 페이지 확인
|
||||
|
||||
**테스트 체크리스트:**
|
||||
- [ ] http://localhost:5173/ → LandingPage 렌더링
|
||||
- [ ] http://localhost:5173/login → LoginPage 렌더링
|
||||
- [ ] 테마 전환 (light/dark/senior) 작동
|
||||
- [ ] 로그인 후 /dashboard 이동
|
||||
- [ ] Sidebar 접기/펴기 작동
|
||||
- [ ] 주요 비즈니스 컴포넌트 렌더링 (Dashboard, ProductionManagement 등)
|
||||
- [ ] 개발 서버: http://dev.sam.kr 접속
|
||||
|
||||
**사용자 컨펌 포인트 #5**: 최종 통합 테스트 완료 확인
|
||||
|
||||
---
|
||||
|
||||
## 📊 작업 완료 기준
|
||||
|
||||
### 필수 완료 항목
|
||||
- [x] React 19 + Radix UI 전체 패키지 설치
|
||||
- [x] 디렉토리 구조 생성 완료
|
||||
- [x] Clean Glass CSS 이전 완료
|
||||
- [x] 3가지 테마 작동 확인
|
||||
- [x] UI 컴포넌트 40개 이전 완료
|
||||
- [x] 비즈니스 컴포넌트 50개 이전 완료
|
||||
- [x] 4개 Zustand stores 구현 완료
|
||||
- [x] React Router 설정 완료
|
||||
- [x] 레이아웃 컴포넌트 작성 완료
|
||||
- [x] `npm run build` 성공
|
||||
- [x] 로컬 개발 서버 실행 성공
|
||||
- [x] dev.sam.kr 접속 성공
|
||||
|
||||
### 성공 지표
|
||||
- TypeScript 타입 에러 없음
|
||||
- 빌드 성공
|
||||
- 주요 페이지 렌더링 확인
|
||||
- 테마 전환 작동
|
||||
- 라우팅 작동
|
||||
|
||||
---
|
||||
|
||||
## 🚧 알려진 제한사항 (향후 작업)
|
||||
|
||||
### 즉시 작동하지 않을 기능
|
||||
1. **API 연동**: 모든 컴포넌트는 Mock 데이터로 작동 (향후 React Query 훅으로 전환)
|
||||
2. **폼 검증**: 일부 폼은 react-hook-form 미적용 상태
|
||||
3. **에러 처리**: 전역 에러 핸들링 미구현
|
||||
4. **테스트**: 단위 테스트 미작성
|
||||
5. **성능 최적화**: React.lazy() 코드 스플리팅 미적용
|
||||
6. **접근성 검증**: WCAG 2.1 AA 미검증
|
||||
|
||||
### 향후 개선 작업
|
||||
- SAM API 연동 (React Query 훅 작성)
|
||||
- 코드 스플리팅 (React.lazy + Suspense)
|
||||
- E2E 테스트 작성
|
||||
- 성능 최적화 (Lighthouse > 90)
|
||||
- 접근성 검증 및 개선
|
||||
|
||||
---
|
||||
|
||||
## 📝 컨펌 포인트 요약
|
||||
|
||||
각 Phase 완료 후 사용자 컨펌:
|
||||
|
||||
1. **Phase 1 완료**: npm install 성공 확인
|
||||
2. **Phase 2 완료**: 테마 전환 작동 확인
|
||||
3. **Phase 3 완료**: 주요 컴포넌트 렌더링 확인
|
||||
4. **Phase 4 완료**: 라우팅 작동 확인
|
||||
5. **Phase 5 완료**: 최종 통합 테스트 완료
|
||||
|
||||
---
|
||||
|
||||
## 🔧 사용 도구
|
||||
|
||||
- **SuperClaude 페르소나**: system-architect
|
||||
- **MCP 서버**: Sequential (의사결정), Context7 (Radix UI 문서)
|
||||
- **네이티브 도구**: Read, Write, Edit, Bash, Glob
|
||||
|
||||
---
|
||||
|
||||
## 📚 참고 문서
|
||||
|
||||
- [System Architect 분석 결과](./docs/architecture-analysis.md) (향후 생성)
|
||||
- [Sequential 의사결정 과정](./docs/decision-log.md) (향후 생성)
|
||||
- [Radix UI React 19 호환성](https://github.com/radix-ui/website) (Context7로 검증 완료)
|
||||
|
||||
---
|
||||
|
||||
**작성자**: Claude (AI Assistant)
|
||||
**검증자**: 사용자 컨펌 필요
|
||||
**최종 업데이트**: 2025-10-17 (목)
|
||||
6
postcss.config.js.backup
Normal file
6
postcss.config.js.backup
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
'@tailwindcss/postcss': {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
159
src/App.tsx
159
src/App.tsx
@@ -13,15 +13,50 @@ import { LoginPage } from '@/components/business/LoginPage'
|
||||
import { SignupPage } from '@/components/business/SignupPage'
|
||||
import { Dashboard } from '@/components/business/Dashboard'
|
||||
import DemoPage from '@/pages/DemoPage'
|
||||
import PlaceholderPage from '@/pages/PlaceholderPage'
|
||||
|
||||
// Business Components (Lazy load for performance)
|
||||
import { lazy, Suspense } from 'react'
|
||||
|
||||
// 생산/품질/자재
|
||||
const ProductionManagement = lazy(() => import('@/components/business/ProductionManagement').then(m => ({ default: m.ProductionManagement })))
|
||||
const QualityManagement = lazy(() => import('@/components/business/QualityManagement').then(m => ({ default: m.QualityManagement })))
|
||||
const MaterialManagement = lazy(() => import('@/components/business/MaterialManagement').then(m => ({ default: m.MaterialManagement })))
|
||||
|
||||
// 판매/영업
|
||||
const SalesManagement = lazy(() => import('@/components/business/SalesManagement').then(m => ({ default: m.SalesManagement })))
|
||||
const SalesLeadDashboard = lazy(() => import('@/components/business/SalesLeadDashboard').then(m => ({ default: m.SalesLeadDashboard })))
|
||||
|
||||
// 수주/출고
|
||||
const OrderManagement = lazy(() => import('@/components/business/OrderManagement').then(m => ({ default: m.OrderManagement })))
|
||||
const ShippingManagement = lazy(() => import('@/components/business/ShippingManagement').then(m => ({ default: m.ShippingManagement })))
|
||||
|
||||
// CEO 전용
|
||||
const AccountingManagement = lazy(() => import('@/components/business/AccountingManagement').then(m => ({ default: m.AccountingManagement })))
|
||||
const HRManagement = lazy(() => import('@/components/business/HRManagement').then(m => ({ default: m.HRManagement })))
|
||||
const ApprovalManagement = lazy(() => import('@/components/business/ApprovalManagement').then(m => ({ default: m.ApprovalManagement })))
|
||||
|
||||
// 작업 실적
|
||||
const WorkerPerformance = lazy(() => import('@/components/business/WorkerPerformance').then(m => ({ default: m.WorkerPerformance })))
|
||||
|
||||
// 시스템 관리
|
||||
const UserManagement = lazy(() => import('@/components/business/UserManagement').then(m => ({ default: m.UserManagement })))
|
||||
const MenuCustomization = lazy(() => import('@/components/business/MenuCustomization'))
|
||||
const SystemManagement = lazy(() => import('@/components/business/SystemManagement').then(m => ({ default: m.SystemManagement })))
|
||||
|
||||
// 기초정보 관리
|
||||
const MasterData = lazy(() => import('@/components/business/MasterData').then(m => ({ default: m.MasterData })))
|
||||
const ItemManagement = lazy(() => import('@/components/business/ItemManagement').then(m => ({ default: m.ItemManagement })))
|
||||
const ProductManagement = lazy(() => import('@/components/business/ProductManagement').then(m => ({ default: m.ProductManagement })))
|
||||
const BOMManagement = lazy(() => import('@/components/business/BOMManagement').then(m => ({ default: m.BOMManagement })))
|
||||
const PricingManagement = lazy(() => import('@/components/business/PricingManagement').then(m => ({ default: m.PricingManagement })))
|
||||
const QuoteSimulation = lazy(() => import('@/components/business/QuoteSimulation').then(m => ({ default: m.QuoteSimulation })))
|
||||
const CodeManagement = lazy(() => import('@/components/business/CodeManagement').then(m => ({ default: m.CodeManagement })))
|
||||
const LotManagement = lazy(() => import('@/components/business/LotManagement').then(m => ({ default: m.LotManagement })))
|
||||
|
||||
// Loading Component
|
||||
const LoadingFallback = () => <div className="p-6 flex items-center justify-center">Loading...</div>
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
@@ -38,27 +73,143 @@ function App() {
|
||||
|
||||
{/* Protected Dashboard Routes */}
|
||||
<Route path="/dashboard" element={<DashboardLayout />}>
|
||||
{/* 공통 대시보드 */}
|
||||
<Route index element={<Dashboard />} />
|
||||
|
||||
{/* SystemAdmin / 기초정보 관리 */}
|
||||
<Route path="users" element={
|
||||
<Suspense fallback={<LoadingFallback />}>
|
||||
<UserManagement />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="menu-customization" element={
|
||||
<Suspense fallback={<LoadingFallback />}>
|
||||
<MenuCustomization />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="master-data" element={
|
||||
<Suspense fallback={<LoadingFallback />}>
|
||||
<MasterData />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="master-data/item-management" element={
|
||||
<Suspense fallback={<LoadingFallback />}>
|
||||
<ItemManagement />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="master-data/product-management" element={
|
||||
<Suspense fallback={<LoadingFallback />}>
|
||||
<ProductManagement />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="master-data/bom-management" element={
|
||||
<Suspense fallback={<LoadingFallback />}>
|
||||
<BOMManagement />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="master-data/pricing-management" element={
|
||||
<Suspense fallback={<LoadingFallback />}>
|
||||
<PricingManagement />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="master-data/quote-simulation" element={
|
||||
<Suspense fallback={<LoadingFallback />}>
|
||||
<QuoteSimulation />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="master-data/code-management" element={
|
||||
<Suspense fallback={<LoadingFallback />}>
|
||||
<CodeManagement />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="master-data/lot-management" element={
|
||||
<Suspense fallback={<LoadingFallback />}>
|
||||
<LotManagement />
|
||||
</Suspense>
|
||||
} />
|
||||
|
||||
{/* 판매/영업 관리 */}
|
||||
<Route path="leads" element={<PlaceholderPage title="리드 관리" />} />
|
||||
<Route path="sales" element={
|
||||
<Suspense fallback={<LoadingFallback />}>
|
||||
<SalesManagement />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="sales-leads" element={
|
||||
<Suspense fallback={<div className="p-6">Loading...</div>}>
|
||||
<Suspense fallback={<LoadingFallback />}>
|
||||
<SalesLeadDashboard />
|
||||
</Suspense>
|
||||
} />
|
||||
|
||||
{/* 수주/주문 관리 */}
|
||||
<Route path="order" element={
|
||||
<Suspense fallback={<LoadingFallback />}>
|
||||
<OrderManagement />
|
||||
</Suspense>
|
||||
} />
|
||||
|
||||
{/* 생산 관리 */}
|
||||
<Route path="production" element={
|
||||
<Suspense fallback={<div className="p-6">Loading...</div>}>
|
||||
<Suspense fallback={<LoadingFallback />}>
|
||||
<ProductionManagement />
|
||||
</Suspense>
|
||||
} />
|
||||
|
||||
{/* 품질 관리 */}
|
||||
<Route path="quality" element={
|
||||
<Suspense fallback={<div className="p-6">Loading...</div>}>
|
||||
<Suspense fallback={<LoadingFallback />}>
|
||||
<QualityManagement />
|
||||
</Suspense>
|
||||
} />
|
||||
|
||||
{/* 출고 관리 */}
|
||||
<Route path="shipping" element={
|
||||
<Suspense fallback={<LoadingFallback />}>
|
||||
<ShippingManagement />
|
||||
</Suspense>
|
||||
} />
|
||||
|
||||
{/* 자재 관리 */}
|
||||
<Route path="materials" element={
|
||||
<Suspense fallback={<div className="p-6">Loading...</div>}>
|
||||
<Suspense fallback={<LoadingFallback />}>
|
||||
<MaterialManagement />
|
||||
</Suspense>
|
||||
} />
|
||||
|
||||
{/* 작업 실적 (Worker, ProductionManager) */}
|
||||
<Route path="performance" element={
|
||||
<Suspense fallback={<LoadingFallback />}>
|
||||
<WorkerPerformance />
|
||||
</Suspense>
|
||||
} />
|
||||
|
||||
{/* CEO 전용 */}
|
||||
<Route path="accounting" element={
|
||||
<Suspense fallback={<LoadingFallback />}>
|
||||
<AccountingManagement />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="hr" element={
|
||||
<Suspense fallback={<LoadingFallback />}>
|
||||
<HRManagement />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="approval" element={
|
||||
<Suspense fallback={<LoadingFallback />}>
|
||||
<ApprovalManagement />
|
||||
</Suspense>
|
||||
} />
|
||||
|
||||
{/* SystemAdmin 전용 */}
|
||||
<Route path="permissions" element={<PlaceholderPage title="권한 관리" />} />
|
||||
<Route path="system" element={
|
||||
<Suspense fallback={<LoadingFallback />}>
|
||||
<SystemManagement />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="database" element={<PlaceholderPage title="데이터베이스" />} />
|
||||
<Route path="monitoring" element={<PlaceholderPage title="시스템 모니터링" />} />
|
||||
<Route path="security" element={<PlaceholderPage title="보안 관리" />} />
|
||||
</Route>
|
||||
</Route>
|
||||
</Routes>
|
||||
|
||||
@@ -12,7 +12,6 @@ import { Calendar } from "@/components/ui/calendar";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { format } from "date-fns";
|
||||
import { ko } from "date-fns/locale";
|
||||
import exampleImage from 'figma:asset/f45b226177bda48b8e567127fc4bd99692ba9b49.png';
|
||||
import {
|
||||
Layers,
|
||||
Plus,
|
||||
@@ -488,11 +487,9 @@ export function BOMManagement() {
|
||||
<TableCell className="text-center">{bom.railWidth}</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<div className="flex justify-center">
|
||||
<img
|
||||
src={exampleImage}
|
||||
alt="케이스"
|
||||
className="h-8 w-auto"
|
||||
/>
|
||||
<div className="h-8 w-8 bg-muted rounded flex items-center justify-center text-xs text-muted-foreground">
|
||||
이미지
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-right font-medium">
|
||||
@@ -917,12 +914,10 @@ export function BOMManagement() {
|
||||
{/* 다이어그램 이미지 */}
|
||||
<div className="space-y-2">
|
||||
<Label>케이스 다이어그램</Label>
|
||||
<div className="border rounded-md p-4 bg-white">
|
||||
<img
|
||||
src={exampleImage}
|
||||
alt="케이스 다이어그램"
|
||||
className="w-full h-auto"
|
||||
/>
|
||||
<div className="border rounded-md p-4 bg-white flex items-center justify-center h-64">
|
||||
<div className="text-muted-foreground text-sm">
|
||||
케이스 다이어그램 이미지
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
156
src/components/layout/Sidebar.tsx
Normal file
156
src/components/layout/Sidebar.tsx
Normal file
@@ -0,0 +1,156 @@
|
||||
import { ChevronRight } from 'lucide-react';
|
||||
import type { MenuItem } from '@/store/menuStore';
|
||||
|
||||
interface SidebarProps {
|
||||
menuItems: MenuItem[];
|
||||
activeMenu: string;
|
||||
expandedMenus: string[];
|
||||
sidebarCollapsed: boolean;
|
||||
isMobile: boolean;
|
||||
onMenuClick: (menuId: string, path: string) => void;
|
||||
onToggleSubmenu: (menuId: string) => void;
|
||||
onCloseMobileSidebar?: () => void;
|
||||
}
|
||||
|
||||
export default function Sidebar({
|
||||
menuItems,
|
||||
activeMenu,
|
||||
expandedMenus,
|
||||
sidebarCollapsed,
|
||||
isMobile,
|
||||
onMenuClick,
|
||||
onToggleSubmenu,
|
||||
onCloseMobileSidebar,
|
||||
}: SidebarProps) {
|
||||
const handleMenuClick = (menuId: string, path: string, hasChildren: boolean) => {
|
||||
if (hasChildren) {
|
||||
onToggleSubmenu(menuId);
|
||||
} else {
|
||||
onMenuClick(menuId, path);
|
||||
if (isMobile && onCloseMobileSidebar) {
|
||||
onCloseMobileSidebar();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`h-full flex flex-col clean-glass rounded-2xl overflow-hidden transition-all duration-300 ${
|
||||
sidebarCollapsed ? 'sidebar-collapsed' : ''
|
||||
}`}>
|
||||
{/* 로고 */}
|
||||
<div
|
||||
className={`text-white relative transition-all duration-300 ${
|
||||
sidebarCollapsed ? 'p-5' : 'p-6 md:p-8'
|
||||
}`}
|
||||
style={{ backgroundColor: '#3B82F6' }}
|
||||
>
|
||||
<div className={`flex items-center relative z-10 transition-all duration-300 ${
|
||||
sidebarCollapsed ? 'justify-center' : 'space-x-4'
|
||||
}`}>
|
||||
<div className={`rounded-xl flex items-center justify-center clean-shadow backdrop-blur-sm transition-all duration-300 sidebar-logo relative overflow-hidden ${
|
||||
sidebarCollapsed ? 'w-11 h-11' : 'w-12 h-12 md:w-14 md:h-14'
|
||||
}`} style={{ backgroundColor: '#3B82F6' }}>
|
||||
<div className={`text-white font-bold transition-all duration-300 ${
|
||||
sidebarCollapsed ? 'text-lg' : 'text-xl md:text-2xl'
|
||||
}`}>
|
||||
S
|
||||
</div>
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-white/10 to-transparent opacity-30"></div>
|
||||
</div>
|
||||
{!sidebarCollapsed && (
|
||||
<div className="transition-all duration-300 opacity-100">
|
||||
<h1 className="text-xl md:text-2xl font-bold tracking-wide">SAM</h1>
|
||||
<p className="text-sm text-white/90 font-medium">Smart Automation Management</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 메뉴 */}
|
||||
<div className={`flex-1 overflow-y-auto transition-all duration-300 ${
|
||||
sidebarCollapsed ? 'px-3 py-4' : 'p-4 md:p-6'
|
||||
}`}>
|
||||
<div className={`transition-all duration-300 ${
|
||||
sidebarCollapsed ? 'space-y-2' : 'space-y-3'
|
||||
}`}>
|
||||
{menuItems.map((item) => {
|
||||
const IconComponent = item.icon;
|
||||
const hasChildren = item.children && item.children.length > 0;
|
||||
const isExpanded = expandedMenus.includes(item.id);
|
||||
const isActive = activeMenu === item.id;
|
||||
|
||||
return (
|
||||
<div key={item.id} className="relative">
|
||||
{/* 메인 메뉴 버튼 */}
|
||||
<button
|
||||
onClick={() => handleMenuClick(item.id, item.path, !!hasChildren)}
|
||||
className={`w-full flex items-center rounded-xl transition-all duration-200 ease-out touch-manipulation group relative overflow-hidden sidebar-menu-item ${
|
||||
sidebarCollapsed ? 'p-3 justify-center' : 'space-x-3 p-3 md:p-4'
|
||||
} ${
|
||||
isActive
|
||||
? "text-white clean-shadow scale-[0.98]"
|
||||
: "text-foreground hover:bg-accent hover:scale-[1.02] active:scale-[0.98]"
|
||||
}`}
|
||||
style={isActive ? { backgroundColor: '#3B82F6' } : {}}
|
||||
title={sidebarCollapsed ? item.label : undefined}
|
||||
>
|
||||
<div className={`rounded-lg flex items-center justify-center transition-all duration-200 sidebar-menu-icon ${
|
||||
sidebarCollapsed ? 'w-8 h-8' : 'w-9 h-9'
|
||||
} ${
|
||||
isActive
|
||||
? "bg-white/20"
|
||||
: "bg-primary/10 group-hover:bg-primary/20"
|
||||
}`}>
|
||||
{IconComponent && <IconComponent className={`transition-all duration-200 ${
|
||||
sidebarCollapsed ? 'h-4 w-4' : 'h-5 w-5'
|
||||
} ${
|
||||
isActive ? "text-white" : "text-primary"
|
||||
}`} />}
|
||||
</div>
|
||||
{!sidebarCollapsed && (
|
||||
<>
|
||||
<span className="flex-1 font-medium transition-all duration-200 opacity-100 text-left text-sm">{item.label}</span>
|
||||
{hasChildren && (
|
||||
<div className={`transition-transform duration-200 ${
|
||||
isExpanded ? 'rotate-90' : ''
|
||||
}`}>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{isActive && !sidebarCollapsed && (
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/10 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* 서브메뉴 */}
|
||||
{hasChildren && isExpanded && !sidebarCollapsed && (
|
||||
<div className="mt-2 ml-4 space-y-1 border-l-2 border-primary/20 pl-4">
|
||||
{item.children?.map((subItem) => {
|
||||
const SubIcon = subItem.icon;
|
||||
return (
|
||||
<button
|
||||
key={subItem.id}
|
||||
onClick={() => handleMenuClick(subItem.id, subItem.path, false)}
|
||||
className={`w-full flex items-center rounded-lg transition-all duration-200 p-2 space-x-2 group ${
|
||||
activeMenu === subItem.id
|
||||
? "bg-primary/10 text-primary"
|
||||
: "text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
<SubIcon className="h-4 w-4" />
|
||||
<span className="text-xs font-medium">{subItem.label}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import { useMenuStore } from '@/store/menuStore';
|
||||
import { useThemeStore } from '@/store/themeStore';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
LayoutDashboard,
|
||||
ShoppingCart,
|
||||
@@ -11,74 +12,224 @@ import {
|
||||
Database,
|
||||
ChevronDown,
|
||||
Menu,
|
||||
Search,
|
||||
Sun,
|
||||
Moon,
|
||||
Eye,
|
||||
User,
|
||||
Users,
|
||||
Sliders,
|
||||
Box,
|
||||
Layers,
|
||||
DollarSign,
|
||||
FileText,
|
||||
Code,
|
||||
Archive,
|
||||
Briefcase,
|
||||
Truck,
|
||||
Warehouse,
|
||||
Shield,
|
||||
Settings,
|
||||
Activity,
|
||||
Server,
|
||||
CheckSquare,
|
||||
UserCog,
|
||||
FileSignature,
|
||||
TrendingUp,
|
||||
ClipboardList,
|
||||
} from 'lucide-react';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet';
|
||||
import Sidebar from '@/components/layout/Sidebar';
|
||||
|
||||
export default function DashboardLayout() {
|
||||
const { menuItems, activeMenu, setActiveMenu, setMenuItems } = useMenuStore();
|
||||
const { menuItems, activeMenu, setActiveMenu, setMenuItems, sidebarCollapsed, toggleSidebar } = useMenuStore();
|
||||
const { theme, setTheme } = useThemeStore();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// 확장된 서브메뉴 관리 (기본적으로 master-data 확장)
|
||||
const [expandedMenus, setExpandedMenus] = useState<string[]>(['master-data']);
|
||||
|
||||
// 모바일 상태 관리
|
||||
const [isMobile, setIsMobile] = useState(false);
|
||||
const [isMobileSidebarOpen, setIsMobileSidebarOpen] = useState(false);
|
||||
|
||||
// 현재 사용자 역할 가져오기
|
||||
const userDataStr = localStorage.getItem("user");
|
||||
const userData = userDataStr ? JSON.parse(userDataStr) : null;
|
||||
const currentRole = userData?.role || "CEO";
|
||||
const [currentRole, setCurrentRole] = useState<string>(() => {
|
||||
const userDataStr = localStorage.getItem("user");
|
||||
const userData = userDataStr ? JSON.parse(userDataStr) : null;
|
||||
return userData?.role || "CEO";
|
||||
});
|
||||
|
||||
const [userName, setUserName] = useState<string>(() => {
|
||||
const userDataStr = localStorage.getItem("user");
|
||||
const userData = userDataStr ? JSON.parse(userDataStr) : null;
|
||||
return userData?.name || "김대표";
|
||||
});
|
||||
|
||||
const [userPosition, setUserPosition] = useState<string>(() => {
|
||||
const userDataStr = localStorage.getItem("user");
|
||||
const userData = userDataStr ? JSON.parse(userDataStr) : null;
|
||||
return userData?.position || "대표이사";
|
||||
});
|
||||
|
||||
// 모바일 감지
|
||||
useEffect(() => {
|
||||
const checkScreenSize = () => {
|
||||
setIsMobile(window.innerWidth < 768);
|
||||
};
|
||||
checkScreenSize();
|
||||
window.addEventListener('resize', checkScreenSize);
|
||||
return () => window.removeEventListener('resize', checkScreenSize);
|
||||
}, []);
|
||||
|
||||
// 역할별 대시보드 정의
|
||||
const roleDashboards = [
|
||||
{ role: 'SystemAdmin', label: '시스템 관리자', icon: Database },
|
||||
{ role: 'CEO', label: 'CEO', icon: LayoutDashboard },
|
||||
{ role: 'CEO', label: '대표이사', icon: LayoutDashboard },
|
||||
{ role: 'ProductionManager', label: '생산 관리자', icon: Factory },
|
||||
{ role: 'Worker', label: '생산작업자', icon: ClipboardList },
|
||||
{ role: 'Sales', label: '영업사원', icon: Briefcase },
|
||||
];
|
||||
|
||||
// 역할별 메뉴 생성 함수
|
||||
const getMenuItems = (userRole: string) => {
|
||||
if (userRole === "SystemAdmin") {
|
||||
return [
|
||||
{ id: "dashboard", label: "시스템 대시보드", icon: LayoutDashboard, path: "/dashboard" },
|
||||
{ id: "users", label: "사용자 관리", icon: Users, path: "/dashboard/users" },
|
||||
{ id: "menu-customization", label: "메뉴 커스터마이징", icon: Sliders, path: "/dashboard/menu-customization" },
|
||||
{
|
||||
id: "master-data",
|
||||
label: "기초정보 관리",
|
||||
icon: Database,
|
||||
path: "/dashboard/master-data",
|
||||
children: [
|
||||
{ id: "item-management", label: "품목관리", icon: Package, path: "/dashboard/master-data/item-management" },
|
||||
{ id: "product-management", label: "제품관리", icon: Box, path: "/dashboard/master-data/product-management" },
|
||||
{ id: "bom-management", label: "BOM관리", icon: Layers, path: "/dashboard/master-data/bom-management" },
|
||||
{ id: "pricing-management", label: "단가관리", icon: DollarSign, path: "/dashboard/master-data/pricing-management" },
|
||||
{ id: "quote-simulation", label: "모의견적하기", icon: FileText, path: "/dashboard/master-data/quote-simulation" },
|
||||
{ id: "code-management", label: "코드관리", icon: Code, path: "/dashboard/master-data/code-management" },
|
||||
{ id: "lot-management", label: "로트관리", icon: Archive, path: "/dashboard/master-data/lot-management" },
|
||||
]
|
||||
},
|
||||
{ id: "sales", label: "판매관리", icon: Briefcase, path: "/dashboard/sales" },
|
||||
{ id: "order", label: "수주관리", icon: ShoppingCart, path: "/dashboard/order" },
|
||||
{ id: "production", label: "생산관리", icon: Factory, path: "/dashboard/production" },
|
||||
{ id: "quality", label: "품질관리", icon: ClipboardCheck, path: "/dashboard/quality" },
|
||||
{ id: "shipping", label: "출고관리", icon: Truck, path: "/dashboard/shipping" },
|
||||
{ id: "material", label: "자재관리", icon: Warehouse, path: "/dashboard/materials" },
|
||||
{ id: "permissions", label: "권한 관리", icon: Shield, path: "/dashboard/permissions" },
|
||||
{ id: "system", label: "시스템 설정", icon: Settings, path: "/dashboard/system" },
|
||||
{ id: "database", label: "데이터베이스", icon: Database, path: "/dashboard/database" },
|
||||
{ id: "monitoring", label: "시스템 모니터링", icon: Activity, path: "/dashboard/monitoring" },
|
||||
{ id: "security", label: "보안 관리", icon: Server, path: "/dashboard/security" },
|
||||
];
|
||||
}
|
||||
|
||||
if (userRole === "Sales") {
|
||||
return [
|
||||
{ id: "leads", label: "리드 관리", icon: Users, path: "/dashboard/leads" },
|
||||
{ id: "sales", label: "판매관리", icon: Briefcase, path: "/dashboard/sales" },
|
||||
{ id: "dashboard", label: "대시보드", icon: LayoutDashboard, path: "/dashboard" },
|
||||
];
|
||||
}
|
||||
|
||||
if (userRole === "Worker") {
|
||||
return [
|
||||
{ id: "dashboard", label: "대시보드", icon: LayoutDashboard, path: "/dashboard" },
|
||||
{ id: "performance", label: "작업 실적", icon: ClipboardList, path: "/dashboard/performance" },
|
||||
{ id: "quality", label: "품질관리", icon: CheckSquare, path: "/dashboard/quality" },
|
||||
];
|
||||
}
|
||||
|
||||
if (userRole === "ProductionManager") {
|
||||
return [
|
||||
{ id: "dashboard", label: "생산 대시보드", icon: Factory, path: "/dashboard" },
|
||||
{ id: "production", label: "생산관리", icon: Factory, path: "/dashboard/production" },
|
||||
{ id: "quality", label: "품질관리", icon: ClipboardCheck, path: "/dashboard/quality" },
|
||||
{ id: "materials", label: "자재관리", icon: Package, path: "/dashboard/materials" },
|
||||
{ id: "performance", label: "작업 실적", icon: ClipboardList, path: "/dashboard/performance" },
|
||||
];
|
||||
}
|
||||
|
||||
// CEO 기본 메뉴
|
||||
return [
|
||||
{ id: "dashboard", label: "대시보드", icon: TrendingUp, path: "/dashboard" },
|
||||
{ id: "sales", label: "판매관리", icon: Briefcase, path: "/dashboard/sales" },
|
||||
{ id: "production", label: "생산관리", icon: Factory, path: "/dashboard/production" },
|
||||
{ id: "shipping", label: "출고관리", icon: Truck, path: "/dashboard/shipping" },
|
||||
{ id: "quality", label: "품질관리", icon: CheckSquare, path: "/dashboard/quality" },
|
||||
{ id: "materials", label: "자재관리", icon: Package, path: "/dashboard/materials" },
|
||||
{ id: "accounting", label: "회계관리", icon: DollarSign, path: "/dashboard/accounting" },
|
||||
{ id: "hr", label: "인사관리", icon: UserCog, path: "/dashboard/hr" },
|
||||
{ id: "approval", label: "전자결재", icon: FileSignature, path: "/dashboard/approval" },
|
||||
{ id: "master", label: "기준정보", icon: Database, path: "/dashboard/master-data" },
|
||||
];
|
||||
};
|
||||
|
||||
// 초기 메뉴 설정
|
||||
useEffect(() => {
|
||||
if (menuItems.length === 0) {
|
||||
setMenuItems([
|
||||
{
|
||||
id: 'dashboard',
|
||||
label: '대시보드',
|
||||
icon: LayoutDashboard,
|
||||
path: '/dashboard'
|
||||
},
|
||||
{
|
||||
id: 'sales-leads',
|
||||
label: '영업 대시보드',
|
||||
icon: ShoppingCart,
|
||||
path: '/dashboard/sales-leads'
|
||||
},
|
||||
{
|
||||
id: 'production',
|
||||
label: '생산관리',
|
||||
icon: Factory,
|
||||
path: '/dashboard/production'
|
||||
},
|
||||
{
|
||||
id: 'quality',
|
||||
label: '품질관리',
|
||||
icon: ClipboardCheck,
|
||||
path: '/dashboard/quality'
|
||||
},
|
||||
{
|
||||
id: 'materials',
|
||||
label: '자재관리',
|
||||
icon: Package,
|
||||
path: '/dashboard/materials'
|
||||
},
|
||||
]);
|
||||
}
|
||||
}, [menuItems.length, setMenuItems]);
|
||||
const roleBasedMenus = getMenuItems(currentRole);
|
||||
setMenuItems(roleBasedMenus);
|
||||
}, [currentRole, setMenuItems]);
|
||||
|
||||
const handleMenuClick = (menuId: string, path: string) => {
|
||||
setActiveMenu(menuId);
|
||||
navigate(path);
|
||||
};
|
||||
|
||||
// 서브메뉴 토글 함수
|
||||
const toggleSubmenu = (menuId: string) => {
|
||||
setExpandedMenus(prev =>
|
||||
prev.includes(menuId)
|
||||
? prev.filter(id => id !== menuId)
|
||||
: [...prev, menuId]
|
||||
);
|
||||
};
|
||||
|
||||
const handleRoleChange = (role: string) => {
|
||||
// 역할 변경 시 localStorage 업데이트
|
||||
if (userData) {
|
||||
userData.role = role;
|
||||
localStorage.setItem("user", JSON.stringify(userData));
|
||||
// 페이지 새로고침하여 역할별 대시보드 표시
|
||||
window.location.reload();
|
||||
// 역할별 기본 사용자 정보
|
||||
const roleUserInfo: Record<string, { name: string; position: string }> = {
|
||||
SystemAdmin: { name: "시스템관리자", position: "시스템 관리자" },
|
||||
CEO: { name: "김대표", position: "대표이사" },
|
||||
ProductionManager: { name: "김팀장", position: "생산 관리자" },
|
||||
Worker: { name: "홍작업자", position: "생산작업자" },
|
||||
Sales: { name: "박영업", position: "영업사원" },
|
||||
};
|
||||
|
||||
const userInfo = roleUserInfo[role] || roleUserInfo.CEO;
|
||||
|
||||
// localStorage 업데이트
|
||||
const userDataStr = localStorage.getItem("user");
|
||||
const userData = userDataStr ? JSON.parse(userDataStr) : {};
|
||||
userData.role = role;
|
||||
userData.name = userInfo.name;
|
||||
userData.position = userInfo.position;
|
||||
localStorage.setItem("user", JSON.stringify(userData));
|
||||
|
||||
// 상태 업데이트
|
||||
setCurrentRole(role);
|
||||
setUserName(userInfo.name);
|
||||
setUserPosition(userInfo.position);
|
||||
|
||||
// 페이지 새로고침 대신 메뉴 재생성
|
||||
const newMenuItems = getMenuItems(role);
|
||||
setMenuItems(newMenuItems);
|
||||
|
||||
// 첫 번째 메뉴로 이동
|
||||
const firstMenu = newMenuItems[0];
|
||||
if (firstMenu) {
|
||||
setActiveMenu(firstMenu.id);
|
||||
navigate(firstMenu.path);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -87,72 +238,136 @@ export default function DashboardLayout() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen w-full">
|
||||
<div className="w-64 border-r bg-gray-50 p-4">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-2xl font-bold">SAM</h1>
|
||||
</div>
|
||||
<nav>
|
||||
<ul className="space-y-2">
|
||||
{menuItems.map((item) => {
|
||||
const IconComponent = item.icon;
|
||||
return (
|
||||
<li key={item.id}>
|
||||
<button
|
||||
onClick={() => handleMenuClick(item.id, item.path)}
|
||||
className={`w-full text-left px-4 py-2 rounded-md flex items-center gap-3 transition-colors ${
|
||||
activeMenu === item.id
|
||||
? 'bg-blue-50 text-blue-900 font-medium border-l-4 border-blue-600'
|
||||
: 'hover:bg-gray-100 border-l-4 border-transparent'
|
||||
}`}
|
||||
>
|
||||
{IconComponent && <IconComponent className="h-5 w-5 flex-shrink-0" />}
|
||||
<span>{item.label}</span>
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</nav>
|
||||
<div className="min-h-screen flex w-full p-3 gap-3">
|
||||
{/* 데스크톱 사이드바 (모바일에서 숨김) */}
|
||||
<div
|
||||
className={`border-none bg-transparent hidden md:block transition-all duration-300 flex-shrink-0 ${
|
||||
sidebarCollapsed ? 'w-24' : 'w-80'
|
||||
}`}
|
||||
>
|
||||
<Sidebar
|
||||
menuItems={menuItems}
|
||||
activeMenu={activeMenu}
|
||||
expandedMenus={expandedMenus}
|
||||
sidebarCollapsed={sidebarCollapsed}
|
||||
isMobile={false}
|
||||
onMenuClick={handleMenuClick}
|
||||
onToggleSubmenu={toggleSubmenu}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<main className="flex-1">
|
||||
<header className="sticky top-0 z-10 flex h-14 items-center gap-4 border-b bg-white px-4 shadow-sm">
|
||||
<button className="p-2 hover:bg-gray-100 rounded-md transition-colors">
|
||||
<Menu className="h-5 w-5" />
|
||||
</button>
|
||||
<div className="flex-1" />
|
||||
{/* 메인 영역 */}
|
||||
<div className="flex-1 flex flex-col min-w-0">
|
||||
{/* 헤더 */}
|
||||
<header className="clean-glass rounded-2xl px-8 py-6 mb-3 clean-shadow relative overflow-hidden flex-shrink-0">
|
||||
<div className="flex items-center justify-between relative z-10">
|
||||
<div className="flex items-center space-x-8">
|
||||
{/* Menu 버튼 - 모바일: Sheet 열기, 데스크톱: 사이드바 토글 */}
|
||||
{isMobile ? (
|
||||
<Sheet open={isMobileSidebarOpen} onOpenChange={setIsMobileSidebarOpen}>
|
||||
<SheetTrigger asChild>
|
||||
<Button variant="ghost" size="sm" className="rounded-xl transition-all duration-200 hover:bg-accent p-3 md:hidden">
|
||||
<Menu className="h-5 w-5" />
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent side="left" className="w-64 p-4 bg-sidebar">
|
||||
<Sidebar
|
||||
menuItems={menuItems}
|
||||
activeMenu={activeMenu}
|
||||
expandedMenus={expandedMenus}
|
||||
sidebarCollapsed={false}
|
||||
isMobile={true}
|
||||
onMenuClick={handleMenuClick}
|
||||
onToggleSubmenu={toggleSubmenu}
|
||||
onCloseMobileSidebar={() => setIsMobileSidebarOpen(false)}
|
||||
/>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
) : (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={toggleSidebar}
|
||||
className="rounded-xl transition-all duration-200 hover:bg-accent p-3 hidden md:block"
|
||||
>
|
||||
<Menu className="h-5 w-5" />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* 역할별 대시보드 전환 버튼 */}
|
||||
<div className="relative group">
|
||||
<button className="flex items-center gap-2 px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 transition-colors">
|
||||
<Database className="h-4 w-4" />
|
||||
<span>{roleDashboards.find(d => d.role === currentRole)?.label || 'CEO'}</span>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</button>
|
||||
{/* 검색바 */}
|
||||
<div className="relative hidden lg:block">
|
||||
<Search className="absolute left-5 top-1/2 transform -translate-y-1/2 text-muted-foreground h-5 w-5" />
|
||||
<Input
|
||||
placeholder="통합 검색..."
|
||||
className="pl-14 w-96 clean-input border-0 bg-input-background/60 text-base"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 드롭다운 메뉴 */}
|
||||
<div className="absolute right-0 mt-2 w-56 bg-white border border-gray-200 rounded-md shadow-lg opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 z-50">
|
||||
{roleDashboards.map((dashboard) => {
|
||||
const DashboardIcon = dashboard.icon;
|
||||
return (
|
||||
<button
|
||||
key={dashboard.role}
|
||||
onClick={() => handleRoleChange(dashboard.role)}
|
||||
className="w-full flex items-center gap-3 px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 first:rounded-t-md last:rounded-b-md transition-colors"
|
||||
>
|
||||
<DashboardIcon className="h-4 w-4" />
|
||||
<span>{dashboard.label}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
<div className="flex items-center space-x-6">
|
||||
{/* 테마 토글 */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="sm" className="p-3 rounded-xl hover:bg-accent transition-all duration-200">
|
||||
{theme === 'light' && <Sun className="h-6 w-6" />}
|
||||
{theme === 'dark' && <Moon className="h-6 w-6" />}
|
||||
{theme === 'senior' && <Eye className="h-6 w-6" />}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-48 clean-glass">
|
||||
<DropdownMenuItem onClick={() => setTheme('light')}>
|
||||
<Sun className="mr-2 h-4 w-4" />
|
||||
일반모드
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme('dark')}>
|
||||
<Moon className="mr-2 h-4 w-4" />
|
||||
다크모드
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme('senior')}>
|
||||
<Eye className="mr-2 h-4 w-4" />
|
||||
시니어모드
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
{/* 유저 프로필 */}
|
||||
<div className="flex items-center space-x-4 pl-6 border-l border-border/30">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-11 h-11 bg-primary/10 rounded-xl flex items-center justify-center clean-shadow-sm">
|
||||
<User className="h-5 w-5 text-primary" />
|
||||
</div>
|
||||
<div className="text-sm hidden lg:block text-left">
|
||||
<p className="font-bold text-foreground text-base">{userName}</p>
|
||||
<p className="text-muted-foreground text-sm">{userPosition}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 역할별 대시보드 전환 셀렉트 박스 */}
|
||||
<select
|
||||
value={currentRole}
|
||||
onChange={(e) => handleRoleChange(e.target.value)}
|
||||
className="ml-4 bg-accent/60 border border-border/50 rounded-2xl backdrop-blur-sm transition-all duration-300 hover:bg-accent/80 hover:scale-105 focus:bg-white focus:border-primary/50 font-semibold text-sm px-4 py-3"
|
||||
style={{ backdropFilter: 'blur(16px)' }}
|
||||
>
|
||||
<option value="CEO">대표이사</option>
|
||||
<option value="ProductionManager">생산관리자</option>
|
||||
<option value="Worker">생산작업자</option>
|
||||
<option value="SystemAdmin">시스템관리자</option>
|
||||
<option value="Sales">영업사원</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Subtle gradient overlay */}
|
||||
<div className="absolute top-0 left-0 right-0 h-1 bg-gradient-to-r from-transparent via-primary/30 to-transparent"></div>
|
||||
</header>
|
||||
<div className="p-6">
|
||||
|
||||
{/* 콘텐츠 */}
|
||||
<main className="flex-1 overflow-auto">
|
||||
<Outlet />
|
||||
</div>
|
||||
</main>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
26
src/pages/PlaceholderPage.tsx
Normal file
26
src/pages/PlaceholderPage.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { Construction } from 'lucide-react';
|
||||
|
||||
interface PlaceholderPageProps {
|
||||
title?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export default function PlaceholderPage({ title, description }: PlaceholderPageProps) {
|
||||
const location = useLocation();
|
||||
|
||||
const displayTitle = title || location.pathname.split('/').pop()?.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase()) || '페이지';
|
||||
const displayDescription = description || `${displayTitle} 페이지는 현재 개발 중입니다.`;
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[calc(100vh-200px)]">
|
||||
<div className="text-center space-y-4">
|
||||
<div className="flex justify-center">
|
||||
<Construction className="h-16 w-16 text-muted-foreground" />
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-foreground">{displayTitle}</h1>
|
||||
<p className="text-muted-foreground max-w-md">{displayDescription}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user