diff --git a/public/listEditor.js b/public/listEditor.js index 966dc7f..b8cdea8 100644 --- a/public/listEditor.js +++ b/public/listEditor.js @@ -8,6 +8,11 @@ images: Array.isArray(INITIAL.images) ? INITIAL.images.slice() : [] } + // 저장되지 않은 변경 추적 + var dirty = false + function markDirty() { dirty = true } + function markClean() { dirty = false } + // ── 탭 ──────────────────────────────────────────── var tabBtns = document.querySelectorAll('.tabBtn') tabBtns.forEach(function (btn) { @@ -144,8 +149,11 @@ var field = el.getAttribute('data-field') var value = (el.textContent || '').replace(/\r?\n/g, ' ').trim() if (!state.music[idx]) return + var prev = field === 'title' ? state.music[idx].title : state.music[idx].artist + if (value === prev) return if (field === 'title') state.music[idx].title = value else if (field === 'artist') state.music[idx].artist = value + markDirty() }) el.addEventListener('keydown', function (e) { if (el.getAttribute('contenteditable') !== 'true') return @@ -227,8 +235,11 @@ var arr = (type === 'music') ? state.music : state.images // 원래 인덱스: state 에서 동일 url 을 찾는 대신 data-index 가 렌더 시점의 위치이므로 사용. var srcIdx = Number(drag.srcEl.dataset.index) - var moved = arr.splice(srcIdx, 1)[0] - arr.splice(newIdx, 0, moved) + if (srcIdx !== newIdx) { + var moved = arr.splice(srcIdx, 1)[0] + arr.splice(newIdx, 0, moved) + markDirty() + } cleanupDrag() if (type === 'music') renderMusic(); else renderImage() }) @@ -268,6 +279,7 @@ if (action === 'delete') { if (t.type === 'music') state.music.splice(t.index, 1) else state.images.splice(t.index, 1) + markDirty() if (t.type === 'music') renderMusic(); else renderImage() } else if (action === 'edit') { openEditModal(t.type, t.index) @@ -324,6 +336,7 @@ var msg = (result.body && result.body.message) ? result.body.message : '메타 조회 실패' ask('메타데이터 조회 실패', msg + '\n주소만 변경하고 제목/가수/시간은 그대로 둘까요?', function () { state.music[editingIdx].url = url + markDirty() closeAllModals() renderMusic() }) @@ -337,6 +350,7 @@ artist: meta.channel || prev.artist || '', durationSec: typeof meta.durationSec === 'number' ? meta.durationSec : (prev.durationSec || 0) } + markDirty() closeAllModals() renderMusic() }).catch(function (err) { @@ -358,7 +372,10 @@ document.getElementById('edit-image-save').addEventListener('click', function () { var url = document.getElementById('edit-image-url').value.trim() if (!url) return - state.images[editingIdx].url = url + if (state.images[editingIdx].url !== url) { + state.images[editingIdx].url = url + markDirty() + } closeAllModals() renderImage() }) @@ -374,6 +391,7 @@ + '현재 사진목록은 모두 사라집니다. 진행할까요?', function () { state.images = state.music.map(function (m) { return { url: m.url } }) + markDirty() renderImage() setStatus('status-image', state.images.length + '개 항목을 불러왔습니다.') }) @@ -381,18 +399,26 @@ // ── 액션 (save/clear/fetch) ─────────────────────── var confirmModal = document.getElementById('confirmModal') + var pendingOk = null function ask(title, message, onOk) { document.getElementById('confirm-title').textContent = title document.getElementById('confirm-message').textContent = message confirmModal.hidden = false - var ok = document.getElementById('confirm-ok') - var handler = function () { - ok.removeEventListener('click', handler) - confirmModal.hidden = true - onOk() - } - ok.addEventListener('click', handler) + pendingOk = onOk } + document.getElementById('confirm-ok').addEventListener('click', function () { + confirmModal.hidden = true + var fn = pendingOk + pendingOk = null + if (fn) fn() + }) + // 취소(×, 취소 버튼, 배경 클릭)로 닫히면 pending 콜백 폐기. + confirmModal.querySelectorAll('[data-modal-close]').forEach(function (b) { + b.addEventListener('click', function () { pendingOk = null }) + }) + confirmModal.addEventListener('click', function (e) { + if (e.target === confirmModal) pendingOk = null + }) document.querySelectorAll('[data-action]').forEach(function (btn) { btn.addEventListener('click', function () { @@ -402,6 +428,7 @@ ask('목록 초기화', '"' + (target === 'music' ? '음악' : '사진') + '목록"을 비웁니다. 진행할까요?', function () { if (target === 'music') { state.music = []; renderMusic() } else { state.images = []; renderImage() } + markDirty() }) } else if (action === 'save') { doSave(target) @@ -432,7 +459,7 @@ }).then(function (r) { return r.json().then(function (body) { return { ok: r.ok, body: body } }) }).then(function (result) { - if (result.ok && result.body.ok) setStatus(statusId, '저장 완료') + if (result.ok && result.body.ok) { setStatus(statusId, '저장 완료'); markClean() } else setStatus(statusId, '저장 실패: ' + (result.body.message || ''), true) }).catch(function (err) { setStatus(statusId, '저장 실패: ' + err.message, true) @@ -469,6 +496,7 @@ state.images = entries.map(function (e) { return { url: e.url } }) renderImage() } + markDirty() setStatus('status-' + target, entries.length + '개 항목을 불러왔습니다.') }).catch(function (err) { setStatus('status-' + target, '실패: ' + err.message, true) @@ -476,6 +504,38 @@ }) } + // 플레이리스트 URL 입력 변경 추적 + ;['music-playlist-url', 'image-playlist-url'].forEach(function (id) { + var el = document.getElementById(id) + if (!el) return + var initialValue = el.value + el.addEventListener('input', function () { + if (el.value !== initialValue) markDirty() + }) + }) + + // ── 페이지 이탈 가드 ─────────────────────────────── + // 1) 돌아가기 링크 : 커스텀 확인 팝업 + document.querySelectorAll('a.ghostLink').forEach(function (a) { + a.addEventListener('click', function (e) { + if (!dirty) return + e.preventDefault() + var href = a.getAttribute('href') + ask('저장되지 않은 변경사항', + '저장하지 않은 변경사항이 있습니다.\n저장 없이 이 페이지를 떠나시겠습니까?', + function () { + markClean() + window.location.href = href + }) + }) + }) + // 2) 탭 닫기 / 새로고침 : 브라우저 네이티브 확인 다이얼로그 + window.addEventListener('beforeunload', function (e) { + if (!dirty) return + e.preventDefault() + e.returnValue = '' + }) + // 초기 렌더 renderMusic() renderImage()