Portfolio — Side Project

🚗 วันนี้จอดรถที่ไหน?

Web App บันทึกที่จอดรถ + แจ้งเตือนผ่าน Google Calendar + ส่ง LINE บอกคนที่บ้าน — เกิดจาก pain point จริงที่เจอทุกวัน

tanplanet.info/carpark

Pain Point ที่เจอจริง

ปัญหาเล็กๆ ที่เกิดขึ้นทุกวัน แต่สร้างความรำคาญซ้ำแล้วซ้ำเล่า

ลืมชั้นจอดรถ — ทั้งที่คอนโดและที่ทำงาน

  • คอนโด — ไม่มีช่องจอดประจำ จอดไม่ตรงชั้นทุกวัน ลงลิฟต์มาแล้วจำไม่ได้ว่าจอดชั้นไหน
  • ออฟฟิศ — ที่ทำงานอยู่ในห้างสรรพสินค้า ลานจอดรถหลายชั้น ทุกชั้นหน้าตาเหมือนกันหมด
  • Google Maps "Save Parking" — เซฟตำแหน่งได้ แต่ไม่บอกว่าจอด ชั้นไหน ของตึก
  • คนที่บ้านเป็นห่วง — ต้องพิมพ์บอกใน LINE ทุกครั้งว่าถึงแล้ว จอดชั้นไหน ซึ่งบางทีก็ลืมบอก

Design Thinking Process

จาก pain point สู่ solution ที่ใช้งานจริงทุกวัน

1. Empathize
สังเกตตัวเอง: ทุกเช้าจอดรถที่คอนโด (ไม่มีช่องประจำ) ตอนเย็นกลับมาจำชั้นไม่ได้ ต้องเดินวนหาทุกชั้น
ที่ทำงานก็เหมือนกัน ลานจอดห้างหลายชั้น ออกจากออฟฟิศทีก็ลืมทุกที
นอกจากนี้ คนที่บ้านก็ถามทุกวันว่า "ถึงยัง?" ต้องพิมพ์ LINE บอกทุกครั้ง
2. Define
Problem Statement:
"ผู้ใช้ต้องการ (1) จำที่จอดรถได้ง่ายโดยไม่ต้องพึ่งความจำ (2) ได้รับแจ้งเตือนก่อนเวลาออก (3) แจ้งคนที่บ้านอัตโนมัติว่าถึงที่หมายแล้วจอดชั้นไหน — ทั้งหมดนี้ทำครั้งเดียวจบ"
3. Ideate
Key Insight: "ไหนๆ ก็ต้องกดบันทึกชั้นที่จอดอยู่แล้ว ทำไมไม่ให้มัน ทำทุกอย่างให้ ในคลิกเดียว?"

ไอเดีย: สร้าง Web App ฟอร์มง่ายๆ → กรอกสถานที่ + ชั้น + เวลาที่อยากให้เตือน → กดบันทึก → ระบบทำ 3 อย่างพร้อมกัน:
1) บันทึกข้อมูลลง Google Sheets
2) สร้าง Google Calendar event พร้อม popup reminder
3) ส่ง LINE แจ้งคนที่บ้าน "ถึงแล้ว จอดชั้น X"
4. Prototype & Test
สร้าง webapp ด้วย Framer (no-code) เป็นหน้าฟอร์ม → เชื่อมกับ Google Sheets → เขียน Google Apps Script ทำ automation

ปัญหาที่พบตอน test:
— Calendar event สร้างซ้ำเมื่อ LINE API ล้มเหลว (trigger retry ทำให้สร้าง event ซ้ำ)
— เวลาคลาดเคลื่อนเพราะ timezone ไม่ตรง
แก้: แยก try-catch Calendar กับ LINE, ใช้ status tracking (CAL_OK / LINE_FAIL / SENT), ใช้ RFC3339 + timezone

User Journey — กดครั้งเดียว ได้ 3 อย่าง

จากมุมมองผู้ใช้ ขั้นตอนทั้งหมดเสร็จใน 15 วินาที

