WKM
แอปเว็บจัดการอีเวนท์กับเพื่อน: ปฏิทินส่วนตัว, กลุ่มแชท, และระบบ "หาเวลาว่าง" ที่ใช้ AI (Gemini) วิเคราะห์ตารางของสมาชิกทุกคน แล้วโพสต์ช่วงเวลาที่ทุกคนว่างตรงกันกลับเข้าแชท
ทุกอย่างเป็นภาษาไทย และใช้โซนเวลา Asia/Bangkok (UTC+7)
สแต็กหลัก
- Framework: Next.js (App Router) + React + TypeScript
- Runtime: Bun — รันด้วย
bun --bun - DB: PostgreSQL + Drizzle
- Auth: better-auth (email/รหัสผ่าน + Google + Turnstile captcha)
- AI: Gemini (
gemini-3.1-flash-lite) — ใช้กับระบบหาเวลาว่างเท่านั้น - UI: shadcn/ui + Tailwind, ฟอนต์ไทย IBM Plex Sans Thai
เริ่มต้นพัฒนา
- ติดตั้ง dependencies:
bun install - สร้าง
.env— คัดลอกจากตารางตัวแปรด้านล่าง (จำเป็นจริง ๆ แค่DATABASE_URLและBETTER_AUTH_SECRET) - ตั้งค่า DB (schema อยู่ใน
db/schema.ts) แล้ว push schema:bunx drizzle-kit push - รัน dev:
bun --bun next dev
ตัวแปร environment
| ตัวแปร | จำเป็น? | ใช้ทำอะไร |
|---|---|---|
DATABASE_URL |
✅ | PostgreSQL connection string |
BETTER_AUTH_SECRET |
✅ | secret เซสชัน |
BETTER_AUTH_URL |
⬜ | base URL (default http://localhost:3000) |
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET |
⬜ | Google OAuth (ถ้าไม่มี Google login ก็ใช้งานได้) |
NEXT_PUBLIC_TURNSTILE_SITE_KEY / TURNSTILE_SECRET_KEY |
⬜ | Turnstile captcha |
GEMINI_API_KEY |
⬜ | ถ้าไม่มี ระบบหาเวลายังทำงานได้แบบ deterministic |
คำสั่งที่ใช้บ่อย
bun --bun next dev # dev server (ห้ามรันพร้อมกันหลาย instance!)
bunx drizzle-kit generate # สร้าง migration ใหม่
bunx tsc --noEmit # typecheck
bunx eslint <files> # lint
โครงสร้าง — ดูไฟล์ไหนถ้าจะแก้เรื่องอะไร
app/
calendar/ # ปฏิทินส่วนตัว (server actions + optimistic UI)
group/ # แชท + กลุ่ม: sidebar, chat UI, หน้า Ranking
[id]/ranking/ # Ranking ขาบิด
auth/ # หน้า sign-in / sign-up
components/
event-calendar/ # ปฏิทิน 4 มุมมอง + drag & drop
free-time/ # Dialog "หาเวลาว่าง"
group-events/ # บัตรนัดในแชท + จบอีเวนท์
ranking/ # podium / list / tier badge
db/schema.ts # Drizzle schema ทั้งหมด
lib/
group-realtime.ts # realtime: SSE + PG LISTEN/NOTIFY + presence
free-time.ts # engine คำนวณช่วงว่าง (deterministic — ต้องแม่น 100%)
gemini.ts # wrapper Gemini
gemini-prompt.ts # prompts + schema ให้ Gemini
สถาปัตยกรรมคร่าว ๆ
ข้อมูลหลัก
- ผู้ใช้ (
user) — ร่วมกับตารางของ better-auth (session,account, ...) - กลุ่ม (
group,group_member) — กลุ่มแชท + สมาชิก (status: pending/accepted/declined) - แชท (
group_message) —senderIdเป็นNULL+senderKind='assistant'สำหรับข้อความ AI (AI ไม่มีบัญชี user) - ปฏิทินส่วนตัว (
calendar_event) — ตารางเวลาของแต่ละคน - นัดกลุ่ม (
group_event,group_event_attendance) — นัดหมายในแชท + ลงชื่อเข้าร่วม/บิด
ระบบหาเวลาว่าง (ฟีเจอร์หลัก)
หลักการสำคัญ: AI ไม่สร้างคำตอบเอง เพื่อการันตี "ไม่มีช่วงเวลาผิด"
- Engine ฝั่ง server (
lib/free-time.ts) คำนวณช่วงว่างจากตารางเวลาจริงใน DB — แม่น 100% - Gemini แค่ช่วย 2 อย่าง: แปลคำขอเป็นเงื่อนไข (
interpretRequest) และจัดอันดับสล็อต (rankSlots) - ผลลัพธ์ถูกโพสต์กลับเข้าแชทเป็นข้อความ AI (markdown)
ถ้า AI ล่มหรือไม่มี API key → fallback ไปใช้ deterministic sort (ยังทำงานถูกต้อง)
Realtime
ระบบ SSE + PostgreSQL LISTEN/NOTIFY (lib/group-realtime.ts) — dispatch ใน process ทันที + เผยแพร่ข้าม process ด้วย pg_notify สำหรับกรณีหลาย replica
ข้อควรระวังตอนแก้ code
- ห้ามรัน dev server หลาย instance พร้อมกัน — realtime เป็นแบบ in-process
- อย่าให้ AI คำนวณสล็อตเอง — ให้ AI ทำงานกับสล็อตที่ engine คำนวณให้เท่านั้น
- ทุกการคำนวณเวลา ใช้ helper
bangkokParts/dayStartBangkok(โซนเวลา UTC+7) ไม่ใช้ local time - เวลาที่เสนอ ใช้ day-band default 06:00–22:00 (กันเวลากลางคืน)
- ทุก mutation ผ่าน server action + validate ด้วย zod +
revalidatePath - AI ข้อความต้องเป็น
senderId = NULL+senderKind = 'assistant'เสมอ
Languages
TypeScript
94.6%
Shell
3.3%
Perl
0.7%
CSS
0.7%
PLpgSQL
0.5%