Files
sam-react-prod/src/components/items/BOMManagementSection.tsx
byeongcheolryu b73603822b refactor: 품목기준관리 hooks 분리 및 다이얼로그 개선
- ItemMasterDataManagement 컴포넌트에서 hooks 분리
- 다이얼로그 컴포넌트들 타입 및 구조 개선
- BOMManagementSection 개선
- HierarchyTab 업데이트

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-26 14:06:48 +09:00

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>
</>
);
}