feat(rebar): 코핑 배근 입력 12개 · CP01·CP02 검사 · 적용 버튼
- core/rebar/coping-settings.ts: 입력 정의, 기본값(코핑철근.md), 범위(코핑_입력범위_제안.md), 검사 함수 - 다리 수는 4·6·8 목록으로 두고 홀수는 CP02로 막음 - 값은 적용 버튼을 누를 때만 draft에 쓰고 다시 생성 (범위 밖이면 반영하지 않음) - PierInput.copingRebar, 모델 copingRebar, 변경 추적 키 추가 Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ec402e22d7
commit
9c93a6aa01
6 files changed
+212
-1
No files matched your search
+2
-1
@@ -23,7 +23,7 @@ export const EDITABLE_FIELDS = [
|
|||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
/** Inputs tracked for overrides and pending changes: the numeric cards plus 열별 본수 (an array, compared by value). */
|
/** Inputs tracked for overrides and pending changes: the numeric cards plus 열별 본수 (an array, compared by value). */
|
||||||
export const TRACKED_KEYS = [...EDITABLE_FIELDS.map((f) => f.key), 'pileRows', 'groundPosition', 'groundWaterOn', 'groundWaterEl', 'floodLevelEl', 'scourDepthM', 'highWaterEl', 'lowWaterEl', 'strataRows', 'bearingType', 'bearingLayout', 'blockRebar'] as const;
|
export const TRACKED_KEYS = [...EDITABLE_FIELDS.map((f) => f.key), 'pileRows', 'groundPosition', 'groundWaterOn', 'groundWaterEl', 'floodLevelEl', 'scourDepthM', 'highWaterEl', 'lowWaterEl', 'strataRows', 'bearingType', 'bearingLayout', 'blockRebar', 'copingRebar'] as const;
|
||||||
|
|
||||||
export type EditableKey = (typeof TRACKED_KEYS)[number];
|
export type EditableKey = (typeof TRACKED_KEYS)[number];
|
||||||
|
|
||||||
@@ -54,6 +54,7 @@ export const FIELD_LABELS: Record<EditableKey, string> = {
|
|||||||
bearingType: '받침 종류·용량',
|
bearingType: '받침 종류·용량',
|
||||||
bearingLayout: '받침 배치',
|
bearingLayout: '받침 배치',
|
||||||
blockRebar: '받침블럭 철근 설정',
|
blockRebar: '받침블럭 철근 설정',
|
||||||
|
copingRebar: '코핑 배근 입력',
|
||||||
strataRows: '지층 행',
|
strataRows: '지층 행',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { buildAxis, copingSpan, fromAxis, kinkArc, nearKink, toAxis } from './axis';
|
import { buildAxis, copingSpan, fromAxis, kinkArc, nearKink, toAxis } from './axis';
|
||||||
import { buildIntegratedBlocks, type IntegratedBlock } from './integrated-block';
|
import { buildIntegratedBlocks, type IntegratedBlock } from './integrated-block';
|
||||||
import { type BlockRebarSettings, DEFAULT_BLOCK_REBAR } from './rebar/bearing-block';
|
import { type BlockRebarSettings, DEFAULT_BLOCK_REBAR } from './rebar/bearing-block';
|
||||||
|
import { type CopingRebarSettings, DEFAULT_COPING_REBAR } from './rebar/coping-settings';
|
||||||
import { layoutText, resolveRowCounts, rowLayout, type PileGeometry } from './piles';
|
import { layoutText, resolveRowCounts, rowLayout, type PileGeometry } from './piles';
|
||||||
import type { PierInput, Row } from './types';
|
import type { PierInput, Row } from './types';
|
||||||
import { type CopingGeom, P1_COPING_GEOM } from './coping-geom';
|
import { type CopingGeom, P1_COPING_GEOM } from './coping-geom';
|
||||||
@@ -176,6 +177,8 @@ export interface PierModel {
|
|||||||
integratedBlocks: IntegratedBlock[];
|
integratedBlocks: IntegratedBlock[];
|
||||||
/** 받침블럭 철근 설정 used by the rebar (승인 2026-10-26). */
|
/** 받침블럭 철근 설정 used by the rebar (승인 2026-10-26). */
|
||||||
blockRebar: BlockRebarSettings;
|
blockRebar: BlockRebarSettings;
|
||||||
|
/** 코핑 배근 입력 used by the coping rebar (승인 2026-10-26). */
|
||||||
|
copingRebar: CopingRebarSettings;
|
||||||
columns: ColumnModel[];
|
columns: ColumnModel[];
|
||||||
footings: FootingModel[];
|
footings: FootingModel[];
|
||||||
blindings: BlindingModel[];
|
blindings: BlindingModel[];
|
||||||
@@ -464,6 +467,7 @@ export function buildPier(input: PierInput): PierModel {
|
|||||||
blocks,
|
blocks,
|
||||||
integratedBlocks: buildIntegratedBlocks(blocks, input.bearingLayout === '일체'),
|
integratedBlocks: buildIntegratedBlocks(blocks, input.bearingLayout === '일체'),
|
||||||
blockRebar: { ...DEFAULT_BLOCK_REBAR, ...input.blockRebar },
|
blockRebar: { ...DEFAULT_BLOCK_REBAR, ...input.blockRebar },
|
||||||
|
copingRebar: { ...DEFAULT_COPING_REBAR, ...input.copingRebar },
|
||||||
columns,
|
columns,
|
||||||
footings,
|
footings,
|
||||||
blindings,
|
blindings,
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
/**
|
||||||
|
* 코핑 배근 입력 (승인 2026-10-26, 제안 = RAG/코핑_입력범위_제안.md). The 12 numbers of the ③ 코핑 배근 입력 panel.
|
||||||
|
* Defaults are the 코핑철근.md values; the ranges are the proposal's, pending the design-standard check.
|
||||||
|
*/
|
||||||
|
export interface CopingRebarSettings {
|
||||||
|
/** 피복 (철근 중심, mm). */
|
||||||
|
cover: number;
|
||||||
|
/** 주철근 기본간격 (mm). */
|
||||||
|
mainSpacing: number;
|
||||||
|
/** 주철근 끝단 최소 여유 (mm). */
|
||||||
|
mainEndGap: number;
|
||||||
|
/** 상면 주철근 단 간격 (mm). */
|
||||||
|
mainLayerGap: number;
|
||||||
|
/** 첫 전단철근 최소 여유 (피복 다음, mm). */
|
||||||
|
firstShearGap: number;
|
||||||
|
/** 전단철근 다리 수 (짝수). */
|
||||||
|
shearLegs: number;
|
||||||
|
/** 전단철근 간격 · 경사 (mm). */
|
||||||
|
spacingS: number;
|
||||||
|
/** 전단철근 간격 · Level (mm). */
|
||||||
|
spacingL: number;
|
||||||
|
/** 측면 수평 간격 (mm). */
|
||||||
|
sideSpacing: number;
|
||||||
|
/** 측면 위 끝 아래 최소 여유 (mm). */
|
||||||
|
sideEndGap: number;
|
||||||
|
/** C14 간격 위 2줄 (mm). */
|
||||||
|
c14UpperSpacing: number;
|
||||||
|
/** C14 간격 아래 줄 (mm). */
|
||||||
|
c14LowerSpacing: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_COPING_REBAR: CopingRebarSettings = {
|
||||||
|
cover: 100,
|
||||||
|
mainSpacing: 125,
|
||||||
|
mainEndGap: 80,
|
||||||
|
mainLayerGap: 57.2,
|
||||||
|
firstShearGap: 200,
|
||||||
|
shearLegs: 6,
|
||||||
|
spacingS: 250,
|
||||||
|
spacingL: 300,
|
||||||
|
sideSpacing: 250,
|
||||||
|
sideEndGap: 400,
|
||||||
|
c14UpperSpacing: 1000,
|
||||||
|
c14LowerSpacing: 2000,
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Allowed range per input (inclusive, mm). The shear legs are checked for evenness separately (CP02). */
|
||||||
|
export const COPING_REBAR_RANGES: Record<keyof CopingRebarSettings, [number, number]> = {
|
||||||
|
cover: [80, 150],
|
||||||
|
mainSpacing: [100, 150],
|
||||||
|
mainEndGap: [60, 100],
|
||||||
|
mainLayerGap: [50, 70],
|
||||||
|
firstShearGap: [100, 300],
|
||||||
|
shearLegs: [4, 8],
|
||||||
|
spacingS: [200, 350],
|
||||||
|
spacingL: [200, 350],
|
||||||
|
sideSpacing: [200, 300],
|
||||||
|
sideEndGap: [300, 500],
|
||||||
|
c14UpperSpacing: [800, 1200],
|
||||||
|
c14LowerSpacing: [1500, 2500],
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface CopingSettingsIssue {
|
||||||
|
code: 'CP01' | 'CP02';
|
||||||
|
key: keyof CopingRebarSettings;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks the settings against the ranges. CP01: a value is not a finite number or is out of range (W).
|
||||||
|
* CP02: the shear leg count is odd (the 폐합 bars come in pairs of legs).
|
||||||
|
*/
|
||||||
|
export function copingSettingsIssues(s: CopingRebarSettings): CopingSettingsIssue[] {
|
||||||
|
const out: CopingSettingsIssue[] = [];
|
||||||
|
for (const key of Object.keys(COPING_REBAR_RANGES) as (keyof CopingRebarSettings)[]) {
|
||||||
|
const v = s[key];
|
||||||
|
const [lo, hi] = COPING_REBAR_RANGES[key];
|
||||||
|
if (!Number.isFinite(v) || v < lo || v > hi) {
|
||||||
|
out.push({ code: 'CP01', key, message: `CP01 ${key} ${v} 범위 밖 (${lo}–${hi})` });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Number.isFinite(s.shearLegs) && s.shearLegs % 2 !== 0) {
|
||||||
|
out.push({ code: 'CP02', key: 'shearLegs', message: `CP02 전단철근 다리 수 ${s.shearLegs}는 홀수 (짝수만 허용)` });
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import type { CopingRebarSettings } from './rebar/coping-settings';
|
||||||
|
|
||||||
export type PierType = 'T형' | '2주형' | '3주형';
|
export type PierType = 'T형' | '2주형' | '3주형';
|
||||||
export type FoundationType = '말뚝기초' | '직접기초' | '현장타설말뚝';
|
export type FoundationType = '말뚝기초' | '직접기초' | '현장타설말뚝';
|
||||||
export type Row = '전열' | '후열';
|
export type Row = '전열' | '후열';
|
||||||
@@ -74,6 +76,8 @@ export interface PierInput {
|
|||||||
blockRebar?: Partial<{ coverTop: number; coverSide: number; firstGap: number; spacingS1: number; spacingS2: number; legLength: number }>;
|
blockRebar?: Partial<{ coverTop: number; coverSide: number; firstGap: number; spacingS1: number; spacingS2: number; legLength: number }>;
|
||||||
/** 받침 배치 (승인 2026-10-26): 분리 (default) or 일체. 일체 is allowed only when every front–back pair is within 100 mm. */
|
/** 받침 배치 (승인 2026-10-26): 분리 (default) or 일체. 일체 is allowed only when every front–back pair is within 100 mm. */
|
||||||
bearingLayout?: '분리' | '일체';
|
bearingLayout?: '분리' | '일체';
|
||||||
|
/** 코핑 배근 입력 (승인 2026-10-26): the 12 numbers of ③ 코핑 배근. Absent = the 코핑철근.md defaults. */
|
||||||
|
copingRebar?: Partial<CopingRebarSettings>;
|
||||||
strataRows?: StrataRow[];
|
strataRows?: StrataRow[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+81
@@ -27,6 +27,7 @@ import { blockRebarPlanSvg } from './view/block-rebar-plan-svg';
|
|||||||
import { copingEndStation, copingFrontSvg, copingSectionSvg } from './view/coping-rebar-svg';
|
import { copingEndStation, copingFrontSvg, copingSectionSvg } from './view/coping-rebar-svg';
|
||||||
import { BLOCK_REBAR_RANGES } from './core/rebar/block-check';
|
import { BLOCK_REBAR_RANGES } from './core/rebar/block-check';
|
||||||
import { DEFAULT_BLOCK_REBAR, type BlockRebarSettings } from './core/rebar/bearing-block';
|
import { DEFAULT_BLOCK_REBAR, type BlockRebarSettings } from './core/rebar/bearing-block';
|
||||||
|
import { COPING_REBAR_RANGES, copingSettingsIssues, type CopingRebarSettings, DEFAULT_COPING_REBAR } from './core/rebar/coping-settings';
|
||||||
import { groundSectionSvg } from './view/ground-svg';
|
import { groundSectionSvg } from './view/ground-svg';
|
||||||
import { parseBearingElevationTable, parseSolplateTable, readDxfTexts } from './core/dxf-table';
|
import { parseBearingElevationTable, parseSolplateTable, readDxfTexts } from './core/dxf-table';
|
||||||
import { columnDiameterIssues } from './core/column-check';
|
import { columnDiameterIssues } from './core/column-check';
|
||||||
@@ -1073,6 +1074,85 @@ function renderBlockRebarSettings(box: HTMLDivElement, pier: Pier): void {
|
|||||||
/** 코핑 배근 (승인 2026-10-26, 추천): 정면, and the 단면·폐합 at a station the user chooses (default mid-length). */
|
/** 코핑 배근 (승인 2026-10-26, 추천): 정면, and the 단면·폐합 at a station the user chooses (default mid-length). */
|
||||||
let copingView: 'front' | 'section' = 'front';
|
let copingView: 'front' | 'section' = 'front';
|
||||||
|
|
||||||
|
/** 코핑 배근 입력 (승인 2026-10-26, 제안 = RAG/코핑_입력범위_제안.md). Order follows the ③ 코핑 배근 panel. */
|
||||||
|
const COPING_REBAR_FIELDS: { key: keyof CopingRebarSettings; label: string }[] = [
|
||||||
|
{ key: 'cover', label: '피복 (철근 중심, mm)' },
|
||||||
|
{ key: 'mainSpacing', label: '주철근 기본간격 (mm)' },
|
||||||
|
{ key: 'mainEndGap', label: '주철근 끝단 최소 여유 (mm)' },
|
||||||
|
{ key: 'mainLayerGap', label: '상면 주철근 단 간격 (mm)' },
|
||||||
|
{ key: 'firstShearGap', label: '첫 전단철근 최소 여유 (피복 다음, mm)' },
|
||||||
|
{ key: 'shearLegs', label: '전단철근 다리 수' },
|
||||||
|
{ key: 'spacingS', label: '전단철근 간격 · 경사 (mm)' },
|
||||||
|
{ key: 'spacingL', label: '전단철근 간격 · Level (mm)' },
|
||||||
|
{ key: 'sideSpacing', label: '측면 수평 간격 (mm)' },
|
||||||
|
{ key: 'sideEndGap', label: '측면 위 끝 아래 최소 여유 (mm)' },
|
||||||
|
{ key: 'c14UpperSpacing', label: 'C14 간격 위 2줄 (mm)' },
|
||||||
|
{ key: 'c14LowerSpacing', label: 'C14 간격 아래 줄 (mm)' },
|
||||||
|
];
|
||||||
|
|
||||||
|
/** 적용: the values go to the draft and the model is rebuilt only when every check passes (CP01·CP02). */
|
||||||
|
function applyCopingRebar(name: string, next: CopingRebarSettings): void {
|
||||||
|
const p = state.piers.find((q) => q.input.name === name);
|
||||||
|
if (!p) return;
|
||||||
|
p.draft = { ...p.draft, copingRebar: next };
|
||||||
|
regenerateOne(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderCopingSettings(box: HTMLDivElement, pier: Pier): void {
|
||||||
|
if (!pier.model) return;
|
||||||
|
const wrap = el('div', undefined, 'bearing-ref');
|
||||||
|
const current: CopingRebarSettings = { ...DEFAULT_COPING_REBAR, ...pier.draft.copingRebar };
|
||||||
|
wrap.appendChild(el('p', '코핑 배근 입력 (기본값 = 코핑철근.md · 적용을 눌러야 반영)', 'lbl'));
|
||||||
|
const grid = el('div', undefined, 'edit-fields');
|
||||||
|
const controls = new Map<keyof CopingRebarSettings, HTMLInputElement | HTMLSelectElement>();
|
||||||
|
for (const f of COPING_REBAR_FIELDS) {
|
||||||
|
const row = el('div', undefined, 'edit-row');
|
||||||
|
const label = el('label', f.label);
|
||||||
|
let control: HTMLInputElement | HTMLSelectElement;
|
||||||
|
if (f.key === 'shearLegs') {
|
||||||
|
// 다리 수는 짝수만 고르도록 목록으로 둔다 (CP02).
|
||||||
|
const sel = el('select');
|
||||||
|
for (const n of [4, 6, 8]) {
|
||||||
|
const opt = el('option', String(n));
|
||||||
|
opt.value = String(n);
|
||||||
|
sel.appendChild(opt);
|
||||||
|
}
|
||||||
|
sel.value = String(current.shearLegs);
|
||||||
|
control = sel;
|
||||||
|
} else {
|
||||||
|
const input = el('input');
|
||||||
|
input.type = 'number';
|
||||||
|
input.step = 'any';
|
||||||
|
input.value = String(current[f.key]);
|
||||||
|
control = input;
|
||||||
|
}
|
||||||
|
control.id = `coping-${f.key}`;
|
||||||
|
label.htmlFor = control.id;
|
||||||
|
controls.set(f.key, control);
|
||||||
|
const [lo, hi] = COPING_REBAR_RANGES[f.key];
|
||||||
|
const changed = current[f.key] !== DEFAULT_COPING_REBAR[f.key] ? ' · 기본과 다름' : '';
|
||||||
|
row.append(label, control, el('span', `범위 ${lo}–${hi} · 기본 ${DEFAULT_COPING_REBAR[f.key]}${changed}`, 'muted small'));
|
||||||
|
grid.appendChild(row);
|
||||||
|
}
|
||||||
|
wrap.appendChild(grid);
|
||||||
|
const msg = el('p', undefined, 'muted small');
|
||||||
|
const apply = el('button', '적용', 'btn small');
|
||||||
|
apply.type = 'button';
|
||||||
|
apply.addEventListener('click', () => {
|
||||||
|
const next = { ...current };
|
||||||
|
for (const f of COPING_REBAR_FIELDS) next[f.key] = Number(controls.get(f.key)?.value);
|
||||||
|
const issues = copingSettingsIssues(next);
|
||||||
|
if (issues.length > 0) {
|
||||||
|
msg.textContent = issues.map((i) => i.message).join(' · ');
|
||||||
|
setError('코핑 배근 입력을 확인하세요. 값은 반영되지 않았습니다.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
applyCopingRebar(pier.input.name, next);
|
||||||
|
});
|
||||||
|
wrap.append(msg, apply);
|
||||||
|
box.appendChild(wrap);
|
||||||
|
}
|
||||||
|
|
||||||
function renderCopingDrawings(box: HTMLDivElement, pier: Pier): void {
|
function renderCopingDrawings(box: HTMLDivElement, pier: Pier): void {
|
||||||
if (!pier.model || !rebar3d) return;
|
if (!pier.model || !rebar3d) return;
|
||||||
const L = pier.model.copingL;
|
const L = pier.model.copingL;
|
||||||
@@ -1147,6 +1227,7 @@ function renderRebar(): void {
|
|||||||
$('rebar-note').textContent = `${pier.input.name} · 형상 기준 계산 · 값은 기본값 기준 문서 규칙`;
|
$('rebar-note').textContent = `${pier.input.name} · 형상 기준 계산 · 값은 기본값 기준 문서 규칙`;
|
||||||
if (state.rebarMember === 'bearing') renderBlockRebarSettings(box, pier);
|
if (state.rebarMember === 'bearing') renderBlockRebarSettings(box, pier);
|
||||||
if (state.rebarMember === 'coping' && rebar3d) renderCopingDrawings(box, pier);
|
if (state.rebarMember === 'coping' && rebar3d) renderCopingDrawings(box, pier);
|
||||||
|
if (state.rebarMember === 'coping') renderCopingSettings(box, pier);
|
||||||
const shown = rows.filter((r) => r.member === state.rebarMember);
|
const shown = rows.filter((r) => r.member === state.rebarMember);
|
||||||
box.appendChild(rebarSelectBar(shown.map((r) => r.name)));
|
box.appendChild(rebarSelectBar(shown.map((r) => r.name)));
|
||||||
const table = el('table', undefined, 'grid-table rebar-table');
|
const table = el('table', undefined, 'grid-table rebar-table');
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { COPING_REBAR_RANGES, copingSettingsIssues, DEFAULT_COPING_REBAR } from '../src/core/rebar/coping-settings';
|
||||||
|
|
||||||
|
describe('코핑 배근 입력 범위 (승인 2026-10-26)', () => {
|
||||||
|
it('the defaults are inside every range and raise no issue', () => {
|
||||||
|
expect(copingSettingsIssues(DEFAULT_COPING_REBAR)).toEqual([]);
|
||||||
|
for (const [key, [lo, hi]] of Object.entries(COPING_REBAR_RANGES)) {
|
||||||
|
const v = DEFAULT_COPING_REBAR[key as keyof typeof DEFAULT_COPING_REBAR];
|
||||||
|
expect(v >= lo && v <= hi, key).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('a value at either end of its range is accepted', () => {
|
||||||
|
const lo = { ...DEFAULT_COPING_REBAR, cover: 80, shearLegs: 4 };
|
||||||
|
const hi = { ...DEFAULT_COPING_REBAR, cover: 150, shearLegs: 8 };
|
||||||
|
expect(copingSettingsIssues(lo)).toEqual([]);
|
||||||
|
expect(copingSettingsIssues(hi)).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('a value outside its range gives CP01 on that key', () => {
|
||||||
|
const issues = copingSettingsIssues({ ...DEFAULT_COPING_REBAR, cover: 151 });
|
||||||
|
expect(issues).toHaveLength(1);
|
||||||
|
expect(issues[0]).toMatchObject({ code: 'CP01', key: 'cover' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('a non-finite value gives CP01', () => {
|
||||||
|
const issues = copingSettingsIssues({ ...DEFAULT_COPING_REBAR, spacingS: Number.NaN });
|
||||||
|
expect(issues).toEqual([expect.objectContaining({ code: 'CP01', key: 'spacingS' })]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('an odd shear leg count gives CP02 (and no CP01 because 5 is in range)', () => {
|
||||||
|
const issues = copingSettingsIssues({ ...DEFAULT_COPING_REBAR, shearLegs: 5 });
|
||||||
|
expect(issues).toEqual([expect.objectContaining({ code: 'CP02', key: 'shearLegs' })]);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in new issue
Block a user