diff --git a/.cursor/mcp.json b/.cursor/mcp.json deleted file mode 100644 index d5e0ca4b..00000000 --- a/.cursor/mcp.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "mcpServers": { - "Framelink Figma MCP": { - "command": "npx", - "args": ["-y", "figma-developer-mcp", "--figma-api-key=figd_NrYdIWf-CnC23NyH6eMym7sBdfbZTuXyS91tI3VS", "--stdio"] - } - } -} diff --git a/.gitignore b/.gitignore index b6114eb7..197ad216 100644 --- a/.gitignore +++ b/.gitignore @@ -185,6 +185,9 @@ popdocs/ # 멀티 에이전트 MCP 태스크 큐 mcp-task-queue/ +.cursor/mcp.json .cursor/rules/multi-agent-pm.mdc .cursor/rules/multi-agent-worker.mdc .cursor/rules/multi-agent-tester.mdc +.cursor/rules/multi-agent-reviewer.mdc +.cursor/rules/multi-agent-knowledge.mdc diff --git a/frontend/components/v2/config-panels/V2ProcessWorkStandardConfigPanel.tsx b/frontend/components/v2/config-panels/V2ProcessWorkStandardConfigPanel.tsx index 33a7ae33..a2b7abdc 100644 --- a/frontend/components/v2/config-panels/V2ProcessWorkStandardConfigPanel.tsx +++ b/frontend/components/v2/config-panels/V2ProcessWorkStandardConfigPanel.tsx @@ -1,17 +1,33 @@ "use client"; /** - * V2 공정 작업기준 설정 패널 - * Progressive Disclosure: 작업 단계 -> 상세 유형 -> 고급 설정(접힘) + * V2 공정 작업기준 설정 패널 (간소화) */ -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; import { Input } from "@/components/ui/input"; import { Switch } from "@/components/ui/switch"; import { Button } from "@/components/ui/button"; -import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"; import { Badge } from "@/components/ui/badge"; -import { Settings, ChevronDown, ChevronRight, Plus, Trash2, 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 type { ProcessWorkStandardConfig, @@ -20,26 +36,87 @@ import type { } from "@/lib/registry/components/v2-process-work-standard/types"; import { defaultConfig } from "@/lib/registry/components/v2-process-work-standard/config"; +interface TableInfo { tableName: string; displayName?: string; } + +function TableCombobox({ value, onChange, tables, loading, label }: { + value: string; onChange: (v: string) => void; tables: TableInfo[]; loading: boolean; label: string; +}) { + const [open, setOpen] = useState(false); + const selected = tables.find((t) => t.tableName === value); + return ( +
+ {label} + + + + + + + + + 테이블을 찾을 수 없습니다. + + {tables.map((t) => ( + { onChange(t.tableName); setOpen(false); }} className="text-xs"> + +
+ {t.displayName || t.tableName} + {t.displayName && {t.tableName}} +
+
+ ))} +
+
+
+
+
+
+ ); +} + interface V2ProcessWorkStandardConfigPanelProps { config: Partial; onChange: (config: Partial) => void; } -export const V2ProcessWorkStandardConfigPanel: React.FC = ({ - config: configProp, - onChange, -}) => { +export const V2ProcessWorkStandardConfigPanel: React.FC< + V2ProcessWorkStandardConfigPanelProps +> = ({ config: configProp, onChange }) => { const [phasesOpen, setPhasesOpen] = useState(false); const [detailTypesOpen, setDetailTypesOpen] = useState(false); - const [advancedOpen, setAdvancedOpen] = useState(false); + const [layoutOpen, setLayoutOpen] = useState(false); const [dataSourceOpen, setDataSourceOpen] = useState(false); + const [tables, setTables] = useState([]); + const [loadingTables, setLoadingTables] = useState(false); + + useEffect(() => { + const loadTables = async () => { + setLoadingTables(true); + try { + const { tableManagementApi } = await import("@/lib/api/tableManagement"); + const res = await tableManagementApi.getTableList(); + if (res.success && res.data) { + setTables(res.data.map((t: any) => ({ tableName: t.tableName, displayName: t.displayName || t.tableName }))); + } + } catch { /* ignore */ } finally { setLoadingTables(false); } + }; + loadTables(); + }, []); const config: ProcessWorkStandardConfig = { ...defaultConfig, ...configProp, dataSource: { ...defaultConfig.dataSource, ...configProp?.dataSource }, - phases: configProp?.phases?.length ? configProp.phases : defaultConfig.phases, - detailTypes: configProp?.detailTypes?.length ? configProp.detailTypes : defaultConfig.detailTypes, + phases: configProp?.phases?.length + ? configProp.phases + : defaultConfig.phases, + detailTypes: configProp?.detailTypes?.length + ? configProp.detailTypes + : defaultConfig.detailTypes, }; const update = (partial: Partial) => { @@ -50,13 +127,16 @@ export const V2ProcessWorkStandardConfigPanel: React.FC { const nextOrder = config.phases.length + 1; update({ 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 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]; next[idx] = { ...next[idx], [field]: value }; update({ phases: next }); }; - // ─── 상세 유형 관리 ─── const addDetailType = () => { update({ 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 i !== idx) }); }; - const updateDetailType = (idx: number, field: keyof DetailTypeDefinition, value: string) => { + const updateDetailType = ( + idx: number, + field: keyof DetailTypeDefinition, + value: string, + ) => { const next = [...config.detailTypes]; next[idx] = { ...next[idx], [field]: value }; update({ detailTypes: next }); @@ -93,31 +183,75 @@ export const V2ProcessWorkStandardConfigPanel: React.FC - {/* ─── 1단계: 작업 단계 설정 (Collapsible + 접이식 카드) ─── */} + {/* 품목 목록 모드 */} +
+ 품목 목록 모드 +
+ + +
+ {config.itemListMode === "registered" && ( +

+ 품목별 라우팅 탭에서 등록한 품목만 표시됩니다. +

+ )} +
+ + {/* 작업 단계 */} -
-

공정별 작업 단계(Phase)를 정의

+
+

+ 공정별 작업 단계를 정의 +

{config.phases.map((phase, idx) => ( @@ -125,18 +259,30 @@ export const V2ProcessWorkStandardConfigPanel: React.FC -
-

작업 항목의 상세 유형 드롭다운 옵션

+
+

+ 작업 항목의 상세 유형 옵션 +

{config.detailTypes.map((dt, idx) => ( @@ -225,18 +386,30 @@ export const V2ProcessWorkStandardConfigPanel: React.FC -
+
+

+ 테이블만 선택하면 컬럼 정보는 엔티티 설정에서 자동으로 가져옵니다. +

+ updateDataSource("itemTable", v)} tables={tables} loading={loadingTables} /> + updateDataSource("routingVersionTable", v)} tables={tables} loading={loadingTables} /> + updateDataSource("routingDetailTable", v)} tables={tables} loading={loadingTables} /> + updateDataSource("processTable", v)} tables={tables} loading={loadingTables} /> +
+ + - {/* 레이아웃 기본 설정 */} -
-
-
- 좌측 패널 비율 (%) -

품목/공정 선택 패널의 너비

-
- update({ splitRatio: parseInt(e.target.value) || 30 })} - className="h-7 w-[80px] text-xs" - /> -
-
- 좌측 패널 제목 - update({ leftPanelTitle: e.target.value })} - placeholder="품목 및 공정 선택" - className="h-7 w-[140px] text-xs" - /> -
-
-
-

읽기 전용

-

수정/삭제 버튼을 숨겨요

-
- update({ readonly: checked })} - /> -
+ {/* 레이아웃 & 기타 */} + + + + + +
+
+
+ + 좌측 패널 비율 (%) + +

+ 품목/공정 선택 패널의 너비 +

+
+ + update({ splitRatio: parseInt(e.target.value) || 30 }) + } + className="h-7 w-[80px] text-xs" + /> +
+
+ + 좌측 패널 제목 + + update({ leftPanelTitle: e.target.value })} + placeholder="품목 및 공정 선택" + className="h-7 w-[140px] text-xs" + /> +
+
+
+

읽기 전용

+

+ 수정/삭제 버튼을 숨겨요 +

+
+ update({ readonly: checked })} + />
- - {/* 데이터 소스 (서브 Collapsible) */} - - - - - -
- 품목 테이블 - updateDataSource("itemTable", e.target.value)} - className="h-7 w-full text-xs" - /> -
-
-
- 품목명 컬럼 - updateDataSource("itemNameColumn", e.target.value)} - className="h-7 text-xs" - /> -
-
- 품목코드 컬럼 - updateDataSource("itemCodeColumn", e.target.value)} - className="h-7 text-xs" - /> -
-
-
- 라우팅 버전 테이블 - updateDataSource("routingVersionTable", e.target.value)} - className="h-7 w-full text-xs" - /> -
-
-
- 품목 연결 FK - updateDataSource("routingFkColumn", e.target.value)} - className="h-7 text-xs" - /> -
-
- 버전명 컬럼 - updateDataSource("routingVersionNameColumn", e.target.value)} - className="h-7 text-xs" - /> -
-
-
- 라우팅 상세 테이블 - updateDataSource("routingDetailTable", e.target.value)} - className="h-7 w-full text-xs" - /> -
-
- 공정 마스터 테이블 - updateDataSource("processTable", e.target.value)} - className="h-7 w-full text-xs" - /> -
-
-
- 공정명 컬럼 - updateDataSource("processNameColumn", e.target.value)} - className="h-7 text-xs" - /> -
-
- 공정코드 컬럼 - updateDataSource("processCodeColumn", e.target.value)} - className="h-7 text-xs" - /> -
-
-
-
-
@@ -462,6 +564,7 @@ export const V2ProcessWorkStandardConfigPanel: React.FC void; tables: TableInfo[]; loading: boolean; +}) { + const [open, setOpen] = useState(false); + const selected = tables.find((t) => t.tableName === value); + return ( + + + + + + + + + 테이블을 찾을 수 없습니다. + + {tables.map((t) => ( + { onChange(t.tableName); setOpen(false); }} className="text-xs"> + +
+ {t.displayName || t.tableName} + {t.displayName && {t.tableName}} +
+
+ ))} +
+
+
+
+
+ ); +} + +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([]); + 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 ( + + + + + + + + + 컬럼을 찾을 수 없습니다. + + {columns.map((c) => ( + { onChange(c.columnName); setOpen(false); }} className="text-xs"> + +
+ {c.displayName || c.columnName} + {c.displayName && {c.columnName}} +
+
+ ))} +
+
+
+
+
+ ); +} + interface ConfigPanelProps { config: Partial; onChange: (config: Partial) => void; @@ -19,6 +117,9 @@ export function ProcessWorkStandardConfigPanel({ config: configProp, onChange, }: ConfigPanelProps) { + const [tables, setTables] = useState([]); + const [loadingTables, setLoadingTables] = useState(false); + const config: ProcessWorkStandardConfig = { ...defaultConfig, ...configProp, @@ -27,6 +128,20 @@ export function ProcessWorkStandardConfigPanel({ 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) => { onChange({ ...configProp, ...partial }); }; @@ -112,72 +227,40 @@ export function ProcessWorkStandardConfigPanel({
- updateDataSource("itemTable", e.target.value)} - className="mt-1 h-8 text-xs" - /> + updateDataSource("itemTable", v)} tables={tables} loading={loadingTables} />
- updateDataSource("itemNameColumn", e.target.value)} - className="mt-1 h-8 text-xs" - /> + updateDataSource("itemNameColumn", v)} tableName={config.dataSource.itemTable} placeholder="품목명" />
- updateDataSource("itemCodeColumn", e.target.value)} - className="mt-1 h-8 text-xs" - /> + updateDataSource("itemCodeColumn", v)} tableName={config.dataSource.itemTable} placeholder="품목코드" />
- updateDataSource("routingVersionTable", e.target.value)} - className="mt-1 h-8 text-xs" - /> + updateDataSource("routingVersionTable", v)} tables={tables} loading={loadingTables} />
- updateDataSource("routingFkColumn", e.target.value)} - className="mt-1 h-8 text-xs" - /> + updateDataSource("routingFkColumn", v)} tableName={config.dataSource.routingVersionTable} placeholder="FK 컬럼" />
- updateDataSource("processTable", e.target.value)} - className="mt-1 h-8 text-xs" - /> + updateDataSource("processTable", v)} tables={tables} loading={loadingTables} />
- updateDataSource("processNameColumn", e.target.value)} - className="mt-1 h-8 text-xs" - /> + updateDataSource("processNameColumn", v)} tableName={config.dataSource.processTable} placeholder="공정명" />
- updateDataSource("processCodeColumn", e.target.value)} - className="mt-1 h-8 text-xs" - /> + updateDataSource("processCodeColumn", v)} tableName={config.dataSource.processTable} placeholder="공정코드" />