import { closestCenter, DndContext, KeyboardSensor, PointerSensor, useSensor, useSensors, type DragEndEvent, } from '@dnd-kit/core'; import { SortableContext, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy, } from '@dnd-kit/sortable'; import {CSS} from '@dnd-kit/utilities'; import type {SectionItem} from '@o2o/shared'; import {GripVertical, Layers, Lock, Shapes} from 'lucide-react'; import {Switch} from '@/components/ui/switch'; import {cn} from '@/lib/utils'; import {useBuilderStore} from '@/stores/builder'; import {resolveVariant} from './canvas/registry'; interface SectionListPanelProps { onAfterSelect?: () => void; } interface SortableSectionProps extends SectionListPanelProps { section: SectionItem; index: number; } function SortableSection({section, index, onAfterSelect}: SortableSectionProps) { const industry = useBuilderStore((s) => s.industry); const setRightTab = useBuilderStore((s) => s.setRightTab); const isSelected = useBuilderStore((s) => s.selectedSectionId === section.id); const selectSection = useBuilderStore((s) => s.selectSection); const toggleSection = useBuilderStore((s) => s.toggleSection); const variant = resolveVariant(section, industry); const {attributes, listeners, setNodeRef, transform, transition, isDragging} = useSortable({ id: section.id, }); const select = () => { selectSection(section.id); setRightTab('content'); onAfterSelect?.(); if (!section.isEnabled) return; // 좁은 화면에서는 onAfterSelect가 캔버스 탭을 먼저 연다. 다음 페인트가 끝난 뒤 // 실제 섹션으로 이동해야 display:none 상태의 위치를 잘못 읽지 않는다. requestAnimationFrame(() => { requestAnimationFrame(() => { document .getElementById(`canvas-section-${section.id}`) ?.scrollIntoView({behavior: 'smooth', block: 'start'}); }); }); }; return (
  • { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); select(); } }} className={cn( 'group flex cursor-pointer items-center justify-between rounded-md border p-2 text-xs transition-[background-color,border-color,opacity,box-shadow]', isDragging && 'cursor-grabbing opacity-70 shadow-lg', isSelected ? 'border-primary bg-primary/8 font-semibold text-foreground' : section.isEnabled ? 'border-border bg-card hover:bg-muted/60' : 'border-border bg-muted/40 text-muted-foreground', )} >
    {index + 1}
    {section.name} {variant && ( { event.stopPropagation(); selectSection(section.id); setRightTab('design'); onAfterSelect?.(); }} onKeyDown={(event) => { if (event.key !== 'Enter' && event.key !== ' ') return; event.preventDefault(); event.stopPropagation(); selectSection(section.id); setRightTab('design'); onAfterSelect?.(); }} className="flex cursor-pointer items-center gap-0.5 truncate text-[9px] font-normal text-muted-foreground hover:text-foreground" > {variant.name} )} {section.isLocked && ( 필수 고정 )}
    event.stopPropagation()} role="presentation"> {section.isLocked ? ( ) : ( toggleSection(section.id)} aria-label={`${section.name} 섹션 표시 여부`} /> )}
  • ); } export function SectionListPanel({onAfterSelect}: SectionListPanelProps) { const sections = useBuilderStore((s) => s.sections); const reorderSection = useBuilderStore((s) => s.reorderSection); const sensors = useSensors( useSensor(PointerSensor, {activationConstraint: {distance: 6}}), useSensor(KeyboardSensor, {coordinateGetter: sortableKeyboardCoordinates}), ); const activeCount = sections.filter((section) => section.isEnabled).length; const handleDragEnd = ({active, over}: DragEndEvent) => { if (!over || active.id === over.id) return; const fromIndex = sections.findIndex((section) => section.id === active.id); const toIndex = sections.findIndex((section) => section.id === over.id); reorderSection(fromIndex, toIndex); }; return (
    섹션 관리
    {activeCount} / {sections.length} 활성

    핸들을 드래그해 순서를 바꾸고, 레이아웃 이름을 누르면 모양을 바꿉니다.

    section.id)} strategy={verticalListSortingStrategy}>
      {sections.map((section, index) => ( ))}

    * 자물쇠 항목은 검색·AI 노출에 필요한 마크업이 달려 있어 고정입니다.

    ); }