feat(plan): ② 교대 카드를 3D 창 아래로 옮기고 교량받침 삽도 3개 추가

- 교대 카드를 왼쪽 목록 아래에서 가운데 3D 창 아래(교각 부재 카드 자리)로 옮김. 부재 · 교대 목록에서 고른 부재의 내용을 보여 주고, 머리줄에 부재 이름과 요약을 표시
- 왼쪽 편집 영역 정리: ②의 편집 버튼과 접히는 편집 영역을 없앰. 왼쪽에는 구조물 목록과 부재 · 교대 목록만 남김
- 교량받침 카드: 블럭 배치(분리 고정, 일체 비활성), 상세 보기(받침 ①–⑤), 삽도 3개(평면 · 단면 · 솔플레이트 상세, src/view/abut-bearing-svg.ts), ELEVATION TABLE, 솔플레이트 TABLE, 공통 · 규칙 입력, 검사 줄
- 삽도 테스트 3건 추가. 전체 650건 통과, 타입 검사·빌드 통과. 브라우저 시각 확인은 미실시

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ypshimandClaude Opus 5.5 committed 2026-11-03 18:28:29 +09:00
1 parent 4e3950eb67
commit 4b02dde153
5 files changed
+231 -32

No files matched your search

+75 -18
View File
@@ -11,6 +11,7 @@ import { ABUT_DETAIL_LABELS, ABUT_SEGMENT_HEADER, abutInputRows, abutSegmentRows
import { applyAbutEdit, applyBearingEdit, applyBearingRule, applyShapeEdit, ABUT_EDIT_FIELDS, BEARING_RULE_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 { abutBearingPlanSvg, abutBearingSectionSvg, abutSolplateSvg, type AbutBearingDetail } from './view/abut-bearing-svg';
import {
ABUT_BEARING_RULE_DEFAULT,
ABUT_BLOCK_MM,
@@ -145,6 +146,8 @@ const state = {
abutActive: null as string | null,
/** The 부재 · 교대 row whose card is open in ② (2026-11-03). */
abutMember: 'bearing' as AbutMemberId,
/** The bearing the ② 교량받침 삽도 draw (상세 보기). */
abutBearingNo: 1,
/** The editable part of the 교대 panel is open (2026-11-03). */
abutEditOpen: false,
/** 교대 기본값 (가져온 그대로), for 기본값 복원. */
@@ -713,7 +716,8 @@ const ABUT_QA_NOTES = [
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: { wing: 'abut-panel-plan-wing', root: 'abut-panel-plan',name: 'abut-panel-plan-name', toggle: 'abut-panel-plan-edit-toggle', editor: 'abut-panel-plan-editor', basic: 'abut-panel-plan-basic', bearings: 'abut-panel-plan-bearings', shape: 'abut-panel-plan-shape', msg: 'abut-panel-plan-msg', prefix: 'abp' },
// ② has no 편집 toggle and no basic items: the card sits under the 3D view and shows the picked member (2026-11-03).
plan: { wing: 'abut-panel-plan-wing', root: 'abut-panel-plan', name: 'abut-panel-plan-name', 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). */
@@ -779,7 +783,18 @@ function fillAbutBearingCard(
}
return tbl;
};
box.append(el('p', '블럭 배치: 분리 (교대 일체 블럭은 규칙 미정 — 교좌면이 계단이라 분리 고정)', 'muted small'));
// 블럭 배치 (시안): 분리 고정, 일체는 규칙 미정이라 고를 수 없다.
const layout = el('div', undefined, 'abut-bearing-pick');
layout.append(el('span', '블럭 배치', 'muted small'));
const sep = el('button', '분리', 'btn small');
sep.type = 'button';
sep.setAttribute('aria-pressed', 'true');
const one = el('button', '일체', 'btn small');
one.type = 'button';
one.disabled = true;
one.title = '교대 일체 블럭은 규칙 미정 (교좌면이 계단이라 분리 고정)';
layout.append(sep, one, el('span', '교대 일체 블럭은 규칙 미정 (교좌면이 계단이라 분리 고정)', 'muted small'));
box.append(layout);
let model: AbutModel;
let members: AbutMembers;
try {
@@ -790,11 +805,46 @@ function fillAbutBearingCard(
return;
}
const m3 = (v: number) => v.toFixed(3);
const elev = abutElevationRows(model, rule);
const sol = abutSolplateRows(model, rule, rec.slopeI);
// 상세 보기 (받침 ①–⑤): the three 삽도 draw the picked bearing.
const pickNo = elev.some((r) => r.no === state.abutBearingNo) ? state.abutBearingNo : elev[0].no;
const picker = el('div', undefined, 'abut-bearing-pick');
picker.append(el('span', '상세 보기 (받침)', 'muted small'));
for (const r of elev) {
const b = el('button', r.seg, 'btn small');
b.type = 'button';
b.setAttribute('aria-pressed', String(r.no === pickNo));
b.addEventListener('click', () => {
state.abutBearingNo = r.no;
renderAll();
});
picker.append(b);
}
box.append(picker);
const er = elev.find((r) => r.no === pickNo)!;
const sr = sol.find((r) => r.no === pickNo)!;
const detail: AbutBearingDetail = {
no: er.no, blockMm: ABUT_BLOCK_MM, padMm: ABUT_PAD_PLAN_MM, A: er.A, B: er.B, L: sr.L, W: sr.W,
h1: er.h1, h2: er.h2, h3: er.h3, h4: er.h4, T1: sr.T1, T2: sr.T2, rotDeg: er.rotDeg, S: sr.S, seatEl: er.seatEl, z: er.z,
};
const figs = el('div', undefined, 'abut-bearing-figs');
for (const [svg, caption] of [
[abutBearingPlanSvg(detail), '평면 · 점선 = 몰탈패드, 주황 점선 = 솔플레이트'],
[abutBearingSectionSvg(detail), '단면 · 교축 방향 · E = h1 + h2 + h3 + h4'],
[abutSolplateSvg(detail), '솔플레이트 상세 · 가정'],
]) {
const fig = el('figure', undefined, 'abut-fig');
fig.innerHTML = svg;
fig.append(el('figcaption', caption, 'muted small'));
figs.append(fig);
}
box.append(figs);
box.append(el('p', '받침 ELEVATION TABLE (mm, EL·위치는 m) · 거리는 IP3(① 쪽)에서 · h3·h4는 가정 (QA-15)', 'lbl'));
box.append(
table(
['No.', '위치 s', '구간', '교좌면 EL', 'h1 블럭', 'h2 패드', 'z = EL.X', 'h3 받침', 'h4 솔플', 'E', '솔플 상면 EL', 'A', 'B', '블럭 ~ 계단 경계', '방위각 CAD / 진북 (°)', '블럭 회전 (°)'],
abutElevationRows(model, rule).map((r) => [
elev.map((r) => [
String(r.no), m3(r.sFromIp3M), r.seg, m3(r.seatEl), String(r.h1), String(r.h2), m3(r.z), String(r.h3), String(r.h4),
String(r.E), m3(r.solplateTopEl), String(r.A), String(r.B), r.stepGapM === null ? '—' : m3(r.stepGapM),
r.azimuthCadDeg === null || r.azimuthNorthDeg === null ? '입력 없음' : `${r.azimuthCadDeg.toFixed(3)} / ${r.azimuthNorthDeg.toFixed(3)}`,
@@ -806,7 +856,7 @@ function fillAbutBearingCard(
box.append(
table(
['No.', 'S (%)', 'L', 'W', 'T', 'T1', 'T2', '검산 T1 − T2 ≈ L·|S|'],
abutSolplateRows(model, rule, rec.slopeI).map((r) => [String(r.no), r.S.toFixed(3), String(r.L), String(r.W), String(r.T), String(r.T1), String(r.T2), r.check]),
sol.map((r) => [String(r.no), r.S.toFixed(3), String(r.L), String(r.W), String(r.T), String(r.T1), String(r.T2), r.check]),
),
);
box.append(el('p', `공통 · 규칙 — 블럭 평면 ${ABUT_BLOCK_MM} × ${ABUT_BLOCK_MM} (기본값) · 몰탈패드 ${ABUT_PAD_PLAN_MM} × ${ABUT_PAD_PLAN_MM} × 50 (기본값) · 블럭 높이 = min(z) − 교좌면 (계산)`, 'muted small'));
@@ -846,11 +896,15 @@ function fillAbutBearingCard(
/** 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-bearings').hidden = member !== 'bearing';
$<HTMLElement>('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단계에서 추가합니다. 현재 값만 보여 줍니다.`;
// The card title is the picked row; its 요약 follows the name (시안 머리줄).
const slabWidth = abutSummaryRows(rec).find(([label]) => label === '접속슬래브 폭')?.[1] ?? '—';
const row = abutMemberRows(rec, slabWidth).find((r) => r.id === member);
$<HTMLElement>('abut-panel-plan-member').textContent = row?.label ?? '';
$<HTMLElement>('abut-panel-plan-name').textContent = `${rec.name} · ${row?.summary ?? ''}`;
$<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. */
@@ -865,16 +919,19 @@ function fillAbutSlot(slot: (typeof ABUT_SLOTS)[keyof typeof ABUT_SLOTS], rec: A
if ('input' in slot) fillList($<HTMLDListElement>(slot.input), abutInputRows(rec));
// ② has no 상세 (계산값) block (2026-11-03); ① keeps it.
if ('summary' in slot) fillList($<HTMLDListElement>(slot.summary), abutSummaryRows(rec).filter(([label]) => ABUT_DETAIL_LABELS.includes(label)));
// 편집 opens the editable fields; they stay closed until asked for (2026-11-03).
const toggle = $<HTMLButtonElement>(slot.toggle);
toggle.textContent = state.abutEditOpen ? '닫기' : '편집';
toggle.setAttribute('aria-expanded', String(state.abutEditOpen));
toggle.onclick = () => {
state.abutEditOpen = !state.abutEditOpen;
renderAll();
};
$<HTMLElement>(slot.editor).hidden = !state.abutEditOpen;
const basic = $<HTMLDivElement>(slot.basic);
// 편집 opens the editable fields in ①; they stay closed until asked for (2026-11-03). ② has no toggle.
if ('toggle' in slot) {
const toggle = $<HTMLButtonElement>(slot.toggle);
toggle.textContent = state.abutEditOpen ? '닫기' : '편집';
toggle.setAttribute('aria-expanded', String(state.abutEditOpen));
toggle.onclick = () => {
state.abutEditOpen = !state.abutEditOpen;
renderAll();
};
$<HTMLElement>(slot.editor).hidden = !state.abutEditOpen;
}
// ② has no basic items; a detached box keeps the shared code simple.
const basic = 'basic' in slot ? $<HTMLDivElement>(slot.basic) : el('div');
const bearings = $<HTMLDivElement>(slot.bearings);
const shape = $<HTMLDivElement>(slot.shape);
const msg = $<HTMLParagraphElement>(slot.msg);