2026-02-25 14:50:51 +09:00
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
import React, { useState, useEffect } from "react";
|
|
|
|
|
import {
|
|
|
|
|
Dialog,
|
|
|
|
|
DialogContent,
|
|
|
|
|
DialogHeader,
|
|
|
|
|
DialogTitle,
|
|
|
|
|
DialogDescription,
|
|
|
|
|
DialogFooter,
|
|
|
|
|
} from "@/components/ui/dialog";
|
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
|
import { Input } from "@/components/ui/input";
|
|
|
|
|
import { Label } from "@/components/ui/label";
|
|
|
|
|
import { Textarea } from "@/components/ui/textarea";
|
|
|
|
|
import { Loader2 } from "lucide-react";
|
2026-02-25 15:34:03 +09:00
|
|
|
import { apiClient } from "@/lib/api/client";
|
2026-02-25 14:50:51 +09:00
|
|
|
|
|
|
|
|
interface BomDetailEditModalProps {
|
|
|
|
|
open: boolean;
|
|
|
|
|
onOpenChange: (open: boolean) => void;
|
|
|
|
|
node: Record<string, any> | null;
|
|
|
|
|
isRootNode?: boolean;
|
|
|
|
|
tableName: string;
|
|
|
|
|
onSaved?: () => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function BomDetailEditModal({
|
|
|
|
|
open,
|
|
|
|
|
onOpenChange,
|
|
|
|
|
node,
|
|
|
|
|
isRootNode = false,
|
|
|
|
|
tableName,
|
|
|
|
|
onSaved,
|
|
|
|
|
}: BomDetailEditModalProps) {
|
|
|
|
|
const [formData, setFormData] = useState<Record<string, any>>({});
|
|
|
|
|
const [saving, setSaving] = useState(false);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (node && open) {
|
|
|
|
|
if (isRootNode) {
|
|
|
|
|
setFormData({
|
|
|
|
|
base_qty: node.base_qty || "",
|
|
|
|
|
unit: node.unit || "",
|
|
|
|
|
remark: node.remark || "",
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
setFormData({
|
|
|
|
|
quantity: node.quantity || "",
|
|
|
|
|
unit: node.unit || node.detail_unit || "",
|
|
|
|
|
process_type: node.process_type || "",
|
|
|
|
|
base_qty: node.base_qty || "",
|
|
|
|
|
loss_rate: node.loss_rate || "",
|
|
|
|
|
remark: node.remark || "",
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}, [node, open, isRootNode]);
|
|
|
|
|
|
|
|
|
|
const handleChange = (field: string, value: string) => {
|
|
|
|
|
setFormData((prev) => ({ ...prev, [field]: value }));
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleSave = async () => {
|
|
|
|
|
if (!node) return;
|
|
|
|
|
setSaving(true);
|
|
|
|
|
try {
|
|
|
|
|
const targetTable = isRootNode ? "bom" : tableName;
|
|
|
|
|
const realId = isRootNode ? node.id?.replace("__root_", "") : node.id;
|
|
|
|
|
await apiClient.put(`/table-management/tables/${targetTable}/${realId}`, formData);
|
|
|
|
|
onSaved?.();
|
|
|
|
|
onOpenChange(false);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("[BomDetailEdit] 저장 실패:", error);
|
|
|
|
|
} finally {
|
|
|
|
|
setSaving(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (!node) return null;
|
|
|
|
|
|
|
|
|
|
const itemCode = isRootNode
|
|
|
|
|
? node.child_item_code || node.item_code || node.bom_number || "-"
|
|
|
|
|
: node.child_item_code || "-";
|
|
|
|
|
const itemName = isRootNode
|
|
|
|
|
? node.child_item_name || node.item_name || "-"
|
|
|
|
|
: node.child_item_name || "-";
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
|
|
|
<DialogContent className="max-w-[95vw] sm:max-w-[500px]">
|
|
|
|
|
<DialogHeader>
|
|
|
|
|
<DialogTitle className="text-base sm:text-lg">
|
|
|
|
|
{isRootNode ? "BOM 헤더 수정" : "품목 수정"}
|
|
|
|
|
</DialogTitle>
|
|
|
|
|
<DialogDescription className="text-xs sm:text-sm">
|
|
|
|
|
{isRootNode
|
|
|
|
|
? "BOM 기본 정보를 수정합니다"
|
|
|
|
|
: "선택한 품목의 BOM 구성 정보를 수정합니다"}
|
|
|
|
|
</DialogDescription>
|
|
|
|
|
</DialogHeader>
|
|
|
|
|
|
|
|
|
|
<div className="space-y-3 sm:space-y-4">
|
|
|
|
|
<div className="grid grid-cols-2 gap-3">
|
|
|
|
|
<div>
|
|
|
|
|
<Label className="text-xs sm:text-sm">품목코드</Label>
|
|
|
|
|
<Input value={itemCode} disabled className="mt-1 h-8 text-xs sm:h-10 sm:text-sm" />
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<Label className="text-xs sm:text-sm">품목명</Label>
|
|
|
|
|
<Input value={itemName} disabled className="mt-1 h-8 text-xs sm:h-10 sm:text-sm" />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="grid grid-cols-2 gap-3">
|
|
|
|
|
<div>
|
|
|
|
|
<Label className="text-xs sm:text-sm">
|
|
|
|
|
{isRootNode ? "기준수량" : "구성수량"} *
|
|
|
|
|
</Label>
|
|
|
|
|
<Input
|
|
|
|
|
type="number"
|
|
|
|
|
value={isRootNode ? formData.base_qty : formData.quantity}
|
|
|
|
|
onChange={(e) => handleChange(isRootNode ? "base_qty" : "quantity", e.target.value)}
|
|
|
|
|
className="mt-1 h-8 text-xs sm:h-10 sm:text-sm"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<Label className="text-xs sm:text-sm">단위</Label>
|
|
|
|
|
<Input
|
|
|
|
|
value={formData.unit}
|
|
|
|
|
onChange={(e) => handleChange("unit", e.target.value)}
|
|
|
|
|
className="mt-1 h-8 text-xs sm:h-10 sm:text-sm"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{!isRootNode && (
|
|
|
|
|
<>
|
|
|
|
|
<div className="grid grid-cols-2 gap-3">
|
|
|
|
|
<div>
|
|
|
|
|
<Label className="text-xs sm:text-sm">공정</Label>
|
|
|
|
|
<Input
|
|
|
|
|
value={formData.process_type}
|
|
|
|
|
onChange={(e) => handleChange("process_type", e.target.value)}
|
|
|
|
|
placeholder="예: 조립공정"
|
|
|
|
|
className="mt-1 h-8 text-xs sm:h-10 sm:text-sm"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<Label className="text-xs sm:text-sm">로스율 (%)</Label>
|
|
|
|
|
<Input
|
|
|
|
|
type="number"
|
|
|
|
|
value={formData.loss_rate}
|
|
|
|
|
onChange={(e) => handleChange("loss_rate", e.target.value)}
|
|
|
|
|
className="mt-1 h-8 text-xs sm:h-10 sm:text-sm"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="grid grid-cols-2 gap-3">
|
|
|
|
|
<div>
|
|
|
|
|
<Label className="text-xs sm:text-sm">규격</Label>
|
|
|
|
|
<Input
|
|
|
|
|
value={node.child_specification || node.specification || "-"}
|
|
|
|
|
disabled
|
|
|
|
|
className="mt-1 h-8 text-xs sm:h-10 sm:text-sm"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<Label className="text-xs sm:text-sm">재질</Label>
|
|
|
|
|
<Input
|
|
|
|
|
value={node.child_material || node.material || "-"}
|
|
|
|
|
disabled
|
|
|
|
|
className="mt-1 h-8 text-xs sm:h-10 sm:text-sm"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<Label className="text-xs sm:text-sm">메모</Label>
|
|
|
|
|
<Textarea
|
|
|
|
|
value={formData.remark}
|
|
|
|
|
onChange={(e) => handleChange("remark", e.target.value)}
|
|
|
|
|
placeholder="비고 사항을 입력하세요"
|
|
|
|
|
className="mt-1 min-h-[60px] text-xs sm:text-sm"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<DialogFooter className="gap-2 sm:gap-0">
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
onClick={() => onOpenChange(false)}
|
|
|
|
|
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
|
|
|
|
|
>
|
|
|
|
|
취소
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
onClick={handleSave}
|
|
|
|
|
disabled={saving}
|
|
|
|
|
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
|
|
|
|
|
>
|
|
|
|
|
{saving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
|
|
|
|
저장
|
|
|
|
|
</Button>
|
|
|
|
|
</DialogFooter>
|
|
|
|
|
</DialogContent>
|
|
|
|
|
</Dialog>
|
|
|
|
|
);
|
|
|
|
|
}
|