0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GoogleCalendar APIを学ぶ(Step3:カレンダーの取得APIを実装する)

Last updated at Posted at 2025-05-17

はじめに

前回までの続きです。
今回はGoogleCalendarAPIを参考に、カレンダーの取得APIを実装します

実装箇所

GoogleCalendarAPIのカレンダーの取得を実施していきます!

image.png

GoogleCalendarAPIの仕様

image.png
→赤枠の部分を実装します。では次にレスポンスを見ていきましょう!

{
  "kind": "calendar#calendarListEntry", // このリソースが calendarListEntry であることを示す固定値
  "etag": "\"p1234567890abcdef\"",      // リソースのバージョン識別子(変更検知用)
  "id": "alice@example.com",            // カレンダーのID(主カレンダーの場合はメールアドレス)

  "summary": "Alice's Work Calendar",   // カレンダーの名称(デフォルト名)
  "description": "カンファレンスや締め切りなどの予定を管理するカレンダー", // 説明文
  "location": "Tokyo Headquarters",     // カレンダーの物理的な場所(任意)
  "timeZone": "Asia/Tokyo",             // このカレンダーのタイムゾーン

  "summaryOverride": "Work",            // 特定ユーザーにとっての表示名(上書き名)
  "colorId": "9",                       // カレンダーの色ID(Googleが定義するプリセット)
  "backgroundColor": "#9fc6e7",         // 背景色(hex値)
  "foregroundColor": "#000000",         // 文字色(hex値)

  "hidden": false,                      // このユーザーにとって非表示かどうか
  "selected": true,                     // UIで選択されているか(表示対象か)

  "accessRole": "owner",                // このユーザーのカレンダーに対する権限(owner, writer, reader など)

  "defaultReminders": [                // デフォルトのリマインダー設定(通知方法と何分前か)
    {
      "method": "popup",               // 通知方法(popup, emailなど)
      "minutes": 10                   // イベントの何分前に通知するか
    },
    {
      "method": "email",
      "minutes": 30
    }
  ],

  "notificationSettings": {            // 通知の種類と方法
    "notifications": [
      {
        "type": "eventCreation",       // 通知トリガーの種類(イベント作成など)
        "method": "email"              // 通知方法(email, sms, popupなど)
      },
      {
        "type": "eventChange",
        "method": "popup"
      }
    ]
  },

  "primary": true,                     // このユーザーにとって主カレンダーか(true は削除不可)
  "deleted": false,                    // 削除済みカレンダーか(差分同期時にのみ true が返る)

  "conferenceProperties": {
    "allowedConferenceSolutionTypes": [ // 許可されている会議ツールの種類
      "hangoutsMeet",                   // Google Meet 会議の許可
      "eventHangout"                    // 従来のHangout
    ]
  }
}

→こんな感じですね。さすがGoogleですね!!!レスポンスを見るだけでも汎用性高く、尚且つパフォーマンスを高めているんですね!!十分すぎるので、ちょっと最低限にレスポンスを変更していこうと思います。

{
  "id": 1,            // カレンダーのID
  "summary": "Alice's Work Calendar",   // カレンダーの名称(デフォルト名)
  "description": "カンファレンスや締め切りなどの予定を管理するカレンダー", // 説明文

  "summaryOverride": "Work",            // 特定ユーザーにとっての表示名(上書き名)
  "color": "blue",                       // カレンダーの色

  "hidden": false,                      // このユーザーにとって非表示かどうか
  "selected": true,                     // UIで選択されているか(表示対象か)

  "accessRole": "owner",                // このユーザーのカレンダーに対する権限(owner, writer, reader など)

  "defaultReminders": [                // デフォルトのリマインダー設定(通知方法と何分前か)
    {
      "method": "popup",               // 通知方法(popup, emailなど)
      "minutes": 10                   // イベントの何分前に通知するか
    },
    {
      "method": "email",
      "minutes": 30
    }
  ],

  "notificationSettings": {            // 通知の種類と方法
    "notifications": [
      {
        "type": "eventCreation",       // 通知トリガーの種類(イベント作成など)
        "method": "email"              // 通知方法(email, sms, popupなど)
      },
      {
        "type": "eventChange",
        "method": "popup"
      }
    ]
  },

  "primary": true,                     // このユーザーにとって主カレンダーか(true は削除不可)
  "deleted": false,                    // 削除済みカレンダーか(差分同期時にのみ true が返る)

  "conferenceProperties": {
    "allowedConferenceSolutionTypes": [ // 許可されている会議ツールの種類
      "hangoutsMeet",                   // Google Meet 会議の許可
      "eventHangout"                    // 従来のHangout
    ]
  }
}

