feat(SplitPanelLayout2): 좌우 패널 수정/삭제 기능 및 모달 자동 닫기 구현
- 좌측 패널에 수정/삭제 버튼 기능 추가
- 좌측 패널 설정에 개별 수정/삭제 UI 추가
- 삭제 API 호출을 백엔드 라우트에 맞게 수정 (DELETE /tables/{tableName}/delete)
- UniversalFormModal 저장 완료 후 closeEditModal 이벤트 발생하여 모달 자동 닫기
- ModalConfig에 showSaveButton 타입 추가
This commit is contained in:
@@ -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 || "버튼"}
|
||||
|
||||
Reference in New Issue
Block a user