feat(bearing): 방위각 입력 · 계산 비교와 계산값 적용 버튼

교각·교대 솔플레이트 표에 받침별 '방위각 입력 (CAD)' · '방위각 계산 (CAD)' · '차이 (″, 계산 − 입력)' ·
[적용] 열을 두고, 표 위에 '계산 방위각 모두 적용 / 모두 입력값으로' 버튼을 둔다 (사용자 요청 2026-11-06,
Q-SH4 "입력값 유지"를 사용자가 고르게 바꿈).
- 계산 방위각: 거더 짝이면 두 받침을 잇는 방향, 교대만의 현 짝이면 원곡선 접선(현 방향 ± 방위각 변화의 절반)
- 적용값은 교각 input.azimuthApplied / 교대 record.azimuthApplied에 받침번호별로 저장, 엑셀 값은 그대로.
  블럭 · 받침 · 솔플레이트 회전과 오르막 방향에 쓰고, 짝 찾기는 엑셀 입력값으로
- 차이 표시: 거더 짝 ≤ 10″ / WS01, 현 짝은 '현 · 참고'. 도·분·초 CAD 각(엑셀과 같은 표기)
- 테스트: P1 전열 계산값 ≤ 0.5″ · 후열 없음, 적용한 받침만 블럭 회전 변화, 원곡선 현 접선 = 입력, 교대 toAbutInput 반영
- 브라우저: 운곡교AA.xlsx A1-광주 ① 적용 → 블럭 회전 4.39° → 4.36°, 모두 적용 5개 · 모두 해제, 예외 0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ypshimandClaude Opus 5.5 committed 2026-11-06 15:55:24 +09:00
1 parent 7c5f129712
commit c7acfccd51
9 files changed
+161 -22

No files matched your search

