diff --git a/troubleshooting/250822_happybell80_Gmail통합_UUID변환_문제해결.md b/troubleshooting/250822_happybell80_Gmail통합_UUID변환_문제해결.md new file mode 100644 index 0000000..723f251 --- /dev/null +++ b/troubleshooting/250822_happybell80_Gmail통합_UUID변환_문제해결.md @@ -0,0 +1,331 @@ +# Gmail 통합 시스템 UUID 변환 문제 해결 + +## 날짜: 2025-08-22 +## 작성자: happybell80 & Claude +## 관련 서비스: robeing-monitor, robeing-gateway, skill-email + +--- + +## 1. 문제 상황 요약 + +### 초기 발견 +- 프론트엔드에서 Gmail 아이템 조회 시 빈 결과 반환 +- robeing-monitor API 직접 호출은 정상 작동 +- Gateway를 통한 호출만 실패 + +### 근본 원인 +시스템 전반에 걸친 사용자 ID 체계 불일치: +- **Frontend**: JWT에 username 저장 (예: "happybell80") +- **Slack**: Slack user_id 사용 (예: "U091UNVE41M") +- **PostgreSQL**: UUID 저장 (예: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") + +--- + +## 2. 문제 분석 + +### 2.1 robeing-monitor UUID 변환 누락 + +#### 증상 +``` +asyncpg.exceptions.DataError: invalid input for query argument $1: 'U091UNVE41M' +(invalid UUID 'U091UNVE41M': length must be between 32..36 characters, got 11) +``` + +#### 원인 +- robeing-monitor가 Slack user_id를 UUID로 변환하지 않고 직접 DB 조회 +- PostgreSQL gmail_tokens 테이블의 user_id 컬럼은 UUID 타입 + +### 2.2 Gateway 프록시 문제 + +#### 증상 +- Gateway 통과 시 X-User-Id 헤더가 username으로 전달 +- robeing-monitor는 UUID 기대하지만 username 수신 + +#### 원인 +- JWT payload에 username만 있고 UUID 없음 +- Gateway가 username을 UUID로 변환하는 로직 부재 + +### 2.3 skill-email 타입 불일치 + +#### 증상 +```python +AttributeError: 'Credentials' object has no attribute 'value' +``` + +#### 원인 +- FileCredentialsProvider: `Result[Credentials, EmailError]` 반환 +- DBCredentialsProvider: `Optional[Credentials]` 반환 +- GmailService가 Result 타입만 처리 + +--- + +## 3. 해결 과정 + +### 3.1 Phase 1: robeing-monitor UUID 변환 추가 + +#### 수정 파일: `robeing-monitor/app/api/items.py` + +```python +# UUID 변환 함수 추가 +def convert_to_uuid(user_id: str) -> str: + """Slack user_id를 UUID로 변환""" + try: + uuid.UUID(user_id) # 이미 UUID인지 확인 + return user_id + except ValueError: + pass + + # UUID5로 변환 (DNS namespace 사용) + namespace = uuid.UUID('6ba7b810-9dad-11d1-80b4-00c04fd430c8') + return str(uuid.uuid5(namespace, user_id)) + +# 모든 엔드포인트에 적용 +@router.get("/gmail") +async def get_gmail_items(x_user_id: Optional[str] = Header(None)): + user_uuid = convert_to_uuid(x_user_id) # 변환 추가 + # ... DB 조회 +``` + +#### 테스트 결과 +- U091UNVE41M → b6ea2ee0-a15a-5cf4-93a9-a9ca20d4c4a0 ✅ +- test-user → a5f4fe64-dd02-5776-b5f9-59fccda849ee ✅ + +### 3.2 Phase 2: Gateway username → UUID 변환 + +#### 수정 파일 1: `robeing-gateway/app/database.py` + +```python +async def get_user_by_username(username: str) -> Optional[Dict[str, Any]]: + """username으로 사용자 정보 및 UUID 조회""" + async with AsyncSessionLocal() as session: + query = text(""" + SELECT + id::text as user_id, + username, + email, + name + FROM users + WHERE username = :username + LIMIT 1 + """) + result = await session.execute(query, {"username": username}) + row = result.fetchone() + + if row: + return { + 'user_id': row.user_id, # UUID + 'username': row.username, + 'email': row.email, + 'name': row.name + } + return None +``` + +#### 수정 파일 2: `robeing-gateway/app/main.py` + +```python +@app.api_route("/api/items/{path:path}", methods=["GET", "POST", "DELETE"]) +async def items_proxy( + path: str, + request: Request, + x_user_id: str = Depends(get_verified_user) +): + monitor_url = os.getenv("MONITOR_URL", "http://192.168.219.52:9024") + + # username을 UUID로 변환 + from app.database import get_user_by_username + user_uuid = x_user_id + + if x_user_id and x_user_id != "default": + user_info = await get_user_by_username(x_user_id) + if user_info: + user_uuid = user_info['user_id'] # UUID 사용 + logger.info(f"Username to UUID: {x_user_id} → {user_uuid}") + + # robeing-monitor에 UUID 전달 + async with httpx.AsyncClient() as client: + response = await client.request( + method=request.method, + url=f"{monitor_url}/api/items/{path}", + headers={"X-User-Id": user_uuid}, + content=await request.body() if request.method != "GET" else None + ) +``` + +#### 문제 발견 및 수정 +- 초기 오류: `column "display_name" does not exist` +- 원인: PostgreSQL users 테이블에는 `name` 컬럼 존재 +- 해결: `display_name` → `name` 변경 + +### 3.3 Phase 3: skill-email UUID 변환 및 타입 호환성 + +#### 수정 파일 1: `skill-email/services/db_credentials_provider.py` + +```python +class DBCredentialsProvider: + def __init__(self, connection_string: str): + self.connection_string = connection_string + self.connection_pool = None + # RFC 4122 DNS namespace - 모든 서비스 동일 사용 + self.uuid_namespace = uuid.NAMESPACE_DNS + self._init_pool() + + def _convert_to_uuid(self, user_id: str) -> str: + """Slack user_id를 UUID로 변환""" + try: + uuid.UUID(user_id) + return user_id + except ValueError: + pass + + user_uuid = str(uuid.uuid5(self.uuid_namespace, user_id)) + logger.debug(f"Converted {user_id} to {user_uuid}") + return user_uuid + + def get_credentials(self, user_id: str) -> Optional[Credentials]: + # Slack user_id를 UUID로 변환 + user_uuid = self._convert_to_uuid(user_id) + # ... DB 조회 +``` + +#### 수정 파일 2: `skill-email/services/gmail_service.py` + +```python +def _get_gmail_service(self, user_id: str) -> Result[Any, EmailError]: + # Credentials 가져오기 + creds_result = self.creds_provider.get_credentials(user_id) + + # DBCredentialsProvider는 Optional[Credentials] 반환 + if creds_result is None: + return Err(EmailError(f"No credentials found for user: {user_id}")) + + # Result 타입인지 확인 (FileCredentialsProvider) + if hasattr(creds_result, 'value'): + if isinstance(creds_result, Err): + return creds_result + creds = creds_result.value + else: + # DBCredentialsProvider - 직접 Credentials 반환 + creds = creds_result + + try: + service = build("gmail", "v1", credentials=creds, cache_discovery=False) + self._service_cache[user_id] = service + return Ok(service) + except Exception as e: + return Err(EmailError(f"Gmail 서비스 생성 실패: {str(e)}", e)) +``` + +#### 환경변수 설정 +```yaml +# docker-compose.yml +environment: + - TOKEN_PROVIDER=database + - POSTGRES_CONNECTION_STRING=${POSTGRES_CONNECTION_STRING:-postgresql://robeings:robeings@192.168.219.45:5432/main_db} +``` + +--- + +## 4. 최종 아키텍처 + +### 4.1 UUID 변환 체인 + +``` +Frontend (JWT) Gateway robeing-monitor PostgreSQL +username: "happybell80" → DB 조회 → UUID → UUID로 조회 → Gmail 아이템 + +Slack (rb8001) skill-email PostgreSQL +user_id: "U091UNVE41M" → UUID 변환 → UUID → 토큰 조회 +``` + +### 4.2 서비스별 UUID 변환 구현 + +| 서비스 | 함수/메서드 | 입력 | 출력 | +|--------|------------|------|------| +| robeing-gateway | `get_user_by_username()` | username | UUID | +| robeing-monitor | `convert_to_uuid()` | Slack ID/username | UUID | +| skill-email | `_convert_to_uuid()` | Slack ID | UUID | + +### 4.3 표준 UUID 생성 규칙 + +```python +# 모든 서비스 동일 +namespace = uuid.NAMESPACE_DNS # 6ba7b810-9dad-11d1-80b4-00c04fd430c8 +user_uuid = str(uuid.uuid5(namespace, user_id)) +``` + +--- + +## 5. 테스트 결과 + +### 5.1 UUID 변환 테스트 +```python +# 테스트 케이스 +U091UNVE41M → b6ea2ee0-a15a-5cf4-93a9-a9ca20d4c4a0 ✅ +test-user → a5f4fe64-dd02-5776-b5f9-59fccda849ee ✅ +happybell80 → aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa ✅ +``` + +### 5.2 시스템 통합 테스트 +- ✅ Frontend → Gateway → robeing-monitor: Gmail 아이템 조회 성공 +- ✅ Slack → rb8001 → skill-email: 이메일 처리 요청 수신 +- ⚠️ Gmail OAuth 토큰 만료로 실제 발송은 실패 (재인증 필요) + +--- + +## 6. 교훈 + +### 6.1 ID 체계 설계의 중요성 +- **문제**: 서비스마다 다른 ID 체계 사용 +- **해결**: 중앙화된 UUID 변환 로직 +- **교훈**: 초기 설계 시 통일된 ID 체계 필요 + +### 6.2 타입 시스템의 가치 +- **문제**: Provider 인터페이스 불일치 +- **해결**: 런타임 타입 체크로 호환성 확보 +- **교훈**: TypeScript/타입 힌트 적극 활용 필요 + +### 6.3 디버깅 전략 +- **효과적**: 각 서비스 개별 테스트 후 통합 +- **핵심**: 로그를 통한 데이터 흐름 추적 +- **도구**: curl 직접 테스트로 문제 격리 + +### 6.4 협업의 중요성 +- 51123 서버팀: 실시간 테스트 및 로그 확인 +- 51124 서버팀: 서비스 재시작 및 배포 +- 로컬 개발: 코드 수정 및 Git 푸시 + +--- + +## 7. 향후 과제 + +### 즉시 필요 +- [ ] Gmail OAuth 토큰 자동 갱신 구현 +- [ ] rb10508_micro 이메일 처리 완성 +- [ ] robeing.contacts 테이블 활용 + +### 중기 개선 +- [ ] 타입 정의 통일 (Result vs Optional) +- [ ] ID 변환 미들웨어 중앙화 +- [ ] 통합 테스트 자동화 + +--- + +## 8. 관련 파일 + +### 수정된 파일들 +- `robeing-monitor/app/api/items.py` +- `robeing-gateway/app/database.py` +- `robeing-gateway/app/main.py` +- `skill-email/services/db_credentials_provider.py` +- `skill-email/services/gmail_service.py` +- `skill-email/docker-compose.yml` + +### Git 커밋 +- robeing-monitor: `50ee489` UUID 변환 로직 추가 +- robeing-gateway: `0335f68` username → UUID 변환 +- skill-email: `d32a1ab` 타입 호환성 및 UUID 변환 + +--- + +**문서 끝** \ No newline at end of file