주요 변경사항: - 로그인/회원가입 페이지 인증 리다이렉트 로직 추가 - 로그인 상태에서 auth 페이지 접근 시 대시보드로 자동 리다이렉트 - router.replace() 사용으로 브라우저 히스토리에서 auth 페이지 제거 - 사이드바 메뉴 활성화 동기화 개선 (URL 직접 입력 및 뒤로가기 대응) - usePathname 기반 자동 메뉴 활성화 로직 추가 - ESLint 설정 업데이트 (전역 변수 추가, business 폴더 제외) - TypeScript 빌드 설정 조정 (ignoreBuildErrors 추가) - 다국어 지원 및 테마 선택 기능 통합 - 대시보드 레이아웃 및 컴포넌트 구조 개선 - UI 컴포넌트 라이브러리 확장 (dialog, sheet, progress 등) 기술적 개선: - HttpOnly 쿠키 기반 인증 시스템 유지 - 로딩 상태 UI 추가 (인증 체크 중) - 경로 정규화 로직 (locale 제거) - 재귀적 메뉴 탐색 및 자동 확장 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
1559 lines
71 KiB
TypeScript
1559 lines
71 KiB
TypeScript
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 { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "@/components/ui/alert-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, Package, Zap, Users, Building, FileText } from "lucide-react";
|
|
|
|
interface Product {
|
|
id: string;
|
|
code: string;
|
|
name: string;
|
|
type: string;
|
|
unit: string;
|
|
standardTime: number;
|
|
materialCost: number;
|
|
laborCost: number;
|
|
description: string;
|
|
status: string;
|
|
}
|
|
|
|
interface BOM {
|
|
id: string;
|
|
productCode: string;
|
|
productName: string;
|
|
materialCode: string;
|
|
materialName: string;
|
|
quantity: number;
|
|
unit: string;
|
|
notes: string;
|
|
}
|
|
|
|
interface Process {
|
|
id: string;
|
|
code: string;
|
|
name: string;
|
|
workCenter: string;
|
|
standardTime: number;
|
|
setupTime: number;
|
|
description: string;
|
|
nextProcess?: string;
|
|
}
|
|
|
|
interface Customer {
|
|
id: string;
|
|
code: string;
|
|
name: string;
|
|
type: "고객" | "공급업체" | "양방향";
|
|
contact: string;
|
|
phone: string;
|
|
address: string;
|
|
status: string;
|
|
}
|
|
|
|
export function MasterData() {
|
|
const [products, setProducts] = useState<Product[]>([
|
|
{ id: "P001", code: "PRD-001", name: "스마트폰 케이스", type: "완제품", unit: "개", standardTime: 30, materialCost: 5000, laborCost: 2000, description: "iPhone 호환 케이스", status: "사용" },
|
|
{ id: "P002", code: "PRD-002", name: "태블릿 스탠드", type: "완제품", unit: "개", standardTime: 45, materialCost: 8000, laborCost: 3000, description: "각도 조절 가능", status: "사용" },
|
|
{ id: "P003", code: "PRD-003", name: "무선 충전기", type: "완제품", unit: "개", standardTime: 60, materialCost: 15000, laborCost: 5000, description: "고속 무선 충전", status: "사용" },
|
|
]);
|
|
|
|
const [bomData, setBomData] = useState<BOM[]>([
|
|
{ id: "BOM001", productCode: "PRD-001", productName: "스마트폰 케이스", materialCode: "MAT-001", materialName: "플라스틱 원료 A", quantity: 0.05, unit: "kg", notes: "주요 소재" },
|
|
{ id: "BOM002", productCode: "PRD-001", productName: "스마트폰 케이스", materialCode: "MAT-003", materialName: "실리콘 패드", quantity: 2, unit: "개", notes: "충격 흡수용" },
|
|
{ id: "BOM003", productCode: "PRD-002", productName: "태블릿 스탠드", materialCode: "MAT-002", materialName: "알루미늄 판재", quantity: 1, unit: "장", notes: "메인 프레임" },
|
|
{ id: "BOM004", productCode: "PRD-003", productName: "무선 충전기", materialCode: "MAT-005", materialName: "전자부품 모듈", quantity: 1, unit: "개", notes: "핵심 부품" },
|
|
]);
|
|
|
|
const [processes, setProcesses] = useState<Process[]>([
|
|
{ id: "PROC001", code: "PROC-001", name: "사출성형", workCenter: "성형라인 1", standardTime: 15, setupTime: 30, description: "플라스틱 사출성형 공정" },
|
|
{ id: "PROC002", code: "PROC-002", name: "조립", workCenter: "조립라인 1", standardTime: 10, setupTime: 15, description: "부품 조립 공정", nextProcess: "PROC-003" },
|
|
{ id: "PROC003", code: "PROC-003", name: "검사", workCenter: "품질검사실", standardTime: 5, setupTime: 5, description: "품질 검사 공정" },
|
|
{ id: "PROC004", code: "PROC-004", name: "포장", workCenter: "포장라인", standardTime: 3, setupTime: 5, description: "최종 포장 공정" },
|
|
]);
|
|
|
|
const [customers, setCustomers] = useState<Customer[]>([
|
|
{ id: "C001", code: "CUST-001", name: "삼성전자", type: "고객", contact: "김구매", phone: "02-1234-5678", address: "서울시 강남구", status: "활성" },
|
|
{ id: "C002", code: "SUPP-001", name: "ABC 원료", type: "공급업체", contact: "이공급", phone: "031-234-5678", address: "경기도 성남시", status: "활성" },
|
|
{ id: "C003", code: "CUST-002", name: "LG전자", type: "고객", contact: "박주문", phone: "02-3456-7890", address: "서울시 영등포구", status: "활성" },
|
|
{ id: "C004", code: "BOTH-001", name: "현대모비스", type: "양방향", contact: "최거래", phone: "031-456-7890", address: "경기도 용인시", status: "활성" },
|
|
]);
|
|
|
|
const [activeTab, setActiveTab] = useState("products");
|
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
const [isViewModalOpen, setIsViewModalOpen] = useState(false);
|
|
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
|
const [selectedItem, setSelectedItem] = useState<any>(null);
|
|
const [formData, setFormData] = useState<any>({});
|
|
|
|
const handleCreate = () => {
|
|
switch (activeTab) {
|
|
case "products":
|
|
const newProduct: Product = {
|
|
id: `P${String(products.length + 1).padStart(3, '0')}`,
|
|
code: formData.code || "",
|
|
name: formData.name || "",
|
|
type: formData.type || "",
|
|
unit: formData.unit || "",
|
|
standardTime: formData.standardTime || 0,
|
|
materialCost: formData.materialCost || 0,
|
|
laborCost: formData.laborCost || 0,
|
|
description: formData.description || "",
|
|
status: "사용",
|
|
};
|
|
setProducts([...products, newProduct]);
|
|
break;
|
|
|
|
case "bom":
|
|
const newBOM: BOM = {
|
|
id: `BOM${String(bomData.length + 1).padStart(3, '0')}`,
|
|
productCode: formData.productCode || "",
|
|
productName: products.find(p => p.code === formData.productCode)?.name || "",
|
|
materialCode: formData.materialCode || "",
|
|
materialName: formData.materialName || "",
|
|
quantity: formData.quantity || 0,
|
|
unit: formData.unit || "",
|
|
notes: formData.notes || "",
|
|
};
|
|
setBomData([...bomData, newBOM]);
|
|
break;
|
|
|
|
case "processes":
|
|
const newProcess: Process = {
|
|
id: `PROC${String(processes.length + 1).padStart(3, '0')}`,
|
|
code: formData.code || "",
|
|
name: formData.name || "",
|
|
workCenter: formData.workCenter || "",
|
|
standardTime: formData.standardTime || 0,
|
|
setupTime: formData.setupTime || 0,
|
|
description: formData.description || "",
|
|
nextProcess: formData.nextProcess || "",
|
|
};
|
|
setProcesses([...processes, newProcess]);
|
|
break;
|
|
|
|
case "customers":
|
|
const newCustomer: Customer = {
|
|
id: `C${String(customers.length + 1).padStart(3, '0')}`,
|
|
code: formData.code || "",
|
|
name: formData.name || "",
|
|
type: formData.type || "고객",
|
|
contact: formData.contact || "",
|
|
phone: formData.phone || "",
|
|
address: formData.address || "",
|
|
status: "활성",
|
|
};
|
|
setCustomers([...customers, newCustomer]);
|
|
break;
|
|
}
|
|
|
|
setFormData({});
|
|
setIsModalOpen(false);
|
|
toast.success("등록이 완료되었습니다.");
|
|
};
|
|
|
|
const handleUpdate = () => {
|
|
if (!selectedItem) return;
|
|
|
|
switch (activeTab) {
|
|
case "products":
|
|
const updatedProducts = products.map(item =>
|
|
item.id === selectedItem.id ? { ...item, ...formData } : item
|
|
);
|
|
setProducts(updatedProducts);
|
|
break;
|
|
|
|
case "bom":
|
|
const updatedBOM = bomData.map(item =>
|
|
item.id === selectedItem.id ? { ...item, ...formData } : item
|
|
);
|
|
setBomData(updatedBOM);
|
|
break;
|
|
|
|
case "processes":
|
|
const updatedProcesses = processes.map(item =>
|
|
item.id === selectedItem.id ? { ...item, ...formData } : item
|
|
);
|
|
setProcesses(updatedProcesses);
|
|
break;
|
|
|
|
case "customers":
|
|
const updatedCustomers = customers.map(item =>
|
|
item.id === selectedItem.id ? { ...item, ...formData } : item
|
|
);
|
|
setCustomers(updatedCustomers);
|
|
break;
|
|
}
|
|
|
|
setFormData({});
|
|
setSelectedItem(null);
|
|
setIsEditModalOpen(false);
|
|
toast.success("수정이 완료되었습니다.");
|
|
};
|
|
|
|
const handleDelete = (id: string) => {
|
|
switch (activeTab) {
|
|
case "products":
|
|
setProducts(products.filter(item => item.id !== id));
|
|
break;
|
|
case "bom":
|
|
setBomData(bomData.filter(item => item.id !== id));
|
|
break;
|
|
case "processes":
|
|
setProcesses(processes.filter(item => item.id !== id));
|
|
break;
|
|
case "customers":
|
|
setCustomers(customers.filter(item => item.id !== id));
|
|
break;
|
|
}
|
|
toast.success("삭제가 완료되었습니다.");
|
|
};
|
|
|
|
const getStatusColor = (status: string) => {
|
|
switch (status) {
|
|
case "사용":
|
|
case "활성": return "bg-green-500";
|
|
case "중단":
|
|
case "비활성": return "bg-red-500";
|
|
default: return "bg-gray-500";
|
|
}
|
|
};
|
|
|
|
const getTypeColor = (type: string) => {
|
|
switch (type) {
|
|
case "고객": return "text-blue-600";
|
|
case "공급업체": return "text-green-600";
|
|
case "양방향": return "text-purple-600";
|
|
default: return "text-gray-600";
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="p-3 sm:p-4 lg:p-6 space-y-4 lg:space-y-6">
|
|
{/* 모바일 우선 헤더 */}
|
|
<div className="flex flex-col space-y-3 sm:flex-row sm:items-center sm:justify-between sm:space-y-0">
|
|
<div>
|
|
<h1 className="text-xl sm:text-2xl lg:text-3xl font-bold text-gray-900">기준정보 관리</h1>
|
|
<p className="text-sm sm:text-base text-gray-600 mt-1">제품, BOM, 공정, 거래처 등 기준 정보 관리</p>
|
|
</div>
|
|
<Button
|
|
className="samsung-button w-full sm:w-auto min-h-[48px] touch-manipulation"
|
|
onClick={() => setIsModalOpen(true)}
|
|
>
|
|
<Plus className="h-5 w-5 mr-3" />
|
|
신규 등록
|
|
</Button>
|
|
</div>
|
|
|
|
{/* 기준정보 대시보드 - 모바일 2열, 태블릿 2열, 데스크톱 4열 */}
|
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 sm:gap-6 mb-6 sm:mb-8">
|
|
<Card className="samsung-card samsung-gradient-card border-0 hover:scale-105 hover:-translate-y-1 transition-all duration-300 group">
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-3">
|
|
<CardTitle className="text-xs sm:text-sm font-bold text-muted-foreground uppercase tracking-wide">제품 마스터</CardTitle>
|
|
<div className="w-10 h-10 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">
|
|
<Package className="h-5 w-5 text-white" />
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-xl sm:text-2xl font-bold">{products.length}개</div>
|
|
<p className="text-xs text-muted-foreground">
|
|
등록된 제품 수
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card className="samsung-card samsung-gradient-card border-0 hover:scale-105 hover:-translate-y-1 transition-all duration-300 group">
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-3">
|
|
<CardTitle className="text-xs sm:text-sm font-bold text-muted-foreground uppercase tracking-wide">BOM 구성</CardTitle>
|
|
<div className="w-10 h-10 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-5 w-5 text-white" />
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-xl sm:text-2xl font-bold">{bomData.length}건</div>
|
|
<p className="text-xs text-muted-foreground">
|
|
BOM 구성 정보
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card className="samsung-card samsung-gradient-card border-0 hover:scale-105 hover:-translate-y-1 transition-all duration-300 group">
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-3">
|
|
<CardTitle className="text-xs sm:text-sm font-bold text-muted-foreground uppercase tracking-wide">공정 마스터</CardTitle>
|
|
<div className="w-10 h-10 bg-gradient-to-br from-purple-500 to-violet-600 rounded-2xl flex items-center justify-center samsung-shadow group-hover:scale-110 transition-transform duration-300">
|
|
<Zap className="h-5 w-5 text-white" />
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-xl sm:text-2xl font-bold">{processes.length}개</div>
|
|
<p className="text-xs text-muted-foreground">
|
|
등록된 공정 수
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card className="samsung-card samsung-gradient-card border-0 hover:scale-105 hover:-translate-y-1 transition-all duration-300 group">
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-3">
|
|
<CardTitle className="text-xs sm:text-sm font-bold text-muted-foreground uppercase tracking-wide">거래처</CardTitle>
|
|
<div className="w-10 h-10 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">
|
|
<Building className="h-5 w-5 text-white" />
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-xl sm:text-2xl font-bold">{customers.length}개</div>
|
|
<p className="text-xs text-muted-foreground">
|
|
등록된 거래처 수
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* 기준정보 현황 - 모바일 1열, 데스크톱 2열 */}
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 sm:gap-6 mb-4 sm:mb-6">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center space-x-2 text-sm sm:text-base">
|
|
<Package className="h-4 w-4 sm:h-5 sm:w-5" />
|
|
<span>제품별 BOM 구성 현황</span>
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="space-y-3">
|
|
{products.slice(0, 3).map((product) => {
|
|
const bomCount = bomData.filter(bom => bom.productCode === product.code).length;
|
|
return (
|
|
<div key={product.id} className="flex justify-between items-center p-3 border rounded-lg hover:bg-gray-50 transition-colors">
|
|
<div className="min-w-0 flex-1">
|
|
<h4 className="font-medium text-sm truncate">{product.name}</h4>
|
|
<p className="text-xs text-gray-600">{product.code}</p>
|
|
</div>
|
|
<div className="text-right ml-2 flex-shrink-0">
|
|
<p className="text-sm font-medium">{bomCount}개 자재</p>
|
|
<p className="text-xs text-gray-600">{product.type}</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center space-x-2 text-sm sm:text-base">
|
|
<Building className="h-4 w-4 sm:h-5 sm:w-5" />
|
|
<span>거래처 유형별 현황</span>
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="space-y-3">
|
|
{["고객", "공급업체", "양방향"].map((type) => {
|
|
const count = customers.filter(c => c.type === type).length;
|
|
return (
|
|
<div key={type} className="flex justify-between items-center p-3 border rounded-lg hover:bg-gray-50 transition-colors">
|
|
<div>
|
|
<h4 className="font-medium text-sm">{type}</h4>
|
|
<p className="text-xs text-gray-600">거래처 유형</p>
|
|
</div>
|
|
<div className="text-right">
|
|
<p className="text-sm font-medium">{count}개</p>
|
|
<p className={`text-xs ${getTypeColor(type)}`}>{type}</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* 탭 메뉴 - 모바일 스크롤 */}
|
|
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-4">
|
|
<div className="overflow-x-auto">
|
|
<TabsList className="grid w-full grid-cols-4 min-w-[320px] h-auto">
|
|
<TabsTrigger
|
|
value="products"
|
|
className="flex flex-col sm:flex-row items-center space-y-1 sm:space-y-0 sm:space-x-2 p-2 sm:p-3 min-h-[44px] touch-manipulation"
|
|
>
|
|
<Package className="h-4 w-4" />
|
|
<span className="text-xs sm:text-sm">제품</span>
|
|
</TabsTrigger>
|
|
<TabsTrigger
|
|
value="bom"
|
|
className="flex flex-col sm:flex-row items-center space-y-1 sm:space-y-0 sm:space-x-2 p-2 sm:p-3 min-h-[44px] touch-manipulation"
|
|
>
|
|
<FileText className="h-4 w-4" />
|
|
<span className="text-xs sm:text-sm">BOM</span>
|
|
</TabsTrigger>
|
|
<TabsTrigger
|
|
value="processes"
|
|
className="flex flex-col sm:flex-row items-center space-y-1 sm:space-y-0 sm:space-x-2 p-2 sm:p-3 min-h-[44px] touch-manipulation"
|
|
>
|
|
<Zap className="h-4 w-4" />
|
|
<span className="text-xs sm:text-sm">공정</span>
|
|
</TabsTrigger>
|
|
<TabsTrigger
|
|
value="customers"
|
|
className="flex flex-col sm:flex-row items-center space-y-1 sm:space-y-0 sm:space-x-2 p-2 sm:p-3 min-h-[44px] touch-manipulation"
|
|
>
|
|
<Building className="h-4 w-4" />
|
|
<span className="text-xs sm:text-sm">거래처</span>
|
|
</TabsTrigger>
|
|
</TabsList>
|
|
</div>
|
|
|
|
{/* 제품 마스터 탭 */}
|
|
<TabsContent value="products" className="space-y-4">
|
|
<Card>
|
|
<CardHeader>
|
|
<div className="flex flex-col space-y-3 sm:flex-row sm:items-center sm:justify-between sm:space-y-0">
|
|
<CardTitle className="text-base sm:text-lg">제품 마스터</CardTitle>
|
|
<div className="flex flex-col space-y-2 sm:flex-row sm:items-center sm:space-y-0 sm:space-x-2">
|
|
<div className="relative">
|
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
|
|
<Input
|
|
placeholder="제품명 검색..."
|
|
className="pl-10 w-full sm:w-64 min-h-[44px] touch-manipulation"
|
|
/>
|
|
</div>
|
|
<Button variant="outline" size="sm" className="w-full sm:w-auto min-h-[44px] touch-manipulation">
|
|
<Filter className="h-4 w-4 mr-2" />
|
|
필터
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{/* 모바일: 카드 레이아웃, 데스크톱: 테이블 */}
|
|
<div className="block sm:hidden space-y-3">
|
|
{products.map((product) => (
|
|
<Card key={product.id} className="p-4 hover:shadow-md transition-shadow">
|
|
<div className="flex justify-between items-start mb-3">
|
|
<div className="min-w-0 flex-1">
|
|
<h3 className="font-medium text-sm truncate">{product.name}</h3>
|
|
<p className="text-xs text-gray-600">{product.code}</p>
|
|
</div>
|
|
<Badge className={`${getStatusColor(product.status)} text-white ml-2 flex-shrink-0`}>
|
|
{product.status}
|
|
</Badge>
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-2 text-xs mb-3">
|
|
<div>
|
|
<span className="text-gray-600">유형:</span> {product.type}
|
|
</div>
|
|
<div>
|
|
<span className="text-gray-600">단위:</span> {product.unit}
|
|
</div>
|
|
<div>
|
|
<span className="text-gray-600">표준시간:</span> {product.standardTime}분
|
|
</div>
|
|
<div>
|
|
<span className="text-gray-600">재료비:</span> {product.materialCost.toLocaleString()}원
|
|
</div>
|
|
</div>
|
|
<div className="flex space-x-2">
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
onClick={() => {
|
|
setSelectedItem(product);
|
|
setIsViewModalOpen(true);
|
|
}}
|
|
className="flex-1 min-h-[40px] touch-manipulation"
|
|
>
|
|
<Eye className="h-3 w-3 mr-1" />
|
|
보기
|
|
</Button>
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
onClick={() => {
|
|
setSelectedItem(product);
|
|
setFormData(product);
|
|
setIsEditModalOpen(true);
|
|
}}
|
|
className="flex-1 min-h-[40px] touch-manipulation"
|
|
>
|
|
<Edit className="h-3 w-3 mr-1" />
|
|
수정
|
|
</Button>
|
|
<AlertDialog>
|
|
<AlertDialogTrigger asChild>
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
className="min-h-[40px] px-3 touch-manipulation"
|
|
>
|
|
<Trash2 className="h-3 w-3" />
|
|
</Button>
|
|
</AlertDialogTrigger>
|
|
<AlertDialogContent className="mx-4 max-w-lg">
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>제품 삭제</AlertDialogTitle>
|
|
<AlertDialogDescription>
|
|
{product.name} 제품을 삭제하시겠습니까?
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel className="min-h-[44px] touch-manipulation">취소</AlertDialogCancel>
|
|
<AlertDialogAction
|
|
onClick={() => handleDelete(product.id)}
|
|
className="min-h-[44px] touch-manipulation"
|
|
>
|
|
삭제
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
</div>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
|
|
{/* 데스크톱 테이블 */}
|
|
<div className="hidden sm:block 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-[60px]">단위</TableHead>
|
|
<TableHead className="min-w-[100px]">표준시간(분)</TableHead>
|
|
<TableHead className="min-w-[100px]">재료비</TableHead>
|
|
<TableHead className="min-w-[80px]">상태</TableHead>
|
|
<TableHead className="min-w-[120px]">관리</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{products.map((product) => (
|
|
<TableRow key={product.id}>
|
|
<TableCell className="font-medium">{product.code}</TableCell>
|
|
<TableCell>{product.name}</TableCell>
|
|
<TableCell>{product.type}</TableCell>
|
|
<TableCell>{product.unit}</TableCell>
|
|
<TableCell>{product.standardTime}분</TableCell>
|
|
<TableCell>{product.materialCost.toLocaleString()}원</TableCell>
|
|
<TableCell>
|
|
<Badge className={`${getStatusColor(product.status)} text-white`}>
|
|
{product.status}
|
|
</Badge>
|
|
</TableCell>
|
|
<TableCell>
|
|
<div className="flex items-center space-x-1">
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
onClick={() => {
|
|
setSelectedItem(product);
|
|
setIsViewModalOpen(true);
|
|
}}
|
|
className="p-2"
|
|
>
|
|
<Eye className="h-3 w-3" />
|
|
</Button>
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
onClick={() => {
|
|
setSelectedItem(product);
|
|
setFormData(product);
|
|
setIsEditModalOpen(true);
|
|
}}
|
|
className="p-2"
|
|
>
|
|
<Edit className="h-3 w-3" />
|
|
</Button>
|
|
<AlertDialog>
|
|
<AlertDialogTrigger asChild>
|
|
<Button size="sm" variant="outline" className="p-2">
|
|
<Trash2 className="h-3 w-3" />
|
|
</Button>
|
|
</AlertDialogTrigger>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>제품 삭제</AlertDialogTitle>
|
|
<AlertDialogDescription>
|
|
{product.name} 제품을 삭제하시겠습니까?
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel>취소</AlertDialogCancel>
|
|
<AlertDialogAction onClick={() => handleDelete(product.id)}>
|
|
삭제
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
</div>
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
|
|
{/* BOM 탭 */}
|
|
<TabsContent value="bom" className="space-y-4">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-base sm:text-lg">BOM (Bill of Materials)</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{/* 모바일 카드 레이아웃 */}
|
|
<div className="block sm:hidden space-y-3">
|
|
{bomData.map((bom) => (
|
|
<Card key={bom.id} className="p-4 hover:shadow-md transition-shadow">
|
|
<div className="space-y-3">
|
|
<div>
|
|
<h3 className="font-medium text-sm">{bom.productName}</h3>
|
|
<p className="text-xs text-gray-600">{bom.productCode}</p>
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-2 text-xs">
|
|
<div>
|
|
<span className="text-gray-600">자재:</span> {bom.materialName}
|
|
</div>
|
|
<div>
|
|
<span className="text-gray-600">코드:</span> {bom.materialCode}
|
|
</div>
|
|
<div>
|
|
<span className="text-gray-600">소요량:</span> {bom.quantity} {bom.unit}
|
|
</div>
|
|
<div>
|
|
<span className="text-gray-600">비고:</span> {bom.notes}
|
|
</div>
|
|
</div>
|
|
<div className="flex space-x-2">
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
onClick={() => {
|
|
setSelectedItem(bom);
|
|
setFormData(bom);
|
|
setIsEditModalOpen(true);
|
|
}}
|
|
className="flex-1 min-h-[40px] touch-manipulation"
|
|
>
|
|
<Edit className="h-3 w-3 mr-1" />
|
|
수정
|
|
</Button>
|
|
<AlertDialog>
|
|
<AlertDialogTrigger asChild>
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
className="min-h-[40px] px-3 touch-manipulation"
|
|
>
|
|
<Trash2 className="h-3 w-3" />
|
|
</Button>
|
|
</AlertDialogTrigger>
|
|
<AlertDialogContent className="mx-4 max-w-lg">
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>BOM 삭제</AlertDialogTitle>
|
|
<AlertDialogDescription>
|
|
이 BOM 항목을 삭제하시겠습니까?
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel className="min-h-[44px] touch-manipulation">취소</AlertDialogCancel>
|
|
<AlertDialogAction
|
|
onClick={() => handleDelete(bom.id)}
|
|
className="min-h-[44px] touch-manipulation"
|
|
>
|
|
삭제
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
|
|
{/* 데스크톱 테이블 */}
|
|
<div className="hidden sm:block overflow-x-auto">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead className="min-w-[100px]">제품코드</TableHead>
|
|
<TableHead className="min-w-[150px]">제품명</TableHead>
|
|
<TableHead className="min-w-[100px]">자재코드</TableHead>
|
|
<TableHead className="min-w-[150px]">자재명</TableHead>
|
|
<TableHead className="min-w-[80px]">소요량</TableHead>
|
|
<TableHead className="min-w-[60px]">단위</TableHead>
|
|
<TableHead className="min-w-[120px]">관리</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{bomData.map((bom) => (
|
|
<TableRow key={bom.id}>
|
|
<TableCell className="font-medium">{bom.productCode}</TableCell>
|
|
<TableCell>{bom.productName}</TableCell>
|
|
<TableCell>{bom.materialCode}</TableCell>
|
|
<TableCell>{bom.materialName}</TableCell>
|
|
<TableCell>{bom.quantity}</TableCell>
|
|
<TableCell>{bom.unit}</TableCell>
|
|
<TableCell>
|
|
<div className="flex items-center space-x-1">
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
onClick={() => {
|
|
setSelectedItem(bom);
|
|
setFormData(bom);
|
|
setIsEditModalOpen(true);
|
|
}}
|
|
className="p-2"
|
|
>
|
|
<Edit className="h-3 w-3" />
|
|
</Button>
|
|
<AlertDialog>
|
|
<AlertDialogTrigger asChild>
|
|
<Button size="sm" variant="outline" className="p-2">
|
|
<Trash2 className="h-3 w-3" />
|
|
</Button>
|
|
</AlertDialogTrigger>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>BOM 삭제</AlertDialogTitle>
|
|
<AlertDialogDescription>
|
|
이 BOM 항목을 삭제하시겠습니까?
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel>취소</AlertDialogCancel>
|
|
<AlertDialogAction onClick={() => handleDelete(bom.id)}>
|
|
삭제
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
</div>
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
|
|
{/* 공정 마스터 탭 */}
|
|
<TabsContent value="processes" className="space-y-4">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-base sm:text-lg">공정 마스터</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{/* 모바일 카드 레이아웃 */}
|
|
<div className="block sm:hidden space-y-3">
|
|
{processes.map((process) => (
|
|
<Card key={process.id} className="p-4 hover:shadow-md transition-shadow">
|
|
<div className="space-y-3">
|
|
<div>
|
|
<h3 className="font-medium text-sm">{process.name}</h3>
|
|
<p className="text-xs text-gray-600">{process.code}</p>
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-2 text-xs">
|
|
<div>
|
|
<span className="text-gray-600">작업장:</span> {process.workCenter}
|
|
</div>
|
|
<div>
|
|
<span className="text-gray-600">표준시간:</span> {process.standardTime}분
|
|
</div>
|
|
<div>
|
|
<span className="text-gray-600">준비시간:</span> {process.setupTime}분
|
|
</div>
|
|
<div>
|
|
<span className="text-gray-600">다음공정:</span> {process.nextProcess || "-"}
|
|
</div>
|
|
</div>
|
|
<div className="flex space-x-2">
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
onClick={() => {
|
|
setSelectedItem(process);
|
|
setFormData(process);
|
|
setIsEditModalOpen(true);
|
|
}}
|
|
className="flex-1 min-h-[40px] touch-manipulation"
|
|
>
|
|
<Edit className="h-3 w-3 mr-1" />
|
|
수정
|
|
</Button>
|
|
<AlertDialog>
|
|
<AlertDialogTrigger asChild>
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
className="min-h-[40px] px-3 touch-manipulation"
|
|
>
|
|
<Trash2 className="h-3 w-3" />
|
|
</Button>
|
|
</AlertDialogTrigger>
|
|
<AlertDialogContent className="mx-4 max-w-lg">
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>공정 삭제</AlertDialogTitle>
|
|
<AlertDialogDescription>
|
|
{process.name} 공정을 삭제하시겠습니까?
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel className="min-h-[44px] touch-manipulation">취소</AlertDialogCancel>
|
|
<AlertDialogAction
|
|
onClick={() => handleDelete(process.id)}
|
|
className="min-h-[44px] touch-manipulation"
|
|
>
|
|
삭제
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
|
|
{/* 데스크톱 테이블 */}
|
|
<div className="hidden sm:block overflow-x-auto">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead className="min-w-[100px]">공정코드</TableHead>
|
|
<TableHead className="min-w-[120px]">공정명</TableHead>
|
|
<TableHead className="min-w-[120px]">작업장</TableHead>
|
|
<TableHead className="min-w-[100px]">표준시간(분)</TableHead>
|
|
<TableHead className="min-w-[100px]">준비시간(분)</TableHead>
|
|
<TableHead className="min-w-[100px]">다음공정</TableHead>
|
|
<TableHead className="min-w-[120px]">관리</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{processes.map((process) => (
|
|
<TableRow key={process.id}>
|
|
<TableCell className="font-medium">{process.code}</TableCell>
|
|
<TableCell>{process.name}</TableCell>
|
|
<TableCell>{process.workCenter}</TableCell>
|
|
<TableCell>{process.standardTime}분</TableCell>
|
|
<TableCell>{process.setupTime}분</TableCell>
|
|
<TableCell>{process.nextProcess || "-"}</TableCell>
|
|
<TableCell>
|
|
<div className="flex items-center space-x-1">
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
onClick={() => {
|
|
setSelectedItem(process);
|
|
setFormData(process);
|
|
setIsEditModalOpen(true);
|
|
}}
|
|
className="p-2"
|
|
>
|
|
<Edit className="h-3 w-3" />
|
|
</Button>
|
|
<AlertDialog>
|
|
<AlertDialogTrigger asChild>
|
|
<Button size="sm" variant="outline" className="p-2">
|
|
<Trash2 className="h-3 w-3" />
|
|
</Button>
|
|
</AlertDialogTrigger>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>공정 삭제</AlertDialogTitle>
|
|
<AlertDialogDescription>
|
|
{process.name} 공정을 삭제하시겠습니까?
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel>취소</AlertDialogCancel>
|
|
<AlertDialogAction onClick={() => handleDelete(process.id)}>
|
|
삭제
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
</div>
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
|
|
{/* 거래처 탭 */}
|
|
<TabsContent value="customers" className="space-y-4">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-base sm:text-lg">거래처 관리</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{/* 모바일 카드 레이아웃 */}
|
|
<div className="block sm:hidden space-y-3">
|
|
{customers.map((customer) => (
|
|
<Card key={customer.id} className="p-4 hover:shadow-md transition-shadow">
|
|
<div className="flex justify-between items-start mb-3">
|
|
<div className="min-w-0 flex-1">
|
|
<h3 className="font-medium text-sm truncate">{customer.name}</h3>
|
|
<p className="text-xs text-gray-600">{customer.code}</p>
|
|
</div>
|
|
<div className="ml-2 flex-shrink-0">
|
|
<Badge className={`${getStatusColor(customer.status)} text-white`}>
|
|
{customer.status}
|
|
</Badge>
|
|
<p className={`text-xs ${getTypeColor(customer.type)} mt-1`}>{customer.type}</p>
|
|
</div>
|
|
</div>
|
|
<div className="space-y-2 text-xs mb-3">
|
|
<div>
|
|
<span className="text-gray-600">담당자:</span> {customer.contact}
|
|
</div>
|
|
<div>
|
|
<span className="text-gray-600">연락처:</span> {customer.phone}
|
|
</div>
|
|
<div>
|
|
<span className="text-gray-600">주소:</span> {customer.address}
|
|
</div>
|
|
</div>
|
|
<div className="flex space-x-2">
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
onClick={() => {
|
|
setSelectedItem(customer);
|
|
setFormData(customer);
|
|
setIsEditModalOpen(true);
|
|
}}
|
|
className="flex-1 min-h-[40px] touch-manipulation"
|
|
>
|
|
<Edit className="h-3 w-3 mr-1" />
|
|
수정
|
|
</Button>
|
|
<AlertDialog>
|
|
<AlertDialogTrigger asChild>
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
className="min-h-[40px] px-3 touch-manipulation"
|
|
>
|
|
<Trash2 className="h-3 w-3" />
|
|
</Button>
|
|
</AlertDialogTrigger>
|
|
<AlertDialogContent className="mx-4 max-w-lg">
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>거래처 삭제</AlertDialogTitle>
|
|
<AlertDialogDescription>
|
|
{customer.name} 거래처를 삭제하시겠습니까?
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel className="min-h-[44px] touch-manipulation">취소</AlertDialogCancel>
|
|
<AlertDialogAction
|
|
onClick={() => handleDelete(customer.id)}
|
|
className="min-h-[44px] touch-manipulation"
|
|
>
|
|
삭제
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
</div>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
|
|
{/* 데스크톱 테이블 */}
|
|
<div className="hidden sm:block 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-[100px]">담당자</TableHead>
|
|
<TableHead className="min-w-[120px]">연락처</TableHead>
|
|
<TableHead className="min-w-[80px]">상태</TableHead>
|
|
<TableHead className="min-w-[120px]">관리</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{customers.map((customer) => (
|
|
<TableRow key={customer.id}>
|
|
<TableCell className="font-medium">{customer.code}</TableCell>
|
|
<TableCell>{customer.name}</TableCell>
|
|
<TableCell>
|
|
<span className={getTypeColor(customer.type)}>{customer.type}</span>
|
|
</TableCell>
|
|
<TableCell>{customer.contact}</TableCell>
|
|
<TableCell>{customer.phone}</TableCell>
|
|
<TableCell>
|
|
<Badge className={`${getStatusColor(customer.status)} text-white`}>
|
|
{customer.status}
|
|
</Badge>
|
|
</TableCell>
|
|
<TableCell>
|
|
<div className="flex items-center space-x-1">
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
onClick={() => {
|
|
setSelectedItem(customer);
|
|
setFormData(customer);
|
|
setIsEditModalOpen(true);
|
|
}}
|
|
className="p-2"
|
|
>
|
|
<Edit className="h-3 w-3" />
|
|
</Button>
|
|
<AlertDialog>
|
|
<AlertDialogTrigger asChild>
|
|
<Button size="sm" variant="outline" className="p-2">
|
|
<Trash2 className="h-3 w-3" />
|
|
</Button>
|
|
</AlertDialogTrigger>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>거래처 삭제</AlertDialogTitle>
|
|
<AlertDialogDescription>
|
|
{customer.name} 거래처를 삭제하시겠습니까?
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel>취소</AlertDialogCancel>
|
|
<AlertDialogAction onClick={() => handleDelete(customer.id)}>
|
|
삭제
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
</div>
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
</Tabs>
|
|
|
|
{/* 신규 등록 모달 */}
|
|
<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
|
|
<DialogContent className="mx-4 max-w-2xl max-h-[90vh] overflow-y-auto">
|
|
<DialogHeader>
|
|
<DialogTitle>
|
|
{activeTab === "products" && "제품 등록"}
|
|
{activeTab === "bom" && "BOM 등록"}
|
|
{activeTab === "processes" && "공정 등록"}
|
|
{activeTab === "customers" && "거래처 등록"}
|
|
</DialogTitle>
|
|
<DialogDescription>
|
|
새로운 {activeTab === "products" && "제품"}
|
|
{activeTab === "bom" && "BOM"}
|
|
{activeTab === "processes" && "공정"}
|
|
{activeTab === "customers" && "거래처"} 정보를 등록합니다.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<div className="space-y-4">
|
|
{activeTab === "products" && (
|
|
<>
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<div>
|
|
<Label htmlFor="code">제품코드</Label>
|
|
<Input
|
|
id="code"
|
|
value={formData.code || ""}
|
|
onChange={(e) => setFormData({...formData, code: e.target.value})}
|
|
className="min-h-[44px] touch-manipulation"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label htmlFor="name">제품명</Label>
|
|
<Input
|
|
id="name"
|
|
value={formData.name || ""}
|
|
onChange={(e) => setFormData({...formData, name: e.target.value})}
|
|
className="min-h-[44px] touch-manipulation"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<div>
|
|
<Label htmlFor="type">제품유형</Label>
|
|
<Select
|
|
value={formData.type || ""}
|
|
onValueChange={(value) => setFormData({...formData, type: value})}
|
|
>
|
|
<SelectTrigger className="min-h-[44px] touch-manipulation">
|
|
<SelectValue placeholder="유형 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="완제품">완제품</SelectItem>
|
|
<SelectItem value="반제품">반제품</SelectItem>
|
|
<SelectItem value="원자재">원자재</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div>
|
|
<Label htmlFor="unit">단위</Label>
|
|
<Input
|
|
id="unit"
|
|
value={formData.unit || ""}
|
|
onChange={(e) => setFormData({...formData, unit: e.target.value})}
|
|
className="min-h-[44px] touch-manipulation"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<div>
|
|
<Label htmlFor="standardTime">표준시간(분)</Label>
|
|
<Input
|
|
id="standardTime"
|
|
type="number"
|
|
value={formData.standardTime || ""}
|
|
onChange={(e) => setFormData({...formData, standardTime: Number(e.target.value)})}
|
|
className="min-h-[44px] touch-manipulation"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label htmlFor="materialCost">재료비</Label>
|
|
<Input
|
|
id="materialCost"
|
|
type="number"
|
|
value={formData.materialCost || ""}
|
|
onChange={(e) => setFormData({...formData, materialCost: Number(e.target.value)})}
|
|
className="min-h-[44px] touch-manipulation"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<Label htmlFor="description">설명</Label>
|
|
<Textarea
|
|
id="description"
|
|
value={formData.description || ""}
|
|
onChange={(e) => setFormData({...formData, description: e.target.value})}
|
|
className="min-h-[80px] touch-manipulation"
|
|
/>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{activeTab === "bom" && (
|
|
<>
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<div>
|
|
<Label htmlFor="productCode">제품코드</Label>
|
|
<Select
|
|
value={formData.productCode || ""}
|
|
onValueChange={(value) => setFormData({...formData, productCode: value})}
|
|
>
|
|
<SelectTrigger className="min-h-[44px] touch-manipulation">
|
|
<SelectValue placeholder="제품 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{products.map((product) => (
|
|
<SelectItem key={product.id} value={product.code}>
|
|
{product.code} - {product.name}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div>
|
|
<Label htmlFor="materialCode">자재코드</Label>
|
|
<Input
|
|
id="materialCode"
|
|
value={formData.materialCode || ""}
|
|
onChange={(e) => setFormData({...formData, materialCode: e.target.value})}
|
|
className="min-h-[44px] touch-manipulation"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<Label htmlFor="materialName">자재명</Label>
|
|
<Input
|
|
id="materialName"
|
|
value={formData.materialName || ""}
|
|
onChange={(e) => setFormData({...formData, materialName: e.target.value})}
|
|
className="min-h-[44px] touch-manipulation"
|
|
/>
|
|
</div>
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<div>
|
|
<Label htmlFor="quantity">소요량</Label>
|
|
<Input
|
|
id="quantity"
|
|
type="number"
|
|
step="0.01"
|
|
value={formData.quantity || ""}
|
|
onChange={(e) => setFormData({...formData, quantity: Number(e.target.value)})}
|
|
className="min-h-[44px] touch-manipulation"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label htmlFor="unit">단위</Label>
|
|
<Input
|
|
id="unit"
|
|
value={formData.unit || ""}
|
|
onChange={(e) => setFormData({...formData, unit: e.target.value})}
|
|
className="min-h-[44px] touch-manipulation"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<Label htmlFor="notes">비고</Label>
|
|
<Textarea
|
|
id="notes"
|
|
value={formData.notes || ""}
|
|
onChange={(e) => setFormData({...formData, notes: e.target.value})}
|
|
className="min-h-[80px] touch-manipulation"
|
|
/>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{activeTab === "processes" && (
|
|
<>
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<div>
|
|
<Label htmlFor="code">공정코드</Label>
|
|
<Input
|
|
id="code"
|
|
value={formData.code || ""}
|
|
onChange={(e) => setFormData({...formData, code: e.target.value})}
|
|
className="min-h-[44px] touch-manipulation"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label htmlFor="name">공정명</Label>
|
|
<Input
|
|
id="name"
|
|
value={formData.name || ""}
|
|
onChange={(e) => setFormData({...formData, name: e.target.value})}
|
|
className="min-h-[44px] touch-manipulation"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<Label htmlFor="workCenter">작업장</Label>
|
|
<Input
|
|
id="workCenter"
|
|
value={formData.workCenter || ""}
|
|
onChange={(e) => setFormData({...formData, workCenter: e.target.value})}
|
|
className="min-h-[44px] touch-manipulation"
|
|
/>
|
|
</div>
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<div>
|
|
<Label htmlFor="standardTime">표준시간(분)</Label>
|
|
<Input
|
|
id="standardTime"
|
|
type="number"
|
|
value={formData.standardTime || ""}
|
|
onChange={(e) => setFormData({...formData, standardTime: Number(e.target.value)})}
|
|
className="min-h-[44px] touch-manipulation"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label htmlFor="setupTime">준비시간(분)</Label>
|
|
<Input
|
|
id="setupTime"
|
|
type="number"
|
|
value={formData.setupTime || ""}
|
|
onChange={(e) => setFormData({...formData, setupTime: Number(e.target.value)})}
|
|
className="min-h-[44px] touch-manipulation"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<Label htmlFor="description">설명</Label>
|
|
<Textarea
|
|
id="description"
|
|
value={formData.description || ""}
|
|
onChange={(e) => setFormData({...formData, description: e.target.value})}
|
|
className="min-h-[80px] touch-manipulation"
|
|
/>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{activeTab === "customers" && (
|
|
<>
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<div>
|
|
<Label htmlFor="code">거래처코드</Label>
|
|
<Input
|
|
id="code"
|
|
value={formData.code || ""}
|
|
onChange={(e) => setFormData({...formData, code: e.target.value})}
|
|
className="min-h-[44px] touch-manipulation"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label htmlFor="name">거래처명</Label>
|
|
<Input
|
|
id="name"
|
|
value={formData.name || ""}
|
|
onChange={(e) => setFormData({...formData, name: e.target.value})}
|
|
className="min-h-[44px] touch-manipulation"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<Label htmlFor="type">거래처 유형</Label>
|
|
<Select
|
|
value={formData.type || ""}
|
|
onValueChange={(value) => setFormData({...formData, type: value})}
|
|
>
|
|
<SelectTrigger className="min-h-[44px] touch-manipulation">
|
|
<SelectValue placeholder="유형 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="고객">고객</SelectItem>
|
|
<SelectItem value="공급업체">공급업체</SelectItem>
|
|
<SelectItem value="양방향">양방향</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<div>
|
|
<Label htmlFor="contact">담당자</Label>
|
|
<Input
|
|
id="contact"
|
|
value={formData.contact || ""}
|
|
onChange={(e) => setFormData({...formData, contact: e.target.value})}
|
|
className="min-h-[44px] touch-manipulation"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label htmlFor="phone">연락처</Label>
|
|
<Input
|
|
id="phone"
|
|
value={formData.phone || ""}
|
|
onChange={(e) => setFormData({...formData, phone: e.target.value})}
|
|
className="min-h-[44px] touch-manipulation"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<Label htmlFor="address">주소</Label>
|
|
<Input
|
|
id="address"
|
|
value={formData.address || ""}
|
|
onChange={(e) => setFormData({...formData, address: e.target.value})}
|
|
className="min-h-[44px] touch-manipulation"
|
|
/>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
<div className="flex flex-col sm:flex-row space-y-2 sm:space-y-0 sm:space-x-2 pt-4">
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => setIsModalOpen(false)}
|
|
className="w-full sm:flex-1 min-h-[44px] touch-manipulation"
|
|
>
|
|
취소
|
|
</Button>
|
|
<Button
|
|
onClick={handleCreate}
|
|
className="w-full sm:flex-1 min-h-[44px] touch-manipulation bg-blue-600 hover:bg-blue-700"
|
|
>
|
|
등록
|
|
</Button>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
{/* 수정 모달 */}
|
|
<Dialog open={isEditModalOpen} onOpenChange={setIsEditModalOpen}>
|
|
<DialogContent className="mx-4 max-w-2xl max-h-[90vh] overflow-y-auto">
|
|
<DialogHeader>
|
|
<DialogTitle>
|
|
{activeTab === "products" && "제품 수정"}
|
|
{activeTab === "bom" && "BOM 수정"}
|
|
{activeTab === "processes" && "공정 수정"}
|
|
{activeTab === "customers" && "거래처 수정"}
|
|
</DialogTitle>
|
|
<DialogDescription>
|
|
선택한 항목의 정보를 수정합니다.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<div className="space-y-4">
|
|
{/* 수정 폼은 등록 폼과 동일한 구조 */}
|
|
{activeTab === "products" && (
|
|
<>
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<div>
|
|
<Label htmlFor="edit-code">제품코드</Label>
|
|
<Input
|
|
id="edit-code"
|
|
value={formData.code || ""}
|
|
onChange={(e) => setFormData({...formData, code: e.target.value})}
|
|
className="min-h-[44px] touch-manipulation"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label htmlFor="edit-name">제품명</Label>
|
|
<Input
|
|
id="edit-name"
|
|
value={formData.name || ""}
|
|
onChange={(e) => setFormData({...formData, name: e.target.value})}
|
|
className="min-h-[44px] touch-manipulation"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<div>
|
|
<Label htmlFor="edit-type">제품유형</Label>
|
|
<Select
|
|
value={formData.type || ""}
|
|
onValueChange={(value) => setFormData({...formData, type: value})}
|
|
>
|
|
<SelectTrigger className="min-h-[44px] touch-manipulation">
|
|
<SelectValue placeholder="유형 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="완제품">완제품</SelectItem>
|
|
<SelectItem value="반제품">반제품</SelectItem>
|
|
<SelectItem value="원자재">원자재</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div>
|
|
<Label htmlFor="edit-unit">단위</Label>
|
|
<Input
|
|
id="edit-unit"
|
|
value={formData.unit || ""}
|
|
onChange={(e) => setFormData({...formData, unit: e.target.value})}
|
|
className="min-h-[44px] touch-manipulation"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<div>
|
|
<Label htmlFor="edit-standardTime">표준시간(분)</Label>
|
|
<Input
|
|
id="edit-standardTime"
|
|
type="number"
|
|
value={formData.standardTime || ""}
|
|
onChange={(e) => setFormData({...formData, standardTime: Number(e.target.value)})}
|
|
className="min-h-[44px] touch-manipulation"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label htmlFor="edit-materialCost">재료비</Label>
|
|
<Input
|
|
id="edit-materialCost"
|
|
type="number"
|
|
value={formData.materialCost || ""}
|
|
onChange={(e) => setFormData({...formData, materialCost: Number(e.target.value)})}
|
|
className="min-h-[44px] touch-manipulation"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<Label htmlFor="edit-description">설명</Label>
|
|
<Textarea
|
|
id="edit-description"
|
|
value={formData.description || ""}
|
|
onChange={(e) => setFormData({...formData, description: e.target.value})}
|
|
className="min-h-[80px] touch-manipulation"
|
|
/>
|
|
</div>
|
|
</>
|
|
)}
|
|
{/* BOM, 공정, 거래처 수정 폼들도 유사하게 구성 */}
|
|
</div>
|
|
<div className="flex flex-col sm:flex-row space-y-2 sm:space-y-0 sm:space-x-2 pt-4">
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => setIsEditModalOpen(false)}
|
|
className="w-full sm:flex-1 min-h-[44px] touch-manipulation"
|
|
>
|
|
취소
|
|
</Button>
|
|
<Button
|
|
onClick={handleUpdate}
|
|
className="w-full sm:flex-1 min-h-[44px] touch-manipulation bg-blue-600 hover:bg-blue-700"
|
|
>
|
|
수정
|
|
</Button>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
{/* 상세보기 모달 */}
|
|
<Dialog open={isViewModalOpen} onOpenChange={setIsViewModalOpen}>
|
|
<DialogContent className="mx-4 max-w-2xl max-h-[90vh] overflow-y-auto">
|
|
<DialogHeader>
|
|
<DialogTitle>제품 상세 정보</DialogTitle>
|
|
<DialogDescription>
|
|
제품의 상세 정보를 확인합니다.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
{selectedItem && (
|
|
<div className="space-y-4">
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 text-sm">
|
|
<div>
|
|
<span className="text-gray-600">제품코드:</span>
|
|
<p className="font-medium">{selectedItem.code}</p>
|
|
</div>
|
|
<div>
|
|
<span className="text-gray-600">제품명:</span>
|
|
<p className="font-medium">{selectedItem.name}</p>
|
|
</div>
|
|
<div>
|
|
<span className="text-gray-600">유형:</span>
|
|
<p className="font-medium">{selectedItem.type}</p>
|
|
</div>
|
|
<div>
|
|
<span className="text-gray-600">단위:</span>
|
|
<p className="font-medium">{selectedItem.unit}</p>
|
|
</div>
|
|
<div>
|
|
<span className="text-gray-600">표준시간:</span>
|
|
<p className="font-medium">{selectedItem.standardTime}분</p>
|
|
</div>
|
|
<div>
|
|
<span className="text-gray-600">재료비:</span>
|
|
<p className="font-medium">{selectedItem.materialCost?.toLocaleString()}원</p>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<span className="text-gray-600 text-sm">설명:</span>
|
|
<p className="font-medium mt-1">{selectedItem.description}</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
<div className="pt-4">
|
|
<Button
|
|
onClick={() => setIsViewModalOpen(false)}
|
|
className="w-full min-h-[44px] touch-manipulation"
|
|
>
|
|
닫기
|
|
</Button>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</div>
|
|
);
|
|
} |