Files
Aislo/common_util/common_util_project_reset.py
T
eomsangdonandClaude Opus 5.5 a44fd98adf feat(765-3): 전처리 체인 = 지형마다 두 벌 — 다른 지형 먼저 짓고 묶음으로 떠 둠 → 설계 지형 → 추천 미리 셈 한 번 · 배수 늘 다시 셈 · 비우기에 묶음 · 죽은 run_redesign_chain 걷음
- run_preprocess_chain: WF1 자동 확정 · 서피스 바꿈 체인이 부름 · 다른 지형 없음 · 실패 = 한 벌 + meta.json 까닭 · 예상노선(공유)은 설계 지형 벌이 씀
- 자동 체인 배수 refresh=True — 저장분이 노선 바뀐 줄 모르던 틈 막음
- purge_output_folders 가 design_sets 도 지움
- run_redesign_chain(앱에서 부르는 곳 0) 걷음 — 시험 정리: test_342_2 의 그 함수 시험 셋 지움(함수 없음) · test_763_3 · test_351_1 은 두 벌 체인으로 · test_74 · test_user_values_survive 의 이월 차례 검사는 살아 있는 자리(B05_Profile_Route_Save)로 옮김
- 시험 test_765_design_sets 에 체인 차례 · 한 벌 · 다른 벌 실패 · 비우기 넷 덧

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 15:47:20 +09:00

123 lines
4.6 KiB
Python

"""입력 자료가 갈릴 때 옛 계산 결과를 지우는 유틸.
새 자료를 올리면 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",
)
# 같은 지형 파일을 다시 올렸을 때 살리는 것 — 전처리 폴더와 그 DB 행.
PREPROCESS_STAGE_DIR = "B04_PreProcess"
_PREPROCESS_TABLES = frozenset({"surface_models", "processed_point_cloud"})
#: 지형마다 설계 한 벌의 쉬는 벌 묶음(765-3) — 산출물이라 늘 함께 지움
DESIGN_SETS_DIR = "design_sets"
def purge_output_folders(project_root: Path, keep_preprocess: bool = False) -> list[str]:
"""단계별 산출물 폴더 · 설계 벌 묶음을 비운다. 지운 폴더 이름을 돌려준다."""
removed: list[str] = []
sets = project_root / DESIGN_SETS_DIR
if sets.exists():
shutil.rmtree(sets, ignore_errors=True)
removed.append(DESIGN_SETS_DIR)
for name in OUTPUT_STAGE_DIRS:
if keep_preprocess and name == PREPROCESS_STAGE_DIR:
continue
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, keep_preprocess: bool = False
) -> 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:
if keep_preprocess and table in _PREPROCESS_TABLES:
continue
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,
keep_preprocess: bool = False,
) -> None:
"""입력 자료 교체 시 옛 계산 결과(파일 + DB)를 함께 지운다.
`keep_preprocess` — 지형 파일이 그대로면 전처리(B04) 결과는 살린다.
"""
removed = purge_output_folders(project_root, keep_preprocess)
deleted = await purge_output_records(connection, project_id, keep_preprocess)
logger.info(
"입력 자료 교체 — 옛 산출물 정리: project_id=%s 전처리 살림=%s 폴더=%s 레코드=%s",
project_id,
keep_preprocess,
",".join(removed) or "없음",
{key: value for key, value in deleted.items() if value},
)