refactor(ui): 구조 대폭 간소화

- 자료형: node/string/int/bool/date 5개만 유지, integer→int, boolean→bool
- 리스트 타입 완전 제거 (기존 데이터 → string으로 자동 마이그레이션)
- nextSep/childSep 제거 (노드 간 구분은 항상 줄바꿈)
- header/footer 제거
- prefix = 노드 전체 앞 문자열, suffix = 노드 전체 뒤 문자열로 재정의
- toText(): prefix/suffix 렌더링, list 로직 제거, 구조 단순화
- toJSON(): list/nextSep/childSep/header/footer 제거
- parseJsonSection: 동일하게 단순화, 구버전 LIST → string 호환
- doImport: flush() → string value 생성
- CSS: list 전용 스타일 제거

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
gm
2026-06-03 01:53:50 +09:00
parent 28e52e9ce0
commit 3449df10bc

View File

@@ -81,29 +81,12 @@ input,textarea,button,select{font-family:inherit}
.val-item.val-node{border:none;background:transparent;padding:0} .val-item.val-node{border:none;background:transparent;padding:0}
.val-head{display:flex;align-items:center;gap:5px;padding:4px 6px;background:var(--surface);border-bottom:1px solid var(--border)} .val-head{display:flex;align-items:center;gap:5px;padding:4px 6px;background:var(--surface);border-bottom:1px solid var(--border)}
.val-type-badge{font-size:10px;font-weight:700;color:var(--muted);background:var(--surface-alt);border:1px solid var(--border);border-radius:3px;padding:1px 5px;text-transform:uppercase;letter-spacing:.04em;flex-shrink:0} .val-type-badge{font-size:10px;font-weight:700;color:var(--muted);background:var(--surface-alt);border:1px solid var(--border);border-radius:3px;padding:1px 5px;text-transform:uppercase;letter-spacing:.04em;flex-shrink:0}
.val-cat-inp{flex:1;font-size:12px;border:1px solid var(--border);border-radius:4px;padding:2px 6px;background:var(--surface);outline:none;min-width:0}
.val-cat-inp:focus{border-color:var(--border-focus)}
.val-cat-inp::placeholder{color:var(--muted)}
.val-body{padding:6px} .val-body{padding:6px}
.val-names{width:100%;min-height:42px;font-size:12px;border:1px solid var(--border);border-radius:4px;padding:4px 6px;resize:vertical;background:var(--surface);line-height:1.55;outline:none}
.val-names:focus{border-color:var(--border-focus)}
.val-data-inp{width:100%;font-size:12px;border:1px solid var(--border);border-radius:4px;padding:4px 6px;background:var(--surface);outline:none} .val-data-inp{width:100%;font-size:12px;border:1px solid var(--border);border-radius:4px;padding:4px 6px;background:var(--surface);outline:none}
.val-data-inp:focus{border-color:var(--border-focus)} .val-data-inp:focus{border-color:var(--border-focus)}
.val-data-ta{width:100%;min-height:60px;font-size:12px;border:1px solid var(--border);border-radius:4px;padding:4px 6px;resize:vertical;background:var(--surface);font-family:monospace;line-height:1.5;outline:none} .sep-chip{font-size:11px;padding:2px 8px;border-radius:4px;border:1px solid var(--border);background:var(--surface);color:var(--muted);cursor:pointer;font-weight:500;transition:all .1s;white-space:nowrap}
.val-data-ta:focus{border-color:var(--border-focus)}
.val-opts{display:flex;flex-direction:column;gap:4px;margin-top:5px;padding-top:5px;border-top:1px dashed var(--border)}
.opt-row{display:flex;align-items:center;gap:5px}
.opt-lbl{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;color:var(--muted);width:36px;flex-shrink:0}
.sep-chips{display:flex;gap:3px;flex-wrap:wrap}
.sep-chip{font-size:11px;padding:2px 6px;border-radius:4px;border:1px solid var(--border);background:var(--surface);color:var(--muted);cursor:pointer;font-weight:500;transition:all .1s;white-space:nowrap}
.sep-chip:hover{border-color:var(--accent);color:var(--accent)} .sep-chip:hover{border-color:var(--accent);color:var(--accent)}
.sep-chip.on{background:var(--accent);color:#fff;border-color:var(--accent)} .sep-chip.on{background:var(--accent);color:#fff;border-color:var(--accent)}
.sep-cust{width:48px;font-size:11px;border:1px solid var(--border);border-radius:4px;padding:2px 4px;background:var(--surface);text-align:center;outline:none}
.sep-cust:focus{border-color:var(--border-focus)}
.npl-inp{width:44px;font-size:12px;border:1px solid var(--border);border-radius:4px;padding:2px 4px;text-align:center;background:var(--surface);outline:none}
.npl-inp:focus{border-color:var(--border-focus)}
.npl-inp:disabled{background:var(--surface-alt);color:var(--muted)}
.opt-hint{font-size:10px;color:var(--muted)}
/* ── 구분자/이름줄바꿈 설정 (노드 설정 내부) ─ */ /* ── 구분자/이름줄바꿈 설정 (노드 설정 내부) ─ */
.struct-sep{display:flex;flex-direction:column;gap:4px;padding-bottom:6px;margin-bottom:6px;border-bottom:1px dashed var(--border)} .struct-sep{display:flex;flex-direction:column;gap:4px;padding-bottom:6px;margin-bottom:6px;border-bottom:1px dashed var(--border)}
@@ -254,25 +237,14 @@ input,textarea,button,select{font-family:inherit}
const VALUE_TYPES = [ const VALUE_TYPES = [
{v:'node', lbl:'노드 (Node)'}, {v:'node', lbl:'노드 (Node)'},
{v:'string', lbl:'문자열 (String)'}, {v:'string', lbl:'문자열 (String)'},
{v:'integer', lbl:'정수 (Integer)'}, {v:'int', lbl:'정수 (Int)'},
{v:'float', lbl:'실수 (Float)'}, {v:'bool', lbl:'불리언 (Bool)'},
{v:'boolean', lbl:'불리언 (Boolean)'},
{v:'null', lbl:'Null'},
{v:'date', lbl:'날짜 (Date)'}, {v:'date', lbl:'날짜 (Date)'},
{v:'datetime', lbl:'날짜+시간 (Datetime)'},
{v:'time', lbl:'시간 (Time)'},
{v:'markdown', lbl:'Markdown'},
{v:'html', lbl:'HTML'},
{v:'json', lbl:'JSON'},
{v:'code', lbl:'코드 (Code)'},
{v:'custom', lbl:'사용자 정의 (Custom)'},
]; ];
const SETTING_OPTIONS = [ const SETTING_OPTIONS = [
{v:'header', lbl:'머리글 (header)'}, {v:'prefix', lbl:'앞 문자열 (prefix)'},
{v:'footer', lbl:'꼬리글 (footer)'}, {v:'suffix', lbl:'뒤 문자열 (suffix)'},
{v:'prefix', lbl:'접두사 (prefix)'},
{v:'suffix', lbl:'접미사 (suffix)'},
{v:'attributes', lbl:'어트리뷰트'}, {v:'attributes', lbl:'어트리뷰트'},
]; ];
@@ -291,13 +263,6 @@ const ATTR_KEYS = [
{v:'_custom', lbl:'직접 입력...'}, {v:'_custom', lbl:'직접 입력...'},
]; ];
const SEP_PRESETS = [
{v:' ', lbl:'공백'},
{v:', ', lbl:', '},
{v:' / ', lbl:'/'},
{v:'\n', lbl:'↵'},
];
// ════════════════════════════════════════════════════════ // ════════════════════════════════════════════════════════
// 팩토리 // 팩토리
// ════════════════════════════════════════════════════════ // ════════════════════════════════════════════════════════
@@ -305,10 +270,7 @@ function uid(){ return Math.random().toString(36).slice(2,9) }
function mkNode(mark='', content=''){ function mkNode(mark='', content=''){
return { id:uid(), mark, content, hidden:false, values:[], settings:{}, return { id:uid(), mark, content, hidden:false, values:[], settings:{},
nextSep:'blank', childSep:'blank', nameNewline:true }; nameNewline:true };
}
function mkListVal(names='', category=''){
return { id:uid(), type:'list', names, sep:' ', namesPerLine:0, category };
} }
function mkNodeVal(childNode){ function mkNodeVal(childNode){
return { id:uid(), type:'node', node: childNode ?? mkNode() }; return { id:uid(), type:'node', node: childNode ?? mkNode() };
@@ -353,11 +315,20 @@ function load(){
S=buildExample(); S=buildExample();
} }
const VALID_TYPES=new Set(['node','string','int','bool','date']);
function normalizeNode(n){ function normalizeNode(n){
if(n.nextSep===undefined) n.nextSep='blank';
if(n.childSep===undefined) n.childSep='blank';
if(n.nameNewline===undefined) n.nameNewline=true; if(n.nameNewline===undefined) n.nameNewline=true;
for(const v of n.values||[]) if(v.type==='node') normalizeNode(v.node); // list → string, 지원하지 않는 타입 → string
n.values=(n.values||[]).map(v=>{
if(v.type==='list'){
const sv=mkScalarVal('string'); sv.data=(v.names??'').trim(); return sv;
}
if(v.type==='node'){ normalizeNode(v.node); return v; }
if(v.type==='integer') v.type='int';
if(v.type==='boolean') v.type='bool';
if(!VALID_TYPES.has(v.type)) v.type='string';
return v;
});
} }
function normalizeState(s){ function normalizeState(s){
for(const n of s.nodes||[]) normalizeNode(n); for(const n of s.nodes||[]) normalizeNode(n);
@@ -376,35 +347,28 @@ function migrateOld(o){
function migSec(s, oldMarks){ function migSec(s, oldMarks){
const mark = s.mark ?? oldMarks[(s.lv??1)-1] ?? ''; const mark = s.mark ?? oldMarks[(s.lv??1)-1] ?? '';
const n = mkNode(mark, s.content ?? s.name ?? ''); const n = mkNode(mark, s.content ?? s.name ?? '');
n.hidden = false; // groups → string values (names 합치기)
// groups → list values
for(const g of s.groups??[]){ for(const g of s.groups??[]){
const lv = mkListVal(g.names??'', g.attributes?.category??''); const names=(g.names??'').trim(); if(!names) continue;
lv.sep = g.sep??' '; lv.namesPerLine = g.namesPerLine??0; const sv=mkScalarVal('string'); sv.data=names; n.values.push(sv);
n.values.push(lv);
} }
// children → node values
for(const c of s.children??[]) n.values.push(mkNodeVal(migSec(c, oldMarks))); for(const c of s.children??[]) n.values.push(mkNodeVal(migSec(c, oldMarks)));
// scalar value
if(s.value!=null && s.data_type && s.data_type!=='unset'){ if(s.value!=null && s.data_type && s.data_type!=='unset'){
const sv=mkScalarVal(s.data_type); sv.data=String(s.value??''); n.values.push(sv); const sv=mkScalarVal('string'); sv.data=String(s.value??''); n.values.push(sv);
} }
// settings
if(s.header) n.settings.header=s.header;
if(s.footer) n.settings.footer=s.footer;
if(s.prefix) n.settings.prefix=s.prefix; if(s.prefix) n.settings.prefix=s.prefix;
if(s.suffix) n.settings.suffix=s.suffix; if(s.suffix) n.settings.suffix=s.suffix;
const filtAttrs=Object.entries(s.attributes??{}).filter(([k])=>k!=='level'&&k!=='category'); const filtAttrs=Object.entries(s.attributes??{}).filter(([k])=>k!=='level'&&k!=='category'&&k!=='mark');
if(filtAttrs.length) n.settings.attributes=Object.fromEntries(filtAttrs); if(filtAttrs.length) n.settings.attributes=Object.fromEntries(filtAttrs);
return n; return n;
} }
function buildExample(){ function buildExample(){
const n1=mkNode('🔹','1번 항목'); const n1=mkNode('🔹','1번 항목');
n1.values.push(mkListVal('홍길동 홍길순 홍길영 홍길준\n홍길호 홍길윤','')); const sv1=mkScalarVal('string'); sv1.data='홍길동 홍길순 홍길영 홍길준'; n1.values.push(sv1);
const n2=mkNode('🔹','2번 항목'); const n2=mkNode('🔹','2번 항목');
const c1=mkNode('🔸','2-1 항목'); c1.values.push(mkListVal('홍길찬 홍길만','건강')); const c1=mkNode('🔸','2-1 항목'); const sv2=mkScalarVal('string'); sv2.data='홍길찬 홍길만'; c1.values.push(sv2);
const c2=mkNode('🔸','2-2 항목'); c2.values.push(mkListVal('홍길주','직장')); const c2=mkNode('🔸','2-2 항목'); const sv3=mkScalarVal('string'); sv3.data='홍길주'; c2.values.push(sv3);
n2.values.push(mkNodeVal(c1)); n2.values.push(mkNodeVal(c2)); n2.values.push(mkNodeVal(c1)); n2.values.push(mkNodeVal(c2));
return {titleMark:'✅', title:'예시 양식', nodes:[n1,n2]}; return {titleMark:'✅', title:'예시 양식', nodes:[n1,n2]};
} }
@@ -440,12 +404,8 @@ function toggleHidden(nid){
function addValue(nid, type){ function addValue(nid, type){
if(!type) return; if(!type) return;
const n=nodeMap[nid]; if(!n) return; const n=nodeMap[nid]; if(!n) return;
let v; const v = type==='node' ? mkNodeVal() : mkScalarVal(type);
if(type==='list') v=mkListVal();
else if(type==='node') v=mkNodeVal();
else v=mkScalarVal(type);
n.values.push(v); refresh(); n.values.push(v); refresh();
if(type==='list') focusNew('.val-names');
} }
function removeValue(vid){ function removeValue(vid){
const e=valueMap[vid]; if(!e) return; const e=valueMap[vid]; if(!e) return;
@@ -590,47 +550,7 @@ function renderValue(nid, val){
return `<div class="val-item val-node" data-vid="${val.id}">${renderNode(val.node,true)}</div>`; return `<div class="val-item val-node" data-vid="${val.id}">${renderNode(val.node,true)}</div>`;
} }
if(val.type==='list'){
const sep=val.sep??' ', npl=val.namesPerLine??0, isNl=sep==='\n';
const chips=SEP_PRESETS.map(p=>`
<span class="sep-chip${sep===p.v?' on':''}"
onclick="valueMap['${val.id}'].val.sep=${JSON.stringify(p.v)};refresh()"
>${p.lbl}</span>`).join('');
const customVal=SEP_PRESETS.find(p=>p.v===sep)?'':esc(sep);
return `
<div class="val-item" data-vid="${val.id}">
<div class="val-head">
<span class="val-type-badge">LIST</span>
<input class="val-cat-inp" placeholder="카테고리 (선택)"
value="${esc(val.category??'')}"
oninput="valueMap['${val.id}'].val.category=this.value;updatePreview();save()">
${mvDel}
</div>
<div class="val-body">
<textarea class="val-names" placeholder="이름 입력 (공백·줄바꿈·쉼표로 구분)"
oninput="valueMap['${val.id}'].val.names=this.value;updatePreview();save()"
>${esc(val.names)}</textarea>
<div class="val-opts">
<div class="opt-row">
<span class="opt-lbl">구분</span>
<div class="sep-chips">${chips}</div>
<input class="sep-cust" placeholder="직접" value="${customVal}"
oninput="valueMap['${val.id}'].val.sep=this.value||' ';updatePreview();save()">
</div>
<div class="opt-row">
<span class="opt-lbl">줄당</span>
<input class="npl-inp" type="number" min="0" value="${npl}" ${isNl?'disabled':''}
oninput="valueMap['${val.id}'].val.namesPerLine=Math.max(0,+this.value||0);updatePreview();save()">
<span class="opt-hint">${isNl?'(줄바꿈 모드)':'개 (0=무제한)'}</span>
</div>
</div>
</div>
</div>`;
}
// 스칼라 타입
const typeLbl=VALUE_TYPES.find(t=>t.v===val.type)?.lbl??val.type.toUpperCase(); const typeLbl=VALUE_TYPES.find(t=>t.v===val.type)?.lbl??val.type.toUpperCase();
const isMulti=['markdown','html','json','code'].includes(val.type);
return ` return `
<div class="val-item" data-vid="${val.id}"> <div class="val-item" data-vid="${val.id}">
<div class="val-head"> <div class="val-head">
@@ -639,60 +559,22 @@ function renderValue(nid, val){
${mvDel} ${mvDel}
</div> </div>
<div class="val-body"> <div class="val-body">
${isMulti <input class="val-data-inp" placeholder="${esc(typeLbl)} 입력"
?`<textarea class="val-data-ta" placeholder="${esc(typeLbl)} 입력"
oninput="valueMap['${val.id}'].val.data=this.value;updatePreview();save()"
>${esc(val.data??'')}</textarea>`
:`<input class="val-data-inp" placeholder="${esc(typeLbl)} 입력"
value="${esc(val.data??'')}" value="${esc(val.data??'')}"
oninput="valueMap['${val.id}'].val.data=this.value;updatePreview();save()">` oninput="valueMap['${val.id}'].val.data=this.value;updatePreview();save()">
}
</div> </div>
</div>`; </div>`;
} }
const SEP_CFG = [
{v:'none', lbl:'없음'},
{v:'newline', lbl:'줄바꿈'},
{v:'blank', lbl:'빈 줄'},
];
function setSep(nid, field, val){
const n=nodeMap[nid]; if(!n) return;
n[field]=val; refresh();
}
function renderSepChips(nid, field, current){
const isCustom = !SEP_CFG.find(p=>p.v===current);
const chips = SEP_CFG.map(p=>`
<span class="sep-chip${current===p.v?' on':''}"
onclick="setSep('${nid}','${field}','${p.v}')">${p.lbl}</span>`).join('');
return `
${chips}
<input class="sep-cust" placeholder="직접" title="직접 입력"
value="${isCustom?esc(current):''}"
oninput="setSep('${nid}','${field}',this.value||'none')"
style="width:56px">`;
}
function setNameNL(nid, val){ function setNameNL(nid, val){
const n=nodeMap[nid]; if(!n) return; const n=nodeMap[nid]; if(!n) return;
n.nameNewline=val; refresh(); n.nameNewline=val; refresh();
} }
function renderStructSettings(node){ function renderStructSettings(node){
const nid=node.id; const nid=node.id, nl=node.nameNewline??true;
const nl=node.nameNewline??true;
return ` return `
<div class="struct-sep"> <div class="struct-sep">
<div class="sep-bar-row">
<span class="sep-bar-lbl">다음 노드</span>
${renderSepChips(nid,'nextSep', node.nextSep??'blank')}
</div>
<div class="sep-bar-row">
<span class="sep-bar-lbl">하위 노드</span>
${renderSepChips(nid,'childSep', node.childSep??'blank')}
</div>
<div class="sep-bar-row"> <div class="sep-bar-row">
<span class="sep-bar-lbl">이름 뒤</span> <span class="sep-bar-lbl">이름 뒤</span>
<span class="sep-chip${nl?' on':''}" onclick="setNameNL('${nid}',true)">줄바꿈</span> <span class="sep-chip${nl?' on':''}" onclick="setNameNL('${nid}',true)">줄바꿈</span>
@@ -746,65 +628,25 @@ function renderSetting(nid, key, val){
function parseNames(raw){ return (raw??'').trim().split(/[\s,\n]+/).filter(Boolean) } function parseNames(raw){ return (raw??'').trim().split(/[\s,\n]+/).filter(Boolean) }
function toText(){ function toText(){
// 구분자 값 → 실제 문자열
function ss(sep){
const s=sep??'blank';
if(s==='none') return '';
if(s==='newline') return '\n';
if(s==='blank') return '\n\n';
return String(s); // 직접 입력값 (예: '/', ', ')
}
// 노드 하나를 문자열로 렌더링
function rNode(node){ function rNode(node){
let t=`${node.mark??''}${node.content||''}`; const pre=node.settings.prefix??'';
const nl=(node.nameNewline??true)?'\n':''; // 이름 뒤 줄바꿈 여부 const suf=node.settings.suffix??'';
let pendingSep=null; const nl=(node.nameNewline??true)?'\n':'';
let prevWasList=false; let t=pre+`${node.mark??''}${node.content||''}`;
let firstContent=true; // 첫 번째 list/scalar 여부 (nameNewline 적용 대상) let first=true;
for(const v of node.values){
for(let i=0;i<node.values.length;i++){ if(v.type==='node'){
const v=node.values[i]; t+='\n'+rNode(v.node); first=false;
const nextV=node.values[i+1]??null; } else if(v.data!==undefined&&v.data!==null&&String(v.data).length){
t+=(first?nl:'\n')+String(v.data); first=false;
if(v.type==='list'){
const ns=parseNames(v.names); if(!ns.length) continue;
const cat=(v.category??'').trim(), pre=cat?`(${cat}) `:'';
const lsep=v.sep??' ', npl=v.namesPerLine??0;
let lt;
if(lsep==='\n') lt=ns.map((n,j)=>(j===0?pre:'')+n).join('\n');
else if(npl>0) lt=Array.from({length:Math.ceil(ns.length/npl)},(_,j)=>(j===0?pre:'')+ns.slice(j*npl,(j+1)*npl).join(lsep)).join('\n');
else lt=pre+ns.join(lsep);
const pre2=firstContent?nl:'\n'; // 첫 list만 nameNewline 적용
t+=pre2+lt+'\n';
prevWasList=true; pendingSep=null; firstContent=false;
} else if(v.type==='node'){
if(!prevWasList){
t+=(pendingSep!==null) ? pendingSep : ss(node.childSep??'blank');
}
t+=rNode(v.node);
prevWasList=false; firstContent=false;
pendingSep=(nextV&&nextV.type==='node') ? ss(v.node.nextSep??'blank') : null;
} else {
if(v.data){
const pre2=firstContent?nl:'\n';
t+=pre2+String(v.data);
firstContent=false;
}
prevWasList=false; pendingSep=null;
} }
} }
t+=suf;
return t; return t;
} }
const title=S.title.trim(); const title=S.title.trim();
let out=title?`${S.titleMark}${title}\n\n`:''; let out=title?`${S.titleMark}${title}\n`:'';
for(let i=0;i<S.nodes.length;i++){ out+=S.nodes.map(rNode).join('\n');
out+=rNode(S.nodes[i]);
if(i<S.nodes.length-1) out+=ss(S.nodes[i].nextSep??'blank');
}
return out.replace(/\n{3,}/g,'\n\n').trimEnd()+'\n'; return out.replace(/\n{3,}/g,'\n\n').trimEnd()+'\n';
} }
@@ -836,25 +678,15 @@ function toJSON(slim=false){
function serNode(node){ function serNode(node){
depth++; depth++;
const children=node.values.map(v=>{ const children=node.values.map(v=>{
if(v.type==='list'){
const ns=parseNames(v.names);
return buildNode({id:v.id,type:'LIST',
attributes:v.category?{category:v.category}:{},
children:ns.map(n=>buildNode({id:uid(),type:'LIST_ITEM',content:n}))});
}
if(v.type==='node') return serNode(v.node); if(v.type==='node') return serNode(v.node);
return buildNode({id:v.id,type:'VALUE',data_type:v.type,value:v.data??null}); return buildNode({id:v.id,type:'VALUE',data_type:v.type,value:v.data??null});
}); });
const attrs={mark:node.mark,level:depth,...(node.settings.attributes??{})}; const attrs={mark:node.mark,level:depth,...(node.settings.attributes??{})};
const r=buildNode({ const r=buildNode({
id:node.id,type:'SECTION',content:node.content, id:node.id,type:'SECTION',content:node.content,
header:node.settings.header??null,
footer:node.settings.footer??null,
prefix:node.settings.prefix??null, prefix:node.settings.prefix??null,
suffix:node.settings.suffix??null, suffix:node.settings.suffix??null,
attributes:attrs,children, attributes:attrs,children,
nextSep:node.nextSep??'blank',
childSep:node.childSep??'blank',
nameNewline:node.nameNewline??true, nameNewline:node.nameNewline??true,
}); });
depth--; depth--;
@@ -959,29 +791,22 @@ function doImportJson(){
function parseJsonSection(sec){ function parseJsonSection(sec){
const mark=sec.attributes?.mark??''; const mark=sec.attributes?.mark??'';
const n=mkNode(mark, sec.content??''); const n=mkNode(mark, sec.content??'');
if(sec.nextSep!==undefined) n.nextSep=sec.nextSep;
if(sec.childSep!==undefined) n.childSep=sec.childSep;
if(sec.nameNewline!==undefined) n.nameNewline=sec.nameNewline; if(sec.nameNewline!==undefined) n.nameNewline=sec.nameNewline;
if(sec.header) n.settings.header=sec.header;
if(sec.footer) n.settings.footer=sec.footer;
if(sec.prefix) n.settings.prefix=sec.prefix; if(sec.prefix) n.settings.prefix=sec.prefix;
if(sec.suffix) n.settings.suffix=sec.suffix; if(sec.suffix) n.settings.suffix=sec.suffix;
const filtAttrs=Object.entries(sec.attributes??{}).filter(([k])=>k!=='mark'&&k!=='level'); const filtAttrs=Object.entries(sec.attributes??{}).filter(([k])=>k!=='mark'&&k!=='level');
if(filtAttrs.length) n.settings.attributes=Object.fromEntries(filtAttrs); if(filtAttrs.length) n.settings.attributes=Object.fromEntries(filtAttrs);
for(const child of sec.children??[]){ for(const child of sec.children??[]){
if(child.type==='LIST'){ if(child.type==='SECTION'){
const names=(child.children??[]).filter(c=>c.type==='LIST_ITEM').map(c=>c.content??'').join(' ');
n.values.push(mkListVal(names, child.attributes?.category??''));
} else if(child.type==='SECTION'){
n.values.push(mkNodeVal(parseJsonSection(child))); n.values.push(mkNodeVal(parseJsonSection(child)));
} else if(child.type==='VALUE'){ } else if(child.type==='VALUE'){
const sv=mkScalarVal(child.data_type??'string'); const sv=mkScalarVal(child.data_type??'string');
sv.data=String(child.value??''); sv.data=String(child.value??'');
n.values.push(sv); n.values.push(sv);
} else if(child.type==='LIST'){
// 구버전 호환: LIST → string value
const names=(child.children??[]).filter(c=>c.type==='LIST_ITEM').map(c=>c.content??'').join(' ');
const sv=mkScalarVal('string'); sv.data=names; n.values.push(sv);
} }
} }
return n; return n;
@@ -998,8 +823,8 @@ function doImport(){
function flush(){ function flush(){
if(!pend||!pend.names.length){ pend=null; return; } if(!pend||!pend.names.length){ pend=null; return; }
const parent=pend.parentId?findNodeInList(nodes,pend.parentId):null; const parent=pend.parentId?findNodeInList(nodes,pend.parentId):null;
const lv=mkListVal(pend.names.join(' '),pend.cat||''); const sv=mkScalarVal('string'); sv.data=pend.names.join(' ');
if(parent) parent.values.push(lv); pend=null; if(parent) parent.values.push(sv); pend=null;
} }
function findNodeInList(list,id){ for(const n of list){ if(n.id===id) return n; for(const v of n.values) if(v.type==='node'){ const f=findNodeInList([v.node],id); if(f) return f; } } return null; } function findNodeInList(list,id){ for(const n of list){ if(n.id===id) return n; for(const v of n.values) if(v.type==='node'){ const f=findNodeInList([v.node],id); if(f) return f; } } return null; }
function topId(){ return stack.length?stack[stack.length-1].id:null } function topId(){ return stack.length?stack[stack.length-1].id:null }