"use client";

import Image from "next/image";
import { useRouter } from "next/navigation";
import {
  startTransition,
  useDeferredValue,
  useEffect,
  useEffectEvent,
  useRef,
  useState,
} from "react";
import type {
  MailboxFileDrawerFilter,
  MailboxFileDrawerItem,
  MailboxFileDrawerSnapshot,
} from "@/lib/mailbox-file-drawer";

type UploadJob = {
  error: string | null;
  file: File;
  id: string;
  progress: number;
  status: "error" | "queued" | "saving" | "uploading" | "complete";
};

type DeleteConfirmation = {
  count: number;
  includesLargeAttachment: boolean;
  mode: "all" | "selected";
};

type FileContextMenu = {
  itemId: string;
  x: number;
  y: number;
};

const MAX_FILE_BYTES = 25 * 1024 * 1024;
const FILE_DRAWER_HELP = "직접 올린 파일과 보낸 대용량 첨부는 파일서랍 용량을 공유합니다. 받은 첨부는 원본 메일에서 불러오고, 수신한 대용량 다운로드 링크는 원본 메일에서 확인합니다.";
const EMPTY_SNAPSHOT: MailboxFileDrawerSnapshot = {
  generatedAt: "",
  hasMore: false,
  items: [],
  mailAttachmentBytes: 0,
  mailAttachmentCount: 0,
  manualFileBytes: 0,
  manualFileCount: 0,
  page: 1,
  pageSize: 60,
  quotaBytes: null,
  quotaIsUnlimited: false,
  quotaUsagePercent: null,
  quotaUsedBytes: null,
  totalCount: 0,
};

function formatBytes(value: number | null) {
  if (value === null) return "집계 중";
  if (value < 1024) return `${value} B`;
  if (value < 1024 * 1024) return `${(value / 1024).toFixed(value < 10 * 1024 ? 1 : 0)} KB`;
  if (value < 1024 * 1024 * 1024) return `${(value / 1024 / 1024).toFixed(value < 10 * 1024 * 1024 ? 1 : 0)} MB`;
  return `${(value / 1024 / 1024 / 1024).toFixed(1)} GB`;
}

function extensionLabel(filename: string) {
  const extension = filename.split(".").at(-1)?.replace(/[^a-z0-9]/gi, "").toUpperCase();
  return extension && extension !== filename.toUpperCase() ? extension.slice(0, 5) : "FILE";
}

function formatDate(value: string) {
  const date = new Date(value);
  return new Intl.DateTimeFormat("ko-KR", {
    dateStyle: "medium",
    timeStyle: "short",
  }).format(date);
}

function directionLabel(item: MailboxFileDrawerItem) {
  if (item.direction === "inbound") return "받은 메일 첨부";
  return item.direction === "uploaded" ? "직접 업로드" : "보낸 대용량 첨부";
}

function FileGlyph({ item }: { item: MailboxFileDrawerItem }) {
  if (item.previewUrl && item.isImage) {
    return (
      <Image
        unoptimized
        src={item.previewUrl}
        alt=""
        fill
        draggable={false}
        sizes="(max-width: 640px) 45vw, (max-width: 1280px) 16vw, 10vw"
        className="object-cover transition-transform duration-300 group-hover:scale-[1.025]"
      />
    );
  }

  return (
    <div className="flex h-full w-full items-center justify-center bg-[linear-gradient(145deg,#f8fbff,#eef4ff)]">
      <div className="relative flex h-20 w-16 items-center justify-center rounded-[14px] border border-[#d7e4fb] bg-white text-[12px] font-extrabold tracking-[0.06em] text-[#2f6df6] shadow-[0_12px_28px_rgba(47,109,246,0.1)]">
        <span className="absolute right-0 top-0 h-5 w-5 rounded-bl-[9px] border-b border-l border-[#d7e4fb] bg-[#edf4ff]" />
        {extensionLabel(item.name)}
      </div>
    </div>
  );
}

function UploadIcon({ className = "h-5 w-5" }: { className?: string }) {
  return (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" className={className}>
      <path d="M12 16V4" />
      <path d="m7.5 8.5 4.5-4.5 4.5 4.5" />
      <path d="M5 20h14" />
    </svg>
  );
}

function DownloadIcon() {
  return (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" className="h-4 w-4">
      <path d="M12 4v11" />
      <path d="m7.5 11 4.5 4.5 4.5-4.5" />
      <path d="M5 20h14" />
    </svg>
  );
}

function TrashIcon() {
  return (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" className="h-4 w-4">
      <path d="M4 7h16" />
      <path d="M9 7V4h6v3" />
      <path d="m6.5 7 .8 13h9.4l.8-13" />
      <path d="M10 11v5M14 11v5" />
    </svg>
  );
}

function ChevronIcon({ direction }: { direction: "left" | "right" }) {
  return (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" className="h-11 w-11 sm:h-14 sm:w-14">
      <path d={direction === "left" ? "m15 5-7 7 7 7" : "m9 5 7 7-7 7"} />
    </svg>
  );
}

function CloseIcon() {
  return (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" className="h-5 w-5">
      <path d="m6 6 12 12" />
      <path d="m18 6-12 12" />
    </svg>
  );
}

function SelectionCheck({ selected }: { selected: boolean }) {
  return (
    <span
      aria-hidden="true"
      className={`inline-flex h-[18px] w-[18px] items-center justify-center rounded-[5px] transition-colors ${selected ? "bg-[#2f6df6] text-white shadow-[0_3px_10px_rgba(47,109,246,0.3)]" : "bg-white/92 text-transparent shadow-[0_3px_12px_rgba(15,23,42,0.16)] ring-1 ring-black/10"}`}
    >
      <svg viewBox="0 0 18 18" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="h-3 w-3">
        <path d="m4 9 3 3 7-7" />
      </svg>
    </span>
  );
}

function SearchIcon() {
  return (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" className="h-[17px] w-[17px]">
      <circle cx="10.5" cy="10.5" r="6.5" />
      <path d="m16 16 4 4" />
    </svg>
  );
}

