dnd-kit 드래그 앤 드롭 기능

Category
log-central
Status
Published
Tags
Guide
drag-and-drop
Description
Published
Slug
 

1. 필수 패키지 설치

yarn add @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilities

2. DragHandle 스타일 추가

// src/pages/CreateProject/CreateProject.style.ts export const DragHandle = styled.div` cursor: grab; display: inline-flex; align-items: center; margin-bottom: 0.5rem; padding: 0.25rem; color: #999; &:hover { color: #333; } /* 💡 모바일 대응 핵심은 센서 설정이므로 아래는 생략 가능하지만, 원하면 추가 가능 */ /* touch-action: none; */ /* user-select: none; */ `;
 

3. 센서 설정 및 Context 통합

Import

import { DndContext, closestCenter, useSensor, useSensors, MouseSensor, TouchSensor, DragEndEvent } from '@dnd-kit/core'; import { SortableContext, useSortable, verticalListSortingStrategy, arrayMove } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities';

Sensor 설정 (모바일 대응)

const sensors = useSensors( useSensor(MouseSensor), // 마우스 useSensor(TouchSensor, { activationConstraint: { delay: 100, // 100ms 이상 터치해야 드래그 시작 tolerance: 5 // 5px 이내의 움직임은 무시 (터치 오작동 방지) } }) );

handleDragEnd 함수 작성

const handleDragEnd = (event: DragEndEvent) => { const { active, over } = event; if (!over || active.id === over.id) return; const oldIndex = fields.findIndex(f => f.id === active.id); const newIndex = fields.findIndex(f => f.id === over.id); setFields(arrayMove(fields, oldIndex, newIndex)); };

4. 필드 드래그 UI 구현

DndContext & SortableContext 래핑

<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd} > <SortableContext items={fields.map(f => f.id)} strategy={verticalListSortingStrategy} > {fields.map(field => ( <SortableField key={field.id} field={field} logType={logType} onFieldChange={handleFieldChange} onRemoveField={handleRemoveField} /> ))} </SortableContext> </DndContext>

SortableField 컴포넌트 작성

function SortableField({ field, logType, onFieldChange, onRemoveField }: { field: { id: string; name: string; path: string }; logType: "json" | "plainText" | "csv" | "xml"; onFieldChange: (id: string, key: "name" | "path", value: string) => void; onRemoveField: (id: string) => void; }) { const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: field.id }); const style = { transform: CSS.Transform.toString(transform), transition, boxShadow: isDragging ? "0 2px 8px rgba(0,0,0,0.2)" : undefined, backgroundColor: isDragging ? "rgba(67,97,238,0.05)" : undefined }; return ( <FieldContainer ref={setNodeRef} style={style}> <DragHandle {...attributes} {...listeners}>☰</DragHandle> <FieldRow> <FieldLabel>필드 이름</FieldLabel> <FieldInputContainer> <input type="text" value={field.name} onChange={e => onFieldChange(field.id, "name", e.target.value)} style={{ width: "100%", padding: "0.75rem", border: "1px solid #e9ecef", borderRadius: "4px" }} placeholder="timestamp, logLevel, message 등" /> </FieldInputContainer> <RemoveButton type="button" onClick={() => onRemoveField(field.id)}> 삭제 </RemoveButton> </FieldRow> <FieldRow> <FieldLabel>{logType === "json" ? "JSON 경로" : "정규식 패턴"}</FieldLabel> <FieldInputContainer> <input type="text" value={field.path} onChange={e => onFieldChange(field.id, "path", e.target.value)} style={{ width: "100%", padding: "0.75rem", border: "1px solid #e9ecef", borderRadius: "4px" }} placeholder={logType === "json" ? "data.timestamp" : "^\\d{4}-\\d{2}-\\d{2}"} /> </FieldInputContainer> </FieldRow> </FieldContainer> ); }