diff --git a/frontend/lib/registry/components/repeat-container/RepeatContainerConfigPanel.tsx b/frontend/lib/registry/components/repeat-container/RepeatContainerConfigPanel.tsx index ddedded8..56ae79a6 100644 --- a/frontend/lib/registry/components/repeat-container/RepeatContainerConfigPanel.tsx +++ b/frontend/lib/registry/components/repeat-container/RepeatContainerConfigPanel.tsx @@ -14,11 +14,12 @@ import { } from "@/components/ui/select"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"; -import { Database, Table2, ChevronsUpDown, Check, LayoutGrid, LayoutList, Rows3, Plus, X, GripVertical, Trash2, Type } from "lucide-react"; +import { Database, Table2, ChevronsUpDown, Check, LayoutGrid, LayoutList, Rows3, Plus, X, GripVertical, Trash2, Type, Settings2, ChevronDown, ChevronUp } from "lucide-react"; import { cn } from "@/lib/utils"; import { RepeatContainerConfig, SlotComponentConfig } from "./types"; import { tableTypeApi } from "@/lib/api/screen"; import { tableManagementApi } from "@/lib/api/tableManagement"; +import { DynamicComponentConfigPanel, hasComponentConfigPanel } from "@/lib/utils/getComponentConfigPanel"; interface RepeatContainerConfigPanelProps { config: RepeatContainerConfig; @@ -263,6 +264,7 @@ export function RepeatContainerConfigPanel({ onChange={onChange} availableColumns={availableColumns} loadingColumns={loadingColumns} + screenTableName={screenTableName} /> {/* 레이아웃 설정 */} @@ -597,11 +599,89 @@ export function RepeatContainerConfigPanel({ // ============================================================ // 슬롯 자식 컴포넌트 관리 섹션 // ============================================================ + +// 슬롯 컴포넌트의 전체 설정 패널을 표시하는 컴포넌트 +interface SlotComponentDetailPanelProps { + child: SlotComponentConfig; + screenTableName?: string; + availableColumns: Array<{ columnName: string; displayName?: string }>; + onConfigChange: (newConfig: Record) => void; + onFieldNameChange: (fieldName: string) => void; + onLabelChange: (label: string) => void; +} + +function SlotComponentDetailPanel({ + child, + screenTableName, + availableColumns, + onConfigChange, + onFieldNameChange, + onLabelChange, +}: SlotComponentDetailPanelProps) { + return ( +
+ {/* 데이터 필드 바인딩 - 모든 컴포넌트에서 사용 가능 */} +
+ + + {child.fieldName && ( +

+ 각 아이템의 "{child.fieldName}" 값이 이 컴포넌트에 표시됩니다 +

+ )} +
+ + {/* 라벨 설정 */} +
+ + onLabelChange(e.target.value)} + placeholder="표시할 라벨" + className="h-7 text-xs" + /> +
+ + {/* 컴포넌트 전용 설정 */} +
+
+ {child.componentType} 상세 설정 +
+ +
+
+ ); +} + interface SlotChildrenSectionProps { config: RepeatContainerConfig; onChange: (config: Partial) => void; availableColumns: Array<{ columnName: string; displayName?: string }>; loadingColumns: boolean; + screenTableName?: string; } function SlotChildrenSection({ @@ -609,12 +689,28 @@ function SlotChildrenSection({ onChange, availableColumns, loadingColumns, + screenTableName, }: SlotChildrenSectionProps) { const [selectedColumn, setSelectedColumn] = useState(""); const [columnComboboxOpen, setColumnComboboxOpen] = useState(false); + // 각 컴포넌트별 상세 설정 열림 상태 + const [expandedIds, setExpandedIds] = useState>(new Set()); const children = config.children || []; + // 상세 설정 열기/닫기 토글 + const toggleExpanded = (id: string) => { + setExpandedIds((prev) => { + const newSet = new Set(prev); + if (newSet.has(id)) { + newSet.delete(id); + } else { + newSet.add(id); + } + return newSet; + }); + }; + // 컴포넌트 추가 const addComponent = (columnName: string, displayName: string) => { const newChild: SlotComponentConfig = { @@ -625,6 +721,7 @@ function SlotChildrenSection({ position: { x: 0, y: children.length * 40 }, size: { width: 200, height: 32 }, componentConfig: {}, + style: {}, }; onChange({ @@ -639,6 +736,11 @@ function SlotChildrenSection({ onChange({ children: children.filter((c) => c.id !== id), }); + setExpandedIds((prev) => { + const newSet = new Set(prev); + newSet.delete(id); + return newSet; + }); }; // 컴포넌트 라벨 변경 @@ -655,6 +757,46 @@ function SlotChildrenSection({ }); }; + // 컴포넌트 필드 바인딩 변경 + const updateComponentFieldName = (id: string, fieldName: string) => { + onChange({ + children: children.map((c) => (c.id === id ? { ...c, fieldName } : c)), + }); + }; + + // 컴포넌트 설정 변경 (componentConfig) + const updateComponentConfig = (id: string, key: string, value: any) => { + onChange({ + children: children.map((c) => + c.id === id + ? { ...c, componentConfig: { ...c.componentConfig, [key]: value } } + : c + ), + }); + }; + + // 컴포넌트 스타일 변경 + const updateComponentStyle = (id: string, key: string, value: any) => { + onChange({ + children: children.map((c) => + c.id === id + ? { ...c, style: { ...c.style, [key]: value } } + : c + ), + }); + }; + + // 컴포넌트 크기 변경 + const updateComponentSize = (id: string, width: number | undefined, height: number | undefined) => { + onChange({ + children: children.map((c) => + c.id === id + ? { ...c, size: { width: width ?? c.size?.width ?? 200, height: height ?? c.size?.height ?? 32 } } + : c + ), + }); + }; + return (
@@ -668,16 +810,18 @@ function SlotChildrenSection({ {/* 추가된 필드 목록 */} {children.length > 0 ? (
- {children.map((child, index) => ( -
-
- {index + 1} -
-
-
+ {children.map((child, index) => { + const isExpanded = expandedIds.has(child.id); + return ( +
+ {/* 기본 정보 헤더 */} +
+
+ {index + 1} +
{child.label || child.fieldName} @@ -700,18 +844,257 @@ function SlotChildrenSection({ 날짜 + +
+ + {/* 상세 설정 패널 (펼침) */} + {isExpanded && ( +
+ {/* 전용 ConfigPanel이 있는 복잡한 컴포넌트인 경우 */} + {hasComponentConfigPanel(child.componentType) ? ( + { + onChange({ + children: children.map((c) => + c.id === child.id + ? { ...c, componentConfig: { ...c.componentConfig, ...newConfig } } + : c + ), + }); + }} + onFieldNameChange={(fieldName) => updateComponentFieldName(child.id, fieldName)} + onLabelChange={(label) => updateComponentLabel(child.id, label)} + /> + ) : ( + <> + {/* 데이터 필드 바인딩 - 가장 중요! */} +
+ + + {child.fieldName && ( +

+ 각 아이템의 "{child.fieldName}" 값이 표시됩니다 +

+ )} +
+ + {/* 라벨 설정 */} +
+ + updateComponentLabel(child.id, e.target.value)} + placeholder="표시할 라벨" + className="h-7 text-xs" + /> +
+ + {/* 크기 설정 */} +
+
+ + + updateComponentSize(child.id, parseInt(e.target.value) || 200, undefined) + } + className="h-7 text-xs" + /> +
+
+ + + updateComponentSize(child.id, undefined, parseInt(e.target.value) || 32) + } + className="h-7 text-xs" + /> +
+
+ + {/* 스타일 설정 */} +
+ +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ +
+ updateComponentStyle(child.id, "color", e.target.value)} + className="h-7 w-10 p-0.5 cursor-pointer" + /> + updateComponentStyle(child.id, "color", e.target.value)} + className="h-7 flex-1 text-xs" + placeholder="#000000" + /> +
+
+
+
+ + {/* 컴포넌트 타입별 추가 설정 */} + {(child.componentType === "number-display" || child.componentType === "number-input") && ( +
+ +
+
+ + + updateComponentConfig(child.id, "decimalPlaces", parseInt(e.target.value) || 0) + } + className="h-7 text-xs" + /> +
+
+ + updateComponentConfig(child.id, "thousandSeparator", checked) + } + /> + +
+
+
+ )} + + {(child.componentType === "date-display" || child.componentType === "date-input") && ( +
+ + +
+ )} + + )} +
+ )}
- -
- ))} + ); + })}
) : (
diff --git a/frontend/lib/utils/getComponentConfigPanel.tsx b/frontend/lib/utils/getComponentConfigPanel.tsx index ee0c27ea..bb86e332 100644 --- a/frontend/lib/utils/getComponentConfigPanel.tsx +++ b/frontend/lib/utils/getComponentConfigPanel.tsx @@ -5,7 +5,9 @@ import React from "react"; // 컴포넌트별 ConfigPanel 동적 import 맵 +// 모든 ConfigPanel이 있는 컴포넌트를 여기에 등록해야 슬롯/중첩 컴포넌트에서 전용 설정 패널이 표시됨 const CONFIG_PANEL_MAP: Record Promise> = { + // ========== 기본 입력 컴포넌트 ========== "text-input": () => import("@/lib/registry/components/text-input/TextInputConfigPanel"), "number-input": () => import("@/lib/registry/components/number-input/NumberInputConfigPanel"), "date-input": () => import("@/lib/registry/components/date-input/DateInputConfigPanel"), @@ -15,34 +17,60 @@ const CONFIG_PANEL_MAP: Record Promise> = { "radio-basic": () => import("@/lib/registry/components/radio-basic/RadioBasicConfigPanel"), "toggle-switch": () => import("@/lib/registry/components/toggle-switch/ToggleSwitchConfigPanel"), "file-upload": () => import("@/lib/registry/components/file-upload/FileUploadConfigPanel"), - "button-primary": () => import("@/components/screen/config-panels/ButtonConfigPanel"), - "text-display": () => import("@/lib/registry/components/text-display/TextDisplayConfigPanel"), "slider-basic": () => import("@/lib/registry/components/slider-basic/SliderBasicConfigPanel"), + "test-input": () => import("@/lib/registry/components/test-input/TestInputConfigPanel"), + + // ========== 버튼 ========== + "button-primary": () => import("@/components/screen/config-panels/ButtonConfigPanel"), + + // ========== 표시 컴포넌트 ========== + "text-display": () => import("@/lib/registry/components/text-display/TextDisplayConfigPanel"), "image-display": () => import("@/lib/registry/components/image-display/ImageDisplayConfigPanel"), "divider-line": () => import("@/lib/registry/components/divider-line/DividerLineConfigPanel"), + "image-widget": () => import("@/lib/registry/components/image-widget/ImageWidgetConfigPanel"), + + // ========== 레이아웃/컨테이너 ========== "accordion-basic": () => import("@/lib/registry/components/accordion-basic/AccordionBasicConfigPanel"), - "table-list": () => import("@/lib/registry/components/table-list/TableListConfigPanel"), "card-display": () => import("@/lib/registry/components/card-display/CardDisplayConfigPanel"), - "split-panel-layout": () => import("@/lib/registry/components/split-panel-layout/SplitPanelLayoutConfigPanel"), - "split-panel-layout2": () => import("@/lib/registry/components/split-panel-layout2/SplitPanelLayout2ConfigPanel"), - "repeater-field-group": () => import("@/components/webtypes/config/RepeaterConfigPanel"), - "flow-widget": () => import("@/components/screen/config-panels/FlowWidgetConfigPanel"), - // 🆕 수주 등록 관련 컴포넌트들 - "autocomplete-search-input": () => - import("@/lib/registry/components/autocomplete-search-input/AutocompleteSearchInputConfigPanel"), - "entity-search-input": () => import("@/lib/registry/components/entity-search-input/EntitySearchInputConfigPanel"), - "modal-repeater-table": () => import("@/lib/registry/components/modal-repeater-table/ModalRepeaterTableConfigPanel"), - // 🆕 조건부 컨테이너 - "conditional-container": () => - import("@/lib/registry/components/conditional-container/ConditionalContainerConfigPanel"), - // 🆕 선택 항목 상세입력 - "selected-items-detail-input": () => - import("@/lib/registry/components/selected-items-detail-input/SelectedItemsDetailInputConfigPanel"), - // 🆕 섹션 그룹화 레이아웃 "section-card": () => import("@/lib/registry/components/section-card/SectionCardConfigPanel"), "section-paper": () => import("@/lib/registry/components/section-paper/SectionPaperConfigPanel"), - // 🆕 탭 컴포넌트 + "split-panel-layout": () => import("@/lib/registry/components/split-panel-layout/SplitPanelLayoutConfigPanel"), + "split-panel-layout2": () => import("@/lib/registry/components/split-panel-layout2/SplitPanelLayout2ConfigPanel"), + "screen-split-panel": () => import("@/lib/registry/components/screen-split-panel/ScreenSplitPanelConfigPanel"), + "conditional-container": () => import("@/lib/registry/components/conditional-container/ConditionalContainerConfigPanel"), + + // ========== 테이블/리스트 ========== + "table-list": () => import("@/lib/registry/components/table-list/TableListConfigPanel"), + "pivot-grid": () => import("@/lib/registry/components/pivot-grid/PivotGridConfigPanel"), + "table-search-widget": () => import("@/lib/registry/components/table-search-widget/TableSearchWidgetConfigPanel"), + "tax-invoice-list": () => import("@/lib/registry/components/tax-invoice-list/TaxInvoiceListConfigPanel"), + + // ========== 리피터/반복 ========== + "repeat-container": () => import("@/lib/registry/components/repeat-container/RepeatContainerConfigPanel"), + "repeater-field-group": () => import("@/components/webtypes/config/RepeaterConfigPanel"), + "unified-repeater": () => import("@/components/unified/config-panels/UnifiedRepeaterConfigPanel"), + "simple-repeater-table": () => import("@/lib/registry/components/simple-repeater-table/SimpleRepeaterTableConfigPanel"), + "modal-repeater-table": () => import("@/lib/registry/components/modal-repeater-table/ModalRepeaterTableConfigPanel"), + "repeat-screen-modal": () => import("@/lib/registry/components/repeat-screen-modal/RepeatScreenModalConfigPanel"), + "related-data-buttons": () => import("@/lib/registry/components/related-data-buttons/RelatedDataButtonsConfigPanel"), + + // ========== 검색/선택 ========== + "autocomplete-search-input": () => import("@/lib/registry/components/autocomplete-search-input/AutocompleteSearchInputConfigPanel"), + "entity-search-input": () => import("@/lib/registry/components/entity-search-input/EntitySearchInputConfigPanel"), + "selected-items-detail-input": () => import("@/lib/registry/components/selected-items-detail-input/SelectedItemsDetailInputConfigPanel"), + "customer-item-mapping": () => import("@/lib/registry/components/customer-item-mapping/CustomerItemMappingConfigPanel"), + "mail-recipient-selector": () => import("@/lib/registry/components/mail-recipient-selector/MailRecipientSelectorConfigPanel"), + "location-swap-selector": () => import("@/lib/registry/components/location-swap-selector/LocationSwapSelectorConfigPanel"), + + // ========== 특수 컴포넌트 ========== + "flow-widget": () => import("@/components/screen/config-panels/FlowWidgetConfigPanel"), "tabs-widget": () => import("@/components/screen/config-panels/TabsConfigPanel"), + "map": () => import("@/lib/registry/components/map/MapConfigPanel"), + "rack-structure": () => import("@/lib/registry/components/rack-structure/RackStructureConfigPanel"), + "aggregation-widget": () => import("@/lib/registry/components/aggregation-widget/AggregationWidgetConfigPanel"), + "numbering-rule": () => import("@/lib/registry/components/numbering-rule/NumberingRuleConfigPanel"), + "category-manager": () => import("@/lib/registry/components/category-manager/CategoryManagerConfigPanel"), + "universal-form-modal": () => import("@/lib/registry/components/universal-form-modal/UniversalFormModalConfigPanel"), }; // ConfigPanel 컴포넌트 캐시 @@ -68,16 +96,43 @@ export async function getComponentConfigPanel(componentId: string): Promise TextInputConfigPanel) + // 2차: 특수 export명들 fallback + // 3차: default export + const pascalCaseName = `${toPascalCase(componentId)}ConfigPanel`; const ConfigPanelComponent = - module[`${toPascalCase(componentId)}ConfigPanel`] || - module.RepeaterConfigPanel || // repeater-field-group의 export명 - module.FlowWidgetConfigPanel || // flow-widget의 export명 - module.CustomerItemMappingConfigPanel || // customer-item-mapping의 export명 - module.SelectedItemsDetailInputConfigPanel || // selected-items-detail-input의 export명 - module.ButtonConfigPanel || // button-primary의 export명 - module.SectionCardConfigPanel || // section-card의 export명 - module.SectionPaperConfigPanel || // section-paper의 export명 - module.TabsConfigPanel || // tabs-widget의 export명 + module[pascalCaseName] || + // 특수 export명들 + module.RepeaterConfigPanel || + module.FlowWidgetConfigPanel || + module.CustomerItemMappingConfigPanel || + module.SelectedItemsDetailInputConfigPanel || + module.ButtonConfigPanel || + module.SectionCardConfigPanel || + module.SectionPaperConfigPanel || + module.TabsConfigPanel || + module.UnifiedRepeaterConfigPanel || + module.RepeatContainerConfigPanel || + module.ScreenSplitPanelConfigPanel || + module.SimpleRepeaterTableConfigPanel || + module.ModalRepeaterTableConfigPanel || + module.RepeatScreenModalConfigPanel || + module.RelatedDataButtonsConfigPanel || + module.AutocompleteSearchInputConfigPanel || + module.EntitySearchInputConfigPanel || + module.MailRecipientSelectorConfigPanel || + module.LocationSwapSelectorConfigPanel || + module.MapConfigPanel || + module.RackStructureConfigPanel || + module.AggregationWidgetConfigPanel || + module.NumberingRuleConfigPanel || + module.CategoryManagerConfigPanel || + module.UniversalFormModalConfigPanel || + module.PivotGridConfigPanel || + module.TableSearchWidgetConfigPanel || + module.TaxInvoiceListConfigPanel || + module.ImageWidgetConfigPanel || + module.TestInputConfigPanel || module.default; if (!ConfigPanelComponent) {