리뷰 반영: selonly 캡처 함정 차단, 더보기/검색 재렌더 링 오류, 검색 중 접힌 컷 결과 숨김 수정

Critical: wrapOf가 ci를 몰라 '선택한 것만 보기' 중 배정 안 된(=display:none) 쌍둥이를
캡처하는 조용한 실패가 있었다. wrapOf(hlId,idx,ci)로 정확한 복사본을 찾게 하고,
buildAll에서 selonly를 이중으로 꺼서 방어한다. 칩 클릭 스크롤도 같은 방식으로 고쳤다.

Important: cardSection.render()가 컷 판정 없이 링을 붙여 '더보기'·검색 재렌더 때 증상이
되살아났다 — refreshSel과 같은 판정식을 render()에 직접 넣었다(패널이 아직 document에
안 붙어 있는 초기 구성 중에도 정확히 동작해야 하므로 refreshSel 재호출 대신 선택).

Important: 검색이 섹션 단위로만 display를 토글해 닫힌 컷 아코디언 안의 결과가 안 보였다.
검색 중엔 그 패널의 컷을 전부 펼치고, 지우면 검색 시작 전 열림 상태로 되돌린다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
hehihoho3@gmail.com 2026-08-04 18:13:00 +09:00
parent d1ebfaff74
commit 058f03265c

View File

@ -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 갖지만 방어로 남긴다. */