feat(view): 체크한 교각을 활성으로 보고 기준 교각 수정값을 활성 교각에 적용

- 체크 = 활성: 체크된 교각(모델 있는 것)과 기준 교각을 3D에 함께 표시
- ② 형상계획도 활성 교각이 2기 이상이면 같은 좌표계로 배치
- 이름 클릭 = 기준 교각: 코핑·말뚝 수정 카드의 대상
- '활성 교각에 적용': 기준 교각의 변경 항목만 다른 활성 교각에 복사 (확인 후, 다시 생성으로 반영)
- 입력값 표: 활성 교각 행 음영, 기준 교각 굵게

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
ypshimandClaude Haiku 5.5 committed 2026-10-27 15:45:05 +09:00
1 parent 60e50b1186
commit 4fc02afb1a
5 files changed
+131 -8

No files matched your search

+46 -8
View File
@@ -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());