Saleor Store
우리 컬렉션을 쇼핑하세요
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기Dress rental marketplace platform
저장소 README에서 가져옴
README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
A full-stack dress rental marketplace. Dress owners list items for rent; renters browse an approved public catalog, check real-time availability, and book by date range and size. Built end-to-end (schema, API, auth, business logic, UI) as an independent project.
UI is in Hebrew with full RTL support.README 전문 보기 ↗A full-stack dress rental marketplace. Dress owners list items for rent; renters browse an approved public catalog, check real-time availability, and book by date range and size. Built end-to-end (schema, API, auth, business logic, UI) as an independent project.
Stack: NestJS · Prisma · PostgreSQL · Next.js (App Router) · React · TypeScript · Tailwind CSS
UI is in Hebrew with full RTL support.
Catalog
page/limit, with total match count)/catalog-pdf) via the browser's own print-to-PDF, listing every approved dress with a real clickable link to its page — for anyone who can browse the images offline but can't reach the live site to click through directlyrouter.back() real history pop, paired with an in-memory cache of the last fetched results (and view state, on the catalog) so the grid re-renders at full height on the same page instantly instead of flashing an empty loading skeleton first (which would otherwise cut the browser's own scroll restoration short)Marketing
/promo.html (apps/web/public/promo.html) — a self-contained static file (own HTML/CSS/JS, no dependency on the rest of the app or any external account) meant to be shared directly for real distribution. Payment-by-credit-card is explicitly marked "coming soon" there, since it isn't live yet.app/dress/[id]/page.tsx is a real Server Component with a generateMetadata that fetches server-side (crawlers never run the client-side fetch the page content itself uses) against a dedicated single-dress endpoint (GET /dresses/approved/:id) rather than the paginated catalog listlocalStorage) so it doesn't nag a returning visitorListings
pendingDetails, pendingAction: ADD/REMOVE on sizes/photos) rather than mutating the live rowBookings
SERIALIZABLE transaction with automatic retry, so two simultaneous requests for the last unit of a size can't both succeedINTERESTED holds that never convert to a confirmed rental auto-expire after 7 days (scheduled job, also runs once on startup so a restart doesn't wait for the next midnight run) and release their date/size back into the calendarDressAvailabilityBlock, folded into the same public availability feed the calendar readsINTERESTED booking herself (hard-deleted — nothing meaningful to preserve), instead of only being able to wait out the 7-day auto-expiry or ask the owner to do it; cancelling an already-RENTED booking still requires the owner, since that's a real confirmed rentalRENTED booking's end date has passed, the renter can leave a 1-5 star rating and an optional comment from "הבקשות שלי" (one review per booking, enforced by a unique constraint). A dress's averageRating/reviewCount are denormalized onto the Dress row itself (recomputed via aggregate whenever a review is created), so the rating shown on every catalog card, and the reviews list on the dress's own page, never costs a join or a live aggregate on a normal page viewRenter-facing UI
Favorite join table (one row per user/dress pair, idempotent add/remove); a "מועדפים" page lists everything favorited, with the same card and unfavorite-to-remove interactionOwner-facing UI
Notifications
INTERESTED hold is about to auto-expire[dev-only] console log used before — nothing breaks, emails just aren't delivered to real inboxes yetNotificationsService.send() choke point — the eventual domain-verified from address change, or any future provider swap, touches one method, not each trigger siteAuth & authorization
USER / ADMIN) — the API refuses to start without a JWT_SECRET env var (no insecure hardcoded fallback)/auth route, and on feedback/review submission and dress photo upload — blocks brute-force login/credential-stuffing, registration/reset-email spam, and repeated hits on the CPU-heavy photo-processing pipeline?redirect= target, resolved through the same URL parser the browser's own navigation uses and only trusted when it still resolves to this origin (rather than a naive string-prefix check, which a crafted value using stripped-by-the-parser control characters could bypass) — a logged-out visitor who tries to express interest in a dress lands back on that exact dress page after signing in, instead of the homepage; the login↔register cross-link carries the same param through so switching between them doesn't lose ittokenVersion, embedded in each issued token and checked on every authenticated request — bumping it (from account settings' "התנתקות מכל המכשירים", or automatically on every password change/reset) instantly invalidates every token issued before that moment, everywhere, without needing a separate session store. A password change issues a fresh token for the device that just changed it, so that one stays logged in while every other session is cutAdmin
/admin/feedback) listing free-text suggestions/bug reports submitted by anyone (logged in or not), most recent first, with the sender's name/email when known or marked anonymous when not, and a delete action per entry so handled/irrelevant feedback doesn't pile up indefinitely| Backend | NestJS, TypeScript, Prisma ORM, PostgreSQL |
| Frontend | Next.js 16 (App Router), React 19, TypeScript, Tailwind CSS v4 |
| Auth | JWT (@nestjs/jwt), bcrypt |
| Testing | Jest + Supertest (unit/integration), Playwright (browser verification) |
apps/
api/ NestJS backend (REST API, Prisma schema & migrations)
web/ Next.js frontend
docker-compose.yml Local PostgreSQL
There's no root workspace config — each app is installed and run independently.
Prerequisites: Node.js 20+, Docker (for local Postgres) or an existing PostgreSQL instance.
# 1. Start Postgres
docker compose up -d
# 2. Backend
cd apps/api
npm install
cp .env.example .env # set DATABASE_URL and JWT_SECRET
npx prisma migrate dev
npm run db:seed # creates a seeded admin user, see below
npm run start:dev # http://localhost:3001
# 3. Frontend (separate terminal)
cd apps/web
npm install
npm run dev # http://localhost:3000
| App | Variable | Notes |
|---|---|---|
apps/api | DATABASE_URL | PostgreSQL connection string |
apps/api | JWT_SECRET | Secret used to sign auth tokens |
apps/api | PORT | Optional, defaults to 3001 |
apps/api | FRONTEND_URL | Optional. Allowed CORS origin, defaults to http://localhost:3000 — set to the real frontend URL on deploy |
apps/api | PUBLIC_APP_URL | Optional. Base URL used inside real email links (password reset, etc.) only — separate from FRONTEND_URL so local dev can send real emails (via a real RESEND_API_KEY) with links pointing at the real production site instead of localhost. Falls back to FRONTEND_URL, so production never needs to set it |
apps/api | GOOGLE_VISION_API_KEY | Optional. Face blurring works for free without this (local BlazeFace model) — this only adds a Cloud Vision fallback for photos the local model misses. Requires billing enabled on the Google Cloud project |
apps/api | RESEND_API_KEY | Optional. Real email delivery. Without a verified sending domain, real recipients get rejected by Resend and notifications fall back to a console log |
apps/api | R2_ACCOUNT_ID, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, R2_BUCKET_NAME, R2_PUBLIC_URL | Optional. Dress photo storage on Cloudflare R2. Without these (or if R2 is unreachable), uploads fall back to local disk under apps/api/uploads automatically — the app works fine either way |
apps/web | NEXT_PUBLIC_API_URL | Optional, defaults to http://localhost:3001 |
[email protected] / Admin123!dev
Change this before deploying anywhere reachable outside your own machine.
cd apps/api
npm test # unit + integration (Jest, Supertest)
npm run test:cov # with coverage
apps/web): Vercel, with the project's root directory set to apps/web. Set NEXT_PUBLIC_API_URL to the deployed API's URL.apps/api) + database: Railway (or any Node host + managed Postgres), with the service's root directory set to apps/api. Build command npm run build, start command npm run start:prod — this runs prisma migrate deploy before starting, so migrations apply automatically on every deploy. Set every variable from the environment variables table above, especially a real (non-default) JWT_SECRET, DATABASE_URL pointing at the production Postgres instance, and FRONTEND_URL pointing at the deployed frontend.apps/api/uploads) is a dev-environment safety net, not meant to hold real production photos long-term, since most Node hosts (Railway included) wipe the local filesystem on every redeploy.prisma migrate deploy build the schema; the seeded dev admin ([email protected]) is for local dev only, per the environment variables section above.ownerId before writing — a missing route guard alone would never be enough to leak data.pendingDetails JSON + pendingAction on child rows), so the public read path never has to branch on edit state — it simply never selects the pending fields.recommended/newest paginate at the database level, while price-sorted queries fetch all matches, sort in application code, and slice — trading one code path's efficiency for correctness rather than reaching for raw SQL.INTERESTED), which meant nothing stopped an owner from just arranging a rental off-platform and never touching the app at all. The fix wasn't a policy — it was making the renter the one who creates the INTERESTED row, with the owner responding rather than reporting, so the interaction actually has to happen on the platform to exist at all.NotificationsService.send(), which calls the real Resend API and falls back to a console log if that call fails or isn't configured. Every trigger site calls a named method on that service, never a provider directly, so the eventual "real domain verified, remove the fallback" change is one-file.