- 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>
259 lines
12 KiB
TypeScript
259 lines
12 KiB
TypeScript
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']);
|
||
});
|
||
});
|