[{"data":1,"prerenderedAt":3819},["ShallowReactive",2],{"all-posts":3,"post:\u002Fithome-2025-day-29":1210},[4,23,35,46,57,68,80,91,102,114,126,138,149,160,171,182,193,204,215,226,237,248,258,268,278,288,298,308,319,330,341,354,368,380,390,400,411,421,431,441,451,461,471,481,491,501,511,521,531,541,551,561,571,581,591,601,611,621,631,641,651,661,675,691,704,719,731,742,752,762,772,782,792,802,812,822,832,842,852,862,871,881,891,901,911,921,931,941,951,961,971,980,990,1000,1010,1020,1036,1050,1062,1073,1083,1095,1107,1121,1134,1145,1156,1167,1180,1191,1201],{"path":5,"title":6,"description":7,"date":8,"updated":9,"tags":10,"categories":14,"draft":15,"wordCount":16,"readingTime":17,"canonical":18,"series":19},"\u002Fithome-2026-day-30","Day 30：Angular Router Providers 與生命週期管理，路由離開後 Service 會銷毀嗎？","平常在 Angular 中，我們可以透過 providers 決定 Service 的注入範圍。例如註冊在 Root，讓整個 Application 共用同一個實例；或註冊在 Component，讓 Service 隨著…","2026-10-14T14:46:39+08:00",null,[11,12,13],"iThome 鐵人賽","2026 鐵人賽","Angular",[11,13],false,7227,7,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10424222",{"name":20,"url":21,"day":22},"2026 iThome 鐵人賽：Angular 22 Signal 進化論","https:\u002F\u002Fithelp.ithome.com.tw\u002Fusers\u002F20116554\u002Fironman\u002F9939",30,{"path":24,"title":25,"description":26,"date":27,"updated":9,"tags":28,"categories":29,"draft":15,"wordCount":30,"readingTime":31,"canonical":32,"series":33},"\u002Fithome-2026-day-29","Day 29：Angular 也有 VueUse？使用 Signality 整合常見 Browser API","在 Vue 生態系中，VueUse 提供許多 Composables，讓我們可以快速使用瀏覽器功能，例如監聽網路狀態、保存使用者設定，或偵測元素是否進入可視範圍。","2026-10-13T23:14:18+08:00",[11,12,13],[11,13],6387,6,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10424070",{"name":20,"url":21,"day":34},29,{"path":36,"title":37,"description":38,"date":39,"updated":9,"tags":40,"categories":41,"draft":15,"wordCount":42,"readingTime":31,"canonical":43,"series":44},"\u002Fithome-2026-day-28","Day 28：Angular 動態建立 Component，NgComponentOutlet 與 ViewContainerRef","平常在 Angular 中使用 Component，通常會直接在 Template 裡面宣告：","2026-10-12T17:27:41+08:00",[11,12,13],[11,13],6565,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10423641",{"name":20,"url":21,"day":45},28,{"path":47,"title":48,"description":49,"date":50,"updated":9,"tags":51,"categories":52,"draft":15,"wordCount":53,"readingTime":17,"canonical":54,"series":55},"\u002Fithome-2026-day-27","Day 27：NgRx Store vs SignalStore，從 Action、Reducer 到直接更新 State","前幾天我們使用 NgRx SignalStore 實作購物車，透過 withState() 管理商品資料、withComputed() 計算總金額，再利用 withMethods() 與 patchState() 更新 …","2026-10-11T20:06:41+08:00",[11,12,13],[11,13],7308,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10423362",{"name":20,"url":21,"day":56},27,{"path":58,"title":59,"description":60,"date":61,"updated":9,"tags":62,"categories":63,"draft":15,"wordCount":64,"readingTime":17,"canonical":65,"series":66},"\u002Fithome-2026-day-26","Day 26：SignalStore 與 Strategy Pattern，透過 DI 動態切換業務邏輯","昨天我們使用 Angular 原生 Signal 與 NgRx SignalStore 實作購物車，透過 State 管理商品資料，再利用 Computed 計算商品數量與總金額。","2026-10-10T22:01:59+08:00",[11,12,13],[11,13],7347,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10423116",{"name":20,"url":21,"day":67},26,{"path":69,"title":70,"description":71,"date":72,"updated":9,"tags":73,"categories":74,"draft":15,"wordCount":75,"readingTime":76,"canonical":77,"series":78},"\u002Fithome-2026-day-25","Day 25：Angular SignalStore，原生 Signal 不夠用嗎？","在 Angular 開發中，如果資料只會被單一 Component 使用，通常可以直接透過 signal() 管理。但當多個 Component 需要共用同一份資料時，就得考慮狀態應該放在哪裡。","2026-10-09T21:28:45+08:00",[11,12,13],[11,13],9117,9,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10422784",{"name":20,"url":21,"day":79},25,{"path":81,"title":82,"description":83,"date":84,"updated":9,"tags":85,"categories":86,"draft":15,"wordCount":87,"readingTime":31,"canonical":88,"series":89},"\u002Fithome-2026-day-24","Day 24：SPA Router 為什麼不再只靠 History API？","平常使用 Angular Router 時，可以透過 routerLink 或 router.navigate() 切換不同頁面。","2026-10-08T14:30:47+08:00",[11,12,13],[11,13],6330,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10422348",{"name":20,"url":21,"day":90},24,{"path":92,"title":93,"description":94,"date":95,"updated":9,"tags":96,"categories":97,"draft":15,"wordCount":98,"readingTime":17,"canonical":99,"series":100},"\u002Fithome-2026-day-23","Day 23：RouteReuseStrategy，切換路由一定要銷毀 Component 嗎？","在後台系統中，很常會看到頁籤式介面，例如：","2026-10-07T15:58:32+08:00",[11,12,13],[11,13],7361,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10422016",{"name":20,"url":21,"day":101},23,{"path":103,"title":104,"description":105,"date":106,"updated":9,"tags":107,"categories":108,"draft":15,"wordCount":109,"readingTime":110,"canonical":111,"series":112},"\u002Fithome-2026-day-22","Day 22：Angular Render Lifecycle，從 afterNextRender 到 DestroyRef","過去在 Angular 中，只要遇到初始化、View 建立或資源清理，通常會先想到 Lifecycle Hooks：","2026-10-06T23:36:23+08:00",[11,12,13],[11,13],8111,8,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10421810",{"name":20,"url":21,"day":113},22,{"path":115,"title":116,"description":117,"date":118,"updated":9,"tags":119,"categories":120,"draft":15,"wordCount":121,"readingTime":122,"canonical":123,"series":124},"\u002Fithome-2026-day-21","Day 21：Angular Template 還適合呼叫 Method 嗎？從 Pipe、RxJS 到 computed","過去在寫 Angular 時，因為 Template 可以直接呼叫 Method，很容易在專案裡看到大量這樣的寫法。","2026-10-05T23:30:32+08:00",[11,12,13],[11,13],5431,5,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10421425",{"name":20,"url":21,"day":125},21,{"path":127,"title":128,"description":129,"date":130,"updated":9,"tags":131,"categories":132,"draft":15,"wordCount":133,"readingTime":134,"canonical":135,"series":136},"\u002Fithome-2026-day-20","Day 20：Signal Forms 非同步驗證，validateHttp 與 validateAsync","前兩篇介紹了 Signal Forms 的基本驗證，以及需要參考其他欄位的 Cross-field Validation。","2026-10-04T20:56:17+08:00",[11,12,13],[11,13],4278,4,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10420910",{"name":20,"url":21,"day":137},20,{"path":139,"title":140,"description":141,"date":142,"updated":9,"tags":143,"categories":144,"draft":15,"wordCount":145,"readingTime":134,"canonical":146,"series":147},"\u002Fithome-2026-day-19","Day 19：Signal Forms Cross-field Validation 與欄位更新時機","上一篇介紹 Signal Forms 時，已經看到可以透過 Schema 定義欄位驗證，例如 required()、email()、minLength()。","2026-10-03T23:18:26+08:00",[11,12,13],[11,13],3783,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10420631",{"name":20,"url":21,"day":148},19,{"path":150,"title":151,"description":152,"date":153,"updated":9,"tags":154,"categories":155,"draft":15,"wordCount":156,"readingTime":31,"canonical":157,"series":158},"\u002Fithome-2026-day-18","Day 18：Angular Signal Forms 穩定了，跟我們用了這麼久的 Reactive Forms 到底差在哪？","在 Angular 中處理表單，很多專案使用的都是 Reactive Forms。","2026-10-02T23:28:36+08:00",[11,12,13],[11,13],5886,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10420249",{"name":20,"url":21,"day":159},18,{"path":161,"title":162,"description":163,"date":164,"updated":9,"tags":165,"categories":166,"draft":15,"wordCount":167,"readingTime":31,"canonical":168,"series":169},"\u002Fithome-2026-day-17","Day 17：effect() 不是拿來同步 Signal 的，Angular 的 Side Effect 該怎麼寫？","effect() 是 Angular signal 裡很容易被誤用的一個 API。因為寫法直覺，只要讀取 signal，就能在值改變時執行一些事情，所以很常被拿來同步狀態、打 API，甚至再去修改其他 signal。","2026-10-01T21:22:13+08:00",[11,12,13],[11,13],6473,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10419779",{"name":20,"url":21,"day":170},17,{"path":172,"title":173,"description":174,"date":175,"updated":9,"tags":176,"categories":177,"draft":15,"wordCount":178,"readingTime":31,"canonical":179,"series":180},"\u002Fithome-2026-day-16","Day 16：即將登場的 Route Resources，Angular Router 為什麼需要新的資料載入方式？","過去在 Angular Router 中，如果希望進入 Component 前先完成資料載入，可以透過 Resolver，讓資料取得流程在 Component 初始化之前執行。","2026-09-30T23:12:05+08:00",[11,12,13],[11,13],6534,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10419409",{"name":20,"url":21,"day":181},16,{"path":183,"title":184,"description":185,"date":186,"updated":9,"tags":187,"categories":188,"draft":15,"wordCount":189,"readingTime":17,"canonical":190,"series":191},"\u002Fithome-2026-day-15","Day 15：搞懂 Injection Context，從 inject()、runInInjectionContext() 到 injectAsync()","前面提到 Injection Context 時，可能會讓人聯想到先前介紹過的 Reactive Context。我一開始也有點搞混，因此花了一些時間重新理解 Angular 裡這些不同的執行環境。","2026-09-29T13:44:28+08:00",[11,12,13],[11,13],6986,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10418752",{"name":20,"url":21,"day":192},15,{"path":194,"title":195,"description":196,"date":197,"updated":9,"tags":198,"categories":199,"draft":15,"wordCount":200,"readingTime":17,"canonical":201,"series":202},"\u002Fithome-2026-day-14","Day 14：Lazy Loading 不只一種，從 Router Preloading、@defer 到 injectAsync()","當 Client Side Rendering 的應用越來越大時，我們通常會開始思考，能不能把原本一次載入的內容拆得更小，等到真正需要時再下載，避免初始載入過多 JavaScript，拉長畫面呈現的時間。","2026-09-28T22:44:46+08:00",[11,12,13],[11,13],7674,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10418485",{"name":20,"url":21,"day":203},14,{"path":205,"title":206,"description":207,"date":208,"updated":9,"tags":209,"categories":210,"draft":15,"wordCount":211,"readingTime":110,"canonical":212,"series":213},"\u002Fithome-2026-day-13","Day 13：從 Router Event 到 Signal","路由狀態管理在前端開發中比較少被單獨拿出來討論，但在 Angular 裡，一次路由導航其實會經過不少階段，也會在過程中發出不同的 Router Event，例如 NavigationStart、NavigationEnd…","2026-09-27T20:37:55+08:00",[11,12,13],[11,13],7777,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10417948",{"name":20,"url":21,"day":214},13,{"path":216,"title":217,"description":218,"date":219,"updated":9,"tags":220,"categories":221,"draft":15,"wordCount":222,"readingTime":122,"canonical":223,"series":224},"\u002Fithome-2026-day-12","Day 12：從 httpResource 到 TanStack Query，認識 Server State","前面我們花了不少時間討論，為什麼「請求本身的狀態」和「元件的顯示狀態」應該分開處理。","2026-09-26T23:35:12+08:00",[11,12,13],[11,13],5119,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10417614",{"name":20,"url":21,"day":225},12,{"path":227,"title":228,"description":229,"date":230,"updated":9,"tags":231,"categories":232,"draft":15,"wordCount":233,"readingTime":122,"canonical":234,"series":235},"\u002Fithome-2026-day-11","Day 11：用 rxResource 串起 Observable 與 Resource","前面介紹了不少 Resource 到 httpResource() 的應用。不過在 Angular 開發中，Observable 與 RxJS 仍然是很常見的非同步資料處理方式，像是 switchMap、map、debo…","2026-09-25T22:42:04+08:00",[11,12,13],[11,13],4908,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10417143",{"name":20,"url":21,"day":236},11,{"path":238,"title":239,"description":240,"date":241,"updated":9,"tags":242,"categories":243,"draft":15,"wordCount":244,"readingTime":31,"canonical":245,"series":246},"\u002Fithome-2026-day-10","Day 10：linkedSignal 讓相依狀態也能被修改","昨天我們透過 httpResource() 搭配 chain() 處理兩個非同步資料之間的依賴。下游 Request 需要使用上游資料時，會等上游完成後再繼續；當 Request 的來源改變，Resource 的狀態也會…","2026-09-24T23:49:49+08:00",[11,12,13],[11,13],5623,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10416746",{"name":20,"url":21,"day":247},10,{"path":249,"title":250,"description":251,"date":252,"updated":9,"tags":253,"categories":254,"draft":15,"wordCount":255,"readingTime":134,"canonical":256,"series":257},"\u002Fithome-2026-day-09","Day 9：httpResource 實務篇，統一回覆格式與 chain 相依請求","昨天我們透過 Orval 串起 API Contract，了解如何根據 OpenAPI 規格產生前端需要的型別與 API 呼叫方式。","2026-09-23T23:42:40+08:00",[11,12,13],[11,13],4358,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10416227",{"name":20,"url":21,"day":76},{"path":259,"title":260,"description":261,"date":262,"updated":9,"tags":263,"categories":264,"draft":15,"wordCount":265,"readingTime":122,"canonical":266,"series":267},"\u002Fithome-2026-day-08","Day 8：用 Orval 串起 Angular API Contract，同時產生 HttpClient、httpResource 與 Zod","前後端在對接 API 時，常見的問題之一，就是雙方對資料格式的定義逐漸不一致。","2026-09-22T23:48:12+08:00",[11,12,13],[11,13],4451,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10415694",{"name":20,"url":21,"day":110},{"path":269,"title":270,"description":271,"date":272,"updated":9,"tags":273,"categories":274,"draft":15,"wordCount":275,"readingTime":17,"canonical":276,"series":277},"\u002Fithome-2026-day-07","Day 7：從 Resource 到 httpResource，用 Signal 管理 HTTP 資料請求","昨天我們提到，Resource API 本身是一套通用的非同步資料模型，比較適合用在「讀取資料」這類情境，而不是新增、修改或刪除資料這種 Mutation 操作。","2026-09-21T22:13:18+08:00",[11,12,13],[11,13],6735,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10415038",{"name":20,"url":21,"day":17},{"path":279,"title":280,"description":281,"date":282,"updated":9,"tags":283,"categories":284,"draft":15,"wordCount":285,"readingTime":122,"canonical":286,"series":287},"\u002Fithome-2026-day-06","Day 6：Resource API，把非同步資料帶進 Signal","今天來看 Angular Signal 裡一個很重要的非同步資料 API：Resource API。","2026-09-20T19:25:37+08:00",[11,12,13],[11,13],5054,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10414377",{"name":20,"url":21,"day":31},{"path":289,"title":290,"description":291,"date":292,"updated":9,"tags":293,"categories":294,"draft":15,"wordCount":295,"readingTime":134,"canonical":296,"series":297},"\u002Fithome-2026-day-05","Day 5：從 Reactive Graph 到 View Tree，看 Angular 如何找到真正需要更新的畫面","前面我們已經知道，當 Signal 的值發生變化後，Angular 會透過 Reactive Graph 找到受到影響的 Consumer。","2026-09-19T20:19:47+08:00",[11,12,13],[11,13],4364,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10413852",{"name":20,"url":21,"day":122},{"path":299,"title":300,"description":301,"date":302,"updated":9,"tags":303,"categories":304,"draft":15,"wordCount":305,"readingTime":122,"canonical":306,"series":307},"\u002Fithome-2026-day-04","Day 4：Angular 怎麼知道誰正在讀 Signal？從 Reactive Context 理解依賴追蹤","前面我們看過，當 Template 讀取 Signal 時，Angular 會記住「誰依賴了誰」；當 Signal 發生變化後，也會透過 Reactive Graph 通知相關的 Consumer。","2026-09-18T21:58:16+08:00",[11,12,13],[11,13],4894,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10413313",{"name":20,"url":21,"day":134},{"path":309,"title":310,"description":311,"date":312,"updated":9,"tags":313,"categories":314,"draft":15,"wordCount":315,"readingTime":122,"canonical":316,"series":317},"\u002Fithome-2026-day-03","Day 3：從 set()、update() 看懂 Angular Signal 的基本使用與更新機制","昨天我們先試著理解，在 Template 中使用 Signal() 時，Angular 是如何建立依賴關係，並在 Signal 發生變化後，知道哪些 View 可能受到影響。","2026-09-17T23:08:58+08:00",[11,12,13],[11,13],5451,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10412826",{"name":20,"url":21,"day":318},3,{"path":320,"title":321,"description":322,"date":323,"updated":9,"tags":324,"categories":325,"draft":15,"wordCount":326,"readingTime":134,"canonical":327,"series":328},"\u002Fithome-2026-day-02","Day 2：Angular Signal 是什麼？從 Change Detection 理解它的運作原理","傳統 Angular 在監測資料變化時，通常會搭配 zone.js。像是 DOM 事件、Promise、Timer、HTTP 請求這些非同步操作發生時，zone.js 會幫忙攔截。","2026-09-16T20:19:12+08:00",[11,12,13],[11,13],4276,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10411944",{"name":20,"url":21,"day":329},2,{"path":331,"title":332,"description":333,"date":334,"updated":9,"tags":335,"categories":336,"draft":15,"wordCount":337,"readingTime":329,"canonical":338,"series":339},"\u002Fithome-2026-day-01","Day 1：為什麼想聊 Angular Signal？","相信講到前端框架，大家第一時間想到的通常會是 Vue、React，甚至也有人會推薦 Svelte 這類近年討論度很高的框架。但一講到 Angular，很多人的第一印象可能就像看到一座大城堡一樣：東西很多、規則很多，一時之…","2026-09-15T23:56:26+08:00",[11,12,13],[11,13],1478,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10411425",{"name":20,"url":21,"day":340},1,{"path":342,"title":343,"description":344,"date":345,"updated":9,"tags":346,"categories":351,"draft":15,"wordCount":353,"readingTime":134,"canonical":9,"series":9},"\u002FTS-Excess-Property-Checks","TypeScript 踩坑筆記：物件字面量的多餘屬性檢查機制","深入探討 TypeScript 的多餘屬性檢查（Excess Property Checks）機制，解析物件字面量與變數賦值的型別檢查差異，並提供實務開發中避免型別陷阱的最佳實踐方法。","2026-01-31T16:29:23+08:00",[347,348,349,350],"Excess Property Checks","Structural Typing","Type Safety","NestJS",[352],"TypeScript",3551,{"path":355,"title":356,"description":357,"date":358,"updated":9,"tags":359,"categories":361,"draft":15,"wordCount":363,"readingTime":329,"canonical":364,"series":365},"\u002Fithome-2025-day-30","Day 30：開發之旅完結 - 感謝與展望","終於來到鐵人賽的最後一天！這是我第二次參加鐵人賽，今年的主題是 LINE Bot 開發。這個主題源自於我參加的十二週讀書會分享，當時我發現大家對 LINE Bot 很感興趣，而我自己也一直很喜歡這個主題，於是早在六月就定…","2025-10-13T20:11:12+08:00",[11,360],"2025 鐵人賽",[11,362],"Line Bot",1751,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10396850",{"name":366,"url":367,"day":22},"2025 iThome 鐵人賽：Line Bot × NestJS：30 天開發日記","https:\u002F\u002Fithelp.ithome.com.tw\u002Fusers\u002F20116554\u002Fironman\u002F8779",{"path":369,"title":370,"description":371,"date":372,"updated":9,"tags":373,"categories":376,"draft":15,"wordCount":377,"readingTime":31,"canonical":378,"series":379},"\u002Fithome-2025-day-29","Day 29：LIFF 會員註冊 - 信箱驗證與 Swagger 文件整合","延續 Day 28 會員註冊的流程，在實際的會員註冊場景中，電子信箱驗證是確保用戶身份真實性的重要環節。本篇將基於昨天的註冊流程，新增兩個核心 API：發送驗證信與驗證驗證碼，讓會員註冊功能更加完整且安全。","2025-10-12T23:15:00+08:00",[11,360,362,374,350,375],"LIFF","Swagger",[11,362],6089,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10396512",{"name":366,"url":367,"day":34},{"path":381,"title":382,"description":383,"date":384,"updated":9,"tags":385,"categories":386,"draft":15,"wordCount":387,"readingTime":122,"canonical":388,"series":389},"\u002Fithome-2025-day-28","Day 28：LIFF 會員註冊 - ID Token 驗證與資料庫整合","今天的開發重點是建立會員卡系統，將 LIFF 前端與 NestJS 後端完整串接。","2025-10-11T22:57:29+08:00",[11,360,362,374,350],[11,362],5254,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10396146",{"name":366,"url":367,"day":45},{"path":391,"title":392,"description":393,"date":394,"updated":9,"tags":395,"categories":396,"draft":15,"wordCount":397,"readingTime":318,"canonical":398,"series":399},"\u002Fithome-2025-day-27","Day 27：群組事件與檔案訊息接收","今天主要補充 LINE Bot 在群組中的專屬特性，包括群組限定的 Webhook 事件，以及前面提到但尚未說明的第 7 種訊息類型 file 檔案訊息。","2025-10-10T21:01:52+08:00",[11,360,362,350],[11,362],2888,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10395785",{"name":366,"url":367,"day":56},{"path":401,"title":402,"description":403,"date":404,"updated":9,"tags":405,"categories":407,"draft":15,"wordCount":408,"readingTime":122,"canonical":409,"series":410},"\u002Fithome-2025-day-26","Day 26：LINE Pay 線上付款","LINE Pay 提供兩種付款方式：線上付款與實體付款。實體付款主要採用店家出示條碼的模式，顧客掃描後即可輸入金額完成付款。這兩種付款方式都支援在沙盒(Sandbox)環境中進行測試。本文將著重說明線上付款的整合流程。","2025-10-09T23:39:32+08:00",[11,360,406],"Line Pay",[11,362],4876,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10395481",{"name":366,"url":367,"day":67},{"path":412,"title":413,"description":414,"date":415,"updated":9,"tags":416,"categories":417,"draft":15,"wordCount":418,"readingTime":318,"canonical":419,"series":420},"\u002Fithome-2025-day-25","Day 25：優惠券設定與 Coupon Message 實作","LINE Official Account 的「優惠券」是提升顧客互動與忠誠度的重要功能。透過獎勵機制，不僅能吸引新客，更能有效促進回購。","2025-10-08T23:49:32+08:00",[11,360,362,350],[11,362],3207,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10395076",{"name":366,"url":367,"day":79},{"path":422,"title":423,"description":424,"date":425,"updated":9,"tags":426,"categories":427,"draft":15,"wordCount":428,"readingTime":134,"canonical":429,"series":430},"\u002Fithome-2025-day-24","Day 24：LIFF 刮刮樂實戰體驗","在了解 LIFF 的基礎設計流程後，本文將透過「刮刮樂」案例示範 LIFF API 的實際應用。從啟動時檢查權限與執行環境、確認用戶分享功能權限，到刮刮樂結束後發送訊息並關閉 LIFF，完整呈現 LIFF 在互動應用中的…","2025-10-07T22:36:58+08:00",[11,360,362,374],[11,362],4200,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10394618",{"name":366,"url":367,"day":90},{"path":432,"title":433,"description":434,"date":435,"updated":9,"tags":436,"categories":437,"draft":15,"wordCount":438,"readingTime":134,"canonical":439,"series":440},"\u002Fithome-2025-day-23","Day 23：LIFF 網頁應用開發入門","在鐵人賽前半段，我們深入探討了 LINE Bot 的兩大核心功能：Richmenu(選單設置)與各種 Reply Message(回覆訊息)的訊息類型。然而在實務應用中，若要提供更豐富的互動體驗，Line 往往需要搭配 …","2025-10-06T23:06:57+08:00",[11,360,362,374],[11,362],4400,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10394035",{"name":366,"url":367,"day":101},{"path":442,"title":443,"description":444,"date":445,"updated":9,"tags":446,"categories":447,"draft":15,"wordCount":448,"readingTime":329,"canonical":449,"series":450},"\u002Fithome-2025-day-22","Day 22：Render 雲端平台部署 Nest 後端伺服器","今天的目標是將系統正式上線。經過這段時間的開發，我們已完成 Line Message API 的核心功能與日誌處理機制。","2025-10-05T22:52:11+08:00",[11,360,362,350],[11,362],1895,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10393809",{"name":366,"url":367,"day":113},{"path":452,"title":453,"description":454,"date":455,"updated":9,"tags":456,"categories":457,"draft":15,"wordCount":458,"readingTime":318,"canonical":459,"series":460},"\u002Fithome-2025-day-21","Day 21：Nest Pino 整合 Grafana Loki 日誌","Loki 環境變數可參考 Day 20 文章的 Grafana Data Source settings 設定","2025-10-04T17:24:26+08:00",[11,360,362,350],[11,362],2740,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10393305",{"name":366,"url":367,"day":125},{"path":462,"title":463,"description":464,"date":465,"updated":9,"tags":466,"categories":467,"draft":15,"wordCount":468,"readingTime":318,"canonical":469,"series":470},"\u002Fithome-2025-day-20","Day 20：Grafana Loki 日誌服務 - 從申請到 API 串接測試","目前已經理解了 LINE Messaging API 的核心功能，包括如何接收訊息、串接第三方服務，以及發送多種類型的訊息。接下來，我們將把原本架設在 ngrok 的後端伺服器，透過 GitHub 自動化部署到 Rend…","2025-10-03T23:16:16+08:00",[11,360,362,350],[11,362],2461,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10392910",{"name":366,"url":367,"day":137},{"path":472,"title":473,"description":474,"date":475,"updated":9,"tags":476,"categories":477,"draft":15,"wordCount":478,"readingTime":318,"canonical":479,"series":480},"\u002Fithome-2025-day-19","Day 19：Flex Message Carousel 食譜分享","Flex Message 可以分成 bubble(單張卡片) 跟 carousel(多張卡片輪播) 兩種方式，昨天示範了 bubble 格式的電子個人名片後，今天主要展示 carousel 的應用，兩者的基本結構大致相同…","2025-10-02T16:06:17+08:00",[11,360,362,350],[11,362],2267,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10392256",{"name":366,"url":367,"day":148},{"path":482,"title":483,"description":484,"date":485,"updated":9,"tags":486,"categories":487,"draft":15,"wordCount":488,"readingTime":122,"canonical":489,"series":490},"\u002Fithome-2025-day-18","Day 18：Flex Message 設計個人名片","從基礎的文字互動開始，我們逐步探索了六大核心訊息類型的發送機制、Template Message 的結構化設計，以及 ImageMap Message 的互動式體驗。今天，我們將進入 LINE Bot 最具彈性的訊息格式…","2025-10-01T17:57:47+08:00",[11,360,362,350],[11,362],4468,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10391723",{"name":366,"url":367,"day":159},{"path":492,"title":493,"description":494,"date":495,"updated":9,"tags":496,"categories":497,"draft":15,"wordCount":498,"readingTime":122,"canonical":499,"series":500},"\u002Fithome-2025-day-17","Day 17：ImageMap Message  從互動海報到影片嵌入","在 Day 16 文章中，我們使用 Cloudflare Workers 實作了圖片代理功能。到目前為止，我們已經完成以下準備工作:","2025-09-30T11:58:32+08:00",[11,360,362,350],[11,362],4807,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10391164",{"name":366,"url":367,"day":170},{"path":502,"title":503,"description":504,"date":505,"updated":9,"tags":506,"categories":507,"draft":15,"wordCount":508,"readingTime":134,"canonical":509,"series":510},"\u002Fithome-2025-day-16","Day 16：Cloudflare Workers 圖片代理實作","在 Day 15 文章中，我們處理了 ImageMap Message 的背景圖，接著我們遇到了圖片不能出現副檔名及動態路徑支援不同解析度的問題。今天主要會透過 Cloudflare Workers 來解決這兩個問題，讓…","2025-09-29T16:15:41+08:00",[11,360,362,350],[11,362],3412,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10390567",{"name":366,"url":367,"day":181},{"path":512,"title":513,"description":514,"date":515,"updated":9,"tags":516,"categories":517,"draft":15,"wordCount":518,"readingTime":134,"canonical":519,"series":520},"\u002Fithome-2025-day-15","Day 15：共通屬性處理優化及 Imagemap  圖片製作","針對 Day 14 的程式碼架構進行優化。由於 Sender 和 Quick Reply 功能在所有回覆訊息類型中都能使用，我們應該將這些共用屬性抽取為獨立函數進行統一處理。","2025-09-28T21:03:38+08:00",[11,360,362,350],[11,362],4033,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10389697",{"name":366,"url":367,"day":192},{"path":522,"title":523,"description":524,"date":525,"updated":9,"tags":526,"categories":527,"draft":15,"wordCount":528,"readingTime":31,"canonical":529,"series":530},"\u002Fithome-2025-day-14","Day 14：LINE Bot Template Message 訊息回覆","經過前幾天的學習，我們已經成功掌握了 LINE Bot 六大基礎訊息類型的發送技巧，為訊息互動奠定了穩固的基礎。今天，我們將踏入更具挑戰性的進階應用領域！","2025-09-27T15:54:06+08:00",[11,360,362,350],[11,362],6372,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10388976",{"name":366,"url":367,"day":203},{"path":532,"title":533,"description":534,"date":535,"updated":9,"tags":536,"categories":537,"draft":15,"wordCount":538,"readingTime":122,"canonical":539,"series":540},"\u002Fithome-2025-day-13","Day 13：LINE Bot 客製化 Sender 與 Quick Reply 功能實作","LINE Bot 所有回覆訊息類型都具備「共用屬性」功能，這些屬性帶來兩項重要的互動體驗提升：","2025-09-26T11:41:34+08:00",[11,360,362,350],[11,362],4426,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10388037",{"name":366,"url":367,"day":214},{"path":542,"title":543,"description":544,"date":545,"updated":9,"tags":546,"categories":547,"draft":15,"wordCount":548,"readingTime":318,"canonical":549,"series":550},"\u002Fithome-2025-day-12","Day 12：LINE Bot Image、Video、Audio、Location 訊息回覆","Day 11 文章中，我們針對 LINE Bot 可以回覆的Text及Sticker基礎型別進行了封裝，不僅新增了 line-message.ts模組，還讓開發者可以用最少的參數產生 LINE Message 所需要的格…","2025-09-25T18:08:09+08:00",[11,360,362,350],[11,362],2977,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10387598",{"name":366,"url":367,"day":225},{"path":552,"title":553,"description":554,"date":555,"updated":9,"tags":556,"categories":557,"draft":15,"wordCount":558,"readingTime":31,"canonical":559,"series":560},"\u002Fithome-2025-day-11","Day 11：LINE Bot Text Emoji 與 Sticker 訊息回覆","目前我們的 LINE Bot 主要使用 Text 方式回覆訊息。今天將探討 Text 類型的 Emoji 應用，以及 Sticker 回覆方式，基於 Day 8 版本進行改寫。","2025-09-24T23:21:11+08:00",[11,360,350,362],[11,362],5659,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10386993",{"name":366,"url":367,"day":236},{"path":562,"title":563,"description":564,"date":565,"updated":9,"tags":566,"categories":567,"draft":15,"wordCount":568,"readingTime":318,"canonical":569,"series":570},"\u002Fithome-2025-day-10","Day 10：LINE Rich Menu Switch Action 提升使用者體驗","在 Day 9 文章中，我們結合 LINE 平台的功能，將天氣服務與圖片上傳服務整合，打造出第一張 LINE 圖文選單，透過 URI action 的方式觸發平台提供的互動效果。接著，我們使用客製化選單製作出 9 格的客…","2025-09-23T15:02:57+08:00",[11,360,362,350],[11,362],2906,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10386165",{"name":366,"url":367,"day":247},{"path":572,"title":573,"description":574,"date":575,"updated":9,"tags":576,"categories":577,"draft":15,"wordCount":578,"readingTime":318,"canonical":579,"series":580},"\u002Fithome-2025-day-09","Day  9：打造 LINE Rich Menu - 後台設計 vs API 客製化","在 Day8 文章中，我們透過整合雲端服務 Cloudinary，學習如何處理使用者上傳的圖片檔案，並將其儲存至雲端。然而，LINE Bot 除了讓使用者輸入文字回覆的方式之外，更常見的是透過圖文選單(Rich Menu…","2025-09-22T21:54:25+08:00",[11,360,362,350],[11,362],3285,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10385658",{"name":366,"url":367,"day":76},{"path":582,"title":583,"description":584,"date":585,"updated":9,"tags":586,"categories":587,"draft":15,"wordCount":588,"readingTime":17,"canonical":589,"series":590},"\u002Fithome-2025-day-08","Day 8：Cloudinary 雲端圖片儲存整合","在 Day 6 中，我們透過整合 OpenWeatherMap API 與座標位置訊息學習了如何結合第三方服務。今天將探索 Cloudinary 雲端服務，解決 LINE 圖片資源會過期的問題，學習如何透過 LINE M…","2025-09-21T23:13:19+08:00",[11,360,362,350],[11,362],7215,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10384968",{"name":366,"url":367,"day":110},{"path":592,"title":593,"description":594,"date":595,"updated":9,"tags":596,"categories":597,"draft":15,"wordCount":598,"readingTime":122,"canonical":599,"series":600},"\u002Fithome-2025-day-07","Day 7：Joi 環境變數統一驗證，重構 LINE Bot 天氣服務模組","在 Day 6 開發 LINE Bot 結合 OpenWeatherMap API 的過程中，我們採用私有方法將 API 請求處理與資料格式化整合在一起。今天將對 Day 6 的實作進行架構重構，主要目標是建立更完善的環…","2025-09-20T23:07:08+08:00",[11,360,362,350],[11,362],5157,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10384302",{"name":366,"url":367,"day":17},{"path":602,"title":603,"description":604,"date":605,"updated":9,"tags":606,"categories":607,"draft":15,"wordCount":608,"readingTime":134,"canonical":609,"series":610},"\u002Fithome-2025-day-06","Day 6：整合第三方 API 天氣查詢服務","Day 5 帶大家了解 LINE Bot 能接收使用者傳遞的「6+1」種訊息類型。之所以這樣強調，是因為 File 類型僅能在群組聊天中使用，這個限制我自己也經常忘記，所以習慣用「6+1」來提醒自己。","2025-09-19T21:07:47+08:00",[11,360,362,350],[11,362],3892,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10383636",{"name":366,"url":367,"day":31},{"path":612,"title":613,"description":614,"date":615,"updated":9,"tags":616,"categories":617,"draft":15,"wordCount":618,"readingTime":122,"canonical":619,"series":620},"\u002Fithome-2025-day-05","Day 5：整合 Pino Logger 提升 LINE Bot 專案的可維護性","看著專案逐漸成型，現在該開始重視日誌記錄的規劃了。在實際部署時，我們通常會區分 develop(開發環境)和 production(正式環境)兩種不同的執行環境，針對不同環境採用相應的日誌策略。","2025-09-18T23:30:00+08:00",[11,360,362,350],[11,362],4746,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10382298",{"name":366,"url":367,"day":122},{"path":622,"title":623,"description":624,"date":625,"updated":9,"tags":626,"categories":627,"draft":15,"wordCount":628,"readingTime":31,"canonical":629,"series":630},"\u002Fithome-2025-day-04","Day 4：掌握 LINE Bot 七種訊息類型與 NestJS 架構優化","目前我們都只使用 LINE Message Event 中的純文字(text)格式進行回覆，但除了純文字之外，LINE Bot 後端伺服器其實可以接收多種不同類型的訊息。今天就讓我們一起來深入了解這些訊息類型吧！","2025-09-17T22:53:37+08:00",[11,360,362,350],[11,362],5828,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10381651",{"name":366,"url":367,"day":134},{"path":632,"title":633,"description":634,"date":635,"updated":9,"tags":636,"categories":637,"draft":15,"wordCount":638,"readingTime":122,"canonical":639,"series":640},"\u002Fithome-2025-day-03","Day 3：Webhook 簽章驗證與訊息回覆策略(Push & Reply)","上一篇帶大家透過「Webhook 模式」成功架設了專屬的 LINE Bot 代理人。相信大家在體驗基礎功能後，對於 LINE Bot Webhook 還能監聽哪些事件、支援哪些類型的訊息回覆，以及如何確保後端收到的訊息確…","2025-09-16T21:53:39+08:00",[11,360,362,350],[11,362],5360,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10381303",{"name":366,"url":367,"day":318},{"path":642,"title":643,"description":644,"date":645,"updated":9,"tags":646,"categories":647,"draft":15,"wordCount":648,"readingTime":31,"canonical":649,"series":650},"\u002Fithome-2025-day-02","Day 2：從 Webhook 到後端，打造你的專屬機器人","上一篇，帶大家透過「Chat 模式」，讓自身變成客服機器人的方式。這篇會從「Webhook 模式」切入，並以 NestJS 作為後端框架，教你如何在休息時間，也能讓後端伺服器當你的替身幫你解惑使用者的問題。","2025-09-15T21:20:49+08:00",[11,360,362,350],[11,362],6590,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10380524",{"name":366,"url":367,"day":329},{"path":652,"title":653,"description":654,"date":655,"updated":9,"tags":656,"categories":657,"draft":15,"wordCount":658,"readingTime":318,"canonical":659,"series":660},"\u002Fithome-2025-day-01","Day 1：Hello World LINE Bot","每個程式語言的開始通常都會以練習 Hello World！作為一個簡單的起手式，LINE Bot 也不意外。這一系列會著重在 LINE Bot 的相關應用，帶大家用輕鬆的方式一起建立屬於自己的 Line Bot 聊天機器…","2025-09-14T22:48:33+08:00",[11,360,362,350],[11,362],2723,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10379895",{"name":366,"url":367,"day":340},{"path":662,"title":663,"description":664,"date":665,"updated":9,"tags":666,"categories":671,"draft":15,"wordCount":674,"readingTime":134,"canonical":9,"series":9},"\u002Fdbdiagram-tool","關聯式資料庫設計必備 - dbdiagram.io ERD 繪圖工具","還在煩惱如何設計關聯式資料表實體關聯圖嗎？ 使用 dbdiagram.io 加快你與同事之間的距離","2024-11-05T20:18:24+08:00",[667,668,669,670],"ERD","dbdiagram.io","DBML","資料庫設計",[672,673],"Database","開發工具",3982,{"path":676,"title":677,"description":678,"date":679,"updated":9,"tags":680,"categories":687,"draft":15,"wordCount":690,"readingTime":122,"canonical":9,"series":9},"\u002FPostgreSQL-1","前端勇士大闖六角後端體驗營 - 初探 PostgreSQL DML 語法","初步踏入後端的世界，了解何謂後端以及資料庫常見語法 DDL、DML。","2024-10-30T22:43:49+08:00",[681,682,683,684,685,686],"PostgreSQL","SQL","DML","DDL","資料庫","六角學院",[672,688,689],"後端實戰","學習心得",5220,{"path":692,"title":693,"description":694,"date":695,"updated":9,"tags":696,"categories":700,"draft":15,"wordCount":703,"readingTime":329,"canonical":9,"series":9},"\u002Fcss-flex-1","CSS flex 軸線對齊技巧 - 預設屬性","CSS flex 排版技巧你都了解了嗎？回歸學習初心的認識 flex 預設屬性","2024-10-24T20:29:08+08:00",[697,698,699],"Flexbox","前端排版","切版技巧",[701,702],"CSS","前端開發",1856,{"path":705,"title":706,"description":707,"date":708,"updated":9,"tags":709,"categories":712,"draft":15,"wordCount":714,"readingTime":329,"canonical":715,"series":716},"\u002Fithome-2024-day-30","Vue 3 用實作帶你看過核心概念 - Day 30：新手村的鐵人證書 - 完賽心得","花了整整半年左右的時間，每天下班都很用心的準備範例或是看書，這一段旅程對我來說真的很重要。擔心、害怕跟不安的心情一直充斥自己心中，擔心寫錯或是寫不好，想讓自己再多準備久一點再開始，所以拖到中秋連假才開始，想下定決心讓自己…","2024-10-13T12:42:25+08:00",[11,710,711],"2024 鐵人賽","Vue 3",[11,713],"Vue",1381,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10367206",{"name":717,"url":718,"day":22},"2024 iThome 鐵人賽：Vue 3 初學者：用實作帶你看過核心概念","https:\u002F\u002Fithelp.ithome.com.tw\u002Fusers\u002F20116554\u002Fironman\u002F7800",{"path":720,"title":721,"description":722,"date":723,"updated":9,"tags":724,"categories":727,"draft":15,"wordCount":728,"readingTime":134,"canonical":729,"series":730},"\u002Fithome-2024-day-29","Vue 3 用實作帶你看過核心概念 - Day 29：Composables(組合式函數)","Composables是 Vue 3 中的一個重要概念，它是利用 Vue 的 Composition API 來創建和管理可重用的有狀態邏輯的函數。Composables 允許我們將組件邏輯抽象成可重用的函數，使得代碼更…","2024-10-12T15:07:55+08:00",[11,710,711,725,726],"Options API","Composition API",[11,713],4233,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10366865",{"name":717,"url":718,"day":34},{"path":732,"title":733,"description":734,"date":735,"updated":9,"tags":736,"categories":738,"draft":15,"wordCount":739,"readingTime":134,"canonical":740,"series":741},"\u002Fithome-2024-day-28","Vue 3 用實作帶你看過核心概念 - Day 28：官方 Vue Router 的應用與技巧","路由設定方式，一般可以分成兩種：","2024-10-11T17:14:44+08:00",[11,710,711,725,737],"Vue Router",[11,713],4095,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10366520",{"name":717,"url":718,"day":45},{"path":743,"title":744,"description":745,"date":746,"updated":9,"tags":747,"categories":748,"draft":15,"wordCount":749,"readingTime":122,"canonical":750,"series":751},"\u002Fithome-2024-day-27","Vue 3 用實作帶你看過核心概念 - Day 27：VeeValidate 表單驗證工具","在開發 Vue 應用程式時，表單驗證至關重要，它能確保使用者輸入的資料符合預期的格式和要求。今天來介紹一款專門用於 Vue 的表單驗證工具—VeeValidate v4。這個工具不僅能幫助開發者輕鬆實現表單驗證，還提供了…","2024-10-10T21:02:16+08:00",[11,710,711,725],[11,713],4461,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10366165",{"name":717,"url":718,"day":56},{"path":753,"title":754,"description":755,"date":756,"updated":9,"tags":757,"categories":758,"draft":15,"wordCount":759,"readingTime":318,"canonical":760,"series":761},"\u002Fithome-2024-day-26","Vue 3 用實作帶你看過核心概念 - Day 26：KeepAlive 與 Teleport 內置組件","預設情況下，切換組件會導致組件的創建及銷毀，無法保持組件內部的狀態。","2024-10-09T18:50:53+08:00",[11,710,711,725],[11,713],3158,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10365724",{"name":717,"url":718,"day":67},{"path":763,"title":764,"description":765,"date":766,"updated":9,"tags":767,"categories":768,"draft":15,"wordCount":769,"readingTime":318,"canonical":770,"series":771},"\u002Fithome-2024-day-25","Vue 3 用實作帶你看過核心概念 - Day 25：自定義指令(Custom Directives)","Vue不僅提供了豐富的內置指令用於操作DOM元素，還允許開發者創建自定義指令。透過自定義指令，我們可以對DOM元素進行更細緻的控制，並在指令的不同生命週期階段執行特定的邏輯，從而實現更靈活的功能擴展。","2024-10-08T22:05:32+08:00",[11,710,711,725],[11,713],3116,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10365355",{"name":717,"url":718,"day":79},{"path":773,"title":774,"description":775,"date":776,"updated":9,"tags":777,"categories":778,"draft":15,"wordCount":779,"readingTime":329,"canonical":780,"series":781},"\u002Fithome-2024-day-24","Vue 3 用實作帶你看過核心概念 - Day 24：異步加載(Lazy loading)","在大型項目中，我們通常需要將應用拆分為更小的代碼模塊，並僅在需要時才從服務器加載相關組件，以減少初次加載的資源量。Vue 提供了 defineAsyncComponent 方法來實現組件的異步加載，這樣可以有效提高應用的…","2024-10-07T20:28:27+08:00",[11,710,711,725],[11,713],2171,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10364861",{"name":717,"url":718,"day":90},{"path":783,"title":784,"description":785,"date":786,"updated":9,"tags":787,"categories":788,"draft":15,"wordCount":789,"readingTime":318,"canonical":790,"series":791},"\u002Fithome-2024-day-23","Vue 3 用實作帶你看過核心概念 - Day 23：依賴注入( Provide-Inject )","在Dayxx中提到過，父子組件之間的數據傳遞可以通過props來實現。然而，當祖孫層組件需要進行數據傳遞時，中間層級的組件可能並不需要這個props，但卻不得不為了使子組件獲取數據而被迫傳遞。","2024-10-06T15:14:51+08:00",[11,710,711,725],[11,713],3183,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10364252",{"name":717,"url":718,"day":101},{"path":793,"title":794,"description":795,"date":796,"updated":9,"tags":797,"categories":798,"draft":15,"wordCount":799,"readingTime":134,"canonical":800,"series":801},"\u002Fithome-2024-day-22","Vue 3 用實作帶你看過核心概念 - Day 22：v-slot 插槽使用","先前Day 18中提到父組件可以通過props將參數資料傳遞給子組件進行渲染，這樣可以有效地將資料處理與畫面渲染的工作分開。然而，當我們需要更加客製化的呈現內容，由父組件來控制子組件的部分顯示時，可以使用v-slot 指…","2024-10-05T12:32:51+08:00",[11,710,711,725],[11,713],4292,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10363713",{"name":717,"url":718,"day":113},{"path":803,"title":804,"description":805,"date":806,"updated":9,"tags":807,"categories":808,"draft":15,"wordCount":809,"readingTime":329,"canonical":810,"series":811},"\u002Fithome-2024-day-21","Vue 3 用實作帶你看過核心概念 - Day 21： 組件透傳屬性(Attributes)的應用","當父組件向子組件傳遞屬性或監聽器，而這些屬性未在子組件中定義為props或emits時，它們會自動繼承並附加到子組件的根元素上，常見的屬性包括class、style、id等。這些屬性可以透過$attrs屬性訪問並進行操作…","2024-10-04T22:42:54+08:00",[11,710,711,725],[11,713],2146,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10363469",{"name":717,"url":718,"day":125},{"path":813,"title":814,"description":815,"date":816,"updated":9,"tags":817,"categories":818,"draft":15,"wordCount":819,"readingTime":122,"canonical":820,"series":821},"\u002Fithome-2024-day-20","Vue 3 用實作帶你看過核心概念 - Day 20：組件使用 v-model 雙向綁定","在Day 13中，我們介紹了表單元素（例如輸入框）如何處理數據雙向綁定。當需要將表單元素與一個響應式變數綁定時，v-model提供雙向綁定功能。這代表當畫面上表單元素的變動會即時影響Vue數據，同時當Vue數據的值改變時…","2024-10-03T23:40:48+08:00",[11,710,711,725],[11,713],5191,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10362957",{"name":717,"url":718,"day":137},{"path":823,"title":824,"description":825,"date":826,"updated":9,"tags":827,"categories":828,"draft":15,"wordCount":829,"readingTime":318,"canonical":830,"series":831},"\u002Fithome-2024-day-19","Vue 3 用實作帶你看過核心概念 - Day 19：子組件向父組件發射事件 - emits","組件可以通過$emit(functionName, params)自定義事件名稱並傳遞參數。自定義事件名稱建議使用kebab-case來命名自定義事件名稱，以確保命名風格與HTML 屬性一致性。此外，自定義事件還支持事件…","2024-10-02T15:51:16+08:00",[11,710,711,725],[11,713],2877,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10362144",{"name":717,"url":718,"day":148},{"path":833,"title":834,"description":835,"date":836,"updated":9,"tags":837,"categories":838,"draft":15,"wordCount":839,"readingTime":31,"canonical":840,"series":841},"\u002Fithome-2024-day-18","Vue 3 用實作帶你看過核心概念 - Day 18：父組件向子組件傳值 - props","組件之間的傳值必須透過聲明 props 的方式，這樣可以避免與HTML的屬性(attribute)混淆。父組件傳遞參數給子組件的方式有以下兩種方式：","2024-10-01T22:28:48+08:00",[11,710,711,725],[11,713],5782,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10361721",{"name":717,"url":718,"day":159},{"path":843,"title":844,"description":845,"date":846,"updated":9,"tags":847,"categories":848,"draft":15,"wordCount":849,"readingTime":318,"canonical":850,"series":851},"\u002Fithome-2024-day-17","Vue 3 用實作帶你看過核心概念 - Day 17：組件基礎 - 全局註冊與區域註冊","在 Day 5 我們提到可以將應用拆分為根組件和多個組件實例，並將功能模塊化。","2024-09-30T23:30:10+08:00",[11,710,711,725],[11,713],2954,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10361253",{"name":717,"url":718,"day":170},{"path":853,"title":854,"description":855,"date":856,"updated":9,"tags":857,"categories":858,"draft":15,"wordCount":859,"readingTime":122,"canonical":860,"series":861},"\u002Fithome-2024-day-16","Vue 3 用實作帶你看過核心概念 - Day 16：使用 ref 屬性引用模板元素","雖然 Vue 透過模板幫助我們處理了大部分的DOM 操作，但在某些情況下，仍然需要手動操作 DOM。例如，在使用第三方套件（如 Bootstrap 的 Modal）時，或者需要調用像video.play()和video.…","2024-09-29T16:25:45+08:00",[11,710,711,725],[11,713],4608,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10360419",{"name":717,"url":718,"day":181},{"path":863,"title":864,"description":865,"date":866,"updated":9,"tags":867,"categories":868,"draft":15,"wordCount":488,"readingTime":122,"canonical":869,"series":870},"\u002Fithome-2024-day-15","Vue 3 用實作帶你看過核心概念 - Day 15：監聽器(Watchers)","在響應式狀態變化時執行特定操作，需要使用watch 監聽器。這些操作可能包含副作用 (side effects)，如發送請求、調用 API，或更新 DOM 以外的系統狀態，亦或是處理自定義的邏輯。","2024-09-28T18:30:31+08:00",[11,710,711,725],[11,713],"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10359958",{"name":717,"url":718,"day":192},{"path":872,"title":873,"description":874,"date":875,"updated":9,"tags":876,"categories":877,"draft":15,"wordCount":878,"readingTime":134,"canonical":879,"series":880},"\u002Fithome-2024-day-14","Vue 3 用實作帶你看過核心概念 - Day 14：生命週期鉤子(Lifecycle Hooks)","在 Vue 中，當一個組件創建時，它會經歷一系列的流程，這些流程決定了組件從初始化到渲染，再到最終銷毀的過程。","2024-09-27T21:57:32+08:00",[11,710,711,725],[11,713],3780,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10359419",{"name":717,"url":718,"day":203},{"path":882,"title":883,"description":884,"date":885,"updated":9,"tags":886,"categories":887,"draft":15,"wordCount":888,"readingTime":318,"canonical":889,"series":890},"\u002Fithome-2024-day-13","Vue 3 用實作帶你看過核心概念 - Day 13：v-model 雙向綁定控制 - 常用於表單輸入","表單輸入是開發中非常常見的需求，原始方式通常需要手動使用v-bind綁定表單元素的值 (value)，並透過v-on綁定input事件來監聽輸入變化。","2024-09-26T21:49:25+08:00",[11,710,711,725],[11,713],3153,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10358850",{"name":717,"url":718,"day":214},{"path":892,"title":893,"description":894,"date":895,"updated":9,"tags":896,"categories":897,"draft":15,"wordCount":898,"readingTime":134,"canonical":899,"series":900},"\u002Fithome-2024-day-12","Vue 3 用實作帶你看過核心概念 - Day 12：v-on 指令 - 事件處理","在 Vue 中，監聽 HTML DOM 元素的事件變化，可以使用v-on 指令將 JavaScript 事件處理器綁定到對應的DOM 事件上。這個指令的縮寫符號為@，例如：v-on:click，其縮寫方式為@click。","2024-09-25T21:42:59+08:00",[11,710,711,725],[11,713],3808,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10358241",{"name":717,"url":718,"day":225},{"path":902,"title":903,"description":904,"date":905,"updated":9,"tags":906,"categories":907,"draft":15,"wordCount":908,"readingTime":134,"canonical":909,"series":910},"\u002Fithome-2024-day-11","Vue 3 用實作帶你看過核心概念 - Day 11：v-for 指令的基本與進階應用","當數據類型是陣列或物件時，我們不會希望將重複的選項或元素直接硬編碼在模板中。這樣的寫法會導致代碼冗長且不具可維護性。","2024-09-24T23:11:25+08:00",[11,710,711,725],[11,713],4381,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10357705",{"name":717,"url":718,"day":236},{"path":912,"title":913,"description":914,"date":915,"updated":9,"tags":916,"categories":917,"draft":15,"wordCount":918,"readingTime":318,"canonical":919,"series":920},"\u002Fithome-2024-day-10","Vue 3 用實作帶你看過核心概念 - Day 10：條件渲染 - v-if 與 v-show 的區別與應用","開發中遇到需要處理【如果是...就要...】的情境時，Vue 提供了 v-if 指令來幫助我們根據條件進行元素的渲染。這樣可以在特定條件滿足時動態地顯示或隱藏某些元素。","2024-09-23T23:39:24+08:00",[11,710,711,725],[11,713],2827,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10357160",{"name":717,"url":718,"day":247},{"path":922,"title":923,"description":924,"date":925,"updated":9,"tags":926,"categories":927,"draft":15,"wordCount":928,"readingTime":122,"canonical":929,"series":930},"\u002Fithome-2024-day-09","Vue 3 用實作帶你看過核心概念 - Day 9：Class 與 Style 的樣式綁定","v-bind 指令通常用於綁定 HTML 屬性，但在實際開發中，最常見的應用場景之一還是綁定class和 style這兩個 HTML 屬性。","2024-09-22T15:05:53+08:00",[11,710,711,725],[11,713],4630,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10356119",{"name":717,"url":718,"day":76},{"path":932,"title":933,"description":934,"date":935,"updated":9,"tags":936,"categories":937,"draft":15,"wordCount":938,"readingTime":134,"canonical":939,"series":940},"\u002Fithome-2024-day-08","Vue 3 用實作帶你看過核心概念 - Day 8：計算屬性應用與實踐","在 Day 6 在使用 Vue 的Mustache語法時，可以用來放置表達式。不過，這邊建議避免在 Mustache 中撰寫過於複雜的邏輯。這樣的建議主要有兩個原因：","2024-09-21T21:33:29+08:00",[11,710,711,725],[11,713],4060,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10355474",{"name":717,"url":718,"day":110},{"path":942,"title":943,"description":944,"date":945,"updated":9,"tags":946,"categories":947,"draft":15,"wordCount":948,"readingTime":122,"canonical":949,"series":950},"\u002Fithome-2024-day-07","Vue 3 用實作帶你看過核心概念 - Day 7： 響應式基礎 - Options API","前面提到了模板搭配指令及 Mustache 的用法。今天則是要介紹所謂透過getter跟setter取值跟賦值的方式在 JavaScript 是怎麼做到的，這部分會跟 Vue 本身的響應式基礎有很大相關。此外，我也會帶著…","2024-09-20T23:41:42+08:00",[11,710,711],[11,713],4407,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10354781",{"name":717,"url":718,"day":17},{"path":952,"title":953,"description":954,"date":955,"updated":9,"tags":956,"categories":957,"draft":15,"wordCount":958,"readingTime":134,"canonical":959,"series":960},"\u002Fithome-2024-day-06","Vue 3 用實作帶你看過核心概念 - Day 6：數據綁定和模板(Template)","Vue 使用基於 HTML 的模板語法（Template Syntax），將 Vue 實例中的數據綁定到模板中進行顯示。根據綁定的類型不同，數據可以影響元素的內容、屬性或樣式。","2024-09-19T20:39:46+08:00",[11,710,711,725],[11,713],3402,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10354231",{"name":717,"url":718,"day":31},{"path":962,"title":963,"description":964,"date":965,"updated":9,"tags":966,"categories":967,"draft":15,"wordCount":968,"readingTime":318,"canonical":969,"series":970},"\u002Fithome-2024-day-05","Vue 3 用實作帶你看過核心概念 - Day 5：Options API vs Composition API","前面已經詳細說明了 Vue 的環境建置方式，接下來將逐步介紹 Vue 應用說明，並比較Options API與Composition API的撰寫風格。","2024-09-18T22:10:36+08:00",[11,710,711,725,726],[11,713],2902,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10353717",{"name":717,"url":718,"day":122},{"path":972,"title":973,"description":974,"date":975,"updated":9,"tags":976,"categories":977,"draft":15,"wordCount":658,"readingTime":318,"canonical":978,"series":979},"\u002Fithome-2024-day-04","Vue 3 用實作帶你看過核心概念 - Day 4：基於 Vite 建構工具的 Vue 專案結構解析","在 Day 3 的教學中，學習了如何使用 Vue 鷹架工具(Vue project scaffolding tool)來生成一個基於 Vite 環境的專案。今天，我將更進一步地解析這個專案的資料夾結構，幫助大家更好地理解…","2024-09-17T23:27:07+08:00",[11,710,711,725],[11,713],"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10352470",{"name":717,"url":718,"day":134},{"path":981,"title":982,"description":983,"date":984,"updated":9,"tags":985,"categories":986,"draft":15,"wordCount":987,"readingTime":329,"canonical":988,"series":989},"\u002Fithome-2024-day-03","Vue 3 用實作帶你看過核心概念 - Day 3：使用 Vite 建構工具搭建 Vue 專案","⭐ 需要特別注意，需要事先安裝Nodejs。若不確定是否已安裝 Nodejs 也可以開啟終端機，執行以下指令確認版本。","2024-09-16T23:29:45+08:00",[11,710,711,725],[11,713],2100,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10351398",{"name":717,"url":718,"day":318},{"path":991,"title":992,"description":993,"date":994,"updated":9,"tags":995,"categories":996,"draft":15,"wordCount":997,"readingTime":329,"canonical":998,"series":999},"\u002Fithome-2024-day-02","Vue 3 用實作帶你看過核心概念 - Day 2：命令式編程 vs 聲明式編程","還記得我們在使用純 HTML 和 JavaScript 時，如何實現按下按鈕來變更文字顏色嗎？讓我們一起回想一下什麼是命令式宣告。","2024-09-15T22:52:27+08:00",[11,710,711,725,726],[11,713],1881,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10350459",{"name":717,"url":718,"day":329},{"path":1001,"title":1002,"description":1003,"date":1004,"updated":9,"tags":1005,"categories":1006,"draft":15,"wordCount":1007,"readingTime":340,"canonical":1008,"series":1009},"\u002Fithome-2024-day-01","Vue 3 用實作帶你看過核心概念 - Day 1：前言介紹","大家好，我是 Antonio(安東尼奧)，目前是一名 Angular 的新手工程師，同時，也是一名熱愛寫程式的工程師。去年參加六角學院的 Vue 直播班，在那時候就深深著迷著 Vue 的語言，自此這半年多的時間裡，我白天…","2024-09-14T17:32:19+08:00",[11,710,711],[11,713],988,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10350457",{"name":717,"url":718,"day":340},{"path":1011,"title":1012,"description":1013,"date":1014,"updated":9,"tags":1015,"categories":1017,"draft":15,"wordCount":1019,"readingTime":329,"canonical":9,"series":9},"\u002Fhexschool-experience","六角學院 - 軟體工程師體驗營","這麼多線上課程? 你不想知道什麼是最吸引我的線上學習課程嗎?","2024-05-31T21:40:05+08:00",[686,1016],"軟體工程師體驗營",[689,1018],"前端成長記",1109,{"path":1021,"title":1022,"description":1023,"date":1024,"updated":9,"tags":1025,"categories":1033,"draft":15,"wordCount":1035,"readingTime":318,"canonical":9,"series":9},"\u002FJS-10","JavaScript：var、let、const之間的差異解析","var、let及const是JavaScript中宣告變數的關鍵字，它們在作用域、對於for迴圈的影響、提升 (Hoisting) 的差異及重複宣告的處理上有所不同。","2023-12-10T23:53:30+08:00",[1026,1027,1028,1029,1030,1031,1032],"ES6","var","let","const","Hoisting","Scope","TDZ",[1034,702],"JavaScript",2873,{"path":1037,"title":1038,"description":1039,"date":1040,"updated":9,"tags":1041,"categories":1048,"draft":15,"wordCount":1049,"readingTime":122,"canonical":9,"series":9},"\u002FJS-9","JavaScript：陣列的常見操作方法","掌握 JavaScript 陣列操作！從forEach、map、filter、findIndex、reduce、sort，深入了解這些方法，使你的程式碼更具靈活性和效率！","2023-12-01T15:01:06+08:00",[1042,1043,1044,1045,1046,1047],"forEach","map","filter","reduce","sort","findIndex",[1034,702],4846,{"path":1051,"title":1052,"description":1053,"date":1054,"updated":9,"tags":1055,"categories":1060,"draft":15,"wordCount":1061,"readingTime":318,"canonical":9,"series":9},"\u002FJS-8","JavaScript：ES module 匯入與匯出","使用\u003C script >標籤讓瀏覽器中的網頁能夠使用 ES 模組功能時，你了解不同的匯入及匯出方法了嗎？","2023-11-30T15:28:23+08:00",[1026,1056,1057,1058,1059],"ES Module","CommonJS","Node.js","模組化開發",[1034,702],2913,{"path":1063,"title":1064,"description":1065,"date":1066,"updated":9,"tags":1067,"categories":1070,"draft":15,"wordCount":1072,"readingTime":134,"canonical":9,"series":9},"\u002FReact-2","React：JSX 是什麼？為何在 React 中使用 JSX？","透過 JSX 語法糖，你能夠在一次 JavaScript 撰寫中包含 HTML 與 CSS 的內容","2023-11-29T16:03:40+08:00",[1068,1034,1069],"JSX","Babel",[1071,702],"React",3595,{"path":1074,"title":1075,"description":1076,"date":1077,"updated":9,"tags":1078,"categories":1081,"draft":15,"wordCount":1082,"readingTime":329,"canonical":9,"series":9},"\u002FReact-1","React：Pure Function 的概念及其在 React 中的應用","探索 Pure Function 概念及其與 React 的相互關係。","2023-11-29T15:50:17+08:00",[1034,1079,1080],"Pure Function","Functional Programming",[1071],1368,{"path":1084,"title":1085,"description":1086,"date":1087,"updated":9,"tags":1088,"categories":1093,"draft":15,"wordCount":1094,"readingTime":122,"canonical":9,"series":9},"\u002FJS-6","JavaScript：深入理解Promise的原理及實際應用","透過Promise，我們能夠有效解決JavaScript中的異步事件，使得程式碼更為清晰和可讀。","2023-11-29T14:49:18+08:00",[1089,1090,1091,1092],"Promise","AsyncAwait","Asynchronous Programming","異步處理",[1034,702],5479,{"path":1096,"title":1097,"description":1098,"date":1099,"updated":9,"tags":1100,"categories":1105,"draft":15,"wordCount":1106,"readingTime":122,"canonical":9,"series":9},"\u002FJS-5","JavaScript：物件傳參考特性(Pass by Reference)","淺拷貝(shallow copy)與深拷貝(deep copy)在JavaScript中的應用與理解","2023-11-29T09:55:50+08:00",[1101,1102,1103,1104],"Pass by Reference","Shallow Copy","Deep Copy","傳值與傳參考",[1034,702],4544,{"path":1108,"title":1109,"description":1110,"date":1111,"updated":9,"tags":1112,"categories":1118,"draft":15,"wordCount":1120,"readingTime":329,"canonical":9,"series":9},"\u002FJS-4","JavaScript：注重關注點的分離","透過關注點分離的設計方式，可顯著提高系統整體的可維護性。","2023-11-29T09:44:29+08:00",[1113,1114,1115,1116,1117],"SoC","MVC","Design Patterns","Clean Code","Refactoring",[1034,1119],"Software Engineering",1977,{"path":1122,"title":1123,"description":1124,"date":1125,"updated":9,"tags":1126,"categories":1132,"draft":15,"wordCount":1133,"readingTime":329,"canonical":9,"series":9},"\u002FJS-3","JavaScript：掌握箭頭函式，優雅簡約的程式寫作","深入解析ES6箭頭函數：揭開與傳統函式的差異。","2023-11-29T01:02:36+08:00",[1127,1128,1129,1130,1131],"Arrow Function","this","Lexical Scoping","Syntactic Sugar","Callback Function",[1034,702],2142,{"path":1135,"title":1136,"description":1137,"date":1138,"updated":9,"tags":1139,"categories":1143,"draft":15,"wordCount":1144,"readingTime":329,"canonical":9,"series":9},"\u002FJS-2","JavaScript：深入了解This的指向與應用","探索JavaScript This 的指向：深入剖析判斷的技巧。","2023-11-28T22:22:45+08:00",[1128,1140,1131,1141,1142],"Execution Context","Binding","Fundamentals",[1034,702],1963,{"path":1146,"title":1147,"description":1148,"date":1149,"updated":9,"tags":1150,"categories":1154,"draft":15,"wordCount":1155,"readingTime":340,"canonical":9,"series":9},"\u002FJS-1","JavaScript：淺談ES6常見縮寫","JavaScript ES6縮寫技巧：輕鬆簡化程式碼，提升效率！。","2023-11-28T13:38:14+08:00",[1026,1151,1152,1153],"Shorthand","Spread Operator","Object",[1034,702],818,{"path":1157,"title":1158,"description":1159,"date":1160,"updated":9,"tags":1161,"categories":1165,"draft":15,"wordCount":1166,"readingTime":340,"canonical":9,"series":9},"\u002FEmmet-1","前端開發神器：使用Emmet提升網頁開發效率","本文將介紹如何利用Emmet來大幅提高前端開發效率，讓你的編碼速度如專家般飛快。","2023-10-31T14:40:54+08:00",[1162,1163,1164],"Emmet","VSCode","HTML",[702,673],865,{"path":1168,"title":1169,"description":1170,"date":1171,"updated":9,"tags":1172,"categories":1177,"draft":15,"wordCount":1179,"readingTime":340,"canonical":9,"series":9},"\u002Fhexo-github-4","建立自己的部落格：了解Hexo指令，更換next部落格主題","今天的你跟我都是Hexo指令指揮官，讓我們將部落格造型大改造一下吧!!!","2023-10-20T14:33:38+08:00",[1173,1174,1175,1176],"Hexo","NexT","git clone","Hexo指令",[1178],"Blog",979,{"path":1181,"title":1182,"description":1183,"date":1184,"updated":9,"tags":1185,"categories":1189,"draft":15,"wordCount":1190,"readingTime":329,"canonical":9,"series":9},"\u002Fhexo-github-3","建立自己的部落格：修改Hexo設定檔中Site，查看其變化","今天的你跟我都是部落格工程師，我們一起來將網頁做一些小小的改變，有一天匯集在一起就會讓你的部落格變的與眾不同。","2023-10-18T20:50:31+08:00",[1173,1163,1186,1187,1188],"Sublime Text","YAML","SEO",[1178],1798,{"path":1192,"title":1193,"description":1194,"date":1195,"updated":9,"tags":1196,"categories":1199,"draft":15,"wordCount":1200,"readingTime":340,"canonical":9,"series":9},"\u002Fhexo-github-2","建立自己的部落格：創建Hexo Blog初始的模板","踏出那關鍵的第一步，從白紙到完整的部落格，一同見證你的部落格誕生的奇妙時刻。","2023-10-17T15:59:37+08:00",[1173,1197,1198],"Hexo-cli","hexo init",[1178],941,{"path":1202,"title":1203,"description":1204,"date":1205,"updated":9,"tags":1206,"categories":1208,"draft":15,"wordCount":1209,"readingTime":340,"canonical":9,"series":9},"\u002Fhexo-github-1","建立自己的部落格：認識Hexo及所其需工具(Nodejs & Git)","打造屬於自己風格的部落格其實不難，讓我們一步步手把手教你如何實現這個目標。","2023-10-16T08:30:27+08:00",[1173,1058,1207],"Git",[1178],790,{"id":1211,"title":370,"body":1212,"canonical":378,"categories":3797,"date":372,"description":371,"draft":15,"extension":3798,"meta":3799,"navigation":1508,"ogImage":9,"path":369,"rawbody":3800,"readingTime":31,"robots":9,"schemaOrg":9,"seo":3801,"series":3802,"sitemap":3803,"slug":3815,"stem":3816,"tags":3817,"updated":9,"wordCount":377,"__hash__":3818},"posts\u002F2025-10-12-ithome-2025-day-29.md",{"type":1213,"value":1214,"toc":3783},"minimark",[1215,1223,1227,1243,1246,1249,1252,1257,1263,1269,1279,1284,1287,1293,1298,1301,1307,1312,1315,1321,1324,1330,1333,1338,1347,1350,1355,2112,2115,2118,2129,2132,2668,2673,2679,2682,2686,2689,2693,2699,2704,2709,3087,3090,3099,3103,3125,3131,3136,3141,3297,3301,3310,3313,3318,3326,3337,3348,3353,3358,3363,3759,3770,3773,3776,3779],[1216,1217,1218],"p",{},[1219,1220],"img",{"alt":1221,"src":1222},"2025 鐵人賽背景圖","https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1760280552\u002F2025%20IT%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay29%EF%BC%9ALIFF%20%E6%9C%83%E5%93%A1%E8%A8%BB%E5%86%8A%20-%20%E4%BF%A1%E7%AE%B1%E9%A9%97%E8%AD%89%E8%88%87%20Swagger%20%E6%96%87%E4%BB%B6%E6%95%B4%E5%90%88\u002F2025_%E9%90%B5%E4%BA%BA%E8%B3%BD%E4%BD%BF%E7%94%A8_27_ykepqs.png",[1224,1225,1226],"h2",{"id":1226},"前言",[1216,1228,1229,1230,1234,1235,1238,1239,1242],{},"延續 ",[1231,1232,1233],"code",{},"Day 28"," 會員註冊的流程，在實際的會員註冊場景中，電子信箱驗證是確保用戶身份真實性的重要環節。本篇將基於昨天的註冊流程，新增兩個核心 API：",[1231,1236,1237],{},"發送驗證信","與",[1231,1240,1241],{},"驗證驗證碼","，讓會員註冊功能更加完整且安全。",[1216,1244,1245],{},"此外，今天也會介紹 NestJS Swagger 的實戰應用。透過裝飾器自動生成 API 文件，不僅能提升開發效率，更能讓前後端協作更加順暢，降低溝通成本。",[1224,1247,1248],{"id":1248},"信箱服務前置作業",[1216,1250,1251],{},"我們使用 nodemailer 搭配 Google SMTP 服務來發送驗證信件。",[1253,1254,1256],"h3",{"id":1255},"google-帳號設定步驟","Google 帳號設定步驟",[1258,1259,1260],"blockquote",{},[1216,1261,1262],{},"Gmail 免費版發信限制：每天最多可發送給 500 位收件者",[1216,1264,1265],{},[1266,1267,1268],"strong",{},"在開始實作之前，需要先完成以下 Google 帳號設定：：",[1270,1271,1272,1276],"ol",{},[1273,1274,1275],"li",{},"啟用兩步驟驗證：前往 Google 帳號安全性設定，啟用兩步驟驗證功能",[1273,1277,1278],{},"建立應用程式密碼：在啟用兩步驟驗證後，前往「應用程式密碼」頁面產生專用密碼",[1216,1280,1281],{},[1266,1282,1283],{},"Step 1：進入 Google 帳號設定",[1216,1285,1286],{},"點選右上角的個人頭像，選擇「管理你的 Google 帳戶」",[1216,1288,1289],{},[1219,1290],{"alt":1291,"src":1292},"Google 信箱個人設定畫面","https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1760280903\u002F2025%20IT%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay29%EF%BC%9ALIFF%20%E6%9C%83%E5%93%A1%E8%A8%BB%E5%86%8A%20-%20%E4%BF%A1%E7%AE%B1%E9%A9%97%E8%AD%89%E8%88%87%20Swagger%20%E6%96%87%E4%BB%B6%E6%95%B4%E5%90%88\u002FGoogle_%E4%BF%A1%E7%AE%B1%E5%80%8B%E4%BA%BA%E8%A8%AD%E5%AE%9A%E7%95%AB%E9%9D%A2_gfic5z.jpg",[1216,1294,1295],{},[1266,1296,1297],{},"Step 2：確認兩步驟驗證狀態",[1216,1299,1300],{},"前往「安全性」頁面，確認「兩步驟驗證」已啟用。若尚未啟用，請先完成兩步驟驗證設定，才能建立應用程式密碼。",[1216,1302,1303],{},[1219,1304],{"alt":1305,"src":1306},"Gmail 信箱二階段驗證位置","https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1760280960\u002F2025%20IT%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay29%EF%BC%9ALIFF%20%E6%9C%83%E5%93%A1%E8%A8%BB%E5%86%8A%20-%20%E4%BF%A1%E7%AE%B1%E9%A9%97%E8%AD%89%E8%88%87%20Swagger%20%E6%96%87%E4%BB%B6%E6%95%B4%E5%90%88\u002F%E4%BF%A1%E7%AE%B1%E4%BA%8C%E9%9A%8E%E6%AE%B5%E9%A9%97%E8%AD%89%E4%BD%8D%E7%BD%AE_ham7kb.png",[1216,1308,1309],{},[1266,1310,1311],{},"Step 3：建立應用程式密碼",[1216,1313,1314],{},"在搜尋框輸入「應用程式密碼」可快速找到設定頁面",[1216,1316,1317],{},[1219,1318],{"alt":1319,"src":1320},"Google 帳戶搜尋應用畫面","https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1760281025\u002F2025%20IT%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay29%EF%BC%9ALIFF%20%E6%9C%83%E5%93%A1%E8%A8%BB%E5%86%8A%20-%20%E4%BF%A1%E7%AE%B1%E9%A9%97%E8%AD%89%E8%88%87%20Swagger%20%E6%96%87%E4%BB%B6%E6%95%B4%E5%90%88\u002FGoogle_%E5%B8%B3%E6%88%B6%E6%90%9C%E5%B0%8B%E6%87%89%E7%94%A8%E7%95%AB%E9%9D%A2_mywu4z.png",[1216,1322,1323],{},"建立完成後會顯示一組 16 碼密碼，請立即複製保存。應用程式密碼只會顯示一次，無法再次查看。若遺失請刪除後重新建立。",[1216,1325,1326],{},[1219,1327],{"alt":1328,"src":1329},"Google 應用程式密碼","https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1760281098\u002F2025%20IT%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay29%EF%BC%9ALIFF%20%E6%9C%83%E5%93%A1%E8%A8%BB%E5%86%8A%20-%20%E4%BF%A1%E7%AE%B1%E9%A9%97%E8%AD%89%E8%88%87%20Swagger%20%E6%96%87%E4%BB%B6%E6%95%B4%E5%90%88\u002FGoogle_%E6%87%89%E7%94%A8%E7%A8%8B%E5%BC%8F%E5%AF%86%E7%A2%BC_t1bqng.jpg",[1224,1331,1332],{"id":1332},"信箱服務模組建立",[1216,1334,1335],{},[1266,1336,1337],{},"核心功能",[1339,1340,1341,1344],"ul",{},[1273,1342,1343],{},"sendVerificationEmail：發送驗證信件的方法，接收目標信箱地址與驗證碼作為參數，用於寄送會員註冊驗證信",[1273,1345,1346],{},"generateVerificationCode：生成隨機驗證碼的方法，產生指定長度（預設六位數）的數字驗證碼，供其他服務模組使用",[1216,1348,1349],{},"這兩個方法將作為可重用的服務，讓其他模組（如會員註冊模組）能夠整合信箱驗證功能。",[1216,1351,1352],{},[1266,1353,1354],{},"mail\u002Fmail.service.ts",[1356,1357,1362],"pre",{"className":1358,"code":1359,"language":1360,"meta":1361,"style":1361},"language-ts shiki shiki-themes github-light github-dark-dimmed","\u002F\u002F 略\n@Injectable()\nexport class MailService {\n  \u002F\u002F 略\n  private createTransporter() {\n    \u002F\u002F 使用 Gmail SMTP 設定\n    this.transporter = nodemailer.createTransport({\n      service: 'gmail',\n      auth: {\n        user: this.EMAIL_USER,\n        pass: this.EMAIL_PASSWORD,\n      },\n    });\n  }\n\n  \u002F**\n   * 發送驗證信\n   *\u002F\n  async sendVerificationEmail(\n    to: string,\n    verificationCode: string,\n    userName: string,\n  ): Promise\u003Cvoid> {\n    try {\n      const mailOptions = {\n        from: this.EMAIL_USER,\n        to,\n        subject: '帳號驗證 - LINE Bot 應用程式',\n        html: this.getVerificationEmailTemplate(verificationCode, userName),\n      };\n\n      const result = await this.transporter.sendMail(mailOptions);\n      this.logger.info(`驗證信發送成功: ${result.messageId}`);\n    } catch (error) {\n      this.logger.error(`發送驗證信失敗: ${error.message}`);\n      throw new Error('發送驗證信失敗');\n    }\n  }\n\n  \u002F**\n   * 生成驗證信的 HTML 模板\n   *\u002F\n  private getVerificationEmailTemplate(\n    verificationCode: string,\n    userName: string,\n  ): string {\n    return `\n      \u003C!DOCTYPE html>\n      \u003Chtml>\n          \u003Cbody>\n            \u003Cdiv class=\"container\">\n              \u003Cdiv class=\"header\">\n                \u003Ch1>帳號驗證\u003C\u002Fh1>\n              \u003C\u002Fdiv>\n              \u003Cdiv class=\"content\">\n                \u003Ch2>親愛的 ${userName}，\u003C\u002Fh2>\n                \u003Cp>感謝您註冊我們的 LINE Bot 應用程式！\u003C\u002Fp>\n                \u003Cp>請使用以下驗證碼完成您的帳號驗證：\u003C\u002Fp>\n                \u003Cdiv class=\"verification-code\">\n                  ${verificationCode}\n                \u003C\u002Fdiv>\n                \u003Cp>此驗證碼將在 \u003Cstrong>10 分鐘\u003C\u002Fstrong> 後失效。\u003C\u002Fp>\n                \u003Cp>如果您沒有註冊此帳號，請忽略此信件。\u003C\u002Fp>\n              \u003C\u002Fdiv>\n              \u003Cdiv class=\"footer\">\n                \u003Cp>此信件由系統自動發送，請勿回覆。\u003C\u002Fp>\n              \u003C\u002Fdiv>\n            \u003C\u002Fdiv>\n          \u003C\u002Fbody>\n      \u003C\u002Fhtml>\n    `;\n  }\n\n  \u002F**\n   * 生成 6 位數驗證碼\n   *\u002F\n  generateVerificationCode(): string {\n    return Math.floor(100000 + Math.random() * 900000).toString();\n  }\n}\n","ts","",[1231,1363,1364,1372,1385,1401,1406,1417,1422,1443,1455,1460,1475,1489,1494,1499,1504,1510,1515,1520,1525,1536,1550,1561,1572,1591,1598,1611,1624,1629,1639,1654,1659,1664,1689,1721,1733,1759,1778,1784,1789,1794,1799,1805,1810,1820,1831,1842,1853,1862,1868,1874,1880,1886,1892,1898,1904,1910,1922,1928,1934,1940,1952,1958,1964,1970,1975,1981,1987,1992,1998,2004,2010,2019,2024,2029,2034,2040,2045,2060,2102,2107],{"__ignoreMap":1361},[1365,1366,1368],"span",{"class":1367,"line":340},"line",[1365,1369,1371],{"class":1370},"sDN9O","\u002F\u002F 略\n",[1365,1373,1374,1378,1382],{"class":1367,"line":329},[1365,1375,1377],{"class":1376},"sYgZi","@",[1365,1379,1381],{"class":1380},"sPO5f","Injectable",[1365,1383,1384],{"class":1376},"()\n",[1365,1386,1387,1391,1394,1398],{"class":1367,"line":318},[1365,1388,1390],{"class":1389},"s7YZ4","export",[1365,1392,1393],{"class":1389}," class",[1365,1395,1397],{"class":1396},"sOLd2"," MailService",[1365,1399,1400],{"class":1376}," {\n",[1365,1402,1403],{"class":1367,"line":134},[1365,1404,1405],{"class":1370},"  \u002F\u002F 略\n",[1365,1407,1408,1411,1414],{"class":1367,"line":122},[1365,1409,1410],{"class":1389},"  private",[1365,1412,1413],{"class":1380}," createTransporter",[1365,1415,1416],{"class":1376},"() {\n",[1365,1418,1419],{"class":1367,"line":31},[1365,1420,1421],{"class":1370},"    \u002F\u002F 使用 Gmail SMTP 設定\n",[1365,1423,1424,1428,1431,1434,1437,1440],{"class":1367,"line":17},[1365,1425,1427],{"class":1426},"sviXB","    this",[1365,1429,1430],{"class":1376},".transporter ",[1365,1432,1433],{"class":1389},"=",[1365,1435,1436],{"class":1376}," nodemailer.",[1365,1438,1439],{"class":1380},"createTransport",[1365,1441,1442],{"class":1376},"({\n",[1365,1444,1445,1448,1452],{"class":1367,"line":110},[1365,1446,1447],{"class":1376},"      service: ",[1365,1449,1451],{"class":1450},"s-HuK","'gmail'",[1365,1453,1454],{"class":1376},",\n",[1365,1456,1457],{"class":1367,"line":76},[1365,1458,1459],{"class":1376},"      auth: {\n",[1365,1461,1462,1465,1467,1470,1473],{"class":1367,"line":247},[1365,1463,1464],{"class":1376},"        user: ",[1365,1466,1128],{"class":1426},[1365,1468,1469],{"class":1376},".",[1365,1471,1472],{"class":1426},"EMAIL_USER",[1365,1474,1454],{"class":1376},[1365,1476,1477,1480,1482,1484,1487],{"class":1367,"line":236},[1365,1478,1479],{"class":1376},"        pass: ",[1365,1481,1128],{"class":1426},[1365,1483,1469],{"class":1376},[1365,1485,1486],{"class":1426},"EMAIL_PASSWORD",[1365,1488,1454],{"class":1376},[1365,1490,1491],{"class":1367,"line":225},[1365,1492,1493],{"class":1376},"      },\n",[1365,1495,1496],{"class":1367,"line":214},[1365,1497,1498],{"class":1376},"    });\n",[1365,1500,1501],{"class":1367,"line":203},[1365,1502,1503],{"class":1376},"  }\n",[1365,1505,1506],{"class":1367,"line":192},[1365,1507,1509],{"emptyLinePlaceholder":1508},true,"\n",[1365,1511,1512],{"class":1367,"line":181},[1365,1513,1514],{"class":1370},"  \u002F**\n",[1365,1516,1517],{"class":1367,"line":170},[1365,1518,1519],{"class":1370},"   * 發送驗證信\n",[1365,1521,1522],{"class":1367,"line":159},[1365,1523,1524],{"class":1370},"   *\u002F\n",[1365,1526,1527,1530,1533],{"class":1367,"line":148},[1365,1528,1529],{"class":1389},"  async",[1365,1531,1532],{"class":1380}," sendVerificationEmail",[1365,1534,1535],{"class":1376},"(\n",[1365,1537,1538,1542,1545,1548],{"class":1367,"line":137},[1365,1539,1541],{"class":1540},"stnAF","    to",[1365,1543,1544],{"class":1389},":",[1365,1546,1547],{"class":1426}," string",[1365,1549,1454],{"class":1376},[1365,1551,1552,1555,1557,1559],{"class":1367,"line":125},[1365,1553,1554],{"class":1540},"    verificationCode",[1365,1556,1544],{"class":1389},[1365,1558,1547],{"class":1426},[1365,1560,1454],{"class":1376},[1365,1562,1563,1566,1568,1570],{"class":1367,"line":113},[1365,1564,1565],{"class":1540},"    userName",[1365,1567,1544],{"class":1389},[1365,1569,1547],{"class":1426},[1365,1571,1454],{"class":1376},[1365,1573,1574,1577,1579,1582,1585,1588],{"class":1367,"line":101},[1365,1575,1576],{"class":1376},"  )",[1365,1578,1544],{"class":1389},[1365,1580,1581],{"class":1396}," Promise",[1365,1583,1584],{"class":1376},"\u003C",[1365,1586,1587],{"class":1426},"void",[1365,1589,1590],{"class":1376},"> {\n",[1365,1592,1593,1596],{"class":1367,"line":90},[1365,1594,1595],{"class":1389},"    try",[1365,1597,1400],{"class":1376},[1365,1599,1600,1603,1606,1609],{"class":1367,"line":79},[1365,1601,1602],{"class":1389},"      const",[1365,1604,1605],{"class":1426}," mailOptions",[1365,1607,1608],{"class":1389}," =",[1365,1610,1400],{"class":1376},[1365,1612,1613,1616,1618,1620,1622],{"class":1367,"line":67},[1365,1614,1615],{"class":1376},"        from: ",[1365,1617,1128],{"class":1426},[1365,1619,1469],{"class":1376},[1365,1621,1472],{"class":1426},[1365,1623,1454],{"class":1376},[1365,1625,1626],{"class":1367,"line":56},[1365,1627,1628],{"class":1376},"        to,\n",[1365,1630,1631,1634,1637],{"class":1367,"line":45},[1365,1632,1633],{"class":1376},"        subject: ",[1365,1635,1636],{"class":1450},"'帳號驗證 - LINE Bot 應用程式'",[1365,1638,1454],{"class":1376},[1365,1640,1641,1644,1646,1648,1651],{"class":1367,"line":34},[1365,1642,1643],{"class":1376},"        html: ",[1365,1645,1128],{"class":1426},[1365,1647,1469],{"class":1376},[1365,1649,1650],{"class":1380},"getVerificationEmailTemplate",[1365,1652,1653],{"class":1376},"(verificationCode, userName),\n",[1365,1655,1656],{"class":1367,"line":22},[1365,1657,1658],{"class":1376},"      };\n",[1365,1660,1662],{"class":1367,"line":1661},31,[1365,1663,1509],{"emptyLinePlaceholder":1508},[1365,1665,1667,1669,1672,1674,1677,1680,1683,1686],{"class":1367,"line":1666},32,[1365,1668,1602],{"class":1389},[1365,1670,1671],{"class":1426}," result",[1365,1673,1608],{"class":1389},[1365,1675,1676],{"class":1389}," await",[1365,1678,1679],{"class":1426}," this",[1365,1681,1682],{"class":1376},".transporter.",[1365,1684,1685],{"class":1380},"sendMail",[1365,1687,1688],{"class":1376},"(mailOptions);\n",[1365,1690,1692,1695,1698,1701,1704,1707,1710,1712,1715,1718],{"class":1367,"line":1691},33,[1365,1693,1694],{"class":1426},"      this",[1365,1696,1697],{"class":1376},".logger.",[1365,1699,1700],{"class":1380},"info",[1365,1702,1703],{"class":1376},"(",[1365,1705,1706],{"class":1450},"`驗證信發送成功: ${",[1365,1708,1709],{"class":1376},"result",[1365,1711,1469],{"class":1450},[1365,1713,1714],{"class":1376},"messageId",[1365,1716,1717],{"class":1450},"}`",[1365,1719,1720],{"class":1376},");\n",[1365,1722,1724,1727,1730],{"class":1367,"line":1723},34,[1365,1725,1726],{"class":1376},"    } ",[1365,1728,1729],{"class":1389},"catch",[1365,1731,1732],{"class":1376}," (error) {\n",[1365,1734,1736,1738,1740,1743,1745,1748,1750,1752,1755,1757],{"class":1367,"line":1735},35,[1365,1737,1694],{"class":1426},[1365,1739,1697],{"class":1376},[1365,1741,1742],{"class":1380},"error",[1365,1744,1703],{"class":1376},[1365,1746,1747],{"class":1450},"`發送驗證信失敗: ${",[1365,1749,1742],{"class":1376},[1365,1751,1469],{"class":1450},[1365,1753,1754],{"class":1376},"message",[1365,1756,1717],{"class":1450},[1365,1758,1720],{"class":1376},[1365,1760,1762,1765,1768,1771,1773,1776],{"class":1367,"line":1761},36,[1365,1763,1764],{"class":1389},"      throw",[1365,1766,1767],{"class":1389}," new",[1365,1769,1770],{"class":1380}," Error",[1365,1772,1703],{"class":1376},[1365,1774,1775],{"class":1450},"'發送驗證信失敗'",[1365,1777,1720],{"class":1376},[1365,1779,1781],{"class":1367,"line":1780},37,[1365,1782,1783],{"class":1376},"    }\n",[1365,1785,1787],{"class":1367,"line":1786},38,[1365,1788,1503],{"class":1376},[1365,1790,1792],{"class":1367,"line":1791},39,[1365,1793,1509],{"emptyLinePlaceholder":1508},[1365,1795,1797],{"class":1367,"line":1796},40,[1365,1798,1514],{"class":1370},[1365,1800,1802],{"class":1367,"line":1801},41,[1365,1803,1804],{"class":1370},"   * 生成驗證信的 HTML 模板\n",[1365,1806,1808],{"class":1367,"line":1807},42,[1365,1809,1524],{"class":1370},[1365,1811,1813,1815,1818],{"class":1367,"line":1812},43,[1365,1814,1410],{"class":1389},[1365,1816,1817],{"class":1380}," getVerificationEmailTemplate",[1365,1819,1535],{"class":1376},[1365,1821,1823,1825,1827,1829],{"class":1367,"line":1822},44,[1365,1824,1554],{"class":1540},[1365,1826,1544],{"class":1389},[1365,1828,1547],{"class":1426},[1365,1830,1454],{"class":1376},[1365,1832,1834,1836,1838,1840],{"class":1367,"line":1833},45,[1365,1835,1565],{"class":1540},[1365,1837,1544],{"class":1389},[1365,1839,1547],{"class":1426},[1365,1841,1454],{"class":1376},[1365,1843,1845,1847,1849,1851],{"class":1367,"line":1844},46,[1365,1846,1576],{"class":1376},[1365,1848,1544],{"class":1389},[1365,1850,1547],{"class":1426},[1365,1852,1400],{"class":1376},[1365,1854,1856,1859],{"class":1367,"line":1855},47,[1365,1857,1858],{"class":1389},"    return",[1365,1860,1861],{"class":1450}," `\n",[1365,1863,1865],{"class":1367,"line":1864},48,[1365,1866,1867],{"class":1450},"      \u003C!DOCTYPE html>\n",[1365,1869,1871],{"class":1367,"line":1870},49,[1365,1872,1873],{"class":1450},"      \u003Chtml>\n",[1365,1875,1877],{"class":1367,"line":1876},50,[1365,1878,1879],{"class":1450},"          \u003Cbody>\n",[1365,1881,1883],{"class":1367,"line":1882},51,[1365,1884,1885],{"class":1450},"            \u003Cdiv class=\"container\">\n",[1365,1887,1889],{"class":1367,"line":1888},52,[1365,1890,1891],{"class":1450},"              \u003Cdiv class=\"header\">\n",[1365,1893,1895],{"class":1367,"line":1894},53,[1365,1896,1897],{"class":1450},"                \u003Ch1>帳號驗證\u003C\u002Fh1>\n",[1365,1899,1901],{"class":1367,"line":1900},54,[1365,1902,1903],{"class":1450},"              \u003C\u002Fdiv>\n",[1365,1905,1907],{"class":1367,"line":1906},55,[1365,1908,1909],{"class":1450},"              \u003Cdiv class=\"content\">\n",[1365,1911,1913,1916,1919],{"class":1367,"line":1912},56,[1365,1914,1915],{"class":1450},"                \u003Ch2>親愛的 ${",[1365,1917,1918],{"class":1376},"userName",[1365,1920,1921],{"class":1450},"}，\u003C\u002Fh2>\n",[1365,1923,1925],{"class":1367,"line":1924},57,[1365,1926,1927],{"class":1450},"                \u003Cp>感謝您註冊我們的 LINE Bot 應用程式！\u003C\u002Fp>\n",[1365,1929,1931],{"class":1367,"line":1930},58,[1365,1932,1933],{"class":1450},"                \u003Cp>請使用以下驗證碼完成您的帳號驗證：\u003C\u002Fp>\n",[1365,1935,1937],{"class":1367,"line":1936},59,[1365,1938,1939],{"class":1450},"                \u003Cdiv class=\"verification-code\">\n",[1365,1941,1943,1946,1949],{"class":1367,"line":1942},60,[1365,1944,1945],{"class":1450},"                  ${",[1365,1947,1948],{"class":1376},"verificationCode",[1365,1950,1951],{"class":1450},"}\n",[1365,1953,1955],{"class":1367,"line":1954},61,[1365,1956,1957],{"class":1450},"                \u003C\u002Fdiv>\n",[1365,1959,1961],{"class":1367,"line":1960},62,[1365,1962,1963],{"class":1450},"                \u003Cp>此驗證碼將在 \u003Cstrong>10 分鐘\u003C\u002Fstrong> 後失效。\u003C\u002Fp>\n",[1365,1965,1967],{"class":1367,"line":1966},63,[1365,1968,1969],{"class":1450},"                \u003Cp>如果您沒有註冊此帳號，請忽略此信件。\u003C\u002Fp>\n",[1365,1971,1973],{"class":1367,"line":1972},64,[1365,1974,1903],{"class":1450},[1365,1976,1978],{"class":1367,"line":1977},65,[1365,1979,1980],{"class":1450},"              \u003Cdiv class=\"footer\">\n",[1365,1982,1984],{"class":1367,"line":1983},66,[1365,1985,1986],{"class":1450},"                \u003Cp>此信件由系統自動發送，請勿回覆。\u003C\u002Fp>\n",[1365,1988,1990],{"class":1367,"line":1989},67,[1365,1991,1903],{"class":1450},[1365,1993,1995],{"class":1367,"line":1994},68,[1365,1996,1997],{"class":1450},"            \u003C\u002Fdiv>\n",[1365,1999,2001],{"class":1367,"line":2000},69,[1365,2002,2003],{"class":1450},"          \u003C\u002Fbody>\n",[1365,2005,2007],{"class":1367,"line":2006},70,[1365,2008,2009],{"class":1450},"      \u003C\u002Fhtml>\n",[1365,2011,2013,2016],{"class":1367,"line":2012},71,[1365,2014,2015],{"class":1450},"    `",[1365,2017,2018],{"class":1376},";\n",[1365,2020,2022],{"class":1367,"line":2021},72,[1365,2023,1503],{"class":1376},[1365,2025,2027],{"class":1367,"line":2026},73,[1365,2028,1509],{"emptyLinePlaceholder":1508},[1365,2030,2032],{"class":1367,"line":2031},74,[1365,2033,1514],{"class":1370},[1365,2035,2037],{"class":1367,"line":2036},75,[1365,2038,2039],{"class":1370},"   * 生成 6 位數驗證碼\n",[1365,2041,2043],{"class":1367,"line":2042},76,[1365,2044,1524],{"class":1370},[1365,2046,2048,2051,2054,2056,2058],{"class":1367,"line":2047},77,[1365,2049,2050],{"class":1380},"  generateVerificationCode",[1365,2052,2053],{"class":1376},"()",[1365,2055,1544],{"class":1389},[1365,2057,1547],{"class":1426},[1365,2059,1400],{"class":1376},[1365,2061,2063,2065,2068,2071,2073,2076,2079,2081,2084,2087,2090,2093,2096,2099],{"class":1367,"line":2062},78,[1365,2064,1858],{"class":1389},[1365,2066,2067],{"class":1376}," Math.",[1365,2069,2070],{"class":1380},"floor",[1365,2072,1703],{"class":1376},[1365,2074,2075],{"class":1426},"100000",[1365,2077,2078],{"class":1389}," +",[1365,2080,2067],{"class":1376},[1365,2082,2083],{"class":1380},"random",[1365,2085,2086],{"class":1376},"() ",[1365,2088,2089],{"class":1389},"*",[1365,2091,2092],{"class":1426}," 900000",[1365,2094,2095],{"class":1376},").",[1365,2097,2098],{"class":1380},"toString",[1365,2100,2101],{"class":1376},"();\n",[1365,2103,2105],{"class":1367,"line":2104},79,[1365,2106,1503],{"class":1376},[1365,2108,2110],{"class":1367,"line":2109},80,[1365,2111,1951],{"class":1376},[1224,2113,2114],{"id":2114},"用戶模組新增信件發送及驗證碼驗證服務",[1216,2116,2117],{},"以下展示 LIFF 前端發送驗證碼時呼叫的後端服務。當用戶在前端註冊表單通過 vee-validate 驗證後，系統會執行以下流程：",[1270,2119,2120,2123,2126],{},[1273,2121,2122],{},"發送驗證信：將驗證碼寄送至用戶註冊的信箱",[1273,2124,2125],{},"儲存驗證碼：將驗證碼及相關資訊記錄至資料庫",[1273,2127,2128],{},"設定有效期限：為驗證碼設置過期時間（例如 10 分鐘）",[1216,2130,2131],{},"用戶必須在有效期限內，輸入信箱中收到的驗證碼，才能完成註冊流程。若驗證碼過期，則需重新發送。",[1356,2133,2135],{"className":1358,"code":2134,"language":1360,"meta":1361,"style":1361},"  async sendVerificationEmail(\n    sendVerificationEmailDto: SendVerificationEmailDto,\n  ): Promise\u003C{ message: string }> {\n    const { idToken, email, userName } = sendVerificationEmailDto;\n    const supabase = this.supabaseService.db;\n\n    try {\n      \u002F\u002F 驗證 LINE ID Token 並獲取 LINE User ID\n      const verifyResult = await this.lineLoginService.verifyIDToken(idToken);\n      const lineUserId = verifyResult.sub;\n\n      this.logger.info(`LINE Token 驗證成功，使用者 ID: ${lineUserId}`);\n\n      \u002F\u002F 生成驗證碼\n      const verificationCode = this.mailService.generateVerificationCode();\n\n      \u002F\u002F 設定過期時間（15分鐘後過期）\n      const expiresAt = new Date();\n      expiresAt.setMinutes(expiresAt.getMinutes() + 15);\n\n      \u002F\u002F 將驗證碼存入資料庫，使用 LINE User ID\n      const { error: insertError } = await supabase\n        .from('verification_codes')\n        .insert([\n          {\n            user_id: lineUserId,\n            code: verificationCode,\n            expires_at: expiresAt.toISOString(),\n            is_used: false,\n          },\n        ]);\n\n      if (insertError) {\n        this.logger.error(`儲存驗證碼失敗: ${insertError.message}`);\n        throw new Error(`儲存驗證碼失敗: ${insertError.message}`);\n      }\n\n      \u002F\u002F 發送驗證信\n      await this.mailService.sendVerificationEmail(\n        email,\n        verificationCode,\n        userName,\n      );\n\n      this.logger.info(\n        `驗證信已發送至: ${email}, 驗證碼已儲存至資料庫，LINE User ID: ${lineUserId}`,\n      );\n\n      return {\n        message: '驗證信已發送成功',\n      };\n    } catch (error) {\n      this.logger.error(`發送驗證信失敗: ${error.message}`);\n      throw new Error(`發送驗證信失敗: ${error.message}`);\n    }\n  }\n",[1231,2136,2137,2147,2152,2174,2179,2191,2195,2200,2205,2227,2239,2243,2263,2267,2272,2291,2295,2300,2316,2340,2344,2349,2374,2390,2400,2405,2410,2415,2426,2436,2441,2446,2450,2458,2482,2505,2510,2514,2519,2532,2537,2542,2547,2552,2556,2566,2583,2587,2591,2598,2608,2612,2617,2638,2660,2664],{"__ignoreMap":1361},[1365,2138,2139,2142,2145],{"class":1367,"line":340},[1365,2140,2141],{"class":1376},"  async ",[1365,2143,2144],{"class":1380},"sendVerificationEmail",[1365,2146,1535],{"class":1376},[1365,2148,2149],{"class":1367,"line":329},[1365,2150,2151],{"class":1376},"    sendVerificationEmailDto: SendVerificationEmailDto,\n",[1365,2153,2154,2157,2159,2161,2164,2166,2169,2172],{"class":1367,"line":318},[1365,2155,2156],{"class":1376},"  ): ",[1365,2158,1089],{"class":1426},[1365,2160,1584],{"class":1389},[1365,2162,2163],{"class":1376},"{ ",[1365,2165,1754],{"class":1396},[1365,2167,2168],{"class":1376},": string }",[1365,2170,2171],{"class":1389},">",[1365,2173,1400],{"class":1376},[1365,2175,2176],{"class":1367,"line":134},[1365,2177,2178],{"class":1376},"    const { idToken, email, userName } = sendVerificationEmailDto;\n",[1365,2180,2181,2184,2186,2188],{"class":1367,"line":122},[1365,2182,2183],{"class":1376},"    const supabase ",[1365,2185,1433],{"class":1389},[1365,2187,1679],{"class":1426},[1365,2189,2190],{"class":1376},".supabaseService.db;\n",[1365,2192,2193],{"class":1367,"line":31},[1365,2194,1509],{"emptyLinePlaceholder":1508},[1365,2196,2197],{"class":1367,"line":17},[1365,2198,2199],{"class":1376},"    try {\n",[1365,2201,2202],{"class":1367,"line":110},[1365,2203,2204],{"class":1370},"      \u002F\u002F 驗證 LINE ID Token 並獲取 LINE User ID\n",[1365,2206,2207,2209,2212,2214,2216,2218,2221,2224],{"class":1367,"line":76},[1365,2208,1602],{"class":1389},[1365,2210,2211],{"class":1426}," verifyResult",[1365,2213,1608],{"class":1389},[1365,2215,1676],{"class":1389},[1365,2217,1679],{"class":1426},[1365,2219,2220],{"class":1376},".lineLoginService.",[1365,2222,2223],{"class":1380},"verifyIDToken",[1365,2225,2226],{"class":1376},"(idToken);\n",[1365,2228,2229,2231,2234,2236],{"class":1367,"line":247},[1365,2230,1602],{"class":1389},[1365,2232,2233],{"class":1426}," lineUserId",[1365,2235,1608],{"class":1389},[1365,2237,2238],{"class":1376}," verifyResult.sub;\n",[1365,2240,2241],{"class":1367,"line":236},[1365,2242,1509],{"emptyLinePlaceholder":1508},[1365,2244,2245,2247,2249,2251,2253,2256,2259,2261],{"class":1367,"line":225},[1365,2246,1694],{"class":1426},[1365,2248,1697],{"class":1376},[1365,2250,1700],{"class":1380},[1365,2252,1703],{"class":1376},[1365,2254,2255],{"class":1450},"`LINE Token 驗證成功，使用者 ID: ${",[1365,2257,2258],{"class":1376},"lineUserId",[1365,2260,1717],{"class":1450},[1365,2262,1720],{"class":1376},[1365,2264,2265],{"class":1367,"line":214},[1365,2266,1509],{"emptyLinePlaceholder":1508},[1365,2268,2269],{"class":1367,"line":203},[1365,2270,2271],{"class":1370},"      \u002F\u002F 生成驗證碼\n",[1365,2273,2274,2276,2279,2281,2283,2286,2289],{"class":1367,"line":192},[1365,2275,1602],{"class":1389},[1365,2277,2278],{"class":1426}," verificationCode",[1365,2280,1608],{"class":1389},[1365,2282,1679],{"class":1426},[1365,2284,2285],{"class":1376},".mailService.",[1365,2287,2288],{"class":1380},"generateVerificationCode",[1365,2290,2101],{"class":1376},[1365,2292,2293],{"class":1367,"line":181},[1365,2294,1509],{"emptyLinePlaceholder":1508},[1365,2296,2297],{"class":1367,"line":170},[1365,2298,2299],{"class":1370},"      \u002F\u002F 設定過期時間（15分鐘後過期）\n",[1365,2301,2302,2304,2307,2309,2311,2314],{"class":1367,"line":159},[1365,2303,1602],{"class":1389},[1365,2305,2306],{"class":1426}," expiresAt",[1365,2308,1608],{"class":1389},[1365,2310,1767],{"class":1389},[1365,2312,2313],{"class":1380}," Date",[1365,2315,2101],{"class":1376},[1365,2317,2318,2321,2324,2327,2330,2332,2335,2338],{"class":1367,"line":148},[1365,2319,2320],{"class":1376},"      expiresAt.",[1365,2322,2323],{"class":1380},"setMinutes",[1365,2325,2326],{"class":1376},"(expiresAt.",[1365,2328,2329],{"class":1380},"getMinutes",[1365,2331,2086],{"class":1376},[1365,2333,2334],{"class":1389},"+",[1365,2336,2337],{"class":1426}," 15",[1365,2339,1720],{"class":1376},[1365,2341,2342],{"class":1367,"line":137},[1365,2343,1509],{"emptyLinePlaceholder":1508},[1365,2345,2346],{"class":1367,"line":125},[1365,2347,2348],{"class":1370},"      \u002F\u002F 將驗證碼存入資料庫，使用 LINE User ID\n",[1365,2350,2351,2353,2356,2358,2361,2364,2367,2369,2371],{"class":1367,"line":113},[1365,2352,1602],{"class":1389},[1365,2354,2355],{"class":1376}," { ",[1365,2357,1742],{"class":1540},[1365,2359,2360],{"class":1376},": ",[1365,2362,2363],{"class":1426},"insertError",[1365,2365,2366],{"class":1376}," } ",[1365,2368,1433],{"class":1389},[1365,2370,1676],{"class":1389},[1365,2372,2373],{"class":1376}," supabase\n",[1365,2375,2376,2379,2382,2384,2387],{"class":1367,"line":101},[1365,2377,2378],{"class":1376},"        .",[1365,2380,2381],{"class":1380},"from",[1365,2383,1703],{"class":1376},[1365,2385,2386],{"class":1450},"'verification_codes'",[1365,2388,2389],{"class":1376},")\n",[1365,2391,2392,2394,2397],{"class":1367,"line":90},[1365,2393,2378],{"class":1376},[1365,2395,2396],{"class":1380},"insert",[1365,2398,2399],{"class":1376},"([\n",[1365,2401,2402],{"class":1367,"line":79},[1365,2403,2404],{"class":1376},"          {\n",[1365,2406,2407],{"class":1367,"line":67},[1365,2408,2409],{"class":1376},"            user_id: lineUserId,\n",[1365,2411,2412],{"class":1367,"line":56},[1365,2413,2414],{"class":1376},"            code: verificationCode,\n",[1365,2416,2417,2420,2423],{"class":1367,"line":45},[1365,2418,2419],{"class":1376},"            expires_at: expiresAt.",[1365,2421,2422],{"class":1380},"toISOString",[1365,2424,2425],{"class":1376},"(),\n",[1365,2427,2428,2431,2434],{"class":1367,"line":34},[1365,2429,2430],{"class":1376},"            is_used: ",[1365,2432,2433],{"class":1426},"false",[1365,2435,1454],{"class":1376},[1365,2437,2438],{"class":1367,"line":22},[1365,2439,2440],{"class":1376},"          },\n",[1365,2442,2443],{"class":1367,"line":1661},[1365,2444,2445],{"class":1376},"        ]);\n",[1365,2447,2448],{"class":1367,"line":1666},[1365,2449,1509],{"emptyLinePlaceholder":1508},[1365,2451,2452,2455],{"class":1367,"line":1691},[1365,2453,2454],{"class":1389},"      if",[1365,2456,2457],{"class":1376}," (insertError) {\n",[1365,2459,2460,2463,2465,2467,2469,2472,2474,2476,2478,2480],{"class":1367,"line":1723},[1365,2461,2462],{"class":1426},"        this",[1365,2464,1697],{"class":1376},[1365,2466,1742],{"class":1380},[1365,2468,1703],{"class":1376},[1365,2470,2471],{"class":1450},"`儲存驗證碼失敗: ${",[1365,2473,2363],{"class":1376},[1365,2475,1469],{"class":1450},[1365,2477,1754],{"class":1376},[1365,2479,1717],{"class":1450},[1365,2481,1720],{"class":1376},[1365,2483,2484,2487,2489,2491,2493,2495,2497,2499,2501,2503],{"class":1367,"line":1735},[1365,2485,2486],{"class":1389},"        throw",[1365,2488,1767],{"class":1389},[1365,2490,1770],{"class":1380},[1365,2492,1703],{"class":1376},[1365,2494,2471],{"class":1450},[1365,2496,2363],{"class":1376},[1365,2498,1469],{"class":1450},[1365,2500,1754],{"class":1376},[1365,2502,1717],{"class":1450},[1365,2504,1720],{"class":1376},[1365,2506,2507],{"class":1367,"line":1761},[1365,2508,2509],{"class":1376},"      }\n",[1365,2511,2512],{"class":1367,"line":1780},[1365,2513,1509],{"emptyLinePlaceholder":1508},[1365,2515,2516],{"class":1367,"line":1786},[1365,2517,2518],{"class":1370},"      \u002F\u002F 發送驗證信\n",[1365,2520,2521,2524,2526,2528,2530],{"class":1367,"line":1791},[1365,2522,2523],{"class":1389},"      await",[1365,2525,1679],{"class":1426},[1365,2527,2285],{"class":1376},[1365,2529,2144],{"class":1380},[1365,2531,1535],{"class":1376},[1365,2533,2534],{"class":1367,"line":1796},[1365,2535,2536],{"class":1376},"        email,\n",[1365,2538,2539],{"class":1367,"line":1801},[1365,2540,2541],{"class":1376},"        verificationCode,\n",[1365,2543,2544],{"class":1367,"line":1807},[1365,2545,2546],{"class":1376},"        userName,\n",[1365,2548,2549],{"class":1367,"line":1812},[1365,2550,2551],{"class":1376},"      );\n",[1365,2553,2554],{"class":1367,"line":1822},[1365,2555,1509],{"emptyLinePlaceholder":1508},[1365,2557,2558,2560,2562,2564],{"class":1367,"line":1833},[1365,2559,1694],{"class":1426},[1365,2561,1697],{"class":1376},[1365,2563,1700],{"class":1380},[1365,2565,1535],{"class":1376},[1365,2567,2568,2571,2574,2577,2579,2581],{"class":1367,"line":1844},[1365,2569,2570],{"class":1450},"        `驗證信已發送至: ${",[1365,2572,2573],{"class":1376},"email",[1365,2575,2576],{"class":1450},"}, 驗證碼已儲存至資料庫，LINE User ID: ${",[1365,2578,2258],{"class":1376},[1365,2580,1717],{"class":1450},[1365,2582,1454],{"class":1376},[1365,2584,2585],{"class":1367,"line":1855},[1365,2586,2551],{"class":1376},[1365,2588,2589],{"class":1367,"line":1864},[1365,2590,1509],{"emptyLinePlaceholder":1508},[1365,2592,2593,2596],{"class":1367,"line":1870},[1365,2594,2595],{"class":1389},"      return",[1365,2597,1400],{"class":1376},[1365,2599,2600,2603,2606],{"class":1367,"line":1876},[1365,2601,2602],{"class":1376},"        message: ",[1365,2604,2605],{"class":1450},"'驗證信已發送成功'",[1365,2607,1454],{"class":1376},[1365,2609,2610],{"class":1367,"line":1882},[1365,2611,1658],{"class":1376},[1365,2613,2614],{"class":1367,"line":1888},[1365,2615,2616],{"class":1376},"    } catch (error) {\n",[1365,2618,2619,2622,2624,2626,2628,2630,2632,2634,2636],{"class":1367,"line":1894},[1365,2620,2621],{"class":1376},"      this.logger.",[1365,2623,1742],{"class":1380},[1365,2625,1703],{"class":1376},[1365,2627,1747],{"class":1450},[1365,2629,1742],{"class":1376},[1365,2631,1469],{"class":1450},[1365,2633,1754],{"class":1376},[1365,2635,1717],{"class":1450},[1365,2637,1720],{"class":1376},[1365,2639,2640,2643,2646,2648,2650,2652,2654,2656,2658],{"class":1367,"line":1900},[1365,2641,2642],{"class":1376},"      throw new ",[1365,2644,2645],{"class":1380},"Error",[1365,2647,1703],{"class":1376},[1365,2649,1747],{"class":1450},[1365,2651,1742],{"class":1376},[1365,2653,1469],{"class":1450},[1365,2655,1754],{"class":1376},[1365,2657,1717],{"class":1450},[1365,2659,1720],{"class":1376},[1365,2661,2662],{"class":1367,"line":1906},[1365,2663,1783],{"class":1376},[1365,2665,2666],{"class":1367,"line":1912},[1365,2667,1503],{"class":1376},[1216,2669,2670],{},[1266,2671,2672],{},"成果展示(註冊流程 - 發送驗證碼信件)",[1216,2674,2675],{},[1219,2676],{"alt":2677,"src":2678},"Gmail 信箱收到驗證信","https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1760281188\u002F2025%20IT%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay29%EF%BC%9ALIFF%20%E6%9C%83%E5%93%A1%E8%A8%BB%E5%86%8A%20-%20%E4%BF%A1%E7%AE%B1%E9%A9%97%E8%AD%89%E8%88%87%20Swagger%20%E6%96%87%E4%BB%B6%E6%95%B4%E5%90%88\u002F%E4%BF%A1%E7%AE%B1%E6%94%B6%E5%88%B0%E9%A9%97%E8%AD%89%E4%BF%A1_fqiefu.png",[1216,2680,2681],{},"用戶在註冊時會收到包含 6 位數驗證碼的驗證信，必須在 10 分鐘有效期限內輸入正確的驗證碼，才能完成註冊流程，完整的註冊流程操作可參考 Day 28 的流程示範影片。",[1224,2683,2685],{"id":2684},"nestjs-swagger-文件撰寫與維護","NestJS Swagger 文件撰寫與維護",[1216,2687,2688],{},"Swagger 採用 OpenAPI 3.0 規範，可以在撰寫 NestJS 程式碼的同時，透過裝飾器（Decorator）和 JSDoc 註解來自動生成 API 文件。這種方式不僅能減少文件維護成本，還能確保文件與程式碼同步更新。",[1253,2690,2692],{"id":2691},"app-層級-swagger-參數說明","App 層級 Swagger 參數說明",[1216,2694,2695],{},[1219,2696],{"alt":2697,"src":2698},"Swagger 參數說明","https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1760278399\u002F2025%20IT%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay%2028%EF%BC%9ALIFF%20%E6%9C%83%E5%93%A1%E5%8D%A1%20-%201\u002FSwagger_Document_%E5%8F%83%E6%95%B8%E8%AA%AA%E6%98%8E_1_fozfg6.jpg",[1258,2700,2701],{},[1216,2702,2703],{},"上圖標示了 Swagger UI 各項目與 main.ts 設定的對應關係，可搭配下方程式碼對照理解各參數的作用",[1216,2705,2706],{},[1266,2707,2708],{},"main.ts",[1356,2710,2712],{"className":1358,"code":2711,"language":1360,"meta":1361,"style":1361},"async function bootstrap() {\n  \u002F\u002F 略\n  const config = new DocumentBuilder()\n    .setTitle('2025 LINE Bot API 文件') \u002F\u002F 文件標題\n    .setDescription(\n      '使用 NestJS 框架開發，整合 LINE LIFF 與信箱驗證功能的後端服務',\n    ) \u002F\u002F 文件描述\n    .setVersion('1.0') \u002F\u002F 版號\n    .setContact('Antonio', '', 'test123@gmail.com') \u002F\u002F 聯絡資訊\n    .addServer('https:\u002F\u002Fnestjs-linebot-ironman.onrender.com', '生產環境') \u002F\u002F 生產環境測試端點\n    .addServer('https:\u002F\u002Fapi.example.com', '測試環境') \u002F\u002F 測試環境測試端點\n    .addTag('heartbeat', '心跳檢測') \u002F\u002F 新增分類的 tag Name 及 descriptions\n    .addTag('users', '用戶資訊') \u002F\u002F 新增分類的 tag Name 及 description\n    .build();\n\n  const document = SwaggerModule.createDocument(app, config, {\n    \u002F\u002F 關閉預設 Tag，預設會把所有掃到的 API Route 都加入 App 的 default tag 內\n    autoTagControllers: false,\n  });\n\n  const options = {\n    jsonDocumentUrl: 'swagger\u002Fjson', \u002F\u002F 啟用 swagger json 讀取 url\n    swaggerOptions: {\n      defaultModelsExpandDepth: 1, \u002F\u002F 禁用 Models 展開\n    },\n  };\n  SwaggerModule.setup('api', app, document, options); \u002F\u002F api 是 swagger UI使用路由\n\n  await app.listen(process.env.PORT ?? 3000);\n}\nbootstrap();\n",[1231,2713,2714,2727,2731,2748,2767,2776,2783,2791,2808,2836,2858,2879,2901,2922,2931,2935,2953,2958,2967,2972,2976,2987,3000,3005,3018,3023,3028,3047,3051,3076,3080],{"__ignoreMap":1361},[1365,2715,2716,2719,2722,2725],{"class":1367,"line":340},[1365,2717,2718],{"class":1389},"async",[1365,2720,2721],{"class":1389}," function",[1365,2723,2724],{"class":1380}," bootstrap",[1365,2726,1416],{"class":1376},[1365,2728,2729],{"class":1367,"line":329},[1365,2730,1405],{"class":1370},[1365,2732,2733,2736,2739,2741,2743,2746],{"class":1367,"line":318},[1365,2734,2735],{"class":1389},"  const",[1365,2737,2738],{"class":1426}," config",[1365,2740,1608],{"class":1389},[1365,2742,1767],{"class":1389},[1365,2744,2745],{"class":1380}," DocumentBuilder",[1365,2747,1384],{"class":1376},[1365,2749,2750,2753,2756,2758,2761,2764],{"class":1367,"line":134},[1365,2751,2752],{"class":1376},"    .",[1365,2754,2755],{"class":1380},"setTitle",[1365,2757,1703],{"class":1376},[1365,2759,2760],{"class":1450},"'2025 LINE Bot API 文件'",[1365,2762,2763],{"class":1376},") ",[1365,2765,2766],{"class":1370},"\u002F\u002F 文件標題\n",[1365,2768,2769,2771,2774],{"class":1367,"line":122},[1365,2770,2752],{"class":1376},[1365,2772,2773],{"class":1380},"setDescription",[1365,2775,1535],{"class":1376},[1365,2777,2778,2781],{"class":1367,"line":31},[1365,2779,2780],{"class":1450},"      '使用 NestJS 框架開發，整合 LINE LIFF 與信箱驗證功能的後端服務'",[1365,2782,1454],{"class":1376},[1365,2784,2785,2788],{"class":1367,"line":17},[1365,2786,2787],{"class":1376},"    ) ",[1365,2789,2790],{"class":1370},"\u002F\u002F 文件描述\n",[1365,2792,2793,2795,2798,2800,2803,2805],{"class":1367,"line":110},[1365,2794,2752],{"class":1376},[1365,2796,2797],{"class":1380},"setVersion",[1365,2799,1703],{"class":1376},[1365,2801,2802],{"class":1450},"'1.0'",[1365,2804,2763],{"class":1376},[1365,2806,2807],{"class":1370},"\u002F\u002F 版號\n",[1365,2809,2810,2812,2815,2817,2820,2823,2826,2828,2831,2833],{"class":1367,"line":76},[1365,2811,2752],{"class":1376},[1365,2813,2814],{"class":1380},"setContact",[1365,2816,1703],{"class":1376},[1365,2818,2819],{"class":1450},"'Antonio'",[1365,2821,2822],{"class":1376},", ",[1365,2824,2825],{"class":1450},"''",[1365,2827,2822],{"class":1376},[1365,2829,2830],{"class":1450},"'test123@gmail.com'",[1365,2832,2763],{"class":1376},[1365,2834,2835],{"class":1370},"\u002F\u002F 聯絡資訊\n",[1365,2837,2838,2840,2843,2845,2848,2850,2853,2855],{"class":1367,"line":247},[1365,2839,2752],{"class":1376},[1365,2841,2842],{"class":1380},"addServer",[1365,2844,1703],{"class":1376},[1365,2846,2847],{"class":1450},"'https:\u002F\u002Fnestjs-linebot-ironman.onrender.com'",[1365,2849,2822],{"class":1376},[1365,2851,2852],{"class":1450},"'生產環境'",[1365,2854,2763],{"class":1376},[1365,2856,2857],{"class":1370},"\u002F\u002F 生產環境測試端點\n",[1365,2859,2860,2862,2864,2866,2869,2871,2874,2876],{"class":1367,"line":236},[1365,2861,2752],{"class":1376},[1365,2863,2842],{"class":1380},[1365,2865,1703],{"class":1376},[1365,2867,2868],{"class":1450},"'https:\u002F\u002Fapi.example.com'",[1365,2870,2822],{"class":1376},[1365,2872,2873],{"class":1450},"'測試環境'",[1365,2875,2763],{"class":1376},[1365,2877,2878],{"class":1370},"\u002F\u002F 測試環境測試端點\n",[1365,2880,2881,2883,2886,2888,2891,2893,2896,2898],{"class":1367,"line":225},[1365,2882,2752],{"class":1376},[1365,2884,2885],{"class":1380},"addTag",[1365,2887,1703],{"class":1376},[1365,2889,2890],{"class":1450},"'heartbeat'",[1365,2892,2822],{"class":1376},[1365,2894,2895],{"class":1450},"'心跳檢測'",[1365,2897,2763],{"class":1376},[1365,2899,2900],{"class":1370},"\u002F\u002F 新增分類的 tag Name 及 descriptions\n",[1365,2902,2903,2905,2907,2909,2912,2914,2917,2919],{"class":1367,"line":214},[1365,2904,2752],{"class":1376},[1365,2906,2885],{"class":1380},[1365,2908,1703],{"class":1376},[1365,2910,2911],{"class":1450},"'users'",[1365,2913,2822],{"class":1376},[1365,2915,2916],{"class":1450},"'用戶資訊'",[1365,2918,2763],{"class":1376},[1365,2920,2921],{"class":1370},"\u002F\u002F 新增分類的 tag Name 及 description\n",[1365,2923,2924,2926,2929],{"class":1367,"line":203},[1365,2925,2752],{"class":1376},[1365,2927,2928],{"class":1380},"build",[1365,2930,2101],{"class":1376},[1365,2932,2933],{"class":1367,"line":192},[1365,2934,1509],{"emptyLinePlaceholder":1508},[1365,2936,2937,2939,2942,2944,2947,2950],{"class":1367,"line":181},[1365,2938,2735],{"class":1389},[1365,2940,2941],{"class":1426}," document",[1365,2943,1608],{"class":1389},[1365,2945,2946],{"class":1376}," SwaggerModule.",[1365,2948,2949],{"class":1380},"createDocument",[1365,2951,2952],{"class":1376},"(app, config, {\n",[1365,2954,2955],{"class":1367,"line":170},[1365,2956,2957],{"class":1370},"    \u002F\u002F 關閉預設 Tag，預設會把所有掃到的 API Route 都加入 App 的 default tag 內\n",[1365,2959,2960,2963,2965],{"class":1367,"line":159},[1365,2961,2962],{"class":1376},"    autoTagControllers: ",[1365,2964,2433],{"class":1426},[1365,2966,1454],{"class":1376},[1365,2968,2969],{"class":1367,"line":148},[1365,2970,2971],{"class":1376},"  });\n",[1365,2973,2974],{"class":1367,"line":137},[1365,2975,1509],{"emptyLinePlaceholder":1508},[1365,2977,2978,2980,2983,2985],{"class":1367,"line":125},[1365,2979,2735],{"class":1389},[1365,2981,2982],{"class":1426}," options",[1365,2984,1608],{"class":1389},[1365,2986,1400],{"class":1376},[1365,2988,2989,2992,2995,2997],{"class":1367,"line":113},[1365,2990,2991],{"class":1376},"    jsonDocumentUrl: ",[1365,2993,2994],{"class":1450},"'swagger\u002Fjson'",[1365,2996,2822],{"class":1376},[1365,2998,2999],{"class":1370},"\u002F\u002F 啟用 swagger json 讀取 url\n",[1365,3001,3002],{"class":1367,"line":101},[1365,3003,3004],{"class":1376},"    swaggerOptions: {\n",[1365,3006,3007,3010,3013,3015],{"class":1367,"line":90},[1365,3008,3009],{"class":1376},"      defaultModelsExpandDepth: ",[1365,3011,3012],{"class":1426},"1",[1365,3014,2822],{"class":1376},[1365,3016,3017],{"class":1370},"\u002F\u002F 禁用 Models 展開\n",[1365,3019,3020],{"class":1367,"line":79},[1365,3021,3022],{"class":1376},"    },\n",[1365,3024,3025],{"class":1367,"line":67},[1365,3026,3027],{"class":1376},"  };\n",[1365,3029,3030,3033,3036,3038,3041,3044],{"class":1367,"line":56},[1365,3031,3032],{"class":1376},"  SwaggerModule.",[1365,3034,3035],{"class":1380},"setup",[1365,3037,1703],{"class":1376},[1365,3039,3040],{"class":1450},"'api'",[1365,3042,3043],{"class":1376},", app, document, options); ",[1365,3045,3046],{"class":1370},"\u002F\u002F api 是 swagger UI使用路由\n",[1365,3048,3049],{"class":1367,"line":45},[1365,3050,1509],{"emptyLinePlaceholder":1508},[1365,3052,3053,3056,3059,3062,3065,3068,3071,3074],{"class":1367,"line":34},[1365,3054,3055],{"class":1389},"  await",[1365,3057,3058],{"class":1376}," app.",[1365,3060,3061],{"class":1380},"listen",[1365,3063,3064],{"class":1376},"(process.env.",[1365,3066,3067],{"class":1426},"PORT",[1365,3069,3070],{"class":1389}," ??",[1365,3072,3073],{"class":1426}," 3000",[1365,3075,1720],{"class":1376},[1365,3077,3078],{"class":1367,"line":22},[1365,3079,1951],{"class":1376},[1365,3081,3082,3085],{"class":1367,"line":1661},[1365,3083,3084],{"class":1380},"bootstrap",[1365,3086,2101],{"class":1376},[1216,3088,3089],{},"完成設定後，重新啟動應用程式，即可透過瀏覽器訪問 Swagger UI 介面。",[1216,3091,3092,3093,3096],{},"以本地開發環境為例，開啟瀏覽器並前往：",[3094,3095],"br",{},[1231,3097,3098],{},"http:\u002F\u002Flocalhost:3000\u002Fapi",[1253,3100,3102],{"id":3101},"controller-層級-swagger-常用裝飾器說明","Controller 層級 Swagger 常用裝飾器說明",[1339,3104,3105,3108,3111,3122],{},[1273,3106,3107],{},"@ApiTags：將 API 歸類到特定標籤群組，對應 main.ts 中定義的標籤",[1273,3109,3110],{},"@ApiExcludeController：隱藏不需要顯示在文件中的 Controller（例如內部測試用的 API）",[1273,3112,3113,3114],{},"@ApiOperation：\n",[1339,3115,3116,3119],{},[1273,3117,3118],{},"summary：顯示在 API 列表的簡短說明",[1273,3120,3121],{},"description：點開後顯示的詳細內容",[1273,3123,3124],{},"@ApiResponse：定義 API 的回應狀態碼與說明，可設定多個不同的回應情境",[1216,3126,3127],{},[1219,3128],{"alt":3129,"src":3130},"Swagger Heartbeats 介面顯示","https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1760279875\u002F2025%20IT%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay%2028%EF%BC%9ALIFF%20%E6%9C%83%E5%93%A1%E5%8D%A1%20-%201\u002FController_Swagger_%E8%A8%AD%E5%AE%9A_m6kt6g.jpg",[1258,3132,3133],{},[1216,3134,3135],{},"上圖標示了 Swagger UI 各項目與 \u002Fheartbeat.controller.ts 設定的對應關係，可搭配下方程式碼對照理解各參數的作用",[1216,3137,3138],{},[1266,3139,3140],{},"heartbeat.controller\u002Fheartbeat.controller.ts",[1356,3142,3144],{"className":1358,"code":3143,"language":1360,"meta":1361,"style":1361},"\u002F\u002F 略\n@ApiTags('heartbeat')\n@Controller('heartbeat')\nexport class HeartbeatController {\n  @ApiOperation({\n    summary: 'UptimeRobot 心跳檢查',\n    description: '用於健康檢查，返回 HTTP 200 狀態碼表示服務正常運行',\n  })\n  @ApiResponse({\n    status: 200,\n    description: '服務正常運行',\n  })\n  @Head('')\n  heartbeat(): string {\n    return 'OK';\n  }\n}\n",[1231,3145,3146,3150,3163,3176,3187,3197,3207,3217,3222,3231,3241,3250,3254,3267,3280,3289,3293],{"__ignoreMap":1361},[1365,3147,3148],{"class":1367,"line":340},[1365,3149,1371],{"class":1370},[1365,3151,3152,3154,3157,3159,3161],{"class":1367,"line":329},[1365,3153,1377],{"class":1376},[1365,3155,3156],{"class":1380},"ApiTags",[1365,3158,1703],{"class":1376},[1365,3160,2890],{"class":1450},[1365,3162,2389],{"class":1376},[1365,3164,3165,3167,3170,3172,3174],{"class":1367,"line":318},[1365,3166,1377],{"class":1376},[1365,3168,3169],{"class":1380},"Controller",[1365,3171,1703],{"class":1376},[1365,3173,2890],{"class":1450},[1365,3175,2389],{"class":1376},[1365,3177,3178,3180,3182,3185],{"class":1367,"line":134},[1365,3179,1390],{"class":1389},[1365,3181,1393],{"class":1389},[1365,3183,3184],{"class":1396}," HeartbeatController",[1365,3186,1400],{"class":1376},[1365,3188,3189,3192,3195],{"class":1367,"line":122},[1365,3190,3191],{"class":1376},"  @",[1365,3193,3194],{"class":1380},"ApiOperation",[1365,3196,1442],{"class":1376},[1365,3198,3199,3202,3205],{"class":1367,"line":31},[1365,3200,3201],{"class":1376},"    summary: ",[1365,3203,3204],{"class":1450},"'UptimeRobot 心跳檢查'",[1365,3206,1454],{"class":1376},[1365,3208,3209,3212,3215],{"class":1367,"line":17},[1365,3210,3211],{"class":1376},"    description: ",[1365,3213,3214],{"class":1450},"'用於健康檢查，返回 HTTP 200 狀態碼表示服務正常運行'",[1365,3216,1454],{"class":1376},[1365,3218,3219],{"class":1367,"line":110},[1365,3220,3221],{"class":1376},"  })\n",[1365,3223,3224,3226,3229],{"class":1367,"line":76},[1365,3225,3191],{"class":1376},[1365,3227,3228],{"class":1380},"ApiResponse",[1365,3230,1442],{"class":1376},[1365,3232,3233,3236,3239],{"class":1367,"line":247},[1365,3234,3235],{"class":1376},"    status: ",[1365,3237,3238],{"class":1426},"200",[1365,3240,1454],{"class":1376},[1365,3242,3243,3245,3248],{"class":1367,"line":236},[1365,3244,3211],{"class":1376},[1365,3246,3247],{"class":1450},"'服務正常運行'",[1365,3249,1454],{"class":1376},[1365,3251,3252],{"class":1367,"line":225},[1365,3253,3221],{"class":1376},[1365,3255,3256,3258,3261,3263,3265],{"class":1367,"line":214},[1365,3257,3191],{"class":1376},[1365,3259,3260],{"class":1380},"Head",[1365,3262,1703],{"class":1376},[1365,3264,2825],{"class":1450},[1365,3266,2389],{"class":1376},[1365,3268,3269,3272,3274,3276,3278],{"class":1367,"line":203},[1365,3270,3271],{"class":1380},"  heartbeat",[1365,3273,2053],{"class":1376},[1365,3275,1544],{"class":1389},[1365,3277,1547],{"class":1426},[1365,3279,1400],{"class":1376},[1365,3281,3282,3284,3287],{"class":1367,"line":192},[1365,3283,1858],{"class":1389},[1365,3285,3286],{"class":1450}," 'OK'",[1365,3288,2018],{"class":1376},[1365,3290,3291],{"class":1367,"line":181},[1365,3292,1503],{"class":1376},[1365,3294,3295],{"class":1367,"line":170},[1365,3296,1951],{"class":1376},[1253,3298,3300],{"id":3299},"dto-搭配-jsdoc-產生-swagger-文件","DTO 搭配 JSDoc 產生 Swagger 文件",[1258,3302,3303],{},[1216,3304,3305,3306,3309],{},"前置需求：需先安裝 ",[1231,3307,3308],{},"@nestjs\u002Fswagger"," 套件",[1216,3311,3312],{},"透過在 DTO 類別中使用 JSDoc 註解，Swagger 會自動解析並生成對應的 API 文件。這讓參數說明與範例值能直接顯示在 Swagger UI 中，方便前端開發人員理解與測試。",[1216,3314,3315],{},[1266,3316,3317],{},"@nestjs\u002Fswagger 的整合優勢：",[1339,3319,3320,3323],{},[1273,3321,3322],{},"@example 標籤會自動顯示為欄位的預設範例值",[1273,3324,3325],{},"自動識別 class-validator 的驗證裝飾器，將驗證規則整合至 Schema 中，產生欄位類型、格式與限制說明",[1216,3327,3328,3331,3333],{},[1266,3329,3330],{},"Swagger UI API 端點呈現",[3094,3332],{},[1219,3334],{"alt":3335,"src":3336},"Swagger register 畫面","https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1760281456\u002F2025%20IT%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay29%EF%BC%9ALIFF%20%E6%9C%83%E5%93%A1%E8%A8%BB%E5%86%8A%20-%20%E4%BF%A1%E7%AE%B1%E9%A9%97%E8%AD%89%E8%88%87%20Swagger%20%E6%96%87%E4%BB%B6%E6%95%B4%E5%90%88\u002FSwagger_register_%E7%95%AB%E9%9D%A2_rw05x3.png",[1216,3338,3339,3342,3344],{},[1266,3340,3341],{},"Swagger UI Schema 定義呈現",[3094,3343],{},[1219,3345],{"alt":3346,"src":3347},"Swagger registry Schema 畫面","https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1760281509\u002F2025%20IT%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay29%EF%BC%9ALIFF%20%E6%9C%83%E5%93%A1%E8%A8%BB%E5%86%8A%20-%20%E4%BF%A1%E7%AE%B1%E9%A9%97%E8%AD%89%E8%88%87%20Swagger%20%E6%96%87%E4%BB%B6%E6%95%B4%E5%90%88\u002FSwagger_registry_Schema_%E7%95%AB%E9%9D%A2_mtcuck.png",[1258,3349,3350],{},[1216,3351,3352],{},"上圖標示了 Swagger UI 各項目與 RegisterUserDto 設定的對應關係，可搭配下方程式碼對照理解各參數的作用",[1216,3354,3355],{},[1266,3356,3357],{},"以 LIFF 會員註冊 API 的 DTO 設定為例：",[1216,3359,3360],{},[1266,3361,3362],{},"user\u002Fdto\u002Fregister-user.dto.ts",[1356,3364,3366],{"className":1358,"code":3365,"language":1360,"meta":1361,"style":1361},"\u002F\u002F 略\nexport class RegisterUserDto {\n  \u002F**\n   * LIFF sdk 產生的 ID Token\n   *\u002F\n  @IsString()\n  @IsNotEmpty({ message: 'LINE ID Token 為必填項目' })\n  idToken: string;\n\n  \u002F**\n   * LIFF 註冊表單用戶姓名\n   * @example \"Antonio\"\n   *\u002F\n  @IsString()\n  @IsNotEmpty({ message: '姓名為必填項目' })\n  @MinLength(2, { message: '姓名至少需要 2 個字元' })\n  @MaxLength(20, { message: '姓名不能超過 20 個字元' })\n  name: string;\n\n  \u002F**\n   * LIFF 註冊表單用戶手機號碼\n   * @example \"0909123321\"\n   *\u002F\n  @IsString()\n  @IsNotEmpty({ message: '電話為必填項目' })\n  @Matches(\u002F^09\\d{8}$\u002F, {\n    message: '請輸入正確的台灣手機號碼格式 (09xxxxxxxx)',\n  })\n  phone: string;\n\n  \u002F**\n   * LIFF 註冊表單用戶生日(YYYY-MM-DD)\n   * @example \"1995-08-17\"\n   *\u002F\n  @IsString()\n  @IsNotEmpty({ message: '生日為必填項目' })\n  birthday: string;\n\n  \u002F**\n   * LIFF 註冊表單用戶信箱\n   * @example \"123@gmail.com\"\n   *\u002F\n  @IsEmail({}, { message: '請輸入正確的電子信箱格式' })\n  @IsNotEmpty({ message: '電子信箱為必填項目' })\n  email: string;\n}\n",[1231,3367,3368,3372,3383,3387,3392,3396,3405,3421,3432,3436,3440,3445,3456,3460,3468,3481,3501,3520,3531,3535,3539,3544,3553,3557,3565,3578,3607,3617,3621,3632,3636,3640,3645,3654,3658,3666,3679,3690,3694,3698,3703,3712,3716,3731,3744,3755],{"__ignoreMap":1361},[1365,3369,3370],{"class":1367,"line":340},[1365,3371,1371],{"class":1370},[1365,3373,3374,3376,3378,3381],{"class":1367,"line":329},[1365,3375,1390],{"class":1389},[1365,3377,1393],{"class":1389},[1365,3379,3380],{"class":1396}," RegisterUserDto",[1365,3382,1400],{"class":1376},[1365,3384,3385],{"class":1367,"line":318},[1365,3386,1514],{"class":1370},[1365,3388,3389],{"class":1367,"line":134},[1365,3390,3391],{"class":1370},"   * LIFF sdk 產生的 ID Token\n",[1365,3393,3394],{"class":1367,"line":122},[1365,3395,1524],{"class":1370},[1365,3397,3398,3400,3403],{"class":1367,"line":31},[1365,3399,3191],{"class":1376},[1365,3401,3402],{"class":1380},"IsString",[1365,3404,1384],{"class":1376},[1365,3406,3407,3409,3412,3415,3418],{"class":1367,"line":17},[1365,3408,3191],{"class":1376},[1365,3410,3411],{"class":1380},"IsNotEmpty",[1365,3413,3414],{"class":1376},"({ message: ",[1365,3416,3417],{"class":1450},"'LINE ID Token 為必填項目'",[1365,3419,3420],{"class":1376}," })\n",[1365,3422,3423,3426,3428,3430],{"class":1367,"line":110},[1365,3424,3425],{"class":1540},"  idToken",[1365,3427,1544],{"class":1389},[1365,3429,1547],{"class":1426},[1365,3431,2018],{"class":1376},[1365,3433,3434],{"class":1367,"line":76},[1365,3435,1509],{"emptyLinePlaceholder":1508},[1365,3437,3438],{"class":1367,"line":247},[1365,3439,1514],{"class":1370},[1365,3441,3442],{"class":1367,"line":236},[1365,3443,3444],{"class":1370},"   * LIFF 註冊表單用戶姓名\n",[1365,3446,3447,3450,3453],{"class":1367,"line":225},[1365,3448,3449],{"class":1370},"   * ",[1365,3451,3452],{"class":1389},"@example",[1365,3454,3455],{"class":1370}," \"Antonio\"\n",[1365,3457,3458],{"class":1367,"line":214},[1365,3459,1524],{"class":1370},[1365,3461,3462,3464,3466],{"class":1367,"line":203},[1365,3463,3191],{"class":1376},[1365,3465,3402],{"class":1380},[1365,3467,1384],{"class":1376},[1365,3469,3470,3472,3474,3476,3479],{"class":1367,"line":192},[1365,3471,3191],{"class":1376},[1365,3473,3411],{"class":1380},[1365,3475,3414],{"class":1376},[1365,3477,3478],{"class":1450},"'姓名為必填項目'",[1365,3480,3420],{"class":1376},[1365,3482,3483,3485,3488,3490,3493,3496,3499],{"class":1367,"line":181},[1365,3484,3191],{"class":1376},[1365,3486,3487],{"class":1380},"MinLength",[1365,3489,1703],{"class":1376},[1365,3491,3492],{"class":1426},"2",[1365,3494,3495],{"class":1376},", { message: ",[1365,3497,3498],{"class":1450},"'姓名至少需要 2 個字元'",[1365,3500,3420],{"class":1376},[1365,3502,3503,3505,3508,3510,3513,3515,3518],{"class":1367,"line":170},[1365,3504,3191],{"class":1376},[1365,3506,3507],{"class":1380},"MaxLength",[1365,3509,1703],{"class":1376},[1365,3511,3512],{"class":1426},"20",[1365,3514,3495],{"class":1376},[1365,3516,3517],{"class":1450},"'姓名不能超過 20 個字元'",[1365,3519,3420],{"class":1376},[1365,3521,3522,3525,3527,3529],{"class":1367,"line":159},[1365,3523,3524],{"class":1540},"  name",[1365,3526,1544],{"class":1389},[1365,3528,1547],{"class":1426},[1365,3530,2018],{"class":1376},[1365,3532,3533],{"class":1367,"line":148},[1365,3534,1509],{"emptyLinePlaceholder":1508},[1365,3536,3537],{"class":1367,"line":137},[1365,3538,1514],{"class":1370},[1365,3540,3541],{"class":1367,"line":125},[1365,3542,3543],{"class":1370},"   * LIFF 註冊表單用戶手機號碼\n",[1365,3545,3546,3548,3550],{"class":1367,"line":113},[1365,3547,3449],{"class":1370},[1365,3549,3452],{"class":1389},[1365,3551,3552],{"class":1370}," \"0909123321\"\n",[1365,3554,3555],{"class":1367,"line":101},[1365,3556,1524],{"class":1370},[1365,3558,3559,3561,3563],{"class":1367,"line":90},[1365,3560,3191],{"class":1376},[1365,3562,3402],{"class":1380},[1365,3564,1384],{"class":1376},[1365,3566,3567,3569,3571,3573,3576],{"class":1367,"line":79},[1365,3568,3191],{"class":1376},[1365,3570,3411],{"class":1380},[1365,3572,3414],{"class":1376},[1365,3574,3575],{"class":1450},"'電話為必填項目'",[1365,3577,3420],{"class":1376},[1365,3579,3580,3582,3585,3587,3590,3593,3596,3599,3602,3604],{"class":1367,"line":67},[1365,3581,3191],{"class":1376},[1365,3583,3584],{"class":1380},"Matches",[1365,3586,1703],{"class":1376},[1365,3588,3589],{"class":1450},"\u002F",[1365,3591,3592],{"class":1389},"^",[1365,3594,3595],{"class":1450},"09",[1365,3597,3598],{"class":1426},"\\d",[1365,3600,3601],{"class":1389},"{8}$",[1365,3603,3589],{"class":1450},[1365,3605,3606],{"class":1376},", {\n",[1365,3608,3609,3612,3615],{"class":1367,"line":56},[1365,3610,3611],{"class":1376},"    message: ",[1365,3613,3614],{"class":1450},"'請輸入正確的台灣手機號碼格式 (09xxxxxxxx)'",[1365,3616,1454],{"class":1376},[1365,3618,3619],{"class":1367,"line":45},[1365,3620,3221],{"class":1376},[1365,3622,3623,3626,3628,3630],{"class":1367,"line":34},[1365,3624,3625],{"class":1540},"  phone",[1365,3627,1544],{"class":1389},[1365,3629,1547],{"class":1426},[1365,3631,2018],{"class":1376},[1365,3633,3634],{"class":1367,"line":22},[1365,3635,1509],{"emptyLinePlaceholder":1508},[1365,3637,3638],{"class":1367,"line":1661},[1365,3639,1514],{"class":1370},[1365,3641,3642],{"class":1367,"line":1666},[1365,3643,3644],{"class":1370},"   * LIFF 註冊表單用戶生日(YYYY-MM-DD)\n",[1365,3646,3647,3649,3651],{"class":1367,"line":1691},[1365,3648,3449],{"class":1370},[1365,3650,3452],{"class":1389},[1365,3652,3653],{"class":1370}," \"1995-08-17\"\n",[1365,3655,3656],{"class":1367,"line":1723},[1365,3657,1524],{"class":1370},[1365,3659,3660,3662,3664],{"class":1367,"line":1735},[1365,3661,3191],{"class":1376},[1365,3663,3402],{"class":1380},[1365,3665,1384],{"class":1376},[1365,3667,3668,3670,3672,3674,3677],{"class":1367,"line":1761},[1365,3669,3191],{"class":1376},[1365,3671,3411],{"class":1380},[1365,3673,3414],{"class":1376},[1365,3675,3676],{"class":1450},"'生日為必填項目'",[1365,3678,3420],{"class":1376},[1365,3680,3681,3684,3686,3688],{"class":1367,"line":1780},[1365,3682,3683],{"class":1540},"  birthday",[1365,3685,1544],{"class":1389},[1365,3687,1547],{"class":1426},[1365,3689,2018],{"class":1376},[1365,3691,3692],{"class":1367,"line":1786},[1365,3693,1509],{"emptyLinePlaceholder":1508},[1365,3695,3696],{"class":1367,"line":1791},[1365,3697,1514],{"class":1370},[1365,3699,3700],{"class":1367,"line":1796},[1365,3701,3702],{"class":1370},"   * LIFF 註冊表單用戶信箱\n",[1365,3704,3705,3707,3709],{"class":1367,"line":1801},[1365,3706,3449],{"class":1370},[1365,3708,3452],{"class":1389},[1365,3710,3711],{"class":1370}," \"123@gmail.com\"\n",[1365,3713,3714],{"class":1367,"line":1807},[1365,3715,1524],{"class":1370},[1365,3717,3718,3720,3723,3726,3729],{"class":1367,"line":1812},[1365,3719,3191],{"class":1376},[1365,3721,3722],{"class":1380},"IsEmail",[1365,3724,3725],{"class":1376},"({}, { message: ",[1365,3727,3728],{"class":1450},"'請輸入正確的電子信箱格式'",[1365,3730,3420],{"class":1376},[1365,3732,3733,3735,3737,3739,3742],{"class":1367,"line":1822},[1365,3734,3191],{"class":1376},[1365,3736,3411],{"class":1380},[1365,3738,3414],{"class":1376},[1365,3740,3741],{"class":1450},"'電子信箱為必填項目'",[1365,3743,3420],{"class":1376},[1365,3745,3746,3749,3751,3753],{"class":1367,"line":1833},[1365,3747,3748],{"class":1540},"  email",[1365,3750,1544],{"class":1389},[1365,3752,1547],{"class":1426},[1365,3754,2018],{"class":1376},[1365,3756,3757],{"class":1367,"line":1844},[1365,3758,1951],{"class":1376},[1216,3760,3761,3762,3769],{},"其他未在文中詳述的部分，我整理在個人的",[3763,3764,3768],"a",{"href":3765,"rel":3766},"https:\u002F\u002Fhackmd.io\u002FRZ-fvt4aRgW2Ro4EtbA7Yw",[3767],"nofollow","學習筆記","中，有興趣的讀者可以參考。",[1224,3771,3772],{"id":3772},"本日結語",[1216,3774,3775],{},"今天我們完成了 LIFF 註冊表單的信箱驗證碼流程，並藉由這個功能實際演示了如何在 NestJS 專案中整合 Swagger 文件。",[1216,3777,3778],{},"關於 API 文件這件事我個人非常堅持，良好的文件維護習慣能大幅降低團隊協作與交接的成本。雖然撰寫文件需要額外投入時間，但若能善用工具（如 Swagger 裝飾器搭配 JSDoc）在開發過程中同步產出文件，不僅能提升開發效率，更能為未來的維護者省下大量理解程式碼的時間。",[3780,3781,3782],"style",{},"html pre.shiki code .sDN9O, html code.shiki .sDN9O{--shiki-default:#6A737D;--shiki-dark:#768390}html pre.shiki code .sYgZi, html code.shiki .sYgZi{--shiki-default:#24292E;--shiki-dark:#ADBAC7}html pre.shiki code .sPO5f, html code.shiki .sPO5f{--shiki-default:#6F42C1;--shiki-dark:#DCBDFB}html pre.shiki code .s7YZ4, html code.shiki .s7YZ4{--shiki-default:#D73A49;--shiki-dark:#F47067}html pre.shiki code .sOLd2, html code.shiki .sOLd2{--shiki-default:#6F42C1;--shiki-dark:#F69D50}html pre.shiki code .sviXB, html code.shiki .sviXB{--shiki-default:#005CC5;--shiki-dark:#6CB6FF}html pre.shiki code .s-HuK, html code.shiki .s-HuK{--shiki-default:#032F62;--shiki-dark:#96D0FF}html pre.shiki code .stnAF, html code.shiki .stnAF{--shiki-default:#E36209;--shiki-dark:#F69D50}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":1361,"searchDepth":134,"depth":134,"links":3784},[3785,3786,3789,3790,3791,3796],{"id":1226,"depth":329,"text":1226},{"id":1248,"depth":329,"text":1248,"children":3787},[3788],{"id":1255,"depth":318,"text":1256},{"id":1332,"depth":329,"text":1332},{"id":2114,"depth":329,"text":2114},{"id":2684,"depth":329,"text":2685,"children":3792},[3793,3794,3795],{"id":2691,"depth":318,"text":2692},{"id":3101,"depth":318,"text":3102},{"id":3299,"depth":318,"text":3300},{"id":3772,"depth":329,"text":3772},[11,362],"md",{},"---\ntitle: \"Day 29：LIFF 會員註冊 - 信箱驗證與 Swagger 文件整合\"\ndate: 2025-10-12 23:15:00\ntags:\n  - \"iThome 鐵人賽\"\n  - \"2025 鐵人賽\"\n  - \"Line Bot\"\n  - \"LIFF\"\n  - \"NestJS\"\n  - \"Swagger\"\ncategories:\n  - \"iThome 鐵人賽\"\n  - \"Line Bot\"\ndescription: \"延續 Day 28 會員註冊的流程，在實際的會員註冊場景中，電子信箱驗證是確保用戶身份真實性的重要環節。本篇將基於昨天的註冊流程，新增兩個核心 API：發送驗證信與驗證驗證碼，讓會員註冊功能更加完整且安全。\"\ncanonical: https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10396512\nseries:\n  name: \"2025 iThome 鐵人賽：Line Bot × NestJS：30 天開發日記\"\n  url: https:\u002F\u002Fithelp.ithome.com.tw\u002Fusers\u002F20116554\u002Fironman\u002F8779\n  day: 29\n---\n\n![2025 鐵人賽背景圖](https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1760280552\u002F2025%20IT%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay29%EF%BC%9ALIFF%20%E6%9C%83%E5%93%A1%E8%A8%BB%E5%86%8A%20-%20%E4%BF%A1%E7%AE%B1%E9%A9%97%E8%AD%89%E8%88%87%20Swagger%20%E6%96%87%E4%BB%B6%E6%95%B4%E5%90%88\u002F2025_%E9%90%B5%E4%BA%BA%E8%B3%BD%E4%BD%BF%E7%94%A8_27_ykepqs.png)\n\n## 前言\n\n延續 `Day 28` 會員註冊的流程，在實際的會員註冊場景中，電子信箱驗證是確保用戶身份真實性的重要環節。本篇將基於昨天的註冊流程，新增兩個核心 API：`發送驗證信`與`驗證驗證碼`，讓會員註冊功能更加完整且安全。\n\n此外，今天也會介紹 NestJS Swagger 的實戰應用。透過裝飾器自動生成 API 文件，不僅能提升開發效率，更能讓前後端協作更加順暢，降低溝通成本。\n\n## 信箱服務前置作業\n\n我們使用 nodemailer 搭配 Google SMTP 服務來發送驗證信件。\n\n### Google 帳號設定步驟\n\n> Gmail 免費版發信限制：每天最多可發送給 500 位收件者\n\n**在開始實作之前，需要先完成以下 Google 帳號設定：：**\n\n1. 啟用兩步驟驗證：前往 Google 帳號安全性設定，啟用兩步驟驗證功能\n2. 建立應用程式密碼：在啟用兩步驟驗證後，前往「應用程式密碼」頁面產生專用密碼\n\n**Step 1：進入 Google 帳號設定**\n\n點選右上角的個人頭像，選擇「管理你的 Google 帳戶」\n\n![Google 信箱個人設定畫面](https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1760280903\u002F2025%20IT%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay29%EF%BC%9ALIFF%20%E6%9C%83%E5%93%A1%E8%A8%BB%E5%86%8A%20-%20%E4%BF%A1%E7%AE%B1%E9%A9%97%E8%AD%89%E8%88%87%20Swagger%20%E6%96%87%E4%BB%B6%E6%95%B4%E5%90%88\u002FGoogle_%E4%BF%A1%E7%AE%B1%E5%80%8B%E4%BA%BA%E8%A8%AD%E5%AE%9A%E7%95%AB%E9%9D%A2_gfic5z.jpg)\n\n**Step 2：確認兩步驟驗證狀態**\n\n前往「安全性」頁面，確認「兩步驟驗證」已啟用。若尚未啟用，請先完成兩步驟驗證設定，才能建立應用程式密碼。\n\n![Gmail 信箱二階段驗證位置](https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1760280960\u002F2025%20IT%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay29%EF%BC%9ALIFF%20%E6%9C%83%E5%93%A1%E8%A8%BB%E5%86%8A%20-%20%E4%BF%A1%E7%AE%B1%E9%A9%97%E8%AD%89%E8%88%87%20Swagger%20%E6%96%87%E4%BB%B6%E6%95%B4%E5%90%88\u002F%E4%BF%A1%E7%AE%B1%E4%BA%8C%E9%9A%8E%E6%AE%B5%E9%A9%97%E8%AD%89%E4%BD%8D%E7%BD%AE_ham7kb.png)\n\n**Step 3：建立應用程式密碼**\n\n在搜尋框輸入「應用程式密碼」可快速找到設定頁面\n\n![Google 帳戶搜尋應用畫面](https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1760281025\u002F2025%20IT%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay29%EF%BC%9ALIFF%20%E6%9C%83%E5%93%A1%E8%A8%BB%E5%86%8A%20-%20%E4%BF%A1%E7%AE%B1%E9%A9%97%E8%AD%89%E8%88%87%20Swagger%20%E6%96%87%E4%BB%B6%E6%95%B4%E5%90%88\u002FGoogle_%E5%B8%B3%E6%88%B6%E6%90%9C%E5%B0%8B%E6%87%89%E7%94%A8%E7%95%AB%E9%9D%A2_mywu4z.png)\n\n建立完成後會顯示一組 16 碼密碼，請立即複製保存。應用程式密碼只會顯示一次，無法再次查看。若遺失請刪除後重新建立。\n\n![Google 應用程式密碼](https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1760281098\u002F2025%20IT%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay29%EF%BC%9ALIFF%20%E6%9C%83%E5%93%A1%E8%A8%BB%E5%86%8A%20-%20%E4%BF%A1%E7%AE%B1%E9%A9%97%E8%AD%89%E8%88%87%20Swagger%20%E6%96%87%E4%BB%B6%E6%95%B4%E5%90%88\u002FGoogle_%E6%87%89%E7%94%A8%E7%A8%8B%E5%BC%8F%E5%AF%86%E7%A2%BC_t1bqng.jpg)\n\n## 信箱服務模組建立\n\n**核心功能**\n\n- sendVerificationEmail：發送驗證信件的方法，接收目標信箱地址與驗證碼作為參數，用於寄送會員註冊驗證信\n- generateVerificationCode：生成隨機驗證碼的方法，產生指定長度（預設六位數）的數字驗證碼，供其他服務模組使用\n\n這兩個方法將作為可重用的服務，讓其他模組（如會員註冊模組）能夠整合信箱驗證功能。\n\n**mail\u002Fmail.service.ts**\n\n```ts\n\u002F\u002F 略\n@Injectable()\nexport class MailService {\n  \u002F\u002F 略\n  private createTransporter() {\n    \u002F\u002F 使用 Gmail SMTP 設定\n    this.transporter = nodemailer.createTransport({\n      service: 'gmail',\n      auth: {\n        user: this.EMAIL_USER,\n        pass: this.EMAIL_PASSWORD,\n      },\n    });\n  }\n\n  \u002F**\n   * 發送驗證信\n   *\u002F\n  async sendVerificationEmail(\n    to: string,\n    verificationCode: string,\n    userName: string,\n  ): Promise\u003Cvoid> {\n    try {\n      const mailOptions = {\n        from: this.EMAIL_USER,\n        to,\n        subject: '帳號驗證 - LINE Bot 應用程式',\n        html: this.getVerificationEmailTemplate(verificationCode, userName),\n      };\n\n      const result = await this.transporter.sendMail(mailOptions);\n      this.logger.info(`驗證信發送成功: ${result.messageId}`);\n    } catch (error) {\n      this.logger.error(`發送驗證信失敗: ${error.message}`);\n      throw new Error('發送驗證信失敗');\n    }\n  }\n\n  \u002F**\n   * 生成驗證信的 HTML 模板\n   *\u002F\n  private getVerificationEmailTemplate(\n    verificationCode: string,\n    userName: string,\n  ): string {\n    return `\n      \u003C!DOCTYPE html>\n      \u003Chtml>\n          \u003Cbody>\n            \u003Cdiv class=\"container\">\n              \u003Cdiv class=\"header\">\n                \u003Ch1>帳號驗證\u003C\u002Fh1>\n              \u003C\u002Fdiv>\n              \u003Cdiv class=\"content\">\n                \u003Ch2>親愛的 ${userName}，\u003C\u002Fh2>\n                \u003Cp>感謝您註冊我們的 LINE Bot 應用程式！\u003C\u002Fp>\n                \u003Cp>請使用以下驗證碼完成您的帳號驗證：\u003C\u002Fp>\n                \u003Cdiv class=\"verification-code\">\n                  ${verificationCode}\n                \u003C\u002Fdiv>\n                \u003Cp>此驗證碼將在 \u003Cstrong>10 分鐘\u003C\u002Fstrong> 後失效。\u003C\u002Fp>\n                \u003Cp>如果您沒有註冊此帳號，請忽略此信件。\u003C\u002Fp>\n              \u003C\u002Fdiv>\n              \u003Cdiv class=\"footer\">\n                \u003Cp>此信件由系統自動發送，請勿回覆。\u003C\u002Fp>\n              \u003C\u002Fdiv>\n            \u003C\u002Fdiv>\n          \u003C\u002Fbody>\n      \u003C\u002Fhtml>\n    `;\n  }\n\n  \u002F**\n   * 生成 6 位數驗證碼\n   *\u002F\n  generateVerificationCode(): string {\n    return Math.floor(100000 + Math.random() * 900000).toString();\n  }\n}\n```\n\n## 用戶模組新增信件發送及驗證碼驗證服務\n\n以下展示 LIFF 前端發送驗證碼時呼叫的後端服務。當用戶在前端註冊表單通過 vee-validate 驗證後，系統會執行以下流程：\n\n1. 發送驗證信：將驗證碼寄送至用戶註冊的信箱\n2. 儲存驗證碼：將驗證碼及相關資訊記錄至資料庫\n3. 設定有效期限：為驗證碼設置過期時間（例如 10 分鐘）\n\n用戶必須在有效期限內，輸入信箱中收到的驗證碼，才能完成註冊流程。若驗證碼過期，則需重新發送。\n\n```ts\n  async sendVerificationEmail(\n    sendVerificationEmailDto: SendVerificationEmailDto,\n  ): Promise\u003C{ message: string }> {\n    const { idToken, email, userName } = sendVerificationEmailDto;\n    const supabase = this.supabaseService.db;\n\n    try {\n      \u002F\u002F 驗證 LINE ID Token 並獲取 LINE User ID\n      const verifyResult = await this.lineLoginService.verifyIDToken(idToken);\n      const lineUserId = verifyResult.sub;\n\n      this.logger.info(`LINE Token 驗證成功，使用者 ID: ${lineUserId}`);\n\n      \u002F\u002F 生成驗證碼\n      const verificationCode = this.mailService.generateVerificationCode();\n\n      \u002F\u002F 設定過期時間（15分鐘後過期）\n      const expiresAt = new Date();\n      expiresAt.setMinutes(expiresAt.getMinutes() + 15);\n\n      \u002F\u002F 將驗證碼存入資料庫，使用 LINE User ID\n      const { error: insertError } = await supabase\n        .from('verification_codes')\n        .insert([\n          {\n            user_id: lineUserId,\n            code: verificationCode,\n            expires_at: expiresAt.toISOString(),\n            is_used: false,\n          },\n        ]);\n\n      if (insertError) {\n        this.logger.error(`儲存驗證碼失敗: ${insertError.message}`);\n        throw new Error(`儲存驗證碼失敗: ${insertError.message}`);\n      }\n\n      \u002F\u002F 發送驗證信\n      await this.mailService.sendVerificationEmail(\n        email,\n        verificationCode,\n        userName,\n      );\n\n      this.logger.info(\n        `驗證信已發送至: ${email}, 驗證碼已儲存至資料庫，LINE User ID: ${lineUserId}`,\n      );\n\n      return {\n        message: '驗證信已發送成功',\n      };\n    } catch (error) {\n      this.logger.error(`發送驗證信失敗: ${error.message}`);\n      throw new Error(`發送驗證信失敗: ${error.message}`);\n    }\n  }\n```\n\n**成果展示(註冊流程 - 發送驗證碼信件)**\n\n![Gmail 信箱收到驗證信](https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1760281188\u002F2025%20IT%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay29%EF%BC%9ALIFF%20%E6%9C%83%E5%93%A1%E8%A8%BB%E5%86%8A%20-%20%E4%BF%A1%E7%AE%B1%E9%A9%97%E8%AD%89%E8%88%87%20Swagger%20%E6%96%87%E4%BB%B6%E6%95%B4%E5%90%88\u002F%E4%BF%A1%E7%AE%B1%E6%94%B6%E5%88%B0%E9%A9%97%E8%AD%89%E4%BF%A1_fqiefu.png)\n\n用戶在註冊時會收到包含 6 位數驗證碼的驗證信，必須在 10 分鐘有效期限內輸入正確的驗證碼，才能完成註冊流程，完整的註冊流程操作可參考 Day 28 的流程示範影片。\n\n## NestJS Swagger 文件撰寫與維護\n\nSwagger 採用 OpenAPI 3.0 規範，可以在撰寫 NestJS 程式碼的同時，透過裝飾器（Decorator）和 JSDoc 註解來自動生成 API 文件。這種方式不僅能減少文件維護成本，還能確保文件與程式碼同步更新。\n\n### App 層級 Swagger 參數說明\n\n![Swagger 參數說明](https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1760278399\u002F2025%20IT%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay%2028%EF%BC%9ALIFF%20%E6%9C%83%E5%93%A1%E5%8D%A1%20-%201\u002FSwagger_Document_%E5%8F%83%E6%95%B8%E8%AA%AA%E6%98%8E_1_fozfg6.jpg)\n\n> 上圖標示了 Swagger UI 各項目與 main.ts 設定的對應關係，可搭配下方程式碼對照理解各參數的作用\n\n**main.ts**\n\n```ts\nasync function bootstrap() {\n  \u002F\u002F 略\n  const config = new DocumentBuilder()\n    .setTitle('2025 LINE Bot API 文件') \u002F\u002F 文件標題\n    .setDescription(\n      '使用 NestJS 框架開發，整合 LINE LIFF 與信箱驗證功能的後端服務',\n    ) \u002F\u002F 文件描述\n    .setVersion('1.0') \u002F\u002F 版號\n    .setContact('Antonio', '', 'test123@gmail.com') \u002F\u002F 聯絡資訊\n    .addServer('https:\u002F\u002Fnestjs-linebot-ironman.onrender.com', '生產環境') \u002F\u002F 生產環境測試端點\n    .addServer('https:\u002F\u002Fapi.example.com', '測試環境') \u002F\u002F 測試環境測試端點\n    .addTag('heartbeat', '心跳檢測') \u002F\u002F 新增分類的 tag Name 及 descriptions\n    .addTag('users', '用戶資訊') \u002F\u002F 新增分類的 tag Name 及 description\n    .build();\n\n  const document = SwaggerModule.createDocument(app, config, {\n    \u002F\u002F 關閉預設 Tag，預設會把所有掃到的 API Route 都加入 App 的 default tag 內\n    autoTagControllers: false,\n  });\n\n  const options = {\n    jsonDocumentUrl: 'swagger\u002Fjson', \u002F\u002F 啟用 swagger json 讀取 url\n    swaggerOptions: {\n      defaultModelsExpandDepth: 1, \u002F\u002F 禁用 Models 展開\n    },\n  };\n  SwaggerModule.setup('api', app, document, options); \u002F\u002F api 是 swagger UI使用路由\n\n  await app.listen(process.env.PORT ?? 3000);\n}\nbootstrap();\n```\n\n完成設定後，重新啟動應用程式，即可透過瀏覽器訪問 Swagger UI 介面。\n\n以本地開發環境為例，開啟瀏覽器並前往：  \n`http:\u002F\u002Flocalhost:3000\u002Fapi`\n\n### Controller 層級 Swagger 常用裝飾器說明\n\n- @ApiTags：將 API 歸類到特定標籤群組，對應 main.ts 中定義的標籤\n- @ApiExcludeController：隱藏不需要顯示在文件中的 Controller（例如內部測試用的 API）\n- @ApiOperation：\n    - summary：顯示在 API 列表的簡短說明\n    - description：點開後顯示的詳細內容\n- @ApiResponse：定義 API 的回應狀態碼與說明，可設定多個不同的回應情境\n\n![Swagger Heartbeats 介面顯示](https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1760279875\u002F2025%20IT%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay%2028%EF%BC%9ALIFF%20%E6%9C%83%E5%93%A1%E5%8D%A1%20-%201\u002FController_Swagger_%E8%A8%AD%E5%AE%9A_m6kt6g.jpg)\n\n> 上圖標示了 Swagger UI 各項目與 \u002Fheartbeat.controller.ts 設定的對應關係，可搭配下方程式碼對照理解各參數的作用\n\n**heartbeat.controller\u002Fheartbeat.controller.ts**\n\n```ts\n\u002F\u002F 略\n@ApiTags('heartbeat')\n@Controller('heartbeat')\nexport class HeartbeatController {\n  @ApiOperation({\n    summary: 'UptimeRobot 心跳檢查',\n    description: '用於健康檢查，返回 HTTP 200 狀態碼表示服務正常運行',\n  })\n  @ApiResponse({\n    status: 200,\n    description: '服務正常運行',\n  })\n  @Head('')\n  heartbeat(): string {\n    return 'OK';\n  }\n}\n```\n\n### DTO 搭配 JSDoc 產生 Swagger 文件\n\n> 前置需求：需先安裝 `@nestjs\u002Fswagger` 套件\n\n透過在 DTO 類別中使用 JSDoc 註解，Swagger 會自動解析並生成對應的 API 文件。這讓參數說明與範例值能直接顯示在 Swagger UI 中，方便前端開發人員理解與測試。\n\n**@nestjs\u002Fswagger 的整合優勢：**\n\n- @example 標籤會自動顯示為欄位的預設範例值\n- 自動識別 class-validator 的驗證裝飾器，將驗證規則整合至 Schema 中，產生欄位類型、格式與限制說明\n\n**Swagger UI API 端點呈現**  \n![Swagger register 畫面](https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1760281456\u002F2025%20IT%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay29%EF%BC%9ALIFF%20%E6%9C%83%E5%93%A1%E8%A8%BB%E5%86%8A%20-%20%E4%BF%A1%E7%AE%B1%E9%A9%97%E8%AD%89%E8%88%87%20Swagger%20%E6%96%87%E4%BB%B6%E6%95%B4%E5%90%88\u002FSwagger_register_%E7%95%AB%E9%9D%A2_rw05x3.png)\n\n**Swagger UI Schema 定義呈現**  \n![Swagger registry Schema 畫面](https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1760281509\u002F2025%20IT%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay29%EF%BC%9ALIFF%20%E6%9C%83%E5%93%A1%E8%A8%BB%E5%86%8A%20-%20%E4%BF%A1%E7%AE%B1%E9%A9%97%E8%AD%89%E8%88%87%20Swagger%20%E6%96%87%E4%BB%B6%E6%95%B4%E5%90%88\u002FSwagger_registry_Schema_%E7%95%AB%E9%9D%A2_mtcuck.png)\n\n> 上圖標示了 Swagger UI 各項目與 RegisterUserDto 設定的對應關係，可搭配下方程式碼對照理解各參數的作用\n\n**以 LIFF 會員註冊 API 的 DTO 設定為例：**\n\n**user\u002Fdto\u002Fregister-user.dto.ts**\n\n```ts\n\u002F\u002F 略\nexport class RegisterUserDto {\n  \u002F**\n   * LIFF sdk 產生的 ID Token\n   *\u002F\n  @IsString()\n  @IsNotEmpty({ message: 'LINE ID Token 為必填項目' })\n  idToken: string;\n\n  \u002F**\n   * LIFF 註冊表單用戶姓名\n   * @example \"Antonio\"\n   *\u002F\n  @IsString()\n  @IsNotEmpty({ message: '姓名為必填項目' })\n  @MinLength(2, { message: '姓名至少需要 2 個字元' })\n  @MaxLength(20, { message: '姓名不能超過 20 個字元' })\n  name: string;\n\n  \u002F**\n   * LIFF 註冊表單用戶手機號碼\n   * @example \"0909123321\"\n   *\u002F\n  @IsString()\n  @IsNotEmpty({ message: '電話為必填項目' })\n  @Matches(\u002F^09\\d{8}$\u002F, {\n    message: '請輸入正確的台灣手機號碼格式 (09xxxxxxxx)',\n  })\n  phone: string;\n\n  \u002F**\n   * LIFF 註冊表單用戶生日(YYYY-MM-DD)\n   * @example \"1995-08-17\"\n   *\u002F\n  @IsString()\n  @IsNotEmpty({ message: '生日為必填項目' })\n  birthday: string;\n\n  \u002F**\n   * LIFF 註冊表單用戶信箱\n   * @example \"123@gmail.com\"\n   *\u002F\n  @IsEmail({}, { message: '請輸入正確的電子信箱格式' })\n  @IsNotEmpty({ message: '電子信箱為必填項目' })\n  email: string;\n}\n```\n\n其他未在文中詳述的部分，我整理在個人的[學習筆記](https:\u002F\u002Fhackmd.io\u002FRZ-fvt4aRgW2Ro4EtbA7Yw)中，有興趣的讀者可以參考。\n\n## 本日結語\n\n今天我們完成了 LIFF 註冊表單的信箱驗證碼流程，並藉由這個功能實際演示了如何在 NestJS 專案中整合 Swagger 文件。\n\n關於 API 文件這件事我個人非常堅持，良好的文件維護習慣能大幅降低團隊協作與交接的成本。雖然撰寫文件需要額外投入時間，但若能善用工具（如 Swagger 裝飾器搭配 JSDoc）在開發過程中同步產出文件，不僅能提升開發效率，更能為未來的維護者省下大量理解程式碼的時間。\n",{"title":370,"description":371},{"name":366,"url":367,"day":34},{"loc":369,"images":3804,"lastmod":372},[3805,3806,3807,3808,3809,3810,3811,3812,3813,3814],{"loc":1222},{"loc":1292},{"loc":1306},{"loc":1320},{"loc":1329},{"loc":2678},{"loc":2698},{"loc":3130},{"loc":3336},{"loc":3347},"ithome-2025-day-29","2025-10-12-ithome-2025-day-29",[11,360,362,374,350,375],"UNaWKbK1cbBCDlXvkXueShd7_BSftqQfOQRikCMPiOc",1792575181028]