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>
This commit is contained in:
ypshimandClaude Haiku 5.5 committed 2026-10-26 10:48:37 +09:00
1 parent 7dd59dfc6c
commit 7b5517e312
6 files changed
+100 -5

No files matched your search

+41
View File
@@ -71,3 +71,44 @@ export function validatePileEmbedment(input: PierInput, model: PierModel, issues
});
}
}
/** N값 as read from the text: a blow count, or a refusal "타수/관입량" with the penetration in mm. */
export type NValue = { kind: 'empty' } | { kind: 'n'; n: number } | { kind: 'refusal'; blows: number; mm: number } | { kind: 'invalid' };
/** Largest blow count and penetration accepted (표준관입시험 의 일반 범위; 연타 한계 50 blows / 300 mm). */
export const N_MAX = 100;
export const PENETRATION_MAX_MM = 300;
/** Reads an N value. "" is empty; "23" is 23 blows; "50/5" is 50 blows for 5 mm (관입량 mm). */
export function parseNValue(text: string | undefined): NValue {
const v = (text ?? '').trim();
if (v === '') return { kind: 'empty' };
if (/^\d+$/.test(v)) {
const n = Number(v);
return n <= N_MAX ? { kind: 'n', n } : { kind: 'invalid' };
}
const m = /^(\d+)\/(\d+)$/.exec(v);
if (m) {
const blows = Number(m[1]);
const mm = Number(m[2]);
if (blows >= 1 && blows <= N_MAX && mm >= 1 && mm <= PENETRATION_MAX_MM) return { kind: 'refusal', blows, mm };
}
return { kind: 'invalid' };
}
/**
* G06 (경고): an N value that is not a blow count or a refusal in the accepted range. Empty is allowed.
* The values are not used in the calculation yet; they will feed the 지지력 and 액상화 checks, so the format is strict.
*/
export function validateStrataNValues(input: PierInput, issues: Issue[]): void {
(input.strataRows ?? []).forEach((row, i) => {
if (parseNValue(row.nValue).kind === 'invalid') {
issues.push({
code: 'G06',
level: 'W',
pier: input.name,
message: `지층 ${i + 1}행 (${row.name}) N값 "${row.nValue}"의 형식이 맞지 않습니다. 타수(0–${N_MAX}) 또는 "타수/관입량 mm"(예: 50/5)으로 적으세요.`,
});
}
});
}
+5
View File
@@ -75,6 +75,11 @@ export function parseProject(text: string): ProjectFile {
obj.piers.forEach((p, i) => {
if (!p || typeof p.id !== 'string' || !p.input) throw new Error(`교각 ${i + 1}: id 또는 input이 없습니다.`);
const input = p.input;
const rows = input.strataRows;
if (rows !== undefined) {
const ok = Array.isArray(rows) && rows.every((r) => typeof r?.name === 'string' && typeof r?.bottomEl === 'number' && Number.isFinite(r.bottomEl) && (r.nValue === undefined || typeof r.nValue === 'string'));
if (!ok) throw new Error(`교각 ${input.name}: 지층 행 형식이 맞지 않습니다.`);
}
if (typeof input.name !== 'string' || !Array.isArray(input.ips) || !Array.isArray(input.bearings)) {
throw new Error(`교각 ${i + 1}(${String(input.name)}): 입력 형식이 맞지 않습니다.`);
}
+2
View File
@@ -70,6 +70,8 @@ export interface StrataRow {
name: string;
/** 이 층의 하단 EL (m). */
bottomEl: number;
/** N값 as written: "23" (blows) or "50/5" (50 blows for 5 mm, 관입량 mm). Absent = not known yet. */
nValue?: string;
}
export type IssueLevel = 'E' | 'W' | 'I';
+2 -1
View File
@@ -1,6 +1,6 @@
import { DIM, type PierModel } from './geometry';
import type { Issue, PierInput, PierType } from './types';
import { validateGround, validatePileEmbedment } from './ground';
import { validateGround, validatePileEmbedment, validateStrataNValues } from './ground';
const EXPECTED_COLUMNS: Record<PierType, number> = { 'T형': 1, '2주형': 2, '3주형': 3 };
@@ -92,4 +92,5 @@ export function validatePier(input: PierInput, model: PierModel, issues: Issue[]
}
validateGround(input, issues);
validatePileEmbedment(input, model, issues);
validateStrataNValues(input, issues);
}