Skip to content

Latest commit

 

History

History
663 lines (537 loc) · 17 KB

File metadata and controls

663 lines (537 loc) · 17 KB

현재 구현 상태 (Current Implementation Status)

최종 업데이트: 2025-11-04 버전: v1.3 - Streaming + Database Persistence


📋 목차 (Table of Contents)

  1. 완료된 주요 기능
  2. 시스템 아키텍처
  3. 코드 플로우
  4. 환경 변수
  5. 실행 중인 서비스
  6. 모바일 앱 구조
  7. 다음 단계

✅ 완료된 주요 기능

1. 실시간 스트리밍 플랜 생성 (Real-time Streaming Plan Generation)

구현일: 2025-11-04

기능:

  • ChatGPT 스타일의 점진적 결과 표시
  • Server-Sent Events (SSE) 프로토콜 사용
  • 실시간 진행률 표시 (0% → 100%)
  • 한글 상태 메시지
  • LLM 생성 과정 실시간 표시

파일:

services/llm-agent/src/routes/langgraph_plans.py   # SSE endpoint
services/llm-agent/src/services/langgraph_planner.py # Streaming logic
mobile/src/api/streaming.ts                         # Mobile SSE client
mobile/src/screens/CreatePlanScreen.tsx             # UI with streaming

엔드포인트:

POST http://localhost:8000/api/v1/plans/generate/stream
Content-Type: text/event-stream

이벤트 타입:

  • status: 현재 작업 상태 (예: "날씨 정보 수집 중...")
  • progress: 진행률 업데이트
  • chunk: LLM 생성 내용 조각
  • complete: 최종 플랜 완성
  • error: 오류 발생
  • done: 스트림 종료

2. Redis 캐싱 (Redis Caching)

구현일: 2025-11-04

기능:

  • 동일한 요청에 대한 즉시 응답 (<1초)
  • 1시간 TTL (Time To Live)
  • 99% 비용 절감 효과

캐시 키 형식:

plan:{location}:{startDate}:{endDate}:{budget}
예: plan:Seoul:2025-11-10:2025-11-12:medium

성능:

  • 첫 생성: ~6초 (LLM 호출)
  • 캐시 히트: <1초 (Redis 조회)

3. 데이터베이스 영속성 (Database Persistence)

구현일: 2025-11-04

기능:

  • PostgreSQL에 플랜 저장
  • 사용자별 플랜 관리
  • 앱 재시작 후에도 플랜 유지
  • CRUD 전체 구현 완료

데이터베이스 스키마:

-- plan_service 스키마
travel_plans
  ├── id (BIGSERIAL)
  ├── user_id (BIGINT)
  ├── title (VARCHAR)
  ├── start_date (DATE)
  ├── end_date (DATE)
  ├── budget_level (VARCHAR) -- low/medium/high
  ├── status (VARCHAR) -- DRAFT/CONFIRMED/COMPLETED
  ├── created_at (TIMESTAMP)
  └── updated_at (TIMESTAMP)

plan_details
  ├── id (BIGSERIAL)
  ├── plan_id (BIGINT FK)
  ├── day (INTEGER)
  ├── location (VARCHAR)
  ├── activity (TEXT)
  └── created_at (TIMESTAMP)

구현 파일:

services/plan-service/src/main/java/com/oddiya/plan/
  ├── entity/
  │   ├── TravelPlan.java
  │   └── PlanDetail.java
  ├── repository/
  │   ├── TravelPlanRepository.java
  │   └── PlanDetailRepository.java
  └── service/
      └── PlanService.java (save/fetch logic)

4. 모바일 앱 - CreatePlan 화면

구현일: 2025-11-04

기능:

  • 스트리밍 진행률 표시
  • 실시간 LLM 출력 표시
  • 타이머 (생성 시간 측정)
  • 캐시 여부 표시 (배지)
  • 자동 데이터베이스 저장
  • Plans 리스트 자동 새로고침

UI 요소:

  • 입력 폼: 목적지, 시작일, 종료일, 예산
  • 진행률 바: 0% → 100%
  • 상태 메시지: 한글 업데이트
  • AI 출력: LLM 청크 실시간 표시
  • 플랜 미리보기: 완성 후 표시
  • 배지: "✨ Newly Generated" vs "💾 Cached"

5. 버그 수정

타이머 버그 (2025-11-04):

// Before: 항상 0.0s 표시
const timer = setInterval(() => {
  setElapsedTime((Date.now() - Date.now()) / 1000); // BUG!
}, 100);

// After: 실제 경과 시간 표시
const startTimestamp = Date.now();
const timer = setInterval(() => {
  setElapsedTime((Date.now() - startTimestamp) / 1000);
}, 100);

🏗️ 시스템 아키텍처

서비스 구성

