From e4e11fa4900bd0e6dd58fe0db10fce45195b0828 Mon Sep 17 00:00:00 2001 From: hyeonsu Date: Fri, 5 Sep 2025 10:54:34 +0900 Subject: [PATCH 01/27] =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=ED=9D=90?= =?UTF-8?q?=EB=A6=84=20=EB=AC=B8=EC=84=9C=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/화면간_데이터_관계_설정_시스템_설계.md | 670 ++++++++++++++++++++ 1 file changed, 670 insertions(+) create mode 100644 docs/화면간_데이터_관계_설정_시스템_설계.md diff --git a/docs/화면간_데이터_관계_설정_시스템_설계.md b/docs/화면간_데이터_관계_설정_시스템_설계.md new file mode 100644 index 00000000..5806867a --- /dev/null +++ b/docs/화면간_데이터_관계_설정_시스템_설계.md @@ -0,0 +1,670 @@ +# 데이터 흐름 관리 시스템 설계 + +## 📋 목차 + +1. [시스템 개요](#시스템-개요) +2. [핵심 기능](#핵심-기능) +3. [데이터베이스 설계](#데이터베이스-설계) +4. [프론트엔드 설계](#프론트엔드-설계) +5. [API 설계](#api-설계) +6. [사용 시나리오](#사용-시나리오) +7. [구현 계획](#구현-계획) + +## 🎯 시스템 개요 + +### 데이터 흐름 관리 시스템이란? + +데이터 흐름 관리 시스템은 회사별로 화면들 간의 데이터 흐름과 관계를 시각적으로 설계하고 관리할 수 있는 시스템입니다. React Flow 라이브러리를 활용하여 직관적인 노드 기반 인터페이스로 1:1, 1:N, N:1, N:N 관계를 지원하며, 다양한 연결 방식과 종류로 복합적인 데이터 흐름을 설계할 수 있습니다. + +### 주요 특징 + +- **React Flow 기반 인터페이스**: 직관적인 노드와 엣지 기반 시각적 설계 +- **회사별 관계 관리**: 사용자 회사 코드에 따른 화면 관계 접근 제어 +- **시각적 관계 설계**: 드래그앤드롭으로 화면 노드 배치 및 필드 간 연결 +- **다양한 관계 타입**: 1:1, 1:N, N:1, N:N 관계 지원 +- **연결 종류별 세부 설정**: 단순 키값, 데이터 저장, 외부 호출 +- **실시간 시뮬레이션**: 설계한 관계의 데이터 흐름 시뮬레이션 +- **중계 테이블 자동 생성**: N:N 관계에서 중계 테이블 자동 생성 +- **인터랙티브 캔버스**: 줌, 팬, 미니맵 등 React Flow의 고급 기능 활용 + +### 지원하는 관계 타입 + +- **1:1 (One to One)**: 한 화면의 필드와 다른 화면의 필드가 1:1로 연결 +- **1:N (One to Many)**: 한 화면의 필드가 여러 화면의 필드와 연결 +- **N:1 (Many to One)**: 여러 화면의 필드가 한 화면의 필드와 연결 +- **N:N (Many to Many)**: 여러 화면의 필드가 여러 화면의 필드와 연결 + +### 지원하는 연결 종류 + +- **단순 키값 연결**: 중계 테이블을 통한 참조 관계 +- **데이터 저장**: 필드 매핑을 통한 데이터 저장 +- **외부 호출**: API, 이메일, 웹훅 등을 통한 외부 시스템 연동 + +## 🚀 핵심 기능 + +### 1. React Flow 기반 화면 노드 관리 + +- **화면 추가**: 회사별 화면 목록에서 관계를 설정할 화면들을 React Flow 캔버스에 추가 +- **화면 배치**: 드래그앤드롭으로 화면 노드를 원하는 위치에 배치 +- **화면 이동**: React Flow의 내장 기능으로 화면 노드 자유롭게 이동 +- **노드 선택**: 단일 또는 다중 노드 선택 지원 +- **자동 정렬**: React Flow의 레이아웃 알고리즘을 활용한 자동 정렬 + +### 2. React Flow 기반 필드 간 연결 설정 + +- **필드 선택**: 첫 번째 화면의 필드를 클릭하여 연결 시작 +- **대상 필드 선택**: 두 번째 화면의 필드를 클릭하여 연결 대상 지정 +- **드래그 연결**: React Flow의 핸들(Handle)을 드래그하여 시각적 연결 +- **관계 타입 선택**: 1:1, 1:N, N:1, N:N 중 선택 +- **연결 종류 선택**: 단순 키값, 데이터 저장, 외부 호출 중 선택 +- **엣지 커스터마이징**: 연결 타입별 색상, 스타일, 라벨 설정 + +### 3. 연결 종류별 세부 설정 + +#### 단순 키값 연결 + +- **중계 테이블명**: 자동 생성 또는 사용자 정의 +- **연결 규칙**: 중계 테이블 생성 및 관리 규칙 +- **참조 무결성**: 외래키 제약조건 설정 + +#### 데이터 저장 + +- **필드 매핑**: 소스 필드와 대상 필드 매핑 설정 +- **저장 조건**: 데이터 저장 조건 정의 +- **데이터 변환**: 필드 값 변환 규칙 + +#### 외부 호출 + +- **REST API**: API URL, HTTP Method, Headers, Body Template +- **이메일**: SMTP 서버, 발신자, 수신자, 제목/본문 템플릿 +- **웹훅**: 웹훅 URL, Payload 형식, Payload 템플릿 +- **FTP**: FTP 서버, 업로드 경로, 파일명 템플릿 +- **메시지 큐**: 큐 시스템, 큐 이름, 메시지 형식 + +### 4. React Flow 기반 시각적 관계 관리 + +- **엣지 렌더링**: React Flow의 커스텀 엣지로 화면 간 관계를 시각적으로 표현 +- **관계 타입별 스타일링**: 연결 종류에 따른 색상, 선 스타일, 라벨 구분 +- **인터랙티브 캔버스**: 줌, 팬, 미니맵을 통한 대규모 다이어그램 탐색 +- **실시간 시뮬레이션**: 데이터 흐름 애니메이션 및 시뮬레이션 +- **관계 검증**: 연결 가능성 및 무결성 검증 +- **엣지 편집**: 연결선 클릭으로 관계 설정 수정 + +### 5. 관계 통계 및 관리 + +- **연결 통계**: 관계 타입별 연결 수 표시 +- **중계 테이블 관리**: 생성된 중계 테이블 목록 및 관리 +- **관계 목록**: 생성된 모든 관계 목록 조회 +- **관계 삭제**: 불필요한 관계 삭제 + +## 🗄️ 데이터베이스 설계 + +### 1. 화면 관계 테이블 + +```sql +-- 화면 간 관계 정의 +CREATE TABLE screen_relationships ( + relationship_id SERIAL PRIMARY KEY, + relationship_name VARCHAR(200) NOT NULL, + from_screen_id INTEGER NOT NULL, + from_field_name VARCHAR(100) NOT NULL, + to_screen_id INTEGER NOT NULL, + to_field_name VARCHAR(100) NOT NULL, + relationship_type VARCHAR(20) NOT NULL, -- 'one-to-one', 'one-to-many', 'many-to-one', 'many-to-many' + connection_type VARCHAR(20) NOT NULL, -- 'simple-key', 'data-save', 'external-call' + company_code VARCHAR(50) NOT NULL, + settings JSONB, -- 연결 종류별 세부 설정 + is_active CHAR(1) DEFAULT 'Y', + created_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + created_by VARCHAR(50), + updated_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_by VARCHAR(50), + + -- 외래키 제약조건 + CONSTRAINT fk_screen_relationships_from_screen + FOREIGN KEY (from_screen_id) REFERENCES screen_definitions(screen_id), + CONSTRAINT fk_screen_relationships_to_screen + FOREIGN KEY (to_screen_id) REFERENCES screen_definitions(screen_id) +); + +-- 회사 코드 인덱스 +CREATE INDEX idx_screen_relationships_company_code ON screen_relationships(company_code); +``` + +### 2. 중계 테이블 관리 + +```sql +-- 중계 테이블 정의 +CREATE TABLE bridge_tables ( + bridge_id SERIAL PRIMARY KEY, + bridge_name VARCHAR(200) NOT NULL, + table_name VARCHAR(100) NOT NULL, + relationship_id INTEGER NOT NULL, + company_code VARCHAR(50) NOT NULL, + description TEXT, + is_active CHAR(1) DEFAULT 'Y', + created_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + created_by VARCHAR(50), + + -- 외래키 제약조건 + CONSTRAINT fk_bridge_tables_relationship + FOREIGN KEY (relationship_id) REFERENCES screen_relationships(relationship_id) +); + +-- 회사 코드 인덱스 +CREATE INDEX idx_bridge_tables_company_code ON bridge_tables(company_code); +``` + +### 3. 외부 호출 설정 + +```sql +-- 외부 호출 설정 +CREATE TABLE external_call_configs ( + config_id SERIAL PRIMARY KEY, + relationship_id INTEGER NOT NULL, + call_type VARCHAR(50) NOT NULL, -- 'rest-api', 'email', 'webhook', 'ftp', 'queue' + parameters JSONB NOT NULL, -- 호출 유형별 파라미터 + is_active CHAR(1) DEFAULT 'Y', + created_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + created_by VARCHAR(50), + + -- 외래키 제약조건 + CONSTRAINT fk_external_call_configs_relationship + FOREIGN KEY (relationship_id) REFERENCES screen_relationships(relationship_id) +); +``` + +### 4. 테이블 간 연계 관계 + +``` +screen_definitions (화면 정의) + ↓ (1:N) +screen_relationships (화면 관계) + ↓ (1:N) +bridge_tables (중계 테이블) + ↓ (1:N) +external_call_configs (외부 호출 설정) +``` + +## 🎨 프론트엔드 설계 + +### 1. React Flow 기반 메인 컴포넌트 + +```typescript +// DataFlowDesigner.tsx +import ReactFlow, { + Node, + Edge, + Controls, + Background, + MiniMap, + useNodesState, + useEdgesState, + addEdge, + Connection, + EdgeChange, + NodeChange, +} from "reactflow"; +import "reactflow/dist/style.css"; + +interface DataFlowDesignerProps { + companyCode: string; + onSave?: (relationships: ScreenRelationship[]) => void; +} + +export const DataFlowDesigner: React.FC = ({ + companyCode, + onSave, +}) => { + const [nodes, setNodes, onNodesChange] = useNodesState([]); + const [edges, setEdges, onEdgesChange] = useEdgesState([]); + const [selectedField, setSelectedField] = useState( + null + ); + const [pendingConnection, setPendingConnection] = + useState(null); + + const onConnect = useCallback( + (params: Connection) => { + setEdges((eds) => addEdge(params, eds)); + }, + [setEdges] + ); + + return ( +
+
+ {/* 사이드바 */} +
+ + + +
+ + {/* React Flow 캔버스 */} +
+ + + + + +
+
+ + +
+ ); +}; +``` + +### 2. React Flow 화면 노드 컴포넌트 + +```typescript +// ScreenNode.tsx +import { Handle, Position } from "reactflow"; + +interface ScreenNodeData { + screen: ScreenDefinition; + onFieldClick: (screenId: string, fieldName: string) => void; +} + +export const ScreenNode: React.FC<{ data: ScreenNodeData }> = ({ data }) => { + const { screen, onFieldClick } = data; + + return ( +
+ {/* 노드 헤더 */} +
+
{screen.screenName}
+
{screen.screenCode}
+
테이블: {screen.tableName}
+
+ + {/* 필드 목록 */} +
+
+ 필드 목록 ({screen.fields.length}개) +
+
+ {screen.fields.map((field) => ( +
onFieldClick(screen.screenId, field.name)} + > +
+
{field.name}
+
{field.description}
+
+
+ {field.type} +
+
+ ))} +
+
+ + {/* React Flow 핸들 */} + + +
+ ); +}; + +// 노드 타입 정의 +export const nodeTypes = { + screenNode: ScreenNode, +}; +``` + +### 3. 연결 설정 모달 + +```typescript +// ConnectionSetupModal.tsx +interface ConnectionSetupModalProps { + isOpen: boolean; + connection: PendingConnection | null; + onConfirm: (config: ConnectionConfig) => void; + onCancel: () => void; +} + +export const ConnectionSetupModal: React.FC = ({ + isOpen, + connection, + onConfirm, + onCancel, +}) => { + const [connectionType, setConnectionType] = + useState("simple-key"); + const [relationshipType, setRelationshipType] = + useState("one-to-one"); + const [settings, setSettings] = useState({}); + + return ( + + + + 필드 연결 설정 + + +
+ {/* 연결 정보 표시 */} + + + {/* 관계 타입 선택 */} + + + {/* 연결 종류 선택 */} + + + {/* 연결 종류별 세부 설정 */} + +
+ + + + + +
+
+ ); +}; +``` + +### 4. React Flow 엣지 컴포넌트 + +```typescript +// CustomEdge.tsx +import { + EdgeProps, + getBezierPath, + EdgeLabelRenderer, + BaseEdge, +} from "reactflow"; + +interface CustomEdgeData { + relationshipType: string; + connectionType: string; + label?: string; +} + +export const CustomEdge: React.FC> = ({ + id, + sourceX, + sourceY, + targetX, + targetY, + sourcePosition, + targetPosition, + data, + markerEnd, +}) => { + const [edgePath, labelX, labelY] = getBezierPath({ + sourceX, + sourceY, + sourcePosition, + targetX, + targetY, + targetPosition, + }); + + const getEdgeColor = (connectionType: string) => { + switch (connectionType) { + case "simple-key": + return "#3B82F6"; // 파란색 + case "data-save": + return "#10B981"; // 초록색 + case "external-call": + return "#F59E0B"; // 주황색 + default: + return "#6B7280"; // 회색 + } + }; + + const getEdgeStyle = (connectionType: string) => { + switch (connectionType) { + case "simple-key": + return { strokeWidth: 2, strokeDasharray: "5,5" }; + case "data-save": + return { strokeWidth: 3 }; + case "external-call": + return { strokeWidth: 2, strokeDasharray: "10,5" }; + default: + return { strokeWidth: 2 }; + } + }; + + return ( + <> + + +
+ {data?.label || data?.relationshipType} +
+
+ + ); +}; + +// 엣지 타입 정의 +export const edgeTypes = { + customEdge: CustomEdge, +}; +``` + +## 🌐 API 설계 + +### 1. 화면 관계 관리 API + +```typescript +// 화면 관계 생성 +POST /api/screen-relationships +Body: { + relationshipName: string; + fromScreenId: number; + fromFieldName: string; + toScreenId: number; + toFieldName: string; + relationshipType: 'one-to-one' | 'one-to-many' | 'many-to-one' | 'many-to-many'; + connectionType: 'simple-key' | 'data-save' | 'external-call'; + settings: ConnectionSettings; +} + +// 화면 관계 목록 조회 (회사별) +GET /api/screen-relationships?companyCode=COMP001 + +// 화면 관계 수정 +PUT /api/screen-relationships/:id + +// 화면 관계 삭제 +DELETE /api/screen-relationships/:id + +// 관계 시뮬레이션 +POST /api/screen-relationships/:id/simulate +``` + +### 2. 중계 테이블 관리 API + +```typescript +// 중계 테이블 생성 +POST /api/bridge-tables +Body: { + bridgeName: string; + tableName: string; + relationshipId: number; + description?: string; +} + +// 중계 테이블 목록 조회 +GET /api/bridge-tables?companyCode=COMP001 + +// 중계 테이블 삭제 +DELETE /api/bridge-tables/:id +``` + +### 3. 외부 호출 설정 API + +```typescript +// 외부 호출 설정 생성 +POST /api/external-call-configs +Body: { + relationshipId: number; + callType: 'rest-api' | 'email' | 'webhook' | 'ftp' | 'queue'; + parameters: Record; +} + +// 외부 호출 설정 조회 +GET /api/external-call-configs?relationshipId=123 + +// 외부 호출 설정 수정 +PUT /api/external-call-configs/:id +``` + +## 🎬 사용 시나리오 + +### 1. 기본 관계 설정 + +1. **화면 추가**: 회사별 화면 목록에서 관계를 설정할 화면들을 캔버스에 추가 +2. **필드 선택**: 첫 번째 화면의 필드를 클릭하여 연결 시작 +3. **대상 필드 선택**: 두 번째 화면의 필드를 클릭하여 연결 대상 지정 +4. **관계 설정**: 관계 타입과 연결 종류를 선택하고 세부 설정 구성 +5. **연결 생성**: 설정 완료 후 연결 생성 + +### 2. 복합 데이터 흐름 설계 + +1. **다중 화면 배치**: 관련된 여러 화면을 캔버스에 배치 +2. **다양한 연결 타입**: 단순 키값, 데이터 저장, 외부 호출을 조합 +3. **중계 테이블 활용**: N:N 관계에서 중계 테이블 자동 생성 +4. **시각적 검증**: 연결선과 색상으로 관계 유형 구분 + +### 3. 외부 시스템 연동 + +1. **API 연결**: REST API 호출을 통한 외부 시스템 연동 +2. **이메일 알림**: 데이터 변경 시 이메일 자동 전송 +3. **웹훅 설정**: 실시간 데이터 동기화 +4. **메시지 큐**: 비동기 데이터 처리 + +## 📅 구현 계획 + +### Phase 1: React Flow 기본 설정 (1주) + +- [ ] React Flow 라이브러리 설치 및 설정 +- [ ] 기본 노드와 엣지 컴포넌트 구현 +- [ ] 화면 노드 컴포넌트 구현 +- [ ] 기본 연결선 그리기 + +### Phase 2: 관계 설정 기능 (2주) + +- [ ] 1:1, 1:N 관계 설정 +- [ ] 단순 키값 연결 +- [ ] 연결 설정 모달 +- [ ] 노드 간 드래그앤드롭 연결 + +### Phase 3: 고급 연결 타입 (2-3주) + +- [ ] 데이터 저장 연결 +- [ ] 외부 호출 연결 +- [ ] 중계 테이블 자동 생성 +- [ ] 커스텀 엣지 스타일링 + +### Phase 4: React Flow 고급 기능 (1-2주) + +- [ ] 줌, 팬, 미니맵 기능 +- [ ] 노드 선택 및 다중 선택 +- [ ] 키보드 단축키 지원 +- [ ] 레이아웃 자동 정렬 + +### Phase 5: 시각화 및 관리 (1-2주) + +- [ ] 관계 시뮬레이션 +- [ ] 연결 통계 및 관리 +- [ ] 관계 검증 +- [ ] 데이터 흐름 애니메이션 + +### Phase 6: 고급 기능 (2-3주) + +- [ ] N:N 관계 지원 +- [ ] 복합 데이터 흐름 +- [ ] 외부 시스템 연동 +- [ ] 성능 최적화 + +## 🎯 결론 + +**데이터 흐름 관리 시스템**을 통해 ERP 시스템의 화면들 간 데이터 흐름을 시각적으로 설계하고 관리할 수 있습니다. React Flow 라이브러리를 활용한 직관적인 노드 기반 인터페이스와 회사별 권한 관리, 기존 화면관리 시스템과의 완벽한 연동을 통해 체계적인 데이터 관계 관리가 가능합니다. + +### 주요 가치 + +- **React Flow 기반 시각적 설계**: 복잡한 데이터 관계를 직관적인 노드와 엣지로 설계 +- **인터랙티브 캔버스**: 줌, 팬, 미니맵 등 고급 시각화 기능 제공 +- **회사별 관리**: 각 회사별로 독립적인 관계 관리 +- **다양한 연결 타입**: 업무 요구사항에 맞는 다양한 연결 방식 +- **자동화**: 중계 테이블 자동 생성 및 외부 시스템 연동 +- **확장성**: 새로운 연결 타입과 관계 유형 쉽게 추가 +- **사용자 친화적**: 드래그앤드롭 기반의 직관적인 사용자 인터페이스 From 0c765921b71e86d466c882057b222470d29a057b Mon Sep 17 00:00:00 2001 From: hyeonsu Date: Fri, 5 Sep 2025 11:30:27 +0900 Subject: [PATCH 02/27] =?UTF-8?q?React=20Flow=20=EA=B8=B0=EB=B3=B8=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/화면간_데이터_관계_설정_시스템_설계.md | 41 +++- frontend/app/(main)/admin/dataflow/page.tsx | 23 ++ frontend/components/dataflow/CustomEdge.tsx | 162 +++++++++++++ .../components/dataflow/DataFlowDesigner.tsx | 209 +++++++++++++++++ frontend/components/dataflow/ScreenNode.tsx | 109 +++++++++ frontend/components/layout/MainSidebar.tsx | 5 +- frontend/constants/layout.ts | 9 + frontend/package-lock.json | 221 ++++++++++++++++++ frontend/package.json | 1 + 9 files changed, 774 insertions(+), 6 deletions(-) create mode 100644 frontend/app/(main)/admin/dataflow/page.tsx create mode 100644 frontend/components/dataflow/CustomEdge.tsx create mode 100644 frontend/components/dataflow/DataFlowDesigner.tsx create mode 100644 frontend/components/dataflow/ScreenNode.tsx diff --git a/docs/화면간_데이터_관계_설정_시스템_설계.md b/docs/화면간_데이터_관계_설정_시스템_설계.md index 5806867a..75f4bfdf 100644 --- a/docs/화면간_데이터_관계_설정_시스템_설계.md +++ b/docs/화면간_데이터_관계_설정_시스템_설계.md @@ -613,12 +613,18 @@ PUT /api/external-call-configs/:id ## 📅 구현 계획 -### Phase 1: React Flow 기본 설정 (1주) +### Phase 1: React Flow 기본 설정 (1주) ✅ **완료** -- [ ] React Flow 라이브러리 설치 및 설정 -- [ ] 기본 노드와 엣지 컴포넌트 구현 -- [ ] 화면 노드 컴포넌트 구현 -- [ ] 기본 연결선 그리기 +- [x] React Flow 라이브러리 설치 및 설정 (@xyflow/react 12.8.4) +- [x] 기본 노드와 엣지 컴포넌트 구현 +- [x] 화면 노드 컴포넌트 구현 (ScreenNode.tsx) +- [x] 기본 연결선 그리기 (CustomEdge.tsx) +- [x] 메인 데이터 흐름 관리 컴포넌트 구현 (DataFlowDesigner.tsx) +- [x] /admin/dataflow 페이지 생성 +- [x] 메뉴 시스템 연동 (SQL 스크립트 제공) +- [x] 샘플 노드 추가/삭제 기능 +- [x] 노드 간 드래그앤드롭 연결 기능 +- [x] 줌, 팬, 미니맵 등 React Flow 기본 기능 ### Phase 2: 관계 설정 기능 (2주) @@ -659,6 +665,31 @@ PUT /api/external-call-configs/:id **데이터 흐름 관리 시스템**을 통해 ERP 시스템의 화면들 간 데이터 흐름을 시각적으로 설계하고 관리할 수 있습니다. React Flow 라이브러리를 활용한 직관적인 노드 기반 인터페이스와 회사별 권한 관리, 기존 화면관리 시스템과의 완벽한 연동을 통해 체계적인 데이터 관계 관리가 가능합니다. +## 📊 구현 현황 + +### ✅ Phase 1 완료 (2024-12-19) + +**구현된 기능:** + +- React Flow 12.8.4 기반 시각적 캔버스 +- 화면 노드 컴포넌트 (필드 정보, 타입별 색상 구분) +- 커스텀 엣지 컴포넌트 (관계 타입별 스타일링) +- 드래그앤드롭 노드 배치 및 연결 +- 줌, 팬, 미니맵 등 고급 시각화 기능 +- 샘플 데이터 생성 및 관리 기능 +- /admin/dataflow 경로 설정 +- 메뉴 시스템 연동 준비 완료 + +**구현된 파일:** + +- `frontend/components/dataflow/DataFlowDesigner.tsx` +- `frontend/components/dataflow/ScreenNode.tsx` +- `frontend/components/dataflow/CustomEdge.tsx` +- `frontend/app/(main)/admin/dataflow/page.tsx` +- `docs/add_dataflow_menu.sql` (메뉴 추가 스크립트) + +**다음 단계:** Phase 2 - 관계 설정 기능 구현 + ### 주요 가치 - **React Flow 기반 시각적 설계**: 복잡한 데이터 관계를 직관적인 노드와 엣지로 설계 diff --git a/frontend/app/(main)/admin/dataflow/page.tsx b/frontend/app/(main)/admin/dataflow/page.tsx new file mode 100644 index 00000000..72eb4f8b --- /dev/null +++ b/frontend/app/(main)/admin/dataflow/page.tsx @@ -0,0 +1,23 @@ +'use client'; + +import React from 'react'; +import { DataFlowDesigner } from '@/components/dataflow/DataFlowDesigner'; +import { useAuth } from '@/hooks/useAuth'; + +export default function DataFlowPage() { + const { user } = useAuth(); + + const handleSave = (relationships: any[]) => { + console.log('저장된 관계:', relationships); + // TODO: API 호출로 관계 저장 + }; + + return ( +
+ +
+ ); +} diff --git a/frontend/components/dataflow/CustomEdge.tsx b/frontend/components/dataflow/CustomEdge.tsx new file mode 100644 index 00000000..e87c76c2 --- /dev/null +++ b/frontend/components/dataflow/CustomEdge.tsx @@ -0,0 +1,162 @@ +"use client"; + +import React from "react"; +import { EdgeProps, getBezierPath, EdgeLabelRenderer, BaseEdge } from "@xyflow/react"; + +interface CustomEdgeData { + relationshipType: string; + connectionType: string; + label?: string; +} + +export const CustomEdge: React.FC> = ({ + id, + sourceX, + sourceY, + targetX, + targetY, + sourcePosition, + targetPosition, + data, + markerEnd, + selected, +}) => { + const [edgePath, labelX, labelY] = getBezierPath({ + sourceX, + sourceY, + sourcePosition, + targetX, + targetY, + targetPosition, + }); + + // 연결 타입에 따른 색상 반환 + const getEdgeColor = (connectionType: string) => { + switch (connectionType) { + case "simple-key": + return "#3B82F6"; // 파란색 - 단순 키값 연결 + case "data-save": + return "#10B981"; // 초록색 - 데이터 저장 + case "external-call": + return "#F59E0B"; // 주황색 - 외부 호출 + default: + return "#6B7280"; // 회색 - 기본 + } + }; + + // 연결 타입에 따른 스타일 반환 + const getEdgeStyle = (connectionType: string) => { + switch (connectionType) { + case "simple-key": + return { + strokeWidth: 2, + strokeDasharray: "5,5", + opacity: selected ? 1 : 0.8, + }; + case "data-save": + return { + strokeWidth: 3, + opacity: selected ? 1 : 0.8, + }; + case "external-call": + return { + strokeWidth: 2, + strokeDasharray: "10,5", + opacity: selected ? 1 : 0.8, + }; + default: + return { + strokeWidth: 2, + opacity: selected ? 1 : 0.6, + }; + } + }; + + // 관계 타입에 따른 아이콘 반환 + const getRelationshipIcon = (relationshipType: string) => { + switch (relationshipType) { + case "one-to-one": + return "1:1"; + case "one-to-many": + return "1:N"; + case "many-to-one": + return "N:1"; + case "many-to-many": + return "N:N"; + default: + return "1:1"; + } + }; + + // 연결 타입에 따른 설명 반환 + const getConnectionTypeDescription = (connectionType: string) => { + switch (connectionType) { + case "simple-key": + return "단순 키값"; + case "data-save": + return "데이터 저장"; + case "external-call": + return "외부 호출"; + default: + return "연결"; + } + }; + + const edgeColor = getEdgeColor(data?.connectionType || ""); + const edgeStyle = getEdgeStyle(data?.connectionType || ""); + + return ( + <> + + +
+
+
+ {data?.label || getRelationshipIcon(data?.relationshipType || "one-to-one")} +
+
+ {getConnectionTypeDescription(data?.connectionType || "simple-key")} +
+
+
+
+ + {/* 선택된 상태일 때 추가 시각적 효과 */} + {selected && ( + + )} + + ); +}; diff --git a/frontend/components/dataflow/DataFlowDesigner.tsx b/frontend/components/dataflow/DataFlowDesigner.tsx new file mode 100644 index 00000000..8d70c2b6 --- /dev/null +++ b/frontend/components/dataflow/DataFlowDesigner.tsx @@ -0,0 +1,209 @@ +"use client"; + +import React, { useState, useCallback } from "react"; +import { + ReactFlow, + Node, + Edge, + Controls, + Background, + MiniMap, + useNodesState, + useEdgesState, + addEdge, + Connection, +} from "@xyflow/react"; +import "@xyflow/react/dist/style.css"; +import { ScreenNode } from "./ScreenNode"; +import { CustomEdge } from "./CustomEdge"; + +// 노드 및 엣지 타입 정의 +const nodeTypes = { + screenNode: ScreenNode, +}; + +const edgeTypes = { + customEdge: CustomEdge, +}; + +interface DataFlowDesignerProps { + companyCode: string; + onSave?: (relationships: any[]) => void; +} + +export const DataFlowDesigner: React.FC = ({ companyCode, onSave }) => { + const [nodes, setNodes, onNodesChange] = useNodesState([]); + const [edges, setEdges, onEdgesChange] = useEdgesState([]); + const [selectedField, setSelectedField] = useState<{ + screenId: string; + fieldName: string; + } | null>(null); + + // 노드 연결 처리 + const onConnect = useCallback( + (params: Connection) => { + const newEdge = { + ...params, + type: "customEdge", + data: { + relationshipType: "one-to-one", + connectionType: "simple-key", + label: "1:1 연결", + }, + }; + setEdges((eds) => addEdge(newEdge, eds)); + }, + [setEdges], + ); + + // 필드 클릭 처리 + const handleFieldClick = useCallback((screenId: string, fieldName: string) => { + setSelectedField({ screenId, fieldName }); + }, []); + + // 샘플 화면 노드 추가 + const addSampleNode = useCallback(() => { + const newNode: Node = { + id: `screen-${Date.now()}`, + type: "screenNode", + position: { x: Math.random() * 300, y: Math.random() * 200 }, + data: { + screen: { + screenId: `screen-${Date.now()}`, + screenName: `샘플 화면 ${nodes.length + 1}`, + screenCode: `SCREEN${nodes.length + 1}`, + tableName: `table_${nodes.length + 1}`, + fields: [ + { name: "id", type: "INTEGER", description: "고유 식별자" }, + { name: "name", type: "VARCHAR(100)", description: "이름" }, + { name: "code", type: "VARCHAR(50)", description: "코드" }, + { name: "created_date", type: "TIMESTAMP", description: "생성일시" }, + ], + }, + onFieldClick: handleFieldClick, + }, + }; + + setNodes((nds) => nds.concat(newNode)); + }, [nodes.length, handleFieldClick, setNodes]); + + // 노드 전체 삭제 + const clearNodes = useCallback(() => { + setNodes([]); + setEdges([]); + }, [setNodes, setEdges]); + + return ( +
+
+ {/* 사이드바 */} +
+
+

