9.6 KiB
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.
บันทึกการตัดสินใจเชิงสถาปัตยกรรม (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 (สำคัญมาก)
# ✅ แยก 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)
การทำงาน
// 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 จุดพร้อมกัน:
packages/types/index.ts→ Schema + Typeapps/backend/routes/pages.ts→ validationapps/spa/DynamicPageRenderer.tsx→ renderapps/admin/PageBuilderPage.tsx→ form
ถ้าเป็น client-specific section → ต้องแก้ Schema กลาง (ผิดหลักการ)
แนวทาง: CUSTOM Block Type
{
"type": "CUSTOM",
"payload": {
"componentId": "BakeryCategories",
"props": {
"title": "หมวดหมู่ของเรา",
"layout": "horizontal"
}
}
}
Dev Workflow ใหม่ (2 ขั้นตอน ทำแค่ Frontend)
// 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.tsxvariant ใน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 แทน