Saleor Store
우리 컬렉션을 쇼핑하세요
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
ระบบร้านเบเกอรี่ทั้ง หน้าร้านออนไลน์ (ลูกค้า) และ หลังร้าน (เจ้าของร้าน/พนักงาน) — โปรเจกต์นี้เป็น frontend อย่างเดียว Backend เป็นคนละโปรเจกต์ คุยกันผ่าน REST APIREADME 전문 보기 ↗
ระบบร้านเบเกอรี่ทั้ง หน้าร้านออนไลน์ (ลูกค้า) และ หลังร้าน (เจ้าของร้าน/พนักงาน) — โปรเจกต์นี้เป็น frontend อย่างเดียว Backend เป็นคนละโปรเจกต์ คุยกันผ่าน REST API
เอกสารนี้เขียนให้คนที่เพิ่งเข้าโปรเจกต์อ่านจบแล้ว แก้โค้ดได้เลย — อ่านจากบนลงล่างครั้งเดียวพอ
docs/ — เส้นทางการอ่าน + อันไหนไว้ทำอะไร/customer/* (ย้ายมาจากโปรเจกต์ FrontOffice เดิม): ดู/ค้นสินค้า · ตะกร้า · ชำระเงิน (QR พร้อมเพย์ + แนบสลิป) · พรีออเดอร์ตามรอบ · บัญชีของฉัน (ที่อยู่ · ประวัติ · แต้ม/คูปอง · รายการโปรด · แจ้งเตือน · รีวิว) · ติดต่อร้าน · ค่าจัดส่ง — 26 หน้า/owner/*: สินค้า · คำสั่งซื้อ · POS · รอบพรีออเดอร์ · วัตถุดิบ · สูตร · การผลิต · พนักงาน/สิทธิ์ · การเงิน · โปรโมชัน · รายงาน/รีวิว · ข้อมูลร้าน · แบนเนอร์ · แจ้งเตือน — 31 หน้า/login (+/login/line) · /register · /profile — รายการเต็มดู docs/SCREEN_MAP.md/api/catalog/* สาธารณะ · /api/shop/* ลูกค้า · /api/admin/* หลังร้าน) → ฐานข้อมูลcustomer/layout.tsx บังคับ locale th) แต่ข้อความอยู่ใน i18n แล้ว (shop.* ทั้ง th/en · #44) และ lint:i18n ตรวจหน้าร้านด้วยmeowmeecake-test (ดู docs/MOCKS.md)npm install
cp .env.example .env.local # แล้วแก้ค่าด้านล่าง
npm run dev # http://localhost:3001 (backend รันที่ :3000)
.env.local (ตัวเต็ม + คำอธิบายดู .env.example):
| ตัวแปร | ใส่อะไร | ถ้าไม่ใส่ |
|---|---|---|
NEXT_PUBLIC_API_BASE_URL | URL ของ backend ต่อท้าย /api เช่น http://localhost:3000/api | request ยิงไม่ถึง backend |
NEXT_PUBLIC_AUTH_COOKIE | ชื่อ cookie session ของ backend (ตอนนี้ session) | proxy.ts มองไม่เห็นว่า login แล้ว |
NEXT_PUBLIC_GOOGLE_CLIENT_ID | client ID ของ Google — ค่าเดียวกับ GOOGLE_CLIENT_ID ของ backend · ใช้กับปุ่ม Google login และปุ่ม "ลืมรหัสผ่าน?" (เลือกบัญชี Google) | ปุ่ม Google กดไม่ได้ · "ลืมรหัสผ่าน?" ไปหน้ากรอกอีเมลแทน |
API_PROXY_TARGET | URL ของ backend (ไม่มี /api) เช่น http://localhost:3000 — ใช้คู่กับ NEXT_PUBLIC_API_BASE_URL=/api ให้ส่งต่อ /api/* ผ่านโดเมนเดียวกัน | ต้องตั้ง NEXT_PUBLIC_API_BASE_URL เป็น URL เต็มแทน |
Google Cloud Console (OAuth client ตัวเดียวกับ backend) → Authorized JavaScript origins ต้องมี http://localhost:3001
(+ URL จริงของ frontend ตอน deploy) — ไม่มี = หน้าต่างเลือกบัญชี Google ไม่ขึ้น / ขึ้น error origin ไม่ถูกต้อง
· frontend ใช้แบบ popup จึงไม่ต้องลงทะเบียน redirect URI ให้ frontend
backend ต้องตั้ง STOREFRONT_URL=http://localhost:3001 (URL ของ frontend นี้) — ลิงก์ในอีเมล (ยืนยันอีเมล · ตั้งรหัสผ่านใหม่) ประกอบจากค่านี้
· ไม่ตั้ง = ใช้ NEXTAUTH_URL (= backend :3000) แล้วลิงก์เปิดไม่เจอหน้า
ทำงานครั้งแรกทำอะไร: รัน backend ที่ :3000 → ตั้ง .env.local ตามตาราง → npm run dev → หน้าร้านที่ http://localhost:3001/ · หลังร้าน login ที่ /login ด้วยบัญชีเจ้าของร้าน/พนักงานของ backend
| คำ | แปลเป็นภาษาคน |
|---|---|
| App Router | ระบบ routing ของ Next.js รุ่นใหม่ — โฟลเดอร์ใน src/app/ = URL (เช่น app/owner/products/page.tsx → /owner/products) |
| Server Component / Client Component | ไฟล์ที่ไม่มี "use client" = รันบน server (เร็ว, ไม่มี state) · มี "use client" = รันบนเบราว์เซอร์ (มี useState, onClick ได้) |
| DTO (Data Transfer Object) | หน้าตาของ JSON ที่ backend ส่งกลับ — เราเขียนเป็น interface ไว้ใน src/types/ เพื่อให้ TypeScript ช่วยเช็ค |
| Service | ฟังก์ชันเรียก API 1 endpoint (เช่น productsService.list()) — อยู่ใน src/services/ |
| ViewModel | custom hook ที่เก็บ "สมอง" ของหน้า (state, เรียก API, ปุ่มกดทำอะไร) — ไม่มี JSX |
| View | component ที่เอา ViewModel มา "วาด" เป็นหน้าจอ — มีแต่ JSX |
| Interceptor | โค้ดที่ดักทุก request/response ของ axios ไว้จัดการรวมศูนย์ (เช่น เจอ error 401 ให้เด้งไป login) |
| Envelope | รูปแบบตายตัวที่ backend ห่อข้อมูลมา: { data: ..., meta: ... } |
| namespace (i18n) | กลุ่มของข้อความแปล เช่น common, products — เวลาเรียกใช้ t("products.title") |
| Permission gate | การซ่อน/โชว์ปุ่มตามสิทธิ์ผู้ใช้ ผ่าน usePermission("orders") |
| proxy.ts | ไฟล์ที่รันก่อนทุกหน้า (เหมือน middleware) — ที่นี่ใช้เช็คว่า login หรือยัง |
src/
app/ ทุกหน้าเว็บ (Next.js App Router)
layout.tsx โครงนอกสุด — ครอบ i18n + antd + React Query ให้ทั้งแอป
page.tsx หน้าแรก "/" — เด้งไปหน้าร้าน /customer (proxy.ts ทำเป็น 307 ให้ก่อน)
login/ register/ login (อีเมล · Google · LINE /login/line) · สมัครสมาชิก — หลัง login ไปหน้าร้านหรือหลังร้านตาม role
profile/ ข้อมูลผู้ใช้หลังร้าน + เชื่อม LINE
customer/ หน้าร้าน (CustomerChrome — Navbar + Footer) — เขียนแบบหน้าเดียวไฟล์เดียว (ยกจาก FrontOffice)
layout.tsx guest เปิดดูได้ · หน้าที่ต้อง login ครอบด้วย CustomerAuthGate
lib/ hooks/ query key ร่วม (shopQueries/catalogQueries) · format · hook ของหน้าร้าน (ตะกร้า · รายการโปรด · แจ้งเตือน)
account/ บัญชีของฉัน (+ _components ที่ใช้ร่วมกัน เช่น SlipPaymentPanel · WriteReviewForm)
owner/ หน้าหลังบ้านทั้งหมด (ใช้ OwnerLayout — มี Sidebar + Navbar)
layout.tsx โหลดข้อมูลผู้ใช้, ประกอบ Sidebar/Navbar, จับ idle timeout
products/ 1 หน้า = page.tsx + useProductsViewModel.ts + ProductsView.tsx
_components/ component ที่ "หน้านี้เท่านั้น" ใช้
proxy.ts ด่านหน้า: "/" → /customer · /owner/* ยังไม่ login → เด้ง /login
lib/ เครื่องมือระดับแอป
http.ts axios instance + interceptor (จุดเดียวที่คุยกับ network)
authClient.ts login (อีเมล · Google · LINE) / logout / me / meOptional · ติดตั้ง handler 401
queryClient.ts ตั้งค่า React Query
alert.ts popup แจ้งเตือน (ครอบ sweetalert2)
exportCsv.ts ดาวน์โหลดตารางเป็น CSV
services/ 1 ไฟล์ต่อ 1 resource — ฟังก์ชันเรียก API
หลังร้าน /admin/* (productsService, ordersService ...) · หน้าร้าน catalog.ts (/catalog/* สาธารณะ)
+ shop*.ts (/shop/* ของลูกค้า: ตะกร้า · ออเดอร์ · พรีออเดอร์ · ชำระเงิน · แต้ม · รีวิว ...) · storeInfo.ts
types/ DTO — หน้าตา JSON จาก backend
constants/ menuKeys (คีย์เมนู/สิทธิ์) · enumConfig (สีของสถานะ/badge)
utils/ ฟังก์ชันช่วยล้วน ๆ (คำนวณส่วนลด, diff ก่อน/หลัง, แปลงหน่วย)
i18n/ ตั้งค่าภาษา + messages/{th,en}.json (ไฟล์คำแปล)
context/ PermissionsContext — เก็บสิทธิ์ผู้ใช้ให้ทั้งแอปเรียกใช้
hooks/ useCurrentUser · useIdleTimeout · hook ที่ใช้หลายที่
components/
base/ ปุ่ม / input / badge พื้นฐาน (ครอบ antd ให้หน้าตาเหมือนกันทั้งแอป)
shared/ component ที่ใช้ ≥ 2 หน้า — แบ่ง layout / data / feedback / stats / charts / form
customer/ component ของหน้าร้าน (Navbar · Footer · ProductCard · StoreLogo · กระดิ่งแจ้งเตือน ...) + shopStyles.ts
providers/ AuthBootstrap (401 → login)
docs/ เอกสารรายละเอียด (ดูข้อ 9)
scripts/check-i18n.mjs สคริปต์เช็คว่าไม่มีข้อความ hard-code
scripts/check-theme.mjs สคริปต์เทียบสี theme/palette.ts ↔ globals.css
ตัวอย่าง: ผู้ใช้เปิดหน้า /owner/products
1. proxy.ts มี auth cookie ไหม? ── ไม่มี → เด้ง /login จบ
└─ มี → ปล่อยผ่าน
2. app/owner/layout.tsx โหลด useCurrentUser() → GET /auth/me → ได้ user + สิทธิ์ (menuAccess)
ประกอบ Sidebar + Navbar รอบ ๆ เนื้อหา
3. app/owner/products/page.tsx เรียก useProductsViewModel() แล้วส่งผลให้ <ProductsView/>
4. useProductsViewModel.ts useQuery(['products', filters], () => productsService.list(filters))
5. src/services/products.ts http.getList('/admin/products', { params: filters })
6. src/lib/http.ts (interceptor) - แนบ header Accept-Language ตามภาษาปัจจุบัน
- ส่ง request ออกไปที่ NEXT_PUBLIC_API_BASE_URL
7. backend จริงตอบ (ไม่มี mock แล้ว — I11)
8. response กลับมา getList แกะ envelope { data: { items, meta } } เป็น { data, meta }
ถ้า error 401 → ล้าง cache + เด้ง /login ทันที (ไม่มี refresh/retry — backend ไม่มี refresh token)
error อื่น → โยน ApiError { status, code, message, fieldErrors } ให้ ViewModel
9. useQuery ได้ data → ViewModel ส่งต่อเป็น props → <ProductsView/> วาดตาราง (ข้อความทุกคำผ่าน t())
จุดสำคัญ: หน้า (page.tsx/View) ไม่เคย เรียก fetch/axios เอง — เรียกผ่าน ViewModel → service → http.ts เสมอ
| อยากทำ... | ไปที่ | ทำยังไง (ย่อ) |
|---|---|---|
| เพิ่มหน้าใหม่ | src/app/owner/<ชื่อ>/ | สร้าง 3 ไฟล์: page.tsx + use<ชื่อ>ViewModel.ts + <ชื่อ>View.tsx (ดู 7.3) |
| เพิ่ม/แก้ข้อความบนจอ | src/i18n/messages/th.json + en.json | เติม key เดียวกันทั้ง 2 ไฟล์ แล้วเรียก t("...") |
| เรียก API endpoint ใหม่ | src/services/<resource>.ts (+ src/types/) | เพิ่มฟังก์ชันที่เรียก http.get/post/... + เขียน DTO |
| สร้าง component ใช้ซ้ำ | src/components/base/ หรือ shared/<หมวด>/ | ดูกติกา "ใช้กี่หน้า?" ใน 7.1 |
| component เฉพาะหน้าเดียว | src/app/owner/<หน้า>/_components/ | สร้างไฟล์ในโฟลเดอร์นั้นได้เลย |
| ซ่อนปุ่มถ้าไม่มีสิทธิ์ | ใน ViewModel | const perm = usePermission("orders") → {perm.create && <ปุ่มเพิ่ม/>} |
| เปลี่ยนสีของ badge สถานะ | src/constants/enumConfig.ts | แก้ hex ในตารางสถานะ (label อยู่คนละที่ — i18n) |
| เปลี่ยนธีมสี/ฟอนต์ | src/app/globals.css + src/app/providers.tsx | ตัวแปรสีน้ำตาล (coffee) + antd token |
| แก้เมนู Sidebar | src/components/shared/layout/Sidebar* + src/constants/menuKeys.ts | เพิ่ม/ลบรายการ + คีย์สิทธิ์ |
component มีได้ 3 ที่เท่านั้น ตัดสินด้วยคำถามเดียว — "ใช้กี่หน้า?"
| ที่วาง | ใช้เมื่อ | ตัวอย่าง | เหตุผล |
|---|---|---|---|
src/components/base/ | UI พื้นฐาน ไม่มี logic ธุรกิจ | Button, Input, Badge, DatePicker, Logo | ครอบ antd ไว้ที่เดียว → เปลี่ยนธีม/ขนาดตัวอักษรทีเดียวได้ทั้งแอป |
src/components/shared/<หมวด>/ | ใช้ ≥ 2 หน้า | DataTable, Navbar, Sidebar, StatCard, LoadingSpin, DetailDrawer | ไม่เขียนซ้ำ · หมวด = layout data feedback stats charts form |
src/app/owner/<หน้า>/_components/ | ใช้ หน้าเดียว | KanbanBoard (มีแค่ Production), CartPanel (มีแค่ POS) | อยู่ใกล้ที่ใช้ → หาง่าย · _ ขึ้นต้น = Next.js ไม่ทำเป็น URL |
กติกาสำคัญ:
base/ เสมอshared/Badge → ไฟล์เดียวพอรายละเอียด:
docs/REBUILD_PLAN.md§6 · ตารางว่า component ไหนอยู่ไหน:docs/COMPONENT_MAP.md(สร้างตอนเฟส 3)
ใช้ next-intl แบบ ไม่มี /th /en ใน URL — ภาษาปัจจุบันเก็บใน cookie mmc_locale
ทำงานยังไง:
cookie mmc_locale ──► src/i18n/request.ts ──► โหลด src/i18n/messages/<ภาษา>.json ──► t("...") ใช้ได้ทุกที่
ใช้ในโค้ด — กฎเดียว: t ตัวเดียว, key = path เต็ม
// client component
import { useTranslations } from "next-intl";
const t = useTranslations(); // ไม่ใส่ namespace
return <h1>{t("products.title")}</h1>; // key ตรงกับ path ใน messages json เป๊ะ
// ค่าแทรก: t("products.stock", { n: 12 }) // json: "stock": "สต็อก {n}"
// server component
import { getTranslations } from "next-intl/server";
const t = await getTranslations();
เพิ่มข้อความใหม่ = 2 ขั้น: (1) เติม key เดียวกันทั้ง th.json + en.json → (2) เรียก t("path.to.key")
3 อย่างที่กันพลาด:
src/i18n/messages.d.ts)npm run lint:i18n เช็ค key ของ th/en ให้ตรงกัน.tsx → npm run lint:i18n จับ (ยกเว้น i18n/ types/ constants/ — หน้าร้านไม่ยกเว้นแล้ว)key จากตัวแปร/config: ต้อง type ให้แคบ (เช่น menu.ts labelKey: NavKey) หรือ cast t(key as Parameters<typeof t>[0])
สลับภาษา: component <LocaleSwitcher/> (ปุ่ม TH/EN) → เขียน cookie + router.refresh() · antd (ปฏิทิน, ปุ่ม "วันนี้") กับ dayjs สลับตามให้อัตโนมัติผ่าน src/app/providers.tsx
namespace มีอะไรบ้าง: common (ปุ่มทั่วไป) · nav (เมนู) · auth · errors · fields (ชื่อฟิลด์ ~130 คำ) · enums (ค่าสถานะ 20 กลุ่ม) · และ 1 อันต่อ 1 กลุ่มหน้า (products, orders, ingredients, ...)
ทำไมสำคัญ: ข้อความอยู่ที่เดียว → นักแปลแก้ได้โดยไม่แตะโค้ด · เพิ่มภาษาที่ 3 ในอนาคตแค่เพิ่มไฟล์ json
รายละเอียด:
docs/I18N_PLAN.md
แยก "สิ่งที่คิด" (ViewModel) ออกจาก "สิ่งที่แสดง" (View) — ใน React ทำผ่าน "custom hook + presentational component" (บางทีเรียก Container/Presentational)
1 หน้า = 3 ไฟล์:
app/owner/products/
page.tsx ← จุดเชื่อม บาง ๆ (5 บรรทัด): เรียก hook แล้วส่งให้ View
useProductsViewModel.ts ← state, เรียก API, handler, คำนวณ, เช็คสิทธิ์ — ไม่มี JSX
ProductsView.tsx ← JSX ล้วน รับ props จาก ViewModel — ไม่มี fetch / useEffect
// page.tsx — เท่านี้จริง ๆ
"use client";
export default function ProductsPage() {
const vm = useProductsViewModel();
return <ProductsView {...vm} />;
}
อยู่ใน ViewModel (use...ts) | อยู่ใน View (...View.tsx) |
|---|---|
useState, useQuery / useMutation (React Query) | JSX + ประกอบ component จาก base/ shared/ |
เรียก services/* (API) | t() ทุกข้อความ |
handler: handleSave, handleDelete | render ตาม props: loading ? <Spin/> : <Table/> |
useMemo คำนวณ (filter, ยอดรวม) | ปุ่มโชว์/ซ่อนตาม perm.create ที่รับมาเป็น prop |
usePermission("products") | ห้าม fetch / เรียก API / business logic |
ทำไมสำคัญ:
use..., หน้าตาอยู่ไฟล์ ...View"รายละเอียด + naming convention:
docs/CODE_STRUCTURE.md
Backend เป็นเจ้าของทุกอย่างที่สำคัญ: ตรวจรหัสผ่าน, ออก/หมุน token, อายุ session, ล็อกตอนกรอกผิดหลายครั้ง, บังคับสิทธิ์จริง
Frontend ทำแค่ 6 อย่าง:
| ส่วน | ไฟล์ | หน้าที่ |
|---|---|---|
| เรียก API auth | lib/authClient.ts | login / loginWithGoogle / lineLoginUrl / logout / me / meOptional (หน้าร้าน — guest ได้ null ไม่เด้ง login) |
| ดักทุก response | lib/http.ts (interceptor) | เจอ 401 → เด้ง /login?reason=expired ทันที — ไม่มี refresh/retry (backend ใช้ JWT อายุ 7 วัน ไม่มี refresh token) · ยกเว้น /auth/login ฯลฯ ที่ 401 = คำตอบของมันเอง (รหัสผิด) และ request ที่ส่ง skipAuthRedirect |
| ข้อมูลผู้ใช้ปัจจุบัน | hooks/useCurrentUser.ts | GET /auth/me → ได้ user + menuAccess (สิทธิ์แต่ละเมนู) |
| เก็บสิทธิ์ให้ทั้งแอป | context/PermissionsContext | usePermission("orders") → { view, create, update, delete, approve } |
| หมดเวลา idle | hooks/useIdleTimeout.ts | ไม่ขยับ 30 นาที → เตือน 60 วิ → logout |
| ด่านหน้า | proxy.ts | ไม่มี auth cookie → เด้ง /login (แค่เช็คว่า มี cookie — ไม่ verify signature) |
Flow ล็อกอิน:
กรอก email/password → POST /auth/login (backend set cookie)
→ GET /auth/me → เก็บ user + สิทธิ์ ใน PermissionsContext
→ ไปต่อตาม role (login/nextPath.ts):
ลูกค้า → ?next= ใต้ /customer หรือ /profile · ไม่งั้น /customer
เจ้าของร้าน/พนักงาน → ?next= ใต้ /owner หรือ /profile · ไม่งั้น /owner/dashboard
Flow ลืมรหัสผ่าน (รายละเอียด §12):
หน้า /login กด "ลืมรหัสผ่าน?" → หน้าต่างเลือกบัญชี Google (popup) → POST /auth/forgot-password { email }
→ ป๊อปอัป "ส่งลิงก์เรียบร้อยแล้ว" (อยู่หน้า login ต่อ)
ลิงก์ในอีเมล → /customer/reset-password?token= → ตั้งรหัสใหม่ → กลับ /login + ป๊อปอัป "เปลี่ยนรหัสผ่านสำเร็จ"
สิทธิ์ = แค่ UX: frontend ซ่อนปุ่ม/เมนูตาม menuAccess เพื่อความสวยงาม — ตัวบังคับจริงอยู่ที่ backend (ต่อให้ผู้ใช้เรียก API ตรง ๆ ก็โดน backend ปฏิเสธ)
ทำไมสำคัญ: ถ้าเข้าใจผิดว่า "ซ่อนปุ่ม = ปลอดภัย" จะเกิดช่องโหว่ · จำไว้ว่า frontend เชื่อถือไม่ได้เสมอ
รายละเอียด (interceptor, cross-tab logout · แผน refresh เดิมที่ไม่ได้ใช้):
docs/AUTH_PLAN.md
ทุก request ยิงไปที่ NEXT_PUBLIC_API_BASE_URL ผ่าน services/* — หน้าไม่เรียก fetch/axios ตรง
หน้าตา response (ตกลงกับ backend ไว้แล้ว):
// list: GET /products?page=1&limit=50
{ "data": [ ...สินค้า... ], "meta": { "page": 1, "limit": 50, "total": 120 } }
// item: GET /products/:id · POST /products · PATCH /products/:id
{ "data": { ...สินค้า 1 ชิ้น... } }
// error (ทุก status ≥ 400)
{ "message": "ข้อความบอกผู้ใช้" }
query params มาตรฐาน: ?page= ?limit= ?search= ?sort=-created_at + filter ตรงตัว เช่น ?order_status=pending
ตัวอย่าง service:
// src/services/products.ts
import { http } from "@/lib/http";
export const productsService = {
list: (params) => http.get("/products", { params }), // → { data, meta }
get: (id) => http.get(`/products/${id}`),
create: (body) => http.post("/products", body),
update: (id, body) => http.patch(`/products/${id}`, body),
remove: (id) => http.delete(`/products/${id}`),
};
ทดสอบ: ไม่มี mock แล้ว — ทดสอบด้วย backend จริงในเครื่อง + MongoDB local meowmeecake-test (ดู docs/MOCKS.md)
ทำไมสำคัญ: contract คือ "สัญญา" ระหว่าง 2 ทีม — frontend เขียนตามนี้, backend ทำให้ตรงนี้ ทั้งหมดอ้างเอกสารเดียว
รายการ endpoint ทั้งหมด (40 resource) + สิ่งที่ frontend ไม่ทำ:
docs/API_CONTRACT.md
จากระบบเดิม (fullstack) — โปรเจกต์นี้เอามาเป็น สเปคของ DTO + หน้าจอ ไม่ใช่โค้ด backend
| หมวด | จำนวน | มีอะไรบ้าง |
|---|---|---|
| Resource / entity | ~38 | สินค้า · ออเดอร์ · พรีออเดอร์ · วัตถุดิบ · หน่วยนับ · สูตร · การผลิต · พนักงาน · role · สิทธิ์ · โปรโมชัน · แบนเนอร์ · ค่าใช้จ่าย · แจ้งเตือน · รีวิว ... |
| หน้าจอ | 27 | Login + 26 หน้าหลังบ้าน (Dashboard, Products, Orders/POS, Ingredients, Production, Recipes, Employees, Finance, Store Design, Notifications, Access Denied) |
| ภาษา | 2 | ไทย (ค่าเริ่มต้น) / อังกฤษ |
| กลุ่มเมนู (สิทธิ์) | 11 | dashboard products orders payments ingredients stock recipes production employees promotions reports × 5 action (view/create/update/delete/approve) |
ทำไมสำคัญ: ก่อนสร้างหน้าใหม่หรือ DTO ใหม่ เปิด INVENTORY.md ดูก่อนว่ามี entity/field นั้นอยู่แล้วไหม ชื่ออะไร enum มีค่าอะไรบ้าง
แจกแจงเต็มทุก entity + field + enum:
docs/INVENTORY.md
| เครื่องมือ | ใช้ทำอะไร | ทำไมเลือกตัวนี้ |
|---|---|---|
| Next.js 16 (App Router) | framework หลัก — routing, SSR, build | มาตรฐาน React · โฟลเดอร์ = URL · มี proxy.ts ทำด่านหน้าได้ |
| React 18 + TypeScript | UI + type safety | TS จับ bug ตั้งแต่เขียน (DTO ผิด shape จะ error ทันที) |
| antd 6 | component สำเร็จรูป (Table, Form, DatePicker, Modal) | ครบ, เสถียร, มี locale ไทย/อังกฤษในตัว |
| Tailwind 4 | จัด layout/spacing/สี ด้วย class | เร็ว, ไม่ต้องตั้งชื่อ CSS class · ใช้คู่ antd (antd = component, Tailwind = จัดวาง) |
| next-intl 4 | ระบบ 2 ภาษา | ทำงานได้ทั้ง server + client component · เก็บภาษาใน cookie ไม่ต้องมี /th /en |
| axios | เรียก HTTP | มี interceptor รวมศูนย์ (จัดการ 401/error ที่เดียว) |
| @tanstack/react-query | จัดการข้อมูลจาก API (loading/error/cache/refetch) | ไม่ต้องเขียน useState+useEffect ซ้ำทุกหน้า · refetch อัตโนมัติเมื่อกลับมาโฟกัสแท็บ |
| sweetalert2 | popup แจ้งเตือน/ยืนยัน | หน้าตาสวย, เรียกผ่าน lib/alert.ts ที่เดียว |
| dayjs | จัดการวันที่ | เบา, antd ใช้ตัวนี้อยู่แล้ว, สลับ locale ได้ |
| recharts | กราฟ (ยอดขาย, การผลิต) | API เป็น React component ตรงไปตรงมา |
React Compiler (reactCompiler: true) | ทำ memoization ให้อัตโนมัติ | ไม่ต้องใส่ useMemo/useCallback เองทุกที่ (build ช้าลงนิดหน่อย แลกมา) · ใช้คู่ react-compiler-runtime เพราะยังเป็น React 18 (Next 16.3+ บังคับ) |
docs/อ่านเป็น ด่าน — จบด่านหนึ่งค่อยไปด่านถัดไป ไม่ต้องอ่านทุกไฟล์รวดเดียว
| ด่าน | อ่านอะไร | เวลา | จบแล้วทำอะไรได้ |
|---|---|---|---|
| 0 · ปฐมนิเทศ | README นี้ทั้งไฟล์ → REBUILD_PLAN.md §1–3 (เป้าหมาย + สถานะ + สถาปัตยกรรม) | ~30 นาที | รู้ว่าแอปทำอะไร ใครใช้ โครงใหญ่ ตอนนี้ทำถึงเฟสไหน |
| 1 · วิธีเขียนหน้า | CODE_STRUCTURE.md ทั้งไฟล์ → ทวน README §5 (เส้นทาง request) + §7.3 (MVVM) | ~45 นาที | สร้างหน้าใหม่ที่ดึงข้อมูลมาแสดง + มีปุ่ม/ฟอร์มได้ |
| 2 · ต่อกับ backend | API_CONTRACT.md §0–3 → MOCKS.md (วิธีทดสอบกับ backend ในเครื่อง) | ~30 นาที | เขียน service ใหม่ + ทดสอบกับ backend จริงได้ |
| 3 · เฉพาะเรื่อง (อ่านตอนถึงงานนั้น) | ดูตารางด้านล่าง | — | — |
ด่าน 3 — อ่านเมื่อถึงงาน:
| จะทำงานเรื่อง... | เปิดอ่าน |
|---|---|
| สร้าง/จัดระเบียบ component ที่ใช้หลายหน้า | REBUILD_PLAN.md §6 + COMPONENT_MAP.md |
| แตะข้อความบนจอ / เพิ่มภาษา / enum แปลไม่ครบ | I18N_PLAN.md |
| แตะ login / session / สิทธิ์ | AUTH_PLAN.md |
| หาว่ามี entity/field/enum อะไรบ้าง ชื่ออะไร | INVENTORY.md (เปิดหาเป็นจุด ๆ ไม่ต้องอ่านรวด) |
| อยากรู้ว่าทำไมโปรเจกต์ตัดสินใจแบบนี้ | PROMPT_HISTORY.md + REBUILD_PLAN.md §8 |
| ไฟล์ | คืออะไร | เปิดอ่านเมื่อ | ทำไมสำคัญ |
|---|---|---|---|
REBUILD_PLAN.md | แผนแม่บท — เป้าหมาย, สถาปัตยกรรม frontend, 8 เฟส, การตัดสินใจ D1–D19 พร้อมเหตุผล | วันแรก / อยากรู้ทำถึงไหน / จะเริ่มเฟสใหม่ | เห็นภาพรวมทั้งโปรเจกต์ในไฟล์เดียว — ทุก decision มี "ทำไม" กำกับ |
CODE_STRUCTURE.md | กติกา MVVM — View/ViewModel วางยังไง ตั้งชื่ออะไร แตกไฟล์เมื่อไหร่ + ตัวอย่างเต็ม | ก่อนสร้างหน้า/component ที่มี logic | ทุกหน้าโครงโค้ดเหมือนกัน → เปิดหน้าไหนก็อ่านออกทันที |
API_CONTRACT.md | สัญญา REST กับ backend — envelope, params, status, auth, 40 resource, ตัวอย่าง request/response | ก่อนเขียน service / DTO ใหม่ | frontend + backend อ้างเอกสารเดียว → ไม่หลุดกัน |
MOCKS.md | ถอด mock แล้ว — วิธีทดสอบกับ backend ในเครื่อง + DB ทดสอบ | ก่อนทดสอบงานที่เรียก API | กันทดสอบกับ DB จริง (Atlas) โดยไม่ตั้งใจ |
I18N_PLAN.md | ระบบ 2 ภาษา — โครง namespace, การจัดการ enum ที่ DB เก็บเป็นภาษาไทย | เพิ่มภาษา / ข้อความแปลไม่ครบ / งง key | เข้าใจว่าทำไมข้อความอยู่ใน json ไม่อยู่ในโค้ด |
AUTH_PLAN.md | auth ฝั่ง frontend — interceptor 401→login, idle timeout, cross-tab logout + ตารางว่าอะไรเป็นหน้าที่ frontend/backend | แตะโค้ด login / session / permission | auth ผิด = ช่องโหว่ · ไฟล์นี้กันเข้าใจผิดว่า "ซ่อนปุ่ม = ปลอดภัย" |
INVENTORY.md | แจกแจงทุก entity / field / enum / util จากระบบเดิม (fullstack) | ก่อนสร้าง DTO / หน้าใหม่ / หา enum | กันสร้างของซ้ำ + ตั้งชื่อฟิลด์ให้ตรงกับ backend |
COMPONENT_MAP.md | ตาราง: component ชื่อนี้ → อยู่ base/shared/หน้าไหน → ใช้กี่หน้า → split ไหม | หา component / ตัดสินใจสร้างใหม่หรือใช้ของเดิม | (สร้างตอนเฟส 3) กัน component ซ้ำซ้อน |
PROMPT_HISTORY.md | ไทม์ไลน์การพัฒนา — แต่ละครั้งสั่งอะไร ทำอะไรไป ติดปัญหาอะไร | อยากรู้ที่มาของโค้ด / เฟสไหนเสร็จแล้ว | context การตัดสินใจที่ไม่ได้อยู่ในโค้ดหรือ git log |
docs/ ทำตามนี้)cookie → request.ts → messages.json → t())ทำ ✅
page.tsx (บาง) + use<X>ViewModel.ts + <X>View.tsxt("...") — เติมทั้ง th.json + en.jsonservices/* → http.ts เท่านั้นcomponents/base/ ไม่เรียก antd ตรงusePermission(...) ใน ViewModelCODE_STRUCTURE.mdอย่า ❌
.tsx (npm run lint:i18n จะจับ)fetch/axios ใน page.tsx หรือ *View.tsxuseState/useEffect โหลดข้อมูลใน *View.tsx (ย้ายไป ViewModel)ก่อน commit: npm run check ต้องผ่าน (+ npm run build ถ้าแตะ route/config)
| คำสั่ง | ทำอะไร |
|---|---|
npm run dev | รัน dev server (hot reload) ที่ :3001 (backend ใช้ :3000) |
npm run build | build production + เช็ค TypeScript ทั้งโปรเจกต์ |
npm run start | รัน build ที่ทำไว้ |
npm run lint | ESLint |
npm run lint:i18n | เช็ค key th/en ตรงกัน + ไม่มีข้อความ hard-code นอกไฟล์คำแปล |
npm run lint:theme | เทียบสีใน theme/palette.ts ↔ globals.css |
npm run check | lint:i18n + lint:theme + tsc --noEmit + eslint รวดเดียว — ต้องเขียวก่อน commit |
ทุกข้อผ่าน tsc --noEmit · eslint · lint:i18n · lint:ui (ไฟล์ที่แก้) แล้ว · ยังไม่ได้ทดสอบในเบราว์เซอร์ด้วยบัญชีลูกค้าจริง
(ข้อ 1 ต้องเลือกบัญชี Google + ส่งอีเมลจริง · ข้อ 3–5 ต้องล็อกอินเป็นลูกค้า) · ยังไม่ได้ commit
| ขั้น | เดิม | ตอนนี้ |
|---|---|---|
กด "ลืมรหัสผ่าน?" ในหน้า /login | popup เลือกบัญชี → กล่องยืนยันอีกชั้น · มีลิงก์ "ใช้อีเมลอื่น" | เปิดหน้าต่างเลือกบัญชี Google ทันที (ไม่มีกล่องยืนยัน · เอาลิงก์ "ใช้อีเมลอื่น" ออก) |
| เลือกบัญชีแล้ว | ส่งลิงก์ + ป๊อปอัป | ส่งลิงก์ไปที่อีเมลนั้น + ป๊อปอัป "ส่งลิงก์เรียบร้อยแล้ว" (อีเมล · ลิงก์ใช้ได้ 1 ชม. · เช็คสแปม) — อยู่หน้า login ต่อ |
ตั้งรหัสใหม่ (/customer/reset-password) | สำเร็จ → การ์ด "สำเร็จ" + ปุ่มไปหน้า login | บันทึกสำเร็จ → ไปหน้า /login ทันที + ป๊อปอัป "เปลี่ยนรหัสผ่านสำเร็จ" |
| ผิดพลาด | — | ปิดหน้าต่าง Google = ไม่ทำอะไร · เลือกบัญชีไม่ได้ / backend ปฏิเสธ (บัญชี Google/LINE ไม่มีรหัสผ่าน · ขอถี่เกิน) = ป๊อปอัปข้อผิดพลาด |
app/login/_components/LoginForm.tsx · app/login/_components/forgotPasswordFlow.ts (ใหม่ — เลือกบัญชี + ส่งลิงก์ ใช้ร่วมกับหน้า forgot-password)
· app/customer/reset-password/page.tsx · app/customer/forgot-password/page.tsx/customer/forgot-password (ฟอร์มกรอกอีเมล + ปุ่ม "เลือกบัญชี Google แทน" → หน้า "ส่งลิงก์แล้ว" + ปุ่มเปิด Gmail) ยังอยู่ —
ใช้เมื่อ: ลิงก์ "ลืมรหัสผ่านปัจจุบัน?" ในหน้าเปลี่ยนรหัสผ่าน · ปุ่ม "ขอลิงก์ใหม่" ตอนลิงก์หมดอายุ · ไม่ได้ตั้ง NEXT_PUBLIC_GOOGLE_CLIENT_IDNEXT_PUBLIC_GOOGLE_CLIENT_ID · origin ใน Google Cloud Console · STOREFRONT_URL ของ backend (§2)/customer/account/member)/customer/account/reviews) — เพิ่มตัวกรองshop.products.* ร่วมกับหน้าสินค้า/customer/checkout + /customer/preorder/checkout)components/customer/PickupDateCalendar.tsx (ใหม่ — ย้าย pickupDateLabel มาไว้ที่นี่ · PickupLocationPicker re-export ให้)
· components/customer/PickupLocationPicker.tsx (เอาแบบปุ่มวันเดิมออก)| การกระทำ | เห็นผลจาก | ไฟล์ |
|---|---|---|
| เพิ่มสินค้าลงตะกร้า | ปุ่มขึ้นสถานะสำเร็จ + จำนวนบนไอคอนตะกร้า | app/customer/hooks/useAddToCart.ts |
| ลบสินค้าออกจากตะกร้า | รายการหายจากตะกร้า | app/customer/cart/page.tsx |
| เพิ่ม/นำออกจากรายการโปรด | ไอคอนหัวใจเปลี่ยน | app/customer/hooks/useFavorites.ts |
th.json / en.json — key ตรงกันทั้งสองไฟล์)auth.forgotSent · shop.password.{pickGoogleFailed, useGoogle, openGmail} · shop.pickup.{prevMonth, nextMonth, selectedDate, pickDateHint}
· shop.myReviews.{countFiltered, searchLabel, searchPlaceholder, sortLabel, sortNewest, sortOldest, sortHighest, sortLowest, filterMedia, filterPreorder, filterStarsLabel, allStars, starsChip, emptyFiltered, clearFilters}auth.{forgotConfirmTitle, forgotConfirm, forgotSend, forgotFailed, forgotPickFailed, forgotOtherEmail} · shop.password.goLogin
· shop.member.{readyToRedeem, collectMore} · shop.myReviews.emptyReplied · shop.cart.removed · shop.cartActions.{added, favoriteAdded, favoriteRemoved}http://localhost:3001 ใน Authorized JavaScript origins ของ Google Cloud Console แล้วลอง flow ลืมรหัสผ่านจนจบด้วยบัญชีที่สมัครด้วยอีเมล+รหัสผ่านSTOREFRONT_URLnpm run lint:ui ยังไม่ผ่านจาก text-gray-400 ในไฟล์หลังร้าน 3 ไฟล์เดิม (owner/account/_components/ProfileTab.tsx · owner/employees/userLog/_components/LogDetailContent.tsx
· owner/production/_components/CompleteProductionModal.tsx) — ไม่เกี่ยวกับงานรอบนี้