Gmail OAuth 토큰 갱신 시스템 트러블슈팅 문서 작성

- 문제 상황 및 해결 과정 정리
- 테이블 구조, OAuth 설정, 자동 갱신 API 문서화
- 주요 이슈 및 해결 방법 기록
- 4시 데모 준비 상태 확인
This commit is contained in:
happybell80 2025-08-23 15:46:19 +09:00
parent b3c7fdf0e2
commit 1c97b904af
2 changed files with 359 additions and 0 deletions

View File

@ -0,0 +1,198 @@
# Gmail OAuth 토큰 갱신 시스템 구축
## 작성일: 2025-08-23
## 작성자: happybell80 with Claude
---
## 1. 문제 상황
### 초기 문제
- Gmail 토큰이 1시간 후 만료되는데 자동 갱신 기능 없음
- 4시 데모를 위해 긴급하게 토큰 갱신 시스템 필요
- 여러 사용자(happybell80, cdctfm, 0914eagle)의 토큰 관리 필요
### 증상
- 토큰 만료 후 Gmail 기능 사용 불가
- 수동으로 재인증 필요
- 사용자별 다른 OAuth 앱 사용 시 관리 어려움
---
## 2. 시스템 분석
### 2.1 테이블 구조 파악
```sql
gmail_tokens 테이블:
- 구조 혼재: 신규 컬럼(access_token, refresh_token)과 기존 컬럼(token_data, oauth_config)
- 두 가지 형식 모두 지원 필요
```
### 2.2 관련 서비스
1. **auth-server**: OAuth 인증 및 토큰 저장
2. **skill-email**: Gmail API 사용
3. **frontend-customer**: 사용자 인터페이스
---
## 3. 해결 과정
### 3.1 Gmail 사용 권한 레벨 조정
```typescript
// frontend-customer/src/components/skills-items-panel.tsx
const gmailItem: GmailItem = {
requiredLevel: 1, // 5 → 1로 변경
capabilities: {
1: '이메일 읽기', // 5 → 1
3: '이메일 보내기', // 7 → 3
5: '실시간 추적' // 11 → 5
}
}
```
### 3.2 OAuth Redirect URI 추가
Google Cloud Console에 추가:
- `https://auth.ro-being.com/auth/gmail/callback`
- `https://auth.ro-being.com/auth/gmail/passport/callback`
### 3.3 Username to UUID 변환 로직
```python
# auth-server/app/providers/gmail_passport.py
async def get_uuid_from_username(username: str, conn) -> str:
row = await conn.fetchrow("""
SELECT id::text FROM users WHERE username = $1
""", username)
if row:
return row['id']
return username # fallback
```
### 3.4 토큰 자동 갱신 API 수정
```python
# auth-server/app/api/gmail_refresh.py
@router.post("/refresh/{user_id}")
async def refresh_gmail_token(user_id: str):
# 1. 새 컬럼 구조와 기존 구조 모두 조회
cur.execute('''
SELECT access_token, refresh_token, expires_at,
token_data, oauth_config, metadata, expiry
FROM gmail_tokens
WHERE user_id = %s::uuid
''', (user_id,))
# 2. 만료 확인 (5분 이상 남았으면 갱신 안함)
if remaining > 300:
return {"status": "valid", ...}
# 3. Google OAuth API로 토큰 갱신
response = requests.post('https://oauth2.googleapis.com/token', ...)
# 4. 새 토큰으로 DB 업데이트
cur.execute('''
UPDATE gmail_tokens
SET access_token = %s, expires_at = %s, ...
''')
```
### 3.5 OAuth Config 저장
```python
# gmail_passport.py에 추가
oauth_config = {
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
"token_uri": "https://oauth2.googleapis.com/token",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"redirect_uris": [REDIRECT_URI]
}
# INSERT 쿼리에 oauth_config 추가
await conn.execute("""
INSERT INTO gmail_tokens (
..., oauth_config, ...
) VALUES (..., $8, ...)
""", ..., json.dumps(oauth_config), ...)
```
---
## 4. 주요 이슈 및 해결
### 4.1 테이블 컬럼 불일치
**문제**: `token_data` NOT NULL 제약으로 INSERT 실패
**해결**: 51123 서버에서 직접 컬럼 제약 제거
```sql
ALTER TABLE gmail_tokens ALTER COLUMN token_data DROP NOT NULL;
```
### 4.2 UUID 타입 오류
**문제**: `invalid UUID 'happybell80'`
**해결**: username을 UUID로 변환하는 함수 추가
### 4.3 Scopes 타입 오류
**문제**: `expected str, got list`
**해결**: `json.dumps(scopes)`로 JSON 문자열 변환
### 4.4 DB 연결 설정
**문제**: `host.docker.internal` vs 실제 IP
**해결**: Docker 환경에서는 `host.docker.internal` 사용
---
## 5. 최종 결과
### 시스템 구성
1. **자동 갱신 API**: `/api/gmail/refresh/{user_id}`
2. **상태 확인 API**: `/api/gmail/check/{user_id}`
3. **OAuth Config 저장**: 사용자별 다른 OAuth 앱 지원
### 사용자별 상태 (4시 데모 준비)
| 사용자 | Token | 만료 시간 | OAuth App |
|--------|-------|-----------|-----------|
| happybell80 | ✅ | 16:28 | 1044056... |
| cdctfm | ✅ | 16:28 | 1044056... |
| 0914eagle | ✅ | 9시간+ | 3191622... |
---
## 6. 교훈
### 테이블 설계
- 마이그레이션 시 신구 컬럼 호환성 고려
- NOT NULL 제약은 신중하게 설정
### OAuth 구현
- Client ID/Secret은 사용자별로 다를 수 있음
- Refresh Token 관리가 핵심
- 토큰 갱신 로직은 5분 전부터 시작
### 디버깅 체인
1. 프론트엔드 요청 확인
2. Gateway 라우팅 확인
3. Auth 서버 처리 확인
4. DB 쿼리 및 데이터 확인
### 협업
- 서버 접근 권한이 다를 때 역할 분담 중요
- 로컬 개발자는 코드 수정, 서버 관리자는 DB/환경 설정
---
## 7. 참고 파일
- `auth-server/app/providers/gmail_passport.py`
- `auth-server/app/api/gmail_refresh.py`
- `frontend-customer/src/components/skills-items-panel.tsx`
- `skill-email/services/db_credentials_provider.py`
---
## 8. 향후 개선사항
1. **자동 갱신 스케줄러**: 만료 5분 전 자동 실행
2. **토큰 상태 모니터링**: 대시보드에서 실시간 확인
3. **에러 알림**: 갱신 실패 시 Slack 알림
4. **다중 OAuth 앱 관리 UI**: 사용자가 직접 OAuth 앱 선택
---
*작성 완료: 2025-08-23 15:45*

