자동 탭: 컷별 쌍둥이 카드 혼동 해소 + 컷 섹션 아코디언
같은 댓글이 컷마다 반복 렌더되는데 카드 DOM에 컷 번호가 없어 refreshSel()이 모든 복사본에 선택 링을 붙였다(1컷에서 고른 게 2컷에도 체크된 것처럼 보임). 쌍둥이를 눌러도 다른 컷으로 옮기려는 의도인데 무조건 해제됐다. - cardEl에 data-ci를 남기고, refreshSel은 실제 배정된 컷의 복사본에만 링을 부여 - 다른 컷에 배정된 쌍둥이는 기존 .ccused 배지 구조를 재사용해 "컷 N에서 사용중"으로 표시 - 이미 선택된 쌍둥이를 다른 컷에서 누르면 해제 대신 그 컷으로 이동(sortSel로 순서 유지) - 컷마다 추천+채우기 섹션을 아코디언으로 묶어 한 번에 한 컷만 펼침(카드 폭증 완화), 헤더에 컷 채움 상태를 실시간 표시 - 빌드 캡처 직전 expandAllCuts()로 모든 컷 섹션을 강제로 펼침 (검색 필터 때와 같은 함정 — 접힌 섹션의 카드는 domToBlob이 빈 이미지로 찍는다) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
0967e88e7b
commit
d1ebfaff74
@ -30,6 +30,9 @@ function hlById(id){
|
||||
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):"";
|
||||
@ -137,6 +140,27 @@ function clearSearch(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){
|
||||
@ -155,15 +179,17 @@ function renderCutPanel(box,panelId,data,opts){
|
||||
const rest=(data.matched||[]).filter(i=>
|
||||
byIdx[i]!==undefined&&!rec.includes(i)&&
|
||||
(byIdx[i].times||[]).some(t=>t>=cs.start&&t<=cs.end));
|
||||
box.appendChild(cardSection(
|
||||
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));
|
||||
// 부족분 채우기 — 컷마다 반복되므로 처음엔 조금만 그리고 나머지는 더보기로
|
||||
if(cand.length)
|
||||
box.appendChild(cardSection(
|
||||
body.appendChild(cardSection(
|
||||
"↳ 좋아요 상위에서 채우기 "+cand.length+"장 (이 컷에 넣기)",
|
||||
cand,panelId,CUT_FILL_PAGE,cu.i));
|
||||
box.appendChild(grp);
|
||||
}
|
||||
}else{
|
||||
// 폴백 — cuts 정보가 없으면(구간 탭·구정보 없음) 예전 화면 그대로: ⭐ + 독립 ➕
|
||||
@ -196,8 +222,18 @@ function toggle(hlId,idx,ci){
|
||||
const hl=hlById(hlId), list=sel[hlId];
|
||||
selCut[hlId]=selCut[hlId]||{};
|
||||
const at=list.indexOf(idx);
|
||||
if(at>=0){ list.splice(at,1); delete selCut[hlId][idx]; }
|
||||
else{
|
||||
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;
|
||||
@ -213,8 +249,25 @@ function toggle(hlId,idx,ci){
|
||||
function refreshSel(hlId){
|
||||
const hl=hlById(hlId), list=sel[hlId];
|
||||
const box=$("#hlbox-"+hlId); if(!box) return;
|
||||
box.querySelectorAll(".ccwrap").forEach(w=>
|
||||
w.classList.toggle("sel",list.includes(parseInt(w.dataset.cidx,10))));
|
||||
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){
|
||||
@ -276,15 +329,29 @@ function usedByOthers(hlId,idx){
|
||||
}
|
||||
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);
|
||||
const ids=usedByOthers(hlId,idx);
|
||||
w.classList.toggle("used",ids.length>0);
|
||||
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(ids.length){
|
||||
if(text){
|
||||
if(!b){b=document.createElement("div");b.className="ccused";w.appendChild(b);}
|
||||
b.textContent="ID "+ids.join("·")+" 사용중";
|
||||
w.title="ID "+ids.join(", ")+"에서 이미 선택한 댓글";
|
||||
b.textContent=text;
|
||||
w.title=text;
|
||||
}else{
|
||||
if(b) b.remove();
|
||||
w.removeAttribute("title");
|
||||
@ -613,6 +680,15 @@ async function captureCard(wrap){
|
||||
}
|
||||
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");
|
||||
@ -642,6 +718,7 @@ async function buildAll(){
|
||||
for(const hl of hls){
|
||||
showId(hl.id); // 캡처는 보이는 상태에서
|
||||
clearSearch(hl.id); // 검색 중이면 카드가 DOM에 없어 캡처가 누락됨
|
||||
expandAllCuts(hl.id); // 접힌 컷 섹션의 카드는 빈 이미지로 캡처된다 — 전부 펼침
|
||||
await nextFrame();
|
||||
boardSet(hl.id,"🔄 진행","", "active");
|
||||
try{
|
||||
|
||||
@ -236,6 +236,21 @@
|
||||
.cutraw pre{margin:0;padding:11px 13px;background:#080A0E;font-family:var(--mono);font-size:11px;
|
||||
line-height:1.55;color:var(--muted2);max-height:280px;overflow:auto;}
|
||||
.cutcopy{margin:0 13px 11px;width:auto;min-height:34px;padding:6px 12px;}
|
||||
/* ── 컷별 카드 아코디언 — 한 번에 한 컷만 펼쳐 후보 카드가 한꺼번에 쌓이지 않게 한다 ── */
|
||||
.cutgrp{margin-top:10px;border:1px solid var(--border);border-radius:9px;
|
||||
background:var(--surf2);overflow:hidden;}
|
||||
.cutgrp-head{width:100%;box-sizing:border-box;display:flex;align-items:center;gap:8px;
|
||||
text-align:left;cursor:pointer;background:transparent;border:0;color:var(--muted2);
|
||||
padding:11px 13px;min-height:44px;font-size:12.5px;font-weight:600;transition:background .15s,color .15s;}
|
||||
.cutgrp-head:hover{color:var(--text);background:var(--surf);}
|
||||
.cutgrp.open>.cutgrp-head{color:var(--text);border-bottom:1px solid var(--border);}
|
||||
.cutgrp-head .chev{flex:none;color:var(--muted);font-size:10px;transition:transform .18s ease;}
|
||||
.cutgrp.open .chev{transform:rotate(90deg);}
|
||||
.cutgrp-head .cgstat{margin-left:auto;flex:none;font-family:var(--mono);font-size:11.5px;
|
||||
color:var(--muted);font-weight:400;}
|
||||
.cutgrp.open .cgstat{color:var(--accent);}
|
||||
.cutgrp-body{display:none;padding:0 13px 12px;}
|
||||
.cutgrp.open .cutgrp-body{display:block;}
|
||||
/* 선택한 댓글 요약 바 */
|
||||
.selbar{display:flex;flex-wrap:wrap;gap:6px;align-items:center;margin-top:10px;
|
||||
padding:10px;background:#101010;border:1px solid var(--border);border-radius:8px;min-height:44px;}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user