From 881b3d1f1028264fed9aae60920d4df20eaff45b Mon Sep 17 00:00:00 2001 From: gm Date: Wed, 3 Jun 2026 00:07:31 +0900 Subject: [PATCH] =?UTF-8?q?feat(ui):=20=EB=85=B8=EB=93=9C=20=EA=B5=AC?= =?UTF-8?q?=EB=B6=84=EC=9E=90=20=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?(=EB=8B=A4=EC=9D=8C=20=EB=85=B8=EB=93=9C=20/=20=ED=95=98?= =?UTF-8?q?=EC=9C=84=20=EB=85=B8=EB=93=9C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 각 노드 카드 바디 상단에 구분자 설정 바 추가. - 다음 노드 (nextSep): 이 노드 뒤~다음 형제 노드 사이 구분 - 하위 노드 (childSep): 이 노드 헤더~첫 자식 노드 사이 구분 - 프리셋: 없음 / 빈 줄 / 직접 입력 - toText()에 applySep() 적용, list 뒤 빈 줄은 기존 유지 - 기존 저장 데이터 normalizeState()로 자동 마이그레이션 Co-Authored-By: Claude Sonnet 4.6 --- ui/builder.html | 92 ++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 80 insertions(+), 12 deletions(-) diff --git a/ui/builder.html b/ui/builder.html index 3e5a5bd..ed8f1cb 100644 --- a/ui/builder.html +++ b/ui/builder.html @@ -105,6 +105,11 @@ input,textarea,button,select{font-family:inherit} .npl-inp:disabled{background:var(--surface-alt);color:var(--muted)} .opt-hint{font-size:10px;color:var(--muted)} +/* ── 구분자 설정 바 ───────────────────────── */ +.sep-bar{background:var(--surface-alt);border:1px solid var(--border);border-radius:5px;padding:5px 8px;display:flex;flex-direction:column;gap:4px} +.sep-bar-row{display:flex;align-items:center;gap:5px} +.sep-bar-lbl{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--muted);width:52px;flex-shrink:0} + /* ── 노드 설정 ────────────────────────────── */ .setting-row{display:flex;align-items:center;gap:5px;padding:3px 0} .setting-lbl{font-size:11px;color:var(--muted);width:54px;flex-shrink:0} @@ -278,7 +283,8 @@ const SEP_PRESETS = [ function uid(){ return Math.random().toString(36).slice(2,9) } 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' }; } function mkListVal(names='', category=''){ return { id:uid(), type:'list', names, sep:' ', namesPerLine:0, category }; @@ -319,15 +325,22 @@ function save(){ try{ localStorage.setItem('ctstruct-v4', JSON.stringify(S)) }ca function load(){ try{ const raw=localStorage.getItem('ctstruct-v4'); - if(raw){ S=JSON.parse(raw); return; } - // try migrating old format + if(raw){ S=JSON.parse(raw); normalizeState(S); return; } const old=localStorage.getItem('ctstruct-builder'); if(old){ const o=JSON.parse(old); S=migrateOld(o); save(); return; } }catch(_){} - // default example S=buildExample(); } +function normalizeNode(n){ + if(n.nextSep===undefined) n.nextSep='blank'; + if(n.childSep===undefined) n.childSep='blank'; + for(const v of n.values||[]) if(v.type==='node') normalizeNode(v.node); +} +function normalizeState(s){ + for(const n of s.nodes||[]) normalizeNode(n); +} + function migrateOld(o){ const titleMark = o.titleMark ?? o.cfg?.titleMark ?? '✅'; const oldMarks = o.cfg?.marks ?? []; @@ -511,6 +524,7 @@ function renderNode(node, nested){ ${!node.hidden?`
+ ${renderSepBar(node)}
@@ -616,6 +630,43 @@ function renderValue(nid, val){
`; } +const SEP_CFG = [ + {v:'none', 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=>` + ${p.lbl}`).join(''); + return ` + ${chips} + `; +} + +function renderSepBar(node){ + return ` +
+
+ 다음 노드 + ${renderSepChips(node.id,'nextSep', node.nextSep??'blank')} +
+
+ 하위 노드 + ${renderSepChips(node.id,'childSep', node.childSep??'blank')} +
+
`; +} + function renderSetting(nid, key, val){ if(key==='attributes'){ const attrs=val??{}; @@ -665,27 +716,44 @@ function toText(){ const title=S.title.trim(); if(title){ lines.push(`${S.titleMark}${title}`); lines.push(''); } + function applySep(sep){ + const s=sep??'blank'; + if(s==='none') return; + if(s==='blank'){ lines.push(''); return; } + for(const l of String(s).replace(/\\n/g,'\n').split('\n')) lines.push(l); + } + function rNode(node){ lines.push(`${node.mark??''}${node.content||''}`); - let hadGroup=false; - for(const v of node.values){ + let prevWasList=false; + for(let i=0;ilines.push((i===0?pre:'')+n)); - else if(npl>0) for(let i=0;ilines.push((j===0?pre:'')+n)); + else if(npl>0) for(let j=0;j