perf(B05): 측구 방향 역반영을 스레드 한 번 + 쓰기 한 문장으로 묶음

route/confirm 이 측점마다 종단 정본을 다시 열고(13.8ms) 행마다 DB 에 썼음(24.5ms).
건당 38.3ms 라 30건이면 1.15초, 67건이면 2.57초로 측점 수에 선형으로 늘었음
(보조 창 서버 내부 측정).

- 다시 계산할 측점을 먼저 고른 뒤 asyncio.to_thread 를 **한 번만** 돌림. 그 안에서
  종단 정본과 포장 제안표·세월교 하강표를 한 번 읽어 측점마다 돌려 씀.
- read_cross_design_inputs 에 preloaded 인자 추가(종단 경로·내용·포장 제안표).
  경로 검증은 resolve_longitudinal_path 로 떼어 재사용.
- 쓰기는 merge_cross_section_designs 한 문장.

자체검증(공용 브라우저 [저장] 3회) — route/confirm 470~834ms -> 324/351/356ms.
버튼 전체 대기 2,624~3,415ms -> 1,826/2,766/2,979ms.
(오늘 누적: 4,137ms -> 1,826~2,979ms) 시험 400 통과·17 건너뜀.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-06 22:27:41 +09:00
co-authored by Claude Opus 5
parent 0b30f5b035
commit 5967d64b63
2 changed files with 86 additions and 49 deletions
+27 -9
View File
@@ -251,19 +251,37 @@ def default_section_modes(longitudinal: dict[str, Any]) -> dict[float, str]:
return mapping
def read_cross_design_inputs(
project_root: Path, longitudinal_file_path: str, chainage_m: float
) -> tuple[list[dict], float | None, bool, dict[str, Any]]:
"""(지반 샘플, 계획고, 포장 제안, 측점 기록) — 측점 기록은 곡선부 확폭 입력을 담고 있다."""
def resolve_longitudinal_path(project_root: Path, longitudinal_file_path: str) -> Path:
"""종단 정본 파일 경로를 검증해 돌려준다 — 저장소 밖 경로를 막는다."""
root = project_root.resolve()
longitudinal_path = (root / longitudinal_file_path).resolve()
if root not in longitudinal_path.parents:
path = (root / longitudinal_file_path).resolve()
if root not in path.parents:
raise ValueError("종단면 파일 경로가 프로젝트 저장소를 벗어났습니다.")
if not longitudinal_path.is_file():
if not path.is_file():
raise FileNotFoundError("종단면 상세 파일을 찾을 수 없습니다.")
longitudinal = json.loads(longitudinal_path.read_text(encoding="utf-8"))
return path
def read_cross_design_inputs(
project_root: Path,
longitudinal_file_path: str,
chainage_m: float,
preloaded: tuple[Path, dict[str, Any], dict[float, bool]] | None = None,
) -> tuple[list[dict], float | None, bool, dict[str, Any]]:
"""(지반 샘플, 계획고, 포장 제안, 측점 기록) — 측점 기록은 곡선부 확폭 입력을 담고 있다.
`preloaded` 는 (종단 경로, 종단 내용, 포장 제안표)다. 여러 측점을 잇달아 볼 때
종단 정본을 측점마다 다시 읽지 않게 넘긴다 — 그 재읽기가 측점당 13.8ms 였다
(2026-09-06 실측, 측구 방향 역반영 루프).
"""
if preloaded is not None:
longitudinal_path, longitudinal, pavement = preloaded
else:
longitudinal_path = resolve_longitudinal_path(project_root, longitudinal_file_path)
longitudinal = json.loads(longitudinal_path.read_text(encoding="utf-8"))
pavement = pavement_suggestions(longitudinal)
design_elevation = design_elevation_from_longitudinal(longitudinal, chainage_m)
suggested = pavement_suggestions(longitudinal).get(round(chainage_m, 3), False)
suggested = pavement.get(round(chainage_m, 3), False)
cross_dir = longitudinal_path.parent.parent / "cross_sections"
cross_path = (cross_dir / cross_filename(chainage_m)).resolve()
if cross_dir.resolve() not in cross_path.parents or not cross_path.is_file():