iT邦幫忙

2026 iThome 鐵人賽

DAY 17
0
Build on Google AI

咖啡、Wi-Fi 與 AI:30 天打造數位遊牧工作地圖系列 第 17

Day 17|咖啡廳資料放哪裡?把現有欄位整理成 Firestore 資料結構

  • 分享至 

  • xImage
  •  

昨天修了地圖 Marker 和 Filter 的同步,原本三個標記會跟著篩選結果顯示或消失。列表另外兩間店還沒有示意位置,先留在待辦裡。
今天接著處理資料來源。前幾天的店家資料都放在專案的 mock 檔案裡,測畫面很方便,但每次修改店家資訊,都得回來改檔案。
這次用 Firestore 存放原本的五間示範店,讓 Laravel 讀取後交給前端。最後再改一次資料庫裡的店名,看看網站重新整理後有沒有跟著變。

先整理要存的欄位

Day 05 開始整理的欄位,現在收在 docs/DATA_FIELDS.md。先把它和目前的 mock 資料放在一起看,確認名稱、型別和允許值。
例如限時規則,已經不是單純的「適合久坐:是/否」。平日不限時、假日客滿限兩小時,要保留 conditional 和備註,不能搬進資料庫後又變回一個 Boolean。
準備存放的內容大致分成這幾組:

類別 目前欄位 搬到資料庫時要注意
店家基本資料 id、name、placeId、address、location、phone、rating 缺少的值保留 null,不補假座標或評分
營業資訊 business.status、todayClosingText、timeZone、source、updatedAt 測試營業狀態不代表真實店況,也不能永久當成即時資訊
核心工作條件 work.powerOutlet、wifiAvailable、wifiStability、timeLimitType 沿用既有狀態值,不縮成有/沒有
補充工作條件 work.noiseLevel、meetingSuitability、minimumSpend 未知照規格保留,沒有低消資料不等於無低消
每項工作資料的證據 value、source、updatedAt、note 來源與備註跟著該項資料走
示範標示 isMock 虛構測試資料搬進 Firestore 後,仍然是示範資料

資料庫沿用這些欄位名稱,前端就能繼續使用原本的判斷方式。
步行時間與距離也先分開。它們和搜尋起點、計算方式有關,不是店家永久固定的屬性。示意 Marker 的畫面位置,更不能當成真實經緯度。

📸 圖片 1|目前的店家資料與欄位規格
https://ithelp.ithome.com.tw/upload/images/20260918/20121296cKV8EMUcpg.png

先請 Agent 評估讀取流程

專案使用 Laravel+Inertia+React,這次讓 Laravel 負責連線、整理欄位,再把資料交給 React:

Firestore 的測試店家資料
↓
Laravel 讀取並整理欄位
↓
Inertia props
↓
React 沿用既有篩選、Cafe Card 與 Marker

搜尋、篩選和卡片沿用目前的做法,這次主要調整資料從哪裡進來。
我先把這段交給 Agent:

先閱讀 docs/ 的 CURRENT 規格、README、
目前 mock 店家資料,以及首頁資料傳入方式。
先不要修改檔案或建立雲端資源。

今天要評估將店家測試資料放進 Firestore。
優先評估由 Laravel 讀取、整理後,
透過 Inertia props 交給 React 的做法。

請列出:
1. 現有欄位到 Firestore 文件的對應,
   沿用 DATA_FIELDS.md,不自行更名或刪掉來源、備註。
2. 建議的 collection 與 document ID。
3. Laravel 需要的依賴、存取身分與權限。
4. Firestore 的日期、null 與巢狀欄位,
   如何轉成前端目前使用的資料型別。
5. 預計修改的檔案、讀取失敗處理及驗證方法。

保留既有搜尋、Filter 與選取狀態邏輯。
測試資料維持 isMock 標示。
不要新增登入、收藏、店家管理介面或即時監聽。
不要替缺少位置的店家捏造真實座標。
不要把憑證放進前端或提交到 repo。
列完計畫後先停下,讓我確認。

