/** * 선택 개소 상세 정보 패널 * * - 제품 정보 (제품명, 오픈사이즈, 제작사이즈, 산출중량, 산출면적, 수량) * - 필수 설정 (가이드레일, 전원, 제어기) * - 탭: 본체(스크린/슬랫), 절곡품-가이드레일, 절곡품-케이스, 절곡품-하단마감재, 모터&제어기, 부자재 * - 탭별 품목 테이블 (각 탭마다 다른 컬럼 구조) */ "use client"; import { useState, useMemo, useEffect } from "react"; import { Package, Settings, Plus, Trash2, Loader2 } from "lucide-react"; import { getItemCategoryTree, type ItemCategoryNode } from "./actions"; import { Badge } from "../ui/badge"; 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 { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs"; import { ItemSearchModal } from "./ItemSearchModal"; import { LocationEditModal } from "./LocationEditModal"; import type { LocationItem } from "./QuoteRegistrationV2"; import type { FinishedGoods } from "./actions"; import type { BomCalculationResultItem } from "./types"; // 납품길이 옵션 const DELIVERY_LENGTH_OPTIONS = [ { value: "3000", label: "3000" }, { value: "4000", label: "4000" }, { value: "5000", label: "5000" }, { value: "6000", label: "6000" }, ]; // 빈 BOM 아이템 생성 함수 (동적 탭에 맞게) function createEmptyBomItems(tabs: TabDefinition[]): Record { const result: Record = {}; tabs.forEach((tab) => { result[tab.value] = []; }); return result; } // ============================================================================= // 상수 // ============================================================================= // 가이드레일 설치 유형 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: "매립형-뒷박스포함" }, ]; // 탭 인터페이스 정의 interface TabDefinition { value: string; // 카테고리 code (예: BODY, BENDING_GUIDE) label: string; // 표시 이름 categoryId: number; // 카테고리 ID parentCode?: string; // 상위 카테고리 코드 } /** * 카테고리 트리를 탭 배열로 변환 * - 본체, 모터&제어기, 부자재 → 1depth 탭 * - 절곡품 → 하위 카테고리를 각각 탭으로 (가이드레일, 케이스, 하단마감재) */ function convertCategoryTreeToTabs(categories: ItemCategoryNode[]): TabDefinition[] { const tabs: TabDefinition[] = []; categories.forEach((category) => { if (category.code === "BENDING") { // 절곡품: 하위 카테고리를 탭으로 변환 category.children.forEach((subCategory) => { tabs.push({ value: subCategory.code, label: `절곡품 - ${subCategory.name}`, categoryId: subCategory.id, parentCode: category.code, }); }); } else { // 본체, 모터&제어기, 부자재: 1depth 탭 tabs.push({ value: category.code, label: category.name, categoryId: category.id, }); } }); return tabs; } // 기본 탭 정의 (API 로딩 전 또는 실패 시 fallback) const DEFAULT_TABS: TabDefinition[] = [ { value: "BODY", label: "본체", categoryId: 0 }, { value: "BENDING_GUIDE", label: "절곡품 - 가이드레일", categoryId: 0, parentCode: "BENDING" }, { value: "BENDING_CASE", label: "절곡품 - 케이스", categoryId: 0, parentCode: "BENDING" }, { value: "BENDING_BOTTOM", label: "절곡품 - 하단마감재", categoryId: 0, parentCode: "BENDING" }, { value: "MOTOR_CTRL", label: "모터 & 제어기", categoryId: 0 }, { value: "ACCESSORY", label: "부자재", categoryId: 0 }, ]; // ============================================================================= // Props // ============================================================================= interface LocationDetailPanelProps { location: LocationItem | null; onUpdateLocation: (locationId: string, updates: Partial) => void; finishedGoods: FinishedGoods[]; disabled?: boolean; } // ============================================================================= // 컴포넌트 // ============================================================================= export function LocationDetailPanel({ location, onUpdateLocation, finishedGoods, disabled = false, }: LocationDetailPanelProps) { // --------------------------------------------------------------------------- // 상태 // --------------------------------------------------------------------------- const [activeTab, setActiveTab] = useState("BODY"); const [itemSearchOpen, setItemSearchOpen] = useState(false); const [editModalOpen, setEditModalOpen] = useState(false); const [itemCategories, setItemCategories] = useState([]); const [categoriesLoading, setCategoriesLoading] = useState(true); // --------------------------------------------------------------------------- // 카테고리 로드 // --------------------------------------------------------------------------- useEffect(() => { async function loadCategories() { try { setCategoriesLoading(true); const result = await getItemCategoryTree(); if (result.success && result.data) { setItemCategories(result.data); } else { console.error("[카테고리 로드 실패]", result.error); } } catch (error) { console.error("[카테고리 로드 에러]", error); } finally { setCategoriesLoading(false); } } loadCategories(); }, []); // --------------------------------------------------------------------------- // 탭 정의 (카테고리 기반 동적 생성) // --------------------------------------------------------------------------- const detailTabs = useMemo(() => { if (itemCategories.length === 0) { return DEFAULT_TABS; } return convertCategoryTreeToTabs(itemCategories); }, [itemCategories]); // --------------------------------------------------------------------------- // 계산된 값 // --------------------------------------------------------------------------- // 제품 정보 const product = useMemo(() => { if (!location?.productCode) return null; return finishedGoods.find((fg) => fg.item_code === location.productCode); }, [location?.productCode, finishedGoods]); // BOM 아이템을 탭별로 분류 (카테고리 코드 기반) const bomItemsByTab = useMemo(() => { // bomResult가 없으면 빈 배열 반환 if (!location?.bomResult?.items) { return createEmptyBomItems(detailTabs); } const items = location.bomResult.items; const result: Record = {}; // 탭별 빈 배열 초기화 detailTabs.forEach((tab) => { result[tab.value] = []; }); // 카테고리 코드 → 탭 value 매핑 생성 // (절곡품 하위 카테고리 매핑 포함) const categoryCodeToTab: Record = {}; itemCategories.forEach((category) => { if (category.code === "BENDING") { // 절곡품: 하위 카테고리별로 탭 매핑 category.children.forEach((subCategory) => { categoryCodeToTab[subCategory.code] = subCategory.code; // 하위의 세부 카테고리도 매핑 subCategory.children?.forEach((detailCategory) => { categoryCodeToTab[detailCategory.code] = subCategory.code; }); }); } else { // 일반 카테고리 categoryCodeToTab[category.code] = category.code; // 하위 카테고리도 상위 탭에 매핑 category.children?.forEach((child) => { categoryCodeToTab[child.code] = category.code; }); } }); items.forEach((item) => { // 1. category_code로 분류 (우선) const categoryCode = (item as { category_code?: string }).category_code?.toUpperCase() || ""; const mappedTab = categoryCodeToTab[categoryCode]; if (mappedTab && result[mappedTab]) { result[mappedTab].push(item); return; } // 2. process_group_key로 분류 (legacy fallback) const processGroupKey = (item as { process_group_key?: string }).process_group_key?.toUpperCase() || ""; // 기존 process_group_key → 새 카테고리 코드 매핑 const legacyMapping: Record = { "SCREEN": "BODY", "ASSEMBLY": "BODY", "BENDING": "BENDING_GUIDE", // 기본적으로 가이드레일에 배치 "STEEL": "BENDING_CASE", "ELECTRIC": "BENDING_BOTTOM", "MOTOR": "MOTOR_CTRL", "ACCESSORY": "ACCESSORY", }; const legacyTab = legacyMapping[processGroupKey]; if (legacyTab && result[legacyTab]) { result[legacyTab].push(item); return; } // 3. process_group (한글명) 기반 분류 (최종 fallback) const processGroup = item.process_group?.toLowerCase() || ""; if (processGroup.includes("본체") || processGroup.includes("스크린") || processGroup.includes("슬랫") || processGroup.includes("조립")) { result["BODY"]?.push(item); } else if (processGroup.includes("가이드") || processGroup.includes("레일")) { result["BENDING_GUIDE"]?.push(item); } else if (processGroup.includes("케이스") || processGroup.includes("철재")) { result["BENDING_CASE"]?.push(item); } else if (processGroup.includes("하단") || processGroup.includes("마감")) { result["BENDING_BOTTOM"]?.push(item); } else if (processGroup.includes("모터") || processGroup.includes("제어기")) { result["MOTOR_CTRL"]?.push(item); } else if (processGroup.includes("부자재")) { result["ACCESSORY"]?.push(item); } else { // 기타 항목은 본체에 포함 result["BODY"]?.push(item); } }); return result; }, [location?.bomResult?.items, detailTabs, itemCategories]); // 탭별 소계 const tabSubtotals = useMemo(() => { const result: Record = {}; Object.entries(bomItemsByTab).forEach(([tab, items]) => { result[tab] = items.reduce((sum: number, item: { total_price?: number }) => sum + (item.total_price || 0), 0); }); return result; }, [bomItemsByTab]); // --------------------------------------------------------------------------- // 핸들러 // --------------------------------------------------------------------------- const handleFieldChange = (field: keyof LocationItem, value: string | number) => { if (!location || disabled) return; onUpdateLocation(location.id, { [field]: value }); }; // --------------------------------------------------------------------------- // 렌더링: 빈 상태 // --------------------------------------------------------------------------- if (!location) { return (

개소를 선택해주세요

왼쪽 목록에서 개소를 선택하면 상세 정보가 표시됩니다

); } // --------------------------------------------------------------------------- // 렌더링: 상세 정보 // --------------------------------------------------------------------------- return (
{/* 헤더 */}

