// App shell — navigation between Hub, Creator, Viewer

const { useState: useStateApp, useEffect: useEffectApp, useRef: useRefApp } = React;
const QUICK_GUIDE_SEEN_KEY = "songfilm-quick-guide-seen";

function QuickGuideModal({ onClose }) {
  useEffectApp(() => {
    const onKey = (e) => {
      if (e.key === "Escape") onClose();
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [onClose]);

  return (
    <div className="modal-overlay quick-guide-overlay" data-role="main-quick-guide-overlay" onClick={onClose}>
      <div className="modal quick-guide-modal" data-role="main-quick-guide-dialog" role="dialog" aria-modal="true" aria-labelledby="quick-guide-title" onClick={e => e.stopPropagation()}>
        <div className="quick-guide-head" data-role="main-quick-guide-header">
          <div>
            <h3 id="quick-guide-title">Quick Guide</h3>
            <p>SongFilm을 처음부터 내보내기까지 빠르게 살펴봅니다.</p>
          </div>
          <button type="button" className="quick-guide-close" data-role="main-quick-guide-close-btn" onClick={onClose} aria-label="Quick Guide 닫기">×</button>
        </div>
        <div className="quick-guide-image-wrap" data-role="main-quick-guide-image-panel">
          <img src="HowToUse.png" alt="SongFilm 사용 가이드" className="quick-guide-image" data-role="main-quick-guide-image"/>
        </div>
      </div>
    </div>
  );
}

function App() {
  const { Icon, AlbumHub, Creator, Viewer, ToastHost, TWEAK_DEFAULTS, TweaksPanel, INITIAL_ALBUM, makeId } = window;
  const buildAlbumSubtitleDefaults = (overrides = {}) => ({
    fontSize: 108,
    lineGap: 1.45,
    subPos: "bottom",
    transition: "fade",
    subtitleContrastMode: "auto",
    subtitleBackdrop: true,
    subtitleCloud: false,
    ...overrides,
  });

  const [albums, setAlbums] = useStateApp(() => {
    try {
      const savedAlbums = localStorage.getItem("songfilm-albums");
      if (savedAlbums) {
        const parsed = JSON.parse(savedAlbums);
        if (Array.isArray(parsed) && parsed.length) return parsed.map(normalizeAlbum);
      }
      // migrate legacy single-album storage
      const legacy = localStorage.getItem("songfilm-album");
      if (legacy) {
        const a = JSON.parse(legacy);
        return [normalizeAlbum({ ...a, id: a.id || "album-" + makeId() })];
      }
    } catch(e) {}
    return [];
  });

  const [currentAlbumId, setCurrentAlbumId] = useStateApp(() => {
    try {
      const saved = localStorage.getItem("songfilm-currentAlbumId");
      if (saved) return saved;
    } catch(e) {}
    return null;
  });

  const [view, setView] = useStateApp(() => {
    try { return localStorage.getItem("songfilm-view") || "hub"; } catch(e) { return "hub"; }
  });
  const [currentSongId, setCurrentSongId] = useStateApp(() => {
    try { return localStorage.getItem("songfilm-songId") || null; } catch(e) { return null; }
  });
  const [viewerSongId, setViewerSongId] = useStateApp(null);
  const [viewerAlbumId, setViewerAlbumId] = useStateApp(null);
  const [tweaks, setTweaks] = useStateApp(() => {
    try { return { ...TWEAK_DEFAULTS, ...JSON.parse(localStorage.getItem("songfilm-tweaks") || "{}") }; }
    catch(e) { return TWEAK_DEFAULTS; }
  });
  const [tweaksOn, setTweaksOn] = useStateApp(false);
  const [sidebarCollapsed, setSidebarCollapsed] = useStateApp(() => {
    try { return localStorage.getItem("songfilm-sidebarCollapsed") === "true"; }
    catch(e) { return false; }
  });
  const SIDEBAR_MIN_WIDTH = 180;
  const SIDEBAR_MAX_WIDTH = 480;
  const SIDEBAR_DEFAULT_WIDTH = 220;
  const [sidebarWidth, setSidebarWidth] = useStateApp(() => {
    try {
      const saved = Number(localStorage.getItem("songfilm-sidebarWidth"));
      if (Number.isFinite(saved) && saved >= SIDEBAR_MIN_WIDTH && saved <= SIDEBAR_MAX_WIDTH) return saved;
    } catch(e) {}
    return SIDEBAR_DEFAULT_WIDTH;
  });
  const [sidebarResizing, setSidebarResizing] = useStateApp(false);
  const appRootRef = useRefApp(null);
  const clampSidebarWidth = (value) => Math.max(SIDEBAR_MIN_WIDTH, Math.min(SIDEBAR_MAX_WIDTH, value));
  const startSidebarResize = (event) => {
    if (sidebarCollapsed) return;
    event.preventDefault();
    const startX = event.clientX;
    const startWidth = sidebarWidth;
    const rootEl = appRootRef.current;
    setSidebarResizing(true);
    const onMove = (moveEvent) => {
      const next = clampSidebarWidth(startWidth + (moveEvent.clientX - startX));
      if (rootEl) rootEl.style.setProperty("--sidebar-width", next + "px");
    };
    const onUp = (upEvent) => {
      document.removeEventListener("pointermove", onMove);
      document.removeEventListener("pointerup", onUp);
      setSidebarResizing(false);
      setSidebarWidth(clampSidebarWidth(startWidth + (upEvent.clientX - startX)));
    };
    document.addEventListener("pointermove", onMove);
    document.addEventListener("pointerup", onUp);
  };
  const [sidebarFoldedAlbumIds, setSidebarFoldedAlbumIds] = useStateApp(() => {
    try {
      const saved = JSON.parse(localStorage.getItem("songfilm-sidebarFoldedAlbumIds") || "[]");
      return Array.isArray(saved) ? saved.filter((id) => typeof id === "string" && id.trim()) : [];
    } catch (e) {
      return [];
    }
  });
  const [sidebarSongDropAlbumId, setSidebarSongDropAlbumId] = useStateApp(null);
  const [albumDownloadBusy, setAlbumDownloadBusy] = useStateApp(false);
  const [albumDownloadKind, setAlbumDownloadKind] = useStateApp(null); // null | "audio" | "video"
  const [albumDownloadProgress, setAlbumDownloadProgress] = useStateApp(0);
  const [albumDownloadStage, setAlbumDownloadStage] = useStateApp("");
  const [batchRenderState, setBatchRenderState] = useStateApp(BATCH_RENDER_INITIAL_STATE);
  const [newAlbumModal, setNewAlbumModal] = useStateApp(false);
  const [shareLinkCopyModal, setShareLinkCopyModal] = useStateApp(false);
  const [importMyAlbumModal, setImportMyAlbumModal] = useStateApp(false);
  const [quickGuideModal, setQuickGuideModal] = useStateApp(false);
  const [albumUploadBusy, setAlbumUploadBusy] = useStateApp(false);
  const [songBackupBusyId, setSongBackupBusyId] = useStateApp(null);
  const [songBackupUploadBusy, setSongBackupUploadBusy] = useStateApp(false);
  const [publicAlbums, setPublicAlbums] = useStateApp([]);
  const [publicAlbumsLoading, setPublicAlbumsLoading] = useStateApp(false);
  const [publicAlbumsError, setPublicAlbumsError] = useStateApp("");
  const [selectedPublicAlbumId, setSelectedPublicAlbumId] = useStateApp(null);
  const [publishingAlbum, setPublishingAlbum] = useStateApp(false);
  const [myAlbums, setMyAlbums] = useStateApp([]);
  const [myAlbumsLoading, setMyAlbumsLoading] = useStateApp(false);
  const [myAlbumsError, setMyAlbumsError] = useStateApp("");
  const [selectedMyAlbumId, setSelectedMyAlbumId] = useStateApp(null);
  const [savingToMyAlbums, setSavingToMyAlbums] = useStateApp(false);
  const batchStopRequestedRef = useRefApp(false);
  const albumUploadInputRef = useRefApp(null);

  const album = albums.find(a => a.id === currentAlbumId) || albums[0];
  useEffectApp(() => {
    if (!albums.find(a => a.id === currentAlbumId)) {
      setCurrentAlbumId(albums[0]?.id || null);
    }
  }, [albums, currentAlbumId]);

  const setAlbum = (updater) => {
    setAlbums(prev => prev.map(a => {
      if (a.id !== album.id) return a;
      return typeof updater === "function" ? updater(a) : updater;
    }));
  };

  // persist
  useEffectApp(() => {
    try {
      localStorage.setItem("songfilm-albums", JSON.stringify(sanitizeAlbumsForStorage(albums)));
    } catch(e){}
  }, [albums]);
  useEffectApp(() => { try { if (currentAlbumId) localStorage.setItem("songfilm-currentAlbumId", currentAlbumId); } catch(e){} }, [currentAlbumId]);
  useEffectApp(() => { try { localStorage.setItem("songfilm-view", view); } catch(e){} }, [view]);
  useEffectApp(() => { try { if (currentSongId) localStorage.setItem("songfilm-songId", currentSongId); } catch(e){} }, [currentSongId]);
  useEffectApp(() => { try { localStorage.setItem("songfilm-tweaks", JSON.stringify(tweaks)); } catch(e){} }, [tweaks]);
  useEffectApp(() => { try { localStorage.setItem("songfilm-sidebarCollapsed", String(sidebarCollapsed)); } catch(e){} }, [sidebarCollapsed]);
  useEffectApp(() => { try { localStorage.setItem("songfilm-sidebarWidth", String(sidebarWidth)); } catch(e){} }, [sidebarWidth]);
  useEffectApp(() => {
    try { localStorage.setItem("songfilm-sidebarFoldedAlbumIds", JSON.stringify(sidebarFoldedAlbumIds)); } catch(e) {}
  }, [sidebarFoldedAlbumIds]);
  useEffectApp(() => {
    try {
      const seen = localStorage.getItem(QUICK_GUIDE_SEEN_KEY) === "true";
      if (!seen) setQuickGuideModal(true);
    } catch (e) {}
  }, []);

  const closeQuickGuideModal = () => {
    setQuickGuideModal(false);
    try { localStorage.setItem(QUICK_GUIDE_SEEN_KEY, "true"); } catch (e) {}
  };

  useEffectApp(() => {
    const onDragEnd = () => setSidebarSongDropAlbumId(null);
    window.addEventListener("dragend", onDragEnd);
    return () => window.removeEventListener("dragend", onDragEnd);
  }, []);

  const hubSongDragMime = () => window.SONGFILM_HUB_SONG_DRAG_MIME || "application/x-songfilm-song-ref";
  const hasHubSongDrag = (e) => {
    const want = hubSongDragMime().toLowerCase();
    return Array.from(e.dataTransfer?.types || []).some((t) => String(t).toLowerCase() === want);
  };

  const handleLibraryAlbumDragOverCapture = (e) => {
    if (!hasHubSongDrag(e)) return;
    e.preventDefault();
    e.stopPropagation();
    e.dataTransfer.dropEffect = "copy";
  };

  const handleLibraryAlbumDragEnter = (e, albumId) => {
    if (!hasHubSongDrag(e)) return;
    e.preventDefault();
    setSidebarSongDropAlbumId(albumId);
  };

  const handleLibraryAlbumDragLeave = (e, albumId) => {
    if (!e.currentTarget.contains(e.relatedTarget)) {
      setSidebarSongDropAlbumId((cur) => (cur === albumId ? null : cur));
    }
  };

  /** 대상 앨범에 같은 제목이 있으면 `제목 2`, `제목 3` … 형태로 바꿈 */
  const uniquifySongTitleAmongSongs = (title, songs) => {
    const base = String(title || "").trim() || "노래";
    const taken = new Set((songs || []).map((s) => String(s?.title || "").trim()).filter(Boolean));
    if (!taken.has(base)) return base;
    let n = 2;
    while (true) {
      const candidate = `${base} ${n}`;
      if (!taken.has(candidate)) return candidate;
      n += 1;
    }
  };

  const handleLibraryAlbumDrop = (e, targetAlbumId) => {
    e.preventDefault();
    e.stopPropagation();
    setSidebarSongDropAlbumId(null);
    const mime = hubSongDragMime();
    const raw = e.dataTransfer.getData(mime);
    if (!raw) return;
    let ref;
    try {
      ref = JSON.parse(raw);
    } catch {
      return;
    }
    const sourceAlbumId = typeof ref?.sourceAlbumId === "string" ? ref.sourceAlbumId : "";
    const songId = typeof ref?.songId === "string" ? ref.songId : "";
    if (!sourceAlbumId || !songId) return;
    if (sourceAlbumId === targetAlbumId) {
      window.toast("같은 앨범입니다. 순서 변경은 목록에서 드래그하세요.");
      return;
    }

    let copiedTitle = "";
    let targetTitle = "";
    setAlbums((prev) => {
      const sourceAlbum = prev.find((x) => x.id === sourceAlbumId);
      const sourceSong = sourceAlbum?.songs?.find((s) => s.id === songId);
      if (!sourceSong) return prev;
      const cloneFn = window.cloneSongJsonForAlbumDrop;
      if (typeof cloneFn !== "function") return prev;
      const newSong = cloneFn(sourceSong, "song-" + makeId());
      if (!newSong) return prev;
      const targetAlbum = prev.find((x) => x.id === targetAlbumId);
      if (!targetAlbum) return prev;
      targetTitle = targetAlbum.title || "";
      const uniqueTitle = uniquifySongTitleAmongSongs(newSong.title, targetAlbum.songs);
      copiedTitle = uniqueTitle;
      const songToAdd = uniqueTitle === newSong.title ? newSong : { ...newSong, title: uniqueTitle };
      return prev.map((al) => {
        if (al.id !== targetAlbumId) return al;
        return { ...al, songs: [...al.songs, songToAdd] };
      });
    });
    if (copiedTitle) {
      window.toast(
        targetTitle
          ? `「${copiedTitle}」을(를) 「${targetTitle}」에 복사했습니다.`
          : `「${copiedTitle}」을(를) 앨범에 복사했습니다.`
      );
    }
  };

  const loadPublicAlbums = async ({ selectFirst = false } = {}) => {
    setPublicAlbumsLoading(true);
    setPublicAlbumsError("");
    try {
      const listResp = await fetch(`${ALBUM_STORAGE_BASE}/public-albums`);
      if (!listResp.ok) throw new Error(`공개 앨범 목록 조회 실패 (${listResp.status})`);
      const listData = await listResp.json();
      const summaries = Array.isArray(listData?.albums) ? listData.albums : [];
      const loaded = await Promise.all(summaries.map(async (summary) => {
        try {
          const detailResp = await fetch(`${ALBUM_STORAGE_BASE}/public-albums/${encodeURIComponent(summary.id)}`);
          if (!detailResp.ok) throw new Error(`상세 조회 실패 (${detailResp.status})`);
          const detail = await detailResp.json();
          const albumPayload = normalizeAlbum(detail?.album || {});
          return {
            ...summary,
            album: albumPayload,
            title: albumPayload.title || summary.title,
            artist: albumPayload.artist || summary.artist,
            songCount: albumPayload.songs?.length || summary.songCount || 0,
          };
        } catch (error) {
          console.warn("[Public Album] 상세 로드 실패:", summary.id, error);
          return { ...summary, album: null };
        }
      }));
      setPublicAlbums(loaded);
      if (selectFirst) {
        setSelectedPublicAlbumId((current) => (
          loaded.some((item) => item.id === current) ? current : loaded[0]?.id || null
        ));
      }
      return loaded;
    } catch (error) {
      const message = error?.message || "공개 앨범을 불러오지 못했습니다.";
      setPublicAlbumsError(message);
      window.toast(message);
      return [];
    } finally {
      setPublicAlbumsLoading(false);
    }
  };

  const loadMyAlbums = async () => {
    const session = window.SongfilmAuth.getSession();
    if (!session) return [];
    setMyAlbumsLoading(true);
    setMyAlbumsError("");
    try {
      const listResp = await fetch(`${ALBUM_STORAGE_BASE}/user-albums`, {
        headers: { Authorization: `Bearer ${session.token}` },
      });
      if (!listResp.ok) throw new Error(`내 앨범 목록 조회 실패 (${listResp.status})`);
      const listData = await listResp.json();
      const summaries = Array.isArray(listData?.albums) ? listData.albums : [];
      const loaded = await Promise.all(summaries.map(async (summary) => {
        try {
          const detailResp = await fetch(`${ALBUM_STORAGE_BASE}/user-albums/${encodeURIComponent(summary.id)}`, {
            headers: { Authorization: `Bearer ${session.token}` },
          });
          if (!detailResp.ok) throw new Error(`상세 조회 실패 (${detailResp.status})`);
          const detail = await detailResp.json();
          const albumPayload = normalizeAlbum(detail?.album || {});
          return {
            ...detail,
            album: albumPayload,
            title: albumPayload.title || summary.title,
            artist: albumPayload.artist || summary.artist,
            songCount: albumPayload.songs?.length || summary.songCount || 0,
            myAlbumSavedAt: summary.myAlbumSavedAt || albumPayload.myAlbumSavedAt || "",
          };
        } catch (error) {
          console.warn("[My Albums] 상세 로드 실패:", summary.id, error);
          return { ...summary, album: null };
        }
      }));
      setMyAlbums(loaded);
      return loaded;
    } catch (error) {
      const message = error?.message || "내 앨범을 불러오지 못했습니다.";
      setMyAlbumsError(message);
      window.toast(message);
      return [];
    } finally {
      setMyAlbumsLoading(false);
    }
  };

  const saveToMyAlbums = async () => {
    if (!album || savingToMyAlbums) return;
    const session = window.SongfilmAuth.getSession();
    if (!session) { window.toast("로그인이 필요합니다."); return; }
    setSavingToMyAlbums(true);
    try {
      window.toast(`"${album.title}" 내 앨범 저장 중…`);
      const listResp = await fetch(`${ALBUM_STORAGE_BASE}/user-albums`, {
        headers: { Authorization: `Bearer ${session.token}` },
      });
      if (!listResp.ok) throw new Error(`내 앨범 목록 조회 실패 (${listResp.status})`);
      const listData = await listResp.json();
      const summaries = Array.isArray(listData?.albums) ? listData.albums : [];

      const payload = await makePublicAlbumPayload(album);
      const albumId = String(album.sourceAlbumId || getPublicAlbumId(payload) || album.id || "album").trim();
      const normalizeTitle = (value) => String(value || "").trim().toLocaleLowerCase();
      const duplicateTitleAlbum = summaries.find((item) => (
        normalizeTitle(item?.title) === normalizeTitle(payload?.title) && String(item?.id || "") !== albumId
      ));
      if (duplicateTitleAlbum) {
        window.toast(`같은 이름의 앨범이 이미 있습니다. 이름을 변경한 뒤 다시 저장해 주세요. (${duplicateTitleAlbum.title})`);
        return;
      }
      const existingSameId = summaries.find((item) => String(item?.id || "") === albumId);
      if (existingSameId) {
        const targetAlbumTitle = String(existingSameId?.title || payload?.title || album?.title || "이 앨범").trim() || "이 앨범";
        const ok = confirm(`서버에 같은 ID(${albumId})의 앨범이 있어 덮어쓰기 저장됩니다. 계속할까요?`);
        if (!ok) return;
        window.toast(`"${targetAlbumTitle}" 앨범을 기존 내 앨범에 덮어써서 저장합니다.`);
      }

      const now = new Date().toISOString();
      const resp = await fetch(`${ALBUM_STORAGE_BASE}/user-albums/${encodeURIComponent(albumId)}`, {
        method: "PUT",
        headers: {
          "Content-Type": "application/json",
          Authorization: `Bearer ${session.token}`,
        },
        body: JSON.stringify({ album: { ...payload, myAlbumSavedAt: now } }),
      });
      if (!resp.ok) {
        let message = `내 앨범 저장 실패 (${resp.status})`;
        try { const body = await resp.json(); message = body?.error || message; } catch {}
        throw new Error(message);
      }
      setAlbum((prevAlbum) => ({ ...prevAlbum, sourceAlbumId: albumId }));
      window.toast(`"${album.title}" 내 앨범에 저장 완료`);
      if (view === "my-albums") loadMyAlbums();
    } catch (error) {
      console.error("[My Albums] 저장 실패:", error);
      window.toast("내 앨범 저장 실패: " + (error?.message || error));
    } finally {
      setSavingToMyAlbums(false);
    }
  };

  const deleteFromMyAlbums = async (albumId) => {
    const session = window.SongfilmAuth.getSession();
    if (!session || !albumId) return;
    try {
      const resp = await fetch(`${ALBUM_STORAGE_BASE}/user-albums/${encodeURIComponent(albumId)}`, {
        method: "DELETE",
        headers: { Authorization: `Bearer ${session.token}` },
      });
      if (!resp.ok && resp.status !== 404) throw new Error(`삭제 실패 (${resp.status})`);
      setMyAlbums((prev) => prev.filter((item) => item.id !== albumId));
      setSelectedMyAlbumId((prev) => (prev === albumId ? null : prev));
      window.toast("내 앨범에서 삭제했습니다.");
    } catch (error) {
      console.error("[My Albums] 삭제 실패:", error);
      window.toast("내 앨범 삭제 실패: " + (error?.message || error));
    }
  };

  const copyToWorkspace = (item) => {
    const sourceAlbum = item?.album;
    if (!sourceAlbum) return;
    const newAlbum = normalizeAlbum({
      ...sourceAlbum,
      id: "album-" + makeId(),
      title: getUniqueWorkspaceAlbumTitle(sourceAlbum.title),
      isPublished: false,
      publicId: "",
      publishedAt: "",
      publicUpdatedAt: "",
      myAlbumSavedAt: "",
    });
    setAlbums((prev) => [...prev, newAlbum]);
    setCurrentAlbumId(newAlbum.id);
    setView("hub");
    window.toast(`"${newAlbum.title}" Workspace에 복사했습니다.`);
  };

  const importMyAlbumToWorkspace = (item) => {
    const sourceAlbum = item?.album;
    const sourceAlbumId = String(item?.id || "").trim();
    if (!sourceAlbum || !sourceAlbumId) return;
    const newAlbum = normalizeAlbum({
      ...sourceAlbum,
      id: "album-" + makeId(),
      sourceAlbumId,
    });
    setAlbums((prev) => [...prev, newAlbum]);
    setCurrentAlbumId(newAlbum.id);
    setView("hub");
    window.toast(`"${newAlbum.title}" 내 앨범을 Workspace로 가져왔습니다.`);
  };

  const openImportMyAlbumModal = async () => {
    const session = window.SongfilmAuth.getSession();
    if (!session) {
      window.toast("로그인이 필요합니다.");
      return;
    }
    setImportMyAlbumModal(true);
    await loadMyAlbums();
  };

  const getUniqueWorkspaceAlbumTitle = (title) => {
    const baseTitle = String(title || "공유 앨범").trim() || "공유 앨범";
    const usedTitles = new Set(albums.map((item) => String(item?.title || "").trim()));
    if (!usedTitles.has(baseTitle)) return baseTitle;
    let index = 2;
    while (usedTitles.has(`${baseTitle} ${index}`)) index += 1;
    return `${baseTitle} ${index}`;
  };

  const parseSharedAlbumId = (input) => {
    const value = String(input || "").trim();
    if (!value) return "";
    try {
      const parsed = new URL(value);
      return String(parsed.searchParams.get("sharedID") || parsed.searchParams.get("shareId") || "").trim();
    } catch {}
    const queryIndex = value.indexOf("?");
    if (queryIndex >= 0) {
      const params = new URLSearchParams(value.slice(queryIndex + 1));
      const id = String(params.get("sharedID") || params.get("shareId") || "").trim();
      if (id) return id;
    }
    return value.replace(/^sharedID=/i, "").trim();
  };

  const copySharedLinkAlbumToWorkspace = async (shareLinkInput) => {
    const sharedId = parseSharedAlbumId(shareLinkInput);
    if (!/^[a-fA-F0-9]{64}$/.test(sharedId)) {
      window.toast("공유 링크 또는 sharedID가 올바르지 않습니다.");
      throw new Error("유효하지 않은 공유 링크입니다.");
    }

    try {
      const resp = await fetch(`${ALBUM_STORAGE_BASE}/share-links/${encodeURIComponent(sharedId)}`);
      const data = await resp.json().catch(() => ({}));
      if (!resp.ok) {
        const fallback = resp.status === 410
          ? "공유 링크가 만료되었거나 회수되었습니다."
          : `공유 앨범 조회 실패 (${resp.status})`;
        throw new Error(data?.error || fallback);
      }

      const sourceAlbum = data?.album;
      if (!sourceAlbum || typeof sourceAlbum !== "object") {
        throw new Error("공유 링크에 앨범 데이터가 없습니다.");
      }

      const copiedAlbum = { ...sourceAlbum };
      delete copiedAlbum.share;
      delete copiedAlbum.publicId;
      delete copiedAlbum.publishedAt;
      delete copiedAlbum.publicUpdatedAt;
      delete copiedAlbum.myAlbumSavedAt;
      delete copiedAlbum.publishedBy;

      const newAlbum = normalizeAlbum({
        ...copiedAlbum,
        id: "album-" + makeId(),
        title: getUniqueWorkspaceAlbumTitle(copiedAlbum.title),
        isPublished: false,
        publicId: "",
        publishedAt: "",
        publicUpdatedAt: "",
        myAlbumSavedAt: "",
      });

      setAlbums((prev) => [...prev, newAlbum]);
      setCurrentAlbumId(newAlbum.id);
      setView("hub");
      window.toast(`"${newAlbum.title}" Workspace에 복사했습니다.`);
    } catch (error) {
      window.toast("공유 링크 앨범 복사 실패: " + (error?.message || error));
      throw error;
    }
  };

  // Edit-mode (Tweaks) handshake
  useEffectApp(() => {
    const onMsg = (e) => {
      if (e.data?.type === "__activate_edit_mode") setTweaksOn(true);
      if (e.data?.type === "__deactivate_edit_mode") setTweaksOn(false);
    };
    window.addEventListener("message", onMsg);
    try { window.parent.postMessage({ type: "__edit_mode_available" }, "*"); } catch(e) {}
    return () => window.removeEventListener("message", onMsg);
  }, []);

  useEffectApp(() => {
    if (view === "public" && !publicAlbumsLoading && !publicAlbums.length) {
      loadPublicAlbums();
    }
    if (view === "my-albums") {
      loadMyAlbums();
    }
  }, [view]);

  // ── Navigation ──────────────────────────────────────────────────────────────

  const openSong = (id) => { setCurrentSongId(id); setView("creator"); };
  const openLyricEditor = (id) => { setCurrentSongId(id); setView("lyric-editor"); };
  const openViewer = (songId = null, options = {}) => {
    const id = typeof songId === "string" && songId.trim() ? songId.trim() : null;
    const albumId = typeof options?.albumId === "string" && options.albumId.trim() ? options.albumId.trim() : null;
    setViewerSongId(id);
    setViewerAlbumId(albumId);
    setView("viewer");
  };
  const openViewerLibrary = () => openViewer(null, { albumId: null });
  const openPublicAlbums = async () => {
    setView("public");
    setSelectedPublicAlbumId(null);
    await loadPublicAlbums();
  };
  const openSongFromAlbum = (song) => {
    if (song?.status === "ready") { openViewer(song.id); return; }
    openSong(song.id);
  };

  const newSong = () => {
    const id = "song-" + makeId();
    const song = {
      id,
      title: "새 노래",
      filename: "",
      duration: 0,
      size: "",
      status: "empty",
      cues: [],
      scenes: [],
      lyricTimeline: null,
      referenceLyricsText: "",
      referenceLyricsName: "",
      lyric_original: "",
      lyric_style: "",
      transcribeMode: "",
      transcribeWorkflow: null,
      paletteName: "midnight-violet",
      workflowStep: "upload",
      storyId: "",
      storageVersion: "",
      savedAt: "",
      audioStorageKey: "",
      audioMimeType: "audio/mpeg",
      audioPersistedAt: "",
      audioByteLength: 0,
      previewSettings: buildAlbumSubtitleDefaults(album?.subtitleDefaults || {}),
      exportState: { status: "idle", progress: 0, done: false },
    };
    setAlbum(prev => ({ ...prev, songs: [...prev.songs, song] }));
    setCurrentSongId(id);
    setView("creator");
  };

  // ── Album CRUD ───────────────────────────────────────────────────────────────

  const createAlbum = ({ title, artist, year }) => {
    const id = "album-" + makeId();
    const newAlbum = {
      id,
      title: title?.trim() || "새 앨범",
      artist: artist?.trim() || "Artist",
      year: year || new Date().getFullYear(),
      cover: "auto",
      subtitleDefaults: buildAlbumSubtitleDefaults(),
      songs: [],
    };
    setAlbums(prev => [...prev, newAlbum]);
    setCurrentAlbumId(id);
    setView("hub");
    setNewAlbumModal(false);
    window.toast(`앨범 "${newAlbum.title}" 생성됨`);
  };

  const deleteAlbum = (id) => {
    if (!confirm("이 앨범을 삭제할까요? 포함된 모든 노래도 함께 제거됩니다.")) return;
    setAlbums(prev => prev.filter(a => a.id !== id));
    window.toast("앨범이 삭제되었습니다");
  };

  const switchAlbum = (id) => {
    setCurrentAlbumId(id);
    setView("hub");
    setCurrentSongId(null);
    setSidebarFoldedAlbumIds((prev) => prev.filter((albumId) => albumId !== id));
  };

  // ── Publish ──────────────────────────────────────────────────────────────────

  const publishAlbum = async () => {
    if (!album || publishingAlbum) return;
    setPublishingAlbum(true);
    try {
      window.toast(`"${album.title}" 공개 앨범 저장 중…`);
      const publicAlbum = await makePublicAlbumPayload(album);
      const session = window.SongfilmAuth.getSession();
      if (session) {
        publicAlbum.publishedBy = {
          email: session.email || "",
          nickname: session.nickname || "",
        };
      }
      const publicId = getPublicAlbumId(publicAlbum);
      const resp = await fetch(`${ALBUM_STORAGE_BASE}/public-albums/${encodeURIComponent(publicId)}`, {
        method: "PUT",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ album: publicAlbum }),
      });
      if (!resp.ok) {
        let message = `공개 앨범 저장 실패 (${resp.status})`;
        try {
          const errorBody = await resp.json();
          message = errorBody?.error || message;
        } catch {}
        throw new Error(message);
      }
      const result = await resp.json();
      setAlbum((prev) => ({
        ...prev,
        isPublished: true,
        publicId: result.id || publicId,
        publishedAt: result.publishedAt || prev.publishedAt || new Date().toISOString(),
        publicUpdatedAt: result.updatedAt || new Date().toISOString(),
        songs: prev.songs.map((song) => {
          const publicSong = publicAlbum.songs.find((item) => item.id === song.id);
          if (!publicSong) return song;
          return {
            ...song,
            audioServerUrl: publicSong.audioServerUrl || song.audioServerUrl || "",
            exportState: {
              ...(song.exportState || {}),
              audioFilename: publicSong.exportState?.audioFilename || song.exportState?.audioFilename || "",
              videoFilename: publicSong.exportState?.videoFilename || song.exportState?.videoFilename || "",
            },
          };
        }),
      }));
      await loadPublicAlbums();
      window.toast(`"${album.title}" 공개 완료`);
    } catch (error) {
      console.error("[Public Album] publish 실패:", error);
      window.toast("앨범 공개 실패: " + (error?.message || error));
    } finally {
      setPublishingAlbum(false);
    }
  };

  const unpublishAlbum = async () => {
    if (!album || publishingAlbum) return;
    const publicId = getPublicAlbumId(album);
    if (!publicId) return;
    if (!confirm(`"${album.title}" 공개를 취소할까요?`)) return;
    setPublishingAlbum(true);
    try {
      const resp = await fetch(`${ALBUM_STORAGE_BASE}/public-albums/${encodeURIComponent(publicId)}`, { method: "DELETE" });
      if (!resp.ok && resp.status !== 404) {
        let message = `공개 취소 실패 (${resp.status})`;
        try {
          const errorBody = await resp.json();
          message = errorBody?.error || message;
        } catch {}
        throw new Error(message);
      }
      setAlbum((prev) => ({ ...prev, isPublished: false, publicUpdatedAt: "" }));
      setPublicAlbums((prev) => prev.filter((item) => item.id !== publicId));
      if (selectedPublicAlbumId === publicId) setSelectedPublicAlbumId(null);
      window.toast("앨범 공개를 취소했습니다");
    } catch (error) {
      console.error("[Public Album] unpublish 실패:", error);
      window.toast("앨범 공개 취소 실패: " + (error?.message || error));
    } finally {
      setPublishingAlbum(false);
    }
  };

  // 공개 앨범 화면에서 직접 unpublish (워크스페이스 album 상태에 의존하지 않음)
  const unpublishPublicAlbum = async (item) => {
    if (publishingAlbum) return;
    const publicId = String(item?.id || getPublicAlbumId(item?.album) || "").trim();
    if (!publicId) return;
    const title = item?.album?.title || item?.title || "이 앨범";
    if (!confirm(`"${title}" 공개를 취소할까요?`)) return;
    setPublishingAlbum(true);
    try {
      const resp = await fetch(`${ALBUM_STORAGE_BASE}/public-albums/${encodeURIComponent(publicId)}`, { method: "DELETE" });
      if (!resp.ok && resp.status !== 404) {
        let message = `공개 취소 실패 (${resp.status})`;
        try {
          const errorBody = await resp.json();
          message = errorBody?.error || message;
        } catch {}
        throw new Error(message);
      }
      setPublicAlbums((prev) => prev.filter((entry) => entry.id !== publicId));
      if (selectedPublicAlbumId === publicId) setSelectedPublicAlbumId(null);
      // 워크스페이스에 동일 공개 앨범이 있으면 상태 동기화 (best-effort)
      setAlbums((prev) => prev.map((a) => (
        getPublicAlbumId(a) === publicId ? { ...a, isPublished: false, publicUpdatedAt: "" } : a
      )));
      window.toast("앨범 공개를 취소했습니다");
    } catch (error) {
      console.error("[Public Album] unpublish 실패:", error);
      window.toast("앨범 공개 취소 실패: " + (error?.message || error));
    } finally {
      setPublishingAlbum(false);
    }
  };

  // ── Batch render ─────────────────────────────────────────────────────────────

  const patchBatchRenderItem = (songId, patch) => {
    setBatchRenderState((prev) => ({
      ...prev,
      items: prev.items.map((item) => (
        item.songId === songId ? { ...item, ...patch } : item
      )),
    }));
  };

  const patchBatchSongExportState = (songId, patch) => {
    setAlbum((prevAlbum) => ({
      ...prevAlbum,
      songs: prevAlbum.songs.map((song) => (
        song.id !== songId
          ? song
          : {
              ...song,
              ...(patch.status === "done" ? {
                status: "ready",
                completedAt: patch.renderedAt || new Date().toISOString(),
                ...(window.clearSongRenderDirty?.() || {}),
              } : {}),
              exportState: { ...(song.exportState || {}), ...patch },
            }
      )),
    }));
  };

  const ensureBatchNotStopped = () => {
    if (batchStopRequestedRef.current) throw createBatchStoppedError();
  };

  const openBatchRenderDialog = () => {
    if (batchRenderState.running) return;
    const ready = album.songs.filter((song) => song.status === "ready");
    if (!ready.length) { window.toast("배치 렌더링할 완성곡이 없습니다."); return; }
    setBatchRenderState({
      ...BATCH_RENDER_INITIAL_STATE,
      open: true,
      status: "config",
      total: ready.length,
      selectedSongIds: ready.map((song) => song.id),
      forceRerender: true,
      stage: "렌더링할 곡을 선택하세요.",
      items: createBatchRenderItems(ready),
    });
  };

  const setBatchRenderForceRerender = (forceRerender) => {
    if (batchRenderState.running) return;
    setBatchRenderState((prev) => ({ ...prev, forceRerender }));
  };

  const toggleBatchRenderSong = (songId) => {
    if (batchRenderState.running) return;
    setBatchRenderState((prev) => {
      const selected = new Set(prev.selectedSongIds || []);
      if (selected.has(songId)) selected.delete(songId);
      else selected.add(songId);
      return {
        ...prev,
        selectedSongIds: Array.from(selected),
        items: prev.items.map((item) => (
          item.songId === songId
            ? {
                ...item,
                selected: selected.has(songId),
                status: selected.has(songId) ? "queued" : "excluded",
                progress: 0,
                message: selected.has(songId) ? "대기 중" : "렌더링 제외",
              }
            : item
        )),
      };
    });
  };

  const selectAllBatchRenderSongs = () => {
    if (batchRenderState.running) return;
    setBatchRenderState((prev) => ({
      ...prev,
      selectedSongIds: prev.items.map((item) => item.songId),
      items: prev.items.map((item) => ({ ...item, selected: true, status: "queued", progress: 0, message: "대기 중" })),
    }));
  };

  const clearBatchRenderSongs = () => {
    if (batchRenderState.running) return;
    setBatchRenderState((prev) => ({
      ...prev,
      selectedSongIds: [],
      items: prev.items.map((item) => ({ ...item, selected: false, status: "excluded", progress: 0, message: "렌더링 제외" })),
    }));
  };

  const pollBatchRenderJob = async ({ song, jobId, audioFilename, index, total }) => {
    for (;;) {
      ensureBatchNotStopped();
      await waitAlbumMs(1200);
      ensureBatchNotStopped();

      const statusResp = await fetch(`${ALBUM_RENDERER_BASE}/render/${jobId}/status`);
      if (!statusResp.ok) throw new Error(`${song.title} 렌더 상태 조회 실패 (${statusResp.status})`);

      const statusData = await statusResp.json();
      const status = String(statusData?.status || "pending");
      const renderProgress = Math.round(Number(statusData?.progress || 0) * 100);
      const statusLabel = status === "bundling" ? "번들링" : status === "uploading" ? "업로드" : "렌더링";
      const itemStatus = status === "uploading" ? "uploading-video" : status;
      const overallProgress = Math.round(((index + Math.min(1, renderProgress / 100)) / Math.max(total, 1)) * 100);

      setBatchRenderState((prev) => ({
        ...prev,
        progress: Math.min(99, overallProgress),
        stage: `${song.title} ${statusLabel} 중… ${Math.min(100, renderProgress)}%`,
      }));
      patchBatchRenderItem(song.id, {
        status: itemStatus,
        progress: renderProgress,
        message: `${statusLabel} 중… ${Math.min(100, renderProgress)}%`,
      });
      patchBatchSongExportState(song.id, {
        status,
        done: status === "done",
        progress: renderProgress,
        error: statusData?.error || "",
        audioFilename,
        jobId,
        videoFilename: statusData?.videoFilename || "",
      });

      if (status === "canceled") throw createBatchStoppedError();

      if (status === "done") {
        const renderedAt = new Date().toISOString();
        patchBatchRenderItem(song.id, { status: "done", progress: 100, message: "완료" });
        patchBatchSongExportState(song.id, {
          status: "done", done: true, progress: 100,
          audioFilename, jobId,
          videoFilename: statusData?.videoFilename || "",
          renderedAt, error: "",
        });
        return;
      }

      if (status === "error") throw new Error(statusData?.error || `${song.title} 렌더링 실패`);
    }
  };

  const renderBatchSong = async (song, index, total, { forceRerender = true } = {}) => {
    const existingVideoFilename = String(song?.exportState?.videoFilename || "").trim();
    const existingJobId = String(song?.exportState?.jobId || "").trim();
    const existingStatus = String(song?.exportState?.status || "");
    const existingDone = !forceRerender && existingStatus === "done" && (existingVideoFilename || existingJobId);
    const activeJobId = !forceRerender && BATCH_RENDER_ACTIVE_STATUSES.has(existingStatus) ? existingJobId : "";

    setBatchRenderState((prev) => ({
      ...prev,
      currentSongId: song.id,
      currentJobId: activeJobId || null,
      currentIndex: index + 1,
      stage: `${song.title} 준비 중…`,
    }));

    if (existingDone) {
      const progress = Math.round(((index + 1) / Math.max(total, 1)) * 100);
      patchBatchRenderItem(song.id, { status: "skipped", progress: 100, message: "기존 렌더 사용" });
      setBatchRenderState((prev) => ({ ...prev, progress, stage: `${song.title} 기존 비디오 사용` }));
      return;
    }

    if (forceRerender) {
      patchBatchSongExportState(song.id, { status: "pending", done: false, progress: 0, error: "" });
    }

    let audioFilename = getAlbumStorageAudioFilename(song);
    if (!audioFilename) {
      ensureBatchNotStopped();
      patchBatchRenderItem(song.id, { status: "uploading", progress: 0, message: "오디오 업로드 중…" });
      setBatchRenderState((prev) => ({ ...prev, stage: `${song.title} 오디오 업로드 중…` }));
      const audioBlob = await loadAlbumSongAudioBlob(song);
      ensureBatchNotStopped();
      const audioForm = new FormData();
      audioForm.append("file", audioBlob, song.filename || `${song.title}.mp3`);
      const audioResp = await fetch(`${ALBUM_STORAGE_BASE}/audio/save`, { method: "POST", body: audioForm });
      if (!audioResp.ok) throw new Error(`${song.title} 오디오 업로드 실패 (${audioResp.status})`);
      const audioData = await audioResp.json();
      audioFilename = String(audioData?.filename || "").trim();
      patchBatchSongExportState(song.id, { audioFilename });
    }

    let jobId = activeJobId;
    if (!jobId) {
      ensureBatchNotStopped();
      patchBatchRenderItem(song.id, { status: "checking", progress: 0, message: "렌더링 서버 확인 중…" });
      setBatchRenderState((prev) => ({ ...prev, stage: "렌더링 서버 확인 중…" }));
      const healthResp = await fetch(`${ALBUM_RENDERER_BASE}/health`);
      if (!healthResp.ok) throw new Error(`렌더링 서버 연결 실패 (${healthResp.status})`);

      ensureBatchNotStopped();
      patchBatchRenderItem(song.id, { status: "pending", progress: 0, message: "이미지 준비 및 렌더링 시작 중…" });
      setBatchRenderState((prev) => ({ ...prev, stage: `${song.title} 이미지 준비 및 비디오 렌더링 시작…` }));
      const songData = await makeAlbumRenderSongData(song);
      ensureBatchNotStopped();
      const startResp = await fetch(`${ALBUM_RENDERER_BASE}/render`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ songData, audioFilename }),
      });
      if (!startResp.ok) {
        let errMsg = `${song.title} 렌더 시작 실패 (${startResp.status})`;
        try { const errBody = await startResp.json(); errMsg = errBody?.error || errBody?.message || errMsg; } catch {}
        throw new Error(errMsg);
      }

      const body = await startResp.json();
      jobId = body?.jobId;
      if (!jobId) throw new Error(`${song.title} 렌더 jobId가 없습니다.`);
      setBatchRenderState((prev) => ({ ...prev, currentJobId: jobId }));
      patchBatchSongExportState(song.id, {
        status: "bundling", done: false, progress: 0, error: "",
        audioFilename, jobId, videoFilename: "", renderedAt: "",
      });
    } else {
      patchBatchRenderItem(song.id, {
        status: existingStatus,
        progress: Number(song.exportState?.progress || 0),
        message: "진행 중인 렌더에 다시 연결",
      });
    }

    await pollBatchRenderJob({ song, jobId, audioFilename, index, total });
  };

  const startBatchRenderVideos = async () => {
    if (batchRenderState.running) return;
    const selected = new Set(batchRenderState.selectedSongIds || []);
    const ready = album.songs.filter((song) => song.status === "ready" && selected.has(song.id));
    if (!ready.length) { window.toast("렌더링할 곡을 하나 이상 선택해 주세요."); return; }

    batchStopRequestedRef.current = false;
    setBatchRenderState((prev) => ({
      ...prev,
      open: true, running: true, status: "running",
      total: ready.length, progress: 0, currentSongId: null, currentIndex: 0, error: "",
      stage: prev.forceRerender ? "선택한 곡을 처음부터 다시 렌더링합니다…" : "선택한 곡의 비디오를 준비합니다…",
      items: prev.items.map((item) => (
        selected.has(item.songId)
          ? { ...item, selected: true, status: "queued", progress: 0, message: "대기 중" }
          : { ...item, selected: false, status: "excluded", progress: 0, message: "렌더링 제외" }
      )),
    }));

    try {
      window.toast(batchRenderState.forceRerender ? "선택한 곡 재렌더링 시작" : "배치 비디오 렌더링 시작");
      for (let index = 0; index < ready.length; index += 1) {
        ensureBatchNotStopped();
        await renderBatchSong(ready[index], index, ready.length, { forceRerender: batchRenderState.forceRerender });
      }
      setBatchRenderState((prev) => ({
        ...prev,
        running: false, stopping: false, status: "done",
        currentSongId: null, currentJobId: null, progress: 100,
        stage: prev.forceRerender ? "선택한 곡 재렌더링 완료" : "배치 비디오 렌더링 완료",
      }));
      window.toast(batchRenderState.forceRerender ? "선택한 곡 재렌더링 완료" : "배치 비디오 렌더링 완료");
    } catch (error) {
      if (error?.name === "BatchRenderStopped") {
        setBatchRenderState((prev) => ({
          ...prev,
          running: false, stopping: false, status: "stopped", currentJobId: null,
          progress: prev.progress, stage: "배치 렌더링이 중지되었습니다.",
          items: prev.items.map((item) => (
            item.songId === prev.currentSongId && !["done", "skipped", "error"].includes(item.status)
              ? { ...item, status: "stopped", message: "중지됨" }
              : item
          )),
        }));
        window.toast("배치 렌더링 중지됨");
        return;
      }
      console.error("[Batch Render] 실패:", error);
      setBatchRenderState((prev) => ({
        ...prev,
        running: false, stopping: false, status: "error", currentJobId: null,
        error: error?.message || String(error), stage: "배치 렌더링 실패",
        items: prev.items.map((item) => (
          item.songId === prev.currentSongId
            ? { ...item, status: "error", message: error?.message || String(error) }
            : item
        )),
      }));
      window.toast("배치 렌더링 실패: " + (error?.message || error));
    }
  };

  const requestStopBatchRender = async () => {
    if (!batchRenderState.running) {
      setBatchRenderState((prev) => ({ ...prev, open: false }));
      return;
    }
    batchStopRequestedRef.current = true;
    const cancelJobId = batchRenderState.currentJobId;
    const cancelSongId = batchRenderState.currentSongId;
    setBatchRenderState((prev) => ({
      ...prev,
      stopping: true,
      stage: cancelJobId ? "현재 서버 렌더를 취소하는 중…" : "현재 준비 작업 후 배치 큐를 중지합니다…",
    }));
    if (!cancelJobId) return;
    try {
      await fetch(`${ALBUM_RENDERER_BASE}/render/${cancelJobId}/cancel`, { method: "POST" });
      if (cancelSongId) {
        patchBatchRenderItem(cancelSongId, { status: "stopped", message: "서버 렌더 취소 요청됨" });
        patchBatchSongExportState(cancelSongId, { status: "canceled", done: false, error: "렌더링이 취소되었습니다." });
      }
    } catch (error) {
      console.warn("[Batch Render] 렌더 취소 요청 실패:", error);
      window.toast("렌더 취소 요청 실패: " + (error?.message || error));
    }
  };

  const closeBatchRenderDialog = () => {
    if (batchRenderState.running) { requestStopBatchRender(); return; }
    setBatchRenderState((prev) => ({ ...prev, open: false }));
  };

  // ── Download / backup ────────────────────────────────────────────────────────

  const downloadAlbumMediaZip = async (mode) => {
    if (albumDownloadBusy) return;
    if (mode !== "audio" && mode !== "video") return;
    const ready = album.songs.filter(s => s.status === "ready");
    if (!ready.length) { window.toast("다운로드할 완성곡이 없습니다."); return; }

    // 비디오는 이 흐름에서 렌더링하지 않는다 — 수정 화면의 내보내기에서 렌더된 결과만 수집한다.
    let targetSongs = ready;
    if (mode === "video") {
      targetSongs = ready.filter((song) => !!getAlbumRenderedVideoUrl(song));
      const missing = ready.filter((song) => !getAlbumRenderedVideoUrl(song));
      if (!targetSongs.length) {
        window.toast("렌더링된 비디오가 없습니다. 곡 수정 화면의 내보내기에서 먼저 렌더링해 주세요.");
        return;
      }
      if (missing.length) {
        window.toast(`렌더링된 비디오가 없는 ${missing.length}곡은 제외됩니다: ${missing.map((s) => s.title || s.filename).join(", ")}`);
      }
    }

    const logPrefix = mode === "audio" ? "[Album Audio ZIP]" : "[Album Video ZIP]";
    setAlbumDownloadKind(mode);
    setAlbumDownloadBusy(true);
    setAlbumDownloadProgress(0);
    setAlbumDownloadStage("JSZip 로드 중…");
    try {
      window.toast(mode === "audio" ? "앨범 오디오 ZIP 준비 중…" : "앨범 비디오 ZIP 준비 중…");
      const JSZip = await loadAlbumJSZip();
      const zip = new JSZip();
      const albumBase = sanitizeAlbumEntryName(album.title || "album", "album");
      const audioFolder = mode === "audio" ? zip.folder("Audio") : null;
      const videoFolder = mode === "video" ? zip.folder("Video") : null;

      setAlbumDownloadProgress(12);
      setAlbumDownloadStage(mode === "audio" ? "오디오 파일 수집 준비…" : "비디오 파일 수집 준비…");

      const failedTitles = [];
      for (let index = 0; index < targetSongs.length; index += 1) {
        const song = targetSongs[index];
        const songBase = sanitizeAlbumEntryName(
          (song.filename || song.title || `track-${index + 1}`).replace(/\.[^.]+$/, ""),
          `track-${index + 1}`
        );
        const order = String(index + 1).padStart(2, "0");
        const collectionProgress = 15 + Math.floor((index / Math.max(targetSongs.length, 1)) * 52);
        setAlbumDownloadProgress(collectionProgress);
        if (mode === "audio") {
          setAlbumDownloadStage(`${song.title || songBase} 오디오 수집 중…`);
          try { audioFolder.file(`${order}_${song.filename || `${songBase}.mp3`}`, await loadAlbumSongAudioBlob(song)); }
          catch (e) { console.warn(logPrefix, "오디오 수집 실패", song.title, e); failedTitles.push(song.title || songBase); }
        } else {
          setAlbumDownloadStage(`${song.title || songBase} 비디오 수집 중…`);
          try { videoFolder.file(`${order}_${songBase}.mp4`, await loadAlbumSongVideoBlob(song)); }
          catch (e) { console.warn(logPrefix, "비디오 수집 실패", song.title, e); failedTitles.push(song.title || songBase); }
        }
      }
      if (failedTitles.length === targetSongs.length) {
        throw new Error("모든 파일 수집에 실패했습니다.");
      }
      if (failedTitles.length) {
        window.toast(`수집에 실패한 ${failedTitles.length}곡은 제외됩니다: ${failedTitles.join(", ")}`);
      }

      setAlbumDownloadProgress(74);
      setAlbumDownloadStage("zip 압축 중…");
      const blob = await zip.generateAsync(
        { type: "blob", compression: "DEFLATE", compressionOptions: { level: 6 } },
        (metadata) => { setAlbumDownloadProgress(Math.min(99, 74 + Math.round((metadata.percent || 0) * 0.25))); }
      );
      const filename = mode === "audio" ? `${albumBase}_audio.zip` : `${albumBase}_video.zip`;
      triggerAlbumBlobDownload(blob, filename);
      setAlbumDownloadProgress(100);
      setAlbumDownloadStage("다운로드 시작…");
      window.toast(`${filename} 다운로드 시작`);
    } catch (e) {
      console.error(`${logPrefix} 생성 실패:`, e);
      window.toast((mode === "audio" ? "앨범 오디오 ZIP" : "앨범 비디오 ZIP") + " 생성 실패: " + (e.message || e));
    } finally {
      setAlbumDownloadBusy(false);
      setAlbumDownloadKind(null);
      setTimeout(() => { setAlbumDownloadProgress(0); setAlbumDownloadStage(""); }, 800);
    }
  };

  const downloadAlbumAudioZip = () => downloadAlbumMediaZip("audio");
  const downloadAlbumVideoZip = () => downloadAlbumMediaZip("video");

  const downloadSongSnapshots = async () => {
    try {
      window.toast("앨범 Song ZIP 준비 중…");
      const filename = await downloadAlbumSongSnapshotsZip(album);
      window.toast(`${filename} 다운로드 시작`);
    } catch (error) {
      console.error("[Album Song ZIP] 생성 실패:", error);
      window.toast("앨범 Song ZIP 생성 실패: " + (error?.message || error));
      throw error;
    }
  };

  const downloadSongBackup = async (songId) => {
    const song = album.songs.find((item) => item.id === songId);
    if (!song || songBackupBusyId) return;
    setSongBackupBusyId(songId);
    try {
      window.toast(`"${song.title || song.filename || "Song"}" 백업 ZIP 준비 중…`);
      const { filename, manifest } = await downloadSongBackupZip({ song, album });
      const missingCount = manifest?.missing?.length || 0;
      window.toast(`${filename} 다운로드 시작${missingCount ? ` · 누락 ${missingCount}개 기록` : ""}`);
    } catch (error) {
      console.error("[Song Backup] 생성 실패:", error);
      window.toast("Song 백업 ZIP 생성 실패: " + (error?.message || error));
      throw error;
    } finally {
      setSongBackupBusyId(null);
    }
  };

  const uploadSongBackup = async (file) => {
    if (!file || songBackupUploadBusy) return;
    setSongBackupUploadBusy(true);
    try {
      window.toast("Song 백업 ZIP 복원 중…");
      const restoredSong = await restoreSongBackupZip(file, makeId);
      setAlbum((prev) => ({ ...prev, songs: [...prev.songs, restoredSong] }));
      setCurrentSongId(restoredSong.id);
      setView("hub");
      window.toast(`"${restoredSong.title || "복원한 Song"}" 복원 완료`);
    } catch (error) {
      console.error("[Song Backup] 복원 실패:", error);
      window.toast("Song 백업 ZIP 복원 실패: " + (error?.message || error));
    } finally {
      setSongBackupUploadBusy(false);
    }
  };

  const uploadAlbumZip = async (file) => {
    if (!file || albumUploadBusy) return;
    setAlbumUploadBusy(true);
    try {
      window.toast("앨범 Song ZIP 업로드 중…");
      const uploadedAlbum = await importAlbumSongSnapshotsZip(file, makeId);
      const cleanedAlbum = { ...uploadedAlbum };
      delete cleanedAlbum.share;
      delete cleanedAlbum.publicId;
      delete cleanedAlbum.publishedAt;
      delete cleanedAlbum.publicUpdatedAt;
      delete cleanedAlbum.myAlbumSavedAt;
      delete cleanedAlbum.publishedBy;

      const workspaceAlbumId = "album-" + makeId();
      let uploadedAlbumTitle = "";
      setAlbums((prev) => {
        const baseTitle = String(cleanedAlbum.title || "공유 앨범").trim() || "공유 앨범";
        const usedTitles = new Set(prev.map((item) => String(item?.title || "").trim()));
        let uniqueTitle = baseTitle;
        if (usedTitles.has(baseTitle)) {
          let index = 2;
          while (usedTitles.has(`${baseTitle} ${index}`)) index += 1;
          uniqueTitle = `${baseTitle} ${index}`;
        }

        const workspaceAlbum = normalizeAlbum({
          ...cleanedAlbum,
          id: workspaceAlbumId,
          title: uniqueTitle,
          isPublished: false,
          publicId: "",
          publishedAt: "",
          publicUpdatedAt: "",
          myAlbumSavedAt: "",
        });
        uploadedAlbumTitle = workspaceAlbum.title;
        return [...prev, workspaceAlbum];
      });
      setCurrentAlbumId(workspaceAlbumId);
      setCurrentSongId(null);
      setView("hub");
      window.toast(`"${uploadedAlbumTitle || "업로드한 앨범"}" 앨범 업로드 완료 · ${cleanedAlbum.songs?.length || 0}곡`);
    } catch (error) {
      console.error("[Album Upload] 실패:", error);
      window.toast("앨범 Song ZIP 업로드 실패: " + (error?.message || error));
    } finally {
      setAlbumUploadBusy(false);
    }
  };

  // ── Breadcrumb ───────────────────────────────────────────────────────────────

  const breadcrumb = () => {
    if (!album) return [{ label: "워크스페이스", current: true }];
    if (view === "public") return [{label: "공개 앨범", current: true}];
    if (view === "my-albums") return [{label: "내 앨범", current: true}];
    if (view === "hub") return [{label: album.title, current: true}];
    if (view === "viewer") return [
      { label: album.title, onClick: () => setView("hub") },
      { label: "앨범 플레이어", current: true },
    ];
    const s = album.songs.find(x => x.id === currentSongId);
    return [
      { label: album.title, onClick: () => setView("hub") },
      { label: s?.title || "노래", current: true },
    ];
  };

  const readySongCount = album ? album.songs.filter(s => s.status === "ready").length : 0;
  const totalSongCount = album ? album.songs.length : 0;

  // ── Render ───────────────────────────────────────────────────────────────────
  return (
    <div
      ref={appRootRef}
      className={`app-root view-${view} ${sidebarCollapsed ? "sidebar-collapsed" : ""} ${sidebarResizing ? "is-resizing" : ""}`}
      style={{ "--sidebar-width": sidebarWidth + "px" }}
      data-role="main-app-root"
    >
      {/* Sidebar */}
      <div className="sidebar" data-role="main-sidebar-panel">
        {!sidebarCollapsed && (
          <div
            className="sidebar-resize-handle"
            data-role="main-sidebar-resize-handle"
            onPointerDown={startSidebarResize}
            onDoubleClick={() => setSidebarWidth(SIDEBAR_DEFAULT_WIDTH)}
            role="separator"
            aria-orientation="vertical"
            title="드래그하여 사이드바 너비 조절 (더블클릭: 기본값)"
          />
        )}
        <div className="sidebar-brand" data-role="main-sidebar-brand-panel">
          <div className="logo" data-role="main-sidebar-brand-logo"/>
          <div className="sidebar-brand-copy" data-role="main-sidebar-brand-copy">
            <div className="sidebar-brand-name" data-role="main-sidebar-brand-name">Songfilm</div>
            <div className="sidebar-brand-sub" data-role="main-sidebar-brand-sub">studio · v0.3</div>
          </div>
          <button
            className="sidebar-toggle"
            data-role="main-sidebar-toggle-btn"
            onClick={() => setSidebarCollapsed(prev => !prev)}
            title={sidebarCollapsed ? "사이드바 펼치기" : "사이드바 접기"}
            aria-label={sidebarCollapsed ? "사이드바 펼치기" : "사이드바 접기"}
          >
            {sidebarCollapsed ? <Icon.ArrowRight size={14}/> : <Icon.ArrowLeft size={14}/>}
          </button>
        </div>

        {(() => {
          const auth = window.useAuth?.();
          if (!auth?.session || sidebarCollapsed) return null;
          const { session, logout } = auth;
          return (
            <div
              style={{
                display: "flex", alignItems: "center", justifyContent: "space-between",
                padding: "5px 10px 8px",
                borderBottom: "1px solid var(--line)",
              }}
              data-role="main-sidebar-session-panel"
            >
              <span
                style={{
                  fontSize: 11.5, color: "var(--accent)", fontWeight: 600,
                  overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap",
                  flex: 1, minWidth: 0,
                }}
              >
                {session.nickname}
                {session.userType === "admin" && (
                  <span style={{marginLeft: 5, fontSize: 10, color: "var(--ink-4)", fontWeight: 400}}>admin</span>
                )}
              </span>
              <button
                onClick={logout}
                data-role="main-sidebar-logout-btn"
                style={{
                  flexShrink: 0, marginLeft: 8,
                  background: "none", border: "1px solid var(--line-2)", borderRadius: 20,
                  padding: "2px 9px", fontSize: 10.5, color: "var(--ink-3)",
                  cursor: "pointer", fontFamily: "inherit",
                  transition: "border-color 0.15s, color 0.15s", lineHeight: 1.6,
                }}
                onMouseEnter={e => { e.currentTarget.style.borderColor = "var(--ink-3)"; e.currentTarget.style.color = "var(--ink)"; }}
                onMouseLeave={e => { e.currentTarget.style.borderColor = "var(--line-2)"; e.currentTarget.style.color = "var(--ink-3)"; }}
              >
                logout
              </button>
            </div>
          );
        })()}

        <div className="nav-section" data-role="main-sidebar-view-section-label">뷰</div>
        <div className={`nav-item ${view === "hub" ? "active" : ""}`} data-role="main-sidebar-nav-hub-btn" onClick={() => setView("hub")} data-tooltip="앨범 홈">
          <Icon.Home size={14}/> <span>앨범 홈</span>
        </div>
        <div className={`nav-item ${view === "viewer" ? "active" : ""}`} data-role="main-sidebar-nav-viewer-btn" onClick={openViewerLibrary} data-tooltip="앨범 플레이어">
          <Icon.Player size={14}/> <span>앨범 플레이어</span>
        </div>
        <div className={`nav-item ${view === "public" ? "active" : ""}`} data-role="main-sidebar-nav-public-btn" onClick={openPublicAlbums} data-tooltip="공개 앨범">
          <Icon.Album size={14}/> <span>공개 앨범</span>
        </div>
        {window.SongfilmAuth.getSession() && (
          <div className={`nav-item ${view === "my-albums" ? "active" : ""}`} data-role="main-sidebar-nav-my-albums-btn" onClick={() => setView("my-albums")} data-tooltip="내 앨범">
            <Icon.Upload size={14}/> <span>내 앨범</span>
          </div>
        )}

        <div className="nav-section" data-role="main-sidebar-workspace-section-label">워크스페이스 ({albums.length})</div>
        {albums.map(a => {
          const isCurrent = a.id === album.id;
          const isFolded = sidebarFoldedAlbumIds.includes(a.id);
          return (
            <React.Fragment key={a.id}>
              <div
                className={`nav-item library-album-item can-delete ${isCurrent && view === "hub" ? "active" : ""}${sidebarSongDropAlbumId === a.id ? " song-drop-target" : ""}`}
                data-role="main-sidebar-album-item"
                onClick={() => switchAlbum(a.id)}
                onDragOverCapture={handleLibraryAlbumDragOverCapture}
                onDragEnter={(e) => handleLibraryAlbumDragEnter(e, a.id)}
                onDragLeave={(e) => handleLibraryAlbumDragLeave(e, a.id)}
                onDrop={(e) => handleLibraryAlbumDrop(e, a.id)}
                style={{fontSize: 13, fontWeight: isCurrent ? 600 : 500}}
                data-tooltip={a.title}
              >
                <button
                  className="library-album-fold"
                  data-role="main-sidebar-album-fold-btn"
                  onClick={(e) => {
                    e.stopPropagation();
                    if (!isCurrent) {
                      switchAlbum(a.id);
                      return;
                    }
                    setSidebarFoldedAlbumIds((prev) => (
                      prev.includes(a.id) ? prev.filter((albumId) => albumId !== a.id) : [...prev, a.id]
                    ));
                  }}
                  title={isFolded ? "앨범 펼치기" : "앨범 접기"}
                  aria-label={`${a.title} ${isFolded ? "펼치기" : "접기"}`}
                >
                  {isFolded ? <Icon.ChevronRight size={12}/> : <Icon.ChevronDown size={12}/>}
                </button>
                <span
                  title={a.title}
                  style={{overflow:"hidden", textOverflow:"ellipsis", whiteSpace:"nowrap", flex: 1, color: isCurrent ? "var(--ink)" : "var(--ink-2)"}}
                >
                  {a.title}
                </span>
                <span className="library-album-action-slot">
                  <span className="library-album-count" data-role="main-sidebar-album-song-count">{a.songs.length}</span>
                  <button
                    className="library-album-delete"
                    data-role="main-sidebar-album-delete-btn"
                    onClick={(e) => { e.stopPropagation(); deleteAlbum(a.id); }}
                    title="앨범 삭제"
                    aria-label={`${a.title} 앨범 삭제`}
                  >
                    <Icon.Trash size={12}/>
                  </button>
                </span>
              </div>

              {isCurrent && !isFolded && (
                <div style={{marginLeft: 14, paddingLeft: 10, borderLeft: "1px solid var(--line)"}} data-role="main-sidebar-song-list-panel">
                  {a.songs.length === 0 && (
                    <div style={{padding:"6px 10px", fontSize: 11.5, color:"var(--ink-4)", fontStyle:"italic"}} data-role="main-sidebar-song-empty-state">노래 없음</div>
                  )}
                  {a.songs.map(s => (
                    <div key={s.id}
                      className={`nav-item ${view === "creator" && currentSongId === s.id ? "active" : ""}`}
                      data-role="main-sidebar-song-item"
                      onClick={() => openSongFromAlbum(s)}
                      style={{fontSize: 12}}
                    >
                      <span
                        className="dot"
                        style={{
                          background: s.status === "ready"
                            ? (window.isSongRenderDirty?.(s) ? "oklch(72% 0.18 62)" : "oklch(72% 0.18 145)")
                            : s.status === "draft" ? "var(--accent-3)" : "var(--ink-4)",
                        }}
                      />
                      <span style={{overflow:"hidden", textOverflow:"ellipsis", whiteSpace:"nowrap"}}>{s.title}</span>
                    </div>
                  ))}
                  <div className="nav-item" data-role="main-sidebar-new-song-btn" onClick={newSong} style={{color:"var(--ink-3)", fontSize: 12}}>
                    <Icon.Plus size={11}/> 새 노래
                  </div>
                </div>
              )}
            </React.Fragment>
          );
        })}

        <div className="nav-item" data-role="main-sidebar-new-album-btn" onClick={() => setNewAlbumModal(true)} style={{color:"var(--ink-3)", marginTop: 6}}>
          <Icon.Plus size={12}/> <span>새 앨범</span>
        </div>
        <div className="nav-item" data-role="main-sidebar-share-link-copy-btn" onClick={() => setShareLinkCopyModal(true)} style={{color:"var(--ink-3)"}}>
          <Icon.Link size={12}/> <span>공유링크앨범복사</span>
        </div>
        <div
          className={`nav-item ${albumUploadBusy ? "disabled" : ""}`}
          data-role="main-sidebar-album-zip-upload-btn"
          onClick={() => !albumUploadBusy && albumUploadInputRef.current?.click()}
          style={{color:"var(--ink-3)"}}
        >
          <Icon.Upload size={12}/> <span>{albumUploadBusy ? "앨범 Song ZIP 업로드 중…" : "앨범 Song ZIP 업로드"}</span>
        </div>
        <div className="nav-item" data-role="main-sidebar-import-my-album-btn" onClick={openImportMyAlbumModal} style={{color:"var(--ink-3)"}}>
          <Icon.Download size={12}/> <span>내 앨범에서 가져오기</span>
        </div>
        <input
          ref={albumUploadInputRef}
          data-role="main-sidebar-album-zip-file-input"
          type="file"
          accept=".zip,application/zip,application/x-zip-compressed"
          hidden
          onChange={(event) => {
            const file = event.target.files?.[0];
            event.target.value = "";
            if (file) uploadAlbumZip(file);
          }}
        />

        <div className="sidebar-spacer"/>
        <div className="sidebar-footer" data-role="main-sidebar-footer-panel">
          <button type="button" className="sidebar-guide-link" data-role="main-sidebar-quick-guide-btn" onClick={() => setQuickGuideModal(true)}>
            Quick Guide
          </button>
          <div className="sidebar-signature" data-role="main-sidebar-signature">SongFilm by Still Coding</div>
        </div>
      </div>

      {/* Main */}
      <div className="main" data-role="main-content-root">
        <div className="topbar" data-role="main-topbar-panel">
          <div className="breadcrumb" data-role="main-topbar-breadcrumb">
            {breadcrumb().map((b, i, arr) => (
              <React.Fragment key={i}>
                <span className={`breadcrumb-item ${b.current ? "current" : ""}`} data-role="main-topbar-breadcrumb-item" onClick={b.onClick} style={{cursor: b.onClick ? "pointer" : "default"}}>
                  {b.label}
                </span>
                {i < arr.length - 1 && <span className="sep" data-role="main-topbar-breadcrumb-separator">/</span>}
              </React.Fragment>
            ))}
          </div>
          <div className="topbar-right" data-role="main-topbar-actions-panel">
            <div
              className="topbar-count"
              data-role="main-topbar-count"
              title={`완성곡 ${readySongCount} / 전체곡 ${totalSongCount}`}
              aria-label={`완성곡 ${readySongCount} / 전체곡 ${totalSongCount}`}
              style={{fontSize: 11.5, color:"var(--ink-4)", fontFamily:"var(--font-mono)"}}
            >
              {view === "public"
                ? `${publicAlbums.length} 공개 앨범`
                : view === "my-albums"
                  ? `${myAlbums.length} 내 앨범`
                  : `${readySongCount}/${totalSongCount}`}
            </div>
            {view !== "public" && (
              <button className="pill-btn topbar-icon-btn" data-role="main-topbar-new-album-btn" onClick={() => setNewAlbumModal(true)}>
                <Icon.Plus size={11}/> <span className="btn-label">새 앨범</span>
              </button>
            )}
            {view !== "viewer" && view !== "public" && readySongCount > 0 && album && (
              <button className="pill-btn accent topbar-icon-btn" data-role="main-topbar-play-album-btn" onClick={() => openViewer(null, { albumId: album.id })}>
                <Icon.Play size={11}/> <span className="btn-label">앨범 재생</span>
              </button>
            )}
          </div>
        </div>

        <div className="view" data-role="main-view-panel">
          {view === "hub" && album && (
            <div data-role="main-view-hub-panel">
              <AlbumHub
                album={album} setAlbum={setAlbum}
                onOpenSong={openSong}
                onNewSong={newSong}
                onOpenViewer={(songId = null) => openViewer(songId, { albumId: album.id })}
                onBatchRenderVideos={openBatchRenderDialog}
                onDownloadAlbumAudio={downloadAlbumAudioZip}
                onDownloadAlbumVideo={downloadAlbumVideoZip}
                albumDownloadKind={albumDownloadKind}
                onDownloadSongSnapshots={downloadSongSnapshots}
                onDownloadSongBackup={downloadSongBackup}
                onUploadSongBackup={uploadSongBackup}
                songBackupBusyId={songBackupBusyId}
                songBackupUploadBusy={songBackupUploadBusy}
                batchRenderRunning={batchRenderState.running}
                albumDownloadBusy={albumDownloadBusy}
                albumDownloadProgress={albumDownloadProgress}
                albumDownloadStage={albumDownloadStage}
                onPublishAlbum={publishAlbum}
                onUnpublishAlbum={unpublishAlbum}
                publishingAlbum={publishingAlbum}
                onSaveToMyAlbums={window.SongfilmAuth.getSession() ? saveToMyAlbums : undefined}
                savingToMyAlbums={savingToMyAlbums}
              />
            </div>
          )}
          {view === "hub" && !album && (
            <div className="empty-state" data-role="main-view-empty-state">
              워크스페이스에 앨범이 없습니다. 사이드바의 "새 앨범"으로 시작해 주세요.
            </div>
          )}
          {view === "public" && (
            <div data-role="main-view-public-panel">
              <PublicAlbumsView
                albums={publicAlbums}
                selectedAlbumId={selectedPublicAlbumId}
                loading={publicAlbumsLoading}
                error={publicAlbumsError}
                onRefresh={() => loadPublicAlbums()}
                onSelectAlbum={setSelectedPublicAlbumId}
                onExit={() => setView("hub")}
                onCopyToWorkspace={copyToWorkspace}
                onUnpublishAlbum={unpublishPublicAlbum}
              />
            </div>
          )}
          {view === "my-albums" && (
            <div data-role="main-view-my-albums-panel">
              <MyAlbumsView
                albums={myAlbums}
                selectedAlbumId={selectedMyAlbumId}
                loading={myAlbumsLoading}
                error={myAlbumsError}
                onRefresh={loadMyAlbums}
                onSelectAlbum={setSelectedMyAlbumId}
                onExit={() => setView("hub")}
                onDeleteAlbum={deleteFromMyAlbums}
                onCopyToWorkspace={copyToWorkspace}
              />
            </div>
          )}
          {(view === "creator" || view === "lyric-editor") && currentSongId && album && (
            <div data-role="main-view-creator-panel">
              <Creator
                album={album} setAlbum={setAlbum}
                songId={currentSongId}
                onBack={() => setView("hub")}
                onExit={() => setView("hub")}
                onOpenLyricEditor={() => openLyricEditor(currentSongId)}
              />
            </div>
          )}
          {view === "viewer" && (
            <div data-role="main-view-viewer-panel">
              <LocalAlbumsPlayerView
                albums={albums}
                initialAlbumId={viewerAlbumId}
                initialSongId={viewerSongId}
                onExit={() => setView("hub")}
              />
            </div>
          )}
        </div>
      </div>

      {tweaksOn && <TweaksPanel tweaks={tweaks} setTweaks={setTweaks}/>} 
      {newAlbumModal && <NewAlbumModal onCreate={createAlbum} onClose={() => setNewAlbumModal(false)}/>} 
      {shareLinkCopyModal && (
        <ShareLinkAlbumCopyModal
          onCopy={copySharedLinkAlbumToWorkspace}
          onClose={() => setShareLinkCopyModal(false)}
        />
      )}
      {importMyAlbumModal && (
        <ImportMyAlbumModal
          albums={myAlbums}
          loading={myAlbumsLoading}
          error={myAlbumsError}
          onRefresh={loadMyAlbums}
          onImport={importMyAlbumToWorkspace}
          onClose={() => setImportMyAlbumModal(false)}
        />
      )}
      {quickGuideModal && <QuickGuideModal onClose={closeQuickGuideModal}/>} 
      {batchRenderState.open && (
        <BatchRenderModal
          state={batchRenderState}
          onStart={startBatchRenderVideos}
          onStop={requestStopBatchRender}
          onClose={closeBatchRenderDialog}
          onToggleSong={toggleBatchRenderSong}
          onSelectAll={selectAllBatchRenderSongs}
          onClearSelection={clearBatchRenderSongs}
          onForceRerenderChange={setBatchRenderForceRerender}
        />
      )}
      <ToastHost/>
      {view === "lyric-editor" && currentSongId && album && (() => {
        const { LyricEditor } = window;
        const song = album.songs.find(s => s.id === currentSongId);
        if (!song || !LyricEditor) return null;
        return (
          <div data-role="main-view-lyric-editor-dialog-panel">
            <LyricEditor
              song={song}
              onSave={(updatedSong, saveMeta = {}) => {
                setAlbum(prev => ({
                  ...prev,
                  songs: prev.songs.map(s => {
                    if (s.id !== updatedSong.id) return s;
                    const dirtyPatch = saveMeta.dirty ? (window.markSongRenderDirty?.(s, "lyrics-editor") || {}) : {};
                    return { ...updatedSong, ...dirtyPatch };
                  }),
                }));
                setView("creator");
              }}
              onExit={() => setView("creator")}
            />
          </div>
        );
      })()}
    </div>
  );
}

ReactDOM.createRoot(document.getElementById("root")).render(
  <AuthGate>
    <App/>
  </AuthGate>
);
