# 카톡 스타일 대화 히스토리 구현 **날짜**: 2025-08-18 **작업자**: happybell80 & Claude **관련 프로젝트**: rb10508_micro, frontend-customer ## 오후 12시 00분 - 요구사항 분석 ### 사용자 요구사항 - 카카오톡처럼 대화 히스토리를 볼 수 있도록 구현 - 날짜 구분선 표시 (오늘, 어제, 날짜 형식) - 무한 스크롤로 이전 대화 로드 - 읽지 않은 메시지나 대화방 개념은 불필요 ### 시스템 구조 파악 - 독립적인 로빙들 (rb8001, rb10508_micro, rb10408) - 로그인 시 사용자별 로빙 배정 - Gateway가 라우팅 (프론트엔드 → Gateway → 로빙) - 각 로빙이 독립적인 ChromaDB 보유 ## 오후 12시 30분 - 백엔드 구현 ### 1. 환경변수 설정 추가 ```python # app/config.py 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)) ``` ### 2. API 엔드포인트 구현 ```python # app/api/endpoints.py @router.get("/messages") # 페이지네이션된 메시지 조회 @router.get("/config") # 프론트엔드 설정 동기화 ``` **함수형 프로그래밍 원칙 준수**: - 환경변수로 설정 관리 (하드코딩 없음) - 순수 함수로 구현 - 불변성 유지 ## 오후 1시 00분 - 프론트엔드 구현 ### 1. robeing-api.ts 확장 - `getConfig()`: 백엔드 설정 가져오기 - `getMessages()`: 페이지네이션 지원 메시지 조회 ### 2. ChatInterface 컴포넌트 개선 - Intersection Observer로 무한 스크롤 - 날짜 구분선 렌더링 로직 - 기존 파일 수정만으로 구현 (새 파일 생성 없음) ## 오후 1시 15분 - API 경로 문제 발견 ### 문제 - API 호출 시 404 Not Found - `/rb10508/api/config` 접근 불가 ### 원인 ```python # app/main.py app.include_router(api_router, prefix="/api") # 라우터 프리픽스 # app/api/endpoints.py @router.get("/api/config") # 잘못된 경로 (중복) ``` 결과: `/api` + `/api/config` = `/api/api/config` ### 해결 ```python @router.get("/config") # 올바른 경로 @router.get("/messages") # 올바른 경로 ``` ## 교훈 ### 1. **라우터 프리픽스 확인 필수** - 엔드포인트 추가 전 main.py에서 라우터 등록 방식 확인 - 프리픽스와 엔드포인트 경로 중복 주의 ### 2. **함수형 프로그래밍 원칙** - 설정값 하드코딩 금지 → 환경변수 사용 - 새 파일 생성 최소화 → 기존 파일 수정 - 코드 재사용성 확인 ### 3. **테스트 환경 관리** - 로컬 포트 충돌 주의 (서버 포트와 겹치지 않도록) - 테스트 파일은 즉시 삭제 - 불필요한 의존성 추가 금지 (Playwright 같은) ### 4. **Git 커밋 원칙** - `git add .` 사용 (선택적 add 대신) - 의존성 변경은 신중하게 검토 ## 최종 결과 ### 구현된 기능 1. **무한 스크롤**: 위로 스크롤 시 이전 메시지 30개씩 로드 2. **날짜 구분선**: "오늘", "어제", "2024년 12월 25일 월요일" 형식 3. **설정 동기화**: 백엔드에서 배치 크기 등 설정 제공 ### 배포 상태 - rb10508_micro: b0003cd (API 경로 수정) - frontend-customer: c3a38e7 (카톡 스타일 UI) ### API 엔드포인트 - GET `/rb10508/api/config` - 설정 조회 - GET `/rb10508/api/messages?before={timestamp}&limit={number}` - 메시지 조회 ## 오후 1시 30분 - 사용자 매핑 문제 발견 ### 문제 1: Username 변환 누락 - API가 `user_id="default_user"`로 검색 - 실제 데이터는 `rb10508_test_happybell80_episodic` 컬렉션에 저장 - `search_memories`가 username 파라미터를 받지 못함 ### 해결 ```python # app/config.py에 매핑 테이블 추가 USER_MAPPING: dict = { "default_user": "happybell80", "U0925SXQFDK": "happybell80", # Slack ID "goeun2dc@gmail.com": "happybell80", # Email } # app/api/endpoints.py에 헬퍼 함수 추가 def resolve_username(user_id: str) -> str: """user_id를 실제 username으로 변환""" if user_id in settings.USER_MAPPING: return settings.USER_MAPPING[user_id] if "_user" in user_id: return user_id.replace("_user", "") return user_id ``` ## 오후 1시 45분 - UUID vs Username 문제 ### 문제 2: ChromaDB where 조건 불일치 - Slack 저장 시: `user_id = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"` (UUID) - 프론트 검색 시: `user_id = "happybell80"` (username) - where 조건 `{"user_id": user_id}` 일치하지 않음 ### 근본 원인 발견 **2025년 8월 9일의 잘못된 결정이 문제의 시작**: ```sql -- 잘못된 예: 테스트용 UUID 하드코딩 (250809_happybell80_robeing-gateway구현.md) INSERT INTO user (id, email, name) VALUES ('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'::uuid, 'goeun2dc@gmail.com', '김종태'), ('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb'::uuid, '0914eagle@gmail.com', '전희재'), ('cccccccc-cccc-cccc-cccc-cccccccccccc'::uuid, 'hhyong91@gmail.com', '황한용'); -- 올바른 방법: gen_random_uuid() 사용 INSERT INTO user (id, email, name, username) VALUES (gen_random_uuid(), 'goeun2dc@gmail.com', '김종태', 'happybell80'); -- 실제 UUID 생성: 'e7a9f3c2-8b4d-4f2e-a1b3-9c8d7e6f5a4b' ``` ### 문제의 연쇄 반응 1. **테스트 데이터가 프로덕션에**: 임시 UUID가 영구 사용 2. **프론트엔드 혼란**: user_id 불명확 → "default_user" 하드코딩 3. **ChromaDB 컬렉션명 혼란**: UUID? username? email? 4. **매핑 지옥**: 여러 식별자 연결하는 복잡한 시스템 필요 ### 왜 이런 실수를 했나 - OAuth 로그인 시 UUID 자동 생성 대신 - 개발 편의를 위해 알아보기 쉬운 UUID 사용 - aaaa..., bbbb..., cccc... 패턴으로 테스트 - username 시스템은 나중에 급하게 추가 (8월 9일) ### 해결 ```python # app/core/memory/storage.py 수정 # username으로 검색하도록 where 조건 변경 where_clause = {"username": username} if username else {"user_id": user_id} results = collection.query( query_texts=[query], n_results=n_results, where=where_clause ) ``` ## 오후 2시 00분 - resolve_username 함수 개선 ### 기존 계획과의 연계 - 250812_slack_user_mapping_구현.md에 이미 Auth 서버 API 구현됨 - `/api/slack/mapping/{slack_user_id}` 엔드포인트 활용 가능 - 프론트엔드 연동 부분만 누락되어 있었음 ### 통합 사용자 식별 시스템 구현 ```python async def resolve_username(user_id: str) -> str: """모든 형태의 user_id를 username으로 해석""" # 1. Slack ID (U로 시작) -> Auth 서버 API 호출 if user_id.startswith('U') and len(user_id) > 8: response = await client.get(f"{AUTH_SERVER_URL}/api/slack/mapping/{user_id}") if response.status_code == 200: return response.json()['username'] # 2. 이메일 (@포함) -> @ 앞부분 추출 if '@' in user_id: username = user_id.split('@')[0] if user_id in USER_MAPPING: # 알려진 이메일 확인 return USER_MAPPING[user_id] return username # 3. 로컬 매핑 테이블 # 4. "_user" 접미사 제거 # 5. 원본 반환 ``` ### 개선 효과 - Slack, 이메일, username 모든 형태 지원 - Auth 서버와 통합하여 중앙 관리 - 5단계 우선순위로 안정적 해석 ## 오후 4시 50분 - 대화 히스토리와 메모리 검색 분리 ### 근본 문제 발견 - **설계 결함**: `/api/messages`가 히스토리 조회와 의미 검색을 같은 함수로 처리 - **부적절한 의존성**: 단순 히스토리 조회에 LLM(Mistral) 필요 없음 - **실수의 원인**: `search_memories()` 함수 재사용 시도 → 빈 query도 임베딩 생성 → LLM 호출 ### 해결책: 완전 분리 1. **`get_chat_history()` 함수 신규 개발** ```python async def get_chat_history( username: str, limit: int = None, before: Optional[str] = None, after: Optional[str] = None ) -> List[Dict]: """LLM 없이 ChromaDB에서 직접 대화 조회""" # collection.get() 사용 (query 없음) # timestamp 기준 정렬 # 페이지네이션 지원 ``` 2. **엔드포인트 교체** - 삭제: `/api/messages` (잘못된 설계) - 신규: `/api/history` (히스토리 전용) - 유지: `/api/search` (의미 검색용 - 향후) 3. **프론트엔드 수정** ```javascript // 변경 전: /api/messages // 변경 후: /api/history const response = await fetch(`${ROBEING_API_URL}/api/history?${params}`) ``` ### 함수형 프로그래밍 원칙 준수 - 환경변수 활용: `MESSAGE_BATCH_SIZE`, `SCROLL_THRESHOLD` - 순수 함수: 부작용 없이 데이터만 조회 - 의존성 주입: collection을 파라미터로 전달 - 비동기 처리로 성능 유지 ## 교훈 ### 1. **"코드 재사용"의 함정** - 목적이 다른 함수를 억지로 재사용하면 복잡도만 증가 - 히스토리 조회 ≠ 의미 검색 (완전히 다른 레이어) - 처음부터 전용 함수를 만드는 것이 더 깔끔 ### 2. **LLM 의존성 최소화** - 단순 CRUD 작업에 AI 불필요 - 임베딩 생성은 검색이 필요할 때만 - 대화 히스토리는 timestamp 정렬이면 충분 ### 3. **명확한 책임 분리** | 기능 | 엔드포인트 | LLM 필요 | 용도 | |------|-----------|---------|------| | 히스토리 | /api/history | X | 시간순 대화 조회 | | 검색 | /api/search | O | 의미 기반 검색 | | 대화 | /api/chat | O | 새 대화 생성 | ### 4. **함수형 프로그래밍 효과** - 환경변수로 설정 관리 → 하드코딩 제거 - 순수 함수 구현 → 테스트 용이 - 명확한 입출력 → 디버깅 간편 ## 최종 결과 ### 구현 완료 - `get_chat_history()`: LLM 없는 순수 조회 함수 - `/api/history`: 대화 히스토리 전용 엔드포인트 - 프론트엔드 경로 수정 완료 ### 개선 효과 - **MISTRAL_API_KEY 없어도 정상 작동** - 응답 속도 대폭 개선 (임베딩 생성 없음) - 명확한 아키텍처로 유지보수 용이 ## 오후 5시 20분 - Gateway DB 연결 문제 해결 ### 문제 상황 - Gateway가 PostgreSQL 인증 실패: `password authentication failed for user "robeings"` - 워크스페이스 정보를 가져올 수 없어 기본 로빙만 사용 - DATABASE_URL 환경변수의 비밀번호가 잘못됨 ### 원인 분석 ```yaml # docker-compose.yml 기본값 (틀림) DATABASE_URL=postgresql+asyncpg://postgres:postgres@host.docker.internal:5432/main_db # 실제 필요한 값 DATABASE_URL=postgresql+asyncpg://robeings:robeings@host.docker.internal:5432/main_db ``` ### 해결 과정 (51123 서버) 1. **환경변수 수정** ```bash cd /home/admin/ivada_project/robeing-gateway echo 'DATABASE_URL=postgresql+asyncpg://robeings:robeings@host.docker.internal:5432/main_db' > .env ``` 2. **컨테이너 재시작** ```bash docker-compose down docker-compose up -d ``` 3. **연결 확인** ```bash docker logs robeing-gateway --tail 50 # "Database connection established successfully" 확인 # "All required tables found in main_db" 확인 ``` ### 해결 결과 - ✅ DB 연결 성공 - ✅ 워크스페이스 조회 정상 작동 - ✅ 사용자별 로빙 할당 가능 - ✅ 전체 시스템 정상화 ## 오후 2시 07분 - 프론트엔드 localStorage 문제 해결 ### 문제 발견 - `getMessages` API가 계속 'default_user'로 요청 - 실제 데이터는 `rb10508_test_happybell80_episodic`에 있음 - localStorage.getItem('user_id')가 null 반환 ### 근본 원인 ```javascript // src/services/robeing-api.ts const userId = localStorage.getItem('user_id') || 'default_user'; // 항상 default_user // src/contexts/auth-context.tsx setUser({ id: username, // username 설정하지만 // localStorage에 저장하지 않음! }); ``` ### 해결 ```javascript // auth-context.tsx 수정 // 1. 토큰 복원 시 localStorage.setItem('user_id', username); // 2. 로그인 성공 시 const username = data.username || data.user_id || 'unknown'; localStorage.setItem('user_id', username); // 3. 로그아웃 시 localStorage.removeItem('user_id'); ``` ### 확인 사항 - UUID가 아닌 username (예: "happybell80") 사용 - localStorage에 올바른 user_id 저장 - API 요청 시 X-User-Id 헤더에 실제 username 전송 ## 교훈 (추가) ### 5. **User ID 체계 통일 필수** - UUID, username, email 3가지 혼재 문제 - 각 시스템이 다른 ID 사용하여 반복적 오류 - 트러블슈팅 문서 확인 습관 필요 ### 6. **ChromaDB 메타데이터 일관성** - 저장 시와 검색 시 키 일치 확인 - username vs user_id 명확히 구분 - where 조건 디버깅 로그 추가 권장 ### 7. **테스트 데이터를 프로덕션에 사용 금지** - 개발 편의를 위한 하드코딩 UUID 사용 금지 - 항상 `gen_random_uuid()` 같은 실제 함수 사용 - 테스트 데이터는 명확히 구분하고 제거 ### 8. **PostgreSQL UUID 올바른 사용법** ```sql -- ❌ 잘못된 방법: 하드코딩 INSERT INTO user (id) VALUES ('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'); -- ✅ 올바른 방법: 함수 사용 INSERT INTO user (id) VALUES (gen_random_uuid()); -- 또는 테이블 정의 시 DEFAULT 설정 CREATE TABLE user ( id UUID PRIMARY KEY DEFAULT gen_random_uuid() ); ``` ### 9. **상태 관리와 영속성 일치 필수** - React state에 저장한 값은 localStorage에도 동기화 - API 요청 시 사용하는 값은 반드시 영속 저장소에 보관 - 로그인/로그아웃 시 모든 관련 데이터 정리 ## 오후 2시 15분 - POST /api/message 엔드포인트 username 미전달 ### 문제 - GET /api/messages (조회)는 정상 작동 - POST /api/message (저장)가 default 컬렉션에 저장 - think_functional 호출 시 username 전달 안됨 ### 해결 ```python # endpoints.py 수정 username = await resolve_username(request.user_id) # lambda 함수 수정 - brain.py 호출 패턴과 일치 search_memories_fn=lambda query, user_id: search_memories( query, user_id, username=username ), ``` ### 주의사항 - brain.py가 keyword arguments로 호출 (`query=`, `user_id=`) - lambda 파라미터 순서가 호출 패턴과 일치해야 함 - 예전에도 같은 실수 반복 - 트러블슈팅 문서 확인 필수 ## 오후 2시 20분 - /api/messages sender 매핑 오류 ### 문제 - ChromaDB에 role: "assistant"로 저장 - 프론트엔드는 sender: "robeing" 기대 - 모든 메시지가 sender: "user"로 표시 ### 해결 ```python # endpoints.py 수정 role = metadata.get('role', 'user') sender = 'robeing' if role == 'assistant' else 'user' messages.append({ "sender": sender, # role을 sender로 변환 }) ``` ## 오후 2시 30분 - 프론트엔드 초기 로드 실패 ### 문제 분석 - 백엔드는 정상 (94개 대화 반환) - 프론트엔드가 페이지 로드 시 getMessages 호출 안함 - historyLoaded 플래그가 너무 일찍 true 설정 ### 근본 원인 ```javascript // 문제의 코드 useEffect(() => { if (historyLoaded) return; // user 로드 전에 실행되면 영원히 막힘 // ... }, [isAuthenticated, user]); ``` ### 해결 ```javascript // 1. 중복 체크 개선 if (messages.length > 0 && historyLoaded) return; // 2. 로그인 확인 강화 } else if (user && isAuthenticated) { // 3. user_id 우선순위 수정 const userId = localStorage.getItem('user_id') || user.id || 'default'; // 4. config 대기하지 않음 const batchSize = config?.message_batch_size || 30; ``` ### 타이밍 문제 해결 - user가 null일 때 historyLoaded 설정 방지 - localStorage의 user_id 우선 사용 - config 로드 기다리지 않고 기본값으로 진행