Merge remote-tracking branch 'origin/main_desktop_1' into sub_desktop_1
This commit is contained in:
@@ -179,6 +179,15 @@
|
||||
"default": 2.5,
|
||||
"required": false,
|
||||
"phase": "detail"
|
||||
},
|
||||
{
|
||||
"key": "revet_foundation",
|
||||
"label": "기슭막이 기초",
|
||||
"input": "select",
|
||||
"choices": ["기초유", "기초버림"],
|
||||
"default": null,
|
||||
"required": false,
|
||||
"phase": "detail"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -202,6 +211,15 @@
|
||||
"default": 2.0,
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"key": "revet_foundation",
|
||||
"label": "기슭막이 기초",
|
||||
"input": "select",
|
||||
"choices": ["기초유", "기초버림"],
|
||||
"default": null,
|
||||
"required": false,
|
||||
"phase": "detail"
|
||||
},
|
||||
{
|
||||
"key": "body_height_m",
|
||||
"label": "본체 높이",
|
||||
@@ -779,6 +797,24 @@
|
||||
"required": false,
|
||||
"phase": "detail"
|
||||
},
|
||||
{
|
||||
"key": "stone_coeff_basis",
|
||||
"label": "야면석 계수",
|
||||
"input": "select",
|
||||
"choices": ["품셈", "실무 관행"],
|
||||
"default": null,
|
||||
"required": false,
|
||||
"phase": "detail"
|
||||
},
|
||||
{
|
||||
"key": "fill_concrete_mpa",
|
||||
"label": "채움 강도",
|
||||
"input": "select",
|
||||
"choices": ["180", "210"],
|
||||
"default": null,
|
||||
"required": false,
|
||||
"phase": "detail"
|
||||
},
|
||||
{
|
||||
"key": "face_slope_ratio",
|
||||
"label": "전면 기울기 (1:n 의 n)",
|
||||
@@ -787,6 +823,15 @@
|
||||
"required": false,
|
||||
"phase": "detail"
|
||||
},
|
||||
{
|
||||
"key": "foundation",
|
||||
"label": "기초",
|
||||
"input": "select",
|
||||
"choices": ["기초유", "기초버림"],
|
||||
"default": null,
|
||||
"required": true,
|
||||
"phase": "detail"
|
||||
},
|
||||
{
|
||||
"key": "side",
|
||||
"label": "설치 측",
|
||||
@@ -870,6 +915,24 @@
|
||||
"required": false,
|
||||
"phase": "detail"
|
||||
},
|
||||
{
|
||||
"key": "stone_coeff_basis",
|
||||
"label": "야면석 계수",
|
||||
"input": "select",
|
||||
"choices": ["품셈", "실무 관행"],
|
||||
"default": null,
|
||||
"required": false,
|
||||
"phase": "detail"
|
||||
},
|
||||
{
|
||||
"key": "fill_concrete_mpa",
|
||||
"label": "채움 강도",
|
||||
"input": "select",
|
||||
"choices": ["180", "210"],
|
||||
"default": null,
|
||||
"required": false,
|
||||
"phase": "detail"
|
||||
},
|
||||
{
|
||||
"key": "face_slope_ratio",
|
||||
"label": "전면 기울기 (1:n 의 n)",
|
||||
@@ -878,6 +941,15 @@
|
||||
"required": false,
|
||||
"phase": "detail"
|
||||
},
|
||||
{
|
||||
"key": "foundation",
|
||||
"label": "기초",
|
||||
"input": "select",
|
||||
"choices": ["기초유", "기초버림"],
|
||||
"default": null,
|
||||
"required": true,
|
||||
"phase": "detail"
|
||||
},
|
||||
{
|
||||
"key": "side",
|
||||
"label": "설치 측",
|
||||
@@ -1026,6 +1098,24 @@
|
||||
"required": false,
|
||||
"phase": "detail"
|
||||
},
|
||||
{
|
||||
"key": "stone_coeff_basis",
|
||||
"label": "야면석 계수",
|
||||
"input": "select",
|
||||
"choices": ["품셈", "실무 관행"],
|
||||
"default": null,
|
||||
"required": false,
|
||||
"phase": "detail"
|
||||
},
|
||||
{
|
||||
"key": "fill_concrete_mpa",
|
||||
"label": "채움 강도",
|
||||
"input": "select",
|
||||
"choices": ["180", "210"],
|
||||
"default": null,
|
||||
"required": false,
|
||||
"phase": "detail"
|
||||
},
|
||||
{
|
||||
"key": "face_slope_ratio",
|
||||
"label": "전면 기울기 (1:n 의 n)",
|
||||
@@ -1034,6 +1124,15 @@
|
||||
"required": false,
|
||||
"phase": "detail"
|
||||
},
|
||||
{
|
||||
"key": "foundation",
|
||||
"label": "기초",
|
||||
"input": "select",
|
||||
"choices": ["기초유", "기초버림"],
|
||||
"default": null,
|
||||
"required": true,
|
||||
"phase": "detail"
|
||||
},
|
||||
{
|
||||
"key": "side",
|
||||
"label": "설치 측",
|
||||
@@ -1139,6 +1238,24 @@
|
||||
"required": false,
|
||||
"phase": "detail"
|
||||
},
|
||||
{
|
||||
"key": "stone_coeff_basis",
|
||||
"label": "야면석 계수",
|
||||
"input": "select",
|
||||
"choices": ["품셈", "실무 관행"],
|
||||
"default": null,
|
||||
"required": false,
|
||||
"phase": "detail"
|
||||
},
|
||||
{
|
||||
"key": "fill_concrete_mpa",
|
||||
"label": "채움 강도",
|
||||
"input": "select",
|
||||
"choices": ["180", "210"],
|
||||
"default": null,
|
||||
"required": false,
|
||||
"phase": "detail"
|
||||
},
|
||||
{
|
||||
"key": "face_slope_ratio",
|
||||
"label": "전면 기울기 (1:n 의 n)",
|
||||
@@ -1147,6 +1264,15 @@
|
||||
"required": false,
|
||||
"phase": "detail"
|
||||
},
|
||||
{
|
||||
"key": "foundation",
|
||||
"label": "기초",
|
||||
"input": "select",
|
||||
"choices": ["기초유", "기초버림"],
|
||||
"default": null,
|
||||
"required": true,
|
||||
"phase": "detail"
|
||||
},
|
||||
{
|
||||
"key": "length_m",
|
||||
"label": "길이",
|
||||
@@ -1192,6 +1318,24 @@
|
||||
"required": false,
|
||||
"phase": "detail"
|
||||
},
|
||||
{
|
||||
"key": "stone_coeff_basis",
|
||||
"label": "야면석 계수",
|
||||
"input": "select",
|
||||
"choices": ["품셈", "실무 관행"],
|
||||
"default": null,
|
||||
"required": false,
|
||||
"phase": "detail"
|
||||
},
|
||||
{
|
||||
"key": "fill_concrete_mpa",
|
||||
"label": "채움 강도",
|
||||
"input": "select",
|
||||
"choices": ["180", "210"],
|
||||
"default": null,
|
||||
"required": false,
|
||||
"phase": "detail"
|
||||
},
|
||||
{
|
||||
"key": "face_slope_ratio",
|
||||
"label": "전면 기울기 (1:n 의 n)",
|
||||
@@ -1200,6 +1344,15 @@
|
||||
"required": false,
|
||||
"phase": "detail"
|
||||
},
|
||||
{
|
||||
"key": "foundation",
|
||||
"label": "기초",
|
||||
"input": "select",
|
||||
"choices": ["기초유", "기초버림"],
|
||||
"default": null,
|
||||
"required": true,
|
||||
"phase": "detail"
|
||||
},
|
||||
{
|
||||
"key": "area_m2",
|
||||
"label": "면적",
|
||||
@@ -1308,6 +1461,24 @@
|
||||
"required": false,
|
||||
"phase": "detail"
|
||||
},
|
||||
{
|
||||
"key": "stone_coeff_basis",
|
||||
"label": "야면석 계수",
|
||||
"input": "select",
|
||||
"choices": ["품셈", "실무 관행"],
|
||||
"default": null,
|
||||
"required": false,
|
||||
"phase": "detail"
|
||||
},
|
||||
{
|
||||
"key": "fill_concrete_mpa",
|
||||
"label": "채움 강도",
|
||||
"input": "select",
|
||||
"choices": ["180", "210"],
|
||||
"default": null,
|
||||
"required": false,
|
||||
"phase": "detail"
|
||||
},
|
||||
{
|
||||
"key": "face_slope_ratio",
|
||||
"label": "전면 기울기 (1:n 의 n)",
|
||||
@@ -1316,6 +1487,15 @@
|
||||
"required": false,
|
||||
"phase": "detail"
|
||||
},
|
||||
{
|
||||
"key": "foundation",
|
||||
"label": "기초",
|
||||
"input": "select",
|
||||
"choices": ["기초유", "기초버림"],
|
||||
"default": null,
|
||||
"required": true,
|
||||
"phase": "detail"
|
||||
},
|
||||
{
|
||||
"key": "height_m",
|
||||
"label": "높이",
|
||||
|
||||
@@ -233,6 +233,9 @@ export interface CulvertSet {
|
||||
side?: string;
|
||||
/** 독립 기슭막이 다단 요청 수(1이면 단일 벽). */
|
||||
tiers?: number;
|
||||
/** 기초 축 — "기초유" | "기초버림"(실무 정본 탭 제목). **비어 있으면 터파기를 안 그린다**
|
||||
* — 고르기 전에는 근거가 없다(2026-09-09 사용자 확정 ④). */
|
||||
foundation?: string | null;
|
||||
}
|
||||
|
||||
/** 세월교 날개벽 한쪽 — 횡단면엔 안 보이고 바닥판 연장량만 넘긴다. */
|
||||
|
||||
@@ -213,6 +213,12 @@ def _culvert_set(options: dict[str, Any] | None) -> dict[str, Any]:
|
||||
"pipe_kind": str(kind) if kind else None,
|
||||
"diameter_m": round((diameter_mm or 1000.0) / 1000.0, 3),
|
||||
"min_cover_m": MIN_PIPE_COVER_M,
|
||||
# 관 기슭막이 기초 축 — **유입·유출 한 칸으로** 받는다(2026-09-09). 한쪽만 기초유로
|
||||
# 하는 일이 드물어 폼을 둘로 늘리지 않았다. 실무가 갈라 쓰면 그때 나눌 것.
|
||||
"foundation": (
|
||||
str(values.get("revet_foundation") or defaults.get("revet_foundation") or "").strip()
|
||||
or None
|
||||
),
|
||||
"inlet": _side_spec(values, defaults, "inlet"),
|
||||
"outlet": _side_spec(values, defaults, "outlet"),
|
||||
}
|
||||
@@ -263,6 +269,8 @@ def _revet_set(options: dict[str, Any] | None) -> dict[str, Any]:
|
||||
"hidden_pipe": True,
|
||||
"side": str(values.get("side") or "양쪽"),
|
||||
"tiers": int(_number(values.get("tiers"), 1.0) or 1),
|
||||
# 기초 축(기초유/기초버림) — 터파기 그림·물량이 같이 쓴다. 비어 있으면 안 그린다.
|
||||
"foundation": (str(values.get("foundation") or "").strip() or None),
|
||||
"pipe_kind": None,
|
||||
"diameter_m": 0.3,
|
||||
"min_cover_m": 0.0,
|
||||
|
||||
@@ -73,6 +73,9 @@ def load_wall_structures(project_root: Path) -> list[dict[str, Any]]:
|
||||
# C군 폼에는 설치 측 칸이 없다 — 비워 두면 화면이 **성토가 나는 쪽**으로
|
||||
# 세운다(`computeRevetmentLayout`). 사용자가 정하고 싶어지면 그때 칸을 낸다.
|
||||
"side": options.get("side"),
|
||||
# 기초 축(기초유/기초버림) — 터파기 그림·물량이 같이 쓴다.
|
||||
# 비어 있으면 화면이 터파기를 안 그린다(근거 없음).
|
||||
"foundation": options.get("foundation"),
|
||||
"tiers": options.get("tiers"),
|
||||
"lift_m": options.get("lift_m"),
|
||||
"shift_m": options.get("shift_m"),
|
||||
|
||||
@@ -23,6 +23,11 @@ import type {
|
||||
PipeEnd,
|
||||
WallLayout,
|
||||
} from "./B06_Section_UI_Cross_Culvert_Geom";
|
||||
import {
|
||||
appendPipeTrench,
|
||||
appendWallTrench,
|
||||
foundationChoice,
|
||||
} from "./B06_Section_UI_Cross_Excavation";
|
||||
import { appendWallHatch } from "./B06_Section_UI_Cross_Wall_Hatch";
|
||||
|
||||
// 기하 계산 진입점과 공개 상수·타입은 여기서 재수출한다 — B05(최소 토피)와 횡단 뷰가
|
||||
@@ -162,6 +167,12 @@ export function appendCulvertOverlay(
|
||||
const revetShapes = new Map<RevetKey, SVGPolygonElement>();
|
||||
/** 집수정 부재 도형 — 유입 선택 강조에 쓴다(2026-08-22 사용자 ③). */
|
||||
const basinShapes: SVGPolygonElement[] = [];
|
||||
// 터파기는 구조물보다 **먼저** 그린다 — 파선이 벽 밑에 깔려야 도면처럼 보인다.
|
||||
// 저장 제원 `foundation`(기초유/기초버림)이 비어 있으면 그리지 않는다 — 근거가 없다.
|
||||
const wallFoundation = foundationChoice(culvert.foundation);
|
||||
for (const { wall } of wallsInOrder) {
|
||||
appendWallTrench(layer, wall, wallFoundation, x, toDisplayY);
|
||||
}
|
||||
for (const { wall, key: wallKey } of wallsInOrder) {
|
||||
// 합성 단면(하부 사다리꼴 + 상부 평행사변형) — 상단 배면이 사면선 접점(사용자 ①·②).
|
||||
const revetShape = polygon(
|
||||
@@ -272,6 +283,26 @@ export function appendCulvertOverlay(
|
||||
const inletFinal = pushOut(pipeCorners.inlet, -1);
|
||||
const outletFinal = pushOut(pipeCorners.outlet, 1);
|
||||
|
||||
// 관 터파기 — 표값 폭으로 관 위 지반선에서 관 바닥까지 수직(KCS 44 40 10 그림 3.2-1).
|
||||
if (!hidePipe) {
|
||||
const invert = Math.min(
|
||||
pipeCorners.inlet.bottom.elevation,
|
||||
pipeCorners.outlet.bottom.elevation,
|
||||
);
|
||||
const centerOffset = (pipeCorners.inlet.bottom.offset + pipeCorners.outlet.bottom.offset) / 2;
|
||||
appendPipeTrench(
|
||||
layer,
|
||||
{
|
||||
centerOffsetM: centerOffset,
|
||||
invertM: invert,
|
||||
topM: invert + diameter + culvert.min_cover_m,
|
||||
diameterMm: Math.round(diameter * 1000),
|
||||
},
|
||||
x,
|
||||
toDisplayY,
|
||||
);
|
||||
}
|
||||
|
||||
const kindLabel = culvert.pipe_kind ? `${culvert.pipe_kind} ` : "";
|
||||
const wallThickness = pipeWallThicknessM(culvert.pipe_kind, diameter);
|
||||
const tip =
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
/* =============================================================================
|
||||
* B06_Section_UI_Cross_Excavation.ts
|
||||
* 횡단도에 **터파기 선**을 얹는다 — 관·벽 두 갈래(2026-09-09 사용자 확정 ④).
|
||||
*
|
||||
* 왜 있나 — 「터파기가 현재 횡단도에서 표현이 안 된다」가 사용자 지적이었다. 수량을
|
||||
* 세기 전에 **도면에 그것이 보여야** 한다.
|
||||
*
|
||||
* ⚠ 근거의 급이 다르므로 **노티스를 갈라 적는다** — 값은 `common_util_excavation` 한 곳에서
|
||||
* 온다(파이썬 짝과 거울 시험으로 묶여 있다). 여기서는 **그리기만** 한다.
|
||||
* 관 : 법정 시방서 표(KCS 44 40 10 그림 3.2-1)
|
||||
* 벽 : 법정 근거 없음 — 실무 정본 식(구조도 기슭막이 xls)
|
||||
* ⚠ **비탈을 주지 않는다 — 수직으로 그린다.** 정본 식에 기울기 항이 없다.
|
||||
* ========================================================================== */
|
||||
|
||||
import {
|
||||
BASIS_PIPE,
|
||||
BASIS_WALL,
|
||||
WALL_BLINDING_DEPTH_M,
|
||||
WALL_BLINDING_WIDTH_M,
|
||||
WALL_FOUNDATION_DEPTH_M,
|
||||
WALL_FOUNDATION_WIDTH_M,
|
||||
WALL_TRENCH_CLEARANCE_M,
|
||||
pipeTrenchWidthM,
|
||||
} from "@util/common_util_excavation";
|
||||
import type { WallLayout } from "./B06_Section_UI_Cross_Culvert_Types";
|
||||
|
||||
const SVG_NS = "http://www.w3.org/2000/svg";
|
||||
|
||||
/** 「기초유」인가 — 저장 제원 `foundation` 의 값. 비어 있으면 **그리지 않는다**(근거 없음). */
|
||||
export function foundationChoice(value: unknown): "기초유" | "기초버림" | null {
|
||||
const text = typeof value === "string" ? value.trim() : "";
|
||||
if (text === "기초유") return "기초유";
|
||||
if (text === "기초버림") return "기초버림";
|
||||
return null;
|
||||
}
|
||||
|
||||
function trenchPath(layer: SVGElement, corners: Array<[number, number]>, tooltip: string): void {
|
||||
const line = document.createElementNS(SVG_NS, "polyline");
|
||||
line.setAttribute("points", corners.map(([px, py]) => `${px},${py}`).join(" "));
|
||||
line.setAttribute("class", "b06-chart__excavation");
|
||||
const title = document.createElementNS(SVG_NS, "title");
|
||||
title.textContent = tooltip;
|
||||
line.append(title);
|
||||
layer.append(line);
|
||||
}
|
||||
|
||||
/**
|
||||
* 관 터파기 — **표값 폭**으로 관 위 지반선에서 관 바닥까지 수직으로 판다.
|
||||
* 표에 없는 관경이면 **아무것도 그리지 않는다**(폭을 지어내지 않는다).
|
||||
*/
|
||||
export function appendPipeTrench(
|
||||
layer: SVGElement,
|
||||
input: {
|
||||
/** 관 중심 offset(m). */
|
||||
centerOffsetM: number;
|
||||
/** 관 바닥(invert) 표고. */
|
||||
invertM: number;
|
||||
/** 터파기 윗면 표고 — 관 위 지반(또는 노면)선. */
|
||||
topM: number;
|
||||
diameterMm: number | null | undefined;
|
||||
},
|
||||
x: (offset: number) => number,
|
||||
toDisplayY: (elevation: number) => number,
|
||||
): number | null {
|
||||
const width = pipeTrenchWidthM(input.diameterMm);
|
||||
if (width === null || !(input.topM > input.invertM)) return null;
|
||||
const half = width / 2;
|
||||
const left = input.centerOffsetM - half;
|
||||
const right = input.centerOffsetM + half;
|
||||
trenchPath(
|
||||
layer,
|
||||
[
|
||||
[x(left), toDisplayY(input.topM)],
|
||||
[x(left), toDisplayY(input.invertM)],
|
||||
[x(right), toDisplayY(input.invertM)],
|
||||
[x(right), toDisplayY(input.topM)],
|
||||
],
|
||||
`관 터파기 폭 ${width.toFixed(2)}m · 깊이 ${(input.topM - input.invertM).toFixed(2)}m\n` +
|
||||
`Φ${Math.round(Number(input.diameterMm))}㎜ — ${BASIS_PIPE}`,
|
||||
);
|
||||
return width;
|
||||
}
|
||||
|
||||
/**
|
||||
* 벽 터파기 — 벽 바닥선 아래로 **수직**. 폭은 바닥 폭 + 여유 0.2, 깊이는 기초 몫.
|
||||
* `foundation` 이 비어 있으면 **그리지 않는다** — 사용자가 고르기 전에는 근거가 없다.
|
||||
*/
|
||||
export function appendWallTrench(
|
||||
layer: SVGElement,
|
||||
wall: WallLayout,
|
||||
foundation: "기초유" | "기초버림" | null,
|
||||
x: (offset: number) => number,
|
||||
toDisplayY: (elevation: number) => number,
|
||||
): { widthM: number; depthM: number } | null {
|
||||
if (foundation === null) return null;
|
||||
const hasFoundation = foundation === "기초유";
|
||||
const depth = hasFoundation ? WALL_FOUNDATION_DEPTH_M : WALL_BLINDING_DEPTH_M;
|
||||
// 폭: 기초 몫은 정본이 정한 값(0.9 / 0.7), 다만 벽 바닥이 그보다 넓으면 그 폭 + 여유.
|
||||
const baseWidth = Math.abs(wall.bottomFront.offset - wall.bottomBack.offset);
|
||||
const minWidth = hasFoundation ? WALL_FOUNDATION_WIDTH_M : WALL_BLINDING_WIDTH_M;
|
||||
const width = Math.max(minWidth, baseWidth + (hasFoundation ? WALL_TRENCH_CLEARANCE_M : 0));
|
||||
const center = (wall.bottomFront.offset + wall.bottomBack.offset) / 2;
|
||||
const top = Math.max(wall.bottomFront.elevation, wall.bottomBack.elevation);
|
||||
const bottom = top - depth;
|
||||
const left = center - width / 2;
|
||||
const right = center + width / 2;
|
||||
trenchPath(
|
||||
layer,
|
||||
[
|
||||
[x(left), toDisplayY(top)],
|
||||
[x(left), toDisplayY(bottom)],
|
||||
[x(right), toDisplayY(bottom)],
|
||||
[x(right), toDisplayY(top)],
|
||||
],
|
||||
`${foundation} 터파기 폭 ${width.toFixed(2)}m · 깊이 ${depth.toFixed(2)}m (수직)\n` +
|
||||
`품셈에 규정이 없어 실무 관행으로 정한 값 — ${BASIS_WALL}`,
|
||||
);
|
||||
return { widthM: width, depthM: depth };
|
||||
}
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
type WallAdjust,
|
||||
type WallLayout,
|
||||
} from "./B06_Section_UI_Cross_Culvert_Types";
|
||||
import { appendWallTrench, foundationChoice } from "./B06_Section_UI_Cross_Excavation";
|
||||
import { appendPlanLine, buildRevetWallGeometry, drawRevetWall } from "./B06_Section_UI_Cross_Wall";
|
||||
|
||||
const SVG_NS = "http://www.w3.org/2000/svg";
|
||||
@@ -53,6 +54,8 @@ export interface RevetmentSpec {
|
||||
height_m?: number | null;
|
||||
/** 사용자가 고른 설치 측 — "좌" | "우". */
|
||||
side?: string | null;
|
||||
/** 기초 축 — "기초유" | "기초버림". **비어 있으면 터파기를 안 그린다**(근거 없음). */
|
||||
foundation?: string | null;
|
||||
/** 단 수(다단). 1이면 단일 벽. */
|
||||
tiers?: number | null;
|
||||
/** (구 모델) 기준 올림·좌우 이동 — 배관식 전환 뒤 자리는 조정창 4축(x·d)이 정한다. */
|
||||
@@ -89,6 +92,8 @@ export interface RevetmentLayout {
|
||||
top: RevetPoint;
|
||||
/** 벽 목록(1단 + 다단) — 배관 벽과 같은 `WallLayout`, 그리기가 그대로 쓴다. */
|
||||
walls: WallLayout[];
|
||||
/** 기초 축 — 저장 칸과 **같은 글자**(`foundation`). 터파기 그림이 이 값으로 갈린다. */
|
||||
foundation: string | null;
|
||||
/** 벽 사이·벽 아래 성토부선(배관 다단과 같은 체계). */
|
||||
fillSegments: OutletFillSegment[];
|
||||
/** 성토 설계선을 벽 상단에서 끊고 노견→벽 성토선을 대신 그리는 트림. */
|
||||
@@ -304,6 +309,7 @@ export function computeRevetmentLayout(
|
||||
requestedTiers,
|
||||
top: jt,
|
||||
walls,
|
||||
foundation: typeof spec.foundation === "string" ? spec.foundation.trim() || null : null,
|
||||
fillSegments: extras.segments,
|
||||
designTrim,
|
||||
appliedAdjust: { x: appliedX, d: appliedDrop, h: adjust?.h ?? null, m: null },
|
||||
@@ -359,6 +365,11 @@ export function appendRevetmentOverlay(
|
||||
` · 성토 물매 1:${(segment.ratio ?? 1.2).toFixed(2)}`,
|
||||
);
|
||||
}
|
||||
// 터파기는 벽보다 **먼저** — 파선이 벽 밑에 깔린다. 기초 축이 비어 있으면 안 그린다.
|
||||
const trenchFoundation = foundationChoice(layout.foundation);
|
||||
for (const wall of layout.walls) {
|
||||
appendWallTrench(svg, wall, trenchFoundation, x, y);
|
||||
}
|
||||
const drawn: SVGPolygonElement[] = [];
|
||||
layout.walls.forEach((wall, index) => {
|
||||
const keyId = index === 0 ? "own" : `own-extra${index - 1}`;
|
||||
|
||||
@@ -379,3 +379,12 @@
|
||||
stroke-dasharray: 4 3;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
/* 터파기 선(2026-09-09 사용자 확정 ④) — **파선**으로 그린다. 구조물 실체가 아니라
|
||||
「여기를 판다」는 표시이고, 근거의 급(법정 표 / 실무 관행)은 말풍선에 적힌다. */
|
||||
.b06-chart__excavation {
|
||||
fill: none;
|
||||
stroke: #8a5c3c;
|
||||
stroke-dasharray: 4 3;
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
@@ -274,6 +274,7 @@ export function putStandardSheetSpec(
|
||||
stone_supply: string | null;
|
||||
back_len_cm: string | null;
|
||||
face_slope_ratio: string | null;
|
||||
foundation: string | null;
|
||||
},
|
||||
): Promise<{ status: string; revision: number; changed: number; notes: string[] }> {
|
||||
return requestJson(`/projects/${projectId}/standard-sheets/spec`, {
|
||||
|
||||
@@ -11,12 +11,14 @@
|
||||
⚠ **수량이 쓰는 그 상수로 그린다** — `B08_Quantity_Engine_UnitQuantity.STONE_MASONRY` 를
|
||||
직접 읽는다. 치수를 여기서 다시 적으면 **그림과 표가 갈린다**(CLAUDE.md 5장).
|
||||
|
||||
상부 두께 = 0.45 + 0.10·H 하부 두께 = 0.45 + 0.40·H
|
||||
상부 두께 = 뒷길이 + 0.30 하부 두께 = 상부 + 0.30 × (H − 1.0)
|
||||
터파기 폭 = 평균두께 + 0.2 되메우기 두께 = 0.2
|
||||
|
||||
ⓘ 이 상수는 **뒷면이 수직인 1:0.3 벽**을 뜻한다 — 하부−상부 = 0.30·H 이고 전면이 뒤로
|
||||
기우는 양도 0.3·H 라 딱 맞는다. 기울기가 0.3 이 아니면 그 차이만큼 **뒷면이 기운다**.
|
||||
지어낸 것이 아니라 상수에서 따라 나오는 결과라 그대로 그린다.
|
||||
⚠ **뒷길이가 두께에 들어간다**(확정 2차 ②, 실무 구조물도 식). 뒷길이가 다르면 벽이
|
||||
두꺼워지고 그림도 그만큼 넓어진다 — 예전 식(0.45+0.10H / 0.45+0.40H)은 뒷길이를
|
||||
아예 안 봐서 35㎝ 든 45㎝ 든 같은 그림이 나왔다.
|
||||
⚠ 뒷길이를 안 정한 장은 두께를 못 낸다 — 그때는 **그리지 않는다**(0 으로 때우면
|
||||
거짓 그림이 된다).
|
||||
|
||||
⚠ **치수가 없는 것은 그리되 치수를 적지 않는다** — 막자갈(뒷채움)은 우리 식이 「입적에서
|
||||
몸통·고임돌을 뺀 것」이라 폭·높이로 정의된 도형이 아니다. 자리만 보이고 값은 표를 가리킨다.
|
||||
@@ -69,12 +71,30 @@ def slope_of(sheet: dict[str, Any]) -> tuple[float, str]:
|
||||
)
|
||||
|
||||
|
||||
def section_points(height_m: float, slope_ratio: float) -> list[tuple[float, float]]:
|
||||
"""벽 단면 네 점(m 단위, 밑면 앞끝이 원점). 앞면이 뒤로 `n·H` 기운다."""
|
||||
top_t = STONE_MASONRY["thickness_base_m"] + STONE_MASONRY["thickness_top_coeff"] * height_m
|
||||
bottom_t = (
|
||||
STONE_MASONRY["thickness_base_m"] + STONE_MASONRY["thickness_bottom_coeff"] * height_m
|
||||
def wall_thickness(height_m: float, back_cm: float) -> tuple[float, float]:
|
||||
"""(상부, 하부) 두께 — **수량이 쓰는 그 식**(`stone_masonry`)과 같은 상수를 읽는다."""
|
||||
top_t = back_cm / 100.0 + STONE_MASONRY["thickness_top_add_m"]
|
||||
bottom_t = top_t + STONE_MASONRY["thickness_slope_per_m"] * max(
|
||||
height_m - STONE_MASONRY["thickness_height_base_m"], 0.0
|
||||
)
|
||||
return top_t, bottom_t
|
||||
|
||||
|
||||
def back_length_cm(sheet: dict[str, Any]) -> float | None:
|
||||
"""장의 뒷길이(㎝). 안 정했으면 `None` — 그때는 두께를 못 내므로 그리지 않는다."""
|
||||
options = sheet.get("options") or {}
|
||||
raw = options.get("back_len_cm") or options.get("stone_back_length_cm")
|
||||
try:
|
||||
return float(raw) if raw not in (None, "") else None
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def section_points(
|
||||
height_m: float, slope_ratio: float, back_cm: float
|
||||
) -> list[tuple[float, float]]:
|
||||
"""벽 단면 네 점(m 단위, 밑면 앞끝이 원점). 앞면이 뒤로 `n·H` 기운다."""
|
||||
top_t, bottom_t = wall_thickness(height_m, back_cm)
|
||||
lean = slope_ratio * height_m
|
||||
return [
|
||||
(0.0, 0.0),
|
||||
@@ -101,7 +121,10 @@ def build_figure(
|
||||
if height_m <= 0:
|
||||
return [], 0.0
|
||||
|
||||
options = sheet.get("options") or {}
|
||||
back_cm = back_length_cm(sheet)
|
||||
if back_cm is None:
|
||||
# 뒷길이가 없으면 두께를 못 낸다 — 0 으로 때우지 않고 그리지 않는다.
|
||||
return [], 0.0
|
||||
slope, slope_note = slope_of(sheet)
|
||||
scale = SCALE_MM_PER_M
|
||||
ox, oy = origin
|
||||
@@ -109,11 +132,8 @@ def build_figure(
|
||||
def mm(point: tuple[float, float]) -> tuple[float, float]:
|
||||
return (ox + point[0] * scale, oy + point[1] * scale)
|
||||
|
||||
points = section_points(height_m, slope)
|
||||
top_t = STONE_MASONRY["thickness_base_m"] + STONE_MASONRY["thickness_top_coeff"] * height_m
|
||||
bottom_t = (
|
||||
STONE_MASONRY["thickness_base_m"] + STONE_MASONRY["thickness_bottom_coeff"] * height_m
|
||||
)
|
||||
points = section_points(height_m, slope, back_cm)
|
||||
top_t, bottom_t = wall_thickness(height_m, back_cm)
|
||||
average_t = (top_t + bottom_t) / 2.0
|
||||
dig_width = average_t + STONE_MASONRY["excavation_extra_m"]
|
||||
backfill_t = STONE_MASONRY["backfill_thickness_m"]
|
||||
@@ -177,9 +197,7 @@ def build_figure(
|
||||
),
|
||||
("막자갈(뒷채움) — 수량은 아래 표", (dig_width + 0.16, height_m * 0.55), "left"),
|
||||
]
|
||||
back = options.get("back_len_cm") or options.get("stone_back_length_cm")
|
||||
if back:
|
||||
labels.append((f"뒷길이 ℓ₃ = {int(back)}㎝", (dig_width + 0.16, height_m * 0.72), "left"))
|
||||
labels.append((f"뒷길이 ℓ₃ = {int(back_cm)}㎝", (dig_width + 0.16, height_m * 0.72), "left"))
|
||||
|
||||
for index, (text, point, align) in enumerate(labels):
|
||||
x, y = mm(point)
|
||||
|
||||
@@ -25,7 +25,18 @@ from typing import Any
|
||||
from B05_Profile.B05_Profile_Structures_Schema import StructureInstance
|
||||
|
||||
#: 표준도에서 받는 칸 — `키 → (이름, 검사)`. 여기 없는 칸은 표준도가 안 만진다.
|
||||
EDITABLE_KEYS: tuple[str, ...] = ("stone_kind", "stone_supply", "back_len_cm", "face_slope_ratio")
|
||||
EDITABLE_KEYS: tuple[str, ...] = (
|
||||
"stone_kind",
|
||||
"stone_supply",
|
||||
"back_len_cm",
|
||||
"face_slope_ratio",
|
||||
"foundation",
|
||||
)
|
||||
|
||||
#: 기초 갈래 — 정본 xls 탭 제목 그대로(`04.구조도(기슭막이).xls`).
|
||||
#: ⚠ **물량과 그림이 같은 칸을 본다** — 터파기 기초 몫이 0.5×(0.7+0.2)=0.45 대
|
||||
#: 0.1×(0.7+0.0)=0.07 ㎥/m 로 갈리고, 횡단도 터파기 선도 이 값으로 그려진다.
|
||||
FOUNDATION_CHOICES: tuple[str, ...] = ("기초유", "기초버림")
|
||||
|
||||
#: 품셈 13-4-3·13-4-4 [주]① 의 일곱 규격. 그 밖의 값은 계수가 없어 물량이 안 선다.
|
||||
BACK_LENGTH_CHOICES: tuple[int, ...] = (25, 30, 35, 45, 55, 60, 75)
|
||||
@@ -39,6 +50,7 @@ FIELD_LABELS: dict[str, str] = {
|
||||
"stone_supply": "조달",
|
||||
"back_len_cm": "뒷길이",
|
||||
"face_slope_ratio": "전면 기울기",
|
||||
"foundation": "기초",
|
||||
}
|
||||
|
||||
|
||||
@@ -95,6 +107,15 @@ def clean_spec(spec: dict[str, Any]) -> tuple[dict[str, Any], list[str]]:
|
||||
"물량이 서지 않습니다."
|
||||
)
|
||||
|
||||
if "foundation" in spec:
|
||||
found = spec.get("foundation")
|
||||
cleaned["foundation"] = str(found) if found not in (None, "") else None
|
||||
if cleaned["foundation"] and cleaned["foundation"] not in FOUNDATION_CHOICES:
|
||||
notes.append(
|
||||
f"기초 「{cleaned['foundation']}」는 정본에 없는 갈래입니다 "
|
||||
f"(있는 것: {' · '.join(FOUNDATION_CHOICES)})."
|
||||
)
|
||||
|
||||
if "face_slope_ratio" in spec:
|
||||
ratio, note = _clean_slope(spec.get("face_slope_ratio"))
|
||||
cleaned["face_slope_ratio"] = ratio
|
||||
|
||||
@@ -70,6 +70,7 @@ class StandardSheetSpecRequest(BaseModel):
|
||||
stone_supply: str | None = None
|
||||
back_len_cm: int | str | None = None
|
||||
face_slope_ratio: float | str | None = None
|
||||
foundation: str | None = None
|
||||
|
||||
|
||||
@router.put("/{project_id}/standard-sheets/spec")
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
const STONE_KINDS = ["야면석·호박돌", "깬잡석", "깬돌", "견치돌"] as const;
|
||||
const SUPPLIES = ["채집", "구입"] as const;
|
||||
const BACK_LENGTHS = ["25", "30", "35", "45", "55", "60", "75"] as const;
|
||||
/** 정본 xls 탭 제목 그대로 — 터파기 기초 몫 0.45 대 0.07 을 가르는 축. */
|
||||
const FOUNDATIONS = ["기초유", "기초버림"] as const;
|
||||
|
||||
/** 표준도 장 하나 — 서버가 낸 것 중 이 폼이 쓰는 것만. */
|
||||
export interface StandardSheetSpec {
|
||||
@@ -31,6 +33,7 @@ export interface StandardSpecResult {
|
||||
stone_supply: string | null;
|
||||
back_len_cm: string | null;
|
||||
face_slope_ratio: string | null;
|
||||
foundation: string | null;
|
||||
}
|
||||
|
||||
/** 이 종류가 돌쌓기 계열인가 — 옹벽·집수정에는 이 칸들이 뜻이 없다. */
|
||||
@@ -106,6 +109,7 @@ export function buildStandardSpecPanel(
|
||||
const kind = select(STONE_KINDS, options.stone_kind, "— 안 정함 —");
|
||||
const supply = select(SUPPLIES, options.stone_supply, "— 안 정함(기본 채집) —");
|
||||
const back = select(BACK_LENGTHS, options.back_len_cm, "— 안 정함 —");
|
||||
const foundation = select(FOUNDATIONS, options.foundation, "— 안 정함 —");
|
||||
|
||||
const slope = document.createElement("input");
|
||||
slope.className = "b07-spec__input";
|
||||
@@ -118,6 +122,7 @@ export function buildStandardSpecPanel(
|
||||
field("돌 종류", kind),
|
||||
field("조달", supply, "비우면 「캔다」로 봅니다."),
|
||||
field("뒷길이 (㎝)", back, "품셈 일곱 규격 밖이면 물량이 서지 않습니다."),
|
||||
field("기초", foundation, "터파기 몫이 갈립니다 — 기초유 0.45 · 기초버림 0.07 ㎥/m."),
|
||||
field(
|
||||
"전면 기울기 1:n",
|
||||
slope,
|
||||
@@ -157,6 +162,7 @@ export function buildStandardSpecPanel(
|
||||
stone_supply: supply.value || null,
|
||||
back_len_cm: back.value || null,
|
||||
face_slope_ratio: slope.value.trim() || null,
|
||||
foundation: foundation.value || null,
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
|
||||
@@ -104,6 +104,7 @@ def build_handoff(
|
||||
ground_classes: list[str] | None = None,
|
||||
ground_methods: dict[str, str | None] | None = None,
|
||||
concrete_placing_method: str | None = None,
|
||||
bench_cut_depth_m: float | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""B09 가 그대로 받는 모양. 없는 표는 건너뛰되 **빈 표와 구별해 적는다**."""
|
||||
table = mapping or load_mapping()
|
||||
@@ -112,7 +113,7 @@ def build_handoff(
|
||||
|
||||
methods = {key: value for key, value in (ground_methods or {}).items() if value}
|
||||
if summary_table:
|
||||
rows, misses = _earthwork_rows(summary_table, table, methods)
|
||||
rows, misses = _earthwork_rows(summary_table, table, methods, bench_cut_depth_m)
|
||||
work_items.extend(rows)
|
||||
unmatched.extend(misses)
|
||||
if haul_table:
|
||||
|
||||
@@ -89,10 +89,17 @@ def _basis_mismatch(entry: dict[str, Any] | None, unit: str) -> tuple[str, str,
|
||||
)
|
||||
|
||||
|
||||
#: 면적(㎡)으로 서지만 **길이를 곱해 ㎥ 로 내보내는** 공종 — 지금은 층따기뿐이다.
|
||||
#: ⭐ 2026-09-09 사용자 확정 2차 ① — 「면적이 정본이고 부피는 사용자가 지정한 길이를 곱해 쓴다」.
|
||||
#: ⚠ 면적을 없애지 않는다(횡단도 하단 표가 면적을 쓴다) — **㎥ 를 덧붙이는 것**이다.
|
||||
AREA_TIMES_DEPTH_GROUPS = {"층따기"}
|
||||
|
||||
|
||||
def _earthwork_rows(
|
||||
summary_table: dict[str, Any],
|
||||
mapping: WorkItemMapping,
|
||||
methods: dict[str, str | None],
|
||||
bench_cut_depth_m: float | None = None,
|
||||
) -> tuple[list[dict[str, Any]], list[str]]:
|
||||
"""토공집계표 줄을 내역 줄로 옮긴다.
|
||||
|
||||
@@ -117,7 +124,12 @@ def _earthwork_rows(
|
||||
amount = float(row.get("amount") or 0.0)
|
||||
mismatch = _basis_mismatch(entry, unit) if code else None
|
||||
spec_detail = ""
|
||||
if mismatch is not None:
|
||||
# 층따기 — 길이가 들어오면 **면적 × 길이**로 ㎥ 를 내 품셈 단가를 그대로 쓴다.
|
||||
depth = float(bench_cut_depth_m or 0.0)
|
||||
if group in AREA_TIMES_DEPTH_GROUPS and unit == "㎡" and depth > 0:
|
||||
spec_detail = f"면적 {amount:,.2f}㎡ × 길이 {depth:g}m"
|
||||
unit, amount, mismatch = "㎥", amount * depth, None
|
||||
elif mismatch is not None:
|
||||
spec_detail = f"집계 {amount:,.2f} {unit} (품셈 밑수 {mismatch[0]})"
|
||||
unit, amount = mismatch[0], 0.0
|
||||
if code is None and not is_subtotal:
|
||||
|
||||
@@ -38,6 +38,14 @@ from dataclasses import dataclass, field
|
||||
from typing import Any, Iterable
|
||||
|
||||
from common_util.common_util_structure_face_role import structure_face_role
|
||||
from common_util.common_util_excavation import (
|
||||
BASIS_WALL,
|
||||
WALL_BLINDING_DEPTH_M,
|
||||
WALL_FOUNDATION_DEPTH_M,
|
||||
WALL_TRENCH_CLEARANCE_M,
|
||||
wall_trench_area_m2,
|
||||
wall_trench_width_m,
|
||||
)
|
||||
from B08_Quantity.B08_Quantity_Engine_ObservedUnit import (
|
||||
BASIS_DERIVED,
|
||||
BASIS_OBSERVED,
|
||||
@@ -67,6 +75,17 @@ STONE_BACK_LENGTH_TABLE: dict[int, dict[str, float | None]] = {
|
||||
}
|
||||
DEFAULT_BACK_LENGTH_CM = 45
|
||||
|
||||
#: ⑤ 돌 중량이 어디서 온 값인가 (2026-09-08 사용자 확정 2차 ⑤ 「DB 확인 → 없으면 현재 유지,
|
||||
#: 나중에 프로그램에서 확인할 수 있게 근거를 남길 것」).
|
||||
#: ⚠ **품셈·교본에는 돌중량표가 없다** — 지식DB 전수 확인 결과 이 값이 나오는 곳은
|
||||
#: 실무 원단위 라이브러리 한 곳뿐이다(기술문서에는 그 라이브러리를 인용한 줄만 있음).
|
||||
#: 그래서 값을 바꾸지 않고 **어디서 온 값인지**를 줄마다 달아 화면에서 되짚게 한다.
|
||||
STONE_WEIGHT_SOURCE = "uljin_library"
|
||||
STONE_WEIGHT_BASIS = (
|
||||
"⚠ 실무 관측값 — 울진 소광 원단위 라이브러리 「돌뒷길이별 원단위표」(35·45·55㎝만 있음)."
|
||||
" 품셈·교본에는 돌중량표가 없음"
|
||||
)
|
||||
|
||||
|
||||
#: 돌 종류별 계수표 — 품셈 13-4-3·13-4-4 [주]① · 교본 7-3.
|
||||
#: ⚠ 지금까지 **건설품셈 참고자료 한 벌**(돌 종류로 안 갈리는 표)로만 돌고 있었다.
|
||||
@@ -92,6 +111,20 @@ def load_stone_kind_table() -> dict[str, Any]:
|
||||
return {}
|
||||
|
||||
|
||||
#: ⑨ 계수를 **어느 열로 볼 것인가** (2026-09-08 사용자 확정 2차 ⑨).
|
||||
#: 기본은 **품셈 열**(야면석 고임돌 0.11 · 채움 0.15)이고, 「실무 관행」으로 두면
|
||||
#: 건설품셈 참고자료 한 벌(깬돌 계열 0.15 · 0.20)로 덮어쓴다.
|
||||
#: ⚠ **빈 칸은 「안 정함」**이고 그때 기본(품셈)으로 선다 — 등록부 default 는 비어 있다.
|
||||
#: 기본값을 등록부가 아니라 여기가 갖는 것은 `face_slope_ratio` 와 같은 결이다.
|
||||
COEFF_BASIS_KEY = "stone_coeff_basis"
|
||||
COEFF_BASIS_PRACTICE = {"실무 관행", "실무관행", "practice", "관행"}
|
||||
|
||||
|
||||
def wants_practice_coefficients(options: dict[str, Any]) -> bool:
|
||||
"""계수를 실무 관행 열로 볼 것인가. 빈 칸이면 **아니오**(= 품셈 열)."""
|
||||
return str(options.get(COEFF_BASIS_KEY) or "").strip() in COEFF_BASIS_PRACTICE
|
||||
|
||||
|
||||
def stone_coefficients(options: dict[str, Any], back_cm: int) -> tuple[dict[str, Any], str]:
|
||||
"""(계수 한 벌, 알림). 돌 종류를 안 고르면 **종전 값**으로 돌되 그 사실을 알린다.
|
||||
|
||||
@@ -105,14 +138,21 @@ def stone_coefficients(options: dict[str, Any], back_cm: int) -> tuple[dict[str,
|
||||
return {}, ""
|
||||
if key not in [str(x) for x in (table.get("back_lengths_cm") or [])]:
|
||||
return {}, (f"뒷길이 {back_cm}㎝ 는 품셈 표(25·30·35·45·55·60·75㎝)에 없어 계수가 없습니다")
|
||||
if not kind:
|
||||
practice = wants_practice_coefficients(options)
|
||||
if not kind or practice:
|
||||
fallback = table.get("fallback") or {}
|
||||
message = (
|
||||
f"「{kind}」이지만 계수를 **실무 관행 열**(건설품셈 참고자료)로 덮어썼습니다 —"
|
||||
" 기본은 품셈 열입니다(확정 ⑨)"
|
||||
if practice and kind
|
||||
else str(fallback.get("message") or "")
|
||||
)
|
||||
return {
|
||||
"wedge_stone_m3_per_m2": (fallback.get("wedge_stone_m3_per_m2") or {}).get(key),
|
||||
"fill_concrete_m3_per_m2": (fallback.get("fill_concrete_m3_per_m2") or {}).get(key),
|
||||
"backfill_ratio": fallback.get("backfill_ratio_of_back_length"),
|
||||
"kind": "",
|
||||
}, str(fallback.get("message") or "")
|
||||
}, message
|
||||
if kind not in (table.get("kinds") or []):
|
||||
return {}, f"「{kind}」은(는) 아는 돌 종류가 아니라 계수를 못 골랐습니다"
|
||||
wedge = ((table.get("wedge_stone_m3_per_m2") or {}).get(kind) or {}).get(key)
|
||||
@@ -139,9 +179,18 @@ STONE_MASONRY = {
|
||||
# ⚠ 하드코딩하면 안 되는 값이다 — 큰돌쌓기는 「1:0.3 **이상**」이라 기울기가
|
||||
# 바뀔 수 있고, 그때 1.04 는 틀린 값이 되지만 `hypot` 은 따라간다.
|
||||
"sheet_check_factor_at_0_3": 1.04, # 1:0.3 에서 시트값과 맞는지 대조하는 자리
|
||||
"thickness_base_m": 0.45, # 평균두께 식의 밑돌 두께
|
||||
"thickness_top_coeff": 0.10, # 상부 두께 계수 (0.45 + 0.10·H)
|
||||
"thickness_bottom_coeff": 0.40, # 하부 두께 계수 (0.45 + 0.40·H)
|
||||
# ⭐ 2026-09-09 확정 2차 ② — **실무 구조물도 식**으로 갈았음(랩탑 보조가 소광리 원본
|
||||
# 네 탭에서 찾아 실측과 소수점까지 대조).
|
||||
# 상부 두께 = 뒷길이(ℓ3) + 0.30
|
||||
# 하부 두께 = 상부 + 0.30 × (H − 1.0)
|
||||
# ⚠ **옛 식에는 뒷길이가 없었다**(0.45 + 0.10·H / 0.45 + 0.40·H) — 뒷길이를 35 로
|
||||
# 바꿔도 두께가 안 바뀌어 돌 부피·막자갈·터파기가 다 같은 값으로 섰다.
|
||||
# ⚠ 정본 xls 의 「상부 0.75 · 하부 0.90」은 **H=1.5 규칙값을 다른 높이 탭에 복사**한
|
||||
# 흔적이라 그대로 쓰면 H=1.5 밖에서 틀린다.
|
||||
# ⚠ 돌조공(H=0.5 · 0.55/0.65)은 이 규칙 밖 — 다른 시설이라 여기서 다루지 않는다.
|
||||
"thickness_top_add_m": 0.30, # 상부 = 뒷길이 + 0.30
|
||||
"thickness_slope_per_m": 0.30, # 하부 = 상부 + 0.30 × (H − 1.0)
|
||||
"thickness_height_base_m": 1.0, # 그 식의 기준 높이
|
||||
# ⚠ 미결 — 법은 「2~3 ㎡당 1개소 **이상**」(구조물_수량.md §물구멍)이고 2.0 은 **실무 관측값**이다.
|
||||
# 범위의 한쪽 끝을 쓰는 것이라 사용자 확정 전까지 잠정이다. 식이 아니라 여기 있으니 갈아끼우면 된다.
|
||||
"weep_hole_area_m2": 2.0, # 물구멍 1개소당 벽면적
|
||||
@@ -150,8 +199,9 @@ STONE_MASONRY = {
|
||||
# 적어 둔다 — 관측 원단위가 있는 종류는 그 표가 이기고, 없는 종류만 이 값으로 선다.
|
||||
"weep_hole_length_m": 0.5, # 물구멍관 1개소당 관 길이
|
||||
"mortar_m3_per_m2": 0.009, # 줄눈 모르터 (찰쌓기만)
|
||||
"excavation_extra_m": 0.2, # 터파기 폭 여유
|
||||
"backfill_thickness_m": 0.2, # 되메우기 두께
|
||||
# ⚠ 터파기 여유폭·되메우기 두께는 **공용 한 벌**로 옮겼다
|
||||
# (`common_util/common_util_excavation.py` — 화면 B06 과 짝인 TS 가 있다).
|
||||
# 여기서 다시 두면 그림과 물량이 갈린다.
|
||||
}
|
||||
|
||||
# 성분이 어디로 가는가 — 이중계상을 막는 표시.
|
||||
@@ -163,9 +213,12 @@ STONE_MASONRY = {
|
||||
#: 구조물 시공이 원활하도록 **100 mm 두께의 버림콘크리트**를 타설하도록 한다」.
|
||||
#: 폭 근거: KCS 34 50 05 「버림 콘크리트의 두께는 설계도서에 따르며, **폭은 잡석다짐의
|
||||
#: 폭과 동일**하게 한다」.
|
||||
#: ⚠ 우리에게 **잡석다짐 폭이 아직 없다**(기초잡석은 품셈에 공종만 있고 두께·폭이 없음).
|
||||
#: 그래서 **터파기 폭(평균두께 + 여유 0.2m)을 잠정으로** 쓰고 근거 문구에 그 사실을 적는다.
|
||||
#: 잡석다짐 폭이 정해지면 이 한 줄만 바꾸면 된다.
|
||||
#: ⭐ 그 잡석다짐 폭이 **정해졌다** — 「기준 구조물에서 유도, 같은 값을 공유」
|
||||
#: (2026-09-08 사용자 확정 2차 ⑪ 「기슭막이면 **하단 길이** × 종단방향 폭 × 높이」).
|
||||
#: ⇒ 벽에서 **하단 길이 = 하부 두께**다. 종전의 「터파기 폭 잠정」을 그 값으로 바꾼다.
|
||||
#: ⚠ **기초잡석(품셈 12-25) 물량은 아직 못 센다** — 폭은 이제 있으나 **두께가 어느
|
||||
#: 원문에도 없다**(품셈은 ㎥당 품만 줌). 실무 관측은 관보호공 날개벽 T=0.2 하나뿐이라
|
||||
#: **다른 구조물 값을 옮겨 쓰지 않고** 사유로 드러낸다.
|
||||
#: 돌을 **사 오나 캐나** — 기본은 「캔다」(2026-09-09 사용자 확정 ②).
|
||||
#: 법이 그쪽을 권한다 — 별표2 「석축 등에 필요한 야면석 등은 **가급적 현장에서
|
||||
#: 채취·사용**하도록 운반거리를 조사한다」. 실무 견적 다섯 권에도 야면석 **구입 단가가
|
||||
@@ -186,6 +239,31 @@ STONE_SUPPLY_PURCHASED = {"구입", "구입품", "사서", "purchase", "purchase
|
||||
#: 뒤집힌다. 우리는 **양수**로 주고, 빼는 것은 받는 쪽이 한다.
|
||||
COLLECTED_STONE_KEY = "collected_stone_deduction_m3"
|
||||
|
||||
#: ⑩ 채움 콘크리트 강도 (2026-09-08 사용자 확정 2차 ⑩ 「기본 210, 고를 수 있게」).
|
||||
#: ⚠ **빈 칸은 「안 정함」**이고 그때 210 으로 선다 — 등록부 default 는 비어 있다.
|
||||
#: 180 은 국가기준 하한(돌쌓기 전용 값)이라 고르면 그 값이 이긴다.
|
||||
FILL_CONCRETE_MPA_KEY = "fill_concrete_mpa"
|
||||
FILL_CONCRETE_MPA_DEFAULT = "210"
|
||||
FILL_CONCRETE_MPA_CHOICES = ("180", "210")
|
||||
|
||||
|
||||
def fill_concrete_mpa(options: dict[str, Any]) -> tuple[str, str]:
|
||||
"""(강도, 근거). 안 정했으면 기본 210 이고 그 사실이 근거에 적힌다."""
|
||||
value = str(options.get(FILL_CONCRETE_MPA_KEY) or "").strip()
|
||||
if value in FILL_CONCRETE_MPA_CHOICES:
|
||||
return value, f"강도 {value} — 저장 제원에서 고른 값"
|
||||
if value:
|
||||
return (
|
||||
FILL_CONCRETE_MPA_DEFAULT,
|
||||
f"강도 {FILL_CONCRETE_MPA_DEFAULT} — ⚠ 저장값 「{value}」은 180·210 중에"
|
||||
" 없어 기본으로 섬",
|
||||
)
|
||||
return (
|
||||
FILL_CONCRETE_MPA_DEFAULT,
|
||||
f"강도 {FILL_CONCRETE_MPA_DEFAULT} — 안 정해 기본값(확정 ⑩)",
|
||||
)
|
||||
|
||||
|
||||
BLINDING_THICKNESS_M = 0.10
|
||||
#: 버림을 뺄 수 있는 칸 — 「기본은 넣고, 빼고 싶으면 뺀다」(사용자 확정 ⑭).
|
||||
#: 저장 제원에 이 칸이 없으면 **넣는 쪽**이 기본이다.
|
||||
@@ -210,6 +288,11 @@ DESTINATION = {
|
||||
# ⚠ 자재도 토공도 아닌 자리 — **유토곡선이 사토에서 뺄 밑수**다. 자재총괄은
|
||||
# `material` 만 모으므로 여기 섞이지 않는다.
|
||||
"채집석": "haul_deduction", # ⚠ 자재 카탈로그가 이름으로 찾는다 — 공백 없는 한 낱말(B09 규약)
|
||||
# ⚠ **보여 주기만 하는 줄** — 자재도 토공도 일위대가도 아니다. 정본 계산표 좌측 열에
|
||||
# 「입적」으로 실려 있어 그 이름 그대로 낸다(2026-09-08 확정 ⑦). 값은 이미 식 안에
|
||||
# 있던 것을 줄로 꺼낸 것뿐이고, `material`·`earthwork` 어디에도 안 섞인다.
|
||||
# ⚠ 「석적」은 정본에 없다(소광리 시트에만) — **안 낸다.**
|
||||
"입적": "reference",
|
||||
}
|
||||
|
||||
# ⚠ 배합 성분 — 산출물에 나타나면 안 된다(㉢). B09 일위대가가 배합표로 분해한다.
|
||||
@@ -233,6 +316,8 @@ class Component:
|
||||
# 두 근거가 한 표에 섞이므로 줄마다 단다. 안 적으면 나중에 못 되짚는다.
|
||||
basis_kind: str = BASIS_DERIVED
|
||||
source: str = ""
|
||||
#: 규격 — 같은 이름이라도 단가가 갈리는 값(채움 콘크리트 강도 등). 받는 쪽이 읽는다.
|
||||
spec: str = ""
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
@@ -359,21 +444,42 @@ def wants_blinding(options: dict[str, Any]) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
def _blinding_component(base_width_m: float, length_m: float, options: dict[str, Any]):
|
||||
"""버림 콘크리트 한 줄. 넣지 않기로 했으면 `None`."""
|
||||
def _blinding_component(
|
||||
base_width_m: float,
|
||||
length_m: float,
|
||||
options: dict[str, Any],
|
||||
width_basis: str = "",
|
||||
):
|
||||
"""버림 콘크리트 한 줄. 넣지 않기로 했으면 `None`.
|
||||
|
||||
폭은 **잡석다짐 폭**(KCS 34 50 05)이고, 그 값은 확정 ⑪ 로 **구조물 하단 길이**다.
|
||||
부르는 쪽이 그 폭과 까닭(`width_basis`)을 함께 넘긴다.
|
||||
"""
|
||||
if not wants_blinding(options) or base_width_m <= 0 or length_m <= 0:
|
||||
return None
|
||||
basis = (
|
||||
f"기초 폭 {base_width_m:.2f}m × 연장 × 두께 {BLINDING_THICKNESS_M:.2f}m"
|
||||
" · 두께는 KDS 44 90 00(100㎜) · 폭은 잡석다짐 폭(KCS 34 50 05)"
|
||||
)
|
||||
if width_basis:
|
||||
basis = f"{basis} = {width_basis}"
|
||||
return Component(
|
||||
"버림콘크리트",
|
||||
"㎥",
|
||||
base_width_m * length_m * BLINDING_THICKNESS_M,
|
||||
DESTINATION["버림콘크리트"],
|
||||
f"기초 폭 {base_width_m:.2f}m × 연장 × 두께 {BLINDING_THICKNESS_M:.2f}m"
|
||||
" · 두께는 KDS 44 90 00(100㎜) · ⚠ 폭은 잡석다짐 폭(KCS 34 50 05)이라야 하나"
|
||||
" 그 값이 아직 없어 **터파기 폭을 잠정**으로 씀",
|
||||
basis,
|
||||
)
|
||||
|
||||
|
||||
#: 기초잡석(품셈 12-25)이 아직 못 서는 까닭 — **폭은 있고 두께가 없다.**
|
||||
RUBBLE_BASE_BLOCKED = (
|
||||
"기초잡석(12-25) 물량이 안 섬 — 폭은 하단 길이 {width:.2f}m 로 섰으나 **두께가 원문에 "
|
||||
"없음**(품셈 12-25 는 ㎥당 품만 줌). 실무 관측은 관보호공 날개벽 T=0.2 하나뿐이라 "
|
||||
"다른 구조물 값을 옮겨 쓰지 않음"
|
||||
)
|
||||
|
||||
|
||||
#: ⚠ **저장 제원의 실제 칸 이름**은 `back_len_cm` 이다(레지스트리 확인).
|
||||
#: 앞서 `stone_back_length_cm` 을 읽고 있어 **저장값이 영영 안 닿고 늘 기본 45㎝ 로 돌았다**
|
||||
#: — 뒷길이를 75 로 골라도 45 계수가 붙던 자리다. 값이 나오므로 아무 시험도 안 잡았다.
|
||||
@@ -418,8 +524,9 @@ BOULDER_MASONRY = {
|
||||
# ⚠ 검산 참고값 — 곱하지 않는다. 까닭은 `STONE_MASONRY` 의 같은 칸 주석을 볼 것.
|
||||
# 큰돌쌓기는 전면 기울기가 「1:0.3 **이상**」이라 특히 하드코딩하면 안 된다.
|
||||
"sheet_check_factor_at_0_3": 1.04,
|
||||
"excavation_extra_m": 0.2, # 터파기 폭 여유
|
||||
"backfill_thickness_m": 0.2, # 되메우기 두께
|
||||
# ⚠ 터파기 여유폭·되메우기 두께는 **공용 한 벌**로 옮겼다
|
||||
# (`common_util/common_util_excavation.py` — 화면 B06 과 짝인 TS 가 있다).
|
||||
# 여기서 다시 두면 그림과 물량이 갈린다.
|
||||
}
|
||||
|
||||
|
||||
@@ -460,7 +567,6 @@ def boulder_masonry(
|
||||
# ⚠ 큰돌쌓기는 **표준경사표 대상이 아니다** — 교본이 「1:0.3 **이상**」으로만 두었다.
|
||||
# 저장 제원에 값이 있으면 그것을 쓰고, 없으면 하한 0.3 으로 둔다.
|
||||
slope_ratio = _num(options.get("face_slope_ratio"), 0.3)
|
||||
constants = BOULDER_MASONRY
|
||||
face_area = height_m * length_m
|
||||
# 기울기 몫은 **한 번만** — 13-4 와 같은 자리다(㉘).
|
||||
masonry_area = face_area * math.hypot(1.0, slope_ratio)
|
||||
@@ -481,30 +587,23 @@ def boulder_masonry(
|
||||
upper_cm = float(str(diameter).split("~")[-1])
|
||||
thickness = upper_cm / 100.0
|
||||
# 버림 콘크리트 — 돌쌓기와 같은 자리(사용자 확정 ⑭). 빼려면 저장 제원에서 「안 넣음」.
|
||||
blinding = _blinding_component(thickness + constants["excavation_extra_m"], length_m, options)
|
||||
blinding = _blinding_component(
|
||||
thickness,
|
||||
length_m,
|
||||
options,
|
||||
f"하단 길이(직경 위끝) {thickness:.2f}m — 확정 ⑪ 준용",
|
||||
)
|
||||
if blinding is not None:
|
||||
components.append(blinding)
|
||||
excavation = height_m * (thickness + constants["excavation_extra_m"]) * length_m
|
||||
backfill = height_m * constants["backfill_thickness_m"] * length_m
|
||||
components.extend(
|
||||
[
|
||||
Component(
|
||||
"터파기",
|
||||
"㎥",
|
||||
excavation,
|
||||
DESTINATION["터파기"],
|
||||
f"높이 × (벽두께 {thickness:g}m + 0.2) × 연장 · ⚠ 큰돌쌓기 터파기 폭 규정이"
|
||||
" 품셈에 없어 돌쌓기 방식을 준용",
|
||||
),
|
||||
Component("되메우기", "㎥", backfill, DESTINATION["되메우기"], "높이 × 0.2 × 연장"),
|
||||
Component(
|
||||
"잔토처리",
|
||||
"㎥",
|
||||
excavation - backfill,
|
||||
DESTINATION["잔토처리"],
|
||||
"터파기 − 되메우기",
|
||||
),
|
||||
]
|
||||
_trench_components(
|
||||
height_m,
|
||||
length_m,
|
||||
thickness,
|
||||
options,
|
||||
notes,
|
||||
"⚠ 큰돌쌓기 터파기 폭 규정이 품셈에 없어 돌쌓기 방식을 준용",
|
||||
)
|
||||
)
|
||||
|
||||
notes.append(
|
||||
@@ -525,6 +624,66 @@ def boulder_masonry(
|
||||
return components, notes
|
||||
|
||||
|
||||
def _trench_components(
|
||||
height_m: float,
|
||||
length_m: float,
|
||||
thickness: float,
|
||||
options: dict[str, Any],
|
||||
notes: list[str],
|
||||
extra_basis: str = "",
|
||||
) -> list[Component]:
|
||||
"""터파기·되메우기·잔토 — **공용 단면 함수 한 벌**(`common_util_excavation`).
|
||||
|
||||
⚠ 여기서 식을 다시 짜지 않는다 — 화면(B06 횡단도)이 그리는 단면과 같은 코드라야
|
||||
그림과 물량이 안 갈린다. 평균두께는 이쪽이 낸 값을 **넘겨 준다**(저쪽은 두께를 모름).
|
||||
⚠ 「기초유 / 기초버림」은 정본 탭 제목이고 **기초분 0.45 냐 0.07 이냐**를 가른다.
|
||||
안 고른 프로젝트는 **비탈분만** 세우고 사유를 남긴다 — 한쪽으로 찍으면 임의값이
|
||||
금액으로 굳는다(기초분이 통째로 0.45㎥/m 다).
|
||||
"""
|
||||
foundation = str(options.get("foundation") or "").strip()
|
||||
if foundation in ("기초유", "기초버림"):
|
||||
has_foundation = foundation == "기초유"
|
||||
area = wall_trench_area_m2(height_m, thickness, has_foundation=has_foundation)
|
||||
base_depth = WALL_FOUNDATION_DEPTH_M if has_foundation else WALL_BLINDING_DEPTH_M
|
||||
base_basis = f"기초분({foundation}) + "
|
||||
else:
|
||||
width = wall_trench_width_m(thickness)
|
||||
area = None if width is None else height_m * width
|
||||
base_depth = 0.0
|
||||
base_basis = ""
|
||||
notes.append(
|
||||
"⚠ 기초 유/무(기초유·기초버림)를 안 정해 **기초 터파기 몫이 안 섰음** — 비탈분만 세움"
|
||||
)
|
||||
if area is None:
|
||||
notes.append("⚠ 높이·평균두께가 없어 터파기가 서지 않음")
|
||||
return []
|
||||
excavation = area * length_m
|
||||
backfill = (base_depth + height_m) * WALL_TRENCH_CLEARANCE_M * length_m
|
||||
basis = (
|
||||
f"{base_basis}H × (평균두께 {thickness:.2f} + {WALL_TRENCH_CLEARANCE_M}) × 연장"
|
||||
f" · {BASIS_WALL}"
|
||||
)
|
||||
if extra_basis:
|
||||
basis = f"{basis} · {extra_basis}"
|
||||
return [
|
||||
Component("터파기", "㎥", excavation, DESTINATION["터파기"], basis),
|
||||
Component(
|
||||
"되메우기",
|
||||
"㎥",
|
||||
backfill,
|
||||
DESTINATION["되메우기"],
|
||||
f"(기초깊이 {base_depth:g} + H) × {WALL_TRENCH_CLEARANCE_M} × 연장",
|
||||
),
|
||||
Component(
|
||||
"잔토처리",
|
||||
"㎥",
|
||||
excavation - backfill,
|
||||
DESTINATION["잔토처리"],
|
||||
"터파기 − 되메우기",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def stone_masonry(
|
||||
height_m: float,
|
||||
length_m: float,
|
||||
@@ -588,10 +747,16 @@ def stone_masonry(
|
||||
# 돌쌓기 면적 = 정면적 × √(1+n²) — 기울어진 만큼 길어진다. **기울기 몫은 한 번만.**
|
||||
# 실무 시트의 「정면적 × 1.04」가 바로 이 값이다(1:0.3 에서 1.0440 ≈ 1.04).
|
||||
masonry_area = face_area * math.hypot(1.0, slope_ratio)
|
||||
thickness = (
|
||||
(constants["thickness_base_m"] + constants["thickness_top_coeff"] * height_m)
|
||||
+ (constants["thickness_base_m"] + constants["thickness_bottom_coeff"] * height_m)
|
||||
) / 2.0
|
||||
# 벽 두께 — 실무 구조물도 식(확정 2차 ②). **뒷길이가 들어간다.**
|
||||
top_thickness = back_cm / 100.0 + constants["thickness_top_add_m"]
|
||||
bottom_thickness = top_thickness + constants["thickness_slope_per_m"] * max(
|
||||
height_m - constants["thickness_height_base_m"], 0.0
|
||||
)
|
||||
thickness = (top_thickness + bottom_thickness) / 2.0
|
||||
thickness_basis = (
|
||||
f"실무 구조물도 식 — 상부 {top_thickness:.2f}(뒷길이 {back_cm}㎝ + 0.30) ·"
|
||||
f" 하부 {bottom_thickness:.2f}(상부 + 0.30×(H−1))"
|
||||
)
|
||||
volume = face_area * thickness # 입적
|
||||
|
||||
components = [
|
||||
@@ -602,6 +767,15 @@ def stone_masonry(
|
||||
DESTINATION["돌쌓기"],
|
||||
f"정면적 × √(1+{slope_ratio:g}²) — 비탈면적 · {slope_basis}",
|
||||
),
|
||||
# 입적 — 정본 계산표 좌측 열의 이름 그대로다(확정 ⑦). 값을 새로 내는 것이 아니라
|
||||
# 이미 쓰고 있던 밑수를 화면에 드러내는 줄이다.
|
||||
Component(
|
||||
"입적",
|
||||
"㎥",
|
||||
volume,
|
||||
DESTINATION["입적"],
|
||||
f"정면적 × 평균두께 {thickness:.2f} · {thickness_basis}",
|
||||
),
|
||||
]
|
||||
# ⚠ 고임돌 계수가 **원문에서 「-」**인 칸이 있다(견치돌 25·30 · 야면석 75 · 깬돌 25).
|
||||
# 그 규격에 그 돌을 안 쓴다는 뜻이라 **0 줄을 만들지 않는다** — 0 은 「없음」과
|
||||
@@ -635,7 +809,8 @@ def stone_masonry(
|
||||
"ton",
|
||||
masonry_area * stone_ton,
|
||||
DESTINATION["야면석"],
|
||||
f"돌쌓기 × {stone_ton} ton/㎡ (뒷길이 {back_cm}㎝)",
|
||||
f"돌쌓기 × {stone_ton} ton/㎡ (뒷길이 {back_cm}㎝) · {STONE_WEIGHT_BASIS}",
|
||||
source=STONE_WEIGHT_SOURCE,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -651,19 +826,22 @@ def stone_masonry(
|
||||
"㎥",
|
||||
rubble,
|
||||
DESTINATION["막자갈"],
|
||||
f"입적 − (면적×뒷길이×{body_ratio:.4g} + 고임돌)"
|
||||
f"입적 − (면적×뒷길이×{body_ratio:.4g} + 고임돌) · {thickness_basis}"
|
||||
+ (f" · {kind_label}" if kind_label else ""),
|
||||
)
|
||||
)
|
||||
|
||||
if wet:
|
||||
mpa, mpa_basis = fill_concrete_mpa(options)
|
||||
components.append(
|
||||
Component(
|
||||
"채움콘크리트",
|
||||
"㎥",
|
||||
masonry_area * _num(table["fill_concrete_m3_per_m2"]),
|
||||
DESTINATION["채움콘크리트"],
|
||||
f"돌쌓기 × {table['fill_concrete_m3_per_m2']} ㎥/㎡ (뒷길이 {back_cm}㎝)",
|
||||
f"돌쌓기 × {table['fill_concrete_m3_per_m2']} ㎥/㎡ (뒷길이 {back_cm}㎝)"
|
||||
f" · {mpa_basis}",
|
||||
spec=f"{mpa}",
|
||||
)
|
||||
)
|
||||
components.append(
|
||||
@@ -717,28 +895,22 @@ def stone_masonry(
|
||||
)
|
||||
|
||||
# 버림 콘크리트 — 기초 바닥에 까는 얇은 층. 빼려면 저장 제원에서 「안 넣음」으로 둔다.
|
||||
base_width = thickness + constants["excavation_extra_m"]
|
||||
blinding = _blinding_component(base_width, length_m, options)
|
||||
# 잡석다짐 폭 = **하단 길이**(확정 ⑪) — 벽에서는 하부 두께다. 버림도 같은 폭을 쓴다.
|
||||
base_width = bottom_thickness
|
||||
blinding = _blinding_component(
|
||||
base_width,
|
||||
length_m,
|
||||
options,
|
||||
f"하단 길이(하부 두께) {bottom_thickness:.2f}m — 확정 ⑪ 「기준 구조물에서 유도」",
|
||||
)
|
||||
if blinding is not None:
|
||||
components.append(blinding)
|
||||
|
||||
notes.append(RUBBLE_BASE_BLOCKED.format(width=base_width))
|
||||
|
||||
# 터파기·되메우기·잔토 — 토공으로 합산되는 값이다(내역 줄이 아니다).
|
||||
excavation = height_m * (thickness + constants["excavation_extra_m"]) * length_m
|
||||
backfill = height_m * constants["backfill_thickness_m"] * length_m
|
||||
components.extend(
|
||||
[
|
||||
Component(
|
||||
"터파기", "㎥", excavation, DESTINATION["터파기"], "높이 × (평균두께+0.2) × 연장"
|
||||
),
|
||||
Component("되메우기", "㎥", backfill, DESTINATION["되메우기"], "높이 × 0.2 × 연장"),
|
||||
Component(
|
||||
"잔토처리",
|
||||
"㎥",
|
||||
excavation - backfill,
|
||||
DESTINATION["잔토처리"],
|
||||
"터파기 − 되메우기",
|
||||
),
|
||||
]
|
||||
_trench_components(height_m, length_m, thickness, options, notes, thickness_basis)
|
||||
)
|
||||
return components, notes
|
||||
|
||||
@@ -1001,6 +1173,7 @@ def build_table(
|
||||
"basis": component.basis,
|
||||
"basis_kind": component.basis_kind,
|
||||
"source": component.source,
|
||||
"spec": component.spec,
|
||||
}
|
||||
for component in item.components
|
||||
],
|
||||
|
||||
@@ -217,11 +217,13 @@ class QuantitySettingsBody(BaseModel):
|
||||
# ⚠ 산식(연장÷500)으로 만들지 않는다 — 임도규정이 「필요시 거리를 조정」이라 하고
|
||||
# 기점 포함·갈림길 중복을 원문이 정하지 않는다. **설계자가 넣는 값**이다.
|
||||
ancillary_counts: dict[str, float] | None = None
|
||||
# 층따기 길이(깊이, m). 면적 × 이 값 = ㎥ (확정 2차 ①).
|
||||
bench_cut_depth_m: float | None = None
|
||||
|
||||
|
||||
#: `None` 이 「안 정함」을 뜻하는 칸 — 저장에서 **버리지 않고 그대로 덮어쓴다**.
|
||||
#: 빈 문자열로 되돌리는 칸(시공법·타설 방식)과 달리 숫자 칸은 되돌릴 값이 `None` 뿐이다.
|
||||
NULLABLE_SETTING_KEYS = ("topsoil_thickness_m",)
|
||||
NULLABLE_SETTING_KEYS = ("topsoil_thickness_m", "bench_cut_depth_m")
|
||||
|
||||
|
||||
@router.put("/{project_id}/quantity/settings")
|
||||
|
||||
@@ -203,6 +203,8 @@ async def get_handoff(project_id: UUID) -> JSONResponse:
|
||||
ground_methods={name: rock_method(settings, name) for name in rock_classes(settings)},
|
||||
# 타설 방식 — 안 정했으면 기본값으로 서되 그 사실을 `placing_notes` 가 알린다.
|
||||
concrete_placing_method=concrete_placing_method(settings)[0],
|
||||
# 층따기 길이 — 면적 × 이 값으로 ㎥ 를 낸다(확정 2차 ①). 안 넣었으면 막히고 사유가 감.
|
||||
bench_cut_depth_m=settings.get("bench_cut_depth_m"),
|
||||
)
|
||||
handoff["summary"] = summarize(handoff)
|
||||
handoff["skipped_structures"] = skipped
|
||||
|
||||
@@ -138,6 +138,10 @@ function culvertSet(
|
||||
pipe_kind: text(options.pipe_kind) ?? text(defaults.pipe_kind),
|
||||
diameter_m: pipeDiameterM(diameterMm),
|
||||
min_cover_m: MIN_PIPE_COVER_M,
|
||||
// 관 기슭막이 기초 축 — **유입·유출 한 칸으로** 받는다. 한 공사에서 한쪽만 기초유로
|
||||
// 하는 일은 드물어 폼을 둘로 늘리지 않았다(2026-09-09). 실무가 갈라 쓰는 것이
|
||||
// 확인되면 그때 나눌 것.
|
||||
foundation: text(options.revet_foundation) ?? text(defaults.revet_foundation),
|
||||
inlet: sideSpec(options, defaults, "inlet"),
|
||||
outlet: sideSpec(options, defaults, "outlet"),
|
||||
};
|
||||
@@ -174,6 +178,8 @@ function revetSet(values: Record<string, unknown>): CulvertSetSpec {
|
||||
hidden_pipe: true,
|
||||
side: String(values.side ?? "양쪽"),
|
||||
tiers: Math.trunc(num(values.tiers, 1.0) || 1),
|
||||
// 기초 축(기초유/기초버림) — 터파기 그림·물량이 같이 쓴다. 비어 있으면 안 그린다.
|
||||
foundation: text(values.foundation),
|
||||
pipe_kind: null,
|
||||
diameter_m: 0.3,
|
||||
min_cover_m: 0.0,
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
"""터파기 단면 — 관·벽 두 갈래. **근거의 급이 다르므로 갈라 둔다.**
|
||||
|
||||
⚠⚠ TS 짝 파일과 **한 벌**이다 — `common_util/common_util_excavation.ts`.
|
||||
거울 시험: `tmp/tests/test_excavation_mirror.py` (고칠 때 같이 돌릴 것).
|
||||
화면(B06 횡단도)이 그리고 서버(B08 수량)가 세는 값이라 두 쪽에 같은 수가 있어야 한다.
|
||||
|
||||
① 관 — **법정 시방서에 표가 있다**
|
||||
`KCS 44 40 10 배수관(23.01) §3.2(4) 그림 3.2-1`.
|
||||
「설계도서에 따로 표기되지 않았을 때에는 그림 3.2-1 터파기 기준 폭에 따라야 한다」
|
||||
⇒ 기본값으로 쓰고 사용자가 덮어쓰는 구조가 원문과 맞다.
|
||||
⚠ **관경 + 2b 로 계산하지 않는다** — 표가 관 두께·작업여유를 이미 담고 있다
|
||||
(Ø800 은 800+2×300=1400 이 아니라 **1600**).
|
||||
여유폭 b 는 같은 그림의 값: 보통지반 0.30 m · 연약지반 0.50 m(한쪽).
|
||||
|
||||
② 벽(돌쌓기·기슭막이) — **법정 근거 없음. 실무 정본 식이 있다**
|
||||
법령·교본은 「설계도서 지시 폭·기울기·깊이」까지만 정한다
|
||||
(지식DB `01_임도/02_상세설계/구조물/구조물_기초_토공.md` §1 — 전수 확인).
|
||||
실무 정본 `original/구조물도/기슭막이/04.구조도(기슭막이).xls` 7탭:
|
||||
|
||||
터파기 = 기초 0.5 × (0.7 + 0.2) × 1.0 = 0.45 (버림만 있는 탭은 0.1 × 0.7 = 0.07)
|
||||
+ 비탈 H × (평균두께 + 0.2) × 1.0
|
||||
|
||||
⇒ **여유폭은 「+0.2」**(평균두께에 더함), **기초는 깊이 0.5 · 폭 0.9**.
|
||||
⇒ 정본 탭 제목이 **「기초유 / 기초버림」**이다 — 기초를 안 두는 것이 아니라
|
||||
**버림으로 얕게 두는 것**이라 뜻이 다르다. 그 축이 곧 **0.45 냐 0.07 이냐**를 가른다.
|
||||
⚠ **비탈을 주지 않는다 — 정본이 수직으로 센다.** 기울기 항이 식에 아예 없다.
|
||||
임의로 기울기를 주면 그림과 물량이 어긋난다.
|
||||
|
||||
⚠ 값을 지어내지 않는다 — 여기 있는 수는 전부 위 두 출처의 것이고, 사용자가 덮어쓰면
|
||||
그 값이 이긴다. 화면에는 **근거의 급**(법정 표 / 실무 식)을 노티스로 드러낸다.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
#: 관경(㎜) → 터파기 폭(㎜). `KCS 44 40 10` 그림 3.2-1 표 그대로.
|
||||
PIPE_TRENCH_WIDTH_MM: dict[int, int] = {
|
||||
300: 700,
|
||||
350: 800,
|
||||
400: 900,
|
||||
450: 1000,
|
||||
500: 1100,
|
||||
600: 1200,
|
||||
700: 1400,
|
||||
800: 1600,
|
||||
900: 1700,
|
||||
1000: 1800,
|
||||
1100: 2000,
|
||||
1200: 2100,
|
||||
1300: 2200,
|
||||
1400: 2400,
|
||||
1500: 2500,
|
||||
}
|
||||
|
||||
#: 한쪽 여유폭(m) — 같은 그림. 표에 없는 관경을 사용자가 넣었을 때의 안내용이다.
|
||||
PIPE_TRENCH_CLEARANCE_NORMAL_M = 0.30
|
||||
PIPE_TRENCH_CLEARANCE_SOFT_M = 0.50
|
||||
|
||||
#: 벽 터파기 — 실무 정본 식의 수(위 문서 참조).
|
||||
WALL_TRENCH_CLEARANCE_M = 0.2 # 평균두께에 더하는 여유
|
||||
WALL_FOUNDATION_DEPTH_M = 0.5 # 기초 깊이
|
||||
WALL_FOUNDATION_WIDTH_M = 0.9 # 기초 폭 (0.7 + 0.2)
|
||||
WALL_BLINDING_DEPTH_M = 0.1 # 기초 없이 버림만 있을 때의 깊이
|
||||
WALL_BLINDING_WIDTH_M = 0.7 # 그때의 폭 (여유폭 0.0 — 버림 탭에는 여유가 없다)
|
||||
|
||||
#: 근거의 급 — 화면 노티스가 이 값을 그대로 쓴다.
|
||||
BASIS_PIPE = "법정 표(KCS 44 40 10 그림 3.2-1) — 설계도서에 따로 없으면 이 값"
|
||||
BASIS_WALL = "법정 근거 없음 · 실무 정본 식(구조도 기슭막이 xls) 기본값 — 확인 후 쓸 것"
|
||||
|
||||
|
||||
def pipe_trench_width_m(diameter_mm: float | int | None) -> float | None:
|
||||
"""관경(㎜)에 대응하는 터파기 폭(m). 표에 없으면 `None`(지어내지 않는다)."""
|
||||
if diameter_mm is None:
|
||||
return None
|
||||
try:
|
||||
key = int(round(float(diameter_mm)))
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
width = PIPE_TRENCH_WIDTH_MM.get(key)
|
||||
return None if width is None else width / 1000.0
|
||||
|
||||
|
||||
def wall_trench_area_m2(
|
||||
height_m: float | None,
|
||||
average_thickness_m: float | None,
|
||||
*,
|
||||
has_foundation: bool,
|
||||
) -> float | None:
|
||||
"""벽 터파기 단면적(㎡/m). 높이나 평균두께가 없으면 `None`.
|
||||
|
||||
실무 정본 식 그대로: `기초분 + H × (평균두께 + 0.2)`.
|
||||
`has_foundation=False` 면 기초분이 **버림 몫(0.1 × 0.7)**으로 줄어든다.
|
||||
"""
|
||||
if height_m is None or average_thickness_m is None:
|
||||
return None
|
||||
try:
|
||||
height = float(height_m)
|
||||
thickness = float(average_thickness_m)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
if height <= 0 or thickness <= 0:
|
||||
return None
|
||||
base = (
|
||||
WALL_FOUNDATION_DEPTH_M * WALL_FOUNDATION_WIDTH_M
|
||||
if has_foundation
|
||||
else WALL_BLINDING_DEPTH_M * WALL_BLINDING_WIDTH_M
|
||||
)
|
||||
return base + height * (thickness + WALL_TRENCH_CLEARANCE_M)
|
||||
|
||||
|
||||
def wall_trench_width_m(average_thickness_m: float | None) -> float | None:
|
||||
"""벽 터파기 폭(m) — 평균두께 + 여유 0.2. 값이 없으면 `None`."""
|
||||
if average_thickness_m is None:
|
||||
return None
|
||||
try:
|
||||
thickness = float(average_thickness_m)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
return None if thickness <= 0 else thickness + WALL_TRENCH_CLEARANCE_M
|
||||
@@ -0,0 +1,88 @@
|
||||
/* =============================================================================
|
||||
* common_util_excavation.ts
|
||||
* 터파기 단면 — 관·벽 두 갈래. **근거의 급이 다르므로 갈라 둔다.**
|
||||
*
|
||||
* ⚠⚠ 파이썬 짝과 **한 벌**이다 — `common_util/common_util_excavation.py`.
|
||||
* 거울 시험: `tmp/tests/test_excavation_mirror.py`. 어느 쪽을 고치든 같이 돌릴 것.
|
||||
* 화면(B06 횡단도)이 그리고 서버(B08 수량)가 세는 값이라 두 쪽 수가 같아야 한다.
|
||||
*
|
||||
* ① 관 — 법정 시방서에 표가 있다: `KCS 44 40 10 배수관(23.01) §3.2(4) 그림 3.2-1`.
|
||||
* 「설계도서에 따로 표기되지 않았을 때에는 … 기준 폭에 따라야 한다」
|
||||
* ⚠ **관경 + 2b 로 계산하지 않는다** — 표가 관 두께·작업여유를 이미 담고 있다.
|
||||
* ② 벽 — 법정 근거 없음. 실무 정본 식(`구조도(기슭막이).xls` 7탭):
|
||||
* 터파기 = 기초 0.5 × 0.9 (기초버림이면 0.1 × 0.7) + H × (평균두께 + 0.2)
|
||||
* ⚠ **비탈을 주지 않는다 — 정본이 수직으로 센다.** 축 이름은 「기초유 / 기초버림」이다.
|
||||
* ========================================================================== */
|
||||
|
||||
/** 관경(㎜) → 터파기 폭(㎜). KCS 44 40 10 그림 3.2-1 표 그대로. */
|
||||
export const PIPE_TRENCH_WIDTH_MM: Record<number, number> = {
|
||||
300: 700,
|
||||
350: 800,
|
||||
400: 900,
|
||||
450: 1000,
|
||||
500: 1100,
|
||||
600: 1200,
|
||||
700: 1400,
|
||||
800: 1600,
|
||||
900: 1700,
|
||||
1000: 1800,
|
||||
1100: 2000,
|
||||
1200: 2100,
|
||||
1300: 2200,
|
||||
1400: 2400,
|
||||
1500: 2500,
|
||||
};
|
||||
|
||||
/** 한쪽 여유폭(m) — 같은 그림. 표에 없는 관경을 넣었을 때의 안내용. */
|
||||
export const PIPE_TRENCH_CLEARANCE_NORMAL_M = 0.3;
|
||||
export const PIPE_TRENCH_CLEARANCE_SOFT_M = 0.5;
|
||||
|
||||
/** 벽 터파기 — 실무 정본 식의 수. */
|
||||
export const WALL_TRENCH_CLEARANCE_M = 0.2;
|
||||
export const WALL_FOUNDATION_DEPTH_M = 0.5;
|
||||
export const WALL_FOUNDATION_WIDTH_M = 0.9;
|
||||
export const WALL_BLINDING_DEPTH_M = 0.1;
|
||||
export const WALL_BLINDING_WIDTH_M = 0.7;
|
||||
|
||||
/** 근거의 급 — 화면 노티스가 이 문구를 그대로 쓴다. */
|
||||
export const BASIS_PIPE = "법정 표(KCS 44 40 10 그림 3.2-1) — 설계도서에 따로 없으면 이 값";
|
||||
export const BASIS_WALL =
|
||||
"법정 근거 없음 · 실무 정본 식(구조도 기슭막이 xls) 기본값 — 확인 후 쓸 것";
|
||||
|
||||
/** 관경(㎜)에 대응하는 터파기 폭(m). 표에 없으면 `null`(지어내지 않는다). */
|
||||
export function pipeTrenchWidthM(diameterMm: number | null | undefined): number | null {
|
||||
if (diameterMm === null || diameterMm === undefined) return null;
|
||||
const key = Math.round(Number(diameterMm));
|
||||
if (!Number.isFinite(key)) return null;
|
||||
const width = PIPE_TRENCH_WIDTH_MM[key];
|
||||
return width === undefined ? null : width / 1000;
|
||||
}
|
||||
|
||||
/**
|
||||
* 벽 터파기 단면적(㎡/m). 높이·평균두께가 없으면 `null`.
|
||||
* `hasFoundation=false` 면 기초분이 버림 몫(0.1 × 0.7)으로 줄어든다.
|
||||
*/
|
||||
export function wallTrenchAreaM2(
|
||||
heightM: number | null | undefined,
|
||||
averageThicknessM: number | null | undefined,
|
||||
hasFoundation: boolean,
|
||||
): number | null {
|
||||
if (heightM === null || heightM === undefined) return null;
|
||||
if (averageThicknessM === null || averageThicknessM === undefined) return null;
|
||||
const height = Number(heightM);
|
||||
const thickness = Number(averageThicknessM);
|
||||
if (!Number.isFinite(height) || !Number.isFinite(thickness)) return null;
|
||||
if (height <= 0 || thickness <= 0) return null;
|
||||
const base = hasFoundation
|
||||
? WALL_FOUNDATION_DEPTH_M * WALL_FOUNDATION_WIDTH_M
|
||||
: WALL_BLINDING_DEPTH_M * WALL_BLINDING_WIDTH_M;
|
||||
return base + height * (thickness + WALL_TRENCH_CLEARANCE_M);
|
||||
}
|
||||
|
||||
/** 벽 터파기 폭(m) — 평균두께 + 여유 0.2. 값이 없으면 `null`. */
|
||||
export function wallTrenchWidthM(averageThicknessM: number | null | undefined): number | null {
|
||||
if (averageThicknessM === null || averageThicknessM === undefined) return null;
|
||||
const thickness = Number(averageThicknessM);
|
||||
if (!Number.isFinite(thickness) || thickness <= 0) return null;
|
||||
return thickness + WALL_TRENCH_CLEARANCE_M;
|
||||
}
|
||||
@@ -109,6 +109,12 @@ def default_settings() -> dict[str, Any]:
|
||||
# ⇒ `ceil(연장÷500)` 을 확정 산식으로 쓰지 않고 **개소를 받는다.**
|
||||
# 비워 두면 물량을 안 낸다(0 으로 때우지 않음).
|
||||
"ancillary_counts": {},
|
||||
# 층따기 길이(깊이, m) — ⭐ 2026-09-09 사용자 확정 2차 ①.
|
||||
# ⚠ **면적이 정본**이고 부피는 **면적 × 이 길이**로 낸다. 그러면 품셈 ㎥ 단가를
|
||||
# 그대로 쓸 수 있다(단위 불일치가 풀림).
|
||||
# ⚠ 기본값을 두지 않는다 — 안 넣으면 물량을 안 낸다(0 으로 때우지 않음).
|
||||
# 교본이 「층따기 높이·폭은 **설계도서에 명시**」라 해 설계 입력이다.
|
||||
"bench_cut_depth_m": None,
|
||||
"dataset_versions": {},
|
||||
},
|
||||
"estimation": {
|
||||
|
||||
@@ -75,9 +75,10 @@
|
||||
"group": "층따기",
|
||||
"work_item_code": "FP-09-18",
|
||||
"master_name": "층따기",
|
||||
"basis_unit": "㎡",
|
||||
"basis_source": "⭐ 2026-09-09 사용자 확정 ⑦ — 층따기 수량 단위는 **㎡**(실무 관행). 밑수는 **원지반 표면의 경사길이**(B06 `design.bench_cut_length_m`)를 평균단면적법으로 면적화한 값이다. ⚠ 품셈 9-18 [주]는 `Q1 = 3600×q×K×f×E/㎝ = ㎥/시간` 이라 **공식은 체적 기준**이다 — 단의 높이·폭이 설계도서 값이라 체적을 지어낼 수 없어 면적으로 간다. ⇒ **받는 쪽(B09)이 ㎡ 단가를 세워야 한다**(㎥ 단가를 그대로 곱하면 금액이 틀림).",
|
||||
"unit_note": "앞서 2026-09-08 에는 이 어긋남을 「막힘」으로 두어 금액이 안 섰다(410만원 자리). 사용자 확정으로 ㎡ 가 정본이 되었으므로 막지 않는다."
|
||||
"basis_unit": "㎥",
|
||||
"basis_source": "품셈 9-18 [주] 「Q1 = 3600×q×K×f×E/㎝ = ㎥/시간」 — 공식이 체적 기준이다. ⭐ 2026-09-09 사용자 확정 2차 ① 로 **면적(㎡)을 정본으로 두고 사용자가 넣은 길이를 곱해 ㎥ 를 낸다** — 그러면 품셈 ㎥ 단가를 그대로 쓸 수 있다(단위 불일치가 풀림).",
|
||||
"mismatch_reason": "층따기 길이(깊이)가 아직 입력되지 않았습니다 — 산출 조건에서 넣으면 면적 × 길이로 물량이 섭니다(면적은 이미 섰습니다).",
|
||||
"mismatch_kind": "input_missing"
|
||||
},
|
||||
{
|
||||
"group": "면고르기",
|
||||
|
||||
Reference in New Issue
Block a user