docs: Gmail 토큰 NULL로 인한 일일 브리핑 실패 해결 가이드 추가
- 매일 9시 브리핑 이메일 수집 실패 문제 (3명 사용자 영향) - 모든 사용자 token_data=NULL 상태 분석 - 단계별 해결 방안 제시: - 즉시: 관리자 대신 OAuth 인증 - 중기: 사용자 재인증 유도 - 장기: 자동 토큰 갱신 구현 - 1시간 내 실행 가능한 상세 가이드 포함
This commit is contained in:
parent
5f2c2c98ca
commit
502f06dd87
313
troubleshooting/250825_gmail_token_null_daily_briefing_fix.md
Normal file
313
troubleshooting/250825_gmail_token_null_daily_briefing_fix.md
Normal file
@ -0,0 +1,313 @@
|
|||||||
|
# Gmail 토큰 NULL로 인한 일일 브리핑 실패 해결 가이드
|
||||||
|
|
||||||
|
## 작성일: 2025-08-25
|
||||||
|
## 작성자: 서버 관리자
|
||||||
|
## 상태: 해결 진행중
|
||||||
|
## 영향: 매일 9시 브리핑 이메일 수집 실패 (사용자 3명)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 문제 상황
|
||||||
|
|
||||||
|
### 1.1 증상
|
||||||
|
- **발생 시간**: 매일 오전 9시 (KST)
|
||||||
|
- **영향 사용자**:
|
||||||
|
- happybell80 (goeun2dc@gmail.com)
|
||||||
|
- 0914eagle (0914eagle@gmail.com)
|
||||||
|
- cdctfm (cdctfm@gmail.com)
|
||||||
|
- **현상**: 이메일 요약 없이 뉴스만 포함된 불완전한 브리핑 전송
|
||||||
|
|
||||||
|
### 1.2 근본 원인
|
||||||
|
```sql
|
||||||
|
-- 현재 DB 상태: 모든 사용자 token_data가 NULL
|
||||||
|
SELECT username, has_token, is_equipped
|
||||||
|
FROM gmail_tokens;
|
||||||
|
|
||||||
|
결과:
|
||||||
|
happybell80 | f (NULL) | t (장착됨) -- 모순 상태
|
||||||
|
0914eagle | f (NULL) | t (장착됨) -- 모순 상태
|
||||||
|
cdctfm | f (NULL) | t (장착됨) -- 모순 상태
|
||||||
|
```
|
||||||
|
|
||||||
|
**문제**: is_equipped=true이지만 실제 token_data=NULL
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 시스템 플로우 분석
|
||||||
|
|
||||||
|
### 2.1 일일 브리핑 플로우
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant Cron as Gateway Cron<br/>(매일 9시)
|
||||||
|
participant RB as rb8001<br/>(51124:8001)
|
||||||
|
participant Email as skill-email<br/>(51124:8501)
|
||||||
|
participant DB as PostgreSQL<br/>(main_db)
|
||||||
|
participant Slack as Slack API
|
||||||
|
|
||||||
|
Cron->>RB: POST /api/cron/daily-summary
|
||||||
|
RB->>Email: GET /messages?user_id={slack_id}
|
||||||
|
Email->>DB: SELECT token_data FROM gmail_tokens
|
||||||
|
DB-->>Email: token_data: NULL ❌
|
||||||
|
Email-->>RB: 500 Internal Server Error
|
||||||
|
RB->>Slack: 불완전한 브리핑 전송<br/>(이메일 없이 뉴스만)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 Gmail OAuth 플로우 (재인증 필요)
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant User as 사용자
|
||||||
|
participant Front as 프론트엔드
|
||||||
|
participant Auth as auth-server<br/>(51123:9000)
|
||||||
|
participant Google as Google OAuth
|
||||||
|
participant DB as PostgreSQL
|
||||||
|
|
||||||
|
User->>Front: Gmail 연결 클릭
|
||||||
|
Front->>Auth: GET /api/gmail/auth?user_id={uuid}
|
||||||
|
Auth->>Auth: OAuth URL 생성
|
||||||
|
Auth-->>Front: Redirect to Google
|
||||||
|
Front->>Google: 사용자 리다이렉트
|
||||||
|
User->>Google: 권한 승인
|
||||||
|
Google->>Auth: Callback with code
|
||||||
|
Auth->>Google: Exchange code for tokens
|
||||||
|
Google-->>Auth: access_token, refresh_token
|
||||||
|
Auth->>DB: UPDATE gmail_tokens<br/>SET token_data = {...}
|
||||||
|
Auth-->>Front: 인증 완료
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 해결 방안
|
||||||
|
|
||||||
|
### 3.1 단기 해결책 (즉시 적용)
|
||||||
|
|
||||||
|
#### 방법 1: 관리자 대신 인증 (추천)
|
||||||
|
```bash
|
||||||
|
# 1. 테스트 계정으로 OAuth 인증 URL 생성
|
||||||
|
curl -X GET "http://localhost:9000/api/gmail/auth?user_id=1e16e9d5-59f3-54da-a661-8abeabff4230"
|
||||||
|
|
||||||
|
# 2. 반환된 URL을 브라우저에서 열어 Google 로그인
|
||||||
|
# 3. 권한 승인 후 콜백 확인
|
||||||
|
|
||||||
|
# 4. 토큰 저장 확인
|
||||||
|
export PGPASSWORD=robeings
|
||||||
|
psql -h localhost -U robeings -d main_db -c \
|
||||||
|
"SELECT user_id, token_data IS NOT NULL FROM gmail_tokens WHERE user_id = '1e16e9d5-59f3-54da-a661-8abeabff4230';"
|
||||||
|
|
||||||
|
# 5. 브리핑 수동 테스트
|
||||||
|
curl -X POST http://192.168.219.52:8001/api/cron/daily-summary \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Authorization: Bearer cron-secret-2024"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 방법 2: 임시 토큰 주입 (비추천, 테스트용)
|
||||||
|
```sql
|
||||||
|
-- 주의: 실제 유효한 토큰이 필요함
|
||||||
|
UPDATE gmail_tokens
|
||||||
|
SET token_data = '{
|
||||||
|
"access_token": "실제_액세스_토큰",
|
||||||
|
"refresh_token": "실제_리프레시_토큰",
|
||||||
|
"token_type": "Bearer",
|
||||||
|
"expires_in": 3599
|
||||||
|
}'::jsonb,
|
||||||
|
expiry = NOW() + INTERVAL '1 hour'
|
||||||
|
WHERE user_id = '1e16e9d5-59f3-54da-a661-8abeabff4230';
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 중기 해결책 (사용자 재인증 유도)
|
||||||
|
|
||||||
|
#### 프론트엔드에 알림 추가
|
||||||
|
```javascript
|
||||||
|
// 로그인 시 Gmail 토큰 체크
|
||||||
|
useEffect(() => {
|
||||||
|
checkGmailToken().then(hasToken => {
|
||||||
|
if (!hasToken) {
|
||||||
|
showNotification({
|
||||||
|
title: "Gmail 재인증 필요",
|
||||||
|
message: "일일 브리핑을 위해 Gmail을 다시 연결해주세요",
|
||||||
|
action: "연결하기",
|
||||||
|
onClick: () => window.location.href = '/settings/gmail'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Slack 메시지로 재인증 안내
|
||||||
|
```python
|
||||||
|
# rb8001의 dm_skill.py에 추가
|
||||||
|
async def notify_reauth_needed(user_slack_id: str):
|
||||||
|
await slack_client.chat_postMessage(
|
||||||
|
channel=user_slack_id,
|
||||||
|
text="🔔 Gmail 재인증이 필요합니다",
|
||||||
|
blocks=[{
|
||||||
|
"type": "section",
|
||||||
|
"text": {
|
||||||
|
"type": "mrkdwn",
|
||||||
|
"text": "일일 브리핑을 위해 Gmail을 다시 연결해주세요:\nhttps://ro-being.com/settings/gmail"
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 장기 해결책 (자동 복구)
|
||||||
|
|
||||||
|
#### 토큰 자동 갱신 구현
|
||||||
|
```python
|
||||||
|
# skill-email 서비스에 추가
|
||||||
|
async def auto_refresh_token(user_id: str):
|
||||||
|
"""만료된 토큰 자동 갱신"""
|
||||||
|
try:
|
||||||
|
# 1. refresh_token으로 새 access_token 획득
|
||||||
|
response = await httpx.post(
|
||||||
|
f"http://auth-server:9000/api/gmail/refresh/{user_id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
logger.info(f"Token refreshed for {user_id}")
|
||||||
|
return True
|
||||||
|
elif response.status_code == 401:
|
||||||
|
# refresh_token도 만료 → 재인증 필요
|
||||||
|
await notify_reauth_needed(user_id)
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Token refresh failed: {e}")
|
||||||
|
return False
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 실행 단계별 가이드
|
||||||
|
|
||||||
|
### Step 1: 현재 상태 확인 (5분)
|
||||||
|
```bash
|
||||||
|
# 1. 토큰 상태 확인
|
||||||
|
export PGPASSWORD=robeings
|
||||||
|
psql -h localhost -U robeings -d main_db -c \
|
||||||
|
"SELECT u.username, gt.token_data IS NOT NULL as has_token,
|
||||||
|
gt.is_equipped, gt.metadata->>'email' as email
|
||||||
|
FROM users u
|
||||||
|
LEFT JOIN gmail_tokens gt ON u.id = gt.user_id
|
||||||
|
WHERE u.username IN ('happybell80', '0914eagle', 'cdctfm');"
|
||||||
|
|
||||||
|
# 2. 크론잡 확인
|
||||||
|
docker exec robeing-gateway crontab -l | grep daily
|
||||||
|
|
||||||
|
# 3. 최근 실패 로그 확인
|
||||||
|
ssh admin@192.168.219.52 "docker logs rb8001 --tail 100 | grep -E 'daily-summary|gmail|500'" 2>/dev/null || echo "직접 확인 필요"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: 테스트 계정 인증 (15분)
|
||||||
|
```bash
|
||||||
|
# 1. OAuth URL 생성 (happybell80 계정)
|
||||||
|
curl -s "http://localhost:9000/api/gmail/auth?user_id=1e16e9d5-59f3-54da-a661-8abeabff4230" | jq -r '.auth_url'
|
||||||
|
|
||||||
|
# 2. 브라우저에서 URL 열고 Google 계정으로 로그인
|
||||||
|
# 3. 권한 승인 (이메일 읽기, 쓰기, 수정)
|
||||||
|
|
||||||
|
# 4. 인증 성공 확인
|
||||||
|
psql -h localhost -U robeings -d main_db -c \
|
||||||
|
"SELECT token_data->'access_token' IS NOT NULL as success
|
||||||
|
FROM gmail_tokens
|
||||||
|
WHERE user_id = '1e16e9d5-59f3-54da-a661-8abeabff4230';"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: 브리핑 테스트 (10분)
|
||||||
|
```bash
|
||||||
|
# 1. 수동 브리핑 실행
|
||||||
|
curl -X POST http://192.168.219.52:8001/api/cron/daily-summary \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Authorization: Bearer cron-secret-2024"
|
||||||
|
|
||||||
|
# 2. 실시간 로그 모니터링
|
||||||
|
docker logs robeing-gateway -f 2>&1 | grep -E "daily|gmail|email"
|
||||||
|
|
||||||
|
# 3. Slack DM 확인
|
||||||
|
# happybell80 사용자의 Slack DM에 브리핑이 도착했는지 확인
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 4: 다른 사용자 처리 (20분)
|
||||||
|
```bash
|
||||||
|
# 0914eagle 계정 인증
|
||||||
|
curl -s "http://localhost:9000/api/gmail/auth?user_id=b6ea2ee0-a15a-5cf4-93a9-a9ca20d4c4a0" | jq -r '.auth_url'
|
||||||
|
|
||||||
|
# cdctfm 계정 인증
|
||||||
|
curl -s "http://localhost:9000/api/gmail/auth?user_id=69ae4ea9-a15f-5110-9f5d-6568e380fcfb" | jq -r '.auth_url'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 5: 모니터링 설정 (10분)
|
||||||
|
```bash
|
||||||
|
# 매일 8시 50분 토큰 상태 체크 크론 추가
|
||||||
|
cat << 'EOF' > /tmp/check_gmail_tokens.sh
|
||||||
|
#!/bin/bash
|
||||||
|
PGPASSWORD=robeings psql -h localhost -U robeings -d main_db -c \
|
||||||
|
"SELECT username, token_data IS NOT NULL as has_token
|
||||||
|
FROM users u
|
||||||
|
JOIN gmail_tokens gt ON u.id = gt.user_id;" | \
|
||||||
|
mail -s "Gmail Token Status" admin@ro-being.com
|
||||||
|
EOF
|
||||||
|
|
||||||
|
chmod +x /tmp/check_gmail_tokens.sh
|
||||||
|
|
||||||
|
# 크론잡 등록 (선택사항)
|
||||||
|
echo "50 8 * * * /tmp/check_gmail_tokens.sh" | crontab -l | crontab -
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 검증 체크리스트
|
||||||
|
|
||||||
|
- [ ] 모든 사용자의 token_data NOT NULL 확인
|
||||||
|
- [ ] 수동 브리핑 실행 시 이메일 포함 확인
|
||||||
|
- [ ] Slack DM으로 완전한 브리핑 수신 확인
|
||||||
|
- [ ] 내일 9시 자동 실행 모니터링 예약
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 관련 파일 및 참고 문서
|
||||||
|
|
||||||
|
### 핵심 파일
|
||||||
|
- **auth-server**: `/home/admin/auth-server/app/providers/gmail_passport.py`
|
||||||
|
- **게이트웨이 크론**: `/home/admin/robeing-gateway/app/crontab.py`
|
||||||
|
- **rb8001 브리핑**: `rb8001/app/skills/dm_skill.py:384` (51124 서버)
|
||||||
|
- **skill-email**: `skill-email/main.py` (51124 서버)
|
||||||
|
|
||||||
|
### 참고 문서
|
||||||
|
- [일일 브리핑 시퀀스 다이어그램](/home/admin/DOCS/300_architecture/sequences/daily_briefing_sequences.md)
|
||||||
|
- [Gmail 토큰 자동 갱신](/home/admin/DOCS/troubleshooting/250821_gmail_token_auto_refresh.md)
|
||||||
|
- [rb8001 크론 실패 분석](/home/admin/DOCS/troubleshooting/250824_rb8001_daily_summary_cron_failure.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 주의사항
|
||||||
|
|
||||||
|
### ⚠️ 보안 관련
|
||||||
|
- 실제 토큰을 로그에 출력하지 말 것
|
||||||
|
- refresh_token은 절대 노출하지 말 것
|
||||||
|
- 테스트 후 불필요한 토큰은 삭제
|
||||||
|
|
||||||
|
### ⚠️ 운영 관련
|
||||||
|
- 프로덕션 사용자의 토큰 직접 수정 지양
|
||||||
|
- 사용자에게 재인증 안내 우선
|
||||||
|
- 크론 실행 시간(9시) 피해서 테스트
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 문제 재발 방지
|
||||||
|
|
||||||
|
### 단기 조치
|
||||||
|
1. 토큰 만료 알림 시스템 구축
|
||||||
|
2. 관리자 대시보드에 토큰 상태 표시
|
||||||
|
|
||||||
|
### 장기 조치
|
||||||
|
1. refresh_token 자동 갱신 로직 구현
|
||||||
|
2. 토큰 만료 7일 전 사용자 알림
|
||||||
|
3. 프론트엔드 Gmail 연결 상태 표시 개선
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 예상 소요 시간: 1시간
|
||||||
|
|
||||||
|
- Step 1-2: 20분 (기본 인증)
|
||||||
|
- Step 3: 10분 (테스트)
|
||||||
|
- Step 4: 20분 (추가 사용자)
|
||||||
|
- Step 5: 10분 (모니터링)
|
||||||
Loading…
x
Reference in New Issue
Block a user