/** * 발주 개소 목록 패널 * * - 개소 목록 테이블 * - 품목 추가 폼 * - 엑셀 업로드/다운로드 */ "use client"; import { useState, useCallback } from "react"; import { Plus, Upload, Download, Pencil, Trash2 } from "lucide-react"; import { toast } from "sonner"; import { Button } from "../ui/button"; import { Input } from "../ui/input"; import { NumberInput } from "../ui/number-input"; import { QuantityInput } from "../ui/quantity-input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "../ui/select"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "../ui/table"; import { DeleteConfirmDialog } from "../ui/confirm-dialog"; import type { LocationItem } from "./QuoteRegistrationV2"; import type { FinishedGoods } from "./actions"; import * as XLSX from "xlsx"; // ============================================================================= // 상수 // ============================================================================= // 가이드레일 설치 유형 const GUIDE_RAIL_TYPES = [ { value: "wall", label: "벽면형" }, { value: "floor", label: "측면형" }, ]; // 모터 전원 const MOTOR_POWERS = [ { value: "single", label: "단상(220V)" }, { value: "three", label: "삼상(380V)" }, ]; // 연동제어기 const CONTROLLERS = [ { value: "basic", label: "단독" }, { value: "smart", label: "연동" }, { value: "premium", label: "매립형-뒷박스포함" }, ]; // ============================================================================= // Props // ============================================================================= interface LocationListPanelProps { locations: LocationItem[]; selectedLocationId: string | null; onSelectLocation: (id: string) => void; onAddLocation: (location: Omit) => Promise; onDeleteLocation: (id: string) => void; onUpdateLocation: (locationId: string, updates: Partial) => void; onExcelUpload: (locations: Omit[]) => void; finishedGoods: FinishedGoods[]; disabled?: boolean; } // ============================================================================= // 컴포넌트 // ============================================================================= export function LocationListPanel({ locations, selectedLocationId, onSelectLocation, onAddLocation, onDeleteLocation, onUpdateLocation, onExcelUpload, finishedGoods, disabled = false, }: LocationListPanelProps) { // --------------------------------------------------------------------------- // 상태 // --------------------------------------------------------------------------- // 추가 폼 상태 const [formData, setFormData] = useState({ floor: "", code: "", openWidth: "", openHeight: "", productCode: "", quantity: "1", guideRailType: "wall", motorPower: "single", controller: "basic", }); // 삭제 확인 다이얼로그 const [deleteTarget, setDeleteTarget] = useState(null); // --------------------------------------------------------------------------- // 핸들러 // --------------------------------------------------------------------------- // 폼 필드 변경 const handleFormChange = useCallback((field: string, value: string) => { setFormData((prev) => ({ ...prev, [field]: value })); }, []); // 개소 추가 (BOM 계산 성공 시에만 폼 초기화) const handleAdd = useCallback(async () => { // 유효성 검사 if (!formData.floor || !formData.code) { toast.error("층과 부호를 입력해주세요."); return; } if (!formData.openWidth || !formData.openHeight) { toast.error("가로와 세로를 입력해주세요."); return; } if (!formData.productCode) { toast.error("제품을 선택해주세요."); return; } const product = finishedGoods.find((fg) => fg.item_code === formData.productCode); const newLocation: Omit = { floor: formData.floor, code: formData.code, openWidth: parseFloat(formData.openWidth) || 0, openHeight: parseFloat(formData.openHeight) || 0, productCode: formData.productCode, productName: product?.item_name || formData.productCode, quantity: parseInt(formData.quantity) || 1, guideRailType: formData.guideRailType, motorPower: formData.motorPower, controller: formData.controller, wingSize: 50, inspectionFee: 50000, }; // BOM 계산 성공 시에만 폼 초기화 const success = await onAddLocation(newLocation); if (success) { // 폼 초기화 (일부 필드 유지) setFormData((prev) => ({ ...prev, floor: "", code: "", openWidth: "", openHeight: "", quantity: "1", })); } }, [formData, finishedGoods, onAddLocation]); // 엑셀 양식 다운로드 const handleDownloadTemplate = useCallback(() => { const templateData = [ { 층: "1층", 부호: "FSS-01", 가로: 5000, 세로: 3000, 제품코드: "KSS01", 수량: 1, 가이드레일: "wall", 전원: "single", 제어기: "basic", }, ]; const ws = XLSX.utils.json_to_sheet(templateData); const wb = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb, ws, "개소목록"); // 컬럼 너비 설정 ws["!cols"] = [ { wch: 10 }, // 층 { wch: 12 }, // 부호 { wch: 10 }, // 가로 { wch: 10 }, // 세로 { wch: 15 }, // 제품코드 { wch: 8 }, // 수량 { wch: 12 }, // 가이드레일 { wch: 12 }, // 전원 { wch: 12 }, // 제어기 ]; XLSX.writeFile(wb, "견적_개소목록_양식.xlsx"); toast.success("엑셀 양식이 다운로드되었습니다."); }, []); // 엑셀 업로드 const handleFileUpload = useCallback( (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (!file) return; const reader = new FileReader(); reader.onload = (e) => { try { const data = new Uint8Array(e.target?.result as ArrayBuffer); const workbook = XLSX.read(data, { type: "array" }); const sheetName = workbook.SheetNames[0]; const worksheet = workbook.Sheets[sheetName]; const jsonData = XLSX.utils.sheet_to_json(worksheet); const parsedLocations: Omit[] = jsonData.map((row: any) => { const productCode = row["제품코드"] || ""; const product = finishedGoods.find((fg) => fg.item_code === productCode); return { floor: String(row["층"] || ""), code: String(row["부호"] || ""), openWidth: parseFloat(row["가로"]) || 0, openHeight: parseFloat(row["세로"]) || 0, productCode: productCode, productName: product?.item_name || productCode, quantity: parseInt(row["수량"]) || 1, guideRailType: row["가이드레일"] || "wall", motorPower: row["전원"] || "single", controller: row["제어기"] || "basic", wingSize: 50, inspectionFee: 50000, }; }); // 유효한 데이터만 필터링 const validLocations = parsedLocations.filter( (loc) => loc.floor && loc.code && loc.openWidth > 0 && loc.openHeight > 0 ); if (validLocations.length === 0) { toast.error("유효한 데이터가 없습니다. 양식을 확인해주세요."); return; } onExcelUpload(validLocations); } catch (error) { console.error("엑셀 파싱 오류:", error); toast.error("엑셀 파일을 읽는 중 오류가 발생했습니다."); } }; reader.readAsArrayBuffer(file); // 파일 입력 초기화 event.target.value = ""; }, [finishedGoods, onExcelUpload] ); // --------------------------------------------------------------------------- // 렌더링 // --------------------------------------------------------------------------- return (
{/* ① 입력 영역 (상단으로 이동) */} {!disabled && (
{/* 1행: 층, 부호, 가로, 세로, 제품코드, 수량 */}
handleFormChange("floor", e.target.value)} className="h-8 text-sm placeholder:text-gray-300" />
handleFormChange("code", e.target.value)} className="h-8 text-sm placeholder:text-gray-300" />
handleFormChange("openWidth", value?.toString() ?? "")} className="h-8 text-sm placeholder:text-gray-300" />
handleFormChange("openHeight", value?.toString() ?? "")} className="h-8 text-sm placeholder:text-gray-300" />
handleFormChange("quantity", value?.toString() ?? "")} className="h-8 text-sm" min={1} />
{/* 2행: 가이드레일, 전원, 제어기, 추가 버튼 */}
)} {/* 발주 개소 목록 헤더 */}

📋 발주 개소 목록 ({locations.length})

{/* 개소 목록 테이블 */}
부호 사이즈 제품 수량 {!disabled && } {locations.length === 0 ? ( 개소를 추가해주세요 ) : ( locations.map((loc) => ( onSelectLocation(loc.id)} > {loc.floor} {loc.code} {loc.openWidth}X{loc.openHeight} {loc.productCode} {loc.quantity} {!disabled && (
)}
)) )}
{/* 삭제 확인 다이얼로그 */} setDeleteTarget(null)} onConfirm={() => { if (deleteTarget) { onDeleteLocation(deleteTarget); setDeleteTarget(null); } }} title="개소 삭제" description="선택한 개소를 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다." />
); }