- Added new endpoints for managing registered items, including retrieval, registration, and batch registration. - Enhanced the existing processWorkStandardController to support filtering and additional columns in item queries. - Updated the processWorkStandardRoutes to include routes for registered items management. - Introduced a new documentation file detailing the design and structure of the POP 작업진행 관리 system. These changes aim to improve the management of registered items within the process work standard, enhancing usability and functionality. Made-with: Cursor
610 lines
33 KiB
TypeScript
610 lines
33 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 }: {
|
|
columns: ColumnDef[];
|
|
onChange: (cols: ColumnDef[]) => void;
|
|
tableName: string;
|
|
title: string;
|
|
icon: 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>
|
|
</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" />}
|
|
/>
|
|
|
|
{/* ─── 품목 필터 조건 ─── */}
|
|
<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;
|