import {
  AppstoreOutlined,
  BgColorsOutlined,
  BorderOutlined,
  CopyOutlined,
  DeleteOutlined,
  DisconnectOutlined,
  DragOutlined,
  EyeInvisibleOutlined,
  FolderOpenOutlined,
  InfoCircleOutlined,
  LineOutlined,
  LinkOutlined,
  MenuFoldOutlined,
  MenuUnfoldOutlined,
  PlusOutlined,
  PushpinOutlined,
  RedoOutlined,
  SaveOutlined,
  UndoOutlined,
} from '@ant-design/icons';
import { Button, Card, Empty, Input, Modal, Spin, Tabs, Typography, message } from 'antd';
import { useEffect, useMemo, useRef, useState } from 'react';
import {
  commitLayoutDrawHistory,
  createLayoutDrawHistoryState,
  redoLayoutDrawHistory,
  undoLayoutDrawHistory,
} from './layoutDrawHistory';
import { findRegionAtPoint, resolveDrawRegions, splitDrawShapes } from './layoutDrawRegions';
import { duplicateShapeWithLabel } from './layoutDrawShapeUtils';
import {
  deleteLayoutDrawComponent,
  listSavedLayoutDrawComponents,
  saveLayoutDrawComponent,
} from './layoutDrawComponentStorage';
import {
  clampSelectionRect,
  findShapesInSelection,
  rebaseShapesToComponentBlueprint,
  resolveGroupedShapeIds,
} from './layoutDrawSelectionUtils';
import { deleteLayoutDraw, listSavedLayoutDraws, saveLayoutDraw } from './layoutDrawStorage';
import type {
  BackgroundMode,
  DrawLine,
  DrawRect,
  DrawRegion,
  DrawShape,
  DrawTool,
  DrawableShape,
  SavedLayoutDrawComponentRecord,
  SavedLayoutDrawRecord,
} from './layoutDrawTypes';
import { resolveLineDraft, type LineOrientation } from './lineDraft';
import './LayoutDrawPage.css';

type DraftState =
  | null
  | {
      type: 'line';
      startX: number;
      startY: number;
      x1: number;
      y1: number;
      orientation: LineOrientation | null;
      x2: number;
      y2: number;
    }
  | {
      type: 'rect';
      startX: number;
      startY: number;
      endX: number;
      endY: number;
    };

type DragState =
  | null
  | {
      shapeIds: string[];
      pointerX: number;
      pointerY: number;
    };

type SelectionBoxState =
  | null
  | {
      startX: number;
      startY: number;
      endX: number;
      endY: number;
      additive: boolean;
    };

type ResizeHandle =
  | 'n'
  | 's'
  | 'e'
  | 'w'
  | 'ne'
  | 'nw'
  | 'se'
  | 'sw'
  | 'line-start'
  | 'line-end';

type ResizeState =
  | null
  | {
      shapeId: string;
      handle: ResizeHandle;
      originX: number;
      originY: number;
      baseShape: DrawableShape;
      groupedIds: Set<string>;
      baseShapes: DrawableShape[];
      bounds: {
        x: number;
        y: number;
        width: number;
        height: number;
      };
    };

type SelectedTarget = null | { kind: 'shape'; id: string } | { kind: 'region'; regionKey: string };

type CanvasSize = {
  width: number;
  height: number;
};

type FloatingBoxKey = 'tools' | 'actions' | 'guide';

type FloatingBoxPosition = {
  x: number;
  y: number;
};

type FloatingBoxSize = {
  width: number;
  height: number;
};

type AttachableToolbarKey = 'tools' | 'actions';

const DRAW_LINE_HIT_TOLERANCE = 12;
const DRAW_RECT_HIT_PADDING = 8;
const DRAW_LINE_ORIENTATION_LOCK_DISTANCE = 10;
const DRAW_RECT_MIN_SIZE = 24;
const DRAW_RESIZE_HANDLE_SIZE = 10;
const ATTACHED_TOOLBAR_IDLE_HIDE_MS = 12000;
const RECT_DEFAULT_FILL_COLOR = 'rgba(147, 197, 253, 0.22)';
const PAINT_COLOR_OPTIONS = [
  { value: '#f9e2d2', label: '피치' },
  { value: '#f7edc6', label: '버터' },
  { value: '#d8e9fb', label: '스카이' },
  { value: '#d8f2e1', label: '민트' },
  { value: '#f8d8e5', label: '로즈' },
  { value: '#e7ddfb', label: '라일락' },
  { value: 'transparent', label: '지우기' },
] as const;
const DRAW_TOOL_OPTIONS = [
  { key: 'select', icon: <DragOutlined /> },
  { key: 'line', icon: <LineOutlined /> },
  { key: 'rect', icon: <BorderOutlined /> },
  { key: 'paint', icon: <BgColorsOutlined /> },
] as const;

function resolveRectResizeHandles(shape: DrawRect) {
  const left = shape.x;
  const right = shape.x + shape.width;
  const top = shape.y;
  const bottom = shape.y + shape.height;
  const midX = shape.x + shape.width / 2;
  const midY = shape.y + shape.height / 2;

  return [
    { handle: 'nw' as const, x: left, y: top, cursor: 'nwse-resize' },
    { handle: 'n' as const, x: midX, y: top, cursor: 'ns-resize' },
    { handle: 'ne' as const, x: right, y: top, cursor: 'nesw-resize' },
    { handle: 'e' as const, x: right, y: midY, cursor: 'ew-resize' },
    { handle: 'se' as const, x: right, y: bottom, cursor: 'nwse-resize' },
    { handle: 's' as const, x: midX, y: bottom, cursor: 'ns-resize' },
    { handle: 'sw' as const, x: left, y: bottom, cursor: 'nesw-resize' },
    { handle: 'w' as const, x: left, y: midY, cursor: 'ew-resize' },
  ];
}

function resolveLineResizeHandles(shape: DrawLine) {
  return [
    {
      handle: 'line-start' as const,
      x: shape.x1,
      y: shape.y1,
      cursor: shape.orientation === 'horizontal' ? 'ew-resize' : 'ns-resize',
    },
    {
      handle: 'line-end' as const,
      x: shape.x2,
      y: shape.y2,
      cursor: shape.orientation === 'horizontal' ? 'ew-resize' : 'ns-resize',
    },
  ];
}

function createShapeId(prefix: string) {
  if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
    return `${prefix}-${crypto.randomUUID()}`;
  }

  return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
}

function clampRect(startX: number, startY: number, endX: number, endY: number) {
  const x = Math.min(startX, endX);
  const y = Math.min(startY, endY);
  const width = Math.abs(endX - startX);
  const height = Math.abs(endY - startY);
  return { x, y, width, height };
}

function resolveCanvasSize(element: SVGSVGElement): CanvasSize {
  const rect = element.getBoundingClientRect();
  if (rect.width > 0 && rect.height > 0) {
    return {
      width: rect.width,
      height: rect.height,
    };
  }

  const viewBox = element.viewBox.baseVal;
  if (viewBox && viewBox.width > 0 && viewBox.height > 0) {
    return {
      width: viewBox.width,
      height: viewBox.height,
    };
  }

  return {
    width: rect.width || element.clientWidth || 1,
    height: rect.height || element.clientHeight || 1,
  };
}

function resolveSvgPoint(event: React.PointerEvent<SVGSVGElement>, element: SVGSVGElement) {
  const rect = element.getBoundingClientRect();
  const canvasSize = resolveCanvasSize(element);
  return {
    x: ((event.clientX - rect.left) / Math.max(rect.width, 1)) * canvasSize.width,
    y: ((event.clientY - rect.top) / Math.max(rect.height, 1)) * canvasSize.height,
  };
}

function moveShape(shape: DrawableShape, dx: number, dy: number): DrawableShape {
  if (shape.type === 'line') {
    return {
      ...shape,
      x1: shape.x1 + dx,
      y1: shape.y1 + dy,
      x2: shape.x2 + dx,
      y2: shape.y2 + dy,
    };
  }

  return {
    ...shape,
    x: shape.x + dx,
    y: shape.y + dy,
  };
}

function clampSize(value: number, minimum: number) {
  return Math.max(minimum, value);
}

function resizeLine(shape: DrawLine, handle: ResizeHandle, pointerX: number, pointerY: number): DrawLine {
  if (shape.orientation === 'horizontal') {
    if (handle === 'line-start') {
      const nextX1 = Math.min(pointerX, shape.x2 - 1);
      return {
        ...shape,
        x1: nextX1,
        y1: shape.y2,
      };
    }

    const nextX2 = Math.max(pointerX, shape.x1 + 1);
    return {
      ...shape,
      x2: nextX2,
      y2: shape.y1,
    };
  }

  if (handle === 'line-start') {
    const nextY1 = Math.min(pointerY, shape.y2 - 1);
    return {
      ...shape,
      x1: shape.x2,
      y1: nextY1,
    };
  }

  const nextY2 = Math.max(pointerY, shape.y1 + 1);
  return {
    ...shape,
    x2: shape.x1,
    y2: nextY2,
  };
}

function resizeRect(shape: DrawRect, handle: ResizeHandle, pointerX: number, pointerY: number): DrawRect {
  const left = shape.x;
  const right = shape.x + shape.width;
  const top = shape.y;
  const bottom = shape.y + shape.height;

  let nextLeft = left;
  let nextRight = right;
  let nextTop = top;
  let nextBottom = bottom;

  if (handle.includes('w')) {
    nextLeft = Math.min(pointerX, right - DRAW_RECT_MIN_SIZE);
  }
  if (handle.includes('e')) {
    nextRight = Math.max(pointerX, left + DRAW_RECT_MIN_SIZE);
  }
  if (handle.includes('n')) {
    nextTop = Math.min(pointerY, bottom - DRAW_RECT_MIN_SIZE);
  }
  if (handle.includes('s')) {
    nextBottom = Math.max(pointerY, top + DRAW_RECT_MIN_SIZE);
  }

  return {
    ...shape,
    x: nextLeft,
    y: nextTop,
    width: clampSize(nextRight - nextLeft, DRAW_RECT_MIN_SIZE),
    height: clampSize(nextBottom - nextTop, DRAW_RECT_MIN_SIZE),
  };
}

function resizeShape(shape: DrawableShape, handle: ResizeHandle, pointerX: number, pointerY: number): DrawableShape {
  if (shape.type === 'line') {
    return resizeLine(shape, handle, pointerX, pointerY);
  }

  return resizeRect(shape, handle, pointerX, pointerY);
}

function resolveShapeBounds(shape: DrawableShape) {
  if (shape.type === 'line') {
    return {
      x: Math.min(shape.x1, shape.x2),
      y: Math.min(shape.y1, shape.y2),
      width: Math.max(1, Math.abs(shape.x2 - shape.x1)),
      height: Math.max(1, Math.abs(shape.y2 - shape.y1)),
    };
  }

  return {
    x: shape.x,
    y: shape.y,
    width: shape.width,
    height: shape.height,
  };
}

function resolveDrawableShapesBounds(shapes: DrawableShape[]) {
  if (shapes.length === 0) {
    return null;
  }

  const firstBounds = resolveShapeBounds(shapes[0]);
  let left = firstBounds.x;
  let top = firstBounds.y;
  let right = firstBounds.x + firstBounds.width;
  let bottom = firstBounds.y + firstBounds.height;

  shapes.slice(1).forEach((shape) => {
    const bounds = resolveShapeBounds(shape);
    left = Math.min(left, bounds.x);
    top = Math.min(top, bounds.y);
    right = Math.max(right, bounds.x + bounds.width);
    bottom = Math.max(bottom, bounds.y + bounds.height);
  });

  return {
    x: left,
    y: top,
    width: Math.max(1, right - left),
    height: Math.max(1, bottom - top),
  };
}

function resizeGroupedShapes(
  shapes: DrawableShape[],
  bounds: { x: number; y: number; width: number; height: number },
  handle: ResizeHandle,
  pointerX: number,
  pointerY: number,
) {
  const nextBounds = resizeRect(
    {
      id: 'group-resize-frame',
      type: 'rect',
      x: bounds.x,
      y: bounds.y,
      width: bounds.width,
      height: bounds.height,
      label: '',
    },
    handle,
    pointerX,
    pointerY,
  );
  const scaleX = nextBounds.width / Math.max(bounds.width, 1);
  const scaleY = nextBounds.height / Math.max(bounds.height, 1);

  return shapes.map((shape) => {
    if (shape.type === 'line') {
      return {
        ...shape,
        x1: nextBounds.x + (shape.x1 - bounds.x) * scaleX,
        y1: nextBounds.y + (shape.y1 - bounds.y) * scaleY,
        x2: nextBounds.x + (shape.x2 - bounds.x) * scaleX,
        y2: nextBounds.y + (shape.y2 - bounds.y) * scaleY,
      };
    }

    return {
      ...shape,
      x: nextBounds.x + (shape.x - bounds.x) * scaleX,
      y: nextBounds.y + (shape.y - bounds.y) * scaleY,
      width: Math.max(8, shape.width * scaleX),
      height: Math.max(8, shape.height * scaleY),
    };
  });
}

function resolveRectFillColor(shape: DrawRect) {
  if (!shape.fillColor || shape.fillColor === 'transparent') {
    return RECT_DEFAULT_FILL_COLOR;
  }

  return shape.fillColor;
}

function resolveLineLabelPosition(line: DrawLine) {
  return {
    x: (line.x1 + line.x2) / 2,
    y: (line.y1 + line.y2) / 2,
  };
}

function resolveRectLabelPosition(shape: DrawRect) {
  return {
    x: shape.x + shape.width / 2,
    y: shape.y + shape.height / 2,
  };
}

function isPointNearLine(shape: DrawLine, x: number, y: number, tolerance = DRAW_LINE_HIT_TOLERANCE) {
  if (shape.orientation === 'horizontal') {
    const minX = Math.min(shape.x1, shape.x2) - tolerance;
    const maxX = Math.max(shape.x1, shape.x2) + tolerance;
    return x >= minX && x <= maxX && Math.abs(y - shape.y1) <= tolerance;
  }

  const minY = Math.min(shape.y1, shape.y2) - tolerance;
  const maxY = Math.max(shape.y1, shape.y2) + tolerance;
  return y >= minY && y <= maxY && Math.abs(x - shape.x1) <= tolerance;
}

function isPointInsideRect(shape: DrawRect, x: number, y: number, padding = DRAW_RECT_HIT_PADDING) {
  return (
    x >= shape.x - padding &&
    x <= shape.x + shape.width + padding &&
    y >= shape.y - padding &&
    y <= shape.y + shape.height + padding
  );
}

function findShapeAtPoint(shapes: DrawableShape[], x: number, y: number) {
  for (let index = shapes.length - 1; index >= 0; index -= 1) {
    const shape = shapes[index];
    const isHit =
      shape.type === 'line' ? isPointNearLine(shape, x, y) : isPointInsideRect(shape, x, y);

    if (isHit) {
      return shape;
    }
  }

  return null;
}

const DRAW_HISTORY_GUARD_KEY = '__layoutDrawBackGestureGuard';
const DRAW_EDGE_GESTURE_HOTZONE_PX = 28;
const DRAW_EDGE_GESTURE_MIN_SWIPE_PX = 10;
const DRAW_EDGE_GESTURE_MAX_VERTICAL_DRIFT_PX = 56;
const DRAW_BACKGROUND_MODE_STORAGE_KEY = 'layout-draw-background-mode';
const INITIAL_BACKGROUND_MODE: BackgroundMode = 'grid';
const MOBILE_LAYOUT_BREAKPOINT_PX = 768;
const FLOATING_BOX_BASE_Z_INDEX = 8;
const COMPONENT_LIBRARY_SECTIONS = [
  {
    key: 'featured',
    label: '추천 화면',
    presetIds: ['filter-toolbar', 'content-card', 'stats-summary', 'detail-panel'],
  },
  {
    key: 'form',
    label: '폼 · 상태',
    presetIds: ['section-header', 'field-row', 'segmented-switch', 'chip-filter-row'],
  },
  {
    key: 'action',
    label: '액션 · 탐색',
    presetIds: ['title-with-actions', 'primary-button', 'button-row', 'footer-actions', 'toggle-button'],
  },
] as const;

