Files
Pier_v1/tests/ground-inputs.test.ts
T
ypshimandClaude Haiku 5.5 7b5517e312 feat(ground): N value per layer row (text, blow count or 타수/관입량 mm)
- StrataRow.nValue: text. "23" is a blow count (0–100), "50/5" a refusal
  (50 blows for 5 mm, 1–300 mm). Empty means not known yet.
- G06 (warning): a malformed N value. Strict, because the values will feed the
  bearing-capacity and liquefaction checks.
- The layer table has an N값 column; the project file checks that the rows
  are well formed.

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

174 lines
7.9 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('지층 행 형식');
});
});