fix(B05,B03): 초기 설계 실패 시 초기값 부재 처리 — 실패 마커·안내 메일·[초기화] 거부
증상: 파일 입력 직후 초기 계산값과 B05 [초기화] 결과가 다름. 원인 - 자동설계 체인이 5단계까지 전부 성공해야만 `save_initial_snapshot()` 호출. 중간에 깨지면 `initial_snapshot/` 미생성 → [초기화]가 복원 대신 재계산 폴백. - 재계산 폴백의 지표면 기준이 체인과 다름 — 체인은 stage 1 확정값 (`get_surface_confirmation_params`), [초기화]는 config 기본값 (`surface_confirmation_defaults`). 실측: 프로젝트 stage1 `classification`/5m 대 config `csf`/1m. - 체인이 깨져도 WF1 은 초기 분석 완료 메일을 그대로 발송. 수정 (2026-09-02 사용자 확정 — 부분 결과는 분석 안 됨과 다르지 않으므로 부분 스냅샷은 만들지 않음) - `initial_design.failed` 마커 신설 — 프로젝트 루트(스냅샷 4트리 밖), 실패 사유 기록. 체인 진입 시 옛 마커 제거, 실패 5지점 + 스냅샷 저장 실패에서 기록. - WF1 이 마커를 읽어 완료 메일 대신 `send_initial_design_failed_email()` 발송 (관리자 주소 `ADMIN_EMAIL`, 없으면 주소 없이 안내). - B05 [초기화]: 스냅샷 있으면 복원, 실패 마커 있으면 409 `initial_design_failed` 로 거부(DELETE 앞에서 반환 — 데이터 무변경), 옛 프로젝트만 종전 재계산 폴백. - 재계산 폴백의 지표면 기준을 `get_surface_confirmation_params()` 로 통일. 자체검증: `pytest tmp/tests/ -q` 366 passed / 14 skipped / 0 failed (+4). 공용 브라우저 실측 — 실패 프로젝트 [초기화] 409 응답·`routes` 행 무변경 확인. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,7 @@ from typing import Any
|
||||
from uuid import UUID
|
||||
|
||||
from common_util.common_util_email import send_email
|
||||
from config.config_system import APP_PUBLIC_BASE_URL
|
||||
from config.config_system import ADMIN_EMAIL, APP_PUBLIC_BASE_URL
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -285,3 +285,53 @@ async def send_analysis_error_email(
|
||||
)
|
||||
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'<p>관리자에게 문의해 주세요 — <a href="mailto:{html.escape(contact)}">'
|
||||
f"{html.escape(contact)}</a></p>"
|
||||
if contact
|
||||
else "<p>관리자에게 문의해 주세요.</p>"
|
||||
)
|
||||
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"""
|
||||
<p>프로젝트 <strong>{safe_name}</strong>의 <strong>초기 설계 자동 계산</strong>이
|
||||
완료되지 못했습니다.</p>
|
||||
<div class="box">
|
||||
{rows}
|
||||
</div>
|
||||
<div class="notice">
|
||||
<p>초기 설계가 끝나지 않아 종·횡단 화면의 값이 기준값으로 확정되지 않았습니다.
|
||||
이 상태에서는 [초기화]로 되돌릴 초기값도 없습니다.</p>
|
||||
{contact_line}
|
||||
</div>
|
||||
<p>기술 지원팀이 확인할 수 있도록 서버 로그에도 같은 내용을 기록했습니다.</p>
|
||||
"""
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user