docs: rb8001 대화 히스토리 구현 전체 문서화

- plans: 완료 상태로 정리
- troubleshooting: 전체 과정과 실수 상세 기록
- sequences: JWT → Gateway → rb8001 → DB 플로우 문서화
- 정확한 정보만 남기고 추측/틀린 내용 모두 명시
This commit is contained in:
happybell80 2025-09-02 00:45:50 +09:00
parent 79758fa496
commit 04832ee48e
3 changed files with 437 additions and 130 deletions

View File

@ -0,0 +1,222 @@
# 대화 히스토리 조회 플로우
## 작성일: 2025-09-02
## 대상: rb8001/rb10508_micro 대화 히스토리
## 상태: ✅ 구현 완료
---
## 1. 전체 플로우
```mermaid
sequenceDiagram
participant F as Frontend
participant G as Gateway
participant R as rb8001
participant DB as PostgreSQL
F->>F: localStorage에서 JWT 토큰 가져오기
F->>F: JWT decode하여 user_id 추출
F->>G: GET /gateway/api/history?limit=30<br/>Authorization: Bearer {JWT}
G->>G: JWT 검증 (서명, 만료시간)
G->>G: JWT에서 sub(UUID) 추출
G->>G: workspace_members 테이블 조회<br/>robeing_id 확인 (rb8001)
G->>R: GET /api/history?limit=30<br/>Authorization: Bearer {JWT}<br/>X-User-Id: {UUID}
R->>R: JWT 검증 및 user_id 추출
R->>DB: SELECT * FROM conversation_logs<br/>WHERE user_id = (:user_id)::uuid
DB-->>R: 대화 기록 반환
R->>R: DB row를 Frontend 형식으로 변환<br/>(user/robeing 메시지 분리)
R-->>G: {"messages": [...], "has_more": true}
G-->>F: 응답 전달
F->>F: 화면에 메시지 렌더링
```
---
## 2. JWT 토큰 구조
### 2.1 auth-server 발급 토큰
```json
{
"sub": "1e16e9d5-59f3-54da-a661-8abeabff4230", // UUID
"email": "goeun2dc@gmail.com",
"name": "김종태",
"username": "happybell80",
"picture": "https://...",
"exp": 1759046456
}
```
### 2.2 중요 필드
- `sub`: 사용자 UUID (users.id)
- `username`: 사용자명
- `exp`: 만료 시간 (Unix timestamp)
---
## 3. Gateway 처리
### 3.1 JWT 검증
```python
# Gateway main.py
def verify_jwt_token(token: str):
payload = jwt.decode(token, JWT_SECRET_KEY, algorithms=["HS256"])
return payload["sub"] # UUID 반환
```
### 3.2 robeing 라우팅
```python
# workspace_members 테이블 조회
SELECT robeing_id FROM workspace_members
WHERE user_id = :user_id
# 결과: rb8001 또는 rb10508_micro
```
### 3.3 헤더 전달
```python
headers = {
"Authorization": request.headers.get("Authorization"), # JWT 전달
"X-User-Id": user_id # UUID 전달
}
```
---
## 4. rb8001 처리
### 4.1 JWT 인증
```python
# rb8001 main.py
async def get_current_user(authorization: str = Header(None)):
token = authorization.replace("Bearer ", "")
payload = jwt.decode(token, JWT_SECRET_KEY, algorithms=["HS256"])
return payload["sub"] # UUID
```
### 4.2 DB 조회
```python
# database.py
async def get_paginated_conversations(user_id: str, before: float = None, limit: int = 30):
query = """
SELECT id, message, response, timestamp
FROM conversation_logs
WHERE user_id = (:user_id)::uuid
AND robeing_id = 'rb8001'
AND (:before::timestamp IS NULL OR timestamp < :before)
ORDER BY timestamp DESC
LIMIT :limit
"""
```
### 4.3 응답 변환
```python
# DB row → Frontend 형식
messages = []
for row in results:
# User 메시지
messages.append({
"id": f"{row['id']}_user",
"text": row["message"],
"sender": "user",
"timestamp": row["timestamp"].isoformat()
})
# Robeing 응답
messages.append({
"id": f"{row['id']}_robeing",
"text": row["response"],
"sender": "robeing",
"timestamp": row["timestamp"].isoformat()
})
```
---
## 5. Frontend 처리
### 5.1 API 호출
```typescript
// robeing-api.ts
export async function getHistory(before?: string, limit: number = 30) {
const params = new URLSearchParams({ limit: limit.toString() });
if (before) params.append('before', before);
const response = await fetch(`${API_URL}/api/history?${params}`, {
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
});
return response.json();
}
```
### 5.2 무한 스크롤
```typescript
// Intersection Observer로 스크롤 감지
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && hasMore) {
const oldestMessage = messages[0];
loadMoreMessages(oldestMessage.timestamp);
}
});
```
---
## 6. 데이터베이스 스키마
### 6.1 users 테이블
```sql
CREATE TABLE users (
id UUID PRIMARY KEY,
email VARCHAR(255) UNIQUE,
username VARCHAR(50) UNIQUE,
name VARCHAR(255)
);
```
### 6.2 workspace_members 테이블
```sql
CREATE TABLE workspace_members (
user_id UUID REFERENCES users(id),
workspace_id UUID,
robeing_id VARCHAR(50) -- rb8001, rb10508_micro 등
);
```
### 6.3 conversation_logs 테이블
```sql
CREATE TABLE conversation_logs (
id INTEGER PRIMARY KEY,
robeing_id VARCHAR,
message VARCHAR,
response VARCHAR,
timestamp TIMESTAMP,
user_id UUID REFERENCES users(id)
);
```
---
## 7. 환경변수 설정
### 7.1 Gateway
```env
JWT_SECRET_KEY=9cc562b6296b87b02dd89045a2e7e11c249713a59a5ac0160d852121f1289664
DEFAULT_ROBEING_HOST=192.168.219.52
DEFAULT_ROBEING_PORT=8001
```
### 7.2 rb8001
```env
JWT_SECRET_KEY=9cc562b6296b87b02dd89045a2e7e11c249713a59a5ac0160d852121f1289664
MESSAGE_BATCH_SIZE=30
MAX_MESSAGES_IN_DOM=200
```
---
## 8. 트러블슈팅 참고
자세한 문제 해결 과정은 다음 문서 참조:
- troubleshooting/250901_rb8001_chat_history_implementation_issues.md

