diff --git a/index.html b/index.html index 7e86439..2844978 100644 --- a/index.html +++ b/index.html @@ -91,23 +91,11 @@

3 · 복사할 항목

- + +
+

복사하지 않는 값: 좌표 · IP · 받침 EL · 코핑 상면 EL · 기초 하단 EL · 지반고 · 기둥 높이 · 말뚝 선단 EL · 계단 구간

diff --git a/src/core/copy-targets.ts b/src/core/copy-targets.ts index 2933ebb..994aedf 100644 --- a/src/core/copy-targets.ts +++ b/src/core/copy-targets.ts @@ -1,3 +1,6 @@ +import type { CopyItem } from './copy'; +import type { MemberId } from './shape-options'; + /** One pier as a target of 속성 복사. */ export interface CopyCandidate { name: string; @@ -26,3 +29,17 @@ export function selectedTargets(piers: CopyCandidate[], sourceName: string, same .filter((c) => c.selectable && ticked.has(c.name)) .map((c) => c.name); } + +/** 부재 → 복사 항목. A member with no copy items (기둥, 받침) copies nothing yet. */ +export const MEMBER_COPY_ITEMS: Record = { + bearing: [], + coping: ['coping-width', 'coping-length'], + column: [], + footing: ['pile-length', 'pile-rows'], + ground: ['ground-position'], +}; + +/** The copy items for the selected 부재. */ +export function copyItemsFor(member: MemberId): CopyItem[] { + return MEMBER_COPY_ITEMS[member]; +} diff --git a/src/main.ts b/src/main.ts index 6f91beb..eed4924 100644 --- a/src/main.ts +++ b/src/main.ts @@ -8,8 +8,8 @@ import { NOT_IMPLEMENTED_RULES, validateInput, validatePier } from './core/valid import { buildTemplateWorkbook } from './core/template'; import { EDITABLE_FIELDS, FIELD_LABELS, overrideKeys, pendingKeys, revertDraft, type EditableKey, type PierValues } from './core/changes'; import { adjustRow, layoutText, resolveRowCounts, rowLayout, totalPiles } from './core/piles'; -import { applyCopy, DEFAULT_COPY_ITEMS, planCopy, type CopyItem } from './core/copy'; -import { copyCandidates, selectedTargets, type CopyCandidate } from './core/copy-targets'; +import { applyCopy, COPY_ITEMS, DEFAULT_COPY_ITEMS, planCopy, type CopyItem } from './core/copy'; +import { copyCandidates, copyItemsFor, selectedTargets, type CopyCandidate } from './core/copy-targets'; import { pileGeometry } from './core/geometry'; import { rebarSummary, type RebarRow } from './core/rebar/summary'; import { shearSegments } from './core/rebar/coping'; @@ -644,10 +644,37 @@ function selectedCopyTargets(): Pier[] { return state.piers.filter((p) => names.has(p.input.name)); } +/** Copy items ticked by the user, limited to the items of the selected 부재. */ function chosenCopyItems(): CopyItem[] { - return Array.from(document.querySelectorAll('#copy-panel input[data-item]')) - .filter((i) => i.checked) - .map((i) => i.dataset.item as CopyItem); + return copyItemsFor(state.member).filter((item) => copyItemChoice.has(item)); +} + +/** The items ticked by the user; kept across 부재 changes. */ +let copyItemChoice = new Set(DEFAULT_COPY_ITEMS); + +/** Shows the copy items of the selected 부재 only. A 부재 without items shows a note instead. */ +function renderCopyItems(): void { + const box = $('cp-items'); + box.replaceChildren(); + const select = $('cp-member'); + if (select.options.length === 0) for (const m of MEMBERS) select.appendChild(new Option(m.label, m.id)); + const items = copyItemsFor(state.member); + for (const item of items) { + const label = el('label', undefined, 'chk'); + const check = el('input'); + check.type = 'checkbox'; + check.dataset.item = item; + check.checked = copyItemChoice.has(item); + check.addEventListener('change', () => { + if (check.checked) copyItemChoice.add(item); + else copyItemChoice.delete(item); + renderCopyPreview(); + }); + label.append(check, document.createTextNode(COPY_ITEMS[item].label)); + box.appendChild(label); + } + $('cp-none').hidden = items.length > 0; + select.value = state.member; } function renderCopyTargets(): void { @@ -703,23 +730,18 @@ function renderCopyPreview(): void { const apply = $('cp-apply'); apply.textContent = selected.length === 0 ? '대상을 고르세요' : `${selected.map((t) => t.input.name).join(', ')} ${selected.length}기에만 적용`; apply.disabled = applied === 0; + if (items.length === 0) apply.textContent = '복사 항목 없음'; } /** The inline 속성 복사 panel: source is the 기준 pier; targets are ticked inside the panel only. */ function renderCopyPanel(): void { const src = activePier(); $('cp-source').textContent = src ? `${src.input.name} · ${src.input.type} · ${src.input.foundationType}` : '교각을 선택하세요'; + renderCopyItems(); renderCopyTargets(); renderCopyPreview(); } -/** Default copy items, set once at start-up. */ -function resetCopyItems(): void { - for (const i of document.querySelectorAll('#copy-panel input[data-item]')) { - i.checked = (DEFAULT_COPY_ITEMS as string[]).includes(i.dataset.item ?? ''); - } -} - function applyCopyPanel(): void { const src = activePier(); if (!src) return; @@ -1064,6 +1086,8 @@ function wireRebarPicking(): void { function placeCopyPanel(): void { const host = state.stage === 'plan' ? $('member-regen').parentElement : $('edit-regen').parentElement; if (host) host.appendChild($('copy-panel')); + // ① has no 부재 list of its own, so the copy panel carries the selector; ② uses its 부재 list. + $('cp-member-row').hidden = state.stage !== 'shape'; } function setStage(stage: Stage): void { @@ -3079,9 +3103,10 @@ $('cp-clear').addEventListener('click', () => { renderCopyTargets(); renderCopyPreview(); }); -for (const i of document.querySelectorAll('#copy-panel input[data-item]')) { - i.addEventListener('change', renderCopyPreview); -} +$('cp-member').addEventListener('change', (ev) => { + state.member = (ev.target as HTMLSelectElement).value as MemberId; + renderAll(); +}); $('cp-regen').addEventListener('change', renderCopyPreview); $('cp-apply').addEventListener('click', applyCopyPanel); $('sv-save').addEventListener('click', confirmSaveDialog); @@ -3238,7 +3263,6 @@ function restorePanel(id: string): void { } restorePanel('parts-info'); restorePanel('color-info'); -resetCopyItems(); $('all-toggle').addEventListener('click', () => { state.allView = !state.allView; diff --git a/tests/copy-targets.test.ts b/tests/copy-targets.test.ts index 39a37de..63591e7 100644 --- a/tests/copy-targets.test.ts +++ b/tests/copy-targets.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { copyCandidates, selectedTargets, type CopyCandidate } from '../src/core/copy-targets'; +import { copyCandidates, copyItemsFor, selectedTargets, type CopyCandidate } from '../src/core/copy-targets'; const piers: CopyCandidate[] = [ { name: 'P1', type: 'T형', hasModel: true }, @@ -39,3 +39,27 @@ describe('selectedTargets (선택한 대상만)', () => { expect(selectedTargets(piers, 'P3', true, picked)).toEqual([]); }); }); + +describe('copyItemsFor (부재 선택에 해당하는 복사 항목)', () => { + it('coping copies only the coping items', () => { + expect(copyItemsFor('coping')).toEqual(['coping-width', 'coping-length']); + }); + + it('확대기초 · 말뚝 copies only the pile items', () => { + expect(copyItemsFor('footing')).toEqual(['pile-length', 'pile-rows']); + }); + + it('지반 copies only the ground position', () => { + expect(copyItemsFor('ground')).toEqual(['ground-position']); + }); + + it('기둥 and 받침 have no copy items yet', () => { + expect(copyItemsFor('column')).toEqual([]); + expect(copyItemsFor('bearing')).toEqual([]); + }); + + it('every copy item belongs to exactly one member', () => { + const all = (['bearing', 'coping', 'column', 'footing', 'ground'] as const).flatMap((m) => copyItemsFor(m)); + expect([...all].sort()).toEqual(['coping-length', 'coping-width', 'ground-position', 'pile-length', 'pile-rows']); + }); +});