perf(영구저장): 343-2 도엽 3D 면 열쇠 · 지표면 모델 id 유지 · 비용면 메모리 기억 · 추천 노선 열쇠 · 횡단 상세 ETag
- 도엽 3D 면 — 결과 옆 dtm_sheet_{방식}.key.json(등고선 이름:mtime:크기 · 노선 · CRS · 격자 · 여유 · 스무딩 · 화면 상자 · ALGO) 같으면 건너뜀
- 지표면 모델 — (방식 · 필터 · 파일) + 파일 지문 같으면 행 id 살려 고쳐 씀 · 이번 결과에 없는 옛 행만 지움
- 비용면 · 계류 메모리 기억(비용면 서명 · 계류 파일 지문 · 좌표계) · 추천 열쇠 = 서명(배열 통째 해시 없앰)
- 추천 저장본 종류(갈래 · 지형)당 최근 2벌 · 꺼내면 최근으로
- 횡단 상세 GET — 측점 파일 안 지움(valid_cross_names) · 본문 해시 ETag 304
- 시험 test_343_2_persist_keys 13건
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014utrjfoW831Vc5wJUd13kL
This commit is contained in:
@@ -337,19 +337,114 @@ async def update_project_status(
|
||||
)
|
||||
|
||||
|
||||
async def delete_project_surface_models(connection: aiomysql.Connection, project_id: UUID) -> int:
|
||||
"""프로젝트의 기존 지표면 모델 행을 모두 제거한다.
|
||||
def _params_dict(value: Any) -> dict[str, Any]:
|
||||
if isinstance(value, (str, bytes)):
|
||||
try:
|
||||
value = json.loads(value)
|
||||
except ValueError:
|
||||
return {}
|
||||
return value if isinstance(value, dict) else {}
|
||||
|
||||
모든 분석 세대가 동일 파일 경로를 재사용하므로 재분석 시 구세대 행을
|
||||
남기면 존재하지 않는 파일을 가리키게 된다. terrain_layers는 FK CASCADE,
|
||||
routes.surface_model_id는 FK SET NULL로 함께 정리된다.
|
||||
"""
|
||||
|
||||
def _model_key(model_type: Any, params: dict[str, Any], model_file_path: Any) -> tuple:
|
||||
"""같은 모델인지 가리는 열쇠 — (방식, 필터, 파일)."""
|
||||
return (str(model_type), str(params.get("source_filter")), str(model_file_path))
|
||||
|
||||
|
||||
async def _existing_models(
|
||||
connection: aiomysql.Connection, project_id: UUID
|
||||
) -> dict[tuple, tuple[int, Any]]:
|
||||
"""기존 지표면 행 — 열쇠 → (id, 파일 지문)."""
|
||||
async with connection.cursor() as cursor:
|
||||
await cursor.execute(
|
||||
"DELETE FROM surface_models WHERE project_id = %s",
|
||||
"SELECT id, model_type, model_file_path, generation_params "
|
||||
"FROM surface_models WHERE project_id = %s ORDER BY id",
|
||||
(str(project_id),),
|
||||
)
|
||||
return int(cursor.rowcount)
|
||||
rows = await cursor.fetchall()
|
||||
existing: dict[tuple, tuple[int, Any]] = {}
|
||||
for model_id, model_type, model_file_path, raw_params in rows:
|
||||
params = _params_dict(raw_params)
|
||||
existing[_model_key(model_type, params, model_file_path)] = (
|
||||
int(model_id),
|
||||
params.get("file_stamp"),
|
||||
)
|
||||
return existing
|
||||
|
||||
|
||||
async def _upsert_surface_model(
|
||||
connection: aiomysql.Connection,
|
||||
existing: dict[tuple, tuple[int, Any]],
|
||||
*,
|
||||
project_id: UUID,
|
||||
model: dict[str, Any],
|
||||
source_file_id: int | None,
|
||||
processed_cloud_id: int | None,
|
||||
crs_epsg: int | None,
|
||||
) -> int:
|
||||
"""파일 지문이 같은 옛 행이면 id 를 살려 고쳐 씀 — 화면 주소 · 노선 연결이 그대로.
|
||||
|
||||
지문이 다르거나 없으면 옛 행을 지우고 새로 넣는다(새 지표면 = 노선 연결도 끊김).
|
||||
"""
|
||||
params = model["generation_params"] or {}
|
||||
hit = existing.pop(_model_key(model["model_type"], params, model["model_file_path"]), None)
|
||||
stamp = params.get("file_stamp")
|
||||
if hit is not None and stamp and hit[1] == stamp:
|
||||
model_id = hit[0]
|
||||
async with connection.cursor() as cursor:
|
||||
await cursor.execute(
|
||||
"""
|
||||
UPDATE surface_models
|
||||
SET source_file_id = %s, processed_cloud_id = %s, status = 'COMPLETE',
|
||||
crs_epsg = %s, resolution_m = %s, generation_params = %s,
|
||||
completed_at = CURRENT_TIMESTAMP
|
||||
WHERE id = %s
|
||||
""",
|
||||
(
|
||||
source_file_id,
|
||||
processed_cloud_id,
|
||||
crs_epsg,
|
||||
model["resolution_m"],
|
||||
json.dumps(params, ensure_ascii=False),
|
||||
model_id,
|
||||
),
|
||||
)
|
||||
await cursor.execute(
|
||||
"DELETE FROM terrain_layers WHERE surface_model_id = %s", (model_id,)
|
||||
)
|
||||
else:
|
||||
# 같은 파일을 가리키는 옛 행은 걷어 낸다 — 열쇠 모양이 옛것이어도 겹치지 않게.
|
||||
async with connection.cursor() as cursor:
|
||||
if model["model_file_path"]:
|
||||
await cursor.execute(
|
||||
"DELETE FROM surface_models WHERE project_id = %s AND model_file_path = %s",
|
||||
(str(project_id), model["model_file_path"]),
|
||||
)
|
||||
elif hit is not None:
|
||||
await cursor.execute("DELETE FROM surface_models WHERE id = %s", (hit[0],))
|
||||
model_id = await create_surface_model(
|
||||
connection,
|
||||
project_id=project_id,
|
||||
model_type=model["model_type"],
|
||||
source_file_id=source_file_id,
|
||||
processed_cloud_id=processed_cloud_id,
|
||||
crs_epsg=crs_epsg,
|
||||
resolution_m=model["resolution_m"],
|
||||
model_file_path=model["model_file_path"],
|
||||
generation_params=model["generation_params"],
|
||||
)
|
||||
for layer in model.get("layers", []):
|
||||
await create_terrain_layer(
|
||||
connection,
|
||||
surface_model_id=model_id,
|
||||
layer_name=layer["layer_name"],
|
||||
geometry_type=layer["geometry_type"],
|
||||
layer_file_path=layer["file_path"],
|
||||
file_format=layer["file_format"],
|
||||
file_size_mb=None,
|
||||
statistics=None,
|
||||
)
|
||||
return model_id
|
||||
|
||||
|
||||
async def save_surface_analysis_to_db(
|
||||
@@ -362,11 +457,12 @@ async def save_surface_analysis_to_db(
|
||||
) -> list[int]:
|
||||
"""WF1 분석 결과를 DB에 저장한다 (기존 모델 행은 교체).
|
||||
|
||||
파일 지문이 같은 모델은 id 를 살리고, 이번 결과에 없는 옛 행은 지운다.
|
||||
이 함수는 트랜잭션을 시작하거나 종료하지 않는다. 호출자는 같은 커넥션에서
|
||||
begin/commit/rollback을 한 번만 수행해야 한다.
|
||||
"""
|
||||
input_file = await get_input_file(connection, project_id, input_file_id)
|
||||
await delete_project_surface_models(connection, project_id)
|
||||
existing = await _existing_models(connection, project_id)
|
||||
processed = analysis_result["processed"]
|
||||
processed_cloud_id = await create_processed_point_cloud(
|
||||
connection,
|
||||
@@ -384,28 +480,26 @@ async def save_surface_analysis_to_db(
|
||||
)
|
||||
surface_model_ids: list[int] = []
|
||||
for model in analysis_result["models"]:
|
||||
model_id = await create_surface_model(
|
||||
connection,
|
||||
project_id=project_id,
|
||||
model_type=model["model_type"],
|
||||
source_file_id=input_file_id,
|
||||
processed_cloud_id=processed_cloud_id,
|
||||
crs_epsg=input_file["crs_epsg"],
|
||||
resolution_m=model["resolution_m"],
|
||||
model_file_path=model["model_file_path"],
|
||||
generation_params=model["generation_params"],
|
||||
)
|
||||
surface_model_ids.append(model_id)
|
||||
for layer in model["layers"]:
|
||||
await create_terrain_layer(
|
||||
surface_model_ids.append(
|
||||
await _upsert_surface_model(
|
||||
connection,
|
||||
surface_model_id=model_id,
|
||||
layer_name=layer["layer_name"],
|
||||
geometry_type=layer["geometry_type"],
|
||||
layer_file_path=layer["file_path"],
|
||||
file_format=layer["file_format"],
|
||||
file_size_mb=None,
|
||||
statistics=None,
|
||||
existing,
|
||||
project_id=project_id,
|
||||
model=model,
|
||||
source_file_id=input_file_id,
|
||||
processed_cloud_id=processed_cloud_id,
|
||||
crs_epsg=input_file["crs_epsg"],
|
||||
)
|
||||
)
|
||||
# 이번 결과에 없는 옛 행 — terrain_layers 는 CASCADE · routes.surface_model_id 는 SET NULL.
|
||||
stale_ids = [model_id for model_id, _ in existing.values()]
|
||||
if stale_ids:
|
||||
async with connection.cursor() as cursor:
|
||||
await cursor.execute(
|
||||
"DELETE FROM surface_models WHERE id IN ("
|
||||
+ ", ".join(["%s"] * len(stale_ids))
|
||||
+ ")",
|
||||
tuple(stale_ids),
|
||||
)
|
||||
return surface_model_ids
|
||||
|
||||
@@ -433,36 +527,19 @@ async def save_sheet_surface_models(
|
||||
row = await cursor.fetchone()
|
||||
source_file_id, processed_cloud_id, crs_epsg = row if row else (None, None, None)
|
||||
|
||||
# 같은 파일 · 같은 지문이면 id 를 살린다 — 다른 행은 건드리지 않는다.
|
||||
existing = await _existing_models(connection, project_id)
|
||||
model_ids: list[int] = []
|
||||
for model in models:
|
||||
model_path = model.get("model_file_path")
|
||||
if model_path:
|
||||
async with connection.cursor() as cursor:
|
||||
await cursor.execute(
|
||||
"DELETE FROM surface_models WHERE project_id = %s AND model_file_path = %s",
|
||||
(str(project_id), model_path),
|
||||
)
|
||||
model_id = await create_surface_model(
|
||||
connection,
|
||||
project_id=project_id,
|
||||
model_type=model["model_type"],
|
||||
source_file_id=source_file_id,
|
||||
processed_cloud_id=processed_cloud_id,
|
||||
crs_epsg=crs_epsg,
|
||||
resolution_m=model["resolution_m"],
|
||||
model_file_path=model_path,
|
||||
generation_params=model["generation_params"],
|
||||
)
|
||||
model_ids.append(model_id)
|
||||
for layer in model.get("layers", []):
|
||||
await create_terrain_layer(
|
||||
model_ids.append(
|
||||
await _upsert_surface_model(
|
||||
connection,
|
||||
surface_model_id=model_id,
|
||||
layer_name=layer["layer_name"],
|
||||
geometry_type=layer["geometry_type"],
|
||||
layer_file_path=layer["file_path"],
|
||||
file_format=layer["file_format"],
|
||||
file_size_mb=None,
|
||||
statistics=None,
|
||||
existing,
|
||||
project_id=project_id,
|
||||
model=model,
|
||||
source_file_id=source_file_id,
|
||||
processed_cloud_id=processed_cloud_id,
|
||||
crs_epsg=crs_epsg,
|
||||
)
|
||||
)
|
||||
return model_ids
|
||||
|
||||
Reference in New Issue
Block a user