"""입력 자료가 갈릴 때 옛 계산 결과를 지우는 유틸. 새 자료를 올리면 B04~B06 자동 계산이 처음부터 다시 돌아 결과를 덮어쓴다. 그런데 옛 자료로 만든 산출물이 남아 있으면, 아직 다시 만들어지지 않은 뒷단계(B07 이후) 화면이 **옛 결과를 그대로 보여준다** — 사용자는 새 자료 기준인 줄 알고 검토하게 된다. 그래서 자료가 갈리는 순간 계산 결과를 통째로 지운다. 화면 쪽 규칙은 단순해진다: **불러올 자료가 없으면 대시보드로 돌려보낸다**(2026-08-08 사용자 지시). 지우지 않는 것: `B03_FileInput/` — 업로드 원본과 그 메타데이터다. 같은 파일을 다시 올렸는지 가리는 중복 검사가 이걸 본다. """ import logging import shutil from pathlib import Path import aiomysql logger = logging.getLogger(__name__) # 계산 결과가 쌓이는 단계 폴더. B03(원본 입력)은 일부러 뺐다. OUTPUT_STAGE_DIRS = ( "B04_PreProcess", "B05_Profile", "B06_Section", "B07_DesignDetail", "B08_Quantity", "B09_Estimation", ) # 프로젝트에 직접 매달린 산출물 테이블. 지우는 순서는 자식 → 부모. _PROJECT_OUTPUT_TABLES = ( "cross_sections", "longitudinal_sections", "structures", "quantity_items", "outputs", "surface_models", "processed_point_cloud", ) def purge_output_folders(project_root: Path) -> list[str]: """단계별 산출물 폴더를 비운다. 지운 폴더 이름을 돌려준다.""" removed: list[str] = [] for name in OUTPUT_STAGE_DIRS: target = project_root / name if not target.exists(): continue shutil.rmtree(target, ignore_errors=True) target.mkdir(parents=True, exist_ok=True) removed.append(name) return removed async def purge_output_records(connection: aiomysql.Connection, project_id: str) -> dict[str, int]: """산출물 DB 레코드를 지운다. 파일만 지우면 화면은 "자료가 있다"고 착각한다. 호출부가 트랜잭션을 관리한다(여기서 커밋하지 않는다). """ deleted: dict[str, int] = {} async with connection.cursor() as cursor: # 노선에 매달린 자식부터 — 외래키 없이도 고아 행이 남지 않게 한다. await cursor.execute( "DELETE rp FROM route_points rp JOIN routes r ON r.id = rp.route_id " "WHERE r.project_id = %s", (project_id,), ) deleted["route_points"] = cursor.rowcount await cursor.execute( "DELETE rs FROM route_statistics rs JOIN routes r ON r.id = rs.route_id " "WHERE r.project_id = %s", (project_id,), ) deleted["route_statistics"] = cursor.rowcount for table in _PROJECT_OUTPUT_TABLES: await cursor.execute(f"DELETE FROM {table} WHERE project_id = %s", (project_id,)) deleted[table] = cursor.rowcount await cursor.execute("DELETE FROM routes WHERE project_id = %s", (project_id,)) deleted["routes"] = cursor.rowcount return deleted async def purge_project_outputs( connection: aiomysql.Connection, project_id: str, project_root: Path ) -> None: """입력 자료 교체 시 옛 계산 결과(파일 + DB)를 함께 지운다.""" removed = purge_output_folders(project_root) deleted = await purge_output_records(connection, project_id) logger.info( "입력 자료 교체 — 옛 산출물 정리: project_id=%s 폴더=%s 레코드=%s", project_id, ",".join(removed) or "없음", {key: value for key, value in deleted.items() if value}, )