"use client";

import { useRouter } from "next/navigation";
import {
  useEffect,
  useId,
  useLayoutEffect,
  useRef,
  useState,
  type FormEvent,
  type ReactNode,
} from "react";
import { submitMailboxMessageAction } from "@/app/actions/session";
import { CloseIconButton } from "@/components/close-icon-button";

type MailComposeFormProps = {
  mailboxAddress: string;
  composeMode: "forward" | "new" | "reply" | "replyAll";
  currentFolder: string;
  defaultBcc: string;
  defaultBody: string;
  defaultBodyHtml: string;
  defaultCc: string;
  defaultSubject: string;
  defaultTo: string;
};

type RecipientFieldProps = {
  autoFocus?: boolean;
  defaultValue: string;
  helperContent?: ReactNode;
  inputId: string;
  label: string;
  name: string;
  placeholder: string;
};

type ComposeRecipientToken = {
  email: string;
  label: string;
};

type ComposeRecipientSuggestion = {
  displayName: string | null;
  email: string;
  kind: "history" | "member";
  label: string;
};

type ComposeUploadKind = "attachment" | "inline-image";

type ComposeUploadItem = {
  contentType: string;
  filename: string;
  id: number;
  isImage: boolean;
  kind: ComposeUploadKind;
  previewUrl: string;
  sizeBytes: number;
  token: string;
};

type ComposeLocalAttachment = {
  contentType: string;
  file: File;
  filename: string;
  id: string;
  isImage: boolean;
  sizeBytes: number;
};

type ComposeUploadProgressState = {
  currentFileName: string;
  kind: ComposeUploadKind;
  percent: number;
  totalFiles: number;
};

type ComposeSendUploadStatus = "complete" | "uploading" | "waiting";

type ComposeSendProgressItem = {
  filename: string;
  id: number | string;
  kind: ComposeUploadKind;
  sizeBytes: number;
  status: ComposeSendUploadStatus;
  uploadedBytes: number;
};

type ComposeSendProgressSnapshot = {
  completedBytes: number;
  items: ComposeSendProgressItem[];
  totalBytes: number;
  totalCount: number;
  uploadedCount: number;
};

type ComposeSendModalState =
  | {
      snapshot: ComposeSendProgressSnapshot;
      stage: "complete" | "sending" | "uploading";
    }
  | {
      errorMessage: string;
      snapshot: ComposeSendProgressSnapshot;
      stage: "error";
    };

type QuillRange = {
  index: number;
  length: number;
};

type QuillLikeInstance = {
  clipboard: {
    dangerouslyPasteHTML: (value: string) => void;
  };
  format?: (name: string, value: unknown, source?: string) => void;
  focus: () => void;
  getFormat?: (range?: QuillRange) => Record<string, unknown>;
  getSelection?: (focus?: boolean) => QuillRange | null;
  getText: () => string;
  insertEmbed?: (index: number, type: string, value: string, source?: string) => void;
  insertText?: (index: number, text: string, source?: string) => void;
  off?: (eventName: string, handler: (...args: unknown[]) => void) => void;
  on: (eventName: string, handler: (...args: unknown[]) => void) => void;
  root: HTMLDivElement;
  setSelection?: (index: number, length: number, source?: string) => void;
};

type QuillNamespace = {
  import: (path: string) => { whitelist?: string[] | null } | undefined;
  register: (definition: unknown, suppressWarning?: boolean) => void;
  new (element: Element, options: unknown): QuillLikeInstance;
};

function splitRecipientChunks(value: string) {
  return value
    .split(/[,;\n]+/)
    .map((item) => item.trim())
    .filter(Boolean);
}

function extractRecipientEmail(value: string) {
  const trimmed = value.trim();

  if (!trimmed) {
    return "";
  }

  const bracketMatch = trimmed.match(/<([^<>]+)>$/);

  if (bracketMatch) {
    return bracketMatch[1].trim().toLowerCase();
  }

  return trimmed.toLowerCase();
}

function buildRecipientLabel(email: string, displayName?: string | null) {
  const normalizedName = displayName?.trim() ?? "";
  return normalizedName ? `${normalizedName} <${email}>` : email;
}

function createRecipientToken(
  value: string,
  input?: {
    displayName?: string | null;
    label?: string;
  },
) {
  const email = extractRecipientEmail(value);

  if (!email) {
    return null;
  }

  return {
    email,
    label: input?.label?.trim() || buildRecipientLabel(email, input?.displayName),
  } satisfies ComposeRecipientToken;
}

function splitRecipientTokens(value: string) {
  return splitRecipientChunks(value).flatMap((item) => {
    const token = createRecipientToken(item, {
      label: item,
    });

    return token ? [token] : [];
  });
}

function splitRecipients(value: string) {
  return splitRecipientTokens(value).map((token) => token.email);
}

function mergeRecipientTokens(current: ComposeRecipientToken[], incoming: ComposeRecipientToken[]) {
  const seen = new Set(current.map((token) => token.email));
  const next = [...current];

  incoming.forEach((recipient) => {
    if (!seen.has(recipient.email)) {
      seen.add(recipient.email);
      next.push(recipient);
    }
  });

  return next;
}

function mergeRecipients(current: string[], incoming: string[]) {
  const seen = new Set(current);
  const next = [...current];

  incoming.forEach((recipient) => {
    if (!seen.has(recipient)) {
      seen.add(recipient);
      next.push(recipient);
    }
  });

  return next;
}

function serializeRecipientEmails(tokens: ComposeRecipientToken[], draft: string) {
  return mergeRecipientTokens(tokens, splitRecipientTokens(draft))
    .map((token) => token.email)
    .join(", ");
}

function serializeRecipients(tokens: string[], draft: string) {
  return mergeRecipients(tokens, splitRecipients(draft)).join(", ");
}

function escapeHtml(value: string) {
  return value
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&#39;");
}

function plainTextToHtml(value: string) {
  const lines = value.replace(/\r/g, "").split("\n");

  if (lines.length === 0) {
    return "<p><br></p>";
  }

  return lines
    .map((line) => (line.trim() ? `<p>${escapeHtml(line)}</p>` : "<p><br></p>"))
    .join("");
}

function ensureLeadingComposeSpaceText(value: string) {
  if (!value.trim()) {
    return "";
  }

  return value.startsWith("\n") ? value : `\n${value}`;
}

function ensureLeadingComposeSpaceHtml(value: string) {
  const normalized = value.trim();

  if (!normalized) {
    return "";
  }

  return normalized.startsWith("<p><br></p>") ? normalized : `<p><br></p>${normalized}`;
}

function normalizeEditorHtml(value: string) {
  const normalized = value.trim();
  return normalized === "<p><br></p>" ? "" : normalized;
}

function normalizeEditorText(value: string) {
  return value
    .replace(/\r/g, "")
    .replace(/\u00a0/g, " ")
    .replace(/\n{3,}/g, "\n\n")
    .trim();
}

function hasVisibleEditorContent(html: string, text: string) {
  if (normalizeEditorText(text).length > 0) {
    return true;
  }

  return /<(img|video|iframe|svg|object|embed)\b/i.test(html);
}

function formatUploadSize(sizeBytes: number) {
  if (sizeBytes < 1024) {
    return `${sizeBytes} B`;
  }

  if (sizeBytes < 1024 * 1024) {
    return `${(sizeBytes / 1024).toFixed(sizeBytes < 10 * 1024 ? 1 : 0)} KB`;
  }

  return `${(sizeBytes / (1024 * 1024)).toFixed(sizeBytes < 10 * 1024 * 1024 ? 1 : 0)} MB`;
}

function mergeUploads(current: ComposeUploadItem[], incoming: ComposeUploadItem[]) {
  const map = new Map(current.map((upload) => [upload.id, upload]));

  incoming.forEach((upload) => {
    map.set(upload.id, upload);
  });

  return [...map.values()];
}

function createLocalAttachmentId() {
  if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
    return crypto.randomUUID();
  }

  return `attachment-${Date.now()}-${Math.random().toString(16).slice(2)}`;
}

function toLocalAttachment(file: File): ComposeLocalAttachment {
  const isImage = file.type.startsWith("image/");

  return {
    contentType: file.type || "application/octet-stream",
    file,
    filename: file.name || "attachment",
    id: createLocalAttachmentId(),
    isImage,
    sizeBytes: file.size,
  };
}

function mergeLocalAttachments(
  current: ComposeLocalAttachment[],
  incoming: ComposeLocalAttachment[],
) {
  if (incoming.length === 0) {
    return current;
  }

  return [...current, ...incoming];
}

function clampProgressRatio(value: number) {
  if (!Number.isFinite(value)) {
    return 0;
  }

  return Math.min(1, Math.max(0, value));
}

function buildComposeUploadProgressState(
  kind: ComposeUploadKind,
  files: File[],
  progressRatio: number,
): ComposeUploadProgressState {
  const safeFiles = files.filter((file) => file.size >= 0);
  const totalBytes = safeFiles.reduce((sum, file) => sum + file.size, 0);
  const normalizedRatio = clampProgressRatio(progressRatio);
  const loadedBytes = totalBytes * normalizedRatio;
  const fallbackName = safeFiles[safeFiles.length - 1]?.name || "파일 업로드 준비 중";
  let cumulativeBytes = 0;
  let currentFileName = fallbackName;

  for (const file of safeFiles) {
    cumulativeBytes += file.size;

    if (loadedBytes < cumulativeBytes || file === safeFiles[safeFiles.length - 1]) {
      currentFileName = file.name || fallbackName;
      break;
    }
  }

  return {
    currentFileName,
    kind,
    percent: Math.round(normalizedRatio * 100),
    totalFiles: Math.max(1, safeFiles.length),
  };
}

