- Introduced `FormatSegment` and `LocationFormatConfig` types to manage the formatting of location codes and names. - Added `defaultFormatConfig` to provide default segment configurations for location codes and names. - Implemented `buildFormattedString` function to generate formatted strings based on active segments and their configurations. - Updated `RackStructureComponent` to utilize the new formatting logic for generating location codes and names. - Enhanced `RackStructureConfigPanel` to allow users to edit format settings for location codes and names using `FormatSegmentEditor`. These changes improve the flexibility and usability of the rack structure component by allowing dynamic formatting of location identifiers.
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>
|
|
);
|
|
}
|