1
เปิด tanplanet.info/carpark บนมือถือ
2
เลือก สถานที่จอด (คอนโด / ออฟฟิศ) + กรอก ชั้น
3
เลือก วัน/เวลา ที่อยากให้เตือน (เช่น 17:30)
4
กด Submit → ข้อมูลบันทึกลง Google Sheets
5
ระบบสร้าง Calendar event พร้อม popup เตือนก่อน 30 นาที Calendar
6
ส่ง LINE Flex Card แจ้งคนที่บ้าน → "ถึงแล้ว จอดชั้น B2" LINE
7
ถึงเวลา → มือถือ popup เตือน "รถจอดชั้น B2 ที่คอนโด" → ไม่ต้องจำเอง!

Technical Solution

Architecture

Framer (Frontend) → Google Sheets (DB) → Google Apps Script (Logic) → Calendar + LINE (Output)

Framer Google Sheets Google Apps Script Google Calendar API LINE Messaging API RFC3339 Timezone Status Tracking Error Recovery
Framer Web App Google Sheets GAS (every 1 min)
Google Calendar LINE Push API
  • Web App (Framer) — ฟอร์มกรอกข้อมูลง่ายๆ บนมือถือ ไม่ต้องลงแอป
  • Google Sheets เป็น database — ดู history ย้อนหลังได้ทุกเมื่อ
  • GAS Trigger ทุก 1 นาที — ตรวจ row ใหม่แล้วประมวลผลทันที
  • Google Calendar event + popup reminder 30 นาทีก่อนเวลาที่ตั้ง
  • LINE Flex Card ส่งแจ้งคนที่บ้านอัตโนมัติ
  • Status tracking (SENT / CAL_OK / LINE_FAIL) ป้องกัน Calendar สร้างซ้ำ
  • Retry mechanism — ถ้า LINE ล้มเหลว ระบบ retry อัตโนมัติทุกชั่วโมง

UI Preview

Web App ฟอร์มที่ใช้กรอก + LINE Flex Card ที่ส่งไปถึงคนที่บ้าน

Web App Form
tanplanet.info/carpark
วันนี้จอดรถที่ไหน?
ปกติ
คอนโด The Base
B2
07/02/2026
17:30
จอดใกล้ลิฟต์ฝั่งซ้าย
บันทึกที่จอดรถ
LINE Flex Card ที่ส่งไปถึง
🚗 ถึงคอนโด The Base แล้ว
Note จอดใกล้ลิฟต์ฝั่งซ้าย
Fl. B2
Time 07/02/2026 | 08:45

Creative Problem Solving

ปัญหาที่เจอระหว่างทำ และวิธีแก้ที่คิดขึ้นมา

สิ่งที่คิดเพิ่มจาก pain point เดิม

1 กด = 3 outputs (Calendar + LINE + Log)
แทนที่จะแค่จดชั้น ก็ให้ระบบทำทุกอย่างในคลิกเดียว — สร้าง Calendar เตือน + ส่ง LINE บอกคนที่บ้าน + log ข้อมูลใน Sheets ดู history ย้อนหลังได้
"ไหนๆ ก็ต้องกด ทำให้ได้มากกว่าแค่จำชั้น"
จุดเริ่มต้นแค่อยากจำชั้นจอดรถ แต่เมื่อคิดต่อก็เห็นว่า "ทุกวันต้องพิมพ์ LINE บอกคนที่บ้านอยู่แล้ว" เลยรวมเป็นฟีเจอร์เดียวกัน — กดบันทึกที่จอดรถ = แจ้งคนที่บ้านไปด้วยในตัว

Technical Challenges & Solutions

  • Calendar สร้างซ้ำ: เมื่อ LINE API ล้มเหลว trigger จะ retry → สร้าง event ซ้ำ → แก้โดยแยก try-catch + mark status "CAL_OK" ทันทีหลัง Calendar สำเร็จ
  • Timezone คลาดเคลื่อน: GAS ใช้ UTC แต่ผู้ใช้กรอกเวลาไทย → แก้ด้วย RFC3339 format พร้อม timeZone: "Asia/Bangkok"
  • LINE Retry: สร้าง retry mechanism แยก — มี trigger ทุกชั่วโมงหา row ที่ status = LINE_FAIL แล้วลองส่งใหม่
  • Sheet เป็น Date object: timeReminder จาก Sheet มาเป็น Date object ไม่ใช่ string → เขียน normalizeTimeHM_() รองรับทั้ง Date, "HH:mm", "HH:mm:ss"
