아키텍처 문서 대규모 업데이트: JWT/UUID 변환 체계 정립
- JWT 검증 플로우: Gateway 내부 처리로 변경 - Username → UUID 변환 메커니즘 문서화 - UUID5 체계: Slack 사용자용 결정적 UUID 생성 - Gateway 프록시 패턴 상세 문서화 - 데이터베이스: gmail_tokens, robeing 스키마 추가 - 서비스 포트 매핑 및 역할 명확화 - auth_db → main_db 마이그레이션 반영
This commit is contained in:
parent
1cdb721b77
commit
9eaa83a76e
@ -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
|
||||
**상태**: 구현 중
|
||||
@ -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 활용
|
||||
- [ ] 슬래시 커맨드 확장
|
||||
|
||||
---
|
||||
|
||||
**문서 끝**
|
||||
@ -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로 마이그레이션 완료
|
||||
|
||||
---
|
||||
|
||||
|
||||
489
300_architecture/gateway_proxy_patterns.md
Normal file
489
300_architecture/gateway_proxy_patterns.md
Normal 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'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**문서 끝**
|
||||
@ -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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@ -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. **구현 우선순위**
|
||||
|
||||
386
300_architecture/uuid_conversion_system.md
Normal file
386
300_architecture/uuid_conversion_system.md
Normal 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를 모르면 생성 불가능
|
||||
- 네임스페이스 비공개 유지 (소스코드에서만 관리)
|
||||
|
||||
---
|
||||
|
||||
**문서 끝**
|
||||
Loading…
x
Reference in New Issue
Block a user