Mobile App (React Native 0.75)
    ↓ HTTP/SSE
API Gateway (8080) ← 일반 REST API
    ↓
┌───────────────────┬────────────────────┬─────────────────┐
│ Auth Service      │ User Service       │ Plan Service    │
│ (8081)            │ (8082)             │ (8083)          │
│ - OAuth 2.0       │ - User CRUD        │ - Plan CRUD     │
│ - JWT RS256       │ - Profile          │ - DB persistence│
└───────────────────┴────────────────────┴─────────────────┘
                                                ↓
                                         LLM Agent (8000)
                                         - FastAPI
                                         - LangChain
                                         - LangGraph
                                         - Gemini API
                                                ↓
                                         Google Gemini
                                         (gemini-2.0-flash-exp)

Mobile App → LLM Agent (8000) ← 스트리밍 직접 연결
    ↓ SSE
LLM Agent streams back

직접 연결 (Direct Connection)

중요: 모바일 앱은 스트리밍을 위해 LLM Agent에 직접 연결합니다.

// mobile/src/api/streaming.ts
const llmAgentUrl = BASE_URL.replace('8080', '8000');
// http://localhost:8080 → http://localhost:8000

이유:

  • API Gateway는 SSE 프록시 지원 제한적
  • 실시간 스트리밍 성능 최적화
  • 단순한 구조

🔄 코드 플로우

1. 플랜 생성 플로우 (Cache Miss)

[모바일 앱]
  1. 사용자가 폼 입력 (Seoul, 2025-11-10, 2025-11-12, Medium)
  2. "Generate Travel Plan" 버튼 탭
  3. generatePlanStreaming() 호출
     ↓
[LLM Agent - langgraph_plans.py]
  4. POST /api/v1/plans/generate/stream 수신
  5. Redis 캐시 확인 (cache_key = "plan:Seoul:...")
  6. 캐시 없음 → LLM 생성 시작
     ↓
[LangGraph Planner]
  7. Step 1: 날씨 정보 수집 (10%)
     → yield {'type': 'status', 'message': '날씨 정보 수집 중...'}
  8. Step 2: AI 플랜 생성 (20-60%)
     → yield {'type': 'chunk', 'content': 'Morning: 경복궁...'}
  9. Step 3-5: 검증/개선/완성 (60-100%)
     → yield {'type': 'progress', 'progress': 70}
  10. Final: 플랜 완성
     → yield {'type': 'complete', 'plan': {...}}
  11. Redis에 캐시 저장 (TTL: 3600초)
     → yield {'type': 'done'}
     ↓
[모바일 앱 - CreatePlanScreen]
  12. onStatus: 상태 메시지 업데이트
  13. onProgress: 진행률 바 업데이트
  14. onChunk: LLM 출력 표시
  15. onComplete: 플랜 미리보기 표시
     ↓
[Plan Service - PlanService.java]
  16. dispatch(createPlan({...})) 호출
  17. POST /api/v1/plans
  18. LLM Agent 호출 (비스트리밍 엔드포인트)
  19. 응답 받은 후 PostgreSQL에 저장
      → TravelPlan entity + PlanDetail entities
  20. 저장 완료 로그: "✅ Plan saved to database: id=1"
     ↓
[모바일 앱]
  21. dispatch(fetchPlans()) 호출
  22. GET /api/v1/plans (X-User-Id: 1)
  23. Plans 리스트 새로고침
  24. 새 플랜이 리스트에 표시됨

2. 플랜 생성 플로우 (Cache Hit)

[모바일 앱]
  1-3. (동일)
     ↓
[LLM Agent]
  4-5. (동일)
  6. 캐시 있음! (Redis에서 조회)
  7. yield {'type': 'status', 'message': '💾 저장된 계획...', 'cached': true}
  8. yield {'type': 'complete', 'plan': {...}, 'cached': true}
  9. yield {'type': 'done'}
     ↓
[모바일 앱]
  10. 상태 박스 녹색으로 변경
  11. 배지: "💾 Cached" 표시
  12. 타이머: <1초
  13. 데이터베이스 저장 스킵 (cached=true)

3. 플랜 목록 조회 플로우

[모바일 앱]
  1. Plans 탭 진입 또는 Pull-to-refresh
  2. dispatch(fetchPlans()) 호출
     ↓
[Plan Service]
  3. GET /api/v1/plans (Header: X-User-Id: 1)
  4. TravelPlanRepository.findByUserIdOrderByCreatedAtDesc(1)
  5. PostgreSQL 쿼리:
     SELECT * FROM plan_service.travel_plans
     WHERE user_id = 1
     ORDER BY created_at DESC
  6. 결과를 PlanResponse DTO로 변환
  7. JSON 응답 반환
     ↓
