Mova — AI 영화 추천 플랫폼

직접 파인튜닝한 EXAONE 3.5 2.4B가 채팅의 판단(도구 선택)과 추천을 맡고, 사실은 데이터에서 가져와 답하는 영화 플랫폼. 카탈로그 3,919편, 박스오피스 랭킹, 에디터 리뷰, 예매 시간표, 봤어요·별점 기록.


주요 기능

기능 설명
채팅 에이전트 판단 모델이 도구 10개 중 다음 행동을 고른다. 출연진·시간표·상영작·OTT는 코드 템플릿이 답하고, LLM 문장은 추천 이유·리뷰 요약에만 쓴다.
AI 맞춤 추천 EXAONE 3.5 2.4B LoRA(GGUF)가 후보 중 3편을 고르고 이유를 쓴다. GPU 서버 실패 시 Gemini 자동 폴백. “여자친구랑 볼 영화” 같은 동행 조건도 장르 선호로 읽는다.
봤어요 · 별점 “인셉션 봤어 4점” 한 문장으로 기록. 제목 일부만 말해도, 후보 중에서 “2026년 거”라고 골라도 이어진다.
취향 추천 · 요약 기록을 바탕으로 한 추천(recommend_for_me), 비슷한 영화(similar_to), 취향 요약(taste_profile).
에디터 리뷰 뉴스 요약으로 리뷰 자동 생성, 감정분석으로 별점 부여, 근거가 된 기사 링크 표시.
박스오피스 랭킹 KOFIC API로 일별·주간·월간 집계. mova 랭킹 — 최근 7일 영화 상세 열람 수(비로그인 포함, 사람·영화·하루 1회).
예매 시간표 지역·날짜로 상영관과 회차 조회(카카오 로컬 · 롯데시네마).
개봉 예정작 TMDB API로 한국 개봉 예정 영화 자동 갱신, 월별 그룹핑.
컬렉션·찜·마이페이지 컬렉션, 찜 목록, 추천 기록, 내 리뷰, 프로필.

기능 요구 사항

ID 기능 설명 상태
M-001 AI 추천 LoRA/Gemini 듀얼 백엔드 개인화 추천, 자동 폴백 ✅
M-002 AI 리뷰 뉴스 요약 → 에디터 리뷰 + 감정분석 별점 + 출처 링크 ✅
M-003 랭킹 KOFIC 박스오피스 일별/주간/월간 집계·시각화 ✅
M-004 채팅 도구 호출 에이전트(판단 모델 v9), 대화 저장·목록 ✅
M-005 개봉 예정작 TMDB API 자동 갱신, 월별 그룹핑 ✅
M-006 컬렉션·찜 개인 영화 목록 생성·관리, 찜 토글 ✅
M-007 마이페이지 추천 기록, 찜, 리뷰 목록, 프로필 관리 ✅
M-008 소셜 로그인 Google·Kakao·Naver OAuth 2.0 + httpOnly 쿠키 세션 ✅
M-009 봤어요·별점 채팅과 상세 화면에서 기록, 상태 조회 ✅
M-010 예매 시간표 지역·날짜별 상영관·회차 ✅

AI 파이프라인

채팅 판단 에이전트 (v9 모델 + 도구 10개, 2026-09-30)

발화 + 최근 대화
    │
    ▼  (직전 응답이 후보 되묻기였다면 → 고른 작품으로 하던 일을 그대로 잇는다)
MovaChatAgent ── 판단 모델(EXAONE 3.5 2.4B LoRA, GGUF, Ollama) ──→ 다음 행동 하나
    │            <tool_call>{"name":"get_movie_details","arguments":{"title":"…"}}</tool_call> | FINAL
    ├─ 데이터 도구: search_movie · get_movie_details · now_showing   → 결과를 붙여 재판단(≤3단계)
    └─ 터미널 도구: recommend_movies · showtimes · where_to_watch
                    mark_watched · recommend_for_me · similar_to · taste_profile
    │
    ▼
