feat: 댓글 답장 초안 일괄 생성
초안 없는 댓글을 한 번에 처리한다. 서버에서 통짜로 돌리지 않고 화면에서 한 건씩 순차 호출한다 — 실측 건당 26~32초라 26건이면 13분짜리 요청이 되어 프록시에서 끊기기 때문이다. 순차 방식이면 진행률이 보이고, 중단해도 앞서 만든 초안은 남는다. - 진행 바 + 남은 건수, 중단 버튼(진행 중인 1건은 마저 끝냄) - 완성되는 대로 카드에 반영되어 기다리는 동안에도 읽을 수 있다 - 실패한 건은 건너뛰고 개수만 보고한다(다시 눌러 재시도) - 확인창에 예상 소요 시간을 알려준다 — 13분짜리 작업을 모르고 시작하지 않게 - 버튼에 남은 건수를 표시하고, 할 게 없으면 비활성화 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d451aff8cc
commit
42ec17b054
@ -14,6 +14,9 @@
|
||||
<p class="sub">내 영상에 달린 댓글 중 아직 답 안 한 것만 모아, 영상 내용까지 보고 초안을 씁니다.</p>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn btn-secondary" id="batchBtn" onclick="draftAll()">
|
||||
<i data-lucide="wand-sparkles" style="width:15px;"></i> 전체 초안 생성
|
||||
</button>
|
||||
<button class="btn btn-primary" id="collectBtn" onclick="collect()">
|
||||
<i data-lucide="download" style="width:15px;"></i> 댓글 수집
|
||||
</button>
|
||||
@ -36,6 +39,22 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 일괄 생성 진행 -->
|
||||
<div id="batchBar" class="card mb-4 hidden" role="status" aria-live="polite">
|
||||
<div class="flex items-center gap-3" style="flex-wrap:wrap;">
|
||||
<i data-lucide="loader-2" class="animate-spin" style="width:16px; color:var(--accent);"></i>
|
||||
<span class="text-sm font-semibold" id="batchLabel">초안 생성 중...</span>
|
||||
<div class="bar-track" style="flex:1; min-width:140px;">
|
||||
<div class="bar-fill" id="batchFill" style="width:0%; background:var(--accent);"></div>
|
||||
</div>
|
||||
<span class="badge badge-muted" id="batchCount">0 / 0</span>
|
||||
<button class="btn btn-secondary" style="padding:0.4rem 0.7rem;" onclick="cancelBatch()">중단</button>
|
||||
</div>
|
||||
<p class="text-xs text-muted mt-2" id="batchNote">
|
||||
영상을 처음 읽는 건은 시간이 더 걸립니다. 만든 초안은 그때그때 저장되니 중단해도 남습니다.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div id="list" class="rp-list"></div>
|
||||
<div id="state" class="rp-state hidden"></div>
|
||||
|
||||
@ -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 = `<i data-lucide="wand-sparkles" style="width:15px;"></i> 전체 초안 생성${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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user