DOCS/troubleshooting/250818_happybell80_대화히스토리구현.md
happybell80 bb8900300a docs: auth_db를 main_db로 일괄 변경
- 모든 문서에서 auth_db 참조를 main_db로 업데이트
- 데이터베이스 이름 변경 반영
- 트러블슈팅 및 아키텍처 문서 수정

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-20 23:21:20 +09:00

15 KiB

카톡 스타일 대화 히스토리 구현

날짜: 2025-08-18
작업자: happybell80 & Claude
관련 프로젝트: rb10508_micro, frontend-customer

오후 12시 00분 - 요구사항 분석

사용자 요구사항

  • 카카오톡처럼 대화 히스토리를 볼 수 있도록 구현
  • 날짜 구분선 표시 (오늘, 어제, 날짜 형식)
  • 무한 스크롤로 이전 대화 로드
  • 읽지 않은 메시지나 대화방 개념은 불필요

시스템 구조 파악

  • 독립적인 로빙들 (rb8001, rb10508_micro, rb10408)
  • 로그인 시 사용자별 로빙 배정
  • Gateway가 라우팅 (프론트엔드 → Gateway → 로빙)
  • 각 로빙이 독립적인 ChromaDB 보유

오후 12시 30분 - 백엔드 구현

1. 환경변수 설정 추가

# 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 엔드포인트 구현

# app/api/endpoints.py
@router.get("/messages")  # 페이지네이션된 메시지 조회
@router.get("/config")    # 프론트엔드 설정 동기화

함수형 프로그래밍 원칙 준수:

  • 환경변수로 설정 관리 (하드코딩 없음)
  • 순수 함수로 구현
  • 불변성 유지

오후 1시 00분 - 프론트엔드 구현

1. robing-api.ts 확장

  • getConfig(): 백엔드 설정 가져오기
  • getMessages(): 페이지네이션 지원 메시지 조회

2. ChatInterface 컴포넌트 개선

  • Intersection Observer로 무한 스크롤
  • 날짜 구분선 렌더링 로직
  • 기존 파일 수정만으로 구현 (새 파일 생성 없음)

오후 1시 15분 - API 경로 문제 발견

문제

  • API 호출 시 404 Not Found
  • /rb10508/api/config 접근 불가

원인

# app/main.py
app.include_router(api_router, prefix="/api")  # 라우터 프리픽스

# app/api/endpoints.py
@router.get("/api/config")  # 잘못된 경로 (중복)

결과: /api + /api/config = /api/api/config

해결

@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 파라미터를 받지 못함

해결

# 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일의 잘못된 결정이 문제의 시작:

-- 잘못된 예: 테스트용 UUID 하드코딩 (250809_happybell80_robing-gateway구현.md)
INSERT INTO users (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 users (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일)

해결

# 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} 엔드포인트 활용 가능
  • 프론트엔드 연동 부분만 누락되어 있었음

통합 사용자 식별 시스템 구현

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() 함수 신규 개발
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 기준 정렬
    # 페이지네이션 지원
  1. 엔드포인트 교체
  • 삭제: /api/messages (잘못된 설계)
  • 신규: /api/history (히스토리 전용)
  • 유지: /api/search (의미 검색용 - 향후)
  1. 프론트엔드 수정
// 변경 전: /api/messages
// 변경 후: /api/history
const response = await fetch(`${ROBING_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 환경변수의 비밀번호가 잘못됨

원인 분석

# 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. 환경변수 수정
cd /home/admin/ivada_project/robeing-gateway
echo 'DATABASE_URL=postgresql+asyncpg://robeings:robeings@host.docker.internal:5432/main_db' > .env
  1. 컨테이너 재시작
docker-compose down
docker-compose up -d
  1. 연결 확인
docker logs robing-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 반환

근본 원인

// src/services/robing-api.ts
const userId = localStorage.getItem('user_id') || 'default_user';  // 항상 default_user

// src/contexts/auth-context.tsx
setUser({
  id: username,  // username 설정하지만
  // localStorage에 저장하지 않음!
});

해결

// 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 올바른 사용법

-- ❌ 잘못된 방법: 하드코딩
INSERT INTO users (id) VALUES ('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa');

-- ✅ 올바른 방법: 함수 사용
INSERT INTO users (id) VALUES (gen_random_uuid());
-- 또는 테이블 정의 시 DEFAULT 설정
CREATE TABLE users (
    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 전달 안됨

해결

# 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: "robing" 기대
  • 모든 메시지가 sender: "user"로 표시

해결

# endpoints.py 수정
role = metadata.get('role', 'user')
sender = 'robing' if role == 'assistant' else 'user'
messages.append({
    "sender": sender,  # role을 sender로 변환
})

오후 2시 30분 - 프론트엔드 초기 로드 실패

문제 분석

  • 백엔드는 정상 (94개 대화 반환)
  • 프론트엔드가 페이지 로드 시 getMessages 호출 안함
  • historyLoaded 플래그가 너무 일찍 true 설정

근본 원인

// 문제의 코드
useEffect(() => {
  if (historyLoaded) return;  // user 로드 전에 실행되면 영원히 막힘
  // ...
}, [isAuthenticated, user]);

해결

// 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 로드 기다리지 않고 기본값으로 진행