Files
Aislo/B06_Section/B06_Section_Repository_Bulk.py
T
eomsangdonandClaude Opus 5.5 857b2d89d4 fix(445 B06 저장 patch): 바탕 = 캐시 설계 사용자 칸 + 세션 얹음 · 되돌림 null · 옮긴 측점 입력 · 집수정 범위 · 버린 수
- H3 — 횡단 patch 바탕을 캐시 설계 사용자 칸(측점마다 통째)으로 · 세션 고침은 벽 · 단 · 쪽 열쇠마다 합침(mergeCrossPatches)
- M2 — 벽 ↺ · 연동 풂 = 세션 null(안쪽 열쇠 지움) · 추가 벽 0 · 빈 전체 폭 0 을 세션에 남김 · 새 탭 − 가 저장분 단 수에서 출발
- H2 — 옮긴 관 · 구조물(정본 행 없음) = 지반 등 입력(CARRY_INPUT_KEYS)까지 실음 · 화면도 들고 감 · 서버 기본 설계가 그 입력으로 섬
- H5 — 노선 바꾼 [저장] 본문에 카드 선택 · 암선 · 암 경사 · 표준 횡단면 · 풍선 자리
- M1 — 집수정 내공 화면 범위 = 서버 1.0~2.0
- M5 — /save · /confirm 응답에 버린 patch 측점 · 기본 설계 실패 · 측점 맞춤 실패 · 화면 알림
- M7 — kindreset 을 측점 옮김 · 노선 저장에 따라 옮김
- 시험 test_445_save_patch · 기존 시험 꼴 맞춤(318 · 384 · 428 · 429 · fills_empty)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013QyNXY2TUKXCFMwAca2APG
2026-10-18 18:55:25 +09:00

162 lines
7.0 KiB
Python

