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:
kjs 2026-03-17 18:19:08 +09:00
parent ae4fe7a66e
commit 5e6261f51a
4 changed files with 454 additions and 273 deletions

View File

@ -1,8 +0,0 @@
{
"mcpServers": {
"Framelink Figma MCP": {
"command": "npx",
"args": ["-y", "figma-developer-mcp", "--figma-api-key=figd_NrYdIWf-CnC23NyH6eMym7sBdfbZTuXyS91tI3VS", "--stdio"]
}
}
}

3
.gitignore vendored
View File

@ -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

View File

@ -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,179 +461,102 @@ 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}>
<div className="flex items-center justify-between py-1"> <CollapsibleTrigger asChild>
<div> <button
<span className="text-xs text-muted-foreground"> (%)</span> type="button"
<p className="text-[10px] text-muted-foreground mt-0.5">/ </p> 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> >
<Input <div className="flex items-center gap-2">
type="number" <Settings className="text-muted-foreground h-4 w-4" />
min={15} <span className="text-sm font-medium"></span>
max={50} </div>
value={config.splitRatio || 30} <ChevronDown
onChange={(e) => update({ splitRatio: parseInt(e.target.value) || 30 })} className={cn(
className="h-7 w-[80px] text-xs" "text-muted-foreground h-4 w-4 transition-transform duration-200",
/> layoutOpen && "rotate-180",
</div> )}
<div className="flex items-center justify-between py-1"> />
<span className="text-xs text-muted-foreground"> </span> </button>
<Input </CollapsibleTrigger>
value={config.leftPanelTitle || ""} <CollapsibleContent>
onChange={(e) => update({ leftPanelTitle: e.target.value })} <div className="space-y-3 rounded-b-lg border border-t-0 p-4">
placeholder="품목 및 공정 선택" <div className="flex items-center justify-between py-1">
className="h-7 w-[140px] text-xs" <div>
/> <span className="text-muted-foreground text-xs">
</div> (%)
<div className="flex items-center justify-between py-1"> </span>
<div> <p className="text-muted-foreground mt-0.5 text-[10px]">
<p className="text-xs"> </p> /
<p className="text-[10px] text-muted-foreground">/ </p> </p>
</div> </div>
<Switch <Input
checked={config.readonly || false} type="number"
onCheckedChange={(checked) => update({ readonly: checked })} min={15}
/> max={50}
</div> 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>
{/* 데이터 소스 (서브 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> </div>
</CollapsibleContent> </CollapsibleContent>
</Collapsible> </Collapsible>
@ -462,6 +564,7 @@ export const V2ProcessWorkStandardConfigPanel: React.FC<V2ProcessWorkStandardCon
); );
}; };
V2ProcessWorkStandardConfigPanel.displayName = "V2ProcessWorkStandardConfigPanel"; V2ProcessWorkStandardConfigPanel.displayName =
"V2ProcessWorkStandardConfigPanel";
export default V2ProcessWorkStandardConfigPanel; export default V2ProcessWorkStandardConfigPanel;

View File

@ -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>