Files
web-page-backend/docs/superpowers/specs/2026-04-24-travel-proxy-perf-design.md

6.5 KiB

Travel-Proxy 성능 개선 설계

목표

travel-proxy의 파일 스캔 기반 아키텍처를 SQLite 인덱스 DB로 전환하여 수천 장의 사진을 무난하게 처리하고, 앨범 커버 지정 + 썸네일 사전 생성을 지원한다.

배경

현재 travel-proxy는 os.scandir으로 NAS 폴더를 매번 스캔하고, 메모리 캐시(TTL 300초)로 결과를 보관한다. 사진 수백 장에서는 문제없지만, 수천 장이면:

  • 캐시 만료 시 1~2초 스캔 지연
  • 콜드 스타트(컨테이너 재시작) 시 첫 요청 느림
  • 전체 리스트를 메모리에 상주
  • 썸네일이 첫 요청 시 동기 생성되어 초기 로딩 지연

아키텍처

변경 전

API 요청 → os.scandir(폴더) → 메모리 캐시 → 슬라이싱 페이지네이션
                                    ↓
                         썸네일 온디맨드 생성 (Pillow)

변경 후

수동 sync 버튼 → 폴더 스캔 → travel.db 동기화 + 썸네일 사전 생성
                                    ↓
API 요청 → SQLite 쿼리 (인덱스) → 페이지네이션

파일 구조

파일 역할
main.py FastAPI 라우트 (기존 + 신규)
db.py (신규) SQLite 스키마 정의, 쿼리 헬퍼
indexer.py (신규) 폴더 스캔 → DB 동기화 + 썸네일 생성

기존 main.pyscan_album, ensure_thumb, 메모리 캐시 로직이 indexer.pydb.py로 이동하고, main.py는 라우트만 남는다.

DB 스키마

CREATE TABLE photos (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    album TEXT NOT NULL,
    filename TEXT NOT NULL,
    mtime REAL NOT NULL,
    has_thumb INTEGER DEFAULT 0,
    indexed_at TEXT NOT NULL,
    UNIQUE(album, filename)
);

CREATE INDEX idx_photos_album ON photos(album);

CREATE TABLE album_covers (
    album TEXT PRIMARY KEY,
    filename TEXT NOT NULL,
    updated_at TEXT NOT NULL
);

설계 포인트

  • photos 테이블에 URL/thumb 경로를 저장하지 않음 — 런타임에 MEDIA_BASE + album + filename으로 조합 (환경변수 변경에 유연)
  • mtime으로 변경 감지 — 동기화 시 파일이 삭제됐거나 mtime이 바뀌면 갱신
  • album_covers가 비어있으면 해당 앨범의 첫 번째 사진이 자동 커버

API 설계

기존 API 변경

엔드포인트 변경 내용
GET /api/travel/photos 내부 로직만 변경 (os.scandir → DB 쿼리). 응답 형식 동일
GET /api/travel/regions 변경 없음
POST /api/travel/reload 제거 (sync로 대체)
GET /media/travel/.thumb/{album}/{filename} 유지 — 동기화 시 이미 썸네일 생성되므로 Pillow 호출 빈도 대폭 감소. 미생성 분 폴백으로 온디맨드 생성 유지

신규 API

메서드 경로 설명
POST /api/travel/sync 폴더 스캔 → DB 동기화 + 썸네일 생성
GET /api/travel/albums 앨범 목록 + 사진 수 + 커버 정보
PUT /api/travel/albums/{album}/cover 앨범 커버 지정

POST /api/travel/sync

폴더를 스캔하여 DB와 동기화하고, 미생성 썸네일을 일괄 생성한다.

요청: 바디 없음

응답:

{
  "added": 42,
  "removed": 3,
  "thumbs_generated": 42,
  "duration_sec": 12.5
}

동기 실행 — 수동 트리거이므로 BackgroundTask 불필요, 응답에 결과 포함.

GET /api/travel/albums

앨범 목록과 각 앨범의 사진 수, 커버 정보를 반환한다.

응답:

[
  {
    "album": "오사카",
    "count": 342,
    "cover_url": "/media/travel/오사카/IMG_3281.jpg",
    "cover_thumb": "/media/travel/.thumb/오사카/IMG_3281.jpg"
  }
]

커버가 지정되지 않은 앨범은 첫 번째 사진(album + filename 정렬 기준)이 자동 커버.

PUT /api/travel/albums/{album}/cover

특정 사진을 앨범 커버로 지정한다.

요청:

{
  "filename": "IMG_3281.jpg"
}

응답:

{
  "album": "오사카",
  "filename": "IMG_3281.jpg",
  "cover_url": "/media/travel/오사카/IMG_3281.jpg",
  "cover_thumb": "/media/travel/.thumb/오사카/IMG_3281.jpg"
}

검증: 해당 album + filename 조합이 photos 테이블에 존재하는지 확인. 없으면 404.

동기화 로직 (indexer.py)

sync 프로세스

  1. region_map.json에서 전체 앨범 폴더 목록 수집
  2. 각 폴더 os.scandir{album, filename, mtime} 세트 수집
  3. DB와 비교:
    • DB에 없는 파일 → INSERT (added)
    • DB에 있지만 폴더에 없는 파일 → DELETE (removed)
    • mtime이 다른 파일 → UPDATE + has_thumb=0 (변경됨)
  4. has_thumb=0인 파일 → 썸네일 생성 → has_thumb=1로 갱신
  5. 결과 반환: {added, removed, thumbs_generated, duration_sec}

삭제된 커버 처리

커버로 지정된 사진이 폴더에서 삭제되면 album_covers에서도 제거 → 자동으로 첫 번째 사진 폴백.

성능

  • NAS Celeron J4025 기준, 2,000장 최초 동기화 + 썸네일 생성 예상: 3~5분
  • 이후 동기화는 변경분만 처리 → 수초 이내

앨범 커버 지정 UX

프론트엔드 앨범 상세 페이지에서 사진을 길게 누르거나 우클릭 → "커버로 설정" 메뉴. PUT /api/travel/albums/{album}/cover 호출.

프론트엔드 변경은 이 스펙 범위 밖 — 백엔드 API만 제공하고, 프론트 연동은 별도 작업.

기존 API 호환성

  • GET /api/travel/photos 응답 형식 (items, total, has_next, matched_albums) 완전히 유지
  • 프론트엔드 useTravelData 훅은 수정 없이 동작
  • GET /api/travel/albums는 선택적 개선용 — 프론트가 앨범 카드 커버를 표시할 때 활용

Docker 변경

  • travel.db 저장 위치: 썸네일 볼륨 내 /data/thumbs/travel.db (추가 볼륨 불필요)
  • requirements.txtaiosqlite 추가 불필요 — 동기 sqlite3 표준 라이브러리 사용
  • Dockerfile 변경 없음

docker-compose.yml 변경

기존 볼륨에 DB를 함께 저장하므로 추가 볼륨 불필요:

volumes:
  - ${PHOTO_PATH}:/data/travel:ro
  - ${RUNTIME_PATH}\travel-thumbs:/data/thumbs:rw  # travel.db도 여기에 저장

제거되는 코드

  • main.pyCACHE, CACHE_TTL, META_MTIME_CACHE 딕셔너리 및 관련 로직
  • main.pyscan_album() 함수 (indexer.py로 이동)
  • main.pyensure_thumb() 함수 (indexer.py로 이동, 온디맨드 폴백은 유지)
  • POST /api/travel/reload 엔드포인트 (sync로 대체)