{location.floor} / {location.code}

제품명: {location.productCode} {location.bomResult && ( 산출완료 )}
{/* 제품 정보 */}
{/* 오픈사이즈 */}
오픈사이즈
handleFieldChange("openWidth", value ?? 0)} disabled={disabled} className="w-24 h-8 text-center font-bold" /> × handleFieldChange("openHeight", value ?? 0)} disabled={disabled} className="w-24 h-8 text-center font-bold" /> {!disabled && ( )}
{/* 제작사이즈, 산출중량, 산출면적, 수량 */}
제작사이즈

{location.manufactureWidth || location.openWidth + 280} × {location.manufactureHeight || location.openHeight + 280}

산출중량

{location.weight?.toFixed(1) || "-"} kg

산출면적

{location.area?.toFixed(1) || "-"}

수량 handleFieldChange("quantity", value ?? 1)} disabled={disabled} className="w-24 h-7 text-center font-semibold" min={1} />
{/* 필수 설정 (읽기 전용) */}

필수 설정

{GUIDE_RAIL_TYPES.find(t => t.value === location.guideRailType)?.label || location.guideRailType}
{MOTOR_POWERS.find(p => p.value === location.motorPower)?.label || location.motorPower}
{CONTROLLERS.find(c => c.value === location.controller)?.label || location.controller}
{/* 탭 및 품목 테이블 */}
{/* 탭 목록 - 스크롤 가능 */}
{categoriesLoading ? (
카테고리 로딩 중...
) : ( {detailTabs.map((tab) => ( {tab.label} ))} )}
{/* 동적 탭 콘텐츠 렌더링 */} {detailTabs.map((tab) => { const items = bomItemsByTab[tab.value] || []; const isBendingTab = tab.parentCode === "BENDING"; const isMotorTab = tab.value === "MOTOR_CTRL"; const isAccessoryTab = tab.value === "ACCESSORY"; return (
품목명 {/* 본체: 제작사이즈 */} {!isBendingTab && !isMotorTab && !isAccessoryTab && ( 제작사이즈 )} {/* 절곡품: 재질, 규격, 납품길이 */} {isBendingTab && ( <> 재질 규격 납품길이 )} {/* 모터: 유형, 사양 */} {isMotorTab && ( <> 유형 사양 )} {/* 부자재: 규격, 납품길이 */} {isAccessoryTab && ( <> 규격 납품길이 )} 수량 작업 {items.map((item: any, index: number) => ( {item.item_name} {/* 본체: 제작사이즈 */} {!isBendingTab && !isMotorTab && !isAccessoryTab && ( {item.manufacture_size || "-"} )} {/* 절곡품: 재질, 규격, 납품길이 */} {isBendingTab && ( <> {item.material || "-"} {item.spec || "-"} )} {/* 모터: 유형, 사양 */} {isMotorTab && ( <> {item.type || "-"} {item.spec || "-"} )} {/* 부자재: 규격, 납품길이 */} {isAccessoryTab && ( <> {item.spec || "-"} )} {}} className="w-16 h-8 text-center" min={1} disabled={disabled} /> ))}
{/* 품목 추가 버튼 + 안내 */}
💡 금액은 아래 견적금액요약에서 확인하세요
); })}
{/* 금액 안내 */} {!location.bomResult && (
💡 금액은 아래 견적금액요약에서 확인하세요
)} {/* 품목 검색 모달 */} { if (!location) return; // 현재 탭 정보 가져오기 const currentTab = detailTabs.find((t) => t.value === activeTab); const categoryCode = activeTab; // 카테고리 코드를 직접 사용 const categoryLabel = currentTab?.label || activeTab; // 새 품목 생성 (카테고리 코드 포함) const newItem: BomCalculationResultItem & { category_code?: string; is_manual?: boolean } = { item_code: item.code, item_name: item.name, specification: item.specification || "", unit: "EA", quantity: 1, unit_price: 0, total_price: 0, process_group: categoryLabel, category_code: categoryCode, // 새 카테고리 코드 사용 is_manual: true, // 수동 추가 품목 표시 }; // 기존 bomResult 가져오기 const existingBomResult = location.bomResult || { finished_goods: { code: location.productCode || "", name: location.productName || "" }, subtotals: {}, grouped_items: {}, grand_total: 0, items: [], }; // 기존 items에 새 아이템 추가 const existingItems = existingBomResult.items || []; const updatedItems = [...existingItems, newItem]; // subtotals 업데이트 (해당 카테고리의 count, subtotal 증가) const existingSubtotals = existingBomResult.subtotals || {}; const rawCategorySubtotal = existingSubtotals[categoryCode]; const categorySubtotal = (typeof rawCategorySubtotal === 'object' && rawCategorySubtotal !== null) ? rawCategorySubtotal : { name: categoryLabel, count: 0, subtotal: 0 }; const updatedSubtotals = { ...existingSubtotals, [categoryCode]: { name: categoryLabel, count: (categorySubtotal.count || 0) + 1, subtotal: (categorySubtotal.subtotal || 0) + (newItem.total_price || 0), }, }; // grouped_items 업데이트 (해당 카테고리의 items 배열에 추가) const existingGroupedItems = existingBomResult.grouped_items || {}; const categoryGroupedItems = existingGroupedItems[categoryCode] || { items: [] }; const updatedGroupedItems = { ...existingGroupedItems, [categoryCode]: { ...categoryGroupedItems, items: [...(categoryGroupedItems.items || []), newItem], }, }; // grand_total 업데이트 const updatedGrandTotal = (existingBomResult.grand_total || 0) + (newItem.total_price || 0); const updatedBomResult = { ...existingBomResult, items: updatedItems, subtotals: updatedSubtotals, grouped_items: updatedGroupedItems, grand_total: updatedGrandTotal, }; // location 업데이트 onUpdateLocation(location.id, { bomResult: updatedBomResult }); console.log(`[품목 추가] ${item.code} - ${item.name} → ${categoryLabel} (${categoryCode})`); }} tabLabel={detailTabs.find((t) => t.value === activeTab)?.label} /> {/* 개소 정보 수정 모달 */} { onUpdateLocation(locationId, updates); }} />
); }