feat: 저장 테이블 제외 조건 추가 및 포커싱 개선
- 저장 테이블 쿼리에 table-list와 체크박스가 활성화된 화면, openModalWithData 버튼이 있는 화면을 제외하는 조건 추가 - 화면 그룹 클릭 시 새 그룹 진입 시 포커싱 없이 시작하도록 로직 개선 - 관련 문서에 제외 조건 및 SQL 예시 추가
This commit is contained in:
parent
af4072cef1
commit
a6569909a2
|
|
@ -1901,6 +1901,9 @@ export const getScreenSubTables = async (req: Request, res: Response) => {
|
|||
// ============================================================
|
||||
// 저장 테이블 정보 추출
|
||||
// ============================================================
|
||||
// 제외 조건:
|
||||
// 1. table-list + 체크박스 활성화 + openModalWithData 버튼이 있는 화면
|
||||
// → 선택 후 다음 화면으로 넘기는 패턴 (실제 DB 저장 아님)
|
||||
const saveTableQuery = `
|
||||
SELECT DISTINCT
|
||||
sd.screen_id,
|
||||
|
|
@ -1915,6 +1918,19 @@ export const getScreenSubTables = async (req: Request, res: Response) => {
|
|||
WHERE sd.screen_id = ANY($1)
|
||||
AND sl.properties->'componentConfig'->'action'->>'type' = 'save'
|
||||
AND sl.properties->'componentConfig'->'action'->>'targetScreenId' IS NULL
|
||||
-- 제외: table-list + 체크박스가 있는 화면
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM screen_layouts sl_list
|
||||
WHERE sl_list.screen_id = sd.screen_id
|
||||
AND sl_list.properties->>'componentType' = 'table-list'
|
||||
AND (sl_list.properties->'componentConfig'->'checkbox'->>'enabled')::boolean = true
|
||||
)
|
||||
-- 제외: openModalWithData 버튼이 있는 화면 (선택 → 다음 화면 패턴)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM screen_layouts sl_modal
|
||||
WHERE sl_modal.screen_id = sd.screen_id
|
||||
AND sl_modal.properties->'componentConfig'->'action'->>'type' = 'openModalWithData'
|
||||
)
|
||||
ORDER BY sd.screen_id
|
||||
`;
|
||||
|
||||
|
|
|
|||
|
|
@ -1117,7 +1117,10 @@ screenSubTables[screenId].subTables.push({
|
|||
18. [x] 테이블/헤더 둥근 모서리 (rounded-xl, rounded-t-xl)
|
||||
19. [x] 필터 테이블 조인선 + 참조 테이블 활성화
|
||||
20. [x] 조인선 색상 상수 통일 (RELATION_COLORS.join.stroke)
|
||||
21. [ ] **선 교차점 이질감 해결** (계획 중)
|
||||
21. [x] 필터 연결선 포커싱 제어 (해당 화면 포커싱 시에만 표시)
|
||||
22. [x] 저장 테이블 제외 조건 추가 (table-list + 체크박스 + openModalWithData)
|
||||
23. [x] 첫 진입 시 포커싱 없이 시작 (트리에서 화면 클릭 시 그룹만 진입)
|
||||
24. [ ] **선 교차점 이질감 해결** (계획 중)
|
||||
22. [ ] 범례 UI 추가 (선택사항)
|
||||
23. [ ] 엣지 라벨에 관계 유형 표시 (선택사항)
|
||||
|
||||
|
|
@ -1142,7 +1145,27 @@ screenSubTables[screenId].subTables.push({
|
|||
1. `componentConfig.action.type = 'save'` (edit, delete 제외)
|
||||
2. `componentConfig.targetTable` (modal-repeater-table 등)
|
||||
3. `action.dataTransfer.targetTable` (데이터 전송 대상)
|
||||
4. **제외 조건**: `action.targetScreenId IS NOT NULL` (모달 열기 버튼)
|
||||
|
||||
**제외 조건:**
|
||||
1. `action.targetScreenId IS NOT NULL` (모달 열기 버튼)
|
||||
2. `table-list` + 체크박스 활성화 + `openModalWithData` 버튼이 있는 화면
|
||||
- 예: "거래처별 품목 추가 모달" - 선택 후 다음 화면으로 넘기는 패턴
|
||||
- 이 경우 "저장" 버튼은 DB 저장이 아닌 **선택 확인 용도**
|
||||
|
||||
```sql
|
||||
-- 제외 조건 SQL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM screen_layouts sl_list
|
||||
WHERE sl_list.screen_id = sd.screen_id
|
||||
AND sl_list.properties->>'componentType' = 'table-list'
|
||||
AND (sl_list.properties->'componentConfig'->'checkbox'->>'enabled')::boolean = true
|
||||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM screen_layouts sl_modal
|
||||
WHERE sl_modal.screen_id = sd.screen_id
|
||||
AND sl_modal.properties->'componentConfig'->'action'->>'type' = 'openModalWithData'
|
||||
)
|
||||
```
|
||||
|
||||
### 시각적 표현 (구현됨)
|
||||
|
||||
|
|
@ -1220,11 +1243,46 @@ transform-origin: top;
|
|||
- 조인 컬럼 주황색 강조 표시
|
||||
|
||||
### 포커싱 제어
|
||||
- 해당 화면이 포커싱됐을 때만 조인선 활성화
|
||||
|
||||
**조인선 (주황색 점선)**
|
||||
- 해당 화면이 포커싱됐을 때만 활성화
|
||||
- 다른 화면 포커싱 시 흐리게 처리 (opacity: 0.3)
|
||||
- 엣지 ID: `edge-filter-join-{screenId}-{sourceTable}-{targetTable}`
|
||||
|
||||
**필터 연결선 (파란색 점선)**
|
||||
- 화면 → 필터 대상 테이블 연결선
|
||||
- 해당 화면이 포커싱됐을 때만 표시 (opacity: 1)
|
||||
- 포커스 해제 시 완전히 숨김 (opacity: 0)
|
||||
- 엣지 ID: `edge-screen-filter-{screenId}-{tableName}`
|
||||
|
||||
**styledEdges 처리:**
|
||||
```typescript
|
||||
// 필터 조인 엣지 (주황색)
|
||||
if (edge.id.startsWith("edge-filter-join-")) {
|
||||
const isActive = focusedScreenId === edgeSourceScreenId;
|
||||
return {
|
||||
...edge,
|
||||
style: {
|
||||
stroke: isActive ? RELATION_COLORS.join.stroke : RELATION_COLORS.join.strokeLight,
|
||||
opacity: isActive ? 1 : 0.3,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// 화면 → 필터 대상 테이블 연결선 (파란색)
|
||||
if (edge.id.startsWith("edge-screen-filter-")) {
|
||||
const isActive = focusedScreenId === edgeSourceScreenId;
|
||||
return {
|
||||
...edge,
|
||||
style: {
|
||||
opacity: isActive ? 1 : 0, // 포커스 해제 시 완전히 숨김
|
||||
},
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 코드 위치
|
||||
- `ScreenRelationFlow.tsx`: 필터 조인 엣지 생성 로직
|
||||
- `ScreenRelationFlow.tsx`: 필터 조인 엣지 생성 + styledEdges 처리
|
||||
- `styledNodes`: 필터 대상 테이블의 조인 참조 테이블 활성화 로직
|
||||
|
||||
---
|
||||
|
|
@ -1245,6 +1303,39 @@ transform-origin: top;
|
|||
- 기본 색상: `#f97316` (orange-500)
|
||||
- 강조 색상: `#ea580c` (orange-600)
|
||||
|
||||
### 첫 진입 시 포커싱 없이 시작
|
||||
|
||||
**문제:**
|
||||
- 트리에서 화면을 클릭하면 해당 화면이 자동 포커싱됨
|
||||
- 첫 진입 시 노드 위치가 안정화되기 전에 필터선이 그려져 "망가진" 모습
|
||||
|
||||
**해결:**
|
||||
- 트리에서 화면 클릭 시: 그룹만 진입, 포커싱 없음
|
||||
- ReactFlow 안에서 화면 클릭 시: 정상 포커싱
|
||||
|
||||
**코드 변경:**
|
||||
```typescript
|
||||
// page.tsx - onScreenSelectInGroup 콜백
|
||||
onScreenSelectInGroup={(group, screenId) => {
|
||||
const isNewGroup = selectedGroup?.id !== group.id;
|
||||
|
||||
if (isNewGroup) {
|
||||
// 새 그룹 진입: 포커싱 없이 시작
|
||||
setSelectedGroup(group);
|
||||
setFocusedScreenIdInGroup(null);
|
||||
} else {
|
||||
// 같은 그룹 내에서 다른 화면 클릭: 포커싱 유지
|
||||
setFocusedScreenIdInGroup(screenId);
|
||||
}
|
||||
setSelectedScreen(null);
|
||||
}}
|
||||
```
|
||||
|
||||
**사용자 경험:**
|
||||
1. 트리에서 화면 클릭 (첫 진입) → 깔끔한 초기 상태 (모든 화면/테이블 동일 밝기)
|
||||
2. 같은 그룹 내에서 다른 화면 클릭 → 포커싱 + 연결선 표시
|
||||
3. ReactFlow에서 화면 노드 클릭 → 포커싱 + 연결선 표시
|
||||
|
||||
---
|
||||
|
||||
## [계획] 선 교차점 이질감 해결
|
||||
|
|
@ -1286,6 +1377,311 @@ transform-origin: top;
|
|||
|
||||
---
|
||||
|
||||
## 화면 관리 시스템 업그레이드 현황
|
||||
|
||||
### 프로젝트 개요
|
||||
|
||||
화면 관리 시스템 업그레이드를 통해 다음 3가지 핵심 기능을 구현:
|
||||
|
||||
| 기능 | 설명 | 상태 |
|
||||
|------|------|------|
|
||||
| **화면 그룹핑** | 관련 화면들을 그룹으로 묶어 관리 (트리 구조) | 기본 구현 완료 |
|
||||
| **화면-테이블 관계 시각화** | React Flow를 사용한 노드 기반 시각화 | 기본 구현 완료 |
|
||||
| **테이블 조인 설정** | 화면 내에서 테이블 간 조인 관계 직접 설정 | 미구현 |
|
||||
|
||||
---
|
||||
|
||||
### 데이터베이스 테이블 (5개)
|
||||
|
||||
| 테이블명 | 용도 | 상태 |
|
||||
|----------|------|------|
|
||||
| `screen_groups` | 화면 그룹 정보 | 생성됨 |
|
||||
| `screen_group_screens` | 화면-그룹 연결 (N:M) | 생성됨 |
|
||||
| `screen_field_joins` | 화면 필드 조인 설정 | 생성됨 |
|
||||
| `screen_data_flows` | 화면 간 데이터 흐름 | 생성됨 |
|
||||
| `screen_table_relations` | 화면-테이블 관계 | 생성됨 |
|
||||
|
||||
---
|
||||
|
||||
### 백엔드 API 현황
|
||||
|
||||
| 파일 | 상태 | 엔드포인트 |
|
||||
|------|------|-----------|
|
||||
| `screenGroupController.ts` | 완성됨 | 그룹/화면/조인/흐름/관계 CRUD |
|
||||
| `screenGroupRoutes.ts` | 완성됨 | `/api/screen-groups/*` |
|
||||
|
||||
---
|
||||
|
||||
### 프론트엔드 컴포넌트 현황
|
||||
|
||||
| 컴포넌트 | 경로 | 상태 |
|
||||
|----------|------|------|
|
||||
| `ScreenGroupTreeView.tsx` | `components/screen/` | **완료** |
|
||||
| `ScreenGroupModal.tsx` | `components/screen/` | **완료** (그룹 CRUD 모달) |
|
||||
| `ScreenRelationFlow.tsx` | `components/screen/` | **완료** |
|
||||
| `ScreenNode.tsx` | `components/screen/` | **완료** |
|
||||
| `FieldJoinPanel.tsx` | `components/screen/panels/` | **완료** (조인 설정) |
|
||||
| `DataFlowPanel.tsx` | `components/screen/panels/` | **완료** (데이터 흐름 설정) |
|
||||
| API 클라이언트 | `lib/api/screenGroup.ts` | **완료** |
|
||||
|
||||
---
|
||||
|
||||
### 구현 완료 목록
|
||||
|
||||
| # | 항목 | 완료일 |
|
||||
|---|------|--------|
|
||||
| 1 | DB 테이블 5개 생성 및 메타데이터 등록 | - |
|
||||
| 2 | 백엔드 API 전체 구현 (CRUD) | - |
|
||||
| 3 | 프론트엔드 API 클라이언트 구현 | - |
|
||||
| 4 | 트리 뷰 기본 구현 (그룹/화면 표시) | - |
|
||||
| 5 | React Flow 시각화 기본 구현 (노드 배치, 연결선) | - |
|
||||
| 6 | 노드 디자인 1차 개선 (정사각형, 흰색 테마) | - |
|
||||
| 7 | 화면 레이아웃 요약 API 추가 | 2026-01-01 |
|
||||
| 8 | 화면 노드 미리보기 구현 (폼/그리드/대시보드) | 2026-01-01 |
|
||||
| 9 | 테이블 노드 개선 (PK/FK 아이콘, 컬럼 목록) | 2026-01-01 |
|
||||
| 10 | 연결선 스타일 개선 (CRUD 라벨 제거, 1:N 표시) | 2026-01-01 |
|
||||
|
||||
---
|
||||
|
||||
### 추가 구현 완료 목록
|
||||
|
||||
| # | 항목 | 컴포넌트 | 상태 |
|
||||
|---|------|----------|------|
|
||||
| 11 | **그룹 관리 UI** | `ScreenGroupModal.tsx` | **완료** |
|
||||
| 12 | **조인 설정 UI** | `FieldJoinPanel.tsx` (414줄) | **완료** |
|
||||
| 13 | **데이터 흐름 설정 UI** | `DataFlowPanel.tsx` (462줄) | **완료** |
|
||||
|
||||
---
|
||||
|
||||
### 미구현 작업 목록 (UI 선택사항)
|
||||
|
||||
| # | 항목 | 설명 | 우선순위 |
|
||||
|---|------|------|----------|
|
||||
| 1 | **화면 미리보기 고도화** | 실제 컴포넌트 렌더링, 더 상세한 폼 필드 표시 | 낮음 |
|
||||
| 2 | 범례(Legend) UI 추가 | 관계 유형별 색상 설명 | 낮음 |
|
||||
| 3 | 뱃지 클릭 시 팝오버 상세정보 | 저장/필터/조인 뱃지 클릭 시 상세 정보 | 낮음 |
|
||||
| 4 | 선 교차점 이질감 해결 | 배경색 테두리 방식 | 낮음 |
|
||||
|
||||
---
|
||||
|
||||
## [다음 단계] 노드 플로워 기반 화면-테이블 설정 시스템
|
||||
|
||||
### 배경 및 목적
|
||||
|
||||
**문제**: 화면 디자이너에 너무 많은 기능이 집중되어 있음
|
||||
- 조인 설정, 필터 설정, 필드-컬럼 매칭, 저장 테이블 설정 등
|
||||
|
||||
**해결책**: 화면 관리 노드 플로워에서 이러한 설정을 **직접** 할 수 있게 함
|
||||
- 노드 플로워 = 화면-테이블 관계 설정의 **또 다른 UI**
|
||||
- 시각적으로 설정하고, DB에 저장되면 화면 디자이너/실제 화면에 자동 반영
|
||||
|
||||
### 핵심 개념
|
||||
|
||||
```
|
||||
노드 플로워에서 화면/테이블 노드 클릭 (우클릭/더블클릭)
|
||||
↓
|
||||
모달/팝업 열림
|
||||
↓
|
||||
설정 (조인, 필터, 필드-컬럼 매칭, 저장 테이블 등)
|
||||
↓
|
||||
DB 저장 (screen_layouts.properties, screen_field_joins 등)
|
||||
↓
|
||||
시각화 자동 반영 (데이터 기반으로 그리니까)
|
||||
화면 디자이너 자동 반영 (같은 데이터 사용)
|
||||
실제 화면 자동 반영 (같은 데이터 사용)
|
||||
```
|
||||
|
||||
### 구현 대상 기능
|
||||
|
||||
| 기능 | 설명 |
|
||||
|------|------|
|
||||
| **테이블 연결 설정** | 화면이 어떤 테이블과 연결되는지 |
|
||||
| **테이블 조인 설정** | 테이블 간 조인 관계 (LEFT, INNER 등) |
|
||||
| **필터링 설정** | 마스터-디테일 필터링 관계 |
|
||||
| **필드-컬럼 매칭** | 화면 필드 ↔ 테이블 컬럼 매핑 |
|
||||
| **저장 테이블 설정** | 어떤 테이블에 데이터가 저장되는지 |
|
||||
|
||||
### 구현 방안 (초안, 미확정)
|
||||
|
||||
#### 방안 A: 통합 설정 모달
|
||||
|
||||
노드 클릭 시 **하나의 모달**에서 탭으로 모든 설정
|
||||
|
||||
```
|
||||
[화면 노드] 더블클릭
|
||||
↓
|
||||
┌─────────────────────────────────┐
|
||||
│ 수주관리 화면 설정 │
|
||||
│ │
|
||||
│ [탭1: 테이블 연결] │
|
||||
│ [탭2: 조인 설정] │
|
||||
│ [탭3: 필터 설정] │
|
||||
│ [탭4: 필드-컬럼 매칭] │
|
||||
│ [탭5: 저장 테이블] │
|
||||
│ │
|
||||
│ [저장] [취소] │
|
||||
└─────────────────────────────────┘
|
||||
```
|
||||
|
||||
#### 방안 B: 기능별 분리 모달
|
||||
|
||||
우클릭 컨텍스트 메뉴로 기능 선택 → 해당 기능 모달 열림
|
||||
|
||||
```
|
||||
[화면 노드] 우클릭
|
||||
↓
|
||||
┌─────────────────┐
|
||||
│ 테이블 연결 설정 │
|
||||
│ 조인 설정 │
|
||||
│ 필터 설정 │
|
||||
│ 필드-컬럼 매칭 │
|
||||
│ 저장 테이블 설정 │
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
#### 방안 C: 사이드 패널
|
||||
|
||||
노드 클릭 시 **오른쪽 패널**에 설정 UI 표시 (모달 없이)
|
||||
|
||||
### 현재 상태
|
||||
|
||||
| 항목 | 상태 |
|
||||
|------|------|
|
||||
| 노드 플로워 시각화 | ✅ 완료 (읽기 전용) |
|
||||
| DB 테이블 | ✅ 있음 (`screen_field_joins`, `screen_data_flows` 등) |
|
||||
| 백엔드 API | ✅ 있음 (CRUD) |
|
||||
| 패널 UI | ✅ 있음 (`FieldJoinPanel`, `DataFlowPanel`) |
|
||||
| **노드에서 직접 설정** | ✅ **구현 완료** (방안 A) |
|
||||
|
||||
---
|
||||
|
||||
## 노드에서 직접 설정 기능 (방안 A: 통합 설정 모달)
|
||||
|
||||
### 구현 완료 (2026-01-09)
|
||||
|
||||
노드 더블클릭 시 통합 설정 모달이 열리며, 4개 탭으로 다양한 설정을 수행할 수 있습니다.
|
||||
|
||||
#### 사용법
|
||||
|
||||
1. **화면 노드** 또는 **테이블 노드**를 **더블클릭**
|
||||
2. 통합 설정 모달이 열림
|
||||
3. 탭 선택하여 설정
|
||||
4. 저장 후 시각화 자동 새로고침
|
||||
|
||||
#### 탭 구성
|
||||
|
||||
| 탭 | 기능 | 설명 |
|
||||
|----|------|------|
|
||||
| 테이블 연결 | 화면-테이블 관계 설정 | 메인/서브/조회/저장 테이블 지정, CRUD 권한 설정 |
|
||||
| 조인 설정 | FK-PK 조인 관계 설정 | 저장 테이블의 FK 컬럼 ↔ 조인 테이블의 PK 컬럼 매핑, 표시 컬럼 지정 |
|
||||
| 데이터 흐름 | 화면 간 데이터 이동 설정 | 소스 화면 → 타겟 화면, 단방향/양방향 흐름 설정 |
|
||||
| 필드 매핑 | 테이블 컬럼 정보 조회 | 현재 테이블의 컬럼 목록, 데이터 타입, 웹 타입 확인 |
|
||||
|
||||
#### 구현 파일
|
||||
|
||||
| 파일 | 역할 |
|
||||
|------|------|
|
||||
| `frontend/components/screen/NodeSettingModal.tsx` | **새로 생성** - 통합 설정 모달 컴포넌트 |
|
||||
| `frontend/components/screen/ScreenRelationFlow.tsx` | 노드 더블클릭 이벤트 핸들러 추가 |
|
||||
|
||||
#### 주요 코드 변경
|
||||
|
||||
**NodeSettingModal.tsx (신규)**
|
||||
- 4개 탭 컴포넌트 내장 (TableRelationTab, JoinSettingTab, DataFlowTab, FieldMappingTab)
|
||||
- 기존 API 활용: `getTableRelations`, `getFieldJoins`, `getDataFlows`
|
||||
- CRUD 연동: `createFieldJoin`, `updateFieldJoin`, `deleteFieldJoin` 등
|
||||
- 저장 후 부모 컴포넌트 새로고침 콜백 (`onRefresh`)
|
||||
|
||||
**ScreenRelationFlow.tsx (수정)**
|
||||
```typescript
|
||||
// 노드 더블클릭 이벤트 핸들러 추가
|
||||
const handleNodeDoubleClick = useCallback((_event: React.MouseEvent, node: Node) => {
|
||||
// 화면/테이블 노드 판별 후 모달 오픈
|
||||
if (node.id.startsWith("screen-")) {
|
||||
// 화면 노드 처리
|
||||
} else if (node.id.startsWith("table-")) {
|
||||
// 테이블 노드 처리
|
||||
}
|
||||
setIsSettingModalOpen(true);
|
||||
}, [screenTableMap, screenSubTableMap]);
|
||||
|
||||
// ReactFlow에 이벤트 연결
|
||||
<ReactFlow
|
||||
onNodeDoubleClick={handleNodeDoubleClick}
|
||||
...
|
||||
/>
|
||||
|
||||
// 모달 렌더링
|
||||
<NodeSettingModal
|
||||
isOpen={isSettingModalOpen}
|
||||
onClose={handleSettingModalClose}
|
||||
onRefresh={handleRefreshVisualization}
|
||||
...
|
||||
/>
|
||||
```
|
||||
|
||||
#### 시각화 새로고침 메커니즘
|
||||
|
||||
```typescript
|
||||
// 강제 새로고침용 키
|
||||
const [refreshKey, setRefreshKey] = useState(0);
|
||||
|
||||
// 새로고침 핸들러
|
||||
const handleRefreshVisualization = useCallback(() => {
|
||||
setRefreshKey(prev => prev + 1);
|
||||
}, []);
|
||||
|
||||
// useEffect 의존성에 refreshKey 추가
|
||||
useEffect(() => {
|
||||
// 데이터 로드 로직
|
||||
}, [screen, selectedGroup, ..., refreshKey]);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 주요 파일 경로
|
||||
|
||||
```
|
||||
backend-node/src/
|
||||
├── controllers/screenGroupController.ts # 화면 그룹 API
|
||||
├── routes/screenGroupRoutes.ts # 라우트 정의
|
||||
|
||||
frontend/
|
||||
├── app/(main)/admin/screenMng/screenMngList/page.tsx # 메인 페이지
|
||||
├── components/screen/
|
||||
│ ├── ScreenGroupTreeView.tsx # 트리 뷰 (그룹/화면 표시)
|
||||
│ ├── ScreenGroupModal.tsx # 그룹 추가/수정 모달
|
||||
│ ├── ScreenRelationFlow.tsx # React Flow 시각화 + 더블클릭 이벤트
|
||||
│ ├── ScreenNode.tsx # 노드 컴포넌트
|
||||
│ ├── NodeSettingModal.tsx # **신규** - 통합 설정 모달
|
||||
│ └── panels/
|
||||
│ ├── FieldJoinPanel.tsx # 필드 조인 설정 UI (개별 패널)
|
||||
│ └── DataFlowPanel.tsx # 데이터 흐름 설정 UI (개별 패널)
|
||||
└── lib/api/screenGroup.ts # API 클라이언트
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 향후 개선 사항
|
||||
|
||||
### 필드 매핑 탭 고도화
|
||||
|
||||
현재 필드 매핑 탭은 테이블 컬럼 정보를 조회만 가능합니다. 향후 다음 기능 추가 가능:
|
||||
|
||||
1. **컬럼-컴포넌트 바인딩 설정**: 화면 컴포넌트와 DB 컬럼 직접 연결
|
||||
2. **드래그 앤 드롭**: 시각적 매핑 UI
|
||||
3. **자동 매핑 추천**: 컬럼명 기반 자동 매핑 제안
|
||||
|
||||
### 관계 시각화 연동
|
||||
|
||||
설정 저장 후 시각화에 즉시 반영되지만, 다음 개선 가능:
|
||||
|
||||
1. **실시간 프리뷰**: 저장 전 미리보기
|
||||
2. **관계 유형별 색상 커스터마이징**
|
||||
3. **관계 라벨 표시 옵션**
|
||||
|
||||
---
|
||||
|
||||
## 관련 문서
|
||||
|
||||
- [멀티테넌시 구현 가이드](.cursor/rules/multi-tenancy-guide.mdc)
|
||||
|
|
|
|||
|
|
@ -160,13 +160,18 @@ export default function ScreenManagementPage() {
|
|||
setFocusedScreenIdInGroup(null); // 포커스 초기화
|
||||
}}
|
||||
onScreenSelectInGroup={(group, screenId) => {
|
||||
// 그룹 내 화면 클릭 시: 해당 화면 포커스
|
||||
// 이미 같은 그룹이 선택된 상태라면 그룹을 다시 설정하지 않음 (데이터 재로드 방지)
|
||||
if (selectedGroup?.id !== group.id) {
|
||||
// 그룹 내 화면 클릭 시
|
||||
const isNewGroup = selectedGroup?.id !== group.id;
|
||||
|
||||
if (isNewGroup) {
|
||||
// 새 그룹 진입: 포커싱 없이 시작 (첫 진입 시 망가지는 문제 방지)
|
||||
setSelectedGroup(group);
|
||||
setFocusedScreenIdInGroup(null);
|
||||
} else {
|
||||
// 같은 그룹 내에서 다른 화면 클릭: 포커싱 유지
|
||||
setFocusedScreenIdInGroup(screenId);
|
||||
}
|
||||
setSelectedScreen(null);
|
||||
setFocusedScreenIdInGroup(screenId);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -32,6 +32,8 @@ import {
|
|||
VisualRelationType,
|
||||
} from "@/lib/api/screenGroup";
|
||||
import { getTableColumns, ColumnTypeInfo } from "@/lib/api/tableManagement";
|
||||
import { ScreenSettingModal } from "./ScreenSettingModal";
|
||||
import { TableSettingModal } from "./TableSettingModal";
|
||||
|
||||
// 관계 유형별 색상 정의
|
||||
const RELATION_COLORS: Record<VisualRelationType, { stroke: string; strokeLight: string; label: string }> = {
|
||||
|
|
@ -86,6 +88,64 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
|
|||
// 그룹 내 포커스된 화면 ID (그룹 모드에서만 사용)
|
||||
const [focusedScreenId, setFocusedScreenId] = useState<number | null>(null);
|
||||
|
||||
// 노드 설정 모달 상태
|
||||
const [isSettingModalOpen, setIsSettingModalOpen] = useState(false);
|
||||
const [settingModalNode, setSettingModalNode] = useState<{
|
||||
nodeType: "screen" | "table";
|
||||
nodeId: string;
|
||||
screenId: number;
|
||||
screenName: string;
|
||||
tableName?: string;
|
||||
tableLabel?: string;
|
||||
// 기존 설정 정보 (화면 디자이너에서 추출)
|
||||
existingConfig?: {
|
||||
joinColumnRefs?: Array<{
|
||||
column: string;
|
||||
refTable: string;
|
||||
refTableLabel?: string;
|
||||
refColumn: string;
|
||||
}>;
|
||||
filterColumns?: string[];
|
||||
fieldMappings?: Array<{
|
||||
targetField: string;
|
||||
sourceField: string;
|
||||
sourceTable?: string;
|
||||
sourceDisplayName?: string;
|
||||
}>;
|
||||
referencedBy?: Array<{
|
||||
fromTable: string;
|
||||
fromTableLabel?: string;
|
||||
fromColumn: string;
|
||||
toColumn: string;
|
||||
toColumnLabel?: string;
|
||||
relationType: string;
|
||||
}>;
|
||||
columns?: Array<{
|
||||
name: string;
|
||||
originalName?: string;
|
||||
type: string;
|
||||
isPrimaryKey?: boolean;
|
||||
isForeignKey?: boolean;
|
||||
}>;
|
||||
// 화면 노드용 테이블 정보
|
||||
mainTable?: string;
|
||||
filterTables?: Array<{
|
||||
tableName: string;
|
||||
tableLabel: string;
|
||||
filterColumns: string[];
|
||||
joinColumnRefs: Array<{
|
||||
column: string;
|
||||
refTable: string;
|
||||
refTableLabel?: string;
|
||||
refColumn: string;
|
||||
}>;
|
||||
}>;
|
||||
};
|
||||
} | null>(null);
|
||||
|
||||
// 강제 새로고침용 키 (설정 저장 후 시각화 재로딩)
|
||||
const [refreshKey, setRefreshKey] = useState(0);
|
||||
|
||||
// 그룹 또는 화면이 변경될 때 포커스 초기화
|
||||
useEffect(() => {
|
||||
setFocusedScreenId(null);
|
||||
|
|
@ -814,12 +874,11 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
|
|||
const exists = newEdges.some((e) => e.id === edgeId);
|
||||
if (exists) return;
|
||||
|
||||
// 관계 타입에 따른 라벨
|
||||
let relationLabel = "참조";
|
||||
if (subTable.relationType === "lookup") relationLabel = "조회";
|
||||
else if (subTable.relationType === "source") relationLabel = "데이터 소스";
|
||||
else if (subTable.relationType === "join") relationLabel = "조인";
|
||||
// 관계 유형 결정 (스타일링용)
|
||||
const visualRelationType = inferVisualRelationType(subTable);
|
||||
const relationColor = RELATION_COLORS[visualRelationType];
|
||||
|
||||
// 메인-서브 조인선 (메인-메인과 동일한 스타일, 라벨 없음)
|
||||
newEdges.push({
|
||||
id: edgeId,
|
||||
source: `table-${mainTable}`,
|
||||
|
|
@ -827,50 +886,47 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
|
|||
sourceHandle: "bottom",
|
||||
targetHandle: "top",
|
||||
type: "smoothstep",
|
||||
label: relationLabel,
|
||||
labelStyle: {
|
||||
fontSize: 9,
|
||||
fill: "#94a3b8",
|
||||
fontWeight: 500
|
||||
},
|
||||
labelBgStyle: {
|
||||
fill: "white",
|
||||
stroke: "#e2e8f0",
|
||||
strokeWidth: 1
|
||||
},
|
||||
labelBgPadding: [3, 2] as [number, number],
|
||||
markerEnd: {
|
||||
type: MarkerType.ArrowClosed,
|
||||
color: "#94a3b8"
|
||||
color: relationColor.strokeLight
|
||||
},
|
||||
animated: false, // 기본: 애니메이션 비활성화 (포커스 시에만 활성화)
|
||||
animated: false,
|
||||
style: {
|
||||
stroke: "#94a3b8",
|
||||
strokeWidth: 1,
|
||||
strokeDasharray: "6,4", // 점선
|
||||
opacity: 0.5, // 기본 투명도
|
||||
stroke: relationColor.strokeLight,
|
||||
strokeWidth: 1.5,
|
||||
strokeDasharray: "8,4",
|
||||
opacity: 0.5,
|
||||
},
|
||||
data: {
|
||||
sourceScreenId,
|
||||
visualRelationType,
|
||||
},
|
||||
data: { sourceScreenId },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// 조인 관계 엣지 (테이블 간 - 1:N 관계 표시)
|
||||
// 조인 관계 엣지 (screen_field_joins 기반 - 라벨 없이 통일된 스타일)
|
||||
joins.forEach((join: any, idx: number) => {
|
||||
if (join.save_table && join.join_table && join.save_table !== join.join_table) {
|
||||
newEdges.push({
|
||||
id: `edge-join-${idx}`,
|
||||
id: `edge-join-db-${idx}`,
|
||||
source: `table-${join.save_table}`,
|
||||
target: `table-${join.join_table}`,
|
||||
sourceHandle: "right",
|
||||
targetHandle: "left",
|
||||
sourceHandle: "bottom",
|
||||
targetHandle: "bottom_target",
|
||||
type: "smoothstep",
|
||||
label: "1:N 관계",
|
||||
labelStyle: { fontSize: 10, fill: "#6366f1", fontWeight: 500 },
|
||||
labelBgStyle: { fill: "white", stroke: "#e2e8f0", strokeWidth: 1 },
|
||||
labelBgPadding: [4, 2] as [number, number],
|
||||
markerEnd: { type: MarkerType.ArrowClosed, color: "#6366f1" },
|
||||
style: { stroke: "#6366f1", strokeWidth: 1.5, strokeDasharray: "5,5" },
|
||||
markerEnd: {
|
||||
type: MarkerType.ArrowClosed,
|
||||
color: RELATION_COLORS.join.strokeLight
|
||||
},
|
||||
animated: false,
|
||||
style: {
|
||||
stroke: RELATION_COLORS.join.strokeLight,
|
||||
strokeWidth: 1.5,
|
||||
strokeDasharray: "8,4",
|
||||
opacity: 0.5,
|
||||
},
|
||||
data: { visualRelationType: 'join' },
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -955,8 +1011,9 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
|
|||
|
||||
loadRelations();
|
||||
// focusedScreenId는 스타일링에만 영향을 미치므로 의존성에서 제외
|
||||
// refreshKey: 설정 저장 후 강제 새로고침용
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [screen, selectedGroup, setNodes, setEdges, loadTableColumns]);
|
||||
}, [screen, selectedGroup, setNodes, setEdges, loadTableColumns, refreshKey]);
|
||||
|
||||
// 데이터 로드 완료 시 fitView 호출 (초기 로드 시에만)
|
||||
useEffect(() => {
|
||||
|
|
@ -985,6 +1042,198 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
|
|||
}
|
||||
}, [selectedGroup]);
|
||||
|
||||
// 테이블 정보에서 조인/필터 정보 추출 (더블클릭 핸들러용)
|
||||
const getTableExistingConfig = useCallback((tableName: string) => {
|
||||
// subTablesDataMap에서 서브 테이블 정보 찾기
|
||||
for (const screenId in subTablesDataMap) {
|
||||
const screenSubTables = subTablesDataMap[parseInt(screenId)];
|
||||
if (screenSubTables?.subTables) {
|
||||
const subTable = screenSubTables.subTables.find(st => st.tableName === tableName);
|
||||
if (subTable) {
|
||||
return {
|
||||
joinColumnRefs: subTable.joinColumnRefs,
|
||||
filterColumns: subTable.filterColumns,
|
||||
fieldMappings: subTable.fieldMappings?.map(m => ({
|
||||
targetField: m.targetField,
|
||||
sourceField: m.sourceField,
|
||||
sourceTable: m.sourceTable,
|
||||
sourceDisplayName: m.sourceDisplayName,
|
||||
})),
|
||||
columns: [], // 컬럼 정보는 노드에서 가져옴
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}, [subTablesDataMap]);
|
||||
|
||||
// 노드 우클릭 핸들러 (설정 모달 열기)
|
||||
const handleNodeContextMenu = useCallback((event: React.MouseEvent, node: Node) => {
|
||||
// 기본 컨텍스트 메뉴 방지
|
||||
event.preventDefault();
|
||||
|
||||
// 화면 노드 우클릭
|
||||
if (node.id.startsWith("screen-")) {
|
||||
const screenId = parseInt(node.id.replace("screen-", ""));
|
||||
const nodeData = node.data as ScreenNodeData;
|
||||
const mainTable = screenTableMap[screenId];
|
||||
|
||||
// 해당 화면의 서브 테이블 (필터 테이블) 정보
|
||||
// 1. screenSubTableMap에서 가져오기
|
||||
const screenSubTables = screenSubTableMap[screenId] || [];
|
||||
|
||||
// 2. edges에서 필터 테이블 찾기 (edge-screen-filter-{screenId}-{tableName})
|
||||
const filterTableNamesFromEdges = edges
|
||||
.filter(e => e.id.startsWith(`edge-screen-filter-${screenId}-`))
|
||||
.map(e => {
|
||||
const match = e.id.match(/edge-screen-filter-\d+-(.+)/);
|
||||
return match ? match[1] : null;
|
||||
})
|
||||
.filter((name): name is string => name !== null);
|
||||
|
||||
// 모든 필터 테이블 합치기 (중복 제거)
|
||||
const allFilterTableNames = [...new Set([...screenSubTables, ...filterTableNamesFromEdges])];
|
||||
|
||||
const filterTables = allFilterTableNames.map(tableName => {
|
||||
// subTablesDataMap에서 해당 테이블 정보 찾기
|
||||
const subTableData = subTablesDataMap[screenId]?.subTables?.find(
|
||||
st => st.tableName === tableName
|
||||
);
|
||||
|
||||
// 또는 nodes에서 테이블 노드 정보 찾기
|
||||
const tableNode = nodes.find(n =>
|
||||
n.id === `table-${tableName}` || n.id === `subtable-${tableName}`
|
||||
);
|
||||
const tableNodeData = tableNode?.data as TableNodeData | undefined;
|
||||
|
||||
return {
|
||||
tableName,
|
||||
tableLabel: subTableData?.tableLabel || tableNodeData?.label || tableName,
|
||||
filterColumns: subTableData?.filterColumns || tableNodeData?.filterColumns || [],
|
||||
joinColumnRefs: subTableData?.joinColumnRefs || tableNodeData?.joinColumnRefs || [],
|
||||
};
|
||||
});
|
||||
|
||||
setSettingModalNode({
|
||||
nodeType: "screen",
|
||||
nodeId: node.id,
|
||||
screenId: screenId,
|
||||
screenName: nodeData.label || `화면 ${screenId}`,
|
||||
tableName: mainTable,
|
||||
tableLabel: nodeData.subLabel,
|
||||
// 화면의 테이블 정보 전달
|
||||
existingConfig: {
|
||||
mainTable: mainTable,
|
||||
filterTables: filterTables,
|
||||
},
|
||||
});
|
||||
setIsSettingModalOpen(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// 메인 테이블 노드 더블클릭
|
||||
if (node.id.startsWith("table-") && !node.id.startsWith("table-sub-")) {
|
||||
const tableName = node.id.replace("table-", "");
|
||||
const nodeData = node.data as TableNodeData;
|
||||
|
||||
// 이 테이블을 사용하는 화면 찾기
|
||||
const screenId = Object.entries(screenTableMap).find(
|
||||
([_, tbl]) => tbl === tableName
|
||||
)?.[0];
|
||||
|
||||
// 백엔드에서 받은 데이터에서 기존 설정 정보 추출
|
||||
const existingConfigFromData = getTableExistingConfig(tableName);
|
||||
|
||||
setSettingModalNode({
|
||||
nodeType: "table",
|
||||
nodeId: node.id,
|
||||
screenId: screenId ? parseInt(screenId) : 0,
|
||||
screenName: nodeData.subLabel || tableName,
|
||||
tableName: tableName,
|
||||
tableLabel: nodeData.label,
|
||||
// 기존 설정 정보 전달
|
||||
existingConfig: existingConfigFromData || {
|
||||
joinColumnRefs: nodeData.joinColumnRefs,
|
||||
filterColumns: nodeData.filterColumns,
|
||||
fieldMappings: nodeData.fieldMappings?.map(m => ({
|
||||
targetField: m.targetField,
|
||||
sourceField: m.sourceField,
|
||||
sourceTable: m.sourceTable,
|
||||
sourceDisplayName: m.sourceDisplayName,
|
||||
})),
|
||||
referencedBy: nodeData.referencedBy?.map(r => ({
|
||||
fromTable: r.fromTable,
|
||||
fromTableLabel: r.fromTableLabel,
|
||||
fromColumn: r.fromColumn,
|
||||
toColumn: r.toColumn,
|
||||
toColumnLabel: r.toColumnLabel,
|
||||
relationType: r.relationType,
|
||||
})),
|
||||
columns: nodeData.columns,
|
||||
},
|
||||
});
|
||||
setIsSettingModalOpen(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// 서브 테이블 노드 더블클릭
|
||||
if (node.id.startsWith("subtable-")) {
|
||||
const tableName = node.id.replace("subtable-", "");
|
||||
const nodeData = node.data as TableNodeData;
|
||||
|
||||
// 이 서브 테이블을 사용하는 화면 찾기
|
||||
const screenId = Object.entries(screenSubTableMap).find(
|
||||
([_, tables]) => tables.includes(tableName)
|
||||
)?.[0];
|
||||
|
||||
// 백엔드에서 받은 데이터에서 기존 설정 정보 추출
|
||||
const existingConfigFromData = getTableExistingConfig(tableName);
|
||||
|
||||
setSettingModalNode({
|
||||
nodeType: "table",
|
||||
nodeId: node.id,
|
||||
screenId: screenId ? parseInt(screenId) : 0,
|
||||
screenName: nodeData.subLabel || tableName,
|
||||
tableName: tableName,
|
||||
tableLabel: nodeData.label,
|
||||
// 기존 설정 정보 전달
|
||||
existingConfig: existingConfigFromData || {
|
||||
joinColumnRefs: nodeData.joinColumnRefs,
|
||||
filterColumns: nodeData.filterColumns,
|
||||
fieldMappings: nodeData.fieldMappings?.map(m => ({
|
||||
targetField: m.targetField,
|
||||
sourceField: m.sourceField,
|
||||
sourceTable: m.sourceTable,
|
||||
sourceDisplayName: m.sourceDisplayName,
|
||||
})),
|
||||
referencedBy: nodeData.referencedBy?.map(r => ({
|
||||
fromTable: r.fromTable,
|
||||
fromTableLabel: r.fromTableLabel,
|
||||
fromColumn: r.fromColumn,
|
||||
toColumn: r.toColumn,
|
||||
toColumnLabel: r.toColumnLabel,
|
||||
relationType: r.relationType,
|
||||
})),
|
||||
columns: nodeData.columns,
|
||||
},
|
||||
});
|
||||
setIsSettingModalOpen(true);
|
||||
return;
|
||||
}
|
||||
}, [screenTableMap, screenSubTableMap, subTablesDataMap, edges, nodes, getTableExistingConfig]);
|
||||
|
||||
// 설정 모달 닫기 및 새로고침
|
||||
const handleSettingModalClose = useCallback(() => {
|
||||
setIsSettingModalOpen(false);
|
||||
setSettingModalNode(null);
|
||||
}, []);
|
||||
|
||||
// 시각화 새로고침 (설정 저장 후 호출)
|
||||
const handleRefreshVisualization = useCallback(() => {
|
||||
// 강제 새로고침: refreshKey 증가로 useEffect 재실행
|
||||
setRefreshKey(prev => prev + 1);
|
||||
}, []);
|
||||
|
||||
// 포커스에 따른 노드 스타일링 (그룹 모드에서 화면 클릭 시)
|
||||
const styledNodes = React.useMemo(() => {
|
||||
// 그룹 모드가 아니면 원본 반환
|
||||
|
|
@ -1686,14 +1935,47 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
|
|||
|
||||
// 화면-테이블 연결선
|
||||
if (edge.source.startsWith("screen-") && edge.target.startsWith("table-")) {
|
||||
const sourceId = parseInt(edge.source.replace("screen-", ""));
|
||||
const isMyConnection = sourceId === focusedScreenId;
|
||||
|
||||
// 필터 연결선 (edge-screen-filter-)은 포커싱 시에만 표시
|
||||
const isFilterEdge = edge.id.startsWith("edge-screen-filter-");
|
||||
|
||||
if (isFilterEdge) {
|
||||
// 포커스가 없거나 다른 화면 포커스 시 숨김
|
||||
if (focusedScreenId === null || !isMyConnection) {
|
||||
return {
|
||||
...edge,
|
||||
animated: false,
|
||||
style: {
|
||||
...edge.style,
|
||||
stroke: "transparent",
|
||||
strokeWidth: 0,
|
||||
opacity: 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// 포커싱된 화면의 필터 연결선은 표시
|
||||
return {
|
||||
...edge,
|
||||
animated: true,
|
||||
style: {
|
||||
...edge.style,
|
||||
stroke: "#3b82f6",
|
||||
strokeWidth: 2,
|
||||
strokeDasharray: "5,5",
|
||||
opacity: 1,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// 메인 테이블 연결선 (edge-screen-table-)은 기존 로직
|
||||
// 포커스가 없으면 모든 화면-테이블 연결선 정상 표시
|
||||
if (focusedScreenId === null) {
|
||||
return edge; // 원본 그대로
|
||||
}
|
||||
|
||||
const sourceId = parseInt(edge.source.replace("screen-", ""));
|
||||
const isMyConnection = sourceId === focusedScreenId;
|
||||
|
||||
return {
|
||||
...edge,
|
||||
animated: isMyConnection,
|
||||
|
|
@ -1707,33 +1989,36 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
|
|||
};
|
||||
}
|
||||
|
||||
// 메인 테이블 → 서브 테이블 연결선
|
||||
// 메인 테이블 → 서브 테이블 연결선 (메인-메인과 동일한 스타일)
|
||||
// 규격: bottom → top 고정 (아래로 문어발처럼 뻗어나감)
|
||||
if (edge.source.startsWith("table-") && edge.target.startsWith("subtable-")) {
|
||||
// 관계 유형별 색상 결정
|
||||
const visualRelationType = (edge.data as any)?.visualRelationType as VisualRelationType || 'join';
|
||||
const relationColor = RELATION_COLORS[visualRelationType];
|
||||
|
||||
// 포커스가 없으면 모든 서브 테이블 연결선 흐리게 (기본 상태)
|
||||
if (focusedScreenId === null) {
|
||||
return {
|
||||
...edge,
|
||||
sourceHandle: "bottom", // 고정: 메인 테이블 하단에서 나감
|
||||
targetHandle: "top", // 고정: 서브 테이블 상단으로 들어감
|
||||
sourceHandle: "bottom",
|
||||
targetHandle: "top",
|
||||
animated: false,
|
||||
style: {
|
||||
...edge.style,
|
||||
stroke: "#d1d5db",
|
||||
strokeWidth: 1,
|
||||
strokeDasharray: "6,4",
|
||||
opacity: 0.3,
|
||||
stroke: relationColor.strokeLight,
|
||||
strokeWidth: 1.5,
|
||||
strokeDasharray: "8,4",
|
||||
opacity: 0.4,
|
||||
},
|
||||
labelStyle: {
|
||||
...edge.labelStyle,
|
||||
opacity: 0.3,
|
||||
markerEnd: {
|
||||
type: MarkerType.ArrowClosed,
|
||||
color: relationColor.strokeLight,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// 엣지 ID에서 화면 ID 추출: edge-main-sub-{screenId}-{mainTable}-{subTable}
|
||||
const idParts = edge.id.split("-");
|
||||
// edge-main-sub-1413-sales_order_mng-customer_mng 형식
|
||||
const edgeScreenId = idParts.length >= 4 ? parseInt(idParts[3]) : null;
|
||||
|
||||
// 포커스된 화면의 서브 테이블 연결인지 확인
|
||||
|
|
@ -1748,20 +2033,20 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
|
|||
|
||||
return {
|
||||
...edge,
|
||||
sourceHandle: "bottom", // 고정
|
||||
targetHandle: "top", // 고정
|
||||
animated: isActive, // 활성화된 것만 애니메이션
|
||||
sourceHandle: "bottom",
|
||||
targetHandle: "top",
|
||||
animated: isActive,
|
||||
style: {
|
||||
...edge.style,
|
||||
stroke: isActive ? RELATION_COLORS.join.stroke : "#d1d5db", // 상수 사용
|
||||
strokeWidth: isActive ? 2.5 : 1,
|
||||
strokeDasharray: "6,4", // 항상 점선
|
||||
opacity: isActive ? 1 : 0.2,
|
||||
},
|
||||
labelStyle: {
|
||||
...edge.labelStyle,
|
||||
stroke: isActive ? relationColor.stroke : relationColor.strokeLight,
|
||||
strokeWidth: isActive ? 2.5 : 1.5,
|
||||
strokeDasharray: "8,4",
|
||||
opacity: isActive ? 1 : 0.3,
|
||||
},
|
||||
markerEnd: {
|
||||
type: MarkerType.ArrowClosed,
|
||||
color: isActive ? relationColor.stroke : relationColor.strokeLight,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -1820,7 +2105,11 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
|
|||
|
||||
// 메인 테이블 → 메인 테이블 연결선 (서브테이블 구간 통과)
|
||||
// 규격: bottom → bottom_target 고정 (아래쪽 서브테이블 선 구간을 통해 연결)
|
||||
if (edge.source.startsWith("table-") && edge.target.startsWith("table-") && edge.id.startsWith("edge-main-main-")) {
|
||||
// edge-main-main-*, edge-join-db-* 모두 동일한 스타일 적용
|
||||
const isMainToMainJoin = edge.source.startsWith("table-") &&
|
||||
edge.target.startsWith("table-") &&
|
||||
(edge.id.startsWith("edge-main-main-") || edge.id.startsWith("edge-join-db-"));
|
||||
if (isMainToMainJoin) {
|
||||
// 관계 유형별 색상 결정
|
||||
const visualRelationType = (edge.data as any)?.visualRelationType as VisualRelationType || 'join';
|
||||
const relationColor = RELATION_COLORS[visualRelationType];
|
||||
|
|
@ -1882,6 +2171,18 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
|
|||
return [...styledOriginalEdges, ...joinEdges];
|
||||
}, [edges, nodes, selectedGroup, focusedScreenId, screen, screenSubTableMap, subTablesDataMap, screenTableMap]);
|
||||
|
||||
// 그룹의 화면 목록 (데이터 흐름 설정용) - 모든 조건부 return 전에 선언해야 함
|
||||
const groupScreensList = React.useMemo(() => {
|
||||
if (!selectedGroup) return [];
|
||||
// nodes에서 screen- 으로 시작하는 노드들 추출
|
||||
return nodes
|
||||
.filter(n => n.id.startsWith("screen-"))
|
||||
.map(n => ({
|
||||
screen_id: parseInt(n.id.replace("screen-", "")),
|
||||
screen_name: (n.data as ScreenNodeData).label || `화면 ${n.id}`,
|
||||
}));
|
||||
}, [selectedGroup, nodes]);
|
||||
|
||||
// 조건부 렌더링 (모든 훅 선언 후에 위치해야 함)
|
||||
if (!screen && !selectedGroup) {
|
||||
return (
|
||||
|
|
@ -1912,6 +2213,7 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
|
|||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onNodeClick={handleNodeClick}
|
||||
onNodeContextMenu={handleNodeContextMenu}
|
||||
nodeTypes={nodeTypes}
|
||||
minZoom={0.3}
|
||||
maxZoom={1.5}
|
||||
|
|
@ -1921,6 +2223,39 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
|
|||
<Controls position="bottom-right" />
|
||||
</ReactFlow>
|
||||
</div>
|
||||
|
||||
{/* 화면 노드 설정 모달 */}
|
||||
{settingModalNode && settingModalNode.nodeType === "screen" && (
|
||||
<ScreenSettingModal
|
||||
isOpen={isSettingModalOpen}
|
||||
onClose={handleSettingModalClose}
|
||||
screenId={settingModalNode.screenId}
|
||||
screenName={settingModalNode.screenName}
|
||||
groupId={selectedGroup?.id}
|
||||
mainTable={settingModalNode.existingConfig?.mainTable}
|
||||
mainTableLabel={settingModalNode.tableLabel}
|
||||
filterTables={settingModalNode.existingConfig?.filterTables}
|
||||
fieldMappings={settingModalNode.existingConfig?.fieldMappings}
|
||||
componentCount={0}
|
||||
onSaveSuccess={handleRefreshVisualization}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 테이블 노드 설정 모달 */}
|
||||
{settingModalNode && settingModalNode.nodeType === "table" && (
|
||||
<TableSettingModal
|
||||
isOpen={isSettingModalOpen}
|
||||
onClose={handleSettingModalClose}
|
||||
tableName={settingModalNode.tableName || ""}
|
||||
tableLabel={settingModalNode.tableLabel}
|
||||
screenId={settingModalNode.screenId}
|
||||
joinColumnRefs={settingModalNode.existingConfig?.joinColumnRefs}
|
||||
referencedBy={settingModalNode.existingConfig?.referencedBy}
|
||||
columns={settingModalNode.existingConfig?.columns}
|
||||
filterColumns={settingModalNode.existingConfig?.filterColumns}
|
||||
onSaveSuccess={handleRefreshVisualization}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -458,3 +458,4 @@ export default function DataFlowPanel({ groupId, screenId, screens = [] }: DataF
|
|||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -410,3 +410,4 @@ export default function FieldJoinPanel({ screenId, componentId, layoutId }: Fiel
|
|||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue