Files
homaroma/ARCHITECTURE_DECISIONS.md
T
2026-10-01 05:49:58 +07:00

9.5 KiB

Architecture Decisions — DBK Platform

Platform scope: HomAroma references in this ADR are reference-tenant context unless a decision explicitly names an operational resource.

บันทึกการตัดสินใจเชิงสถาปัตยกรรม (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 จุดพร้อมกัน:

  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

{
  "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.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 แทน