type ComponentLibrarySectionKey = (typeof COMPONENT_LIBRARY_SECTIONS)[number]['key'] | 'saved';

type ComponentPreviewKind =
  | 'single-button'
  | 'button-row'
  | 'footer-actions'
  | 'button-space'
  | 'toggle-button'
  | 'segmented-switch'
  | 'chip-filter-row'
  | 'title-label'
  | 'section-header'
  | 'title-with-actions'
  | 'field-row'
  | 'content-card'
  | 'filter-toolbar'
  | 'stats-summary'
  | 'detail-panel';

type ComponentLibraryItem = {
  presetId: string;
  title: string;
  description: string;
  category: string;
  previewKind: ComponentPreviewKind;
  blueprints: DrawableShape[];
  source?: 'preset' | 'saved';
  savedRecordId?: string;
};

const COMPONENT_LIBRARY_PRESETS = [
  {
    presetId: 'primary-button',
    title: '기본 버튼',
    description: '단일 CTA 버튼을 빠르게 깔아 두고 길이, 강조색, 라벨만 바로 조정할 때 씁니다.',
    category: '버튼/액션',
    previewKind: 'single-button',
    blueprints: [{ type: 'rect', x: 0, y: 0, width: 168, height: 44, label: '버튼', fillColor: '#d8e9fb' }],
  },
  {
    presetId: 'button-row',
    title: '버튼 묶음',
    description: '나란한 액션 2개를 한 번에 배치할 때 쓰는 기본 간격 프리셋입니다.',
    category: '버튼/액션',
    previewKind: 'button-row',
    blueprints: [
      { type: 'rect', x: 0, y: 0, width: 112, height: 44, label: '취소' },
      { type: 'rect', x: 128, y: 0, width: 112, height: 44, label: '확인', fillColor: '#d8e9fb' },
    ],
  },
  {
    presetId: 'footer-actions',
    title: '하단 액션바',
    description: '취소/저장 같은 하단 고정 액션 블록을 도면 하단에 바로 깔 수 있습니다.',
    category: '버튼/액션',
    previewKind: 'footer-actions',
    blueprints: [
      { type: 'rect', x: 0, y: 0, width: 304, height: 68, label: '하단 액션 영역' },
      { type: 'rect', x: 20, y: 12, width: 120, height: 44, label: '취소' },
      { type: 'rect', x: 164, y: 12, width: 120, height: 44, label: '저장', fillColor: '#d8e9fb' },
    ],
  },
  {
    presetId: 'button-space',
    title: '버튼 공간',
    description: '추후 버튼을 넣을 자리만 먼저 잡아 두고 여백 구조를 본 뒤 채우는 용도입니다.',
    category: '영역/레이아웃',
    previewKind: 'button-space',
    blueprints: [{ type: 'rect', x: 0, y: 0, width: 224, height: 60, label: '버튼 공간', fillColor: '#f8f8f2' }],
  },
  {
    presetId: 'toggle-button',
    title: '토글 버튼',
    description: '켜짐/꺼짐이나 보기 전환처럼 2상태 액션을 직관적으로 배치할 때 적합합니다.',
    category: '토글/선택',
    previewKind: 'toggle-button',
    blueprints: [
      { type: 'rect', x: 0, y: 0, width: 112, height: 44, label: 'ON', fillColor: '#d8f2e1' },
      { type: 'rect', x: 112, y: 0, width: 112, height: 44, label: 'OFF' },
    ],
  },
  {
    presetId: 'segmented-switch',
    title: '분할 선택',
    description: '리스트 상단 보기 전환처럼 텍스트 중심 상태 전환 바를 빠르게 배치합니다.',
    category: '토글/선택',
    previewKind: 'segmented-switch',
    blueprints: [
      { type: 'rect', x: 0, y: 0, width: 312, height: 52, label: '상단 전환 바', fillColor: '#ffffff' },
      { type: 'rect', x: 20, y: 12, width: 64, height: 20, label: '전체', fillColor: '#f8fafc' },
      { type: 'rect', x: 124, y: 12, width: 64, height: 20, label: '웹', fillColor: '#f8fafc' },
      { type: 'rect', x: 228, y: 12, width: 64, height: 20, label: '앱', fillColor: '#f8fafc' },
      { type: 'rect', x: 124, y: 38, width: 64, height: 2, label: '', fillColor: '#93c5fd' },
    ],
  },
  {
    presetId: 'chip-filter-row',
    title: '필터 칩 묶음',
    description: '상태 필터와 건수 배지가 함께 있는 필터 바를 빠르게 깔 수 있습니다.',
    category: '토글/선택',
    previewKind: 'chip-filter-row',
    blueprints: [
      { type: 'rect', x: 0, y: 0, width: 94, height: 32, label: '전체 24', fillColor: '#dbeafe' },
      { type: 'rect', x: 106, y: 0, width: 108, height: 32, label: '진행중 12', fillColor: '#ffffff' },
      { type: 'rect', x: 226, y: 0, width: 94, height: 32, label: '대기 4', fillColor: '#ffffff' },
    ],
  },
  {
    presetId: 'title-label',
    title: '타이틀 라벨',
    description: '섹션명이나 카드 헤더용 라벨을 바로 올려 둘 수 있는 최소 단위입니다.',
    category: '타이틀/라벨',
    previewKind: 'title-label',
    blueprints: [{ type: 'rect', x: 0, y: 0, width: 196, height: 36, label: '타이틀 라벨', fillColor: '#f7edc6' }],
  },
  {
    presetId: 'section-header',
    title: '섹션 헤더',
    description: '업무 화면 상단 제목, 설명, 우측 액션까지 포함한 헤더를 한 번에 배치합니다.',
    category: '타이틀/라벨',
    previewKind: 'section-header',
    blueprints: [
      { type: 'rect', x: 0, y: 0, width: 328, height: 78, label: '섹션 헤더', fillColor: '#ffffff' },
      { type: 'rect', x: 20, y: 18, width: 136, height: 22, label: '섹션 제목', fillColor: '#fef3c7' },
      { type: 'rect', x: 20, y: 48, width: 164, height: 14, label: '보조 설명', fillColor: '#f8fafc' },
      { type: 'rect', x: 236, y: 16, width: 72, height: 32, label: '추가', fillColor: '#dbeafe' },
    ],
  },
  {
    presetId: 'title-with-actions',
    title: '타이틀 + 액션',
    description: '업무 목록 헤더와 우측 등록 버튼을 카드형 헤더로 바로 배치합니다.',
    category: '타이틀/라벨',
    previewKind: 'title-with-actions',
    blueprints: [
      { type: 'rect', x: 0, y: 0, width: 320, height: 60, label: '목록 헤더', fillColor: '#ffffff' },
      { type: 'rect', x: 18, y: 16, width: 148, height: 20, label: '프로젝트 목록', fillColor: '#fef3c7' },
      { type: 'rect', x: 232, y: 12, width: 70, height: 32, label: '등록', fillColor: '#dbeafe' },
    ],
  },
  {
    presetId: 'field-row',
    title: '라벨 + 입력 영역',
    description: '입력 라벨과 필드, 도움 문구까지 포함한 폼 행을 빠르게 만듭니다.',
    category: '영역/레이아웃',
    previewKind: 'field-row',
    blueprints: [
      { type: 'rect', x: 0, y: 0, width: 360, height: 84, label: '폼 행', fillColor: '#ffffff' },
      { type: 'rect', x: 18, y: 18, width: 88, height: 18, label: '이름', fillColor: '#fef3c7' },
      { type: 'rect', x: 122, y: 12, width: 220, height: 34, label: '입력 값', fillColor: '#ffffff' },
      { type: 'rect', x: 122, y: 56, width: 146, height: 12, label: '설명', fillColor: '#f8fafc' },
    ],
  },
  {
    presetId: 'filter-toolbar',
    title: '업무 목록 헤더',
    description: '검색, 필터, 요약 탭, 우측 CTA까지 들어간 실무형 목록 헤더 화면을 바로 깔아 둡니다.',
    category: '추천 화면',
    previewKind: 'filter-toolbar',
    blueprints: [
      { type: 'rect', x: 0, y: 0, width: 396, height: 212, label: '', fillColor: '#ffffff' },
      { type: 'rect', x: 0, y: 0, width: 396, height: 44, label: '', fillColor: '#f8fafc' },
      { type: 'rect', x: 20, y: 16, width: 112, height: 12, label: '', fillColor: '#0f172a' },
      { type: 'rect', x: 302, y: 10, width: 74, height: 24, label: '등록', fillColor: '#dbeafe' },
      { type: 'rect', x: 20, y: 62, width: 184, height: 14, label: '', fillColor: '#1e293b' },
      { type: 'rect', x: 20, y: 84, width: 118, height: 8, label: '', fillColor: '#cbd5e1' },
      { type: 'rect', x: 20, y: 112, width: 174, height: 34, label: '', fillColor: '#f8fafc' },
      { type: 'rect', x: 206, y: 112, width: 72, height: 34, label: '', fillColor: '#f8fafc' },
      { type: 'rect', x: 290, y: 112, width: 86, height: 34, label: '', fillColor: '#f8fafc' },
      { type: 'rect', x: 20, y: 164, width: 76, height: 26, label: '', fillColor: '#e2e8f0' },
      { type: 'rect', x: 108, y: 164, width: 92, height: 26, label: '', fillColor: '#eff6ff' },
      { type: 'rect', x: 212, y: 164, width: 82, height: 26, label: '', fillColor: '#e2e8f0' },
      { type: 'rect', x: 306, y: 164, width: 44, height: 26, label: '', fillColor: '#e2e8f0' },
    ],
  },
  {
    presetId: 'stats-summary',
    title: '대시보드 요약',
    description: '수치 카드와 추이 바가 섞인 대시보드 상단 영역을 디자인된 상태로 바로 배치합니다.',
    category: '추천 화면',
    previewKind: 'stats-summary',
    blueprints: [
      { type: 'rect', x: 0, y: 0, width: 396, height: 226, label: '', fillColor: '#ffffff' },
      { type: 'rect', x: 20, y: 18, width: 108, height: 12, label: '', fillColor: '#0f172a' },
      { type: 'rect', x: 20, y: 42, width: 76, height: 8, label: '', fillColor: '#cbd5e1' },
      { type: 'rect', x: 20, y: 76, width: 110, height: 122, label: '', fillColor: '#eff6ff' },
      { type: 'rect', x: 143, y: 76, width: 110, height: 122, label: '', fillColor: '#ecfdf5' },
      { type: 'rect', x: 266, y: 76, width: 110, height: 122, label: '', fillColor: '#fff7ed' },
      { type: 'rect', x: 36, y: 96, width: 48, height: 8, label: '', fillColor: '#94a3b8' },
      { type: 'rect', x: 36, y: 118, width: 58, height: 20, label: '', fillColor: '#0f172a' },
      { type: 'rect', x: 36, y: 156, width: 58, height: 18, label: '', fillColor: '#bfdbfe' },
      { type: 'rect', x: 159, y: 96, width: 48, height: 8, label: '', fillColor: '#94a3b8' },
      { type: 'rect', x: 159, y: 118, width: 62, height: 20, label: '', fillColor: '#0f172a' },
      { type: 'rect', x: 159, y: 156, width: 58, height: 18, label: '', fillColor: '#bbf7d0' },
      { type: 'rect', x: 282, y: 96, width: 48, height: 8, label: '', fillColor: '#94a3b8' },
      { type: 'rect', x: 282, y: 118, width: 42, height: 20, label: '', fillColor: '#0f172a' },
      { type: 'rect', x: 282, y: 156, width: 58, height: 18, label: '', fillColor: '#fed7aa' },
    ],
  },
  {
    presetId: 'content-card',
    title: '업무 카드',
    description: '썸네일, 상태, 설명, CTA가 같이 있는 실무형 카드 블록을 바로 배치합니다.',
    category: '추천 화면',
    previewKind: 'content-card',
    blueprints: [
      { type: 'rect', x: 0, y: 0, width: 348, height: 220, label: '', fillColor: '#ffffff' },
      { type: 'rect', x: 18, y: 18, width: 84, height: 82, label: '', fillColor: '#dbeafe' },
      { type: 'rect', x: 120, y: 18, width: 62, height: 10, label: '', fillColor: '#cbd5e1' },
      { type: 'rect', x: 120, y: 38, width: 146, height: 16, label: '', fillColor: '#0f172a' },
      { type: 'rect', x: 120, y: 64, width: 182, height: 8, label: '', fillColor: '#cbd5e1' },
      { type: 'rect', x: 120, y: 80, width: 164, height: 8, label: '', fillColor: '#cbd5e1' },
      { type: 'rect', x: 18, y: 122, width: 312, height: 1, label: '', fillColor: '#e2e8f0' },
      { type: 'rect', x: 18, y: 142, width: 66, height: 24, label: '', fillColor: '#f1f5f9' },
      { type: 'rect', x: 94, y: 142, width: 74, height: 24, label: '', fillColor: '#f1f5f9' },
      { type: 'rect', x: 236, y: 138, width: 94, height: 32, label: '열기', fillColor: '#dbeafe' },
      { type: 'rect', x: 18, y: 186, width: 132, height: 10, label: '', fillColor: '#cbd5e1' },
    ],
  },
  {
    presetId: 'detail-panel',
    title: '상세 사이드 패널',
    description: '헤더, 본문, 메타, 하단 액션까지 정리된 우측 상세 패널을 화면형 구조로 배치합니다.',
    category: '추천 화면',
    previewKind: 'detail-panel',
    blueprints: [
      { type: 'rect', x: 0, y: 0, width: 356, height: 286, label: '', fillColor: '#ffffff' },
      { type: 'rect', x: 0, y: 0, width: 356, height: 54, label: '', fillColor: '#f8fafc' },
      { type: 'rect', x: 20, y: 20, width: 122, height: 12, label: '', fillColor: '#0f172a' },
      { type: 'rect', x: 20, y: 72, width: 316, height: 96, label: '', fillColor: '#f8fafc' },
      { type: 'rect', x: 20, y: 186, width: 54, height: 8, label: '', fillColor: '#94a3b8' },
      { type: 'rect', x: 92, y: 178, width: 68, height: 22, label: '', fillColor: '#dbeafe' },
      { type: 'rect', x: 20, y: 220, width: 54, height: 8, label: '', fillColor: '#94a3b8' },
      { type: 'rect', x: 92, y: 212, width: 82, height: 22, label: '', fillColor: '#f1f5f9' },
      { type: 'rect', x: 20, y: 252, width: 112, height: 1, label: '', fillColor: '#e2e8f0' },
      { type: 'rect', x: 236, y: 244, width: 100, height: 30, label: '확인', fillColor: '#dbeafe' },
    ],
  },
] as const satisfies readonly ComponentLibraryItem[];

const { Paragraph, Text } = Typography;

function resolveBlueprintBounds(blueprints: DrawableShape[]) {
  const right = Math.max(
    ...blueprints.map((blueprint) => (blueprint.type === 'line' ? Math.max(blueprint.x1, blueprint.x2) : blueprint.x + blueprint.width)),
  );
  const bottom = Math.max(
    ...blueprints.map((blueprint) => (blueprint.type === 'line' ? Math.max(blueprint.y1, blueprint.y2) : blueprint.y + blueprint.height)),
  );
  return { width: right, height: bottom };
}

