diff --git a/src/main.ts b/src/main.ts
index 6ebffdd..58c8dd3 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -19,6 +19,7 @@ import { parseDrawingRows } from './core/drawing-import';
import { parseBearingElevationTable, parseSolplateTable, readDxfTexts } from './core/dxf-table';
import { columnDiameterIssues } from './core/column-check';
import { findPileSpec, PHC_PILES, STEEL_PILES } from './core/pile-spec';
+import { footingPlanSvg, footingSideSvg } from './view/footing-svg';
import { P1_COPING_GEOM } from './core/coping-geom';
import { applyBearingRules, BEARING_ELEVATION_REF, BEARING_RULES_DEFAULT, type BearingBasis, type BearingRules, type DrawingBearing, DRAWING_TOL_MM, drawingCopingCheck, SOLPLATE_REF, SOLPLATE_T_RULE, solplateTs, withDrawingBasis } from './core/bearing-ref';
@@ -716,6 +717,21 @@ function footingLengthOf(p: Pier): number | undefined {
return p.model?.footings[0]?.length;
}
+/** 확대기초·말뚝 그림 (2단계, 승인 2026-10-23): plan and side views drawn from the model; θ is shown as a note. */
+function renderFootingDrawings(box: HTMLDivElement, pier: Pier): void {
+ if (!pier.model) return;
+ const wrap = el('div', undefined, 'footing-drawings');
+ const plan = el('figure', undefined, 'footing-fig');
+ plan.innerHTML = footingPlanSvg(pier.model);
+ plan.appendChild(el('figcaption', '평면 · 1열이 아래, 열은 교축과 직각 방향 줄', 'muted small'));
+ const side = el('figure', undefined, 'footing-fig');
+ side.innerHTML = footingSideSvg(pier.model);
+ side.appendChild(el('figcaption', '정면 · 점선 = 경사 선택 시 상면 (L2, h2)', 'muted small'));
+ wrap.append(plan, side);
+ wrap.appendChild(el('p', '기초 회전각 θ는 교각축 대비 회전으로, 생성 규칙은 spec에 추가 후 적용합니다 (지금은 0°).', 'muted small'));
+ box.appendChild(wrap);
+}
+
/** 말뚝 규격 (승인 2026-10-23): 강관 and PHC from their lists by outer diameter and wall; the inner diameter, area and weight are read. */
function renderPileSpec(box: HTMLDivElement, pier: Pier): void {
const kind = pier.draft.pileKind ?? '강관';
@@ -1645,6 +1661,7 @@ function renderPlan(): void {
if (member.id === 'footing') {
renderPileSpec(fields, pier);
renderPileRows(fields, pier);
+ renderFootingDrawings(fields, pier);
}
if (member.id === 'coping') renderCopingInfo(fields, pier);
if (member.id === 'bearing') renderBearingRef(fields, pier);
diff --git a/src/style.css b/src/style.css
index 516d0ce..cf82b55 100644
--- a/src/style.css
+++ b/src/style.css
@@ -794,3 +794,8 @@ body { background: #eef1f4; font-family: 'Noto Sans KR', system-ui, sans-serif;
.bearing-ref .table-scroll { overflow-x: auto; margin-bottom: 8px; }
.bearing-ref table { font-size: 12px; }
.bearing-ref input.changed-input { border: 2px solid #e8833a; background: #fff6ee; }
+
+/* ② 확대기초·말뚝 그림 (2단계) */
+.footing-drawings { display: grid; gap: 10px; margin-top: 12px; }
+.footing-fig { margin: 0; }
+.footing-fig svg { width: 100%; height: auto; max-height: 260px; background: #ffffff; border: 1px solid #d5dde6; border-radius: 6px; }
diff --git a/src/view/footing-svg.ts b/src/view/footing-svg.ts
new file mode 100644
index 0000000..b6ec64a
--- /dev/null
+++ b/src/view/footing-svg.ts
@@ -0,0 +1,65 @@
+/**
+ * 확대기초·말뚝 그림 (2단계, 승인 2026-10-23): plan and side views as SVG strings, drawn from the model (footings, piles,
+ * columns) and the input values. Pure: no DOM, so that it can be tested. Units: m in the model; the labels show mm.
+ */
+import type { PierModel } from '../core/geometry';
+
+const mm = (v: number) => Math.round(v * 1000);
+
+/** Plan view: the footing rectangle (length along the 교축, width across), the pile circles, the column circle, the axis. */
+export function footingPlanSvg(model: PierModel): string {
+ const f = model.footings[0];
+ if (!f) return '';
+ const pad = 0.8;
+ const x0 = f.s - f.length / 2 - pad;
+ const x1 = f.s + f.length / 2 + pad;
+ const y0 = -f.width / 2 - pad;
+ const y1 = f.width / 2 + pad;
+ const piles = model.piles.filter((p) => Math.abs(p.s - f.s) <= f.length / 2 + 1e-6);
+ const parts: string[] = [];
+ parts.push(``);
+ for (const p of piles) {
+ parts.push(``);
+ }
+ for (const c of model.columns) {
+ parts.push(``);
+ }
+ // Pier axis (dash-dot) through the footing centre.
+ parts.push(``);
+ // Dimension labels: length and width, in mm.
+ parts.push(`${mm(f.length)}`);
+ parts.push(`${mm(f.width)}`);
+ return ``;
+}
+
+/** Side view (elevation): the footing, the piles down to their tips, and the column above the footing (EL up). */
+export function footingSideSvg(model: PierModel): string {
+ const f = model.footings[0];
+ if (!f) return '';
+ const piles = model.piles.filter((p) => Math.abs(p.s - f.s) <= f.length / 2 + 1e-6);
+ const tipEl = piles.length ? Math.min(...piles.map((p) => p.bottom)) : f.bottom;
+ const topEl = model.levelBottom;
+ const pad = 0.8;
+ const x0 = f.s - f.length / 2 - pad;
+ const x1 = f.s + f.length / 2 + pad;
+ const y0 = -(topEl + pad);
+ const y1 = -(tipEl - pad);
+ const parts: string[] = [];
+ // Elevation: y = −EL so that the level grows upward on the screen.
+ parts.push(``);
+ for (const p of piles) {
+ parts.push(``);
+ }
+ // Column: from the footing top up to the Level underside.
+ const col = model.columns[0];
+ if (col) {
+ parts.push(``);
+ }
+ // Labels: the footing height and the pile length, in mm.
+ parts.push(`h1 ${mm(f.top - f.bottom)}`);
+ if (piles.length) {
+ const pileLen = piles[0].top - piles[0].bottom;
+ parts.push(`말뚝 ${mm(pileLen)}`);
+ }
+ return ``;
+}
diff --git a/tests/footing-svg.test.ts b/tests/footing-svg.test.ts
new file mode 100644
index 0000000..eb3721c
--- /dev/null
+++ b/tests/footing-svg.test.ts
@@ -0,0 +1,30 @@
+import { describe, expect, it } from 'vitest';
+import { buildPier } from '../src/core/geometry';
+import { footingPlanSvg, footingSideSvg } from '../src/view/footing-svg';
+import { loadP1 } from './helpers';
+
+const m = buildPier(loadP1());
+const count = (svg: string, tag: string) => (svg.match(new RegExp(`<${tag} `, 'g')) ?? []).length;
+
+describe('확대기초·말뚝 그림 (2단계, 승인 2026-10-23)', () => {
+ it('the plan draws one circle per pile under the footing, and labels the footing length and width in mm', () => {
+ const svg = footingPlanSvg(m);
+ const piles = m.piles.filter((p) => Math.abs(p.s - m.footings[0].s) <= m.footings[0].length / 2 + 1e-6).length;
+ // The column circles are dashed; the pile circles are the solid ones.
+ expect(count(svg, 'circle') - m.columns.length).toBe(piles);
+ expect(svg).toContain('10400');
+ expect(svg).toContain('8150');
+ });
+
+ it('the side view shows the footing rectangle and one line per pile, with the pile length label', () => {
+ const svg = footingSideSvg(m);
+ expect(count(svg, 'line')).toBeGreaterThanOrEqual(20);
+ expect(svg).toContain('h1 2000');
+ expect(svg).toContain('말뚝 24000');
+ });
+
+ it('the drawings follow the footing inputs (width 9000 mm shows in the plan label)', () => {
+ const wide = buildPier({ ...loadP1(), footingWidthMm: 9000 });
+ expect(footingPlanSvg(wide)).toContain('9000');
+ });
+});