Google Calendar API - knpy/yuka-app GitHub Wiki

Google Calendar API 設定ガイド

前提条件

  • Google Cloud Platform アカウント
  • OAuth 2.0 クライアントID作成済み
  • プロジェクトが作成済み

手順

1. Google Calendar API の有効化

  1. Google Cloud Console にアクセス

  2. プロジェクト選択

    • 対象のプロジェクトを選択
  3. API ライブラリ

    • 左メニュー「APIs & Services」→「Library」
    • 「Google Calendar API」を検索
    • 「ENABLE」をクリック

2. OAuth 2.0 スコープの設定

OAuth consent screen で以下のスコープを追加:

https://www.googleapis.com/auth/calendar.readonly
https://www.googleapis.com/auth/userinfo.email
https://www.googleapis.com/auth/userinfo.profile

3. 実装コード

A. 依存関係のインストール

npm install googleapis google-auth-library

B. API呼び出しエンドポイント

// pages/api/calendar/events.ts
import { NextRequest, NextResponse } from 'next/server';
import { google } from 'googleapis';
import { getServerSession } from 'next-auth';
import { authOptions } from '../auth/[...nextauth]';

export async function GET(request: NextRequest) {
  try {
    const session = await getServerSession(authOptions);
    
    if (!session?.accessToken) {
      return NextResponse.json(
        { error: 'Not authenticated' }, 
        { status: 401 }
      );
    }

    // OAuth2クライアント設定
    const oauth2Client = new google.auth.OAuth2(
      process.env.GOOGLE_CLIENT_ID,
      process.env.GOOGLE_CLIENT_SECRET
    );

    oauth2Client.setCredentials({
      access_token: session.accessToken
    });

    // Calendar API インスタンス
    const calendar = google.calendar({ 
      version: 'v3', 
      auth: oauth2Client 
    });

    // 今日の日付範囲
    const today = new Date();
    const startOfDay = new Date(today);
    startOfDay.setHours(0, 0, 0, 0);
    
    const endOfDay = new Date(today);
    endOfDay.setHours(23, 59, 59, 999);

    // イベント取得
    const response = await calendar.events.list({
      calendarId: 'primary',
      timeMin: startOfDay.toISOString(),
      timeMax: endOfDay.toISOString(),
      singleEvents: true,
      orderBy: 'startTime',
    });

    return NextResponse.json({
      events: response.data.items || [],
      totalEvents: response.data.items?.length || 0,
      date: today.toISOString().split('T')[0]
    });

  } catch (error) {
    console.error('Calendar API Error:', error);
    return NextResponse.json(
      { error: 'Failed to fetch calendar events' },
      { status: 500 }
    );
  }
}

C. フロントエンド実装

// components/calendar/GoogleCalendar.tsx
import React from 'react';
import { useSession } from 'next-auth/react';

interface CalendarEvent {
  id: string;
  summary: string;
  start: {
    dateTime?: string;
    date?: string;
  };
  end: {
    dateTime?: string;
    date?: string;
  };
  location?: string;
  description?: string;
}

export default function GoogleCalendar() {
  const { data: session } = useSession();
  const [events, setEvents] = React.useState<CalendarEvent[]>([]);
  const [loading, setLoading] = React.useState(false);
  const [error, setError] = React.useState<string | null>(null);

  const fetchEvents = React.useCallback(async () => {
    if (!session?.accessToken) return;

    setLoading(true);
    setError(null);

    try {
      const response = await fetch('/api/calendar/events', {
        headers: {
          'Authorization': `Bearer ${session.accessToken}`
        }
      });

      if (!response.ok) {
        throw new Error(`HTTP ${response.status}`);
      }

      const data = await response.json();
      setEvents(data.events);
    } catch (err) {
      setError(err instanceof Error ? err.message : '不明なエラー');
    } finally {
      setLoading(false);
    }
  }, [session?.accessToken]);

  React.useEffect(() => {
    if (session) {
      fetchEvents();
    }
  }, [session, fetchEvents]);

  const formatTime = (dateTime?: string, date?: string) => {
    if (dateTime) {
      return new Date(dateTime).toLocaleTimeString('ja-JP', {
        hour: '2-digit',
        minute: '2-digit'
      });
    }
    return '終日';
  };

  if (loading) return <div>読み込み中...</div>;
  if (error) return <div>エラー: {error}</div>;

  return (
    <div className="space-y-4">
      <h2 className="text-xl font-bold">今日の予定</h2>
      
      {events.length === 0 ? (
        <p>今日の予定はありません</p>
      ) : (
        <div className="space-y-2">
          {events.map((event) => (
            <div key={event.id} className="border p-3 rounded">
              <h3 className="font-semibold">{event.summary}</h3>
              <p className="text-sm text-gray-600">
                {formatTime(event.start.dateTime, event.start.date)} - 
                {formatTime(event.end.dateTime, event.end.date)}
              </p>
              {event.location && (
                <p className="text-sm text-gray-500">📍 {event.location}</p>
              )}
              {event.description && (
                <p className="text-sm mt-2">{event.description}</p>
              )}
            </div>
          ))}
        </div>
      )}
      
      <button 
        onClick={fetchEvents}
        className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"
      >
        再読み込み
      </button>
    </div>
  );
}

4. 必要な環境変数

.env.local に以下を設定:

GOOGLE_CLIENT_ID=your_google_client_id
GOOGLE_CLIENT_SECRET=your_google_client_secret
NEXTAUTH_SECRET=your_nextauth_secret
NEXTAUTH_URL=http://localhost:3000

5. エラーハンドリング

よくあるエラーと解決法

  1. 403 Forbidden

    • Calendar APIが有効になっているか確認
    • 正しいスコープが設定されているか確認
  2. 401 Unauthorized

    • アクセストークンが有効か確認
    • OAuth認証が完了しているか確認
  3. 429 Too Many Requests

    • API制限に達している
    • リクエスト頻度を調整

デバッグ用ログ

// API呼び出し時のデバッグ
console.log('Access Token:', session?.accessToken ? 'Present' : 'Missing');
console.log('API Response Status:', response.status);
console.log('Events Count:', data.events?.length || 0);

6. API制限とクォータ

使用制限

  • クエリ/日: 1,000,000
  • クエリ/分/ユーザー: 300
  • クエリ/秒/ユーザー: 10

最適化方法

  • 必要最小限のフィールドのみ取得
  • キャッシュの活用
  • バッチリクエストの使用

7. セキュリティ考慮事項

アクセストークン管理

  • サーバーサイドでのみトークン使用
  • 適切な有効期限設定
  • リフレッシュトークンによる自動更新

データ保護

  • 個人情報の適切な取り扱い
  • 最小権限の原則
  • ログ出力時の機密情報除外

参考資料

⚠️ **GitHub.com Fallback** ⚠️