import {
  ArrowsAltOutlined,
  CaretDownOutlined,
  CaretUpOutlined,
  CodeOutlined,
  CopyOutlined,
  DeleteOutlined,
  EllipsisOutlined,
  EnterOutlined,
  EyeOutlined,
  FileAddOutlined,
  FileExcelOutlined,
  FileImageOutlined,
  FileMarkdownOutlined,
  FilePdfOutlined,
  FileTextOutlined,
  FileUnknownOutlined,
  FileWordOutlined,
  FileZipOutlined,
  FolderAddOutlined,
  FolderOpenOutlined,
  FolderOutlined,
  MinusSquareOutlined,
  PlusSquareOutlined,
  ReloadOutlined,
  SaveOutlined,
  ScissorOutlined,
  SearchOutlined,
  ShrinkOutlined,
  UploadOutlined,
  VideoCameraOutlined,
  ZoomInOutlined,
  ZoomOutOutlined,
} from '@ant-design/icons';
import { Alert, App, Button, Card, Empty, Input, Modal, Slider, Space, Spin, Tabs, Tree, Typography } from 'antd';
import type { DataNode } from 'antd/es/tree';
import {
  useEffect,
  useLayoutEffect,
  useMemo,
  useRef,
  useState,
  type ReactNode,
  type ClipboardEvent as ReactClipboardEvent,
  type KeyboardEvent as ReactKeyboardEvent,
  type TouchEvent as ReactTouchEvent,
  type MouseEvent as ReactMouseEvent,
} from 'react';
import type { Key } from 'react';
import { MarkdownPreviewContent } from '../../components/markdownPreview/MarkdownPreviewContent';
import { CodexDiffBlock } from '../../components/previewer';
import { inferCodeLanguage, renderEditorBlock } from '../../components/previewer/renderers';
import '../../components/previewer/PreviewerUI.css';
import { copyTextToClipboard } from '../../utils/clipboard';
import { renderModalWithEnterConfirm } from './modalKeyboard';
import {
  copyResourceManagerItem,
  createResourceManagerDirectory,
  createResourceManagerFile,
  deleteResourceManagerItem,
  fetchResourceManagerDirectory,
  fetchResourceManagerFile,
  fetchResourceManagerTree,
  moveResourceManagerItem,
  saveResourceManagerFile,
  uploadResourceManagerFile,
  type ResourceManagerDirectoryEntry,
  type ResourceManagerEntryType,
  type ResourceManagerFileDetail,
  type ResourceManagerTreeNode,
  type ResourceManagerTreeRoot,
} from './resourceManagerApi';
import { ChatDataTablePreview, resolveTabularPreviewModel } from './mainChatPanel/ChatDataTablePreview';
import './ResourceManagementPage.css';

const { Text } = Typography;
const { TextArea } = Input;

type ResourceActionMode = 'copy' | 'move';
type MobilePanel = 'tree' | 'list' | 'preview';

type ContextMenuTarget =
  | {
      kind: 'workspace';
      path: string;
    }
  | {
      kind: 'entry';
      entry: ResourceManagerDirectoryEntry;
    };

type ContextMenuState = {
  open: boolean;
  x: number;
  y: number;
  target: ContextMenuTarget | null;
};

type ContextUploadTargetState = {
  directoryPath: string;
} | null;

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

type HtmlPreviewMode = 'browser' | 'source';

type CreateEntryModalState =
  | {
      type: 'file' | 'folder';
      parentPath: string;
    }
  | null;

type ResourceClipboardItem = {
  path: string;
  name: string;
  type: ResourceManagerEntryType;
};

type ResourceListItem = ResourceManagerDirectoryEntry & {
  isRecursiveMatch?: boolean;
  matchContextPath?: string | null;
};

type PreviewTouchGestureState =
  | {
      mode: 'pinch';
      distance: number;
      zoom: number;
    }
  | {
      mode: 'pan';
      touchX: number;
      touchY: number;
      offsetX: number;
      offsetY: number;
    };

type KeyboardRegion = 'tree' | 'list';
type SelectionInteractionMode = 'keyboard' | 'pointer';
type ResourceListSortKey = 'name' | 'modifiedAt' | 'size';
type ResourceListSortDirection = 'asc' | 'desc';

type ResourceListSortState = {
  key: ResourceListSortKey;
  direction: ResourceListSortDirection;
} | null;

function formatSize(size: number | null) {
  if (size === null) {
    return '-';
  }

  if (size < 1024) {
    return `${size} B`;
  }

  if (size < 1024 * 1024) {
    return `${(size / 1024).toFixed(1)} KB`;
  }

  return `${(size / (1024 * 1024)).toFixed(1)} MB`;
}

function formatPathLabel(targetPath: string) {
  return targetPath ? `resource/${targetPath}` : 'resource/';
}

function pathDirname(targetPath: string) {
  const normalized = targetPath.replace(/\\/g, '/').replace(/^\/+/, '');

  if (!normalized.includes('/')) {
    return '';
  }

  return normalized.split('/').slice(0, -1).join('/');
}

function normalizeManagedPath(targetPath: string) {
  return targetPath.replace(/\\/g, '/').replace(/^\/+/, '').replace(/\/+/g, '/').replace(/\/$/, '');
}

function isSameOrDescendantPath(targetPath: string, candidateParentPath: string) {
  const normalizedTarget = normalizeManagedPath(targetPath);
  const normalizedParent = normalizeManagedPath(candidateParentPath);

  if (!normalizedParent) {
    return normalizedTarget === '';
  }

  return normalizedTarget === normalizedParent || normalizedTarget.startsWith(`${normalizedParent}/`);
}

function replacePathPrefix(targetPath: string, sourcePath: string, targetPrefix: string) {
  const normalizedTarget = normalizeManagedPath(targetPath);
  const normalizedSource = normalizeManagedPath(sourcePath);
  const normalizedPrefix = normalizeManagedPath(targetPrefix);

  if (!normalizedSource || !isSameOrDescendantPath(normalizedTarget, normalizedSource)) {
    return normalizedTarget;
  }

  const suffix = normalizedTarget === normalizedSource ? '' : normalizedTarget.slice(normalizedSource.length + 1);
  return [normalizedPrefix, suffix].filter(Boolean).join('/');
}

function isImageFile(file: ResourceManagerFileDetail | null) {
  if (!file) {
    return false;
  }

  return /^image\//.test(file.mimeType);
}

function isPdfFile(file: ResourceManagerFileDetail | null) {
  return file?.mimeType === 'application/pdf';
}

function isVideoFile(file: ResourceManagerFileDetail | null) {
  if (!file) {
    return false;
  }

  return /^video\//.test(file.mimeType);
}

function normalizeFileExtension(file: ResourceManagerFileDetail | null) {
  return file?.extension?.replace(/^\./, '').trim().toLowerCase() ?? '';
}

function normalizeEntryExtension(extension: string | null | undefined, name = '') {
  const normalizedExtension = extension?.replace(/^\./, '').trim().toLowerCase();

  if (normalizedExtension) {
    return normalizedExtension;
  }

  const extensionFromName = name.toLowerCase().match(/\.([a-z0-9]+)$/i)?.[1];
  return extensionFromName ?? '';
}

function isHtmlFile(file: ResourceManagerFileDetail | null) {
  if (!file) {
    return false;
  }

  return file.mimeType.includes('html') || file.extension === '.html' || file.extension === '.htm';
}

function buildHtmlPreviewDocument(file: ResourceManagerFileDetail) {
  const content = file.content ?? '';

  if (!content.trim()) {
    return '<!doctype html><html><body></body></html>';
  }

  const baseHref = file.previewUrl.replace(/"/g, '&quot;');
  const baseTag = `<base href="${baseHref}">`;

  if (/<head[\s>]/i.test(content)) {
    return content.replace(/<head(\s[^>]*)?>/i, (match) => `${match}${baseTag}`);
  }

  if (/<html[\s>]/i.test(content)) {
    return content.replace(/<html(\s[^>]*)?>/i, (match) => `${match}<head>${baseTag}</head>`);
  }

  return `<!doctype html><html><head>${baseTag}</head><body>${content}</body></html>`;
}

function isMarkdownFile(file: ResourceManagerFileDetail | null) {
  const extension = normalizeFileExtension(file);
  return extension === 'md' || extension === 'markdown';
}

function isDiffFile(file: ResourceManagerFileDetail | null) {
  if (!file) {
    return false;
  }

  const extension = normalizeFileExtension(file);
  if (extension === 'diff' || extension === 'patch') {
    return true;
  }

  const content = file.content ?? '';
  return /^(diff --git|@@\s|---\s|\+\+\+\s)/m.test(content);
}

function resolveCodeLanguageForResource(file: ResourceManagerFileDetail) {
  const extension = normalizeFileExtension(file);

  if (extension === 'tsx' || extension === 'ts') {
    return 'typescript';
  }

  if (extension === 'jsx' || extension === 'js' || extension === 'mjs' || extension === 'cjs') {
    return 'javascript';
  }

  if (extension === 'json') {
    return 'json';
  }

  if (extension === 'css') {
    return 'css';
  }

  if (extension === 'scss') {
    return 'scss';
  }

  if (extension === 'html' || extension === 'htm') {
    return 'html';
  }

  if (extension === 'java') {
    return 'java';
  }

  if (extension === 'kt') {
    return 'kotlin';
  }

  if (extension === 'py') {
    return 'python';
  }

  if (extension === 'go') {
    return 'go';
  }

  if (extension === 'rs') {
    return 'rust';
  }

  if (extension === 'sql') {
    return 'sql';
  }

  if (
    extension === 'sh' ||
    extension === 'bash' ||
    extension === 'zsh' ||
    extension === 'env' ||
    extension === 'ini' ||
    extension === 'conf'
  ) {
    return 'bash';
  }

  if (extension === 'yml' || extension === 'yaml') {
    return 'yaml';
  }

  if (extension === 'xml') {
    return 'xml';
  }

  if (extension === 'svg') {
    return 'html';
  }

  return inferCodeLanguage(extension || 'text');
}

function isNativeContextMenuBypassTarget(target: EventTarget | null) {
  if (!(target instanceof HTMLElement)) {
    return false;
  }

  return Boolean(target.closest('textarea, input, [contenteditable="true"], .ant-modal-root'));
}

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

  window.getSelection()?.removeAllRanges();
}

function preventSelection(event: { preventDefault: () => void; target: EventTarget | null }) {
  if (isNativeContextMenuBypassTarget(event.target)) {
    return;
  }

  event.preventDefault();
  clearTextSelection();
}

function escapeAttributeSelector(value: string) {
  if (typeof CSS !== 'undefined' && typeof CSS.escape === 'function') {
    return CSS.escape(value);
  }

  return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
}

function clampPreviewZoom(value: number) {
  return Math.min(3, Math.max(0.5, Number(value.toFixed(2))));
}

function calculateTouchDistance(firstTouch: ReactTouchEvent['touches'][number], secondTouch: ReactTouchEvent['touches'][number]) {
  return Math.hypot(secondTouch.clientX - firstTouch.clientX, secondTouch.clientY - firstTouch.clientY);
}

function clampPreviewOffsetValue(value: number, zoom: number, viewportSize: number) {
  const maxOffset = Math.max(0, ((zoom - 1) * viewportSize) / 2);
  return Number(Math.min(maxOffset, Math.max(-maxOffset, value)).toFixed(2));
}

function resolveClipboardFileExtension(mimeType: string) {
  const normalized = mimeType.toLowerCase();

  if (normalized === 'image/png') return 'png';
  if (normalized === 'image/jpeg') return 'jpg';
  if (normalized === 'image/webp') return 'webp';
  if (normalized === 'image/gif') return 'gif';
  if (normalized === 'image/svg+xml') return 'svg';
  if (normalized === 'text/html') return 'html';
  if (normalized === 'text/plain') return 'txt';
  if (normalized === 'application/json') return 'json';
  if (normalized === 'application/pdf') return 'pdf';

  const subtype = normalized.split('/')[1]?.trim();
  if (!subtype) {
    return 'bin';
  }

  return subtype.replace(/[^a-z0-9]+/g, '') || 'bin';
}

function buildClipboardFileName(mimeType: string, index: number) {
  const now = new Date();
  const timestamp = [
    now.getFullYear(),
    String(now.getMonth() + 1).padStart(2, '0'),
    String(now.getDate()).padStart(2, '0'),
    '-',
    String(now.getHours()).padStart(2, '0'),
    String(now.getMinutes()).padStart(2, '0'),
    String(now.getSeconds()).padStart(2, '0'),
  ].join('');

  return `clipboard-${timestamp}-${index + 1}.${resolveClipboardFileExtension(mimeType)}`;
}

