/* ────────────────────────────────────────────────────────────
   editor.jsx — 듀얼뷰 스토리보드 편집기 (Wave A 골격 + Wave C 편집/자동저장/버전/AI)
   CONTRACT §3.3:
     GET   /api/projects/:id/slides
       -> {slides:[{slideId,id,title,content,narration,layout_suggestion,...v3}],
           session:{total,sourceFile,courseName,chapterName,fontSettings}}
     PATCH /api/projects/:id/slides/:slideId  {부분 v3 필드}  -> {slide:{slideId,id,...v3}} (단건 낙관적)
     PUT   /api/projects/:id/slides  {slides:[...], version}  -> {slides:[...], version}  (bulkSave 낙관적 잠금)
                                                              409 {error,code:"version_conflict",version}
   §3.6/§3.7 AI 파싱(versions.jsx AiParseModal) · §3.9 버전(versions.jsx VersionsModal)
   상단 토글 [스토리보드 뷰][표 뷰] — 항상 같은 위치(상단 좌측), 활성 border-info.
   표 뷰 컬럼: 페이지·코너·진행·화면제목·화면구조화·나레이션.
   비주얼 뷰: 컷 카드 그리드(visualView.jsx 패턴 승계, 데이터=API).
   ErrorBoundary/옵셔널 체이닝으로 백지 방지. 해요체·이모지 없음·tokens 변수만.
   ──────────────────────────────────────────────────────────── */

/* 레이아웃 배지 (v3 layout_suggestion) */
function LayoutBadge({ layout, sm }) {
  const m = layoutMeta(layout);
  return React.createElement("span", {
    style: {
      display: "inline-flex", alignItems: "center", gap: 4, height: sm ? 20 : 22, padding: "0 8px",
      borderRadius: "var(--r-xs)", background: "var(--bg-alt)", color: m.color,
      fontSize: sm ? 11 : 11.5, fontWeight: 700, lineHeight: 1, border: "1px solid var(--border)",
    },
  }, m.label);
}

/* 진행(확정) 배지 — confirmed 기반 */
function ConfirmBadge({ confirmed }) {
  return confirmed
    ? React.createElement(Pill, { color: "var(--positive)", bg: "var(--positive-bg)" },
        React.createElement("span", { style: { width: 6, height: 6, borderRadius: "50%", background: "var(--positive)" } }), "확정")
    : React.createElement(Pill, { color: "var(--label-alt)", bg: "var(--bg-alt)" },
        React.createElement("span", { style: { width: 6, height: 6, borderRadius: "50%", background: "var(--label-assist)" } }), "작성중");
}

/* ── 뷰 토글 (항상 같은 위치·활성 border-info) ──
   PPT 뷰(3-패널, 기본) ↔ 스토리보드 뷰(카드 그리드) ↔ 표 뷰. */
function ViewToggle({ view, onChange }) {
  const opts = [
    { key: "ppt", label: "PPT 뷰" },
    { key: "visual", label: "스토리보드 뷰" },
    { key: "table", label: "표 뷰" },
  ];
  return React.createElement("div", {
    style: { display: "inline-flex", padding: 3, gap: 3, background: "var(--bg-alt)", borderRadius: "var(--r-sm)", border: "1px solid var(--border)" },
  },
    opts.map((o) => {
      const active = view === o.key;
      return React.createElement("button", {
        key: o.key,
        onClick: () => onChange(o.key),
        style: {
          height: 32, padding: "0 14px", borderRadius: "var(--r-xs)", cursor: "pointer", fontSize: 13, fontWeight: 700,
          background: active ? "var(--bg-normal)" : "transparent",
          color: active ? "var(--primary)" : "var(--label-alt)",
          border: active ? "1px solid var(--info)" : "1px solid transparent",   // 활성 border-info
          boxShadow: active ? "var(--sh-1)" : "none",
          transition: "all var(--t-fast)",
        },
      }, o.label);
    })
  );
}

/* ── PPT 좌측: 슬라이드 썸네일 스트립 ──
   각 슬라이드를 [번호] + 작은 SlidePreview(실제 내용·레이아웃 반영) + 상태점 으로
   세로 나열. 클릭 선택, 현재 선택은 파란 테두리/배경으로 강조. */
function SlideStrip({ slides, selectedId, onSelect }) {
  return React.createElement("div", { style: { display: "flex", flexDirection: "column", gap: 8, padding: "10px 10px 40px" } },
    slides.map((s) => {
      const rowId = s.slideId || s.id;
      const active = rowId === selectedId;
      return React.createElement("button", {
        key: rowId,
        onClick: () => onSelect(rowId),
        style: {
          display: "flex", alignItems: "stretch", gap: 8, width: "100%", textAlign: "left", cursor: "pointer",
          padding: 6, borderRadius: "var(--r-md)", transition: "all var(--t-fast)",
          background: active ? "var(--blue-95)" : "var(--bg-normal)",
          border: active ? "1.5px solid var(--primary)" : "1px solid var(--border)",
          boxShadow: active ? "0 0 0 2px var(--primary-light)" : "none",
        },
        onMouseEnter: (e) => { if (!active) e.currentTarget.style.borderColor = "var(--label-disable)"; },
        onMouseLeave: (e) => { if (!active) e.currentTarget.style.borderColor = "var(--border)"; },
      },
        /* 번호 + 상태점 (세로) */
        React.createElement("div", { style: { display: "flex", flexDirection: "column", alignItems: "center", gap: 5, flexShrink: 0, paddingTop: 1 } },
          React.createElement("span", { className: "tnum", style: { fontSize: 11, fontWeight: 700, color: active ? "var(--primary)" : "var(--label-alt)", minWidth: 18, textAlign: "center" } }, String(s.id).padStart(2, "0")),
          React.createElement("span", {
            "aria-label": s.confirmed ? "확정" : "작성중",
            style: { width: 7, height: 7, borderRadius: "50%", background: s.confirmed ? "var(--positive)" : "var(--label-disable)" },
          })),
        /* 작은 미리보기 — 실제 레이아웃/내용 반영 */
        React.createElement("div", { style: { flex: 1, minWidth: 0 } },
          typeof SlidePreview === "function"
            ? React.createElement(SlidePreview, { slide: s })
            : React.createElement("div", { style: { aspectRatio: "16/9", background: "var(--bg-neutral)", borderRadius: "var(--r-xs)" } })));
    }));
}

