Skip to main content

Gmail 자동 가계부

active
fastapi python gmail-api ollama playwright pwa

개요

“한달에 쿠팡으로 얼마나 쓸까?” 라는 단순한 궁금증에서 시작했습니다.

카드사 실시간 사용 내역은 공개 API가 없어서 자동으로 파싱할 방법이 없습니다. 대신 쿠팡, 네이버페이, 배민 같은 결제 플랫폼(PG사 포함)이 결제 시마다 알림 메일을 보내주는 점에 착안했습니다. 이 메일들을 파싱하면 플랫폼별 지출 추이를 실시간에 가깝게 파악할 수 있습니다. 카드사 실물 내역은 매월 발송되는 이용대금명세서 메일을 파싱해서 보완했습니다.

Claude와 바이브 코딩으로 개발했습니다. 파서 구조 설계, DB 스키마, 카드사 명세서 복호화 로직 등 복잡한 부분을 Claude와 주고받으며 빠르게 구현했습니다.

기능내용
Gmail 자동 폴링5분 주기, History API 커서 방식으로 새 메일만 처리
결제 메일 파싱쿠팡, 배민, 네이버페이, KCP, NICE, Apple 등 10+ 파서
자동 분류키워드 1차 분류 → 미분류 시 Ollama AI Fallback
카드 명세서현대·하나·롯데카드 암호화 첨부 복호화 후 파싱
대시보드월별 소비 현황, 카테고리별 예산 설정, PWA

기술 스택

영역기술
백엔드FastAPI, Uvicorn
DBPostgreSQL, SQLAlchemy, Alembic
메일Google Gmail API (History API)
스케줄링APScheduler
AI 분류Ollama (gemma4:e2b)
HTML 파싱BeautifulSoup4, lxml
명세서 복호화Playwright (Chromium headless), Node.js
프론트엔드Jinja2 템플릿, PWA (Service Worker)
인증Google OAuth2 (화이트리스트 기반)
배포Docker Compose, Caddy 리버스 프록시

아키텍처

flowchart TD
  Gmail[Gmail]
  Poller["APScheduler\n5분 폴링"]
  Parser["발신자별 파서\n결제 메일 10+"]
  Decryptor["Playwright / Node.js\n카드사 암호화 복호화"]
  Categorizer["키워드 분류기\n20개 카테고리"]
  Ollama["Ollama\ngemma4:e2b"]
  DB[("PostgreSQL")]
  Dashboard["FastAPI 대시보드\nPWA"]

  Gmail -->|"History API\n(커서 방식)"| Poller
  Poller -->|"결제 메일"| Parser
  Poller -->|"카드 명세서"| Decryptor
  Parser --> Categorizer
  Categorizer -->|"기타 (미분류)"| Ollama
  Categorizer --> DB
  Ollama --> DB
  Decryptor --> DB
  DB --> Dashboard

주요 구현 포인트

Gmail History API 커서 방식

전체 받은편지함을 매번 스캔하는 대신 historyId를 커서로 활용합니다. poller_state 테이블에 마지막으로 처리한 historyId를 저장해두고, 다음 폴링 때 그 이후에 온 메시지만 가져옵니다.

# gmail_poller.py
def get_or_init_history_id(service, db_session, user_id: str = "me") -> str:
    state = db_session.execute(select(PollerState)).scalar_one_or_none()
    if state:
        return state.history_id

    # 첫 실행: Gmail 현재 historyId를 커서 초기값으로 저장
    profile = service.users().getProfile(userId=user_id).execute()
    history_id = str(profile["historyId"])
    db_session.add(PollerState(history_id=history_id))
    db_session.commit()
    return history_id

발신자 주소를 키로 파서 모듈을 매핑합니다. 새 플랫폼은 파서 하나만 추가하면 됩니다.

SENDER_PARSER_MAP = {
    "noreply@e.coupang.com":               coupang_parser,
    "naverpayadmin_noreply@navercorp.com":  naver_parser,
    "noreply@woowahan.com":                baemin_parser,
    "nice_customer@nicepg.co.kr":          nicepg_parser,
    "no_reply@email.apple.com":            apple_parser,
    # ...
}

2단계 자동 분류

1단계는 정규식 키워드 매칭으로 빠르게 처리하고, 기타로 남은 거래만 Ollama에 넘깁니다. 매시간 정각에 배치로 재분류합니다.

# ollama_classifier.py
def _build_prompt(product_name: str, platform: str = "") -> str:
    categories = ", ".join(ALLOWED_CATEGORIES)
    return f"""
당신은 결제 메일 상품명을 가계부 카테고리로 분류하는 분류기다.
반드시 아래 후보 중 하나만 선택해야 한다.

후보 카테고리: {categories}

입력:
- platform: {platform or "unknown"}
- product_name: {product_name or "unknown"}

규칙:
1. 반드시 JSON만 출력한다.
2. 형식은 {{"category":"후보 중 하나"}} 로 고정한다.
3. 후보에 없는 값은 절대 만들지 않는다.
""".strip()

응답은 JSON 파싱 → 카테고리 유효성 검사 순으로 처리합니다. 파싱 실패 시 기타로 폴백합니다.

카드 명세서 복호화

카드사 명세서 메일의 첨부 파일은 비밀번호로 보호된 HTML입니다. 브라우저 없이는 내용을 볼 수 없는 구조입니다.

하나카드: Playwright로 Chromium을 headless 실행하여 비밀번호 입력 → 복호화된 HTML 추출

# statement_decryptors.py
with sync_playwright() as playwright:
    browser = playwright.chromium.launch(headless=True)
    page = browser.new_page()

    page.goto(attachment_path.as_uri(), wait_until="load", timeout=30000)
    page.wait_for_timeout(1500)
    page.locator("#password").fill(password, timeout=5000)
    page.locator('a[onclick*="UserFunc"]').click(timeout=5000)
    page.wait_for_function(
        "() => document.body && document.body.innerText.includes('입금하실 금액입니다')",
        timeout=20000,
    )
    return page.content()

롯데카드: 자체 암호화 라이브러리(UNISAFE v2)를 사용하기 때문에 Playwright 대신 Node.js 스크립트로 복호화합니다.

복호화된 HTML은 카드사별 파서(hyundai_statement.py, hana_statement.py, lotte_statement.py)가 BeautifulSoup으로 파싱하여 명세서 항목을 추출합니다.

멱등성 설계

transactionscard_statements 테이블 모두 raw_email_idUNIQUE 제약을 걸어 두었습니다. 같은 메일이 두 번 처리되더라도 DB 레벨에서 중복 저장을 막습니다.


화면

대시보드

월별 총 지출, 플랫폼별 분류, 일별 지출 추이 차트, 카테고리 도넛 차트, 예산 진행률, 최근 거래 내역을 한 화면에서 확인할 수 있습니다.

카드 명세서

현대·하나·롯데카드 이용대금명세서를 자동으로 파싱합니다. 카드사별 합산 금액과 항목별 상세 내역을 조회할 수 있습니다.