View File

@ -1,142 +1,29 @@
# rb8001 카톡 스타일 대화 히스토리 구현 계획 # rb8001 카톡 스타일 대화 히스토리 구현 완료
## 작성일: 2025-09-01 ## 작성일: 2025-09-01
## 완료일: 2025-09-02
## 대상: rb8001 (프로덕션 로빙) ## 대상: rb8001 (프로덕션 로빙)
## 참조: rb10508_micro 구현 코드 ## 상태: ✅ 완료
## 수정: PostgreSQL 전용 히스토리 구현으로 변경
--- ---
## 현재 상태 분석 ## 구현 완료 사항
- **rb10508_micro**: /api/config 엔드포인트 구현됨 (endpoints.py:407)
- **rb8001**:
- database.py: ConversationLog 모델 있음 (line 52)
- get_recent_conversations() 함수 있음 (line 81)
- get_paginated_conversations() 함수 없음 (추가 필요)
- main.py: @app.get() 직접 사용, router 분리 없음
## UUID 및 인증 체계 ### rb8001 백엔드
- **내부 처리**: 모든 user_id는 UUID 형식으로 통일 1. ✅ `/api/config` 엔드포인트 추가
- **JWT 토큰**: X-User-Id 헤더로 UUID 전달 2. ✅ `/api/history` 엔드포인트 추가
- **Slack 사용자**: slack_user_mapping 테이블로 UUID 매핑 3. ✅ get_paginated_conversations() 함수 구현
- **Frontend**: JWT에서 user_id(UUID) 추출하여 API 호출 4. ✅ SQL 파라미터 바인딩 문법 수정 ((:user_id)::uuid)
## 수정 필요 파일 ### Gateway
1. ✅ /api/history 라우팅 추가
2. ✅ Authorization 헤더 전달 추가
### 백엔드 (rb8001) ### Frontend
1. ✅ `app/core/config.py` - 환경변수 추가 완료 1. ✅ 이미 구현됨 (rb10508_micro 참조)
2. ✅ `app/state/database.py` - get_paginated_conversations() 추가 완료
3. `main.py` - 엔드포인트 수정 필요
- ✅ `/api/config` 이미 추가됨
- ❌ `/api/messages` 삭제 필요 (잘못 추가함)
- ⚠️ `/api/history` 추가 필요 (이것만!)
4. ✅ `.env` 파일 - 환경변수 설정 완료
### 프론트엔드 (frontend-customer)
1. ✅ 이미 완료됨 (250818 rb10508_micro에서 구현)
2. ✅ `/api/history` 호출 중 (수정 불필요)
--- ---
## API 스펙 ## 참고 문서
- troubleshooting/250901_rb8001_chat_history_implementation_issues.md
```typescript - 300_architecture/sequences/chat_history_flow.md
// GET /api/config
{ message_batch_size: 30, max_messages_in_dom: 200 }
// GET /api/history?before={timestamp}&limit=30
// Frontend 기대 형식 (robeing-api.ts:154-161)
{
messages: Array<{
id: string;
text: string;
sender: 'user' | 'robeing';
timestamp: string;
metadata?: any;
}>;
has_more: boolean;
}
// 현재 rb8001 /api/messages 반환 형식 (변환 필요!)
{
"user_message": "...",
"robeing_response": "...",
"timestamp": "...",
"user_id": "..."
}
```
### PostgreSQL 쿼리 예시
```sql
-- user_id는 항상 UUID 형식
SELECT id, message, response, timestamp, user_id
FROM conversation_logs
WHERE user_id = $1::uuid -- UUID 타입 캐스팅
AND robeing_id = 'rb8001'
AND timestamp < $2
ORDER BY timestamp DESC
LIMIT 30;
```
### API 호출 예시
```javascript
// Frontend에서 JWT의 user_id 사용
const token = localStorage.getItem('token');
const decoded = jwt_decode(token);
const userId = decoded.user_id; // UUID 형식
// API 호출 (Frontend가 이미 호출 중)
await fetch(`/api/history?before=${timestamp}&limit=30`);
```
---
## 데이터베이스 스키마
```sql
-- conversation_logs 테이블 (실제)
CREATE TABLE conversation_logs (
id INTEGER PRIMARY KEY (auto-increment),
robeing_id VARCHAR,
channel_id VARCHAR,
message VARCHAR,
response VARCHAR,
intent VARCHAR,
confidence DOUBLE PRECISION,
timestamp TIMESTAMP,
user_id UUID (FK → users),
slack_user_id VARCHAR(100),
thread_ts VARCHAR(128),
channel_type VARCHAR(32)
);
```
---
## 주의사항
### API 경로
```python
# main.py에 직접 추가 (rb8001은 router 분리 없음)
@app.get("/api/config")
@app.get("/api/messages")
```
### rb10508_micro 참조 코드
```python
# rb10508_micro/app/config.py:101-103
MESSAGE_BATCH_SIZE: int = int(os.getenv("MESSAGE_BATCH_SIZE", 30))
SCROLL_THRESHOLD: int = int(os.getenv("SCROLL_THRESHOLD", 100))
MAX_MESSAGES_IN_DOM: int = int(os.getenv("MAX_MESSAGES_IN_DOM", 200))
```
### 실제 필요 작업
1. ✅ 백엔드 대부분 완료 (bbf9c50 커밋)
2. ⚠️ **rb8001 main.py 수정 필요**:
- `/api/messages``/api/history` 이름 변경
- **응답 형식 변환 필수**:
- DB 각 row를 2개 메시지로 분리 (user, robeing)
- text, sender, timestamp 형식 맞추기
- 배열로 반환
3. ✅ Frontend 수정 불필요 (이미 /api/history 호출 중)
4. ✅ 배포는 Gitea Actions 자동 처리

