- ItemMasterDataManagement 컴포넌트에서 hooks 분리 - 다이얼로그 컴포넌트들 타입 및 구조 개선 - BOMManagementSection 개선 - HierarchyTab 업데이트 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
292 lines
10 KiB
TypeScript
292 lines
10 KiB
TypeScript
'use client';
|
|
|
|
import React, { useState } from 'react';
|
|
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, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Plus, Edit, Trash2, Package, GripVertical } from 'lucide-react';
|
|
import { toast } from 'sonner';
|
|
import type { BOMItem } from '@/contexts/ItemMasterContext';
|
|
|
|
interface BOMManagementSectionProps {
|
|
title?: string;
|
|
description?: string;
|
|
bomItems: BOMItem[];
|
|
onAddItem: (item: Omit<BOMItem, 'id' | 'created_at' | 'updated_at' | 'tenant_id' | 'section_id'>) => void;
|
|
onUpdateItem: (id: number, item: Partial<BOMItem>) => void;
|
|
onDeleteItem: (id: number) => void;
|
|
itemTypeOptions?: { value: string; label: string }[];
|
|
unitOptions?: { value: string; label: string }[];
|
|
}
|
|
|
|
export function BOMManagementSection({
|
|
title = '부품 구성 (BOM)',
|
|
description = '이 제품을 구성하는 하위 품목을 추가하세요',
|
|
bomItems,
|
|
onAddItem,
|
|
onUpdateItem,
|
|
onDeleteItem,
|
|
itemTypeOptions = [],
|
|
unitOptions = [],
|
|
}: BOMManagementSectionProps) {
|
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
|
const [editingId, setEditingId] = useState<number | null>(null);
|
|
const [itemCode, setItemCode] = useState('');
|
|
const [itemName, setItemName] = useState('');
|
|
const [quantity, setQuantity] = useState('1');
|
|
const [unit, setUnit] = useState('EA');
|
|
const [itemType, setItemType] = useState('part');
|
|
const [note, setNote] = useState('');
|
|
const [isSubmitted, setIsSubmitted] = useState(false);
|
|
|
|
// 유효성 검사
|
|
const isItemCodeEmpty = !itemCode.trim();
|
|
const isItemNameEmpty = !itemName.trim();
|
|
const qty = parseFloat(quantity);
|
|
const isQuantityInvalid = isNaN(qty) || qty <= 0;
|
|
|
|
const handleOpenDialog = (item?: BOMItem) => {
|
|
if (item) {
|
|
setEditingId(item.id);
|
|
setItemCode(item.item_code || '');
|
|
setItemName(item.item_name);
|
|
setQuantity(item.quantity.toString());
|
|
setUnit(item.unit || 'EA');
|
|
setItemType('part');
|
|
setNote(item.spec || '');
|
|
} else {
|
|
setEditingId(null);
|
|
setItemCode('');
|
|
setItemName('');
|
|
setQuantity('1');
|
|
setUnit('EA');
|
|
setItemType('part');
|
|
setNote('');
|
|
}
|
|
setIsDialogOpen(true);
|
|
setIsSubmitted(false);
|
|
};
|
|
|
|
const handleClose = () => {
|
|
setIsDialogOpen(false);
|
|
setEditingId(null);
|
|
setItemCode('');
|
|
setItemName('');
|
|
setQuantity('1');
|
|
setUnit('EA');
|
|
setItemType('part');
|
|
setNote('');
|
|
setIsSubmitted(false);
|
|
};
|
|
|
|
const handleSave = () => {
|
|
setIsSubmitted(true);
|
|
if (isItemCodeEmpty || isItemNameEmpty || isQuantityInvalid) {
|
|
return;
|
|
}
|
|
|
|
const itemData = {
|
|
item_code: itemCode,
|
|
item_name: itemName,
|
|
quantity: qty,
|
|
unit,
|
|
spec: note.trim() || undefined,
|
|
};
|
|
|
|
if (editingId) {
|
|
onUpdateItem(editingId, itemData);
|
|
toast.success('BOM 품목이 수정되었습니다');
|
|
} else {
|
|
onAddItem(itemData);
|
|
toast.success('BOM 품목이 추가되었습니다');
|
|
}
|
|
|
|
handleClose();
|
|
};
|
|
|
|
const handleDelete = (id: number) => {
|
|
if (confirm('이 BOM 품목을 삭제하시겠습니까?')) {
|
|
onDeleteItem(id);
|
|
toast.success('BOM 품목이 삭제되었습니다');
|
|
}
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<Card>
|
|
<CardHeader className="pb-3">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<CardTitle className="text-base">{title}</CardTitle>
|
|
<CardDescription className="text-sm">{description}</CardDescription>
|
|
</div>
|
|
<Button size="sm" onClick={() => handleOpenDialog()}>
|
|
<Plus className="h-4 w-4 mr-2" />
|
|
BOM 품목 추가
|
|
</Button>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{bomItems.length === 0 ? (
|
|
<div className="bg-gray-50 border-2 border-dashed border-gray-200 rounded-lg py-16">
|
|
<div className="text-center">
|
|
<div className="w-16 h-16 mx-auto mb-4 bg-gray-100 rounded-lg flex items-center justify-center">
|
|
<Package className="w-8 h-8 text-gray-400" />
|
|
</div>
|
|
<p className="text-gray-600 mb-1">
|
|
BOM 품목을 추가하여면 위의 버튼을 클릭하세요
|
|
</p>
|
|
<p className="text-sm text-gray-500">
|
|
품목의 품목명, 날짜, 상태정보 관리하는 수 있습니다
|
|
</p>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-2">
|
|
{bomItems.map((item) => (
|
|
<div
|
|
key={item.id}
|
|
className="flex items-center justify-between p-3 border rounded hover:bg-gray-50 transition-colors"
|
|
>
|
|
<div className="flex-1">
|
|
<div className="flex items-center gap-2">
|
|
<GripVertical className="h-4 w-4 text-gray-400" />
|
|
<span className="font-medium">{item.item_name}</span>
|
|
{item.item_code && (
|
|
<Badge variant="outline" className="text-xs">
|
|
{item.item_code}
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
<div className="ml-6 text-sm text-gray-500 mt-1">
|
|
수량: {item.quantity} {item.unit || 'EA'}
|
|
{item.spec && <span className="ml-2">• {item.spec}</span>}
|
|
</div>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<Button size="sm" variant="ghost" onClick={() => handleOpenDialog(item)}>
|
|
<Edit className="h-4 w-4 text-blue-500" />
|
|
</Button>
|
|
<Button size="sm" variant="ghost" onClick={() => handleDelete(item.id)}>
|
|
<Trash2 className="h-4 w-4 text-red-500" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* BOM 품목 추가/수정 다이얼로그 */}
|
|
<Dialog
|
|
open={isDialogOpen}
|
|
onOpenChange={(open) => {
|
|
if (!open) handleClose();
|
|
}}
|
|
>
|
|
<DialogContent className="max-w-2xl">
|
|
<DialogHeader>
|
|
<DialogTitle>{editingId ? 'BOM 품목 수정' : 'BOM 품목 추가'}</DialogTitle>
|
|
<DialogDescription>
|
|
하위 품목 정보를 입력하세요
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<div className="space-y-4">
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<Label>품목코드 *</Label>
|
|
<Input
|
|
value={itemCode}
|
|
onChange={(e) => setItemCode(e.target.value)}
|
|
placeholder="예: PART-001"
|
|
className={isSubmitted && isItemCodeEmpty ? 'border-red-500 focus-visible:ring-red-500' : ''}
|
|
/>
|
|
{isSubmitted && isItemCodeEmpty && (
|
|
<p className="text-xs text-red-500 mt-1">품목코드를 입력해주세요</p>
|
|
)}
|
|
</div>
|
|
<div>
|
|
<Label>품목명 *</Label>
|
|
<Input
|
|
value={itemName}
|
|
onChange={(e) => setItemName(e.target.value)}
|
|
placeholder="예: 샤프트"
|
|
className={isSubmitted && isItemNameEmpty ? 'border-red-500 focus-visible:ring-red-500' : ''}
|
|
/>
|
|
{isSubmitted && isItemNameEmpty && (
|
|
<p className="text-xs text-red-500 mt-1">품목명을 입력해주세요</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-3 gap-4">
|
|
<div>
|
|
<Label>수량 *</Label>
|
|
<Input
|
|
type="number"
|
|
value={quantity}
|
|
onChange={(e) => setQuantity(e.target.value)}
|
|
placeholder="1"
|
|
min="0"
|
|
step="0.01"
|
|
className={isSubmitted && isQuantityInvalid ? 'border-red-500 focus-visible:ring-red-500' : ''}
|
|
/>
|
|
{isSubmitted && isQuantityInvalid && (
|
|
<p className="text-xs text-red-500 mt-1">올바른 수량을 입력해주세요 (0보다 큰 숫자)</p>
|
|
)}
|
|
</div>
|
|
<div>
|
|
<Label>단위 *</Label>
|
|
<Select value={unit} onValueChange={setUnit}>
|
|
<SelectTrigger>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{unitOptions.map((opt) => (
|
|
<SelectItem key={opt.value} value={opt.value}>
|
|
{opt.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div>
|
|
<Label>품목유형</Label>
|
|
<Select value={itemType} onValueChange={setItemType}>
|
|
<SelectTrigger>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{itemTypeOptions.map((opt) => (
|
|
<SelectItem key={opt.value} value={opt.value}>
|
|
{opt.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<Label>비고 (선택)</Label>
|
|
<Input
|
|
value={note}
|
|
onChange={(e) => setNote(e.target.value)}
|
|
placeholder="추가 정보를 입력하세요"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<DialogFooter>
|
|
<Button variant="outline" onClick={handleClose}>취소</Button>
|
|
<Button onClick={handleSave}>저장</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</>
|
|
);
|
|
} |