diff --git a/src/main.ts b/src/main.ts
index d442c25..906bf69 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -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: '',
+ wallBracket: '',
+ wallConcave: '',
+ wallSemi: '',
+ blockSeparate:
+ '',
+ blockOne: '',
+};
+
/** ② 교량받침·받침블럭·솔플레이트 카드 (시안): 블럭 배치, 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);
diff --git a/src/style.css b/src/style.css
index 04f7096..e9dc80a 100644
--- a/src/style.css
+++ b/src/style.css
@@ -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; }