feat(ui): JSON → 노드 불러오기 탭 추가

불러오기 모달에 텍스트/JSON 탭 구분 추가.
JSON 탭: ROOT→SECTION→LIST/VALUE 구조를 노드 트리로 복원.
- SECTION: mark, content, header/footer/prefix/suffix, attributes 복원
- LIST: LIST_ITEM들을 names로 결합, category 복원
- SECTION 중첩: node-type value로 재귀 복원
- VALUE: data_type/value → scalar val 복원
- nextSep/childSep/nameNewline은 기본값으로 초기화

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
gm
2026-06-03 01:14:52 +09:00
parent 63cb612a6e
commit 702eb918ee

View File

@@ -140,6 +140,12 @@ input,textarea,button,select{font-family:inherit}
.preview-text.dim{color:var(--dt3);font-style:italic}
.j-key{color:#89b4fa}.j-str{color:#a6e3a1}.j-num{color:#fab387}.j-bool{color:#f38ba8}.j-null{color:#f38ba8;font-style:italic}
/* ── 임포트 탭 ────────────────────────────── */
.import-tabs{display:flex;gap:0;margin-bottom:12px;border-bottom:2px solid var(--border)}
.itab{font-size:12px;padding:6px 16px;border:none;background:transparent;color:var(--muted);cursor:pointer;font-weight:500;border-bottom:2px solid transparent;margin-bottom:-2px;transition:all .15s}
.itab.active{color:var(--accent);border-bottom-color:var(--accent);font-weight:700}
.itab:hover:not(.active){color:var(--text)}
/* ── 모달 ─────────────────────────────────── */
.overlay{position:fixed;inset:0;background:rgba(0,0,0,.45);display:none;align-items:center;justify-content:center;z-index:200}
.overlay.open{display:flex}
@@ -206,7 +212,14 @@ input,textarea,button,select{font-family:inherit}
<div class="overlay" id="importModal">
<div class="modal">
<h2>📥 텍스트 불러오기</h2>
<h2>📥 불러오기</h2>
<div class="import-tabs">
<button class="itab active" id="itab-text" onclick="switchImportTab('text')">텍스트</button>
<button class="itab" id="itab-json" onclick="switchImportTab('json')">JSON</button>
</div>
<!-- 텍스트 탭 -->
<div id="import-text-body">
<p>기존 양식 텍스트를 붙여넣으면 편집기로 불러옵니다.</p>
<div class="modal-field">
<label>제목 구분자</label>
@@ -218,9 +231,18 @@ input,textarea,button,select{font-family:inherit}
</div>
<div class="hsep"></div>
<textarea id="importTxt" placeholder="✅예시 양식&#10;&#10;🔹1번 항목&#10;홍길동 홍길순&#10;&#10;🔹2번 항목&#10;🔸소항목&#10;(건강) 홍길찬"></textarea>
</div>
<!-- JSON 탭 -->
<div id="import-json-body" style="display:none">
<p>JSON slim 또는 JSON full 출력을 붙여넣으면 노드로 복원합니다.<br>
<span style="font-size:11px;color:var(--muted)">※ 구분자·줄바꿈 설정은 기본값으로 초기화됩니다.</span></p>
<textarea id="importJsonTxt" placeholder='{"type":"ROOT", "children":[...]}'></textarea>
</div>
<div class="modal-actions">
<button class="mBtn cancel" onclick="closeImport()">취소</button>
<button class="mBtn ok" onclick="doImport()">불러오기</button>
<button class="mBtn ok" onclick="doImportActive()">불러오기</button>
</div>
</div>
</div>
@@ -890,6 +912,16 @@ function copyOutput(){
// ════════════════════════════════════════════════════════
// 불러오기
// ════════════════════════════════════════════════════════
let importTab='text';
function switchImportTab(tab){
importTab=tab;
document.getElementById('import-text-body').style.display=tab==='text'?'':'none';
document.getElementById('import-json-body').style.display=tab==='json'?'':'none';
document.querySelectorAll('.itab').forEach(el=>el.classList.remove('active'));
document.getElementById('itab-'+tab).classList.add('active');
}
function openImport(){
document.getElementById('importTitleMark').value=S.titleMark;
const marks=[]; function col(nodes){ for(const n of nodes){ if(n.mark&&!marks.includes(n.mark)) marks.push(n.mark); for(const v of n.values) if(v.type==='node') col([v.node]); } }
@@ -899,6 +931,55 @@ function openImport(){
}
function closeImport(){ document.getElementById('importModal').classList.remove('open') }
function doImportActive(){
if(importTab==='json') doImportJson(); else doImport();
}
// ── JSON 파서 ──────────────────────────────────────────
function doImportJson(){
const raw=document.getElementById('importJsonTxt').value.trim();
if(!raw) return;
let obj;
try{ obj=JSON.parse(raw); }
catch(e){ alert('JSON 파싱 오류:\n'+e.message); return; }
if(obj.type!=='ROOT'){ alert('최상위 타입이 ROOT가 아닙니다.'); return; }
S.titleMark = obj.attributes?.mark ?? '✅';
S.title = obj.attributes?.title ?? '';
S.nodes = (obj.children??[]).filter(c=>c.type==='SECTION').map(parseJsonSection);
refresh(); closeImport();
document.getElementById('importJsonTxt').value='';
}
function parseJsonSection(sec){
const mark=sec.attributes?.mark??'';
const n=mkNode(mark, sec.content??'');
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.suffix) n.settings.suffix=sec.suffix;
const filtAttrs=Object.entries(sec.attributes??{}).filter(([k])=>k!=='mark'&&k!=='level');
if(filtAttrs.length) n.settings.attributes=Object.fromEntries(filtAttrs);
for(const child of sec.children??[]){
if(child.type==='LIST'){
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)));
} else if(child.type==='VALUE'){
const sv=mkScalarVal(child.data_type??'string');
sv.data=String(child.value??'');
n.values.push(sv);
}
}
return n;
}
function doImport(){
const raw=document.getElementById('importTxt').value.trim(); if(!raw) return;
const titleMark=document.getElementById('importTitleMark').value||'✅';