Files
Pier_v1/src/core/rebar/summary.ts
T
ypshimandClaude Haiku 5.5 488b65af31 fix(view): alert [보기] highlights the bar it is about (시안 ③, spec §6.1)
[보기] only switched the member tab. The alert now names its bar (RB-F08 → B2),
and [보기] switches to the member, highlights that row, and scrolls to it.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-22 12:13:03 +09:00

177 lines
15 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 { bearingBlockRebar, totals } from './bearing-block';
import { bottomPiece, topBarLength, widthLayout } from './coping-main';
import { shearBars, sideRows, C14_COUNTS, c14Total, STEP_BAR_COUNTS, shearULength, SIDE_LENGTHS, STEP_LENGTHS, capLength, PIECES_PER_POSITION } from './coping';
import { columnRebar, COLUMN_P1 } from './column';
import { bottomBars, crossBars, sideBars, assemblyBars, shearB9 } from './footing';
import { DEFAULT_MATERIAL, isAssumedMaterial, Material, 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<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: '계단부 가장 높은 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' },
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' },
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',
};
/** 정척 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.
*/
type RawRow = Omit<RebarRow, 'spacing' | 'actual' | 'position' | 'direction' | 'shape' | 'basis'>;
export function rebarSummary(model: PierModel, m: Material = DEFAULT_MATERIAL): { rows: RebarRow[]; warnings: RebarWarning[] } {
const rows: RawRow[] = [];
const warnings: RebarWarning[] = [];
if (isAssumedMaterial(m)) {
warnings.push({ code: 'I02', message: `fck ${m.fck} MPa · fy ${m.fyH} / ${m.fyS} MPa는 가정값입니다. 정착·이음 길이에 반영됩니다. 공통 설정에서 확인하세요.`, target: 'common' });
}
const L = model.copingL * 1000;
const B = model.copingB * 1000;
// Bearing blocks (§6.3 · plan C2)
const blk = totals(bearingBlockRebar(model));
rows.push({ member: 'bearing', name: 'S1', size: 'H19', count: blk.S1, length: 2500, note: '블럭 배치 분리, 블럭당 8본' });
rows.push({ member: 'bearing', name: 'S2', size: 'H19', count: blk.S2, length: 2500, note: '블럭 배치 분리, 블럭당 8본' });
// 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(m);
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 shear = shearBars(L);
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) });
const side = sideRows(41.288, 42.538);
rows.push({ member: 'coping', name: 'C12', size: 'H25', count: side.c12.length, length: null, note: `측면, 경사 하면에서 끊김, 일자 ${SIDE_LENGTHS.C12[0]}–${SIDE_LENGTHS.C12[4]}` });
rows.push({ member: 'coping', name: 'C13', size: 'H25', count: side.c13.length, length: SIDE_LENGTHS.C13, note: '측면, 끝면까지' });
rows.push({ member: 'coping', name: 'C14', size: 'H19', count: c14Total(), length: STEP_LENGTHS.C14, note: `위 ${C14_COUNTS.upperLines} 줄 ${C14_COUNTS.upperPerLine}본씩, 아래 ${C14_COUNTS.lower.join(' · ')} (합 ${c14Total()})` });
rows.push({ member: 'coping', name: 'C15', size: 'H19', count: STEP_BAR_COUNTS.C15, length: STEP_LENGTHS.C15, note: '계단부, 가장 높은 구간 (거꾸로 U)' });
rows.push({ member: 'coping', name: 'C16', size: 'H19', count: STEP_BAR_COUNTS.C16, length: STEP_LENGTHS.C16[0], note: `계단부, 3·4구간 (${STEP_LENGTHS.C16[0]} / ${STEP_LENGTHS.C16[1]})` });
// Column (기둥철근.md · plan C4), P1 defaults
const col = columnRebar(COLUMN_P1, m);
rows.push({ member: 'column', name: 'P1', size: 'S32', count: col.mainP1, length: null, note: '외측 r 875, 마디 4697.5 + 갈고리 525 / 3000 ×3 / 3147 (코핑 밖 마디는 기둥철근.md §3.2)' });
rows.push({ member: 'column', name: 'P2', size: 'S32', count: col.mainP2, length: null, note: '내측 r 843.2, 마디 4847.5 / 3000 ×3 / 2389' });
rows.push({ member: 'column', name: '커플러', size: 'L150 Ø60', count: col.couplers, length: 150, note: '@3000, P2 +150' });
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' });
// Footing (확대기초철근 (1).md · plan C5). Only when the footing exists (not 현장타설말뚝).
const f = model.footings[0];
if (f) {
const Lf = f.length * 1000;
const Wf = f.width * 1000;
const Hf = (f.top - f.bottom) * 1000;
// Bars longer than the standard length are split in two (추천안, Q-B6). The count is still the bar count.
const addBar = (b: { name: string; size: string; count: number; length: number }, note: string) => {
const split = splitPieces(b.length, 25.4, m.fyS, m.fck, STANDARD_BAR);
if (split) {
rows.push({ member: 'footing', name: b.name, size: b.size, count: b.count, length: split.pieceLength, note: `${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 });
}
};
for (const b of bottomBars(Lf, Wf, Hf)) addBar(b, '교각축');
for (const b of crossBars(Lf, Wf, Hf)) addBar(b, '교각축 직각');
for (const b of sideBars(Lf, Wf)) rows.push({ member: 'footing', name: b.name, size: b.size, count: b.count, length: b.length, note: '측면 수평' });
for (const b of assemblyBars(Lf, Wf)) rows.push({ member: 'footing', name: b.name, size: b.size, count: b.count, length: b.length, note: '조립 수평' });
const b9 = shearB9(Lf, Wf, Hf);
rows.push({ member: 'footing', name: 'B9', size: 'H16', count: b9.count, length: b9.length, note: `전단 @500 엇갈림, ${b9.rows}행 (위치·갈고리는 Q-B18·B19 제안)` });
} 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' })) };
}