From 22a9758e746f9487cf14e005fbc1e8d21fe4f5dd Mon Sep 17 00:00:00 2001 From: "hehihoho3@gmail.com" Date: Tue, 4 Aug 2026 17:20:21 +0900 Subject: [PATCH] =?UTF-8?q?auto.js:=20=EC=B9=B4=EB=93=9C=20=ED=8C=A8?= =?UTF-8?q?=EB=84=90=20=EB=A0=8C=EB=8D=94=EB=9F=AC=EB=A5=BC=20renderCutPan?= =?UTF-8?q?el()=EB=A1=9C=20=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit onResult(자동 탭)과 ytMatch(구간 탭)에 각각 있던 검색창+컷별/⭐ 섹션+➕후보 렌더 블록이 두 갈래로 갈라져 있어, 곧 붙여넣기 탭까지 더해지면 한 곳만 고치는 실수가 나기 쉬웠다. 두 블록을 renderCutPanel(box,panelId,data,opts) 하나로 합쳐 배지·검색·선택 상한 로직을 한 곳에 모았다. 화면 문구는 완전히 동일하게 보존했다 — 특히 ⭐ 폴백 라벨은 onResult가 "이 구간을", ytMatch가 "구간을"로 이미 서로 달랐기에, unit 값에 따라 원문 그대로 재현하도록 분기했다(무작정 unit을 문자열에 꽂지 않음). Co-Authored-By: Claude Opus 5 (1M context) --- server/static/auto.js | 97 +++++++++++++++++++++---------------------- 1 file changed, 47 insertions(+), 50 deletions(-) diff --git a/server/static/auto.js b/server/static/auto.js index c3780ae..445cf83 100644 --- a/server/static/auto.js +++ b/server/static/auto.js @@ -137,6 +137,48 @@ function clearSearch(hlId){ const bar=box&&box.querySelector(".ccsearch"); if(bar&&bar._clear) bar._clear(); } +/* ── 카드 패널 렌더 — 자동/구간/붙여넣기 세 탭이 공유한다. + 갈라 두면 한 곳만 고치는 실수가 난다(배지·검색·선택 상한이 전부 여기 모여 있다). ── */ +function renderCutPanel(box,panelId,data,opts){ + const unit=(opts&&opts.unit)||"컷"; + const WHY={ts:"⭐",ai:"🤖",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)); + box.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( + "↳ 좋아요 상위에서 채우기 "+cand.length+"장 (이 컷에 넣기)", + cand,panelId,CUT_FILL_PAGE,cu.i)); + } + }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] 는 항상 컷 순서로 정렬해 둔다 (업로드 순서 = 배치 순서라서). */ @@ -537,47 +579,9 @@ function onResult(ev){ box.dataset.titles=JSON.stringify(opts); // 영상 편집안(컷 목록·JSON) — 선택 요약 바 위에, 기본 접힘 box.insertBefore(cutsSection(hl),$("#hlsel-"+hl.id)); - box.appendChild(searchBar(hl.id)); // 카드 섹션들 위 — cardSection 보다 먼저 만들어야 SECS 초기화됨 - const WHY={ts:"⭐",ai:"🤖",like:"➕"}; - const cand=(hl.candidates||[]).filter(i=>byIdx[i]!==undefined); - if(hl.cuts){ - /* 컷별 추천 — 컷마다 두 섹션. 독립 ➕ 섹션을 두지 않는 이유: - 거기서 고른 카드는 컷 소속(ci)이 없어 빌드 때 갈 곳이 없다. 그런데 AI 프롬프트가 - "억지로 채우지 마라"라고 지시하므로 quota 미달이 기본이고, 미달분을 ➕ 에서 - 채우는 게 주 워크플로다. _cards_by_cut 은 컷 '뒤'가 아니라 컷 '안에' 균등 분할하니 - 소속 없는 카드를 한 컷에 몰면 4초 컷에 5장이 들어가 0.8초씩 번쩍인다. */ - 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(""); - /* 추천 뒤에 붙이는 '나머지'는 **그 컷 시간대를 언급한 댓글만**. - hl.matched 전체를 컷마다 복제하면 컷 12개 × 30장 = 360장씩 부풀고, - 같은 댓글이 여러 컷 섹션에 중복돼 한쪽을 고르면 쌍둥이까지 하이라이트된다. */ - const cs=(hl.paste.cuts||[])[cu.i]||{start:0,end:-1}; - const rest=(hl.matched||[]).filter(i=> - byIdx[i]!==undefined&&!rec.includes(i)&& - (byIdx[i].times||[]).some(t=>t>=cs.start&&t<=cs.end)); - 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)); - // 부족분 채우기 — 컷마다 반복되므로 처음엔 조금만 그리고 나머지는 더보기로 - if(cand.length) - box.appendChild(cardSection( - "↳ 좋아요 상위에서 채우기 "+cand.length+"장 (이 컷에 넣기)", - cand,hl.id,CUT_FILL_PAGE,cu.i)); - } - }else{ - // 폴백 — hl.cuts 가 없으면(컷 정보 없음) 예전 화면 그대로: ⭐ + 독립 ➕ - 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))); - if(cand.length){ - box.appendChild(cardSection( - "➕ 좋아요 상위 후보 "+cand.length+"장 (부족분 클릭)", - cand,hl.id,CARD_PAGE)); - } - } + renderCutPanel(box,hl.id, + {cuts:hl.cuts,matched:hl.matched,candidates:hl.candidates, + cutRanges:hl.paste.cuts},{unit:"컷"}); R.appendChild(box); refreshSel(hl.id); } @@ -765,15 +769,8 @@ async function ytMatch(){ ''+ ' 카드 3초 고정 — 모자라도 늘리지 않고 뒤는 비움 (부분삭제 편집용)'+ '
'; - box.appendChild(searchBar("yt")); - box.appendChild(cardSection( - "⭐ 구간을 언급한 댓글 "+matched.length+"장 (좋아요순, 자동 선택)", - matched,"yt",Math.max(CARD_PAGE,sel["yt"].length))); - if(cand.length){ - box.appendChild(cardSection( - "➕ 좋아요 상위 후보 "+cand.length+"장 (부족분 클릭)", - cand,"yt",CARD_PAGE)); - } + renderCutPanel(box,"yt", + {matched:matched,candidates:cand},{unit:"구간"}); area.appendChild(box); refreshSel("yt"); $("#ytccNote").textContent="선택한 카드는 편집 시작 때 자동으로 들어갑니다(폴더 지정보다 우선). "