diff --git a/frontend/components/v2/config-panels/V2ItemRoutingConfigPanel.tsx b/frontend/components/v2/config-panels/V2ItemRoutingConfigPanel.tsx index 0ec5a29f..416b6975 100644 --- a/frontend/components/v2/config-panels/V2ItemRoutingConfigPanel.tsx +++ b/frontend/components/v2/config-panels/V2ItemRoutingConfigPanel.tsx @@ -13,8 +13,9 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@ 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, Plus, Trash2, Check, ChevronsUpDown, + Settings, ChevronDown, ChevronRight, Plus, Trash2, Check, ChevronsUpDown, Database, Monitor, Columns, } from "lucide-react"; import { cn } from "@/lib/utils"; @@ -144,10 +145,12 @@ function ColumnCombobox({ variant="outline" role="combobox" aria-expanded={open} - className="h-7 w-[140px] justify-between text-xs" + className="h-7 w-full justify-between text-xs" disabled={loading || !tableName} > - {loading ? "로딩..." : !tableName ? "테이블 먼저 선택" : selected ? selected.displayName || selected.columnName : placeholder || "컬럼 선택"} + + {loading ? "로딩..." : !tableName ? "테이블 먼저 선택" : selected ? selected.displayName || selected.columnName : placeholder || "컬럼 선택"} + @@ -220,10 +223,12 @@ function ScreenCombobox({ variant="outline" role="combobox" aria-expanded={open} - className="h-7 w-[140px] justify-between text-xs" + className="h-7 w-full justify-between text-xs" disabled={loading} > - {loading ? "로딩..." : selected ? selected.screenName : "화면 선택"} + + {loading ? "로딩..." : selected ? selected.screenName : "화면 선택"} + @@ -262,6 +267,8 @@ export const V2ItemRoutingConfigPanel: React.FC = }) => { const [tables, setTables] = useState([]); const [loadingTables, setLoadingTables] = useState(false); + const [modalOpen, setModalOpen] = useState(false); + const [columnsOpen, setColumnsOpen] = useState(false); const [dataSourceOpen, setDataSourceOpen] = useState(false); const [layoutOpen, setLayoutOpen] = useState(false); @@ -344,107 +351,172 @@ export const V2ItemRoutingConfigPanel: React.FC = return (
- {/* ─── 1단계: 모달 연동 ─── */} -
-
- -

모달 연동

-
-

버전 추가/공정 추가·수정 시 열리는 화면을 설정해요

-
- -
-
- 버전 추가 화면 - updateModals("versionAddScreenId", v)} - /> -
-
- 공정 추가 화면 - updateModals("processAddScreenId", v)} - /> -
-
- 공정 수정 화면 - updateModals("processEditScreenId", v)} - /> -
-
- - {/* ─── 2단계: 공정 테이블 컬럼 ─── */} -
-
- -

공정 테이블 컬럼

-
-

공정 순서 테이블에 표시할 컬럼을 설정해요

-
- -
- {config.processColumns.map((col, idx) => ( -
+ + + + +
+

버전 추가/공정 추가·수정 시 열리는 화면

+
+
+ 버전 추가 + updateModals("versionAddScreenId", v)} + /> +
+
+ 공정 추가 + updateModals("processAddScreenId", v)} + /> +
+
+ 공정 수정 + updateModals("processEditScreenId", v)} + /> +
+
+
+
+ + + {/* ─── 2단계: 공정 테이블 컬럼 (Collapsible + 접이식 카드) ─── */} + + + + + +
+

공정 순서 테이블에 표시할 컬럼

+
+ {config.processColumns.map((col, idx) => ( + +
+ + + + + +
+
+ 컬럼명 + updateColumn(idx, "name", e.target.value)} + className="h-7 text-xs" + placeholder="컬럼명" + /> +
+
+ 표시명 + updateColumn(idx, "label", e.target.value)} + className="h-7 text-xs" + placeholder="표시명" + /> +
+
+ 너비 + updateColumn(idx, "width", parseInt(e.target.value) || 100)} + className="h-7 text-xs" + placeholder="100" + /> +
+
+ 정렬 + +
+
+
+
+
+ ))} +
- ))} - - -
+ + {/* ─── 3단계: 데이터 소스 (Collapsible) ─── */} @@ -456,6 +528,11 @@ export const V2ItemRoutingConfigPanel: React.FC =
데이터 소스 설정 + {config.dataSource.itemTable && ( + + {config.dataSource.itemTable} + + )}
= loading={loadingTables} />
-
+
품목명 컬럼 = placeholder="품목명" />
-
+
품목코드 컬럼 = loading={loadingTables} />
-
+
품목 FK 컬럼 = placeholder="FK 컬럼" />
-
+
버전명 컬럼 = loading={loadingTables} />
-
+
버전 FK 컬럼 = loading={loadingTables} />
-
+
공정명 컬럼 = placeholder="공정명" />
-
+
공정코드 컬럼 작업 단계 관리 -> 상세 유형 관리 -> 레이아웃(접힘) + * Progressive Disclosure: 작업 단계 -> 상세 유형 -> 고급 설정(접힘) */ import React, { useState } from "react"; @@ -10,7 +10,8 @@ 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 { Settings, ChevronDown, Plus, Trash2, GripVertical, Database, Layers } from "lucide-react"; +import { Badge } from "@/components/ui/badge"; +import { Settings, ChevronDown, ChevronRight, Plus, Trash2, Database, Layers, List } from "lucide-react"; import { cn } from "@/lib/utils"; import type { ProcessWorkStandardConfig, @@ -28,8 +29,10 @@ export const V2ProcessWorkStandardConfigPanel: React.FC { + const [phasesOpen, setPhasesOpen] = useState(false); + const [detailTypesOpen, setDetailTypesOpen] = useState(false); + const [advancedOpen, setAdvancedOpen] = useState(false); const [dataSourceOpen, setDataSourceOpen] = useState(false); - const [layoutOpen, setLayoutOpen] = useState(false); const config: ProcessWorkStandardConfig = { ...defaultConfig, @@ -90,220 +93,197 @@ export const V2ProcessWorkStandardConfigPanel: React.FC - {/* ─── 1단계: 작업 단계 설정 ─── */} -
-
- -

작업 단계 설정

-
-

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

-
- -
- {config.phases.map((phase, idx) => ( -
- - updatePhase(idx, "key", e.target.value)} - className="h-7 w-20 text-[10px]" - placeholder="키" - /> - updatePhase(idx, "label", e.target.value)} - className="h-7 flex-1 text-[10px]" - placeholder="표시명" - /> - updatePhase(idx, "sortOrder", parseInt(e.target.value) || 1)} - className="h-7 w-12 text-[10px] text-center" - placeholder="순서" - title="정렬 순서" - /> - -
- ))} - - -
- - {/* ─── 2단계: 상세 유형 옵션 ─── */} -
-

