"""B03 업로드 및 B04 WF1 분석 결과 이메일 발송.""" import html import logging from typing import Any from uuid import UUID from common_util.common_util_email import send_email from config.config_system import ADMIN_EMAIL, APP_PUBLIC_BASE_URL logger = logging.getLogger(__name__) def _format_number(value: Any) -> str: if isinstance(value, int | float): return f"{value:,}" return "N/A" def _format_elevation(value: Any) -> str: if isinstance(value, int | float): return f"{value:.2f}" return "N/A" def _extract_elevation_range(bounds: dict[str, Any], statistics: dict[str, Any]) -> tuple[Any, Any]: if isinstance(bounds.get("z"), list | tuple) and len(bounds["z"]) >= 2: return bounds["z"][0], bounds["z"][1] return ( bounds.get("min_z", statistics.get("min_z")), bounds.get("max_z", statistics.get("max_z")), ) def _app_url(path: str) -> str: return f"{APP_PUBLIC_BASE_URL}/{path.lstrip('/')}" def _email_shell(title: str, body: str, *, accent: str = "#2563eb") -> str: box_style = ( f"border: 1px solid #e5e7eb; border-left: 4px solid {accent}; " "padding: 18px; margin: 20px 0;" ) row_style = ( "display: flex; justify-content: space-between; gap: 16px; padding: 9px 0; " "border-bottom: 1px solid #f3f4f6;" ) return f"""
안녕하세요, {html.escape(user_name or "사용자")}님.
프로젝트 {html.escape(project_name)}의 입력 파일이 저장되었습니다.
프로젝트 {html.escape(project_name)}의 입력 파일 저장과 초기 분석이 모두 끝났습니다.
로그인이 필요한 경우 인증 후 같은 화면으로 이동합니다.
""" subject = f"임도 초기 분석 완료 - {project_name}" success = await send_email( to_email=to_email, subject=subject, html=_email_shell(subject, body), ) logger.info("초기 분석 완료 이메일 발송 %s: %s", "성공" if success else "실패", to_email) return success async def send_analysis_error_email( *, project_id: UUID, project_name: str, to_email: str, error_message: str, ) -> bool: """WF1 Surface 분석 실패 알림 이메일을 발송한다.""" rows = "\n".join( [ _summary_row("프로젝트 ID", html.escape(str(project_id))), _summary_row("오류 내용", html.escape(error_message)), ] ) safe_name = html.escape(project_name) body = f"""프로젝트 {safe_name}의 지표면 분석 중 오류가 발생했습니다.
기술 지원팀에서 확인할 수 있도록 서버 로그에도 같은 오류를 기록했습니다.
""" subject = f"지표면 분석 실패 알림 - {project_name}" success = await send_email( to_email=to_email, subject=subject, html=_email_shell(subject, body, accent="#dc2626"), ) logger.info("WF1 오류 이메일 발송 %s: %s", "성공" if success else "실패", to_email) return success async def send_initial_design_failed_email( *, project_id: UUID, project_name: str, to_email: str, reason: str, ) -> bool: """초기 설계(B05·B06 자동 계산) 실패를 알리고 관리자 문의를 안내한다. 부분 결과를 초기값으로 삼지 않는다는 방침(2026-09-02 사용자 확정)에 따라, 체인이 깨진 프로젝트는 [초기화]가 되돌릴 기준이 없다. 완료 메일 대신 이 메일을 보내 사용자가 화면에서 헛작업을 하지 않게 한다. """ contact = ADMIN_EMAIL.strip() contact_line = ( f'관리자에게 문의해 주세요 — ' f"{html.escape(contact)}
" if contact else "관리자에게 문의해 주세요.
" ) rows = "\n".join( [ _summary_row("프로젝트 ID", html.escape(str(project_id))), _summary_row("중단 지점", html.escape(reason or "초기 설계 처리 실패")), ] ) safe_name = html.escape(project_name) body = f"""프로젝트 {safe_name}의 초기 설계 자동 계산이 완료되지 못했습니다.
초기 설계가 끝나지 않아 종·횡단 화면의 값이 기준값으로 확정되지 않았습니다. 이 상태에서는 [초기화]로 되돌릴 초기값도 없습니다.
{contact_line}기술 지원팀이 확인할 수 있도록 서버 로그에도 같은 내용을 기록했습니다.
""" subject = f"초기 설계 실패 알림 - {project_name}" success = await send_email( to_email=to_email, subject=subject, html=_email_shell(subject, body, accent="#dc2626"), ) logger.info("초기 설계 실패 이메일 발송 %s: %s", "성공" if success else "실패", to_email) return success