計畫裡有幾個地方需要修正

Agent 第一版計畫建議「讀取失敗就退回本機 mock」。這樣首頁雖然還能顯示五間店,我卻無法從畫面判斷 Firestore 是否接通,所以請它分開處理三種情況:

  • 連線或權限出錯:顯示載入失敗。
  • 讀取成功,但資料庫沒有店家:顯示尚無資料。
  • 有店家,但搜尋或篩選後找不到:使用原本的空結果提示。

另外,Travel 是依出發點計算的距離資訊,這次不放進店家文件。座標保留 lat/lng map 或 null;更新時間存成 Firestore Timestamp,讀取時再轉回前端使用的 ISO 8601 字串。
套件也有一處說明錯誤:google/cloud-firestore 需要 gRPC 擴充,不能假設沒有安裝就會自動切換 REST。官方文件
修正版則補上了匯入預覽、讀寫身分分開,以及獨立的 TypeScript 檢查。我再確認兩個細節:匯入要用 create() 避免覆蓋同 ID 文件;欄位填錯時要留下紀錄,不能全部轉成 unknown 就算處理完。
這些要求一起收進後面的實作指令。接著先準備資料庫與存取身分,讓程式有明確的連線目標。

先開 Firebase Console,把資料庫建起來

先在瀏覽器建立資料庫。如果專案裡已經有要使用的資料庫,就直接確認設定,不用再建一個。
先打開 Firebase Console,登入 Google 帳號。

  1. 如果已經有這個網站的 Firebase 專案,就直接選它。
  2. 如果已有要使用的 Google Cloud 專案,可以從新增專案流程為它加入 Firebase,不必再建一個不相關的專案。
  3. 如果都還沒有,就新增專案,名稱可以用 WorkCafe。記下實際的 Project ID,後端連線會用到;顯示名稱和 Project ID 不一定相同。

Project ID 可以在「專案總覽旁的齒輪 → 專案設定 → 一般設定」找到。要複製的是專案 ID,不是顯示名稱或純數字的專案編號。
進入專案後,找到 Firestore Database。若還沒有資料庫,點 建立資料庫(Create database);已有資料庫則查看清單中的 Database ID。
這次使用 Standard edition,Database ID 為 (default),括號也是名稱的一部分。若畫面要求選安全規則模式,就選 Production mode。後端存取權限會在下一段設定。
位置選在接近預計部署後端的區域,建立前先確認,因為資料庫位置之後不能直接修改。
建立完成後,打開 資料(Data) 分頁。第一次進去還沒有店家資料,看到空白資料庫是正常的。

📸 圖片 2|Firestore 資料庫建立完成
https://ithelp.ithome.com.tw/upload/images/20260918/2012129670eC9SLx54.png

接下來設定 Laravel 使用的服務帳戶。後端透過 IAM 權限存取資料庫,不需要把瀏覽器的讀寫規則全面開放。

建立服務帳戶,讓 Laravel 有權限讀寫

資料庫建好後,還要讓程式知道用哪個身分連線。這次的程式分成兩個用途:網站只讀取店家,匯入指令才需要寫入權限,因此分別建立兩個服務帳戶。

1. 建立讀取與匯入帳戶

打開 Google Cloud 服務帳戶管理,確認上方選的是 WorkCafe 使用的專案。本文以 YOUR_PROJECT_ID 代替實際專案 ID,操作時請填入自己的值。
點「建立服務帳戶」,依序建立:

服務帳戶名稱 授予的角色 用途
firestore-reader Cloud Datastore Viewer(roles/datastore.viewer) Laravel 網站讀取
firestore-importer Cloud Datastore User(roles/datastore.user) CLI 匯入測試資料

每個帳戶的操作方式相同:

  1. 輸入服務帳戶名稱,點「建立並繼續」。
  2. 在「授予這個服務帳戶專案存取權」選擇上表對應的角色。
  3. 下一步「授予使用者這個服務帳戶的存取權」先留白。
  4. 點「完成」,再建立另一個帳戶。