Google Apps Script — Core Logic
// Main: ตรวจ row ใหม่ → สร้าง Calendar + ส่ง LINE
function syncParkingRows() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet()
    .getSheetByName("Sheet1");
  const rows = sheet.getRange(2, 1, lastRow - 1, 8).getValues();

  for (let i = 0; i < rows.length; i++) {
    const p = mapSheetRowToPayload_(rows[i]);

    // กันซ้ำ — ข้ามถ้าสร้าง Calendar แล้ว
    if (p.status === "SENT" || p.status === "CAL_OK") continue;

    // ===== 1. สร้าง Calendar =====
    try {
      createCalendarEventWithPopupReminder_(
        calendarId, title, start, end, location, description
      );
      calendarOk = true;
    } catch (e) { continue; }

    // ===== 2. ส่ง LINE (แยก try-catch) =====
    try {
      const flex = buildParkingFlexCard_(p);
      linePush_(to, [flex]);  // to = CENSORED
      lineOk = true;
    } catch (e) { lineErrorMsg = e.message; }

    // ===== 3. Mark Status ทันที =====
    sheet.getRange(rowNumber, 8)
      .setValue(lineOk ? "SENT" : `LINE_FAIL:${err}`);
  }
}
Google Apps Script — Calendar + Timezone Fix
// RFC3339 + timezone — กัน Calendar เวลาคลาดเคลื่อน
function createCalendarEventWithPopupReminder_(
  calendarId, title, start, end, location, description
) {
  const resource = {
    summary: title,
    location: location,
    start: {
      dateTime: toRfc3339_(start),   // "2026-02-07T17:30:00+07:00"
      timeZone: "Asia/Bangkok"
    },
    end: {
      dateTime: toRfc3339_(end),
      timeZone: "Asia/Bangkok"
    },
    reminders: {
      useDefault: false,
      overrides: [
        { method: "popup", minutes: 30 }
      ]
    }
  };

  Calendar.Events.insert(resource, calendarId);  // CALENDAR_ID CENSORED
}

// รองรับ Date / "HH:mm" / "HH:mm:ss" จาก Sheet
function normalizeTimeHM_(v) {
  if (v instanceof Date) {
    return { hh: v.getHours(), mm: v.getMinutes() };
  }
  const m = String(v).match(/^(\d{1,2}):(\d{2})/);
  if (m) return { hh: Number(m[1]), mm: Number(m[2]) };
  return null;
}

Data Model — Google Sheets

แต่ละ row คือการจอดรถ 1 ครั้ง ระบบอ่านแล้วประมวลผลอัตโนมัติ

Column Field Description Example
A Date Timestamp ที่กรอกฟอร์ม 2026-02-07 08:45
B timeReminder เวลาที่ต้องการให้เตือน 17:30
C parkingMap URL แผนที่ (optional)
D parkingFloor ชั้นที่จอด B2
E note โน้ตเพิ่มเติม จอดใกล้ลิฟต์ฝั่งซ้าย
F parkingLocation สถานที่จอด คอนโด
G exitDateReminder วันที่ให้เตือน 2026-02-07
H NoteType โหมดการบันทึก SENT / กรอกย้อนหลัง
I TimeUseTracking เวลาจริงที่ใช้ tracking (แยกจากเวลาเตือน) 22:32 / 09:12

Error Recovery Design

ออกแบบ status tracking เพื่อป้องกัน Calendar ซ้ำ + retry LINE อัตโนมัติ

1
Row ใหม่เข้ามา → status = (ว่าง)
2
Calendar สำเร็จ → status = CAL_OK (ถ้า LINE ยังไม่ส่ง)
3a
LINE สำเร็จ → status = SENT → จบ
3b
LINE ล้มเหลว → status = LINE_FAIL:error message
4
Retry trigger (ทุก 1 ชม.) หา LINE_FAIL → ลองส่ง LINE ใหม่ → ถ้าสำเร็จ → SENT

ผลลัพธ์

15s
เวลาที่ใช้บันทึก
(จากเดิมต้องพิมพ์ LINE)
3
outputs ต่อ 1 กด
(Sheets + Calendar + LINE)
0
ครั้งที่ลืมชั้นจอดรถ
(หลังใช้ระบบ)
100%
คนที่บ้านรับ LINE
ทุกครั้งที่บันทึก

🔀 Iterate: Dual Recording Mode

