feat(core,view): ③ 공통 설정 — fck and fy as inputs (spec §6.2)

- Material {fck, fyH, fyS}, defaults 27 / 400 / 500 as assumed values (I02).
  The rebar calculations take the material: the C7/C8 splice, the P2 development
  length, the P3 lap, and the B2 split use it instead of fixed numbers.
- ③ screen: 공통 설정 inputs with a note that the defaults are assumed (I02).
- Project file stores the material as an optional field; older files open with
  the defaults.
- Tests: fck 27 → 35 shortens the laps by √(27/35); the C7 piece follows fck;
  a non-default fy drops I02; the file round trip.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
ypshimandClaude Haiku 5.5 committed 2026-10-22 12:06:11 +09:00
1 parent 64d415f891
commit 9ba7b55eab
11 files changed
+123 -16

No files matched your search

+9
View File
@@ -64,3 +64,12 @@ describe('project file round trip (spec §8, plan B3)', () => {
expect(reopened.piers[0].draft).toBeUndefined();
});
});
describe('③ 공통 설정 in the project file', () => {
it('fck and fy are kept through save and open; older files get the defaults through the app', () => {
const p = sampleProject();
const withCommon = { ...p, common: { fck: 35, fyH: 400, fyS: 500 } };
expect(parseProject(serializeProject(withCommon)).common).toEqual({ fck: 35, fyH: 400, fyS: 500 });
expect(parseProject(serializeProject(p)).common).toBeUndefined();
});
});
+33
View File
@@ -0,0 +1,33 @@
import { describe, expect, it } from 'vitest';
import { buildPier } from '../src/core/geometry';
import { DEFAULT_MATERIAL, isAssumedMaterial } from '../src/core/rebar/common';
import { columnRebar } from '../src/core/rebar/column';
import { rebarSummary } from '../src/core/rebar/summary';
import { loadP1 } from './helpers';
describe('③ 공통 설정: fck · fy 입력 (spec §6.2)', () => {
const model = buildPier(loadP1());
it('defaults are assumed (I02); changed values are not', () => {
expect(isAssumedMaterial(DEFAULT_MATERIAL)).toBe(true);
expect(isAssumedMaterial({ ...DEFAULT_MATERIAL, fck: 35 })).toBe(false);
});
it('a higher fck shortens the laps: P3 lap scales with 1/√fck (27 → 35)', () => {
const base = columnRebar();
const high = columnRebar(undefined, { ...DEFAULT_MATERIAL, fck: 35 });
expect(high.lapP3 / base.lapP3).toBeCloseTo(Math.sqrt(27 / 35), 3);
expect(high.zTop2).toBeLessThan(base.zTop2);
});
it('the rebar summary follows the material: the C7 splice piece changes with fck', () => {
const base = rebarSummary(model).rows.find((r) => r.name === 'C7')!.length!;
const high = rebarSummary(model, { ...DEFAULT_MATERIAL, fck: 35 }).rows.find((r) => r.name === 'C7')!.length!;
expect(high).toBeLessThan(base);
});
it('a non-default material drops the I02 warning', () => {
const codes = rebarSummary(model, { ...DEFAULT_MATERIAL, fyS: 600 }).warnings.map((w) => w.code);
expect(codes).not.toContain('I02');
});
});
+1 -1
View File
@@ -45,7 +45,7 @@ describe('③ 철근표 요약 (P1)', () => {
it('B9: 320 bars; warnings: B2 needs a splice (13646 > 12000)', () => {
expect(count('B9')).toBe(320);
expect(warnings.map((w) => w.code)).toEqual(['RB-F08']);
expect(warnings.map((w) => w.code)).toEqual(['I02', 'RB-F08']); // defaults are assumed values (I02)
// B2 is split into two pieces (추천안): each piece is about half the 13646 bar plus half the lap.
expect(rows.find((r) => r.name === 'B2')!.length!).toBeCloseTo(7776, 0);
});