From 3cfbdf62f6f38a6df71183f9dddb8095cb5852d6 Mon Sep 17 00:00:00 2001 From: ypshim Date: Mon, 26 Oct 2026 15:54:47 +0900 Subject: [PATCH] =?UTF-8?q?feat(rebar):=20=EC=B2=A0=EA=B7=BC=ED=91=9C=20?= =?UTF-8?q?=EA=B0=84=EA=B2=A9=C2=B7=EC=8B=A4=EA=B0=84=EA=B2=A9=EC=9D=84=20?= =?UTF-8?q?=EC=9E=85=EB=A0=A5=EA=B3=BC=20=EC=97=B0=EB=8F=99,=20=EA=B7=BC?= =?UTF-8?q?=EA=B1=B0=EB=8A=94=20=EA=B8=B0=EB=B3=B8=EA=B0=92/=EC=9E=85?= =?UTF-8?q?=EB=A0=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - C1–C8: 간격 2 × 주철근 기본간격, 실간격 기본간격 - C9·C11 · C10 · C12·C13 · C14: 전단·측면·C14 간격 입력 - S1·S2: 블록 철근 간격 입력 - 근거: 관련 입력이 모두 기본값이면 '기본값', 하나라도 바뀌면 '입력' - 입력이 없는 행(C15·C16, P3~P5, 커플러)은 기존 고정값 유지 Co-Authored-By: Claude Haiku 5.5 --- src/core/rebar/summary.ts | 41 ++++++++++++++++++++++++++++++--- tests/rebar-coping-main.test.ts | 16 +++++++++++++ 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/src/core/rebar/summary.ts b/src/core/rebar/summary.ts index 37a32fe..ac14b9f 100644 --- a/src/core/rebar/summary.ts +++ b/src/core/rebar/summary.ts @@ -1,5 +1,6 @@ import type { PierModel } from '../geometry'; -import { barLength, bearingBlockRebar, totals } from './bearing-block'; +import { barLength, bearingBlockRebar, DEFAULT_BLOCK_REBAR, totals } from './bearing-block'; +import { DEFAULT_COPING_REBAR } from './coping-settings'; import { bottomPiece, topBarLength, widthLayout } from './coping-main'; import { shearBars, shearBarsMulti, copingSideBars, SIDE_HOOK_LEG, c14Bars, c14Length, stairBars, stepDevLength, undersideAt, shearULength, capLength, PIECES_PER_POSITION } from './coping'; import { sideRowLevels } from '../coping-geom'; @@ -117,6 +118,37 @@ function footingBarList(Lf: number, Wf: number, Hf: number, columnX: number[]): type RawRow = Omit; +const MAIN_BARS = ['C1', 'C2', 'C3', 'C4', 'C5', 'C6', 'C7', 'C8']; + +/** + * 간격·실간격 and 근거 of the rows that have a spacing input (승인 2026-10-26). 간격 is the design spacing (the cycle + * pitch for C1–C8, which alternate, so 2 × 기본간격); 실간격 is the spacing of the bars themselves. The basis is '기본값' + * when every input behind the row is at its default, otherwise '입력'. + */ +export function linkedSpacing(name: string, model: PierModel): Pick | undefined { + const c = model.copingRebar; + const d = DEFAULT_COPING_REBAR; + const b = model.blockRebar; + const bd = DEFAULT_BLOCK_REBAR; + const basis = (changed: boolean): Basis => (changed ? 'input' : 'default'); + if (MAIN_BARS.includes(name)) { + return { spacing: String(2 * c.mainSpacing), actual: String(c.mainSpacing), basis: basis(c.mainSpacing !== d.mainSpacing) }; + } + if (name === 'C9' || name === 'C11') return { spacing: String(c.spacingS), actual: String(c.spacingS), basis: basis(c.spacingS !== d.spacingS) }; + if (name === 'C10') return { spacing: String(c.spacingL), actual: String(c.spacingL), basis: basis(c.spacingL !== d.spacingL) }; + if (name === 'C12' || name === 'C13') return { spacing: String(c.sideSpacing), actual: String(c.sideSpacing), basis: basis(c.sideSpacing !== d.sideSpacing) }; + if (name === 'C14') { + return { + spacing: `${c.c14UpperSpacing} / ${c.c14LowerSpacing}`, + actual: String(c.c14UpperSpacing), + basis: basis(c.c14UpperSpacing !== d.c14UpperSpacing || c.c14LowerSpacing !== d.c14LowerSpacing), + }; + } + if (name === 'S1') return { spacing: String(b.spacingS1), actual: String(b.spacingS1), basis: basis(b.spacingS1 !== bd.spacingS1) }; + if (name === 'S2') return { spacing: String(b.spacingS2), actual: String(b.spacingS2), basis: basis(b.spacingS2 !== bd.spacingS2) }; + return undefined; +} + export function rebarSummary( model: PierModel, common: Material = DEFAULT_MATERIAL, @@ -256,7 +288,10 @@ export function rebarSummary( } else { warnings.push({ code: 'RB-F00', message: '현장타설말뚝은 확대기초가 없어 확대기초 철근을 만들지 않습니다.', target: 'footing' }); } - // Table metadata per bar (구분·간격·실간격·위치·방향·형상·근거). - const withMeta = rows.map((r) => ({ ...r, ...(META[r.name] ?? FALLBACK) })); + // Table metadata per bar (구분·간격·실간격·위치·방향·형상·근거). The spacing rows take their values from the inputs (승인 2026-10-26). + const withMeta = rows.map((r) => { + const linked = linkedSpacing(r.name, model); + return { ...r, ...(META[r.name] ?? FALLBACK), ...(linked ?? {}) }; + }); return { rows: withMeta, warnings: warnings.map((w) => ({ ...w, target: w.target ?? 'common' })) }; } diff --git a/tests/rebar-coping-main.test.ts b/tests/rebar-coping-main.test.ts index 8570453..b620252 100644 --- a/tests/rebar-coping-main.test.ts +++ b/tests/rebar-coping-main.test.ts @@ -34,6 +34,22 @@ describe('C1–C8 상·하면 주철근 (코핑철근.md §5, plan C3)', () => { expect(w120.count).toBe(25); }); + it('철근표 간격: P1 기본값은 250 / 125 · 기본값 근거, 기본간격 140이면 280 / 140 · 입력 근거', () => { + const base = rebarSummary(buildPier(loadP1())).rows; + const c1 = base.find((r) => r.name === 'C1')!; + expect([c1.spacing, c1.actual, c1.basis]).toEqual(['250', '125', 'default']); + const c12 = base.find((r) => r.name === 'C12')!; + expect(c12.basis).toBe('default'); + + const m = buildPier({ ...loadP1(), copingRebar: { mainSpacing: 140 }, blockRebar: { spacingS1: 130 } }); + const rows = rebarSummary(m).rows; + const c8 = rows.find((r) => r.name === 'C8')!; + expect([c8.spacing, c8.actual, c8.basis]).toEqual(['280', '140', 'input']); + expect(rows.find((r) => r.name === 'C12')!.basis).toBe('default'); + const s1 = rows.find((r) => r.name === 'S1')!; + expect([s1.spacing, s1.actual, s1.basis]).toEqual(['130', '130', 'input']); + }); + it('CP04: the default P1 has an even count (no warning); 기본간격 120 gives an odd count and a CP04 warning', () => { const base = rebarSummary(buildPier(loadP1())).warnings.filter((w) => w.code === 'CP04'); expect(base).toEqual([]);