/* ── 나레이션 도크 (PPT 노트 스타일) ──
   중앙 캔버스 바로 아래, 전체폭. 선택 슬라이드의 narration 을 textarea 로 편집하고
   디바운스(0.6s) 자동저장 → onPatch({narration}) (상위 patchSlide 흐름 재사용).
   슬라이드 전환 시 외부 값과 동기화. 고정 높이(~120px)·스크롤. 해요체. */
function NarrationDock({ slide, onPatch }) {
  const slideKey = slide ? (slide.slideId || slide.id) : null;
  const [text, setText] = React.useState(slide ? (slide.narration || "") : "");
  const timer = React.useRef(null);

  /* 슬라이드 전환·외부 갱신 시 동기화(편집 중이 아닐 때 외부값 반영) */
  React.useEffect(() => {
    setText(slide ? (slide.narration || "") : "");
  }, [slideKey]);

  React.useEffect(() => () => { if (timer.current) clearTimeout(timer.current); }, []);

  const onChange = (v) => {
    setText(v);
    if (timer.current) clearTimeout(timer.current);
    timer.current = setTimeout(() => { if (onPatch) onPatch({ narration: v }); }, 600);
  };
  /* 포커스 아웃 시 즉시 저장(디바운스 대기 없이) */
  const flush = () => {
    if (timer.current) { clearTimeout(timer.current); timer.current = null; }
    if (onPatch && slide && (slide.narration || "") !== text) onPatch({ narration: text });
  };

  return React.createElement("div", {
    style: { width: "100%", maxWidth: 920, marginTop: 14, background: "var(--bg-normal)", border: "1px solid var(--border)", borderRadius: "var(--r-md)", padding: "11px 14px 13px", boxShadow: "var(--sh-1)" },
  },
    React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 7, marginBottom: 8 } },
      React.createElement("svg", { width: 14, height: 14, viewBox: "0 0 24 24", fill: "none", stroke: "var(--label-alt)", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", style: { flexShrink: 0 } },
        React.createElement("path", { d: "M3 5h12M3 10h12M3 15h8" }), React.createElement("path", { d: "M19 9v8M16 13l3 4 3-4" })),
      React.createElement("span", { className: "t-label-2", style: { fontWeight: 700, color: "var(--label-alt)", letterSpacing: "0.02em" } }, "나레이션 · 성우 낭독 원문")),
    React.createElement("textarea", {
      value: text,
      placeholder: "성우가 낭독할 원고 원문을 적어요. 원문을 그대로 보존해요.",
      onChange: (e) => onChange(e.target.value),
      onBlur: flush,
      style: { width: "100%", height: 96, maxHeight: 120, resize: "vertical", overflowY: "auto", padding: "9px 11px", borderRadius: "var(--r-sm)", border: "1px solid var(--border)", background: "var(--bg-neutral)", fontSize: 13.5, lineHeight: 1.65, color: "var(--label-normal)", fontFamily: "var(--font)", outline: "none", boxSizing: "border-box" },
    }));
}

/* ── PPT 3-패널 본문 ──
   좌: 썸네일 스트립 / 중앙: 16:9 자유배치 캔버스 편집기(SlideCanvas) + 나레이션 도크 / 우: 분기 패널.
   우측 = 오브젝트 선택 시 ObjectInspector(속성), 미선택 시 CutEditForm(슬라이드 메타·나레이션 숨김). */
