change(B03): 업로드/분석 알림 메일을 초기 설계 완료 시점 한 통으로 통합
같은 업로드 한 번에 업로드 완료 메일과 지표면 분석 완료 메일이 두 통 도착했다. 사용자가 실제로 화면을 볼 수 있는 시점은 초기 설계(B04 전처리 + B05 종단 + B06 횡단)까지 끝난 때 하나뿐이므로 그 시점에 통합 메일 한 통만 보낸다. - send_initial_analysis_complete_email 신설: 지표면 요약 + 노선 연장/측점 수 + 종단설계 화면 링크. 자동 체인 미실행/실패 시 전처리 요약만 싣고 안내 문구 전환 - run_auto_design_chain이 요약(route_id/length_m/cross_section_count) 반환 - WF1 서비스: 체인 실행 -> 통합 메일 순으로 재배치 - 업로드 직후 메일 발송 호출 제거(일반·청크 두 경로). 발송 함수는 프로젝트 생성 전 임시 보관함 안내용으로 남겨 두고 주석으로 용도 명시 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -118,7 +118,13 @@ async def send_file_upload_complete_email(
|
||||
file_size_mb: float,
|
||||
metadata: dict[str, Any],
|
||||
) -> bool:
|
||||
"""파일 업로드 완료 직후 사용자에게 WF1 분석 시작을 안내한다."""
|
||||
"""파일 저장만 끝난 시점의 안내 메일.
|
||||
|
||||
프로젝트 업로드 경로에서는 더 이상 쓰지 않는다 — 그 흐름은 초기 설계까지 마친 뒤
|
||||
[[send_initial_analysis_complete_email]] 한 통으로 알린다. 이 메일은 프로젝트를
|
||||
만들기 전 임시 보관함(storage/tmp)에 자료를 올렸을 때 보관 안내용으로 쓴다
|
||||
(2026-08-08 사용자 지시, 임시 보관 기능 구현 시 연결).
|
||||
"""
|
||||
bounds = metadata.get("bounds") or {}
|
||||
statistics = metadata.get("statistics") or {}
|
||||
min_z, max_z = _extract_elevation_range(bounds, statistics)
|
||||
@@ -158,14 +164,22 @@ async def send_file_upload_complete_email(
|
||||
return success
|
||||
|
||||
|
||||
async def send_analysis_completion_email(
|
||||
async def send_initial_analysis_complete_email(
|
||||
*,
|
||||
project_id: UUID,
|
||||
project_name: str,
|
||||
to_email: str,
|
||||
analysis_result: dict[str, Any],
|
||||
design_summary: dict[str, Any] | None = None,
|
||||
) -> bool:
|
||||
"""WF1 Surface 분석 완료 후 결과 요약 이메일을 발송한다."""
|
||||
"""업로드 ~ 초기 설계(B04 전처리·B05 종단·B06 횡단)까지 마친 뒤 한 통으로 알린다.
|
||||
|
||||
예전에는 업로드 직후와 지표면 분석 직후에 각각 메일을 보내 같은 작업으로 두 통이
|
||||
도착했다. 사용자가 실제로 확인할 시점은 초기 설계까지 끝나 화면에서 검토할 수 있게
|
||||
된 때 하나뿐이므로 그때 한 통만 보낸다(2026-08-08 사용자 지시).
|
||||
|
||||
`design_summary`가 없으면(자동 설계 체인 미실행·실패) 전처리 결과까지만 싣는다.
|
||||
"""
|
||||
processed = analysis_result.get("processed") or {}
|
||||
statistics = processed.get("statistics") or {}
|
||||
bounds = processed.get("bounds") or {}
|
||||
@@ -173,39 +187,71 @@ async def send_analysis_completion_email(
|
||||
min_z = statistics.get("min_z", bounds.get("min_z"))
|
||||
max_z = statistics.get("max_z", bounds.get("max_z"))
|
||||
point_count = processed.get("point_count", 0)
|
||||
result_url = _app_url(f"/projects/{project_id}/surface")
|
||||
rows = "\n".join(
|
||||
result_url = _app_url("/#/b05-profile")
|
||||
|
||||
surface_rows = "\n".join(
|
||||
[
|
||||
_summary_row("분석 포인트 수", _format_number(point_count)),
|
||||
_summary_row(
|
||||
"고도 범위",
|
||||
f"{_format_elevation(min_z)} ~ {_format_elevation(max_z)} m",
|
||||
),
|
||||
_summary_row("생성 모델 수", f"{len(models):,}개"),
|
||||
_summary_row("포함 데이터", "DTM, TIN, 미리보기 레이어"),
|
||||
_summary_row("생성 지표면 모델", f"{len(models):,}개"),
|
||||
]
|
||||
)
|
||||
|
||||
body = f"""
|
||||
<p>프로젝트 <strong>{html.escape(project_name)}</strong>의 지표면 분석이 완료되었습니다.</p>
|
||||
if design_summary:
|
||||
length_m = design_summary.get("length_m")
|
||||
design_rows = "\n".join(
|
||||
[
|
||||
_summary_row(
|
||||
"노선 연장",
|
||||
f"{length_m:,.1f} m" if isinstance(length_m, int | float) else "N/A",
|
||||
),
|
||||
_summary_row("측점 수", _format_number(design_summary.get("cross_section_count"))),
|
||||
_summary_row("초기 설계", "종단 계획선·횡단 기본 설계 저장 완료"),
|
||||
]
|
||||
)
|
||||
design_block = f"""
|
||||
<h3 style="margin:24px 0 8px;font-size:15px;">초기 노선·종횡단 설계</h3>
|
||||
<div class="box">
|
||||
{rows}
|
||||
{design_rows}
|
||||
</div>
|
||||
<p>결과는 지표면 분석 페이지에서 확인할 수 있습니다.</p>
|
||||
<div class="notice">
|
||||
기본값으로 계산한 결과입니다. 종단설계 화면에서 검토·수정한 뒤 횡단설계에서
|
||||
[확정]을 누르면 다음 단계(수량 산출)로 넘어갑니다.
|
||||
</div>
|
||||
"""
|
||||
else:
|
||||
design_block = """
|
||||
<div class="notice">
|
||||
초기 노선·종횡단 자동 계산은 완료되지 않았습니다. 종단설계 화면에서 직접
|
||||
경로를 계산해 주세요.
|
||||
</div>
|
||||
"""
|
||||
|
||||
body = f"""
|
||||
<p>프로젝트 <strong>{html.escape(project_name)}</strong>의 입력 파일 저장과
|
||||
초기 분석이 모두 끝났습니다.</p>
|
||||
<h3 style="margin:20px 0 8px;font-size:15px;">지표면 전처리</h3>
|
||||
<div class="box">
|
||||
{surface_rows}
|
||||
</div>
|
||||
{design_block}
|
||||
<p style="text-align: center; margin: 26px 0;">
|
||||
<a class="button" href="{html.escape(result_url)}">분석 결과 보기</a>
|
||||
<a class="button" href="{html.escape(result_url)}">종단설계 화면 열기</a>
|
||||
</p>
|
||||
<p style="color:#6b7280;font-size:13px;">
|
||||
로그인이 필요한 경우 인증 후 같은 결과 페이지로 이동합니다.
|
||||
로그인이 필요한 경우 인증 후 같은 화면으로 이동합니다.
|
||||
</p>
|
||||
"""
|
||||
subject = f"임도 지표면 분석 완료 - {project_name}"
|
||||
subject = f"임도 초기 분석 완료 - {project_name}"
|
||||
success = await send_email(
|
||||
to_email=to_email,
|
||||
subject=subject,
|
||||
html=_email_shell(subject, body),
|
||||
)
|
||||
logger.info("WF1 완료 이메일 발송 %s: %s", "성공" if success else "실패", to_email)
|
||||
logger.info("초기 분석 완료 이메일 발송 %s: %s", "성공" if success else "실패", to_email)
|
||||
return success
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user