feat(view): ③ screen follows the 시안 — table columns, basis badges, alert buttons

- 철근표 columns as in the 시안: 구분 · 번호 · 규격 · 간격 · 실간격 · 위치 · 방향 ·
  형상 · 길이 · 개수 · 근거 · 비고. Metadata per bar from the rebar documents.
- 근거 badges: 입력 / 기본값 / 계산 / 가정 (the 가정 badge is italic amber).
- 번호 click highlights that row.
- 알림: [보기] opens the member tab; I02 [입력] focuses the fck input.
- Member tabs show the warning count.
- 공통 설정 moved to the right column (알림 → 검토 필요 → 공통 설정), as in the 시안.
- The 시안's "fck를 입력해야 생성" blocker is not used: the defaults are assumed
  values (I02), as decided on 2026-10-22.
- Not in this step: the drawings and the 정면·단면 연동 (시안 도면 뷰어).
- Not verified in a browser.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
ypshimandClaude Haiku 5.5 committed 2026-10-22 12:08:21 +09:00
1 parent 9ba7b55eab
commit cdc0e5ac9c
4 files changed
+138 -16

No files matched your search

+7 -7
View File
@@ -138,13 +138,6 @@
<aside class="panel piers" aria-labelledby="rebar-member-title">
<p id="rebar-member-title" class="lbl">부재</p>
<ul id="rebar-tabs" class="members"></ul>
<section class="common" aria-labelledby="common-title">
<p id="common-title" class="lbl">공통 설정</p>
<label class="mat">fck (MPa) <input id="mat-fck" type="number" step="any" /></label>
<label class="mat">fy H · SD400 (MPa) <input id="mat-fyh" type="number" step="any" /></label>
<label class="mat">fy S · SD500 (MPa) <input id="mat-fys" type="number" step="any" /></label>
<p id="mat-note" class="muted small"></p>
</section>
<p class="pnote">형상은 ② 형상계획 기준입니다. 형상이 바뀌면 "형상 바뀜"으로 표시합니다.</p>
</aside>
<section class="panel viewer" aria-labelledby="rebar-title">
@@ -161,6 +154,13 @@
<li>Q-B6 B2·B6 이음 위치와 길이</li>
<li>Q-C9 P4–B11 간섭: 무시로 결정, 문서 반영 필요</li>
</ul>
<section class="common" aria-labelledby="common-title">
<p id="common-title" class="lbl">공통 설정</p>
<label class="mat">fck (MPa) <input id="mat-fck" type="number" step="any" /></label>
<label class="mat">fy H · SD400 (MPa) <input id="mat-fyh" type="number" step="any" /></label>
<label class="mat">fy S · SD500 (MPa) <input id="mat-fys" type="number" step="any" /></label>
<p id="mat-note" class="muted small"></p>
</section>
</aside>
</main>
+67 -5
View File
@@ -6,6 +6,9 @@ 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';
@@ -14,13 +17,68 @@ export interface RebarRow {
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';
}
/** 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;
@@ -28,11 +86,13 @@ 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: RebarRow[] = [];
const rows: RawRow[] = [];
const warnings: RebarWarning[] = [];
if (isAssumedMaterial(m)) {
warnings.push({ code: 'I02', message: `fck ${m.fck} MPa · fy ${m.fyH} / ${m.fyS} MPa는 가정값입니다. 정착·이음 길이에 반영됩니다. 공통 설정에서 확인하세요.` });
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;
@@ -94,7 +154,7 @@ export function rebarSummary(model: PierModel, m: Material = DEFAULT_MATERIAL):
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).` });
warnings.push({ code: 'RB-F08', message: `${b.name} 길이 ${Math.round(b.length)} mm > 정척 ${STANDARD_BAR} mm: 2조각 이음을 추천안으로 적용 (Q-B6).`, target: 'footing' });
} else {
rows.push({ member: 'footing', name: b.name, size: b.size, count: b.count, length: b.length, note });
}
@@ -106,7 +166,9 @@ export function rebarSummary(model: PierModel, m: Material = DEFAULT_MATERIAL):
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: '현장타설말뚝은 확대기초가 없어 확대기초 철근을 만들지 않습니다.' });
warnings.push({ code: 'RB-F00', message: '현장타설말뚝은 확대기초가 없어 확대기초 철근을 만들지 않습니다.', target: 'footing' });
}
return { rows, warnings };
// 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' })) };
}
+55 -4
View File
@@ -727,29 +727,80 @@ function renderRebar(): void {
const table = el('table', undefined, 'grid-table rebar-table');
const head = el('thead');
const hr = el('tr');
for (const h of ['번호', '규격', '개수', '길이 (mm)', '비고']) hr.appendChild(el('th', h));
for (const h of ['구분', '번호', '규격', '간격', '실간격', '위치', '방향', '형상', '길이 (mm)', '개수', '근거', '비고']) hr.appendChild(el('th', h));
head.appendChild(hr);
table.appendChild(head);
const body = el('tbody');
for (const r of shown) {
const tr = el('tr');
const nameCell = el('td', r.name);
nameCell.classList.add('bar-no');
// Clicking a 번호 highlights that row (시안: 번호를 누르면 그 철근만 강조).
nameCell.addEventListener('click', () => tr.classList.toggle('hl'));
tr.append(
el('td', r.name),
el('td', ROLE[r.name] ?? '—'),
nameCell,
el('td', r.size),
el('td', String(r.count)),
el('td', r.spacing),
el('td', r.actual),
el('td', r.position),
el('td', r.direction),
el('td', r.shape),
el('td', r.length === null ? '—' : String(Math.round(r.length * 10) / 10)),
el('td', String(r.count)),
basisCell(r.basis),
el('td', r.note),
);
body.appendChild(tr);
}
table.appendChild(body);
box.appendChild(table);
for (const w of warnings) issues.appendChild(el('li', `${w.code} ${w.message}`));
// Alerts: [보기] goes to the member, [입력] goes to the 공통 설정 input (시안 ③ 알림).
for (const w of warnings) {
const li = el('li', `${w.code} ${w.message} `);
const btn = el('button', w.target === 'common' ? '입력' : '보기', 'btn small');
btn.type = 'button';
btn.addEventListener('click', () => {
if (w.target === 'common') {
$<HTMLInputElement>('mat-fck').focus();
$<HTMLInputElement>('mat-fck').scrollIntoView({ block: 'center' });
} else {
state.rebarMember = w.target;
renderAll();
}
});
li.appendChild(btn);
issues.appendChild(li);
}
if (issues.childElementCount === 0) issues.appendChild(el('li', '알림이 없습니다.'));
// Warning count on each member tab (시안: 부재 탭 배지).
const counts = (member: RebarRow['member']) => warnings.filter((w) => w.target === member).length;
for (const [i, m] of REBAR_MEMBERS.entries()) {
const badge = tabs.children[i]?.querySelector('button');
const n = counts(m.id);
if (badge && n > 0) badge.appendChild(el('span', String(n), 'badge warn'));
}
$('step3-status').textContent = `계산됨 · 경고 ${warnings.length}`;
renderMaterial();
}
/** 구분 (role) of each bar, as in the 시안 ③ table. */
const ROLE: Record<string, string> = {
S1: '받침블럭', S2: '받침블럭',
C1: '주철근', C2: '주철근', C3: '주철근', C4: '주철근', C5: '주철근', C6: '주철근', C7: '주철근', C8: '주철근',
C9: '전단·띠', C10: '전단·띠', C11: '전단·띠', C12: '측면 수평', C13: '측면 수평', C14: '직각 수평', C15: '계단부', C16: '계단부',
P1: '주철근', P2: '주철근', 커플러: '조립', P3: '띠철근', P5: '띠철근', P4: '띠철근', P6: '크로스타이',
B1: '주철근', B2: '주철근', B3: '주철근', B4: '주철근', B5: '주철근', B6: '주철근', B7: '주철근', B8: '주철근',
'B10-1': '측면 수평', 'B10-2': '측면 수평', B11: '조립 수평', B12: '조립 수평', B9: '전단·띠',
};
/** 근거 badge (시안 ③): 입력 / 기본값 / 계산 / 가정. */
function basisCell(b: RebarRow['basis']): HTMLElement {
const label = { input: '입력', default: '기본값', calc: '계산', assumed: '가정' }[b];
return el('td', label, `basis basis-${b}`);
}
/** ③ 공통 설정 inputs. Changes apply on 'change' and regenerate the rebar screen. */
function renderMaterial(): void {
const m = state.material;
+9
View File
@@ -690,4 +690,13 @@ body { background: #eef1f4; font-family: 'Noto Sans KR', system-ui, sans-serif;
.common { margin: 10px 0; display: grid; gap: 6px; }
.mat { display: flex; justify-content: space-between; align-items: center; gap: 8px; font-size: 13px; }
.mat input { width: 96px; padding: 4px 6px; border: 1px solid #b9c4d0; border-radius: 4px; }
/* ③ 철근표: 근거 badge, row highlight, warning count on tabs */
.basis { font-size: 12px; border-radius: 4px; padding: 1px 6px; white-space: nowrap; }
.basis-input { background: #e2f2e7; color: #1d6b3a; }
.basis-default { background: #eef1f4; color: #55606e; }
.basis-calc { background: #e3edf8; color: #123e70; }
.basis-assumed { background: #fff2dc; color: #8a4b00; font-style: italic; }
.rebar-table tr.hl td { background: #fff6d6; }
.rebar-table .bar-no { cursor: pointer; font-weight: 700; }
.members .badge.warn { margin-left: 6px; background: #ffebd6; color: #8a4b00; }