From b4cc844675eaf08e08a53c104f5720de1cde22c1 Mon Sep 17 00:00:00 2001 From: leeheejin Date: Wed, 5 Nov 2025 10:23:00 +0900 Subject: [PATCH] =?UTF-8?q?=EC=97=91=EC=85=80=20=EB=8B=A4=EC=9A=B4?= =?UTF-8?q?=EB=A1=9C=EB=93=9C=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/(main)/screens/[screenId]/page.tsx | 12 +- .../components/screen/RealtimePreview.tsx | 15 +- .../lib/registry/DynamicComponentRenderer.tsx | 8 +- .../button-primary/ButtonPrimaryComponent.tsx | 3 + .../table-list/TableListComponent.tsx | 242 +++++++++++++++++- frontend/lib/utils/buttonActions.ts | 71 ++++- frontend/stores/tableDisplayStore.ts | 110 ++++++++ 7 files changed, 447 insertions(+), 14 deletions(-) create mode 100644 frontend/stores/tableDisplayStore.ts diff --git a/frontend/app/(main)/screens/[screenId]/page.tsx b/frontend/app/(main)/screens/[screenId]/page.tsx index 0c9a681b..741a5175 100644 --- a/frontend/app/(main)/screens/[screenId]/page.tsx +++ b/frontend/app/(main)/screens/[screenId]/page.tsx @@ -44,6 +44,7 @@ export default function ScreenViewPage() { const [tableSortBy, setTableSortBy] = useState(); const [tableSortOrder, setTableSortOrder] = useState<"asc" | "desc">("asc"); const [tableColumnOrder, setTableColumnOrder] = useState(); + const [tableDisplayData, setTableDisplayData] = useState([]); // 화면에 표시된 데이터 (컬럼 순서 포함) // 플로우에서 선택된 데이터 (버튼 액션에 전달) const [flowSelectedData, setFlowSelectedData] = useState([]); @@ -433,13 +434,16 @@ export default function ScreenViewPage() { sortBy={tableSortBy} sortOrder={tableSortOrder} columnOrder={tableColumnOrder} - onSelectedRowsChange={(_, selectedData, sortBy, sortOrder, columnOrder) => { + tableDisplayData={tableDisplayData} + onSelectedRowsChange={(_, selectedData, sortBy, sortOrder, columnOrder, tableDisplayData) => { console.log("🔍 화면에서 선택된 행 데이터:", selectedData); console.log("📊 정렬 정보:", { sortBy, sortOrder, columnOrder }); + console.log("📊 화면 표시 데이터:", { count: tableDisplayData?.length, firstRow: tableDisplayData?.[0] }); setSelectedRowsData(selectedData); setTableSortBy(sortBy); setTableSortOrder(sortOrder || "asc"); setTableColumnOrder(columnOrder); + setTableDisplayData(tableDisplayData || []); }} flowSelectedData={flowSelectedData} flowSelectedStepId={flowSelectedStepId} @@ -494,13 +498,16 @@ export default function ScreenViewPage() { sortBy={tableSortBy} sortOrder={tableSortOrder} columnOrder={tableColumnOrder} - onSelectedRowsChange={(_, selectedData, sortBy, sortOrder, columnOrder) => { + tableDisplayData={tableDisplayData} + onSelectedRowsChange={(_, selectedData, sortBy, sortOrder, columnOrder, tableDisplayData) => { console.log("🔍 화면에서 선택된 행 데이터 (자식):", selectedData); console.log("📊 정렬 정보 (자식):", { sortBy, sortOrder, columnOrder }); + console.log("📊 화면 표시 데이터 (자식):", { count: tableDisplayData?.length, firstRow: tableDisplayData?.[0] }); setSelectedRowsData(selectedData); setTableSortBy(sortBy); setTableSortOrder(sortOrder || "asc"); setTableColumnOrder(columnOrder); + setTableDisplayData(tableDisplayData || []); }} refreshKey={tableRefreshKey} onRefresh={() => { @@ -631,6 +638,7 @@ export default function ScreenViewPage() { userId={user?.userId} userName={userName} companyCode={companyCode} + tableDisplayData={tableDisplayData} selectedRowsData={selectedRowsData} sortBy={tableSortBy} sortOrder={tableSortOrder} diff --git a/frontend/components/screen/RealtimePreview.tsx b/frontend/components/screen/RealtimePreview.tsx index 777facef..097e6c71 100644 --- a/frontend/components/screen/RealtimePreview.tsx +++ b/frontend/components/screen/RealtimePreview.tsx @@ -66,6 +66,7 @@ interface RealtimePreviewProps { // 테이블 정렬 정보 전달용 sortBy?: string; sortOrder?: "asc" | "desc"; + tableDisplayData?: any[]; // 🆕 화면 표시 데이터 [key: string]: any; // 추가 props 허용 } @@ -109,7 +110,14 @@ const renderArea = (component: ComponentData, children?: React.ReactNode) => { }; // 동적 웹 타입 위젯 렌더링 컴포넌트 -const WidgetRenderer: React.FC<{ component: ComponentData; isDesignMode?: boolean }> = ({ component, isDesignMode = false }) => { +const WidgetRenderer: React.FC<{ + component: ComponentData; + isDesignMode?: boolean; + sortBy?: string; + sortOrder?: "asc" | "desc"; + tableDisplayData?: any[]; + [key: string]: any; +}> = ({ component, isDesignMode = false, sortBy, sortOrder, tableDisplayData, ...restProps }) => { // 위젯 컴포넌트가 아닌 경우 빈 div 반환 if (!isWidgetComponent(component)) { return
위젯이 아닙니다
; @@ -158,6 +166,9 @@ const WidgetRenderer: React.FC<{ component: ComponentData; isDesignMode?: boolea readonly: readonly, isDesignMode, isInteractive: !isDesignMode, + sortBy, // 🆕 정렬 정보 + sortOrder, // 🆕 정렬 방향 + tableDisplayData, // 🆕 화면 표시 데이터 }} config={widget.webTypeConfig} /> @@ -231,6 +242,7 @@ export const RealtimePreviewDynamic: React.FC = ({ onFlowSelectedDataChange, sortBy, sortOrder, + tableDisplayData, // 🆕 화면 표시 데이터 ...restProps }) => { const { user } = useAuth(); @@ -557,6 +569,7 @@ export const RealtimePreviewDynamic: React.FC = ({ isDesignMode={isDesignMode} sortBy={sortBy} sortOrder={sortOrder} + tableDisplayData={tableDisplayData} {...restProps} /> diff --git a/frontend/lib/registry/DynamicComponentRenderer.tsx b/frontend/lib/registry/DynamicComponentRenderer.tsx index cdb81291..2c646138 100644 --- a/frontend/lib/registry/DynamicComponentRenderer.tsx +++ b/frontend/lib/registry/DynamicComponentRenderer.tsx @@ -29,10 +29,11 @@ export interface ComponentRenderer { // 테이블 선택된 행 정보 (다중 선택 액션용) selectedRows?: any[]; selectedRowsData?: any[]; - onSelectedRowsChange?: (selectedRows: any[], selectedRowsData: any[], sortBy?: string, sortOrder?: "asc" | "desc", columnOrder?: string[]) => void; + onSelectedRowsChange?: (selectedRows: any[], selectedRowsData: any[], sortBy?: string, sortOrder?: "asc" | "desc", columnOrder?: string[], tableDisplayData?: any[]) => void; // 테이블 정렬 정보 (엑셀 다운로드용) sortBy?: string; sortOrder?: "asc" | "desc"; + tableDisplayData?: any[]; // 🆕 화면 표시 데이터 // 플로우 선택된 데이터 정보 (플로우 위젯 선택 액션용) flowSelectedData?: any[]; flowSelectedStepId?: number | null; @@ -104,11 +105,12 @@ export interface DynamicComponentRendererProps { // 테이블 선택된 행 정보 (다중 선택 액션용) selectedRows?: any[]; selectedRowsData?: any[]; - onSelectedRowsChange?: (selectedRows: any[], selectedRowsData: any[], sortBy?: string, sortOrder?: "asc" | "desc", columnOrder?: string[]) => void; + onSelectedRowsChange?: (selectedRows: any[], selectedRowsData: any[], sortBy?: string, sortOrder?: "asc" | "desc", columnOrder?: string[], tableDisplayData?: any[]) => void; // 테이블 정렬 정보 (엑셀 다운로드용) sortBy?: string; sortOrder?: "asc" | "desc"; columnOrder?: string[]; + tableDisplayData?: any[]; // 🆕 화면 표시 데이터 // 플로우 선택된 데이터 정보 (플로우 위젯 선택 액션용) flowSelectedData?: any[]; flowSelectedStepId?: number | null; @@ -200,6 +202,7 @@ export const DynamicComponentRenderer: React.FC = onSelectedRowsChange, sortBy, // 🆕 정렬 컬럼 sortOrder, // 🆕 정렬 방향 + tableDisplayData, // 🆕 화면 표시 데이터 flowSelectedData, flowSelectedStepId, onFlowSelectedDataChange, @@ -290,6 +293,7 @@ export const DynamicComponentRenderer: React.FC = // 테이블 정렬 정보 전달 sortBy, sortOrder, + tableDisplayData, // 🆕 화면 표시 데이터 // 플로우 선택된 데이터 정보 전달 flowSelectedData, flowSelectedStepId, diff --git a/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx b/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx index a2c584af..db4e150e 100644 --- a/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx +++ b/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx @@ -47,6 +47,7 @@ export interface ButtonPrimaryComponentProps extends ComponentRendererProps { sortBy?: string; sortOrder?: "asc" | "desc"; columnOrder?: string[]; + tableDisplayData?: any[]; // 화면에 표시된 데이터 (컬럼 순서 포함) // 플로우 선택된 데이터 정보 (플로우 위젯 선택 액션용) flowSelectedData?: any[]; @@ -82,6 +83,7 @@ export const ButtonPrimaryComponent: React.FC = ({ sortBy, // 🆕 정렬 컬럼 sortOrder, // 🆕 정렬 방향 columnOrder, // 🆕 컬럼 순서 + tableDisplayData, // 🆕 화면에 표시된 데이터 selectedRows, selectedRowsData, flowSelectedData, @@ -417,6 +419,7 @@ export const ButtonPrimaryComponent: React.FC = ({ sortBy, // 🆕 정렬 컬럼 sortOrder, // 🆕 정렬 방향 columnOrder, // 🆕 컬럼 순서 + tableDisplayData, // 🆕 화면에 표시된 데이터 // 플로우 선택된 데이터 정보 추가 flowSelectedData, flowSelectedStepId, diff --git a/frontend/lib/registry/components/table-list/TableListComponent.tsx b/frontend/lib/registry/components/table-list/TableListComponent.tsx index d5152319..fad0a5c4 100644 --- a/frontend/lib/registry/components/table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx @@ -25,6 +25,7 @@ import { import { Checkbox } from "@/components/ui/checkbox"; import { cn } from "@/lib/utils"; import { toast } from "sonner"; +import { tableDisplayStore } from "@/stores/tableDisplayStore"; import { Dialog, DialogContent, @@ -139,7 +140,7 @@ export interface TableListComponentProps { onClose?: () => void; screenId?: string; userId?: string; // 사용자 ID (컬럼 순서 저장용) - onSelectedRowsChange?: (selectedRows: any[], selectedRowsData: any[], sortBy?: string, sortOrder?: "asc" | "desc", columnOrder?: string[]) => void; + onSelectedRowsChange?: (selectedRows: any[], selectedRowsData: any[], sortBy?: string, sortOrder?: "asc" | "desc", columnOrder?: string[], tableDisplayData?: any[]) => void; onConfigChange?: (config: any) => void; refreshKey?: number; } @@ -266,6 +267,62 @@ export const TableListComponent: React.FC = ({ const [groupByColumns, setGroupByColumns] = useState([]); const [collapsedGroups, setCollapsedGroups] = useState>(new Set()); + // 🆕 초기 로드 시 localStorage에서 컬럼 순서 불러오기 + useEffect(() => { + if (!tableConfig.selectedTable || !userId) return; + + const userKey = userId || 'guest'; + const storageKey = `table_column_order_${tableConfig.selectedTable}_${userKey}`; + const savedOrder = localStorage.getItem(storageKey); + + if (savedOrder) { + try { + const parsedOrder = JSON.parse(savedOrder); + console.log("📂 localStorage에서 컬럼 순서 불러오기:", { storageKey, columnOrder: parsedOrder }); + setColumnOrder(parsedOrder); + + // 부모 컴포넌트에 초기 컬럼 순서 전달 + if (onSelectedRowsChange && parsedOrder.length > 0) { + console.log("✅ 초기 컬럼 순서 전달:", parsedOrder); + + // 초기 데이터도 함께 전달 (컬럼 순서대로 재정렬) + const initialData = data.map((row: any) => { + const reordered: any = {}; + parsedOrder.forEach((colName: string) => { + if (colName in row) { + reordered[colName] = row[colName]; + } + }); + // 나머지 컬럼 추가 + Object.keys(row).forEach((key) => { + if (!(key in reordered)) { + reordered[key] = row[key]; + } + }); + return reordered; + }); + + console.log("📊 초기 화면 표시 데이터 전달:", { count: initialData.length, firstRow: initialData[0] }); + + // 전역 저장소에 데이터 저장 + if (tableConfig.selectedTable) { + tableDisplayStore.setTableData( + tableConfig.selectedTable, + initialData, + parsedOrder.filter(col => col !== '__checkbox__'), + sortColumn, + sortDirection + ); + } + + onSelectedRowsChange([], [], sortColumn, sortDirection, parsedOrder, initialData); + } + } catch (error) { + console.error("❌ 컬럼 순서 파싱 실패:", error); + } + } + }, [tableConfig.selectedTable, userId, data.length]); // data.length 추가 (데이터 로드 후 실행) + const { optimizedConvertCode } = useEntityJoinOptimization(columnMeta, { enableBatchLoading: true, preloadCommonCodes: true, @@ -499,20 +556,78 @@ export const TableListComponent: React.FC = ({ // 정렬 변경 시 선택 정보와 함께 정렬 정보도 전달 if (onSelectedRowsChange) { const selectedRowsData = data.filter((row, index) => selectedRows.has(getRowKey(row, index))); + + // 1단계: 데이터를 정렬 + const sortedData = [...data].sort((a, b) => { + const aVal = a[newSortColumn]; + const bVal = b[newSortColumn]; + + // null/undefined 처리 + if (aVal == null && bVal == null) return 0; + if (aVal == null) return 1; + if (bVal == null) return -1; + + // 숫자 비교 + const aNum = Number(aVal); + const bNum = Number(bVal); + if (!isNaN(aNum) && !isNaN(bNum)) { + return newSortDirection === "desc" ? bNum - aNum : aNum - bNum; + } + + // 문자열 비교 + const aStr = String(aVal); + const bStr = String(bVal); + const comparison = aStr.localeCompare(bStr); + return newSortDirection === "desc" ? -comparison : comparison; + }); + + // 2단계: 정렬된 데이터를 컬럼 순서대로 재정렬 + const reorderedData = sortedData.map((row: any) => { + const reordered: any = {}; + visibleColumns.forEach((col) => { + if (col.columnName in row) { + reordered[col.columnName] = row[col.columnName]; + } + }); + // 나머지 컬럼 추가 + Object.keys(row).forEach((key) => { + if (!(key in reordered)) { + reordered[key] = row[key]; + } + }); + return reordered; + }); + console.log("✅ 정렬 정보 전달:", { selectedRowsCount: selectedRows.size, selectedRowsDataCount: selectedRowsData.length, sortBy: newSortColumn, sortOrder: newSortDirection, - columnOrder: columnOrder.length > 0 ? columnOrder : undefined + columnOrder: columnOrder.length > 0 ? columnOrder : undefined, + tableDisplayDataCount: reorderedData.length, + firstRowAfterSort: reorderedData[0]?.[newSortColumn], + lastRowAfterSort: reorderedData[reorderedData.length - 1]?.[newSortColumn] }); onSelectedRowsChange( Array.from(selectedRows), selectedRowsData, newSortColumn, newSortDirection, - columnOrder.length > 0 ? columnOrder : undefined + columnOrder.length > 0 ? columnOrder : undefined, + reorderedData ); + + // 전역 저장소에 정렬된 데이터 저장 + if (tableConfig.selectedTable) { + const cleanColumnOrder = (columnOrder.length > 0 ? columnOrder : visibleColumns.map(c => c.columnName)).filter(col => col !== '__checkbox__'); + tableDisplayStore.setTableData( + tableConfig.selectedTable, + reorderedData, + cleanColumnOrder, + newSortColumn, + newSortDirection + ); + } } else { console.warn("⚠️ onSelectedRowsChange 콜백이 없습니다!"); } @@ -653,6 +768,55 @@ export const TableListComponent: React.FC = ({ setColumnOrder(newColumnOrder); console.log("🔄 columnOrder 상태 업데이트:", newColumnOrder); + // 컬럼 순서 변경을 부모 컴포넌트에 전달 (엑셀 다운로드용) + if (onSelectedRowsChange) { + const selectedRowsData = data.filter((row, index) => selectedRows.has(getRowKey(row, index))); + + // 화면에 표시된 데이터를 새로운 컬럼 순서대로 재정렬 + const reorderedData = data.map((row: any) => { + const reordered: any = {}; + newColumns.forEach((col) => { + if (col.columnName in row) { + reordered[col.columnName] = row[col.columnName]; + } + }); + // 나머지 컬럼 추가 + Object.keys(row).forEach((key) => { + if (!(key in reordered)) { + reordered[key] = row[key]; + } + }); + return reordered; + }); + + console.log("✅ 컬럼 순서 변경 정보 전달:", { + columnOrder: newColumnOrder, + sortBy: sortColumn, + sortOrder: sortDirection, + reorderedDataCount: reorderedData.length + }); + onSelectedRowsChange( + Array.from(selectedRows), + selectedRowsData, + sortColumn, + sortDirection, + newColumnOrder, + reorderedData + ); + + // 전역 저장소에 컬럼 순서 변경된 데이터 저장 + if (tableConfig.selectedTable) { + const cleanColumnOrder = newColumnOrder.filter(col => col !== '__checkbox__'); + tableDisplayStore.setTableData( + tableConfig.selectedTable, + reorderedData, + cleanColumnOrder, + sortColumn, + sortDirection + ); + } + } + setDraggedColumnIndex(null); setDragOverColumnIndex(null); }; @@ -714,6 +878,78 @@ export const TableListComponent: React.FC = ({ return cols.sort((a, b) => (a.order || 0) - (b.order || 0)); }, [tableConfig.columns, tableConfig.checkbox, columnOrder]); + // 🆕 visibleColumns가 변경될 때마다 현재 컬럼 순서를 부모에게 전달 + const lastColumnOrderRef = useRef(""); + + useEffect(() => { + console.log("🔍 [컬럼 순서 전달 useEffect] 실행됨:", { + hasCallback: !!onSelectedRowsChange, + visibleColumnsLength: visibleColumns.length, + visibleColumnsNames: visibleColumns.map(c => c.columnName), + }); + + if (!onSelectedRowsChange) { + console.warn("⚠️ onSelectedRowsChange 콜백이 없습니다!"); + return; + } + + if (visibleColumns.length === 0) { + console.warn("⚠️ visibleColumns가 비어있습니다!"); + return; + } + + const currentColumnOrder = visibleColumns + .map(col => col.columnName) + .filter(name => name !== "__checkbox__"); // 체크박스 컬럼 제외 + + console.log("🔍 [컬럼 순서] 체크박스 제외 후:", currentColumnOrder); + + // 컬럼 순서가 실제로 변경되었을 때만 전달 (무한 루프 방지) + const columnOrderString = currentColumnOrder.join(","); + console.log("🔍 [컬럼 순서] 비교:", { + current: columnOrderString, + last: lastColumnOrderRef.current, + isDifferent: columnOrderString !== lastColumnOrderRef.current, + }); + + if (columnOrderString === lastColumnOrderRef.current) { + console.log("⏭️ 컬럼 순서 변경 없음, 전달 스킵"); + return; + } + + lastColumnOrderRef.current = columnOrderString; + console.log("📊 현재 화면 컬럼 순서 전달:", currentColumnOrder); + + // 선택된 행 데이터 가져오기 + const selectedRowsData = data.filter((row, index) => selectedRows.has(getRowKey(row, index))); + + // 화면에 표시된 데이터를 컬럼 순서대로 재정렬 + const reorderedData = data.map((row: any) => { + const reordered: any = {}; + visibleColumns.forEach((col) => { + if (col.columnName in row) { + reordered[col.columnName] = row[col.columnName]; + } + }); + // 나머지 컬럼 추가 + Object.keys(row).forEach((key) => { + if (!(key in reordered)) { + reordered[key] = row[key]; + } + }); + return reordered; + }); + + onSelectedRowsChange( + Array.from(selectedRows), + selectedRowsData, + sortColumn, + sortDirection, + currentColumnOrder, + reorderedData + ); + }, [visibleColumns.length, visibleColumns.map(c => c.columnName).join(",")]); // 의존성 단순화 + const getColumnWidth = (column: ColumnConfig) => { if (column.columnName === "__checkbox__") return 50; if (column.width) return column.width; diff --git a/frontend/lib/utils/buttonActions.ts b/frontend/lib/utils/buttonActions.ts index 03d56aed..b3263a6c 100644 --- a/frontend/lib/utils/buttonActions.ts +++ b/frontend/lib/utils/buttonActions.ts @@ -1689,6 +1689,17 @@ export class ButtonActionExecutor { private static async handleExcelDownload(config: ButtonActionConfig, context: ButtonActionContext): Promise { try { console.log("📥 엑셀 다운로드 시작:", { config, context }); + console.log("🔍 context.columnOrder 확인:", { + hasColumnOrder: !!context.columnOrder, + columnOrderLength: context.columnOrder?.length, + columnOrder: context.columnOrder, + }); + console.log("🔍 context.tableDisplayData 확인:", { + hasTableDisplayData: !!context.tableDisplayData, + tableDisplayDataLength: context.tableDisplayData?.length, + tableDisplayDataFirstRow: context.tableDisplayData?.[0], + tableDisplayDataColumns: context.tableDisplayData?.[0] ? Object.keys(context.tableDisplayData[0]) : [], + }); // 동적 import로 엑셀 유틸리티 로드 const { exportToExcel } = await import("@/lib/utils/excelExport"); @@ -1736,8 +1747,38 @@ export class ButtonActionExecutor { }); } } - // 2순위: 테이블 전체 데이터 (API 호출) + // 2순위: 화면 표시 데이터 (컬럼 순서 포함, 정렬 적용됨) + else if (context.tableDisplayData && context.tableDisplayData.length > 0) { + dataToExport = context.tableDisplayData; + console.log("✅ 화면 표시 데이터 사용 (context):", { + count: dataToExport.length, + firstRow: dataToExport[0], + columns: Object.keys(dataToExport[0] || {}), + }); + } + // 2.5순위: 전역 저장소에서 화면 표시 데이터 조회 else if (context.tableName) { + const { tableDisplayStore } = await import("@/stores/tableDisplayStore"); + const storedData = tableDisplayStore.getTableData(context.tableName); + + if (storedData && storedData.data.length > 0) { + dataToExport = storedData.data; + console.log("✅ 화면 표시 데이터 사용 (전역 저장소):", { + tableName: context.tableName, + count: dataToExport.length, + firstRow: dataToExport[0], + lastRow: dataToExport[dataToExport.length - 1], + columns: Object.keys(dataToExport[0] || {}), + columnOrder: storedData.columnOrder, + sortBy: storedData.sortBy, + sortOrder: storedData.sortOrder, + // 정렬 컬럼의 첫/마지막 값 확인 + firstSortValue: storedData.sortBy ? dataToExport[0]?.[storedData.sortBy] : undefined, + lastSortValue: storedData.sortBy ? dataToExport[dataToExport.length - 1]?.[storedData.sortBy] : undefined, + }); + } + // 3순위: 테이블 전체 데이터 (API 호출) + else { console.log("🔄 테이블 전체 데이터 조회 중...", context.tableName); console.log("📊 정렬 정보:", { sortBy: context.sortBy, @@ -1773,6 +1814,7 @@ export class ButtonActionExecutor { } catch (error) { console.error("❌ 테이블 데이터 조회 실패:", error); } + } } // 4순위: 폼 데이터 else if (context.formData && Object.keys(context.formData).length > 0) { @@ -1814,15 +1856,26 @@ export class ButtonActionExecutor { const sheetName = config.excelSheetName || "Sheet1"; const includeHeaders = config.excelIncludeHeaders !== false; - // 🆕 컬럼 순서 재정렬 (사용자가 드래그앤드롭으로 변경한 순서 적용) - if (context.columnOrder && context.columnOrder.length > 0 && dataToExport.length > 0) { - console.log("🔄 컬럼 순서 재정렬:", context.columnOrder); + // 🆕 컬럼 순서 재정렬 (화면에 표시된 순서대로) + let columnOrder: string[] | undefined = context.columnOrder; + + // columnOrder가 없으면 tableDisplayData에서 추출 시도 + if (!columnOrder && context.tableDisplayData && context.tableDisplayData.length > 0) { + columnOrder = Object.keys(context.tableDisplayData[0]); + console.log("📊 tableDisplayData에서 컬럼 순서 추출:", columnOrder); + } + + if (columnOrder && columnOrder.length > 0 && dataToExport.length > 0) { + console.log("🔄 컬럼 순서 재정렬 시작:", { + columnOrder, + originalColumns: Object.keys(dataToExport[0] || {}), + }); dataToExport = dataToExport.map((row: any) => { const reorderedRow: any = {}; // 1. columnOrder에 있는 컬럼들을 순서대로 추가 - context.columnOrder!.forEach((colName: string) => { + columnOrder!.forEach((colName: string) => { if (colName in row) { reorderedRow[colName] = row[colName]; } @@ -1839,9 +1892,15 @@ export class ButtonActionExecutor { }); console.log("✅ 컬럼 순서 재정렬 완료:", { - originalColumns: Object.keys(dataToExport[0] || {}), reorderedColumns: Object.keys(dataToExport[0] || {}), }); + } else { + console.log("⏭️ 컬럼 순서 재정렬 스킵:", { + hasColumnOrder: !!columnOrder, + columnOrderLength: columnOrder?.length, + hasTableDisplayData: !!context.tableDisplayData, + dataToExportLength: dataToExport.length, + }); } console.log("📥 엑셀 다운로드 실행:", { diff --git a/frontend/stores/tableDisplayStore.ts b/frontend/stores/tableDisplayStore.ts new file mode 100644 index 00000000..570f41f0 --- /dev/null +++ b/frontend/stores/tableDisplayStore.ts @@ -0,0 +1,110 @@ +/** + * 테이블 화면 표시 데이터 전역 저장소 + * 엑셀 다운로드 등에서 현재 화면에 표시된 데이터에 접근하기 위함 + */ + +interface TableDisplayState { + data: any[]; + columnOrder: string[]; + sortBy: string | null; + sortOrder: "asc" | "desc"; + tableName: string; +} + +class TableDisplayStore { + private state: Map = new Map(); + private listeners: Set<() => void> = new Set(); + + /** + * 테이블 표시 데이터 저장 + * @param tableName 테이블명 + * @param data 화면에 표시된 데이터 + * @param columnOrder 컬럼 순서 + * @param sortBy 정렬 컬럼 + * @param sortOrder 정렬 방향 + */ + setTableData( + tableName: string, + data: any[], + columnOrder: string[], + sortBy: string | null, + sortOrder: "asc" | "desc" + ) { + this.state.set(tableName, { + data, + columnOrder, + sortBy, + sortOrder, + tableName, + }); + + console.log("📦 [TableDisplayStore] 데이터 저장:", { + tableName, + dataCount: data.length, + columnOrderLength: columnOrder.length, + sortBy, + sortOrder, + firstRow: data[0], + }); + + this.notifyListeners(); + } + + /** + * 테이블 표시 데이터 조회 + * @param tableName 테이블명 + */ + getTableData(tableName: string): TableDisplayState | undefined { + const state = this.state.get(tableName); + + console.log("📤 [TableDisplayStore] 데이터 조회:", { + tableName, + found: !!state, + dataCount: state?.data.length, + }); + + return state; + } + + /** + * 모든 테이블 데이터 조회 + */ + getAllTableData(): Map { + return new Map(this.state); + } + + /** + * 테이블 데이터 삭제 + * @param tableName 테이블명 + */ + clearTableData(tableName: string) { + this.state.delete(tableName); + this.notifyListeners(); + } + + /** + * 모든 데이터 삭제 + */ + clearAll() { + this.state.clear(); + this.notifyListeners(); + } + + /** + * 변경 리스너 등록 + */ + subscribe(listener: () => void) { + this.listeners.add(listener); + return () => { + this.listeners.delete(listener); + }; + } + + private notifyListeners() { + this.listeners.forEach((listener) => listener()); + } +} + +// 싱글톤 인스턴스 +export const tableDisplayStore = new TableDisplayStore(); +