feat(core,view): footing rebar B1–B12 (plan C5) and the ③ rebar screen v1

- Footing (확대기초철근 (1).md): grid counts from W and L, layer heights,
  B1–B4 and B5–B8 counts and lengths (B2 13646, B6 11396), B10-1/B10-2,
  B11 (60) and B12 (47) with their lengths. B9 is not computed yet.
- rebarSummary: one 철근표 per pier from the model (bearing, coping, column,
  footing), with alerts: RB-F08 (B2 needs a splice, Q-B6), RB-F09 (B9 not
  computed).
- ③ screen v1: stage nav button, member tabs (받침블럭 · 코핑 · 기둥 · 확대기초),
  rebar table with 번호 · 규격 · 개수 · 길이 · 비고, alert list, and the open
  questions (Q-B5, Q-B6, Q-C9). Drawings and the 강조 연동 are not in v1.
- Tests: footing values from the document; P1 summary totals; warnings.
- Not verified in a browser.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
ypshimandClaude Haiku 5.5 committed 2026-10-22 11:33:47 +09:00
1 parent a403ab9641
commit 3af8729e9f
7 files changed
+464 -3

No files matched your search

+70 -2
View File
@@ -10,6 +10,7 @@ import { EDITABLE_FIELDS, FIELD_LABELS, overrideKeys, pendingKeys, revertDraft,
import { adjustRow, layoutText, resolveRowCounts, rowLayout, totalPiles } from './core/piles';
import { applyCopy, DEFAULT_COPY_ITEMS, planCopy, type CopyItem } from './core/copy';
import { pileGeometry } from './core/geometry';
import { rebarSummary, type RebarRow } from './core/rebar/summary';
import { allVisibleChecked, setVisibleChecked, type TypeFilter } from './core/selection';
import { choose, defaultSummary, isSelectable, SHAPE_GROUPS, UNDEFINED_REASON, type MemberId } from './core/shape-options';
import { parseProject, SCHEMA_VERSION, serializeProject, type ProjectFile, type ProjectInfo } from './core/project';
@@ -69,7 +70,9 @@ const state = {
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',
stage: 'shape' as 'shape' | 'plan' | 'rebar',
/** Member tab on ③ (철근표). */
rebarMember: 'coping' as RebarRow['member'],
/** 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. */
@@ -393,6 +396,7 @@ function renderAll(): void {
renderPanels();
renderEditCard();
renderPlan();
renderRebar();
scheduleBackup();
}
@@ -666,15 +670,78 @@ function renderShapeOptions(member: MemberId): void {
}
// --- ② 형상계획 (plan.md C1) -------------------------------------------------------------
function setStage(stage: 'shape' | 'plan'): void {
function setStage(stage: 'shape' | 'plan' | 'rebar'): void {
state.stage = stage;
$<HTMLElement>('view-shape').hidden = stage !== 'shape';
$<HTMLElement>('view-plan').hidden = stage !== 'plan';
$<HTMLElement>('view-rebar').hidden = stage !== 'rebar';
$<HTMLButtonElement>('nav-shape').setAttribute('aria-current', String(stage === 'shape'));
$<HTMLButtonElement>('nav-plan').setAttribute('aria-current', String(stage === 'plan'));
$<HTMLButtonElement>('nav-rebar').setAttribute('aria-current', String(stage === 'rebar'));
renderAll();
}
// --- ③ 철근배근 (plan C5 · ③ screen v1) --------------------------------------------------
const REBAR_MEMBERS: { id: RebarRow['member']; label: string }[] = [
{ id: 'bearing', label: '받침블럭' },
{ id: 'coping', label: '코핑' },
{ id: 'column', label: '기둥' },
{ id: 'footing', label: '확대기초' },
];
function renderRebar(): void {
const pier = activePier();
const tabs = $<HTMLUListElement>('rebar-tabs');
tabs.replaceChildren();
for (const m of REBAR_MEMBERS) {
const li = el('li');
const btn = el('button', m.label, 'member');
btn.type = 'button';
btn.setAttribute('aria-pressed', String(m.id === state.rebarMember));
btn.addEventListener('click', () => {
state.rebarMember = m.id;
renderAll();
});
li.appendChild(btn);
tabs.appendChild(li);
}
const box = $<HTMLDivElement>('rebar-table');
box.replaceChildren();
const issues = $<HTMLUListElement>('rebar-issues');
issues.replaceChildren();
if (!pier?.model) {
$('rebar-note').textContent = '교각을 생성하면 철근표가 나옵니다.';
return;
}
const { rows, warnings } = rebarSummary(pier.model);
$('rebar-note').textContent = `${pier.input.name} · 형상 기준 계산 · 값은 기본값 기준 문서 규칙`;
const shown = rows.filter((r) => r.member === state.rebarMember);
const table = el('table', undefined, 'grid-table');
const head = el('thead');
const hr = el('tr');
for (const h of ['번호', '규격', '개수', '길이 (mm)', '비고']) hr.appendChild(el('th', h));
head.appendChild(hr);
table.appendChild(head);
const body = el('tbody');
for (const r of shown) {
const tr = el('tr');
tr.append(
el('td', r.name),
el('td', r.size),
el('td', String(r.count)),
el('td', r.length === null ? '—' : String(Math.round(r.length * 10) / 10)),
el('td', r.note),
);
body.appendChild(tr);
}
table.appendChild(body);
box.appendChild(table);
for (const w of warnings) issues.appendChild(el('li', `${w.code} ${w.message}`));
if (issues.childElementCount === 0) issues.appendChild(el('li', '알림이 없습니다.'));
$('step3-status').textContent = `계산됨 · 경고 ${warnings.length}`;
}
function renderPlan(): void {
const pier = activePier();
const members = $<HTMLUListElement>('members');
@@ -1065,6 +1132,7 @@ $<HTMLButtonElement>('bulk-regen').addEventListener('click', regenerateChecked);
$<HTMLButtonElement>('bulk-copy').addEventListener('click', openCopyDialog);
$<HTMLButtonElement>('nav-shape').addEventListener('click', () => setStage('shape'));
$<HTMLButtonElement>('nav-plan').addEventListener('click', () => setStage('plan'));
$<HTMLButtonElement>('nav-rebar').addEventListener('click', () => setStage('rebar'));
$<HTMLButtonElement>('member-regen').addEventListener('click', () => {
const pier = activePier();
if (pier) regenerateOne(pier.input.name);