feat(B08): 사토장이 서면 사토 운반거리가 측점에서 나옴
사토장이 **측점 위에만** 서므로(사용자 확정 ③) 거리는 「발생점 → 사토장 측점」 누가거리로 그냥 나옴. 가정할 것이 없어짐. - `_spoil_sites` — 측점 설계에 실려 온 사토장 구간값을 구조물 단위로 접음(다시 안 셈) - 거리 = 발생점별 물량 가중평균 + `extra_distance_m`(항상 더함) - 사토장이 없으면 종전대로 설계 입력값 · 그것도 없으면 막힘(임의 거리 안 넣음) - 사유에 사토장 측점·수용량·**못 담는 몫**을 실어 화면이 드러내게 함 - `distance_basis` 칸 신설 — 받는 쪽이 「측점 기준」과 「설계 입력값」을 갈라 봄 ⚠ 사토장에 쌓아도 **유토곡선 사토는 줄지 않음** — 그 흙은 여전히 실어 내야 하는 흙이고 사토장은 목적지임. 줄이면 운반비가 사라짐. (네 창에 확인 요청함) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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"] = [
|
||||
@@ -163,7 +163,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` 는 **공제·가산이 끝난 값**이다(채집석 공제는 빼고 구조물 잔토는 더한 뒤).
|
||||
@@ -225,10 +293,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,
|
||||
|
||||
Reference in New Issue
Block a user