- Introduced a new TableCombobox component for selecting tables, improving user experience by allowing table searches and selections. - Added a ColumnCombobox component to facilitate column selection based on the chosen table, enhancing the configurability of the process work standard settings. - Updated the V2ProcessWorkStandardConfigPanel to utilize the new combobox components, streamlining the configuration process for item tables and columns. - Removed the deprecated mcp.json file and updated .gitignore to reflect recent changes. These enhancements aim to improve the usability and flexibility of the configuration panel, making it easier for users to manage their process work standards.
571 lines
23 KiB
TypeScript
571 lines
23 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 {
|
|
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,
|
|
Layers,
|
|
List,
|
|
} from "lucide-react";
|
|
import { cn } from "@/lib/utils";
|
|
import type {
|
|
ProcessWorkStandardConfig,
|
|
WorkPhaseDefinition,
|
|
DetailTypeDefinition,
|
|
} from "@/lib/registry/components/v2-process-work-standard/types";
|
|
import { defaultConfig } from "@/lib/registry/components/v2-process-work-standard/config";
|
|
|
|
interface TableInfo { tableName: string; displayName?: string; }
|
|
|
|
function TableCombobox({ value, onChange, tables, loading, label }: {
|
|
value: string; onChange: (v: string) => void; tables: TableInfo[]; loading: boolean; label: string;
|
|
}) {
|
|
const [open, setOpen] = useState(false);
|
|
const selected = tables.find((t) => t.tableName === value);
|
|
return (
|
|
<div className="flex items-center justify-between gap-3">
|
|
<span className="text-muted-foreground shrink-0 text-xs">{label}</span>
|
|
<Popover open={open} onOpenChange={setOpen}>
|
|
<PopoverTrigger asChild>
|
|
<Button variant="outline" role="combobox" aria-expanded={open} className="h-7 w-[200px] justify-between text-xs" disabled={loading}>
|
|
<span className="truncate">{loading ? "로딩..." : selected ? selected.displayName || selected.tableName : "테이블 선택"}</span>
|
|
<ChevronsUpDown className="ml-1 h-3 w-3 shrink-0 opacity-50" />
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="w-[240px] p-0" align="end">
|
|
<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-muted-foreground text-[10px]">{t.tableName}</span>}
|
|
</div>
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
</CommandList>
|
|
</Command>
|
|
</PopoverContent>
|
|
</Popover>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface V2ProcessWorkStandardConfigPanelProps {
|
|
config: Partial<ProcessWorkStandardConfig>;
|
|
onChange: (config: Partial<ProcessWorkStandardConfig>) => void;
|
|
}
|
|
|
|
export const V2ProcessWorkStandardConfigPanel: React.FC<
|
|
V2ProcessWorkStandardConfigPanelProps
|
|
> = ({ config: configProp, onChange }) => {
|
|
const [phasesOpen, setPhasesOpen] = useState(false);
|
|
const [detailTypesOpen, setDetailTypesOpen] = useState(false);
|
|
const [layoutOpen, setLayoutOpen] = useState(false);
|
|
const [dataSourceOpen, setDataSourceOpen] = useState(false);
|
|
const [tables, setTables] = useState<TableInfo[]>([]);
|
|
const [loadingTables, setLoadingTables] = useState(false);
|
|
|
|
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 config: ProcessWorkStandardConfig = {
|
|
...defaultConfig,
|
|
...configProp,
|
|
dataSource: { ...defaultConfig.dataSource, ...configProp?.dataSource },
|
|
phases: configProp?.phases?.length
|
|
? configProp.phases
|
|
: defaultConfig.phases,
|
|
detailTypes: configProp?.detailTypes?.length
|
|
? configProp.detailTypes
|
|
: defaultConfig.detailTypes,
|
|
};
|
|
|
|
const update = (partial: Partial<ProcessWorkStandardConfig>) => {
|
|
onChange({ ...configProp, ...partial });
|
|
};
|
|
|
|
const updateDataSource = (field: string, value: string) => {
|
|
update({ dataSource: { ...config.dataSource, [field]: value } });
|
|
};
|
|
|
|
const addPhase = () => {
|
|
const nextOrder = config.phases.length + 1;
|
|
update({
|
|
phases: [
|
|
...config.phases,
|
|
{
|
|
key: `PHASE_${nextOrder}`,
|
|
label: `단계 ${nextOrder}`,
|
|
sortOrder: nextOrder,
|
|
},
|
|
],
|
|
});
|
|
};
|
|
|
|
const removePhase = (idx: number) => {
|
|
update({ phases: config.phases.filter((_, i) => i !== idx) });
|
|
};
|
|
|
|
const updatePhase = (
|
|
idx: number,
|
|
field: keyof WorkPhaseDefinition,
|
|
value: string | number,
|
|
) => {
|
|
const next = [...config.phases];
|
|
next[idx] = { ...next[idx], [field]: value };
|
|
update({ phases: next });
|
|
};
|
|
|
|
const addDetailType = () => {
|
|
update({
|
|
detailTypes: [
|
|
...config.detailTypes,
|
|
{
|
|
value: `TYPE_${config.detailTypes.length + 1}`,
|
|
label: "신규 유형",
|
|
},
|
|
],
|
|
});
|
|
};
|
|
|
|
const removeDetailType = (idx: number) => {
|
|
update({ detailTypes: config.detailTypes.filter((_, i) => i !== idx) });
|
|
};
|
|
|
|
const updateDetailType = (
|
|
idx: number,
|
|
field: keyof DetailTypeDefinition,
|
|
value: string,
|
|
) => {
|
|
const next = [...config.detailTypes];
|
|
next[idx] = { ...next[idx], [field]: value };
|
|
update({ detailTypes: next });
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
{/* 품목 목록 모드 */}
|
|
<div className="space-y-2 rounded-lg border p-4">
|
|
<span className="text-sm font-medium">품목 목록 모드</span>
|
|
<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-muted-foreground text-[10px]">
|
|
모든 품목 표시
|
|
</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-muted-foreground text-[10px]">
|
|
라우팅에 등록된 품목만
|
|
</span>
|
|
</button>
|
|
</div>
|
|
{config.itemListMode === "registered" && (
|
|
<p className="text-muted-foreground pt-1 text-[10px]">
|
|
품목별 라우팅 탭에서 등록한 품목만 표시됩니다.
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* 작업 단계 */}
|
|
<Collapsible open={phasesOpen} onOpenChange={setPhasesOpen}>
|
|
<CollapsibleTrigger asChild>
|
|
<button
|
|
type="button"
|
|
className="bg-muted/30 hover:bg-muted/50 flex w-full items-center justify-between rounded-lg border px-4 py-2.5 text-left transition-colors"
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
<Layers className="text-muted-foreground h-4 w-4" />
|
|
<span className="text-sm font-medium">작업 단계</span>
|
|
<Badge variant="secondary" className="h-5 text-[10px]">
|
|
{config.phases.length}개
|
|
</Badge>
|
|
</div>
|
|
<ChevronDown
|
|
className={cn(
|
|
"text-muted-foreground h-4 w-4 transition-transform duration-200",
|
|
phasesOpen && "rotate-180",
|
|
)}
|
|
/>
|
|
</button>
|
|
</CollapsibleTrigger>
|
|
<CollapsibleContent>
|
|
<div className="space-y-1.5 rounded-b-lg border border-t-0 p-3">
|
|
<p className="text-muted-foreground mb-1 text-[10px]">
|
|
공정별 작업 단계를 정의
|
|
</p>
|
|
<div className="space-y-1">
|
|
{config.phases.map((phase, idx) => (
|
|
<Collapsible key={idx}>
|
|
<div className="rounded-md border">
|
|
<CollapsibleTrigger asChild>
|
|
<button
|
|
type="button"
|
|
className="hover:bg-muted/30 flex w-full items-center gap-1.5 px-2.5 py-1.5 text-left transition-colors"
|
|
>
|
|
<ChevronRight className="text-muted-foreground h-3 w-3 shrink-0 transition-transform [[data-state=open]>&]:rotate-90" />
|
|
<span className="text-muted-foreground shrink-0 text-[10px] font-medium">
|
|
#{idx + 1}
|
|
</span>
|
|
<span className="min-w-0 flex-1 truncate text-xs font-medium">
|
|
{phase.label}
|
|
</span>
|
|
<Badge
|
|
variant="outline"
|
|
className="h-4 shrink-0 text-[9px]"
|
|
>
|
|
{phase.key}
|
|
</Badge>
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
removePhase(idx);
|
|
}}
|
|
className="text-muted-foreground hover:text-destructive h-5 w-5 shrink-0 p-0"
|
|
disabled={config.phases.length <= 1}
|
|
>
|
|
<Trash2 className="h-3 w-3" />
|
|
</Button>
|
|
</button>
|
|
</CollapsibleTrigger>
|
|
<CollapsibleContent>
|
|
<div className="grid grid-cols-3 gap-1.5 border-t px-2.5 py-2">
|
|
<div className="space-y-0.5">
|
|
<span className="text-muted-foreground text-[10px]">
|
|
키
|
|
</span>
|
|
<Input
|
|
value={phase.key}
|
|
onChange={(e) =>
|
|
updatePhase(idx, "key", e.target.value)
|
|
}
|
|
className="h-7 text-xs"
|
|
/>
|
|
</div>
|
|
<div className="space-y-0.5">
|
|
<span className="text-muted-foreground text-[10px]">
|
|
표시명
|
|
</span>
|
|
<Input
|
|
value={phase.label}
|
|
onChange={(e) =>
|
|
updatePhase(idx, "label", e.target.value)
|
|
}
|
|
className="h-7 text-xs"
|
|
/>
|
|
</div>
|
|
<div className="space-y-0.5">
|
|
<span className="text-muted-foreground text-[10px]">
|
|
순서
|
|
</span>
|
|
<Input
|
|
type="number"
|
|
min={1}
|
|
value={phase.sortOrder}
|
|
onChange={(e) =>
|
|
updatePhase(
|
|
idx,
|
|
"sortOrder",
|
|
parseInt(e.target.value) || 1,
|
|
)
|
|
}
|
|
className="h-7 text-center text-xs"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</CollapsibleContent>
|
|
</div>
|
|
</Collapsible>
|
|
))}
|
|
</div>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="h-7 w-full gap-1 border-dashed text-xs"
|
|
onClick={addPhase}
|
|
>
|
|
<Plus className="h-3 w-3" />
|
|
단계 추가
|
|
</Button>
|
|
</div>
|
|
</CollapsibleContent>
|
|
</Collapsible>
|
|
|
|
{/* 상세 유형 */}
|
|
<Collapsible open={detailTypesOpen} onOpenChange={setDetailTypesOpen}>
|
|
<CollapsibleTrigger asChild>
|
|
<button
|
|
type="button"
|
|
className="bg-muted/30 hover:bg-muted/50 flex w-full items-center justify-between rounded-lg border px-4 py-2.5 text-left transition-colors"
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
<List className="text-muted-foreground h-4 w-4" />
|
|
<span className="text-sm font-medium">상세 유형</span>
|
|
<Badge variant="secondary" className="h-5 text-[10px]">
|
|
{config.detailTypes.length}개
|
|
</Badge>
|
|
</div>
|
|
<ChevronDown
|
|
className={cn(
|
|
"text-muted-foreground h-4 w-4 transition-transform duration-200",
|
|
detailTypesOpen && "rotate-180",
|
|
)}
|
|
/>
|
|
</button>
|
|
</CollapsibleTrigger>
|
|
<CollapsibleContent>
|
|
<div className="space-y-1.5 rounded-b-lg border border-t-0 p-3">
|
|
<p className="text-muted-foreground mb-1 text-[10px]">
|
|
작업 항목의 상세 유형 옵션
|
|
</p>
|
|
<div className="space-y-1">
|
|
{config.detailTypes.map((dt, idx) => (
|
|
<Collapsible key={idx}>
|
|
<div className="rounded-md border">
|
|
<CollapsibleTrigger asChild>
|
|
<button
|
|
type="button"
|
|
className="hover:bg-muted/30 flex w-full items-center gap-1.5 px-2.5 py-1.5 text-left transition-colors"
|
|
>
|
|
<ChevronRight className="text-muted-foreground h-3 w-3 shrink-0 transition-transform [[data-state=open]>&]:rotate-90" />
|
|
<span className="text-muted-foreground shrink-0 text-[10px] font-medium">
|
|
#{idx + 1}
|
|
</span>
|
|
<span className="min-w-0 flex-1 truncate text-xs font-medium">
|
|
{dt.label}
|
|
</span>
|
|
<Badge
|
|
variant="outline"
|
|
className="h-4 shrink-0 text-[9px]"
|
|
>
|
|
{dt.value}
|
|
</Badge>
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
removeDetailType(idx);
|
|
}}
|
|
className="text-muted-foreground hover:text-destructive h-5 w-5 shrink-0 p-0"
|
|
disabled={config.detailTypes.length <= 1}
|
|
>
|
|
<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-muted-foreground text-[10px]">
|
|
값
|
|
</span>
|
|
<Input
|
|
value={dt.value}
|
|
onChange={(e) =>
|
|
updateDetailType(idx, "value", e.target.value)
|
|
}
|
|
className="h-7 text-xs"
|
|
/>
|
|
</div>
|
|
<div className="space-y-0.5">
|
|
<span className="text-muted-foreground text-[10px]">
|
|
표시명
|
|
</span>
|
|
<Input
|
|
value={dt.label}
|
|
onChange={(e) =>
|
|
updateDetailType(idx, "label", e.target.value)
|
|
}
|
|
className="h-7 text-xs"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</CollapsibleContent>
|
|
</div>
|
|
</Collapsible>
|
|
))}
|
|
</div>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="h-7 w-full gap-1 border-dashed text-xs"
|
|
onClick={addDetailType}
|
|
>
|
|
<Plus className="h-3 w-3" />
|
|
유형 추가
|
|
</Button>
|
|
</div>
|
|
</CollapsibleContent>
|
|
</Collapsible>
|
|
|
|
{/* 데이터 소스 (테이블만) */}
|
|
<Collapsible open={dataSourceOpen} onOpenChange={setDataSourceOpen}>
|
|
<CollapsibleTrigger asChild>
|
|
<button
|
|
type="button"
|
|
className="bg-muted/30 hover:bg-muted/50 flex w-full items-center justify-between rounded-lg border px-4 py-2.5 text-left transition-colors"
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
<Database className="text-muted-foreground h-4 w-4" />
|
|
<span className="text-sm font-medium">테이블 설정</span>
|
|
</div>
|
|
<ChevronDown
|
|
className={cn(
|
|
"text-muted-foreground h-4 w-4 transition-transform duration-200",
|
|
dataSourceOpen && "rotate-180",
|
|
)}
|
|
/>
|
|
</button>
|
|
</CollapsibleTrigger>
|
|
<CollapsibleContent>
|
|
<div className="space-y-2.5 rounded-b-lg border border-t-0 p-4">
|
|
<p className="text-muted-foreground text-[10px]">
|
|
테이블만 선택하면 컬럼 정보는 엔티티 설정에서 자동으로 가져옵니다.
|
|
</p>
|
|
<TableCombobox label="품목" value={config.dataSource.itemTable} onChange={(v) => updateDataSource("itemTable", v)} tables={tables} loading={loadingTables} />
|
|
<TableCombobox label="라우팅 버전" value={config.dataSource.routingVersionTable} onChange={(v) => updateDataSource("routingVersionTable", v)} tables={tables} loading={loadingTables} />
|
|
<TableCombobox label="라우팅 상세" value={config.dataSource.routingDetailTable} onChange={(v) => updateDataSource("routingDetailTable", v)} tables={tables} loading={loadingTables} />
|
|
<TableCombobox label="공정 마스터" value={config.dataSource.processTable} onChange={(v) => updateDataSource("processTable", v)} tables={tables} loading={loadingTables} />
|
|
</div>
|
|
</CollapsibleContent>
|
|
</Collapsible>
|
|
|
|
{/* 레이아웃 & 기타 */}
|
|
<Collapsible open={layoutOpen} onOpenChange={setLayoutOpen}>
|
|
<CollapsibleTrigger asChild>
|
|
<button
|
|
type="button"
|
|
className="bg-muted/30 hover:bg-muted/50 flex w-full items-center justify-between rounded-lg border px-4 py-2.5 text-left transition-colors"
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
<Settings className="text-muted-foreground h-4 w-4" />
|
|
<span className="text-sm font-medium">레이아웃</span>
|
|
</div>
|
|
<ChevronDown
|
|
className={cn(
|
|
"text-muted-foreground h-4 w-4 transition-transform duration-200",
|
|
layoutOpen && "rotate-180",
|
|
)}
|
|
/>
|
|
</button>
|
|
</CollapsibleTrigger>
|
|
<CollapsibleContent>
|
|
<div className="space-y-3 rounded-b-lg border border-t-0 p-4">
|
|
<div className="flex items-center justify-between py-1">
|
|
<div>
|
|
<span className="text-muted-foreground text-xs">
|
|
좌측 패널 비율 (%)
|
|
</span>
|
|
<p className="text-muted-foreground mt-0.5 text-[10px]">
|
|
품목/공정 선택 패널의 너비
|
|
</p>
|
|
</div>
|
|
<Input
|
|
type="number"
|
|
min={15}
|
|
max={50}
|
|
value={config.splitRatio || 30}
|
|
onChange={(e) =>
|
|
update({ splitRatio: parseInt(e.target.value) || 30 })
|
|
}
|
|
className="h-7 w-[80px] text-xs"
|
|
/>
|
|
</div>
|
|
<div className="flex items-center justify-between py-1">
|
|
<span className="text-muted-foreground text-xs">
|
|
좌측 패널 제목
|
|
</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">
|
|
<div>
|
|
<p className="text-xs">읽기 전용</p>
|
|
<p className="text-muted-foreground text-[10px]">
|
|
수정/삭제 버튼을 숨겨요
|
|
</p>
|
|
</div>
|
|
<Switch
|
|
checked={config.readonly || false}
|
|
onCheckedChange={(checked) => update({ readonly: checked })}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</CollapsibleContent>
|
|
</Collapsible>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
V2ProcessWorkStandardConfigPanel.displayName =
|
|
"V2ProcessWorkStandardConfigPanel";
|
|
|
|
export default V2ProcessWorkStandardConfigPanel;
|