# 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 **현재 코드:** ```python 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` 재호출 시 경고 또는 오류 발생 가능** **계획서 의도:** ```python # 계획서 라인 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 **현재 코드:** ```python 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에서는: ```python await save_surface_analysis_to_db( connection, # ... ) ``` **직접 호출** (asyncio.to_thread 감싸지 않음) - B04_wf1_Surface_Router.py 라인 125에서는: ```python await save_surface_analysis_to_db( # ← 마찬가지로 직접 await connection, # ... ) ``` **계획서 의도 (라인 252):** ```python # 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 **현재 코드:** ```python 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"""
결과는 지표면 분석 페이지에서 확인할 수 있습니다.
분석 결과 보기 # ← /projects/{project_id}/surface
""" ``` **문제점:** - **이메일은 외부 채널에서 발송됨** - 사용자는 이메일 클라이언트(Gmail, Outlook 등)에서 링크 클릭 - 상대 URL(`/projects/...`)은 **프로토콜과 호스트 정보 없음** - 대부분의 이메일 클라이언트는 상대 URL을 처리할 수 없음 → **링크 클릭 불가** **계획서 의도 (라인 82):** ``` └─ https://aislo.example.com/projects/{project_id}/surface ``` **올바른 구현:** ```python 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 **현재 코드:** ```python 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 **현재 코드:** ```python async def trigger_wf1_analysis_and_email( *, project_id: UUID, input_file_id: int, ) -> None: """WF1 Surface 분석을 백그라운드에서 실행하고 완료/오류 이메일을 발송한다.""" ``` **문제점:** - Docstring에는 **"완료/오류 이메일을 발송한다"** 라고 명시 - 하지만 실제 코드 로직: 1. 라인 162-169: WF1 분석 실행 (`asyncio.to_thread`) 2. 라인 171-184: 분석 결과를 DB에 저장 3. 라인 186-192: **SEND_ANALYSIS_COMPLETION_EMAIL 플래그에 따라 조건부** 이메일 발송 4. 라인 194-202: 예외 발생 시 오류 이메일 발송 **개선:** ```python 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. 다음 단계 1. **즉시:** Issue #1, #2, #3 수정 2. **검증:** 수정 후 전체 흐름 테스트 (로컬 이메일 발송 테스트) 3. **배포:** 수정 완료 후 재검증 및 머지