DOCS/troubleshooting/250827_frontend_backend_preferences_API_연동_완료.md
happybell80 ac017f09d9 docs: preferences API 라우팅 완료 상태 업데이트
250827_frontend_backend_preferences_API_연동_완료.md:
- '진행중' → '완료'로 상태 변경
- Gateway 라우팅이 이미 구현되어 있음을 명확히 표시
- 작동 방식도 구현 완료로 업데이트

실제 코드 확인:
- /home/admin/robeing-gateway/app/main.py:380에 구현됨
- /api/preferences/{path:path} 라우팅 존재
2025-08-28 18:48:55 +09:00

11 KiB

Frontend-Backend Preferences API 연동 완료 - asyncpg TIME 타입 변환 해결

작성일: 2025-08-27

작성자: happybell80 / 51123 서버 관리자

상태: 완료 - 2025-08-27 23:30

영향: 사용자 설정 기능 연동

최종 업데이트: 2025-08-27 23:30


1. 현재 상황

완료된 작업

  • Backend: robeing-monitor에 preferences API 구현 (GET/PUT)
  • Frontend: localStorage → API 호출로 변경 완료
  • 배포: robeing-monitor 51124:9024에서 실행 중
  • Gateway 라우팅: /api/preferences → robeing-monitor 프록시 완료
  • PUT 500 에러: asyncpg TIME 타입 변환 문제 해결

2. 현재 구현 상태

2.1 Frontend (ActivityPanel.tsx)

  • 위치: /frontend-customer/src/components/activity-panel.tsx
  • 구현 완료: UI 컴포넌트 및 로직
  • 데이터 저장: localStorage (목업)
  • 기능:
    • 다중 브리핑 작업 관리 (task.id별)
    • 키워드 추가/삭제
    • 스케줄 설정 (매일/평일/주말/커스텀)
    • 포함 항목 선택 (이메일/뉴스/캘린더/슬랙)

2.2 Backend (user_preferences 테이블)

  • 위치: PostgreSQL main_db
  • 구조:
    CREATE TABLE user_preferences (
      id SERIAL PRIMARY KEY,
      user_id UUID REFERENCES users(id),
      slack_user_id VARCHAR(100),
      news_keywords VARCHAR(128)[],  -- 뉴스 키워드 배열
      email_filter VARCHAR(128)[],    -- 이메일 필터 (미사용)
      briefing_enabled BOOLEAN DEFAULT true,
      briefing_time TIME DEFAULT '09:00',
      updated_at TIMESTAMP DEFAULT NOW()
    );
    

2.3 rb8001 사용 현황

  • dm_skill.py: user_preferences에서 news_keywords 조회
  • 사용자별 맞춤 뉴스: 정상 작동 중
  • 브리핑 시간: briefing_time 사용 중

3. 핵심 문제점

3.1 데이터 모델 불일치

Frontend TaskSettings Backend user_preferences 불일치 내용
keywords: string[] news_keywords VARCHAR(128)[] 호환 가능
scheduleTime: string briefing_time TIME ⚠️ 타입 변환 필요
scheduleType: 'everyday' | 'weekdays' | ... - 필드 없음
scheduleDays: string[] - 필드 없음
includeEmail: boolean - 필드 없음
includeNews: boolean - 필드 없음
includeCalendar: boolean - 필드 없음
includeSlack: boolean - 필드 없음

3.2 다중 작업 관리 불가

  • Frontend: 여러 개의 scheduledTask 관리 (일일 브리핑, 주간 리포트 등)
  • Backend: 사용자당 1개 설정만 저장 가능
  • 영향: "일일 브리핑", "주간 리포트" 등 구분 불가

3.3 Mock 데이터 하드코딩

// ActivityPanel.tsx:155-210
const conversations: ConversationSession[] = [...];  // 하드코딩
const activities: ActivityLog[] = [...];            // 하드코딩  
const scheduledTasks: ScheduledTask[] = [...];      // 하드코딩
  • 실제 데이터 조회 API 없음
  • conversation_logs 테이블 조회 엔드포인트 필요

3.4 해결됨: API 구현

  • robeing-monitor: preferences API 완전 구현
  • GET /api/preferences/{user_id}: 조회
  • PUT /api/preferences/{user_id}: 업데이트
  • 고정값 반환: 미구현 필드는 기본값 제공

3.5 🔴 Gateway 라우팅 문제

# robeing-gateway/main.py:407
@app.get("/api/{path:path}")  # 모든 GET → rb8001로
# /api/preferences도 rb8001로 가서 404 발생

필요한 수정:

  • /api/preferences/* → robeing-monitor(9024)로 프록시
  • JWT에서 UUID 추출하여 전달

3.6 실시간 동기화 부재

  • localStorage 기반으로 다른 디바이스와 동기화 안됨
  • WebSocket이나 polling 구현 없음

3.7 권한 검증 누락

  • user_preferences 수정 시 본인 확인 로직 필요
  • 다른 사용자 설정 수정 가능한 보안 문제

4. 해결 방안

4.1 단기 해결책 (최소 수정)

  1. Backend 스키마 확장

    ALTER TABLE user_preferences ADD COLUMN schedule_type VARCHAR(20);
    ALTER TABLE user_preferences ADD COLUMN schedule_days VARCHAR(10)[];
    ALTER TABLE user_preferences ADD COLUMN include_email BOOLEAN DEFAULT true;
    ALTER TABLE user_preferences ADD COLUMN include_news BOOLEAN DEFAULT true;
    ALTER TABLE user_preferences ADD COLUMN include_calendar BOOLEAN DEFAULT false;
    ALTER TABLE user_preferences ADD COLUMN include_slack BOOLEAN DEFAULT false;
    
  2. robeing-monitor에 CRUD API 추가

    GET  /api/preferences/{user_id}
    PUT  /api/preferences/{user_id}
    GET  /api/conversations/{user_id}?limit=10
    GET  /api/activities/{user_id}?limit=10
    
  3. ActivityPanel localStorage → API 호출 변경

4.2 중기 해결책 (구조 개선)

  1. scheduled_tasks 테이블 생성

    CREATE TABLE scheduled_tasks (
      id SERIAL PRIMARY KEY,
      user_id UUID REFERENCES users(id),
      task_type VARCHAR(50),  -- 'daily_briefing', 'weekly_report' 등
      title VARCHAR(255),
      schedule_type VARCHAR(20),
      schedule_days VARCHAR(10)[],
      schedule_time TIME,
      settings JSONB,  -- 유연한 설정 저장
      enabled BOOLEAN DEFAULT true
    );
    
  2. Frontend와 Backend 인터페이스 통일

    • 공통 TypeScript 타입 정의
    • API 응답 형식 표준화

4.3 장기 해결책 (완전 재설계)

  1. 마이크로서비스 분리

    • user-preferences-service 별도 구현
    • GraphQL 도입 검토
  2. 실시간 동기화

    • WebSocket 구현
    • 설정 변경 시 실시간 반영

5. 구현 현황 (2025-08-27 22:00)

완료

  • Backend API: robeing-monitor에 구현 완료
  • Frontend 수정: API 호출 코드 추가, 자동 저장
  • UI 처리: 미구현 필드 비활성화 (opacity-50)
  • 배포: 51124:9024 정상 작동

완료 (Gateway 라우팅 구현됨)

  • Gateway 라우팅: preferences 전용 프록시 구현 완료 (line 380)

작동 방식 (구현 완료)

  • 구현됨: Frontend → Gateway(8100) → robeing-monitor(9024)

6. 아키텍처 상세

데이터 흐름

Frontend (브라우저)
    ↓ 
[자동 저장 트리거]
    ↓
fetch('/api/preferences/{UUID}')  # Gateway 경유
    ↓
Gateway (8100) + JWT 인증
    ↓
[라우팅 필요: preferences → 9024]
    ↓
robeing-monitor (9024)
    ↓
PostgreSQL user_preferences 테이블

UUID 체계

  • JWT sub: UUID (1e16e9d5-59f3-54da-a661-8abeabff4230)
  • localStorage: user_id에 UUID 저장
  • API 경로: /api/preferences/{UUID}

7. 관련 파일

Frontend

  • /home/happybell/projects/ivada/frontend-customer/src/components/activity-panel.tsx

Backend

  • /home/happybell/projects/ivada/robeing-monitor/app/api/items.py
  • /home/happybell/projects/ivada/robeing-gateway/app/main.py
  • /home/happybell/projects/ivada/rb8001/app/skills/dm_skill.py

문서

  • /home/happybell/projects/ivada/DOCS/troubleshooting/250826_slack_id_column_standardization.md

8. Gateway 라우팅 추가 코드

# /robeing-gateway/app/main.py (line 407 앞에 추가)
@app.api_route("/api/preferences/{path:path}", methods=["GET", "PUT"])
async def proxy_preferences(
    path: str, 
    request: Request,
    x_user_id: str = Depends(get_verified_user)
):
    """Proxy preferences to robeing-monitor"""
    monitor_url = "http://192.168.219.52:9024"
    full_path = f"/api/preferences/{path}"
    
    if request.method == "GET":
        response = await http_client.get(f"{monitor_url}{full_path}")
    elif request.method == "PUT":
        body = await request.json()
        response = await http_client.put(
            f"{monitor_url}{full_path}", 
            json=body,
            headers={"X-User-Id": x_user_id}
        )
    
    return response.json() if response.status_code == 200 else {"error": "Failed"}

9. 결론

100% 완료 - 전체 기능 정상 작동

  • Backend API
  • Frontend 수정
  • Gateway 라우팅
  • nginx 프록시 (/gateway/ 사용)
  • JWT 인증
  • GET 요청 (200 OK)
  • PUT 요청 (200 OK - TIME 타입 변환 해결)

10. 구현 과정 (2025-08-27)

Phase 1: Backend API 구현 (20:00)

  • robeing-monitor에 preferences API 추가
  • 기존 컬럼만 사용 (news_keywords, briefing_time)
  • 확장 필드는 고정값 반환

Phase 2: Frontend 수정 (21:00)

  • localStorage → API 호출 변경
  • 자동 저장 방식 구현
  • 미구현 필드 UI 비활성화

Phase 3: Gateway 라우팅 (22:00)

  • preferences 전용 라우팅 추가
  • robeing-monitor(9024)로 프록시

Phase 4: 경로 문제 해결 (22:30)

  • 처음: 직접 호출 (http://192.168.219.52:9024) - Mixed Content 에러
  • 다음: /api/preferences - nginx가 frontend-base(8000)로 전달
  • 해결: /gateway/api/preferences - Gateway(8100) 경유

Phase 5: JWT 인증 추가 (22:40)

  • Frontend에서 Authorization 헤더 누락 발견
  • localStorage의 'token' || 'auth_token' 사용
  • Bearer 토큰 형식으로 전달

최종 상태 (23:30)

  • GET /api/preferences/{user_id}: 200 OK
  • PUT /api/preferences/{user_id}: 200 OK
  • 브리핑 시간 표시: "09:00" 정상 표시

10. PUT 500 에러 해결 과정 (22:45 ~ 23:30)

추가 해결: 브리핑 시간 표시 문제 (01:10)

  • 문제: DB 09:00:00 → Frontend 00:00 표시
  • 원인: TIME 형식 파싱 오류
  • 해결: activity-panel.tsx line 104
// Backend에서 "09:00:00" 반환 → Frontend에서 "09:00"로 변환
scheduleTime: data.briefing_time ? data.briefing_time.substring(0, 5) : '09:00'
  • 결과: 정상 표시 완료

11. 최종 해결 내역

문제 1: 존재하지 않는 컬럼 참조

  • 원인: INSERT/UPDATE 쿼리가 DB에 없는 컬럼 참조
  • 해결: schedule_type, schedule_days, include_* 컬럼 제거

문제 2: Frontend 필드명 불일치

  • 원인: Frontend(keywords) vs Backend(news_keywords)
  • 해결: 필드명 자동 매핑 구현

문제 3: TIME 타입 변환 실패 (핵심)

  • 원인: asyncpg에서 문자열 + ::time 캐스팅 처리 실패
  • 잘못된 방법:
"briefing_time = $2::time"
params.append("08:30:00")  # 문자열
  • 올바른 방법:
from datetime import datetime
time_obj = datetime.strptime("08:30:00", "%H:%M:%S").time()
"briefing_time = $2"  # ::time 제거
params.append(time_obj)  # datetime.time 객체
  • 핵심: asyncpg는 Python datetime.time 객체를 PostgreSQL TIME으로 자동 변환

최종 수정: 2025-08-27 23:30 상태: 완전 해결 - Frontend-Backend preferences 연동 완료