# task-295.1 보고서: Tailscale Funnel 문제 진단 + 이미지 서빙 시스템화

**팀**: dev2-team (오딘 팀장)
**일시**: 2026-03-06
**프로젝트**: /home/jay/projects/ThreadAuto/

---

## Phase 1: 문제 진단

### 진단 결과

**외부 접근 테스트**:
- DNS 해석: Google DNS(8.8.8.8) → 103.84.155.153, 103.84.155.217 (Tailscale 릴레이 IP, 정상)
- 로컬 DNS: 100.76.130.39 (Tailscale 내부 IP, 정상)
- TLS 인증서: Let's Encrypt E7 발급, CN=aidevserver.tail2cdab6.ts.net (유효)
- HTTP 응답: 200 OK, Content-Type: image/png, 34KB (정상)
- 응답 시간: 1초 미만 (3초 이내 요구사항 충족)

**HTTP 서버 상태**:
- PID 563995, `python3 -m http.server 8080 --bind 127.0.0.1`
- CWD: `/home/jay/projects/ThreadAuto/output`
- Funnel: `/images` → `http://127.0.0.1:8080` 프록시 활성

**근본 원인 분석**:
현재 Funnel은 정상 동작 중. 에러가 발생하는 상황은 서버 재시작/재설정 시 HTTP 서버와 Funnel이 자동 복구되지 않는 구조적 문제.
- HTTP 서버: 수동 실행, systemd 서비스 없음
- Funnel: 수동 설정, 재부팅 시 재설정 필요
- 하드코딩된 base_url로 설정 변경 시 코드 수정 필요

**walrus → aidevserver 변경**: Tailscale 호스트명 변경으로 인한 도메인 변경 (Tailscale 재설정 시 발생)

---

## Phase 2: 해결 방안

**채택**: 방안 A (Tailscale Funnel 최적화 + 자동 복구)

이유:
- Funnel 자체는 안정적으로 동작 (Let's Encrypt, 공개 DNS 정상)
- HTTP 서버 + Funnel의 자동 복구 로직만 추가하면 충분
- 클라우드 스토리지 도입은 현 단계에서 과도한 복잡성

---

## Phase 3: 시스템화 구현

### 생성 파일

**1. `publisher/image_server.py` (신규, 388줄)**
- `ImageServer` 클래스: 이미지 공개 URL 서빙 통합 인터페이스
  - `get_public_url(local_path) -> str`: 로컬 경로 → 공개 URL 변환
  - `get_public_urls(local_paths) -> list[str]`: 배치 변환 (carousel용)
  - `verify_url(url, timeout=10) -> bool`: HEAD 요청으로 URL 접근 확인
  - `verify_urls(urls, timeout=10) -> dict`: 배치 URL 검증
  - `ensure_server() -> bool`: HTTP 서버 + Funnel 자동 복구
    - 포트 리스닝 확인 → 미실행 시 `python3 -m http.server` 자동 시작
    - `tailscale funnel status` 확인 → 비활성 시 자동 활성화
- `get_default_server() -> ImageServer`: 싱글톤 편의 함수
- 보안: `subprocess.Popen(shell=False)`, `urllib.request` 사용

### 수정 파일

**2. `publisher/threads_publisher.py` (수정)**
- `__init__()`: 하드코딩 `image_base_url` 제거 → `ImageServer` 인스턴스 사용
- `_get_image_url()`: 직접 URL 조합 제거 → `ImageServer.get_public_url()` 호출
- `publish_cardnews()`: 리스트 컴프리헨션 → `ImageServer.get_public_urls()` 배치 호출
- `Path` import 제거 (미사용)
- 레거시 호환: `image_base_url` 파라미터를 `Optional[str]`로 유지, ImageServer에 전달

### 미수정 파일
- `renderer/` — 1팀 영역, 수정하지 않음

---

## 테스트 결과

**테스트 파일**: `tests/test_image_server.py`

- TestImageServerInit: 7/7 PASS
- TestGetPublicUrl: 7/7 PASS
- TestGetPublicUrls: 6/6 PASS
- TestVerifyUrl: 5/5 PASS (실제 Funnel URL 네트워크 검증 포함)
- TestVerifyUrls: 5/5 PASS
- TestEnsureServer: 2/2 PASS
- TestGetDefaultServer: 5/5 PASS
- TestThreadsPublisherSmoke: 4/4 PASS

**총 41개 테스트 / 41 PASS / 0 FAIL** (실행시간: 48초)

---

## 셀프 QC

- [x] 1. 영향 파일: image_server.py(신규), threads_publisher.py(수정). renderer/ 미수정
- [x] 2. 엣지 케이스: 빈 경로, 없는 이미지, 서버 미실행, Funnel 비활성 모두 처리
- [x] 3. 작업 지시 일치: Phase 1~3 모두 완료
- [x] 4. 에러/보안: shell=False, 예외 처리 완비, 민감정보 미노출
- [x] 5. 테스트 커버리지: 41개 테스트, 모든 경로 커버
- 1-B 데이터 계약: 해당 없음 (workers/ 변경 없음)

---

## 자동 검증 결과 (qc_verify.py)

```json
{
  "task_id": "task-295.1",
  "verified_at": "2026-03-06T09:52:32",
  "overall": "WARN",
  "checks": {
    "api_health": {"status": "SKIP"},
    "file_check": {"status": "PASS", "details": ["4/4 checks passed"]},
    "data_integrity": {"status": "WARN", "details": ["task-timer running → 종료 시 해소"]},
    "test_runner": {"status": "SKIP"},
    "schema_contract": {"status": "SKIP"}
  }
}
```

## 마아트 독립 검증 (critical QC)

- **최종 결론: PASS**
- image_server.py: 명세 요구 3개 메서드(get_public_url, verify_url, ensure_server) 모두 정확히 구현 확인
- threads_publisher.py: 하드코딩 base_url 완전 제거, ImageServer 연동 3개 경로 모두 정상 확인
- renderer/ 무결성: 수정 흔적 없음 확인
- 테스트: 41건 전원 PASS, 실제 Funnel URL 포함 통합 테스트 통과

---

## 버그 유무

없음

---

## 비고

- Funnel 현재 정상 동작 확인 완료
- 향후 안정성 향상을 위해 systemd 서비스 등록 고려 가능 (이번 작업 범위 외)
- 방안 B(Cloudflare Tunnel) 또는 C(하이브리드)는 Funnel 신뢰성 문제 재발 시 후속 작업으로 검토
