dev #46
|
|
@ -718,3 +718,111 @@ export async function getDiagramRelationships(
|
|||
res.status(500).json(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 관계도 복사
|
||||
*/
|
||||
export async function copyDiagram(
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
): Promise<void> {
|
||||
try {
|
||||
const { diagramName } = req.params;
|
||||
const companyCode = (req.user as any)?.company_code || "*";
|
||||
|
||||
if (!diagramName) {
|
||||
const response: ApiResponse<null> = {
|
||||
success: false,
|
||||
message: "관계도 이름이 필요합니다.",
|
||||
error: {
|
||||
code: "MISSING_DIAGRAM_NAME",
|
||||
details: "diagramName 파라미터가 필요합니다.",
|
||||
},
|
||||
};
|
||||
res.status(400).json(response);
|
||||
return;
|
||||
}
|
||||
|
||||
const dataflowService = new DataflowService();
|
||||
const newDiagramName = await dataflowService.copyDiagram(
|
||||
companyCode,
|
||||
decodeURIComponent(diagramName)
|
||||
);
|
||||
|
||||
const response: ApiResponse<{ newDiagramName: string }> = {
|
||||
success: true,
|
||||
message: "관계도가 성공적으로 복사되었습니다.",
|
||||
data: { newDiagramName },
|
||||
};
|
||||
|
||||
res.status(200).json(response);
|
||||
} catch (error) {
|
||||
logger.error("관계도 복사 실패:", error);
|
||||
const response: ApiResponse<null> = {
|
||||
success: false,
|
||||
message: "관계도 복사에 실패했습니다.",
|
||||
error: {
|
||||
code: "DIAGRAM_COPY_FAILED",
|
||||
details:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "알 수 없는 오류가 발생했습니다.",
|
||||
},
|
||||
};
|
||||
res.status(500).json(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 관계도 삭제
|
||||
*/
|
||||
export async function deleteDiagram(
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
): Promise<void> {
|
||||
try {
|
||||
const { diagramName } = req.params;
|
||||
const companyCode = (req.user as any)?.company_code || "*";
|
||||
|
||||
if (!diagramName) {
|
||||
const response: ApiResponse<null> = {
|
||||
success: false,
|
||||
message: "관계도 이름이 필요합니다.",
|
||||
error: {
|
||||
code: "MISSING_DIAGRAM_NAME",
|
||||
details: "diagramName 파라미터가 필요합니다.",
|
||||
},
|
||||
};
|
||||
res.status(400).json(response);
|
||||
return;
|
||||
}
|
||||
|
||||
const dataflowService = new DataflowService();
|
||||
const deletedCount = await dataflowService.deleteDiagram(
|
||||
companyCode,
|
||||
decodeURIComponent(diagramName)
|
||||
);
|
||||
|
||||
const response: ApiResponse<{ deletedCount: number }> = {
|
||||
success: true,
|
||||
message: "관계도가 성공적으로 삭제되었습니다.",
|
||||
data: { deletedCount },
|
||||
};
|
||||
|
||||
res.status(200).json(response);
|
||||
} catch (error) {
|
||||
logger.error("관계도 삭제 실패:", error);
|
||||
const response: ApiResponse<null> = {
|
||||
success: false,
|
||||
message: "관계도 삭제에 실패했습니다.",
|
||||
error: {
|
||||
code: "DIAGRAM_DELETE_FAILED",
|
||||
details:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "알 수 없는 오류가 발생했습니다.",
|
||||
},
|
||||
};
|
||||
res.status(500).json(response);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ import {
|
|||
getTableData,
|
||||
getDataFlowDiagrams,
|
||||
getDiagramRelationships,
|
||||
copyDiagram,
|
||||
deleteDiagram,
|
||||
} from "../controllers/dataflowController";
|
||||
|
||||
const router = express.Router();
|
||||
|
|
@ -94,4 +96,16 @@ router.get("/diagrams", getDataFlowDiagrams);
|
|||
*/
|
||||
router.get("/diagrams/:diagramName/relationships", getDiagramRelationships);
|
||||
|
||||
/**
|
||||
* 관계도 복사
|
||||
* POST /api/dataflow/diagrams/:diagramName/copy
|
||||
*/
|
||||
router.post("/diagrams/:diagramName/copy", copyDiagram);
|
||||
|
||||
/**
|
||||
* 관계도 삭제
|
||||
* DELETE /api/dataflow/diagrams/:diagramName
|
||||
*/
|
||||
router.delete("/diagrams/:diagramName", deleteDiagram);
|
||||
|
||||
export default router;
|
||||
|
|
|
|||
|
|
@ -889,4 +889,113 @@ export class DataflowService {
|
|||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 관계도 복사
|
||||
*/
|
||||
async copyDiagram(
|
||||
companyCode: string,
|
||||
originalDiagramName: string
|
||||
): Promise<string> {
|
||||
try {
|
||||
logger.info(`DataflowService: 관계도 복사 시작 - ${originalDiagramName}`);
|
||||
|
||||
// 원본 관계도의 모든 관계 조회
|
||||
const originalRelationships = await prisma.table_relationships.findMany({
|
||||
where: {
|
||||
company_code: companyCode,
|
||||
relationship_name: originalDiagramName,
|
||||
is_active: "Y",
|
||||
},
|
||||
});
|
||||
|
||||
if (originalRelationships.length === 0) {
|
||||
throw new Error("복사할 관계도를 찾을 수 없습니다.");
|
||||
}
|
||||
|
||||
// 새로운 관계도 이름 생성 (중복 검사)
|
||||
let newDiagramName = `${originalDiagramName} (1)`;
|
||||
let counter = 1;
|
||||
|
||||
while (true) {
|
||||
const existingDiagram = await prisma.table_relationships.findFirst({
|
||||
where: {
|
||||
company_code: companyCode,
|
||||
relationship_name: newDiagramName,
|
||||
is_active: "Y",
|
||||
},
|
||||
});
|
||||
|
||||
if (!existingDiagram) {
|
||||
break;
|
||||
}
|
||||
|
||||
counter++;
|
||||
newDiagramName = `${originalDiagramName} (${counter})`;
|
||||
}
|
||||
|
||||
// 트랜잭션으로 모든 관계 복사
|
||||
const copiedRelationships = await prisma.$transaction(
|
||||
originalRelationships.map((rel) =>
|
||||
prisma.table_relationships.create({
|
||||
data: {
|
||||
relationship_name: newDiagramName,
|
||||
from_table_name: rel.from_table_name,
|
||||
from_column_name: rel.from_column_name,
|
||||
to_table_name: rel.to_table_name,
|
||||
to_column_name: rel.to_column_name,
|
||||
relationship_type: rel.relationship_type,
|
||||
connection_type: rel.connection_type,
|
||||
settings: rel.settings as any,
|
||||
company_code: rel.company_code,
|
||||
is_active: "Y",
|
||||
created_by: rel.created_by,
|
||||
updated_by: rel.updated_by,
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
logger.info(
|
||||
`DataflowService: 관계도 복사 완료 - ${originalDiagramName} → ${newDiagramName}, ${copiedRelationships.length}개 관계 복사`
|
||||
);
|
||||
|
||||
return newDiagramName;
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`DataflowService: 관계도 복사 실패 - ${originalDiagramName}`,
|
||||
error
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 관계도 삭제
|
||||
*/
|
||||
async deleteDiagram(
|
||||
companyCode: string,
|
||||
diagramName: string
|
||||
): Promise<number> {
|
||||
try {
|
||||
logger.info(`DataflowService: 관계도 삭제 시작 - ${diagramName}`);
|
||||
|
||||
// 관계도의 모든 관계 삭제 (하드 삭제)
|
||||
const deleteResult = await prisma.table_relationships.deleteMany({
|
||||
where: {
|
||||
company_code: companyCode,
|
||||
relationship_name: diagramName,
|
||||
},
|
||||
});
|
||||
|
||||
logger.info(
|
||||
`DataflowService: 관계도 삭제 완료 - ${diagramName}, ${deleteResult.count}개 관계 삭제`
|
||||
);
|
||||
|
||||
return deleteResult.count;
|
||||
} catch (error) {
|
||||
logger.error(`DataflowService: 관계도 삭제 실패 - ${diagramName}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
|
|
@ -12,6 +12,14 @@ import {
|
|||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { MoreHorizontal, Trash2, Copy, Plus, Search, Network, Database, Calendar, User } from "lucide-react";
|
||||
import { DataFlowAPI, DataFlowDiagram } from "@/lib/api/dataflow";
|
||||
import { toast } from "sonner";
|
||||
|
|
@ -30,49 +38,86 @@ export default function DataFlowList({ onDiagramSelect, selectedDiagram, onDesig
|
|||
const [totalPages, setTotalPages] = useState(1);
|
||||
const [total, setTotal] = useState(0);
|
||||
|
||||
// 모달 상태
|
||||
const [showCopyModal, setShowCopyModal] = useState(false);
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||
const [selectedDiagramForAction, setSelectedDiagramForAction] = useState<DataFlowDiagram | null>(null);
|
||||
|
||||
// 목록 로드 함수 분리
|
||||
const loadDiagrams = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await DataFlowAPI.getDataFlowDiagrams(currentPage, 20, searchTerm);
|
||||
setDiagrams(response.diagrams || []);
|
||||
setTotal(response.total || 0);
|
||||
setTotalPages(Math.max(1, Math.ceil((response.total || 0) / 20)));
|
||||
} catch (error) {
|
||||
console.error("관계도 목록 조회 실패", error);
|
||||
toast.error("관계도 목록을 불러오는데 실패했습니다.");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [currentPage, searchTerm]);
|
||||
|
||||
// 관계도 목록 로드
|
||||
useEffect(() => {
|
||||
let abort = false;
|
||||
const load = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await DataFlowAPI.getDataFlowDiagrams(currentPage, 20, searchTerm);
|
||||
if (abort) return;
|
||||
|
||||
setDiagrams(response.diagrams || []);
|
||||
setTotal(response.total || 0);
|
||||
setTotalPages(Math.max(1, Math.ceil((response.total || 0) / 20)));
|
||||
} catch (error) {
|
||||
console.error("관계도 목록 조회 실패", error);
|
||||
setDiagrams([]);
|
||||
setTotalPages(1);
|
||||
setTotal(0);
|
||||
toast.error("관계도 목록을 불러오는데 실패했습니다.");
|
||||
} finally {
|
||||
if (!abort) setLoading(false);
|
||||
}
|
||||
};
|
||||
load();
|
||||
return () => {
|
||||
abort = true;
|
||||
};
|
||||
}, [currentPage, searchTerm]);
|
||||
loadDiagrams();
|
||||
}, [loadDiagrams]);
|
||||
|
||||
const handleDiagramSelect = (diagram: DataFlowDiagram) => {
|
||||
onDiagramSelect(diagram);
|
||||
};
|
||||
|
||||
const handleDelete = (diagram: DataFlowDiagram) => {
|
||||
if (confirm(`"${diagram.diagramName}" 관계도를 삭제하시겠습니까?`)) {
|
||||
// 삭제 API 호출
|
||||
console.log("삭제:", diagram);
|
||||
toast.info("삭제 기능은 아직 구현되지 않았습니다.");
|
||||
}
|
||||
setSelectedDiagramForAction(diagram);
|
||||
setShowDeleteModal(true);
|
||||
};
|
||||
|
||||
const handleCopy = (diagram: DataFlowDiagram) => {
|
||||
console.log("복사:", diagram);
|
||||
toast.info("복사 기능은 아직 구현되지 않았습니다.");
|
||||
setSelectedDiagramForAction(diagram);
|
||||
setShowCopyModal(true);
|
||||
};
|
||||
|
||||
// 복사 확인
|
||||
const handleConfirmCopy = async () => {
|
||||
if (!selectedDiagramForAction) return;
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
const newDiagramName = await DataFlowAPI.copyDiagram(selectedDiagramForAction.diagramName);
|
||||
toast.success(`관계도가 성공적으로 복사되었습니다: ${newDiagramName}`);
|
||||
|
||||
// 목록 새로고침
|
||||
await loadDiagrams();
|
||||
} catch (error) {
|
||||
console.error("관계도 복사 실패:", error);
|
||||
toast.error("관계도 복사에 실패했습니다.");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setShowCopyModal(false);
|
||||
setSelectedDiagramForAction(null);
|
||||
}
|
||||
};
|
||||
|
||||
// 삭제 확인
|
||||
const handleConfirmDelete = async () => {
|
||||
if (!selectedDiagramForAction) return;
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
const deletedCount = await DataFlowAPI.deleteDiagram(selectedDiagramForAction.diagramName);
|
||||
toast.success(`관계도가 삭제되었습니다 (${deletedCount}개 관계 삭제)`);
|
||||
|
||||
// 목록 새로고침
|
||||
await loadDiagrams();
|
||||
} catch (error) {
|
||||
console.error("관계도 삭제 실패:", error);
|
||||
toast.error("관계도 삭제에 실패했습니다.");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setShowDeleteModal(false);
|
||||
setSelectedDiagramForAction(null);
|
||||
}
|
||||
};
|
||||
|
||||
// 연결 타입에 따른 배지 색상
|
||||
|
|
@ -291,6 +336,52 @@ export default function DataFlowList({ onDiagramSelect, selectedDiagram, onDesig
|
|||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 복사 확인 모달 */}
|
||||
<Dialog open={showCopyModal} onOpenChange={setShowCopyModal}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>관계도 복사</DialogTitle>
|
||||
<DialogDescription>
|
||||
“{selectedDiagramForAction?.diagramName}” 관계도를 복사하시겠습니까?
|
||||
<br />
|
||||
새로운 관계도는 원본 이름 뒤에 (1), (2), (3)... 형태로 생성됩니다.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setShowCopyModal(false)}>
|
||||
취소
|
||||
</Button>
|
||||
<Button onClick={handleConfirmCopy} disabled={loading}>
|
||||
{loading ? "복사 중..." : "복사"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* 삭제 확인 모달 */}
|
||||
<Dialog open={showDeleteModal} onOpenChange={setShowDeleteModal}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-red-600">관계도 삭제</DialogTitle>
|
||||
<DialogDescription>
|
||||
“{selectedDiagramForAction?.diagramName}” 관계도를 완전히 삭제하시겠습니까?
|
||||
<br />
|
||||
<span className="font-medium text-red-600">
|
||||
이 작업은 되돌릴 수 없으며, 모든 관계 정보가 영구적으로 삭제됩니다.
|
||||
</span>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setShowDeleteModal(false)}>
|
||||
취소
|
||||
</Button>
|
||||
<Button variant="destructive" onClick={handleConfirmDelete} disabled={loading}>
|
||||
{loading ? "삭제 중..." : "삭제"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -395,4 +395,42 @@ export class DataFlowAPI {
|
|||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 관계도 복사
|
||||
static async copyDiagram(diagramName: string): Promise<string> {
|
||||
try {
|
||||
const encodedDiagramName = encodeURIComponent(diagramName);
|
||||
const response = await apiClient.post<ApiResponse<{ newDiagramName: string }>>(
|
||||
`/dataflow/diagrams/${encodedDiagramName}/copy`,
|
||||
);
|
||||
|
||||
if (!response.data.success) {
|
||||
throw new Error(response.data.message || "관계도 복사에 실패했습니다.");
|
||||
}
|
||||
|
||||
return response.data.data?.newDiagramName || "";
|
||||
} catch (error) {
|
||||
console.error("관계도 복사 오류:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 관계도 삭제
|
||||
static async deleteDiagram(diagramName: string): Promise<number> {
|
||||
try {
|
||||
const encodedDiagramName = encodeURIComponent(diagramName);
|
||||
const response = await apiClient.delete<ApiResponse<{ deletedCount: number }>>(
|
||||
`/dataflow/diagrams/${encodedDiagramName}`,
|
||||
);
|
||||
|
||||
if (!response.data.success) {
|
||||
throw new Error(response.data.message || "관계도 삭제에 실패했습니다.");
|
||||
}
|
||||
|
||||
return response.data.data?.deletedCount || 0;
|
||||
} catch (error) {
|
||||
console.error("관계도 삭제 오류:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue