반응형 및 테이블 리스트 컴포넌트 오류 수정

This commit is contained in:
kjs
2025-10-17 15:31:23 +09:00
parent 54e9f45823
commit 2a8081a253
21 changed files with 886 additions and 262 deletions

View File

@@ -155,14 +155,26 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
onSelectedRowsChange,
onConfigChange,
refreshKey,
tableName, // 화면의 기본 테이블명 (screenInfo에서 전달)
}) => {
// 컴포넌트 설정
const tableConfig = {
...config,
...component.config,
...componentConfig,
// selectedTable이 없으면 화면의 기본 테이블 사용
selectedTable:
componentConfig?.selectedTable || component.config?.selectedTable || config?.selectedTable || tableName,
} as TableListConfig;
console.log("🔍 TableListComponent 초기화:", {
componentConfigSelectedTable: componentConfig?.selectedTable,
componentConfigSelectedTable2: component.config?.selectedTable,
configSelectedTable: config?.selectedTable,
screenTableName: tableName,
finalSelectedTable: tableConfig.selectedTable,
});
// 🎨 동적 색상 설정 (속성편집 모달의 "색상" 필드와 연동)
const buttonColor = component.style?.labelColor || "#212121"; // 기본 파란색
const buttonTextColor = component.config?.buttonTextColor || "#ffffff";
@@ -424,20 +436,8 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
}
};
// 디바운싱된 테이블 데이터 가져오기
const fetchTableDataDebounced = useCallback(
debouncedApiCall(
`fetchTableData_${tableConfig.selectedTable}_${currentPage}_${localPageSize}`,
async () => {
return fetchTableDataInternal();
},
200, // 200ms 디바운스
),
[tableConfig.selectedTable, currentPage, localPageSize, searchTerm, sortColumn, sortDirection, searchValues],
);
// 실제 테이블 데이터 가져오기 함수
const fetchTableDataInternal = async () => {
const fetchTableDataInternal = useCallback(async () => {
if (!tableConfig.selectedTable) {
setData([]);
return;
@@ -448,81 +448,54 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
try {
// 🎯 Entity 조인 API 사용 - Entity 조인이 포함된 데이터 조회
console.log("🔗 Entity 조인 데이터 조회 시작:", tableConfig.selectedTable);
// Entity 조인 컬럼 추출 (isEntityJoin === true인 컬럼들)
const entityJoinColumns = tableConfig.columns?.filter((col) => col.isEntityJoin && col.entityJoinInfo) || [];
// 🎯 조인 탭에서 추가한 컬럼들도 포함 (실제로 존재하는 컬럼)
const joinTabColumns =
tableConfig.columns?.filter(
(col) =>
!col.isEntityJoin &&
col.columnName.includes("_") &&
(col.columnName.includes("dept_code_") ||
col.columnName.includes("_dept_code") ||
col.columnName.includes("_company_") ||
col.columnName.includes("_user_")), // 조인 탭에서 추가한 컬럼 패턴들
) || [];
// 🎯 조인 탭에서 추가한 컬럼들 추출 (additionalJoinInfo가 있는 컬럼)
const manualJoinColumns =
tableConfig.columns?.filter((col) => {
return col.additionalJoinInfo !== undefined;
}) || [];
console.log(
"🔍 조인 탭 컬럼들:",
joinTabColumns.map((c) => c.columnName),
"🔗 수동 조인 컬럼 감지:",
manualJoinColumns.map((c) => ({
columnName: c.columnName,
additionalJoinInfo: c.additionalJoinInfo,
})),
);
const additionalJoinColumns = [
...entityJoinColumns.map((col) => ({
// 🎯 추가 조인 컬럼 정보 구성
const additionalJoinColumns: Array<{
sourceTable: string;
sourceColumn: string;
joinAlias: string;
referenceTable?: string;
}> = [];
// Entity 조인 컬럼들
entityJoinColumns.forEach((col) => {
additionalJoinColumns.push({
sourceTable: col.entityJoinInfo!.sourceTable,
sourceColumn: col.entityJoinInfo!.sourceColumn,
joinAlias: col.entityJoinInfo!.joinAlias,
})),
// 🎯 조인 탭에서 추가한 컬럼들도 추가 (실제로 존재하는 컬럼만)
...joinTabColumns
.filter((col) => {
// 실제 API 응답에 존재하는 컬럼만 필터링
const validJoinColumns = ["dept_code_name", "dept_name"];
const isValid = validJoinColumns.includes(col.columnName);
if (!isValid) {
console.log(`🔍 조인 탭 컬럼 제외: ${col.columnName} (유효하지 않음)`);
}
return isValid;
})
.map((col) => {
// 실제 존재하는 조인 컬럼만 처리
let sourceTable = tableConfig.selectedTable;
let sourceColumn = col.columnName;
});
});
if (col.columnName === "dept_code_name" || col.columnName === "dept_name") {
sourceTable = "dept_info";
sourceColumn = "dept_code";
}
console.log(`🔍 조인 탭 컬럼 처리: ${col.columnName} -> ${sourceTable}.${sourceColumn}`);
return {
sourceTable: sourceTable || tableConfig.selectedTable || "",
sourceColumn: sourceColumn,
joinAlias: col.columnName,
};
}),
];
// 🎯 화면별 엔티티 표시 설정 생성
const screenEntityConfigs: Record<string, any> = {};
entityJoinColumns.forEach((col) => {
if (col.entityDisplayConfig) {
const sourceColumn = col.entityJoinInfo!.sourceColumn;
screenEntityConfigs[sourceColumn] = {
displayColumns: col.entityDisplayConfig.displayColumns,
separator: col.entityDisplayConfig.separator || " - ",
};
// 수동 조인 컬럼들 - 저장된 조인 정보 사용
manualJoinColumns.forEach((col) => {
if (col.additionalJoinInfo) {
additionalJoinColumns.push({
sourceTable: col.additionalJoinInfo.sourceTable,
sourceColumn: col.additionalJoinInfo.sourceColumn,
joinAlias: col.additionalJoinInfo.joinAlias,
referenceTable: col.additionalJoinInfo.referenceTable,
});
}
});
console.log("🔗 Entity 조인 컬럼:", entityJoinColumns);
console.log("🔗 조인 탭 컬럼:", joinTabColumns);
console.log("🔗 추가 Entity 조인 컬럼:", additionalJoinColumns);
// console.log("🎯 화면별 엔티티 설정:", screenEntityConfigs);
console.log("🔗 최종 추가 조인 컬럼:", additionalJoinColumns);
const result = await entityJoinApi.getTableDataWithJoins(tableConfig.selectedTable, {
page: currentPage,
@@ -591,7 +564,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
sortOrder: sortDirection,
enableEntityJoin: true, // 🎯 Entity 조인 활성화
additionalJoinColumns: additionalJoinColumns.length > 0 ? additionalJoinColumns : undefined, // 추가 조인 컬럼
screenEntityConfigs: Object.keys(screenEntityConfigs).length > 0 ? screenEntityConfigs : undefined, // 🎯 화면별 엔티티 설정
});
if (result) {
@@ -661,16 +633,16 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
const actualApiColumns = Object.keys(result.data[0]);
console.log("🔍 API 응답의 실제 컬럼들:", actualApiColumns);
// 🎯 조인 컬럼 매핑 테이블 (사용자 설정 → API 응답)
// 실제 API 응답에 존재하는 컬럼 매핑
const newJoinColumnMapping: Record<string, string> = {
dept_code_dept_code: "dept_code", // user_info.dept_code
dept_code_status: "status", // user_info.status (dept_info.status가 조인되지 않음)
dept_code_company_name: "dept_name", // dept_info.dept_name (company_name이 조인되지 않음)
dept_code_name: "dept_code_name", // dept_info.dept_name
dept_name: "dept_name", // dept_info.dept_name
status: "status", // user_info.status
};
// 🎯 조인 컬럼 매핑 테이블 - 동적 생성
// API 응답에 실제로 존재하는 컬럼과 사용자 설정 컬럼을 비교하여 자동 매핑
const newJoinColumnMapping: Record<string, string> = {};
processedColumns.forEach((col) => {
// API 응답에 정확히 일치하는 컬럼이 있으면 그대로 사용
if (actualApiColumns.includes(col.columnName)) {
newJoinColumnMapping[col.columnName] = col.columnName;
}
});
// 🎯 조인 컬럼 매핑 상태 업데이트
setJoinColumnMapping(newJoinColumnMapping);
@@ -795,7 +767,37 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
} finally {
setLoading(false);
}
};
}, [
tableConfig.selectedTable,
tableConfig.columns,
currentPage,
localPageSize,
searchTerm,
sortColumn,
sortDirection,
searchValues,
]);
// 디바운싱된 테이블 데이터 가져오기
const fetchTableDataDebounced = useCallback(
debouncedApiCall(
`fetchTableData_${tableConfig.selectedTable}_${currentPage}_${localPageSize}`,
async () => {
return fetchTableDataInternal();
},
200, // 200ms 디바운스
),
[
tableConfig.selectedTable,
currentPage,
localPageSize,
searchTerm,
sortColumn,
sortDirection,
searchValues,
fetchTableDataInternal,
],
);
// 페이지 변경
const handlePageChange = (newPage: number) => {
@@ -947,12 +949,37 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
}
}, [columnLabels]);
// 🎯 컬럼 개수와 컬럼명을 문자열로 변환하여 의존성 추적
const columnsKey = useMemo(() => {
if (!tableConfig.columns) return "";
return tableConfig.columns.map((col) => col.columnName).join(",");
}, [tableConfig.columns]);
useEffect(() => {
if (tableConfig.autoLoad && !isDesignMode) {
fetchTableDataDebounced();
// autoLoad가 undefined거나 true일 때 자동 로드 (기본값: true)
const shouldAutoLoad = tableConfig.autoLoad !== false;
console.log("🔍 TableList 데이터 로드 조건 체크:", {
shouldAutoLoad,
isDesignMode,
selectedTable: tableConfig.selectedTable,
autoLoadSetting: tableConfig.autoLoad,
willLoad: shouldAutoLoad && !isDesignMode,
});
if (shouldAutoLoad && !isDesignMode) {
console.log("✅ 테이블 데이터 로드 시작:", tableConfig.selectedTable);
fetchTableDataInternal();
} else {
console.warn("⚠️ 테이블 데이터 로드 차단:", {
reason: !shouldAutoLoad ? "autoLoad=false" : "isDesignMode=true",
shouldAutoLoad,
isDesignMode,
});
}
}, [
tableConfig.selectedTable,
columnsKey, // 🎯 컬럼이 추가/변경될 때 데이터 다시 로드 (문자열 비교)
localPageSize,
currentPage,
searchTerm,
@@ -960,6 +987,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
sortDirection,
columnLabels,
searchValues,
fetchTableDataInternal, // 의존성 배열에 추가
]);
// refreshKey 변경 시 테이블 데이터 새로고침
@@ -992,7 +1020,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
};
window.addEventListener("refreshTable", handleRefreshTable);
return () => {
window.removeEventListener("refreshTable", handleRefreshTable);
};
@@ -1314,35 +1342,18 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
onDragEnd,
};
// 디자인 모드에서의 플레이스홀더
if (isDesignMode && !tableConfig.selectedTable) {
return (
<div style={componentStyle} className={className} {...domProps}>
<div className="flex h-full items-center justify-center rounded-2xl border-2 border-dashed border-blue-200 bg-gradient-to-br from-blue-50/30 to-indigo-50/20">
<div className="p-8 text-center">
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-2xl bg-gradient-to-br from-blue-100 to-indigo-100 shadow-sm">
<TableIcon className="h-8 w-8 text-blue-600" />
</div>
<div className="mb-2 text-lg font-semibold text-slate-700"> </div>
<div className="rounded-full bg-white/60 px-4 py-2 text-sm text-slate-500">
</div>
</div>
</div>
</div>
);
}
// 플레이스홀더 제거 - 디자인 모드에서도 바로 테이블 표시
return (
<div
style={{ ...componentStyle, zIndex: 10 }} // 🎯 componentStyle + z-index 추가
className={cn(
"relative overflow-hidden",
"bg-white border border-gray-200/60",
"border border-gray-200/60 bg-white",
"rounded-2xl shadow-sm",
"backdrop-blur-sm",
"transition-all duration-300 ease-out",
isSelected && "ring-2 ring-blue-500/20 shadow-lg shadow-blue-500/10",
isSelected && "shadow-lg ring-2 shadow-blue-500/10 ring-blue-500/20",
className,
)}
{...domProps}
@@ -1359,7 +1370,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
>
<div className="flex items-center space-x-4">
{(tableConfig.title || tableLabel) && (
<h3 className="text-xl font-semibold text-gray-800 tracking-tight">{tableConfig.title || tableLabel}</h3>
<h3 className="text-xl font-semibold tracking-tight text-gray-800">{tableConfig.title || tableLabel}</h3>
)}
</div>
@@ -1377,16 +1388,14 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
size="sm"
onClick={handleRefresh}
disabled={loading}
className="group relative rounded-xl border-gray-200/60 bg-white/80 backdrop-blur-sm shadow-sm hover:shadow-md transition-all duration-200 hover:bg-gray-50/80"
className="group relative rounded-xl border-gray-200/60 bg-white/80 shadow-sm backdrop-blur-sm transition-all duration-200 hover:bg-gray-50/80 hover:shadow-md"
>
<div className="flex items-center space-x-2">
<div className="relative">
<RefreshCw className={cn("h-4 w-4 text-gray-600", loading && "animate-spin")} />
{loading && <div className="absolute -inset-1 animate-pulse rounded-full bg-blue-100/40"></div>}
</div>
<span className="text-sm font-medium text-gray-700">
{loading ? "새로고침 중..." : "새로고침"}
</span>
<span className="text-sm font-medium text-gray-700">{loading ? "새로고침 중..." : "새로고침"}</span>
</div>
</Button>
</div>
@@ -1424,7 +1433,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
{/* 테이블 컨텐츠 */}
<div
className={`w-full overflow-auto flex-1`}
className={`w-full flex-1 overflow-auto`}
style={{
width: "100%",
maxWidth: "100%",
@@ -1622,7 +1631,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
<TableCell
key={column.columnName}
className={cn(
"h-12 px-6 py-4 align-middle text-sm transition-all duration-200 text-gray-600",
"h-12 px-6 py-4 align-middle text-sm text-gray-600 transition-all duration-200",
`text-${column.align}`,
)}
style={{
@@ -1687,7 +1696,8 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
</div>
{/* 푸터/페이지네이션 */}
{tableConfig.showFooter && tableConfig.pagination?.enabled && (
{/* showFooter와 pagination.enabled의 기본값은 true */}
{tableConfig.showFooter !== false && tableConfig.pagination?.enabled !== false && (
<div
className="flex flex-col items-center justify-center space-y-4 border-t border-gray-200 bg-gray-100/80 p-6"
style={{
@@ -1749,7 +1759,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
// 데이터는 useEffect에서 자동으로 다시 로드됨
}}
className="rounded-xl border border-gray-200/60 bg-white/80 backdrop-blur-sm px-4 py-2 text-sm font-medium text-gray-700 shadow-sm transition-all duration-200 hover:border-gray-300/60 hover:bg-white hover:shadow-md"
className="rounded-xl border border-gray-200/60 bg-white/80 px-4 py-2 text-sm font-medium text-gray-700 shadow-sm backdrop-blur-sm transition-all duration-200 hover:border-gray-300/60 hover:bg-white hover:shadow-md"
>
{(tableConfig.pagination?.pageSizeOptions || [10, 20, 50, 100]).map((size) => (
<option key={size} value={size}>
@@ -1760,13 +1770,13 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
)}
{/* 페이지네이션 버튼 */}
<div className="flex items-center space-x-2 rounded-xl border border-gray-200/60 bg-white/80 backdrop-blur-sm p-1 shadow-sm">
<div className="flex items-center space-x-2 rounded-xl border border-gray-200/60 bg-white/80 p-1 shadow-sm backdrop-blur-sm">
<Button
variant="outline"
size="sm"
onClick={() => handlePageChange(1)}
disabled={currentPage === 1}
className="h-8 w-8 p-0 rounded-lg border-gray-200/60 hover:border-gray-300/60 hover:bg-gray-50/80 hover:text-gray-700 disabled:opacity-50 transition-all duration-200"
className="h-8 w-8 rounded-lg border-gray-200/60 p-0 transition-all duration-200 hover:border-gray-300/60 hover:bg-gray-50/80 hover:text-gray-700 disabled:opacity-50"
>
<ChevronsLeft className="h-4 w-4" />
</Button>
@@ -1775,7 +1785,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
size="sm"
onClick={() => handlePageChange(currentPage - 1)}
disabled={currentPage === 1}
className="h-8 w-8 p-0 rounded-lg border-gray-200/60 hover:border-gray-300/60 hover:bg-gray-50/80 hover:text-gray-700 disabled:opacity-50 transition-all duration-200"
className="h-8 w-8 rounded-lg border-gray-200/60 p-0 transition-all duration-200 hover:border-gray-300/60 hover:bg-gray-50/80 hover:text-gray-700 disabled:opacity-50"
>
<ChevronLeft className="h-4 w-4" />
</Button>
@@ -1791,7 +1801,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
size="sm"
onClick={() => handlePageChange(currentPage + 1)}
disabled={currentPage === totalPages}
className="h-8 w-8 p-0 rounded-lg border-gray-200/60 hover:border-gray-300/60 hover:bg-gray-50/80 hover:text-gray-700 disabled:opacity-50 transition-all duration-200"
className="h-8 w-8 rounded-lg border-gray-200/60 p-0 transition-all duration-200 hover:border-gray-300/60 hover:bg-gray-50/80 hover:text-gray-700 disabled:opacity-50"
>
<ChevronRight className="h-4 w-4" />
</Button>
@@ -1800,7 +1810,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
size="sm"
onClick={() => handlePageChange(totalPages)}
disabled={currentPage === totalPages}
className="h-8 w-8 p-0 rounded-lg border-gray-200/60 hover:border-gray-300/60 hover:bg-gray-50/80 hover:text-gray-700 disabled:opacity-50 transition-all duration-200"
className="h-8 w-8 rounded-lg border-gray-200/60 p-0 transition-all duration-200 hover:border-gray-300/60 hover:bg-gray-50/80 hover:text-gray-700 disabled:opacity-50"
>
<ChevronsRight className="h-4 w-4" />
</Button>

View File

@@ -97,13 +97,20 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
>
>({});
// 화면 테이블명이 있으면 자동으로 설정
// 화면 테이블명이 있으면 자동으로 설정 (초기 한 번만)
useEffect(() => {
if (screenTableName && (!config.selectedTable || config.selectedTable !== screenTableName)) {
console.log("🔄 화면 테이블명 자동 설정:", screenTableName);
onChange({ selectedTable: screenTableName });
if (screenTableName && !config.selectedTable) {
// 기존 config의 모든 속성을 유지하면서 selectedTable만 추가/업데이트
const updatedConfig = {
...config,
selectedTable: screenTableName,
// 컬럼이 있으면 유지, 없으면 빈 배열
columns: config.columns || [],
};
onChange(updatedConfig);
}
}, [screenTableName, config.selectedTable, onChange]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [screenTableName]); // config.selectedTable이 없을 때만 실행되도록 의존성 최소화
// 테이블 목록 가져오기
useEffect(() => {
@@ -137,25 +144,32 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
screenTableName,
);
// 컴포넌트에 명시적으로 테이블이 선택되었거나, 화면에 연결된 테이블이 있는 경우에만 컬럼 목록 표시
const shouldShowColumns = config.selectedTable || (screenTableName && config.columns && config.columns.length > 0);
// 컴포넌트에 명시적으로 테이블이 선택되었거나, 화면에 연결된 테이블이 있는 경우 컬럼 목록 표시
const shouldShowColumns = config.selectedTable || screenTableName;
if (!shouldShowColumns) {
console.log("🔧 컬럼 목록 숨김 - 명시적 테이블 선택 또는 설정된 컬럼이 없음");
console.log("🔧 컬럼 목록 숨김 - 테이블 선택되지 않음");
setAvailableColumns([]);
return;
}
// tableColumns prop을 우선 사용하되, 컴포넌트가 명시적으로 설정되었을 때만
if (tableColumns && tableColumns.length > 0 && (config.selectedTable || config.columns?.length > 0)) {
console.log("🔧 tableColumns prop 사용:", tableColumns);
// tableColumns prop을 우선 사용
if (tableColumns && tableColumns.length > 0) {
const mappedColumns = tableColumns.map((column: any) => ({
columnName: column.columnName || column.name,
dataType: column.dataType || column.type || "text",
label: column.label || column.displayName || column.columnLabel || column.columnName || column.name,
}));
console.log("🏷️ availableColumns 설정됨:", mappedColumns);
setAvailableColumns(mappedColumns);
// selectedTable이 없으면 screenTableName으로 설정
if (!config.selectedTable && screenTableName) {
onChange({
...config,
selectedTable: screenTableName,
columns: config.columns || [],
});
}
} else if (config.selectedTable || screenTableName) {
// API에서 컬럼 정보 가져오기
const fetchColumns = async () => {
@@ -190,7 +204,7 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
} else {
setAvailableColumns([]);
}
}, [config.selectedTable, screenTableName, tableColumns, config.columns]);
}, [config.selectedTable, screenTableName, tableColumns]);
// Entity 조인 컬럼 정보 가져오기
useEffect(() => {
@@ -235,7 +249,7 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
// hasOnChange: !!onChange,
// onChangeType: typeof onChange,
// });
const parentValue = config[parentKey] as any;
const newConfig = {
[parentKey]: {
@@ -243,7 +257,7 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
[childKey]: value,
},
};
// console.log("📤 TableListConfigPanel onChange 호출:", newConfig);
onChange(newConfig);
};
@@ -275,8 +289,30 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
// 🎯 조인 컬럼 추가 (조인 탭에서 추가하는 컬럼들은 일반 컬럼으로 처리)
const addEntityColumn = (joinColumn: (typeof entityJoinColumns.availableColumns)[0]) => {
console.log("🔗 조인 컬럼 추가 요청:", {
joinColumn,
joinAlias: joinColumn.joinAlias,
columnLabel: joinColumn.columnLabel,
tableName: joinColumn.tableName,
columnName: joinColumn.columnName,
});
const existingColumn = config.columns?.find((col) => col.columnName === joinColumn.joinAlias);
if (existingColumn) return;
if (existingColumn) {
console.warn("⚠️ 이미 존재하는 컬럼:", joinColumn.joinAlias);
return;
}
// 🎯 joinTables에서 sourceColumn 찾기
const joinTableInfo = entityJoinColumns.joinTables?.find((jt: any) => jt.tableName === joinColumn.tableName);
const sourceColumn = joinTableInfo?.joinConfig?.sourceColumn || "";
console.log("🔍 조인 정보 추출:", {
tableName: joinColumn.tableName,
foundJoinTable: !!joinTableInfo,
sourceColumn,
joinConfig: joinTableInfo?.joinConfig,
});
// 조인 탭에서 추가하는 컬럼들은 일반 컬럼으로 처리 (isEntityJoin: false)
const newColumn: ColumnConfig = {
@@ -289,10 +325,21 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
format: "text",
order: config.columns?.length || 0,
isEntityJoin: false, // 조인 탭에서 추가하는 컬럼은 엔티티 타입이 아님
// 🎯 추가 조인 정보 저장
additionalJoinInfo: {
sourceTable: config.selectedTable || screenTableName || "", // 기준 테이블 (예: user_info)
sourceColumn: sourceColumn, // 기준 컬럼 (예: dept_code) - joinTables에서 추출
referenceTable: joinColumn.tableName, // 참조 테이블 (예: dept_info)
joinAlias: joinColumn.joinAlias, // 조인 별칭 (예: dept_code_company_name)
},
};
handleChange("columns", [...(config.columns || []), newColumn]);
console.log("🔗 조인 컬럼 추가됨 (일반 컬럼으로 처리):", newColumn);
console.log(" 조인 컬럼 추가 완료:", {
columnName: newColumn.columnName,
displayName: newColumn.displayName,
totalColumns: (config.columns?.length || 0) + 1,
});
};
// 컬럼 제거
@@ -309,17 +356,31 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
};
// 🎯 기존 컬럼들을 체크하여 엔티티 타입인 경우 isEntityJoin 플래그 설정
// useRef로 이전 컬럼 개수를 추적하여 새 컬럼 추가 시에만 실행
const prevColumnsLengthRef = React.useRef<number>(0);
useEffect(() => {
const currentLength = config.columns?.length || 0;
const prevLength = prevColumnsLengthRef.current;
console.log("🔍 엔티티 컬럼 감지 useEffect 실행:", {
hasColumns: !!config.columns,
columnsCount: config.columns?.length || 0,
columnsCount: currentLength,
prevColumnsCount: prevLength,
hasTableColumns: !!tableColumns,
tableColumnsCount: tableColumns?.length || 0,
selectedTable: config.selectedTable,
});
if (!config.columns || !tableColumns) {
if (!config.columns || !tableColumns || config.columns.length === 0) {
console.log("⚠️ 컬럼 또는 테이블 컬럼 정보가 없어서 엔티티 감지 스킵");
prevColumnsLengthRef.current = currentLength;
return;
}
// 컬럼 개수가 변경되지 않았고, 이미 체크한 적이 있으면 스킵
if (currentLength === prevLength && prevLength > 0) {
console.log(" 컬럼 개수 변경 없음, 엔티티 감지 스킵");
return;
}
@@ -352,14 +413,14 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
...column,
isEntityJoin: true,
entityJoinInfo: {
sourceTable: config.selectedTable || "",
sourceTable: config.selectedTable || screenTableName || "",
sourceColumn: column.columnName,
joinAlias: column.columnName,
},
entityDisplayConfig: {
displayColumns: [], // 빈 배열로 초기화
separator: " - ",
sourceTable: config.selectedTable || "",
sourceTable: config.selectedTable || screenTableName || "",
joinTable: tableColumn.reference_table || tableColumn.referenceTable || "",
},
};
@@ -377,7 +438,11 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
} else {
console.log(" 엔티티 컬럼 변경사항 없음");
}
}, [config.columns, tableColumns, config.selectedTable]);
// 현재 컬럼 개수를 저장
prevColumnsLengthRef.current = currentLength;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [config.columns?.length, tableColumns, config.selectedTable]); // 컬럼 개수 변경 시에만 실행
// 🎯 엔티티 컬럼의 표시 컬럼 정보 로드
const loadEntityDisplayConfig = async (column: ColumnConfig) => {
@@ -400,6 +465,15 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
// entityDisplayConfig가 없으면 초기화
if (!column.entityDisplayConfig) {
console.log("🔧 entityDisplayConfig 초기화:", column.columnName);
// sourceTable을 결정: entityJoinInfo -> config.selectedTable -> screenTableName 순서
const initialSourceTable = column.entityJoinInfo?.sourceTable || config.selectedTable || screenTableName;
if (!initialSourceTable) {
console.warn("⚠️ sourceTable을 결정할 수 없어서 초기화 실패:", column.columnName);
return;
}
const updatedColumns = config.columns?.map((col) => {
if (col.columnName === column.columnName) {
return {
@@ -407,7 +481,7 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
entityDisplayConfig: {
displayColumns: [],
separator: " - ",
sourceTable: config.selectedTable || "",
sourceTable: initialSourceTable,
joinTable: "",
},
};
@@ -430,15 +504,34 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
console.log("🔍 entityDisplayConfig 전체 구조:", column.entityDisplayConfig);
console.log("🔍 entityDisplayConfig 키들:", Object.keys(column.entityDisplayConfig));
// sourceTable과 joinTable이 없으면 entityJoinInfo에서 가져오기
let sourceTable = column.entityDisplayConfig.sourceTable;
// sourceTable 결정 우선순위:
// 1. entityDisplayConfig.sourceTable
// 2. entityJoinInfo.sourceTable
// 3. config.selectedTable
// 4. screenTableName
let sourceTable =
column.entityDisplayConfig.sourceTable ||
column.entityJoinInfo?.sourceTable ||
config.selectedTable ||
screenTableName;
let joinTable = column.entityDisplayConfig.joinTable;
if (!sourceTable && column.entityJoinInfo) {
sourceTable = column.entityJoinInfo.sourceTable;
// sourceTable이 여전히 비어있으면 에러
if (!sourceTable) {
console.error("❌ sourceTable이 비어있어서 처리 불가:", {
columnName: column.columnName,
entityDisplayConfig: column.entityDisplayConfig,
entityJoinInfo: column.entityJoinInfo,
configSelectedTable: config.selectedTable,
screenTableName,
});
return;
}
if (!joinTable) {
console.log("✅ sourceTable 결정됨:", sourceTable);
if (!joinTable && sourceTable) {
// joinTable이 없으면 tableTypeApi로 조회해서 설정
try {
console.log("🔍 joinTable이 없어서 tableTypeApi로 조회:", sourceTable);
@@ -464,10 +557,15 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
if (updatedColumns) {
handleChange("columns", updatedColumns);
}
} else {
console.warn("⚠️ tableTypeApi에서 조인 테이블 정보를 찾지 못함:", column.columnName);
}
} catch (error) {
console.error("tableTypeApi 컬럼 정보 조회 실패:", error);
console.log("❌ 조회 실패 상세:", { sourceTable, columnName: column.columnName });
}
} else if (!joinTable) {
console.warn("⚠️ sourceTable이 없어서 joinTable 조회 불가:", column.columnName);
}
console.log("🔍 최종 추출한 값:", { sourceTable, joinTable });
@@ -789,15 +887,13 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
<div className="space-y-4 border-t pt-4">
<div className="space-y-3">
<Label className="text-sm font-medium"> </Label>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="cards-per-row"> </Label>
<Select
value={config.cardConfig?.cardsPerRow?.toString() || "3"}
onValueChange={(value) =>
handleNestedChange("cardConfig", "cardsPerRow", parseInt(value))
}
onValueChange={(value) => handleNestedChange("cardConfig", "cardsPerRow", parseInt(value))}
>
<SelectTrigger>
<SelectValue />
@@ -819,9 +915,7 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
id="card-spacing"
type="number"
value={config.cardConfig?.cardSpacing || 16}
onChange={(e) =>
handleNestedChange("cardConfig", "cardSpacing", parseInt(e.target.value))
}
onChange={(e) => handleNestedChange("cardConfig", "cardSpacing", parseInt(e.target.value))}
min="0"
max="50"
/>
@@ -830,15 +924,13 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
<div className="space-y-3">
<Label className="text-sm font-medium"> </Label>
<div className="grid grid-cols-1 gap-3">
<div className="space-y-2">
<Label htmlFor="id-column">ID ( )</Label>
<Select
value={config.cardConfig?.idColumn || ""}
onValueChange={(value) =>
handleNestedChange("cardConfig", "idColumn", value)
}
onValueChange={(value) => handleNestedChange("cardConfig", "idColumn", value)}
>
<SelectTrigger>
<SelectValue placeholder="ID 컬럼 선택" />
@@ -857,9 +949,7 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
<Label htmlFor="title-column"> ( )</Label>
<Select
value={config.cardConfig?.titleColumn || ""}
onValueChange={(value) =>
handleNestedChange("cardConfig", "titleColumn", value)
}
onValueChange={(value) => handleNestedChange("cardConfig", "titleColumn", value)}
>
<SelectTrigger>
<SelectValue placeholder="제목 컬럼 선택" />
@@ -878,9 +968,7 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
<Label htmlFor="subtitle-column"> ( )</Label>
<Select
value={config.cardConfig?.subtitleColumn || ""}
onValueChange={(value) =>
handleNestedChange("cardConfig", "subtitleColumn", value)
}
onValueChange={(value) => handleNestedChange("cardConfig", "subtitleColumn", value)}
>
<SelectTrigger>
<SelectValue placeholder="서브 제목 컬럼 선택 (선택사항)" />
@@ -900,9 +988,7 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
<Label htmlFor="description-column"> </Label>
<Select
value={config.cardConfig?.descriptionColumn || ""}
onValueChange={(value) =>
handleNestedChange("cardConfig", "descriptionColumn", value)
}
onValueChange={(value) => handleNestedChange("cardConfig", "descriptionColumn", value)}
>
<SelectTrigger>
<SelectValue placeholder="설명 컬럼 선택 (선택사항)" />
@@ -924,7 +1010,7 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
<Checkbox
id="show-card-actions"
checked={config.cardConfig?.showActions !== false}
onCheckedChange={(checked) =>
onCheckedChange={(checked) =>
handleNestedChange("cardConfig", "showActions", checked as boolean)
}
/>
@@ -1270,7 +1356,34 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
<Button
variant="outline"
size="sm"
onClick={() => loadEntityDisplayConfig(column)}
onClick={() => {
// sourceTable 정보가 있는지 확인
const hasSourceTable =
column.entityDisplayConfig?.sourceTable ||
column.entityJoinInfo?.sourceTable ||
config.selectedTable ||
screenTableName;
if (!hasSourceTable) {
console.error("❌ sourceTable 정보를 찾을 수 없어서 컬럼 로드 불가:", {
columnName: column.columnName,
entityDisplayConfig: column.entityDisplayConfig,
entityJoinInfo: column.entityJoinInfo,
configSelectedTable: config.selectedTable,
screenTableName,
});
alert("컬럼 정보를 로드할 수 없습니다. 테이블 정보가 없습니다.");
return;
}
loadEntityDisplayConfig(column);
}}
disabled={
!column.entityDisplayConfig?.sourceTable &&
!column.entityJoinInfo?.sourceTable &&
!config.selectedTable &&
!screenTableName
}
className="h-6 text-xs"
>
<Plus className="mr-1 h-3 w-3" />

View File

@@ -72,21 +72,29 @@ export interface ColumnConfig {
// 새로운 기능들
hidden?: boolean; // 숨김 기능 (편집기에서는 연하게, 실제 화면에서는 숨김)
autoGeneration?: AutoGenerationConfig; // 자동생성 설정
// 🎯 추가 조인 컬럼 정보 (조인 탭에서 추가한 컬럼들)
additionalJoinInfo?: {
sourceTable: string; // 원본 테이블
sourceColumn: string; // 원본 컬럼 (예: dept_code)
referenceTable?: string; // 참조 테이블 (예: dept_info)
joinAlias: string; // 조인 별칭 (예: dept_code_company_name)
};
}
/**
* 카드 디스플레이 설정
*/
export interface CardDisplayConfig {
idColumn: string; // ID 컬럼 (사번 등)
titleColumn: string; // 제목 컬럼 (이름 등)
subtitleColumn?: string; // 부제목 컬럼 (부서 등)
idColumn: string; // ID 컬럼 (사번 등)
titleColumn: string; // 제목 컬럼 (이름 등)
subtitleColumn?: string; // 부제목 컬럼 (부서 등)
descriptionColumn?: string; // 설명 컬럼
imageColumn?: string; // 이미지 컬럼
cardsPerRow: number; // 한 행당 카드 수 (기본: 3)
cardSpacing: number; // 카드 간격 (기본: 16px)
showActions: boolean; // 액션 버튼 표시 여부
cardHeight?: number; // 카드 높이 (기본: auto)
imageColumn?: string; // 이미지 컬럼
cardsPerRow: number; // 한 행당 카드 수 (기본: 3)
cardSpacing: number; // 카드 간격 (기본: 16px)
showActions: boolean; // 액션 버튼 표시 여부
cardHeight?: number; // 카드 높이 (기본: auto)
}
/**
@@ -163,11 +171,11 @@ export interface CheckboxConfig {
*/
export interface TableListConfig extends ComponentConfig {
// 표시 모드 설정
displayMode?: "table" | "card"; // 기본: "table"
displayMode?: "table" | "card"; // 기본: "table"
// 카드 디스플레이 설정 (displayMode가 "card"일 때 사용)
cardConfig?: CardDisplayConfig;
// 테이블 기본 설정
selectedTable?: string;
tableName?: string;