ERP-node/docs/화면관계_시각화_개선_보고서.md

73 KiB

화면 관계 시각화 기능 개선 보고서

개요

화면 그룹 관리에서 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 테이블 (테이블 관리에서 설정)

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 (화면 컴포넌트에서 설정)

-- 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_mngrelationType: source로 분류되어 sourceField가 잘못 사용됨

원인

// 기존 잘못된 로직
if (subTable.relationType === 'source') {
  // sourceField를 메인테이블 컬럼으로 사용 (잘못됨)
  joinColumns.push(mapping.sourceField);
}

해결

// 수정된 범용 로직
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 등이 조인 컬럼으로 잘못 표시됨

원인

// 기존 잘못된 로직
if (componentConfig.displayColumns) {
  componentConfig.displayColumns.forEach((col) => {
    joinColumns.push(col.name); // 연관 테이블 컬럼을 메인테이블에 추가 (잘못됨)
  });
}

해결

// 수정된 로직 - displayColumns는 연관 테이블 컬럼이므로 제외
// 조인 컬럼은 parentDataMapping.targetField에서 별도 추출됨
if (componentConfig.displayColumns) {
  // 메인 테이블 joinColumns에 추가하지 않음
}

3. 조인 정보 한글명 미표시

문제

  • 조인 컬럼 옆에 ← customer_code (영문)로 표시됨
  • ← 거래처 코드 (한글)로 표시되어야 함

해결

백엔드에서 column_labels 테이블 조회하여 한글명 적용:

// 모든 테이블/컬럼 조합 수집
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. 주황색 선 로직 개선: 모든 메인-메인 조인을 단일 로직으로 처리
// 메인-메인 조인 엣지 생성 (단일 로직)
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 생성:

// 메인테이블용 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_idcustomer_mng.customer_code 참조 관계
  • 참조하는 쪽(A)이 아닌 참조당하는 쪽(B)에서 연결선이 나가는 오류

원인

// 기존 잘못된 로직
newEdges.push({
  id: edgeId,
  source: `table-${mainTable}`,           // 참조당하는 테이블 (잘못됨)
  target: `table-${subTable.tableName}`,  // 참조하는 테이블 (잘못됨)
  // ...
});

해결

// 수정된 올바른 로직 - 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_codeitem_info를 참조하는 것으로 조인선 표시
  • 해당 필드는 과거 entity 타입이었다가 text로 변경됨
  • reference_table 데이터가 잔존하여 잘못된 조인 관계 생성

원인

-- 기존 잘못된 쿼리
WHERE cl.reference_table IS NOT NULL
  AND cl.reference_table != ''
  AND cl.reference_table != suc.main_table
  -- input_type 체크 없음!

해결

-- 수정된 쿼리 - 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

export interface FieldMappingInfo {
  sourceTable?: string;        // 연관 테이블명 (parentDataMapping에서 사용)
  sourceField: string;
  targetField: string;
  sourceDisplayName?: string;  // 연관 테이블 컬럼 한글명
  targetDisplayName?: string;  // 메인 테이블 컬럼 한글명
}

SubTableInfo

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. 정확성 개선: relationTypesourceTable 기반 정확한 조인 컬럼 식별
  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 구분: 메인-메인 연결을 위한 핸들 추가
    • TableNodeid="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 레벨에서만 수평 이동. 절대 겹치지 않음.

코드 예시:

// 커스텀 경로 계산
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 위치만 다름. 경로가 가까이 지나가서 겹칠 가능성 있음.

코드 예시:

// 핸들 위치로 분리
<Handle id="bottom_join" style={{ left: '30%' }} />
<Handle id="bottom_filter" style={{ left: '70%' }} />

// smoothstep + offset
edge.pathOptions = { offset: lineType === 'filter' ? 50 : 0 };

장점:

  • ReactFlow 기본 기능 활용
  • 구현 상대적 단순

단점:

  • 완벽한 분리 보장 어려움
  • 복잡한 경우 선 겹침 가능

방안 C: 선 대신 마커/뱃지 (종속 조회만)

레이어 다이어그램:

┌─────────────────────────────────────────────────────────────────────┐
│ [화면1]        [화면2]        [화면3]        [화면4]                │
└────┬─────────────┬──────────────┬──────────────┬────────────────────┘
     │             │              │              │
┌────┼─────────────┼──────────────┼──────────────┼────────────────────┐
│ [Table1]     [Table2]       [Table3]       [Table4 🔗]              │
│    │                                          ↑                     │
│    │                              "Table1에서 필터 조회" (툴팁)     │
│    │                                                                │
│    └────────── 조인선만 표시 (주황) ──────────┘                     │
│                                                                     │
│   ※ 종속 조회, 저장 테이블은 선 없이 뱃지/아이콘으로만 표시         │
│   ※ 마우스 오버 시 관계 정보 툴팁 표시                              │
└─────────────────────────────────────────────────────────────────────┘
     │
     ▼
