feat(view): 체크한 교각을 활성으로 보고 기준 교각 수정값을 활성 교각에 적용
- 체크 = 활성: 체크된 교각(모델 있는 것)과 기준 교각을 3D에 함께 표시 - ② 형상계획도 활성 교각이 2기 이상이면 같은 좌표계로 배치 - 이름 클릭 = 기준 교각: 코핑·말뚝 수정 카드의 대상 - '활성 교각에 적용': 기준 교각의 변경 항목만 다른 활성 교각에 복사 (확인 후, 다시 생성으로 반영) - 입력값 표: 활성 교각 행 음영, 기준 교각 굵게 Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
60e50b1186
commit
4fc02afb1a
5 files changed
+131
-8
No files matched your search
@@ -74,6 +74,7 @@
|
||||
<div id="edit-fields"></div>
|
||||
<p id="edit-summary" class="muted small"></p>
|
||||
<button id="edit-regen" class="btn primary" type="button" disabled>다시 생성 · 변경 0</button>
|
||||
<button id="edit-apply-active" class="btn-o" type="button" hidden>활성 교각에 적용</button>
|
||||
</section>
|
||||
</section>
|
||||
</aside>
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
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;
|
||||
}
|
||||
+46
-8
@@ -7,6 +7,7 @@ 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';
|
||||
@@ -350,13 +351,26 @@ function setView(kind: ViewKind): void {
|
||||
}
|
||||
|
||||
/** 전체 보기 applies on ① 형상 생성 only; ③ and the others keep the single-pier scene. */
|
||||
function allViewOn(): boolean {
|
||||
return state.allView && state.stage === 'shape';
|
||||
function selectionOf(p: Pier): PierSelection {
|
||||
return { name: p.input.name, checked: p.checked, hasModel: p.model !== null };
|
||||
}
|
||||
|
||||
/** Builds 전체 보기 from every pier that has a model. Returns false when none does. */
|
||||
/** The piers the 3D view shows together: the checked ones plus the 기준 pier (체크 = 활성, 승인 2026-10-27). */
|
||||
function shownPiers(): Pier[] {
|
||||
const names = new Set(shownNames(state.piers.map(selectionOf), state.active));
|
||||
return state.piers.filter((p) => names.has(p.input.name));
|
||||
}
|
||||
|
||||
/** Several piers in one frame: ① 전체 보기 (button), or ② when two or more piers are shown. */
|
||||
function multiSceneOn(): boolean {
|
||||
if (state.stage === 'shape') return state.allView;
|
||||
if (state.stage === 'plan') return shownPiers().length >= 2;
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Builds the shown piers in one frame. Returns false when none has a model. */
|
||||
function buildAllScene(): boolean {
|
||||
const entries = state.piers.flatMap((p) => (p.model ? [{ name: p.input.name, model: p.model }] : []));
|
||||
const entries = shownPiers().flatMap((p) => (p.model ? [{ name: p.input.name, model: p.model }] : []));
|
||||
if (entries.length === 0) return false;
|
||||
allPiers = buildAllPiers(entries, groundModel?.terrainGrid);
|
||||
world.add(allPiers.piers);
|
||||
@@ -375,7 +389,7 @@ function setupScene(): void {
|
||||
}
|
||||
if (rebar3d) world.remove(rebar3d.group);
|
||||
rebar3d = null;
|
||||
if (allViewOn() && buildAllScene()) {
|
||||
if (multiSceneOn() && buildAllScene()) {
|
||||
setView(state.view);
|
||||
return;
|
||||
}
|
||||
@@ -430,6 +444,7 @@ function renderPierRows(list: HTMLUListElement, mode: Stage): void {
|
||||
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).
|
||||
@@ -485,7 +500,8 @@ 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 tr = el('tr', undefined, p.input.name === state.active ? 'active' : undefined);
|
||||
const classes = [p.checked ? 'shown' : '', p.input.name === state.active ? 'active' : ''].filter(Boolean).join(' ');
|
||||
const tr = el('tr', undefined, classes || undefined);
|
||||
tr.tabIndex = 0;
|
||||
tr.addEventListener('click', () => selectPier(p.input.name));
|
||||
tr.addEventListener('keydown', (e) => {
|
||||
@@ -533,7 +549,7 @@ function renderPanels(): void {
|
||||
|
||||
renderInputTable();
|
||||
$('inputs-active').textContent = pier ? `활성 교각 ${pier.input.name} 상세 (표의 행을 누르면 바뀜)` : '';
|
||||
$('edit-title').textContent = pier ? `코핑·말뚝 입력 수정 · ${pier.input.name}` : '코핑·말뚝 입력 수정';
|
||||
$('edit-title').textContent = pier ? `코핑·말뚝 입력 수정 · 기준 ${pier.input.name}` : '코핑·말뚝 입력 수정';
|
||||
|
||||
if (!pier) {
|
||||
fillKv($('inputs'), []);
|
||||
@@ -796,6 +812,12 @@ 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 = $<HTMLButtonElement>('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. */
|
||||
@@ -1062,7 +1084,7 @@ function setStage(stage: Stage): void {
|
||||
if (stage === 'shape' && state.stage !== 'shape') state.allView = true;
|
||||
state.stage = stage;
|
||||
// Switching 전체 보기 on or off in ① needs the scene rebuilt for the new layout.
|
||||
if ((allPiers !== null) !== allViewOn()) setupScene();
|
||||
if ((allPiers !== null) !== multiSceneOn()) setupScene();
|
||||
if (state.scene) setRebarView(state.scene, stage === 'rebar');
|
||||
$<HTMLElement>('view-shape').hidden = stage !== 'shape';
|
||||
$<HTMLElement>('view-plan').hidden = stage !== 'plan';
|
||||
@@ -3095,6 +3117,22 @@ $<HTMLButtonElement>('edit-regen').addEventListener('click', () => {
|
||||
const pier = activePier();
|
||||
if (pier) regenerateOne(pier.input.name);
|
||||
});
|
||||
|
||||
$<HTMLButtonElement>('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}개 항목을 적용했습니다.`);
|
||||
});
|
||||
$<HTMLInputElement>('project-name').addEventListener('input', updateApply);
|
||||
|
||||
$<HTMLButtonElement>('pick-file').addEventListener('click', () => $<HTMLInputElement>('file').click());
|
||||
|
||||
@@ -816,6 +816,7 @@ 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. */
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user