diff --git a/index.html b/index.html
index a28bbef..3f2c7fc 100644
--- a/index.html
+++ b/index.html
@@ -201,6 +201,7 @@
diff --git a/src/core/quantity-scope.ts b/src/core/quantity-scope.ts
new file mode 100644
index 0000000..9a29148
--- /dev/null
+++ b/src/core/quantity-scope.ts
@@ -0,0 +1,14 @@
+/** One pier as the 재료표 scope sees it. */
+export interface QuantityPier {
+ name: string;
+ hasModel: boolean;
+}
+
+/**
+ * The piers the ④ 재료표 shows: every pier with a model when 전체 is on, otherwise only the chosen pier
+ * (교각 목록에서 고른 교각). A chosen pier without a model shows nothing.
+ */
+export function quantityTargetNames(piers: QuantityPier[], all: boolean, active: string | null): string[] {
+ if (all) return piers.filter((p) => p.hasModel).map((p) => p.name);
+ return piers.filter((p) => p.hasModel && p.name === active).map((p) => p.name);
+}
diff --git a/src/main.ts b/src/main.ts
index 55e4cdb..bd8f1ed 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -10,6 +10,7 @@ import { EDITABLE_FIELDS, FIELD_LABELS, overrideKeys, pendingKeys, revertDraft,
import { adjustRow, layoutText, resolveRowCounts, rowLayout, totalPiles } from './core/piles';
import { applyCopy, COPY_ITEMS, planCopy, type CopyItem } from './core/copy';
import { copyCandidates, copyItemsFor, copyItemsWithOverrides, hasCopyItems, selectedTargets, type CopyCandidate } from './core/copy-targets';
+import { quantityTargetNames } from './core/quantity-scope';
import { pileGeometry } from './core/geometry';
import { rebarSummary, type RebarRow } from './core/rebar/summary';
import { shearSegments } from './core/rebar/coping';
@@ -122,6 +123,8 @@ const state = {
stage: 'shape' as Stage,
/** 전체 보기 on ① 형상 생성: every pier's concrete in one frame (콘크리트만). On by default (승인 2026-10-27). */
allView: true,
+ /** ④ 재료표 scope: true = every pier (전체), false = the pier chosen in the list. */
+ quantityAll: true,
/** ③ 공통 설정 (fck · fy). */
material: { ...DEFAULT_MATERIAL } as Material,
/** ② 부재별 fck. A member without an entry uses the 공통 fck (③). Initial values from 교각도 (025·026, 승인 2026-10-27): 기둥·코핑 40, 기초 27. */
@@ -437,6 +440,7 @@ function renderPierRows(list: HTMLUListElement, mode: Stage): void {
name.setAttribute('aria-pressed', String(pier.input.name === state.active));
name.addEventListener('click', () => {
state.active = pier.input.name;
+ state.quantityAll = false;
state.rebarSelection = new Set();
setupScene();
renderAll();
@@ -1766,8 +1770,10 @@ function renderQuantityLink(box: HTMLElement, rows: RebarRow[]): void {
}
/** ④ 재료표: the 철근재료표 of each target pier, bars by 강종 → 직경 → 번호, with subtotals and the totals. */
+/** The piers the 재료표 shows: 전체, or the pier chosen in the list. */
function quantityTargets(): Pier[] {
- return state.piers.filter((p) => p.model);
+ const names = new Set(quantityTargetNames(state.piers.map((p) => ({ name: p.input.name, hasModel: p.model !== null })), state.quantityAll, state.active));
+ return state.piers.filter((p) => names.has(p.input.name));
}
/** Length cell of a 재료표 line: the length, or 여러 길이 with the pieces on hover. */
@@ -1806,10 +1812,14 @@ function renderQuantityScreen(): void {
const body = $('quantity-body');
body.replaceChildren();
renderPierRows($('quantity-pier-items'), 'quantity');
+ const allButton = $('quantity-all');
+ const generated = state.piers.filter((p) => p.model).length;
+ allButton.textContent = `전체 (${generated}기)`;
+ allButton.setAttribute('aria-pressed', String(state.quantityAll));
const targets = quantityTargets();
- $('step4-status').textContent = targets.length ? `${targets.length}기` : '예정';
+ $('step4-status').textContent = targets.length ? (state.quantityAll ? `${targets.length}기` : targets[0].input.name) : '예정';
$('quantity-note').textContent = targets.length
- ? `${targets.length}기 · 할증 ${state.material.waste}% (기본 0%, 가정). 삿갓 길이는 h − 100으로 가정(Q-K5). 체크한 교각 기준이고, 체크가 없으면 현재 교각입니다.`
+ ? `${state.quantityAll ? `전체 ${targets.length}기` : targets[0].input.name} · 할증 ${state.material.waste}% (기본 0%, 가정). 삿갓 길이는 h − 100으로 가정(Q-K5). 교각 목록에서 고른 교각만 보며, 전체를 누르면 모든 교각을 보여 줍니다.`
: '계산된 교각이 없습니다. 교각을 생성하세요.';
let kg = 0;
let kgWithWaste = 0;
@@ -1845,6 +1855,10 @@ function exportQuantityWorkbook(): void {
XLSX.writeFile(wb, `${state.project.bridge || '교량'}_철근재료표.xlsx`);
}
+$('quantity-all').addEventListener('click', () => {
+ state.quantityAll = true;
+ renderAll();
+});
$('quantity-export').addEventListener('click', exportQuantityWorkbook);
$('quantity-print').addEventListener('click', () => window.print());
diff --git a/tests/quantity-scope.test.ts b/tests/quantity-scope.test.ts
new file mode 100644
index 0000000..960e3a1
--- /dev/null
+++ b/tests/quantity-scope.test.ts
@@ -0,0 +1,26 @@
+import { describe, expect, it } from 'vitest';
+import { quantityTargetNames } from '../src/core/quantity-scope';
+
+const piers = [
+ { name: 'P1', hasModel: true },
+ { name: 'P2', hasModel: true },
+ { name: 'P3', hasModel: false },
+];
+
+describe('quantityTargetNames (④ 재료표 범위)', () => {
+ it('is every pier with a model when 전체 is on', () => {
+ expect(quantityTargetNames(piers, true, 'P2')).toEqual(['P1', 'P2']);
+ });
+
+ it('is only the chosen pier when 전체 is off', () => {
+ expect(quantityTargetNames(piers, false, 'P2')).toEqual(['P2']);
+ });
+
+ it('is empty when the chosen pier has no model', () => {
+ expect(quantityTargetNames(piers, false, 'P3')).toEqual([]);
+ });
+
+ it('is empty when no pier is chosen and 전체 is off', () => {
+ expect(quantityTargetNames(piers, false, null)).toEqual([]);
+ });
+});