# ✅ 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 - **구조**: ```sql 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 데이터 하드코딩 ```typescript // 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 라우팅 문제 ```python # 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 스키마 확장** ```sql 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 추가** ```python 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 테이블 생성** ```sql 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 라우팅 추가 코드 ```python # /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 ```javascript // 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 캐스팅 처리 실패 - **잘못된 방법**: ```python "briefing_time = $2::time" params.append("08:30:00") # 문자열 ``` - **올바른 방법**: ```python 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 연동 완료*