Files
invyone/frontend/components/control/TableNode.tsx
T
DDD1542 2f398ae0b3 chore: 제어모드 IDE 작업 + v2/legacy 레지스트리 컴포넌트 폐기
- 제어모드 IDE: ControlCardPanel, control/ide/* (Canvas/LeftRail/RightRail/PanZoomStage/V3RuleNode 등), schemas, lib/api/control
- 레지스트리 정리: aggregation-widget, status-count, section-card/paper, table-list(legacy/v2), tabs-widget 폐기 → table/_shared/ 로 통합
- InvLegacyButtonConfigPanel cp 마이그레이션
- canonical data view cleanup 후속 노트
2026-05-19 21:31:03 +09:00

130 lines
4.3 KiB
TypeScript

'use client';
import { useRef, useCallback } from 'react';
import { Database, X } from 'lucide-react';
import { PortHandle } from './PortHandle';
import { useControlMode } from './hooks/useControlMode';
interface TableNodeProps {
tableName: string;
label: string;
/** 호환용 — 더 이상 사용 X (V3 컴팩트로 갈아엎으면서 이모지 폐기, Lucide Database 아이콘 고정) */
icon?: string;
columns: Record<string, any>[];
x: number;
y: number;
style?: React.CSSProperties;
onMove?: (name: string, x: number, y: number) => void;
/** 룰 노드 ID (PortHandle 연결용). 없으면 시각 카드만 (read-only) */
nodeId?: string;
onPortDragStart?: (nodeId: string, port: string, e: React.MouseEvent) => void;
onPortDragEnd?: (nodeId: string, port: string) => void;
}
/**
* 테이블 카드 — V3RuleNode 와 일관된 컴팩트 디자인
* - 180px 폭, cyan top stripe, Lucide Database 아이콘
* - 한글 라벨 메인 + mono 영문 sub
* - stats row: `{N} cols · {K} FK`
* - 좌·우 edge 에 단일 port 1개씩 (테이블 단위 연결 — 컬럼은 노드 설정창 dropdown 에서)
*/
export function TableNode({
tableName, label, columns, x, y, style, onMove, nodeId, onPortDragStart, onPortDragEnd,
}: TableNodeProps) {
const nodeRef = useRef<HTMLDivElement>(null);
const removeRuleNode = useControlMode((s) => s.removeRuleNode);
const handleMouseDown = useCallback((e: React.MouseEvent) => {
if (!onMove) return;
const target = e.target as HTMLElement;
if (target.closest('.ctrl-io-port, button')) return;
e.preventDefault();
e.stopPropagation();
const sx = e.clientX, sy = e.clientY;
const sl = x, st = y;
const el = nodeRef.current;
if (el) el.style.zIndex = '30';
let moved = false;
const move = (ev: MouseEvent) => {
const dx = ev.clientX - sx, dy = ev.clientY - sy;
if (!moved && Math.abs(dx) + Math.abs(dy) < 2) return;
moved = true;
onMove(tableName, sl + dx, st + dy);
};
const up = () => {
if (el) el.style.zIndex = '20';
document.removeEventListener('mousemove', move);
document.removeEventListener('mouseup', up);
};
document.addEventListener('mousemove', move);
document.addEventListener('mouseup', up);
}, [onMove, tableName, x, y]);
// stats
const totalCols = columns?.length ?? 0;
const fkCount = (columns ?? []).filter((c) => c.mark === 'FK' || c.type === 'entity').length;
const pkCount = (columns ?? []).filter((c) => c.pk).length;
const hasKoLabel = label && label !== tableName;
return (
<div
ref={nodeRef}
className="tbl-node tbl-node-compact"
data-table={tableName}
data-node-id={nodeId}
onMouseDown={handleMouseDown}
style={{ left: x, top: y, ...style }}
>
{/* cyan top stripe (V3RuleNode cat-stripe 와 일관) */}
<div className="tbl-node-stripe" />
<div className="tbl-node-head">
<div className="tbl-node-ico"><Database size={11} /></div>
<div className="tbl-node-title">
<div className="tbl-node-label">{hasKoLabel ? label : tableName}</div>
{hasKoLabel && <div className="tbl-node-sub">{tableName}</div>}
</div>
{nodeId && (
<button
type="button"
className="tbl-node-del"
title="삭제"
onClick={(e) => { e.stopPropagation(); removeRuleNode(nodeId); }}
>
<X size={10} />
</button>
)}
</div>
<div className="tbl-node-stats">
<span>{totalCols} cols</span>
{pkCount > 0 && <span>· {pkCount} PK</span>}
{fkCount > 0 && <span>· {fkCount} FK</span>}
</div>
{/* 좌·우 단일 port — 테이블 단위 연결 (컬럼 선택은 노드 설정창 dropdown) */}
{nodeId && (
<>
<PortHandle
nodeId={nodeId}
port="in"
type="in"
onDragEnd={onPortDragEnd}
onDragStart={onPortDragStart}
/>
<div className="ctrl-an-ports-out">
<PortHandle
nodeId={nodeId}
port="out"
type="out"
onDragStart={onPortDragStart}
onDragEnd={onPortDragEnd}
/>
</div>
</>
)}
</div>
);
}