feat(SplitPanelLayout2): 좌우 패널 수정/삭제 기능 및 모달 자동 닫기 구현

- 좌측 패널에 수정/삭제 버튼 기능 추가
- 좌측 패널 설정에 개별 수정/삭제 UI 추가
- 삭제 API 호출을 백엔드 라우트에 맞게 수정 (DELETE /tables/{tableName}/delete)
- UniversalFormModal 저장 완료 후 closeEditModal 이벤트 발생하여 모달 자동 닫기
- ModalConfig에 showSaveButton 타입 추가
This commit is contained in:
SeongHyun Kim
2025-12-24 14:01:38 +09:00
parent 171ed6e938
commit 486e5ee29b
5 changed files with 341 additions and 75 deletions

View File

@@ -86,6 +86,7 @@ export const SplitPanelLayout2Component: React.FC<SplitPanelLayout2ComponentProp
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [itemToDelete, setItemToDelete] = useState<any>(null);
const [isBulkDelete, setIsBulkDelete] = useState(false);
const [deleteTargetPanel, setDeleteTargetPanel] = useState<"left" | "right">("right");
// 탭 상태 (좌측/우측 각각)
const [leftActiveTab, setLeftActiveTab] = useState<string | null>(null);
@@ -637,9 +638,6 @@ export const SplitPanelLayout2Component: React.FC<SplitPanelLayout2ComponentProp
}
}
console.log("[SplitPanelLayout2] 모달로 전달할 데이터:", initialData);
console.log("[SplitPanelLayout2] 모달 screenId:", config.rightPanel?.addModalScreenId);
// EditModal 열기 이벤트 발생
const event = new CustomEvent("openEditModal", {
detail: {
@@ -665,11 +663,16 @@ export const SplitPanelLayout2Component: React.FC<SplitPanelLayout2ComponentProp
loadRightData,
]);
// 기본키 컬럼명 가져오기
// 기본키 컬럼명 가져오기 (우측 패널)
const getPrimaryKeyColumn = useCallback(() => {
return config.rightPanel?.primaryKeyColumn || "id";
}, [config.rightPanel?.primaryKeyColumn]);
// 기본키 컬럼명 가져오기 (좌측 패널)
const getLeftPrimaryKeyColumn = useCallback(() => {
return config.leftPanel?.primaryKeyColumn || config.leftPanel?.hierarchyConfig?.idColumn || "id";
}, [config.leftPanel?.primaryKeyColumn, config.leftPanel?.hierarchyConfig?.idColumn]);
// 우측 패널 수정 버튼 클릭
const handleEditItem = useCallback(
(item: any) => {
@@ -697,15 +700,54 @@ export const SplitPanelLayout2Component: React.FC<SplitPanelLayout2ComponentProp
},
});
window.dispatchEvent(event);
console.log("[SplitPanelLayout2] 수정 모달 열기:", item);
console.log("[SplitPanelLayout2] 우측 수정 모달 열기:", item);
},
[config.rightPanel?.editModalScreenId, config.rightPanel?.addModalScreenId, selectedLeftItem, loadRightData],
);
// 좌측 패널 수정 버튼 클릭
const handleLeftEditItem = useCallback(
(item: any) => {
// 수정용 모달 화면 ID 결정 (editModalScreenId 우선, 없으면 addModalScreenId 사용)
const modalScreenId = config.leftPanel?.editModalScreenId || config.leftPanel?.addModalScreenId;
if (!modalScreenId) {
toast.error("연결된 모달 화면이 없습니다.");
return;
}
// EditModal 열기 이벤트 발생 (수정 모드)
const event = new CustomEvent("openEditModal", {
detail: {
screenId: modalScreenId,
title: "수정",
modalSize: "lg",
editData: item, // 기존 데이터 전달
isCreateMode: false, // 수정 모드
onSave: () => {
loadLeftData();
},
},
});
window.dispatchEvent(event);
console.log("[SplitPanelLayout2] 좌측 수정 모달 열기:", item);
},
[config.leftPanel?.editModalScreenId, config.leftPanel?.addModalScreenId, loadLeftData],
);
// 우측 패널 삭제 버튼 클릭 (확인 다이얼로그 표시)
const handleDeleteClick = useCallback((item: any) => {
setItemToDelete(item);
setIsBulkDelete(false);
setDeleteTargetPanel("right");
setDeleteDialogOpen(true);
}, []);
// 좌측 패널 삭제 버튼 클릭 (확인 다이얼로그 표시)
const handleLeftDeleteClick = useCallback((item: any) => {
setItemToDelete(item);
setIsBulkDelete(false);
setDeleteTargetPanel("left");
setDeleteDialogOpen(true);
}, []);
@@ -716,41 +758,54 @@ export const SplitPanelLayout2Component: React.FC<SplitPanelLayout2ComponentProp
return;
}
setIsBulkDelete(true);
setDeleteTargetPanel("right");
setDeleteDialogOpen(true);
}, [selectedRightItems.size]);
// 실제 삭제 실행
const executeDelete = useCallback(async () => {
if (!config.rightPanel?.tableName) {
// 대상 패널에 따라 테이블명과 기본키 컬럼 결정
const tableName = deleteTargetPanel === "left"
? config.leftPanel?.tableName
: config.rightPanel?.tableName;
const pkColumn = deleteTargetPanel === "left"
? getLeftPrimaryKeyColumn()
: getPrimaryKeyColumn();
if (!tableName) {
toast.error("테이블 설정이 없습니다.");
return;
}
const pkColumn = getPrimaryKeyColumn();
try {
if (isBulkDelete) {
// 일괄 삭제
const idsToDelete = Array.from(selectedRightItems);
console.log("[SplitPanelLayout2] 일괄 삭제:", idsToDelete);
// 일괄 삭제 - 선택된 항목들의 데이터를 body로 전달
const itemsToDelete = rightData.filter((item) => selectedRightItems.has(item[pkColumn] as string | number));
console.log("[SplitPanelLayout2] 일괄 삭제:", itemsToDelete);
for (const id of idsToDelete) {
await apiClient.delete(`/table-management/tables/${config.rightPanel.tableName}/data/${id}`);
}
// 백엔드 API는 body로 삭제할 데이터를 받음
await apiClient.delete(`/table-management/tables/${tableName}/delete`, {
data: itemsToDelete,
});
toast.success(`${idsToDelete.length}개 항목이 삭제되었습니다.`);
setSelectedRightItems(new Set());
toast.success(`${itemsToDelete.length}개 항목이 삭제되었습니다.`);
setSelectedRightItems(new Set<string | number>());
} else if (itemToDelete) {
// 단일 삭제
const itemId = itemToDelete[pkColumn];
console.log("[SplitPanelLayout2] 단일 삭제:", itemId);
// 단일 삭제 - 해당 항목 데이터를 body로 전달
console.log(`[SplitPanelLayout2] ${deleteTargetPanel === "left" ? "좌측" : "우측"} 단일 삭제:`, itemToDelete);
await apiClient.delete(`/table-management/tables/${config.rightPanel.tableName}/data/${itemId}`);
await apiClient.delete(`/table-management/tables/${tableName}/delete`, {
data: itemToDelete,
});
toast.success("항목이 삭제되었습니다.");
}
// 데이터 새로고침
if (selectedLeftItem) {
if (deleteTargetPanel === "left") {
loadLeftData();
setSelectedLeftItem(null); // 좌측 선택 초기화
setRightData([]); // 우측 데이터도 초기화
} else if (selectedLeftItem) {
loadRightData(selectedLeftItem);
}
} catch (error: any) {
@@ -762,13 +817,18 @@ export const SplitPanelLayout2Component: React.FC<SplitPanelLayout2ComponentProp
setIsBulkDelete(false);
}
}, [
deleteTargetPanel,
config.leftPanel?.tableName,
config.rightPanel?.tableName,
getLeftPrimaryKeyColumn,
getPrimaryKeyColumn,
isBulkDelete,
selectedRightItems,
itemToDelete,
selectedLeftItem,
loadLeftData,
loadRightData,
rightData,
]);
// 개별 체크박스 선택/해제
@@ -825,7 +885,29 @@ export const SplitPanelLayout2Component: React.FC<SplitPanelLayout2ComponentProp
const selectedId = Array.from(selectedRightItems)[0];
const item = rightData.find((d) => d[pkColumn] === selectedId);
if (item) {
handleEditItem(item);
// 액션 버튼에 모달 화면이 설정되어 있으면 해당 화면 사용
const modalScreenId = btn.modalScreenId || config.rightPanel?.editModalScreenId || config.rightPanel?.addModalScreenId;
if (!modalScreenId) {
toast.error("연결된 모달 화면이 없습니다.");
return;
}
const event = new CustomEvent("openEditModal", {
detail: {
screenId: modalScreenId,
title: btn.label || "수정",
modalSize: "lg",
editData: item,
isCreateMode: false,
onSave: () => {
if (selectedLeftItem) {
loadRightData(selectedLeftItem);
}
},
},
});
window.dispatchEvent(event);
}
} else if (selectedRightItems.size > 1) {
toast.error("수정할 항목을 1개만 선택해주세요.");
@@ -860,6 +942,57 @@ export const SplitPanelLayout2Component: React.FC<SplitPanelLayout2ComponentProp
],
);
// 좌측 패널 액션 버튼 클릭 핸들러
const handleLeftActionButton = useCallback(
(btn: ActionButtonConfig) => {
switch (btn.action) {
case "add":
// 액션 버튼에 설정된 modalScreenId 우선 사용
const modalScreenId = btn.modalScreenId || config.leftPanel?.addModalScreenId;
if (!modalScreenId) {
toast.error("연결된 모달 화면이 없습니다.");
return;
}
// EditModal 열기 이벤트 발생
const event = new CustomEvent("openEditModal", {
detail: {
screenId: modalScreenId,
title: btn.label || "추가",
modalSize: "lg",
editData: {},
isCreateMode: true,
onSave: () => {
loadLeftData();
},
},
});
window.dispatchEvent(event);
console.log("[SplitPanelLayout2] 좌측 액션 버튼 모달 열기:", modalScreenId);
break;
case "edit":
// 좌측 패널에서 수정 (필요시 구현)
console.log("[SplitPanelLayout2] 좌측 수정 액션:", btn);
break;
case "delete":
// 좌측 패널에서 삭제 (필요시 구현)
console.log("[SplitPanelLayout2] 좌측 삭제 액션:", btn);
break;
case "custom":
console.log("[SplitPanelLayout2] 좌측 커스텀 액션:", btn);
break;
default:
break;
}
},
[config.leftPanel?.addModalScreenId, loadLeftData],
);
// 컬럼 라벨 로드
const loadColumnLabels = useCallback(
async (tableName: string, setLabels: (labels: Record<string, string>) => void) => {
@@ -1012,10 +1145,10 @@ export const SplitPanelLayout2Component: React.FC<SplitPanelLayout2ComponentProp
(checked: boolean) => {
if (checked) {
const pkColumn = getPrimaryKeyColumn();
const allIds = new Set(filteredRightData.map((item) => item[pkColumn]));
const allIds = new Set<string | number>(filteredRightData.map((item) => item[pkColumn] as string | number));
setSelectedRightItems(allIds);
} else {
setSelectedRightItems(new Set());
setSelectedRightItems(new Set<string | number>());
}
},
[filteredRightData, getPrimaryKeyColumn],
@@ -1348,6 +1481,27 @@ export const SplitPanelLayout2Component: React.FC<SplitPanelLayout2ComponentProp
</div>
)}
</div>
{/* 좌측 패널 수정/삭제 버튼 */}
{(config.leftPanel?.showEditButton || config.leftPanel?.showDeleteButton) && (
<div className="flex gap-1" onClick={(e) => e.stopPropagation()}>
{config.leftPanel?.showEditButton && (
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={() => handleLeftEditItem(item)}>
<Edit className="h-4 w-4" />
</Button>
)}
{config.leftPanel?.showDeleteButton && (
<Button
variant="ghost"
size="icon"
className="text-destructive hover:text-destructive h-8 w-8"
onClick={() => handleLeftDeleteClick(item)}
>
<Trash2 className="h-4 w-4" />
</Button>
)}
</div>
)}
</div>
{/* 자식 항목 */}
@@ -1360,11 +1514,6 @@ export const SplitPanelLayout2Component: React.FC<SplitPanelLayout2ComponentProp
);
};
// 왼쪽 패널 기본키 컬럼명 가져오기
const getLeftPrimaryKeyColumn = useCallback(() => {
return config.leftPanel?.primaryKeyColumn || config.leftPanel?.hierarchyConfig?.idColumn || "id";
}, [config.leftPanel?.primaryKeyColumn, config.leftPanel?.hierarchyConfig?.idColumn]);
// 왼쪽 패널 테이블 렌더링
const renderLeftTable = () => {
const displayColumns = config.leftPanel?.displayColumns || [];
@@ -1586,8 +1735,8 @@ export const SplitPanelLayout2Component: React.FC<SplitPanelLayout2ComponentProp
const showCheckbox = config.rightPanel?.showCheckbox ?? true; // 테이블 모드는 기본 체크박스 표시
const pkColumn = getPrimaryKeyColumn();
const allSelected =
filteredRightData.length > 0 && filteredRightData.every((item) => selectedRightItems.has(item[pkColumn]));
const someSelected = filteredRightData.some((item) => selectedRightItems.has(item[pkColumn]));
filteredRightData.length > 0 && filteredRightData.every((item) => selectedRightItems.has(item[pkColumn] as string | number));
const someSelected = filteredRightData.some((item) => selectedRightItems.has(item[pkColumn] as string | number));
return (
<div className="rounded-md border">
@@ -1633,7 +1782,7 @@ export const SplitPanelLayout2Component: React.FC<SplitPanelLayout2ComponentProp
</TableRow>
) : (
filteredRightData.map((item, index) => {
const itemId = item[pkColumn];
const itemId = item[pkColumn] as string | number;
return (
<TableRow key={index} className="hover:bg-muted/50">
{showCheckbox && (
@@ -1962,11 +2111,7 @@ export const SplitPanelLayout2Component: React.FC<SplitPanelLayout2ComponentProp
size="sm"
variant={btn.variant || "default"}
className="h-8 text-sm"
onClick={() => {
if (btn.action === "add") {
handleLeftAddClick();
}
}}
onClick={() => handleLeftActionButton(btn)}
>
{btn.icon && <span className="mr-1">{btn.icon}</span>}
{btn.label || "버튼"}