fix(B05,B06): 최소곡선반지름 조회가 없는 컬럼을 읽던 것 · 재계산 중단 사유 로그

보조 창이 노선 편집 [확인] 실측에서 둘을 잡음.

1. projects 에 design_speed_kph / terrain_type 컬럼이 없음(있는 것은 road_type 뿐).
   매 요청마다 OperationalError(1054) 가 ERROR 로그로 남고 기본값 폴백으로 넘어갔음.
   - 임도 종류는 projects.road_type, 설계속도·지형은 워크플로 stage 2 params 에서 읽음.
   - 산식은 이미 있던 B05_Profile_Engine_Grade.legal_plan_radius_min_m 를 씀 —
     내가 같은 표를 다시 짜 두었던 것을 지움(중복 제거).
2. 재확정 체인이 solve_route 400 으로 끊길 때 상태 코드만 남겨 원인을 못 짚었음.
   본문(사유)까지 로그에 남김. 노선 편집 [확인]이 조용히 끊겨 배수유역·관은 새 노선으로
   가고 종횡단만 옛 노선에 남는 어긋남이 굳었던 자리임.

recompute 의 상세 조회와 DB 두 건도 asyncio.gather 로 묶음(원격 DB 왕복 약 12ms/질의).

시험 409 통과·17 건너뜀.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-07 00:37:53 +09:00
co-authored by Claude Opus 5
parent e2cc5e759b
commit eec29a90df
3 changed files with 48 additions and 26 deletions
+8 -1
View File
@@ -443,10 +443,17 @@ async def run_redesign_chain(
)
solve_result: Any = await solve_route(project_id, request)
if isinstance(solve_result, JSONResponse):
# 사유까지 남긴다 — 상태 코드만으로는 무엇이 막았는지 못 짚는다(2026-09-06
# 노선 편집 [확인]이 조용히 400 으로 끊겨 종횡단만 옛 노선에 남았음).
try:
reason = bytes(solve_result.body).decode("utf-8", "replace")[:300]
except Exception: # noqa: BLE001 — 로그용이라 실패해도 흐름을 막지 않는다
reason = "(본문 없음)"
logger.error(
"재확정 체인 중단(B05 재계산 실패): project_id=%s status=%s",
"재확정 체인 중단(B05 재계산 실패): project_id=%s status=%s reason=%s",
project_id,
solve_result.status_code,
reason,
)
return
new_route_id = int(solve_result.route_id)
+31 -18
View File
@@ -122,31 +122,44 @@ def _ensure_expected_route(project_root: Path) -> str:
async def _min_plan_radius_m(project_id: UUID) -> float:
"""이 프로젝트에 적용할 법정 최소곡선반지름(m) — 임도 종류·설계속도·지형으로 고른다.
값의 출처는 지식DB(`01_임도/02_상세설계/평면선형.md`, 별표2 Ⅰ.2.다)이고 코드에서는
`config_system_design` 이 그대로 들고 있다. 프로젝트 설정을 못 읽으면 가장 완화된
조건(설계속도 20·특수지형)으로 떨어진다 — 막지 않고 위반 표시만 하기 때문이다.
값의 출처는 지식DB(`01_임도/02_상세설계/평면선형.md`, 별표2 Ⅰ.2.다)이고 산식은 이미
`B05_Profile_Engine_Grade.legal_plan_radius_min_m` 에 있다 — 여기서 다시 짜지 않는다.
읽는 자리 — 임도 종류는 `projects.road_type`, 설계속도·지형은 **워크플로 stage 2 params**
(노선 풀기 요청이 남긴 값)다. `projects` 에는 설계속도·지형 칸이 없다(2026-09-06 확인:
있는 것은 `road_type` 뿐). 못 읽으면 가장 완화된 조건으로 떨어진다 — 막지 않고 위반
표시만 하기 때문이다.
"""
from B05_Profile.B05_Profile_Engine_Grade import resolve_design_speed
from config.config_system_design import FOREST_ROAD_PROFILE_CRITERIA
import aiomysql
from B05_Profile.B05_Profile_Engine_Grade import legal_plan_radius_min_m, resolve_design_speed
from common_util.common_util_workflow_state import get_workflow_state
table = FOREST_ROAD_PROFILE_CRITERIA["min_plan_radius_m"]
grade_class, design_speed, terrain = "work", None, "special"
try:
pool = get_db_pool()
async with pool.acquire() as connection, connection.cursor() as cursor:
await cursor.execute(
"SELECT road_type, design_speed_kph, terrain_type FROM projects WHERE id = %s",
(str(project_id),),
)
row = await cursor.fetchone()
if row:
grade_class = str(row[0] or grade_class)
design_speed = int(row[1]) if row[1] else None
terrain = "normal" if str(row[2] or "").lower() == "normal" else "special"
async with pool.acquire() as connection:
async with connection.cursor() as cursor:
await cursor.execute(
"SELECT road_type FROM projects WHERE id = %s", (str(project_id),)
)
row = await cursor.fetchone()
if row and row[0]:
grade_class = str(row[0])
async with connection.cursor(aiomysql.DictCursor) as cursor:
state = await get_workflow_state(cursor, str(project_id))
stage2 = next(
(s for s in (state or {}).get("stages", []) if int(s.get("stage_no", -1)) == 2), None
)
params = (stage2 or {}).get("params") or {}
speed = params.get("design_speed_kph")
if isinstance(speed, (int, float)):
design_speed = int(speed)
if params.get("terrain_type") in ("normal", "special"):
terrain = str(params["terrain_type"])
except Exception: # noqa: BLE001 — 설정을 못 읽어도 폴리라인화는 이어 간다
logger.exception("최소곡선반지름 설정을 못 읽어 기본값을 씁니다: %s", project_id)
speed = resolve_design_speed(grade_class, design_speed)
return float(table.get(speed, table[20])[terrain])
return legal_plan_radius_min_m(resolve_design_speed(grade_class, design_speed), terrain)
def _write_planned_polyline(path: Path, points: list[tuple[float, float]], radius_m: float) -> dict:
@@ -37,7 +37,7 @@ from B06_Section.B06_Section_Repository import (
from B06_Section.B06_Section_Repository_Bulk import merge_cross_section_designs
from common_util.common_util_node_bundle import run_bundle_json
from common_util.common_util_storage import resolve_stored_project_path
from config.config_db import get_db_pool
from config.config_db import get_db_pool, run_with_connection
from config.config_system import (
EARTHWORK_CONVERSION_FACTORS,
EARTHWORK_HAUL_EQUIPMENT_LIMITS_M,
@@ -99,18 +99,20 @@ async def _recompute(project_id: UUID | str, route_id: int) -> int:
from B06_Section.B06_Section_Router import get_section_detail
project_uuid = UUID(str(project_id))
response = await get_section_detail(project_uuid, route_id)
# 상세 만들기(파일 읽기 위주)와 DB 두 건은 서로 기다릴 이유가 없다 — 같이 보낸다.
# 원격 DB 라 순차로 내면 왕복이 그대로 더해진다(질의 하나 약 12ms, 2026-09-06 실측).
pool = get_db_pool()
response, stored_path, longitudinal_row = await asyncio.gather(
get_section_detail(project_uuid, route_id),
run_with_connection(get_project_storage_relative_path, project_uuid),
run_with_connection(get_longitudinal_section, project_uuid, route_id),
)
payload = getattr(response, "model_dump", None)
if payload is None: # JSONResponse = 실패
logger.warning("서버 재계산: 종횡단 상세를 못 받음 (route_id=%s)", route_id)
return 0
detail = payload(mode="json")
sections = detail.get("cross_sections") or []
pool = get_db_pool()
async with pool.acquire() as connection:
stored_path = await get_project_storage_relative_path(connection, project_uuid)
longitudinal_row = await get_longitudinal_section(connection, project_uuid, route_id)
project_root = Path(resolve_stored_project_path(stored_path))
from B06_Section.B06_Section_Router_Design import stored_standard_cross_section