feat(b06,b08): 관 기준벽도 벽 몸 ∩ 성토 폐회로 겹침을 줄 사유로 — 다단에만 뜨면 「다단만 문제」로 읽힘(㉡)

서버 Node 가 기준벽 역할별 넓이를 design.wall_fill_overlap 에 남기고 B08 이 다단 목록 통(base 표시 칸)으로 읽어 기준벽 줄 비고에 붙임 · 호출부(Router_Material) 안 늘림 · 936be972 빈 저장: 관 9 곳 유출 벽 258.12 1.417 · 720 1.403 · 804.18 1.166 · 982.64 0.807 · 620 0.136 · 440 0.046 · 533.98 0.004 · 85.05/900 0㎡ · 이 프로젝트 기준벽은 모두 미확정(금액 밖)이라 비고 안 뜸 · 벽 칸을 적으면 「1.42㎡ × 10m ≈ 14.2㎥」 · 금액 변화 0

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq
This commit is contained in:
2026-09-14 15:02:43 +09:00
co-authored by Claude Opus 5
parent eb79091e00
commit c7b9956bf3
4 changed files with 83 additions and 11 deletions
@@ -246,8 +246,15 @@ async def _recompute(project_id: UUID | str, route_id: int) -> int:
rows = output.get("areas") rows = output.get("areas")
mass_haul = output.get("mass_haul") mass_haul = output.get("mass_haul")
# 선 다단 벽 목록(④) — 목록째 얹음(수가 아니라 `_AREA_KEYS` 로는 못 거름). 주인 측점엔 빈 목록도. # 선 다단 벽 목록(④) — 목록째 얹음(수가 아니라 `_AREA_KEYS` 로는 못 거름). 주인 측점엔 빈 목록도.
# 관 기준벽 벽 몸 겹침(㉡) — 역할별 ㎡ 한 벌. 빈 dict 도 실어 옛 값을 지움.
extra_walls = [ 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 [] for row in output.get("extra_walls") or []
if isinstance(row, dict) and isinstance(row.get("extra_walls"), list) if isinstance(row, dict) and isinstance(row.get("extra_walls"), list)
] ]
+17 -5
View File
@@ -251,16 +251,21 @@ export interface BuiltExtraWall {
fill_overlap_m2: number | null; fill_overlap_m2: number | null;
} }
export interface ExtraWallRow {
chainage_m: number;
extra_walls: BuiltExtraWall[];
wall_fill_overlap: Record<string, number>;
}
/** /**
* 관(·독립 기슭막이) 주인 측점마다 **실제로 선** 다단 벽 목록(④, 2026-09-14). * 관(·독립 기슭막이) 주인 측점마다 **실제로 선** 다단 벽 목록(④, 2026-09-14).
* *
* 단 수·높이는 지형이 정해 요청보다 적게 설 수 있음 — 그래서 요청 칸(`extra_wall_counts`)이 아니라 * 단 수·높이는 지형이 정해 요청보다 적게 설 수 있음 — 그래서 요청 칸(`extra_wall_counts`)이 아니라
* 기하가 세운 결과를 남김(관 연장 `pipe_length_m` 과 같은 길). 주인 측점에는 빈 목록도 실어 옛 값을 지움. * 기하가 세운 결과를 남김(관 연장 `pipe_length_m` 과 같은 길). 주인 측점에는 빈 목록도 실어 옛 값을 지움.
* `wall_fill_overlap` — 관 기준벽 역할별 벽 몸 ∩ 성토 폐회로(㎡). B08 기준벽 줄 비고.
*/ */
export function extraWallRows( export function extraWallRows(sections: readonly CrossSection[]): ExtraWallRow[] {
sections: readonly CrossSection[], const rows: ExtraWallRow[] = [];
): Array<{ chainage_m: number; extra_walls: BuiltExtraWall[] }> {
const rows: Array<{ chainage_m: number; extra_walls: BuiltExtraWall[] }> = [];
for (const section of sections) { for (const section of sections) {
if (!section.culvert) continue; if (!section.culvert) continue;
const owner = pipeOwnerChainage(section, sections); const owner = pipeOwnerChainage(section, sections);
@@ -293,7 +298,14 @@ export function extraWallRows(
}); });
}); });
} }
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; return rows;
} }
+38 -5
View File
@@ -337,14 +337,27 @@ NOTE_FILL_OVERLAP = (
) )
#: 관 기준벽(유입·유출) 벽 몸 겹침 ㎡ — `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]]: def extra_walls_from_designs(designs: list[dict[str, Any]] | None) -> dict[float, list[dict]]:
"""측점별 「선 다단 벽 목록」 — 목록이 있는 측점만(빈 목록도 담음 — 「다단 없음」).""" """측점별 「선 다단 벽 목록」 — 목록이 있는 측점만(빈 목록도 담음 — 「다단 없음」).
기준벽 겹침 값이 있으면 `{"base": 역할, "fill_overlap_m2": ㎡}` 칸을 뒤에 붙임."""
found: dict[float, list[dict]] = {} found: dict[float, list[dict]] = {}
for row in designs or []: for row in designs or []:
design = row.get("design") if isinstance(row, dict) else None design = row.get("design") if isinstance(row, dict) else None
walls = design.get(EXTRA_WALLS_KEY) if isinstance(design, dict) else None if not isinstance(design, dict):
if isinstance(walls, list): continue
found[round(float(row.get("chainage_m") or 0.0), 3)] = walls 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 return found
@@ -438,6 +451,12 @@ def facility_structures(
walls[0][3].append(NOTE_SIDE_UNKNOWN.format(side=side)) walls[0][3].append(NOTE_SIDE_UNKNOWN.format(side=side))
foundation = options.get("foundation" if legacy else "revet_foundation") foundation = options.get("foundation" if legacy else "revet_foundation")
kept = {k: v for k, v in options.items() if not k.startswith(("inlet_", "outlet_"))} 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: for role, label, values, notes, withheld, filled in walls:
before, after = _num_or_zero(values["before_m"]), _num_or_zero(values["after_m"]) before, after = _num_or_zero(values["before_m"]), _num_or_zero(values["after_m"])
row = child_row(base, "revetment", label, f"{role}_revet") row = child_row(base, "revetment", label, f"{role}_revet")
@@ -445,6 +464,18 @@ def facility_structures(
if legacy and _blank(values["height_m"]): if legacy and _blank(values["height_m"]):
# 높이는 설계자 입력(계획홍수위 + 여유고) — 없으면 줄만 서고 금액 밖. # 높이는 설계자 입력(계획홍수위 + 여유고) — 없으면 줄만 서고 금액 밖.
reasons.append(own_height_unconfirmed()) 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( row.update(
start_m=chainage - before, start_m=chainage - before,
end_m=chainage + after, end_m=chainage + after,
@@ -458,7 +489,9 @@ def facility_structures(
parents = {role: (values, filled) for role, _label, values, _notes, _w, filled in walls} parents = {role: (values, filled) for role, _label, values, _notes, _w, filled in walls}
counts = {"outlet": 0, "basin": 0} counts = {"outlet": 0, "basin": 0}
first_unset: dict[str, int] = {} 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" side = "basin" if str(tier.get("side")) == "basin" else "outlet"
counts[side] += 1 counts[side] += 1
if not tier.get("height_set"): if not tier.get("height_set"):
+20
View File
@@ -139,6 +139,26 @@ def test_서버가_벽_몸_겹침을_잰다() -> None:
assert "wallBodyInFillM2(" in LAYOUTS and "fill_overlap_m2" in LAYOUTS 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: def test_목록이_없으면_종전과_같다() -> None:
assert not _tiers(facility_structures([POINT])) assert not _tiers(facility_structures([POINT]))
assert not _tiers(facility_structures([POINT], {})) assert not _tiers(facility_structures([POINT], {}))