dev #46

Merged
kjs merged 344 commits from dev into main 2025-09-22 18:17:24 +09:00
5 changed files with 393 additions and 33 deletions
Showing only changes of commit 142cfe022b - Show all commits

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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;
}
}
}

View File

@ -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>
&ldquo;{selectedDiagramForAction?.diagramName}&rdquo; ?
<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>
&ldquo;{selectedDiagramForAction?.diagramName}&rdquo; ?
<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>
);
}

View File

@ -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;
}
}
}