site: 음악목록 항목별 별칭 편집 기능 추가
- MusicListEntry 에 aliases: string[] 필드 추가, 저장 시 trim·중복 제거. - 목록 행에 "별칭" 버튼 표시(개수 있으면 강조), 클릭 시 모달 오픈. - 모달에서 "별칭 추가" → 입력행 생성, "−" 버튼 → 해당 행 삭제, 좌상단 "← 돌아가기" 또는 오버레이 클릭으로 저장 후 닫기.
This commit is contained in:
@@ -99,6 +99,10 @@
|
||||
li.dataset.index = String(idx)
|
||||
// 기본 상태에서는 contenteditable 을 켜지 않는다. 더블클릭 시에만 편집 모드 ON.
|
||||
// 이렇게 해야 어디를 눌러도 드래그가 시작될 수 있다.
|
||||
var aliasCount = Array.isArray(entry.aliases) ? entry.aliases.length : 0
|
||||
var aliasLabel = aliasCount > 0
|
||||
? tt('aliasBtnWithCount', { count: aliasCount })
|
||||
: tt('aliasBtn')
|
||||
li.innerHTML =
|
||||
'<span class="rowNum">' + (idx + 1) + '</span>' +
|
||||
'<img class="rowThumb" src="' + thumbUrl(entry.url) + '" alt="" loading="lazy" draggable="false"/>' +
|
||||
@@ -110,9 +114,13 @@
|
||||
escapeHtml(entry.artist || '') +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<button type="button" class="aliasBtn' + (aliasCount > 0 ? ' hasAliases' : '') + '" data-alias-open="' + idx + '" draggable="false">' +
|
||||
escapeHtml(aliasLabel) +
|
||||
'</button>' +
|
||||
'<span class="rowDur">' + fmtTime(entry.durationSec) + '</span>'
|
||||
attachDraggable(li, 'music', idx)
|
||||
attachInlineEdit(li, idx)
|
||||
attachAliasBtn(li, idx)
|
||||
ol.appendChild(li)
|
||||
})
|
||||
}
|
||||
@@ -402,6 +410,110 @@
|
||||
renderImage()
|
||||
})
|
||||
|
||||
// ── 별칭 모달 ─────────────────────────────────────
|
||||
// 음악 행의 "별칭" 버튼을 누르면 열린다. 헤더의 "← 돌아가기" 버튼 (또는 닫기 동작)이
|
||||
// 호출되면 현재 인풋박스들에 입력된 값을 정규화해 state.music[idx].aliases 에 저장.
|
||||
var aliasModal = document.getElementById('aliasModal')
|
||||
var aliasRowsHost = document.getElementById('alias-rows')
|
||||
var aliasModalTitleEl = document.getElementById('alias-modal-title')
|
||||
var aliasBackBtn = document.getElementById('alias-back')
|
||||
var aliasAddBtn = document.getElementById('alias-add')
|
||||
var aliasEditingIdx = -1
|
||||
|
||||
function attachAliasBtn(li, idx) {
|
||||
var btn = li.querySelector('[data-alias-open]')
|
||||
if (!btn) return
|
||||
// 버튼에서 시작하는 mousedown 은 행 드래그로 전파되지 않도록 차단.
|
||||
btn.addEventListener('mousedown', function (e) { e.stopPropagation() })
|
||||
btn.addEventListener('click', function (e) {
|
||||
e.stopPropagation()
|
||||
openAliasModal(idx)
|
||||
})
|
||||
}
|
||||
|
||||
function openAliasModal(idx) {
|
||||
if (!state.music[idx]) return
|
||||
aliasEditingIdx = idx
|
||||
var entry = state.music[idx]
|
||||
aliasModalTitleEl.textContent = tt('aliasModalTitle', { title: entry.title || tt('titleFallback') })
|
||||
aliasRowsHost.innerHTML = ''
|
||||
var existing = Array.isArray(entry.aliases) ? entry.aliases : []
|
||||
if (existing.length === 0) {
|
||||
// 빈 상태에서도 입력 시작을 쉽게 하려고 첫 줄 하나는 미리 만들어 둔다.
|
||||
appendAliasRow('')
|
||||
} else {
|
||||
existing.forEach(function (a) { appendAliasRow(a) })
|
||||
}
|
||||
aliasModal.hidden = false
|
||||
}
|
||||
|
||||
function appendAliasRow(value) {
|
||||
var row = document.createElement('div')
|
||||
row.className = 'aliasRow'
|
||||
var input = document.createElement('input')
|
||||
input.type = 'text'
|
||||
input.className = 'textInput aliasInput'
|
||||
input.placeholder = tt('aliasPlaceholder')
|
||||
input.value = value || ''
|
||||
var removeBtn = document.createElement('button')
|
||||
removeBtn.type = 'button'
|
||||
removeBtn.className = 'aliasRowRemove'
|
||||
removeBtn.title = tt('aliasRemove')
|
||||
removeBtn.textContent = '−'
|
||||
removeBtn.addEventListener('click', function () { row.remove() })
|
||||
row.appendChild(input)
|
||||
row.appendChild(removeBtn)
|
||||
aliasRowsHost.appendChild(row)
|
||||
return input
|
||||
}
|
||||
|
||||
function readAliasInputs() {
|
||||
var seen = Object.create(null)
|
||||
var out = []
|
||||
var inputs = aliasRowsHost.querySelectorAll('.aliasInput')
|
||||
for (var i = 0; i < inputs.length; i++) {
|
||||
var v = (inputs[i].value || '').trim()
|
||||
if (!v) continue
|
||||
if (seen[v]) continue
|
||||
seen[v] = true
|
||||
out.push(v)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
function closeAliasModalSaving() {
|
||||
if (aliasEditingIdx < 0 || !state.music[aliasEditingIdx]) {
|
||||
aliasModal.hidden = true
|
||||
aliasEditingIdx = -1
|
||||
return
|
||||
}
|
||||
var nextAliases = readAliasInputs()
|
||||
var prev = state.music[aliasEditingIdx].aliases || []
|
||||
var changed = prev.length !== nextAliases.length
|
||||
if (!changed) {
|
||||
for (var i = 0; i < prev.length; i++) {
|
||||
if (prev[i] !== nextAliases[i]) { changed = true; break }
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
state.music[aliasEditingIdx].aliases = nextAliases
|
||||
markDirty()
|
||||
renderMusic()
|
||||
}
|
||||
aliasModal.hidden = true
|
||||
aliasEditingIdx = -1
|
||||
}
|
||||
|
||||
aliasAddBtn.addEventListener('click', function () {
|
||||
var input = appendAliasRow('')
|
||||
input.focus()
|
||||
})
|
||||
aliasBackBtn.addEventListener('click', closeAliasModalSaving)
|
||||
// 모달 바깥 클릭으로 닫혀도 입력값은 보존(저장)되도록 처리.
|
||||
aliasModal.addEventListener('click', function (e) {
|
||||
if (e.target === aliasModal) closeAliasModalSaving()
|
||||
})
|
||||
|
||||
// ── 사진목록: 음악목록 그대로 복사 ─────────────────
|
||||
document.getElementById('image-from-music').addEventListener('click', function () {
|
||||
if (state.music.length === 0) {
|
||||
|
||||
@@ -407,12 +407,42 @@ body.siteBody.centerLayout {
|
||||
.trackList { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 4px; }
|
||||
.trackRow {
|
||||
display: grid;
|
||||
grid-template-columns: 36px 80px 1fr auto;
|
||||
grid-template-columns: 36px 80px 1fr auto auto;
|
||||
gap: 12px; align-items: center;
|
||||
padding: 8px 12px; background: var(--bg-card);
|
||||
border: 1px solid var(--border); border-radius: 8px;
|
||||
cursor: grab; user-select: none;
|
||||
}
|
||||
.aliasBtn {
|
||||
background: var(--bg); border: 1px solid var(--border); color: var(--text);
|
||||
padding: 6px 10px; border-radius: 6px; cursor: pointer; font-size: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.aliasBtn:hover { border-color: var(--accent); }
|
||||
.aliasBtn.hasAliases { border-color: var(--accent); color: var(--accent); }
|
||||
|
||||
/* 별칭 모달 */
|
||||
.aliasModalHeader {
|
||||
display: grid !important;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
.aliasModalHeader h3 { text-align: center; }
|
||||
.aliasModalHeader .ghostLink {
|
||||
background: transparent; border: none; color: var(--accent); cursor: pointer;
|
||||
font-size: 13px; padding: 4px 8px;
|
||||
}
|
||||
.aliasModalHeader .ghostLink:hover { text-decoration: underline; }
|
||||
.aliasRowList { display: flex; flex-direction: column; gap: 8px; }
|
||||
.aliasRow { display: flex; gap: 8px; align-items: center; }
|
||||
.aliasRow .aliasInput { flex: 1; }
|
||||
.aliasRowRemove {
|
||||
background: var(--bg-card); border: 1px solid var(--border); color: var(--danger);
|
||||
width: 32px; height: 32px; border-radius: 6px; cursor: pointer;
|
||||
font-size: 16px; line-height: 1; flex-shrink: 0;
|
||||
}
|
||||
.aliasRowRemove:hover { background: var(--danger); color: #fff; border-color: var(--danger); }
|
||||
.rowNum { color: var(--text-muted); font-size: 14px; text-align: center; }
|
||||
.rowThumb { width: 80px; height: 45px; object-fit: cover; border-radius: 4px; background: #000; }
|
||||
.rowMeta { min-width: 0; }
|
||||
|
||||
Reference in New Issue
Block a user