function PptView({ project, slides, selectedId, onSelect, selectedSlide, onPatch, saving, savedAt,
                   selectedObjId, onSelectObj, onObjects }) {
  const selObj = (selectedSlide && Array.isArray(selectedSlide.objects))
    ? selectedSlide.objects.find((o) => o.id === selectedObjId) || null
    : null;

  /* 오브젝트 한 개 갱신 → 슬라이드 objects 통째 patch(디바운스는 상위 patchSlide) */
  const patchObj = (patch) => {
    if (!selObj || !selectedSlide) return;
    const next = (selectedSlide.objects || []).map((o) => (o.id === selObj.id ? { ...o, ...patch } : o));
    onObjects(next);
  };
  const patchObjStyle = (stylePatch) => {
    if (!selObj || !selectedSlide) return;
    const next = (selectedSlide.objects || []).map((o) => (o.id === selObj.id ? { ...o, style: { ...(o.style || {}), ...stylePatch } } : o));
    onObjects(next);
  };
  const zObj = (dir) => {
    if (!selObj || !selectedSlide) return;
    const sorted = (selectedSlide.objects || []).slice().sort((a, b) => (a.z || 0) - (b.z || 0));
    const idx = sorted.findIndex((o) => o.id === selObj.id);
    if (idx < 0) return;
    let order = sorted.map((o) => o.id);
    order.splice(idx, 1);
    if (dir === "front") order.push(selObj.id);
    else if (dir === "back") order.unshift(selObj.id);
    else if (dir === "up") order.splice(Math.min(order.length, idx + 1), 0, selObj.id);
    else if (dir === "down") order.splice(Math.max(0, idx - 1), 0, selObj.id);
    const zmap = {}; order.forEach((id, i) => { zmap[id] = i; });
    onObjects((selectedSlide.objects || []).map((o) => ({ ...o, z: zmap[o.id] != null ? zmap[o.id] : o.z })));
  };
  const delObj = () => {
    if (!selObj || !selectedSlide) return;
    onObjects((selectedSlide.objects || []).filter((o) => o.id !== selObj.id));
    onSelectObj(null);
  };

  return React.createElement("div", { style: { flex: 1, display: "flex", minHeight: 0, minWidth: 0 } },
    /* 좌측 썸네일 스트립 */
    React.createElement("div", {
      style: { width: 210, flexShrink: 0, overflowY: "auto", overflowX: "hidden", background: "var(--bg-neutral)", borderRight: "1px solid var(--line-normal)" },
    },
      React.createElement(SlideStrip, { slides, selectedId, onSelect })),

    /* 중앙 자유배치 캔버스 편집기 (회색 캔버스) */
    React.createElement("div", {
      style: { flex: 1, minWidth: 0, overflow: "auto", background: "var(--bg-canvas)", display: "flex", flexDirection: "column", alignItems: "center", padding: "22px 28px 40px" },
    },
      selectedSlide
        ? React.createElement(React.Fragment, null,
            /* 페이지 번호 / 레이아웃 배지 (캔버스 위 약간) */
            React.createElement("div", { style: { width: "100%", maxWidth: 920, display: "flex", alignItems: "center", gap: 8, marginBottom: 10 } },
              React.createElement("span", { className: "tnum", style: { fontSize: 12, fontWeight: 700, color: "#fff", background: "var(--primary)", minWidth: 30, height: 22, borderRadius: "var(--r-xs)", display: "inline-flex", alignItems: "center", justifyContent: "center", padding: "0 6px" } }, String(selectedSlide.id).padStart(2, "0")),
              React.createElement(LayoutBadge, { layout: selectedSlide.layout_suggestion, sm: true }),
              React.createElement(ConfirmBadge, { confirmed: selectedSlide.confirmed })),
            /* 자유배치 캔버스 — SlideCanvas(없으면 SlidePreview 폴백) */
            typeof SlideCanvas === "function"
              ? React.createElement(SlideCanvas, {
                  slide: selectedSlide,
                  project,
                  onObjects,
                  selectedObjId, onSelectObj,
                })
              : React.createElement("div", { style: { width: "100%", maxWidth: 920, borderRadius: "var(--r-md)", boxShadow: "var(--sh-2)", overflow: "hidden" } },
                  typeof SlidePreview === "function" ? React.createElement(SlidePreview, { slide: selectedSlide }) : null),
            /* 캔버스 바로 아래 나레이션 도크(전체폭·PPT 노트 스타일) */
            React.createElement(NarrationDock, { slide: selectedSlide, onPatch }))
        : React.createElement(StateMsg, { title: "왼쪽에서 슬라이드를 선택해요" })),

    /* 우측 패널 — 오브젝트 선택 시 속성, 미선택 시 슬라이드 메타 폼 */
    React.createElement("aside", {
      style: { width: 340, minWidth: 300, flexShrink: 0, background: "var(--bg-normal)", borderLeft: "1px solid var(--line-normal)", height: "100%", overflow: "hidden", display: "flex", flexDirection: "column" },
    },
      (selObj && typeof ObjectInspector === "function")
        ? React.createElement(ObjectInspector, {
            obj: selObj,
            onChange: patchObj, onStyle: patchObjStyle, onZ: zObj, onDelete: delObj,
          })
        : React.createElement(CutEditForm, { slide: selectedSlide, onPatch, saving, savedAt, showPreview: false, hideNarration: true })));
}

/* ── 표 뷰 ──
   컬럼: 페이지 · 코너 · 진행 · 화면 제목 · 화면구조화 · 나레이션 */
const TV_COLS = "60px 92px 90px minmax(200px,1.4fr) minmax(280px,2.2fr) minmax(240px,1.8fr)";

function TableView({ slides, selectedId, onSelect }) {
  return React.createElement("div", { style: { minWidth: 1040 } },
    React.createElement("div", {
      style: {
        display: "grid", gridTemplateColumns: TV_COLS, position: "sticky", top: 0, zIndex: 3,
        background: "var(--bg-neutral)", borderBottom: "1px solid var(--line-normal)", boxShadow: "var(--sh-1)",
      },
    },
      ["페이지", "코너", "진행", "화면 제목", "화면구조화", "나레이션"].map((h, i) =>
        React.createElement("div", {
          key: h,
          className: "t-caption-1",
          style: { padding: "11px 16px", fontWeight: 700, color: "var(--label-alt)", letterSpacing: "0.02em", borderLeft: i ? "1px solid var(--line-alt)" : "none" },
        }, h))),

    slides.map((s) => React.createElement(TableRow, { key: s.slideId || s.id, slide: s, selected: (s.slideId || s.id) === selectedId, onSelect }))
  );
}

