From 533eaf5c9ff14bcb99965668b0e8e4b5d0ff7c5d Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Tue, 23 Dec 2025 09:24:59 +0900 Subject: [PATCH] =?UTF-8?q?feat(TableSection):=20=ED=85=8C=EC=9D=B4?= =?UTF-8?q?=EB=B8=94=20=EC=BB=AC=EB=9F=BC=20=EB=B6=80=EB=AA=A8=EA=B0=92=20?= =?UTF-8?q?=EB=B0=9B=EA=B8=B0=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?TableColumnConfig=EC=97=90=20receiveFromParent,=20parentFieldNa?= =?UTF-8?q?me=20=EC=86=8D=EC=84=B1=20=EC=B6=94=EA=B0=80=20allComponents?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EB=B6=80=EB=AA=A8=20=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?=ED=95=84=EB=93=9C=20=EC=9E=90=EB=8F=99=20=EC=B6=94=EC=B6=9C=20?= =?UTF-8?q?=EC=BB=AC=EB=9F=BC=20=EC=84=A4=EC=A0=95=EC=97=90=20"=EB=B6=80?= =?UTF-8?q?=EB=AA=A8=EA=B0=92"=20=EC=8A=A4=EC=9C=84=EC=B9=98=20=EB=B0=8F?= =?UTF-8?q?=20=EB=B6=80=EB=AA=A8=20=ED=95=84=EB=93=9C=20=EC=84=A0=ED=83=9D?= =?UTF-8?q?=20UI=20=EC=B6=94=EA=B0=80=20handleAddItems()=EC=97=90=EC=84=9C?= =?UTF-8?q?=20=EB=B6=80=EB=AA=A8=EA=B0=92=20=EC=9E=90=EB=8F=99=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TableSectionRenderer.tsx | 8 + .../UniversalFormModalConfigPanel.tsx | 206 ++++++++++++- .../modals/FieldDetailSettingsModal.tsx | 57 ++++ .../modals/SaveSettingsModal.tsx | 276 +++++++++++++----- .../modals/SectionLayoutModal.tsx | 226 ++++++++++++-- .../modals/TableSectionSettingsModal.tsx | 120 ++++++++ .../components/universal-form-modal/types.ts | 6 + 7 files changed, 805 insertions(+), 94 deletions(-) diff --git a/frontend/lib/registry/components/universal-form-modal/TableSectionRenderer.tsx b/frontend/lib/registry/components/universal-form-modal/TableSectionRenderer.tsx index 224459f0..047849b6 100644 --- a/frontend/lib/registry/components/universal-form-modal/TableSectionRenderer.tsx +++ b/frontend/lib/registry/components/universal-form-modal/TableSectionRenderer.tsx @@ -621,6 +621,14 @@ export function TableSectionRenderer({ if (col.defaultValue !== undefined && newItem[col.field] === undefined) { newItem[col.field] = col.defaultValue; } + + // 부모에서 값 받기 (receiveFromParent) + if (col.receiveFromParent) { + const parentField = col.parentFieldName || col.field; + if (formData[parentField] !== undefined) { + newItem[col.field] = formData[parentField]; + } + } } return newItem; diff --git a/frontend/lib/registry/components/universal-form-modal/UniversalFormModalConfigPanel.tsx b/frontend/lib/registry/components/universal-form-modal/UniversalFormModalConfigPanel.tsx index aa2386be..b2d5a9da 100644 --- a/frontend/lib/registry/components/universal-form-modal/UniversalFormModalConfigPanel.tsx +++ b/frontend/lib/registry/components/universal-form-modal/UniversalFormModalConfigPanel.tsx @@ -48,13 +48,24 @@ const HelpText = ({ children }: { children: React.ReactNode }) => (

{children}

); -export function UniversalFormModalConfigPanel({ config, onChange }: UniversalFormModalConfigPanelProps) { +// 부모 화면에서 전달 가능한 필드 타입 +interface AvailableParentField { + name: string; // 필드명 (columnName) + label: string; // 표시 라벨 + sourceComponent?: string; // 출처 컴포넌트 (예: "TableList", "SplitPanelLayout2") + sourceTable?: string; // 출처 테이블명 +} + +export function UniversalFormModalConfigPanel({ config, onChange, allComponents = [] }: UniversalFormModalConfigPanelProps) { // 테이블 목록 const [tables, setTables] = useState<{ name: string; label: string }[]>([]); const [tableColumns, setTableColumns] = useState<{ [tableName: string]: { name: string; type: string; label: string }[]; }>({}); + // 부모 화면에서 전달 가능한 필드 목록 + const [availableParentFields, setAvailableParentFields] = useState([]); + // 채번규칙 목록 const [numberingRules, setNumberingRules] = useState<{ id: string; name: string }[]>([]); @@ -72,6 +83,186 @@ export function UniversalFormModalConfigPanel({ config, onChange }: UniversalFor loadNumberingRules(); }, []); + // allComponents에서 부모 화면에서 전달 가능한 필드 추출 + useEffect(() => { + const extractParentFields = async () => { + if (!allComponents || allComponents.length === 0) { + setAvailableParentFields([]); + return; + } + + const fields: AvailableParentField[] = []; + + for (const comp of allComponents) { + // 컴포넌트 타입 추출 (여러 위치에서 확인) + const compType = comp.componentId || comp.componentConfig?.type || comp.componentConfig?.id || comp.type; + const compConfig = comp.componentConfig || {}; + + // 1. TableList / InteractiveDataTable - 테이블 컬럼 추출 + if (compType === "table-list" || compType === "interactive-data-table") { + const tableName = compConfig.selectedTable || compConfig.tableName; + if (tableName) { + // 테이블 컬럼 로드 + try { + const response = await apiClient.get(`/table-management/tables/${tableName}/columns`); + const columns = response.data?.data?.columns; + if (response.data?.success && Array.isArray(columns)) { + columns.forEach((col: any) => { + const colName = col.columnName || col.column_name; + const colLabel = col.displayName || col.columnComment || col.column_comment || colName; + fields.push({ + name: colName, + label: colLabel, + sourceComponent: "TableList", + sourceTable: tableName, + }); + }); + } + } catch (error) { + console.error(`테이블 컬럼 로드 실패 (${tableName}):`, error); + } + } + } + + // 2. SplitPanelLayout2 - 데이터 전달 필드 및 소스 테이블 컬럼 추출 + if (compType === "split-panel-layout2") { + // dataTransferFields 추출 + const transferFields = compConfig.dataTransferFields; + if (transferFields && Array.isArray(transferFields)) { + transferFields.forEach((field: any) => { + if (field.targetColumn) { + fields.push({ + name: field.targetColumn, + label: field.targetColumn, + sourceComponent: "SplitPanelLayout2", + sourceTable: compConfig.leftPanel?.tableName, + }); + } + }); + } + + // 좌측 패널 테이블 컬럼도 추출 + const leftTableName = compConfig.leftPanel?.tableName; + if (leftTableName) { + try { + const response = await apiClient.get(`/table-management/tables/${leftTableName}/columns`); + const columns = response.data?.data?.columns; + if (response.data?.success && Array.isArray(columns)) { + columns.forEach((col: any) => { + const colName = col.columnName || col.column_name; + const colLabel = col.displayName || col.columnComment || col.column_comment || colName; + // 중복 방지 + if (!fields.some(f => f.name === colName && f.sourceTable === leftTableName)) { + fields.push({ + name: colName, + label: colLabel, + sourceComponent: "SplitPanelLayout2 (좌측)", + sourceTable: leftTableName, + }); + } + }); + } + } catch (error) { + console.error(`테이블 컬럼 로드 실패 (${leftTableName}):`, error); + } + } + } + + // 3. 기타 테이블 관련 컴포넌트 + if (compType === "card-display" || compType === "simple-repeater-table") { + const tableName = compConfig.tableName || compConfig.initialDataConfig?.sourceTable; + if (tableName) { + try { + const response = await apiClient.get(`/table-management/tables/${tableName}/columns`); + const columns = response.data?.data?.columns; + if (response.data?.success && Array.isArray(columns)) { + columns.forEach((col: any) => { + const colName = col.columnName || col.column_name; + const colLabel = col.displayName || col.columnComment || col.column_comment || colName; + if (!fields.some(f => f.name === colName && f.sourceTable === tableName)) { + fields.push({ + name: colName, + label: colLabel, + sourceComponent: compType, + sourceTable: tableName, + }); + } + }); + } + } catch (error) { + console.error(`테이블 컬럼 로드 실패 (${tableName}):`, error); + } + } + } + + // 4. 버튼 컴포넌트 - openModalWithData의 fieldMappings/dataMapping에서 소스 컬럼 추출 + if (compType === "button-primary" || compType === "button" || compType === "button-secondary") { + const action = compConfig.action || {}; + + // fieldMappings에서 소스 컬럼 추출 + const fieldMappings = action.fieldMappings || []; + fieldMappings.forEach((mapping: any) => { + if (mapping.sourceColumn && !fields.some(f => f.name === mapping.sourceColumn)) { + fields.push({ + name: mapping.sourceColumn, + label: mapping.sourceColumn, + sourceComponent: "Button (fieldMappings)", + sourceTable: action.sourceTableName, + }); + } + }); + + // dataMapping에서 소스 컬럼 추출 + const dataMapping = action.dataMapping || []; + dataMapping.forEach((mapping: any) => { + if (mapping.sourceColumn && !fields.some(f => f.name === mapping.sourceColumn)) { + fields.push({ + name: mapping.sourceColumn, + label: mapping.sourceColumn, + sourceComponent: "Button (dataMapping)", + sourceTable: action.sourceTableName, + }); + } + }); + } + } + + // 5. 현재 모달의 저장 테이블 컬럼도 추가 (부모에서 전달받을 수 있는 값들) + const currentTableName = config.saveConfig?.tableName; + if (currentTableName) { + try { + const response = await apiClient.get(`/table-management/tables/${currentTableName}/columns`); + const columns = response.data?.data?.columns; + if (response.data?.success && Array.isArray(columns)) { + columns.forEach((col: any) => { + const colName = col.columnName || col.column_name; + const colLabel = col.displayName || col.columnComment || col.column_comment || colName; + if (!fields.some(f => f.name === colName)) { + fields.push({ + name: colName, + label: colLabel, + sourceComponent: "현재 폼 테이블", + sourceTable: currentTableName, + }); + } + }); + } + } catch (error) { + console.error(`현재 테이블 컬럼 로드 실패 (${currentTableName}):`, error); + } + } + + // 중복 제거 (같은 name이면 첫 번째만 유지) + const uniqueFields = fields.filter((field, index, self) => + index === self.findIndex(f => f.name === field.name) + ); + + setAvailableParentFields(uniqueFields); + }; + + extractParentFields(); + }, [allComponents, config.saveConfig?.tableName]); + // 저장 테이블 변경 시 컬럼 로드 useEffect(() => { if (config.saveConfig.tableName) { @@ -85,9 +276,10 @@ export function UniversalFormModalConfigPanel({ config, onChange }: UniversalFor const data = response.data?.data; if (response.data?.success && Array.isArray(data)) { setTables( - data.map((t: { tableName?: string; table_name?: string; tableLabel?: string; table_label?: string }) => ({ + data.map((t: { tableName?: string; table_name?: string; displayName?: string; tableLabel?: string; table_label?: string }) => ({ name: t.tableName || t.table_name || "", - label: t.tableLabel || t.table_label || t.tableName || t.table_name || "", + // displayName 우선, 없으면 tableLabel, 그것도 없으면 테이블명 + label: t.displayName || t.tableLabel || t.table_label || "", })), ); } @@ -620,6 +812,12 @@ export function UniversalFormModalConfigPanel({ config, onChange }: UniversalFor setSelectedField(field); setFieldDetailModalOpen(true); }} + tableName={config.saveConfig.tableName} + tableColumns={tableColumns[config.saveConfig.tableName || ""]?.map(col => ({ + name: col.name, + type: col.type, + label: col.label || col.name + })) || []} /> )} @@ -666,6 +864,7 @@ export function UniversalFormModalConfigPanel({ config, onChange }: UniversalFor tableColumns={tableColumns} numberingRules={numberingRules} onLoadTableColumns={loadTableColumns} + availableParentFields={availableParentFields} /> )} @@ -706,6 +905,7 @@ export function UniversalFormModalConfigPanel({ config, onChange }: UniversalFor )} onLoadTableColumns={loadTableColumns} allSections={config.sections as FormSectionConfig[]} + availableParentFields={availableParentFields} /> )} diff --git a/frontend/lib/registry/components/universal-form-modal/modals/FieldDetailSettingsModal.tsx b/frontend/lib/registry/components/universal-form-modal/modals/FieldDetailSettingsModal.tsx index d53d6e00..c8584a5f 100644 --- a/frontend/lib/registry/components/universal-form-modal/modals/FieldDetailSettingsModal.tsx +++ b/frontend/lib/registry/components/universal-form-modal/modals/FieldDetailSettingsModal.tsx @@ -36,6 +36,17 @@ const HelpText = ({ children }: { children: React.ReactNode }) => (

{children}

); +/** + * 부모 화면에서 전달 가능한 필드 타입 + * 유니버셜 폼 모달에서 "부모에서 값 받기" 설정 시 선택 가능한 필드 목록 + */ +export interface AvailableParentField { + name: string; // 필드명 (columnName) + label: string; // 표시 라벨 + sourceComponent?: string; // 출처 컴포넌트 (예: "TableList", "SplitPanelLayout2") + sourceTable?: string; // 출처 테이블명 +} + interface FieldDetailSettingsModalProps { open: boolean; onOpenChange: (open: boolean) => void; @@ -45,6 +56,8 @@ interface FieldDetailSettingsModalProps { tableColumns: { [tableName: string]: { name: string; type: string; label: string }[] }; numberingRules: { id: string; name: string }[]; onLoadTableColumns: (tableName: string) => void; + // 부모 화면에서 전달 가능한 필드 목록 (선택사항) + availableParentFields?: AvailableParentField[]; } export function FieldDetailSettingsModal({ @@ -56,6 +69,7 @@ export function FieldDetailSettingsModal({ tableColumns, numberingRules, onLoadTableColumns, + availableParentFields = [], }: FieldDetailSettingsModalProps) { // 로컬 상태로 필드 설정 관리 const [localField, setLocalField] = useState(field); @@ -293,6 +307,49 @@ export function FieldDetailSettingsModal({ /> 부모 화면에서 전달받은 값으로 자동 채워집니다 + + {/* 부모에서 값 받기 활성화 시 필드 선택 */} + {localField.receiveFromParent && ( +
+ + {availableParentFields.length > 0 ? ( + + ) : ( +
+ updateField({ parentFieldName: e.target.value })} + placeholder={`예: ${localField.columnName || "parent_field_name"}`} + className="h-8 text-xs" + /> +

+ 부모 화면에서 전달받을 필드명을 입력하세요. 비워두면 "{localField.columnName}"을 사용합니다. +

+
+ )} +
+ )} {/* Accordion으로 고급 설정 */} diff --git a/frontend/lib/registry/components/universal-form-modal/modals/SaveSettingsModal.tsx b/frontend/lib/registry/components/universal-form-modal/modals/SaveSettingsModal.tsx index c9976ed8..5be176dc 100644 --- a/frontend/lib/registry/components/universal-form-modal/modals/SaveSettingsModal.tsx +++ b/frontend/lib/registry/components/universal-form-modal/modals/SaveSettingsModal.tsx @@ -11,7 +11,9 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, Di import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Separator } from "@/components/ui/separator"; -import { Plus, Trash2, Database, Layers, Info } from "lucide-react"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"; +import { Plus, Trash2, Database, Layers, Info, Check, ChevronsUpDown } from "lucide-react"; import { Badge } from "@/components/ui/badge"; import { cn } from "@/lib/utils"; import { SaveConfig, SubTableSaveConfig, SubTableFieldMapping, FormSectionConfig, FormFieldConfig, SectionSaveMode } from "../types"; @@ -50,6 +52,11 @@ export function SaveSettingsModal({ saveConfig.customApiSave?.enabled && saveConfig.customApiSave?.multiTable?.enabled ? "multi" : "single" ); + // 테이블 검색 Popover 상태 + const [singleTableSearchOpen, setSingleTableSearchOpen] = useState(false); + const [mainTableSearchOpen, setMainTableSearchOpen] = useState(false); + const [subTableSearchOpen, setSubTableSearchOpen] = useState>({}); + // open이 변경될 때마다 데이터 동기화 useEffect(() => { if (open) { @@ -376,24 +383,68 @@ export function SaveSettingsModal({
- + + + + + + + + + + 테이블을 찾을 수 없습니다. + + + {tables.map((t) => ( + { + updateSaveConfig({ tableName: t.name }); + onLoadTableColumns(t.name); + setSingleTableSearchOpen(false); + }} + className="text-xs" + > + +
+ {t.name} + {t.label && ( + {t.label} + )} +
+
+ ))} +
+
+
+
+
폼 데이터를 저장할 테이블을 선택하세요
@@ -426,37 +477,81 @@ export function SaveSettingsModal({
- + + + + + + + + + + 테이블을 찾을 수 없습니다. + + + {tables.map((t) => ( + { + updateSaveConfig({ + customApiSave: { + ...localSaveConfig.customApiSave, + apiType: "multi-table", + multiTable: { + ...localSaveConfig.customApiSave?.multiTable, + enabled: true, + mainTable: { + ...localSaveConfig.customApiSave?.multiTable?.mainTable, + tableName: t.name, + }, + }, + }, + }); + onLoadTableColumns(t.name); + setMainTableSearchOpen(false); + }} + className="text-xs" + > + +
+ {t.name} + {t.label && ( + {t.label} + )} +
+
+ ))} +
+
+
+
+
주요 데이터를 저장할 메인 테이블 (예: orders, user_info)
@@ -576,24 +671,71 @@ export function SaveSettingsModal({
- + + + + + + + + + 테이블을 찾을 수 없습니다. + + + {tables.map((t) => ( + { + updateSubTable(subIndex, { tableName: t.name }); + onLoadTableColumns(t.name); + setSubTableSearchOpen(prev => ({ ...prev, [subIndex]: false })); + }} + className="text-xs" + > + +
+ {t.name} + {t.label && ( + {t.label} + )} +
+
+ ))} +
+
+
+
+ 반복 데이터를 저장할 서브 테이블
diff --git a/frontend/lib/registry/components/universal-form-modal/modals/SectionLayoutModal.tsx b/frontend/lib/registry/components/universal-form-modal/modals/SectionLayoutModal.tsx index 4a90a777..b47c2424 100644 --- a/frontend/lib/registry/components/universal-form-modal/modals/SectionLayoutModal.tsx +++ b/frontend/lib/registry/components/universal-form-modal/modals/SectionLayoutModal.tsx @@ -11,7 +11,9 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, Di import { ScrollArea } from "@/components/ui/scroll-area"; import { Separator } from "@/components/ui/separator"; import { Badge } from "@/components/ui/badge"; -import { Plus, Trash2, GripVertical, ChevronUp, ChevronDown, Settings as SettingsIcon } from "lucide-react"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"; +import { Plus, Trash2, GripVertical, ChevronUp, ChevronDown, Settings as SettingsIcon, Check, ChevronsUpDown } from "lucide-react"; import { cn } from "@/lib/utils"; import { FormSectionConfig, FormFieldConfig, OptionalFieldGroupConfig, FIELD_TYPE_OPTIONS } from "../types"; import { defaultFieldConfig, generateFieldId, generateUniqueId } from "../config"; @@ -21,12 +23,22 @@ const HelpText = ({ children }: { children: React.ReactNode }) => (

{children}

); +// 테이블 컬럼 정보 타입 +interface TableColumnInfo { + name: string; + type: string; + label: string; +} + interface SectionLayoutModalProps { open: boolean; onOpenChange: (open: boolean) => void; section: FormSectionConfig; onSave: (updates: Partial) => void; onOpenFieldDetail: (field: FormFieldConfig) => void; + // 저장 테이블의 컬럼 정보 + tableName?: string; + tableColumns?: TableColumnInfo[]; } export function SectionLayoutModal({ @@ -35,8 +47,13 @@ export function SectionLayoutModal({ section, onSave, onOpenFieldDetail, + tableName = "", + tableColumns = [], }: SectionLayoutModalProps) { + // 컬럼 선택 Popover 상태 (필드별) + const [columnSearchOpen, setColumnSearchOpen] = useState>({}); + // 로컬 상태로 섹션 관리 (fields가 없으면 빈 배열로 초기화) const [localSection, setLocalSection] = useState(() => ({ ...section, @@ -443,11 +460,90 @@ export function SectionLayoutModal({
- updateField(field.id, { columnName: e.target.value })} - className="h-6 text-[9px] mt-0.5" - /> + {tableColumns.length > 0 ? ( + setColumnSearchOpen(prev => ({ ...prev, [field.id]: open }))} + > + + + + + + + + + 컬럼을 찾을 수 없습니다. + + + {tableColumns.map((col) => ( + { + updateField(field.id, { + columnName: col.name, + // 라벨이 기본값이면 컬럼 라벨로 자동 설정 + ...(field.label.startsWith("새 필드") || field.label.startsWith("field_") + ? { label: col.label || col.name } + : {}) + }); + setColumnSearchOpen(prev => ({ ...prev, [field.id]: false })); + }} + className="text-xs" + > + +
+
+ {col.name} + {col.label && col.label !== col.name && ( + ({col.label}) + )} +
+ {tableName && ( + {tableName} + )} +
+
+ ))} +
+
+
+
+
+ ) : ( + updateField(field.id, { columnName: e.target.value })} + className="h-6 text-[9px] mt-0.5" + placeholder="저장 테이블을 먼저 설정하세요" + /> + )}
@@ -821,24 +917,106 @@ export function SectionLayoutModal({ className="h-5 text-[8px]" placeholder="라벨" /> - { - const newGroups = localSection.optionalFieldGroups?.map((g) => - g.id === group.id - ? { - ...g, - fields: g.fields.map((f) => - f.id === field.id ? { ...f, columnName: e.target.value } : f - ), - } - : g - ); - updateSection({ optionalFieldGroups: newGroups }); - }} - className="h-5 text-[8px]" - placeholder="컬럼명" - /> + {tableColumns.length > 0 ? ( + setColumnSearchOpen(prev => ({ ...prev, [`opt-${field.id}`]: open }))} + > + + + + + + + + + 컬럼을 찾을 수 없습니다. + + + {tableColumns.map((col) => ( + { + const newGroups = localSection.optionalFieldGroups?.map((g) => + g.id === group.id + ? { + ...g, + fields: g.fields.map((f) => + f.id === field.id + ? { + ...f, + columnName: col.name, + ...(f.label.startsWith("필드 ") ? { label: col.label || col.name } : {}) + } + : f + ), + } + : g + ); + updateSection({ optionalFieldGroups: newGroups }); + setColumnSearchOpen(prev => ({ ...prev, [`opt-${field.id}`]: false })); + }} + className="text-[9px]" + > + +
+ {col.name} + {col.label && col.label !== col.name && ( + ({col.label}) + )} +
+
+ ))} +
+
+
+
+
+ ) : ( + { + const newGroups = localSection.optionalFieldGroups?.map((g) => + g.id === group.id + ? { + ...g, + fields: g.fields.map((f) => + f.id === field.id ? { ...f, columnName: e.target.value } : f + ), + } + : g + ); + updateSection({ optionalFieldGroups: newGroups }); + }} + className="h-5 text-[8px]" + placeholder="컬럼명" + /> + )} onUpdate({ parentFieldName: e.target.value })} + placeholder={col.field} + className="h-8 text-xs" + /> +

