Files
Pier_v1/tests/ground-inputs.test.ts
T
ypshimandClaude Haiku 5.5 26a5aff517 feat(ground): 하상 selectable; 설계홍수위 and 세굴 깊이 for 하상
- shape-options: a per-option "confirmed" flag; 하상 is confirmed, 해상 is not.
- The 위치 chips store the choice in the pier's draft; 선택 shows it.
- 하상: the groundwater inputs give way to 설계홍수위 DHWL (floodLevelEl) and
  세굴 깊이 (scourDepthM). The groundwater value is kept (남겨두기).
- Checks: G07 DHWL missing, G08 DHWL below the 지반고, G09 scour negative or
  the scour line below the footing bottom. The groundwater check is skipped for 하상.
- Section: the DHWL as a solid line and the 세굴선 dashed.

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

219 lines
10 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');
});
});