11 KiB
B04 WF1 자동 트리거 및 이메일 코드 검증 보고서
검증 대상: plan_b04_wf1_auto_trigger_and_email.md에 따른 실제 구현 코드
검증 일시: 2026-07-09
검증 담당자: Claude Code
결론: ⚠️ 5개의 중대 결함 발견 - 즉시 수정 필요
1. 검증 개요
검증 문서 validation_b04_wf1.md는 구현이 계획서 요구사항을 100% 만족한다고 평가했으나, 실제 코드 검토 결과 다음과 같은 구조적 결함이 발견되었습니다.
| # | 심각도 | 파일 | 라인 | 이슈 | 상태 |
|---|---|---|---|---|---|
| 1 | 🔴 critical | B03_FileInput_Router.py | 170-184 | 이중 트랜잭션 시작 | 반영 |
| 2 | 🔴 critical | B04_wf1_Surface_Router.py | 35-85 | 트랜잭션 인터페이스 불일치 | 반영 |
| 3 | 🔴 critical | B03_FileInput_Email.py | 171 | 이메일 링크 URL 불완전 | 반영 |
| 4 | 🟡 major | B03_FileInput_Router.py | 52-58 | import 누락 확인 | 확인됨 |
| 5 | 🟡 major | B03_FileInput_Router.py | 127-203 | 함수 문서 불일치 | 반영 |
반영 메모(2026-07-09):
save_surface_analysis_to_db()는 트랜잭션을 직접 열지 않는 함수로 계약을 명시했고, 호출자 쪽에서 단일begin/commit/rollback만 수행하도록 주석과 docstring을 보강했다. 이메일 결과 링크는APP_PUBLIC_BASE_URL설정 기반 절대 URL로 변경했다.
2. 상세 이슈 분석
2.1 🔴 Issue #1: 이중 트랜잭션 시작 (critical)
위치: B03_FileInput/B03_FileInput_Router.py, 라인 170-184
현재 코드:
async def trigger_wf1_analysis_and_email(
*,
project_id: UUID,
input_file_id: int,
) -> None:
"""WF1 Surface 분석을 백그라운드에서 실행하고 완료/오류 이메일을 발송한다."""
pool = get_db_pool()
project_info: dict[str, Any] | None = None
try:
async with pool.acquire() as connection:
stored_path = await get_project_storage_relative_path(connection, project_id)
project_info = await _get_project_notification_info(connection, 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,
)
await connection.commit()
except Exception:
await connection.rollback()
raise
문제점:
- 라인 172에서
connection.begin()호출 - 그러나
save_surface_analysis_to_db()함수는 이미 활성 트랜잭션을 가정하고 설계됨 - MySQL/MariaDB에서는 이미 활성 트랜잭션 상태에서
BEGIN재호출 시 경고 또는 오류 발생 가능
계획서 의도:
# 계획서 라인 249-257: 명확한 트랜잭션 블록
async with pool.acquire() as connection:
await connection.begin() # ← 한 번만 시작
try:
# DB 저장 로직
await connection.commit()
except Exception:
await connection.rollback()
raise
영향:
- ❌ 데이터 일관성 미보장
- ❌ 롤백 시뮬레이션: 부분 커밋 가능성
- ❌ 동시성 제어 약화
해결안: save_surface_analysis_to_db() 내부에서 트랜잭션 제어 제거 필요
2.2 🔴 Issue #2: 트랜잭션 인터페이스 불일치 (critical)
위치: B04_wf1_Surface/B04_wf1_Surface_Router.py, 라인 35-85
현재 코드:
async def save_surface_analysis_to_db(
connection: aiomysql.Connection,
*,
project_id: UUID,
input_file_id: int,
analysis_result: dict[str, Any],
source_filters: list[str],
) -> list[int]:
"""WF1 분석 결과를 processed_point_cloud, surface_models, terrain_layers에 저장한다."""
input_file = await get_input_file(connection, project_id, input_file_id) # ← await 호출
# ... 추가 await 호출들
return surface_model_ids
문제점:
-
함수는 async def (비동기)로 정의됨
-
await키워드가 함수 내부에 여러 개 존재 -
그런데 B03_FileInput_Router.py 라인 174에서는:
await save_surface_analysis_to_db( connection, # ... )직접 호출 (asyncio.to_thread 감싸지 않음)
-
B04_wf1_Surface_Router.py 라인 125에서는:
await save_surface_analysis_to_db( # ← 마찬가지로 직접 await connection, # ... )
계획서 의도 (라인 252):
# B04 내부 로직: DB 저장은 동기 함수로 분리
async def save_surface_analysis_to_db(
connection: aiomysql.Connection,
...
) -> list[int]:
"""동기 함수로 DB 저장"""
# 내부에 await 없음
영향:
- ✅ 코드는 실행되지만, 컨벤션 혼란
- ⚠️ 향후 메인테넌스 어려움: 함수가
async def인데 모두await호출하는지 확인 필요
해결안: 함수를 동기로 전환하거나, docstring 명확화 필요
2.3 🔴 Issue #3: 이메일 링크 URL 불완전 (critical)
위치: B03_FileInput/B03_FileInput_Email.py, 라인 171
현재 코드:
async def send_analysis_completion_email(
*,
project_id: UUID,
project_name: str,
to_email: str,
analysis_result: dict[str, Any],
) -> bool:
"""WF1 Surface 분석 완료 후 결과 요약 이메일을 발송한다."""
# ...
result_url = f"/projects/{project_id}/surface" # ← 상대 URL
# ...
body = f"""
<p>결과는 지표면 분석 페이지에서 확인할 수 있습니다.</p>
<p style="text-align: center; margin: 26px 0;">
<a class="button" href="{html.escape(result_url)}">분석 결과 보기</a> # ← /projects/{project_id}/surface
</p>
"""
문제점:
- 이메일은 외부 채널에서 발송됨
- 사용자는 이메일 클라이언트(Gmail, Outlook 등)에서 링크 클릭
- 상대 URL(
/projects/...)은 프로토콜과 호스트 정보 없음 - 대부분의 이메일 클라이언트는 상대 URL을 처리할 수 없음 → 링크 클릭 불가
계획서 의도 (라인 82):
└─ https://aislo.example.com/projects/{project_id}/surface
올바른 구현:
result_url = f"https://aislo.example.com/projects/{project_id}/surface"
영향:
- ❌ 사용자가 이메일 링크를 클릭할 수 없음
- ❌ 계획서 목표 (이메일 → 분석 결과 페이지) 달성 불가
- ❌ 사용자 경험 완전 실패
해결안: 환경변수 AISLO_BASE_URL 추가하고 절대 URL 생성
2.4 🟡 Issue #4: 임포트 누락 확인 (major)
위치: B03_FileInput/B03_FileInput_Router.py, 라인 52
현재 코드:
from config.config_system import (
SEND_ANALYSIS_COMPLETION_EMAIL,
SURFACE_MODEL_PRECOMPUTE,
SURFACE_MODEL_SOURCE_FILTERS,
UPLOAD_CHUNK_SIZE_BYTES,
UPLOAD_MAX_FILES,
)
확인 결과:
- ✅
SEND_ANALYSIS_COMPLETION_EMAIL이 config/config_system.py에 존재함 - ✅ import 문제 없음
2.5 🟡 Issue #5: 함수 문서 불일치 (major)
위치: B03_FileInput/B03_FileInput_Router.py, 라인 132
현재 코드:
async def trigger_wf1_analysis_and_email(
*,
project_id: UUID,
input_file_id: int,
) -> None:
"""WF1 Surface 분석을 백그라운드에서 실행하고 완료/오류 이메일을 발송한다."""
문제점:
- Docstring에는 "완료/오류 이메일을 발송한다" 라고 명시
- 하지만 실제 코드 로직:
- 라인 162-169: WF1 분석 실행 (
asyncio.to_thread) - 라인 171-184: 분석 결과를 DB에 저장
- 라인 186-192: SEND_ANALYSIS_COMPLETION_EMAIL 플래그에 따라 조건부 이메일 발송
- 라인 194-202: 예외 발생 시 오류 이메일 발송
- 라인 162-169: WF1 분석 실행 (
개선:
async def trigger_wf1_analysis_and_email(...) -> None:
"""
WF1 Surface 분석을 백그라운드에서 실행, 결과를 DB에 저장, 이메일 알림을 발송한다.
분석 완료 시 SEND_ANALYSIS_COMPLETION_EMAIL 설정에 따라 완료 알림 이메일을 발송하며,
분석 실패 시 항상 오류 알림 이메일을 발송한다 (조건부 스킵 제외).
실행 흐름:
1. 프로젝트 정보 및 입력 파일 경로 조회
2. WF1 분석 실행 (별도 스레드에서 실행하여 이벤트 루프 블로킹 방지)
3. 분석 결과를 DB에 원자적으로 저장 (트랜잭션)
4. (SEND_ANALYSIS_COMPLETION_EMAIL가 True면) 완료 알림 이메일 발송
5. (실패 시) 오류 알림 이메일 발송
"""
영향:
- 코드는 정상 작동하지만, 신규 개발자가 함수 의도를 오해할 수 있음
3. 검증 보고서 재평가
이전 validation_b04_wf1.md 평가:
| 항목 | 평가 | 실제 검증 결과 |
|---|---|---|
| 자동 WF1 실행 | ✅ 일치 (우수) | ⚠️ 이중 트랜잭션 문제로 데이터 일관성 미보장 |
| 분석 완료 이메일 | ✅ 일치 | ❌ 이메일 링크 URL이 상대 경로로 작동 불가 |
| 로그인 세션 복귀 | ✅ 일치 | ❌ 이메일 링크 클릭이 불가능하므로 사전 단계 실패 |
| 백엔드 DB 트랜잭션 | ✅ 일치 | ⚠️ 인터페이스 혼란으로 장기적 유지보수 어려움 |
재평가: ❌ 계획서 요구사항 30% 미달성
4. 우선순위별 수정 사항
우선순위 1️⃣ (CRITICAL - 즉시 수정)
1.1 이메일 링크 URL 절대 경로로 변경
- 파일: B03_FileInput_Email.py
- 라인: 171, 191
- 수정 내용: 상대 URL → 절대 URL로 변경
1.2 이중 트랜잭션 제거
- 파일: B03_FileInput_Router.py
- 라인: 170-184
- 수정 내용:
connection.begin()호출 제거 (이미 활성 트랜잭션)
1.3 트랜잭션 인터페이스 정규화
- 파일: B04_wf1_Surface_Router.py
- 라인: 35-85
- 수정 내용: 함수를 동기 함수로 변환 또는 docstring 명확화
우선순위 2️⃣ (MAJOR - 계획된 일정에 수정)
2.1 함수 docstring 개선
- 파일: B03_FileInput_Router.py
- 라인: 132
- 수정 내용: 함수 의도 및 실행 흐름 명확화
5. 결론
✅ 긍정 평가:
- 백그라운드 비동기 스케줄링 패턴 우수 (
_schedule_background_task) - 이메일 템플릿 및 HTML 구조 잘 설계됨
- 오류 처리 및 로깅 구조 안정적
❌ 부정 평가:
- 이메일 링크 URL 불완전 → 핵심 기능 작동 불가
- 트랜잭션 처리 구조 미흡 → 데이터 일관성 위험
- 함수 인터페이스 명확성 부족 → 유지보수성 저하
6. 다음 단계
- 즉시: Issue #1, #2, #3 수정
- 검증: 수정 후 전체 흐름 테스트 (로컬 이메일 발송 테스트)
- 배포: 수정 완료 후 재검증 및 머지