상세 유형 옵션

-

작업 항목의 상세 유형 드롭다운 옵션을 설정해요

-
- -
- {config.detailTypes.map((dt, idx) => ( -
- updateDetailType(idx, "value", e.target.value)} - className="h-7 w-24 text-[10px]" - placeholder="값" - /> - updateDetailType(idx, "label", e.target.value)} - className="h-7 flex-1 text-[10px]" - placeholder="표시명" - /> - -
- ))} - - -
- - {/* ─── 3단계: 데이터 소스 (Collapsible) ─── */} - + {/* ─── 1단계: 작업 단계 설정 (Collapsible + 접이식 카드) ─── */} + -
-
- 품목 테이블 - updateDataSource("itemTable", e.target.value)} - className="h-7 w-[160px] text-xs" - /> -
-
- 품목명 컬럼 - updateDataSource("itemNameColumn", e.target.value)} - className="h-7 w-[160px] text-xs" - /> -
-
- 품목코드 컬럼 - updateDataSource("itemCodeColumn", e.target.value)} - className="h-7 w-[160px] text-xs" - /> -
-
- 라우팅 버전 테이블 - updateDataSource("routingVersionTable", e.target.value)} - className="h-7 w-[160px] text-xs" - /> -
-
- 품목 연결 FK - updateDataSource("routingFkColumn", e.target.value)} - className="h-7 w-[160px] text-xs" - /> -
-
- 버전명 컬럼 - updateDataSource("routingVersionNameColumn", e.target.value)} - className="h-7 w-[160px] text-xs" - /> -
-
- 라우팅 상세 테이블 - updateDataSource("routingDetailTable", e.target.value)} - className="h-7 w-[160px] text-xs" - /> -
-
- 공정 마스터 테이블 - updateDataSource("processTable", e.target.value)} - className="h-7 w-[160px] text-xs" - /> -
-
- 공정명 컬럼 - updateDataSource("processNameColumn", e.target.value)} - className="h-7 w-[160px] text-xs" - /> -
-
- 공정코드 컬럼 - updateDataSource("processCodeColumn", e.target.value)} - className="h-7 w-[160px] text-xs" - /> +
+

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

