System Architecture Draft · v1

ระบบถอดแบบ & Auto Cut กระจก–อลูมิเนียม

ออกแบบให้ บริษัท อะลูอาท จำกัด ใช้งานก่อน แต่วางโครงสร้าง Multi-tenant ตั้งแต่วันแรก เพื่อต่อยอดเป็น Product SaaS ขายบริษัทกระจกทั่วประเทศ — รันบน INET Cloud, ดูแล (PostgreSQL)

🏢 Tenant 1: Aluart ☁️ INET Cloud (TH) 🐘 PostgreSQL + RLS 🤖 AI Takeoff + Auto Cut 🔌 API จัดซื้อภายนอก

1 Workflow ของระบบ

ไล่ตาม Requirement ที่คุยกับลูกค้าครบทั้ง 9 ขั้น — ตั้งแต่โยนไฟล์เข้าระบบ จนออกรายงานและยิง API ไปจัดซื้อ

flowchart TD
    A["👤 เจ้าหน้าที่ อัปโหลดไฟล์
CAD / PDF / Photo"]:::user --> B{"เลือก Project / Tenant
ตรวจไฟล์"} B --> C["🤖 AI ถอดแบบ (Takeoff)
skill แกะแบบ"]:::ai C --> D["ได้ จำนวนกระจก + ไซซ์
(กว้าง × สูง, จำนวนบาน)"] D --> E["📋 ระบบสร้างรายการอัตโนมัติ
Header + Table กระจก"] E --> F["👤 เจ้าหน้าที่ เลือกแบบกระจก
จาก Master: แบบกระจก"]:::user F --> G["🤖 AI Auto Cut (Optimization)
คำนวณวิธีตัดจากเส้นวัสดุ 6.4 ม.
ให้เหลือเศษน้อยที่สุด"]:::ai G --> H["ถอด BOM วัสดุ
Master: วัสดุ + ปริมาณ + Cutting plan"] H --> I{"📝 Workflow อนุมัติ"}:::gate I -->|"ตีกลับ / แก้ไข"| F I -->|"✅ อนุมัติ"| J["📤 ออกรายงาน
จัดซื้อ · สต็อก · ช่างตัดแบบ"] J --> K["🔌 ส่ง API → ระบบจัดซื้อภายนอก"]:::ext classDef user fill:#dbeafe,stroke:#3b82f6,color:#0b2447; classDef ai fill:#dcfce7,stroke:#16a34a,color:#064e3b; classDef gate fill:#fef3c7,stroke:#d97706,color:#7c2d12; classDef ext fill:#fae8ff,stroke:#c026d3,color:#701a75;
รูปที่ 1 — Business Workflow (9 ขั้น) · จุดสีเขียว = งาน AI, จุดสีเหลือง = จุดอนุมัติ, จุดชมพู = เชื่อมต่อภายนอก

จุดที่เป็น "หัวใจ" (IP)

ขั้น 2 (AI ถอดแบบ) และขั้น 6 (Auto Cut) คือมูลค่าหลักของระบบ — ควรแยกเป็น service เฉพาะเพื่อพัฒนา/ปรับจูนได้อิสระ

Master Data 2 ชุด

แบบกระจก (ใช้ในขั้น 5) และ วัสดุ/เส้นอลูมิเนียม (ใช้ในขั้น 6) เป็นข้อมูลตั้งต้นที่แต่ละ tenant กำหนดเอง

Approval ก่อนออก

ขั้น 8 คั่นก่อนขั้น 7 เสมอ — รายงาน/PO จะไม่ถูกส่งออกจนกว่าจะอนุมัติ พร้อมเก็บ audit log ว่าใครอนุมัติเมื่อไร

2 Data Flow Diagram — Level 0 (Context)

มองระบบเป็นกล่องเดียว เพื่อเห็นว่า "ใคร" ส่งข้อมูลอะไรเข้า-ออกบ้าง รวมถึงผู้เกี่ยวข้องฝั่ง SaaS

flowchart LR
    EST(["👤 เจ้าหน้าที่ถอดแบบ"]):::ent
    APP(["👔 ผู้อนุมัติ / หัวหน้า"]):::ent
    PUR(["🛒 ฝ่ายจัดซื้อ"]):::ent
    STK(["📦 ฝ่ายสต็อก"]):::ent
    CUT(["✂️ ช่างตัดแบบ"]):::ent
    EXT["🔌 ระบบจัดซื้อภายนอก / ERP"]:::sys
    ADM(["📊 เจ้าของ SaaS / Platform Admin"]):::ent

    SYS((("0
ระบบถอดแบบ
& Auto Cut"))):::proc EST -->|"ไฟล์ CAD/PDF/Photo, เลือกแบบกระจก"| SYS SYS -->|"รายการกระจก+ไซซ์, ผล Auto Cut"| EST SYS -->|"คำขออนุมัติรายงาน"| APP APP -->|"อนุมัติ / ตีกลับ"| SYS SYS -->|"รายงานสั่งซื้อ + BOM"| PUR SYS -->|"รายการเบิก-สต็อก"| STK SYS -->|"ใบตัด / Cutting Plan"| CUT SYS -->|"API: PO / รายการวัสดุ"| EXT EXT -->|"สถานะ PO / ราคาวัสดุ"| SYS SYS -->|"Dashboard ยอดขาย + Usage"| ADM classDef ent fill:#dbeafe,stroke:#3b82f6,color:#0b2447; classDef proc fill:#0ea5e9,stroke:#0369a1,color:#ffffff; classDef sys fill:#fae8ff,stroke:#c026d3,color:#701a75;
รูปที่ 2 — Context Diagram (DFD Level 0) · วงกลมกลาง = ทั้งระบบ, กล่องขอบมน = External Entity
หมายเหตุ: "เจ้าของ SaaS / Platform Admin" เป็น external entity ที่เพิ่มเข้ามาเพราะออกแบบเผื่อ SaaS — เขาไม่ยุ่งกับงานถอดแบบของ tenant แต่ดูภาพรวมยอดขาย/การใช้งานข้ามลูกค้า ซึ่งเป็น data flow คนละชั้นกับข้อมูลปฏิบัติงาน

2.1 DFD Level 1 (แตกกระบวนการ — เผื่อไว้)

พอ Level 0 นิ่งแล้ว ขั้นถัดไปคือแตกกล่องกลางเป็น process ย่อย + Data Store — ร่างไว้ให้เห็นทิศทาง

flowchart TB
    EST(["👤 เจ้าหน้าที่"]):::ent
    APP(["👔 ผู้อนุมัติ"]):::ent
    OUT(["🛒📦✂️ จัดซื้อ/สต็อก/ช่างตัด"]):::ent
    EXT["🔌 ระบบจัดซื้อภายนอก"]:::sys

    P1(["1.0 รับ & จัดการไฟล์"]):::proc
    P2(["2.0 AI ถอดแบบ"]):::proc
    P3(["3.0 จัดการรายการ & Master"]):::proc
    P4(["4.0 Auto Cut Optimization"]):::proc
    P5(["5.0 Workflow อนุมัติ"]):::proc
    P6(["6.0 ออกรายงาน & API"]):::proc

    D1[("D1 · Projects / ไฟล์")]:::ds
    D2[("D2 · Master แบบกระจก")]:::ds
    D3[("D3 · Master วัสดุ")]:::ds
    D4[("D4 · Takeoff & Cut Result")]:::ds
    D5[("D5 · รายงาน / Approval log")]:::ds

    EST --> P1 --> D1
    P1 --> P2 --> D4
    EST --> P3
    D2 --> P3 --> D4
    D4 --> P4
    D3 --> P4 --> D4
    P4 --> P5
    P5 <--> APP
    P5 --> D5
    P5 --> P6
    D5 --> P6
    P6 --> OUT
    P6 <--> EXT
    classDef ent fill:#dbeafe,stroke:#3b82f6,color:#0b2447;
    classDef proc fill:#dcfce7,stroke:#16a34a,color:#064e3b;
    classDef ds fill:#fff7ed,stroke:#ea580c,color:#7c2d12;
    classDef sys fill:#fae8ff,stroke:#c026d3,color:#701a75;
    
รูปที่ 3 — DFD Level 1 (ร่างเบื้องต้น) · D1–D5 = Data Store ที่ทุกตัวผูก tenant_id

3 Infrastructure บน INET Cloud

เน้น managed component + จำนวน service น้อยที่สุด เพื่อ MA เบา ใช้ทีมเดียวกับ ERP · ทุกอย่างอยู่ใน DC ไทย (ดีต่อ PDPA และ latency)

flowchart TB
    subgraph U["👥 ผู้ใช้งาน (ทุก tenant)"]
      BR["Web Browser
เจ้าหน้าที่ / ผู้อนุมัติ / จัดซื้อ"] end subgraph INET["☁️ INET Cloud — Thailand"] direction TB EDGE["🛡️ Load Balancer + WAF
TLS / โดเมน *.yoursaas.co.th"]:::edge subgraph APP["Application Tier (Container)"] WEB["🌐 Web App + API
Multi-tenant (tenant_id)
Auth · RBAC"]:::app WRK["⚙️ AI Worker
Takeoff + Auto Cut
(async job)"]:::app end Q[("📨 Queue / Redis
คิวงาน AI")]:::infra DB[("🐘 PostgreSQL
Multi-tenant + RLS
ย้ายมาจาก Neon")]:::db OBJ[("🗄️ Object Storage (S3-compat)
ไฟล์ CAD/PDF/Photo + รายงาน")]:::infra BI["📊 Analytics / BI
ยอดขาย + Usage (read replica)"]:::bi end LLM["🤖 AI Model API
(ถอดแบบ / vision)"]:::ext EXT["🔌 ระบบจัดซื้อภายนอก
ของแต่ละ tenant"]:::ext BR -->|HTTPS| EDGE --> WEB WEB --> DB WEB --> OBJ WEB -->|"enqueue งาน"| Q --> WRK WRK --> LLM WRK --> DB WRK --> OBJ WEB -->|"REST/Webhook"| EXT DB -.->|"replica / ETL"| BI classDef edge fill:#e0f2fe,stroke:#0284c7,color:#082f49; classDef app fill:#dcfce7,stroke:#16a34a,color:#064e3b; classDef db fill:#ede9fe,stroke:#7c3aed,color:#3b0764; classDef infra fill:#fff7ed,stroke:#ea580c,color:#7c2d12; classDef bi fill:#fef9c3,stroke:#ca8a04,color:#713f12; classDef ext fill:#fae8ff,stroke:#c026d3,color:#701a75;
รูปที่ 4 — Infrastructure (Multi-tenant, single deployment) บน INET

ทำไมแยก AI Worker

งานถอดแบบ/Auto Cut กินเวลา (วินาที–นาที) ถ้ารันในเว็บจะค้าง → ใช้ Queue รับงานแล้วให้ Worker ทำแบบ async, เว็บเด้งผลเมื่อเสร็จ scale worker แยกได้

ย้าย Neon → INET

Neon เป็น PostgreSQL อยู่แล้ว ย้ายง่ายด้วย pg_dump / restore · บน INET ใช้ managed Postgres ถ้ามี หรือ Postgres + auto-backup ให้ MA เบา

MA ต่ำสุด

เหลือของที่ต้องดูแลจริง ๆ แค่ 4 กล่อง: App container, Postgres, Object storage, Queue — ที่เหลือเป็น managed/external หมด

1 deployment / ทุก tenant

ไม่แยกเครื่องรายลูกค้า → อัปเดตครั้งเดียวได้ทุกราย ต้นทุน MA ไม่โตตามจำนวนลูกค้า (ดูหัวข้อ 4)

ชั้นบริการที่แนะนำบน INETทำไม / MA
ComputeINET Container / Managed K8s (หรือ VM + Docker Compose ช่วง MVP)เริ่มเล็กด้วย Compose ก่อน แล้วค่อยขยับเป็น K8s เมื่อลูกค้าเยอะ
DatabasePostgreSQL (managed ถ้ามี) + RLS + auto backupโมเดลเดียวกับ ERP → ทีมเดิมดูแลได้
Object StorageINET Object Storage (S3-compatible)เก็บไฟล์แบบ/รายงาน แยกจาก DB, ราคาถูก
QueueRedis / RabbitMQ (container เล็ก)คิวงาน AI แบบ async
AIModel API ภายนอก + skill ถอดแบบของคุณไม่ต้อง MA GPU เอง; สลับผู้ให้บริการได้
ObservabilityLog + Uptime + Backup monitorกันเงียบ ๆ เจ๊ง โดยเฉพาะช่วง SaaS หลายลูกค้า

4 Multi-tenant ตั้งแต่วันแรก

Aluart คือ tenant แรก แต่ schema เตรียมรองรับหลายบริษัท เพื่อไม่ต้องรื้อทีหลัง

แนวทางที่ MA เบาที่สุดสำหรับ SaaS คือ Shared Database + Shared Schema + tenant_id ทุกตาราง แล้วบังคับการแยกข้อมูลด้วย PostgreSQL Row-Level Security (RLS) — ทุก query จะเห็นเฉพาะข้อมูลของ tenant ตัวเองอัตโนมัติ ระดับฐานข้อมูล ไม่ต้องพึ่งโค้ดแอปอย่างเดียว ปลอดภัยกว่าและ deploy ชุดเดียวจบ

Shared schema + RLS แนะนำเริ่มต้น

DB เดียว, ตารางเดียว, กรองด้วย tenant_id + RLS · MA ต่ำสุด, ต้นทุนต่อลูกค้าถูกสุด · เหมาะกับลูกค้า SME กระจกทั่วไป

Schema-per-tenant เผื่ออนาคต

แยก schema ต่อลูกค้ารายใหญ่ที่ขอ isolation มากขึ้น · ยังอยู่ DB เดียว MA พอรับได้

DB-per-tenant เฉพาะราย Enterprise

ลูกค้าใหญ่มาก/ขอ DB แยกจริง · MA สูงขึ้น เก็บเป็น plan ราคาแพง

สิ่งที่ต้องมีในทุกตารางตั้งแต่วันแรก: tenant_id, created_by, created_at และเปิด RLS policy · Master data (แบบกระจก, วัสดุ) ก็ผูก tenant_id ด้วย เพราะแต่ละบริษัทมีสเปก/ราคาของตัวเอง

ฝั่งผู้ใช้ให้มีชั้น Tenant → Users → Roles (RBAC) เช่น เจ้าหน้าที่ถอดแบบ / ผู้อนุมัติ / จัดซื้อ / ผู้ดูแล tenant และแยกอีกชั้นเป็น Platform Admin (ฝั่งคุณ) ที่ดูแลทุก tenant, จัดการ subscription และเห็นภาพยอดขายรวม

5 คิดเผื่อเรื่อง "ข้อมูลการขาย"

คำว่าข้อมูลการขายมี 2 ชั้น ควรออกแบบแยกกันชัด ๆ ตั้งแต่ต้น

ชั้น A — ยอดขายของบริษัทกระจก (ข้อมูลของ tenant)

ระบบถอดแบบ → ได้ BOM → รู้ต้นทุนวัสดุอยู่แล้ว เพียงเพิ่ม ชั้นราคา/ใบเสนอราคา ก็ต่อยอดเป็นข้อมูลการขายได้ทันที: จากแบบ → คำนวณต้นทุน → บวก margin → ออกใบเสนอราคา → ติดตามว่า ปิดการขายได้/ไม่ได้ (win/loss). เก็บ quotation, order, ต้นทุนวัสดุ, กำไร แล้วทำ Dashboard ต่อ tenant: ยอดเสนอราคา, อัตราปิดการขาย, กำไรต่อโปรเจกต์, % เศษวัสดุที่ Auto Cut ช่วยประหยัด (จุดขายที่จับต้องได้)

ชั้น B — ยอดขายของ SaaS (ข้อมูลของคุณ JJ)

เมื่อขายหลายบริษัท ต้องมี metric ธุรกิจของแพลตฟอร์มเอง แยก data store ออกจากข้อมูลปฏิบัติงานของ tenant:

Subscription / Billing

แพ็กเกจ, รอบบิล, MRR/ARR, ลูกค้าใหม่-ยกเลิก (churn)

Usage / Product analytics

จำนวนไฟล์ที่ถอดแบบ, งาน Auto Cut, tenant ที่ active — ใช้คิดราคาตามการใช้งานและดูฟีเจอร์ที่ถูกใช้จริง

Conversion funnel

ทดลองใช้ → จ่ายเงิน, ระบุลูกค้าที่กำลังจะหลุด เพื่อเข้าไปดูแลก่อน

โอกาสระยะยาว (Data product): เมื่อมีลูกค้าหลายราย คุณจะมีข้อมูล ค่าเฉลี่ยเศษวัสดุ / ดัชนีราคากระจก-อลูมิเนียม / สเปกยอดนิยม แบบไม่ระบุตัวตน (anonymized & aggregated) — ต่อยอดเป็น benchmark หรือ report ขายเพิ่มได้ กลายเป็น moat ที่คู่แข่งลอกยาก · ข้อควรระวัง: ต้องแยกสิทธิ์ข้อมูลชัดเจนใน ToS และทำ anonymize จริง เพราะเป็นข้อมูลธุรกิจของลูกค้า
หลักการแยกข้อมูล: ข้อมูลปฏิบัติงานของ tenant (แบบ, BOM, รายงาน) อยู่ใน DB หลักภายใต้ RLS · ส่วนข้อมูลธุรกิจของแพลตฟอร์ม (billing, usage รวม) แยกไป BI/warehouse ผ่าน read replica — ไม่ให้ query วิเคราะห์หนัก ๆ ไปกวน DB ที่ลูกค้ากำลังใช้งาน

6 หมายเหตุ: Auto Cut คือโจทย์ Optimization

ขั้นที่ 6 ในทางเทคนิคคือ 1D Cutting Stock Problem — จัดชิ้นตัดลงเส้นวัสดุมาตรฐาน (เช่น 6.4 ม.) ให้ใช้จำนวนเส้นน้อยสุด/เศษน้อยสุด

เริ่มเร็ว (MVP)

อัลกอริทึม First-Fit Decreasing — เรียงชิ้นจากยาวไปสั้นแล้วหย่อนลงเส้น เขียนง่าย ผลใช้ได้จริงในเวลาไม่กี่ ms

ผลดีที่สุด (ต่อยอด)

ILP / Column Generation ด้วย solver (เช่น OR-Tools, CBC) — ได้แผนตัดที่ใกล้ optimal จริง เหมาะกับงานล็อตใหญ่

เงื่อนไขจริงหน้างาน

เผื่อ ระยะเลื่อยกิน (kerf), ความยาวเศษที่เก็บใช้ต่อได้, จำกัดจำนวนรอยต่อ/เส้น — ใส่เป็นพารามิเตอร์ต่อวัสดุใน Master

ผลลัพธ์ที่ส่งช่างตัดควรเป็น Cutting Plan ต่อเส้น (เส้นที่ 1 ตัด 2000+1800+1200, เหลือเศษ 1400 มม. …) ไม่ใช่แค่ยอดรวมวัสดุ — เพราะนี่คือส่วนที่ช่วยลดของเสียจริง และเป็นจุดขายของระบบ

7 Roadmap: Aluart → SaaS

Schema multi-tenant ตั้งแต่วันแรก แต่ทยอยเปิดฟีเจอร์ SaaS ทีหลัง — ไม่ต้องรื้อ

Phase 0 · MVP (Aluart live)

ทำ workflow หลักให้ครบ + schema multi-tenant

ถอดแบบ → รายการ → Auto Cut (First-Fit) → รายงาน → อนุมัติ · มี tenant_id + RLS แต่มีลูกค้าจริงแค่ Aluart · deploy บน INET ด้วย Compose

Phase 1 · Integration & BI

API จัดซื้อภายนอก + Dashboard ยอดขาย/เศษวัสดุ

เชื่อม API (ข้อ 9), ทำใบเสนอราคา + ต้นทุน (ชั้น A), Auto Cut อัปเป็น solver

Phase 2 · เปิดเป็น SaaS

Self-serve onboarding + Billing + Plan tiers

สมัคร tenant เอง, ระบบแพ็กเกจ/บิล (ชั้น B), Platform Admin console, ย้าย compute ขึ้น managed K8s

Phase 3 · Data product & scale

Benchmark ข้อมูลรวม + มือถือ + hardening

รายงาน benchmark เศษวัสดุ/ราคา (anonymized), แอปมือถือหน้างาน, ทำ SLA/observability เต็มรูปแบบ

8 Tech Stack ที่แนะนำ (ให้ MA ทีมเดียวกับ ERP)

หลักคิด: ใช้ของที่ทีมคุณดูแลอยู่แล้วให้มากที่สุด โดยเฉพาะ Postgres — ลดของใหม่ที่ต้องเรียนรู้

ส่วนแนะนำเหตุผล
DatabasePostgreSQL + RLS คงเดิมตรงกับ ERP/Neon เดิม, migrate ง่าย, RLS รองรับ multi-tenant ในตัว
Backend / APIเฟรมเวิร์กเดียวกับ ERP ของคุณ (Node/NestJS หรือ Laravel)ทีมเดิมดูแลได้ ไม่ต้องมี stack ที่ 2 · ถ้ายังไม่ล็อก แนะนำสาย TypeScript เพราะแชร์ type กับ frontend
FrontendReact / Next.js (SPA)UI ตาราง/ฟอร์ม/แนบไฟล์เยอะ เหมาะกับ component-based
AI WorkerPython service (แยก) + queueระบบ AI/optimization (OR-Tools) อยู่ฝั่ง Python สะดวกสุด แยก scale ได้
QueueRedis + workerเบา, ใช้เป็น cache ได้ด้วย
StorageINET Object Storage (S3 SDK)มาตรฐาน S3 ย้าย/สลับ cloud ได้ในอนาคต
DeployDocker · Compose (MVP) → K8s (SaaS)เริ่มเบา ค่อยขยับตามจำนวนลูกค้า
ถ้าให้เลือกสิ่งที่ "ต้องล็อกวันนี้" 3 อย่าง: (1) ใส่ tenant_id + เปิด RLS ทุกตารางตั้งแต่ commit แรก · (2) แยกงาน AI ออกเป็น worker + queue อย่ายัดในเว็บ · (3) เก็บไฟล์ลง object storage อย่าเก็บลง DB — สามอย่างนี้แก้ทีหลังแพงที่สุด