수정/삭제 기능 구현
This commit is contained in:
parent
672aba8404
commit
afea879920
|
|
@ -561,6 +561,11 @@ class DataService {
|
|||
return validation.error!;
|
||||
}
|
||||
|
||||
// _relationInfo 추출 (조인 관계 업데이트용)
|
||||
const relationInfo = data._relationInfo;
|
||||
const cleanData = { ...data };
|
||||
delete cleanData._relationInfo;
|
||||
|
||||
// Primary Key 컬럼 찾기
|
||||
const pkResult = await query<{ attname: string }>(
|
||||
`SELECT a.attname
|
||||
|
|
@ -575,8 +580,8 @@ class DataService {
|
|||
pkColumn = pkResult[0].attname;
|
||||
}
|
||||
|
||||
const columns = Object.keys(data);
|
||||
const values = Object.values(data);
|
||||
const columns = Object.keys(cleanData);
|
||||
const values = Object.values(cleanData);
|
||||
const setClause = columns
|
||||
.map((col, index) => `"${col}" = $${index + 1}`)
|
||||
.join(", ");
|
||||
|
|
@ -599,6 +604,35 @@ class DataService {
|
|||
};
|
||||
}
|
||||
|
||||
// 🔗 조인 관계가 있는 경우, 연결된 테이블의 FK도 업데이트
|
||||
if (relationInfo && relationInfo.rightTable && relationInfo.leftColumn && relationInfo.rightColumn) {
|
||||
const { rightTable, leftColumn, rightColumn, oldLeftValue } = relationInfo;
|
||||
const newLeftValue = cleanData[leftColumn];
|
||||
|
||||
// leftColumn 값이 변경된 경우에만 우측 테이블 업데이트
|
||||
if (newLeftValue !== undefined && newLeftValue !== oldLeftValue) {
|
||||
console.log("🔗 조인 관계 FK 업데이트:", {
|
||||
rightTable,
|
||||
rightColumn,
|
||||
oldValue: oldLeftValue,
|
||||
newValue: newLeftValue,
|
||||
});
|
||||
|
||||
try {
|
||||
const updateRelatedQuery = `
|
||||
UPDATE "${rightTable}"
|
||||
SET "${rightColumn}" = $1
|
||||
WHERE "${rightColumn}" = $2
|
||||
`;
|
||||
const updateResult = await query(updateRelatedQuery, [newLeftValue, oldLeftValue]);
|
||||
console.log(`✅ 연결된 ${rightTable} 테이블의 ${updateResult.length}개 레코드 업데이트 완료`);
|
||||
} catch (relError) {
|
||||
console.error("❌ 연결된 테이블 업데이트 실패:", relError);
|
||||
// 연결된 테이블 업데이트 실패 시 롤백은 하지 않고 경고만 로그
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: result[0],
|
||||
|
|
|
|||
|
|
@ -183,6 +183,15 @@ body {
|
|||
background: hsl(var(--background));
|
||||
}
|
||||
|
||||
/* Button 기본 커서 스타일 */
|
||||
button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* ===== Dialog/Modal Overlay ===== */
|
||||
/* Radix UI Dialog Overlay - 60% 불투명도 배경 */
|
||||
[data-radix-dialog-overlay],
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ export const dataApi = {
|
|||
*/
|
||||
updateRecord: async (tableName: string, id: string | number, data: Record<string, any>): Promise<any> => {
|
||||
const response = await apiClient.put(`/data/${tableName}/${id}`, data);
|
||||
return response.data?.data || response.data;
|
||||
return response.data; // success, data, message 포함된 전체 응답 반환
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
@ -102,7 +102,8 @@ export const dataApi = {
|
|||
* @param tableName 테이블명
|
||||
* @param id 레코드 ID
|
||||
*/
|
||||
deleteRecord: async (tableName: string, id: string | number): Promise<void> => {
|
||||
await apiClient.delete(`/data/${tableName}/${id}`);
|
||||
deleteRecord: async (tableName: string, id: string | number): Promise<any> => {
|
||||
const response = await apiClient.delete(`/data/${tableName}/${id}`);
|
||||
return response.data; // success, message 포함된 전체 응답 반환
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -771,7 +771,17 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
|||
return;
|
||||
}
|
||||
|
||||
console.log("🖼️ 대표 이미지 로드 시작:", file.realFileName);
|
||||
// objid가 없거나 유효하지 않으면 로드 중단
|
||||
if (!file.objid || file.objid === "0" || file.objid === "") {
|
||||
console.warn("⚠️ 대표 이미지 로드 실패: objid가 없음", file);
|
||||
setRepresentativeImageUrl(null);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("🖼️ 대표 이미지 로드 시작:", {
|
||||
objid: file.objid,
|
||||
fileName: file.realFileName,
|
||||
});
|
||||
|
||||
// API 클라이언트를 통해 Blob으로 다운로드 (인증 토큰 포함)
|
||||
const response = await apiClient.get(`/files/download/${file.objid}`, {
|
||||
|
|
@ -792,8 +802,12 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
|||
|
||||
setRepresentativeImageUrl(url);
|
||||
console.log("✅ 대표 이미지 로드 성공:", url);
|
||||
} catch (error) {
|
||||
console.error("❌ 대표 이미지 로드 실패:", error);
|
||||
} catch (error: any) {
|
||||
console.error("❌ 대표 이미지 로드 실패:", {
|
||||
file: file.realFileName,
|
||||
objid: file.objid,
|
||||
error: error?.response?.status || error?.message,
|
||||
});
|
||||
setRepresentativeImageUrl(null);
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -363,6 +363,22 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
|||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<Label>수정 버튼</Label>
|
||||
<Switch
|
||||
checked={config.leftPanel?.showEdit ?? false}
|
||||
onCheckedChange={(checked) => updateLeftPanel({ showEdit: checked })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<Label>삭제 버튼</Label>
|
||||
<Switch
|
||||
checked={config.leftPanel?.showDelete ?? false}
|
||||
onCheckedChange={(checked) => updateLeftPanel({ showDelete: checked })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<Label>각 항목에 + 버튼</Label>
|
||||
<Switch
|
||||
|
|
@ -1007,6 +1023,22 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
|||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<Label>수정 버튼</Label>
|
||||
<Switch
|
||||
checked={config.rightPanel?.showEdit ?? false}
|
||||
onCheckedChange={(checked) => updateRightPanel({ showEdit: checked })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<Label>삭제 버튼</Label>
|
||||
<Switch
|
||||
checked={config.rightPanel?.showDelete ?? false}
|
||||
onCheckedChange={(checked) => updateRightPanel({ showDelete: checked })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 우측 패널 표시 컬럼 설정 */}
|
||||
<div className="space-y-3 rounded-lg border border-green-200 bg-green-50 p-3">
|
||||
<div className="flex items-center justify-between">
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ export interface SplitPanelLayoutConfig {
|
|||
dataSource?: string; // API 엔드포인트
|
||||
showSearch?: boolean;
|
||||
showAdd?: boolean;
|
||||
showEdit?: boolean; // 수정 버튼
|
||||
showDelete?: boolean; // 삭제 버튼
|
||||
columns?: Array<{
|
||||
name: string;
|
||||
label: string;
|
||||
|
|
@ -45,6 +47,8 @@ export interface SplitPanelLayoutConfig {
|
|||
dataSource?: string;
|
||||
showSearch?: boolean;
|
||||
showAdd?: boolean;
|
||||
showEdit?: boolean; // 수정 버튼
|
||||
showDelete?: boolean; // 삭제 버튼
|
||||
columns?: Array<{
|
||||
name: string;
|
||||
label: string;
|
||||
|
|
|
|||
Loading…
Reference in New Issue