→いまのテーブル構成では対応できない部分があるので、追加でSQLを流します。

ALTER TABLE calendar_memberships
ADD COLUMN summary_override varchar,
ADD COLUMN hidden boolean DEFAULT false,
ADD COLUMN selected boolean DEFAULT true,
ADD COLUMN is_primary boolean DEFAULT false,
ADD COLUMN is_deleted boolean DEFAULT false;

ALTER TABLE calendar_memberships DROP COLUMN IF EXISTS summary_override;

成果物

GET
http://localhost:8081/calendars?user_id=1

・response
[
  {
    "id": 1,
    "summary": "Alice Work",
    "description": "Work calendar for Alice",
    "color": "blue",
    "visibility": "default",
    "hidden": false,
    "selected": true,
    "accessRole": "owner",
    "defaultReminders": [
      {
        "method": "popup",
        "minutes": 10
      }
    ],
    "notificationSettings": {
      "notifications": null
    },
    "primary": true,
    "deleted": false,
    "conferenceProperties": {
      "allowedConferenceSolutionTypes": [
        "hangoutsMeet",
        "eventHangout"
      ]
    }
  },
  {
    "id": 2,
    "summary": "Bob Personal",
    "description": "Personal calendar for Bob",
    "color": "green",
    "visibility": "private",
    "hidden": false,
    "selected": true,
    "accessRole": "owner",
    "defaultReminders": [
      {
        "method": "popup",
        "minutes": 10
      }
    ],
    "notificationSettings": {
      "notifications": null
    },
    "primary": true,
    "deleted": false,
    "conferenceProperties": {
      "allowedConferenceSolutionTypes": [
        "hangoutsMeet",
        "eventHangout"
      ]
    }
  },
  {
    "id": 3,
    "summary": "Team Project",
    "description": "カンファレンスや締切用",
    "color": "blue",
    "visibility": "default",
    "hidden": false,
    "selected": true,
    "accessRole": "owner",
    "defaultReminders": [
      {
        "method": "popup",
        "minutes": 10
      }
    ],
    "notificationSettings": {
      "notifications": null
    },
    "primary": true,
    "deleted": false,
    "conferenceProperties": {
      "allowedConferenceSolutionTypes": [
        "hangoutsMeet",
        "eventHangout"
      ]
    }
  }
]

ソースコード

~/develop/google_calendar_sample  (feat/echo_bun)$ tree -I pgdata/
.
├── docker-compose.yml
├── Dockerfile
├── go.mod
├── go.sum
├── init
│   ├── 01_schema.sql
│   ├── 02_seed.sql
│   └── 03_alter_table.sql
├── internal
│   ├── controller
│   │   └── calendar_controller.go
│   ├── model
│   │   ├── calendar.go
│   │   └── user_settings.go
│   ├── repository
│   │   ├── calendar_repository.go
│   │   └── user_settings_repository.go
│   └── service
│       └── calendar_service.go
main.go
package main

import (
	"database/sql"
	"fmt"
	"log"
	"os"

	"calendar/internal/controller"
	"calendar/internal/repository"
	"calendar/internal/service"

	"github.com/labstack/echo/v4"
	"github.com/uptrace/bun"
	"github.com/uptrace/bun/dialect/pgdialect"
	_ "github.com/uptrace/bun/driver/pgdriver"
)

