🔥 Firebase連携設計書

健康チェックアプリ → Googleスプレッドシート自動転記

1. 全体アーキテクチャ

👤 ユーザー
アンケート回答
⚛️ React App
Vercel
🔥 Firestore
データ保存
⚡ Cloud Functions
トリガー実行
📊 スプレッドシート
データ蓄積

データフロー詳細

  1. ユーザーがアンケートに回答し「診断結果を見る」をクリック
  2. React AppがFirestoreに回答データを書き込み
  3. FirestoreのonCreateトリガーでCloud Functionsが起動
  4. Cloud FunctionsがGoogle Sheets APIでスプレッドシートに行追加
  5. ユーザーには従来通り診断結果を表示(UX変更なし)

2. 実装方式の比較

方式B: Google Apps Script
  • リアルタイム性: △ 5分〜1時間遅延
  • 信頼性: ○ 定期実行
  • コスト: 完全無料
  • 難易度: 低(JavaScript)
  • メリット: 無料、スプレッドシート側で完結
  • デメリット: リアルタイムではない
💡 推奨理由

回答直後にデータが転記されることで、管理者がリアルタイムで状況を把握できます。また、エラー発生時の通知や自動リトライが可能なため、データ欠損リスクが低くなります。

3. Firestore データ構造

コレクション設計

surveys/                          # コレクション
  └── {autoGeneratedId}/          # ドキュメント(自動生成ID)
        ├── employeeId: "EMP12345"
        ├── age: 35
        ├── gender: "male"
        ├── employmentType: "fulltime"
        ├── yearsOfService: 5
        ├── monthsOfService: 3
        ├── jobDescription: "営業"
        ├── healthIssues: {
        │     selectedIssues: ["back_pain", "shoulder"]
        │     primaryIssue: "back_pain"
        │     symptomDays: 10
        │     absenceDays: 2
        │     workQuantity: 6
        │     workQuality: 7
        │     selfCare: "ストレッチ"
        │     wantsTraining: "yes"
        │   }
        ├── workEngagement: {
        │     vigor: 4
        │     dedication: 5
        │     absorption: 3
        │   }
        ├── result: {
        │     engagementScore: 4.0
        │     absenteeismCostPerMonth: 27000
        │     absenteeismCostPerYear: 324000
        │     absenteeismRate: 9.09
        │     presenteeismScore: 65
        │     riskLevel: "moderate"
        │   }
        ├── submittedAt: Timestamp
        └── syncedToSheet: false    # スプレッドシート転記フラグ
        

セキュリティルール

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // surveys コレクション
    match /surveys/{surveyId} {
      // 誰でも書き込み可能(匿名回答のため)
      allow create: if true;

      // 読み取り・更新・削除は認証済み管理者のみ
      allow read, update, delete: if request.auth != null
        && request.auth.token.admin == true;
    }
  }
}
        
⚠️ セキュリティ注意

匿名アンケートのため書き込みは認証なしで許可しますが、読み取りは管理者のみに制限します。これにより他者の回答が見られることを防ぎます。

4. スプレッドシート設計

列構成

項目名 データ型
A回答日時日時2026/02/06 14:30:00
B社員番号文字列EMP12345
C年齢数値35
D性別文字列男性
E雇用形態文字列正社員
F勤続年数文字列5年3ヶ月
G業務内容文字列営業
H健康問題(複数)文字列腰痛, 肩こり
I主要健康問題文字列腰痛
J症状日数数値10
K欠勤日数数値2
L仕事量スコア数値6
M仕事の質スコア数値7
N活力数値4
O熱意数値5
P没頭数値3
QWEスコア数値4.00
R月間損失額通貨¥27,000
S年間損失額通貨¥324,000
T損失率%9.09%
Uプレゼンティーズム数値65
Vリスクレベル文字列中リスク
W自己対策文字列ストレッチ
X講習希望文字列はい

自動集計シート(別シートで作成推奨)

  • 回答数の推移グラフ
  • 健康問題の分布(円グラフ)
  • リスクレベル別人数
  • 部署別・年代別クロス集計
  • 平均損失額サマリー

5. Cloud Functions 実装

functions/src/index.ts

import * as functions from "firebase-functions";
import * as admin from "firebase-admin";
import { google } from "googleapis";

admin.initializeApp();

const SPREADSHEET_ID = "YOUR_SPREADSHEET_ID";
const SHEET_NAME = "回答データ";

// サービスアカウント認証
const auth = new google.auth.GoogleAuth({
  scopes: ["https://www.googleapis.com/auth/spreadsheets"],
});

const sheets = google.sheets({ version: "v4", auth });

// 健康問題の日本語変換
const HEALTH_ISSUE_LABELS: Record<string, string> = {
  none: "問題なし",
  allergy: "アレルギー",
  skin: "皮膚疾患",
  infection: "感染症",
  stomach: "胃腸不調",
  joint: "関節痛",
  back_pain: "腰痛",
  shoulder: "肩こり",
  headache: "頭痛",
  tooth: "歯の不調",
  mental: "精神不調",
  sleep: "睡眠不調",
  fatigue: "倦怠感",
  eye: "眼の不調",
  other: "その他",
};

// Firestore onCreate トリガー
export const syncToSpreadsheet = functions.firestore
  .document("surveys/{surveyId}")
  .onCreate(async (snapshot, context) => {
    const data = snapshot.data();
    const surveyId = context.params.surveyId;

    try {
      // スプレッドシートに追加する行データ
      const row = [
        new Date(data.submittedAt?.toDate() || Date.now()).toLocaleString("ja-JP"),
        data.employeeId || "",
        data.age || "",
        data.gender === "male" ? "男性" : "女性",
        data.employmentType === "fulltime" ? "正社員" :
          data.employmentType === "parttime" ? "パート" : "その他",
        `${data.yearsOfService || 0}年${data.monthsOfService || 0}ヶ月`,
        data.jobDescription || "",
        (data.healthIssues?.selectedIssues || [])
          .map((i: string) => HEALTH_ISSUE_LABELS[i] || i).join(", "),
        HEALTH_ISSUE_LABELS[data.healthIssues?.primaryIssue] || "",
        data.healthIssues?.symptomDays || 0,
        data.healthIssues?.absenceDays || 0,
        data.healthIssues?.workQuantity || 0,
        data.healthIssues?.workQuality || 0,
        data.workEngagement?.vigor || 0,
        data.workEngagement?.dedication || 0,
        data.workEngagement?.absorption || 0,
        data.result?.engagementScore?.toFixed(2) || "0.00",
        data.result?.absenteeismCostPerMonth || 0,
        data.result?.absenteeismCostPerYear || 0,
        ((data.result?.absenteeismRate || 0)).toFixed(2) + "%",
        data.result?.presenteeismScore || 0,
        data.result?.riskLevel === "low" ? "低リスク" :
          data.result?.riskLevel === "moderate" ? "中リスク" :
          data.result?.riskLevel === "high" ? "高リスク" : "最高リスク",
        data.healthIssues?.selfCare || "",
        data.healthIssues?.wantsTraining === "yes" ? "はい" :
          data.healthIssues?.wantsTraining === "no" ? "いいえ" : "検討中",
      ];

      // スプレッドシートに行追加
      await sheets.spreadsheets.values.append({
        spreadsheetId: SPREADSHEET_ID,
        range: `${SHEET_NAME}!A:X`,
        valueInputOption: "USER_ENTERED",
        requestBody: {
          values: [row],
        },
      });

      // 転記完了フラグを更新
      await snapshot.ref.update({ syncedToSheet: true });

      console.log(`Survey ${surveyId} synced to spreadsheet`);
      return { success: true };

    } catch (error) {
      console.error(`Error syncing survey ${surveyId}:`, error);
      throw error; // リトライのためにエラーを再スロー
    }
  });
        

デプロイコマンド

cd functions
npm install
firebase deploy --only functions
        

6. React App 変更点

Firebase SDK インストール

npm install firebase
        

src/lib/firebase.ts

import { initializeApp } from "firebase/app";
import { getFirestore } from "firebase/firestore";

const firebaseConfig = {
  apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
  authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,
  projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
  storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET,
  messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID,
  appId: import.meta.env.VITE_FIREBASE_APP_ID,
};

const app = initializeApp(firebaseConfig);
export const db = getFirestore(app);
        

ConfirmPage.tsx の変更

import { collection, addDoc, serverTimestamp } from "firebase/firestore";
import { db } from "../lib/firebase";

