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