diff --git a/index.html b/index.html index d4f0361..0073078 100644 --- a/index.html +++ b/index.html @@ -193,6 +193,7 @@ 받침 값
+
형상 (벽체·흉벽, 저판, 말뚝)
@@ -204,6 +205,8 @@
+

부재 · 교대

+

부재 · 5개

diff --git a/src/core/abut/member-list.ts b/src/core/abut/member-list.ts new file mode 100644 index 0000000..6e220f9 --- /dev/null +++ b/src/core/abut/member-list.ts @@ -0,0 +1,66 @@ +import type { WingForm } from './geometry'; +import { ABUT_SHAPE_DEFAULT, type AbutShape } from './shape'; + +/** 부재 · 교대 list in ② (시안): the card each row opens. */ +export type AbutMemberId = 'bearing' | 'shape' | 'wing' | 'slab' | 'ground'; + +/** Cards that take input in this step; slab and ground only show their values (phase 1, 2026-11-03). */ +export const ABUT_MEMBER_EDITABLE: Record = { + bearing: true, + shape: true, + wing: true, + slab: false, + ground: false, +}; + +const WING_LABEL: Record = { general: '일반형', short: '단축형', none: '없음' }; + +/** The subset of an abutment record the list reads. */ +export interface AbutMemberInput { + bearings: { seatEl?: number }[]; + shape?: Partial; + wings?: { left: WingForm; right: WingForm }; + groundEl: number; +} + +export interface AbutMemberRow { + id: AbutMemberId; + label: string; + summary: string; + /** True when the value differs from the default (or a 받침 has its own 교좌면 EL). */ + changed: boolean; +} + +/** + * The five rows of the 부재 · 교대 list. `slabWidth` is the computed 접속슬래브 폭 text, passed in + * so this stays a pure function of the record. + */ +export function abutMemberRows(rec: AbutMemberInput, slabWidth: string): AbutMemberRow[] { + const shape = { ...ABUT_SHAPE_DEFAULT, ...(rec.shape ?? {}) }; + const shapeChanged = (Object.keys(ABUT_SHAPE_DEFAULT) as (keyof AbutShape)[]).some( + (key) => (rec.shape?.[key] ?? ABUT_SHAPE_DEFAULT[key]) !== ABUT_SHAPE_DEFAULT[key], + ); + const wings = rec.wings ?? { left: 'general' as WingForm, right: 'general' as WingForm }; + return [ + { + id: 'bearing', + label: '교량받침·받침블럭·솔플레이트', + summary: `${rec.bearings.length}개 · z = 패드 상면`, + changed: rec.bearings.some((b) => b.seatEl !== undefined), + }, + { + id: 'shape', + label: '벽체·흉벽', + summary: `벽체 두께 ${shape.wallThicknessMm} · 흉벽 ${shape.parapetHeightMm}`, + changed: shapeChanged, + }, + { + id: 'wing', + label: '날개벽', + summary: `좌 ${WING_LABEL[wings.left]} · 우 ${WING_LABEL[wings.right]}`, + changed: wings.left !== 'general' || wings.right !== 'general', + }, + { id: 'slab', label: '접속슬래브', summary: slabWidth, changed: false }, + { id: 'ground', label: '지반·지층', summary: `지반고 ${rec.groundEl.toFixed(3)} m`, changed: false }, + ]; +} diff --git a/src/main.ts b/src/main.ts index fcc08b5..0f29089 100644 --- a/src/main.ts +++ b/src/main.ts @@ -11,6 +11,7 @@ import { ABUT_DETAIL_LABELS, abutInputRows, abutSummaryRows } from './core/abut/ import { applyAbutEdit, applyBearingEdit, applyShapeEdit, ABUT_EDIT_FIELDS, SHAPE_EDIT_FIELDS, restoreAbutDefault, setWingForm } from './core/abut/edit'; import { ABUT_SHAPE_DEFAULT, resolveShape } from './core/abut/shape'; import { abutShown, piersShown } from './core/abut/visibility'; +import { abutMemberRows, ABUT_MEMBER_EDITABLE, type AbutMemberId } from './core/abut/member-list'; import { abutSectionSvg } from './view/abut-section-svg'; import type { WingForm } from './core/abut/geometry'; import { buildMembers } from './core/abut/members'; @@ -129,6 +130,8 @@ const state = { abutments: [] as AbutRecord[], /** 교대 편집 카드에서 고른 교대 (2026-11-03). */ abutActive: null as string | null, + /** The 부재 · 교대 row whose card is open in ② (2026-11-03). */ + abutMember: 'bearing' as AbutMemberId, /** The editable part of the 교대 panel is open (2026-11-03). */ abutEditOpen: false, /** 교대 기본값 (가져온 그대로), for 기본값 복원. */ @@ -611,7 +614,7 @@ function focusAbut(name: string): void { /** The 교대 입력 panel slots of ① (replaces the pier info) and ② (under the list). */ const ABUT_SLOTS = { shape: { root: 'abut-panel', name: 'abut-panel-name', input: 'abut-panel-input', toggle: 'abut-panel-edit-toggle', editor: 'abut-panel-editor', summary: 'abut-panel-summary', basic: 'abut-panel-basic', bearings: 'abut-panel-bearings', shape: 'abut-panel-shape', msg: 'abut-panel-msg', prefix: 'abs' }, - plan: { root: 'abut-panel-plan', name: 'abut-panel-plan-name', input: 'abut-panel-plan-input', toggle: 'abut-panel-plan-edit-toggle', editor: 'abut-panel-plan-editor', summary: 'abut-panel-plan-summary', basic: 'abut-panel-plan-basic', bearings: 'abut-panel-plan-bearings', shape: 'abut-panel-plan-shape', msg: 'abut-panel-plan-msg', prefix: 'abp' }, + plan: { wing: 'abut-panel-plan-wing', root: 'abut-panel-plan',name: 'abut-panel-plan-name', input: 'abut-panel-plan-input', toggle: 'abut-panel-plan-edit-toggle', editor: 'abut-panel-plan-editor', summary: 'abut-panel-plan-summary', basic: 'abut-panel-plan-basic', bearings: 'abut-panel-plan-bearings', shape: 'abut-panel-plan-shape', msg: 'abut-panel-plan-msg', prefix: 'abp' }, } as const; /** Shows the 교대 입력 in place of the pier info when an abutment is selected (2026-11-03). */ @@ -625,6 +628,44 @@ function renderAbutEditor(): void { box.hidden = !rec; if (rec) fillAbutSlot(slot, rec); } + if (rec) { + renderAbutMembers(rec); + applyAbutMemberCard(rec); + } else { + $('abut-members').replaceChildren(); + } +} + +/** The 부재 · 교대 rows in ②: each row's value and the card it opens (2026-11-03, phase 1). */ +function renderAbutMembers(rec: AbutRecord): void { + const box = $('abut-members'); + const slabWidth = abutSummaryRows(rec).find(([label]) => label === '접속슬래브 폭')?.[1] ?? '—'; + box.replaceChildren(); + for (const row of abutMemberRows(rec, slabWidth)) { + const btn = el('button', undefined, 'pier-name'); + btn.type = 'button'; + btn.setAttribute('aria-pressed', String(row.id === state.abutMember)); + btn.append(el('span', row.label), el('span', row.summary, 'muted small')); + if (row.changed) btn.append(el('span', '변경', 'badge')); + btn.addEventListener('click', () => { + state.abutMember = row.id; + state.abutEditOpen = true; + renderAll(); + }); + const li = el('li'); + li.append(btn); + box.append(li); + } +} + +/** Shows only the card of the open row in the ② 교대 editor; the basic items stay visible. */ +function applyAbutMemberCard(rec: AbutRecord): void { + const member = state.abutMember; + const group = (id: string) => $(id).closest('details') ?? $(id); + group('abut-panel-plan-bearings').hidden = member !== 'bearing'; + group('abut-panel-plan-shape').hidden = member !== 'shape'; + $('abut-panel-plan-wing').hidden = member !== 'wing'; + $('abut-panel-plan-msg').textContent = ABUT_MEMBER_EDITABLE[member] ? '' : `${rec.name} 입력은 2단계에서 추가합니다. 현재 값만 보여 줍니다.`; } /** Fills one 교대 입력 panel: the basic items, the 받침 values and the 형상 values. Each change is checked and applied. */ @@ -651,6 +692,9 @@ function fillAbutSlot(slot: (typeof ABUT_SLOTS)[keyof typeof ABUT_SLOTS], rec: A const shape = $(slot.shape); const msg = $(slot.msg); basic.replaceChildren(); + // ② puts the wing forms in their own card; ① keeps them in the basic items. + const wingBox = 'wing' in slot ? $(slot.wing) : basic; + wingBox.replaceChildren(); bearings.replaceChildren(); shape.replaceChildren(); // 삽도 (벽체·흉벽 개략 단면): the values it names are the card's values, so the drawing follows each change. @@ -700,7 +744,7 @@ function fillAbutSlot(slot: (typeof ABUT_SLOTS)[keyof typeof ABUT_SLOTS], rec: A for (const [value, text] of [['general', '일반형'], ['short', '단축형'], ['none', '없음']] as const) sel.appendChild(new Option(text, value)); sel.value = rec.wings?.[side] ?? 'general'; sel.addEventListener('change', () => apply(setWingForm(rec, side, sel.value as WingForm))); - basic.append(lab, sel); + wingBox.append(lab, sel); } const restore = el('button', '기본값으로 되돌리기', 'btn small'); restore.type = 'button'; diff --git a/src/style.css b/src/style.css index 436221b..625c96a 100644 --- a/src/style.css +++ b/src/style.css @@ -882,3 +882,10 @@ body { background: #eef1f4; font-family: 'Noto Sans KR', system-ui, sans-serif; #view-plan.abut-selected #members, #view-plan.abut-selected section[aria-labelledby="member-title"], #view-plan.abut-selected #copy-panel { display: none; } + +/* ② 부재 · 교대 list: shown only while an abutment is selected (2026-11-03). */ +.abut-members-title, +#abut-members { display: none; } +#view-plan.abut-selected .abut-members-title, +#view-plan.abut-selected #abut-members { display: block; } +#abut-members .pier-name span:first-child { display: block; } diff --git a/tests/abut-member-list.test.ts b/tests/abut-member-list.test.ts new file mode 100644 index 0000000..419998e --- /dev/null +++ b/tests/abut-member-list.test.ts @@ -0,0 +1,47 @@ +import { describe, expect, it } from 'vitest'; +import { ABUT_MEMBER_EDITABLE, abutMemberRows } from '../src/core/abut/member-list'; + +const base = { + bearings: [{ seatEl: undefined }, { seatEl: undefined }], + groundEl: 33.984, +}; + +describe('교대 부재 목록 (시안 ② 부재 · 교대)', () => { + it('has the five rows of the mockup, in order', () => { + const rows = abutMemberRows(base, '11.253 m'); + expect(rows.map((r) => r.id)).toEqual(['bearing', 'shape', 'wing', 'slab', 'ground']); + expect(rows[0].label).toBe('교량받침·받침블럭·솔플레이트'); + }); + + it('a default abutment shows no change marks', () => { + expect(abutMemberRows(base, '—').every((r) => !r.changed)).toBe(true); + }); + + it('marks the shape when a value differs from the default', () => { + const rows = abutMemberRows({ ...base, shape: { parapetHeightMm: 2200 } }, '—'); + expect(rows.find((r) => r.id === 'shape')?.changed).toBe(true); + expect(rows.find((r) => r.id === 'shape')?.summary).toBe('벽체 두께 2400 · 흉벽 2200'); + }); + + it('marks the wing when a side is not the general form, and names both sides', () => { + const rows = abutMemberRows({ ...base, wings: { left: 'general', right: 'short' } }, '—'); + const wing = rows.find((r) => r.id === 'wing'); + expect(wing?.changed).toBe(true); + expect(wing?.summary).toBe('좌 일반형 · 우 단축형'); + }); + + it('marks a 받침 that has its own 교좌면 EL', () => { + const rows = abutMemberRows({ ...base, bearings: [{ seatEl: 44.4 }, { seatEl: undefined }] }, '—'); + expect(rows[0].changed).toBe(true); + }); + + it('shows the slab width text and the ground EL as given', () => { + const rows = abutMemberRows(base, '11.253 m'); + expect(rows.find((r) => r.id === 'slab')?.summary).toBe('11.253 m'); + expect(rows.find((r) => r.id === 'ground')?.summary).toBe('지반고 33.984 m'); + }); + + it('only the bearing, shape and wing cards take input in this step', () => { + expect(ABUT_MEMBER_EDITABLE).toEqual({ bearing: true, shape: true, wing: true, slab: false, ground: false }); + }); +});