feat(B06): [저장]·[확정] 정본을 서버가 다시 계산하도록 되돌림

2026-09-06 저녁 사용자 확정. 이유는 속도가 아니라 보안 — 저장 경로가 브라우저 계산이면
유토 배분·운반거리 코드(_balance 523 + _settle 235 = 758줄)가 번들에 남아야 해서
화면에서 안 그려도 뺄 수 없음. 서버가 정본을 내면 그 몫이 번들에서 빠짐.
대가는 저장 대기 850ms -> 약 980ms(Node 131ms) — 기다리는 조작이라 허용.

- 저장·확정이 편집을 커밋한 **뒤** recompute_server_side 를 부름(전에는 편집 앞에서
  Node 없이 보정만). 바뀐 벽·측점이 면적·유토곡선에 실림.
- run_node 갈래 제거 — 이제 부르는 자리가 둘 다 Node 를 돎(체인·저장).
  쓰이지 않게 된 enforce_stored_designs 래퍼 삭제.
- 코리도 열쇠 질의: hash 를 isinstance 로 좁힘. 파이썬에서 곧바로 부르면 Query 기본값
  객체가 들어오는데 truthy 라 열쇠 없는 호출이 통째로 stale 로 떨어졌음(보조 창 지적).

시험 395 통과·17 건너뜀.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-06 21:20:55 +09:00
co-authored by Claude Opus 5
parent da2130c988
commit 3cd717d8f6
3 changed files with 43 additions and 34 deletions
+20 -9
View File
@@ -168,19 +168,27 @@ async def _apply_section_edits(
)
async def _enforce_stored_designs(project_id: UUID, route_id: int) -> None:
"""저장분의 포장 구간·세월교 노면 하강을 바로잡는다 — 편집분을 얹기 **전에** 돈다.
async def _recompute_stored_designs(project_id: UUID, route_id: int) -> None:
"""정본을 **서버가 다시 계산**한다 — 사용자 편집이 들어간 **뒤**에 돈다.
구조물 면적·유토곡선은 브라우저가 만들어 보낸다(2026-09-06 사용자 확정). 여기서는
카드를 한 번도 안 그린 측점의 보정만 서버가 챙긴다. 실패는 비치명적이다.
포장 구간·세월교 노면 하강 보정에 더해 구조물 면적·유토곡선(배분·운반거리 포함)까지
Node 진입점으로 새로 낸다. 브라우저가 보낸 값을 그대로 받아 적지 않는다.
**왜 서버인가(2026-09-06 저녁 사용자 확정)** — 속도가 아니라 보안이다. 저장 경로가
브라우저 계산이면 유토 배분·운반거리 코드가 번들에 남아야 해서 화면에서 안 그려도
뺄 수 없다. 서버가 정본을 내면 그 몫이 번들에서 빠진다. 대가는 저장 대기가
850ms → 약 980ms 인데(Node 131ms) 기다리는 조작이라 허용한다.
사용자가 끌어 옮긴 balloon 위치는 서버가 만들지 않으므로 저장분에서 떼어 도로 붙인다
(`B06_Section_Server_Calc_Prebuild`). 실패는 비치명적이다 — 저장은 그대로 남는다.
"""
try:
from B06_Section.B06_Section_Server_Calc_Prebuild import enforce_stored_designs
from B06_Section.B06_Section_Server_Calc_Prebuild import recompute_server_side
await enforce_stored_designs(project_id, route_id)
await recompute_server_side(project_id, route_id)
except Exception:
logger.exception(
"저장분 설계 보정 실패 (저장은 유지): project_id=%s route_id=%s",
"저장분 서버 재계산 실패 (저장은 유지): project_id=%s route_id=%s",
project_id,
route_id,
)
@@ -230,7 +238,6 @@ async def save_sections(
request.standard_cross_section if request else None,
)
await _enforce_stored_designs(project_id, route_id)
async with pool.acquire() as connection:
await connection.begin()
try:
@@ -241,6 +248,8 @@ async def save_sections(
except Exception:
await connection.rollback()
raise
# 편집이 들어간 **뒤** 서버가 정본을 다시 낸다 — 바뀐 벽·측점이 면적·유토곡선에 실린다.
await _recompute_stored_designs(project_id, route_id)
return SectionConfirmResponse(
project_id=str(project_id), route_id=route_id, confirmed=False
)
@@ -304,7 +313,6 @@ async def confirm_sections(
request.standard_cross_section if request else None,
)
await _enforce_stored_designs(project_id, route_id)
async with pool.acquire() as connection:
await connection.begin()
try:
@@ -326,6 +334,9 @@ async def confirm_sections(
await connection.rollback()
raise
# 편집이 들어간 **뒤** 서버가 정본을 다시 낸다(임시저장과 같은 자리).
await _recompute_stored_designs(project_id, route_id)
# 측구 방향(design.ditch_side) 변경을 B05 종단 정본 stations.uphill_side에 역반영한다(E-7).
# 파일 기반·비치명적: 실패해도 확정은 유지한다.
try: