diff --git a/server/static/auto.js b/server/static/auto.js index 7a91d64..30f3c7f 100644 --- a/server/static/auto.js +++ b/server/static/auto.js @@ -58,8 +58,13 @@ function cardEl(c,hlId,ci){ return wrap; } -function wrapOf(hlId,idx){ - return $("#hlbox-"+hlId+" .ccwrap[data-cidx='"+idx+"']"); +/* 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은 점진 생성) ── */ @@ -93,7 +98,14 @@ function cardSection(label,list,hlId,firstBatch,ci){ const c=byIdx[cur[shown]]; if(!c) continue; const w=cardEl(c,hlId,ci); - if(sel[hlId]&&sel[hlId].includes(c.idx)) w.classList.add("sel"); + // 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); @@ -116,6 +128,22 @@ function cardSection(label,list,hlId,firstBatch,ci){ }); 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]=[]; // 패널 재생성 시 이전 섹션 필터 폐기 @@ -127,6 +155,7 @@ function searchBar(hlId){ 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);}); @@ -294,7 +323,12 @@ function refreshSel(hlId){ chip.title=String(c.text||"").slice(0,120); chip.addEventListener("click",(e)=>{ // 칩 클릭 = 카드로 스크롤 if(e.target.classList.contains("x")) return; - const w=wrapOf(hlId,idx); + 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)); @@ -719,6 +753,11 @@ async function buildAll(){ 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{ @@ -744,9 +783,12 @@ async function buildAll(){ const cmap=selCut[hl.id]||{}; let n=0; const sentCuts=[]; for(const idx of sel[hl.id]){ - const w=wrapOf(hl.id,idx); - if(!w) continue; 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 를 갖지만 방어로 남긴다. */