site: 음악목록 항목별 별칭 편집 기능 추가

- MusicListEntry 에 aliases: string[] 필드 추가, 저장 시 trim·중복 제거.
- 목록 행에 "별칭" 버튼 표시(개수 있으면 강조), 클릭 시 모달 오픈.
- 모달에서 "별칭 추가" → 입력행 생성, "−" 버튼 → 해당 행 삭제,
  좌상단 "← 돌아가기" 또는 오버레이 클릭으로 저장 후 닫기.
This commit is contained in:
2026-05-13 15:57:35 +09:00
parent f9cf373550
commit 2344c4b8d2
6 changed files with 188 additions and 2 deletions

View File

@@ -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) {

View File

@@ -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; }