CUTIE STREET 토레카(트레이딩 카드) 분석기 + 개인 컬렉션 DB.
카드 사진 한 장 업로드 → 멤버·시리즈 자동 식별 → 우리가 수집한 메르카리 매물 Top 20 + 시세 → 비교 화면에서 직접 매물 확인 → 본인 컬렉션에 저장 → 갤러리로 모아보기.
| 위치 | URL | |
|---|---|---|
| 🌐 | Live | https://card.taba.asia |
| ✨ | 갤러리 (메인) | https://card.taba.asia/ |
| 📸 | 스캐너 | https://card.taba.asia/scan |
| 📂 | 마스터 DB 카탈로그 | https://card.taba.asia/master |
| 📖 | API Docs (Swagger) | https://card.taba.asia/docs |
| 📖 | API 명세 (사람용) | docs/API.md |
- 페이지 흐름
- 스택
- 데이터 (3개 분리)
- 로컬 실행
- API 한눈에 (23 endpoints) · 자세한 명세는
docs/API.md - 마스터 DB 빌드
- 컬렉션 데이터 보호 & 백업
- 시리즈 카탈로그 (48 시드)
- 호스팅 아키텍처
- 디렉터리
- 윤리 & 라이선스
이전엔 결과 화면에서 바로 등록 폼이 떴는데, 매물 비교를 위한 별도 화면(/scan/compare)을 끼워넣어 잘못된 매물이 그대로 저장되는 사고를 방지.
/scan (4-step 위저드)
Step 1 사진 업로드
Step 2 멤버 선택 (8명 + 모르겠어)
Step 3 시리즈 선택 (정규 / 의상 / 이벤트 / 생탄제)
Step 4 결과
├─ 매치 점수 (gradient hero, 0~100%)
├─ Top 20 매물 그리드 (sample-as-entry — 각 카드 = 매물 1개)
│ 클릭 → 그 매물의 실제 Mercari 페이지 새 탭
├─ Top1 기준 Mercari 시세 한 줄
├─ 💾 컬렉션에 등록하기 → /scan/compare 로 이동
│ · 이동 직전: identify 결과 + 백그라운드 prefetch 한
│ exportTop20 데이터 (시세/이미지 포함) 를 sessionStorage 에
│ 통째 적재 → compare/save 페이지는 fetch 없이 즉시 렌더
├─ 🏠 내 갤러리
├─ 📥 Top20 데이터 다운로드 (.json)
└─ 🔄 다른 사진으로 다시 검색
/scan/compare (등록 전 비교 — 자동 선택 없음)
├─ 좌 내 사진 (방금 업로드한 카드)
├─ 우 선택한 reference 매물 (초기 비어있음 — 사용자가 직접 클릭)
├─ "↓ 아래에서 카드를 골라주세요" 점선 박스 안내
├─ 하단 Top 20 매물 그리드 (3 컬럼)
│ 카드 클릭 → 우측 reference 가 그걸로 바뀜
├─ [다음 → 폼 채우기] (선택된 후보 데이터로 폼 prefill)
└─ [✏️ 선택하지 않기] (빈 폼, 직접 입력)
/scan/save (등록 폼)
├─ prefill 모드 → 매칭 점수 + Mercari 매물 링크 + 폼 자동 채움
│ (source_url 은 notes 에 'Mercari 매물 참고: ...' 로 흔적)
├─ 직접 입력 모드 → 멤버/시리즈 select + 빈 폼
├─ 모든 필드 사용자 수정 가능
└─ 💾 컬렉션에 저장 → SQLite + 본인 사진 → 갤러리로
/ 갤러리 (메인)
│ 통계 (총 카드 / 가치 / kind 분포) · 필터 (kind, 멤버) · 정렬
│ 2-col 카드 썸네일 (멤버 컬러 strip + kind 배지 + 시세 배지)
│ 우측 상단 ⚙️ 마스터 DB 배지
│ · 📂 자세히 보기 / 🔄 갱신 / 🧹 깊은 갱신 / ⛔ 빌드 취소
│ 플로팅 CTA "📸 새 카드 분석" → /scan
/master 마스터 DB 카탈로그 뷰어
├─ Summary (시리즈/엔트리/샘플) · 임베더 버전 · 빌드 시각
├─ 🔍 시리즈 라벨/SKU 검색
├─ kind 필터 + 멤버 필터
└─ kind별 섹션 → 시리즈 카드 (진행률 + 4-col 멤버 썸네일)
└─ 클릭 → /master?sku=X 시리즈 자세히
/master?sku=X 시리즈 자세히
├─ Summary metric × 3 (멤버 N/8, 총 샘플, 평균/멤버)
└─ 멤버 카드 × 8
├─ 큰 reference 이미지 (오버레이: "같은 멤버 N종 보기 →")
└─ 클릭 → /scan/match?sku=X&member=Y
/scan/match?sku=X&member=Y 멤버 매물 그리드
├─ 헤더: kind chip + 시리즈 + 한/일 멤버 이름 + 공식 페이지
└─ 2-col 매물 카드 (이미지 + #rank + 매물 ID)
└─ 클릭 → 실제 Mercari 매물 새 탭
/collection?id=N 본인 카드 상세
├─ 본인 사진 풀폭 + 멤버 컬러 strip
├─ kind tint 헤더 박스 (라벨 + SKU + 멤버 한/일 + 설명)
├─ 🛒 같은 카드 다른 매물 → /scan/match
├─ Mercari 시세 (평균/최저/최고 · Sold N개 기준)
├─ 메모 ✏️ 편집 (PUT API)
├─ 🛍 공식 상품 페이지 / ▾ 메타 / 🗑 삭제
┌ Next.js 15 (web/) ──────── App Router · Tailwind · TS
│ output: 'export', trailingSlash: true
↓ next build → web/out
┌ FastAPI (api/) ─────────── Python · uvicorn :1024 (내부 only)
│ systemd-user · cutie-card.service
↓ uses
┌ core/ ───────────────── DINOv2-L + SigLIP-2 임베더 · 매처
│ sample-as-entry catalog · SQLite 컬렉션
│ Mercari Playwright scraper · 시세 캐시
↓
┌ nginx (system) ──────── card.taba.asia → 127.0.0.1:1024
│ Let's Encrypt 자동 인증서 (certbot --nginx)
↓
인터넷 (HTTPS 443)
| 모델 | 파라미터 | 차원 | 특성 |
|---|---|---|---|
DINOv2 ViT-L/14 (facebook/dinov2-large) |
300M | 1024d | Meta self-supervised. 의상/표정/소품 같은 fine-grained 인스턴스 디테일 |
SigLIP-2 base/patch16-256 (google/siglip2-base-patch16-256) |
90M | 768d | Google text-image contrastive. semantic (의상·배경 의미) |
→ 각 모델 L2-normalize → concat → 다시 L2-normalize → 1792d 단일 벡터 → catalog 와 cosine.
회전 TTA: 카드가 90°/180°/270° 돌아 찍힐 수 있어 query 4 회전 인코딩 → entry 별 max cosine 사용. 디스크 임베딩은 회전 없이 1 회.
매칭 단위 = Mercari 매물 1개. cluster mean 안 만들고 raw 임베딩을 catalog 에 1:1 매핑.
이전 (cluster mean):
CatalogEntry = (시리즈 × 멤버, N매물 묶음, mean 1벡터)
Top-K → K 클러스터 (한 카드 = 5매물 묶음)
현재 (sample-as-entry):
CatalogEntry = (시리즈, 멤버, 매물 1개, 이미지 1장, URL 1개, 임베딩 1벡터)
Top-K → K 매물 (서로 다른 사진/디자인 변형)
같은 (시리즈, 멤버) 라도 매물마다 디자인 변형 (라이브 한정, 발매 시점) 이면 각자 별도 매칭. mean 평균화로 인한 정보 손실 제거.
| 위치 | 무엇 | 어떻게 채워짐 | gitignored |
|---|---|---|---|
master/ |
sample-as-entry catalog (1635 entries) + 1792d 임베딩 + reference 이미지 | make build-master / /api/master/rebuild |
✅ /master/ |
cache/ |
Mercari sold 시세 24h 캐시 | API 호출 시 자동 | ✅ /cache/ |
collection/ |
본인 카드 (SQLite + 사진) — 사용자 데이터 | 결과 → 비교 → 저장 흐름 | ✅ /collection/ |
파일 단위
master/catalog.json(sample-as-entry 리스트) +master/embeddings.npy(N × 1792)master/raw/{sku}/{member_key}/*.jpgmaster/.embedder_version(stale 감지)master/.rebuild.log(백그라운드 빌드 로그) +.rebuild.lock(pid)collection/collection.db(SQLite) +collection/images/{id}.jpg
모든 IO 경로는 core/paths.py 단일 공급 — PROJECT_ROOT 기준 절대경로, cwd 무관.
make # 의존성 + Next 빌드 + master DB + uvicorn 1024 (한 방)
# 분리 실행 (개발용)
uv run uvicorn api.main:app --reload --port 8000
cd web && NEXT_PUBLIC_API_BASE=http://localhost:8000 npm run dev # :3000
# 개별
make setup # uv sync + npm install
make build-web # Next.js 정적 빌드 → web/out
make build-master # 마스터 DB (15~60분)
make run # uvicorn 1024
make test # pytest 79개환경 변수
HF_TOKEN— (선택) HuggingFace 토큰 (모델 다운로드 가속)NEXT_PUBLIC_API_BASE— 웹 dev 모드 백엔드 URL (기본: 빈 문자열 = 동일 origin)UV_BIN— uv 바이너리 경로 (기본~/.local/bin/uv)
전체 명세 + curl 예시 + 응답 스키마:
docs/API.md. Swagger UI: https://card.taba.asia/docs
| Method | Path | 용도 |
|---|---|---|
GET |
/api/members |
멤버 8명 메타 |
GET |
/api/series |
시리즈 시드 |
GET |
/api/master/status |
빌드 여부 + 진행률 |
GET |
/api/master/log |
빌드 로그 꼬리 + raw 디스크 요약 |
GET |
/api/master/catalog |
sample-as-entry catalog 그룹 |
POST |
/api/master/rebuild |
비동기 재빌드 (?fresh, ?discover) |
POST |
/api/master/rebuild/cancel |
진행 중 중단 |
| Method | Path | 용도 |
|---|---|---|
POST |
/api/identify |
Top 20 매칭 (가벼움) |
POST |
/api/export |
Top 20 + 시세 + image_base64 |
GET |
/api/price |
Mercari sold 시세 |
GET |
/api/entry/{id}/image |
sample-as-entry 매물 이미지 |
GET |
/api/sample/{sku}/{member} |
(시리즈, 멤버) 대표 이미지 |
GET |
/api/match/{sku}/{member}/detail |
매물 그리드 메타 |
GET |
/api/match/{sku}/{member}/sample/{idx} |
i 번째 매물 이미지 |
| Method | Path | 용도 |
|---|---|---|
POST |
/api/collection |
새 카드 등록 (multipart) |
GET |
/api/collection |
목록 |
GET |
/api/collection/{id} |
단건 |
GET |
/api/collection/{id}/image |
본인 카드 사진 |
PUT |
/api/collection/{id} |
수정 (image 변경 불가) |
DELETE |
/api/collection/{id} |
row + 이미지 삭제 |
GET |
/api/collection/_diag |
🆕 DB 상태 진단 (orphan 검출) |
| Method | Path | 용도 |
|---|---|---|
POST |
/api/v1/analyze |
외부용 매칭 API |
GET |
/api/v1/collection |
외부용 컬렉션 조회 |
# 매칭만
curl -X POST https://card.taba.asia/api/identify -F "image=@card.jpg"
# 매칭 + 시세 + 이미지 (5~15초)
curl -X POST https://card.taba.asia/api/export -F "image=@card.jpg"
# 컬렉션 등록
curl -X POST https://card.taba.asia/api/collection \
-F "item_code=CS-0170" -F "item_type=random" \
-F "member_id=sano_aika" -F "name=사노 아이카 · ランダム ver.7" \
-F "image=@my-card.jpg"
# 빌드 상태
curl https://card.taba.asia/api/master/status | jq
# 컬렉션 DB 진단 (안 보일 때)
curl https://card.taba.asia/api/collection/_diag | jquv run python scripts/build_master.py # 전체
uv run python scripts/build_master.py --workers 8 # 병렬
uv run python scripts/build_master.py --discover # Phase 0 ASOBIMALL 발굴
uv run python scripts/build_master.py --sku CS-0170 # 단일Phase 0 (옵션 --discover)
ASOBIMALL shop 스크래핑 → 신규 SKU 발견 → seed 머지
Phase 1 멀티프로세스 스크래핑 (N 워커, Playwright/Mercari)
· 검색 키워드: random = ランダム ver.N, 그 외 = トレカ + 라벨 + 멤버
· is_likely_card 필터 — 토레카/포카만 통과
(アクスタ, T 셔츠, 缶バッジ 등 굿즈 차단)
· is_likely_single_card — 묶음 매물 차단 (セット/8 枚/メンバー全員)
· 生写真 (인스탁스 사진) 정책상 제외
· 다운로드 검증: PIL.verify + 크기 + atomic write + Referer 헤더
Phase 2 싱글 프로세스 인코딩 (DINOv2-L + SigLIP-2 모델 1회 로드)
· outlier 필터 — cluster mean 기준 cosine ≥ 0.55 인 sample 만 보존
· 살아남은 raw 임베딩 각각을 catalog entry 1개로 (sample-as-entry)
Phase 3 catalog.json + embeddings.npy (N × 1792) 저장
· master/.embedder_version, .rebuild.log 갱신
- 웹 UI 메인 ⚙️ →
🔄 갱신(raw 재사용 빠름) /🧹 깊은 갱신(fresh + discover, 전부 새로) - API:
POST /api/master/rebuild?fresh=true&discover=true systemd-run --user --collecttransient unit 에서 돌아 deploy 시 같이 안 죽음- 진행 중
⛔ 빌드 취소가능 (SIGTERM → 3초 후 SIGKILL fallback)
ASOBIMALL discover 가 못 잡는 매장·라이브 한정 카드:
uv run python scripts/discover_listings.py
uv run python scripts/discover_listings.py \
--queries '振袖' 'ハロウィン' '生誕祭 2026' \
--min-freq 5 --per-query-limit 200그룹/멤버명 + 추가 키워드 × N 쿼리로 listing 제목 수집 → 노이즈 제거 (가격/일반어/멤버 잔재) → 시드 라벨 정규화 비교 → 빈도 ≥ min_freq 만 후보로 출력 + master/discovered_listings.json 저장 → 사용자가 검수 후 core/series_seed.py 추가.
collection/ 디렉토리는 어떤 자동 작업에도 비워지지 않음. 다음 경로들 모두 검증됨:
| 작업 | collection/ 건드림? |
|---|---|
git push → 서버 git pull |
❌ .gitignore 의 /collection/ 로 추적 안 됨 |
deploy/deploy.sh (매 push) |
❌ build/restart 만 |
cutie-card.service (systemd) |
❌ workdir 만 지정 |
deploy/rebuild-master.sh |
❌ rm -rf master/ 만 — master 한정 |
POST /api/master/rebuild?fresh=true |
❌ shutil.rmtree(P.RAW_DIR) 만 — master/raw/ 한정 |
컬렉션이 비어보이면 진단:
curl https://card.taba.asia/api/collection/_diag | jqdb_exists=false→ 디스크에서 디렉토리 통째로 사라진 거. 외부 정리 작업 의심.row_count=0+image_file_count>0→ DB row 만 비워짐. 이미지는 살아있어 복구 가능 (orphan_images확인 후 수동 INSERT).row_count>0+orphan_rows→ row 는 있는데 사진만 사라짐. 사진 재업로드 필요.
# 서버에 단일 명령 — collection/ 통째 압축
ssh coby@card.taba.asia 'cd ~/Card && tar czf ~/cutie-backup-$(date +%F).tgz collection/'
# 또는 로컬로 끌어오기
scp -r coby@card.taba.asia:~/Card/collection ./collection-backup-$(date +%F)자동화 (서버측 cron) — 매일 03:00 백업, 14일 보존:
# crontab -e
0 3 * * * cd ~/Card && tar czf ~/.cutie-backups/collection-$(date +\%F).tgz collection/ \
&& find ~/.cutie-backups -name 'collection-*.tgz' -mtime +14 -delete| Kind | 개수 | 내용 |
|---|---|---|
| ✨ 정규 | 10 | ver.1 ~ ver.10 |
| 👗 의상 | 11 | LARME lovers · 振袖 2026 · クリスマス 2024/2025 · たすき衣装 · 2nd LARME · 2nd クラゲ · 2025 SUMMER · 万博 · セーラー · チェック制服 |
| 🎤 이벤트 | 21 | 3rd Anniv L · KAWAII LAB 3rd · あそばにゃそんそん · カプセル ver.1/2 · 1st/2nd Single 特典 · Anniversary Tour · TikTok 撮影会 · トークポート · 1st ワンマン · 自撮り · KAWAII LAB SESSION · カワラボ 衣装展 · カワラボカフェ · 振袖 TGC カワコレ · 福岡 抽選会 · 生誕祭 入場特典 · 大特典会 ユニット衣装 |
| 🎂 생탄제 | 8 | 멤버 8명 각 단독 (연도 통합) |
빌드 시 --discover 활성하면 ASOBIMALL/Mercari 에서 신규 시리즈 자동 발견 + master/discovered_series.json 저장. 발견된 거 검토 후 core/series_seed.py 한 줄 추가 → 다음 빌드부터 catalog 포함.
인터넷 ─ HTTPS 443 / HTTP 80
↓
[iptime 라우터] 포트 80/443 포워딩
↓
[nginx (system)] taba.asia 본체 + 다른 앱과 공유
│ server_name card.taba.asia → proxy_pass 127.0.0.1:1024
│ ssl_certificate Let's Encrypt (certbot --nginx, 자동 갱신)
↓
[uvicorn / cutie-card.service] systemd-user, :1024
↓
[FastAPI + Next.js 정적 빌드 (web/out)]
- 도메인:
card.taba.asia→ A118.44.79.109(NS: gabia) - HTTPS: 기존 nginx 에 server block 추가 + certbot 자동 발급/갱신 (
certbot.timer) - 다른 앱 영향 0 — 신규 server block 만 추가
- 포트 1024 외부 노출 X — nginx 가 같은 호스트 내 internal proxy
| 워크플로우 | 트리거 | 소요 | 하는 일 |
|---|---|---|---|
Deploy code (.github/workflows/deploy.yml) |
git push origin main |
2~3분 | rsync + uv sync + next build + nginx sanity + systemctl restart |
Rebuild master DB (rebuild-master.yml) |
수동 (workflow_dispatch) | 30~60분 | rsync + rm -rf master/ + build_master.py --workers 8 |
웹 UI ⚙️ 배지 → 🔄 갱신 으로 백그라운드 트리거도 가능 (재배포 영향 없이 transient unit 에서 돌아감).
| Name | Value |
|---|---|
SSH_HOST |
card.taba.asia |
SSH_USER |
coby |
SSH_PRIVATE_KEY |
private key 전문 |
SSH_PORT |
(선택) 비표준 SSH 포트 |
HF_TOKEN |
(선택) HuggingFace 토큰 |
ssh coby@card.taba.asia
# 1) Playwright Chromium 시스템 라이브러리
sudo apt update && sudo apt install -y \
libnss3 libnspr4 libatk-bridge2.0-0 libdrm2 libxkbcommon0 \
libxcomposite1 libxdamage1 libxfixes3 libxrandr2 libgbm1 \
libpango-1.0-0 libcairo2 libasound2 libatspi2.0-0
# 2) Node.js (Next.js 빌드)
if ! command -v node >/dev/null; then
curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -
sudo apt install -y nodejs
fi
# 3) systemd linger (사용자 service 가 로그아웃 후에도 살아있게)
sudo loginctl enable-linger coby
# 4) nginx server block + Let's Encrypt 인증서
cd ~/Card && git pull
sudo bash deploy/nginx-setup.sh you@example.com# 앱 (uvicorn)
systemctl --user status cutie-card
systemctl --user restart cutie-card
journalctl --user -u cutie-card -f
# nginx (HTTPS)
sudo systemctl status nginx
sudo nginx -t && sudo systemctl reload nginx
# 인증서 자동 갱신
sudo systemctl status certbot.timer
sudo certbot renew --dry-run
# 진행 중 마스터 빌드 (transient unit)
systemctl --user list-units 'cutie-rebuild-*' --all
# 진단
curl https://card.taba.asia/api/master/status | jq
curl https://card.taba.asia/api/master/log?tail=8000 | jq
curl https://card.taba.asia/api/collection/_diag | jqCard/
├── api/
│ └── main.py # FastAPI · 23 endpoints
├── web/ # Next.js (output: 'export', trailingSlash: true)
│ ├── app/
│ │ ├── page.tsx # 갤러리 (메인)
│ │ ├── scan/page.tsx # 스캐너 4-step
│ │ ├── scan/compare/page.tsx # 등록 전 매물 비교 ✨
│ │ ├── scan/save/page.tsx # 등록 폼 (prefill or 직접 입력)
│ │ ├── scan/match/page.tsx # 멤버 매물 그리드
│ │ ├── master/page.tsx # 마스터 DB 카탈로그
│ │ ├── collection/page.tsx # 본인 카드 상세 (?id=)
│ │ ├── layout.tsx · globals.css
│ ├── components/
│ │ ├── steps/ (Upload · Member · Series · Result · SaveToCollection)
│ │ └── Button · Card · Chip · Hero · Progress · Section.tsx
│ ├── lib/api.ts # TS API wrapper (Match · ExportCandidate 등)
│ ├── tailwind.config.ts · next.config.mjs
├── core/ # Python 도메인 (UI 독립)
│ ├── paths.py # 절대경로 단일 공급
│ ├── models.py # Series · CatalogEntry · MatchResult
│ ├── members.py · series_seed.py
│ ├── catalog.py # 로드/저장/필터
│ ├── embedder.py # DINOv2-L + SigLIP-2 (1792d) + 회전 TTA
│ ├── matcher.py # 코사인 Top-K (sample 단위)
│ ├── crawler.py # ASOBIMALL/Mercari + 발굴
│ ├── price.py · noise_filter.py · browser.py
│ └── collection.py # SQLite + 이미지 IO
├── scripts/
│ ├── build_master.py # Phase 0~3
│ └── discover_listings.py # Mercari 빈도 분석
├── deploy/
│ ├── deploy.sh · rebuild-master.sh · cutie-card.service
│ ├── nginx-card.conf · nginx-setup.sh
├── .github/workflows/
│ ├── deploy.yml · rebuild-master.yml
├── docs/
│ ├── API.md # 📖 REST API 명세 (full)
│ └── superpowers/ # 초기 스펙 (Streamlit 시절)
├── tests/ # pytest 79개
├── master/ cache/ collection/ (런타임 생성, gitignored)
- Mercari/ASOBIMALL 스크래핑:
robots.txt존중 + 요청 간 1초 슬립 + jitter - 마스터·시세 데이터는 로컬 분석 전용, 재배포·재판매 안 함
- 카드 이미지 저작권 ASOBISYSTEM. 본인 컬렉션 관리 목적 개인 도구
설계 문서:
- 초기 스펙:
docs/superpowers/specs/2026-05-12-cutie-street-toreca-identifier-design.md - 초기 구현 계획 (Streamlit → Next.js 마이그 전):
docs/superpowers/plans/2026-05-12-cutie-street-toreca-identifier.md - 다른 머신에서 클론 후 컨텍스트 회수:
HANDOFF.md