func main() {
	// Initialize Echo
	e := echo.New()

	// Database connection
	dsn := fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=disable",
		os.Getenv("DB_USER"),
		os.Getenv("DB_PASSWORD"),
		os.Getenv("DB_HOST"),
		os.Getenv("DB_PORT"),
		os.Getenv("DB_NAME"),
	)

	sqldb, err := sql.Open("pg", dsn)
	if err != nil {
		log.Fatal(err)
	}
	db := bun.NewDB(sqldb, pgdialect.New())

	// Test database connection
	if err := db.Ping(); err != nil {
		log.Fatal(err)
	}

	// Initialize repositories
	calendarRepo := repository.NewCalendarRepository(db)
	settingsRepo := repository.NewUserSettingsRepository(db)

	// Initialize service
	calendarService := service.NewCalendarService(calendarRepo, settingsRepo)

	// Initialize controller
	calendarController := controller.NewCalendarController(calendarService)

	// Register routes
	calendarController.RegisterRoutes(e)

	// Start server
	e.Logger.Fatal(e.Start(":8080"))
}

internal/controller/calendar_controller.go
package controller

import (
	"net/http"

	"calendar/internal/controller/dto"
	"calendar/internal/model"
	"calendar/internal/service"

	"github.com/labstack/echo/v4"
)

type CalendarController struct {
	service service.CalendarService
}

func NewCalendarController(service service.CalendarService) *CalendarController {
	return &CalendarController{service: service}
}

func (c *CalendarController) RegisterRoutes(e *echo.Echo) {
	e.GET("/calendars", c.ListCalendars)
}

func (c *CalendarController) ListCalendars(ctx echo.Context) error {
	// リクエストパラメータのバインディング
	req := new(dto.ListCalendarsRequest)
	if err := ctx.Bind(req); err != nil {
		return ctx.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request parameters"})
	}

	// バリデーション
	if req.UserID == 0 {
		return ctx.JSON(http.StatusBadRequest, map[string]string{"error": "user_id is required"})
	}

	// サービス層の呼び出し
	calendars, err := c.service.ListCalendars(ctx.Request().Context(), req.UserID)
	if err != nil {
		return ctx.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
	}

	// レスポンスの変換
	response := make([]dto.CalendarResponse, len(calendars))
	for i, calendar := range calendars {
		response[i] = convertToCalendarResponse(calendar)
	}

	return ctx.JSON(http.StatusOK, response)
}

// convertToCalendarResponse converts a model.Calendar to CalendarResponse
func convertToCalendarResponse(calendar *model.Calendar) dto.CalendarResponse {
	reminders := make([]dto.ReminderResponse, len(calendar.DefaultReminders))
	for i, r := range calendar.DefaultReminders {
		reminders[i] = dto.ReminderResponse{
			Method:  r.Method,
			Minutes: r.Minutes,
		}
	}

	notifications := make([]dto.NotificationResponse, len(calendar.NotificationSettings.Notifications))
	for i, n := range calendar.NotificationSettings.Notifications {
		notifications[i] = dto.NotificationResponse{
			Type:    n.Type,
			Method:  n.Method,
			Enabled: n.Enabled,
		}
	}

	return dto.CalendarResponse{
		ID:                   calendar.ID,
		Summary:              calendar.Summary,
		Description:          calendar.Description,
		Color:                calendar.Color,
		Visibility:           calendar.Visibility,
		SummaryOverride:      calendar.SummaryOverride,
		Hidden:               calendar.Hidden,
		Selected:             calendar.Selected,
		AccessRole:           calendar.AccessRole,
		DefaultReminders:     reminders,
		NotificationSettings: dto.NotificationSettingsResponse{Notifications: notifications},
		Primary:              calendar.Primary,
		Deleted:              calendar.Deleted,
		ConferenceProperties: dto.ConferenceResponse{
			AllowedConferenceSolutionTypes: calendar.ConferenceProperties.AllowedConferenceSolutionTypes,
		},
	}
}

