Files
Pier_v1/tests/ground-inputs.test.ts
T
ypshimandClaude Haiku 5.5 d77f2bfd17 feat(bearing): 받침 종류·용량 as a text input (default 탄성받침)
- PierInput.bearingType: text, absent = 탄성받침. Tracked for changes, so it
  can be reverted. Saved in the project; no rule yet, so it does not change the model.
- Shown on the ② bearing card above the 공통 규칙.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-26 11:39:28 +09:00

259 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, it } from 'vitest';
import { buildPier } from '../src/core/geometry';
import { parseNValue, validateGround, validatePileEmbedment, validateStrataNValues } from '../src/core/ground';
import { parseProject, serializeProject, SCHEMA_VERSION } from '../src/core/project';
import { changedKeys } from '../src/core/changes';
import { COPY_ITEMS, applyCopy, planCopy } from '../src/core/copy';
import type { Issue, PierInput } from '../src/core/types';
import { groundSectionSvg } from '../src/view/ground-svg';
import { loadP1 } from './helpers';
const base = loadP1();
function withGround(extra: Partial<PierInput>): PierInput {
return { ...base, groundEl: 28.931, ...extra };
}
function ground(input: PierInput): string[] {
const issues: Issue[] = [];
validateGround(input, issues);
return issues.map((i) => i.code);
}
const GOOD_ROWS = [
{ name: '매립토', bottomEl: 27.5 },
{ name: '풍화토', bottomEl: 22 },
{ name: '연암', bottomEl: 10 },
];
describe('지반·지층 입력 검사 (G01–G03)', () => {
it('no issues for a well-ordered set of rows and a water level inside the range', () => {
expect(ground(withGround({ strataRows: GOOD_ROWS, groundWaterOn: true, groundWaterEl: 25 }))).toEqual([]);
});
it('G01: a layer whose bottom EL is not below the boundary above it', () => {
const rows = [{ name: '매립토', bottomEl: 27.5 }, { name: '풍화토', bottomEl: 28 }];
expect(ground(withGround({ strataRows: rows }))).toEqual(['G01']);
});
it('G01: the first layer must be below the ground EL', () => {
expect(ground(withGround({ strataRows: [{ name: '매립토', bottomEl: 29 }] }))).toEqual(['G01']);
});
it('G03: a layer name outside the strata list', () => {
expect(ground(withGround({ strataRows: [{ name: '점토', bottomEl: 27 }] }))).toEqual(['G03']);
});
it('G02: the water level above the ground, below the lowest layer, or missing while on', () => {
expect(ground(withGround({ strataRows: GOOD_ROWS, groundWaterOn: true, groundWaterEl: 29 }))).toEqual(['G02']);
expect(ground(withGround({ strataRows: GOOD_ROWS, groundWaterOn: true, groundWaterEl: 5 }))).toEqual(['G02']);
expect(ground(withGround({ groundWaterOn: true }))).toEqual(['G02']);
});
it('the water level is not checked when it is off', () => {
expect(ground(withGround({ strataRows: GOOD_ROWS, groundWaterOn: false, groundWaterEl: 99 }))).toEqual([]);
});
});
describe('일괄 적용 (속성 복사 · 위치·지하수위)', () => {
it('copies only the position and the water level, not the layer rows', () => {
const source = withGround({ groundPosition: '하상', groundWaterOn: true, groundWaterEl: 25, strataRows: GOOD_ROWS });
const target = withGround({ groundPosition: '육상', strataRows: [] });
const plan = planCopy(source, target, ['ground-position']);
expect(plan.changes.map((c) => c.key).sort()).toEqual(['groundPosition', 'groundWaterEl', 'groundWaterOn']);
const next = applyCopy(target, plan);
expect(next.groundPosition).toBe('하상');
expect(next.strataRows).toEqual([]);
});
it('the item is off by default, so the existing 속성 복사 defaults do not change', () => {
expect(COPY_ITEMS['ground-position'].defaultOn).toBe(false);
});
});
describe('변경 추적 (지반·지층 키)', () => {
it('a changed layer row or water level is reported as a change', () => {
const a = withGround({ strataRows: GOOD_ROWS });
const b = withGround({ strataRows: [...GOOD_ROWS, { name: '경암', bottomEl: 5 }] });
expect(changedKeys(a, b)).toEqual(['strataRows']);
expect(changedKeys(a, { ...a, groundWaterEl: 20 })).toEqual(['groundWaterEl']);
});
});
describe('단면 그림 (삽도)', () => {
const model = buildPier(base);
it('shows the ground EL and one band per layer row, with the 연암 top line', () => {
const svg = groundSectionSvg(withGround({ strataRows: GOOD_ROWS }), model);
expect(svg).toContain('지반고 28.931');
expect(svg).toContain('연암층 상면 EL 22.000');
expect((svg.match(/fill="#(EADFC8|DCCBA8|C9B38D)"/g) ?? []).length).toBeGreaterThanOrEqual(3);
});
it('draws the water level only when it is on and has a value', () => {
expect(groundSectionSvg(withGround({ strataRows: GOOD_ROWS, groundWaterOn: true, groundWaterEl: 25 }), model)).toContain('지하수위 25.000');
// The dashed water line and its label are absent (the aria-label always names 지하수위).
const off = groundSectionSvg(withGround({ strataRows: GOOD_ROWS, groundWaterOn: false, groundWaterEl: 25 }), model);
expect(off).not.toContain('stroke-dasharray="6 4"');
expect(off).not.toContain('지하수위 25');
});
it('with no layer rows, the ground band still fills the drawing down to its bottom', () => {
const svg = groundSectionSvg(withGround({}), model);
expect(svg).toContain('<svg');
expect(svg).toContain('지반고 28.931');
});
});
describe('말뚝 선단 근입 (G04·G05)', () => {
// P1 model: pile tips at about EL 2.2 m, D 0.6096 m, so the limit is the 연암 top minus 0.6096.
const model = buildPier(base);
const deepest = Math.max(...model.piles.map((p) => p.bottom));
function embed(input: PierInput): string[] {
const issues: Issue[] = [];
validatePileEmbedment(input, model, issues);
return issues.map((i) => i.code);
}
it('no check and no message when no layers are entered yet', () => {
expect(embed(withGround({}))).toEqual([]);
});
it('G04: a pile tip above the 연암 top minus the embedment', () => {
// 연암 top = the 매립토 bottom 2.5 m, so the limit is 2.5 − 0.6096 = 1.89 m, above the tips at 2.2 m.
expect(deepest).toBeGreaterThan(2.5 - 0.6096);
const rows = [{ name: '매립토', bottomEl: 2.5 }, { name: '연암', bottomEl: 1 }];
expect(embed(withGround({ strataRows: rows }))).toEqual(['G04']);
});
it('no G04 when the 연암 top is high enough above the tips', () => {
const rows = [{ name: '매립토', bottomEl: 20 }, { name: '연암', bottomEl: 10 }];
expect(embed(withGround({ strataRows: rows }))).toEqual([]);
});
it('G05 (info): layers entered but no 연암 row, so the embedment is not checked', () => {
expect(embed(withGround({ strataRows: [{ name: '풍화토', bottomEl: 20 }] }))).toEqual(['G05']);
});
});
describe('N값 입력 (승인 2026-10-26: 글자 입력, 타수/관입량 mm)', () => {
it('reads a blow count, a refusal in mm, and empty; surrounding spaces are ignored', () => {
expect(parseNValue('')).toEqual({ kind: 'empty' });
expect(parseNValue(undefined)).toEqual({ kind: 'empty' });
expect(parseNValue(' 23 ')).toEqual({ kind: 'n', n: 23 });
expect(parseNValue('0')).toEqual({ kind: 'n', n: 0 });
expect(parseNValue('50/5')).toEqual({ kind: 'refusal', blows: 50, mm: 5 });
});
it('rejects text, out-of-range blow counts, and penetrations', () => {
for (const bad of ['abc', '-3', '101', '50/0', '50/301', '50/', '/5', '23.5']) {
expect(parseNValue(bad).kind).toBe('invalid');
}
});
it('G06: a malformed N value is a warning; empty and valid values are not', () => {
const issues: Issue[] = [];
validateStrataNValues(withGround({ strataRows: [{ name: '매립토', bottomEl: 27, nValue: '가' }, { name: '풍화토', bottomEl: 22, nValue: '50/5' }, { name: '연암', bottomEl: 10 }] }), issues);
expect(issues.map((i) => i.code)).toEqual(['G06']);
expect(issues[0].level).toBe('W');
});
it('a project file refuses a layer row whose N value is not text', () => {
const bad = { ...loadP1(), strataRows: [{ name: '연암', bottomEl: 10, nValue: 5 }] } as unknown as PierInput;
const text = serializeProject({
schemaVersion: SCHEMA_VERSION,
project: { name: 'x', bridge: '운곡교', client: '', author: '', crs: 'KATEC', memo: '' },
source: { fileName: 'x.xlsx', sheets: [] },
piers: [{ id: 'P1', input: bad }],
history: [],
});
expect(() => parseProject(text)).toThrow('지층 행 형식');
});
});
describe('하상 (승인 2026-10-26: 설계홍수위 DHWL, 세굴 깊이)', () => {
const river = { groundPosition: '하상' as const };
it('G07: a 하상 without the DHWL', () => {
expect(ground(withGround({ ...river }))).toEqual(['G07']);
});
it('G08: a DHWL below the 지반고', () => {
expect(ground(withGround({ ...river, floodLevelEl: 28 }))).toEqual(['G08']);
});
it('a DHWL above the 지반고 passes', () => {
expect(ground(withGround({ ...river, floodLevelEl: 30 }))).toEqual([]);
});
it('G09: a negative scour depth, or a scour line below the footing bottom', () => {
expect(ground(withGround({ ...river, floodLevelEl: 30, scourDepthM: -1 }))).toEqual(['G09']);
// 28.931 − 3 = 25.931 is below the footing bottom 26.208; 28.931 − 1 = 27.931 is above it
expect(ground(withGround({ ...river, floodLevelEl: 30, scourDepthM: 3 }))).toEqual(['G09']);
expect(ground(withGround({ ...river, floodLevelEl: 30, scourDepthM: 1 }))).toEqual([]);
});
it('the groundwater check does not apply to a 하상, so a high groundwater value is ignored there', () => {
expect(ground(withGround({ ...river, floodLevelEl: 30, groundWaterOn: true, groundWaterEl: 99 }))).toEqual([]);
});
it('the 설계홍수위 is kept when the position is changed back to 육상 (남겨두기)', () => {
const input = withGround({ ...river, floodLevelEl: 30 });
const back = { ...input, groundPosition: '육상' as const };
expect(back.floodLevelEl).toBe(30);
});
it('the copy item carries the 설계홍수위 with the position', () => {
expect(COPY_ITEMS['ground-position'].keys).toContain('floodLevelEl');
});
it('the section draws the 설계홍수위 and the 세굴선, and no groundwater label', () => {
const model = buildPier(base);
const svg = groundSectionSvg(withGround({ ...river, floodLevelEl: 30, scourDepthM: 1, groundWaterOn: true, groundWaterEl: 25 }), model);
expect(svg).toContain('설계홍수위 DHWL 30.000');
expect(svg).toContain('세굴선 27.931');
expect(svg).not.toContain('지하수위 25');
});
});
describe('해상 (승인 2026-10-26: MSL 기준, 고조위·저조위 두 개, 부식 여유 기준값, 파랑·선박 하중 제외)', () => {
const sea = { groundPosition: '해상' as const };
it('G13: a 해상 without both the 고조위 and the 저조위', () => {
expect(ground(withGround({ ...sea, highWaterEl: 30 }))).toEqual(['G13']);
});
it('G10: the 해저면 not below the 고조위', () => {
expect(ground(withGround({ ...sea, highWaterEl: 28, lowWaterEl: 25 }))).toEqual(['G10']);
});
it('G11: the 저조위 not below the 고조위', () => {
expect(ground(withGround({ ...sea, highWaterEl: 30, lowWaterEl: 31 }))).toEqual(['G11']);
});
it('a valid 해상 passes, and the 육상 groundwater and 하상 flood inputs are ignored', () => {
expect(ground(withGround({ ...sea, highWaterEl: 30, lowWaterEl: 25, groundWaterOn: true, groundWaterEl: 99, floodLevelEl: 99 }))).toEqual([]);
});
it('G14: a pile wall at or below the corrosion allowance reference', () => {
const issues: Issue[] = [];
validateGround(withGround({ ...sea, highWaterEl: 30, lowWaterEl: 25, pileThicknessMm: 1 }), issues);
expect(issues.map((i) => i.code)).toEqual(['G14']);
});
it('the section shows the 고조위 (solid), the 저조위 (dashed), and the 해저면 label', () => {
const svg = groundSectionSvg(withGround({ ...sea, highWaterEl: 30, lowWaterEl: 25 }), buildPier(base));
expect(svg).toContain('고조위 HWL 30.000');
expect(svg).toContain('저조위 LWL 25.000');
expect(svg).toContain('해저면 EL 28.931');
});
});
describe('받침 종류·용량 (글자 입력)', () => {
it('is tracked as a change, so it can be reverted to the default', () => {
const a = withGround({});
expect(changedKeys(a, { ...a, bearingType: '고무받침' })).toEqual(['bearingType']);
});
});