173 lines
8.6 KiB
TypeScript
173 lines
8.6 KiB
TypeScript
import type { PierModel } from './geometry';
|
||
import type { Issue, PierInput } from './types';
|
||
|
||
/** 위치 choices of the 지반·지층 card (시안 2026-10-26). */
|
||
export const GROUND_POSITIONS = ['육상', '하상', '해상'] as const;
|
||
|
||
/** 지층 names, in the order of the RAG strata list (지반모델링계획). */
|
||
export const STRATA_NAMES = ['매립토', '퇴적토', '풍화토', '풍화암', '연암', '경암'] as const;
|
||
|
||
/** The 지지층 for the pile tip check (지반 모델 G-7). */
|
||
export const SUPPORT_STRATUM = '연암';
|
||
|
||
/** 근입 = 이 값 × 말뚝 지름 (지반 모델 G-7, embed_d 1.0). */
|
||
export const PILE_EMBED_D = 1.0;
|
||
|
||
/** 해상 datum (승인 2026-10-26): mean sea level. */
|
||
export const SEA_DATUM = 'MSL';
|
||
|
||
/**
|
||
* 부식 여유 (승인 2026-10-26: 기준값으로 고정). PLACEHOLDER: the value is not in the RAG documents yet,
|
||
* so 1 mm is a reference to confirm against the design standard before use.
|
||
*/
|
||
export const CORROSION_ALLOWANCE_MM = 1;
|
||
|
||
/**
|
||
* 지반·지층 입력 검사 (승인 2026-10-26).
|
||
* G01: 층 하단 EL은 위 경계보다 낮아야 함. G02: 지하수위는 지반고와 가장 아래 층 사이에 있어야 함.
|
||
* G03: 층 이름은 지층 목록에 있어야 함. 하상: G07 DHWL 없음, G08 DHWL이 지반고보다 낮음, G09 세굴선 확인.
|
||
* Ground values are inputs only; they do not change the 3D model.
|
||
*/
|
||
export function validateGround(input: PierInput, issues: Issue[]): void {
|
||
const rows = input.strataRows ?? [];
|
||
let upper = input.groundEl;
|
||
rows.forEach((row, i) => {
|
||
const label = `지층 ${i + 1}행 (${row.name})`;
|
||
if (!(STRATA_NAMES as readonly string[]).includes(row.name)) {
|
||
issues.push({ code: 'G03', level: 'W', pier: input.name, message: `${label}: 층 이름이 지층 목록에 없습니다.` });
|
||
}
|
||
if (!(row.bottomEl < upper)) {
|
||
issues.push({ code: 'G01', level: 'W', pier: input.name, message: `${label}: 하단 EL ${row.bottomEl.toFixed(3)}이 위 경계 ${upper.toFixed(3)}보다 낮아야 합니다.` });
|
||
}
|
||
upper = row.bottomEl;
|
||
});
|
||
const position = input.groundPosition ?? '육상';
|
||
if (position === '하상') {
|
||
// 하상 (승인 2026-10-26): the 설계홍수위 DHWL replaces the groundwater level.
|
||
const f = input.floodLevelEl;
|
||
if (f === undefined || !Number.isFinite(f)) {
|
||
issues.push({ code: 'G07', level: 'W', pier: input.name, message: '하상입니다. 설계홍수위(DHWL) EL을 입력하세요.' });
|
||
} else if (f < input.groundEl) {
|
||
issues.push({ code: 'G08', level: 'W', pier: input.name, message: `설계홍수위 ${f.toFixed(3)}이 지반고 ${input.groundEl.toFixed(3)}보다 낮습니다.` });
|
||
}
|
||
validateScour(input, issues);
|
||
} else if (position === '해상') {
|
||
validateSea(input, issues);
|
||
validateScour(input, issues);
|
||
} else if (input.groundWaterOn) {
|
||
const w = input.groundWaterEl;
|
||
const lowest = rows.length ? rows[rows.length - 1].bottomEl : undefined;
|
||
if (w === undefined || !Number.isFinite(w)) {
|
||
issues.push({ code: 'G02', level: 'W', pier: input.name, message: '지하수위를 고려하지만 지하수위 EL이 없습니다.' });
|
||
} else if (w > input.groundEl || (lowest !== undefined && w < lowest)) {
|
||
issues.push({ code: 'G02', level: 'W', pier: input.name, message: `지하수위 EL ${w.toFixed(3)}이 지반고와 가장 아래 층 사이에 있지 않습니다.` });
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 말뚝 선단 근입 (승인 2026-10-26): every pile tip must be at least PILE_EMBED_D × D below the 연암 상면.
|
||
* G04: a tip above that limit. G05 (info): piles but no 연암 row, so the embedment is not checked.
|
||
* The 연암 상면 is the bottom of the row above the 연암 row (ground EL for the first row).
|
||
*/
|
||
export function validatePileEmbedment(input: PierInput, model: PierModel, issues: Issue[]): void {
|
||
if (model.piles.length === 0) return;
|
||
const rows = input.strataRows ?? [];
|
||
const k = rows.findIndex((r) => r.name === SUPPORT_STRATUM);
|
||
if (k < 0) {
|
||
// No layers entered yet: nothing to say. Layers entered but no 연암 row: say so.
|
||
if (rows.length > 0) {
|
||
issues.push({ code: 'G05', level: 'I', pier: input.name, message: '연암 행이 없어 말뚝 선단 근입을 확인하지 않았습니다.' });
|
||
}
|
||
return;
|
||
}
|
||
const top = k === 0 ? input.groundEl : rows[k - 1].bottomEl;
|
||
const deepest = Math.max(...model.piles.map((p) => p.bottom));
|
||
const diameter = Math.max(...model.piles.map((p) => p.diameter));
|
||
const limit = top - PILE_EMBED_D * diameter;
|
||
if (deepest > limit + 1e-9) {
|
||
issues.push({
|
||
code: 'G04',
|
||
level: 'W',
|
||
pier: input.name,
|
||
message: `말뚝 선단 EL ${deepest.toFixed(3)}이 연암 상면 ${top.toFixed(3)} − 근입 ${(PILE_EMBED_D * diameter).toFixed(3)} = ${limit.toFixed(3)}보다 높습니다.`,
|
||
});
|
||
}
|
||
}
|
||
|
||
/** 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)으로 적으세요.`,
|
||
});
|
||
}
|
||
});
|
||
}
|
||
|
||
/** G09: the 세굴 깊이 is not negative, and the scour line stays above the footing bottom (하상·해상). */
|
||
function validateScour(input: PierInput, issues: Issue[]): void {
|
||
const scour = input.scourDepthM;
|
||
if (scour === undefined) return;
|
||
if (!(scour >= 0)) {
|
||
issues.push({ code: 'G09', level: 'W', pier: input.name, message: '세굴 깊이는 0 이상이어야 합니다.' });
|
||
} else if (input.groundEl - scour < input.footingBottomEl) {
|
||
issues.push({ code: 'G09', level: 'W', pier: input.name, message: `세굴선 EL ${(input.groundEl - scour).toFixed(3)}이 확대기초 하면 ${input.footingBottomEl.toFixed(3)}보다 낮습니다 (기초 노출).` });
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 해상 (승인 2026-10-26, MSL 기준): 고조위와 저조위 are required, the 해저면 (groundEl) lies below the 고조위, and
|
||
* 저조위 is below 고조위. G13 missing; G10 해저면이 고조위보다 낮지 않음; G11 저조위가 고조위보다 낮지 않음;
|
||
* G14 강관 두께가 부식 여유 기준 이하. 파랑과 선박 하중은 이번 범위에서 제외.
|
||
*/
|
||
function validateSea(input: PierInput, issues: Issue[]): void {
|
||
const hw = input.highWaterEl;
|
||
const lw = input.lowWaterEl;
|
||
if (hw === undefined || lw === undefined || !Number.isFinite(hw) || !Number.isFinite(lw)) {
|
||
issues.push({ code: 'G13', level: 'W', pier: input.name, message: `해상입니다. 고조위(HWL)와 저조위(LWL)를 ${SEA_DATUM} 기준으로 입력하세요.` });
|
||
} else {
|
||
if (hw <= input.groundEl) {
|
||
issues.push({ code: 'G10', level: 'W', pier: input.name, message: `고조위 ${hw.toFixed(3)}이 해저면 ${input.groundEl.toFixed(3)}보다 높아야 합니다 (수심 확인).` });
|
||
}
|
||
if (lw >= hw) {
|
||
issues.push({ code: 'G11', level: 'W', pier: input.name, message: `저조위 ${lw.toFixed(3)}이 고조위 ${hw.toFixed(3)}보다 낮아야 합니다.` });
|
||
}
|
||
}
|
||
const thickness = input.pileThicknessMm ?? 12;
|
||
if (thickness <= CORROSION_ALLOWANCE_MM) {
|
||
issues.push({ code: 'G14', level: 'W', pier: input.name, message: `말뚝 두께 ${thickness} mm가 부식 여유 기준 ${CORROSION_ALLOWANCE_MM} mm 이하입니다 (기준값 확인 필요).` });
|
||
}
|
||
}
|