"""B04 지표면 분석 진행률 파일 입출력. `B04_PreProcess_Router` 가 700줄을 넘겨 분리한 조각이다(2026-09-04). 라우터 본체와 상태 조회 라우터가 **같은 함수**를 써야 해서 여기에 둔다 — 순환 임포트를 피하려고 양쪽이 이 모듈을 바라본다. 동작·경로·값은 옮기기 전 그대로다. """ import json import logging from pathlib import Path from typing import Any from common_util.common_util_json import atomic_write_json logger = logging.getLogger(__name__) # 분석 진행률 파일: B04 산출 폴더 아래에 원자적으로 기록/조회한다. PROGRESS_FILE_RELATIVE = ("B04_PreProcess", "processed", "progress.json") def _progress_file_path(project_root: Path) -> Path: return project_root.joinpath(*PROGRESS_FILE_RELATIVE) def write_surface_progress(project_root: Path, percent: int, stage: str, message: str) -> None: """WF1 분석 진행률을 progress.json에 원자적으로 기록한다 (실패해도 분석은 계속).""" try: path = _progress_file_path(project_root) path.parent.mkdir(parents=True, exist_ok=True) atomic_write_json( path, {"progress_percent": percent, "current_stage": stage, "message": message}, ) except OSError: logger.warning("WF1 진행률 기록 실패: %s", project_root, exc_info=True) def read_surface_progress(project_root: Path) -> dict[str, Any] | None: """progress.json을 읽어 반환한다. 없거나 손상 시 None.""" path = _progress_file_path(project_root) if not path.is_file(): return None try: data = json.loads(path.read_text(encoding="utf-8")) return data if isinstance(data, dict) else None except (OSError, ValueError): return None