feat(ui): 어트리뷰트 키를 드롭다운 선택으로 변경

ATTR_KEYS 목록 정의 (lang/id/class/href/align/order/required/readonly/
hidden/weight/style + 직접 입력). 이미 추가된 키는 드롭다운에서 제외.
키는 배지 레이블로 표시, 값만 자유 입력. 직접 입력 선택 시 prompt()로
키 이름 수동 입력 지원.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
gm
2026-06-02 23:59:41 +09:00
parent 839a3222ec
commit 65f5d4b422

View File

@@ -112,12 +112,10 @@ input,textarea,button,select{font-family:inherit}
.setting-inp:focus{border-color:var(--border-focus);background:#fff} .setting-inp:focus{border-color:var(--border-focus);background:#fff}
.attrs-hdr{display:flex;align-items:center;gap:5px;margin-bottom:4px} .attrs-hdr{display:flex;align-items:center;gap:5px;margin-bottom:4px}
.attrs-lbl{font-size:11px;color:var(--muted);flex:1} .attrs-lbl{font-size:11px;color:var(--muted);flex:1}
.add-attr-btn{font-size:10px;padding:1px 7px;border-radius:4px;border:1px solid var(--accent);background:var(--accent-bg);color:var(--accent);cursor:pointer;font-weight:600}
.add-attr-btn:hover{background:var(--accent);color:#fff}
.attr-row{display:flex;align-items:center;gap:4px;margin-bottom:3px} .attr-row{display:flex;align-items:center;gap:4px;margin-bottom:3px}
.attr-key{width:80px;font-size:12px;border:1px solid var(--border);border-radius:4px;padding:2px 5px;background:var(--surface-alt);outline:none} .attr-key-lbl{font-size:11px;font-weight:600;color:var(--accent);background:var(--accent-bg);border:1px solid var(--accent);border-radius:4px;padding:2px 7px;white-space:nowrap;max-width:150px;overflow:hidden;text-overflow:ellipsis;flex-shrink:0}
.attr-val{flex:1;font-size:12px;border:1px solid var(--border);border-radius:4px;padding:2px 5px;background:var(--surface-alt);outline:none} .attr-val{flex:1;font-size:12px;border:1px solid var(--border);border-radius:4px;padding:2px 5px;background:var(--surface-alt);outline:none}
.attr-key:focus,.attr-val:focus{border-color:var(--border-focus);background:#fff} .attr-val:focus{border-color:var(--border-focus);background:#fff}
/* ── 미리보기 패널 ────────────────────────── */ /* ── 미리보기 패널 ────────────────────────── */
.preview-panel{flex:1;display:flex;flex-direction:column;overflow:hidden;background:var(--dk)} .preview-panel{flex:1;display:flex;flex-direction:column;overflow:hidden;background:var(--dk)}
@@ -252,6 +250,21 @@ const SETTING_OPTIONS = [
{v:'attributes', lbl:'어트리뷰트'}, {v:'attributes', lbl:'어트리뷰트'},
]; ];
const ATTR_KEYS = [
{v:'lang', lbl:'lang — 언어'},
{v:'id', lbl:'id — 식별자'},
{v:'class', lbl:'class — 분류'},
{v:'href', lbl:'href — 링크'},
{v:'align', lbl:'align — 정렬'},
{v:'order', lbl:'order — 순서'},
{v:'required', lbl:'required — 필수'},
{v:'readonly', lbl:'readonly — 읽기전용'},
{v:'hidden', lbl:'hidden — 숨김'},
{v:'weight', lbl:'weight — 가중치'},
{v:'style', lbl:'style — 스타일'},
{v:'_custom', lbl:'직접 입력...'},
];
const SEP_PRESETS = [ const SEP_PRESETS = [
{v:' ', lbl:'공백'}, {v:' ', lbl:'공백'},
{v:', ', lbl:', '}, {v:', ', lbl:', '},
@@ -431,19 +444,24 @@ function setSettingVal(nid, key, val){
} }
// ── 어트리뷰트 ───────────────────────────────────────── // ── 어트리뷰트 ─────────────────────────────────────────
function addAttr(nid){ function handleAddAttr(nid, key){
const n=nodeMap[nid]; if(!n||!n.settings.attributes) return; if(!key) return;
const k=Object.keys(n.settings.attributes).includes('')? '_'+Date.now() : ''; if(key==='_custom'){
n.settings.attributes[k]=''; refresh(); const k=(prompt('어트리뷰트 키 이름 입력:')??'').trim();
setTimeout(()=>{ if(k) addAttr(nid, k);
const rows=document.querySelectorAll(`[data-nid="${nid}"] .attr-key`); } else {
if(rows.length) rows[rows.length-1].focus(); addAttr(nid, key);
},30); }
} }
function setAttrKey(nid, old, neo){ function addAttr(nid, key){
const n=nodeMap[nid]; if(!n||!n.settings.attributes||old===neo) return; const n=nodeMap[nid]; if(!n||!n.settings.attributes||key in n.settings.attributes) return;
const v=n.settings.attributes[old]; delete n.settings.attributes[old]; n.settings.attributes[key]=''; refresh();
n.settings.attributes[neo]=v; updatePreview(); save(); setTimeout(()=>{
const inp=document.querySelector(`[data-attrnode="${nid}"][data-attrkey] .attr-val`);
// focus the last added attr-val
const all=document.querySelectorAll(`[data-attrnode="${nid}"] .attr-val`);
if(all.length) all[all.length-1].focus();
},40);
} }
function setAttrVal(nid, key, val){ function setAttrVal(nid, key, val){
const n=nodeMap[nid]; if(!n||!n.settings.attributes) return; const n=nodeMap[nid]; if(!n||!n.settings.attributes) return;
@@ -601,20 +619,29 @@ function renderValue(nid, val){
function renderSetting(nid, key, val){ function renderSetting(nid, key, val){
if(key==='attributes'){ if(key==='attributes'){
const attrs=val??{}; const attrs=val??{};
const rows=Object.entries(attrs).map(([k,v])=>` const usedKeys=Object.keys(attrs);
<div class="attr-row"> const availKeys=ATTR_KEYS.filter(o=>o.v==='_custom'||!usedKeys.includes(o.v));
<input class="attr-key" placeholder="키" value="${esc(k)}" const attrKeyOpts=availKeys.map(o=>`<option value="${esc(o.v)}">${esc(o.lbl)}</option>`).join('');
onblur="setAttrKey('${nid}','${esc(k)}',this.value)"> const rows=Object.entries(attrs).map(([k,v])=>{
const def=ATTR_KEYS.find(o=>o.v===k);
const lbl=def?def.lbl:k;
return `
<div class="attr-row" data-attrnode="${nid}" data-attrkey="${esc(k)}">
<span class="attr-key-lbl" title="${esc(k)}">${esc(lbl)}</span>
<input class="attr-val" placeholder="값" value="${esc(String(v??''))}" <input class="attr-val" placeholder="값" value="${esc(String(v??''))}"
oninput="setAttrVal('${nid}','${esc(k)}',this.value)"> oninput="setAttrVal('${nid}','${esc(k)}',this.value)">
<button class="iBtn" onclick="removeAttr('${nid}','${esc(k)}')">✕</button> <button class="iBtn" onclick="removeAttr('${nid}','${esc(k)}')">✕</button>
</div>`).join(''); </div>`;
}).join('');
return ` return `
<div data-nid="${nid}"> <div>
<div class="attrs-hdr"> <div class="attrs-hdr">
<span class="attrs-lbl">어트리뷰트</span> <span class="attrs-lbl">어트리뷰트</span>
<button class="add-attr-btn" onclick="addAttr('${nid}')"> 추가</button> <select class="add-sel" onchange="handleAddAttr('${nid}',this.value);this.selectedIndex=0">
<button class="iBtn" onclick="removeSetting('${nid}','attributes')" style="margin-left:4px">✕</button> <option value=""> 추가</option>
${attrKeyOpts}
</select>
<button class="iBtn" onclick="removeSetting('${nid}','attributes')">✕</button>
</div> </div>
${rows||'<div class="empty-hint" style="padding:2px 0">없음</div>'} ${rows||'<div class="empty-hint" style="padding:2px 0">없음</div>'}
</div>`; </div>`;