fix(B07): 장 확정 수량표 결함 3건 — 측점 구분·계획고·확정 해제
장 단위 확정 흐름을 실측하다 드러난 것들이다(2026-09-03, 용화 검증본 3장).
① **측점 구분 없이 역추출** — `_table_values_from_cells()` 가 도면의 표를 전부 훑어
마지막 값을 모든 측점에 넣었다(실측: 4개 측점 전부 지반고 837.21, 도면에는
840.87·840.33·836.45·837.21 로 제대로 그려져 있었음). 표 엔티티 id 가
`uuid5("{도면id}:qtable")` 이라 측점을 되짚을 수 있어 그 표만 읽는다. 옛 도면은
id 가 안 맞으므로 종전처럼 전부 훑는 폴백을 남긴다.
② **계획고가 빈 채로 나감** — 장 배치 입력의 원본에는 계획고가 없어 계획고·절토고·성토고
세 칸이 통째로 비었다(21개 항목 중 지반고 하나만 채워짐). `_quantity_table()` 이
횡단 설계(`design.design_elevation_m`)도 보게 했다.
③ **확정 해제가 404** — [수정]이 도면 목록을 **설계값 없이** 만들어 장 나눔이 달라졌고,
방금 확정한 장 id 가 목록에 없어 되돌릴 수 없었다. 목록 조회·확정과 같은 인자를 쓴다.
검증(공용 브라우저·실동작) — 확정 해제 404 → **200**, 표 값 `planned=840.87 fill=0.00` 등
측점마다 다름, manifest 수량표가 측점별로 갈림(220 · 240 · 260 · 264 · 280 각각 4/21 항목
채움 — 나머지 17칸은 사용자가 CAD 에서 채우는 자리). 시험 뒤 확정은 모두 해제해 원상복구.
pytest 383 passed·17 skipped, ruff format 무변경.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -223,12 +223,28 @@ def _cross_table_entities(
|
||||
]
|
||||
|
||||
|
||||
def _table_values_from_cells(entities: list[Any]) -> dict[str, float | None]:
|
||||
"""표 객체의 칸에서 값을 읽는다. 칸의 key가 어느 수량인지 알려 준다."""
|
||||
def _table_values_from_cells(
|
||||
entities: list[Any], drawing_id: str | None = None
|
||||
) -> dict[str, float | None]:
|
||||
"""표 객체의 칸에서 값을 읽는다. 칸의 key가 어느 수량인지 알려 준다.
|
||||
|
||||
**한 도면에 표가 여럿이면 그 측점 것만 읽는다**(장은 측점 4~6개를 담는다).
|
||||
표 엔티티 id 는 `uuid5(f"{도면id}:qtable")` 이라 측점을 되짚을 수 있다. 이 걸름이
|
||||
없던 동안 장 확정이 **모든 측점에 마지막 표 값을 넣었다**(2026-09-03 실측: 4개 측점이
|
||||
전부 지반고 837.21 — 도면에는 840.87·840.33·836.45·837.21 로 제대로 그려져 있었다).
|
||||
id 로 못 찾으면(옛 도면) 종전처럼 전부 훑는다.
|
||||
"""
|
||||
wanted = str(uuid5(_ENTITY_NS, f"{drawing_id}:qtable")) if drawing_id else None
|
||||
if wanted is not None and not any(
|
||||
isinstance(entity, dict) and entity.get("id") == wanted for entity in entities
|
||||
):
|
||||
wanted = None
|
||||
table: dict[str, float | None] = {}
|
||||
for entity in entities:
|
||||
if not isinstance(entity, dict) or entity.get("type") != "Table":
|
||||
continue
|
||||
if wanted is not None and entity.get("id") != wanted:
|
||||
continue
|
||||
shape = entity.get("shapeData")
|
||||
rows = shape.get("cells") if isinstance(shape, dict) else None
|
||||
if not isinstance(rows, list):
|
||||
@@ -281,7 +297,7 @@ def extract_quantity_table(
|
||||
entities = drawing.get("entities")
|
||||
if not isinstance(entities, list):
|
||||
return None
|
||||
table = _table_values_from_cells(entities)
|
||||
table = _table_values_from_cells(entities, drawing_id)
|
||||
if not table:
|
||||
table = _table_values_from_texts(drawing_id, entities)
|
||||
if not table:
|
||||
|
||||
@@ -462,7 +462,11 @@ async def invalidate_design_drawing(
|
||||
"""확정 도면 편집 시 B07 및 이후 단계를 미확정 상태로 되돌린다."""
|
||||
try:
|
||||
route_id, project_root, longitudinal_path = await _confirmed_source(project_id)
|
||||
items = await asyncio.to_thread(_drawing_list, project_root, longitudinal_path)
|
||||
# 목록은 **설계값을 넣어** 만든다 — 장 나눔이 측점 표시 폭에 따라 달라지므로,
|
||||
# 설계 없이 만들면 방금 확정한 장 id 가 목록에 없어 [수정]이 404 로 막힌다
|
||||
# (2026-09-03 실측: `cross_s00220m` 확정 후 확정 해제 불가).
|
||||
designs = await _designs_by_chainage(route_id)
|
||||
items = await asyncio.to_thread(_drawing_list, project_root, longitudinal_path, designs)
|
||||
if drawing_id not in {item.id for item in items}:
|
||||
raise FileNotFoundError("변경된 도면을 찾을 수 없습니다.")
|
||||
await asyncio.to_thread(_invalidate_drawing, project_root, drawing_id)
|
||||
@@ -472,7 +476,6 @@ async def invalidate_design_drawing(
|
||||
if cross_match:
|
||||
stale = [int(cross_match.group(1))]
|
||||
elif CROSS_SHEET_ID.fullmatch(drawing_id):
|
||||
designs = await _designs_by_chainage(route_id)
|
||||
plan = await asyncio.to_thread(
|
||||
_cross_sheet_plan, project_root, longitudinal_path, designs
|
||||
)
|
||||
|
||||
@@ -466,12 +466,18 @@ def _cross_sheet_plan(
|
||||
return plan_cross_sheets(blocks)
|
||||
|
||||
|
||||
def _quantity_table(source: dict[str, Any]) -> dict[str, float | None]:
|
||||
def _quantity_table(
|
||||
source: dict[str, Any], design: dict[str, Any] | None = None
|
||||
) -> dict[str, float | None]:
|
||||
"""횡단면 원본에서 편집 가능한 수량 산출표 값을 구조화한다 (납품 양식 키).
|
||||
|
||||
center_z→지반고, design_elevation_m→계획고, 절토고/성토고는 파생 초기값.
|
||||
나머지 항목은 source["quantities"]에 같은 키가 있으면 읽고 없으면 None으로
|
||||
두어 CAD 테이블에서 사용자가 채운다.
|
||||
|
||||
**계획고는 횡단 설계(design)에도 있다** — 장 배치 입력의 원본에는 그 값이 없어
|
||||
계획고·절토고·성토고 세 칸이 통째로 비어 나갔다(2026-09-03 실측: 장 확정 시 21개
|
||||
항목 중 지반고 하나만 채워짐). 원본에 없으면 설계에서 읽는다.
|
||||
"""
|
||||
|
||||
def num(value: Any) -> float | None:
|
||||
@@ -479,6 +485,8 @@ def _quantity_table(source: dict[str, Any]) -> dict[str, float | None]:
|
||||
|
||||
ground = num(source.get("center_z"))
|
||||
planned = num(source.get("planned_elevation_m", source.get("design_elevation_m")))
|
||||
if planned is None and isinstance(design, dict):
|
||||
planned = num(design.get("design_elevation_m"))
|
||||
cut = max(ground - planned, 0.0) if ground is not None and planned is not None else None
|
||||
fill = max(planned - ground, 0.0) if ground is not None and planned is not None else None
|
||||
quantities = source.get("quantities") if isinstance(source.get("quantities"), dict) else {}
|
||||
@@ -568,7 +576,7 @@ def _cross_section_input(
|
||||
"source": source,
|
||||
"design": design,
|
||||
"design_line": _cross_design_line(longitudinal_path, source, design),
|
||||
"quantity_table": _quantity_table(source),
|
||||
"quantity_table": _quantity_table(source, design),
|
||||
"title": station_no_label(float(source.get("chainage_m", chainage)), interval),
|
||||
}
|
||||
|
||||
@@ -697,7 +705,7 @@ def _read_drawing(
|
||||
source = _read_json(path)
|
||||
label = str(source.get("label") or drawing_id)
|
||||
design_line = _cross_design_line(longitudinal_path, source, stored_design)
|
||||
quantity_table = _quantity_table(source)
|
||||
quantity_table = _quantity_table(source, stored_design)
|
||||
# 수량표 제목행 No. 표기: 종단 측점 간격 기준 (납품 도면 양식)
|
||||
longitudinal = _read_json(longitudinal_path)
|
||||
interval = infer_station_interval(longitudinal.get("stations") or [])
|
||||
|
||||
Reference in New Issue
Block a user