function TableRow({ slide, selected, onSelect }) {
  const rowId = slide.slideId || slide.id;
  const cellBorder = { borderLeft: "1px solid var(--line-alt)" };
  return React.createElement("div", {
    onClick: () => onSelect(rowId),
    style: {
      display: "grid", gridTemplateColumns: TV_COLS, cursor: "pointer", position: "relative",
      background: selected ? "var(--blue-95)" : "var(--bg-normal)",
      borderBottom: "1px solid var(--line-alt)",
      boxShadow: selected ? "inset 3px 0 0 var(--primary)" : "inset 3px 0 0 transparent",
      transition: "background var(--t-fast)",
    },
    onMouseEnter: (e) => { if (!selected) e.currentTarget.style.background = "var(--bg-neutral)"; },
    onMouseLeave: (e) => { if (!selected) e.currentTarget.style.background = "var(--bg-normal)"; },
  },
    /* 페이지 = v3 id(order_idx) */
    React.createElement("div", { style: { padding: "14px 12px" } },
      React.createElement("span", { className: "tnum", style: { fontSize: 17, fontWeight: 700, color: "var(--label-normal)" } }, String(slide.id).padStart(2, "0"))),

    /* 코너 — 레이아웃 군으로 단순화 */
    React.createElement("div", { style: { ...cellBorder, padding: "14px 14px" } },
      React.createElement(LayoutBadge, { layout: slide.layout_suggestion, sm: true })),

    /* 진행 */
    React.createElement("div", { style: { ...cellBorder, padding: "14px 12px" } },
      React.createElement(ConfirmBadge, { confirmed: slide.confirmed })),

    /* 화면 제목 + 콘텐츠 키워드 */
    React.createElement("div", { style: { ...cellBorder, padding: "14px 16px", display: "flex", flexDirection: "column", gap: 7, minWidth: 0 } },
      React.createElement("span", { style: { fontSize: 14.5, fontWeight: 600, color: "var(--label-normal)", lineHeight: 1.5 } }, slide.title || React.createElement("span", { style: { color: "var(--label-assist)" } }, "제목 없음")),
      (slide.content && slide.content.length)
        ? React.createElement("div", { style: { display: "flex", flexWrap: "wrap", gap: 5 } },
            slide.content.map((c, i) => React.createElement("span", {
              key: i,
              className: "t-caption-1",
              style: { padding: "2px 7px", borderRadius: "var(--r-xs)", background: "var(--bg-alt)", color: "var(--label-neutral)", fontWeight: 600 },
            }, c)))
        : null),

    /* 화면구조화 */
    React.createElement("div", { style: { ...cellBorder, padding: "14px 16px" } },
      React.createElement(StructureCell, { slide })),

    /* 나레이션 (원문 보존 — 4줄 클램프) */
    React.createElement("div", { style: { ...cellBorder, padding: "14px 16px" } },
      slide.narration
        ? React.createElement("p", { style: { margin: 0, fontSize: 13.5, lineHeight: 1.62, color: "var(--label-neutral)", display: "-webkit-box", WebkitLineClamp: 4, WebkitBoxOrient: "vertical", overflow: "hidden" } }, slide.narration)
        : React.createElement("span", { className: "t-label-2", style: { color: "var(--label-assist)" } }, "나레이션 없음"))
  );
}

/* 화면구조화 셀 — 레이아웃·도식·카드·밀도 메타를 칩으로 요약 */
function StructureCell({ slide }) {
  const chips = [];
  const m = layoutMeta(slide.layout_suggestion);
  chips.push(React.createElement("span", {
    key: "lay",
    style: { display: "inline-flex", alignItems: "center", gap: 5, height: 21, padding: "0 8px", borderRadius: "var(--r-xs)", background: "var(--primary-light)", color: "var(--primary)", fontSize: 11, fontWeight: 700 },
  },
    React.createElement("svg", { width: 12, height: 12, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2 }, React.createElement("path", { d: "M12 2 2 7l10 5 10-5zM2 17l10 5 10-5M2 12l10 5 10-5" })),
    m.label));
  if (slide.diagram_subtype) chips.push(React.createElement(Pill, { key: "diag", color: "var(--cyan)", bg: "var(--cyan-bg)" }, `도식 · ${slide.diagram_subtype}`));
  if (slide.steps_style) chips.push(React.createElement(Pill, { key: "steps", color: "var(--label-neutral)", bg: "var(--bg-alt)" }, slide.steps_style === "numbered" ? "번호" : "화살표"));
  if (typeof slide.card_count === "number") chips.push(React.createElement(Pill, { key: "card", color: "var(--violet)", bg: "var(--violet-bg)" }, `카드 ${slide.card_count}`));
  if (slide.illustration_needed) chips.push(React.createElement(Pill, { key: "ill", color: "var(--caution)", bg: "var(--caution-bg)" }, "삽화 필요"));

  return React.createElement("div", { style: { display: "flex", flexDirection: "column", gap: 8 } },
    React.createElement("div", { style: { display: "flex", flexWrap: "wrap", gap: 6 } }, chips),
    slide.notes
      ? React.createElement("div", { style: { display: "flex", gap: 6, alignItems: "flex-start" } },
          React.createElement("span", { style: { width: 3, alignSelf: "stretch", borderRadius: 2, background: "var(--line-normal)", flexShrink: 0 } }),
          React.createElement("span", { className: "t-caption-1", style: { lineHeight: 1.5, color: "var(--label-alt)", fontWeight: 500 } }, slide.notes))
      : null
  );
}

