"use client";
import React from "react";
import { Handle, Position } from "@xyflow/react";
import {
Monitor,
Database,
FormInput,
Table2,
LayoutDashboard,
MousePointer2,
Key,
Link2,
Columns3,
} from "lucide-react";
import { ScreenLayoutSummary } from "@/lib/api/screenGroup";
// ========== 타입 정의 ==========
// 화면 노드 데이터 인터페이스
export interface ScreenNodeData {
label: string;
subLabel?: string;
type: "screen" | "table" | "action";
tableName?: string;
isMain?: boolean;
// 레이아웃 요약 정보 (미리보기용)
layoutSummary?: ScreenLayoutSummary;
// 그룹 내 포커스 관련 속성
isInGroup?: boolean; // 그룹 모드인지
isFocused?: boolean; // 포커스된 화면인지
isFaded?: boolean; // 흑백 처리할지
screenRole?: string; // 화면 역할 (메인그리드, 등록폼 등)
}
// 필드 매핑 정보 (조인 관계 표시용)
export interface FieldMappingDisplay {
sourceField: string; // 메인 테이블 컬럼 (예: manager_id)
targetField: string; // 서브 테이블 컬럼 (예: user_id)
sourceDisplayName?: string; // 메인 테이블 한글 컬럼명 (예: 담당자)
targetDisplayName?: string; // 서브 테이블 한글 컬럼명 (예: 사용자ID)
}
// 테이블 노드 데이터 인터페이스
export interface TableNodeData {
label: string;
subLabel?: string;
isMain?: boolean;
isFocused?: boolean; // 포커스된 테이블인지
isFaded?: boolean; // 흑백 처리할지
columns?: Array<{
name: string; // 표시용 이름 (한글명)
originalName?: string; // 원본 컬럼명 (영문, 필터링용)
type: string;
isPrimaryKey?: boolean;
isForeignKey?: boolean;
}>;
// 포커스 시 강조할 컬럼 정보
highlightedColumns?: string[]; // 화면에서 사용하는 컬럼 (영문명)
joinColumns?: string[]; // 조인에 사용되는 컬럼
// 필드 매핑 정보 (조인 관계 표시용)
fieldMappings?: FieldMappingDisplay[]; // 서브 테이블일 때 조인 관계 표시
}
// ========== 유틸리티 함수 ==========
// 화면 타입별 아이콘
const getScreenTypeIcon = (screenType?: string) => {
switch (screenType) {
case "grid":
return ;
case "dashboard":
return ;
case "action":
return ;
default:
return ;
}
};
// 화면 타입별 색상 (헤더)
const getScreenTypeColor = (screenType?: string, isMain?: boolean) => {
if (!isMain) return "bg-slate-400";
switch (screenType) {
case "grid":
return "bg-violet-500";
case "dashboard":
return "bg-amber-500";
case "action":
return "bg-rose-500";
default:
return "bg-blue-500";
}
};
// 화면 역할(screenRole)에 따른 색상
const getScreenRoleColor = (screenRole?: string) => {
if (!screenRole) return "bg-slate-400";
// 역할명에 포함된 키워드로 색상 결정
const role = screenRole.toLowerCase();
if (role.includes("그리드") || role.includes("grid") || role.includes("메인") || role.includes("main") || role.includes("list")) {
return "bg-violet-500"; // 보라색 - 메인 그리드
}
if (role.includes("등록") || role.includes("폼") || role.includes("form") || role.includes("register") || role.includes("input")) {
return "bg-blue-500"; // 파란색 - 등록 폼
}
if (role.includes("액션") || role.includes("action") || role.includes("이벤트") || role.includes("event") || role.includes("클릭")) {
return "bg-rose-500"; // 빨간색 - 액션/이벤트
}
if (role.includes("상세") || role.includes("detail") || role.includes("popup") || role.includes("팝업")) {
return "bg-amber-500"; // 주황색 - 상세/팝업
}
return "bg-slate-400"; // 기본 회색
};
// 화면 타입별 라벨
const getScreenTypeLabel = (screenType?: string) => {
switch (screenType) {
case "grid":
return "그리드";
case "dashboard":
return "대시보드";
case "action":
return "액션";
default:
return "폼";
}
};
// ========== 화면 노드 (상단) - 미리보기 표시 ==========
export const ScreenNode: React.FC<{ data: ScreenNodeData }> = ({ data }) => {
const { label, subLabel, isMain, tableName, layoutSummary, isInGroup, isFocused, isFaded, screenRole } = data;
const screenType = layoutSummary?.screenType || "form";
// 그룹 모드에서는 screenRole 기반 색상, 그렇지 않으면 screenType 기반 색상
// isFocused일 때 색상 활성화, isFaded일 때 회색
let headerColor: string;
if (isInGroup) {
if (isFaded) {
headerColor = "bg-gray-300"; // 흑백 처리 - 더 확실한 회색
} else {
// 포커스되었거나 아직 아무것도 선택 안됐을 때: 역할별 색상
headerColor = getScreenRoleColor(screenRole);
}
} else {
headerColor = getScreenTypeColor(screenType, isMain);
}
return (
{/* Handles */}
{/* 헤더 (컬러) */}
{label}
{(isMain || isFocused) && }
{/* 화면 미리보기 영역 (컴팩트) */}
{layoutSummary ? (
) : (
{getScreenTypeIcon(screenType)}
화면: {label}
)}
{/* 필드 매핑 영역 */}
필드 매핑
{layoutSummary?.layoutItems?.filter(i => i.label && !i.componentKind?.includes('button')).length || 0}개
{layoutSummary?.layoutItems
?.filter(item => item.label && !item.componentKind?.includes('button'))
?.slice(0, 6)
?.map((item, idx) => (
{item.label}
{item.componentKind?.split('-')[0] || 'field'}
)) || (
필드 정보 없음
)}
{/* 푸터 (테이블 정보) */}
{tableName || "No Table"}
{getScreenTypeLabel(screenType)}
);
};
// ========== 컴포넌트 종류별 미니어처 색상 ==========
// componentKind는 더 정확한 컴포넌트 타입 (table-list, button-primary 등)
const getComponentColor = (componentKind: string) => {
// 테이블/그리드 관련
if (componentKind === "table-list" || componentKind === "data-grid") {
return "bg-violet-200 border-violet-400";
}
// 검색 필터
if (componentKind === "table-search-widget" || componentKind === "search-filter") {
return "bg-pink-200 border-pink-400";
}
// 버튼 관련
if (componentKind?.includes("button")) {
return "bg-blue-300 border-blue-500";
}
// 입력 필드
if (componentKind?.includes("input") || componentKind?.includes("text")) {
return "bg-slate-200 border-slate-400";
}
// 셀렉트/드롭다운
if (componentKind?.includes("select") || componentKind?.includes("dropdown")) {
return "bg-amber-200 border-amber-400";
}
// 차트
if (componentKind?.includes("chart")) {
return "bg-emerald-200 border-emerald-400";
}
// 커스텀 위젯
if (componentKind === "custom") {
return "bg-pink-200 border-pink-400";
}
return "bg-slate-100 border-slate-300";
};
// ========== 화면 미리보기 컴포넌트 - 화면 타입별 간단한 일러스트 ==========
const ScreenPreview: React.FC<{ layoutSummary: ScreenLayoutSummary; screenType: string }> = ({
layoutSummary,
screenType,
}) => {
const { totalComponents, widgetCounts } = layoutSummary;
// 그리드 화면 일러스트
if (screenType === "grid") {
return (
{/* 상단 툴바 */}
{/* 테이블 헤더 */}
{[...Array(5)].map((_, i) => (
))}
{/* 테이블 행들 */}
{[...Array(7)].map((_, i) => (
{[...Array(5)].map((_, j) => (
))}
))}
{/* 페이지네이션 */}
{/* 컴포넌트 수 */}
{totalComponents}개
);
}
// 폼 화면 일러스트
if (screenType === "form") {
return (
{/* 폼 필드들 */}
{[...Array(6)].map((_, i) => (
))}
{/* 버튼 영역 */}
{/* 컴포넌트 수 */}
{totalComponents}개
);
}
// 대시보드 화면 일러스트
if (screenType === "dashboard") {
return (
{/* 카드/차트들 */}
{[...Array(10)].map((_, i) => (
))}
{/* 컴포넌트 수 */}
{totalComponents}개
);
}
// 액션 화면 일러스트 (버튼 중심)
if (screenType === "action") {
return (
액션 화면
{/* 컴포넌트 수 */}
{totalComponents}개
);
}
// 기본 (알 수 없는 타입)
return (
{getScreenTypeIcon(screenType)}
{totalComponents}개 컴포넌트
);
};
// ========== 테이블 노드 (하단) - 컬럼 목록 표시 (컴팩트) ==========
export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => {
const { label, subLabel, isMain, isFocused, isFaded, columns, highlightedColumns, joinColumns, fieldMappings } = data;
// 강조할 컬럼 세트 (영문 컬럼명 기준)
const highlightSet = new Set(highlightedColumns || []);
const joinSet = new Set(joinColumns || []);
// 필드 매핑 맵 생성 (targetField → { sourceField, sourceDisplayName })
// 서브 테이블에서 targetField가 어떤 메인 테이블 컬럼(sourceField)과 연결되는지
const fieldMappingMap = new Map();
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 (
{/* Handles */}
{/* 헤더 (초록색, 컴팩트) */}
{label}
{subLabel &&
{subLabel}
}
{hasActiveColumns && (
{displayColumns.length}개 활성
)}
{/* 컬럼 목록 - 컴팩트하게 (스크롤 가능) */}
{displayColumns.length > 0 ? (
{displayColumns.map((col, idx) => {
const colOriginal = col.originalName || col.name;
const isJoinColumn = joinSet.has(colOriginal);
const isHighlighted = highlightSet.has(colOriginal);
return (
{/* PK/FK/조인 아이콘 */}
{isJoinColumn &&
}
{!isJoinColumn && col.isPrimaryKey &&
}
{!isJoinColumn && col.isForeignKey && !col.isPrimaryKey &&
}
{!isJoinColumn && !col.isPrimaryKey && !col.isForeignKey &&
}
{/* 컬럼명 */}
{col.name}
{/* 역할 태그 + 참조 관계 표시 */}
{isJoinColumn && (
<>
{/* 참조 관계 표시: ← 한글 컬럼명 (또는 영문) */}
{fieldMappingMap.has(colOriginal) && (
← {fieldMappingMap.get(colOriginal)?.sourceDisplayName}
)}
조인
>
)}
{isHighlighted && !isJoinColumn && (
사용
)}
{/* 타입 */}
{col.type}
);
})}
{/* 더 많은 컬럼이 있을 경우 표시 */}
{remainingCount > 0 && (
+ {remainingCount}개 더
)}
) : (
컬럼 정보 없음
)}
{/* 푸터 (컴팩트) */}
PostgreSQL
{columns && (
{hasActiveColumns ? `${displayColumns.length}/${totalCount}` : totalCount}개 컬럼
)}
{/* CSS 애니메이션 정의 */}
);
};
// ========== 기존 호환성 유지용 ==========
export const LegacyScreenNode = ScreenNode;
export const AggregateNode: React.FC<{ data: any }> = ({ data }) => {
return (
{data.label || "Aggregate"}
);
};