feat(rebar): 기둥 배근 입력 8개 (피복·띠철근 K3/K5/K4·K6·커플러·Δz·n_div), CL01·CL02 검사

- core/rebar/column-settings.ts: 입력 정의, 기본값(기둥철근 (1).md), 범위(제안), 검사 (CL01 범위, CL02 n_div 홀수)
- 주철근 하단 갈고리는 고정 (입력 없음)
- 기둥 탭 패널: 적용 버튼으로 반영, 계산 결과(K2 내측 r, 주철근·커플러 수량) 표시
- P1·P2 등분 수, P3·P5·P4·P6 간격, 커플러 간격·상승을 철근표 간격·비고와 근거(기본값/입력)에 연동
- 3D 주철근 개수도 n_div 입력을 사용 (COLUMN_P1 하드코딩 제거)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
ypshimandClaude Haiku 5.5 committed 2026-10-26 16:22:47 +09:00
1 parent 496e0ea4e5
commit f619b9ea20
9 files changed
+215 -20

No files matched your search

+2 -1
View File
@@ -23,7 +23,7 @@ export const EDITABLE_FIELDS = [
] as const;
/** 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', 'copingRebar', 'topSplice'] 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', 'topSplice', 'columnRebar'] as const;
export type EditableKey = (typeof TRACKED_KEYS)[number];
@@ -56,6 +56,7 @@ export const FIELD_LABELS: Record<EditableKey, string> = {
blockRebar: '받침블럭 철근 설정',
copingRebar: '코핑 배근 입력',
topSplice: '상면 주철근 이음',
columnRebar: '기둥 배근 입력',
strataRows: '지층 행',
};
+4
View File
@@ -2,6 +2,7 @@ import { buildAxis, copingSpan, fromAxis, kinkArc, nearKink, toAxis } from './ax
import { buildIntegratedBlocks, type IntegratedBlock } from './integrated-block';
import { type BlockRebarSettings, DEFAULT_BLOCK_REBAR } from './rebar/bearing-block';
import { type CopingRebarSettings, DEFAULT_COPING_REBAR, DEFAULT_TOP_SPLICE, type TopSpliceSettings } from './rebar/coping-settings';
import { type ColumnRebarSettings, DEFAULT_COLUMN_REBAR } from './rebar/column-settings';
import { layoutText, resolveRowCounts, rowLayout, type PileGeometry } from './piles';
import type { PierInput, Row } from './types';
import { type CopingGeom, P1_COPING_GEOM } from './coping-geom';
@@ -181,6 +182,8 @@ export interface PierModel {
copingRebar: CopingRebarSettings;
/** 상면 주철근 이음 used by the coping rebar (승인 2026-10-26). */
topSplice: TopSpliceSettings;
/** 기둥 배근 입력 used by the column rebar (승인 2026-10-26). */
columnRebar: ColumnRebarSettings;
columns: ColumnModel[];
footings: FootingModel[];
blindings: BlindingModel[];
@@ -471,6 +474,7 @@ export function buildPier(input: PierInput): PierModel {
blockRebar: { ...DEFAULT_BLOCK_REBAR, ...input.blockRebar },
copingRebar: { ...DEFAULT_COPING_REBAR, ...input.copingRebar },
topSplice: { ...DEFAULT_TOP_SPLICE, ...input.topSplice },
columnRebar: { ...DEFAULT_COLUMN_REBAR, ...input.columnRebar },
columns,
footings,
blindings,
+70
View File
@@ -0,0 +1,70 @@
/**
* 기둥 배근 입력 (승인 2026-10-26, 시안 ③ 기둥 삽도). The inputs of the 기둥 panel; the names match ColumnInput.
* Defaults are the 기둥철근 (1).md P1 values; the ranges are proposed and pending the design-standard check.
* The 주철근 하단 갈고리 is fixed (no input).
*/
export interface ColumnRebarSettings {
/** K1 중심 피복 c (기둥 면 ~ P1 중심, mm). */
c: number;
/** 띠철근 간격 K3 (일반 구간, mm). */
ctcP3: number;
/** 띠철근 간격 K5 (단부, mm). */
ctcP5: number;
/** 띠철근 간격 K4 (기초 안, mm). */
ctcP4: number;
/** 크로스타이 K6 간격 (단 간격, mm). */
ctcP6: number;
/** 주철근 커플러 간격 (기초 상면부터, mm). */
ctcCoupler: number;
/** P2 커플러를 P1보다 올리는 높이 (mm). */
dzCp2: number;
/** 주철근 원주 등분 수 n_div (P1·P2 공통, 짝수). */
nDiv: number;
}
export const DEFAULT_COLUMN_REBAR: ColumnRebarSettings = {
c: 125,
ctcP3: 300,
ctcP5: 120,
ctcP4: 120,
ctcP6: 1800,
ctcCoupler: 3000,
dzCp2: 150,
nDiv: 46,
};
/** Allowed range per input (inclusive, mm). */
export const COLUMN_REBAR_RANGES: Record<keyof ColumnRebarSettings, [number, number]> = {
c: [100, 150],
ctcP3: [200, 400],
ctcP5: [80, 200],
ctcP4: [80, 200],
ctcP6: [1200, 2400],
ctcCoupler: [2000, 4000],
dzCp2: [0, 300],
nDiv: [40, 50],
};
export interface ColumnSettingsIssue {
code: 'CL01' | 'CL02';
key: keyof ColumnRebarSettings;
message: string;
}
/**
* CL01: a value is not a finite number or is out of range (W). CL02: n_div is odd (the main bars come in pairs around the circle).
*/
export function columnSettingsIssues(s: ColumnRebarSettings): ColumnSettingsIssue[] {
const out: ColumnSettingsIssue[] = [];
for (const key of Object.keys(COLUMN_REBAR_RANGES) as (keyof ColumnRebarSettings)[]) {
const v = s[key];
const [lo, hi] = COLUMN_REBAR_RANGES[key];
if (!Number.isFinite(v) || v < lo || v > hi) {
out.push({ code: 'CL01', key, message: `CL01 ${key} ${v} 범위 밖 (${lo}–${hi})` });
}
}
if (Number.isFinite(s.nDiv) && s.nDiv % 2 !== 0) {
out.push({ code: 'CL02', key: 'nDiv', message: `CL02 주철근 등분 수 ${s.nDiv}는 홀수 (짝수만 허용)` });
}
return out;
}
+3 -9
View File
@@ -1,6 +1,7 @@
import type { PierModel } from '../geometry';
import { DEFAULT_MATERIAL, developmentLength, fyOf, hookLength, Material, NOMINAL_DIAMETER, spliceLength } from './common';
import { layers } from './footing';
import { DEFAULT_COLUMN_REBAR } from './column-settings';
/**
* 기둥 철근 C4 (plan C4). Source: RAG/기둥철근 (1).md, 2026-10-22 최종 정리. Type-1 원형 중실 기둥.
@@ -54,26 +55,19 @@ export function columnInputFor(model: PierModel, base: ColumnInput = COLUMN_P1):
const eP1 = Math.round((seg.top - (c3 - 0.1)) * 1000);
// e_top: the coping middle level (Level 하면 + 계단 상면)/2 is the top of P3 and P6 (승인 2026-10-23).
const eTop = Math.round((seg.top - (model.levelBottom + seg.top) / 2) * 1000);
return { ...base, Hc, hcop, eP1, eTop, D: model.columnDiameterMm };
return { ...base, ...model.columnRebar, Hc, hcop, eP1, eTop, D: model.columnDiameterMm };
}
export const COLUMN_P1: ColumnInput = {
D: 2000,
Hc: 12703,
hcop: 2644,
c: 125,
...DEFAULT_COLUMN_REBAR,
eP1: 485,
// Coping middle level: (Level 하면 + 계단 상면)/2 on P1 = 42.233 → e_top 1322 (승인 2026-10-23, Q-C3 대체).
eTop: 1322,
Hf: 2000,
nDiv: 46,
ctcCoupler: 3000,
dzCp2: 150,
ctcP3: 300,
ctcP5: 120,
ctcP4: 120,
rP4: 0.5,
ctcP6: 1800,
};
/** A bar in the 확대기초 top layer, height from the 확대기초 상면 (mm, negative = below). */
+15 -3
View File
@@ -1,6 +1,7 @@
import type { PierModel } from '../geometry';
import { barLength, bearingBlockRebar, DEFAULT_BLOCK_REBAR, totals } from './bearing-block';
import { DEFAULT_COPING_REBAR } from './coping-settings';
import { DEFAULT_COLUMN_REBAR } from './column-settings';
import { bottomPiece, topBarLength, widthLayout, topSplicePieces } from './coping-main';
import { shearBars, shearBarsMulti, copingSideBars, SIDE_HOOK_LEG, c14Bars, c14Length, stairBars, stepDevLength, undersideAt, shearULength, capLength, PIECES_PER_POSITION } from './coping';
import { sideRowLevels } from '../coping-geom';
@@ -147,6 +148,17 @@ export function linkedSpacing(name: string, model: PierModel): Pick<RebarRow, 's
basis: basis(c.c14UpperSpacing !== d.c14UpperSpacing || c.c14LowerSpacing !== d.c14LowerSpacing),
};
}
// 기둥 (승인 2026-10-26): P1·P2 등분 수, 주철근 커플러, 띠철근 P3·P5·P4, 크로스타이 P6.
const k = model.columnRebar;
const kd = DEFAULT_COLUMN_REBAR;
if (name === 'P1' || name === 'P2') return { spacing: `원주 ${k.nDiv}등분`, actual: '—', basis: basis(k.nDiv !== kd.nDiv) };
if (name === '주철근 커플러') {
return { spacing: `@${k.ctcCoupler} (P2 +${k.dzCp2})`, actual: '—', basis: basis(k.ctcCoupler !== kd.ctcCoupler || k.dzCp2 !== kd.dzCp2) };
}
if (name === 'P3') return { spacing: String(k.ctcP3), actual: String(k.ctcP3), basis: basis(k.ctcP3 !== kd.ctcP3) };
if (name === 'P5') return { spacing: String(k.ctcP5), actual: String(k.ctcP5), basis: basis(k.ctcP5 !== kd.ctcP5) };
if (name === 'P4') return { spacing: String(k.ctcP4), actual: String(k.ctcP4), basis: basis(k.ctcP4 !== kd.ctcP4) };
if (name === 'P6') return { spacing: String(k.ctcP6), actual: String(k.ctcP6), basis: basis(k.ctcP6 !== kd.ctcP6) };
if (name === 'S1') return { spacing: String(b.spacingS1), actual: String(b.spacingS1), basis: basis(b.spacingS1 !== bd.spacingS1) };
if (name === 'S2') return { spacing: String(b.spacingS2), actual: String(b.spacingS2), basis: basis(b.spacingS2 !== bd.spacingS2) };
return undefined;
@@ -257,11 +269,11 @@ export function rebarSummary(
const col = columnRebar(columnInputFor(model), mColumn);
rows.push({ member: 'column', name: 'P1', size: 'S32', count: col.mainP1, length: null, pieces: col.p1Pieces.map((length) => ({ length, count: col.mainP1 })), note: `외측 r ${col.r1}, 마디 ${col.p1Pieces.join(' / ')} (첫 마디에 갈고리 ${col.p1Hook}, 기둥철근.md §3.2)` });
rows.push({ member: 'column', name: 'P2', size: 'S32', count: col.mainP2, length: null, pieces: col.p2Pieces.map((length) => ({ length, count: col.mainP2 })), note: `내측 r ${col.r2}, 마디 ${col.p2Pieces.join(' / ')}` });
rows.push({ member: 'column', name: '주철근 커플러', size: 'D32용 (L150 Ø60)', count: col.couplers, length: 150, note: '@3000, P2는 +150 mm 상향 엇갈림' });
rows.push({ member: 'column', name: '주철근 커플러', size: 'D32용 (L150 Ø60)', count: col.couplers, length: 150, note: `@${model.columnRebar.ctcCoupler}, P2는 +${model.columnRebar.dzCp2} mm 상향 엇갈림` });
const tieCouplerCount = col.p5Count + col.p4Count * 2;
rows.push({ member: 'column', name: '띠철근 커플러', size: 'D19용 (L100 Ø40)', count: tieCouplerCount, length: 100, note: `단부 P5 1개(${col.p5Count}) · 기초안 P4 2개(${col.p4Count * 2}) 폐합용` });
rows.push({ member: 'column', name: 'P3', size: 'H19', count: col.p3Count, length: col.p3Length, note: `일반 구간 띠철근 @300, 겹침 ${col.lapP3} (추천안, Q-C6)` });
rows.push({ member: 'column', name: 'P5', size: 'H19', count: col.p5Count, length: col.p5Length, note: '단부 띠철근 @120, 커플러 1개' });
rows.push({ member: 'column', name: 'P3', size: 'H19', count: col.p3Count, length: col.p3Length, note: `일반 구간 띠철근 @${model.columnRebar.ctcP3}, 겹침 ${col.lapP3} (추천안, Q-C6)` });
rows.push({ member: 'column', name: 'P5', size: 'H19', count: col.p5Count, length: col.p5Length, note: `단부 띠철근 @${model.columnRebar.ctcP5}, 커플러 1개` });
rows.push({ member: 'column', name: 'P4', size: 'H19', count: col.p4Count, length: col.p4SemicircleLength * 2, note: '확대기초 안, 반원 2개 (길이는 반원 2개 합) · B11 간섭 무시' });
rows.push({ member: 'column', name: 'P6', size: 'H16', count: col.p6Count, length: col.p6Length, note: '크로스타이, 8단 × 4' });
+3
View File
@@ -1,4 +1,5 @@
import type { CopingRebarSettings, TopSpliceSettings } from './rebar/coping-settings';
import type { ColumnRebarSettings } from './rebar/column-settings';
export type PierType = 'T형' | '2주형' | '3주형';
export type FoundationType = '말뚝기초' | '직접기초' | '현장타설말뚝';
@@ -80,6 +81,8 @@ export interface PierInput {
copingRebar?: Partial<CopingRebarSettings>;
/** 상면 주철근(C1–C6) 이음 (승인 2026-10-26). Absent = 이음 무시, 기둥 중심. */
topSplice?: Partial<TopSpliceSettings>;
/** 기둥 배근 입력 (승인 2026-10-26, 시안 ③ 기둥). Absent = the 기둥철근 (1).md defaults. */
columnRebar?: Partial<ColumnRebarSettings>;
strataRows?: StrataRow[];
}
+67
View File
@@ -28,6 +28,8 @@ import { copingEndStation, copingFrontSvg, copingSectionSvg } from './view/copin
import { BLOCK_REBAR_RANGES } from './core/rebar/block-check';
import { DEFAULT_BLOCK_REBAR, type BlockRebarSettings } from './core/rebar/bearing-block';
import { COPING_REBAR_RANGES, copingSettingsIssues, type CopingRebarSettings, DEFAULT_COPING_REBAR, DEFAULT_TOP_SPLICE, type TopSpliceSettings } from './core/rebar/coping-settings';
import { COLUMN_REBAR_RANGES, type ColumnRebarSettings, columnSettingsIssues, DEFAULT_COLUMN_REBAR } from './core/rebar/column-settings';
import { columnInputFor, columnRebar } from './core/rebar/column';
import { groundSectionSvg } from './view/ground-svg';
import { parseBearingElevationTable, parseSolplateTable, readDxfTexts } from './core/dxf-table';
import { columnDiameterIssues } from './core/column-check';
@@ -1183,6 +1185,70 @@ function renderCopingSettings(box: HTMLDivElement, pier: Pier): void {
box.appendChild(wrap);
}
/** 기둥 배근 입력 (승인 2026-10-26, 시안 ③ 기둥 삽도). The 주철근 하단 갈고리 is fixed. The 띠철근 간격 is three inputs (K3 · K5 · K4). */
const COLUMN_REBAR_FIELDS: { key: keyof ColumnRebarSettings; label: string }[] = [
{ key: 'c', label: 'K1 중심 피복 c (mm)' },
{ key: 'ctcP3', label: '띠철근 간격 K3 (mm)' },
{ key: 'ctcP5', label: '띠철근 간격 K5 (mm)' },
{ key: 'ctcP4', label: '띠철근 간격 K4 (mm)' },
{ key: 'ctcP6', label: '크로스타이 K6 간격 (mm)' },
{ key: 'ctcCoupler', label: '커플러 간격 ctcOfcoupler (mm)' },
{ key: 'dzCp2', label: 'P2 커플러 상승 Δz (mm)' },
{ key: 'nDiv', label: '주철근 원주 등분 수 n_div' },
];
/** 적용: the values go to the draft and the model is rebuilt only when every check passes (CL01·CL02). */
function applyColumnRebar(name: string, next: ColumnRebarSettings): void {
const p = state.piers.find((q) => q.input.name === name);
if (!p) return;
p.draft = { ...p.draft, columnRebar: next };
regenerateOne(name);
}
function renderColumnSettings(box: HTMLDivElement, pier: Pier): void {
if (!pier.model) return;
const wrap = el('div', undefined, 'bearing-ref');
const current: ColumnRebarSettings = { ...DEFAULT_COLUMN_REBAR, ...pier.draft.columnRebar };
wrap.appendChild(el('p', '기둥 배근 입력 (기본값 = 기둥철근 (1).md · 적용을 눌러야 반영)', 'lbl'));
const grid = el('div', undefined, 'edit-fields');
const inputs = new Map<keyof ColumnRebarSettings, HTMLInputElement>();
for (const f of COLUMN_REBAR_FIELDS) {
const row = el('div', undefined, 'edit-row');
const label = el('label', f.label);
const input = el('input');
input.type = 'number';
input.step = 'any';
input.value = String(current[f.key]);
input.id = `column-${f.key}`;
label.htmlFor = input.id;
inputs.set(f.key, input);
const [lo, hi] = COLUMN_REBAR_RANGES[f.key];
const changed = current[f.key] !== DEFAULT_COLUMN_REBAR[f.key] ? ' · 기본과 다름' : '';
row.append(label, input, el('span', `범위 ${lo}–${hi} · 기본 ${DEFAULT_COLUMN_REBAR[f.key]}${changed}`, 'muted small'));
grid.appendChild(row);
}
wrap.appendChild(grid);
// Read-only results of the current model (계산 값).
const col = columnRebar(columnInputFor(pier.model));
wrap.appendChild(el('p', `K2 내측 r ${col.r2} mm · P1 ${col.mainP1}본 · P2 ${col.mainP2}본 · 주철근 커플러 ${col.couplers}개`, 'muted small'));
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 COLUMN_REBAR_FIELDS) next[f.key] = Number(inputs.get(f.key)?.value);
const issues = columnSettingsIssues(next);
if (issues.length > 0) {
msg.textContent = issues.map((i) => i.message).join(' · ');
setError('기둥 배근 입력을 확인하세요. 값은 반영되지 않았습니다.');
return;
}
applyColumnRebar(pier.input.name, next);
});
wrap.append(msg, apply);
box.appendChild(wrap);
}
function renderCopingDrawings(box: HTMLDivElement, pier: Pier): void {
if (!pier.model || !rebar3d) return;
const L = pier.model.copingL;
@@ -1258,6 +1324,7 @@ function renderRebar(): void {
if (state.rebarMember === 'bearing') renderBlockRebarSettings(box, pier);
if (state.rebarMember === 'coping' && rebar3d) renderCopingDrawings(box, pier);
if (state.rebarMember === 'coping') renderCopingSettings(box, pier);
if (state.rebarMember === 'column') renderColumnSettings(box, pier);
const shown = rows.filter((r) => r.member === state.rebarMember);
box.appendChild(rebarSelectBar(shown.map((r) => r.name)));
const table = el('table', undefined, 'grid-table rebar-table');
+7 -7
View File
@@ -4,7 +4,7 @@ import { assemblyEndOffset, assemblyJs, b10Levels, b9Bars, FOOTING_DEFAULTS, gri
import { bottomPiece, MAIN, topSplicePieces, widthLayout } from '../core/rebar/coping-main';
import { C14_INSET, c14Bars, CAP, COPING_REBAR, SHEAR_LOOP_STEP, shearBars, shearBarsMulti, shearUHeight, copingSideBars, SIDE_HOOK_LEG, stairBars, undersideAt } from '../core/rebar/coping';
import { sideRowLevels } from '../core/coping-geom';
import { columnRebar, columnInputFor, COLUMN_P1 } from '../core/rebar/column';
import { columnRebar, columnInputFor } from '../core/rebar/column';
import { barDepths, barsAcross } from '../core/rebar/bearing-block';
import { blockFramePoint } from '../core/geometry';
import { INTEGRATED_BLOCK } from '../core/integrated-block';
@@ -294,8 +294,8 @@ export function buildRebar3D(m: PierModel, common: Material = DEFAULT_MATERIAL,
const sc = c.s;
const base = c.bottom;
const rt = mm(col.tieRadius);
for (let i = 0; i < COLUMN_P1.nDiv; i++) {
const th = (2 * Math.PI * i) / COLUMN_P1.nDiv;
for (let i = 0; i < colInput.nDiv; i++) {
const th = (2 * Math.PI * i) / colInput.nDiv;
const r1 = mm(col.r1);
addBar('P1', c.k, [sc + r1 * Math.cos(th), base + mm(zBot), r1 * Math.sin(th)], [sc + r1 * Math.cos(th), base + mm(col.zTop1), r1 * Math.sin(th)], NOMINAL_DIAMETER.S32, COLOR.column);
// 90° bottom hook of P1, outward (기둥철근.md §3.2, Q-C5): the 12·d tail is drawn, the bend is approximated by the tail's start.
@@ -312,8 +312,8 @@ export function buildRebar3D(m: PierModel, common: Material = DEFAULT_MATERIAL,
// Main bar couplers (D32, L150 Ø60, 4단 × 46줄 × 2열 = 368개): P1 at col.couplerP1, P2 at col.couplerP2 (+150 mm)
const r1 = mm(col.r1);
const r2 = mm(col.r2);
for (let i = 0; i < COLUMN_P1.nDiv; i++) {
const th = (2 * Math.PI * i) / COLUMN_P1.nDiv;
for (let i = 0; i < colInput.nDiv; i++) {
const th = (2 * Math.PI * i) / colInput.nDiv;
const cosTh = Math.cos(th);
const sinTh = Math.sin(th);
// P1 couplers (184)
@@ -334,7 +334,7 @@ export function buildRebar3D(m: PierModel, common: Material = DEFAULT_MATERIAL,
// Tie couplers (D19, L100 Ø40, 총 35개): P5 1개 (19개) · P4 2개 (16개)
col.p5.forEach((z, j) => {
const th = (2 * Math.PI * (j * 7 + 0.5)) / COLUMN_P1.nDiv;
const th = (2 * Math.PI * (j * 7 + 0.5)) / colInput.nDiv;
const cx = sc + rt * Math.cos(th);
const cz = rt * Math.sin(th);
const cy = base + mm(z);
@@ -343,7 +343,7 @@ export function buildRebar3D(m: PierModel, common: Material = DEFAULT_MATERIAL,
addBar('띠철근 커플러', c.k, [cx - dx, cy, cz - dz], [cx + dx, cy, cz + dz], 40, COLOR.couplerTie);
});
col.p4.forEach((z) => {
const angles = [(2 * Math.PI * 0.5) / COLUMN_P1.nDiv, (2 * Math.PI * 0.5) / COLUMN_P1.nDiv + Math.PI];
const angles = [(2 * Math.PI * 0.5) / colInput.nDiv, (2 * Math.PI * 0.5) / colInput.nDiv + Math.PI];
angles.forEach((th) => {
const cx = sc + rt * Math.cos(th);
const cz = rt * Math.sin(th);