카드 디스플레이 삭제기능 구현

This commit is contained in:
kjs 2025-12-15 18:29:18 +09:00
parent 6449eb5ac3
commit cb38864ad8
4 changed files with 161 additions and 6 deletions

View File

@ -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<InteractiveDataTableProps> = ({
const { user } = useAuth(); // 사용자 정보 가져오기
const { registerTable, unregisterTable } = useTableOptions(); // Context 훅
const splitPanelContext = useSplitPanelContext(); // 분할 패널 컨텍스트
const screenContext = useScreenContextOptional(); // 화면 컨텍스트 (좌측/우측 위치 확인용)
const splitPanelPosition = screenContext?.splitPanelPosition; // 분할 패널 내 위치
const [data, setData] = useState<Record<string, any>[]>([]);
const [loading, setLoading] = useState(false);
@ -947,7 +950,18 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
}
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(

View File

@ -108,6 +108,65 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
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<CardDisplayComponentProps> = ({
return;
}
// 연결 필터 확인 (분할 패널 내부일 때)
let linkedFilterValues: Record<string, any> = {};
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<string, any> = {};
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<string, any> = {
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<CardDisplayComponentProps> = ({
};
loadTableData();
}, [isDesignMode, tableName, component.componentConfig?.tableName]);
}, [isDesignMode, tableName, component.componentConfig?.tableName, splitPanelContext?.selectedLeftData]);
// 컴포넌트 설정 (기본값 보장)
const componentConfig = {
@ -957,6 +1073,17 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
</button>
)}
{(componentConfig.cardStyle?.showDeleteButton ?? false) && (
<button
className="text-xs text-red-500 hover:text-red-700 transition-colors"
onClick={(e) => {
e.stopPropagation();
handleCardDelete(data, index);
}}
>
</button>
)}
</div>
)}
</div>

View File

@ -306,6 +306,19 @@ export const CardDisplayConfigPanel: React.FC<CardDisplayConfigPanelProps> = ({
</label>
</div>
<div className="flex items-center space-x-2">
<input
type="checkbox"
id="showDeleteButton"
checked={config.cardStyle?.showDeleteButton ?? false}
onChange={(e) => handleNestedChange("cardStyle.showDeleteButton", e.target.checked)}
className="rounded border-gray-300"
/>
<label htmlFor="showDeleteButton" className="text-xs text-gray-600">
</label>
</div>
</div>
)}
</div>

View File

@ -16,6 +16,7 @@ export interface CardStyleConfig {
showActions?: boolean; // 액션 버튼 표시 여부 (전체)
showViewButton?: boolean; // 상세보기 버튼 표시 여부
showEditButton?: boolean; // 편집 버튼 표시 여부
showDeleteButton?: boolean; // 삭제 버튼 표시 여부
}
/**