diff --git a/docs/superpowers/specs/2026-08-04-댓글검색-design.md b/docs/superpowers/specs/2026-08-04-댓글검색-design.md
new file mode 100644
index 0000000..70e5edf
--- /dev/null
+++ b/docs/superpowers/specs/2026-08-04-댓글검색-design.md
@@ -0,0 +1,40 @@
+# 댓글 선택 화면 검색(필터) — 설계
+
+날짜: 2026-08-04 · 대상: `server/static/auto.js`, `server/static/index.html` (서버 변경 없음)
+
+## 목적
+
+댓글 선택 화면(자동 탭 ID 패널 + 유튜브 구간 탭 댓글 매칭)에서 수백~수천 장 카드 중
+원하는 댓글을 텍스트로 찾을 수 있게 한다.
+
+## 동작 (필터 방식)
+
+- 각 패널 상단(선택 요약 바 아래, 카드 섹션 위)에 검색창 1개 + 지우기 ✕.
+- 입력(디바운스 150ms) 시 그 패널의 **모든 카드 섹션**(컷별 ⭐추천 / ➕채우기)이
+ 자기 **전체 목록**에서 일치하는 카드만 다시 그린다 — "더보기"로 안 펼친 카드도
+ 일치하면 나온다. 일치가 많으면 기존처럼 30장 + 더보기.
+- 일치 기준: 댓글 본문(HTML→평문) + 작성자 이름, 대소문자 무시.
+ 댓글마다 검색용 평문을 1회 만들어 캐시(`c._st`).
+- 일치 0장 섹션은 헤더째 숨김. 검색어를 지우면 원래 화면 복원.
+- 선택 로직 불변: 카드는 원래 컷 섹션 소속이므로 클릭하면 그 컷에 들어간다.
+ 선택 링·사용중 배지·"선택한 것만 보기"는 기존 그대로(검색과 AND).
+
+## 캡처 누락 방지 (중요)
+
+빌드/캡처는 `wrapOf()`로 DOM에서 카드를 찾고 **없으면 조용히 건너뛴다**
+(`buildAll`, `window.ytCC.capture`). 검색 필터로 선택 카드가 DOM에서 빠진 채
+빌드하면 카드가 누락되므로:
+
+1. 섹션은 검색 전 렌더 수(`baseShown`)를 기억하고, 검색 해제 시 그만큼 복원한다.
+ (선택은 렌더된 카드에서만 가능 → 복원하면 선택 카드가 항상 DOM에 있음)
+2. `buildAll` 시작 시(각 ID)와 `ytCC.capture` 시작 시 해당 패널의 검색을
+ 강제 초기화(`clearSearch(hlId)`)한 뒤 캡처한다.
+
+## 구현 구조
+
+- `SECS = {hlId: [setFilter,…]}` — 패널별 섹션 필터 레지스트리.
+ `searchBar(hlId)`가 패널 생성 때 배열을 초기화하고, `cardSection()`이 자기
+ `setFilter(q)`를 등록한다(재렌더: grid 비우고 필터된 목록으로 다시 페이징).
+- `cardSection` 소폭 리팩터: `more.remove()` → `display:none` 토글
+ (필터 해제 후 다시 필요할 수 있으므로).
+- CSS: `.ccsearch` 몇 줄 (index.html).
diff --git a/server/static/auto.js b/server/static/auto.js
index e73cf46..c3780ae 100644
--- a/server/static/auto.js
+++ b/server/static/auto.js
@@ -63,6 +63,16 @@ function wrapOf(hlId,idx){
const CARD_PAGE=30;
// '좋아요 상위에서 채우기' 섹션은 컷마다 반복된다 — 초기 렌더를 줄여 DOM 폭증을 막는다
const CUT_FILL_PAGE=6;
+/* 검색용 평문(본문+작성자, 소문자) — 댓글마다 1회 계산해 캐시 */
+function searchStr(c){
+ if(c._st===undefined){
+ const tmp=document.createElement("div");
+ tmp.innerHTML=String(c.text||"").replace(/
/gi,"\n");
+ c._st=((tmp.textContent||"")+" "+(c.authorName||"")).toLowerCase();
+ }
+ return c._st;
+}
+const SECS={}; // hlId → [setFilter,…] (searchBar 가 패널 생성 때 초기화)
function cardSection(label,list,hlId,firstBatch,ci){
const sec=document.createElement("div");sec.className="hlsec";
const head=document.createElement("div");head.textContent=label;
@@ -71,25 +81,62 @@ function cardSection(label,list,hlId,firstBatch,ci){
sec.appendChild(grid);
const more=document.createElement("button");
more.type="button";more.className="ghost cc-more";
- let shown=0;
+ const first=firstBatch||CARD_PAGE;
+ let cur=list,shown=0,baseShown=0; // baseShown: 검색 전 렌더 수 — 해제 시 복원해야
+ // 선택 카드가 DOM에 남는다(캡처는 wrapOf 로 DOM에서 찾음)
function render(batch){
- const end=Math.min(list.length,shown+batch);
+ const end=Math.min(cur.length,shown+batch);
for(;shown=list.length) more.remove();
- else more.textContent="더보기 ▾ (남은 "+(list.length-shown).toLocaleString()+"장)";
+ if(cur===list) baseShown=Math.max(baseShown,shown);
+ if(shown>=cur.length) more.style.display="none";
+ else{
+ more.style.display="";
+ more.textContent="더보기 ▾ (남은 "+(cur.length-shown).toLocaleString()+"장)";
+ }
applyUsedMarks(hlId); // 새로 그린 카드에도 사용중 표시
}
more.addEventListener("click",()=>render(CARD_PAGE));
sec.appendChild(more);
- render(firstBatch||CARD_PAGE);
+ render(first);
+ (SECS[hlId]=SECS[hlId]||[]).push(function setFilter(q){
+ grid.innerHTML="";shown=0;
+ if(!q){cur=list;sec.style.display="";render(Math.max(baseShown,first));return;}
+ cur=list.filter(i=>{const c=byIdx[i];return c&&searchStr(c).includes(q);});
+ sec.style.display=cur.length?"":"none";
+ render(CARD_PAGE);
+ });
return sec;
}
+/* ── 패널 검색창 — 입력하면 그 패널의 모든 섹션이 일치 카드만 다시 그림 ── */
+function searchBar(hlId){
+ SECS[hlId]=[]; // 패널 재생성 시 이전 섹션 필터 폐기
+ const bar=document.createElement("div");
+ bar.className="ccsearch";
+ bar.innerHTML=''+
+ '';
+ const inp=bar.querySelector("input");
+ let t=null;
+ function apply(){
+ const q=inp.value.trim().toLowerCase();
+ (SECS[hlId]||[]).forEach(f=>f(q));
+ }
+ inp.addEventListener("input",()=>{clearTimeout(t);t=setTimeout(apply,150);});
+ bar.querySelector(".ccsx").addEventListener("click",()=>{inp.value="";apply();inp.focus();});
+ bar._clear=()=>{if(inp.value){inp.value="";clearTimeout(t);apply();}};
+ return bar;
+}
+/* 빌드/캡처 전 필수 — 검색으로 카드가 DOM에서 빠진 채 캡처하면 조용히 누락된다 */
+function clearSearch(hlId){
+ const box=$("#hlbox-"+hlId);
+ const bar=box&&box.querySelector(".ccsearch");
+ if(bar&&bar._clear) bar._clear();
+}
/* ── 컷별 선택 ── 카드는 '어느 컷 소속'인지가 배치를 정한다.
selCut[hlId][idx] = 컷 인덱스. sel[hlId] 는 항상 컷 순서로 정렬해 둔다
(업로드 순서 = 배치 순서라서). */
@@ -490,6 +537,7 @@ 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){
@@ -589,6 +637,7 @@ async function buildAll(){
let ok=0,fail=0;
for(const hl of hls){
showId(hl.id); // 캡처는 보이는 상태에서
+ clearSearch(hl.id); // 검색 중이면 카드가 DOM에 없어 캡처가 누락됨
await nextFrame();
boardSet(hl.id,"🔄 진행","", "active");
try{
@@ -716,6 +765,7 @@ async function ytMatch(){
''+
' 카드 3초 고정 — 모자라도 늘리지 않고 뒤는 비움 (부분삭제 편집용)'+
'';
+ box.appendChild(searchBar("yt"));
box.appendChild(cardSection(
"⭐ 구간을 언급한 댓글 "+matched.length+"장 (좋아요순, 자동 선택)",
matched,"yt",Math.max(CARD_PAGE,sel["yt"].length)));
@@ -733,6 +783,7 @@ async function ytMatch(){
window.ytCC={
active:function(){return !!(YT_HL&&sel["yt"]&&sel["yt"].length);},
capture:async function(){
+ clearSearch("yt"); // 검색 중이면 카드가 DOM에 없어 캡처가 누락됨
const out=[];
for(const idx of sel["yt"]){
const w=wrapOf("yt",idx);
diff --git a/server/static/index.html b/server/static/index.html
index b4a1857..17f668e 100644
--- a/server/static/index.html
+++ b/server/static/index.html
@@ -251,6 +251,12 @@
.selonly-label{display:inline-flex;align-items:center;gap:6px;color:var(--muted2);font-size:12px;
cursor:pointer;margin-left:auto;padding:6px;}
.hlbox.selonly .cardsec .ccwrap:not(.sel){display:none;}
+ /* 댓글 검색창 */
+ .ccsearch{display:flex;gap:6px;align-items:center;margin-top:10px;}
+ .ccsearch input{flex:1;max-width:420px;min-height:40px;padding:8px 12px;background:var(--surf);
+ border:1px solid var(--border);border-radius:8px;color:var(--text);font-size:13px;}
+ .ccsearch input:focus{outline:none;border-color:var(--accent);}
+ .ccsearch .ccsx{width:auto;min-height:40px;padding:6px 13px;flex:none;margin:0;}
/* 카드 그리드 */
.cardsec{display:flex;flex-wrap:wrap;gap:12px;margin-top:8px;}
.cardsec .ccwrap{margin:0;}