From a025a612091d7266cd03173df3dbd6da98a3ec06 Mon Sep 17 00:00:00 2001 From: ypshim Date: Wed, 4 Nov 2026 08:59:26 +0900 Subject: [PATCH] =?UTF-8?q?feat(plan):=20=EA=B5=90=EB=8C=80=20=EC=B9=B4?= =?UTF-8?q?=EB=93=9C=20=EC=84=A0=ED=83=9D=20=EC=A4=84=EC=9D=84=20=EC=8B=9C?= =?UTF-8?q?=EC=95=88=20=ED=83=80=EC=9D=BC=EB=A1=9C=20=EB=B0=94=EA=BF=88=20?= =?UTF-8?q?(=EC=95=84=EC=9D=B4=EC=BD=98,=20=EA=B8=B0=EB=B3=B8=20=EB=B0=B0?= =?UTF-8?q?=EC=A7=80)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 공통 타일 helper(optionTiles): 아이콘 타일(.opt)과 글자 타일(.optt), 선택은 파란 테두리, 기본값은 "기본" 배지, 고를 수 없는 것은 점선과 사유 툴팁, 아래 안내문 - 벽체 형식: 시안 아이콘 4개(기본형 · 브라켓형 · 오목형 · 반일체식), 기본형만 선택 (QA-8) - 같은 타일 적용: 블럭 배치(분리 · 일체 아이콘), 상세 보기(받침 ①–⑤), 확대기초 평면(skew · 직사각), 말뚝(원기둥 · 강관 · PHC) - 사용하지 않게 된 버튼 줄 스타일 삭제. 테스트 662건 통과, 빌드 통과. 브라우저 시각 확인은 미실시 Co-Authored-By: Claude Opus 5.5 --- src/main.ts | 163 +++++++++++++++++++++++++++++++++----------------- src/style.css | 13 +++- 2 files changed, 119 insertions(+), 57 deletions(-) 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; }