+ 비워두면 "{col.field}"를 사용합니다. +

+ + )} + + )} + {/* 조회 설정 (조회 ON일 때만 표시) */} {col.lookup?.enabled && (
@@ -1119,6 +1235,8 @@ interface TableSectionSettingsModalProps { onLoadCategoryList?: () => void; // 전체 섹션 목록 (다른 섹션 필드 참조용) allSections?: FormSectionConfig[]; + // 부모 화면에서 전달 가능한 필드 목록 + availableParentFields?: AvailableParentField[]; } export function TableSectionSettingsModal({ @@ -1132,6 +1250,7 @@ export function TableSectionSettingsModal({ categoryList = [], onLoadCategoryList, allSections = [], + availableParentFields = [], }: TableSectionSettingsModalProps) { // 로컬 상태 const [title, setTitle] = useState(section.title); @@ -1693,6 +1812,7 @@ export function TableSectionSettingsModal({ sections={otherSections} formFields={otherSectionFields} tableConfig={tableConfig} + availableParentFields={availableParentFields} onLoadTableColumns={onLoadTableColumns} onUpdate={(updates) => updateColumn(index, updates)} onMoveUp={() => moveColumn(index, "up")} diff --git a/frontend/lib/registry/components/universal-form-modal/types.ts b/frontend/lib/registry/components/universal-form-modal/types.ts index 4e25f7d7..935d46be 100644 --- a/frontend/lib/registry/components/universal-form-modal/types.ts +++ b/frontend/lib/registry/components/universal-form-modal/types.ts @@ -335,6 +335,10 @@ export interface TableColumnConfig { // 날짜 일괄 적용 (type이 "date"일 때만 사용) // 활성화 시 첫 번째 날짜 입력 시 모든 행에 동일한 날짜가 자동 적용됨 batchApply?: boolean; + + // 부모에서 값 받기 (모든 행에 동일한 값 적용) + receiveFromParent?: boolean; // 부모에서 값 받기 활성화 + parentFieldName?: string; // 부모 필드명 (미지정 시 field와 동일) } // ============================================ @@ -705,6 +709,8 @@ export interface UniversalFormModalComponentProps { export interface UniversalFormModalConfigPanelProps { config: UniversalFormModalConfig; onChange: (config: UniversalFormModalConfig) => void; + // 화면 설계 시 같은 화면의 다른 컴포넌트들 (부모 데이터 필드 추출용) + allComponents?: any[]; } // 필드 타입 옵션