function parseComposeUploadResponse(xhr: XMLHttpRequest) {
  const response = xhr.response;

  if (response && typeof response === "object") {
    return response as { error?: string; uploads?: ComposeUploadItem[] };
  }

  if (typeof xhr.responseText === "string" && xhr.responseText.trim()) {
    try {
      return JSON.parse(xhr.responseText) as { error?: string; uploads?: ComposeUploadItem[] };
    } catch {
      return null;
    }
  }

  return null;
}

function getComposeUploadProgressTitle(kind: ComposeUploadKind) {
  return kind === "inline-image" ? "본문 이미지 업로드 중" : "첨부파일 업로드 중";
}

function createEmptyComposeSendProgressSnapshot(): ComposeSendProgressSnapshot {
  return {
    completedBytes: 0,
    items: [],
    totalBytes: 0,
    totalCount: 0,
    uploadedCount: 0,
  };
}

function buildComposeSendSnapshotFromLocalAttachments(
  attachments: ComposeLocalAttachment[],
): ComposeSendProgressSnapshot {
  const items = attachments.map((attachment) => ({
    filename: attachment.filename,
    id: attachment.id,
    kind: "attachment" as const,
    sizeBytes: attachment.sizeBytes,
    status: "waiting" as const,
    uploadedBytes: 0,
  }));

  return {
    completedBytes: 0,
    items,
    totalBytes: items.reduce((sum, item) => sum + item.sizeBytes, 0),
    totalCount: items.length,
    uploadedCount: 0,
  };
}

function applyUploadedBytesToComposeSendSnapshot(
  snapshot: ComposeSendProgressSnapshot,
  uploadedBytesInput: number,
): ComposeSendProgressSnapshot {
  const uploadedBytes = Math.min(
    snapshot.totalBytes,
    Math.max(0, Math.floor(uploadedBytesInput)),
  );
  let remainingBytes = uploadedBytes;

  const items = snapshot.items.map((item) => {
    if (remainingBytes <= 0) {
      return {
        ...item,
        status: "waiting" as const,
        uploadedBytes: 0,
      };
    }

    if (remainingBytes >= item.sizeBytes) {
      remainingBytes -= item.sizeBytes;
      return {
        ...item,
        status: "complete" as const,
        uploadedBytes: item.sizeBytes,
      };
    }

    const currentUploadedBytes = remainingBytes;
    remainingBytes = 0;

    return {
      ...item,
      status: "uploading" as const,
      uploadedBytes: currentUploadedBytes,
    };
  });

  return {
    completedBytes: uploadedBytes,
    items,
    totalBytes: snapshot.totalBytes,
    totalCount: snapshot.totalCount,
    uploadedCount: items.filter((item) => item.status === "complete").length,
  };
}

function getComposeSendUploadStatusLabel(status: ComposeSendUploadStatus) {
  if (status === "complete") {
    return "완료";
  }

  if (status === "uploading") {
    return "업로드 중";
  }

  return "대기";
}

function getComposeSendUploadStatusTone(status: ComposeSendUploadStatus) {
  if (status === "complete") {
    return "text-[#17a34a]";
  }

  if (status === "uploading") {
    return "text-[#2f6df6]";
  }

  return "text-[#7f8ea7]";
}

function buildComposeHeader(
  composeMode: MailComposeFormProps["composeMode"],
  mailboxAddress: string,
) {
  if (composeMode === "reply") {
    return {
      description: `받는 사람과 제목이 자동으로 채워졌습니다. ${mailboxAddress} 계정으로 답장을 보냅니다.`,
      title: "답장 작성",
    };
  }

  if (composeMode === "replyAll") {
    return {
      description: `받는 사람, 참조, 제목이 자동으로 채워졌습니다. ${mailboxAddress} 계정으로 전체 답장을 보냅니다.`,
      title: "전체 답장 작성",
    };
  }

  if (composeMode === "forward") {
    return {
      description: `제목과 인용 본문이 자동으로 채워졌습니다. ${mailboxAddress} 계정으로 전달 메일을 보냅니다.`,
      title: "전달 메일 작성",
    };
  }

  return {
    description: `${mailboxAddress} 계정으로 메일을 보냅니다.`,
    title: "새 메일 작성",
  };
}

function extractFontSizeInputValue(value: unknown) {
  if (typeof value !== "string") {
    return "15";
  }

  const match = value.match(/\d+/);
  return match?.[0] ?? "15";
}

function normalizeToolbarColorValue(value: unknown, fallback: string) {
  if (typeof value !== "string") {
    return fallback;
  }

  const normalized = value.trim();

  if (/^#[0-9a-f]{6}$/i.test(normalized)) {
    return normalized.toLowerCase();
  }

  return fallback;
}

function normalizeFontSizeValue(value: string) {
  const normalized = value.trim().toLowerCase().replace(/px$/, "");
  const numeric = Number(normalized);

  if (!Number.isFinite(numeric)) {
    return null;
  }

  const clamped = Math.max(10, Math.min(96, Math.round(numeric)));
  return `${clamped}px`;
}

const composeActionGroupClassName =
  "col-start-2 row-start-1 inline-flex shrink-0 items-center justify-end gap-2 self-start whitespace-nowrap";

const composePrimaryActionButtonClassName =
  "inline-flex h-10 shrink-0 items-center justify-center rounded-[14px] bg-[linear-gradient(135deg,#2f6df6,#245de3)] px-4 text-[13px] font-semibold text-white shadow-[0_18px_40px_rgba(47,109,246,0.22)] transition-all duration-200 hover:-translate-y-0.5 hover:shadow-[0_24px_50px_rgba(47,109,246,0.28)] disabled:translate-y-0 disabled:opacity-60 disabled:shadow-[0_18px_40px_rgba(47,109,246,0.16)] sm:min-w-[112px]";

const composeSecondaryActionButtonClassName =
  "inline-flex h-10 shrink-0 items-center justify-center rounded-[14px] border border-[#d7e3fb] bg-[linear-gradient(180deg,#ffffff_0%,#f6faff_100%)] px-4 text-[13px] font-semibold text-[#344767] shadow-[0_10px_24px_rgba(25,38,72,0.06)] transition-all duration-200 hover:-translate-y-0.5 hover:border-[#bfd4ff] hover:bg-[linear-gradient(180deg,#ffffff_0%,#f0f6ff_100%)] hover:text-[#245de3] hover:shadow-[0_16px_34px_rgba(25,38,72,0.08)] disabled:translate-y-0 disabled:cursor-not-allowed disabled:opacity-60 disabled:shadow-[0_10px_24px_rgba(25,38,72,0.04)] sm:min-w-[112px]";

const defaultComposeTextColor = "#223250";
const defaultComposeBackgroundColor = "#fff2b3";

function AttachmentIcon({ isImage }: { isImage: boolean }) {
  if (isImage) {
    return (
      <svg viewBox="0 0 24 24" fill="none" className="h-[18px] w-[18px]">
        <path
          d="M5 6.5A1.5 1.5 0 0 1 6.5 5h11A1.5 1.5 0 0 1 19 6.5v11a1.5 1.5 0 0 1-1.5 1.5h-11A1.5 1.5 0 0 1 5 17.5Z"
          stroke="currentColor"
          strokeWidth="1.8"
        />
        <path d="M8 15.5 10.8 12.7 13.2 15.1 15.6 12.7 18 15.1" stroke="currentColor" strokeWidth="1.8" />
        <circle cx="9" cy="9" r="1.2" fill="currentColor" />
      </svg>
    );
  }

  return (
    <svg viewBox="0 0 24 24" fill="none" className="h-[18px] w-[18px]">
      <path
        d="M7 4.8h7l3 3V19a1.8 1.8 0 0 1-1.8 1.8H7A1.8 1.8 0 0 1 5.2 19V6.6A1.8 1.8 0 0 1 7 4.8Z"
        stroke="currentColor"
        strokeWidth="1.8"
      />
      <path d="M14 4.8v3.5h3.5" stroke="currentColor" strokeWidth="1.8" />
      <path d="M8.5 12h7" stroke="currentColor" strokeWidth="1.8" />
      <path d="M8.5 15.5h5.5" stroke="currentColor" strokeWidth="1.8" />
    </svg>
  );
}

