feat: 화면 관리 기능 개선 및 서브 테이블 정보 추가
- 화면 선택 시 그룹을 재설정하지 않도록 로직 개선하여 데이터 재로드 방지 - 테이블 노드 데이터 구조에 필드 매핑 정보 추가 - 서브 테이블과 조인 관계를 시각화하기 위한 컬럼 강조 및 스타일링 개선 - 화면 관계 흐름에서 서브 테이블 연결선 강조 기능 추가 - 사용 컬럼 및 조인 컬럼 정보를 화면별로 매핑하여 관리
This commit is contained in:
parent
48e9840fa0
commit
b279f8d58d
|
|
@ -160,8 +160,11 @@ export default function ScreenManagementPage() {
|
||||||
setFocusedScreenIdInGroup(null); // 포커스 초기화
|
setFocusedScreenIdInGroup(null); // 포커스 초기화
|
||||||
}}
|
}}
|
||||||
onScreenSelectInGroup={(group, screenId) => {
|
onScreenSelectInGroup={(group, screenId) => {
|
||||||
// 그룹 내 화면 클릭 시: 그룹 선택 + 해당 화면 포커스
|
// 그룹 내 화면 클릭 시: 해당 화면 포커스
|
||||||
setSelectedGroup(group);
|
// 이미 같은 그룹이 선택된 상태라면 그룹을 다시 설정하지 않음 (데이터 재로드 방지)
|
||||||
|
if (selectedGroup?.id !== group.id) {
|
||||||
|
setSelectedGroup(group);
|
||||||
|
}
|
||||||
setSelectedScreen(null);
|
setSelectedScreen(null);
|
||||||
setFocusedScreenIdInGroup(screenId);
|
setFocusedScreenIdInGroup(screenId);
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -33,17 +33,33 @@ export interface ScreenNodeData {
|
||||||
screenRole?: string; // 화면 역할 (메인그리드, 등록폼 등)
|
screenRole?: string; // 화면 역할 (메인그리드, 등록폼 등)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 필드 매핑 정보 (조인 관계 표시용)
|
||||||
|
export interface FieldMappingDisplay {
|
||||||
|
sourceField: string; // 메인 테이블 컬럼 (예: manager_id)
|
||||||
|
targetField: string; // 서브 테이블 컬럼 (예: user_id)
|
||||||
|
sourceDisplayName?: string; // 메인 테이블 한글 컬럼명 (예: 담당자)
|
||||||
|
targetDisplayName?: string; // 서브 테이블 한글 컬럼명 (예: 사용자ID)
|
||||||
|
}
|
||||||
|
|
||||||
// 테이블 노드 데이터 인터페이스
|
// 테이블 노드 데이터 인터페이스
|
||||||
export interface TableNodeData {
|
export interface TableNodeData {
|
||||||
label: string;
|
label: string;
|
||||||
subLabel?: string;
|
subLabel?: string;
|
||||||
isMain?: boolean;
|
isMain?: boolean;
|
||||||
|
isFocused?: boolean; // 포커스된 테이블인지
|
||||||
|
isFaded?: boolean; // 흑백 처리할지
|
||||||
columns?: Array<{
|
columns?: Array<{
|
||||||
name: string;
|
name: string; // 표시용 이름 (한글명)
|
||||||
|
originalName?: string; // 원본 컬럼명 (영문, 필터링용)
|
||||||
type: string;
|
type: string;
|
||||||
isPrimaryKey?: boolean;
|
isPrimaryKey?: boolean;
|
||||||
isForeignKey?: boolean;
|
isForeignKey?: boolean;
|
||||||
}>;
|
}>;
|
||||||
|
// 포커스 시 강조할 컬럼 정보
|
||||||
|
highlightedColumns?: string[]; // 화면에서 사용하는 컬럼 (영문명)
|
||||||
|
joinColumns?: string[]; // 조인에 사용되는 컬럼
|
||||||
|
// 필드 매핑 정보 (조인 관계 표시용)
|
||||||
|
fieldMappings?: FieldMappingDisplay[]; // 서브 테이블일 때 조인 관계 표시
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== 유틸리티 함수 ==========
|
// ========== 유틸리티 함수 ==========
|
||||||
|
|
@ -404,13 +420,60 @@ 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, columns } = data;
|
const { label, subLabel, isMain, isFocused, isFaded, columns, highlightedColumns, joinColumns, fieldMappings } = data;
|
||||||
// 최대 5개 컬럼만 표시
|
|
||||||
const displayColumns = columns?.slice(0, 5) || [];
|
// 강조할 컬럼 세트 (영문 컬럼명 기준)
|
||||||
const remainingCount = (columns?.length || 0) - 5;
|
const highlightSet = new Set(highlightedColumns || []);
|
||||||
|
const joinSet = new Set(joinColumns || []);
|
||||||
|
|
||||||
|
// 필드 매핑 맵 생성 (targetField → { sourceField, sourceDisplayName })
|
||||||
|
// 서브 테이블에서 targetField가 어떤 메인 테이블 컬럼(sourceField)과 연결되는지
|
||||||
|
const fieldMappingMap = new Map<string, { sourceField: string; sourceDisplayName: string }>();
|
||||||
|
if (fieldMappings) {
|
||||||
|
fieldMappings.forEach(mapping => {
|
||||||
|
fieldMappingMap.set(mapping.targetField, {
|
||||||
|
sourceField: mapping.sourceField,
|
||||||
|
// 한글명이 있으면 한글명, 없으면 영문명 사용
|
||||||
|
sourceDisplayName: mapping.sourceDisplayName || mapping.sourceField,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 포커스 모드: 사용 컬럼만 필터링하여 표시
|
||||||
|
// originalName (영문) 또는 name으로 매칭 시도
|
||||||
|
const potentialFilteredColumns = columns?.filter(col => {
|
||||||
|
const colOriginal = col.originalName || col.name;
|
||||||
|
return highlightSet.has(colOriginal) || joinSet.has(colOriginal);
|
||||||
|
}) || [];
|
||||||
|
const hasActiveColumns = potentialFilteredColumns.length > 0;
|
||||||
|
|
||||||
|
// 표시할 컬럼:
|
||||||
|
// - 포커스 시 (활성 컬럼 있음): 필터된 컬럼만 표시
|
||||||
|
// - 비포커스 시: 최대 8개만 표시
|
||||||
|
const MAX_DEFAULT_COLUMNS = 8;
|
||||||
|
const allColumns = columns || [];
|
||||||
|
const displayColumns = hasActiveColumns
|
||||||
|
? potentialFilteredColumns
|
||||||
|
: allColumns.slice(0, MAX_DEFAULT_COLUMNS);
|
||||||
|
const remainingCount = hasActiveColumns
|
||||||
|
? 0
|
||||||
|
: Math.max(0, allColumns.length - MAX_DEFAULT_COLUMNS);
|
||||||
|
const totalCount = allColumns.length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="group relative flex w-[260px] flex-col overflow-hidden rounded-lg border border-border bg-card shadow-md transition-all hover:shadow-lg hover:ring-2 hover:ring-emerald-500/20">
|
<div
|
||||||
|
className={`group relative flex w-[260px] flex-col overflow-hidden rounded-lg border bg-card shadow-md transition-all duration-300 ${
|
||||||
|
isFocused
|
||||||
|
? "border-2 border-emerald-500 ring-4 ring-emerald-500/30 shadow-xl"
|
||||||
|
: isFaded
|
||||||
|
? "border-gray-200 opacity-60"
|
||||||
|
: "border-border hover:shadow-lg hover:ring-2 hover:ring-emerald-500/20"
|
||||||
|
}`}
|
||||||
|
style={{
|
||||||
|
filter: isFaded ? "grayscale(80%)" : "none",
|
||||||
|
transition: "all 0.3s ease",
|
||||||
|
}}
|
||||||
|
>
|
||||||
{/* Handles */}
|
{/* Handles */}
|
||||||
<Handle
|
<Handle
|
||||||
type="target"
|
type="target"
|
||||||
|
|
@ -438,40 +501,86 @@ export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 헤더 (초록색, 컴팩트) */}
|
{/* 헤더 (초록색, 컴팩트) */}
|
||||||
<div className={`flex items-center gap-2 px-3 py-1.5 text-white ${isMain ? "bg-emerald-600" : "bg-slate-500"}`}>
|
<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"
|
||||||
|
}`}>
|
||||||
<Database className="h-3.5 w-3.5" />
|
<Database className="h-3.5 w-3.5" />
|
||||||
<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 && (
|
||||||
|
<span className="rounded-full bg-white/20 px-1.5 py-0.5 text-[8px]">
|
||||||
|
{displayColumns.length}개 활성
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 컬럼 목록 (컴팩트) */}
|
{/* 컬럼 목록 - 컴팩트하게 (스크롤 가능) */}
|
||||||
<div className="p-1.5">
|
<div className="p-1.5 transition-all duration-300 max-h-[150px] overflow-y-auto">
|
||||||
{displayColumns.length > 0 ? (
|
{displayColumns.length > 0 ? (
|
||||||
<div className="flex flex-col gap-px">
|
<div className="flex flex-col gap-px">
|
||||||
{displayColumns.map((col, idx) => (
|
{displayColumns.map((col, idx) => {
|
||||||
<div
|
const colOriginal = col.originalName || col.name;
|
||||||
key={idx}
|
const isJoinColumn = joinSet.has(colOriginal);
|
||||||
className="flex items-center gap-1 rounded bg-slate-50 px-1.5 py-0.5 hover:bg-slate-100"
|
const isHighlighted = highlightSet.has(colOriginal);
|
||||||
>
|
|
||||||
{/* PK/FK 아이콘 */}
|
return (
|
||||||
{col.isPrimaryKey && <Key className="h-2.5 w-2.5 text-amber-500" />}
|
<div
|
||||||
{col.isForeignKey && !col.isPrimaryKey && <Link2 className="h-2.5 w-2.5 text-blue-500" />}
|
key={col.name}
|
||||||
{!col.isPrimaryKey && !col.isForeignKey && <div className="w-2.5" />}
|
className={`flex items-center gap-1 rounded px-1.5 py-0.5 transition-all duration-300 ${
|
||||||
|
isJoinColumn
|
||||||
|
? "bg-orange-100 border border-orange-300 shadow-sm"
|
||||||
|
: isHighlighted
|
||||||
|
? "bg-blue-100 border border-blue-300 shadow-sm"
|
||||||
|
: hasActiveColumns
|
||||||
|
? "bg-slate-100"
|
||||||
|
: "bg-slate-50 hover:bg-slate-100"
|
||||||
|
}`}
|
||||||
|
style={{
|
||||||
|
animation: hasActiveColumns ? `fadeIn 0.3s ease ${idx * 50}ms forwards` : undefined,
|
||||||
|
opacity: hasActiveColumns ? 0 : 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* PK/FK/조인 아이콘 */}
|
||||||
|
{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" />}
|
||||||
|
{!isJoinColumn && col.isForeignKey && !col.isPrimaryKey && <Link2 className="h-2.5 w-2.5 text-blue-500" />}
|
||||||
|
{!isJoinColumn && !col.isPrimaryKey && !col.isForeignKey && <div className="w-2.5" />}
|
||||||
|
|
||||||
{/* 컬럼명 */}
|
{/* 컬럼명 */}
|
||||||
<span className="flex-1 truncate font-mono text-[9px] font-medium text-slate-700">
|
<span className={`flex-1 truncate font-mono text-[9px] font-medium ${
|
||||||
{col.name}
|
isJoinColumn ? "text-orange-700" : isHighlighted ? "text-blue-700" : "text-slate-700"
|
||||||
</span>
|
}`}>
|
||||||
|
{col.name}
|
||||||
|
</span>
|
||||||
|
|
||||||
{/* 타입 */}
|
{/* 역할 태그 + 참조 관계 표시 */}
|
||||||
<span className="text-[8px] text-slate-400">{col.type}</span>
|
{isJoinColumn && (
|
||||||
</div>
|
<>
|
||||||
))}
|
{/* 참조 관계 표시: ← 한글 컬럼명 (또는 영문) */}
|
||||||
|
{fieldMappingMap.has(colOriginal) && (
|
||||||
|
<span className="rounded bg-orange-100 px-1 text-[7px] text-orange-600">
|
||||||
|
← {fieldMappingMap.get(colOriginal)?.sourceDisplayName}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span className="rounded bg-orange-200 px-1 text-[7px] text-orange-700">조인</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{isHighlighted && !isJoinColumn && (
|
||||||
|
<span className="rounded bg-blue-200 px-1 text-[7px] text-blue-700">사용</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 타입 */}
|
||||||
|
<span className="text-[8px] text-slate-400">{col.type}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{/* 더 많은 컬럼이 있을 경우 표시 */}
|
||||||
{remainingCount > 0 && (
|
{remainingCount > 0 && (
|
||||||
<div className="text-center text-[8px] text-slate-400 py-0.5">+ {remainingCount}개 더</div>
|
<div className="text-center text-[8px] text-slate-400 py-0.5">
|
||||||
|
+ {remainingCount}개 더
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -486,9 +595,25 @@ export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => {
|
||||||
<div className="flex items-center justify-between border-t border-border bg-muted/30 px-2 py-1">
|
<div className="flex items-center justify-between border-t border-border bg-muted/30 px-2 py-1">
|
||||||
<span className="text-[9px] text-muted-foreground">PostgreSQL</span>
|
<span className="text-[9px] text-muted-foreground">PostgreSQL</span>
|
||||||
{columns && (
|
{columns && (
|
||||||
<span className="text-[9px] text-muted-foreground">{columns.length}개 컬럼</span>
|
<span className="text-[9px] text-muted-foreground">
|
||||||
|
{hasActiveColumns ? `${displayColumns.length}/${totalCount}` : totalCount}개 컬럼
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* CSS 애니메이션 정의 */}
|
||||||
|
<style jsx>{`
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-5px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -38,8 +38,8 @@ const nodeTypes = {
|
||||||
|
|
||||||
// 레이아웃 상수
|
// 레이아웃 상수
|
||||||
const SCREEN_Y = 50; // 화면 노드 Y 위치 (상단)
|
const SCREEN_Y = 50; // 화면 노드 Y 위치 (상단)
|
||||||
const TABLE_Y = 520; // 메인 테이블 노드 Y 위치 (중단)
|
const TABLE_Y = 420; // 메인 테이블 노드 Y 위치 (중단) - 위로 이동
|
||||||
const SUB_TABLE_Y = 780; // 서브 테이블 노드 Y 위치 (하단)
|
const SUB_TABLE_Y = 680; // 서브 테이블 노드 Y 위치 (하단) - 위로 이동
|
||||||
const NODE_WIDTH = 260; // 노드 너비
|
const NODE_WIDTH = 260; // 노드 너비
|
||||||
const NODE_GAP = 40; // 노드 간격
|
const NODE_GAP = 40; // 노드 간격
|
||||||
|
|
||||||
|
|
@ -61,19 +61,42 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [tableColumns, setTableColumns] = useState<Record<string, ColumnTypeInfo[]>>({});
|
const [tableColumns, setTableColumns] = useState<Record<string, ColumnTypeInfo[]>>({});
|
||||||
|
|
||||||
|
// ReactFlow 인스턴스 (fitView 제어용)
|
||||||
|
const reactFlowInstance = useReactFlow();
|
||||||
|
|
||||||
|
// 데이터 로드 버전 (초기 로드 시에만 fitView 호출)
|
||||||
|
const [dataLoadVersion, setDataLoadVersion] = useState(0);
|
||||||
|
|
||||||
|
// 뷰 준비 상태 (fitView 완료 후 true로 설정하여 깜빡임 방지)
|
||||||
|
const [isViewReady, setIsViewReady] = useState(false);
|
||||||
|
|
||||||
|
|
||||||
// 그룹 내 포커스된 화면 ID (그룹 모드에서만 사용)
|
// 그룹 내 포커스된 화면 ID (그룹 모드에서만 사용)
|
||||||
const [focusedScreenId, setFocusedScreenId] = useState<number | null>(null);
|
const [focusedScreenId, setFocusedScreenId] = useState<number | null>(null);
|
||||||
|
|
||||||
|
// 그룹 또는 화면이 변경될 때 포커스 초기화
|
||||||
|
useEffect(() => {
|
||||||
|
setFocusedScreenId(null);
|
||||||
|
}, [selectedGroup?.id, screen?.screenId]);
|
||||||
|
|
||||||
// 외부에서 전달된 초기 포커스 ID 적용 (화면 이동 없이 강조만)
|
// 외부에서 전달된 초기 포커스 ID 적용 (화면 이동 없이 강조만)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initialFocusedScreenId !== undefined && initialFocusedScreenId !== null) {
|
if (initialFocusedScreenId !== undefined) {
|
||||||
setFocusedScreenId(initialFocusedScreenId);
|
setFocusedScreenId(initialFocusedScreenId);
|
||||||
}
|
}
|
||||||
}, [initialFocusedScreenId]);
|
}, [initialFocusedScreenId]);
|
||||||
|
|
||||||
// 화면 ID와 테이블명 매핑 (포커스 시 연결선 강조용)
|
// 화면 ID와 테이블명 매핑 (포커스 시 연결선 강조용)
|
||||||
const [screenTableMap, setScreenTableMap] = useState<Record<number, string>>({});
|
const [screenTableMap, setScreenTableMap] = useState<Record<number, string>>({});
|
||||||
|
|
||||||
|
// 화면 ID별 서브 테이블 매핑 (포커스 시 서브 테이블 연결선 강조용)
|
||||||
|
const [screenSubTableMap, setScreenSubTableMap] = useState<Record<number, string[]>>({});
|
||||||
|
|
||||||
|
// 서브 테이블 데이터 저장 (조인 컬럼 정보 포함)
|
||||||
|
const [subTablesDataMap, setSubTablesDataMap] = useState<Record<number, ScreenSubTablesData>>({});
|
||||||
|
|
||||||
|
// 화면별 사용 컬럼 매핑 (화면 ID -> 테이블명 -> 사용 컬럼들)
|
||||||
|
const [screenUsedColumnsMap, setScreenUsedColumnsMap] = useState<Record<number, Record<string, string[]>>>({});
|
||||||
|
|
||||||
// 테이블 컬럼 정보 로드
|
// 테이블 컬럼 정보 로드
|
||||||
const loadTableColumns = useCallback(
|
const loadTableColumns = useCallback(
|
||||||
|
|
@ -95,11 +118,8 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
|
||||||
},
|
},
|
||||||
[tableColumns]
|
[tableColumns]
|
||||||
);
|
);
|
||||||
|
|
||||||
// 그룹 변경 시 focusedScreenId 초기화
|
// 중복 useEffect 제거됨 (위에서 이미 선언)
|
||||||
useEffect(() => {
|
|
||||||
setFocusedScreenId(null);
|
|
||||||
}, [selectedGroup?.id, screen?.screenId]);
|
|
||||||
|
|
||||||
// 데이터 로드 및 노드/엣지 생성
|
// 데이터 로드 및 노드/엣지 생성
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -112,6 +132,7 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
|
||||||
|
|
||||||
const loadRelations = async () => {
|
const loadRelations = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
setIsViewReady(false); // 뷰 준비 상태 초기화
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let screenList: ScreenDefinition[] = [];
|
let screenList: ScreenDefinition[] = [];
|
||||||
|
|
@ -209,10 +230,57 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
|
||||||
|
|
||||||
if (subTablesRes.success && subTablesRes.data) {
|
if (subTablesRes.success && subTablesRes.data) {
|
||||||
subTablesData = subTablesRes.data as Record<number, ScreenSubTablesData>;
|
subTablesData = subTablesRes.data as Record<number, ScreenSubTablesData>;
|
||||||
|
// 서브 테이블 데이터 저장 (조인 컬럼 정보 포함)
|
||||||
|
setSubTablesDataMap(subTablesData);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("레이아웃 요약/서브 테이블 로드 실패:", e);
|
console.error("레이아웃 요약/서브 테이블 로드 실패:", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 화면별 사용 컬럼 정보 추출 (layoutSummaries에서)
|
||||||
|
const usedColumnsMap: Record<number, Record<string, string[]>> = {};
|
||||||
|
screenList.forEach((screenItem) => {
|
||||||
|
const layout = layoutSummaries[screenItem.screenId];
|
||||||
|
if (layout && layout.layoutItems) {
|
||||||
|
const mainTable = screenItem.tableName;
|
||||||
|
if (mainTable) {
|
||||||
|
// layoutItems에서 사용 컬럼과 조인 컬럼 추출
|
||||||
|
const allUsedColumns: string[] = [];
|
||||||
|
const allJoinColumns: string[] = [];
|
||||||
|
|
||||||
|
layout.layoutItems.forEach((item) => {
|
||||||
|
// usedColumns 배열에서 추출 (columns_config에서 가져온 컬럼명)
|
||||||
|
if (item.usedColumns && Array.isArray(item.usedColumns)) {
|
||||||
|
item.usedColumns.forEach((col) => {
|
||||||
|
if (col && !allUsedColumns.includes(col)) {
|
||||||
|
allUsedColumns.push(col);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// joinColumns 배열에서 추출 (isEntityJoin = true인 컬럼)
|
||||||
|
if (item.joinColumns && Array.isArray(item.joinColumns)) {
|
||||||
|
item.joinColumns.forEach((col) => {
|
||||||
|
if (col && !allJoinColumns.includes(col)) {
|
||||||
|
allJoinColumns.push(col);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// 하위 호환성: bindField도 사용 컬럼에 추가
|
||||||
|
if (item.bindField && !allUsedColumns.includes(item.bindField)) {
|
||||||
|
allUsedColumns.push(item.bindField);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!usedColumnsMap[screenItem.screenId]) {
|
||||||
|
usedColumnsMap[screenItem.screenId] = {};
|
||||||
|
}
|
||||||
|
// 사용 컬럼과 조인 컬럼을 별도 키로 저장
|
||||||
|
usedColumnsMap[screenItem.screenId][mainTable] = allUsedColumns;
|
||||||
|
usedColumnsMap[screenItem.screenId][`${mainTable}__join`] = allJoinColumns;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setScreenUsedColumnsMap(usedColumnsMap);
|
||||||
|
|
||||||
// ========== 상단: 화면 노드들 ==========
|
// ========== 상단: 화면 노드들 ==========
|
||||||
const screenNodes: ScreenNodeType[] = [];
|
const screenNodes: ScreenNodeType[] = [];
|
||||||
|
|
@ -295,14 +363,28 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
|
||||||
|
|
||||||
// 서브 테이블 수집 (componentConfig에서 추출된 테이블들)
|
// 서브 테이블 수집 (componentConfig에서 추출된 테이블들)
|
||||||
// 서브 테이블은 메인 테이블과 다른 테이블들
|
// 서브 테이블은 메인 테이블과 다른 테이블들
|
||||||
Object.values(subTablesData).forEach((screenSubData) => {
|
// 화면별 서브 테이블 매핑도 함께 구축
|
||||||
|
const newScreenSubTableMap: Record<number, string[]> = {};
|
||||||
|
|
||||||
|
Object.entries(subTablesData).forEach(([screenIdStr, screenSubData]) => {
|
||||||
|
const screenId = parseInt(screenIdStr);
|
||||||
|
const subTableNames: string[] = [];
|
||||||
|
|
||||||
screenSubData.subTables.forEach((subTable) => {
|
screenSubData.subTables.forEach((subTable) => {
|
||||||
// 메인 테이블에 없는 것만 서브 테이블로 추가
|
// 메인 테이블에 없는 것만 서브 테이블로 추가
|
||||||
if (!mainTableSet.has(subTable.tableName)) {
|
if (!mainTableSet.has(subTable.tableName)) {
|
||||||
subTableSet.add(subTable.tableName);
|
subTableSet.add(subTable.tableName);
|
||||||
|
subTableNames.push(subTable.tableName);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (subTableNames.length > 0) {
|
||||||
|
newScreenSubTableMap[screenId] = subTableNames;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 화면별 서브 테이블 매핑 저장
|
||||||
|
setScreenSubTableMap(newScreenSubTableMap);
|
||||||
|
|
||||||
// 메인 테이블 노드 배치 (화면들의 중앙 아래에 배치)
|
// 메인 테이블 노드 배치 (화면들의 중앙 아래에 배치)
|
||||||
const mainTableList = Array.from(mainTableSet);
|
const mainTableList = Array.from(mainTableSet);
|
||||||
|
|
@ -315,12 +397,20 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
|
||||||
const mainTableTotalWidth = mainTableList.length * NODE_WIDTH + (mainTableList.length - 1) * NODE_GAP;
|
const mainTableTotalWidth = mainTableList.length * NODE_WIDTH + (mainTableList.length - 1) * NODE_GAP;
|
||||||
const mainTableStartX = screenCenterX - mainTableTotalWidth / 2;
|
const mainTableStartX = screenCenterX - mainTableTotalWidth / 2;
|
||||||
|
|
||||||
// 첫 번째 화면의 테이블 또는 선택된 화면의 테이블
|
// 각 테이블이 어떤 화면들의 메인 테이블인지 매핑
|
||||||
const primaryTableName = screen?.tableName || (screenList.length > 0 ? screenList[0].tableName : null);
|
const tableToScreensMap = new Map<string, string[]>();
|
||||||
|
screenList.forEach((scr: any) => {
|
||||||
|
if (scr.tableName) {
|
||||||
|
const screens = tableToScreensMap.get(scr.tableName) || [];
|
||||||
|
screens.push(scr.screenName);
|
||||||
|
tableToScreensMap.set(scr.tableName, screens);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
for (let idx = 0; idx < mainTableList.length; idx++) {
|
for (let idx = 0; idx < mainTableList.length; idx++) {
|
||||||
const tableName = mainTableList[idx];
|
const tableName = mainTableList[idx];
|
||||||
const isPrimaryTable = tableName === primaryTableName;
|
// mainTableSet에 있는 테이블은 모두 해당 화면의 "메인 테이블"
|
||||||
|
const linkedScreens = tableToScreensMap.get(tableName) || [];
|
||||||
|
|
||||||
// 컬럼 정보 로드
|
// 컬럼 정보 로드
|
||||||
let columns: ColumnTypeInfo[] = [];
|
let columns: ColumnTypeInfo[] = [];
|
||||||
|
|
@ -330,22 +420,28 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
|
|
||||||
// 컬럼 정보를 PK/FK 표시와 함께 변환
|
// 컬럼 정보를 PK/FK 표시와 함께 변환 (전체 컬럼 저장, 표시는 TableNode에서 제한)
|
||||||
const formattedColumns = columns.slice(0, 8).map((col) => ({
|
const formattedColumns = columns.map((col) => ({
|
||||||
name: col.displayName || col.columnName || "",
|
name: col.displayName || col.columnName || "",
|
||||||
|
originalName: col.columnName || "", // 영문 컬럼명 (필터링용)
|
||||||
type: col.dataType || "",
|
type: col.dataType || "",
|
||||||
isPrimaryKey: col.isPrimaryKey || col.columnName === "id",
|
isPrimaryKey: col.isPrimaryKey || col.columnName === "id",
|
||||||
isForeignKey: !!col.referenceTable || (col.columnName?.includes("_id") && col.columnName !== "id"),
|
isForeignKey: !!col.referenceTable || (col.columnName?.includes("_id") && col.columnName !== "id"),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// 여러 화면이 같은 테이블 사용하면 "공통 메인 테이블", 아니면 "메인 테이블"
|
||||||
|
const subLabel = linkedScreens.length > 1
|
||||||
|
? `메인 테이블 (${linkedScreens.length}개 화면)`
|
||||||
|
: "메인 테이블";
|
||||||
|
|
||||||
tableNodes.push({
|
tableNodes.push({
|
||||||
id: `table-${tableName}`,
|
id: `table-${tableName}`,
|
||||||
type: "tableNode",
|
type: "tableNode",
|
||||||
position: { x: mainTableStartX + idx * (NODE_WIDTH + NODE_GAP), y: TABLE_Y },
|
position: { x: mainTableStartX + idx * (NODE_WIDTH + NODE_GAP), y: TABLE_Y },
|
||||||
data: {
|
data: {
|
||||||
label: tableName,
|
label: tableName,
|
||||||
subLabel: isPrimaryTable ? "메인 테이블" : "조인 테이블",
|
subLabel: subLabel,
|
||||||
isMain: isPrimaryTable,
|
isMain: true, // mainTableSet의 모든 테이블은 메인
|
||||||
columns: formattedColumns,
|
columns: formattedColumns,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -370,9 +466,10 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
|
|
||||||
// 컬럼 정보를 PK/FK 표시와 함께 변환
|
// 컬럼 정보를 PK/FK 표시와 함께 변환 (전체 컬럼 저장, 표시는 TableNode에서 제한)
|
||||||
const formattedColumns = columns.slice(0, 5).map((col) => ({
|
const formattedColumns = columns.map((col) => ({
|
||||||
name: col.displayName || col.columnName || "",
|
name: col.displayName || col.columnName || "",
|
||||||
|
originalName: col.columnName || "", // 영문 컬럼명 (필터링용)
|
||||||
type: col.dataType || "",
|
type: col.dataType || "",
|
||||||
isPrimaryKey: col.isPrimaryKey || col.columnName === "id",
|
isPrimaryKey: col.isPrimaryKey || col.columnName === "id",
|
||||||
isForeignKey: !!col.referenceTable || (col.columnName?.includes("_id") && col.columnName !== "id"),
|
isForeignKey: !!col.referenceTable || (col.columnName?.includes("_id") && col.columnName !== "id"),
|
||||||
|
|
@ -398,6 +495,7 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
|
||||||
subLabel: `서브 테이블 (${relationType})`,
|
subLabel: `서브 테이블 (${relationType})`,
|
||||||
isMain: false,
|
isMain: false,
|
||||||
columns: formattedColumns,
|
columns: formattedColumns,
|
||||||
|
isFaded: true, // 기본적으로 흐리게 표시 (포커스 시에만 활성화)
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -431,9 +529,9 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
|
||||||
}
|
}
|
||||||
|
|
||||||
// 각 화면 → 해당 메인 테이블 연결선 생성 (실선)
|
// 각 화면 → 해당 메인 테이블 연결선 생성 (실선)
|
||||||
screenList.forEach((scr, idx) => {
|
// 모든 화면-테이블 연결은 동일한 스타일 (각 화면의 메인 테이블이므로)
|
||||||
|
screenList.forEach((scr) => {
|
||||||
if (scr.tableName && mainTableSet.has(scr.tableName)) {
|
if (scr.tableName && mainTableSet.has(scr.tableName)) {
|
||||||
const isMain = screen ? scr.screenId === screen.screenId : idx === 0;
|
|
||||||
newEdges.push({
|
newEdges.push({
|
||||||
id: `edge-screen-table-${scr.screenId}`,
|
id: `edge-screen-table-${scr.screenId}`,
|
||||||
source: `screen-${scr.screenId}`,
|
source: `screen-${scr.screenId}`,
|
||||||
|
|
@ -441,18 +539,19 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
|
||||||
sourceHandle: "bottom",
|
sourceHandle: "bottom",
|
||||||
targetHandle: "top",
|
targetHandle: "top",
|
||||||
type: "smoothstep",
|
type: "smoothstep",
|
||||||
animated: isMain, // 메인 화면만 애니메이션
|
animated: true, // 모든 메인 테이블 연결은 애니메이션
|
||||||
style: {
|
style: {
|
||||||
stroke: isMain ? "#3b82f6" : "#94a3b8",
|
stroke: "#3b82f6",
|
||||||
strokeWidth: isMain ? 2 : 1.5,
|
strokeWidth: 2,
|
||||||
strokeDasharray: isMain ? undefined : "5,5", // 보조 연결은 점선
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 메인 테이블 → 서브 테이블 연결선 생성 (점선)
|
// 메인 테이블 → 서브 테이블 연결선 생성 (점선 + 애니메이션)
|
||||||
Object.values(subTablesData).forEach((screenSubData) => {
|
// 화면별 서브 테이블 연결을 추적하기 위해 screenId 정보도 엣지 ID에 포함
|
||||||
|
Object.entries(subTablesData).forEach(([screenIdStr, screenSubData]) => {
|
||||||
|
const sourceScreenId = parseInt(screenIdStr);
|
||||||
const mainTable = screenSubData.mainTable;
|
const mainTable = screenSubData.mainTable;
|
||||||
if (!mainTable || !mainTableSet.has(mainTable)) return;
|
if (!mainTable || !mainTableSet.has(mainTable)) return;
|
||||||
|
|
||||||
|
|
@ -460,8 +559,8 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
|
||||||
// 서브 테이블 노드가 실제로 생성되었는지 확인
|
// 서브 테이블 노드가 실제로 생성되었는지 확인
|
||||||
if (!subTableSet.has(subTable.tableName)) return;
|
if (!subTableSet.has(subTable.tableName)) return;
|
||||||
|
|
||||||
// 중복 엣지 방지
|
// 화면별로 고유한 엣지 ID (같은 서브 테이블이라도 다른 화면에서 사용하면 별도 엣지)
|
||||||
const edgeId = `edge-main-sub-${mainTable}-${subTable.tableName}`;
|
const edgeId = `edge-main-sub-${sourceScreenId}-${mainTable}-${subTable.tableName}`;
|
||||||
const exists = newEdges.some((e) => e.id === edgeId);
|
const exists = newEdges.some((e) => e.id === edgeId);
|
||||||
if (exists) return;
|
if (exists) return;
|
||||||
|
|
||||||
|
|
@ -479,15 +578,19 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
|
||||||
targetHandle: "top",
|
targetHandle: "top",
|
||||||
type: "smoothstep",
|
type: "smoothstep",
|
||||||
label: relationLabel,
|
label: relationLabel,
|
||||||
labelStyle: { fontSize: 9, fill: "#f97316", fontWeight: 500 },
|
labelStyle: { fontSize: 9, fill: "#94a3b8", fontWeight: 500 }, // 기본 흐린 색상
|
||||||
labelBgStyle: { fill: "white", stroke: "#e2e8f0", strokeWidth: 1 },
|
labelBgStyle: { fill: "white", stroke: "#e2e8f0", strokeWidth: 1 },
|
||||||
labelBgPadding: [3, 2] as [number, number],
|
labelBgPadding: [3, 2] as [number, number],
|
||||||
markerEnd: { type: MarkerType.ArrowClosed, color: "#f97316" },
|
markerEnd: { type: MarkerType.ArrowClosed, color: "#94a3b8" }, // 기본 흐린 색상
|
||||||
|
animated: false, // 기본: 애니메이션 비활성화 (포커스 시에만 활성화)
|
||||||
style: {
|
style: {
|
||||||
stroke: "#f97316",
|
stroke: "#94a3b8", // 기본 흐린 색상
|
||||||
strokeWidth: 1.5,
|
strokeWidth: 1,
|
||||||
strokeDasharray: "6,4", // 점선
|
strokeDasharray: "6,4", // 점선
|
||||||
|
opacity: 0.5, // 기본 투명도
|
||||||
},
|
},
|
||||||
|
// 화면 ID 정보를 data에 저장 (styledEdges에서 활용)
|
||||||
|
data: { sourceScreenId },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -580,6 +683,9 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
|
||||||
|
|
||||||
setNodes(allNodes);
|
setNodes(allNodes);
|
||||||
setEdges(newEdges);
|
setEdges(newEdges);
|
||||||
|
|
||||||
|
// 데이터 로드 완료 후 버전 증가 (fitView 트리거용)
|
||||||
|
setDataLoadVersion((prev) => prev + 1);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("관계 데이터 로드 실패:", error);
|
console.error("관계 데이터 로드 실패:", error);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -591,6 +697,20 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
|
||||||
// focusedScreenId는 스타일링에만 영향을 미치므로 의존성에서 제외
|
// focusedScreenId는 스타일링에만 영향을 미치므로 의존성에서 제외
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [screen, selectedGroup, setNodes, setEdges, loadTableColumns]);
|
}, [screen, selectedGroup, setNodes, setEdges, loadTableColumns]);
|
||||||
|
|
||||||
|
// 데이터 로드 완료 시 fitView 호출 (초기 로드 시에만)
|
||||||
|
useEffect(() => {
|
||||||
|
if (dataLoadVersion > 0 && nodes.length > 0) {
|
||||||
|
// setTimeout으로 노드 렌더링 완료 후 fitView 호출
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
// duration: 0으로 설정하여 애니메이션 없이 즉시 이동
|
||||||
|
reactFlowInstance.fitView({ padding: 0.2, duration: 0 });
|
||||||
|
// fitView 완료 후 뷰 표시
|
||||||
|
setIsViewReady(true);
|
||||||
|
}, 50);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [dataLoadVersion, reactFlowInstance, nodes.length]);
|
||||||
|
|
||||||
// 노드 클릭 핸들러 (그룹 모드에서 화면 포커스) - 조건부 return 전에 선언해야 함
|
// 노드 클릭 핸들러 (그룹 모드에서 화면 포커스) - 조건부 return 전에 선언해야 함
|
||||||
const handleNodeClick = useCallback((_event: React.MouseEvent, node: Node) => {
|
const handleNodeClick = useCallback((_event: React.MouseEvent, node: Node) => {
|
||||||
|
|
@ -607,11 +727,91 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
|
||||||
|
|
||||||
// 포커스에 따른 노드 스타일링 (그룹 모드에서 화면 클릭 시)
|
// 포커스에 따른 노드 스타일링 (그룹 모드에서 화면 클릭 시)
|
||||||
const styledNodes = React.useMemo(() => {
|
const styledNodes = React.useMemo(() => {
|
||||||
// 그룹 모드에서 포커스된 화면이 있을 때만 추가 스타일링
|
// 그룹 모드가 아니면 원본 반환
|
||||||
if (!selectedGroup || focusedScreenId === null) return nodes;
|
if (!selectedGroup) return nodes;
|
||||||
|
|
||||||
|
// 포커스된 화면의 서브 테이블 목록 (포커스가 없으면 빈 배열)
|
||||||
|
const focusedSubTables = focusedScreenId !== null
|
||||||
|
? (screenSubTableMap[focusedScreenId] || [])
|
||||||
|
: [];
|
||||||
|
|
||||||
|
// 포커스된 화면의 서브 테이블 데이터 (조인 컬럼 정보 포함)
|
||||||
|
const focusedSubTablesData = focusedScreenId !== null
|
||||||
|
? subTablesDataMap[focusedScreenId]
|
||||||
|
: null;
|
||||||
|
|
||||||
|
// 포커스된 화면의 사용 컬럼 정보
|
||||||
|
const focusedUsedColumns = focusedScreenId !== null
|
||||||
|
? screenUsedColumnsMap[focusedScreenId]
|
||||||
|
: null;
|
||||||
|
|
||||||
|
// 연관 테이블 정보 추출 (parentDataMapping, rightPanelRelation 등으로 연결된 테이블)
|
||||||
|
// { tableName: { columns: [sourceField1, sourceField2], displayNames: [displayName1, displayName2] } }
|
||||||
|
const relatedTablesMap: Record<string, { columns: string[], displayNames: string[] }> = {};
|
||||||
|
if (focusedSubTablesData) {
|
||||||
|
focusedSubTablesData.subTables.forEach((subTable) => {
|
||||||
|
// parentDataMapping, rightPanelRelation 타입의 연결에서 sourceTable 추출
|
||||||
|
if ((subTable.relationType === 'parentMapping' || subTable.relationType === 'rightPanelRelation')
|
||||||
|
&& subTable.fieldMappings) {
|
||||||
|
subTable.fieldMappings.forEach((mapping) => {
|
||||||
|
// sourceTable이 명시되어 있으면 그 테이블에 sourceField 추가
|
||||||
|
// sourceTable이 없으면 subTable.tableName을 sourceTable로 간주하지 않음
|
||||||
|
// mapping에서 sourceTable 정보가 필요함 - 현재는 tableName에서 추론
|
||||||
|
// parentDataMapping의 경우: tableName이 targetTable이고, sourceTable은 별도 정보 필요
|
||||||
|
|
||||||
|
// 임시: subTables의 tableName과 다른 메인 테이블들을 확인
|
||||||
|
// fieldMappings에서 sourceField가 다른 테이블에 있다면 그 테이블이 연관 테이블
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 모든 화면의 메인 테이블과 그에 연결된 조인 정보 매핑
|
||||||
|
// 포커스된 화면이 다른 메인 테이블을 참조하는 경우 해당 테이블도 강조
|
||||||
|
const relatedMainTables: Record<string, { columns: string[], displayNames: string[] }> = {};
|
||||||
|
if (focusedSubTablesData) {
|
||||||
|
// screenTableMap에서 다른 화면들의 메인 테이블 확인
|
||||||
|
Object.entries(screenTableMap).forEach(([screenIdStr, mainTableName]) => {
|
||||||
|
const screenId = parseInt(screenIdStr);
|
||||||
|
if (screenId === focusedScreenId) return; // 자신의 메인 테이블은 제외
|
||||||
|
|
||||||
|
// 포커스된 화면의 subTables 중 이 메인 테이블을 참조하는지 확인
|
||||||
|
focusedSubTablesData.subTables.forEach((subTable) => {
|
||||||
|
if (subTable.fieldMappings) {
|
||||||
|
subTable.fieldMappings.forEach((mapping: any) => {
|
||||||
|
// mapping에 sourceTable 정보가 있는 경우 (parentDataMapping에서 설정)
|
||||||
|
if (mapping.sourceTable && mapping.sourceTable === mainTableName) {
|
||||||
|
if (!relatedMainTables[mainTableName]) {
|
||||||
|
relatedMainTables[mainTableName] = { columns: [], displayNames: [] };
|
||||||
|
}
|
||||||
|
if (mapping.sourceField && !relatedMainTables[mainTableName].columns.includes(mapping.sourceField)) {
|
||||||
|
relatedMainTables[mainTableName].columns.push(mapping.sourceField);
|
||||||
|
relatedMainTables[mainTableName].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-")) {
|
||||||
|
if (focusedScreenId === null) {
|
||||||
|
// 포커스 없음: 모든 화면 정상 표시
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
data: {
|
||||||
|
...node.data,
|
||||||
|
isFocused: false,
|
||||||
|
isFaded: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const screenId = parseInt(node.id.replace("screen-", ""));
|
const screenId = parseInt(node.id.replace("screen-", ""));
|
||||||
const isFocused = screenId === focusedScreenId;
|
const isFocused = screenId === focusedScreenId;
|
||||||
const isFaded = !isFocused;
|
const isFaded = !isFocused;
|
||||||
|
|
@ -625,9 +825,325 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 메인 테이블 노드 스타일링
|
||||||
|
if (node.id.startsWith("table-")) {
|
||||||
|
const tableName = node.id.replace("table-", "");
|
||||||
|
|
||||||
|
// 포커스된 화면의 메인 테이블인지 확인
|
||||||
|
const isFocusedTable = focusedScreenId !== null && screenTableMap[focusedScreenId] === tableName;
|
||||||
|
|
||||||
|
// 연관 테이블인지 확인 (다른 화면의 메인 테이블이지만 포커스된 화면에서 참조하는 테이블)
|
||||||
|
const isRelatedTable = relatedMainTables[tableName] !== undefined;
|
||||||
|
const relatedTableInfo = relatedMainTables[tableName];
|
||||||
|
|
||||||
|
// 조인 컬럼 추출
|
||||||
|
// 1. columns_config에서 isEntityJoin=true인 컬럼 (__join 키)
|
||||||
|
// 2. 서브 테이블 연결 시 fieldMappings에서 메인테이블 컬럼 추출
|
||||||
|
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) {
|
||||||
|
// 디버그: subTables 처리 전 로그
|
||||||
|
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) {
|
||||||
|
subTable.fieldMappings.forEach((mapping, mIdx) => {
|
||||||
|
// 각 매핑 디버그 로그
|
||||||
|
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;
|
||||||
|
|
||||||
|
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)) {
|
||||||
|
joinColumns.push(mapping.targetField);
|
||||||
|
}
|
||||||
|
} else if (subTable.relationType === 'reference' || subTable.relationType === 'source') {
|
||||||
|
// reference, source (sourceTable 없는 경우): sourceField가 메인테이블 컬럼
|
||||||
|
if (mapping.sourceField && !joinColumns.includes(mapping.sourceField)) {
|
||||||
|
joinColumns.push(mapping.sourceField);
|
||||||
|
}
|
||||||
|
} 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)) {
|
||||||
|
joinColumns.push(mapping.targetField);
|
||||||
|
}
|
||||||
|
} 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)) {
|
||||||
|
joinColumns.push(mapping.targetField);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 연관 테이블인 경우: 참조되는 컬럼을 조인 컬럼으로 추가
|
||||||
|
if (isRelatedTable && relatedTableInfo) {
|
||||||
|
relatedTableInfo.columns.forEach((col) => {
|
||||||
|
if (!joinColumns.includes(col)) {
|
||||||
|
joinColumns.push(col);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 사용 컬럼 추출 (조인 컬럼 제외)
|
||||||
|
const allUsedColumns = focusedUsedColumns?.[tableName] || [];
|
||||||
|
const highlightedColumns = allUsedColumns.filter(col => !joinColumns.includes(col));
|
||||||
|
|
||||||
|
// 테이블 활성화 여부: 포커스된 화면의 메인 테이블 OR 연관 테이블
|
||||||
|
const isActiveTable = isFocusedTable || isRelatedTable;
|
||||||
|
|
||||||
|
// 메인테이블용 fieldMappings 생성 (조인 컬럼 옆에 연관 테이블 컬럼명 표시)
|
||||||
|
// 예: 거래처 ID ← 거래처 코드 (메인테이블 컬럼 ← 연관테이블 컬럼)
|
||||||
|
let mainTableFieldMappings: Array<{ sourceField: string; targetField: string; sourceDisplayName?: string; targetDisplayName?: string }> = [];
|
||||||
|
if (isFocusedTable && focusedSubTablesData) {
|
||||||
|
focusedSubTablesData.subTables.forEach((subTable) => {
|
||||||
|
if (subTable.fieldMappings) {
|
||||||
|
subTable.fieldMappings.forEach((mapping) => {
|
||||||
|
// 메인테이블에서는:
|
||||||
|
// - sourceField/targetField 중 메인테이블 컬럼이 targetField (표시할 컬럼)
|
||||||
|
// - 연관테이블 컬럼이 sourceField (← 뒤에 표시할 참조 컬럼)
|
||||||
|
if (subTable.relationType === 'source' && mapping.sourceTable) {
|
||||||
|
// parentDataMapping 스타일: targetField = 메인테이블, sourceField = 연관테이블
|
||||||
|
mainTableFieldMappings.push({
|
||||||
|
sourceField: mapping.sourceField, // 연관 테이블 컬럼
|
||||||
|
targetField: mapping.targetField, // 메인 테이블 컬럼
|
||||||
|
sourceDisplayName: mapping.sourceDisplayName || mapping.sourceField,
|
||||||
|
targetDisplayName: mapping.targetDisplayName || mapping.targetField,
|
||||||
|
});
|
||||||
|
} else if (subTable.relationType === 'parentMapping' || subTable.relationType === 'rightPanelRelation') {
|
||||||
|
mainTableFieldMappings.push({
|
||||||
|
sourceField: mapping.sourceField,
|
||||||
|
targetField: mapping.targetField,
|
||||||
|
sourceDisplayName: mapping.sourceDisplayName || mapping.sourceField,
|
||||||
|
targetDisplayName: mapping.targetDisplayName || mapping.targetField,
|
||||||
|
});
|
||||||
|
} else if (subTable.relationType === 'reference' || subTable.relationType === 'source') {
|
||||||
|
// reference/source: sourceField = 메인테이블, targetField = 서브테이블
|
||||||
|
// 메인테이블 표시: sourceField ← targetDisplayName
|
||||||
|
mainTableFieldMappings.push({
|
||||||
|
sourceField: mapping.targetField, // 연관 테이블 컬럼 (표시용)
|
||||||
|
targetField: mapping.sourceField, // 메인 테이블 컬럼
|
||||||
|
sourceDisplayName: mapping.targetDisplayName || mapping.targetField,
|
||||||
|
targetDisplayName: mapping.sourceDisplayName || mapping.sourceField,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// lookup: targetField = 메인테이블, sourceField = 서브테이블
|
||||||
|
mainTableFieldMappings.push({
|
||||||
|
sourceField: mapping.sourceField,
|
||||||
|
targetField: mapping.targetField,
|
||||||
|
sourceDisplayName: mapping.sourceDisplayName || mapping.sourceField,
|
||||||
|
targetDisplayName: mapping.targetDisplayName || mapping.targetField,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 연관 테이블용 fieldMappings 생성
|
||||||
|
let relatedTableFieldMappings: Array<{ sourceField: string; targetField: string; sourceDisplayName?: string; targetDisplayName?: string }> = [];
|
||||||
|
if (isRelatedTable && relatedTableInfo && focusedSubTablesData) {
|
||||||
|
focusedSubTablesData.subTables.forEach((subTable) => {
|
||||||
|
if (subTable.fieldMappings) {
|
||||||
|
subTable.fieldMappings.forEach((mapping) => {
|
||||||
|
if (mapping.sourceTable === tableName) {
|
||||||
|
// 이 테이블이 sourceTable인 경우: sourceField가 이 테이블의 컬럼
|
||||||
|
relatedTableFieldMappings.push({
|
||||||
|
sourceField: mapping.targetField, // 메인 테이블 컬럼 (참조 표시용)
|
||||||
|
targetField: mapping.sourceField, // 연관 테이블 컬럼 (표시할 컬럼)
|
||||||
|
sourceDisplayName: mapping.targetDisplayName || mapping.targetField,
|
||||||
|
targetDisplayName: mapping.sourceDisplayName || mapping.sourceField,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
data: {
|
||||||
|
...node.data,
|
||||||
|
isFocused: isFocusedTable,
|
||||||
|
isRelated: isRelatedTable,
|
||||||
|
isFaded: focusedScreenId !== null && !isActiveTable,
|
||||||
|
highlightedColumns: isActiveTable ? highlightedColumns : [],
|
||||||
|
joinColumns: isActiveTable ? joinColumns : [],
|
||||||
|
fieldMappings: isFocusedTable ? mainTableFieldMappings : (isRelatedTable ? relatedTableFieldMappings : []),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 서브 테이블 노드 스타일링
|
||||||
|
// 기본: 흐리게, 포커스된 화면의 서브 테이블만 활성화
|
||||||
|
if (node.id.startsWith("subtable-")) {
|
||||||
|
const subTableName = node.id.replace("subtable-", "");
|
||||||
|
const isActiveSubTable = focusedSubTables.includes(subTableName);
|
||||||
|
|
||||||
|
// 조인 컬럼 추출 (서브 테이블 측의 컬럼)
|
||||||
|
// relationType에 따라 다름:
|
||||||
|
// - reference: targetField가 서브테이블 컬럼 (예: manager_id -> user_id)
|
||||||
|
// - lookup 등: sourceField가 서브테이블 컬럼
|
||||||
|
let subTableJoinColumns: string[] = [];
|
||||||
|
if (isActiveSubTable && focusedSubTablesData) {
|
||||||
|
const subTableInfo = focusedSubTablesData.subTables.find(st => st.tableName === subTableName);
|
||||||
|
if (subTableInfo?.fieldMappings) {
|
||||||
|
subTableInfo.fieldMappings.forEach((mapping) => {
|
||||||
|
// reference, source, parentMapping, rightPanelRelation 타입: targetField가 서브테이블의 컬럼 (조인 키)
|
||||||
|
// lookup 타입: sourceField가 서브테이블의 컬럼
|
||||||
|
if (subTableInfo.relationType === 'reference' || subTableInfo.relationType === 'source' ||
|
||||||
|
subTableInfo.relationType === 'parentMapping' || subTableInfo.relationType === 'rightPanelRelation') {
|
||||||
|
// reference, source, parentMapping, rightPanelRelation: 메인테이블 컬럼(sourceField) -> 서브테이블 컬럼(targetField)
|
||||||
|
if (mapping.targetField && !subTableJoinColumns.includes(mapping.targetField)) {
|
||||||
|
subTableJoinColumns.push(mapping.targetField);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// lookup 등: sourceField가 서브테이블 컬럼
|
||||||
|
if (mapping.sourceField && !subTableJoinColumns.includes(mapping.sourceField)) {
|
||||||
|
subTableJoinColumns.push(mapping.sourceField);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 디버깅 로그
|
||||||
|
console.log(`서브테이블 ${subTableName} (${subTableInfo?.relationType}):`, {
|
||||||
|
fieldMappings: subTableInfo?.fieldMappings,
|
||||||
|
extractedJoinColumns: subTableJoinColumns
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 서브 테이블의 highlightedColumns도 추가 (화면에서 서브테이블 컬럼을 직접 사용하는 경우)
|
||||||
|
// joinColumns와 별개로 표시 (조인 키 외 사용 컬럼)
|
||||||
|
const subTableHighlightedColumns: string[] = [];
|
||||||
|
|
||||||
|
// 필드 매핑 정보 추출 (조인 관계 표시용)
|
||||||
|
// reference, source, parentMapping, rightPanelRelation 타입: sourceField = 메인테이블 컬럼, targetField = 서브테이블 컬럼
|
||||||
|
// lookup 타입: sourceField = 서브테이블 컬럼, targetField = 메인테이블 컬럼 (swap 필요)
|
||||||
|
let displayFieldMappings: Array<{ sourceField: string; targetField: string; sourceDisplayName?: string; targetDisplayName?: string }> = [];
|
||||||
|
if (isActiveSubTable && focusedSubTablesData) {
|
||||||
|
const subTableInfo = focusedSubTablesData.subTables.find(st => st.tableName === subTableName);
|
||||||
|
if (subTableInfo?.fieldMappings) {
|
||||||
|
displayFieldMappings = subTableInfo.fieldMappings.map((mapping) => {
|
||||||
|
if (subTableInfo.relationType === 'reference' || subTableInfo.relationType === 'source' ||
|
||||||
|
subTableInfo.relationType === 'parentMapping' || subTableInfo.relationType === 'rightPanelRelation') {
|
||||||
|
// reference, source, parentMapping, rightPanelRelation: 백엔드에서 sourceField = 메인테이블, targetField = 서브테이블
|
||||||
|
// 표시: 서브테이블 컬럼(targetField) ← 메인테이블 한글명(sourceDisplayName)
|
||||||
|
return {
|
||||||
|
sourceField: mapping.sourceField, // 메인 테이블 컬럼 (참조)
|
||||||
|
targetField: mapping.targetField, // 서브 테이블 컬럼 (표시)
|
||||||
|
sourceDisplayName: mapping.sourceDisplayName || mapping.sourceField, // 메인 테이블 한글명
|
||||||
|
targetDisplayName: mapping.targetDisplayName || mapping.targetField,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// lookup 등: 백엔드 fieldMappings가 reference/source와 반대
|
||||||
|
// 백엔드: sourceField = 서브테이블 컬럼, targetField = 메인테이블 컬럼
|
||||||
|
// 프론트엔드 표시: 서브테이블 컬럼(sourceField) ← 메인테이블 컬럼(targetField)
|
||||||
|
// 그래서 swap 필요!
|
||||||
|
return {
|
||||||
|
sourceField: mapping.targetField, // 메인 테이블 컬럼 (참조)
|
||||||
|
targetField: mapping.sourceField, // 서브 테이블 컬럼 (표시)
|
||||||
|
sourceDisplayName: mapping.targetDisplayName || mapping.targetField, // 메인 테이블 한글명
|
||||||
|
targetDisplayName: mapping.sourceDisplayName || mapping.sourceField, // 서브 테이블 한글명
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
style: {
|
||||||
|
...node.style,
|
||||||
|
opacity: isActiveSubTable ? 1 : 0.3,
|
||||||
|
filter: isActiveSubTable ? "none" : "grayscale(80%)",
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
...node.data,
|
||||||
|
isFocused: isActiveSubTable,
|
||||||
|
isFaded: !isActiveSubTable,
|
||||||
|
highlightedColumns: isActiveSubTable ? subTableHighlightedColumns : [],
|
||||||
|
joinColumns: isActiveSubTable ? subTableJoinColumns : [],
|
||||||
|
fieldMappings: isActiveSubTable ? displayFieldMappings : [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return node;
|
return node;
|
||||||
});
|
});
|
||||||
}, [nodes, selectedGroup, focusedScreenId]);
|
}, [nodes, selectedGroup, focusedScreenId, screenSubTableMap, subTablesDataMap, screenUsedColumnsMap, screenTableMap]);
|
||||||
|
|
||||||
// 포커스에 따른 엣지 스타일링 (그룹 모드 & 개별 화면 모드)
|
// 포커스에 따른 엣지 스타일링 (그룹 모드 & 개별 화면 모드)
|
||||||
const styledEdges = React.useMemo(() => {
|
const styledEdges = React.useMemo(() => {
|
||||||
|
|
@ -676,12 +1192,67 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 그룹 모드: 포커스된 화면이 없으면 원본 반환
|
// 그룹 모드가 아니면 원본 반환
|
||||||
if (!selectedGroup || focusedScreenId === null) return edges;
|
if (!selectedGroup) return edges;
|
||||||
|
|
||||||
return edges.map((edge) => {
|
// 연관 테이블 간 조인 엣지 생성 (parentDataMapping, rightPanelRelation)
|
||||||
|
const joinEdges: Edge[] = [];
|
||||||
|
|
||||||
|
if (focusedScreenId !== null) {
|
||||||
|
const focusedSubTablesData = subTablesDataMap[focusedScreenId];
|
||||||
|
const focusedMainTable = screenTableMap[focusedScreenId];
|
||||||
|
|
||||||
|
|
||||||
|
if (focusedSubTablesData) {
|
||||||
|
focusedSubTablesData.subTables.forEach((subTable) => {
|
||||||
|
// fieldMappings에 sourceTable이 있는 경우 처리 (parentMapping, rightPanelRelation 등)
|
||||||
|
if (subTable.fieldMappings) {
|
||||||
|
|
||||||
|
subTable.fieldMappings.forEach((mapping: any, idx: number) => {
|
||||||
|
const sourceTable = mapping.sourceTable;
|
||||||
|
if (!sourceTable) return;
|
||||||
|
|
||||||
|
// 연관 테이블 → 포커싱된 화면의 메인 테이블로 연결
|
||||||
|
// sourceTable(연관) → focusedMainTable(메인)
|
||||||
|
const edgeId = `edge-join-relation-${focusedScreenId}-${sourceTable}-${focusedMainTable}-${idx}`;
|
||||||
|
|
||||||
|
// 이미 존재하는 엣지인지 확인
|
||||||
|
if (joinEdges.some(e => e.id === edgeId)) return;
|
||||||
|
|
||||||
|
// 라벨 제거 - 조인 정보는 테이블 노드 내부에서 컬럼 옆에 표시
|
||||||
|
joinEdges.push({
|
||||||
|
id: edgeId,
|
||||||
|
source: `table-${sourceTable}`,
|
||||||
|
target: `table-${focusedMainTable}`,
|
||||||
|
type: 'smoothstep',
|
||||||
|
animated: true,
|
||||||
|
style: {
|
||||||
|
stroke: '#ea580c',
|
||||||
|
strokeWidth: 2,
|
||||||
|
strokeDasharray: '8,4',
|
||||||
|
},
|
||||||
|
markerEnd: {
|
||||||
|
type: MarkerType.ArrowClosed,
|
||||||
|
color: '#ea580c',
|
||||||
|
width: 15,
|
||||||
|
height: 15,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 기존 엣지 스타일링 + 조인 엣지 추가
|
||||||
|
const styledOriginalEdges = edges.map((edge) => {
|
||||||
// 화면 간 연결선 (1, 2, 3 라벨)
|
// 화면 간 연결선 (1, 2, 3 라벨)
|
||||||
if (edge.source.startsWith("screen-") && edge.target.startsWith("screen-")) {
|
if (edge.source.startsWith("screen-") && edge.target.startsWith("screen-")) {
|
||||||
|
// 포커스가 없으면 모든 화면 간 연결선 정상 표시
|
||||||
|
if (focusedScreenId === null) {
|
||||||
|
return edge; // 원본 그대로
|
||||||
|
}
|
||||||
|
|
||||||
// 포커스된 화면과 연결된 화면 간 선만 활성화
|
// 포커스된 화면과 연결된 화면 간 선만 활성화
|
||||||
const sourceId = parseInt(edge.source.replace("screen-", ""));
|
const sourceId = parseInt(edge.source.replace("screen-", ""));
|
||||||
const targetId = parseInt(edge.target.replace("screen-", ""));
|
const targetId = parseInt(edge.target.replace("screen-", ""));
|
||||||
|
|
@ -701,6 +1272,11 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
|
||||||
|
|
||||||
// 화면-테이블 연결선
|
// 화면-테이블 연결선
|
||||||
if (edge.source.startsWith("screen-") && edge.target.startsWith("table-")) {
|
if (edge.source.startsWith("screen-") && edge.target.startsWith("table-")) {
|
||||||
|
// 포커스가 없으면 모든 화면-테이블 연결선 정상 표시
|
||||||
|
if (focusedScreenId === null) {
|
||||||
|
return edge; // 원본 그대로
|
||||||
|
}
|
||||||
|
|
||||||
const sourceId = parseInt(edge.source.replace("screen-", ""));
|
const sourceId = parseInt(edge.source.replace("screen-", ""));
|
||||||
const isMyConnection = sourceId === focusedScreenId;
|
const isMyConnection = sourceId === focusedScreenId;
|
||||||
|
|
||||||
|
|
@ -717,9 +1293,66 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 메인 테이블 → 서브 테이블 연결선
|
||||||
|
// 기본: 흐리게 처리, 포커스된 화면의 서브 테이블만 강조
|
||||||
|
if (edge.source.startsWith("table-") && edge.target.startsWith("subtable-")) {
|
||||||
|
// 포커스가 없으면 모든 서브 테이블 연결선 흐리게 (기본 상태)
|
||||||
|
if (focusedScreenId === null) {
|
||||||
|
return {
|
||||||
|
...edge,
|
||||||
|
animated: false,
|
||||||
|
style: {
|
||||||
|
...edge.style,
|
||||||
|
stroke: "#d1d5db",
|
||||||
|
strokeWidth: 1,
|
||||||
|
strokeDasharray: "6,4",
|
||||||
|
opacity: 0.3,
|
||||||
|
},
|
||||||
|
labelStyle: {
|
||||||
|
...edge.labelStyle,
|
||||||
|
opacity: 0.3,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 엣지 ID에서 화면 ID 추출: edge-main-sub-{screenId}-{mainTable}-{subTable}
|
||||||
|
const idParts = edge.id.split("-");
|
||||||
|
// edge-main-sub-1413-sales_order_mng-customer_mng 형식
|
||||||
|
const edgeScreenId = idParts.length >= 4 ? parseInt(idParts[3]) : null;
|
||||||
|
|
||||||
|
// 포커스된 화면의 서브 테이블 연결인지 확인
|
||||||
|
const isMySubTable = edgeScreenId === focusedScreenId;
|
||||||
|
|
||||||
|
// 대체 방법: screenSubTableMap 사용
|
||||||
|
const focusedSubTables = focusedScreenId ? screenSubTableMap[focusedScreenId] || [] : [];
|
||||||
|
const subTableName = edge.target.replace("subtable-", "");
|
||||||
|
const isMySubTableByMap = focusedSubTables.includes(subTableName);
|
||||||
|
|
||||||
|
const isActive = isMySubTable || isMySubTableByMap;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...edge,
|
||||||
|
animated: isActive, // 활성화된 것만 애니메이션
|
||||||
|
style: {
|
||||||
|
...edge.style,
|
||||||
|
stroke: isActive ? "#f97316" : "#d1d5db",
|
||||||
|
strokeWidth: isActive ? 2 : 1,
|
||||||
|
strokeDasharray: "6,4", // 항상 점선
|
||||||
|
opacity: isActive ? 1 : 0.2,
|
||||||
|
},
|
||||||
|
labelStyle: {
|
||||||
|
...edge.labelStyle,
|
||||||
|
opacity: isActive ? 1 : 0.3,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return edge;
|
return edge;
|
||||||
});
|
});
|
||||||
}, [edges, selectedGroup, focusedScreenId, screen]);
|
|
||||||
|
// 기존 엣지 + 조인 관계 엣지 합치기
|
||||||
|
return [...styledOriginalEdges, ...joinEdges];
|
||||||
|
}, [edges, selectedGroup, focusedScreenId, screen, screenSubTableMap, subTablesDataMap, screenTableMap]);
|
||||||
|
|
||||||
// 조건부 렌더링 (모든 훅 선언 후에 위치해야 함)
|
// 조건부 렌더링 (모든 훅 선언 후에 위치해야 함)
|
||||||
if (!screen && !selectedGroup) {
|
if (!screen && !selectedGroup) {
|
||||||
|
|
@ -743,22 +1376,23 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full">
|
<div className="h-full w-full">
|
||||||
<ReactFlow
|
{/* isViewReady가 false면 숨김 처리하여 깜빡임 방지 */}
|
||||||
nodes={styledNodes}
|
<div className={`h-full w-full transition-opacity duration-0 ${isViewReady ? "opacity-100" : "opacity-0"}`}>
|
||||||
edges={styledEdges}
|
<ReactFlow
|
||||||
onNodesChange={onNodesChange}
|
nodes={styledNodes}
|
||||||
onEdgesChange={onEdgesChange}
|
edges={styledEdges}
|
||||||
onNodeClick={handleNodeClick}
|
onNodesChange={onNodesChange}
|
||||||
nodeTypes={nodeTypes}
|
onEdgesChange={onEdgesChange}
|
||||||
fitView
|
onNodeClick={handleNodeClick}
|
||||||
fitViewOptions={{ padding: 0.2 }}
|
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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -353,6 +353,9 @@ export interface LayoutItem {
|
||||||
componentKind: string; // 정확한 컴포넌트 종류 (table-list, button-primary 등)
|
componentKind: string; // 정확한 컴포넌트 종류 (table-list, button-primary 등)
|
||||||
widgetType: string; // 일반적인 위젯 타입 (button, text 등)
|
widgetType: string; // 일반적인 위젯 타입 (button, text 등)
|
||||||
label?: string;
|
label?: string;
|
||||||
|
bindField?: string; // 바인딩된 필드명 (컬럼명)
|
||||||
|
usedColumns?: string[]; // 이 컴포넌트에서 사용하는 컬럼 목록
|
||||||
|
joinColumns?: string[]; // 이 컴포넌트에서 조인 컬럼 목록 (isEntityJoin=true)
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ScreenLayoutSummary {
|
export interface ScreenLayoutSummary {
|
||||||
|
|
@ -388,11 +391,21 @@ export async function getMultipleScreenLayoutSummary(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 필드 매핑 정보 타입
|
||||||
|
export interface FieldMappingInfo {
|
||||||
|
sourceTable?: string; // 연관 테이블명 (parentDataMapping에서 사용)
|
||||||
|
sourceField: string;
|
||||||
|
targetField: string;
|
||||||
|
sourceDisplayName?: string; // 메인 테이블 한글 컬럼명
|
||||||
|
targetDisplayName?: string; // 서브 테이블 한글 컬럼명
|
||||||
|
}
|
||||||
|
|
||||||
// 서브 테이블 정보 타입
|
// 서브 테이블 정보 타입
|
||||||
export interface SubTableInfo {
|
export interface SubTableInfo {
|
||||||
tableName: string;
|
tableName: string;
|
||||||
componentType: string;
|
componentType: string;
|
||||||
relationType: 'lookup' | 'source' | 'join';
|
relationType: 'lookup' | 'source' | 'join' | 'reference' | 'parentMapping' | 'rightPanelRelation';
|
||||||
|
fieldMappings?: FieldMappingInfo[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ScreenSubTablesData {
|
export interface ScreenSubTablesData {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue