260710_0
This commit is contained in:
@@ -0,0 +1,327 @@
|
||||
# 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"""
|
||||
<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
|
||||
```
|
||||
|
||||
**올바른 구현:**
|
||||
```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. **배포:** 수정 완료 후 재검증 및 머지
|
||||
Reference in New Issue
Block a user