function RecipientField({
  autoFocus = false,
  defaultValue,
  inputId,
  label,
  name,
  placeholder,
}: RecipientFieldProps) {
  const [tokens, setTokens] = useState(() => splitRecipients(defaultValue));
  const [draft, setDraft] = useState("");
  const inputRef = useRef<HTMLInputElement | null>(null);

  const focusInput = () => {
    window.requestAnimationFrame(() => {
      inputRef.current?.focus();
      const valueLength = inputRef.current?.value.length ?? 0;
      inputRef.current?.setSelectionRange(valueLength, valueLength);
    });
  };

  const commitDraft = () => {
    if (!draft.trim()) {
      return;
    }

    setTokens((current) => mergeRecipients(current, splitRecipients(draft)));
    setDraft("");
  };

  const editToken = (index: number) => {
    setTokens((current) => {
      const next = [...current];
      const [removed] = next.splice(index, 1);
      setDraft(removed ?? "");
      return next;
    });
    focusInput();
  };

  const removeToken = (index: number) => {
    setTokens((current) => current.filter((_, tokenIndex) => tokenIndex !== index));
    focusInput();
  };

  return (
    <div className="grid grid-cols-[88px_minmax(0,1fr)] items-start gap-3">
      <label htmlFor={inputId} className="pt-2 text-[13px] font-medium text-[#6f809d]">
        {label}
      </label>
      <div
        onClick={() => inputRef.current?.focus()}
        className="flex min-h-10 flex-wrap items-center gap-1.5 rounded-[10px] border border-[#e2eaf8] bg-[#fbfcff] px-3 py-0.5 transition-colors hover:border-[#cfdbf4] focus-within:border-[#cbdcff] focus-within:bg-white"
      >
        {tokens.map((token, index) => (
          <div
            key={`${token}-${index}`}
            className="group flex h-8 items-center rounded-[10px] border border-[#d8e4fb] bg-white px-2.5 text-[12px] text-[#28405f] shadow-[0_8px_18px_rgba(25,38,72,0.05)] transition-all duration-200 hover:-translate-y-px hover:border-[#bdd1ff] hover:text-[#2f6df6] hover:shadow-[0_14px_24px_rgba(47,109,246,0.1)]"
          >
            <button
              type="button"
              onClick={() => editToken(index)}
              className="max-w-[320px] cursor-pointer truncate pr-2 text-left"
            >
              {token}
            </button>
            <button
              type="button"
              aria-label={`${token} 제거`}
              onClick={(event) => {
                event.stopPropagation();
                removeToken(index);
              }}
              className="inline-flex h-5 w-5 cursor-pointer items-center justify-center rounded-[10px] text-[#98a7be] transition-colors hover:bg-[#eef4ff] hover:text-[#2f6df6]"
            >
              ×
            </button>
          </div>
        ))}

        <input
          id={inputId}
          ref={inputRef}
          type="text"
          autoFocus={autoFocus}
          autoComplete="off"
          value={draft}
          placeholder={tokens.length === 0 ? placeholder : ""}
          onBlur={commitDraft}
          onChange={(event) => {
            const nextValue = event.target.value;

            if (/[,\n;]/.test(nextValue)) {
              setTokens((current) => mergeRecipients(current, splitRecipients(nextValue)));
              setDraft("");
              return;
            }

            setDraft(nextValue);
          }}
          onKeyDown={(event) => {
            if (event.key === "Enter") {
              event.preventDefault();
              commitDraft();
              return;
            }

            if (event.key === "Backspace" && !draft && tokens.length > 0) {
              event.preventDefault();
              const lastToken = tokens[tokens.length - 1] ?? "";
              setTokens((current) => current.slice(0, -1));
              setDraft(lastToken.slice(0, -1));
              focusInput();
            }
          }}
          className="h-8 min-w-[160px] flex-1 rounded-[10px] border-0 bg-transparent px-1 text-[13px] text-[#1e2a43] outline-none placeholder:text-[#a0adc2]"
        />
        <input type="hidden" name={name} value={serializeRecipients(tokens, draft)} />
      </div>
    </div>
  );
}

function RecipientAutocompleteField({
  autoFocus = false,
  defaultValue,
  helperContent,
  inputId,
  label,
  name,
  placeholder,
}: RecipientFieldProps) {
  const suggestionListId = useId().replace(/:/g, "");
  const [tokens, setTokens] = useState(() => splitRecipientTokens(defaultValue));
  const [draft, setDraft] = useState("");
  const [suggestions, setSuggestions] = useState<ComposeRecipientSuggestion[]>([]);
  const [suggestionsOpen, setSuggestionsOpen] = useState(false);
  const [highlightedIndex, setHighlightedIndex] = useState(-1);
  const [isLoadingSuggestions, setIsLoadingSuggestions] = useState(false);
  const inputRef = useRef<HTMLInputElement | null>(null);
  const abortRef = useRef<AbortController | null>(null);

  const focusInput = () => {
    window.requestAnimationFrame(() => {
      inputRef.current?.focus();
      const valueLength = inputRef.current?.value.length ?? 0;
      inputRef.current?.setSelectionRange(valueLength, valueLength);
    });
  };

  const commitDraft = () => {
    if (!draft.trim()) {
      setSuggestions([]);
      setSuggestionsOpen(false);
      setHighlightedIndex(-1);
      return;
    }

    setTokens((current) => mergeRecipientTokens(current, splitRecipientTokens(draft)));
    setDraft("");
    setSuggestions([]);
    setSuggestionsOpen(false);
    setHighlightedIndex(-1);
  };

  const selectSuggestion = (suggestion: ComposeRecipientSuggestion) => {
    const token = createRecipientToken(suggestion.email, {
      displayName: suggestion.displayName,
      label: suggestion.label,
    });

    if (!token) {
      return;
    }

    setTokens((current) => mergeRecipientTokens(current, [token]));
    setDraft("");
    setSuggestions([]);
    setSuggestionsOpen(false);
    setHighlightedIndex(-1);
    focusInput();
  };

  const editToken = (index: number) => {
    setTokens((current) => {
      const next = [...current];
      const [removed] = next.splice(index, 1);
      setDraft(removed?.email ?? "");
      return next;
    });
    focusInput();
  };

  const removeToken = (index: number) => {
    setTokens((current) => current.filter((_, tokenIndex) => tokenIndex !== index));
    focusInput();
  };

  useEffect(() => {
    const normalizedDraft = draft.trim();

    abortRef.current?.abort();
    abortRef.current = null;

    if (!normalizedDraft || /[,;\n]/.test(draft)) {
      setIsLoadingSuggestions(false);
      setSuggestions([]);
      setSuggestionsOpen(false);
      setHighlightedIndex(-1);
      return;
    }

    const timeoutId = window.setTimeout(async () => {
      const controller = new AbortController();
      abortRef.current = controller;
      setIsLoadingSuggestions(true);

      try {
        const response = await fetch(
          `/api/mailbox/recipients?${new URLSearchParams({ q: normalizedDraft }).toString()}`,
          {
            credentials: "same-origin",
            signal: controller.signal,
          },
        );

        if (!response.ok) {
          throw new Error("recipient-suggestions-failed");
        }

        const payload = (await response.json()) as {
          suggestions?: ComposeRecipientSuggestion[];
        };
        const nextSuggestions = Array.isArray(payload.suggestions)
          ? payload.suggestions.filter(
              (suggestion) => !tokens.some((token) => token.email === suggestion.email),
            )
          : [];

        setSuggestions(nextSuggestions);
        setSuggestionsOpen(nextSuggestions.length > 0);
        setHighlightedIndex(nextSuggestions.length > 0 ? 0 : -1);
      } catch (error) {
        if (error instanceof DOMException && error.name === "AbortError") {
          return;
        }

        setSuggestions([]);
        setSuggestionsOpen(false);
        setHighlightedIndex(-1);
      } finally {
        if (abortRef.current === controller) {
          abortRef.current = null;
        }

        setIsLoadingSuggestions(false);
      }
    }, 140);

    return () => {
      window.clearTimeout(timeoutId);
      abortRef.current?.abort();
      abortRef.current = null;
    };
  }, [draft, tokens]);

  const showSuggestionPanel = Boolean(draft.trim()) && (isLoadingSuggestions || suggestionsOpen);
  const effectiveHelperContent =
    name === "to" ? (
      <label className="flex cursor-pointer flex-col gap-1.5 py-1">
        <span className="flex items-center gap-2 text-[13px] font-medium text-[#344767]">
          <input
            type="checkbox"
            name="sendIndividually"
            value="1"
            className="h-4 w-4 rounded-[4px] border-[#cfd7e4] text-[#2f6df6] focus:ring-2 focus:ring-[#d7e5ff] focus:ring-offset-0"
          />
          <span>개별 발송</span>
        </span>
        <span className="pl-6 text-[12px] leading-5 text-[#7d8da8]">
          받는사람이 여러 명이면 각 수신자에게 별도 메일로 발송됩니다. 참조와 숨은참조는 같은 메일에
          함께 적용됩니다.
        </span>
      </label>
    ) : (
      helperContent
    );

  return (
    <div className="grid grid-cols-[88px_minmax(0,1fr)] items-start gap-3">
      <label htmlFor={inputId} className="pt-2 text-[13px] font-medium text-[#6f809d]">
        {label}
      </label>
      <div className="relative">
        <div
          onClick={() => inputRef.current?.focus()}
          className="flex min-h-10 flex-wrap items-center gap-1.5 rounded-[10px] border border-[#e2eaf8] bg-[#fbfcff] px-3 py-0.5 transition-colors hover:border-[#cfdbf4] focus-within:border-[#cbdcff] focus-within:bg-white"
        >
          {tokens.map((token, index) => (
            <div
              key={token.email}
              className="group flex h-8 items-center rounded-[10px] border border-[#d8e4fb] bg-white px-2.5 text-[12px] text-[#28405f] shadow-[0_8px_18px_rgba(25,38,72,0.05)] transition-all duration-200 hover:-translate-y-px hover:border-[#bdd1ff] hover:text-[#2f6df6] hover:shadow-[0_14px_24px_rgba(47,109,246,0.1)]"
            >
              <button
                type="button"
                onClick={() => editToken(index)}
                className="max-w-[320px] cursor-pointer truncate pr-2 text-left"
              >
                {token.label}
              </button>
              <button
                type="button"
                aria-label={`${token.label} 제거`}
                onClick={(event) => {
                  event.stopPropagation();
                  removeToken(index);
                }}
                className="inline-flex h-5 w-5 cursor-pointer items-center justify-center rounded-[10px] text-[#98a7be] transition-colors hover:bg-[#eef4ff] hover:text-[#2f6df6]"
              >
                ×
              </button>
            </div>
          ))}

          <input
            id={inputId}
            ref={inputRef}
            type="text"
            autoFocus={autoFocus}
            autoComplete="off"
            value={draft}
            placeholder={tokens.length === 0 ? placeholder : ""}
            aria-autocomplete="list"
            aria-controls={showSuggestionPanel ? suggestionListId : undefined}
            aria-expanded={showSuggestionPanel}
            onBlur={commitDraft}
            onFocus={() => {
              if (suggestions.length > 0) {
                setSuggestionsOpen(true);
              }
            }}
            onChange={(event) => {
              const nextValue = event.target.value;

              if (/[,\n;]/.test(nextValue)) {
                setTokens((current) => mergeRecipientTokens(current, splitRecipientTokens(nextValue)));
                setDraft("");
                setSuggestions([]);
                setSuggestionsOpen(false);
                setHighlightedIndex(-1);
                return;
              }

              setDraft(nextValue);
            }}
            onKeyDown={(event) => {
              if ((event.key === "ArrowDown" || event.key === "ArrowUp") && suggestions.length > 0) {
                event.preventDefault();
                setSuggestionsOpen(true);
                setHighlightedIndex((current) => {
                  if (event.key === "ArrowDown") {
                    return current < suggestions.length - 1 ? current + 1 : 0;
                  }

                  return current > 0 ? current - 1 : suggestions.length - 1;
                });
                return;
              }

              if (event.key === "Escape" && suggestionsOpen) {
                event.preventDefault();
                setSuggestionsOpen(false);
                setHighlightedIndex(-1);
                return;
              }

              if (event.key === "Enter") {
                event.preventDefault();

                if (suggestionsOpen && highlightedIndex >= 0) {
                  const suggestion = suggestions[highlightedIndex];

                  if (suggestion) {
                    selectSuggestion(suggestion);
                    return;
                  }
                }

                commitDraft();
                return;
              }

              if (event.key === "Backspace" && !draft && tokens.length > 0) {
                event.preventDefault();
                const lastToken = tokens[tokens.length - 1];
                setTokens((current) => current.slice(0, -1));
                setDraft(lastToken?.email.slice(0, -1) ?? "");
                focusInput();
              }
            }}
            className="h-8 min-w-[160px] flex-1 rounded-[10px] border-0 bg-transparent px-1 text-[13px] text-[#1e2a43] outline-none placeholder:text-[#a0adc2]"
          />
          <input type="hidden" name={name} value={serializeRecipientEmails(tokens, draft)} />
        </div>
        {effectiveHelperContent ? <div className="mt-1">{effectiveHelperContent}</div> : null}
        {showSuggestionPanel ? (
          <div
            id={suggestionListId}
            role="listbox"
            className="absolute left-0 right-0 top-[calc(100%+8px)] z-20 overflow-hidden rounded-[14px] border border-[#dfe8f7] bg-[rgba(255,255,255,0.98)] p-1.5 shadow-[0_20px_40px_rgba(20,34,64,0.12)] backdrop-blur-[18px]"
          >
            {isLoadingSuggestions ? (
              <div className="px-3 py-2 text-[12px] text-[#72839f]">주소를 찾는 중입니다.</div>
            ) : (
              suggestions.map((suggestion, index) => {
                const isActive = index === highlightedIndex;

                return (
                  <button
                    key={`${suggestion.kind}-${suggestion.email}`}
                    type="button"
                    role="option"
                    aria-selected={isActive}
                    onMouseDown={(event) => {
                      event.preventDefault();
                    }}
                    onClick={() => selectSuggestion(suggestion)}
                    className={[
                      "flex w-full cursor-pointer items-center justify-between gap-3 rounded-[12px] px-3 py-2 text-left transition-colors",
                      isActive
                        ? "bg-[#eef4ff] text-[#20478f]"
                        : "text-[#31435f] hover:bg-[#f7faff] hover:text-[#2f6df6]",
                    ].join(" ")}
                  >
                    <p className="min-w-0 truncate text-[13px] font-medium">{suggestion.label}</p>
                    <span className="shrink-0 text-[11px] font-semibold text-[#8a99b2]">
                      {suggestion.kind === "member" ? "멤버" : "최근"}
                    </span>
                  </button>
                );
              })
            )}
          </div>
        ) : null}
      </div>
    </div>
  );
}

