From 56362570ec08402dfe39574946d94d49550637b9 Mon Sep 17 00:00:00 2001 From: ypshim Date: Thu, 22 Oct 2026 10:07:34 +0900 Subject: [PATCH] =?UTF-8?q?feat(view):=20=ED=98=95=EC=83=81=EA=B3=84?= =?UTF-8?q?=ED=9A=8D=20=E2=91=A1=20screen=20skeleton=20with=20five=20membe?= =?UTF-8?q?rs=20(plan.md=20C1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ① and ② are switched from the stage nav. ② shows the pier list with change counts, the five members (교량받침, 코핑, 기둥, 확대기초·말뚝, 지반·지층) with a change count each, a member card, and a change list with per-item revert. - The member card shows 기본 and 선택 summaries separately. Only members with existing inputs get fields (코핑 폭·길이, 말뚝 길이); the others show values and a note that their inputs come in a later step. Unselected members' inputs are not shown. - The field rows are shared between ① and ②. - Added a global [hidden] rule so hidden screens stay hidden. - Not verified in a browser. Co-Authored-By: Claude Haiku 5.5 --- index.html | 33 ++++++++++-- src/main.ts | 136 +++++++++++++++++++++++++++++++++++++++++++------- src/style.css | 12 +++++ 3 files changed, 160 insertions(+), 21 deletions(-) diff --git a/index.html b/index.html index e94837a..9e42e2b 100644 --- a/index.html +++ b/index.html @@ -14,8 +14,8 @@ 프로젝트 미지정
@@ -35,7 +35,7 @@
-
+
+
+ + +
+
+

코핑

+
+

+
+

+ +
+ + +
+ diff --git a/src/main.ts b/src/main.ts index 919617b..66a7f69 100644 --- a/src/main.ts +++ b/src/main.ts @@ -41,6 +41,16 @@ const VIEWS: { kind: ViewKind; label: string }[] = [ const TYPES: PierType[] = ['T형', '2주형', '3주형']; const CRS = '국토좌표계 (x = 동, y = 북)'; +/** 시안 ② 부재 목록 (spec §5.2). `keys` are the inputs that exist today; the rest show values only. */ +type MemberId = 'bearing' | 'coping' | 'column' | 'footing' | 'ground'; +const MEMBERS: { id: MemberId; label: string; summary: string; keys: EditableKey[]; note: string }[] = [ + { id: 'bearing', label: '교량받침·받침블럭·솔플레이트', summary: '배치 분리', keys: [], note: '받침 상세(C3)는 Q16 결정 후 추가합니다. 지금은 표시만 합니다.' }, + { id: 'coping', label: '코핑', summary: '교좌면 직각 · 하면 직선', keys: ['copingWidthMm', 'copingLengthMm'], note: '폭과 길이를 입력합니다. 계단 위치·단차선 입력은 다음 단계에서 추가합니다.' }, + { id: 'column', label: '기둥', summary: '원형 · 중실', keys: [], note: '기둥 지름 입력은 다음 단계에서 추가합니다. 높이와 위치는 계산값입니다.' }, + { id: 'footing', label: '확대기초 · 말뚝', summary: '직사각 · 강관', keys: ['pileLengthMm'], note: '말뚝 길이를 입력합니다. 열별 본수는 다음 단계에서 추가합니다.' }, + { id: 'ground', label: '지반·지층', summary: '육상 · 표시만', keys: [], note: '지반고는 입력값을 표시만 합니다. 지층 입력은 다음 단계에서 추가합니다.' }, +]; + interface Pending { fileName: string; entries: Pier[]; @@ -55,6 +65,10 @@ const state = { visible: Object.fromEntries(PART_GROUPS.flatMap((g) => g.parts).map((p) => [p, true])) as Record, scene: null as PierScene | null, project: { name: '', bridge: '', client: '', author: '', crs: CRS, memo: '' } as ProjectInfo, + /** Which stage screen is shown: ① 형상 생성 or ② 형상계획 (plan.md C1). */ + stage: 'shape' as 'shape' | 'plan', + /** Member card selected on ② (시안 ② 부재 목록). */ + member: 'coping' as MemberId, /** True once the project has a file on disk (first save via 시안 ⑤, or an opened file). Later saves go straight to it. */ savedOnce: false, filter: 'all' as 'all' | PierType, @@ -217,10 +231,19 @@ function renderPierList(): void { opt.textContent = `${opt.value === 'all' ? '전체' : opt.value} (${count})`; } - const list = $('pier-items'); - list.replaceChildren(); + renderPierRows($('pier-items'), 'shape'); + renderPierRows($('plan-pier-items'), 'plan'); const visible = state.piers.filter((p) => state.filter === 'all' || p.input.type === state.filter); $('pier-empty').hidden = state.piers.length > 0; + + const allChecked = visible.length > 0 && visible.every((p) => p.checked); + $('select-all').textContent = allChecked ? '선택 해제' : '전체 선택'; +} + +/** Rows of the pier list. ① shows the type and warnings; ② shows the count of changes from the defaults. */ +function renderPierRows(list: HTMLUListElement, mode: 'shape' | 'plan'): void { + list.replaceChildren(); + const visible = state.piers.filter((p) => state.filter === 'all' || p.input.type === state.filter); for (const pier of visible) { const li = el('li'); const check = el('input'); @@ -240,15 +263,14 @@ function renderPierList(): void { setupScene(); renderAll(); }); - const badge = pier.model - ? el('span', `${pier.input.type}${pier.issues.some((i) => i.level !== 'I') ? ' · 경고' : ''}`, 'badge') - : el('span', '오류', 'badge error'); + const badge = mode === 'plan' + ? el('span', `변경 ${overrideKeys(pier).length}`, 'badge') + : pier.model + ? el('span', `${pier.input.type}${pier.issues.some((i) => i.level !== 'I') ? ' · 경고' : ''}`, 'badge') + : el('span', '오류', 'badge error'); li.append(check, name, badge); list.appendChild(li); } - - const allChecked = visible.length > 0 && visible.every((p) => p.checked); - $('select-all').textContent = allChecked ? '선택 해제' : '전체 선택'; } function renderHeader(): void { @@ -368,6 +390,7 @@ function renderAll(): void { renderPierList(); renderPanels(); renderEditCard(); + renderPlan(); scheduleBackup(); } @@ -519,19 +542,31 @@ function regenerateOne(name: string): void { } function renderEditCard(): void { - const box = $('edit-fields'); - box.replaceChildren(); const pier = activePier(); const btn = $('edit-regen'); if (!pier) { + $('edit-fields').replaceChildren(); $('edit-summary').textContent = '교각을 선택하면 입력을 고칠 수 있습니다.'; btn.textContent = '다시 생성 · 변경 0'; btn.disabled = true; return; } + const status = fieldRows($('edit-fields'), pier, EDITABLE_FIELDS.map((f) => f.key)); + $('edit-summary').textContent = status.pending.length + ? `미반영 변경 ${status.pending.length}건. 3D 그림은 생성본을 유지합니다.` + : status.overrides.length + ? `기본값과 ${status.overrides.length}건 다릅니다. 생성본이 최신입니다.` + : '기본값과 같습니다.'; + btn.textContent = `다시 생성 · 변경 ${status.pending.length}`; + btn.disabled = status.pending.length === 0; +} + +/** One row per input key: value in the draft, the imported default beside it, and a revert button when overridden. */ +function fieldRows(box: HTMLDivElement, pier: Pier, keys: EditableKey[]): { overrides: EditableKey[]; pending: EditableKey[] } { + box.replaceChildren(); const overrides = overrideKeys(pier); const pending = pendingKeys(pier); - for (const f of EDITABLE_FIELDS) { + for (const f of EDITABLE_FIELDS.filter((x) => keys.includes(x.key))) { const changed = overrides.includes(f.key); const row = el('div', undefined, changed ? 'edit-row changed' : 'edit-row'); const id = `edit-${f.key}`; @@ -552,13 +587,72 @@ function renderEditCard(): void { } box.appendChild(row); } - $('edit-summary').textContent = pending.length - ? `미반영 변경 ${pending.length}건. 3D 그림은 생성본을 유지합니다.` - : overrides.length - ? `기본값과 ${overrides.length}건 다릅니다. 생성본이 최신입니다.` - : '기본값과 같습니다.'; - btn.textContent = `다시 생성 · 변경 ${pending.length}`; - btn.disabled = pending.length === 0; + return { overrides, pending }; +} + +// --- ② 형상계획 (plan.md C1) ------------------------------------------------------------- +function setStage(stage: 'shape' | 'plan'): void { + state.stage = stage; + $('view-shape').hidden = stage !== 'shape'; + $('view-plan').hidden = stage !== 'plan'; + $('nav-shape').setAttribute('aria-current', String(stage === 'shape')); + $('nav-plan').setAttribute('aria-current', String(stage === 'plan')); + renderAll(); +} + +function renderPlan(): void { + const pier = activePier(); + const members = $('members'); + members.replaceChildren(); + for (const m of MEMBERS) { + const li = el('li'); + const btn = el('button', m.label, 'member'); + btn.type = 'button'; + btn.setAttribute('aria-pressed', String(m.id === state.member)); + const changed = pier ? overrideKeys(pier).filter((k) => m.keys.includes(k)).length : 0; + btn.append(el('span', changed ? `변경 ${changed}` : '변경 0', 'badge')); + btn.addEventListener('click', () => { + state.member = m.id; + renderAll(); + }); + li.appendChild(btn); + members.appendChild(li); + } + + const member = MEMBERS.find((m) => m.id === state.member)!; + $('member-title').textContent = member.label; + $('member-summary').textContent = `기본: ${member.summary} · 선택: ${member.summary}`; + $('member-note').textContent = member.note; + const regen = $('member-regen'); + const fields = $('member-fields'); + if (!pier) { + fields.replaceChildren(); + regen.textContent = '다시 생성 · 변경 0'; + regen.disabled = true; + } else { + fieldRows(fields, pier, member.keys); + regen.textContent = `다시 생성 · 변경 ${pendingKeys(pier).length}`; + regen.disabled = pendingKeys(pier).length === 0; + } + + const changes = $('changes'); + changes.replaceChildren(); + const pending = pier ? pendingKeys(pier) : []; + for (const key of pier ? overrideKeys(pier) : []) { + const f = EDITABLE_FIELDS.find((x) => x.key === key)!; + const li = el('li', `${f.label}: 기본 ${pier!.defaults[key]} → 현재 ${pier!.draft[key]}${pending.includes(key) ? ' (미반영)' : ''}`); + const revert = el('button', '되돌리기', 'btn small'); + revert.type = 'button'; + revert.addEventListener('click', () => revertField(pier!.input.name, key)); + li.appendChild(revert); + changes.appendChild(li); + } + if (changes.childElementCount === 0) changes.appendChild(el('li', '기본값과 같습니다.')); + + const issues = $('plan-issues'); + issues.replaceChildren(); + for (const i of pier?.issues ?? []) issues.appendChild(el('li', `${i.code} ${i.message}`)); + if (issues.childElementCount === 0) issues.appendChild(el('li', '알림이 없습니다.')); } // --- project file (plan B3·B4·B5) -------------------------------------------------------- @@ -892,6 +986,12 @@ $('dlg-apply').addEventListener('click', applyImport); $('regen').addEventListener('click', regenerateChecked); $('bulk-regen').addEventListener('click', regenerateChecked); $('bulk-copy').addEventListener('click', openCopyDialog); +$('nav-shape').addEventListener('click', () => setStage('shape')); +$('nav-plan').addEventListener('click', () => setStage('plan')); +$('member-regen').addEventListener('click', () => { + const pier = activePier(); + if (pier) regenerateOne(pier.input.name); +}); $('cp-same-type').addEventListener('change', () => { renderCopyTargets(); renderCopyPreview(); diff --git a/src/style.css b/src/style.css index fdfe8f9..367757f 100644 --- a/src/style.css +++ b/src/style.css @@ -652,3 +652,15 @@ body { background: #eef1f4; font-family: 'Noto Sans KR', system-ui, sans-serif; .chk.disabled { opacity: 0.5; } #cp-preview td, #cp-preview th { font-size: 12px; } +/* 형상계획 ② (plan.md C1) */ +[hidden] { display: none !important; } +.members { list-style: none; padding: 0; margin: 0; } +.members li { margin: 2px 0; } +.members-title { margin-top: 14px; } +.member { display: flex; align-items: center; justify-content: space-between; gap: 8px; width: 100%; min-height: 44px; padding: 0 10px; border: 0; border-radius: 6px; background: transparent; color: #18212b; font-size: 13px; text-align: left; cursor: pointer; } +.member[aria-pressed='true'] { background: #e3edf8; color: #123e70; font-weight: 700; } +#member-fields { margin-top: 10px; } +#member-summary { color: #55606e; margin: 4px 0 8px; } +.plan .viewer { flex: 1 1 380px; } +.plan .results { flex: 1 1 300px; max-width: 360px; } +