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_mng가relationType: 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개 존재 (주황색, 초록색)
해결
- 초록색 선 로직 완전 제거: 중복 로직으로 인한 혼란 방지
- 주황색 선 로직 개선: 모든 메인-메인 조인을 단일 로직으로 처리
// 메인-메인 조인 엣지 생성 (단일 로직)
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_id→customer_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_code가item_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
└─────────────────────
결론
- 범용성 확보: 모든 NTT 설정 소스에서 조인 관계 자동 추출
- 정확성 개선:
relationType과sourceTable기반 정확한 조인 컬럼 식별 - 사용자 경험 향상: 한글명 표시 및 직관적인 연결 정보 제공
- 유지보수성: 새로운 화면/그룹 추가 시 별도 코드 수정 불필요
- 메인-메인 조인 로직 단일화: 기존 중복 로직(초록색/주황색)을 주황색 단일 로직으로 통합
subTable.tableName이 다른 화면의 메인 테이블인 경우 자동 연결fieldMappings.sourceTable없어도 메인 테이블 간 조인 감지
- 연관 테이블 포커싱 개선: 포커스된 화면의 서브 테이블이 다른 화면의 메인 테이블인 경우 활성화
- 회색 처리 대신 정상 표시 및 조인 컬럼 강조
relatedMainTables생성 로직 확장으로subTable.tableName기반 감지
- 선 연결 규격 정립: 일관되고 직관적인 연결선 방향 규격화
- 메인-메인 연결선:
bottom → bottom_target고정 (서브테이블 구간을 통해 연결) - 서브 테이블 연결선:
bottom → top고정 (아래로 문어발처럼 뻗어나감) - 절대 규칙: 선이 테이블이나 화면을 통과하지 않음
- 메인-메인 연결선:
- 초기 메인-메인 엣지 생성: 그룹 로딩 시점에 메인 테이블 간 연결선 미리 생성
- 연한 주황색(
#fdba74) 점선으로 기본 표시 - 포커싱 시 진한 주황색(
#ea580c)으로 강조 및 애니메이션 - 중복 방지를 위한
pairKey기반 Set 사용
- 연한 주황색(
- ReactFlow Handle ID 구분: 메인-메인 연결을 위한 핸들 추가
TableNode에id="bottom_target"(type="target") 핸들 추가- 메인-메인 엣지는
sourceHandle="bottom",targetHandle="bottom_target"사용 - 서브 테이블 엣지는 기존대로
sourceHandle="bottom",targetHandle="top"사용
- 메인-메인 강조 로직 개선: 중복 연결선 및 잘못된 연결선 방지
- 포커스된 메인 테이블이
source인 경우에만 해당 엣지 강조 - 양방향 중복 강조 방지 (A→B와 B→A 동시 강조 안 함)
- 연결되지 않은 테이블에서 조인선이 나타나는 문제 해결
- 포커스된 메인 테이블이
- 엣지 방향 수정: 참조 방향에 맞게 엣지 source/target 교정
- 기존 잘못된 방향:
mainTable → subTable.tableName(참조당하는 테이블 → 참조하는 테이블) - 수정된 올바른 방향:
subTable.tableName → mainTable(참조하는 테이블 → 참조당하는 테이블) - A가 B를 참조(entity 설정)하면: A 포커스 시 A→B 연결선 표시
- B 포커스 시 연결선 없음 (B는 A를 참조하지 않으므로)
- 기존 잘못된 방향:
- column_labels 필터링 강화:
input_type = 'entity'인 경우만 참조 관계로 인정input_type = 'text'인 경우reference_table이 있어도 조인 관계로 취급하지 않음- 과거 entity 설정 후 text로 변경된 경우 잔존 데이터 무시
- 범용적 엣지 방향 결정 로직:
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 | 화면 → 메인 테이블 | 화면이 사용하는 테이블 | 파란색 (#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+] 확장 구간
선 경로 규격
원칙
- 수직 이동: 노드 사이 공간에서만 (노드 통과 안함)
- 수평 이동: 각 선 종류별 전용 레벨에서만
- 겹침 방지: 서로 다른 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.ts의 getScreenSubTables 함수에서 추가 필드 전달:
// 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: ...,
});
프론트엔드 수정 사항
-
타입 확장 (
screenGroup.ts):SubTableInfo에originalRelationType,foreignKey,leftColumn필드 추가VisualRelationType타입 정의:'filter' | 'hierarchy' | 'lookup' | 'mapping' | 'join'inferVisualRelationType()함수 추가
-
시각화 적용 (
ScreenRelationFlow.tsx):RELATION_COLORS상수 정의 (관계 유형별 색상)- 엣지 생성 시
inferVisualRelationType()호출 - 엣지 스타일에 관계 유형별 색상 적용
2026-01-09 추가 수정 사항
-
방향 수정:
rightPanelRelation엣지 방향을mainTable → subTable로 변경- 이전:
customer_item_mapping → customer_mng(디테일 → 마스터, 잘못됨) - 수정:
customer_mng → customer_item_mapping(마스터 → 디테일, 올바름)
- 이전:
-
화면별 엣지 분리: 같은 테이블 쌍이라도 화면별로 별도 엣지 생성
pairKey에screenId포함:${sourceScreenId}-${[mainTable, subTable].sort().join('-')}edgeId에screenId포함:edge-main-main-${sourceScreenId}-${referrerTable}-${referencedTable}
-
포커스 필터링 개선: 해당 화면에서 생성된 연결선만 표시
- 이전:
sourceTable === focusedMainTable조건만 체크 (다른 화면 연결선도 표시됨) - 수정:
edge.data.sourceScreenId === focusedScreenId조건으로 변경
- 이전:
-
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)
-
참조 테이블 시각적 표시: lookup/reference 관계로 참조되는 테이블에 "X곳 참조" 배지 표시
TableNodeData에referencedBy필드 추가ReferenceInfo인터페이스 정의 (fromTable, fromColumn, toColumn, relationType)- 테이블 노드 헤더에 주황색 배지로 참조 카운트 표시
- 툴팁에 참조하는 테이블 목록 표시
-
마스터-디테일 필터링 관계 표시: 디테일 테이블에 "X 필터" 배지 표시
- 마스터-디테일 관계(rightPanelRelation)도 참조 정보 수집에 추가
- 보라색 배지로 "customer_mng 필터" 형태로 표시
- 툴팁에 FK 컬럼 정보 표시 (예: "customer_mng에서 필터링 (FK: customer_id)")
- lookup 관계는 주황색, filter 관계는 보라색으로 구분
-
FK 컬럼 보라색 강조 + 키값 정보 표시
- 디테일 테이블에서 필터링에 사용되는 FK 컬럼을 보라색 배경으로 강조
- 컬럼 옆에 참조 정보 표시: "← customer_mng.customer_code"
- 배지에 키값 정보 명확히 표시: "customer_mng.customer_code 필터"
TableNodeData에filterColumns필드 추가ReferenceInfo에서toColumn정보로 FK 컬럼 식별
-
포커스 상태 기반 필터 표시 개선
- 문제: 필터 배지가 모든 화면에서 항상 표시되어 혼란 발생
- 해결: 포커스된 화면에서만 해당 관계의 필터 정보 표시
- 노드 생성 시
referencedBy,filterColumns제거 styledNodes함수에서 포커스 상태에 따라 동적으로 설정- 배지를 헤더 아래 별도 영역으로 이동하여 테이블명 가림 방지
결과:
| 화면 | customer_item_mapping 표시 |
|---|---|
| 1번 화면 포커스 | 필터 배지 O + FK 컬럼 보라색 + 상단 정렬 |
| 4번 화면 포커스 | 필터 배지 X, 조인만 표시 |
| 그룹 선택 (포커스 없음) | 필터 배지 X, 테이블명만 표시 |
- 필터 컬럼 상단 정렬
- 필터 컬럼도 파란색/주황색 컬럼처럼 상단에 정렬되어 표시
potentialFilteredColumns에filterSet포함- 정렬 순서: 조인 컬럼 → 필터 컬럼 → 사용 컬럼
- 보라색 강조로 필터링 관계 명확히 구분
정렬 우선순위:
| 순서 | 컬럼 유형 | 색상 | 설명 |
|---|---|---|---|
| 1 | 조인 컬럼 | 주황색 | FK 조인 관계 |
| 2 | 필터 컬럼 | 보라색 | 마스터-디테일 필터링 |
| 3 | 사용 컬럼 | 파란색 | 화면 필드 매핑 |
-
방안 C 적용: 필터선 제거 + 보라색 테두리 애니메이션
- 필터 관계는 선 없이 뱃지 + 테이블 테두리로만 표시 (겹침 방지)
- 필터링된 테이블에 보라색 테두리 적용 (부드러운 색상 전환)
- 조인선(주황)만 표시, 필터선(보라) 제거
-
테이블 높이 부드러운 애니메이션
- 포커스 시 컬럼 목록이 변경될 때 부드러운 높이 전환 적용
transition: height 0.5s cubic-bezier(0.4, 0, 0.2, 1)사용- Debounce 로직 (50ms): 듀얼 그리드에서 filterColumns와 joinColumns가 2단계로 업데이트되는 문제 해결
- 중간 값(늘어났다가 줄어드는 현상) 무시, 최종 값만 적용
-
뱃지 영역 레이아웃 개선
- 뱃지를 컬럼 목록 영역 안에 포함 (높이 늘어남 방지)
calculatedHeight에 뱃지 높이(26px) 포함하여 계산- 뱃지와 컬럼 동시 변경으로 "늘어났다가 줄어드는" 현상 해결
-
뱃지 스타일 개선
- 회색 테두리 (
border-slate-300) + 연한 배경 (bg-slate-50) - 보라색 컬럼과 확실히 구분되는 디자인
- 필터 태그: 보라색 pill 스타일 (
rounded-full bg-violet-600)
- 회색 테두리 (
시각적 표현:
| 관계 유형 | 선 표시 | 테두리 | 배지 |
|---|---|---|---|
| 조인 | ✅ 주황색 점선 | - | "조인" |
| 필터 | ❌ 없음 | 보라색 (부드러운 전환) | "필터 + 키값" |
| 룩업 | ✅ 황색 점선 | - | "N곳 참조" |
구현 상세:
ScreenRelationFlow.tsx:visualRelationType === 'filter'인 경우 엣지 생성 건너뛰기ScreenNode.tsx:hasFilterRelation조건으로 보라색 테두리 + 부드러운 색상 전환 적용calculatedHeight에 뱃지 높이 포함debouncedHeight사용으로 중간 값 무시- 뱃지를 컬럼 목록 div 안에 배치
향후 개선 가능 사항
- 범례(Legend) UI 추가 - 관계 유형별 색상 설명
- 엣지 라벨에 관계 유형 표시
- 툴팁에 상세 관계 정보 표시 (FK, 연결 컬럼 등) - 완료
다음 단계
- 방안 확정 - 방안 1 (추론 로직) 선택
- 색상 팔레트 확정
- 관계 유형 추론 함수 구현
- 방향 및 포커스 필터링 수정
- parentMapping을 join으로 변경
- 참조 테이블 시각적 표시 추가
- 마스터-디테일 필터링 관계 표시 추가
- FK 컬럼 보라색 강조 + 키값 정보 표시
- 포커스 상태 기반 필터 표시 개선
- 필터 컬럼 상단 정렬 (조인 → 필터 → 사용 순서)
- 방안 C 적용: 필터선 제거 + 보라색 테두리 (펄스 → 부드러운 전환으로 변경)
- 테이블 높이 부드러운 애니메이션 + Debounce 적용
- 뱃지 영역 레이아웃 개선 (컬럼 목록 안에 포함)
- 뱃지 스타일 개선 (회색 테두리로 컬럼과 구분)
- 서브테이블 Y 좌표 조정 (690px → 740px)
- 저장 테이블 시각화 (구현 완료)
- 테이블 스크롤 기능 추가 (maxHeight + overflow-y-auto)
- 테이블/헤더 둥근 모서리 (rounded-xl, rounded-t-xl)
- 필터 테이블 조인선 + 참조 테이블 활성화
- 조인선 색상 상수 통일 (RELATION_COLORS.join.stroke)
- 필터 연결선 포커싱 제어 (해당 화면 포커싱 시에만 표시)
- 저장 테이블 제외 조건 추가 (table-list + 체크박스 + openModalWithData)
- 첫 진입 시 포커싱 없이 시작 (트리에서 화면 클릭 시 그룹만 진입)
- 선 교차점 이질감 해결 (계획 중)
- 범례 UI 추가 (선택사항)
- 엣지 라벨에 관계 유형 표시 (선택사항)
저장 테이블 시각화 (구현 완료)
개요
화면에서 데이터가 어떤 테이블에 저장되는지 시각화
저장 테이블 유형
| 유형 | 설명 | 예시 |
|---|---|---|
| 메인 저장 | 화면의 메인 테이블에 직접 저장 | 수주등록 → sales_order_mng |
| 연계 저장 | 버튼 클릭 → 다른 화면의 테이블에 저장 | 수주관리 → 출하계획 → shipment_plan |
| 서브 저장 | 듀얼 그리드에서 서브 테이블에 저장 | 거래처관리 → customer_item_mapping |
데이터 수집 방법 (백엔드)
저장 테이블 정보를 찾을 수 있는 곳:
componentConfig.action.type = 'save'(edit, delete 제외)componentConfig.targetTable(modal-repeater-table 등)action.dataTransfer.targetTable(데이터 전송 대상)
제외 조건:
action.targetScreenId IS NOT NULL(모달 열기 버튼)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) | - |
구현 단계 (완료)
- 백엔드:
getScreenSubTables에서 저장 테이블 정보 추출 - 타입 정의:
SaveTableInfo인터페이스 추가 - 프론트엔드: 핑크색 막대기 UI 구현
- 프론트엔드: 포커싱 시에만 표시
- 프론트엔드: 나타나기/사라지기 애니메이션
- 프론트엔드: 뱃지 클릭 시 팝오버 상세정보 (향후)
필터 테이블 조인선 시각화 (구현 완료)
개요
마스터-디테일 관계에서 필터 대상 테이블이 다른 테이블과 조인하는 경우도 시각화
시나리오
"거래처관리 화면" (1번 화면) 포커싱 시:
customer_mng(마스터) →customer_item_mapping(디테일) 필터 관계customer_item_mapping→item_info조인 관계 (품목 ID → 품번)
구현 내용
-
화면 → 필터 대상 테이블 연결선
- 파란색 점선으로 화면 →
customer_item_mapping연결 - 기존
customer_mng로만 가던 연결 외에 추가
- 파란색 점선으로 화면 →
-
필터 대상 테이블의 조인선
customer_item_mapping→item_info주황색 점선 조인선joinColumnRefs기반으로 자동 생성
-
참조 테이블 활성화
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);
}}
사용자 경험:
- 트리에서 화면 클릭 (첫 진입) → 깔끔한 초기 상태 (모든 화면/테이블 동일 밝기)
- 같은 그룹 내에서 다른 화면 클릭 → 포커싱 + 연결선 표시
- 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개 탭으로 다양한 설정을 수행할 수 있습니다.
사용법
- 화면 노드 또는 테이블 노드를 더블클릭
- 통합 설정 모달이 열림
- 탭 선택하여 설정
- 저장 후 시각화 자동 새로고침
탭 구성
| 탭 | 기능 | 설명 |
|---|---|---|
| 테이블 연결 | 화면-테이블 관계 설정 | 메인/서브/조회/저장 테이블 지정, 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 클라이언트
향후 개선 사항
필드 매핑 탭 고도화
현재 필드 매핑 탭은 테이블 컬럼 정보를 조회만 가능합니다. 향후 다음 기능 추가 가능:
- 컬럼-컴포넌트 바인딩 설정: 화면 컴포넌트와 DB 컬럼 직접 연결
- 드래그 앤 드롭: 시각적 매핑 UI
- 자동 매핑 추천: 컬럼명 기반 자동 매핑 제안
관계 시각화 연동
설정 저장 후 시각화에 즉시 반영되지만, 다음 개선 가능:
- 실시간 프리뷰: 저장 전 미리보기
- 관계 유형별 색상 커스터마이징
- 관계 라벨 표시 옵션
화면 설정 모달 개선 (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 처리 로직 |