import type { PierModel } from '../geometry'; import { barLength, bearingBlockRebar, totals } from './bearing-block'; import { bottomPiece, topBarLength, widthLayout } from './coping-main'; import { shearBars, shearBarsMulti, copingSideBars, SIDE_HOOK_LEG, c14Bars, c14Length, stairBars, stepDevLength, undersideAt, shearULength, capLength, PIECES_PER_POSITION } from './coping'; import { columnRebar, COLUMN_P1 } from './column'; import { bottomBars, crossBars, sideBars, assemblyBars, shearB9 } from './footing'; import { DEFAULT_MATERIAL, FckByMember, isAssumedMaterial, Material, materialFor, splitPieces } 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'; name: string; size: string; count: number; length: number | null; note: string; /** 간격 (설계 간격, mm or text), 실간격 (cycle 묶음 기준), 위치, 방향, 형상, 근거. */ spacing: string; actual: string; position: string; direction: string; shape: string; basis: Basis; } 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; } /** Table metadata per bar (코핑철근.md, 확대기초철근 (1).md, 기둥철근 (1).md, 받침블럭철근.md). */ const META: Record> = { 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: '계단부 가장 높은 5구간', direction: '교각축', shape: '거꾸로 된 U', basis: 'calc' }, C16: { spacing: '250', actual: '250', position: '계단부 3·4구간', direction: '교각축', shape: 'L형', 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 = { spacing: '—', actual: '—', position: '—', direction: '—', shape: '—', basis: 'calc', }; /** 정척 L_bar (mm) from 확대기초철근.md §1.2. Bars longer than this need a splice. */ export const STANDARD_BAR = 12000; /** * 철근표 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[]): FootingBar[] { const out: FootingBar[] = []; for (const b of bottomBars(Lf, Wf, Hf)) out.push({ ...b, note: '교각축' }); for (const b of crossBars(Lf, Wf, Hf)) out.push({ ...b, note: '교각축 직각' }); for (const b of sideBars(Lf, Wf, Hf)) out.push({ ...b, note: '측면 수평' }); for (const b of assemblyBars(Lf, Wf, Hf)) out.push({ ...b, note: '조립 수평' }); const b9 = shearB9(Lf, Wf, Hf, columnX); out.push({ name: 'B9', size: 'H16', count: b9.count, length: b9.length, note: `전단 @500 엇갈림, ${b9.rows}행, 기둥 구역 제외 (위치·갈고리는 Q-B18·B19 규칙 적용)` }); return out; } type RawRow = Omit; export function rebarSummary( model: PierModel, common: Material = DEFAULT_MATERIAL, byMember: FckByMember = {}, ): { rows: RebarRow[]; warnings: RebarWarning[] } { const rows: RawRow[] = []; const warnings: RebarWarning[] = []; const mCoping = materialFor(common, byMember, 'coping'); const mColumn = materialFor(common, byMember, 'column'); 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 · fy ${common.fyH} / ${common.fyS} 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(), note: `블럭 배치 분리, 블럭당 8본 · 묻힘 ${range(emb1)} (블럭 상면 − 69.1)` }); rows.push({ member: 'bearing', name: 'S2', size: 'H19', count: blk.S2, length: barLength(), note: `블럭 배치 분리, 블럭당 8본 · 묻힘 ${range(emb2)} (블럭 상면 − 50)` }); // Coping (코핑철근.md §5–§8) const width = widthLayout(B); const perRow = width.count / 2; // cycle split, 12 per row // C1–C3 and C4–C6 are the same three top rows (row 1, 2, 3), one set per cycle. for (const [name, k] of [['C1', 1], ['C2', 2], ['C3', 3], ['C4', 1], ['C5', 2], ['C6', 3]] as const) { 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 colS = model.columns.map((c) => c.s * 1000); const nCol = Math.max(1, colS.length); const shear = nCol > 1 ? shearBarsMulti(L, colS) : { ...shearBars(L), between: [] as number[] }; const uRange = (xs: number[]) => { const u = xs.map((x) => Math.round(shearULength(x, L))); return `${Math.min(...u)}–${Math.max(...u)}`; }; const capRange = (xs: number[]) => { const c = xs.map((x) => Math.round(capLength(x, L))); 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}`; rows.push({ member: 'coping', name: 'C9', size: 'H25', count: shear.c9.length, length: null, note: shearNote('전단 좌 @250', shear.c9) }); rows.push({ member: 'coping', name: 'C10', size: 'H25', count: shear.c10.length, length: null, note: shearNote('전단 Level @300', shear.c10) }); rows.push({ member: 'coping', name: 'C11', size: 'H25', count: shear.c11.length, length: null, 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, note: `기둥 사이, 양쪽 C10에서 안쪽으로 (추천안, Q-K2), 조각 ${shear.between.length * PIECES_PER_POSITION}` }); } if (nCol > 1) { warnings.push({ code: 'RB-M01', message: `다주형 ${nCol}기둥: C14는 T형 기준으로 두었습니다 (기둥마다 반복 규칙 미정, Q-K11).`, target: 'coping' }); } // Per row: two 일자 (side faces) and two ㄷ (one at each end), so 4 bars per row (코핑철근.md §6.2). const side = copingSideBars(model.copingL * 1000, 41.288, 42.538); const hook = model.copingB * 1000 - 2 * 127 + 2 * SIDE_HOOK_LEG; // ㄷ body across the width + two legs const c12Range = side.c12.length ? `${Math.round(side.c12[0].straight)}–${Math.round(side.c12[side.c12.length - 1].straight)}` : '—'; rows.push({ member: 'coping', name: 'C12', size: 'H25', count: 4 * side.c12.length, length: null, note: `측면, 경사 하면에서 끊김, 일자 ${c12Range} (${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: side.c13.length ? side.c13[0].straight : null, note: `측면, 끝면까지, 일자 (${2 * side.c13.length}) · ㄷ ${Math.round(hook)} (${2 * side.c13.length})` }); // 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.c12, ...side.c13], (x) => undersideAt(L14, x, model.endBottom, model.levelBottom)); 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}개 (하면 밖)` }); const stairs = stairBars(model.copingL * 1000, model.copingB * 1000, model.copingRefTop, mCoping); rows.push({ member: 'coping', name: 'C15', size: 'H19', count: stairs.c15.length, length: stairs.c15[0].length, note: '계단부, 가장 높은 구간 (거꾸로 U)' }); rows.push({ member: 'coping', name: 'C16', size: 'H19', count: stairs.c16.length, length: stairs.c16[0].length, note: `계단부, 3·4구간 (${Math.round(stairs.c16[0].length)} / ${Math.round(stairs.c16[stairs.c16.length - 1].length)})` }); // Column (기둥철근.md · plan C4), P1 defaults const col = columnRebar(COLUMN_P1, mColumn); rows.push({ member: 'column', name: 'P1', size: 'S32', count: col.mainP1, length: null, 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, 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 상향 엇갈림' }); 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: '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' }); // Column bars and couplers repeat for each column (기둥철근 is per column; 다주형은 기둥마다). for (const r of rows) if (r.member === 'column') r.count *= nCol; // 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). const agg = new Map(); for (const f of model.footings) { // Columns on this footing, as x mm from the footing centre (for the B9 column-zone rule). const columnX = model.columns.filter((c) => Math.abs(c.s - f.s) <= f.length / 2).map((c) => (c.s - f.s) * 1000); for (const b of footingBarList(f.length * 1000, f.width * 1000, (f.top - f.bottom) * 1000, columnX)) { const cur = agg.get(b.name); if (cur) { cur.count += b.count; cur.length = Math.max(cur.length, b.length); } else { agg.set(b.name, { ...b }); } } } for (const b of agg.values()) { const split = splitPieces(b.length, 25.4, mFooting.fyS, mFooting.fck, STANDARD_BAR); if (split && b.length > STANDARD_BAR && b.name !== 'B9') { rows.push({ member: 'footing', name: b.name, size: b.size, count: b.count, length: split.pieceLength, note: `${b.note}, 2조각 이음 (추천안, 겹침 ${Math.round(split.lap)})` }); warnings.push({ code: 'RB-F08', message: `${b.name} 길이 ${Math.round(b.length)} mm > 정척 ${STANDARD_BAR} mm: 2조각 이음을 추천안으로 적용 (Q-B6).`, target: 'footing', bar: b.name }); } else { rows.push({ member: 'footing', name: b.name, size: b.size, count: b.count, length: b.length, note: b.note }); } } } else { warnings.push({ code: 'RB-F00', message: '현장타설말뚝은 확대기초가 없어 확대기초 철근을 만들지 않습니다.', target: 'footing' }); } // Table metadata per bar (구분·간격·실간격·위치·방향·형상·근거). const withMeta = rows.map((r) => ({ ...r, ...(META[r.name] ?? FALLBACK) })); return { rows: withMeta, warnings: warnings.map((w) => ({ ...w, target: w.target ?? 'common' })) }; }