/* ============================================================
   mode-b.jsx — B 타입: 상품 N장 → AI 합성 1장 → 4종 배너
   ============================================================
   흐름:
     1) 상품 이미지 N장 업로드
     2) AI(Gemini)가 N장을 하나의 정방형 합성 이미지로 만듦
     3) 그 합성 이미지가 A타입의 source 역할 → 4종 배너 outpaint
   ============================================================ */

const { useState, useEffect, useRef, useCallback } = React;

const SPECS_OUTPAINT = window.BannerGen.BANNER_SPECS;

/* The composed hero image is itself the 5th banner (1:1) — included in export
   as-is, no outpainting needed. Treated as a "synthetic" spec so the UI can
   show it alongside the 4 outpainted variants. */
const HERO_SPEC = {
  id: "HeroSquare",
  name: "Hero · 합성본 (1:1)",
  w: 1024, h: 1024,
  allowed: { x: 0.05, y: 0.05, w: 0.9, h: 0.9 },
  anchor:  { x: 0.5, y: 0.5 },
  rule:    "Composed hero — included as-is",
  negSpace:"",
};
const SPECS_B = [HERO_SPEC, ...SPECS_OUTPAINT];

const TWEAK_DEFAULTS_B = /*EDITMODE-BEGIN-B*/{
  "variation": "natural",
  "canvasScale": 0.55,
  "showZones": false,
  "showAnchors": true,
  "showBbox": false,
  "grainEnabled": true,
  "apiEnabled": false,
  "apiEndpoint": "",
  "pipelineMode": "outpaint",
  "safePadding": 0,
  "fitMode": "contain"
}/*EDITMODE-END-B*/;

/* ============================================================
   COMPOSE PROMPT — 베이스 + 연출 디테일
   ============================================================
   베이스: 매번 똑같이 적용되는 시스템 레벨 규칙 (제품 보존, 렌더링
          스타일, 라벨 텍스트 보존 등). 자주 바꾸지 않음.
   연출: 매 작업마다 바뀔 수 있는 구도 디렉션 (상품 배치, 그림자,
        분위기 등). 사용자가 자주 편집하는 영역.
   호출 시 두 텍스트를 합쳐서 Gemini 에 보낸다.
   ============================================================ */
// Vertical compose prompt — only priority-marked products
const COMPOSE_VERTICAL_DEFAULT = `[제품 보존 (Product Integrity) — 가장 중요]
업로드된 이미지의 형태(Shape), 비율(Proportion), 구조(Structure), 로고 및 CMF(Color, Material, Finish)를 0% 변형(Zero-Deformation) 조건으로 유지할 것. 재해석이나 스타일라이즈를 엄격히 금지함. 상품을 길게 늘리거나 짧게 줄이거나 비율을 임의로 바꾸지 마라. 원본 이미지의 비율을 반드시 100% 유지.

화질개선. 4K화질로 개선.
상품의 세세한 부분들 모두 그대로 유지. 상품이 선명해지도록 개선.
금지 사항: 절대로 상품을 임의로 변경 하지마.
반드시 한국어 텍스트, 영어 텍스트 모두 원본 그대로 유지.
제품 본연의 색도(Color Fidelity)에 간섭하지 않는 5500K 중립 조명 사용. 입체감이 강조된 램브란트 조명 각도. 하이라이팅 강조.

[CRITICAL: 한글 지침 보존 및 반영 원칙 (Text Preservation Protocol)]
한글 텍스트 유지: 본 프롬프트에 포함된 모든 한글 설명은 AI의 임의 번역, 변형, 생략 없이 기재된 원문 그대로의 의미를 최종 결과물에 100% 반영해야 함.
지침 준수: 한글로 기술된 세부 묘사와 분위기는 영문 지시어보다 우선하는 가이드라인으로 작용함.
All label text must remain exactly identical to the reference image. Text is part of the original packaging artwork.
Do not regenerate, reinterpret, correct, enhance or stylize any text.
No AI-generated typography.

[렌더링 스타일 — Rendering Style]
하이엔드 커머셜 스튜디오 사진 (High-end Commercial Photography). 8K 해상도의 사실적인 텍스처, 정교한 레이 트레이싱(Ray-tracing) 기반의 반사 및 하이라이트 구현.

[배경 톤 — Background Tone (Hero 와 정확히 일치)]
배경 톤·색상·분위기는 메인 합성본(Hero, 1:1 이미지) 과 동일해야 함.
배경색은 메인 합성과 동일한 톤을 정확히 매칭. 다른 색이나 분위기 절대 금지.
5500K 중립 조명. 제품 Color Fidelity 보존.

Overview (전체 형상 및 공간감)
- Concept: 제품의 아이덴티티를 보여주는 히어로 샷(Hero Shots).
- Composition: 중앙 정렬, 부유하는 듯한 플로팅(Floating) 레이아웃, 안정적인 여백(Negative Space) 확보.
- Lighting: 부드러운 소프트박스 조명을 활용한 전체적인 입체감 강조.

한 장에 상품들이 어우러져 있는 연출. 일렬로 배치 금지.
상품을 선명하게 보정. 상품이 공중에 떠있음. 연한 그림자.

[금지사항 재강조]
- 상품 비율 변경 금지 (NO aspect ratio change)
- 상품 늘리기·줄이기·찌그러뜨리기 금지 (NO stretch / squash / distortion)
- 원본과 다른 배경 톤 사용 금지 (background tone must match Hero composition exactly)`;

const COMPOSE_BASE_DEFAULT = `[핵심 원칙: 제품 무결성 및 환경 설정]
1. 제품 보존 (Product Integrity): 업로드된 이미지의 형태(Shape), 비율(Proportion), 구조(Structure), 로고 및 CMF(Color, Material, Finish)를 0% 변형(Zero-Deformation) 조건으로 유지할 것. 재해석이나 스타일라이즈를 엄격히 금지함.
2. 렌더링 스타일 (Rendering Style): 하이엔드 커머셜 스튜디오 사진(High-end Commercial Photography). 8K 해상도의 사실적인 텍스처, 정교한 레이 트레이싱(Ray-tracing) 기반의 반사 및 하이라이트 구현.
3. 배경 및 조명 (Background & Lighting): 배경색은 고정값 #E8E9ED를 기준으로 톤온톤(Tone-on-tone) 구성. 제품 본연의 색도(Color Fidelity)에 간섭하지 않는 5500K 중립 조명 사용.

Overview (전체 형상 및 공간감)
- Concept: 제품의 아이덴티티를 보여주는 히어로 샷(Hero Shots).
- Composition: 중앙 정렬, 안정적으로 바닥에 안착한 레이아웃, 안정적인 여백(Negative Space) 확보.
- Lighting: 부드러운 소프트박스 조명을 활용한 전체적인 입체감 강조.

8K + photorealistic + clean + no noise.
DO NOT modify any other elements of the image.

All label text must remain exactly identical to the reference image. Text is part of the original packaging artwork.
Do not regenerate, reinterpret, correct, enhance or stylize any text. No AI-generated typography.

제품을 변형하지 말것.
생략 없이 제품 전체를 그대로 구현할 것.`;