function buildCopiedResourceName(name: string) {
  const extensionIndex = name.lastIndexOf('.');

  if (extensionIndex > 0) {
    const baseName = name.slice(0, extensionIndex);
    const extension = name.slice(extensionIndex);
    return `${baseName}-copy${extension}`;
  }

  return `${name}-copy`;
}

function collectDirectoryTargets(treeRoot: ResourceManagerTreeRoot | null) {
  const directories: Array<{ path: string; label: string }> = [];

  const visit = (node: ResourceManagerTreeNode) => {
    if (node.type === 'directory') {
      directories.push({
        path: node.path,
        label: formatPathLabel(node.path),
      });
      node.children?.forEach(visit);
    }
  };

  if (treeRoot) {
    visit(treeRoot.tree);
  }

  return directories;
}

function collectExpandedKeys(treeRoot: ResourceManagerTreeRoot | null) {
  const keys: string[] = [];

  const visit = (node: ResourceManagerTreeNode) => {
    if (node.type !== 'directory') {
      return;
    }

    keys.push(node.path || '/');
    node.children?.forEach(visit);
  };

  if (treeRoot) {
    visit(treeRoot.tree);
  }

  return keys;
}

function collectCollapsedKeys() {
  return ['/'] as Key[];
}

function findTreeNode(treeRoot: ResourceManagerTreeRoot | null, targetPath: string) {
  if (!treeRoot) {
    return null;
  }

  const normalizedTargetPath = targetPath.trim();

  const visit = (node: ResourceManagerTreeNode): ResourceManagerTreeNode | null => {
    if ((node.path || '') === normalizedTargetPath) {
      return node;
    }

    for (const child of node.children ?? []) {
      const found = visit(child);

      if (found) {
        return found;
      }
    }

    return null;
  };

  return visit(treeRoot.tree);
}

function matchesResourceSearchKeyword(item: Pick<ResourceManagerDirectoryEntry, 'name' | 'path' | 'extension' | 'type'>, keyword: string) {
  const searchableText = [item.name, item.path, item.extension ?? '', item.type].join(' ').toLocaleLowerCase('ko-KR');
  return searchableText.includes(keyword);
}

function collectRecursiveSearchMatches(treeRoot: ResourceManagerTreeRoot | null, targetPath: string, keyword: string) {
  const directoryNode = findTreeNode(treeRoot, targetPath);

  if (!directoryNode || directoryNode.type !== 'directory') {
    return [] as ResourceListItem[];
  }

  const results: ResourceListItem[] = [];

  const visit = (node: ResourceManagerTreeNode) => {
    node.children?.forEach((child) => {
      if (matchesResourceSearchKeyword(child, keyword)) {
        results.push({
          name: child.name,
          path: child.path,
          type: child.type,
          extension: child.extension,
          size: child.size,
          modifiedAt: child.modifiedAt,
          previewUrl: child.previewUrl,
          isRecursiveMatch: pathDirname(child.path) !== targetPath,
          matchContextPath: pathDirname(child.path),
        });
      }

      if (child.type === 'directory') {
        visit(child);
      }
    });
  };

  visit(directoryNode);
  return results;
}

function formatRelativeDirectoryLabel(basePath: string, targetPath: string) {
  const normalizedBasePath = normalizeManagedPath(basePath);
  const normalizedTargetPath = normalizeManagedPath(targetPath);

  if (!normalizedTargetPath) {
    return '현재 폴더';
  }

  if (!normalizedBasePath) {
    return normalizedTargetPath;
  }

  if (normalizedTargetPath === normalizedBasePath) {
    return '현재 폴더';
  }

  if (normalizedTargetPath.startsWith(`${normalizedBasePath}/`)) {
    return normalizedTargetPath.slice(normalizedBasePath.length + 1);
  }

  return normalizedTargetPath;
}

function resolveNextSortState(
  current: ResourceListSortState,
  key: ResourceListSortKey,
): ResourceListSortState {
  const defaultDirection: ResourceListSortDirection = key === 'modifiedAt' ? 'desc' : 'asc';

  if (!current || current.key !== key) {
    return {
      key,
      direction: defaultDirection,
    };
  }

  if (current.direction === defaultDirection) {
    return {
      key,
      direction: defaultDirection === 'asc' ? 'desc' : 'asc',
    };
  }

  return null;
}

function compareNullableNumber(left: number | null, right: number | null, direction: ResourceListSortDirection) {
  if (left === null && right === null) {
    return 0;
  }

  if (left === null) {
    return 1;
  }

  if (right === null) {
    return -1;
  }

  return direction === 'asc' ? left - right : right - left;
}

function flattenVisibleTreeNodes(treeRoot: ResourceManagerTreeRoot | null, expandedKeys: Key[]) {
  if (!treeRoot) {
    return [] as ResourceManagerTreeNode[];
  }

  const expandedKeySet = new Set(expandedKeys.map((key) => String(key)));
  const visibleNodes: ResourceManagerTreeNode[] = [];

  const visit = (node: ResourceManagerTreeNode) => {
    visibleNodes.push(node);

    if (node.type !== 'directory') {
      return;
    }

    const nodeKey = node.path || '/';
    if (!expandedKeySet.has(nodeKey)) {
      return;
    }

    node.children?.forEach(visit);
  };

  visit(treeRoot.tree);
  return visibleNodes;
}

type MobileTreeCompression = {
  visibleNode: ResourceManagerTreeNode;
  collapsedDirectoryCount: number;
};

function resolveMobileTreeCompression(node: ResourceManagerTreeNode): MobileTreeCompression {
  if (node.type !== 'directory') {
    return {
      visibleNode: node,
      collapsedDirectoryCount: 0,
    };
  }

  let visibleNode = node;
  let collapsedDirectoryCount = 0;

  while (true) {
    const children = visibleNode.children ?? [];
    const directoryChildren = children.filter((child) => child.type === 'directory');
    const fileChildrenCount = children.length - directoryChildren.length;

    if (fileChildrenCount > 0 || directoryChildren.length !== 1) {
      break;
    }

    visibleNode = directoryChildren[0];
    collapsedDirectoryCount += 1;
  }

  return {
    visibleNode,
    collapsedDirectoryCount,
  };
}

function resolveResourceEntryIcon(extension: string | null | undefined, type: ResourceManagerEntryType, name = '') {
  if (type === 'directory') {
    return <FolderOutlined />;
  }

  const normalizedExtension = normalizeEntryExtension(extension, name);

  if (['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg', 'bmp', 'ico', 'avif'].includes(normalizedExtension)) {
    return <FileImageOutlined />;
  }

  if (['mp4', 'webm', 'mov', 'm4v', 'ogv'].includes(normalizedExtension)) {
    return <VideoCameraOutlined />;
  }

  if (normalizedExtension === 'pdf') {
    return <FilePdfOutlined />;
  }

  if (normalizedExtension === 'md' || normalizedExtension === 'markdown') {
    return <FileMarkdownOutlined />;
  }

  if (['doc', 'docx'].includes(normalizedExtension)) {
    return <FileWordOutlined />;
  }

  if (['xls', 'xlsx', 'csv'].includes(normalizedExtension)) {
    return <FileExcelOutlined />;
  }

  if (['zip', '7z', 'tar', 'gz', 'tgz', 'rar'].includes(normalizedExtension)) {
    return <FileZipOutlined />;
  }

  if (
    [
      'ts',
      'tsx',
      'js',
      'jsx',
      'mjs',
      'cjs',
      'json',
      'css',
      'scss',
      'html',
      'htm',
      'java',
      'kt',
      'py',
      'go',
      'rs',
      'sql',
      'sh',
      'bash',
      'zsh',
      'env',
      'ini',
      'conf',
      'yml',
      'yaml',
      'xml',
      'diff',
      'patch',
    ].includes(normalizedExtension)
  ) {
    return <CodeOutlined />;
  }

  if (normalizedExtension) {
    return <FileTextOutlined />;
  }

  return <FileUnknownOutlined />;
}

function renderCardTitle(title: string, subtitle?: ReactNode) {
  return (
    <div className="resource-management-page__card-title">
      <span className="resource-management-page__card-title-text">{title}</span>
      {subtitle ? <span className="resource-management-page__card-title-subtitle">{subtitle}</span> : null}
    </div>
  );
}

function renderModifiedAtLabel(value: string) {
  const date = new Date(value);

  if (Number.isNaN(date.getTime())) {
    return value;
  }

  return (
    <span className="resource-management-page__modified-at">
      <span>{date.toLocaleDateString('ko-KR')}</span>
      <span>{date.toLocaleTimeString('ko-KR')}</span>
    </span>
  );
}