데이터 흐름 관리

+ + {/* 회사 정보 */} +
+
회사 코드
+
{companyCode}
+
+ + {/* 컨트롤 버튼들 */} +
+ + + + + +
+ + {/* 통계 정보 */} +
+
통계
+
+
+ 화면 노드: + {nodes.length}개 +
+
+ 연결: + {edges.length}개 +
+
+
+ + {/* 선택된 필드 정보 */} + {selectedField && ( +
+
선택된 필드
+
+
화면: {selectedField.screenId}
+
필드: {selectedField.fieldName}
+
+ +
+ )} +
+
+ + {/* React Flow 캔버스 */} +
+ + + { + switch (node.type) { + case "screenNode": + return "#3B82F6"; + default: + return "#6B7280"; + } + }} + /> + + + + {/* 안내 메시지 */} + {nodes.length === 0 && ( +
+
+
📊
+
데이터 흐름 설계를 시작하세요
+
왼쪽 사이드바에서 "샘플 화면 추가" 버튼을 클릭하세요
+
+
+ )} +
+
+
+ ); +}; diff --git a/frontend/components/dataflow/ScreenNode.tsx b/frontend/components/dataflow/ScreenNode.tsx new file mode 100644 index 00000000..00838449 --- /dev/null +++ b/frontend/components/dataflow/ScreenNode.tsx @@ -0,0 +1,109 @@ +"use client"; + +import React from "react"; +import { Handle, Position } from "@xyflow/react"; + +interface ScreenField { + name: string; + type: string; + description: string; +} + +interface Screen { + screenId: string; + screenName: string; + screenCode: string; + tableName: string; + fields: ScreenField[]; +} + +interface ScreenNodeData { + screen: Screen; + onFieldClick: (screenId: string, fieldName: string) => void; +} + +export const ScreenNode: React.FC<{ data: ScreenNodeData }> = ({ data }) => { + const { screen, onFieldClick } = data; + + // 필드 타입에 따른 색상 반환 + const getFieldTypeColor = (type: string) => { + if (type.includes("INTEGER") || type.includes("NUMERIC")) return "text-blue-600 bg-blue-50"; + if (type.includes("VARCHAR") || type.includes("TEXT")) return "text-green-600 bg-green-50"; + if (type.includes("TIMESTAMP") || type.includes("DATE")) return "text-purple-600 bg-purple-50"; + if (type.includes("BOOLEAN")) return "text-orange-600 bg-orange-50"; + return "text-gray-600 bg-gray-50"; + }; + + return ( +
+ {/* 노드 헤더 */} +
+
{screen.screenName}
+
ID: {screen.screenCode}
+
+ 🗃️ + 테이블: {screen.tableName} +
+
+ + {/* 필드 목록 */} +
+
+
필드 목록
+
{screen.fields.length}개
+
+ +
+ {screen.fields.map((field, index) => ( +
onFieldClick(screen.screenId, field.name)} + > +
+
+
{field.name}
+ {index === 0 && ( + PK + )} +
+
{field.description}
+
+
+ {field.type} +
+
+ ))} +
+
+ + {/* React Flow 핸들 */} + + + + {/* 추가 핸들들 (상하) */} + + +
+ ); +}; diff --git a/frontend/components/layout/MainSidebar.tsx b/frontend/components/layout/MainSidebar.tsx index 7fb246fe..97721831 100644 --- a/frontend/components/layout/MainSidebar.tsx +++ b/frontend/components/layout/MainSidebar.tsx @@ -1,4 +1,4 @@ -import { ChevronDown, ChevronRight, Home, FileText, Users, BarChart3, Cog } from "lucide-react"; +import { ChevronDown, ChevronRight, Home, FileText, Users, BarChart3, Cog, GitBranch } from "lucide-react"; import { cn } from "@/lib/utils"; import { MenuItem } from "@/types/menu"; import { MENU_ICONS, MESSAGES } from "@/constants/layout"; @@ -29,6 +29,9 @@ const getMenuIcon = (menuName: string) => { if (MENU_ICONS.SETTINGS.some((keyword) => menuName.includes(keyword))) { return ; } + if (MENU_ICONS.DATAFLOW.some((keyword) => menuName.includes(keyword))) { + return ; + } return ; }; diff --git a/frontend/constants/layout.ts b/frontend/constants/layout.ts index e245b1de..73b9eb22 100644 --- a/frontend/constants/layout.ts +++ b/frontend/constants/layout.ts @@ -30,3 +30,12 @@ export const MESSAGES = { NO_DATA: "데이터가 없습니다.", NO_MENUS: "사용 가능한 메뉴가 없습니다.", } as const; + +export const MENU_ICONS = { + HOME: ["홈", "메인", "대시보드"], + DOCUMENT: ["문서", "게시판", "공지"], + USERS: ["사용자", "회원", "직원", "인사"], + STATISTICS: ["통계", "분석", "리포트", "차트"], + SETTINGS: ["설정", "관리", "시스템"], + DATAFLOW: ["데이터", "흐름", "관계", "연결"], +} as const; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 57519b47..50d96947 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -28,6 +28,7 @@ "@radix-ui/react-tabs": "^1.1.12", "@tanstack/react-query": "^5.85.6", "@tanstack/react-table": "^8.21.3", + "@xyflow/react": "^12.8.4", "axios": "^1.11.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -2559,6 +2560,55 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -3167,6 +3217,38 @@ "win32" ] }, + "node_modules/@xyflow/react": { + "version": "12.8.4", + "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.8.4.tgz", + "integrity": "sha512-bqUu4T5QSHiCFPkoH+b+LROKwQJdLvcjhGbNW9c1dLafCBRjmH1IYz0zPE+lRDXCtQ9kRyFxz3tG19+8VORJ1w==", + "license": "MIT", + "dependencies": { + "@xyflow/system": "0.0.68", + "classcat": "^5.0.3", + "zustand": "^4.4.0" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@xyflow/system": { + "version": "0.0.68", + "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.68.tgz", + "integrity": "sha512-QDG2wxIG4qX+uF8yzm1ULVZrcXX3MxPBoxv7O52FWsX87qIImOqifUhfa/TwsvLdzn7ic2DDBH1uI8TKbdNTYA==", + "license": "MIT", + "dependencies": { + "@types/d3-drag": "^3.0.7", + "@types/d3-interpolate": "^3.0.4", + "@types/d3-selection": "^3.0.10", + "@types/d3-transition": "^3.0.8", + "@types/d3-zoom": "^3.0.8", + "d3-drag": "^3.0.0", + "d3-interpolate": "^3.0.1", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0" + } + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -3686,6 +3768,12 @@ "url": "https://polar.sh/cva" } }, + "node_modules/classcat": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", + "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==", + "license": "MIT" + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", @@ -3804,6 +3892,111 @@ "devOptional": true, "license": "MIT" }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -8290,6 +8483,34 @@ "funding": { "url": "https://github.com/sponsors/colinhacks" } + }, + "node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } } } } diff --git a/frontend/package.json b/frontend/package.json index afae2e6e..d299c50e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -33,6 +33,7 @@ "@radix-ui/react-tabs": "^1.1.12", "@tanstack/react-query": "^5.85.6", "@tanstack/react-table": "^8.21.3", + "@xyflow/react": "^12.8.4", "axios": "^1.11.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", From b844719da49903bb088b14a275733fd6457d28d9 Mon Sep 17 00:00:00 2001 From: hyeonsu Date: Fri, 5 Sep 2025 16:19:31 +0900 Subject: [PATCH 03/27] =?UTF-8?q?phase2=2070%=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/화면간_데이터_관계_설정_시스템_설계.md | 51 ++- frontend/app/(main)/admin/dataflow/page.tsx | 17 +- .../dataflow/ConnectionSetupModal.tsx | 320 +++++++++++++++ .../components/dataflow/DataFlowDesigner.tsx | 380 ++++++++++++++++-- frontend/components/dataflow/ScreenNode.tsx | 104 +++-- .../components/dataflow/ScreenSelector.tsx | 174 ++++++++ frontend/lib/api/dataflow.ts | 202 ++++++++++ frontend/package-lock.json | 28 +- frontend/package.json | 1 + 9 files changed, 1152 insertions(+), 125 deletions(-) create mode 100644 frontend/components/dataflow/ConnectionSetupModal.tsx create mode 100644 frontend/components/dataflow/ScreenSelector.tsx create mode 100644 frontend/lib/api/dataflow.ts diff --git a/docs/화면간_데이터_관계_설정_시스템_설계.md b/docs/화면간_데이터_관계_설정_시스템_설계.md index 75f4bfdf..3b7180da 100644 --- a/docs/화면간_데이터_관계_설정_시스템_설계.md +++ b/docs/화면간_데이터_관계_설정_시스템_설계.md @@ -626,12 +626,17 @@ PUT /api/external-call-configs/:id - [x] 노드 간 드래그앤드롭 연결 기능 - [x] 줌, 팬, 미니맵 등 React Flow 기본 기능 -### Phase 2: 관계 설정 기능 (2주) +### Phase 2: 관계 설정 기능 (2주) - 🚧 **진행 중 (70% 완료)** -- [ ] 1:1, 1:N 관계 설정 -- [ ] 단순 키값 연결 -- [ ] 연결 설정 모달 -- [ ] 노드 간 드래그앤드롭 연결 +- [x] 연결 설정 모달 UI 구현 +- [x] 1:1, 1:N, N:1, N:N 관계 타입 선택 UI +- [x] 단순 키값, 데이터 저장, 외부 호출 연결 종류 UI +- [x] 필드-to-필드 연결 시스템 (클릭 기반) +- [x] 선택된 필드 정보 표시 및 순서 보장 +- [ ] 연결 생성 로직 구현 (모달에서 실제 엣지 생성) +- [ ] 생성된 연결의 시각적 표시 (React Flow 엣지) +- [ ] 연결 데이터 백엔드 저장 API 연동 +- [ ] 기존 연결 수정/삭제 기능 ### Phase 3: 고급 연결 타입 (2-3주) @@ -672,23 +677,39 @@ PUT /api/external-call-configs/:id **구현된 기능:** - React Flow 12.8.4 기반 시각적 캔버스 -- 화면 노드 컴포넌트 (필드 정보, 타입별 색상 구분) +- 화면 노드 컴포넌트 (필드 정보, 타입별 색상 구분, 노드 리사이징) - 커스텀 엣지 컴포넌트 (관계 타입별 스타일링) - 드래그앤드롭 노드 배치 및 연결 -- 줌, 팬, 미니맵 등 고급 시각화 기능 -- 샘플 데이터 생성 및 관리 기능 +- 줌, 팬, 미니맵 등 고급 시각화 기능 (스크롤 충돌 해결) +- 실제 화면 데이터 연동 (테이블 관리 API 연결) +- 필드-to-필드 연결 시스템 (클릭 기반, 2개 화면 제한) +- 연결 설정 모달 (관계 타입, 연결 종류 선택 UI) - /admin/dataflow 경로 설정 -- 메뉴 시스템 연동 준비 완료 +- 메뉴 시스템 연동 완료 +- 사용자 경험 개선 (토스트 알림, 선택 순서 보장) **구현된 파일:** -- `frontend/components/dataflow/DataFlowDesigner.tsx` -- `frontend/components/dataflow/ScreenNode.tsx` -- `frontend/components/dataflow/CustomEdge.tsx` -- `frontend/app/(main)/admin/dataflow/page.tsx` -- `docs/add_dataflow_menu.sql` (메뉴 추가 스크립트) +- `frontend/components/dataflow/DataFlowDesigner.tsx` - 메인 캔버스 컴포넌트 +- `frontend/components/dataflow/ScreenNode.tsx` - 화면 노드 컴포넌트 (NodeResizer 포함) +- `frontend/components/dataflow/CustomEdge.tsx` - 커스텀 엣지 컴포넌트 +- `frontend/components/dataflow/ConnectionSetupModal.tsx` - 연결 설정 모달 +- `frontend/app/(main)/admin/dataflow/page.tsx` - 데이터 흐름 관리 페이지 +- `frontend/lib/api/dataflow.ts` - 데이터 흐름 API 클라이언트 +- `docs/add_dataflow_menu.sql` - 메뉴 추가 스크립트 -**다음 단계:** Phase 2 - 관계 설정 기능 구현 +**주요 개선사항:** + +1. **스크롤 충돌 해결**: 노드 내부 스크롤과 React Flow 줌/팬 기능 분리 +2. **노드 리사이징**: NodeResizer를 통한 노드 크기 조정 및 내용 반영 +3. **필드-to-필드 연결**: 드래그앤드롭 대신 클릭 기반 필드 선택 방식 +4. **2개 화면 제한**: 최대 2개 화면에서만 필드 선택 가능 +5. **선택 순서 보장**: 사이드바와 모달에서 필드 선택 순서 정확히 반영 +6. **실제 데이터 연동**: 테이블 관리 시스템의 실제 화면/필드 데이터 사용 +7. **사용자 경험**: react-hot-toast를 통한 친화적인 알림 시스템 +8. **React 안정성**: 렌더링 중 상태 변경 문제 해결 + +**다음 단계:** Phase 2 - 실제 연결 생성 및 시각적 표시 기능 구현 ### 주요 가치 diff --git a/frontend/app/(main)/admin/dataflow/page.tsx b/frontend/app/(main)/admin/dataflow/page.tsx index 72eb4f8b..d7a7e316 100644 --- a/frontend/app/(main)/admin/dataflow/page.tsx +++ b/frontend/app/(main)/admin/dataflow/page.tsx @@ -1,23 +1,22 @@ -'use client'; +"use client"; -import React from 'react'; -import { DataFlowDesigner } from '@/components/dataflow/DataFlowDesigner'; -import { useAuth } from '@/hooks/useAuth'; +import React from "react"; +import { Toaster } from "react-hot-toast"; +import { DataFlowDesigner } from "@/components/dataflow/DataFlowDesigner"; +import { useAuth } from "@/hooks/useAuth"; export default function DataFlowPage() { const { user } = useAuth(); const handleSave = (relationships: any[]) => { - console.log('저장된 관계:', relationships); + console.log("저장된 관계:", relationships); // TODO: API 호출로 관계 저장 }; return (
- + +
); } diff --git a/frontend/components/dataflow/ConnectionSetupModal.tsx b/frontend/components/dataflow/ConnectionSetupModal.tsx new file mode 100644 index 00000000..94c9cc7d --- /dev/null +++ b/frontend/components/dataflow/ConnectionSetupModal.tsx @@ -0,0 +1,320 @@ +"use client"; + +import React, { useState, useEffect } from "react"; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Textarea } from "@/components/ui/textarea"; +import { ArrowRight, Database, Link } from "lucide-react"; + +// 연결 정보 타입 +interface ConnectionInfo { + fromNode: { + id: string; + screenName: string; + tableName: string; + }; + toNode: { + id: string; + screenName: string; + tableName: string; + }; + fromField?: string; + toField?: string; + selectedFieldsData?: { + [screenId: string]: { + screenName: string; + fields: string[]; + }; + }; + orderedScreenIds?: string[]; // 선택 순서 정보 +} + +// 연결 설정 타입 +interface ConnectionConfig { + relationshipName: string; + relationshipType: "one-to-one" | "one-to-many" | "many-to-one" | "many-to-many"; + connectionType: "simple-key" | "data-save" | "external-call"; + fromFieldName: string; + toFieldName: string; + settings?: Record; + description?: string; +} + +interface ConnectionSetupModalProps { + isOpen: boolean; + connection: ConnectionInfo | null; + onConfirm: (config: ConnectionConfig) => void; + onCancel: () => void; +} + +export const ConnectionSetupModal: React.FC = ({ + isOpen, + connection, + onConfirm, + onCancel, +}) => { + const [relationshipName, setRelationshipName] = useState(""); + const [relationshipType, setRelationshipType] = useState("one-to-one"); + const [connectionType, setConnectionType] = useState("simple-key"); + const [fromFieldName, setFromFieldName] = useState(""); + const [toFieldName, setToFieldName] = useState(""); + const [description, setDescription] = useState(""); + + // 모달이 열릴 때마다 초기화 + useEffect(() => { + if (isOpen && connection) { + // 기본 관계명 생성 + const defaultName = `${connection.fromNode.screenName}_${connection.toNode.screenName}`; + setRelationshipName(defaultName); + setRelationshipType("one-to-one"); + setConnectionType("simple-key"); + // 시작/대상 필드는 비워둠 (다음 기능에서 사용) + setFromFieldName(""); + setToFieldName(""); + setDescription(""); + } + }, [isOpen, connection]); + + const handleConfirm = () => { + if (!relationshipName.trim()) { + alert("관계명을 입력해주세요."); + return; + } + + const config: ConnectionConfig = { + relationshipName: relationshipName.trim(), + relationshipType, + connectionType, + fromFieldName, + toFieldName, + description: description.trim() || undefined, + }; + + onConfirm(config); + }; + + const getRelationshipTypeDescription = (type: string) => { + switch (type) { + case "one-to-one": + return "1:1 - 한 레코드가 다른 테이블의 한 레코드와 연결"; + case "one-to-many": + return "1:N - 한 레코드가 다른 테이블의 여러 레코드와 연결"; + case "many-to-one": + return "N:1 - 여러 레코드가 다른 테이블의 한 레코드와 연결"; + case "many-to-many": + return "N:N - 여러 레코드가 다른 테이블의 여러 레코드와 연결 (중계 테이블 생성)"; + default: + return ""; + } + }; + + const getConnectionTypeDescription = (type: string) => { + switch (type) { + case "simple-key": + return "단순 키값 연결 - 기본 참조 관계"; + case "data-save": + return "데이터 저장 - 필드 매핑을 통한 데이터 저장"; + case "external-call": + return "외부 호출 - API, 이메일, 웹훅 등을 통한 외부 시스템 연동"; + default: + return ""; + } + }; + + if (!connection) return null; + + return ( + + + + + + 필드 연결 설정 + + + +
+ {/* 연결 정보 표시 */} + + + 연결 정보 + + + {connection.selectedFieldsData && connection.orderedScreenIds ? ( +
+ {/* orderedScreenIds 순서대로 표시 */} + {connection.orderedScreenIds.map((screenId, index) => { + const screenData = connection.selectedFieldsData[screenId]; + if (!screenData) return null; + + return ( + +
+
+
+ {screenData.screenName} +
+
ID: {screenId}
+
+ + {index === 0 ? connection.fromNode.tableName : connection.toNode.tableName} +
+
+
+ {screenData.fields.map((field) => ( + + {field} + + ))} +
+
+ {/* 첫 번째 화면 다음에 화살표 표시 */} + {index === 0 && connection.orderedScreenIds.length > 1 && ( +
+ +
+ )} +
+ ); + })} +
+ ) : ( +
+
+
{connection.fromNode.screenName}
+
+ + {connection.fromNode.tableName} +
+
+ +
+
{connection.toNode.screenName}
+
+ + {connection.toNode.tableName} +
+
+
+ )} +
+
+ +
+ {/* 기본 설정 */} +
+

기본 설정

+ +
+ + setRelationshipName(e.target.value)} + placeholder="관계를 설명하는 이름을 입력하세요" + /> +
+ +
+ + setFromFieldName(e.target.value)} + placeholder="시작 테이블의 필드명" + /> +
+ +
+ + setToFieldName(e.target.value)} + placeholder="대상 테이블의 필드명" + /> +
+ +
+ +