diff --git a/frontend/components/screen/InteractiveDataTable.tsx b/frontend/components/screen/InteractiveDataTable.tsx index 1abc44bc..e44a356c 100644 --- a/frontend/components/screen/InteractiveDataTable.tsx +++ b/frontend/components/screen/InteractiveDataTable.tsx @@ -55,6 +55,7 @@ import { useScreenPreview } from "@/contexts/ScreenPreviewContext"; import { useTableOptions } from "@/contexts/TableOptionsContext"; import { TableFilter, ColumnVisibility } from "@/types/table-options"; import { useSplitPanelContext } from "@/contexts/SplitPanelContext"; +import { useScreenContextOptional } from "@/contexts/ScreenContext"; import { useCascadingDropdown } from "@/hooks/useCascadingDropdown"; import { CascadingDropdownConfig } from "@/types/screen-management"; @@ -184,6 +185,8 @@ export const InteractiveDataTable: React.FC = ({ const { user } = useAuth(); // 사용자 정보 가져오기 const { registerTable, unregisterTable } = useTableOptions(); // Context 훅 const splitPanelContext = useSplitPanelContext(); // 분할 패널 컨텍스트 + const screenContext = useScreenContextOptional(); // 화면 컨텍스트 (좌측/우측 위치 확인용) + const splitPanelPosition = screenContext?.splitPanelPosition; // 분할 패널 내 위치 const [data, setData] = useState[]>([]); const [loading, setLoading] = useState(false); @@ -947,7 +950,18 @@ export const InteractiveDataTable: React.FC = ({ } return newSet; }); - }, []); + + // 분할 패널 좌측에서 선택 시 컨텍스트에 데이터 저장 (연결 필터용) + if (splitPanelContext && splitPanelPosition === "left" && !splitPanelContext.disableAutoDataTransfer) { + if (isSelected && data[rowIndex]) { + splitPanelContext.setSelectedLeftData(data[rowIndex]); + console.log("🔗 [InteractiveDataTable] 좌측 선택 데이터 저장:", data[rowIndex]); + } else if (!isSelected) { + splitPanelContext.setSelectedLeftData(null); + console.log("🔗 [InteractiveDataTable] 좌측 선택 데이터 초기화"); + } + } + }, [data, splitPanelContext, splitPanelPosition]); // 전체 선택/해제 핸들러 const handleSelectAll = useCallback( diff --git a/frontend/lib/registry/components/card-display/CardDisplayComponent.tsx b/frontend/lib/registry/components/card-display/CardDisplayComponent.tsx index a3876188..54b32143 100644 --- a/frontend/lib/registry/components/card-display/CardDisplayComponent.tsx +++ b/frontend/lib/registry/components/card-display/CardDisplayComponent.tsx @@ -108,6 +108,65 @@ export const CardDisplayComponent: React.FC = ({ setEditModalOpen(true); }; + // 삭제 핸들러 + const handleCardDelete = async (data: any, index: number) => { + // 사용자 확인 + if (!confirm("정말로 이 항목을 삭제하시겠습니까?")) { + return; + } + + try { + const tableNameToUse = tableName || component.componentConfig?.tableName; + if (!tableNameToUse) { + alert("테이블 정보가 없습니다."); + return; + } + + // 삭제할 데이터를 배열로 감싸기 (API가 배열을 기대함) + const deleteData = [data]; + + console.log("🗑️ [CardDisplay] 삭제 요청:", { + tableName: tableNameToUse, + data: deleteData, + }); + + // API 호출로 데이터 삭제 (POST 방식으로 변경 - DELETE는 body 전달이 불안정) + // 백엔드 API는 DELETE /api/table-management/tables/:tableName/delete 이지만 + // axios에서 DELETE body 전달 문제가 있어 직접 request 설정 사용 + const response = await apiClient.request({ + method: 'DELETE', + url: `/table-management/tables/${tableNameToUse}/delete`, + data: deleteData, + headers: { + 'Content-Type': 'application/json', + }, + }); + + if (response.data.success) { + console.log("삭제 완료:", response.data.data?.deletedCount || 1, "건"); + alert("삭제되었습니다."); + + // 로컬 상태에서 삭제된 항목 제거 + setLoadedTableData(prev => prev.filter((item, idx) => idx !== index)); + + // 선택된 항목이면 선택 해제 + const cardKey = getCardKey(data, index); + if (selectedRows.has(cardKey)) { + const newSelectedRows = new Set(selectedRows); + newSelectedRows.delete(cardKey); + 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}`); + } + }; + // 편집 폼 데이터 변경 핸들러 const handleEditFormChange = (key: string, value: string) => { setEditData((prev: any) => ({ @@ -155,15 +214,72 @@ export const CardDisplayComponent: React.FC = ({ return; } + // 연결 필터 확인 (분할 패널 내부일 때) + let linkedFilterValues: Record = {}; + let hasLinkedFiltersConfigured = false; + let hasSelectedLeftData = false; + + if (splitPanelContext) { + // 연결 필터 설정 여부 확인 (현재 테이블에 해당하는 필터가 있는지) + const linkedFiltersConfig = splitPanelContext.linkedFilters || []; + hasLinkedFiltersConfigured = linkedFiltersConfig.some( + (filter) => filter.targetColumn?.startsWith(tableNameToUse + ".") || + filter.targetColumn === tableNameToUse + ); + + // 좌측 데이터 선택 여부 확인 + hasSelectedLeftData = splitPanelContext.selectedLeftData && + Object.keys(splitPanelContext.selectedLeftData).length > 0; + + linkedFilterValues = splitPanelContext.getLinkedFilterValues(); + // 현재 테이블에 해당하는 필터만 추출 (테이블명.컬럼명 형식에서) + const tableSpecificFilters: Record = {}; + for (const [key, value] of Object.entries(linkedFilterValues)) { + // key가 "테이블명.컬럼명" 형식인 경우 + if (key.includes(".")) { + const [tblName, columnName] = key.split("."); + if (tblName === tableNameToUse) { + tableSpecificFilters[columnName] = value; + hasLinkedFiltersConfigured = true; + } + } else { + // 테이블명 없이 컬럼명만 있는 경우 그대로 사용 + tableSpecificFilters[key] = value; + } + } + linkedFilterValues = tableSpecificFilters; + + console.log("🎴 [CardDisplay] 연결 필터 확인:", { + tableNameToUse, + hasLinkedFiltersConfigured, + hasSelectedLeftData, + linkedFilterValues, + }); + } + + // 연결 필터가 설정되어 있지만 좌측에서 데이터가 선택되지 않은 경우 빈 데이터 표시 + if (splitPanelContext && hasLinkedFiltersConfigured && !hasSelectedLeftData) { + console.log("🎴 [CardDisplay] 연결 필터 활성화됨 - 좌측 선택 대기"); + setLoadedTableData([]); + setLoading(false); + return; + } + try { setLoading(true); + // API 호출 파라미터에 연결 필터 추가 (search 객체 안에 넣어야 함) + const apiParams: Record = { + page: 1, + size: 50, // 카드 표시용으로 적당한 개수 + search: Object.keys(linkedFilterValues).length > 0 ? linkedFilterValues : undefined, + }; + + console.log("🎴 [CardDisplay] API 호출 파라미터:", apiParams); + // 테이블 데이터, 컬럼 정보, 입력 타입 정보를 병렬로 로드 const [dataResponse, columnsResponse, inputTypesResponse] = await Promise.all([ - tableTypeApi.getTableData(tableNameToUse, { - page: 1, - size: 50, // 카드 표시용으로 적당한 개수 - }), + tableTypeApi.getTableData(tableNameToUse, apiParams), tableTypeApi.getColumns(tableNameToUse), tableTypeApi.getColumnInputTypes(tableNameToUse), ]); @@ -232,7 +348,7 @@ export const CardDisplayComponent: React.FC = ({ }; loadTableData(); - }, [isDesignMode, tableName, component.componentConfig?.tableName]); + }, [isDesignMode, tableName, component.componentConfig?.tableName, splitPanelContext?.selectedLeftData]); // 컴포넌트 설정 (기본값 보장) const componentConfig = { @@ -957,6 +1073,17 @@ export const CardDisplayComponent: React.FC = ({ 편집 )} + {(componentConfig.cardStyle?.showDeleteButton ?? false) && ( + + )} )} diff --git a/frontend/lib/registry/components/card-display/CardDisplayConfigPanel.tsx b/frontend/lib/registry/components/card-display/CardDisplayConfigPanel.tsx index beff4783..52889865 100644 --- a/frontend/lib/registry/components/card-display/CardDisplayConfigPanel.tsx +++ b/frontend/lib/registry/components/card-display/CardDisplayConfigPanel.tsx @@ -306,6 +306,19 @@ export const CardDisplayConfigPanel: React.FC = ({ 편집 버튼 + +
+ handleNestedChange("cardStyle.showDeleteButton", e.target.checked)} + className="rounded border-gray-300" + /> + +
)} diff --git a/frontend/lib/registry/components/card-display/types.ts b/frontend/lib/registry/components/card-display/types.ts index 7154eb72..d4174453 100644 --- a/frontend/lib/registry/components/card-display/types.ts +++ b/frontend/lib/registry/components/card-display/types.ts @@ -16,6 +16,7 @@ export interface CardStyleConfig { showActions?: boolean; // 액션 버튼 표시 여부 (전체) showViewButton?: boolean; // 상세보기 버튼 표시 여부 showEditButton?: boolean; // 편집 버튼 표시 여부 + showDeleteButton?: boolean; // 삭제 버튼 표시 여부 } /**