+
+ {config.phases.map((phase, idx) => ( + +
+ + + + + +
+
+ + updatePhase(idx, "key", e.target.value)} + className="h-7 text-xs" + placeholder="키" + /> +
+
+ 표시명 + updatePhase(idx, "label", e.target.value)} + className="h-7 text-xs" + placeholder="표시명" + /> +
+
+ 순서 + updatePhase(idx, "sortOrder", parseInt(e.target.value) || 1)} + className="h-7 text-xs text-center" + placeholder="1" + /> +
+
+
+
+
+ ))}
+
- {/* ─── 4단계: 레이아웃 & 기타 (Collapsible) ─── */} - + {/* ─── 2단계: 상세 유형 옵션 (Collapsible + 접이식 카드) ─── */} + + + + + +
+

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

+
+ {config.detailTypes.map((dt, idx) => ( + +
+ + + + + +
+
+ + updateDetailType(idx, "value", e.target.value)} + className="h-7 text-xs" + placeholder="값" + /> +
+
+ 표시명 + updateDetailType(idx, "label", e.target.value)} + className="h-7 text-xs" + placeholder="표시명" + /> +
+
+
+
+
+ ))} +
+ +
+
+
+ + {/* ─── 3단계: 고급 설정 (데이터 소스 + 레이아웃 통합) ─── */} + -
-
-
- 좌측 패널 비율 (%) -

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

+
+ + {/* 레이아웃 기본 설정 */} +
+
+
+ 좌측 패널 비율 (%) +

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

+
+ 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-[160px] text-xs" - /> -
+ {/* 데이터 소스 (서브 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" + /> +
+
+
+
-
-
-

읽기 전용

-

수정/삭제 버튼을 숨겨요

-
- update({ readonly: checked })} - /> -
diff --git a/frontend/components/v2/config-panels/V2SelectedItemsDetailInputConfigPanel.tsx b/frontend/components/v2/config-panels/V2SelectedItemsDetailInputConfigPanel.tsx index b8ecd32a..28e846d4 100644 --- a/frontend/components/v2/config-panels/V2SelectedItemsDetailInputConfigPanel.tsx +++ b/frontend/components/v2/config-panels/V2SelectedItemsDetailInputConfigPanel.tsx @@ -36,6 +36,7 @@ import { CommandList, } from "@/components/ui/command"; import { Separator } from "@/components/ui/separator"; +import { Badge } from "@/components/ui/badge"; import { Database, Table2, @@ -44,6 +45,7 @@ import { Trash2, Settings, ChevronDown, + ChevronRight, Check, ChevronsUpDown, Link2, @@ -793,6 +795,11 @@ export const V2SelectedItemsDetailInputConfigPanel: React.FC<
표시 컬럼 (원본 데이터) + {displayColumns.length > 0 && ( + + {displayColumns.length}개 + + )}

전달받은 원본 데이터 중 화면에 표시할 컬럼 @@ -800,24 +807,22 @@ export const V2SelectedItemsDetailInputConfigPanel: React.FC<

{displayColumns.length > 0 && ( -
+
{displayColumns.map((col) => (
-
- {col.label} - - {col.name} - -
+ {col.label} + + {col.name} + @@ -890,688 +895,640 @@ export const V2SelectedItemsDetailInputConfigPanel: React.FC< - {/* ════════ 4단계: 추가 입력 필드 ════════ */} -
-
- - 추가 입력 필드 -
-

- 저장 대상 테이블의 컬럼을 입력 필드로 추가 -

