Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into lhj
; Please enter a commit message to explain why this merge is necessary, ; especially if it merges an updated upstream into a topic branch. ; ; Lines starting with ';' will be ignored, and an empty message aborts ; the commit.
This commit is contained in:
@@ -27,7 +27,6 @@ import { useScreenContextOptional } from "@/contexts/ScreenContext";
|
||||
import { useSplitPanelContext, SplitPanelPosition } from "@/contexts/SplitPanelContext";
|
||||
import { applyMappingRules } from "@/lib/utils/dataMapping";
|
||||
import { apiClient } from "@/lib/api/client";
|
||||
import { useScreenMultiLang } from "@/contexts/ScreenMultiLangContext";
|
||||
|
||||
export interface ButtonPrimaryComponentProps extends ComponentRendererProps {
|
||||
config?: ButtonPrimaryConfig;
|
||||
@@ -108,7 +107,6 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||
const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인
|
||||
const screenContext = useScreenContextOptional(); // 화면 컨텍스트
|
||||
const splitPanelContext = useSplitPanelContext(); // 분할 패널 컨텍스트
|
||||
const { getTranslatedText } = useScreenMultiLang(); // 다국어 컨텍스트
|
||||
// 🆕 ScreenContext에서 splitPanelPosition 가져오기 (중첩 화면에서도 작동)
|
||||
const splitPanelPosition = screenContext?.splitPanelPosition;
|
||||
|
||||
@@ -301,20 +299,6 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||
// 🆕 modalDataStore에서 선택된 데이터 확인 (분할 패널 등에서 저장됨)
|
||||
const [modalStoreData, setModalStoreData] = useState<Record<string, any[]>>({});
|
||||
|
||||
// 🆕 splitPanelContext?.selectedLeftData를 로컬 상태로 추적 (리렌더링 보장)
|
||||
const [trackedSelectedLeftData, setTrackedSelectedLeftData] = useState<Record<string, any> | null>(null);
|
||||
|
||||
// splitPanelContext?.selectedLeftData 변경 감지 및 로컬 상태 동기화
|
||||
useEffect(() => {
|
||||
const newData = splitPanelContext?.selectedLeftData ?? null;
|
||||
setTrackedSelectedLeftData(newData);
|
||||
// console.log("🔄 [ButtonPrimary] selectedLeftData 변경 감지:", {
|
||||
// label: component.label,
|
||||
// hasData: !!newData,
|
||||
// dataKeys: newData ? Object.keys(newData) : [],
|
||||
// });
|
||||
}, [splitPanelContext?.selectedLeftData, component.label]);
|
||||
|
||||
// modalDataStore 상태 구독 (실시간 업데이트)
|
||||
useEffect(() => {
|
||||
const actionConfig = component.componentConfig?.action;
|
||||
@@ -373,8 +357,8 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||
|
||||
// 2. 분할 패널 좌측 선택 데이터 확인
|
||||
if (rowSelectionSource === "auto" || rowSelectionSource === "splitPanelLeft") {
|
||||
// SplitPanelContext에서 확인 (trackedSelectedLeftData 사용으로 리렌더링 보장)
|
||||
if (trackedSelectedLeftData && Object.keys(trackedSelectedLeftData).length > 0) {
|
||||
// SplitPanelContext에서 확인
|
||||
if (splitPanelContext?.selectedLeftData && Object.keys(splitPanelContext.selectedLeftData).length > 0) {
|
||||
if (!hasSelection) {
|
||||
hasSelection = true;
|
||||
selectionCount = 1;
|
||||
@@ -413,7 +397,7 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||
selectionCount,
|
||||
selectionSource,
|
||||
hasSplitPanelContext: !!splitPanelContext,
|
||||
trackedSelectedLeftData: trackedSelectedLeftData,
|
||||
selectedLeftData: splitPanelContext?.selectedLeftData,
|
||||
selectedRowsData: selectedRowsData?.length,
|
||||
selectedRows: selectedRows?.length,
|
||||
flowSelectedData: flowSelectedData?.length,
|
||||
@@ -445,7 +429,7 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||
component.label,
|
||||
selectedRows,
|
||||
selectedRowsData,
|
||||
trackedSelectedLeftData,
|
||||
splitPanelContext?.selectedLeftData,
|
||||
flowSelectedData,
|
||||
splitPanelContext,
|
||||
modalStoreData,
|
||||
@@ -737,99 +721,61 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
if (!screenContext) {
|
||||
toast.error("화면 컨텍스트를 찾을 수 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
let sourceData: any[] = [];
|
||||
// 1. 소스 컴포넌트에서 데이터 가져오기
|
||||
let sourceProvider = screenContext.getDataProvider(dataTransferConfig.sourceComponentId);
|
||||
|
||||
// 1. ScreenContext에서 DataProvider를 통해 데이터 가져오기 시도
|
||||
if (screenContext) {
|
||||
let sourceProvider = screenContext.getDataProvider(dataTransferConfig.sourceComponentId);
|
||||
// 🆕 소스 컴포넌트를 찾을 수 없으면, 현재 화면에서 테이블 리스트 자동 탐색
|
||||
// (조건부 컨테이너의 다른 섹션으로 전환했을 때 이전 컴포넌트 ID가 남아있는 경우 대응)
|
||||
if (!sourceProvider) {
|
||||
console.log(`⚠️ [ButtonPrimary] 지정된 소스 컴포넌트를 찾을 수 없음: ${dataTransferConfig.sourceComponentId}`);
|
||||
console.log(`🔍 [ButtonPrimary] 현재 화면에서 DataProvider 자동 탐색...`);
|
||||
|
||||
const allProviders = screenContext.getAllDataProviders();
|
||||
|
||||
// 테이블 리스트 우선 탐색
|
||||
for (const [id, provider] of allProviders) {
|
||||
if (provider.componentType === "table-list") {
|
||||
sourceProvider = provider;
|
||||
console.log(`✅ [ButtonPrimary] 테이블 리스트 자동 발견: ${id}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 테이블 리스트가 없으면 첫 번째 DataProvider 사용
|
||||
if (!sourceProvider && allProviders.size > 0) {
|
||||
const firstEntry = allProviders.entries().next().value;
|
||||
if (firstEntry) {
|
||||
sourceProvider = firstEntry[1];
|
||||
console.log(
|
||||
`✅ [ButtonPrimary] 첫 번째 DataProvider 사용: ${firstEntry[0]} (${sourceProvider.componentType})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 소스 컴포넌트를 찾을 수 없으면, 현재 화면에서 테이블 리스트 자동 탐색
|
||||
if (!sourceProvider) {
|
||||
console.log(`⚠️ [ButtonPrimary] 지정된 소스 컴포넌트를 찾을 수 없음: ${dataTransferConfig.sourceComponentId}`);
|
||||
console.log(`🔍 [ButtonPrimary] 현재 화면에서 DataProvider 자동 탐색...`);
|
||||
|
||||
const allProviders = screenContext.getAllDataProviders();
|
||||
console.log(`📋 [ButtonPrimary] 등록된 DataProvider 목록:`, Array.from(allProviders.keys()));
|
||||
|
||||
// 테이블 리스트 우선 탐색
|
||||
for (const [id, provider] of allProviders) {
|
||||
if (provider.componentType === "table-list") {
|
||||
sourceProvider = provider;
|
||||
console.log(`✅ [ButtonPrimary] 테이블 리스트 자동 발견: ${id}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 테이블 리스트가 없으면 첫 번째 DataProvider 사용
|
||||
if (!sourceProvider && allProviders.size > 0) {
|
||||
const firstEntry = allProviders.entries().next().value;
|
||||
if (firstEntry) {
|
||||
sourceProvider = firstEntry[1];
|
||||
console.log(
|
||||
`✅ [ButtonPrimary] 첫 번째 DataProvider 사용: ${firstEntry[0]} (${sourceProvider.componentType})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (sourceProvider) {
|
||||
const rawSourceData = sourceProvider.getSelectedData();
|
||||
sourceData = Array.isArray(rawSourceData) ? rawSourceData : rawSourceData ? [rawSourceData] : [];
|
||||
console.log("📦 [ButtonPrimary] ScreenContext에서 소스 데이터 획득:", {
|
||||
rawSourceData,
|
||||
sourceData,
|
||||
count: sourceData.length
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.log("⚠️ [ButtonPrimary] ScreenContext가 없습니다. modalDataStore에서 데이터를 찾습니다.");
|
||||
}
|
||||
|
||||
// 2. ScreenContext에서 데이터를 찾지 못한 경우, modalDataStore에서 fallback 조회
|
||||
if (sourceData.length === 0) {
|
||||
console.log("🔍 [ButtonPrimary] modalDataStore에서 데이터 탐색 시도...");
|
||||
|
||||
try {
|
||||
const { useModalDataStore } = await import("@/stores/modalDataStore");
|
||||
const dataRegistry = useModalDataStore.getState().dataRegistry;
|
||||
|
||||
console.log("📋 [ButtonPrimary] modalDataStore 전체 키:", Object.keys(dataRegistry));
|
||||
|
||||
// sourceTableName이 지정되어 있으면 해당 테이블에서 조회
|
||||
const sourceTableName = dataTransferConfig.sourceTableName || tableName;
|
||||
|
||||
if (sourceTableName && dataRegistry[sourceTableName]) {
|
||||
const modalData = dataRegistry[sourceTableName];
|
||||
sourceData = modalData.map((item: any) => item.originalData || item);
|
||||
console.log(`✅ [ButtonPrimary] modalDataStore에서 데이터 발견 (${sourceTableName}):`, sourceData.length, "건");
|
||||
} else {
|
||||
// 테이블명으로 못 찾으면 첫 번째 데이터 사용
|
||||
const firstKey = Object.keys(dataRegistry)[0];
|
||||
if (firstKey && dataRegistry[firstKey]?.length > 0) {
|
||||
const modalData = dataRegistry[firstKey];
|
||||
sourceData = modalData.map((item: any) => item.originalData || item);
|
||||
console.log(`✅ [ButtonPrimary] modalDataStore 첫 번째 키에서 데이터 발견 (${firstKey}):`, sourceData.length, "건");
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn("⚠️ [ButtonPrimary] modalDataStore 접근 실패:", err);
|
||||
toast.error("데이터를 제공할 수 있는 컴포넌트를 찾을 수 없습니다.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 여전히 데이터가 없으면 에러
|
||||
const rawSourceData = sourceProvider.getSelectedData();
|
||||
|
||||
// 🆕 배열이 아닌 경우 배열로 변환
|
||||
const sourceData = Array.isArray(rawSourceData) ? rawSourceData : rawSourceData ? [rawSourceData] : [];
|
||||
|
||||
console.log("📦 소스 데이터:", { rawSourceData, sourceData, isArray: Array.isArray(rawSourceData) });
|
||||
|
||||
if (!sourceData || sourceData.length === 0) {
|
||||
console.error("❌ [ButtonPrimary] 선택된 데이터를 찾을 수 없습니다.", {
|
||||
hasScreenContext: !!screenContext,
|
||||
sourceComponentId: dataTransferConfig.sourceComponentId,
|
||||
sourceTableName: dataTransferConfig.sourceTableName || tableName,
|
||||
});
|
||||
toast.warning("선택된 데이터가 없습니다. 항목을 먼저 선택해주세요.");
|
||||
toast.warning("선택된 데이터가 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("📦 [ButtonPrimary] 최종 소스 데이터:", { sourceData, count: sourceData.length });
|
||||
|
||||
// 1.5. 추가 데이터 소스 처리 (예: 조건부 컨테이너의 카테고리 값)
|
||||
let additionalData: Record<string, any> = {};
|
||||
|
||||
@@ -1360,10 +1306,7 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||
...userStyle,
|
||||
};
|
||||
|
||||
// 다국어 적용: componentConfig.langKey가 있으면 번역 텍스트 사용
|
||||
const langKey = (component as any).componentConfig?.langKey;
|
||||
const originalButtonText = processedConfig.text !== undefined ? processedConfig.text : component.label || "버튼";
|
||||
const buttonContent = getTranslatedText(langKey, originalButtonText);
|
||||
const buttonContent = processedConfig.text !== undefined ? processedConfig.text : component.label || "버튼";
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -180,8 +180,11 @@ export function ModalRepeaterTableComponent({
|
||||
filterCondition: propFilterCondition,
|
||||
companyCode: propCompanyCode,
|
||||
|
||||
// 🆕 그룹 데이터 (EditModal에서 전달, 같은 그룹의 여러 품목)
|
||||
groupedData,
|
||||
|
||||
...props
|
||||
}: ModalRepeaterTableComponentProps) {
|
||||
}: ModalRepeaterTableComponentProps & { groupedData?: Record<string, any>[] }) {
|
||||
// ✅ config 또는 component.config 또는 개별 prop 우선순위로 병합
|
||||
const componentConfig = {
|
||||
...config,
|
||||
@@ -208,9 +211,16 @@ export function ModalRepeaterTableComponent({
|
||||
// 모달 필터 설정
|
||||
const modalFilters = componentConfig?.modalFilters || [];
|
||||
|
||||
// ✅ value는 formData[columnName] 우선, 없으면 prop 사용
|
||||
// ✅ value는 groupedData 우선, 없으면 formData[columnName], 없으면 prop 사용
|
||||
const columnName = component?.columnName;
|
||||
const externalValue = (columnName && formData?.[columnName]) || componentConfig?.value || propValue || [];
|
||||
|
||||
// 🆕 groupedData가 전달되면 (EditModal에서 그룹 조회 결과) 우선 사용
|
||||
const externalValue = (() => {
|
||||
if (groupedData && groupedData.length > 0) {
|
||||
return groupedData;
|
||||
}
|
||||
return (columnName && formData?.[columnName]) || componentConfig?.value || propValue || [];
|
||||
})();
|
||||
|
||||
// 빈 객체 판단 함수 (수정 모달의 실제 데이터는 유지)
|
||||
const isEmptyRow = (item: any): boolean => {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,6 @@ import { WebType } from "@/types/common";
|
||||
import { tableTypeApi } from "@/lib/api/screen";
|
||||
import { entityJoinApi } from "@/lib/api/entityJoin";
|
||||
import { codeCache } from "@/lib/caching/codeCache";
|
||||
import { getCategoryLabelsByCodes } from "@/lib/api/tableCategoryValue";
|
||||
import { useEntityJoinOptimization } from "@/lib/hooks/useEntityJoinOptimization";
|
||||
import { getFullImageUrl } from "@/lib/api/client";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -67,7 +66,6 @@ import { useAuth } from "@/hooks/useAuth";
|
||||
import { useScreenContextOptional } from "@/contexts/ScreenContext";
|
||||
import { useSplitPanelContext, SplitPanelPosition } from "@/contexts/SplitPanelContext";
|
||||
import type { DataProvidable, DataReceivable, DataReceiverConfig } from "@/types/data-transfer";
|
||||
import { useScreenMultiLang } from "@/contexts/ScreenMultiLangContext";
|
||||
|
||||
// ========================================
|
||||
// 인터페이스
|
||||
@@ -244,11 +242,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||
parentTabsComponentId,
|
||||
companyCode,
|
||||
}) => {
|
||||
// ========================================
|
||||
// 다국어 번역 훅
|
||||
// ========================================
|
||||
const { getTranslatedText } = useScreenMultiLang();
|
||||
|
||||
// ========================================
|
||||
// 설정 및 스타일
|
||||
// ========================================
|
||||
@@ -481,7 +474,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||
}
|
||||
|
||||
// 2. 헤더 필터 적용 (joinColumnMapping 사용 안 함 - 직접 컬럼명 사용)
|
||||
// 🆕 다중 값 지원: 셀 값이 "A,B,C" 형태일 때, 필터에서 "A"를 선택하면 해당 행도 표시
|
||||
if (Object.keys(headerFilters).length > 0) {
|
||||
result = result.filter((row) => {
|
||||
return Object.entries(headerFilters).every(([columnName, values]) => {
|
||||
@@ -491,16 +483,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||
const cellValue = row[columnName] ?? row[columnName.toLowerCase()] ?? row[columnName.toUpperCase()];
|
||||
const cellStr = cellValue !== null && cellValue !== undefined ? String(cellValue) : "";
|
||||
|
||||
// 정확히 일치하는 경우
|
||||
if (values.has(cellStr)) return true;
|
||||
|
||||
// 다중 값인 경우: 콤마로 분리해서 하나라도 포함되면 true
|
||||
if (cellStr.includes(",")) {
|
||||
const cellValues = cellStr.split(",").map(v => v.trim());
|
||||
return cellValues.some(v => values.has(v));
|
||||
}
|
||||
|
||||
return false;
|
||||
return values.has(cellStr);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -871,55 +854,17 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||
};
|
||||
|
||||
// 화면 컨텍스트에 데이터 제공자/수신자로 등록
|
||||
// 🔧 dataProvider와 dataReceiver를 의존성에 포함하지 않고,
|
||||
// 대신 data와 selectedRows가 변경될 때마다 재등록하여 최신 클로저 참조
|
||||
useEffect(() => {
|
||||
if (screenContext && component.id) {
|
||||
// 🔧 매번 새로운 dataProvider를 등록하여 최신 selectedRows 참조
|
||||
const currentDataProvider: DataProvidable = {
|
||||
componentId: component.id,
|
||||
componentType: "table-list",
|
||||
getSelectedData: () => {
|
||||
const selectedData = filteredData.filter((row) => {
|
||||
const rowId = String(row.id || row[tableConfig.selectedTable + "_id"] || "");
|
||||
return selectedRows.has(rowId);
|
||||
});
|
||||
console.log("📊 [TableList] getSelectedData 호출:", {
|
||||
componentId: component.id,
|
||||
selectedRowsSize: selectedRows.size,
|
||||
filteredDataLength: filteredData.length,
|
||||
resultLength: selectedData.length,
|
||||
});
|
||||
return selectedData;
|
||||
},
|
||||
getAllData: () => filteredData,
|
||||
clearSelection: () => {
|
||||
setSelectedRows(new Set());
|
||||
setIsAllSelected(false);
|
||||
},
|
||||
};
|
||||
|
||||
const currentDataReceiver: DataReceivable = {
|
||||
componentId: component.id,
|
||||
componentType: "table",
|
||||
receiveData: dataReceiver.receiveData,
|
||||
getData: () => data,
|
||||
};
|
||||
|
||||
screenContext.registerDataProvider(component.id, currentDataProvider);
|
||||
screenContext.registerDataReceiver(component.id, currentDataReceiver);
|
||||
|
||||
console.log("✅ [TableList] ScreenContext에 등록:", {
|
||||
componentId: component.id,
|
||||
selectedRowsSize: selectedRows.size,
|
||||
});
|
||||
screenContext.registerDataProvider(component.id, dataProvider);
|
||||
screenContext.registerDataReceiver(component.id, dataReceiver);
|
||||
|
||||
return () => {
|
||||
screenContext.unregisterDataProvider(component.id);
|
||||
screenContext.unregisterDataReceiver(component.id);
|
||||
};
|
||||
}
|
||||
}, [screenContext, component.id, data, selectedRows, filteredData, tableConfig.selectedTable]);
|
||||
}, [screenContext, component.id, data, selectedRows]);
|
||||
|
||||
// 분할 패널 컨텍스트에 데이터 수신자로 등록
|
||||
// useSplitPanelPosition 훅으로 위치 가져오기 (중첩된 화면에서도 작동)
|
||||
@@ -1086,16 +1031,14 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||
onGroupSumChange: setGroupSumConfig, // 그룹별 합산 설정
|
||||
// 틀고정 컬럼 관련
|
||||
frozenColumnCount, // 현재 틀고정 컬럼 수
|
||||
onFrozenColumnCountChange: (count: number, updatedColumns?: Array<{ columnName: string; visible: boolean }>) => {
|
||||
onFrozenColumnCountChange: (count: number) => {
|
||||
setFrozenColumnCount(count);
|
||||
// 체크박스 컬럼은 항상 틀고정에 포함
|
||||
const checkboxColumn = (tableConfig.checkbox?.enabled ?? true) ? ["__checkbox__"] : [];
|
||||
// 표시 가능한 컬럼 중 처음 N개를 틀고정 컬럼으로 설정
|
||||
// updatedColumns가 전달되면 그것을 사용, 아니면 columnsToRegister 사용
|
||||
const colsToUse = updatedColumns || columnsToRegister;
|
||||
const visibleCols = colsToUse
|
||||
const visibleCols = columnsToRegister
|
||||
.filter((col) => col.visible !== false)
|
||||
.map((col) => col.columnName || (col as any).field);
|
||||
.map((col) => col.columnName || col.field);
|
||||
const newFrozenColumns = [...checkboxColumn, ...visibleCols.slice(0, count)];
|
||||
setFrozenColumns(newFrozenColumns);
|
||||
},
|
||||
@@ -2106,7 +2049,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||
return row.id || row.uuid || `row-${index}`;
|
||||
};
|
||||
|
||||
const handleRowSelection = (rowKey: string, checked: boolean, rowData?: any) => {
|
||||
const handleRowSelection = (rowKey: string, checked: boolean) => {
|
||||
const newSelectedRows = new Set(selectedRows);
|
||||
if (checked) {
|
||||
newSelectedRows.add(rowKey);
|
||||
@@ -2149,31 +2092,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||
});
|
||||
}
|
||||
|
||||
// 🆕 분할 패널 컨텍스트에 선택된 데이터 저장/해제 (체크박스 선택 시에도 작동)
|
||||
const effectiveSplitPosition = splitPanelPosition || currentSplitPosition;
|
||||
if (splitPanelContext && effectiveSplitPosition === "left" && !splitPanelContext.disableAutoDataTransfer) {
|
||||
if (checked && selectedRowsData.length > 0) {
|
||||
// 선택된 경우: 첫 번째 선택된 데이터 저장 (또는 전달된 rowData)
|
||||
const dataToStore = rowData || selectedRowsData[selectedRowsData.length - 1];
|
||||
splitPanelContext.setSelectedLeftData(dataToStore);
|
||||
console.log("🔗 [TableList] handleRowSelection - 분할 패널 좌측 데이터 저장:", {
|
||||
rowKey,
|
||||
dataToStore,
|
||||
});
|
||||
} else if (!checked && selectedRowsData.length === 0) {
|
||||
// 모든 선택이 해제된 경우: 데이터 초기화
|
||||
splitPanelContext.setSelectedLeftData(null);
|
||||
console.log("🔗 [TableList] handleRowSelection - 분할 패널 좌측 데이터 초기화");
|
||||
} else if (selectedRowsData.length > 0) {
|
||||
// 일부 선택 해제된 경우: 남은 첫 번째 데이터로 업데이트
|
||||
splitPanelContext.setSelectedLeftData(selectedRowsData[0]);
|
||||
console.log("🔗 [TableList] handleRowSelection - 분할 패널 좌측 데이터 업데이트:", {
|
||||
remainingCount: selectedRowsData.length,
|
||||
firstData: selectedRowsData[0],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const allRowsSelected = filteredData.every((row, index) => newSelectedRows.has(getRowKey(row, index)));
|
||||
setIsAllSelected(allRowsSelected && filteredData.length > 0);
|
||||
};
|
||||
@@ -2243,8 +2161,35 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||
const rowKey = getRowKey(row, index);
|
||||
const isCurrentlySelected = selectedRows.has(rowKey);
|
||||
|
||||
// handleRowSelection에서 분할 패널 데이터 처리도 함께 수행됨
|
||||
handleRowSelection(rowKey, !isCurrentlySelected, row);
|
||||
handleRowSelection(rowKey, !isCurrentlySelected);
|
||||
|
||||
// 🆕 분할 패널 컨텍스트에 선택된 데이터 저장 (좌측 화면인 경우)
|
||||
// disableAutoDataTransfer가 true이면 자동 전달 비활성화 (버튼 클릭으로만 전달)
|
||||
// currentSplitPosition을 사용하여 정확한 위치 확인 (splitPanelPosition이 없을 수 있음)
|
||||
const effectiveSplitPosition = splitPanelPosition || currentSplitPosition;
|
||||
|
||||
console.log("🔗 [TableList] 행 클릭 - 분할 패널 위치 확인:", {
|
||||
splitPanelPosition,
|
||||
currentSplitPosition,
|
||||
effectiveSplitPosition,
|
||||
hasSplitPanelContext: !!splitPanelContext,
|
||||
disableAutoDataTransfer: splitPanelContext?.disableAutoDataTransfer,
|
||||
});
|
||||
|
||||
if (splitPanelContext && effectiveSplitPosition === "left" && !splitPanelContext.disableAutoDataTransfer) {
|
||||
if (!isCurrentlySelected) {
|
||||
// 선택된 경우: 데이터 저장
|
||||
splitPanelContext.setSelectedLeftData(row);
|
||||
console.log("🔗 [TableList] 분할 패널 좌측 데이터 저장:", {
|
||||
row,
|
||||
parentDataMapping: splitPanelContext.parentDataMapping,
|
||||
});
|
||||
} else {
|
||||
// 선택 해제된 경우: 데이터 초기화
|
||||
splitPanelContext.setSelectedLeftData(null);
|
||||
console.log("🔗 [TableList] 분할 패널 좌측 데이터 초기화");
|
||||
}
|
||||
}
|
||||
|
||||
console.log("행 클릭:", { row, index, isSelected: !isCurrentlySelected });
|
||||
};
|
||||
@@ -2311,176 +2256,30 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||
// 🆕 편집 모드 진입 placeholder (실제 구현은 visibleColumns 정의 후)
|
||||
const startEditingRef = useRef<() => void>(() => {});
|
||||
|
||||
// 🆕 카테고리 라벨 매핑 (API에서 가져온 것)
|
||||
const [categoryLabelCache, setCategoryLabelCache] = useState<Record<string, string>>({});
|
||||
|
||||
// 🆕 각 컬럼의 고유값 목록 계산 (라벨 포함)
|
||||
// 🆕 각 컬럼의 고유값 목록 계산
|
||||
const columnUniqueValues = useMemo(() => {
|
||||
const result: Record<string, Array<{ value: string; label: string }>> = {};
|
||||
const result: Record<string, string[]> = {};
|
||||
|
||||
if (data.length === 0) return result;
|
||||
|
||||
// 🆕 전체 데이터에서 개별 값 -> 라벨 매핑 테이블 구축 (다중 값 처리용)
|
||||
const globalLabelMap: Record<string, Map<string, string>> = {};
|
||||
|
||||
(tableConfig.columns || []).forEach((column: { columnName: string }) => {
|
||||
if (column.columnName === "__checkbox__") return;
|
||||
|
||||
const mappedColumnName = joinColumnMapping[column.columnName] || column.columnName;
|
||||
// 라벨 컬럼 후보들 (백엔드에서 _name, _label, _value_label 등으로 반환할 수 있음)
|
||||
const labelColumnCandidates = [
|
||||
`${column.columnName}_name`, // 예: division_name
|
||||
`${column.columnName}_label`, // 예: division_label
|
||||
`${column.columnName}_value_label`, // 예: division_value_label
|
||||
];
|
||||
const valuesMap = new Map<string, string>(); // value -> label
|
||||
const singleValueLabelMap = new Map<string, string>(); // 개별 값 -> 라벨 (다중값 처리용)
|
||||
const values = new Set<string>();
|
||||
|
||||
// 1차: 모든 데이터에서 개별 값 -> 라벨 매핑 수집 (단일값 + 다중값 모두)
|
||||
data.forEach((row) => {
|
||||
const val = row[mappedColumnName];
|
||||
if (val !== null && val !== undefined && val !== "") {
|
||||
const valueStr = String(val);
|
||||
|
||||
// 라벨 컬럼에서 라벨 찾기
|
||||
let labelStr = "";
|
||||
for (const labelCol of labelColumnCandidates) {
|
||||
if (row[labelCol] && row[labelCol] !== "") {
|
||||
labelStr = String(row[labelCol]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 단일 값인 경우
|
||||
if (!valueStr.includes(",")) {
|
||||
if (labelStr) {
|
||||
singleValueLabelMap.set(valueStr, labelStr);
|
||||
}
|
||||
} else {
|
||||
// 다중 값인 경우: 값과 라벨을 각각 분리해서 매핑
|
||||
const individualValues = valueStr.split(",").map(v => v.trim());
|
||||
const individualLabels = labelStr ? labelStr.split(",").map(l => l.trim()) : [];
|
||||
|
||||
// 값과 라벨 개수가 같으면 1:1 매핑
|
||||
if (individualValues.length === individualLabels.length) {
|
||||
individualValues.forEach((v, idx) => {
|
||||
if (individualLabels[idx] && !singleValueLabelMap.has(v)) {
|
||||
singleValueLabelMap.set(v, individualLabels[idx]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
values.add(String(val));
|
||||
}
|
||||
});
|
||||
|
||||
// 2차: 모든 값 처리 (다중 값 포함) - 필터 목록용
|
||||
data.forEach((row) => {
|
||||
const val = row[mappedColumnName];
|
||||
if (val !== null && val !== undefined && val !== "") {
|
||||
const valueStr = String(val);
|
||||
|
||||
// 콤마로 구분된 다중 값인지 확인
|
||||
if (valueStr.includes(",")) {
|
||||
// 다중 값: 각각 분리해서 개별 라벨 찾기
|
||||
const individualValues = valueStr.split(",").map(v => v.trim());
|
||||
// 🆕 singleValueLabelMap → categoryLabelCache 순으로 라벨 찾기
|
||||
const individualLabels = individualValues.map(v =>
|
||||
singleValueLabelMap.get(v) || categoryLabelCache[v] || v
|
||||
);
|
||||
valuesMap.set(valueStr, individualLabels.join(", "));
|
||||
} else {
|
||||
// 단일 값: 매핑에서 찾거나 캐시에서 찾거나 원본 사용
|
||||
const label = singleValueLabelMap.get(valueStr) || categoryLabelCache[valueStr] || valueStr;
|
||||
valuesMap.set(valueStr, label);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
globalLabelMap[column.columnName] = singleValueLabelMap;
|
||||
|
||||
// value-label 쌍으로 저장하고 라벨 기준 정렬
|
||||
result[column.columnName] = Array.from(valuesMap.entries())
|
||||
.map(([value, label]) => ({ value, label }))
|
||||
.sort((a, b) => a.label.localeCompare(b.label));
|
||||
result[column.columnName] = Array.from(values).sort();
|
||||
});
|
||||
|
||||
return result;
|
||||
}, [data, tableConfig.columns, joinColumnMapping, categoryLabelCache]);
|
||||
|
||||
// 🆕 라벨을 못 찾은 CATEGORY_ 코드들을 API로 조회
|
||||
useEffect(() => {
|
||||
const unlabeledCodes = new Set<string>();
|
||||
|
||||
// columnUniqueValues에서 라벨이 코드 그대로인 항목 찾기
|
||||
Object.values(columnUniqueValues).forEach(items => {
|
||||
items.forEach(item => {
|
||||
// 라벨에 CATEGORY_가 포함되어 있으면 라벨을 못 찾은 것
|
||||
if (item.label.includes("CATEGORY_")) {
|
||||
// 콤마로 분리해서 개별 코드 추출
|
||||
const codes = item.label.split(",").map(c => c.trim());
|
||||
codes.forEach(code => {
|
||||
if (code.startsWith("CATEGORY_") && !categoryLabelCache[code]) {
|
||||
unlabeledCodes.add(code);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (unlabeledCodes.size === 0) return;
|
||||
|
||||
// API로 라벨 조회
|
||||
const fetchLabels = async () => {
|
||||
try {
|
||||
const response = await getCategoryLabelsByCodes(Array.from(unlabeledCodes));
|
||||
if (response.success && response.data) {
|
||||
setCategoryLabelCache(prev => ({ ...prev, ...response.data }));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("카테고리 라벨 조회 실패:", error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchLabels();
|
||||
}, [columnUniqueValues, categoryLabelCache]);
|
||||
|
||||
// 🆕 데이터에서 CATEGORY_ 코드를 찾아 라벨 미리 로드 (테이블 셀 렌더링용)
|
||||
useEffect(() => {
|
||||
if (data.length === 0) return;
|
||||
|
||||
const categoryCodesToFetch = new Set<string>();
|
||||
|
||||
// 모든 데이터 행에서 CATEGORY_ 코드 수집
|
||||
data.forEach((row) => {
|
||||
Object.entries(row).forEach(([key, value]) => {
|
||||
if (value && typeof value === "string") {
|
||||
// 콤마로 구분된 다중 값도 처리
|
||||
const codes = value.split(",").map((v) => v.trim());
|
||||
codes.forEach((code) => {
|
||||
if (code.startsWith("CATEGORY_") && !categoryLabelCache[code]) {
|
||||
categoryCodesToFetch.add(code);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (categoryCodesToFetch.size === 0) return;
|
||||
|
||||
// API로 라벨 조회
|
||||
const fetchLabels = async () => {
|
||||
try {
|
||||
const response = await getCategoryLabelsByCodes(Array.from(categoryCodesToFetch));
|
||||
if (response.success && response.data && Object.keys(response.data).length > 0) {
|
||||
setCategoryLabelCache((prev) => ({ ...prev, ...response.data }));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("CATEGORY_ 라벨 조회 실패:", error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchLabels();
|
||||
}, [data, categoryLabelCache]);
|
||||
}, [data, tableConfig.columns, joinColumnMapping]);
|
||||
|
||||
// 🆕 헤더 필터 토글
|
||||
const toggleHeaderFilter = useCallback((columnName: string, value: string) => {
|
||||
@@ -4125,7 +3924,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||
if (enterRow) {
|
||||
const rowKey = getRowKey(enterRow, rowIndex);
|
||||
const isCurrentlySelected = selectedRows.has(rowKey);
|
||||
handleRowSelection(rowKey, !isCurrentlySelected, enterRow);
|
||||
handleRowSelection(rowKey, !isCurrentlySelected);
|
||||
}
|
||||
break;
|
||||
case " ": // Space
|
||||
@@ -4135,7 +3934,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||
if (spaceRow) {
|
||||
const currentRowKey = getRowKey(spaceRow, rowIndex);
|
||||
const isChecked = selectedRows.has(currentRowKey);
|
||||
handleRowSelection(currentRowKey, !isChecked, spaceRow);
|
||||
handleRowSelection(currentRowKey, !isChecked);
|
||||
}
|
||||
break;
|
||||
case "F2":
|
||||
@@ -4349,7 +4148,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||
return (
|
||||
<Checkbox
|
||||
checked={isChecked}
|
||||
onCheckedChange={(checked) => handleRowSelection(rowKey, checked as boolean, row)}
|
||||
onCheckedChange={(checked) => handleRowSelection(rowKey, checked as boolean)}
|
||||
aria-label={`행 ${index + 1} 선택`}
|
||||
/>
|
||||
);
|
||||
@@ -4638,36 +4437,10 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||
case "boolean":
|
||||
return value ? "예" : "아니오";
|
||||
default:
|
||||
// 🆕 CATEGORY_ 코드 자동 변환 (inputType이 category가 아니어도)
|
||||
const strValue = String(value);
|
||||
if (strValue.startsWith("CATEGORY_")) {
|
||||
// rowData에서 _label 필드 찾기
|
||||
if (rowData) {
|
||||
const labelFieldCandidates = [
|
||||
`${column.columnName}_label`,
|
||||
`${column.columnName}_name`,
|
||||
`${column.columnName}_value_label`,
|
||||
];
|
||||
for (const labelField of labelFieldCandidates) {
|
||||
if (rowData[labelField] && rowData[labelField] !== "") {
|
||||
return String(rowData[labelField]);
|
||||
}
|
||||
}
|
||||
}
|
||||
// categoryMappings에서 찾기
|
||||
const mapping = categoryMappings[column.columnName];
|
||||
if (mapping && mapping[strValue]) {
|
||||
return mapping[strValue].label;
|
||||
}
|
||||
// categoryLabelCache에서 찾기 (필터용 캐시)
|
||||
if (categoryLabelCache[strValue]) {
|
||||
return categoryLabelCache[strValue];
|
||||
}
|
||||
}
|
||||
return strValue;
|
||||
return String(value);
|
||||
}
|
||||
},
|
||||
[columnMeta, joinedColumnMeta, optimizedConvertCode, categoryMappings, categoryLabelCache],
|
||||
[columnMeta, joinedColumnMeta, optimizedConvertCode, categoryMappings],
|
||||
);
|
||||
|
||||
// ========================================
|
||||
@@ -4806,22 +4579,9 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||
});
|
||||
setColumnWidths(newWidths);
|
||||
|
||||
// 틀고정 컬럼 업데이트 (보이는 컬럼 기준으로 처음 N개를 틀고정)
|
||||
// 기존 frozen 개수를 유지하면서, 숨겨진 컬럼을 제외한 보이는 컬럼 중 처음 N개를 틀고정
|
||||
const checkboxColumn = (tableConfig.checkbox?.enabled ?? true) ? ["__checkbox__"] : [];
|
||||
const visibleCols = config.columns
|
||||
.filter((col) => col.visible && col.columnName !== "__checkbox__")
|
||||
.map((col) => col.columnName);
|
||||
|
||||
// 현재 설정된 frozen 컬럼 개수 (체크박스 제외)
|
||||
const currentFrozenCount = config.columns.filter(
|
||||
(col) => col.frozen && col.columnName !== "__checkbox__"
|
||||
).length;
|
||||
|
||||
// 보이는 컬럼 중 처음 currentFrozenCount개를 틀고정으로 설정
|
||||
const newFrozenColumns = [...checkboxColumn, ...visibleCols.slice(0, currentFrozenCount)];
|
||||
// 틀고정 컬럼 업데이트
|
||||
const newFrozenColumns = config.columns.filter((col) => col.frozen).map((col) => col.columnName);
|
||||
setFrozenColumns(newFrozenColumns);
|
||||
setFrozenColumnCount(currentFrozenCount);
|
||||
|
||||
// 그리드선 표시 업데이트
|
||||
setShowGridLines(config.showGridLines);
|
||||
@@ -5865,10 +5625,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||
rowSpan={2}
|
||||
className="border-primary/10 border-r px-2 py-1 text-center text-xs font-semibold sm:px-4 sm:text-sm"
|
||||
>
|
||||
{/* langKey가 있으면 다국어 번역 사용 */}
|
||||
{(column as any).langKey
|
||||
? getTranslatedText((column as any).langKey, columnLabels[column.columnName] || column.columnName)
|
||||
: columnLabels[column.columnName] || column.columnName}
|
||||
{columnLabels[column.columnName] || column.columnName}
|
||||
</th>
|
||||
);
|
||||
}
|
||||
@@ -5887,18 +5644,13 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||
{visibleColumns.map((column, columnIndex) => {
|
||||
const columnWidth = columnWidths[column.columnName];
|
||||
const isFrozen = frozenColumns.includes(column.columnName);
|
||||
|
||||
// 틀고정된 컬럼의 left 위치 계산 (보이는 컬럼 기준으로 계산)
|
||||
// 숨겨진 컬럼은 제외하고 보이는 틀고정 컬럼만 포함
|
||||
const visibleFrozenColumns = visibleColumns
|
||||
.filter(col => frozenColumns.includes(col.columnName))
|
||||
.map(col => col.columnName);
|
||||
const frozenIndex = visibleFrozenColumns.indexOf(column.columnName);
|
||||
|
||||
const frozenIndex = frozenColumns.indexOf(column.columnName);
|
||||
|
||||
// 틀고정된 컬럼의 left 위치 계산
|
||||
let leftPosition = 0;
|
||||
if (isFrozen && frozenIndex > 0) {
|
||||
for (let i = 0; i < frozenIndex; i++) {
|
||||
const frozenCol = visibleFrozenColumns[i];
|
||||
const frozenCol = frozenColumns[i];
|
||||
// 체크박스 컬럼은 48px 고정
|
||||
const frozenColWidth = frozenCol === "__checkbox__" ? 48 : columnWidths[frozenCol] || 150;
|
||||
leftPosition += frozenColWidth;
|
||||
@@ -5964,12 +5716,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||
<Lock className="text-muted-foreground h-3 w-3" />
|
||||
</span>
|
||||
)}
|
||||
<span>
|
||||
{/* langKey가 있으면 다국어 번역 사용 */}
|
||||
{(column as any).langKey
|
||||
? getTranslatedText((column as any).langKey, columnLabels[column.columnName] || column.displayName || column.columnName)
|
||||
: columnLabels[column.columnName] || column.displayName}
|
||||
</span>
|
||||
<span>{columnLabels[column.columnName] || column.displayName}</span>
|
||||
{column.sortable !== false && sortColumn === column.columnName && (
|
||||
<span>{sortDirection === "asc" ? "↑" : "↓"}</span>
|
||||
)}
|
||||
@@ -6017,16 +5764,16 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||
)}
|
||||
</div>
|
||||
<div className="max-h-48 space-y-1 overflow-y-auto">
|
||||
{columnUniqueValues[column.columnName]?.slice(0, 50).map((item) => {
|
||||
const isSelected = headerFilters[column.columnName]?.has(item.value);
|
||||
{columnUniqueValues[column.columnName]?.slice(0, 50).map((val) => {
|
||||
const isSelected = headerFilters[column.columnName]?.has(val);
|
||||
return (
|
||||
<div
|
||||
key={item.value}
|
||||
key={val}
|
||||
className={cn(
|
||||
"hover:bg-muted flex cursor-pointer items-center gap-2 rounded px-2 py-1 text-xs",
|
||||
isSelected && "bg-primary/10",
|
||||
)}
|
||||
onClick={() => toggleHeaderFilter(column.columnName, item.value)}
|
||||
onClick={() => toggleHeaderFilter(column.columnName, val)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
@@ -6036,7 +5783,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||
>
|
||||
{isSelected && <Check className="text-primary-foreground h-3 w-3" />}
|
||||
</div>
|
||||
<span className="truncate">{item.label || "(빈 값)"}</span>
|
||||
<span className="truncate">{val || "(빈 값)"}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@@ -6209,17 +5956,13 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||
const isNumeric = inputType === "number" || inputType === "decimal";
|
||||
|
||||
const isFrozen = frozenColumns.includes(column.columnName);
|
||||
|
||||
// 틀고정된 컬럼의 left 위치 계산 (보이는 컬럼 기준으로 계산)
|
||||
const visibleFrozenColumns = visibleColumns
|
||||
.filter(col => frozenColumns.includes(col.columnName))
|
||||
.map(col => col.columnName);
|
||||
const frozenIndex = visibleFrozenColumns.indexOf(column.columnName);
|
||||
const frozenIndex = frozenColumns.indexOf(column.columnName);
|
||||
|
||||
// 틀고정된 컬럼의 left 위치 계산
|
||||
let leftPosition = 0;
|
||||
if (isFrozen && frozenIndex > 0) {
|
||||
for (let i = 0; i < frozenIndex; i++) {
|
||||
const frozenCol = visibleFrozenColumns[i];
|
||||
const frozenCol = frozenColumns[i];
|
||||
// 체크박스 컬럼은 48px 고정
|
||||
const frozenColWidth =
|
||||
frozenCol === "__checkbox__" ? 48 : columnWidths[frozenCol] || 150;
|
||||
@@ -6366,12 +6109,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||
const isNumeric = inputType === "number" || inputType === "decimal";
|
||||
|
||||
const isFrozen = frozenColumns.includes(column.columnName);
|
||||
|
||||
// 틀고정된 컬럼의 left 위치 계산 (보이는 컬럼 기준으로 계산)
|
||||
const visibleFrozenColumns = visibleColumns
|
||||
.filter(col => frozenColumns.includes(col.columnName))
|
||||
.map(col => col.columnName);
|
||||
const frozenIndex = visibleFrozenColumns.indexOf(column.columnName);
|
||||
const frozenIndex = frozenColumns.indexOf(column.columnName);
|
||||
|
||||
// 셀 포커스 상태
|
||||
const isCellFocused = focusedCell?.rowIndex === index && focusedCell?.colIndex === colIndex;
|
||||
@@ -6385,10 +6123,11 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||
// 🆕 검색 하이라이트 여부
|
||||
const isSearchHighlighted = searchHighlights.has(`${index}-${colIndex}`);
|
||||
|
||||
// 틀고정된 컬럼의 left 위치 계산
|
||||
let leftPosition = 0;
|
||||
if (isFrozen && frozenIndex > 0) {
|
||||
for (let i = 0; i < frozenIndex; i++) {
|
||||
const frozenCol = visibleFrozenColumns[i];
|
||||
const frozenCol = frozenColumns[i];
|
||||
// 체크박스 컬럼은 48px 고정
|
||||
const frozenColWidth =
|
||||
frozenCol === "__checkbox__" ? 48 : columnWidths[frozenCol] || 150;
|
||||
@@ -6548,17 +6287,13 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||
const summary = summaryData[column.columnName];
|
||||
const columnWidth = columnWidths[column.columnName];
|
||||
const isFrozen = frozenColumns.includes(column.columnName);
|
||||
|
||||
// 틀고정된 컬럼의 left 위치 계산 (보이는 컬럼 기준으로 계산)
|
||||
const visibleFrozenColumns = visibleColumns
|
||||
.filter(col => frozenColumns.includes(col.columnName))
|
||||
.map(col => col.columnName);
|
||||
const frozenIndex = visibleFrozenColumns.indexOf(column.columnName);
|
||||
const frozenIndex = frozenColumns.indexOf(column.columnName);
|
||||
|
||||
// 틀고정된 컬럼의 left 위치 계산
|
||||
let leftPosition = 0;
|
||||
if (isFrozen && frozenIndex > 0) {
|
||||
for (let i = 0; i < frozenIndex; i++) {
|
||||
const frozenCol = visibleFrozenColumns[i];
|
||||
const frozenCol = frozenColumns[i];
|
||||
// 체크박스 컬럼은 48px 고정
|
||||
const frozenColWidth = frozenCol === "__checkbox__" ? 48 : columnWidths[frozenCol] || 150;
|
||||
leftPosition += frozenColWidth;
|
||||
|
||||
Reference in New Issue
Block a user