우측화면 데이터 필터링 수정

This commit is contained in:
kjs
2025-12-16 11:49:10 +09:00
parent 4e74c7b5ba
commit d8329d31e4
12 changed files with 128 additions and 378 deletions

View File

@@ -61,20 +61,17 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
// 테이블 데이터 상태 관리
const [loadedTableData, setLoadedTableData] = useState<any[]>([]);
const [loadedTableColumns, setLoadedTableColumns] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const [loading, setLoading] = useState(true); // 초기 로딩 상태를 true로 설정
const [initialLoadDone, setInitialLoadDone] = useState(false); // 초기 로드 완료 여부
const [hasEverSelectedLeftData, setHasEverSelectedLeftData] = useState(false); // 좌측 데이터 선택 이력
// 필터 상태 (검색 필터 위젯에서 전달받은 필터)
const [filters, setFiltersInternal] = useState<TableFilter[]>([]);
// 필터 상태 변경 래퍼 (로깅용)
// 필터 상태 변경 래퍼
const setFilters = useCallback((newFilters: TableFilter[]) => {
console.log("🎴 [CardDisplay] setFilters 호출됨:", {
componentId: component.id,
filtersCount: newFilters.length,
filters: newFilters,
});
setFiltersInternal(newFilters);
}, [component.id]);
}, []);
// 카테고리 매핑 상태 (카테고리 코드 -> 라벨/색상)
const [columnMeta, setColumnMeta] = useState<
@@ -125,10 +122,6 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
// 삭제할 데이터를 배열로 감싸기 (API가 배열을 기대함)
const deleteData = [data];
console.log("🗑️ [CardDisplay] 삭제 요청:", {
tableName: tableNameToUse,
data: deleteData,
});
// API 호출로 데이터 삭제 (POST 방식으로 변경 - DELETE는 body 전달이 불안정)
// 백엔드 API는 DELETE /api/table-management/tables/:tableName/delete 이지만
@@ -143,7 +136,6 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
});
if (response.data.success) {
console.log("삭제 완료:", response.data.data?.deletedCount || 1, "건");
alert("삭제되었습니다.");
// 로컬 상태에서 삭제된 항목 제거
@@ -157,11 +149,9 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
setSelectedRows(newSelectedRows);
}
} else {
console.error("삭제 실패:", response.data.error);
alert(`삭제 실패: ${response.data.message || response.data.error || "알 수 없는 오류"}`);
}
} catch (error: any) {
console.error("삭제 중 오류 발생:", error);
const errorMessage = error.response?.data?.message || error.message || "알 수 없는 오류";
alert(`삭제 중 오류가 발생했습니다: ${errorMessage}`);
}
@@ -194,8 +184,7 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
// loadTableData();
} catch (error) {
console.error("❌ 편집 저장 실패:", error);
alert("❌ 저장에 실패했습니다.");
alert("저장 실패했습니다.");
}
};
@@ -204,6 +193,25 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
const loadTableData = async () => {
// 디자인 모드에서는 테이블 데이터를 로드하지 않음
if (isDesignMode) {
setLoading(false);
setInitialLoadDone(true);
return;
}
// 우측 패널인 경우, 좌측 데이터가 선택되지 않으면 데이터 로드하지 않음 (깜빡임 방지)
// splitPanelPosition이 "right"이면 분할 패널 내부이므로 연결 필터가 있을 가능성이 높음
const isRightPanelEarly = splitPanelPosition === "right";
const hasSelectedLeftDataEarly = splitPanelContext?.selectedLeftData &&
Object.keys(splitPanelContext.selectedLeftData).length > 0;
if (isRightPanelEarly && !hasSelectedLeftDataEarly) {
// 우측 패널이고 좌측 데이터가 선택되지 않은 경우 - 기존 데이터 유지 (깜빡임 방지)
// 초기 로드가 아닌 경우에는 데이터를 지우지 않음
if (!initialLoadDone) {
setLoadedTableData([]);
}
setLoading(false);
setInitialLoadDone(true);
return;
}
@@ -211,6 +219,8 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
const tableNameToUse = tableName || component.componentConfig?.tableName || 'user_info'; // 기본 테이블명 설정
if (!tableNameToUse) {
setLoading(false);
setInitialLoadDone(true);
return;
}
@@ -251,19 +261,23 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
}
linkedFilterValues = tableSpecificFilters;
console.log("🎴 [CardDisplay] 연결 필터 확인:", {
tableNameToUse,
hasLinkedFiltersConfigured,
hasSelectedLeftData,
linkedFilterValues,
});
}
// 연결 필터가 설정되어 있지만 좌측에서 데이터가 선택되지 않은 경우 빈 데이터 표시
if (splitPanelContext && hasLinkedFiltersConfigured && !hasSelectedLeftData) {
console.log("🎴 [CardDisplay] 연결 필터 활성화됨 - 좌측 선택 대기");
// 우측 패널이고 연결 필터가 설정되어 있지만 좌측에서 데이터가 선택되지 않은 경우 빈 데이터 표시
// 또는 우측 패널이고 linkedFilters 설정이 있으면 좌측 선택 필수
// splitPanelPosition은 screenContext에서 가져오거나, splitPanelContext에서 screenId로 확인
const isRightPanelFromContext = splitPanelPosition === "right";
const isRightPanelFromSplitContext = screenId && splitPanelContext?.getPositionByScreenId
? splitPanelContext.getPositionByScreenId(screenId as number) === "right"
: false;
const isRightPanel = isRightPanelFromContext || isRightPanelFromSplitContext;
const hasLinkedFiltersInConfig = splitPanelContext?.linkedFilters && splitPanelContext.linkedFilters.length > 0;
if (isRightPanel && (hasLinkedFiltersConfigured || hasLinkedFiltersInConfig) && !hasSelectedLeftData) {
setLoadedTableData([]);
setLoading(false);
setInitialLoadDone(true);
return;
}
@@ -277,7 +291,6 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
search: Object.keys(linkedFilterValues).length > 0 ? linkedFilterValues : undefined,
};
console.log("🎴 [CardDisplay] API 호출 파라미터:", apiParams);
// 테이블 데이터, 컬럼 정보, 입력 타입 정보를 병렬로 로드
const [dataResponse, columnsResponse, inputTypesResponse] = await Promise.all([
@@ -298,7 +311,6 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
codeCategory: item.codeCategory || item.code_category,
};
});
console.log("📋 [CardDisplay] 컬럼 메타 정보:", meta);
setColumnMeta(meta);
// 카테고리 타입 컬럼 찾기 및 매핑 로드
@@ -306,17 +318,14 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
.filter(([_, m]) => m.inputType === "category")
.map(([columnName]) => columnName);
console.log("📋 [CardDisplay] 카테고리 컬럼:", categoryColumns);
if (categoryColumns.length > 0) {
const mappings: Record<string, Record<string, { label: string; color?: string }>> = {};
for (const columnName of categoryColumns) {
try {
console.log(`📋 [CardDisplay] 카테고리 매핑 로드 시작: ${tableNameToUse}/${columnName}`);
const response = await apiClient.get(`/table-categories/${tableNameToUse}/${columnName}/values`);
console.log(`📋 [CardDisplay] 카테고리 API 응답 [${columnName}]:`, response.data);
if (response.data.success && response.data.data) {
const mapping: Record<string, { label: string; color?: string }> = {};
@@ -328,29 +337,27 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
const rawColor = item.color ?? item.badge_color;
const color = (rawColor && rawColor !== "none") ? rawColor : undefined;
mapping[code] = { label, color };
console.log(`📋 [CardDisplay] 매핑 추가: ${code} -> ${label} (color: ${color})`);
});
mappings[columnName] = mapping;
}
} catch (error) {
console.error(`❌ CardDisplay: 카테고리 매핑 로드 실패 [${columnName}]`, error);
// 카테고리 매핑 로드 실패 시 무시
}
}
console.log("📋 [CardDisplay] 최종 카테고리 매핑:", mappings);
setCategoryMappings(mappings);
}
} catch (error) {
console.error(`❌ CardDisplay: 데이터 로딩 실패`, error);
setLoadedTableData([]);
setLoadedTableColumns([]);
} finally {
setLoading(false);
setInitialLoadDone(true);
}
};
loadTableData();
}, [isDesignMode, tableName, component.componentConfig?.tableName, splitPanelContext?.selectedLeftData]);
}, [isDesignMode, tableName, component.componentConfig?.tableName, splitPanelContext?.selectedLeftData, splitPanelPosition]);
// 컴포넌트 설정 (기본값 보장)
const componentConfig = {
@@ -390,8 +397,34 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
componentStyle.borderColor = isSelected ? "hsl(var(--ring))" : "hsl(var(--border))";
}
// 우측 패널 + 좌측 미선택 상태 체크를 위한 값들 (displayData 외부에서 계산)
const isRightPanelForDisplay = splitPanelPosition === "right" ||
(screenId && splitPanelContext?.getPositionByScreenId?.(screenId as number) === "right");
const hasLinkedFiltersForDisplay = splitPanelContext?.linkedFilters && splitPanelContext.linkedFilters.length > 0;
const selectedLeftDataForDisplay = splitPanelContext?.selectedLeftData;
const hasSelectedLeftDataForDisplay = selectedLeftDataForDisplay &&
Object.keys(selectedLeftDataForDisplay).length > 0;
// 좌측 데이터가 한 번이라도 선택된 적이 있으면 기록
useEffect(() => {
if (hasSelectedLeftDataForDisplay) {
setHasEverSelectedLeftData(true);
}
}, [hasSelectedLeftDataForDisplay]);
// 우측 패널이고 연결 필터가 있고, 좌측 데이터가 한 번도 선택된 적이 없는 경우에만 "선택해주세요" 표시
// 한 번이라도 선택된 적이 있으면 깜빡임 방지를 위해 기존 데이터 유지
const shouldHideDataForRightPanel = isRightPanelForDisplay &&
!hasEverSelectedLeftData &&
!hasSelectedLeftDataForDisplay;
// 표시할 데이터 결정 (로드된 테이블 데이터 우선 사용)
const displayData = useMemo(() => {
// 우측 패널이고 linkedFilters가 설정되어 있지만 좌측 데이터가 선택되지 않은 경우 빈 배열 반환
if (shouldHideDataForRightPanel) {
return [];
}
// 로드된 테이블 데이터가 있으면 항상 우선 사용 (dataSource 설정 무시)
if (loadedTableData.length > 0) {
return loadedTableData;
@@ -408,7 +441,7 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
// 데이터가 없으면 빈 배열 반환
return [];
}, [componentConfig.dataSource, loadedTableData, tableData, componentConfig.staticData]);
}, [shouldHideDataForRightPanel, loadedTableData, tableData, componentConfig.staticData]);
// 실제 사용할 테이블 컬럼 정보 (로드된 컬럼 우선 사용)
const actualTableColumns = loadedTableColumns.length > 0 ? loadedTableColumns : tableColumns;
@@ -453,13 +486,8 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
additionalData: {},
}));
useModalDataStore.getState().setData(tableNameToUse, modalItems);
console.log("[CardDisplay] modalDataStore에 데이터 저장:", {
dataSourceId: tableNameToUse,
count: modalItems.length,
});
} else if (tableNameToUse && selectedRowsData.length === 0) {
useModalDataStore.getState().clearData(tableNameToUse);
console.log("[CardDisplay] modalDataStore 데이터 제거:", tableNameToUse);
}
// 분할 패널 컨텍스트에 선택된 데이터 저장 (좌측 화면인 경우)
@@ -467,13 +495,8 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
if (splitPanelContext && splitPanelPosition === "left" && !splitPanelContext.disableAutoDataTransfer) {
if (checked) {
splitPanelContext.setSelectedLeftData(data);
console.log("[CardDisplay] 분할 패널 좌측 데이터 저장:", {
data,
parentDataMapping: splitPanelContext.parentDataMapping,
});
} else {
splitPanelContext.setSelectedLeftData(null);
console.log("[CardDisplay] 분할 패널 좌측 데이터 초기화");
}
}
}, [displayData, getCardKey, onFormDataChange, componentConfig.dataSource?.tableName, tableName, splitPanelContext, splitPanelPosition]);
@@ -540,21 +563,38 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
}, [categoryMappings]);
// 필터가 변경되면 데이터 다시 로드 (테이블 리스트와 동일한 패턴)
// 초기 로드 여부 추적
const isInitialLoadRef = useRef(true);
// 초기 로드 여부 추적 - 마운트 카운터 사용 (Strict Mode 대응)
const mountCountRef = useRef(0);
useEffect(() => {
mountCountRef.current += 1;
const currentMount = mountCountRef.current;
if (!tableNameToUse || isDesignMode) return;
// 초기 로드는 별도 useEffect에서 처리하므로 스킵
if (isInitialLoadRef.current) {
isInitialLoadRef.current = false;
// 우측 패널이고 linkedFilters가 설정되어 있지만 좌측 데이터가 선택되지 않은 경우 스킵
const isRightPanel = splitPanelPosition === "right" ||
(screenId && splitPanelContext?.getPositionByScreenId?.(screenId as number) === "right");
const hasLinkedFiltersInConfig = splitPanelContext?.linkedFilters && splitPanelContext.linkedFilters.length > 0;
const hasSelectedLeftData = splitPanelContext?.selectedLeftData &&
Object.keys(splitPanelContext.selectedLeftData).length > 0;
// 우측 패널이고 좌측 데이터가 선택되지 않은 경우 - 기존 데이터 유지 (깜빡임 방지)
if (isRightPanel && !hasSelectedLeftData) {
// 데이터를 지우지 않고 로딩만 false로 설정
setLoading(false);
return;
}
// 첫 2번의 마운트는 초기 로드 useEffect에서 처리 (Strict Mode에서 2번 호출됨)
// 필터 변경이 아닌 경우 스킵
if (currentMount <= 2 && filters.length === 0) {
return;
}
const loadFilteredData = async () => {
try {
setLoading(true);
// 로딩 상태를 true로 설정하지 않음 - 기존 데이터 유지하면서 새 데이터 로드 (깜빡임 방지)
// 필터 값을 검색 파라미터로 변환
const searchParams: Record<string, any> = {};
@@ -564,12 +604,6 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
}
});
console.log("🔍 [CardDisplay] 필터 적용 데이터 로드:", {
tableName: tableNameToUse,
filtersCount: filters.length,
searchParams,
});
// search 파라미터로 검색 조건 전달 (API 스펙에 맞게)
const dataResponse = await tableTypeApi.getTableData(tableNameToUse, {
page: 1,
@@ -584,16 +618,14 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
tableOptionsContext.updateTableDataCount(tableId, dataResponse.data?.length || 0);
}
} catch (error) {
console.error("❌ [CardDisplay] 필터 적용 실패:", error);
} finally {
setLoading(false);
// 필터 적용 실패 시 무시
}
};
// 필터 변경 시 항상 데이터 다시 로드 (빈 필터 = 전체 데이터)
loadFilteredData();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [filters, tableNameToUse, isDesignMode, tableId]);
}, [filters, tableNameToUse, isDesignMode, tableId, splitPanelContext?.selectedLeftData, splitPanelPosition]);
// 컬럼 고유 값 조회 함수 (select 타입 필터용)
const getColumnUniqueValues = useCallback(async (columnName: string): Promise<Array<{ label: string; value: string }>> => {
@@ -616,7 +648,6 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
label: mapping?.[value]?.label || value,
}));
} catch (error) {
console.error(`❌ [CardDisplay] 고유 값 조회 실패: ${columnName}`, error);
return [];
}
}, [tableNameToUse]);
@@ -663,10 +694,6 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
// onFilterChange는 ref를 통해 최신 함수를 호출하는 래퍼 사용
const onFilterChangeWrapper = (newFilters: TableFilter[]) => {
console.log("🎴 [CardDisplay] onFilterChange 래퍼 호출:", {
tableId,
filtersCount: newFilters.length,
});
setFiltersRef.current(newFilters);
};
@@ -686,20 +713,12 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
getColumnUniqueValues: getColumnUniqueValuesWrapper,
};
console.log("📋 [CardDisplay] TableOptionsContext에 등록:", {
tableId,
tableName: tableNameToUse,
columnsCount: columns.length,
dataCount: loadedTableData.length,
});
registerTableRef.current(registration);
const unregister = unregisterTableRef.current;
const currentTableId = tableId;
return () => {
console.log("📋 [CardDisplay] TableOptionsContext에서 해제:", currentTableId);
unregister(currentTableId);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -711,8 +730,34 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
columnsKey, // 컬럼 변경 시에만 재등록
]);
// 로딩 중인 경우 로딩 표시
if (loading) {
// 우측 패널이고 좌측 데이터가 한 번도 선택된 적이 없는 경우에만 "선택해주세요" 표시
// 한 번이라도 선택된 적이 있으면 로딩 중에도 기존 데이터 유지 (깜빡임 방지)
if (shouldHideDataForRightPanel) {
return (
<div
className={className}
style={{
...componentStyle,
...style,
display: "flex",
alignItems: "center",
justifyContent: "center",
padding: "20px",
background: "#f8fafc",
borderRadius: "12px",
}}
>
<div className="text-muted-foreground text-center">
<div className="text-lg mb-2"> </div>
<div className="text-sm text-gray-400"> </div>
</div>
</div>
);
}
// 로딩 중이고 데이터가 없는 경우에만 로딩 표시
// 데이터가 있으면 로딩 중에도 기존 데이터 유지 (깜빡임 방지)
if (loading && displayData.length === 0 && !hasEverSelectedLeftData) {
return (
<div
className={className}