Gmail OAuth 토큰 갱신 시스템 트러블슈팅 문서 작성
- 문제 상황 및 해결 과정 정리 - 테이블 구조, OAuth 설정, 자동 갱신 API 문서화 - 주요 이슈 및 해결 방법 기록 - 4시 데모 준비 상태 확인
This commit is contained in:
parent
b3c7fdf0e2
commit
1c97b904af
198
troubleshooting/250823_happybell80_Gmail_OAuth_토큰_갱신_시스템_구축.md
Normal file
198
troubleshooting/250823_happybell80_Gmail_OAuth_토큰_갱신_시스템_구축.md
Normal 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*
|
||||
161
troubleshooting/250823_happybell80_rb8001_레벨1_표시_문제.md
Normal file
161
troubleshooting/250823_happybell80_rb8001_레벨1_표시_문제.md
Normal 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`
|
||||
Loading…
x
Reference in New Issue
Block a user