feat(plan): 교대 카드 선택 줄을 시안 타일로 바꿈 (아이콘, 기본 배지)

- 공통 타일 helper(optionTiles): 아이콘 타일(.opt)과 글자 타일(.optt), 선택은 파란 테두리, 기본값은 "기본" 배지, 고를 수 없는 것은 점선과 사유 툴팁, 아래 안내문
- 벽체 형식: 시안 아이콘 4개(기본형 · 브라켓형 · 오목형 · 반일체식), 기본형만 선택 (QA-8)
- 같은 타일 적용: 블럭 배치(분리 · 일체 아이콘), 상세 보기(받침 ①–⑤), 확대기초 평면(skew · 직사각), 말뚝(원기둥 · 강관 · PHC)
- 사용하지 않게 된 버튼 줄 스타일 삭제. 테스트 662건 통과, 빌드 통과. 브라우저 시각 확인은 미실시

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ypshimandClaude Opus 5.5 committed 2026-11-04 08:59:26 +09:00
1 parent 4eb92d381d
commit a025a61209
2 files changed
+119 -57

No files matched your search

+108 -55
View File
@@ -764,6 +764,62 @@ function renderAbutMembers(rec: AbutRecord): void {
}
}
/** One option of a 시안 tile row: the label, an optional icon (SVG inner markup), on/off, default badge, and a reason when disabled. */
interface TileOption {
label: string;
icon?: string;
on: boolean;
isDefault?: boolean;
disabled?: string;
pick?: () => void;
}
/** 시안 선택 줄 (.grp · .opts · .opt / .optt): icon tiles when the options have icons, text tiles otherwise, and a note below. */
function optionTiles(title: string, options: TileOption[], note?: string): HTMLDivElement {
const group = el('div', undefined, 'abut-grp');
group.setAttribute('role', 'group');
group.setAttribute('aria-label', title);
group.append(el('span', title, 'abut-grp-title'));
const row = el('div', undefined, 'abut-opts');
for (const o of options) {
const b = el('button', undefined, `${o.icon ? 'abut-opt' : 'abut-optt'}${o.on ? ' on' : ''}`);
b.type = 'button';
b.setAttribute('aria-pressed', String(o.on));
if (o.isDefault) b.append(el('span', '기본', 'abut-def'));
if (o.icon) {
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('width', '44');
svg.setAttribute('height', '24');
svg.setAttribute('viewBox', '0 0 48 24');
svg.setAttribute('aria-hidden', 'true');
svg.innerHTML = o.icon;
b.append(svg);
}
b.append(document.createTextNode(o.label));
if (o.disabled) {
b.disabled = true;
b.title = o.disabled;
} else if (o.pick) {
b.addEventListener('click', o.pick);
}
row.append(b);
}
group.append(row);
if (note) group.append(el('span', note, 'abut-tbdn'));
return group;
}
/** 시안 아이콘 (선 하나의 단면·평면 모식도): 벽체 형식 4개, 블럭 배치 2개. */
const ICON = {
wallBasic: '<path d="M12 22V6h3v4h2V2h4v12h12v8z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>',
wallBracket: '<path d="M12 22V9l4-3V2h4v12h13v8z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>',
wallConcave: '<path d="M12 22V8l5-2V2h4v6l-2 3v3h14v8z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>',
wallSemi: '<path d="M13 22V10h2v-2h2v6h16v8z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>',
blockSeparate:
'<rect x="3" y="7" width="8" height="10" fill="none" stroke="currentColor" stroke-width="1.6"/><rect x="15" y="7" width="8" height="10" fill="none" stroke="currentColor" stroke-width="1.6"/><rect x="27" y="7" width="8" height="10" fill="none" stroke="currentColor" stroke-width="1.6"/><rect x="39" y="7" width="6" height="10" fill="none" stroke="currentColor" stroke-width="1.6"/>',
blockOne: '<rect x="3" y="7" width="42" height="10" fill="none" stroke="currentColor" stroke-width="1.6"/>',
};
/** ② 교량받침·받침블럭·솔플레이트 카드 (시안): 블럭 배치, ELEVATION TABLE, 솔플레이트 TABLE, 공통 · 규칙 입력, 검사 줄. */
function fillAbutBearingCard(
box: HTMLDivElement,
@@ -787,17 +843,16 @@ function fillAbutBearingCard(
return tbl;
};
// 블럭 배치 (시안): 분리 고정, 일체는 규칙 미정이라 고를 수 없다.
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);
box.append(
optionTiles(
'블럭 배치',
[
{ label: '분리', icon: ICON.blockSeparate, on: true, isDefault: true },
{ label: '일체', icon: ICON.blockOne, on: false, disabled: '교대 일체 블럭은 규칙 미정 (교좌면이 계단이라 분리 고정)' },
],
'교대 일체 블럭은 규칙 미정 (교좌면이 계단이라 분리 고정)',
),
);
let model: AbutModel;
let members: AbutMembers;
try {
@@ -814,19 +869,19 @@ function fillAbutBearingCard(
const sol = abutSolplateRows(model, rule, H);
// 상세 보기 (받침 ①–⑤): 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);
box.append(
optionTiles(
'상세 보기 (받침)',
elev.map((r) => ({
label: r.seg,
on: r.no === pickNo,
pick: () => {
state.abutBearingNo = r.no;
renderAll();
},
})),
),
);
const er = elev.find((r) => r.no === pickNo)!;
const sr = sol.find((r) => r.no === pickNo)!;
const detail: AbutBearingDetail = {
@@ -1125,20 +1180,19 @@ function fillAbutSlot(slot: (typeof ABUT_SLOTS)[keyof typeof ABUT_SLOTS], rec: A
} catch {
// A model that does not build shows no 삽도 or 구간 표 (the errors are reported on import).
}
const kinds = el('div', undefined, 'abut-bearing-pick');
kinds.append(el('span', '벽체 형식', 'muted small'));
for (const [name, on] of [['기본형', true], ['브라켓형', false], ['오목형', false], ['반일체식', false]] as const) {
const b = el('button', name, 'btn small');
b.type = 'button';
b.setAttribute('aria-pressed', String(on));
if (!on) {
b.disabled = true;
b.title = '규칙 미정 (QA-8)';
}
kinds.append(b);
}
kinds.append(el('span', '브라켓형 · 오목형 · 반일체식은 규칙 미정 (QA-8)', 'muted small'));
shape.append(kinds);
const qa8 = '규칙 미정 (QA-8)';
shape.append(
optionTiles(
'벽체 형식',
[
{ label: '기본형', icon: ICON.wallBasic, on: true, isDefault: true },
{ label: '브라켓형', icon: ICON.wallBracket, on: false, disabled: qa8 },
{ label: '오목형', icon: ICON.wallConcave, on: false, disabled: qa8 },
{ label: '반일체식', icon: ICON.wallSemi, on: false, disabled: qa8 },
],
'브라켓형 · 오목형 · 반일체식은 규칙 미정이라 선택할 수 없습니다',
),
);
if (built) {
const v = { ...ABUT_SHAPE_DEFAULT, ...normalizeShape(rec.shape) };
const s = built.members.shape;
@@ -1177,24 +1231,23 @@ function fillAbutSlot(slot: (typeof ABUT_SLOTS)[keyof typeof ABUT_SLOTS], rec: A
// ② 확대기초 · 말뚝 카드 (시안, 2026-11-04): 평면 · 말뚝 선택 → 평면 삽도 → 입력 격자 → 자동 값 → 열 위치 → 열별 본수 → 메모.
const shapeAll = { ...ABUT_SHAPE_DEFAULT, ...normalizeShape(rec.shape) };
if (planCard) {
const pick = (title: string, options: [string, boolean, string?][]) => {
const row = el('div', undefined, 'abut-bearing-pick');
row.append(el('span', title, 'muted small'));
for (const [name, on, why] of options) {
const b = el('button', name, 'btn small');
b.type = 'button';
b.setAttribute('aria-pressed', String(on));
if (why) {
b.disabled = true;
b.title = why;
}
row.append(b);
}
return row;
};
footingBox.append(
pick('평면', [['skew 평행사변형 (기본)', true], ['직사각', false, '다음 단계 (길이 = 교좌면 길이 + 폭 × |cot skew|, 100 올림)']]),
pick('말뚝', [[`원기둥 D${shapeAll.pileDiameterMm}`, true], ['강관 · PHC', false, '규칙 미정']]),
optionTiles(
'평면',
[
{ label: 'skew 평행사변형', on: true, isDefault: true },
{ label: '직사각', on: false, disabled: '다음 단계 (길이 = 교좌면 길이 + 폭 × |cot skew|, 100 올림)' },
],
'직사각 평면은 다음 단계입니다',
),
optionTiles(
'말뚝',
[
{ label: `원기둥 D${shapeAll.pileDiameterMm}`, on: true, isDefault: true },
{ label: '강관 · PHC', on: false, disabled: '규칙 미정' },
],
'강관 · PHC는 규칙 미정입니다 (QA-5)',
),
);
if (built) {
const rows = pileRowsMm(shapeAll);
+11 -2
View File
@@ -903,8 +903,6 @@ body { background: #eef1f4; font-family: 'Noto Sans KR', system-ui, sans-serif;
#abut-panel-plan-bearings > .abut-seg th { color: #55606e; font-weight: 500; font-family: inherit; }
/* ② 교대 카드 under the 3D view (2026-11-03): pickers, the three 받침 삽도 side by side, full-width rows. */
#abut-panel-plan .abut-bearing-pick { grid-column: 1 / -1; display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
#abut-panel-plan .abut-bearing-pick .btn[aria-pressed='true'] { background: #1f5fa8; border-color: #1f5fa8; color: #ffffff; }
#abut-panel-plan .abut-bearing-figs { grid-column: 1 / -1; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; }
#abut-panel-plan .abut-fig { margin: 0; background: #f6f8fa; border: 1px solid #d5dbe2; border-radius: 6px; padding: 6px; }
#abut-panel-plan .abut-fig svg { display: block; width: 100%; height: auto; }
@@ -925,3 +923,14 @@ body { background: #eef1f4; font-family: 'Noto Sans KR', system-ui, sans-serif;
#abut-panel-plan-footing > .abut-seg th, #abut-panel-plan-footing > .abut-seg td { border-bottom: 1px solid #e6eaef; padding: 4px 6px; text-align: left; font-family: 'JetBrains Mono', ui-monospace, monospace; }
#abut-panel-plan-footing > .abut-seg th { color: #55606e; font-weight: 500; font-family: inherit; }
#abut-panel-plan .abut-footing-fig { max-width: 760px; }
/* 시안 선택 줄 (.grp · .opts · .opt / .optt, 2026-11-04): icon tiles and text tiles, the 기본 badge, dashed when not selectable. */
#abut-panel-plan .abut-grp { grid-column: 1 / -1; display: flex; flex-direction: column; gap: 6px; margin: 4px 0; }
.abut-grp-title { font-size: 12px; color: #55606e; }
.abut-opts { display: flex; flex-wrap: wrap; gap: 8px; }
.abut-opt { position: relative; font: inherit; font-size: 12px; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px; width: 84px; min-height: 56px; padding: 6px 4px; border: 1px solid #c3cbd4; border-radius: 6px; background: #ffffff; color: #18212b; cursor: pointer; }
.abut-optt { position: relative; font: inherit; font-size: 13px; min-width: 64px; min-height: 44px; padding: 0 12px; border: 1px solid #c3cbd4; border-radius: 6px; background: #ffffff; color: #18212b; cursor: pointer; }
.abut-opt.on, .abut-optt.on { border: 2px solid #1f5fa8; background: #f2f7fc; color: #123e70; font-weight: 700; }
.abut-opt:disabled, .abut-optt:disabled { cursor: not-allowed; border-style: dashed; color: #5f6a76; background: #f6f8fa; }
.abut-def { position: absolute; top: -9px; right: 4px; font-size: 11px; font-weight: 600; line-height: 1; padding: 3px 6px; border-radius: 8px; background: #4a5868; color: #ffffff; }
.abut-tbdn { display: block; font-size: 12px; color: #55606e; }