551 lines
27 KiB
TypeScript
551 lines
27 KiB
TypeScript
"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<Set<string>>(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<string, any>) => {
|
|
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<string, any>, colName: string) => {
|
|
return item[colName] ?? item[`item_${colName}`] ?? "-";
|
|
};
|
|
|
|
if (isPreview) {
|
|
return (
|
|
<div className="flex h-full items-center justify-center rounded-lg border-2 border-dashed border-muted-foreground/20 bg-muted/10 p-4">
|
|
<div className="text-center">
|
|
<ListOrdered className="mx-auto mb-2 h-8 w-8 text-muted-foreground/50" />
|
|
<p className="text-sm font-medium text-muted-foreground">품목별 라우팅 관리</p>
|
|
<p className="mt-1 text-xs text-muted-foreground/70">
|
|
{isRegisteredMode ? "등록 품목 모드" : "전체 품목 모드"}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="flex h-full flex-col overflow-hidden rounded-lg border bg-background">
|
|
<div className="flex flex-1 overflow-hidden">
|
|
{/* ════ 좌측 패널: 품목 목록 (테이블) ════ */}
|
|
<div style={{ width: `${splitRatio}%` }} className="flex shrink-0 flex-col overflow-hidden border-r">
|
|
<div className="flex items-center justify-between border-b px-3 py-2">
|
|
<h3 className="text-sm font-semibold">
|
|
{config.leftPanelTitle || "품목 목록"}
|
|
{isRegisteredMode && (
|
|
<span className="ml-1.5 text-[10px] font-normal text-muted-foreground">(등록 모드)</span>
|
|
)}
|
|
</h3>
|
|
{isRegisteredMode && !config.readonly && (
|
|
<Button variant="outline" size="sm" className="h-6 gap-1 text-[10px]" onClick={handleOpenAddDialog}>
|
|
<Plus className="h-3 w-3" /> 품목 추가
|
|
</Button>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex gap-1.5 border-b px-3 py-2">
|
|
<Input value={searchText} onChange={(e) => setSearchText(e.target.value)}
|
|
onKeyDown={(e) => { if (e.key === "Enter") handleSearch(); }}
|
|
placeholder="품목명/품번 검색" className="h-8 text-xs" />
|
|
<Button variant="outline" size="icon" className="h-8 w-8 shrink-0" onClick={handleSearch}>
|
|
<Search className="h-3.5 w-3.5" />
|
|
</Button>
|
|
</div>
|
|
|
|
{/* 품목 테이블 */}
|
|
<div className="flex-1 overflow-auto">
|
|
{items.length === 0 ? (
|
|
<div className="flex h-full flex-col items-center justify-center gap-2 p-4">
|
|
<p className="text-xs text-muted-foreground">
|
|
{loading ? "로딩 중..." : isRegisteredMode ? "등록된 품목이 없습니다" : "품목이 없습니다"}
|
|
</p>
|
|
{isRegisteredMode && !loading && !config.readonly && (
|
|
<Button variant="outline" size="sm" className="h-7 gap-1 text-xs" onClick={handleOpenAddDialog}>
|
|
<Plus className="h-3 w-3" /> 품목 추가하기
|
|
</Button>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
{itemDisplayCols.map((col) => (
|
|
<TableHead key={col.name}
|
|
style={{ width: col.width ? `${col.width}px` : undefined }}
|
|
className={cn("text-[11px] py-1.5", col.align === "center" && "text-center", col.align === "right" && "text-right")}>
|
|
{col.label}
|
|
</TableHead>
|
|
))}
|
|
{isRegisteredMode && !config.readonly && (
|
|
<TableHead className="w-[36px] py-1.5" />
|
|
)}
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{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 (
|
|
<TableRow key={item.registered_id || item.id}
|
|
className={cn("cursor-pointer group", isSelected && "bg-primary/10")}
|
|
onClick={() => selectItem(itemCode, itemName)}>
|
|
{itemDisplayCols.map((col) => (
|
|
<TableCell key={col.name}
|
|
className={cn("text-xs py-1.5", col.align === "center" && "text-center", col.align === "right" && "text-right")}>
|
|
{getCellValue(item, col.name)}
|
|
</TableCell>
|
|
))}
|
|
{isRegisteredMode && !config.readonly && item.registered_id && (
|
|
<TableCell className="py-1.5 text-center">
|
|
<Button variant="ghost" size="icon"
|
|
className="h-5 w-5 opacity-0 group-hover:opacity-100 transition-opacity"
|
|
onClick={(e) => { e.stopPropagation(); handleUnregisterItem(item.registered_id, itemName); }}
|
|
title="등록 해제">
|
|
<X className="h-3 w-3 text-muted-foreground hover:text-destructive" />
|
|
</Button>
|
|
</TableCell>
|
|
)}
|
|
</TableRow>
|
|
);
|
|
})}
|
|
</TableBody>
|
|
</Table>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* ════ 우측 패널: 버전 + 공정 ════ */}
|
|
<div className="flex flex-1 flex-col overflow-hidden">
|
|
{selectedItemCode ? (
|
|
<>
|
|
<div className="flex items-center justify-between border-b px-4 py-2">
|
|
<div>
|
|
<h3 className="text-sm font-semibold">{selectedItemName}</h3>
|
|
<p className="text-xs text-muted-foreground">{selectedItemCode}</p>
|
|
</div>
|
|
{!config.readonly && (
|
|
<Button variant="outline" size="sm" className="h-7 gap-1 text-xs" onClick={handleAddVersion}>
|
|
<Plus className="h-3 w-3" /> {config.versionAddButtonText || "+ 라우팅 버전 추가"}
|
|
</Button>
|
|
)}
|
|
</div>
|
|
|
|
{versions.length > 0 ? (
|
|
<div className="flex flex-wrap items-center gap-1.5 border-b px-4 py-2">
|
|
<span className="mr-1 text-xs text-muted-foreground">버전:</span>
|
|
{versions.map((ver) => {
|
|
const isActive = selectedVersionId === ver.id;
|
|
const isDefault = ver.is_default === true;
|
|
return (
|
|
<div key={ver.id} className="flex items-center gap-0.5">
|
|
<Badge variant={isActive ? "default" : "outline"}
|
|
className={cn("cursor-pointer px-2.5 py-0.5 text-xs transition-colors",
|
|
isActive && "bg-primary text-primary-foreground",
|
|
isDefault && !isActive && "border-amber-400 bg-amber-50 text-amber-700")}
|
|
onClick={() => selectVersion(ver.id)}>
|
|
{isDefault && <Star className="mr-1 h-3 w-3 fill-current" />}
|
|
{ver[config.dataSource.routingVersionNameColumn] || ver.version_name || ver.id}
|
|
</Badge>
|
|
{!config.readonly && (
|
|
<>
|
|
<Button variant="ghost" size="icon"
|
|
className={cn("h-5 w-5", isDefault ? "text-amber-500 hover:text-amber-600" : "text-muted-foreground hover:text-amber-500")}
|
|
onClick={(e) => { e.stopPropagation(); handleToggleDefault(ver.id, isDefault); }}
|
|
title={isDefault ? "기본 버전 해제" : "기본 버전으로 설정"}>
|
|
<Star className={cn("h-3 w-3", isDefault && "fill-current")} />
|
|
</Button>
|
|
<Button variant="ghost" size="icon" className="h-5 w-5 text-muted-foreground hover:text-destructive"
|
|
onClick={(e) => { e.stopPropagation(); setDeleteTarget({ type: "version", id: ver.id, name: ver.version_name || ver.id }); }}>
|
|
<Trash2 className="h-3 w-3" />
|
|
</Button>
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
) : (
|
|
<div className="border-b px-4 py-3 text-center">
|
|
<p className="text-xs text-muted-foreground">라우팅 버전이 없습니다. 버전을 추가해주세요.</p>
|
|
</div>
|
|
)}
|
|
|
|
{selectedVersionId ? (
|
|
<div className="flex flex-1 flex-col overflow-hidden">
|
|
<div className="flex items-center justify-between px-4 py-2">
|
|
<h4 className="text-xs font-medium text-muted-foreground">
|
|
{config.rightPanelTitle || "공정 순서"} ({details.length}건)
|
|
</h4>
|
|
{!config.readonly && (
|
|
<Button variant="outline" size="sm" className="h-7 gap-1 text-xs" onClick={handleAddProcess}>
|
|
<Plus className="h-3 w-3" /> {config.processAddButtonText || "+ 공정 추가"}
|
|
</Button>
|
|
)}
|
|
</div>
|
|
<div className="flex-1 overflow-auto px-4 pb-4">
|
|
{details.length === 0 ? (
|
|
<div className="flex h-32 items-center justify-center">
|
|
<p className="text-xs text-muted-foreground">{loading ? "로딩 중..." : "등록된 공정이 없습니다"}</p>
|
|
</div>
|
|
) : (
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
{config.processColumns.map((col) => (
|
|
<TableHead key={col.name}
|
|
style={{ width: col.width ? `${col.width}px` : undefined }}
|
|
className={cn("text-xs", col.align === "center" && "text-center", col.align === "right" && "text-right")}>
|
|
{col.label}
|
|
</TableHead>
|
|
))}
|
|
{!config.readonly && <TableHead className="w-[80px] text-center text-xs">관리</TableHead>}
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{details.map((detail) => (
|
|
<TableRow key={detail.id}>
|
|
{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 (
|
|
<TableCell key={col.name}
|
|
className={cn("text-xs", col.align === "center" && "text-center", col.align === "right" && "text-right")}>
|
|
{v ?? "-"}
|
|
</TableCell>
|
|
);
|
|
})}
|
|
{!config.readonly && (
|
|
<TableCell className="text-center">
|
|
<div className="flex items-center justify-center gap-1">
|
|
<Button variant="ghost" size="icon" className="h-6 w-6" onClick={() => handleEditProcess(detail)}>
|
|
<Edit className="h-3 w-3" />
|
|
</Button>
|
|
<Button variant="ghost" size="icon" className="h-6 w-6 text-destructive hover:text-destructive"
|
|
onClick={() => setDeleteTarget({ type: "detail", id: detail.id, name: `공정 ${detail.seq_no || detail.id}` })}>
|
|
<Trash2 className="h-3 w-3" />
|
|
</Button>
|
|
</div>
|
|
</TableCell>
|
|
)}
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
)}
|
|
</div>
|
|
</div>
|
|
) : (
|
|
versions.length > 0 && (
|
|
<div className="flex flex-1 items-center justify-center">
|
|
<p className="text-xs text-muted-foreground">라우팅 버전을 선택해주세요</p>
|
|
</div>
|
|
)
|
|
)}
|
|
</>
|
|
) : (
|
|
<div className="flex flex-1 flex-col items-center justify-center text-center">
|
|
<ListOrdered className="mb-3 h-12 w-12 text-muted-foreground/30" />
|
|
<p className="text-sm font-medium text-muted-foreground">좌측에서 품목을 선택하세요</p>
|
|
<p className="mt-1 text-xs text-muted-foreground/70">품목을 선택하면 라우팅 버전별 공정 순서를 관리할 수 있습니다</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* ════ 삭제 확인 ════ */}
|
|
<AlertDialog open={!!deleteTarget} onOpenChange={() => setDeleteTarget(null)}>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle className="text-base">삭제 확인</AlertDialogTitle>
|
|
<AlertDialogDescription className="text-sm">
|
|
{deleteTarget?.name}을(를) 삭제하시겠습니까?
|
|
{deleteTarget?.type === "version" && (<><br />해당 버전에 포함된 모든 공정 정보도 함께 삭제됩니다.</>)}
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel>취소</AlertDialogCancel>
|
|
<AlertDialogAction onClick={handleConfirmDelete} className="bg-destructive text-destructive-foreground hover:bg-destructive/90">
|
|
삭제
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
|
|
{/* ════ 품목 추가 다이얼로그 (테이블 형태 + 검색) ════ */}
|
|
<Dialog open={addDialogOpen} onOpenChange={setAddDialogOpen}>
|
|
<DialogContent className="max-w-[95vw] sm:max-w-[600px]">
|
|
<DialogHeader>
|
|
<DialogTitle className="text-base sm:text-lg">품목 추가</DialogTitle>
|
|
<DialogDescription className="text-xs sm:text-sm">
|
|
좌측 목록에 표시할 품목을 선택하세요
|
|
{(config.itemFilterConditions?.length ?? 0) > 0 && (
|
|
<span className="ml-1 text-[10px] text-muted-foreground">
|
|
(필터 {config.itemFilterConditions!.length}건 적용됨)
|
|
</span>
|
|
)}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className="flex gap-1.5">
|
|
<Input value={addSearchText} onChange={(e) => 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" />
|
|
<Button variant="outline" size="icon" className="h-8 w-8 shrink-0 sm:h-10 sm:w-10"
|
|
onClick={() => fetchAllItems(addSearchText || undefined)}>
|
|
<Search className="h-3.5 w-3.5" />
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="max-h-[340px] overflow-auto rounded-md border">
|
|
{allItems.length === 0 ? (
|
|
<div className="flex items-center justify-center py-8">
|
|
<p className="text-xs text-muted-foreground">품목이 없습니다</p>
|
|
</div>
|
|
) : (
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead className="w-[40px] text-center text-[11px] py-1.5" />
|
|
{modalDisplayCols.map((col) => (
|
|
<TableHead key={col.name}
|
|
style={{ width: col.width ? `${col.width}px` : undefined }}
|
|
className={cn("text-[11px] py-1.5", col.align === "center" && "text-center", col.align === "right" && "text-right")}>
|
|
{col.label}
|
|
</TableHead>
|
|
))}
|
|
<TableHead className="w-[60px] text-center text-[11px] py-1.5">상태</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{allItems.map((item) => {
|
|
const isAlreadyRegistered = registeredItemIds.has(item.id);
|
|
const isChecked = selectedAddItems.has(item.id);
|
|
return (
|
|
<TableRow key={item.id}
|
|
className={cn("cursor-pointer", isAlreadyRegistered && "opacity-50", isChecked && "bg-primary/5")}
|
|
onClick={() => { if (!isAlreadyRegistered) handleToggleAddItem(item.id); }}>
|
|
<TableCell className="text-center py-1.5">
|
|
<Checkbox checked={isChecked || isAlreadyRegistered} disabled={isAlreadyRegistered} className="h-4 w-4" />
|
|
</TableCell>
|
|
{modalDisplayCols.map((col) => (
|
|
<TableCell key={col.name}
|
|
className={cn("text-xs py-1.5", col.align === "center" && "text-center", col.align === "right" && "text-right")}>
|
|
{getCellValue(item, col.name)}
|
|
</TableCell>
|
|
))}
|
|
<TableCell className="text-center py-1.5">
|
|
{isAlreadyRegistered && (
|
|
<Badge variant="secondary" className="h-5 text-[10px]">등록됨</Badge>
|
|
)}
|
|
</TableCell>
|
|
</TableRow>
|
|
);
|
|
})}
|
|
</TableBody>
|
|
</Table>
|
|
)}
|
|
</div>
|
|
|
|
{selectedAddItems.size > 0 && (
|
|
<p className="text-xs text-muted-foreground">{selectedAddItems.size}개 선택됨</p>
|
|
)}
|
|
|
|
<DialogFooter className="gap-2 sm:gap-0">
|
|
<Button variant="outline" onClick={() => setAddDialogOpen(false)}
|
|
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm">취소</Button>
|
|
<Button onClick={handleConfirmAdd} disabled={selectedAddItems.size === 0 || addLoading}
|
|
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm">
|
|
{addLoading ? "등록 중..." : `${selectedAddItems.size}개 추가`}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</div>
|
|
);
|
|
}
|