internal/controller/dto/calendar.go
package dto

// CalendarResponse represents the response structure for calendar data
type CalendarResponse struct {
	ID                   int64                        `json:"id"`
	Summary              string                       `json:"summary"`
	Description          string                       `json:"description"`
	Color                string                       `json:"color"`
	Visibility           string                       `json:"visibility"`
	SummaryOverride      string                       `json:"summaryOverride,omitempty"`
	Hidden               bool                         `json:"hidden"`
	Selected             bool                         `json:"selected"`
	AccessRole           string                       `json:"accessRole"`
	DefaultReminders     []ReminderResponse           `json:"defaultReminders"`
	NotificationSettings NotificationSettingsResponse `json:"notificationSettings"`
	Primary              bool                         `json:"primary"`
	Deleted              bool                         `json:"deleted"`
	ConferenceProperties ConferenceResponse           `json:"conferenceProperties"`
}

type ReminderResponse struct {
	Method  string `json:"method"`
	Minutes int    `json:"minutes"`
}

type NotificationResponse struct {
	Type    string `json:"type"`
	Method  string `json:"method"`
	Enabled bool   `json:"enabled"`
}

type NotificationSettingsResponse struct {
	Notifications []NotificationResponse `json:"notifications"`
}

type ConferenceResponse struct {
	AllowedConferenceSolutionTypes []string `json:"allowedConferenceSolutionTypes"`
}

// ListCalendarsRequest represents the request parameters for listing calendars
type ListCalendarsRequest struct {
	UserID int64 `query:"user_id" validate:"required"`
}
internal/service/calendar_service.go
package service

import (
	"context"

	"calendar/internal/model"
	"calendar/internal/repository"
)

type CalendarService interface {
	ListCalendars(ctx context.Context, userID int64) ([]*model.Calendar, error)
}

type calendarService struct {
	calendarRepo repository.CalendarRepository
	settingsRepo repository.UserSettingsRepository
}

func NewCalendarService(
	calendarRepo repository.CalendarRepository,
	settingsRepo repository.UserSettingsRepository,
) CalendarService {
	return &calendarService{
		calendarRepo: calendarRepo,
		settingsRepo: settingsRepo,
	}
}

func (s *calendarService) ListCalendars(ctx context.Context, userID int64) ([]*model.Calendar, error) {
	calendars, err := s.calendarRepo.List(ctx)
	if err != nil {
		return nil, err
	}

	// ユーザー設定の取得
	settings, err := s.settingsRepo.GetByUserID(ctx, userID)
	if err != nil {
		return nil, err
	}

	// レスポンスデータの整形
	for _, calendar := range calendars {
		// ユーザー設定からデフォルトのリマインダー設定を取得
		reminders := make([]model.Reminder, 0)
		for method, minutes := range settings.DefaultReminders {
			reminders = append(reminders, model.Reminder{
				Method:  method,
				Minutes: minutes,
			})
		}
		calendar.DefaultReminders = reminders

		// ユーザー設定から通知設定を取得
		calendar.NotificationSettings = settings.NotificationSettings

		// 会議プロパティ
		calendar.ConferenceProperties = model.ConferenceProperties{
			AllowedConferenceSolutionTypes: []string{"hangoutsMeet", "eventHangout"},
		}

		// その他のデフォルト値
		calendar.Hidden = false
		calendar.Selected = true
		calendar.AccessRole = "owner"
		calendar.Primary = true
		calendar.Deleted = false
	}

	return calendars, nil
}
internal/repository/calendar_repository.go
package repository

import (
	"context"

	"calendar/internal/model"

	"github.com/uptrace/bun"
)

type CalendarRepository interface {
	List(ctx context.Context) ([]*model.Calendar, error)
}