export function ResourceManagementPage() {
  const { message } = App.useApp();
  const containerRef = useRef<HTMLDivElement | null>(null);
  const treeRegionRef = useRef<HTMLDivElement | null>(null);
  const listRegionRef = useRef<HTMLDivElement | null>(null);
  const fileInputRef = useRef<HTMLInputElement | null>(null);
  const contextMenuRef = useRef<HTMLDivElement | null>(null);
  const longPressTimerRef = useRef<number | null>(null);
  const lastTapRef = useRef<{ key: string; timestamp: number }>({ key: '', timestamp: 0 });
  const previewTouchGestureRef = useRef<PreviewTouchGestureState | null>(null);
  const previewShellRef = useRef<HTMLDivElement | null>(null);
  const [treeRoot, setTreeRoot] = useState<ResourceManagerTreeRoot | null>(null);
  const [expandedKeys, setExpandedKeys] = useState<Key[]>(() => collectCollapsedKeys());
  const [selectedTreePath, setSelectedTreePath] = useState('');
  const [selectedDirectoryPath, setSelectedDirectoryPath] = useState('');
  const [selectedListPath, setSelectedListPath] = useState('');
  const [directoryItems, setDirectoryItems] = useState<ResourceManagerDirectoryEntry[]>([]);
  const [selectedFile, setSelectedFile] = useState<ResourceManagerFileDetail | null>(null);
  const [editorContent, setEditorContent] = useState('');
  const [activePreviewTab, setActivePreviewTab] = useState('preview');
  const [htmlPreviewMode, setHtmlPreviewMode] = useState<HtmlPreviewMode>('browser');
  const [viewportHeight, setViewportHeight] = useState<number | null>(null);
  const [isMobileViewport, setIsMobileViewport] = useState(false);
  const [isCompactViewport, setIsCompactViewport] = useState(false);
  const [mobilePanel, setMobilePanel] = useState<MobilePanel>('list');
  const [treeLoading, setTreeLoading] = useState(true);
  const [directoryLoading, setDirectoryLoading] = useState(false);
  const [fileLoading, setFileLoading] = useState(false);
  const [saving, setSaving] = useState(false);
  const [errorMessage, setErrorMessage] = useState('');
  const [actionTarget, setActionTarget] = useState<ResourceManagerDirectoryEntry | null>(null);
  const [resourceActionMode, setResourceActionMode] = useState<ResourceActionMode | null>(null);
  const [resourceActionDirectoryPath, setResourceActionDirectoryPath] = useState('');
  const [resourceActionName, setResourceActionName] = useState('');
  const [contextMenu, setContextMenu] = useState<ContextMenuState>({
    open: false,
    x: 0,
    y: 0,
    target: null,
  });
  const [contextUploadTarget, setContextUploadTarget] = useState<ContextUploadTargetState>(null);
  const [createEntryModal, setCreateEntryModal] = useState<CreateEntryModalState>(null);
  const [createEntryName, setCreateEntryName] = useState('');
  const [deleteTarget, setDeleteTarget] = useState<ResourceManagerDirectoryEntry | null>(null);
  const [isPreviewMaximized, setIsPreviewMaximized] = useState(false);
  const [previewZoom, setPreviewZoom] = useState(1);
  const [previewOffset, setPreviewOffset] = useState<PreviewOffset>({ x: 0, y: 0 });
  const [copiedItem, setCopiedItem] = useState<ResourceClipboardItem | null>(null);
  const [activeKeyboardRegion, setActiveKeyboardRegion] = useState<KeyboardRegion>('list');
  const [selectionInteractionMode, setSelectionInteractionMode] = useState<SelectionInteractionMode>('pointer');
  const [searchInputValue, setSearchInputValue] = useState('');
  const [searchKeyword, setSearchKeyword] = useState('');
  const [listSortState, setListSortState] = useState<ResourceListSortState>(null);

  const visibleMobilePanels = useMemo<MobilePanel[]>(() => ['tree', 'list', 'preview'], []);
  const isKeyboardTreeSelectionVisible = selectionInteractionMode === 'keyboard' && activeKeyboardRegion === 'tree';
  const isKeyboardListSelectionVisible = selectionInteractionMode === 'keyboard' && activeKeyboardRegion === 'list';

  const setPreviewZoomPercent = (value: number) => {
    setPreviewZoom(clampPreviewZoom(value / 100));
  };

  const clampPreviewOffset = (offset: PreviewOffset, zoom = previewZoom) => {
    const shell = previewShellRef.current;

    if (!shell) {
      return { x: 0, y: 0 };
    }

    return {
      x: clampPreviewOffsetValue(offset.x, zoom, shell.clientWidth),
      y: clampPreviewOffsetValue(offset.y, zoom, shell.clientHeight),
    };
  };

  const handlePreviewTouchStart = (event: ReactTouchEvent<HTMLDivElement>) => {
    if (event.touches.length >= 2) {
      previewTouchGestureRef.current = {
        mode: 'pinch',
        distance: calculateTouchDistance(event.touches[0], event.touches[1]),
        zoom: previewZoom,
      };
      return;
    }

    const touch = event.touches[0];

    if (!touch || previewZoom <= 1) {
      previewTouchGestureRef.current = null;
      return;
    }

    previewTouchGestureRef.current = {
      mode: 'pan',
      touchX: touch.clientX,
      touchY: touch.clientY,
      offsetX: previewOffset.x,
      offsetY: previewOffset.y,
    };
  };

  const handlePreviewTouchMove = (event: ReactTouchEvent<HTMLDivElement>) => {
    const gestureState = previewTouchGestureRef.current;

    if (!gestureState) {
      return;
    }

    if (gestureState.mode === 'pinch') {
      if (event.touches.length < 2) {
        return;
      }

      const nextDistance = calculateTouchDistance(event.touches[0], event.touches[1]);

      if (!Number.isFinite(nextDistance) || nextDistance <= 0 || gestureState.distance <= 0) {
        return;
      }

      event.preventDefault();
      setPreviewZoom(clampPreviewZoom(gestureState.zoom * (nextDistance / gestureState.distance)));
      return;
    }

    const touch = event.touches[0];

    if (!touch || previewZoom <= 1) {
      return;
    }

    event.preventDefault();
    setPreviewOffset(
      clampPreviewOffset({
        x: gestureState.offsetX + (touch.clientX - gestureState.touchX),
        y: gestureState.offsetY + (touch.clientY - gestureState.touchY),
      }),
    );
  };

  const handlePreviewTouchEnd = (event: ReactTouchEvent<HTMLDivElement>) => {
    if (event.touches.length >= 2) {
      previewTouchGestureRef.current = {
        mode: 'pinch',
        distance: calculateTouchDistance(event.touches[0], event.touches[1]),
        zoom: previewZoom,
      };
      return;
    }

    const touch = event.touches[0];

    if (touch && previewZoom > 1) {
      previewTouchGestureRef.current = {
        mode: 'pan',
        touchX: touch.clientX,
        touchY: touch.clientY,
        offsetX: previewOffset.x,
        offsetY: previewOffset.y,
      };
      return;
    }

    previewTouchGestureRef.current = null;
  };

  const directoryTargets = useMemo(() => collectDirectoryTargets(treeRoot), [treeRoot]);
  const visibleTreeNodes = useMemo(() => flattenVisibleTreeNodes(treeRoot, expandedKeys), [expandedKeys, treeRoot]);
  const selectedTreeNode = useMemo(() => findTreeNode(treeRoot, selectedTreePath), [selectedTreePath, treeRoot]);
  const currentDirectoryLabel = formatPathLabel(selectedDirectoryPath);
  const treeCardPathLabel = formatPathLabel(selectedTreePath || selectedDirectoryPath);
  const listCardPathLabel = formatPathLabel(selectedListPath || selectedDirectoryPath);
  const previewCardPathLabel = selectedFile ? formatPathLabel(selectedFile.path) : '선택한 파일 없음';

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

    const container = containerRef.current;
    const parent = container?.parentElement;
    const layoutWidth = parent?.clientWidth ?? container?.clientWidth ?? window.innerWidth;
    const nextIsMobileViewport = window.innerWidth <= 768;
    const nextIsCompactViewport = !nextIsMobileViewport && layoutWidth <= 1180;
    const viewportHeightSource = window.visualViewport?.height ?? window.innerHeight;
    const containerRect = container?.getBoundingClientRect();
    const parentRect = parent?.getBoundingClientRect();
    const viewportBasedHeight = Math.floor(viewportHeightSource - (containerRect?.top ?? 0) - 16);
    const parentBasedHeight = containerRect && parentRect ? Math.floor(parentRect.bottom - containerRect.top) : Number.NaN;
    const nextHeight = Number.isFinite(parentBasedHeight)
      ? Math.min(viewportBasedHeight, parentBasedHeight)
      : viewportBasedHeight;

    setViewportHeight(Math.max(nextIsMobileViewport ? 0 : 520, nextHeight));
    setIsMobileViewport(nextIsMobileViewport);
    setIsCompactViewport(nextIsCompactViewport);
  };

  useEffect(() => {
    updateViewportHeight();
    window.addEventListener('resize', updateViewportHeight);

    return () => {
      window.removeEventListener('resize', updateViewportHeight);
    };
  }, []);

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

    const { body, documentElement } = document;
    const lockedScrollY = window.scrollY;
    const previousBodyOverflow = body.style.overflow;
    const previousBodyPosition = body.style.position;
    const previousBodyTop = body.style.top;
    const previousBodyLeft = body.style.left;
    const previousBodyRight = body.style.right;
    const previousBodyWidth = body.style.width;
    const previousBodyOverscrollBehavior = body.style.overscrollBehavior;
    const previousBodyTouchAction = body.style.touchAction;
    const previousHtmlOverflow = documentElement.style.overflow;
    const previousHtmlOverscrollBehavior = documentElement.style.overscrollBehavior;
    const previousHtmlTouchAction = documentElement.style.touchAction;

    if (isPreviewMaximized) {
      body.style.overflow = 'hidden';
      body.style.position = 'fixed';
      body.style.top = `-${lockedScrollY}px`;
      body.style.left = '0';
      body.style.right = '0';
      body.style.width = '100%';
      body.style.overscrollBehavior = 'none';
      body.style.touchAction = 'none';
      documentElement.style.overflow = 'hidden';
      documentElement.style.overscrollBehavior = 'none';
      documentElement.style.touchAction = 'none';
    }

    return () => {
      body.style.overflow = previousBodyOverflow;
      body.style.position = previousBodyPosition;
      body.style.top = previousBodyTop;
      body.style.left = previousBodyLeft;
      body.style.right = previousBodyRight;
      body.style.width = previousBodyWidth;
      body.style.overscrollBehavior = previousBodyOverscrollBehavior;
      body.style.touchAction = previousBodyTouchAction;
      documentElement.style.overflow = previousHtmlOverflow;
      documentElement.style.overscrollBehavior = previousHtmlOverscrollBehavior;
      documentElement.style.touchAction = previousHtmlTouchAction;
      if (isPreviewMaximized) {
        window.scrollTo(0, lockedScrollY);
      }
    };
  }, [isPreviewMaximized]);

  useEffect(() => {
    setPreviewOffset((current) => {
      if (previewZoom <= 1) {
        return current.x === 0 && current.y === 0 ? current : { x: 0, y: 0 };
      }

      const nextOffset = clampPreviewOffset(current, previewZoom);
      return nextOffset.x === current.x && nextOffset.y === current.y ? current : nextOffset;
    });
  }, [previewZoom]);

  useEffect(() => {
    setPreviewZoom(1);
    setPreviewOffset({ x: 0, y: 0 });
    previewTouchGestureRef.current = null;
    setHtmlPreviewMode('browser');
  }, [selectedFile?.path, isPreviewMaximized]);

  const cancelLongPress = () => {
    if (longPressTimerRef.current !== null) {
      window.clearTimeout(longPressTimerRef.current);
      longPressTimerRef.current = null;
    }
  };

  const scheduleLongPress = (event: ReactTouchEvent<HTMLElement>, target: ContextMenuTarget) => {
    cancelLongPress();
    const touch = event.touches[0];

    if (!touch) {
      return;
    }

    longPressTimerRef.current = window.setTimeout(() => {
      clearTextSelection();
      setContextMenu({
        open: true,
        x: touch.clientX,
        y: touch.clientY,
        target,
      });
      longPressTimerRef.current = null;
    }, 420);
  };

  const openContextMenu = (event: ReactMouseEvent<HTMLElement>, target: ContextMenuTarget) => {
    event.preventDefault();
    event.stopPropagation();
    clearTextSelection();
    setContextMenu({
      open: true,
      x: event.clientX,
      y: event.clientY,
      target,
    });
  };

  useLayoutEffect(() => {
    if (!contextMenu.open || typeof window === 'undefined') {
      return;
    }

    const menu = contextMenuRef.current;

    if (!menu) {
      return;
    }

    const rect = menu.getBoundingClientRect();
    const maxLeft = Math.max(12, window.innerWidth - rect.width - 12);
    const maxTop = Math.max(12, window.innerHeight - rect.height - 12);
    const nextLeft = Math.max(12, Math.min(contextMenu.x, maxLeft));
    const nextTop = Math.max(12, Math.min(contextMenu.y, maxTop));

    if (nextLeft !== contextMenu.x || nextTop !== contextMenu.y) {
      setContextMenu((current) => {
        if (!current.open || (current.x === nextLeft && current.y === nextTop)) {
          return current;
        }

        return {
          ...current,
          x: nextLeft,
          y: nextTop,
        };
      });
    }
  }, [contextMenu.open, contextMenu.x, contextMenu.y, contextMenu.target]);

  const refreshTree = async (preserveExpandedKeys = true) => {
    setTreeLoading(true);
    setErrorMessage('');

    try {
      const nextTree = await fetchResourceManagerTree();
      setTreeRoot(nextTree);
      setExpandedKeys((current) => (preserveExpandedKeys && current.length > 0 ? current : collectCollapsedKeys()));
    } catch (error) {
      setErrorMessage(error instanceof Error ? error.message : '리소스 트리를 불러오지 못했습니다.');
    } finally {
      setTreeLoading(false);
    }
  };

  const refreshDirectory = async (targetPath: string, clearSelection = true) => {
    setDirectoryLoading(true);
    setErrorMessage('');

    try {
      const detail = await fetchResourceManagerDirectory(targetPath);
      setSelectedDirectoryPath(detail.path);
      setSelectedTreePath(detail.path);
      setDirectoryItems(detail.items);
      if (isMobileViewport) {
        setMobilePanel('list');
      }
      if (clearSelection) {
        setSelectedListPath('');
        setSelectedFile(null);
        setEditorContent('');
      }
    } catch (error) {
      setErrorMessage(error instanceof Error ? error.message : '디렉터리 목록을 불러오지 못했습니다.');
    } finally {
      setDirectoryLoading(false);
    }
  };

  const refreshAll = async (targetDirectoryPath = selectedDirectoryPath, clearSelection = false) => {
    await refreshTree();
    await refreshDirectory(targetDirectoryPath, clearSelection);
  };

  const openFile = async (entry: ResourceManagerDirectoryEntry) => {
    setSelectedListPath(entry.path);
    setSelectedTreePath(pathDirname(entry.path));
    setFileLoading(true);
    setErrorMessage('');
    setActivePreviewTab('preview');

    try {
      const file = await fetchResourceManagerFile(entry.path);
      setSelectedFile(file);
      setEditorContent(file.content ?? '');
      if (isMobileViewport) {
        setMobilePanel('preview');
      }
    } catch (error) {
      setErrorMessage(error instanceof Error ? error.message : '파일을 불러오지 못했습니다.');
    } finally {
      setFileLoading(false);
    }
  };

  const openFileByPath = async (targetPath: string) => {
    setSelectedListPath(targetPath);
    setSelectedTreePath(pathDirname(targetPath));
    setFileLoading(true);
    setErrorMessage('');
    setActivePreviewTab('preview');

    try {
      const file = await fetchResourceManagerFile(targetPath);
      setSelectedFile(file);
      setEditorContent(file.content ?? '');
      if (isMobileViewport) {
        setMobilePanel('preview');
      }
    } catch (error) {
      setSelectedFile(null);
      setEditorContent('');
      setErrorMessage(error instanceof Error ? error.message : '파일을 불러오지 못했습니다.');
    } finally {
      setFileLoading(false);
    }
  };

  const focusRegion = (region: KeyboardRegion) => {
    const target = region === 'tree' ? treeRegionRef.current : listRegionRef.current;

    if (!target) {
      return;
    }

    try {
      target.focus({ preventScroll: true });
    } catch {
      target.focus();
    }
  };

  const copyEntryToClipboardBuffer = (entry: Pick<ResourceManagerDirectoryEntry, 'path' | 'name' | 'type'>) => {
    setCopiedItem({
      path: entry.path,
      name: entry.name,
      type: entry.type,
    });
    message.success(`${entry.type === 'directory' ? '폴더' : '파일'}을 복사 대상으로 저장했습니다.`);
  };

  const copyManagedPath = async (targetPath: string) => {
    const pathLabel = formatPathLabel(targetPath);

    try {
      await copyTextToClipboard(pathLabel);
      message.success('경로를 복사했습니다.');
    } catch (error) {
      setErrorMessage('');
      message.warning(error instanceof Error ? error.message : '브라우저가 자동 복사를 차단했습니다.');
    }
  };

  const uploadClipboardFilesToDirectory = async (targetDirectoryPath: string, files: File[]) => {
    if (files.length === 0) {
      message.warning('클립보드에 붙여넣을 파일이 없습니다.');
      return;
    }

    setSaving(true);
    setErrorMessage('');

    try {
      for (const file of files) {
        await uploadResourceManagerFile(targetDirectoryPath, file);
      }

      await refreshAll(targetDirectoryPath);
      message.success(`파일 ${files.length}건을 붙여넣었습니다.`);
    } catch (error) {
      setErrorMessage(error instanceof Error ? error.message : '클립보드 붙여넣기에 실패했습니다.');
    } finally {
      setSaving(false);
    }
  };

  const handlePasteFromClipboardEvent = async (
    event: ReactClipboardEvent<HTMLElement>,
    target: ContextMenuTarget | null,
  ) => {
    if (copiedItem) {
      event.preventDefault();
      await handlePasteIntoDirectory(target);
      return;
    }

    const fileList = Array.from(event.clipboardData.files ?? []);

    if (fileList.length === 0) {
      return;
    }

    event.preventDefault();
    await uploadClipboardFilesToDirectory(resolvePasteDirectoryPath(target), fileList);
  };

  const openEntry = async (entry: ResourceManagerDirectoryEntry | ResourceManagerTreeNode) => {
    if (entry.type === 'directory') {
      await refreshDirectory(entry.path);
      return;
    }

    await openFile({
      name: entry.name,
      path: entry.path,
      type: 'file',
      extension: entry.extension,
      size: entry.size,
      modifiedAt: entry.modifiedAt,
      previewUrl: entry.previewUrl,
    });
  };

  useEffect(() => {
    void (async () => {
      await refreshTree(false);
      await refreshDirectory('');
    })();
  }, []);

  useEffect(() => {
    if (isMobileViewport && mobilePanel === 'preview' && !selectedFile) {
      setMobilePanel('list');
    }
  }, [isMobileViewport, mobilePanel, selectedFile]);

  useEffect(() => {
    if (!isMobileViewport) {
      return;
    }

    if (!visibleMobilePanels.includes(mobilePanel)) {
      setMobilePanel(visibleMobilePanels[0]);
    }
  }, [isMobileViewport, mobilePanel, visibleMobilePanels]);

  useEffect(() => {
    setPreviewZoom(1);

    if (activePreviewTab !== 'preview') {
      setIsPreviewMaximized(false);
    }
  }, [activePreviewTab, selectedFile?.path]);

  const handlePotentialDoubleTap = (key: string, action: () => void) => {
    const now = Date.now();
    const isSameTarget = lastTapRef.current.key === key;
    const isDoubleTap = isSameTarget && now - lastTapRef.current.timestamp < 320;

    lastTapRef.current = {
      key,
      timestamp: now,
    };

    if (isDoubleTap) {
      action();
      lastTapRef.current = { key: '', timestamp: 0 };
    }
  };

  const handleTreeSelect = (selectedKeys: Key[]) => {
    const rawKey = String(selectedKeys[0] ?? '');
    const targetPath = rawKey === '/' ? '' : rawKey;
    setActiveKeyboardRegion('tree');
    setSelectedTreePath(targetPath);

    const selectedNode = findTreeNode(treeRoot, targetPath);

    if (!selectedNode) {
      return;
    }

    handlePotentialDoubleTap(rawKey, () => {
      void openEntry(selectedNode);
    });
  };

  useEffect(() => {
    const treePathKey = selectedTreePath || '/';
    const treeNodeElement = containerRef.current?.querySelector<HTMLElement>(
      `[data-resource-tree-path="${escapeAttributeSelector(treePathKey)}"]`,
    );
    treeNodeElement?.scrollIntoView({ block: 'nearest' });
  }, [selectedTreePath, expandedKeys, treeRoot]);

  useEffect(() => {
    const listPathKey = selectedListPath || (selectedDirectoryPath && !selectedFile ? selectedDirectoryPath : '');

    if (!listPathKey) {
      return;
    }

    const listRowElement = containerRef.current?.querySelector<HTMLElement>(
      `[data-resource-list-path="${escapeAttributeSelector(listPathKey)}"]`,
    );
    listRowElement?.scrollIntoView({ block: 'nearest' });
  }, [selectedDirectoryPath, selectedFile, selectedListPath]);

  const openCreateFolderModal = (parentPath = selectedDirectoryPath) => {
    setCreateEntryModal({ type: 'folder', parentPath });
    setCreateEntryName('');
  };

  const openCreateFileModal = (parentPath = selectedDirectoryPath) => {
    setCreateEntryModal({ type: 'file', parentPath });
    setCreateEntryName('');
  };

  const handleCreateEntry = async () => {
    if (!createEntryModal) {
      return;
    }

    setSaving(true);
    setErrorMessage('');

    try {
      if (createEntryModal.type === 'folder') {
        await createResourceManagerDirectory(createEntryModal.parentPath, createEntryName);
        message.success('폴더를 생성했습니다.');
      } else {
        await createResourceManagerFile(createEntryModal.parentPath, createEntryName, '');
        message.success('파일을 생성했습니다.');
      }

      await refreshAll(createEntryModal.parentPath);
      setCreateEntryModal(null);
      setCreateEntryName('');
    } catch (error) {
      setErrorMessage(error instanceof Error ? error.message : '항목 생성에 실패했습니다.');
    } finally {
      setSaving(false);
    }
  };

  const handleUploadFiles = async (files: FileList | null, targetDirectoryPath = selectedDirectoryPath) => {
    if (!files || files.length === 0) {
      return;
    }

    setSaving(true);
    setErrorMessage('');

    try {
      for (const file of Array.from(files)) {
        await uploadResourceManagerFile(targetDirectoryPath, file);
      }

      await refreshAll(targetDirectoryPath);
      message.success(`파일 ${files.length}건을 업로드했습니다.`);
    } catch (error) {
      setErrorMessage(error instanceof Error ? error.message : '파일 업로드에 실패했습니다.');
    } finally {
      setSaving(false);
      setContextUploadTarget(null);
      if (fileInputRef.current) {
        fileInputRef.current.value = '';
      }
    }
  };

  const resolveUploadDirectoryPath = (target: ContextMenuTarget | null) => {
    if (!target) {
      return selectedDirectoryPath;
    }

    if (target.kind === 'workspace') {
      return target.path;
    }

    return target.entry.type === 'directory' ? target.entry.path : selectedDirectoryPath;
  };

  const openUploadDialog = (targetDirectoryPath = selectedDirectoryPath) => {
    const input = fileInputRef.current;
    setContextUploadTarget({
      directoryPath: targetDirectoryPath,
    });

    if (!input) {
      return;
    }

    input.value = '';
    if (typeof input.showPicker === 'function') {
      input.showPicker();
      return;
    }

    input.click();
  };

  const handleSaveFile = async () => {
    if (!selectedFile?.isTextEditable) {
      return;
    }

    setSaving(true);
    setErrorMessage('');

    try {
      await saveResourceManagerFile(selectedFile.path, editorContent);
      const refreshedFile = await fetchResourceManagerFile(selectedFile.path);
      await refreshAll(pathDirname(selectedFile.path), false);
      setSelectedFile(refreshedFile);
      setEditorContent(refreshedFile.content ?? '');
      message.success('파일을 저장했습니다.');
    } catch (error) {
      setErrorMessage(error instanceof Error ? error.message : '파일 저장에 실패했습니다.');
    } finally {
      setSaving(false);
    }
  };

  const openCopyMoveModal = (mode: ResourceActionMode, entry: ResourceManagerDirectoryEntry) => {
    setResourceActionMode(mode);
    setActionTarget(entry);
    setResourceActionDirectoryPath(selectedDirectoryPath);
    setResourceActionName(entry.name);
  };

  const openRenameModal = (entry: ResourceManagerDirectoryEntry) => {
    setResourceActionMode('move');
    setActionTarget(entry);
    setResourceActionDirectoryPath(pathDirname(entry.path));
    setResourceActionName(entry.name);
  };

  const handleCopyMove = async () => {
    if (!actionTarget || !resourceActionMode) {
      return;
    }

    setSaving(true);
    setErrorMessage('');

    try {
      const selectedFilePath = selectedFile?.path ?? '';
      const requestedName = resourceActionName.trim() || actionTarget.name;
      const movedTargetPath =
        resourceActionMode === 'move'
          ? [resourceActionDirectoryPath, requestedName].filter(Boolean).join('/')
          : null;
      const shouldReopenSelectedFile =
        resourceActionMode === 'move' &&
        Boolean(selectedFilePath) &&
        movedTargetPath !== null &&
        isSameOrDescendantPath(selectedFilePath, actionTarget.path);
      const nextSelectedFilePath =
        shouldReopenSelectedFile && movedTargetPath !== null
          ? replacePathPrefix(selectedFilePath, actionTarget.path, movedTargetPath)
          : null;

      if (resourceActionMode === 'copy') {
        await copyResourceManagerItem(actionTarget.path, resourceActionDirectoryPath, resourceActionName);
      } else {
        await moveResourceManagerItem(actionTarget.path, resourceActionDirectoryPath, resourceActionName);
      }

      const nextDirectory =
        resourceActionMode === 'move' && movedTargetPath !== null
          ? replacePathPrefix(selectedDirectoryPath, actionTarget.path, movedTargetPath)
          : selectedDirectoryPath;
      const isRename = resourceActionMode === 'move' && resourceActionDirectoryPath === pathDirname(actionTarget.path);
      setResourceActionMode(null);
      setActionTarget(null);
      await refreshAll(nextDirectory, Boolean(nextSelectedFilePath));
      if (nextSelectedFilePath) {
        await openFileByPath(nextSelectedFilePath);
      }
      message.success(resourceActionMode === 'copy' ? '복사를 완료했습니다.' : isRename ? '이름을 변경했습니다.' : '이동을 완료했습니다.');
    } catch (error) {
      setErrorMessage(error instanceof Error ? error.message : '작업을 완료하지 못했습니다.');
    } finally {
      setSaving(false);
    }
  };

  const handleDeleteItem = (entry: ResourceManagerDirectoryEntry) => {
    setDeleteTarget(entry);
  };

  const requestDeleteForKeyboardTarget = (
    entry: Pick<ResourceManagerDirectoryEntry, 'name' | 'path' | 'type' | 'extension' | 'size' | 'modifiedAt' | 'previewUrl'>,
  ) => {
    handleDeleteItem({
      name: entry.name,
      path: entry.path,
      type: entry.type,
      extension: entry.extension ?? null,
      size: entry.size ?? null,
      modifiedAt: entry.modifiedAt,
      previewUrl: entry.previewUrl ?? null,
    });
  };

  const confirmDeleteItem = async () => {
    if (!deleteTarget) {
      return;
    }

    setSaving(true);
    setErrorMessage('');

    try {
      const deletingCurrentDirectory =
        deleteTarget.type === 'directory' && isSameOrDescendantPath(selectedDirectoryPath, deleteTarget.path);
      const deletingSelectedFile =
        Boolean(selectedFile?.path) &&
        (selectedFile?.path === deleteTarget.path ||
          (deleteTarget.type === 'directory' && isSameOrDescendantPath(selectedFile?.path ?? '', deleteTarget.path)));
      const nextDirectoryPath = deletingCurrentDirectory ? pathDirname(deleteTarget.path) : selectedDirectoryPath;

      await deleteResourceManagerItem(deleteTarget.path);
      await refreshAll(nextDirectoryPath, deletingSelectedFile);
      if (deletingSelectedFile) {
        setSelectedFile(null);
        setEditorContent('');
      }
      if (
        selectedListPath === deleteTarget.path ||
        (deleteTarget.type === 'directory' && isSameOrDescendantPath(selectedListPath, deleteTarget.path))
      ) {
        setSelectedListPath('');
      }
      setDeleteTarget(null);
      message.success('항목을 삭제했습니다.');
    } catch (error) {
      setErrorMessage(error instanceof Error ? error.message : '항목 삭제에 실패했습니다.');
    } finally {
      setSaving(false);
    }
  };

  const resolvePasteDirectoryPath = (target: ContextMenuTarget | null) => {
    if (!target) {
      return selectedDirectoryPath;
    }

    if (target.kind === 'workspace') {
      return target.path;
    }

    if (target.entry.type === 'directory') {
      return target.entry.path;
    }

    return selectedDirectoryPath;
  };

  const handlePasteClipboardFiles = async (target: ContextMenuTarget | null) => {
    if (typeof navigator === 'undefined' || !navigator.clipboard?.read || typeof ClipboardItem === 'undefined') {
      message.warning('이 브라우저에서는 클립보드 파일 붙여넣기를 지원하지 않습니다.');
      return;
    }

    const targetDirectoryPath = resolvePasteDirectoryPath(target);

    try {
      const clipboardItems = await navigator.clipboard.read();
      const files: File[] = [];

      for (const clipboardItem of clipboardItems) {
        for (const type of clipboardItem.types) {
          if (/^text\//i.test(type)) {
            continue;
          }

          const blob = await clipboardItem.getType(type);
          if (blob.size === 0) {
            continue;
          }

          const mimeType = blob.type || type || 'application/octet-stream';
          if (/^text\//i.test(mimeType)) {
            continue;
          }

          const fileName = buildClipboardFileName(mimeType, files.length);
          files.push(new File([blob], fileName, { type: mimeType }));
        }
      }

      if (files.length === 0) {
        message.warning('클립보드에 붙여넣을 파일이 없습니다.');
        return;
      }

      await uploadClipboardFilesToDirectory(targetDirectoryPath, files);
    } catch (error) {
      setErrorMessage(error instanceof Error ? error.message : '클립보드 붙여넣기에 실패했습니다.');
    }
  };

  const handlePasteIntoDirectory = async (target: ContextMenuTarget | null) => {
    const targetDirectoryPath = resolvePasteDirectoryPath(target);

    if (!copiedItem) {
      await handlePasteClipboardFiles(target);
      return;
    }

    setSaving(true);
    setErrorMessage('');

    try {
      const nextName =
        pathDirname(copiedItem.path) === targetDirectoryPath ? buildCopiedResourceName(copiedItem.name) : copiedItem.name;
      await copyResourceManagerItem(copiedItem.path, targetDirectoryPath, nextName);
      await refreshAll(targetDirectoryPath);
      message.success(`${copiedItem.type === 'directory' ? '폴더' : '파일'}을 복사했습니다.`);
    } catch (error) {
      setErrorMessage(error instanceof Error ? error.message : '붙여넣기를 완료하지 못했습니다.');
    } finally {
      setSaving(false);
    }
  };

  const handleContextMenuAction = async (
    action: 'open' | 'copy' | 'copy-path' | 'move' | 'rename' | 'delete' | 'new-folder' | 'new-file' | 'paste',
  ) => {
    const target = contextMenu.target;
    setContextMenu((current) => ({ ...current, open: false, target: null }));

    if (!target) {
      return;
    }

    if (action === 'new-folder') {
      openCreateFolderModal(target.kind === 'workspace' ? target.path : target.entry.type === 'directory' ? target.entry.path : selectedDirectoryPath);
      return;
    }

    if (action === 'new-file') {
      openCreateFileModal(target.kind === 'workspace' ? target.path : target.entry.type === 'directory' ? target.entry.path : selectedDirectoryPath);
      return;
    }

    if (action === 'paste') {
      await handlePasteIntoDirectory(target);
      return;
    }

    if (target.kind !== 'entry') {
      return;
    }

    if (action === 'open') {
      await openEntry(target.entry);
      return;
    }

    if (action === 'copy') {
      copyEntryToClipboardBuffer(target.entry);
      return;
    }

    if (action === 'copy-path') {
      await copyManagedPath(target.entry.path);
      return;
    }

    if (action === 'move') {
      openCopyMoveModal('move', target.entry);
      return;
    }

    if (action === 'rename') {
      openRenameModal(target.entry);
      return;
    }

    if (action === 'delete') {
      handleDeleteItem(target.entry);
    }
  };

  const canZoomPreview =
    activePreviewTab === 'preview' &&
    (isImageFile(selectedFile) || (isHtmlFile(selectedFile) && htmlPreviewMode === 'browser'));

  const renderPreviewContent = (maximized = false) => {
    if (!selectedFile) {
      return null;
    }

    const fileContent = selectedFile.content ?? '';

    if (isImageFile(selectedFile)) {
      if (!maximized) {
        return <img alt={selectedFile.name} src={selectedFile.previewUrl} className="resource-management-page__image-preview" />;
      }

      return (
        <div
          ref={previewShellRef}
          className="resource-management-page__zoom-shell resource-management-page__zoom-shell--image resource-management-page__zoom-shell--touch-zoom"
          onTouchStart={handlePreviewTouchStart}
          onTouchMove={handlePreviewTouchMove}
          onTouchEnd={handlePreviewTouchEnd}
          onTouchCancel={handlePreviewTouchEnd}
        >
          <img
            alt={selectedFile.name}
            src={selectedFile.previewUrl}
            className="resource-management-page__image-preview resource-management-page__image-preview--zoomable"
            style={{ transform: `translate(${previewOffset.x}px, ${previewOffset.y}px) scale(${previewZoom})` }}
          />
        </div>
      );
    }

    if (isPdfFile(selectedFile)) {
      return <iframe title={selectedFile.name} src={selectedFile.previewUrl} className="resource-management-page__preview-frame" />;
    }

    if (isVideoFile(selectedFile)) {
      return (
        <video
          key={selectedFile.previewUrl}
          controls
          preload="metadata"
          src={selectedFile.previewUrl}
          className="resource-management-page__video-preview"
        />
      );
    }

    if (isMarkdownFile(selectedFile)) {
      return (
        <div className="resource-management-page__rich-preview resource-management-page__rich-preview--markdown">
          <div className="resource-management-page__markdown-scroll-viewport">
            <MarkdownPreviewContent content={fileContent || '# Preview\n\n표시할 markdown 본문이 없습니다.'} />
          </div>
        </div>
      );
    }

    if (isHtmlFile(selectedFile)) {
      const previewDocument = buildHtmlPreviewDocument(selectedFile);
      const browserPreview = maximized ? (
        <div
          ref={previewShellRef}
          className="resource-management-page__zoom-shell resource-management-page__zoom-shell--frame resource-management-page__zoom-shell--touch-zoom"
          onTouchStart={handlePreviewTouchStart}
          onTouchMove={handlePreviewTouchMove}
          onTouchEnd={handlePreviewTouchEnd}
          onTouchCancel={handlePreviewTouchEnd}
        >
          <div
            className="resource-management-page__frame-zoom-shell"
            style={{ transform: `translate(${previewOffset.x}px, ${previewOffset.y}px) scale(${previewZoom})` }}
          >
            <iframe
              title={selectedFile.name}
              srcDoc={previewDocument}
              className="resource-management-page__preview-frame resource-management-page__preview-frame--zoomable"
              style={{ width: '100%', height: '100%' }}
            />
          </div>
        </div>
      ) : (
        <iframe title={selectedFile.name} srcDoc={previewDocument} className="resource-management-page__preview-frame" />
      );

      const sourcePreview = (
        <div className="resource-management-page__rich-preview resource-management-page__rich-preview--code">
          {renderEditorBlock(fileContent || '표시할 preview 본문이 없습니다.', resolveCodeLanguageForResource(selectedFile), 'code')}
        </div>
      );

      return <div className="resource-management-page__html-preview">{htmlPreviewMode === 'browser' ? browserPreview : sourcePreview}</div>;
    }

    if (selectedFile.isTextEditable) {
      const tabularModel = resolveTabularPreviewModel(
        {
          label: selectedFile.name,
          url: selectedFile.previewUrl,
          kind: 'document',
        },
        fileContent,
      );

      if (tabularModel) {
        return (
          <div className="resource-management-page__rich-preview resource-management-page__rich-preview--table">
            <ChatDataTablePreview model={tabularModel} />
          </div>
        );
      }
    }

    if (isDiffFile(selectedFile)) {
      return (
        <div className="resource-management-page__rich-preview resource-management-page__rich-preview--code">
          <CodexDiffBlock
            diffText={fileContent}
            summary={`${selectedFile.name} 기준 raw diff preview입니다.`}
            showToolbar={false}
          />
        </div>
      );
    }

    if (selectedFile.isTextEditable) {
      return (
        <div className="resource-management-page__rich-preview resource-management-page__rich-preview--code">
          {renderEditorBlock(fileContent || '표시할 preview 본문이 없습니다.', resolveCodeLanguageForResource(selectedFile), 'code')}
        </div>
      );
    }

    return <iframe title={selectedFile.name} src={selectedFile.previewUrl} className="resource-management-page__preview-frame" />;
  };

  const previewTabItems = selectedFile
    ? [
        {
          key: 'preview',
          label: (
            <Space size={6}>
              <EyeOutlined />
              <span>미리보기</span>
            </Space>
          ),
          children: <div className="resource-management-page__tab-panel">{renderPreviewContent()}</div>,
        },
        {
          key: 'edit',
          label: (
            <Space size={6}>
              <FileTextOutlined />
              <span>편집</span>
            </Space>
          ),
          children: selectedFile.isTextEditable ? (
            <div className="resource-management-page__tab-panel resource-management-page__editor-panel">
              <TextArea
                value={editorContent}
                onChange={(event) => {
                  setEditorContent(event.target.value);
                }}
                className="resource-management-page__editor"
              />
              <Space className="resource-management-page__editor-actions">
                <Button type="primary" icon={<SaveOutlined />} loading={saving} onClick={() => void handleSaveFile()}>
                  저장
                </Button>
                <Text type="secondary">{selectedFile.mimeType}</Text>
              </Space>
            </div>
          ) : (
            <Alert showIcon type="warning" message="이 파일은 텍스트 편집을 지원하지 않습니다." />
          ),
        },
      ]
    : [];

  const directoryListItems = useMemo<ResourceListItem[]>(() => {
    if (!selectedDirectoryPath) {
      return directoryItems.map((item) => ({
        ...item,
        isRecursiveMatch: false,
        matchContextPath: pathDirname(item.path),
      }));
    }

    const parentPath = pathDirname(selectedDirectoryPath);

    return [
      {
        name: '..',
        path: parentPath,
        type: 'directory' as const,
        extension: null,
        size: null,
        modifiedAt: new Date(0).toISOString(),
        previewUrl: null,
        isRecursiveMatch: false,
        matchContextPath: null,
      },
      ...directoryItems.map((item) => ({
        ...item,
        isRecursiveMatch: false,
        matchContextPath: pathDirname(item.path),
      })),
    ];
  }, [directoryItems, selectedDirectoryPath]);

  const isSearchFilterActive = searchKeyword.trim().length > 0;

  const filteredDirectoryListItems = useMemo<ResourceListItem[]>(() => {
    const keyword = searchKeyword.trim().toLocaleLowerCase('ko-KR');

    if (!keyword) {
      return directoryListItems;
    }

    const parentShortcut = directoryListItems.find((item) => item.name === '..');
    const directMatches = directoryListItems.filter((item) => item.name !== '..' && matchesResourceSearchKeyword(item, keyword));
    const nestedMatches = collectRecursiveSearchMatches(treeRoot, selectedDirectoryPath, keyword).filter(
      (item) => !directMatches.some((matchedItem) => matchedItem.path === item.path),
    );

    return [...(parentShortcut ? [parentShortcut] : []), ...directMatches, ...nestedMatches];
  }, [directoryListItems, searchKeyword, selectedDirectoryPath, treeRoot]);

  const sortedDirectoryListItems = useMemo<ResourceListItem[]>(() => {
    if (!listSortState) {
      return filteredDirectoryListItems;
    }

    const parentShortcut = filteredDirectoryListItems.find((item) => item.name === '..') ?? null;
    const items = filteredDirectoryListItems.filter((item) => item.name !== '..');

    const sortedItems = items
      .map((item, index) => ({ item, index }))
      .sort((left, right) => {
        if (left.item.type !== right.item.type) {
          return left.item.type === 'directory' ? -1 : 1;
        }

        let result = 0;

        if (listSortState.key === 'name') {
          result =
            listSortState.direction === 'asc'
              ? left.item.name.localeCompare(right.item.name, 'ko-KR')
              : right.item.name.localeCompare(left.item.name, 'ko-KR');
        } else if (listSortState.key === 'modifiedAt') {
          const leftTime = Number.isFinite(Date.parse(left.item.modifiedAt)) ? Date.parse(left.item.modifiedAt) : null;
          const rightTime = Number.isFinite(Date.parse(right.item.modifiedAt)) ? Date.parse(right.item.modifiedAt) : null;
          result = compareNullableNumber(leftTime, rightTime, listSortState.direction);
        } else if (listSortState.key === 'size') {
          result = compareNullableNumber(left.item.size, right.item.size, listSortState.direction);
        }

        if (result !== 0) {
          return result;
        }

        const nameFallback = left.item.name.localeCompare(right.item.name, 'ko-KR');
        if (nameFallback !== 0) {
          return nameFallback;
        }

        return left.index - right.index;
      })
      .map(({ item }) => item);

    return parentShortcut ? [parentShortcut, ...sortedItems] : sortedItems;
  }, [filteredDirectoryListItems, listSortState]);

  const applySearchKeyword = () => {
    setSearchKeyword(searchInputValue.trim());
    setSelectedListPath('');
  };

  const handleTreeRegionKeyDown = (event: ReactKeyboardEvent<HTMLDivElement>) => {
    if (isNativeContextMenuBypassTarget(event.target) || contextMenu.open || saving || treeLoading || visibleTreeNodes.length === 0) {
      return;
    }

    if (activeKeyboardRegion !== 'tree') {
      return;
    }

    setSelectionInteractionMode('keyboard');

    const selectedIndex = Math.max(
      0,
      visibleTreeNodes.findIndex((node) => node.path === selectedTreePath),
    );
    const selectedNode = visibleTreeNodes[selectedIndex] ?? visibleTreeNodes[0];

    if (!selectedNode) {
      return;
    }

    const moveSelection = (offset: number) => {
      const nextIndex = Math.min(visibleTreeNodes.length - 1, Math.max(0, selectedIndex + offset));
      const nextNode = visibleTreeNodes[nextIndex];
      if (!nextNode) {
        return;
      }
      setSelectedTreePath(nextNode.path);
    };

    if ((event.metaKey || event.ctrlKey) && !event.altKey && !event.shiftKey && event.key.toLowerCase() === 'c') {
      event.preventDefault();
      if (selectedNode.path) {
        copyEntryToClipboardBuffer(selectedNode);
      }
      return;
    }

    if ((event.metaKey || event.ctrlKey) && !event.altKey && event.shiftKey && event.key.toLowerCase() === 'c') {
      event.preventDefault();
      void copyManagedPath(selectedNode.path);
      return;
    }

    if ((event.metaKey || event.ctrlKey) && !event.altKey && !event.shiftKey && event.key === 'Backspace' && selectedNode.path) {
      event.preventDefault();
      requestDeleteForKeyboardTarget(selectedNode);
      return;
    }

    if ((event.metaKey || event.ctrlKey) && !event.altKey && !event.shiftKey && event.key.toLowerCase() === 'v') {
      event.preventDefault();
      const pasteTarget: ContextMenuTarget =
        selectedNode.type === 'directory'
          ? { kind: 'entry', entry: selectedNode }
          : { kind: 'workspace', path: pathDirname(selectedNode.path) };
      void handlePasteIntoDirectory(pasteTarget);
      return;
    }

    if (event.key === 'ArrowDown') {
      event.preventDefault();
      moveSelection(1);
      return;
    }

    if (event.key === 'ArrowUp') {
      event.preventDefault();
      moveSelection(-1);
      return;
    }

    if (event.key === 'ArrowRight') {
      event.preventDefault();
      if (selectedNode.type === 'directory') {
        const nodeKey = selectedNode.path || '/';
        if (!expandedKeys.map(String).includes(nodeKey)) {
          setExpandedKeys((current) => Array.from(new Set([...current, nodeKey])));
          return;
        }
      }
      void openEntry(selectedNode);
      return;
    }

    if (event.key === 'ArrowLeft') {
      event.preventDefault();
      if (selectedNode.type === 'directory') {
        const nodeKey = selectedNode.path || '/';
        if (expandedKeys.map(String).includes(nodeKey) && (selectedNode.children?.length ?? 0) > 0) {
          setExpandedKeys((current) => current.filter((key) => String(key) !== nodeKey));
          return;
        }
      }
      setSelectedTreePath(pathDirname(selectedNode.path));
      return;
    }

    if (event.key === 'Enter') {
      event.preventDefault();
      void openEntry(selectedNode);
    }
  };

  const handleListRegionKeyDown = (event: ReactKeyboardEvent<HTMLDivElement>) => {
    if (
      isNativeContextMenuBypassTarget(event.target) ||
      contextMenu.open ||
      saving ||
      directoryLoading ||
      sortedDirectoryListItems.length === 0
    ) {
      return;
    }

    if (activeKeyboardRegion !== 'list') {
      return;
    }

    setSelectionInteractionMode('keyboard');

    const resolvedSelectedPath =
      selectedListPath || (selectedDirectoryPath && !selectedFile ? selectedDirectoryPath : sortedDirectoryListItems[0]?.path ?? '');
    const selectedIndex = Math.max(
      0,
      sortedDirectoryListItems.findIndex((item) => item.path === resolvedSelectedPath),
    );
    const selectedItem = sortedDirectoryListItems[selectedIndex] ?? sortedDirectoryListItems[0];

    if (!selectedItem) {
      return;
    }

    const moveSelection = (offset: number) => {
      const nextIndex = Math.min(sortedDirectoryListItems.length - 1, Math.max(0, selectedIndex + offset));
      const nextItem = sortedDirectoryListItems[nextIndex];
      if (!nextItem) {
        return;
      }
      setSelectedListPath(nextItem.path);
    };

    if ((event.metaKey || event.ctrlKey) && !event.altKey && !event.shiftKey && event.key.toLowerCase() === 'c') {
      if (selectedItem.name !== '..') {
        event.preventDefault();
        copyEntryToClipboardBuffer(selectedItem);
      }
      return;
    }

    if ((event.metaKey || event.ctrlKey) && !event.altKey && event.shiftKey && event.key.toLowerCase() === 'c') {
      event.preventDefault();
      void copyManagedPath(selectedItem.path);
      return;
    }

    if ((event.metaKey || event.ctrlKey) && !event.altKey && !event.shiftKey && event.key === 'Backspace') {
      event.preventDefault();
      if (selectedItem.name !== '..') {
        requestDeleteForKeyboardTarget(selectedItem);
      }
      return;
    }

    if ((event.metaKey || event.ctrlKey) && !event.altKey && !event.shiftKey && event.key.toLowerCase() === 'v') {
      event.preventDefault();
      const pasteTarget: ContextMenuTarget =
        selectedItem.type === 'directory' && selectedItem.name !== '..'
          ? { kind: 'entry', entry: selectedItem }
          : { kind: 'workspace', path: selectedDirectoryPath };
      void handlePasteIntoDirectory(pasteTarget);
      return;
    }

    if (event.key === 'ArrowDown') {
      event.preventDefault();
      moveSelection(1);
      return;
    }

    if (event.key === 'ArrowUp') {
      event.preventDefault();
      moveSelection(-1);
      return;
    }

    if (event.key === 'ArrowLeft') {
      event.preventDefault();
      if (selectedDirectoryPath) {
        void refreshDirectory(pathDirname(selectedDirectoryPath));
      }
      return;
    }

    if (event.key === 'Backspace') {
      event.preventDefault();
      if (selectedDirectoryPath) {
        void refreshDirectory(pathDirname(selectedDirectoryPath));
      }
      return;
    }

    if (event.key === 'ArrowRight' || event.key === 'Enter') {
      event.preventDefault();
      void openEntry(selectedItem);
    }
  };

  const treeNodes: DataNode[] = useMemo(() => {
    if (!treeRoot) {
      return [];
    }

    const mapNode = (node: ResourceManagerTreeNode, compressForMobile = false): DataNode => {
      const mobileCompression = isMobileViewport && compressForMobile ? resolveMobileTreeCompression(node) : null;
      const visibleNode = mobileCompression?.visibleNode ?? node;
      const collapsedDirectoryCount = mobileCompression?.collapsedDirectoryCount ?? 0;

      return {
        key: visibleNode.path || '/',
        isLeaf: visibleNode.type === 'file',
        title: (
          <div
            className="resource-management-page__tree-title"
            data-resource-tree-path={visibleNode.path || '/'}
            onContextMenu={(event) =>
              openContextMenu(event, {
                kind: 'entry',
                entry: {
                  name: visibleNode.name,
                  path: visibleNode.path,
                  type: visibleNode.type,
                  extension: visibleNode.extension,
                  size: visibleNode.size,
                  modifiedAt: visibleNode.modifiedAt,
                  previewUrl: visibleNode.previewUrl,
                },
              })
            }
            onDoubleClick={() => {
              void openEntry(visibleNode);
            }}
            onClick={() => {
              setSelectionInteractionMode('pointer');
              setActiveKeyboardRegion('tree');
              focusRegion('tree');
            }}
            onTouchStart={(event) => {
              setSelectionInteractionMode('pointer');
              scheduleLongPress(event, {
                kind: 'entry',
                entry: {
                  name: visibleNode.name,
                  path: visibleNode.path,
                  type: visibleNode.type,
                  extension: visibleNode.extension,
                  size: visibleNode.size,
                  modifiedAt: visibleNode.modifiedAt,
                  previewUrl: visibleNode.previewUrl,
                },
              });
            }}
            onTouchEnd={cancelLongPress}
            onTouchMove={cancelLongPress}
            onTouchCancel={cancelLongPress}
          >
            <span className="resource-management-page__entry-name">
              <span className="resource-management-page__entry-icon">
                {visibleNode.path ? resolveResourceEntryIcon(visibleNode.extension, visibleNode.type, visibleNode.name) : <FolderOpenOutlined />}
              </span>
              <span className="resource-management-page__entry-name-text">{visibleNode.name || node.name}</span>
              {visibleNode.type === 'directory' && collapsedDirectoryCount > 0 ? (
                <span className="resource-management-page__entry-branch-count">+{collapsedDirectoryCount}</span>
              ) : null}
            </span>
            <Button
              type="text"
              size="small"
              icon={<EnterOutlined />}
              aria-label={node.type === 'directory' ? '탐색기 열기' : '파일 열기'}
              onClick={(event) => {
                event.preventDefault();
                event.stopPropagation();
                void openEntry(visibleNode);
              }}
            />
          </div>
        ),
        children: visibleNode.children?.map((child) => mapNode(child, true)),
      };
    };

    return [mapNode(treeRoot.tree)];
  }, [isMobileViewport, treeRoot]);

  const renderTreeCard = () => (
    <Card
      title={renderCardTitle('리소스 트리', treeCardPathLabel)}
      className="resource-management-page__sidebar"
      extra={(
        <Space size={4}>
            <Button
              type="text"
              size="small"
              icon={<PlusSquareOutlined />}
              aria-label="전체 펼치기"
              onClick={() => setExpandedKeys(collectExpandedKeys(treeRoot))}
            />
            <Button
              type="text"
              size="small"
              icon={<MinusSquareOutlined />}
              aria-label="전체 접기"
              onClick={() => setExpandedKeys(collectCollapsedKeys())}
            />
          <Button
            type="text"
            size="small"
            icon={treeLoading ? <ReloadOutlined spin /> : <ReloadOutlined />}
            aria-label="새로고침"
            onClick={() => void refreshTree()}
          />
        </Space>
      )}
    >
      {errorMessage ? <Alert showIcon type="error" message={errorMessage} style={{ marginBottom: 12 }} /> : null}
      <div
        ref={treeRegionRef}
        className={[
          'resource-management-page__tree-region',
          isKeyboardTreeSelectionVisible ? 'resource-management-page__keyboard-region--active' : '',
        ]
          .filter(Boolean)
          .join(' ')}
        tabIndex={0}
        role="tree"
        aria-label="리소스 트리"
        onKeyDown={handleTreeRegionKeyDown}
        onPaste={(event) => {
          void handlePasteFromClipboardEvent(event, {
            kind: 'workspace',
            path: selectedTreeNode?.type === 'directory' ? selectedTreeNode.path : pathDirname(selectedTreeNode?.path ?? ''),
          });
        }}
      >
        {treeLoading ? (
          <div className="resource-management-page__empty">
            <Spin />
          </div>
        ) : (
          <Tree
            className="resource-management-page__tree"
            treeData={treeNodes}
            selectedKeys={isKeyboardTreeSelectionVisible ? [selectedTreePath || '/'] : []}
            expandedKeys={expandedKeys}
            onExpand={(keys) => setExpandedKeys(keys)}
            onSelect={handleTreeSelect}
          />
        )}
      </div>
    </Card>
  );

  const renderListCard = () => (
    <Card
      title={renderCardTitle('폴더 목록', listCardPathLabel)}
      className="resource-management-page__content"
      extra={
        <Button icon={<ReloadOutlined />} onClick={() => void refreshAll(selectedDirectoryPath, false)}>
          새로고침
        </Button>
      }
    >
        <div className="resource-management-page__workspace">
          <div className="resource-management-page__toolbar">
            <div className="resource-management-page__toolbar-main">
              <div className="resource-management-page__toolbar-search-group">
                <Input
                  allowClear
                  className="resource-management-page__toolbar-search"
                  placeholder="검색어"
                  value={searchInputValue}
                  onChange={(event) => setSearchInputValue(event.target.value)}
                  onPressEnter={applySearchKeyword}
                />
                <Button
                  className="resource-management-page__toolbar-search-button"
                  icon={<SearchOutlined />}
                  aria-label="조회"
                  title="조회"
                  onClick={applySearchKeyword}
                />
              </div>
              <div className="resource-management-page__toolbar-status" aria-live="polite">
                <span
                  className={[
                    'resource-management-page__filter-badge',
                    isSearchFilterActive ? 'resource-management-page__filter-badge--active' : '',
                  ]
                    .filter(Boolean)
                    .join(' ')}
                >
                  {isSearchFilterActive ? `필터 적용: ${searchKeyword}` : '필터 없음'}
                </span>
                {isSearchFilterActive ? (
                  <span className="resource-management-page__filter-summary">
                    현재 폴더와 하위 폴더에서 {filteredDirectoryListItems.filter((item) => item.name !== '..').length}건
                  </span>
                ) : null}
              </div>
              <div className="resource-management-page__toolbar-path">
                <Text strong ellipsis={{ tooltip: currentDirectoryLabel }}>
                  {currentDirectoryLabel}
                </Text>
              </div>
            {selectedDirectoryPath ? (
              <Button
                size="small"
                onClick={() => {
                  void refreshDirectory(pathDirname(selectedDirectoryPath));
                }}
              >
                상위
              </Button>
            ) : null}
          </div>
          <div className="resource-management-page__toolbar-actions">
            <Button icon={<FolderAddOutlined />} aria-label="폴더 추가" title="폴더 추가" onClick={() => openCreateFolderModal()} />
            <Button icon={<FileAddOutlined />} aria-label="파일 추가" title="파일 추가" onClick={() => openCreateFileModal()} />
            <Button
              icon={<UploadOutlined />}
              aria-label="업로드"
              title="업로드"
              loading={saving}
              onClick={() => {
                openUploadDialog(selectedDirectoryPath);
              }}
            />
          </div>
        </div>

        <div
          ref={listRegionRef}
          className="resource-management-page__list-shell"
          tabIndex={0}
          role="listbox"
          aria-label="폴더 목록"
          aria-activedescendant={selectedListPath || undefined}
          onKeyDown={handleListRegionKeyDown}
          onPaste={(event) => {
            void handlePasteFromClipboardEvent(event, {
              kind: 'workspace',
              path: selectedDirectoryPath,
            });
          }}
          onContextMenu={(event) =>
            openContextMenu(event, {
              kind: 'workspace',
              path: selectedDirectoryPath,
            })
          }
          onTouchStart={(event) => {
            setSelectionInteractionMode('pointer');
            scheduleLongPress(event, {
              kind: 'workspace',
              path: selectedDirectoryPath,
            });
          }}
          onTouchEnd={cancelLongPress}
          onTouchMove={cancelLongPress}
          onTouchCancel={cancelLongPress}
        >
          <div className="resource-management-page__list-header">
            {(
              [
                ['name', '이름'],
                ['modifiedAt', '수정'],
                ['size', '크기'],
              ] as Array<[ResourceListSortKey, string]>
            ).map(([key, label]) => {
              const isActive = listSortState?.key === key;
              const sortDirection = isActive ? listSortState?.direction : null;

              return (
                <button
                  key={key}
                  type="button"
                  data-sort-key={key}
                  className={[
                    'resource-management-page__list-header-button',
                    isActive ? 'resource-management-page__list-header-button--active' : '',
                  ]
                    .filter(Boolean)
                    .join(' ')}
                  onClick={() => {
                    setSelectionInteractionMode('pointer');
                    setListSortState((current) => resolveNextSortState(current, key));
                  }}
                >
                  <span>{label}</span>
                  <span className="resource-management-page__list-header-sort-icon" aria-hidden="true">
                    {sortDirection === 'asc' ? <CaretUpOutlined /> : sortDirection === 'desc' ? <CaretDownOutlined /> : null}
                  </span>
                </button>
              );
            })}
            <span className="resource-management-page__list-header-action">작업</span>
          </div>
          <div className="resource-management-page__list-body">
            {directoryLoading ? (
              <div className="resource-management-page__empty">
                <Spin />
              </div>
            ) : sortedDirectoryListItems.length === 0 ? (
              <Empty
                description={searchKeyword ? '검색 결과가 없습니다.' : '선택한 디렉터리에 항목이 없습니다.'}
                className="resource-management-page__empty"
              />
            ) : (
              sortedDirectoryListItems.map((item) => {
                const isParentShortcut = item.name === '..';
                const isParentShortcutSelected =
                  isKeyboardListSelectionVisible && isParentShortcut && selectedListPath === item.path;
                const isSelected = isKeyboardListSelectionVisible && !isParentShortcut && selectedListPath === item.path;

                return (
                  <div
                    key={isParentShortcut ? '__parent__' : item.path}
                    id={isParentShortcut ? '__parent__' : item.path}
                    data-resource-list-path={item.path}
                    className={[
                      'resource-management-page__list-row',
                      isSelected ? 'resource-management-page__list-row--selected' : '',
                      isParentShortcut ? 'resource-management-page__list-row--parent' : '',
                      isParentShortcutSelected ? 'resource-management-page__list-row--parent-selected' : '',
                    ]
                      .filter(Boolean)
                      .join(' ')}
                    onClick={() => {
                      setSelectionInteractionMode('pointer');
                      setActiveKeyboardRegion('list');
                      focusRegion('list');
                      setSelectedListPath(item.path);
                      handlePotentialDoubleTap(isParentShortcut ? '__parent__' : item.path, () => {
                        void openEntry(item);
                      });
                    }}
                    onDoubleClick={() => {
                      void openEntry(item);
                    }}
                    onContextMenu={
                      isParentShortcut
                        ? undefined
                        : (event) =>
                            openContextMenu(event, {
                              kind: 'entry',
                              entry: item,
                            })
                    }
                    onTouchStart={
                      isParentShortcut
                        ? undefined
                        : (event) => {
                            setSelectionInteractionMode('pointer');
                            scheduleLongPress(event, {
                              kind: 'entry',
                              entry: item,
                            });
                          }
                    }
                    onTouchEnd={cancelLongPress}
                    onTouchMove={cancelLongPress}
                    onTouchCancel={cancelLongPress}
                  >
                    <div className="resource-management-page__list-name">
                      <span className="resource-management-page__entry-icon">
                        {isParentShortcut ? <FolderOpenOutlined /> : resolveResourceEntryIcon(item.extension, item.type, item.name)}
                      </span>
                      <span className="resource-management-page__entry-name-stack">
                        <span className="resource-management-page__entry-name-text">
                          {isParentShortcut ? '.. 상위 폴더' : item.name}
                        </span>
                        {!isParentShortcut && isSearchFilterActive ? (
                          <span className="resource-management-page__entry-context-text">
                            {item.isRecursiveMatch ? `하위 경로: ${formatRelativeDirectoryLabel(selectedDirectoryPath, item.matchContextPath ?? '')}` : '현재 폴더'}
                          </span>
                        ) : null}
                      </span>
                    </div>
                    <div className="resource-management-page__list-meta">
                      <span>{isParentShortcut ? formatPathLabel(item.path) : renderModifiedAtLabel(item.modifiedAt)}</span>
                      <span>{isParentShortcut ? '-' : formatSize(item.size)}</span>
                    </div>
                    <Space size={4}>
                      <Button
                        type="text"
                        size="small"
                        icon={<EnterOutlined />}
                        aria-label="열기"
                        onClick={(event) => {
                          event.stopPropagation();
                          void openEntry(item);
                        }}
                      />
                      {isParentShortcut ? null : (
                        <Button
                          type="text"
                          size="small"
                          icon={<EllipsisOutlined />}
                          aria-label="컨텍스트 메뉴"
                          onClick={(event) => {
                            event.stopPropagation();
                            const rect = event.currentTarget.getBoundingClientRect();
                            setContextMenu({
                              open: true,
                              x: rect.left,
                              y: rect.bottom + 8,
                              target: {
                                kind: 'entry',
                                entry: item,
                              },
                            });
                          }}
                        />
                      )}
                    </Space>
                  </div>
                );
              })
            )}
          </div>
        </div>
      </div>
    </Card>
  );

  const renderPreviewCard = () => (
    <Card
      title={
        renderCardTitle(
          '미리보기 / 편집',
          selectedFile ? (
            <span className="resource-management-page__preview-title-file" title={formatPathLabel(selectedFile.path)}>
              {previewCardPathLabel}
            </span>
          ) : (
            previewCardPathLabel
          ),
        )
      }
      className="resource-management-page__preview-card"
      extra={
        selectedFile && activePreviewTab === 'preview' ? (
          <Button
            type={isPreviewMaximized ? 'primary' : 'default'}
            icon={isPreviewMaximized ? <ShrinkOutlined /> : <ArrowsAltOutlined />}
            onClick={() => {
              setPreviewZoom(1);
              setIsPreviewMaximized((current) => !current);
            }}
          >
            {isPreviewMaximized ? '최대화 해제' : '최대화'}
          </Button>
        ) : null
      }
    >
      {fileLoading ? (
        <div className="resource-management-page__empty">
          <Spin />
        </div>
      ) : selectedFile ? (
        <>
          <div className="resource-management-page__preview-meta">
            <Text type="secondary" ellipsis={{ tooltip: formatPathLabel(selectedFile.path) }}>
              {formatPathLabel(selectedFile.path)}
            </Text>
            <Text copyable={{ text: selectedFile.previewUrl }} className="resource-management-page__preview-copy">
              미리보기 URL 복사
            </Text>
            {isHtmlFile(selectedFile) ? (
              <div className="resource-management-page__html-mode-switch" role="group" aria-label="HTML 미리보기 모드">
                <Button
                  size="small"
                  className="resource-management-page__html-mode-button"
                  type={htmlPreviewMode === 'browser' ? 'primary' : 'default'}
                  onClick={() => setHtmlPreviewMode('browser')}
                >
                  브라우저
                </Button>
                <Button
                  size="small"
                  className="resource-management-page__html-mode-button"
                  type={htmlPreviewMode === 'source' ? 'primary' : 'default'}
                  onClick={() => setHtmlPreviewMode('source')}
                >
                  소스
                </Button>
              </div>
            ) : null}
          </div>
          <Tabs
            className="resource-management-page__preview-tabs"
            activeKey={activePreviewTab}
            onChange={setActivePreviewTab}
            items={previewTabItems}
          />
        </>
      ) : (
        <Empty
          description="파일을 열면 이 영역에서 미리보기와 편집을 진행합니다."
          className="resource-management-page__empty"
        />
      )}
    </Card>
  );

  const renderMobileNavButton = (panel: MobilePanel, label: string, icon: ReactNode, disabled = false) => {
    const isActive = mobilePanel === panel;

    return (
      <button
        key={panel}
        type="button"
        role="tab"
        aria-selected={isActive}
        aria-disabled={disabled}
        disabled={disabled}
        className={[
          'resource-management-page__mobile-nav-button',
          isActive ? 'resource-management-page__mobile-nav-button--active' : '',
        ]
          .filter(Boolean)
          .join(' ')}
        onClick={() => {
          setSelectionInteractionMode('pointer');
          if (!disabled) {
            setMobilePanel(panel);
          }
        }}
      >
        <span className="resource-management-page__mobile-nav-button-icon">{icon}</span>
        <span className="resource-management-page__mobile-nav-button-label">{label}</span>
      </button>
    );
  };

  const renderCompactDesktopPanels = () => {
    if (selectedFile) {
      return (
        <>
          {renderListCard()}
          {renderPreviewCard()}
        </>
      );
    }

    return (
      <>
        {renderTreeCard()}
        {renderListCard()}
      </>
    );
  };

  const bindContextMenuAction = (
    action: 'open' | 'copy' | 'copy-path' | 'move' | 'rename' | 'delete' | 'new-folder' | 'new-file' | 'paste',
  ) => ({
    onPointerDown: (event: ReactMouseEvent<HTMLButtonElement> | ReactTouchEvent<HTMLButtonElement>) => {
      event.preventDefault();
      event.stopPropagation();
    },
    onClick: (event: ReactMouseEvent<HTMLButtonElement>) => {
      event.preventDefault();
      event.stopPropagation();
      void handleContextMenuAction(action);
    },
  });

  return (
    <div
      ref={containerRef}
      className={[
        'resource-management-page',
        isMobileViewport ? 'resource-management-page--mobile' : '',
        isCompactViewport ? 'resource-management-page--compact' : '',
        selectedFile ? 'resource-management-page--has-preview' : '',
        `resource-management-page--panel-${mobilePanel}`,
      ]
        .filter(Boolean)
        .join(' ')}
      style={!isMobileViewport && viewportHeight ? { height: `${viewportHeight}px` } : undefined}
      onContextMenuCapture={(event) => {
        if (!isNativeContextMenuBypassTarget(event.target)) {
          event.preventDefault();
        }
      }}
      onMouseDownCapture={preventSelection}
      onDragStartCapture={preventSelection}
      onPointerDownCapture={(event) => {
        if ((event.pointerType === 'touch' || event.pointerType === 'pen') && !isNativeContextMenuBypassTarget(event.target)) {
          setSelectionInteractionMode('pointer');
        }
        if (event.pointerType === 'touch' && !isNativeContextMenuBypassTarget(event.target)) {
          clearTextSelection();
        }
      }}
    >
      {isMobileViewport ? (
        <>
          <div className="resource-management-page__mobile-nav" role="tablist" aria-label="리소스 보기 전환">
            {visibleMobilePanels.map((panel) => {
              if (panel === 'tree') {
                return renderMobileNavButton(panel, '트리', <FolderOpenOutlined />);
              }

              if (panel === 'list') {
                return renderMobileNavButton(panel, '폴더형', <FileTextOutlined />);
              }

              return renderMobileNavButton(panel, '미리보기', <EyeOutlined />, !selectedFile);
            })}
          </div>
          <div className="resource-management-page__mobile-card">
            {mobilePanel === 'tree' ? renderTreeCard() : null}
            {mobilePanel === 'list' ? renderListCard() : null}
            {mobilePanel === 'preview' ? renderPreviewCard() : null}
          </div>
        </>
      ) : isCompactViewport ? (
        renderCompactDesktopPanels()
      ) : (
        <>
          {renderTreeCard()}
          {renderListCard()}
          {renderPreviewCard()}
        </>
      )}

      {contextMenu.open && contextMenu.target ? (
        <>
          <div
            className="resource-management-page__context-menu-backdrop"
            onPointerDown={() => {
              setContextMenu((current) => ({ ...current, open: false, target: null }));
            }}
          />
          <div
            ref={contextMenuRef}
            className="resource-management-page__context-menu"
            style={{ left: `${contextMenu.x}px`, top: `${contextMenu.y}px` }}
            onPointerDown={(event) => {
              event.stopPropagation();
            }}
          >
            {contextMenu.target.kind === 'entry' ? (
              <>
                <button type="button" {...bindContextMenuAction('open')}>
                  <EnterOutlined />
                  <span>{contextMenu.target.entry.type === 'directory' ? '탐색기 열기' : '파일 열기'}</span>
                </button>
                <button type="button" {...bindContextMenuAction('copy')}>
                  <CopyOutlined />
                  <span>복사</span>
                </button>
                <button type="button" {...bindContextMenuAction('copy-path')}>
                  <CopyOutlined />
                  <span>경로 복사</span>
                </button>
                <button type="button" {...bindContextMenuAction('move')}>
                  <ScissorOutlined />
                  <span>이동</span>
                </button>
                <button type="button" {...bindContextMenuAction('rename')}>
                  <FileTextOutlined />
                  <span>이름 변경</span>
                </button>
                {contextMenu.target.entry.type === 'directory' ? (
                  <button type="button" {...bindContextMenuAction('new-folder')}>
                    <FolderAddOutlined />
                    <span>하위 폴더 생성</span>
                  </button>
                ) : null}
                {contextMenu.target.entry.type === 'directory' ? (
                  <button type="button" {...bindContextMenuAction('new-file')}>
                    <FileAddOutlined />
                    <span>하위 파일 생성</span>
                  </button>
                ) : null}
                {contextMenu.target.entry.type === 'directory' ? (
                  <button type="button" {...bindContextMenuAction('paste')}>
                    <UploadOutlined />
                    <span>붙여넣기</span>
                  </button>
                ) : null}
                {contextMenu.target.entry.type === 'directory' ? (
                  <button
                    type="button"
                    onPointerDown={(event) => {
                      event.preventDefault();
                      event.stopPropagation();
                      const targetDirectoryPath = resolveUploadDirectoryPath(contextMenu.target);
                      setContextMenu((current) => ({ ...current, open: false, target: null }));
                      openUploadDialog(targetDirectoryPath);
                    }}
                  >
                    <UploadOutlined />
                    <span>파일 업로드</span>
                  </button>
                ) : null}
                <button type="button" className="danger" {...bindContextMenuAction('delete')}>
                  <DeleteOutlined />
                  <span>삭제</span>
                </button>
              </>
            ) : (
              <>
                <button type="button" {...bindContextMenuAction('new-folder')}>
                  <FolderAddOutlined />
                  <span>새 폴더</span>
                </button>
                <button type="button" {...bindContextMenuAction('new-file')}>
                  <FileAddOutlined />
                  <span>새 파일</span>
                </button>
                <button type="button" {...bindContextMenuAction('paste')}>
                  <UploadOutlined />
                  <span>붙여넣기</span>
                </button>
                <button
                  type="button"
                  onPointerDown={(event) => {
                    event.preventDefault();
                    event.stopPropagation();
                    const targetDirectoryPath = resolveUploadDirectoryPath(contextMenu.target);
                    setContextMenu((current) => ({ ...current, open: false, target: null }));
                    openUploadDialog(targetDirectoryPath);
                  }}
                >
                  <UploadOutlined />
                  <span>파일 업로드</span>
                </button>
              </>
            )}
          </div>
        </>
      ) : null}

      <input
        ref={fileInputRef}
        type="file"
        multiple
        className="resource-management-page__file-input"
        onChange={(event) => {
          void handleUploadFiles(event.target.files, contextUploadTarget?.directoryPath ?? selectedDirectoryPath);
        }}
      />

      <Modal
        open={Boolean(createEntryModal)}
        title={createEntryModal?.type === 'folder' ? '새 폴더' : '새 파일'}
        okText="생성"
        cancelText="취소"
        confirmLoading={saving}
        modalRender={renderModalWithEnterConfirm}
        onOk={() => void handleCreateEntry()}
        onCancel={() => {
          setCreateEntryModal(null);
          setCreateEntryName('');
        }}
      >
        <Input
          autoFocus
          placeholder={createEntryModal?.type === 'folder' ? '폴더 이름' : '예: README.md'}
          value={createEntryName}
          onChange={(event) => setCreateEntryName(event.target.value)}
        />
      </Modal>

      <Modal
        open={Boolean(deleteTarget)}
        title={deleteTarget?.type === 'directory' ? '폴더 삭제' : '파일 삭제'}
        okText="삭제"
        okButtonProps={{ danger: true }}
        cancelText="취소"
        confirmLoading={saving}
        modalRender={renderModalWithEnterConfirm}
        onOk={() => void confirmDeleteItem()}
        onCancel={() => {
          setDeleteTarget(null);
        }}
      >
        <Text>{deleteTarget ? `${deleteTarget.name} 항목을 삭제합니다. 계속하시겠습니까?` : ''}</Text>
      </Modal>

      <Modal
        open={Boolean(resourceActionMode && actionTarget)}
        title={
          resourceActionMode === 'copy' ? '리소스 복사' : resourceActionDirectoryPath === pathDirname(actionTarget?.path ?? '') ? '이름 변경' : '리소스 이동'
        }
        okText={resourceActionMode === 'copy' ? '복사' : resourceActionDirectoryPath === pathDirname(actionTarget?.path ?? '') ? '변경' : '이동'}
        cancelText="취소"
        confirmLoading={saving}
        modalRender={renderModalWithEnterConfirm}
        onOk={() => void handleCopyMove()}
        onCancel={() => {
          setResourceActionMode(null);
          setActionTarget(null);
        }}
      >
        <Space direction="vertical" size={12} style={{ width: '100%' }}>
          <Text>
            {actionTarget
              ? resourceActionMode === 'copy'
                ? `${actionTarget.name} 항목을 복사합니다.`
                : resourceActionDirectoryPath === pathDirname(actionTarget.path)
                  ? `${actionTarget.name} 항목의 이름을 변경합니다.`
                  : `${actionTarget.name} 항목의 위치를 변경합니다.`
              : ''}
          </Text>
          <div>
            <Text strong>대상 디렉터리</Text>
            <Input
              style={{ marginTop: 8 }}
              list="resource-management-directory-targets"
              value={formatPathLabel(resourceActionDirectoryPath)}
              onChange={(event) => {
                const rawValue = event.target.value.trim();
                const matched = directoryTargets.find((item) => item.label === rawValue);

                if (matched) {
                  setResourceActionDirectoryPath(matched.path);
                  return;
                }

                const normalized = rawValue.replace(/^resource\/?/, '').replace(/^\/+/, '');
                setResourceActionDirectoryPath(normalized);
              }}
            />
            <datalist id="resource-management-directory-targets">
              {directoryTargets.map((item) => (
                <option key={item.path || '/'} value={item.label} />
              ))}
            </datalist>
          </div>
          <div>
            <Text strong>이름</Text>
            <Input style={{ marginTop: 8 }} value={resourceActionName} onChange={(event) => setResourceActionName(event.target.value)} />
          </div>
        </Space>
      </Modal>

      <Modal
        open={isPreviewMaximized && Boolean(selectedFile) && activePreviewTab === 'preview'}
        title={selectedFile ? `미리보기 - ${selectedFile.name}` : '미리보기'}
        footer={null}
        onCancel={() => {
          setIsPreviewMaximized(false);
          setPreviewZoom(1);
          setPreviewOffset({ x: 0, y: 0 });
          previewTouchGestureRef.current = null;
        }}
        width={isMobileViewport ? '100vw' : 'calc(100vw - 32px)'}
        style={isMobileViewport ? { top: 0, maxWidth: '100vw', paddingBottom: 0 } : { top: 16 }}
        styles={{
          content: {
            overflow: 'hidden',
          },
          body: {
            height: 'calc(100vh - 132px)',
            padding: 0,
            overflow: 'hidden',
          },
        }}
        wrapClassName={isMobileViewport ? 'resource-management-page__preview-modal-wrap--mobile' : undefined}
      >
        <div className="resource-management-page__preview-modal-shell">
          <div className="resource-management-page__preview-modal-body">{renderPreviewContent(true)}</div>
          {canZoomPreview ? (
            <div className="resource-management-page__preview-modal-toolbar">
              <Button
                className="resource-management-page__preview-modal-toolbar-button"
                icon={<ZoomOutOutlined />}
                onClick={() => setPreviewZoom((current) => clampPreviewZoom(current - 0.1))}
              />
              <div className="resource-management-page__preview-modal-toolbar-slider">
                <Slider
                  min={50}
                  max={300}
                  step={10}
                  value={Math.round(previewZoom * 100)}
                  onChange={(value) => {
                    if (typeof value === 'number') {
                      setPreviewZoomPercent(value);
                    }
                  }}
                />
              </div>
              <Text className="resource-management-page__preview-modal-toolbar-value">{Math.round(previewZoom * 100)}%</Text>
              <Button
                className="resource-management-page__preview-modal-toolbar-button"
                icon={<ZoomInOutlined />}
                onClick={() => setPreviewZoom((current) => clampPreviewZoom(current + 0.1))}
              />
            </div>
          ) : null}
        </div>
      </Modal>
    </div>
  );
}
