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

+59
View File
@@ -0,0 +1,59 @@
/**
* 형상 선택지 (spec §5.2 · plan C5). Only the default of each group can be chosen until the rules for the other
* options are confirmed (Q6). The other options are shown but cannot be selected.
*/
export type MemberId = 'bearing' | 'coping' | 'column' | 'footing' | 'ground';
export interface ShapeOption {
label: string;
/** 굵은 글씨 in the spec: the default that the 1단계 model builds. */
isDefault: boolean;
}
export interface ShapeGroup {
title: string;
options: ShapeOption[];
}
const d = (label: string): ShapeOption => ({ label, isDefault: true });
const x = (label: string): ShapeOption => ({ label, isDefault: false });
export const SHAPE_GROUPS: Record<MemberId, ShapeGroup[]> = {
bearing: [{ title: '배치', options: [d('분리'), x('일체')] }],
coping: [
{ title: '교좌면', options: [d('직각'), x('모따기'), x('라운드')] },
{ title: '하면', options: [d('직선'), x('곡선 오목'), x('곡선 볼록')] },
],
column: [
{ title: '단면', options: [d('원형'), x('트랙'), x('사각')] },
{ title: '중공', options: [d('중실'), x('중공')] },
],
footing: [
{ title: '형상', options: [d('직사각'), x('경사')] },
{ title: '모따기', options: [d('없음'), x('있음')] },
{ title: '말뚝 종류', options: [d('강관'), x('PHC'), x('복합')] },
{ title: '경사말뚝', options: [d('없음'), x('있음')] },
],
ground: [{ title: '위치', options: [d('육상'), x('하상'), x('해상')] }],
};
export const UNDEFINED_REASON = '규칙 미정 (Q6 확정 전 선택 불가)';
/** Options currently selectable. Defaults are the only choices today. */
export function isSelectable(option: ShapeOption): boolean {
return option.isDefault;
}
/** The default summary of a member, e.g. "교좌면 직각 · 하면 직선". */
export function defaultSummary(member: MemberId): string {
return SHAPE_GROUPS[member].map((g) => g.options.find((o) => o.isDefault)!.label).join(' · ');
}
/** Chooses an option in a group. Refuses options whose rules are not confirmed. */
export function choose(member: MemberId, groupIndex: number, optionLabel: string): string {
const group = SHAPE_GROUPS[member][groupIndex];
const option = group?.options.find((o) => o.label === optionLabel);
if (!option) throw new Error(`알 수 없는 선택지: ${optionLabel}`);
if (!isSelectable(option)) throw new Error(`${optionLabel}: ${UNDEFINED_REASON}`);
return option.label;
}