From 7861bc3fdf2687d34109198dac96c6325425e4d7 Mon Sep 17 00:00:00 2001 From: ypshim Date: Tue, 27 Oct 2026 17:08:05 +0900 Subject: [PATCH] =?UTF-8?q?feat(quantity):=20=E2=91=A3=20=EC=9E=AC?= =?UTF-8?q?=EB=A3=8C=ED=91=9C=20=EB=B2=94=EC=9C=84=EB=A5=BC=20=EA=B5=90?= =?UTF-8?q?=EA=B0=81=20=EB=AA=A9=EB=A1=9D=EC=9D=98=20=EC=84=A0=ED=83=9D?= =?UTF-8?q?=EA=B3=BC=20=EB=A7=9E=EC=B6=A4=20(=EC=A0=84=EC=B2=B4=20?= =?UTF-8?q?=EB=98=90=EB=8A=94=20=EA=B3=A0=EB=A5=B8=20=EA=B5=90=EA=B0=81)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 교각 목록 맨 위에 "전체 (N기)" 항목 추가 (기본 선택) - 교각 이름을 누르면 그 교각의 재료표만 표시, 제목·합계 문구도 그 교각 기준 - 범위 규칙을 core/quantity-scope.ts로 분리하고 테스트 4건 추가 Co-Authored-By: Claude Haiku 5.5 --- index.html | 1 + src/core/quantity-scope.ts | 14 ++++++++++++++ src/main.ts | 20 +++++++++++++++++--- tests/quantity-scope.test.ts | 26 ++++++++++++++++++++++++++ 4 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 src/core/quantity-scope.ts create mode 100644 tests/quantity-scope.test.ts 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([]); + }); +});