feat(plan): 형상계획 ②에 교대 부재 목록과 부재별 카드 추가 (1단계)
- 부재 · 교대 목록(받침, 벽체·흉벽, 날개벽, 접속슬래브, 지반·지층)과 각 행의 값·변경 표시 (src/core/abut/member-list.ts) - 행을 누르면 그 부재의 카드만 표시. 날개벽 형식은 별도 카드로 분리 - 접속슬래브·지반·지층은 값만 표시 (입력은 2단계) - 모델 변경 없음. 테스트 630건 통과, 타입 검사·빌드 통과. 브라우저 시각 확인은 미실시 Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
929f8eb4a2
commit
18524460e8
5 files changed
+169
-2
No files matched your search
@@ -193,6 +193,7 @@
|
||||
<summary>받침 값</summary>
|
||||
<div id="abut-panel-plan-bearings" class="edit-row"></div>
|
||||
</details>
|
||||
<div id="abut-panel-plan-wing" class="edit-row"></div>
|
||||
<details class="abut-group">
|
||||
<summary>형상 (벽체·흉벽, 저판, 말뚝)</summary>
|
||||
<div id="abut-panel-plan-shape" class="edit-row"></div>
|
||||
@@ -204,6 +205,8 @@
|
||||
<dl id="abut-panel-plan-summary" class="kv wide"></dl>
|
||||
</details>
|
||||
</section>
|
||||
<p class="lbl abut-members-title">부재 · 교대</p>
|
||||
<ul id="abut-members" class="members abut-members" aria-label="교대 부재"></ul>
|
||||
<p class="lbl members-title">부재 · 5개</p>
|
||||
<ul id="members" class="members"></ul>
|
||||
</aside>
|
||||
|
||||
@@ -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<AbutMemberId, boolean> = {
|
||||
bearing: true,
|
||||
shape: true,
|
||||
wing: true,
|
||||
slab: false,
|
||||
ground: false,
|
||||
};
|
||||
|
||||
const WING_LABEL: Record<WingForm, string> = { general: '일반형', short: '단축형', none: '없음' };
|
||||
|
||||
/** The subset of an abutment record the list reads. */
|
||||
export interface AbutMemberInput {
|
||||
bearings: { seatEl?: number }[];
|
||||
shape?: Partial<AbutShape>;
|
||||
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 },
|
||||
];
|
||||
}
|
||||
+46
-2
@@ -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 {
|
||||
$<HTMLUListElement>('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 = $<HTMLUListElement>('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) => $<HTMLElement>(id).closest<HTMLElement>('details') ?? $<HTMLElement>(id);
|
||||
group('abut-panel-plan-bearings').hidden = member !== 'bearing';
|
||||
group('abut-panel-plan-shape').hidden = member !== 'shape';
|
||||
$<HTMLElement>('abut-panel-plan-wing').hidden = member !== 'wing';
|
||||
$<HTMLElement>('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 = $<HTMLDivElement>(slot.shape);
|
||||
const msg = $<HTMLParagraphElement>(slot.msg);
|
||||
basic.replaceChildren();
|
||||
// ② puts the wing forms in their own card; ① keeps them in the basic items.
|
||||
const wingBox = 'wing' in slot ? $<HTMLDivElement>(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';
|
||||
|
||||
@@ -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; }
|
||||
@@ -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 });
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user