จาก "บันทึกได้แค่ตอนจอด" สู่ "กรอกย้อนหลังได้ + แยก Tracking Time ออกจาก Reminder Time"

🔍 ปัญหาที่เจอ

บางครั้งลืมกรอกตอนจอดรถ ต้องกรอกย้อนหลัง แต่ระบบเดิมต้องตั้งเวลาเตือนทุกครั้ง → เวลาที่เอาไป track ปนกับเวลาเตือน ข้อมูลไม่ตรง

Design Thinking: Iterate
Insight: "เวลาที่ใช้เตือน" กับ "เวลาที่ถึงจริง" คือคนละอย่าง — ต้องแยกออกจากกัน

Solution: เพิ่ม Dropdown "บันทึกแบบไหน?" แยก 2 โหมด + เพิ่ม column TimeUseTracking เก็บเวลาจริงที่ใช้ track แยกจากเวลาเตือน
1
เลือก "บันทึกแบบไหน?" → ปกติ / กรอกย้อนหลัง
2a
โหมดปกติ — กรอกวันที่ + เวลาเตือน → ระบบตั้ง schedule ส่ง LINE + Calendar
2b
กรอกย้อนหลัง — กรอกเวลาที่ลืมบันทึก → ระบบบันทึก tracking เท่านั้น (ไม่ส่ง LINE)
3
TimeUseTracking บันทึกเวลาจริงทั้ง 2 โหมด → Dashboard ดึงจาก column นี้เท่านั้น

💡 ผลลัพธ์ที่ได้

  • ข้อมูลไม่ปนกัน — เวลาที่ใช้เตือน (timeReminder) แยกจากเวลาจริง (TimeUseTracking)
  • กรอกย้อนหลังได้ — ไม่ต้อง skip การบันทึกเพราะลืมกรอกตอนจอด
  • Dashboard แม่นยำขึ้น — ใช้ TimeUseTracking เป็น source of truth ทุก chart/metric
  • ไม่ยิง LINE ผิดโหมด — โหมดกรอกย้อนหลังไม่สร้าง Calendar + ไม่ส่ง LINE

📊 Iterate: Analytics Dashboard

จาก "แค่บันทึกข้อมูล" สู่ "เข้าใจพฤติกรรมตัวเอง" ผ่าน data visualization

🔍 ทำไมต้องมี Dashboard?

หลังใช้งานไปสักพัก เริ่มสงสัยว่า "เราถึงที่ทำงานกี่โมงโดยเฉลี่ย?" "ถึงคอนโดกี่โมง?" — ข้อมูลมีอยู่ใน Google Sheets แล้ว แต่ไม่สามารถมองเห็น pattern ได้

Design Thinking: Observe → Insight
Observation: มีข้อมูลการจอดรถสะสมหลายเดือน แต่ดูแค่ตารางใน Sheets ไม่เห็นภาพรวม

Insight: "ถ้าเห็นเป็นกราฟ จะรู้ว่าเราถึงที่ทำงานเฉลี่ยกี่โมง ถึงคอนโดกี่โมง ช่วยวางแผนเวลาได้ดีขึ้น"

Solution: สร้าง Live Dashboard อ่านข้อมูลจาก Google Sheets แบบ real-time แสดงเป็นกราฟและ metrics
React + Vite Recharts Google Sheets CSV Auto Refresh Data Visualization
  • ⏰ เวลาที่บันทึกเฉลี่ย — แสดงว่าถึงคอนโดโดยเฉลี่ยกี่โมง ถึงที่ทำงานกี่โมง แยกตามสถานที่ พร้อม filter สัปดาห์นี้ / เดือนนี้ / ทั้งหมด
  • 📈 Trend เวลาที่บันทึก — กราฟแสดง 2 เส้น (คอนโด vs ที่ทำงาน) เฉพาะวันจันทร์-ศุกร์ ใช้ TimeUseTracking เป็น source of truth
  • 📍 สัดส่วนสถานที่จอด — pie chart แสดงว่าจอดที่ไหนบ่อยที่สุด
  • 🅿️ ชั้นที่จอดบ่อยสุด — bar chart แสดงว่าที่คอนโดจอดชั้นไหนบ่อยที่สุด
  • 📊 จำนวนบันทึกรายวัน — timeline การใช้งานระบบ วันที่ครบ 2 ครั้ง/วันแสดง ✓ เป็น visual indicator
  • 📱 Mobile Responsive — ซ่อนคอลัมน์ที่ไม่จำเป็นบนมือถือ location badge ไม่ตัดบรรทัด
  • 🔴 Live Data — ดึงข้อมูลจาก Google Sheets แบบ real-time ทุก 5 นาที