+80 -15
View File
@@ -967,7 +967,7 @@ function fillAbutBearingCard(
const m3 = (v: number) => v.toFixed(3);
const H = rec.bearingHeightMm;
// 솔플레이트 S: 짝 받침(P1 전열 등) 계산, 없으면 교대 종단경사 i (2026-11-06).
const ctx = { pairs: state.bridgePairs, slopeI: rec.slopeI };
const ctx = { pairs: state.bridgePairs, slopeI: rec.slopeI, inputAzimuthDeg: Object.fromEntries(rec.bearings.filter((b) => b.azimuthDeg !== undefined).map((b) => [String(b.no), b.azimuthDeg!])) };
const elev = abutElevationRows(model, rule, H, ctx);
const sol = abutSolplateRows(model, rule, H, ctx);
// 상세 보기 (받침 ①–⑤): the three 삽도 draw the picked bearing.
@@ -1016,12 +1016,23 @@ function fillAbutBearingCard(
),
);
box.append(el('p', '솔플레이트 상세 TABLE (mm) · S는 같은 거더 양끝 받침에서 계산', 'lbl'));
box.append(
table(
['No.', '짝 받침', '방위각 검사 (″)', 'S (%)', 'S 근거', 'L', 'W', 'T', 'T1 (오르막 쪽)', 'T2', '검산 T1 − T2 ≈ L·|S|/100', '검사'],
sol.map((r) => [String(r.no), r.pair ?? '없음', azimuthCheckText(r.azimuthSec, r.pairKind), r.S.toFixed(3), r.source, String(r.L), String(r.W), String(r.T), String(r.T1), String(r.T2), r.check, r.warnings.length ? r.warnings.join(' · ') : '—']),
),
);
const setApplied = (next: Record<string, number> | undefined) => apply({ ...rec, azimuthApplied: next && Object.keys(next).length ? next : undefined });
box.append(azimuthBulk(sol, rec.azimuthApplied, setApplied));
{
const tbl = table(['No.', '짝 받침', ...AZIMUTH_HEADERS, 'S (%)', 'S 근거', 'L', 'W', 'T', 'T1 (오르막 쪽)', 'T2', '검산 T1 − T2 ≈ L·|S|/100', '검사'], []);
const body = tbl.tBodies[0];
for (const r of sol) {
const key = String(r.no);
const tr = body.insertRow();
tr.append(el('td', String(r.no)), el('td', r.pair ?? '없음'), ...azimuthCells(r.azimuthInputDeg, r.azimuthCalcDeg, r.pairKind, rec.azimuthApplied?.[key], (v) => {
const next = { ...(rec.azimuthApplied ?? {}) };
if (v === undefined) delete next[key]; else next[key] = v;
setApplied(next);
}));
for (const c of [r.S.toFixed(3), r.source, String(r.L), String(r.W), String(r.T), String(r.T1), String(r.T2), r.check, r.warnings.length ? r.warnings.join(' · ') : '—']) tr.append(el('td', c));
}
box.append(tbl);
}
// 솔플레이트 입력 (결정 2026-11-06): 교각 카드와 같은 항목, 받침이 1열이라 T 하나.
box.append(el('p', `솔플레이트 입력 (이 교대) · T = max(T_min, ⌈T_min + L·|S|/2⌉) · 거더 하면 고정 → 블럭 상면 = z − (T − T_ref)`, 'lbl'));
const auto = sol[0];
@@ -5136,10 +5147,11 @@ function renderElevationTable(wrap: HTMLDivElement, pier: Pier): void {
function renderSolplateTable(wrap: HTMLDivElement, pier: Pier): void {
if (!pier.model) return;
wrap.appendChild(el('p', '솔플레이트 TABLE (모델값 · mm) · S는 같은 거더 양끝 받침에서 계산', 'lbl'));
wrap.appendChild(azimuthBulk(solplateRows(pier.model), pier.draft.azimuthApplied, (next) => setAzimuthApplied(pier.input.name, next ?? {})));
const table = el('table', undefined, 'grid-table rebar-table');
const head = el('thead');
const hr = el('tr');
for (const h of ['No', '짝 받침', '방위각 검사 (″)', 'S (%)', 'S 근거', 'A', 'B', 'L', 'W', 'T', 'T1 (오르막 쪽)', 'T2', '검산 T1 − T2 ≈ L·|S|/100', '검사']) hr.appendChild(el('th', h));
for (const h of ['No', '짝 받침', ...AZIMUTH_HEADERS, 'S (%)', 'S 근거', 'A', 'B', 'L', 'W', 'T', 'T1 (오르막 쪽)', 'T2', '검산 T1 − T2 ≈ L·|S|/100', '검사']) hr.appendChild(el('th', h));
head.appendChild(hr);
table.appendChild(head);
const body = el('tbody');
@@ -5155,7 +5167,11 @@ function renderSolplateTable(wrap: HTMLDivElement, pier: Pier): void {
for (const r of solplateRows(pier.model)) {
const key = String(r.no);
const tr = el('tr');
tr.append(el('td', String(r.no)), el('td', r.pair ?? '없음'), el('td', azimuthCheckText(r.azimuthSec, r.pairKind)));
tr.append(el('td', String(r.no)), el('td', r.pair ?? '없음'), ...azimuthCells(r.azimuthInputDeg, r.azimuthCalcDeg, r.pairKind, pier.draft.azimuthApplied?.[key], (v) => {
const next = { ...(pier.draft.azimuthApplied ?? {}) };
if (v === undefined) delete next[key]; else next[key] = v;
setAzimuthApplied(pier.input.name, next);
}));
tr.appendChild(cellInput(Math.round(r.S * 1000) / 1000, r.source === '수정됨', `받침 ${r.no} S (%)`, (v) => {
const next = { ...(settings.slopePct ?? {}) };
if (v === undefined) delete next[key]; else next[key] = v;
@@ -5180,12 +5196,61 @@ function renderSolplateTable(wrap: HTMLDivElement, pier: Pier): void {
wrap.appendChild(el('div', undefined, 'table-scroll')).appendChild(table);
}
/** 방위각 검사 칸 (Q-SH4): the difference to the pair direction in arc seconds, ≤ 10″ 통과 / > 10″ WS01; '—' without a pair. */
function azimuthCheckText(sec: number | undefined, kind?: 'girder' | 'chord'): string {
if (sec === undefined) return '— (짝 없음)';
// 교대만 A1 ↔ A2 현: chord against the mean azimuth, for reference (the 10″ rule is for one girder).
if (kind === 'chord') return `현 ${sec.toFixed(0)}″ (참고)`;
return `${sec.toFixed(1)}″ ${sec > AZIMUTH_CHECK_SEC ? 'WS01' : '≤ 10″'}`;
/** CAD 각 (엑셀과 같은 동 = 0° 반시계) as d°mm′ss.s″, from a model 진북 azimuth (deg). */
function cadDms(az: number): string {
const cad = (((90 - az) % 360) + 360) % 360;
let d = Math.floor(cad), m = Math.floor((cad - d) * 60), s = Math.round(((cad - d) * 60 - m) * 600) / 10;
if (s >= 60) { s -= 60; m += 1; }
if (m >= 60) { m -= 60; d += 1; }
return `${d}°${String(m).padStart(2, '0')}′${s.toFixed(1).padStart(4, '0')}″`;
}
const AZIMUTH_HEADERS = ['방위각 입력 (CAD)', '방위각 계산 (CAD)', '차이 (″, 계산 − 입력)', '적용'];
/**
* 방위각 입력 · 계산 · 차이 · 적용 칸 (2026-11-06). 계산 = 짝 받침 방향 (현 짝이면 원곡선 접선). The button applies the 계산
* 방위각 to the block / bearing / solplate rotation or goes back to the workbook value.
*/
function azimuthCells(input: number, calc: number | undefined, kind: 'girder' | 'chord' | undefined, applied: number | undefined, onSet: (value: number | undefined) => void): HTMLTableCellElement[] {
const cells = [el('td', cadDms(input))];
if (calc === undefined) return [...cells, el('td', '— (짝 없음)'), el('td', '—'), el('td', '—')];
// CAD runs the other way round from the 진북 azimuth, so the CAD difference is −(calc − input).
const sec = -(((calc - input + 540) % 360) - 180) * 3600;
const status = kind === 'chord' ? '현 · 참고' : Math.abs(sec) > AZIMUTH_CHECK_SEC ? 'WS01' : '≤ 10″';
cells.push(el('td', `${cadDms(calc)}${kind === 'chord' ? ' (현)' : ''}`), el('td', `${sec >= 0 ? '+' : ''}${sec.toFixed(1)}″ ${status}`));
const on = applied !== undefined;
const button = el('button', on ? '적용됨 · 입력값으로' : '계산값 적용', 'btn small');
button.type = 'button';
button.setAttribute('aria-pressed', String(on));
button.title = on ? `계산 방위각 ${cadDms(applied)} 사용 중 · 누르면 엑셀 입력값으로` : '블럭 · 받침 · 솔플레이트 회전에 계산 방위각을 씁니다';
button.addEventListener('click', () => onSet(on ? undefined : calc));
const td = el('td'); td.appendChild(button);
return [...cells, td];
}
/** '모두 적용 / 모두 입력값으로' buttons above a solplate table. */
function azimuthBulk(rows: { no: number; azimuthCalcDeg?: number }[], applied: Record<string, number> | undefined, onSet: (next: Record<string, number> | undefined) => void): HTMLDivElement {
const box = el('div', undefined, 'btn-row');
const all = el('button', '계산 방위각 모두 적용', 'btn small');
all.type = 'button';
all.disabled = !rows.some((r) => r.azimuthCalcDeg !== undefined);
all.addEventListener('click', () => onSet(Object.fromEntries(rows.filter((r) => r.azimuthCalcDeg !== undefined).map((r) => [String(r.no), r.azimuthCalcDeg!]))));
const none = el('button', '모두 입력값으로', 'btn small');
none.type = 'button';
none.disabled = !applied || Object.keys(applied).length === 0;
none.addEventListener('click', () => onSet(undefined));
box.append(all, none, el('span', `적용 ${applied ? Object.keys(applied).length : 0}개 · 짝 찾기는 엑셀 입력값으로 합니다`, 'muted small'));
return box;
}
/** Sets the bearings that use the 계산 방위각 (2026-11-06) and rebuilds that pier. */
function setAzimuthApplied(name: string, applied: Record<string, number>): void {
const p = state.piers.find((q) => q.input.name === name);
if (!p) return;
p.draft = { ...p.draft, azimuthApplied: Object.keys(applied).length ? applied : undefined };
state.piers = state.piers.map((q) => (q === p ? regenerateEntry(q) : q));
setupScene();
renderAll();
}
/** Sets one pier's solplate settings (2026-11-06) and rebuilds that pier. */