# 화면 관계 시각화 기능 개선 보고서 ## 개요 화면 그룹 관리에서 ReactFlow를 사용한 화면-테이블 관계 시각화 기능의 범용성 및 정확성 개선 작업을 수행했습니다. --- ## 수정된 파일 목록 | 파일 경로 | 역할 | |----------|------| | `backend-node/src/controllers/screenGroupController.ts` | 화면 서브테이블 정보 API | | `frontend/components/screen/ScreenRelationFlow.tsx` | ReactFlow 시각화 컴포넌트 | | `frontend/lib/api/screenGroup.ts` | API 인터페이스 정의 | --- ## 사용된 데이터베이스 테이블 ### 화면 정의 관련 | 테이블명 | 용도 | 주요 컬럼 | |----------|------|----------| | `screen_definitions` | 화면 정의 정보 | `screen_id`, `screen_name`, `table_name`, `company_code` | | `screen_layouts` | 화면 레이아웃/컴포넌트 정보 | `screen_id`, `properties` (JSONB - componentConfig 포함) | | `screen_groups` | 화면 그룹 정보 | `group_id`, `group_code`, `group_name`, `parent_group_id` | | `screen_group_mappings` | 화면-그룹 매핑 | `group_id`, `screen_id`, `display_order` | ### 메타데이터 관련 | 테이블명 | 용도 | 주요 컬럼 | |----------|------|----------| | `column_labels` | 컬럼 한글명/참조 정보 | `table_name`, `column_name`, `column_label`, `reference_table`, `reference_column` | | `table_info` | 테이블 메타 정보 | `table_name`, `table_label`, `column_count` | ### 조인 정보 추출 소스 #### 1. column_labels 테이블 (테이블 관리에서 설정) ```sql SELECT cl.table_name, cl.column_name, cl.reference_table, -- 참조 테이블 cl.reference_column, -- 참조 컬럼 cl.column_label -- 한글명 FROM column_labels cl WHERE cl.reference_table IS NOT NULL ``` #### 2. screen_layouts.properties (화면 컴포넌트에서 설정) ```sql -- parentDataMapping 추출 SELECT sd.screen_id, sl.properties->'componentConfig'->'parentDataMapping' as parent_data_mapping FROM screen_definitions sd JOIN screen_layouts sl ON sd.screen_id = sl.screen_id WHERE sl.properties->'componentConfig'->'parentDataMapping' IS NOT NULL -- rightPanel.relation 추출 SELECT sd.screen_id, sl.properties->'componentConfig'->'rightPanel'->'relation' as right_panel_relation FROM screen_definitions sd JOIN screen_layouts sl ON sd.screen_id = sl.screen_id WHERE sl.properties->'componentConfig'->'rightPanel'->'relation' IS NOT NULL -- fieldMappings 추출 SELECT sd.screen_id, sl.properties->'componentConfig'->'fieldMappings' as field_mappings FROM screen_definitions sd JOIN screen_layouts sl ON sd.screen_id = sl.screen_id WHERE sl.properties->'componentConfig'->'fieldMappings' IS NOT NULL ``` ### 테이블 관계도 ``` screen_groups (그룹) │ ├─── screen_group_mappings (매핑) │ │ │ └─── screen_definitions (화면) │ │ │ └─── screen_layouts (레이아웃/컴포넌트) │ │ │ └─── properties.componentConfig │ ├── fieldMappings │ ├── parentDataMapping │ ├── columns.mapping │ └── rightPanel.relation │ └─── column_labels (컬럼 메타데이터) │ ├── reference_table (참조 테이블) └── column_label (한글명) ``` --- ## 핵심 문제 및 해결 ### 1. 조인 컬럼 식별 오류 #### 문제 - `customer_item_mapping` 테이블에서 `customer_id`가 조인 컬럼으로 표시되지 않음 - `customer_mng`가 `relationType: source`로 분류되어 `sourceField`가 잘못 사용됨 #### 원인 ```typescript // 기존 잘못된 로직 if (subTable.relationType === 'source') { // sourceField를 메인테이블 컬럼으로 사용 (잘못됨) joinColumns.push(mapping.sourceField); } ``` #### 해결 ```typescript // 수정된 범용 로직 if (subTable.relationType === 'source' && mapping.sourceTable) { // sourceTable이 있으면 parentDataMapping과 유사 // targetField가 메인테이블 컬럼 joinColumns.push(mapping.targetField); } else if (subTable.relationType === 'source') { // 일반 source 타입 joinColumns.push(mapping.sourceField); } ``` --- ### 2. displayColumns 잘못된 처리 #### 문제 - `selected-items-detail-input` 컴포넌트의 `displayColumns`가 메인테이블 `joinColumns`에 추가됨 - `customer_name`, `customer_code` 등이 조인 컬럼으로 잘못 표시됨 #### 원인 ```typescript // 기존 잘못된 로직 if (componentConfig.displayColumns) { componentConfig.displayColumns.forEach((col) => { joinColumns.push(col.name); // 연관 테이블 컬럼을 메인테이블에 추가 (잘못됨) }); } ``` #### 해결 ```typescript // 수정된 로직 - displayColumns는 연관 테이블 컬럼이므로 제외 // 조인 컬럼은 parentDataMapping.targetField에서 별도 추출됨 if (componentConfig.displayColumns) { // 메인 테이블 joinColumns에 추가하지 않음 } ``` --- ### 3. 조인 정보 한글명 미표시 #### 문제 - 조인 컬럼 옆에 `← customer_code` (영문)로 표시됨 - `← 거래처 코드` (한글)로 표시되어야 함 #### 해결 백엔드에서 `column_labels` 테이블 조회하여 한글명 적용: ```typescript // 모든 테이블/컬럼 조합 수집 const columnLookups = []; screenSubTables.forEach((screenData) => { screenData.subTables.forEach((subTable) => { subTable.fieldMappings?.forEach((mapping) => { if (mapping.sourceTable && mapping.sourceField) { columnLookups.push({ tableName: mapping.sourceTable, columnName: mapping.sourceField }); } if (screenData.mainTable && mapping.targetField) { columnLookups.push({ tableName: screenData.mainTable, columnName: mapping.targetField }); } }); }); }); // column_labels에서 한글명 조회 const columnLabelsQuery = ` SELECT table_name, column_name, column_label FROM column_labels WHERE (table_name, column_name) IN (...) `; // 각 fieldMapping에 한글명 적용 mapping.sourceDisplayName = columnLabelsMap[`${sourceTable}.${sourceField}`]; mapping.targetDisplayName = columnLabelsMap[`${mainTable}.${targetField}`]; ``` --- ### 4. 연결 선 라벨 제거 및 단일 로직화 #### 문제 - 테이블 간 연결 선에 `customer_code → customer_id` 라벨이 표시됨 - 조인 정보가 테이블 노드 내부에 이미 표시되므로 중복 - 메인-메인 테이블 조인 로직이 2개 존재 (주황색, 초록색) #### 해결 1. **초록색 선 로직 완전 제거**: 중복 로직으로 인한 혼란 방지 2. **주황색 선 로직 개선**: 모든 메인-메인 조인을 단일 로직으로 처리 ```typescript // 메인-메인 조인 엣지 생성 (단일 로직) const joinEdges: Edge[] = []; // 모든 화면의 메인 테이블 목록 const allMainTables = new Set(Object.values(screenTableMap)); focusedSubTablesData.subTables.forEach((subTable) => { // 1. subTable.tableName이 다른 화면의 메인 테이블인 경우 const isTargetMainTable = allMainTables.has(subTable.tableName) && subTable.tableName !== focusedMainTable; if (isTargetMainTable) { joinEdges.push({ id: `edge-main-join-${focusedScreenId}-${subTable.tableName}-${focusedMainTable}`, source: `table-${subTable.tableName}`, target: `table-${focusedMainTable}`, type: 'smoothstep', animated: true, style: { stroke: '#ea580c', // 주황색 (단일 색상) strokeWidth: 2, strokeDasharray: '8,4', }, markerEnd: { type: MarkerType.ArrowClosed, color: '#ea580c' }, }); } // 2. fieldMappings.sourceTable이 메인 테이블인 경우도 처리 // ... (parentMapping, rightPanelRelation 등) }); ``` --- ### 5. 메인테이블 fieldMappings 미전달 #### 문제 - 서브테이블에만 `fieldMappings`가 전달되어 조인 정보 표시됨 - 메인테이블에는 조인 컬럼 옆 연결 정보가 미표시 #### 해결 메인테이블과 연관테이블에 각각 `fieldMappings` 생성: ```typescript // 메인테이블용 fieldMappings 생성 let mainTableFieldMappings = []; if (isFocusedTable && focusedSubTablesData) { focusedSubTablesData.subTables.forEach((subTable) => { subTable.fieldMappings?.forEach((mapping) => { if (subTable.relationType === 'source' && mapping.sourceTable) { mainTableFieldMappings.push({ sourceField: mapping.sourceField, targetField: mapping.targetField, sourceDisplayName: mapping.sourceDisplayName, targetDisplayName: mapping.targetDisplayName, }); } // ... 기타 relationType 처리 }); }); } // 연관 테이블용 fieldMappings 생성 let relatedTableFieldMappings = []; if (isRelatedTable && relatedTableInfo && focusedSubTablesData) { // 이 테이블이 sourceTable인 경우의 매핑 추출 } // node.data에 fieldMappings 전달 return { ...node, data: { ...node.data, fieldMappings: isFocusedTable ? mainTableFieldMappings : (isRelatedTable ? relatedTableFieldMappings : []), }, }; ``` --- ### 6. 엣지 방향 오류 (참조 방향 반대) #### 문제 - `customer_mng` 화면 포커스 시 `customer_item_mapping`으로 연결선이 표시됨 - 실제로는 `customer_item_mapping.customer_id` → `customer_mng.customer_code` 참조 관계 - 참조하는 쪽(A)이 아닌 참조당하는 쪽(B)에서 연결선이 나가는 오류 #### 원인 ```typescript // 기존 잘못된 로직 newEdges.push({ id: edgeId, source: `table-${mainTable}`, // 참조당하는 테이블 (잘못됨) target: `table-${subTable.tableName}`, // 참조하는 테이블 (잘못됨) // ... }); ``` #### 해결 ```typescript // 수정된 올바른 로직 - source/target 교환 newEdges.push({ id: edgeId, source: `table-${subTable.tableName}`, // 참조하는 테이블 (올바름) target: `table-${mainTable}`, // 참조당하는 테이블 (올바름) // ... }); ``` #### 동작 예시 | 관계 | 포커스된 화면 | 예상 동작 | 실제 동작 (수정 후) | |------|--------------|----------|-------------------| | A가 B를 참조 | A 포커스 | A→B 연결선 ✅ | A→B 연결선 ✅ | | A가 B를 참조 | B 포커스 | 연결선 없음 ✅ | 연결선 없음 ✅ | --- ### 7. column_labels 필터링 누락 #### 문제 - `inbound_mng.item_code`가 `item_info`를 참조하는 것으로 조인선 표시 - 해당 필드는 과거 `entity` 타입이었다가 `text`로 변경됨 - `reference_table` 데이터가 잔존하여 잘못된 조인 관계 생성 #### 원인 ```sql -- 기존 잘못된 쿼리 WHERE cl.reference_table IS NOT NULL AND cl.reference_table != '' AND cl.reference_table != suc.main_table -- input_type 체크 없음! ``` #### 해결 ```sql -- 수정된 쿼리 - input_type = 'entity' 조건 추가 WHERE cl.reference_table IS NOT NULL AND cl.reference_table != '' AND cl.reference_table != suc.main_table AND cl.input_type = 'entity' -- 추가됨 ``` --- ## NTT 설정 소스별 처리 시스템에서 조인 정보가 정의되는 모든 소스를 범용적으로 처리합니다: | 소스 | 위치 | 처리 상태 | |------|------|----------| | `column_labels.reference_table` | 테이블 관리 | ✅ 처리됨 | | `componentConfig.fieldMappings` | autocomplete, entity-search | ✅ 처리됨 | | `componentConfig.columns.mapping` | modal-repeater-table | ✅ 처리됨 | | `componentConfig.parentDataMapping` | selected-items-detail-input | ✅ 처리됨 | | `componentConfig.rightPanel.relation` | split-panel-layout | ✅ 처리됨 | --- ## API 인터페이스 변경 ### FieldMappingInfo ```typescript export interface FieldMappingInfo { sourceTable?: string; // 연관 테이블명 (parentDataMapping에서 사용) sourceField: string; targetField: string; sourceDisplayName?: string; // 연관 테이블 컬럼 한글명 targetDisplayName?: string; // 메인 테이블 컬럼 한글명 } ``` ### SubTableInfo ```typescript export interface SubTableInfo { tableName: string; componentType: string; relationType: 'lookup' | 'source' | 'join' | 'reference' | 'parentMapping' | 'rightPanelRelation'; fieldMappings?: FieldMappingInfo[]; } ``` --- ## 테스트 결과 ### 판매품목정보 그룹 (3번째 화면 포커스) | 테이블 | 컬럼 | 표시 내용 | |--------|------|----------| | `customer_item_mapping` | 거래처 ID | ← 거래처 코드 (조인) | | `customer_item_mapping` | 품목 ID | ← 품번 (조인) | | `customer_mng` | 거래처 코드 | ← 거래처 ID (조인) | | `item_info` | 품번 | ← 품목 ID (조인) | ### 수주관리 그룹 (기존 정상 작동 확인) 모든 조인 관계 및 컬럼 표시 정상 작동 --- ## 범용성 검증 | 항목 | 상태 | |------|------| | 특정 테이블명 하드코딩 | ❌ 없음 | | 특정 컬럼명 하드코딩 | ❌ 없음 | | 조건 기반 분기 | ✅ `sourceTable` 존재 여부, `relationType`으로 판단 | | 새로운 화면/그룹 적용 | ✅ 자동 적용 | --- ## 시각화 결과 예시 ### 포커스 전 (그룹 전체 선택) ``` [화면1] ─── [화면2] ─── [화면3] │ │ │ ▼ ▼ ▼ [item_info] [customer_mng] [customer_item_mapping] (메인) (메인) (메인) ``` ### 3번째 화면 포커스 시 ``` [화면1] ─── [화면2] ─── [화면3] ← 활성 │ │ │ ▼ ▼ ▼ [item_info] [customer_mng] [customer_item_mapping] (연관) (연관) (메인/활성) │ │ │ │ │ ┌───────────┴───────────┐ │ │ │ │ └─────────────┼──────┤ 거래처 ID ← 거래처 코드 │ │ │ 품목 ID ← 품번 │ │ │ (+ 13개 사용 컬럼) │ │ └───────────────────────┘ ┌─────────────┘ │ 거래처 코드 ← 거래처 ID └───────────────────── ``` --- ## 결론 1. **범용성 확보**: 모든 NTT 설정 소스에서 조인 관계 자동 추출 2. **정확성 개선**: `relationType`과 `sourceTable` 기반 정확한 조인 컬럼 식별 3. **사용자 경험 향상**: 한글명 표시 및 직관적인 연결 정보 제공 4. **유지보수성**: 새로운 화면/그룹 추가 시 별도 코드 수정 불필요 5. **메인-메인 조인 로직 단일화**: 기존 중복 로직(초록색/주황색)을 주황색 단일 로직으로 통합 - `subTable.tableName`이 다른 화면의 메인 테이블인 경우 자동 연결 - `fieldMappings.sourceTable` 없어도 메인 테이블 간 조인 감지 6. **연관 테이블 포커싱 개선**: 포커스된 화면의 서브 테이블이 다른 화면의 메인 테이블인 경우 활성화 - 회색 처리 대신 정상 표시 및 조인 컬럼 강조 - `relatedMainTables` 생성 로직 확장으로 `subTable.tableName` 기반 감지 7. **선 연결 규격 정립**: 일관되고 직관적인 연결선 방향 규격화 - **메인-메인 연결선**: `bottom → bottom_target` 고정 (서브테이블 구간을 통해 연결) - **서브 테이블 연결선**: `bottom → top` 고정 (아래로 문어발처럼 뻗어나감) - **절대 규칙**: 선이 테이블이나 화면을 통과하지 않음 8. **초기 메인-메인 엣지 생성**: 그룹 로딩 시점에 메인 테이블 간 연결선 미리 생성 - 연한 주황색(`#fdba74`) 점선으로 기본 표시 - 포커싱 시 진한 주황색(`#ea580c`)으로 강조 및 애니메이션 - 중복 방지를 위한 `pairKey` 기반 Set 사용 9. **ReactFlow Handle ID 구분**: 메인-메인 연결을 위한 핸들 추가 - `TableNode`에 `id="bottom_target"` (type="target") 핸들 추가 - 메인-메인 엣지는 `sourceHandle="bottom"`, `targetHandle="bottom_target"` 사용 - 서브 테이블 엣지는 기존대로 `sourceHandle="bottom"`, `targetHandle="top"` 사용 10. **메인-메인 강조 로직 개선**: 중복 연결선 및 잘못된 연결선 방지 - 포커스된 메인 테이블이 `source`인 경우에만 해당 엣지 강조 - 양방향 중복 강조 방지 (A→B와 B→A 동시 강조 안 함) - 연결되지 않은 테이블에서 조인선이 나타나는 문제 해결 11. **엣지 방향 수정**: 참조 방향에 맞게 엣지 source/target 교정 - 기존 잘못된 방향: `mainTable → subTable.tableName` (참조당하는 테이블 → 참조하는 테이블) - 수정된 올바른 방향: `subTable.tableName → mainTable` (참조하는 테이블 → 참조당하는 테이블) - A가 B를 참조(entity 설정)하면: A 포커스 시 A→B 연결선 표시 - B 포커스 시 연결선 없음 (B는 A를 참조하지 않으므로) 12. **column_labels 필터링 강화**: `input_type = 'entity'`인 경우만 참조 관계로 인정 - `input_type = 'text'`인 경우 `reference_table`이 있어도 조인 관계로 취급하지 않음 - 과거 entity 설정 후 text로 변경된 경우 잔존 데이터 무시 13. **범용적 엣지 방향 결정 로직**: `relationType`에 따른 조건부 방향 결정 - `parentMapping` (sourceTable 있음): `mainTable → sourceTable` 방향 - `rightPanelRelation` (foreignKey 있음): `subTable → mainTable` 방향 - `reference` (column_labels): `mainTable → subTable` 방향 - 기본값: `subTable → mainTable` 방향 ### 연결선 규격 다이어그램 ``` [화면1] [화면2] [화면3] [화면4] │ │ │ │ ▼ ▼ ▼ ▼ [Table1] [Table2] [Table3] [Table4] ← 메인 테이블 │ │ │ │ │ bottom │ bottom │ bottom │ bottom ▼ ▼ ▼ ▼ ┌─────────────────────────────────────────┐ ← 서브테이블 구간 │ (메인-메인 연결선이 이 구간을 통과) │ │ ◄── bottom ─ bottom_target ───► │ │ (서브테이블 연결선도 이 구간에서 연결) │ └─────────────────────────────────────────┘ │ │ ▼ top ▼ top [서브1] [서브2] [서브3] ← 서브 테이블 ``` --- ## [계획] 연결선 정리 시스템 설계 > **상태**: 요건 정의 단계 (미구현) ### 배경 및 필요성 현재 시스템에서 연결선 종류가 증가함에 따라 체계적인 선 관리 시스템이 필요합니다. | 현재 상태 | 문제점 | |-----------|--------| | 조인선만 존재 | 종속 조회, 저장 테이블 등 추가 선 종류 필요 | | 경로 규격 미정립 | 선이 테이블을 통과할 가능성 | | 겹침 방지 미흡 | 같은 경로 사용 시 선 겹침 | --- ### 절대 규칙 (위반 불가) 1. **선이 테이블 노드를 가로로 통과하면 안됨** 2. **선이 화면 노드를 통과하면 안됨** 3. **같은 종류의 선은 같은 구간에서 이동** 4. **다른 종류의 선은 겹치지 않아야 함** --- ### 연결선 종류 정의 #### 현재 구현됨 | 번호 | 선 종류 | 의미 | 색상 | 스타일 | 상태 | |------|---------|------|------|--------|------| | 1 | 화면 → 메인 테이블 | 화면이 사용하는 테이블 | 파란색 (`#3b82f6`) | 실선 | 구현됨 | | 2 | 조인선 (엔티티 참조) | 테이블 간 데이터 병합 | 주황색 (`#ea580c`) | 점선 `8,4` | 구현됨 | #### 추가 예정 | 번호 | 선 종류 | 의미 | 색상 (예정) | 스타일 (예정) | 상태 | |------|---------|------|-------------|---------------|------| | 3 | 종속 조회선 (Master-Detail) | 선택 기준 필터 조회 | 시안/보라색 | 점선 `4,4` | 미구현 | | 4 | 저장 테이블선 | 데이터 저장 대상 | 녹색 | 실선 or 점선 | 미구현 | | 5+ | 기타 확장 | 데이터 플로우 등 | 미정 | 미정 | 미구현 | --- ### 개념 구분: Join vs 종속 조회 | 구분 | Join (조인) | 종속 조회 (Filter) | |------|------------|-------------------| | **데이터 처리** | 두 테이블 **병합** | 한 테이블로 다른 테이블 **필터링** | | **표시 방식** | 같은 그리드에 합쳐서 | 별도 그리드에 각각 | | **SQL** | `JOIN ON A.key = B.key` | `WHERE B.key = (선택된 A.key)` | | **예시** | `inbound_mng` + `item_info` 정보 합침 | `customer_mng` 선택 → `customer_item_mapping` 별도 조회 | | **현재 표현** | 주황색 점선 | 미표현 | **더블 그리드 패턴 예시:** ``` [customer_mng 그리드] | [customer_item_mapping 그리드] (메인/선택) → (필터링된 결과) ``` - 이 경우 `customer_mng`가 진짜 메인 - `customer_item_mapping`은 **종속 조회** (메인이 아님, 선택에 따라 필터링) --- ### 레이아웃 구간 정의 ``` Y: 0-200 ┌─────────────────────────────────────────────┐ │ [화면1] [화면2] [화면3] [화면4] │ ← 화면 구간 └─────────────────────────────────────────────┘ │ │ │ │ Y: 200-300 ════════════════════════════════════════════════ ← 화면-테이블 연결 구간 (파란 실선) │ │ │ │ Y: 300-500 ┌─────────────────────────────────────────────┐ │ [Table1] [Table2] [Table3] [Table4] │ ← 메인 테이블 구간 └─────────────────────────────────────────────┘ │ │ │ │ ▼ ▼ ▼ ▼ Y: 500-700 ┌─────────────────────────────────────────────┐ │ [서브1] [서브2] [서브3] │ ← 서브 테이블 구간 └─────────────────────────────────────────────┘ │ │ │ │ Y: 700-750 ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ← [레벨1] 조인선 구간 (주황) Y: 750-800 ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ← [레벨2] 종속 조회선 구간 (시안/보라) Y: 800-850 ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ← [레벨3] 저장 테이블선 구간 (녹색) Y: 850+ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ← [레벨4+] 확장 구간 ``` --- ### 선 경로 규격 #### 원칙 1. **수직 이동**: 노드 사이 공간에서만 (노드 통과 안함) 2. **수평 이동**: 각 선 종류별 전용 레벨에서만 3. **겹침 방지**: 서로 다른 Y 레벨 사용 #### 선 종류별 경로 | 선 종류 | 출발 핸들 | 도착 핸들 | 수평 이동 레벨 | |---------|-----------|-----------|----------------| | 화면 → 메인 | 화면 bottom | 테이블 top | Y: 200-300 (화면-테이블 구간) | | 메인 → 서브 | 테이블 bottom | 서브 top | Y: 500-700 (서브 테이블 구간 내) | | 조인선 (메인-메인) | 테이블 bottom | 테이블 bottom_target | **Y: 700-750** (레벨1) | | 종속 조회선 | 테이블 bottom | 테이블 bottom_target | **Y: 750-800** (레벨2) | | 저장 테이블선 | 테이블 bottom | 테이블 bottom_target | **Y: 800-850** (레벨3) | --- ### 핸들 설계 현재 `TableNode`에 필요한 핸들: ``` [top] (target) ← 화면에서 오는 선 │ ┌────────┼────────┐ │ │ │ │ 테이블 노드 │ │ │ └────────┬────────┘ │ [bottom] (source) ← 나가는 선 (서브테이블, 조인 등) │ [bottom_target] (target) ← 메인-메인 연결용 들어오는 선 ``` **추가 필요 핸들 (겹침 방지용):** ``` ┌────────────────────────┐ │ │ │ 테이블 노드 │ │ │ └───┬───────┬───────┬────┘ │ │ │ (30%) (50%) (70%) ← X 위치 │ │ │ [bottom_join] [bottom] [bottom_filter] │ │ 조인선 전용 종속 조회선 전용 ``` --- ### 구현 방안 비교 --- #### 방안 A: 커스텀 엣지 경로 (완벽 분리) **레이어 다이어그램:** ``` ┌─────────────────────────────────────────────────────────────────────┐ │ [화면1] [화면2] [화면3] [화면4] │ Y: 0-200 │ │ │ │ │ │ └────┼─────────────┼──────────────┼──────────────┼────────────────────┘ │ │ │ │ ═════╧═════════════╧══════════════╧══════════════╧════════════════════ Y: 250 (화면-테이블 구간) │ │ │ │ ┌────┼─────────────┼──────────────┼──────────────┼────────────────────┐ │ [Table1] [Table2] [Table3] [Table4] │ Y: 300-500 │ │ │ │ │ │ └────┼─────────────┼──────────────┼──────────────┼────────────────────┘ │ │ │ │ ▼ ▼ ▼ ▼ ┌─────────────────────────────────────────────────────────────────────┐ │ [서브1] [서브2] [서브3] │ Y: 550-650 └─────────────────────────────────────────────────────────────────────┘ │ │ ─────┴───────────── 레벨1: 조인선 (주황) ────────┴───────────────────── Y: 700 │ │ └───────────────────────────────────────────┘ │ │ ─────┴───────────── 레벨2: 종속 조회 (보라) ─────┴───────────────────── Y: 750 │ │ └─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ │ ─────┴───────────── 레벨3: 저장 테이블 (녹색) ───┴───────────────────── Y: 800 │ │ └─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ ``` **특징**: 각 선 종류가 **전용 Y 레벨**에서만 수평 이동. 절대 겹치지 않음. **코드 예시:** ```typescript // 커스텀 경로 계산 const getCustomPath = (source, target, lineType) => { const levels = { join: 725, // 조인선 Y 레벨 filter: 775, // 종속 조회선 Y 레벨 save: 825, // 저장 테이블선 Y 레벨 }; const level = levels[lineType]; // 수직 → 수평(레벨) → 수직 경로 return `M${source.x},${source.y} L${source.x},${level} L${target.x},${level} L${target.x},${target.y}`; }; ``` **장점:** - 완벽한 분리 보장 - 절대 규칙 100% 준수 - 확장성 우수 **단점:** - 구현 복잡도 높음 - ReactFlow 기본 기능 대신 커스텀 필요 --- #### 방안 B: 핸들 위치 분리 + smoothstep **레이어 다이어그램:** ``` ┌─────────────────────────────────────────────────────────────────────┐ │ [화면1] [화면2] [화면3] [화면4] │ └────┬─────────────┬──────────────┬──────────────┬────────────────────┘ │ │ │ │ ┌────┼─────────────┼──────────────┼──────────────┼────────────────────┐ │ [Table1] [Table2] [Table3] [Table4] │ │ 30% 50% 70% 30% 50% 70% │ ← 핸들 X 위치 │ │ │ │ │ │ │ │ └───┼───┼───┼──────────────────────────────────┼───┼───┼──────────────┘ │ │ │ │ │ │ │ │ └── 종속 조회 (보라) ──────────────┼───┼───┘ ← 70% 위치 │ │ │ │ │ └────── 조인선 (주황) ─────────────────┼───┘ ← 50% 위치 │ │ └────────── 저장 테이블 (녹색) ────────────┘ ← 30% 위치 ※ 시작점은 다르지만 경로가 가까움 (smoothstep 자동 계산) ※ 선이 가까이 지나가서 겹칠 가능성 있음 ``` **특징**: 핸들 X 위치만 다름. **경로가 가까이 지나가서 겹칠 가능성** 있음. **코드 예시:** ```typescript // 핸들 위치로 분리 // smoothstep + offset edge.pathOptions = { offset: lineType === 'filter' ? 50 : 0 }; ``` **장점:** - ReactFlow 기본 기능 활용 - 구현 상대적 단순 **단점:** - 완벽한 분리 보장 어려움 - 복잡한 경우 선 겹침 가능 --- #### 방안 C: 선 대신 마커/뱃지 (종속 조회만) **레이어 다이어그램:** ``` ┌─────────────────────────────────────────────────────────────────────┐ │ [화면1] [화면2] [화면3] [화면4] │ └────┬─────────────┬──────────────┬──────────────┬────────────────────┘ │ │ │ │ ┌────┼─────────────┼──────────────┼──────────────┼────────────────────┐ │ [Table1] [Table2] [Table3] [Table4 🔗] │ │ │ ↑ │ │ │ "Table1에서 필터 조회" (툴팁) │ │ │ │ │ └────────── 조인선만 표시 (주황) ──────────┘ │ │ │ │ ※ 종속 조회, 저장 테이블은 선 없이 뱃지/아이콘으로만 표시 │ │ ※ 마우스 오버 시 관계 정보 툴팁 표시 │ └─────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────┐ │ [서브1] [서브2] [서브3] │ └─────────────────────────────────────────────────────────────────────┘ ``` **특징**: 선 없음 = **겹침/통과 문제 없음**. 하지만 관계 시각화 약함. **코드 예시:** ```typescript // 테이블 노드에 관계 뱃지 표시 ``` **장점:** - 선 없음 = 겹침/통과 문제 없음 - 화면 깔끔 **단점:** - 관계 시각화 약함 - 일관성 부족 (조인은 선, 종속은 뱃지) --- #### 방안 D: 하이브리드 (커스텀 경로 통일) - 권장 **레이어 다이어그램:** ``` ┌─────────────────────────────────────────────────────────────────────┐ │ 레이어 0: 화면 노드 │ │ [화면1] [화면2] [화면3] [화면4] │ └─────────────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────────────┐ │ 레이어 1: 화면-테이블 연결 (파란 실선) │ │ ═══════════════════════════════════════════════════════════════════ │ Y: 250 └─────────────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────────────┐ │ 레이어 2: 메인 테이블 노드 │ │ [Table1] [Table2] [Table3] [Table4] │ └─────────────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────────────┐ │ 레이어 3: 서브 테이블 노드 │ │ [서브1] [서브2] [서브3] │ └─────────────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────────────┐ │ 레이어 4: 조인선 구간 (주황색) │ │ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ Y: 700-725 │ Table1 ──────────────────────────────────► Table4 │ └─────────────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────────────┐ │ 레이어 5: 종속 조회 구간 (보라색) │ │ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ Y: 750-775 │ Table1 ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─► Table3 │ └─────────────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────────────┐ │ 레이어 6: 저장 테이블 구간 (녹색) │ │ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ Y: 800-825 │ Table2 ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─► Table4 │ └─────────────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────────────┐ │ 레이어 7+: 확장 가능 │ │ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ Y: 850+ │ (미래 확장용) │ └─────────────────────────────────────────────────────────────────────┘ ``` **특징**: 모든 선이 **전용 레이어**에서 이동. 확장성 최고. 절대 겹치지 않음. **구현 방식:** - **조인선**: 커스텀 경로 (방안 A) - 레벨4 (Y: 700-725) - **종속 조회선**: 커스텀 경로 (방안 A) - 레벨5 (Y: 750-775) - **저장 테이블선**: 커스텀 경로 (방안 A) - 레벨6 (Y: 800-825) 모든 선을 동일한 커스텀 경로 시스템으로 통일. **장점:** - 일관된 시스템 - 완벽한 분리 - 확장성 최고 - 절대 규칙 100% 준수 **단점:** - 초기 구현 비용 높음 --- ### 방안 비교 요약표 | 방안 | 겹침 방지 | 절대규칙 준수 | 구현 복잡도 | 확장성 | 시각적 일관성 | |------|-----------|---------------|-------------|--------|---------------| | **A** | 완벽 | 100% | 높음 | 좋음 | 좋음 | | **B** | 불완전 | 90% | 낮음 | 보통 | 보통 | | **C** | 완벽 (선 없음) | 100% | 낮음 | 좋음 | 약함 | | **D** | **완벽** | **100%** | 높음 | **최고** | **최고** | --- ### 구현 우선순위 (제안) | 순서 | 작업 | 설명 | |------|------|------| | 1 | 커스텀 엣지 컴포넌트 개발 | 레벨 기반 경로 계산 | | 2 | 기존 조인선 마이그레이션 | smoothstep → 커스텀 경로 | | 3 | 종속 조회선 구현 | 레벨2 경로 + 시안/보라 색상 | | 4 | 저장 테이블선 구현 | 레벨3 경로 + 녹색 | | 5 | 테스트 및 최적화 | 다양한 그룹에서 검증 | --- ### 색상 팔레트 (확정 - 2026-01-08) | 관계 유형 | 시각적 유형 | 기본 색상 | 강조 색상 | 설명 | |-----------|-------------|-----------|-----------|------| | 화면 → 메인 | - | `#3b82f6` (파랑) | `#2563eb` | 화면-테이블 연결 | | 마스터-디테일 | `filter` | `#8b5cf6` (보라) | `#c4b5fd` | split-panel의 필터링 관계 | | 계층 구조 | `hierarchy` | `#06b6d4` (시안) | `#a5f3fc` | 부모-자식 자기 참조 | | 코드 참조 | `lookup` | `#f59e0b` (주황) | `#fcd34d` | autocomplete 등 코드→명칭 | | 데이터 매핑 | `mapping` | `#10b981` (녹색) | `#6ee7b7` | parentDataMapping | | 엔티티 조인 | `join` | `#ea580c` (주황) | `#fdba74` | 실제 LEFT/INNER JOIN | --- ## 관계 유형 추론 시스템 (2026-01-08 구현) ### 구현 개요 테이블 간 연결선이 단순 "조인"만이 아니라 다양한 관계 유형을 가지므로, 기존 컴포넌트 설정을 기반으로 관계 유형을 추론하여 시각화에 반영합니다. ### 관계 유형 분류 | 유형 | 기술적 의미 | 컴포넌트 | 식별 조건 | |------|------------|----------|-----------| | `filter` | 마스터-디테일 필터링 | split-panel-layout | `relationType='rightPanelRelation'` + `originalRelationType='join'` | | `hierarchy` | 자기 참조 계층 구조 | split-panel-layout | `relationType='rightPanelRelation'` + `originalRelationType='detail'` | | `mapping` | 데이터 참조 주입 | selected-items-detail-input | `relationType='parentMapping'` | | `lookup` | 코드→명칭 변환 | autocomplete, entity-search | `relationType='lookup'` | | `join` | 실제 엔티티 조인 | column_labels 참조 | `relationType='reference'` | ### 백엔드 수정 사항 `screenGroupController.ts`의 `getScreenSubTables` 함수에서 추가 필드 전달: ```typescript // rightPanel.relation 파싱 시 screenSubTables[screenId].subTables.push({ tableName: subTable, componentType: componentType, relationType: 'rightPanelRelation', originalRelationType: relation?.type || 'join', // 추가: 원본 relation.type foreignKey: relation?.foreignKey, // 추가: FK 컬럼 leftColumn: relation?.leftColumn, // 추가: 마스터 컬럼 fieldMappings: ..., }); ``` ### 프론트엔드 수정 사항 1. **타입 확장** (`screenGroup.ts`): - `SubTableInfo`에 `originalRelationType`, `foreignKey`, `leftColumn` 필드 추가 - `VisualRelationType` 타입 정의: `'filter' | 'hierarchy' | 'lookup' | 'mapping' | 'join'` - `inferVisualRelationType()` 함수 추가 2. **시각화 적용** (`ScreenRelationFlow.tsx`): - `RELATION_COLORS` 상수 정의 (관계 유형별 색상) - 엣지 생성 시 `inferVisualRelationType()` 호출 - 엣지 스타일에 관계 유형별 색상 적용 ### 2026-01-09 추가 수정 사항 1. **방향 수정**: `rightPanelRelation` 엣지 방향을 `mainTable → subTable`로 변경 - 이전: `customer_item_mapping → customer_mng` (디테일 → 마스터, 잘못됨) - 수정: `customer_mng → customer_item_mapping` (마스터 → 디테일, 올바름) 2. **화면별 엣지 분리**: 같은 테이블 쌍이라도 화면별로 별도 엣지 생성 - `pairKey`에 `screenId` 포함: `${sourceScreenId}-${[mainTable, subTable].sort().join('-')}` - `edgeId`에 `screenId` 포함: `edge-main-main-${sourceScreenId}-${referrerTable}-${referencedTable}` 3. **포커스 필터링 개선**: 해당 화면에서 생성된 연결선만 표시 - 이전: `sourceTable === focusedMainTable` 조건만 체크 (다른 화면 연결선도 표시됨) - 수정: `edge.data.sourceScreenId === focusedScreenId` 조건으로 변경 4. **parentMapping을 join으로 변경**: `selected-items-detail-input`의 `parentDataMapping`은 FK 관계이므로 `join`으로 분류 - `customer_item_mapping → customer_mng`: 주황색 (FK: customer_id → customer_code) - `customer_item_mapping → item_info`: 주황색 (FK: item_id → item_number) 5. **참조 테이블 시각적 표시**: lookup/reference 관계로 참조되는 테이블에 "X곳 참조" 배지 표시 - `TableNodeData`에 `referencedBy` 필드 추가 - `ReferenceInfo` 인터페이스 정의 (fromTable, fromColumn, toColumn, relationType) - 테이블 노드 헤더에 주황색 배지로 참조 카운트 표시 - 툴팁에 참조하는 테이블 목록 표시 6. **마스터-디테일 필터링 관계 표시**: 디테일 테이블에 "X 필터" 배지 표시 - 마스터-디테일 관계(rightPanelRelation)도 참조 정보 수집에 추가 - **보라색 배지**로 "customer_mng 필터" 형태로 표시 - 툴팁에 FK 컬럼 정보 표시 (예: "customer_mng에서 필터링 (FK: customer_id)") - lookup 관계는 주황색, filter 관계는 보라색으로 구분 7. **FK 컬럼 보라색 강조 + 키값 정보 표시** - 디테일 테이블에서 필터링에 사용되는 FK 컬럼을 **보라색 배경**으로 강조 - 컬럼 옆에 참조 정보 표시: "← customer_mng.customer_code" - 배지에 키값 정보 명확히 표시: "customer_mng.customer_code 필터" - `TableNodeData`에 `filterColumns` 필드 추가 - `ReferenceInfo`에서 `toColumn` 정보로 FK 컬럼 식별 8. **포커스 상태 기반 필터 표시 개선** - **문제**: 필터 배지가 모든 화면에서 항상 표시되어 혼란 발생 - **해결**: 포커스된 화면에서만 해당 관계의 필터 정보 표시 - 노드 생성 시 `referencedBy`, `filterColumns` 제거 - `styledNodes` 함수에서 포커스 상태에 따라 동적으로 설정 - 배지를 헤더 아래 별도 영역으로 이동하여 테이블명 가림 방지 **결과:** | 화면 | customer_item_mapping 표시 | |------|----------------------------| | 1번 화면 포커스 | 필터 배지 O + FK 컬럼 보라색 + **상단 정렬** | | 4번 화면 포커스 | 필터 배지 X, 조인만 표시 | | 그룹 선택 (포커스 없음) | 필터 배지 X, 테이블명만 표시 | 9. **필터 컬럼 상단 정렬** - 필터 컬럼도 파란색/주황색 컬럼처럼 상단에 정렬되어 표시 - `potentialFilteredColumns`에 `filterSet` 포함 - 정렬 순서: **조인 컬럼 → 필터 컬럼 → 사용 컬럼** - 보라색 강조로 필터링 관계 명확히 구분 **정렬 우선순위:** | 순서 | 컬럼 유형 | 색상 | 설명 | |------|----------|------|------| | 1 | 조인 컬럼 | 주황색 | FK 조인 관계 | | 2 | 필터 컬럼 | 보라색 | 마스터-디테일 필터링 | | 3 | 사용 컬럼 | 파란색 | 화면 필드 매핑 | 10. **방안 C 적용: 필터선 제거 + 보라색 테두리 애니메이션** - 필터 관계는 선 없이 뱃지 + 테이블 테두리로만 표시 (겹침 방지) - 필터링된 테이블에 **보라색 테두리** 적용 (부드러운 색상 전환) - 조인선(주황)만 표시, 필터선(보라) 제거 11. **테이블 높이 부드러운 애니메이션** - 포커스 시 컬럼 목록이 변경될 때 **부드러운 높이 전환** 적용 - `transition: height 0.5s cubic-bezier(0.4, 0, 0.2, 1)` 사용 - **Debounce 로직** (50ms): 듀얼 그리드에서 filterColumns와 joinColumns가 2단계로 업데이트되는 문제 해결 - 중간 값(늘어났다가 줄어드는 현상) 무시, 최종 값만 적용 12. **뱃지 영역 레이아웃 개선** - 뱃지를 컬럼 목록 영역 **안에 포함** (높이 늘어남 방지) - `calculatedHeight`에 뱃지 높이(26px) 포함하여 계산 - 뱃지와 컬럼 동시 변경으로 "늘어났다가 줄어드는" 현상 해결 13. **뱃지 스타일 개선** - 회색 테두리 (`border-slate-300`) + 연한 배경 (`bg-slate-50`) - 보라색 컬럼과 확실히 구분되는 디자인 - 필터 태그: 보라색 pill 스타일 (`rounded-full bg-violet-600`) **시각적 표현:** | 관계 유형 | 선 표시 | 테두리 | 배지 | |----------|---------|--------|------| | 조인 | ✅ 주황색 점선 | - | "조인" | | 필터 | ❌ 없음 | 보라색 (부드러운 전환) | "필터 + 키값" | | 룩업 | ✅ 황색 점선 | - | "N곳 참조" | **구현 상세:** - `ScreenRelationFlow.tsx`: `visualRelationType === 'filter'`인 경우 엣지 생성 건너뛰기 - `ScreenNode.tsx`: - `hasFilterRelation` 조건으로 보라색 테두리 + 부드러운 색상 전환 적용 - `calculatedHeight`에 뱃지 높이 포함 - `debouncedHeight` 사용으로 중간 값 무시 - 뱃지를 컬럼 목록 div 안에 배치 ### 향후 개선 가능 사항 1. [ ] 범례(Legend) UI 추가 - 관계 유형별 색상 설명 2. [ ] 엣지 라벨에 관계 유형 표시 3. [x] 툴팁에 상세 관계 정보 표시 (FK, 연결 컬럼 등) - 완료 --- ### 다음 단계 1. [x] 방안 확정 - 방안 1 (추론 로직) 선택 2. [x] 색상 팔레트 확정 3. [x] 관계 유형 추론 함수 구현 4. [x] 방향 및 포커스 필터링 수정 5. [x] parentMapping을 join으로 변경 6. [x] 참조 테이블 시각적 표시 추가 7. [x] 마스터-디테일 필터링 관계 표시 추가 8. [x] FK 컬럼 보라색 강조 + 키값 정보 표시 9. [x] 포커스 상태 기반 필터 표시 개선 10. [x] 필터 컬럼 상단 정렬 (조인 → 필터 → 사용 순서) 11. [x] 방안 C 적용: 필터선 제거 + 보라색 테두리 (펄스 → 부드러운 전환으로 변경) 12. [x] 테이블 높이 부드러운 애니메이션 + Debounce 적용 13. [x] 뱃지 영역 레이아웃 개선 (컬럼 목록 안에 포함) 14. [x] 뱃지 스타일 개선 (회색 테두리로 컬럼과 구분) 15. [x] 서브테이블 Y 좌표 조정 (690px → 740px) 16. [x] **저장 테이블 시각화** (구현 완료) 17. [x] 테이블 스크롤 기능 추가 (maxHeight + overflow-y-auto) 18. [x] 테이블/헤더 둥근 모서리 (rounded-xl, rounded-t-xl) 19. [x] 필터 테이블 조인선 + 참조 테이블 활성화 20. [x] 조인선 색상 상수 통일 (RELATION_COLORS.join.stroke) 21. [x] 필터 연결선 포커싱 제어 (해당 화면 포커싱 시에만 표시) 22. [x] 저장 테이블 제외 조건 추가 (table-list + 체크박스 + openModalWithData) 23. [x] 첫 진입 시 포커싱 없이 시작 (트리에서 화면 클릭 시 그룹만 진입) 24. [ ] **선 교차점 이질감 해결** (계획 중) 22. [ ] 범례 UI 추가 (선택사항) 23. [ ] 엣지 라벨에 관계 유형 표시 (선택사항) --- ## 저장 테이블 시각화 (구현 완료) ### 개요 화면에서 데이터가 **어떤 테이블에 저장**되는지 시각화 ### 저장 테이블 유형 | 유형 | 설명 | 예시 | |------|------|------| | **메인 저장** | 화면의 메인 테이블에 직접 저장 | 수주등록 → `sales_order_mng` | | **연계 저장** | 버튼 클릭 → 다른 화면의 테이블에 저장 | 수주관리 → 출하계획 → `shipment_plan` | | **서브 저장** | 듀얼 그리드에서 서브 테이블에 저장 | 거래처관리 → `customer_item_mapping` | ### 데이터 수집 방법 (백엔드) 저장 테이블 정보를 찾을 수 있는 곳: 1. `componentConfig.action.type = 'save'` (edit, delete 제외) 2. `componentConfig.targetTable` (modal-repeater-table 등) 3. `action.dataTransfer.targetTable` (데이터 전송 대상) **제외 조건:** 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' ) ``` ### 시각적 표현 (구현됨) **핑크색 막대기 표시** - 테이블 노드 **왼쪽 바깥**에 핑크색 세로 막대기 표시 - 위에서 아래로 나타나는 애니메이션 (`scaleY` 트랜지션) - 포커스 해제 시 사라지는 애니메이션 - 막대기 양끝 그라데이션 (투명 → 핑크 → 투명) **스타일:** ```css /* 저장 막대기 스타일 */ position: absolute; left: -6px; /* -left-1.5 */ top: 4px; bottom: 4px; width: 2px; /* w-0.5 */ background: linear-gradient( to bottom, transparent 0%, #f472b6 15%, /* pink-400 */ #f472b6 85%, transparent 100% ); transition: all 0.5s ease-out; transform-origin: top; ``` **애니메이션:** - 포커스 시: `opacity: 1, scaleY: 1` (나타남) - 포커스 해제 시: `opacity: 0, scaleY: 0` (사라짐) ### 색상 팔레트 | 관계 유형 | 선 색상 | 뱃지/막대 색상 | 컬럼 강조 | |----------|---------|---------------|----------| | 조인 | 주황 (#F97316) | 주황 | 주황 | | 필터 | - | 보라 (#8B5CF6) | 보라 | | 룩업 | 황색 (#EAB308) | 황색 | - | | **저장** | - | 핑크 (#F472B6) | - | ### 구현 단계 (완료) 1. [x] 백엔드: `getScreenSubTables`에서 저장 테이블 정보 추출 2. [x] 타입 정의: `SaveTableInfo` 인터페이스 추가 3. [x] 프론트엔드: 핑크색 막대기 UI 구현 4. [x] 프론트엔드: 포커싱 시에만 표시 5. [x] 프론트엔드: 나타나기/사라지기 애니메이션 6. [ ] 프론트엔드: 뱃지 클릭 시 팝오버 상세정보 (향후) --- ## 필터 테이블 조인선 시각화 (구현 완료) ### 개요 마스터-디테일 관계에서 **필터 대상 테이블**이 **다른 테이블과 조인**하는 경우도 시각화 ### 시나리오 "거래처관리 화면" (1번 화면) 포커싱 시: - `customer_mng` (마스터) → `customer_item_mapping` (디테일) 필터 관계 - `customer_item_mapping` → `item_info` **조인 관계** (품목 ID → 품번) ### 구현 내용 1. **화면 → 필터 대상 테이블 연결선** - 파란색 점선으로 화면 → `customer_item_mapping` 연결 - 기존 `customer_mng`로만 가던 연결 외에 추가 2. **필터 대상 테이블의 조인선** - `customer_item_mapping` → `item_info` 주황색 점선 조인선 - `joinColumnRefs` 기반으로 자동 생성 3. **참조 테이블 활성화** - `item_info` 테이블도 함께 활성화 (회색 처리 안 함) - 조인 컬럼 주황색 강조 표시 ### 포커싱 제어 **조인선 (주황색 점선)** - 해당 화면이 포커싱됐을 때만 활성화 - 다른 화면 포커싱 시 흐리게 처리 (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`: 필터 조인 엣지 생성 + styledEdges 처리 - `styledNodes`: 필터 대상 테이블의 조인 참조 테이블 활성화 로직 --- ## 테이블 노드 UI 개선 (구현 완료) ### 스크롤 기능 - 컬럼이 많을 경우 스크롤 가능 (`overflow-y-auto`) - 최대 높이 제한 (`maxHeight: 300px`) - 얇은 스크롤바 (`scrollbar-thin`) ### 둥근 모서리 - 테이블 전체: `rounded-xl` (12px) - 헤더: `rounded-t-xl` (상단만 12px) ### 조인선 색상 통일 - 모든 조인선이 `RELATION_COLORS.join.stroke` 상수 사용 - 기본 색상: `#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에서 화면 노드 클릭 → 포커싱 + 연결선 표시 --- ## [계획] 선 교차점 이질감 해결 > **상태**: 방안 검토 중 (미구현) ### 배경 여러 파란색 연결선이 서로 교차할 때 시각적 이질감 발생 ### 해결 방안 #### 방안 C: 배경색 테두리 (Outline) - 권장 - 각 선에 **흰색 테두리(outline)** 추가 - 교차할 때 위에 있는 선이 아래 선을 "덮는" 효과 - SVG stroke에 흰색 outline 적용 **구현 방식:** ```typescript // 커스텀 엣지 컴포넌트에서 ``` **장점:** - 구현 비교적 쉬움 - 교차점이 깔끔하게 분리되어 보임 - 핸들 위치/경로 변경 없음 **단점:** - 선이 약간 두꺼워 보일 수 있음 --- ## 화면 관리 시스템 업그레이드 현황 ### 프로젝트 개요 화면 관리 시스템 업그레이드를 통해 다음 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에 이벤트 연결 // 모달 렌더링 ``` #### 시각화 새로고침 메커니즘 ```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. **관계 라벨 표시 옵션** --- ## 화면 설정 모달 개선 (2026-01-12) ### 개요 화면 노드 우클릭 시 열리는 설정 모달을 대폭 개선했습니다. ### 주요 변경 사항 #### 1. 테이블 정보 시각화 개선 | 항목 | 변경 내용 | |------|----------| | 메인 테이블 | 아코디언 형식으로 모든 컬럼 표시 | | 필터 테이블 | 아코디언 형식 + 필터/조인 키 색상 구분 | | 사용 중 컬럼 | 파란색 배경 + "필드" 배지로 강조 | #### 2. 화면 프리뷰 상시 표시 - 모달 레이아웃: 좌측 40% (탭) / 우측 60% (프리뷰) - 탭 전환해도 프리뷰 항상 표시 #### 3. 줌/드래그 기능 (react-zoom-pan-pinch 라이브러리) ```bash npm install react-zoom-pan-pinch ``` | 기능 | 동작 | |------|------| | 휠 스크롤 | 마우스 포인터 기준 확대/축소 (20%~300%) | | 드래그 | 화면 이동 | | 클릭 | iframe 내부 버튼/목록 상호작용 | #### 4. 프리뷰 company_code 전달 문제 해결 | 문제 | 해결 | |------|------| | 최고 관리자로 다른 회사 프리뷰 불가 | `companyCodeOverride` 파라미터 도입 | | URL 파라미터 무시됨 | 백엔드에서 admin 전용 오버라이드 처리 | ### 관련 파일 | 파일 | 변경 내용 | |------|----------| | `ScreenSettingModal.tsx` | 전체 UI 개선, 줌/드래그 기능 | | `entityJoin.ts` | `companyCodeOverride` 파라미터 추가 | | `SplitPanelLayoutComponent.tsx` | `companyCode` prop 추가 | | `entityJoinController.ts` | `companyCodeOverride` 처리 로직 | ### 상세 문서 - [화면설정모달_개선_완료_보고서.md](./화면설정모달_개선_완료_보고서.md) --- ## 관련 문서 - [멀티테넌시 구현 가이드](.cursor/rules/multi-tenancy-guide.mdc) - [API 클라이언트 사용 규칙](.cursor/rules/api-client-usage.mdc) - [관리자 페이지 스타일 가이드](.cursor/rules/admin-page-style-guide.mdc)