-
- - {localFields.length > 0 && ( -
- {localFields.map((field, index) => ( -
-
- - 필드 {index + 1}: {field.label || field.name} - - -
- -
- {/* 필드명 (컬럼 선택) */} -
- - - updateField(index, { - name, - label: col.columnLabel || name, - inputType: col.inputType || "text", - codeCategory: col.codeCategory, - }) - } - /> -
- - {/* 라벨 */} -
- - - updateField(index, { label: e.target.value }) - } - placeholder="필드 라벨" - className="h-7 text-xs" - /> -
-
- -
- {/* 타입 (자동) */} -
- - -
- - {/* Placeholder */} -
- - - updateField(index, { placeholder: e.target.value }) - } - placeholder="입력 안내" - className="h-7 text-xs" - /> -
-
- - {/* 필드 그룹 선택 */} - {localFieldGroups.length > 0 && ( -
- - -
- )} - - {/* 필수 / 자동 채우기 */} -
-
- - updateField(index, { required: checked }) - } - /> - -
- {field.autoFillFrom && ( - - 자동 채우기: {field.autoFillFrom} - - )} -
-
- ))} -
- )} - - - - - - {/* ════════ 접히는 섹션들 ════════ */} - - {/* ─── 필드 그룹 관리 (Collapsible) ─── */} + {/* ════════ 4단계: 추가 입력 필드 (Collapsible) ════════ */} toggleSection("fieldGroups")} + open={openSections["inputFields"] ?? (localFields.length > 0)} + onOpenChange={(open) => setOpenSections((prev) => ({ ...prev, inputFields: open }))} > - -
- - 필드 그룹 관리 - {localFieldGroups.length > 0 && ( - - {localFieldGroups.length} - - )} -
- -
- -

- 추가 입력 필드를 여러 카드로 나눠서 표시 (예: 거래처 정보, 단가 - 정보) -

- - {localFieldGroups.map((group, index) => ( -
-
- - 그룹 {index + 1}: {group.title} - - -
- -
-
- - - updateFieldGroup(group.id, { id: e.target.value }) - } - className="h-7 text-xs" - /> -
-
- - - updateFieldGroup(group.id, { title: e.target.value }) - } - className="h-7 text-xs" - /> -
-
- -
-
- - - updateFieldGroup(group.id, { - description: e.target.value, - }) - } - placeholder="그룹 설명" - className="h-7 text-xs" - /> -
-
- - - updateFieldGroup(group.id, { - order: parseInt(e.target.value) || 0, - }) - } - className="h-7 text-xs" - min="0" - /> -
-
- - {/* 소스 테이블 (그룹별) */} -
- - - updateFieldGroup(group.id, { sourceTable: v }) - } - /> -
- - {/* 최대 항목 수 */} -
- - - updateFieldGroup(group.id, { - maxEntries: parseInt(e.target.value) || undefined, - }) - } - placeholder="무제한" - className="h-7 w-20 text-xs" - min="1" - /> -
-
- ))} - - +
+ + 입력 필드 + + {localFields.length}개 + +
+ 0)) && "rotate-180", + )} + /> + + + +
+

+ 저장 대상 테이블의 컬럼을 입력 필드로 추가 +

+ + {localFields.length > 0 && ( +
+ {localFields.map((field, index) => ( + +
+ + + + + +
+
+
+ + + updateField(index, { + name, + label: col.columnLabel || name, + inputType: col.inputType || "text", + codeCategory: col.codeCategory, + }) + } + /> +
+
+ + + updateField(index, { label: e.target.value }) + } + placeholder="필드 라벨" + className="h-7 text-xs" + /> +
+
+ +
+ + + updateField(index, { placeholder: e.target.value }) + } + placeholder="입력 안내" + className="h-7 text-xs" + /> +
+ + {localFieldGroups.length > 0 && ( +
+ + +
+ )} + +
+
+ + updateField(index, { required: checked }) + } + /> + +
+ {field.autoFillFrom && ( + + 자동: {field.autoFillFrom} + + )} +
+
+
+
+
+ ))} +
+ )} + + +
- {/* ─── 부모 데이터 매핑 (Collapsible) ─── */} + {/* ════════ 5단계: 고급 설정 (서브 Collapsible 통합) ════════ */} toggleSection("parentMapping")} + open={openSections["advanced"] ?? false} + onOpenChange={() => toggleSection("advanced")} > - -
- - 부모 데이터 매핑 - {parentMappings.length > 0 && ( - - {parentMappings.length} - - )} -
- + + - -

- 이전 화면(거래처 선택 등)에서 넘어온 데이터를 자동으로 매핑 -

+ +
- {parentMappings.map((mapping, index) => { - const isAutoDetected = autoDetectedFks.some( - (fk) => - fk.mappingType === "parent" && - fk.columnName === mapping.targetField, - ); - return ( -
- {isAutoDetected && ( - - FK 자동 감지 - - )} + {/* ─── 기본 고급 설정 ─── */} +
+
+ + handleChange("sourceKeyField", name)} + /> +

+ 대상 테이블에서 원본을 참조하는 FK 컬럼 +

+
- {/* 소스 테이블 */} -
- - { - updateParentMapping(index, { - sourceTable: v, - sourceField: "", - }); - loadMappingColumns(v, index); +
+ + handleChange("showIndex", v)} + /> +
+
+ + handleChange("allowRemove", v)} + /> +
+
+ + handleChange("disabled", v)} + /> +
+
+ + handleChange("readonly", v)} + /> +
+
+ + handleChange("emptyMessage", e.target.value)} + placeholder="전달받은 데이터가 없습니다." + className="h-8 text-xs" + /> +
+
+ + + + {/* ─── 필드 그룹 관리 (서브 Collapsible) ─── */} + toggleSection("fieldGroups")} + > + + + + +

+ 추가 입력 필드를 여러 카드로 나눠서 표시 (예: 거래처 정보, 단가 정보) +

+ + {localFieldGroups.map((group, index) => ( +
+
+ + 그룹 {index + 1}: {group.title} + + +
+
+
+ + updateFieldGroup(group.id, { id: e.target.value })} + className="h-7 text-xs" + /> +
+
+ + updateFieldGroup(group.id, { title: e.target.value })} + className="h-7 text-xs" + /> +
+
+
+
+ + updateFieldGroup(group.id, { description: e.target.value })} + placeholder="그룹 설명" + className="h-7 text-xs" + /> +
+
+ + updateFieldGroup(group.id, { order: parseInt(e.target.value) || 0 })} + className="h-7 text-xs" + min="0" + /> +
+
+
+ + updateFieldGroup(group.id, { sourceTable: v })} + /> +
+
+ + updateFieldGroup(group.id, { maxEntries: parseInt(e.target.value) || undefined })} + placeholder="무제한" + className="h-7 w-20 text-xs" + min="1" + /> +
+
+ ))} + + +
+
+ + {/* ─── 부모 데이터 매핑 (서브 Collapsible) ─── */} + toggleSection("parentMapping")} + > + + + + +

+ 이전 화면(거래처 선택 등)에서 넘어온 데이터를 자동으로 매핑 +

+ + {parentMappings.map((mapping, index) => { + const isAutoDetected = autoDetectedFks.some( + (fk) => fk.mappingType === "parent" && fk.columnName === mapping.targetField, + ); + return ( +
+ {isAutoDetected && ( + + FK 자동 감지 + + )} +
+ + { + updateParentMapping(index, { sourceTable: v, sourceField: "" }); + loadMappingColumns(v, index); + }} + /> +
+
+ + updateParentMapping(index, { sourceField: name })} + /> +
+
+ + updateParentMapping(index, { targetField: name })} + /> +
+
+ updateParentMapping(index, { defaultValue: e.target.value || undefined })} + placeholder="기본값 (선택)" + className="h-7 flex-1 text-xs" + /> + +
+
+ ); + })} + + +
+
+ + {/* ─── 자동 계산 (서브 Collapsible) ─── */} + toggleSection("autoCalc")} + > + + + + +
+ + { + if (checked) { + handleChange("autoCalculation", { + targetField: "", + mode: "template", + inputFields: { + basePrice: "", + discountType: "", + discountValue: "", + roundingType: "", + roundingUnit: "", + }, + calculationType: "price", + valueMapping: {}, + calculationSteps: [], + }); + } else { + handleChange("autoCalculation", undefined); + } }} />
- {/* 원본 필드 */} -
- - - updateParentMapping(index, { sourceField: name }) - } - /> -
- - {/* 저장 필드 */} -
- - - updateParentMapping(index, { targetField: name }) - } - /> -
- - {/* 기본값 + 삭제 */} -
- - updateParentMapping(index, { - defaultValue: e.target.value || undefined, - }) - } - placeholder="기본값 (선택)" - className="h-7 flex-1 text-xs" - /> - -
-
- ); - })} - - - - - - {/* ─── 자동 계산 설정 (Collapsible) ─── */} - toggleSection("autoCalc")} - > - -
- - 자동 계산 - {config.autoCalculation && ( - - 활성 - - )} -
- -
- -
- - { - if (checked) { - handleChange("autoCalculation", { - targetField: "", - mode: "template", - inputFields: { - basePrice: "", - discountType: "", - discountValue: "", - roundingType: "", - roundingUnit: "", - }, - calculationType: "price", - valueMapping: {}, - calculationSteps: [], - }); - } else { - handleChange("autoCalculation", undefined); - } - }} - /> -
- - {config.autoCalculation && ( -
- {/* 계산 모드 */} -
- - -
- - {/* 계산 결과 필드 */} -
- - -
- - {/* 템플릿 모드 필드 매핑 */} - {config.autoCalculation.mode === "template" && ( -
- - 필드 매핑 - - {( - [ - ["basePrice", "기준 단가"], - ["discountType", "할인 방식"], - ["discountValue", "할인값"], - ["roundingType", "반올림 방식"], - ["roundingUnit", "반올림 단위"], - ] as const - ).map(([key, label]) => ( -
- - {label} - + {config.autoCalculation && ( +
+
+ +
+ +
+ +
- ))} -
- )} -
- )} - - - {/* ─── 고급 설정 (Collapsible) ─── */} - toggleSection("advanced")} - > - -
- - 고급 설정 -
- -
- - {/* sourceKeyField */} -
- - handleChange("sourceKeyField", name)} - /> -