// 「診断結果を見る」ボタンのハンドラ
const handleSubmit = async () => {
  try {
    // 計算結果を取得
    const result = calculateProductivityResult(surveyData);

    // Firestoreに保存
    await addDoc(collection(db, "surveys"), {
      ...surveyData.basicInfo,
      healthIssues: surveyData.healthIssues,
      workEngagement: surveyData.workEngagement,
      result: {
        engagementScore: result.engagementScore.totalScore,
        absenteeismCostPerMonth: result.absenteeismCostPerMonth,
        absenteeismCostPerYear: result.absenteeismCostPerYear,
        absenteeismRate: result.absenteeismRate,
        presenteeismScore: result.presenteeismScore,
        riskLevel: result.riskLevel,
      },
      submittedAt: serverTimestamp(),
      syncedToSheet: false,
    });

    // 結果画面へ遷移
    navigate("/result");

  } catch (error) {
    console.error("Error saving survey:", error);
    // エラーでも結果画面は表示(UX優先)
    navigate("/result");
  }
};
        
✅ ユーザー体験への影響

Firebase保存は非同期で行われ、エラーが発生しても診断結果は表示されます。ユーザーには従来通りのスムーズな体験を提供します。

7. セットアップ手順

  1. Firebaseプロジェクト作成
    • Firebase Console にアクセス
    • 「プロジェクトを追加」→ プロジェクト名を入力
    • Google アナリティクスは任意
  2. Firestoreデータベース作成
    • 「Firestore Database」→「データベースを作成」
    • 本番モードで開始
    • ロケーション: asia-northeast1(東京)
  3. Blazeプラン(従量課金)へアップグレード
    • Cloud Functions使用には必須
    • 無料枠内なら課金なし
  4. Webアプリ登録
    • 「プロジェクトの設定」→「アプリを追加」→ Web
    • Firebase SDK設定をコピー
  5. サービスアカウント作成
    • 「プロジェクトの設定」→「サービスアカウント」
    • 「新しい秘密鍵を生成」→ JSONファイル保存
  6. Googleスプレッドシート準備
    • 新規スプレッドシート作成
    • 1行目にヘッダー行を入力(列構成参照)
    • サービスアカウントのメールアドレスに編集権限を付与
  7. Cloud Functionsデプロイ
    • Firebase CLI インストール: npm install -g firebase-tools
    • ログイン: firebase login
    • 初期化: firebase init functions
    • コード作成・デプロイ
  8. React App 環境変数設定
    • .env ファイルにFirebase設定を追加
    • Vercelの環境変数にも設定

8. コスト見積もり

月間100件回答の場合

サービス 無料枠 予想使用量 料金
Firestore 書き込み 20,000回/日 100回/月 $0
Firestore ストレージ 1GB 〜1MB $0
Cloud Functions 呼び出し 200万回/月 100回/月 $0
Cloud Functions 時間 40万GB秒/月 〜100秒 $0
Google Sheets API 無制限 100回/月 $0
合計(月間100件の場合) $0 無料

月間1,000件回答の場合

サービス 予想使用量 料金
Firestore 1,000回/月 $0
Cloud Functions 1,000回/月 $0
合計 $0 無料
💰 コストまとめ

月間数千件程度の回答であれば、Firebase無料枠内で運用可能です。大規模(月間10万件以上)になった場合のみ従量課金が発生します。

9. 代替案: Google Apps Script(完全無料)

Cloud Functionsを使わない場合、Google Apps Scriptで定期的にFirestoreからデータを取得する方法もあります。

メリット

  • 完全無料(Blazeプラン不要)
  • スプレッドシート側で完結
  • 設定が比較的簡単

デメリット

  • リアルタイムではない(5分〜1時間の遅延)
  • Firestore REST API の認証設定が必要
  • 大量データ時の実行時間制限(6分)

実装概要

// Google Apps Script
function syncFromFirestore() {
  const FIRESTORE_URL = "https://firestore.googleapis.com/v1/projects/YOUR_PROJECT/databases/(default)/documents/surveys";

  // Firestore REST API でデータ取得
  const response = UrlFetchApp.fetch(FIRESTORE_URL, {
    headers: {
      "Authorization": "Bearer " + ScriptApp.getOAuthToken()
    }
  });

  const data = JSON.parse(response.getContentText());

  // スプレッドシートに追記
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("回答データ");

  data.documents.forEach(doc => {
    if (!doc.fields.syncedToSheet?.booleanValue) {
      // 行を追加
      sheet.appendRow([...]);
    }
  });
}

// トリガー設定: 5分ごとに実行
        

10. 次のステップ

✅ 実装を開始する場合

以下のコマンドで実装を依頼してください:

Cloud Functions方式で実装して

または

Google Apps Script方式で実装して

実装に必要な情報

  • Firebaseプロジェクト名(新規作成 or 既存使用)
  • スプレッドシートのURL または ID
  • 追加したい列項目があれば