/* ── 비주얼 뷰 (컷 카드 그리드) — visualView.jsx 패턴 승계, 데이터=API ── */
function VisualView({ slides, selectedId, onSelect }) {
  return React.createElement("div", { style: { padding: "20px 22px 60px" } },
    React.createElement("div", { style: { display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(300px,1fr))", gap: 18 } },
      slides.map((s) => React.createElement(CutCard, { key: s.slideId || s.id, slide: s, selected: (s.slideId || s.id) === selectedId, onSelect }))));
}

function CutCard({ slide, selected, onSelect }) {
  const rowId = slide.slideId || slide.id;
  return React.createElement("div", {
    onClick: () => onSelect(rowId),
    style: {
      background: "var(--bg-normal)", border: "1px solid", borderColor: selected ? "var(--primary)" : "var(--border)",
      borderRadius: "var(--r-lg)", overflow: "hidden", cursor: "pointer",
      boxShadow: selected ? "0 0 0 3px var(--primary-light), var(--sh-2)" : "var(--sh-1)", transition: "all var(--t-fast)",
    },
    onMouseEnter: (e) => { if (!selected) { e.currentTarget.style.boxShadow = "var(--sh-2)"; e.currentTarget.style.borderColor = "var(--label-disable)"; } },
    onMouseLeave: (e) => { if (!selected) { e.currentTarget.style.boxShadow = "var(--sh-1)"; e.currentTarget.style.borderColor = "var(--border)"; } },
  },
    /* head: 페이지 번호 + 레이아웃 배지 + 진행 */
    React.createElement("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between", padding: "11px 14px 9px" } },
      React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8 } },
        React.createElement("span", { className: "tnum", style: { fontSize: 13, fontWeight: 700, color: "#fff", background: "var(--primary)", minWidth: 30, height: 22, borderRadius: "var(--r-xs)", display: "inline-flex", alignItems: "center", justifyContent: "center", padding: "0 6px" } }, String(slide.id).padStart(2, "0")),
        React.createElement(LayoutBadge, { layout: slide.layout_suggestion, sm: true })),
      React.createElement(ConfirmBadge, { confirmed: slide.confirmed })),

    /* 16:9 미리보기 — 레이아웃이 적용된 실제 슬라이드 모습(SlidePreview) */
    React.createElement("div", { style: { margin: "0 14px" } },
      typeof SlidePreview === "function"
        ? React.createElement(SlidePreview, { slide })
        : React.createElement("div", { style: { aspectRatio: "16/9", background: "var(--bg-neutral)", borderRadius: "var(--r-md)", border: "1px solid var(--border)" } })),

    /* body: 나레이션 스니펫 + 메타 */
    React.createElement("div", { style: { padding: "13px 14px 15px", display: "flex", flexDirection: "column", gap: 8 } },
      React.createElement("span", { className: "t-caption-2", style: { color: "var(--label-assist)", fontWeight: 700, letterSpacing: "0.02em" } }, "나레이션"),
      slide.narration
        ? React.createElement("p", { style: { margin: 0, fontSize: 13, lineHeight: 1.6, color: "var(--label-neutral)", display: "-webkit-box", WebkitLineClamp: 3, WebkitBoxOrient: "vertical", overflow: "hidden" } }, slide.narration)
        : React.createElement("span", { className: "t-caption-1", style: { color: "var(--label-assist)" } }, "나레이션 없음"),
      (slide.diagram_subtype || slide.steps_style || typeof slide.card_count === "number")
        ? React.createElement("div", { style: { display: "flex", flexWrap: "wrap", gap: 5, paddingTop: 9, borderTop: "1px solid var(--line-alt)" } },
            slide.diagram_subtype ? React.createElement(Pill, { color: "var(--cyan)", bg: "var(--cyan-bg)" }, `도식 · ${slide.diagram_subtype}`) : null,
            slide.steps_style ? React.createElement(Pill, { color: "var(--label-neutral)", bg: "var(--bg-alt)" }, slide.steps_style === "numbered" ? "번호" : "화살표") : null,
            typeof slide.card_count === "number" ? React.createElement(Pill, { color: "var(--violet)", bg: "var(--violet-bg)" }, `카드 ${slide.card_count}`) : null)
        : null)
  );
}

