feat(abut): 벽체 배근 입력 저장 · 교대 벽체 등 fck(35)
- AbutRecord.rebar.wall (바꾼 값만), applyWallRebar, 프로젝트 파일 검증 - 교대 벽체 · 흉벽 · 날개벽 · 접속슬래브 fck(abutBody, 기본 35)를 ② 벽체·흉벽 카드에서 입력. 저판 fck 행과 같은 함수 Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1da8dd74e2
commit
66e7d4d90d
7 files changed
+59
-20
No files matched your search
@@ -9,6 +9,7 @@ import { ABUT_SHAPE_DEFAULT, normalizeShape, shapeIssues, type AbutShape } from
|
|||||||
import { resolveSlab, slabIssues, type AbutSlab, type SlabForm } from './slab';
|
import { resolveSlab, slabIssues, type AbutSlab, type SlabForm } from './slab';
|
||||||
import type { AbutGround } from './ground';
|
import type { AbutGround } from './ground';
|
||||||
import { footingRebarIssues, resolveFootingRebar, type AbutFootingRebarSettings } from './rebar/footing';
|
import { footingRebarIssues, resolveFootingRebar, type AbutFootingRebarSettings } from './rebar/footing';
|
||||||
|
import { resolveWallRebar, wallRebarIssues, type AbutWallRebarSettings } from './rebar/wall';
|
||||||
import { abutBearingRuleIssues, resolveBearingRule, type AbutBearingRule } from './bearing-table';
|
import { abutBearingRuleIssues, resolveBearingRule, type AbutBearingRule } from './bearing-table';
|
||||||
|
|
||||||
export type AbutEditKey = 'seatEl' | 'foundationBottomEl' | 'groundEl' | 'pileLengthMm' | 'skewDeg' | 'slopeI';
|
export type AbutEditKey = 'seatEl' | 'foundationBottomEl' | 'groundEl' | 'pileLengthMm' | 'skewDeg' | 'slopeI';
|
||||||
@@ -178,6 +179,16 @@ export function applyFootingRebar(rec: AbutRecord, patch: Partial<AbutFootingReb
|
|||||||
return { ...rec, rebar: { ...(rec.rebar ?? {}), footing } };
|
return { ...rec, rebar: { ...(rec.rebar ?? {}), footing } };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Changes ③ 벽체 배근 values (only the changed ones are kept; undefined clears one), refused with a Korean message. */
|
||||||
|
export function applyWallRebar(rec: AbutRecord, patch: Partial<AbutWallRebarSettings>): AbutRecord {
|
||||||
|
for (const v of Object.values(patch)) if (typeof v === 'number' && !Number.isFinite(v)) throw new Error('숫자를 입력하세요.');
|
||||||
|
const wall: Partial<AbutWallRebarSettings> = { ...(rec.rebar?.wall ?? {}), ...patch };
|
||||||
|
for (const key of Object.keys(wall) as (keyof AbutWallRebarSettings)[]) if (wall[key] === undefined) delete wall[key];
|
||||||
|
const issues = wallRebarIssues(resolveWallRebar(wall));
|
||||||
|
if (issues.length > 0) throw new Error(issues[0]);
|
||||||
|
return { ...rec, rebar: { ...(rec.rebar ?? {}), wall } };
|
||||||
|
}
|
||||||
|
|
||||||
/** Changes one 접속슬래브 dim, refused with a Korean message when the slab would not build. */
|
/** Changes one 접속슬래브 dim, refused with a Korean message when the slab would not build. */
|
||||||
export function applySlabEdit(rec: AbutRecord, key: (typeof SLAB_EDIT_FIELDS)[number]['key'], value: number): AbutRecord {
|
export function applySlabEdit(rec: AbutRecord, key: (typeof SLAB_EDIT_FIELDS)[number]['key'], value: number): AbutRecord {
|
||||||
if (!Number.isFinite(value)) throw new Error('숫자를 입력하세요.');
|
if (!Number.isFinite(value)) throw new Error('숫자를 입력하세요.');
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import type { WingDims } from './wings';
|
|||||||
import type { AbutSlab } from './slab';
|
import type { AbutSlab } from './slab';
|
||||||
import type { AbutGround } from './ground';
|
import type { AbutGround } from './ground';
|
||||||
import type { AbutFootingRebarSettings } from './rebar/footing';
|
import type { AbutFootingRebarSettings } from './rebar/footing';
|
||||||
|
import type { AbutWallRebarSettings } from './rebar/wall';
|
||||||
|
|
||||||
type Cell = string | number | null | undefined;
|
type Cell = string | number | null | undefined;
|
||||||
type Grid = Cell[][];
|
type Grid = Cell[][];
|
||||||
@@ -67,7 +68,7 @@ export interface AbutRecord {
|
|||||||
/** 지반·지층 입력 (② 지반·지층 카드, 교각과 같은 필드). Absent = 육상, 지하수위 없음, 지층 행 없음. */
|
/** 지반·지층 입력 (② 지반·지층 카드, 교각과 같은 필드). Absent = 육상, 지하수위 없음, 지층 행 없음. */
|
||||||
ground?: AbutGround;
|
ground?: AbutGround;
|
||||||
/** ③ 배근 입력 (부재별, 바꾼 값만). Absent = 엑셀 Abut 시트 기본값. */
|
/** ③ 배근 입력 (부재별, 바꾼 값만). Absent = 엑셀 Abut 시트 기본값. */
|
||||||
rebar?: { footing?: Partial<AbutFootingRebarSettings> };
|
rebar?: { footing?: Partial<AbutFootingRebarSettings>; wall?: Partial<AbutWallRebarSettings> };
|
||||||
/** 예전 "좌우 같게" 값 (2026-11-04 이전 파일). 지금은 읽기만 하고 쓰지 않는다. */
|
/** 예전 "좌우 같게" 값 (2026-11-04 이전 파일). 지금은 읽기만 하고 쓰지 않는다. */
|
||||||
wingsLinked?: boolean;
|
wingsLinked?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-1
@@ -68,7 +68,7 @@ export function parseProject(text: string): ProjectFile {
|
|||||||
if (!Array.isArray(obj.piers)) throw new Error('piers 목록이 없습니다.');
|
if (!Array.isArray(obj.piers)) throw new Error('piers 목록이 없습니다.');
|
||||||
if (obj.fckByMember !== undefined) {
|
if (obj.fckByMember !== undefined) {
|
||||||
for (const [member, value] of Object.entries(obj.fckByMember)) {
|
for (const [member, value] of Object.entries(obj.fckByMember)) {
|
||||||
if (!['column', 'coping', 'footing', 'abutFooting'].includes(member) || typeof value !== 'number' || !(value > 0)) {
|
if (!['column', 'coping', 'footing', 'abutFooting', 'abutBody'].includes(member) || typeof value !== 'number' || !(value > 0)) {
|
||||||
throw new Error(`부재별 fck 값이 맞지 않습니다 (${member}: ${String(value)}).`);
|
throw new Error(`부재별 fck 값이 맞지 않습니다 (${member}: ${String(value)}).`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -168,6 +168,9 @@ export function checkStoredAbutments(list: unknown): void {
|
|||||||
const ok = key === 'dir' ? x === 'skew' || x === 'perp' : finite(x);
|
const ok = key === 'dir' ? x === 'skew' || x === 'perp' : finite(x);
|
||||||
if (!ok) throw new Error(`${who}: 저판 배근 값 ${key}이(가) 맞지 않습니다.`);
|
if (!ok) throw new Error(`${who}: 저판 배근 값 ${key}이(가) 맞지 않습니다.`);
|
||||||
}
|
}
|
||||||
|
for (const [key, v] of Object.entries(a.rebar.wall ?? {})) {
|
||||||
|
if (!finite(v)) throw new Error(`${who}: 벽체 배근 값 ${key}이(가) 맞지 않습니다.`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (a.wingsLinked !== undefined && typeof a.wingsLinked !== 'boolean') throw new Error(`${who}: 좌우 같게 값이 맞지 않습니다.`);
|
if (a.wingsLinked !== undefined && typeof a.wingsLinked !== 'boolean') throw new Error(`${who}: 좌우 같게 값이 맞지 않습니다.`);
|
||||||
if (a.wings !== undefined) {
|
if (a.wings !== undefined) {
|
||||||
|
|||||||
@@ -84,15 +84,15 @@ export function materialFrom(raw: unknown): Material {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Members that can take their own fck on ② (부재 카드). Other members always use the 공통 fck. */
|
/** Members that can take their own fck on ② (부재 카드). Other members always use the 공통 fck. */
|
||||||
export type FckMember = 'column' | 'coping' | 'footing' | 'abutFooting';
|
export type FckMember = 'column' | 'coping' | 'footing' | 'abutFooting' | 'abutBody';
|
||||||
/** The members of {@link FckMember}, for the ② cards. */
|
/** The members of {@link FckMember}, for the ② cards. */
|
||||||
export const FCK_MEMBERS: readonly FckMember[] = ['column', 'coping', 'footing', 'abutFooting'];
|
export const FCK_MEMBERS: readonly FckMember[] = ['column', 'coping', 'footing', 'abutFooting', 'abutBody'];
|
||||||
|
|
||||||
/** Per-member fck (MPa). A member without an entry uses the ③ 공통 설정 fck. */
|
/** Per-member fck (MPa). A member without an entry uses the ③ 공통 설정 fck. */
|
||||||
export type FckByMember = Partial<Record<FckMember, number>>;
|
export type FckByMember = Partial<Record<FckMember, number>>;
|
||||||
|
|
||||||
/** 교각도 · 교대도 기준 부재별 기본 fck (MPa). 되돌리기는 이 값으로 돌아가고, 이 값과 다르면 주황색으로 표시합니다. 교대 저판 27 (spec_abut §6, 2026-11-03). */
|
/** 교각도 · 교대도 기준 부재별 기본 fck (MPa). 되돌리기는 이 값으로 돌아가고, 이 값과 다르면 주황색으로 표시합니다. 교대 저판 27, 교대 벽체 · 흉벽 · 날개벽 · 접속슬래브 35 (spec_abut §6, 2026-11-03). */
|
||||||
export const IMPORTED_FCK: Record<FckMember, number> = { column: 40, coping: 40, footing: 27, abutFooting: 27 };
|
export const IMPORTED_FCK: Record<FckMember, number> = { column: 40, coping: 40, footing: 27, abutFooting: 27, abutBody: 35 };
|
||||||
|
|
||||||
/** True when the member's fck is set and differs from its imported default. An unset member is not changed. */
|
/** True when the member's fck is set and differs from its imported default. An unset member is not changed. */
|
||||||
export function fckChanged(byMember: FckByMember, member: FckMember): boolean {
|
export function fckChanged(byMember: FckByMember, member: FckMember): boolean {
|
||||||
|
|||||||
+18
-12
@@ -1499,18 +1499,7 @@ function fillAbutFootingExtras(
|
|||||||
auto('길이 (자동)', `${L.toFixed(3)} m`, '교좌면 길이 (skew)');
|
auto('길이 (자동)', `${L.toFixed(3)} m`, '교좌면 길이 (skew)');
|
||||||
auto('말뚝 길이', `${Math.round(rec.pileLengthMm)} mm`, '① 기본 정보에서 수정');
|
auto('말뚝 길이', `${Math.round(rec.pileLengthMm)} mm`, '① 기본 정보에서 수정');
|
||||||
auto('선단 EL (자동)', built.members.pileBottomEl === undefined ? '—' : `${built.members.pileBottomEl.toFixed(3)} m`, '= 기초 하단 − 말뚝 길이');
|
auto('선단 EL (자동)', built.members.pileBottomEl === undefined ? '—' : `${built.members.pileBottomEl.toFixed(3)} m`, '= 기초 하단 − 말뚝 길이');
|
||||||
// 교대 저판 fck: 교각 기초 fck와 따로 둔다 (사용자 결정 2026-11-04). 비우면 기본값으로 돌아간다.
|
abutFckRow(box, 'abutFooting', idOf, '교각 기초와 별도');
|
||||||
const fckId = idOf('fck');
|
|
||||||
const fckLab = el('label', 'fck (MPa)');
|
|
||||||
fckLab.htmlFor = fckId;
|
|
||||||
const fckIn = el('input');
|
|
||||||
fckIn.id = fckId;
|
|
||||||
fckIn.type = 'number';
|
|
||||||
fckIn.step = 'any';
|
|
||||||
fckIn.value = String(state.fckByMember.abutFooting ?? state.material.fck);
|
|
||||||
if (fckChanged(state.fckByMember, 'abutFooting')) fckIn.classList.add('changed-input');
|
|
||||||
fckIn.addEventListener('change', () => setMemberFck('abutFooting', fckIn.value));
|
|
||||||
box.append(fckLab, fckIn, el('span', `기본 ${IMPORTED_FCK.abutFooting} · 교각 기초와 별도 · 배근에만 반영`, 'muted small'));
|
|
||||||
// 열 위치 (배면부터): "650 · 2000 · 1300 · 650". The last one (전면 연단) follows from the 폭.
|
// 열 위치 (배면부터): "650 · 2000 · 1300 · 650". The last one (전면 연단) follows from the 폭.
|
||||||
const id = idOf('pile-rows');
|
const id = idOf('pile-rows');
|
||||||
const lab = el('label', '열 위치 · 배면부터 (mm)');
|
const lab = el('label', '열 위치 · 배면부터 (mm)');
|
||||||
@@ -1848,6 +1837,8 @@ function fillAbutSlot(slot: (typeof ABUT_SLOTS)[keyof typeof ABUT_SLOTS], rec: A
|
|||||||
const note = el('span', `기본 ${ABUT_SHAPE_DEFAULT[f.key]}${f.note ? ` · ${f.note}` : ''}`, f.note ? 'muted small abut-note-warn' : 'muted small');
|
const note = el('span', `기본 ${ABUT_SHAPE_DEFAULT[f.key]}${f.note ? ` · ${f.note}` : ''}`, f.note ? 'muted small abut-note-warn' : 'muted small');
|
||||||
(planCard && f.group === 'footing' ? footingBox : shape).append(lab, input, note);
|
(planCard && f.group === 'footing' ? footingBox : shape).append(lab, input, note);
|
||||||
}
|
}
|
||||||
|
// 교대 벽체 · 흉벽 · 날개벽 · 접속슬래브 fck (도면 35): ② 벽체·흉벽 카드에서 입력.
|
||||||
|
if (planCard) abutFckRow(shape, 'abutBody', idOf, '벽체 · 흉벽 · 날개벽 · 접속슬래브');
|
||||||
if (planCard) {
|
if (planCard) {
|
||||||
// 자동 값 (시안 입력 격자 마지막 줄): 벽체 두께, 흉벽 상단 IP3 / IP1 끝 (가정, QA-4), 교대축.
|
// 자동 값 (시안 입력 격자 마지막 줄): 벽체 두께, 흉벽 상단 IP3 / IP1 끝 (가정, QA-4), 교대축.
|
||||||
const v = { ...ABUT_SHAPE_DEFAULT, ...normalizeShape(rec.shape) };
|
const v = { ...ABUT_SHAPE_DEFAULT, ...normalizeShape(rec.shape) };
|
||||||
@@ -3759,6 +3750,21 @@ function setMemberFck(member: FckMember, raw: string): void {
|
|||||||
renderAll();
|
renderAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 교대 fck row on a ② card (label · input · note, the card's 3-column grid). 교각 fck와 따로 둔다 (사용자 결정 2026-11-04). Empty = default. */
|
||||||
|
function abutFckRow(box: HTMLElement, member: 'abutFooting' | 'abutBody', idOf: (suffix: string) => string, note: string): void {
|
||||||
|
const id = idOf(`fck-${member}`);
|
||||||
|
const lab = el('label', 'fck (MPa)');
|
||||||
|
lab.htmlFor = id;
|
||||||
|
const input = el('input');
|
||||||
|
input.id = id;
|
||||||
|
input.type = 'number';
|
||||||
|
input.step = 'any';
|
||||||
|
input.value = String(state.fckByMember[member] ?? state.material.fck);
|
||||||
|
if (fckChanged(state.fckByMember, member)) input.classList.add('changed-input');
|
||||||
|
input.addEventListener('change', () => setMemberFck(member, input.value));
|
||||||
|
box.append(lab, input, el('span', `기본 ${IMPORTED_FCK[member]} · ${note} · 배근에만 반영`, 'muted small'));
|
||||||
|
}
|
||||||
|
|
||||||
/** fck row on a ② card for a member that has its own fck: value, the 공통 value beside it, and a revert when set. */
|
/** fck row on a ② card for a member that has its own fck: value, the 공통 value beside it, and a revert when set. */
|
||||||
function renderMemberFck(box: HTMLDivElement, member: FckMember): void {
|
function renderMemberFck(box: HTMLDivElement, member: FckMember): void {
|
||||||
const own = state.fckByMember[member];
|
const own = state.fckByMember[member];
|
||||||
|
|||||||
@@ -136,3 +136,21 @@ describe('교대 벽체 배근 A1 (AD4, 교대벽체철근.md §11)', () => {
|
|||||||
expect(bar(r, 'W1').modified).toBe(false);
|
expect(bar(r, 'W1').modified).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
import { applyFootingRebar, applyWallRebar } from '../src/core/abut/edit';
|
||||||
|
import { checkStoredAbutments } from '../src/core/project';
|
||||||
|
|
||||||
|
describe('③ 벽체 배근 입력 저장', () => {
|
||||||
|
it('stores only the changed values next to the 저판 ones; undefined links a group back; refuses bad values', () => {
|
||||||
|
const a = applyWallRebar(applyFootingRebar(rec, { sW: 200 }), { sHoriz: 150, k7L: 6 });
|
||||||
|
expect(a.rebar).toEqual({ footing: { sW: 200 }, wall: { sHoriz: 150, k7L: 6 } });
|
||||||
|
expect(applyWallRebar(a, { sHoriz: undefined }).rebar?.wall).toEqual({ k7L: 6 });
|
||||||
|
expect(() => applyWallRebar(rec, { k7V: 2.5 })).toThrow('W7 배수');
|
||||||
|
expect(() => applyWallRebar(rec, { c: 0 })).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('the project file keeps the 벽체 input and refuses a non-number', () => {
|
||||||
|
expect(() => checkStoredAbutments([applyWallRebar(rec, { sL: 150, eMin: 200 })])).not.toThrow();
|
||||||
|
expect(() => checkStoredAbutments([{ ...rec, rebar: { wall: { sL: 'x' } } as never }])).toThrow('벽체 배근');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -2,8 +2,8 @@ import { describe, expect, it } from 'vitest';
|
|||||||
import { DEFAULT_MATERIAL, fckChanged, IMPORTED_FCK, materialFor } from '../src/core/rebar/common';
|
import { DEFAULT_MATERIAL, fckChanged, IMPORTED_FCK, materialFor } from '../src/core/rebar/common';
|
||||||
|
|
||||||
describe('IMPORTED_FCK (교각도 · 교대도 기준 부재별 기본 fck)', () => {
|
describe('IMPORTED_FCK (교각도 · 교대도 기준 부재별 기본 fck)', () => {
|
||||||
it('is 40 for 기둥·코핑, 27 for 확대기초 and 27 for 교대 저판', () => {
|
it('is 40 for 기둥·코핑, 27 for 확대기초, 27 for 교대 저판 and 35 for 교대 벽체 등', () => {
|
||||||
expect(IMPORTED_FCK).toEqual({ column: 40, coping: 40, footing: 27, abutFooting: 27 });
|
expect(IMPORTED_FCK).toEqual({ column: 40, coping: 40, footing: 27, abutFooting: 27, abutBody: 35 });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user