diff --git a/locales/server/ko-kr.json b/locales/server/ko-kr.json
index f3ee3b8..4c0ad09 100644
--- a/locales/server/ko-kr.json
+++ b/locales/server/ko-kr.json
@@ -78,6 +78,11 @@
"aliasPlaceholder": "별칭 입력",
"aliasRemove": "삭제",
"aliasHint": "정답으로 인정할 다른 표기·번역·약칭을 추가할 수 있습니다.",
+ "descBtn": "설명",
+ "descModalTitle": "설명 - {{title}}",
+ "descBack": "← 돌아가기",
+ "descPlaceholder": "이 곡에 대한 설명을 입력하세요",
+ "descHint": "곡 소개·트리비아 등 자유 메모. 정답 채점이나 데이터팩에는 사용되지 않습니다.",
"metaLoading": "메타데이터 가져오는 중…",
"metaFailedShort": "메타 조회 실패",
"metaFailedTitle": "메타데이터 조회 실패",
diff --git a/public/listEditor.js b/public/listEditor.js
index 09fe40a..6b7ea60 100644
--- a/public/listEditor.js
+++ b/public/listEditor.js
@@ -103,6 +103,7 @@
var aliasLabel = aliasCount > 0
? tt('aliasBtnWithCount', { count: aliasCount })
: tt('aliasBtn')
+ var hasDesc = typeof entry.description === 'string' && entry.description.trim().length > 0
li.innerHTML =
'' + (idx + 1) + '' +
'' +
@@ -114,12 +115,16 @@
escapeHtml(entry.artist || '') +
'' +
'' +
+ '' +
'' +
'' + fmtTime(entry.durationSec) + ''
attachDraggable(li, 'music', idx)
attachInlineEdit(li, idx)
+ attachDescBtn(li, idx)
attachAliasBtn(li, idx)
ol.appendChild(li)
})
@@ -527,6 +532,57 @@
if (e.target === aliasModal) closeAliasModalSaving()
})
+ // ── 설명 모달 (음악) ─────────────────────────────────
+ // 별칭 모달과 같은 패턴: 모달 닫힐 때 textarea 값을 state.music[idx].description 에 저장.
+ var descModal = document.getElementById('descModal')
+ var descTextarea = document.getElementById('desc-textarea')
+ var descModalTitleEl = document.getElementById('desc-modal-title')
+ var descBackBtn = document.getElementById('desc-back')
+ var descEditingIdx = -1
+
+ function attachDescBtn(li, idx) {
+ var btn = li.querySelector('[data-desc-open]')
+ if (!btn) return
+ btn.addEventListener('mousedown', function (e) { e.stopPropagation() })
+ btn.addEventListener('click', function (e) {
+ e.stopPropagation()
+ openDescModal(idx)
+ })
+ }
+
+ function openDescModal(idx) {
+ if (!state.music[idx]) return
+ descEditingIdx = idx
+ var entry = state.music[idx]
+ descModalTitleEl.textContent = tt('descModalTitle', { title: entry.title || tt('titleFallback') })
+ descTextarea.value = typeof entry.description === 'string' ? entry.description : ''
+ descModal.hidden = false
+ setTimeout(function () { descTextarea.focus() }, 0)
+ }
+
+ function closeDescModalSaving() {
+ if (descEditingIdx < 0 || !state.music[descEditingIdx]) {
+ descModal.hidden = true
+ descEditingIdx = -1
+ return
+ }
+ // textarea 값을 그대로 저장하되, 줄바꿈은 보존하고 양끝 공백만 다듬는다.
+ var nextDesc = (descTextarea.value || '').replace(/\r\n/g, '\n').trim()
+ var prev = state.music[descEditingIdx].description || ''
+ if (nextDesc !== prev) {
+ state.music[descEditingIdx].description = nextDesc
+ markDirty()
+ renderMusic()
+ }
+ descModal.hidden = true
+ descEditingIdx = -1
+ }
+
+ descBackBtn.addEventListener('click', closeDescModalSaving)
+ descModal.addEventListener('click', function (e) {
+ if (e.target === descModal) closeDescModalSaving()
+ })
+
// ── 사진목록: 음악목록 그대로 복사 ─────────────────
document.getElementById('image-from-music').addEventListener('click', function () {
if (state.music.length === 0) {
@@ -637,7 +693,7 @@
var entries = result.body.entries || []
if (target === 'music') {
state.music = entries.map(function (e) {
- return { url: e.url, title: e.title || '', artist: e.channel || '', durationSec: e.durationSec || 0 }
+ return { url: e.url, title: e.title || '', artist: e.channel || '', durationSec: e.durationSec || 0, aliases: [], description: '' }
})
renderMusic()
} else {
diff --git a/public/styles.css b/public/styles.css
index 48ff982..352068c 100644
--- a/public/styles.css
+++ b/public/styles.css
@@ -407,19 +407,24 @@ 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 auto;
+ grid-template-columns: 36px 80px 1fr auto 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 {
+.aliasBtn, .descBtn {
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); }
+.aliasBtn:hover, .descBtn:hover { border-color: var(--accent); }
+.aliasBtn.hasAliases, .descBtn.hasDesc { border-color: var(--accent); color: var(--accent); }
+.descTextarea {
+ width: 100%; min-height: 140px; resize: vertical;
+ font-family: inherit; font-size: 13px; line-height: 1.5;
+ padding: 8px 10px;
+}
/* 별칭 모달 */
.aliasModalHeader {
diff --git a/src/shared/store.ts b/src/shared/store.ts
index 39516b6..afeb5f2 100644
--- a/src/shared/store.ts
+++ b/src/shared/store.ts
@@ -287,7 +287,8 @@ export function normalizePackList(input: unknown): PackList {
title: sanitizeStr(entry.title),
artist: sanitizeStr(entry.artist),
durationSec: sanitizeNumber(entry.durationSec),
- aliases: sanitizeAliases(entry.aliases)
+ aliases: sanitizeAliases(entry.aliases),
+ description: sanitizeStr(entry.description)
}))
.filter((entry) => entry.url.length > 0),
images: images
diff --git a/src/shared/types.ts b/src/shared/types.ts
index d8e62c3..854de10 100644
--- a/src/shared/types.ts
+++ b/src/shared/types.ts
@@ -57,6 +57,8 @@ export interface MusicListEntry {
durationSec: number
/** 정답으로 인정할 별칭 목록. 빈 배열이면 정답은 title 뿐. */
aliases: string[]
+ /** 곡 설명 / 트리비아 메모. 정답 채점이나 데이터팩 생성에는 사용되지 않는다. */
+ description: string
}
export interface ImageListEntry {
diff --git a/views/op/listEditor.ejs b/views/op/listEditor.ejs
index e62acae..f7f2d94 100644
--- a/views/op/listEditor.ejs
+++ b/views/op/listEditor.ejs
@@ -124,6 +124,21 @@
+
+
<%= t('listEditor.descHint') %>
+ +