[agent-pipeline] pipe-20260309055714-23ry round-3

This commit is contained in:
DDD1542
2026-03-09 17:18:45 +09:00
parent 790592ec76
commit e4de414dfb
12 changed files with 311 additions and 273 deletions

View File

@@ -308,6 +308,253 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
[codeOptions],
);
// 데이터 로드 함수 (useEffect보다 먼저 선언해야 함 - TS2448 방지)
const loadData = useCallback(
async (page: number = 1, searchParams: Record<string, any> = {}) => {
if (!component.tableName) return;
// 프리뷰 모드에서는 샘플 데이터만 표시
if (isPreviewMode) {
const sampleData = Array.from({ length: 3 }, (_, i) => {
const sample: Record<string, any> = { id: i + 1 };
component.columns.forEach((col) => {
if (col.widgetType === "number") {
sample[col.columnName] = Math.floor(Math.random() * 1000);
} else if (col.widgetType === "boolean") {
sample[col.columnName] = i % 2 === 0 ? "Y" : "N";
} else {
sample[col.columnName] = `샘플 ${col.label} ${i + 1}`;
}
});
return sample;
});
setData(sampleData);
setTotal(3);
setTotalPages(1);
setCurrentPage(1);
setLoading(false);
return;
}
setLoading(true);
try {
// 🆕 연결 필터 값 가져오기 (분할 패널 내부일 때)
let linkedFilterValues: Record<string, any> = {};
let hasLinkedFiltersConfigured = false; // 연결 필터가 설정되어 있는지 여부
let hasSelectedLeftData = false; // 좌측에서 데이터가 선택되었는지 여부
if (splitPanelContext) {
// 연결 필터 설정 여부 확인 (현재 테이블에 해당하는 필터가 있는지)
const linkedFiltersConfig = splitPanelContext.linkedFilters || [];
hasLinkedFiltersConfigured = linkedFiltersConfig.some(
(filter) =>
filter.targetColumn?.startsWith(component.tableName + ".") || filter.targetColumn === component.tableName,
);
// 좌측 데이터 선택 여부 확인
hasSelectedLeftData =
splitPanelContext.selectedLeftData && Object.keys(splitPanelContext.selectedLeftData).length > 0;
linkedFilterValues = splitPanelContext.getLinkedFilterValues();
// 현재 테이블에 해당하는 필터만 추출 (테이블명.컬럼명 형식에서)
const tableSpecificFilters: Record<string, any> = {};
for (const [key, value] of Object.entries(linkedFilterValues)) {
// key가 "테이블명.컬럼명" 형식인 경우
if (key.includes(".")) {
const [tableName, columnName] = key.split(".");
if (tableName === component.tableName) {
tableSpecificFilters[columnName] = value;
hasLinkedFiltersConfigured = true; // 이 테이블에 대한 필터가 있음
}
} else {
// 테이블명 없이 컬럼명만 있는 경우 그대로 사용
tableSpecificFilters[key] = value;
}
}
linkedFilterValues = tableSpecificFilters;
}
// 🆕 연결 필터가 설정되어 있지만 좌측에서 데이터가 선택되지 않은 경우
// → 빈 데이터 표시 (모든 데이터를 보여주지 않음)
if (hasLinkedFiltersConfigured && !hasSelectedLeftData) {
console.log("⚠️ [InteractiveDataTable] 연결 필터 설정됨 but 좌측 데이터 미선택 → 빈 데이터 표시");
setData([]);
setTotal(0);
setTotalPages(0);
setCurrentPage(1);
setLoading(false);
return;
}
// 🆕 RelatedDataButtons 필터 적용
const relatedButtonFilterValues: Record<string, any> = {};
if (relatedButtonFilter) {
relatedButtonFilterValues[relatedButtonFilter.filterColumn] = relatedButtonFilter.filterValue;
}
// 검색 파라미터와 연결 필터 병합
const mergedSearchParams = {
...searchParams,
...linkedFilterValues,
...relatedButtonFilterValues, // 🆕 RelatedDataButtons 필터 추가
};
const currentPageSize = component.pagination?.pageSize || 10;
console.log("🔍 데이터 조회 시작:", {
tableName: component.tableName,
page,
pageSize: currentPageSize,
linkedFilterValues,
relatedButtonFilterValues,
mergedSearchParams,
});
const result = await tableTypeApi.getTableData(component.tableName, {
page,
size: currentPageSize,
search: mergedSearchParams,
autoFilter: component.autoFilter, // 🆕 자동 필터 설정 전달
});
console.log("✅ 데이터 조회 완료:", {
tableName: component.tableName,
dataLength: result.data.length,
total: result.total,
page: result.page,
});
setData(result.data);
setTotal(result.total);
setTotalPages(result.totalPages);
setCurrentPage(result.page);
// 카테고리 코드 패턴(CATEGORY_*) 검출 및 라벨 조회
const detectAndLoadCategoryLabels = async () => {
const categoryCodes = new Set<string>();
result.data.forEach((row: Record<string, any>) => {
Object.values(row).forEach((value) => {
if (typeof value === "string" && value.startsWith("CATEGORY_")) {
categoryCodes.add(value);
}
});
});
console.log("🏷️ [InteractiveDataTable] 감지된 카테고리 코드:", Array.from(categoryCodes));
// 새로운 카테고리 코드만 필터링 (이미 캐시된 것 제외)
const newCodes = Array.from(categoryCodes);
if (newCodes.length > 0) {
try {
console.log("🏷️ [InteractiveDataTable] 카테고리 라벨 API 호출:", newCodes);
const response = await apiClient.post("/table-categories/labels-by-codes", { valueCodes: newCodes });
console.log("🏷️ [InteractiveDataTable] 카테고리 라벨 API 응답:", response.data);
if (response.data.success && response.data.data) {
setCategoryCodeLabels((prev) => {
const newLabels = {
...prev,
...response.data.data,
};
console.log("🏷️ [InteractiveDataTable] 카테고리 라벨 캐시 업데이트:", newLabels);
return newLabels;
});
}
} catch (error) {
console.error("카테고리 라벨 조회 실패:", error);
}
}
};
detectAndLoadCategoryLabels();
// 각 행의 파일 상태 확인 (전체 행 + 가상 파일 컬럼별)
const fileStatusPromises = result.data.map(async (rowData: Record<string, any>) => {
const primaryKeyField = Object.keys(rowData)[0];
const recordId = rowData[primaryKeyField];
if (!recordId) return { rowKey: recordId, statuses: {} };
try {
const fileResponse = await getLinkedFiles(component.tableName, recordId);
const allFiles = fileResponse.files || [];
// 전체 행에 대한 파일 상태
const rowStatus = {
hasFiles: allFiles.length > 0,
fileCount: allFiles.length,
};
// 가상 파일 컬럼별 파일 상태
const columnStatuses: Record<string, { hasFiles: boolean; fileCount: number }> = {};
// 가상 파일 컬럼 찾기
const virtualFileColumns = component.columns.filter((col) => col.isVirtualFileColumn);
virtualFileColumns.forEach((column) => {
// 해당 컬럼의 파일만 필터링 (targetObjid로 수정)
let columnFiles = allFiles.filter((file: any) => file.targetObjid?.endsWith(`:${column.columnName}`));
// fallback: 컬럼명으로 찾지 못한 경우 모든 파일 컬럼 파일 포함
if (columnFiles.length === 0) {
columnFiles = allFiles.filter((file: any) =>
file.targetObjid?.startsWith(`${component.tableName}:${recordId}:file_column_`),
);
}
const columnKey = `${recordId}_${column.columnName}`;
columnStatuses[columnKey] = {
hasFiles: columnFiles.length > 0,
fileCount: columnFiles.length,
};
});
return {
rowKey: recordId,
statuses: {
[recordId]: rowStatus, // 전체 행 상태
...columnStatuses, // 컬럼별 상태
},
};
} catch {
// 에러 시 기본값
const defaultStatuses: Record<string, { hasFiles: boolean; fileCount: number }> = {
[recordId]: { hasFiles: false, fileCount: 0 },
};
// 가상 파일 컬럼에 대해서도 기본값 설정
const virtualFileColumns = component.columns.filter((col) => col.isVirtualFileColumn);
virtualFileColumns.forEach((column) => {
const columnKey = `${recordId}_${column.columnName}`;
defaultStatuses[columnKey] = { hasFiles: false, fileCount: 0 };
});
return { rowKey: recordId, statuses: defaultStatuses };
}
});
// 파일 상태 업데이트
Promise.all(fileStatusPromises).then((results) => {
const statusMap: Record<string, { hasFiles: boolean; fileCount: number }> = {};
results.forEach((result) => {
Object.assign(statusMap, result.statuses);
});
setFileStatusMap(statusMap);
});
} catch (error) {
// console.error("❌ 테이블 데이터 조회 실패:", error);
setData([]);
setTotal(0);
setTotalPages(1);
} finally {
setLoading(false);
}
},
[component.tableName, component.pagination?.pageSize, component.autoFilter, splitPanelContext?.selectedLeftData, relatedButtonFilter],
);
// 🆕 전역 테이블 새로고침 이벤트 리스너
useEffect(() => {
const handleRefreshTable = () => {
@@ -695,251 +942,6 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
}
}, [visibleColumns]);
// 데이터 로드 함수
const loadData = useCallback(
async (page: number = 1, searchParams: Record<string, any> = {}) => {
if (!component.tableName) return;
// 프리뷰 모드에서는 샘플 데이터만 표시
if (isPreviewMode) {
const sampleData = Array.from({ length: 3 }, (_, i) => {
const sample: Record<string, any> = { id: i + 1 };
component.columns.forEach((col) => {
if (col.widgetType === "number") {
sample[col.columnName] = Math.floor(Math.random() * 1000);
} else if (col.widgetType === "boolean") {
sample[col.columnName] = i % 2 === 0 ? "Y" : "N";
} else {
sample[col.columnName] = `샘플 ${col.label} ${i + 1}`;
}
});
return sample;
});
setData(sampleData);
setTotal(3);
setTotalPages(1);
setCurrentPage(1);
setLoading(false);
return;
}
setLoading(true);
try {
// 🆕 연결 필터 값 가져오기 (분할 패널 내부일 때)
let linkedFilterValues: Record<string, any> = {};
let hasLinkedFiltersConfigured = false; // 연결 필터가 설정되어 있는지 여부
let hasSelectedLeftData = false; // 좌측에서 데이터가 선택되었는지 여부
if (splitPanelContext) {
// 연결 필터 설정 여부 확인 (현재 테이블에 해당하는 필터가 있는지)
const linkedFiltersConfig = splitPanelContext.linkedFilters || [];
hasLinkedFiltersConfigured = linkedFiltersConfig.some(
(filter) =>
filter.targetColumn?.startsWith(component.tableName + ".") || filter.targetColumn === component.tableName,
);
// 좌측 데이터 선택 여부 확인
hasSelectedLeftData =
splitPanelContext.selectedLeftData && Object.keys(splitPanelContext.selectedLeftData).length > 0;
linkedFilterValues = splitPanelContext.getLinkedFilterValues();
// 현재 테이블에 해당하는 필터만 추출 (테이블명.컬럼명 형식에서)
const tableSpecificFilters: Record<string, any> = {};
for (const [key, value] of Object.entries(linkedFilterValues)) {
// key가 "테이블명.컬럼명" 형식인 경우
if (key.includes(".")) {
const [tableName, columnName] = key.split(".");
if (tableName === component.tableName) {
tableSpecificFilters[columnName] = value;
hasLinkedFiltersConfigured = true; // 이 테이블에 대한 필터가 있음
}
} else {
// 테이블명 없이 컬럼명만 있는 경우 그대로 사용
tableSpecificFilters[key] = value;
}
}
linkedFilterValues = tableSpecificFilters;
}
// 🆕 연결 필터가 설정되어 있지만 좌측에서 데이터가 선택되지 않은 경우
// → 빈 데이터 표시 (모든 데이터를 보여주지 않음)
if (hasLinkedFiltersConfigured && !hasSelectedLeftData) {
console.log("⚠️ [InteractiveDataTable] 연결 필터 설정됨 but 좌측 데이터 미선택 → 빈 데이터 표시");
setData([]);
setTotal(0);
setTotalPages(0);
setCurrentPage(1);
setLoading(false);
return;
}
// 🆕 RelatedDataButtons 필터 적용
const relatedButtonFilterValues: Record<string, any> = {};
if (relatedButtonFilter) {
relatedButtonFilterValues[relatedButtonFilter.filterColumn] = relatedButtonFilter.filterValue;
}
// 검색 파라미터와 연결 필터 병합
const mergedSearchParams = {
...searchParams,
...linkedFilterValues,
...relatedButtonFilterValues, // 🆕 RelatedDataButtons 필터 추가
};
console.log("🔍 데이터 조회 시작:", {
tableName: component.tableName,
page,
pageSize,
linkedFilterValues,
relatedButtonFilterValues,
mergedSearchParams,
});
const result = await tableTypeApi.getTableData(component.tableName, {
page,
size: pageSize,
search: mergedSearchParams,
autoFilter: component.autoFilter, // 🆕 자동 필터 설정 전달
});
console.log("✅ 데이터 조회 완료:", {
tableName: component.tableName,
dataLength: result.data.length,
total: result.total,
page: result.page,
});
setData(result.data);
setTotal(result.total);
setTotalPages(result.totalPages);
setCurrentPage(result.page);
// 카테고리 코드 패턴(CATEGORY_*) 검출 및 라벨 조회
const detectAndLoadCategoryLabels = async () => {
const categoryCodes = new Set<string>();
result.data.forEach((row: Record<string, any>) => {
Object.values(row).forEach((value) => {
if (typeof value === "string" && value.startsWith("CATEGORY_")) {
categoryCodes.add(value);
}
});
});
console.log("🏷️ [InteractiveDataTable] 감지된 카테고리 코드:", Array.from(categoryCodes));
// 새로운 카테고리 코드만 필터링 (이미 캐시된 것 제외)
const newCodes = Array.from(categoryCodes);
if (newCodes.length > 0) {
try {
console.log("🏷️ [InteractiveDataTable] 카테고리 라벨 API 호출:", newCodes);
const response = await apiClient.post("/table-categories/labels-by-codes", { valueCodes: newCodes });
console.log("🏷️ [InteractiveDataTable] 카테고리 라벨 API 응답:", response.data);
if (response.data.success && response.data.data) {
setCategoryCodeLabels((prev) => {
const newLabels = {
...prev,
...response.data.data,
};
console.log("🏷️ [InteractiveDataTable] 카테고리 라벨 캐시 업데이트:", newLabels);
return newLabels;
});
}
} catch (error) {
console.error("카테고리 라벨 조회 실패:", error);
}
}
};
detectAndLoadCategoryLabels();
// 각 행의 파일 상태 확인 (전체 행 + 가상 파일 컬럼별)
const fileStatusPromises = result.data.map(async (rowData: Record<string, any>) => {
const primaryKeyField = Object.keys(rowData)[0];
const recordId = rowData[primaryKeyField];
if (!recordId) return { rowKey: recordId, statuses: {} };
try {
const fileResponse = await getLinkedFiles(component.tableName, recordId);
const allFiles = fileResponse.files || [];
// 전체 행에 대한 파일 상태
const rowStatus = {
hasFiles: allFiles.length > 0,
fileCount: allFiles.length,
};
// 가상 파일 컬럼별 파일 상태
const columnStatuses: Record<string, { hasFiles: boolean; fileCount: number }> = {};
// 가상 파일 컬럼 찾기
const virtualFileColumns = component.columns.filter((col) => col.isVirtualFileColumn);
virtualFileColumns.forEach((column) => {
// 해당 컬럼의 파일만 필터링 (targetObjid로 수정)
let columnFiles = allFiles.filter((file: any) => file.targetObjid?.endsWith(`:${column.columnName}`));
// fallback: 컬럼명으로 찾지 못한 경우 모든 파일 컬럼 파일 포함
if (columnFiles.length === 0) {
columnFiles = allFiles.filter((file: any) =>
file.targetObjid?.startsWith(`${component.tableName}:${recordId}:file_column_`),
);
}
const columnKey = `${recordId}_${column.columnName}`;
columnStatuses[columnKey] = {
hasFiles: columnFiles.length > 0,
fileCount: columnFiles.length,
};
});
return {
rowKey: recordId,
statuses: {
[recordId]: rowStatus, // 전체 행 상태
...columnStatuses, // 컬럼별 상태
},
};
} catch {
// 에러 시 기본값
const defaultStatuses: Record<string, { hasFiles: boolean; fileCount: number }> = {
[recordId]: { hasFiles: false, fileCount: 0 },
};
// 가상 파일 컬럼에 대해서도 기본값 설정
const virtualFileColumns = component.columns.filter((col) => col.isVirtualFileColumn);
virtualFileColumns.forEach((column) => {
const columnKey = `${recordId}_${column.columnName}`;
defaultStatuses[columnKey] = { hasFiles: false, fileCount: 0 };
});
return { rowKey: recordId, statuses: defaultStatuses };
}
});
// 파일 상태 업데이트
Promise.all(fileStatusPromises).then((results) => {
const statusMap: Record<string, { hasFiles: boolean; fileCount: number }> = {};
results.forEach((result) => {
Object.assign(statusMap, result.statuses);
});
setFileStatusMap(statusMap);
});
} catch (error) {
// console.error("❌ 테이블 데이터 조회 실패:", error);
setData([]);
setTotal(0);
setTotalPages(1);
} finally {
setLoading(false);
}
},
[component.tableName, pageSize, component.autoFilter, splitPanelContext?.selectedLeftData, relatedButtonFilter], // 🆕 autoFilter, 연결필터, RelatedDataButtons 필터 추가
);
// 현재 사용자 정보 로드
useEffect(() => {
const fetchCurrentUser = async () => {
@@ -2681,7 +2683,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
className="h-32 text-center"
>
<div className="text-muted-foreground flex flex-col items-center gap-2">
<Database className="h-6 w-6" />
<Database className="h-8 w-8 opacity-40" />
<p> </p>
<p className="text-xs"> </p>
</div>
@@ -2759,7 +2761,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
) : (
<div className="flex flex-1 items-center justify-center">
<div className="text-muted-foreground flex flex-col items-center gap-2">
<Database className="h-6 w-6" />
<Database className="h-8 w-8 opacity-40" />
<p className="text-sm"> </p>
<p className="text-xs"> </p>
</div>