ERP-node/frontend/components/dataflow/connection/redesigned/RightPanel/VisualMapping/FieldColumn.tsx

195 lines
6.5 KiB
TypeScript

"use client";
import React, { useEffect, useRef } from "react";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Badge } from "@/components/ui/badge";
import { Link, GripVertical } from "lucide-react";
// 타입 import
import { ColumnInfo } from "@/lib/types/multiConnection";
interface FieldColumnProps {
fields: ColumnInfo[];
type: "from" | "to";
selectedField: ColumnInfo | null;
onFieldSelect: (field: ColumnInfo | null) => void;
onFieldPositionUpdate: (fieldId: string, element: HTMLElement) => void;
isFieldMapped: (field: ColumnInfo, type: "from" | "to") => boolean;
onDragStart?: (field: ColumnInfo) => void;
onDragEnd?: () => void;
onDrop?: (targetField: ColumnInfo, sourceField: ColumnInfo) => void;
isDragOver?: boolean;
draggedField?: ColumnInfo | null;
}
/**
* 📋 필드 컬럼 컴포넌트
* - 필드 목록 표시
* - 선택 상태 관리
* - 위치 정보 업데이트
*/
const FieldColumn: React.FC<FieldColumnProps> = ({
fields,
type,
selectedField,
onFieldSelect,
onFieldPositionUpdate,
isFieldMapped,
onDragStart,
onDragEnd,
onDrop,
isDragOver,
draggedField,
}) => {
const fieldRefs = useRef<Record<string, HTMLDivElement>>({});
// 필드 위치 업데이트
useEffect(() => {
const updatePositions = () => {
Object.entries(fieldRefs.current).forEach(([fieldId, element]) => {
if (element) {
onFieldPositionUpdate(fieldId, element);
}
});
};
// 약간의 지연을 두어 DOM이 완전히 렌더링된 후 위치 업데이트
const timeoutId = setTimeout(updatePositions, 100);
return () => clearTimeout(timeoutId);
}, [fields.length]); // fields 배열 대신 length만 의존성으로 사용
// 드래그 앤 드롭 핸들러
const handleDragStart = (e: React.DragEvent, field: ColumnInfo) => {
if (type === "from" && onDragStart) {
e.dataTransfer.setData("text/plain", JSON.stringify(field));
e.dataTransfer.effectAllowed = "copy";
onDragStart(field);
}
};
const handleDragEnd = (e: React.DragEvent) => {
if (onDragEnd) {
onDragEnd();
}
};
const handleDragOver = (e: React.DragEvent) => {
if (type === "to") {
e.preventDefault();
e.dataTransfer.dropEffect = "copy";
}
};
const handleDrop = (e: React.DragEvent, targetField: ColumnInfo) => {
if (type === "to" && onDrop) {
e.preventDefault();
// 이미 매핑된 TO 필드인지 확인
const isMapped = isFieldMapped(targetField, "to");
if (isMapped) {
// 이미 매핑된 필드에는 드롭할 수 없음을 시각적으로 표시
return;
}
try {
const sourceFieldData = e.dataTransfer.getData("text/plain");
const sourceField = JSON.parse(sourceFieldData) as ColumnInfo;
onDrop(targetField, sourceField);
} catch (error) {
console.error("드롭 처리 중 오류:", error);
}
}
};
// 필드 렌더링
const renderField = (field: ColumnInfo, index: number) => {
const fieldId = `${type}_${field.columnName}`;
const isSelected = selectedField?.columnName === field.columnName;
const isMapped = isFieldMapped(field, type);
const displayName = field.displayName || field.columnName;
const isDragging = draggedField?.columnName === field.columnName;
const isDropTarget = type === "to" && isDragOver && draggedField && !isMapped;
const isBlockedDropTarget = type === "to" && isDragOver && draggedField && isMapped;
return (
<div
key={`${type}_${field.columnName}_${index}`}
ref={(el) => {
if (el) {
fieldRefs.current[fieldId] = el;
}
}}
className={`relative cursor-pointer rounded-lg border p-3 transition-all duration-200 ${
isDragging
? "border-primary bg-primary/20 scale-105 transform opacity-50 shadow-lg"
: isSelected
? "border-primary bg-primary/10 shadow-md"
: isMapped
? "border-green-500 bg-green-50 shadow-sm"
: isBlockedDropTarget
? "border-red-400 bg-destructive/10 shadow-md"
: isDropTarget
? "border-blue-400 bg-accent shadow-md"
: "border-border hover:bg-muted/50 hover:shadow-sm"
} `}
draggable={type === "from" && !isMapped}
onDragStart={(e) => handleDragStart(e, field)}
onDragEnd={handleDragEnd}
onDragOver={handleDragOver}
onDrop={(e) => handleDrop(e, field)}
onClick={() => onFieldSelect(isSelected ? null : field)}
>
{/* 연결점 표시 */}
<div
className={`absolute ${type === "from" ? "right-0" : "left-0"} top-1/2 h-3 w-3 -translate-y-1/2 transform rounded-full border-2 transition-colors ${
isSelected
? "bg-primary border-primary"
: isMapped
? "border-green-500 bg-green-500"
: "border-gray-300 bg-white"
} `}
style={{
[type === "from" ? "right" : "left"]: "-6px",
}}
/>
<div className="flex items-center justify-between">
<div className="flex min-w-0 flex-1 items-center gap-2">
{type === "from" && !isMapped && <GripVertical className="h-3 w-3 flex-shrink-0 text-gray-400" />}
<span className="truncate text-sm font-medium">{displayName}</span>
{isMapped && <Link className="h-3 w-3 flex-shrink-0 text-green-600" />}
</div>
<Badge variant="outline" className="flex-shrink-0 text-xs">
{field.webType || field.dataType || "unknown"}
</Badge>
</div>
{field.description && <p className="text-muted-foreground mt-1 truncate text-xs">{field.description}</p>}
{/* 선택 상태 표시 */}
{isSelected && <div className="border-primary pointer-events-none absolute inset-0 rounded-lg border-2" />}
</div>
);
};
return (
<div className="h-full">
<ScrollArea className="h-full rounded-lg border">
<div className="space-y-2 p-2">
{fields.map((field, index) => renderField(field, index))}
{fields.length === 0 && (
<div className="text-muted-foreground py-8 text-center text-sm">
<p> .</p>
<p className="mt-1 text-xs"> .</p>
</div>
)}
</div>
</ScrollArea>
</div>
);
};
export default FieldColumn;