Merge remote-tracking branch 'origin/main_desktop_1' into sub_desktop_1
This commit is contained in:
@@ -1010,7 +1010,16 @@
|
||||
"key": "form",
|
||||
"label": "형식",
|
||||
"input": "select",
|
||||
"choices": ["콘크리트", "돌(찰)", "돌(메)", "블록쌓기", "돌망태", "흙포대", "통나무쌓기"],
|
||||
"choices": [
|
||||
"콘크리트",
|
||||
"돌(찰)",
|
||||
"돌(메)",
|
||||
"블록쌓기",
|
||||
"돌망태",
|
||||
"흙포대",
|
||||
"통나무쌓기",
|
||||
"떼"
|
||||
],
|
||||
"default": null,
|
||||
"required": true,
|
||||
"phase": "detail"
|
||||
@@ -1273,6 +1282,24 @@
|
||||
"required": true,
|
||||
"phase": "detail"
|
||||
},
|
||||
{
|
||||
"key": "top_length_m",
|
||||
"label": "상장 ⓐ (윗변)",
|
||||
"input": "number",
|
||||
"unit": "m",
|
||||
"default": null,
|
||||
"required": true,
|
||||
"phase": "detail"
|
||||
},
|
||||
{
|
||||
"key": "bottom_length_m",
|
||||
"label": "하장 ⓑ (아랫변)",
|
||||
"input": "number",
|
||||
"unit": "m",
|
||||
"default": null,
|
||||
"required": true,
|
||||
"phase": "detail"
|
||||
},
|
||||
{
|
||||
"key": "length_m",
|
||||
"label": "길이",
|
||||
|
||||
@@ -453,6 +453,25 @@ export interface CrossDesign {
|
||||
/** 암반부에 적용할 지반유형(`ripping_rock`/`blasting_rock`). 토사 측점은 null. */
|
||||
cut_rock_kind?: GroundType | null;
|
||||
fill_area_m2: number;
|
||||
/**
|
||||
* 사토장(유용토운반작업장) 몫 — **`fill_area_m2` 와 합치지 않는다**(2026-09-09 확정 ㉠).
|
||||
* 노면 끝 바깥은 노선 성토가 아니라 사토장 성토라, 받는 쪽이 갈라 볼 수 있어야 한다.
|
||||
* 사토장이 없는 측점은 전부 0·null 이다(구 데이터에는 아예 없어 optional).
|
||||
*/
|
||||
spoil_fill_area_m2?: number;
|
||||
spoil_fill_side?: "left" | "right" | null;
|
||||
spoil_fill_width_m?: number;
|
||||
spoil_fill_max_width_m?: number;
|
||||
spoil_fill_line?: Array<{ offset_m: number; elevation_m: number }>;
|
||||
spoil_fill_unclosed?: boolean;
|
||||
/** 사토장이 대신 차지해 노선 성토에서 뺀 몫(㎡) — 되짚기용. 합계에 또 넣지 말 것. */
|
||||
spoil_fill_replaced_fill_m2?: number;
|
||||
/** 구간(구조물) 단위 값 — 측점마다 같은 값이 실린다. 말풍선·수량이 되짚는 데 쓴다. */
|
||||
spoil_fill_capacity_m3?: number;
|
||||
spoil_fill_placed_m3?: number;
|
||||
spoil_fill_unplaced_m3?: number;
|
||||
spoil_fill_structure_id?: string | null;
|
||||
spoil_fill_extra_distance_m?: number | null;
|
||||
/** 성토측 자연 지반 경사(rise/run). 자연방토 판정 입력. 성토측이 없으면 null. */
|
||||
fill_ground_slope?: number | null;
|
||||
/**
|
||||
|
||||
@@ -89,7 +89,23 @@ export const USER_TOUCHED_KEYS = [
|
||||
] as const;
|
||||
|
||||
/** 다시 계산해도 살려 두는 값 — 위 사용자 값에 **상태를 나르는 둘**을 더한 것. */
|
||||
const PRESERVED_KEYS = ["status", "pavement_suggested", ...USER_TOUCHED_KEYS] as const;
|
||||
// 사토장 구간값 — **브라우저가 못 만드는 값**이라 이어 붙인다. 구간 전체를 봐야 나오는
|
||||
// 값이고(용량 배분) 정본은 [저장] 때 서버가 다시 낸다. 안 이으면 계획선을 만지는 순간
|
||||
// 말풍선에서 「구간 용량 …」이 사라진다.
|
||||
const SPOIL_KEYS = [
|
||||
"spoil_fill_capacity_m3",
|
||||
"spoil_fill_placed_m3",
|
||||
"spoil_fill_unplaced_m3",
|
||||
"spoil_fill_structure_id",
|
||||
"spoil_fill_extra_distance_m",
|
||||
] as const;
|
||||
|
||||
const PRESERVED_KEYS = [
|
||||
"status",
|
||||
"pavement_suggested",
|
||||
...USER_TOUCHED_KEYS,
|
||||
...SPOIL_KEYS,
|
||||
] as const;
|
||||
|
||||
function preserveUserFields(
|
||||
next: NonNullable<CrossSection["design"]>,
|
||||
@@ -281,6 +297,19 @@ function refreshLocally(input: CrossRefreshInput): number[] | null {
|
||||
? section.curve_outer_side
|
||||
: null,
|
||||
curveWideningM: section.curve_widening_m ?? null,
|
||||
// 사토장 — **저장분 폭을 그대로 잇는다**. 폭은 구간 용량에서 서버가 정한 값이라
|
||||
// 브라우저가 다시 풀지 않는다(다시 풀면 그 측점만 폭이 달라져 작업장 모양이 깨진다).
|
||||
// 이어 붙이지 않으면 계획선을 만질 때마다 사토장이 그림에서 사라진다.
|
||||
spoilFill:
|
||||
typeof design.spoil_fill_width_m === "number" &&
|
||||
design.spoil_fill_width_m > 0 &&
|
||||
(design.spoil_fill_side === "left" || design.spoil_fill_side === "right")
|
||||
? {
|
||||
side: design.spoil_fill_side,
|
||||
widthM: design.spoil_fill_width_m,
|
||||
slopeRatioN: null,
|
||||
}
|
||||
: null,
|
||||
},
|
||||
);
|
||||
} catch {
|
||||
|
||||
@@ -375,6 +375,21 @@ export function buildAreaReadout(
|
||||
],
|
||||
},
|
||||
];
|
||||
// 사토장이 선 측점만 한 줄 더 — **성토와 합치지 않는다**(확정 ㉠). 없는 측점에 빈 줄을
|
||||
// 세우면 어느 측점에 사토장이 있는지 표에서 안 보인다.
|
||||
const spoilArea = Number(design.spoil_fill_area_m2 ?? 0);
|
||||
if (spoilArea > 0) {
|
||||
rows.push({
|
||||
label: L("B06_Design_SpoilFill_Area"),
|
||||
variant: "spoil-fill",
|
||||
cells: [
|
||||
{ key: null, value: null, variant: "spoil-fill" },
|
||||
{ key: null, value: null, variant: "spoil-fill" },
|
||||
{ key: null, value: null, variant: "spoil-fill" },
|
||||
{ key: null, value: spoilArea, variant: "spoil-fill" },
|
||||
],
|
||||
});
|
||||
}
|
||||
for (const row of rows) {
|
||||
const tr = document.createElement("tr");
|
||||
const th = document.createElement("th");
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
/* =============================================================================
|
||||
* B06_Section_UI_Cross_SpoilFill.ts
|
||||
* 횡단도에 **사토장(유용토운반작업장) 성토선**을 얹는다.
|
||||
*
|
||||
* 왜 있나 — 임도기술교본 6장 3절이 **운반처리 위치를 평면도와 횡단도에 표시**하도록
|
||||
* 요구한다(지식DB `01_임도/02_상세설계/유용토운반작업장.md` §2). 수량을 세기 전에
|
||||
* 도면에 그것이 보여야 한다.
|
||||
*
|
||||
* ⚠ **터파기 파선과 선 종류를 가른다** — 터파기는 짧은 점선, 사토장은 긴 파선+점.
|
||||
* 같은 파선으로 두면 도면에서 둘을 구별할 수 없다(2026-09-09 네 창 확정).
|
||||
* ⚠ 값은 설계 결과(`spoil_fill_*`)에서 온다 — 여기서 **다시 세지 않는다**.
|
||||
* ========================================================================== */
|
||||
|
||||
const SVG_NS = "http://www.w3.org/2000/svg";
|
||||
|
||||
/** 설계 결과에서 사토장 그리기에 쓰는 값만 추려 받는다. */
|
||||
export interface SpoilFillDrawing {
|
||||
spoil_fill_line?: Array<{ offset_m: number; elevation_m: number }> | null;
|
||||
spoil_fill_area_m2?: number | null;
|
||||
spoil_fill_width_m?: number | null;
|
||||
spoil_fill_unclosed?: boolean | null;
|
||||
spoil_fill_capacity_m3?: number | null;
|
||||
spoil_fill_placed_m3?: number | null;
|
||||
spoil_fill_unplaced_m3?: number | null;
|
||||
}
|
||||
|
||||
/** 말풍선 문구 — 무엇이 얼마나 쌓였는지와 **근거**를 함께 적는다. */
|
||||
export function spoilFillTooltip(design: SpoilFillDrawing): string {
|
||||
const area = Number(design.spoil_fill_area_m2 ?? 0);
|
||||
const width = Number(design.spoil_fill_width_m ?? 0);
|
||||
const lines = [
|
||||
`유용토운반작업장(구 사토장) · 단면 ${area.toFixed(2)}㎡ · 폭 ${width.toFixed(2)}m`,
|
||||
];
|
||||
const capacity = design.spoil_fill_capacity_m3;
|
||||
if (typeof capacity === "number" && capacity > 0) {
|
||||
const placed = Number(design.spoil_fill_placed_m3 ?? 0);
|
||||
lines.push(`구간 용량 ${capacity.toFixed(1)}㎥ 중 ${placed.toFixed(1)}㎥ 담김`);
|
||||
const unplaced = Number(design.spoil_fill_unplaced_m3 ?? 0);
|
||||
if (unplaced > 0) {
|
||||
lines.push(`⚠ ${unplaced.toFixed(1)}㎥ 는 못 담음 — 지반 자료가 있는 데까지만 넓힘`);
|
||||
}
|
||||
}
|
||||
if (design.spoil_fill_unclosed) {
|
||||
lines.push("⚠ 비탈이 원지반을 못 만나 잘림 — 지반 자료 범위를 넘어감");
|
||||
}
|
||||
lines.push("폭은 노면 끝(노견이 시작하는 자리)에서 잼 — 그 구간 노견도 이 성토 안에 듦");
|
||||
lines.push("교본 6장 3절이 평면도·횡단도 표시를 요구함");
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* 사토장 성토선을 그린다. 선이 없으면 아무것도 하지 않는다.
|
||||
* 돌려주는 값은 그린 폴리라인(없으면 `null`) — 부르는 쪽이 강조에 쓸 수 있다.
|
||||
*/
|
||||
export function appendSpoilFillOverlay(
|
||||
layer: SVGElement,
|
||||
design: SpoilFillDrawing | null | undefined,
|
||||
x: (offset: number) => number,
|
||||
toDisplayY: (elevation: number) => number,
|
||||
): SVGElement | null {
|
||||
const line = design?.spoil_fill_line;
|
||||
if (!design || !Array.isArray(line) || line.length < 2) return null;
|
||||
const polyline = document.createElementNS(SVG_NS, "polyline");
|
||||
polyline.setAttribute(
|
||||
"points",
|
||||
line.map((point) => `${x(point.offset_m)},${toDisplayY(point.elevation_m)}`).join(" "),
|
||||
);
|
||||
polyline.setAttribute(
|
||||
"class",
|
||||
design.spoil_fill_unclosed ? "b06-chart__spoil-fill is-unclosed" : "b06-chart__spoil-fill",
|
||||
);
|
||||
const title = document.createElementNS(SVG_NS, "title");
|
||||
title.textContent = spoilFillTooltip(design);
|
||||
polyline.append(title);
|
||||
layer.append(polyline);
|
||||
|
||||
// 이름표 — 평상 한가운데 위에 얹는다. 선만 있으면 그것이 무엇인지 도면에서 모른다.
|
||||
const first = line[0];
|
||||
const last = line[line.length - 1];
|
||||
const label = document.createElementNS(SVG_NS, "text");
|
||||
label.setAttribute("x", String((x(first.offset_m) + x(last.offset_m)) / 2));
|
||||
label.setAttribute("y", String(toDisplayY(Math.max(first.elevation_m, last.elevation_m)) - 4));
|
||||
label.setAttribute("text-anchor", "middle");
|
||||
label.setAttribute("class", "b06-chart__spoil-fill-label");
|
||||
label.textContent = `유용토운반작업장 ${Number(design.spoil_fill_area_m2 ?? 0).toFixed(2)}㎡`;
|
||||
const labelTitle = document.createElementNS(SVG_NS, "title");
|
||||
labelTitle.textContent = spoilFillTooltip(design);
|
||||
label.append(labelTitle);
|
||||
layer.append(label);
|
||||
return polyline;
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
appendFordSurfaceDropPlan,
|
||||
} from "./B06_Section_UI_Cross_Ford_Pavement";
|
||||
import { culvertWallsStandAt } from "./B06_Section_UI_Cross_Culvert_Wire";
|
||||
import { appendSpoilFillOverlay } from "./B06_Section_UI_Cross_SpoilFill";
|
||||
import { appendRevetmentOverlay, computeRevetmentLayout } from "./B06_Section_UI_Cross_Revetment";
|
||||
import {
|
||||
appendCrossDesignOverlay,
|
||||
@@ -467,6 +468,9 @@ export function createCrossSectionCard(
|
||||
// 같은 트림 값을 쓰므로 "그림은 이런데 수량은 저렇다"가 생기지 않는다.
|
||||
applyStructureAreas(section, designTrim);
|
||||
appendCrossDesignOverlay(plotLayer, section.design, x, toDisplayY, drawSamples, designTrim);
|
||||
// 사토장(유용토운반작업장) — 교본 6장 3절이 횡단도 표시를 요구한다. 값은 설계 결과에서
|
||||
// 그대로 오고 여기서 다시 세지 않는다. 선 종류는 터파기 파선과 갈라 둔다.
|
||||
appendSpoilFillOverlay(plotLayer, section.design, x, toDisplayY);
|
||||
// 암 경계선 = 지면선 복사 + 오프셋(계획선 기준 아님).
|
||||
if (rockBoundary && section.design.geometry_preset === "rock") {
|
||||
appendRockBoundaryOverlay(
|
||||
|
||||
@@ -71,6 +71,11 @@
|
||||
color: var(--color-chart-0);
|
||||
}
|
||||
|
||||
/* 사토장 — 성토와 **다른 색**으로 둔다. 같은 색이면 표에서 한 덩어리로 읽힌다. */
|
||||
.b06-design__area--spoil-fill {
|
||||
color: #6d7a20;
|
||||
}
|
||||
|
||||
.b06-design__area--unset {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
@@ -388,3 +393,26 @@
|
||||
stroke-dasharray: 4 3;
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
/* 사토장(유용토운반작업장) — **터파기 파선과 선 종류를 가른다**(2026-09-09).
|
||||
터파기는 짧은 점선(4 3), 사토장은 **긴 파선 + 점**(9 3 2 3)이라 한눈에 갈린다.
|
||||
색은 등록부 사토장 색(#8c9a2e)을 그대로 쓴다. */
|
||||
.b06-chart__spoil-fill {
|
||||
fill: none;
|
||||
stroke: #8c9a2e;
|
||||
stroke-dasharray: 9 3 2 3;
|
||||
stroke-width: 1.4;
|
||||
}
|
||||
|
||||
.b06-chart__spoil-fill-label {
|
||||
fill: #6d7a20;
|
||||
font-size: 10px;
|
||||
paint-order: stroke;
|
||||
stroke: rgba(255, 255, 255, 0.9);
|
||||
stroke-width: 3;
|
||||
}
|
||||
|
||||
/* 지반을 못 만나 잘린 사토장 — 사면 미폐합 경고와 같은 결로 붉게 알린다. */
|
||||
.b06-chart__spoil-fill.is-unclosed {
|
||||
stroke: #c0392b;
|
||||
}
|
||||
|
||||
@@ -301,6 +301,22 @@ SOURCE_BASIS_NOTE_RE = re.compile(r"([\d,]*\.?\d*)\s*개소\s*사용\s*당")
|
||||
SOURCE_NOTE_LOOKAHEAD = 8
|
||||
|
||||
|
||||
#: ⚠⚠ **「인」은 품의 단위이지 공종 밑수가 아니다**(2026-09-09 원문 대조로 넷이 오독으로 드러남).
|
||||
#: 8-6-2 드론방제·8-6-3 지상방제 「(단위 : 인)」 — 그 「인」은 **소요인력**이고 공종 밑수는
|
||||
#: ha 다. 13-2-4 야면석 채집 「(단위: 인 당)」 — 표 안이 **㎡당·㎥당** 두 줄이다.
|
||||
#: ⇒ **원문에 「<숫자>인당」이 그대로 있을 때만** 밑수로 인정한다.
|
||||
#: ⚠ **넓게 「인」을 통째로 버리면 2-2-5 천공기가 사라진다** — 그 표는 셀 안에 「천공인부
|
||||
#: **1인당** 1대」로 숫자가 붙어 있다. 그래서 「숫자가 붙었나」로 좁게 가른다.
|
||||
PERSON_UNITS = {"인", "공"}
|
||||
|
||||
|
||||
def person_basis_ok(raw_quantity: str | None, unit: str | None) -> bool:
|
||||
"""「인」 계열 밑수를 인정할 것인가 — **숫자가 붙어 있을 때만** 참."""
|
||||
if unit not in PERSON_UNITS:
|
||||
return True
|
||||
return bool((raw_quantity or "").strip())
|
||||
|
||||
|
||||
def basis_from_source(lines: list[str], line_no: int) -> tuple[float | None, str | None]:
|
||||
"""표 바로 위 본문에서 밑수를 읽는다. 못 찾으면 `(None, None)` — 1 로 단정하지 않는다.
|
||||
|
||||
@@ -315,6 +331,8 @@ def basis_from_source(lines: list[str], line_no: int) -> tuple[float | None, str
|
||||
break # 앞 표에 닿았다 — 그 위는 남의 밑수다
|
||||
if m := SOURCE_BASIS_RATIO_RE.search(text):
|
||||
raw = (m.group(1) or "").replace(",", "")
|
||||
if not person_basis_ok(raw, m.group(2)):
|
||||
continue # 「인」에 숫자가 안 붙었다 — 품의 단위이지 밑수가 아니다
|
||||
try:
|
||||
quantity = float(raw) if raw else 1.0
|
||||
except ValueError:
|
||||
@@ -324,6 +342,8 @@ def basis_from_source(lines: list[str], line_no: int) -> tuple[float | None, str
|
||||
unit = m.group("u1") or m.group("u2")
|
||||
raw = (m.group(1) if m.group("u1") else m.group(3)) or ""
|
||||
raw = raw.replace(",", "")
|
||||
if not person_basis_ok(raw, unit):
|
||||
continue # 위와 같음 — 「(단위 : 인)」·「(단위: 인 당)」은 밑수가 아니다
|
||||
try:
|
||||
quantity = float(raw) if raw else 1.0
|
||||
except ValueError:
|
||||
@@ -347,14 +367,29 @@ def basis_from_source(lines: list[str], line_no: int) -> tuple[float | None, str
|
||||
|
||||
|
||||
def detect_basis(table: dict[str, Any]) -> tuple[float | None, str | None]:
|
||||
"""「100㎥당」 같은 밑수. 없으면 `(None, None)` — 단위당 1 로 단정하지 않는다."""
|
||||
hay = " ".join(norm(h) for h in table.get("headers", []))
|
||||
hay += " " + " ".join(norm(c) for r in table.get("rows", [])[:2] for c in r)
|
||||
if m := BASIS_RE.search(hay):
|
||||
try:
|
||||
return float(m.group(1).replace(",", "")), m.group(2)
|
||||
except ValueError:
|
||||
return None, m.group(2)
|
||||
"""「100㎥당」 같은 밑수. 없으면 `(None, None)` — 단위당 1 로 단정하지 않는다.
|
||||
|
||||
⚠⚠ **칸 하나 안에서만 본다**(2026-09-09). 여러 칸을 이어 붙여 보면 **앞 칸의 값**과
|
||||
**뒤 칸의 단위**가 붙어 없는 밑수가 생긴다 — 13-2-4 야면석 채집이 그랬다:
|
||||
행 ① 인 부 | ㎡당 | 0.11 | 0.17 | 0.22 | 0.28 | **0.36**
|
||||
행 ② **㎥당** | 0.60 | …
|
||||
이어 붙이면 「0.36 ㎥당」이 되어 **밑수 0.36㎥** 라는 값이 선다(원문에 없는 값).
|
||||
⚠ 그 표는 **㎡당·㎥당 두 줄**이라 애초에 하나로 못 정한다 — 「미확보」가 정직하다.
|
||||
⚠ **「인」은 여기서도 숫자가 붙어야 인정한다** — `BASIS_RE` 가 숫자를 요구하므로
|
||||
2-2-5 「천공인부 **1인당** 1대」는 그대로 서고 「(단위 : 인)」은 안 선다.
|
||||
"""
|
||||
cells = [norm(h) for h in table.get("headers", []) or []]
|
||||
cells += [norm(c) for r in (table.get("rows", []) or [])[:2] for c in r]
|
||||
for cell in cells:
|
||||
if not cell:
|
||||
continue
|
||||
if m := BASIS_RE.search(cell):
|
||||
if not person_basis_ok(m.group(1), m.group(2)):
|
||||
continue
|
||||
try:
|
||||
return float(m.group(1).replace(",", "")), m.group(2)
|
||||
except ValueError:
|
||||
return None, m.group(2)
|
||||
return None, None
|
||||
|
||||
|
||||
|
||||
@@ -170,13 +170,14 @@ def stone_coefficients(options: dict[str, Any], back_cm: int) -> tuple[dict[str,
|
||||
|
||||
|
||||
# 돌쌓기 전개식의 상수 — 실무 수식에 박혀 있던 값을 뺀 것.
|
||||
# ⚠⚠ **기슭막이 계열 전용이다. 다른 구조물로 넓히지 말 것**(2026-09-09).
|
||||
# 골막이는 **두께식부터 다르다** — 소광리 숨김탭 「골막이(찰)(치수조서연결)」 C26:
|
||||
# 평균두께 = {(0.45 + 0.1×H) + (0.45 + 0.4×H)} ÷ 2
|
||||
# 우리가 확정 ② 로 버린 옛 식과 **글자까지 같다.** 그 식은 틀린 식이 아니라 **골막이 식**이고,
|
||||
# 그것을 기슭막이에까지 쓰고 있었던 것이 어긋남의 정체였다.
|
||||
# ⚠ 골막이는 그 밖에도 셋이 더 다르다 — 밑수가 **돌쌓기 + 돌붙임**(기슭막이는 돌쌓기만) ·
|
||||
# 단위가 **개소당**(기슭막이는 m당) · 정면적이 **사다리꼴**. **상수만 보고 가져다 쓰지 말 것.**
|
||||
#: ⚠⚠ **기슭막이 계열(돌쌓기 찰·메) 전용이다. 다른 구조물로 넓히지 말 것**(2026-09-09).
|
||||
#: 골막이는 **두께식부터 다르다** — 소광리 숨김탭 「골막이(찰)(치수조서연결)」 C26:
|
||||
#: 평균두께 = {(0.45 + 0.1×H) + (0.45 + 0.4×H)} ÷ 2
|
||||
#: 우리가 확정 ② 로 버린 옛 식과 **글자까지 같다.** 그 식은 틀린 식이 아니라 **골막이 식**이고,
|
||||
#: 그것을 기슭막이에까지 쓰고 있었던 것이 어긋남의 정체였다.
|
||||
#: ⚠ 상수만 보고 골막이에 가져다 쓰면 **값이 나오므로 아무 시험도 안 잡는다.**
|
||||
#: ⚠ 골막이는 그 밖에도 셋이 더 다르다 — 밑수가 **돌쌓기 + 돌붙임**(기슭막이는 돌쌓기만) ·
|
||||
#: 단위가 **개소당**(기슭막이는 m당) · 정면적이 **사다리꼴**.
|
||||
STONE_MASONRY = {
|
||||
# ⚠ **곱하는 값이 아니라 검산 참고값이다** (2026-09-08 ㉘ 에서 고침).
|
||||
# 실무 시트의 「돌쌓기 = 정면적 × 1.04」에서 그 1.04 가 **곧 기울기 몫**이다
|
||||
@@ -961,6 +962,18 @@ EXPANDERS = {
|
||||
"masonry_dry": lambda h, l, o, f=None, r="": stone_masonry(h, l, o, False, f, r),
|
||||
# 큰돌쌓기는 표준경사 표 대상이 아니라 성절토를 안 쓴다(교본 「1:0.3 이상」).
|
||||
"boulder_masonry": lambda h, l, o, f=None, r="": boulder_masonry(h, l, o),
|
||||
# 기슭막이는 **형태가 돌쌓기면 돌쌓기 식**이다 — 실무 정본 탭 제목이 「돌기슭막이(…찰쌓기…)」
|
||||
# 이고 그 안의 계산이 같다. 갈래·사유는 `_UnitQuantity_Revetment` 가 든다(늦게 부른다 —
|
||||
# 그쪽이 이 모듈의 `stone_masonry` 를 쓰므로 위에서 부르면 맞물린다).
|
||||
"revetment": lambda h, l, o, f=None, r="": _revetment()(h, l, o, f, r),
|
||||
# 골막이·바닥막이는 돌 계열이나 **돌쌓기 식을 그대로 못 쓴다** — 까닭은 그 모듈에 적었다.
|
||||
"erosion_check": lambda h, l, o, f=None, r="": _stone_kin()("erosion_check"),
|
||||
# 개거는 **m당** 원단위라 연장이 밑수다(정본 「개거(150-200)」·「L형수로-(201)」).
|
||||
"open_ditch": lambda h, l, o, f=None, r="": _open_ditch()(l, o),
|
||||
# 흙막이는 **아직 못 세움** — 정본에 떼흙막이 하나뿐인데 우리 형식 칸에 「떼」가 없음.
|
||||
"soil_guard": lambda h, l, o, f=None, r="": _soil_guard()(o),
|
||||
# 바닥막이는 **돌붙임 ㎡당**이라 높이·연장이 아니라 **면적**이 밑수다(정본 「돌붙임L3=…」).
|
||||
"bed_sill": lambda h, l, o, f=None, r="": _bed_sill()(_num(o.get("area_m2"), 0.0), o),
|
||||
# ⚠⚠ **큰돌쌓기(`boulder_masonry`)를 여기에 두지 않는다** (2026-09-07 발견).
|
||||
# 큰돌쌓기는 품셈 **13-6** 이고 돌쌓기는 **13-4** 다 — **규격 축이 다르다.**
|
||||
# 돌쌓기는 **뒷길이**(35·45·55·60㎝), 큰돌쌓기는 **직경**(40~60·60~80·80~100㎝).
|
||||
@@ -970,6 +983,42 @@ EXPANDERS = {
|
||||
# 전개식·관측 원단위가 설 때까지 **미확보로 드러낸다.**
|
||||
}
|
||||
|
||||
|
||||
def _soil_guard():
|
||||
"""흙막이 사유 함수를 늦게 가져온다."""
|
||||
from B08_Quantity.B08_Quantity_Engine_UnitQuantity_Revetment import soil_guard
|
||||
|
||||
return soil_guard
|
||||
|
||||
|
||||
def _open_ditch():
|
||||
"""개거 전개 함수를 늦게 가져온다."""
|
||||
from B08_Quantity.B08_Quantity_Engine_UnitQuantity_Revetment import open_ditch
|
||||
|
||||
return open_ditch
|
||||
|
||||
|
||||
def _bed_sill():
|
||||
"""바닥막이 전개 함수를 늦게 가져온다."""
|
||||
from B08_Quantity.B08_Quantity_Engine_UnitQuantity_Revetment import bed_sill
|
||||
|
||||
return bed_sill
|
||||
|
||||
|
||||
def _stone_kin():
|
||||
"""골막이·바닥막이 사유 함수 — 늦게 가져온다(서로 부르는 것을 푸는 자리)."""
|
||||
from B08_Quantity.B08_Quantity_Engine_UnitQuantity_Revetment import stone_kin_withheld
|
||||
|
||||
return stone_kin_withheld
|
||||
|
||||
|
||||
def _revetment():
|
||||
"""기슭막이 전개 함수를 늦게 가져온다 — 서로 부르는 것을 풀기 위한 자리."""
|
||||
from B08_Quantity.B08_Quantity_Engine_UnitQuantity_Revetment import revetment
|
||||
|
||||
return revetment
|
||||
|
||||
|
||||
#: 전개식을 일부러 안 두는 종류 — 왜 안 두는지 사람이 읽게 적는다.
|
||||
EXPANDER_WITHHELD: dict[str, str] = {}
|
||||
|
||||
|
||||
@@ -0,0 +1,346 @@
|
||||
"""기슭막이 전개 — **형태로 갈라 돌쌓기 식을 그대로 쓴다**(3단계, 2026-09-09).
|
||||
|
||||
실무 정본이 근거다 — `구조물도/기슭막이/04.구조도(기슭막이).xls` 의 탭 제목이
|
||||
**「돌기슭막이(H=2.0m, 찰쌓기, 기초유)」** 이고, 그 안의 계산이 돌쌓기와 **같은 식**이다.
|
||||
|
||||
정면적 → 비탈면적(×√(1+n²)) → 평균두께 → 입적 → 돌 · 막자갈 · 고임돌 →
|
||||
터파기(기초 + 비탈) → 되메우기 → 잔토
|
||||
|
||||
⇒ **기슭막이는 새 식이 아니라 「형태가 돌쌓기면 돌쌓기 식」**이다. 여기서 식을 다시 짜면
|
||||
같은 계산이 두 벌이 되어 돌쌓기와 갈린다(CLAUDE.md 5장).
|
||||
|
||||
⚠ **형태가 돌쌓기가 아닌 갈래는 식이 없다** — 콘크리트·돌망태·통나무/목재틀·바자.
|
||||
지어내지 않고 **왜 없는지**를 줄로 남긴다.
|
||||
|
||||
⚠ **뒷길이·돌 종류 칸이 기슭막이 등록부에 아직 없다**(2026-09-09 실측). 돌쌓기 식은 그 둘로
|
||||
계수가 갈리는데, 없으면 `_back_length` 가 **조용히 45㎝ 로 돈다**. 값이 나오므로 아무도
|
||||
못 알아채는 자리라 **그 사실을 줄로 드러낸다** — 「값이 있기는 하니 안 보이는」 그것.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from B08_Quantity.B08_Quantity_Engine_UnitQuantity import Component, stone_masonry
|
||||
|
||||
#: 형태 → 돌쌓기 갈래(찰이면 True). 여기 없는 형태는 전개식이 없다.
|
||||
STONE_FORMS: dict[str, bool] = {
|
||||
"돌쌓기(찰)": True,
|
||||
"돌쌓기(메)": False,
|
||||
}
|
||||
|
||||
#: 식이 없는 형태와 **왜 없는지**. 품셈 장이 다르거나 원단위가 원문에 없다.
|
||||
WITHHELD_FORMS: dict[str, str] = {
|
||||
"콘크리트": (
|
||||
"콘크리트 기슭막이는 돌쌓기(품셈 13-4)가 아니라 콘크리트 구조물이라 전개식이 다름 "
|
||||
"— 벽 두께·저판이 정본에 없어 물량이 서지 않음"
|
||||
),
|
||||
"돌망태": ("돌망태는 품셈 13-8 이고 규격 축이 망태 치수라 돌쌓기 표를 못 씀 — 원단위 미확보"),
|
||||
"통나무·목재틀": (
|
||||
"목재틀은 품셈 13-13 이고 밑수가 ㎥당 목공 품이라 돌쌓기 표를 못 씀 "
|
||||
"— 각재·판재 자재가 카탈로그에 없어 값이 모자람"
|
||||
),
|
||||
"바자": "바자얽기(품셈 5-15)는 별도 일위대가를 만들어 잇기로 확정(2026-09-09 ⑧-4)",
|
||||
}
|
||||
|
||||
#: 골막이·바닥막이 — **왜 아직 안 서는지**. 둘 다 돌 계열이나 돌쌓기 식을 그대로 못 쓴다.
|
||||
#: ⚠ 근거 없는 식을 만들지 않는다(2026-09-09 확인). 칸·표가 오면 그때 붙인다.
|
||||
#:
|
||||
#: ⚠⚠ **골막이를 돌쌓기와 합치지 말 것** — 「같은 돌쌓기니 한 함수로」가 되기 쉬운 자리다.
|
||||
#: 소광리 정본 「골막이(찰)(치수조서연결)」과 「돌기슭막이」를 나란히 재면 **넷이 다르다**:
|
||||
#:
|
||||
#: 두께식 골막이 (0.45+0.1H)/(0.45+0.4H) ↔ 기슭막이 뒷길이 기반(ℓ3+0.30 …)
|
||||
#: 밑수 골막이 **돌쌓기 + 돌붙임** ↔ 기슭막이 돌쌓기만
|
||||
#: 단위 골막이 **개소당** ↔ 기슭막이 m당 (확정 ⑦ 「통일 안 함」)
|
||||
#: 정면적 골막이 **사다리꼴**(상장·하장) ↔ 기슭막이 직사각(H × 1)
|
||||
#:
|
||||
#: ⓘ 같은 것도 있다 — 막자갈 **2/3**·야면석 **0.88 ton/㎡**·물구멍 **2㎡/개소**·
|
||||
#: 기울기 몫 **1.04**·고임돌 **0.15**·채움콘크리트 **0.2** 는 두 시트가 같은 값이다.
|
||||
#: 우리 기본값이 어디서 왔는지의 답이기도 하다.
|
||||
STONE_KIN_WITHHELD: dict[str, str] = {
|
||||
"erosion_check": (
|
||||
"골막이는 돌쌓기와 **두께식·밑수·단위가 다름**(소광리 정본 「골막이(찰)(치수조서연결)」 "
|
||||
"실측) — 평균두께 {(0.45+0.1H)+(0.45+0.4H)}÷2 · 밑수 「돌쌓기+돌붙임」 · 개소당. "
|
||||
"게다가 정면적이 사다리꼴이라 **상장·하장 칸이 있어야** 셈이 서는데 등록부에 없음"
|
||||
),
|
||||
}
|
||||
|
||||
#: 돌붙임 원단위 — 소광리 정본 「돌붙임L3=45(사방)」(찰) · 「돌붙임L3=30(야면석메붙임)」(메).
|
||||
#: ⚠⚠ **돌쌓기 표(13-4)와 합치지 말 것.** 값이 겹쳐 보여 합치고 싶어지는 자리다:
|
||||
#:
|
||||
#: 돌쌓기(13-4) 밑수 **비탈면적** · 뒷길이 **일곱 규격** · **기울기 몫 √(1+n²)**
|
||||
#: 돌붙임(13-5) 밑수 **평면적** · 뒷길이 **넷** · **기울기 몫 없음**
|
||||
#: 메는 모르타르·채움콘크리트가 통째로 빠진다
|
||||
#:
|
||||
#: ⚠ 45㎝ 에서 고임돌 0.15 · 채움 0.20 이 **우연히 같다** — 그것이 합치고 싶어지는 까닭이고,
|
||||
#: 합치면 뒷길이 25·30·60·75 에서 조용히 틀린다(돌붙임 표에 없는 규격이다).
|
||||
#:
|
||||
#: ⚠ **뒷길이는 지금 붙박이다** — 찰 0.45m · 메 0.30m. 정본 탭이 그 둘로만 있고 바닥막이
|
||||
#: 등록부에 뒷길이 칸이 없다. 칸이 생기면 이 표를 열면 된다.
|
||||
#: ⓘ 돌중량은 **관측값이 아니라 유도값**이다 — `뒷길이 × 0.77(채움률) × 2.65(비중)`.
|
||||
#: 찰 0.45 × 0.77 × 2.65 = 0.918 ton/㎡. 메(야면석)는 0.42 로 **원문에 값이 직접 적혀 있어**
|
||||
#: 같은 유도식이 안 맞는다(야면석은 공극이 커 ㎥당 무게가 다르다) — 그대로 옮긴다.
|
||||
BED_SILL_FORMS: dict[str, dict[str, Any]] = {
|
||||
"돌붙임(찰)": {
|
||||
"back_len_m": 0.45,
|
||||
"stone_name": "돌",
|
||||
"stone_spec": "30×30×45㎝",
|
||||
"stone_ton_per_m2": 0.45 * 0.77 * 2.65,
|
||||
"wedge_stone_m3_per_m2": 0.15,
|
||||
"mortar_m3_per_m2": 0.009,
|
||||
"fill_concrete_m3_per_m2": 0.2,
|
||||
"blinding_m3_per_m2": 0.1,
|
||||
},
|
||||
"돌붙임(메)": {
|
||||
"back_len_m": 0.30,
|
||||
"stone_name": "야면석",
|
||||
"stone_spec": "20×20×30㎝",
|
||||
"stone_ton_per_m2": 0.42,
|
||||
"wedge_stone_m3_per_m2": 0.07,
|
||||
# 메붙임은 모르타르·채움콘크리트·버림이 **없다**(정본 탭에 줄 자체가 없음).
|
||||
"mortar_m3_per_m2": None,
|
||||
"fill_concrete_m3_per_m2": None,
|
||||
"blinding_m3_per_m2": None,
|
||||
},
|
||||
}
|
||||
|
||||
#: 돌쌓기 식이 계수를 가르는 데 쓰는 칸 — 기슭막이 등록부에 아직 없는 것을 알린다.
|
||||
STONE_SPEC_KEYS: tuple[tuple[str, str], ...] = (
|
||||
("back_len_cm", "뒷길이"),
|
||||
("stone_kind", "돌 종류"),
|
||||
)
|
||||
|
||||
|
||||
def revetment(
|
||||
height_m: float,
|
||||
length_m: float,
|
||||
options: dict[str, Any],
|
||||
face: str | None = None,
|
||||
face_reason: str = "",
|
||||
) -> tuple[list[Component], list[str]]:
|
||||
"""기슭막이 1구간 전개 — 형태가 돌쌓기면 그 식, 아니면 사유만."""
|
||||
form = str(options.get("form") or "").strip()
|
||||
if form not in STONE_FORMS:
|
||||
why = WITHHELD_FORMS.get(form)
|
||||
if why:
|
||||
return [], [f"기슭막이 형태 「{form}」 — {why}"]
|
||||
return [], [
|
||||
f"기슭막이 형태가 정해지지 않았습니다(지금 「{form or '빈 값'}」) "
|
||||
"— 형태를 골라야 물량이 섭니다"
|
||||
]
|
||||
|
||||
components, notes = stone_masonry(
|
||||
height_m, length_m, options, STONE_FORMS[form], face, face_reason
|
||||
)
|
||||
# ⚠ 계수를 가르는 칸이 없으면 **기본값으로 조용히 돈다** — 그 사실을 드러낸다.
|
||||
missing = [label for key, label in STONE_SPEC_KEYS if not options.get(key)]
|
||||
if missing and components:
|
||||
notes.append(
|
||||
f"⚠ 기슭막이 제원에 {' · '.join(missing)} 칸이 없어 **돌쌓기 기본값으로 섰음** "
|
||||
"— 그 값이 고임돌·야면석·채움콘크리트 계수를 가름"
|
||||
)
|
||||
return components, notes
|
||||
|
||||
|
||||
def stone_kin_withheld(type_id: str) -> tuple[list[Component], list[str]]:
|
||||
"""골막이·바닥막이 — **식을 안 두는 까닭**만 낸다.
|
||||
|
||||
⚠ 돌 계열이라 `stone_masonry` 를 부르고 싶어지는 자리다. 실제로 부르면 **값이 나오므로**
|
||||
아무 시험도 안 잡는다 — 오늘 큰돌쌓기에서 같은 일이 있었다(직경 60~80㎝ 가 「뒷길이
|
||||
45㎝」 계수로 돌던 자리). 그래서 **일부러 안 부른다.**
|
||||
"""
|
||||
why = STONE_KIN_WITHHELD.get(type_id)
|
||||
return [], [why] if why else []
|
||||
|
||||
|
||||
#: 개거(겉도랑) 원단위 — 소광리 정본 「개거(150-200) (2)」·「L형수로-(201)」, **둘 다 m당**.
|
||||
#: ⚠⚠ **두 표가 담는 것이 다르다.** 값이 비슷해 보여 합치고 싶어지는 자리다:
|
||||
#:
|
||||
#: 콘크리트 개거 150×200 터파기 · 유로폼 · 면목 **세 줄뿐** — **콘크리트 본체 줄이 없다**
|
||||
#: 콘크리트 L형수로 H=0.2 터파기 · 되메우기 · 잔토 · **콘크리트** · PVC Φ50 · 이형철근 D13 ·
|
||||
#: 거푸집 · 면목 **여덟 줄**
|
||||
#:
|
||||
#: ⓘ 같은 것 — **둘 다 m당**이고 거푸집 계열(유로폼·거푸집)과 면목이 있다.
|
||||
#: ⚠ 개거 표에 콘크리트가 없는 것은 **빠뜨린 것이 아니라 원문 그대로**다. 0 으로 때우거나
|
||||
#: L형수로 값을 옮겨 채우지 않는다(2026-09-09 판단 — 돌붙임(메)에서 세 줄을 안 만든 그 자리).
|
||||
#:
|
||||
#: ⓘ **「다른 탭에 본체가 있나」를 찾아봤고 없었다**(2026-09-09 53탭 전수):
|
||||
#: · 「개거」 글자가 나오는 탭은 **둘뿐** — 이 탭과 「콘크리트개거」.
|
||||
#: 뒤엣것은 **제목만 「콘크리트개거 (물넘이형)」이고 내용이 국가지점번호판 표**다.
|
||||
#: · 탭 이름의 「(2)」는 **짝 번호가 아니라 엑셀 사본 표식**이다 — `Φ1000` ↔ `Φ1000(2)`,
|
||||
#: `떼수로(윤주1.08)` ↔ `…(2)` 가 **내용까지 똑같다**(대조 확인). 「(1)」은 없다.
|
||||
#: · 같은 시트의 「측구수로400/500」도 제목이 **「터파기 계산서」**이고 본체가 없다 —
|
||||
#: **한 구조물을 부분 계산서로 쪼개는 버릇**이 있는 시트다.
|
||||
#: ⇒ 그러므로 이 표는 **부분 계산서일 가능성이 남아 있다.** 다만 이 원본 안에서는
|
||||
#: 본체 표를 못 찾았으므로 **「원문에 없음」으로 두고 그 사실을 화면에 적는다** —
|
||||
#: 「콘크리트 개거인데 콘크리트가 0」이 우리 결함으로 읽히지 않게.
|
||||
#:
|
||||
#: ⚠ **규격이 붙박이다** — 정본에 「150×200」 하나뿐이고 `open_ditch` 등록부에 **옵션이 하나도
|
||||
#: 없다**(규격 칸 없음). 칸이 생기면 이 표를 열면 된다.
|
||||
OPEN_DITCH_FORMS: dict[str, dict[str, Any]] = {
|
||||
"콘크리트 개거 150×200": {
|
||||
"rows": (
|
||||
("터파기", "㎥", 0.3 * 0.1 * 1.0, "0.3 × 0.1 × 1"),
|
||||
("유로폼", "㎡", (0.2 + 0.2 + 0.15) * 1.0, "(0.2 + 0.2 + 0.15) × 1"),
|
||||
("면목", "m", 2.0 * 1.0, "2 × 1"),
|
||||
),
|
||||
},
|
||||
"콘크리트 L형수로 H=0.2": {
|
||||
"rows": (
|
||||
("터파기", "㎥", 0.31645, "[{(1.11+1.51)÷2×0.2} + {(0.33×0.33)÷2}] × 1.0"),
|
||||
("되메우기", "㎥", 0.129975, "(0.2+0.30)÷2×0.25×1 + (0.2+0.19)÷2×0.33×1.0"),
|
||||
("잔토처리", "㎥", 0.186475, "터파기 − 되메우기"),
|
||||
(
|
||||
"콘크리트",
|
||||
"㎥",
|
||||
0.1508,
|
||||
"{(0.2+0.15)÷2×0.7} + (0.21×0.13)×1 + {(0.15+0.21)÷2×0.20}×1",
|
||||
),
|
||||
("이형철근 D13", "kg", 0.398, "0.2 × 2 × 0.995"),
|
||||
("거푸집", "㎡", 0.738, "0.2 + 0.33 + √(0.2² + 0.06²)"),
|
||||
("면목", "m", 1.0, "1"),
|
||||
),
|
||||
# ⚠ 정본에 줄은 있으나 **수량 칸이 비어 있다**(PVC Φ50). 0 으로 만들지 않고 알린다.
|
||||
"blank_rows": (("PVC 파이프 Φ50㎜", "m"),),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
#: 떼흙막이 원단위 — 소광리 정본 「떼흙막이」, **개소당**. 치수는 전부 **평균 붙박이**다
|
||||
#: (상단 1.5 · 하단 1.1 · 높이 0.5 · 두께 0.2 · 기슭 0.54 — 원문에 「(평균)」이라 적혀 있다).
|
||||
#: ⚠⚠ **아직 못 쓴다** — 우리 `soil_guard` 형식이 「콘크리트 · 돌(찰) · 돌(메) · 블록쌓기 ·
|
||||
#: 돌망태」 다섯이고 **「떼」가 없다.** 형식 칸이 생기면 이 표를 그대로 붙이면 된다.
|
||||
#: ⚠ 나머지 다섯 형식은 **정본에 탭이 없다.** 돌(찰)/(메)에 기슭막이 식을 빌려 쓰지 않는다 —
|
||||
#: 흙막이는 교본상 앞면 1:0.3·뒷면 수직·천단 30㎝ 라 축이 다르고, 무엇보다 **근거가 없다.**
|
||||
SOIL_GUARD_SOD: dict[str, Any] = {
|
||||
"unit_label": "개소당",
|
||||
"rows": (
|
||||
(
|
||||
"떼",
|
||||
"㎡",
|
||||
1.39,
|
||||
"머리떼 1.5×0.2 + 바닥떼 1.1×0.2 + 정면떼 (1.5+1.1)÷2×0.5 "
|
||||
"+ 양기슭바닥떼 0.54×0.2×2 · 규격 20×20㎝",
|
||||
),
|
||||
("바닥파기", "㎥", 0.17, "(0.54×0.4×0.2×2) + (1.1×0.4×0.2)"),
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def soil_guard(options: dict[str, Any]) -> tuple[list[Component], list[str]]:
|
||||
"""흙막이 — **아직 못 세운다.** 어느 형식이 왜 안 되는지만 낸다."""
|
||||
form = str(options.get("form") or "").strip()
|
||||
if not form:
|
||||
return [], ["흙막이 형식이 정해지지 않았습니다 — 형식을 골라야 물량이 섭니다"]
|
||||
return [], [
|
||||
f"흙막이 형태 「{form}」 — 정본(소광리 53탭)에 그 형식의 산출식이 없습니다. "
|
||||
"있는 것은 **떼흙막이** 하나인데 우리 형식 칸에 「떼」가 없습니다. "
|
||||
"⚠ 돌(찰)·돌(메)에 기슭막이 식을 빌려 쓰지 않습니다 — 흙막이는 앞면 1:0.3·뒷면 수직·"
|
||||
"천단 30㎝(교본)라 축이 다르고 근거가 없습니다"
|
||||
]
|
||||
|
||||
|
||||
def open_ditch(length_m: float, options: dict[str, Any]) -> tuple[list[Component], list[str]]:
|
||||
"""개거(겉도랑) 전개 — **m당** 원단위를 연장에 곱한다(소광리 정본 두 탭).
|
||||
|
||||
⚠ 규격을 고를 칸이 없어 지금은 **「콘크리트 개거 150×200」 붙박이**다. 그 사실을 줄로 낸다.
|
||||
"""
|
||||
from B08_Quantity.B08_Quantity_Engine_UnitQuantity import DESTINATION
|
||||
|
||||
if length_m <= 0:
|
||||
return [], ["연장이 없어 전개하지 않음"]
|
||||
spec = str(options.get("ditch_spec") or "콘크리트 개거 150×200").strip()
|
||||
table = OPEN_DITCH_FORMS.get(spec)
|
||||
if table is None:
|
||||
return [], [
|
||||
f"개거 규격 「{spec}」 원단위가 없습니다 — 있는 것: {' · '.join(OPEN_DITCH_FORMS)}"
|
||||
]
|
||||
|
||||
components = [
|
||||
Component(
|
||||
name,
|
||||
unit,
|
||||
per_m * length_m,
|
||||
DESTINATION.get(name, "quantity"),
|
||||
f"{basis} × 연장 {length_m:g}m (m당 {per_m:g})",
|
||||
)
|
||||
for name, unit, per_m, basis in table["rows"]
|
||||
]
|
||||
notes: list[str] = []
|
||||
if "ditch_spec" not in options:
|
||||
notes.append(f"규격이 「{spec}」 붙박이입니다 — 개거 제원에 규격 칸이 없습니다")
|
||||
if spec == "콘크리트 개거 150×200":
|
||||
notes.append(
|
||||
"⚠ 이 표에는 **콘크리트 본체 줄이 없습니다** — 정본 원문 그대로입니다"
|
||||
"(터파기·유로폼·면목 세 줄뿐). 같은 원본 53탭에서 본체 표를 찾았으나 없었고"
|
||||
"(「콘크리트개거」 탭은 제목만 그 이름이고 내용이 다른 표), 이 시트가"
|
||||
"「측구수로 터파기 계산서」처럼 **부분 계산서를 쓰는 버릇**이 있어"
|
||||
"**다른 문서에 본체가 있을 수 있습니다.** 지어내지 않았습니다"
|
||||
)
|
||||
for name, unit in table.get("blank_rows", ()):
|
||||
notes.append(f"{name}({unit}) 은 정본에 줄은 있으나 **수량이 비어 있어** 안 세웠습니다")
|
||||
return components, notes
|
||||
|
||||
|
||||
def bed_sill(area_m2: float, options: dict[str, Any]) -> tuple[list[Component], list[str]]:
|
||||
"""바닥막이 전개 — **돌붙임 ㎡당** 원단위를 면적에 곱한다(소광리 정본 두 탭).
|
||||
|
||||
⚠ 밑수가 **면적**이다. 돌쌓기처럼 높이·연장으로 셈하지 않는다 — 정본 표 제목이 「㎡당」이고
|
||||
등록부 밑수도 `area_m2` 라 축이 그대로 맞는다.
|
||||
⚠ 기울기 몫이 **없다** — 평면에 붙이는 것이라 √(1+n²) 를 곱하지 않는다.
|
||||
"""
|
||||
from B08_Quantity.B08_Quantity_Engine_UnitQuantity import DESTINATION
|
||||
|
||||
form = str(options.get("form") or "").strip()
|
||||
table = BED_SILL_FORMS.get(form)
|
||||
if table is None:
|
||||
return [], [
|
||||
f"바닥막이 형태 「{form or '빈 값'}」 — 돌붙임(찰)·돌붙임(메)만 원단위가 있습니다"
|
||||
]
|
||||
if area_m2 <= 0:
|
||||
return [], ["면적이 없어 전개하지 않음"]
|
||||
|
||||
back = table["back_len_m"]
|
||||
rows: list[tuple[str, str, float, str]] = [
|
||||
("돌붙임", "㎡", area_m2, f"면적 {area_m2:g}㎡ (평면적 — 기울기 몫 없음)"),
|
||||
("입적", "㎥", area_m2 * back, f"면적 × 두께 {back:g}m"),
|
||||
(
|
||||
table["stone_name"],
|
||||
"ton",
|
||||
area_m2 * table["stone_ton_per_m2"],
|
||||
f"면적 × {table['stone_ton_per_m2']:.3f} ton/㎡ · {table['stone_spec']}"
|
||||
+ (
|
||||
f" (= 뒷길이 {back:g} × 0.77 × 2.65)"
|
||||
if form == "돌붙임(찰)"
|
||||
else " (원문 직접값 — 야면석은 유도식이 안 맞음)"
|
||||
),
|
||||
),
|
||||
(
|
||||
"고임돌",
|
||||
"㎥",
|
||||
area_m2 * table["wedge_stone_m3_per_m2"],
|
||||
f"면적 × {table['wedge_stone_m3_per_m2']} ㎥/㎡",
|
||||
),
|
||||
]
|
||||
for name, key, unit in (
|
||||
("모르터", "mortar_m3_per_m2", "㎥"),
|
||||
("채움콘크리트", "fill_concrete_m3_per_m2", "㎥"),
|
||||
("버림콘크리트", "blinding_m3_per_m2", "㎥"),
|
||||
):
|
||||
value = table[key]
|
||||
if value is not None:
|
||||
rows.append((name, unit, area_m2 * value, f"면적 × {value} ㎥/㎡"))
|
||||
# 터파기 — 정본은 「두께 × 1 × 1」이라 **면적 × 두께**와 같다.
|
||||
rows.append(("터파기", "㎥", area_m2 * back, f"면적 × 두께 {back:g}m"))
|
||||
|
||||
components = [
|
||||
Component(name, unit, amount, DESTINATION.get(name, "quantity"), basis)
|
||||
for name, unit, amount, basis in rows
|
||||
]
|
||||
notes = [f"뒷길이 {back:g}m 는 정본 탭 붙박이 값입니다 — 바닥막이 제원에 뒷길이 칸이 없습니다"]
|
||||
if form == "돌붙임(찰)":
|
||||
notes.append(
|
||||
"버림 콘크리트 0.1 ㎥/㎡ 포함 — 정본 주기 「바닥 10㎝ 이상 콘크리트(버림) 포설 후 "
|
||||
"돌붙임」(KDS 44 90 00 의 100㎜ 와 같음)"
|
||||
)
|
||||
return components, notes
|
||||
@@ -87,7 +87,7 @@ async def get_earthwork_table(project_id: UUID, route_id: int) -> JSONResponse:
|
||||
# 사토 — **운반 줄이 되는 값**인데 유토곡선의 띠·이동에는 안 들어 있다(잔량으로 남는다).
|
||||
# 여기서 그 값을 운반표에 실어 인계가 「사토 운반」 한 줄을 세우게 한다.
|
||||
# ⚠ 거리는 품셈이 정하지 않는다 — 설계 입력(`spoil_site_distance_m`)이고 없으면 막힌다.
|
||||
haul["spoil"] = _spoil_of(plan, settings)
|
||||
haul["spoil"] = _spoil_of(plan, settings, _spoil_sites(designs))
|
||||
# 배수관 연장 — B06 이 측점 `design.pipe_length_m` 에 남긴 값. **여기서 짓지 않는다.**
|
||||
# 인계가 관 줄을 세울 때 쓴다. 단면을 두 번 읽지 않으려고 이 응답에 실어 보낸다.
|
||||
table["pipe_lengths"] = [
|
||||
@@ -167,7 +167,75 @@ _COMPACTED_FACTOR = {
|
||||
}
|
||||
|
||||
|
||||
def _spoil_of(plan: dict[str, Any] | None, settings: dict[str, Any]) -> dict[str, Any]:
|
||||
def _spoil_sites(designs: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
"""배치된 사토장(유용토운반작업장) — 측점 설계에 실려 온 구간값을 모은다.
|
||||
|
||||
⚠ 여기서 **다시 세지 않는다** — 용량·담긴 양은 B06 이 정한 값이고, 이 함수는 그것을
|
||||
구조물 단위로 접어 「어디에 얼마나 담기나」만 만든다.
|
||||
"""
|
||||
sites: dict[str, dict[str, Any]] = {}
|
||||
for row in designs:
|
||||
design = (row or {}).get("design") or {}
|
||||
key = str(design.get("spoil_fill_structure_id") or "")
|
||||
if not key or not float(design.get("spoil_fill_area_m2") or 0.0) > 0:
|
||||
continue
|
||||
chainage = float(row.get("chainage_m") or 0.0)
|
||||
site = sites.setdefault(
|
||||
key,
|
||||
{
|
||||
"structure_id": key,
|
||||
"from_m": chainage,
|
||||
"to_m": chainage,
|
||||
"capacity_m3": float(design.get("spoil_fill_capacity_m3") or 0.0),
|
||||
"placed_m3": float(design.get("spoil_fill_placed_m3") or 0.0),
|
||||
"unplaced_m3": float(design.get("spoil_fill_unplaced_m3") or 0.0),
|
||||
"extra_distance_m": design.get("spoil_fill_extra_distance_m"),
|
||||
},
|
||||
)
|
||||
site["from_m"] = min(site["from_m"], chainage)
|
||||
site["to_m"] = max(site["to_m"], chainage)
|
||||
for site in sites.values():
|
||||
site["center_m"] = (site["from_m"] + site["to_m"]) / 2
|
||||
return sorted(sites.values(), key=lambda item: item["center_m"])
|
||||
|
||||
|
||||
def _site_distance_m(
|
||||
sites: list[dict[str, Any]], residuals: list[dict[str, Any]]
|
||||
) -> tuple[float | None, str]:
|
||||
"""사토장까지의 **가중평균 운반거리**(m)와 근거 문구.
|
||||
|
||||
「발생점 → 사토장 측점」 누가거리다(2026-09-09 사용자 확정 ③ — 사토장이 측점 위에만
|
||||
서므로 가정할 것이 없다). 사토가 여러 자리에 남으면 물량으로 가중평균한다.
|
||||
⚠ 사토장이 없으면 `None` — 설계 입력(`spoil_site_distance_m`)으로 되돌아간다.
|
||||
**임의 거리를 넣지 않는다**(그대로 금액이 된다).
|
||||
"""
|
||||
if not sites:
|
||||
return None, ""
|
||||
work = 0.0
|
||||
volume = 0.0
|
||||
for residual in residuals:
|
||||
if str(residual.get("kind") or "") != "spoil":
|
||||
continue
|
||||
amount = float(residual.get("volume_m3") or 0.0) - float(residual.get("natural_m3") or 0.0)
|
||||
if amount <= 0:
|
||||
continue
|
||||
center = (float(residual.get("from_m") or 0.0) + float(residual.get("to_m") or 0.0)) / 2
|
||||
nearest = min(sites, key=lambda site: abs(site["center_m"] - center))
|
||||
extra = nearest.get("extra_distance_m")
|
||||
distance = abs(nearest["center_m"] - center) + float(extra or 0.0)
|
||||
work += amount * distance
|
||||
volume += amount
|
||||
if volume <= 0:
|
||||
return None, ""
|
||||
where = " · ".join(f"{site['center_m']:,.1f}m" for site in sites)
|
||||
return work / volume, f"사토장 측점({where})까지 발생점 기준 가중평균"
|
||||
|
||||
|
||||
def _spoil_of(
|
||||
plan: dict[str, Any] | None,
|
||||
settings: dict[str, Any],
|
||||
sites: list[dict[str, Any]] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""사토 — 실어 낼 물량과 거리. 유토곡선 결과에서 **다시 세지 않고 그대로** 가져온다.
|
||||
|
||||
⚠ `spoil_m3` 는 **공제·가산이 끝난 값**이다(채집석 공제는 빼고 구조물 잔토는 더한 뒤).
|
||||
@@ -229,10 +297,25 @@ def _spoil_of(plan: dict[str, Any] | None, settings: dict[str, Any]) -> dict[str
|
||||
}
|
||||
if unknown > 0:
|
||||
note_parts.append(f"⚠ 갈래를 못 붙인 {unknown:,.2f}㎥ 는 상태도 못 되돌림")
|
||||
# 거리 — 사토장이 서 있으면 **그 측점까지의 누가거리**로 나온다. 없으면 설계 입력값.
|
||||
site_distance, site_basis = _site_distance_m(sites or [], source.get("residuals") or [])
|
||||
if site_distance is not None:
|
||||
note_parts.append(site_basis)
|
||||
placed = sum(float(site.get("placed_m3") or 0.0) for site in sites or [])
|
||||
unplaced = sum(float(site.get("unplaced_m3") or 0.0) for site in sites or [])
|
||||
note_parts.append(f"사토장 수용 {placed:,.1f}㎥")
|
||||
if unplaced > 0:
|
||||
note_parts.append(f"⚠ 못 담는 {unplaced:,.1f}㎥ 는 밖으로 내야 함")
|
||||
return {
|
||||
"volume_m3": round(volume, 3),
|
||||
"volume_basis": "compacted",
|
||||
"distance_m": settings.get("spoil_site_distance_m"),
|
||||
"distance_m": (
|
||||
round(site_distance, 3)
|
||||
if site_distance is not None
|
||||
else settings.get("spoil_site_distance_m")
|
||||
),
|
||||
"distance_basis": ("사토장(측점) 기준" if site_distance is not None else "설계 입력값"),
|
||||
"sites": sites or [],
|
||||
"note": " · ".join(note_parts),
|
||||
"by_ground_m3": {key: round(value, 3) for key, value in grounds.items()},
|
||||
"natural_m3_by_ground": natural_by_ground,
|
||||
|
||||
@@ -50,6 +50,7 @@ from B05_Profile.B05_Profile_Router_Lifecycle import router as b05_route_lifecyc
|
||||
from B05_Profile.B05_Profile_Router_Replan import router as b05_route_replan_router
|
||||
from B05_Profile.B05_Profile_Structures_Router import router as b05_structures_router
|
||||
from B06_Section.B06_Section_Router import router as b06_section_router
|
||||
from B06_Section.B06_Section_Router_Stations import router as b06_section_stations_router
|
||||
from B06_Section.B06_Section_Router_Confirm import (
|
||||
router as b06_section_confirm_router,
|
||||
)
|
||||
@@ -538,6 +539,7 @@ app.include_router(b05_corridor_router, dependencies=protected_with_company)
|
||||
app.include_router(b05_route_replan_router, dependencies=protected_with_company)
|
||||
app.include_router(b05_structures_router, dependencies=protected_with_company)
|
||||
app.include_router(b06_section_router, dependencies=protected_with_company)
|
||||
app.include_router(b06_section_stations_router, dependencies=protected_with_company)
|
||||
app.include_router(b06_section_confirm_router, dependencies=protected_with_company)
|
||||
app.include_router(b06_section_haul_plan_router, dependencies=protected_with_company)
|
||||
app.include_router(b07_design_router, dependencies=protected_with_company)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"dataset_id": "data_work_item_master_manifest",
|
||||
"generated_at": "2026-09-08T08:33:10+09:00",
|
||||
"generated_at": "2026-09-09T11:44:28+09:00",
|
||||
"built_by": "B08_Quantity/B08_Quantity_Build_WorkItemMaster.py",
|
||||
"source": {
|
||||
"dataset_id": "pum_forest",
|
||||
@@ -12,8 +12,8 @@
|
||||
"files": [
|
||||
{
|
||||
"file": "work_item_master_2026-01-01.json",
|
||||
"sha256": "3752af1e0328a2faf4947268e449e0bc602a0738cc6a29a78ed71d343cf2e583",
|
||||
"size_bytes": 856832
|
||||
"sha256": "3462899767156784158f724513c793052711806260e60db59d3e2a5fbf1fab21",
|
||||
"size_bytes": 856810
|
||||
},
|
||||
{
|
||||
"file": "form_undetermined_2026-01-01.json",
|
||||
@@ -22,8 +22,8 @@
|
||||
},
|
||||
{
|
||||
"file": "basis_missing_2026-01-01.json",
|
||||
"sha256": "244851609c65ba8adbfc318668224cbf3a81032e9271cfda916de435d460dd92",
|
||||
"size_bytes": 18360
|
||||
"sha256": "644b179f7d4b57641fc4ea6df27ef3e3904a03a0bf49c6a4a8bcf44f1195bdb1",
|
||||
"size_bytes": 19018
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -280,6 +280,12 @@
|
||||
"pum_form": "requirement",
|
||||
"line": 2653
|
||||
},
|
||||
{
|
||||
"pum_table_id": "F0127",
|
||||
"section": "5-19-2. 표토펴기 및 고르기",
|
||||
"pum_form": "requirement",
|
||||
"line": 2664
|
||||
},
|
||||
{
|
||||
"pum_table_id": "F0129",
|
||||
"section": "5-21. 표토이식",
|
||||
@@ -586,6 +592,24 @@
|
||||
"pum_form": "requirement",
|
||||
"line": 4434
|
||||
},
|
||||
{
|
||||
"pum_table_id": "F0228",
|
||||
"section": "8-6-2. 드론방제",
|
||||
"pum_form": "requirement",
|
||||
"line": 4516
|
||||
},
|
||||
{
|
||||
"pum_table_id": "F0229",
|
||||
"section": "8-6-2. 드론방제",
|
||||
"pum_form": "requirement",
|
||||
"line": 4536
|
||||
},
|
||||
{
|
||||
"pum_table_id": "F0230",
|
||||
"section": "8-6-3. 지상방제",
|
||||
"pum_form": "requirement",
|
||||
"line": 4557
|
||||
},
|
||||
{
|
||||
"pum_table_id": "F0232",
|
||||
"section": "8-7. 방제 실행 등록",
|
||||
@@ -754,6 +778,12 @@
|
||||
"pum_form": "requirement",
|
||||
"line": 6943
|
||||
},
|
||||
{
|
||||
"pum_table_id": "F0401",
|
||||
"section": "13-2-4. 야면석 채집(인력)",
|
||||
"pum_form": "requirement",
|
||||
"line": 7025
|
||||
},
|
||||
{
|
||||
"pum_table_id": "F0405",
|
||||
"section": "13-3. 기초다짐 및 뒤채움’ 항을 적용한다.",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"schema_version": "1.0",
|
||||
"dataset_id": "work_item_master_forest",
|
||||
"effective_date": "2026-01-01",
|
||||
"generated_at": "2026-09-08T08:33:10+09:00",
|
||||
"generated_at": "2026-09-09T11:44:28+09:00",
|
||||
"dataset_version": {
|
||||
"dataset_id": "pum_forest",
|
||||
"effective_date": "2026-01-01",
|
||||
@@ -21,9 +21,9 @@
|
||||
"tables_attached": 456,
|
||||
"tables_orphan": 19,
|
||||
"form_undetermined": 77,
|
||||
"basis_found": 185,
|
||||
"basis_missing": 135,
|
||||
"basis_grouped": 36
|
||||
"basis_found": 180,
|
||||
"basis_missing": 140,
|
||||
"basis_grouped": 35
|
||||
},
|
||||
"orphan_tables": [
|
||||
{
|
||||
@@ -15789,9 +15789,9 @@
|
||||
"source_line": 2664,
|
||||
"pum_form": "requirement",
|
||||
"form_basis": "직종 표기((인)·인부·공)",
|
||||
"basis_quantity": 30.0,
|
||||
"basis_unit": "㎥",
|
||||
"basis_source": "표 안",
|
||||
"basis_quantity": null,
|
||||
"basis_unit": null,
|
||||
"basis_source": null,
|
||||
"resource_shares": {},
|
||||
"partial_ratio": false,
|
||||
"expression_cells": [],
|
||||
@@ -26997,9 +26997,9 @@
|
||||
"source_line": 4516,
|
||||
"pum_form": "requirement",
|
||||
"form_basis": "직종 표기((인)·인부·공)",
|
||||
"basis_quantity": 1.0,
|
||||
"basis_unit": "인",
|
||||
"basis_source": "본문",
|
||||
"basis_quantity": null,
|
||||
"basis_unit": null,
|
||||
"basis_source": null,
|
||||
"resource_shares": {},
|
||||
"partial_ratio": false,
|
||||
"expression_cells": [],
|
||||
@@ -27087,9 +27087,9 @@
|
||||
"source_line": 4536,
|
||||
"pum_form": "requirement",
|
||||
"form_basis": "직종 표기((인)·인부·공)",
|
||||
"basis_quantity": 1.0,
|
||||
"basis_unit": "인",
|
||||
"basis_source": "본문",
|
||||
"basis_quantity": null,
|
||||
"basis_unit": null,
|
||||
"basis_source": null,
|
||||
"resource_shares": {},
|
||||
"partial_ratio": false,
|
||||
"expression_cells": [],
|
||||
@@ -27187,9 +27187,9 @@
|
||||
"source_line": 4557,
|
||||
"pum_form": "requirement",
|
||||
"form_basis": "직종 표기((인)·인부·공)",
|
||||
"basis_quantity": 1.0,
|
||||
"basis_unit": "인",
|
||||
"basis_source": "본문",
|
||||
"basis_quantity": null,
|
||||
"basis_unit": null,
|
||||
"basis_source": null,
|
||||
"resource_shares": {},
|
||||
"partial_ratio": false,
|
||||
"expression_cells": [],
|
||||
@@ -39551,9 +39551,9 @@
|
||||
"source_line": 7025,
|
||||
"pum_form": "requirement",
|
||||
"form_basis": "직종 표기((인)·인부·공)",
|
||||
"basis_quantity": 1.0,
|
||||
"basis_unit": "인",
|
||||
"basis_source": "본문",
|
||||
"basis_quantity": null,
|
||||
"basis_unit": null,
|
||||
"basis_source": null,
|
||||
"resource_shares": {},
|
||||
"partial_ratio": false,
|
||||
"expression_cells": [],
|
||||
|
||||
@@ -446,6 +446,9 @@ export const ui_locales_b2 = {
|
||||
B06_Design_Area_Unit: ["㎡", "㎡"],
|
||||
B06_Design_Area_Highlight: ["누르면 해당 면적을 강조합니다", "Click to highlight this area"],
|
||||
B06_Design_Fill_Area: ["성토", "Fill"],
|
||||
// 사토장(유용토운반작업장) — **성토와 갈라 보인다**. 합치면 「쌓기」가 갑자기 커 보여
|
||||
// 사람이 오해한다(2026-09-09 네 창 확정 ㉠).
|
||||
B06_Design_SpoilFill_Area: ["사토장", "Spoil site"],
|
||||
B06_Design_Unset: ["미지정", "Not set"],
|
||||
B06_Design_DitchType_Legend: ["측구형식", "Ditch type"],
|
||||
B06_Design_DitchType_Standard: ["일반", "Standard"],
|
||||
|
||||
Reference in New Issue
Block a user