ν μ΄λΈμ€λ Β· ν€μ€μ€ν¬ μ¨μ΄ν Β· μ£Όλ°© Β· ν Β· ν¬μ€λ₯Ό νλλ‘ λ¬Άμ λ§€μ₯ μ΄μ SaaS
μλμ΄ μ£Όλ¬Έμ λ£λ μκ°λΆν° μ£Όλ°©Β·νΒ·ν¬μ€κΉμ§, λ§€μ₯ μμ λͺ¨λ νλ©΄μ΄ κ°μ μνλ₯Ό λ΄ λλ€.
κ°μ β’ λ°λͺ¨ β’ μν€ν μ² β’ μ£Όμ κΈ°λ₯ β’ μ€κ³ ν¬μΈνΈ β’ μμνκΈ°
μλΉ ν κ³³μμ λ²μ΄μ§λ μΌμ μκ°λ³΄λ€ μ¬λ¬ νλ©΄μ κ±Έμ³ μμ΅λλ€. μ ꡬμμλ μ¨μ΄ν μ λ°κ³ , ν μ΄λΈμμλ μ£Όλ¬Έμ΄ λ€μ΄μ€κ³ , μ£Όλ°©μ κ·Έ μ£Όλ¬Έμ 쑰리 μμλ‘ λ΄μΌ νκ³ , νμ μλΉ μ¬λΆλ₯Ό, ν¬μ€λ κ²°μ λ₯Ό μ²λ¦¬ν©λλ€. λ³΄ν΅ μ΄ νλ©΄λ€μ μλ‘ λ€λ₯Έ μ νμ΄κ³ , κ·Έλμ μνκ° μ΄κΈλ©λλ€.
KiOrderλ μ΄ νλ¦ μ 체λ₯Ό νλμ λ°μ΄ν° μμ€ μμ μ¬λ¦½λλ€. μ£Όλ¬Έ νλκ° μκΈ°λ©΄ μ£Όλ°©Β·νΒ·ν¬μ€κ° ν΄λ§ μμ΄ λμμ κ·Έ μ¬μ€μ μλλ€. μ¨μ΄ν λ±λ‘λ λ§μ°¬κ°μ§λ‘, ν€μ€μ€ν¬μμ μλμ΄ λ²νΈλ₯Ό λ°λ μκ° μ¬μ₯λ κ΄λ¦¬ νλ©΄μ λνλ©λλ€.
ν¬νΈν΄λ¦¬μ€ νλ‘μ νΈλ‘ 6κ° μ¬μ©μ μ ν(μλ Β· μ£Όλ°© Β· ν Β· ν¬μ€ Β· μ¬μ₯λ Β· μμ€ν κ΄λ¦¬μ)μ νλ©΄μ μ€μ λ‘ κ΅¬ννμ΅λλ€.
Tip
λ°λ‘ λλ¬λ³΄κΈ° β https://kiorder.vercel.app
λλ© νμ΄μ§μ νλ©΄ μΉ΄λλ₯Ό λλ₯΄λ©΄ λ‘κ·ΈμΈ μμ΄ ν΄λΉ νλ©΄μΌλ‘ λ€μ΄κ°λλ€.
μ§μ λ‘κ·ΈμΈνλ €λ©΄ owner1@test.com / test1234 λ₯Ό μ°μΈμ.
| κ°λ° κΈ°κ° | 2026.04.25 ~ 2026.08.08 (μ½ 3.5κ°μ) |
| νμ΄μ§ λΌμ°νΈ | 17κ° (λ§€μ₯ μ΄μ νλ©΄ 14κ°) |
| REST μλν¬μΈνΈ | 22κ° |
| DB ν μ΄λΈ | 7κ° |
| λ¨Έμ§λ PR | 20건 |
μλμ΄ ν
μ΄λΈμ€λλ‘ μ£Όλ¬Έμ λ£μΌλ©΄ μ£Όλ°© μΉΈλ°μ μ¦μ μΉ΄λκ° μκΉλλ€. ν΄λ§μ΄ μλλΌ Supabase Realtimeμ postgres_changes ꡬλ
μ
λλ€.
μ¨μ΄ν λ±λ‘ β QR λκΈ°ν λ°κΈ β μλμ΄ QRλ‘ λκΈ°νν© νμΈ β μ¬μ₯λ νΈμΆ β μλ μλ΅κΉμ§, μ κ³Όμ μ΄ λκΈ°μ§ μκ³ μ΄μ΄μ§λλ€.
ν μ΄λΈμ€λλ μλ κ°μΈ ν΄λν°μμλ μ΄λ¦½λλ€. 3μ΄ κ³ μ λ μ΄μμμ΄ 390pxμμ 무λμ§λ κ²μ μΈλ‘ μ μΈ΅μΌλ‘ λ°κΎΈκ³ , μ₯λ°κ΅¬λλ νλ¨ λ° + μνΈλ‘ λΆλ¦¬νμ΅λλ€.
| Before | After |
|---|---|
![]() |
![]() |
λ κ°λμ μλ μ λ ₯μ΄ νλμ NestJS APIλ‘ λͺ¨μ΄κ³ , DB λ³κ²½μ΄ RealtimeμΌλ‘ λ§€μ₯ μ΄μ νλ©΄μ νΌμ§λλ€. NestJSλ μμ RESTλ§ λ΄λΉνκ³ μ€μκ° μ νλ Supabaseκ° λ§‘λ ꡬ쑰μ λλ€.
[μλ β ν€μ€μ€ν¬] [μλ β ν
μ΄λΈμ€λ]
β β
POST /waiting POST /orders
βββββββββββββ¬βββββββββββββ
βΌ
NestJS API (REST Β· JWT Β· class-validator)
β
Prisma 7 (@prisma/adapter-pg)
β
Supabase PostgreSQL
β
Supabase Realtime (CDC)
β
βββββββββββββββΌββββββββββββββ¬βββββββββββββββ
βΌ βΌ βΌ βΌ
[μ£Όλ°© μΉΈλ°] [ν μ£Όλ¬Έ] [ν¬μ€ κ²°μ ] [μ€λ μ¨μ΄ν
]
λ‘κ·ΈμΈ β NestJSκ° HttpOnly μΏ ν€(access_token) λ°κΈ
β
βΌ
Next.js proxy.ts β jose jwtVerify λ‘ μλͺ
κ²μ¦
β
βββββββββββ΄ββββββββββ
βΌ βΌ
SYSTEM_ADMIN STORE_OWNER
/system-admin/* /owner/* Β· /kitchen Β· /hall Β· /pos Β· /kiosk
- XSS λ°©μ΄ β HttpOnlyλΌ JSμμ μΏ ν€μ μ κ·Όν μ μμ΅λλ€
- CSRF λ°©μ΄ β
SameSite=lax - μλͺ
κ²μ¦ β λ¨μ λμ½λ©μ΄ μλλΌ
jose jwtVerify
Note
DBμ Role enumμ SYSTEM_ADMIN, STORE_OWNER 2μ’
μ
λλ€.
μμμ λ§ν "6κ° μ¬μ©μ μ ν"μ νλ©΄ κΈ°μ€μ΄λ©°, μλΒ·μ£Όλ°©Β·νΒ·ν¬μ€ νλ©΄μ λ‘κ·ΈμΈ κ³μ μ΄ μλλΌ κ²½λ‘λ‘ κ΅¬λΆν©λλ€.
| μ¬μ©μ μ ν | κ²½λ‘ | νλ μΌ |
|---|---|---|
| μλ β ν€μ€μ€ν¬ | /kiosk/waiting |
μ¨μ΄ν λ±λ‘, QR λκΈ°ν μλ Ή |
| μλ β λκΈ°νν© | /waiting/[waitingId] |
λ΄ μλ² νμΈ, μ¬μ₯λ νΈμΆμ μλ΅ |
| μλ β ν μ΄λΈμ€λ | /table-order/[tableId]/menu |
λ©λ΄ μ‘°ν, μ₯λ°κ΅¬λ, μ£Όλ¬Έ μ μΆ |
| μ£Όλ°© | /kitchen/orders |
μ£Όλ¬Έ μμ , 쑰리 μν μ ν |
| ν | /hall/orders |
μ£Όλ¬Έ νμΈ λ° μλΉ μ²λ¦¬ |
| ν¬μ€ | /pos |
ν μ΄λΈλ³ μ£Όλ¬Έ μ§κ³, κ²°μ |
| μ¬μ₯λ | /owner/dashboard |
λ©λ΄ Β· μ¨μ΄ν Β· ν μ΄λΈ μ€μ |
| μμ€ν κ΄λ¦¬μ | /system-admin/stores |
λ§€μ₯ λͺ©λ‘, ꡬλ μΉμΈ/ν΄μ |
ν°μΉ ν€ν¨λλ‘ μ νλ²νΈλ₯Ό μ
λ ₯νκ³ μΈμμλ₯Ό κ³ λ₯΄λ©΄ POST /waiting μΌλ‘ λ±λ‘λ©λλ€. μλ£ νλ©΄μ νλΈλ¦Ώ κ°λ‘ κΈ°μ€ 2λΆν (λκΈ°λ²νΈ + QR)μ΄κ³ , QRμ μ°μΌλ©΄ μλ μ μ© λκΈ°νν© νμ΄μ§λ‘ λμ΄κ°λλ€.
μ ν΄ μνκ° μ΄μ΄μ§λ©΄ κ΄κ³ ν면보νΈκΈ°κ° λΉλλ€ β μ€μ λ§€μ₯ ν€μ€μ€ν¬κ° λκΈ° μκ°μ λμ§ μκ² νλ μ₯μΉμ λλ€.
15μ΄λ§λ€ μλ κ°±μ λκ³ (setInterval + fetchRef ν¨ν΄μΌλ‘ stale closure ννΌ), μλ μλ‘κ³ μΉ¨μ 1μ΄ μΏ¨λ€μ΄μ λ‘λλ€. μ¬μ₯λμ΄ νΈμΆνλ©΄ μλμ κ°κ³ μμ΄μ / λ¦μ΄μ / μ·¨μ λ‘ μλ΅ν μ μμ΅λλ€(PATCH /waiting/:id/guest-response). μ
μ₯μλ£Β·μ·¨μ μνκ° λλ©΄ λ§λ£ νμ΄μ§λ‘ λ체ν©λλ€.
- μ£Όλ°© β μ μλ¨ β μ‘°λ¦¬μ€ β μλ£ μΉΈλ°. 쑰리 μκ°μ μ¬κ³ 15λΆμ λκΈ°λ©΄ νμ΄λ¨Έκ° λΉ¨κ°κ² λ°λλλ€
- ν β
PATCH /orders/:id/hall-receiveλ‘ μλΉ μ²λ¦¬ - ν¬μ€ β ν μ΄λΈλ³ μ£Όλ¬Έμ λͺ¨μ κ²°μ μ²λ¦¬, κ²°μ ν ν μ΄λΈ μ μ ν΄μ
μΈ νλ©΄ λͺ¨λ μ§μ
μ GET /orders λ‘ μ΄κΈ° λ‘λν λ€ Realtime ꡬλ
μΌλ‘ μ΄μ΄λ°μ΅λλ€.
ν€μ€μ€ν¬ μ κ· λ±λ‘μ΄ RealtimeμΌλ‘ μ¦μ λνλ©λλ€. μ
μ₯μλ£Β·μ·¨μν νμ λͺ©λ‘μμ μ§μ°μ§ μκ³ λ§¨ λ€λ‘ 보λ΄λ©° μ·¨μμ μ²λ¦¬ ν©λλ€ β λ°©κΈ λ¬΄μ¨ μΌμ΄ μμλμ§κ° μ΄μ μ€μλ μ 보μ΄κΈ° λλ¬Έμ
λλ€. μ νλ²νΈλ 010-****-5678 λ‘ λ§μ€νΉν©λλ€.
react-hook-form + zod μ€ν€λ§ κ²μ¦, νλ§€μ€/νμ Switch ν κΈ(PATCH /menu/:id), μΉ΄ν
κ³ λ¦¬ νν°λ§μ μ§μν©λλ€.
| κΈ°μ | μν |
|---|---|
| Next.js 16 (App Router) | Route Group (public) / (owner) / (system) μΌλ‘ μν λ³ λ μ΄μμ λΆλ¦¬ |
| React 19 Β· TypeScript | μ λ μ΄μ΄ νμ μμ μ± |
| TanStack Query v5 | μλ² μν κ΄λ¦¬, μΊμ 무ν¨ν |
| Tailwind CSS v4 + shadcn/ui | λ€ν¬ ν
λ§ λμμΈ μμ€ν
(DESIGN.md) |
| Supabase Realtime | WebSocket κΈ°λ° DB λ³κ²½ ꡬλ |
| motion v12 | fly-to-cart, μμ°¨ λ±μ₯ λ± μΈν°λμ |
| react-hook-form + zod | νΌ μν + μ€ν€λ§ κ²μ¦ |
| jose | proxy.ts μμ JWT μλͺ
κ²μ¦ |
| react-qr-code Β· sonner | QR μμ± Β· Toast |
| κΈ°μ | μν |
|---|---|
| NestJS 11 | REST API, λͺ¨λ/μλΉμ€/컨νΈλ‘€λ¬ ꡬ쑰 |
| Prisma 7 | νμ
μΈμ΄ν ORM (@prisma/adapter-pg μ΄λν° λ°©μ) |
| Supabase (PostgreSQL) | λ©μΈ DB + Realtime CDC |
| Passport JWT + bcrypt | HttpOnly μΏ ν€ κΈ°λ° μΈμ¦ |
| class-validator | DTO μ
λ ₯ κ²μ¦, ValidationPipe μ μ λ±λ‘ |
User 1ββ1 Store
Store 1ββN Table
Store 1ββN MenuItem
Store 1ββN Order
Store 1ββN WaitingEntry
Order 1ββN OrderItem
Table 1ββN Order
MenuItem 1ββN OrderItem
μ Supabase RealtimeμΈκ° WebSocket μλ²λ₯Ό μ§μ μΈμ°λ λμ PostgreSQLμ CDC(Change Data Capture)λ₯Ό ꡬλ ν©λλ€. NestJSλ RESTμλ§ μ§μ€νκ³ μ€μκ° λ μ΄μ΄λ Supabaseκ° λ΄λΉνλ κ΄μ¬μ¬ λΆλ¦¬μ λλ€. μ£Όλ°©Β·νΒ·ν¬μ€κ° κ°κ° ν΄λ§νλ€λ©΄ νλ©΄ μλ§νΌ μμ²μ΄ λμμ κ²μ λλ€.
μ HttpOnly μΏ ν€μΈκ° localStorage λ°©μμ XSS ν λ²μ΄λ©΄ ν ν°μ΄ ν΅μ§Έλ‘ νΈλ¦½λλ€. ν€μ€μ€ν¬Β·ν μ΄λΈμ€λμ²λΌ λΆνΉμ λ€μκ° λ§μ§λ κΈ°κΈ°μμλ JSκ° ν ν°μ μ κ·Όν μ μμ΄μΌ ν©λλ€.
μ λκΈ° ν μλ₯Ό μ μλ²μμ μΈλκ°
ν΄λΌμ΄μΈνΈμμ κ³μ°νλ €λ©΄ μ 체 λκΈ° λͺ©λ‘μ λ΄λ €μ€μΌ νκ³ , κ·Έ λͺ©λ‘μλ λ€λ₯Έ μλμ μ νλ²νΈκ° λ€μ΄ μμ΅λλ€. μλ²μμ status IN ('λκΈ°μ€','νΈμΆμ€') AND number < λ΄λ²νΈ λ‘ μ§κ³κ°λ§ λ°νν΄ κ°μΈμ 보 λ
ΈμΆ κ²½λ‘λ₯Ό μμ΄μ΅λλ€.
μ£Όλ¬Έ μ€λ³΅μ μ μλ²μμ λ§λκ° λ²νΌ μ°νλ‘ κ°μ μ£Όλ¬Έμ΄ λ λ² λ€μ΄κ°λ λ¬Έμ λ₯Ό ν΄λΌμ΄μΈνΈ κ°λλ‘ λ¨Όμ λ§μμ§λ§, κ·Έκ²λ§μΌλ‘λ μμ²μ μ§μ λ§λ€μ΄ 보λ΄λ κ²½μ°λ₯Ό λͺ» λ§μ΅λλ€. μλ²μμ νμ μ¬λΆΒ·μλΒ·κ°κ²©Β·νλ§€μ₯ λ©λ΄κΉμ§ λ€μ κ²μ¦ν©λλ€. κ°κ²©μ ν΄λΌμ΄μΈνΈκ° 보λ΄μ€ κ°μΌλ‘ λ―Ώμ§ μλ κ²μ΄ ν΅μ¬μ λλ€.
λ©λ΄λ μ μννΈ μμ μΈκ°
μ£Όλ¬Έ μ΄λ ₯μ΄ μλ λ©λ΄λ₯Ό νλ μμ νλ©΄ OrderItem μ FKκ° κΉ¨μ Έ 500μ΄ λ©λλ€. μ§λ μ£Όλ¬Έμμμ λ©λ΄ μ΄λ¦μ΄ μ¬λΌμ§λ κ²μ λ§€μ₯ μ
μ₯μμλ μ¬κ³ μ΄λ―λ‘, μμ λμ λΉνμ± μ²λ¦¬ν΄ μ°Έμ‘° 무결μ±κ³Ό κ³Όκ±° λ°μ΄ν°λ₯Ό ν¨κ» μ§ν΅λλ€.
- Node.js 20+
- Supabase νλ‘μ νΈ (PostgreSQL)
cd backend
npm install
# .env λ₯Ό λ§λ€κ³ μλ νκ²½λ³μλ₯Ό μ±μλλ€
npx prisma migrate dev
npm run start:devcd frontend
npm install
# .env.local μ λ§λ€κ³ μλ νκ²½λ³μλ₯Ό μ±μλλ€
npm run devbackend/.env
DATABASE_URL= # Supabase pooler (port 6543)
DIRECT_URL= # Supabase direct (port 5432)
JWT_SECRET= # 64λ°μ΄νΈ λλ€ hex
FRONTEND_URL= # CORS νμ© originfrontend/.env.local
NEXT_PUBLIC_BACKEND_URL=
NEXT_PUBLIC_FRONTEND_URL=
NEXT_PUBLIC_SUPABASE_URL=
NEXT_PUBLIC_SUPABASE_ANON_KEY=
JWT_SECRET= # λ°±μλμ λ°λμ λμΌν κ°Important
JWT_SECRET μ νλ‘ νΈΒ·λ°±μλκ° κ°μμΌ ν©λλ€. proxy.ts μ jose jwtVerify κ° μ΄ κ°μΌλ‘ μλͺ
μ κ²μ¦νκΈ° λλ¬Έμ, κ°μ΄ λ€λ₯΄λ©΄ λ‘κ·ΈμΈμ λλλ° λͺ¨λ λ³΄νΈ λΌμ°νΈμμ νκΉλλ€.
Note
Prisma 7 μ£Όμ β schema.prisma μ datasource url μ μΈ μ μμ΅λλ€.
λ°νμμ @prisma/adapter-pg λ‘ μ§μ μ°κ²°νκ³ , CLI λ§μ΄κ·Έλ μ΄μ
μ prisma.config.ts μ DIRECT_URL μ μ¬μ©ν©λλ€.
kiorder/
βββ frontend/ # Next.js 16 App Router
β βββ app/
β β βββ page.tsx # λλ© β νλ©΄ λλ¬λ³΄κΈ° + μλ λ‘κ·ΈμΈ
β β βββ (public)/ # μΈμ¦ λΆνμ (λ‘κ·ΈμΈ, μλ λκΈ°νν©)
β β βββ (owner)/ # kiosk Β· kitchen Β· hall Β· pos Β· table-order Β· owner/*
β β βββ (system)/ # μμ€ν
κ΄λ¦¬μ
β βββ components/ # kiosk Β· kitchen Β· hall Β· owner Β· table-order Β· screensaver Β· shared Β· ui
β βββ hooks/ # useTableMenu Β· useOwnerMenu Β· usePosOrders Β· useFlyToCart Β· useIdle β¦
β βββ lib/
β β βββ api.ts # apiFetch κ³΅ν΅ ν΄λΌμ΄μΈνΈ
β β βββ supabase.ts # Realtime ν΄λΌμ΄μΈνΈ
β β βββ motion.ts # λͺ¨μ
ν ν°
β βββ types/ # menu Β· order Β· store Β· waiting
β βββ proxy.ts # Route Guard (jose jwtVerify)
β
βββ backend/ # NestJS 11
βββ src/
β βββ auth/ # JWT μΈμ¦, HttpOnly μΏ ν€
β βββ store/ # λ§€μ₯ κ΄λ¦¬
β βββ menu/ # λ©λ΄ CRUD (μννΈ μμ )
β βββ order/ # μ£Όλ¬Έ Β· μν μ ν Β· μλ² κ²μ¦
β βββ table/ # ν
μ΄λΈ
β βββ waiting-entry/ # μ¨μ΄ν
(μ λκΈ° ν μ§κ³ ν¬ν¨)
β βββ prisma/ # PrismaService (@prisma/adapter-pg)
βββ prisma/
β βββ schema.prisma
βββ prisma.config.ts # Prisma 7 CLI μ€μ (DIRECT_URL)
| λμ | νλ«νΌ |
|---|---|
| Frontend | Vercel |
| Backend | Render |
| Database | Supabase |
Render λ¬΄λ£ νλμ μ¬λ¦½μ νΌνκΈ° μν΄ GET /health λ₯Ό λκ³ μ£ΌκΈ°μ μΌλ‘ κΉ¨μλλ€.



