From 46aff1a6a2e76f2a6c895a03526494ccf69f4427 Mon Sep 17 00:00:00 2001 From: byeongcheolryu Date: Wed, 12 Nov 2025 18:09:12 +0900 Subject: [PATCH] =?UTF-8?q?[feat]:=20Shadcn=20UI=20=EB=AA=A8=EB=8B=AC=20Se?= =?UTF-8?q?lect=20=EB=A0=88=EC=9D=B4=EC=95=84=EC=9B=83=20=EC=8B=9C?= =?UTF-8?q?=ED=94=84=ED=8A=B8=20=EB=B0=A9=EC=A7=80=20=EB=B0=8F=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 주요 변경사항: - 테마/언어 선택을 모달 스타일로 변경 (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 --- .gitignore | 3 + eslint.config.mjs | 2 +- .../[locale]/(protected)/[...slug]/page.tsx | 91 +- src/app/api/auth/check/route.ts | 46 +- src/app/api/auth/login/route.ts | 28 +- src/app/api/auth/logout/route.ts | 26 +- src/app/api/auth/refresh/route.ts | 36 +- src/app/api/auth/signup/route.ts | 32 +- src/app/globals.css | 20 + src/components/LanguageSwitcher.tsx | 45 - src/components/NavigationMenu.tsx | 43 - src/components/WelcomeMessage.tsx | 20 - src/components/auth/LoginPage.tsx | 12 +- src/components/auth/SignupPage.tsx | 10 +- .../business/AccountingManagement.tsx | 437 -- .../business/ApprovalManagement.tsx | 1527 ----- src/components/business/BOMManagement.tsx | 1029 ---- src/components/business/Board.tsx | 656 -- src/components/business/CodeManagement.tsx | 659 -- src/components/business/ContactModal.tsx | 202 - src/components/business/Dashboard.tsx | 69 +- src/components/business/DemoRequestPage.tsx | 280 - src/components/business/DrawingCanvas.tsx | 340 -- .../business/EquipmentManagement.tsx | 1043 ---- src/components/business/HRManagement.tsx | 836 --- src/components/business/ItemManagement.tsx | 1848 ------ src/components/business/LandingPage.tsx | 527 -- src/components/business/LoginPage.tsx | 258 - src/components/business/LotManagement.tsx | 370 -- .../{CEODashboard.tsx => MainDashboard.tsx} | 17 +- src/components/business/MasterData.tsx | 1559 ----- .../business/MaterialManagement.tsx | 1624 ----- src/components/business/MenuCustomization.tsx | 1509 ----- .../business/MenuCustomizationGuide.tsx | 112 - src/components/business/OrderManagement.tsx | 622 -- src/components/business/PricingManagement.tsx | 1794 ------ src/components/business/ProductManagement.tsx | 980 --- .../business/ProductionManagement.tsx | 5409 ----------------- .../business/ProductionManagerDashboard.tsx | 266 - src/components/business/QualityManagement.tsx | 2507 -------- src/components/business/QuoteCreation.tsx | 4023 ------------ src/components/business/QuoteSimulation.tsx | 370 -- src/components/business/ReceivingWrite.tsx | 350 -- src/components/business/Reports.tsx | 510 -- .../business/SalesLeadDashboard.tsx | 663 -- .../business/SalesManagement-clean.tsx | 52 - src/components/business/SalesManagement.tsx | 94 - .../business/ShippingManagement.tsx | 1370 ----- src/components/business/SignupPage.tsx | 524 -- .../business/SystemAdminDashboard.tsx | 573 -- src/components/business/SystemManagement.tsx | 985 --- src/components/business/UserManagement.tsx | 511 -- src/components/business/WorkerDashboard.tsx | 172 - src/components/business/WorkerPerformance.tsx | 301 - src/hooks/useAuthGuard.ts | 11 +- src/layouts/DashboardLayout.tsx | 22 +- tsconfig.json | 2 +- 57 files changed, 307 insertions(+), 37120 deletions(-) delete mode 100644 src/components/LanguageSwitcher.tsx delete mode 100644 src/components/NavigationMenu.tsx delete mode 100644 src/components/WelcomeMessage.tsx delete mode 100644 src/components/business/AccountingManagement.tsx delete mode 100644 src/components/business/ApprovalManagement.tsx delete mode 100644 src/components/business/BOMManagement.tsx delete mode 100644 src/components/business/Board.tsx delete mode 100644 src/components/business/CodeManagement.tsx delete mode 100644 src/components/business/ContactModal.tsx delete mode 100644 src/components/business/DemoRequestPage.tsx delete mode 100644 src/components/business/DrawingCanvas.tsx delete mode 100644 src/components/business/EquipmentManagement.tsx delete mode 100644 src/components/business/HRManagement.tsx delete mode 100644 src/components/business/ItemManagement.tsx delete mode 100644 src/components/business/LandingPage.tsx delete mode 100644 src/components/business/LoginPage.tsx delete mode 100644 src/components/business/LotManagement.tsx rename src/components/business/{CEODashboard.tsx => MainDashboard.tsx} (99%) delete mode 100644 src/components/business/MasterData.tsx delete mode 100644 src/components/business/MaterialManagement.tsx delete mode 100644 src/components/business/MenuCustomization.tsx delete mode 100644 src/components/business/MenuCustomizationGuide.tsx delete mode 100644 src/components/business/OrderManagement.tsx delete mode 100644 src/components/business/PricingManagement.tsx delete mode 100644 src/components/business/ProductManagement.tsx delete mode 100644 src/components/business/ProductionManagement.tsx delete mode 100644 src/components/business/ProductionManagerDashboard.tsx delete mode 100644 src/components/business/QualityManagement.tsx delete mode 100644 src/components/business/QuoteCreation.tsx delete mode 100644 src/components/business/QuoteSimulation.tsx delete mode 100644 src/components/business/ReceivingWrite.tsx delete mode 100644 src/components/business/Reports.tsx delete mode 100644 src/components/business/SalesLeadDashboard.tsx delete mode 100644 src/components/business/SalesManagement-clean.tsx delete mode 100644 src/components/business/SalesManagement.tsx delete mode 100644 src/components/business/ShippingManagement.tsx delete mode 100644 src/components/business/SignupPage.tsx delete mode 100644 src/components/business/SystemAdminDashboard.tsx delete mode 100644 src/components/business/SystemManagement.tsx delete mode 100644 src/components/business/UserManagement.tsx delete mode 100644 src/components/business/WorkerDashboard.tsx delete mode 100644 src/components/business/WorkerPerformance.tsx diff --git a/.gitignore b/.gitignore index a1b7a050..97ee9b0b 100644 --- a/.gitignore +++ b/.gitignore @@ -99,3 +99,6 @@ build/ claudedocs/ .env.local .env*.local + +# ---> Unused components (archived) +src/components/_unused/ diff --git a/eslint.config.mjs b/eslint.config.mjs index 247f4fd4..093890ea 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -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 ], }, diff --git a/src/app/[locale]/(protected)/[...slug]/page.tsx b/src/app/[locale]/(protected)/[...slug]/page.tsx index e4b41d00..a5b12478 100644 --- a/src/app/[locale]/(protected)/[...slug]/page.tsx +++ b/src/app/[locale]/(protected)/[...slug]/page.tsx @@ -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(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 ( +
+
Loading...
+
+ ); + } + + // 메뉴에 없는 경로 → 404 + if (!isValidPath) { + notFound(); + } + + // 메뉴에 있지만 구현되지 않은 페이지 → EmptyPage return ( { - // Get the pathname without the current locale - const pathnameWithoutLocale = pathname.replace(`/${locale}`, ''); - - // Navigate to the new locale - router.push(`/${newLocale}${pathnameWithoutLocale}`); - }; - - return ( -
- {locales.map((loc) => ( - - ))} -
- ); -} \ No newline at end of file diff --git a/src/components/NavigationMenu.tsx b/src/components/NavigationMenu.tsx deleted file mode 100644 index 7e49e4d8..00000000 --- a/src/components/NavigationMenu.tsx +++ /dev/null @@ -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 ( - - ); -} \ No newline at end of file diff --git a/src/components/WelcomeMessage.tsx b/src/components/WelcomeMessage.tsx deleted file mode 100644 index 2da703e2..00000000 --- a/src/components/WelcomeMessage.tsx +++ /dev/null @@ -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 ( -
-

{t('welcome')}

-

{t('appName')}

-
- ); -} \ No newline at end of file diff --git a/src/components/auth/LoginPage.tsx b/src/components/auth/LoginPage.tsx index bb48d059..1384b618 100644 --- a/src/components/auth/LoginPage.tsx +++ b/src/components/auth/LoginPage.tsx @@ -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() {
- - + + diff --git a/src/components/auth/SignupPage.tsx b/src/components/auth/SignupPage.tsx index 5b1bd2dd..3a9d2ae5 100644 --- a/src/components/auth/SignupPage.tsx +++ b/src/components/auth/SignupPage.tsx @@ -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() {
- - + + diff --git a/src/components/business/AccountingManagement.tsx b/src/components/business/AccountingManagement.tsx deleted file mode 100644 index 83b8e501..00000000 --- a/src/components/business/AccountingManagement.tsx +++ /dev/null @@ -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 ( -
- {/* 헤더 */} -
-
-
-

회계 관리

-

매출/매입, 미수금, 원가 분석 및 손익 현황

-
-
- - -
-
-
- - {/* 주요 지표 */} -
- - - - 당월 매출 - - - - -
650M원
-
- - 전월 대비 +12.1% -
-
-
- - - - - 당월 매입 - - - - -
380M원
-
- - 전월 대비 +11.8% -
-
-
- - - - - 당월 순이익 - - - - -
270M원
-
- 이익률: 41.5% -
-
-
- - - - - 총 미수금 - - - - -
148M원
-
- 30일 초과: 70M원 -
-
-
-
- - {/* 탭 메뉴 */} - - - 매출/매입 - 미수금 관리 - 원가 분석 - 손익 현황 - 입출금 - - - {/* 매출/매입 관리 */} - - - - - - 매출/매입 추이 - - - -
- - - - - - - - - - - -
-
-
-
- - {/* 미수금 관리 */} - - - - - - 거래처별 미수금 현황 - - - - - - - 거래처 - 미수금액 - 경과일수 - 상태 - 관리 - - - - {receivables.map((item, index) => ( - - {item.company} - - {item.amount.toLocaleString()}원 - - {item.days}일 - - - {item.status} - - - - - - - ))} - -
-
-
-
- - {/* 원가 분석 */} - -
- - - - - 원가 구성 비율 - - - -
- - - - - {costComposition.map((entry, index) => ( - - ))} - - - -
-
- {costComposition.map((item, index) => ( -
-
-
- {item.name} -
- {item.value}% -
- ))} -
- - - - - - - - 건별 원가 분석 - - - -
- {costAnalysis.slice(0, 3).map((item, index) => ( -
-
-
- {item.orderNo} -

{item.product}

-
- = 25 - ? "bg-green-500 text-white" - : item.profitRate >= 20 - ? "bg-blue-500 text-white" - : "bg-yellow-500 text-white" - } - > - {item.profitRate}% - -
-
-
- 매출: - - {(item.salesAmount / 1000000).toFixed(1)}M - -
-
- 원가: - - {(item.totalCost / 1000000).toFixed(1)}M - -
-
- 이익: - - {(item.profit / 1000000).toFixed(1)}M - -
-
-
- ))} -
-
-
-
- - - {/* 손익 현황 */} - - - - - - 월별 손익 현황 - - - -
- - - - - - - - - -
-
-
-
- - {/* 입출금 내역 */} - - - - - - 거래처 입출금 내역 - - - -
-
-
-
- 입금 -

삼성전자

-

2024-10-13 14:30

-
-
-

+25,000,000원

-

제품 출하대금

-
-
-
-
-
-
- 출금 -

포스코

-

2024-10-13 11:20

-
-
-

-15,000,000원

-

원자재 구매비

-
-
-
-
-
-
-
- -
- ); -} diff --git a/src/components/business/ApprovalManagement.tsx b/src/components/business/ApprovalManagement.tsx deleted file mode 100644 index a575906f..00000000 --- a/src/components/business/ApprovalManagement.tsx +++ /dev/null @@ -1,1527 +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 { - FileText, - Plus, - Search, - Download, - Send, - CheckCircle, - Clock, - XCircle, - Eye, - Printer, - User, - DollarSign, - ShoppingCart, - Plane, - FileSignature, - ArrowRight, - ArrowLeft, - TrendingUp, - AlertCircle, - Users -} from "lucide-react"; - -export function ApprovalManagement() { - const [activeTab, setActiveTab] = useState("pending"); - const [isNewDocumentOpen, setIsNewDocumentOpen] = useState(false); - const [documentType, setDocumentType] = useState(""); - const [documentStep, setDocumentStep] = useState(1); - const [selectedDocument, setSelectedDocument] = useState(null); - const [isViewDocumentOpen, setIsViewDocumentOpen] = useState(false); - const [isPrintMode, setIsPrintMode] = useState(false); - const [approvalComment, setApprovalComment] = useState(""); - const [approvalTemplates, setApprovalTemplates] = useState([ - { - name: "기본 결재선", - approvers: [ - { level: 1, name: "박팀장", position: "팀장", status: "대기" }, - { level: 2, name: "김부장", position: "부장", status: "대기" }, - { level: 3, name: "김대표", position: "대표이사", status: "대기" } - ] - }, - { - name: "간단 결재선 (팀장-대표)", - approvers: [ - { level: 1, name: "박팀장", position: "팀장", status: "대기" }, - { level: 2, name: "김대표", position: "대표이사", status: "대기" } - ] - }, - { - name: "고액 결재선", - approvers: [ - { level: 1, name: "박팀장", position: "팀장", status: "대기" }, - { level: 2, name: "한차장", position: "차장", status: "대기" }, - { level: 3, name: "김부장", position: "부장", status: "대기" }, - { level: 4, name: "김대표", position: "대표이사", status: "대기" } - ] - } - ]); - - // 문서 양식 데이터 - const [formData, setFormData] = useState({ - // 공통 정보 - drafter: "이생산", - department: "생산부", - position: "과장", - draftDate: new Date().toISOString().split('T')[0], - title: "", - - // 지출결의서 - expenseType: "", - expenseAmount: 0, - expenseDate: "", - expenseVendor: "", - expenseAccount: "", - expenseReason: "", - - // 품의서 - proposalSubject: "", - proposalBackground: "", - proposalContent: "", - proposalExpectedEffect: "", - proposalBudget: 0, - - // 휴가신청서 - leaveType: "", - leaveStartDate: "", - leaveEndDate: "", - leaveDays: 0, - leaveReason: "", - leaveEmergencyContact: "", - - // 구매요청서 - purchaseItem: "", - purchaseQuantity: 0, - purchaseUnitPrice: 0, - purchaseTotalAmount: 0, - purchaseVendor: "", - purchaseDeliveryDate: "", - purchasePurpose: "", - - // 결재선 - approvers: [ - { level: 1, name: "박팀장", position: "팀장", status: "대기" }, - { level: 2, name: "김부장", position: "부장", status: "대기" }, - { level: 3, name: "김대표", position: "대표이사", status: "대기" } - ] - }); - - // 결재 문서 목록 - const documents = [ - { - id: "DOC-2024-001", - type: "지출결의서", - title: "사무용품 구매 지출", - drafter: "이생산", - drafterPosition: "과장", - department: "생산부", - draftDate: "2024-10-10", - amount: 350000, - status: "승인대기", - currentApprover: "박팀장", - approvalLine: [ - { name: "박팀장", position: "팀장", status: "대기", date: "", comment: "" }, - { name: "김부장", position: "부장", status: "대기", date: "", comment: "" }, - { name: "김대표", position: "대표이사", status: "대기", date: "", comment: "" } - ], - urgency: "보통", - details: { - expenseType: "사무용품", - expenseDate: "2024-10-09", - expenseVendor: "ABC문구", - expenseAccount: "소모품비", - expenseReason: "프린터 토너 및 복사용지 구매 필요" - } - }, - { - id: "DOC-2024-002", - type: "휴가신청서", - title: "연차 사용 신청", - drafter: "정설비", - drafterPosition: "사원", - department: "설비부", - draftDate: "2024-10-11", - amount: 0, - status: "승인완료", - currentApprover: "-", - approvalLine: [ - { name: "박팀장", position: "팀장", status: "승인", date: "2024-10-11", comment: "승인합니다" }, - { name: "김대표", position: "대표이사", status: "승인", date: "2024-10-11", comment: "승인" } - ], - urgency: "보통", - details: { - leaveType: "연차", - leaveStartDate: "2024-10-15", - leaveEndDate: "2024-10-16", - leaveDays: 2, - leaveReason: "가족 여행", - leaveEmergencyContact: "010-4444-4444" - } - }, - { - id: "DOC-2024-003", - type: "품의서", - title: "신규 설비 도입 건", - drafter: "박품질", - drafterPosition: "대리", - department: "품질부", - draftDate: "2024-10-09", - amount: 15000000, - status: "진행중", - currentApprover: "김부장", - approvalLine: [ - { name: "박팀장", position: "팀장", status: "승인", date: "2024-10-09", comment: "품질 향상에 필요" }, - { name: "김부장", position: "부장", status: "대기", date: "", comment: "" }, - { name: "김대표", position: "대표이사", status: "대기", date: "", comment: "" } - ], - urgency: "긴급", - details: { - proposalSubject: "자동 검사 설비 도입", - proposalBackground: "현재 수작업 검사로 인한 시간 지연 및 인적 오류 발생", - proposalContent: "AI 기반 자동 검사 설비를 도입하여 검사 시간을 50% 단축하고 정확도를 99.5%로 향상", - proposalExpectedEffect: "검사 시간 단축, 불량률 감소, 인건비 절감", - proposalBudget: 15000000 - } - }, - { - id: "DOC-2024-004", - type: "구매요청서", - title: "원자재 긴급 구매", - drafter: "최자재", - drafterPosition: "주임", - department: "자재부", - draftDate: "2024-10-08", - amount: 2500000, - status: "반려", - currentApprover: "-", - approvalLine: [ - { name: "박팀장", position: "팀장", status: "반려", date: "2024-10-08", comment: "재고 확인 후 재신청 요망" } - ], - urgency: "긴급", - details: { - purchaseItem: "알루미늄 가이드레일", - purchaseQuantity: 500, - purchaseUnitPrice: 5000, - purchaseTotalAmount: 2500000, - purchaseVendor: "알텍솔루션", - purchaseDeliveryDate: "2024-10-15", - purchasePurpose: "생산 계획 대비 재고 부족" - } - }, - ]; - - const handleApprove = (docId: string) => { - console.log("문서 승인:", docId, "의견:", approvalComment); - alert("문서가 승인되었습니다."); - setIsViewDocumentOpen(false); - setApprovalComment(""); - }; - - const handleReject = (docId: string) => { - if (!approvalComment.trim()) { - alert("반려 사유를 입력해주세요."); - return; - } - console.log("문서 반려:", docId, "사유:", approvalComment); - alert("문서가 반려되었습니다."); - setIsViewDocumentOpen(false); - setApprovalComment(""); - }; - - const handlePrint = () => { - setIsPrintMode(true); - setTimeout(() => { - window.print(); - setIsPrintMode(false); - }, 100); - }; - - const getStatusBadge = (status: string) => { - const statusConfig: Record = { - "승인대기": { className: "bg-yellow-500 text-white" }, - "진행중": { className: "bg-blue-500 text-white" }, - "승인완료": { className: "bg-green-500 text-white" }, - "반려": { className: "bg-red-500 text-white" }, - }; - - const config = statusConfig[status] || { className: "bg-gray-500 text-white" }; - - return ( - - {status} - - ); - }; - - const getUrgencyBadge = (urgency: string) => { - const urgencyConfig: Record = { - "긴급": "bg-red-100 text-red-800 border-red-300", - "보통": "bg-blue-100 text-blue-800 border-blue-300", - "낮음": "bg-gray-100 text-gray-800 border-gray-300", - }; - - return ( - - {urgency} - - ); - }; - - const documentTypes = [ - { value: "expense", label: "지출결의서", icon: DollarSign }, - { value: "proposal", label: "품의서", icon: FileSignature }, - { value: "leave", label: "휴가신청서", icon: Plane }, - { value: "purchase", label: "구매요청서", icon: ShoppingCart }, - ]; - - // 결재 가능한 임직원 목록 - const availableApprovers = [ - { name: "김대표", position: "대표이사", department: "경영진" }, - { name: "김부장", position: "부장", department: "생산부" }, - { name: "박부장", position: "부장", department: "품질부" }, - { name: "이부장", position: "부장", department: "영업부" }, - { name: "최부장", position: "부장", department: "지원부" }, - { name: "박팀장", position: "팀장", department: "생산1팀" }, - { name: "강팀장", position: "팀장", department: "생산2팀" }, - { name: "오팀장", position: "팀장", department: "품질팀" }, - { name: "정팀장", position: "팀장", department: "영업팀" }, - { name: "윤팀장", position: "팀장", department: "인사팀" }, - { name: "한차장", position: "차장", department: "생산부" }, - { name: "송차장", position: "차장", department: "품질부" }, - ]; - - const handleNextStep = () => { - setDocumentStep(prev => Math.min(prev + 1, 3)); - }; - - const handlePrevStep = () => { - setDocumentStep(prev => Math.max(prev - 1, 1)); - }; - - const handleSubmit = () => { - console.log("결재 문서 제출:", formData); - setIsNewDocumentOpen(false); - setDocumentStep(1); - setDocumentType(""); - }; - - return ( -
- {/* 헤더 */} -
-
-
-

전자결재

-

결재 문서 작성 및 승인 관리

-
-
- - - - - - - - - - 결재 문서 작성 - - - 문서 종류를 선택하고 내용을 작성하세요. - - - - {/* 진행 단계 */} -
-
= 1 ? 'text-primary' : 'text-muted-foreground'}`}> -
= 1 ? 'bg-primary text-white' : 'bg-muted'}`}> - 1 -
- 문서선택 -
-
-
= 2 ? 'text-primary' : 'text-muted-foreground'}`}> -
= 2 ? 'bg-primary text-white' : 'bg-muted'}`}> - 2 -
- 내용작성 -
-
-
= 3 ? 'text-primary' : 'text-muted-foreground'}`}> -
= 3 ? 'bg-primary text-white' : 'bg-muted'}`}> - 3 -
- 결재선 -
-
- - {/* Step 1: 문서 종류 선택 */} - {documentStep === 1 && ( -
-

문서 종류를 선택하세요

-
- {documentTypes.map((type) => { - const Icon = type.icon; - return ( -
{ - setDocumentType(type.value); - handleNextStep(); - }} - className={`cursor-pointer p-6 border-2 rounded-lg transition-all hover:shadow-lg ${ - documentType === type.value - ? 'border-primary bg-primary/5' - : 'border-border hover:border-primary/50' - }`} - > - -

{type.label}

-
- ); - })} -
-
- )} - - {/* Step 2: 내용 작성 */} - {documentStep === 2 && ( -
- {/* 기본 정보 */} -
-

기본 정보

-
-
- - -
-
- - -
-
- - -
-
- - -
-
-
- - {/* 지출결의서 */} - {documentType === "expense" && ( -
-

지출 내역

-
- - setFormData({...formData, title: e.target.value})} - /> -
-
-
- - -
-
- - setFormData({...formData, expenseAmount: parseInt(e.target.value) || 0})} - /> -
-
- - setFormData({...formData, expenseDate: e.target.value})} - /> -
-
- - setFormData({...formData, expenseVendor: e.target.value})} - /> -
-
-
- - -
-
- -