feat: 플레이리스트 추가 화면 임시저장(draft) 기능
다운로드 시작 전, 수정해둔 값(플레이리스트 URL + 항목별 제목/ID/시작/길이/ ID모드/자릿수채우기)을 폴더당 1개 저장하고 나중에 불러올 수 있게 함. - db: playlist_drafts 테이블 (folder_id PK, JSON data, updated_at) - storeDb: savePlaylistDraft/getPlaylistDraft/deletePlaylistDraft - op: GET/POST/POST(delete) /playlist/draft (하위 폴더 한정) - UI: 임시저장 / 불러오기 / 삭제 버튼 + 진입 시 기존 draft 자동 감지 Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
This commit is contained in:
@@ -1003,6 +1003,179 @@
|
||||
renderTrimBar()
|
||||
})
|
||||
|
||||
// ─── 임시저장(draft) ───────────────────────────────────────────────────
|
||||
// 다운로드 시작 전, 수정해둔 값(URL + 항목별 제목/ID/시작/길이 등)을 폴더당 1개 저장/복원.
|
||||
var draftUrl = folderBaseUrl + '/playlist/draft'
|
||||
var draftDeleteUrl = folderBaseUrl + '/playlist/draft/delete'
|
||||
var draftSaveBtn = document.getElementById('draftSaveBtn')
|
||||
var draftLoadBtn = document.getElementById('draftLoadBtn')
|
||||
var draftDeleteBtn = document.getElementById('draftDeleteBtn')
|
||||
var draftStatus = document.getElementById('draftStatus')
|
||||
|
||||
function setDraftStatus(text, cls) {
|
||||
if (!draftStatus) return
|
||||
draftStatus.textContent = text || ''
|
||||
draftStatus.className = cls || 'muted'
|
||||
}
|
||||
|
||||
function formatSavedAt(iso) {
|
||||
try {
|
||||
var d = new Date(iso)
|
||||
if (isNaN(d.getTime())) return ''
|
||||
return d.toLocaleString()
|
||||
} catch (_) { return '' }
|
||||
}
|
||||
|
||||
// 저장 대상: 편집 가능한 값만 추린다 (idStatus/idDebounce 같은 휘발 상태 제외).
|
||||
function serializeDraft() {
|
||||
return {
|
||||
url: (urlInput.value || '').trim(),
|
||||
idMode: state.idMode,
|
||||
zeroPad: state.zeroPad,
|
||||
entries: state.entries.map(function (e) {
|
||||
return {
|
||||
url: e.url,
|
||||
title: e.title || '',
|
||||
videoId: e.videoId || '',
|
||||
durationSec: e.durationSec != null ? e.durationSec : null,
|
||||
thumbnailUrl: e.thumbnailUrl || null,
|
||||
ytId: e.ytId || null,
|
||||
startSec: e.startSec || 0,
|
||||
lengthSec: e.lengthSec != null ? e.lengthSec : DEFAULT_LENGTH
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// idMode 라디오 / zeroPad 토글을 상태에 맞춰 반영.
|
||||
function reflectIdModeControls() {
|
||||
for (var i = 0; i < idModeRadios.length; i++) {
|
||||
idModeRadios[i].checked = idModeRadios[i].value === state.idMode
|
||||
}
|
||||
zeroPadWrap.hidden = state.idMode !== 'sequential'
|
||||
zeroPadToggle.checked = !!state.zeroPad
|
||||
}
|
||||
|
||||
function restoreDraft(d) {
|
||||
if (!d || typeof d !== 'object') return
|
||||
urlInput.value = d.url || ''
|
||||
state.idMode = d.idMode === 'sequential' ? 'sequential' : 'random'
|
||||
state.zeroPad = !!d.zeroPad
|
||||
var raw = Array.isArray(d.entries) ? d.entries : []
|
||||
state.entries = raw.map(function (e) {
|
||||
return {
|
||||
url: e.url,
|
||||
title: e.title || '',
|
||||
videoId: (e.videoId || '').trim(),
|
||||
durationSec: e.durationSec != null ? e.durationSec : null,
|
||||
thumbnailUrl: e.thumbnailUrl || null,
|
||||
ytId: e.ytId || extractYouTubeId(e.url),
|
||||
startSec: e.startSec || 0,
|
||||
lengthSec: e.lengthSec != null && e.lengthSec > 0 ? e.lengthSec : DEFAULT_LENGTH,
|
||||
idStatus: 'pending',
|
||||
idReason: ''
|
||||
}
|
||||
})
|
||||
reflectIdModeControls()
|
||||
render()
|
||||
entriesSection.hidden = state.entries.length === 0
|
||||
// 저장된 ID 는 그대로 두고(자동 재생성 금지), 가용성만 다시 확인.
|
||||
for (var i = 0; i < state.entries.length; i++) scheduleIdCheck(i, 0)
|
||||
}
|
||||
|
||||
function showDraftButtons(updatedAt) {
|
||||
if (draftLoadBtn) draftLoadBtn.hidden = false
|
||||
if (draftDeleteBtn) draftDeleteBtn.hidden = false
|
||||
setDraftStatus('임시저장됨: ' + formatSavedAt(updatedAt), 'muted')
|
||||
}
|
||||
function hideDraftButtons() {
|
||||
if (draftLoadBtn) draftLoadBtn.hidden = true
|
||||
if (draftDeleteBtn) draftDeleteBtn.hidden = true
|
||||
}
|
||||
|
||||
if (draftSaveBtn) {
|
||||
draftSaveBtn.addEventListener('click', function () {
|
||||
draftSaveBtn.disabled = true
|
||||
setDraftStatus('저장 중…', 'muted')
|
||||
fetch(draftUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ data: serializeDraft() })
|
||||
})
|
||||
.then(function (r) { return r.json() })
|
||||
.then(function (j) {
|
||||
if (j && j.ok) {
|
||||
showDraftButtons(j.updatedAt)
|
||||
setDraftStatus('임시저장 완료 (' + formatSavedAt(j.updatedAt) + ')', 'idStatusOk')
|
||||
} else {
|
||||
setDraftStatus((j && j.message) || '저장 실패', 'idStatusBad')
|
||||
}
|
||||
})
|
||||
.catch(function (err) {
|
||||
setDraftStatus('저장 실패: ' + (err && err.message ? err.message : err), 'idStatusBad')
|
||||
})
|
||||
.finally(function () { draftSaveBtn.disabled = false })
|
||||
})
|
||||
}
|
||||
|
||||
if (draftLoadBtn) {
|
||||
draftLoadBtn.addEventListener('click', function () {
|
||||
if (state.entries.length > 0 &&
|
||||
!window.confirm('현재 편집 중인 목록을 임시저장 내용으로 덮어쓸까요?')) {
|
||||
return
|
||||
}
|
||||
draftLoadBtn.disabled = true
|
||||
setDraftStatus('불러오는 중…', 'muted')
|
||||
fetch(draftUrl, { cache: 'no-store' })
|
||||
.then(function (r) { return r.json() })
|
||||
.then(function (j) {
|
||||
if (j && j.ok && j.draft) {
|
||||
var data
|
||||
try { data = JSON.parse(j.draft.data) } catch (_) { data = null }
|
||||
if (!data) { setDraftStatus('임시저장 데이터를 읽을 수 없습니다.', 'idStatusBad'); return }
|
||||
restoreDraft(data)
|
||||
setDraftStatus('임시저장 불러옴 (' + formatSavedAt(j.draft.updatedAt) + ')', 'idStatusOk')
|
||||
} else {
|
||||
hideDraftButtons()
|
||||
setDraftStatus('저장된 임시저장이 없습니다.', 'muted')
|
||||
}
|
||||
})
|
||||
.catch(function (err) {
|
||||
setDraftStatus('불러오기 실패: ' + (err && err.message ? err.message : err), 'idStatusBad')
|
||||
})
|
||||
.finally(function () { draftLoadBtn.disabled = false })
|
||||
})
|
||||
}
|
||||
|
||||
if (draftDeleteBtn) {
|
||||
draftDeleteBtn.addEventListener('click', function () {
|
||||
if (!window.confirm('저장된 임시저장을 삭제할까요?')) return
|
||||
draftDeleteBtn.disabled = true
|
||||
fetch(draftDeleteUrl, { method: 'POST' })
|
||||
.then(function (r) { return r.json() })
|
||||
.then(function (j) {
|
||||
if (j && j.ok) {
|
||||
hideDraftButtons()
|
||||
setDraftStatus('임시저장을 삭제했습니다.', 'muted')
|
||||
} else {
|
||||
setDraftStatus((j && j.message) || '삭제 실패', 'idStatusBad')
|
||||
}
|
||||
})
|
||||
.catch(function (err) {
|
||||
setDraftStatus('삭제 실패: ' + (err && err.message ? err.message : err), 'idStatusBad')
|
||||
})
|
||||
.finally(function () { draftDeleteBtn.disabled = false })
|
||||
})
|
||||
}
|
||||
|
||||
// 진입 시 기존 임시저장 여부 확인 → 있으면 불러오기/삭제 버튼 노출.
|
||||
fetch(draftUrl, { cache: 'no-store' })
|
||||
.then(function (r) { return r.json() })
|
||||
.then(function (j) {
|
||||
if (j && j.ok && j.draft) showDraftButtons(j.draft.updatedAt)
|
||||
})
|
||||
.catch(function () {})
|
||||
|
||||
// 초기 상태
|
||||
updateRegisterEnable()
|
||||
})()
|
||||
|
||||
Reference in New Issue
Block a user