View File

@ -0,0 +1,161 @@
# rb8001 레벨 1 표시 문제 해결
## 작성일: 2025-08-23
## 작성자: happybell80 with Claude
---
## 1. 문제 상황
### 증상
- DB에 rb8001의 레벨이 20으로 저장되어 있음
- 프론트엔드에서는 계속 레벨 1로 표시됨
- `/api/stats/rb8001` 요청시 기본값(레벨 1) 반환
### 영향
- 사용자가 로빙의 실제 성장 상태를 확인할 수 없음
- Gmail 아이템 등 레벨 기반 기능 사용 제한
---
## 2. 원인 분석
### 2.1 Gateway의 하드코딩 문제
**위치**: `robeing-gateway/app/main.py:267`
```python
# 문제 코드
response = await http_client.get(
f"http://192.168.219.52:10508/api/stats/{robeing_id}" # 항상 rb10508_micro로!
)
```
**문제점**:
- 모든 stats 요청을 rb10508_micro(포트 10508)로 강제 라우팅
- 사용자별 로빙 무시
### 2.2 프론트엔드 헤더 누락
**위치**: `frontend-customer/src/services/robeing-api.ts:447`
```typescript
// 문제 코드
const response = await fetch(`${ROBEING_API_URL}/api/stats/${robeingId}`);
// X-User-Id 헤더 없음!
```
**문제점**:
- Gateway가 사용자를 식별할 수 없음
- 기본값 반환
### 2.3 잘못된 엔드포인트 경로
**실제 엔드포인트**:
- rb8001: `/stats` (api 없음)
- rb10508_micro: `/stats/{robeing_id}` (api 없음)
**Gateway 요청**:
- `/api/stats/{robeing_id}` → 404 에러
### 2.4 서버 위치 오류
**Gateway 설정**:
- rb8001: `http://localhost:8001` → 51123 서버에 없음
**실제 위치**:
- rb8001: `http://192.168.219.52:8001` (51124 서버)
### 2.5 State Service 연결 실패
**rb8001 환경변수**:
- `STATE_SERVICE_URL` 미설정
- State Service에서 DB 스탯 로드 불가
- 기본값 사용 (각 스탯 10, 총 50 → 레벨 1)
---
## 3. 해결 과정
### 3.1 Gateway 사용자별 라우팅 구현
```python
# robeing-gateway/app/main.py
@app.get("/api/stats/{robeing_id}")
async def get_stats(
robeing_id: str,
x_user_id: Optional[str] = Header(None) # 사용자 식별 추가
):
# 사용자별 로빙 정보 조회
robeing_info = await get_robeing_info(x_user_id)
# 로빙별 올바른 서버로 라우팅
if robeing_id == "rb8001":
target_url = f"http://192.168.219.52:8001/stats"
elif robeing_id == "rb10508_micro":
target_url = f"http://192.168.219.52:10508/stats/{robeing_id}"
```
### 3.2 프론트엔드 헤더 추가
```typescript
// frontend-customer/src/services/robeing-api.ts
const headers: HeadersInit = {
'X-User-Id': userId,
'Authorization': `Bearer ${authToken}`
};
const response = await fetch(`${ROBEING_API_URL}/api/stats/${robeingId}`, {
headers
});
```
### 3.3 응답 형식 정규화
```python
# rb8001 응답 형식 변환
if robeing_id == "rb8001" and "stats" in data:
return {
"robeing_id": robeing_id,
"level": data.get("level", 1),
"experience": stats_data.get("experience", 0),
# ... 표준 형식으로 변환
}
```
---
## 4. 남은 문제
### rb8001의 State Service 미연결
- `STATE_SERVICE_URL` 환경변수 없음
- DB 스탯 로드 실패, 자체 계산 사용
- 실제 DB 레벨(20)과 불일치
**해결 방안**:
1. docker-compose.yml에 STATE_SERVICE_URL 추가
2. 또는 rb8001이 직접 DB 접근하도록 수정
---
## 5. 교훈
### 아키텍처 일관성
- 모든 로빙이 동일한 API 경로 규칙을 따라야 함
- `/api/stats` vs `/stats` 혼용 문제
### 헤더 전달 중요성
- 프론트엔드는 항상 사용자 식별 헤더 포함 필요
- X-User-Id, Authorization 등
### 환경변수 관리
- 필수 서비스 URL은 반드시 설정
- 누락시 fallback 처리 필요
### 디버깅 체인
1. 프론트엔드 요청 확인
2. Gateway 라우팅 확인
3. 실제 서비스 엔드포인트 확인
4. 서비스 내부 로직 확인
---
## 6. 참고 파일
- `robeing-gateway/app/main.py`
- `frontend-customer/src/services/robeing-api.ts`
- `rb8001/main.py`
- `rb8001/app/router/router.py`
- `rb8001/docker-compose.yml`