feat(ui): 노드 구분자 설정 추가 (다음 노드 / 하위 노드)
각 노드 카드 바디 상단에 구분자 설정 바 추가. - 다음 노드 (nextSep): 이 노드 뒤~다음 형제 노드 사이 구분 - 하위 노드 (childSep): 이 노드 헤더~첫 자식 노드 사이 구분 - 프리셋: 없음 / 빈 줄 / 직접 입력 - toText()에 applySep() 적용, list 뒤 빈 줄은 기존 유지 - 기존 저장 데이터 normalizeState()로 자동 마이그레이션 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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){
|
||||
</div>
|
||||
${!node.hidden?`
|
||||
<div class="node-body">
|
||||
${renderSepBar(node)}
|
||||
<!-- 값 -->
|
||||
<div class="body-sec">
|
||||
<div class="body-sec-hdr">
|
||||
@@ -616,6 +630,43 @@ function renderValue(nid, val){
|
||||
</div>`;
|
||||
}
|
||||
|
||||
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=>`
|
||||
<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 renderSepBar(node){
|
||||
return `
|
||||
<div class="sep-bar">
|
||||
<div class="sep-bar-row">
|
||||
<span class="sep-bar-lbl">다음 노드</span>
|
||||
${renderSepChips(node.id,'nextSep', node.nextSep??'blank')}
|
||||
</div>
|
||||
<div class="sep-bar-row">
|
||||
<span class="sep-bar-lbl">하위 노드</span>
|
||||
${renderSepChips(node.id,'childSep', node.childSep??'blank')}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
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;i<node.values.length;i++){
|
||||
const v=node.values[i];
|
||||
const nextV=node.values[i+1]??null;
|
||||
if(v.type==='list'){
|
||||
const ns=parseNames(v.names); if(!ns.length) continue;
|
||||
const cat=(v.category??'').trim(), pre=cat?`(${cat}) `:'';
|
||||
const sep=v.sep??' ', npl=v.namesPerLine??0;
|
||||
if(sep==='\n') ns.forEach((n,i)=>lines.push((i===0?pre:'')+n));
|
||||
else if(npl>0) for(let i=0;i<ns.length;i+=npl) lines.push((i===0?pre:'')+ns.slice(i,i+npl).join(sep));
|
||||
if(sep==='\n') ns.forEach((n,j)=>lines.push((j===0?pre:'')+n));
|
||||
else if(npl>0) for(let j=0;j<ns.length;j+=npl) lines.push((j===0?pre:'')+ns.slice(j,j+npl).join(sep));
|
||||
else lines.push(pre+ns.join(sep));
|
||||
lines.push(''); hadGroup=true;
|
||||
lines.push(''); // list 뒤 빈 줄 (항상)
|
||||
prevWasList=true;
|
||||
} else if(v.type==='node'){
|
||||
if(!hadGroup) lines.push('');
|
||||
rNode(v.node); hadGroup=false;
|
||||
if(!prevWasList) applySep(node.childSep); // list가 없을 때만 childSep 적용
|
||||
rNode(v.node);
|
||||
if(nextV&&nextV.type==='node') applySep(v.node.nextSep); // 다음도 노드면 nextSep
|
||||
prevWasList=false;
|
||||
} else {
|
||||
if(v.data) lines.push(String(v.data));
|
||||
prevWasList=false;
|
||||
}
|
||||
}
|
||||
}
|
||||
for(const n of S.nodes) rNode(n);
|
||||
|
||||
for(let i=0;i<S.nodes.length;i++){
|
||||
rNode(S.nodes[i]);
|
||||
if(i<S.nodes.length-1) applySep(S.nodes[i].nextSep);
|
||||
}
|
||||
return lines.join('\n').replace(/\n{3,}/g,'\n\n').trimEnd()+'\n';
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user