🚀 ดู Live Dashboard

💡 Insights ที่ได้จาก Dashboard

  • รู้ว่าเราถึงที่ทำงานเฉลี่ยกี่โมง — ช่วยวางแผนว่าควรออกจากบ้านกี่โมงถึงจะไม่สาย สามารถดูเป็นรายสัปดาห์/เดือนได้
  • เห็น pattern การจอดรถ — เช่น "ชั้น 3 จอดบ่อยสุดเพราะใกล้ลิฟต์" หรือ "ช่วงนี้จอดที่ทำงานบ่อยกว่าคอนโด"
  • ตรวจสอบความ consistent — Trend chart แสดงเฉพาะวันทำงาน (จ.-ศ.) เห็นชัดว่าเวลาถึงผันผวนแค่ไหน
  • Track ความสม่ำเสมอ — วันที่บันทึกครบ 2 ครั้ง (ไป-กลับ) แสดง ✓ เป็น visual goal
  • Data-driven decision — ตัดสินใจจากข้อมูลจริง ไม่ใช่จากความรู้สึก

🎨 Technical Implementation

React + Recharts สำหรับ Data Visualization
ใช้ React functional components + Recharts library สร้างกราฟ interactive (Line Chart, Pie Chart, Bar Chart) ที่ responsive ทุก device
Google Sheets เป็น "Free Database"
Publish Sheets เป็น CSV URL แล้วใช้ PapaCSV parse → ไม่ต้องมี backend server, ไม่เสียค่าใช้จ่าย, แก้ไขข้อมูลได้ง่าย
Auto Refresh ทุก 5 นาที
ใช้ setInterval ดึงข้อมูลใหม่ทุก 5 นาที → Dashboard แสดงข้อมูล real-time โดยไม่ต้อง refresh หน้าเว็บ
Light Theme + WCAG AAA Compliance
ออกแบบ light theme ที่อ่านง่าย พร้อม contrast ratio ที่ผ่านมาตรฐาน WCAG 2.1 AA/AAA สำหรับทุกคน
Period Filter (Week/Month) + Weekday Filter
เวลาเฉลี่ย filter ได้ตามสัปดาห์/เดือน auto reset ทุกสัปดาห์ Trend chart แสดงเฉพาะ จ.-ศ. ตัด noise จากวันหยุด

สิ่งที่ได้เรียนรู้

  • Design Thinking เริ่มจากตัวเองได้ — ปัญหาเล็กๆ ที่เจอทุกวัน (ลืมชั้นจอดรถ) พอคิดต่อกลายเป็น solution ที่ช่วยได้ทั้งตัวเองและคนที่บ้าน
  • "1 action = multiple outputs" — กดครั้งเดียว ได้ทั้งบันทึก + เตือน + แจ้งคนที่บ้าน ช่วยลดขั้นตอนที่ต้องทำซ้ำทุกวัน
  • Status Tracking สำคัญ — ถ้าไม่มี status tracking ระบบจะสร้าง Calendar ซ้ำเมื่อ LINE API ล้มเหลว ต้องคิดเรื่อง idempotency ตั้งแต่แรก
  • Error Recovery ต้องออกแบบ — แยก try-catch + retry mechanism ทำให้ระบบ resilient โดยไม่ต้องมานั่งดูแลทุกวัน
  • Timezone เป็นปัญหาคลาสสิก — GAS ใช้ UTC ภายใน แต่ผู้ใช้คิดเป็นเวลาไทย ต้อง explicit ทุก datetime operation
  • No-code + code ผสมกันได้ดี — ใช้ Framer ทำ frontend (เร็ว สวย) + GAS ทำ backend logic (ฟรี มี trigger) เหมาะกับ side project

Tech Stack

ผสม no-code (Framer) กับ code (GAS) เพื่อ ship ให้ไวที่สุด

Framer Google Apps Script Google Sheets Google Calendar API LINE Messaging API Flex Message RFC3339 / Timezone
Portfolio v1.3.0 · Dashboard v1.5.1
Aw mascot