feat(abut): 접속슬래브 경사는 형식이 정함, 단면 · 접합 상세를 파단선 단면 하나로 합침, 흉벽 높이 입력

- 경사 선택 삭제: 매립형 = 하향 4 %, 노출형 = 종단경사 따라 (노트 표기). 예전 파일의 slope 값은 읽고 무시
- 단면 + 접합 상세: 흉벽 쪽과 먼 끝만 그리고 중간 생략, 부속재 · 치수(7000 · 450 · 850 · 295 · 헌치) · EL을 한 그림에
- 흉벽 높이 (브라켓 상면 ~ 흉벽 상단) 입력 = 벽체·흉벽 카드 브라켓 높이와 같은 값
- 자동 값: 브라켓 얹힘 길이 = 브라켓 폭 − 흉벽과 간격 (295)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
ypshimandClaude Haiku 5.5 committed 2026-11-04 10:27:20 +09:00
1 parent 4b8b80ae97
commit 9835fb42b3
7 files changed
+157 -199

No files matched your search

+44 -50
View File
@@ -24,11 +24,10 @@ import {
setWingForm,
applySlabEdit,
setSlabForm,
setSlabSlope,
SLAB_EDIT_FIELDS,
} from './core/abut/edit';
import { ABUT_SLAB_DEFAULT, resolveSlab, SLAB_FORM_LABEL, slabDefaultSlope, slabGradePct, slabSection, slabSlopeLabel, type SlabForm, type SlabSlope } from './core/abut/slab';
import { abutSlabJointSvg, abutSlabPlanSvg, abutSlabSectionSvg } from './view/abut-slab-svg';
import { ABUT_SLAB_DEFAULT, resolveSlab, SLAB_FORM_LABEL, slabGradePct, slabSection, slabSlopeLabel, type SlabForm } from './core/abut/slab';
import { abutSlabPlanSvg, abutSlabSectionSvg } from './view/abut-slab-svg';
import { ABUT_SHAPE_DEFAULT, footingEnds, normalizeShape, pileRowsMm, resolveShape, wallThicknessMm, type AbutShape } from './core/abut/shape';
import { abutFootingPlanSvg } from './view/abut-footing-svg';
import { abutWallElevationSvg, abutWallSectionSvg, type AbutWallDrawing } from './view/abut-wall-svg';
@@ -1190,21 +1189,17 @@ function fillAbutWingCard(
/**
* 접속슬래브 타일 그림 (2026-11-04, 48 × 24, 사용자 삽도 기준): 오른쪽이 흉벽 · 브라켓 단차, 왼쪽으로 슬래브가 뻗는다.
* 매립형 = 흉벽 상단 아래 일정 두께, 노출형 = 상면이 흉벽 상단과 같고 끝에 헌치. 경사는 하향(왼쪽이 낮음) / 종단경사(높음).
* 매립형 = 흉벽 상단 아래 일정 두께, 하향 4 % (왼쪽이 낮음). 노출형 = 상면이 흉벽 상단과 같고 끝에 헌치, 종단경사 (왼쪽이 높음).
*/
const SLAB_ICON = {
section(form: SlabForm, slope: SlabSlope): string {
section(form: SlabForm): string {
const wall = '<path d="M31 22V12h3V2h4v20z" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/>';
const fall = slope === 'down4';
const slab =
form === 'buried'
? fall ? 'M4 11L30 8v4L4 15z' : 'M4 6L30 8v4L4 10z'
: fall ? 'M4 4.5L30 2v10L23 6.5L4 8.5z' : 'M4 0.8L30 2v10L23 5.5L4 4.8z';
const slab = form === 'buried' ? 'M4 11L30 8v4L4 15z' : 'M4 0.8L30 2v10L23 5.5L4 4.8z';
return `${wall}<path d="${slab}" fill="currentColor" fill-opacity="0.18" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/>`;
},
};
/** ② 접속슬래브 카드 (시안, 2026-11-04): 형식 · 경사 타일 → 삽도 3개 → 입력 → 자동 값 → 메모. */
/** ② 접속슬래브 카드 (시안, 2026-11-04): 형식 타일 (경사는 노트) → 평면 · 단면(접합 상세 합침) → 입력 → 자동 값 → 메모. */
function fillAbutSlabCard(
box: HTMLDivElement,
rec: AbutRecord,
@@ -1215,13 +1210,24 @@ function fillAbutSlabCard(
): void {
const slab = resolveSlab(rec.slab);
const say = (err: unknown) => ($<HTMLElement>('abut-panel-plan-msg').textContent = (err as Error).message);
const tiles = el('div', undefined, 'abut-tile-row');
tiles.append(
let model: AbutModel | null = null;
let members: AbutMembers | null = null;
let wings: WingSolid[] = [];
try {
model = buildAbut(toAbutInput(rec));
members = buildMembers(model, { bottomEl: rec.foundationBottomEl, pileLengthMm: rec.pileLengthMm }, rec.shape);
wings = buildWings(model, members, rec.wings, rec.wingDims);
} catch {
// shown below the tiles
}
// 형식 (경사는 형식이 정한다, 2026-11-04): 매립형 = 하향 4 %, 노출형 = 종단경사 따라.
const grade = slabSlopeLabel(resolveSlab({ form: 'exposed' }), model?.wing?.slopePct);
box.append(
optionTiles(
'형식',
(['buried', 'exposed'] as const).map((f) => ({
label: SLAB_FORM_LABEL[f],
icon: SLAB_ICON.section(f, slabDefaultSlope(f)),
icon: SLAB_ICON.section(f),
on: slab.form === f,
isDefault: f === 'buried',
pick: () => {
@@ -1233,29 +1239,10 @@ function fillAbutSlabCard(
}
},
})),
),
optionTiles(
'경사',
(['down4', 'grade'] as const).map((s) => ({
label: s === 'down4' ? '하향 4 %' : '종단경사 따라',
icon: SLAB_ICON.section(slab.form, s),
on: slab.slope === s,
isDefault: s === slabDefaultSlope(slab.form),
pick: () => {
if (slab.slope !== s) apply(setSlabSlope(rec, s));
},
})),
`경사 · 매립형 = 하향 4 %, 노출형 = ${grade.replace('종단경사', '종단경사 따라')}`,
),
);
box.append(tiles);
let model: AbutModel;
let members: AbutMembers;
let wings: WingSolid[];
try {
model = buildAbut(toAbutInput(rec));
members = buildMembers(model, { bottomEl: rec.foundationBottomEl, pileLengthMm: rec.pileLengthMm }, rec.shape);
wings = buildWings(model, members, rec.wings, rec.wingDims);
} catch {
if (!model || !members) {
box.append(el('p', '교대 모델을 만들 수 없어 접속슬래브를 그리지 않습니다.', 'muted small'));
return;
}
@@ -1269,7 +1256,7 @@ function fillAbutSlabCard(
// 단면은 교대축 가운데의 흉벽 상단에서 잰다.
const parapetTopMid = (members.parapetTopStartEl + members.parapetTopEndEl) / 2;
const sh = members.shape;
const figs = el('div', undefined, 'abut-wall-figs abut-wing-figs');
const figs = el('div', undefined, 'abut-wall-figs abut-slab-figs');
const addFig = (svg: string, caption: string) => {
const fig = el('figure', undefined, 'abut-fig');
fig.innerHTML = svg;
@@ -1300,20 +1287,26 @@ function fillAbutSlabCard(
parapetGapMm: slab.parapetGapMm,
haunchLengthMm: slab.haunchLengthMm,
}),
`단면 · 교대축 직각 (교대축 가운데) · ${SLAB_FORM_LABEL[slab.form]} · 경사 ${slopeText} · 축척 같음`,
);
addFig(
abutSlabJointSvg({
form: slab.form,
gradePct,
parapetTopEl: parapetTopMid,
bracketHeightM: sh.bracketHeightM,
parapetGapMm: slab.parapetGapMm,
thicknessMm: slab.thicknessMm,
}),
'흉벽–접속슬래브 접합 상세 · 모식도 (부속재는 표시용)',
`단면 · 접합 상세 · 교대축 직각 (교대축 가운데) · ${SLAB_FORM_LABEL[slab.form]} · 경사 ${slopeText} · 축척 같음, 중간 생략 · 간격 · 고무판은 키워 그림, 부속재는 표시용`,
);
box.append(figs);
// 흉벽 높이 = 브라켓 상면 ~ 흉벽 상단 (2026-11-04): 벽체·흉벽 카드의 "브라켓 높이"와 같은 값.
{
const id = idOf('slab-parapet');
const lab = el('label', '흉벽 높이 · 브라켓 상면 ~ 흉벽 상단 (mm)');
lab.htmlFor = id;
const now = Math.round(sh.bracketHeightM * 1000);
const input = numberInput(id, String(now));
if (now !== ABUT_SHAPE_DEFAULT.bracketHeightMm) input.classList.add('changed-input');
input.addEventListener('change', () => {
try {
apply(applyShapeEdit(rec, 'bracketHeightMm', Number(input.value)));
} catch (err) {
fail(err, input, String(now));
}
});
box.append(lab, input, el('span', `기본 ${ABUT_SHAPE_DEFAULT.bracketHeightMm} · = 벽체·흉벽 카드 브라켓 높이`, 'muted small'));
}
for (const f of SLAB_EDIT_FIELDS) {
const id = idOf(`slab-${f.key}`);
const lab = el('label', f.label);
@@ -1335,12 +1328,13 @@ function fillAbutSlabCard(
const side = (mm: number, w: WingSolid | undefined) => (w ? (mm > 0 ? `${mm} × ${mm}` : '없음') : '날개벽 없음');
auto('모따기 (자동)', `좌 ${side(chamfer.left, leftWing)} · 우 ${side(chamfer.right, rightWing)}`, '= 그 측 날개벽 헌치');
auto('폭 (자동)', `${widthM.toFixed(3)} m`, '도면 11.670 · QA-6');
auto('경사', slopeText, slab.slope === slabDefaultSlope(slab.form) ? `${SLAB_FORM_LABEL[slab.form]} 기본` : `변경 (${SLAB_FORM_LABEL[slab.form]} 기본 = ${slabDefaultSlope(slab.form) === 'down4' ? '하향 4 %' : '종단경사'})`);
const bearing = Math.round((sh.parapetBackM - sh.wallBackM) * 1000) - slab.parapetGapMm;
auto('브라켓 얹힘 길이 (자동)', `${bearing} mm`, '= 브라켓 폭 − 흉벽과 간격');
if (slab.form === 'exposed') auto('끝 두께 (자동)', `${Math.round(sh.bracketHeightM * 1000)} mm`, '= 흉벽 상단 − 브라켓 상면');
box.append(
el(
'p',
'시안 메모 · 매립형은 브라켓 상면에 얹혀 두께가 일정하고, 벽체 배면 뒤에서 경사를 따릅니다. 노출형(2026-11-04 삽도 기준, QA-8 가정)은 상면이 흉벽 상단과 같고 끝은 브라켓 위에 얹히며, 벽체 배면부터 헌치 길이 동안 두께가 끝 두께에서 슬래브 두께로 줄어듭니다. 형식을 바꾸면 경사가 그 형식의 기본값으로 바뀝니다. 접합 부속재(고무판, 타르페이퍼, 다웰, 강관)는 표시용이고, 배근은 철근 단계에서 정합니다(노출형 배근 규칙 미정).',
'시안 메모 · 매립형은 브라켓 상면에 얹혀 두께가 일정하고, 벽체 배면 뒤에서 경사를 따릅니다. 노출형(2026-11-04 삽도 기준, QA-8 가정)은 상면이 흉벽 상단과 같고 끝은 브라켓 위에 얹히며, 벽체 배면부터 헌치 길이 동안 두께가 끝 두께에서 슬래브 두께로 줄어듭니다. 경사는 형식이 정합니다(매립형 하향 4 %, 노출형 종단경사). 흉벽 높이는 벽체·흉벽 카드의 브라켓 높이와 같은 값입니다. 접합 부속재(고무판, 타르페이퍼, 다웰, 강관)는 표시용이고, 배근은 철근 단계에서 정합니다(노출형 배근 규칙 미정).',
'muted small abut-memo',
),
);