제어관리 외부커넥션 설정기능
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
"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-red-50 shadow-md"
|
||||
: isDropTarget
|
||||
? "border-blue-400 bg-blue-50 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;
|
||||
Reference in New Issue
Block a user