ui: add collect/fixed role distinction to nodes (v0.6)
- Builder: 역할 토글(🔒고정/📥수집) in detail panel struct settings - Canvas: collect 노드 헤더 파란 배경 + 📥 배지 - Tab2 template view: selective mode일 때 수집/고정 배지 표시 - Tab2 merge: collect 섹션만 토큰 수집, 고정 섹션은 템플릿 값 그대로 출력 - Backwards compatible: collect 섹션이 없으면 기존처럼 전체 수집 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -237,6 +237,14 @@ input,textarea,button,select{font-family:inherit}
|
|||||||
.filler-textarea{flex:1;width:100%;font-size:13px;font-family:inherit;border:1.5px solid var(--border);border-radius:var(--r);padding:10px 12px;outline:none;resize:none;line-height:1.75;background:var(--surface);color:var(--text)}
|
.filler-textarea{flex:1;width:100%;font-size:13px;font-family:inherit;border:1.5px solid var(--border);border-radius:var(--r);padding:10px 12px;outline:none;resize:none;line-height:1.75;background:var(--surface);color:var(--text)}
|
||||||
.filler-textarea:focus{border-color:var(--border-focus)}
|
.filler-textarea:focus{border-color:var(--border-focus)}
|
||||||
.filler-textarea::placeholder{color:var(--muted)}
|
.filler-textarea::placeholder{color:var(--muted)}
|
||||||
|
|
||||||
|
/* ── 수집/고정 구분 ──────────────────────────── */
|
||||||
|
.cnode.collect .cnode-head{background:#e8eeff}
|
||||||
|
.cnode.collect.selected .cnode-head{background:var(--accent-bg)}
|
||||||
|
.ftpl-sec.is-collect{border-color:var(--accent);background:var(--accent-bg)}
|
||||||
|
.ftpl-role{font-size:9px;border-radius:3px;padding:1px 5px;font-weight:700;margin-left:2px;letter-spacing:.03em}
|
||||||
|
.ftpl-role.collect{background:var(--accent-bg);color:var(--accent)}
|
||||||
|
.ftpl-role.fixed{background:var(--surface-alt);color:var(--muted);border:1px solid var(--border)}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -244,7 +252,7 @@ input,textarea,button,select{font-family:inherit}
|
|||||||
<header class="app-header">
|
<header class="app-header">
|
||||||
<span style="font-size:18px">🏗️</span>
|
<span style="font-size:18px">🏗️</span>
|
||||||
<h1>CTStruct</h1>
|
<h1>CTStruct</h1>
|
||||||
<span class="badge">v0.5</span>
|
<span class="badge">v0.6</span>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<nav class="main-nav">
|
<nav class="main-nav">
|
||||||
@@ -716,13 +724,15 @@ function renderCanvas(){
|
|||||||
function renderCNode(node){
|
function renderCNode(node){
|
||||||
const p=pos.get(node.id); if(!p) return;
|
const p=pos.get(node.id); if(!p) return;
|
||||||
const sel=node.id===selectedNodeId;
|
const sel=node.id===selectedNodeId;
|
||||||
|
const col=node.settings?.attributes?.collect==='true';
|
||||||
const vals=node.values.filter(v=>v.type!=='node').length;
|
const vals=node.values.filter(v=>v.type!=='node').length;
|
||||||
const kids=node.values.filter(v=>v.type==='node').length;
|
const kids=node.values.filter(v=>v.type==='node').length;
|
||||||
const hint=[vals?`값 ${vals}`:'',kids?`하위 ${kids}`:''].filter(Boolean).join(' · ')||'비어있음';
|
const hint=[vals?`값 ${vals}`:'',kids?`하위 ${kids}`:''].filter(Boolean).join(' · ')||'비어있음';
|
||||||
cards+=`<div class="cnode${sel?' selected':''}" style="left:${p.x}px;top:${p.y}px" onclick="selectNode('${node.id}')">
|
cards+=`<div class="cnode${sel?' selected':''}${col?' collect':''}" style="left:${p.x}px;top:${p.y}px" onclick="selectNode('${node.id}')">
|
||||||
<div class="cnode-head">
|
<div class="cnode-head">
|
||||||
<span class="cnode-mark">${esc(node.mark||'·')}</span>
|
<span class="cnode-mark">${esc(node.mark||'·')}</span>
|
||||||
<span class="cnode-name" title="${esc(node.content||'')}">${esc(node.content||'이름 없음')}</span>
|
<span class="cnode-name" title="${esc(node.content||'')}">${esc(node.content||'이름 없음')}</span>
|
||||||
|
${col?'<span style="font-size:9px;color:var(--accent);font-weight:700;flex-shrink:0;line-height:1">📥</span>':''}
|
||||||
</div>
|
</div>
|
||||||
<div class="cnode-foot">
|
<div class="cnode-foot">
|
||||||
<span class="cnode-vals">${hint}</span>
|
<span class="cnode-vals">${hint}</span>
|
||||||
@@ -845,8 +855,21 @@ function setNameNL(nid, val){
|
|||||||
n.nameNewline=val; refresh();
|
n.nameNewline=val; refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setCollect(nid, on){
|
||||||
|
const n=nodeMap[nid]; if(!n) return;
|
||||||
|
if(on){
|
||||||
|
if(!n.settings.attributes) n.settings.attributes={};
|
||||||
|
n.settings.attributes.collect='true';
|
||||||
|
} else {
|
||||||
|
if(n.settings.attributes){ delete n.settings.attributes.collect; }
|
||||||
|
if(n.settings.attributes && !Object.keys(n.settings.attributes).length) delete n.settings.attributes;
|
||||||
|
}
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
function renderStructSettings(node){
|
function renderStructSettings(node){
|
||||||
const nid=node.id, nl=node.nameNewline??true;
|
const nid=node.id, nl=node.nameNewline??true;
|
||||||
|
const col=node.settings?.attributes?.collect==='true';
|
||||||
return `
|
return `
|
||||||
<div class="struct-sep">
|
<div class="struct-sep">
|
||||||
<div class="sep-bar-row">
|
<div class="sep-bar-row">
|
||||||
@@ -854,6 +877,11 @@ function renderStructSettings(node){
|
|||||||
<span class="sep-chip${nl?' on':''}" onclick="setNameNL('${nid}',true)">줄바꿈</span>
|
<span class="sep-chip${nl?' on':''}" onclick="setNameNL('${nid}',true)">줄바꿈</span>
|
||||||
<span class="sep-chip${!nl?' on':''}" onclick="setNameNL('${nid}',false)">없음</span>
|
<span class="sep-chip${!nl?' on':''}" onclick="setNameNL('${nid}',false)">없음</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="sep-bar-row">
|
||||||
|
<span class="sep-bar-lbl">역할</span>
|
||||||
|
<span class="sep-chip${!col?' on':''}" onclick="setCollect('${nid}',false)" title="값이 항상 고정 출력됩니다">🔒 고정</span>
|
||||||
|
<span class="sep-chip${col?' on':''}" onclick="setCollect('${nid}',true)" title="합치기 시 값이 수집됩니다">📥 수집</span>
|
||||||
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1175,15 +1203,26 @@ function clearFiller(){
|
|||||||
updateFillerPreview();
|
updateFillerPreview();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 수집 섹션 키 추출 ──────────────────────────────────
|
||||||
|
function getCollectKeys(root){
|
||||||
|
const keys=new Set();
|
||||||
|
function walk(n){ for(const c of n.children??[]){ if(c.type==='SECTION'){ if(c.attributes?.collect==='true'){ keys.add((c.attributes?.mark??'')+(c.content??'')); } walk(c); } } }
|
||||||
|
walk(root);
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
|
||||||
// ── 템플릿 구조 표시 ───────────────────────────────────
|
// ── 템플릿 구조 표시 ───────────────────────────────────
|
||||||
function renderFillerTemplateView(){
|
function renderFillerTemplateView(){
|
||||||
const el=document.getElementById('ftplView');
|
const el=document.getElementById('ftplView');
|
||||||
if(!FT){ el.innerHTML='<span class="ftpl-empty">← 템플릿을 먼저 불러오세요</span>'; return; }
|
if(!FT){ el.innerHTML='<span class="ftpl-empty">← 템플릿을 먼저 불러오세요</span>'; return; }
|
||||||
|
const selective=getCollectKeys(FT).size>0;
|
||||||
function walkSec(sec, depth){
|
function walkSec(sec, depth){
|
||||||
const m=sec.attributes?.mark??'';
|
const m=sec.attributes?.mark??'';
|
||||||
const n=sec.content??'';
|
const n=sec.content??'';
|
||||||
const pl=sec.attributes?.per_line;
|
const pl=sec.attributes?.per_line;
|
||||||
let html=`<span class="ftpl-sec">${depth?'<span class="ftpl-arr">▸</span>':''}<span class="ftpl-mark">${esc(m)}</span><span class="ftpl-name">${esc(n)}</span>${pl?`<span class="ftpl-badge">${esc(pl)}개/줄</span>`:''}</span>`;
|
const isCol=sec.attributes?.collect==='true';
|
||||||
|
const roleHtml=selective?`<span class="ftpl-role ${isCol?'collect':'fixed'}">${isCol?'📥수집':'🔒고정'}</span>`:'';
|
||||||
|
let html=`<span class="ftpl-sec${isCol?' is-collect':''}">${depth?'<span class="ftpl-arr">▸</span>':''}<span class="ftpl-mark">${esc(m)}</span><span class="ftpl-name">${esc(n)}</span>${roleHtml}${pl?`<span class="ftpl-badge">${esc(pl)}개/줄</span>`:''}</span>`;
|
||||||
for(const c of sec.children??[]) if(c.type==='SECTION') html+=walkSec(c,depth+1);
|
for(const c of sec.children??[]) if(c.type==='SECTION') html+=walkSec(c,depth+1);
|
||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
@@ -1250,9 +1289,12 @@ function parseAndMerge(){
|
|||||||
const instances=splitIntoInstances(rawText,titleMark);
|
const instances=splitIntoInstances(rawText,titleMark);
|
||||||
fillerInstCount=instances.length;
|
fillerInstCount=instances.length;
|
||||||
fillerMerged={};
|
fillerMerged={};
|
||||||
|
const collectKeys=getCollectKeys(FT);
|
||||||
|
const selective=collectKeys.size>0;
|
||||||
for(const text of instances){
|
for(const text of instances){
|
||||||
const parsed=parseInstance(text,titleMark,sectionMarks);
|
const parsed=parseInstance(text,titleMark,sectionMarks);
|
||||||
for(const [key,tokens] of Object.entries(parsed.sections)){
|
for(const [key,tokens] of Object.entries(parsed.sections)){
|
||||||
|
if(selective&&!collectKeys.has(key)) continue;
|
||||||
if(!fillerMerged[key]) fillerMerged[key]=[];
|
if(!fillerMerged[key]) fillerMerged[key]=[];
|
||||||
fillerMerged[key].push(...tokens);
|
fillerMerged[key].push(...tokens);
|
||||||
}
|
}
|
||||||
@@ -1264,13 +1306,17 @@ function parseAndMerge(){
|
|||||||
// ── 텍스트 출력 ────────────────────────────────────────
|
// ── 텍스트 출력 ────────────────────────────────────────
|
||||||
function mergedToText(){
|
function mergedToText(){
|
||||||
if(!FT) return '';
|
if(!FT) return '';
|
||||||
|
const collectKeys=getCollectKeys(FT);
|
||||||
|
const selective=collectKeys.size>0;
|
||||||
function walkSec(sec){
|
function walkSec(sec){
|
||||||
const mark=sec.attributes?.mark??'';
|
const mark=sec.attributes?.mark??'';
|
||||||
const content=sec.content??'';
|
const content=sec.content??'';
|
||||||
const key=mark+content;
|
const key=mark+content;
|
||||||
|
const isCol=selective?(sec.attributes?.collect==='true'):true;
|
||||||
const perLine=parseInt(sec.attributes?.per_line??'0')||0;
|
const perLine=parseInt(sec.attributes?.per_line??'0')||0;
|
||||||
const tokens=fillerMerged[key]??[];
|
|
||||||
let t=mark+content+'\n';
|
let t=mark+content+'\n';
|
||||||
|
if(isCol){
|
||||||
|
const tokens=fillerMerged[key]??[];
|
||||||
if(tokens.length){
|
if(tokens.length){
|
||||||
if(perLine>0){
|
if(perLine>0){
|
||||||
for(let i=0;i<tokens.length;i+=perLine) t+=tokens.slice(i,i+perLine).join(' ')+'\n';
|
for(let i=0;i<tokens.length;i+=perLine) t+=tokens.slice(i,i+perLine).join(' ')+'\n';
|
||||||
@@ -1278,6 +1324,11 @@ function mergedToText(){
|
|||||||
t+=tokens.join(' ')+'\n';
|
t+=tokens.join(' ')+'\n';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
for(const c of sec.children??[]){
|
||||||
|
if(c.type==='VALUE'&&c.value!=null&&String(c.value).trim()) t+=String(c.value)+'\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
for(const c of sec.children??[]) if(c.type==='SECTION') t+=walkSec(c);
|
for(const c of sec.children??[]) if(c.type==='SECTION') t+=walkSec(c);
|
||||||
return t;
|
return t;
|
||||||
}
|
}
|
||||||
@@ -1291,14 +1342,20 @@ function mergedToText(){
|
|||||||
// ── JSON 출력 ───────────────────────────────────────────
|
// ── JSON 출력 ───────────────────────────────────────────
|
||||||
function mergedToJSON(){
|
function mergedToJSON(){
|
||||||
if(!FT) return '{}';
|
if(!FT) return '{}';
|
||||||
|
const collectKeys=getCollectKeys(FT);
|
||||||
|
const selective=collectKeys.size>0;
|
||||||
function buildSec(sec){
|
function buildSec(sec){
|
||||||
const mark=sec.attributes?.mark??'';
|
const mark=sec.attributes?.mark??'';
|
||||||
const content=sec.content??'';
|
const content=sec.content??'';
|
||||||
const key=mark+content;
|
const key=mark+content;
|
||||||
|
const isCol=selective?(sec.attributes?.collect==='true'):true;
|
||||||
|
let valChildren;
|
||||||
|
if(isCol){
|
||||||
const tokens=fillerMerged[key]??[];
|
const tokens=fillerMerged[key]??[];
|
||||||
const valChildren=tokens.map((t,i)=>({
|
valChildren=tokens.map((t,i)=>({id:'mv'+i,type:'VALUE',data_type:'string',value:t}));
|
||||||
id:'mv'+i,type:'VALUE',data_type:'string',value:t
|
} else {
|
||||||
}));
|
valChildren=(sec.children??[]).filter(c=>c.type==='VALUE');
|
||||||
|
}
|
||||||
const secChildren=(sec.children??[]).filter(c=>c.type==='SECTION').map(buildSec);
|
const secChildren=(sec.children??[]).filter(c=>c.type==='SECTION').map(buildSec);
|
||||||
return {...sec, children:[...valChildren,...secChildren]};
|
return {...sec, children:[...valChildren,...secChildren]};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user