const COMPOSE_STAGING_DEFAULT = `한 장에 상품들이 어우러져 있는 연출. 일렬로 배치 금지.
상품을 선명하게 보정. 연한 그림자.`;

// Combined default (for back-compat with the legacy single-field state).
const COMPOSE_PROMPT_DEFAULT = COMPOSE_BASE_DEFAULT + "\n\n[연출 디테일]\n" + COMPOSE_STAGING_DEFAULT;

function ModeBApp({ onBackToEntry }) {
  /* products: [{ id, name, file, dataUrl, img, dim }] */
  const [products, setProducts] = useState([]);
  /* composed: { dataUrl, img, dim } | null */
  const [composed, setComposed] = useState(null);
  /* composedVertical: 우선순위 상품만으로 만든 세로형 전용 합성본 */
  const [composedVertical, setComposedVertical] = useState(null);
  const [composeVerticalLoading, setComposeVerticalLoading] = useState(false);
  const [composeLoading, setComposeLoading] = useState(false);
  const [composeError, setComposeError] = useState(null);
  // 베이스(시스템 규칙) + 연출(자주 변경) 으로 분리. 호출 시 합쳐서 전송.
  const [composeBase, setComposeBase] = useState(COMPOSE_BASE_DEFAULT);
  const [composeStaging, setComposeStaging] = useState(COMPOSE_STAGING_DEFAULT);
  const composePrompt = composeBase.trim()
    + (composeStaging.trim() ? "\n\n[연출 디테일]\n" + composeStaging.trim() : "");
  const baseIsDefault    = composeBase.trim()    === COMPOSE_BASE_DEFAULT.trim();
  const stagingIsDefault = composeStaging.trim() === COMPOSE_STAGING_DEFAULT.trim();
  // User must explicitly approve the composed image before we burn credits on
  // the 4 outpaint calls. Resets to false whenever compose runs again.
  const [composeApproved, setComposeApproved] = useState(false);
  // Show prompt editor inline below the preview when user clicks "수정"
  const [showPromptEditor, setShowPromptEditor] = useState(false);

  /* After composition, becomes the A-type source pipeline */
  const [shotSpec, setShotSpec] = useState(null);
  const [specLoading, setSpecLoading] = useState(false);
  const [banners, setBanners] = useState(() =>
    SPECS_B.map(s => ({
      id: s.id, status: "idle", progress: 0,
      dataUrl: null, blob: null,
      anchor: { ...s.anchor }, scale: 1.0, lastSource: null, lastSpec: null,
    }))
  );

  const [tab, setTab] = useState("upload"); // upload | compose | spec | banners | api
  const [selectedId, setSelectedId] = useState(null);
  const [exporting, setExporting] = useState(false);
  const [tweaks, setTweak] = window.useTweaks(TWEAK_DEFAULTS_B);

  const Icon = window.Icon;
  const apiConfig = { enabled: tweaks.apiEnabled, endpoint: tweaks.apiEndpoint };

  /* ---- Add product files ---- */
  const addFiles = useCallback(async (files) => {
    const valid = (Array.from(files || [])).filter(f => f.type.startsWith("image/"));
    if (!valid.length) return;
    const loaded = await Promise.all(valid.map(async (file) => {
      const dataUrl = await new Promise((res, rej) => {
        const r = new FileReader();
        r.onload = () => res(r.result); r.onerror = rej;
        r.readAsDataURL(file);
      });
      const img = await new Promise((res, rej) => {
        const i = new Image();
        i.onload = () => res(i); i.onerror = rej;
        i.src = dataUrl;
      });
      return {
        id: "prod_" + Math.random().toString(36).slice(2, 9),
        name: file.name, file, dataUrl, img,
        dim: { w: img.naturalWidth, h: img.naturalHeight },
        priority: false,
      };
    }));
    setProducts(prev => [...prev, ...loaded]);
    // New uploads invalidate previous composition.
    setComposed(null); setShotSpec(null);
    setBanners(SPECS_B.map(s => ({
      id: s.id, status: "idle", progress: 0,
      dataUrl: null, blob: null,
      anchor: { ...s.anchor }, scale: 1.0, lastSource: null, lastSpec: null,
    })));
    if (tab === "upload" && loaded.length) setTab("compose");
  }, [tab]);

  const removeProduct = useCallback((id) => {
    setProducts(prev => prev.filter(p => p.id !== id));
    setComposed(null); setShotSpec(null);
  }, []);

  /* ---- Toggle product priority ---- */
  const togglePriority = useCallback((id) => {
    setProducts(prev => prev.map(p => p.id === id ? { ...p, priority: !p.priority } : p));
    setComposed(null); // invalidate composition when priority changes
  }, []);

  /* ---- Compose: N products → 1 image ---- */
  const runCompose = useCallback(async () => {
    if (!products.length) return;
    setComposeLoading(true);
    setComposeError(null);
    setComposeApproved(false);
    try {
      // 중요도 표시된 상품을 prompt 에 명시 (Gemini 가 그 상품을 더 부각하도록 힌트)
      const priorityNames = products.filter(p => p.priority).map((p, i) => p.name).join(", ");
      const promptWithPriority = priorityNames
        ? composePrompt + `\n\n[중요도 높음 — 다음 상품을 더 부각하고 중앙·전면에 배치할 것]\n${priorityNames}`
        : composePrompt;
      const dataUrl = await window.BannerGen.composeProducts(
        products.map(p => p.dataUrl),
        { apiConfig, prompt: promptWithPriority, width: 1024, height: 1024 },
      );
      const img = await new Promise((res, rej) => {
        const i = new Image();
        i.onload = () => res(i); i.onerror = rej;
        i.src = dataUrl;
      });
      // 중요도 표시 상품이 있으면 별도의 vertical compose 도 생성 (세로형 배너 전용)
      const priorityProducts = products.filter(p => p.priority);
      if (priorityProducts.length > 0 && apiConfig.enabled) {
        setComposeVerticalLoading(true);
        try {
          const vUrl = await window.BannerGen.composeProducts(
            priorityProducts.map(p => p.dataUrl),
            { apiConfig, prompt: COMPOSE_VERTICAL_DEFAULT, width: 1024, height: 1024 },
          );
          const vImg = await new Promise((res, rej) => {
            const i = new Image();
            i.onload = () => res(i); i.onerror = rej;
            i.src = vUrl;
          });
          setComposedVertical({ dataUrl: vUrl, img: vImg, dim: { w: vImg.naturalWidth, h: vImg.naturalHeight }});
        } catch (e) {
          console.warn("vertical compose failed", e);
        } finally {
          setComposeVerticalLoading(false);
        }
      } else {
        setComposedVertical(null);
      }
      const composedBlob = await (await fetch(dataUrl)).blob();
      setComposed({ dataUrl, img, dim: { w: img.naturalWidth, h: img.naturalHeight }});
      // Reset downstream; hero banner is marked done immediately since
      // the composed image *is* the hero banner.
      setShotSpec(null);
      setBanners(SPECS_B.map(s => s.id === HERO_SPEC.id
        ? {
            id: s.id, status: "done", progress: 100,
            dataUrl, blob: composedBlob,
            anchor: { ...s.anchor }, scale: 1.0,
            lastSource: "compose", lastSpec: "hero",
          }
        : {
            id: s.id, status: "idle", progress: 0,
            dataUrl: null, blob: null,
            anchor: { ...s.anchor }, scale: 1.0, lastSource: null, lastSpec: null,
          }
      ));
      setShowPromptEditor(false);
    } catch (e) {
      console.error("compose failed", e);
      setComposeError(e.message || "합성 실패");
    } finally {
      setComposeLoading(false);
    }
  }, [products, composeBase, composeStaging, apiConfig]);

  /* ---- Approve composed image → unlock batch outpaint ---- */
  const approveCompose = useCallback(() => {
    setComposeApproved(true);
    setTab("banners");
  }, []);

  /* ---- Auto-generate shot_spec after composed image ready ---- */
  useEffect(() => {
    if (!composed || shotSpec) return;
    let cancelled = false;
    setSpecLoading(true);
    window.BannerGen.generateShotSpec(composed.dataUrl).then(s => {
      if (cancelled) return;
      setShotSpec(s); setSpecLoading(false);
    });
    return () => { cancelled = true; };
  }, [composed, shotSpec]);

  /* ---- Generate one banner (hybrid by default, legacy on opt-in) ---- */
  const generateOne = useCallback(async (specId, opts = {}) => {
    if (!composed) return;
    const spec = SPECS_B.find(s => s.id === specId);
    if (specId === HERO_SPEC.id) return; // hero is already done from compose
    const current = banners.find(b => b.id === specId);
    setBanners(prev => prev.map(b => b.id === specId ? { ...b, status: "gen", progress: 0, dataUrl: null } : b));
    try {
      // -------- HYBRID PIPELINE (default) --------
      if (tweaks.pipelineMode === "hybrid") {
        const { canvas, source: gSource, crop } =
          await window.BannerGen.generateBannerFromMaster(composed.img, spec, {
            grain: tweaks.grainEnabled,
            onProgress: (p) => setBanners(prev => prev.map(b => b.id === specId ? { ...b, progress: p } : b)),
          });
        const blob = await window.BannerGen.canvasToBlob(canvas);
        const dataUrl = canvas.toDataURL("image/png");
        const snap = "hybrid:" + spec.id;
        setBanners(prev => prev.map(b =>
          b.id === specId
            ? { ...b, status: "done", progress: 100, dataUrl, blob, lastSource: gSource, lastSpec: snap, lastCrop: crop }
            : b
        ));
        return;
      }

      // -------- LEGACY: per-size AI outpaint --------
      // Ranking, Promo (세로형) → composedVertical 이 있으면 그걸 source 로
      const isVertical = specId === "PromotionRankingBanner" || specId === "BeautyPromoBanner";
      const sourceComp = (isVertical && composedVertical) ? composedVertical : composed;
      // 이전 결과 있으면 base 로 사용 — 재호출 시 placement 안 하고 변경분만
      const inputDataUrl = current && current.dataUrl ? current.dataUrl : null;
      const { canvas, source: gSource } = await window.BannerGen.generateBanner(
        sourceComp.img, sourceComp.dataUrl, spec, shotSpec,
        {
          apiConfig,
          variation: opts.variation || tweaks.variation,
          anchor: opts.anchor || current.anchor,
          scale: opts.scale != null ? opts.scale : current.scale,
          productMargin: 1 - (tweaks.safePadding || 15) / 100,
          fitMode: "contain", // aspect-preserving; cover 비활성
          inputDataUrl,
          grain: tweaks.grainEnabled,
          onProgress: (p) => setBanners(prev => prev.map(b => b.id === specId ? { ...b, progress: p } : b)),
        }
      );
      const blob = await window.BannerGen.canvasToBlob(canvas);
      const dataUrl = canvas.toDataURL("image/png");
      const snap = JSON.stringify({ shared: shotSpec?.shared || null, shot: shotSpec?.shots?.find(s => s.banner === specId) || null, variation: opts.variation || tweaks.variation });
      setBanners(prev => prev.map(b =>
        b.id === specId
          ? { ...b, status: "done", progress: 100, dataUrl, blob, lastSource: gSource, lastSpec: snap }
          : b
      ));
    } catch (e) {
      console.error("ModeB generateBanner failed", e);
      setBanners(prev => prev.map(b => b.id === specId ? { ...b, status: "err", progress: 0, errorMsg: e.message || "AI 호출 실패" } : b));
    }
  }, [composed, banners, shotSpec, tweaks, apiConfig]);

  /* ---- Sequential gen (skips HERO — it's already done from compose) ---- */
  const seqRef = useRef(false);
  const OUTPAINT_SPECS = SPECS_OUTPAINT; // hero excluded
  const generateAllSequentially = useCallback(async (specs = OUTPAINT_SPECS) => {
    if (seqRef.current) return;
    seqRef.current = true;
    try {
      for (let i = 0; i < specs.length; i++) {
        if (specs[i].id === HERO_SPEC.id) continue;
        await generateOne(specs[i].id);
        // Hybrid runs locally, no rate-limit delay needed
        const delay = tweaks.pipelineMode === "hybrid" ? 0 : 12000;
        if (i < specs.length - 1 && delay > 0) await new Promise(r => setTimeout(r, delay));
      }
    } finally {
      seqRef.current = false;
    }
  }, [generateOne]);

  const generateChangedOnly = useCallback(async () => {
    if (seqRef.current) return;
    const changed = OUTPAINT_SPECS.filter(s => {
      const b = banners.find(x => x.id === s.id);
      if (!b || b.status !== "done") return true;
      const snap = JSON.stringify({ shared: shotSpec?.shared || null, shot: shotSpec?.shots?.find(x => x.banner === s.id) || null, variation: tweaks.variation });
      return snap !== b.lastSpec;
    });
    if (!changed.length) return;
    await generateAllSequentially(changed);
  }, [banners, shotSpec, tweaks.variation, generateAllSequentially]);

  /* ---- Generate ONE NEXT outpaint banner (선택된 배너 우선) ---- */
  const generateNextOne = useCallback(async () => {
    if (seqRef.current) return;
    const snapFor = (specId) => JSON.stringify({
      shared: shotSpec?.shared || null,
      shot:   shotSpec?.shots?.find(x => x.banner === specId) || null,
      variation: tweaks.variation,
    });
    const isPending = (b, specId) => {
      if (!b || b.status === "gen") return false;
      if (b.status !== "done") return true;
      return snapFor(specId) !== b.lastSpec;
    };
    // 1) 선택된 배너 우선 (단 HERO 는 제외)
    if (selectedId && selectedId !== HERO_SPEC.id) {
      const sel = banners.find(x => x.id === selectedId);
      if (isPending(sel, selectedId)) {
        await generateOne(selectedId);
        return;
      }
    }
    // 2) 아니면 OUTPAINT_SPECS 순서대로 첫 pending
    const pending = OUTPAINT_SPECS.find(s => isPending(banners.find(x => x.id === s.id), s.id));
    if (!pending) return;
    await generateOne(pending.id);
  }, [banners, shotSpec, tweaks.variation, generateOne, selectedId]);

  /* ---- Anchor drag handler ---- */
  const anchorDebounce = useRef({});
  const onAnchorChange = useCallback((id, anchor) => {
    setBanners(prev => prev.map(b => b.id === id ? { ...b, anchor } : b));
    if (anchorDebounce.current[id]) clearTimeout(anchorDebounce.current[id]);
    anchorDebounce.current[id] = setTimeout(() => generateOne(id, { anchor }), 400);
  }, [generateOne]);

  /* ---- Export ZIP ---- */
  const handleExport = useCallback(async () => {
    if (!banners.every(b => b.blob)) return;
    setExporting(true);
    try {
      const zip = new JSZip();
      const folder = zip.folder("banner-forge-export-b");
      const scale = window.BannerGen.RENDER_SCALE || 1;
      banners.forEach((b, i) => {
        const spec = SPECS_B[i];
        folder.file(`${spec.id}_${spec.w}x${spec.h}@${scale}x.png`, b.blob);
      });
      if (shotSpec) folder.file("shot_spec.json", JSON.stringify(shotSpec, null, 2));
      // Save the composed hero source + original products
      if (composed) {
        const composedBlob = await (await fetch(composed.dataUrl)).blob();
        folder.file("composed_source.png", composedBlob);
      }
      const productsFolder = folder.folder("products");
      products.forEach(p => productsFolder.file(p.name, p.file));
      if (composePrompt) folder.file("compose_prompt.txt", composePrompt);
      if (composeBase) folder.file("compose_base.txt", composeBase);
      if (composeStaging) folder.file("compose_staging.txt", composeStaging);

      const blob = await zip.generateAsync({ type: "blob" });
      const url = URL.createObjectURL(blob);
      const a = document.createElement("a");
      a.href = url;
      const stamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 16);
      a.download = `banner-export-B-${stamp}.zip`;
      document.body.appendChild(a); a.click(); a.remove();
      setTimeout(() => URL.revokeObjectURL(url), 4000);
    } finally {
      setExporting(false);
    }
  }, [banners, shotSpec, composed, products, composeBase, composeStaging]);

  /* ---- Derived ---- */
  const doneCount = banners.filter(b => b.status === "done").length;
  const genCount  = banners.filter(b => b.status === "gen").length;
  const errCount  = banners.filter(b => b.status === "err").length;
  const totalCount = banners.length;
  const changedCount = banners.filter(b => {
    if (b.status !== "done") return true;
    const snap = JSON.stringify({ shared: shotSpec?.shared || null, shot: shotSpec?.shots?.find(s => s.banner === b.id) || null, variation: tweaks.variation });
    return snap !== b.lastSpec;
  }).length;

  /* ============================================================
     Render
     ============================================================ */
  return (
    <div className="app">
      <window.TopBar
        sourceName={composed ? `합성본 (${products.length}개 상품)` : products.length ? `${products.length}개 상품` : ""}
        banners={banners}
        onExport={handleExport}
        exporting={exporting}
        mode="b"
        onBackToEntry={onBackToEntry}
      />
      <div className="workspace">
        {/* ---- Sidebar ---- */}
        <div className="sidebar">
          <div className="sidebar-tabs">
            <button className={"tab " + (tab === "upload" ? "active" : "")} onClick={() => setTab("upload")}>Upload</button>
            <button className={"tab " + (tab === "compose" ? "active" : "")} onClick={() => setTab("compose")}>
              Compose{composed ? <span style={{color:"var(--success)", marginLeft: 4}}>●</span> : ""}
            </button>
            <button className={"tab " + (tab === "spec" ? "active" : "")} onClick={() => setTab("spec")} disabled={!composeApproved}>Spec</button>
            <button className={"tab " + (tab === "banners" ? "active" : "")} onClick={() => setTab("banners")} disabled={!composeApproved}>Banners</button>
            <button className={"tab " + (tab === "api" ? "active" : "")} onClick={() => setTab("api")}>
              API {tweaks.apiEnabled ? <span style={{color:"var(--success)", marginLeft: 2}}>●</span> : ""}
            </button>
          </div>
          <div className="sidebar-body">

            {/* ===== Upload tab ===== */}
            {tab === "upload" && (
              <div className="fadein">
                <div className="section">
                  <div className="section-head">
                    <span className={"num " + (products.length ? "done" : "active")}>1</span>
                    상품 이미지 업로드
                  </div>
                  <window.Dropzone
                    multi
                    onFiles={addFiles}
                    title="상품 이미지 드래그 또는 클릭"
                    sub="여러 장 OK · PNG · JPG · WebP"
                  />
                </div>
                {products.length > 0 && (
                  <div className="section fadein">
                    <div className="section-head">
                      <span className="num done">·</span>
                      업로드 ({products.length})
                    </div>
                    <div style={{display:"flex", flexDirection:"column", gap: 4}}>
                      {products.map((p, i) => (
                        <div key={p.id} className="banner-row" style={{cursor:"default"}}>
                          <div className="swatch" style={{
                            width: 28, height: 28,
                            backgroundImage: `url(${p.dataUrl})`,
                            backgroundSize: "cover", backgroundPosition: "center",
                          }}/>
                          <div style={{flex: 1, minWidth: 0}}>
                            <div style={{fontSize: 12, overflow:"hidden", textOverflow:"ellipsis", whiteSpace:"nowrap"}}>
                              {p.priority && <span style={{color: "var(--accent)", marginRight: 4}}>★</span>}
                              {p.name}
                            </div>
                            <div className="ratio">
                              {p.dim.w}×{p.dim.h}
                              {p.priority && <span style={{color: "var(--accent)", marginLeft: 6}}>HIGH PRIORITY</span>}
                            </div>
                          </div>
                          <button
                            className="icon-btn"
                            onClick={() => togglePriority(p.id)}
                            title={p.priority ? "중요도 해제" : "중요도 높음으로 표시"}
                            style={{ color: p.priority ? "var(--accent)" : "var(--text-3)" }}
                          >
                            <Icon name="bolt" size={12} />
                          </button>
                          <button className="icon-btn" onClick={() => removeProduct(p.id)} title="제거">
                            <Icon name="trash" size={12} />
                          </button>
                        </div>
                      ))}
                    </div>
                    <button className="btn primary full" style={{marginTop: 10}} onClick={() => setTab("compose")}>
                      <Icon name="sparkle" /> 다음: AI 합성하기
                    </button>
                  </div>
                )}
              </div>
            )}

            {/* ===== Compose tab ===== */}
            {tab === "compose" && (
              <div className="fadein">
                {products.length === 0 ? (
                  <div style={{padding: 14, background:"var(--bg-2)", borderRadius: 6, color:"var(--text-2)", fontSize: 12}}>
                    먼저 상품 이미지를 업로드해주세요.
                  </div>
                ) : (
                  <>
                    <div className="section">
                      <div className="section-head">
                        <span className={"num " + (composed ? "done" : "active")}>2</span>
                        AI 합성 (1회 호출)
                      </div>
                      <div style={{fontSize: 11, color:"var(--text-2)", lineHeight: 1.55, marginBottom: 10}}>
                        {products.length}개 상품 → 1장의 정방향 합성 이미지. 합성본이 마음에 들면
                        승인 후 4종 배너 outpaint(추가 4회 호출)가 진행됩니다.
                      </div>
                      <div className="field">
                        <div className="field-label" style={{display:"flex", alignItems:"center", gap: 6}}>
                          <span>베이스 프롬프트</span>
                          <span style={{
                            marginLeft: "auto",
                            fontSize: 9, fontFamily: "var(--font-mono)",
                            color: baseIsDefault ? "var(--text-3)" : "var(--accent)",
                            letterSpacing: "0.06em",
                          }}>
                            {baseIsDefault ? "DEFAULT" : "CUSTOM"}
                          </span>
                          <button
                            className="btn ghost sm"
                            style={{padding: "2px 6px", fontSize: 10}}
                            onClick={() => setComposeBase(COMPOSE_BASE_DEFAULT)}
                            disabled={baseIsDefault}
                            title="기본 베이스 프롬프트로 되돌리기"
                          >
                            <Icon name="refresh" size={10} /> Reset
                          </button>
                        </div>
                        <textarea
                          className="textarea" rows={10}
                          placeholder={COMPOSE_BASE_DEFAULT}
                          value={composeBase}
                          onChange={e => setComposeBase(e.target.value)}
                          style={{
                            fontSize: 11, lineHeight: 1.55,
                            fontFamily: "var(--font-mono)",
                            background: baseIsDefault ? undefined : "var(--accent-soft)",
                          }}
                        />
                        <div style={{fontSize: 10, color: "var(--text-3)", marginTop: 4, lineHeight: 1.5}}>
                          제품 보존 / 렌더링 스타일 / 라벨 보존 같은 시스템 규칙. 잘 안 바꾸는 영역이에요.
                        </div>
                      </div>

                      <div className="field">
                        <div className="field-label" style={{display:"flex", alignItems:"center", gap: 6}}>
                          <Icon name="bolt" size={11} />
                          <span>연출 디테일</span>
                          <span style={{
                            marginLeft: "auto",
                            fontSize: 9, fontFamily: "var(--font-mono)",
                            color: stagingIsDefault ? "var(--text-3)" : "var(--accent)",
                            letterSpacing: "0.06em",
                          }}>
                            {stagingIsDefault ? "DEFAULT" : "CUSTOM"}
                          </span>
                          <button
                            className="btn ghost sm"
                            style={{padding: "2px 6px", fontSize: 10}}
                            onClick={() => setComposeStaging(COMPOSE_STAGING_DEFAULT)}
                            disabled={stagingIsDefault}
                            title="기본 연출 디테일로 되돌리기"
                          >
                            <Icon name="refresh" size={10} /> Reset
                          </button>
                        </div>
                        <textarea
                          className="textarea" rows={6}
                          placeholder={"예) 한 장에 어우러진 연출, 일렬 배치 금지\n1·2번 공중에 떠있음, 3번 바닥에 놓임. 연한 그림자.\n상품을 선명하게 보정."}
                          value={composeStaging}
                          onChange={e => setComposeStaging(e.target.value)}
                          style={{
                            fontSize: 12, lineHeight: 1.55,
                            background: stagingIsDefault ? undefined : "var(--accent-soft)",
                          }}
                        />
                        <div style={{fontSize: 10, color: "var(--text-3)", marginTop: 4, lineHeight: 1.5}}>
                          매 작업마다 바뀌는 구도·배치·분위기 디렉션. 호출 시 베이스 프롬프트 끝에 자동으로 합쳐져요.
                        </div>
                      </div>
                      <button
                        className="btn primary full"
                        onClick={runCompose}
                        disabled={composeLoading}
                      >
                        <Icon name="sparkle" />
                        {composeLoading ? "합성 중…" : composed ? "프롬프트 반영해 다시 합성" : tweaks.apiEnabled ? "합성 시작" : "로컬 테스트 합성"}
                      </button>
                      {!tweaks.apiEnabled && (
                        <div style={{fontSize: 11, color: "var(--warning)", marginTop: 6, lineHeight: 1.55}}>
                          ⚠ AI 비활성 — 로컬 캔버스 콜라주(테스트용)로 합성됩니다. 진짜 AI 합성은 API 탭에서 활성화하세요.
                        </div>
                      )}
                      {composeError && (
                        <div style={{fontSize: 11, color: "var(--danger)", marginTop: 6, lineHeight: 1.55}}>
                          ✗ {composeError}
                        </div>
                      )}
                    </div>
                    {composed && !composeApproved && (
                      <div className="section fadein">
                        <div className="section-head">
                          <span className="num active">·</span>
                          합성 결과 확인
                        </div>
                        <div style={{
                          aspectRatio: "1 / 1",
                          backgroundImage: `url(${composed.dataUrl})`,
                          backgroundSize: "cover", backgroundPosition: "center",
                          borderRadius: 6,
                          boxShadow: "0 4px 16px rgba(0,0,0,0.4), 0 0 0 1px var(--border-2)",
                          marginBottom: 10,
                        }}/>
                        <button className="btn primary full" onClick={approveCompose}>
                          <Icon name="check" /> 승인 → 4종 배너 생성으로
                        </button>
                        <div style={{fontSize: 11, color:"var(--text-3)", marginTop: 6, lineHeight: 1.55, textAlign:"center"}}>
                          마음에 안 들면 위 프롬프트를 수정하고 "다시 합성"
                        </div>
                      </div>
                    )}
                    {composeApproved && composed && (
                      <div className="section fadein">
                        <div className="section-head">
                          <span className="num done">·</span>
                          승인 완료
                        </div>
                        <div style={{
                          aspectRatio: "1 / 1",
                          backgroundImage: `url(${composed.dataUrl})`,
                          backgroundSize: "cover", backgroundPosition: "center",
                          borderRadius: 6,
                          boxShadow: "0 4px 16px rgba(0,0,0,0.4), 0 0 0 1px var(--accent-line)",
                          marginBottom: 10,
                        }}/>
                        <button className="btn full" onClick={() => setComposeApproved(false)}>
                          <Icon name="refresh" /> 승인 취소 / 다시 합성
                        </button>
                        <button className="btn primary full" style={{marginTop: 6}} onClick={() => setTab("banners")}>
                          <Icon name="grid" /> 4종 배너 생성으로 →
                        </button>
                      </div>
                    )}
                  </>
                )}
              </div>
            )}

            {/* ===== Spec tab (only when approved) ===== */}
            {tab === "spec" && composeApproved && composed && (
              <div className="fadein">
                {specLoading && (
                  <div className="section">
                    <div className="section-head"><span className="num active">·</span> AI 분석 중</div>
                    <div className="skel" style={{position:"relative", height: 60, borderRadius: 4}} />
                  </div>
                )}
                {shotSpec && (
                  <>
                    <div className="section">
                      <div className="section-head"><span className="num done">·</span> Shared</div>
                      <div className="field">
                        <div className="field-label">Mood</div>
                        <input
                          className="input"
                          value={shotSpec.shared.mood}
                          onChange={e => setShotSpec({ ...shotSpec, shared: { ...shotSpec.shared, mood: e.target.value }})}
                        />
                      </div>
                      <div className="field">
                        <div className="field-label">Lighting note</div>
                        <textarea
                          className="textarea" rows={3}
                          value={shotSpec.shared.source_lighting_note}
                          onChange={e => setShotSpec({ ...shotSpec, shared: { ...shotSpec.shared, source_lighting_note: e.target.value }})}
                        />
                      </div>
                      <div className="field">
                        <div className="field-label">
                          <Icon name="bolt" size={11} /> 공통 직접 지시
                        </div>
                        <textarea
                          className="textarea" rows={2}
                          placeholder='예) "전체 톤 베이지", "그림자 강조"'
                          value={shotSpec.shared.user_directive || ""}
                          onChange={e => setShotSpec({ ...shotSpec, shared: { ...shotSpec.shared, user_directive: e.target.value }})}
                          style={{ background: (shotSpec.shared.user_directive || "").trim() ? "var(--accent-soft)" : undefined }}
                        />
                      </div>
                    </div>
                    <div className="section">
                      <div className="section-head"><span className="num">·</span> 배너별 지시</div>
                      {shotSpec.shots.map((shot, i) => (
                        <window.SpecBlock
                          key={shot.banner}
                          spec={SPECS_B[i]}
                          shotSpec={shot}
                          onChange={updated => {
                            const next = { ...shotSpec, shots: [...shotSpec.shots] };
                            next.shots[i] = updated;
                            setShotSpec(next);
                          }}
                        />
                      ))}
                    </div>
                    <button
                      className={"btn full " + (changedCount > 0 ? "primary" : "")}
                      disabled={changedCount === 0 || genCount > 0}
                      onClick={() => tweaks.pipelineMode === "outpaint" ? generateNextOne() : generateChangedOnly()}
                    >
                      <Icon name="refresh" />
                      {changedCount === 0
                        ? "모두 최신 상태"
                        : tweaks.pipelineMode === "outpaint"
                          ? `다음 1개 생성 (${changedCount} 남음)`
                          : `변경된 ${changedCount}개만 다시 생성`}
                    </button>
                  </>
                )}
              </div>
            )}

            {/* ===== Banners tab (only when approved) ===== */}
            {tab === "banners" && composeApproved && composed && (
              <div className="fadein">
                <div className="section">
                  <div className="section-head">
                    <span className={"num " + (doneCount === totalCount ? "done" : "active")}>·</span>
                    {SPECS_B.length} banners
                  </div>
                  {SPECS_B.map(s => {
                    const b = banners.find(x => x.id === s.id);
                    return (
                      <window.BannerRow
                        key={s.id}
                        spec={s}
                        banner={b}
                        selected={selectedId === s.id}
                        onSelect={() => setSelectedId(s.id)}
                        onRegen={() => generateOne(s.id)}
                      />
                    );
                  })}
                </div>

                {/* Per-banner refinement editor — directive textarea + regen */}
                {selectedId && selectedId !== HERO_SPEC.id && shotSpec && (() => {
                  const selSpec = SPECS_B.find(s => s.id === selectedId);
                  const shotIdx = shotSpec.shots.findIndex(x => x.banner === selectedId);
                  if (shotIdx < 0) return null;
                  const shot = shotSpec.shots[shotIdx];
                  const directive = shot.user_directive || "";
                  const selBanner = banners.find(b => b.id === selectedId);
                  const canRefine = !!(selBanner && selBanner.dataUrl);
                  return (
                    <div className="section fadein">
                      <div className="section-head">
                        <span className="num">·</span>
                        {selSpec.name} · 추가 수정
                      </div>
                      {canRefine && (
                        <div style={{
                          aspectRatio: `${selSpec.w} / ${selSpec.h}`,
                          maxHeight: 200,
                          backgroundImage: `url(${selBanner.dataUrl})`,
                          backgroundSize: "contain",
                          backgroundPosition: "center",
                          backgroundRepeat: "no-repeat",
                          backgroundColor: "var(--bg-2)",
                          borderRadius: 4,
                          marginBottom: 8,
                        }}/>
                      )}
                      <div className="field">
                        <div className="field-label" style={{display:"flex", alignItems:"center", gap: 6}}>
                          <Icon name="bolt" size={11} />
                          <span>수정 지시 (생성된 이미지 기준 재호출)</span>
                        </div>
                        <textarea
                          className="textarea"
                          rows={4}
                          placeholder={"예) 배경 톤 더 따뜻하게\n상품 그림자 진하게\n상단을 더 밝게"}
                          value={directive}
                          onChange={e => {
                            const next = { ...shotSpec, shots: [...shotSpec.shots] };
                            next.shots[shotIdx] = { ...shot, user_directive: e.target.value };
                            setShotSpec(next);
                          }}
                          style={{ fontSize: 12, lineHeight: 1.55, background: directive.trim() ? "var(--accent-soft)" : undefined }}
                        />
                        <div style={{fontSize: 10, color: "var(--text-3)", marginTop: 4, lineHeight: 1.5}}>
                          {canRefine
                            ? "이미 생성된 결과를 base 로 두고, 위 지시문만 반영해 재호출합니다."
                            : "이 배너가 아직 생성되지 않았습니다. 일반 호출이 실행돼요."}
                        </div>
                      </div>
                      <button
                        className="btn primary full"
                        disabled={genCount > 0}
                        onClick={() => generateOne(selectedId)}
                      >
                        <Icon name="refresh" />
                        {canRefine ? "수정 반영 재호출" : "이 배너 생성"}
                      </button>
                    </div>
                  );
                })()}

                <button
                  className="btn primary full"
                  disabled={doneCount !== totalCount || exporting}
                  onClick={handleExport}
                  style={{marginTop: 10}}
                >
                  <Icon name="download" />
                  {exporting ? "Packing…" : "Export ZIP"}
                </button>
              </div>
            )}

            {/* ===== API tab ===== */}
            {tab === "api" && (
              <div className="fadein">
                <div className="section">
                  <div className="section-head">
                    <span className={"num " + (tweaks.apiEnabled ? "done" : "")}>·</span>
                    연결 설정 (A타입과 공유)
                  </div>
                  <div className="field">
                    <div className="field-label">Endpoint URL</div>
                    <input
                      className="input"
                      placeholder="https://banner-forge-proxy.YOUR-NAME.workers.dev"
                      value={tweaks.apiEndpoint}
                      onChange={e => setTweak("apiEndpoint", e.target.value)}
                    />
                  </div>
                  <button
                    className={"btn full " + (tweaks.apiEnabled ? "primary" : "")}
                    onClick={() => setTweak("apiEnabled", !tweaks.apiEnabled)}
                  >
                    {tweaks.apiEnabled ? "✓ AI 사용 중" : "토글하여 활성화"}
                  </button>
                  <div style={{fontSize: 11, color:"var(--text-2)", marginTop: 6, lineHeight: 1.55}}>
                    B타입은 합성(compose) + 배너 생성(outpaint) 두 번 AI를 호출합니다.
                    합성 1회 + 배너 최대 4회 = 최대 5회/세션.
                  </div>
                </div>
              </div>
            )}
          </div>
        </div>

        {/* ---- Canvas ---- */}
        <div className="canvas" data-screen-label="ModeB-Main">
          <div className="canvas-body">
            {!products.length ? (
              <window.EmptyState
                title="B타입 — 상품 이미지로 합성 후 5종 배너"
                desc="여러 상품을 한 번에 올리면 AI가 한 장의 합성 이미지로 만들고, 그걸 포함해 총 5종 배너로 확장합니다."
              />
            ) : !composed ? (
              /* ----- Stage 1: Pre-compose — product grid + compose CTA ----- */
              <div className="modeb-compose-stage">
                <div className="modeb-compose-head">
                  <div className="label">{products.length} PRODUCTS</div>
                  <div className="title">합성 대상 상품</div>
                </div>
                <div className="modeb-product-grid">
                  {products.map(p => (
                    <div
                      key={p.id}
                      className={"modeb-product-card " + (p.priority ? "priority" : "")}
                      onClick={() => togglePriority(p.id)}
                      style={{ cursor: "pointer" }}
                      title={p.priority ? "중요도 해제 (클릭)" : "중요도 표시 (클릭)"}
                    >
                      <div className="thumb" style={{ backgroundImage: `url(${p.dataUrl})` }} />
                      {p.priority && (
                        <div className="priority-badge">★ 중요도 높음</div>
                      )}
                      <div className="meta">
                        <div className="name">{p.name}</div>
                        <div className="sub">{p.dim.w}×{p.dim.h}</div>
                      </div>
                    </div>
                  ))}
                </div>
                <div className="modeb-compose-cta">
                  <Icon name="sparkle" size={20} />
                  <div>
                    <strong>AI가 이 상품들을 1장의 정방향 이미지로 합성합니다.</strong>
                    <div className="sub">합성본 1회 호출 (Gemini). 마음에 들면 승인 후 4종 배너 outpaint를 진행해요.</div>
                  </div>
                  <button
                    className="btn primary"
                    onClick={runCompose}
                    disabled={composeLoading}
                  >
                    <Icon name="bolt" size={12} />
                    {composeLoading ? "합성 중…" : tweaks.apiEnabled ? "합성 시작" : "로컬 테스트 합성"}
                  </button>
                </div>
                {composeError && (
                  <div className="modeb-compose-error">✗ {composeError}</div>
                )}
              </div>
            ) : !composeApproved ? (
              /* ----- Stage 2: Compose preview — approval gate (코인 방지) ----- */
              <div className="modeb-approval-stage">
                <div className="modeb-approval-head">
                  <div className="label">
                    REVIEW · 합성 결과
                    {!tweaks.apiEnabled && (
                      <span className="local-badge">LOCAL TEST</span>
                    )}
                  </div>
                  <div className="title">이 이미지로 진행할까요?</div>
                  <div className="subtitle">
                    {tweaks.apiEnabled
                      ? "승인하면 이 합성본을 1번 배너로 두고, 4가지 사이즈로 outpaint(추가 Gemini 4회)를 진행합니다. 마음에 안 들면 프롬프트를 수정해서 다시 합성하세요."
                      : "현재 로컬 캔버스 콜라주입니다 (실제 AI 합성 아님). 흐름 검증용. 진짜 AI 합성은 API 탭에서 Cloudflare Worker 활성화 후 사용하세요."}
                  </div>
                </div>

                <div className="modeb-approval-body">
                  {/* Sources stack on left */}
                  <div className="modeb-approval-sources">
                    <div className="modeb-approval-sources-label">SOURCES · {products.length}</div>
                    <div className="modeb-approval-sources-grid">
                      {products.slice(0, 6).map(p => (
                        <div
                          key={p.id}
                          className="modeb-approval-source-thumb"
                          style={{ backgroundImage: `url(${p.dataUrl})` }}
                          title={p.name}
                        />
                      ))}
                      {products.length > 6 && (
                        <div className="modeb-approval-source-thumb more">+{products.length - 6}</div>
                      )}
                    </div>
                  </div>

                  {/* Arrow */}
                  <div className="modeb-approval-arrow">→</div>

                  {/* Big composed preview */}
                  <div className="modeb-approval-preview">
                    <div
                      className="modeb-approval-preview-img"
                      style={{ backgroundImage: `url(${composed.dataUrl})` }}
                    />
                    <div className="modeb-approval-preview-meta">
                      {composed.dim.w} × {composed.dim.h} · 합성본 (1:1)
                    </div>
                  </div>
                </div>

                {/* Inline prompt editor for re-compose */}
                <div className={"modeb-approval-prompt " + (showPromptEditor ? "open" : "")}>
                  <button
                    className="btn ghost full sm"
                    onClick={() => setShowPromptEditor(o => !o)}
                  >
                    <Icon name={showPromptEditor ? "eye" : "eyeoff"} size={12} />
                    {showPromptEditor ? "연출 디테일 닫기" : "연출 디테일 수정해서 다시 합성"}
                  </button>
                  {showPromptEditor && (
                    <div style={{marginTop: 10}}>
                      <div style={{display:"flex", alignItems:"center", gap: 6, marginBottom: 6}}>
                        <span style={{fontSize: 11, color: "var(--text-1)", fontWeight: 500}}>
                          연출 디테일
                        </span>
                        <span style={{
                          fontSize: 10, fontFamily: "var(--font-mono)",
                          letterSpacing: "0.06em",
                          color: stagingIsDefault ? "var(--text-3)" : "var(--accent)",
                        }}>
                          {stagingIsDefault ? "DEFAULT" : "CUSTOM"}
                        </span>
                        <button
                          className="btn ghost sm"
                          style={{marginLeft: "auto", padding: "2px 6px", fontSize: 10}}
                          onClick={() => setComposeStaging(COMPOSE_STAGING_DEFAULT)}
                          disabled={stagingIsDefault}
                        >
                          <Icon name="refresh" size={10} /> Reset
                        </button>
                      </div>
                      <textarea
                        className="textarea" rows={5}
                        placeholder={"예) 한 장에 어우러진 연출, 일렬 배치 금지\n1·2번 공중, 3번 바닥. 연한 그림자."}
                        value={composeStaging}
                        onChange={e => setComposeStaging(e.target.value)}
                        style={{
                          fontSize: 12, lineHeight: 1.55,
                          background: stagingIsDefault ? undefined : "var(--accent-soft)",
                        }}
                      />
                      <div style={{fontSize: 10, color: "var(--text-3)", marginTop: 4, lineHeight: 1.5}}>
                          베이스 프롬프트는 Compose 탭에서 편집할 수 있어요. 여기는 매번 바뀌는 연출만.
                      </div>
                      <button
                        className="btn full"
                        style={{marginTop: 8}}
                        onClick={runCompose}
                        disabled={composeLoading}
                      >
                        <Icon name="refresh" size={12} />
                        {composeLoading ? "합성 중…" : tweaks.apiEnabled ? "다시 합성 (Gemini 1회)" : "다시 합성 (로컬)"}
                      </button>
                    </div>
                  )}
                </div>

                {/* Approval CTA */}
                <div className="modeb-approval-actions">
                  <button
                    className="btn primary lg"
                    onClick={approveCompose}
                    disabled={composeLoading}
                  >
                    <Icon name="check" size={14} />
                    승인 · 5종 배너 생성으로 진행
                    <span className="cost-chip">+Gemini 4회 호출</span>
                  </button>
                </div>

                {composeError && (
                  <div className="modeb-compose-error">✗ {composeError}</div>
                )}
              </div>
            ) : (
              /* ----- Post-compose: A-style canvas ----- */
              <>
                <div className={"api-status-bar " + (tweaks.apiEnabled ? "" : "off") + (errCount > 0 ? " err" : "")}>
                  <div className="api-status-left">
                    <span className={"status-dot " + (tweaks.apiEnabled ? (errCount ? "red" : "green") : "gray")} />
                    <div className="api-status-text">
                      <strong>
                        {composeLoading
                          ? "합성 중…"
                          : genCount > 0
                            ? `배너 생성 중 · ${doneCount}/${totalCount}`
                            : doneCount === totalCount
                              ? "모두 완료"
                              : changedCount > 0
                                ? `${changedCount}개 호출 대기`
                                : "최신 상태"}
                      </strong>
                      <span className="sub">{tweaks.pipelineMode === "outpaint" ? "테스트 모드: 1개씩 호출 — 결과 확인 후 다음 진행" : "합성본 1장 + 4종 outpaint = 5종 배너"}</span>
                    </div>
                  </div>
                  <div className="api-status-right">
                    <button
                      className="btn sm"
                      onClick={() => setTab("compose")}
                      title="합성 다시 하기"
                    >
                      <Icon name="refresh" size={12} /> 다시 합성
                    </button>
                    <button
                      className="btn primary sm"
                      disabled={genCount > 0 || changedCount === 0}
                      onClick={() => tweaks.pipelineMode === "outpaint" ? generateNextOne() : generateChangedOnly()}
                      title={
                        tweaks.pipelineMode === "outpaint"
                          ? `다음 1개 배너만 호출 (남은 ${changedCount}개)`
                          : `변경된 ${changedCount}개 즉시 생성`
                      }
                    >
                      <Icon name="bolt" size={12} />
                      {errCount > 0
                        ? `다시 시도 (${errCount})`
                        : changedCount === 0
                          ? "최신 상태"
                          : tweaks.pipelineMode === "outpaint"
                            ? (() => {
                                const snapFor = (specId) => JSON.stringify({
                                  shared: shotSpec?.shared || null,
                                  shot: shotSpec?.shots?.find(x => x.banner === specId) || null,
                                  variation: tweaks.variation,
                                });
                                const isPending = (b, specId) => {
                                  if (!b || b.status === "gen") return false;
                                  if (b.status !== "done") return true;
                                  return snapFor(specId) !== b.lastSpec;
                                };
                                let next;
                                if (selectedId && selectedId !== HERO_SPEC.id) {
                                  const sel = OUTPAINT_SPECS.find(s => s.id === selectedId);
                                  const selBanner = banners.find(x => x.id === selectedId);
                                  if (sel && isPending(selBanner, selectedId)) next = sel;
                                }
                                if (!next) next = OUTPAINT_SPECS.find(s => isPending(banners.find(x => x.id === s.id), s.id));
                                if (!next) return "최신 상태";
                                const isSelected = next.id === selectedId;
                                const nextLabel = next.name.replace(/^.*?·\s*/, "");
                                return `${isSelected ? "★ " : ""}${nextLabel} 생성 (${changedCount} 남음)`;
                              })()
                            : doneCount > 0
                              ? `변경된 ${changedCount}개`
                              : "AI 호출하기"}
                    </button>
                  </div>
                </div>

                <div className="canvas-stage">
                  {/* Composed source (square) */}
                  <div className="canvas-source">
                    <div className="label">COMPOSED · 1:1</div>
                    <div className="src-frame" style={{ backgroundImage: `url(${composed.dataUrl})` }}/>
                    <div style={{fontSize: 10, color:"var(--text-3)", fontFamily:"var(--font-mono)", marginTop: 6}}>
                      ← {products.length} products
                    </div>
                  </div>

                  <div className="flow-arrow">
                    <div className="stem"/>
                    <div>OUTPAINTING</div>
                    <div className="stem"/>
                    <div style={{color:"var(--text-2)"}}>▼</div>
                  </div>

                  <div className="banner-grid">
                    {SPECS_B.map(spec => {
                      const b = banners.find(x => x.id === spec.id);
                      return (
                        <window.BannerCard
                          key={spec.id}
                          spec={spec}
                          banner={b}
                          scale={tweaks.canvasScale}
                          selected={selectedId === spec.id}
                          onSelect={() => setSelectedId(spec.id)}
                          showZones={tweaks.showZones}
                          showAnchor={tweaks.showAnchors && selectedId === spec.id}
                          showBbox={tweaks.showBbox && tweaks.pipelineMode === "hybrid"}
                          onAnchorChange={a => onAnchorChange(spec.id, a)}
                          onRegen={() => generateOne(spec.id)}
                        />
                      );
                    })}
                  </div>
                </div>

                {doneCount === totalCount && totalCount > 0 && (
                  <window.DraggableStrip className="fadein">
                    <Icon name="check" size={14} style={{color:"var(--success)"}}/>
                    <span style={{fontSize: 12}}>{totalCount}개 배너 모두 준비 완료</span>
                    <button className="btn primary sm" onClick={handleExport} disabled={exporting}>
                      <Icon name="download" size={12}/>
                      {exporting ? "Packing…" : "Export ZIP"}
                    </button>
                  </window.DraggableStrip>
                )}
              </>
            )}
          </div>
        </div>
      </div>

      <window.TweaksPanel title="Tweaks">
        <window.TweakSection label="Pipeline">
          <window.TweakRadio
            label="Mode"
            value={tweaks.pipelineMode}
            onChange={v => setTweak("pipelineMode", v)}
            options={[
              { value: "outpaint", label: "Safe-Area Outpaint (AI)" },
              { value: "hybrid",   label: "Crop only (no AI)" },
            ]}
          />
          <window.TweakSlider
            label="Safe area 여백"
            value={tweaks.safePadding}
            onChange={v => setTweak("safePadding", v)}
            min={0} max={30} step={1}
            unit="%"
          />
          <window.TweakToggle
            label="Show bbox overlay"
            value={tweaks.showBbox}
            onChange={v => setTweak("showBbox", v)}
          />
        </window.TweakSection>
        <window.TweakSection label="Variation">
          <window.TweakRadio
            label="Background"
            value={tweaks.variation}
            onChange={v => setTweak("variation", v)}
            options={[
              { value: "natural", label: "Natural" },
              { value: "studio",  label: "Studio" },
              { value: "vivid",   label: "Vivid" },
            ]}
          />
        </window.TweakSection>
        <window.TweakSection label="Canvas">
          <window.TweakSlider
            label="Preview scale"
            value={Math.round(tweaks.canvasScale * 100)}
            onChange={v => setTweak("canvasScale", v / 100)}
            min={30} max={120} step={5}
            unit="%"
          />
          <window.TweakToggle
            label="Show red zones"
            value={tweaks.showZones}
            onChange={v => setTweak("showZones", v)}
          />
          <window.TweakToggle
            label="Show anchor"
            value={tweaks.showAnchors}
            onChange={v => setTweak("showAnchors", v)}
          />
        </window.TweakSection>
      </window.TweaksPanel>
    </div>
  );
}

window.ModeBApp = ModeBApp;