type calendarRepository struct {
	db *bun.DB
}

func NewCalendarRepository(db *bun.DB) CalendarRepository {
	return &calendarRepository{db: db}
}

func (r *calendarRepository) List(ctx context.Context) ([]*model.Calendar, error) {
	var calendars []*model.Calendar
	err := r.db.NewSelect().Model(&calendars).Scan(ctx)
	return calendars, err
}

internal/repository/user_settings_repository.go
package repository

import (
	"context"

	"calendar/internal/model"

	"github.com/uptrace/bun"
)

type UserSettingsRepository interface {
	GetByUserID(ctx context.Context, userID int64) (*model.UserSettings, error)
}

type userSettingsRepository struct {
	db *bun.DB
}

func NewUserSettingsRepository(db *bun.DB) UserSettingsRepository {
	return &userSettingsRepository{db: db}
}

func (r *userSettingsRepository) GetByUserID(ctx context.Context, userID int64) (*model.UserSettings, error) {
	settings := new(model.UserSettings)
	err := r.db.NewSelect().
		Model(settings).
		Where("user_id = ?", userID).
		Scan(ctx)
	return settings, err
}
internal/model/calendar.go
package model

import (
	"time"

	"github.com/uptrace/bun"
)

type Reminder struct {
	Method  string `json:"method"`
	Minutes int    `json:"minutes"`
}

type Notification struct {
	Type    string `json:"type"`
	Method  string `json:"method"`
	Enabled bool   `json:"enabled"`
}

type NotificationSettings struct {
	Notifications []Notification `json:"notifications"`
}

type ConferenceProperties struct {
	AllowedConferenceSolutionTypes []string `json:"allowedConferenceSolutionTypes"`
}

type Calendar struct {
	bun.BaseModel `bun:"table:calendars"`

	ID                   int64                `bun:"id,pk,autoincrement" json:"id"`
	OwnerID              int64                `bun:"owner_id,notnull" json:"-"`
	Summary              string               `bun:"summary,notnull" json:"summary"`
	Description          string               `bun:"description" json:"description"`
	Color                string               `bun:"color" json:"color"`
	Visibility           string               `bun:"visibility" json:"visibility"`
	SummaryOverride      string               `bun:"-" json:"summaryOverride,omitempty"`
	Hidden               bool                 `bun:"-" json:"hidden"`
	Selected             bool                 `bun:"-" json:"selected"`
	AccessRole           string               `bun:"-" json:"accessRole"`
	DefaultReminders     []Reminder           `bun:"-" json:"defaultReminders"`
	NotificationSettings NotificationSettings `bun:"-" json:"notificationSettings"`
	Primary              bool                 `bun:"-" json:"primary"`
	Deleted              bool                 `bun:"-" json:"deleted"`
	ConferenceProperties ConferenceProperties `bun:"-" json:"conferenceProperties"`
	CreatedAt            time.Time            `bun:"created_at,notnull,default:current_timestamp" json:"-"`
	UpdatedAt            time.Time            `bun:"updated_at,notnull,default:current_timestamp" json:"-"`
}

internal/model/user_settings.go
package model

import (
	"time"

	"github.com/uptrace/bun"
)

type UserSettings struct {
	bun.BaseModel `bun:"table:user_settings"`

	ID                   int64                `bun:"id,pk,autoincrement" json:"-"`
	UserID               int64                `bun:"user_id,notnull" json:"-"`
	DefaultReminders     map[string]int       `bun:"default_reminders,type:jsonb" json:"defaultReminders"`
	WorkingHours         []byte               `bun:"working_hours,type:jsonb" json:"-"`
	ColorTheme           string               `bun:"color_theme" json:"-"`
	NotificationSettings NotificationSettings `bun:"notification_settings,type:jsonb" json:"notificationSettings"`
	UpdatedAt            time.Time            `bun:"updated_at,notnull,default:current_timestamp" json:"-"`
}

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?