refactor(B04): 지표면 라우터 700줄 분리 (경로·응답 불변)
B04_PreProcess_Router.py 718줄 -> 493줄. - B04_PreProcess_Router_Status.py(221줄) — 분석 상태 조회·모델 프리뷰 엔드포인트. prefix 없이 만들고 본체가 include_router 로 실어 붙임(본체 prefix 를 그대로 상속). - B04_PreProcess_Router_Progress.py(47줄) — progress.json 읽기·쓰기 헬퍼. 본체와 상태 라우터가 함께 써야 해 순환 임포트를 피하려고 별도 모듈로 둠. 본체는 write_surface_progress 를 계속 재노출(B03_FileInput_Service_WF1 이 참조). 검증: ruff check/format 통과. 라우터 경로 9개가 분리 전과 동일(중복 prefix 없음 확인). 백엔드 재시작(8001) 후 openapi 에 surface 경로 12개 그대로, 브라우저에서 /surface/status 200(진행률 100·모델 3), /surface/models 200, /surface/models/415/preview 200 model/gltf-binary 7,946,756바이트. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,10 +7,9 @@ from pathlib import Path
|
||||
from typing import Any
|
||||
from uuid import UUID
|
||||
|
||||
import aiomysql
|
||||
import numpy as np
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
from fastapi.responses import JSONResponse, Response
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from B03_FileInput.B03_FileInput_Repository import get_project_storage_relative_path
|
||||
from B04_PreProcess.B04_PreProcess_Engine import (
|
||||
@@ -30,6 +29,10 @@ from B04_PreProcess.B04_PreProcess_Repository import (
|
||||
list_surface_models,
|
||||
save_surface_analysis_to_db,
|
||||
)
|
||||
from B04_PreProcess.B04_PreProcess_Router_Progress import (
|
||||
write_surface_progress,
|
||||
)
|
||||
from B04_PreProcess.B04_PreProcess_Router_Status import router as status_router
|
||||
from B04_PreProcess.B04_PreProcess_Schema import (
|
||||
SurfaceAnalyzeRequest,
|
||||
SurfaceAnalyzeResponse,
|
||||
@@ -45,9 +48,6 @@ from B04_PreProcess.B04_PreProcess_Schema import (
|
||||
)
|
||||
from B04_PreProcess.B04_PreProcess_Service import confirm_surface_selection
|
||||
from common_util.common_util_auth import require_system_admin
|
||||
from common_util.common_util_http_cache import cached_file_response
|
||||
from common_util.common_util_initial_snapshot import is_designing
|
||||
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_surface_confirmation import (
|
||||
get_surface_confirmation_params,
|
||||
@@ -63,37 +63,10 @@ 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 = 500_000
|
||||
# 분석 진행률 파일: 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
|
||||
# 상태 조회·모델 프리뷰 엔드포인트는 700줄 제한으로 `_Router_Status` 로 떼어내
|
||||
# 여기서 그대로 실어 붙인다(경로·응답 불변, 2026-09-04).
|
||||
router.include_router(status_router)
|
||||
|
||||
|
||||
@router.post("/{project_id}/surface/analyze", response_model=SurfaceAnalyzeResponse)
|
||||
@@ -518,201 +491,3 @@ async def get_surface_ground_stats(project_id: UUID) -> SurfaceGroundStatsRespon
|
||||
status_code=500,
|
||||
content={"status": "error", "message": "지면 통계 조회 중 오류가 발생했습니다."},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{project_id}/surface/status")
|
||||
async def get_wf1_analysis_status(project_id: UUID) -> dict:
|
||||
"""WF1 분석 상태를 조회한다."""
|
||||
pool = get_db_pool()
|
||||
try:
|
||||
async with pool.acquire() as connection:
|
||||
async with connection.cursor(aiomysql.DictCursor) as cursor:
|
||||
await cursor.execute(
|
||||
"""
|
||||
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)),
|
||||
)
|
||||
row = await cursor.fetchone()
|
||||
|
||||
# 만약 새 테이블에 정보가 없다면 기존 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"
|
||||
current_stage = "failed"
|
||||
if not message:
|
||||
message = "WF1 분석에 실패했습니다."
|
||||
elif state == "COMPLETE":
|
||||
status = "completed"
|
||||
progress_percent = 100
|
||||
current_stage = "completed"
|
||||
if not message:
|
||||
message = "WF1 분석이 완료되었습니다."
|
||||
elif state == "IN_PROGRESS":
|
||||
status = "in_progress"
|
||||
current_stage = "surface_analysis"
|
||||
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"
|
||||
if not message:
|
||||
message = "WF1 분석 대기 중입니다."
|
||||
|
||||
# 초기 설계 체인이 도는 동안은 아직 들어갈 때가 아니다 — WF1(stage 1)이 COMPLETE라도
|
||||
# 마커가 있으면 진행 중으로 돌려준다(2026-08-29 사용자 확정, CLAUDE.md 5장).
|
||||
# 여기서 덮어쓰는 이유: 위 분기는 stage 1만 보므로 체인 구간을 "완료"로 답한다.
|
||||
try:
|
||||
async with pool.acquire() as connection:
|
||||
stored_path = await get_project_storage_relative_path(connection, project_id)
|
||||
if stored_path and is_designing(Path(resolve_stored_project_path(stored_path))):
|
||||
status = "in_progress"
|
||||
current_stage = "initial_design"
|
||||
message = "초기 설계를 계산하는 중입니다."
|
||||
except (LookupError, OSError, ValueError):
|
||||
pass
|
||||
|
||||
return {
|
||||
"project_id": str(project_id),
|
||||
"status": status,
|
||||
"model_count": model_count,
|
||||
"progress_percent": progress_percent,
|
||||
"current_stage": current_stage,
|
||||
"message": message,
|
||||
}
|
||||
except Exception:
|
||||
logger.exception("WF1 분석 상태 조회 실패: project_id=%s", project_id)
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={"status": "error", "message": "분석 상태 조회 중 오류가 발생했습니다."},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{project_id}/surface/models/{model_id}/preview", response_model=None)
|
||||
async def get_surface_model_preview(
|
||||
request: Request,
|
||||
project_id: UUID,
|
||||
model_id: int,
|
||||
smooth: bool = False,
|
||||
) -> Response | JSONResponse:
|
||||
"""지표면 모델의 3D 프리뷰 파일(GLB/PLY)을 반환한다."""
|
||||
pool = get_db_pool()
|
||||
try:
|
||||
async with pool.acquire() as connection:
|
||||
stored_path = await get_project_storage_relative_path(connection, project_id)
|
||||
async with connection.cursor() as cursor:
|
||||
await cursor.execute(
|
||||
"""
|
||||
SELECT model_type, model_file_path
|
||||
FROM surface_models
|
||||
WHERE id = %s AND project_id = %s
|
||||
""",
|
||||
(model_id, str(project_id)),
|
||||
)
|
||||
row = await cursor.fetchone()
|
||||
if not row:
|
||||
return JSONResponse(
|
||||
status_code=404,
|
||||
content={"status": "error", "message": "해당 모델을 찾을 수 없습니다."},
|
||||
)
|
||||
model_type, model_file_path = row[0], row[1]
|
||||
project_root = Path(resolve_stored_project_path(stored_path))
|
||||
if not model_file_path:
|
||||
return JSONResponse(
|
||||
status_code=404,
|
||||
content={"status": "error", "message": "모델 파일 경로가 없습니다."},
|
||||
)
|
||||
model_path = project_root / model_file_path
|
||||
models_dir = model_path.parent
|
||||
stem = model_path.stem
|
||||
|
||||
ext = "ply" if model_type == "meshfree" else "glb"
|
||||
if smooth and model_type in ("dtm", "tin"):
|
||||
preview_filename = f"{stem}_smooth_preview.glb"
|
||||
else:
|
||||
preview_filename = f"{stem}_preview.{ext}"
|
||||
|
||||
preview_path = models_dir / preview_filename
|
||||
if not preview_path.is_file():
|
||||
return JSONResponse(
|
||||
status_code=404,
|
||||
content={
|
||||
"status": "error",
|
||||
"message": "프리뷰 파일이 생성되지 않았거나 존재하지 않습니다.",
|
||||
},
|
||||
)
|
||||
|
||||
media_type = "application/octet-stream"
|
||||
if ext == "glb":
|
||||
media_type = "model/gltf-binary"
|
||||
elif ext == "ply":
|
||||
media_type = "application/ply"
|
||||
|
||||
# 브라우저가 이미 같은 파일을 갖고 있으면 304만 돌려준다(새로고침이 빨라진다).
|
||||
return cached_file_response(request, preview_path, media_type, preview_filename)
|
||||
|
||||
except Exception:
|
||||
logger.exception(
|
||||
"지표면 모델 프리뷰 조회 실패: project_id=%s, model_id=%s", project_id, model_id
|
||||
)
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={"status": "error", "message": "프리뷰 파일 조회 중 오류가 발생했습니다."},
|
||||
)
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
"""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
|
||||
@@ -0,0 +1,223 @@
|
||||
"""B04 지표면 분석 **상태 조회·모델 프리뷰** 라우터.
|
||||
|
||||
`B04_PreProcess_Router` 가 700줄을 넘겨 떼어낸 조각이다(2026-09-04). 경로·응답·로직은
|
||||
옮기기 전 그대로이고, 본체가 `include_router` 로 이 라우터를 그대로 실어 붙인다.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from uuid import UUID
|
||||
|
||||
import aiomysql
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.responses import JSONResponse, Response
|
||||
|
||||
from B03_FileInput.B03_FileInput_Repository import get_project_storage_relative_path
|
||||
from B04_PreProcess.B04_PreProcess_Router_Progress import read_surface_progress
|
||||
from common_util.common_util_http_cache import cached_file_response
|
||||
from common_util.common_util_initial_snapshot import is_designing
|
||||
from common_util.common_util_storage import resolve_stored_project_path
|
||||
from config.config_db import get_db_pool
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
# prefix 는 본체(`_Router`)가 `include_router` 할 때 붙는다 — 여기서 또 주면
|
||||
# 경로가 `/api/projects/api/projects/...` 로 겹친다.
|
||||
router = APIRouter(tags=["B04 Surface Analysis"])
|
||||
|
||||
|
||||
@router.get("/{project_id}/surface/status")
|
||||
async def get_wf1_analysis_status(project_id: UUID) -> dict:
|
||||
"""WF1 분석 상태를 조회한다."""
|
||||
pool = get_db_pool()
|
||||
try:
|
||||
async with pool.acquire() as connection:
|
||||
async with connection.cursor(aiomysql.DictCursor) as cursor:
|
||||
await cursor.execute(
|
||||
"""
|
||||
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)),
|
||||
)
|
||||
row = await cursor.fetchone()
|
||||
|
||||
# 만약 새 테이블에 정보가 없다면 기존 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"
|
||||
current_stage = "failed"
|
||||
if not message:
|
||||
message = "WF1 분석에 실패했습니다."
|
||||
elif state == "COMPLETE":
|
||||
status = "completed"
|
||||
progress_percent = 100
|
||||
current_stage = "completed"
|
||||
if not message:
|
||||
message = "WF1 분석이 완료되었습니다."
|
||||
elif state == "IN_PROGRESS":
|
||||
status = "in_progress"
|
||||
current_stage = "surface_analysis"
|
||||
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"
|
||||
if not message:
|
||||
message = "WF1 분석 대기 중입니다."
|
||||
|
||||
# 초기 설계 체인이 도는 동안은 아직 들어갈 때가 아니다 — WF1(stage 1)이 COMPLETE라도
|
||||
# 마커가 있으면 진행 중으로 돌려준다(2026-08-29 사용자 확정, CLAUDE.md 5장).
|
||||
# 여기서 덮어쓰는 이유: 위 분기는 stage 1만 보므로 체인 구간을 "완료"로 답한다.
|
||||
try:
|
||||
async with pool.acquire() as connection:
|
||||
stored_path = await get_project_storage_relative_path(connection, project_id)
|
||||
if stored_path and is_designing(Path(resolve_stored_project_path(stored_path))):
|
||||
status = "in_progress"
|
||||
current_stage = "initial_design"
|
||||
message = "초기 설계를 계산하는 중입니다."
|
||||
except (LookupError, OSError, ValueError):
|
||||
pass
|
||||
|
||||
return {
|
||||
"project_id": str(project_id),
|
||||
"status": status,
|
||||
"model_count": model_count,
|
||||
"progress_percent": progress_percent,
|
||||
"current_stage": current_stage,
|
||||
"message": message,
|
||||
}
|
||||
except Exception:
|
||||
logger.exception("WF1 분석 상태 조회 실패: project_id=%s", project_id)
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={"status": "error", "message": "분석 상태 조회 중 오류가 발생했습니다."},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{project_id}/surface/models/{model_id}/preview", response_model=None)
|
||||
async def get_surface_model_preview(
|
||||
request: Request,
|
||||
project_id: UUID,
|
||||
model_id: int,
|
||||
smooth: bool = False,
|
||||
) -> Response | JSONResponse:
|
||||
"""지표면 모델의 3D 프리뷰 파일(GLB/PLY)을 반환한다."""
|
||||
pool = get_db_pool()
|
||||
try:
|
||||
async with pool.acquire() as connection:
|
||||
stored_path = await get_project_storage_relative_path(connection, project_id)
|
||||
async with connection.cursor() as cursor:
|
||||
await cursor.execute(
|
||||
"""
|
||||
SELECT model_type, model_file_path
|
||||
FROM surface_models
|
||||
WHERE id = %s AND project_id = %s
|
||||
""",
|
||||
(model_id, str(project_id)),
|
||||
)
|
||||
row = await cursor.fetchone()
|
||||
if not row:
|
||||
return JSONResponse(
|
||||
status_code=404,
|
||||
content={"status": "error", "message": "해당 모델을 찾을 수 없습니다."},
|
||||
)
|
||||
model_type, model_file_path = row[0], row[1]
|
||||
project_root = Path(resolve_stored_project_path(stored_path))
|
||||
if not model_file_path:
|
||||
return JSONResponse(
|
||||
status_code=404,
|
||||
content={"status": "error", "message": "모델 파일 경로가 없습니다."},
|
||||
)
|
||||
model_path = project_root / model_file_path
|
||||
models_dir = model_path.parent
|
||||
stem = model_path.stem
|
||||
|
||||
ext = "ply" if model_type == "meshfree" else "glb"
|
||||
if smooth and model_type in ("dtm", "tin"):
|
||||
preview_filename = f"{stem}_smooth_preview.glb"
|
||||
else:
|
||||
preview_filename = f"{stem}_preview.{ext}"
|
||||
|
||||
preview_path = models_dir / preview_filename
|
||||
if not preview_path.is_file():
|
||||
return JSONResponse(
|
||||
status_code=404,
|
||||
content={
|
||||
"status": "error",
|
||||
"message": "프리뷰 파일이 생성되지 않았거나 존재하지 않습니다.",
|
||||
},
|
||||
)
|
||||
|
||||
media_type = "application/octet-stream"
|
||||
if ext == "glb":
|
||||
media_type = "model/gltf-binary"
|
||||
elif ext == "ply":
|
||||
media_type = "application/ply"
|
||||
|
||||
# 브라우저가 이미 같은 파일을 갖고 있으면 304만 돌려준다(새로고침이 빨라진다).
|
||||
return cached_file_response(request, preview_path, media_type, preview_filename)
|
||||
|
||||
except Exception:
|
||||
logger.exception(
|
||||
"지표면 모델 프리뷰 조회 실패: project_id=%s, model_id=%s", project_id, model_id
|
||||
)
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={"status": "error", "message": "프리뷰 파일 조회 중 오류가 발생했습니다."},
|
||||
)
|
||||
Reference in New Issue
Block a user