Skip to content

Latest commit

 

History

History
420 lines (335 loc) · 17.8 KB

File metadata and controls

420 lines (335 loc) · 17.8 KB

WatchDocs – คู่มือคัดลอกโค้ดไปรันบนพีซี (Self-Host)

เอกสารเดียวจบ ครอบคลุมทั้ง 1) การติดตั้งโค้ดบนพีซี, 2) การเชื่อมต่อฐานข้อมูล PostgreSQL ที่รันเป็นคอนเทนเนอร์อยู่แล้ว, 3) การสร้างตารางที่จำเป็น และ 4) การ build/run แบบ production


1. สถานะปัจจุบันของโปรเจกต์

โปรเจกต์นี้เป็น Next.js 16 (App Router) + React 19 + Tailwind v4

มี 4 หน้าจอหลักที่ใช้งานได้ครบ (ทุกหน้ารองรับการใช้งานบน มือถือ แล้ว):

หน้า สถานะ หมายเหตุ
Automation Playground ใช้งานได้ครบ ลาก/วางโหนด, เชื่อมเส้น, Inspector, Save JSON, Run (จำลอง)
Preview (Markdown / HTML) ใช้งานได้ครบ เอดิเตอร์สดทั้ง MD และ HTML, Export, Share
Fast-Note ใช้งานได้ครบ Rich text, เปลี่ยนฟอนต์/ขนาด/จัดเรียง, Save เป็นไฟล์ HTML
Playground Diagram ใช้งานได้ครบ อุปกรณ์เครือข่าย/กล้อง/คลาวด์ 28 ชนิด, Text Box, Region, อนิเมชันการไหล, Export JSON + SVG

❗ ตอนนี้ข้อมูลทั้งหมดเก็บอยู่ใน state ของเบราว์เซอร์ เท่านั้น หากต้องการ "บันทึกถาวร" ให้ทำตามหัวข้อ 5 (เชื่อม Postgres)

สิ่งที่ยัง "ไม่ผูก backend" (เป็น mock)

  • ปุ่ม Save (Automation / Diagram) → ยังเป็นแค่ toast แจ้งเตือน
  • ปุ่ม Run ใน Automation Playground → ยังเป็นอนิเมชันจำลองเท่านั้น
  • Fast-Note ไม่มี autosave ขึ้น server (บันทึกเป็นไฟล์ลงเครื่องเท่านั้น)

ส่วนอื่น ๆ (ลาก, ซูม, pan, เชื่อมเส้น, Inspector, Export) ทำงานจริงทุกอย่าง


2. Prerequisite บนพีซี

node --version     # ต้องการ >= 20.x
pnpm --version     # ต้องการ >= 9.x  (หรือใช้ npm/yarn ก็ได้)
docker --version   # ถ้าจะรันเป็นคอนเทนเนอร์
psql --version     # ไม่บังคับ แต่สะดวกเวลารัน SQL

ถ้ายังไม่มี pnpm:

npm install -g pnpm

3. คัดลอกโค้ด + ติดตั้ง dependency

  1. ดาวน์โหลดโค้ดจาก Repository ของคุณ (หรือ Clone ผ่าน Git)
  2. แตกไฟล์ลงโฟลเดอร์ที่ต้องการ เช่น D:\projects\watchdocs
  3. เปิด terminal ที่โฟลเดอร์นั้น แล้วรัน:
pnpm install
pnpm dev

เปิดเบราว์เซอร์ไปที่ http://localhost:3000 จะเห็นแอปทำงานทันที


4. สร้างไฟล์ .env.local

สร้างไฟล์ .env.local ที่ root ของโปรเจกต์ (ไฟล์นี้ถูก .gitignore ไว้แล้ว)

# --- PostgreSQL ที่รันเป็นคอนเทนเนอร์บนเครื่องคุณ ---
# แก้ host / port / user / password / dbname ตามที่ตั้งไว้ในคอนเทนเนอร์
DATABASE_URL=postgres://watchdocs:your_password@localhost:5432/watchdocs

# ถ้าต้องใช้ SSL (เช่น Postgres จาก Neon/Supabase) ต่อ ?sslmode=require ท้าย URL
# DATABASE_URL=postgres://user:pass@host:5432/db?sslmode=require

# --- Optional: ตั้ง base URL ของเว็บ ---
NEXT_PUBLIC_APP_URL=http://localhost:3000

💡 ถ้า Next.js รันเป็นคอนเทนเนอร์ด้วย ใน docker network เดียวกัน ให้เปลี่ยน localhost เป็นชื่อ service ของ Postgres เช่น postgres ตัวอย่าง: postgres://watchdocs:pass@postgres:5432/watchdocs


5. ตั้งค่า PostgreSQL + สร้างตาราง

5.1 เข้าไปในคอนเทนเนอร์ Postgres ที่รันอยู่

# ดูรายชื่อคอนเทนเนอร์
docker ps

# เข้า shell ของ postgres
docker exec -it <ชื่อคอนเทนเนอร์> psql -U postgres

5.2 สร้างฐานข้อมูล + user (ถ้ายังไม่มี)

CREATE USER watchdocs WITH PASSWORD 'your_password';
CREATE DATABASE watchdocs OWNER watchdocs;
GRANT ALL PRIVILEGES ON DATABASE watchdocs TO watchdocs;

-- เชื่อมเข้า database ที่เพิ่งสร้าง
\c watchdocs

5.3 สคริปต์ SQL สำหรับสร้างตารางทั้งหมดที่แอปนี้จะใช้

คัดลอก SQL ด้านล่างนี้ทั้งก้อนไปรันใน psql (หรือเซฟเป็นไฟล์ schema.sql แล้ว docker exec -i <postgres> psql -U watchdocs -d watchdocs < schema.sql)

-- =========================================================
--  WatchDocs schema  v1.0
-- =========================================================
CREATE EXTENSION IF NOT EXISTS "pgcrypto";

-- ผู้ใช้งาน (ถ้ายังไม่มีระบบ auth ภายนอก ใช้ตารางนี้)
CREATE TABLE IF NOT EXISTS app_users (
  id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  email       TEXT UNIQUE NOT NULL,
  name        TEXT,
  password_hash TEXT,           -- ใช้ bcrypt เท่านั้น ถ้าทำ auth เอง
  created_at  TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at  TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- Fast-Note: เก็บโน้ตแบบ rich-text (HTML)
CREATE TABLE IF NOT EXISTS notes (
  id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id     UUID REFERENCES app_users(id) ON DELETE CASCADE,
  title       TEXT NOT NULL DEFAULT 'Untitled',
  content_html TEXT NOT NULL DEFAULT '',
  font_family TEXT DEFAULT 'Inter, system-ui, sans-serif',
  font_size   INT  DEFAULT 16,
  text_align  TEXT DEFAULT 'left',
  created_at  TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at  TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_notes_user ON notes(user_id);
CREATE INDEX IF NOT EXISTS idx_notes_updated ON notes(updated_at DESC);

-- Preview: เก็บเอกสาร markdown / html
CREATE TABLE IF NOT EXISTS preview_docs (
  id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id     UUID REFERENCES app_users(id) ON DELETE CASCADE,
  title       TEXT NOT NULL DEFAULT 'Untitled',
  mode        TEXT NOT NULL CHECK (mode IN ('markdown','html')),
  body        TEXT NOT NULL DEFAULT '',
  created_at  TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at  TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_preview_user ON preview_docs(user_id);

-- Automation Playground: แต่ละ workflow มี nodes + edges เป็น JSON
CREATE TABLE IF NOT EXISTS automations (
  id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id     UUID REFERENCES app_users(id) ON DELETE CASCADE,
  name        TEXT NOT NULL DEFAULT 'Untitled Workflow',
  description TEXT,
  nodes       JSONB NOT NULL DEFAULT '[]'::jsonb,
  edges       JSONB NOT NULL DEFAULT '[]'::jsonb,
  variables   JSONB NOT NULL DEFAULT '{}'::jsonb,
  created_at  TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at  TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_automations_user ON automations(user_id);

-- บันทึกการรัน workflow แต่ละครั้ง
CREATE TABLE IF NOT EXISTS automation_runs (
  id            UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  automation_id UUID NOT NULL REFERENCES automations(id) ON DELETE CASCADE,
  status        TEXT NOT NULL CHECK (status IN ('pending','running','success','failed')),
  started_at    TIMESTAMPTZ NOT NULL DEFAULT now(),
  finished_at   TIMESTAMPTZ,
  logs          JSONB NOT NULL DEFAULT '[]'::jsonb,
  error         TEXT
);
CREATE INDEX IF NOT EXISTS idx_runs_auto ON automation_runs(automation_id, started_at DESC);

-- Playground Diagram: เก็บ canvas ทั้งหมด (devices, connections, text boxes, regions)
CREATE TABLE IF NOT EXISTS diagrams (
  id           UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id      UUID REFERENCES app_users(id) ON DELETE CASCADE,
  name         TEXT NOT NULL DEFAULT 'Untitled Diagram',
  description  TEXT,
  canvas       JSONB NOT NULL DEFAULT '{}'::jsonb,   -- zoom, pan, grid settings
  devices      JSONB NOT NULL DEFAULT '[]'::jsonb,
  connections  JSONB NOT NULL DEFAULT '[]'::jsonb,
  text_boxes   JSONB NOT NULL DEFAULT '[]'::jsonb,
  regions      JSONB NOT NULL DEFAULT '[]'::jsonb,
  flow_animations JSONB NOT NULL DEFAULT '[]'::jsonb,
  background_image BYTEA,
  created_at   TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at   TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_diagrams_user ON diagrams(user_id);

-- trigger อัปเดต updated_at อัตโนมัติ
CREATE OR REPLACE FUNCTION touch_updated_at() RETURNS trigger AS $$
BEGIN
  NEW.updated_at = now();
  RETURN NEW;
END; $$ LANGUAGE plpgsql;

DO $$
DECLARE t text;
BEGIN
  FOREACH t IN ARRAY ARRAY['app_users','notes','preview_docs','automations','diagrams']
  LOOP
    EXECUTE format('DROP TRIGGER IF EXISTS trg_%1$s_updated ON %1$s', t);
    EXECUTE format('CREATE TRIGGER trg_%1$s_updated BEFORE UPDATE ON %1$s
                    FOR EACH ROW EXECUTE FUNCTION touch_updated_at()', t);
  END LOOP;
END $$;

-- ตรวจสอบผล
\dt

5.4 ตรวจว่าเชื่อมต่อได้

psql "$DATABASE_URL" -c "SELECT count(*) FROM automations;"

ถ้าเห็น count = 0 แปลว่าเชื่อมต่อได้แล้ว


6. เพิ่มโค้ดเชื่อมต่อ Postgres (Optional – ถ้าอยากเก็บข้อมูลถาวร)

ตอนนี้โค้ดยังไม่มี driver pg เวลาจะต่อ backend ให้ทำดังนี้:

6.1 ติดตั้ง driver

pnpm add pg
pnpm add -D @types/pg

6.2 สร้าง lib/db.ts

import { Pool } from "pg"

// ใช้ global เพื่อป้องกัน dev HMR สร้าง pool ซ้ำ
const globalForPool = globalThis as unknown as { pgPool?: Pool }

export const pool =
  globalForPool.pgPool ??
  new Pool({
    connectionString: process.env.DATABASE_URL,
    max: 10,
    idleTimeoutMillis: 30_000,
  })

if (process.env.NODE_ENV !== "production") globalForPool.pgPool = pool

export async function query<T = any>(text: string, params: unknown[] = []) {
  const client = await pool.connect()
  try {
    const res = await client.query<T>(text, params)
    return res.rows
  } finally {
    client.release()
  }
}

6.3 ตัวอย่าง Route Handler (app/api/diagrams/route.ts)

import { NextResponse } from "next/server"
import { query } from "@/lib/db"

export async function GET() {
  const rows = await query(`SELECT id, name, updated_at FROM diagrams ORDER BY updated_at DESC`)
  return NextResponse.json(rows)
}

export async function POST(req: Request) {
  const body = await req.json()
  const [row] = await query(
    `INSERT INTO diagrams (name, devices, connections, text_boxes, regions, canvas)
     VALUES ($1,$2,$3,$4,$5,$6) RETURNING id`,
    [body.name, JSON.stringify(body.devices), JSON.stringify(body.connections),
     JSON.stringify(body.textBoxes), JSON.stringify(body.regions), JSON.stringify(body.canvas)]
  )
  return NextResponse.json(row)
}

จากนั้นแก้ handler saveDiagram ใน playground-diagram.tsx ให้ยิง fetch("/api/diagrams", { method: "POST", body: JSON.stringify(...) }) แทน toast


7. Build + Run แบบ Production

วิธีที่ 1: รันตรงบนเครื่อง

pnpm build
pnpm start         # start บน port 3000
# หรือกำหนด port เอง
PORT=8080 pnpm start

วิธีที่ 2: รันเป็นคอนเทนเนอร์ Docker (แนะนำ)

สร้างไฟล์ Dockerfile ที่ root:

# ---------- deps ----------
FROM node:20-alpine AS deps
WORKDIR /app
RUN corepack enable && corepack prepare pnpm@latest --activate
COPY package.json pnpm-lock.yaml* ./
RUN pnpm install --frozen-lockfile || pnpm install

# ---------- build ----------
FROM node:20-alpine AS builder
WORKDIR /app
RUN corepack enable && corepack prepare pnpm@latest --activate
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
RUN pnpm build

# ---------- runner ----------
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production NEXT_TELEMETRY_DISABLED=1
RUN corepack enable && corepack prepare pnpm@latest --activate
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./package.json
EXPOSE 3000
CMD ["pnpm", "start"]

และเพิ่ม service ลงใน docker-compose.yml (ตัวอย่างเมื่อ postgres รันอยู่แล้ว):

services:
  postgres:            # ← อันนี้ของคุณอยู่แล้ว
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: watchdocs
      POSTGRES_PASSWORD: your_password
      POSTGRES_DB: watchdocs
    volumes:
      - pgdata:/var/lib/postgresql/data
    ports:
      - "5432:5432"

  watchdocs:           # ← เพิ่มใหม่
    build: .
    environment:
      DATABASE_URL: postgres://watchdocs:your_password@postgres:5432/watchdocs
      NEXT_PUBLIC_APP_URL: http://localhost:3000
    ports:
      - "3000:3000"
    depends_on:
      - postgres
    restart: unless-stopped

volumes:
  pgdata:

รันด้วยคำสั่ง:

docker compose up -d --build
docker compose logs -f watchdocs

💡 สำคัญ: เมื่อ Next.js รันเป็นคอนเทนเนอร์ ต้องใช้ postgres (ชื่อ service) แทน localhost ใน DATABASE_URL


8. การเปิดใช้จากมือถือในเครือข่ายเดียวกัน

  1. หา IP เครื่องพีซี เช่น 192.168.1.50
  2. เปิด firewall port 3000 (Windows: Defender Firewall > Inbound Rules)
  3. บนมือถือ เข้า http://192.168.1.50:3000

เพราะตอนนี้แอปถูกปรับให้ใช้บนมือถือครบทุกหน้าแล้ว:

  • Navbar มี hamburger menu + ธีม toggle
  • Automation Playground มี Sheet สำหรับ Palette + Inspector
  • Preview จะ stack เอดิเตอร์/พรีวิวแบบบน-ล่างอัตโนมัติ
  • Fast-Note มีแถบฟลอตปุ่ม Save/Share
  • Playground Diagram มี dock เครื่องมือที่ขอบล่างของ canvas

9. Troubleshooting

ปัญหา วิธีแก้
ECONNREFUSED 127.0.0.1:5432 คอนเทนเนอร์ postgres ไม่ได้ expose port → เช็ค ports: - "5432:5432"
password authentication failed DATABASE_URL พิมพ์รหัสผ่านผิด หรือ pg_hba.conf ไม่อนุญาต
relation "diagrams" does not exist ยังไม่ได้รันสคริปต์ SQL หัวข้อ 5.3
Next.js คอนเทนเนอร์ต่อ postgres ไม่ติด ใช้ชื่อ service (postgres) ไม่ใช่ localhost
Port 3000 ชน ใช้ PORT=xxxx pnpm start หรือแก้ ports: ใน compose
Hot reload ไม่ทำงาน ใช้ pnpm dev (ไม่ใช่ pnpm start) ตอน dev
SSL error ตอนต่อ Postgres ข้ามเน็ต เติม ?sslmode=require ท้าย DATABASE_URL

10. Checklist สุดท้ายก่อน Deploy

  • รัน pnpm build ผ่านโดยไม่มี error
  • .env.local / environment ของ compose มี DATABASE_URL ถูกต้อง
  • รันสคริปต์ SQL ในหัวข้อ 5.3 เรียบร้อย
  • psql "$DATABASE_URL" -c "\dt" เห็นตารางครบ 6 ตาราง
  • เปิด http://<host>:3000 บนพีซีและมือถือแล้วใช้งานได้
  • Backup: docker exec -t postgres pg_dump -U watchdocs watchdocs > backup.sql

เสร็จแล้ว พร้อมใช้งานจริง 🎉