[x] 0. 動作確認用ローカルフィードサーバーをセットアップする
/tmp/feed.json にサンプルフィードファイルを作成する。emoji あり・なし(未設定時は 📢 フォールバック確認)、title/body の多言語フィールド(ja_JP, en_US)、url あり・なし、conditions.targetRoles(admin のみ、全ユーザー)の両パターンを含む複数アイテムで構成するcd /tmp && python3 -m http.server 8099 を起動し、http://localhost:8099/feed.json でアクセスできることを確認する.env に NEWS_FEED_URL=http://localhost:8099/feed.json を追加する[x] 1. データモデルを実装する
[x] 1.1 (P) NewsItem モデルを実装する
externalId(ユニークインデックス)、多言語 title/body(Map of String)、emoji、url、publishedAt(インデックス)、fetchedAt(TTL 90日インデックス)、conditions.targetRoles を持つ Mongoose スキーマを定義するINewsItem と INewsItemHasId を定義する[x] 1.2 (P) NewsReadStatus モデルを実装する
userId・newsItemId の複合ユニークインデックス、readAt を持つ Mongoose スキーマを定義するINewsReadStatus を定義する[x] 2. ニュースサービス層を実装する
[x] 2.1 ニュース一覧取得ロジックを実装する
listForUser(userId, userRoles, { limit, offset, onlyUnread }) を実装するconditions.targetRoles が未設定または userRoles に一致するアイテムのみ返すロール別フィルタを適用するisRead: boolean を付与するpublishedAt 降順で返す[x] 2.2 既読管理ロジックを実装する
markRead(userId, newsItemId) を実装する。NewsReadStatus を upsert することで冪等性を保証するmarkAllRead(userId, userRoles) を実装する。ロール別フィルタに合致する全未読アイテムを一括既読にするgetUnreadCount(userId, userRoles) を実装する[x] 2.3 フィード同期ロジックを実装する
upsertNewsItems(items) を実装する。externalId をキーに upsert し、fetchedAt を更新するdeleteNewsItemsByExternalIds(externalIds) を実装する[x] 3. News API エンドポイントを実装する
[x] 3.1 (P) ニュース取得エンドポイントを実装する
GET /apiv3/news/list(limit, offset, onlyUnread クエリパラメータ)を実装するGET /apiv3/news/unread-count を実装するloginRequiredStrictly と accessTokenParser を適用する[x] 3.2 (P) ニュース既読操作エンドポイントを実装する
POST /apiv3/news/mark-read(newsItemId を受け取る)を実装する。newsItemId を mongoose.isValidObjectId() で検証するPOST /apiv3/news/mark-all-read を実装するloginRequiredStrictly と accessTokenParser を適用する[x] 3.3 News API ルートをアプリに登録する
news.ts を追加する[x] 4. NewsCronService を実装する
[x] 4.1 (P) フィード取得・DB 同期処理を実装する
CronService を継承し getCronSchedule() で '0 1 * * *' を返すexecuteJob() を実装する:NEWS_FEED_URL 未設定時はスキップ、HTTP GET、取得失敗時はログ記録のみ(既存データ維持)growiVersionRegExps と現バージョンを照合し、不一致アイテムを除外する。不正 regex は try-catch でスキップしてログ警告する[x] 4.2 cron をアプリ起動時に登録する
NewsCronService.startCron() を呼ぶ[x] 5. フロントエンド SWR フックを実装する
[x] 5.1 (P) ニュース用 SWR フックを新設する
useSWRINFxNews(limit, options) を useSWRInfinite ベースで実装する。キーに limit, pageIndex, onlyUnread を含めるuseSWRxNewsUnreadCount() を実装する[x] 5.2 (P) InAppNotification 用の無限スクロール対応フックを追加する
useSWRxInAppNotifications(useSWR ベース)に加えて useSWRINFxInAppNotifications(limit, options) を useSWRInfinite ベースで新設するInAppNotificationPage.tsx での利用のため維持する[x] 6. InAppNotification パネルを改修する
[x] 6.1 フィルタタブを追加する
InAppNotification.tsx に activeFilter: 'all' | 'news' | 'notifications' の state(デフォルト 'all')を追加し、InAppNotificationForms と InAppNotificationContent へ prop として渡すInAppNotificationForms に Bootstrap btn-group でフィルタボタン(「すべて」「通知」「お知らせ」)を追加する。既存「未読のみ」トグルは維持する[x] 6.2 無限スクロールを導入する
InAppNotificationContent で useSWRINFxNews と useSWRINFxInAppNotifications を使用するよう変更するInfiniteScroll コンポーネントをラップしてリストを表示する// TODO: Infinite scroll implemented コメントを解消する[x] 6.3 「すべて」フィルタ時のクライアントサイドマージを実装する
activeFilter === 'all' の場合、通知(createdAt)とニュース(publishedAt)を日時降順でマージして表示するactiveFilter === 'news' の場合は NewsItem のみ、activeFilter === 'notifications' の場合は InAppNotification のみ表示する[x] 7. NewsItem コンポーネントを実装する
[x] 7.1 (P) ニュースアイテムの表示コンポーネントを実装する
emoji フィールドをタイトル前に表示する。未設定時は 📢 をフォールバックとするbrowserLocale → ja_JP → en_US → 最初に利用可能なキーfw-bold + 左端に bg-primary 8px 丸ドット、既読時は fw-normal + 同幅の透明スペーサーで表示する[x] 7.2 (P) ニュースアイテムのクリック処理を実装する
POST /apiv3/news/mark-read を呼び、SWR キャッシュを mutate して未読インジケータを更新するurl が設定されている場合は新しいタブで開く[x] 8. (P) 未読バッジにニュース未読数を合算する
PrimaryItemForNotification で useSWRxNewsUnreadCount を呼び、既存の InAppNotification 未読カウントと合算してバッジに表示する[x] 9. (P) i18n ロケールファイルを更新する
commons.json の in_app_notification 名前空間に以下のキーを全ロケール(ja_JP, en_US, zh_CN, ko_KR, fr_FR)に追加する:news(お知らせ)、notifications(通知)、all(すべて)、no_news(ニュースはありません)[x] 10. サーバーサイドテストを実装する
[x] 10.1 NewsCronService のテストを実装する
executeJob() が正常取得時に upsert・削除を行うことを確認するNEWS_FEED_URL 未設定時にスキップすることを確認するgrowiVersionRegExps の一致・不一致・不正 regex の各ケースをテストする[x] 10.2 NewsService のテストを実装する
listForUser() がロール別フィルタを正しく適用し isRead を付与することを確認するonlyUnread=true で未読のみ返ることを確認するmarkRead() の冪等性(2回呼んでもエラーなし)を確認するgetUnreadCount() が markAllRead() 後に 0 を返すことを確認する[x] 10.3 News API 統合テストを実装する
GET /apiv3/news/list がロール別フィルタを強制することを確認するPOST /apiv3/news/mark-read が冪等であることを確認する[x] 11. フロントエンドテストを実装する
[x] 11.1 NewsItem コンポーネントのテストを実装する
emoji 未設定時に 📢 が表示されることをテストするbrowserLocale → ja_JP → en_US)をテストするfw-bold、青ドット、スペーサー)をテストするmark-read が呼ばれ、url がある場合に新タブで開くことをテストする[x]* 11.2 InAppNotification パネルのフィルタ動作をテストする
[x] 12. 既存コードの不具合修正(実装後検証で発覚)
[x] 12.1 既存通知の未読ドットを修正する
InAppNotificationElm.tsx の grw-unopend-notification クラスに対応する CSS 定義がコードベースに存在しないため、未読ドットが表示されないwidth/height/display: inline-block のインラインスタイルを追加する[x] 12.2 全面サイドバー(② dock/drawer モード)での通知表示エリアを拡張する
InAppNotificationSubstance.tsx の各フィルタ表示エリアに style={{ maxHeight: '60vh' }} が固定されており、② dock/drawer モードでもホバーパネル(①)サイズに制限されるuseSidebarMode() で collapsed モードを判定し、collapsed 時のみ maxHeight: '60vh' を適用する。dock/drawer モードでは制約を外し、外側の SimpleBar コンテナによるスクロールに委ねる[x] 12.3 アプリ内通知の未読ドットをクリック時に即時消去する
InAppNotificationSubstance.tsx の handleNotificationRead で useSWRInfinite の mutate(updater, { revalidate: false }) を使って既読状態をキャッシュに書き込もうとしていたが、ナビゲーション(<a href>)によってコンポーネントがアンマウントされた後に useSWRInfinite のページ単位キャッシュが古い状態に戻るため、ドットが再表示されるuseState<Set<string>> でローカルに開封済み通知 ID を管理し、各 InAppNotificationElm のレンダリング時に status をその場でオーバーライドすることで、SWR キャッシュに依存せず即時反映を実現する[x] 13. PR レビュー FB 対応によるコード品質改善
[x] 13.1 型アサーションを排除する(FB ①)
interfaces/in-app-notification.ts に IInAppNotificationHasId = IInAppNotification & HasObjectId を追加stores/in-app-notification.ts で apiv3Get<InAppNotificationPaginateResult>() にジェネリクスを注入し、response.data as ... を削除InAppNotificationSubstance.tsx の allModeSWRResponse を SWRInfiniteResponse<PaginateResult<INewsItemWithReadStatus>, Error> として明示的に宣言し、as unknown as Parameters<typeof InfiniteScroll>[0]... を撤去notificationResponse.data.flatMap(...) as (IInAppNotification & HasObjectId)[] の cast を削除(型情報が自然に流れる)[x] 13.2 SWR state-less による未読ドット即時消去へ差し替える(FB ②)
useState<Set<string>> を撤去し、notificationResponse.mutate((pages) => ..., { revalidate: false }) による SWR ネイティブの楽観更新に置換SWRConfig プロバイダのキャッシュ Map がアンマウント/リマウントを跨いで保持されるため、再マウント時もドットは消えたまま(実機検証済み)[x] 13.3 NewsItem の言語ユーティリティと Bootstrap クラスを既存パターンに統一する(FB ③)
navigator.language の独自ロジックを撤去し、useTranslation() の i18n.language を使用(ActivityListItem と同パターン)date-fns format + getLocale(i18n.language) に統一cursor/width/textAlign/background)を Bootstrap クラス w-100 text-start bg-transparent に置換fontSize/lineHeight を fs-5 lh-1 に置換UnreadDot.module.scss の共通 CSS Module に抽出し、NewsItem.tsx と InAppNotificationElm.tsx の両者から参照して見た目を統一browserLanguage prop を廃止し、テストも i18n モックへ合わせて更新