export function MailComposeForm({
  mailboxAddress,
  composeMode,
  currentFolder,
  defaultBcc,
  defaultBody,
  defaultBodyHtml,
  defaultCc,
  defaultSubject,
  defaultTo,
}: MailComposeFormProps) {
  const router = useRouter();
  const toolbarId = useId().replace(/:/g, "");
  const formRef = useRef<HTMLFormElement | null>(null);
  const attachmentInputRef = useRef<HTMLInputElement | null>(null);
  const imageInputRef = useRef<HTMLInputElement | null>(null);
  const editorRootRef = useRef<HTMLDivElement | null>(null);
  const fallbackTextareaRef = useRef<HTMLTextAreaElement | null>(null);
  const fontSizeInputRef = useRef<HTMLInputElement | null>(null);
  const lastSelectionRef = useRef<QuillRange | null>(null);
  const quillRef = useRef<QuillLikeInstance | null>(null);
  const syncEditorRef = useRef<(() => void) | null>(null);
  const syncToolbarStateRef = useRef<(() => void) | null>(null);
  const shouldFocusEditor = composeMode !== "new";
  const normalizedDefaultBody = defaultBody.replace(/\r/g, "");
  const normalizedDefaultBodyHtml = defaultBodyHtml.trim();
  const initialBodyText = shouldFocusEditor
    ? ensureLeadingComposeSpaceText(normalizedDefaultBody)
    : normalizedDefaultBody;
  const initialBodyHtml = shouldFocusEditor
    ? ensureLeadingComposeSpaceHtml(normalizedDefaultBodyHtml)
    : normalizedDefaultBodyHtml;
  const [attachmentFiles, setAttachmentFiles] = useState<ComposeLocalAttachment[]>([]);
  const [attachmentDragActive, setAttachmentDragActive] = useState(false);
  const [editorReady, setEditorReady] = useState(false);
  const [editorFallback, setEditorFallback] = useState(false);
  const [inlineImageUploads, setInlineImageUploads] = useState<ComposeUploadItem[]>([]);
  const [subject, setSubject] = useState(defaultSubject);
  const [bodyText, setBodyText] = useState(initialBodyText);
  const [bodyHtml, setBodyHtml] = useState(initialBodyHtml);
  const [fontSizeInput, setFontSizeInput] = useState("15");
  const [backgroundColor, setBackgroundColor] = useState(defaultComposeBackgroundColor);
  const [textColor, setTextColor] = useState(defaultComposeTextColor);
  const [uploadProgress, setUploadProgress] = useState<ComposeUploadProgressState | null>(null);
  const [uploadError, setUploadError] = useState<string | null>(null);
  const [uploadingCount, setUploadingCount] = useState(0);
  const [sendModalState, setSendModalState] = useState<ComposeSendModalState | null>(null);
  const activeUploadProgressSessionRef = useRef(0);
  const uploadProgressHideTimerRef = useRef<number | null>(null);
  const sendRedirectTimerRef = useRef<number | null>(null);
  const composeHeader = buildComposeHeader(composeMode, mailboxAddress);
  const isUploading = uploadingCount > 0;
  const isSendProcessing = sendModalState !== null && sendModalState.stage !== "error";
  const submitLocked = isUploading || isSendProcessing;
  const uploadFilesRef = useRef<(kind: ComposeUploadKind, files: File[]) => Promise<ComposeUploadItem[]>>(
    async () => [],
  );
  const handleAttachmentFilesRef = useRef<(files: File[]) => Promise<void>>(async () => undefined);
  const handleInlineImageFilesRef = useRef<(files: File[]) => Promise<void>>(async () => undefined);

  useLayoutEffect(() => {
    const formElement = formRef.current;

    if (!formElement) {
      return undefined;
    }

    const scrollComposeViewToTop = () => {
      formElement.scrollTop = 0;

      let parent: HTMLElement | null = formElement.parentElement;

      while (parent) {
        const style = window.getComputedStyle(parent);
        const isScrollable =
          /(auto|scroll)/.test(style.overflowY) && parent.scrollHeight > parent.clientHeight;

        if (isScrollable) {
          parent.scrollTop = 0;
        }

        parent = parent.parentElement;
      }

      window.scrollTo(0, 0);
      document.documentElement.scrollTop = 0;
      document.body.scrollTop = 0;
    };

    scrollComposeViewToTop();
    const frameId = window.requestAnimationFrame(scrollComposeViewToTop);

    return () => {
      window.cancelAnimationFrame(frameId);
    };
  }, []);

  const applyEditorFormat = (name: "background" | "color" | "size", value: string) => {
    const quill = quillRef.current;

    if (!quill?.format) {
      return;
    }

    quill.focus();

    if (lastSelectionRef.current) {
      quill.setSelection?.(lastSelectionRef.current.index, lastSelectionRef.current.length, "silent");
    }

    quill.format(name, value, "user");
    syncEditorRef.current?.();
    syncToolbarStateRef.current?.();
  };

  const commitFontSizeInput = () => {
    const nextValue = normalizeFontSizeValue(fontSizeInput);

    if (!nextValue) {
      syncToolbarStateRef.current?.();
      return;
    }

    setFontSizeInput(extractFontSizeInputValue(nextValue));
    applyEditorFormat("size", nextValue);
  };

  useEffect(() => {
    return () => {
      if (uploadProgressHideTimerRef.current !== null) {
        window.clearTimeout(uploadProgressHideTimerRef.current);
      }

      if (sendRedirectTimerRef.current !== null) {
        window.clearTimeout(sendRedirectTimerRef.current);
      }
    };
  }, []);

  useEffect(() => {
    uploadFilesRef.current = async (kind: ComposeUploadKind, files: File[]) => {
      if (files.length === 0) {
        return [];
      }

      setUploadError(null);
      const uploadSessionId = activeUploadProgressSessionRef.current + 1;
      activeUploadProgressSessionRef.current = uploadSessionId;

      if (uploadProgressHideTimerRef.current !== null) {
        window.clearTimeout(uploadProgressHideTimerRef.current);
        uploadProgressHideTimerRef.current = null;
      }

      setUploadProgress(buildComposeUploadProgressState(kind, files, 0));

      try {
        const formData = new FormData();
        formData.set("kind", kind);
        files.forEach((file) => {
          formData.append("files", file);
        });

        const uploaded = await new Promise<ComposeUploadItem[]>((resolve, reject) => {
          const xhr = new XMLHttpRequest();
          xhr.open("POST", "/api/mailbox/uploads");
          xhr.responseType = "json";
          xhr.upload.addEventListener("progress", (event) => {
            const progressRatio =
              event.lengthComputable && event.total > 0 ? event.loaded / event.total : 0;
            if (activeUploadProgressSessionRef.current !== uploadSessionId) {
              return;
            }

            setUploadProgress(buildComposeUploadProgressState(kind, files, progressRatio));
          });

          xhr.addEventListener("load", () => {
            const payload = parseComposeUploadResponse(xhr);

            if (xhr.status < 200 || xhr.status >= 300 || !payload?.uploads) {
              reject(new Error(payload?.error || "파일 업로드에 실패했습니다."));
              return;
            }

            resolve(payload.uploads);
          });

          xhr.addEventListener("error", () => {
            reject(new Error("파일 업로드에 실패했습니다."));
          });

          xhr.addEventListener("abort", () => {
            reject(new Error("파일 업로드가 중단되었습니다."));
          });

          xhr.send(formData);
        });

        if (activeUploadProgressSessionRef.current === uploadSessionId) {
          setUploadProgress(buildComposeUploadProgressState(kind, files, 1));
        }

        return uploaded;
      } catch (error) {
        const message = error instanceof Error ? error.message : "파일 업로드에 실패했습니다.";
        setUploadError(message);
        throw error;
      } finally {
        uploadProgressHideTimerRef.current = window.setTimeout(() => {
          if (activeUploadProgressSessionRef.current === uploadSessionId) {
            setUploadProgress(null);
          }
        }, 420);
      }
    };

    handleAttachmentFilesRef.current = async (files: File[]) => {
      if (files.length === 0) {
        return;
      }

      setUploadError(null);
      setAttachmentFiles((current) => mergeLocalAttachments(current, files.map(toLocalAttachment)));
    };

    handleInlineImageFilesRef.current = async (files: File[]) => {
      const imageFiles = files.filter((file) => file.type.startsWith("image/"));

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

      setUploadingCount((current) => current + 1);

      try {
        const uploaded = await uploadFilesRef.current("inline-image", imageFiles);
        setInlineImageUploads((current) => mergeUploads(current, uploaded));

        const quill = quillRef.current;

        if (!quill?.insertEmbed) {
          return;
        }

        let insertionIndex =
          quill.getSelection?.(true)?.index ?? Math.max(0, quill.getText().length - 1);

        uploaded.forEach((upload) => {
          quill.insertEmbed?.(insertionIndex, "image", upload.previewUrl, "user");
          insertionIndex += 1;
          quill.insertText?.(insertionIndex, "\n", "user");
          insertionIndex += 1;
        });

        syncEditorRef.current?.();
        quill.focus();
        quill.setSelection?.(insertionIndex, 0, "silent");
      } finally {
        setUploadingCount((current) => Math.max(0, current - 1));
      }
    };
  });

  useEffect(() => {
    let disposed = false;

    const bootEditor = async () => {
      if (!editorRootRef.current) {
        return;
      }

      try {
        const { default: ImportedQuill } = await import("quill");
        const Quill = ImportedQuill as unknown as QuillNamespace;

        const fontStyle = Quill.import("attributors/style/font");
        if (fontStyle) {
          fontStyle.whitelist = [
            "Arial",
            "Georgia",
            "Malgun Gothic",
            "Nanum Gothic",
            "Pretendard",
            "monospace",
          ];
          Quill.register(fontStyle, true);
        }

        const sizeStyle = Quill.import("attributors/style/size");
        if (sizeStyle) {
          sizeStyle.whitelist = null;
          Quill.register(sizeStyle, true);
        }

        const colorStyle = Quill.import("attributors/style/color");
        if (colorStyle) {
          Quill.register(colorStyle, true);
        }

        const backgroundStyle = Quill.import("attributors/style/background");
        if (backgroundStyle) {
          Quill.register(backgroundStyle, true);
        }

        if (disposed || !editorRootRef.current) {
          return;
        }

        const quill = new Quill(editorRootRef.current, {
          modules: {
            toolbar: {
              container: `#${toolbarId}`,
              handlers: {
                image: () => imageInputRef.current?.click(),
              },
            },
          },
          placeholder: "메일 내용을 입력하세요",
          theme: "snow",
        });

        quill.root.setAttribute("spellcheck", "true");

        const initialHtml = initialBodyHtml
          ? initialBodyHtml
          : initialBodyText.trim()
            ? plainTextToHtml(initialBodyText)
            : "<p><br></p>";
        quill.clipboard.dangerouslyPasteHTML(initialHtml);

        const syncEditor = () => {
          const rawHtml = quill.root.innerHTML;
          const rawText = quill.getText();
          const nextHtml = normalizeEditorHtml(rawHtml);
          const nextText = normalizeEditorText(rawText);
          quill.root.classList.toggle("ql-blank", !hasVisibleEditorContent(rawHtml, rawText));
          setBodyHtml(nextHtml);
          setBodyText(nextText);
        };

        const syncToolbarState = () => {
          const range = quill.getSelection?.() ?? null;

          if (range) {
            lastSelectionRef.current = range;
          } else if (!lastSelectionRef.current) {
            return;
          }

          const formats = quill.getFormat?.(range ?? lastSelectionRef.current ?? undefined) ?? {};
          setFontSizeInput(extractFontSizeInputValue(formats.size));
          setTextColor(normalizeToolbarColorValue(formats.color, defaultComposeTextColor));
          setBackgroundColor(
            normalizeToolbarColorValue(formats.background, defaultComposeBackgroundColor),
          );
        };

        const handleEditorDrop = (event: DragEvent) => {
          const files = Array.from(event.dataTransfer?.files ?? []).filter((file) =>
            file.type.startsWith("image/"),
          );

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

          event.preventDefault();
          void handleInlineImageFilesRef.current(files);
        };

        const handleEditorPaste = (event: ClipboardEvent) => {
          const files = Array.from(event.clipboardData?.files ?? []).filter((file) =>
            file.type.startsWith("image/"),
          );

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

          event.preventDefault();
          void handleInlineImageFilesRef.current(files);
        };

        quill.root.addEventListener("drop", handleEditorDrop);
        quill.root.addEventListener("paste", handleEditorPaste);
        quill.on("text-change", syncEditor);
        quill.on("selection-change", syncToolbarState);
        syncEditorRef.current = syncEditor;
        syncToolbarStateRef.current = syncToolbarState;
        quillRef.current = quill;
        syncEditor();
        syncToolbarState();

        if (shouldFocusEditor) {
          window.requestAnimationFrame(() => {
            quill.focus();
            quill.setSelection?.(0, 0, "silent");
            syncToolbarState();
          });
        }

        setEditorReady(true);

        return () => {
          quill.root.removeEventListener("drop", handleEditorDrop);
          quill.root.removeEventListener("paste", handleEditorPaste);
        };
      } catch {
        if (!disposed) {
          setEditorFallback(true);
        }
      }

      return undefined;
    };

    let detachHandlers: (() => void) | undefined;

    void bootEditor().then((cleanup) => {
      detachHandlers = cleanup;
    });

    return () => {
      disposed = true;
      detachHandlers?.();

      if (quillRef.current && syncEditorRef.current && quillRef.current.off) {
        quillRef.current.off("text-change", syncEditorRef.current);
      }

      if (quillRef.current && syncToolbarStateRef.current && quillRef.current.off) {
        quillRef.current.off("selection-change", syncToolbarStateRef.current);
      }

      quillRef.current = null;
      syncEditorRef.current = null;
      syncToolbarStateRef.current = null;
    };
  }, [initialBodyHtml, initialBodyText, shouldFocusEditor, toolbarId]);

  useEffect(() => {
    if (!editorFallback || !shouldFocusEditor) {
      return;
    }

    window.requestAnimationFrame(() => {
      fallbackTextareaRef.current?.focus();
      fallbackTextareaRef.current?.setSelectionRange(0, 0);
    });
  }, [editorFallback, shouldFocusEditor]);

  const effectiveBodyHtml = bodyHtml || (bodyText ? plainTextToHtml(bodyText) : "");

  const removeAttachmentUpload = async (upload: ComposeLocalAttachment) => {
    setAttachmentFiles((current) => current.filter((item) => item.id !== upload.id));
    setUploadError(null);
  };

  const openAttachmentPreview = (upload: ComposeLocalAttachment) => {
    if (typeof window === "undefined") {
      return;
    }

    const previewUrl = URL.createObjectURL(upload.file);
    const popup = window.open(previewUrl, "_blank", "noopener,noreferrer");
    popup?.focus();

    window.setTimeout(() => {
      URL.revokeObjectURL(previewUrl);
    }, 60_000);
  };

  const handleSend = async () => {
    if (!formRef.current || submitLocked) {
      return;
    }

    if (sendRedirectTimerRef.current !== null) {
      window.clearTimeout(sendRedirectTimerRef.current);
      sendRedirectTimerRef.current = null;
    }

    setUploadError(null);
    const initialSendSnapshot =
      attachmentFiles.length > 0
        ? buildComposeSendSnapshotFromLocalAttachments(attachmentFiles)
        : createEmptyComposeSendProgressSnapshot();
    setSendModalState({
      snapshot: initialSendSnapshot,
      stage: attachmentFiles.length > 0 ? "uploading" : "sending",
    });

    try {
      const requestFormData = new FormData(formRef.current);
      requestFormData.set("intent", "send");
      attachmentFiles.forEach((attachment) => {
        requestFormData.append("attachmentFiles", attachment.file, attachment.filename);
      });

      await new Promise<void>((resolve, reject) => {
        const xhr = new XMLHttpRequest();
        let pendingChunk = "";
        let consumedResponseLength = 0;

        const handleStreamLine = (line: string) => {
          const event = JSON.parse(line) as
            | {
                redirectTo: string;
                type: "done" | "error";
              }
            | {
                snapshot: ComposeSendProgressSnapshot;
                type: "upload-progress";
              }
            | {
                stage: "sending" | "uploading";
                type: "stage";
              };

          if (event.type === "upload-progress") {
            setSendModalState({
              snapshot: event.snapshot,
              stage: "uploading",
            });
            return;
          }

          if (event.type === "stage") {
            setSendModalState((current) => ({
              snapshot: current?.snapshot ?? createEmptyComposeSendProgressSnapshot(),
              stage: event.stage,
            }));
            return;
          }

          if (event.type === "done") {
            setSendModalState((current) => ({
              snapshot: current?.snapshot ?? createEmptyComposeSendProgressSnapshot(),
              stage: "complete",
            }));
            sendRedirectTimerRef.current = window.setTimeout(() => {
              router.push(event.redirectTo);
            }, 320);
            return;
          }

          setSendModalState(null);
          router.push(event.redirectTo);
        };

        const consumeResponseText = () => {
          const nextChunk = xhr.responseText.slice(consumedResponseLength);

          if (!nextChunk) {
            return;
          }

          consumedResponseLength = xhr.responseText.length;
          pendingChunk += nextChunk;
          const lines = pendingChunk.split("\n");
          pendingChunk = lines.pop() ?? "";

          for (const line of lines) {
            const normalizedLine = line.trim();

            if (!normalizedLine) {
              continue;
            }

            handleStreamLine(normalizedLine);
          }
        };

        xhr.open("POST", "/api/mailbox/compose-send");

        xhr.upload.addEventListener("progress", (event) => {
          if (attachmentFiles.length === 0 || initialSendSnapshot.totalBytes <= 0) {
            return;
          }

          const uploadRatio =
            event.lengthComputable && event.total > 0 ? event.loaded / event.total : 0;
          const uploadedBytes = Math.round(initialSendSnapshot.totalBytes * clampProgressRatio(uploadRatio));
          setSendModalState({
            snapshot: applyUploadedBytesToComposeSendSnapshot(initialSendSnapshot, uploadedBytes),
            stage: "uploading",
          });
        });

        xhr.upload.addEventListener("load", () => {
          if (attachmentFiles.length === 0) {
            return;
          }

          setSendModalState({
            snapshot: applyUploadedBytesToComposeSendSnapshot(
              initialSendSnapshot,
              initialSendSnapshot.totalBytes,
            ),
            stage: "sending",
          });
        });

        xhr.addEventListener("progress", consumeResponseText);
        xhr.addEventListener("readystatechange", () => {
          if (xhr.readyState >= XMLHttpRequest.LOADING) {
            consumeResponseText();
          }
        });

        xhr.addEventListener("load", () => {
          consumeResponseText();

          const trailingLine = pendingChunk.trim();

          if (trailingLine) {
            handleStreamLine(trailingLine);
            pendingChunk = "";
          }

          if (xhr.status >= 200 && xhr.status < 300) {
            resolve();
            return;
          }

          try {
            const payload = JSON.parse(xhr.responseText) as { redirectTo?: string } | null;

            if (payload?.redirectTo) {
              setSendModalState(null);
              router.push(payload.redirectTo);
              resolve();
              return;
            }
          } catch {
            // ignore malformed error payloads
          }

          reject(new Error("mail-send-request-failed"));
        });

        xhr.addEventListener("error", () => {
          reject(new Error("mail-send-request-failed"));
        });

        xhr.addEventListener("abort", () => {
          reject(new Error("mail-send-request-aborted"));
        });

        xhr.send(requestFormData);
      });
    } catch {
      setSendModalState((current) => ({
        errorMessage: "메일 전송 중 오류가 발생했습니다. 다시 시도해주세요.",
        snapshot: current?.snapshot ?? createEmptyComposeSendProgressSnapshot(),
        stage: "error",
      }));
    }
  };

  const handleComposeSubmit = (event: FormEvent<HTMLFormElement>) => {
    if (submitLocked) {
      event.preventDefault();
      return;
    }

    const submitter = (event.nativeEvent as SubmitEvent).submitter as
      | HTMLButtonElement
      | HTMLInputElement
      | null;

    if (submitter?.getAttribute("name") === "intent" && submitter.getAttribute("value") === "draft") {
      return;
    }

    event.preventDefault();
    void handleSend();
  };

  const sendSnapshot = sendModalState?.snapshot ?? createEmptyComposeSendProgressSnapshot();
  const sendProgressPercent =
    sendSnapshot.totalBytes > 0
      ? Math.round((sendSnapshot.completedBytes / sendSnapshot.totalBytes) * 100)
      : sendModalState?.stage === "sending" || sendModalState?.stage === "complete"
        ? 100
        : 0;
  const hasSendAttachments = sendSnapshot.items.length > 0;
  const sendStageMessage =
    sendModalState?.stage === "uploading"
      ? "첨부파일을 메일과 함께 업로드하고 있습니다."
      : sendModalState?.stage === "sending"
        ? "첨부 업로드가 끝나 메일 서버로 전송하고 있습니다."
        : sendModalState?.stage === "complete"
          ? "메일 전송이 완료되어 메일함으로 이동합니다."
          : sendModalState?.stage === "error"
            ? sendModalState.errorMessage
            : "";

  return (
    <form
      ref={formRef}
      action={submitMailboxMessageAction}
      onSubmit={handleComposeSubmit}
      className="flex h-full min-h-0 flex-col overflow-y-auto overscroll-contain"
    >
      <input type="hidden" name="folder" value={currentFolder} />
      <input type="hidden" name="body" value={bodyText} />
      <input type="hidden" name="bodyHtml" value={effectiveBodyHtml} />
      <input
        type="hidden"
        name="inlineImageUploadIds"
        value={inlineImageUploads.map((upload) => String(upload.id)).join(",")}
      />

      <input
        ref={attachmentInputRef}
        type="file"
        multiple
        className="hidden"
        onChange={(event) => {
          const files = Array.from(event.currentTarget.files ?? []);
          event.currentTarget.value = "";
          void handleAttachmentFilesRef.current(files);
        }}
      />
      <input
        ref={imageInputRef}
        type="file"
        accept="image/*"
        multiple
        className="hidden"
        onChange={(event) => {
          const files = Array.from(event.currentTarget.files ?? []);
          event.currentTarget.value = "";
          void handleInlineImageFilesRef.current(files);
        }}
      />

      <div className="border-b border-[#edf2fb] px-8 py-8">
        <div className="grid grid-cols-[minmax(0,1fr)_auto] items-start gap-x-4 gap-y-4">
          <div className="min-w-0 max-w-[720px] pr-2">
            <p className="text-[28px] font-semibold tracking-[-0.04em] text-[#1a2943]">
              {composeHeader.title}
            </p>
            <p className="mt-2 max-w-[760px] text-[14px] leading-6 text-[#7484a0]">
              {composeHeader.description}
            </p>
          </div>
          <div className={composeActionGroupClassName}>
            <button
              type="submit"
              disabled={submitLocked}
              className={`cursor-pointer disabled:cursor-not-allowed ${composePrimaryActionButtonClassName}`}
            >
              보내기
            </button>
            <button
              type="submit"
              name="intent"
              value="draft"
              disabled={submitLocked}
              className={composeSecondaryActionButtonClassName}
            >
              임시 저장
            </button>
          </div>
        </div>
      </div>

      <div className="grid shrink-0 gap-3.5 border-b border-[#edf2fb] px-8 py-6">
        {false ? (
          <div className="grid grid-cols-[88px_minmax(0,1fr)] items-start gap-3">
          <div className="pt-1 text-[13px] font-medium text-[#6f809d]">발송 옵션</div>
          <label className="flex max-w-[720px] cursor-pointer flex-col gap-2 rounded-[12px] border border-[#e2eaf8] bg-[#fbfcff] px-4 py-3 transition-colors hover:border-[#d4e0f7] hover:bg-white">
            <span className="flex items-center gap-3">
              <input
                type="checkbox"
                name="sendIndividually"
                value="1"
                className="h-4 w-4 rounded-[4px] border-[#cfd7e4] text-[#2f6df6] focus:ring-2 focus:ring-[#d7e5ff] focus:ring-offset-0"
              />
              <span className="text-[14px] font-semibold text-[#243555]">개별 발송</span>
            </span>
            <span className="pl-7 text-[12px] leading-5 text-[#7d8da8]">
              받는 사람이 여러 명이면 각 수신자에게 별도 메일로 전송합니다. 참조와
              숨은참조는 각 메일에 동일하게 적용됩니다.
            </span>
          </label>
          </div>
        ) : null}

        <RecipientAutocompleteField
          autoFocus={!shouldFocusEditor}
          defaultValue={defaultTo}
          helperContent={
            <label className="flex max-w-[720px] cursor-pointer flex-col gap-2 rounded-[12px] border border-[#e2eaf8] bg-[#fbfcff] px-4 py-3 transition-colors hover:border-[#d4e0f7] hover:bg-white">
              <span className="flex items-center gap-3">
                <input
                  type="checkbox"
                  name="sendIndividually"
                  value="1"
                  className="h-4 w-4 rounded-[4px] border-[#cfd7e4] text-[#2f6df6] focus:ring-2 focus:ring-[#d7e5ff] focus:ring-offset-0"
                />
                <span className="text-[14px] font-semibold text-[#243555]">媛쒕퀎 諛쒖넚</span>
              </span>
              <span className="pl-7 text-[12px] leading-5 text-[#7d8da8]">
                諛쏅뒗 ?щ엺???щ윭 紐낆씠硫?媛??섏떊?먯뿉寃?蹂꾨룄 硫붿씪濡??꾩넚?⑸땲?? 李몄“?
                ?⑥?李몄“??媛?硫붿씪???숈씪?섍쾶 ?곸슜?⑸땲??
              </span>
            </label>
          }
          inputId={`${toolbarId}-to`}
          label="받는 사람"
          name="to"
          placeholder="받는 사람 이메일 주소"
        />

        <RecipientAutocompleteField
          defaultValue={defaultCc}
          inputId={`${toolbarId}-cc`}
          label="참조"
          name="cc"
          placeholder="참조 이메일 주소"
        />

        <RecipientAutocompleteField
          defaultValue={defaultBcc}
          inputId={`${toolbarId}-bcc`}
          label="숨은참조"
          name="bcc"
          placeholder="숨은참조 이메일 주소"
        />

        <div className="grid grid-cols-[88px_minmax(0,1fr)] items-center gap-3">
          <label htmlFor={`${toolbarId}-subject`} className="text-[13px] font-medium text-[#6f809d]">
            제목
          </label>
          <input
            id={`${toolbarId}-subject`}
            name="subject"
            type="text"
            value={subject}
            onChange={(event) => setSubject(event.target.value)}
            placeholder="제목을 입력하세요"
            className="h-10 rounded-[10px] border border-[#e2eaf8] bg-[#fbfcff] px-3.5 text-[14px] text-[#1e2a43] outline-none placeholder:text-[#a0adc2] transition-colors focus:border-[#cbdcff] focus:bg-white"
          />
        </div>

        <div className="grid grid-cols-[88px_minmax(0,1fr)] items-start gap-3 border-t border-[#edf2fb] pt-5">
          <div className="pt-3 text-[13px] font-medium text-[#6f809d]">첨부 파일</div>
          <div className="space-y-3">
            <div
              role="button"
              tabIndex={0}
              onClick={() => attachmentInputRef.current?.click()}
              onKeyDown={(event) => {
                if (event.key === "Enter" || event.key === " ") {
                  event.preventDefault();
                  attachmentInputRef.current?.click();
                }
              }}
              onDragEnter={(event) => {
                event.preventDefault();
                setAttachmentDragActive(true);
              }}
              onDragOver={(event) => {
                event.preventDefault();
                setAttachmentDragActive(true);
              }}
              onDragLeave={(event) => {
                event.preventDefault();
                if (event.currentTarget.contains(event.relatedTarget as Node | null)) {
                  return;
                }
                setAttachmentDragActive(false);
              }}
              onDrop={(event) => {
                event.preventDefault();
                setAttachmentDragActive(false);
                void handleAttachmentFilesRef.current(Array.from(event.dataTransfer.files ?? []));
              }}
              className={`flex h-14 w-full cursor-pointer items-center justify-center rounded-[8px] border border-dashed px-5 text-center transition-all duration-200 ${
                attachmentDragActive
                  ? "border-[#7ea5ff] bg-[#f4f8ff] text-[#2f6df6] shadow-[0_16px_32px_rgba(47,109,246,0.08)]"
                  : "border-[#d9e1ee] bg-white text-[#7b879a] hover:border-[#c7d4ea] hover:bg-[#fbfcfe]"
              }`}
            >
              <div className="flex min-w-0 items-center justify-center gap-2 text-[14px]">
                <svg
                  viewBox="0 0 24 24"
                  fill="none"
                  stroke="currentColor"
                  strokeWidth="1.7"
                  strokeLinecap="round"
                  strokeLinejoin="round"
                  className="h-[16px] w-[16px] shrink-0"
                  aria-hidden="true"
                >
                  <path d="M12 7v10" />
                  <path d="M7 12h10" />
                  <path d="M6.8 4.8h7l3 3V19a1.8 1.8 0 0 1-1.8 1.8H6.8A1.8 1.8 0 0 1 5 19V6.6a1.8 1.8 0 0 1 1.8-1.8Z" />
                  <path d="M13.8 4.8v3.4h3.4" />
                </svg>
                <span className="truncate">파일을 마우스로 끌어 오세요</span>
              </div>
            </div>

            {attachmentFiles.length > 0 ? (
              <div className="overflow-hidden rounded-[16px] border border-[#e2e9f5] bg-white">
                {attachmentFiles.map((upload, index) => (
                  <div
                    key={upload.id}
                    className={`flex items-center gap-4 px-4 py-3 ${
                      index === 0 ? "" : "border-t border-[#eef2f8]"
                    }`}
                  >
                    <div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-[12px] bg-[#f4f8ff] text-[#5f7fc6]">
                      <AttachmentIcon isImage={upload.isImage} />
                    </div>

                    <div className="min-w-0 flex-1">
                      <p className="truncate text-[14px] font-medium text-[#1f2d46]">
                        {upload.filename}
                      </p>
                      <p className="mt-1 text-[12px] text-[#7a8ba7]">{formatUploadSize(upload.sizeBytes)}</p>
                    </div>

                    <div className="flex shrink-0 items-center gap-2">
                      {upload.isImage ? (
                        <button
                          type="button"
                          onClick={() => openAttachmentPreview(upload)}
                          className="inline-flex h-9 items-center justify-center rounded-[12px] px-3 text-[12px] font-medium text-[#516684] transition-colors hover:bg-[#f4f7fc] hover:text-[#2f6df6]"
                        >
                          보기
                        </button>
                      ) : null}
                      <button
                        type="button"
                        onClick={() => void removeAttachmentUpload(upload)}
                        className="inline-flex h-9 items-center justify-center rounded-[12px] px-3 text-[12px] font-medium text-[#8b97aa] transition-colors hover:bg-[#fff4f4] hover:text-[#e25656]"
                      >
                        제거
                      </button>
                    </div>
                  </div>
                ))}
              </div>
            ) : null}

            {uploadError ? <p className="text-[13px] text-[#d24747]">{uploadError}</p> : null}
          </div>
        </div>
      </div>

      <div className="min-h-0 flex-1 px-8 py-7">
        <div className="official-compose-editor flex h-full min-h-[420px] flex-col overflow-visible rounded-[10px] border border-[#e5ecf9] bg-[#fcfdff]">
          <div
            id={toolbarId}
            className="rounded-t-[10px] border-b border-[#edf2fb] bg-[#fcfdff] px-4 py-3"
          >
            <span className="ql-formats">
              <select className="ql-font" defaultValue="Pretendard">
                <option value="Pretendard">프리텐다드</option>
                <option value="Arial">Arial</option>
                <option value="Malgun Gothic">맑은 고딕</option>
                <option value="Nanum Gothic">나눔고딕</option>
                <option value="Georgia">Georgia</option>
                <option value="monospace">Monospace</option>
              </select>
              <div className="compose-toolbar-size-control">
                <input
                  ref={fontSizeInputRef}
                  type="text"
                  inputMode="numeric"
                  list={`${toolbarId}-font-size-options`}
                  value={fontSizeInput}
                  aria-label="Font size"
                  onChange={(event) => {
                    const nextValue = event.target.value.replace(/[^\d]/g, "");
                    setFontSizeInput(nextValue);
                  }}
                  onBlur={commitFontSizeInput}
                  onFocus={(event) => event.currentTarget.select()}
                  onKeyDown={(event) => {
                    if (event.key === "Enter") {
                      event.preventDefault();
                      commitFontSizeInput();
                      fontSizeInputRef.current?.blur();
                    }

                    if (event.key === "Escape") {
                      event.preventDefault();
                      syncToolbarStateRef.current?.();
                      fontSizeInputRef.current?.blur();
                    }
                  }}
                  className="compose-toolbar-size-input"
                />
                <span className="compose-toolbar-size-suffix">px</span>
                <datalist id={`${toolbarId}-font-size-options`}>
                  <option value="13" />
                  <option value="15" />
                  <option value="17" />
                  <option value="19" />
                  <option value="24" />
                  <option value="32" />
                </datalist>
              </div>
            </span>
            <span className="ql-formats">
              <button type="button" className="ql-bold" aria-label="굵게" />
              <button type="button" className="ql-italic" aria-label="기울임" />
              <button type="button" className="ql-underline" aria-label="밑줄" />
              <button type="button" className="ql-strike" aria-label="취소선" />
            </span>
            <span className="ql-formats">
              <label className="compose-toolbar-swatch-button">
                <input
                  type="color"
                  value={textColor}
                  aria-label="Text color"
                  className="compose-toolbar-swatch-input"
                  onChange={(event) => {
                    const nextValue = event.currentTarget.value;
                    setTextColor(nextValue);
                    applyEditorFormat("color", nextValue);
                  }}
                />
                <span className="compose-toolbar-swatch-label">A</span>
                <span
                  className="compose-toolbar-swatch-chip"
                  style={{ backgroundColor: textColor }}
                />
              </label>
              <label className="compose-toolbar-swatch-button">
                <input
                  type="color"
                  value={backgroundColor}
                  aria-label="Highlight color"
                  className="compose-toolbar-swatch-input"
                  onChange={(event) => {
                    const nextValue = event.currentTarget.value;
                    setBackgroundColor(nextValue);
                    applyEditorFormat("background", nextValue);
                  }}
                />
                <span className="compose-toolbar-swatch-label">Bg</span>
                <span
                  className="compose-toolbar-swatch-chip compose-toolbar-swatch-chip-wide"
                  style={{ backgroundColor }}
                />
              </label>
            </span>
            <span className="ql-formats">
              <button type="button" className="ql-blockquote" aria-label="인용문" />
              <button type="button" className="ql-code-block" aria-label="코드 블록" />
              <button type="button" className="ql-list" value="ordered" aria-label="번호 목록" />
              <button type="button" className="ql-list" value="bullet" aria-label="글머리 목록" />
            </span>
            <span className="ql-formats">
              <button type="button" className="ql-link" aria-label="링크" />
              <button type="button" className="ql-image" aria-label="이미지" />
              <button type="button" className="ql-clean" aria-label="서식 지우기" />
            </span>
          </div>

          {editorFallback ? (
            <textarea
              ref={fallbackTextareaRef}
              value={bodyText}
              onChange={(event) => {
                setBodyText(event.target.value);
                setBodyHtml(plainTextToHtml(event.target.value));
              }}
              placeholder="메일 내용을 입력하세요"
              className="h-full min-h-[360px] w-full resize-none rounded-b-[10px] border-0 bg-transparent px-5 py-5 text-[15px] leading-8 text-[#223250] outline-none placeholder:text-[#a0adc2]"
            />
          ) : (
            <div className="relative min-h-0 flex-1 rounded-b-[10px] bg-[#fcfdff]">
              {!editorReady ? (
                <div className="pointer-events-none absolute inset-0 flex items-center justify-center rounded-b-[10px] bg-[rgba(252,253,255,0.88)] text-[14px] text-[#8a97ac]">
                  편집기를 불러오는 중입니다...
                </div>
              ) : null}
              <div ref={editorRootRef} className="h-full min-h-[360px]" />
            </div>
          )}
        </div>
      </div>

      {sendModalState ? (
        <div className="fixed inset-0 z-[220] flex items-center justify-center bg-[rgba(15,23,42,0.26)] px-5 py-8 backdrop-blur-[3px]">
          <div
            className={`w-full rounded-[20px] bg-white px-7 py-6 shadow-[0_28px_80px_rgba(15,23,42,0.22)] ${
              hasSendAttachments ? "max-w-[860px]" : "max-w-[540px]"
            }`}
          >
            <div className="flex items-start justify-between gap-4">
              <div>
                <p className="text-[28px] font-semibold tracking-[-0.04em] text-[#1b2942]">
                  {hasSendAttachments ? "첨부파일 업로드" : "메일 전송"}
                </p>
                <p className="mt-2 text-[14px] leading-6 text-[#6f809d]">
                  {sendStageMessage}
                </p>
              </div>
              <div className="flex items-center gap-3">
                <span
                  className={`om-badge om-badge-md ${
                    sendModalState.stage === "complete"
                      ? "om-badge-success"
                      : sendModalState.stage === "error"
                        ? "om-badge-danger"
                        : "om-badge-info"
                  }`}
                >
                  {sendModalState.stage === "uploading"
                    ? "업로드 중"
                    : sendModalState.stage === "sending"
                      ? "전송 중"
                      : sendModalState.stage === "complete"
                        ? "완료"
                        : "오류"}
                </span>
                {sendModalState.stage === "error" ? (
                  <CloseIconButton
                    ariaLabel="메일 전송 오류 닫기"
                    onClick={() => setSendModalState(null)}
                  />
                ) : null}
              </div>
            </div>

            <div className="mt-5 flex flex-wrap items-center justify-between gap-3 text-[14px]">
              <p className="font-semibold text-[#1f2d46]">
                {hasSendAttachments
                  ? `${sendSnapshot.uploadedCount}개 / ${sendSnapshot.totalCount}개  ${formatUploadSize(sendSnapshot.completedBytes)} / ${formatUploadSize(sendSnapshot.totalBytes)}`
                  : sendModalState.stage === "sending"
                    ? "첨부 업로드 없이 메일 전송을 준비하고 있습니다."
                    : "전송 준비 중입니다."}
              </p>
              <p className="text-[#73839d]">
                {sendModalState.stage === "uploading"
                  ? `${sendProgressPercent}% 업로드`
                  : sendModalState.stage === "sending"
                    ? "전송 처리 중"
                    : sendModalState.stage === "complete"
                      ? "전송 완료"
                      : "다시 시도해주세요"}
              </p>
            </div>

            <div
              className="mt-3 h-4 overflow-hidden rounded-[999px] bg-[#e9eef6]"
              role="progressbar"
              aria-label="메일 전송 준비"
              aria-valuemin={0}
              aria-valuemax={100}
              aria-valuenow={sendProgressPercent}
            >
              <div
                className={`h-full rounded-[999px] transition-[width] duration-300 ${
                  sendModalState.stage === "error"
                    ? "bg-[#d24747]"
                    : "bg-[linear-gradient(135deg,#16c05a,#12a348)]"
                }`}
                style={{ width: `${Math.max(sendProgressPercent, sendModalState.stage === "sending" ? 100 : 0)}%` }}
              />
            </div>

            {hasSendAttachments ? (
              <div className="mt-5 overflow-hidden rounded-[16px] border border-[#dfe7f3]">
                <div className="grid grid-cols-[minmax(0,1.8fr)_140px_180px] gap-4 border-b border-[#edf2f8] bg-[#f8fbff] px-5 py-3 text-[13px] font-semibold text-[#5d6d88]">
                  <span>파일명</span>
                  <span>전송상태</span>
                  <span>전송량</span>
                </div>
                <div className="max-h-[280px] overflow-y-auto">
                  {sendSnapshot.items.map((item, index) => (
                    <div
                      key={item.id}
                      className={`grid grid-cols-[minmax(0,1.8fr)_140px_180px] items-center gap-4 px-5 py-3 ${
                        index === 0 ? "" : "border-t border-[#f0f4fa]"
                      }`}
                    >
                      <div className="flex min-w-0 items-center gap-3">
                        <div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-[10px] bg-[#f4f8ff] text-[#5f7fc6]">
                          <AttachmentIcon isImage={item.kind === "inline-image"} />
                        </div>
                        <p className="truncate text-[14px] font-medium text-[#1f2d46]">
                          {item.filename}
                        </p>
                      </div>
                      <span className={`text-[14px] font-medium ${getComposeSendUploadStatusTone(item.status)}`}>
                        {getComposeSendUploadStatusLabel(item.status)}
                      </span>
                      <span className="text-[14px] text-[#5f6f8b]">
                        {formatUploadSize(item.uploadedBytes)} / {formatUploadSize(item.sizeBytes)}
                      </span>
                    </div>
                  ))}
                </div>
              </div>
            ) : null}

          </div>
        </div>
      ) : null}

      {uploadProgress ? (
        <div className="pointer-events-none fixed inset-x-0 bottom-6 z-[180] flex justify-center px-4">
          <div className="w-full max-w-[420px] rounded-[22px] border border-[#d7e3fb] bg-[rgba(255,255,255,0.98)] px-5 py-4 shadow-[0_22px_54px_rgba(24,38,70,0.16)] backdrop-blur-[18px]">
            <p className="text-[13px] font-semibold text-[#2f6df6]">
              {getComposeUploadProgressTitle(uploadProgress.kind)}
            </p>
            <p className="mt-2 truncate text-[14px] font-semibold text-[#223250]">
              {uploadProgress.currentFileName}
            </p>
            <p className="mt-1 text-[13px] text-[#6f809d]">
              전체 {uploadProgress.totalFiles}개 파일 중 {uploadProgress.percent}% 업로드
            </p>
            <div
              className="mt-3 h-2 overflow-hidden rounded-full bg-[#e6edf9]"
              role="progressbar"
              aria-label={getComposeUploadProgressTitle(uploadProgress.kind)}
              aria-valuemin={0}
              aria-valuemax={100}
              aria-valuenow={uploadProgress.percent}
            >
              <div
                className="h-full rounded-full bg-[linear-gradient(135deg,#2f6df6,#245de3)] transition-[width] duration-200"
                style={{ width: `${uploadProgress.percent}%` }}
              />
            </div>
          </div>
        </div>
      ) : null}
    </form>
  );
}
