Files
sam-react-prod/src/components/items/BOMManagementSection.tsx
byeongcheolryu df3db155dd [feat]: Item Master 데이터 관리 기능 구현 및 타입 에러 수정
- ItemMasterDataManagement 컴포넌트 구조화 (tabs, dialogs, components 분리)
- HierarchyTab 타입 에러 수정 (BOMItem section_id, updated_at 추가)
- API 클라이언트 구현 (item-master.ts, 13개 엔드포인트)
- ItemMasterContext 구현 (상태 관리 및 데이터 흐름)
- 백엔드 요구사항 문서 작성 (CORS 설정, API 스펙 등)
- SSR 호환성 수정 (navigator API typeof window 체크)
- 미사용 변수 ESLint 에러 해결
- Context 리팩토링 (AuthContext, RootProvider 추가)
- API 유틸리티 추가 (error-handler, logger, transformers)

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-23 16:10:27 +09:00

278 lines
9.6 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' | 'createdAt' | '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 = [
{ value: 'product', label: '제품' },
{ value: 'part', label: '부품' },
{ value: 'material', label: '원자재' },
],
unitOptions = [
{ value: 'EA', label: 'EA' },
{ value: 'KG', label: 'KG' },
{ value: 'M', label: 'M' },
{ value: 'L', label: 'L' },
],
}: 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 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);
};
const handleSave = () => {
if (!itemCode.trim() || !itemName.trim()) {
return toast.error('품목코드와 품목명을 입력해주세요');
}
const qty = parseFloat(quantity);
if (isNaN(qty) || qty <= 0) {
return toast.error('올바른 수량을 입력해주세요');
}
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 품목이 추가되었습니다');
}
setIsDialogOpen(false);
};
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) => {
setIsDialogOpen(open);
if (!open) {
setEditingId(null);
}
}}
>
<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"
/>
</div>
<div>
<Label> *</Label>
<Input
value={itemName}
onChange={(e) => setItemName(e.target.value)}
placeholder="예: 샤프트"
/>
</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"
/>
</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={() => setIsDialogOpen(false)}>
</Button>
<Button onClick={handleSave}></Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
}