diff --git a/B05_Profile/B05_Profile_Router_Corridor.py b/B05_Profile/B05_Profile_Router_Corridor.py index d0ba5c41..97d46137 100644 --- a/B05_Profile/B05_Profile_Router_Corridor.py +++ b/B05_Profile/B05_Profile_Router_Corridor.py @@ -110,7 +110,10 @@ async def get_corridor( status_code=404, content={"status": "error", "message": "저장된 코리도가 없습니다."}, ) - if hash: + # `isinstance` 로 좁히는 이유 — 이 함수를 파이썬에서 곧바로 부르면 `hash` 에 `None` + # 대신 **`Query(...)` 기본값 객체**가 들어오고 그것이 truthy 라, 열쇠를 안 준 호출이 + # 통째로 `stale` 로 떨어졌다(2026-09-06 보조 창 지적). HTTP 경로만 보면 안 드러난다. + if isinstance(hash, str) and hash: stored = await _stored_hash(path) if stored is not None and stored != hash: return JSONResponse(content={"status": "stale", "hash": stored}) diff --git a/B06_Section/B06_Section_Router_Confirm.py b/B06_Section/B06_Section_Router_Confirm.py index ac43045f..c5f8c20f 100644 --- a/B06_Section/B06_Section_Router_Confirm.py +++ b/B06_Section/B06_Section_Router_Confirm.py @@ -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: diff --git a/B06_Section/B06_Section_Server_Calc_Prebuild.py b/B06_Section/B06_Section_Server_Calc_Prebuild.py index 98af86fd..4fd59c1c 100644 --- a/B06_Section/B06_Section_Server_Calc_Prebuild.py +++ b/B06_Section/B06_Section_Server_Calc_Prebuild.py @@ -6,8 +6,12 @@ ② 그 면적을 쌓아 만드는 유토곡선. 왜 — 사용자가 B06 을 한 번도 안 열어도 **초기값**에는 이 값이 있어야 한다. -여기(Node 실행)는 **초기값 산출 전용**이다 — 사용자가 화면을 만지는 동안과 [저장]·[확정] -때의 계산은 브라우저 몫이다(2026-09-06 사용자 확정: 「일단은 브라우저 계산으로」). + +**부르는 자리 둘** — 파일입력 자동설계 체인(초기값)과 [저장]·[확정](정본). +사용자가 화면을 만지는 **동안**은 브라우저 몫이지만(왕복 없이 즉시 따라와야 한다), +저장 시점은 서버가 다시 낸다(2026-09-06 저녁 사용자 확정). 이유는 속도가 아니라 +**보안**이다 — 저장 경로가 브라우저 계산이면 유토 배분·운반거리 코드가 번들에 남아야 +해서 화면에서 안 그려도 뺄 수 없다. 대가는 저장 대기 850ms → 약 980ms(Node 131ms). **계산을 다시 짜지 않는다.** 화면이 쓰는 TS 를 Node 진입점(`B06_Section_Server_Calc_Node.ts`) 으로 감싸 그대로 돌린다. 파이썬으로 포팅하면 같은 기하가 두 벌이 되어 「그림은 이런데 @@ -82,22 +86,17 @@ def _enforce_stored_designs( enforce_ford_surface_drops(longitudinal, sections, project_root, standard) -async def enforce_stored_designs(project_id: UUID | str, route_id: int) -> int: - """저장분 설계의 포장 구간·세월교 노면 하강만 바로잡는다(Node 실행 없음). - - [저장]·[확정]이 부른다. 구조물 면적·유토곡선은 브라우저가 만들어 보내므로 여기서 - 다시 만들지 않는다(2026-09-06 사용자 확정). 카드를 한 번도 안 그린 측점의 포장· - 세월교 보정만 서버가 챙긴다. - """ - return await _recompute(project_id, route_id, run_node=False) - - async def recompute_server_side(project_id: UUID | str, route_id: int) -> int: - """초기값 산출 — 위 보정에 더해 구조물 면적·유토곡선까지 Node 로 만들어 저장한다.""" - return await _recompute(project_id, route_id, run_node=True) + """포장 구간·세월교 노면 하강 보정에 더해 구조물 면적·유토곡선까지 Node 로 만들어 저장한다. + + 부르는 자리 둘 — 파일입력 자동설계 체인(초기값)과 [저장]·[확정](정본). 저장 때도 + 서버가 내는 것으로 2026-09-06 저녁 되돌렸다(그 사이 잠깐 브라우저 계산이었다): + 이유는 속도가 아니라 **배분·운반거리 코드를 브라우저 번들에서 빼기 위해서**다. + """ + return await _recompute(project_id, route_id) -async def _recompute(project_id: UUID | str, route_id: int, *, run_node: bool) -> int: +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)) @@ -128,15 +127,11 @@ async def _recompute(project_id: UUID | str, route_id: int, *, run_node: bool) - if json.dumps(item.get("design"), sort_keys=True, default=str) != before[index] ] - output = ( - await asyncio.to_thread( - run_bundle_json, - BUNDLE, - _NPM_SCRIPT, - {"detail": detail, "context": _mass_haul_context()}, - ) - if run_node - else {} + output = await asyncio.to_thread( + run_bundle_json, + BUNDLE, + _NPM_SCRIPT, + {"detail": detail, "context": _mass_haul_context()}, ) if not isinstance(output, dict): output = {}