digramId를 사용해 제어 관계 그룹화

This commit is contained in:
hyeonsu 2025-09-09 18:42:01 +09:00
parent 7bcd405a04
commit 5043b11149
11 changed files with 312 additions and 113 deletions

View File

@ -5108,6 +5108,7 @@ model code_info {
// 테이블 간 관계 정의
model table_relationships {
relationship_id Int @id @default(autoincrement())
diagram_id Int // 관계도 그룹 식별자
relationship_name String @db.VarChar(200)
from_table_name String @db.VarChar(100)
from_column_name String @db.VarChar(100)
@ -5127,8 +5128,10 @@ model table_relationships {
bridges data_relationship_bridge[]
@@index([company_code], map: "idx_table_relationships_company_code")
@@index([diagram_id], map: "idx_table_relationships_diagram_id")
@@index([from_table_name], map: "idx_table_relationships_from_table")
@@index([to_table_name], map: "idx_table_relationships_to_table")
@@index([company_code, diagram_id], map: "idx_table_relationships_company_diagram")
}
// 테이블 간 데이터 관계 중계 테이블 - 실제 데이터 연결 정보 저장

View File

@ -15,6 +15,7 @@ export async function createTableRelationship(
logger.info("=== 테이블 관계 생성 시작 ===");
const {
diagramId,
relationshipName,
fromTableName,
fromColumnName,
@ -52,6 +53,7 @@ export async function createTableRelationship(
const dataflowService = new DataflowService();
const relationship = await dataflowService.createTableRelationship({
diagramId: diagramId ? parseInt(diagramId) : undefined,
relationshipName,
fromTableName,
fromColumnName,
@ -828,7 +830,62 @@ export async function deleteDiagram(
}
/**
* relationship_id로
* diagram_id로
*/
export async function getDiagramRelationshipsByDiagramId(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const { diagramId } = req.params;
const companyCode = (req.user as any)?.company_code || "*";
if (!diagramId) {
const response: ApiResponse<null> = {
success: false,
message: "관계도 ID가 필요합니다.",
error: {
code: "MISSING_DIAGRAM_ID",
details: "diagramId 파라미터가 필요합니다.",
},
};
res.status(400).json(response);
return;
}
const dataflowService = new DataflowService();
const relationships =
await dataflowService.getDiagramRelationshipsByDiagramId(
companyCode,
parseInt(diagramId)
);
const response: ApiResponse<any[]> = {
success: true,
message: "관계도 관계 목록을 성공적으로 조회했습니다.",
data: relationships,
};
res.status(200).json(response);
} catch (error) {
logger.error("관계도 관계 조회 실패:", error);
const response: ApiResponse<null> = {
success: false,
message: "관계도 관계 조회에 실패했습니다.",
error: {
code: "DIAGRAM_RELATIONSHIPS_FETCH_FAILED",
details:
error instanceof Error
? error.message
: "알 수 없는 오류가 발생했습니다.",
},
};
res.status(500).json(response);
}
}
/**
* relationship_id로 ( )
*/
export async function getDiagramRelationshipsByRelationshipId(
req: AuthenticatedRequest,
@ -852,10 +909,11 @@ export async function getDiagramRelationshipsByRelationshipId(
}
const dataflowService = new DataflowService();
const relationships = await dataflowService.getDiagramRelationshipsByRelationshipId(
companyCode,
parseInt(relationshipId)
);
const relationships =
await dataflowService.getDiagramRelationshipsByRelationshipId(
companyCode,
parseInt(relationshipId)
);
const response: ApiResponse<any[]> = {
success: true,
@ -871,7 +929,10 @@ export async function getDiagramRelationshipsByRelationshipId(
message: "관계도 관계 조회에 실패했습니다.",
error: {
code: "DIAGRAM_RELATIONSHIPS_FETCH_FAILED",
details: error instanceof Error ? error.message : "알 수 없는 오류가 발생했습니다.",
details:
error instanceof Error
? error.message
: "알 수 없는 오류가 발생했습니다.",
},
};
res.status(500).json(response);

View File

@ -12,6 +12,7 @@ import {
getTableData,
getDataFlowDiagrams,
getDiagramRelationships,
getDiagramRelationshipsByDiagramId,
getDiagramRelationshipsByRelationshipId,
copyDiagram,
deleteDiagram,
@ -92,10 +93,22 @@ router.get("/table-data/:tableName", getTableData);
router.get("/diagrams", getDataFlowDiagrams);
/**
*
* GET /api/dataflow/diagrams/:diagramName/relationships
* (diagram_id로)
* GET /api/dataflow/diagrams/:diagramId/relationships
*/
router.get("/diagrams/:diagramName/relationships", getDiagramRelationships);
router.get(
"/diagrams/:diagramId/relationships",
getDiagramRelationshipsByDiagramId
);
/**
* (diagramName으로 - )
* GET /api/dataflow/diagrams/name/:diagramName/relationships
*/
router.get(
"/diagrams/name/:diagramName/relationships",
getDiagramRelationships
);
/**
*
@ -109,7 +122,7 @@ router.post("/diagrams/:diagramName/copy", copyDiagram);
*/
router.delete("/diagrams/:diagramName", deleteDiagram);
// relationship_id로 관계도 관계 조회
// relationship_id로 관계도 관계 조회 (하위 호환성)
router.get(
"/relationships/:relationshipId/diagram",
getDiagramRelationshipsByRelationshipId

View File

@ -5,6 +5,7 @@ const prisma = new PrismaClient();
// 테이블 관계 생성 데이터 타입
interface CreateTableRelationshipData {
diagramId?: number; // 기존 관계도에 추가하는 경우
relationshipName: string;
fromTableName: string;
fromColumnName: string;
@ -38,9 +39,31 @@ export class DataflowService {
try {
logger.info("DataflowService: 테이블 관계 생성 시작", data);
// 중복 관계 확인
// diagram_id 결정 로직
let diagramId = data.diagramId;
if (!diagramId) {
// 새로운 관계도인 경우, 새로운 diagram_id 생성
// 현재 최대 diagram_id + 1
const maxDiagramId = await prisma.table_relationships.findFirst({
where: {
company_code: data.companyCode,
},
orderBy: {
diagram_id: "desc",
},
select: {
diagram_id: true,
},
});
diagramId = (maxDiagramId?.diagram_id || 0) + 1;
}
// 중복 관계 확인 (같은 diagram_id 내에서)
const existingRelationship = await prisma.table_relationships.findFirst({
where: {
diagram_id: diagramId,
from_table_name: data.fromTableName,
from_column_name: data.fromColumnName,
to_table_name: data.toTableName,
@ -56,9 +79,10 @@ export class DataflowService {
);
}
// 새 관계 생성 (중계 테이블은 별도로 생성하지 않음)
// 새 관계 생성
const relationship = await prisma.table_relationships.create({
data: {
diagram_id: diagramId,
relationship_name: data.relationshipName,
from_table_name: data.fromTableName,
from_column_name: data.fromColumnName,
@ -74,7 +98,7 @@ export class DataflowService {
});
logger.info(
`DataflowService: 테이블 관계 생성 완료 - ID: ${relationship.relationship_id}`
`DataflowService: 테이블 관계 생성 완료 - ID: ${relationship.relationship_id}, Diagram ID: ${relationship.diagram_id}`
);
return relationship;
} catch (error) {
@ -731,7 +755,7 @@ export class DataflowService {
}
/**
* ( )
* (diagram_id별)
*/
async getDataFlowDiagrams(
companyCode: string,
@ -744,7 +768,7 @@ export class DataflowService {
`DataflowService: 관계도 목록 조회 시작 - ${companyCode}, page: ${page}, size: ${size}, search: ${searchTerm}`
);
// 관계도 이름별로 그룹화하여 조회
// diagram_id별로 그룹화하여 조회
const whereCondition = {
company_code: companyCode,
is_active: "Y",
@ -772,11 +796,12 @@ export class DataflowService {
}),
};
// 관계도별로 그룹화된 데이터 조회 (관계도 이름을 기준으로)
// diagram_id별로 그룹화된 데이터 조회
const relationships = await prisma.table_relationships.findMany({
where: whereCondition,
select: {
relationship_id: true,
diagram_id: true,
relationship_name: true,
from_table_name: true,
to_table_name: true,
@ -787,19 +812,19 @@ export class DataflowService {
updated_date: true,
updated_by: true,
},
orderBy: [{ relationship_name: "asc" }, { created_date: "desc" }],
orderBy: [{ diagram_id: "asc" }, { created_date: "desc" }],
});
// 관계도 이름별로 그룹화
const diagramMap = new Map<string, any>();
// diagram_id별로 그룹화
const diagramMap = new Map<number, any>();
relationships.forEach((rel) => {
const diagramName = rel.relationship_name;
const diagramId = rel.diagram_id;
if (!diagramMap.has(diagramName)) {
diagramMap.set(diagramName, {
relationshipId: rel.relationship_id, // 첫 번째 관계의 ID를 대표 ID로 사용
diagramName: diagramName,
if (!diagramMap.has(diagramId)) {
diagramMap.set(diagramId, {
diagramId: diagramId,
diagramName: rel.relationship_name, // 첫 번째 관계의 이름을 사용
connectionType: rel.connection_type,
relationshipType: rel.relationship_type,
tableCount: new Set<string>(),
@ -812,7 +837,7 @@ export class DataflowService {
});
}
const diagram = diagramMap.get(diagramName);
const diagram = diagramMap.get(diagramId);
diagram.tableCount.add(rel.from_table_name);
diagram.tableCount.add(rel.to_table_name);
diagram.relationshipCount++;
@ -893,7 +918,7 @@ export class DataflowService {
}
/**
*
* (diagram_id )
*/
async copyDiagram(
companyCode: string,
@ -936,11 +961,27 @@ export class DataflowService {
newDiagramName = `${originalDiagramName} (${counter})`;
}
// 새로운 diagram_id 생성
const maxDiagramId = await prisma.table_relationships.findFirst({
where: {
company_code: companyCode,
},
orderBy: {
diagram_id: "desc",
},
select: {
diagram_id: true,
},
});
const newDiagramId = (maxDiagramId?.diagram_id || 0) + 1;
// 트랜잭션으로 모든 관계 복사
const copiedRelationships = await prisma.$transaction(
originalRelationships.map((rel) =>
prisma.table_relationships.create({
data: {
diagram_id: newDiagramId,
relationship_name: newDiagramName,
from_table_name: rel.from_table_name,
from_column_name: rel.from_column_name,
@ -959,7 +1000,7 @@ export class DataflowService {
);
logger.info(
`DataflowService: 관계도 복사 완료 - ${originalDiagramName}${newDiagramName}, ${copiedRelationships.length}개 관계 복사`
`DataflowService: 관계도 복사 완료 - ${originalDiagramName}${newDiagramName} (diagram_id: ${newDiagramId}), ${copiedRelationships.length}개 관계 복사`
);
return newDiagramName;
@ -1002,7 +1043,46 @@ export class DataflowService {
}
/**
* relationship_id로
* diagram_id로
*/
async getDiagramRelationshipsByDiagramId(
companyCode: string,
diagramId: number
) {
try {
logger.info(
`DataflowService: diagram_id로 관계도 관계 조회 - ${diagramId}`
);
// diagram_id로 모든 관계 조회
const relationships = await prisma.table_relationships.findMany({
where: {
diagram_id: diagramId,
company_code: companyCode,
is_active: "Y",
},
orderBy: [{ relationship_id: "asc" }],
});
logger.info(
`DataflowService: diagram_id로 관계도 관계 조회 완료 - ${relationships.length}개 관계`
);
return relationships.map((rel) => ({
...rel,
settings: rel.settings as any,
}));
} catch (error) {
logger.error(
`DataflowService: diagram_id로 관계도 관계 조회 실패 - ${diagramId}`,
error
);
throw error;
}
}
/**
* relationship_id로 ( )
*/
async getDiagramRelationshipsByRelationshipId(
companyCode: string,
@ -1013,7 +1093,7 @@ export class DataflowService {
`DataflowService: relationship_id로 관계도 관계 조회 - ${relationshipId}`
);
// 먼저 해당 relationship_id의 관계도명을 찾음
// 먼저 해당 relationship_id의 diagram_id를 찾음
const targetRelationship = await prisma.table_relationships.findFirst({
where: {
relationship_id: relationshipId,
@ -1021,7 +1101,7 @@ export class DataflowService {
is_active: "Y",
},
select: {
relationship_name: true,
diagram_id: true,
},
});
@ -1029,24 +1109,11 @@ export class DataflowService {
throw new Error("해당 관계 ID를 찾을 수 없습니다.");
}
// 같은 관계도명을 가진 모든 관계 조회
const relationships = await prisma.table_relationships.findMany({
where: {
relationship_name: targetRelationship.relationship_name,
company_code: companyCode,
is_active: "Y",
},
orderBy: [{ relationship_id: "asc" }],
});
logger.info(
`DataflowService: relationship_id로 관계도 관계 조회 완료 - ${relationships.length}개 관계`
// diagram_id로 모든 관계 조회
return this.getDiagramRelationshipsByDiagramId(
companyCode,
targetRelationship.diagram_id
);
return relationships.map((rel) => ({
...rel,
settings: rel.settings as any,
}));
} catch (error) {
logger.error(
`DataflowService: relationship_id로 관계도 관계 조회 실패 - ${relationshipId}`,

View File

@ -11,19 +11,19 @@ import { toast } from "sonner";
export default function DataFlowEditPage() {
const params = useParams();
const router = useRouter();
const [relationshipId, setRelationshipId] = useState<string>("");
const [diagramId, setDiagramId] = useState<number>(0);
const [diagramName, setDiagramName] = useState<string>("");
useEffect(() => {
if (params.relationshipId) {
// URL에서 relationship_id 설정
const id = params.relationshipId as string;
setRelationshipId(id);
if (params.diagramId) {
// URL에서 diagram_id 설정
const id = parseInt(params.diagramId as string);
setDiagramId(id);
// relationship_id로 관계도명 조회
// diagram_id로 관계도명 조회
const fetchDiagramName = async () => {
try {
const relationships = await DataFlowAPI.getDiagramRelationshipsByRelationshipId(id);
const relationships = await DataFlowAPI.getDiagramRelationshipsByDiagramId(id);
if (relationships.length > 0) {
setDiagramName(relationships[0].relationship_name);
} else {
@ -37,13 +37,13 @@ export default function DataFlowEditPage() {
fetchDiagramName();
}
}, [params.relationshipId]);
}, [params.diagramId]);
const handleBackToList = () => {
router.push("/admin/dataflow");
};
if (!relationshipId || !diagramName) {
if (!diagramId || !diagramName) {
return (
<div className="flex h-64 items-center justify-center">
<div className="text-center">
@ -74,11 +74,7 @@ export default function DataFlowEditPage() {
{/* 데이터플로우 디자이너 */}
<div className="rounded-lg border border-gray-200 bg-white">
<DataFlowDesigner
selectedDiagram={diagramName}
relationshipId={relationshipId}
onBackToList={handleBackToList}
/>
<DataFlowDesigner selectedDiagram={diagramName} diagramId={diagramId} onBackToList={handleBackToList} />
</div>
</div>
);

View File

@ -72,8 +72,10 @@ export default function DataFlowPage() {
const handleSave = (relationships: TableRelationship[]) => {
console.log("저장된 관계:", relationships);
// 저장 후 목록으로 돌아가기
goToStep("list");
// 저장 후 목록으로 돌아가기 - 다음 렌더링 사이클로 지연
setTimeout(() => {
goToStep("list");
}, 0);
};
const handleDiagramSelect = (diagram: DataFlowDiagram) => {
@ -83,7 +85,7 @@ export default function DataFlowPage() {
const handleDesignDiagram = (diagram: DataFlowDiagram | null) => {
if (diagram) {
// 기존 관계도 편집 - 새로운 URL로 이동
router.push(`/admin/dataflow/edit/${diagram.relationshipId}`);
router.push(`/admin/dataflow/edit/${diagram.diagramId}`);
} else {
// 새 관계도 생성 - 현재 페이지에서 처리
setSelectedDiagram(null);

View File

@ -70,6 +70,7 @@ interface ConnectionSetupModalProps {
isOpen: boolean;
connection: ConnectionInfo | null;
companyCode: string;
diagramId?: number;
onConfirm: (relationship: TableRelationship) => void;
onCancel: () => void;
}
@ -78,6 +79,7 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
isOpen,
connection,
companyCode,
diagramId,
onConfirm,
onCancel,
}) => {
@ -188,15 +190,17 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
toast.loading("관계를 생성하고 있습니다...", { id: "create-relationship" });
// 단일 관계 데이터 준비 (모든 선택된 컬럼 정보 포함)
const relationshipData: Omit<TableRelationship, "relationship_id"> = {
relationship_name: config.relationshipName,
from_table_name: connection.fromNode.tableName,
from_column_name: fromColumns.join(","), // 여러 컬럼을 콤마로 구분
to_table_name: connection.toNode.tableName,
to_column_name: toColumns.join(","), // 여러 컬럼을 콤마로 구분
relationship_type: config.relationshipType,
connection_type: config.connectionType,
company_code: companyCode,
// API 요청용 데이터 (camelCase)
const apiRequestData = {
...(diagramId && diagramId > 0 ? { diagramId: diagramId } : {}), // diagramId가 유효할 때만 추가
relationshipName: config.relationshipName,
fromTableName: connection.fromNode.tableName,
fromColumnName: fromColumns.join(","), // 여러 컬럼을 콤마로 구분
toTableName: connection.toNode.tableName,
toColumnName: toColumns.join(","), // 여러 컬럼을 콤마로 구분
relationshipType: config.relationshipType,
connectionType: config.connectionType,
companyCode: companyCode,
settings: {
...settings,
multiColumnMapping: {
@ -211,11 +215,10 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
to: toColumns.length,
},
},
is_active: "Y",
};
// API 호출
const createdRelationship = await DataFlowAPI.createRelationship(relationshipData);
const createdRelationship = await DataFlowAPI.createRelationship(apiRequestData as any);
toast.success("관계가 성공적으로 생성되었습니다!", { id: "create-relationship" });

View File

@ -20,10 +20,10 @@ import { ConnectionSetupModal } from "./ConnectionSetupModal";
import { TableDefinition, TableRelationship, DataFlowAPI, DataFlowDiagram } from "@/lib/api/dataflow";
// 고유 ID 생성 함수
const generateUniqueId = (prefix: string, relationshipId?: number): string => {
const generateUniqueId = (prefix: string, diagramId?: number): string => {
const timestamp = Date.now();
const random = Math.random().toString(36).substr(2, 9);
return `${prefix}-${relationshipId || timestamp}-${random}`;
return `${prefix}-${diagramId || timestamp}-${random}`;
};
// 테이블 노드 데이터 타입 정의
@ -53,7 +53,8 @@ interface DataFlowDesignerProps {
companyCode?: string;
onSave?: (relationships: TableRelationship[]) => void;
selectedDiagram?: DataFlowDiagram | string | null;
relationshipId?: string;
diagramId?: number;
relationshipId?: string; // 하위 호환성 유지
onBackToList?: () => void;
}
@ -61,7 +62,8 @@ interface DataFlowDesignerProps {
export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
companyCode = "*",
relationshipId,
diagramId,
relationshipId, // 하위 호환성 유지
onSave,
selectedDiagram,
onBackToList, // eslint-disable-line @typescript-eslint/no-unused-vars
@ -86,6 +88,7 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
};
} | null>(null);
const [relationships, setRelationships] = useState<TableRelationship[]>([]); // eslint-disable-line @typescript-eslint/no-unused-vars
const [currentDiagramId, setCurrentDiagramId] = useState<number | null>(null); // 현재 화면의 diagram_id
const toastShownRef = useRef(false); // eslint-disable-line @typescript-eslint/no-unused-vars
// 키보드 이벤트 핸들러 (Del 키로 선택된 노드 삭제)
@ -169,15 +172,21 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
// 선택된 관계도의 관계 로드
const loadSelectedDiagramRelationships = useCallback(async () => {
if (!relationshipId) return;
const currentDiagramId = diagramId || (relationshipId ? parseInt(relationshipId) : null);
if (!currentDiagramId || isNaN(currentDiagramId)) return;
try {
console.log("🔍 관계도 로드 시작 (relationshipId):", relationshipId);
console.log("🔍 관계도 로드 시작 (diagramId):", currentDiagramId);
toast.loading("관계도를 불러오는 중...", { id: "load-diagram" });
// relationshipId로 해당 관계도의 모든 관계 조회
const diagramRelationships = await DataFlowAPI.getDiagramRelationshipsByRelationshipId(relationshipId);
// diagramId로 해당 관계도의 모든 관계 조회
const diagramRelationships = await DataFlowAPI.getDiagramRelationshipsByDiagramId(currentDiagramId);
console.log("📋 관계도 관계 데이터:", diagramRelationships);
if (!Array.isArray(diagramRelationships)) {
throw new Error("관계도 데이터 형식이 올바르지 않습니다.");
}
console.log("📋 첫 번째 관계 상세:", diagramRelationships[0]);
console.log(
"📋 관계 객체 키들:",
@ -185,11 +194,18 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
);
setRelationships(diagramRelationships);
// 현재 diagram_id 설정 (기존 관계도 편집 시)
if (diagramRelationships.length > 0) {
setCurrentDiagramId(diagramRelationships[0].diagram_id || null);
}
// 관계도의 모든 테이블 추출
const tableNames = new Set<string>();
diagramRelationships.forEach((rel) => {
tableNames.add(rel.from_table_name);
tableNames.add(rel.to_table_name);
if (rel && rel.from_table_name && rel.to_table_name) {
tableNames.add(rel.from_table_name);
tableNames.add(rel.to_table_name);
}
});
console.log("📊 추출된 테이블 이름들:", Array.from(tableNames));
@ -222,10 +238,21 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
} = {};
diagramRelationships.forEach((rel) => {
if (!rel || !rel.from_table_name || !rel.to_table_name || !rel.from_column_name || !rel.to_column_name) {
console.warn("⚠️ 관계 데이터가 불완전합니다:", rel);
return;
}
const fromTable = rel.from_table_name;
const toTable = rel.to_table_name;
const fromColumns = rel.from_column_name.split(",").map((col) => col.trim());
const toColumns = rel.to_column_name.split(",").map((col) => col.trim());
const fromColumns = rel.from_column_name
.split(",")
.map((col) => col.trim())
.filter((col) => col);
const toColumns = rel.to_column_name
.split(",")
.map((col) => col.trim())
.filter((col) => col);
// 소스 테이블의 컬럼들을 source로 표시
if (!connectedColumnsInfo[fromTable]) connectedColumnsInfo[fromTable] = {};
@ -283,12 +310,28 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
const relationshipEdges: Edge[] = [];
diagramRelationships.forEach((rel) => {
if (!rel || !rel.from_table_name || !rel.to_table_name || !rel.from_column_name || !rel.to_column_name) {
console.warn("⚠️ 에지 생성 시 관계 데이터가 불완전합니다:", rel);
return;
}
const fromTable = rel.from_table_name;
const toTable = rel.to_table_name;
const fromColumns = rel.from_column_name.split(",").map((col) => col.trim());
const toColumns = rel.to_column_name.split(",").map((col) => col.trim());
const fromColumns = rel.from_column_name
.split(",")
.map((col) => col.trim())
.filter((col) => col);
const toColumns = rel.to_column_name
.split(",")
.map((col) => col.trim())
.filter((col) => col);
// 각 from 컬럼을 각 to 컬럼에 연결 (1:1 매핑이거나 many:many인 경우)
if (fromColumns.length === 0 || toColumns.length === 0) {
console.warn("⚠️ 컬럼 정보가 없습니다:", { fromColumns, toColumns });
return;
}
const maxConnections = Math.max(fromColumns.length, toColumns.length);
for (let i = 0; i < maxConnections; i++) {
@ -296,7 +339,7 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
const toColumn = toColumns[i] || toColumns[0]; // 컬럼이 부족하면 첫 번째 컬럼 재사용
relationshipEdges.push({
id: generateUniqueId("edge", rel.relationship_id),
id: generateUniqueId("edge", rel.diagram_id),
source: `table-${fromTable}`,
target: `table-${toTable}`,
sourceHandle: `${fromTable}-${fromColumn}-source`,
@ -329,7 +372,7 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
console.error("선택된 관계도 로드 실패:", error);
toast.error("관계도를 불러오는데 실패했습니다.", { id: "load-diagram" });
}
}, [relationshipId, setNodes, setEdges, selectedColumns, handleColumnClick]);
}, [diagramId, relationshipId, setNodes, setEdges, selectedColumns, handleColumnClick]);
// 기존 관계 로드 (새 관계도 생성 시)
const loadExistingRelationships = useCallback(async () => {
@ -356,7 +399,7 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
const toColumn = toColumns[i] || toColumns[0];
existingEdges.push({
id: generateUniqueId("edge", rel.relationship_id),
id: generateUniqueId("edge", rel.diagram_id),
source: `table-${fromTable}`,
target: `table-${toTable}`,
sourceHandle: `${fromTable}-${fromColumn}-source`,
@ -391,13 +434,13 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
// 컴포넌트 마운트 시 관계 로드
useEffect(() => {
if (companyCode) {
if (relationshipId) {
if (diagramId || relationshipId) {
loadSelectedDiagramRelationships();
} else {
loadExistingRelationships();
}
}
}, [companyCode, relationshipId, loadExistingRelationships, loadSelectedDiagramRelationships]);
}, [companyCode, diagramId, relationshipId, loadExistingRelationships, loadSelectedDiagramRelationships]);
// 노드 선택 변경 핸들러
const onSelectionChange = useCallback(({ nodes }: { nodes: Node<TableNodeData>[] }) => {
@ -561,6 +604,7 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
setSelectedColumns({});
setSelectionOrder([]);
setSelectedNodes([]);
setCurrentDiagramId(null); // 현재 diagram_id도 초기화
}, [setNodes, setEdges]);
// 현재 추가된 테이블명 목록 가져오기
@ -587,7 +631,7 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
const toColumn = toColumns[i] || toColumns[0];
newEdges.push({
id: generateUniqueId("edge", relationship.relationship_id),
id: generateUniqueId("edge", relationship.diagram_id),
source: pendingConnection.fromNode.id,
target: pendingConnection.toNode.id,
sourceHandle: `${fromTable}-${fromColumn}-source`,
@ -615,17 +659,19 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
setRelationships((prev) => [...prev, relationship]);
setPendingConnection(null);
console.log("관계 생성 완료:", relationship);
// 저장 콜백 호출 (필요한 경우)
if (onSave) {
// 현재 모든 관계를 수집하여 전달
setRelationships((currentRelationships) => {
onSave([...currentRelationships, relationship]);
return currentRelationships;
});
// 첫 번째 관계 생성 시 currentDiagramId 설정 (새 관계도 생성 시)
if (!currentDiagramId && relationship.diagram_id) {
setCurrentDiagramId(relationship.diagram_id);
}
console.log("관계 생성 완료:", relationship);
// 관계 생성 완료 후 자동으로 목록 새로고침을 위한 콜백 (선택적)
// 렌더링 중 상태 업데이트 방지를 위해 제거
// if (onSave) {
// onSave([...relationships, relationship]);
// }
},
[pendingConnection, setEdges, onSave],
[pendingConnection, setEdges, currentDiagramId],
);
// 연결 설정 취소
@ -684,6 +730,10 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
<span>:</span>
<span className="font-medium">{edges.length}</span>
</div>
<div className="flex justify-between">
<span> ID:</span>
<span className="font-medium">{currentDiagramId || "미설정"}</span>
</div>
</div>
</div>
@ -806,6 +856,7 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
isOpen={!!pendingConnection}
connection={pendingConnection}
companyCode={companyCode}
diagramId={currentDiagramId || diagramId || (relationshipId ? parseInt(relationshipId) : undefined)}
onConfirm={handleConfirmConnection}
onCancel={handleCancelConnection}
/>

View File

@ -232,9 +232,9 @@ export default function DataFlowList({ onDiagramSelect, selectedDiagram, onDesig
<TableBody>
{diagrams.map((diagram) => (
<TableRow
key={diagram.diagramName}
key={diagram.diagramId}
className={`cursor-pointer hover:bg-gray-50 ${
selectedDiagram?.diagramName === diagram.diagramName ? "border-blue-200 bg-blue-50" : ""
selectedDiagram?.diagramId === diagram.diagramId ? "border-blue-200 bg-blue-50" : ""
}`}
onClick={() => handleDiagramSelect(diagram)}
>

View File

@ -15,8 +15,8 @@ const getApiBaseUrl = (): string => {
port: currentPort,
});
// 로컬 개발환경: localhost:9771 → localhost:8080
if ((currentHost === "localhost" || currentHost === "127.0.0.1") && currentPort === "9771") {
// 로컬 개발환경: localhost:9771 또는 localhost:3000 → localhost:8080
if ((currentHost === "localhost" || currentHost === "127.0.0.1") && (currentPort === "9771" || currentPort === "3000")) {
console.log("🏠 로컬 개발 환경 감지 → localhost:8080/api");
return "http://localhost:8080/api";
}

View File

@ -39,6 +39,7 @@ export interface TableInfo {
export interface TableRelationship {
relationship_id?: number;
diagram_id?: number; // 새 관계도 생성 시에는 optional
relationship_name: string;
from_table_name: string;
from_column_name: string;
@ -89,7 +90,7 @@ export interface TableDataResponse {
// 관계도 정보 인터페이스
export interface DataFlowDiagram {
relationshipId: number;
diagramId: number;
diagramName: string;
connectionType: string;
relationshipType: string;
@ -173,7 +174,9 @@ export class DataFlowAPI {
/**
*
*/
static async createRelationship(relationship: Omit<TableRelationship, "relationshipId">): Promise<TableRelationship> {
static async createRelationship(
relationship: any, // 백엔드 API 형식 (camelCase)
): Promise<TableRelationship> {
try {
const response = await apiClient.post<ApiResponse<TableRelationship>>(
"/dataflow/table-relationships",
@ -435,11 +438,11 @@ export class DataFlowAPI {
}
}
// 특정 관계도의 모든 관계 조회 (relationship_id로)
static async getDiagramRelationshipsByRelationshipId(relationshipId: string): Promise<TableRelationship[]> {
// 특정 관계도의 모든 관계 조회 (diagram_id로)
static async getDiagramRelationshipsByDiagramId(diagramId: number): Promise<TableRelationship[]> {
try {
const response = await apiClient.get<ApiResponse<TableRelationship[]>>(
`/dataflow/relationships/${relationshipId}/diagram`,
`/dataflow/diagrams/${diagramId}/relationships`,
);
if (!response.data.success) {