Ecommerce Template
Next.js·React 등으로 만든 최신 전자상거래 템플릿
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기Personal VHS collection manager backed by PostgreSQL/Neon. Scan tapes with barcode or AI vision, track condition and value, and manage your collection with optional serverless deployment.
저장소 README에서 가져옴
README 전체를 불러오는 중입니다.
README 전문 보기 ↗A personal tool to catalog a VHS collection — capturing what each tape is, what it might be worth, and building a record you can actually use (sell, store, share). Live at vhs.nitsuah.io. Backed by PostgreSQL, served by Express, containerized with Docker.
public/) served via Express (src/server.js)VHS-XXXX IDs; REST CRUD at /api/tapesin_collection, for_sale, sold, donated, missing, wanted; great/good/fair/poor with notesfor_sale tapes; copy-ready title, description, and suggested price per tapefor_sale tapesGOOGLE_CLIENT_ID blank to run as single-user/c/<uuid> URL per usernpm run test:journeys (Docker: see tests/journeys/README.md); visual baselines per step, failures filed as bot:journey issues.github/workflows/visual-docs.yml (shown below)src/server.js + src/modules/**)node --check syntax, dep-install check, Jest unit tests, Docker build smoke testsite/ at nitsuah.github.io/vhsnetlify/functions/server.js, serverless-http)Architecture — docs/diagrams/architecture.svg · source docs/diagrams/architecture.mmd
Capture flow — docs/diagrams/capture-flow.svg · source docs/diagrams/capture-flow.mmd
Job lifecycle — docs/diagrams/job-lifecycle.svg · source docs/diagrams/job-lifecycle.mmd
Ai photo scanning — docs/screenshots/ai-photo-scanning.png
Collection first default view — docs/screenshots/collection-first-default-view.png
Condition tracking — docs/screenshots/condition-tracking.png
Csv export — docs/screenshots/csv-export.png
Full text search — docs/screenshots/full-text-search.png
Hamburger drawer — docs/screenshots/hamburger-drawer.png
Mobile ui — docs/screenshots/mobile-ui.png
Sort — docs/screenshots/sort.png
Sortable resizable wall table columns — docs/screenshots/sortable-resizable-wall-table-columns.png
Tabbed edit form — docs/screenshots/tabbed-edit-form.png
Wall view — docs/screenshots/wall-view.png
cp .env.example .env # fill in DATABASE_URL and HOST_IP at minimum
docker compose -f config/docker-compose.yml up -d --build
App at http://localhost:8080 (or whatever APP_PORT you set in .env).
HTTPS at https://localhost:8443.
The Express app can also run as a Netlify Function. The serverless-http package wraps it as a Lambda-compatible handler. Static files are served from public/; all /api/* and /auth/* routes are proxied to the function via netlify.toml.
# Deploy via Netlify CLI
netlify deploy --prod
Migrations run automatically on each cold start (idempotent). Set the same .env variables (especially DATABASE_URL) as Netlify environment variables.
Note: The Ollama proxy and local HTTPS cert generation are Docker-only features. The Netlify path assumes Claude Vision API (
ANTHROPIC_API_KEY) for AI scanning.
Auth is disabled by default — the app runs as a single-user tool with no login required.
To enable Google OAuth:
http://localhost:8080/auth/google/callback (or your APP_BASE_URL).env:
GOOGLE_CLIENT_ID=...
GOOGLE_CLIENT_SECRET=...
JWT_SECRET=<run: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))">
APP_BASE_URL=http://localhost:8080
To migrate existing un-owned tapes to your Google account on first login:
[email protected]
Mobile browsers block camera access on plain HTTP. The app auto-generates a self-signed TLS cert on first boot and serves it for easy installation.
One-time setup per device:
HOST_IP=<your LAN IP> in .env (e.g. HOST_IP=192.168.1.171)docker compose -f config/docker-compose.yml up -d --builddocker compose -f config/docker-compose.yml exec web openssl x509 -in /app/certs/ca.crt -noout -fingerprint -sha256
http://192.168.1.171:8080/api/ca-certhttps://192.168.1.171:8443 on your phone — camera will workIf your IP changes:
docker volume rm vhs_certs # forces cert regeneration with new IP on next start
Note: The cert is self-signed by a local CA that only your devices trust. Traffic never leaves your LAN. The cert lasts 10 years.
vhs/
├── config/
│ └── docker-compose.yml
├── docs/ ← CHANGELOG, FEATURES, ROADMAP, TASKS
├── migrations/ ← SQL schema migrations (run on startup)
├── public/ ← browser app static files
│ └── js/ ← ES-module frontend
│ └── modules/ ← sub-modules (list-view, wall-view, etc.)
├── src/
│ ├── server.js ← Express entry point
│ └── modules/ ← auth, config, routes
├── tests/ ← Jest unit tests
├── tests_playwright/ ← Playwright E2E tests
├── .env.example ← copy to .env and fill in
└── Dockerfile
Each tape is one row in the PostgreSQL tapes table. The row schema is:
| SQL Column | Type | Notes |
|---|---|---|
id | TEXT | Immutable identifier (primary key); VHS-XXXX format when assigned at creation, or the scanned UPC barcode if a barcode is captured before the VHS-XXXX is assigned |
data | JSONB | Full tape object — all metadata fields live here |
scanned_at | TEXT | ISO timestamp; default sort key |
owner_id | TEXT | Google account sub; NULL in single-user mode |
All tape metadata is stored inside the data JSONB column as a single document. Key fields within that document:
| Field | Notes |
|---|---|
id | Mirrors the SQL id; VHS-XXXX or barcode UPC when valid |
title | Required; search, sort, display, lookup key for Fill |
year | OMDb authoritative source; fill target |
label | VHS distributor/studio; e.g. Paramount Home Video |
format | VHS, DVD, Blu-ray, etc. |
condition | great, good, fair, poor |
condition_notes | Free text; detail modal only |
status | in_collection, for_sale, sold, donated, missing, wanted |
tags | Array of genre/custom tag strings |
value_low / value_high | USD resale estimate range |
sold_price | Actual sale price (shown when status = sold) |
barcode | UPC/EAN scan identifier; dedup guard |
imdb_id | OMDb confidence gate for Fill |
photos | Array of base64-encoded compressed images (max 1200px, JPEG 0.75) |
photo_thumbnail | Auto-set on confirm; universal thumbnail fallback |
photo_face | User-pinned in detail modal; cover-wall primary |
photo_spine | User-pinned in detail modal; spine-wall primary |
notes | Searchable free-text field |
Immutable IDs: once a tape gets a VHS-XXXX ID, it keeps it forever. If a barcode is scanned first and no VHS-XXXX exists yet, the UPC becomes the id if it is not already in use.
tapes.json when multi-user auth arrived. PostgreSQL via Neon handles concurrent writes, upserts, and user-scoped queries cleanly. The ID scheme (VHS-XXXX) is still immutable. (data/tapes.json remains in the repo as an empty legacy file from the initial version.)config/docker-compose.yml) and Netlify Functions (see netlify/functions/server.js and netlify.toml). The serverless-http package wraps the Express app as a Lambda-compatible handler.GOOGLE_CLIENT_ID is not set, the app runs with no login wall and all tapes are global."great". Notes field for anything specific.status field handles everything: in_collection, for_sale, sold, donated. No archive table needed.Every doc at the repo root (other than this README) and under docs/ (the files mirrored into the Obsidian vault), so none of them is orphaned.
docs/CHANGELOG.mddocs/EASTEREGG.mddocs/FEATURES.mddocs/METRICS.mddocs/ROADMAP.mddocs/TASKS.mddocs/system-flow.md