우측화면 데이터 필터링 수정
This commit is contained in:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user