답변: 사실(출연진·시간표·상영작·OTT·기록 확인)은 코드 템플릿 · 문장은 추천 이유·리뷰 요약만 LLM
  • 왜 바꿨나: 6칸 슬롯(intent·title·region·time·chain·followup)에 자리가 없는 질문(출연진, 직전 카드 전체, 현재 상영작)이 전부 오답이었고, 발화에 없는 제목을 지어내는 일이 있었다.
  • 학습 데이터는 템플릿 합성(교사 비용 0원). 평가셋 86(실사용 대화 포함)에서 v9 77. 뒤이은 v10·v11은 새로 학습할 때마다 68~72로 오히려 내려가 채택하지 않았다 — 2.4B의 천장으로 보고, 새 도구 4개는 재학습 없이 프롬프트 예시만으로 v9에 얹었다.
  • 가드는 학습이 아니라 코드: 제목·지역 근거(대화에 없는 값 차단), 반복 호출 차단, 호출 예산.
  • 기록은 더 엄격하다: 별점은 사용자가 이번에 말한 숫자일 때만 남긴다. 안내 문구의 예시 숫자를 모델이 베껴 말하지 않은 별점이 기록된 적이 있어서다(2026-09-30).
  • 운영 회귀 하네스: 단일턴 28질의 28/28, 멀티턴 17장면 16/17(날짜에 의존하는 검사 1건).

추천 엔진 — LoRA + Gemini 폴백

추천 요청 (에이전트 recommend_movies)
    │
    ▼
RAG 후보 (pgvector HNSW, hub_knowledge) + 태그 실매칭 → 품질 하한·시리즈당 1편
    │
    ├─ lora-server :8200 (EXAONE 3.5 2.4B LoRA, llama.cpp GGUF Q5_K_M, 집 서버 GTX 1650)
    └─ 실패·서킷 오픈 시 Gemini API 폴백 (자동, 60초 쿨다운 후 복귀)
  • 학습은 Colab(L4)에서 어댑터 병합 → GGUF 양자화까지 끝낸다(노트북 GPU는 운영 전용). 내보내기 판단은 노트북에서 GGUF로 다시 채점한 점수와 운영 회귀 하네스 기준.
  • Gemini 블라인드 비교(2026-09-29): 추천 픽 27건 중 Gemini 12·EXAONE 3·동률 12 — 패인은 “3편을 채우려 주제 밖 작품을 끼움”. 다음 학습 회차의 목표다.

에디터 리뷰 파이프라인

박스오피스·카탈로그 → 리뷰 없는 작품 선정(30일 재시도 간격)
    │
    ▼
Google News RSS (제목 + 요약만, 원문 미수집) ── 기사 출처 목록 보관
    │
    ▼
Gemini ──→ "Mova 에디터" 리뷰 (영화당 1건, 재실행 안전)
    │
    ▼
EXAONE 감정분석 LoRA(NSMC) ──→ 긍정/부정 + 확신도 → 0.5~5.0 자동 별점
    │
    ▼
reviews 저장 (감정 라벨 · 뉴스 소스 수 · 출처 링크) → 상세 화면에 신뢰도 배지와 기사 링크
  • 별점 변환: 긍정 2.5 + score × 2.5, 부정 2.5 − score × 2.0, 0.5 단위 반올림.
  • 평점순 정렬은 베이지안 가중 평점 (v·R + m·C)/(v+m)(m=50, C=3.0) — 투표 1건짜리 5.0이 명작을 밀어내지 못한다.

기술 스택