function renderComponentPreview(kind: ComponentPreviewKind) {
  if (kind === 'single-button') {
    return (
      <div className="layout-draw-page__component-preview-shot layout-draw-page__component-preview-shot--cta">
        <div className="layout-draw-page__component-preview-shot-topbar">
          <span className="layout-draw-page__component-preview-eyebrow">Quick Action</span>
        </div>
        <div className="layout-draw-page__component-preview-hero-button">승인 요청</div>
      </div>
    );
  }

  if (kind === 'button-row') {
    return (
      <div className="layout-draw-page__component-preview-shot layout-draw-page__component-preview-shot--inline-actions">
        <div className="layout-draw-page__component-preview-shot-topbar">
          <span className="layout-draw-page__component-preview-eyebrow">Modal Footer</span>
        </div>
        <div className="layout-draw-page__component-preview-row layout-draw-page__component-preview-row--end">
          <div className="layout-draw-page__component-preview-button">닫기</div>
          <div className="layout-draw-page__component-preview-button layout-draw-page__component-preview-button--primary">저장</div>
        </div>
      </div>
    );
  }

  if (kind === 'footer-actions') {
    return (
      <div className="layout-draw-page__component-preview-shot layout-draw-page__component-preview-shot--panel">
        <div className="layout-draw-page__component-preview-text-block">
          <div className="layout-draw-page__component-preview-text-line layout-draw-page__component-preview-text-line--title" />
          <div className="layout-draw-page__component-preview-text-line" />
          <div className="layout-draw-page__component-preview-text-line layout-draw-page__component-preview-text-line--short" />
        </div>
        <div className="layout-draw-page__component-preview-row">
          <div className="layout-draw-page__component-preview-button">취소</div>
          <div className="layout-draw-page__component-preview-button layout-draw-page__component-preview-button--primary">저장</div>
        </div>
      </div>
    );
  }

  if (kind === 'button-space') {
    return (
      <div className="layout-draw-page__component-preview-shot layout-draw-page__component-preview-shot--placeholder">
        <div className="layout-draw-page__component-preview-space">버튼 영역 예약</div>
      </div>
    );
  }

  if (kind === 'toggle-button') {
    return (
      <div className="layout-draw-page__component-preview-shot layout-draw-page__component-preview-shot--toolbar">
        <div className="layout-draw-page__component-preview-shot-topbar">
          <span className="layout-draw-page__component-preview-eyebrow">Mode</span>
        </div>
        <div className="layout-draw-page__component-preview-row layout-draw-page__component-preview-row--tight">
          <div className="layout-draw-page__component-preview-segment layout-draw-page__component-preview-segment--active">편집</div>
          <div className="layout-draw-page__component-preview-segment">미리보기</div>
        </div>
      </div>
    );
  }

  if (kind === 'segmented-switch') {
    return (
      <div className="layout-draw-page__component-preview-shot layout-draw-page__component-preview-shot--tabbar">
        <div className="layout-draw-page__component-preview-tabbar">
          <span>전체</span>
          <span className="layout-draw-page__component-preview-tabbar-active">웹</span>
          <span>앱</span>
        </div>
      </div>
    );
  }

  if (kind === 'chip-filter-row') {
    return (
      <div className="layout-draw-page__component-preview-shot layout-draw-page__component-preview-shot--toolbar">
        <div className="layout-draw-page__component-preview-filter-pills">
          <div className="layout-draw-page__component-preview-chip layout-draw-page__component-preview-chip--active">전체 24</div>
          <div className="layout-draw-page__component-preview-chip">진행중 12</div>
          <div className="layout-draw-page__component-preview-chip">대기 4</div>
        </div>
      </div>
    );
  }

  if (kind === 'title-label') {
    return (
      <div className="layout-draw-page__component-preview-shot layout-draw-page__component-preview-shot--header">
        <div className="layout-draw-page__component-preview-title">타이틀 라벨</div>
      </div>
    );
  }

  if (kind === 'section-header') {
    return (
      <div className="layout-draw-page__component-preview-shot layout-draw-page__component-preview-shot--header">
        <div className="layout-draw-page__component-preview-row">
          <div className="layout-draw-page__component-preview-column layout-draw-page__component-preview-column--grow">
            <div className="layout-draw-page__component-preview-title">섹션 제목</div>
            <div className="layout-draw-page__component-preview-caption">보조 설명</div>
          </div>
          <div className="layout-draw-page__component-preview-button layout-draw-page__component-preview-button--compact layout-draw-page__component-preview-button--primary">추가</div>
        </div>
      </div>
    );
  }

  if (kind === 'title-with-actions') {
    return (
      <div className="layout-draw-page__component-preview-shot layout-draw-page__component-preview-shot--header">
        <div className="layout-draw-page__component-preview-row">
          <div className="layout-draw-page__component-preview-title layout-draw-page__component-preview-title--flex">프로젝트 목록</div>
          <div className="layout-draw-page__component-preview-button layout-draw-page__component-preview-button--compact layout-draw-page__component-preview-button--primary">등록</div>
        </div>
      </div>
    );
  }

  if (kind === 'field-row') {
    return (
      <div className="layout-draw-page__component-preview-shot layout-draw-page__component-preview-shot--form">
        <div className="layout-draw-page__component-preview-row layout-draw-page__component-preview-row--form">
          <div className="layout-draw-page__component-preview-title layout-draw-page__component-preview-title--label">담당자</div>
          <div className="layout-draw-page__component-preview-column layout-draw-page__component-preview-column--grow">
            <div className="layout-draw-page__component-preview-space layout-draw-page__component-preview-space--input">이름을 입력하세요</div>
            <div className="layout-draw-page__component-preview-caption">필수 입력 항목</div>
          </div>
        </div>
      </div>
    );
  }

  if (kind === 'filter-toolbar') {
    return (
      <div className="layout-draw-page__component-preview-shot layout-draw-page__component-preview-shot--dashboard">
        <div className="layout-draw-page__component-preview-window">
          <div className="layout-draw-page__component-preview-window-header">
            <div className="layout-draw-page__component-preview-window-dots">
              <span />
              <span />
              <span />
            </div>
            <div className="layout-draw-page__component-preview-window-bar" />
          </div>
          <div className="layout-draw-page__component-preview-row">
            <div className="layout-draw-page__component-preview-column layout-draw-page__component-preview-column--grow">
              <div className="layout-draw-page__component-preview-title">프로젝트 목록</div>
              <div className="layout-draw-page__component-preview-caption">검색과 필터를 함께 쓰는 업무형 상단 레이아웃</div>
            </div>
            <div className="layout-draw-page__component-preview-button layout-draw-page__component-preview-button--compact layout-draw-page__component-preview-button--primary">등록</div>
          </div>
          <div className="layout-draw-page__component-preview-row">
            <div className="layout-draw-page__component-preview-space layout-draw-page__component-preview-space--input">검색</div>
            <div className="layout-draw-page__component-preview-space">담당자</div>
            <div className="layout-draw-page__component-preview-space">상태</div>
          </div>
          <div className="layout-draw-page__component-preview-tabbar">
            <span>전체</span>
            <span className="layout-draw-page__component-preview-tabbar-active">진행중</span>
            <span>검수</span>
            <span>보류</span>
          </div>
        </div>
      </div>
    );
  }

  if (kind === 'stats-summary') {
    return (
      <div className="layout-draw-page__component-preview-shot layout-draw-page__component-preview-shot--stats">
        <div className="layout-draw-page__component-preview-shot-topbar">
          <span className="layout-draw-page__component-preview-eyebrow">Dashboard</span>
          <div className="layout-draw-page__component-preview-caption">주간 현황</div>
        </div>
        <div className="layout-draw-page__component-preview-card-grid">
          <div className="layout-draw-page__component-preview-metric-card">
            <div className="layout-draw-page__component-preview-caption">총 요청</div>
            <div className="layout-draw-page__component-preview-metric">128</div>
            <div className="layout-draw-page__component-preview-spark" />
          </div>
          <div className="layout-draw-page__component-preview-metric-card">
            <div className="layout-draw-page__component-preview-caption">완료율</div>
            <div className="layout-draw-page__component-preview-metric">92%</div>
            <div className="layout-draw-page__component-preview-spark layout-draw-page__component-preview-spark--green" />
          </div>
          <div className="layout-draw-page__component-preview-metric-card">
            <div className="layout-draw-page__component-preview-caption">지연 건</div>
            <div className="layout-draw-page__component-preview-metric">6</div>
            <div className="layout-draw-page__component-preview-spark layout-draw-page__component-preview-spark--amber" />
          </div>
        </div>
      </div>
    );
  }

  if (kind === 'detail-panel') {
    return (
      <div className="layout-draw-page__component-preview-shot layout-draw-page__component-preview-shot--detail">
        <div className="layout-draw-page__component-preview-shot-topbar">
          <div className="layout-draw-page__component-preview-title layout-draw-page__component-preview-title--flex">상세 정보</div>
          <div className="layout-draw-page__component-preview-button layout-draw-page__component-preview-button--compact">닫기</div>
        </div>
        <div className="layout-draw-page__component-preview-space layout-draw-page__component-preview-space--body">선택한 항목의 설명과 메타 정보를 보여주는 영역</div>
        <div className="layout-draw-page__component-preview-property-list">
          <div className="layout-draw-page__component-preview-property-item">
            <span>상태</span>
            <strong>진행중</strong>
          </div>
          <div className="layout-draw-page__component-preview-property-item">
            <span>담당</span>
            <strong>디자인팀</strong>
          </div>
        </div>
        <div className="layout-draw-page__component-preview-divider" />
        <div className="layout-draw-page__component-preview-row layout-draw-page__component-preview-row--end">
          <div className="layout-draw-page__component-preview-button layout-draw-page__component-preview-button--compact">메모</div>
          <div className="layout-draw-page__component-preview-button layout-draw-page__component-preview-button--compact layout-draw-page__component-preview-button--primary">확인</div>
        </div>
      </div>
    );
  }

  return (
    <div className="layout-draw-page__component-preview-shot layout-draw-page__component-preview-shot--panel">
      <div className="layout-draw-page__component-preview-title">제목</div>
      <div className="layout-draw-page__component-preview-space layout-draw-page__component-preview-space--body">설명 영역</div>
      <div className="layout-draw-page__component-preview-row layout-draw-page__component-preview-row--end">
        <div className="layout-draw-page__component-preview-button layout-draw-page__component-preview-button--compact layout-draw-page__component-preview-button--primary">액션</div>
      </div>
    </div>
  );
}

function resolveFloatingBoxDefaultPosition(key: FloatingBoxKey, viewportWidth: number, viewportHeight: number) {
  const isMobile = viewportWidth <= MOBILE_LAYOUT_BREAKPOINT_PX;
  const topInset = isMobile ? 8 : 12;
  const sideInset = isMobile ? 8 : 12;
  const mobileBottomY = Math.max(sideInset, viewportHeight - 62);

  if (key === 'tools') {
    return isMobile
      ? {
          x: sideInset,
          y: mobileBottomY,
        }
      : {
          x: sideInset,
          y: topInset,
        };
  }

  if (key === 'actions') {
    return isMobile
      ? {
          x: Math.max(sideInset, viewportWidth - 56),
          y: mobileBottomY,
        }
      : {
          x: Math.max(sideInset, viewportWidth - 64),
          y: topInset,
        };
  }

  return isMobile
    ? {
        x: sideInset,
        y: topInset,
      }
    : {
        x: 72,
        y: topInset,
      };
}

function clampFloatingBoxPosition(
  position: FloatingBoxPosition,
  element: HTMLElement | null,
  boundary: HTMLElement | null,
  sizeOverride?: FloatingBoxSize,
) {
  if (typeof window === 'undefined') {
    return position;
  }

  const boundaryWidth = boundary?.clientWidth ?? window.innerWidth;
  const boundaryHeight = boundary?.clientHeight ?? window.innerHeight;
  const width = sizeOverride?.width ?? element?.offsetWidth ?? 0;
  const height = sizeOverride?.height ?? element?.offsetHeight ?? 0;
  const maxX = Math.max(0, boundaryWidth - width);
  const maxY = Math.max(0, boundaryHeight - height);

  return {
    x: Math.min(Math.max(0, position.x), maxX),
    y: Math.min(Math.max(0, position.y), maxY),
  };
}

function resolveFloatingBoxSize(element: HTMLElement | null, fallback: FloatingBoxSize): FloatingBoxSize {
  return {
    width: element?.offsetWidth ?? fallback.width,
    height: element?.offsetHeight ?? fallback.height,
  };
}

function snapFloatingBoxToClosestEdge(
  position: FloatingBoxPosition,
  size: FloatingBoxSize,
  boundary: HTMLElement | null,
) {
  const clamped = clampFloatingBoxPosition(position, null, boundary, size);

  if (typeof window === 'undefined') {
    return clamped;
  }

  const boundaryWidth = boundary?.clientWidth ?? window.innerWidth;
  const boundaryHeight = boundary?.clientHeight ?? window.innerHeight;
  const maxX = Math.max(0, boundaryWidth - size.width);
  const maxY = Math.max(0, boundaryHeight - size.height);
  const distances = [
    { edge: 'left', distance: clamped.x },
    { edge: 'right', distance: maxX - clamped.x },
    { edge: 'top', distance: clamped.y },
    { edge: 'bottom', distance: maxY - clamped.y },
  ] as const;
  const nearestEdge = distances.reduce((closest, candidate) =>
    candidate.distance < closest.distance ? candidate : closest,
  );

  if (nearestEdge.edge === 'left') {
    return { x: 0, y: clamped.y };
  }

  if (nearestEdge.edge === 'right') {
    return { x: maxX, y: clamped.y };
  }

  if (nearestEdge.edge === 'top') {
    return { x: clamped.x, y: 0 };
  }

  return { x: clamped.x, y: maxY };
}

function createInitialDocument(backgroundMode: BackgroundMode) {
  return {
    backgroundMode,
    shapes: [],
  };
}

function createSavedDrawId() {
  return createShapeId('draw');
}

function createDefaultSaveName() {
  const now = new Date();
  const pad = (value: number) => value.toString().padStart(2, '0');
  return `도면 ${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}`;
}

function cloneShape(shape: DrawShape): DrawShape {
  return { ...shape };
}

function isDrawableShape(shape: DrawShape): shape is DrawableShape {
  return shape.type === 'line' || shape.type === 'rect';
}

function isRegionShape(shape: DrawShape): shape is DrawRegion {
  return shape.type === 'region';
}

function formatSavedAt(value: string) {
  const time = Date.parse(value);
  if (Number.isNaN(time)) {
    return value;
  }

  return new Intl.DateTimeFormat('ko-KR', {
    year: 'numeric',
    month: '2-digit',
    day: '2-digit',
    hour: '2-digit',
    minute: '2-digit',
  }).format(new Date(time));
}

