Skip to content

CobyApp/Card

Repository files navigation

Cutie Card

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단 (결과 → 비교 → 저장)

이전엔 결과 화면에서 바로 등록 폼이 떴는데, 매물 비교를 위한 별도 화면(/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-L + SigLIP-2 앙상블 + 회전 TTA

모델 파라미터 차원 특성
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 회.

sample-as-entry catalog

매칭 단위 = 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 평균화로 인한 정보 손실 제거.


데이터 (3개 분리)

위치 무엇 어떻게 채워짐 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}/*.jpg
  • master/.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)

API 한눈에 (23 endpoints)

전체 명세 + curl 예시 + 응답 스키마: docs/API.md. Swagger UI: https://card.taba.asia/docs

Master / Reference (7)

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 진행 중 중단

식별·매칭 (7)

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 번째 매물 이미지

Collection CRUD (7)

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 검출)

Public v1 (2)

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 | jq

마스터 DB 빌드

uv 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 --collect transient unit 에서 돌아 deploy 시 같이 안 죽음
  • 진행 중 ⛔ 빌드 취소 가능 (SIGTERM → 3초 후 SIGKILL fallback)

신규 시리즈 발굴 — Mercari listing 빈도 분석

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 | jq
  • db_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

시리즈 카탈로그 (48 시드)

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 → A 118.44.79.109 (NS: gabia)
  • HTTPS: 기존 nginx 에 server block 추가 + certbot 자동 발급/갱신 (certbot.timer)
  • 다른 앱 영향 0 — 신규 server block 만 추가
  • 포트 1024 외부 노출 X — nginx 가 같은 호스트 내 internal proxy

자동 배포 (GitHub Actions)

워크플로우 트리거 소요 하는 일
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 에서 돌아감).

GitHub Secrets

Name Value
SSH_HOST card.taba.asia
SSH_USER coby
SSH_PRIVATE_KEY private key 전문
SSH_PORT (선택) 비표준 SSH 포트
HF_TOKEN (선택) HuggingFace 토큰

서버 1회 셋업 (sudo 필요)

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 | jq

디렉터리

Card/
├── 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. 본인 컬렉션 관리 목적 개인 도구

설계 문서:

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors