feat(plan): 날개벽 형식을 좌 · 우 1행으로 요약하고 "반대쪽에도 적용" 추가

- 선택 줄을 "날개벽 형식  좌 [일반형 · 단축형 · 없음] │ 우 [일반형 · 단축형 · 없음]" 한 줄로 합침 (작은 그림 타일). "좌우" · "편집할 측" 줄과 ① 쪽 / ⑤ 쪽 표시는 없앰 (위치는 모델창에서 확인)
- 타일이나 "좌" / "우" 글자를 누르면 그 측이 편집 대상이 되고, 편집 중인 묶음은 진한 테두리로 표시. 아래 치수 입력과 측면 삽도가 그 측을 따름
- "좌우 같게" 삭제: 형식과 치수는 측별로만 저장. 예전 파일의 wingsLinked 값은 읽기만 하고 쓰지 않음
- 반대쪽에도 적용 (copyWingDims): 두께 · 끝 높이 · 헌치를 한 번 복사하고, 길이는 두 측 형식이 같을 때만 복사. 반대쪽이 없음이면 비활성
- 테스트 갱신. 전체 670건 통과, 타입 검사 · 빌드 통과. 브라우저 시각 확인은 미실시

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ypshimandClaude Opus 5.5 committed 2026-11-04 09:36:53 +09:00
1 parent 4ef3650454
commit 886347471e
5 files changed
+101 -75

No files matched your search

+52 -43
View File
@@ -20,9 +20,8 @@ import {
SHAPE_EDIT_FIELDS,
WING_DIM_FIELDS,
restoreAbutDefault,
copyWingDims,
setWingForm,
setWingsLinked,
wingsLinked,
} from './core/abut/edit';
import { ABUT_SHAPE_DEFAULT, footingEnds, normalizeShape, pileRowsMm, resolveShape, wallThicknessMm, type AbutShape } from './core/abut/shape';
import { abutFootingPlanSvg } from './view/abut-footing-svg';
@@ -1003,7 +1002,7 @@ function dms(deg: number): string {
return `${d}°${String(m).padStart(2, '0')}′${String(s).padStart(2, '0')}″`;
}
/** ② 날개벽 카드 (시안, 2026-11-04): 좌우 · 편집할 측 · 형식 → 삽도 3개 → 측별 치수 → 자동 값 → 생성되는 날개벽 → 메모. */
/** ② 날개벽 카드 (시안, 2026-11-04): 형식 1행(좌 · 우) → 삽도 3개 → 편집 중인 측의 치수 · 반대쪽에도 적용 → 자동 값 → 생성되는 날개벽 → 메모. */
function fillAbutWingCard(
box: HTMLDivElement,
rec: AbutRecord,
@@ -1014,47 +1013,39 @@ function fillAbutWingCard(
): void {
const NAME: Record<WingForm, string> = { general: '일반형', short: '단축형', none: '없음' };
const forms = rec.wings ?? { left: 'general' as WingForm, right: 'general' as WingForm };
const linked = wingsLinked(rec);
const side = state.abutWingSide;
const lastNo = rec.bearings.length;
const circled = (n: number) => ['①', '②', '③', '④', '⑤', '⑥', '⑦', '⑧', '⑨', '⑩'][n - 1] ?? String(n);
const sideName = (s: 'left' | 'right') => (s === 'left' ? '좌 · ① 쪽' : `우 · ${circled(lastNo)} 쪽`);
box.append(
optionTiles('좌우', [
// 같게: the left form on both sides with "="; 따로: the current forms, or long / short when they happen to match.
{ label: '좌우 같게', icon: WING_ICON.plan(forms.left, forms.left, undefined, true), on: linked, isDefault: true, pick: () => apply(setWingsLinked(rec, true)) },
{
label: '따로 정하기',
icon: forms.left === forms.right ? WING_ICON.plan('general', 'short') : WING_ICON.plan(forms.left, forms.right),
on: !linked,
pick: () => apply(setWingsLinked(rec, false)),
},
]),
optionTiles(
'편집할 측',
(['left', 'right'] as const).map((s) => ({
// The picture shows both wings in their current form, with this side drawn bold.
label: sideName(s),
icon: WING_ICON.plan(forms.left, forms.right, s),
on: side === s,
pick: () => {
state.abutWingSide = s;
renderAll();
},
})),
linked ? '좌우 같게: 형식과 치수가 양쪽에 함께 적용됩니다' : undefined,
),
optionTiles(
`${side === 'left' ? '좌' : '우'} 날개벽 (${sideName(side).split(' · ')[1]}) 형식`,
const sideKo = (s: 'left' | 'right') => (s === 'left' ? '좌' : '우');
// 날개벽 형식 1행 (2026-11-04): 좌 · 우 묶음을 나란히. 타일을 누르면 그 측의 형식이 바뀌고 그 측이 편집 대상이 된다.
const formsRow = el('div', undefined, 'abut-wing-forms');
formsRow.append(el('span', '날개벽 형식', 'abut-grp-title'));
for (const s of ['left', 'right'] as const) {
const group = optionTiles(
sideKo(s),
(['general', 'short', 'none'] as const).map((f) => ({
label: NAME[f],
icon: WING_ICON.side(f),
on: forms[side] === f,
on: forms[s] === f,
isDefault: f === 'general',
pick: () => apply(setWingForm(rec, side, f)),
pick: () => {
state.abutWingSide = s;
if (forms[s] === f) renderAll();
else apply(setWingForm(rec, s, f));
},
})),
),
);
);
group.classList.add('compact');
if (s === side) group.classList.add('editing');
const title = group.querySelector<HTMLElement>('.abut-grp-title');
if (title) {
title.title = `${sideKo(s)} 날개벽 치수 편집`;
title.addEventListener('click', () => {
state.abutWingSide = s;
renderAll();
});
}
formsRow.append(group);
}
box.append(formsRow);
let model: AbutModel;
let members: AbutMembers;
let wings: WingSolid[];
@@ -1093,7 +1084,7 @@ function fillAbutWingCard(
if (wing) {
addFig(
abutWingSideSvg({
name: `${side === 'left' ? '좌' : '우'} 날개벽`,
name: `${sideKo(side)} 날개벽`,
form: wing.form,
lengthM: wing.lengthM,
endHeightM: wing.endHeightM,
@@ -1104,7 +1095,7 @@ function fillAbutWingCard(
seatEl: rec.seatEl,
footingTopEl: members.footingTopEl,
}),
`${side === 'left' ? '좌' : '우'} 날개벽 측면 · ${NAME[wing.form]} (날개벽 축 방향으로 잘라 본 모습) · 축척 같음`,
`${sideKo(side)} 날개벽 측면 · ${NAME[wing.form]} (날개벽 축 방향으로 잘라 본 모습) · 축척 같음`,
);
}
addFig(
@@ -1122,7 +1113,7 @@ function fillAbutWingCard(
box.append(figs);
// 측별 치수 (선택한 측의 형식에 맞는 칸).
const form = forms[side];
box.append(el('p', `${side === 'left' ? '좌' : '우'} 날개벽 (${sideName(side).split(' · ')[1]}) · ${NAME[form]}`, 'lbl'));
box.append(el('p', `${sideKo(side)} 날개벽 · ${NAME[form]} (편집 중)`, 'lbl'));
if (form === 'none' || !wing) {
box.append(el('p', '입력 없음 · 접속슬래브 측면은 교좌면 끝선에 맞춥니다.', 'muted small'));
} else {
@@ -1141,8 +1132,26 @@ function fillAbutWingCard(
fail(err, input, String(dims[f.key]));
}
});
box.append(lab, input, el('span', `기본 ${defaults[f.key]}${linked ? ' · 양쪽' : ''}`, 'muted small'));
box.append(lab, input, el('span', `기본 ${defaults[f.key]}`, 'muted small'));
}
// 반대쪽에도 적용: one copy now (두께 · 끝 높이 · 헌치, and the 길이 only when both forms match).
const other = side === 'left' ? 'right' : 'left';
const copy = el('button', `반대쪽(${sideKo(other)})에도 적용`, 'btn small');
copy.type = 'button';
copy.disabled = forms[other] === 'none';
copy.title = forms[other] === 'none'
? '반대쪽 날개벽이 없습니다'
: forms[side] === forms[other]
? '길이 · 두께 · 끝 높이 · 헌치를 한 번 복사합니다 (이후 연결되지 않음)'
: '형식이 달라 길이는 복사하지 않습니다. 두께 · 끝 높이 · 헌치만 한 번 복사합니다';
copy.addEventListener('click', () => {
try {
apply(copyWingDims(rec, side));
} catch (err) {
$<HTMLElement>('abut-panel-plan-msg').textContent = (err as Error).message;
}
});
box.append(el('span', ''), copy, el('span', forms[side] === forms[other] ? '길이 포함' : '형식이 달라 길이 제외', 'muted small'));
const auto = (name: string, value: string, note: string) => box.append(el('label', name), el('span', value, 'num'), el('span', note, 'muted small'));
const slope = model.wing?.slopePct ?? 0;
auto(`경사 (자동, ${rec.type} = ${rec.type === 'A1' ? '−i' : '+i'})`, `${slope >= 0 ? '+' : ''}${slope.toFixed(2)} %`, '종단경사에서');
@@ -1159,7 +1168,7 @@ function fillAbutWingCard(
box.append(
el(
'p',
'시안 메모 · 형식은 ②에서만 정합니다(엑셀 입력 없음). "좌우 같게"를 끄면 우측을 따로 정할 수 있습니다. 단축형은 하단이 모두 확대기초 상면이고 길이 기본값은 뒷굽 끝이며, 늘리면 뒷굽 밖 구간의 하단이 떠 있게 됩니다(정보 AI02). 두께는 접속슬래브 폭에, 헌치는 연직 헌치와 저판 상면 헌치에 함께 반영됩니다. 모서리 상세의 슬래브 모따기는 3D에는 아직 그리지 않습니다.',
'시안 메모 · 형식은 ②에서만 정합니다(엑셀 입력 없음). 좌 · 우 형식과 치수는 따로 정하고, "반대쪽에도 적용"으로 치수를 한 번 복사할 수 있습니다(길이는 형식이 같을 때만). 단축형은 하단이 모두 확대기초 상면이고 길이 기본값은 뒷굽 끝이며, 늘리면 뒷굽 밖 구간의 하단이 떠 있게 됩니다(정보 AI02). 두께는 접속슬래브 폭에, 헌치는 연직 헌치와 저판 상면 헌치에 함께 반영됩니다. 모서리 상세의 슬래브 모따기는 3D에는 아직 그리지 않습니다.',
'muted small abut-memo',
),
);