export function LayoutDrawPage() {
  const pageRef = useRef<HTMLDivElement | null>(null);
  const svgRef = useRef<SVGSVGElement | null>(null);
  const floatingBoxRefs = useRef<Record<FloatingBoxKey, HTMLElement | null>>({
    tools: null,
    actions: null,
    guide: null,
  });
  const [canvasSize, setCanvasSize] = useState<CanvasSize>({ width: 1, height: 1 });
  const [tool, setTool] = useState<DrawTool>('select');
  const [historyState, setHistoryState] = useState(() =>
    createLayoutDrawHistoryState(
      createInitialDocument(
        (() => {
          if (typeof window === 'undefined') {
            return INITIAL_BACKGROUND_MODE;
          }

          return window.localStorage.getItem(DRAW_BACKGROUND_MODE_STORAGE_KEY) === 'plain' ? 'plain' : 'grid';
        })(),
      ),
    ),
  );
  const [selectedTarget, setSelectedTarget] = useState<SelectedTarget>(null);
  const [selectedShapeIdsState, setSelectedShapeIdsState] = useState<string[]>([]);
  const [draft, setDraft] = useState<DraftState>(null);
  const [dragState, setDragState] = useState<DragState>(null);
  const [selectionBox, setSelectionBox] = useState<SelectionBoxState>(null);
  const [resizeState, setResizeState] = useState<ResizeState>(null);
  const [liveShapes, setLiveShapes] = useState<DrawShape[] | null>(null);
  const [storagePanel, setStoragePanel] = useState<'save' | 'load' | null>(null);
  const [saveName, setSaveName] = useState(() => createDefaultSaveName());
  const [savedDraws, setSavedDraws] = useState<SavedLayoutDrawRecord[]>([]);
  const [savedComponents, setSavedComponents] = useState<SavedLayoutDrawComponentRecord[]>([]);
  const [isStorageLoading, setIsStorageLoading] = useState(false);
  const [isStorageSaving, setIsStorageSaving] = useState(false);
  const [isComponentStorageLoading, setIsComponentStorageLoading] = useState(false);
  const [isComponentSaving, setIsComponentSaving] = useState(false);
  const [componentSaveOpen, setComponentSaveOpen] = useState(false);
  const [componentSaveName, setComponentSaveName] = useState('');
  const [componentSaveCategory, setComponentSaveCategory] = useState('내 컴포넌트');
  const [shapeLabelDraft, setShapeLabelDraft] = useState('');
  const [paintColor, setPaintColor] = useState<string>(PAINT_COLOR_OPTIONS[0].value);
  const [storageError, setStorageError] = useState<string | null>(null);
  const [guideVisible, setGuideVisible] = useState(true);
  const [attachedToolbars, setAttachedToolbars] = useState<Record<AttachableToolbarKey, boolean>>({
    tools: false,
    actions: false,
  });
  const [collapsedAttachedToolbars, setCollapsedAttachedToolbars] = useState<Record<AttachableToolbarKey, boolean>>({
    tools: false,
    actions: false,
  });
  const [isMobileViewport, setIsMobileViewport] = useState(() => {
    if (typeof window === 'undefined') {
      return false;
    }

    return window.innerWidth <= MOBILE_LAYOUT_BREAKPOINT_PX;
  });
  const [mobileActionsExpanded, setMobileActionsExpanded] = useState(false);
  const [componentLibraryOpen, setComponentLibraryOpen] = useState(false);
  const [componentLibrarySection, setComponentLibrarySection] = useState<ComponentLibrarySectionKey>('featured');
  const [floatingBoxPositions, setFloatingBoxPositions] = useState<Record<FloatingBoxKey, FloatingBoxPosition>>(() => {
    if (typeof window === 'undefined') {
      return {
        tools: { x: 12, y: 12 },
        actions: { x: 12, y: 12 },
        guide: { x: 72, y: 12 },
      };
    }

    return {
      tools: resolveFloatingBoxDefaultPosition('tools', window.innerWidth, window.innerHeight),
      actions: resolveFloatingBoxDefaultPosition('actions', window.innerWidth, window.innerHeight),
      guide: resolveFloatingBoxDefaultPosition('guide', window.innerWidth, window.innerHeight),
    };
  });
  const [floatingBoxLayerOrder, setFloatingBoxLayerOrder] = useState<FloatingBoxKey[]>(['tools', 'actions', 'guide']);
  const [draggingBox, setDraggingBox] = useState<{
    key: FloatingBoxKey;
    pointerOffsetX: number;
    pointerOffsetY: number;
  } | null>(null);
  const dragStartShapesRef = useRef<DrawShape[] | null>(null);
  const shapesRef = useRef<DrawShape[]>([]);
  const attachedToolbarLastUsedRef = useRef<Record<AttachableToolbarKey, number>>({
    tools: Date.now(),
    actions: Date.now(),
  });
  const [messageApi, messageContextHolder] = message.useMessage();
  const backgroundMode = historyState.present.backgroundMode;
  const shapes = liveShapes ?? historyState.present.shapes;
  const { drawableShapes, regionAssignments } = splitDrawShapes(shapes);
  const drawRegions = resolveDrawRegions(shapes, canvasSize.width, canvasSize.height);
  const hasRegionLayout = drawableShapes.some((shape) => shape.type === 'line') || regionAssignments.length > 0;
  const selectedShapeId = selectedTarget?.kind === 'shape' ? selectedTarget.id : null;
  const selectedRegionKey = selectedTarget?.kind === 'region' ? selectedTarget.regionKey : null;
  const selectedShapeIds = useMemo(
    () => resolveGroupedShapeIds(drawableShapes, selectedShapeIdsState),
    [drawableShapes, selectedShapeIdsState],
  );
  const selectedShape = selectedShapeId
    ? drawableShapes.find((shape) => shape.id === selectedShapeId) ?? null
    : null;
  const selectedRegion = selectedRegionKey ? drawRegions.find((region) => region.key === selectedRegionKey) ?? null : null;
  const selectedDrawableShapes = drawableShapes.filter((shape) => selectedShapeIds.has(shape.id));
  const hasMultiSelection = selectedShapeIds.size > 1;
  const canUndo = historyState.past.length > 0;
  const canRedo = historyState.future.length > 0;
  const componentLibraryGroups = useMemo(() => {
    const presetGroups = COMPONENT_LIBRARY_SECTIONS.map((section) => ({
      ...section,
      items: section.presetIds
        .map((presetId) => COMPONENT_LIBRARY_PRESETS.find((preset) => preset.presetId === presetId) ?? null)
        .filter((item): item is ComponentLibraryItem => item !== null),
    }));

    const savedGroup = {
      key: 'saved',
      label: '저장 컴포넌트',
      items: savedComponents.map(
        (record) =>
          ({
            presetId: `saved:${record.id}`,
            title: record.name,
            description: `${record.category} 묶음으로 저장된 사용자 컴포넌트입니다.`,
            category: record.category,
            previewKind: 'content-card',
            blueprints: record.shapes,
            source: 'saved',
            savedRecordId: record.id,
          }) satisfies ComponentLibraryItem,
      ),
    };

    return savedComponents.length > 0 ? [savedGroup, ...presetGroups] : presetGroups;
  }, [savedComponents]);
  const activeComponentLibraryGroup =
    componentLibraryGroups.find((section) => section.key === componentLibrarySection) ?? componentLibraryGroups[0];

  const assignFloatingBoxRef = (key: FloatingBoxKey) => (element: HTMLElement | null) => {
    floatingBoxRefs.current[key] = element;
  };

  const updateFloatingBoxPosition = (key: FloatingBoxKey, nextPosition: FloatingBoxPosition) => {
    setFloatingBoxPositions((previous) => ({
      ...previous,
      [key]: clampFloatingBoxPosition(nextPosition, floatingBoxRefs.current[key], pageRef.current),
    }));
  };

  const bringFloatingBoxToFront = (key: FloatingBoxKey) => {
    setFloatingBoxLayerOrder((previous) => {
      if (previous[previous.length - 1] === key) {
        return previous;
      }

      return previous.filter((entry) => entry !== key).concat(key);
    });
  };

  const resolveFloatingBoxZIndex = (key: FloatingBoxKey) => {
    const orderIndex = floatingBoxLayerOrder.indexOf(key);
    return FLOATING_BOX_BASE_Z_INDEX + (orderIndex >= 0 ? orderIndex : 0);
  };

  const markToolbarInteraction = (key: AttachableToolbarKey) => {
    bringFloatingBoxToFront(key);
    attachedToolbarLastUsedRef.current[key] = Date.now();
    setCollapsedAttachedToolbars((previous) => (previous[key] ? { ...previous, [key]: false } : previous));
  };

  const snapToolbarToAttachedPosition = (key: AttachableToolbarKey) => {
    const boundary = pageRef.current;
    const element = floatingBoxRefs.current[key];
    const fallbackSize = isMobileViewport ? { width: 38, height: 38 } : { width: 44, height: 44 };
    setFloatingBoxPositions((previous) => {
      const size = resolveFloatingBoxSize(element, fallbackSize);
      const currentPosition = previous[key];
      const nextPosition =
        currentPosition.x === 0 && currentPosition.y === 0
          ? resolveFloatingBoxDefaultPosition(
              key,
              boundary?.clientWidth ?? (typeof window === 'undefined' ? 0 : window.innerWidth),
              boundary?.clientHeight ?? (typeof window === 'undefined' ? 0 : window.innerHeight),
            )
          : currentPosition;
      const snappedPosition = snapFloatingBoxToClosestEdge(nextPosition, size, boundary);

      if (snappedPosition.x === currentPosition.x && snappedPosition.y === currentPosition.y) {
        return previous;
      }

      return {
        ...previous,
        [key]: snappedPosition,
      };
    });
  };

  const toggleToolbarAttached = (key: AttachableToolbarKey) => {
    setAttachedToolbars((previous) => {
      const nextAttached = !previous[key];
      if (nextAttached) {
        attachedToolbarLastUsedRef.current[key] = Date.now();
        setCollapsedAttachedToolbars((collapsedPrevious) => ({ ...collapsedPrevious, [key]: false }));
        window.requestAnimationFrame(() => {
          snapToolbarToAttachedPosition(key);
        });
      }
      return {
        ...previous,
        [key]: nextAttached,
      };
    });
  };

  const expandAttachedToolbar = (key: AttachableToolbarKey) => {
    bringFloatingBoxToFront(key);
    markToolbarInteraction(key);
    setCollapsedAttachedToolbars((previous) => ({ ...previous, [key]: false }));
    if (attachedToolbars[key]) {
      window.requestAnimationFrame(() => {
        snapToolbarToAttachedPosition(key);
      });
    }
  };

  const commitDocument = (nextBackgroundMode: BackgroundMode, nextShapes: DrawShape[]) => {
    setHistoryState((previous) =>
      commitLayoutDrawHistory(previous, {
        backgroundMode: nextBackgroundMode,
        shapes: nextShapes,
      }),
    );
  };

  const setSelectedShapes = (shapeIds: Iterable<string>, anchorId?: string | null) => {
    const nextIds = [...resolveGroupedShapeIds(drawableShapes, shapeIds)];
    setSelectedShapeIdsState(nextIds);
    if (nextIds.length === 0) {
      setSelectedTarget(null);
      return;
    }

    const nextAnchorId = anchorId && nextIds.includes(anchorId) ? anchorId : nextIds[0];
    setSelectedTarget(nextAnchorId ? { kind: 'shape', id: nextAnchorId } : null);
  };

  const clearShapeSelection = () => {
    setSelectedShapeIdsState([]);
    if (selectedTarget?.kind === 'shape') {
      setSelectedTarget(null);
    }
  };

  const syncSelection = (nextShapes: DrawShape[]) => {
    const nextDrawableShapes = nextShapes.filter(isDrawableShape);
    const nextSelectedIds = [...selectedShapeIds].filter((id) => nextDrawableShapes.some((shape) => shape.id === id));
    if (nextSelectedIds.length !== selectedShapeIds.size) {
      setSelectedShapeIdsState(nextSelectedIds);
    }
    if (selectedTarget?.kind === 'shape' && !nextSelectedIds.includes(selectedTarget.id)) {
      setSelectedTarget(null);
    }
  };

  const removeSelectedTarget = () => {
    if (selectedShapeIds.size > 0) {
      commitDocument(
        backgroundMode,
        shapesRef.current.filter((shape) => !(isDrawableShape(shape) && selectedShapeIds.has(shape.id))),
      );
      clearShapeSelection();
      return;
    }

    if (!selectedTarget) {
      return;
    }

    commitDocument(
      backgroundMode,
      shapesRef.current.filter((shape) => !(isRegionShape(shape) && shape.regionKey === selectedTarget.regionKey)),
    );
    setSelectedTarget(null);
  };

  const openSavePanel = () => {
    setStorageError(null);
    setSaveName(createDefaultSaveName());
    setStoragePanel('save');
  };

  const loadSavedDraws = async () => {
    setIsStorageLoading(true);
    setStorageError(null);

    try {
      const items = await listSavedLayoutDraws();
      setSavedDraws(items);
    } catch (error) {
      const nextMessage = error instanceof Error ? error.message : '저장 목록을 불러오지 못했습니다.';
      setStorageError(nextMessage);
    } finally {
      setIsStorageLoading(false);
    }
  };

  const openLoadPanel = () => {
    setStorageError(null);
    setStoragePanel('load');
    void loadSavedDraws();
  };

  const openComponentLibrary = () => {
    setComponentLibraryOpen(true);
    void loadSavedComponents();
  };

  const loadSavedComponents = async () => {
    setIsComponentStorageLoading(true);
    setStorageError(null);

    try {
      const items = await listSavedLayoutDrawComponents();
      setSavedComponents(items);
    } catch (error) {
      setStorageError(error instanceof Error ? error.message : '저장 컴포넌트를 불러오지 못했습니다.');
    } finally {
      setIsComponentStorageLoading(false);
    }
  };

  const handleInsertComponentPreset = (preset: ComponentLibraryItem) => {
    const bounds = resolveBlueprintBounds(preset.blueprints);
    const centerX = Math.max(24, canvasSize.width / 2);
    const centerY = Math.max(24, canvasSize.height / 2);
    const offsetX = Math.max(12, centerX - bounds.width / 2);
    const offsetY = Math.max(12, centerY - bounds.height / 2);
    const groupId = createShapeId('group');
    const nextShapes = preset.blueprints.map((blueprint) =>
      blueprint.type === 'line'
        ? ({
            id: createShapeId('line'),
            type: 'line',
            groupId,
            x1: Math.round(offsetX + blueprint.x1),
            y1: Math.round(offsetY + blueprint.y1),
            x2: Math.round(offsetX + blueprint.x2),
            y2: Math.round(offsetY + blueprint.y2),
            orientation: blueprint.orientation,
            label: blueprint.label,
          } satisfies DrawLine)
        : ({
            id: createShapeId('rect'),
            type: 'rect',
            groupId,
            x: Math.round(offsetX + blueprint.x),
            y: Math.round(offsetY + blueprint.y),
            width: blueprint.width,
            height: blueprint.height,
            label: blueprint.label,
            fillColor: blueprint.fillColor ?? null,
          } satisfies DrawRect),
    );

    commitDocument(backgroundMode, [...shapesRef.current, ...nextShapes]);
    setSelectedShapes(nextShapes.map((shape) => shape.id), nextShapes[0]?.id ?? null);
    setShapeLabelDraft(nextShapes[0].label);
    setTool('select');
    setStorageError(null);
    messageApi.success(`"${preset.title}" 컴포넌트를 도면에 추가했습니다.`);
  };

  const handleGroupSelectedShapes = () => {
    if (selectedDrawableShapes.length < 2) {
      setStorageError('둘 이상 선택한 뒤 그룹으로 묶어 주세요.');
      return;
    }

    const groupId = createShapeId('group');
    const nextIds = new Set(selectedDrawableShapes.map((shape) => shape.id));
    commitDocument(
      backgroundMode,
      shapesRef.current.map((shape) =>
        isDrawableShape(shape) && nextIds.has(shape.id)
          ? {
              ...shape,
              groupId,
            }
          : shape,
      ),
    );
    setStorageError(null);
    messageApi.success('선택한 오브젝트를 그룹으로 묶었습니다.');
  };

  const handleUngroupSelectedShapes = () => {
    if (selectedDrawableShapes.length === 0 || !selectedDrawableShapes.some((shape) => shape.groupId)) {
      setStorageError('해제할 그룹 선택이 없습니다.');
      return;
    }

    commitDocument(
      backgroundMode,
      shapesRef.current.map((shape) =>
        isDrawableShape(shape) && selectedShapeIds.has(shape.id)
          ? {
              ...shape,
              groupId: null,
            }
          : shape,
      ),
    );
    setStorageError(null);
    messageApi.success('선택한 그룹을 해제했습니다.');
  };

  const openComponentSaveModal = () => {
    if (selectedDrawableShapes.length === 0) {
      setStorageError('컴포넌트로 저장할 오브젝트를 먼저 선택해 주세요.');
      return;
    }

    setComponentSaveName(selectedShape?.label || `컴포넌트 ${savedComponents.length + 1}`);
    setComponentSaveCategory('내 컴포넌트');
    setComponentSaveOpen(true);
    setStorageError(null);
  };

  const handleCopySelectedShape = () => {
    if (selectedDrawableShapes.length === 0) {
      setStorageError('복사할 객체를 먼저 선택해 주세요.');
      return;
    }

    if (selectedDrawableShapes.length > 1 || selectedDrawableShapes[0]?.groupId) {
      const groupedShapes = selectedDrawableShapes;
      const selectedIndex = selectedShape ? groupedShapes.findIndex((shape) => shape.id === selectedShape.id) : 0;
      const nextGroupId = createShapeId('group');
      const duplicatedShapes = groupedShapes.map((shape) =>
        duplicateShapeWithLabel(shape, createShapeId(shape.type), shape.label, nextGroupId),
      );
      const nextSelectedShape = duplicatedShapes[selectedIndex] ?? duplicatedShapes[0];
      commitDocument(backgroundMode, [...shapesRef.current, ...duplicatedShapes]);
      setSelectedShapes(
        duplicatedShapes.map((shape) => shape.id),
        nextSelectedShape?.id ?? null,
      );
      setStorageError(null);
      setShapeLabelDraft(nextSelectedShape?.label ?? '');
      messageApi.success(`"${selectedShape?.label || '선택 컴포넌트'}" 묶음을 복사했습니다.`);
      return;
    }

    const baseShape = selectedDrawableShapes[0];
    const nextShape = duplicateShapeWithLabel(baseShape, createShapeId(baseShape.type));
    commitDocument(backgroundMode, [...shapesRef.current, nextShape]);
    setSelectedShapes([nextShape.id], nextShape.id);
    setStorageError(null);
    setShapeLabelDraft(nextShape.label);
    messageApi.success(`"${nextShape.label || '선택 객체'}"를 복사했습니다.`);
  };

  const handleSelectedShapeLabelChange = () => {
    if (selectedShapeIds.size > 1) {
      return;
    }

    if (!selectedShape && !selectedRegion) {
      return;
    }

    const nextLabel = shapeLabelDraft.trim();
    if (selectedShape) {
      if (nextLabel === selectedShape.label) {
        return;
      }

      const nextShapes = shapesRef.current.map((shape) =>
        isDrawableShape(shape) && shape.id === selectedShape.id
          ? {
              ...shape,
              label: nextLabel,
            }
          : shape,
      );
      commitDocument(backgroundMode, nextShapes);
      setStorageError(null);
      return;
    }

    if (!selectedRegion) {
      return;
    }

    const existingRegionShape = regionAssignments.find((shape) => shape.regionKey === selectedRegion.key) ?? null;
    if (existingRegionShape && nextLabel === existingRegionShape.label) {
      return;
    }

    const nextShapes = existingRegionShape
      ? !nextLabel && !existingRegionShape.fillColor
        ? shapesRef.current.filter((shape) => !(isRegionShape(shape) && shape.id === existingRegionShape.id))
        : shapesRef.current.map((shape) =>
            isRegionShape(shape) && shape.id === existingRegionShape.id
              ? {
                  ...shape,
                  label: nextLabel,
                }
              : shape,
          )
      : nextLabel
        ? [
            ...shapesRef.current,
            {
              id: createShapeId('region'),
              type: 'region',
              regionKey: selectedRegion.key,
              label: nextLabel,
              fillColor: null,
            } satisfies DrawRegion,
          ]
        : shapesRef.current;

    commitDocument(backgroundMode, nextShapes);
    setStorageError(null);
  };

  const handleUndo = () => {
    setDraft(null);
    setDragState(null);
    setSelectionBox(null);
    setResizeState(null);
    setLiveShapes(null);
    dragStartShapesRef.current = null;
    setHistoryState((previous) => undoLayoutDrawHistory(previous));
  };

  const handleRedo = () => {
    setDraft(null);
    setDragState(null);
    setSelectionBox(null);
    setResizeState(null);
    setLiveShapes(null);
    dragStartShapesRef.current = null;
    setHistoryState((previous) => redoLayoutDrawHistory(previous));
  };

  const handleSave = async () => {
    const name = saveName.trim();
    if (!name) {
      setStorageError('저장 이름을 입력해 주세요.');
      return;
    }

    setIsStorageSaving(true);
    setStorageError(null);

    const now = new Date().toISOString();

    try {
      await saveLayoutDraw({
        id: createSavedDrawId(),
        name,
        createdAt: now,
        updatedAt: now,
        backgroundMode,
        shapes,
      });
      setStoragePanel(null);
      setSaveName(createDefaultSaveName());
      messageApi.success('현재 도면을 저장했습니다.');
    } catch (error) {
      setStorageError(error instanceof Error ? error.message : '도면 저장에 실패했습니다.');
    } finally {
      setIsStorageSaving(false);
    }
  };

  const handleLoadSavedDraw = (record: SavedLayoutDrawRecord) => {
    setDraft(null);
    setDragState(null);
    setSelectionBox(null);
    setResizeState(null);
    setLiveShapes(null);
    dragStartShapesRef.current = null;
    setSelectedTarget(null);
    setSelectedShapeIdsState([]);
    setTool('select');
    commitDocument(record.backgroundMode, record.shapes);
    setStoragePanel(null);
    messageApi.success(`"${record.name}" 도면을 불러왔습니다.`);
  };

  const handleDeleteSavedDraw = async (record: SavedLayoutDrawRecord) => {
    try {
      await deleteLayoutDraw(record.id);
      setSavedDraws((previous) => previous.filter((item) => item.id !== record.id));
      messageApi.success(`"${record.name}" 저장본을 삭제했습니다.`);
    } catch (error) {
      setStorageError(error instanceof Error ? error.message : '저장본 삭제에 실패했습니다.');
    }
  };

  const [initialBackgroundMode] = useState<BackgroundMode>(() => {
    if (typeof window === 'undefined') {
      return INITIAL_BACKGROUND_MODE;
    }

    return window.localStorage.getItem(DRAW_BACKGROUND_MODE_STORAGE_KEY) === 'plain' ? 'plain' : 'grid';
  });

  useEffect(() => {
    shapesRef.current = shapes.map(cloneShape);
  }, [shapes]);

  useEffect(() => {
    if (typeof window === 'undefined') {
      return;
    }

    window.localStorage.setItem(DRAW_BACKGROUND_MODE_STORAGE_KEY, backgroundMode);
  }, [backgroundMode]);

  useEffect(() => {
    if (historyState.present.backgroundMode === initialBackgroundMode && historyState.present.shapes.length === 0) {
      return;
    }

    syncSelection(historyState.present.shapes);
  }, [historyState.present, initialBackgroundMode, selectedShapeIdsState, selectedTarget]);

  useEffect(() => {
    if (selectedTarget?.kind === 'region' && !selectedRegion) {
      setSelectedTarget(null);
    }
  }, [selectedRegion, selectedTarget]);

  useEffect(() => {
    if (selectedTarget?.kind === 'shape' && selectedShapeIds.size === 0) {
      setSelectedTarget(null);
    }
  }, [selectedShapeIds, selectedTarget]);

  useEffect(() => {
    const element = svgRef.current;
    if (!element) {
      return undefined;
    }

    const updateCanvasSize = () => {
      setCanvasSize(resolveCanvasSize(element));
    };

    updateCanvasSize();
    if (typeof ResizeObserver === 'undefined') {
      return undefined;
    }

    const observer = new ResizeObserver(() => {
      updateCanvasSize();
    });
    observer.observe(element);
    return () => {
      observer.disconnect();
    };
  }, [attachedToolbars, isMobileViewport]);

  useEffect(() => {
    if (typeof window === 'undefined') {
      return undefined;
    }

    let previousIsMobileViewport = window.innerWidth <= MOBILE_LAYOUT_BREAKPOINT_PX;
    const handleResize = () => {
      const nextIsMobileViewport = window.innerWidth <= MOBILE_LAYOUT_BREAKPOINT_PX;
      const boundary = pageRef.current;
      const boundaryWidth = boundary?.clientWidth ?? window.innerWidth;
      const boundaryHeight = boundary?.clientHeight ?? window.innerHeight;

      setIsMobileViewport(nextIsMobileViewport);
      setFloatingBoxPositions((previous) => {
        if (nextIsMobileViewport !== previousIsMobileViewport) {
          previousIsMobileViewport = nextIsMobileViewport;
          return {
            tools: resolveFloatingBoxDefaultPosition('tools', boundaryWidth, boundaryHeight),
            actions: resolveFloatingBoxDefaultPosition('actions', boundaryWidth, boundaryHeight),
            guide: resolveFloatingBoxDefaultPosition('guide', boundaryWidth, boundaryHeight),
          };
        }

        return {
          tools: attachedToolbars.tools
            ? snapFloatingBoxToClosestEdge(
                previous.tools,
                resolveFloatingBoxSize(
                  floatingBoxRefs.current.tools,
                  nextIsMobileViewport ? { width: 38, height: 38 } : { width: 44, height: 44 },
                ),
                boundary,
              )
            : clampFloatingBoxPosition(previous.tools, floatingBoxRefs.current.tools, boundary),
          actions: attachedToolbars.actions
            ? snapFloatingBoxToClosestEdge(
                previous.actions,
                resolveFloatingBoxSize(
                  floatingBoxRefs.current.actions,
                  nextIsMobileViewport ? { width: 38, height: 38 } : { width: 44, height: 44 },
                ),
                boundary,
              )
            : clampFloatingBoxPosition(previous.actions, floatingBoxRefs.current.actions, boundary),
          guide: clampFloatingBoxPosition(previous.guide, floatingBoxRefs.current.guide, boundary),
        };
      });
    };

    handleResize();
    window.addEventListener('resize', handleResize);
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, [attachedToolbars.actions, attachedToolbars.tools]);

  useEffect(() => {
    const boundary = pageRef.current;
    if (!boundary || typeof ResizeObserver === 'undefined') {
      return undefined;
    }

    const observer = new ResizeObserver(() => {
      setFloatingBoxPositions((previous) => ({
        tools: attachedToolbars.tools
          ? snapFloatingBoxToClosestEdge(
              previous.tools,
              resolveFloatingBoxSize(
                floatingBoxRefs.current.tools,
                isMobileViewport ? { width: 38, height: 38 } : { width: 44, height: 44 },
              ),
              boundary,
            )
          : clampFloatingBoxPosition(previous.tools, floatingBoxRefs.current.tools, boundary),
        actions: attachedToolbars.actions
          ? snapFloatingBoxToClosestEdge(
              previous.actions,
              resolveFloatingBoxSize(
                floatingBoxRefs.current.actions,
                isMobileViewport ? { width: 38, height: 38 } : { width: 44, height: 44 },
              ),
              boundary,
            )
          : clampFloatingBoxPosition(previous.actions, floatingBoxRefs.current.actions, boundary),
        guide: clampFloatingBoxPosition(previous.guide, floatingBoxRefs.current.guide, boundary),
      }));
    });

    observer.observe(boundary);
    return () => {
      observer.disconnect();
    };
  }, [attachedToolbars.actions, attachedToolbars.tools, isMobileViewport]);

  useEffect(() => {
    if (typeof ResizeObserver === 'undefined') {
      return undefined;
    }

    const observers: ResizeObserver[] = [];

    (['tools', 'actions'] as const).forEach((key) => {
      if (!attachedToolbars[key]) {
        return;
      }

      const element = floatingBoxRefs.current[key];
      if (!element) {
        return;
      }

      const observer = new ResizeObserver(() => {
        setFloatingBoxPositions((previous) => {
          const nextPosition = snapFloatingBoxToClosestEdge(
            previous[key],
            resolveFloatingBoxSize(element, {
              width: element.offsetWidth,
              height: element.offsetHeight,
            }),
            pageRef.current,
          );

          if (nextPosition.x === previous[key].x && nextPosition.y === previous[key].y) {
            return previous;
          }

          return {
            ...previous,
            [key]: nextPosition,
          };
        });
      });

      observer.observe(element);
      observers.push(observer);
    });

    return () => {
      observers.forEach((observer) => {
        observer.disconnect();
      });
    };
  }, [attachedToolbars.actions, attachedToolbars.tools]);

  useEffect(() => {
    (['tools', 'actions'] as const).forEach((key) => {
      if (!attachedToolbars[key]) {
        return;
      }

      window.requestAnimationFrame(() => {
        snapToolbarToAttachedPosition(key);
      });
    });
  }, [
    attachedToolbars.actions,
    attachedToolbars.tools,
    collapsedAttachedToolbars.actions,
    collapsedAttachedToolbars.tools,
    isMobileViewport,
  ]);

  useEffect(() => {
    if (!draggingBox || typeof window === 'undefined') {
      return undefined;
    }

    const handlePointerMove = (event: PointerEvent) => {
      updateFloatingBoxPosition(draggingBox.key, {
        x: event.clientX - draggingBox.pointerOffsetX,
        y: event.clientY - draggingBox.pointerOffsetY,
      });
    };

    const stopDragging = () => {
      setDraggingBox(null);
    };

    window.addEventListener('pointermove', handlePointerMove);
    window.addEventListener('pointerup', stopDragging);
    window.addEventListener('pointercancel', stopDragging);
    return () => {
      window.removeEventListener('pointermove', handlePointerMove);
      window.removeEventListener('pointerup', stopDragging);
      window.removeEventListener('pointercancel', stopDragging);
    };
  }, [draggingBox]);

  useEffect(() => {
    if (typeof window === 'undefined') {
      return undefined;
    }

    const interval = window.setInterval(() => {
      const now = Date.now();
      setCollapsedAttachedToolbars((previous) => {
        let changed = false;
        const nextState = { ...previous };

        (Object.keys(attachedToolbars) as AttachableToolbarKey[]).forEach((key) => {
          const shouldCollapse =
            attachedToolbars[key] &&
            !previous[key] &&
            now - attachedToolbarLastUsedRef.current[key] >= ATTACHED_TOOLBAR_IDLE_HIDE_MS;

          if (shouldCollapse) {
            nextState[key] = true;
            changed = true;
          }
        });

        return changed ? nextState : previous;
      });
    }, 1000);

    return () => {
      window.clearInterval(interval);
    };
  }, [attachedToolbars]);

  useEffect(() => {
    const handleKeyDown = (event: KeyboardEvent) => {
      if (componentLibraryOpen && event.key === 'Escape') {
        setComponentLibraryOpen(false);
        return;
      }

      if (event.key === 'Escape') {
        setDraft(null);
        setDragState(null);
        setResizeState(null);
        setLiveShapes(null);
        setTool('select');
      }

      const isModifierPressed = event.ctrlKey || event.metaKey;
      if (isModifierPressed && event.key.toLowerCase() === 'z') {
        event.preventDefault();

        if (event.shiftKey) {
          handleRedo();
          return;
        }

        handleUndo();
        return;
      }

      if (isModifierPressed && event.key.toLowerCase() === 'y') {
        event.preventDefault();
        handleRedo();
        return;
      }

      if ((event.key === 'Delete' || event.key === 'Backspace') && selectedTarget) {
        const activeTag = document.activeElement instanceof HTMLElement ? document.activeElement.tagName : '';
        if (activeTag === 'INPUT' || activeTag === 'TEXTAREA') {
          return;
        }

        removeSelectedTarget();
      }
    };

    window.addEventListener('keydown', handleKeyDown);
    return () => {
      window.removeEventListener('keydown', handleKeyDown);
    };
  }, [componentLibraryOpen, removeSelectedTarget, selectedTarget]);

  useEffect(() => {
    if (typeof window === 'undefined') {
      return undefined;
    }

    const currentUrl = `${window.location.pathname}${window.location.search}${window.location.hash}`;
    const currentState =
      window.history.state && typeof window.history.state === 'object' ? window.history.state : {};

    if (!currentState[DRAW_HISTORY_GUARD_KEY]) {
      window.history.pushState(
        {
          ...currentState,
          [DRAW_HISTORY_GUARD_KEY]: true,
        },
        '',
        currentUrl,
      );
    }

    const handlePopState = (event: PopStateEvent) => {
      const nextState = event.state && typeof event.state === 'object' ? event.state : {};
      if (nextState[DRAW_HISTORY_GUARD_KEY]) {
        return;
      }

      window.history.pushState(
        {
          ...nextState,
          [DRAW_HISTORY_GUARD_KEY]: true,
        },
        '',
        currentUrl,
      );
    };

    window.addEventListener('popstate', handlePopState);
    return () => {
      window.removeEventListener('popstate', handlePopState);
    };
  }, []);

  useEffect(() => {
    const element = svgRef.current;
    if (!element || typeof window === 'undefined' || typeof document === 'undefined') {
      return undefined;
    }

    const root = document.documentElement;
    const body = document.body;
    const previousRootOverscrollBehavior = root.style.overscrollBehavior;
    const previousRootOverscrollBehaviorX = root.style.overscrollBehaviorX;
    const previousBodyOverscrollBehavior = body.style.overscrollBehavior;
    const previousBodyOverscrollBehaviorX = body.style.overscrollBehaviorX;
    const previousBodyTouchAction = body.style.touchAction;

    root.style.overscrollBehavior = 'none';
    root.style.overscrollBehaviorX = 'none';
    body.style.overscrollBehavior = 'none';
    body.style.overscrollBehaviorX = 'none';
    body.style.touchAction = 'none';

    let edgeGestureTracking:
      | null
      | {
          startX: number;
          startY: number;
          direction: 'back' | 'forward';
        } = null;

    const isEventInsideDrawPage = (target: EventTarget | null) =>
      target instanceof Node && element.contains(target);

    const resetEdgeGestureTracking = () => {
      edgeGestureTracking = null;
    };

    const handleTouchStart = (event: TouchEvent) => {
      const touch = event.touches[0];
      if (!touch || event.touches.length !== 1 || !isEventInsideDrawPage(event.target)) {
        edgeGestureTracking = null;
        return;
      }

      if (touch.clientX <= DRAW_EDGE_GESTURE_HOTZONE_PX) {
        edgeGestureTracking = {
          startX: touch.clientX,
          startY: touch.clientY,
          direction: 'back',
        };
        return;
      }

      if (touch.clientX >= window.innerWidth - DRAW_EDGE_GESTURE_HOTZONE_PX) {
        edgeGestureTracking = {
          startX: touch.clientX,
          startY: touch.clientY,
          direction: 'forward',
        };
        return;
      }

      edgeGestureTracking = null;
    };

    const handleTouchMove = (event: TouchEvent) => {
      const touch = event.touches[0];
      if (!touch || !edgeGestureTracking || !isEventInsideDrawPage(event.target)) {
        return;
      }

      const deltaX = touch.clientX - edgeGestureTracking.startX;
      const deltaY = touch.clientY - edgeGestureTracking.startY;
      const horizontalSwipe =
        edgeGestureTracking.direction === 'back' ? deltaX >= DRAW_EDGE_GESTURE_MIN_SWIPE_PX : deltaX <= -DRAW_EDGE_GESTURE_MIN_SWIPE_PX;
      const verticalDrift = Math.abs(deltaY);

      if (horizontalSwipe && verticalDrift <= DRAW_EDGE_GESTURE_MAX_VERTICAL_DRIFT_PX) {
        event.preventDefault();
      }
    };

    const handleGestureEvent = (event: Event) => {
      if (!isEventInsideDrawPage(event.target)) {
        return;
      }

      event.preventDefault();
    };

    document.addEventListener('touchstart', handleTouchStart, { passive: true, capture: true });
    document.addEventListener('touchmove', handleTouchMove, { passive: false, capture: true });
    document.addEventListener('touchend', resetEdgeGestureTracking, { passive: true, capture: true });
    document.addEventListener('touchcancel', resetEdgeGestureTracking, { passive: true, capture: true });
    document.addEventListener('gesturestart', handleGestureEvent, { passive: false, capture: true });
    document.addEventListener('gesturechange', handleGestureEvent, { passive: false, capture: true });

    return () => {
      root.style.overscrollBehavior = previousRootOverscrollBehavior;
      root.style.overscrollBehaviorX = previousRootOverscrollBehaviorX;
      body.style.overscrollBehavior = previousBodyOverscrollBehavior;
      body.style.overscrollBehaviorX = previousBodyOverscrollBehaviorX;
      body.style.touchAction = previousBodyTouchAction;
      document.removeEventListener('touchstart', handleTouchStart, true);
      document.removeEventListener('touchmove', handleTouchMove, true);
      document.removeEventListener('touchend', resetEdgeGestureTracking, true);
      document.removeEventListener('touchcancel', resetEdgeGestureTracking, true);
      document.removeEventListener('gesturestart', handleGestureEvent, true);
      document.removeEventListener('gesturechange', handleGestureEvent, true);
    };
  }, []);

  const beginLineDraft = (x: number, y: number) => {
    setDraft({
      type: 'line',
      startX: x,
      startY: y,
      x1: x,
      y1: y,
      orientation: null,
      x2: x,
      y2: y,
    });
  };

  const updateLineDraft = (x: number, y: number) => {
    setDraft((previous) => {
      if (!previous || previous.type !== 'line') {
        return previous;
      }

      const deltaX = x - previous.startX;
      const deltaY = y - previous.startY;
      const draftDistance = Math.hypot(deltaX, deltaY);

      if (!previous.orientation && draftDistance < DRAW_LINE_ORIENTATION_LOCK_DISTANCE) {
        return {
          ...previous,
          x1: previous.startX,
          y1: previous.startY,
          x2: previous.startX,
          y2: previous.startY,
        };
      }

      const orientation: LineOrientation =
        previous.orientation ?? (Math.abs(deltaX) >= Math.abs(deltaY) ? 'horizontal' : 'vertical');

      const nextLine = resolveLineDraft(
        previous.startX,
        previous.startY,
        x,
        y,
        shapesRef.current.filter(isDrawableShape),
        canvasSize.width,
        canvasSize.height,
        orientation,
      );

      return {
        ...previous,
        orientation,
        x1: nextLine.startX,
        y1: nextLine.startY,
        x2: nextLine.endX,
        y2: nextLine.endY,
      };
    });
  };

  const commitLineDraft = () => {
    setDraft((previous) => {
      if (!previous || previous.type !== 'line') {
        return previous;
      }

      const samePoint = previous.x1 === previous.x2 && previous.y1 === previous.y2;
      if (samePoint) {
        return null;
      }

      const orientation = previous.y1 === previous.y2 ? 'horizontal' : 'vertical';
      const nextShape: DrawLine = {
        id: createShapeId('line'),
        type: 'line',
        x1: previous.x1,
        y1: previous.y1,
        x2: previous.x2,
        y2: previous.y2,
        orientation,
        label: '',
      };
      commitDocument(backgroundMode, [...shapesRef.current, nextShape]);
      setSelectedShapes([nextShape.id], nextShape.id);
      return null;
    });
  };

  const beginRectDraft = (x: number, y: number) => {
    setDraft({
      type: 'rect',
      startX: x,
      startY: y,
      endX: x,
      endY: y,
    });
  };

  const updateRectDraft = (x: number, y: number) => {
    setDraft((previous) => {
      if (!previous || previous.type !== 'rect') {
        return previous;
      }

      return {
        ...previous,
        endX: x,
        endY: y,
      };
    });
  };

  const commitRectDraft = () => {
    setDraft((previous) => {
      if (!previous || previous.type !== 'rect') {
        return previous;
      }

      const nextRect = clampRect(previous.startX, previous.startY, previous.endX, previous.endY);
      if (nextRect.width === 0 || nextRect.height === 0) {
        return null;
      }

      const nextShape: DrawRect = {
        id: createShapeId('rect'),
        type: 'rect',
        ...nextRect,
        label: '',
      };
      commitDocument(backgroundMode, [...shapesRef.current, nextShape]);
      setSelectedShapes([nextShape.id], nextShape.id);
      return null;
    });
  };

  const clearCanvas = () => {
    setDraft(null);
    setDragState(null);
    setSelectionBox(null);
    setResizeState(null);
    setLiveShapes(null);
    clearShapeSelection();
    dragStartShapesRef.current = null;
    commitDocument(backgroundMode, []);
    setTool('select');
  };

  const handlePaintRect = (shape: DrawRect) => {
    const nextFillColor = paintColor === 'transparent' ? null : paintColor;
    if ((shape.fillColor ?? null) === nextFillColor) {
      setSelectedShapes([shape.id], shape.id);
      return;
    }

    const nextShapes = shapesRef.current.map((item) =>
      item.id === shape.id && item.type === 'rect'
        ? {
            ...item,
            fillColor: nextFillColor,
          }
        : item,
    );
    commitDocument(backgroundMode, nextShapes);
    setSelectedShapes([shape.id], shape.id);
  };

  const handlePaintRegion = (regionKey: string) => {
    const nextFillColor = paintColor === 'transparent' ? null : paintColor;
    const existingRegionShape = regionAssignments.find((shape) => shape.regionKey === regionKey) ?? null;

    if ((existingRegionShape?.fillColor ?? null) === nextFillColor) {
      setSelectedTarget({ kind: 'region', regionKey });
      return;
    }

    const nextShapes = existingRegionShape
      ? !nextFillColor && !existingRegionShape.label
        ? shapesRef.current.filter((shape) => !(isRegionShape(shape) && shape.id === existingRegionShape.id))
        : shapesRef.current.map((shape) =>
            isRegionShape(shape) && shape.id === existingRegionShape.id
              ? {
                  ...shape,
                  fillColor: nextFillColor,
                }
              : shape,
          )
      : nextFillColor
        ? [
            ...shapesRef.current,
            {
              id: createShapeId('region'),
              type: 'region',
              regionKey,
              label: '',
              fillColor: nextFillColor,
            } satisfies DrawRegion,
          ]
        : shapesRef.current;

    commitDocument(backgroundMode, nextShapes);
    setSelectedTarget({ kind: 'region', regionKey });
  };

  const handleSaveComponent = async () => {
    const name = componentSaveName.trim();
    const category = componentSaveCategory.trim() || '내 컴포넌트';
    if (!name) {
      setStorageError('컴포넌트 이름을 입력해 주세요.');
      return;
    }
    if (selectedDrawableShapes.length === 0) {
      setStorageError('저장할 컴포넌트를 먼저 선택해 주세요.');
      return;
    }

    setIsComponentSaving(true);
    setStorageError(null);
    const now = new Date().toISOString();

    try {
      await saveLayoutDrawComponent({
        id: createShapeId('component'),
        name,
        category,
        createdAt: now,
        updatedAt: now,
        shapes: rebaseShapesToComponentBlueprint(selectedDrawableShapes).map((shape) => ({
          ...shape,
          groupId: selectedDrawableShapes.length > 1 ? 'component-group' : shape.groupId ?? null,
        })),
      });
      setComponentSaveOpen(false);
      await loadSavedComponents();
      messageApi.success(`"${name}" 컴포넌트를 저장했습니다.`);
    } catch (error) {
      setStorageError(error instanceof Error ? error.message : '컴포넌트 저장에 실패했습니다.');
    } finally {
      setIsComponentSaving(false);
    }
  };

  const handleCanvasPointerDown = (event: React.PointerEvent<SVGSVGElement>) => {
    const element = svgRef.current;
    if (!element) {
      return;
    }

    const point = resolveSvgPoint(event, element);
    const target = event.target as SVGElement | null;
    const resizeHandle = (target?.dataset.resizeHandle as ResizeHandle | undefined) ?? null;
    const shapeId = target?.dataset.shapeId ?? null;
    const regionKey = target?.dataset.regionKey ?? null;
    const currentDrawableShapes = splitDrawShapes(shapesRef.current).drawableShapes;
    const currentDrawRegions = resolveDrawRegions(shapesRef.current, canvasSize.width, canvasSize.height);
    const isAdditiveSelection = event.shiftKey || event.metaKey || event.ctrlKey;
    const resizeShapeTarget =
      resizeHandle && selectedShape ? currentDrawableShapes.find((shape) => shape.id === selectedShape.id) ?? null : null;

    if (resizeHandle && resizeShapeTarget && tool === 'select') {
      const groupedIds = resolveGroupedShapeIds(currentDrawableShapes, [resizeShapeTarget.id]);
      const baseShapes = currentDrawableShapes
        .filter((shape) => groupedIds.has(shape.id))
        .map((shape) => cloneShape(shape) as DrawableShape);
      const bounds = resolveDrawableShapesBounds(baseShapes);
      if (!bounds) {
        return;
      }

      dragStartShapesRef.current = shapesRef.current.map(cloneShape);
      setResizeState({
        shapeId: resizeShapeTarget.id,
        handle: resizeHandle,
        originX: point.x,
        originY: point.y,
        baseShape: cloneShape(resizeShapeTarget) as DrawableShape,
        groupedIds,
        baseShapes,
        bounds,
      });
      setLiveShapes(shapesRef.current.map(cloneShape));
      element.setPointerCapture(event.pointerId);
      event.preventDefault();
      event.stopPropagation();
      return;
    }

    const hitShape =
      (shapeId ? currentDrawableShapes.find((shape) => shape.id === shapeId) ?? null : null) ??
      findShapeAtPoint(currentDrawableShapes, point.x, point.y);
    const hitRegion =
      hitShape || !currentDrawableShapes.some((shape) => shape.type === 'line')
        ? null
        : (regionKey ? currentDrawRegions.find((region) => region.key === regionKey) ?? null : null) ??
          findRegionAtPoint(currentDrawRegions, point.x, point.y);

    if (hitShape && tool === 'paint') {
      if (hitShape.type === 'rect') {
        handlePaintRect(hitShape);
      } else {
        setSelectedShapes([hitShape.id], hitShape.id);
      }
      return;
    }

    if (hitRegion && tool === 'paint') {
      handlePaintRegion(hitRegion.key);
      return;
    }

    if (hitShape && tool === 'select') {
      const groupedIds = resolveGroupedShapeIds(currentDrawableShapes, [hitShape.id]);
      const nextIds = [...groupedIds];
      if (isAdditiveSelection) {
        const previousIds = new Set(selectedShapeIdsState);
        const shouldRemove = nextIds.every((id) => previousIds.has(id));
        const mergedIds = shouldRemove
          ? selectedShapeIdsState.filter((id) => !groupedIds.has(id))
          : [...new Set([...selectedShapeIdsState, ...nextIds])];
        setSelectedShapes(mergedIds, shouldRemove ? null : hitShape.id);
        return;
      }

      setSelectedShapes(nextIds, hitShape.id);
      dragStartShapesRef.current = shapesRef.current.map(cloneShape);

      setDragState({
        shapeIds: nextIds,
        pointerX: point.x,
        pointerY: point.y,
      });
      if (groupedIds.size > 1) {
        setShapeLabelDraft(hitShape.label);
      }
      element.setPointerCapture(event.pointerId);

      return;
    }

    if (hitRegion && tool === 'select') {
      setSelectedShapeIdsState([]);
      setSelectedTarget({ kind: 'region', regionKey: hitRegion.key });
      return;
    }

    if (tool === 'select' || tool === 'paint') {
      if (tool === 'select') {
        setSelectionBox({
          startX: point.x,
          startY: point.y,
          endX: point.x,
          endY: point.y,
          additive: isAdditiveSelection,
        });
        element.setPointerCapture(event.pointerId);
        return;
      }

      setSelectedTarget(null);
      setSelectedShapeIdsState([]);
      return;
    }

    setSelectedTarget(null);
    setSelectedShapeIdsState([]);
    element.setPointerCapture(event.pointerId);

    if (tool === 'line') {
      beginLineDraft(point.x, point.y);
      return;
    }

    beginRectDraft(point.x, point.y);
  };

  const handleCanvasPointerMove = (event: React.PointerEvent<SVGSVGElement>) => {
    const element = svgRef.current;
    if (!element) {
      return;
    }

    const point = resolveSvgPoint(event, element);

    if (resizeState) {
      const currentShapes = liveShapes ?? shapesRef.current;
      const resizedShapes =
        resizeState.groupedIds.size > 1
          ? resizeGroupedShapes(resizeState.baseShapes, resizeState.bounds, resizeState.handle, point.x, point.y)
          : [resizeShape(resizeState.baseShape, resizeState.handle, point.x, point.y)];
      const resizedShapeMap = new Map(resizedShapes.map((shape) => [shape.id, shape]));
      setLiveShapes(
        currentShapes.map((shape) =>
          isDrawableShape(shape) && resizeState.groupedIds.has(shape.id) ? (resizedShapeMap.get(shape.id) ?? shape) : shape,
        ),
      );
      return;
    }

    if (dragState) {
      const deltaX = point.x - dragState.pointerX;
      const deltaY = point.y - dragState.pointerY;
      if (deltaX === 0 && deltaY === 0) {
        return;
      }

      const currentShapes = liveShapes ?? shapesRef.current;
      setLiveShapes(
        currentShapes.map((shape) =>
          isDrawableShape(shape) && dragState.shapeIds.includes(shape.id) ? moveShape(shape, deltaX, deltaY) : shape,
        ),
      );
      setDragState({
        shapeIds: dragState.shapeIds,
        pointerX: point.x,
        pointerY: point.y,
      });
      return;
    }

    if (selectionBox) {
      setSelectionBox((previous) => (previous ? { ...previous, endX: point.x, endY: point.y } : previous));
      return;
    }

    if (draft?.type === 'line') {
      updateLineDraft(point.x, point.y);
      return;
    }

    if (draft?.type === 'rect') {
      updateRectDraft(point.x, point.y);
    }
  };

  const handleCanvasPointerUp = (event: React.PointerEvent<SVGSVGElement>) => {
    const element = svgRef.current;
    if (element?.hasPointerCapture(event.pointerId)) {
      element.releasePointerCapture(event.pointerId);
    }

    if (resizeState) {
      const nextShapes = liveShapes ?? shapesRef.current;
      setLiveShapes(null);
      setResizeState(null);
      if (dragStartShapesRef.current) {
        commitDocument(backgroundMode, nextShapes);
      }
      dragStartShapesRef.current = null;
      return;
    }

    if (dragState) {
      const nextShapes = liveShapes ?? shapesRef.current;
      setLiveShapes(null);
      setDragState(null);
      if (dragStartShapesRef.current) {
        commitDocument(backgroundMode, nextShapes);
      }
      dragStartShapesRef.current = null;
      return;
    }

    if (selectionBox) {
      const nextSelection = clampSelectionRect(
        selectionBox.startX,
        selectionBox.startY,
        selectionBox.endX,
        selectionBox.endY,
      );
      const hitShapes = findShapesInSelection(splitDrawShapes(shapesRef.current).drawableShapes, nextSelection);
      const nextIds = hitShapes.map((shape) => shape.id);
      if (selectionBox.additive) {
        setSelectedShapes([...new Set([...selectedShapeIdsState, ...nextIds])], nextIds[0] ?? selectedShapeId);
      } else {
        setSelectedShapes(nextIds, nextIds[0] ?? null);
      }
      setSelectionBox(null);
      return;
    }

    if (draft?.type === 'line') {
      commitLineDraft();
      return;
    }

    if (draft?.type === 'rect') {
      commitRectDraft();
    }
  };

  const beginFloatingBoxDrag = (key: FloatingBoxKey, event: React.PointerEvent<HTMLElement>) => {
    if ((key === 'tools' || key === 'actions') && attachedToolbars[key]) {
      return;
    }

    const box = floatingBoxRefs.current[key];
    if (!box) {
      return;
    }

    const rect = box.getBoundingClientRect();
    event.preventDefault();
    event.stopPropagation();
    bringFloatingBoxToFront(key);
    setDraggingBox({
      key,
      pointerOffsetX: event.clientX - rect.left,
      pointerOffsetY: event.clientY - rect.top,
    });
  };

  useEffect(() => {
    setShapeLabelDraft(selectedShapeIds.size > 1 ? '' : selectedShape?.label ?? selectedRegion?.label ?? '');
  }, [selectedRegion?.label, selectedShape, selectedShapeIds.size]);

  const selectedShapeResizeHandles =
    tool === 'select' &&
    selectedShape &&
    (selectedShapeIds.size === 1 ||
      (selectedShape.groupId ? selectedDrawableShapes.every((shape) => shape.groupId === selectedShape.groupId) : false))
      ? (() => {
          if (selectedShape.type === 'line') {
            return resolveLineResizeHandles(selectedShape);
          }

          const groupedShapes =
            selectedShape.groupId
              ? drawableShapes.filter((shape) => shape.groupId === selectedShape.groupId)
              : [selectedShape];
          const groupedBounds = resolveDrawableShapesBounds(groupedShapes);
          if (!groupedBounds) {
            return resolveRectResizeHandles(selectedShape);
          }

          return resolveRectResizeHandles({
            id: 'group-resize-frame',
            type: 'rect',
            x: groupedBounds.x,
            y: groupedBounds.y,
            width: groupedBounds.width,
            height: groupedBounds.height,
            label: selectedShape.label,
          });
        })()
      : [];

  const toolsAttachButton = (
    <Button
      className="layout-draw-page__icon-button layout-draw-page__icon-button--chrome"
      size="small"
      type={attachedToolbars.tools ? 'primary' : 'default'}
      icon={<PushpinOutlined />}
      aria-label={attachedToolbars.tools ? '그리기 도구 고정 해제' : '그리기 도구 attach 고정'}
      onClick={() => toggleToolbarAttached('tools')}
    />
  );

  const actionsAttachButton = (
    <Button
      className="layout-draw-page__icon-button layout-draw-page__icon-button--chrome"
      size="small"
      type={attachedToolbars.actions ? 'primary' : 'default'}
      icon={<PushpinOutlined />}
      aria-label={attachedToolbars.actions ? '편집 도구 고정 해제' : '편집 도구 attach 고정'}
      onClick={() => toggleToolbarAttached('actions')}
    />
  );

  const mobilePrimaryActionButtons = (
    <div
      className="layout-draw-page__tool-group layout-draw-page__tool-group--compact"
      aria-label="빠른 편집"
    >
      <div className="layout-draw-page__toolbar-buttons layout-draw-page__toolbar-buttons--row">
        <Button
          className="layout-draw-page__icon-button"
          shape="circle"
          icon={<InfoCircleOutlined />}
          type={guideVisible ? 'primary' : 'default'}
          onClick={() => setGuideVisible((previous) => !previous)}
        />
        <Button className="layout-draw-page__icon-button" shape="circle" icon={<UndoOutlined />} disabled={!canUndo} onClick={handleUndo} />
        <Button className="layout-draw-page__icon-button" shape="circle" icon={<RedoOutlined />} disabled={!canRedo} onClick={handleRedo} />
      </div>
    </div>
  );

  const editButtons = (
    <>
      <Button
        className="layout-draw-page__icon-button"
        shape="circle"
        icon={<CopyOutlined />}
        disabled={selectedDrawableShapes.length === 0}
        onClick={handleCopySelectedShape}
      />
      <Button
        className="layout-draw-page__icon-button"
        shape="circle"
        icon={<LinkOutlined />}
        disabled={selectedDrawableShapes.length < 2}
        aria-label="선택 오브젝트 그룹 묶기"
        onClick={handleGroupSelectedShapes}
      />
      <Button
        className="layout-draw-page__icon-button"
        shape="circle"
        icon={<DisconnectOutlined />}
        disabled={selectedDrawableShapes.length === 0 || !selectedDrawableShapes.some((shape) => shape.groupId)}
        aria-label="선택 그룹 해제"
        onClick={handleUngroupSelectedShapes}
      />
      <Button
        className="layout-draw-page__icon-button"
        shape="circle"
        icon={<SaveOutlined />}
        disabled={selectedDrawableShapes.length === 0}
        aria-label="선택 오브젝트 컴포넌트 저장"
        onClick={openComponentSaveModal}
      />
      <Button
        className="layout-draw-page__icon-button"
        shape="circle"
        icon={<DeleteOutlined />}
        disabled={!selectedTarget && selectedDrawableShapes.length === 0}
        onClick={removeSelectedTarget}
      />
    </>
  );

  const historyButtons = (
    <>
      <Button className="layout-draw-page__icon-button" shape="circle" icon={<UndoOutlined />} disabled={!canUndo} onClick={handleUndo} />
      <Button className="layout-draw-page__icon-button" shape="circle" icon={<RedoOutlined />} disabled={!canRedo} onClick={handleRedo} />
    </>
  );

  const fileButtons = (
    <>
      <Button className="layout-draw-page__icon-button" shape="circle" icon={<SaveOutlined />} onClick={openSavePanel} />
      <Button className="layout-draw-page__icon-button" shape="circle" icon={<FolderOpenOutlined />} onClick={openLoadPanel} />
      <Button className="layout-draw-page__icon-button" shape="circle" icon={<PlusOutlined />} onClick={clearCanvas} />
    </>
  );

  const viewButtons = (
    <>
      <Button
        className="layout-draw-page__icon-button"
        shape="circle"
        icon={<AppstoreOutlined />}
        aria-label="컴포넌트 모음 열기"
        onClick={openComponentLibrary}
      />
      <Button
        className="layout-draw-page__icon-button"
        shape="circle"
        icon={<InfoCircleOutlined />}
        type={guideVisible ? 'primary' : 'default'}
        onClick={() => setGuideVisible((previous) => !previous)}
      />
      <Button
        shape="circle"
        icon={<BgColorsOutlined />}
        className="layout-draw-page__icon-button layout-draw-page__action-button layout-draw-page__action-button--background"
        onClick={() => commitDocument(backgroundMode === 'grid' ? 'plain' : 'grid', shapesRef.current)}
      />
    </>
  );

  const toolButtons = DRAW_TOOL_OPTIONS
    .filter((option) => option.key === tool)
    .concat(DRAW_TOOL_OPTIONS.filter((option) => option.key !== tool))
    .map((option) => (
      <Button
        key={option.key}
        className="layout-draw-page__icon-button"
        type={tool === option.key ? 'primary' : 'default'}
        shape="circle"
        icon={option.icon}
        onClick={() => setTool(option.key)}
      />
    ));

  return (
    <div ref={pageRef} className={`layout-draw-page layout-draw-page--${backgroundMode}`}>
      {messageContextHolder}
      {collapsedAttachedToolbars.tools ? (
        <button
          type="button"
          ref={assignFloatingBoxRef('tools')}
          className="layout-draw-page__toolbar-anchor"
          style={{
            left: floatingBoxPositions.tools.x,
            top: floatingBoxPositions.tools.y,
            zIndex: resolveFloatingBoxZIndex('tools'),
          }}
          aria-label="그리기 도구모음 다시 열기"
          onPointerDownCapture={() => bringFloatingBoxToFront('tools')}
          onFocusCapture={() => bringFloatingBoxToFront('tools')}
          onClick={() => expandAttachedToolbar('tools')}
        >
          {tool === 'select' ? <DragOutlined /> : tool === 'line' ? <LineOutlined /> : tool === 'rect' ? <BorderOutlined /> : <BgColorsOutlined />}
        </button>
      ) : (
        <aside
          ref={assignFloatingBoxRef('tools')}
          className={`layout-draw-page__toolbar layout-draw-page__toolbar--tools${attachedToolbars.tools ? ' layout-draw-page__toolbar--attached' : ''}`}
          style={{
            left: floatingBoxPositions.tools.x,
            top: floatingBoxPositions.tools.y,
            zIndex: resolveFloatingBoxZIndex('tools'),
          }}
          onPointerDownCapture={() => markToolbarInteraction('tools')}
          onFocusCapture={() => markToolbarInteraction('tools')}
        >
          <div className="layout-draw-page__toolbar-chrome">
            <button
              type="button"
              className="layout-draw-page__box-handle"
              aria-label="도구 박스 이동"
              disabled={attachedToolbars.tools}
              onPointerDown={(event) => beginFloatingBoxDrag('tools', event)}
            >
              <DragOutlined />
            </button>
            {toolsAttachButton}
          </div>
          <div className="layout-draw-page__tool-group" aria-label="그리기 도구">
            <div className="layout-draw-page__toolbar-buttons layout-draw-page__toolbar-buttons--tools">
              {toolButtons}
            </div>
            {tool === 'paint' ? (
              <div className="layout-draw-page__paint-palette" aria-label="색 채우기 팔레트">
                {PAINT_COLOR_OPTIONS.map((option) => (
                  <button
                    key={option.value}
                    type="button"
                    className={`layout-draw-page__paint-swatch${paintColor === option.value ? ' layout-draw-page__paint-swatch--active' : ''}${option.value === 'transparent' ? ' layout-draw-page__paint-swatch--clear' : ''}`}
                    style={option.value === 'transparent' ? undefined : { background: option.value }}
                    aria-label={`${option.label} 채우기`}
                    title={option.label}
                    onClick={() => setPaintColor(option.value)}
                  >
                    {option.value === 'transparent' ? 'X' : null}
                  </button>
                ))}
              </div>
            ) : null}
          </div>
        </aside>
      )}
      {collapsedAttachedToolbars.actions ? (
        <button
          type="button"
          ref={assignFloatingBoxRef('actions')}
          className="layout-draw-page__toolbar-anchor"
          style={{
            left: floatingBoxPositions.actions.x,
            top: floatingBoxPositions.actions.y,
            zIndex: resolveFloatingBoxZIndex('actions'),
          }}
          aria-label="편집 도구모음 다시 열기"
          onPointerDownCapture={() => bringFloatingBoxToFront('actions')}
          onFocusCapture={() => bringFloatingBoxToFront('actions')}
          onClick={() => expandAttachedToolbar('actions')}
        >
          <MenuUnfoldOutlined />
        </button>
      ) : (
        <aside
          ref={assignFloatingBoxRef('actions')}
          className={`layout-draw-page__toolbar layout-draw-page__toolbar--actions${isMobileViewport && mobileActionsExpanded ? ' layout-draw-page__toolbar--mobile-expanded' : ''}${attachedToolbars.actions ? ' layout-draw-page__toolbar--attached' : ''}`}
          style={{
            left: floatingBoxPositions.actions.x,
            top: floatingBoxPositions.actions.y,
            zIndex: resolveFloatingBoxZIndex('actions'),
          }}
          onPointerDownCapture={() => markToolbarInteraction('actions')}
          onFocusCapture={() => markToolbarInteraction('actions')}
        >
          <div className="layout-draw-page__toolbar-chrome">
            <button
              type="button"
              className="layout-draw-page__box-handle"
              aria-label="편집 도구 박스 이동"
              disabled={attachedToolbars.actions}
              onPointerDown={(event) => beginFloatingBoxDrag('actions', event)}
            >
              <DragOutlined />
            </button>
            {actionsAttachButton}
          </div>
          {isMobileViewport ? (
            <>
              {mobilePrimaryActionButtons}
              <div
                className="layout-draw-page__tool-group layout-draw-page__tool-group--toggle"
                aria-label="추가 기능"
              >
                <div className="layout-draw-page__toolbar-buttons layout-draw-page__toolbar-buttons--row">
                  <Button
                    className="layout-draw-page__icon-button"
                    shape="circle"
                    icon={mobileActionsExpanded ? <MenuFoldOutlined /> : <MenuUnfoldOutlined />}
                    onClick={() => setMobileActionsExpanded((previous) => !previous)}
                  />
                </div>
              </div>
            </>
          ) : (
            <>
              <div className="layout-draw-page__tool-group" aria-label="표시 도구">
                <div className="layout-draw-page__toolbar-buttons layout-draw-page__toolbar-buttons--actions">
                  {viewButtons}
                </div>
              </div>
              <div className="layout-draw-page__tool-group" aria-label="편집 도구">
                <div className="layout-draw-page__toolbar-buttons layout-draw-page__toolbar-buttons--actions">
                  {editButtons}
                </div>
                <div className="layout-draw-page__label-editor">
                  <Input
                    size="small"
                    value={shapeLabelDraft}
                    placeholder={
                      selectedShapeIds.size > 1
                        ? '다중 선택 중에는 개별 라벨 편집이 잠깁니다'
                        : selectedTarget
                          ? '선택 영역 또는 객체 라벨 입력'
                          : '영역 또는 객체를 선택하세요'
                    }
                    disabled={!selectedTarget || selectedShapeIds.size > 1}
                    aria-label="선택 객체 라벨 입력"
                    maxLength={40}
                    onChange={(event) => setShapeLabelDraft(event.target.value)}
                    onBlur={handleSelectedShapeLabelChange}
                    onPressEnter={handleSelectedShapeLabelChange}
                  />
                </div>
              </div>
              <div className="layout-draw-page__tool-group" aria-label="이력 도구">
                <div className="layout-draw-page__toolbar-buttons layout-draw-page__toolbar-buttons--actions">
                  {historyButtons}
                </div>
              </div>
              <div className="layout-draw-page__tool-group" aria-label="파일 도구">
                <div className="layout-draw-page__toolbar-buttons layout-draw-page__toolbar-buttons--actions">
                  {fileButtons}
                </div>
              </div>
            </>
          )}
          {isMobileViewport && mobileActionsExpanded ? (
            <>
              <div className="layout-draw-page__tool-group" aria-label="표시 도구">
                <div className="layout-draw-page__toolbar-buttons layout-draw-page__toolbar-buttons--mobile-actions">
                  {viewButtons}
                </div>
              </div>
              <div className="layout-draw-page__tool-group" aria-label="편집 도구">
                <div className="layout-draw-page__toolbar-buttons layout-draw-page__toolbar-buttons--mobile-actions">
                  {editButtons}
                </div>
                <div className="layout-draw-page__label-editor">
                  <Input
                    size="small"
                    value={shapeLabelDraft}
                    placeholder={
                      selectedShapeIds.size > 1
                        ? '다중 선택 중에는 개별 라벨 편집이 잠깁니다'
                        : selectedTarget
                          ? '선택 영역 또는 객체 라벨 입력'
                          : '영역 또는 객체를 선택하세요'
                    }
                    disabled={!selectedTarget || selectedShapeIds.size > 1}
                    aria-label="선택 객체 라벨 입력"
                    maxLength={40}
                    onChange={(event) => setShapeLabelDraft(event.target.value)}
                    onBlur={handleSelectedShapeLabelChange}
                    onPressEnter={handleSelectedShapeLabelChange}
                  />
                </div>
              </div>
              <div className="layout-draw-page__tool-group" aria-label="파일 도구">
                <div className="layout-draw-page__toolbar-buttons layout-draw-page__toolbar-buttons--mobile-actions">
                  {fileButtons}
                </div>
              </div>
            </>
          ) : null}
        </aside>
      )}
      <Modal
        open={componentLibraryOpen}
        footer={null}
        width={isMobileViewport ? 'calc(100vw - 16px)' : 1100}
        centered
        destroyOnClose={false}
        className="layout-draw-page__component-modal"
        title="컴포넌트 모음"
        onCancel={() => setComponentLibraryOpen(false)}
      >
        <div className="layout-draw-page__component-modal-copy">
          <strong>Draw에서 바로 쓰는 업무형 화면 블록과 직접 저장한 컴포넌트를 같은 라이브러리에서 불러옵니다.</strong>
          <span>단순 박스 골격보다 화면 썸네일처럼 보이는 샘플을 앞에 두고, 선택하면 비슷한 밀도의 구조가 도면 중앙에 바로 들어가도록 정리했습니다.</span>
        </div>
        {isComponentStorageLoading ? (
          <div className="layout-draw-page__component-modal-loading">
            <Spin size="small" />
          </div>
        ) : !activeComponentLibraryGroup || activeComponentLibraryGroup.items.length === 0 ? (
          <Empty description="표시할 컴포넌트 샘플이 없습니다." image={Empty.PRESENTED_IMAGE_SIMPLE} />
        ) : (
          <Tabs
            activeKey={componentLibrarySection}
            className="layout-draw-page__component-tabs"
            onChange={(activeKey) => setComponentLibrarySection(activeKey as ComponentLibrarySectionKey)}
            items={componentLibraryGroups.map((section) => ({
              key: section.key,
              label: section.label,
              children: (
                <div className="layout-draw-page__component-grid">
                  {section.items.map((item) => (
                    <Card
                      key={item.presetId}
                      size="small"
                      className="layout-draw-page__component-card"
                      title={item.title}
                      extra={
                        item.source === 'saved' ? (
                          <Button
                            size="small"
                            danger
                            onClick={() => {
                              if (!item.savedRecordId) {
                                return;
                              }
                              void deleteLayoutDrawComponent(item.savedRecordId)
                                .then(async () => {
                                  await loadSavedComponents();
                                  messageApi.success(`"${item.title}" 컴포넌트를 삭제했습니다.`);
                                })
                                .catch((error) => {
                                  setStorageError(error instanceof Error ? error.message : '컴포넌트 삭제에 실패했습니다.');
                                });
                            }}
                          >
                            삭제
                          </Button>
                        ) : null
                      }
                    >
                      <div className="layout-draw-page__component-card-preview">
                        {renderComponentPreview(item.previewKind)}
                      </div>
                      <div className="layout-draw-page__component-card-copy">
                        <Paragraph>{item.description}</Paragraph>
                        <div className="layout-draw-page__component-card-meta">
                          <Text type="secondary">{item.source === 'saved' ? item.category : item.title}</Text>
                          <Button type="primary" size="small" onClick={() => handleInsertComponentPreset(item)}>
                            도면에 추가
                          </Button>
                        </div>
                      </div>
                    </Card>
                  ))}
                </div>
              ),
            }))}
          />
        )}
      </Modal>
      <Modal
        open={componentSaveOpen}
        title="선택 컴포넌트 저장"
        okText="저장"
        cancelText="취소"
        confirmLoading={isComponentSaving}
        onOk={() => void handleSaveComponent()}
        onCancel={() => setComponentSaveOpen(false)}
      >
        <div className="layout-draw-page__component-save-form">
          <span className="layout-draw-page__panel-label">컴포넌트 이름</span>
          <Input
            value={componentSaveName}
            placeholder="예: 상단 필터 바"
            onChange={(event) => setComponentSaveName(event.target.value)}
            onPressEnter={() => void handleSaveComponent()}
          />
          <span className="layout-draw-page__panel-label">분류</span>
          <Input
            value={componentSaveCategory}
            placeholder="예: 내 컴포넌트"
            onChange={(event) => setComponentSaveCategory(event.target.value)}
            onPressEnter={() => void handleSaveComponent()}
          />
          <span className="layout-draw-page__panel-meta">
            선택 {selectedDrawableShapes.length}개 오브젝트를 한 컴포넌트로 저장합니다.
          </span>
          {storageError ? <p className="layout-draw-page__panel-error">{storageError}</p> : null}
        </div>
      </Modal>
      {guideVisible ? (
        <div
          ref={assignFloatingBoxRef('guide')}
          className="layout-draw-page__status"
          style={{
            left: floatingBoxPositions.guide.x,
            top: floatingBoxPositions.guide.y,
            zIndex: resolveFloatingBoxZIndex('guide'),
          }}
          onPointerDownCapture={() => bringFloatingBoxToFront('guide')}
          onFocusCapture={() => bringFloatingBoxToFront('guide')}
        >
          <div className="layout-draw-page__status-header">
            <button
              type="button"
              className="layout-draw-page__box-handle layout-draw-page__box-handle--status"
              aria-label="작업 안내 카드 이동"
              onPointerDown={(event) => beginFloatingBoxDrag('guide', event)}
            >
              <DragOutlined />
            </button>
            <Button type="text" size="small" icon={<EyeInvisibleOutlined />} onClick={() => setGuideVisible(false)}>
              숨기기
            </Button>
          </div>
          <strong>{tool === 'select' ? '선택' : tool === 'line' ? '구분선' : tool === 'rect' ? '사각형' : '색 채우기'}</strong>
          <span>
            {tool === 'select'
              ? '기존 오브젝트 선택과 이동'
              : tool === 'line'
                ? '기존 선 선택 없이 새 구분선 바로 그리기'
                : tool === 'rect'
                  ? '기존 도형 선택 없이 새 사각형 바로 그리기'
                  : '사각형과 선으로 나뉜 영역을 눌러 선택한 색으로 채우기'}
          </span>
        </div>
      ) : null}
      {storagePanel ? (
        <section
          className="layout-draw-page__panel"
          aria-label={storagePanel === 'save' ? '도면 저장' : '도면 불러오기'}
        >
          <div className="layout-draw-page__panel-header">
            <strong>
              {storagePanel === 'save' ? '현재 도면 저장' : '저장된 도면 불러오기'}
            </strong>
            <Button
              type="text"
              size="small"
              onClick={() => {
                setStoragePanel(null);
                setStorageError(null);
              }}
            >
              닫기
            </Button>
          </div>
          {storagePanel === 'save' ? (
            <div className="layout-draw-page__panel-body">
              <span className="layout-draw-page__panel-label">저장 이름</span>
              <Input
                value={saveName}
                placeholder="예: 거실 분할안"
                onChange={(event) => setSaveName(event.target.value)}
                onPressEnter={() => void handleSave()}
              />
              <span className="layout-draw-page__panel-meta">
                도형 {shapes.length}개, 배경 {backgroundMode === 'grid' ? '모눈' : '단색'}
              </span>
              {storageError ? <p className="layout-draw-page__panel-error">{storageError}</p> : null}
              <Button type="primary" block loading={isStorageSaving} onClick={() => void handleSave()}>
                저장
              </Button>
            </div>
          ) : (
            <div className="layout-draw-page__panel-body">
              <div className="layout-draw-page__panel-actions">
                <Button onClick={() => void loadSavedDraws()}>목록 새로고침</Button>
              </div>
              {storageError ? <p className="layout-draw-page__panel-error">{storageError}</p> : null}
              {isStorageLoading ? (
                <div className="layout-draw-page__panel-loading">
                  <Spin size="small" />
                </div>
              ) : savedDraws.length === 0 ? (
                <Empty description="저장된 도면이 없습니다." image={Empty.PRESENTED_IMAGE_SIMPLE} />
              ) : (
                <div className="layout-draw-page__saved-list">
                  {savedDraws.map((record) => (
                    <article key={record.id} className="layout-draw-page__saved-item">
                      <div className="layout-draw-page__saved-copy">
                        <strong>{record.name}</strong>
                        <span>{formatSavedAt(record.updatedAt)}</span>
                        <span>
                          도형 {record.shapes.length}개, 배경 {record.backgroundMode === 'grid' ? '모눈' : '단색'}
                        </span>
                      </div>
                      <div className="layout-draw-page__saved-actions">
                        <Button type="primary" onClick={() => handleLoadSavedDraw(record)}>
                          불러오기
                        </Button>
                        <Button danger onClick={() => void handleDeleteSavedDraw(record)}>
                          삭제
                        </Button>
                      </div>
                    </article>
                  ))}
                </div>
              )}
            </div>
          )}
        </section>
      ) : null}
      <svg
        ref={svgRef}
        className="layout-draw-page__canvas"
        viewBox={`0 0 ${Math.max(canvasSize.width, 1)} ${Math.max(canvasSize.height, 1)}`}
        onPointerDown={handleCanvasPointerDown}
        onPointerMove={handleCanvasPointerMove}
        onPointerUp={handleCanvasPointerUp}
      >
        <defs>
          <pattern id="layout-draw-grid-minor" width="20" height="20" patternUnits="userSpaceOnUse">
            <path d="M 20 0 L 0 0 0 20" fill="none" className="layout-draw-page__grid-minor" />
          </pattern>
          <pattern id="layout-draw-grid-major" width="100" height="100" patternUnits="userSpaceOnUse">
            <rect width="100" height="100" fill="url(#layout-draw-grid-minor)" />
            <path d="M 100 0 L 0 0 0 100" fill="none" className="layout-draw-page__grid-major" />
          </pattern>
        </defs>
        <rect width="100%" height="100%" className="layout-draw-page__paper" />
        {backgroundMode === 'grid' ? <rect width="100%" height="100%" fill="url(#layout-draw-grid-major)" /> : null}
        {hasRegionLayout ? drawRegions.map((region) => {
          const isSelected = selectedRegionKey === region.key;
          const regionFill = region.fillColor ?? (isSelected ? 'rgba(59, 130, 246, 0.12)' : null);

          return (
            <g key={region.key}>
              {region.cells.map((cell) => (
                <rect
                  key={`${region.key}-${cell.x}-${cell.y}`}
                  x={cell.x}
                  y={cell.y}
                  width={cell.width}
                  height={cell.height}
                  data-region-key={region.key}
                  className={`layout-draw-page__region${isSelected ? ' layout-draw-page__region--selected' : ''}`}
                  style={{ fill: regionFill ?? 'transparent' }}
                />
              ))}
              {region.label ? (
                <text x={region.labelPosition.x} y={region.labelPosition.y} className="layout-draw-page__shape-label">
                  {region.label}
                </text>
              ) : null}
            </g>
          );
        }) : null}
        {drawableShapes.map((shape) => {
          if (shape.type === 'line') {
            const labelPosition = resolveLineLabelPosition(shape);
            return (
              <g key={shape.id}>
                <line
                  x1={shape.x1}
                  y1={shape.y1}
                  x2={shape.x2}
                  y2={shape.y2}
                  data-shape-id={shape.id}
                  className={`layout-draw-page__line${selectedShapeIds.has(shape.id) ? ' layout-draw-page__line--selected' : ''}`}
                />
                {shape.label ? (
                  <text x={labelPosition.x} y={labelPosition.y} className="layout-draw-page__shape-label">
                    {shape.label}
                  </text>
                ) : null}
              </g>
            );
          }

          const labelPosition = resolveRectLabelPosition(shape);
          return (
            <g key={shape.id}>
              <rect
                x={shape.x}
                y={shape.y}
                width={shape.width}
                height={shape.height}
                data-shape-id={shape.id}
                style={{ fill: resolveRectFillColor(shape) }}
                className={`layout-draw-page__rect${selectedShapeIds.has(shape.id) ? ' layout-draw-page__rect--selected' : ''}`}
              />
              {shape.label ? (
                <text x={labelPosition.x} y={labelPosition.y} className="layout-draw-page__shape-label">
                  {shape.label}
                </text>
              ) : null}
            </g>
          );
        })}
        {selectedShape
          ? selectedShapeResizeHandles.map((handleItem) => (
              <rect
                key={`${selectedShape.id}-${handleItem.handle}`}
                x={handleItem.x - DRAW_RESIZE_HANDLE_SIZE / 2}
                y={handleItem.y - DRAW_RESIZE_HANDLE_SIZE / 2}
                width={DRAW_RESIZE_HANDLE_SIZE}
                height={DRAW_RESIZE_HANDLE_SIZE}
                rx={2}
                ry={2}
                data-shape-id={selectedShape.id}
                data-resize-handle={handleItem.handle}
                className="layout-draw-page__resize-handle"
                style={{ cursor: handleItem.cursor }}
              />
            ))
          : null}
        {selectionBox ? (
          <rect
            x={Math.min(selectionBox.startX, selectionBox.endX)}
            y={Math.min(selectionBox.startY, selectionBox.endY)}
            width={Math.abs(selectionBox.endX - selectionBox.startX)}
            height={Math.abs(selectionBox.endY - selectionBox.startY)}
            className="layout-draw-page__selection-box"
          />
        ) : null}
        {draft?.type === 'line' ? (
          <line
            x1={draft.x1}
            y1={draft.y1}
            x2={draft.x2}
            y2={draft.y2}
            className="layout-draw-page__line layout-draw-page__line--draft"
          />
        ) : null}
        {draft?.type === 'rect'
          ? (() => {
              return (
                <polygon
                  points={[
                    `${draft.startX},${draft.startY}`,
                    `${draft.endX},${draft.startY}`,
                    `${draft.endX},${draft.endY}`,
                    `${draft.startX},${draft.endY}`,
                  ].join(' ')}
                  className="layout-draw-page__rect layout-draw-page__rect--draft"
                />
              );
            })()
          : null}
      </svg>
    </div>
  );
}
