feat(abut): 교대 지반·지층 카드 — 교각과 같은 위치 · 수위 · 지층 행 입력, 교대 단면 삽도, 검사
- 위치 (육상 · 하상 · 해상)와 위치별 수위 입력, 지층 행 표 (교각과 공용 strataRowsEditor), 지반 모델 지층 불러오기 - 검사: 교각과 같은 G01–G14, 말뚝 근입 G04, 지반고 < 저판 하면이면 정보 AI01 - 삽도: 지층 · 수위 띠를 교각과 공용으로 빼고, 교대축 직각 단면(저판 · 벽체 · 흉벽 · 말뚝 열)을 EL. 표기로 그림 - 자동 값: 지반고 (엑셀 입력), 연암층 상면 EL, 지층 수. 프로젝트 파일 저장 · 검증 Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9835fb42b3
commit
59c2607418
12 files changed
+527
-135
No files matched your search
+197
-62
@@ -25,7 +25,9 @@ import {
|
||||
applySlabEdit,
|
||||
setSlabForm,
|
||||
SLAB_EDIT_FIELDS,
|
||||
setAbutGround,
|
||||
} from './core/abut/edit';
|
||||
import { abutGroundIssues, supportTopEl, type AbutGround } from './core/abut/ground';
|
||||
import { ABUT_SLAB_DEFAULT, resolveSlab, SLAB_FORM_LABEL, slabGradePct, slabSection, slabSlopeLabel, type SlabForm } from './core/abut/slab';
|
||||
import { abutSlabPlanSvg, abutSlabSectionSvg } from './view/abut-slab-svg';
|
||||
import { ABUT_SHAPE_DEFAULT, footingEnds, normalizeShape, pileRowsMm, resolveShape, wallThicknessMm, type AbutShape } from './core/abut/shape';
|
||||
@@ -66,7 +68,7 @@ import { buildRebar3D, type RebarColorMode, type RebarScene } from './view/rebar
|
||||
import { DEFAULT_MATERIAL, FCK_MEMBERS, fckChanged, fyOf, GRADE_CHOICES, IMPORTED_FCK, isAssumedMaterial, materialFrom, type BendFactors, type FckByMember, type FckMember, type GradeKey, type Material } from './core/rebar/common';
|
||||
import { buildQuantity, quantityRows, type Quantity, type QuantityLine } from './core/rebar/quantity';
|
||||
import { groundElFor, parseGroundModel, strataRowsFor, type GroundMapLayer, type GroundModelFile } from './core/ground-model';
|
||||
import { CORROSION_ALLOWANCE_MM, parseNValue, SEA_DATUM, STRATA_NAMES } from './core/ground';
|
||||
import { CORROSION_ALLOWANCE_MM, GROUND_POSITIONS, parseNValue, SEA_DATUM, STRATA_NAMES } from './core/ground';
|
||||
import { bearingEdgeDistances, integratedCheck, PAIR_ECCENTRICITY_LIMIT_MM, pairEccentricities } from './core/bearing-layout';
|
||||
import { bearingPlanSvg, PAIR_CHIP_LABELS } from './view/bearing-plan-svg';
|
||||
import { bearingSectionSvg } from './view/bearing-section-svg';
|
||||
@@ -86,7 +88,7 @@ import { footingCornerSvg } from './view/footing-corner-svg';
|
||||
import { footingLegRows } from './core/rebar/footing-legs';
|
||||
import { columnElevationSvg } from './view/column-elevation-svg';
|
||||
import { type ColumnSection, columnSectionSvg } from './view/column-section-svg';
|
||||
import { groundSectionSvg } from './view/ground-svg';
|
||||
import { abutGroundSectionSvg, groundSectionSvg } from './view/ground-svg';
|
||||
import { columnDiameterIssues } from './core/column-check';
|
||||
import { findPileSpec, PHC_PILES, STEEL_PILES } from './core/pile-spec';
|
||||
import { footingPlanSvg, footingSideSvg } from './view/footing-svg';
|
||||
@@ -742,7 +744,7 @@ const ABUT_QA_NOTES = [
|
||||
const ABUT_SLOTS = {
|
||||
shape: { root: 'abut-panel', name: 'abut-panel-name', input: 'abut-panel-input', toggle: 'abut-panel-edit-toggle', editor: 'abut-panel-editor', summary: 'abut-panel-summary', basic: 'abut-panel-basic', bearings: 'abut-panel-bearings', shape: 'abut-panel-shape', msg: 'abut-panel-msg', prefix: 'abs' },
|
||||
// ② has no 편집 toggle and no basic items: the card sits under the 3D view and shows the picked member (2026-11-03).
|
||||
plan: { wing: 'abut-panel-plan-wing', slab: 'abut-panel-plan-slab', footing: 'abut-panel-plan-footing', root: 'abut-panel-plan', name: 'abut-panel-plan-name', bearings: 'abut-panel-plan-bearings', shape: 'abut-panel-plan-shape', msg: 'abut-panel-plan-msg', prefix: 'abp' },
|
||||
plan: { wing: 'abut-panel-plan-wing', slab: 'abut-panel-plan-slab', ground: 'abut-panel-plan-ground', footing: 'abut-panel-plan-footing', root: 'abut-panel-plan', name: 'abut-panel-plan-name', bearings: 'abut-panel-plan-bearings', shape: 'abut-panel-plan-shape', msg: 'abut-panel-plan-msg', prefix: 'abp' },
|
||||
} as const;
|
||||
|
||||
/** Shows the 교대 입력 in place of the pier info when an abutment is selected (2026-11-03). */
|
||||
@@ -1340,6 +1342,122 @@ function fillAbutSlabCard(
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* ② 지반·지층 카드 (교대, 2026-11-04: 교각 카드와 같은 입력): 위치 → 위치별 수위 입력 → 단면 삽도 → 자동 값 → 지층 행 →
|
||||
* 지반 모델 → 검사 → 메모. 표시만 하고 형상 계산에는 쓰지 않는다. 지반고는 ①에서 고친다.
|
||||
*/
|
||||
function fillAbutGroundCard(
|
||||
box: HTMLDivElement,
|
||||
rec: AbutRecord,
|
||||
apply: (next: AbutRecord) => void,
|
||||
fail: (err: unknown, input: HTMLInputElement, back: string) => void,
|
||||
idOf: (suffix: string) => string,
|
||||
numberInput: (id: string, value: string) => HTMLInputElement,
|
||||
): void {
|
||||
const g: AbutGround = rec.ground ?? {};
|
||||
const position = g.groundPosition ?? '육상';
|
||||
const set = (patch: Partial<AbutGround>) => apply(setAbutGround(rec, patch));
|
||||
box.append(
|
||||
optionTiles(
|
||||
'위치',
|
||||
GROUND_POSITIONS.map((p) => ({ label: p, on: position === p, isDefault: p === '육상', pick: () => position !== p && set({ groundPosition: p }) })),
|
||||
'표시만 합니다 (형상 계산에 쓰지 않음)',
|
||||
),
|
||||
);
|
||||
// 위치별 수위 입력 (교각과 같음): 육상 지하수위, 하상 설계홍수위 · 세굴, 해상 고조위 · 저조위 · 세굴. 비우면 값 없음.
|
||||
const level = (key: 'groundWaterEl' | 'floodLevelEl' | 'scourDepthM' | 'highWaterEl' | 'lowWaterEl', label: string, note: string, disabled = false) => {
|
||||
const id = idOf(`ground-${key}`);
|
||||
const lab = el('label', label);
|
||||
lab.htmlFor = id;
|
||||
const v = g[key];
|
||||
const input = numberInput(id, v === undefined ? '' : String(v));
|
||||
input.disabled = disabled;
|
||||
input.addEventListener('change', () => {
|
||||
try {
|
||||
set({ [key]: input.value === '' ? undefined : Number(input.value) });
|
||||
} catch (err) {
|
||||
fail(err, input, v === undefined ? '' : String(v));
|
||||
}
|
||||
});
|
||||
box.append(lab, input, el('span', note, 'muted small'));
|
||||
};
|
||||
if (position === '하상') {
|
||||
level('floodLevelEl', '설계홍수위 DHWL EL (m)', '하상');
|
||||
level('scourDepthM', '세굴 깊이 (m)', '지반고 아래');
|
||||
} else if (position === '해상') {
|
||||
level('highWaterEl', '고조위 HWL (m)', `기준면 ${SEA_DATUM}`);
|
||||
level('lowWaterEl', '저조위 LWL (m)', `기준면 ${SEA_DATUM}`);
|
||||
level('scourDepthM', '세굴 깊이 (m)', `지반고 칸 = 해저면 EL · 부식 여유 ${CORROSION_ALLOWANCE_MM} mm (확인 필요)`);
|
||||
} else {
|
||||
const id = idOf('ground-water-on');
|
||||
const check = el('input');
|
||||
check.type = 'checkbox';
|
||||
check.id = id;
|
||||
check.checked = g.groundWaterOn === true;
|
||||
check.addEventListener('change', () => set({ groundWaterOn: check.checked }));
|
||||
const lab = el('label', '지하수위 고려');
|
||||
lab.htmlFor = id;
|
||||
box.append(lab, check, el('span', '', 'muted small'));
|
||||
level('groundWaterEl', '지하수위 EL (m)', g.groundWaterOn ? '' : '지하수위 고려를 켜면 입력', g.groundWaterOn !== true);
|
||||
}
|
||||
let members: AbutMembers | undefined;
|
||||
try {
|
||||
const model = buildAbut(toAbutInput(rec));
|
||||
members = buildMembers(model, { bottomEl: rec.foundationBottomEl, pileLengthMm: rec.pileLengthMm }, rec.shape);
|
||||
} catch {
|
||||
members = undefined;
|
||||
}
|
||||
if (members && members.footingBottomEl !== undefined) {
|
||||
const s = members.shape;
|
||||
const fig = el('figure', undefined, 'ground-fig');
|
||||
fig.innerHTML = abutGroundSectionSvg({
|
||||
ground: { ...g, groundEl: rec.groundEl },
|
||||
footingTopEl: members.footingTopEl,
|
||||
footingBottomEl: members.footingBottomEl,
|
||||
seatEl: rec.seatEl,
|
||||
parapetTopEl: members.parapetTopStartEl,
|
||||
bracketHeightM: s.bracketHeightM,
|
||||
wallFrontM: s.wallFrontM,
|
||||
wallBackM: s.wallBackM,
|
||||
parapetBackM: s.parapetBackM,
|
||||
footingFrontM: s.footingFrontM,
|
||||
footingBackM: s.footingBackM,
|
||||
pileRowsT: members.piles.map((r) => r.t),
|
||||
pileBottomEl: members.pileBottomEl,
|
||||
});
|
||||
fig.append(el('figcaption', '단면 · 교대축 직각 · 저판 · 벽체 · 흉벽 · 말뚝은 생성본, 지층과 수위는 입력값 · 세로는 EL 축척, 가로는 모식', 'muted small'));
|
||||
box.append(fig);
|
||||
}
|
||||
const rows = g.strataRows ?? [];
|
||||
const top = supportTopEl(rec.groundEl, rows);
|
||||
const auto = (name: string, value: string, note: string) => box.append(el('label', name), el('span', value, 'num'), el('span', note, 'muted small'));
|
||||
auto('지반고 EL (엑셀 입력)', `EL.${rec.groundEl.toFixed(3)}`, '① 기본 정보에서 수정');
|
||||
auto('연암층 상면 EL (자동)', top === undefined ? '연암 행 없음' : `EL.${top.toFixed(3)}`, '지층 행에서');
|
||||
auto('지층 수 (자동)', `${rows.length}`, '지층 행에서');
|
||||
box.append(...strataRowsEditor(rows, rec.groundEl, (next) => set({ strataRows: next.length ? next : undefined })));
|
||||
// 지반 모델의 지층 (ground_model.json에 이 교대가 있을 때): one click fills the rows.
|
||||
if (groundModel) {
|
||||
const modelRows = strataRowsFor(groundModel, rec.name);
|
||||
if (modelRows) {
|
||||
const fromModel = el('button', `지반 모델의 지층 ${modelRows.length}개 불러오기`, 'btn small');
|
||||
fromModel.type = 'button';
|
||||
fromModel.addEventListener('click', () => set({ strataRows: modelRows.map((r) => ({ ...r })) }));
|
||||
box.append(fromModel);
|
||||
} else {
|
||||
box.append(el('p', `지반 모델에 ${rec.name}의 지층이 없습니다.`, 'muted small'));
|
||||
}
|
||||
}
|
||||
const issues = abutGroundIssues(rec, members);
|
||||
box.append(el('p', `검사 · ${issues.length ? issues.map((i) => `${i.code} ${i.message}`).join(' · ') : '경고 없음'}`, 'muted small abut-checks'));
|
||||
box.append(
|
||||
el(
|
||||
'p',
|
||||
'시안 메모 · 교각과 같은 카드입니다. 위치 · 수위 · 지층은 표시와 검사에만 쓰고 형상 계산에는 쓰지 않습니다(3D는 지반면만). 연암 행이 있으면 말뚝 선단 근입(연암 상면 − 1.0 D)을 검사합니다(G04). 지반고가 저판 하면보다 낮으면 흙덮개가 없다는 정보(AI01)를 표시합니다. 다른 구조물에 적용하기는 교대 속성 복사 범위 밖이라 두지 않았습니다.',
|
||||
'muted small abut-memo',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/** 확대기초 · 말뚝 카드의 자동 값, 열 위치, 열별 본수 표(− / +), 메모 (시안, 2026-11-04). 말뚝 길이는 ①에서 고친다. */
|
||||
function fillAbutFootingExtras(
|
||||
box: HTMLDivElement,
|
||||
@@ -1436,6 +1554,7 @@ function applyAbutMemberCard(rec: AbutRecord): void {
|
||||
$<HTMLElement>('abut-panel-plan-footing').hidden = member !== 'footing';
|
||||
$<HTMLElement>('abut-panel-plan-wing').hidden = member !== 'wing';
|
||||
$<HTMLElement>('abut-panel-plan-slab').hidden = member !== 'slab';
|
||||
$<HTMLElement>('abut-panel-plan-ground').hidden = member !== 'ground';
|
||||
// The card title is the picked row; its 요약 follows the name (시안 머리줄).
|
||||
const slabWidth = abutSummaryRows(rec).find(([label]) => label === '접속슬래브 폭')?.[1] ?? '—';
|
||||
const row = abutMemberRows(rec, slabWidth).find((r) => r.id === member);
|
||||
@@ -1521,6 +1640,11 @@ function fillAbutSlot(slot: (typeof ABUT_SLOTS)[keyof typeof ABUT_SLOTS], rec: A
|
||||
slabBox.replaceChildren();
|
||||
fillAbutSlabCard(slabBox, rec, apply, fail, idOf, numberInput);
|
||||
}
|
||||
if ('ground' in slot) {
|
||||
const groundBox = $<HTMLDivElement>(slot.ground);
|
||||
groundBox.replaceChildren();
|
||||
fillAbutGroundCard(groundBox, rec, apply, fail, idOf, numberInput);
|
||||
}
|
||||
for (const side of planCard ? [] : (['left', 'right'] as const)) {
|
||||
const id = idOf(`wing-${side}`);
|
||||
const lab = el('label', side === 'left' ? '좌 날개벽 (① 쪽)' : '우 날개벽 (n 쪽)');
|
||||
@@ -3430,6 +3554,75 @@ function setStrataRows(name: string, rows: StrataRow[]): void {
|
||||
renderAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* 지층 행 표 (승인 2026-10-26; 2026-11-04 교각 · 교대 공용): one row per layer from the top down (지층 · 하단 EL · N값 · 삭제) and
|
||||
* the 행 추가 button. `set` receives the new rows.
|
||||
*/
|
||||
function strataRowsEditor(rows: StrataRow[], groundEl: number, set: (rows: StrataRow[]) => void): HTMLElement[] {
|
||||
const out: HTMLElement[] = [];
|
||||
out.push(el('p', `지층 행 (위에서 아래 · 하단 EL m · 지반고 ${groundEl.toFixed(3)} 아래)`, 'lbl'));
|
||||
const table = el('table', undefined, 'grid-table rebar-table');
|
||||
const head = el('thead');
|
||||
const hr = el('tr');
|
||||
for (const h of ['지층', '하단 EL (m)', 'N값 (타수 · 타수/관입량 mm)', '']) hr.appendChild(el('th', h));
|
||||
head.appendChild(hr);
|
||||
table.appendChild(head);
|
||||
const body = el('tbody');
|
||||
rows.forEach((row, i) => {
|
||||
const tr = el('tr');
|
||||
const select = el('select');
|
||||
for (const s of STRATA_NAMES) {
|
||||
const o = el('option', s);
|
||||
o.value = s;
|
||||
select.appendChild(o);
|
||||
}
|
||||
if (!(STRATA_NAMES as readonly string[]).includes(row.name)) {
|
||||
const o = el('option', row.name);
|
||||
o.value = row.name;
|
||||
select.appendChild(o);
|
||||
}
|
||||
select.value = row.name;
|
||||
select.addEventListener('change', () => set(rows.map((r, j) => (j === i ? { ...r, name: select.value } : r))));
|
||||
const bottomInput = el('input');
|
||||
bottomInput.type = 'number';
|
||||
bottomInput.step = '0.001';
|
||||
bottomInput.value = String(row.bottomEl);
|
||||
bottomInput.addEventListener('change', () => set(rows.map((r, j) => (j === i ? { ...r, bottomEl: Number(bottomInput.value) } : r))));
|
||||
// N값 as text: a blow count or "타수/관입량 mm" (승인 2026-10-26). Empty means not known yet.
|
||||
const nInput = el('input');
|
||||
nInput.type = 'text';
|
||||
nInput.placeholder = '예: 23 · 50/5';
|
||||
nInput.value = row.nValue ?? '';
|
||||
nInput.addEventListener('change', () => {
|
||||
const v = nInput.value.trim();
|
||||
set(rows.map((r, j) => (j === i ? { ...r, nValue: v === '' ? undefined : v } : r)));
|
||||
});
|
||||
if (parseNValue(row.nValue).kind === 'invalid') nInput.classList.add('warn-text');
|
||||
const del = el('button', '삭제', 'btn small');
|
||||
del.type = 'button';
|
||||
del.addEventListener('click', () => set(rows.filter((_, j) => j !== i)));
|
||||
tr.append(...[select, bottomInput, nInput, del].map((x) => {
|
||||
const td = el('td');
|
||||
td.appendChild(x);
|
||||
return td;
|
||||
}));
|
||||
body.appendChild(tr);
|
||||
});
|
||||
table.appendChild(body);
|
||||
const scroll = el('div', undefined, 'table-scroll');
|
||||
scroll.appendChild(table);
|
||||
out.push(scroll);
|
||||
const add = el('button', '지층 행 추가', 'btn small');
|
||||
add.type = 'button';
|
||||
add.addEventListener('click', () => {
|
||||
const last = rows[rows.length - 1];
|
||||
const next = last ? (STRATA_NAMES as readonly string[])[Math.min((STRATA_NAMES as readonly string[]).indexOf(last.name) + 1, STRATA_NAMES.length - 1)] : STRATA_NAMES[0];
|
||||
set([...rows, { name: next, bottomEl: Number(((last?.bottomEl ?? groundEl) - 2).toFixed(3)) }]);
|
||||
});
|
||||
out.push(add);
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* ② 지반·지층 card (시안 2026-10-26; 승인 2026-10-26: 지반고는 계산에 쓰고, 지층은 행으로 입력하며, 지하수위는 직접 입력).
|
||||
* The ground model import and the 지반고 apply stay at the bottom, as before.
|
||||
@@ -3506,65 +3699,7 @@ function renderGroundModel(box: HTMLDivElement, pier: Pier): void {
|
||||
}
|
||||
|
||||
// 지층 행 (승인 2026-10-26): one row per layer, from the top down; the EL is the layer's bottom EL.
|
||||
const rows = d.strataRows ?? [];
|
||||
wrap.appendChild(el('p', `지층 행 (위에서 아래 · 하단 EL m · 지반고 ${d.groundEl.toFixed(3)} 아래)`, 'lbl'));
|
||||
const table = el('table', undefined, 'grid-table rebar-table');
|
||||
const head = el('thead');
|
||||
const hr = el('tr');
|
||||
for (const h of ['지층', '하단 EL (m)', 'N값 (타수 · 타수/관입량 mm)', '']) hr.appendChild(el('th', h));
|
||||
head.appendChild(hr);
|
||||
table.appendChild(head);
|
||||
const body = el('tbody');
|
||||
rows.forEach((row, i) => {
|
||||
const tr = el('tr');
|
||||
const select = el('select');
|
||||
for (const s of STRATA_NAMES) {
|
||||
const o = el('option', s);
|
||||
o.value = s;
|
||||
select.appendChild(o);
|
||||
}
|
||||
if (!(STRATA_NAMES as readonly string[]).includes(row.name)) {
|
||||
const o = el('option', row.name);
|
||||
o.value = row.name;
|
||||
select.appendChild(o);
|
||||
}
|
||||
select.value = row.name;
|
||||
select.addEventListener('change', () => setStrataRows(name, rows.map((r, j) => (j === i ? { ...r, name: select.value } : r))));
|
||||
const bottomInput = el('input');
|
||||
bottomInput.type = 'number';
|
||||
bottomInput.step = '0.001';
|
||||
bottomInput.value = String(row.bottomEl);
|
||||
bottomInput.addEventListener('change', () => setStrataRows(name, rows.map((r, j) => (j === i ? { ...r, bottomEl: Number(bottomInput.value) } : r))));
|
||||
// N값 as text: a blow count or "타수/관입량 mm" (승인 2026-10-26). Empty means not known yet.
|
||||
const nInput = el('input');
|
||||
nInput.type = 'text';
|
||||
nInput.placeholder = '예: 23 · 50/5';
|
||||
nInput.value = row.nValue ?? '';
|
||||
nInput.addEventListener('change', () => {
|
||||
const v = nInput.value.trim();
|
||||
setStrataRows(name, rows.map((r, j) => (j === i ? { ...r, nValue: v === '' ? undefined : v } : r)));
|
||||
});
|
||||
if (parseNValue(row.nValue).kind === 'invalid') nInput.classList.add('warn-text');
|
||||
const del = el('button', '삭제', 'btn small');
|
||||
del.type = 'button';
|
||||
del.addEventListener('click', () => setStrataRows(name, rows.filter((_, j) => j !== i)));
|
||||
tr.append(...[select, bottomInput, nInput, del].map((x) => {
|
||||
const td = el('td');
|
||||
td.appendChild(x);
|
||||
return td;
|
||||
}));
|
||||
body.appendChild(tr);
|
||||
});
|
||||
table.appendChild(body);
|
||||
wrap.appendChild(el('div', undefined, 'table-scroll')).appendChild(table);
|
||||
const add = el('button', '지층 행 추가', 'btn small');
|
||||
add.type = 'button';
|
||||
add.addEventListener('click', () => {
|
||||
const last = rows[rows.length - 1];
|
||||
const next = last ? (STRATA_NAMES as readonly string[])[Math.min((STRATA_NAMES as readonly string[]).indexOf(last.name) + 1, STRATA_NAMES.length - 1)] : STRATA_NAMES[0];
|
||||
setStrataRows(name, [...rows, { name: next, bottomEl: Number(((last?.bottomEl ?? d.groundEl) - 2).toFixed(3)) }]);
|
||||
});
|
||||
wrap.appendChild(add);
|
||||
wrap.append(...strataRowsEditor(d.strataRows ?? [], d.groundEl, (rows) => setStrataRows(name, rows)));
|
||||
// 지반 모델의 지층 기둥 (Python, 시추 자료가 있을 때): one click fills the rows (승인 2026-10-26).
|
||||
if (groundModel) {
|
||||
const modelRows = strataRowsFor(groundModel, name);
|
||||
|
||||
Reference in new issue
Block a user