From c71641c32cd29605c4cd3ad7ab18030091a2a68f Mon Sep 17 00:00:00 2001 From: kjs Date: Thu, 15 Jan 2026 17:50:52 +0900 Subject: [PATCH] =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=A0=95=EB=A6=AC=20?= =?UTF-8?q?=EB=B0=8F=20=EC=8A=A4=ED=83=80=EC=9D=BC=20=EA=B0=9C=EC=84=A0:?= =?UTF-8?q?=20ComponentsPanel=EA=B3=BC=20UnifiedRepeater=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=EC=97=90=EC=84=9C=20=EB=B6=88?= =?UTF-8?q?=ED=95=84=EC=9A=94=ED=95=9C=20=EA=B3=B5=EB=B0=B1=EC=9D=84=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0=ED=95=98=EA=B3=A0,=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EA=B0=80=EB=8F=85=EC=84=B1=EC=9D=84=20=ED=96=A5=EC=83=81?= =?UTF-8?q?=EC=8B=9C=EC=BC=B0=EC=8A=B5=EB=8B=88=EB=8B=A4.=20=EB=98=90?= =?UTF-8?q?=ED=95=9C,=20UnifiedRepeaterConfigPanel=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=BB=AC=EB=9F=BC=20=EC=84=A0=ED=83=9D=20UI=EC=9D=98=20?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0=EB=A5=BC=20=EA=B0=9C=EC=84=A0=ED=95=98?= =?UTF-8?q?=EC=97=AC=20=EC=82=AC=EC=9A=A9=EC=9E=90=20=EA=B2=BD=ED=97=98?= =?UTF-8?q?=EC=9D=84=20=EA=B0=9C=EC=84=A0=ED=96=88=EC=8A=B5=EB=8B=88?= =?UTF-8?q?=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../screen/panels/ComponentsPanel.tsx | 12 +- .../components/unified/UnifiedRepeater.tsx | 118 +++++++++--------- .../config-panels/UnifiedListConfigPanel.tsx | 2 +- .../UnifiedRepeaterConfigPanel.tsx | 58 ++++----- .../modal-repeater-table/RepeaterTable.tsx | 16 +-- .../components/modal-repeater-table/types.ts | 2 +- .../SplitPanelLayoutConfigPanel.tsx | 24 ++-- .../table-list/TableListConfigPanel.tsx | 46 +++---- frontend/types/unified-repeater.ts | 10 +- 9 files changed, 144 insertions(+), 144 deletions(-) diff --git a/frontend/components/screen/panels/ComponentsPanel.tsx b/frontend/components/screen/panels/ComponentsPanel.tsx index 95ab8f41..48d5a601 100644 --- a/frontend/components/screen/panels/ComponentsPanel.tsx +++ b/frontend/components/screen/panels/ComponentsPanel.tsx @@ -275,13 +275,13 @@ export function ComponentsPanel({ {/* 테이블 컬럼 탭 */} - {})} + {})} onDragStart={onTableDragStart || (() => {})} - selectedTableName={selectedTableName} - placedColumns={placedColumns} + selectedTableName={selectedTableName} + placedColumns={placedColumns} onTableSelect={onTableSelect} showTableSelector={showTableSelector} /> diff --git a/frontend/components/unified/UnifiedRepeater.tsx b/frontend/components/unified/UnifiedRepeater.tsx index 1ed649b1..1437b5cf 100644 --- a/frontend/components/unified/UnifiedRepeater.tsx +++ b/frontend/components/unified/UnifiedRepeater.tsx @@ -6,7 +6,7 @@ * 렌더링 모드: * - inline: 현재 테이블 컬럼 직접 입력 * - modal: 엔티티 선택 (FK 저장) + 추가 입력 컬럼 - * + * * RepeaterTable 및 ItemSelectionModal 재사용 */ @@ -62,7 +62,7 @@ export const UnifiedRepeater: React.FC = ({ // 🆕 데이터 변경 시 자동으로 컬럼 너비 조정 트리거 const [autoWidthTrigger, setAutoWidthTrigger] = useState(0); - + // 소스 테이블 컬럼 라벨 매핑 const [sourceColumnLabels, setSourceColumnLabels] = useState>({}); @@ -71,10 +71,10 @@ export const UnifiedRepeater: React.FC = ({ // 🆕 카테고리 코드 → 라벨 매핑 (RepeaterTable 표시용) const [categoryLabelMap, setCategoryLabelMap] = useState>({}); - + // 현재 테이블 컬럼 정보 (inputType 매핑용) const [currentTableColumnInfo, setCurrentTableColumnInfo] = useState>({}); - + // 동적 데이터 소스 상태 const [activeDataSources, setActiveDataSources] = useState>({}); @@ -113,7 +113,7 @@ export const UnifiedRepeater: React.FC = ({ // 🆕 마스터 테이블에서 생성된 ID (FK 연결용) const masterRecordId = event.detail?.masterRecordId || mainFormData?.id; - + if (!tableName || data.length === 0) { console.log("📋 UnifiedRepeater 저장 스킵:", { tableName, dataLength: data.length }); return; @@ -139,10 +139,10 @@ export const UnifiedRepeater: React.FC = ({ } catch { console.warn("테이블 컬럼 정보 조회 실패"); } - + for (let i = 0; i < data.length; i++) { const row = data[i]; - + // 내부 필드 제거 const cleanRow = Object.fromEntries(Object.entries(row).filter(([key]) => !key.startsWith("_"))); @@ -159,11 +159,11 @@ export const UnifiedRepeater: React.FC = ({ } } else { // 기존 방식: 메인 폼 데이터 병합 - const { id: _mainId, ...mainFormDataWithoutId } = mainFormData || {}; + const { id: _mainId, ...mainFormDataWithoutId } = mainFormData || {}; mergedData = { - ...mainFormDataWithoutId, - ...cleanRow, - }; + ...mainFormDataWithoutId, + ...cleanRow, + }; } // 유효하지 않은 컬럼 제거 @@ -173,7 +173,7 @@ export const UnifiedRepeater: React.FC = ({ filteredData[key] = value; } } - + await apiClient.post(`/table-management/tables/${tableName}/add`, filteredData); } @@ -199,7 +199,7 @@ export const UnifiedRepeater: React.FC = ({ try { const response = await apiClient.get(`/table-management/tables/${tableName}/columns`); const columns = response.data?.data?.columns || response.data?.columns || response.data || []; - + const columnMap: Record = {}; columns.forEach((col: any) => { const name = col.columnName || col.column_name || col.name; @@ -292,7 +292,7 @@ export const UnifiedRepeater: React.FC = ({ try { const response = await apiClient.get(`/table-management/tables/${resolvedSourceTable}/columns`); const columns = response.data?.data?.columns || response.data?.columns || response.data || []; - + const labels: Record = {}; const categoryCols: string[] = []; @@ -336,13 +336,13 @@ export const UnifiedRepeater: React.FC = ({ calculated: true, width: col.width === "auto" ? undefined : col.width, }; - } - + } + // 일반 입력 컬럼 let type: "text" | "number" | "date" | "select" | "category" = "text"; - if (inputType === "number" || inputType === "decimal") type = "number"; - else if (inputType === "date" || inputType === "datetime") type = "date"; - else if (inputType === "code") type = "select"; + if (inputType === "number" || inputType === "decimal") type = "number"; + else if (inputType === "date" || inputType === "datetime") type = "date"; + else if (inputType === "code") type = "select"; else if (inputType === "category") type = "category"; // 🆕 카테고리 타입 // 🆕 카테고리 참조 ID 가져오기 (tableName.columnName 형식) @@ -355,19 +355,19 @@ export const UnifiedRepeater: React.FC = ({ categoryRef = `${tableName}.${col.key}`; } } - - return { - field: col.key, - label: col.title || colInfo?.displayName || col.key, - type, - editable: col.editable !== false, - width: col.width === "auto" ? undefined : col.width, - required: false, + + return { + field: col.key, + label: col.title || colInfo?.displayName || col.key, + type, + editable: col.editable !== false, + width: col.width === "auto" ? undefined : col.width, + required: false, categoryRef, // 🆕 카테고리 참조 ID 전달 hidden: col.hidden, // 🆕 히든 처리 autoFill: col.autoFill, // 🆕 자동 입력 설정 - }; - }); + }; + }); }, [config.columns, sourceColumnLabels, currentTableColumnInfo, resolvedSourceTable, config.dataSource?.tableName]); // 🆕 데이터 변경 시 카테고리 라벨 로드 (RepeaterTable 표시용) @@ -423,8 +423,8 @@ export const UnifiedRepeater: React.FC = ({ // 데이터 변경 핸들러 const handleDataChange = useCallback( (newData: any[]) => { - setData(newData); - onDataChange?.(newData); + setData(newData); + onDataChange?.(newData); // 🆕 데이터 변경 시 자동으로 컬럼 너비 조정 setAutoWidthTrigger((prev) => prev + 1); }, @@ -434,11 +434,11 @@ export const UnifiedRepeater: React.FC = ({ // 행 변경 핸들러 const handleRowChange = useCallback( (index: number, newRow: any) => { - const newData = [...data]; - newData[index] = newRow; + const newData = [...data]; + newData[index] = newRow; // 🆕 handleDataChange 대신 직접 호출 (행 변경마다 너비 조정은 불필요) - setData(newData); - onDataChange?.(newData); + setData(newData); + onDataChange?.(newData); }, [data, onDataChange], ); @@ -446,16 +446,16 @@ export const UnifiedRepeater: React.FC = ({ // 행 삭제 핸들러 const handleRowDelete = useCallback( (index: number) => { - const newData = data.filter((_, i) => i !== index); + const newData = data.filter((_, i) => i !== index); handleDataChange(newData); // 🆕 handleDataChange 사용 - - // 선택 상태 업데이트 - const newSelected = new Set(); - selectedRows.forEach((i) => { - if (i < index) newSelected.add(i); - else if (i > index) newSelected.add(i - 1); - }); - setSelectedRows(newSelected); + + // 선택 상태 업데이트 + const newSelected = new Set(); + selectedRows.forEach((i) => { + if (i < index) newSelected.add(i); + else if (i > index) newSelected.add(i - 1); + }); + setSelectedRows(newSelected); }, [data, selectedRows, handleDataChange], ); @@ -537,7 +537,7 @@ export const UnifiedRepeater: React.FC = ({ } else if (autoValue !== undefined) { newRow[col.key] = autoValue; } else { - newRow[col.key] = ""; + newRow[col.key] = ""; } } @@ -549,23 +549,23 @@ export const UnifiedRepeater: React.FC = ({ // 모달에서 항목 선택 - 비동기로 변경 const handleSelectItems = useCallback( async (items: Record[]) => { - const fkColumn = config.dataSource?.foreignKey; + const fkColumn = config.dataSource?.foreignKey; const currentRowCount = data.length; // 채번이 필요한 컬럼 찾기 const numberingColumns = config.columns.filter( (col) => !col.isSourceDisplay && col.autoFill?.type === "numbering" && col.autoFill.numberingRuleId ); - + const newRows = await Promise.all( items.map(async (item, index) => { - const row: any = { _id: `new_${Date.now()}_${Math.random()}` }; - + const row: any = { _id: `new_${Date.now()}_${Math.random()}` }; + // FK 값 저장 (resolvedReferenceKey 사용) if (fkColumn && item[resolvedReferenceKey]) { row[fkColumn] = item[resolvedReferenceKey]; - } - + } + // 모든 컬럼 처리 (순서대로) for (const col of config.columns) { if (col.isSourceDisplay) { @@ -581,18 +581,18 @@ export const UnifiedRepeater: React.FC = ({ row[col.key] = autoValue; } else if (row[col.key] === undefined) { // 입력 컬럼: 빈 값으로 초기화 - row[col.key] = ""; - } + row[col.key] = ""; + } } } - - return row; + + return row; }) ); - - const newData = [...data, ...newRows]; + + const newData = [...data, ...newRows]; handleDataChange(newData); - setModalOpen(false); + setModalOpen(false); }, [config.dataSource?.foreignKey, resolvedReferenceKey, config.columns, data, handleDataChange, generateAutoFillValueSync, generateNumberingCode], ); @@ -615,7 +615,7 @@ export const UnifiedRepeater: React.FC = ({ const formData = customEvent.detail?.formData; if (!formData || !dataRef.current.length) return; - + // 채번 placeholder가 있는 행들을 찾아서 실제 값으로 변환 const processedData = await Promise.all( dataRef.current.map(async (row) => { diff --git a/frontend/components/unified/config-panels/UnifiedListConfigPanel.tsx b/frontend/components/unified/config-panels/UnifiedListConfigPanel.tsx index cc66dbfb..00f75579 100644 --- a/frontend/components/unified/config-panels/UnifiedListConfigPanel.tsx +++ b/frontend/components/unified/config-panels/UnifiedListConfigPanel.tsx @@ -105,7 +105,7 @@ export const UnifiedListConfigPanel: React.FC = ({ } if (partialConfig.useCustomTable !== undefined) { newConfig.useCustomTable = partialConfig.useCustomTable; - } + } if (partialConfig.customTableName !== undefined) { newConfig.customTableName = partialConfig.customTableName; } diff --git a/frontend/components/unified/config-panels/UnifiedRepeaterConfigPanel.tsx b/frontend/components/unified/config-panels/UnifiedRepeaterConfigPanel.tsx index 85015604..19be9518 100644 --- a/frontend/components/unified/config-panels/UnifiedRepeaterConfigPanel.tsx +++ b/frontend/components/unified/config-panels/UnifiedRepeaterConfigPanel.tsx @@ -976,15 +976,15 @@ export const UnifiedRepeaterConfigPanel: React.FC {/* 통합 컬럼 선택 */} -
+
-

+

{isModalMode ? "표시할 컬럼과 입력 컬럼을 선택하세요. 아이콘으로 표시/입력 구분" : "입력받을 컬럼을 선택하세요" } -

- +

+ {/* 모달 모드: 소스 테이블 컬럼 (표시용) */} {isModalMode && config.dataSource?.sourceTable && ( <> @@ -1019,9 +1019,9 @@ export const UnifiedRepeaterConfigPanel: React.FC )} - - )} - + + )} + {/* 저장 테이블 컬럼 (입력용) */}
@@ -1113,15 +1113,15 @@ export const UnifiedRepeaterConfigPanel: React.FC ) : ( - + )} - updateColumnProp(col.key, "title", e.target.value)} - placeholder="제목" - className="h-6 flex-1 text-xs" - /> + updateColumnProp(col.key, "title", e.target.value)} + placeholder="제목" + className="h-6 flex-1 text-xs" + /> {/* 히든 토글 (입력 컬럼만) */} {!col.isSourceDisplay && ( @@ -1133,7 +1133,7 @@ export const UnifiedRepeaterConfigPanel: React.FC + > {col.hidden ? : } )} @@ -1145,17 +1145,17 @@ export const UnifiedRepeaterConfigPanel: React.FC updateColumnProp(col.key, "editable", !!checked)} - title="편집 가능" - /> + updateColumnProp(col.key, "editable", !!checked)} + title="편집 가능" + /> )} - -
+ className="text-destructive h-6 w-6 p-0" + > + + +
{/* 확장된 상세 설정 (입력 컬럼만) */} {!col.isSourceDisplay && expandedColumn === col.key && ( diff --git a/frontend/lib/registry/components/modal-repeater-table/RepeaterTable.tsx b/frontend/lib/registry/components/modal-repeater-table/RepeaterTable.tsx index 055cd6f1..e6876a49 100644 --- a/frontend/lib/registry/components/modal-repeater-table/RepeaterTable.tsx +++ b/frontend/lib/registry/components/modal-repeater-table/RepeaterTable.tsx @@ -231,8 +231,8 @@ export function RepeaterTable({ columns .filter((col) => !col.hidden) .forEach((col) => { - widths[col.field] = col.width ? parseInt(col.width) : 120; - }); + widths[col.field] = col.width ? parseInt(col.width) : 120; + }); return widths; }); @@ -415,10 +415,10 @@ export function RepeaterTable({ // 데이터가 있으면 데이터 기반 자동 맞춤, 없으면 균등 분배 const timer = setTimeout(() => { if (data.length > 0) { - applyAutoFitWidths(); - } else { - applyEqualizeWidths(); - } + applyAutoFitWidths(); + } else { + applyEqualizeWidths(); + } }, 50); return () => clearTimeout(timer); @@ -799,7 +799,7 @@ export function RepeaterTable({ {/* 드래그 핸들 - 좌측 고정 */} @@ -818,7 +818,7 @@ export function RepeaterTable({ {/* 체크박스 - 좌측 고정 */} diff --git a/frontend/lib/registry/components/modal-repeater-table/types.ts b/frontend/lib/registry/components/modal-repeater-table/types.ts index 7089c958..11ea517a 100644 --- a/frontend/lib/registry/components/modal-repeater-table/types.ts +++ b/frontend/lib/registry/components/modal-repeater-table/types.ts @@ -62,7 +62,7 @@ export interface RepeaterColumnConfig { fixedValue?: string | number; // fixed 타입일 때 고정값 format?: string; // 날짜 포맷 등 }; - + // 컬럼 매핑 설정 mapping?: ColumnMapping; // 이 컬럼의 데이터를 어디서 가져올지 설정 diff --git a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutConfigPanel.tsx b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutConfigPanel.tsx index 724bcbc3..d678332e 100644 --- a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutConfigPanel.tsx +++ b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutConfigPanel.tsx @@ -1098,25 +1098,25 @@ export const SplitPanelLayoutConfigPanel: React.FC { - const loadAllTables = async () => { - try { - const { tableManagementApi } = await import("@/lib/api/tableManagement"); - const response = await tableManagementApi.getTableList(); - if (response.success && response.data) { + const loadAllTables = async () => { + try { + const { tableManagementApi } = await import("@/lib/api/tableManagement"); + const response = await tableManagementApi.getTableList(); + if (response.success && response.data) { console.log("✅ 분할패널: 전체 테이블 목록 로드", response.data.length, "개"); - setAllTables(response.data); + setAllTables(response.data); + } + } catch (error) { + console.error("❌ 전체 테이블 목록 로드 실패:", error); } - } catch (error) { - console.error("❌ 전체 테이블 목록 로드 실패:", error); - } - }; - loadAllTables(); + }; + loadAllTables(); }, []); // 초기 로드 시 좌측 패널 테이블이 없으면 화면 테이블로 설정 useEffect(() => { if (screenTableName && !config.leftPanel?.tableName) { - updateLeftPanel({ tableName: screenTableName }); + updateLeftPanel({ tableName: screenTableName }); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [screenTableName]); diff --git a/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx b/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx index 9e7f63c4..0b1d1bed 100644 --- a/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx +++ b/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx @@ -200,7 +200,7 @@ export const TableListConfigPanel: React.FC = ({ const result = await tableManagementApi.getColumnList(targetTableName); console.log("🔧 tableManagementApi 응답:", result); - if (result.success && result.data) { + if (result.success && result.data) { // API 응답 구조: { columns: [...], total, page, ... } const columns = Array.isArray(result.data) ? result.data : result.data.columns; console.log("🔧 컬럼 배열:", columns); @@ -862,7 +862,7 @@ export const TableListConfigPanel: React.FC = ({ "mr-2 h-3 w-3", config.customTableName === table.tableName ? "opacity-100" : "opacity-0", )} - /> + /> {table.displayName || table.tableName} @@ -1283,25 +1283,25 @@ export const TableListConfigPanel: React.FC = ({
- {joinTable.availableColumns.map((column, colIndex) => { - const matchingJoinColumn = entityJoinColumns.availableColumns.find( - (jc) => jc.tableName === joinTable.tableName && jc.columnName === column.columnName, - ); + {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, - ); + const isAlreadyAdded = config.columns?.some( + (col) => col.columnName === matchingJoinColumn?.joinAlias, + ); if (!matchingJoinColumn) return null; - return ( -
{ + onClick={() => { if (isAlreadyAdded) { // 컬럼 제거 handleChange("columns", config.columns?.filter((c) => c.columnName !== matchingJoinColumn.joinAlias) || []); @@ -1311,28 +1311,28 @@ export const TableListConfigPanel: React.FC = ({ } }} > - { if (isAlreadyAdded) { handleChange("columns", config.columns?.filter((c) => c.columnName !== matchingJoinColumn.joinAlias) || []); - } else { + } else { addEntityColumn(matchingJoinColumn); - } - }} + } + }} className="pointer-events-none h-3.5 w-3.5" /> {column.columnLabel} {column.inputType || column.dataType} -
- ); - })} -
- + + ); + })} + + ))} - - + + )} )} diff --git a/frontend/types/unified-repeater.ts b/frontend/types/unified-repeater.ts index c4274612..84385985 100644 --- a/frontend/types/unified-repeater.ts +++ b/frontend/types/unified-repeater.ts @@ -1,6 +1,6 @@ /** * UnifiedRepeater 컴포넌트 타입 정의 - * + * * 렌더링 모드: * - inline: 현재 테이블 컬럼 직접 입력 (simple-repeater-table) * - modal: 소스 테이블에서 검색/선택 후 복사 (modal-repeater-table) @@ -77,11 +77,11 @@ export interface RepeaterModalConfig { size: ModalSize; title?: string; // 모달 제목 buttonText?: string; // 검색 버튼 텍스트 - + // 소스 테이블 표시 설정 (modal 모드) sourceDisplayColumns?: ModalDisplayColumn[]; // 모달에 표시할 소스 테이블 컬럼 (라벨 포함) searchFields?: string[]; // 검색에 사용할 필드 - + // 화면 기반 모달 (옵션) screenId?: number; titleTemplate?: { @@ -105,13 +105,13 @@ export interface RepeaterFeatureOptions { // 데이터 소스 설정 export interface RepeaterDataSource { // inline 모드: 현재 테이블 설정은 필요 없음 (컬럼만 선택) - + // modal 모드: 소스 테이블 설정 sourceTable?: string; // 검색할 테이블 (엔티티 참조 테이블) foreignKey?: string; // 현재 테이블의 FK 컬럼 (part_objid 등) referenceKey?: string; // 소스 테이블의 PK 컬럼 (id 등) displayColumn?: string; // 표시할 컬럼 (item_name 등) - + // 추가 필터 filter?: { column: string;