기타 수정
This commit is contained in:
parent
b02e9610ea
commit
37fac630b9
|
|
@ -623,13 +623,16 @@ PUT /api/external-call-configs/:id
|
||||||
- [x] 노드 간 드래그앤드롭 연결 기능
|
- [x] 노드 간 드래그앤드롭 연결 기능
|
||||||
- [x] 줌, 팬, 미니맵 등 React Flow 기본 기능
|
- [x] 줌, 팬, 미니맵 등 React Flow 기본 기능
|
||||||
|
|
||||||
### Phase 2: 관계 설정 기능 (2주) - 🚧 **진행 중 (70% 완료)**
|
### Phase 2: 관계 설정 기능 (2주) - 🚧 **진행 중 (85% 완료)**
|
||||||
|
|
||||||
- [x] 연결 설정 모달 UI 구현
|
- [x] 연결 설정 모달 UI 구현
|
||||||
- [x] 1:1, 1:N, N:1, N:N 관계 타입 선택 UI
|
- [x] 1:1, 1:N, N:1, N:N 관계 타입 선택 UI
|
||||||
- [x] 단순 키값, 데이터 저장, 외부 호출 연결 종류 UI
|
- [x] 단순 키값, 데이터 저장, 외부 호출 연결 종류 UI
|
||||||
- [x] 컬럼-to-컬럼 연결 시스템 (클릭 기반)
|
- [x] 컬럼-to-컬럼 연결 시스템 (클릭 기반)
|
||||||
- [x] 선택된 컬럼 정보 표시 및 순서 보장
|
- [x] 선택된 컬럼 정보 표시 및 순서 보장
|
||||||
|
- [x] 드래그 다중 선택 기능 (부분 터치 선택 지원)
|
||||||
|
- [x] 테이블 기반 시스템으로 전환 (화면 → 테이블)
|
||||||
|
- [x] 코드 정리 및 최적화 (불필요한 props 제거)
|
||||||
- [ ] 연결 생성 로직 구현 (모달에서 실제 엣지 생성)
|
- [ ] 연결 생성 로직 구현 (모달에서 실제 엣지 생성)
|
||||||
- [ ] 생성된 연결의 시각적 표시 (React Flow 엣지)
|
- [ ] 생성된 연결의 시각적 표시 (React Flow 엣지)
|
||||||
- [ ] 연결 데이터 백엔드 저장 API 연동
|
- [ ] 연결 데이터 백엔드 저장 API 연동
|
||||||
|
|
@ -698,13 +701,15 @@ PUT /api/external-call-configs/:id
|
||||||
**주요 개선사항:**
|
**주요 개선사항:**
|
||||||
|
|
||||||
1. **스크롤 충돌 해결**: 노드 내부 스크롤과 React Flow 줌/팬 기능 분리
|
1. **스크롤 충돌 해결**: 노드 내부 스크롤과 React Flow 줌/팬 기능 분리
|
||||||
2. **노드 리사이징**: NodeResizer를 통한 노드 크기 조정 및 내용 반영
|
2. **테이블 기반 시스템**: 화면 기반에서 테이블 기반으로 완전 전환
|
||||||
3. **컬럼-to-컬럼 연결**: 드래그앤드롭 대신 클릭 기반 컬럼 선택 방식
|
3. **컬럼-to-컬럼 연결**: 드래그앤드롭 대신 클릭 기반 컬럼 선택 방식
|
||||||
4. **2개 테이블 제한**: 최대 2개 테이블에서만 컬럼 선택 가능
|
4. **2개 테이블 제한**: 최대 2개 테이블에서만 컬럼 선택 가능
|
||||||
5. **선택 순서 보장**: 사이드바와 모달에서 컬럼 선택 순서 정확히 반영
|
5. **선택 순서 보장**: 사이드바와 모달에서 컬럼 선택 순서 정확히 반영
|
||||||
6. **실제 데이터 연동**: 테이블 관리 시스템의 실제 테이블/컬럼 데이터 사용
|
6. **실제 데이터 연동**: 테이블 관리 시스템의 실제 테이블/컬럼 데이터 사용
|
||||||
7. **사용자 경험**: react-hot-toast를 통한 친화적인 알림 시스템
|
7. **사용자 경험**: react-hot-toast를 통한 친화적인 알림 시스템
|
||||||
8. **React 안정성**: 렌더링 중 상태 변경 문제 해결
|
8. **React 안정성**: 렌더링 중 상태 변경 문제 해결
|
||||||
|
9. **드래그 다중 선택**: 부분 터치로도 노드 선택 가능한 고급 선택 기능
|
||||||
|
10. **코드 최적화**: 불필요한 props 및 컴포넌트 제거로 성능 향상
|
||||||
|
|
||||||
**다음 단계:** Phase 2 - 실제 연결 생성 및 시각적 표시 기능 구현
|
**다음 단계:** Phase 2 - 실제 연결 생성 및 시각적 표시 기능 구현
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,18 +4,19 @@ import React from "react";
|
||||||
import { Toaster } from "react-hot-toast";
|
import { Toaster } from "react-hot-toast";
|
||||||
import { DataFlowDesigner } from "@/components/dataflow/DataFlowDesigner";
|
import { DataFlowDesigner } from "@/components/dataflow/DataFlowDesigner";
|
||||||
import { useAuth } from "@/hooks/useAuth";
|
import { useAuth } from "@/hooks/useAuth";
|
||||||
|
import { TableRelationship } from "@/lib/api/dataflow";
|
||||||
|
|
||||||
export default function DataFlowPage() {
|
export default function DataFlowPage() {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
|
||||||
const handleSave = (relationships: any[]) => {
|
const handleSave = (relationships: TableRelationship[]) => {
|
||||||
console.log("저장된 관계:", relationships);
|
console.log("저장된 관계:", relationships);
|
||||||
// TODO: API 호출로 관계 저장
|
// TODO: API 호출로 관계 저장
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-screen bg-gray-50">
|
<div className="h-screen bg-gray-50">
|
||||||
<DataFlowDesigner companyCode={user?.companyCode || "COMP001"} onSave={handleSave} />
|
<DataFlowDesigner companyCode={user?.company_code || "COMP001"} onSave={handleSave} />
|
||||||
<Toaster />
|
<Toaster />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,10 @@ import {
|
||||||
Edge,
|
Edge,
|
||||||
Controls,
|
Controls,
|
||||||
Background,
|
Background,
|
||||||
MiniMap,
|
|
||||||
useNodesState,
|
useNodesState,
|
||||||
useEdgesState,
|
useEdgesState,
|
||||||
BackgroundVariant,
|
BackgroundVariant,
|
||||||
|
SelectionMode,
|
||||||
} from "@xyflow/react";
|
} from "@xyflow/react";
|
||||||
import "@xyflow/react/dist/style.css";
|
import "@xyflow/react/dist/style.css";
|
||||||
import { TableNode } from "./TableNode";
|
import { TableNode } from "./TableNode";
|
||||||
|
|
@ -435,10 +435,9 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({ companyCode,
|
||||||
<div key={`selected-${tableName}-${index}`}>
|
<div key={`selected-${tableName}-${index}`}>
|
||||||
<div className="w-full min-w-0 rounded-lg border border-blue-300 bg-white p-3">
|
<div className="w-full min-w-0 rounded-lg border border-blue-300 bg-white p-3">
|
||||||
<div className="mb-2 flex flex-wrap items-center gap-2">
|
<div className="mb-2 flex flex-wrap items-center gap-2">
|
||||||
<div className="flex-shrink-0 rounded bg-blue-600 px-2 py-1 text-xs font-medium text-white">
|
<div className="flex-shrink-0 rounded px-2 py-1 text-xs font-medium text-blue-600">
|
||||||
{displayName}
|
{displayName}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-shrink-0 text-xs text-gray-500">{tableName}</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full min-w-0 flex-wrap gap-1">
|
<div className="flex w-full min-w-0 flex-wrap gap-1">
|
||||||
{columns.map((column, columnIndex) => (
|
{columns.map((column, columnIndex) => (
|
||||||
|
|
@ -466,9 +465,9 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({ companyCode,
|
||||||
<button
|
<button
|
||||||
onClick={openConnectionModal}
|
onClick={openConnectionModal}
|
||||||
disabled={!canCreateConnection()}
|
disabled={!canCreateConnection()}
|
||||||
className={`rounded px-3 py-1 text-xs font-medium transition-colors ${
|
className={`w-full rounded px-3 py-1 text-xs font-medium transition-colors ${
|
||||||
canCreateConnection()
|
canCreateConnection()
|
||||||
? "bg-blue-600 text-white hover:bg-blue-700"
|
? "cursor-pointer bg-blue-600 text-white hover:bg-blue-700"
|
||||||
: "cursor-not-allowed bg-gray-300 text-gray-500"
|
: "cursor-not-allowed bg-gray-300 text-gray-500"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
|
@ -479,7 +478,7 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({ companyCode,
|
||||||
setSelectedColumns({});
|
setSelectedColumns({});
|
||||||
setSelectionOrder([]);
|
setSelectionOrder([]);
|
||||||
}}
|
}}
|
||||||
className="rounded bg-gray-200 px-3 py-1 text-xs font-medium text-gray-600 hover:bg-gray-300"
|
className="w-full cursor-pointer rounded bg-gray-200 px-3 py-1 text-xs font-medium text-gray-600 hover:bg-gray-300"
|
||||||
>
|
>
|
||||||
선택 초기화
|
선택 초기화
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -506,19 +505,14 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({ companyCode,
|
||||||
panOnScroll={false}
|
panOnScroll={false}
|
||||||
zoomOnScroll={true}
|
zoomOnScroll={true}
|
||||||
zoomOnPinch={true}
|
zoomOnPinch={true}
|
||||||
panOnDrag={true}
|
panOnDrag={[1, 2]}
|
||||||
|
selectionOnDrag={true}
|
||||||
|
multiSelectionKeyCode={null}
|
||||||
|
selectNodesOnDrag={false}
|
||||||
|
selectionMode={SelectionMode.Partial}
|
||||||
>
|
>
|
||||||
<Controls />
|
<Controls />
|
||||||
<MiniMap
|
|
||||||
nodeColor={(node) => {
|
|
||||||
switch (node.type) {
|
|
||||||
case "tableNode":
|
|
||||||
return "#3B82F6";
|
|
||||||
default:
|
|
||||||
return "#6B7280";
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Background variant={BackgroundVariant.Dots} gap={20} size={1} color="#E5E7EB" />
|
<Background variant={BackgroundVariant.Dots} gap={20} size={1} color="#E5E7EB" />
|
||||||
</ReactFlow>
|
</ReactFlow>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Handle, Position, NodeResizer } from "@xyflow/react";
|
|
||||||
|
|
||||||
interface TableColumn {
|
interface TableColumn {
|
||||||
name: string;
|
name: string;
|
||||||
|
|
@ -21,66 +20,22 @@ interface TableNodeData {
|
||||||
onColumnClick: (tableName: string, columnName: string) => void;
|
onColumnClick: (tableName: string, columnName: string) => void;
|
||||||
onScrollAreaEnter?: () => void;
|
onScrollAreaEnter?: () => void;
|
||||||
onScrollAreaLeave?: () => void;
|
onScrollAreaLeave?: () => void;
|
||||||
selected?: boolean;
|
|
||||||
selectedColumns?: string[]; // 선택된 컬럼 목록
|
selectedColumns?: string[]; // 선택된 컬럼 목록
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TableNode: React.FC<{ data: TableNodeData; selected?: boolean }> = ({ data, selected }) => {
|
export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => {
|
||||||
const { table, onColumnClick, onScrollAreaEnter, onScrollAreaLeave, selectedColumns = [] } = data;
|
const { table, onColumnClick, onScrollAreaEnter, onScrollAreaLeave, selectedColumns = [] } = data;
|
||||||
|
|
||||||
// 컬럼 개수에 따른 높이 계산
|
|
||||||
// 헤더: ~80px (제목 + 설명 + 패딩)
|
|
||||||
const headerHeight = table.description ? 80 : 65;
|
|
||||||
|
|
||||||
// 컬럼 높이: 각 컬럼은 실제로 더 높음 (px-2 py-1 + 텍스트 2줄 + 설명 + space-y-1)
|
|
||||||
// 설명이 있는 컬럼: ~45px, 없는 컬럼: ~35px, 간격: ~4px
|
|
||||||
const avgColumnHeight = 45; // 여유있게 계산
|
|
||||||
const idealColumnHeight = table.columns.length * avgColumnHeight;
|
|
||||||
|
|
||||||
// 컨테이너 패딩
|
|
||||||
const padding = 20;
|
|
||||||
|
|
||||||
// 이상적인 높이 vs 최대 허용 높이 (너무 길면 스크롤)
|
|
||||||
const idealHeight = headerHeight + idealColumnHeight + padding;
|
|
||||||
const maxAllowedHeight = 800; // 최대 800px
|
|
||||||
const calculatedHeight = Math.max(200, Math.min(idealHeight, maxAllowedHeight));
|
|
||||||
|
|
||||||
// 스크롤이 필요한지 판단
|
|
||||||
const needsScroll = idealHeight > maxAllowedHeight;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="relative flex min-w-[280px] flex-col overflow-hidden rounded-lg border-2 border-gray-300 bg-white shadow-lg">
|
||||||
className="relative flex min-w-[280px] flex-col overflow-hidden rounded-lg border-2 border-gray-300 bg-white shadow-lg"
|
|
||||||
style={{ height: `${calculatedHeight}px`, minHeight: `${calculatedHeight}px` }}
|
|
||||||
>
|
|
||||||
{/* NodeResizer for resizing functionality */}
|
|
||||||
<NodeResizer
|
|
||||||
color="#ff0071"
|
|
||||||
isVisible={selected}
|
|
||||||
minWidth={280}
|
|
||||||
minHeight={calculatedHeight}
|
|
||||||
keepAspectRatio={false}
|
|
||||||
handleStyle={{
|
|
||||||
width: 8,
|
|
||||||
height: 8,
|
|
||||||
backgroundColor: "#ff0071",
|
|
||||||
border: "1px solid white",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* 테이블 헤더 */}
|
{/* 테이블 헤더 */}
|
||||||
<div className="rounded-t-lg bg-blue-600 p-3 text-white">
|
<div className="bg-blue-600 p-3 text-white">
|
||||||
<h3 className="truncate text-sm font-semibold">{table.displayName}</h3>
|
<h3 className="truncate text-sm font-semibold">{table.displayName}</h3>
|
||||||
<p className="truncate text-xs opacity-90">{table.tableName}</p>
|
|
||||||
{table.description && <p className="mt-1 truncate text-xs opacity-75">{table.description}</p>}
|
{table.description && <p className="mt-1 truncate text-xs opacity-75">{table.description}</p>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 컬럼 목록 */}
|
{/* 컬럼 목록 */}
|
||||||
<div
|
<div className="flex-1 overflow-hidden p-2" onMouseEnter={onScrollAreaEnter} onMouseLeave={onScrollAreaLeave}>
|
||||||
className={`flex-1 p-2 ${needsScroll ? "overflow-y-auto" : "overflow-hidden"}`}
|
|
||||||
onMouseEnter={onScrollAreaEnter}
|
|
||||||
onMouseLeave={onScrollAreaLeave}
|
|
||||||
>
|
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{table.columns.map((column) => {
|
{table.columns.map((column) => {
|
||||||
const isSelected = selectedColumns.includes(column.name);
|
const isSelected = selectedColumns.includes(column.name);
|
||||||
|
|
@ -103,20 +58,6 @@ export const TableNode: React.FC<{ data: TableNodeData; selected?: boolean }> =
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* React Flow Handles */}
|
|
||||||
<Handle
|
|
||||||
type="target"
|
|
||||||
position={Position.Left}
|
|
||||||
className="h-3 w-3 border-2 border-gray-400 bg-white"
|
|
||||||
isConnectable={false}
|
|
||||||
/>
|
|
||||||
<Handle
|
|
||||||
type="source"
|
|
||||||
position={Position.Right}
|
|
||||||
className="h-3 w-3 border-2 border-gray-400 bg-white"
|
|
||||||
isConnectable={false}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue