204 lines
5.4 KiB
TypeScript
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>
|
|
);
|
|
}
|