아키텍처 문서 대규모 업데이트: JWT/UUID 변환 체계 정립

- JWT 검증 플로우: Gateway 내부 처리로 변경
- Username → UUID 변환 메커니즘 문서화
- UUID5 체계: Slack 사용자용 결정적 UUID 생성
- Gateway 프록시 패턴 상세 문서화
- 데이터베이스: gmail_tokens, robeing 스키마 추가
- 서비스 포트 매핑 및 역할 명확화
- auth_db → main_db 마이그레이션 반영
This commit is contained in:
happybell80 2025-08-22 19:52:23 +09:00
parent 1cdb721b77
commit 9eaa83a76e
7 changed files with 1385 additions and 142 deletions

View File

@ -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
**상태**: 설계 완료, 구현 준비
**버전**: 2.0
**상태**: 구현 중

View File

@ -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
```
모든 설정이 완료되면 로빙이 정상적으로 응답할 것입니다!
---
## 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 활용
- [ ] 슬래시 커맨드 확장
---
**문서 끝**

View File

@ -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로 마이그레이션 완료
---

View File

@ -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:<br/>X-User-Id: UUID<br/>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'
```
---
**문서 끝**

View File

@ -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<br/>(실제 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<br/>WHERE id = ?
DB-->>Auth: username
Auth->>Auth: JWT 토큰 생성
Note over Auth: 기존 user_id(UUID) 사용
Note over Auth: username, email, exp<br/>(실제 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<br/>name: Slack User ID<br/>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<br/>username: slack_U0925SXQ<br/>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<br/>WHERE username = ?
DB-->>Gateway: user_id (UUID)
Gateway->>Service: 요청 전달
Note over Service: X-User-Id: {UUID}
Note over Service: X-User-Id: {UUID}<br/>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<br/>aaaaaaaa-aaaa..."]
B -->|Slack OAuth| G[Slack ID 받음]
G --> H["UUID5 생성<br/>uuid5(namespace, slack_id)"]
H --> I{기존 UUID 존재?}
I -->|Yes| E
I -->|No| J[UUID5로 새 사용자]
B -->|시스템 사용자| I[예약 UUID 사용]
B -->|테스트 사용자| K[수동 UUID 할당]
K --> L["하드코딩 UUID<br/>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
```
---

View File

@ -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 헤더로<br/>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 토큰 포함<br/>to, subject, body
Gateway->>Gateway: JWT 검증 (내부)
Note over Gateway: username 추출
Gateway->>DB: username → UUID 변환
Note over DB: users 테이블 조회<br/>SELECT id FROM users<br/>WHERE username = ?
DB-->>Gateway: user_id (UUID)
Gateway->>RB: 이메일 발송 요청
Note over RB: X-User-Id 헤더로<br/>UUID 전달
RB->>Monitor: GET /api/items/gmail/status
Note over Monitor: 장착 상태 확인
@ -244,8 +260,11 @@ sequenceDiagram
RB->>RB: 의도 분류
Note over RB: INTENT: EMAIL_SEND<br/>수신자: 종태님<br/>내용: 회의 일정
RB->>DB: slack_user_mapping 조회
Note over DB: U0925SXQFDK → user_id
RB->>RB: Slack ID → UUID5 변환
Note over RB: UUID5 생성:<br/>namespace: 6ba7b810-9dad-11d1-80b4-00c04fd430c8<br/>name: U0925SXQFDK
RB->>DB: UUID로 사용자 조회
Note over DB: SELECT * FROM users<br/>WHERE id = uuid5(...)
DB-->>RB: user_id, name: "김종태"
RB->>Monitor: Gmail 아이템 상태 확인
@ -262,9 +281,9 @@ sequenceDiagram
RB->>RB: 이메일 내용 생성
Note over RB: LLM으로 메일 본문 작성<br/>"안녕하세요 종태님,<br/>회의 일정 관련..."
RB->>DB: 수신자 이메일 조회
Note over DB: "종태님" → goeun2dc@gmail.com
DB-->>RB: 이메일 주소
RB->>DB: robeing.contacts 조회
Note over DB: SELECT email FROM robeing.contacts<br/>WHERE robeing_id = $1<br/>AND name ILIKE '%종태%'
DB-->>RB: 이메일 주소 (goeun2dc@gmail.com)
RB->>Skill: POST /send-email
Note over Skill: to: goeun2dc@gmail.com<br/>subject: "회의 일정 안내"<br/>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. **구현 우선순위**

View File

@ -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를 모르면 생성 불가능
- 네임스페이스 비공개 유지 (소스코드에서만 관리)
---
**문서 끝**