feat(folders): nested folder tree (max depth 2) + id-based mutations
- new storeDb.ts: DB+FS layer (folders w/ parent_id, videos w/ global id)
- depth cap: sub-folder cannot have children (enforced in createFolder)
- video id: editable anytime (changeVideoId), uniqueness via PK
- routes:
- public /folder/:topName[/:subName], /video/:topName[/:subName]/:videoId
- admin /op/folder/:topName[/:subName], id-based /op/folders/:id/*
and /op/videos/:id/* (rename/changeId/delete/save + idAvailable check)
- views: breadcrumb + subFolder grid; 플레이리스트 추가 button shows at 2단계
- client JS: id-based mutation endpoints, share URL data attribute,
in-site player pushes share URL to address bar
- store.ts slimmed to Account + readAccounts (FS layer moved to storeDb)
- pickIntId accepts numeric JSON bodies
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
(function () {
|
||||
var ctx = window.__EDITOR__ || { folder: '', video: null }
|
||||
var folder = ctx.folder
|
||||
var ctx = window.__EDITOR__ || { folderPath: [], folderId: null, video: null }
|
||||
var folderPathEnc = (ctx.folderPath || []).map(encodeURIComponent).join('/')
|
||||
var folderBaseUrl = '/op/folder/' + folderPathEnc
|
||||
var video = ctx.video
|
||||
|
||||
var dropZone = document.getElementById('dropZone')
|
||||
@@ -46,7 +47,7 @@
|
||||
form.append('title', titleInput.value || file.name)
|
||||
uploadStatus.textContent = '업로드 중...'
|
||||
var xhr = new XMLHttpRequest()
|
||||
xhr.open('POST', '/op/folder/' + encodeURIComponent(folder) + '/video/upload')
|
||||
xhr.open('POST', folderBaseUrl + '/video/upload')
|
||||
xhr.upload.addEventListener('progress', function (e) {
|
||||
if (e.lengthComputable) {
|
||||
var pct = Math.round((e.loaded / e.total) * 100)
|
||||
@@ -57,7 +58,7 @@
|
||||
try {
|
||||
var res = JSON.parse(xhr.responseText)
|
||||
if (res.ok) {
|
||||
location.href = '/op/folder/' + encodeURIComponent(folder) + '/video/editor?id=' + encodeURIComponent(res.videoId)
|
||||
location.href = folderBaseUrl + '/video/editor?id=' + encodeURIComponent(res.videoId)
|
||||
} else {
|
||||
uploadStatus.textContent = '업로드 실패: ' + (res.message || '')
|
||||
}
|
||||
@@ -89,7 +90,7 @@
|
||||
probeInfo.textContent = '확인 중...'
|
||||
ytProbeBtn.disabled = true
|
||||
ytStartBtn.disabled = true
|
||||
fetch('/op/folder/' + encodeURIComponent(folder) + '/video/youtube/probe', {
|
||||
fetch(folderBaseUrl + '/video/youtube/probe', {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ url: url })
|
||||
@@ -144,7 +145,7 @@
|
||||
}
|
||||
// 중복 클릭 방지
|
||||
ytStartBtn.disabled = true
|
||||
fetch('/op/folder/' + encodeURIComponent(folder) + '/video/youtube/start', {
|
||||
fetch(folderBaseUrl + '/video/youtube/start', {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ url: url, title: titleInput.value })
|
||||
@@ -180,7 +181,7 @@
|
||||
dlProgress.value = job.progress
|
||||
probeInfo.textContent = job.message
|
||||
if (job.status === 'done') {
|
||||
location.href = '/op/folder/' + encodeURIComponent(folder) + '/video/editor?id=' + encodeURIComponent(videoId)
|
||||
location.href = folderBaseUrl + '/video/editor?id=' + encodeURIComponent(videoId)
|
||||
} else if (job.status === 'error') {
|
||||
probeInfo.textContent = '실패: ' + (job.error || '')
|
||||
} else {
|
||||
@@ -364,14 +365,13 @@
|
||||
saveBtn.addEventListener('click', function () {
|
||||
if (!video) { alert('먼저 영상을 추가하세요.'); return }
|
||||
var payload = {
|
||||
id: video.id,
|
||||
title: titleInput.value,
|
||||
startSec: trimStart,
|
||||
endSec: trimEnd
|
||||
}
|
||||
saveBtn.disabled = true
|
||||
saveBtn.textContent = '저장 중...'
|
||||
fetch('/op/folder/' + encodeURIComponent(folder) + '/video/save', {
|
||||
fetch('/op/videos/' + encodeURIComponent(video.id) + '/save', {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
@@ -380,7 +380,7 @@
|
||||
saveBtn.textContent = '저장'
|
||||
if (j.ok) {
|
||||
alert(j.note || '저장 완료')
|
||||
location.href = '/op/folder/' + encodeURIComponent(folder)
|
||||
location.href = folderBaseUrl
|
||||
} else {
|
||||
alert(j.message || '저장 실패')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user