- 대상 테이블에서 원본을 참조하는 FK 컬럼 -

-
+ {config.autoCalculation.mode === "template" && ( +
+ 필드 매핑 + {( + [ + ["basePrice", "기준 단가"], + ["discountType", "할인 방식"], + ["discountValue", "할인값"], + ["roundingType", "반올림 방식"], + ["roundingUnit", "반올림 단위"], + ] as const + ).map(([key, label]) => ( +
+ {label} + +
+ ))} +
+ )} +
+ )} + + - {/* 항목 번호 표시 */} -
- - handleChange("showIndex", v)} - /> -
- - {/* 항목 삭제 허용 */} -
- - handleChange("allowRemove", v)} - /> -
- - {/* 비활성화 */} -
- - handleChange("disabled", v)} - /> -
- - {/* 읽기 전용 */} -
- - handleChange("readonly", v)} - /> -
- - {/* 빈 상태 메시지 */} -
- - - handleChange("emptyMessage", e.target.value) - } - placeholder="전달받은 데이터가 없습니다." - className="h-8 text-xs" - />
diff --git a/frontend/components/v2/config-panels/V2TableListConfigPanel.tsx b/frontend/components/v2/config-panels/V2TableListConfigPanel.tsx index 489cd1dc..7a65741e 100644 --- a/frontend/components/v2/config-panels/V2TableListConfigPanel.tsx +++ b/frontend/components/v2/config-panels/V2TableListConfigPanel.tsx @@ -49,7 +49,7 @@ import { SortableContext, useSortable, verticalListSortingStrategy, arrayMove } import { CSS } from "@dnd-kit/utilities"; import type { TableListConfig, ColumnConfig } from "@/lib/registry/components/v2-table-list/types"; -// ─── DnD 정렬 가능한 컬럼 행 ─── +// ─── DnD 정렬 가능한 컬럼 행 (접이식) ─── function SortableColumnRow({ id, col, @@ -69,40 +69,57 @@ function SortableColumnRow({ }) { const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id }); const style = { transform: CSS.Transform.toString(transform), transition }; + const [expanded, setExpanded] = useState(false); return (
-
- +
+
+ +
+ {isEntityJoin ? ( + + ) : ( + #{index + 1} + )} + + {col.width && ( + {col.width}px + )} +
- {isEntityJoin ? ( - - ) : ( - #{index + 1} + {expanded && ( +
+ onLabelChange(e.target.value)} + placeholder="표시명" + className="h-7 min-w-0 text-xs" + /> + onWidthChange(parseInt(e.target.value) || 100)} + placeholder="너비" + className="h-7 shrink-0 text-xs text-center" + /> +
)} - onLabelChange(e.target.value)} - placeholder="표시명" - className="h-6 min-w-0 flex-1 text-xs" - /> - onWidthChange(parseInt(e.target.value) || 100)} - placeholder="너비" - className="h-6 w-14 shrink-0 text-xs" - /> -
); } @@ -230,6 +247,11 @@ export const V2TableListConfigPanel: React.FC = ({ // Collapsible 상태 const [advancedOpen, setAdvancedOpen] = useState(false); const [entityDisplayOpen, setEntityDisplayOpen] = useState(false); + const [columnSelectOpen, setColumnSelectOpen] = useState(() => (config.columns?.length || 0) > 0); + const [entityJoinOpen, setEntityJoinOpen] = useState(false); + const [displayColumnsOpen, setDisplayColumnsOpen] = useState(() => (config.columns?.length || 0) > 0); + const [columnSearchText, setColumnSearchText] = useState(""); + const [entityJoinSubOpen, setEntityJoinSubOpen] = useState>({}); // 이전 컬럼 개수 추적 (엔티티 감지용) const prevColumnsLengthRef = useRef(0); @@ -740,149 +762,215 @@ export const V2TableListConfigPanel: React.FC = ({
{/* ═══════════════════════════════════════ */} - {/* 2단계: 컬럼 선택 */} + {/* 2단계: 컬럼 선택 (Collapsible) */} {/* ═══════════════════════════════════════ */} {targetTableName && availableColumns.length > 0 && ( <> -
- - - -
- {availableColumns.map((column) => { - const isAdded = config.columns?.some((c) => c.columnName === column.columnName); - return ( -
{ - if (isAdded) { - updateField("columns", config.columns?.filter((c) => c.columnName !== column.columnName) || []); - } else { - addColumn(column.columnName); - } - }} - > - { - if (isAdded) { - updateField("columns", config.columns?.filter((c) => c.columnName !== column.columnName) || []); - } else { - addColumn(column.columnName); - } - }} - className="pointer-events-none h-3.5 w-3.5" - /> - - {column.label || column.columnName} - {isAdded && ( - - )} - - {column.input_type || column.dataType} - -
- ); - })} -
-
- - {/* Entity 조인 컬럼 */} - {entityJoinColumns.joinTables.length > 0 && ( -
- - -
- {entityJoinColumns.joinTables.map((joinTable, tableIndex) => ( -
-
- - {joinTable.tableName} - - {joinTable.currentDisplayColumn} - -
-
- {joinTable.availableColumns.map((column, colIndex) => { - const matchingJoinColumn = entityJoinColumns.availableColumns.find( - (jc) => jc.tableName === joinTable.tableName && jc.columnName === column.columnName, - ); - const isAlreadyAdded = config.columns?.some( - (col) => col.columnName === matchingJoinColumn?.joinAlias, - ); - if (!matchingJoinColumn) return null; - - return ( -
{ - if (isAlreadyAdded) { - updateField("columns", config.columns?.filter((c) => c.columnName !== matchingJoinColumn.joinAlias) || []); + + + + + +
+ setColumnSearchText(e.target.value)} + placeholder="컬럼 검색..." + className="h-7 text-xs" + /> +
+ {availableColumns + .filter((column) => { + if (!columnSearchText) return true; + const search = columnSearchText.toLowerCase(); + return ( + column.columnName.toLowerCase().includes(search) || + (column.label || "").toLowerCase().includes(search) + ); + }) + .map((column) => { + const isAdded = config.columns?.some((c) => c.columnName === column.columnName); + return ( +
{ + if (isAdded) { + updateField("columns", config.columns?.filter((c) => c.columnName !== column.columnName) || []); + } else { + addColumn(column.columnName); + } + }} + > + { + if (isAdded) { + updateField("columns", config.columns?.filter((c) => c.columnName !== column.columnName) || []); } else { - addEntityColumn(matchingJoinColumn); + addColumn(column.columnName); } }} - > - { - if (isAlreadyAdded) { - updateField("columns", config.columns?.filter((c) => c.columnName !== matchingJoinColumn.joinAlias) || []); - } else { - addEntityColumn(matchingJoinColumn); + className="pointer-events-none h-3.5 w-3.5" + /> + + {column.label || column.columnName} + {isAdded && ( +
- ); - })} -
-
- ))} + > + {config.columns?.find((c) => c.columnName === column.columnName)?.editable === false ? ( + + ) : ( + + )} + + )} + + {column.input_type || column.dataType} + +
+ ); + })} +
-
+ + + + {/* Entity 조인 컬럼 (Collapsible) */} + {entityJoinColumns.joinTables.length > 0 && ( + + + + + +
+ {entityJoinColumns.joinTables.map((joinTable, tableIndex) => { + const addedCount = joinTable.availableColumns.filter((col) => { + const match = entityJoinColumns.availableColumns.find( + (jc) => jc.tableName === joinTable.tableName && jc.columnName === col.columnName, + ); + return match && config.columns?.some((c) => c.columnName === match.joinAlias); + }).length; + const isSubOpen = entityJoinSubOpen[tableIndex] ?? false; + + return ( + setEntityJoinSubOpen((prev) => ({ ...prev, [tableIndex]: open }))}> + + + + +
+ {joinTable.availableColumns.map((column, colIndex) => { + const matchingJoinColumn = entityJoinColumns.availableColumns.find( + (jc) => jc.tableName === joinTable.tableName && jc.columnName === column.columnName, + ); + const isAlreadyAdded = config.columns?.some( + (col) => col.columnName === matchingJoinColumn?.joinAlias, + ); + if (!matchingJoinColumn) return null; + + return ( +
{ + if (isAlreadyAdded) { + updateField("columns", config.columns?.filter((c) => c.columnName !== matchingJoinColumn.joinAlias) || []); + } else { + addEntityColumn(matchingJoinColumn); + } + }} + > + { + if (isAlreadyAdded) { + updateField("columns", config.columns?.filter((c) => c.columnName !== matchingJoinColumn.joinAlias) || []); + } else { + addEntityColumn(matchingJoinColumn); + } + }} + className="pointer-events-none h-3.5 w-3.5" + /> + + {column.columnLabel} + + {column.inputType || column.dataType} + +
+ ); + })} +
+
+
+ ); + })} +
+
+
)} )} @@ -905,59 +993,73 @@ export const V2TableListConfigPanel: React.FC = ({ )} {/* ═══════════════════════════════════════ */} - {/* 3단계: 선택된 컬럼 순서 (DnD) */} + {/* 3단계: 표시할 컬럼 (Collapsible + DnD) */} {/* ═══════════════════════════════════════ */} {config.columns && config.columns.length > 0 && ( -
- - - { - const { active, over } = event; - if (!over || active.id === over.id) return; - const columns = [...(config.columns || [])]; - const oldIndex = columns.findIndex((c) => c.columnName === active.id); - const newIndex = columns.findIndex((c) => c.columnName === over.id); - if (oldIndex !== -1 && newIndex !== -1) { - const reordered = arrayMove(columns, oldIndex, newIndex); - reordered.forEach((col, idx) => { col.order = idx; }); - updateField("columns", reordered); - } - }} - > - c.columnName)} - strategy={verticalListSortingStrategy} + + + + + +
+

드래그하여 순서 변경, 클릭하여 표시명/너비 수정

+ { + const { active, over } = event; + if (!over || active.id === over.id) return; + const columns = [...(config.columns || [])]; + const oldIndex = columns.findIndex((c) => c.columnName === active.id); + const newIndex = columns.findIndex((c) => c.columnName === over.id); + if (oldIndex !== -1 && newIndex !== -1) { + const reordered = arrayMove(columns, oldIndex, newIndex); + reordered.forEach((col, idx) => { col.order = idx; }); + updateField("columns", reordered); + } + }} + > + c.columnName)} + strategy={verticalListSortingStrategy} + > +
+ {(config.columns || []).map((column, idx) => { + const resolvedLabel = + column.displayName && column.displayName !== column.columnName + ? column.displayName + : availableColumns.find((c) => c.columnName === column.columnName)?.label || column.displayName || column.columnName; + const colWithLabel = { ...column, displayName: resolvedLabel }; + return ( + updateColumn(column.columnName, { displayName: value })} + onWidthChange={(value) => updateColumn(column.columnName, { width: value })} + onRemove={() => removeColumn(column.columnName)} + /> + ); + })} +
+
+
+
+
+
)} {/* ═══════════════════════════════════════ */}