feat(ui): per-group separator and namesPerLine settings
- 전역 namesPerLine 제거 → 그룹별 개별 설정으로 전환 - 각 이름 그룹 카드에 두 가지 옵션 추가: - 구분자: 공백 / ", " / " / " / 줄바꿈(↵) 칩 + 직접 입력 - 한 줄 N개: 0=제한없음, 줄바꿈 모드 시 자동 비활성화 - mkGrp() 헬퍼로 그룹 기본값(sep=' ', namesPerLine=0) 통일 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
102
ui/builder.html
102
ui/builder.html
@@ -157,6 +157,30 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;backgrou
|
||||
.grp-names:focus{outline:none;border-color:var(--border-focus)}
|
||||
.grp-hint{font-size:10px;color:var(--muted);margin-top:3px}
|
||||
|
||||
/* ── 그룹 옵션 (구분자 / 한 줄) ─────────────────────────── */
|
||||
.grp-opts{display:flex;flex-direction:column;gap:5px;margin-top:6px;padding-top:6px;border-top:1px dashed var(--border)}
|
||||
.opt-row{display:flex;align-items:center;gap:6px}
|
||||
.opt-lbl{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;color:var(--muted);width:40px;flex-shrink:0}
|
||||
.sep-chips{display:flex;gap:3px;flex-wrap:wrap}
|
||||
.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-custom{
|
||||
width:52px;font-size:11px;border:1px solid var(--border);border-radius:4px;
|
||||
padding:2px 5px;background:var(--surface);text-align:center
|
||||
}
|
||||
.sep-custom:focus{outline:none;border-color:var(--border-focus)}
|
||||
.npl-input{
|
||||
width:46px;font-size:12px;border:1px solid var(--border);border-radius:4px;
|
||||
padding:2px 5px;text-align:center;background:var(--surface)
|
||||
}
|
||||
.npl-input:focus{outline:none;border-color:var(--border-focus)}
|
||||
.npl-input:disabled{background:var(--surface-alt);color:var(--muted)}
|
||||
|
||||
/* ── 섹션 하단 액션 ──────────────────────────────────────── */
|
||||
.sec-actions{display:flex;gap:5px;flex-wrap:wrap;padding-top:2px}
|
||||
.sBtn{
|
||||
@@ -266,13 +290,6 @@ input,textarea,button{font-family:inherit}
|
||||
<div class="marks-wrap" id="marksList"></div>
|
||||
<button class="btn-add-mark" onclick="addMark()">+ 레벨</button>
|
||||
</div>
|
||||
<div class="config-row">
|
||||
<span class="config-lbl">한 줄</span>
|
||||
<input id="cfgNpl" type="number" min="0" step="1"
|
||||
style="width:56px;text-align:center;font-size:13px;border:1px solid var(--border);border-radius:6px;padding:3px 6px;background:var(--surface)"
|
||||
value="0" oninput="syncConfig()">
|
||||
<span style="font-size:12px;color:var(--muted)">개 <span style="font-size:11px">(0 = 제한 없음)</span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 문서 제목 -->
|
||||
@@ -325,16 +342,19 @@ input,textarea,button{font-family:inherit}
|
||||
// ════════════════════════════════════════════════════════════
|
||||
// 상태
|
||||
// ════════════════════════════════════════════════════════════
|
||||
function mkGrp(cat='',names='',sep=' ',npl=0){
|
||||
return {id:id(),cat,names,sep,namesPerLine:npl};
|
||||
}
|
||||
let S = {
|
||||
cfg: { titleMark:'✅', marks:['🔹','🔸'], namesPerLine:0 },
|
||||
cfg: { titleMark:'✅', marks:['🔹','🔸'] },
|
||||
title: '예시 양식',
|
||||
sections: [
|
||||
{ id:id(), lv:1, name:'1번 항목',
|
||||
groups:[{id:id(),cat:'',names:'홍길동 홍길순 홍길영 홍길준\n홍길호 홍길윤'}],
|
||||
groups:[mkGrp('','홍길동 홍길순 홍길영 홍길준\n홍길호 홍길윤')],
|
||||
children:[] },
|
||||
{ id:id(), lv:1, name:'2번 항목', groups:[], children:[
|
||||
{ id:id(), lv:2, name:'2-1 항목', groups:[{id:id(),cat:'건강',names:'홍길찬 홍길만'}], children:[] },
|
||||
{ id:id(), lv:2, name:'2-2 항목', groups:[{id:id(),cat:'직장',names:'홍길주'}], children:[] }
|
||||
{ id:id(), lv:2, name:'2-1 항목', groups:[mkGrp('건강','홍길찬 홍길만')], children:[] },
|
||||
{ id:id(), lv:2, name:'2-2 항목', groups:[mkGrp('직장','홍길주')], children:[] }
|
||||
]}
|
||||
]
|
||||
};
|
||||
@@ -359,7 +379,6 @@ function load(){
|
||||
// ════════════════════════════════════════════════════════════
|
||||
function syncConfig(){
|
||||
S.cfg.titleMark = document.getElementById('cfgTitle').value || '✅';
|
||||
S.cfg.namesPerLine = Math.max(0, parseInt(document.getElementById('cfgNpl').value)||0);
|
||||
document.getElementById('titleMarkBadge').textContent = S.cfg.titleMark;
|
||||
renderMarks();
|
||||
renderSections();
|
||||
@@ -438,7 +457,7 @@ function updateName(id,v){ const s=findSec(S.sections,id); if(s) s.name=v; updat
|
||||
// ════════════════════════════════════════════════════════════
|
||||
function addGroup(sid){
|
||||
const s=findSec(S.sections,sid); if(!s) return;
|
||||
const ng={id:id(),cat:'',names:''};
|
||||
const ng=mkGrp();
|
||||
s.groups.push(ng);
|
||||
renderSections();
|
||||
updatePreview();
|
||||
@@ -476,7 +495,23 @@ function renderSec(sec, idx, totalInParent){
|
||||
const maxLv = S.cfg.marks.length;
|
||||
const canChild = sec.lv < maxLv;
|
||||
|
||||
const grpsHtml = sec.groups.map((g,gi)=>`
|
||||
const SEP_PRESETS=[
|
||||
{v:' ', label:'공백'},
|
||||
{v:', ', label:', '},
|
||||
{v:' / ',label:'/'},
|
||||
{v:'\n', label:'↵ 줄바꿈'},
|
||||
];
|
||||
const grpsHtml = sec.groups.map((g,gi)=>{
|
||||
const sep = g.sep ?? ' ';
|
||||
const npl = g.namesPerLine ?? 0;
|
||||
const isNl = sep==='\n';
|
||||
const presetMatch = SEP_PRESETS.find(p=>p.v===sep);
|
||||
const sepChips = SEP_PRESETS.map(p=>`
|
||||
<span class="sep-chip${sep===p.v?' on':''}"
|
||||
onclick="updateGroup('${sec.id}','${g.id}','sep',${JSON.stringify(p.v)});renderSections();updatePreview();save()"
|
||||
>${p.label}</span>`).join('');
|
||||
const customVal = presetMatch ? '' : esc(sep);
|
||||
return `
|
||||
<div class="grp" data-gid="${g.id}">
|
||||
<div class="grp-head">
|
||||
<span class="grp-cat-lbl">카테고리</span>
|
||||
@@ -488,10 +523,27 @@ function renderSec(sec, idx, totalInParent){
|
||||
<button class="iBtn" onclick="deleteGroup('${sec.id}','${g.id}')" title="삭제">✕</button>
|
||||
</div>
|
||||
<textarea class="grp-names"
|
||||
placeholder="이름 입력 (공백/줄바꿈으로 구분)"
|
||||
placeholder="이름 입력 (공백·줄바꿈·쉼표로 구분)"
|
||||
oninput="updateGroup('${sec.id}','${g.id}','names',this.value)">${esc(g.names)}</textarea>
|
||||
<div class="grp-hint">스페이스·줄바꿈·쉼표로 이름을 구분합니다</div>
|
||||
</div>`).join('');
|
||||
<div class="grp-opts">
|
||||
<div class="opt-row">
|
||||
<span class="opt-lbl">구분자</span>
|
||||
<div class="sep-chips">${sepChips}</div>
|
||||
<input class="sep-custom" placeholder="직접"
|
||||
value="${customVal}"
|
||||
title="직접 입력 시 엔터(\\n)도 사용 가능"
|
||||
oninput="updateGroup('${sec.id}','${g.id}','sep',this.value||' ');updatePreview();save()">
|
||||
</div>
|
||||
<div class="opt-row">
|
||||
<span class="opt-lbl">한 줄</span>
|
||||
<input class="npl-input" type="number" min="0" step="1"
|
||||
value="${npl}" ${isNl?'disabled title="줄바꿈 모드에서는 자동 1개"':''}
|
||||
oninput="updateGroup('${sec.id}','${g.id}','namesPerLine',Math.max(0,+this.value||0));updatePreview();save()">
|
||||
<span style="font-size:11px;color:var(--muted)">${isNl?'(줄바꿈 모드)':'개 (0 = 제한없음)'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
const childrenHtml = sec.children.map((c,ci)=>renderSec(c,ci,sec.children.length)).join('');
|
||||
|
||||
@@ -521,7 +573,6 @@ function renderSections(){
|
||||
renderMarks();
|
||||
document.getElementById('titleMarkBadge').textContent = S.cfg.titleMark;
|
||||
document.getElementById('cfgTitle').value = S.cfg.titleMark;
|
||||
document.getElementById('cfgNpl').value = S.cfg.namesPerLine ?? 0;
|
||||
document.getElementById('docTitle').value = S.title;
|
||||
document.getElementById('secList').innerHTML = S.sections.map((s,i)=>renderSec(s,i,S.sections.length)).join('');
|
||||
}
|
||||
@@ -545,13 +596,16 @@ function toText(){
|
||||
for(const g of sec.groups){
|
||||
const ns=parseNames(g.names);
|
||||
if(!ns.length) continue;
|
||||
const pre=g.cat.trim()?`(${g.cat.trim()}) `:'';
|
||||
const npl=S.cfg.namesPerLine;
|
||||
if(npl>0){
|
||||
const pre = g.cat.trim()?`(${g.cat.trim()}) `:'';
|
||||
const sep = g.sep??' ';
|
||||
const npl = g.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(' '));
|
||||
lines.push((i===0?pre:'')+ns.slice(i,i+npl).join(sep));
|
||||
} else {
|
||||
lines.push(pre+ns.join(' '));
|
||||
lines.push(pre+ns.join(sep));
|
||||
}
|
||||
lines.push('');
|
||||
}
|
||||
@@ -687,7 +741,7 @@ function doImport(){
|
||||
function flush(){
|
||||
if(!pend||!pend.names.length){ pend=null; return; }
|
||||
const parent = pend.parentId ? findSec(sections, pend.parentId) : null;
|
||||
const grp={id:id(),cat:pend.cat||'',names:pend.names.join(' ')};
|
||||
const grp=mkGrp(pend.cat||'',pend.names.join(' '));
|
||||
if(parent) parent.groups.push(grp);
|
||||
pend=null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user