昨天完成 Firebase Auth 匿名登入後,每位造訪《喵語日誌》的使用者都擁有了專屬且唯一的 UID。
不過,目前使用者的對話內容,以及 Gemini 分析出的情緒分數,仍然只存在於 React 的前端記憶體中。只要重新整理網頁,剛剛的對話、情緒紀錄與貓咪的暖心回覆就會全部重置。
如果想讓《喵語日誌》成為真正具備記錄價值的情緒日記,就必須將資料保存到雲端。
因此,今天正式接通 Google Cloud Firestore,從 NoSQL 資料結構規劃開始,完成第一筆日記資料的雲端寫入,讓每一次與貓咪的互動都能被妥善保存。
在關聯式資料庫中,我們習慣使用資料表與外鍵建立資料之間的關聯;但 Firestore 是以 Collection 與 Document 為核心的 NoSQL 資料庫,因此資料結構需要換一種方式思考。
這次採用的資料路徑如下:
users/{uid}/diaries/{diaryId}
其中:
users 是使用者集合
{uid} 是 Firebase Auth 建立的使用者識別碼
diaries 是該使用者的日記子集合
{diaryId} 是每一筆日記文件的唯一 ID
每次使用者完成一筆對話與情緒分析,就會在自己的 diaries 子集合中建立一份新的 Document。
這種階層式結構主要有幾個好處。
首先,每位使用者的資料都會依照 UID 分隔,未來設定 Firestore Security Rules 時,可以精確限制使用者只能讀取與修改自己的資料。
其次,每筆日記都是獨立文件,不需要將所有紀錄不斷累積在同一個陣列中。長期使用時,可以避免單一文件內容無限膨脹,也更適合之後進行分頁、排序與條件查詢。
最後,當未來要載入情緒圖表或歷史紀錄時,前端可以只查詢需要的資料,例如特定月份或最近幾筆日記,降低不必要的資料傳輸與讀取成本。
每筆日記文件會保存一次完整的互動資料,包括使用者輸入、心情標籤、Gemini 分析結果與貓咪回覆。
目前規劃的欄位如下:
userText 使用者輸入的文字
moodKey 使用者選擇的心情標籤
moodScore Gemini 評估的情緒分數
lifestyleLabel 生活場景標籤
catResponse 貓咪回覆內容
imageBase64 上傳的圖片資料
createdAt Firestore 伺服器時間
這樣的設計可以讓原始互動內容與 AI 分析後的結構化結果同時被保存。
未來在製作情緒折線圖、情緒日曆或成就判定時,就能直接使用這些資料進行計算。
接著,我在 Firebase Console 中啟用 Cloud Firestore 資料庫,並選擇距離使用者較近的亞洲區域作為資料庫位置(如 asia-east1),希望降低資料傳輸延遲。
開發初期為了方便測試,暫時使用測試模式規則。但這只適合開發階段使用,正式上線前仍然必須設定嚴謹的 Security Rules,避免未授權的使用者讀取或修改其他人的資料。
完成資料庫建立後,我在 src/services/firebase.ts 中封裝 saveDiaryEntry 函式,負責將單筆日記寫入使用者專屬的子集合。
// src/services/firebase.ts
import {
addDoc,
collection,
serverTimestamp,
} from "firebase/firestore";
export interface DiaryEntryData {
userText: string;
moodKey?: string | null;
moodScore: number;
lifestyleLabel: string;
catResponse: string;
imageBase64?: string | null;
}
export async function saveDiaryEntry(
uid: string,
entry: DiaryEntryData
) {
try {
const userDiariesRef = collection(
db,
"users",
uid,
"diaries"
);
const docRef = await addDoc(userDiariesRef, {
...entry,
createdAt: serverTimestamp(),
});
console.log(
"✅ 日誌已成功存入 Firestore,Doc ID:",
docRef.id
);
return docRef.id;
} catch (error) {
console.error("❌ 寫入 Firestore 失敗:", error);
throw error;
}
}
這裡使用 addDoc 新增文件,Firestore 會自動產生唯一的 Document ID。
另外,createdAt 使用 serverTimestamp(),而不是直接使用瀏覽器的本地時間。這樣可以避免因為使用者裝置時間不正確,造成日記排序或日期判斷錯亂。
完成服務函式後,接著在 ChatContainer.tsx 中整合資料寫入流程。
當使用者送出訊息後,程式會先呼叫 Gemini 進行分析,取得情緒分數、生活場景標籤與貓咪回覆;畫面更新完成後,再將完整資料寫入 Firestore。
// src/components/ChatContainer.tsx
const result = await analyzeDiary(
trimmed,
currentMood,
currentImage
);
const catReply: Message = {
id: Date.now() + 1,
role: "cat",
text: result.cat_response,
};
setMessages((prev) => [...prev, catReply]);
if (auth.currentUser) {
await saveDiaryEntry(auth.currentUser.uid, {
userText: trimmed,
moodKey: currentMood,
moodScore: result.mood_score,
lifestyleLabel: result.lifestyle_label,
catResponse: result.cat_response,
imageBase64: currentImage,
});
}
目前的完整流程如下:
使用者輸入文字或圖片
↓
呼叫 Gemini 進行分析
↓
取得情緒分數與貓咪回覆
↓
更新前端對話畫面
↓
將資料寫入 Firestore
這也代表《喵語日誌》第一次打通了:
AI 分析 → 前端回覆 → 雲端存檔
完成整合後,我在前端選取「😻 Great」心情,並輸入以下測試內容:
我今天在家裡休息一整天,好開心~
送出訊息後,Gemini 成功產生貓咪回覆與情緒分析結果,接著程式也在背景呼叫 saveDiaryEntry,將資料寫入 Firestore。

▲ 前端成功觸發 Gemini 回覆,並在 Console 印出 Firestore Document ID:YkODIw7iim4s8LsaXjOh
接著進入 Firebase Console 的 Firestore 管理介面確認資料是否正確落地。

▲ Firestore 後台成功在 users/{uid}/diaries 子集合下建立文件,並保存情緒分數、生活標籤與伺服器時間戳記
這次整合 Firestore 寫入的過程相當順利,也讓我更清楚理解前端應用程式如何與雲端資料庫高效互動。
透過將 Gemini 分析出的結構化資料,與原始的文字、圖片及貓咪回覆一起保存,我們不只解決了重新整理後資料消失的問題,也為後續的情緒折線圖、情緒日曆、每週情緒統計與成就解鎖系統備妥了真實且高品質的數據來源。
這次實作也提醒我,資料庫不只是把資料存起來而已,資料結構是否清晰,會直接影響後續的查詢方式、元件設計與功能擴充性。
今天我們成功打通了 Firestore 雲端資料庫寫入!從匿名登入、UID 識別到子集合與文件欄位規劃,每一則與貓咪的互動、情緒分數及 Gemini 分析出的生活標籤,都能完整且安全地保存在使用者專屬的雲端空間中。
明天我們將邁入數據動態同步階段,實作非同步資料載入與 React 狀態提升,透過 getDocs 或 onSnapshot 從 Firestore 讀取紀錄,並將數據即時渲染至情緒日曆與折線圖中,讓重新整理後的圖表也能精準呈現,讓《喵語日誌》的資料真正活起來!
明天見,喵~