如果帳戶已經建立,卻忘了選角色,可以到 Google Cloud → IAM 與管理 → IAM 補上:

  1. 確認目前選的是資料庫所在的專案。
  2. 找到 firestore-reader 的服務帳戶電子郵件,點右側鉛筆「編輯主體」,加入 Cloud Datastore Viewer(roles/datastore.viewer,儲存。這個帳戶供 Laravel 網站讀取資料。
  3. 找到 firestore-importer 的服務帳戶電子郵件,同樣編輯角色,加入 Cloud Datastore User(roles/datastore.user,儲存。這個帳戶供 CLI 匯入資料。
  4. 如果 IAM 清單沒有該帳戶,點「授予存取權」,在「新增主體」貼上該服務帳戶的完整電子郵件,再選對應角色。

這裡設定的是「服務帳戶對專案的存取權」。角色名稱雖然寫 Datastore,也適用於 Firestore;importer 具有資料讀寫權限,匯入時不覆蓋既有文件,仍由程式的 create-only 操作來落實。
選角色時要注意:Firebase Rules Admin 管理的是安全規則,不能替代匯入所需的 Cloud Datastore User。官方角色權限說明
操作可對照 建立服務帳戶Firestore IAM 權限 官方說明。

2. 各自下載一份 JSON 金鑰

先點進 firestore-reader

  1. 開啟「金鑰(Keys)」分頁。
  2. 點「新增金鑰 → 建立新的金鑰」。
  3. 選 JSON,再點「建立」。
  4. 瀏覽器下載檔案後,重新命名為 firestore-reader.json

再對 firestore-importer 重複一次,將它的檔案命名為 firestore-importer.json。這是兩個不同帳戶的金鑰,不是把同一份檔案複製成兩個名字。金鑰建立流程

3. 放到專案裡,設定檔案路徑

在專案的 storage 下建立 credentials 資料夾,放入剛下載的檔案:

storage/credentials/
├─ firestore-reader.json
└─ firestore-importer.json

以我的專案位置來說,完整資料夾路徑是 C:\ServBay\www\workcafe\storage\credentials\

接著在 .env 設定:

FIRESTORE_PROJECT_ID=YOUR_PROJECT_ID
FIRESTORE_CREDENTIALS=storage/credentials/firestore-reader.json
FIRESTORE_ADMIN_CREDENTIALS=storage/credentials/firestore-importer.json

上面兩個 CREDENTIALS 值填的是檔案路徑,不是 JSON 內容。檔案必須真的存在,Project ID 也要對應到資料庫所在的專案。
這次程式已安排將 storage/credentials 排除在 Git 之外,放入金鑰後仍要確認忽略規則有生效。JSON 含有私鑰,不貼進對話、文章或截圖,也不提交到 Git。
存取設定準備好後,就可以接著處理五筆店家資料。

集合、文件、欄位是怎麼建立的?

Firestore 不需要先像 SQL 一樣建好一張表、指定每個欄位,再開始放資料。
我們這次的結構是:

cafes                        集合:店家放在這裡
└─ mock-cafe-001              文件:一間店,ID 沿用現有資料
   ├─ name                   string
   ├─ isMock                 boolean
   ├─ business               map
   └─ work                   map
      └─ powerOutlet         map
         ├─ value            string
         ├─ source           string 或 null
         ├─ updatedAt        timestamp 或 null
         └─ note             string 或 null

欄位是在寫入文件時一起建立的。沒有資料庫強制的固定表格,不代表可以隨意填;這次還是以 DATA_FIELDS.md 當共同規則。
這次會用程式一次匯入五筆資料。若想了解 Console 的手動新增方式,可以參考下面步驟;採用程式匯入時可直接跳到下一節:

  1. 在 Data 分頁按 開始建立集合(Start collection)
  2. Collection ID 填 cafes
  3. Document ID 填現有店家 ID,例如 mock-cafe-001,不用自動產生另一個 ID。
  4. 在欄位區新增名稱、選型別,再填入該店家原本的值。
  5. workbusiness 選 map;在 work 裡再建立 powerOutlet 這類 map,放入 value、source、updatedAt、note。
  6. null 就選 null 型別,不要填成字串 "null";isMock 選 boolean,值為 true。
  7. 核對整筆內容後儲存。完成第一筆文件時,cafes 集合也就有了資料。

這段是在說明 Console 怎麼操作。實際五間店都有不少巢狀欄位,我準備用下一段的匯入方式一起建立,避免一欄一欄手打出不同版本。
如果已手動建立同 ID 文件,後面的匯入指令會跳過它。這些文件要另外核對是否完整,不能只看到「跳過」就當成內容正確。

匯入目前的五間測試店家

每間店各存成一份文件:

cafes/{cafeId}

文件 ID 沿用既有店家 ID,讀取時再確保 React 收到同一個 id。這樣列表、Marker 和選取狀態才不會因為換資料來源就認不出同一家店。
測試資料沿用目前五間示範店,方便和昨天的篩選結果比較。
其中要保留插座多與部分座位有、固定不限時與條件限時、營業中與已打烊,以及未知資料的差別。全部填成最理想的狀態,Filter 反而測不出問題。
資料庫與憑證準備好後,就能請 Agent 實作。下面把前面確認過的要求整理成一份指令;如果已完成本機程式,請它核對現有實作即可,不用重做:

依照剛才確認的計畫,
先核對實際 Project ID 與 (default) 資料庫,
建立五間測試店家的匯入指令與 Laravel 讀取流程,
經 Inertia 將店家資料傳給現有 React 頁面。

匯入前先預覽文件 ID、內容及欄位型別,
確認與目前 mock 資料和 DATA_FIELDS.md 對應。
不要重新建立 Firebase 專案或資料庫。
寫入 cafes/{cafeId},讓集合與欄位隨文件寫入建立。
location 使用 lat/lng map 或 null,不把 Travel 加入 Cafe 文件。
lat/lng 必須是數值,緯度介於 -90~90、經度介於 -180~180。
updatedAt 存 Timestamp 或 null,讀取時轉成 ISO 8601 字串或 null。
Observation.value 依各欄位允許值處理,isMock 保留 boolean。
區分缺少欄位與非法值;非法值降級時記錄文件 ID 與欄位。
匯入驗證未通過的資料不得寫入。
匯入使用有寫入權限的身分,網站讀取使用唯讀身分。

匯入使用 create() 等只建立新文件的操作,
不能只靠先查 exists() 再一般寫入來避免覆蓋。
同 ID 已存在時跳過並回報。
沿用目前五間 mock 店家的內容與 ID,
保留 isMock、unknown、null 及各項資料來源。

保留原始 mock 檔作為測試參考,
但本次 Firestore 讀取頁面不能在失敗時偷偷退回 mock。
讀取失敗要有可辨識的錯誤狀態;
成功但沒有資料,則顯示空資料狀態。

不要修改 UI 設計、篩選規則或補做另外兩間店的 Marker。
不新增即時監聽、登入或資料編輯介面。
若存取設定尚未完成,列出阻礙,不宣稱已串接成功。

確認命令列與網頁使用的 PHP 均能載入所需的 grpc 擴充。
完成後分別執行 typecheck、build 與相關後端測試,
列出修改檔案、實際檢查結果與未驗證事項。
不要自動 commit 或 push。

指令建立後,先預覽,再實際寫入

第一次跑完時,Console 裡沒有出現資料。看了回覆才發現,當時只完成五筆資料的預覽,憑證路徑也還沒填。預覽檢查的是資料格式,並沒有寫入 Firestore。
完成上面的憑證設定後,在專案 Terminal 執行:

php artisan config:clear
php artisan firestore:seed-cafes

第一行讓 Laravel 重新讀取設定;第二行只預覽資料,不會寫入。核對五筆文件 ID、內容與型別後,再執行:

php artisan firestore:seed-cafes --execute

查看每筆文件的結果:建立成功才算寫入;同 ID 已存在則跳過。若回報憑證或權限錯誤,就先處理錯誤,不能把預覽通過當成匯入成功。

📸 圖片 3|Firestore 中的測試店家資料
https://ithelp.ithome.com.tw/upload/images/20260918/20121296NEUoP3Z7ID.png

建立完成後,回到 Console 的 Data 分頁重新整理,確認 cafes 下有對應的五個文件 ID,再展開欄位核對值與型別。這一步確認資料真的寫進指定資料庫;網站讀不讀得到,還要看接下來的驗證。

改一次店名,確認網站讀到的是哪份資料

匯入後,先重新整理網站,確認店家卡片能載入。接著做一次前後對照:

  1. 在 Firestore 的 Data 分頁開啟 cafes → mock-cafe-001
  2. 記下原本的 name,在店名後加上「|讀取測試」,儲存。
  3. 截下文件 ID 與修改後的 name,作為圖片 4-1。
  4. 回到本機網站,清空搜尋與 Filter,再按 Ctrl+R 重新整理。
  5. 找到同一間店,確認卡片出現「讀取測試」,截下網址列、店名與示範資料標示,作為圖片 4-2。
  6. 回 Firestore 還原店名,再重新整理網站確認。

這次驗證的是重新載入後取得新資料,沒有加入即時監聽。如果店名沒變,先核對專案、資料庫和讀取流程,再往下測試。

資料來源換好後,也要重跑 Day 16 的搜尋與篩選。三個既有 Marker 應該繼續跟著結果顯示或消失,另外兩間店的示意位置仍留在待辦。

請驗證這次資料來源切換:
1. 確認頁面資料確實經 Laravel 從 Firestore 取得。
2. 修改指定測試文件的店名後,重新載入頁面,
   確認同 ID 卡片更新,再還原測試修改。
3. 檢查 unknown、null 與條件限時的顯示和篩選。
4. 重跑搜尋、單一 Filter、多條件 AND 與零結果。
5. 驗證「選店 → 篩除 → 取消條件」:
   Marker 恢復後,不自動恢復原選取。
6. 分別確認讀取失敗與成功但空資料的畫面。
7. 執行 build 與獨立的 TypeScript 型別檢查,分別回報結果。

不能以本機 mock 回退掩蓋讀取失敗。
回報實際結果;沒執行的項目標示未驗證。
測試只能修改指定的示範資料,不動其他店家資料。

📸 圖片 4-1|Firestore 修改測試店名
https://ithelp.ithome.com.tw/upload/images/20260918/20121296Ru6P9ge4wi.png

📸 圖片 4-2|重新載入後的店家卡片
https://ithelp.ithome.com.tw/upload/images/20260918/20121296Blk7PUc6v4.png

今天多了一段資料讀取流程

Day 16 處理的是篩選後哪些店留在畫面上,今天則把資料來源一路接到 Firestore:建立資料庫、設定服務帳戶、匯入,再用修改店名來驗證讀取。
這次最容易漏掉的是「預覽」和「寫入」之間那一步。指令跑完沒有報錯,還要看它到底做了什麼;Console 裡的文件和重新整理後的卡片,才是接下來要對照的結果。
目前用的仍是五間示範店。之後加入真實資料時,欄位裡的來源、更新時間與未知值都還要保留,才能知道哪些資訊已查核、哪些還需要補。


上一篇
Day 16|把 AI 變成工程師:用 Antigravity 開始開發
下一篇
Day 18|加入 Google 登入,找咖啡廳還是不用先登入
系列文
咖啡、Wi-Fi 與 AI:30 天打造數位遊牧工作地圖20
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言