fix(job-poll): disable caching and recover from fetch errors

Review P1: 진행률 폴링이 304 응답을 받으면 r.json() 이 reject 되는데
.catch() 가 없어 setTimeout 도 안 걸리고 폴링이 영구 중단됐습니다.
이게 "60fps 변환 확인 중" 에서 바가 멈춰 보이던 진짜 원인이었어요.

세 곳을 다 고침:

1) src/routes/op.ts `/op/job/:id`
   - `Cache-Control: no-store, no-cache, must-revalidate` + `Pragma: no-cache`
   - 브라우저가 conditional GET 으로 304 를 받지 않게 한다.

2) public/editor.js fetch
   - `{ cache: 'no-store' }` 옵션. 서버 헤더 + 클라 옵션 둘 다.

3) public/editor.js pollJob
   - `.catch()` 추가. 일시적 네트워크/파싱 오류여도 2 초 백오프로
     폴링을 재개한다. 변환이 오래 걸려도 바가 계속 갱신됨을 보장.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Claude
2026-05-16 03:00:58 +09:00
parent cdf56b96b7
commit 67d4fb89b8
2 changed files with 29 additions and 16 deletions

View File

@@ -123,22 +123,31 @@
})
function pollJob(jobId, videoId) {
fetch('/op/job/' + encodeURIComponent(jobId)).then(function (r) { return r.json() }).then(function (j) {
if (!j.ok) {
probeInfo.textContent = j.message || '작업을 찾을 수 없음'
return
}
var job = j.job
dlProgress.value = job.progress
probeInfo.textContent = job.message
if (job.status === 'done') {
location.href = '/op/folder/' + encodeURIComponent(folder) + '/video/editor?id=' + encodeURIComponent(videoId)
} else if (job.status === 'error') {
probeInfo.textContent = '실패: ' + (job.error || '')
} else {
setTimeout(function () { pollJob(jobId, videoId) }, 1500)
}
})
// cache: 'no-store' 로 304 가 나지 않게 강제. 304 면 body 가 비어
// r.json() 이 reject → 폴링 중단되는 문제 방지.
fetch('/op/job/' + encodeURIComponent(jobId), { cache: 'no-store' })
.then(function (r) { return r.json() })
.then(function (j) {
if (!j.ok) {
probeInfo.textContent = j.message || '작업을 찾을 수 없음'
return
}
var job = j.job
dlProgress.value = job.progress
probeInfo.textContent = job.message
if (job.status === 'done') {
location.href = '/op/folder/' + encodeURIComponent(folder) + '/video/editor?id=' + encodeURIComponent(videoId)
} else if (job.status === 'error') {
probeInfo.textContent = '실패: ' + (job.error || '')
} else {
setTimeout(function () { pollJob(jobId, videoId) }, 1500)
}
})
.catch(function (err) {
// 네트워크 일시 오류로 폴링이 영구 중단되지 않도록 짧게 백오프 후 재시도.
console.warn('[pollJob] fetch 실패, 재시도:', err)
setTimeout(function () { pollJob(jobId, videoId) }, 2000)
})
}
// ── 타임라인 트림 (lossless-cut 류 핸들 UI) ─────────────────────────

View File

@@ -291,6 +291,10 @@ opRouter.post('/op/folder/:name/video/youtube/start', requireAuth, async (req, r
})
opRouter.get('/op/job/:id', requireAuth, (req, res) => {
// 폴링 응답이 304 로 캐싱되면 브라우저가 body 없는 응답을 돌려줘서
// 클라이언트의 r.json() 이 reject → 폴링이 중단된다. 항상 신선한 응답.
res.set('Cache-Control', 'no-store, no-cache, must-revalidate')
res.set('Pragma', 'no-cache')
const job = getJob(req.params.id)
if (!job) {
res.status(404).json({ ok: false, message: '작업을 찾을 수 없습니다.' })