ERP-node/frontend/lib/registry/components/v2-rack-structure/FormatSegmentEditor.tsx

204 lines
5.4 KiB
TypeScript

"use client";
import React, { useMemo } from "react";
import { GripVertical } from "lucide-react";
import { Input } from "@/components/ui/input";
import { Checkbox } from "@/components/ui/checkbox";
import { cn } from "@/lib/utils";
import {
DndContext,
closestCenter,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
type DragEndEvent,
} from "@dnd-kit/core";
import {
SortableContext,
verticalListSortingStrategy,
useSortable,
arrayMove,
} from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { FormatSegment } from "./types";
import { SEGMENT_TYPE_LABELS, buildFormattedString, SAMPLE_VALUES } from "./config";
// 개별 세그먼트 행
interface SortableSegmentRowProps {
segment: FormatSegment;
index: number;
onChange: (index: number, updates: Partial<FormatSegment>) => void;
}
function SortableSegmentRow({ segment, index, onChange }: SortableSegmentRowProps) {
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id: `${segment.type}-${index}` });
const style = {
transform: CSS.Transform.toString(transform),
transition,
};
return (
<div
ref={setNodeRef}
style={style}
className={cn(
"grid grid-cols-[16px_56px_18px_1fr_1fr_1fr] items-center gap-1 rounded border bg-white px-2 py-1.5",
isDragging && "opacity-50",
)}
>
<div
{...attributes}
{...listeners}
className="cursor-grab text-gray-400 hover:text-gray-600"
>
<GripVertical className="h-3.5 w-3.5" />
</div>
<span className="truncate text-xs font-medium">
{SEGMENT_TYPE_LABELS[segment.type]}
</span>
<Checkbox
checked={segment.showLabel}
onCheckedChange={(checked) =>
onChange(index, { showLabel: checked === true })
}
className="h-3.5 w-3.5"
/>
<Input
value={segment.label}
onChange={(e) => onChange(index, { label: e.target.value })}
placeholder=""
className={cn(
"h-6 px-1 text-xs",
!segment.showLabel && "text-gray-400 line-through",
)}
/>
<Input
value={segment.separatorAfter}
onChange={(e) => onChange(index, { separatorAfter: e.target.value })}
placeholder=""
className="h-6 px-1 text-center text-xs"
/>
<Input
type="number"
min={0}
max={5}
value={segment.pad}
onChange={(e) =>
onChange(index, { pad: parseInt(e.target.value) || 0 })
}
disabled={segment.type !== "row" && segment.type !== "level"}
className={cn(
"h-6 px-1 text-center text-xs",
segment.type !== "row" && segment.type !== "level" && "bg-gray-100 opacity-50",
)}
/>
</div>
);
}
// FormatSegmentEditor 메인 컴포넌트
interface FormatSegmentEditorProps {
label: string;
segments: FormatSegment[];
onChange: (segments: FormatSegment[]) => void;
sampleValues?: Record<string, string>;
}
export function FormatSegmentEditor({
label,
segments,
onChange,
sampleValues = SAMPLE_VALUES,
}: FormatSegmentEditorProps) {
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: { distance: 5 },
}),
useSensor(KeyboardSensor),
);
const preview = useMemo(
() => buildFormattedString(segments, sampleValues),
[segments, sampleValues],
);
const sortableIds = useMemo(
() => segments.map((seg, i) => `${seg.type}-${i}`),
[segments],
);
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event;
if (!over || active.id === over.id) return;
const oldIndex = sortableIds.indexOf(active.id as string);
const newIndex = sortableIds.indexOf(over.id as string);
if (oldIndex === -1 || newIndex === -1) return;
onChange(arrayMove([...segments], oldIndex, newIndex));
};
const handleSegmentChange = (index: number, updates: Partial<FormatSegment>) => {
const updated = segments.map((seg, i) =>
i === index ? { ...seg, ...updates } : seg,
);
onChange(updated);
};
return (
<div className="space-y-2">
<div className="text-xs font-medium text-gray-600">{label}</div>
<div className="grid grid-cols-[16px_56px_18px_1fr_1fr_1fr] items-center gap-1 px-2 text-[10px] text-gray-500">
<span />
<span />
<span />
<span></span>
<span className="text-center"></span>
<span className="text-center">릿</span>
</div>
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
>
<SortableContext items={sortableIds} strategy={verticalListSortingStrategy}>
<div className="space-y-1">
{segments.map((segment, index) => (
<SortableSegmentRow
key={sortableIds[index]}
segment={segment}
index={index}
onChange={handleSegmentChange}
/>
))}
</div>
</SortableContext>
</DndContext>
<div className="rounded bg-gray-50 px-2 py-1.5">
<span className="text-[10px] text-gray-500">: </span>
<span className="text-xs font-medium text-gray-800">
{preview || "(빈 값)"}
</span>
</div>
</div>
);
}