검토 화면을 컷별 카드 묶음으로 렌더
서버가 하이라이트마다 컷 단위 추천(hl.cuts)을 내려주기 시작해서(6~8번
커밋), 화면도 컷 소속을 알아야 카드를 그 컷 구간 안에 배치할 수 있다.
카드가 어느 컷에 속하는지(selCut)를 추적해 컷별 상한(quota)을 걸고,
sel[hlId]를 항상 컷 순서로 정렬해 업로드 파일명(001.png…)과
card_cuts 배열 인덱스가 어긋나지 않게 했다. hl.cuts가 없으면(추천
실패) 기존 ⭐ 통짜 화면으로 그대로 폴백한다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c3556edb9d
commit
5b58e94ad1
@ -27,7 +27,7 @@ function hlById(id){
|
||||
}
|
||||
|
||||
/* ── 카드 DOM (h-lab renderCards 구조와 동일) ── */
|
||||
function cardEl(c,hlId){
|
||||
function cardEl(c,hlId,ci){
|
||||
const wrap=document.createElement("div");
|
||||
wrap.className="ccwrap"; wrap.dataset.cidx=c.idx;
|
||||
const card=document.createElement("div");
|
||||
@ -51,7 +51,7 @@ function cardEl(c,hlId){
|
||||
const badge=document.createElement("div");
|
||||
badge.className="ccbadge"; badge.textContent="✓";
|
||||
wrap.appendChild(card); wrap.appendChild(badge);
|
||||
wrap.addEventListener("click",()=>toggle(hlId,c.idx));
|
||||
wrap.addEventListener("click",()=>toggle(hlId,c.idx,ci));
|
||||
return wrap;
|
||||
}
|
||||
|
||||
@ -61,7 +61,7 @@ function wrapOf(hlId,idx){
|
||||
|
||||
/* ── 카드 섹션 — 30장씩 렌더 + 더보기 (전체 댓글을 받아도 DOM은 점진 생성) ── */
|
||||
const CARD_PAGE=30;
|
||||
function cardSection(label,list,hlId,firstBatch){
|
||||
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);
|
||||
@ -75,7 +75,7 @@ function cardSection(label,list,hlId,firstBatch){
|
||||
for(;shown<end;shown++){
|
||||
const c=byIdx[list[shown]];
|
||||
if(!c) continue;
|
||||
const w=cardEl(c,hlId);
|
||||
const w=cardEl(c,hlId,ci);
|
||||
if(sel[hlId]&&sel[hlId].includes(c.idx)) w.classList.add("sel");
|
||||
grid.appendChild(w);
|
||||
}
|
||||
@ -88,13 +88,34 @@ function cardSection(label,list,hlId,firstBatch){
|
||||
render(firstBatch||CARD_PAGE);
|
||||
return sec;
|
||||
}
|
||||
function toggle(hlId,idx){
|
||||
/* ── 컷별 선택 ── 카드는 '어느 컷 소속'인지가 배치를 정한다.
|
||||
selCut[hlId][idx] = 컷 인덱스. sel[hlId] 는 항상 컷 순서로 정렬해 둔다
|
||||
(업로드 순서 = 배치 순서라서). */
|
||||
const selCut={};
|
||||
function cutOf(hlId,idx){ return (selCut[hlId]||{})[idx]; }
|
||||
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) list.splice(at,1);
|
||||
if(at>=0){ list.splice(at,1); delete selCut[hlId][idx]; }
|
||||
else{
|
||||
if(list.length>=hl.need) return; // 필요 장수 초과 선택 방지
|
||||
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);
|
||||
}
|
||||
@ -435,7 +456,17 @@ function onResult(ev){
|
||||
// 패널
|
||||
const box=document.createElement("div");
|
||||
box.className="hlbox";box.id="hlbox-"+hl.id;
|
||||
sel[hl.id]=(hl.matched||[]).slice(0,hl.need).filter(i=>byIdx[i]!==undefined);
|
||||
// 자동 선택: 컷별 추천을 컷 순서대로. 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);
|
||||
}
|
||||
const cuts=hl.paste.cuts;
|
||||
let html="<h3>ID "+hl.id+" · "+fmtT(hl.start)+"~"+fmtT(hl.end)+
|
||||
" <span style='color:var(--muted2);font-weight:400;'>"+esc(hl.reason||"")+"</span></h3>"+
|
||||
@ -458,11 +489,25 @@ function onResult(ev){
|
||||
box.dataset.titles=JSON.stringify(opts);
|
||||
// 영상 편집안(컷 목록·JSON) — 선택 요약 바 위에, 기본 접힘
|
||||
box.insertBefore(cutsSection(hl),$("#hlsel-"+hl.id));
|
||||
// ⭐ 구간 언급 댓글 (전체 — 30장씩 더보기)
|
||||
const m=(hl.matched||[]).filter(i=>byIdx[i]!==undefined);
|
||||
box.appendChild(cardSection(
|
||||
"⭐ 이 구간을 언급한 댓글 "+m.length+"장 (좋아요순, 자동 선택)",
|
||||
m,hl.id,Math.max(CARD_PAGE,sel[hl.id].length))); // 자동 선택분은 첫 화면에 다 보이게
|
||||
const WHY={ts:"⭐",ai:"🤖",like:"➕"};
|
||||
if(hl.cuts){
|
||||
// 컷별 추천 — 추천분 먼저, 그 컷 시간대 언급 댓글을 뒤에 붙여 갈아끼울 수 있게
|
||||
for(const cu of hl.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 rest=(hl.matched||[]).filter(i=>byIdx[i]!==undefined&&!rec.includes(i));
|
||||
const label="컷 "+(cu.i+1)+" · "+cu.sec+"초 · 카드 "+cu.quota+"장"+
|
||||
(cu.bottom?" — "+cu.bottom:"")+(why?" "+why:"");
|
||||
box.appendChild(cardSection(label,rec.concat(rest),hl.id,
|
||||
Math.max(CARD_PAGE,rec.length),cu.i));
|
||||
}
|
||||
}else{
|
||||
// 폴백 — 추천 실패 시 기존 화면 그대로
|
||||
const m=(hl.matched||[]).filter(i=>byIdx[i]!==undefined);
|
||||
box.appendChild(cardSection(
|
||||
"⭐ 이 구간을 언급한 댓글 "+m.length+"장 (좋아요순, 자동 선택)",
|
||||
m,hl.id,Math.max(CARD_PAGE,sel[hl.id].length)));
|
||||
}
|
||||
// ➕ 좋아요 상위 후보 (전체 — 30장씩 더보기)
|
||||
const cand=(hl.candidates||[]).filter(i=>byIdx[i]!==undefined);
|
||||
if(cand.length){
|
||||
@ -551,7 +596,8 @@ async function buildAll(){
|
||||
fd.append("remove_silence",$("#rmsilence").checked?"1":"0");
|
||||
fd.append("asr_bottom",$("#asrbottom").checked?"1":"0");
|
||||
fd.append("cards_fixed",($("#autoCardsFixed")&&$("#autoCardsFixed").checked)?"1":"0");
|
||||
let n=0;
|
||||
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;
|
||||
@ -559,8 +605,10 @@ async function buildAll(){
|
||||
try{
|
||||
const blob=await captureCard(w);
|
||||
fd.append("cards",blob,String(n).padStart(3,"0")+".png");
|
||||
}catch(e){boardSet(hl.id,null,"카드 1장 캡처 실패(건너뜀)","active");}
|
||||
sentCuts.push(cmap[idx]??((hl.paste.cuts||[]).length-1));
|
||||
}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);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user