diff --git a/ui/builder.html b/ui/builder.html index abd40a2..2ac5e6a 100644 --- a/ui/builder.html +++ b/ui/builder.html @@ -81,29 +81,12 @@ input,textarea,button,select{font-family:inherit} .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-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-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: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} -.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{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} .sep-chip:hover{border-color:var(--accent);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)} @@ -252,27 +235,16 @@ input,textarea,button,select{font-family:inherit} // 상수 // ════════════════════════════════════════════════════════ const VALUE_TYPES = [ - {v:'node', lbl:'노드 (Node)'}, - {v:'string', lbl:'문자열 (String)'}, - {v:'integer', lbl:'정수 (Integer)'}, - {v:'float', lbl:'실수 (Float)'}, - {v:'boolean', lbl:'불리언 (Boolean)'}, - {v:'null', lbl:'Null'}, - {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)'}, + {v:'node', lbl:'노드 (Node)'}, + {v:'string', lbl:'문자열 (String)'}, + {v:'int', lbl:'정수 (Int)'}, + {v:'bool', lbl:'불리언 (Bool)'}, + {v:'date', lbl:'날짜 (Date)'}, ]; const SETTING_OPTIONS = [ - {v:'header', lbl:'머리글 (header)'}, - {v:'footer', lbl:'꼬리글 (footer)'}, - {v:'prefix', lbl:'접두사 (prefix)'}, - {v:'suffix', lbl:'접미사 (suffix)'}, + {v:'prefix', lbl:'앞 문자열 (prefix)'}, + {v:'suffix', lbl:'뒤 문자열 (suffix)'}, {v:'attributes', lbl:'어트리뷰트'}, ]; @@ -291,13 +263,6 @@ const ATTR_KEYS = [ {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=''){ return { id:uid(), mark, content, hidden:false, values:[], settings:{}, - nextSep:'blank', childSep:'blank', nameNewline:true }; -} -function mkListVal(names='', category=''){ - return { id:uid(), type:'list', names, sep:' ', namesPerLine:0, category }; + nameNewline:true }; } function mkNodeVal(childNode){ return { id:uid(), type:'node', node: childNode ?? mkNode() }; @@ -353,11 +315,20 @@ function load(){ S=buildExample(); } +const VALID_TYPES=new Set(['node','string','int','bool','date']); function normalizeNode(n){ - if(n.nextSep===undefined) n.nextSep='blank'; - if(n.childSep===undefined) n.childSep='blank'; 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){ for(const n of s.nodes||[]) normalizeNode(n); @@ -376,35 +347,28 @@ function migrateOld(o){ function migSec(s, oldMarks){ const mark = s.mark ?? oldMarks[(s.lv??1)-1] ?? ''; const n = mkNode(mark, s.content ?? s.name ?? ''); - n.hidden = false; - // groups → list values + // groups → string values (names 합치기) for(const g of s.groups??[]){ - const lv = mkListVal(g.names??'', g.attributes?.category??''); - lv.sep = g.sep??' '; lv.namesPerLine = g.namesPerLine??0; - n.values.push(lv); + const names=(g.names??'').trim(); if(!names) continue; + const sv=mkScalarVal('string'); sv.data=names; n.values.push(sv); } - // children → node values 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'){ - 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.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); return n; } function buildExample(){ const n1=mkNode('🔹','1번 항목'); - n1.values.push(mkListVal('홍길동 홍길순 홍길영 홍길준\n홍길호 홍길윤','')); + const sv1=mkScalarVal('string'); sv1.data='홍길동 홍길순 홍길영 홍길준'; n1.values.push(sv1); const n2=mkNode('🔹','2번 항목'); - const c1=mkNode('🔸','2-1 항목'); c1.values.push(mkListVal('홍길찬 홍길만','건강')); - const c2=mkNode('🔸','2-2 항목'); c2.values.push(mkListVal('홍길주','직장')); + const c1=mkNode('🔸','2-1 항목'); const sv2=mkScalarVal('string'); sv2.data='홍길찬 홍길만'; c1.values.push(sv2); + 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)); return {titleMark:'✅', title:'예시 양식', nodes:[n1,n2]}; } @@ -440,12 +404,8 @@ function toggleHidden(nid){ function addValue(nid, type){ if(!type) return; const n=nodeMap[nid]; if(!n) return; - let v; - if(type==='list') v=mkListVal(); - else if(type==='node') v=mkNodeVal(); - else v=mkScalarVal(type); + const v = type==='node' ? mkNodeVal() : mkScalarVal(type); n.values.push(v); refresh(); - if(type==='list') focusNew('.val-names'); } function removeValue(vid){ const e=valueMap[vid]; if(!e) return; @@ -590,47 +550,7 @@ function renderValue(nid, val){ return `