feat(ground): accept ground_model.json written before the schema number
- A file with no `schema` key is read as schema 1, but every pier must have a `ground` object, so a renamed field is refused instead of loading as empty. - `schema` null or any other number is still refused. - The project file keeps the normalised model and checks it with its own check (checkStoredGroundModel), so a saved model reopens. - Temporary: remove the no-schema branch once examples/out is regenerated. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c3606011b5
commit
be76425c8f
4 files changed
+453
-6
No files matched your search
@@ -25,21 +25,44 @@ function numOrNull(v: unknown): number | null {
|
||||
return typeof v === 'number' && Number.isFinite(v) ? v : null;
|
||||
}
|
||||
|
||||
/** Checks the shape of a parsed ground_model.json. Throws with the first problem named in Korean. */
|
||||
/**
|
||||
* Checks the shape of a parsed ground_model.json. Throws with the first problem named in Korean.
|
||||
* A file with no `schema` key is the layout written before the number was added: it is read as schema 1, and the
|
||||
* `ground` object is required so that a renamed field is refused instead of loading as empty values.
|
||||
* TODO: remove the no-schema branch once the 옛 파일 (examples/out) is regenerated with schema 1.
|
||||
*/
|
||||
export function checkGroundModel(raw: unknown): GroundModelFile {
|
||||
const obj = raw as Partial<GroundModelFile> | null;
|
||||
if (!obj || typeof obj !== 'object') throw new Error('지반 모델 파일 형식이 아닙니다.');
|
||||
if (obj.schema !== GROUND_MODEL_SCHEMA) {
|
||||
if (obj.schema !== undefined && obj.schema !== GROUND_MODEL_SCHEMA) {
|
||||
throw new Error(`지반 모델 형식 번호 ${String(obj.schema)}을(를) 읽을 수 없습니다 (지원: ${GROUND_MODEL_SCHEMA}).`);
|
||||
}
|
||||
if (!Array.isArray(obj.piers)) throw new Error('지반 모델 파일에 교각 목록(piers)이 없습니다.');
|
||||
const piers = obj.piers.map((p, i) => {
|
||||
const id = (p as { id?: unknown })?.id;
|
||||
if (typeof id !== 'string' || !id) throw new Error(`지반 모델 ${i + 1}번째 교각의 이름(id)이 없습니다.`);
|
||||
const ground = (p as { ground?: { center?: unknown; excel?: unknown } }).ground ?? {};
|
||||
const ground = (p as { ground?: { center?: unknown; excel?: unknown } }).ground;
|
||||
if (!ground || typeof ground !== 'object') throw new Error(`지반 모델 ${id}: 지반 값(ground)이 없습니다.`);
|
||||
return { id, centerEl: numOrNull(ground.center), excelEl: numOrNull(ground.excel) };
|
||||
});
|
||||
return { schema: obj.schema, generated: typeof obj.generated === 'string' ? obj.generated : '', piers };
|
||||
return { schema: GROUND_MODEL_SCHEMA, generated: typeof obj.generated === 'string' ? obj.generated : '', piers };
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the normalised model stored in a project file (the form checkGroundModel returns). The project keeps this
|
||||
* form, not the raw ground_model.json, so it is checked separately.
|
||||
*/
|
||||
export function checkStoredGroundModel(raw: unknown): GroundModelFile {
|
||||
const obj = raw as Partial<GroundModelFile> | null;
|
||||
if (!obj || typeof obj !== 'object' || obj.schema !== GROUND_MODEL_SCHEMA) {
|
||||
throw new Error('프로젝트 안의 지반 모델 형식 번호가 맞지 않습니다.');
|
||||
}
|
||||
if (!Array.isArray(obj.piers)) throw new Error('프로젝트 안의 지반 모델에 교각 목록이 없습니다.');
|
||||
for (const p of obj.piers as Partial<GroundModelPier>[]) {
|
||||
const ok = typeof p?.id === 'string' && (p.centerEl === null || typeof p.centerEl === 'number') && (p.excelEl === null || typeof p.excelEl === 'number');
|
||||
if (!ok) throw new Error(`프로젝트 안의 지반 모델 교각 ${String(p?.id)}의 값이 맞지 않습니다.`);
|
||||
}
|
||||
return obj as GroundModelFile;
|
||||
}
|
||||
|
||||
/** Parses the file text. Throws when it is not JSON or does not match the schema. */
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import type { FckByMember, Material } from './rebar/common';
|
||||
import { checkGroundModel, type GroundModelFile } from './ground-model';
|
||||
import { checkStoredGroundModel, type GroundModelFile } from './ground-model';
|
||||
import type { PierInput } from './types';
|
||||
|
||||
/** Project file schema (spec §8). Bump only with a migration. */
|
||||
@@ -71,7 +71,7 @@ export function parseProject(text: string): ProjectFile {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (obj.groundModel !== undefined) checkGroundModel(obj.groundModel);
|
||||
if (obj.groundModel !== undefined) checkStoredGroundModel(obj.groundModel);
|
||||
obj.piers.forEach((p, i) => {
|
||||
if (!p || typeof p.id !== 'string' || !p.input) throw new Error(`교각 ${i + 1}: id 또는 input이 없습니다.`);
|
||||
const input = p.input;
|
||||
|
||||
+393
@@ -0,0 +1,393 @@
|
||||
{
|
||||
"generated": "2026-10-23",
|
||||
"coord": "측량 X(북) · Y(동), m. 치수 mm",
|
||||
"terrain": {
|
||||
"file": "C0050202-001.dxf",
|
||||
"spot_blocks": 433,
|
||||
"spot_texts": 190,
|
||||
"contours": 67,
|
||||
"line3d_vertices": 0,
|
||||
"points_used": 3429,
|
||||
"by_source": {
|
||||
"spot": 431,
|
||||
"text": 5,
|
||||
"contour": 2993,
|
||||
"line3d": 0
|
||||
},
|
||||
"removed": 1,
|
||||
"boreholes": 18,
|
||||
"extent": [
|
||||
259779.6342279386,
|
||||
179066.3432863709,
|
||||
260208.0132279157,
|
||||
179261.3784074617
|
||||
]
|
||||
},
|
||||
"removed_points": [
|
||||
{
|
||||
"X": 260155.762358268,
|
||||
"Y": 179189.228890184,
|
||||
"Z": -18.115,
|
||||
"source": "spot",
|
||||
"reason": "EL 범위 밖 (0.0–200.0)"
|
||||
}
|
||||
],
|
||||
"boreholes_dxf": [
|
||||
{
|
||||
"id": "BB-24",
|
||||
"X": 260110.5200305235,
|
||||
"Y": 179142.87077588285
|
||||
},
|
||||
{
|
||||
"id": "BB-28",
|
||||
"X": 259832.7395636612,
|
||||
"Y": 179078.6772609205
|
||||
},
|
||||
{
|
||||
"id": "NBB-21",
|
||||
"X": 260169.7415555437,
|
||||
"Y": 179217.2592038325
|
||||
},
|
||||
{
|
||||
"id": "NBB-22",
|
||||
"X": 260119.0843895569,
|
||||
"Y": 179207.447663092
|
||||
},
|
||||
{
|
||||
"id": "NBB-23",
|
||||
"X": 260063.7209158014,
|
||||
"Y": 179200.7170868495
|
||||
},
|
||||
{
|
||||
"id": "NBB-24",
|
||||
"X": 260013.7365954409,
|
||||
"Y": 179193.2691158374
|
||||
},
|
||||
{
|
||||
"id": "NBB-25",
|
||||
"X": 259959.8230206913,
|
||||
"Y": 179182.585023065
|
||||
},
|
||||
{
|
||||
"id": "NBB-26",
|
||||
"X": 259908.1691879086,
|
||||
"Y": 179170.4141072329
|
||||
},
|
||||
{
|
||||
"id": "NBB-27",
|
||||
"X": 259862.0303893618,
|
||||
"Y": 179162.6491102049
|
||||
},
|
||||
{
|
||||
"id": "NBB-28",
|
||||
"X": 259804.6978413376,
|
||||
"Y": 179143.6022597326
|
||||
},
|
||||
{
|
||||
"id": "NBB-29",
|
||||
"X": 260162.3349768046,
|
||||
"Y": 179177.2095564581
|
||||
},
|
||||
{
|
||||
"id": "NBB-30",
|
||||
"X": 260115.11201831407,
|
||||
"Y": 179163.3224655297
|
||||
},
|
||||
{
|
||||
"id": "NBB-31",
|
||||
"X": 260052.92384203407,
|
||||
"Y": 179154.4466218247
|
||||
},
|
||||
{
|
||||
"id": "NBB-32",
|
||||
"X": 260004.4464295526,
|
||||
"Y": 179155.3905176037
|
||||
},
|
||||
{
|
||||
"id": "NBB-33",
|
||||
"X": 259953.9248062317,
|
||||
"Y": 179147.6435384383
|
||||
},
|
||||
{
|
||||
"id": "NBB-34",
|
||||
"X": 259903.7958026133,
|
||||
"Y": 179135.2767765673
|
||||
},
|
||||
{
|
||||
"id": "NBB-35",
|
||||
"X": 259855.9217242609,
|
||||
"Y": 179124.5131579286
|
||||
},
|
||||
{
|
||||
"id": "NBB-36",
|
||||
"X": 259804.9274191266,
|
||||
"Y": 179111.1866481418
|
||||
}
|
||||
],
|
||||
"borehole_issues": [],
|
||||
"piers": [
|
||||
{
|
||||
"id": "P1",
|
||||
"X": 259865.676,
|
||||
"Y": 179138.094,
|
||||
"axis_dir": [
|
||||
0.16722,
|
||||
-0.98592
|
||||
],
|
||||
"ground": {
|
||||
"center": 29.355,
|
||||
"corners": [
|
||||
29.536,
|
||||
29.441,
|
||||
29.18,
|
||||
29.27
|
||||
],
|
||||
"min": 29.18,
|
||||
"max": 29.536,
|
||||
"used": 29.355,
|
||||
"source": "계산",
|
||||
"dtm_spacing": 19.0,
|
||||
"excel": 28.931,
|
||||
"diff_vs_excel": 0.424
|
||||
},
|
||||
"footing": {
|
||||
"L_mm": 10400.0,
|
||||
"W_mm": 8150.0,
|
||||
"top_el": 28.208,
|
||||
"bottom_el": 26.208,
|
||||
"cover": 0.972
|
||||
},
|
||||
"pile": {
|
||||
"tip_el": 2.208,
|
||||
"d_mm": 609.6
|
||||
},
|
||||
"soil": null,
|
||||
"alerts": [
|
||||
{
|
||||
"code": "W09",
|
||||
"level": "경고",
|
||||
"pier": "P1",
|
||||
"message": "엑셀 지반고 28.931와 지형 29.355의 차이 +0.424 m가 허용 0.3 m를 넘습니다."
|
||||
},
|
||||
{
|
||||
"code": "I05",
|
||||
"level": "정보",
|
||||
"pier": "P1",
|
||||
"message": "시추 자료가 없어 지층을 판정하지 않았습니다."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "P2",
|
||||
"X": 259914.439,
|
||||
"Y": 179149.112,
|
||||
"axis_dir": [
|
||||
0.167151,
|
||||
-0.985931
|
||||
],
|
||||
"ground": {
|
||||
"center": 28.167,
|
||||
"corners": [],
|
||||
"min": 28.167,
|
||||
"max": 28.167,
|
||||
"used": 28.167,
|
||||
"source": "계산",
|
||||
"dtm_spacing": 19.5,
|
||||
"excel": null,
|
||||
"diff_vs_excel": null
|
||||
},
|
||||
"footing": {
|
||||
"L_mm": null,
|
||||
"W_mm": null,
|
||||
"top_el": null,
|
||||
"bottom_el": null,
|
||||
"cover": null
|
||||
},
|
||||
"pile": {
|
||||
"tip_el": null,
|
||||
"d_mm": null
|
||||
},
|
||||
"soil": null,
|
||||
"alerts": [
|
||||
{
|
||||
"code": "I05",
|
||||
"level": "정보",
|
||||
"pier": "P2",
|
||||
"message": "시추 자료가 없어 지층을 판정하지 않았습니다."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "P3",
|
||||
"X": 259963.44,
|
||||
"Y": 179159.017,
|
||||
"axis_dir": [
|
||||
0.167117,
|
||||
-0.985937
|
||||
],
|
||||
"ground": {
|
||||
"center": 28.12,
|
||||
"corners": [],
|
||||
"min": 28.12,
|
||||
"max": 28.12,
|
||||
"used": 28.12,
|
||||
"source": "계산",
|
||||
"dtm_spacing": 48.6,
|
||||
"excel": null,
|
||||
"diff_vs_excel": null
|
||||
},
|
||||
"footing": {
|
||||
"L_mm": null,
|
||||
"W_mm": null,
|
||||
"top_el": null,
|
||||
"bottom_el": null,
|
||||
"cover": null
|
||||
},
|
||||
"pile": {
|
||||
"tip_el": null,
|
||||
"d_mm": null
|
||||
},
|
||||
"soil": null,
|
||||
"alerts": [
|
||||
{
|
||||
"code": "I07",
|
||||
"level": "정보",
|
||||
"pier": "P3",
|
||||
"message": "지반고를 만든 지형 점이 최대 48.6 m 떨어져 있습니다 (기준 30 m). 현장 측량값을 확인하세요."
|
||||
},
|
||||
{
|
||||
"code": "I05",
|
||||
"level": "정보",
|
||||
"pier": "P3",
|
||||
"message": "시추 자료가 없어 지층을 판정하지 않았습니다."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "P4",
|
||||
"X": 260012.654,
|
||||
"Y": 179167.802,
|
||||
"axis_dir": [
|
||||
0.167117,
|
||||
-0.985937
|
||||
],
|
||||
"ground": {
|
||||
"center": 28.608,
|
||||
"corners": [],
|
||||
"min": 28.608,
|
||||
"max": 28.608,
|
||||
"used": 28.608,
|
||||
"source": "계산",
|
||||
"dtm_spacing": 44.3,
|
||||
"excel": null,
|
||||
"diff_vs_excel": null
|
||||
},
|
||||
"footing": {
|
||||
"L_mm": null,
|
||||
"W_mm": null,
|
||||
"top_el": null,
|
||||
"bottom_el": null,
|
||||
"cover": null
|
||||
},
|
||||
"pile": {
|
||||
"tip_el": null,
|
||||
"d_mm": null
|
||||
},
|
||||
"soil": null,
|
||||
"alerts": [
|
||||
{
|
||||
"code": "I07",
|
||||
"level": "정보",
|
||||
"pier": "P4",
|
||||
"message": "지반고를 만든 지형 점이 최대 44.3 m 떨어져 있습니다 (기준 30 m). 현장 측량값을 확인하세요."
|
||||
},
|
||||
{
|
||||
"code": "I05",
|
||||
"level": "정보",
|
||||
"pier": "P4",
|
||||
"message": "시추 자료가 없어 지층을 판정하지 않았습니다."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "P5",
|
||||
"X": 260062.055,
|
||||
"Y": 179175.464,
|
||||
"axis_dir": [
|
||||
0.1671,
|
||||
-0.98594
|
||||
],
|
||||
"ground": {
|
||||
"center": 27.242,
|
||||
"corners": [],
|
||||
"min": 27.242,
|
||||
"max": 27.242,
|
||||
"used": 27.242,
|
||||
"source": "계산",
|
||||
"dtm_spacing": 2.7,
|
||||
"excel": null,
|
||||
"diff_vs_excel": null
|
||||
},
|
||||
"footing": {
|
||||
"L_mm": null,
|
||||
"W_mm": null,
|
||||
"top_el": null,
|
||||
"bottom_el": null,
|
||||
"cover": null
|
||||
},
|
||||
"pile": {
|
||||
"tip_el": null,
|
||||
"d_mm": null
|
||||
},
|
||||
"soil": null,
|
||||
"alerts": [
|
||||
{
|
||||
"code": "I05",
|
||||
"level": "정보",
|
||||
"pier": "P5",
|
||||
"message": "시추 자료가 없어 지층을 판정하지 않았습니다."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "P6",
|
||||
"X": 260116.582,
|
||||
"Y": 179182.59,
|
||||
"axis_dir": [
|
||||
0.167062,
|
||||
-0.985946
|
||||
],
|
||||
"ground": {
|
||||
"center": 28.766,
|
||||
"corners": [],
|
||||
"min": 28.766,
|
||||
"max": 28.766,
|
||||
"used": 28.766,
|
||||
"source": "계산",
|
||||
"dtm_spacing": 14.3,
|
||||
"excel": null,
|
||||
"diff_vs_excel": null
|
||||
},
|
||||
"footing": {
|
||||
"L_mm": null,
|
||||
"W_mm": null,
|
||||
"top_el": null,
|
||||
"bottom_el": null,
|
||||
"cover": null
|
||||
},
|
||||
"pile": {
|
||||
"tip_el": null,
|
||||
"d_mm": null
|
||||
},
|
||||
"soil": null,
|
||||
"alerts": [
|
||||
{
|
||||
"code": "I05",
|
||||
"level": "정보",
|
||||
"pier": "P6",
|
||||
"message": "시추 자료가 없어 지층을 판정하지 않았습니다."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { groundElFor, parseGroundModel } from '../src/core/ground-model';
|
||||
import { changedKeys, overrideKeys, revertDraft, type PierValues } from '../src/core/changes';
|
||||
@@ -42,6 +43,29 @@ describe('지반 모델 읽기 (ground_model.json, schema 1)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('번호 없는 옛 지반 모델 (B′: 임시 경로)', () => {
|
||||
// examples/out/ground_model.json as written before the schema number existed (P1–P6, same values as the report).
|
||||
const LEGACY = readFileSync(new URL('./fixtures/ground-model-legacy.json', import.meta.url), 'utf-8');
|
||||
|
||||
it('reads the unversioned file and reports it as schema 1', () => {
|
||||
const m = parseGroundModel(LEGACY);
|
||||
expect(m.schema).toBe(1);
|
||||
expect(m.piers.map((p) => p.id)).toEqual(['P1', 'P2', 'P3', 'P4', 'P5', 'P6']);
|
||||
expect(groundElFor(m, 'P1')).toBe(29.355);
|
||||
expect(m.piers[0].excelEl).toBe(28.931);
|
||||
});
|
||||
|
||||
it('refuses a pier that has no ground object (a renamed field must not load as empty values)', () => {
|
||||
const data = JSON.parse(LEGACY);
|
||||
delete data.piers[1].ground;
|
||||
expect(() => parseGroundModel(JSON.stringify(data))).toThrow('P2: 지반 값(ground)이 없습니다');
|
||||
});
|
||||
|
||||
it('refuses a null schema number instead of treating it as missing', () => {
|
||||
expect(() => parseGroundModel(LEGACY.replace('{', '{"schema": null,'))).toThrow('형식 번호 null');
|
||||
});
|
||||
});
|
||||
|
||||
describe('지반고 적용 (사용자가 누를 때만, 변경으로 기록)', () => {
|
||||
const base = loadP1();
|
||||
const pier: PierValues = { defaults: base, input: base, draft: { ...base, groundEl: 29.355 } };
|
||||
@@ -80,6 +104,13 @@ describe('프로젝트 파일의 지반 모델', () => {
|
||||
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();
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user