Fixes #1 ### Summary of Changes - Modernized architecture documents to establish DBK Platform as the canonical multi-tenant commerce platform identity, treating HomAroma as the reference tenant. - Updated frontend comparison to accurately reflect apps/spa (React 19 + Vite) vs apps/website (Next.js 15 SSR) instead of legacy names. - Cleaned typos in PRISMA7_GUIDE and added cross-references to docs/PLATFORM_OVERVIEW.md. - Updated docs/DOCUMENTATION_MIGRATION.md completion tracking. --------- Co-authored-by: Your Name <you@example.com> Reviewed-on: #5 Co-authored-by: antigravity-bot <bot@kittisak.com>
228 lines
9.6 KiB
Markdown
228 lines
9.6 KiB
Markdown
# Architecture Decisions — DBK Platform
|
|
|
|
> **Platform scope:** DBK Platform architecture decisions. HomAroma references in this ADR are reference-tenant context unless a decision explicitly names an operational resource. See [`docs/PLATFORM_OVERVIEW.md`](docs/PLATFORM_OVERVIEW.md).
|
|
|
|
> บันทึกการตัดสินใจเชิงสถาปัตยกรรม (ADR) จากการประชุมทีม
|
|
> อัปเดตล่าสุด: 2026-07-12
|
|
|
|
---
|
|
|
|
## 1. Page Builder CMS — Section Types ที่มีอยู่
|
|
|
|
ระบบมี Block/Section ทั้งหมด **19 แบบ** กำหนดใน `packages/types/index.ts` → `PAGE_SECTION_TYPES`
|
|
|
|
### กลุ่ม Headers & Banners
|
|
| Type | คำอธิบาย |
|
|
|---|---|
|
|
| `HERO` | Hero หลัก (image, text, 1 button) |
|
|
| `HERO_V2` | Hero อัปเกรด (2 buttons, alignment, ornament) |
|
|
| `BANNER` | แบนเนอร์รูปภาพ + link |
|
|
| `ANNOUNCEMENT_BAR` | แถบประกาศด้านบน (auto-slide, ปรับสีได้) |
|
|
|
|
### กลุ่ม Products & Categories
|
|
| Type | คำอธิบาย |
|
|
|---|---|
|
|
| `PRODUCTS` | แสดงสินค้า (by brand/category/custom, grid/carousel) |
|
|
| `PRODUCT_CAROUSEL` | สไลด์สินค้าแนวนอน |
|
|
| `CATEGORIES` | แสดงหมวดหมู่สินค้า |
|
|
|
|
### กลุ่ม Content & Editorials
|
|
| Type | คำอธิบาย |
|
|
|---|---|
|
|
| `TEXT` | ข้อความทั่วไป (จัดตำแหน่งได้) |
|
|
| `RICH_TEXT` | เนื้อหาจัดรูปแบบได้ |
|
|
| `EDITORIAL_TILE` | รูปภาพ + เนื้อหา (สลับซ้าย-ขวาได้) |
|
|
| `EDITORIAL_CAROUSEL` | สไลด์บทความ (standard / split screen) |
|
|
| `HERO_ARTICLE` | หัวบทความ (ผู้เขียน, วันที่, หมวดหมู่) |
|
|
| `QUOTE_CALLOUT` | โควทคำพูด / คำโปรย |
|
|
|
|
### กลุ่ม Brand Features
|
|
| Type | คำอธิบาย |
|
|
|---|---|
|
|
| `SCENT_NOTES_BREAKDOWN` | แสดง Top/Heart/Base Notes ของกลิ่น |
|
|
| `USP_BAR` | จุดเด่นของร้าน (max 4 items) |
|
|
| `ACCORDION` | Q&A / FAQ แบบย่อ-ขยาย |
|
|
|
|
### กลุ่ม Forms & Contacts
|
|
| Type | คำอธิบาย |
|
|
|---|---|
|
|
| `NEWSLETTER` | ฟอร์มรับอีเมล + privacy checkbox |
|
|
| `CONTACT_INFO` | ที่อยู่, อีเมล, LINE, เวลาทำการ |
|
|
| `CONTACT_FORM` | ฟอร์มส่งข้อความ |
|
|
|
|
---
|
|
|
|
## 2. Multi-Client White-Label Strategy
|
|
|
|
### หลักการสำคัญ
|
|
|
|
> **CMS เก็บข้อมูล (Data + Semantics) — Renderer คือสิ่งที่เปลี่ยนตาม Client**
|
|
|
|
ความแตกต่างระหว่าง Client มี 2 ระดับที่แก้ต่างกัน:
|
|
|
|
| ระดับ | ตัวอย่าง | แก้ด้วย |
|
|
|---|---|---|
|
|
| **Visual Style** | สี, font, spacing ต่างกัน | Design Tokens / CSS Variables |
|
|
| **Layout Structure** | Card ตั้ง vs Card นอน (HTML ต่างกัน) | Component Variants ใน Renderer |
|
|
|
|
> Theme Token ช่วยไม่ได้กับ Layout Structure เพราะ HTML shape ต่างกันทั้งหมด
|
|
|
|
### Git Repository Strategy
|
|
|
|
```
|
|
[product-template] ← Source of Truth เสมอ
|
|
│
|
|
├── fork → [homaroma-client]
|
|
├── fork → [client-a-repo]
|
|
│ └── good feature → cherry-pick กลับ product-template
|
|
└── fork → [client-b-repo] ← clone จาก product-template (ที่ updated แล้ว)
|
|
```
|
|
|
|
**กฎ:** Client B ไม่ได้ start จาก Client A แต่ start จาก **product-template ที่รับ feature ดีๆ กลับมาแล้ว**
|
|
|
|
### Commit Discipline (สำคัญมาก)
|
|
|
|
```bash
|
|
# ✅ แยก commit ให้ชัดเสมอ
|
|
git commit -m "feat(categories): add horizontal-card layout variant" # generic → กลับ master ได้
|
|
git commit -m "feat(client-a): set default theme to warm-beige" # client-specific
|
|
git commit -m "fix(checkout): correct discount calculation" # generic → กลับ master ได้
|
|
|
|
# ❌ อย่าทำแบบนี้
|
|
git commit -m "client A changes" # cherry-pick ยากมาก
|
|
```
|
|
|
|
### Decision: Feature กลับ master ได้หรือไม่?
|
|
|
|
| ลักษณะ Feature | กลับ master? |
|
|
|---|---|
|
|
| Generic component / layout variant ใหม่ | ✅ ควร |
|
|
| Bug fix | ✅ เสมอ |
|
|
| Infrastructure / DevOps | ✅ เสมอ |
|
|
| Branding เฉพาะ client (สี, logo) | ❌ |
|
|
| Business logic เฉพาะ client | ❌ |
|
|
| Config ที่ structure generic แต่ค่าต่างกัน | ✅ structure กลับ, ค่าอยู่ที่ client |
|
|
|
|
---
|
|
|
|
## 3. componentRegistry Pattern
|
|
|
|
### เป้าหมาย
|
|
ให้ **Backend + Schema กลางไม่ต้องเปลี่ยน** เมื่อ Client ต้องการ Layout ที่ต่างออกไป
|
|
|
|
### โครงสร้างที่ควรมี
|
|
|
|
```
|
|
apps/spa/src/
|
|
config/
|
|
componentRegistry.ts ← Client-specific (ไม่ขึ้น master)
|
|
themeConfig.ts ← Client-specific
|
|
components/
|
|
dynamic/
|
|
categories/
|
|
VerticalCard.tsx ← Generic variant (ขึ้น master)
|
|
HorizontalCard.tsx ← Generic variant (ขึ้น master)
|
|
```
|
|
|
|
### การทำงาน
|
|
|
|
```ts
|
|
// apps/spa/src/config/componentRegistry.ts
|
|
export const componentRegistry = {
|
|
CATEGORIES: DynamicCategoriesVertical, // HomAroma เลือก vertical
|
|
HERO: DynamicHeroV2,
|
|
}
|
|
|
|
// DynamicPageRenderer.tsx — generic, ไม่มี hardcode switch-case
|
|
sections.map(section => {
|
|
const Component = componentRegistry[section.type]
|
|
return <Component key={section.id} payload={section.payload} />
|
|
})
|
|
```
|
|
|
|
**ผลลัพธ์:** เพิ่ม Client ใหม่ = สร้าง `componentRegistry.ts` ใหม่ + component ที่ต้องการ ไม่แตะ Backend เลย
|
|
|
|
---
|
|
|
|
## 4. CUSTOM Block — Developer Escape Hatch
|
|
|
|
### เหตุผลที่ต้องมี
|
|
|
|
ทุกครั้งที่ Dev เพิ่ม Section ใหม่แบบเดิม ต้องแก้ **4 จุดพร้อมกัน:**
|
|
|
|
1. `packages/types/index.ts` → Schema + Type
|
|
2. `apps/backend/routes/pages.ts` → validation
|
|
3. `apps/spa/DynamicPageRenderer.tsx` → render
|
|
4. `apps/admin/PageBuilderPage.tsx` → form
|
|
|
|
ถ้าเป็น client-specific section → ต้องแก้ Schema กลาง (ผิดหลักการ)
|
|
|
|
### แนวทาง: CUSTOM Block Type
|
|
|
|
```json
|
|
{
|
|
"type": "CUSTOM",
|
|
"payload": {
|
|
"componentId": "BakeryCategories",
|
|
"props": {
|
|
"title": "หมวดหมู่ของเรา",
|
|
"layout": "horizontal"
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Dev Workflow ใหม่ (2 ขั้นตอน ทำแค่ Frontend)
|
|
|
|
```ts
|
|
// Step 1: สร้าง Component ใดๆ ก็ได้
|
|
// apps/spa/src/components/custom/BakeryCategories.tsx
|
|
export default function BakeryCategories({ title, layout }) {
|
|
return <div>...JSX ได้เลย ไม่มีข้อจำกัด...</div>
|
|
}
|
|
|
|
// Step 2: Register ใน customRegistry
|
|
// apps/spa/src/config/componentRegistry.ts
|
|
export const customRegistry = {
|
|
'BakeryCategories': BakeryCategories,
|
|
}
|
|
```
|
|
|
|
**Backend ไม่ต้องแก้เลย**
|
|
|
|
### กฎการตัดสินใจ
|
|
|
|
```
|
|
ถาม: "Section นี้จะถูกใช้ใน project อื่นด้วยไหม?"
|
|
|
|
ใช่ → สร้าง Block Type ใหม่ตาม flow เดิม (เพิ่มเข้า master)
|
|
ไม่ → ใช้ CUSTOM block + register ใน client customRegistry เท่านั้น
|
|
```
|
|
|
|
---
|
|
|
|
## 5. Backlog — สิ่งที่ต้อง Implement
|
|
|
|
### ต้องทำ (ครั้งเดียว — foundation)
|
|
- [ ] เพิ่ม `'CUSTOM'` เข้า `PAGE_SECTION_TYPES` ใน `packages/types/index.ts`
|
|
- [ ] สร้าง `apps/spa/src/config/componentRegistry.ts` แยกออกมาจาก DynamicPageRenderer
|
|
- [ ] เพิ่ม `case 'CUSTOM'` ใน `apps/spa/src/components/DynamicPageRenderer.tsx`
|
|
- [ ] (Optional) เพิ่มใน `apps/website` ด้วย
|
|
|
|
### ควรทำ (เพิ่ม variants)
|
|
- [ ] สร้าง `HorizontalCard.tsx` variant ใน `components/dynamic/categories/`
|
|
- [ ] สร้าง `VerticalCard.tsx` (refactor จากของเดิม)
|
|
|
|
### Long-term
|
|
- [ ] `packages/ui/` — generic component library สำหรับ white-label
|
|
|
|
---
|
|
|
|
## 6. Anti-patterns — สิ่งที่ไม่ควรทำ
|
|
|
|
- ❌ **อย่าเพิ่ม UI layout ลงใน Schema กลาง** เช่น `layout: 'vertical' | 'horizontal'` ใน `CategoriesSectionPayloadSchema` — Schema จะพองและผูก UI กับ Data
|
|
- ❌ **อย่าให้ Admin ใส่ Raw HTML ตรงๆ** — XSS risk และ bypass design system
|
|
- ❌ **อย่า start Client B จาก Client A repo** — ใช้ product-template เสมอ
|
|
- ❌ **อย่า commit รวมกัน** เช่น "client A changes" — cherry-pick ยากมาก
|
|
- ❌ **อย่า hardcode switch-case ใน DynamicPageRenderer** — ใช้ componentRegistry แทน
|