/* 자동 탭 — 분석(SSE) → 검토(ID별 탭에서 카드 선택) → 순차 빌드.
   카드 DOM·캡처 방식은 h-lab comment-cards 와 동일 계열(modern-screenshot, 4배). */
(function(){
const $=(s)=>document.querySelector(s);
let A=null;                 // 1차 결과(분석) {highlights}  — 편집안·타이틀 후보만, 댓글 없음
let P=null;                 // 2차 결과(준비) {highlights:[{id,cuts,need,cutRanges,matched}],comments,candidates,warnings}
let AUTO_AID=null;          // /auto/analyze 가 돌려준 analysis_id — /auto/prepare 에 넘긴다
let PREP_ID=null;           // /auto/prepare 가 돌려준 prepare_id — SSE 구독용
let TITLE_PICKS={};         // hl.id → {top,main} — "준비 시작" 클릭 시점의 제목 선택을 잠가 둔다
let byIdx={};               // idx → comment
let sel={};                 // hl.id → idx 배열(선택 순서 유지: matched 먼저)
let curId=null;             // 현재 보고 있는 하이라이트 ID
let dropped=new Set();      // 1차 검토에서 X로 제외한 ID(문자열) — 준비에서 제외. 되돌리기 가능
const isDropped=(id)=>dropped.has(String(id));

/* ── h-lab timeAgo 이식 ── */
function timeAgo(iso){
  if(!iso) return "";
  const then=new Date(iso).getTime(); if(isNaN(then)) return "";
  const sec=Math.floor((Date.now()-then)/1000);
  const u=[["년",31536000],["개월",2592000],["일",86400],["시간",3600],["분",60]];
  for(const [l,s] of u){const v=Math.floor(sec/s); if(v>=1) return v+l+" 전";}
  return "방금 전";
}
function esc(s){const d=document.createElement("div");d.textContent=String(s==null?"":s);return d.innerHTML;}
function fmtT(sec){const m=Math.floor(sec/60),s=Math.floor(sec%60);return m+":"+String(s).padStart(2,"0");}
let YT_HL=null;              // 유튜브 구간 탭의 가상 하이라이트 {id:"yt", need}
let PASTE_HL=null;           // 📋 붙여넣기 탭의 가상 하이라이트 {id:"paste", need, cuts, paste:{cuts}}
function hlById(id){
  if(id==="yt") return YT_HL;
  if(id==="paste") return PASTE_HL;
  // 자동 탭: cuts/need 는 2차(prepare) 결과에만 있다 — 카드 선택은 항상 2차에서만 일어난다.
  return P&&P.highlights.find(h=>h.id===id);
}

/* ── 카드 DOM (h-lab renderCards 구조와 동일) ── */
function cardEl(c,hlId,ci){
  const wrap=document.createElement("div");
  wrap.className="ccwrap"; wrap.dataset.cidx=c.idx;
  // 컷 소속을 DOM에 남긴다 — 없으면(폴백/구간 탭) 어느 복사본이 어느 컷 것인지 구분 불가해
  // refreshSel 이 모든 복사본에 선택 링을 붙이는 버그가 난다(컷마다 후보 전체가 반복 렌더되므로).
  if(ci!==undefined) wrap.dataset.ci=ci;
  const card=document.createElement("div");
  card.className="comment-card mosaic";        // 검정배경은 CSS 기본값
  const av=c.profileImageUrl?"/auto/avatar?url="+encodeURIComponent(c.profileImageUrl):"";
  card.innerHTML=
    '<div class="cc-head">'+
      '<img class="cc-avatar" loading="lazy" decoding="async" alt="" src="'+av+'">'+
      '<div class="cc-meta">'+
        '<div><span class="cc-author"></span><span class="cc-time"></span></div>'+
        '<div class="cc-text"></div>'+
        '<div class="cc-stats"><span>👍 '+(c.likeCount||0).toLocaleString()+'</span>'+
        '<span>💬 '+(c.replyCount||0).toLocaleString()+'</span></div>'+
      '</div></div>';
  card.querySelector(".cc-author").textContent=c.authorName||"";
  card.querySelector(".cc-time").textContent=timeAgo(c.publishedAt);
  // textDisplay(HTML) → 평문 (h-lab toPlainText와 동일)
  const tmp=document.createElement("div");
  tmp.innerHTML=String(c.text||"").replace(/<br\s*\/?>/gi,"\n");
  card.querySelector(".cc-text").textContent=tmp.textContent||"";
  const badge=document.createElement("div");
  badge.className="ccbadge"; badge.textContent="✓";
  wrap.appendChild(card); wrap.appendChild(badge);
  wrap.addEventListener("click",()=>toggle(hlId,c.idx,ci));
  return wrap;
}

/* ci를 주면 그 컷의 복사본을 우선으로 찾는다. 안 주거나 못 찾으면 DOM에서 처음 만나는
   복사본으로 폴백(예전 동작·폴백 패널). ci를 안 주고 querySelector 하나만 쓰면 '선택한
   것만 보기'가 켜졌을 때 배정 안 된(=display:none인) 쌍둥이가 먼저 잡혀 캡처가 빈
   이미지가 되거나 스크롤이 숨은 카드로 튀는 사고가 난다 — 반드시 ci를 넘겨라. */
function wrapOf(hlId,idx,ci){
  const q="#hlbox-"+hlId+" .ccwrap[data-cidx='"+idx+"']";
  return (ci!==undefined&&$(q+"[data-ci='"+ci+"']"))||$(q);
}

/* ── 카드 섹션 — 30장씩 렌더 + 더보기 (전체 댓글을 받아도 DOM은 점진 생성) ── */
const CARD_PAGE=30;
// '좋아요 상위에서 채우기' 섹션은 컷마다 반복된다 — 초기 렌더를 줄여 DOM 폭증을 막는다
const CUT_FILL_PAGE=6;
/* 검색용 평문(본문+작성자, 소문자) — 댓글마다 1회 계산해 캐시 */
function searchStr(c){
  if(c._st===undefined){
    const tmp=document.createElement("div");
    tmp.innerHTML=String(c.text||"").replace(/<br\s*\/?>/gi,"\n");
    c._st=((tmp.textContent||"")+" "+(c.authorName||"")).toLowerCase();
  }
  return c._st;
}
const SECS={};               // hlId → [setFilter,…] (searchBar 가 패널 생성 때 초기화)
function cardSection(label,list,hlId,firstBatch,ci){
  const sec=document.createElement("div");sec.className="hlsec";
  const head=document.createElement("div");head.textContent=label;
  sec.appendChild(head);
  const grid=document.createElement("div");grid.className="cardsec";
  sec.appendChild(grid);
  const more=document.createElement("button");
  more.type="button";more.className="ghost cc-more";
  const first=firstBatch||CARD_PAGE;
  let cur=list,shown=0,baseShown=0;   // baseShown: 검색 전 렌더 수 — 해제 시 복원해야
                                      // 선택 카드가 DOM에 남는다(캡처는 wrapOf 로 DOM에서 찾음)
  function render(batch){
    const end=Math.min(cur.length,shown+batch);
    for(;shown<end;shown++){
      const c=byIdx[cur[shown]];
      if(!c) continue;
      const w=cardEl(c,hlId,ci);
      // refreshSel과 같은 규칙(배정 컷 없음=폴백, 있으면 이 섹션의 컷과 같을 때만 링).
      // 이 섹션의 모든 카드는 같은 ci를 공유하므로 DOM dataset을 다시 읽을 필요 없이
      // 클로저의 ci로 바로 판정한다 — '더보기'·검색 재렌더로 새로 그려질 때마다 필요하고,
      // refreshSel을 호출하는 방식은 패널이 아직 document에 안 붙어 있으면(최초 구성 중)
      // 조용히 no-op 되므로 쓰지 않는다.
      const assigned=(selCut[hlId]||{})[c.idx];
      if(sel[hlId]&&sel[hlId].includes(c.idx)&&(assigned===undefined||assigned===ci))
        w.classList.add("sel");
      grid.appendChild(w);
    }
    if(cur===list) baseShown=Math.max(baseShown,shown);
    if(shown>=cur.length) more.style.display="none";
    else{
      more.style.display="";
      more.textContent="더보기 ▾  (남은 "+(cur.length-shown).toLocaleString()+"장)";
    }
    applyUsedMarks(hlId);        // 새로 그린 카드에도 사용중 표시
  }
  more.addEventListener("click",()=>render(CARD_PAGE));
  sec.appendChild(more);
  render(first);
  (SECS[hlId]=SECS[hlId]||[]).push(function setFilter(q){
    grid.innerHTML="";shown=0;
    if(!q){cur=list;sec.style.display="";render(Math.max(baseShown,first));return;}
    cur=list.filter(i=>{const c=byIdx[i];return c&&searchStr(c).includes(q);});
    sec.style.display=cur.length?"":"none";
    render(CARD_PAGE);
  });
  return sec;
}
/* 검색 중엔 닫힌 컷 안의 결과도 봐야 하므로 그 패널의 컷 아코디언을 전부 펼친다.
   검색어를 지우면 검색 시작 전에 열려 있던 컷으로 되돌린다 — 그냥 첫 컷으로 리셋하면
   사용자가 보던 컷을 잃어버린다. 컷 개념이 없는 폴백 패널은 .cutgrp 가 없어 자동 no-op. */
function syncCutGroupsForSearch(hlId,q){
  const box=$("#hlbox-"+hlId); if(!box) return;
  const grps=box.querySelectorAll(".cutgrp"); if(!grps.length) return;
  if(q){
    if(box.dataset.presearchOpen===undefined)
      box.dataset.presearchOpen=JSON.stringify(Array.from(grps).map(g=>g.classList.contains("open")));
    grps.forEach(g=>g.classList.add("open"));
  }else if(box.dataset.presearchOpen!==undefined){
    const opens=JSON.parse(box.dataset.presearchOpen);
    grps.forEach((g,i)=>g.classList.toggle("open",!!opens[i]));
    delete box.dataset.presearchOpen;
  }
}
/* ── 패널 검색창 — 입력하면 그 패널의 모든 섹션이 일치 카드만 다시 그림 ── */
function searchBar(hlId){
  SECS[hlId]=[];               // 패널 재생성 시 이전 섹션 필터 폐기
  const bar=document.createElement("div");
  bar.className="ccsearch";
  bar.innerHTML='<input type="search" placeholder="🔍 댓글 검색 (내용·작성자)">'+
    '<button type="button" class="ghost ccsx" title="검색 지우기">✕</button>';
  const inp=bar.querySelector("input");
  let t=null;
  function apply(){
    const q=inp.value.trim().toLowerCase();
    syncCutGroupsForSearch(hlId,q);
    (SECS[hlId]||[]).forEach(f=>f(q));
  }
  inp.addEventListener("input",()=>{clearTimeout(t);t=setTimeout(apply,150);});
  bar.querySelector(".ccsx").addEventListener("click",()=>{inp.value="";apply();inp.focus();});
  bar._clear=()=>{if(inp.value){inp.value="";clearTimeout(t);apply();}};
  return bar;
}
/* 빌드/캡처 전 필수 — 검색으로 카드가 DOM에서 빠진 채 캡처하면 조용히 누락된다 */
function clearSearch(hlId){
  const box=$("#hlbox-"+hlId);
  const bar=box&&box.querySelector(".ccsearch");
  if(bar&&bar._clear) bar._clear();
}
/* ── 컷 아코디언 — 컷마다 '추천 + 채우기' 두 섹션을 한 블록으로 접는다.
   한 번에 하나만 펼쳐 화면에 후보가 다닥다닥 쌓이는 걸 막는다(카드는 컷 수만큼 DOM에
   복제되므로 전부 펼치면 매우 길어진다). 헤더 채움 상태는 refreshSel 에서 갱신한다. */
function cutGroup(ci,title,sec,quota,isOpen){
  const grp=document.createElement("div");
  grp.className="cutgrp"+(isOpen?" open":"");
  grp.dataset.ci=ci; grp.dataset.sec=sec; grp.dataset.quota=quota;
  const head=document.createElement("button");
  head.type="button"; head.className="cutgrp-head";
  head.innerHTML='<span class="chev" aria-hidden="true">▶</span>'+
    '<span class="cgtitle"></span><span class="cgstat"></span>';
  head.querySelector(".cgtitle").textContent=title;
  head.addEventListener("click",()=>{
    const opening=!grp.classList.contains("open");
    grp.parentElement.querySelectorAll(":scope > .cutgrp").forEach(g=>g.classList.remove("open"));
    grp.classList.toggle("open",opening);
  });
  const body=document.createElement("div"); body.className="cutgrp-body";
  grp.appendChild(head); grp.appendChild(body);
  return {grp,body};
}
/* ── 카드 패널 렌더 — 자동/구간/붙여넣기 세 탭이 공유한다.
   갈라 두면 한 곳만 고치는 실수가 난다(배지·검색·선택 상한이 전부 여기 모여 있다). ── */
function renderCutPanel(box,panelId,data,opts){
  const unit=(opts&&opts.unit)||"컷";
  const WHY={ts:"⭐",ai:"🤖",word:"🔤",like:"➕"};
  box.appendChild(searchBar(panelId));   // 카드 섹션들 위 — cardSection 보다 먼저 만들어야 SECS 초기화됨
  const cand=(data.candidates||[]).filter(i=>byIdx[i]!==undefined);
  if(data.cuts){
    /* 컷별 추천 — 컷마다 두 섹션. 독립 ➕ 섹션을 두지 않는 이유: 거기서 고른 카드는
       컷 소속(ci)이 없어 빌드 때 갈 곳이 없다. quota 미달분을 ➕ 에서 채우는 게 주 워크플로다.
       rest(추천 뒤 '나머지')는 그 컷 시간대를 언급한 댓글만 — 안 그러면 컷마다 전체가 중복된다. */
    for(const cu of data.cuts){
      const rec=(cu.picks||[]).map(p=>p.idx).filter(i=>byIdx[i]!==undefined);
      const why=(cu.picks||[]).map(p=>WHY[p.why]||"").join("");
      const cs=(data.cutRanges||[])[cu.i]||{start:0,end:-1};
      const rest=(data.matched||[]).filter(i=>
        byIdx[i]!==undefined&&!rec.includes(i)&&
        (byIdx[i].times||[]).some(t=>t>=cs.start&&t<=cs.end));
      const {grp,body}=cutGroup(cu.i,unit+" "+(cu.i+1),cu.sec,cu.quota,cu.i===0);
      body.appendChild(cardSection(
        unit+" "+(cu.i+1)+" · "+cu.sec+"초 · 카드 "+cu.quota+"장"+
        (cu.bottom?"  — "+cu.bottom:"")+(why?"   "+why:""),
        rec.concat(rest),panelId,Math.max(CARD_PAGE,rec.length),cu.i));
      // 부족분 채우기 — 컷마다 반복되므로 처음엔 조금만 그리고 나머지는 더보기로.
      // 이 컷 추천(rec)에 이미 나온 카드는 뺀다 — 안 빼면 같은 컷 그룹 안에 같은 댓글이
      // 두 장 뜬다(다른 컷의 복사본은 '이동'용이라 남기지만, 같은 컷 안은 순수 중복).
      const fill=cand.filter(i=>!rec.includes(i));
      if(fill.length)
        body.appendChild(cardSection(
          "↳ 좋아요 상위에서 채우기 "+fill.length+"장 (이 컷에 넣기)",
          fill,panelId,CUT_FILL_PAGE,cu.i));
      box.appendChild(grp);
    }
  }else{
    // 폴백 — cuts 정보가 없으면(구간 탭·구정보 없음) 예전 화면 그대로: ⭐ + 독립 ➕
    // ⚠ 이 ⭐ 문구는 예전부터 "이 구간을"로 고정돼 있었다(unit="컷"이어도) — 화면 불변 원칙상 유지
    const m=(data.matched||[]).filter(i=>byIdx[i]!==undefined);
    box.appendChild(cardSection(
      "⭐ "+(unit==="컷"?"이 구간":unit)+"을 언급한 댓글 "+m.length+"장 (좋아요순, 자동 선택)",
      m,panelId,Math.max(CARD_PAGE,(sel[panelId]||[]).length)));
    if(cand.length){
      box.appendChild(cardSection(
        "➕ 좋아요 상위 후보 "+cand.length+"장 (부족분 클릭)",
        cand,panelId,CARD_PAGE));
    }
  }
}
/* ── 컷별 선택 ── 카드는 '어느 컷 소속'인지가 배치를 정한다.
   selCut[hlId][idx] = 컷 인덱스. sel[hlId] 는 항상 컷 순서로 정렬해 둔다
   (업로드 순서 = 배치 순서라서). */
let selCut={};
function sortSel(hlId){
  const m=selCut[hlId]||{};
  sel[hlId].sort((a,b)=>(m[a]??999)-(m[b]??999));
}
function cutFull(hl,ci){
  const m=selCut[hl.id]||{};
  const q=(hl.cuts&&hl.cuts[ci])?hl.cuts[ci].quota:hl.need;
  return sel[hl.id].filter(i=>m[i]===ci).length>=q;
}
function toggle(hlId,idx,ci){
  const hl=hlById(hlId), list=sel[hlId];
  selCut[hlId]=selCut[hlId]||{};
  const at=list.indexOf(idx);
  if(at>=0){
    const cur=selCut[hlId][idx];
    // 이미 다른 컷에 배정된 댓글의 쌍둥이 카드를 누르면 해제가 아니라 '그 컷으로 옮기기'다
    // — 사용자는 이 댓글을 지우려는 게 아니라 지금 보고 있는 컷에 넣으려는 것.
    if(hl.cuts&&ci!==undefined&&cur!==undefined&&cur!==ci){
      if(cutFull(hl,ci)) return;               // 옮길 컷이 이미 꽉 참 — 아무것도 안 함
      selCut[hlId][idx]=ci;
      sortSel(hlId);                           // 업로드 순서 = 컷 순서 유지
    }else{
      list.splice(at,1); delete selCut[hlId][idx];
    }
  }else{
    if(hl.cuts&&ci!==undefined){
      if(cutFull(hl,ci)) return;               // 그 컷 장수 초과 방지
      selCut[hlId][idx]=ci;
    }else{
      if(list.length>=hl.need) return;         // 폴백(컷 정보 없음)
    }
    list.push(idx);
    sortSel(hlId);
  }
  refreshSel(hlId);
}
/* 선택 상태를 카드 링·요약 칩·탭 카운터에 일괄 반영 */
function refreshSel(hlId){
  const hl=hlById(hlId), list=sel[hlId];
  const box=$("#hlbox-"+hlId); if(!box) return;
  const m=selCut[hlId]||{};
  // 컷별 채우기 섹션은 같은 댓글의 복사본을 컷마다 다시 그린다 — 링은 '배정된 컷'의
  // 복사본에만 붙인다(배정이 없으면 폴백 패널이라 항상 붙인다). 그래야 1컷에서 고른 카드가
  // 2컷 복사본에도 ✓로 보여 헷갈리는 일이 없다. 나머지 복사본은 applyUsedMarks 가 표시한다.
  box.querySelectorAll(".ccwrap").forEach(w=>{
    const idx=parseInt(w.dataset.cidx,10);
    const assigned=m[idx];
    const wCi=("ci" in w.dataset)?parseInt(w.dataset.ci,10):undefined;
    const isThisCopy=(assigned===undefined)||(assigned===wCi);
    w.classList.toggle("sel",list.includes(idx)&&isThisCopy);
  });
  // 컷 아코디언 헤더 채움 상태(선택 수/quota) — 선택이 바뀔 때마다 갱신
  box.querySelectorAll(".cutgrp").forEach(g=>{
    const ci=parseInt(g.dataset.ci,10);
    const cnt=list.filter(i=>m[i]===ci).length;
    const stat=g.querySelector(".cgstat");
    if(stat) stat.textContent="· "+g.dataset.sec+"초 · "+cnt+"/"+g.dataset.quota+"장";
  });
  applyUsedMarks(hlId);        // 크로스 ID 사용중 + 같은 ID 안 다른 컷 쌍둥이 표시(한 곳에서 관리)
  // 탭 카운터
  const tab=$("#idtab-"+hlId);
  if(tab){
    const ts=tab.querySelector(".ts");
    const off=isDropped(hlId);
    ts.textContent=off?"제외됨 · 생성 안 함":("카드 "+list.length+"/"+hl.need);
    tab.classList.toggle("full",!off&&list.length>=hl.need);
  }
  // 헤더 카운터
  const cnt=$("#hlcnt-"+hlId);
  if(cnt) cnt.textContent=list.length+" / "+hl.need+"장 선택";
  // 선택 요약 칩
  const bar=$("#hlsel-"+hlId);
  if(bar){
    bar.innerHTML='<span class="selbar-l">✔ 선택한 댓글</span>';
    list.forEach((idx,i)=>{
      const c=byIdx[idx]; if(!c) return;
      const chip=document.createElement("span");
      chip.className="selchip";
      chip.innerHTML='<span class="no">'+(i+1)+'</span><span class="nm"></span>'+
        '<span class="x" title="선택 해제">✕</span>';
      chip.querySelector(".nm").textContent=
        (c.authorName||"익명")+" · 👍"+(c.likeCount||0).toLocaleString();
      chip.title=String(c.text||"").slice(0,120);
      chip.addEventListener("click",(e)=>{         // 칩 클릭 = 카드로 스크롤
        if(e.target.classList.contains("x")) return;
        const ci=(selCut[hlId]||{})[idx];
        if(ci!==undefined){        // 카드가 속한 컷 아코디언을 펼쳐야 스크롤 결과가 보인다
          box.querySelectorAll(".cutgrp").forEach(g=>
            g.classList.toggle("open",parseInt(g.dataset.ci,10)===ci));
        }
        const w=wrapOf(hlId,idx,ci);   // ci를 넘겨 배정된(=보이는) 복사본을 정확히 찾는다
        if(w) w.scrollIntoView({behavior:"smooth",block:"center"});
      });
      chip.querySelector(".x").addEventListener("click",()=>toggle(hlId,idx));
      bar.appendChild(chip);
    });
    if(!list.length){
      const em=document.createElement("span");
      em.className="selbar-l"; em.textContent="없음 — 아래 카드를 클릭해 선택";
      bar.appendChild(em);
    }
    // 선택만 보기 토글(유지)
    const lb=document.createElement("label");
    lb.className="selonly-label";
    const on=box.classList.contains("selonly");
    lb.innerHTML='<input type="checkbox" '+(on?"checked ":"")+
      'style="accent-color:var(--accent);width:14px;height:14px;"> 선택한 것만 보기';
    lb.querySelector("input").addEventListener("change",(e)=>
      box.classList.toggle("selonly",e.target.checked));
    bar.appendChild(lb);
  }
}

/* ── 다른 ID에서 이미 선택한 댓글 표시 ── */
function usedByOthers(hlId,idx){
  const ids=[];
  for(const k in sel){
    if(String(k)===String(hlId)) continue;
    if(isDropped(k)) continue;                 // 제외된 ID는 '사용중'으로 치지 않음
    if(sel[k]&&sel[k].includes(idx)) ids.push(k);
  }
  return ids;
}
function applyUsedMarks(hlId){
  const box=$("#hlbox-"+hlId); if(!box) return;
  const hl=hlById(hlId);
  const m=(hl&&hl.cuts)?(selCut[hlId]||{}):null;   // 컷 개념이 있는 패널만 쌍둥이 체크
  box.querySelectorAll(".ccwrap").forEach(w=>{
    const idx=parseInt(w.dataset.cidx,10);
    let text=null;
    // ① 같은 ID 안에서, 이 댓글이 실제로 배정된 컷과 다른 컷의 복사본 — '이동' 대상임을 알려준다
    if(m){
      const assigned=m[idx];
      const wCi=("ci" in w.dataset)?parseInt(w.dataset.ci,10):undefined;
      if(assigned!==undefined&&wCi!==undefined&&assigned!==wCi)
        text="컷 "+(assigned+1)+"에서 사용중";
    }
    // ② 다른 ID(탭)에서 이미 선택한 댓글 — 기존 표시(문구 구분 유지)
    if(!text){
      const ids=usedByOthers(hlId,idx);
      if(ids.length) text="ID "+ids.join("·")+" 사용중";
    }
    w.classList.toggle("used",!!text);
    let b=w.querySelector(".ccused");
    if(text){
      if(!b){b=document.createElement("div");b.className="ccused";w.appendChild(b);}
      b.textContent=text;
      w.title=text;
    }else{
      if(b) b.remove();
      w.removeAttribute("title");
    }
  });
}

/* ── 댓글 카드 영역 접기/펼치기 ── */
function setFolded(on){
  const R=$("#autoReview");if(!R)return;
  R.classList.toggle("folded",on);
  const b=$("#ccFoldBtn");
  if(b){
    b.querySelector(".tt").textContent=on?"▾ 댓글 펼치기":"▴ 댓글 접기";
    b.querySelector(".ts").textContent=on?"카드 숨김 상태":"카드 표시 상태";
  }
}

/* ── ID 탭 전환 ── */
function showId(hlId){
  if(isDropped(hlId)) return;    // 제외된 ID는 열지 않음
  curId=hlId;
  document.querySelectorAll(".hlbox:not(.errbox)").forEach(b=>
    b.classList.toggle("show",b.id==="hlbox-"+hlId));
  document.querySelectorAll(".idtab").forEach(t=>
    t.classList.toggle("active",t.id==="idtab-"+hlId));
  applyUsedMarks(hlId);          // 탭 열 때 최신 선택 상태로 갱신
}

/* ── 1차 검토: ID 제외 / 되돌리기 ──
   지우지 않고 '제외' 상태로만 둔다 → 실수로 눌러도 한 번에 복구 가능(되돌리기 지원).
   제외된 ID는 /auto/prepare 에 안 넘어간다 — 받아쓰기를 아예 안 돌려 낭비가 없다(Task 7). */
function liveIds(){
  if(!A) return [];
  return A.highlights.filter(h=>!h.error&&!isDropped(h.id)).map(h=>h.id);
}
function updatePrepBtn(){
  const btn=$("#autoPrepGo"); if(!btn||!A) return;
  const n=liveIds().length;
  btn.disabled=(n===0);
  btn.textContent=n?(n+"개 준비 시작"):"준비할 ID가 없습니다 — 제외를 해제하세요";
}
function togglePickDrop(hlId){
  const on=!isDropped(hlId);
  if(on) dropped.add(String(hlId)); else dropped.delete(String(hlId));
  const card=$("#pick-"+hlId);
  if(card) card.classList.toggle("dropped-pick",on);
  const btn=$("#pickx-"+hlId);
  if(btn){
    btn.textContent=on?"↺ 되돌리기":"✕ 제외";
    btn.title=on?"다시 포함":"이 ID 제외 (준비 안 함)";
  }
  updatePrepBtn();
}
/* ── 2차 검토: '전부 만들기' 버튼 — 준비(prepare)를 마친 편집안 개수 기준 ── */
function updateBuildBtn(){
  const btn=$("#autoBuild"); if(!btn||!P) return;
  const n=(P.highlights||[]).length;
  btn.disabled=(n===0);
  btn.textContent=n?(n+"개 전부 만들기"):"만들 편집안이 없습니다";
}

/* ── 영상 편집안(JSON 컷) — 기본 접힘 ──
   hl.paste.cuts = [{start, end, bottom, effect}] (start/end 는 원본 영상 기준 초). */
function cutsSection(hl){
  const cuts=(hl.paste&&hl.paste.cuts)||[];
  const d=document.createElement("details");
  d.className="cutsbox";
  const sum=document.createElement("summary");
  sum.innerHTML='<span class="chev" aria-hidden="true">▶</span>'+
    '<span class="ctitle">영상 편집안</span>'+
    '<span class="cprev"></span>'+
    '<span class="cnum tnum">컷 '+cuts.length+"개 · "+(hl.total!=null?hl.total:0)+"초</span>";
  const first=cuts.map(c=>(c.bottom||"").split("\n")[0].trim()).filter(Boolean)[0]||"";
  sum.querySelector(".cprev").textContent=first?("— "+first):"";
  d.appendChild(sum);

  const list=document.createElement("div");
  list.className="cutlist";
  cuts.forEach((c,i)=>{
    const row=document.createElement("div");
    row.className="cutrow";
    const dur=Math.max(0,(c.end||0)-(c.start||0));
    row.innerHTML='<span class="cno">'+(i+1)+"</span>"+
      '<span class="ctime">'+fmtT(c.start||0)+"~"+fmtT(c.end||0)+
        '<span class="cdur">'+dur.toFixed(1)+"초</span></span>"+
      '<span class="cbody"><span class="cbot"></span><span class="ceff"></span></span>';
    const bot=String(c.bottom||"").trim();
    const bEl=row.querySelector(".cbot");
    bEl.textContent=bot||"(자막 없음)";
    if(!bot) bEl.classList.add("cempty");
    const eff=String(c.effect||"").trim();
    if(eff) row.querySelector(".ceff").textContent=eff;
    else row.querySelector(".ceff").remove();
    list.appendChild(row);
  });
  if(!cuts.length){
    const em=document.createElement("div");
    em.className="cutrow";em.style.gridTemplateColumns="1fr";
    em.textContent="컷이 없습니다.";
    list.appendChild(em);
  }
  d.appendChild(list);

  // JSON 원문(더 깊이 접어둠) + 복사
  const raw=document.createElement("details");
  raw.className="cutraw";
  const rs=document.createElement("summary");
  rs.textContent="{ } JSON 원문";
  raw.appendChild(rs);
  const pre=document.createElement("pre");
  pre.textContent=JSON.stringify(hl.paste,null,1);
  raw.appendChild(pre);
  const cp=document.createElement("button");
  cp.type="button";cp.className="ghost cutcopy";cp.textContent="JSON 복사";
  cp.addEventListener("click",async()=>{
    try{await navigator.clipboard.writeText(pre.textContent);cp.textContent="복사됨";}
    catch(e){cp.textContent="복사 실패";}
    setTimeout(()=>{cp.textContent="JSON 복사";},1200);
  });
  raw.appendChild(cp);
  d.appendChild(raw);
  return d;
}

/* ── 방식 선택 ── */
function curMode(){
  const r=document.querySelector('input[name="amode"]:checked');
  return r?r.value:"full";
}
// ⚠ 네 모드 전부 /auto/prepare 가 댓글 매칭을 위해 asr_bottom=True 로 고정해서 돈다.
// 무음 제거는 패널의 #autoRmsilence 체크박스(기본 켬)를 폼으로 넘겨 선택 가능 —
// 댓글 매칭은 원본 시각(orig) 기준이라 무음 제거를 꺼도 어긋나지 않는다.
// (공통 옵션의 "무음 제거" 체크박스는 자동 탭에서 여전히 숨김 — setMode() 참조.)
const MODE_HELP={
  full:"Gemini가 영상에서 하이라이트 구간 5개를 골라 편집안을 만들고, h-lab에서 그 구간을 언급한 댓글을 찾아옵니다. 받아쓰기(Whisper)는 댓글 매칭을 위해 항상 적용되고, 무음 제거는 위 체크박스로 켜고 끕니다.",
  whole:"Gemini가 구간 5개만 고르고(Step1), 각 구간을 컷 편집 없이 통짜로 만듭니다. 받아쓰기(Whisper)는 댓글 매칭을 위해 항상 적용되고, 무음 제거는 위 체크박스로 켜고 끕니다.",
  wpaste:"Gemini를 쓰지 않습니다. 구간 JSON(candidates 5개)을 붙여넣으면 그 구간을 그대로 통짜로 만들고, 제목은 JSON의 title_top·title_main을 그대로 씁니다. 1차 검토 없이 바로 다운로드·받아쓰기·댓글 매칭까지 진행되고 댓글 선택 화면으로 갑니다. 받아쓰기(Whisper)는 항상 적용되고, 무음 제거는 위 체크박스로 켜고 끕니다.",
  politics:"정치 구간 JSON을 그대로 사용합니다. 댓글 수집과 검토를 생략하고, 125% 전면 영상+확대 배경의 정치 레이아웃으로 만든 뒤 CapCut을 자동 실행합니다.",
  paste:"Gemini를 쓰지 않습니다. 오팔에서 받은 JSON 5개를 통째로 붙여넣으면 댓글 선택 화면으로 갑니다. URL을 비우면 JSON 안의 url을 씁니다. 받아쓰기(Whisper)는 항상 적용되고, 무음 제거는 위 체크박스로 켜고 끕니다.",
};
const PASTE_UI={
  paste:{label:"오팔 JSON 붙여넣기 (여러 개를 통째로 — 사이에 구분선·타이틀 후보가 섞여 있어도 됨)",
    ph:'오팔 Step 3 결과(JSON 코드블록) 5개를 순서대로 전부 붙여넣으세요.\n블록 ②·③ 텍스트가 섞여 들어와도 자동으로 JSON만 골라냅니다.',
    note:"(선택 — 비우면 JSON의 url 사용)"},
  wpaste:{label:"구간 JSON 붙여넣기 (candidates 배열 — 구간 5개 · title_top/title_main 선택)",
    ph:'{\n  "candidates": [\n    {"id": 1, "start_time": "14:28", "end_time": "16:15", "reason": "구간 선정 이유",\n     "title_top": "상단 제목 (선택)", "title_main": "메인 제목 (선택)"},\n    … (총 5개)\n  ]\n}',
    note:"(필수 — 구간 JSON에는 URL이 없습니다)"},
  politics:{label:"정치 구간 JSON 붙여넣기 (speaker·target·title_top·title_main 사용)",
    ph:'{\n  "candidates": [\n    {"id": 1, "start_time": "04:27", "end_time": "06:21",\n     "speaker": "발언자", "target": "관련 기관",\n     "title_top": "상단 제목", "title_main": "메인 제목"}\n  ]\n}',
    note:"(필수 — 구간 JSON에는 URL이 없습니다)"},
};
function applyMode(){
  const m=curMode();
  const p=PASTE_UI[m];
  const politics=m==="politics";
  $("#apasteField").style.display=p?"block":"none";
  if(p){
    $("#apasteLabel").textContent=p.label;
    $("#apaste").placeholder=p.ph;
  }
  $("#autoUrlNote").textContent=p?p.note:"";
  $("#autoModeHelp").textContent=MODE_HELP[m];
  const runField=$("#autoRunField"); if(runField) runField.style.display=politics?"none":"block";
  const silenceNote=$("#autoSilenceNote");
  if(silenceNote) silenceNote.textContent=politics
    ?"받아쓰기(Whisper)는 적용합니다. 체크를 끄면 원본 구간 길이를 그대로 유지합니다."
    :"받아쓰기(Whisper)·댓글 매칭은 항상 돌지만, 무음 제거는 꺼도 매칭이 어긋나지 않습니다.";
  $("#autoGo").textContent=politics?"정치 숏폼 생성 → CapCut 자동 실행":
    ((p?"댓글 매칭 시작":"분석 시작 (하이라이트 5개)")+
     (autoRunOn()?" → 영상까지 자동":""));
}
/* "끝까지 진행" — 검토 화면 두 곳(1차 제목 선택 · 2차 댓글 선택)에서 멈추지 않고
   기본값·추천 그대로 다음 단계로 넘어간다. 화면 편의 기능이라 서버는 이 상태를 모른다. */
function autoRunOn(){const el=$("#autoAutoRun");return !!(el&&el.checked);}

/* ── 분석 (1차: 편집안·타이틀 후보만 — 댓글 매칭 없음) ── */
async function analyze(){
  const m=curMode();
  const url=$("#autoUrl").value.trim();
  if(m!=="paste"&&!url){
    alert((m==="wpaste"||m==="politics")?"유튜브 URL을 입력하세요. (구간 JSON에는 URL이 없습니다)"
                      :"유튜브 URL을 입력하세요.");return;}
  if(PASTE_UI[m]&&!$("#apaste").value.trim()){
    alert(m==="wpaste"?"구간 JSON을 붙여넣으세요.":"오팔 JSON을 붙여넣으세요.");return;}
  $("#autoGo").disabled=true;$("#autoGo").textContent="분석 중…";
  A=null;P=null;dropped=new Set();TITLE_PICKS={};AUTO_AID=null;PREP_ID=null;
  $("#autoPickReview").innerHTML="";
  $("#autoPrepGo").style.display="none";$("#autoPrepGo").disabled=false;
  $("#autoPrepSteps").innerHTML="";$("#autoPrepSteps").style.display="none";
  $("#autoPrepLog").innerHTML="";
  $("#autoReview").innerHTML="";$("#autoSummary").style.display="none";
  $("#autoBuild").style.display="none";$("#autoLog").innerHTML="";
  $("#buildBoard").style.display="none";$("#buildBoard").innerHTML="";
  if($("#autoFixedWrap")) $("#autoFixedWrap").style.display="none";
  let res;
  try{
    const fd=new FormData();
    fd.append("url",url);
    fd.append("mode",m);
    if(PASTE_UI[m]) fd.append("data",$("#apaste").value);
    res=await(await fetch("/auto/analyze",{method:"POST",body:fd})).json();
  }catch(e){return failA("요청 실패: "+e);}
  if(res.error) return failA(res.error);
  AUTO_AID=res.analysis_id;
  const es=new EventSource("/auto/stream/"+AUTO_AID);
  es.onmessage=(m)=>{
    const ev=JSON.parse(m.data);
    if(ev.type==="manifest") renderASteps(ev.steps);
    else if(ev.type==="step") updateAStep(ev);
    else if(ev.type==="log") alog(ev.msg);
    else if(ev.type==="result"){es.close();onResult(ev);doneA();
      // 구간 JSON 모드: 제목이 JSON에 이미 있어 1차 검토(제목 선택)가 무의미 →
      // 유효 구간이 있으면(autoPrepGo 표시 = ok>0) 바로 준비(다운로드·받아쓰기·댓글 매칭) 시작.
      // "끝까지 진행"이 켜져 있으면 나머지 모드도 제목 기본값(첫 후보)으로 그냥 넘어간다.
      if(m==="politics"){
        TITLE_PICKS={};
        for(const h of A.highlights) if(!h.error)
          TITLE_PICKS[h.id]={top:h.paste.title_top||"",main:h.paste.title_main||""};
        alog("정치 모드 — 댓글과 검토를 생략하고 바로 생성 준비를 시작합니다.");
        prepareAll();
      }else if((m==="wpaste"||autoRunOn())&&$("#autoPrepGo").style.display==="block"){
        alog(m==="wpaste"?"구간 JSON 모드 — 제목이 JSON에 있으므로 바로 준비를 시작합니다."
                         :"끝까지 진행 — 제목은 기본 후보로 두고 바로 준비를 시작합니다.");
        prepareAll();
      }}
    else if(ev.type==="error"){es.close();failA(ev.message);}
  };
  es.onerror=()=>{es.close();failA("연결이 끊겼습니다.");};
}
function doneA(){$("#autoGo").disabled=false;applyMode();}
function failA(m){alog("⚠️ "+m);doneA();}
function alog(m){
  const d=document.createElement("div");
  d.style.cssText="font-family:var(--mono);font-size:12px;color:var(--muted2);margin-top:4px;white-space:pre-wrap;";
  d.textContent=m;$("#autoLog").appendChild(d);
}
function renderASteps(steps){
  $("#autoSteps").innerHTML=steps.map(s=>
    '<div class="step" id="ast-'+s.id+'"><span class="sdot"></span>'+
    '<div class="slabel"><div class="t">'+esc(s.label)+'</div><div class="sdetail" hidden></div></div>'+
    '<div class="selapsed"></div></div>').join("");
  $("#autoSteps").style.display="block";
}
function updateAStep(ev){
  const el=$("#ast-"+ev.id);if(!el)return;
  if(ev.status==="start"){el.classList.add("active");}
  else if(ev.status==="done"){el.classList.remove("active");el.classList.add("done");
    if(ev.detail){const d=el.querySelector(".sdetail");d.hidden=false;d.textContent=ev.detail;}}
}

/* ── 1차 검토 화면 — 하이라이트 카드(제목 선택 · ✕ 제외 · 편집안 접기). 댓글 영역 없음.
   탭 전환 없이 세로로 쌓는다(보통 5개뿐이라 전부 보여도 부담 없다) — 지금 검토 화면에서
   댓글 영역만 뺀 것이 1차 검토라는 설계를 그대로 따른다. */
function onResult(ev){
  A=ev;
  const R=$("#autoPickReview");R.innerHTML="";
  let ok=0;
  for(const hl of ev.highlights){
    const card=document.createElement("div");
    card.className="hlbox show pickcard";card.id="pick-"+hl.id;
    if(hl.error){
      card.classList.add("errbox");
      card.innerHTML='<div class="pickhead"><h3 style="margin:0;">ID '+hl.id+' ✗</h3>'+
        '<span style="color:var(--danger);font-size:12.5px;">생성 실패</span></div>'+
        '<div class="hlmeta" style="color:var(--danger);">'+esc(hl.error)+'</div>';
      R.appendChild(card);
      continue;
    }
    ok++;
    const cuts=hl.paste.cuts;
    let html='<div class="pickhead"><h3 style="margin:0;">ID '+hl.id+" · "+fmtT(hl.start)+"~"+fmtT(hl.end)+
      "</h3><span style='color:var(--muted2);font-size:12.5px;'>"+esc(hl.reason||"")+"</span>"+
      '<button type="button" class="ghost pickx" id="pickx-'+hl.id+'">✕ 제외</button></div>'+
      '<div class="hlmeta">컷 '+cuts.length+"개 · 총 "+hl.total+"초 · 카드 "+hl.need+"장 필요</div>";
    let opts=[];
    if(hl.editable_title){        // 구간 통짜: 제목 직접 입력(비우면 제목 없이)
      html+='<div class="hlsec">제목: '+
        '<input class="hlinput" id="hltop-'+hl.id+'" placeholder="윗줄 · 주황 (선택)"> '+
        '<input class="hlinput" id="hlmain-'+hl.id+'" placeholder="아랫줄 · 흰색 (선택)"></div>';
    }else{
      const t0={top:hl.paste.title_top,main:hl.paste.title_main,kind:"최종 선택"};
      opts=[t0].concat((hl.titles||[]).filter(t=>t.top!==t0.top||t.main!==t0.main));
      html+='<div class="hlsec">제목: <select id="hlt-'+hl.id+'">'+
        opts.map((t,i)=>'<option value="'+i+'">'+esc(t.top)+" / "+esc(t.main)+
          (t.kind?"  — "+esc(t.kind):"")+"</option>").join("")+"</select></div>";
    }
    card.innerHTML=html;
    if(hl.editable_title){   // 구간 JSON의 title_top/title_main 미리 채움 (.value라 따옴표 이스케이프 불필요)
      const ti=card.querySelector("#hltop-"+hl.id),mi=card.querySelector("#hlmain-"+hl.id);
      if(ti) ti.value=hl.paste.title_top||"";
      if(mi) mi.value=hl.paste.title_main||"";
    }
    card.dataset.titles=JSON.stringify(opts);
    card.appendChild(cutsSection(hl));   // 영상 편집안(컷 목록·JSON) — 기본 접힘
    R.appendChild(card);
    $("#pickx-"+hl.id).addEventListener("click",()=>togglePickDrop(hl.id));
  }
  if(ok){$("#autoPrepGo").style.display="block";updatePrepBtn();}
  else $("#autoPrepGo").style.display="none";
}

/* ── "준비 시작" 클릭 시점의 제목 선택을 잠가 둔다 — 2차(prepare) 결과가 오면
   1차 카드의 select/input 값을 더는 참조하지 않으므로 미리 읽어 저장해야 한다. */
function collectTitlePicks(){
  const out={};
  for(const hl of A.highlights){
    if(hl.error||isDropped(hl.id)) continue;
    let pick;
    if(hl.editable_title){
      pick={top:($("#hltop-"+hl.id)?$("#hltop-"+hl.id).value.trim():""),
            main:($("#hlmain-"+hl.id)?$("#hlmain-"+hl.id).value.trim():"")};
    }else{
      const card=$("#pick-"+hl.id);
      const opts=card?JSON.parse(card.dataset.titles||"[]"):[];
      const selEl=$("#hlt-"+hl.id);
      pick=opts[parseInt(selEl?selEl.value:0,10)||0]||{top:"",main:""};
    }
    out[hl.id]=pick;
  }
  return out;
}

/* ── 2단계: 준비(다운로드·받아쓰기·댓글 매칭) — 제외 안 한 ID만, 서버가 순차로 돈다 ── */
async function prepareAll(){
  if(!A) return;
  const ids=liveIds();
  if(!ids.length){alert("준비할 ID가 없습니다. 제외(✕)를 하나 이상 해제하세요.");return;}
  if(curMode()!=="politics") TITLE_PICKS=collectTitlePicks();
  const btn=$("#autoPrepGo");
  btn.disabled=true;btn.textContent="준비 중…";
  $("#autoPrepSteps").innerHTML="";$("#autoPrepLog").innerHTML="";
  $("#autoReview").innerHTML="";$("#autoSummary").style.display="none";
  $("#autoBuild").style.display="none";
  $("#buildBoard").style.display="none";$("#buildBoard").innerHTML="";
  if($("#autoFixedWrap")) $("#autoFixedWrap").style.display="none";
  let res;
  try{
    const fd=new FormData();
    fd.append("aid",AUTO_AID);
    fd.append("ids",JSON.stringify(ids));
    const rs=$("#autoRmsilence");
    fd.append("remove_silence",(!rs||rs.checked)?"1":"");   // 체크박스 없으면 기본 켬
    res=await(await fetch("/auto/prepare",{method:"POST",body:fd})).json();
  }catch(e){return prepFail("요청 실패: "+e);}
  if(res.error) return prepFail(res.error);
  PREP_ID=res.prepare_id;
  const es=new EventSource("/auto/prepare/"+PREP_ID);
  es.onmessage=(ev0)=>{
    const ev=JSON.parse(ev0.data);
    if(ev.type==="manifest") renderPrepSteps(ev.steps);
    else if(ev.type==="step") updatePrepStep(ev);
    else if(ev.type==="log") prepLog(ev.msg);
    else if(ev.type==="result"){es.close();onPrepareResult(ev);prepDone();}
    else if(ev.type==="error"){es.close();prepFail(ev.message);}
  };
  es.onerror=()=>{es.close();prepFail("연결이 끊겼습니다.");};
}
function prepDone(){$("#autoPrepGo").disabled=false;updatePrepBtn();}
function prepFail(m){prepLog("⚠️ "+m);prepDone();}
function prepLog(m){
  const d=document.createElement("div");
  d.style.cssText="font-family:var(--mono);font-size:12px;color:var(--muted2);margin-top:4px;white-space:pre-wrap;";
  d.textContent=m;$("#autoPrepLog").appendChild(d);
}
function renderPrepSteps(steps){
  $("#autoPrepSteps").innerHTML=steps.map(s=>
    '<div class="step" id="apst-'+s.id+'"><span class="sdot"></span>'+
    '<div class="slabel"><div class="t">'+esc(s.label)+'</div><div class="sdetail" hidden></div></div>'+
    '<div class="selapsed"></div></div>').join("");
  $("#autoPrepSteps").style.display="block";
}
function updatePrepStep(ev){
  const el=$("#apst-"+ev.id);if(!el)return;
  if(ev.status==="start"){el.classList.add("active");}
  else if(ev.status==="done"){el.classList.remove("active");el.classList.add("done");
    if(ev.detail){const d=el.querySelector(".sdetail");d.hidden=false;d.textContent=ev.detail;}}
}

/* ── 검토는 한 번에 하나 ──
   byIdx/sel/selCut 는 탭(auto/yt/paste) 구분 없는 전역 상태다. 세 result 핸들러가
   전부 byIdx={} 로 전역 리셋하면서 sel/selCut 은 자기 키만 건드리면, 다른 탭이
   그려둔 카드가 방금 리셋된 byIdx/sel 을 참조해 "엉뚱한 댓글을 가리킴"·"ID n 사용중"
   오표시·클릭 시 TypeError(toggle()의 list.indexOf, sel[hlId]가 undefined)가 난다.
   → 새 결과가 오면 sel/selCut 도 전역 리셋하고, 다른 탭의 검토 DOM·빌드 버튼까지
   지워 클릭 경로 자체를 없앤다. */
const PANEL_DOM={auto:{review:"#autoReview",build:"#autoBuild"},
                 yt:{review:"#ytReview",build:"#ytBuild"},
                 paste:{review:"#pasteReview",build:"#pasteBuild"}};
function clearOtherPanels(exceptId){
  for(const id in PANEL_DOM){
    if(id===exceptId) continue;
    const d=PANEL_DOM[id];
    const r=$(d.review); if(r) r.innerHTML="";
    const b=$(d.build); if(b) b.style.display="none";
  }
}

/* ── 2차 검토 화면 — 지금까지와 같은 ID별 탭 + renderCutPanel(컷별 댓글 섹션). ──
   준비(prepare)를 마친 ID만 온다 — 실패한 ID는 경고 로그로만 알린다(탭 자체가 없음). */
function onPrepareResult(ev){
  P=ev;byIdx={};sel={};selCut={};curId=null;
  clearOtherPanels("auto");
  if(curMode()==="politics"){
    (ev.warnings||[]).forEach(w=>prepLog("⚠️ "+w));
    $("#autoPickReview").innerHTML="";
    $("#autoReview").innerHTML="";
    if(!ev.highlights||!ev.highlights.length){
      prepLog("⚠️ 준비된 정치 편집안이 없습니다.");
      return;
    }
    prepLog("정치 모드 — 준비 완료, 댓글 카드 없이 드래프트를 생성합니다.");
    setTimeout(()=>buildPolitics(ev.highlights),0);
    return;
  }
  (ev.comments||[]).forEach(c=>{byIdx[c.idx]=c;});
  (ev.warnings||[]).forEach(w=>prepLog("⚠️ "+w));
  const R=$("#autoReview");R.innerHTML="";
  if(!ev.highlights||!ev.highlights.length){
    R.innerHTML='<div class="hlbox show"><div class="hlmeta">준비된 편집안이 없습니다. 위 로그를 확인하세요.</div></div>';
    $("#autoBuild").style.display="none";
    return;
  }
  const tabs=document.createElement("div");tabs.className="idtabs";tabs.id="idTabs";
  R.appendChild(tabs);
  let firstId=null;
  for(const hl of ev.highlights){
    const src=A.highlights.find(h=>h.id===hl.id)||{};
    if(firstId===null) firstId=hl.id;
    const tab=document.createElement("button");
    tab.type="button";tab.className="idtab";tab.id="idtab-"+hl.id;
    tab.innerHTML='<span class="tt">ID '+hl.id+(src.start!=null?(" · "+fmtT(src.start)+"~"+fmtT(src.end)):"")+
      '</span><span class="ts"></span>';
    tab.addEventListener("click",()=>showId(hl.id));
    tabs.appendChild(tab);

    const box=document.createElement("div");
    box.className="hlbox";box.id="hlbox-"+hl.id;
    // 자동 선택: 컷별 추천을 컷 순서대로. hl.cuts 가 없으면 기존 방식으로 폴백.
    sel[hl.id]=[]; selCut[hl.id]={};
    if(hl.cuts){
      for(const cu of hl.cuts)
        for(const p of (cu.picks||[]))
          if(byIdx[p.idx]!==undefined&&!sel[hl.id].includes(p.idx)){
            sel[hl.id].push(p.idx); selCut[hl.id][p.idx]=cu.i;
          }
    }else{
      sel[hl.id]=(hl.matched||[]).slice(0,hl.need).filter(i=>byIdx[i]!==undefined);
    }
    box.innerHTML='<div class="hlmeta">카드 '+hl.need+"장 필요 · "+
      '<span id="hlcnt-'+hl.id+'" class="tnum"></span></div>'+
      '<div class="selbar" id="hlsel-'+hl.id+'"></div>';
    renderCutPanel(box,hl.id,
      {cuts:hl.cuts,matched:hl.matched,candidates:ev.candidates,cutRanges:hl.cutRanges},
      {unit:"컷"});
    R.appendChild(box);
    refreshSel(hl.id);
  }
  // 접기/펼치기 버튼 — 탭 바 오른쪽 끝
  const fb=document.createElement("button");
  fb.type="button";fb.className="idtab ccfoldbtn";fb.id="ccFoldBtn";
  fb.innerHTML='<span class="tt">▴ 댓글 접기</span><span class="ts">카드 표시 상태</span>';
  fb.addEventListener("click",()=>setFolded(!$("#autoReview").classList.contains("folded")));
  tabs.appendChild(fb);
  if(firstId!==null) showId(firstId);
  $("#autoBuild").style.display="block";
  updateBuildBtn();
  if($("#autoFixedWrap")) $("#autoFixedWrap").style.display="flex";
  // "끝까지 진행" — 댓글 선택 화면에서 기다리지 않고, 위에서 자동 선택해 둔 컷별 추천을
  // 그대로 확정해 바로 빌드. setTimeout(0)으로 이 SSE 핸들러를 끝낸 뒤 시작해야
  // buildAll 의 캡처가 방금 그린 DOM의 레이아웃 확정 후에 돈다.
  if(autoRunOn()&&!$("#autoBuild").disabled){
    prepLog("끝까지 진행 — 추천 댓글을 그대로 쓰고 바로 영상 생성으로 넘어갑니다.");
    setTimeout(buildAll,0);
  }
}

async function buildPolitics(hls){
  boardInit(hls);
  let ok=0,fail=0;
  for(const hl of hls){
    boardSet(hl.id,"🔄 진행","정치 레이아웃 생성 중…","active");
    try{
      const pick=TITLE_PICKS[hl.id]||{top:"",main:""};
      const fd=new FormData();
      fd.append("aid",AUTO_AID); fd.append("id",String(hl.id));
      fd.append("title_top",pick.top||""); fd.append("title_main",pick.main||"");
      fd.append("video_scale","160");
      fd.append("flip",$("#flip").checked?"1":"");
      fd.append("scene",$("#scene").checked?"1":"");
      fd.append("bg_white",""); fd.append("asr_bottom","1");
      const res=await(await fetch("/auto/build",{method:"POST",body:fd})).json();
      if(res.error) throw new Error(res.error);
      const r=await streamJob(res.job_id,hl.id);
      boardSet(hl.id,"✅ 완료",(r&&r.draft_name)||"",""); ok++;
    }catch(e){boardSet(hl.id,"❌ 실패",String(e.message||e),"err");fail++;}
  }
  const s=$("#autoSummary");s.style.display="block";
  s.textContent=ok+"개 성공"+(fail?", "+fail+"개 실패":"")+" — 정치 레이아웃 생성 완료";
  if(ok) fetch("/open-capcut",{method:"POST"});
}

/* ── 캡처 + 순차 빌드 ── */
async function captureCard(wrap){
  const img=wrap.querySelector(".cc-avatar");
  if(img&&img.src&&!img.complete){          // 숨겨진 탭의 lazy 아바타 → 강제 로드 후 캡처
    img.loading="eager";
    try{await img.decode();}catch(e){}
  }
  const card=wrap.querySelector(".comment-card");
  // 배경이 검정(흰색 배경 체크 해제)이면 흰 카드로 캡처 — 검은 카드는 검은 배경에 묻힌다.
  // 캡처 순간에만 .light 를 붙였다 떼므로 검토 화면 표시는 어두운 테마 그대로다.
  const light=!($("#bgwhite")&&$("#bgwhite").checked);
  if(light) card.classList.add("light");
  try{
    return await window.modernScreenshot.domToBlob(card,{backgroundColor:null,scale:4});
  }finally{
    if(light) card.classList.remove("light");
  }
}
const nextFrame=()=>new Promise(r=>requestAnimationFrame(()=>requestAnimationFrame(r)));

/* 빌드/캡처 전 필수(검색 초기화와 같은 이유) — 접힌 컷 섹션은 display:none 이라
   레이아웃이 확정되지 않아 그 안의 카드를 domToBlob 으로 찍으면 빈 이미지가 나온다.
   캡처는 wrapOf() 가 DOM에서 '첫 번째로 찾은' 복사본을 찍으므로, 그 복사본이 하필
   접힌 컷 섹션 안에 있으면 조용히 깨진다 — 다음 사람이 "펼칠 필요 없는데?" 하고
   지우면 이 버그가 다시 살아난다. 지우지 말 것. */
function expandAllCuts(hlId){
  const box=$("#hlbox-"+hlId); if(!box) return;
  box.querySelectorAll(".cutgrp").forEach(g=>g.classList.add("open"));
}
/* 빌드 진행판 — 버튼 아래 ID별 한 줄씩, 전체 상황이 한눈에 보임 */
function boardInit(hls){
  const bd=$("#buildBoard");
  bd.innerHTML=hls.map(hl=>
    '<div class="bbrow" id="bb-'+hl.id+'">'+
    '<span class="bbid">ID '+hl.id+'</span>'+
    '<span class="bbstat">⏳ 대기</span>'+
    '<span class="bbmsg"></span></div>').join("");
  bd.style.display="block";
}
function boardSet(hlId,stat,msg,cls){
  const row=$("#bb-"+hlId); if(!row) return;
  row.className="bbrow"+(cls?" "+cls:"");
  if(stat!=null) row.querySelector(".bbstat").textContent=stat;
  if(msg!=null) row.querySelector(".bbmsg").textContent=msg;
}

async function buildAll(){
  const btn=$("#autoBuild");
  const hls=(P&&P.highlights)||[];   // 준비(prepare)를 마친 ID만 — 실패·제외된 ID는 여기 없음
  if(!hls.length){alert("만들 편집안이 없습니다.");return;}
  btn.disabled=true;
  setFolded(true);               // 빌드 시작 → 댓글 영역 접기(진행판에 집중)
  boardInit(hls);
  $("#buildBoard").scrollIntoView({behavior:"smooth",block:"nearest"});
  let ok=0,fail=0;
  for(const hl of hls){
    showId(hl.id);                           // 캡처는 보이는 상태에서
    clearSearch(hl.id);                      // 검색 중이면 카드가 DOM에 없어 캡처가 누락됨
    expandAllCuts(hl.id);                    // 접힌 컷 섹션의 카드는 빈 이미지로 캡처된다 — 전부 펼침
    // '선택한 것만 보기'가 켜진 채면 배정 안 된 쌍둥이가 display:none으로 남는다.
    // wrapOf(ci) 로 정확한 복사본을 찾도록 고쳤지만, 실패는 조용해서(빈 이미지·캡처 스킵)
    // 사용자가 CapCut에서 드래프트를 열기 전엔 모른다 — 이중 방어로 아예 꺼 버린다.
    const buildBox=$("#hlbox-"+hl.id);
    if(buildBox) buildBox.classList.remove("selonly");
    await nextFrame();
    boardSet(hl.id,"🔄 진행","", "active");
    try{
      // 제목은 1차 검토("준비 시작" 클릭 시점)에 이미 잠가 뒀다 — 다운로드·받아쓰기가
      // 끝난 지금은 1차 카드의 select/input 이 그대로 있어도 참조하지 않는다.
      const pick=TITLE_PICKS[hl.id]||{top:"",main:""};
      const fd=new FormData();
      fd.append("aid",AUTO_AID);
      fd.append("id",String(hl.id));
      fd.append("title_top",pick.top||"");
      fd.append("title_main",pick.main||"");
      fd.append("video_scale",$("#vscale").value||"144");
      fd.append("flip",$("#flip").checked?"1":"0");
      fd.append("scene",$("#scene").checked?"1":"0");
      fd.append("bg_white",$("#bgwhite").checked?"1":"0");
      fd.append("asr_bottom",$("#asrbottom").checked?"1":"0");
      fd.append("cards_fixed",($("#autoCardsFixed")&&$("#autoCardsFixed").checked)?"1":"0");
      const cmap=selCut[hl.id]||{};
      let n=0; const sentCuts=[];
      for(const idx of sel[hl.id]){
        const ci=cmap[idx];
        // ci를 반드시 wrapOf에 넘겨야 한다 — 안 넘기면 DOM에서 '처음 만나는' 복사본을
        // 집는데, 컷 그룹은 컷 순서로 붙으므로 예를 들어 컷3에 배정된 댓글이라도 컷1의
        // 채우기 섹션 복사본이 먼저 잡힐 수 있다(그 복사본은 배정 컷이 아니라 링도 없다).
        const w=wrapOf(hl.id,idx,ci);
        if(!w) continue;
        /* 컷 소속을 모르는 카드는 통째로 건너뛴다. 예전엔 마지막 컷으로 폴백했는데,
           _cards_by_cut 은 컷 '안에' 균등 분할하므로 그러면 4초짜리 마지막 컷에
           여러 장이 몰려 1초 미만으로 번쩍인다. 이제 모든 카드가 ci 를 갖지만 방어로 남긴다. */
        if(hl.cuts&&ci===undefined) continue;
        boardSet(hl.id,null,"카드 캡처 중… "+(++n)+"/"+sel[hl.id].length,"active");
        try{
          const blob=await captureCard(w);
          // ⚠ append 와 push 는 반드시 쌍으로 — 하나만 돌면 카드가 통째로 엉뚱한 컷에 붙는다
          fd.append("cards",blob,String(n).padStart(3,"0")+".png");
          sentCuts.push(ci);
        }catch(e){n--;boardSet(hl.id,null,"카드 1장 캡처 실패(건너뜀)","active");}
      }
      if(hl.cuts) fd.append("card_cuts",JSON.stringify(sentCuts));
      boardSet(hl.id,null,"빌드 요청 중…","active");
      const res=await(await fetch("/auto/build",{method:"POST",body:fd})).json();
      if(res.error) throw new Error(res.error);
      const r=await streamJob(res.job_id,hl.id);   // 완료/실패까지 대기(순차)
      boardSet(hl.id,"✅ 완료",(r&&r.draft_name)?r.draft_name:"","");
      ok++;
      const tab=$("#idtab-"+hl.id);
      if(tab) tab.querySelector(".ts").textContent="✅ 완료";
    }catch(e){
      boardSet(hl.id,"❌ 실패",String(e.message||e),"err");fail++;
      const tab=$("#idtab-"+hl.id);
      if(tab) tab.querySelector(".ts").textContent="❌ 실패";
    }
  }
  btn.disabled=false;btn.textContent="다시 만들기";
  const s=$("#autoSummary");s.style.display="block";
  s.textContent=ok+"개 성공"+(fail?", "+fail+"개 실패":"")+
    " — CapCut 프로젝트 목록에서 드래프트를 여세요.";
  if(ok&&$("#autoopen")&&$("#autoopen").checked)
    fetch("/open-capcut",{method:"POST"});
}
function streamJob(jobId,hlId){
  return new Promise((resolve,reject)=>{
    const es=new EventSource("/stream/"+jobId);
    es.onmessage=(m)=>{
      const ev=JSON.parse(m.data);
      if(ev.type==="log") boardSet(hlId,null,ev.msg,"active");
      else if(ev.type==="step"&&ev.status==="start") boardSet(hlId,null,"▶ "+ev.id,"active");
      else if(ev.type==="step"&&ev.status==="done"&&ev.detail)
        boardSet(hlId,null,"✓ "+ev.id+" — "+ev.detail,"active");
      else if(ev.type==="result"){es.close();resolve(ev);}
      else if(ev.type==="error"){es.close();reject(new Error(ev.message));}
    };
    es.onerror=()=>{es.close();reject(new Error("연결 끊김"));};
  });
}

/* ── ▶ 유튜브 구간 탭: 분석(SSE) → 검토(컷별 카드 선택) → 생성 ──
   📋 붙여넣기 탭(Task 4)과 같은 흐름 — 다만 "댓글 매칭 시작" 같은 전용 버튼이 없고
   공용 "편집 시작"(#run, index.html 인라인 스크립트)이 분석을 돈다. 패널 하나
   (panelId="yt")만 renderCutPanel 로 그린다(#hlbox-yt — wrapOf·clearSearch·
   expandAllCuts·refreshSel 이 전부 이 id로 찾는다). */
function ytSec(v){
  v=(v||"").trim(); if(!v) return null;
  const p=v.split(":").map(Number);
  if(p.some(isNaN)) return null;
  return p.length===3?p[0]*3600+p[1]*60+p[2]:p.length===2?p[0]*60+p[1]:p[0];
}
function ytRanges(){
  // ⚠ 함정: 여기서 초 숫자로 바꿔 보내면 다운로드 경로(cut_youtube→valid_time)가
  // "MM:SS"/"HH:MM:SS"만 받아들여 매 분석이 다운로드 단계에서 죽는다(구간 탭
  // 전면 불능 — 서버는 방어적으로 재정규화하지만 프런트도 옛 fmtTime 동작대로
  // 문자열 그대로 보내는 게 안전하다). 입력칸은 index.html의 focusout 리스너가
  // 이미 fmtTime()으로 "16:07" 형식을 넣어 두므로 그 문자열을 그대로 쓴다.
  const out=[];
  document.querySelectorAll("#ranges .rng").forEach(row=>{
    const sv=(row.querySelector(".rstart").value||"").trim();
    const sSec=ytSec(sv);
    if(sSec==null) return;
    let ev2=(row.querySelector(".rend").value||"").trim();
    let eSec=ytSec(ev2);
    if(eSec==null){ eSec=sSec+90; ev2=fmtT(eSec); }   // 끝 비면 시작+1:30 (실행 로직과 동일)
    if(eSec>sSec) out.push([sv,ev2]);
  });
  return out;
}
let YT_AID=null;
function ytLog(m){
  const d=document.createElement("div");
  d.style.cssText="font-family:var(--mono);font-size:12px;color:var(--muted2);margin-top:4px;white-space:pre-wrap;";
  d.textContent=m;$("#ytLog").appendChild(d);
}
function renderYSteps(steps){
  $("#ytSteps").innerHTML=steps.map(s=>
    '<div class="step" id="yst-'+s.id+'"><span class="sdot"></span>'+
    '<div class="slabel"><div class="t">'+esc(s.label)+'</div><div class="sdetail" hidden></div></div>'+
    '<div class="selapsed"></div></div>').join("");
  $("#ytSteps").style.display="block";
}
function updateYStep(ev){
  const el=$("#yst-"+ev.id);if(!el)return;
  if(ev.status==="start"){el.classList.add("active");}
  else if(ev.status==="done"){el.classList.remove("active");el.classList.add("done");
    if(ev.detail){const d=el.querySelector(".sdetail");d.hidden=false;d.textContent=ev.detail;}}
}
function ytDone(){$("#run").disabled=false;$("#run").textContent="편집 시작";}
function ytFail(m){ytLog("⚠️ "+m);ytDone();}

/* 분석 결과 → 검토 화면. 제목·출처는 분석(/yt/analyze)에 넘긴 값이 그대로 오거나
   (channel 은 유튜브에서 자동인식됐을 수 있음) 여기서 입력칸에 반영해 보여준다 —
   사용자가 여기서 또 고치면 그 값이 생성(ytBuildAll)에 그대로 실린다. */
function onYtResult(ev){
  byIdx={};sel={};selCut={};curId=null;   // 검토는 한 번에 하나 — 다른 탭 선택 상태까지 전부 리셋
  clearOtherPanels("yt");
  (ev.comments||[]).forEach(c=>{byIdx[c.idx]=c;});
  (ev.warnings||[]).forEach(w=>ytLog("⚠️ "+w));
  if(ev.title_top!=null) $("#yttop").value=ev.title_top;
  if(ev.title_main!=null) $("#ytmain").value=ev.title_main;
  if(ev.channel) $("#ytchan").value=ev.channel;   // 자동인식된 채널명 — 빈 값이면 건드리지 않음
  YT_HL={id:"yt",need:ev.need,cuts:ev.cuts};
  sel["yt"]=[];selCut["yt"]={};
  // 컷별 추천 실패 시(ev.cuts=null) 붙여넣기 탭과 동일하게 매칭 댓글로 자동 선택
  if(ev.cuts){
    ev.cuts.forEach(cu=>{
      for(const p of (cu.picks||[]))
        if(byIdx[p.idx]!==undefined&&!sel["yt"].includes(p.idx)){
          sel["yt"].push(p.idx);selCut["yt"][p.idx]=cu.i;
        }
    });
  }else{
    sel["yt"]=(ev.matched||[]).slice(0,ev.need).filter(i=>byIdx[i]!==undefined);
  }
  const area=$("#ytReview");area.innerHTML="";
  const box=document.createElement("div");
  box.className="hlbox show";box.id="hlbox-yt";
  box.innerHTML='<div class="hlmeta">구간 '+(ev.cutRanges||[]).length+"개 · 총 "+
    (ev.total!=null?ev.total:0)+"초 · 카드 "+ev.need+"장 필요 · "+
    '<span id="hlcnt-yt" class="tnum"></span></div>'+
    '<label class="selonly-label" style="margin-left:0;padding:2px 0;">'+
    '<input type="checkbox" id="ytfixed" checked style="accent-color:var(--accent);width:14px;height:14px;">'+
    ' 카드 3초 고정 — 모자라도 늘리지 않고 뒤는 비움 (부분삭제 편집용)</label>'+
    '<div class="selbar" id="hlsel-yt"></div>';
  renderCutPanel(box,"yt",
    {cuts:ev.cuts,matched:ev.matched,candidates:ev.candidates,cutRanges:ev.cutRanges},
    {unit:"구간"});
  area.appendChild(box);
  refreshSel("yt");
  $("#ytBuild").style.display="block";
}

function ytAnalyze(){
  const url=($("#yurl").value||"").trim();
  if(!url){alert("유튜브 URL을 입력하세요.");return;}
  const rng=ytRanges();
  if(!rng.length){alert("구간을 하나 이상 입력하세요.");return;}
  const btn=$("#run");
  btn.disabled=true;btn.textContent="분석 중…";
  $("#ytReview").innerHTML="";$("#ytSummary").style.display="none";
  $("#ytBuild").style.display="none";$("#ytLog").innerHTML="";
  $("#ytBoard").style.display="none";$("#ytBoard").innerHTML="";
  const fd=new FormData();
  fd.append("url",url);fd.append("ranges",JSON.stringify(rng));
  fd.append("title_top",$("#yttop").value);
  fd.append("title_main",$("#ytmain").value);
  fd.append("channel",$("#ytchan").value);
  // 무음 제거는 공통 옵션의 #rmsilence 체크박스(구간 탭에서 보임)를 그대로 넘긴다.
  // 예전엔 이 값을 아무도 안 읽어서 체크를 꺼도 bg_analyze 가 무조건 무음을 잘랐다.
  fd.append("remove_silence",$("#rmsilence")&&!$("#rmsilence").checked?"0":"1");
  fetch("/yt/analyze",{method:"POST",body:fd}).then(r=>r.json()).then(res=>{
    if(res.error){ytFail(res.error);return;}
    YT_AID=res.analysis_id;
    const es=new EventSource("/yt/stream/"+YT_AID);
    es.onmessage=(m)=>{
      const ev=JSON.parse(m.data);
      if(ev.type==="manifest") renderYSteps(ev.steps);
      else if(ev.type==="step") updateYStep(ev);
      else if(ev.type==="log") ytLog(ev.msg);
      else if(ev.type==="result"){es.close();onYtResult(ev);ytDone();}
      else if(ev.type==="error"){es.close();ytFail(ev.message);}
    };
    es.onerror=()=>{es.close();ytFail("연결이 끊겼습니다.");};
  }).catch(e=>ytFail("요청 실패: "+e));
}
window.ytAnalyze=ytAnalyze;   // index.html 인라인 스크립트(#run 클릭)에서 호출하는 훅

/* ── 생성 — 📋 붙여넣기 탭 pasteBuildAll 과 같은 캡처 루프(캡처 함정 3종 방어 포함). ──
   카드를 하나도 못 보내면 예전처럼 폴더 지정으로 하위호환한다. */
async function ytBuildAll(){
  if(!YT_AID) return;
  const btn=$("#ytBuild");
  btn.disabled=true;
  $("#ytBoard").innerHTML='<div class="bbrow" id="bb-yt">'+
    '<span class="bbid">▶ 유튜브 구간</span><span class="bbstat">⏳ 대기</span>'+
    '<span class="bbmsg"></span></div>';
  $("#ytBoard").style.display="block";
  $("#ytBoard").scrollIntoView({behavior:"smooth",block:"nearest"});
  clearSearch("yt");                     // 검색 중이면 카드가 DOM에서 빠져 캡처가 누락됨
  expandAllCuts("yt");                   // 접힌 컷 섹션의 카드는 빈 이미지로 캡처된다
  const ybox=$("#hlbox-yt");
  if(ybox) ybox.classList.remove("selonly");   // 배정 안 된 쌍둥이가 숨는 것 방지
  await nextFrame();
  boardSet("yt","🔄 진행","","active");
  try{
    const fd=new FormData();
    fd.append("aid",YT_AID);
    fd.append("title_top",$("#yttop").value);
    fd.append("title_main",$("#ytmain").value);
    fd.append("channel",$("#ytchan").value);
    fd.append("video_scale",$("#vscale").value||"144");
    fd.append("flip",$("#flip").checked?"1":"0");
    fd.append("scene",$("#scene").checked?"1":"0");
    fd.append("bg_white",$("#bgwhite").checked?"1":"0");
    const yf=$("#ytfixed");
    fd.append("cards_fixed",(yf&&yf.checked)?"1":"0");
    const cmap=selCut["yt"]||{};
    const selList=sel["yt"]||[];
    let n=0; const sentCuts=[];
    for(const idx of selList){
      const ci=cmap[idx];
      const w=wrapOf("yt",idx,ci);
      if(!w) continue;
      if(YT_HL.cuts&&ci===undefined) continue;   // 컷 소속 모르는 카드는 건너뜀
      boardSet("yt",null,"카드 캡처 중… "+(++n)+"/"+selList.length,"active");
      try{
        const blob=await captureCard(w);
        // ⚠ append 와 push 는 반드시 쌍으로 — 하나만 돌면 카드가 통째로 엉뚱한 컷에 붙는다
        fd.append("cards",blob,String(n).padStart(3,"0")+".png");
        sentCuts.push(ci);
      }catch(e){n--;boardSet("yt",null,"카드 1장 캡처 실패(건너뜀)","active");}
    }
    if(YT_HL.cuts) fd.append("card_cuts",JSON.stringify(sentCuts));
    if(n===0) fd.append("comments_dir",$("#cdir").value.trim());   // 카드 없음 → 폴더 지정 하위호환
    boardSet("yt",null,"빌드 요청 중…","active");
    const res=await(await fetch("/yt/build",{method:"POST",body:fd})).json();
    if(res.error) throw new Error(res.error);
    const r=await streamJob(res.job_id,"yt");
    boardSet("yt","✅ 완료",(r&&r.draft_name)?r.draft_name:"","");
    const s=$("#ytSummary");s.style.display="block";
    s.textContent="완료 — CapCut 프로젝트 목록에서 드래프트를 여세요.";
    if($("#autoopen")&&$("#autoopen").checked) fetch("/open-capcut",{method:"POST"});
  }catch(e){
    boardSet("yt","❌ 실패",String(e.message||e),"err");
  }
  btn.disabled=false;btn.textContent="다시 만들기";
}

/* ── 📋 붙여넣기 탭: 분석 → 검토 → 생성 ──
   자동 탭과 같은 흐름(analyze→SSE→onResult→build)이지만 편집안이 하나뿐이라
   ID 탭이 없다 — 패널 하나(panelId="paste")만 renderCutPanel 로 그린다. */
let PASTE_AID=null;
function pasteLog(m){
  const d=document.createElement("div");
  d.style.cssText="font-family:var(--mono);font-size:12px;color:var(--muted2);margin-top:4px;white-space:pre-wrap;";
  d.textContent=m;$("#pasteLog").appendChild(d);
}
function renderPSteps(steps){
  $("#pasteSteps").innerHTML=steps.map(s=>
    '<div class="step" id="pst-'+s.id+'"><span class="sdot"></span>'+
    '<div class="slabel"><div class="t">'+esc(s.label)+'</div><div class="sdetail" hidden></div></div>'+
    '<div class="selapsed"></div></div>').join("");
  $("#pasteSteps").style.display="block";
}
function updatePStep(ev){
  const el=$("#pst-"+ev.id);if(!el)return;
  if(ev.status==="start"){el.classList.add("active");}
  else if(ev.status==="done"){el.classList.remove("active");el.classList.add("done");
    if(ev.detail){const d=el.querySelector(".sdetail");d.hidden=false;d.textContent=ev.detail;}}
}
function pasteDone(){$("#pasteGo").disabled=false;$("#pasteGo").textContent="댓글 매칭 시작";}
function pasteFail(m){pasteLog("⚠️ "+m);pasteDone();}

/* 컷별 추천 결과를 renderCutPanel 로 그린다. hlbox-paste 는 wrapOf·clearSearch·
   expandAllCuts·refreshSel 이 전부 "#hlbox-"+panelId 로 찾으므로 id를 정확히 맞춰야 한다
   (yt 탭의 #hlbox-yt 와 동일한 방식). */
function onPasteResult(ev){
  byIdx={};sel={};selCut={};curId=null;   // 검토는 한 번에 하나 — 다른 탭 선택 상태까지 전부 리셋
  clearOtherPanels("paste");
  (ev.comments||[]).forEach(c=>{byIdx[c.idx]=c;});
  (ev.warnings||[]).forEach(w=>pasteLog("⚠️ "+w));
  PASTE_HL={id:"paste",need:ev.need,cuts:ev.cuts,paste:{cuts:ev.cutRanges}};
  sel["paste"]=[];selCut["paste"]={};
  // 컷별 추천 실패 시(ev.cuts=null) 자동 탭 폴백과 동일하게 매칭 댓글로 자동 선택
  if(ev.cuts){
    ev.cuts.forEach(cu=>{
      for(const p of (cu.picks||[]))
        if(byIdx[p.idx]!==undefined&&!sel["paste"].includes(p.idx)){
          sel["paste"].push(p.idx);selCut["paste"][p.idx]=cu.i;
        }
    });
  }else{
    sel["paste"]=(ev.matched||[]).slice(0,ev.need).filter(i=>byIdx[i]!==undefined);
  }
  const area=$("#pasteReview");area.innerHTML="";
  const box=document.createElement("div");
  box.className="hlbox show";box.id="hlbox-paste";
  box.innerHTML='<div class="hlmeta">컷 '+(ev.cuts||[]).length+"개 · 총 "+
    (ev.total!=null?ev.total:0)+"초 · 카드 "+ev.need+"장 필요 · "+
    '<span id="hlcnt-paste" class="tnum"></span></div>'+
    '<div class="selbar" id="hlsel-paste"></div>';
  renderCutPanel(box,"paste",
    {cuts:ev.cuts,matched:ev.matched,candidates:ev.candidates,cutRanges:ev.cutRanges},
    {unit:"컷"});
  area.appendChild(box);
  refreshSel("paste");
  $("#pasteBuild").style.display="block";
}

function pasteAnalyze(){
  const txt=$("#pjson").value.trim();
  if(!txt){alert("편집안 JSON을 붙여넣으세요.");return;}
  const btn=$("#pasteGo");
  btn.disabled=true;btn.textContent="분석 중…";
  $("#pasteReview").innerHTML="";$("#pasteSummary").style.display="none";
  $("#pasteBuild").style.display="none";$("#pasteLog").innerHTML="";
  $("#pasteBoard").style.display="none";$("#pasteBoard").innerHTML="";
  const fd=new FormData();fd.append("data",txt);
  fetch("/paste/analyze",{method:"POST",body:fd}).then(r=>r.json()).then(res=>{
    if(res.error){pasteFail(res.error);return;}
    PASTE_AID=res.analysis_id;
    const es=new EventSource("/paste/stream/"+PASTE_AID);
    es.onmessage=(m)=>{
      const ev=JSON.parse(m.data);
      if(ev.type==="manifest") renderPSteps(ev.steps);
      else if(ev.type==="step") updatePStep(ev);
      else if(ev.type==="log") pasteLog(ev.msg);
      else if(ev.type==="result"){es.close();onPasteResult(ev);pasteDone();}
      else if(ev.type==="error"){es.close();pasteFail(ev.message);}
    };
    es.onerror=()=>{es.close();pasteFail("연결이 끊겼습니다.");};
  }).catch(e=>pasteFail("요청 실패: "+e));
}

/* ── 생성 — 자동 탭 buildAll의 캡처 루프를 그대로 따른다(캡처 함정 3종 방어 포함). ──
   카드를 하나도 못 보내면(고르지 않았거나 캡처 전부 실패) 예전처럼 폴더 지정으로
   하위호환한다 — comments_dir 필드를 그대로 넘긴다. */
async function pasteBuildAll(){
  if(!PASTE_AID) return;
  const btn=$("#pasteBuild");
  btn.disabled=true;
  $("#pasteBoard").innerHTML='<div class="bbrow" id="bb-paste">'+
    '<span class="bbid">📋 붙여넣기</span><span class="bbstat">⏳ 대기</span>'+
    '<span class="bbmsg"></span></div>';
  $("#pasteBoard").style.display="block";
  $("#pasteBoard").scrollIntoView({behavior:"smooth",block:"nearest"});
  clearSearch("paste");                     // 검색 중이면 카드가 DOM에서 빠져 캡처가 누락됨
  expandAllCuts("paste");                   // 접힌 컷 섹션의 카드는 빈 이미지로 캡처된다
  const pbox=$("#hlbox-paste");
  if(pbox) pbox.classList.remove("selonly");   // 배정 안 된 쌍둥이가 숨는 것 방지
  await nextFrame();
  boardSet("paste","🔄 진행","","active");
  try{
    const fd=new FormData();
    fd.append("aid",PASTE_AID);
    fd.append("video_scale",$("#vscale").value||"144");
    fd.append("flip",$("#flip").checked?"1":"0");
    fd.append("scene",$("#scene").checked?"1":"0");
    fd.append("bg_white",$("#bgwhite").checked?"1":"0");
    fd.append("asr_bottom",$("#asrbottom").checked?"1":"0");
    const cmap=selCut["paste"]||{};
    const selList=sel["paste"]||[];
    let n=0; const sentCuts=[];
    for(const idx of selList){
      const ci=cmap[idx];
      const w=wrapOf("paste",idx,ci);
      if(!w) continue;
      if(PASTE_HL.cuts&&ci===undefined) continue;   // 컷 소속 모르는 카드는 건너뜀
      boardSet("paste",null,"카드 캡처 중… "+(++n)+"/"+selList.length,"active");
      try{
        const blob=await captureCard(w);
        // ⚠ append 와 push 는 반드시 쌍으로 — 하나만 돌면 카드가 통째로 엉뚱한 컷에 붙는다
        fd.append("cards",blob,String(n).padStart(3,"0")+".png");
        sentCuts.push(ci);
      }catch(e){n--;boardSet("paste",null,"카드 1장 캡처 실패(건너뜀)","active");}
    }
    if(PASTE_HL.cuts) fd.append("card_cuts",JSON.stringify(sentCuts));
    if(n===0) fd.append("comments_dir",$("#cdir").value.trim());   // 카드 없음 → 폴더 지정 하위호환
    boardSet("paste",null,"빌드 요청 중…","active");
    const res=await(await fetch("/paste/build",{method:"POST",body:fd})).json();
    if(res.error) throw new Error(res.error);
    const r=await streamJob(res.job_id,"paste");
    boardSet("paste","✅ 완료",(r&&r.draft_name)?r.draft_name:"","");
    const s=$("#pasteSummary");s.style.display="block";
    s.textContent="완료 — CapCut 프로젝트 목록에서 드래프트를 여세요.";
    if($("#autoopen")&&$("#autoopen").checked) fetch("/open-capcut",{method:"POST"});
  }catch(e){
    boardSet("paste","❌ 실패",String(e.message||e),"err");
  }
  btn.disabled=false;btn.textContent="다시 만들기";
}

/* ── ⚙ 지침·모델 설정 ── */
async function loadPrompts(){
  try{
    const r=await(await fetch("/prompts")).json();
    if(r.error){$("#pMsg").textContent="⚠ "+r.error;return;}
    $("#pStep1").value=r.step1;$("#pStep3").value=r.step3;
    $("#pConfig").value=JSON.stringify(r.config,null,1);
  }catch(e){$("#pMsg").textContent="⚠ 불러오기 실패";}
}
async function savePrompts(reset){
  const fd=new FormData();
  if(reset) fd.append("reset","1");
  else{
    fd.append("step1",$("#pStep1").value);
    fd.append("step3",$("#pStep3").value);
    fd.append("config",$("#pConfig").value);
  }
  try{
    const r=await(await fetch("/prompts",{method:"POST",body:fd})).json();
    $("#pMsg").textContent=r.error?("⚠ "+r.error):"✓ 저장됨 (다음 분석부터 적용)";
    if(!r.error&&reset) loadPrompts();
  }catch(e){$("#pMsg").textContent="⚠ 저장 실패";}
}

document.addEventListener("DOMContentLoaded",()=>{
  $("#autoGo").addEventListener("click",analyze);
  $("#autoUrl").addEventListener("keydown",(e)=>{if(e.key==="Enter")analyze();});
  document.querySelectorAll('input[name="amode"]').forEach(r=>
    r.addEventListener("change",applyMode));
  if($("#autoAutoRun")) $("#autoAutoRun").addEventListener("change",applyMode);
  applyMode();
  $("#autoPrepGo").addEventListener("click",prepareAll);
  $("#autoBuild").addEventListener("click",buildAll);
  $("#autoSettings").addEventListener("toggle",()=>{if($("#autoSettings").open)loadPrompts();});
  $("#pSave").addEventListener("click",()=>savePrompts(false));
  $("#pReset").addEventListener("click",()=>savePrompts(true));
  if($("#ytBuild")) $("#ytBuild").addEventListener("click",ytBuildAll);
  if($("#pasteGo")) $("#pasteGo").addEventListener("click",pasteAnalyze);
  if($("#pasteBuild")) $("#pasteBuild").addEventListener("click",pasteBuildAll);
});

/* ── 옵션 기억 — 체크 상태를 localStorage 에 저장, 다음 방문에도 유지.
   서버는 이 상태를 모른다(폼 전송 값만 봄) — 순수 화면 편의 기능. */
(function(){
  for(const [id,key] of [["#bgwhite","opt_bgwhite"],["#autoRmsilence","opt_auto_rmsilence"],
                         ["#autoAutoRun","opt_auto_autorun"]]){
    const el=$(id); if(!el) continue;
    const saved=localStorage.getItem(key);
    if(saved!==null) el.checked=saved==="1";
    el.addEventListener("change",()=>localStorage.setItem(key,el.checked?"1":"0"));
  }
})();
})();
