Files
Pier_v1/src/core/rebar/summary.ts
T
ypshimandClaude Haiku 5.5 ac646d554c feat(shape): 꺾임점에서 겹치는 기초를 하나의 꺾인 기초로 합친다
꺾임을 넘는 기초를 합치지 않아, 기둥 간격이 기초 길이보다 짧은 꺾인 교각(산외교 P11: 2.75 m)에서 두 기초가
겹치고(콘크리트 쐐기 틈, 철근 겹침, 수량 이중 계산) 말뚝도 겹쳤다. 꺾임 전후 기초가 겹치거나 미터 퍼짐 안에서
맞닿으면 하나의 꺾인 기초로 본다:
- 형상: 구간별 조각 둘(bent), 각 조각을 미터 퍼짐 + 10 mm 너머로 늘려 미터면에서 틈 없이 잇는다
- 말뚝: 편 길이에 한 번 배치하고 꺾임점 기준으로 구간을 나눈다
- 철근: 편 좌표의 한 기초(footing-units.ts)로 철근표와 3D가 같이 계산, 3D는 미터점에서 접는다

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
2026-11-10 17:46:47 +09:00

457 lines
37 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { PierModel } from '../geometry';
import { barLength, bearingBlockRebar, DEFAULT_BLOCK_REBAR, totals } from './bearing-block';
import { copingDefaults } from './coping-settings';
import { DEFAULT_COLUMN_REBAR } from './column-settings';
import { DEFAULT_FOOTING_REBAR, type FootingRebarSettings } from './footing-settings';
import { bottomPiece, topBarLength, widthLayout, topSplicePieces } from './coping-main';
import { shearBars, shearBarsMulti, copingSideBars, SIDE_HOOK_LEG, c14Bars, c14Length, COPING_REBAR, tStepBars, stepDevLength, undersideAt, shearULength, capLength, PIECES_PER_POSITION } from './coping';
import { sideRowLevels } from '../coping-geom';
import { CAST_PENDING } from './cast-in-place';
import { castRebarLayout } from './cast-layout';
import { multicolCopingInfo } from './multicol-coping';
import { stepBlockBars } from './step-bars';
import { footingUnits } from './footing-units';
import { columnRebar, columnInputFor } from './column';
import { bottomBars, crossBars, sideBars, assemblyBars, shearB9 } from './footing';
import { DEFAULT_MATERIAL, developmentLength, FckByMember, fyOf, isAssumedMaterial, Material, materialFor, NOMINAL_DIAMETER, splitPieces, STANDARD_BAR } from './common';
/** 근거 badge (spec §6.1): where the value comes from. */
export type Basis = 'input' | 'default' | 'calc' | 'assumed';
/** One line of a 철근표. length is null where the document gives no length yet. */
export interface RebarRow {
member: 'bearing' | 'coping' | 'column' | 'footing' | 'pile';
name: string;
size: string;
count: number;
/** One length (mm) when the row has a single length; null when the row has several (see pieces). */
length: number | null;
/** Lengths of the bars in this row, as pieces: count bars of length (mm). Their sum is the row's total (재료표). */
pieces?: { length: number; count: number }[];
note: string;
/** 간격 (설계 간격, mm or text), 실간격 (cycle 묶음 기준), 위치, 방향, 형상, 근거. */
spacing: string;
actual: string;
position: string;
direction: string;
shape: string;
basis: Basis;
/** Explicit dedicated-section properties; absent for legacy rows. */
diameterMm?: number;
gradeMpa?: number;
}
export interface RebarWarning {
code: string;
message: string;
/** Where the alert points: a member tab, or the 공통 설정 (for I02). */
target: RebarRow['member'] | 'common';
/** The bar the alert is about, so that [보기] can highlight its row. */
bar?: string;
}
/** 다주형 코핑 번호별 위치 (3주형형상배근.md §3, 번호 = 역할). */
const MULTI_NOTE: Record<string, string> = {
C1: '상면 1단 cycle-1, 양끝 갈고리, 8 m 이음', C4: '상면 1단 cycle-2, 직선, 8 m 이음 (± l_s 엇갈림)',
C3: '상면 2단 cycle-1, 끝단 쪽 (기둥 중심 + 4450까지)', C2: '상면 2단 cycle-2, 끝단 쪽',
'C3-1': '상면 2단 cycle-1, 가운데 기둥 위 ± 4450', 'C2-1': '상면 2단 cycle-2, 가운데 기둥 위',
C7: '하면 전 길이, Level 꺾임점 겹침, 양끝 위로 갈고리', C8: '경간마다 오목 하면, 기둥 중심 ± 2230 안쪽',
C9: '전단 · 테이퍼·헌치', C10: '전단 · 기둥 Level', C11: '전단 · 기둥 사이 오목',
C12: '측면 상단 (전 길이), 다리 위치마다', C13: '측면 하단 변화구간 (하면에서 끊김), 다리 위치마다',
C14: '크로스타이 연직 600 · 교각축 1200 엇갈림', C15: '계단부 ⊓ (가장 높은 계단)', C16: '계단부 L형 (그 밖의 계단, 낮은 쪽 다리)',
C17: '전·후열 단차 300 이상 블록 ㄷ형 띠 (전단간격)', C18: '전·후열 단차 블록 수평 피부철근 (측면간격)',
};
/** Table metadata per bar (코핑철근.md, 확대기초철근 (1).md, 기둥철근 (1).md, 받침블럭철근.md). */
const META: Record<string, Pick<RebarRow, 'spacing' | 'actual' | 'position' | 'direction' | 'shape' | 'basis'>> = {
S1: { spacing: '125', actual: '125', position: '블럭 상면 − 69.1', direction: '교각축 직각', shape: '양끝 다리 꺾임', basis: 'calc' },
S2: { spacing: '125', actual: '125', position: '블럭 상면 − 50', direction: '교각축', shape: '양끝 다리 꺾임', basis: 'calc' },
C1: { spacing: '250', actual: '125', position: '상면 1단 (cycle-1)', direction: '교각축', shape: '양끝 꺾임', basis: 'calc' },
C2: { spacing: '250', actual: '125', position: '상면 2단 (cycle-1)', direction: '교각축', shape: '양끝 꺾임', basis: 'calc' },
C3: { spacing: '250', actual: '125', position: '상면 3단 (cycle-1)', direction: '교각축', shape: '양끝 꺾임', basis: 'calc' },
C4: { spacing: '250', actual: '125', position: '상면 1단 (cycle-2)', direction: '교각축', shape: '양끝 꺾임', basis: 'calc' },
C5: { spacing: '250', actual: '125', position: '상면 2단 (cycle-2)', direction: '교각축', shape: '양끝 꺾임', basis: 'calc' },
C6: { spacing: '250', actual: '125', position: '상면 3단 (cycle-2)', direction: '교각축', shape: '양끝 꺾임', basis: 'calc' },
C7: { spacing: '250', actual: '125', position: '하면 1단 (cycle-1)', direction: '교각축', shape: '하면 추종, 양끝 꺾임, 2조각 겹침', basis: 'calc' },
C8: { spacing: '250', actual: '125', position: '하면 1단 (cycle-2)', direction: '교각축', shape: '하면 추종, 양끝 꺾임, 2조각 겹침', basis: 'calc' },
C9: { spacing: '250', actual: '250', position: '전단 좌 경사', direction: '연직', shape: '폐합 (하부 U + 상부 삿갓)', basis: 'assumed' },
C10: { spacing: '300', actual: '300', position: '전단 Level', direction: '연직', shape: '폐합 (하부 U + 상부 삿갓)', basis: 'assumed' },
C11: { spacing: '250', actual: '250', position: '전단 우 경사', direction: '연직', shape: '폐합 (하부 U + 상부 삿갓)', basis: 'assumed' },
C12: { spacing: '250', actual: '250', position: '측면 상단 (끝면까지)', direction: '교각축', shape: '일자 + 좌우 ㄷ형', basis: 'calc' },
C13: { spacing: '250', actual: '250', position: '측면 하단 변화구간 (경사 하면에서 끊김)', direction: '교각축', shape: '일자 + 좌우 ㄷ형', basis: 'calc' },
C14: { spacing: '1000 / 2000', actual: '1000', position: '직각 수평 (위 2줄 · 아래 엇갈림)', direction: '교각축 직각', shape: '⊓ (폭 방향)', basis: 'calc' },
C15: { spacing: '250', actual: '250', position: '계단부 가장 높은 구간', direction: '교각축', shape: '거꾸로 된 U', basis: 'calc' },
C16: { spacing: '250', actual: '250', position: '계단부 그 밖의 구간 (최저 상면 + 100 이상)', direction: '교각축', shape: 'L형', basis: 'calc' },
C17: { spacing: '250', actual: '250', position: '전·후열 단차 300 이상 블록', direction: '교각축 직각', shape: 'ㄷ형', basis: 'calc' },
C18: { spacing: '250', actual: '250', position: '전·후열 단차 블록 단차면', direction: '교각축', shape: '직선', basis: 'calc' },
P1: { spacing: '원주 46등분', actual: '—', position: '외측 r 875', direction: '연직', shape: '아래 끝 90° 갈고리, 커플러 이음', basis: 'calc' },
P2: { spacing: '원주 46등분', actual: '—', position: '내측 r 843.2', direction: '연직', shape: '직선, 커플러 이음', basis: 'calc' },
'주철근 커플러': { spacing: '@3000 (P2 +150)', actual: '—', position: 'P1·P2 각 줄', direction: '원통', shape: 'L 150 · Ø60', basis: 'default' },
'띠철근 커플러': { spacing: '120', actual: '120', position: '단부 P5 · 기초안 P4', direction: '원통', shape: 'L 100 · Ø40', basis: 'assumed' },
커플러: { spacing: '@3000 (P2 +150)', actual: '—', position: 'P1·P2 각 줄', direction: '원통', shape: 'L 150 · Ø60', basis: 'default' },
P3: { spacing: '300', actual: '300', position: '일반 구간 (코핑 안 포함)', direction: '원형', shape: '겹침 (추천안)', basis: 'assumed' },
P5: { spacing: '120', actual: '120', position: '단부 구간 (첫 P3 아래)', direction: '원형', shape: '반원 2개가 아닌 원형 1개, 커플러 1개', basis: 'calc' },
P4: { spacing: '120', actual: '120', position: '확대기초 안 (마지막 P5 아래)', direction: '반원 2개', shape: '반원 2개, 커플러 2개', basis: 'calc' },
P6: { spacing: '1800 (단)', actual: '—', position: '첫 P4부터, 단마다 4본', direction: 'U형', shape: '양끝 90° 갈고리', basis: 'calc' },
B1: { spacing: '250', actual: '250', position: 'B5 아래 2층', direction: '교각축', shape: '⊓ 양끝 아래로', basis: 'assumed' },
B2: { spacing: '250', actual: '125', position: '하면 1단 최외측 (cycle-2)', direction: '교각축', shape: '⊔, 다리 B1 높이까지, 2조각', basis: 'calc' },
B3: { spacing: '250', actual: '250', position: '하면 2단 (B2·B4 + 100)', direction: '교각축', shape: '⊔ 양끝 위로', basis: 'assumed' },
B4: { spacing: '250', actual: '125', position: '하면 1단 최외측 (cycle-1)', direction: '교각축', shape: '직선', basis: 'calc' },
B5: { spacing: '250', actual: '250', position: '상면 최외측', direction: '교각축 직각', shape: '⊓ 양끝 아래로', basis: 'assumed' },
B6: { spacing: '250', actual: '125', position: '하면 1단 (cycle-2)', direction: '교각축 직각', shape: '⊔, 다리 B5 높이까지', basis: 'calc' },
B7: { spacing: '250', actual: '250', position: '하면 2단 (B6·B8 + 100)', direction: '교각축 직각', shape: '⊔ 양끝 위로', basis: 'assumed' },
B8: { spacing: '250', actual: '125', position: '하면 1단 (cycle-1)', direction: '교각축 직각', shape: '직선', basis: 'calc' },
'B10-1': { spacing: '200', actual: '200', position: '폭 W 양끝 면, 다리 안쪽', direction: '교각축', shape: '양끝 90° 갈고리', basis: 'calc' },
'B10-2': { spacing: '200', actual: '200', position: '길이 L 양끝 면, 다리 안쪽', direction: '교각축 직각', shape: '양끝 90° 갈고리', basis: 'calc' },
B11: { spacing: '500 (4s)', actual: '500', position: '조립 단 (B10-2 높이)', direction: '교각축 직각', shape: 'U 양끝 아래로', basis: 'calc' },
B12: { spacing: '500 (4s)', actual: '500', position: '조립 단 (B10-1 높이)', direction: '교각축', shape: 'U 양끝 아래로', basis: 'calc' },
'C9·C11 사이': { spacing: '250', actual: '250', position: '기둥 사이 (양쪽 C10에서 안쪽)', direction: '연직', shape: '폐합 (추천안, Q-K2)', basis: 'assumed' },
B9: { spacing: '500 (4s)', actual: '250 (2s 엇갈림)', position: '연직, B5–B2·B4 감쌈', direction: '연직', shape: '양끝 135° 갈고리', basis: 'assumed' },
};
const FALLBACK: Pick<RebarRow, 'spacing' | 'actual' | 'position' | 'direction' | 'shape' | 'basis'> = {
spacing: '—', actual: '—', position: '—', direction: '—', shape: '—', basis: 'calc',
};
export { STANDARD_BAR };
/**
* 철근표 for one pier (③ screen v1): bearing blocks, coping, column, footing. Built from the generated model.
* Values come from the document rules in src/core/rebar/*. B9 (전단철근) is not computed yet.
*/
interface FootingBar {
name: string;
size: string;
count: number;
length: number;
note: string;
}
/** The footing bars of one footing (B1–B12 and B9), with the notes used in the 철근표. */
function footingBarList(Lf: number, Wf: number, Hf: number, columnX: number[], f: FootingRebarSettings): FootingBar[] {
const out: FootingBar[] = [];
for (const b of bottomBars(Lf, Wf, Hf, f)) out.push({ ...b, note: '교각축' });
for (const b of crossBars(Lf, Wf, Hf, f)) out.push({ ...b, note: '교각축 직각' });
for (const b of sideBars(Lf, Wf, Hf, f)) out.push({ ...b, note: '측면 수평' });
for (const b of assemblyBars(Lf, Wf, Hf, f)) out.push({ ...b, note: '조립 수평' });
const b9 = shearB9(Lf, Wf, Hf, columnX, 2000, f);
out.push({ name: 'B9', size: 'H16', count: b9.count, length: b9.length, note: `전단 @500 엇갈림, ${b9.rows}행, 기둥 구역 제외 (위치·갈고리는 Q-B18·B19 규칙 적용)` });
return out;
}
type RawRow = Omit<RebarRow, 'spacing' | 'actual' | 'position' | 'direction' | 'shape' | 'basis'>;
const MAIN_BARS = ['C1', 'C2', 'C3', 'C4', 'C5', 'C6', 'C7', 'C8'];
/**
* 간격·실간격 and 근거 of the rows that have a spacing input (승인 2026-10-26). 간격 is the design spacing (the cycle
* pitch for C1–C8, which alternate, so 2 × 기본간격); 실간격 is the spacing of the bars themselves. The basis is '기본값'
* when every input behind the row is at its default, otherwise '입력'.
*/
export function linkedSpacing(name: string, model: PierModel): Pick<RebarRow, 'spacing' | 'actual' | 'basis'> | undefined {
const c = model.copingRebar;
const d = copingDefaults(model.columns.length);
const b = model.blockRebar;
const bd = DEFAULT_BLOCK_REBAR;
const basis = (changed: boolean): Basis => (changed ? 'input' : 'default');
if (MAIN_BARS.includes(name)) {
return { spacing: String(2 * c.mainSpacing), actual: String(c.mainSpacing), basis: basis(c.mainSpacing !== d.mainSpacing) };
}
if (name === 'C15' || name === 'C16') {
// C15·C16 are on the same width pitch as the main bars (2 × 기본간격); 250 at P1.
return { spacing: String(2 * c.mainSpacing), actual: String(2 * c.mainSpacing), basis: basis(c.mainSpacing !== d.mainSpacing) };
}
if (name === 'C9' || name === 'C11') return { spacing: String(c.spacingS), actual: String(c.spacingS), basis: basis(c.spacingS !== d.spacingS) };
if (name === 'C10') return { spacing: String(c.spacingL), actual: String(c.spacingL), basis: basis(c.spacingL !== d.spacingL) };
if (name === 'C12' || name === 'C13') return { spacing: String(c.sideSpacing), actual: String(c.sideSpacing), basis: basis(c.sideSpacing !== d.sideSpacing) };
if (name === 'C14') {
return {
spacing: `${c.c14UpperSpacing} / ${c.c14LowerSpacing}`,
actual: String(c.c14UpperSpacing),
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) };
// 확대기초 (승인 2026-10-26): B1–B8 간격 2s (k = 2), 실간격 s on the cycle bars (B2·B4·B6·B8); B9 4s (2s 엇갈림);
// B10 s_v; B11·B12 4s. The basis is 기본값 unless the grid spacing s or s_v differs from its default.
const fs = model.footingRebar;
const fd = DEFAULT_FOOTING_REBAR;
if (/^B[1-4]$/.test(name)) {
const cycle = name === 'B2' || name === 'B4';
return { spacing: String(2 * fs.sL), actual: String(cycle ? fs.sL : 2 * fs.sL), basis: basis(fs.sL !== fd.sL) };
}
if (/^B[5-8]$/.test(name)) {
const cycle = name === 'B6' || name === 'B8';
return { spacing: String(2 * fs.sW), actual: String(cycle ? fs.sW : 2 * fs.sW), basis: basis(fs.sW !== fd.sW) };
}
if (name === 'B9') return { spacing: `${4 * fs.sL} (4s)`, actual: `${2 * fs.sL} (2s 엇갈림)`, basis: basis(fs.sL !== fd.sL) };
// B11 along L uses the 교각축 직각 grid, B12 along W uses the 교각축 grid; each is k · s (승인 2026-10-26).
if (name === 'B11') return { spacing: `${fs.kB11 * fs.sL} (${fs.kB11}s)`, actual: String(fs.kB11 * fs.sL), basis: basis(fs.sL !== fd.sL || fs.kB11 !== fd.kB11) };
if (name === 'B12') return { spacing: `${fs.kB12 * fs.sW} (${fs.kB12}s)`, actual: String(fs.kB12 * fs.sW), basis: basis(fs.sW !== fd.sW || fs.kB12 !== fd.kB12) };
if (name === 'B10-1' || name === 'B10-2') return { spacing: String(fs.sV), actual: String(fs.sV), basis: basis(fs.sV !== fd.sV) };
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;
}
export function rebarSummary(
model: PierModel,
common: Material = DEFAULT_MATERIAL,
byMember: FckByMember = {},
): { rows: RebarRow[]; warnings: RebarWarning[]; incomplete: string[] } {
const rows: RawRow[] = [];
const warnings: RebarWarning[] = [];
const mCoping = materialFor(common, byMember, 'coping');
const mColumn = materialFor(common, byMember, 'column');
const cast = model.castInput ? castRebarLayout(model, mColumn) : undefined;
const mFooting = materialFor(common, byMember, 'footing');
// The I02 note stays while the common values are the assumed ones, unless every fck-bearing member has its own fck.
const ownFck = (['column', 'coping', 'footing'] as const).every((k) => byMember[k] !== undefined);
if (isAssumedMaterial(common) && !ownFck) {
warnings.push({ code: 'I02', message: `fck ${common.fck} MPa는 가정값입니다. 정착·이음 길이에 반영됩니다. 공통 설정에서 확인하세요.`, target: 'common' });
}
const L = model.copingL * 1000;
const B = model.copingB * 1000;
// Bearing blocks (§6.3 · plan C2)
// Embedment in the coping (받침블럭철근.md §4): l_leg − (block height − bar depth), with the model's block heights.
const blocks = bearingBlockRebar(model);
const blk = totals(blocks);
const embedments = (name: 'S1' | 'S2') => blocks.flatMap((b) => b.bars.filter((r) => r.name === name).map((r) => r.embedment));
const emb1 = embedments('S1');
const emb2 = embedments('S2');
const range = (xs: number[]) => `${Math.round(Math.min(...xs))}–${Math.round(Math.max(...xs))}`;
const anchorage = stepDevLength(mCoping); // RB-S04: the embedment must reach the H19 development length
const shortest = Math.min(...emb1, ...emb2);
if (shortest < anchorage) {
warnings.push({ code: 'RB-S04', message: `받침블럭 철근 묻힘 ${Math.round(shortest)} mm < 정착길이 ${anchorage} mm (코핑 안 묻힘 부족)`, target: 'bearing' });
}
rows.push({ member: 'bearing', name: 'S1', size: 'H19', count: blk.S1, length: barLength(1200, model.blockRebar), note: `블럭 배치 분리, 블럭당 8본 · 묻힘 ${range(emb1)} (블럭 상면 − 69.1)` });
rows.push({ member: 'bearing', name: 'S2', size: 'H19', count: blk.S2, length: barLength(1200, model.blockRebar), note: `블럭 배치 분리, 블럭당 8본 · 묻힘 ${range(emb2)} (블럭 상면 − 50)` });
// Coping (코핑철근.md §5–§8)
const colS = model.columns.map((c) => c.s * 1000);
const nCol = Math.max(1, colS.length);
// A bent multi-column coping keeps the multi-column rules (2026-11-10); only one column uses the T형 rows.
if (nCol <= 1) {
const width = widthLayout(B, model.copingRebar);
const perRow = width.count / 2; // cycle split, 12 per row
// CP04 (승인 2026-10-26): the cycle split needs an even count (n + 3), so n must be odd. Warning only; the input is kept.
if (width.count % 2 !== 0) {
warnings.push({ code: 'CP04', message: `주철근 개수 ${width.count}(n+3)은 홀수라 cycle-1·cycle-2를 나눌 수 없습니다 (기본간격 ${width.spacing}, 끝단 여유 ${model.copingRebar.mainEndGap}). 기본간격을 확인하세요.`, target: 'coping' });
}
// C1–C3 and C4–C6 are the same three top rows (row 1, 2, 3), one set per cycle.
const AT_LABEL = { center: '기둥 중심', end: '단부' } as const;
for (const [name, k] of [['C1', 1], ['C2', 2], ['C3', 3], ['C4', 1], ['C5', 2], ['C6', 3]] as const) {
const sp = model.topSplice.mode === 'apply' ? topSplicePieces(L, k, model.topSplice.at, mCoping) : null;
if (sp) {
rows.push({ member: 'coping', name, size: 'H29', count: perRow, length: null, pieces: [{ length: sp.pieces[0], count: perRow }, { length: sp.pieces[1], count: perRow }], note: `상면, 2조각 겹침이음 (${AT_LABEL[model.topSplice.at]}, l_s ${Math.round(sp.lap)})` });
} else {
rows.push({ member: 'coping', name, size: 'H29', count: perRow, length: topBarLength(L, k), note: '상면, 이음 무시' });
}
}
const bottom = bottomPiece(mCoping);
rows.push({ member: 'coping', name: 'C7', size: 'H29', count: perRow, length: bottom.piece, note: '하면, 2조각 겹침이음' });
rows.push({ member: 'coping', name: 'C8', size: 'H29', count: perRow, length: bottom.piece, note: '하면, 2조각 겹침이음' });
const geom = model.copingGeom;
const shear = nCol > 1 ? shearBarsMulti(L, colS, geom) : { ...shearBars(L, undefined, geom), between: [] as number[] };
const uRange = (xs: number[]) => {
const u = xs.map((x) => Math.round(shearULength(x, L, geom)));
return `${Math.min(...u)}–${Math.max(...u)}`;
};
const capRange = (xs: number[]) => {
const c = xs.map((x) => Math.round(capLength(x, L, geom)));
return `${Math.min(...c)}–${Math.max(...c)}`;
};
const shearNote = (label: string, xs: number[]) =>
`${label}, 하부 U ${uRange(xs)}, 삿갓 ${capRange(xs)} (l_cap 추천안, Q-K5), 조각 ${xs.length * PIECES_PER_POSITION}`;
// Each position has three 폐합, each with one 하부 U and one 삿갓 (6 pieces). Pieces: U and 삿갓 lengths at each position.
const loopPieces = (xs: number[]) => xs.flatMap((x) => [
{ length: shearULength(x, L, model.copingGeom), count: 3 },
{ length: capLength(x, L, model.copingGeom), count: 3 },
]);
rows.push({ member: 'coping', name: 'C9', size: 'H25', count: shear.c9.length, length: null, pieces: loopPieces(shear.c9), note: shearNote('전단 좌 @250', shear.c9) });
rows.push({ member: 'coping', name: 'C10', size: 'H25', count: shear.c10.length, length: null, pieces: loopPieces(shear.c10), note: shearNote('전단 Level @300', shear.c10) });
rows.push({ member: 'coping', name: 'C11', size: 'H25', count: shear.c11.length, length: null, pieces: loopPieces(shear.c11), note: shearNote('전단 우 @250', shear.c11) });
if (shear.between.length > 0) {
rows.push({ member: 'coping', name: 'C9·C11 사이', size: 'H25', count: shear.between.length, length: null, pieces: loopPieces(shear.between), note: `기둥 사이, 양쪽 C10에서 안쪽으로 (추천안, Q-K2), 조각 ${shear.between.length * PIECES_PER_POSITION}` });
}
if (nCol > 1) {
warnings.push({ code: 'RB-M01', message: `다주형 ${nCol}기둥: C14는 전용 삽도의 H16·기본600 간격과 실제 하면을 사용합니다. 기둥별 반복·번호 대응 상세는 확인이 필요합니다.`, target: 'coping' });
}
// Per row: two 일자 (side faces) and two ㄷ (one at each end), so 4 bars per row (코핑철근.md §6.2).
const levels = sideRowLevels(model.levelBottom, model.endBottom, model.copingRefTop);
const side = copingSideBars(model.copingL * 1000, levels.zBottom, levels.zBreak, model.copingGeom, levels.zTop);
const hook = model.copingB * 1000 - 2 * 127 + 2 * SIDE_HOOK_LEG; // ㄷ body across the width + two legs
const c13Range = side.c13.length ? `${Math.round(side.c13[0].straight)}–${Math.round(side.c13[side.c13.length - 1].straight)}` : '—';
// Per bar: 2 일자 (straight) and 2 ㄷ (hook body + legs), so 4 bars per row position.
const sidePieces = (bars: { straight: number }[]) => bars.flatMap((b) => [{ length: b.straight, count: 2 }, { length: hook, count: 2 }]);
rows.push({ member: 'coping', name: 'C12', size: 'H25', count: 4 * side.c12.length, length: null, pieces: sidePieces(side.c12), note: `측면 상단, 끝면까지, 일자 (${2 * side.c12.length}) · ㄷ ${Math.round(hook)} (${2 * side.c12.length})` });
rows.push({ member: 'coping', name: 'C13', size: 'H25', count: 4 * side.c13.length, length: null, pieces: sidePieces(side.c13), note: `측면 하단 변화구간, 경사 하면에서 끊김, 일자 ${c13Range} (${2 * side.c13.length}) · ㄷ ${Math.round(hook)} (${2 * side.c13.length}) · 도면 하단 단 중 하면 단(EL ${(model.levelBottom + 0.127).toFixed(3)})은 하면 주철근 C7·C8로 계산 (문서 §6.1)` });
// C14 / C15 / C16 are derived from the model (코핑철근.md §7–8); the document's 38 / 11 / 22 and 3852 / 4070 / 4069 are checked in the tests.
const L14 = model.copingL * 1000;
const c14 = c14Bars(L14, model.copingRefTop, [...side.c13, ...side.c12], (x) => undersideAt(L14, x, model.endBottom, model.levelBottom, model.copingGeom.levelWidth), model.copingRebar.mainLayerGap / 1000);
const lineCount = (line: number) => c14.filter((b) => b.line === line).length;
const omitted = c14.filter((b) => !b.legs).length;
rows.push({ member: 'coping', name: 'C14', size: 'H19', count: c14.length, length: c14Length(model.copingB * 1000), note: `위 2 줄 ${lineCount(1)} · ${lineCount(2)}본씩, 아래 ${[3, 4, 5, 6].map(lineCount).join(' · ')} (합 ${c14.length}) · 연직 발 생략 ${omitted}개 (하면 밖)` });
// C15 · C16 from the model's own steps (결정 2026-11-10): none when no step is h_16 above the lowest top.
const steps = tStepBars(model, mCoping);
for (const name of ['C15', 'C16'] as const) {
const bars = steps.bars.filter((b) => b.name === name);
if (!bars.length) continue;
const byLength = new Map<number, number>();
for (const b of bars) byLength.set(Math.round(b.length), (byLength.get(Math.round(b.length)) ?? 0) + 1);
const pieces = [...byLength].map(([length, count]) => ({ length, count }));
rows.push({ member: 'coping', name, size: 'H19', count: bars.length, length: pieces.length === 1 ? pieces[0].length : null, ...(pieces.length > 1 ? { pieces } : {}),
note: `${name === 'C15' ? '계단부, 가장 높은 구간 (거꾸로 U)' : '계단부, 그 밖의 계단 구간 (L형, 낮은 쪽 다리)'} · ${pieces.map((q) => `${q.length} × ${q.count}`).join(' / ')}` });
}
for (const message of steps.warnings) warnings.push({ code: 'RB-S01', target: 'coping', message });
// 전·후열 단차 블록 보강 C17 · C18 (결정 2026-11-10): 단차 300 mm 이상인 구간만.
const block = stepBlockBars(model, { cover: COPING_REBAR.cover, d: NOMINAL_DIAMETER.H16, ld: Math.round(developmentLength(NOMINAL_DIAMETER.H16, fyOf(mCoping, 'H16'), mCoping.fck)),
tieSpacing: model.copingRebar.spacingS, skinSpacing: model.copingRebar.sideSpacing });
for (const name of ['C17', 'C18'] as const) {
const bars = block.filter((b) => b.name === name);
if (!bars.length) continue;
const byLength = new Map<number, number>();
for (const b of bars) byLength.set(Math.round(b.length), (byLength.get(Math.round(b.length)) ?? 0) + 1);
const pieces = [...byLength].map(([length, count]) => ({ length, count }));
rows.push({ member: 'coping', name, size: 'H16', count: bars.length, length: pieces.length === 1 ? pieces[0].length : null, ...(pieces.length > 1 ? { pieces } : {}),
note: name === 'C17' ? '전·후열 단차 300 이상 블록, ㄷ형 띠 (다리 낮은 상면 아래 l_d)' : '전·후열 단차 블록, 단차면 수평 피부철근' });
}
}
// Column (기둥철근.md · plan C4), P1 defaults
if (model.footings.length > 0) {
const columnRows: RawRow[] = [];
for (let columnIndex = 0; columnIndex < model.columns.length; columnIndex++) {
const first = rows.length;
const input = columnInputFor(model, undefined, columnIndex);
const col = columnRebar(input, mColumn);
rows.push({ member: 'column', name: 'P1', size: 'S32', count: col.mainP1, length: null, pieces: col.p1Pieces.map((length) => ({ length, count: input.nDiv })), 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: input.nDiv })), note: `내측 r ${col.r2}, 마디 ${col.p2Pieces.join(' / ')}` });
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: `일반 구간 띠철근 @${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: `크로스타이, ${col.p6Levels.length}단 × 4` });
columnRows.push(...rows.splice(first));
}
for (const r of columnRows) {
const existing = rows.find(v => v.member === 'column' && v.name === r.name);
if (!existing) rows.push({ ...r, pieces: r.pieces ? [...r.pieces] : undefined });
else {
if (existing.pieces || r.pieces || existing.length !== r.length) {
existing.pieces ??= [{ length: existing.length!, count: existing.count }];
existing.pieces.push(...(r.pieces ?? [{ length: r.length!, count: r.count }]));
existing.length = null;
}
existing.count += r.count;
existing.note = '기둥별 실제 배치·길이 합산 · 마디별 수량은 각 기둥의 원주 등분 수';
}
}
} else if (model.castInput) {
const layout = cast!;
const grouped = new Map<string, typeof layout.bars>();
for (const bar of layout.bars) {
const key = `${bar.name}|${bar.diameterMm}|${bar.gradeMpa}`;
const group = grouped.get(key) ?? [];
group.push(bar); grouped.set(key, group);
}
for (const group of grouped.values()) {
const first = group[0];
const byLength = new Map<number, { length: number; count: number }>();
for (const bar of group) {
const key = Math.round(bar.lengthMm * 1e6);
const piece = byLength.get(key) ?? { length: bar.lengthMm, count: 0 };
piece.count++; byLength.set(key, piece);
}
const pieces = [...byLength.values()];
rows.push({ member: first.member, name: first.name, size: first.kind === 'coupler' ? `커플러 Ø${first.diameterMm.toFixed(1)}` : `Ø${first.diameterMm}`,
count: group.length, length: pieces.length === 1 ? pieces[0].length : null, pieces,
diameterMm: first.diameterMm, gradeMpa: first.gradeMpa, note: first.note });
}
layout.missing.forEach(message => warnings.push({ code: 'RB-CIP01', message, target: 'column' }));
} else {
warnings.push({ code: 'RB-CIP01', message: CAST_PENDING.join(' · ') + ' — 전용 입력을 저장할 수 있으며 3D·철근표·재료표 계산은 상세 확정 후 제공합니다.', target: 'column' });
warnings.push({ code: 'RB-CIP02', message: '현장타설말뚝 철근 수량은 미계산입니다. 지원되는 다른 부재만 집계합니다.', target: 'pile' });
}
// Footing (확대기초철근 (1).md · plan C5). Only when the footing exists (not 현장타설말뚝).
if (model.footings.length > 0) {
// Each footing (분리 or 일체) gives the same bar set. Several footings add their counts per bar name;
// the length is the longest of them (the largest footing governs the bar).
// Per footing (2026-11-10): the columns of its own stretch only, and every bar length kept (a bent pier has footings
// of different lengths; the longest no longer stands for all).
const byName = new Map<string, { first: FootingBar; lengths: Map<number, number> }>();
// One unit per real footing: a bent footing across the kink is one footing in unfolded g (footing-units.ts).
for (const f of footingUnits(model)) {
for (const b of footingBarList(f.length * 1000, f.width * 1000, (f.top - f.bottom) * 1000, f.columnX, model.footingRebar)) {
const cur = byName.get(b.name) ?? { first: b, lengths: new Map<number, number>() };
cur.lengths.set(b.length, (cur.lengths.get(b.length) ?? 0) + b.count);
byName.set(b.name, cur);
}
}
for (const { first: b, lengths } of byName.values()) {
// Over 정척: two pieces lapped (추천안, Q-B6); both pieces count (the row used to weigh only one per bar).
const pieces: { length: number; count: number }[] = [];
let lap: number | null = null;
for (const [length, count] of lengths) {
const split = b.name === 'B9' ? null : splitPieces(length, 25.4, fyOf(mFooting, b.size), mFooting.fck, STANDARD_BAR);
if (split) { pieces.push({ length: split.pieceLength, count: 2 * count }); lap = split.lap; } else pieces.push({ length, count });
}
const count = [...lengths.values()].reduce((sum, n) => sum + n, 0);
const longest = Math.max(...lengths.keys());
if (lap !== null) {
rows.push({ member: 'footing', name: b.name, size: b.size, count, length: pieces.length === 1 ? pieces[0].length : null, pieces,
note: `${b.note}, 2조각 이음 (추천안, 겹침 ${Math.round(lap)})` });
warnings.push({ code: 'RB-F08', message: `${b.name} 길이 ${Math.round(longest)} mm > 정척 ${STANDARD_BAR} mm: 2조각 이음을 추천안으로 적용 (Q-B6).`, target: 'footing', bar: b.name });
} else {
rows.push({ member: 'footing', name: b.name, size: b.size, count, length: pieces.length === 1 ? pieces[0].length : null, ...(pieces.length > 1 ? { pieces } : {}), note: b.note });
}
}
} else {
warnings.push({ code: 'RB-F00', message: '현장타설말뚝은 확대기초가 없어 확대기초 철근을 만들지 않습니다.', target: 'footing' });
}
if (nCol > 1) {
const info = multicolCopingInfo(model, mCoping);
const grouped = new Map<string, typeof info.bars>();
for (const b of info.bars) {
const group = grouped.get(b.name) ?? [];
group.push(b); grouped.set(b.name, group);
}
for (const [name, bars] of grouped) rows.push({ member: 'coping', name, size: bars[0].size,
count: bars.length, length: null, pieces: bars.map(b => ({ length: b.length, count: 1 })),
note: `다주형 (3주형형상배근.md) · ${MULTI_NOTE[name] ?? ''} · 조각별 중심선 길이` });
for (const w of info.warnings) warnings.push({ code: w.code, target: 'coping', message: w.message });
warnings.push({ code: 'RB-M06', target: 'coping', message: '다주형 전단 다리와 코핑 안 기둥 주철근의 간섭은 검토하지 않았습니다 (T형 Q-K3과 같음). 밀집 구간·다리 8개·측면 형상·피복 120은 가정값입니다 (Q-M2·Q-M3·Q-M5·Q-M6).' });
warnings.push({ code: 'RB-M08', target: 'column', message: '다주형 기둥 단부는 지름 이상·코핑 반높이 연장을 적용했습니다 (그림 D, Pier A337 1.5D — Q-M8 미결).' });
}
// Table metadata per bar (구분·간격·실간격·위치·방향·형상·근거). The spacing rows take their values from the inputs (승인 2026-10-26).
const withMeta = rows.map((r) => {
if (r.diameterMm !== undefined) return { ...r, spacing: '입력 구간별', actual: '중심선 계산',
position: r.name.startsWith('기둥·말뚝') ? '기둥~말뚝 연속' : r.member === 'pile' ? '지중 말뚝' : '지상 기둥·코핑', direction: '배근 경로',
shape: r.name.includes('커플러') ? '커플러' : r.name.includes('나선') ? '나선' : r.name.includes('크로스타이') ? '양끝 갈고리' : /P[35]/.test(r.name) ? '원형' : '직선', basis: 'calc' as const };
const linked = linkedSpacing(r.name, model);
const multi = nCol > 1 && r.member === 'coping';
return { ...r, ...(META[r.name] ?? FALLBACK), ...(linked ?? {}), ...(nCol > 1 && r.member === 'column' && r.name === 'P5' ? { position: '상·하 단부 · 코핑 반높이까지 (번호 확인 중)' } : {}), ...(multi ? {
shape: '다주형 조각별 경로 (검토용)', position: '다주형 전용 삽도 · 실제 하면',
...(r.name === 'C9·C11 사이' ? { spacing: String(model.copingRebar.spacingS), actual: String(model.copingRebar.spacingS) } : {}) } : {}) };
});
return { rows: withMeta, warnings: warnings.map((w) => ({ ...w, target: w.target ?? 'common' })), incomplete: [
...(cast ? cast.missing : model.footings.length ? [] : CAST_PENDING),
...(nCol > 1 ? ['다주형 철근 번호 대응·cycle별 이음 위치·수평철근 갈고리 상세 확인 필요'] : []),
] };
}