Merge remote-tracking branch 'origin/dev' into main_laptop_1
This commit is contained in:
@@ -246,8 +246,15 @@ async def _recompute(project_id: UUID | str, route_id: int) -> int:
|
||||
rows = output.get("areas")
|
||||
mass_haul = output.get("mass_haul")
|
||||
# 선 다단 벽 목록(④) — 목록째 얹음(수가 아니라 `_AREA_KEYS` 로는 못 거름). 주인 측점엔 빈 목록도.
|
||||
# 관 기준벽 벽 몸 겹침(㉡) — 역할별 ㎡ 한 벌. 빈 dict 도 실어 옛 값을 지움.
|
||||
extra_walls = [
|
||||
(float(row["chainage_m"]), {"extra_walls": row["extra_walls"]})
|
||||
(
|
||||
float(row["chainage_m"]),
|
||||
{
|
||||
"extra_walls": row["extra_walls"],
|
||||
"wall_fill_overlap": row.get("wall_fill_overlap") or {},
|
||||
},
|
||||
)
|
||||
for row in output.get("extra_walls") or []
|
||||
if isinstance(row, dict) and isinstance(row.get("extra_walls"), list)
|
||||
]
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
* DOM·SVG 를 부르지 않는다. 브라우저에서도 Node 에서도 같은 결과가 나와야 한다.
|
||||
* ========================================================================== */
|
||||
|
||||
import { computeStructureAreas } from "@util/common_util_cross_structure_areas";
|
||||
import { computeStructureAreas, wallBodyInFillM2 } from "@util/common_util_cross_structure_areas";
|
||||
import type { CrossSection } from "./B06_Section_Api_Fetch";
|
||||
import { computeBoxLayout, DEFAULT_BOX_SIDE_ADJUST } from "./B06_Section_UI_Cross_Box_Geom";
|
||||
import { REVET_EMBED_DEPTH_M } from "./B06_Section_UI_Cross_Culvert_Const";
|
||||
@@ -149,6 +149,22 @@ export const STRUCTURE_ROW_KEYS = [
|
||||
"pipe_length_m",
|
||||
] as const;
|
||||
|
||||
/** 폐회로 면적 입력(설계선·지반선·트림) — 트림이나 설계선이 모자라면 null. */
|
||||
function areaInputOf(section: CrossSection, layouts: StoredLayouts) {
|
||||
const trim = trimOfLayouts(layouts);
|
||||
const designLine = layouts.design.design_line;
|
||||
if (!trim || !Array.isArray(designLine) || designLine.length < 2) return null;
|
||||
const ground = section.samples
|
||||
.filter((sample) => sample.valid !== false && typeof sample.elevation_m === "number")
|
||||
.map((sample) => ({ offset: sample.offset_m ?? 0, elevation: sample.elevation_m as number }))
|
||||
.sort((a, b) => a.offset - b.offset);
|
||||
return {
|
||||
designLine: designLine as Array<{ offset_m: number; elevation_m: number }>,
|
||||
ground,
|
||||
trim,
|
||||
};
|
||||
}
|
||||
|
||||
/** 측점 하나의 폐회로 면적 — 구조물이 없으면 null(표준 계산값이 이미 맞다). */
|
||||
function areaRowOf(
|
||||
section: CrossSection,
|
||||
@@ -181,17 +197,11 @@ function areaRowOf(
|
||||
typeof pipeLengthM === "number" && pipeLengthM > 0
|
||||
? { chainage_m: section.chainage_m, pipe_length_m: Number(pipeLengthM.toFixed(4)) }
|
||||
: null;
|
||||
const trim = trimOfLayouts(layouts);
|
||||
const areaInput = areaInputOf(section, layouts);
|
||||
if (!areaInput) return pipeRow;
|
||||
const design = layouts.design;
|
||||
if (!trim || !Array.isArray(design.design_line) || design.design_line.length < 2) return pipeRow;
|
||||
const ground = section.samples
|
||||
.filter((sample) => sample.valid !== false && typeof sample.elevation_m === "number")
|
||||
.map((sample) => ({ offset: sample.offset_m ?? 0, elevation: sample.elevation_m as number }))
|
||||
.sort((a, b) => a.offset - b.offset);
|
||||
const areas = computeStructureAreas({
|
||||
designLine: design.design_line as Array<{ offset_m: number; elevation_m: number }>,
|
||||
ground,
|
||||
trim,
|
||||
...areaInput,
|
||||
rockBoundaryOffsetM:
|
||||
typeof design.rock_boundary_offset_m === "number" ? design.rock_boundary_offset_m : null,
|
||||
});
|
||||
@@ -236,6 +246,15 @@ export interface BuiltExtraWall {
|
||||
form_set: boolean;
|
||||
before_m: number;
|
||||
after_m: number;
|
||||
/** 벽 몸 중 성토 폐회로 안에 든 넓이(㎡) — 구조물 면적을 안 빼서(2026-09-06 확정) 성토에도 셈.
|
||||
* B08 이 「두 번 셈」 사유로 올림(2026-09-14 브레인). 면적을 못 내는 측점은 null. */
|
||||
fill_overlap_m2: number | null;
|
||||
}
|
||||
|
||||
export interface ExtraWallRow {
|
||||
chainage_m: number;
|
||||
extra_walls: BuiltExtraWall[];
|
||||
wall_fill_overlap: Record<string, number>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -243,17 +262,18 @@ export interface BuiltExtraWall {
|
||||
*
|
||||
* 단 수·높이는 지형이 정해 요청보다 적게 설 수 있음 — 그래서 요청 칸(`extra_wall_counts`)이 아니라
|
||||
* 기하가 세운 결과를 남김(관 연장 `pipe_length_m` 과 같은 길). 주인 측점에는 빈 목록도 실어 옛 값을 지움.
|
||||
* `wall_fill_overlap` — 관 기준벽 역할별 벽 몸 ∩ 성토 폐회로(㎡). B08 기준벽 줄 비고.
|
||||
*/
|
||||
export function extraWallRows(
|
||||
sections: readonly CrossSection[],
|
||||
): Array<{ chainage_m: number; extra_walls: BuiltExtraWall[] }> {
|
||||
const rows: Array<{ chainage_m: number; extra_walls: BuiltExtraWall[] }> = [];
|
||||
export function extraWallRows(sections: readonly CrossSection[]): ExtraWallRow[] {
|
||||
const rows: ExtraWallRow[] = [];
|
||||
for (const section of sections) {
|
||||
if (!section.culvert) continue;
|
||||
const owner = pipeOwnerChainage(section, sections);
|
||||
if (owner !== null && Math.abs(owner - section.chainage_m) > CHAINAGE_TOLERANCE_M) continue;
|
||||
const culvert = computeStoredLayouts(section, sections)?.culvert;
|
||||
if (!culvert) continue;
|
||||
const layouts = computeStoredLayouts(section, sections);
|
||||
const culvert = layouts?.culvert;
|
||||
if (!layouts || !culvert) continue;
|
||||
const areaInput = areaInputOf(section, layouts);
|
||||
const walls: BuiltExtraWall[] = [];
|
||||
const built: Array<["outlet" | "basin", WallLayout[], WallAdjust[]]> = [
|
||||
["outlet", culvert.extraWalls, culvert.revetShift.extras],
|
||||
@@ -272,10 +292,20 @@ export function extraWallRows(
|
||||
form_set: section.design?.revet_adjust?.[key]?.m != null,
|
||||
before_m: span.beforeM,
|
||||
after_m: span.afterM,
|
||||
fill_overlap_m2: areaInput
|
||||
? Number(wallBodyInFillM2(wall.points, areaInput).toFixed(3))
|
||||
: null,
|
||||
});
|
||||
});
|
||||
}
|
||||
rows.push({ chainage_m: section.chainage_m, extra_walls: walls });
|
||||
// 관 기준벽(유입·유출)도 같은 겹침 — 다단에만 사유가 뜨면 「다단만 문제」로 읽힘(㉡ 2026-09-14).
|
||||
const wall_fill_overlap: Record<string, number> = {};
|
||||
for (const wall of culvert.walls) {
|
||||
if (areaInput && (wall.role === "inlet" || wall.role === "outlet")) {
|
||||
wall_fill_overlap[wall.role] = Number(wallBodyInFillM2(wall.points, areaInput).toFixed(3));
|
||||
}
|
||||
}
|
||||
rows.push({ chainage_m: section.chainage_m, extra_walls: walls, wall_fill_overlap });
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
@@ -328,16 +328,36 @@ UNCONFIRMED_EXTRA_HEIGHT = (
|
||||
UNCONFIRMED_EXTRA_FORM = "다단 형태를 안 정해 기준벽의 기본 형태 「{form}」로 섰음"
|
||||
#: 첫 미확정 단 아래는 횡단도 면적도 없는 것으로 봄(B06 `confirmedOnly`) — 같은 선에서 끊음.
|
||||
UNCONFIRMED_EXTRA_ABOVE = "윗단({above}단)이 미확정이라 이 단도 미확정 — 윗단 높이를 적으면 섬"
|
||||
#: 벽 몸이 성토 폐회로에도 듦(2026-09-06 사용자 확정 「구조물 면적 안 뺌」) — 고치지 않고 사유로(2026-09-14 브레인).
|
||||
#: 넓이는 서버 Node 가 횡단도 벽 몸 ∩ 성토 폐회로로 잼(`wallBodyInFillM2` · `fill_overlap_m2`).
|
||||
NOTE_FILL_OVERLAP = (
|
||||
"⚠ 벽 몸 자리가 성토 면적에도 들어 있음(2026-09-06 확정 「구조물 면적 안 뺌」) — 이 측점 횡단도 "
|
||||
"벽 몸 {area:.2f}㎡ × 연장 {length:g}m ≈ {volume:.1f}㎥ 를 벽과 성토에 두 번 셈 · "
|
||||
"벽 뒤 막자갈 자리도 성토선 아래라 더 겹침"
|
||||
)
|
||||
|
||||
|
||||
#: 관 기준벽(유입·유출) 벽 몸 겹침 ㎡ — `design.wall_fill_overlap` {inlet, outlet}(서버 Node 가 잼, ㉡ 2026-09-14).
|
||||
#: 호출부(`Router_Material`)를 안 늘리려고 다단 목록 통에 `base` 표시 칸으로 실어 옴 — 다단 줄은 거름.
|
||||
WALL_OVERLAP_KEY = "wall_fill_overlap"
|
||||
|
||||
|
||||
def extra_walls_from_designs(designs: list[dict[str, Any]] | None) -> dict[float, list[dict]]:
|
||||
"""측점별 「선 다단 벽 목록」 — 목록이 있는 측점만(빈 목록도 담음 — 「다단 없음」)."""
|
||||
"""측점별 「선 다단 벽 목록」 — 목록이 있는 측점만(빈 목록도 담음 — 「다단 없음」).
|
||||
기준벽 겹침 값이 있으면 `{"base": 역할, "fill_overlap_m2": ㎡}` 칸을 뒤에 붙임."""
|
||||
found: dict[float, list[dict]] = {}
|
||||
for row in designs or []:
|
||||
design = row.get("design") if isinstance(row, dict) else None
|
||||
walls = design.get(EXTRA_WALLS_KEY) if isinstance(design, dict) else None
|
||||
if isinstance(walls, list):
|
||||
found[round(float(row.get("chainage_m") or 0.0), 3)] = walls
|
||||
if not isinstance(design, dict):
|
||||
continue
|
||||
walls = design.get(EXTRA_WALLS_KEY)
|
||||
overlaps = design.get(WALL_OVERLAP_KEY)
|
||||
if not isinstance(walls, list) and not isinstance(overlaps, dict):
|
||||
continue
|
||||
entries = list(walls) if isinstance(walls, list) else []
|
||||
if isinstance(overlaps, dict):
|
||||
entries += [{"base": role, "fill_overlap_m2": area} for role, area in overlaps.items()]
|
||||
found[round(float(row.get("chainage_m") or 0.0), 3)] = entries
|
||||
return found
|
||||
|
||||
|
||||
@@ -431,6 +451,12 @@ def facility_structures(
|
||||
walls[0][3].append(NOTE_SIDE_UNKNOWN.format(side=side))
|
||||
foundation = options.get("foundation" if legacy else "revet_foundation")
|
||||
kept = {k: v for k, v in options.items() if not k.startswith(("inlet_", "outlet_"))}
|
||||
station_walls = _extra_walls_at(extra_walls, chainage)
|
||||
base_overlap = {
|
||||
str(entry["base"]): _num_or_zero(entry.get("fill_overlap_m2"))
|
||||
for entry in station_walls
|
||||
if entry.get("base")
|
||||
}
|
||||
for role, label, values, notes, withheld, filled in walls:
|
||||
before, after = _num_or_zero(values["before_m"]), _num_or_zero(values["after_m"])
|
||||
row = child_row(base, "revetment", label, f"{role}_revet")
|
||||
@@ -438,6 +464,18 @@ def facility_structures(
|
||||
if legacy and _blank(values["height_m"]):
|
||||
# 높이는 설계자 입력(계획홍수위 + 여유고) — 없으면 줄만 서고 금액 밖.
|
||||
reasons.append(own_height_unconfirmed())
|
||||
# 좌·우 한 벽으로 합친 독립 기슭막이는 그 측점에 선 벽 몸 전부.
|
||||
overlap = (
|
||||
sum(base_overlap.values())
|
||||
if legacy and len(walls) == 1
|
||||
else base_overlap.get(role, 0.0)
|
||||
)
|
||||
if overlap > 0 and not reasons: # 미확정 벽은 금액에 안 듦 — 두 번 셀 금액이 없음
|
||||
notes.append(
|
||||
NOTE_FILL_OVERLAP.format(
|
||||
area=overlap, length=before + after, volume=overlap * (before + after)
|
||||
)
|
||||
)
|
||||
row.update(
|
||||
start_m=chainage - before,
|
||||
end_m=chainage + after,
|
||||
@@ -451,7 +489,9 @@ def facility_structures(
|
||||
parents = {role: (values, filled) for role, _label, values, _notes, _w, filled in walls}
|
||||
counts = {"outlet": 0, "basin": 0}
|
||||
first_unset: dict[str, int] = {}
|
||||
for tier in _extra_walls_at(extra_walls, chainage):
|
||||
for tier in station_walls:
|
||||
if tier.get("base"):
|
||||
continue
|
||||
side = "basin" if str(tier.get("side")) == "basin" else "outlet"
|
||||
counts[side] += 1
|
||||
if not tier.get("height_set"):
|
||||
@@ -471,6 +511,16 @@ def facility_structures(
|
||||
reasons.append(UNCONFIRMED_EXTRA_FORM.format(form=form))
|
||||
label = f"{EXTRA_SIDE_LABELS[side]} 다단 기슭막이 {counts[side]}단"
|
||||
row = child_row(base, "revetment", label, str(tier.get("key") or label))
|
||||
tier_notes = [
|
||||
f"ⓘ 횡단도에 선 다단 벽 — 높이 {height:g}m · 형태 {form} · 전 {before:g}/후 {after:g}m"
|
||||
]
|
||||
overlap = _num_or_zero(tier.get("fill_overlap_m2"))
|
||||
if overlap > 0 and not reasons: # 미확정 단은 면적에도 금액에도 안 듦
|
||||
tier_notes.append(
|
||||
NOTE_FILL_OVERLAP.format(
|
||||
area=overlap, length=before + after, volume=overlap * (before + after)
|
||||
)
|
||||
)
|
||||
row.update(
|
||||
start_m=chainage - before,
|
||||
end_m=chainage + after,
|
||||
@@ -483,9 +533,7 @@ def facility_structures(
|
||||
"after_m": after,
|
||||
"foundation": foundation,
|
||||
},
|
||||
notes=[
|
||||
f"ⓘ 횡단도에 선 다단 벽 — 높이 {height:g}m · 형태 {form} · 전 {before:g}/후 {after:g}m"
|
||||
],
|
||||
notes=tier_notes,
|
||||
withheld=False,
|
||||
unconfirmed=" · ".join(reasons),
|
||||
)
|
||||
|
||||
@@ -72,12 +72,10 @@ function interpolator(
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 구조물이 선 뒤의 절·성토 면적. 트림 바깥은 구조물이 그리는 폴리라인을 따르고, 그 선이
|
||||
* 없으면 트림 경계 표고에서 끊어 **지반선에 붙인다**(그 바깥은 손대지 않은 원지반이라
|
||||
* 면적이 0이 된다).
|
||||
*/
|
||||
export function computeStructureAreas(input: StructureAreaInput): StructureAreaResult | null {
|
||||
/** 폐회로의 위(실제로 그려지는 설계선)·아래(지반선) 보간기 — 면적과 벽 겹침이 같은 선을 봄. */
|
||||
function drawnProfile(
|
||||
input: StructureAreaInput,
|
||||
): { drawnZ: (offset: number) => number; ground: (offset: number) => number } | null {
|
||||
const design = interpolator(
|
||||
input.designLine.map((point) => ({ offset: point.offset_m, elevation: point.elevation_m })),
|
||||
);
|
||||
@@ -117,6 +115,19 @@ export function computeStructureAreas(input: StructureAreaInput): StructureAreaR
|
||||
}
|
||||
return design(offset);
|
||||
};
|
||||
return { drawnZ, ground };
|
||||
}
|
||||
|
||||
/**
|
||||
* 구조물이 선 뒤의 절·성토 면적. 트림 바깥은 구조물이 그리는 폴리라인을 따르고, 그 선이
|
||||
* 없으면 트림 경계 표고에서 끊어 **지반선에 붙인다**(그 바깥은 손대지 않은 원지반이라
|
||||
* 면적이 0이 된다).
|
||||
*/
|
||||
export function computeStructureAreas(input: StructureAreaInput): StructureAreaResult | null {
|
||||
const profile = drawnProfile(input);
|
||||
if (!profile) return null;
|
||||
const { drawnZ, ground } = profile;
|
||||
const { trim } = input;
|
||||
|
||||
// 적분 격자 = 지반 샘플 ∪ 설계선 꼭짓점 ∪ 트림 경계 ∪ 구조물 폴리라인 꼭짓점.
|
||||
// 꺾이는 자리를 모두 넣어야 사다리꼴 적분이 모서리를 잘라먹지 않는다.
|
||||
@@ -154,3 +165,39 @@ export function computeStructureAreas(input: StructureAreaInput): StructureAreaR
|
||||
cutRockAreaM2: cutRock,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 벽 몸 도형(볼록 다각형) 중 **성토 폐회로 안에 든 넓이**(㎡).
|
||||
* 폐회로는 구조물 면적을 안 뺌(2026-09-06 확정)이라 벽 몸이 성토에도 셈 — 고치지 않고 그 겹침을
|
||||
* 재서 수량 사유로 올림(2026-09-14 브레인). 세로줄로 잘라 [max(몸 밑, 지반), min(몸 위, 설계선)] 을 쌓음.
|
||||
*/
|
||||
export function wallBodyInFillM2(
|
||||
points: Array<{ offset: number; elevation: number }>,
|
||||
input: StructureAreaInput,
|
||||
steps = 400,
|
||||
): number {
|
||||
const profile = drawnProfile(input);
|
||||
if (!profile || points.length < 3) return 0;
|
||||
const offsets = points.map((point) => point.offset);
|
||||
const lo = Math.min(...offsets);
|
||||
const hi = Math.max(...offsets);
|
||||
if (!(hi > lo)) return 0;
|
||||
const dx = (hi - lo) / steps;
|
||||
let area = 0;
|
||||
for (let index = 0; index < steps; index += 1) {
|
||||
const x = lo + (index + 0.5) * dx;
|
||||
const hits: number[] = [];
|
||||
points.forEach((p, k) => {
|
||||
const q = points[(k + 1) % points.length];
|
||||
if ((p.offset - x) * (q.offset - x) > 0 || Math.abs(q.offset - p.offset) < 1e-12) return;
|
||||
hits.push(
|
||||
p.elevation + ((q.elevation - p.elevation) * (x - p.offset)) / (q.offset - p.offset),
|
||||
);
|
||||
});
|
||||
if (hits.length < 2) continue;
|
||||
const top = Math.min(Math.max(...hits), profile.drawnZ(x));
|
||||
const bottom = Math.max(Math.min(...hits), profile.ground(x));
|
||||
if (top > bottom) area += (top - bottom) * dx;
|
||||
}
|
||||
return area;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
"""벽 몸 중 **성토 폐회로 안에 든 넓이** — `wallBodyInFillM2` (2026-09-14 브레인 「사유로 드러낼 것」).
|
||||
|
||||
폐회로 면적은 구조물 면적을 안 뺌(2026-09-06 사용자 확정)이라 벽 몸이 성토에도 들어감.
|
||||
그 겹침을 재서 B08 줄 사유로 올림 — 고치지 않음. 실제 코드를 컴파일해 Node 로 돌림.
|
||||
"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
TSC = PROJECT_ROOT / "config" / "node_modules" / "typescript" / "bin" / "tsc"
|
||||
MODULE = PROJECT_ROOT / "common_util" / "common_util_cross_structure_areas.ts"
|
||||
|
||||
# 지반 0 · 트림 밖(offset 5~20)은 표고 1.0 수평 성토선 · 벽 몸 offset 10~11 · 표고 −0.5~1.5
|
||||
# ⇒ 성토 안(지반 0 ~ 선 1.0)에 든 몸 = 1 × 1 = 1.0㎡ · 지반 아래·선 위는 안 셈.
|
||||
_RUNNER = """
|
||||
const { wallBodyInFillM2 } = require(process.argv[3]);
|
||||
const { writeFileSync } = require("node:fs");
|
||||
const line = [];
|
||||
for (let o = -20; o <= 20; o += 1) line.push({ offset: o, elevation: 0 });
|
||||
const input = {
|
||||
designLine: line.map((p) => ({ offset_m: p.offset, elevation_m: 0 })),
|
||||
ground: line,
|
||||
trim: { minOffset: -5, maxOffset: 5, maxSlope: { points: [{ offset: 5, elevation: 1 }, { offset: 20, elevation: 1 }] } },
|
||||
};
|
||||
const body = [
|
||||
{ offset: 10, elevation: -0.5 },
|
||||
{ offset: 10, elevation: 1.5 },
|
||||
{ offset: 11, elevation: 1.5 },
|
||||
{ offset: 11, elevation: -0.5 },
|
||||
];
|
||||
const outside = body.map((p) => ({ offset: p.offset - 30, elevation: p.elevation }));
|
||||
writeFileSync(process.argv[2], JSON.stringify({ inFill: wallBodyInFillM2(body, input), outside: wallBodyInFillM2(outside, input) }));
|
||||
"""
|
||||
|
||||
|
||||
@pytest.mark.skipif(not TSC.is_file(), reason="프론트엔드 의존성 미설치")
|
||||
def test_벽_몸_중_성토_안_넓이만_잰다(tmp_path: Path) -> None:
|
||||
out = tmp_path / "out"
|
||||
subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
[
|
||||
"node",
|
||||
str(TSC),
|
||||
"--ignoreConfig",
|
||||
"--target",
|
||||
"es2022",
|
||||
"--module",
|
||||
"commonjs",
|
||||
"--skipLibCheck",
|
||||
"--outDir",
|
||||
str(out),
|
||||
str(MODULE),
|
||||
],
|
||||
cwd=str(PROJECT_ROOT),
|
||||
check=False,
|
||||
capture_output=True,
|
||||
)
|
||||
compiled = next(out.rglob("common_util_cross_structure_areas.js"), None)
|
||||
assert compiled is not None, "tsc 실패"
|
||||
(out / "runner.cjs").write_text(_RUNNER, encoding="utf-8")
|
||||
result = tmp_path / "result.json"
|
||||
subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
["node", str(out / "runner.cjs"), str(result), str(compiled)],
|
||||
cwd=str(PROJECT_ROOT),
|
||||
check=True,
|
||||
capture_output=True,
|
||||
)
|
||||
produced = json.loads(result.read_text(encoding="utf-8"))
|
||||
assert produced["inFill"] == pytest.approx(1.0, abs=1e-3)
|
||||
assert produced["outside"] == pytest.approx(0.0, abs=1e-9) # 설계선이 지반 그대로인 자리
|
||||
@@ -125,6 +125,40 @@ def test_미확정_단은_성토를_안_깎는다() -> None:
|
||||
assert "applied.h == null" in geom
|
||||
|
||||
|
||||
def test_벽_몸이_성토에도_든_겹침을_사유로_드러냄() -> None:
|
||||
"""2026-09-14 브레인 — 폐회로는 구조물 면적을 안 뺌(2026-09-06 확정)이라 벽 몸이 성토에도 셈.
|
||||
고치지 않고 사유로: 258.12 실측 1단 벽 몸 1.335㎡ 가 성토 폐회로 안."""
|
||||
walls = [dict(WALLS[0], fill_overlap_m2=1.336, before_m=5.0, after_m=5.0)]
|
||||
note = " ".join(_tiers(facility_structures([POINT], {85.0: walls}))[0]["notes"])
|
||||
assert "성토 면적에도" in note and "1.34㎡" in note and "13.4㎥" in note
|
||||
bare = " ".join(_tiers(facility_structures([POINT], {85.0: WALLS[:1]}))[0]["notes"])
|
||||
assert "성토 면적에도" not in bare # 값이 없으면(옛 저장본) 지어내지 않음
|
||||
|
||||
|
||||
def test_서버가_벽_몸_겹침을_잰다() -> None:
|
||||
assert "wallBodyInFillM2(" in LAYOUTS and "fill_overlap_m2" in LAYOUTS
|
||||
|
||||
|
||||
def test_관_기준벽도_겹침_사유가_뜬다() -> None:
|
||||
"""2026-09-14 브레인 ㉡ — 같은 겹침이 관 기준벽에도 있음. 다단에만 뜨면 「다단만 문제」로 읽힘.
|
||||
258.12 실측 유출 벽 몸 1.417㎡ 가 성토 폐회로 안."""
|
||||
designs = [
|
||||
{"chainage_m": 85.0, "design": {"extra_walls": [], "wall_fill_overlap": {"outlet": 1.417}}}
|
||||
]
|
||||
rows = facility_structures([POINT], extra_walls_from_designs(designs))
|
||||
by_label = {
|
||||
row["attachment_label"]: " ".join(row["notes"])
|
||||
for row in rows
|
||||
if row.get("notes") is not None
|
||||
}
|
||||
outlet = next(text for label, text in by_label.items() if "유출" in label)
|
||||
inlet = next(text for label, text in by_label.items() if "유입" in label)
|
||||
assert "성토 면적에도" in outlet and "1.42㎡" in outlet and "14.2㎥" in outlet
|
||||
assert "성토 면적에도" not in inlet # 잰 값이 없는 벽엔 지어내지 않음
|
||||
assert not _tiers(rows) # 기준벽 값이 다단 줄로 새지 않음
|
||||
assert "wall_fill_overlap" in LAYOUTS and '"wall_fill_overlap"' in PREBUILD
|
||||
|
||||
|
||||
def test_목록이_없으면_종전과_같다() -> None:
|
||||
assert not _tiers(facility_structures([POINT]))
|
||||
assert not _tiers(facility_structures([POINT], {}))
|
||||
|
||||
Reference in New Issue
Block a user