[{"data":1,"prerenderedAt":2058},["ShallowReactive",2],{"portfolio":3},[4,286,750,1012,1686,1896],{"_path":5,"_dir":6,"_draft":7,"_partial":7,"_locale":8,"title":9,"description":8,"category":10,"client":11,"timeline":12,"role":13,"date":14,"url":15,"tags":16,"body":24,"_type":280,"_id":281,"_source":282,"_file":283,"_stem":284,"_extension":285},"\u002Fportfolio\u002Ffreshmarket-platform","portfolio",false,"","FreshMarket — แพลตฟอร์มตลาดสดออนไลน์","E-commerce","FreshMarket Co., Ltd.","10 สัปดาห์","Full-stack development, UI\u002FUX Design, DevOps","2024-08-15","https:\u002F\u002Ffreshmarket.example.com",[17,18,19,20,21,22,23],"Next.js","TypeScript","PostgreSQL","Prisma","2C2P","Tailwind CSS","Cloudflare",{"type":25,"children":26,"toc":272},"root",[27,35,41,46,51,71,82,94,100,112,117,240,254,260],{"type":28,"tag":29,"props":30,"children":32},"element","h2",{"id":31},"โจทย์ที่เราได้รับ",[33],{"type":34,"value":31},"text",{"type":28,"tag":36,"props":37,"children":38},"p",{},[39],{"type":34,"value":40},"FreshMarket ต้องการแพลตฟอร์มที่เชื่อมต่อเกษตรกรในชนบทกับผู้บริโภคในเมืองโดยตรง โดยไม่ผ่านคนกลาง เป้าหมายหลักคือลดค่าใช้จ่ายให้เกษตรกร และให้ผู้บริโภคได้สินค้าสดกว่าและราคาถูกกว่าในซูเปอร์มาร์เก็ตทั่วไป",{"type":28,"tag":36,"props":42,"children":43},{},[44],{"type":34,"value":45},"ความท้าทายหลักมีสองประการ: หนึ่ง — ระบบต้องรองรับสินค้าที่มีสต็อกแปรผันตามฤดูกาลและราคาที่เปลี่ยนได้ทุกวัน สอง — กลุ่มผู้ใช้ฝั่งเกษตรกรมีความคุ้นเคยกับสมาร์ทโฟนในระดับพื้นฐาน UI จึงต้องง่ายมาก",{"type":28,"tag":29,"props":47,"children":49},{"id":48},"วิธีที่เราแก้ปัญหา",[50],{"type":34,"value":48},{"type":28,"tag":36,"props":52,"children":53},{},[54,56,62,64,69],{"type":34,"value":55},"เราเลือก ",{"type":28,"tag":57,"props":58,"children":59},"strong",{},[60],{"type":34,"value":61},"Next.js 14",{"type":34,"value":63}," สำหรับ frontend เพราะต้องการ SSR ที่ดีเพื่อ SEO ในหน้า product listing และ ",{"type":28,"tag":57,"props":65,"children":66},{},[67],{"type":34,"value":68},"PostgreSQL + Prisma",{"type":34,"value":70}," สำหรับ database เพราะ schema ของ e-commerce มีความสัมพันธ์ซับซ้อนที่ relational database จัดการได้ดีกว่า",{"type":28,"tag":36,"props":72,"children":73},{},[74,76,80],{"type":34,"value":75},"สำหรับ payment gateway เราเลือก ",{"type":28,"tag":57,"props":77,"children":78},{},[79],{"type":34,"value":21},{"type":34,"value":81}," เพราะรองรับ PromptPay, True Money Wallet และบัตรเครดิต ซึ่งเป็น payment method หลักของกลุ่มเป้าหมาย",{"type":28,"tag":36,"props":83,"children":84},{},[85,87,92],{"type":34,"value":86},"ส่วน inventory system เราออกแบบให้เป็น ",{"type":28,"tag":57,"props":88,"children":89},{},[90],{"type":34,"value":91},"event-sourced",{"type":34,"value":93}," เพื่อให้ติดตาม stock movement ได้แม่นยำและมี audit trail สำหรับการคืนสินค้า",{"type":28,"tag":29,"props":95,"children":97},{"id":96},"สิ่งที่ทำให้-project-นี้น่าสนใจ",[98],{"type":34,"value":99},"สิ่งที่ทำให้ Project นี้น่าสนใจ",{"type":28,"tag":36,"props":101,"children":102},{},[103,105,110],{"type":34,"value":104},"การออกแบบ UI สำหรับสองกลุ่มผู้ใช้ที่แตกต่างกันมากเป็นความท้าทายที่น่าสนใจ เราทำ ",{"type":28,"tag":57,"props":106,"children":107},{},[108],{"type":34,"value":109},"separate app shells",{"type":34,"value":111}," — หน้า seller dashboard ใช้ navigation pattern ที่เรียบง่ายมาก ฟอนต์ใหญ่ ปุ่มใหญ่ เพื่อให้เกษตรกรที่ใช้มือถือกลางแจ้งกดได้ง่าย ขณะที่ buyer experience เน้น discovery และ browsing ที่ลื่นไหล",{"type":28,"tag":29,"props":113,"children":115},{"id":114},"ผลลัพธ์",[116],{"type":34,"value":114},{"type":28,"tag":118,"props":119,"children":120},"table",{},[121,145],{"type":28,"tag":122,"props":123,"children":124},"thead",{},[125],{"type":28,"tag":126,"props":127,"children":128},"tr",{},[129,135,140],{"type":28,"tag":130,"props":131,"children":132},"th",{},[133],{"type":34,"value":134},"Metric",{"type":28,"tag":130,"props":136,"children":137},{},[138],{"type":34,"value":139},"ก่อน",{"type":28,"tag":130,"props":141,"children":142},{},[143],{"type":34,"value":144},"หลัง 6 เดือน",{"type":28,"tag":146,"props":147,"children":148},"tbody",{},[149,168,186,204,222],{"type":28,"tag":126,"props":150,"children":151},{},[152,158,163],{"type":28,"tag":153,"props":154,"children":155},"td",{},[156],{"type":34,"value":157},"GMV รายเดือน",{"type":28,"tag":153,"props":159,"children":160},{},[161],{"type":34,"value":162},"450,000 บาท",{"type":28,"tag":153,"props":164,"children":165},{},[166],{"type":34,"value":167},"1,350,000 บาท",{"type":28,"tag":126,"props":169,"children":170},{},[171,176,181],{"type":28,"tag":153,"props":172,"children":173},{},[174],{"type":34,"value":175},"เกษตรกรที่ลงทะเบียน",{"type":28,"tag":153,"props":177,"children":178},{},[179],{"type":34,"value":180},"23 ราย",{"type":28,"tag":153,"props":182,"children":183},{},[184],{"type":34,"value":185},"187 ราย",{"type":28,"tag":126,"props":187,"children":188},{},[189,194,199],{"type":28,"tag":153,"props":190,"children":191},{},[192],{"type":34,"value":193},"Conversion rate",{"type":28,"tag":153,"props":195,"children":196},{},[197],{"type":34,"value":198},"1.8%",{"type":28,"tag":153,"props":200,"children":201},{},[202],{"type":34,"value":203},"4.2%",{"type":28,"tag":126,"props":205,"children":206},{},[207,212,217],{"type":28,"tag":153,"props":208,"children":209},{},[210],{"type":34,"value":211},"Lighthouse Performance",{"type":28,"tag":153,"props":213,"children":214},{},[215],{"type":34,"value":216},"—",{"type":28,"tag":153,"props":218,"children":219},{},[220],{"type":34,"value":221},"97",{"type":28,"tag":126,"props":223,"children":224},{},[225,230,235],{"type":28,"tag":153,"props":226,"children":227},{},[228],{"type":34,"value":229},"Average order value",{"type":28,"tag":153,"props":231,"children":232},{},[233],{"type":34,"value":234},"285 บาท",{"type":28,"tag":153,"props":236,"children":237},{},[238],{"type":34,"value":239},"410 บาท",{"type":28,"tag":241,"props":242,"children":243},"blockquote",{},[244,249],{"type":28,"tag":36,"props":245,"children":246},{},[247],{"type":34,"value":248},"\"Khantuna เข้าใจ business ของเราจริงๆ ไม่ใช่แค่ทำตาม spec แต่ช่วยคิด solution ที่ดีกว่าที่เราขอไปด้วย\"",{"type":28,"tag":36,"props":250,"children":251},{},[252],{"type":34,"value":253},"— คุณสมชาย วงษ์เกษตร, CEO FreshMarket",{"type":28,"tag":29,"props":255,"children":257},{"id":256},"lessons-learned",[258],{"type":34,"value":259},"Lessons Learned",{"type":28,"tag":36,"props":261,"children":262},{},[263,265,270],{"type":34,"value":264},"โปรเจกต์นี้สอนให้เราเห็นว่า ",{"type":28,"tag":57,"props":266,"children":267},{},[268],{"type":34,"value":269},"user research ที่ดีสามารถเปลี่ยน solution ทั้งหมดได้",{"type":34,"value":271}," เราเคย plan ว่าจะทำ mobile app แต่หลังจากสัมภาษณ์เกษตรกร 15 ราย พบว่าพวกเขาสะดวกใช้ LINE มากกว่าแอปใหม่ เราจึงเพิ่ม LINE Notify integration เพื่อแจ้งเตือนออเดอร์ใหม่ ซึ่งลด onboarding friction ลงได้มาก",{"title":8,"searchDepth":273,"depth":273,"links":274},2,[275,276,277,278,279],{"id":31,"depth":273,"text":31},{"id":48,"depth":273,"text":48},{"id":96,"depth":273,"text":99},{"id":114,"depth":273,"text":114},{"id":256,"depth":273,"text":259},"markdown","content:portfolio:freshmarket-platform.md","content","portfolio\u002Ffreshmarket-platform.md","portfolio\u002Ffreshmarket-platform","md",{"_path":287,"_dir":6,"_draft":7,"_partial":7,"_locale":8,"title":288,"description":8,"category":289,"client":290,"timeline":291,"role":292,"date":293,"tags":294,"body":299,"_type":280,"_id":747,"_source":282,"_file":748,"_stem":749,"_extension":285},"\u002Fportfolio\u002Fmedsync-dashboard","MedSync — ระบบจัดการคลินิกและนัดหมายผู้ป่วย","SaaS","MedSync Thailand","14 สัปดาห์","Full-stack development, System Architecture, UI\u002FUX","2024-06-20",[295,296,18,297,19,22,298],"Vue 3","Nuxt 3","Supabase","Pinia",{"type":25,"children":300,"toc":741},[301,306,311,316,327,347,357,375,381,386,405,662,666,722,735],{"type":28,"tag":29,"props":302,"children":304},{"id":303},"ภาพรวมโปรเจกต์",[305],{"type":34,"value":303},{"type":28,"tag":36,"props":307,"children":308},{},[309],{"type":34,"value":310},"MedSync เป็น SaaS platform สำหรับคลินิกทั่วไปและคลินิกเฉพาะทางขนาดเล็กถึงกลาง เป้าหมายหลักคือลดภาระงานธุรการ เช่น การจัดตารางนัด การบันทึกประวัติผู้ป่วย และการออกเอกสารทางการแพทย์ ที่ยังทำด้วย Excel และกระดาษในคลินิกส่วนใหญ่",{"type":28,"tag":29,"props":312,"children":314},{"id":313},"สถาปัตยกรรมที่เราเลือก",[315],{"type":34,"value":313},{"type":28,"tag":36,"props":317,"children":318},{},[319,320,325],{"type":34,"value":55},{"type":28,"tag":57,"props":321,"children":322},{},[323],{"type":34,"value":324},"Nuxt 3 + Vue 3",{"type":34,"value":326}," เพราะ:",{"type":28,"tag":328,"props":329,"children":330},"ul",{},[331,337,342],{"type":28,"tag":332,"props":333,"children":334},"li",{},[335],{"type":34,"value":336},"Composition API ทำให้ reuse logic ระหว่าง components ได้ง่าย",{"type":28,"tag":332,"props":338,"children":339},{},[340],{"type":34,"value":341},"Built-in SSR สำหรับหน้า public (landing page, pricing)",{"type":28,"tag":332,"props":343,"children":344},{},[345],{"type":34,"value":346},"TypeScript support ดีเยี่ยม ซึ่งสำคัญมากสำหรับ healthcare data",{"type":28,"tag":36,"props":348,"children":349},{},[350,352,356],{"type":34,"value":351},"สำหรับ backend เลือก ",{"type":28,"tag":57,"props":353,"children":354},{},[355],{"type":34,"value":297},{"type":34,"value":326},{"type":28,"tag":328,"props":358,"children":359},{},[360,365,370],{"type":28,"tag":332,"props":361,"children":362},{},[363],{"type":34,"value":364},"Row Level Security ทำให้ isolate ข้อมูลระหว่างคลินิกได้ง่าย",{"type":28,"tag":332,"props":366,"children":367},{},[368],{"type":34,"value":369},"Realtime subscriptions สำหรับ live appointment board",{"type":28,"tag":332,"props":371,"children":372},{},[373],{"type":34,"value":374},"Auth ที่ครบ พร้อม JWT ที่ integrate กับ Vue ได้ตรงไปตรงมา",{"type":28,"tag":29,"props":376,"children":378},{"id":377},"ความท้าทายหลัก-realtime-appointment-board",[379],{"type":34,"value":380},"ความท้าทายหลัก: Realtime Appointment Board",{"type":28,"tag":36,"props":382,"children":383},{},[384],{"type":34,"value":385},"ฟีเจอร์ที่ยากที่สุดคือ appointment board ที่ต้องอัปเดต realtime เมื่อนัดถูกสร้าง เลื่อน หรือยกเลิก โดยไม่กระทบ performance ของหน้าอื่น",{"type":28,"tag":36,"props":387,"children":388},{},[389,391,396,398,403],{"type":34,"value":390},"เราใช้ ",{"type":28,"tag":57,"props":392,"children":393},{},[394],{"type":34,"value":395},"Supabase Realtime Channels",{"type":34,"value":397}," ร่วมกับ ",{"type":28,"tag":57,"props":399,"children":400},{},[401],{"type":34,"value":402},"Pinia store",{"type":34,"value":404}," ออกแบบให้ events จาก websocket ไป mutate store โดยตรง โดยไม่ต้อง refetch ข้อมูลทั้งหมด",{"type":28,"tag":406,"props":407,"children":411},"pre",{"className":408,"code":409,"language":410,"meta":8,"style":8},"language-typescript shiki shiki-themes github-light github-dark","\u002F\u002F composables\u002FuseAppointments.ts\nconst channel = supabase.channel('appointments')\n  .on('postgres_changes', {\n    event: '*',\n    schema: 'public',\n    table: 'appointments',\n    filter: `clinic_id=eq.${clinicId}`,\n  }, (payload) => {\n    appointmentStore.handleRealtimeEvent(payload)\n  })\n  .subscribe()\n","typescript",[412],{"type":28,"tag":413,"props":414,"children":415},"code",{"__ignoreMap":8},[416,428,476,504,523,541,558,586,616,635,644],{"type":28,"tag":417,"props":418,"children":421},"span",{"class":419,"line":420},"line",1,[422],{"type":28,"tag":417,"props":423,"children":425},{"style":424},"--shiki-default:#6A737D;--shiki-dark:#6A737D",[426],{"type":34,"value":427},"\u002F\u002F composables\u002FuseAppointments.ts\n",{"type":28,"tag":417,"props":429,"children":430},{"class":419,"line":273},[431,437,443,448,454,460,465,471],{"type":28,"tag":417,"props":432,"children":434},{"style":433},"--shiki-default:#D73A49;--shiki-dark:#F97583",[435],{"type":34,"value":436},"const",{"type":28,"tag":417,"props":438,"children":440},{"style":439},"--shiki-default:#005CC5;--shiki-dark:#79B8FF",[441],{"type":34,"value":442}," channel",{"type":28,"tag":417,"props":444,"children":445},{"style":433},[446],{"type":34,"value":447}," =",{"type":28,"tag":417,"props":449,"children":451},{"style":450},"--shiki-default:#24292E;--shiki-dark:#E1E4E8",[452],{"type":34,"value":453}," supabase.",{"type":28,"tag":417,"props":455,"children":457},{"style":456},"--shiki-default:#6F42C1;--shiki-dark:#B392F0",[458],{"type":34,"value":459},"channel",{"type":28,"tag":417,"props":461,"children":462},{"style":450},[463],{"type":34,"value":464},"(",{"type":28,"tag":417,"props":466,"children":468},{"style":467},"--shiki-default:#032F62;--shiki-dark:#9ECBFF",[469],{"type":34,"value":470},"'appointments'",{"type":28,"tag":417,"props":472,"children":473},{"style":450},[474],{"type":34,"value":475},")\n",{"type":28,"tag":417,"props":477,"children":479},{"class":419,"line":478},3,[480,485,490,494,499],{"type":28,"tag":417,"props":481,"children":482},{"style":450},[483],{"type":34,"value":484},"  .",{"type":28,"tag":417,"props":486,"children":487},{"style":456},[488],{"type":34,"value":489},"on",{"type":28,"tag":417,"props":491,"children":492},{"style":450},[493],{"type":34,"value":464},{"type":28,"tag":417,"props":495,"children":496},{"style":467},[497],{"type":34,"value":498},"'postgres_changes'",{"type":28,"tag":417,"props":500,"children":501},{"style":450},[502],{"type":34,"value":503},", {\n",{"type":28,"tag":417,"props":505,"children":507},{"class":419,"line":506},4,[508,513,518],{"type":28,"tag":417,"props":509,"children":510},{"style":450},[511],{"type":34,"value":512},"    event: ",{"type":28,"tag":417,"props":514,"children":515},{"style":467},[516],{"type":34,"value":517},"'*'",{"type":28,"tag":417,"props":519,"children":520},{"style":450},[521],{"type":34,"value":522},",\n",{"type":28,"tag":417,"props":524,"children":526},{"class":419,"line":525},5,[527,532,537],{"type":28,"tag":417,"props":528,"children":529},{"style":450},[530],{"type":34,"value":531},"    schema: ",{"type":28,"tag":417,"props":533,"children":534},{"style":467},[535],{"type":34,"value":536},"'public'",{"type":28,"tag":417,"props":538,"children":539},{"style":450},[540],{"type":34,"value":522},{"type":28,"tag":417,"props":542,"children":544},{"class":419,"line":543},6,[545,550,554],{"type":28,"tag":417,"props":546,"children":547},{"style":450},[548],{"type":34,"value":549},"    table: ",{"type":28,"tag":417,"props":551,"children":552},{"style":467},[553],{"type":34,"value":470},{"type":28,"tag":417,"props":555,"children":556},{"style":450},[557],{"type":34,"value":522},{"type":28,"tag":417,"props":559,"children":561},{"class":419,"line":560},7,[562,567,572,577,582],{"type":28,"tag":417,"props":563,"children":564},{"style":450},[565],{"type":34,"value":566},"    filter: ",{"type":28,"tag":417,"props":568,"children":569},{"style":467},[570],{"type":34,"value":571},"`clinic_id=eq.${",{"type":28,"tag":417,"props":573,"children":574},{"style":450},[575],{"type":34,"value":576},"clinicId",{"type":28,"tag":417,"props":578,"children":579},{"style":467},[580],{"type":34,"value":581},"}`",{"type":28,"tag":417,"props":583,"children":584},{"style":450},[585],{"type":34,"value":522},{"type":28,"tag":417,"props":587,"children":589},{"class":419,"line":588},8,[590,595,601,606,611],{"type":28,"tag":417,"props":591,"children":592},{"style":450},[593],{"type":34,"value":594},"  }, (",{"type":28,"tag":417,"props":596,"children":598},{"style":597},"--shiki-default:#E36209;--shiki-dark:#FFAB70",[599],{"type":34,"value":600},"payload",{"type":28,"tag":417,"props":602,"children":603},{"style":450},[604],{"type":34,"value":605},") ",{"type":28,"tag":417,"props":607,"children":608},{"style":433},[609],{"type":34,"value":610},"=>",{"type":28,"tag":417,"props":612,"children":613},{"style":450},[614],{"type":34,"value":615}," {\n",{"type":28,"tag":417,"props":617,"children":619},{"class":419,"line":618},9,[620,625,630],{"type":28,"tag":417,"props":621,"children":622},{"style":450},[623],{"type":34,"value":624},"    appointmentStore.",{"type":28,"tag":417,"props":626,"children":627},{"style":456},[628],{"type":34,"value":629},"handleRealtimeEvent",{"type":28,"tag":417,"props":631,"children":632},{"style":450},[633],{"type":34,"value":634},"(payload)\n",{"type":28,"tag":417,"props":636,"children":638},{"class":419,"line":637},10,[639],{"type":28,"tag":417,"props":640,"children":641},{"style":450},[642],{"type":34,"value":643},"  })\n",{"type":28,"tag":417,"props":645,"children":647},{"class":419,"line":646},11,[648,652,657],{"type":28,"tag":417,"props":649,"children":650},{"style":450},[651],{"type":34,"value":484},{"type":28,"tag":417,"props":653,"children":654},{"style":456},[655],{"type":34,"value":656},"subscribe",{"type":28,"tag":417,"props":658,"children":659},{"style":450},[660],{"type":34,"value":661},"()\n",{"type":28,"tag":29,"props":663,"children":664},{"id":114},[665],{"type":34,"value":114},{"type":28,"tag":328,"props":667,"children":668},{},[669,681,693,705],{"type":28,"tag":332,"props":670,"children":671},{},[672,674,679],{"type":34,"value":673},"เวลาที่ใช้ในการจัดตารางนัดลดจาก ",{"type":28,"tag":57,"props":675,"children":676},{},[677],{"type":34,"value":678},"12 นาที → 2 นาที",{"type":34,"value":680}," ต่อผู้ป่วย",{"type":28,"tag":332,"props":682,"children":683},{},[684,686,691],{"type":34,"value":685},"Error rate ในการออกใบเสร็จลดลง ",{"type":28,"tag":57,"props":687,"children":688},{},[689],{"type":34,"value":690},"95%",{"type":34,"value":692}," (จาก manual entry)",{"type":28,"tag":332,"props":694,"children":695},{},[696,698,703],{"type":34,"value":697},"Staff satisfaction score ",{"type":28,"tag":57,"props":699,"children":700},{},[701],{"type":34,"value":702},"4.6\u002F5",{"type":34,"value":704}," (จาก 8 คลินิก pilot)",{"type":28,"tag":332,"props":706,"children":707},{},[708,710,715,717],{"type":34,"value":709},"Lighthouse score: Performance ",{"type":28,"tag":57,"props":711,"children":712},{},[713],{"type":34,"value":714},"94",{"type":34,"value":716},", Accessibility ",{"type":28,"tag":57,"props":718,"children":719},{},[720],{"type":34,"value":721},"98",{"type":28,"tag":241,"props":723,"children":724},{},[725,730],{"type":28,"tag":36,"props":726,"children":727},{},[728],{"type":34,"value":729},"\"ก่อนหน้านี้ reception ต้องเปิด Excel หลายไฟล์พร้อมกัน ตอนนี้ทุกอย่างอยู่ในหน้าเดียว\"",{"type":28,"tag":36,"props":731,"children":732},{},[733],{"type":34,"value":734},"— คุณพรรณี สุขสวัสดิ์, ผู้จัดการคลินิก",{"type":28,"tag":736,"props":737,"children":738},"style",{},[739],{"type":34,"value":740},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":8,"searchDepth":273,"depth":273,"links":742},[743,744,745,746],{"id":303,"depth":273,"text":303},{"id":313,"depth":273,"text":313},{"id":377,"depth":273,"text":380},{"id":114,"depth":273,"text":114},"content:portfolio:medsync-dashboard.md","portfolio\u002Fmedsync-dashboard.md","portfolio\u002Fmedsync-dashboard",{"_path":751,"_dir":6,"_draft":7,"_partial":7,"_locale":8,"title":752,"description":8,"category":753,"client":754,"timeline":755,"role":756,"date":757,"tags":758,"body":766,"_type":280,"_id":1009,"_source":282,"_file":1010,"_stem":1011,"_extension":285},"\u002Fportfolio\u002Frunclub-app","RunClub — แอปสำหรับชมรมวิ่งและ community นักวิ่งไทย","Mobile","RunClub Thailand","12 สัปดาห์","Mobile Development, UI\u002FUX, Backend API","2024-04-10",[759,760,761,762,763,764,765],"Flutter","Dart","Firebase","Google Maps","Node.js","Express","MongoDB",{"type":25,"children":767,"toc":998},[768,773,778,784,791,804,810,815,821,826,832,837,879,883,981,986],{"type":28,"tag":29,"props":769,"children":771},{"id":770},"ที่มาของโปรเจกต์",[772],{"type":34,"value":770},{"type":28,"tag":36,"props":774,"children":775},{},[776],{"type":34,"value":777},"RunClub เริ่มจาก LINE group นักวิ่งที่มีสมาชิก 3,000+ คน แต่การจัดกิจกรรม ติดตามผล และสร้าง community ผ่าน LINE เริ่มไม่เพียงพอ ลูกค้าต้องการแอปที่ทำให้ running experience เชื่อมต่อกับ community ได้จริง",{"type":28,"tag":29,"props":779,"children":781},{"id":780},"features-หลัก",[782],{"type":34,"value":783},"Features หลัก",{"type":28,"tag":785,"props":786,"children":788},"h3",{"id":787},"gps-activity-tracking",[789],{"type":34,"value":790},"GPS Activity Tracking",{"type":28,"tag":36,"props":792,"children":793},{},[794,796,802],{"type":34,"value":795},"ใช้ ",{"type":28,"tag":413,"props":797,"children":799},{"className":798},[],[800],{"type":34,"value":801},"geolocator",{"type":34,"value":803}," package ร่วมกับ Kalman filter เพื่อลด GPS noise ในพื้นที่ที่มีตึกสูง — ปัญหาหลักของนักวิ่งในกรุงเทพฯ",{"type":28,"tag":785,"props":805,"children":807},{"id":806},"challenge-system",[808],{"type":34,"value":809},"Challenge System",{"type":28,"tag":36,"props":811,"children":812},{},[813],{"type":34,"value":814},"ระบบ challenge ที่ผู้ใช้สร้างเองได้ เช่น \"วิ่ง 100 กม. ใน 30 วัน\" พร้อม leaderboard realtime และ badge rewards",{"type":28,"tag":785,"props":816,"children":818},{"id":817},"social-feed",[819],{"type":34,"value":820},"Social Feed",{"type":28,"tag":36,"props":822,"children":823},{},[824],{"type":34,"value":825},"Feed ที่แสดง activity ของ friends พร้อม kudos system (คล้าย Strava แต่เน้น community ท้องถิ่น)",{"type":28,"tag":29,"props":827,"children":829},{"id":828},"ความท้าทายด้าน-performance",[830],{"type":34,"value":831},"ความท้าทายด้าน Performance",{"type":28,"tag":36,"props":833,"children":834},{},[835],{"type":34,"value":836},"การแสดงผล map route หลังวิ่งเสร็จเป็นจุดที่ต้องปรับแต่ง performance มากที่สุด เพราะ polyline จาก GPS อาจมี 10,000+ จุด เราแก้ด้วย:",{"type":28,"tag":838,"props":839,"children":840},"ol",{},[841,851,861],{"type":28,"tag":332,"props":842,"children":843},{},[844,849],{"type":28,"tag":57,"props":845,"children":846},{},[847],{"type":34,"value":848},"Douglas-Peucker algorithm",{"type":34,"value":850}," เพื่อ simplify route ก่อน render",{"type":28,"tag":332,"props":852,"children":853},{},[854,859],{"type":28,"tag":57,"props":855,"children":856},{},[857],{"type":34,"value":858},"Lazy loading",{"type":34,"value":860}," สำหรับ activity history ด้วย pagination",{"type":28,"tag":332,"props":862,"children":863},{},[864,869,871,877],{"type":28,"tag":57,"props":865,"children":866},{},[867],{"type":34,"value":868},"Image caching",{"type":34,"value":870}," ด้วย ",{"type":28,"tag":413,"props":872,"children":874},{"className":873},[],[875],{"type":34,"value":876},"cached_network_image",{"type":34,"value":878}," สำหรับ profile pictures",{"type":28,"tag":29,"props":880,"children":881},{"id":114},[882],{"type":34,"value":114},{"type":28,"tag":118,"props":884,"children":885},{},[886,906],{"type":28,"tag":122,"props":887,"children":888},{},[889],{"type":28,"tag":126,"props":890,"children":891},{},[892,896,901],{"type":28,"tag":130,"props":893,"children":894},{},[895],{"type":34,"value":134},{"type":28,"tag":130,"props":897,"children":898},{},[899],{"type":34,"value":900},"เป้าหมาย",{"type":28,"tag":130,"props":902,"children":903},{},[904],{"type":34,"value":905},"ผล 3 เดือน",{"type":28,"tag":146,"props":907,"children":908},{},[909,927,945,963],{"type":28,"tag":126,"props":910,"children":911},{},[912,917,922],{"type":28,"tag":153,"props":913,"children":914},{},[915],{"type":34,"value":916},"Daily Active Users",{"type":28,"tag":153,"props":918,"children":919},{},[920],{"type":34,"value":921},"2,000",{"type":28,"tag":153,"props":923,"children":924},{},[925],{"type":34,"value":926},"4,200",{"type":28,"tag":126,"props":928,"children":929},{},[930,935,940],{"type":28,"tag":153,"props":931,"children":932},{},[933],{"type":34,"value":934},"Session duration",{"type":28,"tag":153,"props":936,"children":937},{},[938],{"type":34,"value":939},"8 นาที",{"type":28,"tag":153,"props":941,"children":942},{},[943],{"type":34,"value":944},"14 นาที",{"type":28,"tag":126,"props":946,"children":947},{},[948,953,958],{"type":28,"tag":153,"props":949,"children":950},{},[951],{"type":34,"value":952},"App Store Rating",{"type":28,"tag":153,"props":954,"children":955},{},[956],{"type":34,"value":957},"4.0",{"type":28,"tag":153,"props":959,"children":960},{},[961],{"type":34,"value":962},"4.7 ⭐",{"type":28,"tag":126,"props":964,"children":965},{},[966,971,976],{"type":28,"tag":153,"props":967,"children":968},{},[969],{"type":34,"value":970},"Crash-free rate",{"type":28,"tag":153,"props":972,"children":973},{},[974],{"type":34,"value":975},"99%",{"type":28,"tag":153,"props":977,"children":978},{},[979],{"type":34,"value":980},"99.6%",{"type":28,"tag":29,"props":982,"children":984},{"id":983},"สิ่งที่ภูมิใจที่สุด",[985],{"type":34,"value":983},{"type":28,"tag":36,"props":987,"children":988},{},[989,991,996],{"type":34,"value":990},"การออกแบบ ",{"type":28,"tag":57,"props":992,"children":993},{},[994],{"type":34,"value":995},"onboarding flow",{"type":34,"value":997}," ที่ทำให้ผู้ใช้ใหม่เห็น \"aha moment\" ได้เร็ว — เราออกแบบให้ผู้ใช้เห็น community ของ sub-district ตัวเองภายใน 30 วินาทีหลัง sign up โดยไม่ต้องค้นหาเอง ส่งผลให้ Day-7 retention อยู่ที่ 58% ซึ่งสูงกว่า benchmark ของ fitness app ทั่วไป",{"title":8,"searchDepth":273,"depth":273,"links":999},[1000,1001,1006,1007,1008],{"id":770,"depth":273,"text":770},{"id":780,"depth":273,"text":783,"children":1002},[1003,1004,1005],{"id":787,"depth":478,"text":790},{"id":806,"depth":478,"text":809},{"id":817,"depth":478,"text":820},{"id":828,"depth":273,"text":831},{"id":114,"depth":273,"text":114},{"id":983,"depth":273,"text":983},"content:portfolio:runclub-app.md","portfolio\u002Frunclub-app.md","portfolio\u002Frunclub-app",{"_path":1013,"_dir":6,"_draft":7,"_partial":7,"_locale":8,"title":1014,"description":8,"category":1015,"client":1016,"timeline":1017,"role":1018,"date":1019,"url":1020,"tags":1021,"body":1025,"_type":280,"_id":1683,"_source":282,"_file":1684,"_stem":1685,"_extension":285},"\u002Fportfolio\u002Fbaan-design-studio","Baan Design Studio — Portfolio Website สำหรับสตูดิโอออกแบบ","Web","Baan Design Studio","4 สัปดาห์","Frontend Development, Animation, Performance Optimization","2024-02-28","https:\u002F\u002Fbaan-design.example.com",[296,1022,1023,22,1024,18],"GSAP","Lenis","Cloudflare Pages",{"type":25,"children":1026,"toc":1673},[1027,1033,1038,1043,1049,1054,1060,1077,1320,1326,1331,1479,1485,1490,1539,1543,1656,1669],{"type":28,"tag":29,"props":1028,"children":1030},{"id":1029},"โจทย์-เว็บที่ต้องขายงานได้ด้วยตัวเอง",[1031],{"type":34,"value":1032},"โจทย์: เว็บที่ต้องขายงานได้ด้วยตัวเอง",{"type":28,"tag":36,"props":1034,"children":1035},{},[1036],{"type":34,"value":1037},"Baan Design Studio เป็นสตูดิโอออกแบบภายในที่มีผลงานโดดเด่น แต่เว็บไซต์เดิมทำให้ผลงานดูด้อยค่า พวกเขาต้องการเว็บที่ \"สวยพอๆ กับงานที่เราทำ\" และต้องโหลดเร็วพอที่จะไม่ทำให้ลูกค้าหนีก่อนเห็นงาน",{"type":28,"tag":29,"props":1039,"children":1041},{"id":1040},"สิ่งที่เราทำ",[1042],{"type":34,"value":1040},{"type":28,"tag":785,"props":1044,"children":1046},{"id":1045},"animation-first-design-process",[1047],{"type":34,"value":1048},"Animation-first Design Process",{"type":28,"tag":36,"props":1050,"children":1051},{},[1052],{"type":34,"value":1053},"เราเริ่มจากการ prototype animation ใน CodePen ก่อน แล้วค่อย implement จริง เพราะ animation ที่ดีต้องออกแบบมาพร้อมกับ layout ไม่ใช่ add-on ทีหลัง",{"type":28,"tag":785,"props":1055,"children":1057},{"id":1056},"horizontal-scroll-gallery",[1058],{"type":34,"value":1059},"Horizontal Scroll Gallery",{"type":28,"tag":36,"props":1061,"children":1062},{},[1063,1064,1069,1071,1075],{"type":34,"value":795},{"type":28,"tag":57,"props":1065,"children":1066},{},[1067],{"type":34,"value":1068},"GSAP ScrollTrigger",{"type":34,"value":1070}," + ",{"type":28,"tag":57,"props":1072,"children":1073},{},[1074],{"type":34,"value":1023},{"type":34,"value":1076}," ทำ horizontal pinned section ที่ scroll ตาม vertical ได้ลื่นไหล:",{"type":28,"tag":406,"props":1078,"children":1082},{"className":1079,"code":1080,"language":1081,"meta":8,"style":8},"language-javascript shiki shiki-themes github-light github-dark","gsap.to('.gallery-track', {\n  x: () => -(gallery.scrollWidth - window.innerWidth),\n  ease: 'none',\n  scrollTrigger: {\n    trigger: '.gallery-section',\n    start: 'top top',\n    end: () => `+=${gallery.scrollWidth - window.innerWidth}`,\n    pin: true,\n    scrub: 1,\n  },\n})\n","javascript",[1083],{"type":28,"tag":413,"props":1084,"children":1085},{"__ignoreMap":8},[1086,1112,1149,1166,1174,1191,1208,1270,1287,1304,1312],{"type":28,"tag":417,"props":1087,"children":1088},{"class":419,"line":420},[1089,1094,1099,1103,1108],{"type":28,"tag":417,"props":1090,"children":1091},{"style":450},[1092],{"type":34,"value":1093},"gsap.",{"type":28,"tag":417,"props":1095,"children":1096},{"style":456},[1097],{"type":34,"value":1098},"to",{"type":28,"tag":417,"props":1100,"children":1101},{"style":450},[1102],{"type":34,"value":464},{"type":28,"tag":417,"props":1104,"children":1105},{"style":467},[1106],{"type":34,"value":1107},"'.gallery-track'",{"type":28,"tag":417,"props":1109,"children":1110},{"style":450},[1111],{"type":34,"value":503},{"type":28,"tag":417,"props":1113,"children":1114},{"class":419,"line":273},[1115,1120,1125,1129,1134,1139,1144],{"type":28,"tag":417,"props":1116,"children":1117},{"style":456},[1118],{"type":34,"value":1119},"  x",{"type":28,"tag":417,"props":1121,"children":1122},{"style":450},[1123],{"type":34,"value":1124},": () ",{"type":28,"tag":417,"props":1126,"children":1127},{"style":433},[1128],{"type":34,"value":610},{"type":28,"tag":417,"props":1130,"children":1131},{"style":433},[1132],{"type":34,"value":1133}," -",{"type":28,"tag":417,"props":1135,"children":1136},{"style":450},[1137],{"type":34,"value":1138},"(gallery.scrollWidth ",{"type":28,"tag":417,"props":1140,"children":1141},{"style":433},[1142],{"type":34,"value":1143},"-",{"type":28,"tag":417,"props":1145,"children":1146},{"style":450},[1147],{"type":34,"value":1148}," window.innerWidth),\n",{"type":28,"tag":417,"props":1150,"children":1151},{"class":419,"line":478},[1152,1157,1162],{"type":28,"tag":417,"props":1153,"children":1154},{"style":450},[1155],{"type":34,"value":1156},"  ease: ",{"type":28,"tag":417,"props":1158,"children":1159},{"style":467},[1160],{"type":34,"value":1161},"'none'",{"type":28,"tag":417,"props":1163,"children":1164},{"style":450},[1165],{"type":34,"value":522},{"type":28,"tag":417,"props":1167,"children":1168},{"class":419,"line":506},[1169],{"type":28,"tag":417,"props":1170,"children":1171},{"style":450},[1172],{"type":34,"value":1173},"  scrollTrigger: {\n",{"type":28,"tag":417,"props":1175,"children":1176},{"class":419,"line":525},[1177,1182,1187],{"type":28,"tag":417,"props":1178,"children":1179},{"style":450},[1180],{"type":34,"value":1181},"    trigger: ",{"type":28,"tag":417,"props":1183,"children":1184},{"style":467},[1185],{"type":34,"value":1186},"'.gallery-section'",{"type":28,"tag":417,"props":1188,"children":1189},{"style":450},[1190],{"type":34,"value":522},{"type":28,"tag":417,"props":1192,"children":1193},{"class":419,"line":543},[1194,1199,1204],{"type":28,"tag":417,"props":1195,"children":1196},{"style":450},[1197],{"type":34,"value":1198},"    start: ",{"type":28,"tag":417,"props":1200,"children":1201},{"style":467},[1202],{"type":34,"value":1203},"'top top'",{"type":28,"tag":417,"props":1205,"children":1206},{"style":450},[1207],{"type":34,"value":522},{"type":28,"tag":417,"props":1209,"children":1210},{"class":419,"line":560},[1211,1216,1220,1224,1229,1234,1239,1244,1248,1253,1257,1262,1266],{"type":28,"tag":417,"props":1212,"children":1213},{"style":456},[1214],{"type":34,"value":1215},"    end",{"type":28,"tag":417,"props":1217,"children":1218},{"style":450},[1219],{"type":34,"value":1124},{"type":28,"tag":417,"props":1221,"children":1222},{"style":433},[1223],{"type":34,"value":610},{"type":28,"tag":417,"props":1225,"children":1226},{"style":467},[1227],{"type":34,"value":1228}," `+=${",{"type":28,"tag":417,"props":1230,"children":1231},{"style":450},[1232],{"type":34,"value":1233},"gallery",{"type":28,"tag":417,"props":1235,"children":1236},{"style":467},[1237],{"type":34,"value":1238},".",{"type":28,"tag":417,"props":1240,"children":1241},{"style":450},[1242],{"type":34,"value":1243},"scrollWidth",{"type":28,"tag":417,"props":1245,"children":1246},{"style":433},[1247],{"type":34,"value":1133},{"type":28,"tag":417,"props":1249,"children":1250},{"style":450},[1251],{"type":34,"value":1252}," window",{"type":28,"tag":417,"props":1254,"children":1255},{"style":467},[1256],{"type":34,"value":1238},{"type":28,"tag":417,"props":1258,"children":1259},{"style":450},[1260],{"type":34,"value":1261},"innerWidth",{"type":28,"tag":417,"props":1263,"children":1264},{"style":467},[1265],{"type":34,"value":581},{"type":28,"tag":417,"props":1267,"children":1268},{"style":450},[1269],{"type":34,"value":522},{"type":28,"tag":417,"props":1271,"children":1272},{"class":419,"line":588},[1273,1278,1283],{"type":28,"tag":417,"props":1274,"children":1275},{"style":450},[1276],{"type":34,"value":1277},"    pin: ",{"type":28,"tag":417,"props":1279,"children":1280},{"style":439},[1281],{"type":34,"value":1282},"true",{"type":28,"tag":417,"props":1284,"children":1285},{"style":450},[1286],{"type":34,"value":522},{"type":28,"tag":417,"props":1288,"children":1289},{"class":419,"line":618},[1290,1295,1300],{"type":28,"tag":417,"props":1291,"children":1292},{"style":450},[1293],{"type":34,"value":1294},"    scrub: ",{"type":28,"tag":417,"props":1296,"children":1297},{"style":439},[1298],{"type":34,"value":1299},"1",{"type":28,"tag":417,"props":1301,"children":1302},{"style":450},[1303],{"type":34,"value":522},{"type":28,"tag":417,"props":1305,"children":1306},{"class":419,"line":637},[1307],{"type":28,"tag":417,"props":1308,"children":1309},{"style":450},[1310],{"type":34,"value":1311},"  },\n",{"type":28,"tag":417,"props":1313,"children":1314},{"class":419,"line":646},[1315],{"type":28,"tag":417,"props":1316,"children":1317},{"style":450},[1318],{"type":34,"value":1319},"})\n",{"type":28,"tag":785,"props":1321,"children":1323},{"id":1322},"image-reveal-animation",[1324],{"type":34,"value":1325},"Image Reveal Animation",{"type":28,"tag":36,"props":1327,"children":1328},{},[1329],{"type":34,"value":1330},"ทุกรูปเข้าด้วย clip-path animation ที่ทำให้รู้สึกเหมือน \"เปิดม่าน\":",{"type":28,"tag":406,"props":1332,"children":1336},{"className":1333,"code":1334,"language":1335,"meta":8,"style":8},"language-css shiki shiki-themes github-light github-dark","@keyframes reveal {\n  from { clip-path: inset(0 100% 0 0); }\n  to   { clip-path: inset(0 0% 0 0); }\n}\n","css",[1337],{"type":28,"tag":413,"props":1338,"children":1339},{"__ignoreMap":8},[1340,1357,1418,1471],{"type":28,"tag":417,"props":1341,"children":1342},{"class":419,"line":420},[1343,1348,1353],{"type":28,"tag":417,"props":1344,"children":1345},{"style":433},[1346],{"type":34,"value":1347},"@keyframes",{"type":28,"tag":417,"props":1349,"children":1350},{"style":597},[1351],{"type":34,"value":1352}," reveal",{"type":28,"tag":417,"props":1354,"children":1355},{"style":450},[1356],{"type":34,"value":615},{"type":28,"tag":417,"props":1358,"children":1359},{"class":419,"line":273},[1360,1365,1370,1375,1380,1385,1389,1394,1399,1404,1409,1413],{"type":28,"tag":417,"props":1361,"children":1362},{"style":456},[1363],{"type":34,"value":1364},"  from",{"type":28,"tag":417,"props":1366,"children":1367},{"style":450},[1368],{"type":34,"value":1369}," { ",{"type":28,"tag":417,"props":1371,"children":1372},{"style":439},[1373],{"type":34,"value":1374},"clip-path",{"type":28,"tag":417,"props":1376,"children":1377},{"style":450},[1378],{"type":34,"value":1379},": ",{"type":28,"tag":417,"props":1381,"children":1382},{"style":439},[1383],{"type":34,"value":1384},"inset",{"type":28,"tag":417,"props":1386,"children":1387},{"style":450},[1388],{"type":34,"value":464},{"type":28,"tag":417,"props":1390,"children":1391},{"style":439},[1392],{"type":34,"value":1393},"0",{"type":28,"tag":417,"props":1395,"children":1396},{"style":439},[1397],{"type":34,"value":1398}," 100",{"type":28,"tag":417,"props":1400,"children":1401},{"style":433},[1402],{"type":34,"value":1403},"%",{"type":28,"tag":417,"props":1405,"children":1406},{"style":439},[1407],{"type":34,"value":1408}," 0",{"type":28,"tag":417,"props":1410,"children":1411},{"style":439},[1412],{"type":34,"value":1408},{"type":28,"tag":417,"props":1414,"children":1415},{"style":450},[1416],{"type":34,"value":1417},"); }\n",{"type":28,"tag":417,"props":1419,"children":1420},{"class":419,"line":478},[1421,1426,1431,1435,1439,1443,1447,1451,1455,1459,1463,1467],{"type":28,"tag":417,"props":1422,"children":1423},{"style":456},[1424],{"type":34,"value":1425},"  to",{"type":28,"tag":417,"props":1427,"children":1428},{"style":450},[1429],{"type":34,"value":1430},"   { ",{"type":28,"tag":417,"props":1432,"children":1433},{"style":439},[1434],{"type":34,"value":1374},{"type":28,"tag":417,"props":1436,"children":1437},{"style":450},[1438],{"type":34,"value":1379},{"type":28,"tag":417,"props":1440,"children":1441},{"style":439},[1442],{"type":34,"value":1384},{"type":28,"tag":417,"props":1444,"children":1445},{"style":450},[1446],{"type":34,"value":464},{"type":28,"tag":417,"props":1448,"children":1449},{"style":439},[1450],{"type":34,"value":1393},{"type":28,"tag":417,"props":1452,"children":1453},{"style":439},[1454],{"type":34,"value":1408},{"type":28,"tag":417,"props":1456,"children":1457},{"style":433},[1458],{"type":34,"value":1403},{"type":28,"tag":417,"props":1460,"children":1461},{"style":439},[1462],{"type":34,"value":1408},{"type":28,"tag":417,"props":1464,"children":1465},{"style":439},[1466],{"type":34,"value":1408},{"type":28,"tag":417,"props":1468,"children":1469},{"style":450},[1470],{"type":34,"value":1417},{"type":28,"tag":417,"props":1472,"children":1473},{"class":419,"line":506},[1474],{"type":28,"tag":417,"props":1475,"children":1476},{"style":450},[1477],{"type":34,"value":1478},"}\n",{"type":28,"tag":29,"props":1480,"children":1482},{"id":1481},"performance-optimization",[1483],{"type":34,"value":1484},"Performance Optimization",{"type":28,"tag":36,"props":1486,"children":1487},{},[1488],{"type":34,"value":1489},"ความท้าทายใหญ่คือรูปภาพคุณภาพสูงจาก photographer มืออาชีพที่ไฟล์ใหญ่มาก เราแก้ด้วย:",{"type":28,"tag":328,"props":1491,"children":1492},{},[1493,1503,1519,1529],{"type":28,"tag":332,"props":1494,"children":1495},{},[1496,1501],{"type":28,"tag":57,"props":1497,"children":1498},{},[1499],{"type":34,"value":1500},"@nuxt\u002Fimage",{"type":34,"value":1502}," + AVIF format ลดขนาดไฟล์ได้ 60-80%",{"type":28,"tag":332,"props":1504,"children":1505},{},[1506,1511,1513],{"type":28,"tag":57,"props":1507,"children":1508},{},[1509],{"type":34,"value":1510},"Intersection Observer",{"type":34,"value":1512}," สำหรับ lazy load ที่แม่นยำกว่า native ",{"type":28,"tag":413,"props":1514,"children":1516},{"className":1515},[],[1517],{"type":34,"value":1518},"loading=\"lazy\"",{"type":28,"tag":332,"props":1520,"children":1521},{},[1522,1527],{"type":28,"tag":57,"props":1523,"children":1524},{},[1525],{"type":34,"value":1526},"Blur placeholder",{"type":34,"value":1528}," เพื่อให้หน้าดู complete ระหว่างรูปโหลด",{"type":28,"tag":332,"props":1530,"children":1531},{},[1532,1537],{"type":28,"tag":57,"props":1533,"children":1534},{},[1535],{"type":34,"value":1536},"Priority load",{"type":34,"value":1538}," เฉพาะรูป above-the-fold",{"type":28,"tag":29,"props":1540,"children":1541},{"id":114},[1542],{"type":34,"value":114},{"type":28,"tag":118,"props":1544,"children":1545},{},[1546,1565],{"type":28,"tag":122,"props":1547,"children":1548},{},[1549],{"type":28,"tag":126,"props":1550,"children":1551},{},[1552,1556,1560],{"type":28,"tag":130,"props":1553,"children":1554},{},[1555],{"type":34,"value":134},{"type":28,"tag":130,"props":1557,"children":1558},{},[1559],{"type":34,"value":139},{"type":28,"tag":130,"props":1561,"children":1562},{},[1563],{"type":34,"value":1564},"หลัง",{"type":28,"tag":146,"props":1566,"children":1567},{},[1568,1584,1602,1620,1638],{"type":28,"tag":126,"props":1569,"children":1570},{},[1571,1575,1580],{"type":28,"tag":153,"props":1572,"children":1573},{},[1574],{"type":34,"value":211},{"type":28,"tag":153,"props":1576,"children":1577},{},[1578],{"type":34,"value":1579},"52",{"type":28,"tag":153,"props":1581,"children":1582},{},[1583],{"type":34,"value":721},{"type":28,"tag":126,"props":1585,"children":1586},{},[1587,1592,1597],{"type":28,"tag":153,"props":1588,"children":1589},{},[1590],{"type":34,"value":1591},"LCP",{"type":28,"tag":153,"props":1593,"children":1594},{},[1595],{"type":34,"value":1596},"4.8s",{"type":28,"tag":153,"props":1598,"children":1599},{},[1600],{"type":34,"value":1601},"0.9s",{"type":28,"tag":126,"props":1603,"children":1604},{},[1605,1610,1615],{"type":28,"tag":153,"props":1606,"children":1607},{},[1608],{"type":34,"value":1609},"Bounce rate",{"type":28,"tag":153,"props":1611,"children":1612},{},[1613],{"type":34,"value":1614},"71%",{"type":28,"tag":153,"props":1616,"children":1617},{},[1618],{"type":34,"value":1619},"38%",{"type":28,"tag":126,"props":1621,"children":1622},{},[1623,1628,1633],{"type":28,"tag":153,"props":1624,"children":1625},{},[1626],{"type":34,"value":1627},"Average time on site",{"type":28,"tag":153,"props":1629,"children":1630},{},[1631],{"type":34,"value":1632},"1:20",{"type":28,"tag":153,"props":1634,"children":1635},{},[1636],{"type":34,"value":1637},"4:45",{"type":28,"tag":126,"props":1639,"children":1640},{},[1641,1646,1651],{"type":28,"tag":153,"props":1642,"children":1643},{},[1644],{"type":34,"value":1645},"Lead form submissions",{"type":28,"tag":153,"props":1647,"children":1648},{},[1649],{"type":34,"value":1650},"3\u002Fเดือน",{"type":28,"tag":153,"props":1652,"children":1653},{},[1654],{"type":34,"value":1655},"12\u002Fเดือน",{"type":28,"tag":241,"props":1657,"children":1658},{},[1659,1664],{"type":28,"tag":36,"props":1660,"children":1661},{},[1662],{"type":34,"value":1663},"\"ลูกค้าคนแรกที่ติดต่อมาหลังเว็บใหม่ออนไลน์ บอกว่าตัดสินใจจากเว็บเลย ไม่ได้ดู portfolio เพิ่มเติม\"",{"type":28,"tag":36,"props":1665,"children":1666},{},[1667],{"type":34,"value":1668},"— คุณวรรณา สุนทร, Creative Director",{"type":28,"tag":736,"props":1670,"children":1671},{},[1672],{"type":34,"value":740},{"title":8,"searchDepth":273,"depth":273,"links":1674},[1675,1676,1681,1682],{"id":1029,"depth":273,"text":1032},{"id":1040,"depth":273,"text":1040,"children":1677},[1678,1679,1680],{"id":1045,"depth":478,"text":1048},{"id":1056,"depth":478,"text":1059},{"id":1322,"depth":478,"text":1325},{"id":1481,"depth":273,"text":1484},{"id":114,"depth":273,"text":114},"content:portfolio:baan-design-studio.md","portfolio\u002Fbaan-design-studio.md","portfolio\u002Fbaan-design-studio",{"_path":1687,"_dir":6,"_draft":7,"_partial":7,"_locale":8,"title":1688,"description":8,"category":289,"client":1689,"timeline":1690,"role":1691,"date":1692,"tags":1693,"body":1698,"_type":280,"_id":1893,"_source":282,"_file":1894,"_stem":1895,"_extension":285},"\u002Fportfolio\u002Forchard-saas","Orchard — HR & Payroll SaaS สำหรับ SME ไทย","Orchard HR Tech","20 สัปดาห์","Full-stack Architecture, Frontend, Backend API","2023-11-15",[1694,17,18,763,19,1695,1696,1697],"React","Redis","Docker","AWS ECS",{"type":25,"children":1699,"toc":1883},[1700,1705,1710,1716,1722,1734,1752,1758,1770,1776,1781,1804,1809,1821,1832,1836],{"type":28,"tag":29,"props":1701,"children":1703},{"id":1702},"ภาพรวม",[1704],{"type":34,"value":1702},{"type":28,"tag":36,"props":1706,"children":1707},{},[1708],{"type":34,"value":1709},"Orchard เป็นโปรเจกต์ที่ใหญ่และซับซ้อนที่สุดที่เราเคยทำ — HR SaaS สำหรับบริษัทขนาด 20-500 คน ที่ต้องรองรับทั้ง time attendance, leave management, payroll calculation และ compliance กับกฎหมายแรงงานไทย",{"type":28,"tag":29,"props":1711,"children":1713},{"id":1712},"ความท้าทายด้าน-architecture",[1714],{"type":34,"value":1715},"ความท้าทายด้าน Architecture",{"type":28,"tag":785,"props":1717,"children":1719},{"id":1718},"multi-tenant-data-isolation",[1720],{"type":34,"value":1721},"Multi-tenant Data Isolation",{"type":28,"tag":36,"props":1723,"children":1724},{},[1725,1727,1732],{"type":34,"value":1726},"เราออกแบบ ",{"type":28,"tag":57,"props":1728,"children":1729},{},[1730],{"type":34,"value":1731},"schema-per-tenant",{"type":34,"value":1733}," บน PostgreSQL แทน row-level isolation เพราะ:",{"type":28,"tag":328,"props":1735,"children":1736},{},[1737,1742,1747],{"type":28,"tag":332,"props":1738,"children":1739},{},[1740],{"type":34,"value":1741},"Query performance ดีกว่าเมื่อ dataset ใหญ่ขึ้น",{"type":28,"tag":332,"props":1743,"children":1744},{},[1745],{"type":34,"value":1746},"Database maintenance ทำได้ง่ายกว่า",{"type":28,"tag":332,"props":1748,"children":1749},{},[1750],{"type":34,"value":1751},"สามารถ provision ทรัพยากรแยกกันได้สำหรับ enterprise tier",{"type":28,"tag":785,"props":1753,"children":1755},{"id":1754},"payroll-calculation-engine",[1756],{"type":34,"value":1757},"Payroll Calculation Engine",{"type":28,"tag":36,"props":1759,"children":1760},{},[1761,1763,1768],{"type":34,"value":1762},"กฎหมายแรงงานไทยมีความซับซ้อนสูง ทั้งการคำนวณ OT, วันหยุดชดเชย, ประกันสังคม, ภาษี ณ ที่จ่าย เราออกแบบเป็น ",{"type":28,"tag":57,"props":1764,"children":1765},{},[1766],{"type":34,"value":1767},"rule engine",{"type":34,"value":1769}," ที่อ่าน business rules จาก configuration แทนการ hardcode เพื่อรองรับการเปลี่ยนแปลงกฎหมายในอนาคต",{"type":28,"tag":785,"props":1771,"children":1773},{"id":1772},"performance-ของ-report-generation",[1774],{"type":34,"value":1775},"Performance ของ Report Generation",{"type":28,"tag":36,"props":1777,"children":1778},{},[1779],{"type":34,"value":1780},"รายงาน payroll บริษัท 300 คนต้องสร้างใน \u003C 3 วินาที เราแก้ด้วยการ:",{"type":28,"tag":838,"props":1782,"children":1783},{},[1784,1794,1799],{"type":28,"tag":332,"props":1785,"children":1786},{},[1787,1789],{"type":34,"value":1788},"Pre-compute ข้อมูลสรุปทุกสิ้นเดือนเก็บไว้ใน ",{"type":28,"tag":57,"props":1790,"children":1791},{},[1792],{"type":34,"value":1793},"Redis cache",{"type":28,"tag":332,"props":1795,"children":1796},{},[1797],{"type":34,"value":1798},"Generate PDF แบบ async บน background job",{"type":28,"tag":332,"props":1800,"children":1801},{},[1802],{"type":34,"value":1803},"ส่ง email + notification เมื่อ PDF พร้อม",{"type":28,"tag":29,"props":1805,"children":1807},{"id":1806},"สิ่งที่ทำให้โปรเจกต์นี้พิเศษ",[1808],{"type":34,"value":1806},{"type":28,"tag":36,"props":1810,"children":1811},{},[1812,1814,1819],{"type":34,"value":1813},"การทำ ",{"type":28,"tag":57,"props":1815,"children":1816},{},[1817],{"type":34,"value":1818},"audit trail",{"type":34,"value":1820}," ที่สมบูรณ์สำหรับทุก payroll transaction — ทุกการเปลี่ยนแปลงต้องบันทึกว่าใคร เปลี่ยนอะไร เมื่อไหร่ และทำไม เพราะ HR data มี legal implication ที่สูงมาก",{"type":28,"tag":36,"props":1822,"children":1823},{},[1824,1825,1830],{"type":34,"value":390},{"type":28,"tag":57,"props":1826,"children":1827},{},[1828],{"type":34,"value":1829},"event sourcing pattern",{"type":34,"value":1831}," บน PostgreSQL (ไม่ใช่ event database พิเศษ) ซึ่งง่ายกว่าแต่ให้ auditability ที่เพียงพอ",{"type":28,"tag":29,"props":1833,"children":1834},{"id":114},[1835],{"type":34,"value":114},{"type":28,"tag":328,"props":1837,"children":1838},{},[1839,1849,1861,1873],{"type":28,"tag":332,"props":1840,"children":1841},{},[1842,1844],{"type":34,"value":1843},"ลูกค้า pilot 8 บริษัท เวลาทำ payroll เฉลี่ยลดจาก ",{"type":28,"tag":57,"props":1845,"children":1846},{},[1847],{"type":34,"value":1848},"6 ชั่วโมง → 45 นาที",{"type":28,"tag":332,"props":1850,"children":1851},{},[1852,1854,1859],{"type":34,"value":1853},"Error ใน payroll calculation ลดลง ",{"type":28,"tag":57,"props":1855,"children":1856},{},[1857],{"type":34,"value":1858},"100%",{"type":34,"value":1860}," (จาก manual Excel)",{"type":28,"tag":332,"props":1862,"children":1863},{},[1864,1866,1871],{"type":34,"value":1865},"Net Promoter Score: ",{"type":28,"tag":57,"props":1867,"children":1868},{},[1869],{"type":34,"value":1870},"72",{"type":34,"value":1872}," (ระดับ excellent)",{"type":28,"tag":332,"props":1874,"children":1875},{},[1876,1878],{"type":34,"value":1877},"Uptime 12 เดือนหลัง launch: ",{"type":28,"tag":57,"props":1879,"children":1880},{},[1881],{"type":34,"value":1882},"99.94%",{"title":8,"searchDepth":273,"depth":273,"links":1884},[1885,1886,1891,1892],{"id":1702,"depth":273,"text":1702},{"id":1712,"depth":273,"text":1715,"children":1887},[1888,1889,1890],{"id":1718,"depth":478,"text":1721},{"id":1754,"depth":478,"text":1757},{"id":1772,"depth":478,"text":1775},{"id":1806,"depth":273,"text":1806},{"id":114,"depth":273,"text":114},"content:portfolio:orchard-saas.md","portfolio\u002Forchard-saas.md","portfolio\u002Forchard-saas",{"_path":1897,"_dir":6,"_draft":7,"_partial":7,"_locale":8,"title":1898,"description":8,"category":753,"client":1899,"timeline":1900,"role":1901,"date":1902,"tags":1903,"body":1905,"_type":280,"_id":2055,"_source":282,"_file":2056,"_stem":2057,"_extension":285},"\u002Fportfolio\u002Fbitebuddy-food-app","BiteBuddy — แอปค้นหาร้านอาหารและรีวิวเพื่อชุมชน","BiteBuddy Co., Ltd.","16 สัปดาห์","Mobile Development, AI Integration, UX Research","2023-08-01",[759,761,762,1904,763,18,1695],"OpenAI API",{"type":25,"children":1906,"toc":2045},[1907,1912,1917,1923,1929,1941,1953,1959,1964,1970,1975,1981,1999,2003],{"type":28,"tag":29,"props":1908,"children":1910},{"id":1909},"ที่มาของไอเดีย",[1911],{"type":34,"value":1909},{"type":28,"tag":36,"props":1913,"children":1914},{},[1915],{"type":34,"value":1916},"ลูกค้ามาหาเราพร้อมกับปัญหาที่คนไทยหลายล้านคนเจอทุกวัน: \"จะกินอะไรดี?\" — แต่วิธีแก้ปัญหาที่มีอยู่ (Google Maps, Wongnai) ให้ผลลัพธ์ที่กว้างเกินไป ไม่ contextual และไม่รู้สึกเป็น \"เพื่อนแนะนำ\"",{"type":28,"tag":29,"props":1918,"children":1920},{"id":1919},"features-ที่น่าสนใจ",[1921],{"type":34,"value":1922},"Features ที่น่าสนใจ",{"type":28,"tag":785,"props":1924,"children":1926},{"id":1925},"วันนี้กินอะไรดี-ai-recommendation",[1927],{"type":34,"value":1928},"\"วันนี้กินอะไรดี?\" AI Recommendation",{"type":28,"tag":36,"props":1930,"children":1931},{},[1932,1934,1939],{"type":34,"value":1933},"ระบบถามเพียง 3 คำถามสั้น (อารมณ์, งบ, คนกี่คน) แล้วแนะนำร้านที่ match ด้วย ",{"type":28,"tag":57,"props":1935,"children":1936},{},[1937],{"type":34,"value":1938},"OpenAI GPT-4o-mini",{"type":34,"value":1940}," ที่มี context ของร้านในฐานข้อมูล",{"type":28,"tag":36,"props":1942,"children":1943},{},[1944,1946,1951],{"type":34,"value":1945},"เราออกแบบให้ AI response เป็น ",{"type":28,"tag":57,"props":1947,"children":1948},{},[1949],{"type":34,"value":1950},"structured JSON",{"type":34,"value":1952}," เสมอ เพื่อให้ parse ง่ายและ fallback ได้เมื่อ AI ตอบผิด format",{"type":28,"tag":785,"props":1954,"children":1956},{"id":1955},"hyperlocal-review-system",[1957],{"type":34,"value":1958},"Hyperlocal Review System",{"type":28,"tag":36,"props":1960,"children":1961},{},[1962],{"type":34,"value":1963},"รีวิวแบ่งตาม \"context\" — รีวิวสำหรับกินคนเดียว vs มาเป็นครอบครัว vs business lunch ให้ผู้ใช้เลือก context ที่ตรงกับตัวเองก่อนอ่านรีวิว",{"type":28,"tag":785,"props":1965,"children":1967},{"id":1966},"real-time-คนกำลังกินอยู่",[1968],{"type":34,"value":1969},"Real-time \"คนกำลังกินอยู่\"",{"type":28,"tag":36,"props":1971,"children":1972},{},[1973],{"type":34,"value":1974},"แสดงจำนวน active user ที่ check-in ร้านนั้น ณ ขณะนั้น ทำให้รู้ว่าร้านคึกคักไหม โดยไม่ต้องโทรถาม",{"type":28,"tag":29,"props":1976,"children":1978},{"id":1977},"architecture-decision-firebase-vs-custom-backend",[1979],{"type":34,"value":1980},"Architecture Decision: Firebase vs Custom Backend",{"type":28,"tag":36,"props":1982,"children":1983},{},[1984,1986,1990,1992,1997],{"type":34,"value":1985},"เราตัดสินใจใช้ ",{"type":28,"tag":57,"props":1987,"children":1988},{},[1989],{"type":34,"value":761},{"type":34,"value":1991}," สำหรับ auth, realtime features และ push notifications แต่ ",{"type":28,"tag":57,"props":1993,"children":1994},{},[1995],{"type":34,"value":1996},"Node.js custom API",{"type":34,"value":1998}," สำหรับ restaurant data และ AI calls เพราะ Firebase query ไม่ flexible พอสำหรับ geo-based search ที่ซับซ้อน",{"type":28,"tag":29,"props":2000,"children":2001},{"id":114},[2002],{"type":34,"value":114},{"type":28,"tag":328,"props":2004,"children":2005},{},[2006,2011,2028,2033],{"type":28,"tag":332,"props":2007,"children":2008},{},[2009],{"type":34,"value":2010},"28,000+ downloads ใน 4 เดือนแรก (organic, no paid ads)",{"type":28,"tag":332,"props":2012,"children":2013},{},[2014,2016,2021,2023],{"type":34,"value":2015},"App Store Rating: ",{"type":28,"tag":57,"props":2017,"children":2018},{},[2019],{"type":34,"value":2020},"4.5 ⭐",{"type":34,"value":2022}," Play Store: ",{"type":28,"tag":57,"props":2024,"children":2025},{},[2026],{"type":34,"value":2027},"4.4 ⭐",{"type":28,"tag":332,"props":2029,"children":2030},{},[2031],{"type":34,"value":2032},"Daily Active Users: 4,800 (17% DAU\u002FMAU ratio)",{"type":28,"tag":332,"props":2034,"children":2035},{},[2036,2038,2043],{"type":34,"value":2037},"AI recommendation feature ถูกใช้ ",{"type":28,"tag":57,"props":2039,"children":2040},{},[2041],{"type":34,"value":2042},"43%",{"type":34,"value":2044}," ของ sessions",{"title":8,"searchDepth":273,"depth":273,"links":2046},[2047,2048,2053,2054],{"id":1909,"depth":273,"text":1909},{"id":1919,"depth":273,"text":1922,"children":2049},[2050,2051,2052],{"id":1925,"depth":478,"text":1928},{"id":1955,"depth":478,"text":1958},{"id":1966,"depth":478,"text":1969},{"id":1977,"depth":273,"text":1980},{"id":114,"depth":273,"text":114},"content:portfolio:bitebuddy-food-app.md","portfolio\u002Fbitebuddy-food-app.md","portfolio\u002Fbitebuddy-food-app",1779878302282]