B05 페이지 초안

This commit is contained in:
2026-07-18 17:48:51 +09:00
parent b03d38084e
commit 050a5a2ba3
24 changed files with 1749 additions and 600 deletions
+7 -145
View File
@@ -7,12 +7,10 @@ from typing import Any
from uuid import UUID, uuid4
import aiomysql
from fastapi import APIRouter, File, Form, UploadFile
from fastapi import APIRouter, Depends, File, Form, UploadFile
from fastapi.responses import JSONResponse
from B03_FileInput.B03_FileInput_Email import (
send_analysis_completion_email,
send_analysis_error_email,
send_file_upload_complete_email,
)
from B03_FileInput.B03_FileInput_Engine import (
@@ -44,20 +42,17 @@ from B03_FileInput.B03_FileInput_Schema import (
UploadFinalizeRequest,
UploadStatusResponse,
)
from B03_FileInput.B03_FileInput_Service_WF1 import trigger_wf1_analysis_and_email
from common_util.common_util_auth import verify_session
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 import load_project_workflow
from common_util.common_util_workflow_state import (
complete_stage,
fail_stage,
get_workflow_state,
start_stage,
)
from config.config_db import get_db_pool
from config.config_system import (
SEND_ANALYSIS_COMPLETION_EMAIL,
SURFACE_MODEL_PRECOMPUTE,
SURFACE_MODEL_SOURCE_FILTERS,
UPLOAD_CHUNK_SIZE_BYTES,
UPLOAD_MAX_FILES,
)
@@ -144,147 +139,11 @@ async def _send_upload_complete_notification(
)
async def trigger_wf1_analysis_and_email(
*,
project_id: UUID,
input_file_id: int,
) -> None:
"""WF1 분석 실행, DB 저장, 완료/오류 이메일 발송을 백그라운드에서 수행한다.
실행 흐름:
1. 프로젝트 저장 경로와 입력 파일 정보를 조회한다.
2. 무거운 WF1 분석은 워커 스레드에서 실행한다.
3. 분석 결과를 단일 DB 트랜잭션으로 저장한다.
4. 완료 이메일은 SEND_ANALYSIS_COMPLETION_EMAIL 설정이 켜진 경우에만 발송한다.
5. 분석 또는 DB 저장 실패 시 오류 이메일을 발송한다.
"""
pool = get_db_pool()
project_info: dict[str, Any] | None = None
try:
source_filters = list(SURFACE_MODEL_SOURCE_FILTERS)
methods = list(SURFACE_MODEL_PRECOMPUTE)
params = {
"input_file_id": str(input_file_id),
"source_filters": source_filters,
"methods": methods,
"force": False,
}
async with pool.acquire() as connection:
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_info = await _get_project_notification_info(connection, project_id)
if not project_info or not project_info.get("user_email"):
logger.warning("WF1 분석 이메일 수신자 없음: project_id=%s", project_id)
from B04_wf1_Surface.B04_wf1_Surface_Repository import get_input_file
input_file = await get_input_file(connection, project_id, input_file_id)
project_root = Path(resolve_stored_project_path(stored_path))
las_path = project_root / Path(str(input_file["raw_file_path"]))
if not las_path.is_file():
raise FileNotFoundError("원본 LAS/LAZ 파일을 찾을 수 없습니다.")
logger.info(
"WF1 백그라운드 분석 시작: project_id=%s input_file_id=%s",
project_id,
input_file_id,
)
from B04_wf1_Surface.B04_wf1_Surface_Engine import run_surface_analysis
from B04_wf1_Surface.B04_wf1_Surface_Router import (
save_surface_analysis_to_db,
write_surface_progress,
)
# 업로드 자동 분석도 progress.json에 진행률을 기록한다 (PLAN C-4)
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)
logger.info("WF1 분석 엔진 시작: las_path=%s", las_path)
analysis_result = await asyncio.to_thread(
run_surface_analysis,
project_root,
las_path,
source_filters=source_filters,
methods=methods,
force=False,
on_progress=_on_progress,
)
logger.info("WF1 분석 엔진 완료: 모델 %d개 생성됨", len(analysis_result.get("models", [])))
logger.info("WF1 분석 결과 DB 저장 시작: project_id=%s", project_id)
async with pool.acquire() as connection:
await connection.begin()
try:
await save_surface_analysis_to_db(
connection,
project_id=project_id,
input_file_id=input_file_id,
analysis_result=analysis_result,
source_filters=source_filters,
)
async with connection.cursor() as cursor:
await complete_stage(cursor, str(project_id), 1)
await connection.commit()
logger.info("WF1 분석 결과 DB 저장 완료: project_id=%s", project_id)
except Exception as e:
logger.exception("WF1 분석 결과 DB 저장 실패: %s", e)
await connection.rollback()
raise
write_surface_progress(
project_root,
100,
"awaiting_confirmation",
"WF1 분석이 완료되었습니다. 사용할 모델을 확정하세요.",
)
logger.info(
"SEND_ANALYSIS_COMPLETION_EMAIL=%s, project_info=%s",
SEND_ANALYSIS_COMPLETION_EMAIL,
bool(project_info),
)
if SEND_ANALYSIS_COMPLETION_EMAIL and project_info and project_info.get("user_email"):
logger.info("WF1 완료 이메일 발송 시작: to=%s", project_info["user_email"])
await send_analysis_completion_email(
project_id=project_id,
project_name=str(project_info.get("project_name") or project_id),
to_email=str(project_info["user_email"]),
analysis_result=analysis_result,
)
logger.info("WF1 완료 이메일 발송 완료: project_id=%s", project_id)
else:
logger.info(
"WF1 완료 이메일 발송 스킵: SEND_ANALYSIS_COMPLETION_EMAIL=%s, has_email=%s",
SEND_ANALYSIS_COMPLETION_EMAIL,
project_info and project_info.get("user_email") is not None,
)
logger.info("WF1 백그라운드 분석 완료: project_id=%s", project_id)
except Exception as exc:
logger.exception("WF1 백그라운드 분석 실패: project_id=%s", project_id)
async with pool.acquire() as connection, connection.cursor() as cursor:
await fail_stage(cursor, str(project_id), 1, str(exc))
await connection.commit()
if project_info and project_info.get("user_email"):
await send_analysis_error_email(
project_id=project_id,
project_name=str(project_info.get("project_name") or project_id),
to_email=str(project_info["user_email"]),
error_message=str(exc),
)
@router.post("/{project_id}/files", response_model=FileUploadResponse)
async def upload_project_files(
project_id: UUID,
files: list[UploadFile] = File(...),
session: dict[str, Any] = Depends(verify_session),
) -> FileUploadResponse | JSONResponse:
"""프로젝트 입력 파일을 저장·분석하고 DB 메타데이터를 기록한다."""
if not files or len(files) > UPLOAD_MAX_FILES:
@@ -391,6 +250,7 @@ async def upload_project_files(
trigger_wf1_analysis_and_email(
project_id=project_id,
input_file_id=point_cloud_result.input_file_id,
user_role=str(session["role"]),
),
task_name=f"b04-wf1-auto-{project_id}",
)
@@ -525,6 +385,7 @@ async def upload_project_chunk(
async def finalize_project_upload(
project_id: UUID,
payload: UploadFinalizeRequest,
session: dict[str, Any] = Depends(verify_session),
) -> FileUploadResponse | JSONResponse:
"""청크 업로드를 최종 병합하고 input_files 메타데이터를 기록한다."""
pool = get_db_pool()
@@ -617,6 +478,7 @@ async def finalize_project_upload(
trigger_wf1_analysis_and_email(
project_id=project_id,
input_file_id=result.input_file_id,
user_role=str(session["role"]),
),
task_name=f"b04-wf1-auto-{project_id}",
)