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 || "버튼"}
|
||||
|
||||
@@ -992,6 +992,42 @@ export const SplitPanelLayout2ConfigPanel: React.FC<SplitPanelLayout2ConfigPanel
|
||||
</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="flex items-center justify-between">
|
||||
@@ -1274,6 +1310,42 @@ export const SplitPanelLayout2ConfigPanel: React.FC<SplitPanelLayout2ConfigPanel
|
||||
</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="flex items-center justify-between">
|
||||
@@ -1348,39 +1420,6 @@ export const SplitPanelLayout2ConfigPanel: React.FC<SplitPanelLayout2ConfigPanel
|
||||
onCheckedChange={(checked) => updateConfig("rightPanel.showCheckbox", checked)}
|
||||
/>
|
||||
</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>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
@@ -158,6 +158,9 @@ export interface LeftPanelConfig {
|
||||
showAddButton?: boolean; // 추가 버튼 표시 (하위 호환성)
|
||||
addButtonLabel?: string; // 추가 버튼 라벨 (하위 호환성)
|
||||
addModalScreenId?: number; // 추가 모달 화면 ID (하위 호환성)
|
||||
showEditButton?: boolean; // 수정 버튼 표시
|
||||
showDeleteButton?: boolean; // 삭제 버튼 표시
|
||||
editModalScreenId?: number; // 수정 모달 화면 ID
|
||||
actionButtons?: ActionButtonConfig[]; // 복수 액션 버튼 배열
|
||||
displayMode?: "card" | "table"; // 표시 모드 (card: 카드형, table: 테이블형)
|
||||
primaryKeyColumn?: string; // 기본키 컬럼명 (선택용, 기본: id)
|
||||
|
||||
@@ -1223,12 +1223,11 @@ export function UniversalFormModalComponent({
|
||||
if (subTableConfig.options?.saveMainAsFirst) {
|
||||
mainFieldMappings = [];
|
||||
|
||||
// 메인 섹션(비반복)의 필드들을 서브 테이블에 매핑
|
||||
// 서브 테이블의 fieldMappings에서 targetColumn을 찾아서 매핑
|
||||
// fieldMappings에 정의된 targetColumn만 매핑 (서브 테이블에 존재하는 컬럼만)
|
||||
for (const mapping of subTableConfig.fieldMappings || []) {
|
||||
if (mapping.targetColumn) {
|
||||
// 메인 데이터에서 동일한 컬럼명이 있으면 매핑
|
||||
if (mainData[mapping.targetColumn] !== undefined) {
|
||||
if (mainData[mapping.targetColumn] !== undefined && mainData[mapping.targetColumn] !== null && mainData[mapping.targetColumn] !== "") {
|
||||
mainFieldMappings.push({
|
||||
formField: mapping.targetColumn,
|
||||
targetColumn: mapping.targetColumn,
|
||||
@@ -1239,7 +1238,7 @@ export function UniversalFormModalComponent({
|
||||
config.sections.forEach((section) => {
|
||||
if (section.repeatable || section.type === "table") return;
|
||||
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({
|
||||
formField: matchingField.columnName,
|
||||
targetColumn: mapping.targetColumn,
|
||||
@@ -1374,6 +1373,11 @@ export function UniversalFormModalComponent({
|
||||
if (onSave) {
|
||||
onSave({ ...formData, _saveCompleted: true });
|
||||
}
|
||||
|
||||
// 저장 완료 후 모달 닫기 이벤트 발생
|
||||
if (config.saveConfig.afterSave?.closeModal !== false) {
|
||||
window.dispatchEvent(new CustomEvent("closeEditModal"));
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("저장 실패:", error);
|
||||
// axios 에러의 경우 서버 응답 메시지 추출
|
||||
@@ -1492,6 +1496,22 @@ export function UniversalFormModalComponent({
|
||||
return `${valueVal} - ${displayVal}`;
|
||||
case "name_code":
|
||||
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":
|
||||
default:
|
||||
return String(displayVal);
|
||||
|
||||
@@ -81,7 +81,10 @@ export interface FormFieldConfig {
|
||||
enabled?: boolean; // 사용 여부
|
||||
sourceTable?: string; // 소스 테이블 (예: dept_info)
|
||||
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이 드롭다운 값으로 사용됨)
|
||||
};
|
||||
|
||||
@@ -254,6 +257,53 @@ export interface TableSectionConfig {
|
||||
multiSelect?: boolean; // 다중 선택 허용 (기본: true)
|
||||
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;
|
||||
|
||||
// 버튼 설정
|
||||
showSaveButton?: boolean; // 저장 버튼 표시 (기본: true)
|
||||
saveButtonText?: string; // 저장 버튼 텍스트 (기본: "저장")
|
||||
cancelButtonText?: string; // 취소 버튼 텍스트 (기본: "취소")
|
||||
showResetButton?: boolean; // 초기화 버튼 표시
|
||||
@@ -748,6 +799,7 @@ export const LINKED_FIELD_DISPLAY_FORMAT_OPTIONS = [
|
||||
{ value: "name_only", label: "이름만 (예: 영업부)" },
|
||||
{ value: "code_name", label: "코드 - 이름 (예: SALES - 영업부)" },
|
||||
{ value: "name_code", label: "이름 (코드) (예: 영업부 (SALES))" },
|
||||
{ value: "custom", label: "커스텀 형식 (직접 입력)" },
|
||||
] as const;
|
||||
|
||||
// ============================================
|
||||
@@ -815,3 +867,10 @@ export const LOOKUP_CONDITION_SOURCE_OPTIONS = [
|
||||
{ value: "sectionField", label: "다른 섹션" },
|
||||
{ value: "externalTable", label: "외부 테이블" },
|
||||
] 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