┌─────────────────────────────────────────────────────────────────────┐
│          [서브1]         [서브2]         [서브3]                    │
└─────────────────────────────────────────────────────────────────────┘

특징: 선 없음 = 겹침/통과 문제 없음. 하지만 관계 시각화 약함.

코드 예시:

// 테이블 노드에 관계 뱃지 표시
<TableNode>
  <Badge icon="filter" tooltip="customer_mng에서 필터 조회" />
</TableNode>

장점:

  • 선 없음 = 겹침/통과 문제 없음
  • 화면 깔끔

단점:

  • 관계 시각화 약함
  • 일관성 부족 (조인은 선, 종속은 뱃지)

방안 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.tsgetScreenSubTables 함수에서 추가 필드 전달:

// 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):

    • SubTableInfooriginalRelationType, 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. 화면별 엣지 분리: 같은 테이블 쌍이라도 화면별로 별도 엣지 생성

    • pairKeyscreenId 포함: ${sourceScreenId}-${[mainTable, subTable].sort().join('-')}
    • edgeIdscreenId 포함: edge-main-main-${sourceScreenId}-${referrerTable}-${referencedTable}
  3. 포커스 필터링 개선: 해당 화면에서 생성된 연결선만 표시

    • 이전: sourceTable === focusedMainTable 조건만 체크 (다른 화면 연결선도 표시됨)
    • 수정: edge.data.sourceScreenId === focusedScreenId 조건으로 변경
  4. parentMapping을 join으로 변경: selected-items-detail-inputparentDataMapping은 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곳 참조" 배지 표시

    • TableNodeDatareferencedBy 필드 추가
    • 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 필터"
    • TableNodeDatafilterColumns 필드 추가
    • ReferenceInfo에서 toColumn 정보로 FK 컬럼 식별
  8. 포커스 상태 기반 필터 표시 개선

    • 문제: 필터 배지가 모든 화면에서 항상 표시되어 혼란 발생
    • 해결: 포커스된 화면에서만 해당 관계의 필터 정보 표시
    • 노드 생성 시 referencedBy, filterColumns 제거
    • styledNodes 함수에서 포커스 상태에 따라 동적으로 설정
    • 배지를 헤더 아래 별도 영역으로 이동하여 테이블명 가림 방지

결과:

화면 customer_item_mapping 표시
1번 화면 포커스 필터 배지 O + FK 컬럼 보라색 + 상단 정렬
4번 화면 포커스 필터 배지 X, 조인만 표시
그룹 선택 (포커스 없음) 필터 배지 X, 테이블명만 표시
  1. 필터 컬럼 상단 정렬
    • 필터 컬럼도 파란색/주황색 컬럼처럼 상단에 정렬되어 표시
    • potentialFilteredColumnsfilterSet 포함
    • 정렬 순서: 조인 컬럼 → 필터 컬럼 → 사용 컬럼
    • 보라색 강조로 필터링 관계 명확히 구분

정렬 우선순위:

