diff --git a/index.html b/index.html index 4772efc..45ecf70 100644 --- a/index.html +++ b/index.html @@ -46,18 +46,11 @@ -

불러온 교각이 없습니다.

시트 1장 = 교각 1기입니다. 오류가 난 교각만 건너뛰고 나머지는 생성합니다.

-
- 선택 0기 - - - -

입력값

@@ -74,7 +67,62 @@

- +
+
+

속성 복사

+

기준 교각의 설정을 다른 교각에 한 번에 적용합니다

+
+
+
+
+

1 · 원본 교각

+

+

현재 입력값 기준 (미반영 변경 포함)

+
+
+

2 · 복사할 항목

+
    +
  • 코핑 + + +
  • +
  • 지반 + +
  • +
  • 확대기초 · 말뚝 + + + +
  • +
  • 기둥 · 받침 · 철근배근 + +
  • +
+

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

+
+
+

3 · 대상 교각

+ +
    +
    +
    +
    +
    +

    4 · 적용 미리보기

    + + + +
    대상형식바뀌는 값건너뜀내용
    + +

    적용하면 대상 교각마다 "변경 n건"이 표시되고, 입력 수정 카드에서 항목별로 되돌릴 수 있습니다.

    +
    +
    +
    + +
    @@ -187,11 +235,6 @@

    교각 목록

    -
    - 선택 0기 - - -

    부재 · 배근 타입

    형상은 ② 형상계획 기준입니다. 형상이 바뀌면 "형상 바뀜"으로 표시합니다.

    @@ -374,61 +417,7 @@ - -
    -

    속성 복사

    -

    한 교각의 설정을 여러 교각에 한 번에 적용합니다

    - -
    -
    -
    -
    -

    1 · 원본 교각

    -

    -

    현재 입력값 기준 (미반영 변경 포함)

    -
    -
    -

    2 · 복사할 항목

    -
      -
    • 코핑 - - -
    • -
    • 확대기초 · 말뚝 - - - -
    • -
    • 기둥 · 받침 · 철근배근 - -
    • -
    -

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

    -
    -
    -

    3 · 대상 교각

    - -
      -
      -
      -
      -
      -

      4 · 적용 미리보기

      - - - -
      대상형식바뀌는 값건너뜀내용
      - -

      적용하면 대상 교각마다 "변경 n건"이 표시되고, 입력 수정 카드에서 항목별로 되돌릴 수 있습니다.

      -
      -
      -
      - -
      + diff --git a/src/core/active-piers.ts b/src/core/active-piers.ts deleted file mode 100644 index 2fca62e..0000000 --- a/src/core/active-piers.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { overrideKeys, type EditableKey, type PierValues } from './changes'; -import type { PierInput } from './types'; - -/** One list row as the selection rules see it. */ -export interface PierSelection { - name: string; - checked: boolean; - hasModel: boolean; -} - -/** - * Piers shown in the 3D view: every checked pier that has a model, plus the 기준 pier (the one whose card is open) - * even when it is unchecked. Order follows the list (승인 2026-10-27, 체크 = 활성). - */ -export function shownNames(list: PierSelection[], active: string | null): string[] { - return list.filter((p) => p.hasModel && (p.checked || p.name === active)).map((p) => p.name); -} - -/** Piers that receive the 기준 pier's overrides: the checked piers with a model, other than the 기준 pier. */ -export function overrideTargets(list: PierSelection[], active: string | null): string[] { - return list.filter((p) => p.checked && p.hasModel && p.name !== active).map((p) => p.name); -} - -/** The target input with the source's overridden values copied in; every other value of the target is kept. */ -export function applyOverrides(source: PierValues, target: PierInput): PierInput { - const next: PierInput = { ...target }; - for (const key of overrideKeys(source) as EditableKey[]) next[key] = source.draft[key] as never; - return next; -} diff --git a/src/main.ts b/src/main.ts index 89c2bbf..d952fc3 100644 --- a/src/main.ts +++ b/src/main.ts @@ -7,7 +7,6 @@ import { buildPier, type PierModel } from './core/geometry'; import { NOT_IMPLEMENTED_RULES, validateInput, validatePier } from './core/validate'; import { buildTemplateWorkbook } from './core/template'; import { EDITABLE_FIELDS, FIELD_LABELS, overrideKeys, pendingKeys, revertDraft, type EditableKey, type PierValues } from './core/changes'; -import { applyOverrides, overrideTargets, shownNames, type PierSelection } from './core/active-piers'; import { adjustRow, layoutText, resolveRowCounts, rowLayout, totalPiles } from './core/piles'; import { applyCopy, DEFAULT_COPY_ITEMS, planCopy, type CopyItem } from './core/copy'; import { pileGeometry } from './core/geometry'; @@ -56,7 +55,7 @@ let bearingDrawing: Record = {}; /** 지반 모델 (ground_model.json) for the project. Its values reach a pier's 지반고 only when the user applies them. */ let groundModel: GroundModelFile | null = null; -import { allVisibleChecked, setVisibleChecked, type TypeFilter } from './core/selection'; +import type { TypeFilter } from './core/selection'; import { choose, defaultSummary, isSelectable, SHAPE_GROUPS, UNDEFINED_REASON, type MemberId } from './core/shape-options'; import { parseProject, SCHEMA_VERSION, serializeProject, type ProjectFile, type ProjectInfo } from './core/project'; import { clearBackup, readBackup, writeBackup, type Backup } from './view/backup'; @@ -351,21 +350,16 @@ function setView(kind: ViewKind): void { } /** 전체 보기 applies on ① 형상 생성 only; ③ and the others keep the single-pier scene. */ -function selectionOf(p: Pier): PierSelection { - return { name: p.input.name, checked: p.checked, hasModel: p.model !== null }; -} - -/** The piers the 3D view shows together: the checked ones plus the 기준 pier (체크 = 활성, 승인 2026-10-27). */ +/** The piers the 3D view shows together: all generated piers in ① (전체 보기), the 기준 pier alone in ②. */ function shownPiers(): Pier[] { - const names = new Set(shownNames(state.piers.map(selectionOf), state.active)); - return state.piers.filter((p) => names.has(p.input.name)); + if (state.stage === 'shape') return state.piers.filter((p) => p.model); + const active = activePier(); + return active?.model ? [active] : []; } -/** Several piers in one frame: ① 전체 보기 (button), or ② when two or more piers are shown. */ +/** Several piers in one frame: only ① with 전체 보기 on. ② shows the 기준 pier alone. */ function multiSceneOn(): boolean { - if (state.stage === 'shape') return state.allView; - if (state.stage === 'plan') return shownPiers().length >= 2; - return false; + return state.stage === 'shape' && state.allView; } /** Builds the shown piers in one frame. Returns false when none has a model. */ @@ -428,7 +422,6 @@ function renderPierList(): void { const visible = state.piers.filter((p) => state.filter === 'all' || p.input.type === state.filter); $('pier-empty').hidden = state.piers.length > 0; - $('select-all').textContent = allVisibleChecked(state.piers, state.filter) ? '선택 해제' : '전체 선택'; } /** Rows of the pier list. ① shows the type and warnings; ② shows the count of changes from the defaults. */ @@ -437,16 +430,6 @@ function renderPierRows(list: HTMLUListElement, mode: Stage): void { const visible = state.piers.filter((p) => state.filter === 'all' || p.input.type === state.filter); for (const pier of visible) { const li = el('li'); - const check = el('input'); - check.type = 'checkbox'; - check.checked = pier.checked; - check.disabled = !pier.model; - check.setAttribute('aria-label', `${pier.input.name} 선택`); - check.addEventListener('change', () => { - pier.checked = check.checked; - setupScene(); - renderAll(); - }); // The pier name and type are shown here, since the 3D title band is gone on wide screens (승인 2026-10-23). const name = el('button', `${pier.input.name} · ${pier.input.type}`, 'pier-name'); name.type = 'button'; @@ -464,7 +447,7 @@ function renderPierRows(list: HTMLUListElement, mode: Stage): void { : pier.model ? el('span', `${pier.input.type}${pier.issues.some((i) => i.level !== 'I') ? ' · 경고' : ''}`, 'badge') : el('span', '오류', 'badge error'); - li.append(check, name, badge); + li.append(name, badge); list.appendChild(li); } } @@ -478,14 +461,12 @@ function renderHeader(): void { $('step1-status').textContent = pier?.model ? '생성됨' : '미생성'; renderSaveStatus(); const total = pendingTotal(); - const checked = state.piers.filter((p) => p.checked).length; + const targets = generationTargets(); $('step2-status').textContent = total ? `변경 ${total}건 · 재생성 필요` : '예정'; const regen = $('regen'); - regen.disabled = checked === 0; - regen.textContent = total ? `선택 교각 다시 생성 · 변경 ${total}` : '선택 교각 형상 생성'; - $('bulk-count').textContent = `선택 ${checked}기`; - $('bulk-regen').disabled = checked === 0; - $('bulk-copy').disabled = !activePier(); + regen.disabled = targets.length === 0; + regen.textContent = targets.length ? `변경 교각 다시 생성 · ${targets.length}기` : '교각 형상 생성'; + renderCopyPanel(); } // --- results panel (시안 ① 입력값 · 계산 결과) ---------------------------------------- @@ -500,8 +481,7 @@ function renderInputTable(): void { const inp = p.input; const front = inp.bearings.filter((b) => b.row === '전열').length; const back = inp.bearings.filter((b) => b.row === '후열').length; - const classes = [p.checked ? 'shown' : '', p.input.name === state.active ? 'active' : ''].filter(Boolean).join(' '); - const tr = el('tr', undefined, classes || undefined); + const tr = el('tr', undefined, p.input.name === state.active ? 'active' : undefined); tr.tabIndex = 0; tr.addEventListener('click', () => selectPier(p.input.name)); tr.addEventListener('keydown', (e) => { @@ -648,7 +628,7 @@ function renderAll(): void { } // --- 속성 복사 (시안 ⑥, plan C8) --------------------------------------------------------- -/** Names of the pier targets ticked in the copy dialog. */ +/** Names of the pier targets ticked in the copy panel. */ let copySelection = new Set(); function copyTargets(): Pier[] { @@ -663,7 +643,7 @@ function selectedCopyTargets(): Pier[] { } function chosenCopyItems(): CopyItem[] { - return Array.from(document.querySelectorAll('#copy-dialog input[data-item]')) + return Array.from(document.querySelectorAll('#copy-panel input[data-item]')) .filter((i) => i.checked) .map((i) => i.dataset.item as CopyItem); } @@ -719,20 +699,22 @@ function renderCopyPreview(): void { apply.disabled = applied === 0; } -function openCopyDialog(): void { +/** The inline 속성 복사 panel: source is the 기준 pier; targets are ticked inside the panel only. */ +function renderCopyPanel(): void { const src = activePier(); - if (!src) return; - $('cp-source').textContent = `${src.input.name} · ${src.input.type} · ${src.input.foundationType}`; - for (const i of document.querySelectorAll('#copy-dialog input[data-item]')) { - i.checked = (DEFAULT_COPY_ITEMS as string[]).includes(i.dataset.item ?? ''); - } - copySelection = new Set(state.piers.filter((p) => p.checked && p !== src).map((p) => p.input.name)); + $('cp-source').textContent = src ? `${src.input.name} · ${src.input.type} · ${src.input.foundationType}` : '교각을 선택하세요'; renderCopyTargets(); renderCopyPreview(); - $('copy-dialog').showModal(); } -function applyCopyDialog(): void { +/** 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; const items = chosenCopyItems(); @@ -749,7 +731,6 @@ function applyCopyDialog(): void { const next = { ...t, draft: applyCopy(t.draft, plan) }; return regen ? regenerateEntry(next) : next; }); - $('copy-dialog').close(); setupScene(); renderAll(); setStatus(`${applied}기에 속성을 복사했습니다. 변경 ${changes}건${regen ? ' (형상 다시 생성)' : ' · 재생성 필요'}.`); @@ -757,7 +738,7 @@ function applyCopyDialog(): void { // --- input cards and change tracking (plan B1 · C2) ------------------------------------- function pendingTotal(): number { - return state.piers.filter((p) => p.checked).reduce((n, p) => n + pendingKeys(p).length, 0); + return state.piers.reduce((n, p) => n + pendingKeys(p).length, 0); } function fieldLabel(key: EditableKey): string { @@ -812,12 +793,6 @@ function renderEditCard(): void { : '기본값과 같습니다.'; btn.textContent = `다시 생성 · 변경 ${status.pending.length}`; btn.disabled = status.pending.length === 0; - // The 기준 pier's overrides can go to the other checked piers; they take effect on 선택 교각 다시 생성. - const targets = overrideTargets(state.piers.map(selectionOf), state.active); - const applyBtn = $('edit-apply-active'); - applyBtn.hidden = targets.length === 0; - applyBtn.textContent = `활성 교각 ${targets.length}기에 적용`; - applyBtn.disabled = overrideKeys(pier).length === 0; } /** One row per input key: value in the draft, the imported default beside it, and a revert button when overridden. */ @@ -1079,10 +1054,17 @@ function wireRebarPicking(): void { }); } +/** The 속성 복사 panel sits at the bottom of the 부재별 편집창: the ① edit card, or the ② member panel. */ +function placeCopyPanel(): void { + const host = state.stage === 'plan' ? $('member-regen').parentElement : $('edit-regen').parentElement; + if (host) host.appendChild($('copy-panel')); +} + function setStage(stage: Stage): void { // ① opens on 전체 보기 (default). The button is gone, so 전체 보기 returns each time ① is entered. if (stage === 'shape' && state.stage !== 'shape') state.allView = true; state.stage = stage; + placeCopyPanel(); // Switching 전체 보기 on or off in ① needs the scene rebuilt for the new layout. if ((allPiers !== null) !== multiSceneOn()) setupScene(); if (state.scene) setRebarView(state.scene, stage === 'rebar'); @@ -1607,9 +1589,6 @@ function renderRebar(): void { const pier = activePier(); // 교각 목록 with the 배근 status (시안 ③). The checks are the same as in ① and ②. renderPierRows($('rebar-pier-items'), 'rebar'); - const checked = state.piers.filter((p) => p.checked).length; - $('rebar-bulk-count').textContent = `선택 ${checked}기`; - $('rebar-bulk-copy').disabled = !pier; const tabs = $('rebar-tabs'); tabs.replaceChildren(); for (const m of REBAR_MEMBERS) { @@ -1769,10 +1748,7 @@ function renderQuantityLink(box: HTMLElement, rows: RebarRow[]): void { /** ④ 재료표: the 철근재료표 of each target pier, bars by 강종 → 직경 → 번호, with subtotals and the totals. */ function quantityTargets(): Pier[] { - const checked = state.piers.filter((p) => p.checked && p.model); - if (checked.length) return checked; - const active = activePier(); - return active?.model ? [active] : []; + return state.piers.filter((p) => p.model); } /** Length cell of a 재료표 line: the length, or 여러 길이 with the pieces on hover. */ @@ -2288,16 +2264,6 @@ function setStrataRows(name: string, rows: StrataRow[]): void { renderAll(); } -/** 일괄 적용: the source's 위치 and 지하수위 go to the checked piers, which are then regenerated. */ -function applyGroundPositionTo(source: Pier, targets: Pier[]): void { - for (const target of targets) { - target.draft = applyCopy(target.draft, planCopy(source.draft, target.draft, ['ground-position'])); - } - regenerateAllPiers(); - renderAll(); - setStatus(`${targets.length}기에 위치와 지하수위를 적용했습니다.`); -} - /** * ② 지반·지층 card (시안 2026-10-26; 승인 2026-10-26: 지반고는 계산에 쓰고, 지층은 행으로 입력하며, 지하수위는 직접 입력). * The ground model import and the 지반고 apply stay at the bottom, as before. @@ -2454,15 +2420,6 @@ function renderGroundModel(box: HTMLDivElement, pier: Pier): void { wrap.appendChild(fig); } - // 일괄 적용: copies the 위치 and 지하수위 to the other checked piers (속성 복사 item 'ground-position'). - const others = state.piers.filter((q) => q.checked && q.input.name !== name); - const batch = el('button', `${name} 외 일괄 대상 ${others.length}기에 적용`, 'btn small'); - batch.type = 'button'; - batch.title = '위치와 지하수위 설정을 체크한 다른 교각에 적용합니다.'; - batch.disabled = others.length === 0; - batch.addEventListener('click', () => applyGroundPositionTo(pier, others)); - wrap.appendChild(batch); - // ② 지반 모델 (승인 2026-10-23): the pier-centre ground EL from ground_model.json. Applied only when the user presses the button. wrap.appendChild(el('p', '지반 모델 (지반 모델 결과 · ground_model.json)', 'lbl')); @@ -3059,19 +3016,23 @@ async function fetchSample(): Promise { return new Uint8Array(await res.arrayBuffer()); } -function regenerateChecked(): void { +/** Piers that need a (re)build: never generated, or with changes not yet in the 3D model. */ +function generationTargets(): Pier[] { + return state.piers.filter((p) => !p.model || pendingKeys(p).length > 0); +} + +function regenerateChanged(): void { + const names = new Set(generationTargets().map((p) => p.input.name)); let errors = 0; - let count = 0; state.piers = state.piers.map((p) => { - if (!p.checked) return p; - count++; + if (!names.has(p.input.name)) return p; const next = regenerateEntry(p); if (!next.model) errors++; return next; }); setupScene(); renderAll(); - setStatus(`${count}기 형상을 생성했습니다.${errors ? ` 오류 ${errors}기는 생성되지 않았습니다.` : ''}`); + setStatus(`${names.size}기 형상을 생성했습니다.${errors ? ` 오류 ${errors}기는 생성되지 않았습니다.` : ''}`); } // --- events ----------------------------------------------------------------------------- @@ -3079,10 +3040,7 @@ $('open-import').addEventListener('click', openImport); $('dlg-close').addEventListener('click', () => $('import-dialog').close()); $('dlg-cancel').addEventListener('click', () => $('import-dialog').close()); $('dlg-apply').addEventListener('click', applyImport); -$('regen').addEventListener('click', regenerateChecked); -$('bulk-regen').addEventListener('click', regenerateChecked); -$('bulk-copy').addEventListener('click', openCopyDialog); -$('rebar-bulk-copy').addEventListener('click', openCopyDialog); +$('regen').addEventListener('click', regenerateChanged); $('nav-shape').addEventListener('click', () => setStage('shape')); $('nav-plan').addEventListener('click', () => setStage('plan')); $('nav-rebar').addEventListener('click', () => setStage('rebar')); @@ -3103,13 +3061,11 @@ $('cp-same-type').addEventListener('change', () => { renderCopyTargets(); renderCopyPreview(); }); -for (const i of document.querySelectorAll('#copy-dialog input[data-item]')) { +for (const i of document.querySelectorAll('#copy-panel input[data-item]')) { i.addEventListener('change', renderCopyPreview); } $('cp-regen').addEventListener('change', renderCopyPreview); -$('cp-apply').addEventListener('click', applyCopyDialog); -$('cp-cancel').addEventListener('click', () => $('copy-dialog').close()); -$('cp-close').addEventListener('click', () => $('copy-dialog').close()); +$('cp-apply').addEventListener('click', applyCopyPanel); $('sv-save').addEventListener('click', confirmSaveDialog); $('sv-cancel').addEventListener('click', () => $('save-dialog').close()); $('sv-close').addEventListener('click', () => $('save-dialog').close()); @@ -3118,21 +3074,6 @@ $('edit-regen').addEventListener('click', () => { if (pier) regenerateOne(pier.input.name); }); -$('edit-apply-active').addEventListener('click', () => { - const base = activePier(); - if (!base) return; - const targets = overrideTargets(state.piers.map(selectionOf), state.active); - const keys = overrideKeys(base); - if (targets.length === 0 || keys.length === 0) return; - const labels = keys.map((k) => FIELD_LABELS[k]).join(', '); - const ok = window.confirm( - `${targets.join(', ')}에 기준 ${base.input.name}의 ${keys.length}개 항목(${labels})을 적용합니다.\n적용 후 '선택 교각 다시 생성'을 눌러야 3D에 반영됩니다.`, - ); - if (!ok) return; - for (const p of state.piers) if (targets.includes(p.input.name)) p.draft = applyOverrides(base, p.draft); - renderAll(); - setStatus(`${targets.length}기에 ${keys.length}개 항목을 적용했습니다.`); -}); $('project-name').addEventListener('input', updateApply); $('pick-file').addEventListener('click', () => $('file').click()); @@ -3189,10 +3130,6 @@ $('type-filter').addEventListener('change', (ev) => { state.filter = (ev.target as HTMLSelectElement).value as 'all' | PierType; renderPierList(); }); -$('select-all').addEventListener('click', () => { - state.piers = setVisibleChecked(state.piers, state.filter, !allVisibleChecked(state.piers, state.filter)); - renderAll(); -}); $('save-project').addEventListener('click', saveProject); $('open-project').addEventListener('click', () => $('project-file').click()); @@ -3283,6 +3220,7 @@ function restorePanel(id: string): void { } restorePanel('parts-info'); restorePanel('color-info'); +resetCopyItems(); $('all-toggle').addEventListener('click', () => { state.allView = !state.allView; diff --git a/src/style.css b/src/style.css index 4cd668c..e14162d 100644 --- a/src/style.css +++ b/src/style.css @@ -816,8 +816,15 @@ body { background: #eef1f4; font-family: 'Noto Sans KR', system-ui, sans-serif; #inputs-all { font-size: 12px; width: 100%; } #inputs-all th, #inputs-all td { white-space: nowrap; padding: 4px 6px; } #inputs-all tbody tr { cursor: pointer; } -#inputs-all tbody tr.shown td { background: #f4f8fc; } #inputs-all tbody tr.active td { background: #e3edf8; font-weight: 700; } /* Import preview: all piers' 받침·IP rows scroll inside the dialog. */ .pv-scroll { max-height: 240px; overflow: auto; } + +/* 속성 복사: an inline panel at the bottom of the 부재별 편집창 (was a modal dialog). */ +.copy-panel { margin-top: 12px; border-top: 1px solid #d5dde6; padding-top: 10px; } +.copy-panel .copy-head h3 { margin: 0 0 4px; font-size: 13px; } +.copy-panel .dlg-body { display: block; } +.copy-panel .dlg-col + .dlg-col { margin-top: 10px; } +.copy-panel .grid-table { font-size: 12px; display: block; overflow-x: auto; } +.copy-panel .copy-foot { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; justify-content: space-between; margin-top: 10px; } diff --git a/tests/active-piers.test.ts b/tests/active-piers.test.ts deleted file mode 100644 index baa7767..0000000 --- a/tests/active-piers.test.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { applyOverrides, overrideTargets, shownNames, type PierSelection } from '../src/core/active-piers'; -import { loadP1 } from './helpers'; - -const list: PierSelection[] = [ - { name: 'P1', checked: true, hasModel: true }, - { name: 'P2', checked: true, hasModel: true }, - { name: 'P3', checked: false, hasModel: true }, - { name: 'P4', checked: true, hasModel: false }, -]; - -describe('shownNames (체크 = 활성, 기준 교각은 항상 표시)', () => { - it('shows every checked pier that has a model, in list order', () => { - expect(shownNames(list, 'P1')).toEqual(['P1', 'P2']); - }); - - it('keeps the 기준 pier even when it is unchecked', () => { - expect(shownNames(list, 'P3')).toEqual(['P1', 'P2', 'P3']); - }); - - it('never shows a pier without a model', () => { - expect(shownNames(list, 'P4')).toEqual(['P1', 'P2']); - }); - - it('shows only the 기준 pier when nothing else is checked', () => { - const none = list.map((p) => ({ ...p, checked: false })); - expect(shownNames(none, 'P2')).toEqual(['P2']); - }); -}); - -describe('overrideTargets (기준 교각의 수정 값을 받을 교각)', () => { - it('is the checked piers other than the 기준 pier', () => { - expect(overrideTargets(list, 'P1')).toEqual(['P2']); - expect(overrideTargets(list, 'P3')).toEqual(['P1', 'P2']); - }); -}); - -describe('applyOverrides (기준 교각의 변경 항목만 옮김)', () => { - const base = loadP1(); - const defaults = base; - const edited = { ...base, copingWidthMm: base.copingWidthMm + 100 }; - const target = { ...base, copingLengthMm: base.copingLengthMm + 500 }; - - it('copies only the keys the 기준 pier overrides, keeping the target own values', () => { - const next = applyOverrides({ defaults, input: defaults, draft: edited }, target); - expect(next.copingWidthMm).toBe(edited.copingWidthMm); - expect(next.copingLengthMm).toBe(target.copingLengthMm); - }); - - it('returns the target unchanged when the 기준 pier has no overrides', () => { - const next = applyOverrides({ defaults, input: defaults, draft: defaults }, target); - expect(next).toEqual(target); - }); -});