feat: 서브 테이블 정보 및 관계 시각화 개선
- 화면 서브 테이블에서 valueField, parentFieldId, cascadingParentField, controlField 정보를 추출하는 쿼리 추가 - 서브 테이블의 관계 유형을 추론하기 위한 추가 정보 필드(originalRelationType, foreignKey, leftColumn) 포함 - 필터링에 사용되는 FK 컬럼을 TableNodeData 인터페이스에 추가하여 시각화 개선 - 관계 유형별 색상 정의 및 시각적 관계 유형 추론 함수 추가 - 화면 관계 흐름에서 서브 테이블 연결선 및 필터링 참조 정보 표시 기능 개선
This commit is contained in:
parent
b279f8d58d
commit
8928d851ca
|
|
@ -1382,6 +1382,58 @@ export const getScreenSubTables = async (req: Request, res: Response) => {
|
||||||
OR sl.properties->'componentConfig'->>'field' IS NOT NULL
|
OR sl.properties->'componentConfig'->>'field' IS NOT NULL
|
||||||
OR sl.properties->>'field' IS NOT NULL
|
OR sl.properties->>'field' IS NOT NULL
|
||||||
)
|
)
|
||||||
|
|
||||||
|
UNION
|
||||||
|
|
||||||
|
-- valueField 추출 (entity-search-input, autocomplete-search-input 등에서 사용)
|
||||||
|
SELECT
|
||||||
|
sd.screen_id,
|
||||||
|
sd.screen_name,
|
||||||
|
sd.table_name as main_table,
|
||||||
|
sl.properties->'componentConfig'->>'valueField' as column_name
|
||||||
|
FROM screen_definitions sd
|
||||||
|
JOIN screen_layouts sl ON sd.screen_id = sl.screen_id
|
||||||
|
WHERE sd.screen_id = ANY($1)
|
||||||
|
AND sl.properties->'componentConfig'->>'valueField' IS NOT NULL
|
||||||
|
|
||||||
|
UNION
|
||||||
|
|
||||||
|
-- parentFieldId 추출 (캐스케이딩 관계에서 사용)
|
||||||
|
SELECT
|
||||||
|
sd.screen_id,
|
||||||
|
sd.screen_name,
|
||||||
|
sd.table_name as main_table,
|
||||||
|
sl.properties->'componentConfig'->>'parentFieldId' as column_name
|
||||||
|
FROM screen_definitions sd
|
||||||
|
JOIN screen_layouts sl ON sd.screen_id = sl.screen_id
|
||||||
|
WHERE sd.screen_id = ANY($1)
|
||||||
|
AND sl.properties->'componentConfig'->>'parentFieldId' IS NOT NULL
|
||||||
|
|
||||||
|
UNION
|
||||||
|
|
||||||
|
-- cascadingParentField 추출 (캐스케이딩 부모 필드)
|
||||||
|
SELECT
|
||||||
|
sd.screen_id,
|
||||||
|
sd.screen_name,
|
||||||
|
sd.table_name as main_table,
|
||||||
|
sl.properties->'componentConfig'->>'cascadingParentField' as column_name
|
||||||
|
FROM screen_definitions sd
|
||||||
|
JOIN screen_layouts sl ON sd.screen_id = sl.screen_id
|
||||||
|
WHERE sd.screen_id = ANY($1)
|
||||||
|
AND sl.properties->'componentConfig'->>'cascadingParentField' IS NOT NULL
|
||||||
|
|
||||||
|
UNION
|
||||||
|
|
||||||
|
-- controlField 추출 (conditional-container에서 사용)
|
||||||
|
SELECT
|
||||||
|
sd.screen_id,
|
||||||
|
sd.screen_name,
|
||||||
|
sd.table_name as main_table,
|
||||||
|
sl.properties->'componentConfig'->>'controlField' as column_name
|
||||||
|
FROM screen_definitions sd
|
||||||
|
JOIN screen_layouts sl ON sd.screen_id = sl.screen_id
|
||||||
|
WHERE sd.screen_id = ANY($1)
|
||||||
|
AND sl.properties->'componentConfig'->>'controlField' IS NOT NULL
|
||||||
)
|
)
|
||||||
SELECT DISTINCT
|
SELECT DISTINCT
|
||||||
suc.screen_id,
|
suc.screen_id,
|
||||||
|
|
@ -1398,6 +1450,7 @@ export const getScreenSubTables = async (req: Request, res: Response) => {
|
||||||
WHERE cl.reference_table IS NOT NULL
|
WHERE cl.reference_table IS NOT NULL
|
||||||
AND cl.reference_table != ''
|
AND cl.reference_table != ''
|
||||||
AND cl.reference_table != suc.main_table
|
AND cl.reference_table != suc.main_table
|
||||||
|
AND cl.input_type = 'entity'
|
||||||
ORDER BY suc.screen_id
|
ORDER BY suc.screen_id
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
@ -1625,13 +1678,27 @@ export const getScreenSubTables = async (req: Request, res: Response) => {
|
||||||
existingSubTable.fieldMappings!.push(newMapping);
|
existingSubTable.fieldMappings!.push(newMapping);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
// 추가 정보도 업데이트
|
||||||
|
if (relation?.type) {
|
||||||
|
(existingSubTable as any).originalRelationType = relation.type;
|
||||||
|
}
|
||||||
|
if (relation?.foreignKey) {
|
||||||
|
(existingSubTable as any).foreignKey = relation.foreignKey;
|
||||||
|
}
|
||||||
|
if (relation?.leftColumn) {
|
||||||
|
(existingSubTable as any).leftColumn = relation.leftColumn;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
screenSubTables[screenId].subTables.push({
|
screenSubTables[screenId].subTables.push({
|
||||||
tableName: subTable,
|
tableName: subTable,
|
||||||
componentType: componentType,
|
componentType: componentType,
|
||||||
relationType: 'rightPanelRelation',
|
relationType: 'rightPanelRelation',
|
||||||
|
// 관계 유형 추론을 위한 추가 정보
|
||||||
|
originalRelationType: relation?.type || 'join', // 원본 relation.type ("join" | "detail")
|
||||||
|
foreignKey: relation?.foreignKey, // 디테일 테이블의 FK 컬럼
|
||||||
|
leftColumn: relation?.leftColumn, // 마스터 테이블의 선택 기준 컬럼
|
||||||
fieldMappings: fieldMappings.length > 0 ? fieldMappings : undefined,
|
fieldMappings: fieldMappings.length > 0 ? fieldMappings : undefined,
|
||||||
});
|
} as any);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -149,8 +149,8 @@ export function ScreenGroupTreeView({
|
||||||
const getScreensInGroup = (groupId: number): ScreenDefinition[] => {
|
const getScreensInGroup = (groupId: number): ScreenDefinition[] => {
|
||||||
const group = groups.find((g) => g.id === groupId);
|
const group = groups.find((g) => g.id === groupId);
|
||||||
if (!group?.screens) {
|
if (!group?.screens) {
|
||||||
const screenIds = groupScreensMap.get(groupId) || [];
|
const screenIds = groupScreensMap.get(groupId) || [];
|
||||||
return screens.filter((screen) => screenIds.includes(screen.screenId));
|
return screens.filter((screen) => screenIds.includes(screen.screenId));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 그룹의 screens 배열에서 display_order 정보를 가져와서 정렬
|
// 그룹의 screens 배열에서 display_order 정보를 가져와서 정렬
|
||||||
|
|
@ -428,15 +428,15 @@ export function ScreenGroupTreeView({
|
||||||
{groups
|
{groups
|
||||||
.filter((g) => !(g as any).parent_group_id) // 대분류만 (parent_group_id가 null)
|
.filter((g) => !(g as any).parent_group_id) // 대분류만 (parent_group_id가 null)
|
||||||
.map((group) => {
|
.map((group) => {
|
||||||
const groupId = String(group.id);
|
const groupId = String(group.id);
|
||||||
const isExpanded = expandedGroups.has(groupId);
|
const isExpanded = expandedGroups.has(groupId);
|
||||||
const groupScreens = getScreensInGroup(group.id);
|
const groupScreens = getScreensInGroup(group.id);
|
||||||
|
|
||||||
// 하위 그룹들 찾기
|
// 하위 그룹들 찾기
|
||||||
const childGroups = groups.filter((g) => (g as any).parent_group_id === group.id);
|
const childGroups = groups.filter((g) => (g as any).parent_group_id === group.id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={groupId} className="mb-1">
|
<div key={groupId} className="mb-1">
|
||||||
{/* 그룹 헤더 */}
|
{/* 그룹 헤더 */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,14 @@ export interface FieldMappingDisplay {
|
||||||
targetDisplayName?: string; // 서브 테이블 한글 컬럼명 (예: 사용자ID)
|
targetDisplayName?: string; // 서브 테이블 한글 컬럼명 (예: 사용자ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 참조 관계 정보 (다른 테이블에서 이 테이블을 참조하는 경우)
|
||||||
|
export interface ReferenceInfo {
|
||||||
|
fromTable: string; // 참조하는 테이블명
|
||||||
|
fromColumn: string; // 참조하는 컬럼명
|
||||||
|
toColumn: string; // 참조되는 컬럼명 (이 테이블의 컬럼)
|
||||||
|
relationType: 'lookup' | 'join' | 'filter'; // 참조 유형
|
||||||
|
}
|
||||||
|
|
||||||
// 테이블 노드 데이터 인터페이스
|
// 테이블 노드 데이터 인터페이스
|
||||||
export interface TableNodeData {
|
export interface TableNodeData {
|
||||||
label: string;
|
label: string;
|
||||||
|
|
@ -58,8 +66,11 @@ export interface TableNodeData {
|
||||||
// 포커스 시 강조할 컬럼 정보
|
// 포커스 시 강조할 컬럼 정보
|
||||||
highlightedColumns?: string[]; // 화면에서 사용하는 컬럼 (영문명)
|
highlightedColumns?: string[]; // 화면에서 사용하는 컬럼 (영문명)
|
||||||
joinColumns?: string[]; // 조인에 사용되는 컬럼
|
joinColumns?: string[]; // 조인에 사용되는 컬럼
|
||||||
|
filterColumns?: string[]; // 필터링에 사용되는 FK 컬럼 (마스터-디테일 관계)
|
||||||
// 필드 매핑 정보 (조인 관계 표시용)
|
// 필드 매핑 정보 (조인 관계 표시용)
|
||||||
fieldMappings?: FieldMappingDisplay[]; // 서브 테이블일 때 조인 관계 표시
|
fieldMappings?: FieldMappingDisplay[]; // 서브 테이블일 때 조인 관계 표시
|
||||||
|
// 참조 관계 정보 (다른 테이블에서 이 테이블을 참조하는 경우)
|
||||||
|
referencedBy?: ReferenceInfo[]; // 이 테이블을 참조하는 관계들
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== 유틸리티 함수 ==========
|
// ========== 유틸리티 함수 ==========
|
||||||
|
|
@ -420,10 +431,11 @@ const ScreenPreview: React.FC<{ layoutSummary: ScreenLayoutSummary; screenType:
|
||||||
|
|
||||||
// ========== 테이블 노드 (하단) - 컬럼 목록 표시 (컴팩트) ==========
|
// ========== 테이블 노드 (하단) - 컬럼 목록 표시 (컴팩트) ==========
|
||||||
export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => {
|
export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => {
|
||||||
const { label, subLabel, isMain, isFocused, isFaded, columns, highlightedColumns, joinColumns, fieldMappings } = data;
|
const { label, subLabel, isMain, isFocused, isFaded, columns, highlightedColumns, joinColumns, filterColumns, fieldMappings, referencedBy } = data;
|
||||||
|
|
||||||
// 강조할 컬럼 세트 (영문 컬럼명 기준)
|
// 강조할 컬럼 세트 (영문 컬럼명 기준)
|
||||||
const highlightSet = new Set(highlightedColumns || []);
|
const highlightSet = new Set(highlightedColumns || []);
|
||||||
|
const filterSet = new Set(filterColumns || []); // 필터링에 사용되는 FK 컬럼
|
||||||
const joinSet = new Set(joinColumns || []);
|
const joinSet = new Set(joinColumns || []);
|
||||||
|
|
||||||
// 필드 매핑 맵 생성 (targetField → { sourceField, sourceDisplayName })
|
// 필드 매핑 맵 생성 (targetField → { sourceField, sourceDisplayName })
|
||||||
|
|
@ -475,12 +487,21 @@ export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Handles */}
|
{/* Handles */}
|
||||||
|
{/* top target: 화면 → 메인테이블 연결용 */}
|
||||||
<Handle
|
<Handle
|
||||||
type="target"
|
type="target"
|
||||||
position={Position.Top}
|
position={Position.Top}
|
||||||
id="top"
|
id="top"
|
||||||
className="!h-2 !w-2 !border-2 !border-background !bg-emerald-500 opacity-0 transition-opacity group-hover:opacity-100"
|
className="!h-2 !w-2 !border-2 !border-background !bg-emerald-500 opacity-0 transition-opacity group-hover:opacity-100"
|
||||||
/>
|
/>
|
||||||
|
{/* top source: 메인테이블 → 메인테이블 연결용 (위쪽으로 나가는 선) */}
|
||||||
|
<Handle
|
||||||
|
type="source"
|
||||||
|
position={Position.Top}
|
||||||
|
id="top_source"
|
||||||
|
style={{ top: -4 }}
|
||||||
|
className="!h-2 !w-2 !border-2 !border-background !bg-orange-500 opacity-0 transition-opacity group-hover:opacity-100"
|
||||||
|
/>
|
||||||
<Handle
|
<Handle
|
||||||
type="target"
|
type="target"
|
||||||
position={Position.Left}
|
position={Position.Left}
|
||||||
|
|
@ -499,22 +520,65 @@ export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => {
|
||||||
id="bottom"
|
id="bottom"
|
||||||
className="!h-2 !w-2 !border-2 !border-background !bg-orange-500 opacity-0 transition-opacity group-hover:opacity-100"
|
className="!h-2 !w-2 !border-2 !border-background !bg-orange-500 opacity-0 transition-opacity group-hover:opacity-100"
|
||||||
/>
|
/>
|
||||||
|
{/* bottom target: 메인테이블 ← 메인테이블 연결용 (아래에서 들어오는 선) */}
|
||||||
|
<Handle
|
||||||
|
type="target"
|
||||||
|
position={Position.Bottom}
|
||||||
|
id="bottom_target"
|
||||||
|
style={{ bottom: -4 }}
|
||||||
|
className="!h-2 !w-2 !border-2 !border-background !bg-orange-500 opacity-0 transition-opacity group-hover:opacity-100"
|
||||||
|
/>
|
||||||
|
|
||||||
{/* 헤더 (초록색, 컴팩트) */}
|
{/* 헤더 (초록색, 컴팩트) */}
|
||||||
<div className={`flex items-center gap-2 px-3 py-1.5 text-white transition-colors duration-300 ${
|
<div className={`flex items-center gap-2 px-3 py-1.5 text-white transition-colors duration-300 ${
|
||||||
isFaded ? "bg-gray-400" : isMain ? "bg-emerald-600" : "bg-slate-500"
|
isFaded ? "bg-gray-400" : isMain ? "bg-emerald-600" : "bg-slate-500"
|
||||||
}`}>
|
}`}>
|
||||||
<Database className="h-3.5 w-3.5" />
|
<Database className="h-3.5 w-3.5 shrink-0" />
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="truncate text-[11px] font-semibold">{label}</div>
|
<div className="truncate text-[11px] font-semibold">{label}</div>
|
||||||
{subLabel && <div className="truncate text-[9px] opacity-80">{subLabel}</div>}
|
{subLabel && <div className="truncate text-[9px] opacity-80">{subLabel}</div>}
|
||||||
</div>
|
</div>
|
||||||
{hasActiveColumns && (
|
{hasActiveColumns && (
|
||||||
<span className="rounded-full bg-white/20 px-1.5 py-0.5 text-[8px]">
|
<span className="rounded-full bg-white/20 px-1.5 py-0.5 text-[8px] shrink-0">
|
||||||
{displayColumns.length}개 활성
|
{displayColumns.length}개 활성
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 필터링/참조 관계 표시 (포커스 시에만 표시, 헤더 아래 별도 영역) */}
|
||||||
|
{referencedBy && referencedBy.length > 0 && (() => {
|
||||||
|
const filterRefs = referencedBy.filter(r => r.relationType === 'filter');
|
||||||
|
const lookupRefs = referencedBy.filter(r => r.relationType === 'lookup');
|
||||||
|
|
||||||
|
if (filterRefs.length === 0 && lookupRefs.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-1 px-2 py-1 bg-violet-50 border-b border-violet-100 text-[9px]">
|
||||||
|
{filterRefs.length > 0 && (
|
||||||
|
<span
|
||||||
|
className="flex items-center gap-0.5 rounded bg-violet-500 px-1.5 py-0.5 text-white font-medium"
|
||||||
|
title={`마스터-디테일 필터링\n${filterRefs.map(r => `${r.fromTable}.${r.fromColumn || 'id'} → ${r.toColumn}`).join('\n')}`}
|
||||||
|
>
|
||||||
|
<Link2 className="h-2.5 w-2.5" />
|
||||||
|
<span>필터</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{filterRefs.length > 0 && (
|
||||||
|
<span className="text-violet-600 truncate">
|
||||||
|
{filterRefs.map(r => `${r.fromTable}.${r.fromColumn || 'id'}`).join(', ')}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{lookupRefs.length > 0 && (
|
||||||
|
<span
|
||||||
|
className="flex items-center gap-0.5 rounded bg-amber-500 px-1.5 py-0.5 text-white font-medium"
|
||||||
|
title={`코드 참조 (lookup)\n${lookupRefs.map(r => `${r.fromTable} → ${r.toColumn}`).join('\n')}`}
|
||||||
|
>
|
||||||
|
{lookupRefs.length}곳 참조
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
|
||||||
{/* 컬럼 목록 - 컴팩트하게 (스크롤 가능) */}
|
{/* 컬럼 목록 - 컴팩트하게 (스크롤 가능) */}
|
||||||
<div className="p-1.5 transition-all duration-300 max-h-[150px] overflow-y-auto">
|
<div className="p-1.5 transition-all duration-300 max-h-[150px] overflow-y-auto">
|
||||||
|
|
@ -523,14 +587,22 @@ export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => {
|
||||||
{displayColumns.map((col, idx) => {
|
{displayColumns.map((col, idx) => {
|
||||||
const colOriginal = col.originalName || col.name;
|
const colOriginal = col.originalName || col.name;
|
||||||
const isJoinColumn = joinSet.has(colOriginal);
|
const isJoinColumn = joinSet.has(colOriginal);
|
||||||
|
const isFilterColumn = filterSet.has(colOriginal); // 필터링 FK 컬럼
|
||||||
const isHighlighted = highlightSet.has(colOriginal);
|
const isHighlighted = highlightSet.has(colOriginal);
|
||||||
|
|
||||||
|
// 필터링 참조 정보 (어떤 테이블의 어떤 컬럼에서 필터링되는지)
|
||||||
|
const filterRefInfo = referencedBy?.find(
|
||||||
|
r => r.relationType === 'filter' && r.toColumn === colOriginal
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={col.name}
|
key={col.name}
|
||||||
className={`flex items-center gap-1 rounded px-1.5 py-0.5 transition-all duration-300 ${
|
className={`flex items-center gap-1 rounded px-1.5 py-0.5 transition-all duration-300 ${
|
||||||
isJoinColumn
|
isJoinColumn
|
||||||
? "bg-orange-100 border border-orange-300 shadow-sm"
|
? "bg-orange-100 border border-orange-300 shadow-sm"
|
||||||
|
: isFilterColumn
|
||||||
|
? "bg-violet-100 border border-violet-300 shadow-sm" // 필터 컬럼: 보라색
|
||||||
: isHighlighted
|
: isHighlighted
|
||||||
? "bg-blue-100 border border-blue-300 shadow-sm"
|
? "bg-blue-100 border border-blue-300 shadow-sm"
|
||||||
: hasActiveColumns
|
: hasActiveColumns
|
||||||
|
|
@ -542,15 +614,19 @@ export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => {
|
||||||
opacity: hasActiveColumns ? 0 : 1,
|
opacity: hasActiveColumns ? 0 : 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* PK/FK/조인 아이콘 */}
|
{/* PK/FK/조인/필터 아이콘 */}
|
||||||
{isJoinColumn && <Link2 className="h-2.5 w-2.5 text-orange-500" />}
|
{isJoinColumn && <Link2 className="h-2.5 w-2.5 text-orange-500" />}
|
||||||
{!isJoinColumn && col.isPrimaryKey && <Key className="h-2.5 w-2.5 text-amber-500" />}
|
{isFilterColumn && !isJoinColumn && <Link2 className="h-2.5 w-2.5 text-violet-500" />}
|
||||||
{!isJoinColumn && col.isForeignKey && !col.isPrimaryKey && <Link2 className="h-2.5 w-2.5 text-blue-500" />}
|
{!isJoinColumn && !isFilterColumn && col.isPrimaryKey && <Key className="h-2.5 w-2.5 text-amber-500" />}
|
||||||
{!isJoinColumn && !col.isPrimaryKey && !col.isForeignKey && <div className="w-2.5" />}
|
{!isJoinColumn && !isFilterColumn && col.isForeignKey && !col.isPrimaryKey && <Link2 className="h-2.5 w-2.5 text-blue-500" />}
|
||||||
|
{!isJoinColumn && !isFilterColumn && !col.isPrimaryKey && !col.isForeignKey && <div className="w-2.5" />}
|
||||||
|
|
||||||
{/* 컬럼명 */}
|
{/* 컬럼명 */}
|
||||||
<span className={`flex-1 truncate font-mono text-[9px] font-medium ${
|
<span className={`flex-1 truncate font-mono text-[9px] font-medium ${
|
||||||
isJoinColumn ? "text-orange-700" : isHighlighted ? "text-blue-700" : "text-slate-700"
|
isJoinColumn ? "text-orange-700"
|
||||||
|
: isFilterColumn ? "text-violet-700"
|
||||||
|
: isHighlighted ? "text-blue-700"
|
||||||
|
: "text-slate-700"
|
||||||
}`}>
|
}`}>
|
||||||
{col.name}
|
{col.name}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -567,7 +643,12 @@ export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => {
|
||||||
<span className="rounded bg-orange-200 px-1 text-[7px] text-orange-700">조인</span>
|
<span className="rounded bg-orange-200 px-1 text-[7px] text-orange-700">조인</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{isHighlighted && !isJoinColumn && (
|
{isFilterColumn && !isJoinColumn && filterRefInfo && (
|
||||||
|
<span className="rounded bg-violet-200 px-1 text-[7px] text-violet-700">
|
||||||
|
← {filterRefInfo.fromTable}.{filterRefInfo.fromColumn || 'id'}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{isHighlighted && !isJoinColumn && !isFilterColumn && (
|
||||||
<span className="rounded bg-blue-200 px-1 text-[7px] text-blue-700">사용</span>
|
<span className="rounded bg-blue-200 px-1 text-[7px] text-blue-700">사용</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ import {
|
||||||
import "@xyflow/react/dist/style.css";
|
import "@xyflow/react/dist/style.css";
|
||||||
|
|
||||||
import { ScreenDefinition } from "@/types/screen";
|
import { ScreenDefinition } from "@/types/screen";
|
||||||
import { ScreenNode, TableNode, ScreenNodeData, TableNodeData } from "./ScreenNode";
|
import { ScreenNode, TableNode, ScreenNodeData, TableNodeData, ReferenceInfo } from "./ScreenNode";
|
||||||
import {
|
import {
|
||||||
getFieldJoins,
|
getFieldJoins,
|
||||||
getDataFlows,
|
getDataFlows,
|
||||||
|
|
@ -27,9 +27,21 @@ import {
|
||||||
getScreenSubTables,
|
getScreenSubTables,
|
||||||
ScreenLayoutSummary,
|
ScreenLayoutSummary,
|
||||||
ScreenSubTablesData,
|
ScreenSubTablesData,
|
||||||
|
SubTableInfo,
|
||||||
|
inferVisualRelationType,
|
||||||
|
VisualRelationType,
|
||||||
} from "@/lib/api/screenGroup";
|
} from "@/lib/api/screenGroup";
|
||||||
import { getTableColumns, ColumnTypeInfo } from "@/lib/api/tableManagement";
|
import { getTableColumns, ColumnTypeInfo } from "@/lib/api/tableManagement";
|
||||||
|
|
||||||
|
// 관계 유형별 색상 정의
|
||||||
|
const RELATION_COLORS: Record<VisualRelationType, { stroke: string; strokeLight: string; label: string }> = {
|
||||||
|
filter: { stroke: '#8b5cf6', strokeLight: '#c4b5fd', label: '마스터-디테일' }, // 보라색
|
||||||
|
hierarchy: { stroke: '#06b6d4', strokeLight: '#a5f3fc', label: '계층 구조' }, // 시안색
|
||||||
|
lookup: { stroke: '#f59e0b', strokeLight: '#fcd34d', label: '코드 참조' }, // 주황색 (기존)
|
||||||
|
mapping: { stroke: '#10b981', strokeLight: '#6ee7b7', label: '데이터 매핑' }, // 녹색
|
||||||
|
join: { stroke: '#ea580c', strokeLight: '#fdba74', label: '엔티티 조인' }, // 주황색 (진한)
|
||||||
|
};
|
||||||
|
|
||||||
// 노드 타입 등록
|
// 노드 타입 등록
|
||||||
const nodeTypes = {
|
const nodeTypes = {
|
||||||
screenNode: ScreenNode,
|
screenNode: ScreenNode,
|
||||||
|
|
@ -38,8 +50,8 @@ const nodeTypes = {
|
||||||
|
|
||||||
// 레이아웃 상수
|
// 레이아웃 상수
|
||||||
const SCREEN_Y = 50; // 화면 노드 Y 위치 (상단)
|
const SCREEN_Y = 50; // 화면 노드 Y 위치 (상단)
|
||||||
const TABLE_Y = 420; // 메인 테이블 노드 Y 위치 (중단) - 위로 이동
|
const TABLE_Y = 420; // 메인 테이블 노드 Y 위치 (중단)
|
||||||
const SUB_TABLE_Y = 680; // 서브 테이블 노드 Y 위치 (하단) - 위로 이동
|
const SUB_TABLE_Y = 690; // 서브 테이블 노드 Y 위치 (하단) - 메인과 270px 간격
|
||||||
const NODE_WIDTH = 260; // 노드 너비
|
const NODE_WIDTH = 260; // 노드 너비
|
||||||
const NODE_GAP = 40; // 노드 간격
|
const NODE_GAP = 40; // 노드 간격
|
||||||
|
|
||||||
|
|
@ -385,6 +397,43 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
|
||||||
|
|
||||||
// 화면별 서브 테이블 매핑 저장
|
// 화면별 서브 테이블 매핑 저장
|
||||||
setScreenSubTableMap(newScreenSubTableMap);
|
setScreenSubTableMap(newScreenSubTableMap);
|
||||||
|
|
||||||
|
// ========== 참조 관계 수집 (어떤 테이블이 어디서 참조되는지) ==========
|
||||||
|
// 테이블명 → 참조 정보 배열 (이 테이블을 참조하는 관계들)
|
||||||
|
const tableReferencesMap = new Map<string, ReferenceInfo[]>();
|
||||||
|
|
||||||
|
Object.entries(subTablesData).forEach(([screenIdStr, screenSubData]) => {
|
||||||
|
const mainTable = screenSubData.mainTable;
|
||||||
|
|
||||||
|
screenSubData.subTables.forEach((subTable) => {
|
||||||
|
const visualType = inferVisualRelationType(subTable);
|
||||||
|
|
||||||
|
// 1. lookup, reference 관계: 참조되는 테이블에 정보 추가
|
||||||
|
if (subTable.relationType === 'lookup' || subTable.relationType === 'reference') {
|
||||||
|
const existingRefs = tableReferencesMap.get(subTable.tableName) || [];
|
||||||
|
existingRefs.push({
|
||||||
|
fromTable: mainTable,
|
||||||
|
fromColumn: subTable.fieldMappings?.[0]?.sourceField || '',
|
||||||
|
toColumn: subTable.fieldMappings?.[0]?.targetField || '',
|
||||||
|
relationType: 'lookup',
|
||||||
|
});
|
||||||
|
tableReferencesMap.set(subTable.tableName, existingRefs);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. rightPanelRelation (마스터-디테일 필터링): 디테일 테이블에 정보 추가
|
||||||
|
// 마스터(mainTable) → 디테일(subTable.tableName) 필터링 관계
|
||||||
|
if (subTable.relationType === 'rightPanelRelation') {
|
||||||
|
const existingRefs = tableReferencesMap.get(subTable.tableName) || [];
|
||||||
|
existingRefs.push({
|
||||||
|
fromTable: mainTable,
|
||||||
|
fromColumn: subTable.leftColumn || '', // 마스터 테이블의 선택 기준 컬럼
|
||||||
|
toColumn: subTable.foreignKey || '', // 디테일 테이블의 FK 컬럼
|
||||||
|
relationType: 'filter',
|
||||||
|
});
|
||||||
|
tableReferencesMap.set(subTable.tableName, existingRefs);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// 메인 테이블 노드 배치 (화면들의 중앙 아래에 배치)
|
// 메인 테이블 노드 배치 (화면들의 중앙 아래에 배치)
|
||||||
const mainTableList = Array.from(mainTableSet);
|
const mainTableList = Array.from(mainTableSet);
|
||||||
|
|
@ -434,6 +483,7 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
|
||||||
? `메인 테이블 (${linkedScreens.length}개 화면)`
|
? `메인 테이블 (${linkedScreens.length}개 화면)`
|
||||||
: "메인 테이블";
|
: "메인 테이블";
|
||||||
|
|
||||||
|
// 이 테이블을 참조하는 관계들
|
||||||
tableNodes.push({
|
tableNodes.push({
|
||||||
id: `table-${tableName}`,
|
id: `table-${tableName}`,
|
||||||
type: "tableNode",
|
type: "tableNode",
|
||||||
|
|
@ -443,10 +493,11 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
|
||||||
subLabel: subLabel,
|
subLabel: subLabel,
|
||||||
isMain: true, // mainTableSet의 모든 테이블은 메인
|
isMain: true, // mainTableSet의 모든 테이블은 메인
|
||||||
columns: formattedColumns,
|
columns: formattedColumns,
|
||||||
|
// referencedBy, filterColumns는 styledNodes에서 포커스 상태에 따라 동적으로 설정
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== 하단: 서브 테이블 노드들 (참조/조회용) ==========
|
// ========== 하단: 서브 테이블 노드들 (참조/조회용) ==========
|
||||||
const subTableList = Array.from(subTableSet);
|
const subTableList = Array.from(subTableSet);
|
||||||
|
|
||||||
|
|
@ -496,6 +547,7 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
|
||||||
isMain: false,
|
isMain: false,
|
||||||
columns: formattedColumns,
|
columns: formattedColumns,
|
||||||
isFaded: true, // 기본적으로 흐리게 표시 (포커스 시에만 활성화)
|
isFaded: true, // 기본적으로 흐리게 표시 (포커스 시에만 활성화)
|
||||||
|
// referencedBy, filterColumns는 styledNodes에서 포커스 상태에 따라 동적으로 설정
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -503,26 +555,26 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
|
||||||
|
|
||||||
// ========== 엣지: 연결선 생성 ==========
|
// ========== 엣지: 연결선 생성 ==========
|
||||||
const newEdges: Edge[] = [];
|
const newEdges: Edge[] = [];
|
||||||
|
|
||||||
// 그룹 선택 시: 화면 간 연결선 (display_order 순)
|
// 그룹 선택 시: 화면 간 연결선 (display_order 순)
|
||||||
if (selectedGroup && screenList.length > 1) {
|
if (selectedGroup && screenList.length > 1) {
|
||||||
for (let i = 0; i < screenList.length - 1; i++) {
|
for (let i = 0; i < screenList.length - 1; i++) {
|
||||||
const currentScreen = screenList[i];
|
const currentScreen = screenList[i];
|
||||||
const nextScreen = screenList[i + 1];
|
const nextScreen = screenList[i + 1];
|
||||||
|
|
||||||
newEdges.push({
|
newEdges.push({
|
||||||
id: `edge-screen-flow-${i}`,
|
id: `edge-screen-flow-${i}`,
|
||||||
source: `screen-${currentScreen.screenId}`,
|
source: `screen-${currentScreen.screenId}`,
|
||||||
target: `screen-${nextScreen.screenId}`,
|
target: `screen-${nextScreen.screenId}`,
|
||||||
sourceHandle: "right",
|
sourceHandle: "right",
|
||||||
targetHandle: "left",
|
targetHandle: "left",
|
||||||
type: "smoothstep",
|
type: "smoothstep",
|
||||||
label: `${i + 1}`,
|
label: `${i + 1}`,
|
||||||
labelStyle: { fontSize: 11, fill: "#0ea5e9", fontWeight: 600 },
|
labelStyle: { fontSize: 11, fill: "#0ea5e9", fontWeight: 600 },
|
||||||
labelBgStyle: { fill: "white", stroke: "#e2e8f0", strokeWidth: 1 },
|
labelBgStyle: { fill: "white", stroke: "#e2e8f0", strokeWidth: 1 },
|
||||||
labelBgPadding: [4, 2] as [number, number],
|
labelBgPadding: [4, 2] as [number, number],
|
||||||
markerEnd: { type: MarkerType.ArrowClosed, color: "#0ea5e9" },
|
markerEnd: { type: MarkerType.ArrowClosed, color: "#0ea5e9" },
|
||||||
animated: true,
|
animated: true,
|
||||||
style: { stroke: "#0ea5e9", strokeWidth: 2 },
|
style: { stroke: "#0ea5e9", strokeWidth: 2 },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -536,9 +588,9 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
|
||||||
id: `edge-screen-table-${scr.screenId}`,
|
id: `edge-screen-table-${scr.screenId}`,
|
||||||
source: `screen-${scr.screenId}`,
|
source: `screen-${scr.screenId}`,
|
||||||
target: `table-${scr.tableName}`,
|
target: `table-${scr.tableName}`,
|
||||||
sourceHandle: "bottom",
|
sourceHandle: "bottom",
|
||||||
targetHandle: "top",
|
targetHandle: "top",
|
||||||
type: "smoothstep",
|
type: "smoothstep",
|
||||||
animated: true, // 모든 메인 테이블 연결은 애니메이션
|
animated: true, // 모든 메인 테이블 연결은 애니메이션
|
||||||
style: {
|
style: {
|
||||||
stroke: "#3b82f6",
|
stroke: "#3b82f6",
|
||||||
|
|
@ -547,17 +599,111 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 메인 테이블 → 서브 테이블 연결선 생성 (점선 + 애니메이션)
|
// 메인 테이블 → 서브 테이블 연결선 생성 (점선)
|
||||||
|
// 메인 테이블 → 메인 테이블 연결선도 생성 (점선, 연한 주황색)
|
||||||
// 화면별 서브 테이블 연결을 추적하기 위해 screenId 정보도 엣지 ID에 포함
|
// 화면별 서브 테이블 연결을 추적하기 위해 screenId 정보도 엣지 ID에 포함
|
||||||
|
const mainToMainEdgeSet = new Set<string>(); // 중복 방지용
|
||||||
|
|
||||||
Object.entries(subTablesData).forEach(([screenIdStr, screenSubData]) => {
|
Object.entries(subTablesData).forEach(([screenIdStr, screenSubData]) => {
|
||||||
const sourceScreenId = parseInt(screenIdStr);
|
const sourceScreenId = parseInt(screenIdStr);
|
||||||
const mainTable = screenSubData.mainTable;
|
const mainTable = screenSubData.mainTable;
|
||||||
if (!mainTable || !mainTableSet.has(mainTable)) return;
|
if (!mainTable || !mainTableSet.has(mainTable)) return;
|
||||||
|
|
||||||
screenSubData.subTables.forEach((subTable) => {
|
screenSubData.subTables.forEach((subTable) => {
|
||||||
// 서브 테이블 노드가 실제로 생성되었는지 확인
|
const isTargetSubTable = subTableSet.has(subTable.tableName);
|
||||||
if (!subTableSet.has(subTable.tableName)) return;
|
const isTargetMainTable = mainTableSet.has(subTable.tableName);
|
||||||
|
|
||||||
|
// 자기 자신 연결 방지
|
||||||
|
if (mainTable === subTable.tableName) return;
|
||||||
|
|
||||||
|
// 메인 테이블 → 메인 테이블 연결 (서브테이블 구간을 통해 연결)
|
||||||
|
// 규격: bottom → bottom_target (참조하는 테이블에서 참조당하는 테이블로)
|
||||||
|
//
|
||||||
|
// 방향 결정 로직 (범용성):
|
||||||
|
// 1. parentMapping (fieldMappings에 sourceTable 있음): mainTable → sourceTable
|
||||||
|
// - mainTable이 sourceTable을 참조하는 관계
|
||||||
|
// - 예: customer_item_mapping.customer_id → customer_mng.customer_code
|
||||||
|
// 2. rightPanelRelation (foreignKey 있음): subTable → mainTable
|
||||||
|
// - subTable이 mainTable을 참조하는 관계
|
||||||
|
// - 예: customer_item_mapping.customer_id → customer_mng.customer_code
|
||||||
|
// 3. reference (column_labels): mainTable → subTable
|
||||||
|
// - mainTable이 subTable을 참조하는 관계
|
||||||
|
if (isTargetMainTable) {
|
||||||
|
// 실제 참조 방향 결정
|
||||||
|
let referrerTable: string; // 참조하는 테이블 (source)
|
||||||
|
let referencedTable: string; // 참조당하는 테이블 (target)
|
||||||
|
|
||||||
|
// fieldMappings에서 sourceTable 확인
|
||||||
|
const hasSourceTable = subTable.fieldMappings?.some(
|
||||||
|
(fm: any) => fm.sourceTable && fm.sourceTable === subTable.tableName
|
||||||
|
);
|
||||||
|
|
||||||
|
if (subTable.relationType === 'parentMapping' || hasSourceTable) {
|
||||||
|
// parentMapping: mainTable이 sourceTable(=subTable.tableName)을 참조
|
||||||
|
// 방향: mainTable → subTable.tableName
|
||||||
|
referrerTable = mainTable;
|
||||||
|
referencedTable = subTable.tableName;
|
||||||
|
} else if (subTable.relationType === 'rightPanelRelation') {
|
||||||
|
// rightPanelRelation: split-panel-layout의 마스터-디테일 관계
|
||||||
|
// mainTable(leftPanel, 마스터)이 subTable(rightPanel, 디테일)을 필터링
|
||||||
|
// 방향: mainTable(마스터) → subTable(디테일)
|
||||||
|
referrerTable = mainTable;
|
||||||
|
referencedTable = subTable.tableName;
|
||||||
|
} else if (subTable.relationType === 'reference') {
|
||||||
|
// reference (column_labels): mainTable이 subTable을 참조
|
||||||
|
// 방향: mainTable → subTable.tableName
|
||||||
|
referrerTable = mainTable;
|
||||||
|
referencedTable = subTable.tableName;
|
||||||
|
} else {
|
||||||
|
// 기본: subTable이 mainTable을 참조한다고 가정
|
||||||
|
referrerTable = subTable.tableName;
|
||||||
|
referencedTable = mainTable;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 화면별로 고유한 키 생성 (같은 테이블 쌍이라도 다른 화면에서는 별도 엣지)
|
||||||
|
const pairKey = `${sourceScreenId}-${[mainTable, subTable.tableName].sort().join('-')}`;
|
||||||
|
if (!mainToMainEdgeSet.has(pairKey)) {
|
||||||
|
mainToMainEdgeSet.add(pairKey);
|
||||||
|
|
||||||
|
// 관계 유형 추론 및 색상 결정
|
||||||
|
const visualRelationType = inferVisualRelationType(subTable as SubTableInfo);
|
||||||
|
const relationColor = RELATION_COLORS[visualRelationType];
|
||||||
|
|
||||||
|
// 화면별로 고유한 엣지 ID
|
||||||
|
const edgeId = `edge-main-main-${sourceScreenId}-${referrerTable}-${referencedTable}`;
|
||||||
|
newEdges.push({
|
||||||
|
id: edgeId,
|
||||||
|
source: `table-${referrerTable}`, // 참조하는 테이블
|
||||||
|
target: `table-${referencedTable}`, // 참조당하는 테이블
|
||||||
|
sourceHandle: "bottom", // 하단에서 나감 (서브테이블 구간으로)
|
||||||
|
targetHandle: "bottom_target", // 하단으로 들어감
|
||||||
|
type: "smoothstep",
|
||||||
|
animated: false,
|
||||||
|
style: {
|
||||||
|
stroke: relationColor.strokeLight, // 관계 유형별 연한 색상
|
||||||
|
strokeWidth: 1.5,
|
||||||
|
strokeDasharray: "8,4",
|
||||||
|
opacity: 0.5,
|
||||||
|
},
|
||||||
|
markerEnd: {
|
||||||
|
type: MarkerType.ArrowClosed,
|
||||||
|
color: relationColor.strokeLight
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
sourceScreenId,
|
||||||
|
isMainToMain: true,
|
||||||
|
referrerTable,
|
||||||
|
referencedTable,
|
||||||
|
visualRelationType, // 관계 유형 저장
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return; // 메인-메인은 위에서 처리했으므로 스킵
|
||||||
|
}
|
||||||
|
|
||||||
|
// 서브 테이블이 아니면 스킵
|
||||||
|
if (!isTargetSubTable) return;
|
||||||
|
|
||||||
// 화면별로 고유한 엣지 ID (같은 서브 테이블이라도 다른 화면에서 사용하면 별도 엣지)
|
// 화면별로 고유한 엣지 ID (같은 서브 테이블이라도 다른 화면에서 사용하면 별도 엣지)
|
||||||
const edgeId = `edge-main-sub-${sourceScreenId}-${mainTable}-${subTable.tableName}`;
|
const edgeId = `edge-main-sub-${sourceScreenId}-${mainTable}-${subTable.tableName}`;
|
||||||
|
|
@ -578,18 +724,28 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
|
||||||
targetHandle: "top",
|
targetHandle: "top",
|
||||||
type: "smoothstep",
|
type: "smoothstep",
|
||||||
label: relationLabel,
|
label: relationLabel,
|
||||||
labelStyle: { fontSize: 9, fill: "#94a3b8", fontWeight: 500 }, // 기본 흐린 색상
|
labelStyle: {
|
||||||
labelBgStyle: { fill: "white", stroke: "#e2e8f0", strokeWidth: 1 },
|
fontSize: 9,
|
||||||
|
fill: "#94a3b8",
|
||||||
|
fontWeight: 500
|
||||||
|
},
|
||||||
|
labelBgStyle: {
|
||||||
|
fill: "white",
|
||||||
|
stroke: "#e2e8f0",
|
||||||
|
strokeWidth: 1
|
||||||
|
},
|
||||||
labelBgPadding: [3, 2] as [number, number],
|
labelBgPadding: [3, 2] as [number, number],
|
||||||
markerEnd: { type: MarkerType.ArrowClosed, color: "#94a3b8" }, // 기본 흐린 색상
|
markerEnd: {
|
||||||
|
type: MarkerType.ArrowClosed,
|
||||||
|
color: "#94a3b8"
|
||||||
|
},
|
||||||
animated: false, // 기본: 애니메이션 비활성화 (포커스 시에만 활성화)
|
animated: false, // 기본: 애니메이션 비활성화 (포커스 시에만 활성화)
|
||||||
style: {
|
style: {
|
||||||
stroke: "#94a3b8", // 기본 흐린 색상
|
stroke: "#94a3b8",
|
||||||
strokeWidth: 1,
|
strokeWidth: 1,
|
||||||
strokeDasharray: "6,4", // 점선
|
strokeDasharray: "6,4", // 점선
|
||||||
opacity: 0.5, // 기본 투명도
|
opacity: 0.5, // 기본 투명도
|
||||||
},
|
},
|
||||||
// 화면 ID 정보를 data에 저장 (styledEdges에서 활용)
|
|
||||||
data: { sourceScreenId },
|
data: { sourceScreenId },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -769,34 +925,56 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
|
||||||
// 모든 화면의 메인 테이블과 그에 연결된 조인 정보 매핑
|
// 모든 화면의 메인 테이블과 그에 연결된 조인 정보 매핑
|
||||||
// 포커스된 화면이 다른 메인 테이블을 참조하는 경우 해당 테이블도 강조
|
// 포커스된 화면이 다른 메인 테이블을 참조하는 경우 해당 테이블도 강조
|
||||||
const relatedMainTables: Record<string, { columns: string[], displayNames: string[] }> = {};
|
const relatedMainTables: Record<string, { columns: string[], displayNames: string[] }> = {};
|
||||||
|
|
||||||
|
// 모든 화면의 메인 테이블 Set (빠른 조회용)
|
||||||
|
const allMainTableSet = new Set(Object.values(screenTableMap));
|
||||||
|
|
||||||
if (focusedSubTablesData) {
|
if (focusedSubTablesData) {
|
||||||
// screenTableMap에서 다른 화면들의 메인 테이블 확인
|
// 포커스된 화면의 subTables 순회
|
||||||
Object.entries(screenTableMap).forEach(([screenIdStr, mainTableName]) => {
|
focusedSubTablesData.subTables.forEach((subTable) => {
|
||||||
const screenId = parseInt(screenIdStr);
|
// 1. subTable.tableName 자체가 다른 화면의 메인 테이블인 경우
|
||||||
if (screenId === focusedScreenId) return; // 자신의 메인 테이블은 제외
|
if (allMainTableSet.has(subTable.tableName) && subTable.tableName !== focusedSubTablesData.mainTable) {
|
||||||
|
if (!relatedMainTables[subTable.tableName]) {
|
||||||
// 포커스된 화면의 subTables 중 이 메인 테이블을 참조하는지 확인
|
relatedMainTables[subTable.tableName] = { columns: [], displayNames: [] };
|
||||||
focusedSubTablesData.subTables.forEach((subTable) => {
|
}
|
||||||
|
|
||||||
|
// fieldMappings가 있으면 조인 컬럼 정보 추출
|
||||||
if (subTable.fieldMappings) {
|
if (subTable.fieldMappings) {
|
||||||
subTable.fieldMappings.forEach((mapping: any) => {
|
subTable.fieldMappings.forEach((mapping: any) => {
|
||||||
// mapping에 sourceTable 정보가 있는 경우 (parentDataMapping에서 설정)
|
// reference, source 타입: targetField가 서브(연관) 테이블 컬럼
|
||||||
if (mapping.sourceTable && mapping.sourceTable === mainTableName) {
|
// parentMapping 등: sourceField가 연관 테이블 컬럼
|
||||||
if (!relatedMainTables[mainTableName]) {
|
const relatedColumn = mapping.sourceTable
|
||||||
relatedMainTables[mainTableName] = { columns: [], displayNames: [] };
|
? mapping.sourceField // parentMapping 스타일
|
||||||
}
|
: mapping.targetField; // reference/source 스타일
|
||||||
if (mapping.sourceField && !relatedMainTables[mainTableName].columns.includes(mapping.sourceField)) {
|
const displayName = mapping.sourceTable
|
||||||
relatedMainTables[mainTableName].columns.push(mapping.sourceField);
|
? (mapping.sourceDisplayName || mapping.sourceField)
|
||||||
relatedMainTables[mainTableName].displayNames.push(mapping.sourceDisplayName || mapping.sourceField);
|
: (mapping.targetDisplayName || mapping.targetField);
|
||||||
}
|
|
||||||
|
if (relatedColumn && !relatedMainTables[subTable.tableName].columns.includes(relatedColumn)) {
|
||||||
|
relatedMainTables[subTable.tableName].columns.push(relatedColumn);
|
||||||
|
relatedMainTables[subTable.tableName].displayNames.push(displayName);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
|
// 2. fieldMappings.sourceTable이 다른 화면의 메인 테이블인 경우
|
||||||
|
if (subTable.fieldMappings) {
|
||||||
|
subTable.fieldMappings.forEach((mapping: any) => {
|
||||||
|
if (mapping.sourceTable && allMainTableSet.has(mapping.sourceTable) && mapping.sourceTable !== focusedSubTablesData.mainTable) {
|
||||||
|
if (!relatedMainTables[mapping.sourceTable]) {
|
||||||
|
relatedMainTables[mapping.sourceTable] = { columns: [], displayNames: [] };
|
||||||
|
}
|
||||||
|
if (mapping.sourceField && !relatedMainTables[mapping.sourceTable].columns.includes(mapping.sourceField)) {
|
||||||
|
relatedMainTables[mapping.sourceTable].columns.push(mapping.sourceField);
|
||||||
|
relatedMainTables[mapping.sourceTable].displayNames.push(mapping.sourceDisplayName || mapping.sourceField);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[DEBUG] relatedMainTables:', relatedMainTables);
|
|
||||||
|
|
||||||
return nodes.map((node) => {
|
return nodes.map((node) => {
|
||||||
// 화면 노드 스타일링 (포커스가 있을 때만)
|
// 화면 노드 스타일링 (포커스가 있을 때만)
|
||||||
if (node.id.startsWith("screen-")) {
|
if (node.id.startsWith("screen-")) {
|
||||||
|
|
@ -843,102 +1021,25 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
|
||||||
let joinColumns: string[] = [...(focusedUsedColumns?.[`${tableName}__join`] || [])];
|
let joinColumns: string[] = [...(focusedUsedColumns?.[`${tableName}__join`] || [])];
|
||||||
|
|
||||||
// 서브 테이블 연결 정보에서도 추가 (포커스된 화면의 메인 테이블인 경우)
|
// 서브 테이블 연결 정보에서도 추가 (포커스된 화면의 메인 테이블인 경우)
|
||||||
// relationType에 따라 다름:
|
|
||||||
// - reference, source: sourceField가 메인테이블 컬럼 (예: manager_id -> user_id, material -> material)
|
|
||||||
// - parentMapping, rightPanelRelation: targetField가 메인테이블 컬럼
|
|
||||||
// - lookup 등: targetField가 메인테이블 컬럼
|
|
||||||
console.log('[DEBUG] joinColumns before subTable processing:', {
|
|
||||||
tableName,
|
|
||||||
focusedMainTable: focusedSubTablesData?.mainTable,
|
|
||||||
subTables: focusedSubTablesData?.subTables?.map((st: any) => ({
|
|
||||||
tableName: st.tableName,
|
|
||||||
relationType: st.relationType,
|
|
||||||
fieldMappings: st.fieldMappings
|
|
||||||
}))
|
|
||||||
});
|
|
||||||
if (focusedSubTablesData && focusedSubTablesData.mainTable === tableName) {
|
if (focusedSubTablesData && focusedSubTablesData.mainTable === tableName) {
|
||||||
// 디버그: subTables 처리 전 로그
|
focusedSubTablesData.subTables.forEach((subTable) => {
|
||||||
if (tableName === 'customer_item_mapping') {
|
|
||||||
console.log('[DEBUG] Processing subTables for mainTable:', {
|
|
||||||
mainTable: tableName,
|
|
||||||
subTablesCount: focusedSubTablesData.subTables.length,
|
|
||||||
subTables: focusedSubTablesData.subTables.map(st => ({
|
|
||||||
tableName: st.tableName,
|
|
||||||
relationType: st.relationType,
|
|
||||||
fieldMappingsCount: st.fieldMappings?.length || 0,
|
|
||||||
fieldMappings: st.fieldMappings?.map(fm => ({
|
|
||||||
sourceField: fm.sourceField,
|
|
||||||
targetField: fm.targetField,
|
|
||||||
})),
|
|
||||||
})),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
focusedSubTablesData.subTables.forEach((subTable, stIdx) => {
|
|
||||||
// 각 서브테이블 디버그 로그
|
|
||||||
if (tableName === 'customer_item_mapping') {
|
|
||||||
console.log(`[DEBUG] SubTable ${stIdx}:`, {
|
|
||||||
tableName: subTable.tableName,
|
|
||||||
relationType: subTable.relationType,
|
|
||||||
hasFieldMappings: !!subTable.fieldMappings,
|
|
||||||
fieldMappingsCount: subTable.fieldMappings?.length || 0,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (subTable.fieldMappings) {
|
if (subTable.fieldMappings) {
|
||||||
subTable.fieldMappings.forEach((mapping, mIdx) => {
|
subTable.fieldMappings.forEach((mapping) => {
|
||||||
// 각 매핑 디버그 로그
|
|
||||||
if (tableName === 'customer_item_mapping') {
|
|
||||||
console.log(`[DEBUG] SubTable ${stIdx} Mapping ${mIdx}:`, {
|
|
||||||
sourceField: mapping.sourceField,
|
|
||||||
targetField: mapping.targetField,
|
|
||||||
relationType: subTable.relationType,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// sourceTable이 있으면 parentDataMapping/rightPanelRelation에서 추가된 것이므로
|
|
||||||
// relationType과 관계없이 targetField가 메인테이블 컬럼
|
|
||||||
const hasSourceTable = 'sourceTable' in mapping && mapping.sourceTable;
|
const hasSourceTable = 'sourceTable' in mapping && mapping.sourceTable;
|
||||||
|
|
||||||
if (hasSourceTable) {
|
if (hasSourceTable) {
|
||||||
// parentDataMapping/rightPanelRelation: targetField가 메인테이블 컬럼
|
|
||||||
if (tableName === 'customer_item_mapping') {
|
|
||||||
console.log('[DEBUG] Adding targetField to joinColumns (has sourceTable):', {
|
|
||||||
subTableName: subTable.tableName,
|
|
||||||
relationType: subTable.relationType,
|
|
||||||
sourceTable: mapping.sourceTable,
|
|
||||||
targetField: mapping.targetField,
|
|
||||||
alreadyIncludes: joinColumns.includes(mapping.targetField),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (mapping.targetField && !joinColumns.includes(mapping.targetField)) {
|
if (mapping.targetField && !joinColumns.includes(mapping.targetField)) {
|
||||||
joinColumns.push(mapping.targetField);
|
joinColumns.push(mapping.targetField);
|
||||||
}
|
}
|
||||||
} else if (subTable.relationType === 'reference' || subTable.relationType === 'source') {
|
} else if (subTable.relationType === 'reference' || subTable.relationType === 'source') {
|
||||||
// reference, source (sourceTable 없는 경우): sourceField가 메인테이블 컬럼
|
|
||||||
if (mapping.sourceField && !joinColumns.includes(mapping.sourceField)) {
|
if (mapping.sourceField && !joinColumns.includes(mapping.sourceField)) {
|
||||||
joinColumns.push(mapping.sourceField);
|
joinColumns.push(mapping.sourceField);
|
||||||
}
|
}
|
||||||
} else if (subTable.relationType === 'parentMapping' || subTable.relationType === 'rightPanelRelation') {
|
} else if (subTable.relationType === 'parentMapping' || subTable.relationType === 'rightPanelRelation') {
|
||||||
// parentMapping, rightPanelRelation: targetField가 메인테이블 컬럼
|
|
||||||
if (tableName === 'customer_item_mapping') {
|
|
||||||
console.log('[DEBUG] Adding targetField to joinColumns (parentMapping):', {
|
|
||||||
subTableName: subTable.tableName,
|
|
||||||
relationType: subTable.relationType,
|
|
||||||
targetField: mapping.targetField,
|
|
||||||
alreadyIncludes: joinColumns.includes(mapping.targetField),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (mapping.targetField && !joinColumns.includes(mapping.targetField)) {
|
if (mapping.targetField && !joinColumns.includes(mapping.targetField)) {
|
||||||
joinColumns.push(mapping.targetField);
|
joinColumns.push(mapping.targetField);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// lookup 등: targetField가 메인테이블 컬럼
|
|
||||||
if (tableName === 'customer_item_mapping') {
|
|
||||||
console.log('[DEBUG] Adding targetField to joinColumns (else branch):', {
|
|
||||||
subTableName: subTable.tableName,
|
|
||||||
relationType: subTable.relationType,
|
|
||||||
targetField: mapping.targetField,
|
|
||||||
alreadyIncludes: joinColumns.includes(mapping.targetField),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (mapping.targetField && !joinColumns.includes(mapping.targetField)) {
|
if (mapping.targetField && !joinColumns.includes(mapping.targetField)) {
|
||||||
joinColumns.push(mapping.targetField);
|
joinColumns.push(mapping.targetField);
|
||||||
}
|
}
|
||||||
|
|
@ -1014,8 +1115,33 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
|
||||||
|
|
||||||
// 연관 테이블용 fieldMappings 생성
|
// 연관 테이블용 fieldMappings 생성
|
||||||
let relatedTableFieldMappings: Array<{ sourceField: string; targetField: string; sourceDisplayName?: string; targetDisplayName?: string }> = [];
|
let relatedTableFieldMappings: Array<{ sourceField: string; targetField: string; sourceDisplayName?: string; targetDisplayName?: string }> = [];
|
||||||
if (isRelatedTable && relatedTableInfo && focusedSubTablesData) {
|
if (isRelatedTable && focusedSubTablesData) {
|
||||||
focusedSubTablesData.subTables.forEach((subTable) => {
|
focusedSubTablesData.subTables.forEach((subTable) => {
|
||||||
|
// 1. subTable.tableName === tableName인 경우 (메인-메인 조인)
|
||||||
|
if (subTable.tableName === tableName && subTable.fieldMappings) {
|
||||||
|
subTable.fieldMappings.forEach((mapping) => {
|
||||||
|
// reference/source 타입: sourceField가 메인테이블, targetField가 이 연관테이블
|
||||||
|
// 연관 테이블 표시: targetField ← sourceDisplayName (메인테이블 컬럼 한글명)
|
||||||
|
if (subTable.relationType === 'reference' || subTable.relationType === 'source') {
|
||||||
|
relatedTableFieldMappings.push({
|
||||||
|
sourceField: mapping.sourceField, // 메인 테이블 컬럼 (참조 표시용)
|
||||||
|
targetField: mapping.targetField, // 연관 테이블 컬럼 (표시할 컬럼)
|
||||||
|
sourceDisplayName: mapping.sourceDisplayName || mapping.sourceField,
|
||||||
|
targetDisplayName: mapping.targetDisplayName || mapping.targetField,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// lookup, parentMapping 등: targetField가 메인테이블, sourceField가 연관테이블
|
||||||
|
relatedTableFieldMappings.push({
|
||||||
|
sourceField: mapping.targetField, // 메인 테이블 컬럼 (참조 표시용)
|
||||||
|
targetField: mapping.sourceField, // 연관 테이블 컬럼 (표시할 컬럼)
|
||||||
|
sourceDisplayName: mapping.targetDisplayName || mapping.targetField,
|
||||||
|
targetDisplayName: mapping.sourceDisplayName || mapping.sourceField,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. mapping.sourceTable === tableName인 경우 (parentDataMapping 등)
|
||||||
if (subTable.fieldMappings) {
|
if (subTable.fieldMappings) {
|
||||||
subTable.fieldMappings.forEach((mapping) => {
|
subTable.fieldMappings.forEach((mapping) => {
|
||||||
if (mapping.sourceTable === tableName) {
|
if (mapping.sourceTable === tableName) {
|
||||||
|
|
@ -1030,6 +1156,39 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 중복 제거
|
||||||
|
const seen = new Set<string>();
|
||||||
|
relatedTableFieldMappings = relatedTableFieldMappings.filter(fm => {
|
||||||
|
const key = `${fm.sourceField}-${fm.targetField}`;
|
||||||
|
if (seen.has(key)) return false;
|
||||||
|
seen.add(key);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 포커스된 화면에서 이 테이블이 필터링 대상인지 확인
|
||||||
|
// (rightPanelRelation의 서브테이블인 경우)
|
||||||
|
let focusedFilterColumns: string[] = [];
|
||||||
|
let focusedReferencedBy: ReferenceInfo[] = [];
|
||||||
|
|
||||||
|
if (focusedScreenId !== null && focusedSubTablesData) {
|
||||||
|
// 포커스된 화면에서 이 테이블이 rightPanelRelation의 서브테이블인 경우
|
||||||
|
focusedSubTablesData.subTables.forEach((subTable) => {
|
||||||
|
if (subTable.tableName === tableName && subTable.relationType === 'rightPanelRelation') {
|
||||||
|
// FK 컬럼 추출
|
||||||
|
if (subTable.foreignKey) {
|
||||||
|
focusedFilterColumns.push(subTable.foreignKey);
|
||||||
|
}
|
||||||
|
// 참조 정보 생성
|
||||||
|
focusedReferencedBy.push({
|
||||||
|
fromTable: focusedSubTablesData.mainTable,
|
||||||
|
fromColumn: subTable.leftColumn || 'id',
|
||||||
|
toColumn: subTable.foreignKey || '',
|
||||||
|
relationType: 'filter',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -1041,6 +1200,8 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
|
||||||
isFaded: focusedScreenId !== null && !isActiveTable,
|
isFaded: focusedScreenId !== null && !isActiveTable,
|
||||||
highlightedColumns: isActiveTable ? highlightedColumns : [],
|
highlightedColumns: isActiveTable ? highlightedColumns : [],
|
||||||
joinColumns: isActiveTable ? joinColumns : [],
|
joinColumns: isActiveTable ? joinColumns : [],
|
||||||
|
filterColumns: focusedFilterColumns, // 포커스 상태에서만 표시
|
||||||
|
referencedBy: focusedReferencedBy.length > 0 ? focusedReferencedBy : undefined, // 포커스 상태에서만 표시
|
||||||
fieldMappings: isFocusedTable ? mainTableFieldMappings : (isRelatedTable ? relatedTableFieldMappings : []),
|
fieldMappings: isFocusedTable ? mainTableFieldMappings : (isRelatedTable ? relatedTableFieldMappings : []),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -1195,48 +1356,79 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
|
||||||
// 그룹 모드가 아니면 원본 반환
|
// 그룹 모드가 아니면 원본 반환
|
||||||
if (!selectedGroup) return edges;
|
if (!selectedGroup) return edges;
|
||||||
|
|
||||||
// 연관 테이블 간 조인 엣지 생성 (parentDataMapping, rightPanelRelation)
|
// 연관 테이블 간 조인 엣지 생성 (메인 테이블 간 조인 관계)
|
||||||
const joinEdges: Edge[] = [];
|
const joinEdges: Edge[] = [];
|
||||||
|
|
||||||
if (focusedScreenId !== null) {
|
if (focusedScreenId !== null) {
|
||||||
const focusedSubTablesData = subTablesDataMap[focusedScreenId];
|
const focusedSubTablesData = subTablesDataMap[focusedScreenId];
|
||||||
const focusedMainTable = screenTableMap[focusedScreenId];
|
const focusedMainTable = screenTableMap[focusedScreenId];
|
||||||
|
|
||||||
|
// 모든 화면의 메인 테이블 목록 (메인-메인 조인 판단용)
|
||||||
|
const allMainTables = new Set(Object.values(screenTableMap));
|
||||||
|
|
||||||
|
// 이미 추가된 테이블 쌍 추적 (중복 방지)
|
||||||
|
const addedPairs = new Set<string>();
|
||||||
|
|
||||||
if (focusedSubTablesData) {
|
if (focusedSubTablesData) {
|
||||||
focusedSubTablesData.subTables.forEach((subTable) => {
|
focusedSubTablesData.subTables.forEach((subTable) => {
|
||||||
// fieldMappings에 sourceTable이 있는 경우 처리 (parentMapping, rightPanelRelation 등)
|
// 1. subTable.tableName이 다른 화면의 메인 테이블인 경우 (메인-메인 조인)
|
||||||
if (subTable.fieldMappings) {
|
const isTargetMainTable = allMainTables.has(subTable.tableName) && subTable.tableName !== focusedMainTable;
|
||||||
|
|
||||||
|
if (isTargetMainTable) {
|
||||||
|
const pairKey = `${subTable.tableName}-${focusedMainTable}`;
|
||||||
|
if (addedPairs.has(pairKey)) return;
|
||||||
|
addedPairs.add(pairKey);
|
||||||
|
|
||||||
subTable.fieldMappings.forEach((mapping: any, idx: number) => {
|
// 메인 테이블 간 조인 연결선 - edge-main-main 스타일 업데이트만 수행
|
||||||
|
// 별도의 edge-main-join을 생성하지 않고, styledEdges에서 edge-main-main을 강조 처리
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. fieldMappings.sourceTable이 있는 경우 (parentMapping, rightPanelRelation 등)
|
||||||
|
if (subTable.fieldMappings) {
|
||||||
|
subTable.fieldMappings.forEach((mapping: any) => {
|
||||||
const sourceTable = mapping.sourceTable;
|
const sourceTable = mapping.sourceTable;
|
||||||
if (!sourceTable) return;
|
if (!sourceTable) return;
|
||||||
|
|
||||||
// 연관 테이블 → 포커싱된 화면의 메인 테이블로 연결
|
// sourceTable이 메인 테이블인 경우만 메인-메인 조인선 추가
|
||||||
// sourceTable(연관) → focusedMainTable(메인)
|
if (!allMainTables.has(sourceTable) || sourceTable === focusedMainTable) return;
|
||||||
const edgeId = `edge-join-relation-${focusedScreenId}-${sourceTable}-${focusedMainTable}-${idx}`;
|
|
||||||
|
const pairKey = `${sourceTable}-${focusedMainTable}`;
|
||||||
|
if (addedPairs.has(pairKey)) return;
|
||||||
|
addedPairs.add(pairKey);
|
||||||
|
|
||||||
|
const edgeId = `edge-join-relation-${focusedScreenId}-${sourceTable}-${focusedMainTable}`;
|
||||||
|
const sourceNodeId = `table-${sourceTable}`;
|
||||||
|
const targetNodeId = `table-${focusedMainTable}`;
|
||||||
|
|
||||||
// 이미 존재하는 엣지인지 확인
|
// 이미 존재하는 엣지인지 확인
|
||||||
if (joinEdges.some(e => e.id === edgeId)) return;
|
if (joinEdges.some(e => e.id === edgeId)) return;
|
||||||
|
|
||||||
// 라벨 제거 - 조인 정보는 테이블 노드 내부에서 컬럼 옆에 표시
|
// 관계 유형 추론 및 색상 결정
|
||||||
|
const visualRelationType = inferVisualRelationType(subTable as SubTableInfo);
|
||||||
|
const relationColor = RELATION_COLORS[visualRelationType];
|
||||||
|
|
||||||
joinEdges.push({
|
joinEdges.push({
|
||||||
id: edgeId,
|
id: edgeId,
|
||||||
source: `table-${sourceTable}`,
|
source: sourceNodeId,
|
||||||
target: `table-${focusedMainTable}`,
|
target: targetNodeId,
|
||||||
|
sourceHandle: 'bottom', // 고정: 서브테이블 구간 통과
|
||||||
|
targetHandle: 'bottom_target', // 고정: 서브테이블 구간 통과
|
||||||
type: 'smoothstep',
|
type: 'smoothstep',
|
||||||
animated: true,
|
animated: true,
|
||||||
style: {
|
style: {
|
||||||
stroke: '#ea580c',
|
stroke: relationColor.stroke, // 관계 유형별 색상
|
||||||
strokeWidth: 2,
|
strokeWidth: 2,
|
||||||
strokeDasharray: '8,4',
|
strokeDasharray: '8,4',
|
||||||
},
|
},
|
||||||
markerEnd: {
|
markerEnd: {
|
||||||
type: MarkerType.ArrowClosed,
|
type: MarkerType.ArrowClosed,
|
||||||
color: '#ea580c',
|
color: relationColor.stroke,
|
||||||
width: 15,
|
width: 15,
|
||||||
height: 15,
|
height: 15,
|
||||||
},
|
},
|
||||||
|
data: {
|
||||||
|
visualRelationType,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -1294,12 +1486,14 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
|
||||||
}
|
}
|
||||||
|
|
||||||
// 메인 테이블 → 서브 테이블 연결선
|
// 메인 테이블 → 서브 테이블 연결선
|
||||||
// 기본: 흐리게 처리, 포커스된 화면의 서브 테이블만 강조
|
// 규격: bottom → top 고정 (아래로 문어발처럼 뻗어나감)
|
||||||
if (edge.source.startsWith("table-") && edge.target.startsWith("subtable-")) {
|
if (edge.source.startsWith("table-") && edge.target.startsWith("subtable-")) {
|
||||||
// 포커스가 없으면 모든 서브 테이블 연결선 흐리게 (기본 상태)
|
// 포커스가 없으면 모든 서브 테이블 연결선 흐리게 (기본 상태)
|
||||||
if (focusedScreenId === null) {
|
if (focusedScreenId === null) {
|
||||||
return {
|
return {
|
||||||
...edge,
|
...edge,
|
||||||
|
sourceHandle: "bottom", // 고정: 메인 테이블 하단에서 나감
|
||||||
|
targetHandle: "top", // 고정: 서브 테이블 상단으로 들어감
|
||||||
animated: false,
|
animated: false,
|
||||||
style: {
|
style: {
|
||||||
...edge.style,
|
...edge.style,
|
||||||
|
|
@ -1332,6 +1526,8 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...edge,
|
...edge,
|
||||||
|
sourceHandle: "bottom", // 고정
|
||||||
|
targetHandle: "top", // 고정
|
||||||
animated: isActive, // 활성화된 것만 애니메이션
|
animated: isActive, // 활성화된 것만 애니메이션
|
||||||
style: {
|
style: {
|
||||||
...edge.style,
|
...edge.style,
|
||||||
|
|
@ -1347,12 +1543,69 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 메인 테이블 → 메인 테이블 연결선 (서브테이블 구간 통과)
|
||||||
|
// 규격: bottom → bottom_target 고정 (아래쪽 서브테이블 선 구간을 통해 연결)
|
||||||
|
if (edge.source.startsWith("table-") && edge.target.startsWith("table-") && edge.id.startsWith("edge-main-main-")) {
|
||||||
|
// 관계 유형별 색상 결정
|
||||||
|
const visualRelationType = (edge.data as any)?.visualRelationType as VisualRelationType || 'join';
|
||||||
|
const relationColor = RELATION_COLORS[visualRelationType];
|
||||||
|
|
||||||
|
if (focusedScreenId === null) {
|
||||||
|
// 포커스 없으면 관계 유형별 연한 색상으로 표시
|
||||||
|
return {
|
||||||
|
...edge,
|
||||||
|
sourceHandle: "bottom",
|
||||||
|
targetHandle: "bottom_target",
|
||||||
|
animated: false,
|
||||||
|
style: {
|
||||||
|
...edge.style,
|
||||||
|
stroke: relationColor.strokeLight,
|
||||||
|
strokeWidth: 1.5,
|
||||||
|
strokeDasharray: "8,4",
|
||||||
|
opacity: 0.4,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 포커스된 화면에서 생성된 연결선만 표시
|
||||||
|
const edgeSourceScreenId = (edge.data as any)?.sourceScreenId;
|
||||||
|
const isMyConnection = edgeSourceScreenId === focusedScreenId;
|
||||||
|
|
||||||
|
// 포커스된 화면과 관련 없는 메인-메인 엣지는 숨김
|
||||||
|
if (!isMyConnection) {
|
||||||
|
return {
|
||||||
|
...edge,
|
||||||
|
hidden: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...edge,
|
||||||
|
sourceHandle: "bottom", // 고정: 서브테이블 구간 통과
|
||||||
|
targetHandle: "bottom_target", // 고정: 서브테이블 구간 통과
|
||||||
|
animated: true,
|
||||||
|
style: {
|
||||||
|
...edge.style,
|
||||||
|
stroke: relationColor.stroke, // 관계 유형별 진한 색상
|
||||||
|
strokeWidth: 2.5,
|
||||||
|
strokeDasharray: "8,4",
|
||||||
|
opacity: 1,
|
||||||
|
},
|
||||||
|
markerEnd: {
|
||||||
|
type: MarkerType.ArrowClosed,
|
||||||
|
color: relationColor.stroke,
|
||||||
|
width: 15,
|
||||||
|
height: 15,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return edge;
|
return edge;
|
||||||
});
|
});
|
||||||
|
|
||||||
// 기존 엣지 + 조인 관계 엣지 합치기
|
// 기존 엣지 + 조인 관계 엣지 합치기
|
||||||
return [...styledOriginalEdges, ...joinEdges];
|
return [...styledOriginalEdges, ...joinEdges];
|
||||||
}, [edges, selectedGroup, focusedScreenId, screen, screenSubTableMap, subTablesDataMap, screenTableMap]);
|
}, [edges, nodes, selectedGroup, focusedScreenId, screen, screenSubTableMap, subTablesDataMap, screenTableMap]);
|
||||||
|
|
||||||
// 조건부 렌더링 (모든 훅 선언 후에 위치해야 함)
|
// 조건부 렌더링 (모든 훅 선언 후에 위치해야 함)
|
||||||
if (!screen && !selectedGroup) {
|
if (!screen && !selectedGroup) {
|
||||||
|
|
@ -1378,20 +1631,20 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
|
||||||
<div className="h-full w-full">
|
<div className="h-full w-full">
|
||||||
{/* isViewReady가 false면 숨김 처리하여 깜빡임 방지 */}
|
{/* isViewReady가 false면 숨김 처리하여 깜빡임 방지 */}
|
||||||
<div className={`h-full w-full transition-opacity duration-0 ${isViewReady ? "opacity-100" : "opacity-0"}`}>
|
<div className={`h-full w-full transition-opacity duration-0 ${isViewReady ? "opacity-100" : "opacity-0"}`}>
|
||||||
<ReactFlow
|
<ReactFlow
|
||||||
nodes={styledNodes}
|
nodes={styledNodes}
|
||||||
edges={styledEdges}
|
edges={styledEdges}
|
||||||
onNodesChange={onNodesChange}
|
onNodesChange={onNodesChange}
|
||||||
onEdgesChange={onEdgesChange}
|
onEdgesChange={onEdgesChange}
|
||||||
onNodeClick={handleNodeClick}
|
onNodeClick={handleNodeClick}
|
||||||
nodeTypes={nodeTypes}
|
nodeTypes={nodeTypes}
|
||||||
minZoom={0.3}
|
minZoom={0.3}
|
||||||
maxZoom={1.5}
|
maxZoom={1.5}
|
||||||
proOptions={{ hideAttribution: true }}
|
proOptions={{ hideAttribution: true }}
|
||||||
>
|
>
|
||||||
<Background variant={BackgroundVariant.Dots} gap={20} size={1} color="#e2e8f0" />
|
<Background variant={BackgroundVariant.Dots} gap={20} size={1} color="#e2e8f0" />
|
||||||
<Controls position="bottom-right" />
|
<Controls position="bottom-right" />
|
||||||
</ReactFlow>
|
</ReactFlow>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -456,3 +456,5 @@ export default function DataFlowPanel({ groupId, screenId, screens = [] }: DataF
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -408,3 +408,5 @@ export default function FieldJoinPanel({ screenId, componentId, layoutId }: Fiel
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -406,6 +406,44 @@ export interface SubTableInfo {
|
||||||
componentType: string;
|
componentType: string;
|
||||||
relationType: 'lookup' | 'source' | 'join' | 'reference' | 'parentMapping' | 'rightPanelRelation';
|
relationType: 'lookup' | 'source' | 'join' | 'reference' | 'parentMapping' | 'rightPanelRelation';
|
||||||
fieldMappings?: FieldMappingInfo[];
|
fieldMappings?: FieldMappingInfo[];
|
||||||
|
// rightPanelRelation에서 추가 정보 (관계 유형 추론용)
|
||||||
|
originalRelationType?: 'join' | 'detail'; // 원본 relation.type
|
||||||
|
foreignKey?: string; // 디테일 테이블의 FK 컬럼
|
||||||
|
leftColumn?: string; // 마스터 테이블의 선택 기준 컬럼
|
||||||
|
}
|
||||||
|
|
||||||
|
// 시각적 관계 유형 (시각화에서 사용)
|
||||||
|
export type VisualRelationType = 'filter' | 'hierarchy' | 'lookup' | 'mapping' | 'join';
|
||||||
|
|
||||||
|
// 관계 유형 추론 함수
|
||||||
|
export function inferVisualRelationType(subTable: SubTableInfo): VisualRelationType {
|
||||||
|
// 1. split-panel-layout의 rightPanel.relation
|
||||||
|
if (subTable.relationType === 'rightPanelRelation') {
|
||||||
|
// 원본 relation.type 기반 구분
|
||||||
|
if (subTable.originalRelationType === 'detail') {
|
||||||
|
return 'hierarchy'; // 부모-자식 계층 구조 (같은 테이블 자기 참조)
|
||||||
|
}
|
||||||
|
return 'filter'; // 마스터-디테일 필터링
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. selected-items-detail-input의 parentDataMapping
|
||||||
|
// parentDataMapping은 FK 관계를 정의하므로 조인으로 분류
|
||||||
|
if (subTable.relationType === 'parentMapping') {
|
||||||
|
return 'join'; // FK 조인 (sourceTable.sourceField → targetTable.targetField)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. column_labels.reference_table
|
||||||
|
if (subTable.relationType === 'reference') {
|
||||||
|
return 'join'; // 실제 엔티티 조인 (LEFT JOIN 등)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. autocomplete, entity-search
|
||||||
|
if (subTable.relationType === 'lookup') {
|
||||||
|
return 'lookup'; // 코드→명칭 변환
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 기타 (source, join 등)
|
||||||
|
return 'join';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ScreenSubTablesData {
|
export interface ScreenSubTablesData {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue