260710_1
This commit is contained in:
@@ -32,12 +32,45 @@ from B04_wf1_Surface.B04_wf1_Surface_Schema import (
|
||||
SurfaceModelSummary,
|
||||
SurfacePointCloudSampleResponse,
|
||||
)
|
||||
from common_util.common_util_json import atomic_write_json
|
||||
from common_util.common_util_storage import resolve_stored_project_path
|
||||
from common_util.common_util_workflow_state import complete_stage, fail_stage, start_stage
|
||||
from config.config_db import get_db_pool
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter(prefix="/api/projects", tags=["B04 Surface Analysis"])
|
||||
POINT_CLOUD_SAMPLE_LIMIT = 100_000
|
||||
# 분석 진행률 파일: B04 산출 폴더 아래에 원자적으로 기록/조회한다.
|
||||
PROGRESS_FILE_RELATIVE = ("B04_wf1_Surface", "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
|
||||
|
||||
|
||||
async def update_project_status(
|
||||
@@ -124,9 +157,17 @@ async def analyze_surface(
|
||||
|
||||
pool = get_db_pool()
|
||||
try:
|
||||
params = {
|
||||
"input_file_id": request.input_file_id,
|
||||
"source_filters": source_filters,
|
||||
"methods": methods,
|
||||
"force": request.force,
|
||||
}
|
||||
async with pool.acquire() as connection:
|
||||
await update_project_status(connection, project_id, "WF1_ANALYZING")
|
||||
await connection.commit()
|
||||
async with connection.cursor() as cursor:
|
||||
await start_stage(cursor, str(project_id), 1, params)
|
||||
await connection.commit()
|
||||
|
||||
stored_path = await get_project_storage_relative_path(connection, project_id)
|
||||
project_root = Path(resolve_stored_project_path(stored_path))
|
||||
input_file = await get_input_file(connection, project_id, request.input_file_id)
|
||||
@@ -137,6 +178,12 @@ async def analyze_surface(
|
||||
content={"status": "error", "message": "원본 LAS 파일을 찾을 수 없습니다."},
|
||||
)
|
||||
|
||||
# 분석 시작 진행률 기록 (별도 스레드의 콜백은 파일에만 원자적 기록).
|
||||
write_surface_progress(project_root, 5, "analyzing", "WF1 분석을 시작합니다.")
|
||||
|
||||
def _on_progress(percent: int, stage: str, message: str) -> None:
|
||||
write_surface_progress(project_root, percent, stage, message)
|
||||
|
||||
# 무거운 지형 연산은 이벤트 루프를 막지 않도록 별도 스레드에서 실행.
|
||||
result = await asyncio.to_thread(
|
||||
run_surface_analysis,
|
||||
@@ -145,6 +192,7 @@ async def analyze_surface(
|
||||
source_filters=source_filters,
|
||||
methods=methods,
|
||||
force=request.force,
|
||||
on_progress=_on_progress,
|
||||
)
|
||||
|
||||
# DB 기록 (트랜잭션)
|
||||
@@ -157,12 +205,15 @@ async def analyze_surface(
|
||||
analysis_result=result,
|
||||
source_filters=source_filters,
|
||||
)
|
||||
await update_project_status(connection, project_id, "WF1_COMPLETE")
|
||||
async with connection.cursor() as cursor:
|
||||
await complete_stage(cursor, str(project_id), 1)
|
||||
await connection.commit()
|
||||
except Exception:
|
||||
await connection.rollback()
|
||||
raise
|
||||
|
||||
write_surface_progress(project_root, 100, "completed", "WF1 분석이 완료되었습니다.")
|
||||
|
||||
return SurfaceAnalyzeResponse(
|
||||
project_id=str(project_id),
|
||||
ground_summary=result["ground_summary"],
|
||||
@@ -172,14 +223,14 @@ async def analyze_surface(
|
||||
except LookupError as exc:
|
||||
return JSONResponse(status_code=404, content={"status": "error", "message": str(exc)})
|
||||
except (OSError, ValueError) as exc:
|
||||
async with pool.acquire() as connection:
|
||||
await update_project_status(connection, project_id, "WF1_FAILED")
|
||||
async with pool.acquire() as connection, connection.cursor() as cursor:
|
||||
await fail_stage(cursor, str(project_id), 1, str(exc))
|
||||
await connection.commit()
|
||||
return JSONResponse(status_code=400, content={"status": "error", "message": str(exc)})
|
||||
except Exception:
|
||||
except Exception as exc:
|
||||
logger.exception("B04 지표면 분석 실패: project_id=%s", project_id)
|
||||
async with pool.acquire() as connection:
|
||||
await update_project_status(connection, project_id, "WF1_FAILED")
|
||||
async with pool.acquire() as connection, connection.cursor() as cursor:
|
||||
await fail_stage(cursor, str(project_id), 1, str(exc))
|
||||
await connection.commit()
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
@@ -317,43 +368,93 @@ async def get_wf1_analysis_status(project_id: UUID) -> dict:
|
||||
async with connection.cursor(aiomysql.DictCursor) as cursor:
|
||||
await cursor.execute(
|
||||
"""
|
||||
SELECT p.status as project_status, COUNT(sm.id) as model_count
|
||||
FROM projects p
|
||||
LEFT JOIN surface_models sm ON sm.project_id = p.id
|
||||
WHERE p.id = %s AND p.deleted_at IS NULL
|
||||
GROUP BY p.id, p.status
|
||||
SELECT state, progress_percent, message,
|
||||
(SELECT COUNT(*) FROM surface_models
|
||||
WHERE project_id = %s) as model_count
|
||||
FROM project_workflow_stages
|
||||
WHERE project_id = %s AND stage_no = 1
|
||||
""",
|
||||
(str(project_id),),
|
||||
(str(project_id), str(project_id)),
|
||||
)
|
||||
row = await cursor.fetchone()
|
||||
if not row:
|
||||
return JSONResponse(
|
||||
status_code=404,
|
||||
content={"status": "error", "message": "프로젝트를 찾을 수 없습니다."},
|
||||
)
|
||||
model_count = int(row["model_count"]) if row else 0
|
||||
|
||||
project_status = str(row.get("project_status") or "NEW")
|
||||
if project_status == "WF1_FAILED":
|
||||
# 만약 새 테이블에 정보가 없다면 기존 projects 테이블에서 조회 (백필 미작동 대비)
|
||||
if not row:
|
||||
await cursor.execute(
|
||||
"""
|
||||
SELECT p.status as project_status, COUNT(sm.id) as model_count
|
||||
FROM projects p
|
||||
LEFT JOIN surface_models sm ON sm.project_id = p.id
|
||||
WHERE p.id = %s AND p.deleted_at IS NULL
|
||||
GROUP BY p.id, p.status
|
||||
""",
|
||||
(str(project_id),),
|
||||
)
|
||||
fallback_row = await cursor.fetchone()
|
||||
if not fallback_row:
|
||||
return JSONResponse(
|
||||
status_code=404,
|
||||
content={"status": "error", "message": "프로젝트를 찾을 수 없습니다."},
|
||||
)
|
||||
model_count = int(fallback_row["model_count"])
|
||||
project_status = str(fallback_row.get("project_status") or "NEW")
|
||||
|
||||
if project_status == "WF1_FAILED":
|
||||
state = "FAILED"
|
||||
progress_percent = 0
|
||||
message = "WF1 분석에 실패했습니다."
|
||||
elif model_count > 0 or project_status == "WF1_COMPLETE":
|
||||
state = "COMPLETE"
|
||||
progress_percent = 100
|
||||
message = "WF1 분석이 완료되었습니다."
|
||||
elif project_status == "WF1_ANALYZING":
|
||||
state = "IN_PROGRESS"
|
||||
progress_percent = 30
|
||||
message = "WF1 분석이 진행 중입니다."
|
||||
else:
|
||||
state = "NOT_STARTED"
|
||||
progress_percent = 0
|
||||
message = "WF1 분석 대기 중입니다."
|
||||
else:
|
||||
state = row["state"]
|
||||
progress_percent = row["progress_percent"]
|
||||
message = row["message"] or ""
|
||||
model_count = int(row["model_count"])
|
||||
|
||||
if state == "FAILED":
|
||||
status = "failed"
|
||||
progress_percent = 0
|
||||
current_stage = "failed"
|
||||
message = "WF1 분석에 실패했습니다."
|
||||
elif model_count > 0 or project_status == "WF1_COMPLETE":
|
||||
if not message:
|
||||
message = "WF1 분석에 실패했습니다."
|
||||
elif state == "COMPLETE":
|
||||
status = "completed"
|
||||
progress_percent = 100
|
||||
current_stage = "completed"
|
||||
message = "WF1 분석이 완료되었습니다."
|
||||
elif project_status == "WF1_ANALYZING":
|
||||
if not message:
|
||||
message = "WF1 분석이 완료되었습니다."
|
||||
elif state == "IN_PROGRESS":
|
||||
status = "in_progress"
|
||||
progress_percent = 30
|
||||
current_stage = "surface_analysis"
|
||||
message = "WF1 분석이 진행 중입니다."
|
||||
if not message:
|
||||
message = "WF1 분석이 진행 중입니다."
|
||||
# 진행률 파일이 있으면 실제 단계별 진행률로 대체
|
||||
try:
|
||||
async with pool.acquire() as connection:
|
||||
stored_path = await get_project_storage_relative_path(connection, project_id)
|
||||
progress = read_surface_progress(Path(resolve_stored_project_path(stored_path)))
|
||||
if progress:
|
||||
progress_percent = int(progress.get("progress_percent", progress_percent))
|
||||
current_stage = str(progress.get("current_stage", current_stage))
|
||||
message = str(progress.get("message", message))
|
||||
except LookupError:
|
||||
pass
|
||||
else:
|
||||
status = "pending"
|
||||
progress_percent = 0
|
||||
current_stage = "pending"
|
||||
message = "WF1 분석 대기 중입니다."
|
||||
if not message:
|
||||
message = "WF1 분석 대기 중입니다."
|
||||
|
||||
return {
|
||||
"project_id": str(project_id),
|
||||
"status": status,
|
||||
|
||||
Reference in New Issue
Block a user