Files
vexplor_dev/frontend/components/v2/config-panels/V2ItemRoutingConfigPanel.tsx
kjs 2976cad0a5 feat: enhance item routing modal configuration
- Added new properties for configuring the item addition modal's maximum width and list height in the ItemRoutingConfig interface.
- Updated the ColumnEditor component to include additional content for modal size settings, allowing users to specify width and height in pixels.
- Adjusted the ItemRoutingComponent to apply the new configuration settings for the modal's dimensions, improving the user experience by providing more control over modal appearance.

These changes aim to enhance the flexibility and usability of the item routing modal within the application.
2026-03-17 16:54:56 +09:00

633 lines
34 KiB
TypeScript

"use client";
/**
* V2 품목별 라우팅 설정 패널
*/
import React, { useState, useEffect } from "react";
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import { Button } from "@/components/ui/button";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
import { Badge } from "@/components/ui/badge";
import {
Settings, ChevronDown, ChevronRight, Plus, Trash2, Check, ChevronsUpDown,
Database, Monitor, Columns, List, Filter, Eye,
} from "lucide-react";
import { cn } from "@/lib/utils";
import type { ItemRoutingConfig, ProcessColumnDef, ColumnDef, ItemFilterCondition } from "@/lib/registry/components/v2-item-routing/types";
import { defaultConfig } from "@/lib/registry/components/v2-item-routing/config";
interface V2ItemRoutingConfigPanelProps {
config: Partial<ItemRoutingConfig>;
onChange: (config: Partial<ItemRoutingConfig>) => void;
}
interface TableInfo { tableName: string; displayName?: string; }
interface ColumnInfo { columnName: string; displayName?: string; dataType?: string; }
interface ScreenInfo { screenId: number; screenName: string; screenCode: string; }
// ─── 공용: 테이블 Combobox ───
function TableCombobox({ value, onChange, tables, loading }: {
value: string; onChange: (v: string) => void; tables: TableInfo[]; loading: boolean;
}) {
const [open, setOpen] = useState(false);
const selected = tables.find((t) => t.tableName === value);
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button variant="outline" role="combobox" aria-expanded={open} className="h-7 w-full justify-between text-xs" disabled={loading}>
{loading ? "로딩 중..." : selected ? selected.displayName || selected.tableName : "테이블 선택"}
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="p-0" style={{ width: "var(--radix-popover-trigger-width)" }} align="start">
<Command>
<CommandInput placeholder="테이블 검색..." className="text-xs" />
<CommandList>
<CommandEmpty className="py-4 text-center text-xs"> .</CommandEmpty>
<CommandGroup className="max-h-[200px] overflow-auto">
{tables.map((t) => (
<CommandItem key={t.tableName} value={`${t.displayName || ""} ${t.tableName}`}
onSelect={() => { onChange(t.tableName); setOpen(false); }} className="text-xs">
<Check className={cn("mr-2 h-3 w-3", value === t.tableName ? "opacity-100" : "opacity-0")} />
<div className="flex flex-col">
<span className="font-medium">{t.displayName || t.tableName}</span>
{t.displayName && <span className="text-[10px] text-muted-foreground">{t.tableName}</span>}
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
}
// ─── 공용: 컬럼 Combobox ───
function ColumnCombobox({ value, onChange, tableName, placeholder }: {
value: string; onChange: (v: string, displayName?: string) => void; tableName: string; placeholder?: string;
}) {
const [open, setOpen] = useState(false);
const [columns, setColumns] = useState<ColumnInfo[]>([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (!tableName) { setColumns([]); return; }
const load = async () => {
setLoading(true);
try {
const { tableManagementApi } = await import("@/lib/api/tableManagement");
const res = await tableManagementApi.getColumnList(tableName);
if (res.success && res.data?.columns) setColumns(res.data.columns);
} catch { /* ignore */ } finally { setLoading(false); }
};
load();
}, [tableName]);
const selected = columns.find((c) => c.columnName === value);
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button variant="outline" role="combobox" aria-expanded={open} className="h-7 w-full justify-between text-xs" disabled={loading || !tableName}>
<span className="truncate">
{loading ? "로딩..." : !tableName ? "테이블 먼저 선택" : selected ? selected.displayName || selected.columnName : placeholder || "컬럼 선택"}
</span>
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[240px] p-0" align="start">
<Command>
<CommandInput placeholder="컬럼 검색..." className="text-xs" />
<CommandList>
<CommandEmpty className="py-4 text-center text-xs"> .</CommandEmpty>
<CommandGroup className="max-h-[200px] overflow-auto">
{columns.map((c) => (
<CommandItem key={c.columnName} value={`${c.displayName || ""} ${c.columnName}`}
onSelect={() => { onChange(c.columnName, c.displayName); setOpen(false); }} className="text-xs">
<Check className={cn("mr-2 h-3 w-3", value === c.columnName ? "opacity-100" : "opacity-0")} />
<div className="flex flex-col">
<span className="font-medium">{c.displayName || c.columnName}</span>
{c.displayName && <span className="text-[10px] text-muted-foreground">{c.columnName}</span>}
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
}
// ─── 공용: 화면 Combobox ───
function ScreenCombobox({ value, onChange }: { value?: number; onChange: (v?: number) => void; }) {
const [open, setOpen] = useState(false);
const [screens, setScreens] = useState<ScreenInfo[]>([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
const load = async () => {
setLoading(true);
try {
const { screenApi } = await import("@/lib/api/screen");
const res = await screenApi.getScreens({ page: 1, size: 1000 });
if (res.data) {
setScreens(res.data.map((s: any) => ({
screenId: s.screenId, screenName: s.screenName || `화면 ${s.screenId}`, screenCode: s.screenCode || "",
})));
}
} catch { /* ignore */ } finally { setLoading(false); }
};
load();
}, []);
const selected = screens.find((s) => s.screenId === value);
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button variant="outline" role="combobox" aria-expanded={open} className="h-7 w-full justify-between text-xs" disabled={loading}>
<span className="truncate">{loading ? "로딩..." : selected ? selected.screenName : "화면 선택"}</span>
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[260px] p-0" align="start">
<Command>
<CommandInput placeholder="화면 검색..." className="text-xs" />
<CommandList>
<CommandEmpty className="py-4 text-center text-xs"> .</CommandEmpty>
<CommandGroup className="max-h-[200px] overflow-auto">
{screens.map((s) => (
<CommandItem key={s.screenId} value={`${s.screenName} ${s.screenCode} ${s.screenId}`}
onSelect={() => { onChange(s.screenId); setOpen(false); }} className="text-xs">
<Check className={cn("mr-2 h-3 w-3", value === s.screenId ? "opacity-100" : "opacity-0")} />
<div className="flex flex-col">
<span className="font-medium">{s.screenName}</span>
<span className="text-[10px] text-muted-foreground">ID: {s.screenId}</span>
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
}
// ─── 컬럼 편집 카드 (품목/모달/공정 공용) ───
function ColumnEditor({ columns, onChange, tableName, title, icon, extraContent }: {
columns: ColumnDef[];
onChange: (cols: ColumnDef[]) => void;
tableName: string;
title: string;
icon: React.ReactNode;
extraContent?: React.ReactNode;
}) {
const [open, setOpen] = useState(false);
const addColumn = () => onChange([...columns, { name: "", label: "새 컬럼", width: 100, align: "left" }]);
const removeColumn = (idx: number) => onChange(columns.filter((_, i) => i !== idx));
const updateColumn = (idx: number, field: keyof ColumnDef, value: string | number) => {
const next = [...columns];
next[idx] = { ...next[idx], [field]: value };
onChange(next);
};
return (
<Collapsible open={open} onOpenChange={setOpen}>
<CollapsibleTrigger asChild>
<button type="button" className="flex w-full items-center justify-between rounded-lg border bg-muted/30 px-4 py-2.5 text-left transition-colors hover:bg-muted/50">
<div className="flex items-center gap-2">
{icon}
<span className="text-sm font-medium">{title}</span>
<Badge variant="secondary" className="text-[10px] h-5">{columns.length}</Badge>
</div>
<ChevronDown className={cn("h-4 w-4 text-muted-foreground transition-transform duration-200", open && "rotate-180")} />
</button>
</CollapsibleTrigger>
<CollapsibleContent>
<div className="rounded-b-lg border border-t-0 p-3 space-y-1.5">
{columns.map((col, idx) => (
<Collapsible key={idx}>
<div className="rounded-md border">
<CollapsibleTrigger asChild>
<button type="button" className="flex w-full items-center gap-1.5 px-2.5 py-1.5 text-left hover:bg-muted/30 transition-colors">
<ChevronRight className="h-3 w-3 text-muted-foreground transition-transform [[data-state=open]>&]:rotate-90 shrink-0" />
<span className="text-[10px] text-muted-foreground font-medium shrink-0">#{idx + 1}</span>
<span className="text-xs font-medium truncate flex-1 min-w-0">{col.label || col.name || "미설정"}</span>
<Badge variant="outline" className="text-[9px] h-4 shrink-0">{col.name || "?"}</Badge>
<Button type="button" variant="ghost" size="sm"
onClick={(e) => { e.stopPropagation(); removeColumn(idx); }}
className="h-5 w-5 p-0 text-muted-foreground hover:text-destructive shrink-0">
<Trash2 className="h-3 w-3" />
</Button>
</button>
</CollapsibleTrigger>
<CollapsibleContent>
<div className="grid grid-cols-2 gap-1.5 border-t px-2.5 py-2">
<div className="space-y-0.5">
<span className="text-[10px] text-muted-foreground"></span>
<ColumnCombobox value={col.name} onChange={(v, displayName) => {
updateColumn(idx, "name", v);
if (!col.label || col.label === "새 컬럼" || col.label === col.name) updateColumn(idx, "label", displayName || v);
}} tableName={tableName} placeholder="컬럼 선택" />
</div>
<div className="space-y-0.5">
<span className="text-[10px] text-muted-foreground"></span>
<Input value={col.label} onChange={(e) => updateColumn(idx, "label", e.target.value)} className="h-7 text-xs" />
</div>
<div className="space-y-0.5">
<span className="text-[10px] text-muted-foreground"></span>
<Input type="number" value={col.width || 100} onChange={(e) => updateColumn(idx, "width", parseInt(e.target.value) || 100)} className="h-7 text-xs" />
</div>
<div className="space-y-0.5">
<span className="text-[10px] text-muted-foreground"></span>
<Select value={col.align || "left"} onValueChange={(v) => updateColumn(idx, "align", v)}>
<SelectTrigger className="h-7 text-xs"><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="left"></SelectItem>
<SelectItem value="center"></SelectItem>
<SelectItem value="right"></SelectItem>
</SelectContent>
</Select>
</div>
</div>
</CollapsibleContent>
</div>
</Collapsible>
))}
<Button variant="outline" size="sm" className="h-7 w-full gap-1 text-xs border-dashed" onClick={addColumn}>
<Plus className="h-3 w-3" />
</Button>
{extraContent}
</div>
</CollapsibleContent>
</Collapsible>
);
}
// ─── 메인 컴포넌트 ───
export const V2ItemRoutingConfigPanel: React.FC<V2ItemRoutingConfigPanelProps> = ({ config: configProp, onChange }) => {
const [tables, setTables] = useState<TableInfo[]>([]);
const [loadingTables, setLoadingTables] = useState(false);
const [modalOpen, setModalOpen] = useState(false);
const [dataSourceOpen, setDataSourceOpen] = useState(false);
const [layoutOpen, setLayoutOpen] = useState(false);
const [filterOpen, setFilterOpen] = useState(false);
const config: ItemRoutingConfig = {
...defaultConfig,
...configProp,
dataSource: { ...defaultConfig.dataSource, ...configProp?.dataSource },
modals: { ...defaultConfig.modals, ...configProp?.modals },
processColumns: configProp?.processColumns?.length ? configProp.processColumns : defaultConfig.processColumns,
itemDisplayColumns: configProp?.itemDisplayColumns?.length ? configProp.itemDisplayColumns : defaultConfig.itemDisplayColumns,
modalDisplayColumns: configProp?.modalDisplayColumns?.length ? configProp.modalDisplayColumns : defaultConfig.modalDisplayColumns,
itemFilterConditions: configProp?.itemFilterConditions || [],
};
useEffect(() => {
const loadTables = async () => {
setLoadingTables(true);
try {
const { tableManagementApi } = await import("@/lib/api/tableManagement");
const res = await tableManagementApi.getTableList();
if (res.success && res.data) {
setTables(res.data.map((t: any) => ({ tableName: t.tableName, displayName: t.displayName || t.tableName })));
}
} catch { /* ignore */ } finally { setLoadingTables(false); }
};
loadTables();
}, []);
const dispatchConfigEvent = (newConfig: Partial<ItemRoutingConfig>) => {
if (typeof window !== "undefined") {
window.dispatchEvent(new CustomEvent("componentConfigChanged", { detail: { config: { ...config, ...newConfig } } }));
}
};
const update = (partial: Partial<ItemRoutingConfig>) => {
const merged = { ...configProp, ...partial };
onChange(merged);
dispatchConfigEvent(partial);
};
const updateDataSource = (field: string, value: string) => {
const newDS = { ...config.dataSource, [field]: value };
onChange({ ...configProp, dataSource: newDS });
dispatchConfigEvent({ dataSource: newDS });
};
const updateModals = (field: string, value?: number) => {
const newM = { ...config.modals, [field]: value };
onChange({ ...configProp, modals: newM });
dispatchConfigEvent({ modals: newM });
};
// 필터 조건 관리
const filters = config.itemFilterConditions || [];
const addFilter = () => update({ itemFilterConditions: [...filters, { column: "", operator: "equals", value: "" }] });
const removeFilter = (idx: number) => update({ itemFilterConditions: filters.filter((_, i) => i !== idx) });
const updateFilter = (idx: number, field: keyof ItemFilterCondition, val: string) => {
const next = [...filters];
next[idx] = { ...next[idx], [field]: val };
update({ itemFilterConditions: next });
};
return (
<div className="space-y-4">
{/* ─── 품목 목록 모드 ─── */}
<div className="rounded-lg border p-4 space-y-3">
<div className="flex items-center gap-2">
<List className="h-4 w-4 text-muted-foreground" />
<span className="text-sm font-medium"> </span>
</div>
<p className="text-[10px] text-muted-foreground"> </p>
<div className="grid grid-cols-2 gap-2">
<button type="button"
className={cn("flex flex-col items-center gap-1 rounded-md border px-3 py-2.5 text-xs transition-colors",
(config.itemListMode || "all") === "all" ? "border-primary bg-primary/5 text-primary" : "border-input hover:bg-muted/50")}
onClick={() => update({ itemListMode: "all" })}>
<span className="font-medium"> </span>
<span className="text-[10px] text-muted-foreground"> </span>
</button>
<button type="button"
className={cn("flex flex-col items-center gap-1 rounded-md border px-3 py-2.5 text-xs transition-colors",
config.itemListMode === "registered" ? "border-primary bg-primary/5 text-primary" : "border-input hover:bg-muted/50")}
onClick={() => update({ itemListMode: "registered" })}>
<span className="font-medium"> </span>
<span className="text-[10px] text-muted-foreground"> </span>
</button>
</div>
{config.itemListMode === "registered" && (
<p className="text-[10px] text-muted-foreground pt-1">
ID를 .
</p>
)}
</div>
{/* ─── 품목 표시 컬럼 ─── */}
<ColumnEditor
columns={config.itemDisplayColumns || []}
onChange={(cols) => update({ itemDisplayColumns: cols })}
tableName={config.dataSource.itemTable}
title="품목 목록 컬럼"
icon={<Eye className="h-4 w-4 text-muted-foreground" />}
/>
{/* ─── 품목 추가 모달 (컬럼 + 크기 설정) ─── */}
<ColumnEditor
columns={config.modalDisplayColumns || []}
onChange={(cols) => update({ modalDisplayColumns: cols })}
tableName={config.dataSource.itemTable}
title="품목 추가 모달"
icon={<Columns className="h-4 w-4 text-muted-foreground" />}
extraContent={
<div className="mt-3 space-y-2 border-t pt-3">
<span className="text-[10px] font-medium text-muted-foreground"> (px)</span>
<div className="grid grid-cols-2 gap-2">
<div className="space-y-0.5">
<span className="text-[10px] text-muted-foreground"> ()</span>
<Input type="number" min={300} max={1600}
value={parseInt(config.addModalMaxWidth || "600")}
onChange={(e) => update({ addModalMaxWidth: `${e.target.value}px` })}
placeholder="600" className="h-7 text-xs" />
</div>
<div className="space-y-0.5">
<span className="text-[10px] text-muted-foreground"> ( )</span>
<Input type="number" min={150} max={900}
value={parseInt(config.addModalListMaxHeight || "340")}
onChange={(e) => update({ addModalListMaxHeight: `${e.target.value}px` })}
placeholder="340" className="h-7 text-xs" />
</div>
</div>
</div>
}
/>
{/* ─── 품목 필터 조건 ─── */}
<Collapsible open={filterOpen} onOpenChange={setFilterOpen}>
<CollapsibleTrigger asChild>
<button type="button" className="flex w-full items-center justify-between rounded-lg border bg-muted/30 px-4 py-2.5 text-left transition-colors hover:bg-muted/50">
<div className="flex items-center gap-2">
<Filter className="h-4 w-4 text-muted-foreground" />
<span className="text-sm font-medium"> </span>
{filters.length > 0 && <Badge variant="secondary" className="text-[10px] h-5">{filters.length}</Badge>}
</div>
<ChevronDown className={cn("h-4 w-4 text-muted-foreground transition-transform duration-200", filterOpen && "rotate-180")} />
</button>
</CollapsibleTrigger>
<CollapsibleContent>
<div className="rounded-b-lg border border-t-0 p-3 space-y-2">
<p className="text-[10px] text-muted-foreground"> </p>
{filters.map((f, idx) => (
<div key={idx} className="flex items-end gap-1.5 rounded-md border p-2">
<div className="flex-1 space-y-0.5">
<span className="text-[10px] text-muted-foreground"></span>
<ColumnCombobox value={f.column} onChange={(v) => updateFilter(idx, "column", v)}
tableName={config.dataSource.itemTable} placeholder="필터 컬럼" />
</div>
<div className="w-[90px] space-y-0.5">
<span className="text-[10px] text-muted-foreground"></span>
<Select value={f.operator} onValueChange={(v) => updateFilter(idx, "operator", v)}>
<SelectTrigger className="h-7 text-xs"><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="equals"></SelectItem>
<SelectItem value="contains"></SelectItem>
<SelectItem value="not_equals"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex-1 space-y-0.5">
<span className="text-[10px] text-muted-foreground"></span>
<Input value={f.value} onChange={(e) => updateFilter(idx, "value", e.target.value)}
placeholder="필터값" className="h-7 text-xs" />
</div>
<Button type="button" variant="ghost" size="sm"
onClick={() => removeFilter(idx)}
className="h-7 w-7 p-0 text-muted-foreground hover:text-destructive shrink-0">
<Trash2 className="h-3 w-3" />
</Button>
</div>
))}
<Button variant="outline" size="sm" className="h-7 w-full gap-1 text-xs border-dashed" onClick={addFilter}>
<Plus className="h-3 w-3" />
</Button>
</div>
</CollapsibleContent>
</Collapsible>
{/* ─── 모달 연동 ─── */}
<Collapsible open={modalOpen} onOpenChange={setModalOpen}>
<CollapsibleTrigger asChild>
<button type="button" className="flex w-full items-center justify-between rounded-lg border bg-muted/30 px-4 py-2.5 text-left transition-colors hover:bg-muted/50">
<div className="flex items-center gap-2">
<Monitor className="h-4 w-4 text-muted-foreground" />
<span className="text-sm font-medium"> </span>
<Badge variant="secondary" className="text-[10px] h-5">
{[config.modals.versionAddScreenId, config.modals.processAddScreenId, config.modals.processEditScreenId].filter(Boolean).length}
</Badge>
</div>
<ChevronDown className={cn("h-4 w-4 text-muted-foreground transition-transform duration-200", modalOpen && "rotate-180")} />
</button>
</CollapsibleTrigger>
<CollapsibleContent>
<div className="rounded-b-lg border border-t-0 p-3 space-y-2">
<p className="text-[10px] text-muted-foreground"> / · </p>
<div className="grid grid-cols-2 gap-2">
<div className="space-y-1">
<span className="text-[10px] text-muted-foreground"> </span>
<ScreenCombobox value={config.modals.versionAddScreenId} onChange={(v) => updateModals("versionAddScreenId", v)} />
</div>
<div className="space-y-1">
<span className="text-[10px] text-muted-foreground"> </span>
<ScreenCombobox value={config.modals.processAddScreenId} onChange={(v) => updateModals("processAddScreenId", v)} />
</div>
<div className="space-y-1">
<span className="text-[10px] text-muted-foreground"> </span>
<ScreenCombobox value={config.modals.processEditScreenId} onChange={(v) => updateModals("processEditScreenId", v)} />
</div>
</div>
</div>
</CollapsibleContent>
</Collapsible>
{/* ─── 공정 테이블 컬럼 ─── */}
<ColumnEditor
columns={config.processColumns}
onChange={(cols) => update({ processColumns: cols })}
tableName={config.dataSource.routingDetailTable}
title="공정 테이블 컬럼"
icon={<Columns className="h-4 w-4 text-muted-foreground" />}
/>
{/* ─── 데이터 소스 ─── */}
<Collapsible open={dataSourceOpen} onOpenChange={setDataSourceOpen}>
<CollapsibleTrigger asChild>
<button type="button" className="flex w-full items-center justify-between rounded-lg border bg-muted/30 px-4 py-2.5 text-left transition-colors hover:bg-muted/50">
<div className="flex items-center gap-2">
<Database className="h-4 w-4 text-muted-foreground" />
<span className="text-sm font-medium"> </span>
{config.dataSource.itemTable && (
<Badge variant="secondary" className="text-[10px] h-5 truncate max-w-[100px]">{config.dataSource.itemTable}</Badge>
)}
</div>
<ChevronDown className={cn("h-4 w-4 text-muted-foreground transition-transform duration-200", dataSourceOpen && "rotate-180")} />
</button>
</CollapsibleTrigger>
<CollapsibleContent>
<div className="rounded-b-lg border border-t-0 p-4 space-y-3">
<div className="space-y-1">
<span className="text-xs text-muted-foreground"> </span>
<TableCombobox value={config.dataSource.itemTable} onChange={(v) => updateDataSource("itemTable", v)} tables={tables} loading={loadingTables} />
</div>
<div className="space-y-1">
<span className="text-xs text-muted-foreground"> </span>
<ColumnCombobox value={config.dataSource.itemNameColumn} onChange={(v) => updateDataSource("itemNameColumn", v)} tableName={config.dataSource.itemTable} placeholder="품목명" />
</div>
<div className="space-y-1">
<span className="text-xs text-muted-foreground"> </span>
<ColumnCombobox value={config.dataSource.itemCodeColumn} onChange={(v) => updateDataSource("itemCodeColumn", v)} tableName={config.dataSource.itemTable} placeholder="품목코드" />
</div>
<div className="space-y-1 pt-2">
<span className="text-xs text-muted-foreground"> </span>
<TableCombobox value={config.dataSource.routingVersionTable} onChange={(v) => updateDataSource("routingVersionTable", v)} tables={tables} loading={loadingTables} />
</div>
<div className="space-y-1">
<span className="text-xs text-muted-foreground"> FK </span>
<ColumnCombobox value={config.dataSource.routingVersionFkColumn} onChange={(v) => updateDataSource("routingVersionFkColumn", v)} tableName={config.dataSource.routingVersionTable} placeholder="FK 컬럼" />
</div>
<div className="space-y-1">
<span className="text-xs text-muted-foreground"> </span>
<ColumnCombobox value={config.dataSource.routingVersionNameColumn} onChange={(v) => updateDataSource("routingVersionNameColumn", v)} tableName={config.dataSource.routingVersionTable} placeholder="버전명" />
</div>
<div className="space-y-1 pt-2">
<span className="text-xs text-muted-foreground"> </span>
<TableCombobox value={config.dataSource.routingDetailTable} onChange={(v) => updateDataSource("routingDetailTable", v)} tables={tables} loading={loadingTables} />
</div>
<div className="space-y-1">
<span className="text-xs text-muted-foreground"> FK </span>
<ColumnCombobox value={config.dataSource.routingDetailFkColumn} onChange={(v) => updateDataSource("routingDetailFkColumn", v)} tableName={config.dataSource.routingDetailTable} placeholder="FK 컬럼" />
</div>
<div className="space-y-1 pt-2">
<span className="text-xs text-muted-foreground"> </span>
<TableCombobox value={config.dataSource.processTable} onChange={(v) => updateDataSource("processTable", v)} tables={tables} loading={loadingTables} />
</div>
<div className="space-y-1">
<span className="text-xs text-muted-foreground"> </span>
<ColumnCombobox value={config.dataSource.processNameColumn} onChange={(v) => updateDataSource("processNameColumn", v)} tableName={config.dataSource.processTable} placeholder="공정명" />
</div>
<div className="space-y-1">
<span className="text-xs text-muted-foreground"> </span>
<ColumnCombobox value={config.dataSource.processCodeColumn} onChange={(v) => updateDataSource("processCodeColumn", v)} tableName={config.dataSource.processTable} placeholder="공정코드" />
</div>
</div>
</CollapsibleContent>
</Collapsible>
{/* ─── 레이아웃 & 기타 ─── */}
<Collapsible open={layoutOpen} onOpenChange={setLayoutOpen}>
<CollapsibleTrigger asChild>
<button type="button" className="flex w-full items-center justify-between rounded-lg border bg-muted/30 px-4 py-2.5 text-left transition-colors hover:bg-muted/50">
<div className="flex items-center gap-2">
<Settings className="h-4 w-4 text-muted-foreground" />
<span className="text-sm font-medium"> & </span>
</div>
<ChevronDown className={cn("h-4 w-4 text-muted-foreground transition-transform duration-200", layoutOpen && "rotate-180")} />
</button>
</CollapsibleTrigger>
<CollapsibleContent>
<div className="rounded-b-lg border border-t-0 p-4 space-y-3">
<div className="flex items-center justify-between py-1">
<div>
<span className="text-xs text-muted-foreground"> (%)</span>
<p className="text-[10px] text-muted-foreground mt-0.5"> </p>
</div>
<Input type="number" min={20} max={60} value={config.splitRatio || 40}
onChange={(e) => update({ splitRatio: parseInt(e.target.value) || 40 })} className="h-7 w-[80px] text-xs" />
</div>
<div className="flex items-center justify-between py-1">
<span className="text-xs text-muted-foreground"> </span>
<Input value={config.leftPanelTitle || ""} onChange={(e) => update({ leftPanelTitle: e.target.value })} placeholder="품목 목록" className="h-7 w-[140px] text-xs" />
</div>
<div className="flex items-center justify-between py-1">
<span className="text-xs text-muted-foreground"> </span>
<Input value={config.rightPanelTitle || ""} onChange={(e) => update({ rightPanelTitle: e.target.value })} placeholder="공정 순서" className="h-7 w-[140px] text-xs" />
</div>
<div className="flex items-center justify-between py-1">
<span className="text-xs text-muted-foreground"> </span>
<Input value={config.versionAddButtonText || ""} onChange={(e) => update({ versionAddButtonText: e.target.value })} placeholder="+ 라우팅 버전 추가" className="h-7 w-[140px] text-xs" />
</div>
<div className="flex items-center justify-between py-1">
<span className="text-xs text-muted-foreground"> </span>
<Input value={config.processAddButtonText || ""} onChange={(e) => update({ processAddButtonText: e.target.value })} placeholder="+ 공정 추가" className="h-7 w-[140px] text-xs" />
</div>
<div className="flex items-center justify-between py-1">
<div>
<p className="text-sm"> </p>
<p className="text-[11px] text-muted-foreground"> </p>
</div>
<Switch checked={config.autoSelectFirstVersion !== false} onCheckedChange={(checked) => update({ autoSelectFirstVersion: checked })} />
</div>
<div className="flex items-center justify-between py-1">
<div>
<p className="text-sm"> </p>
<p className="text-[11px] text-muted-foreground">// </p>
</div>
<Switch checked={config.readonly || false} onCheckedChange={(checked) => update({ readonly: checked })} />
</div>
</div>
</CollapsibleContent>
</Collapsible>
</div>
);
};
V2ItemRoutingConfigPanel.displayName = "V2ItemRoutingConfigPanel";
export default V2ItemRoutingConfigPanel;