수정/삭제 기능 구현
This commit is contained in:
@@ -6,7 +6,7 @@ import { SplitPanelLayoutConfig } from "./types";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Plus, Search, GripVertical, Loader2, ChevronDown, ChevronUp, Save, ChevronRight } from "lucide-react";
|
||||
import { Plus, Search, GripVertical, Loader2, ChevronDown, ChevronUp, Save, ChevronRight, Pencil, Trash2 } from "lucide-react";
|
||||
import { dataApi } from "@/lib/api/data";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { tableTypeApi } from "@/lib/api/screen";
|
||||
@@ -54,6 +54,17 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||
const [showAddModal, setShowAddModal] = useState(false);
|
||||
const [addModalPanel, setAddModalPanel] = useState<"left" | "right" | "left-item" | null>(null);
|
||||
const [addModalFormData, setAddModalFormData] = useState<Record<string, any>>({});
|
||||
|
||||
// 수정 모달 상태
|
||||
const [showEditModal, setShowEditModal] = useState(false);
|
||||
const [editModalPanel, setEditModalPanel] = useState<"left" | "right" | null>(null);
|
||||
const [editModalItem, setEditModalItem] = useState<any>(null);
|
||||
const [editModalFormData, setEditModalFormData] = useState<Record<string, any>>({});
|
||||
|
||||
// 삭제 확인 모달 상태
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||
const [deleteModalPanel, setDeleteModalPanel] = useState<"left" | "right" | null>(null);
|
||||
const [deleteModalItem, setDeleteModalItem] = useState<any>(null);
|
||||
|
||||
// 리사이저 드래그 상태
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
@@ -286,6 +297,169 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||
setShowAddModal(true);
|
||||
}, []);
|
||||
|
||||
// 수정 버튼 핸들러
|
||||
const handleEditClick = useCallback((panel: "left" | "right", item: any) => {
|
||||
setEditModalPanel(panel);
|
||||
setEditModalItem(item);
|
||||
setEditModalFormData({ ...item });
|
||||
setShowEditModal(true);
|
||||
}, []);
|
||||
|
||||
// 수정 모달 저장
|
||||
const handleEditModalSave = useCallback(async () => {
|
||||
const tableName = editModalPanel === "left"
|
||||
? componentConfig.leftPanel?.tableName
|
||||
: componentConfig.rightPanel?.tableName;
|
||||
|
||||
const sourceColumn = componentConfig.leftPanel?.itemAddConfig?.sourceColumn || 'id';
|
||||
const primaryKey = editModalItem[sourceColumn] || editModalItem.id || editModalItem.ID;
|
||||
|
||||
if (!tableName || !primaryKey) {
|
||||
toast({
|
||||
title: "수정 오류",
|
||||
description: "테이블명 또는 Primary Key가 없습니다.",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log("📝 데이터 수정:", { tableName, primaryKey, data: editModalFormData });
|
||||
|
||||
// 프론트엔드 전용 필드 제거 (children, level 등)
|
||||
const cleanData = { ...editModalFormData };
|
||||
delete cleanData.children;
|
||||
delete cleanData.level;
|
||||
|
||||
// 좌측 패널 수정 시, 조인 관계 정보 포함
|
||||
let updatePayload: any = cleanData;
|
||||
|
||||
if (editModalPanel === "left" && componentConfig.rightPanel?.relation?.type === "join") {
|
||||
// 조인 관계가 있는 경우, 관계 정보를 페이로드에 추가
|
||||
updatePayload._relationInfo = {
|
||||
rightTable: componentConfig.rightPanel.tableName,
|
||||
leftColumn: componentConfig.rightPanel.relation.leftColumn,
|
||||
rightColumn: componentConfig.rightPanel.relation.rightColumn,
|
||||
oldLeftValue: editModalItem[componentConfig.rightPanel.relation.leftColumn],
|
||||
};
|
||||
console.log("🔗 조인 관계 정보 추가:", updatePayload._relationInfo);
|
||||
}
|
||||
|
||||
const result = await dataApi.updateRecord(tableName, primaryKey, updatePayload);
|
||||
|
||||
if (result.success) {
|
||||
toast({
|
||||
title: "성공",
|
||||
description: "데이터가 성공적으로 수정되었습니다.",
|
||||
});
|
||||
|
||||
// 모달 닫기
|
||||
setShowEditModal(false);
|
||||
setEditModalFormData({});
|
||||
setEditModalItem(null);
|
||||
|
||||
// 데이터 새로고침
|
||||
if (editModalPanel === "left") {
|
||||
loadLeftData();
|
||||
// 우측 패널도 새로고침 (FK가 변경되었을 수 있음)
|
||||
if (selectedLeftItem) {
|
||||
loadRightData(selectedLeftItem);
|
||||
}
|
||||
} else if (editModalPanel === "right" && selectedLeftItem) {
|
||||
loadRightData(selectedLeftItem);
|
||||
}
|
||||
} else {
|
||||
toast({
|
||||
title: "수정 실패",
|
||||
description: result.message || "데이터 수정에 실패했습니다.",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("데이터 수정 오류:", error);
|
||||
toast({
|
||||
title: "오류",
|
||||
description: error?.response?.data?.message || "데이터 수정 중 오류가 발생했습니다.",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
}, [editModalPanel, componentConfig, editModalItem, editModalFormData, toast, selectedLeftItem, loadLeftData, loadRightData]);
|
||||
|
||||
// 삭제 버튼 핸들러
|
||||
const handleDeleteClick = useCallback((panel: "left" | "right", item: any) => {
|
||||
setDeleteModalPanel(panel);
|
||||
setDeleteModalItem(item);
|
||||
setShowDeleteModal(true);
|
||||
}, []);
|
||||
|
||||
// 삭제 확인
|
||||
const handleDeleteConfirm = useCallback(async () => {
|
||||
const tableName = deleteModalPanel === "left"
|
||||
? componentConfig.leftPanel?.tableName
|
||||
: componentConfig.rightPanel?.tableName;
|
||||
|
||||
const sourceColumn = componentConfig.leftPanel?.itemAddConfig?.sourceColumn || 'id';
|
||||
const primaryKey = deleteModalItem[sourceColumn] || deleteModalItem.id || deleteModalItem.ID;
|
||||
|
||||
if (!tableName || !primaryKey) {
|
||||
toast({
|
||||
title: "삭제 오류",
|
||||
description: "테이블명 또는 Primary Key가 없습니다.",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log("🗑️ 데이터 삭제:", { tableName, primaryKey });
|
||||
|
||||
const result = await dataApi.deleteRecord(tableName, primaryKey);
|
||||
|
||||
if (result.success) {
|
||||
toast({
|
||||
title: "성공",
|
||||
description: "데이터가 성공적으로 삭제되었습니다.",
|
||||
});
|
||||
|
||||
// 모달 닫기
|
||||
setShowDeleteModal(false);
|
||||
setDeleteModalItem(null);
|
||||
|
||||
// 데이터 새로고침
|
||||
if (deleteModalPanel === "left") {
|
||||
loadLeftData();
|
||||
// 삭제된 항목이 선택되어 있었으면 선택 해제
|
||||
if (selectedLeftItem && selectedLeftItem[sourceColumn] === primaryKey) {
|
||||
setSelectedLeftItem(null);
|
||||
setRightData(null);
|
||||
}
|
||||
} else if (deleteModalPanel === "right" && selectedLeftItem) {
|
||||
loadRightData(selectedLeftItem);
|
||||
}
|
||||
} else {
|
||||
toast({
|
||||
title: "삭제 실패",
|
||||
description: result.message || "데이터 삭제에 실패했습니다.",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("데이터 삭제 오류:", error);
|
||||
|
||||
// 외래키 제약조건 에러 처리
|
||||
let errorMessage = "데이터 삭제 중 오류가 발생했습니다.";
|
||||
if (error?.response?.data?.error?.includes("foreign key")) {
|
||||
errorMessage = "이 데이터를 참조하는 다른 데이터가 있어 삭제할 수 없습니다.";
|
||||
}
|
||||
|
||||
toast({
|
||||
title: "오류",
|
||||
description: errorMessage,
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
}, [deleteModalPanel, componentConfig, deleteModalItem, toast, selectedLeftItem, loadLeftData, loadRightData]);
|
||||
|
||||
// 항목별 추가 버튼 핸들러 (좌측 항목의 + 버튼 - 하위 항목 추가)
|
||||
const handleItemAddClick = useCallback((item: any) => {
|
||||
const itemAddConfig = componentConfig.leftPanel?.itemAddConfig;
|
||||
@@ -527,7 +701,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||
<CardTitle className="text-base font-semibold">
|
||||
{componentConfig.leftPanel?.title || "좌측 패널"}
|
||||
</CardTitle>
|
||||
{componentConfig.leftPanel?.showAdd && !isDesignMode && (
|
||||
{!isDesignMode && componentConfig.leftPanel?.showAdd && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
@@ -693,25 +867,54 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||
<div className="w-5" />
|
||||
)}
|
||||
|
||||
{/* 항목 내용 */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="truncate font-medium">{displayTitle}</div>
|
||||
{displaySubtitle && <div className="truncate text-xs text-muted-foreground">{displaySubtitle}</div>}
|
||||
</div>
|
||||
|
||||
{/* 항목별 추가 버튼 */}
|
||||
{componentConfig.leftPanel?.showItemAddButton && !isDesignMode && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleItemAddClick(item);
|
||||
}}
|
||||
className="flex-shrink-0 opacity-0 transition-opacity group-hover:opacity-100"
|
||||
title="하위 항목 추가"
|
||||
>
|
||||
<Plus className="h-5 w-5 rounded-md bg-primary p-1 text-primary-foreground hover:bg-primary/90" />
|
||||
</button>
|
||||
)}
|
||||
{/* 항목 내용 */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="truncate font-medium">{displayTitle}</div>
|
||||
{displaySubtitle && <div className="truncate text-xs text-muted-foreground">{displaySubtitle}</div>}
|
||||
</div>
|
||||
|
||||
{/* 항목별 버튼들 */}
|
||||
{!isDesignMode && (
|
||||
<div className="flex flex-shrink-0 items-center gap-1 opacity-0 transition-opacity group-hover:opacity-100">
|
||||
{/* 수정 버튼 */}
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleEditClick("left", item);
|
||||
}}
|
||||
className="rounded p-1 hover:bg-gray-200 transition-colors"
|
||||
title="수정"
|
||||
>
|
||||
<Pencil className="h-4 w-4 text-gray-600" />
|
||||
</button>
|
||||
|
||||
{/* 삭제 버튼 */}
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDeleteClick("left", item);
|
||||
}}
|
||||
className="rounded p-1 hover:bg-red-100 transition-colors"
|
||||
title="삭제"
|
||||
>
|
||||
<Trash2 className="h-4 w-4 text-red-600" />
|
||||
</button>
|
||||
|
||||
{/* 항목별 추가 버튼 */}
|
||||
{componentConfig.leftPanel?.showItemAddButton && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleItemAddClick(item);
|
||||
}}
|
||||
className="rounded p-1 hover:bg-gray-200 transition-colors"
|
||||
title="하위 항목 추가"
|
||||
>
|
||||
<Plus className="h-4 w-4 text-gray-600" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -765,15 +968,20 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||
<CardTitle className="text-base font-semibold">
|
||||
{componentConfig.rightPanel?.title || "우측 패널"}
|
||||
</CardTitle>
|
||||
{componentConfig.rightPanel?.showAdd && !isDesignMode && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handleAddClick("right")}
|
||||
>
|
||||
<Plus className="mr-1 h-4 w-4" />
|
||||
추가
|
||||
</Button>
|
||||
{!isDesignMode && (
|
||||
<div className="flex items-center gap-2">
|
||||
{componentConfig.rightPanel?.showAdd && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handleAddClick("right")}
|
||||
>
|
||||
<Plus className="mr-1 h-4 w-4" />
|
||||
추가
|
||||
</Button>
|
||||
)}
|
||||
{/* 우측 패널 수정/삭제는 각 카드에서 처리 */}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{componentConfig.rightPanel?.showSearch && (
|
||||
@@ -871,13 +1079,13 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||
key={itemId}
|
||||
className="bg-card overflow-hidden rounded-lg border shadow-sm transition-all hover:shadow-md"
|
||||
>
|
||||
{/* 요약 정보 (클릭 가능) */}
|
||||
<div
|
||||
onClick={() => toggleRightItemExpansion(itemId)}
|
||||
className="cursor-pointer p-3 transition-colors hover:bg-muted"
|
||||
>
|
||||
{/* 요약 정보 */}
|
||||
<div className="p-3">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div
|
||||
className="min-w-0 flex-1 cursor-pointer"
|
||||
onClick={() => toggleRightItemExpansion(itemId)}
|
||||
>
|
||||
{firstValues.map(([key, value], idx) => (
|
||||
<div key={key} className="mb-1 last:mb-0">
|
||||
<div className="text-xs font-medium text-muted-foreground">{getColumnLabel(key)}</div>
|
||||
@@ -887,12 +1095,44 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-shrink-0 items-start pt-1">
|
||||
{isExpanded ? (
|
||||
<ChevronUp className="h-5 w-5 text-muted-foreground" />
|
||||
) : (
|
||||
<ChevronDown className="h-5 w-5 text-muted-foreground" />
|
||||
<div className="flex flex-shrink-0 items-start gap-1 pt-1">
|
||||
{/* 수정 버튼 */}
|
||||
{!isDesignMode && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleEditClick("right", item);
|
||||
}}
|
||||
className="rounded p-1 hover:bg-gray-200 transition-colors"
|
||||
title="수정"
|
||||
>
|
||||
<Pencil className="h-4 w-4 text-gray-600" />
|
||||
</button>
|
||||
)}
|
||||
{/* 삭제 버튼 */}
|
||||
{!isDesignMode && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDeleteClick("right", item);
|
||||
}}
|
||||
className="rounded p-1 hover:bg-red-100 transition-colors"
|
||||
title="삭제"
|
||||
>
|
||||
<Trash2 className="h-4 w-4 text-red-600" />
|
||||
</button>
|
||||
)}
|
||||
{/* 확장/접기 버튼 */}
|
||||
<button
|
||||
onClick={() => toggleRightItemExpansion(itemId)}
|
||||
className="rounded p-1 hover:bg-gray-200 transition-colors"
|
||||
>
|
||||
{isExpanded ? (
|
||||
<ChevronUp className="h-5 w-5 text-muted-foreground" />
|
||||
) : (
|
||||
<ChevronDown className="h-5 w-5 text-muted-foreground" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1085,6 +1325,157 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* 수정 모달 */}
|
||||
<Dialog open={showEditModal} onOpenChange={setShowEditModal}>
|
||||
<DialogContent className="max-w-[95vw] sm:max-w-[500px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-base sm:text-lg">
|
||||
{editModalPanel === "left"
|
||||
? `${componentConfig.leftPanel?.title} 수정`
|
||||
: `${componentConfig.rightPanel?.title} 수정`}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-xs sm:text-sm">
|
||||
데이터를 수정합니다. 필요한 항목을 변경해주세요.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-3 sm:space-y-4">
|
||||
{editModalItem && (() => {
|
||||
// 좌측 패널 수정: leftColumn만 수정 가능
|
||||
if (editModalPanel === "left") {
|
||||
const leftColumn = componentConfig.rightPanel?.relation?.leftColumn;
|
||||
|
||||
// leftColumn만 표시
|
||||
if (!leftColumn || editModalFormData[leftColumn] === undefined) {
|
||||
return <p className="text-sm text-muted-foreground">수정 가능한 컬럼이 없습니다.</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Label htmlFor={`edit-${leftColumn}`} className="text-xs sm:text-sm">
|
||||
{leftColumn}
|
||||
</Label>
|
||||
<Input
|
||||
id={`edit-${leftColumn}`}
|
||||
value={editModalFormData[leftColumn] || ""}
|
||||
onChange={(e) => {
|
||||
setEditModalFormData(prev => ({
|
||||
...prev,
|
||||
[leftColumn]: e.target.value
|
||||
}));
|
||||
}}
|
||||
placeholder={`${leftColumn} 입력`}
|
||||
className="h-8 text-xs sm:h-10 sm:text-sm"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 우측 패널 수정: 우측 패널에 설정된 표시 컬럼들만
|
||||
if (editModalPanel === "right") {
|
||||
const rightColumns = componentConfig.rightPanel?.columns;
|
||||
|
||||
if (rightColumns && rightColumns.length > 0) {
|
||||
// 설정된 컬럼만 표시
|
||||
return rightColumns.map((col) => (
|
||||
<div key={col.name}>
|
||||
<Label htmlFor={`edit-${col.name}`} className="text-xs sm:text-sm">
|
||||
{col.label || col.name}
|
||||
</Label>
|
||||
<Input
|
||||
id={`edit-${col.name}`}
|
||||
value={editModalFormData[col.name] || ""}
|
||||
onChange={(e) => {
|
||||
setEditModalFormData(prev => ({
|
||||
...prev,
|
||||
[col.name]: e.target.value
|
||||
}));
|
||||
}}
|
||||
placeholder={`${col.label || col.name} 입력`}
|
||||
className="h-8 text-xs sm:h-10 sm:text-sm"
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
} else {
|
||||
// 설정이 없으면 모든 컬럼 표시 (company_code, company_name 제외)
|
||||
return Object.entries(editModalFormData)
|
||||
.filter(([key]) => key !== 'company_code' && key !== 'company_name')
|
||||
.map(([key, value]) => (
|
||||
<div key={key}>
|
||||
<Label htmlFor={`edit-${key}`} className="text-xs sm:text-sm">
|
||||
{key}
|
||||
</Label>
|
||||
<Input
|
||||
id={`edit-${key}`}
|
||||
value={editModalFormData[key] || ""}
|
||||
onChange={(e) => {
|
||||
setEditModalFormData(prev => ({
|
||||
...prev,
|
||||
[key]: e.target.value
|
||||
}));
|
||||
}}
|
||||
placeholder={`${key} 입력`}
|
||||
className="h-8 text-xs sm:h-10 sm:text-sm"
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
})()}
|
||||
</div>
|
||||
|
||||
<DialogFooter className="gap-2 sm:gap-0">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setShowEditModal(false)}
|
||||
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
|
||||
>
|
||||
취소
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleEditModalSave}
|
||||
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
|
||||
>
|
||||
<Save className="mr-1 h-3 w-3 sm:h-4 sm:w-4" />
|
||||
저장
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* 삭제 확인 모달 */}
|
||||
<Dialog open={showDeleteModal} onOpenChange={setShowDeleteModal}>
|
||||
<DialogContent className="max-w-[95vw] sm:max-w-[500px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-base sm:text-lg">삭제 확인</DialogTitle>
|
||||
<DialogDescription className="text-xs sm:text-sm">
|
||||
정말로 이 데이터를 삭제하시겠습니까?
|
||||
<br />이 작업은 되돌릴 수 없습니다.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<DialogFooter className="gap-2 sm:gap-0">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setShowDeleteModal(false)}
|
||||
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
|
||||
>
|
||||
취소
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={handleDeleteConfirm}
|
||||
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
|
||||
>
|
||||
<Trash2 className="mr-1 h-3 w-3 sm:h-4 sm:w-4" />
|
||||
삭제
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user