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 [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||||
const [itemToDelete, setItemToDelete] = useState<any>(null);
|
const [itemToDelete, setItemToDelete] = useState<any>(null);
|
||||||
const [isBulkDelete, setIsBulkDelete] = useState(false);
|
const [isBulkDelete, setIsBulkDelete] = useState(false);
|
||||||
|
const [deleteTargetPanel, setDeleteTargetPanel] = useState<"left" | "right">("right");
|
||||||
|
|
||||||
// 탭 상태 (좌측/우측 각각)
|
// 탭 상태 (좌측/우측 각각)
|
||||||
const [leftActiveTab, setLeftActiveTab] = useState<string | null>(null);
|
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 열기 이벤트 발생
|
// EditModal 열기 이벤트 발생
|
||||||
const event = new CustomEvent("openEditModal", {
|
const event = new CustomEvent("openEditModal", {
|
||||||
detail: {
|
detail: {
|
||||||
@@ -665,11 +663,16 @@ export const SplitPanelLayout2Component: React.FC<SplitPanelLayout2ComponentProp
|
|||||||
loadRightData,
|
loadRightData,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// 기본키 컬럼명 가져오기
|
// 기본키 컬럼명 가져오기 (우측 패널)
|
||||||
const getPrimaryKeyColumn = useCallback(() => {
|
const getPrimaryKeyColumn = useCallback(() => {
|
||||||
return config.rightPanel?.primaryKeyColumn || "id";
|
return config.rightPanel?.primaryKeyColumn || "id";
|
||||||
}, [config.rightPanel?.primaryKeyColumn]);
|
}, [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(
|
const handleEditItem = useCallback(
|
||||||
(item: any) => {
|
(item: any) => {
|
||||||
@@ -697,15 +700,54 @@ export const SplitPanelLayout2Component: React.FC<SplitPanelLayout2ComponentProp
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
window.dispatchEvent(event);
|
window.dispatchEvent(event);
|
||||||
console.log("[SplitPanelLayout2] 수정 모달 열기:", item);
|
console.log("[SplitPanelLayout2] 우측 수정 모달 열기:", item);
|
||||||
},
|
},
|
||||||
[config.rightPanel?.editModalScreenId, config.rightPanel?.addModalScreenId, selectedLeftItem, loadRightData],
|
[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) => {
|
const handleDeleteClick = useCallback((item: any) => {
|
||||||
setItemToDelete(item);
|
setItemToDelete(item);
|
||||||
setIsBulkDelete(false);
|
setIsBulkDelete(false);
|
||||||
|
setDeleteTargetPanel("right");
|
||||||
|
setDeleteDialogOpen(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 좌측 패널 삭제 버튼 클릭 (확인 다이얼로그 표시)
|
||||||
|
const handleLeftDeleteClick = useCallback((item: any) => {
|
||||||
|
setItemToDelete(item);
|
||||||
|
setIsBulkDelete(false);
|
||||||
|
setDeleteTargetPanel("left");
|
||||||
setDeleteDialogOpen(true);
|
setDeleteDialogOpen(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -716,41 +758,54 @@ export const SplitPanelLayout2Component: React.FC<SplitPanelLayout2ComponentProp
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setIsBulkDelete(true);
|
setIsBulkDelete(true);
|
||||||
|
setDeleteTargetPanel("right");
|
||||||
setDeleteDialogOpen(true);
|
setDeleteDialogOpen(true);
|
||||||
}, [selectedRightItems.size]);
|
}, [selectedRightItems.size]);
|
||||||
|
|
||||||
// 실제 삭제 실행
|
// 실제 삭제 실행
|
||||||
const executeDelete = useCallback(async () => {
|
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("테이블 설정이 없습니다.");
|
toast.error("테이블 설정이 없습니다.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const pkColumn = getPrimaryKeyColumn();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (isBulkDelete) {
|
if (isBulkDelete) {
|
||||||
// 일괄 삭제
|
// 일괄 삭제 - 선택된 항목들의 데이터를 body로 전달
|
||||||
const idsToDelete = Array.from(selectedRightItems);
|
const itemsToDelete = rightData.filter((item) => selectedRightItems.has(item[pkColumn] as string | number));
|
||||||
console.log("[SplitPanelLayout2] 일괄 삭제:", idsToDelete);
|
console.log("[SplitPanelLayout2] 일괄 삭제:", itemsToDelete);
|
||||||
|
|
||||||
for (const id of idsToDelete) {
|
// 백엔드 API는 body로 삭제할 데이터를 받음
|
||||||
await apiClient.delete(`/table-management/tables/${config.rightPanel.tableName}/data/${id}`);
|
await apiClient.delete(`/table-management/tables/${tableName}/delete`, {
|
||||||
}
|
data: itemsToDelete,
|
||||||
|
});
|
||||||
|
|
||||||
toast.success(`${idsToDelete.length}개 항목이 삭제되었습니다.`);
|
toast.success(`${itemsToDelete.length}개 항목이 삭제되었습니다.`);
|
||||||
setSelectedRightItems(new Set());
|
setSelectedRightItems(new Set<string | number>());
|
||||||
} else if (itemToDelete) {
|
} else if (itemToDelete) {
|
||||||
// 단일 삭제
|
// 단일 삭제 - 해당 항목 데이터를 body로 전달
|
||||||
const itemId = itemToDelete[pkColumn];
|
console.log(`[SplitPanelLayout2] ${deleteTargetPanel === "left" ? "좌측" : "우측"} 단일 삭제:`, itemToDelete);
|
||||||
console.log("[SplitPanelLayout2] 단일 삭제:", itemId);
|
|
||||||
|
|
||||||
await apiClient.delete(`/table-management/tables/${config.rightPanel.tableName}/data/${itemId}`);
|
await apiClient.delete(`/table-management/tables/${tableName}/delete`, {
|
||||||
|
data: itemToDelete,
|
||||||
|
});
|
||||||
toast.success("항목이 삭제되었습니다.");
|
toast.success("항목이 삭제되었습니다.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 데이터 새로고침
|
// 데이터 새로고침
|
||||||
if (selectedLeftItem) {
|
if (deleteTargetPanel === "left") {
|
||||||
|
loadLeftData();
|
||||||
|
setSelectedLeftItem(null); // 좌측 선택 초기화
|
||||||
|
setRightData([]); // 우측 데이터도 초기화
|
||||||
|
} else if (selectedLeftItem) {
|
||||||
loadRightData(selectedLeftItem);
|
loadRightData(selectedLeftItem);
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@@ -762,13 +817,18 @@ export const SplitPanelLayout2Component: React.FC<SplitPanelLayout2ComponentProp
|
|||||||
setIsBulkDelete(false);
|
setIsBulkDelete(false);
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
|
deleteTargetPanel,
|
||||||
|
config.leftPanel?.tableName,
|
||||||
config.rightPanel?.tableName,
|
config.rightPanel?.tableName,
|
||||||
|
getLeftPrimaryKeyColumn,
|
||||||
getPrimaryKeyColumn,
|
getPrimaryKeyColumn,
|
||||||
isBulkDelete,
|
isBulkDelete,
|
||||||
selectedRightItems,
|
selectedRightItems,
|
||||||
itemToDelete,
|
itemToDelete,
|
||||||
selectedLeftItem,
|
selectedLeftItem,
|
||||||
|
loadLeftData,
|
||||||
loadRightData,
|
loadRightData,
|
||||||
|
rightData,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// 개별 체크박스 선택/해제
|
// 개별 체크박스 선택/해제
|
||||||
@@ -825,7 +885,29 @@ export const SplitPanelLayout2Component: React.FC<SplitPanelLayout2ComponentProp
|
|||||||
const selectedId = Array.from(selectedRightItems)[0];
|
const selectedId = Array.from(selectedRightItems)[0];
|
||||||
const item = rightData.find((d) => d[pkColumn] === selectedId);
|
const item = rightData.find((d) => d[pkColumn] === selectedId);
|
||||||
if (item) {
|
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) {
|
} else if (selectedRightItems.size > 1) {
|
||||||
toast.error("수정할 항목을 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(
|
const loadColumnLabels = useCallback(
|
||||||
async (tableName: string, setLabels: (labels: Record<string, string>) => void) => {
|
async (tableName: string, setLabels: (labels: Record<string, string>) => void) => {
|
||||||
@@ -1012,10 +1145,10 @@ export const SplitPanelLayout2Component: React.FC<SplitPanelLayout2ComponentProp
|
|||||||
(checked: boolean) => {
|
(checked: boolean) => {
|
||||||
if (checked) {
|
if (checked) {
|
||||||
const pkColumn = getPrimaryKeyColumn();
|
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);
|
setSelectedRightItems(allIds);
|
||||||
} else {
|
} else {
|
||||||
setSelectedRightItems(new Set());
|
setSelectedRightItems(new Set<string | number>());
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[filteredRightData, getPrimaryKeyColumn],
|
[filteredRightData, getPrimaryKeyColumn],
|
||||||
@@ -1348,6 +1481,27 @@ export const SplitPanelLayout2Component: React.FC<SplitPanelLayout2ComponentProp
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</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>
|
</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 renderLeftTable = () => {
|
||||||
const displayColumns = config.leftPanel?.displayColumns || [];
|
const displayColumns = config.leftPanel?.displayColumns || [];
|
||||||
@@ -1586,8 +1735,8 @@ export const SplitPanelLayout2Component: React.FC<SplitPanelLayout2ComponentProp
|
|||||||
const showCheckbox = config.rightPanel?.showCheckbox ?? true; // 테이블 모드는 기본 체크박스 표시
|
const showCheckbox = config.rightPanel?.showCheckbox ?? true; // 테이블 모드는 기본 체크박스 표시
|
||||||
const pkColumn = getPrimaryKeyColumn();
|
const pkColumn = getPrimaryKeyColumn();
|
||||||
const allSelected =
|
const allSelected =
|
||||||
filteredRightData.length > 0 && filteredRightData.every((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]));
|
const someSelected = filteredRightData.some((item) => selectedRightItems.has(item[pkColumn] as string | number));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-md border">
|
<div className="rounded-md border">
|
||||||
@@ -1633,7 +1782,7 @@ export const SplitPanelLayout2Component: React.FC<SplitPanelLayout2ComponentProp
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
) : (
|
) : (
|
||||||
filteredRightData.map((item, index) => {
|
filteredRightData.map((item, index) => {
|
||||||
const itemId = item[pkColumn];
|
const itemId = item[pkColumn] as string | number;
|
||||||
return (
|
return (
|
||||||
<TableRow key={index} className="hover:bg-muted/50">
|
<TableRow key={index} className="hover:bg-muted/50">
|
||||||
{showCheckbox && (
|
{showCheckbox && (
|
||||||
@@ -1962,11 +2111,7 @@ export const SplitPanelLayout2Component: React.FC<SplitPanelLayout2ComponentProp
|
|||||||
size="sm"
|
size="sm"
|
||||||
variant={btn.variant || "default"}
|
variant={btn.variant || "default"}
|
||||||
className="h-8 text-sm"
|
className="h-8 text-sm"
|
||||||
onClick={() => {
|
onClick={() => handleLeftActionButton(btn)}
|
||||||
if (btn.action === "add") {
|
|
||||||
handleLeftAddClick();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{btn.icon && <span className="mr-1">{btn.icon}</span>}
|
{btn.icon && <span className="mr-1">{btn.icon}</span>}
|
||||||
{btn.label || "버튼"}
|
{btn.label || "버튼"}
|
||||||
|
|||||||
@@ -992,6 +992,42 @@ export const SplitPanelLayout2ConfigPanel: React.FC<SplitPanelLayout2ConfigPanel
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 개별 수정/삭제 버튼 (좌측) */}
|
||||||
|
<div className="border-t pt-3">
|
||||||
|
<Label className="text-xs font-medium">개별 수정/삭제</Label>
|
||||||
|
<p className="text-muted-foreground mb-2 text-[10px]">각 행에 표시되는 수정/삭제 버튼</p>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label className="text-xs">수정 버튼</Label>
|
||||||
|
<Switch
|
||||||
|
checked={config.leftPanel?.showEditButton || false}
|
||||||
|
onCheckedChange={(checked) => updateConfig("leftPanel.showEditButton", checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label className="text-xs">삭제 버튼</Label>
|
||||||
|
<Switch
|
||||||
|
checked={config.leftPanel?.showDeleteButton || false}
|
||||||
|
onCheckedChange={(checked) => updateConfig("leftPanel.showDeleteButton", checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{(config.leftPanel?.showEditButton || config.leftPanel?.showDeleteButton) && (
|
||||||
|
<div className="mt-2">
|
||||||
|
<Label className="text-xs">기본키 컬럼</Label>
|
||||||
|
<ColumnSelect
|
||||||
|
columns={leftColumns}
|
||||||
|
value={config.leftPanel?.primaryKeyColumn || ""}
|
||||||
|
onValueChange={(value) => updateConfig("leftPanel.primaryKeyColumn", value)}
|
||||||
|
placeholder="기본키 컬럼 선택 (기본: id)"
|
||||||
|
/>
|
||||||
|
<p className="text-muted-foreground mt-1 text-[10px]">
|
||||||
|
수정/삭제 시 레코드 식별에 사용
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 탭 설정 (좌측) */}
|
{/* 탭 설정 (좌측) */}
|
||||||
<div className="border-t pt-3">
|
<div className="border-t pt-3">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
@@ -1274,6 +1310,42 @@ export const SplitPanelLayout2ConfigPanel: React.FC<SplitPanelLayout2ConfigPanel
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 개별 수정/삭제 버튼 */}
|
||||||
|
<div className="border-t pt-3">
|
||||||
|
<Label className="text-xs font-medium">개별 수정/삭제</Label>
|
||||||
|
<p className="text-muted-foreground mb-2 text-[10px]">각 행에 표시되는 수정/삭제 버튼</p>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label className="text-xs">수정 버튼</Label>
|
||||||
|
<Switch
|
||||||
|
checked={config.rightPanel?.showEditButton || false}
|
||||||
|
onCheckedChange={(checked) => updateConfig("rightPanel.showEditButton", checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label className="text-xs">삭제 버튼</Label>
|
||||||
|
<Switch
|
||||||
|
checked={config.rightPanel?.showDeleteButton || false}
|
||||||
|
onCheckedChange={(checked) => updateConfig("rightPanel.showDeleteButton", checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{(config.rightPanel?.showEditButton || config.rightPanel?.showDeleteButton) && (
|
||||||
|
<div className="mt-2">
|
||||||
|
<Label className="text-xs">기본키 컬럼</Label>
|
||||||
|
<ColumnSelect
|
||||||
|
columns={rightColumns}
|
||||||
|
value={config.rightPanel?.primaryKeyColumn || ""}
|
||||||
|
onValueChange={(value) => updateConfig("rightPanel.primaryKeyColumn", value)}
|
||||||
|
placeholder="기본키 컬럼 선택 (기본: id)"
|
||||||
|
/>
|
||||||
|
<p className="text-muted-foreground mt-1 text-[10px]">
|
||||||
|
수정/삭제 시 레코드 식별에 사용
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 탭 설정 (우측) */}
|
{/* 탭 설정 (우측) */}
|
||||||
<div className="border-t pt-3">
|
<div className="border-t pt-3">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
@@ -1348,39 +1420,6 @@ export const SplitPanelLayout2ConfigPanel: React.FC<SplitPanelLayout2ConfigPanel
|
|||||||
onCheckedChange={(checked) => updateConfig("rightPanel.showCheckbox", checked)}
|
onCheckedChange={(checked) => updateConfig("rightPanel.showCheckbox", checked)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 수정/삭제 버튼 */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label className="text-xs font-medium">개별 수정/삭제</Label>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<Label className="text-xs">수정 버튼 표시</Label>
|
|
||||||
<Switch
|
|
||||||
checked={config.rightPanel?.showEditButton || false}
|
|
||||||
onCheckedChange={(checked) => updateConfig("rightPanel.showEditButton", checked)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<Label className="text-xs">삭제 버튼 표시</Label>
|
|
||||||
<Switch
|
|
||||||
checked={config.rightPanel?.showDeleteButton || false}
|
|
||||||
onCheckedChange={(checked) => updateConfig("rightPanel.showDeleteButton", checked)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 기본키 컬럼 */}
|
|
||||||
<div>
|
|
||||||
<Label className="text-xs">기본키 컬럼</Label>
|
|
||||||
<ColumnSelect
|
|
||||||
columns={rightColumns}
|
|
||||||
value={config.rightPanel?.primaryKeyColumn || ""}
|
|
||||||
onValueChange={(value) => updateConfig("rightPanel.primaryKeyColumn", value)}
|
|
||||||
placeholder="기본키 컬럼 선택 (기본: id)"
|
|
||||||
/>
|
|
||||||
<p className="text-muted-foreground mt-1 text-[10px]">
|
|
||||||
수정/삭제 시 사용할 기본키 컬럼 (미선택 시 id 사용)
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -158,6 +158,9 @@ export interface LeftPanelConfig {
|
|||||||
showAddButton?: boolean; // 추가 버튼 표시 (하위 호환성)
|
showAddButton?: boolean; // 추가 버튼 표시 (하위 호환성)
|
||||||
addButtonLabel?: string; // 추가 버튼 라벨 (하위 호환성)
|
addButtonLabel?: string; // 추가 버튼 라벨 (하위 호환성)
|
||||||
addModalScreenId?: number; // 추가 모달 화면 ID (하위 호환성)
|
addModalScreenId?: number; // 추가 모달 화면 ID (하위 호환성)
|
||||||
|
showEditButton?: boolean; // 수정 버튼 표시
|
||||||
|
showDeleteButton?: boolean; // 삭제 버튼 표시
|
||||||
|
editModalScreenId?: number; // 수정 모달 화면 ID
|
||||||
actionButtons?: ActionButtonConfig[]; // 복수 액션 버튼 배열
|
actionButtons?: ActionButtonConfig[]; // 복수 액션 버튼 배열
|
||||||
displayMode?: "card" | "table"; // 표시 모드 (card: 카드형, table: 테이블형)
|
displayMode?: "card" | "table"; // 표시 모드 (card: 카드형, table: 테이블형)
|
||||||
primaryKeyColumn?: string; // 기본키 컬럼명 (선택용, 기본: id)
|
primaryKeyColumn?: string; // 기본키 컬럼명 (선택용, 기본: id)
|
||||||
|
|||||||
@@ -1223,12 +1223,11 @@ export function UniversalFormModalComponent({
|
|||||||
if (subTableConfig.options?.saveMainAsFirst) {
|
if (subTableConfig.options?.saveMainAsFirst) {
|
||||||
mainFieldMappings = [];
|
mainFieldMappings = [];
|
||||||
|
|
||||||
// 메인 섹션(비반복)의 필드들을 서브 테이블에 매핑
|
// fieldMappings에 정의된 targetColumn만 매핑 (서브 테이블에 존재하는 컬럼만)
|
||||||
// 서브 테이블의 fieldMappings에서 targetColumn을 찾아서 매핑
|
|
||||||
for (const mapping of subTableConfig.fieldMappings || []) {
|
for (const mapping of subTableConfig.fieldMappings || []) {
|
||||||
if (mapping.targetColumn) {
|
if (mapping.targetColumn) {
|
||||||
// 메인 데이터에서 동일한 컬럼명이 있으면 매핑
|
// 메인 데이터에서 동일한 컬럼명이 있으면 매핑
|
||||||
if (mainData[mapping.targetColumn] !== undefined) {
|
if (mainData[mapping.targetColumn] !== undefined && mainData[mapping.targetColumn] !== null && mainData[mapping.targetColumn] !== "") {
|
||||||
mainFieldMappings.push({
|
mainFieldMappings.push({
|
||||||
formField: mapping.targetColumn,
|
formField: mapping.targetColumn,
|
||||||
targetColumn: mapping.targetColumn,
|
targetColumn: mapping.targetColumn,
|
||||||
@@ -1239,7 +1238,7 @@ export function UniversalFormModalComponent({
|
|||||||
config.sections.forEach((section) => {
|
config.sections.forEach((section) => {
|
||||||
if (section.repeatable || section.type === "table") return;
|
if (section.repeatable || section.type === "table") return;
|
||||||
const matchingField = (section.fields || []).find((f) => f.columnName === mapping.targetColumn);
|
const matchingField = (section.fields || []).find((f) => f.columnName === mapping.targetColumn);
|
||||||
if (matchingField && mainData[matchingField.columnName] !== undefined) {
|
if (matchingField && mainData[matchingField.columnName] !== undefined && mainData[matchingField.columnName] !== null && mainData[matchingField.columnName] !== "") {
|
||||||
mainFieldMappings!.push({
|
mainFieldMappings!.push({
|
||||||
formField: matchingField.columnName,
|
formField: matchingField.columnName,
|
||||||
targetColumn: mapping.targetColumn,
|
targetColumn: mapping.targetColumn,
|
||||||
@@ -1374,6 +1373,11 @@ export function UniversalFormModalComponent({
|
|||||||
if (onSave) {
|
if (onSave) {
|
||||||
onSave({ ...formData, _saveCompleted: true });
|
onSave({ ...formData, _saveCompleted: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 저장 완료 후 모달 닫기 이벤트 발생
|
||||||
|
if (config.saveConfig.afterSave?.closeModal !== false) {
|
||||||
|
window.dispatchEvent(new CustomEvent("closeEditModal"));
|
||||||
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("저장 실패:", error);
|
console.error("저장 실패:", error);
|
||||||
// axios 에러의 경우 서버 응답 메시지 추출
|
// axios 에러의 경우 서버 응답 메시지 추출
|
||||||
@@ -1492,6 +1496,22 @@ export function UniversalFormModalComponent({
|
|||||||
return `${valueVal} - ${displayVal}`;
|
return `${valueVal} - ${displayVal}`;
|
||||||
case "name_code":
|
case "name_code":
|
||||||
return `${displayVal} (${valueVal})`;
|
return `${displayVal} (${valueVal})`;
|
||||||
|
case "custom":
|
||||||
|
// 커스텀 형식: {컬럼명}을 실제 값으로 치환
|
||||||
|
if (lfg.customDisplayFormat) {
|
||||||
|
let result = lfg.customDisplayFormat;
|
||||||
|
// {컬럼명} 패턴을 찾아서 실제 값으로 치환
|
||||||
|
const matches = result.match(/\{([^}]+)\}/g);
|
||||||
|
if (matches) {
|
||||||
|
matches.forEach((match) => {
|
||||||
|
const columnName = match.slice(1, -1); // { } 제거
|
||||||
|
const columnValue = row[columnName];
|
||||||
|
result = result.replace(match, columnValue !== undefined && columnValue !== null ? String(columnValue) : "");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return String(displayVal);
|
||||||
case "name_only":
|
case "name_only":
|
||||||
default:
|
default:
|
||||||
return String(displayVal);
|
return String(displayVal);
|
||||||
|
|||||||
@@ -81,7 +81,10 @@ export interface FormFieldConfig {
|
|||||||
enabled?: boolean; // 사용 여부
|
enabled?: boolean; // 사용 여부
|
||||||
sourceTable?: string; // 소스 테이블 (예: dept_info)
|
sourceTable?: string; // 소스 테이블 (예: dept_info)
|
||||||
displayColumn?: string; // 표시할 컬럼 (예: dept_name) - 드롭다운에 보여줄 텍스트
|
displayColumn?: string; // 표시할 컬럼 (예: dept_name) - 드롭다운에 보여줄 텍스트
|
||||||
displayFormat?: "name_only" | "code_name" | "name_code"; // 표시 형식
|
displayFormat?: "name_only" | "code_name" | "name_code" | "custom"; // 표시 형식
|
||||||
|
// 커스텀 표시 형식 (displayFormat이 "custom"일 때 사용)
|
||||||
|
// 형식: {컬럼명} 으로 치환됨 (예: "{item_name} ({item_number})" → "철판 (ITEM-001)")
|
||||||
|
customDisplayFormat?: string;
|
||||||
mappings?: LinkedFieldMapping[]; // 저장할 컬럼 매핑 (첫 번째 매핑의 sourceColumn이 드롭다운 값으로 사용됨)
|
mappings?: LinkedFieldMapping[]; // 저장할 컬럼 매핑 (첫 번째 매핑의 sourceColumn이 드롭다운 값으로 사용됨)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -254,6 +257,53 @@ export interface TableSectionConfig {
|
|||||||
multiSelect?: boolean; // 다중 선택 허용 (기본: true)
|
multiSelect?: boolean; // 다중 선택 허용 (기본: true)
|
||||||
maxHeight?: string; // 테이블 최대 높이 (기본: "400px")
|
maxHeight?: string; // 테이블 최대 높이 (기본: "400px")
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 7. 조건부 테이블 설정 (고급)
|
||||||
|
conditionalTable?: ConditionalTableConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 조건부 테이블 설정
|
||||||
|
* 조건(검사유형 등)에 따라 다른 데이터를 표시하고 저장합니다.
|
||||||
|
*
|
||||||
|
* 사용 예시:
|
||||||
|
* - 품목검사정보: 검사유형(입고/공정/출고/재고/최종)별로 검사항목 관리
|
||||||
|
* - BOM 관리: 품목유형별 자재 구성
|
||||||
|
* - 공정 관리: 공정유형별 작업 항목
|
||||||
|
*/
|
||||||
|
export interface ConditionalTableConfig {
|
||||||
|
enabled: boolean;
|
||||||
|
|
||||||
|
// 트리거 UI 타입
|
||||||
|
// - checkbox: 체크박스로 다중 선택 (선택된 조건들을 탭으로 표시)
|
||||||
|
// - dropdown: 드롭다운으로 단일 선택
|
||||||
|
// - tabs: 모든 옵션을 탭으로 표시
|
||||||
|
triggerType: "checkbox" | "dropdown" | "tabs";
|
||||||
|
|
||||||
|
// 조건 값을 저장할 컬럼 (예: inspection_type)
|
||||||
|
// 저장 시 각 행에 이 컬럼으로 조건 값이 자동 저장됨
|
||||||
|
conditionColumn: string;
|
||||||
|
|
||||||
|
// 조건 옵션 목록
|
||||||
|
options: ConditionalTableOption[];
|
||||||
|
|
||||||
|
// 옵션을 테이블에서 동적으로 로드할 경우
|
||||||
|
optionSource?: {
|
||||||
|
enabled: boolean;
|
||||||
|
tableName: string; // 예: inspection_type_code
|
||||||
|
valueColumn: string; // 예: type_code
|
||||||
|
labelColumn: string; // 예: type_name
|
||||||
|
filterCondition?: string; // 예: is_active = 'Y'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 조건부 테이블 옵션
|
||||||
|
*/
|
||||||
|
export interface ConditionalTableOption {
|
||||||
|
id: string;
|
||||||
|
value: string; // 저장될 값 (예: "입고검사")
|
||||||
|
label: string; // 표시 라벨 (예: "입고검사")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -650,6 +700,7 @@ export interface ModalConfig {
|
|||||||
showCloseButton?: boolean;
|
showCloseButton?: boolean;
|
||||||
|
|
||||||
// 버튼 설정
|
// 버튼 설정
|
||||||
|
showSaveButton?: boolean; // 저장 버튼 표시 (기본: true)
|
||||||
saveButtonText?: string; // 저장 버튼 텍스트 (기본: "저장")
|
saveButtonText?: string; // 저장 버튼 텍스트 (기본: "저장")
|
||||||
cancelButtonText?: string; // 취소 버튼 텍스트 (기본: "취소")
|
cancelButtonText?: string; // 취소 버튼 텍스트 (기본: "취소")
|
||||||
showResetButton?: boolean; // 초기화 버튼 표시
|
showResetButton?: boolean; // 초기화 버튼 표시
|
||||||
@@ -748,6 +799,7 @@ export const LINKED_FIELD_DISPLAY_FORMAT_OPTIONS = [
|
|||||||
{ value: "name_only", label: "이름만 (예: 영업부)" },
|
{ value: "name_only", label: "이름만 (예: 영업부)" },
|
||||||
{ value: "code_name", label: "코드 - 이름 (예: SALES - 영업부)" },
|
{ value: "code_name", label: "코드 - 이름 (예: SALES - 영업부)" },
|
||||||
{ value: "name_code", label: "이름 (코드) (예: 영업부 (SALES))" },
|
{ value: "name_code", label: "이름 (코드) (예: 영업부 (SALES))" },
|
||||||
|
{ value: "custom", label: "커스텀 형식 (직접 입력)" },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
@@ -815,3 +867,10 @@ export const LOOKUP_CONDITION_SOURCE_OPTIONS = [
|
|||||||
{ value: "sectionField", label: "다른 섹션" },
|
{ value: "sectionField", label: "다른 섹션" },
|
||||||
{ value: "externalTable", label: "외부 테이블" },
|
{ value: "externalTable", label: "외부 테이블" },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
// 조건부 테이블 트리거 타입 옵션
|
||||||
|
export const CONDITIONAL_TABLE_TRIGGER_OPTIONS = [
|
||||||
|
{ value: "checkbox", label: "체크박스 (다중 선택)" },
|
||||||
|
{ value: "dropdown", label: "드롭다운 (단일 선택)" },
|
||||||
|
{ value: "tabs", label: "탭 (전체 표시)" },
|
||||||
|
] as const;
|
||||||
|
|||||||
Reference in New Issue
Block a user