순서 컬럼 유형 색상 설명
1 조인 컬럼 주황색 FK 조인 관계
2 필터 컬럼 보라색 마스터-디테일 필터링
3 사용 컬럼 파란색 화면 필드 매핑
  1. 방안 C 적용: 필터선 제거 + 보라색 테두리 애니메이션

    • 필터 관계는 선 없이 뱃지 + 테이블 테두리로만 표시 (겹침 방지)
    • 필터링된 테이블에 보라색 테두리 적용 (부드러운 색상 전환)
    • 조인선(주황)만 표시, 필터선(보라) 제거
  2. 테이블 높이 부드러운 애니메이션

    • 포커스 시 컬럼 목록이 변경될 때 부드러운 높이 전환 적용
    • transition: height 0.5s cubic-bezier(0.4, 0, 0.2, 1) 사용
    • Debounce 로직 (50ms): 듀얼 그리드에서 filterColumns와 joinColumns가 2단계로 업데이트되는 문제 해결
    • 중간 값(늘어났다가 줄어드는 현상) 무시, 최종 값만 적용
  3. 뱃지 영역 레이아웃 개선

    • 뱃지를 컬럼 목록 영역 안에 포함 (높이 늘어남 방지)
    • calculatedHeight에 뱃지 높이(26px) 포함하여 계산
    • 뱃지와 컬럼 동시 변경으로 "늘어났다가 줄어드는" 현상 해결
  4. 뱃지 스타일 개선

    • 회색 테두리 (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. 툴팁에 상세 관계 정보 표시 (FK, 연결 컬럼 등) - 완료

다음 단계

  1. 방안 확정 - 방안 1 (추론 로직) 선택
  2. 색상 팔레트 확정
  3. 관계 유형 추론 함수 구현
  4. 방향 및 포커스 필터링 수정
  5. parentMapping을 join으로 변경
  6. 참조 테이블 시각적 표시 추가
  7. 마스터-디테일 필터링 관계 표시 추가
  8. FK 컬럼 보라색 강조 + 키값 정보 표시
  9. 포커스 상태 기반 필터 표시 개선
  10. 필터 컬럼 상단 정렬 (조인 → 필터 → 사용 순서)
  11. 방안 C 적용: 필터선 제거 + 보라색 테두리 (펄스 → 부드러운 전환으로 변경)
  12. 테이블 높이 부드러운 애니메이션 + Debounce 적용
  13. 뱃지 영역 레이아웃 개선 (컬럼 목록 안에 포함)
  14. 뱃지 스타일 개선 (회색 테두리로 컬럼과 구분)
  15. 서브테이블 Y 좌표 조정 (690px → 740px)
  16. 저장 테이블 시각화 (구현 완료)
  17. 테이블 스크롤 기능 추가 (maxHeight + overflow-y-auto)
  18. 테이블/헤더 둥근 모서리 (rounded-xl, rounded-t-xl)
  19. 필터 테이블 조인선 + 참조 테이블 활성화
  20. 조인선 색상 상수 통일 (RELATION_COLORS.join.stroke)
  21. 필터 연결선 포커싱 제어 (해당 화면 포커싱 시에만 표시)
  22. 저장 테이블 제외 조건 추가 (table-list + 체크박스 + openModalWithData)
  23. 첫 진입 시 포커싱 없이 시작 (트리에서 화면 클릭 시 그룹만 진입)
  24. 선 교차점 이질감 해결 (계획 중)
  25. 범례 UI 추가 (선택사항)
  26. 엣지 라벨에 관계 유형 표시 (선택사항)

저장 테이블 시각화 (구현 완료)

개요

화면에서 데이터가 어떤 테이블에 저장되는지 시각화

저장 테이블 유형

유형 설명 예시
메인 저장 화면의 메인 테이블에 직접 저장 수주등록 → 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
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 트랜지션)
  • 포커스 해제 시 사라지는 애니메이션
  • 막대기 양끝 그라데이션 (투명 → 핑크 → 투명)

스타일:

/* 저장 막대기 스타일 */
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. 백엔드: getScreenSubTables에서 저장 테이블 정보 추출
  2. 타입 정의: SaveTableInfo 인터페이스 추가
  3. 프론트엔드: 핑크색 막대기 UI 구현
  4. 프론트엔드: 포커싱 시에만 표시
  5. 프론트엔드: 나타나기/사라지기 애니메이션
  6. 프론트엔드: 뱃지 클릭 시 팝오버 상세정보 (향후)

필터 테이블 조인선 시각화 (구현 완료)

개요

마스터-디테일 관계에서 필터 대상 테이블다른 테이블과 조인하는 경우도 시각화

시나리오

"거래처관리 화면" (1번 화면) 포커싱 시:

  • customer_mng (마스터) → customer_item_mapping (디테일) 필터 관계
  • customer_item_mappingitem_info 조인 관계 (품목 ID → 품번)

구현 내용

  1. 화면 → 필터 대상 테이블 연결선

    • 파란색 점선으로 화면 → customer_item_mapping 연결
    • 기존 customer_mng로만 가던 연결 외에 추가
  2. 필터 대상 테이블의 조인선

    • customer_item_mappingitem_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 처리:

// 필터 조인 엣지 (주황색)
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 안에서 화면 클릭 시: 정상 포커싱

코드 변경:

// 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 적용

구현 방식:

// 커스텀 엣지 컴포넌트에서
<path 
  d={edgePath} 
  stroke="white" 
  strokeWidth={strokeWidth + 4}  // 배경 테두리
/>
<path 
  d={edgePath} 
  stroke={strokeColor} 
  strokeWidth={strokeWidth}  // 실제 
/>

장점:

  • 구현 비교적 쉬움
  • 교차점이 깔끔하게 분리되어 보임
  • 핸들 위치/경로 변경 없음

단점:

  • 선이 약간 두꺼워 보일 수 있음

화면 관리 시스템 업그레이드 현황

프로젝트 개요

화면 관리 시스템 업그레이드를 통해 다음 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 (수정)

// 노드 더블클릭 이벤트 핸들러 추가
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}
  ...
/>

시각화 새로고침 메커니즘

// 강제 새로고침용 키
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 라이브러리)

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 처리 로직

상세 문서


관련 문서