diff --git a/src/main/resources/templates/reply.html b/src/main/resources/templates/reply.html
index b8a9d57..b7fa7e8 100644
--- a/src/main/resources/templates/reply.html
+++ b/src/main/resources/templates/reply.html
@@ -14,6 +14,9 @@
내 영상에 달린 댓글 중 아직 답 안 한 것만 모아, 영상 내용까지 보고 초안을 씁니다.
+
@@ -36,6 +39,22 @@
+
+
+
+
+
초안 생성 중...
+
+
0 / 0
+
+
+
+ 영상을 처음 읽는 건은 시간이 더 걸립니다. 만든 초안은 그때그때 저장되니 중단해도 남습니다.
+
+
+
@@ -299,9 +318,21 @@
}
hideState();
document.getElementById('list').innerHTML = items.map(cardHtml).join('');
+ updateBatchBtn();
if(window.lucide) lucide.createIcons();
}
+ /** 일괄 생성 버튼에 남은 건수를 붙이고, 할 게 없으면 끈다. */
+ function updateBatchBtn(){
+ const btn = document.getElementById('batchBtn');
+ if(!btn || btn.disabled) return; // 실행 중이면 건드리지 않는다
+ const n = items.filter(c => c.status !== 'DONE' && !c.draftA && !c.draftB).length;
+ btn.innerHTML = ` 전체 초안 생성${n ? ` (${n})` : ''}`;
+ btn.classList.toggle('btn-secondary', true);
+ btn.style.opacity = n ? '' : '.5';
+ btn.style.pointerEvents = n ? '' : 'none';
+ }
+
// ---------- 액션 ----------
async function collect(){
const btn = document.getElementById('collectBtn');
@@ -343,6 +374,68 @@
}
}
+ // ---------- 일괄 생성 ----------
+ // 서버에서 통짜로 돌리면 몇 분짜리 요청이 되어 프록시에서 끊긴다.
+ // 한 건씩 순차 호출해 진행률을 보여주고, 중단해도 앞서 만든 초안은 남게 한다.
+ let batchAbort = false;
+
+ function setBatchUI(on, done, total, label){
+ const bar = document.getElementById('batchBar');
+ bar.classList.toggle('hidden', !on);
+ document.getElementById('batchBtn').disabled = on;
+ document.getElementById('collectBtn').disabled = on;
+ if(on){
+ document.getElementById('batchCount').textContent = `${done} / ${total}`;
+ document.getElementById('batchFill').style.width = total ? (done/total*100)+'%' : '0%';
+ if(label) document.getElementById('batchLabel').textContent = label;
+ if(window.lucide) lucide.createIcons();
+ }
+ }
+
+ function cancelBatch(){
+ batchAbort = true;
+ document.getElementById('batchLabel').textContent = '중단하는 중... (진행 중인 1건은 끝냅니다)';
+ }
+
+ async function draftAll(){
+ const targets = items.filter(c => c.status !== 'DONE' && !c.draftA && !c.draftB);
+ if(targets.length === 0){
+ toast('초안이 없는 댓글이 없습니다');
+ return;
+ }
+ // 실측 건당 26~32초(영상을 처음 읽는 비용). 예상 시간을 알려주고 시작한다.
+ const mins = Math.max(1, Math.round(targets.length * 30 / 60));
+ if(!confirm(`초안 없는 댓글 ${targets.length}건에 초안을 만듭니다.\n`
+ + `건당 30초 안팎이라 약 ${mins}분 걸립니다.\n\n`
+ + `창을 켜둔 채로 두시면 되고, 만든 초안은 그때그때 저장됩니다.\n진행할까요?`)) return;
+
+ batchAbort = false;
+ let done = 0, failed = 0;
+ setBatchUI(true, 0, targets.length, '초안 생성 중...');
+
+ for(const c of targets){
+ if(batchAbort) break;
+ try {
+ const updated = await api(`${API}/${c.id}/draft?refreshContext=false`, { method:'POST' });
+ const i = items.findIndex(v => v.id === c.id);
+ if(i >= 0) items[i] = updated;
+ } catch(e){
+ failed++;
+ console.warn('초안 실패', c.id, e.message);
+ }
+ done++;
+ setBatchUI(true, done, targets.length);
+ render(); // 완성되는 대로 카드에 반영
+ }
+
+ setBatchUI(false);
+ loadStats();
+ const msg = batchAbort
+ ? `중단했습니다 — ${done - failed}건 생성`
+ : `${done - failed}건 생성 완료`;
+ toast(failed ? `${msg} · ${failed}건 실패` : msg, failed > 0);
+ }
+
async function copyDraft(btn, id, key){
const c = items.find(v => v.id === id);
if(!c) return;