From 9eaa83a76e9e1987d024654632cc32800929b178 Mon Sep 17 00:00:00 2001 From: happybell80 Date: Fri, 22 Aug 2025 19:52:23 +0900 Subject: [PATCH] =?UTF-8?q?=EC=95=84=ED=82=A4=ED=85=8D=EC=B2=98=20?= =?UTF-8?q?=EB=AC=B8=EC=84=9C=20=EB=8C=80=EA=B7=9C=EB=AA=A8=20=EC=97=85?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=8A=B8:=20JWT/UUID=20=EB=B3=80=ED=99=98=20?= =?UTF-8?q?=EC=B2=B4=EA=B3=84=20=EC=A0=95=EB=A6=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - JWT 검증 플로우: Gateway 내부 처리로 변경 - Username → UUID 변환 메커니즘 문서화 - UUID5 체계: Slack 사용자용 결정적 UUID 생성 - Gateway 프록시 패턴 상세 문서화 - 데이터베이스: gmail_tokens, robeing 스키마 추가 - 서비스 포트 매핑 및 역할 명확화 - auth_db → main_db 마이그레이션 반영 --- ..._컨테이너와_마이크로서비스.md | 125 +++-- ...320_Slack_기반_인터페이스_설계.md | 351 ++++++++++--- 300_architecture/database/tables.md | 55 +- 300_architecture/gateway_proxy_patterns.md | 489 ++++++++++++++++++ .../sequences/auth_login_sequences.md | 62 ++- 300_architecture/sequences/email_sequences.md | 59 ++- 300_architecture/uuid_conversion_system.md | 386 ++++++++++++++ 7 files changed, 1385 insertions(+), 142 deletions(-) create mode 100644 300_architecture/gateway_proxy_patterns.md create mode 100644 300_architecture/uuid_conversion_system.md diff --git a/300_architecture/310_전체_시스템_구조_컨테이너와_마이크로서비스.md b/300_architecture/310_전체_시스템_구조_컨테이너와_마이크로서비스.md index 2e22e81..93cb802 100644 --- a/300_architecture/310_전체_시스템_구조_컨테이너와_마이크로서비스.md +++ b/300_architecture/310_전체_시스템_구조_컨테이너와_마이크로서비스.md @@ -23,29 +23,42 @@ ## 전체 아키텍처 +### 서버 구성 +| 서버 | IP | 역할 | 주요 서비스 | +|------|-----|------|------------| +| 51123 | 192.168.219.45 | 메인 서버 | Gitea, nginx, auth-server, PostgreSQL | +| 51124 | 192.168.219.52 | 로빙/스킬 서버 | rb8001, rb10508, skill-email, ChromaDB | + ### 기본 구조 ``` ┌─────────────────────────────────────┐ -│ 대시보드 서버 (1개) │ +│ 51123 서버 (메인 서버) │ │ ┌─────────────────────────────┐ │ -│ │ 웹 인터페이스 │ │ -│ │ 사용자 A 로그인 → A 로빙 │ │ -│ │ 사용자 B 로그인 → B 로빙 │ │ +│ │ 프론트엔드 (3000) │ │ +│ │ Gateway (8100) │ │ +│ │ auth-server (9000) │ │ +│ │ robeing-monitor (9024) │ │ │ └─────────────────────────────┘ │ │ ┌─────────────────────────────┐ │ -│ │ 공통 DB │ │ -│ │ users, robeings, stats │ │ +│ │ PostgreSQL (5432) │ │ +│ │ main_db (구 auth_db) │ │ │ └─────────────────────────────┘ │ └─────────────────────────────────────┘ │ - │ API 호출 + │ SSH/API 통신 ▼ -┌─────────────────┐ ┌─────────────────┐ -│ 로빙 A 컨테이너 │ │ 로빙 B 컨테이너 │ -│ (2GB 메모리) │ │ (8GB 메모리) │ -│ 스탯: 초보 │ │ 스탯: 고급 │ -│ 스킬: 3개 │ │ 스킬: 15개 │ -└─────────────────┘ └─────────────────┘ +┌─────────────────────────────────────┐ +│ 51124 서버 (로빙/스킬) │ +│ ┌─────────────────────────────┐ │ +│ │ rb8001 (8001) │ │ +│ │ rb10508_micro (10508) │ │ +│ │ skill-email (8501) │ │ +│ │ skill-news (8502) │ │ +│ └─────────────────────────────┘ │ +│ ┌─────────────────────────────┐ │ +│ │ ChromaDB (8000) │ │ +│ └─────────────────────────────┘ │ +└─────────────────────────────────────┘ ``` ### 서비스 모델 @@ -72,19 +85,23 @@ ## 데이터 구조 -### 대시보드 DB (공통) +### main_db (PostgreSQL - 51123) ```sql -- 사용자 정보 -users: id, name, email, created_at +users: id(UUID), username, email, name, created_at --- 로빙 메타데이터 -robeings: id, user_id, name, level, stats, container_id, status +-- Gmail 토큰 (아이템) +gmail_tokens: id, user_id, email, is_equipped, equipped_to --- 스킬 및 아이템 설정 -skills: id, robeing_id, skill_type, config, enabled +-- Gmail 감사 로그 +gmail_audit_logs: id, user_id, robeing_id, action, created_at --- 성능 통계 -performance: id, robeing_id, date, tasks_completed, success_rate +-- 로빙 통계 +robeing_stats: id, user_id, robeing_id, level, experience + +-- 로빙 전용 스키마 +robeing.contacts: id, robeing_id, name, email, phone +robeing.conversations: id, robeing_id, user_message, robeing_response ``` ### 로빙 컨테이너 DB (개별) @@ -110,17 +127,39 @@ performance: id, robeing_id, date, tasks_completed, success_rate 설정 전달 업데이트 ``` -### API 엔드포인트 (API = 서비스끼리 대화하는 방법) -``` -대시보드 → 로빙: -- POST /api/config/skills (스킬 설정 전달) -- POST /api/config/stats (스탯 조정) -- GET /api/status (현재 상태 확인) +### 서비스 포트 매핑 -로빙 → 대시보드: -- POST /dashboard/api/stats (성장 상태 업데이트) -- POST /dashboard/api/performance (작업 성과 보고) -- POST /dashboard/api/events (중요 이벤트 기록) +#### 51123 서버 포트 +| 포트 | 서비스 | 용도 | +|------|--------|------| +| 3000 | Gitea | Git 저장소 | +| 3001 | frontend-base | 프론트엔드 | +| 8100 | robeing-gateway | API Gateway (프록시) | +| 9000 | auth-server | OAuth 인증 | +| 9024 | robeing-monitor | Gmail 아이템 관리 | +| 5432 | PostgreSQL | main_db | + +#### 51124 서버 포트 +| 포트 | 서비스 | 용도 | +|------|--------|------| +| 8001 | rb8001 | 프로덕션 로빙 | +| 10508 | rb10508_micro | 테스트 로빙 | +| 10408 | rb10408_test | 개발 로빙 | +| 8501 | skill-email | Gmail 스킬 | +| 8502 | skill-news | 뉴스 스킬 | +| 8000 | ChromaDB | 벡터 DB | + +### API 엔드포인트 +``` +Gateway → 로빙: +- POST /api/robeing/chat (대화 요청) +- POST /api/robeing/email/send (이메일 발송) +- GET /api/items/gmail/status (아이템 상태) + +로빙 → Gateway: +- Headers: X-User-Id (UUID) +- POST /api/stats/update (경험치 업데이트) +- POST /api/events/log (이벤트 기록) ``` ## 코드로 보는 구조 @@ -179,6 +218,25 @@ class RobeingContainer: 고급 로빙: 4CPU, 8GB RAM, 50GB Disk ``` +## Gateway 프록시 패턴 + +### JWT 인증 및 UUID 변환 +``` +사용자 요청 (JWT with username) + ↓ +Gateway (8100) + ├─ JWT 검증 (내부) + ├─ username → UUID 변환 (DB 조회) + └─ X-User-Id 헤더 추가 + ↓ +백엔드 서비스 (UUID 기반 처리) +``` + +### UUID 체계 +- **일반 사용자**: uuid4() 랜덤 생성 +- **Slack 사용자**: uuid5(namespace, slack_id) 결정적 생성 +- **Namespace**: 6ba7b810-9dad-11d1-80b4-00c04fd430c8 + ## 리소스 효율성 관리 ### 수면/각성 시스템 @@ -275,6 +333,7 @@ class RobeingContainer: --- **문서 작성일**: 2025-07-05 +**업데이트**: 2025-08-21 **작성자**: 로빙 개발팀 -**버전**: 1.0 -**상태**: 설계 완료, 구현 준비 중 \ No newline at end of file +**버전**: 2.0 +**상태**: 구현 중 \ No newline at end of file diff --git a/300_architecture/320_Slack_기반_인터페이스_설계.md b/300_architecture/320_Slack_기반_인터페이스_설계.md index b46d754..2017a63 100644 --- a/300_architecture/320_Slack_기반_인터페이스_설계.md +++ b/300_architecture/320_Slack_기반_인터페이스_설계.md @@ -1,95 +1,300 @@ -# 🎯 Slack 앱 최종 설정 가이드 +# Slack 기반 인터페이스 아키텍처 -## ✅ 현재 상태 -- ngrok 터널: `https://dc5c-59-9-195-150.ngrok-free.app` -- FastAPI 서버: 실행 중 -- URL 검증: 성공 -- 이벤트 처리: 동작 확인 +## 작성일: 2025-08-21 +## 작성자: Claude (51123 서버 관리자) -## 📋 Slack 앱 설정 단계 +--- -### 1. Event Subscriptions 설정 -1. https://api.slack.com/apps 접속 -2. 로빙 앱 선택 -3. **"Event Subscriptions"** 클릭 -4. **"Enable Events"** 토글 ON -5. **Request URL** 입력: - ``` - https://dc5c-59-9-195-150.ngrok-free.app/api/slack/events - ``` -6. ✅ **"Verified"** 표시 확인 +## 1. 개요 -### 2. Bot Events 구독 -**"Subscribe to bot events"** 섹션에서 다음 이벤트 추가: -- `app_mention` - 봇 멘션시 알림 -- `message.channels` - 채널 메시지 -- `message.groups` - 그룹 메시지 -- `message.im` - 직접 메시지 -- `message.mpim` - 멀티파티 DM +Slack을 통한 로빙 서비스 접근 아키텍처. 실시간 대화형 인터페이스를 제공하며, UUID5 기반 사용자 식별 체계를 사용합니다. -### 3. OAuth & Permissions 확인 -다음 권한이 있는지 확인: -- `app_mentions:read` -- `channels:read` -- `chat:write` -- `chat:write.public` -- `im:read` -- `im:write` -- `users:read` +--- -### 4. 앱 재설치 -설정 변경 후: -1. **"Save Changes"** 클릭 -2. **"Install to Workspace"** 또는 **"Reinstall App"** -3. 권한 승인 +## 2. 시스템 구조 -## 🧪 테스트 방법 +### 2.1 전체 플로우 -### 1. 봇 초대 -``` -/invite @Roving +```mermaid +sequenceDiagram + participant User as Slack 사용자 + participant Slack as Slack API + participant RB as rb10508_micro + participant Gateway as Gateway(8100) + participant Auth as auth-server(9000) + participant DB as PostgreSQL + participant Skill as 스킬 서비스 + + User->>Slack: 메시지 전송 + Slack->>RB: Event Webhook + Note over RB: Slack User ID: U0925SXQFDK + + RB->>RB: UUID5 생성 + Note over RB: uuid5(namespace, slack_id) + + RB->>DB: 사용자 조회 (UUID5) + DB-->>RB: 사용자 정보 + + RB->>RB: 의도 분류 + RB->>Skill: 필요시 스킬 호출 + Skill-->>RB: 처리 결과 + + RB->>Slack: 응답 전송 + Slack-->>User: 메시지 표시 ``` -### 2. 직접 메시지 -``` -안녕하세요 로빙! +### 2.2 사용자 식별 체계 + +#### UUID5 변환 로직 +```python +import uuid + +NAMESPACE = uuid.UUID('6ba7b810-9dad-11d1-80b4-00c04fd430c8') + +def get_user_uuid(slack_user_id: str) -> str: + """Slack User ID를 UUID5로 변환""" + return str(uuid.uuid5(NAMESPACE, slack_user_id)) + +# 예시 +slack_id = "U0925SXQFDK" +user_uuid = get_user_uuid(slack_id) # 결정적 UUID 생성 ``` -### 3. 멘션 테스트 -``` -@Roving 오늘 할 일을 정리해주세요 +#### 사용자 데이터 구조 +```sql +-- users 테이블 +CREATE TABLE users ( + id UUID PRIMARY KEY, -- UUID5로 생성 + username VARCHAR(50), -- slack_U0925SXQ 형태 + email VARCHAR(255), + name VARCHAR(255), + oauth_provider VARCHAR(50) DEFAULT 'slack', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); ``` -## 📊 예상 결과 -``` -안녕하세요! 테스트 모드에서 실행 중입니다. 메시지를 받았습니다: '[메시지]' +--- + +## 3. 서비스 구성 + +### 3.1 서버 배치 +| 서버 | 서비스 | 포트 | 역할 | +|------|--------|------|------| +| 51123 | auth-server | 9000 | OAuth 인증 | +| 51123 | robeing-gateway | 8100 | API 프록시 | +| 51124 | rb10508_micro | 10508 | Slack 이벤트 처리 | +| 51124 | rb8001 | 8001 | 프로덕션 로빙 | + +### 3.2 Slack 앱 설정 + +#### Event Subscriptions +```yaml +Request URL: https://[도메인]/api/slack/events +Events: + - app_mention + - message.channels + - message.groups + - message.im + - message.mpim ``` -## 🔍 로그 확인 -실시간 로그 모니터링: -- ngrok: http://localhost:4040 -- 서버 로그에서 "Received Slack event" 메시지 확인 +#### OAuth Scopes +```yaml +Bot Token Scopes: + - app_mentions:read + - channels:read + - chat:write + - chat:write.public + - im:read + - im:write + - users:read +``` -## 🐛 문제 해결 +--- -### URL 검증 실패 -- ngrok URL 정확성 확인 -- 서버 실행 상태 확인 +## 4. 메시지 처리 플로우 -### 봇 무응답 -- Event Subscriptions 저장 확인 -- 앱 재설치 완료 확인 -- 봇 권한 확인 +### 4.1 일반 대화 +```mermaid +flowchart TD + A[Slack 메시지] --> B{이벤트 타입} + B -->|app_mention| C[멘션 처리] + B -->|message| D[일반 메시지] + + C --> E[UUID5 변환] + D --> E + + E --> F[사용자 컨텍스트 로드] + F --> G[의도 분류] + + G --> H{의도 타입} + H -->|대화| I[LLM 응답 생성] + H -->|이메일| J[skill-email 호출] + H -->|뉴스| K[skill-news 호출] + + I --> L[Slack 응답] + J --> L + K --> L +``` -### 권한 오류 -- OAuth Scopes 재확인 -- 워크스페이스 관리자 권한 확인 +### 4.2 Gmail 연동 플로우 +```mermaid +sequenceDiagram + participant User as Slack 사용자 + participant RB as rb10508_micro + participant Monitor as robeing-monitor + participant Skill as skill-email + participant Gmail as Gmail API -## ✨ 설정 완료 후 -Slack에서 메시지를 보내면: -1. ngrok 로그에 요청 기록 -2. 서버에서 이벤트 처리 -3. AI 서비스에서 응답 생성 -4. Slack으로 응답 전송 + User->>RB: "이메일 보내줘" + RB->>RB: UUID5 변환 + RB->>Monitor: Gmail 아이템 확인 + + alt 아이템 미장착 + Monitor-->>RB: NOT_EQUIPPED + RB-->>User: "Gmail 연결이 필요합니다" + else 아이템 장착됨 + Monitor-->>RB: EQUIPPED + RB->>Skill: 이메일 발송 요청 + Skill->>Gmail: API 호출 + Gmail-->>Skill: 발송 완료 + Skill-->>RB: 성공 + RB-->>User: "메일을 보냈습니다" + end +``` -모든 설정이 완료되면 로빙이 정상적으로 응답할 것입니다! \ No newline at end of file +--- + +## 5. 3초 룰 대응 + +### 5.1 비동기 처리 패턴 +```python +@app.post("/api/slack/events") +async def handle_slack_event(request: Request): + # 1. 즉시 응답 (3초 내) + background_tasks.add_task(process_event, event_data) + return {"status": "ok"} + +async def process_event(event_data): + # 2. 실제 처리 (백그라운드) + response = await generate_response(event_data) + + # 3. Slack API로 응답 전송 + await slack_client.chat_postMessage( + channel=event_data['channel'], + text=response + ) +``` + +### 5.2 타이핑 인디케이터 +```python +async def show_typing(channel: str): + """처리 중임을 표시""" + await slack_client.chat_postEphemeral( + channel=channel, + user=user_id, + text="생각 중... 🤔" + ) +``` + +--- + +## 6. 데이터베이스 구조 + +### 6.1 Slack 관련 테이블 +```sql +-- Slack 사용자 매핑 (더 이상 필요 없음 - UUID5 직접 사용) +-- 대신 users 테이블에서 직접 관리 + +-- 대화 로그 +CREATE TABLE conversation_logs ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID, -- UUID5로 생성된 ID + robeing_id VARCHAR(50), + channel_id VARCHAR(50), + message_type VARCHAR(20), -- 'user' or 'bot' + message TEXT, + metadata JSONB, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); +``` + +--- + +## 7. 에러 처리 + +### 7.1 일반 에러 응답 +```python +ERROR_MESSAGES = { + "NO_USER": "사용자를 찾을 수 없습니다. 관리자에게 문의하세요.", + "TIMEOUT": "처리 시간이 초과되었습니다. 다시 시도해주세요.", + "SKILL_ERROR": "기능 실행 중 오류가 발생했습니다.", + "AUTH_ERROR": "인증이 필요합니다." +} +``` + +### 7.2 재시도 로직 +```python +@retry(max_attempts=3, delay=1) +async def send_slack_message(channel: str, text: str): + """실패시 재시도하는 메시지 전송""" + return await slack_client.chat_postMessage( + channel=channel, + text=text + ) +``` + +--- + +## 8. 모니터링 + +### 8.1 메트릭 수집 +- 응답 시간 +- 에러율 +- 사용자별 요청 수 +- 스킬 사용 통계 + +### 8.2 로그 구조 +```json +{ + "timestamp": "2025-08-21T10:30:00Z", + "user_id": "UUID5-생성값", + "slack_user_id": "U0925SXQFDK", + "channel": "C1234567890", + "message": "사용자 메시지", + "response": "로빙 응답", + "processing_time": 1.5, + "status": "success" +} +``` + +--- + +## 9. 보안 고려사항 + +### 9.1 인증 +- Slack 서명 검증 필수 +- 재전송 공격 방지 (timestamp 검증) + +### 9.2 권한 관리 +- 채널별 접근 권한 +- 사용자별 기능 제한 +- 레벨 기반 스킬 접근 + +--- + +## 10. 향후 개선사항 + +### 10.1 단기 +- [ ] 스레드 대화 지원 +- [ ] 이모지 반응 처리 +- [ ] 파일 업로드 지원 + +### 10.2 장기 +- [ ] Slack 워크플로우 통합 +- [ ] 블록 UI 활용 +- [ ] 슬래시 커맨드 확장 + +--- + +**문서 끝** \ No newline at end of file diff --git a/300_architecture/database/tables.md b/300_architecture/database/tables.md index fa9d14a..87d557f 100644 --- a/300_architecture/database/tables.md +++ b/300_architecture/database/tables.md @@ -131,7 +131,7 @@ | 컬럼명 | 타입 | NULL | 기본값 | 설명 | |--------|------|------|--------|------| | id | SERIAL | NO | | 로그 ID | -| user_id | VARCHAR(100) | YES | | 사용자 ID (⚠️ VARCHAR - 수정 필요) | +| user_id | UUID | YES | | 사용자 ID (FK → users) | | robeing_id | VARCHAR(50) | YES | | 로빙 ID | | action | VARCHAR(50) | YES | | 작업 유형 (equip/unequip/reauth) | | success | BOOLEAN | YES | | 성공 여부 | @@ -157,10 +157,6 @@ | created_at | TIMESTAMP | YES | CURRENT_TIMESTAMP | 생성 시각 | | updated_at | TIMESTAMP | YES | CURRENT_TIMESTAMP | 수정 시각 | -### robeing_stats (구버전) -- **용도**: 구버전 로빙 통계 (사용 중단 예정) -- **설명**: robeing_stats로 마이그레이션 필요 - ### robeing_settings - **용도**: 로빙 설정 정보 - **Primary Key**: id (SERIAL) @@ -175,7 +171,45 @@ --- -## 6. 대화 로그 테이블 +## 6. robeing 전용 스키마 테이블 + +### robeing.contacts +- **용도**: 로빙이 관리하는 연락처 정보 +- **Primary Key**: id (UUID) +- **스키마**: robeing + +| 컴럼명 | 타입 | NULL | 기본값 | 설명 | +|--------|------|------|--------|------| +| id | UUID | NO | gen_random_uuid() | 연락처 ID | +| robeing_id | VARCHAR(50) | NO | | 로빙 ID | +| name | VARCHAR(100) | NO | | 이름 | +| email | VARCHAR(255) | YES | | 이메일 주소 | +| phone | VARCHAR(50) | YES | | 전화번호 | +| company | VARCHAR(200) | YES | | 회사명 | +| relationship | VARCHAR(100) | YES | | 관계 (동료, 고객 등) | +| notes | TEXT | YES | | 메모 | +| extra | JSONB | YES | {} | 추가 정보 | +| created_at | TIMESTAMP | YES | CURRENT_TIMESTAMP | 생성 시각 | +| updated_at | TIMESTAMP | YES | CURRENT_TIMESTAMP | 수정 시각 | + +### robeing.conversations +- **용도**: 로빙 대화 기록 +- **Primary Key**: id (UUID) +- **스키마**: robeing + +| 컴럼명 | 타입 | NULL | 기본값 | 설명 | +|--------|------|------|--------|------| +| id | UUID | NO | gen_random_uuid() | 대화 ID | +| robeing_id | VARCHAR(50) | NO | | 로빙 ID | +| user_message | TEXT | YES | | 사용자 메시지 | +| robeing_response | TEXT | YES | | 로빙 응답 | +| context | JSONB | YES | {} | 대화 컨텍스트 | +| metadata | JSONB | YES | {} | 메타데이터 | +| timestamp | TIMESTAMP | YES | CURRENT_TIMESTAMP | 대화 시각 | + +--- + +## 7. 대화 로그 테이블 ### conversation_logs - **용도**: 대화 기록 저장 @@ -203,6 +237,8 @@ - slack_user_mapping: slack_user_id, user_id - workspace_members: workspace_id, user_id - conversation_logs: user_id, robeing_id, created_at +- robeing.contacts: robeing_id, name, email +- robeing.conversations: robeing_id, timestamp --- @@ -223,14 +259,13 @@ ## 주의사항 ### 데이터 타입 일관성 -- **user_id**: 모든 테이블에서 UUID 타입 사용 (gmail_audit_logs 제외) +- **user_id**: 모든 테이블에서 UUID 타입 사용 - **robeing_id**: VARCHAR(50) 통일 - **timestamp**: TIMESTAMP WITHOUT TIME ZONE 사용 ### 개선 필요 사항 -1. gmail_audit_logs.user_id를 UUID로 변경 필요 (현재 VARCHAR) -2. robeing_stats 테이블을 robeing_stats로 통합 필요 -3. 일부 테이블의 소유자가 postgres로 되어있어 권한 조정 필요 +1. 일부 테이블의 소유자가 postgres로 되어있어 권한 조정 필요 +2. auth_db → main_db로 마이그레이션 완료 --- diff --git a/300_architecture/gateway_proxy_patterns.md b/300_architecture/gateway_proxy_patterns.md new file mode 100644 index 0000000..6820f8e --- /dev/null +++ b/300_architecture/gateway_proxy_patterns.md @@ -0,0 +1,489 @@ +# Gateway 프록시 패턴 아키텍처 + +## 작성일: 2025-08-21 +## 작성자: Claude (51123 서버 관리자) + +--- + +## 1. 개요 + +robeing-gateway (포트 8100)의 프록시 패턴 및 JWT 인증 처리 아키텍처 문서입니다. Gateway는 모든 API 요청의 진입점으로서 인증, 라우팅, UUID 변환을 담당합니다. + +--- + +## 2. 핵심 역할 + +### 2.1 주요 기능 +- **JWT 토큰 검증**: 내부적으로 토큰 유효성 검증 +- **Username → UUID 변환**: JWT의 username을 UUID로 변환 +- **요청 라우팅**: 적절한 백엔드 서비스로 프록시 +- **헤더 주입**: X-User-Id, X-Username 헤더 추가 +- **보안 게이트웨이**: 인증되지 않은 요청 차단 + +### 2.2 서비스 위치 +- **서버**: 51123 +- **포트**: 8100 +- **컨테이너**: robeing-gateway + +--- + +## 3. 인증 플로우 + +### 3.1 JWT 검증 프로세스 + +```mermaid +sequenceDiagram + participant Client as 클라이언트 + participant Gateway as Gateway(8100) + participant DB as PostgreSQL + participant Service as 백엔드 서비스 + + Client->>Gateway: API 요청 + JWT Token + + Gateway->>Gateway: JWT 서명 검증 + Note over Gateway: HS256 알고리즘 + + alt 토큰 무효 + Gateway-->>Client: 401 Unauthorized + else 토큰 유효 + Gateway->>Gateway: username 추출 + Note over Gateway: JWT payload에서 + + Gateway->>DB: SELECT id FROM users WHERE username = ? + DB-->>Gateway: UUID + + Gateway->>Service: 요청 전달 + Note over Service: Headers:
X-User-Id: UUID
X-Username: username + + Service-->>Gateway: 응답 + Gateway-->>Client: 응답 전달 + end +``` + +### 3.2 JWT 구조 + +```json +{ + "header": { + "alg": "HS256", + "typ": "JWT" + }, + "payload": { + "username": "happybell80", + "email": "goeun2dc@gmail.com", + "exp": 1724356800, + "iat": 1724270400 + } +} +``` + +--- + +## 4. UUID 변환 메커니즘 + +### 4.1 변환 로직 + +```python +class GatewayProxy: + async def convert_username_to_uuid(self, username: str) -> str: + """Username을 UUID로 변환""" + query = "SELECT id FROM users WHERE username = $1" + result = await self.db.fetchone(query, username) + + if not result: + raise UserNotFoundError(f"User {username} not found") + + return str(result['id']) + + async def process_request(self, request): + # 1. JWT에서 username 추출 + token = request.headers.get('Authorization') + payload = self.verify_jwt(token) + username = payload['username'] + + # 2. UUID 변환 + user_uuid = await self.convert_username_to_uuid(username) + + # 3. 헤더 추가 + request.headers['X-User-Id'] = user_uuid + request.headers['X-Username'] = username + + # 4. 프록시 + return await self.proxy_to_service(request) +``` + +### 4.2 캐싱 전략 + +```python +from functools import lru_cache +from datetime import datetime, timedelta + +class UserCache: + def __init__(self, ttl_seconds=300): + self.cache = {} + self.ttl = timedelta(seconds=ttl_seconds) + + async def get_uuid(self, username: str) -> str: + # 캐시 확인 + if username in self.cache: + entry = self.cache[username] + if datetime.now() < entry['expires']: + return entry['uuid'] + + # DB 조회 + uuid = await self.fetch_from_db(username) + + # 캐시 저장 + self.cache[username] = { + 'uuid': uuid, + 'expires': datetime.now() + self.ttl + } + + return uuid +``` + +--- + +## 5. 라우팅 규칙 + +### 5.1 서비스 매핑 + +```yaml +routes: + # 인증 서비스 + - path: /api/auth/* + service: auth-server + host: localhost + port: 9000 + auth_required: false + + # Gmail 아이템 + - path: /api/items/gmail/* + service: robeing-monitor + host: localhost + port: 9024 + auth_required: true + + # 로빙 서비스 (51124 서버) + - path: /api/robeing/* + service: rb10508_micro + host: 192.168.219.52 + port: 10508 + auth_required: true + + # 스킬 서비스 + - path: /api/skills/email/* + service: skill-email + host: 192.168.219.52 + port: 8501 + auth_required: true +``` + +### 5.2 동적 라우팅 + +```python +class DynamicRouter: + def __init__(self): + self.routes = self.load_routes() + + async def route_request(self, path: str, headers: dict): + # 경로 매칭 + for route in self.routes: + if self.match_path(path, route['path']): + # 인증 확인 + if route['auth_required'] and not headers.get('X-User-Id'): + raise AuthenticationError() + + # 프록시 + return await self.proxy( + host=route['host'], + port=route['port'], + path=path, + headers=headers + ) + + raise NotFoundError(f"No route for {path}") +``` + +--- + +## 6. 에러 처리 + +### 6.1 에러 응답 포맷 + +```json +{ + "error": { + "code": "AUTHENTICATION_FAILED", + "message": "Invalid or expired token", + "timestamp": "2025-08-21T10:30:00Z", + "request_id": "req_123456" + } +} +``` + +### 6.2 에러 코드 + +| 코드 | HTTP Status | 설명 | +|------|-------------|------| +| INVALID_TOKEN | 401 | JWT 토큰 무효 | +| TOKEN_EXPIRED | 401 | JWT 토큰 만료 | +| USER_NOT_FOUND | 404 | Username에 해당하는 사용자 없음 | +| SERVICE_UNAVAILABLE | 503 | 백엔드 서비스 응답 없음 | +| RATE_LIMIT_EXCEEDED | 429 | 요청 제한 초과 | + +--- + +## 7. 성능 최적화 + +### 7.1 Connection Pooling + +```python +class ConnectionPool: + def __init__(self): + self.pools = {} + + async def get_connection(self, service: str): + if service not in self.pools: + self.pools[service] = await aiohttp.ClientSession( + connector=aiohttp.TCPConnector( + limit=100, + limit_per_host=30, + ttl_dns_cache=300 + ) + ) + return self.pools[service] +``` + +### 7.2 Request/Response 압축 + +```python +@app.middleware("http") +async def compression_middleware(request: Request, call_next): + response = await call_next(request) + + # gzip 압축 적용 + if "gzip" in request.headers.get("Accept-Encoding", ""): + response.headers["Content-Encoding"] = "gzip" + response.body = gzip.compress(response.body) + + return response +``` + +--- + +## 8. 모니터링 + +### 8.1 메트릭 수집 + +```python +metrics = { + "request_count": Counter("gateway_requests_total"), + "request_duration": Histogram("gateway_request_duration_seconds"), + "auth_failures": Counter("gateway_auth_failures_total"), + "proxy_errors": Counter("gateway_proxy_errors_total") +} + +@app.middleware("http") +async def metrics_middleware(request: Request, call_next): + start_time = time.time() + + try: + response = await call_next(request) + metrics["request_count"].inc() + return response + except Exception as e: + metrics["proxy_errors"].inc() + raise + finally: + duration = time.time() - start_time + metrics["request_duration"].observe(duration) +``` + +### 8.2 로그 포맷 + +```json +{ + "timestamp": "2025-08-21T10:30:00Z", + "level": "INFO", + "service": "gateway", + "request_id": "req_123456", + "method": "POST", + "path": "/api/robeing/chat", + "username": "happybell80", + "user_id": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", + "backend_service": "rb10508_micro", + "response_time_ms": 150, + "status_code": 200 +} +``` + +--- + +## 9. 보안 고려사항 + +### 9.1 Rate Limiting + +```python +from datetime import datetime, timedelta + +class RateLimiter: + def __init__(self, requests_per_minute=60): + self.limits = {} + self.max_requests = requests_per_minute + + async def check_limit(self, user_id: str) -> bool: + now = datetime.now() + minute_ago = now - timedelta(minutes=1) + + # 사용자별 요청 기록 + if user_id not in self.limits: + self.limits[user_id] = [] + + # 1분 이내 요청만 유지 + self.limits[user_id] = [ + ts for ts in self.limits[user_id] + if ts > minute_ago + ] + + # 제한 확인 + if len(self.limits[user_id]) >= self.max_requests: + return False + + self.limits[user_id].append(now) + return True +``` + +### 9.2 CORS 설정 + +```python +from fastapi.middleware.cors import CORSMiddleware + +app.add_middleware( + CORSMiddleware, + allow_origins=["https://app.ro-being.com"], + allow_credentials=True, + allow_methods=["GET", "POST", "PUT", "DELETE"], + allow_headers=["Authorization", "Content-Type"], + max_age=3600 +) +``` + +--- + +## 10. 구현 예제 + +### 10.1 완전한 Gateway 클래스 + +```python +from fastapi import FastAPI, Request, HTTPException +import httpx +import jwt +import asyncpg + +class RobeingGateway: + def __init__(self): + self.app = FastAPI() + self.db_pool = None + self.http_client = httpx.AsyncClient() + self.setup_routes() + + async def startup(self): + self.db_pool = await asyncpg.create_pool( + "postgresql://robeings:robeings@localhost/main_db" + ) + + def setup_routes(self): + @self.app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"]) + async def proxy(request: Request, path: str): + # JWT 검증 + token = request.headers.get("Authorization", "").replace("Bearer ", "") + if not token and self.requires_auth(path): + raise HTTPException(401, "Missing token") + + # Username → UUID 변환 + if token: + payload = self.verify_jwt(token) + username = payload['username'] + user_uuid = await self.get_user_uuid(username) + + # 헤더 추가 + headers = dict(request.headers) + headers['X-User-Id'] = user_uuid + headers['X-Username'] = username + else: + headers = dict(request.headers) + + # 백엔드 서비스로 프록시 + backend_url = self.get_backend_url(path) + response = await self.http_client.request( + method=request.method, + url=backend_url, + headers=headers, + content=await request.body() + ) + + return response.json() + + def verify_jwt(self, token: str) -> dict: + try: + return jwt.decode( + token, + "your-secret-key", + algorithms=["HS256"] + ) + except jwt.InvalidTokenError: + raise HTTPException(401, "Invalid token") + + async def get_user_uuid(self, username: str) -> str: + async with self.db_pool.acquire() as conn: + row = await conn.fetchrow( + "SELECT id FROM users WHERE username = $1", + username + ) + if not row: + raise HTTPException(404, f"User {username} not found") + return str(row['id']) +``` + +--- + +## 11. 테스트 전략 + +### 11.1 단위 테스트 + +```python +import pytest +from unittest.mock import Mock, patch + +class TestGateway: + @pytest.mark.asyncio + async def test_username_to_uuid_conversion(self): + gateway = RobeingGateway() + + # Mock DB response + with patch.object(gateway, 'db_pool') as mock_pool: + mock_pool.acquire.return_value.__aenter__.return_value.fetchrow.return_value = { + 'id': 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' + } + + uuid = await gateway.get_user_uuid('happybell80') + assert uuid == 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' + + def test_jwt_verification(self): + gateway = RobeingGateway() + + # Valid token + token = jwt.encode( + {'username': 'test', 'exp': datetime.now() + timedelta(hours=1)}, + 'your-secret-key', + algorithm='HS256' + ) + + payload = gateway.verify_jwt(token) + assert payload['username'] == 'test' +``` + +--- + +**문서 끝** \ No newline at end of file diff --git a/300_architecture/sequences/auth_login_sequences.md b/300_architecture/sequences/auth_login_sequences.md index 841ef77..26ba7da 100644 --- a/300_architecture/sequences/auth_login_sequences.md +++ b/300_architecture/sequences/auth_login_sequences.md @@ -58,7 +58,7 @@ sequenceDiagram Note over DB: id(UUID), email, name, picture Auth->>Auth: JWT 토큰 생성 - Note over Auth: user_id, email, exp + Note over Auth: username, email, exp
(실제 user_id 포함 안함) Auth->>Redis: 임시 코드 저장 (60초 TTL) Note over Redis: code → JWT token @@ -99,8 +99,12 @@ sequenceDiagram Auth->>DB: UPDATE users SET last_login=NOW() + Auth->>DB: username 조회 + Note over DB: SELECT username FROM users
WHERE id = ? + DB-->>Auth: username + Auth->>Auth: JWT 토큰 생성 - Note over Auth: 기존 user_id(UUID) 사용 + Note over Auth: username, email, exp
(실제 user_id 포함 안함) Note over Auth,Front: 토큰 전달 (1.1과 동일) ``` @@ -137,11 +141,18 @@ sequenceDiagram Auth->>SlackAPI: POST /oauth.v2.access SlackAPI-->>Auth: access_token, user info + Note over Auth: Slack User ID: U0925SXQFDK - Auth->>DB: SELECT * FROM users WHERE email=? + Auth->>Auth: UUID5 생성 + Note over Auth: namespace: 6ba7b810-9dad-11d1-80b4-00c04fd430c8
name: Slack User ID
UUID5 = uuid5(namespace, slack_id) + + Auth->>DB: SELECT * FROM users WHERE id=? + Note over DB: UUID5로 직접 조회 alt 신규 사용자 - Auth->>Auth: UUID 생성 + Auth->>Auth: username 생성 + Note over Auth: slack_{slack_id[:8]} Auth->>DB: INSERT INTO users + Note over DB: id: UUID5
username: slack_U0925SXQ
email: slack.email else 기존 사용자 Auth->>DB: UPDATE users end @@ -172,9 +183,15 @@ sequenceDiagram Gateway->>Gateway: 만료 시간 확인 alt 토큰 유효 - Gateway->>Gateway: user_id 추출 + Gateway->>Gateway: username 추출 + Note over Gateway: JWT payload에서 username + + Gateway->>DB: username → UUID 변환 + Note over DB: SELECT id FROM users
WHERE username = ? + DB-->>Gateway: user_id (UUID) + Gateway->>Service: 요청 전달 - Note over Service: X-User-Id: {UUID} + Note over Service: X-User-Id: {UUID}
X-Username: {username} Service-->>Gateway: 응답 Gateway-->>Front: 응답 else 토큰 만료/무효 @@ -193,20 +210,25 @@ sequenceDiagram flowchart TD A[사용자 로그인/가입] --> B{사용자 타입} - B -->|OAuth 로그인| C[이메일로 DB 조회] + B -->|Google OAuth| C[이메일로 DB 조회] C --> D{기존 사용자?} D -->|Yes| E[기존 UUID 사용] D -->|No| F["uuid.uuid4() 생성"] - B -->|테스트 사용자| G[수동 UUID 할당] - G --> H["하드코딩 UUID
aaaaaaaa-aaaa..."] + B -->|Slack OAuth| G[Slack ID 받음] + G --> H["UUID5 생성
uuid5(namespace, slack_id)"] + H --> I{기존 UUID 존재?} + I -->|Yes| E + I -->|No| J[UUID5로 새 사용자] - B -->|시스템 사용자| I[예약 UUID 사용] + B -->|테스트 사용자| K[수동 UUID 할당] + K --> L["하드코딩 UUID
aaaaaaaa-aaaa..."] - F --> J[DB 저장] - E --> K[JWT 토큰 생성] - H --> J - J --> K + F --> M[DB 저장] + J --> M + L --> M + E --> N[username으로 JWT 생성] + M --> N ``` ### 4.2 사용자 테이블 구조 @@ -267,6 +289,7 @@ stateDiagram-v2 - HttpOnly 쿠키 사용 권장 (현재는 localStorage) - 짧은 만료 시간 (현재 24시간) - 서명 검증 필수 + - username 기반 payload (user_id 포함 안함) 2. **임시 코드** - Redis 60초 TTL @@ -283,14 +306,19 @@ stateDiagram-v2 ```yaml 식별 체계: Primary Key: UUID (36자) + - Google 사용자: uuid4() 랜덤 생성 + - Slack 사용자: uuid5(namespace, slack_id) 결정적 생성 Unique Keys: - email (OAuth provider에서 제공) - - username (사용자 정의, optional) + - username (사용자 정의 또는 자동 생성) 관계: - users.id (UUID) ← gmail_tokens.user_id - - users.id (UUID) ← slack_users.user_id - - users.id (UUID) ← robeing_assignments.user_id + - users.id (UUID) ← slack_user_mapping.user_id + - users.id (UUID) ← robeing_stats.user_id + +UUID5 Namespace: + 6ba7b810-9dad-11d1-80b4-00c04fd430c8 ``` --- diff --git a/300_architecture/sequences/email_sequences.md b/300_architecture/sequences/email_sequences.md index f3492e4..b9ca8ae 100644 --- a/300_architecture/sequences/email_sequences.md +++ b/300_architecture/sequences/email_sequences.md @@ -108,10 +108,15 @@ sequenceDiagram User->>Front: 인벤토리 페이지 접속 Front->>Gateway: GET /api/items/gmail - Gateway->>Auth: JWT 토큰 검증 - Auth-->>Gateway: user_id 확인 + Gateway->>Gateway: JWT 토큰 검증 (내부) + Note over Gateway: username 추출 + + Gateway->>DB: username → UUID 변환 + Note over DB: users 테이블 조회 + DB-->>Gateway: user_id (UUID) Gateway->>Monitor: 아이템 목록 요청 + Note over Monitor: X-User-Id 헤더로
UUID 전달 Monitor->>DB: gmail_tokens 조회 Note over DB: user_id로 필터링 @@ -130,8 +135,11 @@ sequenceDiagram Front->>Gateway: POST /api/items/gmail/{userId}/equip Note over Gateway: Body: {robeing_id: "rb10508_micro"} - Gateway->>Auth: JWT 검증 - Auth-->>Gateway: 인증 확인 + Gateway->>Gateway: JWT 검증 (내부) + Note over Gateway: username 추출 + + Gateway->>DB: username → UUID 변환 + DB-->>Gateway: user_id (UUID) Gateway->>Monitor: 장착 요청 Monitor->>DB: robeing_stats 레벨 확인 @@ -179,7 +187,15 @@ sequenceDiagram Front->>Gateway: POST /api/robeing/email/send Note over Gateway: JWT 토큰 포함
to, subject, body + Gateway->>Gateway: JWT 검증 (내부) + Note over Gateway: username 추출 + + Gateway->>DB: username → UUID 변환 + Note over DB: users 테이블 조회
SELECT id FROM users
WHERE username = ? + DB-->>Gateway: user_id (UUID) + Gateway->>RB: 이메일 발송 요청 + Note over RB: X-User-Id 헤더로
UUID 전달 RB->>Monitor: GET /api/items/gmail/status Note over Monitor: 장착 상태 확인 @@ -244,8 +260,11 @@ sequenceDiagram RB->>RB: 의도 분류 Note over RB: INTENT: EMAIL_SEND
수신자: 종태님
내용: 회의 일정 - RB->>DB: slack_user_mapping 조회 - Note over DB: U0925SXQFDK → user_id + RB->>RB: Slack ID → UUID5 변환 + Note over RB: UUID5 생성:
namespace: 6ba7b810-9dad-11d1-80b4-00c04fd430c8
name: U0925SXQFDK + + RB->>DB: UUID로 사용자 조회 + Note over DB: SELECT * FROM users
WHERE id = uuid5(...) DB-->>RB: user_id, name: "김종태" RB->>Monitor: Gmail 아이템 상태 확인 @@ -262,9 +281,9 @@ sequenceDiagram RB->>RB: 이메일 내용 생성 Note over RB: LLM으로 메일 본문 작성
"안녕하세요 종태님,
회의 일정 관련..." - RB->>DB: 수신자 이메일 조회 - Note over DB: "종태님" → goeun2dc@gmail.com - DB-->>RB: 이메일 주소 + RB->>DB: robeing.contacts 조회 + Note over DB: SELECT email FROM robeing.contacts
WHERE robeing_id = $1
AND name ILIKE '%종태%' + DB-->>RB: 이메일 주소 (goeun2dc@gmail.com) RB->>Skill: POST /send-email Note over Skill: to: goeun2dc@gmail.com
subject: "회의 일정 안내"
body: LLM 생성 내용 @@ -521,6 +540,28 @@ sequenceDiagram --- +## 9. UUID 변환 체계 + +### 9.1 일반 사용자 (프론트엔드) +``` +JWT Token (username: "happybell80") + ↓ +Gateway: username → UUID 변환 + ↓ (users 테이블 조회) +UUID4: aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa +``` + +### 9.2 Slack 사용자 +``` +Slack User ID: U0925SXQFDK + ↓ +UUID5 생성 (namespace + Slack ID) + ↓ +UUID5: 생성된 UUID +``` + +--- + ## 다음 단계 1. **구현 우선순위** diff --git a/300_architecture/uuid_conversion_system.md b/300_architecture/uuid_conversion_system.md new file mode 100644 index 0000000..82c0ca2 --- /dev/null +++ b/300_architecture/uuid_conversion_system.md @@ -0,0 +1,386 @@ +# UUID 변환 시스템 아키텍처 + +## 작성일: 2025-08-21 +## 작성자: Claude (51123 서버 관리자) + +--- + +## 1. 개요 + +RO-BEING 시스템의 UUID 변환 체계 문서입니다. 일반 사용자는 UUID4, Slack 사용자는 UUID5를 사용하여 일관된 사용자 식별 체계를 유지합니다. + +--- + +## 2. UUID 타입별 용도 + +### 2.1 UUID 타입 구분 + +| 타입 | 생성 방식 | 용도 | 특징 | +|------|----------|------|------| +| UUID4 | 랜덤 생성 | Google OAuth 사용자 | 매번 다른 값 생성 | +| UUID5 | 네임스페이스 + 이름 해시 | Slack 사용자 | 같은 입력에 같은 출력 (결정적) | + +### 2.2 네임스페이스 + +```python +# DNS 네임스페이스 (표준 UUID) +NAMESPACE = uuid.UUID('6ba7b810-9dad-11d1-80b4-00c04fd430c8') +``` + +--- + +## 3. 사용자별 UUID 생성 + +### 3.1 Google OAuth 사용자 (UUID4) + +```mermaid +flowchart LR + A[Google 로그인] --> B[이메일 확인] + B --> C{기존 사용자?} + C -->|Yes| D[기존 UUID 사용] + C -->|No| E[uuid4() 생성] + E --> F[DB 저장] + F --> G[JWT 생성] +``` + +#### 구현 코드 +```python +import uuid + +class GoogleAuthHandler: + async def create_or_get_user(self, email: str, name: str): + # 기존 사용자 확인 + user = await self.db.fetchone( + "SELECT * FROM users WHERE email = $1", + email + ) + + if user: + return user['id'] + + # 신규 사용자 - UUID4 생성 + new_user_id = str(uuid.uuid4()) + + await self.db.execute(""" + INSERT INTO users (id, email, name, username, oauth_provider) + VALUES ($1, $2, $3, $4, 'google') + """, new_user_id, email, name, self.generate_username(email)) + + return new_user_id +``` + +### 3.2 Slack 사용자 (UUID5) + +```mermaid +flowchart LR + A[Slack 이벤트] --> B[Slack User ID] + B --> C[UUID5 생성] + C --> D[네임스페이스 + Slack ID] + D --> E[SHA-1 해시] + E --> F[결정적 UUID] + F --> G{DB 확인} + G -->|없음| H[신규 등록] + G -->|있음| I[기존 사용] +``` + +#### 구현 코드 +```python +import uuid + +class SlackUserHandler: + NAMESPACE = uuid.UUID('6ba7b810-9dad-11d1-80b4-00c04fd430c8') + + def get_user_uuid(self, slack_user_id: str) -> str: + """Slack User ID를 UUID5로 변환""" + return str(uuid.uuid5(self.NAMESPACE, slack_user_id)) + + async def get_or_create_user(self, slack_user_id: str, slack_user_info: dict): + # UUID5 생성 (항상 같은 결과) + user_uuid = self.get_user_uuid(slack_user_id) + + # 기존 사용자 확인 + user = await self.db.fetchone( + "SELECT * FROM users WHERE id = $1", + user_uuid + ) + + if user: + return user_uuid + + # 신규 사용자 등록 + username = f"slack_{slack_user_id[:8]}" + await self.db.execute(""" + INSERT INTO users (id, username, email, name, oauth_provider) + VALUES ($1, $2, $3, $4, 'slack') + """, user_uuid, username, + slack_user_info.get('email', ''), + slack_user_info.get('real_name', '')) + + return user_uuid +``` + +--- + +## 4. Gateway에서의 UUID 변환 + +### 4.1 JWT Token → UUID 변환 플로우 + +```mermaid +sequenceDiagram + participant Client + participant Gateway + participant DB + participant Service + + Client->>Gateway: Request + JWT (username) + Gateway->>Gateway: JWT 검증 + Gateway->>Gateway: username 추출 + + Gateway->>DB: SELECT id FROM users WHERE username = ? + DB-->>Gateway: UUID + + Gateway->>Service: Request + X-User-Id (UUID) + Service-->>Gateway: Response + Gateway-->>Client: Response +``` + +### 4.2 변환 구현 + +```python +class GatewayUUIDConverter: + def __init__(self, db_pool): + self.db_pool = db_pool + self.cache = {} # username -> uuid 캐시 + + async def username_to_uuid(self, username: str) -> str: + """Username을 UUID로 변환 (캐싱 포함)""" + # 캐시 확인 + if username in self.cache: + return self.cache[username] + + # DB 조회 + async with self.db_pool.acquire() as conn: + row = await conn.fetchrow( + "SELECT id FROM users WHERE username = $1", + username + ) + + if not row: + raise UserNotFoundError(f"User {username} not found") + + user_uuid = str(row['id']) + + # 캐시 저장 (5분 TTL) + self.cache[username] = user_uuid + asyncio.create_task(self.clear_cache_after(username, 300)) + + return user_uuid + + async def clear_cache_after(self, username: str, seconds: int): + """일정 시간 후 캐시 제거""" + await asyncio.sleep(seconds) + self.cache.pop(username, None) +``` + +--- + +## 5. 테스트 사용자 UUID + +### 5.1 하드코딩된 테스트 UUID + +```sql +-- 테스트 사용자 (고정 UUID) +INSERT INTO users (id, username, email, name) VALUES + ('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'happybell80', 'goeun2dc@gmail.com', '김종태'), + ('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', 'eagle0914', '0914eagle@gmail.com', '전희재'), + ('cccccccc-cccc-cccc-cccc-cccccccccccc', 'test_user', 'test@example.com', 'Test User'); +``` + +### 5.2 Slack 테스트 사용자 UUID5 예시 + +```python +# Slack User ID 예시 +slack_ids = { + "U0925SXQFDK": "종태", # UUID5: 생성된 값 + "U0123ABCDEF": "테스트" # UUID5: 생성된 값 +} + +# UUID5 생성 예시 +for slack_id, name in slack_ids.items(): + user_uuid = uuid.uuid5(NAMESPACE, slack_id) + print(f"{name} ({slack_id}): {user_uuid}") +``` + +--- + +## 6. UUID 마이그레이션 + +### 6.1 기존 VARCHAR user_id → UUID 마이그레이션 + +```sql +-- 1. 임시 컬럼 추가 +ALTER TABLE gmail_tokens ADD COLUMN user_uuid UUID; + +-- 2. UUID 매핑 +UPDATE gmail_tokens gt +SET user_uuid = u.id +FROM users u +WHERE gt.user_id = u.username; + +-- 3. 기존 컬럼 제거 및 이름 변경 +ALTER TABLE gmail_tokens DROP COLUMN user_id; +ALTER TABLE gmail_tokens RENAME COLUMN user_uuid TO user_id; + +-- 4. 외래키 제약 추가 +ALTER TABLE gmail_tokens +ADD CONSTRAINT fk_user_id +FOREIGN KEY (user_id) REFERENCES users(id); +``` + +--- + +## 7. UUID 유효성 검증 + +### 7.1 검증 함수 + +```python +import re + +def is_valid_uuid(uuid_string: str) -> bool: + """UUID 형식 검증""" + uuid_pattern = re.compile( + r'^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$', + re.IGNORECASE + ) + return bool(uuid_pattern.match(uuid_string)) + +def get_uuid_version(uuid_string: str) -> int: + """UUID 버전 확인""" + try: + u = uuid.UUID(uuid_string) + return u.version + except ValueError: + return None + +# 사용 예시 +user_id = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" +if is_valid_uuid(user_id): + version = get_uuid_version(user_id) + print(f"Valid UUID v{version}") +``` + +--- + +## 8. UUID 관련 API 응답 + +### 8.1 사용자 정보 API + +```python +@app.get("/api/users/me") +async def get_current_user(request: Request): + user_id = request.headers.get("X-User-Id") + username = request.headers.get("X-Username") + + return { + "id": user_id, # UUID + "username": username, + "uuid_version": get_uuid_version(user_id), + "is_slack_user": get_uuid_version(user_id) == 5 + } +``` + +### 8.2 UUID 디버깅 엔드포인트 + +```python +@app.get("/api/debug/uuid/{username}") +async def debug_uuid(username: str): + """개발 환경에서만 사용""" + user = await db.fetchone( + "SELECT id, username, oauth_provider FROM users WHERE username = $1", + username + ) + + if not user: + return {"error": "User not found"} + + return { + "username": username, + "uuid": str(user['id']), + "uuid_version": get_uuid_version(str(user['id'])), + "oauth_provider": user['oauth_provider'], + "is_deterministic": get_uuid_version(str(user['id'])) == 5 + } +``` + +--- + +## 9. 트러블슈팅 + +### 9.1 일반적인 문제 + +| 문제 | 원인 | 해결 방법 | +|------|------|----------| +| UUID 불일치 | Slack ID 변경 | UUID5는 결정적이므로 같은 Slack ID는 항상 같은 UUID | +| 중복 UUID | UUID4 충돌 (매우 드물음) | 재생성 또는 UNIQUE 제약 확인 | +| 변환 실패 | username 없음 | users 테이블 username 필드 확인 | +| 캐시 불일치 | TTL 만료 전 DB 변경 | 캐시 무효화 또는 TTL 단축 | + +### 9.2 디버깅 쿼리 + +```sql +-- UUID 버전별 사용자 수 +SELECT + CASE + WHEN id::text LIKE '________-____-4___-____-____________' THEN 'UUID4 (Google)' + WHEN id::text LIKE '________-____-5___-____-____________' THEN 'UUID5 (Slack)' + ELSE 'Other' + END as uuid_type, + COUNT(*) as count +FROM users +GROUP BY uuid_type; + +-- Username-UUID 매핑 확인 +SELECT username, id, oauth_provider +FROM users +WHERE username = 'happybell80'; + +-- Slack 사용자 UUID5 검증 +SELECT + username, + id, + id = uuid_generate_v5('6ba7b810-9dad-11d1-80b4-00c04fd430c8'::uuid, + SUBSTRING(username FROM 7)) as is_valid_uuid5 +FROM users +WHERE oauth_provider = 'slack'; +``` + +--- + +## 10. 보안 고려사항 + +### 10.1 UUID 노출 최소화 + +```python +class SecureUUIDHandler: + @staticmethod + def mask_uuid(uuid_string: str) -> str: + """UUID 일부 마스킹""" + # aaaaaaaa-****-****-****-aaaaaaaaaaaa + parts = uuid_string.split('-') + return f"{parts[0]}-****-****-****-{parts[4]}" + + @staticmethod + def should_expose_uuid(user_role: str) -> bool: + """역할별 UUID 노출 여부""" + return user_role in ['admin', 'developer'] +``` + +### 10.2 UUID 추측 방지 + +- UUID4: 122비트 랜덤 엔트로피로 추측 불가능 +- UUID5: Slack ID를 모르면 생성 불가능 +- 네임스페이스 비공개 유지 (소스코드에서만 관리) + +--- + +**문서 끝** \ No newline at end of file