/* ── 편집기 셸 ── */
function EditorView({ project, user, onBack, onLogout }) {
  const [view, setView] = useState("ppt");             // ppt(3-패널, 기본) | visual | table
  const [slides, setSlides] = useState(null);          // null=로딩 (정규화된 배열)
  const [session, setSession] = useState(null);
  const [selectedId, setSelectedId] = useState(null);
  const [selectedObjId, setSelectedObjId] = useState(null);  // PPT 캔버스 오브젝트 선택
  const [err, setErr] = useState("");
  const [conflictNote, setConflictNote] = useState(""); // 409 안내(해요체)

  /* 자동저장 상태 */
  const [version, setVersion] = useState(typeof project.version === "number" ? project.version : 0);
  const [saving, setSaving] = useState(false);
  const [savedAt, setSavedAt] = useState(null);
  const saveTimers = useRef({});       // slideId -> timeout (디바운스, 슬라이드별)
  const pendingPatch = useRef({});     // slideId -> 누적 부분필드

  /* Wave B: 마스터/원고/내보내기 상태 */
  const [masterActive, setMasterActive] = useState(project.master || null);
  const [masters, setMasters] = useState(null);
  const [sourceFileName, setSourceFileName] = useState(project.sourceFileName || null);
  const [panel, setPanel] = useState(null);              // null | "masters" | "export" | "versions" | "ai"

  const load = useCallback(async (keepSel) => {
    setErr(""); setConflictNote("");
    try {
      const data = await api(`/projects/${project.id}/slides`);
      const norm = Array.isArray(data?.slides) ? data.slides.map(normSlide) : [];
      setSlides(norm);
      setSession(data?.session || null);
      setSelectedId((cur) => {
        const want = keepSel ? cur : cur;
        if (want && norm.some((s) => (s.slideId || s.id) === want)) return want;
        return norm.length ? (norm[0].slideId || norm[0].id) : null;
      });
    } catch (ex) {
      setErr(ex.message || "슬라이드를 불러오지 못했어요.");
      setSlides([]);
    }
  }, [project.id]);
  useEffect(() => { load(); }, [load]);

  /* 프로젝트 상세 — 적용 마스터/원고/버전 (§3.2) */
  const loadProject = useCallback(async () => {
    try {
      const { project: p } = await api(`/projects/${project.id}`);
      if (p) {
        if (p.master !== undefined) setMasterActive(p.master || null);
        else if (p.masterId) setMasterActive((cur) => cur && cur.id === p.masterId ? cur : { id: p.masterId, name: cur?.name || "마스터" });
        if (p.sourceFileName !== undefined) setSourceFileName(p.sourceFileName || null);
        if (typeof p.version === "number") setVersion(p.version);
      }
    } catch (ex) { /* 비치명적 */ }
  }, [project.id]);
  useEffect(() => { loadProject(); }, [loadProject]);

  const loadMasters = useCallback(async () => {
    try {
      const data = await api("/masters");
      setMasters(Array.isArray(data?.items) ? data.items : []);
    } catch (ex) { setMasters([]); }
  }, []);
  useEffect(() => { loadMasters(); }, [loadMasters]);

  const onMasterAssigned = useCallback((master) => {
    setMasterActive(master ? { id: master.id, name: master.name } : null);
    loadMasters();
  }, [loadMasters]);

  /* 언마운트 시 보류 타이머 정리 */
  useEffect(() => () => { Object.values(saveTimers.current).forEach((t) => clearTimeout(t)); }, []);

  /* ── 단건 자동저장: 낙관적 업데이트 + 디바운스 PATCH ──
     onPatch(slideId, partial): 로컬 state 즉시 반영 → 0.7s 디바운스 후 PATCH.
     _expr 은 클라이언트 전용(서버 비저장) — PATCH 본문에서 제외. */
  const patchSlide = useCallback((slideId, partial) => {
    setSlides((cur) => (cur || []).map((s) => ((s.slideId || s.id) === slideId ? { ...s, ...partial } : s)));
    setConflictNote("");
    // 서버 전송 대상 누적(클라 전용 _expr 제외)
    const { _expr, ...serverFields } = partial;
    if (Object.keys(serverFields).length === 0) { setSavedAt(Date.now()); return; }
    pendingPatch.current[slideId] = { ...(pendingPatch.current[slideId] || {}), ...serverFields };
    if (saveTimers.current[slideId]) clearTimeout(saveTimers.current[slideId]);
    saveTimers.current[slideId] = setTimeout(() => { flushPatch(slideId); }, 700);
  }, []);

  const flushPatch = useCallback(async (slideId) => {
    const body = pendingPatch.current[slideId];
    if (!body || Object.keys(body).length === 0) return;
    pendingPatch.current[slideId] = {};
    // slideId 가 UUID 가 아닌(아직 미저장) 경우 대비: 실제 PATCH 경로엔 slideId(UUID) 필요
    setSaving(true); setErr("");
    try {
      const data = await api(`/projects/${project.id}/slides/${slideId}`, { method: "PATCH", body });
      // 서버 정규화 반영(선택 — id/order 변동 가능)
      if (data?.slide) {
        const ns = normSlide(data.slide);
        setSlides((cur) => (cur || []).map((s) => ((s.slideId || s.id) === slideId ? { ...s, ...ns, _expr: s._expr } : s)));
      }
      setSavedAt(Date.now());
    } catch (ex) {
      if (ex && ex.code === "version_conflict") {
        setConflictNote("다른 곳에서 먼저 저장됐어요. 최신 내용을 다시 불러왔어요.");
        if (typeof ex.payload?.version === "number") setVersion(ex.payload.version);
        load(true);
      } else {
        setErr(ex.message || "자동 저장에 실패했어요. 잠시 후 다시 시도해 주세요.");
      }
    } finally {
      setSaving(false);
    }
  }, [project.id, load]);

  /* ── 전체 bulkSave (PUT) — 낙관적 잠금(version). 명시적 "모두 저장" 버튼용 ── */
  const bulkSave = useCallback(async () => {
    if (!Array.isArray(slides)) return;
    setSaving(true); setErr(""); setConflictNote("");
    // 서버 전송 페이로드: v3 필드만(_expr/slideId 클라 메타 제거하되 id 보존)
    const payload = slides.map((s) => {
      const { _expr, slideId, ...rest } = s;
      return rest;
    });
    try {
      const data = await api(`/projects/${project.id}/slides`, { method: "PUT", body: { slides: payload, version } });
      if (typeof data?.version === "number") setVersion(data.version);
      if (Array.isArray(data?.slides)) setSlides(data.slides.map(normSlide));
      setSavedAt(Date.now());
    } catch (ex) {
      if (ex && ex.code === "version_conflict") {
        setConflictNote("다른 곳에서 먼저 저장됐어요. 최신 내용을 다시 불러왔어요. 변경분을 확인하고 다시 저장해 주세요.");
        if (typeof ex.payload?.version === "number") setVersion(ex.payload.version);
        load(true);
      } else {
        setErr(ex.message || "전체 저장에 실패했어요.");
      }
    } finally {
      setSaving(false);
    }
  }, [slides, version, project.id, load]);

  const selectedSlide = Array.isArray(slides)
    ? slides.find((s) => (s.slideId || s.id) === selectedId) || null
    : null;

  /* 슬라이드 전환 시 오브젝트 선택 해제 */
  useEffect(() => { setSelectedObjId(null); }, [selectedId]);

  /* 캔버스 오브젝트 저장 — objects 통째로 patchSlide(디바운스·영속) */
  const onObjects = useCallback((nextObjects) => {
    if (selectedSlide) patchSlide(selectedSlide.slideId || selectedSlide.id, { objects: nextObjects });
  }, [selectedSlide, patchSlide]);

  const title = (session?.chapterName || project.chapterName || project.courseName || "스토리보드");
  const subtitle = session?.courseName || project.courseName || "";
  const hasSlides = Array.isArray(slides) && slides.length > 0;

  return React.createElement("div", { style: { minHeight: "100vh", display: "flex", flexDirection: "column" } },
    /* 상단 바 */
    React.createElement("header", {
      style: { height: 60, display: "flex", alignItems: "center", gap: 14, padding: "0 20px", background: "var(--bg-normal)", borderBottom: "1px solid var(--line-normal)", position: "sticky", top: 0, zIndex: 20 },
    },
      React.createElement("button", { onClick: onBack, "aria-label": "대시보드로", style: { ...btnStyle("ghost", "sm"), padding: "0 8px" } },
        React.createElement("svg", { width: 18, height: 18, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round" }, React.createElement("path", { d: "M15 18l-6-6 6-6" }))),
      React.createElement(ViewToggle, { view, onChange: setView }),
      React.createElement("div", { style: { display: "flex", flexDirection: "column", lineHeight: 1.2, minWidth: 0 } },
        React.createElement("span", { className: "t-headline-2", style: { color: "var(--label-normal)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } }, title),
        subtitle ? React.createElement("span", { className: "t-caption-1", style: { color: "var(--label-alt)" } }, subtitle) : null),
      React.createElement("div", { style: { marginLeft: "auto", display: "flex", alignItems: "center", gap: 8 } },
        slides ? React.createElement("span", { className: "t-label-2 tnum", style: { color: "var(--label-alt)" } }, `슬라이드 ${slides.length}장`) : null,
        React.createElement(SaveIndicator, { saving, savedAt }),
        masterActive
          ? React.createElement(Pill, { color: "var(--cyan)", bg: "var(--cyan-bg)" },
              React.createElement("span", { style: { width: 6, height: 6, borderRadius: "50%", background: "var(--cyan)" } }), masterActive.name || "마스터")
          : null,
        /* AI 파싱 */
        React.createElement(Button, { kind: "outline", size: "sm", onClick: () => setPanel("ai") },
          React.createElement("svg", { width: 15, height: 15, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round" }, React.createElement("path", { d: "M12 3v3M12 18v3M5.6 5.6l2.1 2.1M16.3 16.3l2.1 2.1M3 12h3M18 12h3M5.6 18.4l2.1-2.1M16.3 7.7l2.1-2.1" })),
          "AI 생성"),
        /* 버전 */
        React.createElement(Button, { kind: "outline", size: "sm", onClick: () => setPanel("versions") },
          React.createElement("svg", { width: 15, height: 15, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round" }, React.createElement("path", { d: "M12 8v4l3 3" }), React.createElement("circle", { cx: 12, cy: 12, r: 9 })),
          "버전"),
        /* 마스터 */
        React.createElement(Button, { kind: "outline", size: "sm", onClick: () => setPanel("masters") },
          React.createElement("svg", { width: 15, height: 15, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round" }, React.createElement("rect", { x: 3, y: 3, width: 18, height: 18, rx: 2 }), React.createElement("path", { d: "M3 9h18M9 21V9" })),
          "마스터"),
        /* 내보내기 */
        React.createElement(Button, { kind: "primary", size: "sm", onClick: () => setPanel("export") },
          React.createElement("svg", { width: 15, height: 15, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round" }, React.createElement("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3" })),
          "내보내기"),
        React.createElement(Button, { kind: "ghost", size: "sm", onClick: onLogout }, "로그아웃"))),

    /* 충돌/오류 알림(해요체) */
    conflictNote ? React.createElement("div", { className: "t-label-2", style: { color: "var(--caution)", background: "var(--caution-bg)", margin: "12px 16px 0", borderRadius: "var(--r-sm)", padding: "9px 14px", display: "flex", alignItems: "center", gap: 8 } },
      React.createElement("svg", { width: 15, height: 15, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2.2, strokeLinecap: "round", strokeLinejoin: "round" }, React.createElement("path", { d: "M12 9v4M12 17h.01M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z" })),
      conflictNote) : null,
    err ? React.createElement("div", { className: "t-label-2", style: { color: "var(--negative)", background: "var(--negative-bg)", margin: "12px 16px 0", borderRadius: "var(--r-sm)", padding: "10px 14px" } }, err) : null,

    /* 본문 — 로딩/빈 상태는 전체폭, 그 외엔 뷰별 레이아웃
       (PPT 뷰=좌 썸네일·중앙 큰 슬라이드·우 편집옵션 3-패널 / 표·스토리보드 뷰=중앙+우 폼 스플릿) */
    React.createElement("main", { style: { flex: 1, overflow: "hidden", background: "var(--bg-canvas)", display: "flex", minHeight: 0 } },
      slides === null
        ? React.createElement("div", { style: { flex: 1 } }, React.createElement(StateMsg, { icon: React.createElement(Spinner, { s: 24 }), title: "슬라이드를 불러오는 중이에요" }))
        : slides.length === 0
          ? React.createElement("div", { style: { flex: 1, padding: 24, overflow: "auto" } },
              React.createElement("div", { style: { border: "1px dashed var(--border)", borderRadius: "var(--r-lg)", background: "var(--bg-normal)" } },
                React.createElement(StateMsg, {
                  title: "아직 슬라이드가 없어요",
                  desc: "원고 PDF를 올리고 'AI 생성'을 실행하면 슬라이드가 채워져요.",
                  action: React.createElement("div", { style: { display: "flex", gap: 8 } },
                    React.createElement(Button, { kind: "primary", onClick: () => setPanel("ai") }, "AI로 스토리보드 생성"),
                    React.createElement(Button, { kind: "outline", onClick: () => setPanel("export") }, "원고 올리기")),
                })))
          : view === "ppt"
            /* PPT 3-패널 (좌 썸네일 · 중앙 큰 슬라이드 · 우 편집옵션) */
            ? React.createElement(PptView, {
                project,
                slides, selectedId, onSelect: setSelectedId, selectedSlide,
                onPatch: (partial) => { if (selectedSlide) patchSlide(selectedSlide.slideId || selectedSlide.id, partial); },
                saving, savedAt,
                selectedObjId, onSelectObj: setSelectedObjId, onObjects,
              })
            /* 표 뷰 · 스토리보드(카드 그리드) 뷰 — 중앙 + 우 편집폼 스플릿 */
            : React.createElement(React.Fragment, null,
                React.createElement("div", { style: { flex: 1, overflow: "auto", minWidth: 0 } },
                  view === "table"
                    ? React.createElement("div", { style: { padding: "16px 20px 60px" } },
                        React.createElement("div", { style: { border: "1px solid var(--line-normal)", borderRadius: "var(--r-md)", overflow: "hidden", background: "var(--bg-normal)" } },
                          React.createElement(TableView, { slides, selectedId, onSelect: setSelectedId })))
                    : React.createElement(VisualView, { slides, selectedId, onSelect: setSelectedId })),
                hasSlides ? React.createElement("aside", {
                  style: { width: 380, minWidth: 320, flexShrink: 0, background: "var(--bg-normal)", borderLeft: "1px solid var(--line-normal)", height: "100%", overflow: "hidden", display: "flex", flexDirection: "column" },
                },
                  React.createElement(CutEditForm, { slide: selectedSlide, onPatch: (partial) => { if (selectedSlide) patchSlide(selectedSlide.slideId || selectedSlide.id, partial); }, saving, savedAt })
                ) : null)
    ),

    /* ── Wave B/C 모달 ── */
    panel === "masters" ? React.createElement(MasterManagerModal, {
      project, activeMasterId: masterActive?.id || null,
      onAssigned: (master) => { onMasterAssigned(master); },
      onClose: () => setPanel(null),
    }) : null,

    panel === "export" ? React.createElement(ExportPanelModal, {
      project, masterActive, masters, sourceFileName,
      onSourceUploaded: (name) => setSourceFileName(name),
      onMasterChanged: (masterId) => {
        if (!masterId) { setMasterActive(null); return; }
        const m = (masters || []).find((x) => x.id === masterId);
        setMasterActive(m ? { id: m.id, name: m.name } : { id: masterId, name: "마스터" });
      },
      onOpenMasters: () => setPanel("masters"),
      onClose: () => setPanel(null),
    }) : null,

    panel === "versions" ? React.createElement(VersionsModal, {
      project,
      onRestored: (restoredSlides) => {
        if (Array.isArray(restoredSlides)) {
          const norm = restoredSlides.map(normSlide);
          setSlides(norm);
          setSelectedId(norm.length ? (norm[0].slideId || norm[0].id) : null);
        } else {
          load(false);
        }
        loadProject();
      },
      onClose: () => setPanel(null),
    }) : null,

    panel === "ai" ? React.createElement(AiParseModal, {
      project, sourceFileName,
      onParsed: () => { load(false); loadProject(); },
      // 모달 내부 인라인 업로드 성공 시 에디터 메타(sourceFileName) 동기화 — 패널 점프 없음
      onUploaded: (name) => { setSourceFileName(name); loadProject(); },
      onClose: () => setPanel(null),
    }) : null
  );
}

Object.assign(window, { EditorView, ViewToggle, TableView, VisualView, CutCard, LayoutBadge, ConfirmBadge, StructureCell, SlideStrip, PptView, NarrationDock });
