feat(quantity): ④ 재료표 범위를 교각 목록의 선택과 맞춤 (전체 또는 고른 교각)

- 교각 목록 맨 위에 "전체 (N기)" 항목 추가 (기본 선택)
- 교각 이름을 누르면 그 교각의 재료표만 표시, 제목·합계 문구도 그 교각 기준
- 범위 규칙을 core/quantity-scope.ts로 분리하고 테스트 4건 추가

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
ypshimandClaude Haiku 5.5 committed 2026-10-27 17:08:05 +09:00
1 parent 278d19faa5
commit 7861bc3fdf
4 files changed
+58 -3

No files matched your search

+1
View File
@@ -201,6 +201,7 @@
<main class="grid quantity" id="view-quantity" hidden>
<aside class="panel piers" aria-labelledby="quantity-pier-title">
<p id="quantity-pier-title" class="lbl">교각 목록</p>
<button id="quantity-all" class="pier-name" type="button" aria-pressed="true">전체</button>
<ul id="quantity-pier-items" class="pier-items"></ul>
<p class="pnote" id="quantity-note"></p>
</aside>
+14
View File
@@ -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);
}
+17 -3
View File
@@ -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 = $<HTMLDivElement>('quantity-body');
body.replaceChildren();
renderPierRows($<HTMLUListElement>('quantity-pier-items'), 'quantity');
const allButton = $<HTMLButtonElement>('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`);
}
$<HTMLButtonElement>('quantity-all').addEventListener('click', () => {
state.quantityAll = true;
renderAll();
});
$<HTMLButtonElement>('quantity-export').addEventListener('click', exportQuantityWorkbook);
$<HTMLButtonElement>('quantity-print').addEventListener('click', () => window.print());
+26
View File
@@ -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([]);
});
});