[모바일 앱]
  8. Redux state 업데이트
  9. PlansScreen 리렌더링
  10. 플랜 리스트 표시

🌍 환경 변수

LLM Agent (.env)

# Google Gemini AI (필수)
GOOGLE_API_KEY=AIzaSyDlMvCLaGNMbPJXvnNkpjf_d4gOQOr5Hbk
GEMINI_MODEL=gemini-2.0-flash-exp

# Redis (필수 - 캐싱용)
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_CACHE_TTL=3600  # 1시간

# PostgreSQL (Plan Service용)
DB_HOST=localhost
DB_PORT=5432
DB_NAME=oddiya
DB_USER=admin
DB_PASSWORD=4321

Plan Service (application.yml)

spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/oddiya?currentSchema=plan_service
    username: admin
    password: 4321
    hikari:
      maximum-pool-size: 5

llm:
  agent:
    base-url: http://localhost:8000

Mobile App (src/constants/config.ts)

export const API_CONFIG = {
  LOCAL_SIMULATOR: 'http://localhost:8080',  // API Gateway
  // Streaming은 8000 포트로 직접 연결
};

export const BASE_URL = getBaseURL();
// iOS Simulator: http://localhost:8080
// Android Emulator: http://10.0.2.2:8080

🚀 실행 중인 서비스

백엔드 서비스

# 확인 명령어
ps aux | grep -E "python.*main.py|java.*plan-service|redis-server"

# 1. LLM Agent (Python FastAPI)
# Port: 8000
# PID: 89540
# 로그: /tmp/llm-agent.log
python main.py

# 2. Plan Service (Spring Boot)
# Port: 8083
# PID: 10107
# 로그: /tmp/plan-service.log
java -jar build/libs/plan-service-0.1.0.jar

# 3. Redis
# Port: 6379
# 확인: redis-cli ping → PONG
brew services start redis

# 4. PostgreSQL
# Port: 5432
# Database: oddiya
# Schemas: plan_service, user_service, auth_service
# 확인: PGPASSWORD=4321 psql -h localhost -U admin -d oddiya

서비스 상태 확인

# Health checks
curl http://localhost:8000/health        # LLM Agent
curl http://localhost:8083/actuator/health  # Plan Service
redis-cli ping                           # Redis
pg_isready -h localhost -U admin         # PostgreSQL

# 데이터베이스 확인
PGPASSWORD=4321 psql -h localhost -U admin -d oddiya -c \
  "SELECT COUNT(*) FROM plan_service.travel_plans;"

# Redis 캐시 확인
redis-cli keys "plan:*"
redis-cli GET "plan:Seoul:2025-11-10:2025-11-12:medium"

📱 모바일 앱 구조

화면 구성

App.tsx
  └── AppNavigator
      ├── WelcomeScreen (로그인/가입)
      └── MainTabs
          ├── PlansScreen (플랜 목록)
          │   └── → CreatePlanScreen (새 플랜 생성)
          │       └── → PlanDetailScreen (플랜 상세)
          ├── ExploreScreen
          ├── BookmarksScreen
          └── ProfileScreen

주요 파일

mobile/
├── src/
│   ├── api/
│   │   ├── services.ts          # REST API 호출
│   │   └── streaming.ts         # SSE 스트리밍 (NEW)
│   ├── screens/
│   │   ├── WelcomeScreen.tsx
│   │   ├── PlansScreen.tsx
│   │   └── CreatePlanScreen.tsx # 스트리밍 UI (NEW)
│   ├── store/
│   │   └── slices/
│   │       ├── authSlice.ts
│   │       └── plansSlice.ts    # 플랜 상태 관리
│   ├── navigation/
│   │   ├── AppNavigator.tsx     # CreatePlan 등록 (UPDATED)
│   │   └── types.ts             # 네비게이션 타입 (UPDATED)
│   ├── types/
│   │   └── index.ts             # TravelPlan 타입 (UPDATED)
│   └── constants/
│       └── config.ts            # API 설정
└── package.json                 # native-stack 추가 (UPDATED)

Redux State 구조

// authSlice
{
  isAuthenticated: boolean,
  user: User | null,
  accessToken: string | null,
  refreshToken: string | null
}

// plansSlice
{
  plans: TravelPlan[],  // 데이터베이스에서 조회한 플랜 목록
  loading: boolean,
  error: string | null
}

API 엔드포인트 (Mobile → Backend)

// services.ts (REST API)
POST   /api/v1/plans              # 플랜 저장
GET    /api/v1/plans              # 플랜 목록 조회
GET    /api/v1/plans/:id          # 플랜 상세 조회
PATCH  /api/v1/plans/:id          # 플랜 수정
DELETE /api/v1/plans/:id          # 플랜 삭제

