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

## 작성일: 2025-08-27
## 작성자: happybell80 / 51123 서버 관리자
## 상태: 🟡 부분 구현 (2025-08-27 완료, 2025-09-14 스키마 불일치로 오류)
## 영향: 사용자 설정 기능 연동
## 최종 업데이트: 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_preference 테이블)
- **위치**: PostgreSQL main_db
- **구조**:
  ```sql
  CREATE TABLE user_preference (
    id SERIAL PRIMARY KEY,
    user_id UUID REFERENCES user(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_preference에서 news_keywords 조회
- **사용자별 맞춤 뉴스**: 정상 작동 중
- **브리핑 시간**: briefing_time 사용 중

---

## 3. 핵심 문제점

### 3.1 데이터 모델 불일치

| Frontend TaskSettings | Backend user_preference | 불일치 내용 |
|---------------------|------------------------|------------|
| 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_log 테이블 조회 엔드포인트 필요

### 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 ✅ 권한 검증 구현 완료 (2025-09-15 해결)

#### 확인된 사실 (2025-09-15)

**전체 플로우:**
1. **Frontend** (`/home/admin/frontend-customer/src/components/activity-panel.tsx`)
   - Line 85: `GET ${API_BASE}/api/preferences/${userId}`
   - Line 155: `PUT ${API_BASE}/api/preferences/${userId}`
   - userId는 localStorage에서 읽음 (Line 76)
   - Authorization 헤더에 Bearer 토큰 포함 (Line 87, 158)

2. **Gateway** (`/home/admin/robeing-gateway/app/main.py`) - 51123:8100
   - Line 365-391: `/api/preferences/{path}` 엔드포인트
   - Line 369: `user_uuid = Depends(get_verified_user)` - JWT에서 UUID 추출
   - Line 375-376 (GET): `http://192.168.219.52:9024/api/preferences/{path}` 프록시, 헤더 없음
   - Line 377-382 (PUT):
     - robeing-monitor로 프록시
     - `X-User-Id: user_uuid` 헤더 추가 (Line 382)
   - **문제점**: URL의 {path}와 JWT의 user_uuid 비교 없음

3. **robeing-monitor** (`/home/admin/ivada_project/robeing-monitor/app/api/monitor.py`) - 51124:9024
   - Line 129: `async def get_preferences(user_id: str):`
   - Line 194: `async def update_preferences(user_id: str, preferences: UserPreferencesUpdate):`
   - 파라미터: URL의 {user_id}만 사용, Request 객체 없음
   - **문제점**: JWT/헤더 검증 불가능

#### ✅ 해결 완료: JWT.sub = URL.user_id 검증 구현

**해결된 문제**: 타인 데이터 접근 차단
- Gateway: Line 372-374에 `if path != user_uuid: raise 403` 추가
- robeing-monitor: commit f3b0235로 헤더 검증 추가

**테스트 결과 (2025-09-15):**
```bash
# TEST 1: 본인 데이터 접근 → 성공
GET /api/preferences/53529291-5050-4daa-89fb-008b546feb63
Authorization: Bearer [JWT with sub=53529291... (happybell80)]
Response: 200 OK (또는 500 서비스 문제)

# TEST 2: 타인 데이터 접근 시도 → 차단 ✅
GET /api/preferences/b6ea2ee0-a15a-5cf4-93a9-a9ca20d4c4a0
Authorization: Bearer [JWT with sub=53529291... (happybell80)]
Response: 403 Forbidden - "Can only access your own preferences"
```


#### 관련 서비스 JWT 검증 현황 (2025-09-15 확인)

**JWT 토큰 발급:**
- auth-server: JWT sub에 user.id(UUID) 사용 (`/home/admin/auth-server/app/providers/gmail.py` L198~, `slack.py` L329)
- 알고리즘: HS256, 만료: 30일 (`/home/admin/auth-server/app/core/auth.py`)
- Gmail OAuth JWT: `sub`(user UUID), `username`, `email`, `name`, `exp`(만료시간), `iat`(발급시간)
- Slack OAuth JWT: `sub`(user UUID), `email`, `name`, `username`, `picture`(프로필 이미지), `slack_user_id`, `slack_team_id`, `exp`, `iat`

**JWT 검증 구현된 서비스:**
- Gateway: 모든 주요 엔드포인트에서 `Depends(get_verified_user)` 사용
- rb8001: `/api/message`에서 JWT 검증, sub(UUID)를 user_id로 사용

**JWT.sub 매칭 없는 취약 엔드포인트:**
- Gateway: `/api/workspace/{user_id}`, `/api/workspace/assign` - Depends(get_verified_user) 없음
- robeing-monitor: Preferences API (`/api/preferences/{user_id}`) - 경로 user_id만 사용
- robeing-monitor: Gmail Items API - X-User-Id와 경로 user_id 일치만 확인, JWT.sub 매칭 없음
- rb10508_micro: `/memories/{user_id}`, `/user/{user_id}/history` - JWT 검증 없음

---

## 4. 해결 방안

### 4.1 단기 해결책 (최소 수정)
1. **Backend 스키마 확장**
   ```sql
   ALTER TABLE user_preference ADD COLUMN schedule_type VARCHAR(20);
   ALTER TABLE user_preference ADD COLUMN schedule_days VARCHAR(10)[];
   ALTER TABLE user_preference ADD COLUMN include_email BOOLEAN DEFAULT true;
   ALTER TABLE user_preference ADD COLUMN include_news BOOLEAN DEFAULT true;
   ALTER TABLE user_preference ADD COLUMN include_calendar BOOLEAN DEFAULT false;
   ALTER TABLE user_preference 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 user(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_preference 테이블
```

### 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 연동 완료*
