Files
Pier_v1/tests/ground-model.test.ts
T
ypshimandClaude Haiku 5.5 325c841733 feat(ground): pile-tip embedment check and soil layers from the ground model
- ground.ts: G04 when a pile tip is above the 연암 상면 minus 1.0 × D; G05
  (info) when layers are entered but there is no 연암 row. No message while
  no layers are entered at all.
- ground-model.ts: reads the Python soil.layers as strata rows; the project
  file keeps and checks them.
- ② card: a button fills the layer rows from the ground model for the pier.

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

196 lines
8.1 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 { groundElFor, parseGroundModel, strataRowsFor } from '../src/core/ground-model';
import { changedKeys, overrideKeys, revertDraft, type PierValues } from '../src/core/changes';
import { parseProject, serializeProject, SCHEMA_VERSION, type ProjectFile } from '../src/core/project';
import { loadP1 } from './helpers';
/** The shape written by RAG/pierzainer_ground (schema 1). */
const SAMPLE = JSON.stringify({
schema: 1,
generated: '2026-10-23',
coord: '측량 X(북) · Y(동), m. 치수 mm',
piers: [
{ id: 'P1', ground: { center: 29.355, excel: 28.931, used: 29.355, source: '계산' }, footing: { cover: 0.972 } },
{ id: 'P2', ground: { center: null, excel: null, used: null, source: '없음' } },
],
});
describe('지반 모델 읽기 (ground_model.json, schema 1)', () => {
it('reads the pier-centre ground EL and the excel value', () => {
const m = parseGroundModel(SAMPLE);
expect(m.piers[0]).toEqual({ id: 'P1', centerEl: 29.355, excelEl: 28.931 });
expect(groundElFor(m, 'P1')).toBe(29.355);
});
it('a pier outside the terrain has no value, and an unknown pier has none either', () => {
const m = parseGroundModel(SAMPLE);
expect(groundElFor(m, 'P2')).toBeUndefined();
expect(groundElFor(m, 'P9')).toBeUndefined();
});
it('refuses another schema number instead of guessing the layout', () => {
expect(() => parseGroundModel(SAMPLE.replace('"schema":1', '"schema":3'))).toThrow('형식 번호 3');
});
it('refuses a file without a pier list or without pier names', () => {
expect(() => parseGroundModel('{"schema": 1}')).toThrow('교각 목록');
expect(() => parseGroundModel('{"schema": 1, "piers": [{"ground": {}}]}')).toThrow('이름(id)');
});
it('refuses text that is not JSON', () => {
expect(() => parseGroundModel('not json')).toThrow('JSON으로 읽히지 않습니다');
});
});
const LEGACY_SAMPLE = JSON.stringify({ schema: 1, piers: [{ id: 'P1', ground: { center: 29.355, excel: 28.931 } }] });
describe('지형 격자 (schema 2)', () => {
const WITH_GRID = JSON.stringify({
schema: 2,
generated: '2026-10-23',
piers: [{ id: 'P1', ground: { center: 29.355, excel: 28.931 } }],
terrain_grid: { origin: [259815.676, 179088.094], step: 5, nx: 2, ny: 2, z: [28, null, 29, 30] },
});
it('reads the terrain grid as terrainGrid', () => {
const m = parseGroundModel(WITH_GRID);
expect(m.schema).toBe(2);
expect(m.terrainGrid).toEqual({ origin: [259815.676, 179088.094], step: 5, nx: 2, ny: 2, z: [28, null, 29, 30] });
});
it('a schema 1 file has no terrain grid', () => {
expect(parseGroundModel(LEGACY_SAMPLE).terrainGrid).toBeUndefined();
});
it('refuses a grid whose value count is not nx × ny', () => {
expect(() => parseGroundModel(WITH_GRID.replace('"z":[28,null,29,30]', '"z":[28,29]'))).toThrow('nx × ny');
});
it('refuses a grid value that is not a number or null', () => {
expect(() => parseGroundModel(WITH_GRID.replace('"z":[28,null,29,30]', '"z":[28,"x",29,30]'))).toThrow('숫자가 아닌');
});
it('refuses an unknown schema number', () => {
expect(() => parseGroundModel(WITH_GRID.replace('"schema":2', '"schema":3'))).toThrow('형식 번호 3');
});
it('keeps the grid in a saved project', () => {
const m = parseGroundModel(WITH_GRID);
const text = serializeProject({
schemaVersion: SCHEMA_VERSION,
project: { name: 'x', bridge: '운곡교', client: '', author: '', crs: 'KATEC', memo: '' },
source: { fileName: 'x.xlsx', sheets: [] },
piers: [{ id: 'P1', input: loadP1() }],
history: [],
groundModel: m,
});
expect(parseProject(text).groundModel?.terrainGrid).toEqual(m.terrainGrid);
});
});
describe('형식 번호 검사', () => {
it('refuses a file with no schema number (the B′ fallback is removed)', () => {
expect(() => parseGroundModel(JSON.stringify({ piers: [{ id: 'P1', ground: { center: 29.355, excel: 28.931 } }] }))).toThrow('형식 번호 undefined');
});
it('refuses a null schema number', () => {
expect(() => parseGroundModel('{"schema": null, "piers": []}')).toThrow('형식 번호 null');
});
it('refuses a pier that has no ground object', () => {
expect(() => parseGroundModel(JSON.stringify({ schema: 1, piers: [{ id: 'P2' }] }))).toThrow('P2: 지반 값(ground)이 없습니다');
});
});
describe('지반고 적용 (사용자가 누를 때만, 변경으로 기록)', () => {
const base = loadP1();
const pier: PierValues = { defaults: base, input: base, draft: { ...base, groundEl: 29.355 } };
it('an applied value counts as an override and can be reverted to the excel value', () => {
expect(overrideKeys(pier)).toContain('groundEl');
expect(revertDraft(pier, 'groundEl').groundEl).toBe(base.groundEl);
});
it('the draft and the generated input are compared, so the change waits for regeneration', () => {
expect(changedKeys(pier.input, pier.draft)).toEqual(['groundEl']);
});
});
describe('프로젝트 파일의 지반 모델', () => {
function project(extra: Partial<ProjectFile>): ProjectFile {
const p1 = loadP1();
return {
schemaVersion: SCHEMA_VERSION,
project: { name: 'x', bridge: '운곡교', client: '', author: '', crs: 'KATEC', memo: '' },
source: { fileName: 'x.xlsx', sheets: [] },
piers: [{ id: 'P1', input: p1 }],
history: [],
...extra,
};
}
it('keeps the imported ground model across save and open', () => {
const m = parseGroundModel(SAMPLE);
const reopened = parseProject(serializeProject(project({ groundModel: m })));
expect(reopened.groundModel).toEqual(m);
});
it('refuses a damaged ground model inside the project file', () => {
const text = serializeProject(project({})).replace('"history": []', '"history": [], "groundModel": {"schema": 1}');
expect(() => parseProject(text)).toThrow('교각 목록');
});
it('refuses a stored model whose pier values are not numbers or null', () => {
const m = parseGroundModel(SAMPLE);
const bad = { ...m, piers: [{ id: 'P1', centerEl: '29.3', excelEl: null }] };
const text = serializeProject(project({})).replace('"history": []', `"history": [], "groundModel": ${JSON.stringify(bad)}`);
expect(() => parseProject(text)).toThrow('P1의 값이 맞지 않습니다');
});
it('opens an older file that has no ground model', () => {
expect(parseProject(serializeProject(project({}))).groundModel).toBeUndefined();
});
});
describe('지반 모델의 지층 기둥 (soil.layers)', () => {
const WITH_SOIL = JSON.stringify({
schema: 2,
piers: [{
id: 'P1',
ground: { center: 29.355, excel: 28.931 },
soil: { layers: [{ name: '매립토', top_el: 29.355, bottom_el: 27.5 }, { name: '연암', top_el: 27.5, bottom_el: 10 }] },
}],
});
it('reads the layers as strata rows (name and bottom EL, top to bottom)', () => {
const m = parseGroundModel(WITH_SOIL);
expect(strataRowsFor(m, 'P1')).toEqual([{ name: '매립토', bottomEl: 27.5 }, { name: '연암', bottomEl: 10 }]);
expect(strataRowsFor(m, 'P9')).toBeUndefined();
});
it('a pier without a soil column has no layers', () => {
expect(strataRowsFor(parseGroundModel(SAMPLE), 'P1')).toBeUndefined();
});
it('refuses a soil layers entry that is not a list', () => {
const data = JSON.parse(WITH_SOIL);
data.piers[0].soil.layers = { x: 1 };
expect(() => parseGroundModel(JSON.stringify(data))).toThrow('지층(soil.layers) 형식');
});
it('keeps the layers in a saved project and refuses a damaged layer', () => {
const m = parseGroundModel(WITH_SOIL);
const text = serializeProject({
schemaVersion: SCHEMA_VERSION,
project: { name: 'x', bridge: '운곡교', client: '', author: '', crs: 'KATEC', memo: '' },
source: { fileName: 'x.xlsx', sheets: [] },
piers: [{ id: 'P1', input: loadP1() }],
history: [],
groundModel: m,
});
expect(strataRowsFor(parseProject(text).groundModel!, 'P1')).toEqual(strataRowsFor(m, 'P1'));
const bad = text.replace('"bottomEl": 10', '"bottomEl": "10"');
expect(() => parseProject(bad)).toThrow('지층 값이 맞지 않습니다');
});
});