feat: enhance V2 process work standard configuration panel
- 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.
This commit is contained in:
parent
ae4fe7a66e
commit
5e6261f51a
|
|
@ -1,8 +0,0 @@
|
||||||
{
|
|
||||||
"mcpServers": {
|
|
||||||
"Framelink Figma MCP": {
|
|
||||||
"command": "npx",
|
|
||||||
"args": ["-y", "figma-developer-mcp", "--figma-api-key=figd_NrYdIWf-CnC23NyH6eMym7sBdfbZTuXyS91tI3VS", "--stdio"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -185,6 +185,9 @@ popdocs/
|
||||||
|
|
||||||
# 멀티 에이전트 MCP 태스크 큐
|
# 멀티 에이전트 MCP 태스크 큐
|
||||||
mcp-task-queue/
|
mcp-task-queue/
|
||||||
|
.cursor/mcp.json
|
||||||
.cursor/rules/multi-agent-pm.mdc
|
.cursor/rules/multi-agent-pm.mdc
|
||||||
.cursor/rules/multi-agent-worker.mdc
|
.cursor/rules/multi-agent-worker.mdc
|
||||||
.cursor/rules/multi-agent-tester.mdc
|
.cursor/rules/multi-agent-tester.mdc
|
||||||
|
.cursor/rules/multi-agent-reviewer.mdc
|
||||||
|
.cursor/rules/multi-agent-knowledge.mdc
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,33 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* V2 공정 작업기준 설정 패널
|
* V2 공정 작업기준 설정 패널 (간소화)
|
||||||
* Progressive Disclosure: 작업 단계 -> 상세 유형 -> 고급 설정(접힘)
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
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 { Badge } from "@/components/ui/badge";
|
||||||
import { Settings, ChevronDown, ChevronRight, Plus, Trash2, Database, Layers, List } from "lucide-react";
|
import {
|
||||||
|
Settings,
|
||||||
|
ChevronDown,
|
||||||
|
ChevronRight,
|
||||||
|
Plus,
|
||||||
|
Trash2,
|
||||||
|
Check,
|
||||||
|
ChevronsUpDown,
|
||||||
|
Database,
|
||||||
|
Layers,
|
||||||
|
List,
|
||||||
|
} from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import type {
|
import type {
|
||||||
ProcessWorkStandardConfig,
|
ProcessWorkStandardConfig,
|
||||||
|
|
@ -20,26 +36,87 @@ import type {
|
||||||
} from "@/lib/registry/components/v2-process-work-standard/types";
|
} from "@/lib/registry/components/v2-process-work-standard/types";
|
||||||
import { defaultConfig } from "@/lib/registry/components/v2-process-work-standard/config";
|
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 {
|
interface V2ProcessWorkStandardConfigPanelProps {
|
||||||
config: Partial<ProcessWorkStandardConfig>;
|
config: Partial<ProcessWorkStandardConfig>;
|
||||||
onChange: (config: Partial<ProcessWorkStandardConfig>) => void;
|
onChange: (config: Partial<ProcessWorkStandardConfig>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const V2ProcessWorkStandardConfigPanel: React.FC<V2ProcessWorkStandardConfigPanelProps> = ({
|
export const V2ProcessWorkStandardConfigPanel: React.FC<
|
||||||
config: configProp,
|
V2ProcessWorkStandardConfigPanelProps
|
||||||
onChange,
|
> = ({ config: configProp, onChange }) => {
|
||||||
}) => {
|
|
||||||
const [phasesOpen, setPhasesOpen] = useState(false);
|
const [phasesOpen, setPhasesOpen] = useState(false);
|
||||||
const [detailTypesOpen, setDetailTypesOpen] = useState(false);
|
const [detailTypesOpen, setDetailTypesOpen] = useState(false);
|
||||||
const [advancedOpen, setAdvancedOpen] = useState(false);
|
const [layoutOpen, setLayoutOpen] = useState(false);
|
||||||
const [dataSourceOpen, setDataSourceOpen] = 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 = {
|
const config: ProcessWorkStandardConfig = {
|
||||||
...defaultConfig,
|
...defaultConfig,
|
||||||
...configProp,
|
...configProp,
|
||||||
dataSource: { ...defaultConfig.dataSource, ...configProp?.dataSource },
|
dataSource: { ...defaultConfig.dataSource, ...configProp?.dataSource },
|
||||||
phases: configProp?.phases?.length ? configProp.phases : defaultConfig.phases,
|
phases: configProp?.phases?.length
|
||||||
detailTypes: configProp?.detailTypes?.length ? configProp.detailTypes : defaultConfig.detailTypes,
|
? configProp.phases
|
||||||
|
: defaultConfig.phases,
|
||||||
|
detailTypes: configProp?.detailTypes?.length
|
||||||
|
? configProp.detailTypes
|
||||||
|
: defaultConfig.detailTypes,
|
||||||
};
|
};
|
||||||
|
|
||||||
const update = (partial: Partial<ProcessWorkStandardConfig>) => {
|
const update = (partial: Partial<ProcessWorkStandardConfig>) => {
|
||||||
|
|
@ -50,13 +127,16 @@ export const V2ProcessWorkStandardConfigPanel: React.FC<V2ProcessWorkStandardCon
|
||||||
update({ dataSource: { ...config.dataSource, [field]: value } });
|
update({ dataSource: { ...config.dataSource, [field]: value } });
|
||||||
};
|
};
|
||||||
|
|
||||||
// ─── 작업 단계 관리 ───
|
|
||||||
const addPhase = () => {
|
const addPhase = () => {
|
||||||
const nextOrder = config.phases.length + 1;
|
const nextOrder = config.phases.length + 1;
|
||||||
update({
|
update({
|
||||||
phases: [
|
phases: [
|
||||||
...config.phases,
|
...config.phases,
|
||||||
{ key: `PHASE_${nextOrder}`, label: `단계 ${nextOrder}`, sortOrder: nextOrder },
|
{
|
||||||
|
key: `PHASE_${nextOrder}`,
|
||||||
|
label: `단계 ${nextOrder}`,
|
||||||
|
sortOrder: nextOrder,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
@ -65,18 +145,24 @@ export const V2ProcessWorkStandardConfigPanel: React.FC<V2ProcessWorkStandardCon
|
||||||
update({ phases: config.phases.filter((_, i) => i !== idx) });
|
update({ phases: config.phases.filter((_, i) => i !== idx) });
|
||||||
};
|
};
|
||||||
|
|
||||||
const updatePhase = (idx: number, field: keyof WorkPhaseDefinition, value: string | number) => {
|
const updatePhase = (
|
||||||
|
idx: number,
|
||||||
|
field: keyof WorkPhaseDefinition,
|
||||||
|
value: string | number,
|
||||||
|
) => {
|
||||||
const next = [...config.phases];
|
const next = [...config.phases];
|
||||||
next[idx] = { ...next[idx], [field]: value };
|
next[idx] = { ...next[idx], [field]: value };
|
||||||
update({ phases: next });
|
update({ phases: next });
|
||||||
};
|
};
|
||||||
|
|
||||||
// ─── 상세 유형 관리 ───
|
|
||||||
const addDetailType = () => {
|
const addDetailType = () => {
|
||||||
update({
|
update({
|
||||||
detailTypes: [
|
detailTypes: [
|
||||||
...config.detailTypes,
|
...config.detailTypes,
|
||||||
{ value: `TYPE_${config.detailTypes.length + 1}`, label: "신규 유형" },
|
{
|
||||||
|
value: `TYPE_${config.detailTypes.length + 1}`,
|
||||||
|
label: "신규 유형",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
@ -85,7 +171,11 @@ export const V2ProcessWorkStandardConfigPanel: React.FC<V2ProcessWorkStandardCon
|
||||||
update({ detailTypes: config.detailTypes.filter((_, i) => i !== idx) });
|
update({ detailTypes: config.detailTypes.filter((_, i) => i !== idx) });
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateDetailType = (idx: number, field: keyof DetailTypeDefinition, value: string) => {
|
const updateDetailType = (
|
||||||
|
idx: number,
|
||||||
|
field: keyof DetailTypeDefinition,
|
||||||
|
value: string,
|
||||||
|
) => {
|
||||||
const next = [...config.detailTypes];
|
const next = [...config.detailTypes];
|
||||||
next[idx] = { ...next[idx], [field]: value };
|
next[idx] = { ...next[idx], [field]: value };
|
||||||
update({ detailTypes: next });
|
update({ detailTypes: next });
|
||||||
|
|
@ -93,31 +183,75 @@ export const V2ProcessWorkStandardConfigPanel: React.FC<V2ProcessWorkStandardCon
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* ─── 1단계: 작업 단계 설정 (Collapsible + 접이식 카드) ─── */}
|
{/* 품목 목록 모드 */}
|
||||||
|
<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}>
|
<Collapsible open={phasesOpen} onOpenChange={setPhasesOpen}>
|
||||||
<CollapsibleTrigger asChild>
|
<CollapsibleTrigger asChild>
|
||||||
<button
|
<button
|
||||||
type="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"
|
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">
|
<div className="flex items-center gap-2">
|
||||||
<Layers className="h-4 w-4 text-muted-foreground" />
|
<Layers className="text-muted-foreground h-4 w-4" />
|
||||||
<span className="text-sm font-medium">작업 단계</span>
|
<span className="text-sm font-medium">작업 단계</span>
|
||||||
<Badge variant="secondary" className="text-[10px] h-5">
|
<Badge variant="secondary" className="h-5 text-[10px]">
|
||||||
{config.phases.length}개
|
{config.phases.length}개
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<ChevronDown
|
<ChevronDown
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-4 w-4 text-muted-foreground transition-transform duration-200",
|
"text-muted-foreground h-4 w-4 transition-transform duration-200",
|
||||||
phasesOpen && "rotate-180",
|
phasesOpen && "rotate-180",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
<CollapsibleContent>
|
<CollapsibleContent>
|
||||||
<div className="rounded-b-lg border border-t-0 p-3 space-y-1.5">
|
<div className="space-y-1.5 rounded-b-lg border border-t-0 p-3">
|
||||||
<p className="text-[10px] text-muted-foreground mb-1">공정별 작업 단계(Phase)를 정의</p>
|
<p className="text-muted-foreground mb-1 text-[10px]">
|
||||||
|
공정별 작업 단계를 정의
|
||||||
|
</p>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{config.phases.map((phase, idx) => (
|
{config.phases.map((phase, idx) => (
|
||||||
<Collapsible key={idx}>
|
<Collapsible key={idx}>
|
||||||
|
|
@ -125,18 +259,30 @@ export const V2ProcessWorkStandardConfigPanel: React.FC<V2ProcessWorkStandardCon
|
||||||
<CollapsibleTrigger asChild>
|
<CollapsibleTrigger asChild>
|
||||||
<button
|
<button
|
||||||
type="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"
|
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="h-3 w-3 text-muted-foreground transition-transform [[data-state=open]>&]:rotate-90 shrink-0" />
|
<ChevronRight className="text-muted-foreground h-3 w-3 shrink-0 transition-transform [[data-state=open]>&]:rotate-90" />
|
||||||
<span className="text-[10px] text-muted-foreground font-medium shrink-0">#{idx + 1}</span>
|
<span className="text-muted-foreground shrink-0 text-[10px] font-medium">
|
||||||
<span className="text-xs font-medium truncate flex-1 min-w-0">{phase.label}</span>
|
#{idx + 1}
|
||||||
<Badge variant="outline" className="text-[9px] h-4 shrink-0">{phase.key}</Badge>
|
</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
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={(e) => { e.stopPropagation(); removePhase(idx); }}
|
onClick={(e) => {
|
||||||
className="h-5 w-5 p-0 text-muted-foreground hover:text-destructive shrink-0"
|
e.stopPropagation();
|
||||||
|
removePhase(idx);
|
||||||
|
}}
|
||||||
|
className="text-muted-foreground hover:text-destructive h-5 w-5 shrink-0 p-0"
|
||||||
disabled={config.phases.length <= 1}
|
disabled={config.phases.length <= 1}
|
||||||
>
|
>
|
||||||
<Trash2 className="h-3 w-3" />
|
<Trash2 className="h-3 w-3" />
|
||||||
|
|
@ -146,32 +292,45 @@ export const V2ProcessWorkStandardConfigPanel: React.FC<V2ProcessWorkStandardCon
|
||||||
<CollapsibleContent>
|
<CollapsibleContent>
|
||||||
<div className="grid grid-cols-3 gap-1.5 border-t px-2.5 py-2">
|
<div className="grid grid-cols-3 gap-1.5 border-t px-2.5 py-2">
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<span className="text-[10px] text-muted-foreground">키</span>
|
<span className="text-muted-foreground text-[10px]">
|
||||||
|
키
|
||||||
|
</span>
|
||||||
<Input
|
<Input
|
||||||
value={phase.key}
|
value={phase.key}
|
||||||
onChange={(e) => updatePhase(idx, "key", e.target.value)}
|
onChange={(e) =>
|
||||||
|
updatePhase(idx, "key", e.target.value)
|
||||||
|
}
|
||||||
className="h-7 text-xs"
|
className="h-7 text-xs"
|
||||||
placeholder="키"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<span className="text-[10px] text-muted-foreground">표시명</span>
|
<span className="text-muted-foreground text-[10px]">
|
||||||
|
표시명
|
||||||
|
</span>
|
||||||
<Input
|
<Input
|
||||||
value={phase.label}
|
value={phase.label}
|
||||||
onChange={(e) => updatePhase(idx, "label", e.target.value)}
|
onChange={(e) =>
|
||||||
|
updatePhase(idx, "label", e.target.value)
|
||||||
|
}
|
||||||
className="h-7 text-xs"
|
className="h-7 text-xs"
|
||||||
placeholder="표시명"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<span className="text-[10px] text-muted-foreground">순서</span>
|
<span className="text-muted-foreground text-[10px]">
|
||||||
|
순서
|
||||||
|
</span>
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
min={1}
|
min={1}
|
||||||
value={phase.sortOrder}
|
value={phase.sortOrder}
|
||||||
onChange={(e) => updatePhase(idx, "sortOrder", parseInt(e.target.value) || 1)}
|
onChange={(e) =>
|
||||||
className="h-7 text-xs text-center"
|
updatePhase(
|
||||||
placeholder="1"
|
idx,
|
||||||
|
"sortOrder",
|
||||||
|
parseInt(e.target.value) || 1,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className="h-7 text-center text-xs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -183,7 +342,7 @@ export const V2ProcessWorkStandardConfigPanel: React.FC<V2ProcessWorkStandardCon
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="h-7 w-full gap-1 text-xs border-dashed"
|
className="h-7 w-full gap-1 border-dashed text-xs"
|
||||||
onClick={addPhase}
|
onClick={addPhase}
|
||||||
>
|
>
|
||||||
<Plus className="h-3 w-3" />
|
<Plus className="h-3 w-3" />
|
||||||
|
|
@ -193,31 +352,33 @@ export const V2ProcessWorkStandardConfigPanel: React.FC<V2ProcessWorkStandardCon
|
||||||
</CollapsibleContent>
|
</CollapsibleContent>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
|
|
||||||
{/* ─── 2단계: 상세 유형 옵션 (Collapsible + 접이식 카드) ─── */}
|
{/* 상세 유형 */}
|
||||||
<Collapsible open={detailTypesOpen} onOpenChange={setDetailTypesOpen}>
|
<Collapsible open={detailTypesOpen} onOpenChange={setDetailTypesOpen}>
|
||||||
<CollapsibleTrigger asChild>
|
<CollapsibleTrigger asChild>
|
||||||
<button
|
<button
|
||||||
type="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"
|
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">
|
<div className="flex items-center gap-2">
|
||||||
<List className="h-4 w-4 text-muted-foreground" />
|
<List className="text-muted-foreground h-4 w-4" />
|
||||||
<span className="text-sm font-medium">상세 유형</span>
|
<span className="text-sm font-medium">상세 유형</span>
|
||||||
<Badge variant="secondary" className="text-[10px] h-5">
|
<Badge variant="secondary" className="h-5 text-[10px]">
|
||||||
{config.detailTypes.length}개
|
{config.detailTypes.length}개
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<ChevronDown
|
<ChevronDown
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-4 w-4 text-muted-foreground transition-transform duration-200",
|
"text-muted-foreground h-4 w-4 transition-transform duration-200",
|
||||||
detailTypesOpen && "rotate-180",
|
detailTypesOpen && "rotate-180",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
<CollapsibleContent>
|
<CollapsibleContent>
|
||||||
<div className="rounded-b-lg border border-t-0 p-3 space-y-1.5">
|
<div className="space-y-1.5 rounded-b-lg border border-t-0 p-3">
|
||||||
<p className="text-[10px] text-muted-foreground mb-1">작업 항목의 상세 유형 드롭다운 옵션</p>
|
<p className="text-muted-foreground mb-1 text-[10px]">
|
||||||
|
작업 항목의 상세 유형 옵션
|
||||||
|
</p>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{config.detailTypes.map((dt, idx) => (
|
{config.detailTypes.map((dt, idx) => (
|
||||||
<Collapsible key={idx}>
|
<Collapsible key={idx}>
|
||||||
|
|
@ -225,18 +386,30 @@ export const V2ProcessWorkStandardConfigPanel: React.FC<V2ProcessWorkStandardCon
|
||||||
<CollapsibleTrigger asChild>
|
<CollapsibleTrigger asChild>
|
||||||
<button
|
<button
|
||||||
type="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"
|
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="h-3 w-3 text-muted-foreground transition-transform [[data-state=open]>&]:rotate-90 shrink-0" />
|
<ChevronRight className="text-muted-foreground h-3 w-3 shrink-0 transition-transform [[data-state=open]>&]:rotate-90" />
|
||||||
<span className="text-[10px] text-muted-foreground font-medium shrink-0">#{idx + 1}</span>
|
<span className="text-muted-foreground shrink-0 text-[10px] font-medium">
|
||||||
<span className="text-xs font-medium truncate flex-1 min-w-0">{dt.label}</span>
|
#{idx + 1}
|
||||||
<Badge variant="outline" className="text-[9px] h-4 shrink-0">{dt.value}</Badge>
|
</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
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={(e) => { e.stopPropagation(); removeDetailType(idx); }}
|
onClick={(e) => {
|
||||||
className="h-5 w-5 p-0 text-muted-foreground hover:text-destructive shrink-0"
|
e.stopPropagation();
|
||||||
|
removeDetailType(idx);
|
||||||
|
}}
|
||||||
|
className="text-muted-foreground hover:text-destructive h-5 w-5 shrink-0 p-0"
|
||||||
disabled={config.detailTypes.length <= 1}
|
disabled={config.detailTypes.length <= 1}
|
||||||
>
|
>
|
||||||
<Trash2 className="h-3 w-3" />
|
<Trash2 className="h-3 w-3" />
|
||||||
|
|
@ -246,21 +419,27 @@ export const V2ProcessWorkStandardConfigPanel: React.FC<V2ProcessWorkStandardCon
|
||||||
<CollapsibleContent>
|
<CollapsibleContent>
|
||||||
<div className="grid grid-cols-2 gap-1.5 border-t px-2.5 py-2">
|
<div className="grid grid-cols-2 gap-1.5 border-t px-2.5 py-2">
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<span className="text-[10px] text-muted-foreground">값</span>
|
<span className="text-muted-foreground text-[10px]">
|
||||||
|
값
|
||||||
|
</span>
|
||||||
<Input
|
<Input
|
||||||
value={dt.value}
|
value={dt.value}
|
||||||
onChange={(e) => updateDetailType(idx, "value", e.target.value)}
|
onChange={(e) =>
|
||||||
|
updateDetailType(idx, "value", e.target.value)
|
||||||
|
}
|
||||||
className="h-7 text-xs"
|
className="h-7 text-xs"
|
||||||
placeholder="값"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<span className="text-[10px] text-muted-foreground">표시명</span>
|
<span className="text-muted-foreground text-[10px]">
|
||||||
|
표시명
|
||||||
|
</span>
|
||||||
<Input
|
<Input
|
||||||
value={dt.label}
|
value={dt.label}
|
||||||
onChange={(e) => updateDetailType(idx, "label", e.target.value)}
|
onChange={(e) =>
|
||||||
|
updateDetailType(idx, "label", e.target.value)
|
||||||
|
}
|
||||||
className="h-7 text-xs"
|
className="h-7 text-xs"
|
||||||
placeholder="표시명"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -272,7 +451,7 @@ export const V2ProcessWorkStandardConfigPanel: React.FC<V2ProcessWorkStandardCon
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="h-7 w-full gap-1 text-xs border-dashed"
|
className="h-7 w-full gap-1 border-dashed text-xs"
|
||||||
onClick={addDetailType}
|
onClick={addDetailType}
|
||||||
>
|
>
|
||||||
<Plus className="h-3 w-3" />
|
<Plus className="h-3 w-3" />
|
||||||
|
|
@ -282,46 +461,83 @@ export const V2ProcessWorkStandardConfigPanel: React.FC<V2ProcessWorkStandardCon
|
||||||
</CollapsibleContent>
|
</CollapsibleContent>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
|
|
||||||
{/* ─── 3단계: 고급 설정 (데이터 소스 + 레이아웃 통합) ─── */}
|
{/* 데이터 소스 (테이블만) */}
|
||||||
<Collapsible open={advancedOpen} onOpenChange={setAdvancedOpen}>
|
<Collapsible open={dataSourceOpen} onOpenChange={setDataSourceOpen}>
|
||||||
<CollapsibleTrigger asChild>
|
<CollapsibleTrigger asChild>
|
||||||
<button
|
<button
|
||||||
type="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"
|
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">
|
<div className="flex items-center gap-2">
|
||||||
<Settings className="h-4 w-4 text-muted-foreground" />
|
<Database className="text-muted-foreground h-4 w-4" />
|
||||||
<span className="text-sm font-medium">고급 설정</span>
|
<span className="text-sm font-medium">테이블 설정</span>
|
||||||
</div>
|
</div>
|
||||||
<ChevronDown
|
<ChevronDown
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-4 w-4 text-muted-foreground transition-transform duration-200",
|
"text-muted-foreground h-4 w-4 transition-transform duration-200",
|
||||||
advancedOpen && "rotate-180",
|
dataSourceOpen && "rotate-180",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
<CollapsibleContent>
|
<CollapsibleContent>
|
||||||
<div className="rounded-b-lg border border-t-0 p-3 space-y-3">
|
<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>
|
||||||
|
|
||||||
{/* 레이아웃 기본 설정 */}
|
{/* 레이아웃 & 기타 */}
|
||||||
<div className="space-y-2">
|
<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 className="flex items-center justify-between py-1">
|
||||||
<div>
|
<div>
|
||||||
<span className="text-xs text-muted-foreground">좌측 패널 비율 (%)</span>
|
<span className="text-muted-foreground text-xs">
|
||||||
<p className="text-[10px] text-muted-foreground mt-0.5">품목/공정 선택 패널의 너비</p>
|
좌측 패널 비율 (%)
|
||||||
|
</span>
|
||||||
|
<p className="text-muted-foreground mt-0.5 text-[10px]">
|
||||||
|
품목/공정 선택 패널의 너비
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
min={15}
|
min={15}
|
||||||
max={50}
|
max={50}
|
||||||
value={config.splitRatio || 30}
|
value={config.splitRatio || 30}
|
||||||
onChange={(e) => update({ splitRatio: parseInt(e.target.value) || 30 })}
|
onChange={(e) =>
|
||||||
|
update({ splitRatio: parseInt(e.target.value) || 30 })
|
||||||
|
}
|
||||||
className="h-7 w-[80px] text-xs"
|
className="h-7 w-[80px] text-xs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between py-1">
|
<div className="flex items-center justify-between py-1">
|
||||||
<span className="text-xs text-muted-foreground">좌측 패널 제목</span>
|
<span className="text-muted-foreground text-xs">
|
||||||
|
좌측 패널 제목
|
||||||
|
</span>
|
||||||
<Input
|
<Input
|
||||||
value={config.leftPanelTitle || ""}
|
value={config.leftPanelTitle || ""}
|
||||||
onChange={(e) => update({ leftPanelTitle: e.target.value })}
|
onChange={(e) => update({ leftPanelTitle: e.target.value })}
|
||||||
|
|
@ -332,7 +548,9 @@ export const V2ProcessWorkStandardConfigPanel: React.FC<V2ProcessWorkStandardCon
|
||||||
<div className="flex items-center justify-between py-1">
|
<div className="flex items-center justify-between py-1">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs">읽기 전용</p>
|
<p className="text-xs">읽기 전용</p>
|
||||||
<p className="text-[10px] text-muted-foreground">수정/삭제 버튼을 숨겨요</p>
|
<p className="text-muted-foreground text-[10px]">
|
||||||
|
수정/삭제 버튼을 숨겨요
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
<Switch
|
||||||
checked={config.readonly || false}
|
checked={config.readonly || false}
|
||||||
|
|
@ -340,128 +558,13 @@ export const V2ProcessWorkStandardConfigPanel: React.FC<V2ProcessWorkStandardCon
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 데이터 소스 (서브 Collapsible) */}
|
|
||||||
<Collapsible open={dataSourceOpen} onOpenChange={setDataSourceOpen}>
|
|
||||||
<CollapsibleTrigger asChild>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="flex w-full items-center justify-between rounded-md border px-3 py-2 transition-colors hover:bg-muted/30"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Database className="h-3.5 w-3.5 text-muted-foreground" />
|
|
||||||
<span className="text-xs 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-3.5 w-3.5 text-muted-foreground transition-transform",
|
|
||||||
dataSourceOpen && "rotate-180",
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</CollapsibleTrigger>
|
|
||||||
<CollapsibleContent className="space-y-2 pt-2">
|
|
||||||
<div className="space-y-1">
|
|
||||||
<span className="text-[10px] text-muted-foreground">품목 테이블</span>
|
|
||||||
<Input
|
|
||||||
value={config.dataSource.itemTable}
|
|
||||||
onChange={(e) => updateDataSource("itemTable", e.target.value)}
|
|
||||||
className="h-7 w-full text-xs"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
|
||||||
<div className="space-y-1">
|
|
||||||
<span className="text-[10px] text-muted-foreground">품목명 컬럼</span>
|
|
||||||
<Input
|
|
||||||
value={config.dataSource.itemNameColumn}
|
|
||||||
onChange={(e) => updateDataSource("itemNameColumn", e.target.value)}
|
|
||||||
className="h-7 text-xs"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1">
|
|
||||||
<span className="text-[10px] text-muted-foreground">품목코드 컬럼</span>
|
|
||||||
<Input
|
|
||||||
value={config.dataSource.itemCodeColumn}
|
|
||||||
onChange={(e) => updateDataSource("itemCodeColumn", e.target.value)}
|
|
||||||
className="h-7 text-xs"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1 pt-1">
|
|
||||||
<span className="text-[10px] text-muted-foreground">라우팅 버전 테이블</span>
|
|
||||||
<Input
|
|
||||||
value={config.dataSource.routingVersionTable}
|
|
||||||
onChange={(e) => updateDataSource("routingVersionTable", e.target.value)}
|
|
||||||
className="h-7 w-full text-xs"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
|
||||||
<div className="space-y-1">
|
|
||||||
<span className="text-[10px] text-muted-foreground">품목 연결 FK</span>
|
|
||||||
<Input
|
|
||||||
value={config.dataSource.routingFkColumn}
|
|
||||||
onChange={(e) => updateDataSource("routingFkColumn", e.target.value)}
|
|
||||||
className="h-7 text-xs"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1">
|
|
||||||
<span className="text-[10px] text-muted-foreground">버전명 컬럼</span>
|
|
||||||
<Input
|
|
||||||
value={config.dataSource.routingVersionNameColumn}
|
|
||||||
onChange={(e) => updateDataSource("routingVersionNameColumn", e.target.value)}
|
|
||||||
className="h-7 text-xs"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1 pt-1">
|
|
||||||
<span className="text-[10px] text-muted-foreground">라우팅 상세 테이블</span>
|
|
||||||
<Input
|
|
||||||
value={config.dataSource.routingDetailTable}
|
|
||||||
onChange={(e) => updateDataSource("routingDetailTable", e.target.value)}
|
|
||||||
className="h-7 w-full text-xs"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1 pt-1">
|
|
||||||
<span className="text-[10px] text-muted-foreground">공정 마스터 테이블</span>
|
|
||||||
<Input
|
|
||||||
value={config.dataSource.processTable}
|
|
||||||
onChange={(e) => updateDataSource("processTable", e.target.value)}
|
|
||||||
className="h-7 w-full text-xs"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
|
||||||
<div className="space-y-1">
|
|
||||||
<span className="text-[10px] text-muted-foreground">공정명 컬럼</span>
|
|
||||||
<Input
|
|
||||||
value={config.dataSource.processNameColumn}
|
|
||||||
onChange={(e) => updateDataSource("processNameColumn", e.target.value)}
|
|
||||||
className="h-7 text-xs"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1">
|
|
||||||
<span className="text-[10px] text-muted-foreground">공정코드 컬럼</span>
|
|
||||||
<Input
|
|
||||||
value={config.dataSource.processCodeColumn}
|
|
||||||
onChange={(e) => updateDataSource("processCodeColumn", e.target.value)}
|
|
||||||
className="h-7 text-xs"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CollapsibleContent>
|
|
||||||
</Collapsible>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</CollapsibleContent>
|
</CollapsibleContent>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
V2ProcessWorkStandardConfigPanel.displayName = "V2ProcessWorkStandardConfigPanel";
|
V2ProcessWorkStandardConfigPanel.displayName =
|
||||||
|
"V2ProcessWorkStandardConfigPanel";
|
||||||
|
|
||||||
export default V2ProcessWorkStandardConfigPanel;
|
export default V2ProcessWorkStandardConfigPanel;
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,113 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { Plus, Trash2, GripVertical } from "lucide-react";
|
import { Plus, Trash2, GripVertical, Check, ChevronsUpDown } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||||
|
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
import { ProcessWorkStandardConfig, WorkPhaseDefinition, DetailTypeDefinition } from "./types";
|
import { ProcessWorkStandardConfig, WorkPhaseDefinition, DetailTypeDefinition } from "./types";
|
||||||
import { defaultConfig } from "./config";
|
import { defaultConfig } from "./config";
|
||||||
|
|
||||||
|
interface TableInfo { tableName: string; displayName?: string; }
|
||||||
|
interface ColumnInfo { columnName: string; displayName?: string; dataType?: string; }
|
||||||
|
|
||||||
|
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="mt-1 h-8 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ColumnCombobox({ value, onChange, tableName, placeholder }: {
|
||||||
|
value: string; onChange: (v: 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="mt-1 h-8 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); 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
interface ConfigPanelProps {
|
interface ConfigPanelProps {
|
||||||
config: Partial<ProcessWorkStandardConfig>;
|
config: Partial<ProcessWorkStandardConfig>;
|
||||||
onChange: (config: Partial<ProcessWorkStandardConfig>) => void;
|
onChange: (config: Partial<ProcessWorkStandardConfig>) => void;
|
||||||
|
|
@ -19,6 +117,9 @@ export function ProcessWorkStandardConfigPanel({
|
||||||
config: configProp,
|
config: configProp,
|
||||||
onChange,
|
onChange,
|
||||||
}: ConfigPanelProps) {
|
}: ConfigPanelProps) {
|
||||||
|
const [tables, setTables] = useState<TableInfo[]>([]);
|
||||||
|
const [loadingTables, setLoadingTables] = useState(false);
|
||||||
|
|
||||||
const config: ProcessWorkStandardConfig = {
|
const config: ProcessWorkStandardConfig = {
|
||||||
...defaultConfig,
|
...defaultConfig,
|
||||||
...configProp,
|
...configProp,
|
||||||
|
|
@ -27,6 +128,20 @@ export function ProcessWorkStandardConfigPanel({
|
||||||
detailTypes: configProp?.detailTypes?.length ? configProp.detailTypes : defaultConfig.detailTypes,
|
detailTypes: configProp?.detailTypes?.length ? configProp.detailTypes : defaultConfig.detailTypes,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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 update = (partial: Partial<ProcessWorkStandardConfig>) => {
|
const update = (partial: Partial<ProcessWorkStandardConfig>) => {
|
||||||
onChange({ ...configProp, ...partial });
|
onChange({ ...configProp, ...partial });
|
||||||
};
|
};
|
||||||
|
|
@ -112,72 +227,40 @@ export function ProcessWorkStandardConfigPanel({
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-xs">품목 테이블</Label>
|
<Label className="text-xs">품목 테이블</Label>
|
||||||
<Input
|
<TableCombobox value={config.dataSource.itemTable} onChange={(v) => updateDataSource("itemTable", v)} tables={tables} loading={loadingTables} />
|
||||||
value={config.dataSource.itemTable}
|
|
||||||
onChange={(e) => updateDataSource("itemTable", e.target.value)}
|
|
||||||
className="mt-1 h-8 text-xs"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-xs">품목명 컬럼</Label>
|
<Label className="text-xs">품목명 컬럼</Label>
|
||||||
<Input
|
<ColumnCombobox value={config.dataSource.itemNameColumn} onChange={(v) => updateDataSource("itemNameColumn", v)} tableName={config.dataSource.itemTable} placeholder="품목명" />
|
||||||
value={config.dataSource.itemNameColumn}
|
|
||||||
onChange={(e) => updateDataSource("itemNameColumn", e.target.value)}
|
|
||||||
className="mt-1 h-8 text-xs"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-xs">품목코드 컬럼</Label>
|
<Label className="text-xs">품목코드 컬럼</Label>
|
||||||
<Input
|
<ColumnCombobox value={config.dataSource.itemCodeColumn} onChange={(v) => updateDataSource("itemCodeColumn", v)} tableName={config.dataSource.itemTable} placeholder="품목코드" />
|
||||||
value={config.dataSource.itemCodeColumn}
|
|
||||||
onChange={(e) => updateDataSource("itemCodeColumn", e.target.value)}
|
|
||||||
className="mt-1 h-8 text-xs"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-xs">라우팅 버전 테이블</Label>
|
<Label className="text-xs">라우팅 버전 테이블</Label>
|
||||||
<Input
|
<TableCombobox value={config.dataSource.routingVersionTable} onChange={(v) => updateDataSource("routingVersionTable", v)} tables={tables} loading={loadingTables} />
|
||||||
value={config.dataSource.routingVersionTable}
|
|
||||||
onChange={(e) => updateDataSource("routingVersionTable", e.target.value)}
|
|
||||||
className="mt-1 h-8 text-xs"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-xs">품목 연결 FK 컬럼</Label>
|
<Label className="text-xs">품목 연결 FK 컬럼</Label>
|
||||||
<Input
|
<ColumnCombobox value={config.dataSource.routingFkColumn} onChange={(v) => updateDataSource("routingFkColumn", v)} tableName={config.dataSource.routingVersionTable} placeholder="FK 컬럼" />
|
||||||
value={config.dataSource.routingFkColumn}
|
|
||||||
onChange={(e) => updateDataSource("routingFkColumn", e.target.value)}
|
|
||||||
className="mt-1 h-8 text-xs"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-xs">공정 마스터 테이블</Label>
|
<Label className="text-xs">공정 마스터 테이블</Label>
|
||||||
<Input
|
<TableCombobox value={config.dataSource.processTable} onChange={(v) => updateDataSource("processTable", v)} tables={tables} loading={loadingTables} />
|
||||||
value={config.dataSource.processTable}
|
|
||||||
onChange={(e) => updateDataSource("processTable", e.target.value)}
|
|
||||||
className="mt-1 h-8 text-xs"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-xs">공정명 컬럼</Label>
|
<Label className="text-xs">공정명 컬럼</Label>
|
||||||
<Input
|
<ColumnCombobox value={config.dataSource.processNameColumn} onChange={(v) => updateDataSource("processNameColumn", v)} tableName={config.dataSource.processTable} placeholder="공정명" />
|
||||||
value={config.dataSource.processNameColumn}
|
|
||||||
onChange={(e) => updateDataSource("processNameColumn", e.target.value)}
|
|
||||||
className="mt-1 h-8 text-xs"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-xs">공정코드 컬럼</Label>
|
<Label className="text-xs">공정코드 컬럼</Label>
|
||||||
<Input
|
<ColumnCombobox value={config.dataSource.processCodeColumn} onChange={(v) => updateDataSource("processCodeColumn", v)} tableName={config.dataSource.processTable} placeholder="공정코드" />
|
||||||
value={config.dataSource.processCodeColumn}
|
|
||||||
onChange={(e) => updateDataSource("processCodeColumn", e.target.value)}
|
|
||||||
className="mt-1 h-8 text-xs"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue