เอกสารเดียวจบ ครอบคลุมทั้ง 1) การติดตั้งโค้ดบนพีซี, 2) การเชื่อมต่อฐานข้อมูล PostgreSQL ที่รันเป็นคอนเทนเนอร์อยู่แล้ว, 3) การสร้างตารางที่จำเป็น และ 4) การ build/run แบบ production
โปรเจกต์นี้เป็น 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)
- ปุ่ม Save (Automation / Diagram) → ยังเป็นแค่ toast แจ้งเตือน
- ปุ่ม Run ใน Automation Playground → ยังเป็นอนิเมชันจำลองเท่านั้น
- Fast-Note ไม่มี autosave ขึ้น server (บันทึกเป็นไฟล์ลงเครื่องเท่านั้น)
ส่วนอื่น ๆ (ลาก, ซูม, pan, เชื่อมเส้น, Inspector, Export) ทำงานจริงทุกอย่าง
node --version # ต้องการ >= 20.x
pnpm --version # ต้องการ >= 9.x (หรือใช้ npm/yarn ก็ได้)
docker --version # ถ้าจะรันเป็นคอนเทนเนอร์
psql --version # ไม่บังคับ แต่สะดวกเวลารัน SQLถ้ายังไม่มี pnpm:
npm install -g pnpm- ดาวน์โหลดโค้ดจาก Repository ของคุณ (หรือ Clone ผ่าน Git)
- แตกไฟล์ลงโฟลเดอร์ที่ต้องการ เช่น
D:\projects\watchdocs - เปิด terminal ที่โฟลเดอร์นั้น แล้วรัน:
pnpm install
pnpm devเปิดเบราว์เซอร์ไปที่ http://localhost:3000 จะเห็นแอปทำงานทันที
สร้างไฟล์ .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
# ดูรายชื่อคอนเทนเนอร์
docker ps
# เข้า shell ของ postgres
docker exec -it <ชื่อคอนเทนเนอร์> psql -U postgresCREATE USER watchdocs WITH PASSWORD 'your_password';
CREATE DATABASE watchdocs OWNER watchdocs;
GRANT ALL PRIVILEGES ON DATABASE watchdocs TO watchdocs;
-- เชื่อมเข้า database ที่เพิ่งสร้าง
\c watchdocsคัดลอก 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 $$;
-- ตรวจสอบผล
\dtpsql "$DATABASE_URL" -c "SELECT count(*) FROM automations;"ถ้าเห็น count = 0 แปลว่าเชื่อมต่อได้แล้ว
ตอนนี้โค้ดยังไม่มี driver pg เวลาจะต่อ backend ให้ทำดังนี้:
pnpm add pg
pnpm add -D @types/pgimport { 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()
}
}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
pnpm build
pnpm start # start บน port 3000
# หรือกำหนด port เอง
PORT=8080 pnpm startสร้างไฟล์ 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
- หา IP เครื่องพีซี เช่น
192.168.1.50 - เปิด firewall port 3000 (Windows: Defender Firewall > Inbound Rules)
- บนมือถือ เข้า
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
| ปัญหา | วิธีแก้ |
|---|---|
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 |
- รัน
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
เสร็จแล้ว พร้อมใช้งานจริง 🎉