구분 기술
백엔드 Python 3.12 · FastAPI · PostgreSQL 16 + pgvector · Redis · SQLAlchemy 2.0 Async
프론트엔드 Next.js 16 · TypeScript 5.7 · React 19 · Tailwind CSS 4 · shadcn/ui
모바일 Flutter · Dart
AI/ML EXAONE 3.5 2.4B LoRA(추천·판단 에이전트) · EXAONE 3.5 7.8B(이해·잡담) · Google Gemini(폴백·리뷰 요약) · pgvector RAG
데이터 TMDB API · KOFIC/KOBIS API · 카카오 로컬(영화관) · 롯데시네마 시간표 · Google News · 위키피디아
인프라 집 서버·노트북 k3s · Docker(DB·Redis) · llama.cpp · Ollama · Tailscale · Cloudflare Tunnel · AWS S3

트러블슈팅

LoRA 서버 ↔ Gemini 자동 폴백

  • 상황: GPU 서버(lora-server)가 내려가면 추천 API가 그대로 실패했다
  • 원인: 처음엔 환경 변수를 손으로 바꾸고 재기동해야 했고(왕복 8초), 자동 폴백 어댑터를 만든 뒤에도 DI에 연결되지 않아 동작하지 않았다
  • 해결: FallbackRecommendationAdapter(primary=LoRA, fallback=Gemini)를 DI에 연결, 연속 2회 실패 시 60초 우회하는 서킷 브레이커
  • 결과: GPU 서버가 죽어도 추천이 끊기지 않고, 살아나면 자동 복귀

채팅에서 “봤어”가 9턴 내내 기록되지 않음 (2026-09-30)

  • 상황: “스파이더맨 최신꺼 봤어” → 후보 제시 → “2026작품이겠지?” → “찾지 못했어요” → “브랜드 뉴 데이 봤어” → “못 찾았어요”
  • 원인: 모델이 아니라 코드 네 곳 — 후보 문구 형식이 두 가지라 파서가 세 편을 한 제목으로 읽음, 선택 이어받기가 에이전트 뒤에 있어 실행되지 않음, 제목 검증이 정확 일치뿐, 선택 뒤 이을 일이 평가·예매뿐
  • 해결: 문구 형식 하나로 통일, 이어받기를 에이전트 앞으로, 봤어요 전용 제목 해석(연도 꼬리 제거 → 정확 → 부분 한 편 → 연도 → 직전 응답의 작품 → 되묻기)
  • 결과: 풀네임 없이 기록된다. 회귀 하네스에 없던 장면이라 실사용으로만 드러났다 — 하네스에 기록 후속 장면을 추가하는 것이 다음 과제

“좀비 영화” 추천 0건 — 세 겹의 원인

  • 상황: 카탈로그에 좀비 영화가 있는데 추천이 0건
  • 원인: ① RAG 히트가 있으면 태그 검색을 건너뜀 ② 2.4B는 후보 목록 앞쪽에 끌림 ③ 문두 담화어 정규식(좀\s*)이 “좀비”의 “좀”을 떼어 “비 영화”로 만듦
  • 해결: RAG·태그 합집합, 태그 실매칭 우선 정렬, 담화어는 공백 필수(\s+)
  • 결과: 좀비딸 · 좀비스쿨 · #살아있다

IDOR 취약점 5건 전수 수정

  • 상황: user_id를 경로·바디 파라미터로 받아 다른 사용자의 리소스에 접근 가능
  • 원인: 초기 개발 시 user_id를 클라이언트 입력으로 신뢰, 토큰 기반 신원 확인 누락
  • 해결: 전수 조사로 5건(mypage·profile·watchlist·picks·chat) 식별 후 전부 JWT 토큰 기반으로 전환
  • 결과: 모든 리소스 접근에 소유권 검증 적용, IDOR 취약점 0건

스크린샷

Mova 영화 탭 — 인기 검색 영화와 장르별 목록
Mova 영화 탭 — 인기 검색 영화와 장르별 목록
Mova 작품 상세 — 예고편 · 출연진 · 비슷한 영화
Mova 작품 상세 — 예고편 · 출연진 · 비슷한 영화
Mova 박스오피스 랭킹
Mova 박스오피스 랭킹