export function MailFileDrawer() {
  const router = useRouter();
  const fileInputRef = useRef<HTMLInputElement | null>(null);
  const helpRef = useRef<HTMLDivElement | null>(null);
  const scrollContainerRef = useRef<HTMLDivElement | null>(null);
  const loadMoreSentinelRef = useRef<HTMLDivElement | null>(null);
  const loadingMoreRef = useRef(false);
  const viewerPointerStartRef = useRef<number | null>(null);
  const [snapshot, setSnapshot] = useState(EMPTY_SNAPSHOT);
  const [filter, setFilter] = useState<MailboxFileDrawerFilter>("all");
  const [query, setQuery] = useState("");
  const [helpOpen, setHelpOpen] = useState(false);
  const deferredQuery = useDeferredValue(query);
  const [loading, setLoading] = useState(true);
  const [loadingMore, setLoadingMore] = useState(false);
  const [dragActive, setDragActive] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [uploadJobs, setUploadJobs] = useState<UploadJob[]>([]);
  const [uploadModalOpen, setUploadModalOpen] = useState(false);
  const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
  const [deleteConfirmation, setDeleteConfirmation] = useState<DeleteConfirmation | null>(null);
  const [deleting, setDeleting] = useState(false);
  const [viewerIndex, setViewerIndex] = useState<number | null>(null);
  const [viewerDirection, setViewerDirection] = useState<-1 | 0 | 1>(0);
  const [viewerAnimationKey, setViewerAnimationKey] = useState(0);
  const [viewerZoom, setViewerZoom] = useState(1);
  const [contextMenu, setContextMenu] = useState<FileContextMenu | null>(null);
  const [shareItemId, setShareItemId] = useState<string | null>(null);
  const [shareAccess, setShareAccess] = useState<"owner" | "public">("owner");
  const [shareUrl, setShareUrl] = useState<string | null>(null);
  const [shareSaving, setShareSaving] = useState(false);
  const [shareCopied, setShareCopied] = useState(false);
  const activeRequestsRef = useRef(new Set<XMLHttpRequest>());
  const snapshotReceivedAtRef = useRef(0);

  const loadFiles = async (page = 1, append = false, signal?: AbortSignal, silent = false) => {
    if (append && loadingMoreRef.current) return null;
    if (append) {
      loadingMoreRef.current = true;
      setLoadingMore(true);
    }
    else if (!silent) setLoading(true);

    try {
      const params = new URLSearchParams({
        filter,
        page: String(page),
        pageSize: "60",
      });
      if (deferredQuery.trim()) params.set("q", deferredQuery.trim());
      const response = await fetch(`/api/mailbox/files?${params}`, {
        cache: "no-store",
        credentials: "same-origin",
        signal,
      });
      const payload = await response.json().catch(() => null);
      if (!response.ok || !payload?.snapshot) {
        throw new Error(payload?.message || "파일 목록을 불러오지 못했습니다.");
      }
      const nextSnapshot = payload.snapshot as MailboxFileDrawerSnapshot;
      snapshotReceivedAtRef.current = Date.now();
      setSnapshot((current) => {
        const preserveLoadedPages = silent && !append && current.page > 1;
        const items = append
          ? [...current.items, ...nextSnapshot.items]
          : preserveLoadedPages
            ? [...nextSnapshot.items, ...current.items]
            : nextSnapshot.items;
        const visibleItems = items.filter(
          (item, index) =>
            items.findIndex((candidate) => candidate.id === item.id) === index &&
            (!item.expiresAt || Date.parse(item.expiresAt) > Date.parse(nextSnapshot.generatedAt)),
        ).slice(0, nextSnapshot.totalCount);
        return {
          ...nextSnapshot,
          items: visibleItems,
          page: append ? nextSnapshot.page : preserveLoadedPages ? current.page : nextSnapshot.page,
          hasMore: append ? nextSnapshot.hasMore : preserveLoadedPages
            ? visibleItems.length < nextSnapshot.totalCount : nextSnapshot.hasMore,
        };
      });
      setError(null);
      return nextSnapshot;
    } catch (loadError) {
      if ((loadError as { name?: string })?.name !== "AbortError") {
        setError(loadError instanceof Error ? loadError.message : "파일 목록을 불러오지 못했습니다.");
      }
      return null;
    } finally {
      if (append) {
        loadingMoreRef.current = false;
        setLoadingMore(false);
      }
      else if (!silent) setLoading(false);
    }
  };

  const loadFirstPage = useEffectEvent((signal: AbortSignal) => loadFiles(1, false, signal));
  const refreshFiles = useEffectEvent(() => { void loadFiles(1, false, undefined, true); });
  useEffect(() => {
    const controller = new AbortController();
    const timer = window.setTimeout(() => {
      setSelectedIds(new Set());
      setViewerIndex(null);
      void loadFirstPage(controller.signal);
    }, 0);
    return () => {
      window.clearTimeout(timer);
      controller.abort();
    };
  }, [deferredQuery, filter]);

  useEffect(() => {
    const controller = new AbortController();
    void fetch("/api/mailbox/files/sync", {
      credentials: "same-origin",
      method: "POST",
      signal: controller.signal,
    }).then(async (response) => {
      const result = response.ok ? await response.json().catch(() => null) : null;
      if (result?.changed && !controller.signal.aborted) refreshFiles();
    }).catch(() => undefined);
    return () => controller.abort();
    // This sync is intentionally once per drawer opening, not on each search/filter.
  }, []);

  useEffect(() => {
    const expiries = snapshot.items
      .map((item) => item.expiresAt ? Date.parse(item.expiresAt) : NaN)
      .filter(Number.isFinite);
    if (expiries.length === 0 || !snapshot.generatedAt) return;
    const serverNow = Date.parse(snapshot.generatedAt);
    if (!Number.isFinite(serverNow)) return;
    const elapsedSinceSnapshot = Date.now() - snapshotReceivedAtRef.current;
    const delay = Math.max(0, Math.min(2_147_000_000,
      Math.min(...expiries) - serverNow - elapsedSinceSnapshot + 100));
    const timer = window.setTimeout(() => refreshFiles(), delay);
    return () => window.clearTimeout(timer);
  }, [snapshot.generatedAt, snapshot.items]);

  useEffect(() => {
    const refreshWhenVisible = () => {
      if (!document.hidden) refreshFiles();
    };
    window.addEventListener("focus", refreshWhenVisible);
    document.addEventListener("visibilitychange", refreshWhenVisible);
    return () => {
      window.removeEventListener("focus", refreshWhenVisible);
      document.removeEventListener("visibilitychange", refreshWhenVisible);
    };
  }, []);

  const loadNextPage = useEffectEvent(() => {
    if (!snapshot.hasMore || loadingMoreRef.current) return;
    void loadFiles(snapshot.page + 1, true);
  });

  useEffect(() => {
    const sentinel = loadMoreSentinelRef.current;
    const root = scrollContainerRef.current;
    if (!sentinel || !root || !snapshot.hasMore) return;

    const observer = new IntersectionObserver(
      (entries) => {
        if (entries.some((entry) => entry.isIntersecting)) loadNextPage();
      },
      { root, rootMargin: "240px 0px", threshold: 0.01 },
    );
    observer.observe(sentinel);
    return () => observer.disconnect();
  }, [snapshot.hasMore, snapshot.page]);

  useEffect(() => () => {
    for (const request of activeRequestsRef.current) request.abort();
  }, []);

  useEffect(() => {
    if (!helpOpen) return;
    const closeOnOutside = (event: PointerEvent) => {
      if (!helpRef.current?.contains(event.target as Node)) setHelpOpen(false);
    };
    const closeOnEscape = (event: KeyboardEvent) => {
      if (event.key === "Escape") setHelpOpen(false);
    };
    document.addEventListener("pointerdown", closeOnOutside);
    document.addEventListener("keydown", closeOnEscape);
    return () => {
      document.removeEventListener("pointerdown", closeOnOutside);
      document.removeEventListener("keydown", closeOnEscape);
    };
  }, [helpOpen]);

  useEffect(() => {
    if (viewerIndex === null && !deleteConfirmation && !shareItemId) return;
    const previousOverflow = document.body.style.overflow;
    document.body.style.overflow = "hidden";
    return () => {
      document.body.style.overflow = previousOverflow;
    };
  }, [deleteConfirmation, shareItemId, viewerIndex]);

  useEffect(() => {
    if (!contextMenu) return;
    const close = () => setContextMenu(null);
    const closeOnKey = (event: KeyboardEvent) => {
      if (event.key === "Escape") close();
    };
    window.addEventListener("click", close);
    window.addEventListener("resize", close);
    window.addEventListener("scroll", close, true);
    window.addEventListener("keydown", closeOnKey);
    return () => {
      window.removeEventListener("click", close);
      window.removeEventListener("resize", close);
      window.removeEventListener("scroll", close, true);
      window.removeEventListener("keydown", closeOnKey);
    };
  }, [contextMenu]);

  useEffect(() => {
    if (viewerIndex === null) return;
    const handleKeyDown = (event: KeyboardEvent) => {
      if (event.key === "Escape") {
        setViewerIndex(null);
        return;
      }
      if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return;
      const direction = event.key === "ArrowLeft" ? -1 : 1;
      const nextIndex = viewerIndex + direction;
      if (nextIndex < 0 || nextIndex >= snapshot.items.length) return;
      setViewerDirection(direction as -1 | 1);
      setViewerAnimationKey((current) => current + 1);
      setViewerIndex(nextIndex);
      setViewerZoom(1);
    };
    window.addEventListener("keydown", handleKeyDown);
    return () => window.removeEventListener("keydown", handleKeyDown);
  }, [snapshot.items.length, viewerIndex]);

  const updateUploadJob = (id: string, update: Partial<UploadJob>) => {
    setUploadJobs((current) => current.map((job) => (job.id === id ? { ...job, ...update } : job)));
  };

  const uploadSingleFile = (job: UploadJob) =>
    new Promise<void>((resolve) => {
      const request = new XMLHttpRequest();
      const formData = new FormData();
      formData.append("files", job.file, job.file.name);
      activeRequestsRef.current.add(request);
      request.open("POST", "/api/mailbox/files");
      request.responseType = "json";
      request.upload.addEventListener("progress", (event) => {
        if (!event.lengthComputable) return;
        const ratio = event.loaded / event.total;
        updateUploadJob(job.id, {
          progress: Math.min(95, Math.round(ratio * 95)),
          status: ratio >= 1 ? "saving" : "uploading",
        });
      });
      request.addEventListener("load", () => {
        activeRequestsRef.current.delete(request);
        const payload = request.response;
        if (request.status >= 200 && request.status < 300) {
          updateUploadJob(job.id, { progress: 100, status: "complete" });
        } else {
          updateUploadJob(job.id, {
            error: payload?.message || "업로드하지 못했습니다.",
            progress: 100,
            status: "error",
          });
        }
        resolve();
      });
      request.addEventListener("error", () => {
        activeRequestsRef.current.delete(request);
        updateUploadJob(job.id, { error: "네트워크 연결을 확인해주세요.", progress: 100, status: "error" });
        resolve();
      });
      request.addEventListener("abort", () => {
        activeRequestsRef.current.delete(request);
        updateUploadJob(job.id, { error: "업로드가 취소되었습니다.", progress: 100, status: "error" });
        resolve();
      });
      updateUploadJob(job.id, { status: "uploading" });
      request.send(formData);
    });

  const uploadFiles = async (files: File[]) => {
    const accepted = files.filter((file) => file.size > 0 && file.size <= MAX_FILE_BYTES);
    const rejected = files.filter((file) => file.size < 1 || file.size > MAX_FILE_BYTES);
    const now = Date.now();
    const jobs: UploadJob[] = [
      ...accepted.map((file, index) => ({
        error: null,
        file,
        id: `${now}-${index}-${file.name}`,
        progress: 0,
        status: "queued" as const,
      })),
      ...rejected.map((file, index) => ({
        error: file.size < 1 ? "비어 있는 파일입니다." : "파일 한 개는 최대 25MB까지 올릴 수 있습니다.",
        file,
        id: `${now}-rejected-${index}-${file.name}`,
        progress: 100,
        status: "error" as const,
      })),
    ];
    if (jobs.length === 0) return;

    setUploadJobs(jobs);
    setUploadModalOpen(true);
    setDragActive(false);
    for (const job of jobs) {
      if (job.status !== "error") await uploadSingleFile(job);
    }
    await loadFiles(1);
    router.refresh();
  };

  const toggleSelection = (itemId: string) => {
    setSelectedIds((current) => {
      const next = new Set(current);
      if (next.has(itemId)) next.delete(itemId);
      else next.add(itemId);
      return next;
    });
  };

  const openViewer = (itemId: string) => {
    const nextIndex = snapshot.items.findIndex((item) => item.id === itemId);
    if (nextIndex < 0) return;
    setViewerDirection(0);
    setViewerAnimationKey((current) => current + 1);
    setViewerZoom(1);
    setViewerIndex(nextIndex);
  };

  const moveViewer = (direction: -1 | 1) => {
    if (viewerIndex === null) return;
    const nextIndex = viewerIndex + direction;
    if (nextIndex < 0 || nextIndex >= snapshot.items.length) return;
    setViewerDirection(direction);
    setViewerAnimationKey((current) => current + 1);
    setViewerIndex(nextIndex);
    setViewerZoom(1);
  };

  const requestDelete = (mode: "all" | "selected") => {
    if (mode === "all") {
      const count = snapshot.manualFileCount + snapshot.mailAttachmentCount;
      if (count < 1) return;
      setDeleteConfirmation({ count, includesLargeAttachment: snapshot.mailAttachmentCount > 0, mode });
      return;
    }

    const selectedItems = snapshot.items.filter((item) => selectedIds.has(item.id) && item.direction !== "inbound");
    if (selectedItems.length < 1) return;
    setDeleteConfirmation({
      count: selectedItems.length,
      includesLargeAttachment: selectedItems.some((item) => item.id.startsWith("large_")),
      mode,
    });
  };

  const openShareDialog = (item: MailboxFileDrawerItem) => {
    setContextMenu(null);
    setShareItemId(item.id);
    setShareAccess(item.shareAccess);
    setShareUrl(item.shareEnabled ? item.shareUrl : null);
    setShareCopied(false);
  };

  const updateShare = async (enabled: boolean) => {
    if (!shareItemId || shareSaving) return;
    setShareSaving(true);
    try {
      const response = await fetch(`/api/mailbox/files/${encodeURIComponent(shareItemId)}/share`, {
        body: JSON.stringify({ access: shareAccess, enabled }),
        credentials: "same-origin",
        headers: { "Content-Type": "application/json" },
        method: "PATCH",
      });
      const payload = await response.json().catch(() => null);
      if (!response.ok || !payload?.share) {
        throw new Error(payload?.message || "공유 설정을 저장하지 못했습니다.");
      }
      const nextShare = payload.share as {
        access: "owner" | "public";
        enabled: boolean;
        url: string | null;
      };
      setShareAccess(nextShare.access);
      setShareUrl(nextShare.enabled ? nextShare.url : null);
      setShareCopied(false);
      setSnapshot((current) => ({
        ...current,
        items: current.items.map((item) => item.id === shareItemId
          ? {
              ...item,
              shareAccess: nextShare.access,
              shareEnabled: nextShare.enabled,
              shareUrl: nextShare.url,
            }
          : item),
      }));
    } catch (shareError) {
      setError(shareError instanceof Error ? shareError.message : "공유 설정을 저장하지 못했습니다.");
    } finally {
      setShareSaving(false);
    }
  };

  const copyShareUrl = async () => {
    if (!shareUrl) return;
    const absoluteUrl = new URL(shareUrl, window.location.origin).toString();
    try {
      await navigator.clipboard.writeText(absoluteUrl);
      setShareCopied(true);
    } catch {
      setError("공유 링크를 복사하지 못했습니다.");
    }
  };

  const confirmDelete = async () => {
    if (!deleteConfirmation || deleting) return;
    setDeleting(true);

    try {
      const response = await fetch("/api/mailbox/files", {
        body: JSON.stringify(
          deleteConfirmation.mode === "all"
            ? { all: true }
            : { itemIds: [...selectedIds] },
        ),
        credentials: "same-origin",
        headers: { "Content-Type": "application/json" },
        method: "DELETE",
      });
      const payload = await response.json().catch(() => null);
      if (!response.ok) throw new Error(payload?.message || "파일을 삭제하지 못했습니다.");

      setDeleteConfirmation(null);
      setSelectedIds(new Set());
      setViewerIndex(null);
      await loadFiles(1);
      router.refresh();
    } catch (deleteError) {
      setError(deleteError instanceof Error ? deleteError.message : "파일을 삭제하지 못했습니다.");
    } finally {
      setDeleting(false);
    }
  };

  const completedUploads = uploadJobs.filter((job) => job.status === "complete").length;
  const uploadFinished = uploadJobs.length > 0 && uploadJobs.every((job) => job.status === "complete" || job.status === "error");
  const overallProgress = uploadJobs.length > 0
    ? Math.round(uploadJobs.reduce((sum, job) => sum + job.progress, 0) / uploadJobs.length)
    : 0;
  const activeViewerItem = viewerIndex === null ? null : snapshot.items[viewerIndex] ?? null;
  const contextMenuItem = contextMenu
    ? snapshot.items.find((item) => item.id === contextMenu.itemId) ?? null
    : null;
  const shareItem = shareItemId
    ? snapshot.items.find((item) => item.id === shareItemId) ?? null
    : null;
  const isLargeAttachmentShare = shareItem?.id.startsWith("large_") ?? false;

  return (
    <div
      className="relative flex h-full min-h-0 flex-col overflow-hidden bg-[#f8faff]"
      onDragEnter={(event) => {
        if (event.dataTransfer.types.includes("Files")) {
          event.preventDefault();
          setDragActive(true);
        }
      }}
      onDragOver={(event) => {
        if (event.dataTransfer.types.includes("Files")) {
          event.preventDefault();
          event.dataTransfer.dropEffect = "copy";
        }
      }}
      onDragLeave={(event) => {
        if (!event.currentTarget.contains(event.relatedTarget as Node | null)) setDragActive(false);
      }}
      onDrop={(event) => {
        event.preventDefault();
        setDragActive(false);
        if (!event.dataTransfer.types.includes("Files")) return;

        const droppedFiles = Array.from(event.dataTransfer.files);
        if (droppedFiles.length === 0) return;
        void uploadFiles(droppedFiles);
      }}
    >
      <input
        ref={fileInputRef}
        type="file"
        multiple
        className="hidden"
        onChange={(event) => {
          void uploadFiles(Array.from(event.currentTarget.files ?? []));
          event.currentTarget.value = "";
        }}
      />

      <div className="shrink-0 border-b border-[#e9eef8] bg-white px-5 py-5 sm:px-8 lg:px-10 lg:py-7">
        <div>
          <div>
            <div className="flex flex-wrap items-center gap-x-3 gap-y-2">
              <div ref={helpRef} className="relative flex items-center gap-2">
                <h1 className="text-[24px] font-extrabold tracking-[-0.05em] text-[#15213a] sm:text-[28px]">파일서랍</h1>
                <button
                  type="button"
                  aria-label="파일서랍 도움말"
                  aria-expanded={helpOpen}
                  aria-describedby={helpOpen ? "file-drawer-help" : undefined}
                  onClick={() => setHelpOpen((current) => !current)}
                  className="inline-flex h-6 w-6 shrink-0 cursor-pointer items-center justify-center rounded-full border border-[#cbdaf5] bg-[#f5f8ff] text-[12px] font-bold text-[#5573ad] transition-colors hover:border-[#8fb2fb] hover:bg-[#edf4ff] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#2f6df6]"
                >
                  ?
                </button>
                {helpOpen ? (
                  <div id="file-drawer-help" role="tooltip" className="absolute left-0 top-[calc(100%+10px)] z-30 w-[min(320px,calc(100vw-40px))] rounded-[12px] border border-[#dce6f5] bg-white px-3.5 py-3 text-[12px] leading-5 text-[#526784] shadow-[0_12px_32px_rgba(27,48,83,0.14)]">
                    {FILE_DRAWER_HELP}
                  </div>
                ) : null}
              </div>
              <span className="rounded-full bg-[#edf4ff] px-2.5 py-1 text-[12px] font-bold text-[#2f6df6]">{snapshot.totalCount}</span>
              <div className="flex min-w-[210px] max-w-[360px] flex-1 items-center gap-2.5" aria-label={`파일 보관 용량 ${formatBytes(snapshot.quotaUsedBytes)} / ${formatBytes(snapshot.quotaBytes)}`}>
                <span className="shrink-0 text-[11px] font-semibold tabular-nums text-[#6f809d]">{formatBytes(snapshot.quotaUsedBytes)}</span>
                <div className="h-1.5 min-w-[72px] flex-1 overflow-hidden rounded-full bg-[#dce8ff]">
                  <div className="h-full rounded-full bg-[#2f6df6] transition-[width]" style={{ width: `${snapshot.quotaUsagePercent ?? 0}%` }} />
                </div>
                <span className="shrink-0 text-[11px] font-semibold tabular-nums text-[#6f809d]">{formatBytes(snapshot.quotaBytes)}</span>
              </div>
            </div>
          </div>
        </div>

        <div className="mt-4 flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
          <div className="flex min-w-0 flex-1 flex-col gap-2 sm:flex-row">
            <label className="flex h-11 min-h-11 min-w-0 shrink-0 flex-1 items-center gap-2 rounded-[14px] border border-[#dfe7f5] bg-[#f9fbff] px-4 text-[#8190a8] transition-colors focus-within:border-[#a9c3fb] focus-within:bg-white sm:max-w-[460px] sm:shrink">
              <SearchIcon />
              <input value={query} onChange={(event) => setQuery(event.currentTarget.value)} placeholder="파일명 검색" className="h-full min-w-0 flex-1 bg-transparent text-[16px] text-[#253653] outline-none placeholder:text-[#9aa7ba] sm:text-[13px]" />
            </label>
            <div className="inline-flex h-11 items-center rounded-[14px] border border-[#dfe7f5] bg-[#f9fbff] p-1">
              {([['all', '전체'], ['image', '이미지'], ['file', '파일']] as const).map(([value, label]) => (
                <button
                  key={value}
                  type="button"
                  aria-pressed={filter === value}
                  onClick={() => startTransition(() => setFilter(value))}
                  className={`h-9 cursor-pointer rounded-[10px] px-3.5 text-[12px] font-semibold transition-all ${filter === value ? "bg-white text-[#2f6df6] shadow-[0_4px_14px_rgba(31,55,99,0.09)]" : "text-[#71819a] hover:text-[#365981]"}`}
                >
                  {label}
                </button>
              ))}
            </div>
          </div>
          <button type="button" onClick={() => fileInputRef.current?.click()} className="inline-flex h-11 shrink-0 cursor-pointer items-center justify-center gap-2 rounded-[14px] bg-[#2f6df6] px-5 text-[13px] font-semibold text-white shadow-[0_12px_26px_rgba(47,109,246,0.22)] transition-all hover:-translate-y-0.5 hover:bg-[#245de3]">
            <UploadIcon /> 파일 올리기
          </button>
        </div>

      </div>

      <div ref={scrollContainerRef} className="min-h-0 flex-1 overflow-y-auto px-5 py-5 sm:px-8 lg:px-10 lg:py-8">
        {error ? (
          <div role="alert" className="mb-5 flex items-center justify-between gap-4 rounded-[14px] border border-[#ffd6d6] bg-[#fff7f7] px-4 py-3 text-[13px] text-[#c54848]">
            <span>{error}</span>
            <button type="button" onClick={() => setError(null)} className="font-semibold">닫기</button>
          </div>
        ) : null}

        <div className="mb-4 flex min-h-7 flex-wrap items-center justify-between gap-x-4 gap-y-1 px-0.5">
          <div className="text-[12px] font-medium text-[#7b89a0]">
            {selectedIds.size > 0 ? `${selectedIds.size.toLocaleString("ko-KR")}개 선택됨` : "파일을 눌러 선택하세요"}
          </div>
          <div className="flex items-center gap-4">
            <button
              type="button"
              disabled={snapshot.manualFileCount + snapshot.mailAttachmentCount < 1}
              onClick={() => requestDelete("all")}
              className="cursor-pointer text-[12px] font-semibold text-[#d05454] transition-colors hover:text-[#b53838] disabled:cursor-not-allowed disabled:text-[#b8c1cf]"
            >
              보관 파일 전체 삭제
            </button>
            <button
              type="button"
              disabled={selectedIds.size < 1}
              onClick={() => requestDelete("selected")}
              className="cursor-pointer text-[12px] font-semibold text-[#526784] transition-colors hover:text-[#2f6df6] disabled:cursor-not-allowed disabled:text-[#b8c1cf]"
            >
              선택 삭제
            </button>
          </div>
        </div>

        {loading ? (
          <div className="grid grid-cols-2 gap-x-3 gap-y-5 sm:grid-cols-4 lg:grid-cols-8 xl:grid-cols-10">
            {Array.from({ length: 20 }, (_, index) => <div key={index} className="aspect-[1.1] animate-pulse rounded-[10px] bg-[#eaf0f8]" />)}
          </div>
        ) : snapshot.items.length > 0 ? (
          <>
            <div className="grid grid-cols-2 gap-x-3 gap-y-5 sm:grid-cols-4 lg:grid-cols-8 xl:grid-cols-10">
              {snapshot.items.map((item) => (
                <article
                  key={item.id}
                  role={item.direction === "inbound" ? "button" : "checkbox"}
                  aria-checked={item.direction === "inbound" ? undefined : selectedIds.has(item.id)}
                  tabIndex={0}
                  draggable={false}
                  onClick={() => item.direction === "inbound" ? openViewer(item.id) : toggleSelection(item.id)}
                  onDoubleClick={() => item.direction !== "inbound" && openViewer(item.id)}
                  onContextMenu={(event) => {
                    event.preventDefault();
                    event.stopPropagation();
                    setContextMenu({
                      itemId: item.id,
                      x: Math.min(event.clientX, window.innerWidth - 216),
                      y: Math.min(event.clientY, window.innerHeight - 220),
                    });
                  }}
                  onKeyDown={(event) => {
                    if (event.key === "Enter") openViewer(item.id);
                    if (event.key === " " && item.direction === "inbound") {
                      event.preventDefault();
                      openViewer(item.id);
                    }
                    if (event.key === " " && item.direction !== "inbound") {
                      event.preventDefault();
                      toggleSelection(item.id);
                    }
                  }}
                  onDragStartCapture={(event) => event.preventDefault()}
                  className="group min-w-0 cursor-pointer select-none outline-none"
                >
                  <div className={`relative aspect-[1.15] overflow-hidden rounded-[10px] bg-[#edf2f9] transition-all duration-200 ${selectedIds.has(item.id) ? "ring-2 ring-[#2f6df6] ring-offset-2 ring-offset-[#f8faff]" : "group-hover:shadow-[0_10px_28px_rgba(31,48,82,0.12)] group-focus-visible:ring-2 group-focus-visible:ring-[#8bb0ff]"}`}>
                    <FileGlyph item={item} />
                    {item.direction !== "inbound" ? <button
                      type="button"
                      aria-label={`${item.name} ${selectedIds.has(item.id) ? "선택 해제" : "선택"}`}
                      aria-pressed={selectedIds.has(item.id)}
                      onClick={(event) => {
                        event.stopPropagation();
                        toggleSelection(item.id);
                      }}
                      className="absolute left-2 top-2 inline-flex h-7 w-7 cursor-pointer items-center justify-center"
                    >
                      <SelectionCheck selected={selectedIds.has(item.id)} />
                    </button> : null}
                  </div>
                  <div className="min-w-0 px-0.5 pt-2">
                    <p title={item.name} className="truncate text-[12px] font-semibold text-[#24334e]">{item.name}</p>
                    <div className="mt-1 flex items-center justify-between gap-1 text-[10px] font-medium text-[#8b98aa]">
                      <span className="truncate">{directionLabel(item)}</span>
                      <span className="shrink-0">{formatBytes(item.sizeBytes)}</span>
                    </div>
                  </div>
                </article>
              ))}
            </div>
            <div ref={loadMoreSentinelRef} aria-hidden="true" className="flex h-20 items-center justify-center">
              {loadingMore ? <span className="h-5 w-5 animate-spin rounded-full border-2 border-[#cddaf0] border-t-[#2f6df6]" /> : null}
            </div>
          </>
        ) : (
          <div className="flex min-h-[360px] flex-col items-center justify-center rounded-[24px] border border-dashed border-[#cedbf1] bg-white px-6 text-center">
            <div className="flex h-16 w-16 items-center justify-center rounded-[20px] bg-[#edf4ff] text-[#2f6df6]"><UploadIcon className="h-7 w-7" /></div>
            <p className="mt-5 text-[16px] font-bold text-[#283956]">아직 보관된 파일이 없습니다</p>
            <p className="mt-2 text-[13px] leading-6 text-[#7b8aa2]">파일을 끌어놓거나 파일 올리기 버튼으로<br />메일과 분리된 공간에 보관할 수 있습니다.</p>
            <button type="button" onClick={() => fileInputRef.current?.click()} className="mt-5 cursor-pointer text-[13px] font-semibold text-[#2f6df6]">첫 파일 올리기</button>
          </div>
        )}
      </div>

      {dragActive ? (
        <div className="pointer-events-none absolute inset-3 z-40 flex items-center justify-center rounded-[26px] border-2 border-dashed border-[#2f6df6] bg-[rgba(239,245,255,0.94)] backdrop-blur-sm">
          <div className="text-center text-[#2f6df6]"><UploadIcon className="mx-auto h-10 w-10" /><p className="mt-3 text-[17px] font-bold">파일을 놓아 업로드</p><p className="mt-1 text-[12px] text-[#6785b8]">여러 파일을 한 번에 올릴 수 있습니다.</p></div>
        </div>
      ) : null}

      {contextMenu && contextMenuItem ? (
        <div
          role="menu"
          aria-label={`${contextMenuItem.name} 파일 메뉴`}
          className="fixed z-[280] w-[200px] overflow-hidden rounded-[14px] border border-[#dfe7f3] bg-white p-1.5 shadow-[0_18px_46px_rgba(18,32,65,0.2)]"
          style={{ left: Math.max(8, contextMenu.x), top: Math.max(8, contextMenu.y) }}
          onClick={(event) => event.stopPropagation()}
        >
          <button type="button" role="menuitem" onClick={() => openViewer(contextMenuItem.id)} className="flex w-full cursor-pointer items-center rounded-[9px] px-3 py-2 text-left text-[13px] font-medium text-[#344767] hover:bg-[#f3f7ff] hover:text-[#2f6df6]">열기</button>
          <a
            role="menuitem"
            href={contextMenuItem.downloadUrl}
            download={contextMenuItem.id.startsWith("large_") ? undefined : true}
            target={contextMenuItem.id.startsWith("large_") ? "_blank" : undefined}
            rel={contextMenuItem.id.startsWith("large_") ? "noopener" : undefined}
            onClick={() => {
              if (contextMenuItem.id.startsWith("large_")) {
                setContextMenu(null);
                window.setTimeout(() => void loadFiles(1), 1000);
              }
            }}
            className="flex w-full cursor-pointer items-center rounded-[9px] px-3 py-2 text-[13px] font-medium text-[#344767] hover:bg-[#f3f7ff] hover:text-[#2f6df6]"
          >다운로드</a>
          {contextMenuItem.direction !== "inbound" ? <button type="button" role="menuitem" onClick={() => openShareDialog(contextMenuItem)} className="flex w-full cursor-pointer items-center rounded-[9px] px-3 py-2 text-left text-[13px] font-medium text-[#344767] hover:bg-[#f3f7ff] hover:text-[#2f6df6]">공유</button> : null}
          {contextMenuItem.direction !== "inbound" ? <div className="my-1 h-px bg-[#edf1f7]" /> : null}
          {contextMenuItem.direction !== "inbound" ? <button
            type="button"
            role="menuitem"
            onClick={() => {
              setContextMenu(null);
              setSelectedIds(new Set([contextMenuItem.id]));
              setDeleteConfirmation({ count: 1, includesLargeAttachment: contextMenuItem.id.startsWith("large_"), mode: "selected" });
            }}
            className="flex w-full cursor-pointer items-center rounded-[9px] px-3 py-2 text-left text-[13px] font-medium text-[#c74d4d] hover:bg-[#fff3f3]"
          >
            삭제
          </button> : null}
        </div>
      ) : null}

      {shareItem ? (
        <div className="fixed inset-0 z-[270] flex items-center justify-center bg-black/35 px-4 py-6 backdrop-blur-[3px]" onClick={() => !shareSaving && setShareItemId(null)}>
          <div role="dialog" aria-modal="true" aria-labelledby="file-share-title" className="w-full max-w-[500px] rounded-[18px] bg-white p-5 shadow-[0_28px_80px_rgba(15,23,42,0.28)] sm:p-6" onClick={(event) => event.stopPropagation()}>
            <div className="flex items-start justify-between gap-4">
              <div className="min-w-0">
                <h2 id="file-share-title" className="text-[18px] font-bold tracking-[-0.03em] text-[#1d2940]">파일 공유</h2>
                <p className="mt-1 truncate text-[12px] text-[#7a899f]">{shareItem.name}</p>
              </div>
              <button type="button" disabled={shareSaving} onClick={() => setShareItemId(null)} className="inline-flex h-9 w-9 cursor-pointer items-center justify-center rounded-[9px] text-[#71819a] hover:bg-[#f3f6fa] disabled:opacity-50" aria-label="공유 창 닫기"><CloseIcon /></button>
            </div>

            {!isLargeAttachmentShare ? <div className="mt-5 space-y-2">
              {([
                ["owner", "나만 접근", "링크를 열어도 이 파일의 소유자로 로그인해야 합니다."],
                ["public", "링크 공개", "로그인 없이 링크를 아는 누구나 파일을 열 수 있습니다."],
              ] as const).map(([value, label, description]) => (
                <label key={value} className={`flex cursor-pointer items-start gap-3 rounded-[13px] border px-4 py-3 transition-colors ${shareAccess === value ? "border-[#9ebcff] bg-[#f3f7ff]" : "border-[#e1e8f3] hover:border-[#c7d5eb]"}`}>
                  <input type="radio" name="file-share-access" value={value} checked={shareAccess === value} onChange={() => setShareAccess(value)} className="mt-0.5 h-4 w-4 cursor-pointer accent-[#2f6df6]" />
                  <span className="min-w-0"><span className="block text-[13px] font-semibold text-[#253653]">{label}</span><span className="mt-1 block text-[11px] leading-5 text-[#75859d]">{description}</span></span>
                </label>
              ))}
            </div> : null}

            {shareUrl ? (
              <div className="mt-4 rounded-[13px] bg-[#f6f8fc] p-3">
                <p className="text-[11px] font-semibold text-[#6d7d95]">공유 링크</p>
                <div className="mt-2 flex items-center gap-2">
                  <input readOnly value={new URL(shareUrl, window.location.origin).toString()} className="h-10 min-w-0 flex-1 rounded-[9px] border border-[#dce4f0] bg-white px-3 text-[12px] text-[#344767] outline-none" />
                  <button type="button" onClick={() => void copyShareUrl()} className="h-10 shrink-0 cursor-pointer rounded-[9px] bg-[#2f6df6] px-4 text-[12px] font-semibold text-white hover:bg-[#245de3]">{shareCopied ? "복사됨" : "복사"}</button>
                </div>
              </div>
            ) : null}

            <p className="mt-4 text-[11px] leading-5 text-[#8794a8]">{isLargeAttachmentShare
              ? "대용량 첨부 링크는 발송 후 30일 또는 다운로드 100회 중 먼저 도달하는 시점에 만료됩니다."
              : "공유를 해제하면 현재 링크는 즉시 폐기됩니다. 다시 공유할 때는 새로운 링크가 발급됩니다."}</p>
            {!isLargeAttachmentShare ? <div className="mt-5 flex flex-wrap justify-end gap-2">
              {shareUrl ? <button type="button" disabled={shareSaving} onClick={() => void updateShare(false)} className="h-10 cursor-pointer rounded-[9px] px-4 text-[13px] font-semibold text-[#c34d4d] hover:bg-[#fff3f3] disabled:opacity-50">공유 해제</button> : null}
              <button type="button" disabled={shareSaving} onClick={() => void updateShare(true)} className="h-10 cursor-pointer rounded-[9px] bg-[#2f6df6] px-4 text-[13px] font-semibold text-white hover:bg-[#245de3] disabled:opacity-60">
                {shareSaving ? "저장 중" : shareUrl ? "설정 저장 및 새 링크 발급" : "공유 링크 만들기"}
              </button>
            </div> : null}
          </div>
        </div>
      ) : null}

      {activeViewerItem ? (
        <div
          role="dialog"
          aria-modal="true"
          aria-label={`${activeViewerItem.name} 상세 보기`}
          className="fixed inset-0 z-[250] flex items-center justify-center bg-black/72 px-3 py-4 backdrop-blur-[2px] sm:px-16 sm:py-8"
          onClick={() => setViewerIndex(null)}
        >
          <button
            type="button"
            aria-label="상세 보기 닫기"
            onClick={() => setViewerIndex(null)}
            className="absolute right-4 top-4 z-20 inline-flex h-10 w-10 items-center justify-center text-white/80 transition-colors hover:text-white sm:right-7 sm:top-6"
          >
            <CloseIcon />
          </button>

          <button
            type="button"
            aria-label="이전 파일"
            disabled={viewerIndex === 0}
            onClick={(event) => {
              event.stopPropagation();
              moveViewer(-1);
            }}
            className="absolute left-0 top-1/2 z-20 -translate-y-1/2 text-white/70 transition-all hover:-translate-x-0.5 hover:text-white disabled:pointer-events-none disabled:opacity-20 sm:left-2"
          >
            <ChevronIcon direction="left" />
          </button>
          <button
            type="button"
            aria-label="다음 파일"
            disabled={viewerIndex === snapshot.items.length - 1}
            onClick={(event) => {
              event.stopPropagation();
              moveViewer(1);
            }}
            className="absolute right-0 top-1/2 z-20 -translate-y-1/2 text-white/70 transition-all hover:translate-x-0.5 hover:text-white disabled:pointer-events-none disabled:opacity-20 sm:right-2"
          >
            <ChevronIcon direction="right" />
          </button>

          <div
            className="flex h-[min(88vh,900px)] w-full max-w-[1240px] flex-col overflow-hidden rounded-[12px] bg-[#11151d] shadow-[0_30px_100px_rgba(0,0,0,0.55)]"
            onClick={(event) => event.stopPropagation()}
          >
            <div
              className="relative min-h-0 flex-1 touch-pan-y overflow-hidden bg-[#0a0d12]"
              onPointerDown={(event) => {
                viewerPointerStartRef.current = event.clientX;
              }}
              onPointerUp={(event) => {
                const start = viewerPointerStartRef.current;
                viewerPointerStartRef.current = null;
                if (start === null || Math.abs(event.clientX - start) < 52) return;
                moveViewer(event.clientX < start ? 1 : -1);
              }}
              onWheel={(event) => {
                if (!activeViewerItem.isImage) return;
                event.preventDefault();
                setViewerZoom((current) => Math.min(5, Math.max(1, Number((current + (event.deltaY < 0 ? 0.2 : -0.2)).toFixed(1)))));
              }}
            >
              <div
                key={`${activeViewerItem.id}-${viewerAnimationKey}`}
                className={viewerDirection < 0 ? "file-drawer-viewer-prev h-full" : viewerDirection > 0 ? "file-drawer-viewer-next h-full" : "file-drawer-viewer-open h-full"}
              >
                {activeViewerItem.previewUrl && activeViewerItem.isImage ? (
                  <div className="h-full w-full overflow-auto overscroll-contain">
                    <div
                      className="relative min-h-full min-w-full transition-[width,height] duration-150 ease-out"
                      style={{ height: `${viewerZoom * 100}%`, width: `${viewerZoom * 100}%` }}
                    >
                      <Image
                        unoptimized
                        src={activeViewerItem.previewUrl}
                        alt={activeViewerItem.name}
                        fill
                        draggable={false}
                        sizes="100vw"
                        className="object-contain"
                      />
                    </div>
                  </div>
                ) : activeViewerItem.previewUrl && activeViewerItem.mimeType.toLowerCase() === "application/pdf" ? (
                  <iframe title={activeViewerItem.name} src={activeViewerItem.previewUrl} className="h-full w-full border-0 bg-white" />
                ) : (
                  <div className="flex h-full flex-col items-center justify-center px-6 text-center">
                    <div className="relative flex h-28 w-24 items-center justify-center rounded-[16px] bg-white text-[17px] font-extrabold tracking-[0.08em] text-[#2f6df6] shadow-[0_18px_50px_rgba(0,0,0,0.25)]">
                      <span className="absolute right-0 top-0 h-7 w-7 rounded-bl-[10px] bg-[#edf4ff]" />
                      {extensionLabel(activeViewerItem.name)}
                    </div>
                    <p className="mt-5 text-[13px] text-white/55">이 파일 형식은 미리보기를 지원하지 않습니다.</p>
                  </div>
                )}
              </div>
            </div>

            <div className="flex shrink-0 items-center justify-between gap-4 border-t border-white/10 bg-[#151a23] px-4 py-3 text-white sm:px-5">
              <div className="min-w-0">
                <p className="truncate text-[13px] font-semibold">{activeViewerItem.name}</p>
                <p className="mt-1 truncate text-[11px] text-white/48">
                  {directionLabel(activeViewerItem)} · {formatBytes(activeViewerItem.sizeBytes)} · {formatDate(activeViewerItem.createdAt)}
                  {activeViewerItem.isImage && viewerZoom > 1 ? ` · ${Math.round(viewerZoom * 100)}%` : ""}
                </p>
              </div>
              <a
                href={activeViewerItem.downloadUrl}
                download={activeViewerItem.id.startsWith("large_") ? undefined : true}
                target={activeViewerItem.id.startsWith("large_") ? "_blank" : undefined}
                rel={activeViewerItem.id.startsWith("large_") ? "noopener" : undefined}
                onClick={() => {
                  if (activeViewerItem.id.startsWith("large_")) {
                    setViewerIndex(null);
                    window.setTimeout(() => void loadFiles(1), 1000);
                  }
                }}
                className="inline-flex h-9 shrink-0 items-center gap-2 rounded-[8px] bg-white/10 px-3 text-[12px] font-semibold text-white transition-colors hover:bg-white/16"
              >
                <DownloadIcon /> 다운로드
              </a>
            </div>
          </div>
        </div>
      ) : null}

      {deleteConfirmation ? (
        <div className="fixed inset-0 z-[260] flex items-center justify-center bg-black/35 px-4 py-6 backdrop-blur-[3px]" onClick={() => !deleting && setDeleteConfirmation(null)}>
          <div role="alertdialog" aria-modal="true" aria-labelledby="file-delete-title" className="w-full max-w-[430px] rounded-[16px] bg-white p-5 shadow-[0_28px_80px_rgba(15,23,42,0.28)] sm:p-6" onClick={(event) => event.stopPropagation()}>
            <div className="flex items-start gap-3">
              <span className="mt-0.5 inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-[10px] bg-[#fff0f0] text-[#d65050]"><TrashIcon /></span>
              <div className="min-w-0">
                <h2 id="file-delete-title" className="text-[17px] font-bold tracking-[-0.03em] text-[#1d2940]">
                  {deleteConfirmation.mode === "all" ? "보관 파일을 모두 삭제할까요?" : `${deleteConfirmation.count.toLocaleString("ko-KR")}개 파일을 삭제할까요?`}
                </h2>
                <p className="mt-2 text-[13px] leading-6 text-[#66758c]">
                  {deleteConfirmation.includesLargeAttachment
                    ? "보낸 대용량 첨부를 삭제하면 이미 보낸 메일의 다운로드 링크도 즉시 작동하지 않습니다. 삭제한 파일은 복구할 수 없습니다."
                    : "선택한 보관 파일을 삭제하면 기존 공유 링크도 작동하지 않습니다. 삭제한 파일은 복구할 수 없습니다."}
                </p>
              </div>
            </div>
            <div className="mt-6 flex justify-end gap-2">
              <button type="button" disabled={deleting} onClick={() => setDeleteConfirmation(null)} className="h-10 cursor-pointer rounded-[9px] px-4 text-[13px] font-semibold text-[#63728a] transition-colors hover:bg-[#f3f6fa] disabled:opacity-50">취소</button>
              <button type="button" disabled={deleting} onClick={() => void confirmDelete()} className="h-10 cursor-pointer rounded-[9px] bg-[#d94f4f] px-4 text-[13px] font-semibold text-white transition-colors hover:bg-[#c53d3d] disabled:opacity-60">
                {deleting ? "삭제 중" : deleteConfirmation.mode === "all" ? "보관 파일 전체 삭제" : "선택 파일 삭제"}
              </button>
            </div>
          </div>
        </div>
      ) : null}

      {uploadModalOpen ? (
        <div className="fixed inset-0 z-[240] flex items-center justify-center bg-[rgba(15,23,42,0.2)] px-4 py-6 backdrop-blur-[3px]">
          <div role="dialog" aria-modal="true" aria-labelledby="file-upload-title" className="flex max-h-[min(720px,90vh)] w-full max-w-[600px] flex-col overflow-hidden rounded-[22px] border border-[#dde6f4] bg-white shadow-[0_30px_80px_rgba(15,23,42,0.2)]">
            <div className="border-b border-[#edf1f7] px-5 py-5 sm:px-6">
              <div className="flex items-start justify-between gap-4">
                <div><h2 id="file-upload-title" className="text-[18px] font-bold tracking-[-0.03em] text-[#1f2d46]">파일 업로드</h2><p className="mt-1 text-[12px] text-[#7a899f]">NCP 스토리지에 안전하게 보관하고 있습니다.</p></div>
                {uploadFinished ? <button type="button" onClick={() => setUploadModalOpen(false)} className="rounded-[10px] px-3 py-2 text-[12px] font-semibold text-[#526784] hover:bg-[#f3f6fb]">닫기</button> : <span className="text-[12px] font-semibold text-[#2f6df6]">{completedUploads}/{uploadJobs.length}</span>}
              </div>
              <div className="mt-4 flex items-center gap-3"><div className="h-2 flex-1 overflow-hidden rounded-full bg-[#e9eef6]"><div className="h-full rounded-full bg-[linear-gradient(90deg,#2f6df6,#79a4ff)] transition-[width] duration-200" style={{ width: `${overallProgress}%` }} /></div><span className="w-10 text-right text-[12px] font-bold tabular-nums text-[#536987]">{overallProgress}%</span></div>
            </div>
            <div className="min-h-0 flex-1 space-y-2 overflow-y-auto px-5 py-4 sm:px-6">
              {uploadJobs.map((job) => (
                <div key={job.id} className="rounded-[14px] border border-[#e5ebf5] bg-[#fbfcff] px-4 py-3">
                  <div className="flex items-center justify-between gap-4"><div className="min-w-0"><p className="truncate text-[13px] font-semibold text-[#2c3a54]">{job.file.name}</p><p className={`mt-1 text-[11px] ${job.status === "error" ? "text-[#c54848]" : "text-[#8391a6]"}`}>{job.error || (job.status === "queued" ? "대기 중" : job.status === "uploading" ? "전송 중" : job.status === "saving" ? "NCP에 저장 중" : "업로드 완료")}</p></div><span className="shrink-0 text-[11px] font-semibold text-[#71839e]">{formatBytes(job.file.size)}</span></div>
                  <div className="mt-2.5 h-1.5 overflow-hidden rounded-full bg-[#e9eef5]"><div className={`h-full rounded-full transition-[width] duration-200 ${job.status === "error" ? "bg-[#e26868]" : job.status === "complete" ? "bg-[#39a66b]" : "bg-[#2f6df6]"}`} style={{ width: `${job.progress}%` }} /></div>
                </div>
              ))}
            </div>
          </div>
        </div>
      ) : null}
    </div>
  );
}
