기타 수정

This commit is contained in:
hyeonsu 2025-09-08 10:33:00 +09:00
parent b02e9610ea
commit 37fac630b9
4 changed files with 25 additions and 84 deletions

View File

@ -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 - 실제 연결 생성 및 시각적 표시 기능 구현

View File

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

View File

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

View File

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