diff --git a/public/editor.js b/public/editor.js index 4913ac8..67bc18e 100644 --- a/public/editor.js +++ b/public/editor.js @@ -20,6 +20,103 @@ var endSec = document.getElementById('endSec') var saveBtn = document.getElementById('saveBtn') + // ── 영상 ID 인라인 편집 (실시간 가용성 표시) ─────────────────────── + var videoIdInput = document.getElementById('videoIdInput') + var idStatus = document.getElementById('idStatus') + var changeIdBtn = document.getElementById('changeIdBtn') + var copyShareBtn = document.getElementById('copyShareBtn') + var ID_RE = /^[a-zA-Z0-9_-]{1,64}$/ + var idDebounce = null + + function setIdStatus(text, cls) { + if (!idStatus) return + idStatus.textContent = text + idStatus.className = 'idStatus ' + (cls || 'muted') + } + + function refreshIdStatus() { + if (!video || !videoIdInput) return + var v = (videoIdInput.value || '').trim() + if (!v) { + setIdStatus('비어 있음', 'idStatusBad') + changeIdBtn.disabled = true + return + } + if (v === video.id) { + setIdStatus('현재 ID', 'muted') + changeIdBtn.disabled = true + return + } + if (!ID_RE.test(v)) { + setIdStatus('형식: 영문/숫자/_/- (1~64자)', 'idStatusBad') + changeIdBtn.disabled = true + return + } + setIdStatus('확인 중…', 'muted') + changeIdBtn.disabled = true + clearTimeout(idDebounce) + idDebounce = setTimeout(function () { + fetch('/op/videos/idAvailable?id=' + encodeURIComponent(v), { cache: 'no-store' }) + .then(function (r) { return r.json() }) + .then(function (j) { + // stale 응답 (사용자가 그 사이에 입력을 더 바꾼 경우) 무시 + if ((videoIdInput.value || '').trim() !== v) return + if (j && j.ok && j.available) { + setIdStatus('사용 가능', 'idStatusOk') + changeIdBtn.disabled = false + } else { + setIdStatus(j && j.reason ? j.reason : '이미 사용 중', 'idStatusBad') + changeIdBtn.disabled = true + } + }) + .catch(function () { + if ((videoIdInput.value || '').trim() !== v) return + setIdStatus('확인 실패', 'idStatusBad') + changeIdBtn.disabled = true + }) + }, 250) + } + + if (videoIdInput && video) { + videoIdInput.addEventListener('input', refreshIdStatus) + changeIdBtn.addEventListener('click', function () { + var newId = (videoIdInput.value || '').trim() + if (!newId || newId === video.id) return + changeIdBtn.disabled = true + setIdStatus('변경 중…', 'muted') + fetch('/op/videos/' + encodeURIComponent(video.id) + '/changeId', { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ newId: newId }) + }).then(function (r) { return r.json() }).then(function (j) { + if (!j.ok) { + setIdStatus(j.message || 'ID 변경 실패', 'idStatusBad') + changeIdBtn.disabled = false + return + } + // 같은 페이지를 새 ID 로 다시 진입 (URL ?id= 도 갱신). + location.replace(folderBaseUrl + '/video/editor?id=' + encodeURIComponent(j.video.id)) + }).catch(function (e) { + setIdStatus('변경 실패: ' + e.message, 'idStatusBad') + changeIdBtn.disabled = false + }) + }) + if (copyShareBtn) { + copyShareBtn.addEventListener('click', function () { + // 입력란의 후보 ID 가 아니라 실제로 DB 에 저장된 video.id 로 URL 을 만든다. + // (변경 전에는 새 URL 이 아직 살아있지 않으므로.) + var shareUrl = location.origin + '/video/' + folderPathEnc + '/' + encodeURIComponent(video.id) + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(shareUrl).then(function () { + setIdStatus('공유 주소 복사됨', 'idStatusOk') + }, function () { window.prompt('주소를 복사하세요:', shareUrl) }) + } else { + window.prompt('주소를 복사하세요:', shareUrl) + } + }) + } + } + // 드래그&드롭 ;['dragenter', 'dragover'].forEach(function (evt) { dropZone.addEventListener(evt, function (e) { diff --git a/public/styles.css b/public/styles.css index b3deb94..c92cea1 100644 --- a/public/styles.css +++ b/public/styles.css @@ -230,6 +230,20 @@ body.siteBody.centerLayout { display: flex; align-items: center; justify-content .videoPanel video { width: 100%; max-height: 60vh; background: #000; border-radius: 10px; } +.videoIdRow { + display: flex; align-items: center; gap: 10px; flex-wrap: wrap; + background: var(--bg-card); border: 1px solid var(--border); + border-radius: 10px; padding: 10px 14px; +} +.videoIdRow label { color: var(--text-muted); font-size: 13px; } +.videoIdRow input[type="text"] { + background: var(--bg); border: 1px solid var(--border); border-radius: 6px; + color: var(--text); padding: 6px 10px; font-family: ui-monospace, monospace; + font-size: 14px; min-width: 260px; flex: 1; max-width: 420px; +} +.idStatus { font-size: 13px; min-width: 90px; } +.idStatusOk { color: #5dd28a; } +.idStatusBad { color: #e57373; } .trimTimeline { background: var(--bg-card); border: 1px solid var(--border); border-radius: 10px; padding: 16px; diff --git a/views/op/editor.ejs b/views/op/editor.ejs index 73e9e06..b1eafe9 100644 --- a/views/op/editor.ejs +++ b/views/op/editor.ejs @@ -39,6 +39,15 @@