--- tags: [infra, robeing, rb8001, skill-email, naverworks, plans] --- # 260309 9시 네이버 이메일 분석 실패 은닉 해결 계획 ## 상위 원칙 - [Infra Project Identity](../../00_Philosophy/00_IDENTITY/Infra_Project_Identity.md) - [Core Infrastructure Principles](../../00_Philosophy/01_PRINCIPLES/Core_Infrastructure_Principles.md) - [Operational Guardrails](../../00_Philosophy/02_GUARDRAILS/Operational_Guardrails.md) - 공통 작성 원칙: [0_VALUE Writing Principles](https://github.com/happybell80/0_VALUE/blob/main/02_Governance/writing-principles.md) ## 관련 문서 - [Infra Journey](../README.md) - [9시 네이버 이메일 분석 미전송과 실패 은닉 이슈](../troubleshooting/260309_9시_네이버이메일분석_미전송_실패은닉_이슈.md) - [9시 네이버 이메일 분석 미전송 실패 은닉 리서치](../research/260309_9시_네이버이메일분석_미전송_실패은닉_리서치.md) - [24서버 실서비스 운영전환 계획](./260309_24서버_실서비스운영전환_계획.md) ## 목표 - 네이버 이메일 분석 브리핑에서 `메일 조회 실패`와 `실제 메일 0건`을 분리한다. - `rb8001` 스케줄러가 실패를 성공처럼 기록하지 않도록 한다. - 재발 시 `auth refresh 지연`인지 `NAVER WORKS 외부 API 지연`인지 로그만으로 구분 가능하게 만든다. - `expires_at` 시간대 정합성 위험을 별도 판단 가능한 상태로 만든다. ## 문제 정의 - 현재 `rb8001/app/services/skills/naverworks_briefing.py`는 `skill-email /messages` 호출 실패를 `return []`로 삼킨다. - 그 결과 `process_briefing()`는 실패를 `No emails in the last 24 hours`로 오인하고 종료한다. - 이 구조는 상위 SSOT의 `Truth First`, `근본 원인 직접 수정 우선`, `광범위 예외 폴백 금지` 원칙과 충돌한다. ## 범위 고정 ### 1차 수정 대상 - `rb8001/app/services/skills/naverworks_briefing.py` - `skill-email/main.py` - `skill-email/services/naverworks_provider.py` ### 1차 검증 대상 - `rb8001` 09:00 스케줄 실행 경로 - `skill-email /messages?provider=naverworks` - `auth-server /auth/naverworks/passport/refresh` ### 2차 판단 대상 - `main_db.naverworks_token.expires_at` 컬럼 타입 변경 필요 여부 - 컨테이너 시간대 통일 여부 또는 만료 판단 로직의 시간대 명시 처리 ## 실행 원칙 - 실패를 `메일 0건`으로 대체하지 않는다. - `except Exception`으로 원인성 실패를 일괄 래핑하지 않는다. - `timeout`, `비200 응답`, `응답 구조 이상`, `토큰 갱신 실패`를 구분해 기록한다. - 수정 후에는 로그, 헬스체크, 재현 테스트로 실제 실패 표현이 교정됐는지 확인한다. ## 단계별 계획 ### 1. `rb8001`에서 실패 은닉을 제거한다 - `_fetch_recent_emails()`가 `[]` 대신 원인성 예외를 올리도록 바꾼다. - 최소 분기 대상: - `httpx.ReadTimeout` - `httpx.ConnectError` - `비200 응답` - 응답 JSON 구조 이상 - `process_briefing()`는 `실제 0건`일 때만 `No emails`로 처리하고, 조회 실패는 스케줄러 실패로 남기게 바꾼다. ### 2. `skill-email` 단계별 원인 로그를 보강한다 - `/messages` 경로에서 `provider=naverworks` 처리 시 다음 단계 로그를 남긴다. - DB 토큰 조회 시작/종료 - 만료 판단 결과 - refresh 호출 여부와 응답 코드 - NAVER WORKS 외부 API 호출 시작/종료와 소요시간 - 로그는 성공/실패 모두 같은 요청 흐름에서 이어서 대조 가능해야 한다. ### 3. `expires_at` 정합성 위험을 코드와 데이터 기준으로 판단한다 - 현재 `naverworks_token.expires_at`가 `timestamp without time zone`인 상태를 전제로, 실제 만료 판단이 어떤 시간대 기준으로 이뤄지는지 명시한다. - `skill-email`의 만료 비교 시각과 DB 기록 시각이 같은 시간대 기준으로 비교되도록 정리한다. - 선택지는 둘 중 하나로 고정한다. - 컬럼 타입을 `timestamptz`로 승격 - 비교 전후를 모두 명시 timezone 기준으로 변환 - 이 단계는 폴백 추가가 아니라, 판단 기준을 하나로 고정하는 작업이어야 한다. ### 4. 재현 검증으로 닫는다 - 정상 케이스: - 현재와 같은 `/messages` 요청이 정상 응답하고 브리핑이 실제 전송되는지 확인한다. - 실패 케이스: - 내부적으로 timeout 또는 비정상 응답을 재현했을 때 스케줄러가 실패로 남는지 확인한다. - 토큰 만료 케이스: - 만료 상태에서 refresh가 정상 수행되면 성공으로 이어지고, refresh 실패 시 원인성 실패로 남는지 확인한다. ## 검증 계획 ### 1. 코드 검증 - `rb8001`에 `return []` 기반 실패 은닉 경로가 남아 있지 않은지 확인한다. - `skill-email` 로그가 단계별로 남는지 확인한다. ### 2. 로그 검증 - 성공 케이스에서는 `메일 0건`과 `조회 성공 0건`이 구분된다. - 실패 케이스에서는 `No emails` 대신 `timeout`, `refresh failed`, `works api failed` 같은 원인 로그가 남는다. - 스케줄러 로그에는 실패 시 `completed successfully`가 남지 않는다. ### 3. 실행 검증 - `rb8001`에서 수동 브리핑 실행 시 정상 메일 조회와 Slack 전송이 통과한다. - `/messages` 단건 호출로 성공/실패 응답 시간이 확인된다. - `auth-server /refresh`와 `skill-email /messages`를 같은 시각대에 대조해 병목 위치를 구분할 수 있다. ## 완료 조건 - 네이버 이메일 조회 실패가 더 이상 `No emails`로 표시되지 않는다. - 9시 브리핑 실패 시 스케줄러와 로그가 모두 실패로 기록된다. - `skill-email` 로그만으로도 `DB/refresh/외부 API` 중 어느 단계에서 지연됐는지 구분 가능하다. - `expires_at` 만료 판단 기준이 시간대 혼선 없이 하나로 설명 가능하다.