From b4bc97c2b6b76b912b65bc3bd4eab4bc95cb316d Mon Sep 17 00:00:00 2001 From: Kanta Oikawa Date: Wed, 22 Apr 2026 18:55:35 +0900 Subject: [PATCH 1/9] =?UTF-8?q?=E9=80=9A=E7=9F=A5=E3=83=A2=E3=83=87?= =?UTF-8?q?=E3=83=AB=E3=81=ABFCM=E3=82=AA=E3=83=97=E3=82=B7=E3=83=A7?= =?UTF-8?q?=E3=83=B3=E3=82=92=E8=BF=BD=E5=8A=A0=E3=81=97=E6=9C=AC=E6=96=87?= =?UTF-8?q?=E3=83=95=E3=82=A3=E3=83=BC=E3=83=AB=E3=83=89=E3=82=92Body?= =?UTF-8?q?=E3=81=B8=E6=94=B9=E7=A7=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ImageURL/AnalyticsLabel/APNs(Badge/Sound/ContentAvailable)/Android(ChannelID/Priority/TTLSeconds)/WebpushLinkをドメインとGORMモデルに追加し、FCM送信で扱える追加設定を保持できるようにした。 あわせて本文フィールドをMessageからBodyへ改称し、FCM SDKのNotification.Body命名と揃えた。APIスキーマは後続対応のため変更せず、ハンドラのコンバータ側でBodyとMessageのマッピングを維持している。 Co-Authored-By: Claude Opus 4.7 (1M context) --- internal/database/notification.go | 68 ++++++++++++++++++++++--------- internal/domain/notification.go | 27 ++++++++---- internal/handler/converter.go | 4 +- 3 files changed, 71 insertions(+), 28 deletions(-) diff --git a/internal/database/notification.go b/internal/database/notification.go index 4e3958f..af738d0 100644 --- a/internal/database/notification.go +++ b/internal/database/notification.go @@ -7,10 +7,22 @@ import ( ) type Notification struct { - ID string `gorm:"type:text;primaryKey"` - Title string `gorm:"type:text;not null"` - Message string `gorm:"type:text;not null"` - URL *string `gorm:"type:text"` + ID string `gorm:"type:text;primaryKey"` + + Title string `gorm:"type:text;not null"` + Body string `gorm:"type:text;not null"` + ImageURL *string `gorm:"type:text"` + AnalyticsLabel *string `gorm:"type:text"` + APNsBadge *int `gorm:"type:integer"` + APNsSound *string `gorm:"type:text"` + APNsContentAvailable *bool `gorm:"type:boolean"` + AndroidChannelID *string `gorm:"type:text"` + AndroidPriority *string `gorm:"type:text"` + AndroidTTLSeconds *int `gorm:"type:integer"` + WebpushLink *string `gorm:"type:text"` + + URL *string `gorm:"type:text"` + NotifyAfter time.Time `gorm:"type:timestamptz;not null;index"` NotifyBefore time.Time `gorm:"type:timestamptz;not null;index"` IsNotified bool `gorm:"type:boolean;not null;default:false;index"` @@ -18,25 +30,43 @@ type Notification struct { func (n *Notification) ToDomain(targetUserIDs []string) domain.Notification { return domain.Notification{ - ID: n.ID, - Title: n.Title, - Message: n.Message, - URL: n.URL, - NotifyAfter: n.NotifyAfter, - NotifyBefore: n.NotifyBefore, - IsNotified: n.IsNotified, - TargetUserIDs: targetUserIDs, + ID: n.ID, + Title: n.Title, + Body: n.Body, + ImageURL: n.ImageURL, + AnalyticsLabel: n.AnalyticsLabel, + APNsBadge: n.APNsBadge, + APNsSound: n.APNsSound, + APNsContentAvailable: n.APNsContentAvailable, + AndroidChannelID: n.AndroidChannelID, + AndroidPriority: n.AndroidPriority, + AndroidTTLSeconds: n.AndroidTTLSeconds, + WebpushLink: n.WebpushLink, + URL: n.URL, + NotifyAfter: n.NotifyAfter, + NotifyBefore: n.NotifyBefore, + IsNotified: n.IsNotified, + TargetUserIDs: targetUserIDs, } } func NotificationFromDomain(n domain.Notification) Notification { return Notification{ - ID: n.ID, - Title: n.Title, - Message: n.Message, - URL: n.URL, - NotifyAfter: n.NotifyAfter, - NotifyBefore: n.NotifyBefore, - IsNotified: n.IsNotified, + ID: n.ID, + Title: n.Title, + Body: n.Body, + ImageURL: n.ImageURL, + AnalyticsLabel: n.AnalyticsLabel, + APNsBadge: n.APNsBadge, + APNsSound: n.APNsSound, + APNsContentAvailable: n.APNsContentAvailable, + AndroidChannelID: n.AndroidChannelID, + AndroidPriority: n.AndroidPriority, + AndroidTTLSeconds: n.AndroidTTLSeconds, + WebpushLink: n.WebpushLink, + URL: n.URL, + NotifyAfter: n.NotifyAfter, + NotifyBefore: n.NotifyBefore, + IsNotified: n.IsNotified, } } diff --git a/internal/domain/notification.go b/internal/domain/notification.go index d1a9bd8..9d2373c 100644 --- a/internal/domain/notification.go +++ b/internal/domain/notification.go @@ -3,12 +3,25 @@ package domain import "time" type Notification struct { - ID string - Title string - Message string - URL *string - NotifyAfter time.Time - NotifyBefore time.Time - IsNotified bool + ID string + + Title string + Body string + ImageURL *string + AnalyticsLabel *string + APNsBadge *int + APNsSound *string + APNsContentAvailable *bool + AndroidChannelID *string + AndroidPriority *string + AndroidTTLSeconds *int + WebpushLink *string + + URL *string + + NotifyAfter time.Time + NotifyBefore time.Time + IsNotified bool + TargetUserIDs []string } diff --git a/internal/handler/converter.go b/internal/handler/converter.go index 0a16c0d..3d539c3 100644 --- a/internal/handler/converter.go +++ b/internal/handler/converter.go @@ -85,7 +85,7 @@ func toAPINotification(n domain.Notification) api.Notification { return api.Notification{ Id: n.ID, Title: n.Title, - Message: n.Message, + Message: n.Body, Url: n.URL, NotifyAfter: n.NotifyAfter, NotifyBefore: n.NotifyBefore, @@ -106,7 +106,7 @@ func toDomainNotification(id string, req api.NotificationRequest) domain.Notific return domain.Notification{ ID: id, Title: req.Title, - Message: req.Message, + Body: req.Message, URL: req.Url, NotifyAfter: req.NotifyAfter, NotifyBefore: req.NotifyBefore, From bc07f6102d52977979b71c263a908f054a8fb275 Mon Sep 17 00:00:00 2001 From: Kanta Oikawa Date: Wed, 22 Apr 2026 18:55:40 +0900 Subject: [PATCH 2/9] =?UTF-8?q?=E9=80=9A=E7=9F=A5=E3=83=87=E3=82=A3?= =?UTF-8?q?=E3=82=B9=E3=83=91=E3=83=83=E3=83=81=E3=81=A7FCM=E3=82=AA?= =?UTF-8?q?=E3=83=97=E3=82=B7=E3=83=A7=E3=83=B3=E3=82=92MulticastMessage?= =?UTF-8?q?=E3=81=AB=E5=8F=8D=E6=98=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ドメインに追加したFCMオプション群(ImageURL/AnalyticsLabel/APNs/Android/Webpush)を、MulticastMessageのNotification.ImageURL・FCMOptions・APNSConfig・AndroidConfig・WebpushConfigへそれぞれ設定するようにした。 各プラットフォーム設定は該当フィールドが全てnilのときは付与せず、部分指定でもそのまま送信できるようbuildAndroidConfig/buildAPNSConfig/buildWebpushConfigで組み立てを局所化している。 Co-Authored-By: Claude Opus 4.7 (1M context) --- internal/service/notification_dispatch.go | 75 +++++++++++++++++++++-- 1 file changed, 69 insertions(+), 6 deletions(-) diff --git a/internal/service/notification_dispatch.go b/internal/service/notification_dispatch.go index 9d65f18..78f8a68 100644 --- a/internal/service/notification_dispatch.go +++ b/internal/service/notification_dispatch.go @@ -3,6 +3,7 @@ package service import ( "context" "log" + "time" "firebase.google.com/go/v4/messaging" "github.com/fun-dotto/user-api/internal/domain" @@ -87,16 +88,34 @@ func (s *NotificationService) sendToTokens(ctx context.Context, n domain.Notific data["url"] = *n.URL } + notification := &messaging.Notification{ + Title: n.Title, + Body: n.Body, + } + if n.ImageURL != nil { + notification.ImageURL = *n.ImageURL + } + + var fcmOptions *messaging.FCMOptions + if n.AnalyticsLabel != nil { + fcmOptions = &messaging.FCMOptions{AnalyticsLabel: *n.AnalyticsLabel} + } + + androidConfig := buildAndroidConfig(n) + apnsConfig := buildAPNSConfig(n) + webpushConfig := buildWebpushConfig(n) + totalSuccess := 0 for start := 0; start < len(tokens); start += fcmMulticastBatchSize { end := min(start+fcmMulticastBatchSize, len(tokens)) msg := &messaging.MulticastMessage{ - Tokens: tokens[start:end], - Notification: &messaging.Notification{ - Title: n.Title, - Body: n.Message, - }, - Data: data, + Tokens: tokens[start:end], + Notification: notification, + Data: data, + Android: androidConfig, + APNS: apnsConfig, + Webpush: webpushConfig, + FCMOptions: fcmOptions, } resp, err := s.messagingClient.SendEachForMulticast(ctx, msg) if err != nil { @@ -113,3 +132,47 @@ func (s *NotificationService) sendToTokens(ctx context.Context, n domain.Notific } return totalSuccess, nil } + +func buildAndroidConfig(n domain.Notification) *messaging.AndroidConfig { + if n.AndroidChannelID == nil && n.AndroidPriority == nil && n.AndroidTTLSeconds == nil { + return nil + } + cfg := &messaging.AndroidConfig{} + if n.AndroidPriority != nil { + cfg.Priority = *n.AndroidPriority + } + if n.AndroidTTLSeconds != nil { + ttl := time.Duration(*n.AndroidTTLSeconds) * time.Second + cfg.TTL = &ttl + } + if n.AndroidChannelID != nil { + cfg.Notification = &messaging.AndroidNotification{ChannelID: *n.AndroidChannelID} + } + return cfg +} + +func buildAPNSConfig(n domain.Notification) *messaging.APNSConfig { + if n.APNsBadge == nil && n.APNsSound == nil && n.APNsContentAvailable == nil { + return nil + } + aps := &messaging.Aps{} + if n.APNsBadge != nil { + aps.Badge = n.APNsBadge + } + if n.APNsSound != nil { + aps.Sound = *n.APNsSound + } + if n.APNsContentAvailable != nil { + aps.ContentAvailable = *n.APNsContentAvailable + } + return &messaging.APNSConfig{Payload: &messaging.APNSPayload{Aps: aps}} +} + +func buildWebpushConfig(n domain.Notification) *messaging.WebpushConfig { + if n.WebpushLink == nil { + return nil + } + return &messaging.WebpushConfig{ + FCMOptions: &messaging.WebpushFCMOptions{Link: *n.WebpushLink}, + } +} From ee3e59900a840d43323850506bd5abed66f2662d Mon Sep 17 00:00:00 2001 From: kantacky <51151242+kantacky@users.noreply.github.com> Date: Fri, 24 Apr 2026 11:34:33 +0000 Subject: [PATCH 3/9] Update OpenAPI schema from dotto-typespec --- openapi/openapi.yaml | 109 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 104 insertions(+), 5 deletions(-) diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index 2dbf91e..96725a9 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -403,7 +403,7 @@ components: required: - id - title - - message + - body - notifyAfter - notifyBefore - isNotified @@ -411,15 +411,71 @@ components: properties: id: type: string + description: 通知ID title: type: string - message: + description: 通知タイトル + body: + type: string + description: 通知本文 + imageUrl: + type: string + description: 通知に表示する画像のURL + analyticsLabel: + type: string + description: |- + Firebase Analytics に記録する分析ラベル + + 通知の開封率計測などに利用 + apnsBadge: + type: integer + format: int32 + description: |- + APNs のバッジ数 + + アプリアイコンに表示される数値。0 でバッジを消去 + apnsSound: + type: string + description: |- + APNs の通知音 + + - "default": OS 標準の通知音 + - "" または省略: 無音 + - 任意のファイル名(例: "alert.caf"): アプリバンドルまたは Library/Sounds/ に同梱されたカスタムサウンド + 対応フォーマットは .caf / .aiff / .wav(Linear PCM, MA4, µ-law, a-law)、最大30秒 + apnsContentAvailable: + type: boolean + description: |- + APNs の content-available フラグ + + true でサイレントプッシュ(バックグラウンド更新)になる + androidChannelId: + type: string + description: |- + Android の通知チャンネルID + + Android 8.0 以降、通知はチャンネル単位で管理される + androidPriority: + type: string + description: |- + Android の通知優先度 + + "normal" または "high"。"high" は即時配信される + androidTtlSeconds: + type: integer + format: int32 + description: |- + Android の通知TTL(秒) + + FCM がメッセージを保持する最大時間。期限切れで破棄される + webpushLink: type: string + description: Web Push 通知をクリックした際に開くURL url: type: string description: |- 通知をタップした時に開くURL - アプリを開くのみの場合はnull + アプリを開くのみの場合は未指定 notifyAfter: type: string format: date-time @@ -440,27 +496,70 @@ components: type: object required: - title - - message + - body - notifyAfter - notifyBefore - targetUserIds properties: title: type: string - message: + description: 通知タイトル + body: + type: string + description: 通知本文 + imageUrl: + type: string + description: 通知に表示する画像のURL + analyticsLabel: + type: string + description: Firebase Analytics に記録する分析ラベル + apnsBadge: + type: integer + format: int32 + description: APNs のバッジ数 + apnsSound: + type: string + description: |- + APNs の通知音 + + - "default": OS 標準の通知音 + - "" または省略: 無音 + - 任意のファイル名(例: "alert.caf"): アプリバンドルまたは Library/Sounds/ に同梱されたカスタムサウンド + 対応フォーマットは .caf / .aiff / .wav(Linear PCM, MA4, µ-law, a-law)、最大30秒 + apnsContentAvailable: + type: boolean + description: APNs の content-available フラグ(true でサイレントプッシュ) + androidChannelId: + type: string + description: Android の通知チャンネルID + androidPriority: type: string + description: Android の通知優先度("normal" または "high") + androidTtlSeconds: + type: integer + format: int32 + description: Android の通知TTL(秒) + webpushLink: + type: string + description: Web Push 通知をクリックした際に開くURL url: type: string + description: |- + 通知をタップした時に開くURL + アプリを開くのみの場合は未指定 notifyAfter: type: string format: date-time + description: 通知送信可能になる日時(この時刻以降に送信対象となる) notifyBefore: type: string format: date-time + description: 通知送信期限日時(この時刻を過ぎた場合は送信しない) targetUserIds: type: array items: type: string + description: 対象ユーザーIDのリスト User: type: object required: From e98e74aa8f62bd5ef02b2cd51d2804c315f333ca Mon Sep 17 00:00:00 2001 From: Kanta Oikawa Date: Fri, 24 Apr 2026 20:40:47 +0900 Subject: [PATCH 4/9] Regenerate API code for Notification schema changes Co-Authored-By: Claude Opus 4.7 (1M context) --- generated/api.gen.go | 113 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 103 insertions(+), 10 deletions(-) diff --git a/generated/api.gen.go b/generated/api.gen.go index 39d53a0..e513db5 100644 --- a/generated/api.gen.go +++ b/generated/api.gen.go @@ -161,11 +161,55 @@ type FCMTokenRequest struct { // Notification defines model for Notification. type Notification struct { + // AnalyticsLabel Firebase Analytics に記録する分析ラベル + // + // 通知の開封率計測などに利用 + AnalyticsLabel *string `json:"analyticsLabel,omitempty"` + + // AndroidChannelId Android の通知チャンネルID + // + // Android 8.0 以降、通知はチャンネル単位で管理される + AndroidChannelId *string `json:"androidChannelId,omitempty"` + + // AndroidPriority Android の通知優先度 + // + // "normal" または "high"。"high" は即時配信される + AndroidPriority *string `json:"androidPriority,omitempty"` + + // AndroidTtlSeconds Android の通知TTL(秒) + // + // FCM がメッセージを保持する最大時間。期限切れで破棄される + AndroidTtlSeconds *int32 `json:"androidTtlSeconds,omitempty"` + + // ApnsBadge APNs のバッジ数 + // + // アプリアイコンに表示される数値。0 でバッジを消去 + ApnsBadge *int32 `json:"apnsBadge,omitempty"` + + // ApnsContentAvailable APNs の content-available フラグ + // + // true でサイレントプッシュ(バックグラウンド更新)になる + ApnsContentAvailable *bool `json:"apnsContentAvailable,omitempty"` + + // ApnsSound APNs の通知音 + // + // - "default": OS 標準の通知音 + // - "" または省略: 無音 + // - 任意のファイル名(例: "alert.caf"): アプリバンドルまたは Library/Sounds/ に同梱されたカスタムサウンド + // 対応フォーマットは .caf / .aiff / .wav(Linear PCM, MA4, µ-law, a-law)、最大30秒 + ApnsSound *string `json:"apnsSound,omitempty"` + + // Body 通知本文 + Body string `json:"body"` + + // Id 通知ID Id string `json:"id"` + // ImageUrl 通知に表示する画像のURL + ImageUrl *string `json:"imageUrl,omitempty"` + // IsNotified 通知が送信されたかどうか - IsNotified bool `json:"isNotified"` - Message string `json:"message"` + IsNotified bool `json:"isNotified"` // NotifyAfter 通知送信可能になる日時(この時刻以降に送信対象となる) NotifyAfter time.Time `json:"notifyAfter"` @@ -175,21 +219,70 @@ type Notification struct { // TargetUserIds 対象ユーザーIDのリスト TargetUserIds []string `json:"targetUserIds"` - Title string `json:"title"` + + // Title 通知タイトル + Title string `json:"title"` // Url 通知をタップした時に開くURL - // アプリを開くのみの場合はnull + // アプリを開くのみの場合は未指定 Url *string `json:"url,omitempty"` + + // WebpushLink Web Push 通知をクリックした際に開くURL + WebpushLink *string `json:"webpushLink,omitempty"` } // NotificationRequest defines model for NotificationRequest. type NotificationRequest struct { - Message string `json:"message"` - NotifyAfter time.Time `json:"notifyAfter"` - NotifyBefore time.Time `json:"notifyBefore"` - TargetUserIds []string `json:"targetUserIds"` - Title string `json:"title"` - Url *string `json:"url,omitempty"` + // AnalyticsLabel Firebase Analytics に記録する分析ラベル + AnalyticsLabel *string `json:"analyticsLabel,omitempty"` + + // AndroidChannelId Android の通知チャンネルID + AndroidChannelId *string `json:"androidChannelId,omitempty"` + + // AndroidPriority Android の通知優先度("normal" または "high") + AndroidPriority *string `json:"androidPriority,omitempty"` + + // AndroidTtlSeconds Android の通知TTL(秒) + AndroidTtlSeconds *int32 `json:"androidTtlSeconds,omitempty"` + + // ApnsBadge APNs のバッジ数 + ApnsBadge *int32 `json:"apnsBadge,omitempty"` + + // ApnsContentAvailable APNs の content-available フラグ(true でサイレントプッシュ) + ApnsContentAvailable *bool `json:"apnsContentAvailable,omitempty"` + + // ApnsSound APNs の通知音 + // + // - "default": OS 標準の通知音 + // - "" または省略: 無音 + // - 任意のファイル名(例: "alert.caf"): アプリバンドルまたは Library/Sounds/ に同梱されたカスタムサウンド + // 対応フォーマットは .caf / .aiff / .wav(Linear PCM, MA4, µ-law, a-law)、最大30秒 + ApnsSound *string `json:"apnsSound,omitempty"` + + // Body 通知本文 + Body string `json:"body"` + + // ImageUrl 通知に表示する画像のURL + ImageUrl *string `json:"imageUrl,omitempty"` + + // NotifyAfter 通知送信可能になる日時(この時刻以降に送信対象となる) + NotifyAfter time.Time `json:"notifyAfter"` + + // NotifyBefore 通知送信期限日時(この時刻を過ぎた場合は送信しない) + NotifyBefore time.Time `json:"notifyBefore"` + + // TargetUserIds 対象ユーザーIDのリスト + TargetUserIds []string `json:"targetUserIds"` + + // Title 通知タイトル + Title string `json:"title"` + + // Url 通知をタップした時に開くURL + // アプリを開くのみの場合は未指定 + Url *string `json:"url,omitempty"` + + // WebpushLink Web Push 通知をクリックした際に開くURL + WebpushLink *string `json:"webpushLink,omitempty"` } // User defines model for User. From b776a571f142d2e5a0773f0f9c1ebda3105a60f3 Mon Sep 17 00:00:00 2001 From: Kanta Oikawa Date: Fri, 24 Apr 2026 20:40:47 +0900 Subject: [PATCH 5/9] Map Notification body field to domain Message Co-Authored-By: Claude Opus 4.7 (1M context) --- internal/handler/converter.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/handler/converter.go b/internal/handler/converter.go index 0a16c0d..48077a2 100644 --- a/internal/handler/converter.go +++ b/internal/handler/converter.go @@ -85,7 +85,7 @@ func toAPINotification(n domain.Notification) api.Notification { return api.Notification{ Id: n.ID, Title: n.Title, - Message: n.Message, + Body: n.Message, Url: n.URL, NotifyAfter: n.NotifyAfter, NotifyBefore: n.NotifyBefore, @@ -106,7 +106,7 @@ func toDomainNotification(id string, req api.NotificationRequest) domain.Notific return domain.Notification{ ID: id, Title: req.Title, - Message: req.Message, + Message: req.Body, URL: req.Url, NotifyAfter: req.NotifyAfter, NotifyBefore: req.NotifyBefore, From 90afc1dcdb83f945bbd27e58b5bac2fdb1596145 Mon Sep 17 00:00:00 2001 From: Kanta Oikawa Date: Fri, 24 Apr 2026 20:45:59 +0900 Subject: [PATCH 6/9] =?UTF-8?q?Notification=20Converter=E3=81=A7=20FCM=20?= =?UTF-8?q?=E3=82=AA=E3=83=97=E3=82=B7=E3=83=A7=E3=83=B3=E9=A0=85=E7=9B=AE?= =?UTF-8?q?=E3=82=82=E3=83=9E=E3=83=83=E3=83=94=E3=83=B3=E3=82=B0=E3=81=99?= =?UTF-8?q?=E3=82=8B=E3=82=88=E3=81=86=E6=8B=A1=E5=BC=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Notification のドメインモデルとAPI型に追加されていた ImageURL / AnalyticsLabel / APNs・Android の配信オプション / WebpushLink などのフィールドが、Converter で取りこぼされていた。 toAPINotification と toDomainNotification の双方で これらのフィールドを相互に変換するようにし、 APNsBadge と AndroidTTLSeconds については ドメイン側の int とAPI側の int32 の差異を明示的に変換する。 --- internal/handler/converter.go | 74 ++++++++++++++++++++++++++--------- 1 file changed, 55 insertions(+), 19 deletions(-) diff --git a/internal/handler/converter.go b/internal/handler/converter.go index 7e442bf..726e316 100644 --- a/internal/handler/converter.go +++ b/internal/handler/converter.go @@ -82,16 +82,34 @@ func toDomainFCMToken(req api.FCMTokenRequest) domain.FCMToken { } func toAPINotification(n domain.Notification) api.Notification { - return api.Notification{ - Id: n.ID, - Title: n.Title, - Body: n.Body, - Url: n.URL, - NotifyAfter: n.NotifyAfter, - NotifyBefore: n.NotifyBefore, - IsNotified: n.IsNotified, - TargetUserIds: n.TargetUserIDs, - } + notification := api.Notification{ + Id: n.ID, + Title: n.Title, + Body: n.Body, + ImageUrl: n.ImageURL, + AnalyticsLabel: n.AnalyticsLabel, + ApnsSound: n.APNsSound, + ApnsContentAvailable: n.APNsContentAvailable, + AndroidChannelId: n.AndroidChannelID, + AndroidPriority: n.AndroidPriority, + WebpushLink: n.WebpushLink, + Url: n.URL, + NotifyAfter: n.NotifyAfter, + NotifyBefore: n.NotifyBefore, + IsNotified: n.IsNotified, + TargetUserIds: n.TargetUserIDs, + } + + if n.APNsBadge != nil { + badge := int32(*n.APNsBadge) + notification.ApnsBadge = &badge + } + if n.AndroidTTLSeconds != nil { + ttl := int32(*n.AndroidTTLSeconds) + notification.AndroidTtlSeconds = &ttl + } + + return notification } func toAPINotifications(notifications []domain.Notification) []api.Notification { @@ -103,15 +121,33 @@ func toAPINotifications(notifications []domain.Notification) []api.Notification } func toDomainNotification(id string, req api.NotificationRequest) domain.Notification { - return domain.Notification{ - ID: id, - Title: req.Title, - Body: req.Body, - URL: req.Url, - NotifyAfter: req.NotifyAfter, - NotifyBefore: req.NotifyBefore, - TargetUserIDs: req.TargetUserIds, - } + notification := domain.Notification{ + ID: id, + Title: req.Title, + Body: req.Body, + ImageURL: req.ImageUrl, + AnalyticsLabel: req.AnalyticsLabel, + APNsSound: req.ApnsSound, + APNsContentAvailable: req.ApnsContentAvailable, + AndroidChannelID: req.AndroidChannelId, + AndroidPriority: req.AndroidPriority, + WebpushLink: req.WebpushLink, + URL: req.Url, + NotifyAfter: req.NotifyAfter, + NotifyBefore: req.NotifyBefore, + TargetUserIDs: req.TargetUserIds, + } + + if req.ApnsBadge != nil { + badge := int(*req.ApnsBadge) + notification.APNsBadge = &badge + } + if req.AndroidTtlSeconds != nil { + ttl := int(*req.AndroidTtlSeconds) + notification.AndroidTTLSeconds = &ttl + } + + return notification } func toDomainNotificationListFilter(params api.NotificationV1ListParams) domain.NotificationListFilter { From b2f35ab2036616fb66125550f92f63872dac9b86 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 24 Apr 2026 12:09:39 +0000 Subject: [PATCH 7/9] Update OpenAPI schema from dotto-typespec --- openapi/openapi.yaml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index 96725a9..f35fc9a 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -429,7 +429,6 @@ components: 通知の開封率計測などに利用 apnsBadge: type: integer - format: int32 description: |- APNs のバッジ数 @@ -463,7 +462,6 @@ components: "normal" または "high"。"high" は即時配信される androidTtlSeconds: type: integer - format: int32 description: |- Android の通知TTL(秒) @@ -515,7 +513,6 @@ components: description: Firebase Analytics に記録する分析ラベル apnsBadge: type: integer - format: int32 description: APNs のバッジ数 apnsSound: type: string @@ -537,7 +534,6 @@ components: description: Android の通知優先度("normal" または "high") androidTtlSeconds: type: integer - format: int32 description: Android の通知TTL(秒) webpushLink: type: string From 8d93e9998a20024e51cf9e6806be1f174a08d3c3 Mon Sep 17 00:00:00 2001 From: Kanta Oikawa Date: Fri, 24 Apr 2026 21:12:51 +0900 Subject: [PATCH 8/9] =?UTF-8?q?Notification=E3=82=B9=E3=82=AD=E3=83=BC?= =?UTF-8?q?=E3=83=9E=E3=81=AE=E6=95=B0=E5=80=A4=E5=9E=8B=E5=A4=89=E6=9B=B4?= =?UTF-8?q?=E3=81=AB=E7=94=9F=E6=88=90=E3=82=B3=E3=83=BC=E3=83=89=E3=82=92?= =?UTF-8?q?=E8=BF=BD=E5=BE=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit androidTtlSeconds と apnsBadge の型が int32 から int に変更されたため、生成コードを再生成した。 Co-Authored-By: Claude Opus 4.7 (1M context) --- generated/api.gen.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/generated/api.gen.go b/generated/api.gen.go index e513db5..916e0b5 100644 --- a/generated/api.gen.go +++ b/generated/api.gen.go @@ -179,12 +179,12 @@ type Notification struct { // AndroidTtlSeconds Android の通知TTL(秒) // // FCM がメッセージを保持する最大時間。期限切れで破棄される - AndroidTtlSeconds *int32 `json:"androidTtlSeconds,omitempty"` + AndroidTtlSeconds *int `json:"androidTtlSeconds,omitempty"` // ApnsBadge APNs のバッジ数 // // アプリアイコンに表示される数値。0 でバッジを消去 - ApnsBadge *int32 `json:"apnsBadge,omitempty"` + ApnsBadge *int `json:"apnsBadge,omitempty"` // ApnsContentAvailable APNs の content-available フラグ // @@ -243,10 +243,10 @@ type NotificationRequest struct { AndroidPriority *string `json:"androidPriority,omitempty"` // AndroidTtlSeconds Android の通知TTL(秒) - AndroidTtlSeconds *int32 `json:"androidTtlSeconds,omitempty"` + AndroidTtlSeconds *int `json:"androidTtlSeconds,omitempty"` // ApnsBadge APNs のバッジ数 - ApnsBadge *int32 `json:"apnsBadge,omitempty"` + ApnsBadge *int `json:"apnsBadge,omitempty"` // ApnsContentAvailable APNs の content-available フラグ(true でサイレントプッシュ) ApnsContentAvailable *bool `json:"apnsContentAvailable,omitempty"` From ff34db47cb3b5c7919d7a23d064ce4bc0091a147 Mon Sep 17 00:00:00 2001 From: Kanta Oikawa Date: Fri, 24 Apr 2026 21:15:55 +0900 Subject: [PATCH 9/9] =?UTF-8?q?Notification=20Converter=E3=81=AE=E6=95=B0?= =?UTF-8?q?=E5=80=A4=E5=9E=8B=E5=A4=89=E6=8F=9B=E3=81=A8=E6=9D=A1=E4=BB=B6?= =?UTF-8?q?=E5=88=86=E5=B2=90=E3=82=92=E6=95=B4=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit APIとドメイン双方で APNsBadge / AndroidTTLSeconds が *int に統一されたため、 不要な int/int32 変換と nil 分岐を削除し、構造体リテラル内で直接代入する形に整理した。 振る舞いは変わらず、ビルド不整合の解消とコードの簡素化のみを行う。 --- internal/handler/converter.go | 30 ++++++------------------------ 1 file changed, 6 insertions(+), 24 deletions(-) diff --git a/internal/handler/converter.go b/internal/handler/converter.go index 726e316..e3c10a7 100644 --- a/internal/handler/converter.go +++ b/internal/handler/converter.go @@ -82,16 +82,18 @@ func toDomainFCMToken(req api.FCMTokenRequest) domain.FCMToken { } func toAPINotification(n domain.Notification) api.Notification { - notification := api.Notification{ + return api.Notification{ Id: n.ID, Title: n.Title, Body: n.Body, ImageUrl: n.ImageURL, AnalyticsLabel: n.AnalyticsLabel, ApnsSound: n.APNsSound, + ApnsBadge: n.APNsBadge, ApnsContentAvailable: n.APNsContentAvailable, AndroidChannelId: n.AndroidChannelID, AndroidPriority: n.AndroidPriority, + AndroidTtlSeconds: n.AndroidTTLSeconds, WebpushLink: n.WebpushLink, Url: n.URL, NotifyAfter: n.NotifyAfter, @@ -99,17 +101,6 @@ func toAPINotification(n domain.Notification) api.Notification { IsNotified: n.IsNotified, TargetUserIds: n.TargetUserIDs, } - - if n.APNsBadge != nil { - badge := int32(*n.APNsBadge) - notification.ApnsBadge = &badge - } - if n.AndroidTTLSeconds != nil { - ttl := int32(*n.AndroidTTLSeconds) - notification.AndroidTtlSeconds = &ttl - } - - return notification } func toAPINotifications(notifications []domain.Notification) []api.Notification { @@ -121,33 +112,24 @@ func toAPINotifications(notifications []domain.Notification) []api.Notification } func toDomainNotification(id string, req api.NotificationRequest) domain.Notification { - notification := domain.Notification{ + return domain.Notification{ ID: id, Title: req.Title, Body: req.Body, ImageURL: req.ImageUrl, AnalyticsLabel: req.AnalyticsLabel, APNsSound: req.ApnsSound, + APNsBadge: req.ApnsBadge, APNsContentAvailable: req.ApnsContentAvailable, AndroidChannelID: req.AndroidChannelId, AndroidPriority: req.AndroidPriority, + AndroidTTLSeconds: req.AndroidTtlSeconds, WebpushLink: req.WebpushLink, URL: req.Url, NotifyAfter: req.NotifyAfter, NotifyBefore: req.NotifyBefore, TargetUserIDs: req.TargetUserIds, } - - if req.ApnsBadge != nil { - badge := int(*req.ApnsBadge) - notification.APNsBadge = &badge - } - if req.AndroidTtlSeconds != nil { - ttl := int(*req.AndroidTtlSeconds) - notification.AndroidTTLSeconds = &ttl - } - - return notification } func toDomainNotificationListFilter(params api.NotificationV1ListParams) domain.NotificationListFilter {