View File

@ -0,0 +1,198 @@
# rb8001 대화 히스토리 구현 트러블슈팅
## 작성일: 2025-09-02
## 작성자: 51123 서버 관리자
## 대상: rb8001 카톡 스타일 대화 히스토리
## 최종 상태: ✅ 해결 완료
---
## 1. 초기 계획과 실제 구현의 차이
### 1.1 잘못된 초기 계획
- **착각**: `/api/messages` 엔드포인트 추가하려 함
- **실제**: `/api/history`가 정답 (rb10508_micro 문서 확인 미흡)
- **원인**: 250818 문서를 제대로 읽지 않음
### 1.2 놓친 문서 확인
- Frontend 응답 형식 확인 안 함 (robeing-api.ts:154-161)
- Gateway 라우팅 구조 확인 안 함
- rb10508_micro 구현 내용 상세 확인 미흡
---
## 2. Gateway 라우팅 문제
### 2.1 문제 상황
```bash
# Frontend 요청
GET /gateway/api/history?limit=1
# 응답
{} # 빈 객체 (content-length: 2)
```
### 2.2 원인 분석
- Gateway에 `/api/history` 라우팅 없었음
- `/api/config`는 있었지만 `/api/history`는 누락
### 2.3 해결
- Gateway main.py에 `/api/history` → rb8001 프록시 라우팅 추가
---
## 3. UUID 변환 문제
### 3.1 잘못된 추측
- **추측**: users 테이블 id가 integer
- **실제**: users.id는 UUID 타입
- **확인 방법**: `\d users` 명령으로 확인 필요했음
### 3.2 Gateway UUID 변환 제거
- JWT의 sub 필드는 이미 UUID
- username 변환 시도가 불필요했음
- 직접 UUID 전달로 해결
---
## 4. JWT Authorization 헤더 미전달
### 4.1 문제 상황
```python
# rb8001 응답
{"detail": "Invalid authentication credentials"} # 401 Unauthorized
```
### 4.2 원인
- Gateway가 X-User-Id 헤더만 전달
- Authorization 헤더 전달 안 함
- rb8001은 JWT 토큰 검증 필요
### 4.3 해결
```python
# Gateway main.py Line 463
headers = {
"X-User-Id": x_user_id,
"Authorization": request.headers.get("Authorization") # 추가
}
```
---
## 5. SQL 파라미터 파싱 에러
### 5.1 에러 메시지
```
syntax error at or near ":"
WHERE user_id = :user_id::uuid
```
### 5.2 원인 분석
- SQLAlchemy text()는 `:user_id`를 파라미터로 인식
- PostgreSQL `::uuid` 캐스팅과 충돌
- `:user_id::uuid` 전체를 하나의 토큰으로 파싱
### 5.3 해결 방법
```sql
-- 잘못된 문법
WHERE user_id = :user_id::uuid
-- 올바른 문법 (괄호로 분리)
WHERE user_id = (:user_id)::uuid
```
---
## 6. Frontend 응답 형식 불일치
### 6.1 Frontend 기대 형식
```typescript
{
messages: Array<{
id: string;
text: string;
sender: 'user' | 'robeing';
timestamp: string;
}>;
has_more: boolean;
}
```
### 6.2 rb8001 초기 반환 형식
```json
{
"user_message": "...",
"robeing_response": "...",
"timestamp": "..."
}
```
### 6.3 해결
- DB 각 row를 2개 메시지로 분리 (user, robeing)
- text, sender, timestamp 형식으로 변환
---
## 7. 전체 작업 순서 (실제)
### 7.1 rb8001 백엔드
1. ✅ config.py 환경변수 추가 (MESSAGE_BATCH_SIZE=30)
2. ✅ database.py get_paginated_conversations() 구현
3. ❌ `/api/messages` 잘못 추가 → ✅ `/api/history`로 수정
4. ✅ SQL 문법 수정: `(:user_id)::uuid`
### 7.2 Gateway
1. ✅ `/api/history` 라우팅 추가
2. ✅ UUID 변환 제거 (JWT sub 직접 사용)
3. ✅ Authorization 헤더 전달 추가
### 7.3 Frontend
- 수정 불필요 (이미 250818에 구현됨)
---
## 8. 교훈
### 8.1 문서 확인의 중요성
- 기존 구현 문서 먼저 상세히 읽기
- Frontend 코드 확인 후 백엔드 작업
- Gateway 라우팅 구조 먼저 파악
### 8.2 테스트 우선
- curl로 직접 테스트
- JWT 토큰 생성해서 확인
- 로그 확인 필수
### 8.3 추측 금지
- DB 스키마는 직접 확인
- 에러 메시지 정확히 읽기
- 문서화된 내용 신뢰
---
## 9. 최종 작동 확인
### 9.1 테스트 결과
```bash
# Gateway 통해 요청
GET /gateway/api/history?limit=30
Authorization: Bearer {JWT_TOKEN}
# 정상 응답
{
"messages": [...],
"has_more": true
}
```
### 9.2 Frontend
- 무한 스크롤 정상 작동
- 날짜 구분선 표시
- 대화 히스토리 로드 성공
---
## 10. 참고 문서
- plans/250901_rb8001_chat_history_implementation_plan.md (완료)
- troubleshooting/250818_happybell80_대화히스토리구현.md (rb10508_micro)
- 300_architecture/sequences/chat_history_flow.md (신규 작성)