// streaming.ts (SSE)
POST   http://localhost:8000/api/v1/plans/generate/stream  # 스트리밍 생성

📊 데이터 모델

TravelPlan (Mobile & Backend)

// mobile/src/types/index.ts
interface TravelPlan {
  id?: number;
  userId?: number;
  title: string;
  startDate?: string;
  endDate?: string;
  days?: PlanDay[];              // LLM에서 생성
  totalEstimatedCost?: number;   // LLM에서 계산
  weatherSummary?: string;       // LLM에서 생성
  tips?: string[];               // LLM에서 생성
  metadata?: PlanMetadata;
  createdAt?: string;
  updatedAt?: string;
}

interface PlanDay {
  day: number;
  location: string;
  activity: string;
  estimatedCost: number;
  weatherTip?: string;
}

Budget Mapping (Mobile → Backend)

// mobile/src/screens/CreatePlanScreen.tsx
const budgetMap = {
  low: 50000,      // ₩50,000/day
  medium: 100000,  // ₩100,000/day
  high: 200000,    // ₩200,000/day
};

// 예: 3일 medium budget
// totalBudget = 100000 * 3 = 300000

🔧 개발 워크플로우

로컬 개발 시작

# 1. 백엔드 서비스 시작
cd /Users/wjs/cursor/oddiya/services/llm-agent
source venv/bin/activate
python main.py  # Port 8000

cd /Users/wjs/cursor/oddiya/services/plan-service
./gradlew bootRun  # Port 8083

# 2. 데이터베이스 & 캐시 시작
brew services start redis
brew services start postgresql

# 3. 모바일 앱 시작
cd /Users/wjs/cursor/oddiya/mobile
npm start  # Metro bundler
npm run ios  # iOS Simulator

테스트 시나리오

시나리오 1: 첫 플랜 생성

  1. CreatePlan 화면 진입
  2. Seoul, 2025-11-10, 2025-11-12, Medium 입력
  3. Generate 버튼 탭
  4. 스트리밍 진행 확인 (~6초)
  5. Plans 리스트에 플랜 추가 확인

시나리오 2: 캐시된 플랜 생성

  1. 동일한 파라미터로 재생성
  2. 즉시 완료 (<1초) 확인
  3. "💾 Cached" 배지 확인
  4. 중복 저장 안됨 확인

시나리오 3: 앱 재시작

  1. 앱 종료
  2. 앱 재시작
  3. Plans 탭 진입
  4. 플랜이 여전히 표시됨

📝 로그 확인

Backend Logs

# LLM Agent
tail -f /tmp/llm-agent.log | grep -E "Streaming|Cache|ERROR"

# 성공 로그:
# → Checking cache for key: plan:Seoul:2025-11-10:2025-11-12:medium
# → Cache miss, generating new plan
# → Streaming plan generation started
# → Saved plan to cache

# Plan Service
tail -f /tmp/plan-service.log | grep "PlanService"

# 성공 로그:
# [PlanService] Creating plan for user=1, destination='Seoul'
# [PlanService] → Python LLM Agent: LlmRequest(...)
# [PlanService] ← Python LLM Agent returned plan: 3 days
# [PlanService] ✅ Plan saved to database: id=1
# [PlanService] Fetching all plans for user=1
# [PlanService] Found 1 plans for user=1

Mobile Logs

# Metro Console
# [Streaming] Connecting to: http://localhost:8000/api/v1/plans/generate/stream
# [Streaming] Request: {location: "Seoul", ...}
# [Streaming] Response status: 200
# [Streaming] Stream opened, reading events...
# [Streaming] Status: Seoul의 날씨 정보를 수집하고 있습니다... 10
# [Streaming] Chunk: Morning: 경복궁...
# [Streaming] Complete! Plan: Seoul 3-Day Trip
# Saving plan to database...
# Plan saved successfully!

🚧 다음 단계

즉시 구현 가능

  1. PlanDetail 화면 완성

    • 플랜 상세 내역 표시
    • 일별 일정 표시
    • 예상 비용 표시
  2. 플랜 수정/삭제

    • 수정 화면
    • 삭제 확인 모달
  3. 오류 처리 개선

    • 네트워크 오류 처리
    • LLM 실패 시 재시도
    • 타임아웃 처리

추후 구현

  1. 인증 통합

    • OAuth 로그인 완성
    • JWT 토큰 관리
    • 자동 로그인
  2. 오프라인 지원

    • AsyncStorage에 플랜 캐싱
    • 오프라인 모드
  3. 비디오 생성

    • 플랜에서 비디오 생성
    • S3 업로드
    • SNS 공유

📚 관련 문서


마지막 테스트: 2025-11-04 상태: ✅ 모든 기능 구현 완료, 테스트 준비 완료 다음: 모바일 앱에서 실제 테스트