[feat]: Shadcn UI 모달 Select 레이아웃 시프트 방지 및 코드 정리

주요 변경사항:
- 테마/언어 선택을 모달 스타일로 변경 (native={false})
  - LoginPage, SignupPage, DashboardLayout 적용
- CSS 2줄로 레이아웃 시프트 완전 제거
  - body { overflow: visible !important }
  - body[data-scroll-locked] { margin-right: 0 !important }
- 미사용 business 컴포넌트 대량 삭제 (코드 정리)
- CEODashboard → MainDashboard 이름 변경
- 구현 문서 작성: [IMPL-2025-11-12] modal-select-layout-shift-fix.md

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
byeongcheolryu
2025-11-12 18:09:12 +09:00
parent a68a25b737
commit 46aff1a6a2
57 changed files with 307 additions and 37120 deletions

3
.gitignore vendored
View File

@@ -99,3 +99,6 @@ build/
claudedocs/
.env.local
.env*.local
# ---> Unused components (archived)
src/components/_unused/

View File

@@ -14,7 +14,7 @@ const eslintConfig = [
"dist/**",
"node_modules/**",
"next-env.d.ts",
"src/components/business/**", // Demo/example components
"src/components/_unused/**", // Archived unused components
"src/hooks/useCurrentTime.ts", // Demo hook
],
},

View File

@@ -1,4 +1,8 @@
'use client';
import { notFound } from 'next/navigation';
import { EmptyPage } from '@/components/common/EmptyPage';
import { useEffect, useState } from 'react';
interface PageProps {
params: Promise<{
@@ -10,16 +14,89 @@ interface PageProps {
/**
* Catch-all 라우트: 정의되지 않은 모든 경로를 처리
*
* 예시:
* - /base/product/lists → EmptyPage 표시
* - /system/user/lists → EmptyPage 표시
* - /custom/path → EmptyPage 표시
* 로직:
* 1. localStorage의 user.menu에서 유효한 경로인지 확인
* 2. 메뉴에 있는 경로 (구현 안됨) → EmptyPage 표시
* 3. 메뉴에 없는 완전 엉뚱한 경로 → not-found 페이지 표시
*
* 실제 페이지를 추가하려면 해당 경로에 page.tsx 파일을 생성하세요.
* 예시:
* - /base/product/lists (메뉴에 있음) → EmptyPage
* - /system/user/lists (메뉴에 있음) → EmptyPage
* - /completely/random/path (메뉴에 없음) → 404
*/
export default async function CatchAllPage({ params }: PageProps) {
const { slug: _slug } = await params;
interface MenuItem {
path?: string;
children?: MenuItem[];
}
export default function CatchAllPage({ params }: PageProps) {
const [isValidPath, setIsValidPath] = useState<boolean | null>(null);
useEffect(() => {
const checkPath = async () => {
const { slug } = await params;
// localStorage에서 메뉴 데이터 가져오기
const userStr = localStorage.getItem('user');
if (!userStr) {
console.log('❌ localStorage에 user 정보 없음');
setIsValidPath(false);
return;
}
const userData = JSON.parse(userStr);
const menus = userData.menu || [];
// slug를 경로로 변환 (예: ['base', 'product', 'lists'] → '/base/product/lists')
const requestedPath = `/${slug.join('/')}`;
console.log('🔍 요청된 경로:', requestedPath);
console.log('📋 메뉴 데이터:', menus);
// 메뉴 구조를 재귀적으로 탐색하여 경로 확인
const isPathInMenu = (menuItems: MenuItem[], path: string): boolean => {
for (const item of menuItems) {
console.log(' - 비교 중:', item.path, 'vs', path);
// path가 요청된 경로와 일치하는지 확인
if (item.path === path) {
console.log(' ✅ 일치!');
return true;
}
// 하위 메뉴 확인
if (item.children && item.children.length > 0) {
if (isPathInMenu(item.children, path)) {
return true;
}
}
}
return false;
};
const pathExists = isPathInMenu(menus, requestedPath);
console.log('📌 경로 존재 여부:', pathExists);
setIsValidPath(pathExists);
};
void checkPath();
}, [params]);
// 로딩 중
if (isValidPath === null) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="text-muted-foreground">Loading...</div>
</div>
);
}
// 메뉴에 없는 경로 → 404
if (!isValidPath) {
notFound();
}
// 메뉴에 있지만 구현되지 않은 페이지 → EmptyPage
return (
<EmptyPage
iconName="FileSearch"

View File

@@ -2,12 +2,28 @@ import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
/**
* Auth Check Route Handler
* 🔵 Next.js 내부 API - 인증 상태 확인 (PHP 백엔드 X)
*
* Purpose:
* - Check if user is authenticated (HttpOnly cookie validation)
* - Prevent browser back button cache issues
* - Real-time authentication validation
* ⚡ 설계 목적:
* - 성능 최적화: 매번 PHP 백엔드 호출 대신 로컬 쿠키만 확인
* - 백엔드 부하 감소: 간단한 인증 확인은 Next.js에서 처리
* - 사용자 경험: 즉시 응답으로 빠른 페이지 전환
*
* 📍 사용 위치:
* - LoginPage.tsx: 이미 로그인된 사용자를 대시보드로 리다이렉트
* - SignupPage.tsx: 이미 로그인된 사용자를 대시보드로 리다이렉트
* - 뒤로가기 시 캐시 문제 방지
*
* 🔄 동작 방식:
* 1. HttpOnly 쿠키에서 access_token, refresh_token 확인
* 2. access_token 있음 → { authenticated: true } 즉시 응답
* 3. refresh_token만 있음 → PHP /api/v1/refresh 호출하여 토큰 갱신
* 4. 둘 다 없음 → { authenticated: false } 응답
*
* ⚠️ 주의:
* - 이 API는 PHP 백엔드에 존재하지 않습니다
* - Next.js 프론트엔드 자체 유틸리티 API입니다
* - 실제 인증 로직은 여전히 PHP 백엔드가 담당합니다
*/
export async function GET(request: NextRequest) {
try {
@@ -18,7 +34,7 @@ export async function GET(request: NextRequest) {
// No tokens at all - not authenticated
if (!accessToken && !refreshToken) {
return NextResponse.json(
{ error: 'Not authenticated', authenticated: false },
{ error: 'Not authenticated' },
{ status: 401 }
);
}
@@ -34,6 +50,8 @@ export async function GET(request: NextRequest) {
// Only has refresh token - try to refresh
if (refreshToken && !accessToken) {
console.log('🔄 Access token missing, attempting refresh...');
console.log('🔍 Refresh token exists:', refreshToken.substring(0, 20) + '...');
console.log('🔍 Backend URL:', process.env.NEXT_PUBLIC_API_URL);
// Attempt token refresh
try {
@@ -42,11 +60,15 @@ export async function GET(request: NextRequest) {
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${refreshToken}`,
'X-API-KEY': process.env.NEXT_PUBLIC_API_KEY || '',
},
body: JSON.stringify({
refresh_token: refreshToken,
}),
});
console.log('🔍 Refresh API response status:', refreshResponse.status);
if (refreshResponse.ok) {
const data = await refreshResponse.json();
@@ -80,21 +102,25 @@ export async function GET(request: NextRequest) {
response.headers.append('Set-Cookie', refreshTokenCookie);
return response;
} else {
const errorData = await refreshResponse.text();
console.error('❌ Refresh API failed:', refreshResponse.status, errorData);
}
} catch (error) {
console.error('Token refresh failed in auth check:', error);
console.error('Token refresh failed in auth check:', error);
}
// Refresh failed - not authenticated
console.log('⚠️ Returning 401 due to refresh failure');
return NextResponse.json(
{ error: 'Token refresh failed', authenticated: false },
{ error: 'Token refresh failed' },
{ status: 401 }
);
}
// Fallback - not authenticated
return NextResponse.json(
{ error: 'Not authenticated', authenticated: false },
{ error: 'Not authenticated' },
{ status: 401 }
);

View File

@@ -1,6 +1,32 @@
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
/**
* 🔵 Next.js 내부 API - 로그인 프록시 (PHP 백엔드로 전달)
*
* ⚡ 설계 목적:
* - 보안: HttpOnly 쿠키로 토큰 저장 (JavaScript 접근 불가)
* - 프록시 패턴: PHP 백엔드 API 호출 후 토큰을 안전하게 쿠키로 설정
* - 클라이언트 보호: 토큰을 절대 클라이언트 JavaScript에 노출하지 않음
*
* 🔄 동작 흐름:
* 1. 클라이언트 → Next.js /api/auth/login (user_id, user_pwd)
* 2. Next.js → PHP /api/v1/login (인증 요청)
* 3. PHP → Next.js (access_token, refresh_token, 사용자 정보)
* 4. Next.js: 토큰을 HttpOnly 쿠키로 설정
* 5. Next.js → 클라이언트 (토큰 제외한 사용자 정보만 전달)
*
* 🔐 보안 특징:
* - 토큰은 클라이언트에 절대 노출되지 않음
* - HttpOnly: XSS 공격 방지
* - Secure: HTTPS만 전송
* - SameSite=Strict: CSRF 공격 방지
*
* ⚠️ 주의:
* - 이 API는 PHP /api/v1/login의 프록시입니다
* - 실제 인증 로직은 PHP 백엔드에서 처리됩니다
*/
/**
* 백엔드 API 로그인 응답 타입
*/
@@ -23,7 +49,7 @@ interface BackendLoginResponse {
company_name: string;
business_num: string;
tenant_st_code: string;
other_tenants: any[];
other_tenants: unknown[];
};
menus: Array<{
id: number;

View File

@@ -2,12 +2,28 @@ import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
/**
* Logout Proxy Route Handler
* 🔵 Next.js 내부 API - 로그아웃 프록시 (PHP 백엔드로 전달)
*
* Purpose:
* - Call PHP backend logout API
* - Clear HttpOnly cookie
* - Ensure complete session cleanup
* ⚡ 설계 목적:
* - 완전한 로그아웃: PHP 백엔드 토큰 무효화 + 쿠키 삭제
* - 보안: HttpOnly 쿠키에서 토큰을 읽어 백엔드로 전달
* - 세션 정리: 클라이언트와 서버 양쪽 모두 세션 종료
*
* 🔄 동작 흐름:
* 1. 클라이언트 → Next.js /api/auth/logout
* 2. Next.js: HttpOnly 쿠키에서 access_token 읽기
* 3. Next.js → PHP /api/v1/logout (토큰 무효화 요청)
* 4. Next.js: access_token, refresh_token 쿠키 삭제
* 5. Next.js → 클라이언트 (로그아웃 성공 응답)
*
* 🔐 보안 특징:
* - 백엔드에서 토큰 블랙리스트 처리 (재사용 방지)
* - 쿠키 완전 삭제 (Max-Age=0)
* - 로그아웃 실패해도 쿠키는 삭제 (클라이언트 보호)
*
* ⚠️ 주의:
* - 이 API는 PHP /api/v1/logout의 프록시입니다
* - 백엔드 호출 실패해도 쿠키는 삭제됩니다 (안전 우선)
*/
export async function POST(request: NextRequest) {
try {

View File

@@ -2,12 +2,34 @@ import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
/**
* Token Refresh Route Handler
* 🔵 Next.js 내부 API - 토큰 갱신 프록시 (PHP 백엔드로 전달)
*
* Purpose:
* - Refresh expired access_token using refresh_token
* - Update HttpOnly cookies with new tokens
* - Maintain user session without re-login
* ⚡ 설계 목적:
* - 자동 토큰 갱신: access_token 만료 시 재로그인 없이 세션 유지
* - 보안: refresh_token을 HttpOnly 쿠키에서 읽어 백엔드로 전달
* - 사용자 경험: 끊김없는 사용 (2시간마다 자동 갱신)
*
* 🔄 동작 흐름:
* 1. 클라이언트 → Next.js /api/auth/refresh
* 2. Next.js: HttpOnly 쿠키에서 refresh_token 읽기
* 3. Next.js → PHP /api/v1/refresh (새 토큰 요청)
* 4. PHP → Next.js (새 access_token, refresh_token)
* 5. Next.js: 새 토큰을 HttpOnly 쿠키로 업데이트
* 6. Next.js → 클라이언트 (갱신 성공 응답)
*
* 📍 호출 시점:
* - /api/auth/check에서 access_token 없을 때 자동 호출
* - API 호출 시 401 응답 받을 때 (withTokenRefresh 헬퍼)
* - 미들웨어에서 토큰 만료 감지 시
*
* 🔐 보안 특징:
* - Token Rotation: 갱신 시 새로운 refresh_token도 발급
* - HttpOnly: 토큰이 JavaScript에 노출되지 않음
* - 자동 만료: access_token 2시간, refresh_token 7일
*
* ⚠️ 주의:
* - 이 API는 PHP /api/v1/refresh의 프록시입니다
* - refresh_token 만료 시 재로그인 필요 (401 응답)
*/
export async function POST(request: NextRequest) {
try {
@@ -27,9 +49,11 @@ export async function POST(request: NextRequest) {
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${refreshToken}`,
'X-API-KEY': process.env.NEXT_PUBLIC_API_KEY || '',
},
body: JSON.stringify({
refresh_token: refreshToken,
}),
});
if (!response.ok) {

View File

@@ -2,12 +2,34 @@ import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
/**
* Signup Proxy Route Handler
* 🔵 Next.js 내부 API - 회원가입 프록시 (PHP 백엔드로 전달)
*
* Purpose:
* - Proxy signup requests to PHP backend
* - Handle registration errors gracefully
* - Return user-friendly error messages
* ⚡ 설계 목적:
* - 입력 검증: 클라이언트에서 받은 데이터 유효성 확인
* - 에러 처리: 백엔드 에러를 사용자 친화적인 메시지로 변환
* - 보안: 백엔드 상세 에러 메시지 노출 방지
*
* 🔄 동작 흐름:
* 1. 클라이언트 → Next.js /api/auth/signup (회원가입 정보)
* 2. Next.js: 필수 필드 유효성 검증
* 3. Next.js → PHP /api/v1/register (회원가입 요청)
* 4. PHP → Next.js (성공/실패 응답)
* 5. Next.js → 클라이언트 (사용자 친화적 메시지)
*
* 📋 필수 필드:
* - user_id, name, email, phone
* - password, password_confirmation
* - company_name, business_num, company_scale, industry
* - position (선택)
*
* 🔐 보안 특징:
* - 백엔드 상세 에러 숨김 (정보 유출 방지)
* - 일반화된 에러 메시지 제공
* - 입력 데이터 사전 검증
*
* ⚠️ 주의:
* - 이 API는 PHP /api/v1/register의 프록시입니다
* - 회원가입 시 토큰은 발급하지 않음 (로그인 페이지로 리다이렉트)
*/
export async function POST(request: NextRequest) {
try {

View File

@@ -204,6 +204,11 @@
font-family: 'Pretendard', -apple-system, BlinkMacSystemFont, system-ui, Roboto, 'Helvetica Neue', 'Segoe UI', 'Apple SD Gothic Neo', 'Noto Sans KR', 'Malgun Gothic', sans-serif;
}
html {
/* 🔧 Always show scrollbar to prevent layout shift */
/*overflow-y: scroll;*/
}
body {
@apply bg-background text-foreground;
font-family: 'Pretendard', -apple-system, BlinkMacSystemFont, system-ui, Roboto, 'Helvetica Neue', 'Segoe UI', 'Apple SD Gothic Neo', 'Noto Sans KR', 'Malgun Gothic', sans-serif;
@@ -213,6 +218,21 @@
text-rendering: optimizeLegibility;
background: var(--background);
min-height: 100vh;
/* 🔧 Body has no overflow - html handles all scrolling */
overflow: visible !important;
}
/* 🔧 Override Radix's scroll-lock completely to prevent any layout shift */
body[data-scroll-locked] {
/*overflow: visible !important;*/
/*position: static !important;*/
/*padding-right: 0 !important;*/
margin-right: 0 !important;
}
/* 🔧 Prevent scroll on modal backdrop instead of body */
[data-radix-portal] {
/*position: fixed;*/
}
/* Clean glass utilities */

View File

@@ -1,45 +0,0 @@
'use client';
import { useLocale } from 'next-intl';
import { useRouter, usePathname } from 'next/navigation';
import { locales, localeNames, localeFlags, type Locale } from '@/i18n/config';
/**
* Language Switcher Component
*
* Allows users to switch between available locales
* Usage: Place in header or navigation bar
*/
export default function LanguageSwitcher() {
const locale = useLocale();
const router = useRouter();
const pathname = usePathname();
const handleLocaleChange = (newLocale: Locale) => {
// Get the pathname without the current locale
const pathnameWithoutLocale = pathname.replace(`/${locale}`, '');
// Navigate to the new locale
router.push(`/${newLocale}${pathnameWithoutLocale}`);
};
return (
<div className="flex gap-2">
{locales.map((loc) => (
<button
key={loc}
onClick={() => handleLocaleChange(loc)}
className={`px-3 py-2 rounded-md text-sm font-medium transition-colors ${
locale === loc
? 'bg-blue-600 text-white'
: 'bg-gray-200 text-gray-700 hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600'
}`}
aria-label={`Switch to ${localeNames[loc]}`}
>
<span className="mr-1">{localeFlags[loc]}</span>
{localeNames[loc]}
</button>
))}
</div>
);
}

View File

@@ -1,43 +0,0 @@
'use client';
import { useTranslations } from 'next-intl';
import Link from 'next/link';
import { useLocale } from 'next-intl';
/**
* Navigation Menu Component
*
* Demonstrates translation in navigation elements
* Shows how to use translations with dynamic content
*/
export default function NavigationMenu() {
const t = useTranslations('navigation');
const locale = useLocale();
const menuItems = [
{ key: 'dashboard' as const, href: '/dashboard' },
{ key: 'inventory' as const, href: '/inventory' },
{ key: 'finance' as const, href: '/finance' },
{ key: 'hr' as const, href: '/hr' },
{ key: 'crm' as const, href: '/crm' },
{ key: 'reports' as const, href: '/reports' },
{ key: 'settings' as const, href: '/settings' },
] as const;
return (
<nav className="bg-gray-100 dark:bg-gray-900 p-4 rounded-lg">
<ul className="flex flex-wrap gap-4">
{menuItems.map((item) => (
<li key={item.key}>
<Link
href={`/${locale}${item.href}`}
className="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300 font-medium"
>
{t(item.key)}
</Link>
</li>
))}
</ul>
</nav>
);
}

View File

@@ -1,20 +0,0 @@
'use client';
import { useTranslations } from 'next-intl';
/**
* Welcome Message Component
*
* Demonstrates basic translation usage
* Shows how to use useTranslations hook in client components
*/
export default function WelcomeMessage() {
const t = useTranslations('common');
return (
<div className="p-6 bg-white dark:bg-gray-800 rounded-lg shadow-md">
<h2 className="text-2xl font-bold mb-2">{t('welcome')}</h2>
<p className="text-gray-600 dark:text-gray-300">{t('appName')}</p>
</div>
);
}

View File

@@ -33,14 +33,17 @@ export function LoginPage() {
useEffect(() => {
const checkAuth = async () => {
try {
// 🔵 Next.js 내부 API - 쿠키에서 토큰 확인 (PHP 호출 X, 성능 최적화)
const response = await fetch('/api/auth/check');
if (response.ok) {
// 이미 로그인됨 → 대시보드로 리다이렉트 (replace로 히스토리에서 제거)
router.replace('/dashboard');
return;
}
// 인증 안됨 (401) → 현재 페이지 유지
} catch {
// 인증 안됨 → 현재 페이지 유지
// API 호출 실패 → 현재 페이지 유지
} finally {
setIsChecking(false);
}
@@ -59,8 +62,7 @@ export function LoginPage() {
}
try {
// ✅ HttpOnly Cookie 방식: Next.js API Route로 프록시
// 토큰은 JavaScript에서 접근 불가능한 HttpOnly 쿠키로 저장됨
// 🔵 Next.js 프록시 → PHP /api/v1/login (토큰을 HttpOnly 쿠키로 저장)
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: {
@@ -156,8 +158,8 @@ export function LoginPage() {
</div>
</button>
<div className="flex items-center gap-3">
<ThemeSelect />
<LanguageSelect />
<ThemeSelect native={false} />
<LanguageSelect native={false} />
<Button variant="ghost" onClick={() => router.push("/signup")} className="rounded-xl">
{t('signUp')}
</Button>

View File

@@ -127,14 +127,17 @@ export function SignupPage() {
useEffect(() => {
const checkAuth = async () => {
try {
// 🔵 Next.js 내부 API - 쿠키에서 토큰 확인 (PHP 호출 X, 성능 최적화)
const response = await fetch('/api/auth/check');
if (response.ok) {
// 이미 로그인됨 → 대시보드로 리다이렉트 (replace로 히스토리에서 제거)
router.replace('/dashboard');
return;
}
// 인증 안됨 (401) → 현재 페이지 유지
} catch {
// 인증 안됨 → 현재 페이지 유지
// API 호출 실패 → 현재 페이지 유지
} finally {
setIsChecking(false);
}
@@ -163,6 +166,7 @@ export function SignupPage() {
industry: formData.industry,
};
// 🔵 Next.js 프록시 → PHP /api/v1/register (회원가입 처리)
const response = await fetch('/api/auth/signup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -282,8 +286,8 @@ export function SignupPage() {
</div>
</button>
<div className="flex items-center gap-3">
<ThemeSelect />
<LanguageSelect />
<ThemeSelect native={false} />
<LanguageSelect native={false} />
<Button variant="ghost" onClick={() => router.push("/login")} className="rounded-xl">
{t("login")}
</Button>

View File

@@ -1,437 +0,0 @@
import { useState } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
DollarSign,
TrendingUp,
TrendingDown,
CreditCard,
Banknote,
PieChart,
Calculator,
FileText,
AlertCircle,
CheckCircle,
ArrowUpRight,
ArrowDownRight,
Building2,
Calendar
} from "lucide-react";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, LineChart, Line, PieChart as RechartsPieChart, Cell } from "recharts";
export function AccountingManagement() {
const [selectedPeriod, setSelectedPeriod] = useState("month");
// 매출/매입 데이터
const salesPurchaseData = [
{ month: "1월", sales: 450, purchase: 280, profit: 170 },
{ month: "2월", sales: 520, purchase: 310, profit: 210 },
{ month: "3월", sales: 480, purchase: 295, profit: 185 },
{ month: "4월", sales: 610, purchase: 350, profit: 260 },
{ month: "5월", sales: 580, purchase: 340, profit: 240 },
{ month: "6월", sales: 650, purchase: 380, profit: 270 }
];
// 거래처별 미수금
const receivables = [
{ company: "삼성전자", amount: 45000000, days: 45, status: "위험" },
{ company: "LG전자", amount: 32000000, days: 28, status: "주의" },
{ company: "현대자동차", amount: 28000000, days: 15, status: "정상" },
{ company: "SK하이닉스", amount: 25000000, days: 52, status: "위험" },
{ company: "네이버", amount: 18000000, days: 22, status: "정상" }
];
// 건별 원가 분석
const costAnalysis = [
{
orderNo: "ORD-2024-001",
product: "방화셔터 3000×3000",
salesAmount: 15000000,
materialCost: 6500000,
laborCost: 3500000,
overheadCost: 2000000,
totalCost: 12000000,
profit: 3000000,
profitRate: 20.0
},
{
orderNo: "ORD-2024-002",
product: "일반셔터 2500×2500",
salesAmount: 8500000,
materialCost: 3200000,
laborCost: 2100000,
overheadCost: 1200000,
totalCost: 6500000,
profit: 2000000,
profitRate: 23.5
},
{
orderNo: "ORD-2024-003",
product: "특수셔터 4000×3500",
salesAmount: 22000000,
materialCost: 9500000,
laborCost: 5200000,
overheadCost: 2800000,
totalCost: 17500000,
profit: 4500000,
profitRate: 20.5
}
];
// 원가 구성 비율
const costComposition = [
{ name: "자재비", value: 54, color: "#3B82F6" },
{ name: "인건비", value: 29, color: "#10B981" },
{ name: "경비", value: 17, color: "#F59E0B" }
];
return (
<div className="p-4 md:p-6 space-y-6">
{/* 헤더 */}
<div className="bg-card border border-border/20 rounded-xl p-6">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<h1 className="text-3xl font-bold text-foreground mb-2"> </h1>
<p className="text-muted-foreground">/, , </p>
</div>
<div className="flex space-x-2">
<Button variant="outline">
<FileText className="h-4 w-4 mr-2" />
</Button>
<Button className="bg-primary">
<Calculator className="h-4 w-4 mr-2" />
</Button>
</div>
</div>
</div>
{/* 주요 지표 */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<Card className="border border-border/20">
<CardHeader className="pb-3">
<CardTitle className="text-sm text-muted-foreground flex items-center justify-between">
<span> </span>
<DollarSign className="h-4 w-4 text-green-600" />
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-foreground mb-2">650M원</div>
<div className="flex items-center space-x-1 text-sm text-green-600">
<ArrowUpRight className="h-3 w-3" />
<span> +12.1%</span>
</div>
</CardContent>
</Card>
<Card className="border border-border/20">
<CardHeader className="pb-3">
<CardTitle className="text-sm text-muted-foreground flex items-center justify-between">
<span> </span>
<CreditCard className="h-4 w-4 text-orange-600" />
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-foreground mb-2">380M원</div>
<div className="flex items-center space-x-1 text-sm text-orange-600">
<ArrowUpRight className="h-3 w-3" />
<span> +11.8%</span>
</div>
</CardContent>
</Card>
<Card className="border border-border/20">
<CardHeader className="pb-3">
<CardTitle className="text-sm text-muted-foreground flex items-center justify-between">
<span> </span>
<TrendingUp className="h-4 w-4 text-blue-600" />
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-foreground mb-2">270M원</div>
<div className="flex items-center space-x-1 text-sm text-blue-600">
<span>이익률: 41.5%</span>
</div>
</CardContent>
</Card>
<Card className="border border-border/20">
<CardHeader className="pb-3">
<CardTitle className="text-sm text-muted-foreground flex items-center justify-between">
<span> </span>
<AlertCircle className="h-4 w-4 text-red-600" />
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-foreground mb-2">148M원</div>
<div className="flex items-center space-x-1 text-sm text-red-600">
<span>30 초과: 70M원</span>
</div>
</CardContent>
</Card>
</div>
{/* 탭 메뉴 */}
<Tabs defaultValue="sales" className="space-y-4">
<TabsList className="grid w-full grid-cols-5">
<TabsTrigger value="sales">/</TabsTrigger>
<TabsTrigger value="receivables"> </TabsTrigger>
<TabsTrigger value="cost"> </TabsTrigger>
<TabsTrigger value="profit"> </TabsTrigger>
<TabsTrigger value="transactions"></TabsTrigger>
</TabsList>
{/* 매출/매입 관리 */}
<TabsContent value="sales" className="space-y-4">
<Card className="border border-border/20">
<CardHeader>
<CardTitle className="flex items-center space-x-3">
<TrendingUp className="h-6 w-6 text-primary" />
<span>/ </span>
</CardTitle>
</CardHeader>
<CardContent>
<div className="h-80">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={salesPurchaseData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="month" />
<YAxis />
<Tooltip />
<Bar dataKey="sales" fill="#10B981" name="매출" />
<Bar dataKey="purchase" fill="#F59E0B" name="매입" />
<Bar dataKey="profit" fill="#3B82F6" name="이익" />
</BarChart>
</ResponsiveContainer>
</div>
</CardContent>
</Card>
</TabsContent>
{/* 미수금 관리 */}
<TabsContent value="receivables" className="space-y-4">
<Card className="border border-border/20">
<CardHeader>
<CardTitle className="flex items-center space-x-3">
<CreditCard className="h-6 w-6 text-red-600" />
<span> </span>
</CardTitle>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead className="text-right"></TableHead>
<TableHead className="text-center"></TableHead>
<TableHead className="text-center"></TableHead>
<TableHead className="text-center"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{receivables.map((item, index) => (
<TableRow key={index}>
<TableCell className="font-medium">{item.company}</TableCell>
<TableCell className="text-right font-bold">
{item.amount.toLocaleString()}
</TableCell>
<TableCell className="text-center">{item.days}</TableCell>
<TableCell className="text-center">
<Badge
className={
item.status === "위험"
? "bg-red-500 text-white"
: item.status === "주의"
? "bg-yellow-500 text-white"
: "bg-green-500 text-white"
}
>
{item.status}
</Badge>
</TableCell>
<TableCell className="text-center">
<Button size="sm" variant="outline">
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
</TabsContent>
{/* 원가 분석 */}
<TabsContent value="cost" className="space-y-4">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<Card className="border border-border/20">
<CardHeader>
<CardTitle className="flex items-center space-x-3">
<PieChart className="h-6 w-6 text-primary" />
<span> </span>
</CardTitle>
</CardHeader>
<CardContent>
<div className="h-64">
<ResponsiveContainer width="100%" height="100%">
<RechartsPieChart>
<Tooltip />
<RechartsPieChart>
{costComposition.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</RechartsPieChart>
</RechartsPieChart>
</ResponsiveContainer>
</div>
<div className="space-y-2 mt-4">
{costComposition.map((item, index) => (
<div key={index} className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<div
className="w-4 h-4 rounded"
style={{ backgroundColor: item.color }}
/>
<span className="text-sm font-medium">{item.name}</span>
</div>
<span className="font-bold">{item.value}%</span>
</div>
))}
</div>
</CardContent>
</Card>
<Card className="border border-border/20">
<CardHeader>
<CardTitle className="flex items-center space-x-3">
<Calculator className="h-6 w-6 text-primary" />
<span> </span>
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-3">
{costAnalysis.slice(0, 3).map((item, index) => (
<div
key={index}
className="p-3 bg-muted/50 rounded-lg border border-border/50"
>
<div className="flex justify-between items-start mb-2">
<div>
<span className="text-xs text-muted-foreground">{item.orderNo}</span>
<p className="font-bold text-sm">{item.product}</p>
</div>
<Badge
className={
item.profitRate >= 25
? "bg-green-500 text-white"
: item.profitRate >= 20
? "bg-blue-500 text-white"
: "bg-yellow-500 text-white"
}
>
{item.profitRate}%
</Badge>
</div>
<div className="grid grid-cols-2 gap-2 text-xs">
<div>
<span className="text-muted-foreground">: </span>
<span className="font-bold">
{(item.salesAmount / 1000000).toFixed(1)}M
</span>
</div>
<div>
<span className="text-muted-foreground">: </span>
<span className="font-bold">
{(item.totalCost / 1000000).toFixed(1)}M
</span>
</div>
<div className="col-span-2">
<span className="text-muted-foreground">: </span>
<span className="font-bold text-green-600">
{(item.profit / 1000000).toFixed(1)}M
</span>
</div>
</div>
</div>
))}
</div>
</CardContent>
</Card>
</div>
</TabsContent>
{/* 손익 현황 */}
<TabsContent value="profit" className="space-y-4">
<Card className="border border-border/20">
<CardHeader>
<CardTitle className="flex items-center space-x-3">
<Banknote className="h-6 w-6 text-primary" />
<span> </span>
</CardTitle>
</CardHeader>
<CardContent>
<div className="h-80">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={salesPurchaseData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="month" />
<YAxis />
<Tooltip />
<Line type="monotone" dataKey="profit" stroke="#10B981" strokeWidth={2} name="순이익" />
</LineChart>
</ResponsiveContainer>
</div>
</CardContent>
</Card>
</TabsContent>
{/* 입출금 내역 */}
<TabsContent value="transactions" className="space-y-4">
<Card className="border border-border/20">
<CardHeader>
<CardTitle className="flex items-center space-x-3">
<Building2 className="h-6 w-6 text-primary" />
<span> </span>
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-3">
<div className="p-4 bg-green-50 dark:bg-green-950/20 rounded-lg border border-green-200">
<div className="flex justify-between items-center">
<div>
<Badge className="bg-green-600 text-white mb-2"></Badge>
<p className="font-bold"></p>
<p className="text-sm text-muted-foreground">2024-10-13 14:30</p>
</div>
<div className="text-right">
<p className="text-2xl font-bold text-green-600">+25,000,000</p>
<p className="text-xs text-muted-foreground"> </p>
</div>
</div>
</div>
<div className="p-4 bg-red-50 dark:bg-red-950/20 rounded-lg border border-red-200">
<div className="flex justify-between items-center">
<div>
<Badge className="bg-red-600 text-white mb-2"></Badge>
<p className="font-bold"></p>
<p className="text-sm text-muted-foreground">2024-10-13 11:20</p>
</div>
<div className="text-right">
<p className="text-2xl font-bold text-red-600">-15,000,000</p>
<p className="text-xs text-muted-foreground"> </p>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
);
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,656 +0,0 @@
import { useState } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Badge } from "@/components/ui/badge";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogTrigger } from "@/components/ui/dialog";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import { toast } from "sonner";
import { Search, Plus, Download, Filter, Eye, Edit, Trash2, MessageSquare, FileText, Bell, Pin, Upload, Calendar } from "lucide-react";
interface Notice {
id: string;
title: string;
content: string;
author: string;
department: string;
date: string;
views: number;
isPinned: boolean;
isImportant: boolean;
category: string;
}
interface Document {
id: string;
title: string;
description: string;
fileName: string;
fileSize: string;
version: string;
author: string;
uploadDate: string;
downloads: number;
category: string;
accessLevel: string;
}
interface Approval {
id: string;
title: string;
type: string;
requestor: string;
department: string;
requestDate: string;
status: string;
currentApprover: string;
amount?: number;
urgency: string;
}
export function Board() {
const [notices, setNotices] = useState<Notice[]>([
{
id: "N001",
title: "2025년 1분기 생산 계획 공지",
content: "2025년 1분기 생산 계획이 확정되었습니다. 각 부서는 계획에 따라 준비하시기 바랍니다.",
author: "김경영",
department: "경영팀",
date: "2025-09-25",
views: 45,
isPinned: true,
isImportant: true,
category: "일반공지"
},
{
id: "N002",
title: "안전교육 실시 안내",
content: "월간 안전교육을 다음 주에 실시합니다. 전직원 필수 참석 바랍니다.",
author: "박안전",
department: "안전관리팀",
date: "2025-09-24",
views: 32,
isPinned: true,
isImportant: false,
category: "안전공지"
},
{
id: "N003",
title: "신규 설비 도입 완료",
content: "CNC 머시닝센터 3호기 설치가 완료되었습니다.",
author: "이설비",
department: "설비팀",
date: "2025-09-23",
views: 28,
isPinned: false,
isImportant: false,
category: "업무공지"
},
{
id: "N004",
title: "시스템 업데이트 예정",
content: "SAM 시스템 정기 업데이트가 금요일 밤에 진행됩니다.",
author: "최IT",
department: "IT팀",
date: "2025-09-22",
views: 67,
isPinned: false,
isImportant: true,
category: "시스템"
}
]);
const [documents, setDocuments] = useState<Document[]>([
{
id: "D001",
title: "품질관리 매뉴얼 v2.1",
description: "품질관리 표준 작업 절차서 및 체크리스트",
fileName: "QMS_Manual_v2.1.pdf",
fileSize: "2.4MB",
version: "v2.1",
author: "박품질",
uploadDate: "2025-09-20",
downloads: 23,
category: "매뉴얼",
accessLevel: "전체"
},
{
id: "D002",
title: "생산 공정도 템플릿",
description: "표준 생산 공정도 작성 템플릿",
fileName: "Process_Template.xlsx",
fileSize: "156KB",
version: "v1.3",
author: "이생산",
uploadDate: "2025-09-18",
downloads: 15,
category: "템플릿",
accessLevel: "생산팀"
},
{
id: "D003",
title: "안전관리 체크리스트",
description: "일일 안전점검 체크리스트 양식",
fileName: "Safety_Checklist.pdf",
fileSize: "890KB",
version: "v1.0",
author: "박안전",
uploadDate: "2025-09-15",
downloads: 41,
category: "안전자료",
accessLevel: "전체"
}
]);
const [approvals, setApprovals] = useState<Approval[]>([
{
id: "A001",
title: "원자재 구매 품의",
type: "구매품의",
requestor: "최자재",
department: "자재팀",
requestDate: "2025-09-25",
status: "결재대기",
currentApprover: "김부장",
amount: 15000000,
urgency: "긴급"
},
{
id: "A002",
title: "설비 수리비 지출 결의",
type: "지출결의",
requestor: "정설비",
department: "설비팀",
requestDate: "2025-09-24",
status: "승인완료",
currentApprover: "-",
amount: 2500000,
urgency: "보통"
},
{
id: "A003",
title: "신제품 개발 품의",
type: "개발품의",
requestor: "김개발",
department: "개발팀",
requestDate: "2025-09-23",
status: "검토중",
currentApprover: "이이사",
urgency: "보통"
}
]);
const [activeTab, setActiveTab] = useState("notices");
const [isModalOpen, setIsModalOpen] = useState(false);
const [selectedItem, setSelectedItem] = useState<any>(null);
const [isViewModalOpen, setIsViewModalOpen] = useState(false);
const getCategoryColor = (category: string) => {
switch (category) {
case "일반공지": return "bg-blue-500";
case "안전공지": return "bg-red-500";
case "업무공지": return "bg-green-500";
case "시스템": return "bg-purple-500";
default: return "bg-gray-500";
}
};
const getStatusColor = (status: string) => {
switch (status) {
case "결재대기": return "bg-yellow-500";
case "승인완료": return "bg-green-500";
case "반려": return "bg-red-500";
case "검토중": return "bg-blue-500";
default: return "bg-gray-500";
}
};
const getUrgencyColor = (urgency: string) => {
switch (urgency) {
case "긴급": return "text-red-600";
case "보통": return "text-yellow-600";
case "낮음": return "text-green-600";
default: return "text-gray-600";
}
};
const handleViewItem = (item: any) => {
setSelectedItem(item);
setIsViewModalOpen(true);
// 조회수 증가 (공지사항의 경우)
if (activeTab === "notices" && item.id) {
setNotices(prev => prev.map(notice =>
notice.id === item.id ? { ...notice, views: notice.views + 1 } : notice
));
}
};
return (
<div className="p-4 md:p-8 space-y-6 md:space-y-8">
{/* 헤더 */}
<div className="samsung-card samsung-gradient-card relative overflow-hidden">
<div className="absolute top-0 left-0 right-0 h-2 samsung-hero-gradient"></div>
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6 pt-2">
<div>
<h1 className="text-3xl md:text-4xl font-bold text-foreground mb-2">·</h1>
<p className="text-muted-foreground text-lg">, , </p>
</div>
<Button
className="samsung-button w-full md:w-auto min-h-[48px]"
onClick={() => setIsModalOpen(true)}
>
<Plus className="h-5 w-5 mr-3" />
</Button>
</div>
</div>
{/* 협업 대시보드 */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 md:gap-8 mb-8">
<Card className="samsung-card samsung-gradient-card hover:scale-105 hover:-translate-y-2 transition-all duration-500 border-0 group">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-4">
<CardTitle className="text-sm font-bold text-muted-foreground uppercase tracking-wide"> </CardTitle>
<div className="w-12 h-12 bg-gradient-to-br from-blue-500 to-indigo-600 rounded-2xl flex items-center justify-center samsung-shadow group-hover:scale-110 transition-transform duration-300">
<Bell className="h-6 w-6 text-white" />
</div>
</CardHeader>
<CardContent>
<div className="text-3xl font-black text-blue-600 mb-3">
{notices.filter(n => {
const today = new Date();
const noticeDate = new Date(n.date);
const diffTime = Math.abs(today.getTime() - noticeDate.getTime());
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
return diffDays <= 7;
}).length}
</div>
<p className="text-sm text-muted-foreground bg-gradient-to-r from-blue-50 to-indigo-50 rounded-xl px-3 py-2 font-semibold">
7
</p>
</CardContent>
</Card>
<Card className="samsung-card samsung-gradient-card hover:scale-105 hover:-translate-y-2 transition-all duration-500 border-0 group">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-4">
<CardTitle className="text-sm font-bold text-muted-foreground uppercase tracking-wide"></CardTitle>
<div className="w-12 h-12 bg-gradient-to-br from-green-500 to-emerald-600 rounded-2xl flex items-center justify-center samsung-shadow group-hover:scale-110 transition-transform duration-300">
<FileText className="h-6 w-6 text-white" />
</div>
</CardHeader>
<CardContent>
<div className="text-3xl font-black text-green-600 mb-3">{documents.length}</div>
<p className="text-sm text-muted-foreground bg-gradient-to-r from-green-50 to-emerald-50 rounded-xl px-3 py-2 font-semibold">
</p>
</CardContent>
</Card>
<Card className="samsung-card samsung-gradient-card hover:scale-105 hover:-translate-y-2 transition-all duration-500 border-0 group">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-4">
<CardTitle className="text-sm font-bold text-muted-foreground uppercase tracking-wide"> </CardTitle>
<div className="w-12 h-12 bg-gradient-to-br from-orange-500 to-red-500 rounded-2xl flex items-center justify-center samsung-shadow group-hover:scale-110 transition-transform duration-300">
<MessageSquare className="h-6 w-6 text-white" />
</div>
</CardHeader>
<CardContent>
<div className="text-3xl font-black text-orange-600 mb-3">
{approvals.filter(a => a.status === "결재대기").length}
</div>
<p className="text-sm text-muted-foreground bg-gradient-to-r from-orange-50 to-red-50 rounded-xl px-3 py-2 font-semibold">
</p>
</CardContent>
</Card>
<Card className="samsung-card samsung-gradient-card hover:scale-105 hover:-translate-y-2 transition-all duration-500 border-0 group">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-4">
<CardTitle className="text-sm font-bold text-muted-foreground uppercase tracking-wide"> </CardTitle>
<div className="w-12 h-12 bg-gradient-to-br from-red-500 to-pink-500 rounded-2xl flex items-center justify-center samsung-shadow group-hover:scale-110 transition-transform duration-300">
<Calendar className="h-6 w-6 text-white" />
</div>
</CardHeader>
<CardContent>
<div className="text-3xl font-black text-red-600 mb-3">
{approvals.filter(a => a.urgency === "긴급").length}
</div>
<p className="text-sm text-muted-foreground bg-gradient-to-r from-red-50 to-pink-50 rounded-xl px-3 py-2 font-semibold">
</p>
</CardContent>
</Card>
</div>
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-6">
<div className="overflow-x-auto">
<TabsList className="grid w-full grid-cols-3 min-w-[400px] samsung-glass h-14 p-2 rounded-2xl">
<TabsTrigger value="notices" className="flex items-center space-x-3 rounded-xl h-10 transition-all duration-300 data-[state=active]:samsung-hero-gradient data-[state=active]:text-white">
<Bell className="h-5 w-5" />
<span className="hidden sm:inline font-semibold"></span>
<span className="sm:hidden font-semibold"></span>
</TabsTrigger>
<TabsTrigger value="documents" className="flex items-center space-x-3 rounded-xl h-10 transition-all duration-300 data-[state=active]:samsung-hero-gradient data-[state=active]:text-white">
<FileText className="h-5 w-5" />
<span className="hidden sm:inline font-semibold"></span>
<span className="sm:hidden font-semibold"></span>
</TabsTrigger>
<TabsTrigger value="approvals" className="flex items-center space-x-3 rounded-xl h-10 transition-all duration-300 data-[state=active]:samsung-hero-gradient data-[state=active]:text-white">
<MessageSquare className="h-5 w-5" />
<span className="hidden sm:inline font-semibold"></span>
<span className="sm:hidden font-semibold"></span>
</TabsTrigger>
</TabsList>
</div>
<TabsContent value="notices" className="space-y-6">
<Card className="samsung-card samsung-gradient-card border-0">
<CardHeader>
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6">
<CardTitle className="text-2xl font-bold text-foreground">📢 </CardTitle>
<div className="flex flex-col md:flex-row items-stretch md:items-center space-y-3 md:space-y-0 md:space-x-4">
<div className="relative">
<Search className="absolute left-4 top-1/2 transform -translate-y-1/2 text-muted-foreground h-5 w-5" />
<Input placeholder="제목 검색..." className="pl-12 w-full md:w-80 samsung-input border-0" />
</div>
<Select defaultValue="all">
<SelectTrigger className="w-full md:w-40 samsung-input border-0">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="일반공지"></SelectItem>
<SelectItem value="안전공지"></SelectItem>
<SelectItem value="업무공지"></SelectItem>
<SelectItem value="시스템"></SelectItem>
</SelectContent>
</Select>
</div>
</div>
</CardHeader>
<CardContent>
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead className="min-w-[60px]"></TableHead>
<TableHead className="min-w-[80px]"></TableHead>
<TableHead className="min-w-[200px]"></TableHead>
<TableHead className="min-w-[80px]"></TableHead>
<TableHead className="min-w-[100px]"></TableHead>
<TableHead className="min-w-[100px]"></TableHead>
<TableHead className="min-w-[60px]"></TableHead>
<TableHead className="min-w-[80px]"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{notices.map((notice) => (
<TableRow key={notice.id}>
<TableCell>
<div className="flex items-center space-x-1">
{notice.isPinned && (
<Pin className="h-3 w-3 text-red-500" />
)}
{notice.isImportant && (
<span className="text-red-500 text-xs"></span>
)}
</div>
</TableCell>
<TableCell>
<Badge className={`${getCategoryColor(notice.category)} text-white text-xs`}>
{notice.category}
</Badge>
</TableCell>
<TableCell>
<button
onClick={() => handleViewItem(notice)}
className="text-left hover:text-blue-600 hover:underline"
>
{notice.title}
</button>
</TableCell>
<TableCell>{notice.author}</TableCell>
<TableCell>{notice.department}</TableCell>
<TableCell>{notice.date}</TableCell>
<TableCell>{notice.views}</TableCell>
<TableCell>
<div className="flex items-center space-x-1">
<Button
size="sm"
variant="outline"
onClick={() => handleViewItem(notice)}
className="p-3 rounded-xl hover:scale-105 transition-all duration-300 border-primary/20 hover:bg-primary/10"
>
<Eye className="h-4 w-4" />
</Button>
<Button size="sm" variant="outline" className="p-3 rounded-xl hover:scale-105 transition-all duration-300 border-primary/20 hover:bg-primary/10">
<Edit className="h-4 w-4" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="documents" className="space-y-6">
<Card className="samsung-card samsung-gradient-card border-0">
<CardHeader>
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6">
<CardTitle className="text-2xl font-bold text-foreground">📁 </CardTitle>
<Button className="samsung-button w-full md:w-auto min-h-[48px]">
<Upload className="h-5 w-5 mr-3" />
</Button>
</div>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{documents.map((doc) => (
<Card key={doc.id} className="samsung-card samsung-gradient-card border-0 p-6 hover:scale-105 hover:-translate-y-2 transition-all duration-500 group">
<div className="flex items-start justify-between mb-4">
<div className="flex-1">
<h4 className="font-bold text-base mb-2 text-foreground">{doc.title}</h4>
<p className="text-sm text-muted-foreground mb-3">{doc.description}</p>
<div className="flex items-center space-x-2 text-sm text-muted-foreground bg-muted/50 rounded-xl px-3 py-2">
<span>{doc.fileName}</span>
<span></span>
<span className="font-semibold">{doc.fileSize}</span>
</div>
</div>
<Badge variant="outline" className="text-sm font-semibold px-3 py-1 rounded-xl">
{doc.version}
</Badge>
</div>
<div className="flex justify-between items-center text-sm text-muted-foreground mb-4">
<span className="font-semibold">{doc.author}</span>
<span>{doc.uploadDate}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground">: <span className="font-bold text-primary">{doc.downloads}</span></span>
<div className="flex space-x-2">
<Button size="sm" variant="outline" className="p-3 rounded-xl hover:scale-105 transition-all duration-300 border-primary/20 hover:bg-primary/10">
<Download className="h-4 w-4" />
</Button>
<Button size="sm" variant="outline" className="p-3 rounded-xl hover:scale-105 transition-all duration-300 border-primary/20 hover:bg-primary/10">
<Eye className="h-4 w-4" />
</Button>
</div>
</div>
</Card>
))}
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="approvals" className="space-y-6">
<Card className="samsung-card samsung-gradient-card border-0">
<CardHeader>
<CardTitle className="text-2xl font-bold text-foreground">📋 </CardTitle>
</CardHeader>
<CardContent>
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead className="min-w-[100px]"></TableHead>
<TableHead className="min-w-[150px]"></TableHead>
<TableHead className="min-w-[80px]"></TableHead>
<TableHead className="min-w-[80px]"></TableHead>
<TableHead className="min-w-[80px]"></TableHead>
<TableHead className="min-w-[100px]"></TableHead>
<TableHead className="min-w-[80px]"></TableHead>
<TableHead className="min-w-[100px]"></TableHead>
<TableHead className="min-w-[80px]"></TableHead>
<TableHead className="min-w-[80px]"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{approvals.map((approval) => (
<TableRow key={approval.id}>
<TableCell className="font-medium">{approval.id}</TableCell>
<TableCell>
<button
onClick={() => handleViewItem(approval)}
className="text-left hover:text-blue-600 hover:underline"
>
{approval.title}
</button>
</TableCell>
<TableCell>{approval.type}</TableCell>
<TableCell>{approval.requestor}</TableCell>
<TableCell>{approval.department}</TableCell>
<TableCell>{approval.requestDate}</TableCell>
<TableCell>
<Badge className={`${getStatusColor(approval.status)} text-white text-xs`}>
{approval.status}
</Badge>
</TableCell>
<TableCell>{approval.currentApprover}</TableCell>
<TableCell>
<span className={getUrgencyColor(approval.urgency)}>
{approval.urgency}
</span>
</TableCell>
<TableCell>
<Button
size="sm"
variant="outline"
onClick={() => handleViewItem(approval)}
className="p-3 rounded-xl hover:scale-105 transition-all duration-300 border-primary/20 hover:bg-primary/10"
>
<Eye className="h-4 w-4" />
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
{/* 상세보기 모달 */}
<Dialog open={isViewModalOpen} onOpenChange={setIsViewModalOpen}>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto samsung-glass rounded-3xl border-0">
<DialogHeader>
<DialogTitle>
{activeTab === "notices" && "공지사항 상세"}
{activeTab === "documents" && "문서 상세"}
{activeTab === "approvals" && "결재 상세"}
</DialogTitle>
<DialogDescription>
{activeTab === "notices" && "공지사항의 상세 내용을 확인합니다."}
{activeTab === "documents" && "문서의 상세 정보를 확인합니다."}
{activeTab === "approvals" && "결재 건의 상세 내용을 확인합니다."}
</DialogDescription>
</DialogHeader>
{selectedItem && (
<div className="space-y-6">
{activeTab === "notices" && (
<div>
<div className="border-b pb-4 mb-4">
<div className="flex items-center space-x-2 mb-2">
<h2 className="text-xl font-bold">{selectedItem.title}</h2>
{selectedItem.isPinned && <Pin className="h-4 w-4 text-red-500" />}
{selectedItem.isImportant && <span className="text-red-500"></span>}
</div>
<div className="flex items-center space-x-4 text-sm text-gray-600">
<span>: {selectedItem.author}</span>
<span>: {selectedItem.department}</span>
<span>: {selectedItem.date}</span>
<span>: {selectedItem.views}</span>
</div>
</div>
<div className="prose max-w-none">
<p>{selectedItem.content}</p>
</div>
</div>
)}
{activeTab === "approvals" && (
<div>
<div className="border-b pb-4 mb-4">
<h2 className="text-xl font-bold mb-2">{selectedItem.title}</h2>
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="text-gray-600">:</span> {selectedItem.id}
</div>
<div>
<span className="text-gray-600">:</span> {selectedItem.type}
</div>
<div>
<span className="text-gray-600">:</span> {selectedItem.requestor}
</div>
<div>
<span className="text-gray-600">:</span> {selectedItem.department}
</div>
<div>
<span className="text-gray-600">:</span> {selectedItem.requestDate}
</div>
<div>
<span className="text-gray-600">:</span>
<Badge className={`ml-2 ${getStatusColor(selectedItem.status)} text-white text-xs`}>
{selectedItem.status}
</Badge>
</div>
{selectedItem.amount && (
<div>
<span className="text-gray-600">:</span> {selectedItem.amount.toLocaleString()}
</div>
)}
<div>
<span className="text-gray-600">:</span>
<span className={getUrgencyColor(selectedItem.urgency)}> {selectedItem.urgency}</span>
</div>
</div>
</div>
{selectedItem.status === "결재대기" && (
<div className="flex space-x-4">
<Button className="samsung-button bg-gradient-to-r from-green-500 to-emerald-600"></Button>
<Button variant="outline" className="samsung-button-secondary border-red-500 text-red-600 hover:bg-red-50"></Button>
</div>
)}
</div>
)}
</div>
)}
<div className="flex justify-end">
<Button variant="outline" onClick={() => setIsViewModalOpen(false)} className="samsung-button-secondary"></Button>
</div>
</DialogContent>
</Dialog>
</div>
);
}

View File

@@ -1,659 +0,0 @@
import { useState } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog";
import { Label } from "@/components/ui/label";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
Code,
Plus,
Search,
Edit,
Trash2,
FolderTree,
Tag,
CheckCircle,
XCircle
} from "lucide-react";
interface CodeGroup {
id: string;
groupCode: string;
groupName: string;
description: string;
itemCount: number;
isActive: boolean;
}
interface CodeItem {
id: string;
groupCode: string;
itemCode: string;
itemName: string;
description: string;
sortOrder: number;
isActive: boolean;
}
export function CodeManagement() {
const [searchTerm, setSearchTerm] = useState("");
const [isAddGroupDialogOpen, setIsAddGroupDialogOpen] = useState(false);
const [isAddItemDialogOpen, setIsAddItemDialogOpen] = useState(false);
const [selectedGroup, setSelectedGroup] = useState("");
const [isEditGroupDialogOpen, setIsEditGroupDialogOpen] = useState(false);
const [isEditItemDialogOpen, setIsEditItemDialogOpen] = useState(false);
const [isDeleteGroupDialogOpen, setIsDeleteGroupDialogOpen] = useState(false);
const [isDeleteItemDialogOpen, setIsDeleteItemDialogOpen] = useState(false);
const [selectedCodeGroup, setSelectedCodeGroup] = useState<CodeGroup | null>(null);
const [selectedCodeItem, setSelectedCodeItem] = useState<CodeItem | null>(null);
const [codeGroups, setCodeGroups] = useState<CodeGroup[]>([
{
id: "1",
groupCode: "ITEM_TYPE",
groupName: "품목 유형",
description: "원자재, 부자재, 반제품, 완제품 구분",
itemCount: 4,
isActive: true
},
{
id: "2",
groupCode: "UNIT",
groupName: "단위",
description: "재고 및 거래 단위",
itemCount: 8,
isActive: true
},
{
id: "3",
groupCode: "PROCESS_STATUS",
groupName: "공정 상태",
description: "생산 공정 진행 상태",
itemCount: 6,
isActive: true
},
{
id: "4",
groupCode: "QUALITY_GRADE",
groupName: "품질 등급",
description: "제품 품질 등급 분류",
itemCount: 3,
isActive: true
},
{
id: "5",
groupCode: "CUSTOMER_TYPE",
groupName: "고객 유형",
description: "고객 분류 코드",
itemCount: 5,
isActive: true
}
]);
const [codeItems, setCodeItems] = useState<CodeItem[]>([
// 품목 유형
{ id: "1", groupCode: "ITEM_TYPE", itemCode: "RAW", itemName: "원자재", description: "가공되지 않은 원재료", sortOrder: 1, isActive: true },
{ id: "2", groupCode: "ITEM_TYPE", itemCode: "SUB", itemName: "부자재", description: "보조 자재", sortOrder: 2, isActive: true },
{ id: "3", groupCode: "ITEM_TYPE", itemCode: "SEMI", itemName: "반제품", description: "중간 생산품", sortOrder: 3, isActive: true },
{ id: "4", groupCode: "ITEM_TYPE", itemCode: "FINISHED", itemName: "완제품", description: "최종 제품", sortOrder: 4, isActive: true },
// 단위
{ id: "5", groupCode: "UNIT", itemCode: "EA", itemName: "개", description: "낱개 단위", sortOrder: 1, isActive: true },
{ id: "6", groupCode: "UNIT", itemCode: "KG", itemName: "킬로그램", description: "무게 단위", sortOrder: 2, isActive: true },
{ id: "7", groupCode: "UNIT", itemCode: "M", itemName: "미터", description: "길이 단위", sortOrder: 3, isActive: true },
{ id: "8", groupCode: "UNIT", itemCode: "BOX", itemName: "박스", description: "포장 단위", sortOrder: 4, isActive: true },
// 공정 상태
{ id: "9", groupCode: "PROCESS_STATUS", itemCode: "READY", itemName: "준비", description: "작업 준비 중", sortOrder: 1, isActive: true },
{ id: "10", groupCode: "PROCESS_STATUS", itemCode: "PROGRESS", itemName: "진행", description: "작업 진행 중", sortOrder: 2, isActive: true },
{ id: "11", groupCode: "PROCESS_STATUS", itemCode: "COMPLETE", itemName: "완료", description: "작업 완료", sortOrder: 3, isActive: true },
{ id: "12", groupCode: "PROCESS_STATUS", itemCode: "HOLD", itemName: "보류", description: "일시 중단", sortOrder: 4, isActive: true },
// 품질 등급
{ id: "13", groupCode: "QUALITY_GRADE", itemCode: "A", itemName: "A등급", description: "최상급", sortOrder: 1, isActive: true },
{ id: "14", groupCode: "QUALITY_GRADE", itemCode: "B", itemName: "B등급", description: "우수", sortOrder: 2, isActive: true },
{ id: "15", groupCode: "QUALITY_GRADE", itemCode: "C", itemName: "C등급", description: "양호", sortOrder: 3, isActive: true },
]);
const handleEditGroup = (group: CodeGroup) => {
setSelectedCodeGroup(group);
setIsEditGroupDialogOpen(true);
};
const handleDeleteGroup = (group: CodeGroup) => {
setSelectedCodeGroup(group);
setIsDeleteGroupDialogOpen(true);
};
const handleEditItem = (item: CodeItem) => {
setSelectedCodeItem(item);
setIsEditItemDialogOpen(true);
};
const handleDeleteItem = (item: CodeItem) => {
setSelectedCodeItem(item);
setIsDeleteItemDialogOpen(true);
};
const confirmDeleteGroup = () => {
if (selectedCodeGroup) {
setCodeGroups(codeGroups.filter(g => g.id !== selectedCodeGroup.id));
setIsDeleteGroupDialogOpen(false);
setSelectedCodeGroup(null);
}
};
const confirmDeleteItem = () => {
if (selectedCodeItem) {
setCodeItems(codeItems.filter(i => i.id !== selectedCodeItem.id));
setIsDeleteItemDialogOpen(false);
setSelectedCodeItem(null);
}
};
const saveEditGroup = () => {
if (selectedCodeGroup) {
setCodeGroups(codeGroups.map(g =>
g.id === selectedCodeGroup.id ? selectedCodeGroup : g
));
setIsEditGroupDialogOpen(false);
setSelectedCodeGroup(null);
}
};
const saveEditItem = () => {
if (selectedCodeItem) {
setCodeItems(codeItems.map(i =>
i.id === selectedCodeItem.id ? selectedCodeItem : i
));
setIsEditItemDialogOpen(false);
setSelectedCodeItem(null);
}
};
const filteredGroups = codeGroups.filter(group =>
group.groupName.toLowerCase().includes(searchTerm.toLowerCase()) ||
group.groupCode.toLowerCase().includes(searchTerm.toLowerCase())
);
const filteredItems = selectedGroup
? codeItems.filter(item => item.groupCode === selectedGroup)
: codeItems;
return (
<div className="p-6 space-y-6">
{/* 헤더 */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<h1 className="text-3xl font-bold text-foreground flex items-center gap-3">
<div className="w-10 h-10 bg-teal-100 rounded-lg flex items-center justify-center">
<Code className="h-6 w-6 text-teal-600" />
</div>
</h1>
<p className="text-muted-foreground mt-1"> </p>
</div>
<div className="flex gap-2">
<Dialog open={isAddGroupDialogOpen} onOpenChange={setIsAddGroupDialogOpen}>
<DialogTrigger asChild>
<Button variant="outline">
<FolderTree className="h-4 w-4 mr-2" />
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle> </DialogTitle>
<DialogDescription>
.
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label> *</Label>
<Input placeholder="예: ITEM_TYPE" />
</div>
<div className="space-y-2">
<Label> *</Label>
<Input placeholder="예: 품목 유형" />
</div>
<div className="space-y-2">
<Label></Label>
<Input placeholder="코드 그룹 설명" />
</div>
</div>
<div className="flex justify-end gap-2">
<Button variant="outline" onClick={() => setIsAddGroupDialogOpen(false)}>
</Button>
<Button className="bg-teal-600 hover:bg-teal-700">
</Button>
</div>
</DialogContent>
</Dialog>
<Dialog open={isAddItemDialogOpen} onOpenChange={setIsAddItemDialogOpen}>
<DialogTrigger asChild>
<Button className="bg-teal-600 hover:bg-teal-700">
<Plus className="h-4 w-4 mr-2" />
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle> </DialogTitle>
<DialogDescription>
.
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label> *</Label>
<Input placeholder="ITEM_TYPE" />
</div>
<div className="space-y-2">
<Label> *</Label>
<Input placeholder="RAW" />
</div>
<div className="space-y-2">
<Label> *</Label>
<Input placeholder="원자재" />
</div>
<div className="space-y-2">
<Label></Label>
<Input placeholder="코드 설명" />
</div>
<div className="space-y-2">
<Label> </Label>
<Input type="number" defaultValue="1" />
</div>
</div>
<div className="flex justify-end gap-2">
<Button variant="outline" onClick={() => setIsAddItemDialogOpen(false)}>
</Button>
<Button className="bg-teal-600 hover:bg-teal-700">
</Button>
</div>
</DialogContent>
</Dialog>
</div>
</div>
{/* 통계 카드 */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
<Card className="border-l-4 border-l-teal-500">
<CardHeader className="pb-3">
<CardTitle className="text-sm text-muted-foreground"> </CardTitle>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-teal-600">{codeGroups.length}</div>
<p className="text-xs text-muted-foreground mt-1"> </p>
</CardContent>
</Card>
<Card className="border-l-4 border-l-blue-500">
<CardHeader className="pb-3">
<CardTitle className="text-sm text-muted-foreground"> </CardTitle>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-blue-600">{codeItems.length}</div>
<p className="text-xs text-muted-foreground mt-1"> </p>
</CardContent>
</Card>
<Card className="border-l-4 border-l-green-500">
<CardHeader className="pb-3">
<CardTitle className="text-sm text-muted-foreground"></CardTitle>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-green-600">
{codeGroups.filter(g => g.isActive).length}
</div>
<p className="text-xs text-muted-foreground mt-1"> </p>
</CardContent>
</Card>
<Card className="border-l-4 border-l-purple-500">
<CardHeader className="pb-3">
<CardTitle className="text-sm text-muted-foreground"> </CardTitle>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-purple-600">
{Math.round(codeItems.length / codeGroups.length)}
</div>
<p className="text-xs text-muted-foreground mt-1"> </p>
</CardContent>
</Card>
</div>
{/* 탭 */}
<Tabs defaultValue="groups" className="space-y-6">
<TabsList>
<TabsTrigger value="groups">
<FolderTree className="h-4 w-4 mr-2" />
</TabsTrigger>
<TabsTrigger value="items">
<Tag className="h-4 w-4 mr-2" />
</TabsTrigger>
</TabsList>
{/* 코드 그룹 탭 */}
<TabsContent value="groups" className="space-y-6">
<Card>
<CardContent className="pt-6">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="그룹명 또는 그룹 코드로 검색..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle> ({filteredGroups.length})</CardTitle>
</CardHeader>
<CardContent>
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-40"> </TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="text-center"> </TableHead>
<TableHead className="text-center"></TableHead>
<TableHead className="text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredGroups.map((group) => (
<TableRow key={group.id}>
<TableCell className="font-mono text-sm font-medium">
{group.groupCode}
</TableCell>
<TableCell className="font-medium">{group.groupName}</TableCell>
<TableCell className="text-sm text-muted-foreground">
{group.description}
</TableCell>
<TableCell className="text-center">
<Badge variant="outline">{group.itemCount}</Badge>
</TableCell>
<TableCell className="text-center">
{group.isActive ? (
<Badge className="bg-green-500 text-white">
<CheckCircle className="h-3 w-3 mr-1" />
</Badge>
) : (
<Badge className="bg-gray-500 text-white">
<XCircle className="h-3 w-3 mr-1" />
</Badge>
)}
</TableCell>
<TableCell className="text-right">
<div className="flex justify-end gap-2">
<Button variant="ghost" size="sm">
<Edit className="h-4 w-4" />
</Button>
<Button variant="ghost" size="sm">
<Trash2 className="h-4 w-4 text-red-500" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
</TabsContent>
{/* 코드 항목 탭 */}
<TabsContent value="items" className="space-y-6">
<Card>
<CardContent className="pt-6">
<div className="flex gap-4">
<div className="flex-1 relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="코드 또는 항목명으로 검색..."
className="pl-10"
/>
</div>
<Button
variant={selectedGroup ? "default" : "outline"}
onClick={() => setSelectedGroup("")}
>
</Button>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle> ({filteredItems.length})</CardTitle>
</CardHeader>
<CardContent>
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-32"> </TableHead>
<TableHead className="w-32"> </TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="text-center"></TableHead>
<TableHead className="text-center"></TableHead>
<TableHead className="text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredItems.map((item) => (
<TableRow key={item.id}>
<TableCell className="font-mono text-xs text-muted-foreground">
{item.groupCode}
</TableCell>
<TableCell className="font-mono text-sm font-medium text-teal-600">
{item.itemCode}
</TableCell>
<TableCell className="font-medium">{item.itemName}</TableCell>
<TableCell className="text-sm text-muted-foreground">
{item.description}
</TableCell>
<TableCell className="text-center text-sm">
{item.sortOrder}
</TableCell>
<TableCell className="text-center">
{item.isActive ? (
<Badge className="bg-green-500 text-white text-xs">
<CheckCircle className="h-3 w-3 mr-1" />
</Badge>
) : (
<Badge className="bg-gray-500 text-white text-xs">
<XCircle className="h-3 w-3 mr-1" />
</Badge>
)}
</TableCell>
<TableCell className="text-right">
<div className="flex justify-end gap-2">
<Button variant="ghost" size="sm">
<Edit className="h-4 w-4" />
</Button>
<Button variant="ghost" size="sm">
<Trash2 className="h-4 w-4 text-red-500" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
{/* 코드 그룹 수정 다이얼로그 */}
<Dialog open={isEditGroupDialogOpen} onOpenChange={setIsEditGroupDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle> </DialogTitle>
<DialogDescription>
.
</DialogDescription>
</DialogHeader>
{selectedCodeGroup && (
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label> </Label>
<Input
value={selectedCodeGroup.groupCode}
onChange={(e) => setSelectedCodeGroup({...selectedCodeGroup, groupCode: e.target.value})}
/>
</div>
<div className="space-y-2">
<Label></Label>
<Input
value={selectedCodeGroup.groupName}
onChange={(e) => setSelectedCodeGroup({...selectedCodeGroup, groupName: e.target.value})}
/>
</div>
<div className="space-y-2">
<Label></Label>
<Input
value={selectedCodeGroup.description}
onChange={(e) => setSelectedCodeGroup({...selectedCodeGroup, description: e.target.value})}
/>
</div>
</div>
)}
<div className="flex justify-end gap-2">
<Button variant="outline" onClick={() => setIsEditGroupDialogOpen(false)}>
</Button>
<Button className="bg-teal-600 hover:bg-teal-700" onClick={saveEditGroup}>
</Button>
</div>
</DialogContent>
</Dialog>
{/* 코드 항목 수정 다이얼로그 */}
<Dialog open={isEditItemDialogOpen} onOpenChange={setIsEditItemDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle> </DialogTitle>
<DialogDescription>
.
</DialogDescription>
</DialogHeader>
{selectedCodeItem && (
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label> </Label>
<Input
value={selectedCodeItem.itemCode}
onChange={(e) => setSelectedCodeItem({...selectedCodeItem, itemCode: e.target.value})}
/>
</div>
<div className="space-y-2">
<Label></Label>
<Input
value={selectedCodeItem.itemName}
onChange={(e) => setSelectedCodeItem({...selectedCodeItem, itemName: e.target.value})}
/>
</div>
<div className="space-y-2">
<Label></Label>
<Input
value={selectedCodeItem.description}
onChange={(e) => setSelectedCodeItem({...selectedCodeItem, description: e.target.value})}
/>
</div>
<div className="space-y-2">
<Label> </Label>
<Input
type="number"
value={selectedCodeItem.sortOrder}
onChange={(e) => setSelectedCodeItem({...selectedCodeItem, sortOrder: parseInt(e.target.value)})}
/>
</div>
</div>
)}
<div className="flex justify-end gap-2">
<Button variant="outline" onClick={() => setIsEditItemDialogOpen(false)}>
</Button>
<Button className="bg-teal-600 hover:bg-teal-700" onClick={saveEditItem}>
</Button>
</div>
</DialogContent>
</Dialog>
{/* 코드 그룹 삭제 확인 다이얼로그 */}
<AlertDialog open={isDeleteGroupDialogOpen} onOpenChange={setIsDeleteGroupDialogOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle> </AlertDialogTitle>
<AlertDialogDescription>
"{selectedCodeGroup?.groupName}" ?<br/>
.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogAction
onClick={confirmDeleteGroup}
className="bg-red-600 hover:bg-red-700"
>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
{/* 코드 항목 삭제 확인 다이얼로그 */}
<AlertDialog open={isDeleteItemDialogOpen} onOpenChange={setIsDeleteItemDialogOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle> </AlertDialogTitle>
<AlertDialogDescription>
"{selectedCodeItem?.itemName}" ?<br/>
.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogAction
onClick={confirmDeleteItem}
className="bg-red-600 hover:bg-red-700"
>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
);
}

View File

@@ -1,202 +0,0 @@
import { useState } from "react";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Building2, Mail, Phone, Briefcase, MessageSquare } from "lucide-react";
interface ContactModalProps {
isOpen: boolean;
onClose: () => void;
title?: string;
description?: string;
}
export function ContactModal({ isOpen, onClose, title, description }: ContactModalProps) {
const [formData, setFormData] = useState({
name: "",
company: "",
email: "",
phone: "",
industry: "",
message: ""
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
// localStorage에 리드 저장
const leadId = `lead_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const newLead = {
id: leadId,
...formData,
status: "pending",
submittedAt: new Date().toISOString(),
};
const existingLeads = JSON.parse(localStorage.getItem("salesLeads") || "[]");
existingLeads.push(newLead);
localStorage.setItem("salesLeads", JSON.stringify(existingLeads));
console.log("Form submitted:", formData);
alert("데모 요청이 접수되었습니다. 1영업일 내에 연락드리겠습니다.");
onClose();
// 폼 초기화
setFormData({
name: "",
company: "",
email: "",
phone: "",
industry: "",
message: ""
});
};
const handleChange = (field: string, value: string) => {
setFormData(prev => ({ ...prev, [field]: value }));
};
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="sm:max-w-[600px] max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="text-2xl font-bold">
{title || "데모 요청하기"}
</DialogTitle>
<DialogDescription className="text-base">
{description || "귀사의 정보를 입력해주시면 전문 영업사원이 1영업일 내에 연락드립니다."}
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-6 mt-4">
{/* 이름 */}
<div className="space-y-2">
<Label htmlFor="name" className="text-sm font-semibold flex items-center gap-2">
<Building2 className="w-4 h-4 text-blue-600" />
*
</Label>
<Input
id="name"
placeholder="홍길동"
value={formData.name}
onChange={(e) => handleChange("name", e.target.value)}
required
className="h-11"
/>
</div>
{/* 회사명 */}
<div className="space-y-2">
<Label htmlFor="company" className="text-sm font-semibold flex items-center gap-2">
<Building2 className="w-4 h-4 text-blue-600" />
*
</Label>
<Input
id="company"
placeholder="(주)샘플컴퍼니"
value={formData.company}
onChange={(e) => handleChange("company", e.target.value)}
required
className="h-11"
/>
</div>
{/* 이메일 & 연락처 */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="email" className="text-sm font-semibold flex items-center gap-2">
<Mail className="w-4 h-4 text-blue-600" />
*
</Label>
<Input
id="email"
type="email"
placeholder="contact@company.com"
value={formData.email}
onChange={(e) => handleChange("email", e.target.value)}
required
className="h-11"
/>
</div>
<div className="space-y-2">
<Label htmlFor="phone" className="text-sm font-semibold flex items-center gap-2">
<Phone className="w-4 h-4 text-blue-600" />
*
</Label>
<Input
id="phone"
type="tel"
placeholder="010-1234-5678"
value={formData.phone}
onChange={(e) => handleChange("phone", e.target.value)}
required
className="h-11"
/>
</div>
</div>
{/* 산업 분야 */}
<div className="space-y-2">
<Label htmlFor="industry" className="text-sm font-semibold flex items-center gap-2">
<Briefcase className="w-4 h-4 text-blue-600" />
*
</Label>
<Select value={formData.industry} onValueChange={(value) => handleChange("industry", value)} required>
<SelectTrigger className="h-11">
<SelectValue placeholder="산업 분야를 선택해주세요" />
</SelectTrigger>
<SelectContent>
<SelectItem value="automotive"> </SelectItem>
<SelectItem value="electronics">/</SelectItem>
<SelectItem value="machinery">/</SelectItem>
<SelectItem value="food"> </SelectItem>
<SelectItem value="chemical">/</SelectItem>
<SelectItem value="plastic">/</SelectItem>
<SelectItem value="metal"> </SelectItem>
<SelectItem value="other"></SelectItem>
</SelectContent>
</Select>
</div>
{/* 요청사항 */}
<div className="space-y-2">
<Label htmlFor="message" className="text-sm font-semibold flex items-center gap-2">
<MessageSquare className="w-4 h-4 text-blue-600" />
()
</Label>
<Textarea
id="message"
placeholder="궁금하신 점이나 특별히 보고 싶은 기능이 있으시면 작성해주세요."
value={formData.message}
onChange={(e) => handleChange("message", e.target.value)}
rows={4}
className="resize-none"
/>
</div>
{/* 버튼 */}
<div className="flex flex-col-reverse sm:flex-row gap-3 pt-4">
<Button
type="button"
variant="outline"
onClick={onClose}
className="flex-1 h-12"
>
</Button>
<Button
type="submit"
className="flex-1 h-12 bg-gradient-to-r from-blue-500 to-purple-600 hover:from-blue-600 hover:to-purple-700 text-white font-semibold"
>
</Button>
</div>
</form>
</DialogContent>
</Dialog>
);
}

View File

@@ -1,24 +1,18 @@
'use client';
import { lazy, Suspense } from "react";
import { useUserRole } from "@/hooks/useUserRole";
import { Suspense } from "react";
import { MainDashboard } from "./MainDashboard";
// ✅ Lazy Loading: 모든 역할별 대시보드를 개별 컴포넌트로 분리
const CEODashboard = lazy(() =>
import("./CEODashboard").then(m => ({ default: m.CEODashboard }))
);
const ProductionManagerDashboard = lazy(() =>
import("./ProductionManagerDashboard").then(m => ({ default: m.ProductionManagerDashboard }))
);
const WorkerDashboard = lazy(() =>
import("./WorkerDashboard").then(m => ({ default: m.WorkerDashboard }))
);
const SystemAdminDashboard = lazy(() =>
import("./SystemAdminDashboard").then(m => ({ default: m.SystemAdminDashboard }))
);
/**
* Dashboard - 통합 대시보드 컴포넌트
*
* 사용자 역할과 메뉴는 백엔드(PHP)에서 관리하며,
* 프론트엔드는 단일 통합 대시보드만 제공합니다.
*
* - 역할별 데이터 필터링: 백엔드 API에서 처리
* - 메뉴 구조: 로그인 시 받은 user.menu 데이터 사용
* - 권한 제어: 백엔드에서 역할에 따라 데이터 제한
*/
// 공통 로딩 컴포넌트
const DashboardLoading = () => (
@@ -31,45 +25,10 @@ const DashboardLoading = () => (
);
export function Dashboard() {
const userRole = useUserRole();
// 역할별 대시보드 라우팅 with Suspense
if (userRole === "CEO") {
return (
<Suspense fallback={<DashboardLoading />}>
<CEODashboard />
</Suspense>
);
}
if (userRole === "ProductionManager") {
return (
<Suspense fallback={<DashboardLoading />}>
<ProductionManagerDashboard />
</Suspense>
);
}
if (userRole === "Worker") {
return (
<Suspense fallback={<DashboardLoading />}>
<WorkerDashboard />
</Suspense>
);
}
if (userRole === "SystemAdmin") {
return (
<Suspense fallback={<DashboardLoading />}>
<SystemAdminDashboard />
</Suspense>
);
}
// Sales 역할 (기본 대시보드)
console.log('🎨 Dashboard component rendering...');
return (
<Suspense fallback={<DashboardLoading />}>
<CEODashboard />
<MainDashboard />
</Suspense>
);
}

View File

@@ -1,280 +0,0 @@
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Card } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import {
ArrowLeft,
Send,
CheckCircle2,
Clock,
Mail,
Phone,
User,
Building2,
Sparkles
} from "lucide-react";
interface DemoRequestPageProps {
onNavigateToLanding: () => void;
onRequestComplete: () => void;
}
export function DemoRequestPage({ onNavigateToLanding, onRequestComplete }: DemoRequestPageProps) {
const [formData, setFormData] = useState({
name: "",
company: "",
email: "",
phone: "",
industry: "",
requirements: ""
});
const [isSubmitted, setIsSubmitted] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
setIsSubmitting(true);
// 데모 리드 데이터를 로컬스토리지에 저장
const existingLeads = JSON.parse(localStorage.getItem('demoLeads') || '[]');
const newLead = {
id: Date.now().toString(),
...formData,
status: "신규",
createdAt: new Date().toISOString(),
assignedTo: null,
demoLink: null
};
localStorage.setItem('demoLeads', JSON.stringify([...existingLeads, newLead]));
// 제출 시뮬레이션
setTimeout(() => {
setIsSubmitting(false);
setIsSubmitted(true);
}, 1500);
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
setFormData(prev => ({
...prev,
[e.target.name]: e.target.value
}));
};
if (isSubmitted) {
return (
<div className="min-h-screen flex items-center justify-center p-4" style={{ background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' }}>
<Card className="max-w-2xl w-full p-12 text-center clean-shadow-xl">
<div className="w-20 h-20 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-6">
<CheckCircle2 className="w-12 h-12 text-green-600" />
</div>
<h2 className="text-3xl font-bold text-foreground mb-4">
!
</h2>
<p className="text-muted-foreground text-lg mb-8">
<strong className="text-foreground">{formData.name}</strong>, .
</p>
<div className="bg-primary/10 rounded-2xl p-6 mb-8">
<div className="flex items-center justify-center space-x-3 mb-3">
<Clock className="w-6 h-6 text-primary" />
<h3 className="font-bold text-foreground text-xl"> </h3>
</div>
<p className="text-muted-foreground">
<br />
<strong className="text-primary">1 </strong> .
</p>
</div>
<div className="grid md:grid-cols-2 gap-4 mb-8">
<div className="bg-accent/50 rounded-xl p-4">
<Mail className="w-5 h-5 text-primary mb-2 mx-auto" />
<p className="text-sm text-muted-foreground"></p>
<p className="font-semibold text-foreground">{formData.email}</p>
</div>
<div className="bg-accent/50 rounded-xl p-4">
<Phone className="w-5 h-5 text-primary mb-2 mx-auto" />
<p className="text-sm text-muted-foreground"></p>
<p className="font-semibold text-foreground">{formData.phone}</p>
</div>
</div>
<div className="flex flex-col sm:flex-row gap-3 justify-center">
<Button
onClick={onNavigateToLanding}
className="rounded-xl px-8 py-6 bg-primary hover:bg-primary/90"
>
<ArrowLeft className="w-4 h-4 mr-2" />
</Button>
</div>
</Card>
</div>
);
}
return (
<div className="min-h-screen flex items-center justify-center p-4" style={{ background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' }}>
<Card className="max-w-3xl w-full p-8 md:p-12 clean-shadow-xl">
<Button
variant="ghost"
onClick={onNavigateToLanding}
className="mb-6 rounded-xl"
>
<ArrowLeft className="w-4 h-4 mr-2" />
</Button>
<div className="text-center mb-8">
<Badge className="mb-4 bg-primary/10 text-primary border-primary/20 rounded-full px-4 py-2">
<Sparkles className="w-4 h-4 mr-2 inline" />
</Badge>
<h1 className="text-3xl md:text-4xl font-bold text-foreground mb-4">
SAM
</h1>
<p className="text-muted-foreground text-lg">
</p>
</div>
<form onSubmit={handleSubmit} className="space-y-6">
<div className="grid md:grid-cols-2 gap-6">
<div className="space-y-2">
<Label htmlFor="name" className="text-base flex items-center">
<User className="w-4 h-4 mr-2 text-primary" />
*
</Label>
<Input
id="name"
name="name"
value={formData.name}
onChange={handleChange}
placeholder="홍길동"
required
className="clean-input border-0 bg-input-background/60 text-base py-6"
/>
</div>
<div className="space-y-2">
<Label htmlFor="company" className="text-base flex items-center">
<Building2 className="w-4 h-4 mr-2 text-primary" />
*
</Label>
<Input
id="company"
name="company"
value={formData.company}
onChange={handleChange}
placeholder="㈜ 제조기업"
required
className="clean-input border-0 bg-input-background/60 text-base py-6"
/>
</div>
</div>
<div className="grid md:grid-cols-2 gap-6">
<div className="space-y-2">
<Label htmlFor="email" className="text-base flex items-center">
<Mail className="w-4 h-4 mr-2 text-primary" />
*
</Label>
<Input
id="email"
name="email"
type="email"
value={formData.email}
onChange={handleChange}
placeholder="example@company.com"
required
className="clean-input border-0 bg-input-background/60 text-base py-6"
/>
</div>
<div className="space-y-2">
<Label htmlFor="phone" className="text-base flex items-center">
<Phone className="w-4 h-4 mr-2 text-primary" />
*
</Label>
<Input
id="phone"
name="phone"
type="tel"
value={formData.phone}
onChange={handleChange}
placeholder="010-1234-5678"
required
className="clean-input border-0 bg-input-background/60 text-base py-6"
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="industry" className="text-base">
*
</Label>
<Input
id="industry"
name="industry"
value={formData.industry}
onChange={handleChange}
placeholder="예: 자동차 부품 제조, 식품 가공, 전자 제품 조립 등"
required
className="clean-input border-0 bg-input-background/60 text-base py-6"
/>
</div>
<div className="space-y-2">
<Label htmlFor="requirements" className="text-base">
</Label>
<Textarea
id="requirements"
name="requirements"
value={formData.requirements}
onChange={handleChange}
placeholder="관심 있는 기능이나 해결하고 싶은 문제를 자유롭게 작성해주세요"
rows={5}
className="clean-input border-0 bg-input-background/60 text-base resize-none"
/>
</div>
<div className="bg-accent/50 rounded-xl p-4 flex items-start space-x-3">
<Clock className="w-5 h-5 text-primary flex-shrink-0 mt-0.5" />
<div className="text-sm">
<p className="font-semibold text-foreground mb-1"> </p>
<p className="text-muted-foreground">
1 .
</p>
</div>
</div>
<Button
type="submit"
disabled={isSubmitting}
className="w-full rounded-xl py-7 text-lg bg-primary hover:bg-primary/90 clean-shadow-lg"
>
{isSubmitting ? (
<>
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin mr-2" />
...
</>
) : (
<>
<Send className="w-5 h-5 mr-2" />
</>
)}
</Button>
</form>
</Card>
</div>
);
}

View File

@@ -1,340 +0,0 @@
import { useState, useRef, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Slider } from "@/components/ui/slider";
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Pen, Square, Circle, Type, Minus, Eraser, Trash2, Undo2, Save } from "lucide-react";
interface DrawingCanvasProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onSave?: (imageData: string) => void;
initialImage?: string;
}
type Tool = "pen" | "line" | "rect" | "circle" | "text" | "eraser";
export function DrawingCanvas({ open, onOpenChange, onSave, initialImage }: DrawingCanvasProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const [isDrawing, setIsDrawing] = useState(false);
const [tool, setTool] = useState<Tool>("pen");
const [color, setColor] = useState("#000000");
const [lineWidth, setLineWidth] = useState(2);
const [startPos, setStartPos] = useState({ x: 0, y: 0 });
const [history, setHistory] = useState<ImageData[]>([]);
const [historyStep, setHistoryStep] = useState(-1);
const colors = [
"#000000", "#FF0000", "#00FF00", "#0000FF",
"#FFFF00", "#FF00FF", "#00FFFF", "#FFA500",
"#800080", "#FFC0CB"
];
useEffect(() => {
if (open && canvasRef.current) {
const canvas = canvasRef.current;
const ctx = canvas.getContext("2d");
if (ctx) {
// 캔버스 초기화
ctx.fillStyle = "#FFFFFF";
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 초기 이미지가 있으면 로드
if (initialImage) {
const img = new Image();
img.onload = () => {
ctx.drawImage(img, 0, 0);
saveToHistory();
};
img.src = initialImage;
} else {
saveToHistory();
}
}
}
}, [open]);
const saveToHistory = () => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const newHistory = history.slice(0, historyStep + 1);
newHistory.push(imageData);
setHistory(newHistory);
setHistoryStep(newHistory.length - 1);
};
const undo = () => {
if (historyStep > 0) {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
const newStep = historyStep - 1;
ctx.putImageData(history[newStep], 0, 0);
setHistoryStep(newStep);
}
};
const clearCanvas = () => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
ctx.fillStyle = "#FFFFFF";
ctx.fillRect(0, 0, canvas.width, canvas.height);
saveToHistory();
};
const getMousePos = (e: React.MouseEvent<HTMLCanvasElement>) => {
const canvas = canvasRef.current;
if (!canvas) return { x: 0, y: 0 };
const rect = canvas.getBoundingClientRect();
return {
x: e.clientX - rect.left,
y: e.clientY - rect.top
};
};
const startDrawing = (e: React.MouseEvent<HTMLCanvasElement>) => {
const pos = getMousePos(e);
setStartPos(pos);
setIsDrawing(true);
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
ctx.strokeStyle = color;
ctx.lineWidth = lineWidth;
ctx.lineCap = "round";
ctx.lineJoin = "round";
if (tool === "pen" || tool === "eraser") {
ctx.beginPath();
ctx.moveTo(pos.x, pos.y);
if (tool === "eraser") {
ctx.globalCompositeOperation = "destination-out";
} else {
ctx.globalCompositeOperation = "source-over";
}
}
};
const draw = (e: React.MouseEvent<HTMLCanvasElement>) => {
if (!isDrawing) return;
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
const pos = getMousePos(e);
if (tool === "pen" || tool === "eraser") {
ctx.lineTo(pos.x, pos.y);
ctx.stroke();
} else {
// 도형 그리기: 임시로 표시하기 위해 이전 상태 복원
if (historyStep >= 0) {
ctx.putImageData(history[historyStep], 0, 0);
}
ctx.globalCompositeOperation = "source-over";
ctx.strokeStyle = color;
ctx.fillStyle = color;
if (tool === "line") {
ctx.beginPath();
ctx.moveTo(startPos.x, startPos.y);
ctx.lineTo(pos.x, pos.y);
ctx.stroke();
} else if (tool === "rect") {
const width = pos.x - startPos.x;
const height = pos.y - startPos.y;
ctx.strokeRect(startPos.x, startPos.y, width, height);
} else if (tool === "circle") {
const radius = Math.sqrt(
Math.pow(pos.x - startPos.x, 2) + Math.pow(pos.y - startPos.y, 2)
);
ctx.beginPath();
ctx.arc(startPos.x, startPos.y, radius, 0, 2 * Math.PI);
ctx.stroke();
}
}
};
const stopDrawing = () => {
if (isDrawing) {
setIsDrawing(false);
saveToHistory();
}
};
const handleSave = () => {
const canvas = canvasRef.current;
if (!canvas) return;
const imageData = canvas.toDataURL("image/png");
onSave?.(imageData);
onOpenChange(false);
};
const handleText = () => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
const text = prompt("입력할 텍스트:");
if (text) {
ctx.font = `${lineWidth * 8}px sans-serif`;
ctx.fillStyle = color;
ctx.fillText(text, 50, 50);
saveToHistory();
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-4xl">
<DialogHeader>
<DialogTitle> </DialogTitle>
<DialogDescription>
.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
{/* 도구 모음 */}
<div className="flex items-center gap-2 p-3 border rounded-lg bg-muted/30">
<Button
variant={tool === "pen" ? "default" : "outline"}
size="sm"
onClick={() => setTool("pen")}
>
<Pen className="h-4 w-4" />
</Button>
<Button
variant={tool === "line" ? "default" : "outline"}
size="sm"
onClick={() => setTool("line")}
>
<Minus className="h-4 w-4" />
</Button>
<Button
variant={tool === "rect" ? "default" : "outline"}
size="sm"
onClick={() => setTool("rect")}
>
<Square className="h-4 w-4" />
</Button>
<Button
variant={tool === "circle" ? "default" : "outline"}
size="sm"
onClick={() => setTool("circle")}
>
<Circle className="h-4 w-4" />
</Button>
<Button
variant={tool === "text" ? "default" : "outline"}
size="sm"
onClick={() => {
setTool("text");
handleText();
}}
>
<Type className="h-4 w-4" />
</Button>
<Button
variant={tool === "eraser" ? "default" : "outline"}
size="sm"
onClick={() => setTool("eraser")}
>
<Eraser className="h-4 w-4" />
</Button>
<div className="h-6 w-px bg-border mx-2" />
<Button
variant="outline"
size="sm"
onClick={undo}
disabled={historyStep <= 0}
>
<Undo2 className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="sm"
onClick={clearCanvas}
>
<Trash2 className="h-4 w-4" />
</Button>
<div className="h-6 w-px bg-border mx-2" />
{/* 색상 팔레트 */}
<div className="flex gap-1">
{colors.map((c) => (
<button
key={c}
className={`w-6 h-6 rounded border-2 ${
color === c ? "border-primary" : "border-transparent"
}`}
style={{ backgroundColor: c }}
onClick={() => setColor(c)}
/>
))}
</div>
</div>
{/* 선 두께 조절 */}
<div className="flex items-center gap-4 px-3">
<Label className="text-sm whitespace-nowrap"> : {lineWidth}px</Label>
<Slider
value={[lineWidth]}
onValueChange={(value) => setLineWidth(value[0])}
min={1}
max={20}
step={1}
className="flex-1"
/>
</div>
{/* 캔버스 */}
<div className="border rounded-lg overflow-hidden bg-white">
<canvas
ref={canvasRef}
width={700}
height={400}
className="cursor-crosshair"
onMouseDown={startDrawing}
onMouseMove={draw}
onMouseUp={stopDrawing}
onMouseLeave={stopDrawing}
/>
</div>
{/* 하단 버튼 */}
<div className="flex justify-end gap-2">
<Button variant="outline" onClick={() => onOpenChange(false)}>
</Button>
<Button className="bg-blue-600 hover:bg-blue-700" onClick={handleSave}>
<Save className="h-4 w-4 mr-2" />
</Button>
</div>
</div>
</DialogContent>
</Dialog>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,836 +0,0 @@
import { useState } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger, DialogFooter } from "@/components/ui/dialog";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Progress } from "@/components/ui/progress";
import {
Users,
UserPlus,
Search,
Download,
Building2,
Award,
Calendar,
TrendingUp,
Target,
Eye,
Edit,
Network,
User
} from "lucide-react";
import { BarChart, Bar, PieChart, Pie, Cell, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from "recharts";
export function HRManagement() {
const [activeTab, setActiveTab] = useState("organization");
const [isAddEmployeeOpen, setIsAddEmployeeOpen] = useState(false);
// 직원 데이터
const employees = [
{
id: "EMP001",
name: "김대표",
position: "대표이사",
department: "경영진",
team: "-",
joinDate: "2015-01-01",
email: "ceo@company.com",
phone: "010-1111-1111",
address: "서울시 강남구",
birthDate: "1975-03-15",
education: "서울대 경영학 석사",
annualLeave: 0,
usedLeave: 0,
salary: "비공개",
performance: 95,
status: "재직",
kpi: [
{ metric: "매출 달성률", target: 100, actual: 112, unit: "%" },
{ metric: "영업이익률", target: 15, actual: 18, unit: "%" }
]
},
{
id: "EMP002",
name: "이생산",
position: "과장",
department: "생산부",
team: "생산1팀",
joinDate: "2018-03-15",
email: "lee@company.com",
phone: "010-2222-2222",
address: "경기도 안산시",
birthDate: "1985-07-20",
education: "한양대 기계공학 학사",
annualLeave: 15,
usedLeave: 8,
salary: "5,500,000",
performance: 88,
status: "재직",
kpi: [
{ metric: "생산 목표 달성률", target: 100, actual: 105, unit: "%" },
{ metric: "불량률", target: 2, actual: 1.2, unit: "%" },
{ metric: "납기준수율", target: 95, actual: 98, unit: "%" }
]
},
{
id: "EMP003",
name: "박품질",
position: "대리",
department: "품질부",
team: "품질관리팀",
joinDate: "2019-06-01",
email: "park@company.com",
phone: "010-3333-3333",
address: "경기도 시흥시",
birthDate: "1988-11-10",
education: "인하대 산업공학 학사",
annualLeave: 15,
usedLeave: 5,
salary: "4,800,000",
performance: 92,
status: "재직",
kpi: [
{ metric: "품질 합격률", target: 98, actual: 99.2, unit: "%" },
{ metric: "검사 처리시간", target: 24, actual: 18, unit: "시간" },
{ metric: "고객 클레임", target: 5, actual: 2, unit: "건" }
]
},
{
id: "EMP004",
name: "정설비",
position: "사원",
department: "설비부",
team: "설비관리팀",
joinDate: "2021-09-01",
email: "jung@company.com",
phone: "010-4444-4444",
address: "서울시 금천구",
birthDate: "1993-05-25",
education: "서울과기대 전기공학 학사",
annualLeave: 15,
usedLeave: 3,
salary: "3,800,000",
performance: 85,
status: "재직",
kpi: [
{ metric: "설비 가동률", target: 90, actual: 93, unit: "%" },
{ metric: "고장 처리시간", target: 4, actual: 3, unit: "시간" },
{ metric: "예방정비 수행률", target: 100, actual: 100, unit: "%" }
]
},
{
id: "EMP005",
name: "최자재",
position: "주임",
department: "자재부",
team: "구매팀",
joinDate: "2020-02-15",
email: "choi@company.com",
phone: "010-5555-5555",
address: "경기도 광명시",
birthDate: "1990-08-30",
education: "중앙대 물류학 학사",
annualLeave: 15,
usedLeave: 10,
salary: "4,200,000",
performance: 90,
status: "재직",
kpi: [
{ metric: "구매 절감률", target: 5, actual: 7, unit: "%" },
{ metric: "납기 준수율", target: 95, actual: 96, unit: "%" },
{ metric: "재고 회전율", target: 12, actual: 14, unit: "회" }
]
}
];
// 조직도 데이터
const organizationData = {
name: "김대표",
position: "대표이사",
children: [
{
name: "생산부",
position: "부장",
children: [
{ name: "생산1팀", position: "이생산 (과장)", members: 8 },
{ name: "생산2팀", position: "강생산 (과장)", members: 7 }
]
},
{
name: "품질부",
position: "부장",
children: [
{ name: "품질관리팀", position: "박품질 (대리)", members: 5 },
{ name: "품질보증팀", position: "오품질 (대리)", members: 4 }
]
},
{
name: "영업부",
position: "부장",
children: [
{ name: "영업1팀", position: "김영업 (과장)", members: 6 },
{ name: "영업2팀", position: "이영업 (과장)", members: 5 }
]
},
{
name: "지원부",
position: "부장",
children: [
{ name: "인사팀", position: "박인사 (과장)", members: 3 },
{ name: "총무팀", position: "최총무 (대리)", members: 4 },
{ name: "회계팀", position: "정회계 (과장)", members: 4 }
]
}
]
};
// 승진 후보 데이터
const promotionCandidates = [
{
id: "EMP002",
name: "이생산",
currentPosition: "과장",
targetPosition: "차장",
department: "생산부",
yearsInPosition: 3.5,
performance: 88,
evaluationScore: 92,
status: "후보",
expectedDate: "2025-01-01"
},
{
id: "EMP003",
name: "박품질",
currentPosition: "대리",
targetPosition: "과장",
department: "품질부",
yearsInPosition: 2.8,
performance: 92,
evaluationScore: 90,
status: "후보",
expectedDate: "2025-01-01"
},
{
id: "EMP005",
name: "최자재",
currentPosition: "주임",
targetPosition: "대리",
department: "자재부",
yearsInPosition: 2.2,
performance: 90,
evaluationScore: 88,
status: "검토중",
expectedDate: "2025-03-01"
}
];
// 부서별 통계
const departmentStats = [
{ department: "생산부", count: 15, avgPerformance: 87 },
{ department: "품질부", count: 9, avgPerformance: 90 },
{ department: "영업부", count: 11, avgPerformance: 85 },
{ department: "설비부", count: 6, avgPerformance: 86 },
{ department: "자재부", count: 7, avgPerformance: 88 },
{ department: "지원부", count: 11, avgPerformance: 84 }
];
// 직급별 분포
const positionDistribution = [
{ position: "임원", count: 1 },
{ position: "부장", count: 4 },
{ position: "차장", count: 6 },
{ position: "과장", count: 12 },
{ position: "대리", count: 15 },
{ position: "주임", count: 10 },
{ position: "사원", count: 11 }
];
const COLORS = ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899', '#06b6d4'];
const getPerformanceBadge = (score: number) => {
if (score >= 90) return <Badge className="bg-green-500 text-white"></Badge>;
if (score >= 80) return <Badge className="bg-blue-500 text-white"></Badge>;
if (score >= 70) return <Badge className="bg-yellow-500 text-white"></Badge>;
return <Badge className="bg-red-500 text-white"></Badge>;
};
const getStatusBadge = (status: string) => {
const statusConfig: Record<string, string> = {
"재직": "bg-green-500 text-white",
"휴직": "bg-yellow-500 text-white",
"퇴직": "bg-gray-500 text-white",
};
return <Badge className={statusConfig[status] || "bg-gray-500 text-white"}>{status}</Badge>;
};
return (
<div className="p-4 md:p-6 space-y-6">
{/* 헤더 */}
<div className="bg-card border border-border/20 rounded-xl p-6">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<h1 className="text-3xl font-bold text-foreground mb-2"></h1>
<p className="text-muted-foreground">, , , KPI </p>
</div>
<div className="flex space-x-3">
<Button variant="outline" className="border-border/50">
<Download className="h-4 w-4 mr-2" />
</Button>
<Dialog open={isAddEmployeeOpen} onOpenChange={setIsAddEmployeeOpen}>
<DialogTrigger asChild>
<Button className="bg-primary text-primary-foreground">
<UserPlus className="h-4 w-4 mr-2" />
</Button>
</DialogTrigger>
<DialogContent className="max-w-5xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle> </DialogTitle>
<DialogDescription> .</DialogDescription>
</DialogHeader>
<div className="space-y-6">
<div className="bg-muted/50 p-4 rounded-lg">
<h3 className="font-bold text-lg mb-4"> </h3>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="name"> *</Label>
<Input id="name" placeholder="홍길동" />
</div>
<div className="space-y-2">
<Label htmlFor="employeeId"></Label>
<Input id="employeeId" placeholder="EMP006" />
</div>
<div className="space-y-2">
<Label htmlFor="department"> *</Label>
<Select>
<SelectTrigger>
<SelectValue placeholder="선택하세요" />
</SelectTrigger>
<SelectContent>
<SelectItem value="생산부"></SelectItem>
<SelectItem value="품질부"></SelectItem>
<SelectItem value="영업부"></SelectItem>
<SelectItem value="설비부"></SelectItem>
<SelectItem value="자재부"></SelectItem>
<SelectItem value="지원부"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="position"> *</Label>
<Select>
<SelectTrigger>
<SelectValue placeholder="선택하세요" />
</SelectTrigger>
<SelectContent>
<SelectItem value="사원"></SelectItem>
<SelectItem value="주임"></SelectItem>
<SelectItem value="대리"></SelectItem>
<SelectItem value="과장"></SelectItem>
<SelectItem value="차장"></SelectItem>
<SelectItem value="부장"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="joinDate"> *</Label>
<Input id="joinDate" type="date" />
</div>
<div className="space-y-2">
<Label htmlFor="birthDate"></Label>
<Input id="birthDate" type="date" />
</div>
</div>
</div>
<div className="bg-muted/50 p-4 rounded-lg">
<h3 className="font-bold text-lg mb-4"></h3>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="email"> *</Label>
<Input id="email" type="email" placeholder="name@company.com" />
</div>
<div className="space-y-2">
<Label htmlFor="phone"> *</Label>
<Input id="phone" placeholder="010-0000-0000" />
</div>
<div className="space-y-2 col-span-2">
<Label htmlFor="address"></Label>
<Input id="address" placeholder="서울시 강남구..." />
</div>
</div>
</div>
<div className="bg-muted/50 p-4 rounded-lg">
<h3 className="font-bold text-lg mb-4"> </h3>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="education"></Label>
<Input id="education" placeholder="예) 서울대 경영학 학사" />
</div>
<div className="space-y-2">
<Label htmlFor="career"></Label>
<Textarea id="career" placeholder="이전 경력을 입력하세요" rows={3} />
</div>
</div>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsAddEmployeeOpen(false)}>
</Button>
<Button className="bg-primary" onClick={() => setIsAddEmployeeOpen(false)}>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</div>
</div>
{/* 통계 카드 */}
<div className="grid grid-cols-1 md:grid-cols-5 gap-4">
<Card className="border border-border/20">
<CardHeader className="pb-3">
<CardTitle className="text-sm text-muted-foreground"> </CardTitle>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-foreground">59</div>
<p className="text-sm text-green-600 mt-1">
<TrendingUp className="h-3 w-3 inline mr-1" />
+3
</p>
</CardContent>
</Card>
<Card className="border border-border/20">
<CardHeader className="pb-3">
<CardTitle className="text-sm text-muted-foreground"> </CardTitle>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-blue-600">4.2</div>
<p className="text-sm text-muted-foreground mt-1"> </p>
</CardContent>
</Card>
<Card className="border border-border/20">
<CardHeader className="pb-3">
<CardTitle className="text-sm text-muted-foreground"> </CardTitle>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-green-600">87</div>
<p className="text-sm text-green-600 mt-1">
+7
</p>
</CardContent>
</Card>
<Card className="border border-border/20">
<CardHeader className="pb-3">
<CardTitle className="text-sm text-muted-foreground"> </CardTitle>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-purple-600">5</div>
<p className="text-sm text-muted-foreground mt-1">2025 1</p>
</CardContent>
</Card>
<Card className="border border-border/20">
<CardHeader className="pb-3">
<CardTitle className="text-sm text-muted-foreground"></CardTitle>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-yellow-600">5.2%</div>
<p className="text-sm text-muted-foreground mt-1"> </p>
</CardContent>
</Card>
</div>
{/* 탭 메뉴 */}
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-6">
<TabsList className="grid w-full grid-cols-5 h-auto p-1 bg-muted/50 rounded-xl">
<TabsTrigger value="organization" className="py-3 rounded-lg data-[state=active]:bg-primary data-[state=active]:text-primary-foreground">
<Network className="h-4 w-4 mr-2" />
</TabsTrigger>
<TabsTrigger value="employees" className="py-3 rounded-lg data-[state=active]:bg-primary data-[state=active]:text-primary-foreground">
<Users className="h-4 w-4 mr-2" />
</TabsTrigger>
<TabsTrigger value="leave" className="py-3 rounded-lg data-[state=active]:bg-primary data-[state=active]:text-primary-foreground">
<Calendar className="h-4 w-4 mr-2" />
</TabsTrigger>
<TabsTrigger value="promotion" className="py-3 rounded-lg data-[state=active]:bg-primary data-[state=active]:text-primary-foreground">
<Award className="h-4 w-4 mr-2" />
</TabsTrigger>
<TabsTrigger value="kpi" className="py-3 rounded-lg data-[state=active]:bg-primary data-[state=active]:text-primary-foreground">
<Target className="h-4 w-4 mr-2" />
KPI
</TabsTrigger>
</TabsList>
{/* 조직도 */}
<TabsContent value="organization" className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* 조직도 시각화 */}
<Card className="border border-border/20 md:col-span-2">
<CardHeader>
<CardTitle> </CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
{/* CEO */}
<div className="flex justify-center">
<div className="bg-primary text-primary-foreground p-4 rounded-lg text-center w-48">
<Building2 className="h-6 w-6 mx-auto mb-2" />
<p className="font-bold">{organizationData.name}</p>
<p className="text-sm">{organizationData.position}</p>
</div>
</div>
{/* 부서 */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{organizationData.children?.map((dept, idx) => (
<div key={idx} className="space-y-3">
<div className="bg-blue-100 dark:bg-blue-900/30 p-3 rounded-lg text-center">
<p className="font-bold text-sm">{dept.name}</p>
<p className="text-xs text-muted-foreground">{dept.position}</p>
</div>
{dept.children?.map((team, tidx) => (
<div key={tidx} className="bg-muted/50 p-2 rounded text-center ml-2">
<p className="text-sm font-medium">{team.name}</p>
<p className="text-xs text-muted-foreground">{team.position}</p>
<Badge variant="outline" className="mt-1 text-xs">
{team.members}
</Badge>
</div>
))}
</div>
))}
</div>
</div>
</CardContent>
</Card>
{/* 부서별 인원 통계 */}
<Card className="border border-border/20">
<CardHeader>
<CardTitle> </CardTitle>
</CardHeader>
<CardContent>
<ResponsiveContainer width="100%" height={300}>
<BarChart data={departmentStats}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="department" />
<YAxis />
<Tooltip />
<Bar dataKey="count" fill="#3b82f6" name="인원" />
</BarChart>
</ResponsiveContainer>
</CardContent>
</Card>
{/* 직급별 분포 */}
<Card className="border border-border/20">
<CardHeader>
<CardTitle> </CardTitle>
</CardHeader>
<CardContent>
<ResponsiveContainer width="100%" height={300}>
<PieChart>
<Pie
data={positionDistribution}
dataKey="count"
nameKey="position"
cx="50%"
cy="50%"
outerRadius={100}
label={(entry) => `${entry.position} (${entry.count})`}
>
{positionDistribution.map((entry, index) => (
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
))}
</Pie>
<Tooltip />
</PieChart>
</ResponsiveContainer>
</CardContent>
</Card>
</div>
</TabsContent>
{/* 직원 관리 */}
<TabsContent value="employees" className="space-y-4">
{/* 검색 및 필터 */}
<Card className="border border-border/20">
<CardContent className="pt-6">
<div className="flex flex-col md:flex-row gap-4">
<div className="flex-1 relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input placeholder="이름, 부서로 검색..." className="pl-10" />
</div>
<Select>
<SelectTrigger className="w-full md:w-48">
<SelectValue placeholder="부서" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="생산부"></SelectItem>
<SelectItem value="품질부"></SelectItem>
<SelectItem value="영업부"></SelectItem>
</SelectContent>
</Select>
<Select>
<SelectTrigger className="w-full md:w-48">
<SelectValue placeholder="직급" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="부장"></SelectItem>
<SelectItem value="과장"></SelectItem>
<SelectItem value="대리"></SelectItem>
</SelectContent>
</Select>
</div>
</CardContent>
</Card>
{/* 직원 목록 */}
<Card className="border border-border/20">
<CardContent className="p-0">
<Table>
<TableHeader>
<TableRow className="bg-muted/50">
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="text-center"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{employees.map((emp) => (
<TableRow key={emp.id} className="hover:bg-muted/50">
<TableCell className="font-mono text-sm">{emp.id}</TableCell>
<TableCell className="font-medium">{emp.name}</TableCell>
<TableCell>
<Badge variant="outline">{emp.department}</Badge>
</TableCell>
<TableCell>{emp.position}</TableCell>
<TableCell className="text-sm">{emp.joinDate}</TableCell>
<TableCell className="text-sm">{emp.phone}</TableCell>
<TableCell>{getPerformanceBadge(emp.performance)}</TableCell>
<TableCell>{getStatusBadge(emp.status)}</TableCell>
<TableCell>
<div className="flex items-center justify-center space-x-2">
<Button variant="ghost" size="sm">
<Eye className="h-4 w-4" />
</Button>
<Button variant="ghost" size="sm">
<Edit className="h-4 w-4" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
</TabsContent>
{/* 연차 관리 */}
<TabsContent value="leave" className="space-y-4">
<Card className="border border-border/20">
<CardHeader>
<CardTitle> </CardTitle>
</CardHeader>
<CardContent className="p-0">
<Table>
<TableHeader>
<TableRow className="bg-muted/50">
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="text-right"> </TableHead>
<TableHead className="text-right"></TableHead>
<TableHead className="text-right"></TableHead>
<TableHead></TableHead>
<TableHead className="text-center"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{employees.filter(e => e.annualLeave > 0).map((emp) => {
const remaining = emp.annualLeave - emp.usedLeave;
const usageRate = (emp.usedLeave / emp.annualLeave) * 100;
return (
<TableRow key={emp.id} className="hover:bg-muted/50">
<TableCell className="font-medium">{emp.name}</TableCell>
<TableCell>
<Badge variant="outline">{emp.department}</Badge>
</TableCell>
<TableCell>{emp.position}</TableCell>
<TableCell className="text-right font-bold">{emp.annualLeave}</TableCell>
<TableCell className="text-right text-blue-600">{emp.usedLeave}</TableCell>
<TableCell className="text-right text-green-600">{remaining}</TableCell>
<TableCell>
<div className="space-y-1">
<Progress value={usageRate} className="h-2" />
<p className="text-xs text-muted-foreground">{usageRate.toFixed(0)}%</p>
</div>
</TableCell>
<TableCell className="text-center">
<Button variant="ghost" size="sm">
<Eye className="h-4 w-4" />
</Button>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</CardContent>
</Card>
</TabsContent>
{/* 승진 관리 */}
<TabsContent value="promotion" className="space-y-4">
<Card className="border border-border/20">
<CardHeader>
<CardTitle> </CardTitle>
</CardHeader>
<CardContent className="p-0">
<Table>
<TableHeader>
<TableRow className="bg-muted/50">
<TableHead></TableHead>
<TableHead> </TableHead>
<TableHead> </TableHead>
<TableHead></TableHead>
<TableHead> </TableHead>
<TableHead> </TableHead>
<TableHead> </TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{promotionCandidates.map((candidate) => (
<TableRow key={candidate.id} className="hover:bg-muted/50">
<TableCell className="font-medium">{candidate.name}</TableCell>
<TableCell>
<Badge variant="outline">{candidate.currentPosition}</Badge>
</TableCell>
<TableCell>
<Badge className="bg-purple-500 text-white">
<Award className="h-3 w-3 mr-1" />
{candidate.targetPosition}
</Badge>
</TableCell>
<TableCell>{candidate.department}</TableCell>
<TableCell>{candidate.yearsInPosition}</TableCell>
<TableCell>
<div className="flex items-center space-x-2">
<Progress value={candidate.performance} className="h-2 w-16" />
<span className="text-sm">{candidate.performance}</span>
</div>
</TableCell>
<TableCell>
<div className="flex items-center space-x-2">
<Progress value={candidate.evaluationScore} className="h-2 w-16" />
<span className="text-sm">{candidate.evaluationScore}</span>
</div>
</TableCell>
<TableCell>
<Badge className={candidate.status === "후보" ? "bg-green-500 text-white" : "bg-yellow-500 text-white"}>
{candidate.status}
</Badge>
</TableCell>
<TableCell className="text-sm">{candidate.expectedDate}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
</TabsContent>
{/* KPI 관리 */}
<TabsContent value="kpi" className="space-y-4">
{employees.slice(0, 3).map((emp) => (
<Card key={emp.id} className="border border-border/20">
<CardHeader>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<div className="w-12 h-12 bg-primary/10 rounded-full flex items-center justify-center">
<User className="h-6 w-6 text-primary" />
</div>
<div>
<CardTitle>{emp.name}</CardTitle>
<p className="text-sm text-muted-foreground">
{emp.department} · {emp.position}
</p>
</div>
</div>
<div className="text-right">
<p className="text-sm text-muted-foreground"> </p>
<p className="text-2xl font-bold text-primary">{emp.performance}</p>
</div>
</div>
</CardHeader>
<CardContent>
<div className="space-y-4">
{emp.kpi.map((kpi, idx) => {
const achievement = (kpi.actual / kpi.target) * 100;
const isGood = kpi.metric.includes("불량률") || kpi.metric.includes("처리시간") || kpi.metric.includes("클레임")
? kpi.actual <= kpi.target
: kpi.actual >= kpi.target;
return (
<div key={idx} className="space-y-2">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<Target className="h-4 w-4 text-primary" />
<span className="font-medium">{kpi.metric}</span>
</div>
<div className="text-right">
<span className="font-bold text-lg">
{kpi.actual}{kpi.unit}
</span>
<span className="text-sm text-muted-foreground ml-2">
/ {kpi.target}{kpi.unit}
</span>
</div>
</div>
<div className="flex items-center space-x-3">
<Progress value={Math.min(achievement, 100)} className="h-3" />
<Badge className={isGood ? "bg-green-500 text-white" : "bg-red-500 text-white"}>
{achievement.toFixed(0)}%
</Badge>
</div>
</div>
);
})}
</div>
</CardContent>
</Card>
))}
</TabsContent>
</Tabs>
</div>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,527 +0,0 @@
import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { ContactModal } from "./ContactModal";
import {
Factory,
CheckSquare,
Package,
TrendingUp,
Shield,
BarChart3,
Award,
ArrowRight,
Building2,
Sparkles,
Target,
Star,
Cpu,
Activity,
Users,
Zap,
Settings,
FileText
} from "lucide-react";
export function LandingPage() {
const [isVisible, setIsVisible] = useState(false);
const [isModalOpen, setIsModalOpen] = useState(false);
const [viewMode, setViewMode] = useState<"client" | "sales">("client");
const navigate = useNavigate();
useEffect(() => {
setIsVisible(true);
}, []);
const handleDemoRequest = () => {
setIsModalOpen(true);
};
const handleNavigateToDashboard = () => {
navigate("/dashboard");
};
const handleNavigateToSalesDashboard = () => {
navigate("/dashboard/sales-leads");
};
// 플로팅 기능 카드 데이터
const floatingFeatures = [
{ icon: Factory, title: "생산 관리", subtitle: "실시간 현황", color: "bg-blue-500", position: "top-20 left-10" },
{ icon: CheckSquare, title: "품질 관리", subtitle: "불량 추적", color: "bg-green-500", position: "top-40 right-20" },
{ icon: Package, title: "자재 관리", subtitle: "재고 최적화", color: "bg-orange-500", position: "bottom-40 left-20" },
{ icon: Cpu, title: "설비 관리", subtitle: "가동률 모니터링", color: "bg-purple-500", position: "top-60 left-1/4" },
{ icon: BarChart3, title: "실시간 분석", subtitle: "데이터 기반 의사결정", color: "bg-indigo-500", position: "bottom-32 right-1/4" },
{ icon: Users, title: "인사 관리", subtitle: "근태 및 급여", color: "bg-pink-500", position: "top-32 right-1/3" },
];
return (
<div className="min-h-screen bg-gradient-to-b from-blue-50 via-white to-gray-50 overflow-hidden">
{/* Header */}
<header className="backdrop-blur-sm bg-white/80 border-b border-gray-200/50 sticky top-0 z-50">
<div className="container mx-auto px-6 py-4">
<div className="flex items-center justify-between">
<button className="flex items-center space-x-3 hover:scale-105 transition-transform">
<div className="w-10 h-10 rounded-xl flex items-center justify-center shadow-md relative overflow-hidden bg-gradient-to-br from-blue-500 to-blue-600">
<div className="text-white font-bold text-lg">S</div>
</div>
<div>
<h1 className="font-bold text-gray-900">SAM</h1>
<p className="text-xs text-gray-500">Smart Automation Management</p>
</div>
</button>
<div className="flex items-center gap-3">
{/* View Mode Toggle */}
<div className="flex items-center gap-2 bg-gray-100 rounded-full p-1">
<button
onClick={() => setViewMode("client")}
className={`px-4 py-2 rounded-full text-sm font-semibold transition-all ${
viewMode === "client"
? "bg-white text-blue-600 shadow-md"
: "text-gray-600 hover:text-gray-900"
}`}
>
</button>
<button
onClick={() => setViewMode("sales")}
className={`px-4 py-2 rounded-full text-sm font-semibold transition-all ${
viewMode === "sales"
? "bg-white text-blue-600 shadow-md"
: "text-gray-600 hover:text-gray-900"
}`}
>
</button>
</div>
<Button
onClick={handleNavigateToDashboard}
className="rounded-full bg-blue-600 hover:bg-blue-700 text-white shadow-lg px-6"
>
<ArrowRight className="ml-2 h-4 w-4" />
</Button>
{viewMode === "client" ? (
<Button
onClick={handleDemoRequest}
variant="outline"
className="rounded-full border-2 border-blue-600 text-blue-600 hover:bg-blue-50 px-6"
>
</Button>
) : (
<Button
onClick={handleNavigateToSalesDashboard}
className="rounded-full bg-purple-600 hover:bg-purple-700 text-white shadow-lg px-6"
>
</Button>
)}
</div>
</div>
</div>
</header>
{/* Hero Section with Infographic Style */}
<section className="relative py-20 md:py-32 overflow-hidden">
{/* Decorative Background Elements */}
<div className="absolute inset-0 pointer-events-none">
{/* Wave patterns */}
<svg className="absolute top-20 left-0 w-32 h-32 text-blue-200 opacity-50" viewBox="0 0 100 100">
<path d="M0 50 Q 25 40, 50 50 T 100 50" stroke="currentColor" fill="none" strokeWidth="2"/>
<path d="M0 60 Q 25 50, 50 60 T 100 60" stroke="currentColor" fill="none" strokeWidth="2"/>
<path d="M0 70 Q 25 60, 50 70 T 100 70" stroke="currentColor" fill="none" strokeWidth="2"/>
</svg>
<svg className="absolute bottom-20 right-0 w-32 h-32 text-purple-200 opacity-50" viewBox="0 0 100 100">
<path d="M0 50 Q 25 40, 50 50 T 100 50" stroke="currentColor" fill="none" strokeWidth="2"/>
<path d="M0 60 Q 25 50, 50 60 T 100 60" stroke="currentColor" fill="none" strokeWidth="2"/>
<path d="M0 70 Q 25 60, 50 70 T 100 70" stroke="currentColor" fill="none" strokeWidth="2"/>
</svg>
{/* Dot patterns */}
<div className="absolute top-40 right-20 grid grid-cols-4 gap-2 opacity-20">
{[...Array(16)].map((_, i) => (
<div key={i} className="w-2 h-2 rounded-full bg-blue-400"></div>
))}
</div>
<div className="absolute bottom-60 left-32 grid grid-cols-4 gap-2 opacity-20">
{[...Array(16)].map((_, i) => (
<div key={i} className="w-2 h-2 rounded-full bg-purple-400"></div>
))}
</div>
</div>
<div className="container mx-auto px-6 relative z-10">
{/* Main Hero Content */}
<div className="text-center max-w-4xl mx-auto mb-16">
<Badge className="mb-6 bg-blue-100 text-blue-700 border-0 rounded-full px-4 py-1.5">
<Sparkles className="w-3.5 h-3.5 inline mr-2" />
SAM Pro
</Badge>
<h1 className="mb-6 text-gray-900 leading-tight">
<span className="block text-4xl md:text-6xl font-extrabold mb-3">
The Ultimate Manufacturing
</span>
<span className="block text-4xl md:text-6xl font-extrabold">
& Business Management Solution
</span>
</h1>
<p className="mb-12 text-gray-600 text-lg md:text-xl max-w-3xl mx-auto leading-relaxed">
, , .<br />
SAM으로 .
</p>
</div>
{/* Infographic Section with Floating Cards */}
<div className="relative max-w-6xl mx-auto h-[600px]">
{/* Floating Feature Cards - Hidden on mobile, visible on desktop */}
<div className="hidden lg:block">
{/* Top Left - 생산 관리 */}
<div
className={`absolute top-12 left-8 ${isVisible ? 'animate-float' : 'opacity-0'}`}
style={{ animationDelay: '0s' }}
>
<div className="bg-white rounded-2xl shadow-lg p-4 flex items-center gap-3 hover:shadow-xl transition-shadow">
<div className="w-12 h-12 rounded-xl bg-blue-500 flex items-center justify-center flex-shrink-0">
<Factory className="w-6 h-6 text-white" />
</div>
<div>
<div className="font-bold text-gray-900"> </div>
<div className="text-sm text-gray-500"> </div>
</div>
</div>
</div>
{/* Top Right - 품질 관리 */}
<div
className={`absolute top-24 right-12 ${isVisible ? 'animate-float' : 'opacity-0'}`}
style={{ animationDelay: '0.2s' }}
>
<div className="bg-white rounded-2xl shadow-lg p-4 flex items-center gap-3 hover:shadow-xl transition-shadow">
<div className="w-12 h-12 rounded-xl bg-green-500 flex items-center justify-center flex-shrink-0">
<CheckSquare className="w-6 h-6 text-white" />
</div>
<div>
<div className="font-bold text-gray-900"> </div>
<div className="text-sm text-gray-500"> </div>
</div>
</div>
</div>
{/* Left Middle - 설비 관리 */}
<div
className={`absolute top-48 left-4 ${isVisible ? 'animate-float' : 'opacity-0'}`}
style={{ animationDelay: '0.4s' }}
>
<div className="bg-white rounded-2xl shadow-lg p-4 flex items-center gap-3 hover:shadow-xl transition-shadow">
<div className="w-12 h-12 rounded-xl bg-purple-500 flex items-center justify-center flex-shrink-0">
<Cpu className="w-6 h-6 text-white" />
</div>
<div>
<div className="font-bold text-gray-900"> </div>
<div className="text-sm text-gray-500"> </div>
</div>
</div>
</div>
{/* Right Middle - 인사 관리 */}
<div
className={`absolute top-40 right-6 ${isVisible ? 'animate-float' : 'opacity-0'}`}
style={{ animationDelay: '0.6s' }}
>
<div className="bg-white rounded-2xl shadow-lg p-4 flex items-center gap-3 hover:shadow-xl transition-shadow">
<div className="w-12 h-12 rounded-xl bg-pink-500 flex items-center justify-center flex-shrink-0">
<Users className="w-6 h-6 text-white" />
</div>
<div>
<div className="font-bold text-gray-900"> </div>
<div className="text-sm text-gray-500"> </div>
</div>
</div>
</div>
{/* Bottom Left - 자재 관리 */}
<div
className={`absolute bottom-20 left-16 ${isVisible ? 'animate-float' : 'opacity-0'}`}
style={{ animationDelay: '0.8s' }}
>
<div className="bg-white rounded-2xl shadow-lg p-4 flex items-center gap-3 hover:shadow-xl transition-shadow">
<div className="w-12 h-12 rounded-xl bg-orange-500 flex items-center justify-center flex-shrink-0">
<Package className="w-6 h-6 text-white" />
</div>
<div>
<div className="font-bold text-gray-900"> </div>
<div className="text-sm text-gray-500"> </div>
</div>
</div>
</div>
{/* Bottom Right - 실시간 분석 */}
<div
className={`absolute bottom-28 right-20 ${isVisible ? 'animate-float' : 'opacity-0'}`}
style={{ animationDelay: '1s' }}
>
<div className="bg-white rounded-2xl shadow-lg p-4 flex items-center gap-3 hover:shadow-xl transition-shadow">
<div className="w-12 h-12 rounded-xl bg-indigo-500 flex items-center justify-center flex-shrink-0">
<BarChart3 className="w-6 h-6 text-white" />
</div>
<div>
<div className="font-bold text-gray-900"> </div>
<div className="text-sm text-gray-500"> </div>
</div>
</div>
</div>
{/* Avatar-like icons */}
<div className={`absolute top-32 left-1/3 ${isVisible ? 'animate-float' : 'opacity-0'}`} style={{ animationDelay: '0.3s' }}>
<div className="w-14 h-14 rounded-full bg-gradient-to-br from-blue-400 to-blue-600 shadow-lg flex items-center justify-center">
<Factory className="w-7 h-7 text-white" />
</div>
</div>
<div className={`absolute bottom-40 right-1/3 ${isVisible ? 'animate-float' : 'opacity-0'}`} style={{ animationDelay: '0.7s' }}>
<div className="w-14 h-14 rounded-full bg-gradient-to-br from-green-400 to-green-600 shadow-lg flex items-center justify-center">
<Award className="w-7 h-7 text-white" />
</div>
</div>
</div>
{/* Center Dashboard Mockup */}
<div className={`absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-full max-w-3xl transition-all duration-1000 ${isVisible ? 'opacity-100 scale-100' : 'opacity-0 scale-95'}`}>
<div className="relative">
<div className="absolute -inset-2 bg-gradient-to-r from-blue-600 via-purple-600 to-pink-600 rounded-3xl blur-2xl opacity-20"></div>
<div className="relative bg-white rounded-2xl shadow-2xl overflow-hidden border-2 border-gray-100">
{/* Browser Chrome */}
<div className="bg-gray-100 px-4 py-3 flex items-center justify-between border-b border-gray-200">
<div className="flex items-center space-x-2">
<div className="w-3 h-3 rounded-full bg-red-400"></div>
<div className="w-3 h-3 rounded-full bg-yellow-400"></div>
<div className="w-3 h-3 rounded-full bg-green-400"></div>
</div>
<div className="flex-1 mx-4 bg-white rounded-lg px-3 py-1 text-xs text-gray-500 text-center">
https://sam-mes.com/dashboard
</div>
<div className="w-16"></div>
</div>
{/* Dashboard Content */}
<div className="p-6 bg-gradient-to-br from-gray-50 to-white">
{/* Stats Cards */}
<div className="grid grid-cols-3 gap-3 mb-4">
<div className="bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl p-4 shadow-md">
<div className="text-white/80 text-xs mb-1"> </div>
<div className="text-white text-2xl font-bold">94.2%</div>
<div className="text-white/70 text-xs mt-1"> 12.5%</div>
</div>
<div className="bg-gradient-to-br from-green-500 to-green-600 rounded-xl p-4 shadow-md">
<div className="text-white/80 text-xs mb-1"> </div>
<div className="text-white text-2xl font-bold">98.5</div>
<div className="text-white/70 text-xs mt-1"> 8.2%</div>
</div>
<div className="bg-gradient-to-br from-purple-500 to-purple-600 rounded-xl p-4 shadow-md">
<div className="text-white/80 text-xs mb-1"></div>
<div className="text-white text-2xl font-bold">87.3%</div>
<div className="text-white/70 text-xs mt-1"> 15.1%</div>
</div>
</div>
{/* Chart */}
<div className="bg-white rounded-xl p-4 shadow-sm">
<div className="h-32 relative">
<div className="absolute inset-0 flex items-end justify-between space-x-0.5">
{[65, 75, 60, 85, 70, 90, 75, 80, 85, 78, 88, 92, 95, 90, 88, 85, 92, 88].map((height, i) => (
<div
key={i}
className="flex-1 bg-gradient-to-t from-blue-500 to-purple-500 rounded-t hover:from-blue-600 hover:to-purple-600 transition-all"
style={{ height: `${height}%` }}
></div>
))}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
{/* Why SAM Section */}
<section className="relative py-16 bg-white">
<div className="container mx-auto px-6">
<div className="text-center mb-12 max-w-3xl mx-auto">
<h2 className="mb-4 text-gray-900 text-3xl md:text-4xl font-extrabold">
Why SAM <span className="text-blue-600">Pro</span>
</h2>
<p className="text-gray-600 text-lg">
· MES <br />
</p>
</div>
<div className="grid md:grid-cols-3 gap-8 max-w-5xl mx-auto">
{[
{
icon: Zap,
title: "빠른 도입",
desc: "복잡한 설정 없이 3일 내 구축 가능",
color: "bg-yellow-500"
},
{
icon: Target,
title: "맞춤형 솔루션",
desc: "8개 산업 분야별 최적화된 프리셋",
color: "bg-blue-500"
},
{
icon: Award,
title: "검증된 성과",
desc: "200+ 기업의 생산성 47% 향상",
color: "bg-green-500"
},
].map((item, i) => {
const Icon = item.icon;
return (
<div key={i} className="text-center">
<div className={`w-16 h-16 mx-auto mb-4 rounded-2xl ${item.color} flex items-center justify-center shadow-lg`}>
<Icon className="w-8 h-8 text-white" />
</div>
<h3 className="mb-2 font-bold text-gray-900 text-xl">{item.title}</h3>
<p className="text-gray-600">{item.desc}</p>
</div>
);
})}
</div>
</div>
</section>
{/* Features Grid */}
<section className="relative py-16 bg-gray-50">
<div className="container mx-auto px-6">
<div className="text-center mb-12">
<h2 className="mb-4 text-gray-900 text-3xl md:text-4xl font-extrabold">
8
</h2>
</div>
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-6 max-w-6xl mx-auto">
{[
{ icon: Factory, title: "생산관리", color: "from-blue-500 to-blue-600" },
{ icon: CheckSquare, title: "품질관리", color: "from-green-500 to-green-600" },
{ icon: Package, title: "자재관리", color: "from-orange-500 to-orange-600" },
{ icon: Cpu, title: "설비관리", color: "from-purple-500 to-purple-600" },
{ icon: BarChart3, title: "대시보드", color: "from-indigo-500 to-indigo-600" },
{ icon: Shield, title: "시스템관리", color: "from-red-500 to-red-600" },
{ icon: FileText, title: "기준정보", color: "from-teal-500 to-teal-600" },
{ icon: TrendingUp, title: "보고서", color: "from-pink-500 to-pink-600" },
].map((feature, i) => {
const Icon = feature.icon;
return (
<div key={i} className="bg-white rounded-xl p-6 text-center shadow-md hover:shadow-lg transition-shadow">
<div className={`w-14 h-14 mx-auto mb-4 rounded-xl bg-gradient-to-br ${feature.color} flex items-center justify-center shadow-md`}>
<Icon className="w-7 h-7 text-white" />
</div>
<h3 className="font-bold text-gray-900">{feature.title}</h3>
</div>
);
})}
</div>
</div>
</section>
{/* CTA Section */}
<section className="relative py-20 bg-gradient-to-br from-blue-600 via-blue-500 to-purple-600">
<div className="container mx-auto px-6 text-center">
<h2 className="mb-6 text-white text-3xl md:text-5xl font-extrabold">
</h2>
<p className="text-white/90 text-lg mb-8">
1
</p>
<Button
size="lg"
onClick={handleDemoRequest}
className="rounded-full px-12 py-7 text-lg bg-white text-blue-600 hover:bg-gray-50 shadow-2xl hover:scale-105 transition-all font-bold"
>
<Sparkles className="mr-2 w-5 h-5" />
<ArrowRight className="ml-2 w-5 h-5" />
</Button>
</div>
</section>
{/* Footer */}
<footer className="bg-white border-t border-gray-200">
<div className="container mx-auto px-6 py-12">
<div className="grid md:grid-cols-4 gap-8 mb-8">
<div className="md:col-span-2">
<div className="flex items-center space-x-3 mb-4">
<div className="w-10 h-10 rounded-xl flex items-center justify-center shadow-md bg-gradient-to-br from-blue-500 to-blue-600">
<div className="text-white font-bold text-lg">S</div>
</div>
<div>
<h3 className="font-bold text-gray-900">SAM</h3>
<p className="text-xs text-gray-500">Smart Automation Management</p>
</div>
</div>
<p className="text-gray-600 text-sm max-w-md">
· MES
</p>
</div>
<div>
<h4 className="font-bold mb-3 text-gray-900"></h4>
<ul className="space-y-2 text-sm text-gray-600">
<li><a href="#" className="hover:text-blue-600"> </a></li>
<li><a href="#" onClick={(e) => { e.preventDefault(); handleDemoRequest(); }} className="hover:text-blue-600 cursor-pointer"> </a></li>
</ul>
</div>
<div>
<h4 className="font-bold mb-3 text-gray-900"></h4>
<ul className="space-y-2 text-sm text-gray-600">
<li><a href="#" className="hover:text-blue-600"> </a></li>
<li><a href="#" className="hover:text-blue-600"></a></li>
</ul>
</div>
</div>
<div className="border-t border-gray-200 pt-6 text-center text-sm text-gray-500">
<p>© 2025 SAM. All rights reserved.</p>
</div>
</div>
</footer>
{/* Contact Modal */}
<ContactModal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
/>
{/* Float Animation */}
<style>{`
@keyframes float {
0%, 100% {
transform: translateY(0px);
}
50% {
transform: translateY(-20px);
}
}
.animate-float {
animation: float 3s ease-in-out infinite;
}
`}</style>
</div>
);
}

View File

@@ -1,258 +0,0 @@
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Badge } from "@/components/ui/badge";
import {
Mail,
Lock,
Eye,
EyeOff,
ArrowRight,
Building2
} from "lucide-react";
export function LoginPage() {
const navigate = useNavigate();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [showPassword, setShowPassword] = useState(false);
const [rememberMe, setRememberMe] = useState(false);
const [error, setError] = useState("");
const handleLogin = () => {
setError("");
// 간단한 데모 로그인 검증
if (!email || !password) {
setError("이메일과 비밀번호를 입력해주세요");
return;
}
// 데모 계정들
const demoAccounts = [
{ email: "ceo@demo.com", password: "demo1234", role: "CEO", name: "김대표" },
{ email: "manager@demo.com", password: "demo1234", role: "ProductionManager", name: "이생산" },
{ email: "worker@demo.com", password: "demo1234", role: "Worker", name: "박작업" },
{ email: "admin@demo.com", password: "demo1234", role: "SystemAdmin", name: "최시스템" },
{ email: "sales@demo.com", password: "demo1234", role: "Sales", name: "박영업" },
];
const account = demoAccounts.find(acc => acc.email === email && acc.password === password);
if (account) {
// Save user data to localStorage
const userData = {
email: account.email,
role: account.role,
name: account.name,
companyName: "데모 기업",
};
localStorage.setItem("user", JSON.stringify(userData));
// Navigate to dashboard
navigate("/dashboard");
} else {
setError("이메일 또는 비밀번호가 올바르지 않습니다");
}
};
const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === "Enter") {
handleLogin();
}
};
return (
<div className="min-h-screen bg-background flex flex-col">
{/* Header */}
<header className="clean-glass border-b border-border">
<div className="container mx-auto px-6 py-4">
<div className="flex items-center justify-between">
<button
onClick={() => navigate("/")}
className="flex items-center space-x-3 hover:opacity-80 transition-opacity"
>
<div className="w-10 h-10 rounded-xl flex items-center justify-center clean-shadow relative overflow-hidden" style={{ backgroundColor: '#3B82F6' }}>
<div className="text-white font-bold text-lg">S</div>
<div className="absolute inset-0 bg-gradient-to-br from-white/10 to-transparent opacity-30"></div>
</div>
<div>
<h1 className="text-xl font-bold tracking-wide">SAM</h1>
<p className="text-xs text-muted-foreground"></p>
</div>
</button>
<Button variant="ghost" onClick={() => navigate("/signup")} className="rounded-xl">
</Button>
</div>
</div>
</header>
{/* Main Content */}
<div className="flex-1 flex items-center justify-center px-6 py-12">
<div className="w-full max-w-md">
{/* Login Card */}
<div className="clean-glass rounded-2xl p-8 clean-shadow space-y-6">
<div className="text-center">
<h2 className="mb-2 text-foreground"></h2>
<p className="text-muted-foreground">SAM MES </p>
</div>
{error && (
<div className="bg-destructive/10 border border-destructive/30 rounded-xl p-4">
<p className="text-sm text-destructive text-center">{error}</p>
</div>
)}
<div className="space-y-4">
<div>
<Label htmlFor="email" className="flex items-center space-x-2 mb-2">
<Mail className="w-4 h-4" />
<span></span>
</Label>
<Input
id="email"
type="email"
placeholder="example@company.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
onKeyPress={handleKeyPress}
className="clean-input"
/>
</div>
<div>
<Label htmlFor="password" className="flex items-center space-x-2 mb-2">
<Lock className="w-4 h-4" />
<span></span>
</Label>
<div className="relative">
<Input
id="password"
type={showPassword ? "text" : "password"}
placeholder="비밀번호를 입력하세요"
value={password}
onChange={(e) => setPassword(e.target.value)}
onKeyPress={handleKeyPress}
className="clean-input pr-10"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
>
{showPassword ? (
<EyeOff className="w-5 h-5" />
) : (
<Eye className="w-5 h-5" />
)}
</button>
</div>
</div>
<div className="flex items-center justify-between">
<label className="flex items-center space-x-2 cursor-pointer">
<input
type="checkbox"
checked={rememberMe}
onChange={(e) => setRememberMe(e.target.checked)}
className="w-4 h-4 rounded border-border"
/>
<span className="text-sm text-muted-foreground"> </span>
</label>
<button className="text-sm text-primary hover:underline">
</button>
</div>
</div>
<Button
onClick={handleLogin}
className="w-full rounded-xl bg-primary hover:bg-primary/90"
>
<ArrowRight className="ml-2 w-4 h-4" />
</Button>
<div className="relative">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-border"></div>
</div>
<div className="relative flex justify-center text-sm">
<span className="px-4 bg-card text-muted-foreground"></span>
</div>
</div>
<div className="space-y-3">
<Button
variant="outline"
onClick={() => navigate("/signup")}
className="w-full rounded-xl"
>
</Button>
</div>
</div>
{/* Demo Info */}
<div className="mt-6 clean-glass rounded-xl p-6 clean-shadow">
<div className="flex items-start space-x-3">
<Building2 className="w-5 h-5 text-primary flex-shrink-0 mt-0.5" />
<div>
<h3 className="font-semibold text-foreground mb-2"> </h3>
<p className="text-sm text-muted-foreground mb-3">
:
</p>
<div className="space-y-2 text-sm">
<div className="bg-muted/50 rounded-lg p-3">
<p className="font-medium text-foreground"></p>
<p className="text-muted-foreground">이메일: ceo@demo.com</p>
<p className="text-muted-foreground">비밀번호: demo1234</p>
</div>
<div className="bg-muted/50 rounded-lg p-3">
<p className="font-medium text-foreground"></p>
<p className="text-muted-foreground">이메일: manager@demo.com</p>
<p className="text-muted-foreground">비밀번호: demo1234</p>
</div>
<div className="bg-muted/50 rounded-lg p-3">
<p className="font-medium text-foreground"></p>
<p className="text-muted-foreground">이메일: worker@demo.com</p>
<p className="text-muted-foreground">비밀번호: demo1234</p>
</div>
<div className="bg-muted/50 rounded-lg p-3">
<p className="font-medium text-foreground"></p>
<p className="text-muted-foreground">이메일: admin@demo.com</p>
<p className="text-muted-foreground">비밀번호: demo1234</p>
</div>
<div className="bg-primary/10 rounded-lg p-3 border border-primary/30">
<p className="font-medium text-foreground flex items-center">
( )
<Badge className="ml-2 bg-primary text-white text-xs">NEW</Badge>
</p>
<p className="text-muted-foreground">이메일: sales@demo.com</p>
<p className="text-muted-foreground">비밀번호: demo1234</p>
</div>
</div>
</div>
</div>
</div>
{/* Signup Link */}
<div className="text-center mt-6">
<p className="text-sm text-muted-foreground">
?{" "}
<button
onClick={() => navigate("/signup")}
className="text-primary font-medium hover:underline"
>
30
</button>
</p>
</div>
</div>
</div>
</div>
);
}

View File

@@ -1,370 +0,0 @@
import { useState } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import {
Archive,
Search,
Filter,
BarChart3,
Package,
Calendar,
AlertTriangle,
CheckCircle,
Clock,
QrCode,
MapPin
} from "lucide-react";
interface Lot {
id: string;
lotNumber: string;
itemCode: string;
itemName: string;
quantity: number;
unit: string;
manufactureDate: string;
expiryDate: string;
location: string;
status: "정상" | "만료임박" | "만료";
supplier: string;
daysUntilExpiry: number;
}
export function LotManagement() {
const [searchTerm, setSearchTerm] = useState("");
const [selectedStatus, setSelectedStatus] = useState("all");
const [selectedLocation, setSelectedLocation] = useState("all");
const lots: Lot[] = [
{
id: "1",
lotNumber: "LOT-2024-A-001",
itemCode: "RM-001",
itemName: "스테인리스 강판 304",
quantity: 850,
unit: "kg",
manufactureDate: "2024-08-15",
expiryDate: "2025-08-15",
location: "A동-1구역",
status: "정상",
supplier: "포스코",
daysUntilExpiry: 304
},
{
id: "2",
lotNumber: "LOT-2024-B-015",
itemCode: "RM-002",
itemName: "구리 파이프",
quantity: 120,
unit: "m",
manufactureDate: "2024-09-20",
expiryDate: "2025-09-20",
location: "A동-2구역",
status: "정상",
supplier: "LG화학",
daysUntilExpiry: 340
},
{
id: "3",
lotNumber: "LOT-2023-C-042",
itemCode: "RM-003",
itemName: "플라스틱 원료 ABS",
quantity: 450,
unit: "kg",
manufactureDate: "2023-11-10",
expiryDate: "2024-11-10",
location: "B동-1구역",
status: "만료임박",
supplier: "한화솔루션",
daysUntilExpiry: 26
},
{
id: "4",
lotNumber: "LOT-2023-A-128",
itemCode: "SM-001",
itemName: "볼트 M6",
quantity: 5000,
unit: "개",
manufactureDate: "2023-06-15",
expiryDate: "2024-06-15",
location: "C동-3구역",
status: "만료",
supplier: "삼성정밀",
daysUntilExpiry: -122
},
{
id: "5",
lotNumber: "LOT-2024-D-008",
itemCode: "WP-001",
itemName: "중간 조립품 A",
quantity: 45,
unit: "개",
manufactureDate: "2024-10-01",
expiryDate: "2025-10-01",
location: "D동-1구역",
status: "정상",
supplier: "자체생산",
daysUntilExpiry: 351
}
];
const locations = ["all", "A동-1구역", "A동-2구역", "B동-1구역", "C동-3구역", "D동-1구역"];
const statuses = ["all", "정상", "만료임박", "만료"];
const filteredLots = lots.filter(lot => {
const matchesSearch = lot.lotNumber.toLowerCase().includes(searchTerm.toLowerCase()) ||
lot.itemName.toLowerCase().includes(searchTerm.toLowerCase()) ||
lot.itemCode.toLowerCase().includes(searchTerm.toLowerCase());
const matchesStatus = selectedStatus === "all" || lot.status === selectedStatus;
const matchesLocation = selectedLocation === "all" || lot.location === selectedLocation;
return matchesSearch && matchesStatus && matchesLocation;
});
const stats = {
total: lots.length,
normal: lots.filter(l => l.status === "정상").length,
nearExpiry: lots.filter(l => l.status === "만료임박").length,
expired: lots.filter(l => l.status === "만료").length
};
return (
<div className="p-6 space-y-6">
{/* 헤더 */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<h1 className="text-3xl font-bold text-foreground flex items-center gap-3">
<div className="w-10 h-10 bg-orange-100 rounded-lg flex items-center justify-center">
<Archive className="h-6 w-6 text-orange-600" />
</div>
</h1>
<p className="text-muted-foreground mt-1"> </p>
</div>
<div className="flex gap-2">
<Button variant="outline">
<QrCode className="h-4 w-4 mr-2" />
QR
</Button>
<Button variant="outline">
<BarChart3 className="h-4 w-4 mr-2" />
</Button>
</div>
</div>
{/* 통계 카드 */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
<Card className="border-l-4 border-l-orange-500">
<CardHeader className="pb-3">
<CardTitle className="text-sm text-muted-foreground"> </CardTitle>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-orange-600">{stats.total}</div>
<p className="text-xs text-muted-foreground mt-1"> </p>
</CardContent>
</Card>
<Card className="border-l-4 border-l-green-500">
<CardHeader className="pb-3">
<CardTitle className="text-sm text-muted-foreground"></CardTitle>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-green-600">{stats.normal}</div>
<p className="text-xs text-muted-foreground mt-1"> </p>
</CardContent>
</Card>
<Card className="border-l-4 border-l-yellow-500">
<CardHeader className="pb-3">
<CardTitle className="text-sm text-muted-foreground"> </CardTitle>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-yellow-600">{stats.nearExpiry}</div>
<p className="text-xs text-muted-foreground mt-1">30 </p>
</CardContent>
</Card>
<Card className="border-l-4 border-l-red-500">
<CardHeader className="pb-3">
<CardTitle className="text-sm text-muted-foreground"></CardTitle>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-red-600">{stats.expired}</div>
<p className="text-xs text-muted-foreground mt-1"> </p>
</CardContent>
</Card>
</div>
{/* 검색 및 필터 */}
<Card>
<CardContent className="pt-6">
<div className="flex flex-col md:flex-row gap-4">
<div className="flex-1 relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="로트번호, 품목명, 품목코드로 검색..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
<Select value={selectedStatus} onValueChange={setSelectedStatus}>
<SelectTrigger className="w-full md:w-48">
<SelectValue placeholder="상태" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"> </SelectItem>
{statuses.filter(s => s !== "all").map(status => (
<SelectItem key={status} value={status}>{status}</SelectItem>
))}
</SelectContent>
</Select>
<Select value={selectedLocation} onValueChange={setSelectedLocation}>
<SelectTrigger className="w-full md:w-48">
<SelectValue placeholder="보관 위치" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"> </SelectItem>
{locations.filter(l => l !== "all").map(location => (
<SelectItem key={location} value={location}>{location}</SelectItem>
))}
</SelectContent>
</Select>
</div>
</CardContent>
</Card>
{/* 로트 목록 테이블 */}
<Card>
<CardHeader>
<CardTitle> ({filteredLots.length})</CardTitle>
</CardHeader>
<CardContent>
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-40"> </TableHead>
<TableHead> </TableHead>
<TableHead></TableHead>
<TableHead className="text-right"></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="text-right"></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredLots.map((lot) => (
<TableRow key={lot.id}>
<TableCell className="font-mono text-sm font-medium">
{lot.lotNumber}
</TableCell>
<TableCell className="font-mono text-xs text-muted-foreground">
{lot.itemCode}
</TableCell>
<TableCell className="font-medium">{lot.itemName}</TableCell>
<TableCell className="text-right font-medium">
{lot.quantity.toLocaleString()}
</TableCell>
<TableCell className="text-sm">{lot.unit}</TableCell>
<TableCell className="text-sm">
<div className="flex items-center gap-1">
<Calendar className="h-3 w-3 text-muted-foreground" />
{lot.manufactureDate}
</div>
</TableCell>
<TableCell className="text-sm">
<div className="flex items-center gap-1">
<Clock className="h-3 w-3 text-muted-foreground" />
{lot.expiryDate}
</div>
</TableCell>
<TableCell className="text-right">
<span className={`font-medium ${
lot.daysUntilExpiry < 0 ? 'text-red-600' :
lot.daysUntilExpiry <= 30 ? 'text-yellow-600' :
'text-green-600'
}`}>
{lot.daysUntilExpiry < 0 ?
`만료 ${Math.abs(lot.daysUntilExpiry)}` :
`${lot.daysUntilExpiry}`
}
</span>
</TableCell>
<TableCell className="text-sm">
<div className="flex items-center gap-1">
<MapPin className="h-3 w-3 text-muted-foreground" />
{lot.location}
</div>
</TableCell>
<TableCell className="text-sm text-muted-foreground">
{lot.supplier}
</TableCell>
<TableCell>
<Badge className={
lot.status === "정상" ? "bg-green-500 text-white" :
lot.status === "만료임박" ? "bg-yellow-500 text-white" :
"bg-red-500 text-white"
}>
{lot.status === "정상" && <CheckCircle className="h-3 w-3 mr-1" />}
{lot.status === "만료임박" && <AlertTriangle className="h-3 w-3 mr-1" />}
{lot.status === "만료" && <AlertTriangle className="h-3 w-3 mr-1" />}
{lot.status}
</Badge>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
{/* 만료 임박 알림 */}
{stats.nearExpiry > 0 && (
<Card className="border-2 border-yellow-500 bg-yellow-50">
<CardContent className="pt-6">
<div className="flex items-start gap-3">
<AlertTriangle className="h-5 w-5 text-yellow-600 mt-0.5" />
<div>
<h3 className="font-bold text-yellow-900 mb-1">
{stats.nearExpiry}
</h3>
<p className="text-sm text-yellow-800">
30 . .
</p>
</div>
</div>
</CardContent>
</Card>
)}
{/* 만료 알림 */}
{stats.expired > 0 && (
<Card className="border-2 border-red-500 bg-red-50">
<CardContent className="pt-6">
<div className="flex items-start gap-3">
<AlertTriangle className="h-5 w-5 text-red-600 mt-0.5" />
<div>
<h3 className="font-bold text-red-900 mb-1">
{stats.expired}
</h3>
<p className="text-sm text-red-800">
. .
</p>
</div>
</div>
</CardContent>
</Card>
)}
</div>
);
}

View File

@@ -10,11 +10,8 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
import {
DollarSign,
TrendingUp,
Factory,
Users,
Package,
AlertTriangle,
CheckCircle,
Clock,
FileText,
Truck,
@@ -22,7 +19,6 @@ import {
ArrowUpRight,
ArrowDownRight,
Zap,
Shield,
Activity,
Banknote,
CreditCard,
@@ -51,7 +47,14 @@ import {
} from "lucide-react";
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, LineChart, Line, Area, AreaChart } from "recharts";
export function CEODashboard() {
/**
* MainDashboard -
*
* ,
* .
* API에서 .
*/
export function MainDashboard() {
const currentTime = useCurrentTime();
const [calendarDate, setCalendarDate] = useState<Date | undefined>(new Date());
@@ -1219,7 +1222,7 @@ export function CEODashboard() {
borderRadius: '8px',
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1)'
}}
formatter={(value: any) => [`${value}M원`, '']}
formatter={(value: number | string) => [`${value}M원`, '']}
labelFormatter={(label) => `${label.split('-')[1]}`}
/>
<Bar dataKey="target" fill="#94a3b8" name="목표" radius={[4, 4, 0, 0]} />
@@ -1287,7 +1290,7 @@ export function CEODashboard() {
borderRadius: '8px',
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1)'
}}
formatter={(value: any) => [`${value}M원`, '']}
formatter={(value: number | string) => [`${value}M원`, '']}
labelFormatter={(label) => `${label.split('-')[1]}`}
/>
<Line

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,112 +0,0 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Settings, Code, Sparkles, Eye, CheckCircle } from "lucide-react";
export default function MenuCustomizationGuide() {
return (
<div className="max-w-4xl mx-auto p-6 space-y-6">
<Card className="bg-gradient-to-br from-purple-50 to-pink-50 border-purple-200">
<CardHeader>
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-xl bg-purple-500 flex items-center justify-center">
<Code className="w-6 h-6 text-white" />
</div>
<div>
<CardTitle className="text-2xl"> </CardTitle>
<CardDescription> </CardDescription>
</div>
</div>
</CardHeader>
<CardContent className="space-y-6">
<div>
<h3 className="font-bold text-lg mb-3 flex items-center gap-2">
<Eye className="w-5 h-5 text-purple-600" />
</h3>
<div className="space-y-2">
<div className="flex items-start gap-3 p-3 bg-white rounded-lg">
<Badge className="bg-purple-500 text-white">1</Badge>
<div>
<p className="font-semibold"> </p>
<p className="text-sm text-gray-600"> "역할 선택" <strong>"시스템관리자"</strong> </p>
</div>
</div>
<div className="flex items-start gap-3 p-3 bg-white rounded-lg">
<Badge className="bg-purple-500 text-white">2</Badge>
<div>
<p className="font-semibold"> </p>
<p className="text-sm text-gray-600"> <Settings className="w-4 h-4 inline text-purple-600" /> <strong>"시스템 설정"</strong> </p>
</div>
</div>
<div className="flex items-start gap-3 p-3 bg-white rounded-lg">
<Badge className="bg-purple-500 text-white">3</Badge>
<div>
<p className="font-semibold"> </p>
<p className="text-sm text-gray-600"> <Code className="w-4 h-4 inline text-purple-600" /> <strong>"메뉴"</strong> </p>
</div>
</div>
<div className="flex items-start gap-3 p-3 bg-white rounded-lg">
<Badge className="bg-purple-500 text-white">4</Badge>
<div>
<p className="font-semibold"> !</p>
<p className="text-sm text-gray-600">, , </p>
</div>
</div>
</div>
</div>
<div className="border-t pt-6">
<h3 className="font-bold text-lg mb-3 flex items-center gap-2">
<Sparkles className="w-5 h-5 text-pink-600" />
AI
</h3>
<div className="space-y-2">
<div className="flex items-start gap-3 p-3 bg-white rounded-lg">
<Badge className="bg-pink-500 text-white">1</Badge>
<div>
<p className="font-semibold">AI </p>
<p className="text-sm text-gray-600"> <Sparkles className="w-4 h-4 inline text-pink-600" /> "AI 자동 매칭" </p>
</div>
</div>
<div className="flex items-start gap-3 p-3 bg-white rounded-lg">
<Badge className="bg-pink-500 text-white">2</Badge>
<div>
<p className="font-semibold"> </p>
<p className="text-sm text-gray-600">8 (, , , )</p>
</div>
</div>
<div className="flex items-start gap-3 p-3 bg-white rounded-lg">
<Badge className="bg-pink-500 text-white">3</Badge>
<div>
<p className="font-semibold"> </p>
<p className="text-sm text-gray-600">"프리셋 적용" AI가 !</p>
</div>
</div>
</div>
</div>
<div className="border-t pt-6">
<h3 className="font-bold text-lg mb-3 flex items-center gap-2">
<CheckCircle className="w-5 h-5 text-green-600" />
</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
<div className="p-3 bg-white rounded-lg border-l-4 border-blue-500">
<p className="font-semibold text-sm"> </p>
<p className="text-xs text-gray-600 mt-1"> </p>
</div>
<div className="p-3 bg-white rounded-lg border-l-4 border-green-500">
<p className="font-semibold text-sm"> </p>
<p className="text-xs text-gray-600 mt-1"> , , </p>
</div>
<div className="p-3 bg-white rounded-lg border-l-4 border-purple-500">
<p className="font-semibold text-sm"> </p>
<p className="text-xs text-gray-600 mt-1"> </p>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
);
}

View File

@@ -1,622 +0,0 @@
import { useState } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Badge } from "@/components/ui/badge";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
import { Progress } from "@/components/ui/progress";
import { Textarea } from "@/components/ui/textarea";
import {
Search,
Plus,
Download,
Filter,
ShoppingCart,
Calendar,
User,
Package,
Truck,
CheckCircle,
Eye,
Edit,
Clock,
FileText,
Building,
Phone,
Mail,
Calculator,
TrendingUp,
AlertTriangle,
RefreshCw
} from "lucide-react";
import { BarChart, Bar, LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from "recharts";
export function OrderManagement() {
const [activeTab, setActiveTab] = useState("orders");
const [searchTerm, setSearchTerm] = useState("");
const [selectedStatus, setSelectedStatus] = useState("all");
const [isAddOrderOpen, setIsAddOrderOpen] = useState(false);
// 주문 데이터
const orders = [
{
id: "ORD001",
orderNo: "ORD-2024-12-001",
customerName: "삼성전자",
customerCode: "CUS-001",
productName: "자동 가이드 레일 시스템",
productCode: "AGRS-2024",
quantity: 50,
unitPrice: 850000,
totalAmount: 42500000,
orderDate: "2024-12-25",
deliveryDate: "2025-01-15",
status: "진행중",
progress: 65,
manager: "한영업"
},
{
id: "ORD002",
orderNo: "ORD-2024-12-002",
customerName: "LG전자",
customerCode: "CUS-002",
productName: "스마트 케이스 모듈",
productCode: "SCM-2024",
quantity: 100,
unitPrice: 450000,
totalAmount: 45000000,
orderDate: "2024-12-28",
deliveryDate: "2025-01-20",
status: "접수",
progress: 10,
manager: "한영업"
},
{
id: "ORD003",
orderNo: "ORD-2024-12-003",
customerName: "현대자동차",
customerCode: "CUS-003",
productName: "하단 마감재 어셈블리",
productCode: "BFA-2024",
quantity: 200,
unitPrice: 250000,
totalAmount: 50000000,
orderDate: "2024-12-30",
deliveryDate: "2025-02-01",
status: "견적",
progress: 5,
manager: "김영업"
}
];
// 고객사 데이터
const customers = [
{
id: "CUS-001",
name: "삼성전자",
code: "SEC",
contact: "김구매",
phone: "02-2255-0114",
email: "purchase@samsung.com",
address: "서울시 서초구 서초대로 74길 11",
rating: "A+",
totalOrders: 25,
totalAmount: 850000000,
lastOrderDate: "2024-12-25"
},
{
id: "CUS-002",
name: "LG전자",
code: "LGE",
contact: "이구매",
phone: "02-3777-1114",
email: "order@lge.co.kr",
address: "서울시 영등포구 여의대로 128",
rating: "A",
totalOrders: 18,
totalAmount: 650000000,
lastOrderDate: "2024-12-28"
},
{
id: "CUS-003",
name: "현대자동차",
code: "HMC",
contact: "박구매",
phone: "02-3464-1114",
email: "procurement@hyundai.com",
address: "서울시 서초구 헌릉로 12",
rating: "A+",
totalOrders: 32,
totalAmount: 1200000000,
lastOrderDate: "2024-12-30"
}
];
// 주문 통계
const orderStats = {
totalOrders: orders.length,
totalAmount: orders.reduce((sum, order) => sum + order.totalAmount, 0),
pendingOrders: orders.filter(order => order.status === "견적").length,
processingOrders: orders.filter(order => order.status === "진행중").length,
completedOrders: orders.filter(order => order.status === "완료").length
};
// 월별 주문 추이 데이터
const monthlyOrders = [
{ month: "8월", orders: 15, amount: 125000000 },
{ month: "9월", orders: 18, amount: 145000000 },
{ month: "10월", orders: 22, amount: 180000000 },
{ month: "11월", orders: 20, amount: 165000000 },
{ month: "12월", orders: 25, amount: 195000000 }
];
const getStatusColor = (status: string) => {
switch (status) {
case "견적": return "bg-gray-500 text-white";
case "접수": return "bg-blue-500 text-white";
case "진행중": return "bg-orange-500 text-white";
case "완료": return "bg-green-500 text-white";
case "취소": return "bg-red-500 text-white";
default: return "bg-gray-500 text-white";
}
};
const getRatingColor = (rating: string) => {
switch (rating) {
case "A+": return "bg-green-500 text-white";
case "A": return "bg-blue-500 text-white";
case "B": return "bg-yellow-500 text-white";
case "C": return "bg-orange-500 text-white";
default: return "bg-gray-500 text-white";
}
};
const filteredOrders = orders.filter(order => {
const matchesSearch = order.customerName.toLowerCase().includes(searchTerm.toLowerCase()) ||
order.orderNo.toLowerCase().includes(searchTerm.toLowerCase()) ||
order.productName.toLowerCase().includes(searchTerm.toLowerCase());
const matchesStatus = selectedStatus === "all" || order.status === selectedStatus;
return matchesSearch && matchesStatus;
});
return (
<div className="p-6 space-y-6 bg-background min-h-screen">
{/* 헤더 */}
<div className="flex justify-between items-center">
<div>
<h1 className="text-3xl font-bold text-foreground"> </h1>
<p className="text-muted-foreground mt-1"> </p>
</div>
<div className="flex space-x-3">
<Button variant="outline">
<FileText className="w-4 h-4 mr-2" />
</Button>
<Dialog open={isAddOrderOpen} onOpenChange={setIsAddOrderOpen}>
<DialogTrigger asChild>
<Button>
<Plus className="w-4 h-4 mr-2" />
</Button>
</DialogTrigger>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle> </DialogTitle>
</DialogHeader>
<div className="space-y-4 max-h-96 overflow-y-auto">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="customer"></Label>
<Select>
<SelectTrigger>
<SelectValue placeholder="고객사 선택" />
</SelectTrigger>
<SelectContent>
{customers.map(customer => (
<SelectItem key={customer.id} value={customer.id}>
{customer.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="orderDate"></Label>
<Input id="orderDate" type="date" />
</div>
</div>
<div className="space-y-2">
<Label htmlFor="product"></Label>
<Input id="product" placeholder="제품명 입력" />
</div>
<div className="grid grid-cols-3 gap-4">
<div className="space-y-2">
<Label htmlFor="quantity"></Label>
<Input id="quantity" type="number" placeholder="50" />
</div>
<div className="space-y-2">
<Label htmlFor="unitPrice"></Label>
<Input id="unitPrice" type="number" placeholder="850000" />
</div>
<div className="space-y-2">
<Label htmlFor="deliveryDate"></Label>
<Input id="deliveryDate" type="date" />
</div>
</div>
<div className="space-y-2">
<Label htmlFor="requirements"></Label>
<Textarea id="requirements" placeholder="고객 요구사항 및 특이사항" />
</div>
<div className="flex space-x-2 pt-4">
<Button className="flex-1" onClick={() => setIsAddOrderOpen(false)}>
</Button>
<Button variant="outline" className="flex-1" onClick={() => setIsAddOrderOpen(false)}>
</Button>
</div>
</div>
</DialogContent>
</Dialog>
</div>
</div>
{/* 주문 현황 대시보드 */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<Card className="samsung-card">
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-muted-foreground"> </p>
<p className="text-2xl font-bold text-primary">{orderStats.totalOrders}</p>
</div>
<ShoppingCart className="h-8 w-8 text-muted-foreground" />
</div>
</CardContent>
</Card>
<Card className="samsung-card">
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-muted-foreground"> </p>
<p className="text-2xl font-bold text-green-600">{orderStats.totalAmount.toLocaleString()}</p>
</div>
<Calculator className="h-8 w-8 text-green-500" />
</div>
</CardContent>
</Card>
<Card className="samsung-card">
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-muted-foreground"> </p>
<p className="text-2xl font-bold text-orange-600">{orderStats.processingOrders}</p>
</div>
<Clock className="h-8 w-8 text-orange-500" />
</div>
</CardContent>
</Card>
<Card className="samsung-card">
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-muted-foreground"> </p>
<p className="text-2xl font-bold text-blue-600">{orderStats.pendingOrders}</p>
</div>
<FileText className="h-8 w-8 text-blue-500" />
</div>
</CardContent>
</Card>
</div>
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-6">
<TabsList className="grid w-full grid-cols-3 bg-muted rounded-2xl p-1">
<TabsTrigger value="orders" className="rounded-xl">
<ShoppingCart className="w-4 h-4 mr-2" />
</TabsTrigger>
<TabsTrigger value="customers" className="rounded-xl">
<Building className="w-4 h-4 mr-2" />
</TabsTrigger>
<TabsTrigger value="analysis" className="rounded-xl">
<TrendingUp className="w-4 h-4 mr-2" />
</TabsTrigger>
</TabsList>
<TabsContent value="orders">
{/* 필터 및 검색 */}
<Card className="samsung-card">
<CardContent className="p-6">
<div className="flex flex-wrap gap-4 items-end">
<div className="flex-1 min-w-64">
<Label htmlFor="search" className="text-sm font-medium"></Label>
<div className="relative mt-1">
<Search className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
<Input
id="search"
placeholder="고객사명, 주문번호, 제품명으로 검색..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
</div>
<div className="w-32">
<Label className="text-sm font-medium"></Label>
<Select value={selectedStatus} onValueChange={setSelectedStatus}>
<SelectTrigger className="mt-1">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="견적"></SelectItem>
<SelectItem value="접수"></SelectItem>
<SelectItem value="진행중"></SelectItem>
<SelectItem value="완료"></SelectItem>
</SelectContent>
</Select>
</div>
<Button variant="outline">
<Download className="w-4 h-4 mr-2" />
Excel
</Button>
</div>
</CardContent>
</Card>
{/* 주문 목록 */}
<Card className="samsung-card">
<CardHeader>
<CardTitle className="flex items-center justify-between">
<span> ({filteredOrders.length})</span>
<Button size="sm" variant="outline">
<RefreshCw className="w-4 h-4 mr-2" />
</Button>
</CardTitle>
</CardHeader>
<CardContent>
<div className="rounded-lg border overflow-hidden">
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredOrders.map((order) => (
<TableRow key={order.id}>
<TableCell className="font-mono text-sm">{order.orderNo}</TableCell>
<TableCell className="font-medium">{order.customerName}</TableCell>
<TableCell>{order.productName}</TableCell>
<TableCell>{order.quantity.toLocaleString()}</TableCell>
<TableCell>{order.unitPrice.toLocaleString()}</TableCell>
<TableCell className="font-medium">{order.totalAmount.toLocaleString()}</TableCell>
<TableCell>{order.orderDate}</TableCell>
<TableCell>{order.deliveryDate}</TableCell>
<TableCell>
<div className="w-16">
<Progress value={order.progress} className="h-2" />
<span className="text-xs text-muted-foreground">
{order.progress}%
</span>
</div>
</TableCell>
<TableCell>
<Badge className={getStatusColor(order.status)}>
{order.status}
</Badge>
</TableCell>
<TableCell>
<div className="flex items-center space-x-1">
<User className="w-3 h-3 text-muted-foreground" />
<span className="text-sm">{order.manager}</span>
</div>
</TableCell>
<TableCell>
<div className="flex items-center space-x-2">
<Button size="sm" variant="outline" className="px-2">
<Eye className="w-3 h-3" />
</Button>
<Button size="sm" variant="outline" className="px-2">
<Edit className="w-3 h-3" />
</Button>
<Button size="sm" variant="outline" className="px-2">
<FileText className="w-3 h-3" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="customers">
<Card className="samsung-card">
<CardHeader>
<CardTitle className="flex items-center justify-between">
<span> </span>
<Button>
<Plus className="w-4 h-4 mr-2" />
</Button>
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-6">
{customers.map((customer) => (
<div key={customer.id} className="p-6 border rounded-2xl bg-gray-50/50">
<div className="flex justify-between items-start mb-4">
<div>
<h4 className="font-semibold text-lg">{customer.name}</h4>
<p className="text-muted-foreground text-sm">{customer.code}</p>
</div>
<Badge className={getRatingColor(customer.rating)}>
{customer.rating}
</Badge>
</div>
<div className="space-y-3">
<div className="flex items-center space-x-2">
<User className="w-4 h-4 text-muted-foreground" />
<span className="text-sm">{customer.contact}</span>
</div>
<div className="flex items-center space-x-2">
<Phone className="w-4 h-4 text-muted-foreground" />
<span className="text-sm">{customer.phone}</span>
</div>
<div className="flex items-center space-x-2">
<Mail className="w-4 h-4 text-muted-foreground" />
<span className="text-sm">{customer.email}</span>
</div>
</div>
<div className="mt-4 pt-4 border-t">
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<p className="text-muted-foreground"> </p>
<p className="font-medium">{customer.totalOrders}</p>
</div>
<div>
<p className="text-muted-foreground"> </p>
<p className="font-medium">{customer.totalAmount.toLocaleString()}</p>
</div>
</div>
</div>
<div className="mt-4 pt-4 border-t">
<div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground">
: {customer.lastOrderDate}
</span>
<div className="flex space-x-2">
<Button size="sm" variant="outline" className="px-2">
<Eye className="w-3 h-3" />
</Button>
<Button size="sm" variant="outline" className="px-2">
<Edit className="w-3 h-3" />
</Button>
</div>
</div>
</div>
</div>
))}
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="analysis">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* 월별 주문 추이 */}
<Card className="samsung-card">
<CardHeader>
<CardTitle> </CardTitle>
</CardHeader>
<CardContent>
<ResponsiveContainer width="100%" height={300}>
<LineChart data={monthlyOrders}>
<CartesianGrid strokeDasharray="3 3" stroke="#e0e0e0" />
<XAxis dataKey="month" stroke="#666" />
<YAxis stroke="#666" />
<Tooltip />
<Line
type="monotone"
dataKey="orders"
stroke="#1428A0"
strokeWidth={3}
name="주문 건수"
/>
<Line
type="monotone"
dataKey="amount"
stroke="#00D084"
strokeWidth={3}
name="주문 금액(원)"
yAxisId="right"
/>
<Legend />
</LineChart>
</ResponsiveContainer>
</CardContent>
</Card>
{/* 고객사별 주문 현황 */}
<Card className="samsung-card">
<CardHeader>
<CardTitle> </CardTitle>
</CardHeader>
<CardContent>
<ResponsiveContainer width="100%" height={300}>
<BarChart data={customers.slice(0, 5)}>
<CartesianGrid strokeDasharray="3 3" stroke="#e0e0e0" />
<XAxis dataKey="code" stroke="#666" />
<YAxis stroke="#666" />
<Tooltip />
<Bar dataKey="totalOrders" fill="#1428A0" name="주문 건수" />
<Legend />
</BarChart>
</ResponsiveContainer>
</CardContent>
</Card>
</div>
{/* 주문 현황 요약 */}
<Card className="samsung-card">
<CardHeader>
<CardTitle> </CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
<div className="text-center p-6 border rounded-2xl bg-blue-50">
<div className="text-3xl font-bold text-blue-600 mb-2">{orderStats.pendingOrders}</div>
<p className="text-blue-700 font-medium"> </p>
<p className="text-sm text-blue-600 mt-1"> </p>
</div>
<div className="text-center p-6 border rounded-2xl bg-orange-50">
<div className="text-3xl font-bold text-orange-600 mb-2">{orderStats.processingOrders}</div>
<p className="text-orange-700 font-medium"> </p>
<p className="text-sm text-orange-600 mt-1"> </p>
</div>
<div className="text-center p-6 border rounded-2xl bg-green-50">
<div className="text-3xl font-bold text-green-600 mb-2">{orderStats.completedOrders}</div>
<p className="text-green-700 font-medium"></p>
<p className="text-sm text-green-600 mt-1"> </p>
</div>
<div className="text-center p-6 border rounded-2xl bg-purple-50">
<div className="text-3xl font-bold text-purple-600 mb-2">
{(orderStats.totalAmount / 1000000).toFixed(0)}M
</div>
<p className="text-purple-700 font-medium"> </p>
<p className="text-sm text-purple-600 mt-1"> </p>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,980 +0,0 @@
import { useState } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Label } from "@/components/ui/label";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Checkbox } from "@/components/ui/checkbox";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Separator } from "@/components/ui/separator";
import {
Box,
Plus,
Search,
Edit,
Trash2,
Package,
ChevronRight,
Settings2,
FileText
} from "lucide-react";
interface Product {
id: string;
code: string;
name: string;
lotNumber: string;
description: string;
status: "활성" | "비활성";
category?: string;
bomItems?: BOMItem[];
}
interface BOMItem {
id: string;
itemCode: string;
itemName: string;
specification: string;
quantity: number;
unit: string;
note?: string;
}
interface AvailableItem {
id: string;
type: string;
code: string;
name: string;
specification: string;
unit: string;
defaultQty: number;
}
export function ProductManagement() {
const [searchTerm, setSearchTerm] = useState("");
const [selectedStatus, setSelectedStatus] = useState("all");
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
const [selectedProduct, setSelectedProduct] = useState<Product | null>(null);
const [currentTab, setCurrentTab] = useState("basic");
// BOM 관리
const [bomCategory, setBomCategory] = useState("BOM관리");
const [isItemSelectOpen, setIsItemSelectOpen] = useState(false);
const [currentBomGroup, setCurrentBomGroup] = useState<string | null>(null);
const [bomItems, setBomItems] = useState<BOMItem[]>([]);
const [itemSearchTerm, setItemSearchTerm] = useState("");
// 신규 제품
const [newProduct, setNewProduct] = useState<Partial<Product>>({
code: "",
name: "",
lotNumber: "",
description: "",
status: "활성"
});
// 샘플 제품 데이터
const [products, setProducts] = useState<Product[]>([
{
id: "1",
code: "KS501",
name: "스크린센터",
lotNumber: "SS",
description: "스크린센터",
status: "활성"
},
{
id: "2",
code: "KWK503",
name: "대형 세터",
lotNumber: "WS",
description: "11700*8500 이상",
status: "비활성"
},
{
id: "3",
code: "KOTS01",
name: "플랫 세터",
lotNumber: "TS",
description: "플랫 세터(+수지형)",
status: "활성"
}
]);
// 사용 가능한 품목 목록
const availableItems: AvailableItem[] = [
{ id: "1", type: "자재", code: "K-BE-C-E-E0R1330", name: "몸판판", specification: "몸판판-KEU-EAT-'919*3000", unit: "SET", defaultQty: 1 },
{ id: "2", type: "부품", code: "H-SC-S-X-KF02*700", name: "신액배션", specification: "", unit: "SET", defaultQty: 1 },
{ id: "3", type: "자재", code: "K-BE-C-E-E0R2045", name: "몸판판", specification: "몸판판-KEU-I-SET-'125*425", unit: "SET", defaultQty: 1 },
{ id: "4", type: "부품", code: "H-SC-S-X-KF02*C80122", name: "신액배션", specification: "신액배션스크린-U220", unit: "SET", defaultQty: 1 },
{ id: "5", type: "자재", code: "P-ET-C-S-KWS01", name: "관산판", specification: "관산판 스크린(콘솔내판)", unit: "SET", defaultQty: 0 },
{ id: "6", type: "부품", code: "K-ET-C-X-YT01", name: "국제판", specification: "국제판-스틸샷", unit: "SET", defaultQty: 0 },
{ id: "7", type: "자재", code: "P-ET-S-X-YM01", name: "국제판", specification: "국제판-스틸샷", unit: "SET", defaultQty: 0 },
{ id: "8", type: "부품", code: "P-ET-S-X-KGT5001", name: "관산판", specification: "관산판 스크린(콘솔내판)", unit: "SET", defaultQty: 0 },
{ id: "9", type: "자재", code: "P-ET-S-X-KGT1902", name: "버튼판", specification: "버튼판 스크린(콘솔)", unit: "SET", defaultQty: 0 },
{ id: "10", type: "부품", code: "P-ET-S-B-S450", name: "국제판", specification: "국제판 스크린(콘솔)", unit: "SET", defaultQty: 0 }
];
// BOM 그룹 샘플 데이터
const bomGroups = [
{ id: "g1", name: "분류 ID: 3", items: 2 },
{ id: "g2", name: "분류 ID: 4", items: 2 }
];
const filteredProducts = products.filter(product => {
const matchesSearch = product.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
product.code.toLowerCase().includes(searchTerm.toLowerCase());
const matchesStatus = selectedStatus === "all" || product.status === selectedStatus;
return matchesSearch && matchesStatus;
});
const filteredItems = availableItems.filter(item => {
return item.name.toLowerCase().includes(itemSearchTerm.toLowerCase()) ||
item.code.toLowerCase().includes(itemSearchTerm.toLowerCase());
});
const handleEdit = (product: Product) => {
setSelectedProduct(product);
if (product.bomItems) {
setBomItems(product.bomItems);
} else {
setBomItems([]);
}
setIsEditDialogOpen(true);
};
const handleDelete = (product: Product) => {
setSelectedProduct(product);
setIsDeleteDialogOpen(true);
};
const confirmDelete = () => {
if (selectedProduct) {
setProducts(products.filter(p => p.id !== selectedProduct.id));
setIsDeleteDialogOpen(false);
setSelectedProduct(null);
}
};
const handleAddProduct = () => {
const productToAdd: Product = {
id: `${Date.now()}`,
code: newProduct.code || "",
name: newProduct.name || "",
lotNumber: newProduct.lotNumber || "",
description: newProduct.description || "",
status: newProduct.status || "활성",
bomItems: bomItems
};
setProducts([...products, productToAdd]);
setIsAddDialogOpen(false);
setNewProduct({
code: "",
name: "",
lotNumber: "",
description: "",
status: "활성"
});
setBomItems([]);
};
const handleUpdateProduct = () => {
if (selectedProduct) {
setProducts(products.map(p =>
p.id === selectedProduct.id
? { ...selectedProduct, bomItems }
: p
));
setIsEditDialogOpen(false);
setSelectedProduct(null);
}
};
const handleAddBomItems = (selectedIds: string[]) => {
const itemsToAdd = availableItems
.filter(item => selectedIds.includes(item.id))
.map(item => ({
id: `bom-${Date.now()}-${item.id}`,
itemCode: item.code,
itemName: item.name,
specification: item.specification,
quantity: item.defaultQty,
unit: item.unit
}));
setBomItems([...bomItems, ...itemsToAdd]);
setIsItemSelectOpen(false);
};
const stats = {
total: products.length,
active: products.filter(p => p.status === "활성").length,
inactive: products.filter(p => p.status === "비활성").length
};
return (
<div className="p-6 space-y-6">
{/* 헤더 */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<h1 className="text-3xl font-bold text-foreground flex items-center gap-3">
<div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center">
<Box className="h-6 w-6 text-blue-600" />
</div>
</h1>
<p className="text-muted-foreground mt-1">(BOM) </p>
</div>
<div className="flex gap-2">
<Button
className="bg-blue-600 hover:bg-blue-700"
onClick={() => setIsAddDialogOpen(true)}
>
<Plus className="h-4 w-4 mr-2" />
</Button>
</div>
</div>
{/* 통계 카드 */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<Card className="border-l-4 border-l-blue-500">
<CardHeader className="pb-3">
<CardTitle className="text-sm text-muted-foreground"> </CardTitle>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-blue-600">{stats.total}</div>
<p className="text-xs text-muted-foreground mt-1"> </p>
</CardContent>
</Card>
<Card className="border-l-4 border-l-green-500">
<CardHeader className="pb-3">
<CardTitle className="text-sm text-muted-foreground"> </CardTitle>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-green-600">{stats.active}</div>
<p className="text-xs text-muted-foreground mt-1">/ </p>
</CardContent>
</Card>
<Card className="border-l-4 border-l-gray-500">
<CardHeader className="pb-3">
<CardTitle className="text-sm text-muted-foreground"> </CardTitle>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-gray-600">{stats.inactive}</div>
<p className="text-xs text-muted-foreground mt-1">/</p>
</CardContent>
</Card>
</div>
{/* 검색 및 필터 */}
<Card>
<CardContent className="pt-6">
<div className="flex flex-col md:flex-row gap-4">
<div className="flex-1 relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="제품코드/명 검색..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
<Select value={selectedStatus} onValueChange={setSelectedStatus}>
<SelectTrigger className="w-full md:w-48">
<SelectValue placeholder="상태" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"> </SelectItem>
<SelectItem value="활성"></SelectItem>
<SelectItem value="비활성"></SelectItem>
</SelectContent>
</Select>
</div>
</CardContent>
</Card>
{/* 제품 목록 */}
<Card>
<CardContent className="pt-6">
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-32"></TableHead>
<TableHead></TableHead>
<TableHead className="w-24"></TableHead>
<TableHead></TableHead>
<TableHead className="w-24 text-center"></TableHead>
<TableHead className="w-32 text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredProducts.length === 0 ? (
<TableRow>
<TableCell colSpan={6} className="text-center py-8 text-muted-foreground">
.
</TableCell>
</TableRow>
) : (
filteredProducts.map((product) => (
<TableRow key={product.id}>
<TableCell className="font-mono text-sm">{product.code}</TableCell>
<TableCell className="font-medium">{product.name}</TableCell>
<TableCell className="text-sm">{product.lotNumber}</TableCell>
<TableCell className="text-sm text-muted-foreground">{product.description}</TableCell>
<TableCell className="text-center">
<Badge
className={
product.status === "활성"
? "bg-green-500 text-white hover:bg-green-600"
: "bg-gray-500 text-white hover:bg-gray-600"
}
>
{product.status}
</Badge>
</TableCell>
<TableCell className="text-right">
<div className="flex justify-end gap-2">
<Button
variant="outline"
size="sm"
onClick={() => handleEdit(product)}
>
</Button>
<Button
variant="outline"
size="sm"
className="text-red-600 hover:text-red-700"
onClick={() => handleDelete(product)}
>
</Button>
</div>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
{/* 신규 제품 등록 다이얼로그 */}
<Dialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}>
<DialogContent className="max-w-5xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>(BOM) </DialogTitle>
<DialogDescription>
BOM( ) .
</DialogDescription>
</DialogHeader>
<div className="grid grid-cols-12 gap-6">
{/* 왼쪽 사이드바 */}
<div className="col-span-2 space-y-2">
<div className="text-sm font-medium mb-2"></div>
<Button
variant={bomCategory === "BOM관리" ? "default" : "ghost"}
className="w-full justify-start"
onClick={() => setBomCategory("BOM관리")}
>
BOM관리
</Button>
<Button
variant={bomCategory === "품목" ? "default" : "ghost"}
className="w-full justify-start"
onClick={() => setBomCategory("품목")}
>
</Button>
<Button
variant={bomCategory === "불량" ? "default" : "ghost"}
className="w-full justify-start"
onClick={() => setBomCategory("불량")}
>
</Button>
</div>
{/* 메인 콘텐츠 */}
<div className="col-span-10 space-y-6">
{bomCategory === "BOM관리" && (
<>
{/* 기본 정보 */}
<div className="grid grid-cols-4 gap-4">
<div className="space-y-2">
<Label></Label>
<Input
value={newProduct.code}
onChange={(e) => setNewProduct({...newProduct, code: e.target.value})}
placeholder="PR-A-SS001"
/>
</div>
<div className="space-y-2">
<Label> </Label>
<Input
value={newProduct.name}
onChange={(e) => setNewProduct({...newProduct, name: e.target.value})}
placeholder="강화판"
/>
</div>
<div className="space-y-2">
<Label></Label>
<Select>
<SelectTrigger>
<SelectValue placeholder="제품" />
</SelectTrigger>
<SelectContent>
<SelectItem value="product"></SelectItem>
<SelectItem value="part"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label></Label>
<Input placeholder="ea" />
</div>
</div>
<div className="grid grid-cols-3 gap-4">
<div className="space-y-2">
<Label>1</Label>
<Select>
<SelectTrigger>
<SelectValue placeholder="1차카테고리" />
</SelectTrigger>
<SelectContent>
<SelectItem value="metal"></SelectItem>
<SelectItem value="plastic"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>2</Label>
<Select>
<SelectTrigger>
<SelectValue placeholder="2차카테고리" />
</SelectTrigger>
<SelectContent>
<SelectItem value="steel"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>3</Label>
<Select>
<SelectTrigger>
<SelectValue placeholder="3차카테고리" />
</SelectTrigger>
<SelectContent>
<SelectItem value="sheet"></SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label> ()</Label>
<Input
value={newProduct.lotNumber}
onChange={(e) => setNewProduct({...newProduct, lotNumber: e.target.value})}
placeholder="EGI-1.1ST-1219*3000"
/>
</div>
<div className="space-y-2">
<Label></Label>
<Input
value={newProduct.description}
onChange={(e) => setNewProduct({...newProduct, description: e.target.value})}
placeholder="철 국 판"
/>
</div>
</div>
<Separator />
{/* 상태 플래그 */}
<div className="space-y-2">
<Label> </Label>
<div className="flex gap-6">
<div className="flex items-center space-x-2">
<Checkbox id="sellable" />
<Label htmlFor="sellable" className="cursor-pointer"> </Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox id="purchasable" />
<Label htmlFor="purchasable" className="cursor-pointer"> </Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox id="producible" defaultChecked />
<Label htmlFor="producible" className="cursor-pointer"> </Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox id="active" defaultChecked />
<Label htmlFor="active" className="cursor-pointer"></Label>
</div>
</div>
</div>
<Separator />
{/* 규격 정보 */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label> </Label>
<Button size="sm" variant="outline">
<Plus className="h-4 w-4 mr-2" />
</Button>
</div>
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-12">#</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="w-20 text-center"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell className="text-center">1</TableCell>
<TableCell></TableCell>
<TableCell>1.5</TableCell>
<TableCell>T</TableCell>
<TableCell className="text-center">
<Checkbox defaultChecked />
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
</div>
<Separator />
{/* BOM 품목 목록 */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label> ID: 3</Label>
<div className="flex gap-2">
<Button
size="sm"
variant="outline"
onClick={() => setIsItemSelectOpen(true)}
>
<Plus className="h-4 w-4 mr-2" />
</Button>
<Button size="sm" variant="outline">
</Button>
<Button size="sm" variant="outline" className="text-red-600">
</Button>
</div>
</div>
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-12">#</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="w-24"></TableHead>
<TableHead className="w-24"></TableHead>
<TableHead className="w-24"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{bomItems.length === 0 ? (
<TableRow>
<TableCell colSpan={8} className="text-center py-8 text-muted-foreground">
BOM .
</TableCell>
</TableRow>
) : (
bomItems.map((item, index) => (
<TableRow key={item.id}>
<TableCell className="text-center">{index + 1}</TableCell>
<TableCell></TableCell>
<TableCell className="font-mono text-sm">{item.itemCode}</TableCell>
<TableCell>{item.itemName}</TableCell>
<TableCell className="text-sm text-muted-foreground">{item.specification}</TableCell>
<TableCell>{item.unit}</TableCell>
<TableCell>
<Input
type="number"
value={item.quantity}
onChange={(e) => {
const updated = [...bomItems];
updated[index].quantity = parseInt(e.target.value) || 0;
setBomItems(updated);
}}
className="w-20"
/>
</TableCell>
<TableCell>
<Button
variant="outline"
size="sm"
className="text-red-600"
onClick={() => {
setBomItems(bomItems.filter((_, i) => i !== index));
}}
>
</Button>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
</div>
</>
)}
{bomCategory === "품목" && (
<div className="text-center py-12 text-muted-foreground">
</div>
)}
{bomCategory === "불량" && (
<div className="text-center py-12 text-muted-foreground">
</div>
)}
</div>
</div>
<div className="flex justify-end gap-2 pt-4 border-t">
<Button variant="outline" onClick={() => setIsAddDialogOpen(false)}>
</Button>
<Button className="bg-blue-600 hover:bg-blue-700" onClick={handleAddProduct}>
</Button>
</div>
</DialogContent>
</Dialog>
{/* 수정 다이얼로그 (신규와 동일한 구조) */}
<Dialog open={isEditDialogOpen} onOpenChange={setIsEditDialogOpen}>
<DialogContent className="max-w-5xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>(BOM) </DialogTitle>
<DialogDescription>
BOM( ) .
</DialogDescription>
</DialogHeader>
{selectedProduct && (
<div className="grid grid-cols-12 gap-6">
{/* 왼쪽 사이드바 */}
<div className="col-span-2 space-y-2">
<div className="text-sm font-medium mb-2"></div>
<Button
variant={bomCategory === "BOM관리" ? "default" : "ghost"}
className="w-full justify-start"
onClick={() => setBomCategory("BOM관리")}
>
BOM관리
</Button>
<Button
variant={bomCategory === "품목" ? "default" : "ghost"}
className="w-full justify-start"
onClick={() => setBomCategory("품목")}
>
</Button>
<Button
variant={bomCategory === "불량" ? "default" : "ghost"}
className="w-full justify-start"
onClick={() => setBomCategory("불량")}
>
</Button>
</div>
{/* 메인 콘텐츠 */}
<div className="col-span-10 space-y-6">
{bomCategory === "BOM관리" && (
<>
<div className="grid grid-cols-4 gap-4">
<div className="space-y-2">
<Label></Label>
<Input
value={selectedProduct.code}
onChange={(e) => setSelectedProduct({...selectedProduct, code: e.target.value})}
/>
</div>
<div className="space-y-2">
<Label> </Label>
<Input
value={selectedProduct.name}
onChange={(e) => setSelectedProduct({...selectedProduct, name: e.target.value})}
/>
</div>
<div className="space-y-2">
<Label></Label>
<Select defaultValue="product">
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="product"></SelectItem>
<SelectItem value="part"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label></Label>
<Input defaultValue="ea" />
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label> ()</Label>
<Input
value={selectedProduct.lotNumber}
onChange={(e) => setSelectedProduct({...selectedProduct, lotNumber: e.target.value})}
/>
</div>
<div className="space-y-2">
<Label></Label>
<Input
value={selectedProduct.description}
onChange={(e) => setSelectedProduct({...selectedProduct, description: e.target.value})}
/>
</div>
</div>
<Separator />
{/* BOM 품목 목록 */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label> ID: 3</Label>
<div className="flex gap-2">
<Button
size="sm"
variant="outline"
onClick={() => setIsItemSelectOpen(true)}
>
<Plus className="h-4 w-4 mr-2" />
</Button>
</div>
</div>
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-12">#</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="w-24"></TableHead>
<TableHead className="w-24"></TableHead>
<TableHead className="w-24"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{bomItems.length === 0 ? (
<TableRow>
<TableCell colSpan={8} className="text-center py-8 text-muted-foreground">
BOM .
</TableCell>
</TableRow>
) : (
bomItems.map((item, index) => (
<TableRow key={item.id}>
<TableCell className="text-center">{index + 1}</TableCell>
<TableCell></TableCell>
<TableCell className="font-mono text-sm">{item.itemCode}</TableCell>
<TableCell>{item.itemName}</TableCell>
<TableCell className="text-sm text-muted-foreground">{item.specification}</TableCell>
<TableCell>{item.unit}</TableCell>
<TableCell>
<Input
type="number"
value={item.quantity}
onChange={(e) => {
const updated = [...bomItems];
updated[index].quantity = parseInt(e.target.value) || 0;
setBomItems(updated);
}}
className="w-20"
/>
</TableCell>
<TableCell>
<Button
variant="outline"
size="sm"
className="text-red-600"
onClick={() => {
setBomItems(bomItems.filter((_, i) => i !== index));
}}
>
</Button>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
</div>
</>
)}
</div>
</div>
)}
<div className="flex justify-end gap-2 pt-4 border-t">
<Button variant="outline" onClick={() => setIsEditDialogOpen(false)}>
</Button>
<Button className="bg-blue-600 hover:bg-blue-700" onClick={handleUpdateProduct}>
</Button>
</div>
</DialogContent>
</Dialog>
{/* 부품 선택 다이얼로그 */}
<Dialog open={isItemSelectOpen} onOpenChange={setIsItemSelectOpen}>
<DialogContent className="max-w-3xl">
<DialogHeader>
<DialogTitle> </DialogTitle>
<DialogDescription>
BOM에 .
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="flex gap-2">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="부품 검색..."
value={itemSearchTerm}
onChange={(e) => setItemSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
<Button variant="outline">
<FileText className="h-4 w-4 mr-2" />
</Button>
</div>
<div className="rounded-md border max-h-96 overflow-y-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-12"></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredItems.map((item) => (
<TableRow key={item.id}>
<TableCell>
<Checkbox id={`item-${item.id}`} />
</TableCell>
<TableCell>{item.type}</TableCell>
<TableCell className="font-mono text-sm">{item.code}</TableCell>
<TableCell>{item.name}</TableCell>
<TableCell className="text-sm text-muted-foreground">{item.specification}</TableCell>
<TableCell>{item.unit}</TableCell>
<TableCell>{item.defaultQty}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
<div className="text-sm text-muted-foreground">
{filteredItems.length}
</div>
</div>
<div className="flex justify-end gap-2 pt-4 border-t">
<Button variant="outline" onClick={() => setIsItemSelectOpen(false)}>
</Button>
<Button
className="bg-blue-600 hover:bg-blue-700"
onClick={() => {
// 선택된 항목들의 ID를 수집 (실제로는 체크박스 상태를 추적해야 함)
const selectedIds = filteredItems.slice(0, 2).map(i => i.id);
handleAddBomItems(selectedIds);
}}
>
</Button>
</div>
</DialogContent>
</Dialog>
{/* 삭제 확인 다이얼로그 */}
<AlertDialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle> </AlertDialogTitle>
<AlertDialogDescription>
? .
{selectedProduct && (
<div className="mt-4 p-3 bg-muted rounded-md">
<p className="font-medium">{selectedProduct.name}</p>
<p className="text-sm text-muted-foreground">: {selectedProduct.code}</p>
</div>
)}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogAction
className="bg-red-600 hover:bg-red-700"
onClick={confirmDelete}
>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,266 +0,0 @@
import { useMemo } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { useCurrentTime } from "@/hooks/useCurrentTime";
import {
Factory,
Package,
CheckCircle,
Users,
Truck,
AlertTriangle,
BarChart3,
Settings
} from "lucide-react";
export function ProductionManagerDashboard() {
const currentTime = useCurrentTime();
const productionManagerData = useMemo(() => {
return {
production: {
planned: 1500,
actual: 1320,
efficiency: 88
},
quality: {
passed: 1280,
failed: 40,
defectRate: 3.0
},
materials: {
consumed: 2400,
remaining: 8600,
critical: 3
},
equipment: {
operating: 18,
maintenance: 2,
downtime: 4.5
},
delivery: {
onTime: 45,
delayed: 3,
shipped: 48
},
workers: {
shift1: 42,
shift2: 38,
shift3: 25
}
};
}, []);
return (
<div className="p-4 md:p-6 space-y-4 md:space-y-6">
{/* 생산관리자 헤더 */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<h1 className="text-2xl md:text-3xl font-bold text-foreground"> </h1>
<p className="text-muted-foreground mt-1"> · {currentTime}</p>
</div>
<div className="flex space-x-2">
<Button variant="outline" size="sm">
<Factory className="h-4 w-4 mr-2" />
</Button>
<Button className="bg-blue-600 hover:bg-blue-700" size="sm">
<BarChart3 className="h-4 w-4 mr-2" />
</Button>
</div>
</div>
{/* 생산 현황 KPI */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 md:gap-6">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"> </CardTitle>
<Factory className="h-4 w-4 text-blue-600" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-blue-600">
{productionManagerData.production.efficiency}%
</div>
<p className="text-xs text-muted-foreground">
: {productionManagerData.production.actual} / : {productionManagerData.production.planned}
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"> </CardTitle>
<CheckCircle className="h-4 w-4 text-green-600" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-green-600">
{Math.round((productionManagerData.quality.passed / (productionManagerData.quality.passed + productionManagerData.quality.failed)) * 100)}%
</div>
<p className="text-xs text-muted-foreground">
: {productionManagerData.quality.defectRate}%
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"> </CardTitle>
<Settings className="h-4 w-4 text-purple-600" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-purple-600">
{Math.round((productionManagerData.equipment.operating / (productionManagerData.equipment.operating + productionManagerData.equipment.maintenance)) * 100)}%
</div>
<p className="text-xs text-muted-foreground">
: {productionManagerData.equipment.downtime}
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"> </CardTitle>
<Truck className="h-4 w-4 text-orange-600" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-orange-600">
{Math.round((productionManagerData.delivery.onTime / productionManagerData.delivery.shipped) * 100)}%
</div>
<p className="text-xs text-muted-foreground">
: {productionManagerData.delivery.delayed}
</p>
</CardContent>
</Card>
</div>
{/* 현장 운영 현황 */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* 자재 사용량 */}
<Card>
<CardHeader>
<CardTitle className="flex items-center space-x-2">
<Package className="h-5 w-5" />
<span> </span>
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="flex justify-between items-center">
<span className="text-sm"> </span>
<span className="font-bold">{productionManagerData.materials.consumed.toLocaleString()}kg</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm"> </span>
<span className="font-bold">{productionManagerData.materials.remaining.toLocaleString()}kg</span>
</div>
<div className="pt-2 border-t">
<div className="flex justify-between items-center">
<span className="text-sm text-red-600"> </span>
<Badge className="bg-red-500 text-white">{productionManagerData.materials.critical}</Badge>
</div>
</div>
</div>
</CardContent>
</Card>
{/* 교대별 인력 현황 */}
<Card>
<CardHeader>
<CardTitle className="flex items-center space-x-2">
<Users className="h-5 w-5" />
<span> </span>
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-3">
<div className="flex justify-between items-center p-2 bg-blue-50 rounded">
<span className="text-sm font-medium">1 (08:00-16:00)</span>
<span className="font-bold">{productionManagerData.workers.shift1}</span>
</div>
<div className="flex justify-between items-center p-2 bg-muted/50 dark:bg-muted/20 rounded">
<span className="text-sm font-medium">2 (16:00-24:00)</span>
<span className="font-bold">{productionManagerData.workers.shift2}</span>
</div>
<div className="flex justify-between items-center p-2 bg-purple-50 rounded">
<span className="text-sm font-medium">3 (24:00-08:00)</span>
<span className="font-bold">{productionManagerData.workers.shift3}</span>
</div>
</div>
</CardContent>
</Card>
</div>
{/* 설비 및 차량 현황 */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<Card>
<CardHeader>
<CardTitle className="flex items-center space-x-2">
<Settings className="h-5 w-5" />
<span> </span>
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2">
<div className="flex justify-between">
<span></span>
<Badge className="bg-green-500 text-white">{productionManagerData.equipment.operating}</Badge>
</div>
<div className="flex justify-between">
<span></span>
<Badge className="bg-orange-500 text-white">{productionManagerData.equipment.maintenance}</Badge>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="flex items-center space-x-2">
<Truck className="h-5 w-5" />
<span> </span>
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2">
<div className="flex justify-between">
<span></span>
<Badge className="bg-blue-500 text-white">3</Badge>
</div>
<div className="flex justify-between">
<span></span>
<Badge className="bg-muted text-muted-foreground">2</Badge>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="flex items-center space-x-2">
<AlertTriangle className="h-5 w-5" />
<span> </span>
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2">
<div className="flex items-center space-x-2 text-sm">
<div className="w-2 h-2 bg-red-500 rounded-full"></div>
<span> 3</span>
</div>
<div className="flex items-center space-x-2 text-sm">
<div className="w-2 h-2 bg-yellow-500 rounded-full"></div>
<span> 2</span>
</div>
<div className="flex items-center space-x-2 text-sm">
<div className="w-2 h-2 bg-orange-500 rounded-full"></div>
<span> 5</span>
</div>
</div>
</CardContent>
</Card>
</div>
</div>
);
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,370 +0,0 @@
import { useState } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
FileText,
Plus,
Trash2,
Calculator,
Download,
Send,
TrendingUp,
Package,
DollarSign,
Percent,
Save
} from "lucide-react";
interface QuoteItem {
id: string;
productCode: string;
productName: string;
quantity: number;
unitPrice: number;
discount: number;
total: number;
}
export function QuoteSimulation() {
const [customerName, setCustomerName] = useState("");
const [customerType, setCustomerType] = useState("new");
const [quoteItems, setQuoteItems] = useState<QuoteItem[]>([]);
const [selectedProduct, setSelectedProduct] = useState("");
const [quantity, setQuantity] = useState(1);
const [discount, setDiscount] = useState(0);
const products = [
{ code: "PRO-2024-001", name: "스마트 센서 모듈 A", price: 150000 },
{ code: "PRO-2024-002", name: "프리미엄 컨트롤러", price: 350000 },
{ code: "PRO-2024-008", name: "산업용 모터 드라이버", price: 280000 }
];
const addQuoteItem = () => {
if (!selectedProduct) return;
const product = products.find(p => p.code === selectedProduct);
if (!product) return;
const unitPrice = product.price;
const discountAmount = unitPrice * (discount / 100);
const finalUnitPrice = unitPrice - discountAmount;
const total = finalUnitPrice * quantity;
const newItem: QuoteItem = {
id: Date.now().toString(),
productCode: product.code,
productName: product.name,
quantity: quantity,
unitPrice: unitPrice,
discount: discount,
total: total
};
setQuoteItems([...quoteItems, newItem]);
setSelectedProduct("");
setQuantity(1);
setDiscount(0);
};
const removeQuoteItem = (id: string) => {
setQuoteItems(quoteItems.filter(item => item.id !== id));
};
const subtotal = quoteItems.reduce((sum, item) => sum + item.total, 0);
const vat = subtotal * 0.1;
const total = subtotal + vat;
const avgDiscountRate = quoteItems.length > 0
? quoteItems.reduce((sum, item) => sum + item.discount, 0) / quoteItems.length
: 0;
return (
<div className="p-6 space-y-6">
{/* 헤더 */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<h1 className="text-3xl font-bold text-foreground flex items-center gap-3">
<div className="w-10 h-10 bg-indigo-100 rounded-lg flex items-center justify-center">
<FileText className="h-6 w-6 text-indigo-600" />
</div>
</h1>
<p className="text-muted-foreground mt-1"> </p>
</div>
<div className="flex gap-2">
<Button variant="outline">
<Save className="h-4 w-4 mr-2" />
</Button>
<Button variant="outline">
<Download className="h-4 w-4 mr-2" />
PDF
</Button>
<Button className="bg-indigo-600 hover:bg-indigo-700">
<Send className="h-4 w-4 mr-2" />
</Button>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* 왼쪽: 견적 작성 폼 */}
<div className="lg:col-span-2 space-y-6">
{/* 고객 정보 */}
<Card>
<CardHeader>
<CardTitle className="text-lg"> </CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label> *</Label>
<Input
placeholder="고객사명을 입력하세요"
value={customerName}
onChange={(e) => setCustomerName(e.target.value)}
/>
</div>
<div className="space-y-2">
<Label> </Label>
<Select value={customerType} onValueChange={setCustomerType}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="new"> </SelectItem>
<SelectItem value="regular"> ()</SelectItem>
<SelectItem value="vip"> (VIP)</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label></Label>
<Input placeholder="담당자명" />
</div>
<div className="space-y-2">
<Label></Label>
<Input placeholder="연락처" />
</div>
</div>
</CardContent>
</Card>
{/* 품목 추가 */}
<Card>
<CardHeader>
<CardTitle className="text-lg"> </CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-12 gap-4">
<div className="col-span-5 space-y-2">
<Label> </Label>
<Select value={selectedProduct} onValueChange={setSelectedProduct}>
<SelectTrigger>
<SelectValue placeholder="제품을 선택하세요" />
</SelectTrigger>
<SelectContent>
{products.map((product) => (
<SelectItem key={product.code} value={product.code}>
{product.name} ({product.price.toLocaleString()})
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="col-span-2 space-y-2">
<Label></Label>
<Input
type="number"
min="1"
value={quantity}
onChange={(e) => setQuantity(parseInt(e.target.value) || 1)}
/>
</div>
<div className="col-span-2 space-y-2">
<Label> (%)</Label>
<Input
type="number"
min="0"
max="100"
value={discount}
onChange={(e) => setDiscount(parseInt(e.target.value) || 0)}
/>
</div>
<div className="col-span-3 flex items-end">
<Button
className="w-full bg-indigo-600 hover:bg-indigo-700"
onClick={addQuoteItem}
disabled={!selectedProduct}
>
<Plus className="h-4 w-4 mr-2" />
</Button>
</div>
</div>
</CardContent>
</Card>
{/* 견적 품목 목록 */}
<Card>
<CardHeader>
<CardTitle className="text-lg"> ({quoteItems.length})</CardTitle>
</CardHeader>
<CardContent>
{quoteItems.length === 0 ? (
<div className="text-center py-12 text-muted-foreground">
<Package className="h-12 w-12 mx-auto mb-3 opacity-50" />
<p> </p>
</div>
) : (
<div className="space-y-3">
{quoteItems.map((item) => (
<div key={item.id} className="p-4 bg-muted/30 rounded-lg border">
<div className="flex items-start justify-between mb-3">
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<span className="font-medium">{item.productName}</span>
<Badge variant="outline" className="text-xs">
{item.productCode}
</Badge>
</div>
<div className="text-sm text-muted-foreground">
: {item.unitPrice.toLocaleString()} |
: {item.quantity} |
: {item.discount}%
</div>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => removeQuoteItem(item.id)}
>
<Trash2 className="h-4 w-4 text-red-500" />
</Button>
</div>
<div className="flex justify-between items-center pt-3 border-t">
<span className="text-sm text-muted-foreground"></span>
<span className="text-lg font-bold text-indigo-600">
{item.total.toLocaleString()}
</span>
</div>
</div>
))}
</div>
)}
</CardContent>
</Card>
</div>
{/* 오른쪽: 견적 요약 */}
<div className="space-y-6">
{/* 금액 요약 */}
<Card className="border-2 border-indigo-200">
<CardHeader className="bg-gradient-to-r from-indigo-50 to-purple-50">
<CardTitle className="text-lg flex items-center gap-2">
<Calculator className="h-5 w-5 text-indigo-600" />
</CardTitle>
</CardHeader>
<CardContent className="space-y-4 pt-6">
<div className="space-y-3">
<div className="flex justify-between items-center pb-3 border-b">
<span className="text-muted-foreground"> </span>
<span className="font-medium">{quoteItems.length}</span>
</div>
<div className="flex justify-between items-center pb-3 border-b">
<span className="text-muted-foreground"> </span>
<span className="font-medium">
{quoteItems.reduce((sum, item) => sum + item.quantity, 0)}
</span>
</div>
<div className="flex justify-between items-center pb-3 border-b">
<span className="text-muted-foreground"> </span>
<span className="font-medium text-orange-600">
{avgDiscountRate.toFixed(1)}%
</span>
</div>
<div className="flex justify-between items-center pb-3 border-b">
<span className="text-muted-foreground"></span>
<span className="font-medium">{subtotal.toLocaleString()}</span>
</div>
<div className="flex justify-between items-center pb-3 border-b">
<span className="text-muted-foreground"> (10%)</span>
<span className="font-medium">{vat.toLocaleString()}</span>
</div>
<div className="flex justify-between items-center pt-2">
<span className="text-lg font-bold"> </span>
<span className="text-2xl font-bold text-indigo-600">
{total.toLocaleString()}
</span>
</div>
</div>
</CardContent>
</Card>
{/* 통계 카드 */}
<Card>
<CardHeader>
<CardTitle className="text-sm"> </CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div className="flex items-center justify-between p-3 bg-blue-50 rounded-lg">
<div className="flex items-center gap-2">
<DollarSign className="h-4 w-4 text-blue-600" />
<span className="text-sm"> </span>
</div>
<span className="font-bold text-blue-600">
{quoteItems.length > 0 ? Math.round(subtotal / quoteItems.reduce((sum, item) => sum + item.quantity, 0) || 0).toLocaleString() : 0}
</span>
</div>
<div className="flex items-center justify-between p-3 bg-purple-50 rounded-lg">
<div className="flex items-center gap-2">
<Percent className="h-4 w-4 text-purple-600" />
<span className="text-sm"> </span>
</div>
<span className="font-bold text-purple-600">
{Math.round(quoteItems.reduce((sum, item) => sum + (item.unitPrice * item.quantity * item.discount / 100), 0)).toLocaleString()}
</span>
</div>
<div className="flex items-center justify-between p-3 bg-green-50 rounded-lg">
<div className="flex items-center gap-2">
<TrendingUp className="h-4 w-4 text-green-600" />
<span className="text-sm"> </span>
</div>
<span className="font-bold text-green-600">
35.2%
</span>
</div>
</CardContent>
</Card>
{/* 빠른 템플릿 */}
<Card>
<CardHeader>
<CardTitle className="text-sm"> 릿</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
<Button variant="outline" className="w-full justify-start" size="sm">
<FileText className="h-4 w-4 mr-2" />
(A)
</Button>
<Button variant="outline" className="w-full justify-start" size="sm">
<FileText className="h-4 w-4 mr-2" />
(B)
</Button>
<Button variant="outline" className="w-full justify-start" size="sm">
<FileText className="h-4 w-4 mr-2" />
VIP (C)
</Button>
</CardContent>
</Card>
</div>
</div>
</div>
);
}

View File

@@ -1,350 +0,0 @@
import React, { useState } from "react";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Badge } from "@/components/ui/badge";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import { Switch } from "@/components/ui/switch";
import { Plus, X, BarChart, ArrowLeft } from "lucide-react";
// 입고등록 페이지
interface ReceivingWritePageProps {
onBack: () => void;
}
export function ReceivingWritePage({ onBack }: ReceivingWritePageProps) {
const [selectedItem, setSelectedItem] = useState("EGIT-SST");
const [spec, setSpec] = useState("1219*3500");
const [receivingDate, setReceivingDate] = useState("2025-09-03");
const [receivingUnit, setReceivingUnit] = useState("예");
const [receivingQty, setReceivingQty] = useState("");
const [inspectionEnabled, setInspectionEnabled] = useState(false);
const [lotNumber, setLotNumber] = useState("250723-03");
const [detailNumber, setDetailNumber] = useState("M-250723-0001");
const [activeTab, setActiveTab] = useState<"origin" | "invoice">("origin");
const [supplier, setSupplier] = useState("");
const [manager, setManager] = useState("");
const [manufacturer, setManufacturer] = useState("");
// 추가정보 동적 필드
const [additionalFields, setAdditionalFields] = useState([
{ id: 1, name: "출하(예: 제조, 중량, 길이 등)", value: "0" }
]);
// 필드 추가
const addField = () => {
const newId = additionalFields.length > 0 ? Math.max(...additionalFields.map(f => f.id)) + 1 : 1;
setAdditionalFields([...additionalFields, { id: newId, name: "", value: "" }]);
};
// 필드 삭제
const removeField = (id: number) => {
setAdditionalFields(additionalFields.filter(f => f.id !== id));
};
// 필드 업데이트
const updateField = (id: number, key: "name" | "value", newValue: string) => {
setAdditionalFields(additionalFields.map(f =>
f.id === id ? { ...f, [key]: newValue } : f
));
};
return (
<div className="space-y-6">
{/* 헤더 */}
<div className="flex items-center gap-4">
<Button variant="ghost" size="icon" onClick={onBack}>
<ArrowLeft className="h-5 w-5" />
</Button>
<h1 className="text-3xl font-bold"> </h1>
</div>
<Card>
<CardContent className="p-4 md:p-6 space-y-6">
{/* 1. 품목 */}
<div className="space-y-4">
<div className="flex items-center gap-2">
<div className="bg-red-500 text-white rounded-full w-6 h-6 flex items-center justify-center text-sm font-bold flex-shrink-0">
1
</div>
<Label className="text-lg font-semibold"></Label>
</div>
<Select value={selectedItem} onValueChange={setSelectedItem}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="EGIT-SST">EGIT-SST - EGIT-SST</SelectItem>
<SelectItem value="EGIT-SST2">EGIT-SST2 - EGIT-SST2</SelectItem>
<SelectItem value="KSS-01">KSS-01 - KSS-01</SelectItem>
</SelectContent>
</Select>
</div>
{/* 2. 기본 정보 */}
<div className="space-y-4">
<div className="flex items-center gap-2">
<div className="bg-red-500 text-white rounded-full w-6 h-6 flex items-center justify-center text-sm font-bold flex-shrink-0">
2
</div>
<Label className="text-lg font-semibold"> </Label>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* 규격 */}
<div className="space-y-2">
<Label></Label>
<Input value={spec} onChange={(e) => setSpec(e.target.value)} />
</div>
{/* 품목 */}
<div className="space-y-2">
<Label></Label>
<div className="flex items-center gap-2 flex-wrap">
<Badge variant="outline" className="text-sm">
E-1-SST
</Badge>
<Badge variant="outline" className="text-sm">
-1219
</Badge>
<Badge variant="outline" className="text-sm">
-3500
</Badge>
</div>
<p className="text-xs text-muted-foreground">
제품:정기마감등도 ID 3528, SECCO
</p>
</div>
{/* 입고일자 */}
<div className="space-y-2">
<Label></Label>
<Input
type="date"
value={receivingDate}
onChange={(e) => setReceivingDate(e.target.value)}
/>
</div>
{/* 입고단위 */}
<div className="space-y-2">
<Label></Label>
<Select value={receivingUnit} onValueChange={setReceivingUnit}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="예"></SelectItem>
<SelectItem value="kg">kg</SelectItem>
<SelectItem value="m">m</SelectItem>
<SelectItem value="EA">EA</SelectItem>
</SelectContent>
</Select>
</div>
</div>
{/* 입고량 */}
<div className="space-y-2">
<Label></Label>
<div className="flex items-center gap-2">
<Input
placeholder="기본 수량"
value={receivingQty}
onChange={(e) => setReceivingQty(e.target.value)}
className="flex-1"
type="number"
/>
<Button
variant="ghost"
size="icon"
onClick={() => setReceivingQty("")}
>
<X className="h-4 w-4" />
</Button>
</div>
</div>
</div>
{/* 3. 추가정보 */}
<div className="space-y-4">
<div className="flex items-center gap-2">
<div className="bg-red-500 text-white rounded-full w-6 h-6 flex items-center justify-center text-sm font-bold flex-shrink-0">
3
</div>
<Label className="text-lg font-semibold"></Label>
</div>
<div className="space-y-3">
{additionalFields.map((field) => (
<div key={field.id} className="flex flex-col md:flex-row items-start md:items-center gap-2">
<Input
placeholder="출하(예: 제조, 중량, 길이 등)"
value={field.name}
onChange={(e) => updateField(field.id, "name", e.target.value)}
className="flex-1"
/>
<Input
value={field.value}
onChange={(e) => updateField(field.id, "value", e.target.value)}
className="w-full md:w-32"
type="number"
/>
<Button
variant="ghost"
size="icon"
onClick={() => removeField(field.id)}
className="self-end md:self-auto"
>
<X className="h-4 w-4" />
</Button>
</div>
))}
<Button
variant="outline"
onClick={addField}
className="w-full"
>
<Plus className="h-4 w-4 mr-2" />
</Button>
<p className="text-xs text-muted-foreground">
*
</p>
</div>
{/* 납품업체, 담당자, 제조사 */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="space-y-2">
<Label></Label>
<Input
placeholder="거주자스톡"
value={supplier}
onChange={(e) => setSupplier(e.target.value)}
/>
</div>
<div className="space-y-2">
<Label></Label>
<Input
placeholder="입고 담당"
value={manager}
onChange={(e) => setManager(e.target.value)}
/>
</div>
<div className="space-y-2">
<Label></Label>
<Input
placeholder="KG스틸 등"
value={manufacturer}
onChange={(e) => setManufacturer(e.target.value)}
/>
</div>
</div>
</div>
{/* 4. 검사여부 저장 */}
<div className="flex items-center gap-4 p-4 bg-muted/50 rounded-lg">
<div className="bg-red-500 text-white rounded-full w-6 h-6 flex items-center justify-center text-sm font-bold flex-shrink-0">
4
</div>
<div className="flex items-center gap-3">
<Switch
checked={inspectionEnabled}
onCheckedChange={setInspectionEnabled}
/>
<Label className="cursor-pointer"> </Label>
</div>
</div>
{/* 5. 상세내역 */}
<div className="space-y-4">
<div className="flex items-center gap-2">
<div className="bg-red-500 text-white rounded-full w-6 h-6 flex items-center justify-center text-sm font-bold flex-shrink-0">
5
</div>
<Label className="text-lg font-semibold"> ( )</Label>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* 입고 로트번호 */}
<div className="space-y-2">
<Label> (YYMMDD-XX)</Label>
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground whitespace-nowrap">:</span>
<Input
value={lotNumber}
onChange={(e) => setLotNumber(e.target.value)}
className="flex-1"
placeholder="250723-03"
/>
</div>
</div>
{/* 상세내역 */}
<div className="space-y-2">
<Label> ( )</Label>
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground whitespace-nowrap">:</span>
<Input
value={detailNumber}
onChange={(e) => setDetailNumber(e.target.value)}
className="flex-1"
placeholder="M-250723-0001"
/>
<Button variant="outline" size="icon">
<BarChart className="h-4 w-4" />
</Button>
</div>
</div>
</div>
</div>
{/* 정보 (원산지명, 거래명세표) */}
<div className="space-y-4">
<Label className="text-lg font-semibold"> (, )</Label>
<Tabs value={activeTab} onValueChange={(v) => setActiveTab(v as "origin" | "invoice")}>
<TabsList className="grid w-full grid-cols-2 max-w-md">
<TabsTrigger value="origin"> </TabsTrigger>
<TabsTrigger value="invoice"> </TabsTrigger>
</TabsList>
<TabsContent value="origin" className="space-y-2 mt-4">
<Textarea
placeholder="원산지 정보를 입력하세요"
rows={4}
/>
</TabsContent>
<TabsContent value="invoice" className="space-y-2 mt-4">
<Textarea
placeholder="거래명세표 정보를 입력하세요"
rows={4}
/>
</TabsContent>
</Tabs>
</div>
{/* 6. 하단 버튼 */}
<div className="flex flex-col md:flex-row items-center justify-end gap-3 pt-4 border-t">
<div className="bg-red-500 text-white rounded-full w-6 h-6 flex items-center justify-center text-sm font-bold md:mr-2">
6
</div>
<div className="flex items-center gap-3 w-full md:w-auto">
<Button variant="outline" onClick={onBack} className="flex-1 md:flex-none">
</Button>
<Button className="bg-blue-600 hover:bg-blue-700 flex-1 md:flex-none">
</Button>
</div>
</div>
</CardContent>
</Card>
</div>
);
}

View File

@@ -1,510 +0,0 @@
import { useState } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { BarChart, Bar, LineChart, Line, PieChart, Pie, Cell, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Area, AreaChart } from "recharts";
import { Download, Calendar, Filter, TrendingUp, TrendingDown, Minus, FileText, BarChart3, PieChart as PieChartIcon, Activity } from "lucide-react";
export function Reports() {
const [selectedDateRange, setSelectedDateRange] = useState("month");
const [selectedReport, setSelectedReport] = useState("production");
// 생산 실적 데이터
const productionData = [
{ date: "2025-09-01", planned: 1200, actual: 1150, efficiency: 95.8 },
{ date: "2025-09-02", planned: 1300, actual: 1280, efficiency: 98.5 },
{ date: "2025-09-03", planned: 1100, actual: 1050, efficiency: 95.5 },
{ date: "2025-09-04", planned: 1400, actual: 1420, efficiency: 101.4 },
{ date: "2025-09-05", planned: 1250, actual: 1200, efficiency: 96.0 },
{ date: "2025-09-06", planned: 1350, actual: 1300, efficiency: 96.3 },
{ date: "2025-09-07", planned: 1200, actual: 1180, efficiency: 98.3 },
];
// 품질 데이터
const qualityData = [
{ product: "스마트폰 케이스", passRate: 98.5, defectRate: 1.5, totalInspected: 2500 },
{ product: "태블릿 스탠드", passRate: 97.2, defectRate: 2.8, totalInspected: 1800 },
{ product: "무선 충전기", passRate: 99.1, defectRate: 0.9, totalInspected: 3200 },
{ product: "이어폰 케이스", passRate: 96.8, defectRate: 3.2, totalInspected: 2100 },
];
// 자재 현황 데이터
const materialData = [
{ material: "플라스틱 원료", stock: 1250, minStock: 500, value: 3125000, turnover: 12.5 },
{ material: "알루미늄 판재", stock: 85, minStock: 100, value: 1275000, turnover: 8.2 },
{ material: "실리콘 패드", stock: 3200, minStock: 1000, value: 1600000, turnover: 15.8 },
{ material: "전자부품 모듈", stock: 75, minStock: 100, value: 1875000, turnover: 6.5 },
];
// 설비 가동률 데이터
const equipmentData = [
{ equipment: "CNC 머시닝센터 1호", uptime: 94.2, downtime: 5.8, productivity: 98.5 },
{ equipment: "사출성형기 A라인", uptime: 89.1, downtime: 10.9, productivity: 92.3 },
{ equipment: "자동포장기 1호", uptime: 96.8, downtime: 3.2, productivity: 99.1 },
{ equipment: "품질검사기 QC-01", uptime: 98.5, downtime: 1.5, productivity: 97.8 },
];
// 월별 매출 데이터
const salesData = [
{ month: "1월", sales: 85000000, cost: 62000000, profit: 23000000 },
{ month: "2월", sales: 92000000, cost: 68000000, profit: 24000000 },
{ month: "3월", sales: 78000000, cost: 59000000, profit: 19000000 },
{ month: "4월", sales: 105000000, cost: 75000000, profit: 30000000 },
{ month: "5월", sales: 98000000, cost: 72000000, profit: 26000000 },
{ month: "6월", sales: 112000000, cost: 79000000, profit: 33000000 },
{ month: "7월", sales: 108000000, cost: 77000000, profit: 31000000 },
{ month: "8월", sales: 95000000, cost: 70000000, profit: 25000000 },
{ month: "9월", sales: 118000000, cost: 82000000, profit: 36000000 },
];
const COLORS = ['#3b82f6', '#ef4444', '#10b981', '#f59e0b', '#8b5cf6'];
const formatCurrency = (value: number) => {
return `${(value / 1000000).toFixed(0)}M`;
};
const getStatusIcon = (current: number, previous: number) => {
if (current > previous) return <TrendingUp className="h-4 w-4 text-green-500" />;
if (current < previous) return <TrendingDown className="h-4 w-4 text-red-500" />;
return <Minus className="h-4 w-4 text-gray-500" />;
};
return (
<div className="p-4 md:p-6 space-y-4 md:space-y-6">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<h1 className="text-2xl md:text-3xl font-bold text-gray-900"> </h1>
<p className="text-gray-600 mt-1">, , , , </p>
</div>
<div className="flex flex-col md:flex-row gap-2">
<Select value={selectedDateRange} onValueChange={setSelectedDateRange}>
<SelectTrigger className="w-full md:w-40">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="week"> 1</SelectItem>
<SelectItem value="month"> 1</SelectItem>
<SelectItem value="quarter"> 3</SelectItem>
<SelectItem value="year"> 1</SelectItem>
</SelectContent>
</Select>
<Button variant="outline" className="w-full md:w-auto">
<Download className="h-4 w-4 mr-2" />
</Button>
</div>
</div>
{/* 핵심 지표 요약 */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"> </CardTitle>
<Activity className="h-4 w-4 text-blue-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">8,950</div>
<div className="flex items-center space-x-1 text-xs text-muted-foreground">
{getStatusIcon(8950, 8200)}
<span>+9.1% </span>
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"> </CardTitle>
<TrendingUp className="h-4 w-4 text-green-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">97.9%</div>
<div className="flex items-center space-x-1 text-xs text-muted-foreground">
{getStatusIcon(97.9, 96.8)}
<span>+1.1% </span>
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"> </CardTitle>
<BarChart3 className="h-4 w-4 text-purple-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">94.7%</div>
<div className="flex items-center space-x-1 text-xs text-muted-foreground">
{getStatusIcon(94.7, 92.3)}
<span>+2.4% </span>
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"> </CardTitle>
<TrendingUp className="h-4 w-4 text-orange-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">118M</div>
<div className="flex items-center space-x-1 text-xs text-muted-foreground">
{getStatusIcon(118, 95)}
<span>+24.2% </span>
</div>
</CardContent>
</Card>
</div>
<Tabs defaultValue="production" className="space-y-4">
<div className="overflow-x-auto">
<TabsList className="grid w-full grid-cols-5 min-w-[500px]">
<TabsTrigger value="production" className="flex items-center space-x-2">
<Activity className="h-4 w-4" />
<span className="hidden sm:inline"></span>
<span className="sm:hidden"></span>
</TabsTrigger>
<TabsTrigger value="quality" className="flex items-center space-x-2">
<TrendingUp className="h-4 w-4" />
<span className="hidden sm:inline"></span>
<span className="sm:hidden"></span>
</TabsTrigger>
<TabsTrigger value="material" className="flex items-center space-x-2">
<BarChart3 className="h-4 w-4" />
<span className="hidden sm:inline"></span>
<span className="sm:hidden"></span>
</TabsTrigger>
<TabsTrigger value="equipment" className="flex items-center space-x-2">
<PieChartIcon className="h-4 w-4" />
<span className="hidden sm:inline"></span>
<span className="sm:hidden"></span>
</TabsTrigger>
<TabsTrigger value="sales" className="flex items-center space-x-2">
<FileText className="h-4 w-4" />
<span className="hidden sm:inline"></span>
<span className="sm:hidden"></span>
</TabsTrigger>
</TabsList>
</div>
<TabsContent value="production" className="space-y-4">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card>
<CardHeader>
<CardTitle> </CardTitle>
</CardHeader>
<CardContent>
<ResponsiveContainer width="100%" height={300}>
<BarChart data={productionData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="date" />
<YAxis />
<Tooltip />
<Bar dataKey="planned" fill="#94a3b8" name="계획" />
<Bar dataKey="actual" fill="#3b82f6" name="실적" />
</BarChart>
</ResponsiveContainer>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle> </CardTitle>
</CardHeader>
<CardContent>
<ResponsiveContainer width="100%" height={300}>
<LineChart data={productionData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="date" />
<YAxis domain={[90, 105]} />
<Tooltip />
<Line type="monotone" dataKey="efficiency" stroke="#10b981" strokeWidth={3} name="효율성 (%)" />
</LineChart>
</ResponsiveContainer>
</CardContent>
</Card>
</div>
<Card>
<CardHeader>
<CardTitle> </CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{qualityData.map((item, index) => (
<div key={index} className="p-4 border rounded-lg">
<h4 className="font-medium mb-2">{item.product}</h4>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span>:</span>
<span className="font-medium">{item.totalInspected.toLocaleString()}</span>
</div>
<div className="flex justify-between">
<span>:</span>
<span className="text-green-600 font-medium">{item.passRate}%</span>
</div>
<div className="flex justify-between">
<span>:</span>
<span className="text-red-600 font-medium">{item.defectRate}%</span>
</div>
</div>
</div>
))}
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="quality" className="space-y-4">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card>
<CardHeader>
<CardTitle> </CardTitle>
</CardHeader>
<CardContent>
<ResponsiveContainer width="100%" height={300}>
<BarChart data={qualityData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="product" />
<YAxis domain={[90, 100]} />
<Tooltip />
<Bar dataKey="passRate" fill="#10b981" name="합격률 (%)" />
</BarChart>
</ResponsiveContainer>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle> </CardTitle>
</CardHeader>
<CardContent>
<ResponsiveContainer width="100%" height={300}>
<PieChart>
<Pie
data={qualityData.map(item => ({ name: item.product, value: item.defectRate }))}
cx="50%"
cy="50%"
innerRadius={60}
outerRadius={100}
paddingAngle={5}
dataKey="value"
>
{qualityData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
))}
</Pie>
<Tooltip />
</PieChart>
</ResponsiveContainer>
<div className="mt-4 space-y-2">
{qualityData.map((item, index) => (
<div key={index} className="flex items-center justify-between">
<div className="flex items-center">
<div
className="w-3 h-3 rounded-full mr-2"
style={{ backgroundColor: COLORS[index % COLORS.length] }}
></div>
<span className="text-sm">{item.product}</span>
</div>
<span className="text-sm font-medium">{item.defectRate}%</span>
</div>
))}
</div>
</CardContent>
</Card>
</div>
</TabsContent>
<TabsContent value="material" className="space-y-4">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card>
<CardHeader>
<CardTitle> </CardTitle>
</CardHeader>
<CardContent>
<ResponsiveContainer width="100%" height={300}>
<BarChart data={materialData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="material" />
<YAxis />
<Tooltip />
<Bar dataKey="stock" fill="#3b82f6" name="현재고" />
<Bar dataKey="minStock" fill="#ef4444" name="최소재고" />
</BarChart>
</ResponsiveContainer>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle> </CardTitle>
</CardHeader>
<CardContent>
<ResponsiveContainer width="100%" height={300}>
<LineChart data={materialData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="material" />
<YAxis />
<Tooltip />
<Line type="monotone" dataKey="turnover" stroke="#10b981" strokeWidth={3} name="회전율" />
</LineChart>
</ResponsiveContainer>
</CardContent>
</Card>
</div>
<Card>
<CardHeader>
<CardTitle> </CardTitle>
</CardHeader>
<CardContent>
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b">
<th className="text-left p-2"></th>
<th className="text-right p-2"></th>
<th className="text-right p-2"></th>
<th className="text-right p-2"></th>
<th className="text-center p-2"></th>
</tr>
</thead>
<tbody>
{materialData.map((item, index) => (
<tr key={index} className="border-b">
<td className="p-2 font-medium">{item.material}</td>
<td className="text-right p-2">{item.stock.toLocaleString()}</td>
<td className="text-right p-2">{item.value.toLocaleString()}</td>
<td className="text-right p-2">{item.turnover}</td>
<td className="text-center p-2">
{item.stock < item.minStock ? (
<span className="text-red-600 text-sm"></span>
) : (
<span className="text-green-600 text-sm"></span>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="equipment" className="space-y-4">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card>
<CardHeader>
<CardTitle> </CardTitle>
</CardHeader>
<CardContent>
<ResponsiveContainer width="100%" height={300}>
<BarChart data={equipmentData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="equipment" />
<YAxis />
<Tooltip />
<Bar dataKey="uptime" fill="#10b981" name="가동률 (%)" />
</BarChart>
</ResponsiveContainer>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle> </CardTitle>
</CardHeader>
<CardContent>
<ResponsiveContainer width="100%" height={300}>
<LineChart data={equipmentData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="equipment" />
<YAxis domain={[85, 100]} />
<Tooltip />
<Line type="monotone" dataKey="productivity" stroke="#3b82f6" strokeWidth={3} name="생산성 (%)" />
</LineChart>
</ResponsiveContainer>
</CardContent>
</Card>
</div>
</TabsContent>
<TabsContent value="sales" className="space-y-4">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card>
<CardHeader>
<CardTitle> </CardTitle>
</CardHeader>
<CardContent>
<ResponsiveContainer width="100%" height={300}>
<AreaChart data={salesData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="month" />
<YAxis tickFormatter={formatCurrency} />
<Tooltip formatter={(value) => `${(value as number).toLocaleString()}`} />
<Area type="monotone" dataKey="sales" stackId="1" stroke="#3b82f6" fill="#3b82f6" name="매출" />
<Area type="monotone" dataKey="cost" stackId="1" stroke="#ef4444" fill="#ef4444" name="비용" />
</AreaChart>
</ResponsiveContainer>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle> </CardTitle>
</CardHeader>
<CardContent>
<ResponsiveContainer width="100%" height={300}>
<BarChart data={salesData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="month" />
<YAxis tickFormatter={formatCurrency} />
<Tooltip formatter={(value) => `${(value as number).toLocaleString()}`} />
<Bar dataKey="profit" fill="#10b981" name="순이익" />
</BarChart>
</ResponsiveContainer>
</CardContent>
</Card>
</div>
<Card>
<CardHeader>
<CardTitle> </CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="text-center p-4 border rounded-lg">
<div className="text-2xl font-bold text-blue-600">863M원</div>
<p className="text-sm text-gray-600"> </p>
<div className="flex items-center justify-center mt-2">
<TrendingUp className="h-4 w-4 text-green-500 mr-1" />
<span className="text-sm text-green-600">+15.2% YoY</span>
</div>
</div>
<div className="text-center p-4 border rounded-lg">
<div className="text-2xl font-bold text-green-600">237M원</div>
<p className="text-sm text-gray-600"> </p>
<div className="flex items-center justify-center mt-2">
<TrendingUp className="h-4 w-4 text-green-500 mr-1" />
<span className="text-sm text-green-600">+22.8% YoY</span>
</div>
</div>
<div className="text-center p-4 border rounded-lg">
<div className="text-2xl font-bold text-purple-600">27.5%</div>
<p className="text-sm text-gray-600"></p>
<div className="flex items-center justify-center mt-2">
<TrendingUp className="h-4 w-4 text-green-500 mr-1" />
<span className="text-sm text-green-600">+2.1%p</span>
</div>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
);
}

View File

@@ -1,663 +0,0 @@
import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog";
import {
ArrowLeft,
User,
Building2,
Mail,
Phone,
Briefcase,
MessageSquare,
Calendar,
ExternalLink,
Copy,
CheckCircle2,
Clock,
Send,
Sparkles
} from "lucide-react";
import { toast } from "sonner";
interface Lead {
id: string;
name: string;
company: string;
email: string;
phone: string;
industry: string;
message: string;
status: "pending" | "contacted" | "demo-sent";
submittedAt: string;
demoLink?: string;
demoExpiryDate?: string;
industryPreset?: string;
demoDuration?: number;
}
interface SalesLeadDashboardProps {
onStartDemo?: (config: any) => void;
}
interface DemoConfig {
demoId: string;
leadId: string;
industryPreset: string;
demoDuration: number;
expiryDate: string;
createdAt: string;
clientEmail: string;
clientName: string;
company: string;
}
export function SalesLeadDashboard({ onStartDemo }: SalesLeadDashboardProps) {
const navigate = useNavigate();
const [leads, setLeads] = useState<Lead[]>([]);
const [selectedLead, setSelectedLead] = useState<Lead | null>(null);
const [isCreateDemoOpen, setIsCreateDemoOpen] = useState(false);
// Demo 생성 폼 데이터
const [industryPreset, setIndustryPreset] = useState("");
const [demoDuration, setDemoDuration] = useState("7");
const [generatedLink, setGeneratedLink] = useState("");
const [isCopied, setIsCopied] = useState(false);
useEffect(() => {
loadLeads();
}, []);
const loadLeads = () => {
const storedLeads = JSON.parse(localStorage.getItem("salesLeads") || "[]");
setLeads(storedLeads);
};
const handleCreateDemo = (lead: Lead) => {
setSelectedLead(lead);
setIndustryPreset(lead.industry || "");
setIsCreateDemoOpen(true);
setGeneratedLink("");
setIsCopied(false);
};
const handleGenerateLink = () => {
if (!selectedLead) return;
const demoId = `demo_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const expiryDate = new Date();
expiryDate.setDate(expiryDate.getDate() + parseInt(demoDuration));
const demoLink = `${window.location.origin}/#/demo/${demoId}`;
// Demo 설정 저장
const demoConfig = {
demoId,
leadId: selectedLead.id,
industryPreset,
demoDuration: parseInt(demoDuration),
expiryDate: expiryDate.toISOString(),
createdAt: new Date().toISOString(),
clientEmail: selectedLead.email,
clientName: selectedLead.name,
company: selectedLead.company,
};
// localStorage에 demo 설정 저장
const existingDemos = JSON.parse(localStorage.getItem("demoConfigs") || "{}");
existingDemos[demoId] = demoConfig;
localStorage.setItem("demoConfigs", JSON.stringify(existingDemos));
// Lead 상태 업데이트
const updatedLeads = leads.map(l =>
l.id === selectedLead.id
? {
...l,
status: "demo-sent" as const,
demoLink,
demoExpiryDate: expiryDate.toISOString(),
industryPreset,
demoDuration: parseInt(demoDuration)
}
: l
);
setLeads(updatedLeads);
localStorage.setItem("salesLeads", JSON.stringify(updatedLeads));
setGeneratedLink(demoLink);
toast.success("데모 링크가 생성되었습니다!", {
description: `${selectedLead.email}로 이메일을 전송할 수 있습니다.`,
});
};
const handleCopyLink = () => {
if (!generatedLink) return;
// Try modern Clipboard API first
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(generatedLink).then(() => {
setIsCopied(true);
toast.success("링크가 클립보드에 복사되었습니다!");
setTimeout(() => setIsCopied(false), 2000);
}).catch(() => {
// Fallback to legacy method
fallbackCopyTextToClipboard(generatedLink);
});
} else {
// Use fallback for non-secure contexts
fallbackCopyTextToClipboard(generatedLink);
}
};
const fallbackCopyTextToClipboard = (text: string) => {
const textArea = document.createElement("textarea");
textArea.value = text;
textArea.style.position = "fixed";
textArea.style.left = "-999999px";
textArea.style.top = "-999999px";
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
const successful = document.execCommand('copy');
if (successful) {
setIsCopied(true);
toast.success("링크가 클립보드에 복사되었습니다!");
setTimeout(() => setIsCopied(false), 2000);
} else {
toast.error("복사에 실패했습니다. 링크를 수동으로 복사해주세요.");
}
} catch (err) {
toast.error("복사에 실패했습니다. 링크를 수동으로 복사해주세요.");
}
document.body.removeChild(textArea);
};
const handleSendEmail = () => {
if (!selectedLead || !generatedLink) return;
const industryLabel = getIndustryLabel(industryPreset);
const subject = encodeURIComponent(`[SAM] ${selectedLead.company} 맞춤형 데모 링크`);
const body = encodeURIComponent(
`안녕하세요 ${selectedLead.name}님,\n\n` +
`${selectedLead.company}${industryLabel} 산업 환경에 최적화된 SAM MES 데모를 준비했습니다.\n\n` +
`아래 링크를 통해 ${demoDuration}일간 무료로 체험하실 수 있습니다:\n` +
`${generatedLink}\n\n` +
`궁금하신 점이 있으시면 언제든 연락 주세요.\n\n` +
`감사합니다.\n` +
`SAM 영업팀 드림`
);
window.open(`mailto:${selectedLead.email}?subject=${subject}&body=${body}`);
toast.success("이메일 클라이언트가 열렸습니다!");
};
const handleOpenDemo = (lead: Lead) => {
// Load demo config and start demo directly
const demoConfigs = JSON.parse(localStorage.getItem("demoConfigs") || "{}");
const demoId = lead.demoLink?.split("/demo/")[1];
if (demoId && demoConfigs[demoId]) {
const config = demoConfigs[demoId];
// Check if expired
const expiryDate = new Date(config.expiryDate);
const now = new Date();
if (now < expiryDate) {
console.log("Opening demo with config:", config);
if (onStartDemo) {
onStartDemo(config);
}
} else {
toast.error("이 데모는 만료되었습니다.");
}
} else {
console.error("Demo config not found. Demo ID:", demoId);
console.error("Available configs:", demoConfigs);
toast.error("데모 설정을 찾을 수 없습니다.");
}
};
const getStatusBadge = (status: string) => {
switch (status) {
case "pending":
return <Badge variant="outline" className="bg-yellow-50 text-yellow-700 border-yellow-300"><Clock className="w-3 h-3 mr-1" /></Badge>;
case "contacted":
return <Badge variant="outline" className="bg-blue-50 text-blue-700 border-blue-300"><Phone className="w-3 h-3 mr-1" /></Badge>;
case "demo-sent":
return <Badge variant="outline" className="bg-green-50 text-green-700 border-green-300"><CheckCircle2 className="w-3 h-3 mr-1" /></Badge>;
default:
return <Badge variant="outline"></Badge>;
}
};
const getIndustryLabel = (industry: string) => {
const labels: { [key: string]: string } = {
"automotive": "자동차 부품",
"electronics": "전자/전기",
"machinery": "기계/설비",
"food": "식품 가공",
"chemical": "화학/제약",
"plastic": "플라스틱/고무",
"metal": "금속 가공",
"other": "기타"
};
return labels[industry] || industry;
};
return (
<div className="min-h-screen bg-gradient-to-b from-purple-50 via-white to-blue-50">
{/* Header */}
<header className="backdrop-blur-sm bg-white/80 border-b border-gray-200/50 sticky top-0 z-50">
<div className="container mx-auto px-6 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<Button
variant="ghost"
onClick={() => navigate("/")}
className="rounded-xl hover:bg-gray-100"
>
<ArrowLeft className="w-5 h-5 mr-2" />
</Button>
<div className="flex items-center space-x-3">
<div className="w-10 h-10 rounded-xl flex items-center justify-center shadow-md bg-gradient-to-br from-purple-500 to-purple-600">
<div className="text-white font-bold text-lg">S</div>
</div>
<div>
<h1 className="font-bold text-gray-900">SAM </h1>
<p className="text-xs text-gray-500"> </p>
</div>
</div>
</div>
<Badge className="bg-purple-100 text-purple-700 px-4 py-2">
<Briefcase className="w-4 h-4 mr-2" />
</Badge>
</div>
</div>
</header>
{/* Main Content */}
<div className="container mx-auto px-6 py-8">
<div className="mb-8">
<h2 className="text-3xl font-extrabold text-gray-900 mb-2"> </h2>
<p className="text-gray-600"> </p>
</div>
{/* Stats */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
<Card>
<CardContent className="pt-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600 mb-1"> </p>
<p className="text-3xl font-bold text-gray-900">{leads.length}</p>
</div>
<div className="w-12 h-12 rounded-xl bg-blue-100 flex items-center justify-center">
<User className="w-6 h-6 text-blue-600" />
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600 mb-1"></p>
<p className="text-3xl font-bold text-yellow-600">{leads.filter(l => l.status === "pending").length}</p>
</div>
<div className="w-12 h-12 rounded-xl bg-yellow-100 flex items-center justify-center">
<Clock className="w-6 h-6 text-yellow-600" />
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600 mb-1"></p>
<p className="text-3xl font-bold text-blue-600">{leads.filter(l => l.status === "contacted").length}</p>
</div>
<div className="w-12 h-12 rounded-xl bg-blue-100 flex items-center justify-center">
<Phone className="w-6 h-6 text-blue-600" />
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600 mb-1"></p>
<p className="text-3xl font-bold text-green-600">{leads.filter(l => l.status === "demo-sent").length}</p>
</div>
<div className="w-12 h-12 rounded-xl bg-green-100 flex items-center justify-center">
<CheckCircle2 className="w-6 h-6 text-green-600" />
</div>
</div>
</CardContent>
</Card>
</div>
{/* Leads Table */}
<Card>
<CardHeader>
<CardTitle> </CardTitle>
<CardDescription> </CardDescription>
</CardHeader>
<CardContent>
{leads.length === 0 ? (
<div className="text-center py-12">
<User className="w-16 h-16 text-gray-300 mx-auto mb-4" />
<p className="text-gray-500 mb-2"> </p>
<p className="text-sm text-gray-400"> </p>
</div>
) : (
<div className="space-y-4">
{leads.map((lead) => (
<div
key={lead.id}
className="border border-gray-200 rounded-xl p-6 hover:shadow-lg transition-shadow bg-white"
>
<div className="flex items-start justify-between mb-4">
<div className="flex items-start gap-4">
<div className="w-12 h-12 rounded-xl bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center flex-shrink-0">
<User className="w-6 h-6 text-white" />
</div>
<div>
<div className="flex items-center gap-3 mb-2">
<h3 className="font-bold text-gray-900 text-lg">{lead.name}</h3>
{getStatusBadge(lead.status)}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-2 text-sm text-gray-600">
<div className="flex items-center gap-2">
<Building2 className="w-4 h-4 text-gray-400" />
{lead.company}
</div>
<div className="flex items-center gap-2">
<Briefcase className="w-4 h-4 text-gray-400" />
{getIndustryLabel(lead.industry)}
</div>
<div className="flex items-center gap-2">
<Mail className="w-4 h-4 text-gray-400" />
{lead.email}
</div>
<div className="flex items-center gap-2">
<Phone className="w-4 h-4 text-gray-400" />
{lead.phone}
</div>
</div>
{lead.message && (
<div className="mt-3 flex items-start gap-2 text-sm">
<MessageSquare className="w-4 h-4 text-gray-400 mt-0.5" />
<p className="text-gray-600 italic">{lead.message}</p>
</div>
)}
<div className="mt-3 text-xs text-gray-400">
: {new Date(lead.submittedAt).toLocaleString('ko-KR')}
</div>
</div>
</div>
<div className="flex flex-col gap-2">
{lead.status === "demo-sent" && lead.demoLink ? (
<>
<Button
size="sm"
variant="outline"
onClick={() => {
const link = lead.demoLink!;
// Try modern Clipboard API first
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(link).then(() => {
toast.success("링크가 복사되었습니다!");
}).catch(() => {
// Fallback
const textArea = document.createElement("textarea");
textArea.value = link;
textArea.style.position = "fixed";
textArea.style.left = "-999999px";
document.body.appendChild(textArea);
textArea.select();
try {
document.execCommand('copy');
toast.success("링크가 복사되었습니다!");
} catch (err) {
toast.error("복사 실패. 수동으로 복사해주세요.");
}
document.body.removeChild(textArea);
});
} else {
// Fallback for non-secure contexts
const textArea = document.createElement("textarea");
textArea.value = link;
textArea.style.position = "fixed";
textArea.style.left = "-999999px";
document.body.appendChild(textArea);
textArea.select();
try {
document.execCommand('copy');
toast.success("링크가 복사되었습니다!");
} catch (err) {
toast.error("복사 실패. 수동으로 복사해주세요.");
}
document.body.removeChild(textArea);
}
}}
className="rounded-lg"
>
<Copy className="w-4 h-4 mr-2" />
</Button>
<Button
size="sm"
variant="outline"
onClick={() => handleOpenDemo(lead)}
className="rounded-lg"
>
<ExternalLink className="w-4 h-4 mr-2" />
</Button>
{lead.demoExpiryDate && (
<p className="text-xs text-gray-500 text-center mt-1">
: {new Date(lead.demoExpiryDate).toLocaleDateString('ko-KR')}
</p>
)}
</>
) : (
<Button
size="sm"
onClick={() => handleCreateDemo(lead)}
className="rounded-lg bg-gradient-to-r from-blue-500 to-purple-600 hover:from-blue-600 hover:to-purple-700 text-white"
>
<Sparkles className="w-4 h-4 mr-2" />
</Button>
)}
</div>
</div>
</div>
))}
</div>
)}
</CardContent>
</Card>
</div>
{/* Create Demo Modal */}
<Dialog open={isCreateDemoOpen} onOpenChange={setIsCreateDemoOpen}>
<DialogContent className="sm:max-w-[600px]">
<DialogHeader>
<DialogTitle className="text-2xl font-bold"> </DialogTitle>
<DialogDescription>
{selectedLead && `${selectedLead.name}님을 위한 데모를 설정하세요`}
</DialogDescription>
</DialogHeader>
{selectedLead && (
<div className="space-y-6 mt-4">
{/* Client Info */}
<div className="bg-blue-50 rounded-xl p-4 border border-blue-200">
<h4 className="font-semibold text-gray-900 mb-3 flex items-center gap-2">
<User className="w-4 h-4 text-blue-600" />
</h4>
<div className="grid grid-cols-2 gap-2 text-sm">
<div><span className="text-gray-600">:</span> <span className="font-semibold">{selectedLead.name}</span></div>
<div><span className="text-gray-600">:</span> <span className="font-semibold">{selectedLead.company}</span></div>
<div><span className="text-gray-600">:</span> <span className="font-semibold">{selectedLead.email}</span></div>
<div><span className="text-gray-600">:</span> <span className="font-semibold">{selectedLead.phone}</span></div>
<div className="col-span-2"><span className="text-gray-600">:</span> <span className="font-semibold">{getIndustryLabel(selectedLead.industry)}</span></div>
{selectedLead.message && (
<div className="col-span-2"><span className="text-gray-600">:</span> <span className="font-semibold">{selectedLead.message}</span></div>
)}
</div>
</div>
{/* Demo Settings */}
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="industryPreset" className="text-sm font-semibold flex items-center gap-2">
<Briefcase className="w-4 h-4 text-purple-600" />
*
</Label>
<Select value={industryPreset} onValueChange={setIndustryPreset} required>
<SelectTrigger className="h-11">
<SelectValue placeholder="산업 분야를 선택해주세요" />
</SelectTrigger>
<SelectContent>
<SelectItem value="automotive"> </SelectItem>
<SelectItem value="electronics">/</SelectItem>
<SelectItem value="machinery">/</SelectItem>
<SelectItem value="food"> </SelectItem>
<SelectItem value="chemical">/</SelectItem>
<SelectItem value="plastic">/</SelectItem>
<SelectItem value="metal"> </SelectItem>
<SelectItem value="other"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="demoDuration" className="text-sm font-semibold flex items-center gap-2">
<Calendar className="w-4 h-4 text-purple-600" />
*
</Label>
<Select value={demoDuration} onValueChange={setDemoDuration} required>
<SelectTrigger className="h-11">
<SelectValue placeholder="체험 기간을 선택해주세요" />
</SelectTrigger>
<SelectContent>
<SelectItem value="3">3</SelectItem>
<SelectItem value="7">7 ()</SelectItem>
<SelectItem value="14">14</SelectItem>
<SelectItem value="30">30</SelectItem>
</SelectContent>
</Select>
</div>
</div>
{/* Generated Link */}
{generatedLink && (
<div className="bg-green-50 rounded-xl p-4 border border-green-200">
<h4 className="font-semibold text-gray-900 mb-3 flex items-center gap-2">
<CheckCircle2 className="w-4 h-4 text-green-600" />
</h4>
<div className="flex items-center gap-2">
<Input
value={generatedLink}
readOnly
className="flex-1 bg-white"
/>
<Button
onClick={handleCopyLink}
variant="outline"
size="sm"
className="flex-shrink-0"
>
{isCopied ? <CheckCircle2 className="w-4 h-4 text-green-600" /> : <Copy className="w-4 h-4" />}
</Button>
</div>
<p className="text-xs text-gray-600 mt-2">
: {new Date(Date.now() + parseInt(demoDuration) * 24 * 60 * 60 * 1000).toLocaleDateString('ko-KR')}
</p>
</div>
)}
{/* Buttons */}
<div className="flex flex-col-reverse sm:flex-row gap-3 pt-4">
{!generatedLink ? (
<>
<Button
type="button"
variant="outline"
onClick={() => setIsCreateDemoOpen(false)}
className="flex-1 h-12"
>
</Button>
<Button
onClick={handleGenerateLink}
disabled={!industryPreset || !demoDuration}
className="flex-1 h-12 bg-gradient-to-r from-blue-500 to-purple-600 hover:from-blue-600 hover:to-purple-700 text-white font-semibold"
>
<Sparkles className="w-4 h-4 mr-2" />
</Button>
</>
) : (
<>
<Button
type="button"
variant="outline"
onClick={() => setIsCreateDemoOpen(false)}
className="flex-1 h-12"
>
</Button>
<Button
onClick={() => {
if (selectedLead) {
handleOpenDemo(selectedLead);
}
}}
variant="outline"
className="flex-1 h-12 border-2 border-purple-500 text-purple-700 hover:bg-purple-50 font-semibold"
>
<ExternalLink className="w-4 h-4 mr-2" />
</Button>
<Button
onClick={handleSendEmail}
className="flex-1 h-12 bg-gradient-to-r from-green-500 to-green-600 hover:from-green-600 hover:to-green-700 text-white font-semibold"
>
<Send className="w-4 h-4 mr-2" />
</Button>
</>
)}
</div>
</div>
)}
</DialogContent>
</Dialog>
</div>
);
}

View File

@@ -1,52 +0,0 @@
import { useState } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent } from "@/components/ui/dialog";
import { Plus, Download } from "lucide-react";
import { QuoteCreation, QuoteList } from "./QuoteCreation";
export function SalesManagement() {
const [isQuoteCreationOpen, setIsQuoteCreationOpen] = useState(false);
return (
<div className="p-4 md:p-6 space-y-6">
{/* 헤더 */}
<div className="bg-card border border-border/20 rounded-xl p-6">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<h1 className="text-3xl font-bold text-foreground mb-2"></h1>
<p className="text-muted-foreground">, , </p>
</div>
<div className="flex space-x-3">
<Button variant="outline" className="border-border/50">
<Download className="h-4 w-4 mr-2" />
Excel
</Button>
<Button
className="bg-primary text-primary-foreground"
onClick={() => setIsQuoteCreationOpen(true)}
>
<Plus className="h-4 w-4 mr-2" />
</Button>
</div>
</div>
</div>
{/* 견적 산출하기 모달 */}
<Dialog open={isQuoteCreationOpen} onOpenChange={setIsQuoteCreationOpen}>
<DialogContent className="max-w-[95vw] w-[95vw] max-h-[95vh] h-[95vh] overflow-hidden p-0">
<div className="h-full overflow-y-auto">
<QuoteCreation onClose={() => setIsQuoteCreationOpen(false)} />
</div>
</DialogContent>
</Dialog>
{/* 견적 목록 */}
<div className="bg-card border border-border/20 rounded-xl p-6">
<h2 className="text-xl font-bold mb-4"> </h2>
<QuoteList />
</div>
</div>
);
}

View File

@@ -1,94 +0,0 @@
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogTitle, DialogDescription } from "@/components/ui/dialog";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Plus, Download } from "lucide-react";
import { QuoteCreation, QuoteList, OrderList, OrderRegistration } from "./QuoteCreation";
export function SalesManagement() {
const [isQuoteCreationOpen, setIsQuoteCreationOpen] = useState(false);
const [isOrderRegistrationOpen, setIsOrderRegistrationOpen] = useState(false);
const [activeTab, setActiveTab] = useState("quote");
return (
<div className="p-4 md:p-6 space-y-6">
{/* 헤더 */}
<div className="bg-card border border-border/20 rounded-xl p-6">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<h1 className="text-3xl font-bold text-foreground mb-2"></h1>
<p className="text-muted-foreground">, , </p>
</div>
<div className="flex space-x-3">
<Button variant="outline" className="border-border/50">
<Download className="h-4 w-4 mr-2" />
Excel
</Button>
{activeTab === "quote" && (
<Button
className="bg-primary text-primary-foreground"
onClick={() => setIsQuoteCreationOpen(true)}
>
<Plus className="h-4 w-4 mr-2" />
</Button>
)}
{activeTab === "order" && (
<Button
className="bg-primary text-primary-foreground"
onClick={() => setIsOrderRegistrationOpen(true)}
>
<Plus className="h-4 w-4 mr-2" />
</Button>
)}
</div>
</div>
</div>
{/* 견적 산출하기 모달 */}
<Dialog open={isQuoteCreationOpen} onOpenChange={setIsQuoteCreationOpen}>
<DialogContent className="max-w-[95vw] w-[95vw] max-h-[95vh] h-[95vh] overflow-hidden p-0">
<DialogTitle className="sr-only"> </DialogTitle>
<DialogDescription className="sr-only">
.
</DialogDescription>
<div className="h-full overflow-y-auto">
<QuoteCreation onClose={() => setIsQuoteCreationOpen(false)} />
</div>
</DialogContent>
</Dialog>
{/* 수주 등록하기 모달 */}
<Dialog open={isOrderRegistrationOpen} onOpenChange={setIsOrderRegistrationOpen}>
<DialogContent className="max-w-[95vw] w-[95vw] max-h-[95vh] h-[95vh] overflow-hidden p-0">
<DialogTitle className="sr-only"> </DialogTitle>
<DialogDescription className="sr-only">
.
</DialogDescription>
<div className="h-full overflow-y-auto">
<OrderRegistration onClose={() => setIsOrderRegistrationOpen(false)} />
</div>
</DialogContent>
</Dialog>
{/* 견적/수주 탭 */}
<div className="bg-card border border-border/20 rounded-xl p-6">
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="grid w-full max-w-md grid-cols-2">
<TabsTrigger value="quote"> </TabsTrigger>
<TabsTrigger value="order"> </TabsTrigger>
</TabsList>
<TabsContent value="quote" className="mt-6">
<QuoteList />
</TabsContent>
<TabsContent value="order" className="mt-6">
<OrderList />
</TabsContent>
</Tabs>
</div>
</div>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,524 +0,0 @@
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Badge } from "@/components/ui/badge";
import {
ArrowLeft,
Building2,
User,
Mail,
Phone,
Lock,
Tag,
CheckCircle2,
Briefcase,
Users,
FileText
} from "lucide-react";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
export function SignupPage() {
const navigate = useNavigate();
const [step, setStep] = useState(1);
const [formData, setFormData] = useState({
// 회사 정보
companyName: "",
businessNumber: "",
industry: "",
companySize: "",
// 담당자 정보
name: "",
position: "",
email: "",
phone: "",
userId: "",
password: "",
passwordConfirm: "",
// 플랜 및 추천인
plan: "demo",
salesCode: "",
// 약관
agreeTerms: false,
agreePrivacy: false,
});
const [salesCodeValid, setSalesCodeValid] = useState<boolean | null>(null);
const [discount, setDiscount] = useState(0);
const handleInputChange = (field: string, value: any) => {
setFormData(prev => ({ ...prev, [field]: value }));
};
const validateSalesCode = (code: string) => {
// 영업사원 코드 검증 로직 (실제로는 API 호출)
const validCodes: { [key: string]: number } = {
"SALES2024": 20,
"PARTNER30": 30,
"VIP50": 50,
};
if (validCodes[code]) {
setSalesCodeValid(true);
setDiscount(validCodes[code]);
} else if (code === "") {
setSalesCodeValid(null);
setDiscount(0);
} else {
setSalesCodeValid(false);
setDiscount(0);
}
};
const handleSalesCodeChange = (code: string) => {
handleInputChange("salesCode", code);
validateSalesCode(code);
};
const handleSubmit = () => {
// 회원가입 처리 (실제로는 API 호출)
const userData = {
...formData,
discount,
role: "CEO", // 기본 역할
};
// Save user data to localStorage
localStorage.setItem("user", JSON.stringify(userData));
// Navigate to dashboard
navigate("/dashboard");
};
const isStep1Valid = formData.companyName && formData.businessNumber && formData.industry && formData.companySize;
const isStep2Valid = formData.name && formData.email && formData.phone && formData.userId && formData.password && formData.password === formData.passwordConfirm;
const isStep3Valid = formData.agreeTerms && formData.agreePrivacy;
return (
<div className="min-h-screen bg-background flex flex-col">
{/* Header */}
<header className="clean-glass border-b border-border">
<div className="container mx-auto px-6 py-4">
<div className="flex items-center justify-between">
<button
onClick={() => navigate("/")}
className="flex items-center space-x-3 hover:opacity-80 transition-opacity"
>
<div className="w-10 h-10 rounded-xl flex items-center justify-center clean-shadow relative overflow-hidden" style={{ backgroundColor: '#3B82F6' }}>
<div className="text-white font-bold text-lg">S</div>
<div className="absolute inset-0 bg-gradient-to-br from-white/10 to-transparent opacity-30"></div>
</div>
<div>
<h1 className="text-xl font-bold tracking-wide">SAM</h1>
<p className="text-xs text-muted-foreground"></p>
</div>
</button>
<Button variant="ghost" onClick={() => navigate("/login")} className="rounded-xl">
</Button>
</div>
</div>
</header>
{/* Main Content */}
<div className="flex-1 container mx-auto px-6 py-12">
<div className="max-w-2xl mx-auto">
{/* Progress Steps */}
<div className="mb-12">
<div className="flex items-center justify-between mb-4">
{[1, 2, 3].map((stepNumber) => (
<div key={stepNumber} className="flex items-center flex-1">
<div className={`w-10 h-10 rounded-full flex items-center justify-center font-semibold transition-colors ${
step >= stepNumber
? "bg-primary text-white"
: "bg-muted text-muted-foreground"
}`}>
{stepNumber}
</div>
{stepNumber < 3 && (
<div className={`flex-1 h-1 mx-4 rounded transition-colors ${
step > stepNumber ? "bg-primary" : "bg-muted"
}`} />
)}
</div>
))}
</div>
<div className="flex justify-between text-sm">
<span className={step >= 1 ? "text-foreground font-medium" : "text-muted-foreground"}>
</span>
<span className={step >= 2 ? "text-foreground font-medium" : "text-muted-foreground"}>
</span>
<span className={step >= 3 ? "text-foreground font-medium" : "text-muted-foreground"}>
</span>
</div>
</div>
{/* Step 1: 회사 정보 */}
{step === 1 && (
<div className="clean-glass rounded-2xl p-8 clean-shadow space-y-6">
<div>
<h2 className="mb-2 text-foreground"> </h2>
<p className="text-muted-foreground">MES </p>
</div>
<div className="space-y-4">
<div>
<Label htmlFor="companyName" className="flex items-center space-x-2 mb-2">
<Building2 className="w-4 h-4" />
<span> *</span>
</Label>
<Input
id="companyName"
placeholder="예: 삼성전자"
value={formData.companyName}
onChange={(e) => handleInputChange("companyName", e.target.value)}
className="clean-input"
/>
</div>
<div>
<Label htmlFor="businessNumber" className="flex items-center space-x-2 mb-2">
<FileText className="w-4 h-4" />
<span> *</span>
</Label>
<Input
id="businessNumber"
placeholder="000-00-00000"
value={formData.businessNumber}
onChange={(e) => handleInputChange("businessNumber", e.target.value)}
className="clean-input"
/>
</div>
<div>
<Label htmlFor="industry" className="flex items-center space-x-2 mb-2">
<Briefcase className="w-4 h-4" />
<span> *</span>
</Label>
<Select value={formData.industry} onValueChange={(value) => handleInputChange("industry", value)}>
<SelectTrigger className="clean-input">
<SelectValue placeholder="업종을 선택하세요" />
</SelectTrigger>
<SelectContent>
<SelectItem value="electronics">/</SelectItem>
<SelectItem value="machinery">/</SelectItem>
<SelectItem value="automotive">/</SelectItem>
<SelectItem value="chemical">/</SelectItem>
<SelectItem value="food">/</SelectItem>
<SelectItem value="textile">/</SelectItem>
<SelectItem value="metal">/</SelectItem>
<SelectItem value="other"> </SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="companySize" className="flex items-center space-x-2 mb-2">
<Users className="w-4 h-4" />
<span> *</span>
</Label>
<Select value={formData.companySize} onValueChange={(value) => handleInputChange("companySize", value)}>
<SelectTrigger className="clean-input">
<SelectValue placeholder="기업 규모를 선택하세요" />
</SelectTrigger>
<SelectContent>
<SelectItem value="small"> ( 10-50)</SelectItem>
<SelectItem value="medium"> ( 50-300)</SelectItem>
<SelectItem value="large"> ( 300 )</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<Button
onClick={() => setStep(2)}
disabled={!isStep1Valid}
className="w-full rounded-xl bg-primary hover:bg-primary/90"
>
</Button>
</div>
)}
{/* Step 2: 담당자 정보 */}
{step === 2 && (
<div className="clean-glass rounded-2xl p-8 clean-shadow space-y-6">
<div>
<h2 className="mb-2 text-foreground"> </h2>
<p className="text-muted-foreground"> </p>
</div>
<div className="space-y-4">
<div>
<Label htmlFor="name" className="flex items-center space-x-2 mb-2">
<User className="w-4 h-4"/>
<span> *</span>
</Label>
<Input
id="name"
placeholder="홍길동"
value={formData.name}
onChange={(e) => handleInputChange("name", e.target.value)}
className="clean-input"
/>
</div>
<div>
<Label htmlFor="position" className="flex items-center space-x-2 mb-2">
<Briefcase className="w-4 h-4"/>
<span></span>
</Label>
<Input
id="position"
placeholder="예: 생산관리팀장"
value={formData.position}
onChange={(e) => handleInputChange("position", e.target.value)}
className="clean-input"
/>
</div>
<div>
<Label htmlFor="email" className="flex items-center space-x-2 mb-2">
<Mail className="w-4 h-4"/>
<span> *</span>
</Label>
<Input
id="email"
type="email"
placeholder="example@company.com"
value={formData.email}
onChange={(e) => handleInputChange("email", e.target.value)}
className="clean-input"
/>
</div>
<div>
<Label htmlFor="phone" className="flex items-center space-x-2 mb-2">
<Phone className="w-4 h-4"/>
<span> *</span>
</Label>
<Input
id="phone"
placeholder="010-0000-0000"
value={formData.phone}
onChange={(e) => handleInputChange("phone", e.target.value)}
className="clean-input"
/>
</div>
<div>
<Label htmlFor="userId" className="flex items-center space-x-2 mb-2">
<User className="w-4 h-4"/>
<span> *</span>
</Label>
<Input
id="userId"
placeholder="영문, 숫자 조합 6자 이상"
value={formData.userId}
onChange={(e) => handleInputChange("userId", e.target.value)}
className="clean-input"
/>
</div>
<div>
<Label htmlFor="password" className="flex items-center space-x-2 mb-2">
<Lock className="w-4 h-4"/>
<span> *</span>
</Label>
<Input
id="password"
type="password"
placeholder="8자 이상 입력"
value={formData.password}
onChange={(e) => handleInputChange("password", e.target.value)}
className="clean-input"
/>
</div>
<div>
<Label htmlFor="passwordConfirm" className="flex items-center space-x-2 mb-2">
<Lock className="w-4 h-4"/>
<span> *</span>
</Label>
<Input
id="passwordConfirm"
type="password"
placeholder="비밀번호 재입력"
value={formData.passwordConfirm}
onChange={(e) => handleInputChange("passwordConfirm", e.target.value)}
className="clean-input"
/>
{formData.passwordConfirm && formData.password !== formData.passwordConfirm && (
<p className="text-sm text-destructive mt-1"> </p>
)}
</div>
</div>
<div className="flex space-x-4">
<Button
variant="outline"
onClick={() => setStep(1)}
className="flex-1 rounded-xl"
>
<ArrowLeft className="w-4 h-4 mr-2"/>
</Button>
<Button
onClick={() => setStep(3)}
disabled={!isStep2Valid}
className="flex-1 rounded-xl bg-primary hover:bg-primary/90"
>
</Button>
</div>
</div>
)}
{/* Step 3: 플랜 선택 */}
{step === 3 && (
<div className="space-y-6">
<div className="clean-glass rounded-2xl p-8 clean-shadow space-y-6">
<div>
<h2 className="mb-2 text-foreground"> </h2>
<p className="text-muted-foreground"> 30 </p>
</div>
<div className="space-y-3">
{[
{ id: "demo", name: "데모 체험판", desc: "30일 무료 체험 (모든 기능 이용)", badge: "추천" },
{ id: "standard", name: "스탠다드", desc: "중소기업 최적화 플랜" },
{ id: "premium", name: "프리미엄", desc: "중견기업 맞춤형 플랜" },
].map((plan) => (
<button
key={plan.id}
onClick={() => handleInputChange("plan", plan.id)}
className={`w-full p-4 rounded-xl border-2 transition-all text-left ${
formData.plan === plan.id
? "border-primary bg-primary/5"
: "border-border hover:border-primary/50"
}`}
>
<div className="flex items-center justify-between">
<div>
<div className="flex items-center space-x-2">
<span className="font-semibold">{plan.name}</span>
{plan.badge && (
<Badge className="bg-primary text-white text-xs">
{plan.badge}
</Badge>
)}
</div>
<p className="text-sm text-muted-foreground mt-1">{plan.desc}</p>
</div>
{formData.plan === plan.id && (
<CheckCircle2 className="w-6 h-6 text-primary" />
)}
</div>
</button>
))}
</div>
<div>
<Label htmlFor="salesCode" className="flex items-center space-x-2 mb-2">
<Tag className="w-4 h-4" />
<span> ()</span>
</Label>
<div className="relative">
<Input
id="salesCode"
placeholder="추천코드를 입력하면 할인 혜택을 받을 수 있습니다"
value={formData.salesCode}
onChange={(e) => handleSalesCodeChange(e.target.value)}
className={`clean-input pr-10 ${
salesCodeValid === true ? "border-green-500" :
salesCodeValid === false ? "border-destructive" : ""
}`}
/>
{salesCodeValid === true && (
<CheckCircle2 className="w-5 h-5 text-green-500 absolute right-3 top-1/2 -translate-y-1/2" />
)}
</div>
{salesCodeValid === true && (
<p className="text-sm text-green-600 mt-2 flex items-center space-x-2">
<CheckCircle2 className="w-4 h-4" />
<span> ! {discount}% </span>
</p>
)}
{salesCodeValid === false && (
<p className="text-sm text-destructive mt-2"> </p>
)}
<p className="text-xs text-muted-foreground mt-2">
💡 코드: SALES2024 (20%), PARTNER30 (30%), VIP50 (50%)
</p>
</div>
<div className="space-y-3 pt-4 border-t border-border">
<label className="flex items-start space-x-3 cursor-pointer">
<input
type="checkbox"
checked={formData.agreeTerms}
onChange={(e) => handleInputChange("agreeTerms", e.target.checked)}
className="mt-1 w-4 h-4 rounded border-border"
/>
<span className="text-sm">
<span className="font-medium text-foreground">[]</span>
</span>
</label>
<label className="flex items-start space-x-3 cursor-pointer">
<input
type="checkbox"
checked={formData.agreePrivacy}
onChange={(e) => handleInputChange("agreePrivacy", e.target.checked)}
className="mt-1 w-4 h-4 rounded border-border"
/>
<span className="text-sm">
<span className="font-medium text-foreground">[]</span>
</span>
</label>
</div>
</div>
<div className="flex space-x-4">
<Button
variant="outline"
onClick={() => setStep(2)}
className="flex-1 rounded-xl"
>
<ArrowLeft className="w-4 h-4 mr-2" />
</Button>
<Button
onClick={handleSubmit}
disabled={!isStep3Valid}
className="flex-1 rounded-xl bg-primary hover:bg-primary/90"
>
</Button>
</div>
</div>
)}
{/* Login Link */}
<div className="text-center mt-6">
<p className="text-sm text-muted-foreground">
?{" "}
<button
onClick={() => navigate("/login")}
className="text-primary font-medium hover:underline"
>
</button>
</p>
</div>
</div>
</div>
</div>
);
}

View File

@@ -1,573 +0,0 @@
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Progress } from "@/components/ui/progress";
import {
Server,
Database,
Users,
Shield,
Activity,
AlertTriangle,
CheckCircle,
TrendingUp,
TrendingDown,
Clock,
HardDrive,
Cpu,
MemoryStick,
Network,
Wifi,
FileText,
RefreshCw,
Eye,
Settings,
UserCheck,
UserX,
Lock,
Unlock
} from "lucide-react";
import { LineChart, Line, AreaChart, Area, BarChart, Bar, PieChart, Pie, Cell, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from "recharts";
export function SystemAdminDashboard() {
// 시스템 상태 데이터
const systemStatus = {
servers: {
total: 8,
online: 7,
offline: 1,
warning: 2
},
database: {
connections: 45,
maxConnections: 100,
queryPerformance: 98.5,
backupStatus: "완료",
lastBackup: "2024-12-30 02:00"
},
users: {
total: 125,
active: 89,
inactive: 36,
newToday: 3,
loginToday: 67
},
security: {
threatLevel: "낮음",
blockedAttacks: 12,
securityEvents: 3,
lastScan: "2024-12-30 08:00"
}
};
// 서버 리소스 데이터
const serverResources = [
{ name: "WEB-01", cpu: 45, memory: 62, disk: 78, status: "정상" },
{ name: "DB-01", cpu: 78, memory: 85, disk: 45, status: "주의" },
{ name: "APP-01", cpu: 32, memory: 48, disk: 67, status: "정상" },
{ name: "API-01", cpu: 89, memory: 92, disk: 56, status: "경고" },
{ name: "FILE-01", cpu: 23, memory: 34, disk: 89, status: "정상" },
{ name: "BACKUP-01", cpu: 15, memory: 28, disk: 95, status: "주의" }
];
// 시스템 사용량 트렌드
const usageTrend = [
{ time: "00:00", cpu: 25, memory: 40, network: 15 },
{ time: "04:00", cpu: 20, memory: 35, network: 10 },
{ time: "08:00", cpu: 65, memory: 70, network: 45 },
{ time: "12:00", cpu: 85, memory: 80, network: 65 },
{ time: "16:00", cpu: 90, memory: 85, network: 70 },
{ time: "20:00", cpu: 75, memory: 75, network: 55 },
{ time: "23:59", cpu: 45, memory: 55, network: 35 }
];
// 사용자 활동 분석
const userActivity = [
{ name: "로그인", value: 245, color: "#1428A0" },
{ name: "작업", value: 189, color: "#00D084" },
{ name: "보고서", value: 156, color: "#FF6B35" },
{ name: "승인", value: 89, color: "#8B5FBF" },
{ name: "시스템", value: 45, color: "#FF4444" }
];
// 보안 이벤트 로그
const securityEvents = [
{ time: "09:15", event: "성공적인 로그인", user: "김대표", ip: "192.168.1.100", severity: "정보" },
{ time: "09:12", event: "비정상 접근 차단", user: "Unknown", ip: "203.142.15.23", severity: "경고" },
{ time: "08:45", event: "권한 변경", user: "최시스템", ip: "192.168.1.105", severity: "정보" },
{ time: "08:30", event: "다중 로그인 실패", user: "이생산", ip: "192.168.1.110", severity: "주의" },
{ time: "08:15", event: "백업 완료", user: "System", ip: "Local", severity: "정보" }
];
// 데이터베이스 성능 지표
const dbPerformance = [
{ metric: "평균 응답시간", value: "12ms", status: "excellent" },
{ metric: "동시 연결", value: "45/100", status: "good" },
{ metric: "쿼리 처리량", value: "1,250/분", status: "good" },
{ metric: "인덱스 효율성", value: "98.5%", status: "excellent" },
{ metric: "캐시 적중률", value: "94.2%", status: "excellent" },
{ metric: "디스크 I/O", value: "2.3MB/s", status: "good" }
];
const getStatusColor = (status: string) => {
switch (status) {
case "정상": return "bg-green-500";
case "주의": return "bg-yellow-500";
case "경고": return "bg-orange-500";
case "오프라인": return "bg-red-500";
default: return "bg-muted";
}
};
const getSeverityColor = (severity: string) => {
switch (severity) {
case "정보": return "text-blue-600 bg-blue-50";
case "주의": return "text-yellow-600 bg-yellow-50";
case "경고": return "text-orange-600 bg-orange-50";
case "위험": return "text-red-600 bg-red-50";
default: return "text-muted-foreground bg-muted/50";
}
};
return (
<div className="p-6 space-y-6 bg-background min-h-screen">
{/* 헤더 */}
<div className="flex justify-between items-center">
<div>
<h1 className="text-3xl font-bold text-foreground"> </h1>
<p className="text-muted-foreground mt-1">SAM </p>
</div>
<div className="flex space-x-3">
<Button size="sm" className="samsung-button">
<RefreshCw className="w-4 h-4 mr-2" />
</Button>
<Button size="sm" variant="outline">
<Settings className="w-4 h-4 mr-2" />
</Button>
</div>
</div>
{/* 시스템 상태 개요 */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<Card className="samsung-card">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"> </CardTitle>
<Server className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="flex items-center justify-between">
<div>
<div className="text-2xl font-bold text-green-600">
{systemStatus.servers.online}/{systemStatus.servers.total}
</div>
<p className="text-xs text-muted-foreground">/</p>
</div>
<div className="text-right">
<Badge className="bg-green-500 text-white mb-1">
{systemStatus.servers.online}
</Badge>
{systemStatus.servers.warning > 0 && (
<Badge className="bg-yellow-500 text-white block">
{systemStatus.servers.warning}
</Badge>
)}
</div>
</div>
</CardContent>
</Card>
<Card className="samsung-card">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"></CardTitle>
<Database className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-2xl font-bold text-blue-600">
{systemStatus.database.connections}
</span>
<span className="text-sm text-muted-foreground">
/{systemStatus.database.maxConnections}
</span>
</div>
<Progress
value={(systemStatus.database.connections / systemStatus.database.maxConnections) * 100}
className="h-2"
/>
<p className="text-xs text-muted-foreground">
: {systemStatus.database.queryPerformance}%
</p>
</div>
</CardContent>
</Card>
<Card className="samsung-card">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"> </CardTitle>
<Users className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="space-y-2">
<div className="flex justify-between">
<span className="text-2xl font-bold text-primary">
{systemStatus.users.active}
</span>
<span className="text-sm text-muted-foreground">
/{systemStatus.users.total}
</span>
</div>
<div className="flex justify-between text-xs">
<span className="text-green-600">
<UserCheck className="w-3 h-3 inline mr-1" />
: {systemStatus.users.loginToday}
</span>
</div>
<div className="text-xs text-muted-foreground">
: {systemStatus.users.newToday}
</div>
</div>
</CardContent>
</Card>
<Card className="samsung-card">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"> </CardTitle>
<Shield className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="space-y-2">
<div className="flex items-center justify-between">
<Badge className="bg-green-500 text-white">
{systemStatus.security.threatLevel}
</Badge>
<CheckCircle className="h-5 w-5 text-green-500" />
</div>
<div className="text-xs space-y-1">
<div className="flex justify-between">
<span> :</span>
<span className="font-medium">{systemStatus.security.blockedAttacks}</span>
</div>
<div className="flex justify-between">
<span> :</span>
<span className="font-medium">{systemStatus.security.securityEvents}</span>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
{/* 시스템 리소스 및 성능 */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card className="samsung-card">
<CardHeader>
<CardTitle className="flex items-center space-x-2">
<Activity className="w-5 h-5" />
<span> </span>
</CardTitle>
</CardHeader>
<CardContent>
<ResponsiveContainer width="100%" height={300}>
<AreaChart data={usageTrend}>
<CartesianGrid strokeDasharray="3 3" stroke="#e0e0e0" />
<XAxis dataKey="time" stroke="#666" />
<YAxis stroke="#666" />
<Tooltip />
<Area type="monotone" dataKey="cpu" stackId="1" stroke="#1428A0" fill="#1428A0" name="CPU %" />
<Area type="monotone" dataKey="memory" stackId="1" stroke="#00D084" fill="#00D084" name="메모리 %" />
<Area type="monotone" dataKey="network" stackId="1" stroke="#FF6B35" fill="#FF6B35" name="네트워크 %" />
<Legend />
</AreaChart>
</ResponsiveContainer>
</CardContent>
</Card>
<Card className="samsung-card">
<CardHeader>
<CardTitle className="flex items-center space-x-2">
<Users className="w-5 h-5" />
<span> </span>
</CardTitle>
</CardHeader>
<CardContent>
<ResponsiveContainer width="100%" height={300}>
<PieChart>
<Pie
data={userActivity}
cx="50%"
cy="50%"
outerRadius={80}
fill="#8884d8"
dataKey="value"
label={({ name, percent }) => `${name} ${(percent * 100).toFixed(0)}%`}
>
{userActivity.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Pie>
<Tooltip />
</PieChart>
</ResponsiveContainer>
</CardContent>
</Card>
</div>
{/* 서버 리소스 상세 */}
<Card className="samsung-card">
<CardHeader>
<CardTitle className="flex items-center space-x-2">
<Server className="w-5 h-5" />
<span> </span>
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{serverResources.map((server) => (
<div key={server.name} className="p-4 border rounded-2xl bg-muted/50 dark:bg-muted/20">
<div className="flex justify-between items-center mb-3">
<h4 className="font-semibold">{server.name}</h4>
<Badge className={getStatusColor(server.status)}>
{server.status}
</Badge>
</div>
<div className="space-y-3">
<div>
<div className="flex justify-between text-sm mb-1">
<span className="flex items-center">
<Cpu className="w-3 h-3 mr-1" />
CPU
</span>
<span>{server.cpu}%</span>
</div>
<Progress value={server.cpu} className="h-2" />
</div>
<div>
<div className="flex justify-between text-sm mb-1">
<span className="flex items-center">
<MemoryStick className="w-3 h-3 mr-1" />
</span>
<span>{server.memory}%</span>
</div>
<Progress value={server.memory} className="h-2" />
</div>
<div>
<div className="flex justify-between text-sm mb-1">
<span className="flex items-center">
<HardDrive className="w-3 h-3 mr-1" />
</span>
<span>{server.disk}%</span>
</div>
<Progress value={server.disk} className="h-2" />
</div>
</div>
</div>
))}
</div>
</CardContent>
</Card>
{/* 데이터베이스 성능 및 보안 로그 */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card className="samsung-card">
<CardHeader>
<CardTitle className="flex items-center space-x-2">
<Database className="w-5 h-5" />
<span> </span>
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
{dbPerformance.map((item, index) => (
<div key={index} className="flex justify-between items-center p-3 bg-muted/50 dark:bg-muted/20 rounded-lg">
<span className="font-medium">{item.metric}</span>
<div className="flex items-center space-x-2">
<span className="font-semibold">{item.value}</span>
{item.status === "excellent" && <CheckCircle className="w-4 h-4 text-green-500" />}
{item.status === "good" && <CheckCircle className="w-4 h-4 text-blue-500" />}
{item.status === "warning" && <AlertTriangle className="w-4 h-4 text-yellow-500" />}
</div>
</div>
))}
</div>
<div className="mt-4 p-3 bg-blue-50 rounded-lg">
<div className="flex items-center justify-between">
<span className="text-sm font-medium"> </span>
<span className="text-sm text-blue-600">{systemStatus.database.lastBackup}</span>
</div>
<Badge className="bg-green-500 text-white mt-2">
{systemStatus.database.backupStatus}
</Badge>
</div>
</CardContent>
</Card>
<Card className="samsung-card">
<CardHeader>
<CardTitle className="flex items-center space-x-2">
<Shield className="w-5 h-5" />
<span> </span>
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-3 max-h-80 overflow-y-auto custom-scrollbar">
{securityEvents.map((event, index) => (
<div key={index} className="flex justify-between items-start p-3 border rounded-lg">
<div className="flex-1">
<div className="flex items-center space-x-2 mb-1">
<span className="text-sm font-medium">{event.event}</span>
<Badge className={`text-xs ${getSeverityColor(event.severity)}`}>
{event.severity}
</Badge>
</div>
<div className="text-xs text-muted-foreground space-y-1">
<div>: {event.user}</div>
<div>IP: {event.ip}</div>
</div>
</div>
<div className="text-xs text-muted-foreground flex items-center">
<Clock className="w-3 h-3 mr-1" />
{event.time}
</div>
</div>
))}
</div>
<div className="mt-4 flex space-x-2">
<Button size="sm" variant="outline" className="flex-1">
<Eye className="w-4 h-4 mr-2" />
</Button>
<Button size="sm" variant="outline">
<RefreshCw className="w-4 h-4" />
</Button>
</div>
</CardContent>
</Card>
</div>
{/* 시스템 알림 및 작업 */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<Card className="samsung-card">
<CardHeader>
<CardTitle className="flex items-center space-x-2">
<AlertTriangle className="w-5 h-5" />
<span> </span>
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-3">
<div className="p-3 bg-yellow-50 border-l-4 border-yellow-400 rounded">
<div className="flex justify-between items-start">
<div>
<p className="font-medium text-sm">DB-01 </p>
<p className="text-xs text-muted-foreground mt-1">85% - </p>
</div>
<Badge className="bg-yellow-500 text-white text-xs"></Badge>
</div>
</div>
<div className="p-3 bg-orange-50 border-l-4 border-orange-400 rounded">
<div className="flex justify-between items-start">
<div>
<p className="font-medium text-sm">API-01 CPU </p>
<p className="text-xs text-muted-foreground mt-1">89% - </p>
</div>
<Badge className="bg-orange-500 text-white text-xs"></Badge>
</div>
</div>
<div className="p-3 bg-green-50 border-l-4 border-green-400 rounded">
<div className="flex justify-between items-start">
<div>
<p className="font-medium text-sm"> </p>
<p className="text-xs text-muted-foreground mt-1"> 02:00 </p>
</div>
<Badge className="bg-green-500 text-white text-xs"></Badge>
</div>
</div>
</div>
</CardContent>
</Card>
<Card className="samsung-card">
<CardHeader>
<CardTitle className="flex items-center space-x-2">
<Settings className="w-5 h-5" />
<span> </span>
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<Button variant="outline" className="w-full justify-start">
<Users className="w-4 h-4 mr-2" />
</Button>
<Button variant="outline" className="w-full justify-start">
<Database className="w-4 h-4 mr-2" />
</Button>
<Button variant="outline" className="w-full justify-start">
<Shield className="w-4 h-4 mr-2" />
</Button>
<Button variant="outline" className="w-full justify-start">
<FileText className="w-4 h-4 mr-2" />
</Button>
<Button variant="outline" className="w-full justify-start">
<RefreshCw className="w-4 h-4 mr-2" />
</Button>
</CardContent>
</Card>
<Card className="samsung-card">
<CardHeader>
<CardTitle className="flex items-center space-x-2">
<Activity className="w-5 h-5" />
<span> </span>
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="flex justify-between items-center">
<span className="text-sm"> </span>
<div className="flex items-center space-x-2">
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
<span className="text-sm font-medium">87.5% </span>
</div>
</div>
<div className="flex justify-between items-center">
<span className="text-sm"></span>
<div className="flex items-center space-x-2">
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
<span className="text-sm font-medium">98.5% </span>
</div>
</div>
<div className="flex justify-between items-center">
<span className="text-sm"> </span>
<div className="flex items-center space-x-2">
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
<span className="text-sm font-medium"></span>
</div>
</div>
<div className="flex justify-between items-center">
<span className="text-sm"> </span>
<div className="flex items-center space-x-2">
<div className="w-2 h-2 bg-blue-500 rounded-full"></div>
<span className="text-sm font-medium">71% </span>
</div>
</div>
<div className="pt-3 border-t">
<div className="text-xs text-muted-foreground text-center">
: {new Date().toLocaleTimeString()}
</div>
</div>
</div>
</CardContent>
</Card>
</div>
</div>
);
}

View File

@@ -1,985 +0,0 @@
import { useState, useEffect } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Progress } from "@/components/ui/progress";
import { SystemAdminDashboard } from "./SystemAdminDashboard";
import { UserManagement } from "./UserManagement";
import MenuCustomization from "./MenuCustomization";
import {
Users,
Settings,
Database,
Shield,
Server,
Activity,
Bell,
FileText,
Download,
Upload,
RefreshCw,
AlertTriangle,
CheckCircle,
Clock,
TrendingUp,
Lock,
Key,
HardDrive,
Monitor,
Wifi,
Code,
Eye,
Search,
Filter,
Plus
} from "lucide-react";
interface SystemManagementProps {
userRole?: string;
defaultTab?: string;
}
export function SystemManagement({ userRole, defaultTab = "dashboard" }: SystemManagementProps) {
const [activeTab, setActiveTab] = useState(defaultTab);
// Update activeTab when defaultTab prop changes
useEffect(() => {
if (defaultTab) {
setActiveTab(defaultTab);
}
}, [defaultTab]);
// 시스템관리자가 아닌 경우 기본 인사관리 화면 표시
if (userRole !== "SystemAdmin") {
return (
<div className="p-6 space-y-6 bg-background min-h-screen">
<div className="flex justify-between items-center">
<div>
<h1 className="text-3xl font-bold text-foreground"> </h1>
<p className="text-muted-foreground mt-1"> </p>
</div>
<div className="flex space-x-3">
<Button variant="outline">
<Download className="w-4 h-4 mr-2" />
</Button>
<Button>
<Upload className="w-4 h-4 mr-2" />
</Button>
</div>
</div>
{/* 인사 현황 개요 */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<Card className="samsung-card">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"> </CardTitle>
<Users className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-primary">125</div>
<p className="text-xs text-muted-foreground"></p>
<div className="flex items-center pt-2">
<TrendingUp className="h-4 w-4 text-green-500 mr-1" />
<span className="text-xs text-green-600"> +3</span>
</div>
</CardContent>
</Card>
<Card className="samsung-card">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"></CardTitle>
<CheckCircle className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-green-600">96.8%</div>
<p className="text-xs text-muted-foreground"> </p>
<div className="flex items-center pt-2">
<CheckCircle className="h-4 w-4 text-green-500 mr-1" />
<span className="text-xs text-green-600">121 </span>
</div>
</CardContent>
</Card>
<Card className="samsung-card">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"> </CardTitle>
<Clock className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-orange-600">8</div>
<p className="text-xs text-muted-foreground"></p>
<div className="flex items-center pt-2">
<Clock className="h-4 w-4 text-orange-500 mr-1" />
<span className="text-xs text-orange-600"> 5, 3</span>
</div>
</CardContent>
</Card>
<Card className="samsung-card">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"> </CardTitle>
<Users className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-blue-600">3</div>
<p className="text-xs text-muted-foreground"> ( )</p>
<div className="flex items-center pt-2">
<TrendingUp className="h-4 w-4 text-blue-500 mr-1" />
<span className="text-xs text-blue-600"> 2 </span>
</div>
</CardContent>
</Card>
</div>
{/* 부서별 현황 */}
<Card className="samsung-card">
<CardHeader>
<CardTitle> </CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{[
{ name: "생산부", total: 45, present: 43, absent: 2 },
{ name: "품질부", total: 18, present: 18, absent: 0 },
{ name: "관리부", total: 22, present: 20, absent: 2 },
{ name: "영업부", total: 15, present: 14, absent: 1 },
{ name: "연구소", total: 12, present: 11, absent: 1 },
{ name: "구매부", total: 8, present: 8, absent: 0 },
{ name: "IT부", total: 3, present: 3, absent: 0 },
{ name: "재무부", total: 2, present: 2, absent: 0 }
].map((dept) => (
<div key={dept.name} className="p-4 border rounded-2xl bg-gray-50/50">
<h4 className="font-semibold mb-2">{dept.name}</h4>
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span></span>
<span className="font-medium">{dept.total}</span>
</div>
<div className="flex justify-between text-sm">
<span></span>
<span className="font-medium text-green-600">{dept.present}</span>
</div>
<div className="flex justify-between text-sm">
<span></span>
<span className="font-medium text-red-600">{dept.absent}</span>
</div>
</div>
</div>
))}
</div>
</CardContent>
</Card>
</div>
);
}
// 시스템관리자용 인터페이스
return (
<div className="p-6 space-y-6 bg-background min-h-screen">
<div className="flex justify-between items-center">
<div>
<h1 className="text-3xl font-bold text-foreground"> </h1>
<p className="text-muted-foreground mt-1">SAM </p>
</div>
</div>
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-6">
<TabsList className="grid w-full grid-cols-4 lg:grid-cols-8 bg-muted rounded-2xl p-1">
<TabsTrigger value="dashboard" className="rounded-xl">
<Activity className="w-4 h-4 mr-0 lg:mr-2" />
<span className="hidden lg:inline"></span>
</TabsTrigger>
<TabsTrigger value="users" className="rounded-xl">
<Users className="w-4 h-4 mr-0 lg:mr-2" />
<span className="hidden lg:inline"></span>
</TabsTrigger>
<TabsTrigger value="menu-customization" className="rounded-xl">
<Code className="w-4 h-4 mr-0 lg:mr-2" />
<span className="hidden lg:inline"></span>
</TabsTrigger>
<TabsTrigger value="permissions" className="rounded-xl">
<Shield className="w-4 h-4 mr-0 lg:mr-2" />
<span className="hidden lg:inline"></span>
</TabsTrigger>
<TabsTrigger value="system" className="rounded-xl">
<Settings className="w-4 h-4 mr-0 lg:mr-2" />
<span className="hidden lg:inline"></span>
</TabsTrigger>
<TabsTrigger value="database" className="rounded-xl">
<Database className="w-4 h-4 mr-0 lg:mr-2" />
<span className="hidden lg:inline">DB</span>
</TabsTrigger>
<TabsTrigger value="monitoring" className="rounded-xl">
<Monitor className="w-4 h-4 mr-0 lg:mr-2" />
<span className="hidden lg:inline"></span>
</TabsTrigger>
<TabsTrigger value="security" className="rounded-xl">
<Lock className="w-4 h-4 mr-0 lg:mr-2" />
<span className="hidden lg:inline"></span>
</TabsTrigger>
</TabsList>
<TabsContent value="dashboard">
<SystemAdminDashboard />
</TabsContent>
<TabsContent value="users">
<UserManagement />
</TabsContent>
<TabsContent value="menu-customization">
<MenuCustomization />
</TabsContent>
<TabsContent value="permissions">
<PermissionManagement />
</TabsContent>
<TabsContent value="system">
<SystemSettings />
</TabsContent>
<TabsContent value="database">
<DatabaseManagement />
</TabsContent>
<TabsContent value="monitoring">
<SystemMonitoring />
</TabsContent>
<TabsContent value="security">
<SecurityManagement />
</TabsContent>
</Tabs>
</div>
);
}
// 권한 관리 컴포넌트
function PermissionManagement() {
return (
<Card className="samsung-card">
<CardHeader>
<CardTitle className="flex items-center space-x-2">
<Shield className="w-5 h-5" />
<span> </span>
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* 역할별 권한 설정 */}
<div className="space-y-4">
<div className="flex justify-between items-center">
<h3 className="text-lg font-semibold"> </h3>
<Button size="sm">
<Plus className="w-4 h-4 mr-2" />
</Button>
</div>
<div className="space-y-3">
{[
{ role: "CEO", name: "대표이사", permissions: ["전체관리", "승인권한", "시스템설정"], count: 1, color: "bg-purple-500" },
{ role: "SystemAdmin", name: "시스템관리자", permissions: ["시스템관리", "사용자관리", "권한관리"], count: 1, color: "bg-blue-500" },
{ role: "ProductionManager", name: "생산관리자", permissions: ["생산관리", "품질관리", "재고관리"], count: 3, color: "bg-green-500" },
{ role: "QualityManager", name: "품질관리자", permissions: ["품질관리", "검사기록", "품질보고서"], count: 2, color: "bg-orange-500" },
{ role: "Worker", name: "작업자", permissions: ["작업등록", "작업조회"], count: 89, color: "bg-gray-500" }
].map((role) => (
<div key={role.role} className="p-4 border rounded-lg bg-gray-50/50">
<div className="flex justify-between items-start mb-3">
<div>
<div className="flex items-center space-x-2">
<h4 className="font-medium">{role.name}</h4>
<Badge className={`${role.color} text-white`}>
{role.count}
</Badge>
</div>
<p className="text-sm text-muted-foreground mt-1">{role.role}</p>
</div>
<Button size="sm" variant="outline">
<Key className="w-3 h-3 mr-1" />
</Button>
</div>
<div className="space-y-1">
{role.permissions.map((permission) => (
<Badge key={permission} variant="secondary" className="mr-1 mb-1">
{permission}
</Badge>
))}
</div>
</div>
))}
</div>
</div>
{/* 모듈별 접근 권한 */}
<div className="space-y-4">
<div className="flex justify-between items-center">
<h3 className="text-lg font-semibold"> </h3>
<Button size="sm" variant="outline">
<Settings className="w-4 h-4 mr-2" />
</Button>
</div>
<div className="space-y-3">
{[
{ module: "대시보드", roles: ["CEO", "관리자", "매니저"], icon: Activity, color: "border-l-blue-500" },
{ module: "재무관리", roles: ["CEO", "재무담당자"], icon: FileText, color: "border-l-green-500" },
{ module: "운영관리", roles: ["CEO", "생산관리자", "품질관리자"], icon: Settings, color: "border-l-orange-500" },
{ module: "인사관리", roles: ["CEO", "인사담당자"], icon: Users, color: "border-l-purple-500" },
{ module: "품질관리", roles: ["CEO", "품질관리자", "생산관리자"], icon: CheckCircle, color: "border-l-red-500" },
{ module: "승인관리", roles: ["CEO", "관리자"], icon: Shield, color: "border-l-yellow-500" },
{ module: "시스템관리", roles: ["SystemAdmin"], icon: Lock, color: "border-l-indigo-500" }
].map((module) => (
<div key={module.module} className={`p-4 border-l-4 ${module.color} border rounded-lg bg-gray-50/50`}>
<div className="flex justify-between items-center mb-2">
<div className="flex items-center space-x-2">
<module.icon className="w-4 h-4 text-muted-foreground" />
<h4 className="font-medium">{module.module}</h4>
</div>
<Button size="sm" variant="outline"></Button>
</div>
<div className="flex flex-wrap gap-1">
{module.roles.map((role) => (
<Badge key={role} className="bg-blue-500 text-white">
{role}
</Badge>
))}
</div>
</div>
))}
</div>
</div>
</div>
</CardContent>
</Card>
);
}
// 시스템 설정 컴포넌트
function SystemSettings() {
return (
<div className="space-y-6">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card className="samsung-card">
<CardHeader>
<CardTitle className="flex items-center space-x-2">
<Settings className="w-5 h-5" />
<span> </span>
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex justify-between items-center p-3 border rounded-lg">
<div>
<h4 className="font-medium"> </h4>
<p className="text-sm text-muted-foreground"> 2 </p>
</div>
<div className="flex items-center space-x-2">
<CheckCircle className="w-5 h-5 text-green-500" />
<span className="text-sm text-green-600"></span>
</div>
</div>
<div className="flex justify-between items-center p-3 border rounded-lg">
<div>
<h4 className="font-medium"> </h4>
<p className="text-sm text-muted-foreground"> </p>
</div>
<div className="flex items-center space-x-2">
<CheckCircle className="w-5 h-5 text-green-500" />
<span className="text-sm text-green-600"></span>
</div>
</div>
<div className="flex justify-between items-center p-3 border rounded-lg">
<div>
<h4 className="font-medium"> </h4>
<p className="text-sm text-muted-foreground">30 </p>
</div>
<div className="flex items-center space-x-2">
<CheckCircle className="w-5 h-5 text-green-500" />
<span className="text-sm text-green-600"></span>
</div>
</div>
<div className="flex justify-between items-center p-3 border rounded-lg">
<div>
<h4 className="font-medium"> </h4>
<p className="text-sm text-muted-foreground"> </p>
</div>
<div className="flex items-center space-x-2">
<Clock className="w-5 h-5 text-orange-500" />
<span className="text-sm text-orange-600"></span>
</div>
</div>
</CardContent>
</Card>
<Card className="samsung-card">
<CardHeader>
<CardTitle className="flex items-center space-x-2">
<Shield className="w-5 h-5" />
<span> </span>
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex justify-between items-center p-3 border rounded-lg">
<div>
<h4 className="font-medium"> </h4>
<p className="text-sm text-muted-foreground">8 , </p>
</div>
<div className="flex items-center space-x-2">
<CheckCircle className="w-5 h-5 text-green-500" />
<span className="text-sm text-green-600"></span>
</div>
</div>
<div className="flex justify-between items-center p-3 border rounded-lg">
<div>
<h4 className="font-medium"> </h4>
<p className="text-sm text-muted-foreground">5 </p>
</div>
<div className="flex items-center space-x-2">
<CheckCircle className="w-5 h-5 text-green-500" />
<span className="text-sm text-green-600"></span>
</div>
</div>
<div className="flex justify-between items-center p-3 border rounded-lg">
<div>
<h4 className="font-medium"> </h4>
<p className="text-sm text-muted-foreground">8 </p>
</div>
<div className="flex items-center space-x-2">
<CheckCircle className="w-5 h-5 text-green-500" />
<span className="text-sm text-green-600"></span>
</div>
</div>
<div className="flex justify-between items-center p-3 border rounded-lg">
<div>
<h4 className="font-medium">2 </h4>
<p className="text-sm text-muted-foreground"> 2FA </p>
</div>
<div className="flex items-center space-x-2">
<AlertTriangle className="w-5 h-5 text-yellow-500" />
<span className="text-sm text-yellow-600"></span>
</div>
</div>
</CardContent>
</Card>
</div>
{/* 시스템 정보 */}
<Card className="samsung-card">
<CardHeader>
<CardTitle className="flex items-center space-x-2">
<Server className="w-5 h-5" />
<span> </span>
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div className="p-4 border rounded-lg bg-gray-50/50">
<h4 className="font-medium mb-2"></h4>
<div className="space-y-1 text-sm">
<div className="flex justify-between">
<span></span>
<span className="font-medium">v2.1.0</span>
</div>
<div className="flex justify-between">
<span></span>
<span className="font-medium">20241230</span>
</div>
<div className="flex justify-between">
<span></span>
<span className="font-medium">Production</span>
</div>
</div>
</div>
<div className="p-4 border rounded-lg bg-gray-50/50">
<h4 className="font-medium mb-2"></h4>
<div className="space-y-1 text-sm">
<div className="flex justify-between">
<span></span>
<span className="font-medium">PostgreSQL</span>
</div>
<div className="flex justify-between">
<span></span>
<span className="font-medium">15.4</span>
</div>
<div className="flex justify-between">
<span></span>
<span className="font-medium">2.3GB</span>
</div>
</div>
</div>
<div className="p-4 border rounded-lg bg-gray-50/50">
<h4 className="font-medium mb-2"></h4>
<div className="space-y-1 text-sm">
<div className="flex justify-between">
<span>OS</span>
<span className="font-medium">Ubuntu 22.04</span>
</div>
<div className="flex justify-between">
<span>Node.js</span>
<span className="font-medium">v18.17.0</span>
</div>
<div className="flex justify-between">
<span></span>
<span className="font-medium">16GB</span>
</div>
</div>
</div>
<div className="p-4 border rounded-lg bg-gray-50/50">
<h4 className="font-medium mb-2"></h4>
<div className="space-y-1 text-sm">
<div className="flex justify-between">
<span></span>
<span className="font-medium">Enterprise</span>
</div>
<div className="flex justify-between">
<span></span>
<span className="font-medium">2025-12-31</span>
</div>
<div className="flex justify-between">
<span> </span>
<span className="font-medium"></span>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
);
}
// 데이터베이스 관리 컴포넌트
function DatabaseManagement() {
return (
<div className="space-y-6">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card className="samsung-card">
<CardHeader>
<CardTitle className="flex items-center space-x-2">
<Download className="w-5 h-5" />
<span> </span>
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="p-4 border rounded-lg bg-green-50">
<div className="flex justify-between items-center mb-2">
<span className="font-medium"> </span>
<Badge className="bg-green-500 text-white"></Badge>
</div>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span></span>
<span className="font-medium">2024-12-30 02:00</span>
</div>
<div className="flex justify-between">
<span></span>
<span className="font-medium">2.3GB</span>
</div>
<div className="flex justify-between">
<span></span>
<span className="font-medium">3 45</span>
</div>
</div>
</div>
<div className="space-y-2">
<h4 className="font-medium"> </h4>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span> </span>
<span className="font-medium text-green-600"></span>
</div>
<div className="flex justify-between">
<span></span>
<span className="font-medium"> 02:00</span>
</div>
<div className="flex justify-between">
<span> </span>
<span className="font-medium">30</span>
</div>
</div>
</div>
<div className="flex space-x-2 pt-4">
<Button className="flex-1">
<Download className="w-4 h-4 mr-2" />
</Button>
<Button variant="outline" className="flex-1">
<Upload className="w-4 h-4 mr-2" />
</Button>
</div>
</CardContent>
</Card>
<Card className="samsung-card">
<CardHeader>
<CardTitle className="flex items-center space-x-2">
<Activity className="w-5 h-5" />
<span> </span>
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-3">
<div className="flex justify-between items-center p-3 border rounded-lg">
<span className="font-medium"> </span>
<span className="font-medium text-green-600">98.5%</span>
</div>
<div className="flex justify-between items-center p-3 border rounded-lg">
<span className="font-medium"> </span>
<span className="font-medium text-blue-600"></span>
</div>
<div className="flex justify-between items-center p-3 border rounded-lg">
<span className="font-medium"> </span>
<span className="font-medium text-muted-foreground">1 </span>
</div>
<div className="flex justify-between items-center p-3 border rounded-lg">
<span className="font-medium"> </span>
<span className="font-medium text-green-600">94.2%</span>
</div>
</div>
<Button className="w-full mt-4">
<RefreshCw className="w-4 h-4 mr-2" />
</Button>
</CardContent>
</Card>
</div>
{/* 데이터베이스 상태 */}
<Card className="samsung-card">
<CardHeader>
<CardTitle className="flex items-center space-x-2">
<Database className="w-5 h-5" />
<span> </span>
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="space-y-4">
<h4 className="font-medium"> </h4>
<div className="space-y-3">
<div>
<div className="flex justify-between mb-1">
<span className="text-sm"> </span>
<span className="text-sm font-medium">45/100</span>
</div>
<Progress value={45} className="h-2" />
</div>
<div>
<div className="flex justify-between mb-1">
<span className="text-sm"> </span>
<span className="text-sm font-medium">3</span>
</div>
<Progress value={3} className="h-2" />
</div>
</div>
</div>
<div className="space-y-4">
<h4 className="font-medium"></h4>
<div className="space-y-3">
<div>
<div className="flex justify-between mb-1">
<span className="text-sm"> </span>
<span className="text-sm font-medium">2.3GB</span>
</div>
<Progress value={23} className="h-2" />
</div>
<div>
<div className="flex justify-between mb-1">
<span className="text-sm"> </span>
<span className="text-sm font-medium">890MB</span>
</div>
<Progress value={8.9} className="h-2" />
</div>
</div>
</div>
<div className="space-y-4">
<h4 className="font-medium"> </h4>
<div className="space-y-3">
<div>
<div className="flex justify-between mb-1">
<span className="text-sm">/</span>
<span className="text-sm font-medium">1,250</span>
</div>
<Progress value={75} className="h-2" />
</div>
<div>
<div className="flex justify-between mb-1">
<span className="text-sm"></span>
<span className="text-sm font-medium">12ms</span>
</div>
<Progress value={12} className="h-2" />
</div>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
);
}
// 시스템 모니터링 컴포넌트
function SystemMonitoring() {
return (
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<Card className="samsung-card">
<CardHeader>
<CardTitle className="text-center flex flex-col items-center space-y-2">
<Server className="w-8 h-8 text-green-500" />
<span> </span>
</CardTitle>
</CardHeader>
<CardContent className="text-center">
<div className="text-3xl font-bold text-green-600 mb-2"></div>
<p className="text-sm text-muted-foreground mb-4"> </p>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span></span>
<span className="font-medium">28 14</span>
</div>
<div className="flex justify-between">
<span></span>
<span className="font-medium">12ms</span>
</div>
</div>
</CardContent>
</Card>
<Card className="samsung-card">
<CardHeader>
<CardTitle className="text-center flex flex-col items-center space-y-2">
<Wifi className="w-8 h-8 text-blue-500" />
<span></span>
</CardTitle>
</CardHeader>
<CardContent className="text-center">
<div className="text-3xl font-bold text-blue-600 mb-2"></div>
<p className="text-sm text-muted-foreground mb-4">지연시간: 12ms</p>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span></span>
<span className="font-medium">2.3 MB/s</span>
</div>
<div className="flex justify-between">
<span></span>
<span className="font-medium">5.7 MB/s</span>
</div>
</div>
</CardContent>
</Card>
<Card className="samsung-card">
<CardHeader>
<CardTitle className="text-center flex flex-col items-center space-y-2">
<HardDrive className="w-8 h-8 text-orange-500" />
<span></span>
</CardTitle>
</CardHeader>
<CardContent className="text-center">
<div className="text-3xl font-bold text-orange-600 mb-2"></div>
<p className="text-sm text-muted-foreground mb-4">사용률: 78%</p>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span></span>
<span className="font-medium">780GB / 1TB</span>
</div>
<div className="flex justify-between">
<span></span>
<span className="font-medium">220GB</span>
</div>
</div>
</CardContent>
</Card>
</div>
{/* 실시간 리소스 모니터링 */}
<Card className="samsung-card">
<CardHeader>
<CardTitle className="flex items-center space-x-2">
<Activity className="w-5 h-5" />
<span> </span>
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<div className="space-y-3">
<h4 className="font-medium flex items-center space-x-2">
<div className="w-3 h-3 bg-blue-500 rounded-full"></div>
<span>CPU </span>
</h4>
<div className="text-2xl font-bold">45%</div>
<Progress value={45} className="h-3" />
<p className="text-sm text-muted-foreground">평균: 32%</p>
</div>
<div className="space-y-3">
<h4 className="font-medium flex items-center space-x-2">
<div className="w-3 h-3 bg-green-500 rounded-full"></div>
<span> </span>
</h4>
<div className="text-2xl font-bold">62%</div>
<Progress value={62} className="h-3" />
<p className="text-sm text-muted-foreground">10.2GB / 16GB</p>
</div>
<div className="space-y-3">
<h4 className="font-medium flex items-center space-x-2">
<div className="w-3 h-3 bg-orange-500 rounded-full"></div>
<span> I/O</span>
</h4>
<div className="text-2xl font-bold">23%</div>
<Progress value={23} className="h-3" />
<p className="text-sm text-muted-foreground">읽기: 45MB/s</p>
</div>
<div className="space-y-3">
<h4 className="font-medium flex items-center space-x-2">
<div className="w-3 h-3 bg-purple-500 rounded-full"></div>
<span></span>
</h4>
<div className="text-2xl font-bold">12%</div>
<Progress value={12} className="h-3" />
<p className="text-sm text-muted-foreground">5.7MB/s</p>
</div>
</div>
</CardContent>
</Card>
</div>
);
}
// 보안 관리 컴포넌트
function SecurityManagement() {
return (
<div className="space-y-6">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card className="samsung-card">
<CardHeader>
<CardTitle className="flex items-center space-x-2">
<Shield className="w-5 h-5" />
<span> </span>
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="p-4 border rounded-lg bg-green-50">
<div className="flex justify-between items-center mb-2">
<span className="font-medium"> </span>
<Badge className="bg-green-500 text-white"></Badge>
</div>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span> </span>
<span className="font-medium">12 ()</span>
</div>
<div className="flex justify-between">
<span> </span>
<span className="font-medium">3</span>
</div>
<div className="flex justify-between">
<span> </span>
<span className="font-medium">2 </span>
</div>
</div>
</div>
<Button className="w-full">
<Shield className="w-4 h-4 mr-2" />
</Button>
</CardContent>
</Card>
<Card className="samsung-card">
<CardHeader>
<CardTitle className="flex items-center space-x-2">
<Lock className="w-5 h-5" />
<span> </span>
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-3">
<div className="flex justify-between items-center p-3 border rounded-lg">
<span className="font-medium"> </span>
<span className="font-medium text-blue-600">89</span>
</div>
<div className="flex justify-between items-center p-3 border rounded-lg">
<span className="font-medium"> </span>
<span className="font-medium text-green-600">0</span>
</div>
<div className="flex justify-between items-center p-3 border rounded-lg">
<span className="font-medium"> IP</span>
<span className="font-medium text-orange-600">3</span>
</div>
<div className="flex justify-between items-center p-3 border rounded-lg">
<span className="font-medium"> </span>
<span className="font-medium text-red-600">5</span>
</div>
</div>
<Button variant="outline" className="w-full">
<Eye className="w-4 h-4 mr-2" />
</Button>
</CardContent>
</Card>
</div>
{/* 보안 이벤트 로그 */}
<Card className="samsung-card">
<CardHeader>
<CardTitle className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<FileText className="w-5 h-5" />
<span> </span>
</div>
<Button size="sm" variant="outline">
<Filter className="w-4 h-4 mr-2" />
</Button>
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-3">
{[
{ time: "09:15", event: "성공적인 로그인", user: "김대표", ip: "192.168.1.100", severity: "정보", color: "border-l-blue-500 bg-blue-50" },
{ time: "09:12", event: "비정상 접근 차단", user: "Unknown", ip: "203.142.15.23", severity: "경고", color: "border-l-orange-500 bg-orange-50" },
{ time: "08:45", event: "권한 변경", user: "최시스템", ip: "192.168.1.105", severity: "정보", color: "border-l-blue-500 bg-blue-50" },
{ time: "08:30", event: "다중 로그인 실패", user: "이생산", ip: "192.168.1.110", severity: "주의", color: "border-l-yellow-500 bg-yellow-50" },
{ time: "08:15", event: "백업 완료", user: "System", ip: "Local", severity: "정보", color: "border-l-green-500 bg-green-50" }
].map((event, index) => (
<div key={index} className={`p-4 border-l-4 ${event.color} border rounded-lg`}>
<div className="flex justify-between items-start">
<div className="flex-1">
<div className="flex items-center space-x-2 mb-1">
<span className="font-medium">{event.event}</span>
<Badge className={`text-xs ${
event.severity === '정보' ? 'bg-blue-500 text-white' :
event.severity === '주의' ? 'bg-yellow-500 text-white' :
event.severity === '경고' ? 'bg-orange-500 text-white' : 'bg-red-500 text-white'
}`}>
{event.severity}
</Badge>
</div>
<div className="text-sm text-muted-foreground">
: {event.user} | IP: {event.ip}
</div>
</div>
<div className="text-sm text-muted-foreground flex items-center">
<Clock className="w-3 h-3 mr-1" />
{event.time}
</div>
</div>
</div>
))}
</div>
</CardContent>
</Card>
</div>
);
}

View File

@@ -1,511 +0,0 @@
import { useState } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
Users,
UserPlus,
UserMinus,
Search,
Edit,
Trash2,
Eye,
EyeOff,
Shield,
Lock,
Unlock,
Calendar,
Mail,
Phone,
Building,
UserCheck,
UserX,
Filter,
Download,
RefreshCw
} from "lucide-react";
export function UserManagement() {
const [searchTerm, setSearchTerm] = useState("");
const [selectedDepartment, setSelectedDepartment] = useState("all");
const [selectedRole, setSelectedRole] = useState("all");
const [selectedStatus, setSelectedStatus] = useState("all");
const [isAddUserOpen, setIsAddUserOpen] = useState(false);
// 사용자 데이터
const users = [
{
id: 1,
name: "김대표",
email: "ceo@company.com",
phone: "010-1234-5678",
department: "경영진",
position: "대표이사",
role: "CEO",
status: "활성",
lastLogin: "2024-12-30 09:15",
createdAt: "2024-01-01",
permissions: ["전체관리", "승인권한", "시스템설정"]
},
{
id: 2,
name: "이생산",
email: "production@company.com",
phone: "010-2345-6789",
department: "생산부",
position: "생산관리자",
role: "ProductionManager",
status: "활성",
lastLogin: "2024-12-30 08:45",
createdAt: "2024-01-15",
permissions: ["생산관리", "품질관리", "재고관리"]
},
{
id: 3,
name: "박작업",
email: "worker@company.com",
phone: "010-3456-7890",
department: "생산부",
position: "생산작업자",
role: "Worker",
status: "활성",
lastLogin: "2024-12-30 07:30",
createdAt: "2024-02-01",
permissions: ["작업등록", "작업조회"]
},
{
id: 4,
name: "최시스템",
email: "sysadmin@company.com",
phone: "010-4567-8901",
department: "IT부",
position: "시스템관리자",
role: "SystemAdmin",
status: "활성",
lastLogin: "2024-12-30 09:00",
createdAt: "2024-01-01",
permissions: ["시스템관리", "사용자관리", "권한관리", "보안관리"]
},
{
id: 5,
name: "정품질",
email: "quality@company.com",
phone: "010-5678-9012",
department: "품질부",
position: "품질관리자",
role: "QualityManager",
status: "활성",
lastLogin: "2024-12-29 18:20",
createdAt: "2024-01-20",
permissions: ["품질관리", "검사기록", "품질보고서"]
},
{
id: 6,
name: "송구매",
email: "purchase@company.com",
phone: "010-6789-0123",
department: "구매부",
position: "구매담당자",
role: "PurchaseStaff",
status: "비활성",
lastLogin: "2024-12-28 17:45",
createdAt: "2024-03-01",
permissions: ["구매관리", "발주관리"]
},
{
id: 7,
name: "한영업",
email: "sales@company.com",
phone: "010-7890-1234",
department: "영업부",
position: "영업담당자",
role: "SalesStaff",
status: "활성",
lastLogin: "2024-12-30 08:15",
createdAt: "2024-02-15",
permissions: ["고객관리", "주문관리", "견적관리"]
},
{
id: 8,
name: "김회계",
email: "accounting@company.com",
phone: "010-8901-2345",
department: "재무부",
position: "회계담당자",
role: "AccountingStaff",
status: "활성",
lastLogin: "2024-12-30 09:30",
createdAt: "2024-01-10",
permissions: ["재무관리", "회계처리", "예산관리"]
}
];
// 부서 목록
const departments = ["전체", "경영진", "생산부", "품질부", "구매부", "영업부", "재무부", "IT부"];
// 역할 목록
const roles = [
{ value: "all", label: "전체" },
{ value: "CEO", label: "대표이사" },
{ value: "ProductionManager", label: "생산관리자" },
{ value: "QualityManager", label: "품질관리자" },
{ value: "Worker", label: "작업자" },
{ value: "SystemAdmin", label: "시스템관리자" },
{ value: "PurchaseStaff", label: "구매담당자" },
{ value: "SalesStaff", label: "영업담당자" },
{ value: "AccountingStaff", label: "회계담당자" }
];
// 필터링된 사용자 목록
const filteredUsers = users.filter(user => {
const matchesSearch = user.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
user.email.toLowerCase().includes(searchTerm.toLowerCase()) ||
user.department.toLowerCase().includes(searchTerm.toLowerCase());
const matchesDepartment = selectedDepartment === "all" || user.department === selectedDepartment;
const matchesRole = selectedRole === "all" || user.role === selectedRole;
const matchesStatus = selectedStatus === "all" || user.status === selectedStatus;
return matchesSearch && matchesDepartment && matchesRole && matchesStatus;
});
// 사용자 통계
const userStats = {
total: users.length,
active: users.filter(u => u.status === "활성").length,
inactive: users.filter(u => u.status === "비활성").length,
newThisMonth: users.filter(u => new Date(u.createdAt).getMonth() === new Date().getMonth()).length
};
const getStatusBadge = (status: string) => {
return status === "활성"
? <Badge className="bg-green-500 text-white"></Badge>
: <Badge className="bg-gray-500 text-white"></Badge>;
};
const getRoleBadge = (role: string) => {
const roleColors: { [key: string]: string } = {
"CEO": "bg-purple-500 text-white",
"SystemAdmin": "bg-blue-500 text-white",
"ProductionManager": "bg-green-500 text-white",
"QualityManager": "bg-orange-500 text-white",
"Worker": "bg-gray-500 text-white",
"PurchaseStaff": "bg-cyan-500 text-white",
"SalesStaff": "bg-pink-500 text-white",
"AccountingStaff": "bg-indigo-500 text-white"
};
const roleLabels: { [key: string]: string } = {
"CEO": "대표이사",
"SystemAdmin": "시스템관리자",
"ProductionManager": "생산관리자",
"QualityManager": "품질관리자",
"Worker": "작업자",
"PurchaseStaff": "구매담당자",
"SalesStaff": "영업담당자",
"AccountingStaff": "회계담당자"
};
return <Badge className={roleColors[role] || "bg-gray-500 text-white"}>
{roleLabels[role] || role}
</Badge>;
};
return (
<div className="p-6 space-y-6 bg-background min-h-screen">
{/* 헤더 */}
<div className="flex justify-between items-center">
<div>
<h1 className="text-3xl font-bold text-foreground"> </h1>
<p className="text-muted-foreground mt-1"> </p>
</div>
<div className="flex space-x-3">
<Dialog open={isAddUserOpen} onOpenChange={setIsAddUserOpen}>
<DialogTrigger asChild>
<Button className="samsung-button">
<UserPlus className="w-4 h-4 mr-2" />
</Button>
</DialogTrigger>
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle> </DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="name"></Label>
<Input id="name" placeholder="사용자 이름" />
</div>
<div className="space-y-2">
<Label htmlFor="email"></Label>
<Input id="email" type="email" placeholder="email@company.com" />
</div>
<div className="space-y-2">
<Label htmlFor="phone"></Label>
<Input id="phone" placeholder="010-0000-0000" />
</div>
<div className="space-y-2">
<Label htmlFor="department"></Label>
<Select>
<SelectTrigger>
<SelectValue placeholder="부서 선택" />
</SelectTrigger>
<SelectContent>
{departments.slice(1).map(dept => (
<SelectItem key={dept} value={dept}>{dept}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="role"></Label>
<Select>
<SelectTrigger>
<SelectValue placeholder="역할 선택" />
</SelectTrigger>
<SelectContent>
{roles.slice(1).map(role => (
<SelectItem key={role.value} value={role.value}>{role.label}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="password"> </Label>
<Input id="password" type="password" placeholder="임시 비밀번호" />
</div>
<div className="flex space-x-2 pt-4">
<Button className="flex-1" onClick={() => setIsAddUserOpen(false)}>
</Button>
<Button variant="outline" className="flex-1" onClick={() => setIsAddUserOpen(false)}>
</Button>
</div>
</div>
</DialogContent>
</Dialog>
<Button variant="outline">
<Download className="w-4 h-4 mr-2" />
</Button>
</div>
</div>
{/* 사용자 통계 */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
<Card className="samsung-card">
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-muted-foreground"> </p>
<p className="text-2xl font-bold text-primary">{userStats.total}</p>
</div>
<Users className="h-8 w-8 text-muted-foreground" />
</div>
</CardContent>
</Card>
<Card className="samsung-card">
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-muted-foreground"> </p>
<p className="text-2xl font-bold text-green-600">{userStats.active}</p>
</div>
<UserCheck className="h-8 w-8 text-green-500" />
</div>
</CardContent>
</Card>
<Card className="samsung-card">
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-muted-foreground"> </p>
<p className="text-2xl font-bold text-gray-600">{userStats.inactive}</p>
</div>
<UserX className="h-8 w-8 text-gray-500" />
</div>
</CardContent>
</Card>
<Card className="samsung-card">
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-muted-foreground"> </p>
<p className="text-2xl font-bold text-blue-600">{userStats.newThisMonth}</p>
</div>
<UserPlus className="h-8 w-8 text-blue-500" />
</div>
</CardContent>
</Card>
</div>
{/* 필터 및 검색 */}
<Card className="samsung-card">
<CardContent className="p-6">
<div className="flex flex-wrap gap-4 items-end">
<div className="flex-1 min-w-64">
<Label htmlFor="search" className="text-sm font-medium"></Label>
<div className="relative mt-1">
<Search className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
<Input
id="search"
placeholder="이름, 이메일, 부서로 검색..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
</div>
<div className="w-40">
<Label className="text-sm font-medium"></Label>
<Select value={selectedDepartment} onValueChange={setSelectedDepartment}>
<SelectTrigger className="mt-1">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
{departments.slice(1).map(dept => (
<SelectItem key={dept} value={dept}>{dept}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="w-40">
<Label className="text-sm font-medium"></Label>
<Select value={selectedRole} onValueChange={setSelectedRole}>
<SelectTrigger className="mt-1">
<SelectValue />
</SelectTrigger>
<SelectContent>
{roles.map(role => (
<SelectItem key={role.value} value={role.value}>{role.label}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="w-32">
<Label className="text-sm font-medium"></Label>
<Select value={selectedStatus} onValueChange={setSelectedStatus}>
<SelectTrigger className="mt-1">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="활성"></SelectItem>
<SelectItem value="비활성"></SelectItem>
</SelectContent>
</Select>
</div>
<Button variant="outline" className="px-3">
<RefreshCw className="h-4 w-4" />
</Button>
</div>
</CardContent>
</Card>
{/* 사용자 목록 */}
<Card className="samsung-card">
<CardHeader>
<CardTitle className="flex items-center justify-between">
<span> ({filteredUsers.length})</span>
<div className="flex items-center space-x-2 text-sm text-muted-foreground">
<Filter className="w-4 h-4" />
<span> </span>
</div>
</CardTitle>
</CardHeader>
<CardContent>
<div className="rounded-lg border overflow-hidden">
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead>/</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead> </TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredUsers.map((user) => (
<TableRow key={user.id}>
<TableCell>
<div className="flex items-center space-x-3">
<div className="w-10 h-10 bg-gradient-to-br from-primary/20 to-primary/40 rounded-full flex items-center justify-center">
<span className="text-primary font-semibold text-sm">
{user.name.charAt(0)}
</span>
</div>
<div>
<p className="font-medium">{user.name}</p>
<p className="text-sm text-muted-foreground">{user.email}</p>
</div>
</div>
</TableCell>
<TableCell>
<div className="space-y-1">
<div className="flex items-center text-sm">
<Mail className="w-3 h-3 mr-1 text-muted-foreground" />
{user.email}
</div>
<div className="flex items-center text-sm">
<Phone className="w-3 h-3 mr-1 text-muted-foreground" />
{user.phone}
</div>
</div>
</TableCell>
<TableCell>
<div className="space-y-1">
<div className="flex items-center text-sm">
<Building className="w-3 h-3 mr-1 text-muted-foreground" />
{user.department}
</div>
<p className="text-sm text-muted-foreground">{user.position}</p>
</div>
</TableCell>
<TableCell>
{getRoleBadge(user.role)}
</TableCell>
<TableCell>
{getStatusBadge(user.status)}
</TableCell>
<TableCell>
<div className="flex items-center text-sm">
<Calendar className="w-3 h-3 mr-1 text-muted-foreground" />
{user.lastLogin}
</div>
</TableCell>
<TableCell>
<div className="flex items-center space-x-2">
<Button size="sm" variant="outline" className="px-2">
<Edit className="w-3 h-3" />
</Button>
<Button size="sm" variant="outline" className="px-2">
<Shield className="w-3 h-3" />
</Button>
<Button size="sm" variant="outline" className="px-2">
{user.status === "활성" ? <Lock className="w-3 h-3" /> : <Unlock className="w-3 h-3" />}
</Button>
<Button size="sm" variant="outline" className="px-2 text-red-600 hover:text-red-700">
<Trash2 className="w-3 h-3" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
</div>
);
}

View File

@@ -1,172 +0,0 @@
import { useMemo } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { useCurrentTime } from "@/hooks/useCurrentTime";
import {
CheckCircle,
Clock,
Shield,
Package,
AlertTriangle,
Factory,
Activity,
FileText,
Settings
} from "lucide-react";
export function WorkerDashboard() {
const currentTime = useCurrentTime();
const workerData = useMemo(() => {
return {
myTasks: [
{ id: "W001", product: "스마트폰 케이스", quantity: 150, deadline: "14:00", status: "진행중" },
{ id: "W002", product: "태블릿 스탠드", quantity: 80, deadline: "16:30", status: "대기" }
],
currentShift: "1교대",
workTime: "08:00-17:00",
todayProduction: 120,
targetProduction: 150,
safetyAlerts: 0,
equipment: {
machine1: "정상",
machine2: "점검필요"
},
qualityChecks: 12
};
}, []);
return (
<div className="p-4 md:p-6 space-y-4 md:space-y-6">
{/* 작업자 헤더 */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<h1 className="text-2xl md:text-3xl font-bold text-foreground"> </h1>
<p className="text-muted-foreground mt-1">{workerData.currentShift} · {workerData.workTime} · {currentTime}</p>
</div>
<div className="flex space-x-2">
<Button variant="outline" size="sm">
<Shield className="h-4 w-4 mr-2" />
</Button>
<Button className="bg-blue-600 hover:bg-blue-700" size="sm">
<CheckCircle className="h-4 w-4 mr-2" />
</Button>
</div>
</div>
{/* 개인 실적 */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 md:gap-6">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"> </CardTitle>
<Factory className="h-4 w-4 text-blue-600" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-blue-600">
{workerData.todayProduction}
</div>
<p className="text-xs text-muted-foreground">
: {workerData.targetProduction} ({Math.round((workerData.todayProduction / workerData.targetProduction) * 100)}%)
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"> </CardTitle>
<CheckCircle className="h-4 w-4 text-green-600" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-green-600">
{workerData.qualityChecks}
</div>
<p className="text-xs text-muted-foreground">
불량률: 0%
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"> </CardTitle>
<Shield className="h-4 w-4 text-green-600" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-green-600">
</div>
<p className="text-xs text-muted-foreground">
: {workerData.safetyAlerts}
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"> </CardTitle>
<Activity className="h-4 w-4 text-purple-600" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-purple-600">
{Math.round((workerData.todayProduction / workerData.targetProduction) * 100)}%
</div>
<p className="text-xs text-muted-foreground">
: {workerData.targetProduction - workerData.todayProduction}
</p>
</CardContent>
</Card>
</div>
{/* 개인 작업 현황 */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card>
<CardHeader>
<CardTitle className="flex items-center space-x-2">
<FileText className="h-5 w-5" />
<span> </span>
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-3">
{workerData.myTasks.map((task, index) => (
<div key={index} className="flex justify-between items-center p-3 bg-muted/50 dark:bg-muted/20 rounded">
<div>
<p className="font-medium">{task.product}</p>
<p className="text-sm text-muted-foreground">: {task.quantity} | : {task.deadline}</p>
</div>
<Badge className={task.status === "진행중" ? "bg-blue-500 text-white" : "bg-muted text-muted-foreground"}>
{task.status}
</Badge>
</div>
))}
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="flex items-center space-x-2">
<Settings className="h-5 w-5" />
<span> </span>
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-3">
<div className="flex justify-between items-center p-3 bg-green-50 rounded">
<span className="font-medium"> #1</span>
<Badge className="bg-green-500 text-white">{workerData.equipment.machine1}</Badge>
</div>
<div className="flex justify-between items-center p-3 bg-yellow-50 rounded">
<span className="font-medium"> #2</span>
<Badge className="bg-yellow-500 text-white">{workerData.equipment.machine2}</Badge>
</div>
</div>
</CardContent>
</Card>
</div>
</div>
);
}

View File

@@ -1,301 +0,0 @@
import { useState } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import {
ClipboardList,
Play,
Pause,
CheckCircle,
AlertTriangle,
Clock,
Target,
TrendingUp,
Package,
Calendar,
User
} from "lucide-react";
import { Progress } from "@/components/ui/progress";
export function WorkerPerformance() {
const [workStatus, setWorkStatus] = useState<"idle" | "working" | "paused">("idle");
const [currentTime, setCurrentTime] = useState("00:00:00");
// 금일 작업 지시 데이터
const todayTasks = [
{
id: "WO-2024-001",
product: "방화셔터 3000×3000",
quantity: 2,
priority: "긴급",
deadline: "14:00",
status: "진행중",
progress: 60,
startTime: "09:00"
},
{
id: "WO-2024-002",
product: "일반셔터 2500×2500",
quantity: 3,
priority: "보통",
deadline: "17:00",
status: "대기",
progress: 0,
startTime: null
},
{
id: "WO-2024-003",
product: "특수셔터 4000×3500",
quantity: 1,
priority: "긴급",
deadline: "16:00",
status: "대기",
progress: 0,
startTime: null
}
];
// 완료 작업 데이터
const completedTasks = [
{
id: "WO-2024-000",
product: "방화셔터 2800×2800",
quantity: 2,
completedTime: "08:45",
qualityCheck: "합격"
}
];
const handleStartWork = (taskId: string) => {
setWorkStatus("working");
console.log("작업 시작:", taskId);
};
const handlePauseWork = () => {
setWorkStatus("paused");
};
const handleCompleteWork = (taskId: string) => {
setWorkStatus("idle");
console.log("작업 완료:", taskId);
};
return (
<div className="p-4 md:p-6 space-y-6">
{/* 헤더 */}
<div className="bg-card border border-border/20 rounded-xl p-6">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<h1 className="text-3xl font-bold text-foreground mb-2"> </h1>
<p className="text-muted-foreground"> </p>
</div>
<div className="flex items-center space-x-3">
<div className="text-right">
<p className="text-sm text-muted-foreground"></p>
<p className="font-bold text-foreground"></p>
</div>
<div className="w-12 h-12 bg-primary/10 rounded-xl flex items-center justify-center">
<User className="h-6 w-6 text-primary" />
</div>
</div>
</div>
</div>
{/* 작업 현황 카드 */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<Card className="border border-border/20">
<CardHeader className="pb-3">
<CardTitle className="text-sm text-muted-foreground"> </CardTitle>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-primary">{todayTasks.length}</div>
<p className="text-xs text-muted-foreground mt-1"></p>
</CardContent>
</Card>
<Card className="border border-border/20">
<CardHeader className="pb-3">
<CardTitle className="text-sm text-muted-foreground"></CardTitle>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-blue-600">
{todayTasks.filter(t => t.status === "진행중").length}
</div>
<p className="text-xs text-muted-foreground mt-1"></p>
</CardContent>
</Card>
<Card className="border border-border/20">
<CardHeader className="pb-3">
<CardTitle className="text-sm text-muted-foreground"></CardTitle>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-green-600">{completedTasks.length}</div>
<p className="text-xs text-muted-foreground mt-1"></p>
</CardContent>
</Card>
<Card className="border border-border/20">
<CardHeader className="pb-3">
<CardTitle className="text-sm text-muted-foreground"> </CardTitle>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-orange-600">75%</div>
<p className="text-xs text-muted-foreground mt-1"></p>
</CardContent>
</Card>
</div>
{/* 금일 작업 지시 */}
<Card className="border border-border/20">
<CardHeader>
<CardTitle className="flex items-center space-x-3">
<ClipboardList className="h-6 w-6 text-primary" />
<span> </span>
<Badge className="bg-blue-500 text-white">{todayTasks.length}</Badge>
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
{todayTasks.map((task, index) => (
<div
key={index}
className={`p-4 rounded-xl border-2 transition-all ${
task.status === "진행중"
? "border-blue-500 bg-blue-50 dark:bg-blue-950/20"
: "border-border/50 bg-card"
}`}
>
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div className="flex-1">
<div className="flex items-center space-x-3 mb-2">
<Badge
className={`${
task.priority === "긴급"
? "bg-red-500"
: task.priority === "높음"
? "bg-orange-500"
: "bg-blue-500"
} text-white`}
>
{task.priority}
</Badge>
<span className="font-bold text-foreground">{task.id}</span>
<Badge
variant="outline"
className={
task.status === "진행중"
? "border-blue-500 text-blue-600"
: "border-gray-500 text-gray-600"
}
>
{task.status}
</Badge>
</div>
<h3 className="font-bold text-lg text-foreground mb-1">{task.product}</h3>
<div className="flex items-center space-x-4 text-sm text-muted-foreground">
<div className="flex items-center space-x-1">
<Package className="h-4 w-4" />
<span>: {task.quantity}</span>
</div>
<div className="flex items-center space-x-1">
<Clock className="h-4 w-4" />
<span>: {task.deadline}</span>
</div>
{task.startTime && (
<div className="flex items-center space-x-1">
<Play className="h-4 w-4" />
<span>: {task.startTime}</span>
</div>
)}
</div>
{task.status === "진행중" && (
<div className="mt-3">
<div className="flex justify-between text-sm mb-1">
<span className="text-muted-foreground"></span>
<span className="font-bold text-blue-600">{task.progress}%</span>
</div>
<Progress value={task.progress} className="h-2" />
</div>
)}
</div>
<div className="flex flex-col space-y-2 md:w-48">
{task.status === "대기" && (
<Button
className="bg-blue-600 hover:bg-blue-700 text-white"
onClick={() => handleStartWork(task.id)}
>
<Play className="h-4 w-4 mr-2" />
</Button>
)}
{task.status === "진행중" && (
<>
<Button
variant="outline"
className="border-orange-500 text-orange-600"
onClick={handlePauseWork}
>
<Pause className="h-4 w-4 mr-2" />
</Button>
<Button
className="bg-green-600 hover:bg-green-700 text-white"
onClick={() => handleCompleteWork(task.id)}
>
<CheckCircle className="h-4 w-4 mr-2" />
</Button>
</>
)}
</div>
</div>
</div>
))}
</div>
</CardContent>
</Card>
{/* 금일 완료 작업 */}
<Card className="border border-border/20">
<CardHeader>
<CardTitle className="flex items-center space-x-3">
<CheckCircle className="h-6 w-6 text-green-600" />
<span> </span>
<Badge className="bg-green-500 text-white">{completedTasks.length}</Badge>
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-3">
{completedTasks.map((task, index) => (
<div
key={index}
className="p-4 bg-green-50 dark:bg-green-950/20 rounded-xl border border-green-200 dark:border-green-800"
>
<div className="flex items-center justify-between">
<div>
<div className="flex items-center space-x-2 mb-1">
<span className="font-bold text-foreground">{task.id}</span>
<Badge className="bg-green-600 text-white"></Badge>
<Badge
variant="outline"
className="border-green-600 text-green-600"
>
{task.qualityCheck}
</Badge>
</div>
<h3 className="font-bold text-foreground">{task.product}</h3>
<p className="text-sm text-muted-foreground">
: {task.quantity} · : {task.completedTime}
</p>
</div>
<CheckCircle className="h-8 w-8 text-green-600" />
</div>
</div>
))}
</div>
</CardContent>
</Card>
</div>
);
}

View File

@@ -21,17 +21,26 @@ export function useAuthGuard() {
const router = useRouter();
useEffect(() => {
console.log('🔄 useAuthGuard: Starting auth check...');
// 페이지 로드 시 인증 확인
const checkAuth = async () => {
try {
console.log('📡 Fetching /api/auth/check...');
// 🔵 Next.js 내부 API - 쿠키에서 토큰 확인 (PHP 호출 X, 성능 최적화)
const response = await fetch('/api/auth/check', {
method: 'GET',
cache: 'no-store',
});
console.log('📥 Response status:', response.status);
if (!response.ok) {
// 인증 실패 시 로그인 페이지로 이동
console.log('⚠️ 인증 실패: 로그인 페이지로 이동');
router.replace('/login');
} else {
console.log('✅ 인증 성공');
}
} catch (error) {
console.error('❌ 인증 확인 오류:', error);
@@ -56,5 +65,5 @@ export function useAuthGuard() {
return () => {
window.removeEventListener('pageshow', handlePageShow);
};
}, [router]);
}, []);
}

View File

@@ -51,7 +51,9 @@ export default function DashboardLayout({ children }: DashboardLayoutProps) {
// 서버에서 받은 사용자 정보로 초기화
useEffect(() => {
if (!_hasHydrated) return;
// ⚠️ Allow rendering even before hydration (Zustand persist rehydration can be slow)
// Commenting out the hydration check prevents infinite loading spinner
// if (!_hasHydrated) return;
// localStorage에서 사용자 정보 가져오기
const userDataStr = localStorage.getItem("user");
@@ -151,17 +153,11 @@ export default function DashboardLayout({ children }: DashboardLayoutProps) {
}
};
// hydration 완료 및 menuItems 설정 대기
if (!_hasHydrated || menuItems.length === 0) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="text-center">
<div className="inline-block h-8 w-8 animate-spin rounded-full border-4 border-solid border-primary border-r-transparent mb-4"></div>
<p className="text-muted-foreground">Loading...</p>
</div>
</div>
);
}
// ⚠️ FIXED: Removed hydration check to prevent infinite loading spinner
// The hydration check was causing the dashboard to show a loading spinner indefinitely
// because Zustand persist rehydration was taking too long or not completing properly.
// By removing this check, we allow the component to render immediately with default values
// and update once hydration completes through the useEffect above.
return (
<div className="min-h-screen flex w-full p-3 gap-3">
@@ -232,7 +228,7 @@ export default function DashboardLayout({ children }: DashboardLayoutProps) {
<div className="flex items-center space-x-6">
{/* 테마 선택 */}
<ThemeSelect />
<ThemeSelect native={false} />
{/* 유저 프로필 */}
<div className="flex items-center space-x-4 pl-6 border-l border-border/30">

View File

@@ -38,6 +38,6 @@
],
"exclude": [
"node_modules",
"src/components/business"
"src/components/_unused"
]
}