feat(core,view): shape-option rules (plan C5) and pier selection rules (plan C6)

- C5: each member's options come from one table. Only the defaults (the 1단계
  model) are selectable. The others show as disabled chips with 규칙 미정 (Q6).
  The member summary is built from the defaults.
- C6: 전체 선택 acts only on visible, buildable piers; it reads 선택 해제 only
  when all of those are checked. A check survives a change of the type filter.
  The pier list scrolls past 300 px, for 20 piers.
- Tests: defaults are the only choices; unknown options refused; the 20-pier
  mixed list with two failed piers; filter and check persistence.
- Not verified in a browser.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
ypshimandClaude Haiku 5.5 committed 2026-10-22 10:15:03 +09:00
1 parent 56362570ec
commit 5b162c2441
7 files changed
+228 -13

No files matched your search

+39 -13
View File
@@ -8,6 +8,8 @@ import { NOT_IMPLEMENTED_RULES, validateInput, validatePier } from './core/valid
import { buildTemplateWorkbook } from './core/template';
import { EDITABLE_FIELDS, overrideKeys, pendingKeys, revertDraft, type EditableKey, type PierValues } from './core/changes';
import { applyCopy, DEFAULT_COPY_ITEMS, planCopy, type CopyItem } from './core/copy';
import { allVisibleChecked, setVisibleChecked, 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';
import type { Issue, PierInput, PierType } from './core/types';
@@ -42,13 +44,12 @@ const TYPES: PierType[] = ['T형', '2주형', '3주형'];
const CRS = '국토좌표계 (x = 동, y = 북)';
/** 시안 ② 부재 목록 (spec §5.2). `keys` are the inputs that exist today; the rest show values only. */
type MemberId = 'bearing' | 'coping' | 'column' | 'footing' | 'ground';
const MEMBERS: { id: MemberId; label: string; summary: string; keys: EditableKey[]; note: string }[] = [
{ id: 'bearing', label: '교량받침·받침블럭·솔플레이트', summary: '배치 분리', keys: [], note: '받침 상세(C3)는 Q16 결정 후 추가합니다. 지금은 표시만 합니다.' },
{ id: 'coping', label: '코핑', summary: '교좌면 직각 · 하면 직선', keys: ['copingWidthMm', 'copingLengthMm'], note: '폭과 길이를 입력합니다. 계단 위치·단차선 입력은 다음 단계에서 추가합니다.' },
{ id: 'column', label: '기둥', summary: '원형 · 중실', keys: [], note: '기둥 지름 입력은 다음 단계에서 추가합니다. 높이와 위치는 계산값입니다.' },
{ id: 'footing', label: '확대기초 · 말뚝', summary: '직사각 · 강관', keys: ['pileLengthMm'], note: '말뚝 길이를 입력합니다. 열별 본수는 다음 단계에서 추가합니다.' },
{ id: 'ground', label: '지반·지층', summary: '육상 · 표시만', keys: [], note: '지반고는 입력값을 표시만 합니다. 지층 입력은 다음 단계에서 추가합니다.' },
const MEMBERS: { id: MemberId; label: string; keys: EditableKey[]; note: string }[] = [
{ id: 'bearing', label: '교량받침·받침블럭·솔플레이트', keys: [], note: '받침 상세(C3)는 Q16 결정 후 추가합니다. 지금은 표시만 합니다.' },
{ id: 'coping', label: '코핑', keys: ['copingWidthMm', 'copingLengthMm'], note: '폭과 길이를 입력합니다. 계단 위치·단차선 입력은 다음 단계에서 추가합니다.' },
{ id: 'column', label: '기둥', keys: [], note: '기둥 지름 입력은 다음 단계에서 추가합니다. 높이와 위치는 계산값입니다.' },
{ id: 'footing', label: '확대기초 · 말뚝', keys: ['pileLengthMm'], note: '말뚝 길이를 입력합니다. 열별 본수는 다음 단계에서 추가합니다.' },
{ id: 'ground', label: '지반·지층', keys: [], note: '지반고는 입력값을 표시만 합니다. 지층 입력은 다음 단계에서 추가합니다.' },
];
interface Pending {
@@ -236,8 +237,7 @@ function renderPierList(): void {
const visible = state.piers.filter((p) => state.filter === 'all' || p.input.type === state.filter);
$<HTMLParagraphElement>('pier-empty').hidden = state.piers.length > 0;
const allChecked = visible.length > 0 && visible.every((p) => p.checked);
$<HTMLButtonElement>('select-all').textContent = allChecked ? '선택 해제' : '전체 선택';
$<HTMLButtonElement>('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. */
@@ -590,6 +590,32 @@ function fieldRows(box: HTMLDivElement, pier: Pier, keys: EditableKey[]): { over
return { overrides, pending };
}
/** 형상 선택칩 (plan.md C5): the default is pressed; every other option is shown and disabled with the reason. */
function renderShapeOptions(member: MemberId): void {
const box = $<HTMLDivElement>('member-options');
box.replaceChildren();
SHAPE_GROUPS[member].forEach((group, gi) => {
const row = el('div', undefined, 'shape-row');
row.appendChild(el('span', group.title, 'lbl'));
for (const option of group.options) {
const chip = el('button', option.label, 'chip-opt');
chip.type = 'button';
chip.setAttribute('aria-pressed', String(option.isDefault));
chip.disabled = !isSelectable(option);
if (chip.disabled) chip.title = UNDEFINED_REASON;
chip.addEventListener('click', () => {
try {
choose(member, gi, option.label);
} catch (err) {
setStatus((err as Error).message);
}
});
row.appendChild(chip);
}
box.appendChild(row);
});
}
// --- ② 형상계획 (plan.md C1) -------------------------------------------------------------
function setStage(stage: 'shape' | 'plan'): void {
state.stage = stage;
@@ -621,7 +647,9 @@ function renderPlan(): void {
const member = MEMBERS.find((m) => m.id === state.member)!;
$('member-title').textContent = member.label;
$('member-summary').textContent = `기본: ${member.summary} · 선택: ${member.summary}`;
// Default and current selection are shown separately (spec §5.1). Only defaults are selectable today.
$('member-summary').textContent = `기본: ${defaultSummary(member.id)} · 선택: ${defaultSummary(member.id)}`;
renderShapeOptions(member.id);
$('member-note').textContent = member.note;
const regen = $<HTMLButtonElement>('member-regen');
const fields = $<HTMLDivElement>('member-fields');
@@ -1056,9 +1084,7 @@ $<HTMLSelectElement>('type-filter').addEventListener('change', (ev) => {
renderPierList();
});
$<HTMLButtonElement>('select-all').addEventListener('click', () => {
const visible = state.piers.filter((p) => (state.filter === 'all' || p.input.type === state.filter) && p.model);
const all = visible.every((p) => p.checked);
for (const p of visible) p.checked = !all;
state.piers = setVisibleChecked(state.piers, state.filter, !allVisibleChecked(state.piers, state.filter));
renderAll();
});