"""측점 설계를 **여러 행 한 번에** 쓰는 자리.
왜 (2026-09-06 실측) — [저장]이 측점마다 `update_cross_section_design` ·
`merge_cross_section_design_patch` 를 불렀고, 그 하나가 `SELECT` + `UPDATE` 두 왕복이다.
DB 가 원격(`dsm.chemifactory.com`)이라 왕복 하나가 **약 12ms** 다. 22행이면 왕복 44번,
곧 **670ms**. 측점이 많은 프로젝트일수록 선형으로 늘어난다.
여기서는 세 문장으로 끝낸다 —
① 노선의 측점 행을 **한 번에** 읽고
② 파이썬에서 chainage 를 맞춰 JSON 을 합치고
③ `UPDATE … SET data = CASE id …` 한 문장으로 되돌려 쓴다(없는 행은 다중 INSERT).
`B06_Section_Repository` 가 685줄이라 700줄 한계에 걸려 파일을 나눴다. 한 행짜리 함수는
그쪽에 그대로 두고, 여러 행을 쓸 때만 이쪽을 쓴다.
"""
from __future__ import annotations
import json
from typing import Any
from uuid import UUID
import aiomysql
# 측점을 같은 자리로 볼 허용 오차(m) — 한 행짜리 함수와 같은 값을 쓴다.
_CHAINAGE_TOLERANCE_M = 0.01
async def merge_cross_section_designs(
connection: aiomysql.Connection,
*,
route_id: int,
entries: list[tuple[float, dict[str, Any]]],
replace: bool,
project_id: UUID | None = None,
missed: list[float] | None = None,
) -> int:
"""측점 여러 곳의 `data.design` 을 한 번에 쓴다. 실제로 바뀐 행 수를 돌려준다.
`replace=True` 면 design 을 통째로 갈아 끼우고(`update_cross_section_design` 과 같은 뜻),
`False` 면 키만 얹는다(`merge_cross_section_design_patch` 와 같은 뜻 · 값 None = 그 키 지움).
행이 없는 측점은 `project_id` 가 오면 새로 만든다 — 구조물(비정규) 측점은 B05 확정이
파일만 쓰고 DB 행을 안 만들기 때문이다(한 행짜리 함수와 같은 규칙).
"""
return await merge_cross_section_design_batches(
connection, route_id=route_id, batches=[(entries, replace, project_id)], missed=missed
)
async def merge_cross_section_design_batches(
connection: aiomysql.Connection,
*,
route_id: int,
batches: list[tuple[list[tuple[float, dict[str, Any]]], bool, UUID | None]],
missed: list[float] | None = None,
) -> int:
"""여러 벌(`(entries, replace, project_id)`)을 차례로 얹되 읽기 한 번 · 쓰기 한 번(342-2).
앞 벌이 만든 새 행도 뒤 벌이 찾는다(따로 부르던 때와 같은 결과). 값이 그대로인 행은 쓰지
않는다 — 같은 입력의 [저장]·[확정]이 행을 다시 쓰지 않게. `missed` 를 주면 행이 없어 못 얹은
측점(값이 있는 줄만)을 담음 — 버린 수 알림(445 M5).
"""
if not any(entries for entries, _, _ in batches):
return 0
async with connection.cursor() as cursor:
await cursor.execute(
"SELECT id, chainage_m, data FROM cross_sections WHERE route_id = %s ORDER BY id",
(route_id,),
)
rows = await cursor.fetchall()
existing: dict[int, dict[str, Any]] = {}
for row_id, _chainage, raw in rows:
data = json.loads(raw) if isinstance(raw, str) else raw
existing[int(row_id)] = data if isinstance(data, dict) else {}
original = dict(existing) # 행 dict 는 제자리에서 안 고침(아래가 늘 사본을 만듦) — 대조 기준
# 같은 측점이 여러 행이면 뒤에 온 것(=큰 id)을 쓴다 — 한 행짜리 함수의 `ORDER BY id DESC`.
ordered = [(float(chainage), int(row_id)) for row_id, chainage, _ in rows]
# 새 행 = 음수 id · 뒤 벌이 찾음(있는 행이 먼저 · 새 행끼리는 먼저 만든 것) · 끝에 INSERT
fresh: dict[int, tuple[str, float]] = {}
def find(chainage_m: float) -> int | None:
best: int | None = None
for value, row_id in ordered:
if abs(value - chainage_m) < _CHAINAGE_TOLERANCE_M and (best is None or row_id > best):
best = row_id
return best
# 행마다 한 벌 — 같은 행에 여러 줄이 오면 앞 줄 결과 위에 얹음(`CASE id` 는 첫 WHEN 만 씀 ·
# 318-4 옛 조정값 비움 뒤 새 값)
for entries, replace, project_id in batches:
for chainage_m, payload in entries:
if not payload and not replace:
continue
# 얹기의 None = 그 열쇠 지움(JSON merge-patch · 318-4 관 종류 바꿈 옛 조정값)
drop = [] if replace else [key for key, value in payload.items() if value is None]
if drop:
payload = {key: value for key, value in payload.items() if value is not None}
row_id = find(chainage_m)
if row_id is None:
if project_id is None or not (payload or replace):
if missed is not None and payload:
missed.append(chainage_m)
continue
row_id = -(len(fresh) + 1)
fresh[row_id] = (str(project_id), chainage_m)
ordered.append((chainage_m, row_id))
existing[row_id] = {"design": payload}
continue
data = dict(existing[row_id])
if replace:
data["design"] = payload
else:
design = data.get("design")
design = dict(design) if isinstance(design, dict) else {}
design.update(payload)
for key in drop:
design.pop(key, None)
data["design"] = design
existing[row_id] = data
updates = {
row_id: json.dumps(data, ensure_ascii=False)
for row_id, data in existing.items()
if row_id > 0 and data != original[row_id]
}
inserts = [
(owner, route_id, chainage_m, json.dumps(existing[row_id], ensure_ascii=False))
for row_id, (owner, chainage_m) in fresh.items()
]
written = 0
async with connection.cursor() as cursor:
if updates:
# 한 문장 — `CASE id WHEN … THEN …` 이라 왕복이 한 번이다.
cases = " ".join("WHEN %s THEN %s" for _ in updates)
params: list[Any] = []
for row_id, blob in updates.items():
params.extend((row_id, blob))
params.extend(updates)
placeholders = ", ".join("%s" for _ in updates)
await cursor.execute(
f"UPDATE cross_sections SET data = CASE id {cases} END "
f"WHERE id IN ({placeholders})",
params,
)
written += len(updates)
if inserts:
values = ", ".join("(%s, %s, %s, %s, 'DRAFT')" for _ in inserts)
flat: list[Any] = []
for item in inserts:
flat.extend(item)
await cursor.execute(
"INSERT INTO cross_sections (project_id, route_id, chainage_m, data, status) "
f"VALUES {values}",
flat,
)
written += len(inserts)
return written