꺾임을 넘는 기초를 합치지 않아, 기둥 간격이 기초 길이보다 짧은 꺾인 교각(산외교 P11: 2.75 m)에서 두 기초가 겹치고(콘크리트 쐐기 틈, 철근 겹침, 수량 이중 계산) 말뚝도 겹쳤다. 꺾임 전후 기초가 겹치거나 미터 퍼짐 안에서 맞닿으면 하나의 꺾인 기초로 본다: - 형상: 구간별 조각 둘(bent), 각 조각을 미터 퍼짐 + 10 mm 너머로 늘려 미터면에서 틈 없이 잇는다 - 말뚝: 편 길이에 한 번 배치하고 꺾임점 기준으로 구간을 나눈다 - 철근: 편 좌표의 한 기초(footing-units.ts)로 철근표와 3D가 같이 계산, 3D는 미터점에서 접는다 Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
457 lines
37 KiB
TypeScript
457 lines
37 KiB
TypeScript
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별 이음 위치·수평철근 갈고리 상세 확인 필요'] : []),
|
||
] };
|
||
}
|