"use client"; import React, { useState, useEffect, useCallback } from "react"; import { Search, Plus, Trash2, Edit, ListOrdered, Package, Star, X } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Checkbox } from "@/components/ui/checkbox"; import { cn } from "@/lib/utils"; import { useToast } from "@/hooks/use-toast"; import { ItemRoutingComponentProps, ColumnDef } from "./types"; import { useItemRouting } from "./hooks/useItemRouting"; const DEFAULT_ITEM_COLS: ColumnDef[] = [ { name: "item_name", label: "품명" }, { name: "item_code", label: "품번", width: 100 }, ]; export function ItemRoutingComponent({ config: configProp, isPreview, screenId, }: ItemRoutingComponentProps) { const { toast } = useToast(); const resolvedConfig = React.useMemo(() => { if (configProp?.itemListMode === "registered" && !configProp?.screenCode && screenId) { return { ...configProp, screenCode: `screen_${screenId}` }; } return configProp; }, [configProp, screenId]); const { config, items, allItems, versions, details, loading, selectedItemCode, selectedItemName, selectedVersionId, isRegisteredMode, fetchItems, fetchRegisteredItems, fetchAllItems, registerItemsBatch, unregisterItem, selectItem, selectVersion, refreshVersions, refreshDetails, deleteDetail, deleteVersion, setDefaultVersion, unsetDefaultVersion, } = useItemRouting(resolvedConfig || {}); const [searchText, setSearchText] = useState(""); const [deleteTarget, setDeleteTarget] = useState<{ type: "version" | "detail"; id: string; name: string; } | null>(null); // 품목 추가 다이얼로그 const [addDialogOpen, setAddDialogOpen] = useState(false); const [addSearchText, setAddSearchText] = useState(""); const [selectedAddItems, setSelectedAddItems] = useState>(new Set()); const [addLoading, setAddLoading] = useState(false); const itemDisplayCols = config.itemDisplayColumns?.length ? config.itemDisplayColumns : DEFAULT_ITEM_COLS; const modalDisplayCols = config.modalDisplayColumns?.length ? config.modalDisplayColumns : DEFAULT_ITEM_COLS; // 초기 로딩 const mountedRef = React.useRef(false); useEffect(() => { if (!mountedRef.current) { mountedRef.current = true; if (isRegisteredMode) fetchRegisteredItems(); else fetchItems(); } }, [fetchItems, fetchRegisteredItems, isRegisteredMode]); // 모달 저장 성공 감지 const refreshVersionsRef = React.useRef(refreshVersions); const refreshDetailsRef = React.useRef(refreshDetails); refreshVersionsRef.current = refreshVersions; refreshDetailsRef.current = refreshDetails; useEffect(() => { const h = () => { refreshVersionsRef.current(); refreshDetailsRef.current(); }; window.addEventListener("saveSuccessInModal", h); return () => window.removeEventListener("saveSuccessInModal", h); }, []); // 검색 const handleSearch = useCallback(() => { if (isRegisteredMode) fetchRegisteredItems(searchText || undefined); else fetchItems(searchText || undefined); }, [fetchItems, fetchRegisteredItems, isRegisteredMode, searchText]); // ──── 품목 추가 모달 ──── const handleOpenAddDialog = useCallback(() => { setAddSearchText(""); setSelectedAddItems(new Set()); setAddDialogOpen(true); fetchAllItems(); }, [fetchAllItems]); const handleToggleAddItem = useCallback((itemId: string) => { setSelectedAddItems((prev) => { const next = new Set(prev); next.has(itemId) ? next.delete(itemId) : next.add(itemId); return next; }); }, []); const handleConfirmAdd = useCallback(async () => { if (selectedAddItems.size === 0) return; setAddLoading(true); const itemList = allItems .filter((item) => selectedAddItems.has(item.id)) .map((item) => ({ itemId: item.id, itemCode: item.item_code || item[config.dataSource.itemCodeColumn] || "", })); const success = await registerItemsBatch(itemList); setAddLoading(false); if (success) { toast({ title: `${itemList.length}개 품목이 등록되었습니다` }); setAddDialogOpen(false); } else { toast({ title: "품목 등록 실패", variant: "destructive" }); } }, [selectedAddItems, allItems, config.dataSource.itemCodeColumn, registerItemsBatch, toast]); const handleUnregisterItem = useCallback( async (registeredId: string, itemName: string) => { const success = await unregisterItem(registeredId); if (success) toast({ title: `${itemName} 등록 해제됨` }); else toast({ title: "등록 해제 실패", variant: "destructive" }); }, [unregisterItem, toast] ); // ──── 기존 핸들러 ──── const handleAddVersion = useCallback(() => { if (!selectedItemCode) { toast({ title: "품목을 먼저 선택해주세요", variant: "destructive" }); return; } const sid = config.modals.versionAddScreenId; if (!sid) return; window.dispatchEvent(new CustomEvent("openScreenModal", { detail: { screenId: sid, urlParams: { mode: "add", tableName: config.dataSource.routingVersionTable }, splitPanelParentData: { [config.dataSource.routingVersionFkColumn]: selectedItemCode } }, })); }, [selectedItemCode, config, toast]); const handleAddProcess = useCallback(() => { if (!selectedVersionId) { toast({ title: "라우팅 버전을 먼저 선택해주세요", variant: "destructive" }); return; } const sid = config.modals.processAddScreenId; if (!sid) return; window.dispatchEvent(new CustomEvent("openScreenModal", { detail: { screenId: sid, urlParams: { mode: "add", tableName: config.dataSource.routingDetailTable }, splitPanelParentData: { [config.dataSource.routingDetailFkColumn]: selectedVersionId } }, })); }, [selectedVersionId, config, toast]); const handleEditProcess = useCallback( (detail: Record) => { const sid = config.modals.processEditScreenId; if (!sid) return; window.dispatchEvent(new CustomEvent("openScreenModal", { detail: { screenId: sid, urlParams: { mode: "edit", tableName: config.dataSource.routingDetailTable }, editData: detail }, })); }, [config] ); const handleToggleDefault = useCallback( async (versionId: string, currentIsDefault: boolean) => { const success = currentIsDefault ? await unsetDefaultVersion(versionId) : await setDefaultVersion(versionId); if (success) toast({ title: currentIsDefault ? "기본 버전이 해제되었습니다" : "기본 버전으로 설정되었습니다" }); else toast({ title: "기본 버전 변경 실패", variant: "destructive" }); }, [setDefaultVersion, unsetDefaultVersion, toast] ); const handleConfirmDelete = useCallback(async () => { if (!deleteTarget) return; const success = deleteTarget.type === "version" ? await deleteVersion(deleteTarget.id) : await deleteDetail(deleteTarget.id); toast({ title: success ? `${deleteTarget.name} 삭제 완료` : "삭제 실패", variant: success ? undefined : "destructive" }); setDeleteTarget(null); }, [deleteTarget, deleteVersion, deleteDetail, toast]); const splitRatio = config.splitRatio || 40; const registeredItemIds = React.useMemo(() => new Set(items.map((i) => i.id)), [items]); // ──── 셀 값 추출 헬퍼 ──── const getCellValue = (item: Record, colName: string) => { return item[colName] ?? item[`item_${colName}`] ?? "-"; }; if (isPreview) { return (

품목별 라우팅 관리

{isRegisteredMode ? "등록 품목 모드" : "전체 품목 모드"}

); } return (
{/* ════ 좌측 패널: 품목 목록 (테이블) ════ */}

{config.leftPanelTitle || "품목 목록"} {isRegisteredMode && ( (등록 모드) )}

{isRegisteredMode && !config.readonly && ( )}
setSearchText(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") handleSearch(); }} placeholder="품목명/품번 검색" className="h-8 text-xs" />
{/* 품목 테이블 */}
{items.length === 0 ? (

{loading ? "로딩 중..." : isRegisteredMode ? "등록된 품목이 없습니다" : "품목이 없습니다"}

{isRegisteredMode && !loading && !config.readonly && ( )}
) : ( {itemDisplayCols.map((col) => ( {col.label} ))} {isRegisteredMode && !config.readonly && ( )} {items.map((item) => { const itemCode = item[config.dataSource.itemCodeColumn] || item.item_code || item.item_number; const itemName = item[config.dataSource.itemNameColumn] || item.item_name; const isSelected = selectedItemCode === itemCode; return ( selectItem(itemCode, itemName)}> {itemDisplayCols.map((col) => ( {getCellValue(item, col.name)} ))} {isRegisteredMode && !config.readonly && item.registered_id && ( )} ); })}
)}
{/* ════ 우측 패널: 버전 + 공정 ════ */}
{selectedItemCode ? ( <>

{selectedItemName}

{selectedItemCode}

{!config.readonly && ( )}
{versions.length > 0 ? (
버전: {versions.map((ver) => { const isActive = selectedVersionId === ver.id; const isDefault = ver.is_default === true; return (
selectVersion(ver.id)}> {isDefault && } {ver[config.dataSource.routingVersionNameColumn] || ver.version_name || ver.id} {!config.readonly && ( <> )}
); })}
) : (

라우팅 버전이 없습니다. 버전을 추가해주세요.

)} {selectedVersionId ? (

{config.rightPanelTitle || "공정 순서"} ({details.length}건)

{!config.readonly && ( )}
{details.length === 0 ? (

{loading ? "로딩 중..." : "등록된 공정이 없습니다"}

) : ( {config.processColumns.map((col) => ( {col.label} ))} {!config.readonly && 관리} {details.map((detail) => ( {config.processColumns.map((col) => { let v = detail[col.name]; if (v == null) { const ak = Object.keys(detail).find((k) => k.endsWith(`_${col.name}`)); if (ak) v = detail[ak]; } return ( {v ?? "-"} ); })} {!config.readonly && (
)}
))}
)}
) : ( versions.length > 0 && (

라우팅 버전을 선택해주세요

) )} ) : (

좌측에서 품목을 선택하세요

품목을 선택하면 라우팅 버전별 공정 순서를 관리할 수 있습니다

)}
{/* ════ 삭제 확인 ════ */} setDeleteTarget(null)}> 삭제 확인 {deleteTarget?.name}을(를) 삭제하시겠습니까? {deleteTarget?.type === "version" && (<>
해당 버전에 포함된 모든 공정 정보도 함께 삭제됩니다.)}
취소 삭제
{/* ════ 품목 추가 다이얼로그 (테이블 형태 + 검색) ════ */} 품목 추가 좌측 목록에 표시할 품목을 선택하세요 {(config.itemFilterConditions?.length ?? 0) > 0 && ( (필터 {config.itemFilterConditions!.length}건 적용됨) )}
setAddSearchText(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") fetchAllItems(addSearchText || undefined); }} placeholder="품목명/품번 검색" className="h-8 text-xs sm:h-10 sm:text-sm" />
{allItems.length === 0 ? (

품목이 없습니다

) : ( {modalDisplayCols.map((col) => ( {col.label} ))} 상태 {allItems.map((item) => { const isAlreadyRegistered = registeredItemIds.has(item.id); const isChecked = selectedAddItems.has(item.id); return ( { if (!isAlreadyRegistered) handleToggleAddItem(item.id); }}> {modalDisplayCols.map((col) => ( {getCellValue(item, col.name)} ))} {isAlreadyRegistered && ( 등록됨 )} ); })}
)}
{selectedAddItems.size > 0 && (

{selectedAddItems.size}개 선택됨

)}
); }