無料主義アプリ(freeism-app)
詳細
- GitHub:https://github.com/yuichisugio/freeism-app_v1
- ※今後、大きく仕様が変更する&残しておきたいコードもあるため URL が変わる可能性あり
- Zenn
Books:https://zenn.dev/329/books/0dbe4578af702a
概要
- 無料主義アプリ(
freeism-app)は、無料主義の管理を行うことができるアプリです - 無料主義とは
- 何かしら作業した人を第三者がそれぞれの評価軸で評価して、その評価結果をもとに他者が提供する商材を優先的に得られるようにする仕組みです
- 作成した理由・解決したい課題・なぜ作ったか
- OSS などの収益化できない or 報酬が十分ではない労働に対して、報酬を与えられる仕組みを作りたいと考えました
- まずは、無料主義の仕組みを理解してもらい、どうすればより良い仕組みとなるかの材料としたいと考えました
- ターゲット層
- 無料主義の仕組みを実践してみたい人
- 初期はプログラマー向け
- 実装期間
- 2025 年 01 月〜2025 年 07 月
- バージョン
- freeism-app ver1
実装の進め方
- 仕様を markdown にまとめます
- 仕様 markdown を Cursor に渡して AI に要件定義・設計漏れを指摘してもらい、追記します
- 仕様 markdown を Cursor に渡して AI に実行タスクをまとめてもらいます
- 仕様/タスク markdown を Cursor に渡して AI に実装してもらいます
- AI の実装コードをすべて読んで、理解できない点があれば解説してもらい、ロジックを理解できる状態にします
エラー対処法
- Cursor に、エラーログと関連コードを渡して修正してもらいます
- それでも直らない場合は、
console.log(),console.trace()を記載してバグ箇所を特定して手動 or AI に修正依頼します - その後、関連コードにバグりにくいコードに変更 & わかりやすく注意するコメントを入れます
- テストコードを実行したり、Playwright MCP を使用したり、LocalHost から操作して、バグがある場合は AI に依頼 or 手動で修正します
実装で意識したこと
- ライブラリなどを利用するときは、ブログや記事や AI の解説だけでなく、公式ドキュメントの確認を習慣づけました
アプリ作成で苦労した点
-
必要工数の算出
- 説明
- どの機能をどれだけで実装できるか算定が難しかったです
- 必要工数の算出が甘かったが仕様に入れてしまったため、アプリ作成が大きく遅れました
- 解決方法
- 感覚的に判断せず、実装方法と実装によるよくあるバグを AI で調査した結果と、影響範囲の小さい部分で最小限の実装をしてみて、どれくらいの期間がかかるか判断して、工数算出の精度を高くしました
- 様々な機能を実装することで難易度/工数を算出する能力が上がると実感したため、本質的ではないが経験を積めるいろいろな機能を実装して、工数算出の精度を高くしました
- 例
- 全画面表示に対応する機能の実装
- 工数予測は、簡単に実装できるので 1 日だと考えていました
- 実際は、全画面にすると Index の関係でテーブル内の Sort や Filter 機能が表示されなくなったため 3 日ほど必要でした
- 全画面表示に対応する機能の実装
- 説明
-
要件定義
- アプリとして必要な本質的な機能は何なのかを考える作業
- 例)オークション機能では「出品者が商品を登録できる」「入札者が価格を提示できる」という 2 つのコア機能に絞り込み、通知機能は後回しにしました
- 解決方法
- ユーザーストーリーマッピングで優先順位付けしました
- 「誰が」「何のために」「何をする」を整理し、Minimum Viable Product(MVP)として最小限必要な機能を定義しました
- 類似サービスの分析と GitHub Issues の調査をしました
- 既存のオークションサービス(ヤフオク、メルカリ等)のコア機能を分析しました
- Claude 等の AI で技術的実現可能性を確認しながら仕様を詰めました
- 実装難易度が高すぎる機能は代替案を検討しました
- ユーザーストーリーマッピングで優先順位付けしました
- 「エッジケース対応や機能の UI/UX の充実さ」と「アプリ実装の難易度や実装期間」のトレードオフがあった際の意思決定
- 例)オークション入札では、
Server-Sent Events,WebSocketのどちらが必要か、またはどちらも不要なのか - 解決方法
- 技術選択の判断基準を 3 軸で評価しました
- ユーザー体験への影響度(高:Server-Sent Events 採用、低:ポーリング可)
- 実装・運用コスト(学習時間、サーバー負荷、障害対応の複雑さ)
- アプリの目的(無料主義の仕組み理解を目的とするので、エッジケースや UI/UX よりロジックの可読性を意識しました)
- 技術選択の判断基準を 3 軸で評価しました
- 例)オークション入札では、
- 技術の選定
- どのライブラリ・フレームワークを使用するのか、AI や技術記事からメリット・デメリットを判断して選ぶ作業
- 解決方法
- 公式ドキュメントと技術記事の比較検討をしました
- Next.js の App Router と Pages Router の選択では、公式ドキュメントの推奨事項と実際の採用事例(Vercel のブログ、GitHub Discussions)を照らし合わせて判断しました
- O/R マッパ の選定では、Prisma、Drizzle、TypeORM の公式ドキュメントを読み比べ、型安全性と開発体験を重視して Prisma を選択しました
- コミュニティの活発さとメンテナンス状況の確認をしました
- GitHub のスター数、Issue/PR の対応速度、最終更新日を確認しました
- 長期的に使用できる技術かを判断材料に含めました
- 公式ドキュメントと技術記事の比較検討をしました
- デザインの決定
- どのような UI にするのか
- 解決方法
- 既存サービスの UI/UX パターン分析をしました
- 類似サービスの優れた UI を Figma で模写しながら学習しました
- Material Design、Human Interface Guidelines などのデザインシステムから一般的なパターンを理解しました
- Claude に複数の UI を実装してもらい、Claude 内の Preview から UI を選定しました
- 既存サービスの UI/UX パターン分析をしました
- 解決方法
- どのような UI にするのか
- アプリとして必要な本質的な機能は何なのかを考える作業
-
AI の使い方
- 苦労した点
- ハルシネーションに惑わされました。ドキュメント/GitHub Issues をよく読む重要性を実感しました
- 例
- Auth.js の設定方法を AI に聞いた際、v4 の情報と v5 の情報が混在しました
- Next.js の App Router と Pages Router が混在しました
- 解決方法
- AI 出力の検証プロセスの確立
- AI が提示したコードや情報について、必ず公式ドキュメントで裏取りする習慣を徹底しました
- 特にバージョン依存の情報(非推奨 API、破壊的変更等)は、公式のマイグレーションガイドや CHANGELOG で確認してから採用しました
- プロンプトエンジニアリングの改善
- 曖昧な質問を避け、バージョン番号や実現したい具体的な挙動を明示しました
- 「Next.js 14 の App Router で、Server Actions を使ってフォーム送信する方法」のように技術スタックを具体的に指定しました
- エラー解決時の段階的なアプローチ
- まずエラーメッセージをそのまま Google 検索し、Stack Overflow や GitHub Issues で同様の問題と解決策を確認しました
- 公式ドキュメントの該当箇所を読んで根本原因を理解しました
- その上で AI に「このエラーの原因は ○ だと理解したが、解決方法として △ と □ のどちらが適切か」と具体的な選択肢を提示して意見を求めました
- Model Context Protocol(MCP)を活用した正確性の向上
- AI 出力の検証プロセスの確立
- 苦労した点
アプリの改善点
- Supabase の RLS の設定
- version2.0.0 をこの後実装予定なので、そこで行います
- Next.js の Middleware の実行ランタイムと認証ロジックの修正
-
freeism-app ver2では、2025/2/26 にリリースされた Next.js v15.2.0 で experimental ですが Node.js runtime が正式サポートされたので、Node runtime で認証処理するよう修正したいと考えています
-
主な言語
- TypeScript
- なぜ使用したか
- Next.js/React のエコシステムで設定・ビルド・スクリプト用途に必須だからです
- Node.js ランタイムでの CLI・運用スクリプト(
scripts/)に適合し、周辺ツールが充実しているからです - 型安全と保守性を向上させたいためです
- 主に学んだサイト
- なぜ使用したか
開発環境
- GitHub
- なぜ使用したか
- リポジトリのバージョン管理をするためです
- GitHub Actions による CI/CD 連携のためです
- 教材が豊富だからです
- なぜ使用したか
- Cursor
- なぜ使用したか
- tab 補完が便利だからです
- 多くのエディタの中でも AI の使い方が上手で便利機能が多いと感じるためです
- なぜ使用したか
- Mac OS
- Windows
主に使用したライブラリ
-
React
- なぜ使用したか
- 業界標準を実践的に学びたかったためです
- 公式ドキュメントが分かりやすく、フロントエンドの UI を作るライブラリとして最良だと感じたためです
- なぜ使用したか
-
Next.js
- なぜ使用したか
- ルーティング、サーバーアクション、メタデータ管理、ISR/SWR 相当のキャッシュ制御をフルスタックで統合できるためです
- App Router・Server Actions・API を同一プロジェクトで完結できるためです
- 解説記事が豊富だからです
- なぜ使用したか
-
TanStack Query v5
- なぜ使用したか
- クライアントサイドのキャッシュを行いたいためです
- TanStack 系は、Table や Form など豊富で今後の勉強のために Query で慣れておきたいからです
- その他
-
src/library-setting/tanstack-query.tsで QueryClient を拡張し、idb-keyvalと連携して IndexedDB 永続化やトースト連携を共通化しました
-
- なぜ使用したか
-
Auth.js(NextAuth v5)
- なぜ使用したか
- Google OAuth を利用したシングルサインオンと JWT セッション管理を短時間で構築するためです
- Next.js 系の認証ライブラリを選択したいからです
- 技術記事や公式ドキュメントが豊富で分かりやすかったためです
- なぜ使用したか
-
Tailwind CSS
- なぜ使用したか
- AI にコーディングしてもらうのに適していると感じたためです
- 1 つのファイルで AI にコンテキストを渡せるため便利です
- 色々な CSS 系ライブラリを試してみた結果、一番理解しやすかったためです
- AI にコーディングしてもらうのに適していると感じたためです
- なぜ使用したか
-
Prisma
- なぜ使用したか
- Supabase/PostgreSQL 上で複雑なリレーション(Auction・Task・Notification など)を型安全に扱うためです
- 系ライブラリの中で、一番ドキュメントの英語が分かりやすく、量も豊富だったためです
- その他
- Drizzle も気になっています
- なぜ使用したか
-
Zod
- なぜ使用したか
- サーバー/クライアント双方で環境変数やフォーム入力をスキーマバリデーションするためです
- その他
-
src/library-setting/env.tsで@t3-oss/env-nextjsと組み合わせ、ビルド時に必須環境変数の欠落を検出しています
-
- なぜ使用したか
-
Upstash Redis(@upstash/redis)
- なぜ使用したか
- オークションの入札イベントを Pub/Sub で即時配信し、
Server-Sent-Eventsへ連携するリアルタイム基盤を実現するためです - 公式ドキュメントや Redis の GUI がとても分かりやすかったためです
- オークションの入札イベントを Pub/Sub で即時配信し、
- なぜ使用したか
-
AWS SDK for JavaScript v3(Cloudflare R2)
- なぜ使用したか
- Cloudflare R2 の S3 互換 API を通じて画像アップロードと署名付き URL の生成を行うためです
- その他
-
src/actions/cloudflare/r2-client.tsでS3Clientを生成し、環境変数の有無によってアップロード機能の有効/無効を切り替えています
-
- なぜ使用したか
-
React Hook Form + Zod Resolver
- なぜ使用したか
- Dashbord でのグループ/タスク作成フォームのバリデーションとステップ管理を少ない再レンダリングで実装するためです
- なぜ使用したか
-
Framer Motion
- なぜ使用したか
- 画像アップロードや入札 UI の表示状態を滑らかに切り替え、ユーザー操作に応じたアニメーション表現を加えるためです
- なぜ使用したか
-
Commitizen/commitlint/Husky/lint-staged
- 説明
- Conventional Commits を採用し、Commitizen/commitlint でコミット品質を統一しています
- Husky + lint-staged でコミット前に自動整形と Lint 実行を行っています
- なぜ使用したか
- コミット前に整理されたコードとコミットメッセージで分かりやすく履歴を残すためです
- 説明
-
ESLint / Prettier
- なぜ使用したか
- ルール群で型安全・可読性を担保し、コードの一貫性を確保したいためです
- その他
- Husky + lint-staged でコミット前に自動整形と Lint 実行を行っています
- なぜ使用したか
-
Vitest
- なぜ使用したか
- ECMAScript Modules(ESM)をネイティブでサポートしているためです
- 実行速度が Jest より早いためです
- なぜ使用したか
-
PostgreSQL(Supabase)
- なぜ使用したか
- 簡単に DB を使用できるためです
- また、ドキュメントや技術記事も豊富だからです
- なぜ使用したか
-
pnpm
- なぜ使用したか
- npm・yarn と比較してディスク効率とインストール速度に優れているとドキュメントや技術記事で見たためです
- なぜ使用したか
-
Vercel
- なぜ使用したか
- Next.js との統合が最適化されており、エッジでの SSR・ISR といったモダンなレンダリング戦略を実践的に学べる環境として最適だと感じたためです
- プルリクエストごとに自動生成されるプレビュー環境も便利だからです
- デプロイの自動化によってインフラ設定に時間を取られず、アプリケーションロジックの実装に集中できるためです
- なぜ使用したか
-
GitHub Actions(CI/CD)
- なぜ使用したか
- コードの品質を自動的に担保する仕組みが欲しかったためです
- CI/CD による自動テスト・Lint・型チェックの実行をしたいからです
- GitHub との統合によってワークフローが一元管理できてとても便利だからです
- なぜ使用したか
-
その他使用ツール
- draw.io
- Resend
- Notion
コーディングのルール
-
フォーマッタ
- 概要
- Prettier を採用
-
pnpm format:fixで全体整形 - プロジェクトの設定は、公開されている色々なベストプラクティスから自分好みに採用した
- 設定内容
- import 整理
-
@ianvs/prettier-plugin-sort-importsで自動並び替え、型は type-only import を強制
-
- import 整理
- 概要
-
コーディング規約
- 概要
- ESLint(Next.js core-web-vitals + TypeScript ルール)を使用し、CI 前に
pnpm lint/pnpm lint:fix - プロジェクトの設定は、公開されている色々なベストプラクティスから自分好みに採用した
- ESLint(Next.js core-web-vitals + TypeScript ルール)を使用し、CI 前に
- 設定内容
- 命名規則
- コンポーネントは PascalCase、フックは camelCase、ファイル名は kebab-case を厳守
- エクスポート方針
- 基本は named export。
app/page.tsx等の Next.js 予約ファイルのみ default export を許可
- 基本は named export。
- 未使用
-
noUnusedLocals/noUnusedParametersを有効化し未使用を検出
-
- 型安全性
- TypeScript
strict有効
- TypeScript
- 型スタイル
-
consistent-type-definitions: "type"を適用し、interface より type alias を推奨
-
- 非同期安全
-
no-misused-promisesを適用し、属性ハンドラでの void 戻り値チェックを適切化
-
- 関数スタイル
- 関数宣言を基本としつつ、コールバックはアロー関数を推奨
- アクセシビリティ
-
eslint-plugin-jsx-a11yの推奨設定を適用
-
- React ルール
- React/React Hooks の推奨セットを適用(依存配列やフック規約の逸脱を検出)
- TanStack Query
-
@tanstack/eslint-plugin-queryの推奨設定でクエリのアンチパターンを検出
-
- 命名規則
- 概要
-
テスト
- 概要
- Vitest + Testing Library。
*.test.ts(x)を対象に - Coverage 目標 Lines 90%/Funcs 85%/Branches 80%
- Vitest + Testing Library。
- 概要
-
コミット前
- 概要
- コミット前に、Formatter,Linter、型チェックを行う
- 概要
サイトマップ(画面遷移図)
-
/ホーム- サインイン:
/auth/signin - 利用規約:
/terms - プライバシー:
/privacy - オフライン:
/offline
- サインイン:
-
/auth/signinサインイン- 認証成功 →
/dashboard/* - 認証不要ページへ戻る →
///terms//privacy
- 認証成功 →
-
/dashboardダッシュボード(認証必須・未認証は/auth/signinへ)- グループ一覧:
/dashboard/group-list- グループ詳細:
/dashboard/group/[id]
- グループ詳細:
- 自分のグループ一覧:
/dashboard/my-group - 自分のタスク一覧:
/dashboard/my-task - グループ作成:
/dashboard/create-group - タスク作成:
/dashboard/create-task - レビュー検索:
/dashboard/review-search - 通知作成:
/dashboard/create-notification - 設定:
/dashboard/settings - 出品/入札:
/dashboard/auction- 入札履歴:
/dashboard/auction/history - オークション詳細:
/dashboard/auction/[auctionId] - 落札詳細:
/dashboard/auction/won-detail/[auctionId] - 出品詳細:
/dashboard/auction/created-detail/[auctionId]
- 入札履歴:
- グループ一覧:
- 補足
/dashboard/*- 配下は
loading/error/not-foundを内部でハンドリング(直接遷移不可)
テーブル定義書
| table | desc |
|---|---|
| User | ユーザー基本情報、権限、各種リレーション |
| Account | OAuth アカウント連携(NextAuth 準拠) |
| セッション | セッション管理(JWT/セッショントークン) |
| VerificationToken | 認証用トークン(メールリンク等) |
| UserSettings | 通知可否/ユーザー名/ライフゴール等の設定 |
| Group | グループ本体(名称/目標/参加上限/作成者) |
| GroupMembership | グループ所属関係(所有者フラグ含む) |
| GroupPoint | グループ内ポイント残高/固定ポイント合計 |
| Task | タスク本体(状態/カテゴリ/固定評価など) |
| TaskExecutor | タスク実行者(User 紐付け任意) |
| TaskReporter | タスク報告者(User 紐付け任意) |
| TaskWatchList | タスク/オークションのウォッチリスト |
| Analytics | 貢献度評価(評価者/ポイント/ロジック) |
| Auction | オークション本体(期間/最高入札/延長設定) |
| BidHistory | 入札履歴(自動入札フラグ/デポジット等) |
| AutoBid | 自動入札設定(上限額/刻み/有効フラグ) |
| AuctionMessage | オークション Q&A/メッセージ |
| AuctionReview | 相互レビュー(評価/コメント/役割) |
| Notification | 通知(対象/送信方法/既読 JSONB/予約日時) |
| PushSubscription | Web Push 購読情報(endpoint/p256dh/auth) |
主な機能
-
入札機能
- 概要
- 入札
- 自動入札
- 入札履歴
- 質問(Q&A)チャット機能
- ウォッチリスト/
- レビュー
- 入札通知
- 終了間際の入札時の自動延長
- 延長回数上限機能
- デポジット返還スクリプト
- 画面のスクリーンショット
- 詳細
-
Server-Sent-Events×Upstash Redis Pub/Subの組合せ- 大変だった実装
- サーバー側の再接続制御
- クライアント側の
Server-Sent-Events接続が切れた際の画面チラつきが発生していました - そのチラつきを無くすために、サーバー側の
whileで持続できるよう工夫しました
- クライアント側の
-
Upstash,VercelのServer-Sent-Eventsの長時間接続の制約への対処- そもそも購読ユーザーに対して一斉に通知するには、特別な実装が必要だと理解しておらず、AI に的外れな相談ばかりしていました
- その後、Vercel のサーバーの性質(Vercel のサーバーレスはステートレスで、複数のインスタンスで実行してインスタンス間では情報共有されないため、
Server-Sent-Eventsの配信先の情報が取得できない)を理解して、Upstash Redis Pub/Subを使用すれば、外部のインメモリのストレージを使用してServer-Sent-Eventsの配信先の情報を保存できることを理解しました - その後、サーバーからクライアントへは
Server-Sent-Eventsで接続して、サーバーには Upstash Redis Pub/Sub でリアルタイム通信を行う実装に変更しました - その後、
Upstash Redis Pub/Subの接続が 1 分弱の短時間で切れる事象が発生しました- Upstash の無料枠の接続数上限に引っかかりやすくなるため、接続時間を増やすため、
fetchオプションでcache: "no-store"を削除して接続時間を伸ばしました
- Upstash の無料枠の接続数上限に引っかかりやすくなるため、接続時間を増やすため、
- 次に、接続が 5 分で切れる事象が発生しました
- Vercel の実装やドキュメントを読み込んで原因調査した結果、Vercel Edge Runtime で fetch したときの内部で使用される
undiciライブラリの接続時間のデフォルトが 300 秒で、それを回避する方法はないことが判明しました
- Vercel の実装やドキュメントを読み込んで原因調査した結果、Vercel Edge Runtime で fetch したときの内部で使用される
- 改善するためには、
Cloudflare Workersにデプロイすれば改善しそうですが、工数的にいったんは Vercel デプロイのままにしました
- クライアント側の
EventSourceベースのデータ取得処理・再接続・エラー時の UX を実装(use-auction-bid-sse.ts)-
Server-Sent-Eventsで受け取るデータ構造が少し複雑だったので、データを抽出する処理が手間取りました
-
- サーバー側の再接続制御
- 実装理由
- 競り上がりの体験をリアルタイムかつ低レイテンシで実現し、ページ再読み込みに依存しない快適な UX を提供したいと考えたためです
- 使用したライブラリ、サービス
- Upstash Redis Pub/Sub
- 同じオークション商品ページを開いているクライアント全員に一括通知するために使用しています
- 他者が入札したら、同じページを開いている全員の画面が更新されます
- Upstash Redis Pub/Sub
- 大変だった実装
- Prisma のトランザクションと楽観的ロック(
Auction.version)で同時入札を整合的に処理 - 最高入札者の更新時に OUTBID 通知(Web Push/Email/In-App)を即時送出
-
- 概要
-
push 通知
- 概要
- VAPID 署名付き Web Push。Service Worker で受信し、OS ネイティブ通知を表示
- 購読は設定画面から ON/OFF を切替
- 画面のスクリーンショット
- 実装した理由
- 入札競合・締切・結果など重要イベントをアプリ外でも即時に伝達し、再訪率を高めるためです
- 大変だった実装
-
pushsubscriptionchangeの購読更新(SW→ クライアント or 直接 API 経由でsubscription-updateに保存)を実装しました - デバイスで重複の購読をしないよう実装しました
- 同じユーザーでも端末ごとの購読を管理できるようにしました(同じユーザーでは設定を共有することも可能)
- 無効購読(404/410)の自動削除を実装しました
- Service Worker の特殊なライフサイクルを理解して実装するのが大変でした
-
- 工夫・意識したこと
- 設定画面から push 通知を ON にできる UI にしました
- その際に chrome の権限許諾が出て OFF にしたらアプリ側の設定も OFF にするようにしました
- 概要
-
出品一覧
- 概要
- カテゴリ/状態/価格帯/残り時間/グループ/フリーテキストを クエリパラメータ(
nuqs)と同期しフィルター・ソート・ページネーション - サジェスト取得・件数取得をサーバー側でキャッシュし、描画と操作の体感速度を最適化
- INDEX を作成して、検索速度の向上
- カテゴリ/状態/価格帯/残り時間/グループ/フリーテキストを クエリパラメータ(
- 実装した理由
- 多条件での探索性・再現性(URL 共有)・パフォーマンスを両立するためです
- 大変だった実装
- いいね(お気に入り)を実装しました
- ページネーションを実装しました
- 並び替え+ページネーション保持を実装しました
- 絞り込み検索+ページネーション保持を実装しました
- N+1 問題解消、索引最適化、キャッシュレイヤとの整合を行いました
- 工夫・意識したこと
- 条件オブジェクト(
AuctionListingsConditions)を型で表現し、URL⇄ 状態の相互変換を一元化しました - キャッシュ/API レイヤと UI の責務分離(一覧/件数/サジェストを個別最適化)しました
- 「Bigram の 2 文字分割」と「TokenMekab による形態素解析の分割」の 2 つのトークナイザーでインデックスして、誤字脱字と多少の意味的な単位で検索やサジェストができるようにしました
- ベクトル検索は費用面を考えて実装しませんでした
- 条件オブジェクト(
- 概要
-
CSV エクスポート
- 概要
- グループのタスク情報や貢献度分析を CSV で出力します
- 実装した理由
- 活動実績の共有・二次分析・証跡管理のためです
- 大変だった実装
- 期間・状態での抽出、join 後の整形、名前解決、件数上限や日付境界の扱いが大変でした
- 工夫・意識したこと
-
use cacheで重い集計をキャッシュしました。列構成を TypeScript 型で固定し、整形の破壊的変更を防止しました
-
- 概要
-
ログイン機能/ログアウト
- 概要
- Auth.js(NextAuth v5 の Google OAuth)で認証
- ミドルウェアで
/dashboard/*を保護 - JWT セッションを 30 日ローテーション
- 大変だった実装
- Middleware で認証チェックする実装
- Auth.js で
/dashboard/*の場合は Middleware で認証チェックしたいのですが、Auth.js のデフォルト処理(strategy: "db")で Prisma を使用すると、Prisma が Middleware はエッジ環境で実行できない、かつ Middleware は Node runtime の http に対応していない? ため、Supabase(PostgreSQL)の URL にアクセスができませんでした- なので、Auth.js のプラグインを使用せず、
strategy: "jwt"にして、DB に保存する処理は、別でログインロジックを独自実装しました
- なので、Auth.js のプラグインを使用せず、
-
freeism-app ver2では、2025/2/26 にリリースされた Next.js v15.2.0 で experimental ですが、Node.js runtime が正式サポートされたので、Node runtime で認証処理するよう修正したいと考えています
- Auth.js で
- Middleware で認証チェックする実装
- その他
- 現在は
Auth.jsで実装していますが、パスキー認証を実装したいこともあり、Auth.js が取り込まれたことから、Better Auth に移行することを検討中です
- 現在は
- 概要
-
権限チェック
- 概要
-
checkIsPermissionで App オーナー/Group オーナー/タスク作成者・報告者・実行者を網羅し、操作可否を統制します
-
- 実装した理由
- グループ単位の安全な運用と、役割に応じた柔軟な操作権限を両立するためです
- 工夫・意識したこと
- 1 つの関数であらゆる種類の権限チェックに対応できるようにまとめました
- 概要
-
フォーム
- 概要
- フォームは React Hook Form + Zod でバリデーションを行っています
- 工夫・意識したこと
- 権限に応じて UI を出し分けています
- 概要
-
共通テーブル
- 概要
- 外部ライブラリを使わず、共通テーブル
ShareTableでフィルター/ソート/ページネーション/全画面表示を提供しています - ヘッダ固定・モーダル/ポップオーバのポータル先制御・オーバーレイ型ローディングなど、複数画面で共通の UX を統一しています
- 外部ライブラリを使わず、共通テーブル
- ライブラリ不使用で実装した理由
- ライブラリを使用せず、独自実装することで力試ししてみたかったためです
- 大変だった実装
- 全画面表示時に
<dialog>や<popover>が隠れる問題を解消するため、container/refを渡してポータルの描画先をテーブル内部へ固定しました -
nuqsとページネーション/ソート条件の双方向同期(クエリパラメータ維持)を実装しました - TanStack Query キャッシュとの整合を実装しました
- 全画面表示時に
- 工夫・意識したこと
- 再利用可能なテーブル
- 再利用可能なテーブルを作成して、画面ごとに要件が異なるテーブル(参加/脱退モーダル、編集/削除、ステータス変更)で活用してメンテナンス性を高めました
- 共通フック
- 画面ごとに
use-*-table.tsを用意し、URL 同期・権限・削除/編集・API 呼び出しを集約しました
- 画面ごとに
- フルスクリーン
- フルスクリーン切替時は
document.fullscreenElement監視とbodyクラス制御で UX を維持しました
- フルスクリーン切替時は
- ローディング
- ローディングはオーバーレイで
ShareTableを置換しない(フルスクリーン/selection 状態を保つ)ようにしました
- ローディングはオーバーレイで
- 再利用可能なテーブル
- 各画面のテーブル
- My Task 一覧(
/dashboard/my-task)- 概要
- 自分のタスクを一覧表示します。ステータス変更(コンボボックス)、編集モーダル、削除確認、貢献度/算出者列、オークション導線を提供しています
- 実装した理由
- 自身の担当・進捗管理と、オークション連携のハブとするためです
- 大変だった実装
- ステータス変更用ポップオーバのポータル先をテーブル内に限定し、全画面表示でもただしく重なるように制御しました
- 編集モーダルの開閉とタスク更新後のキャッシュ整合/再描画の最小化を実装しました
- 工夫・意識したこと
- URL 同期(
page/sort_field/sort_direction/q/task_status/contributionType)を実装しました - 権限チェック経由の「編集可/削除可」ボタン状態制御を実装しました
- URL 同期(
- 概要
- Group 一覧(
/dashboard/group-list)- 概要
- 参加可否表示と参加モーダル(未参加時のみ有効化)、参加人数/上限/評価方法/目標/デポジット期間の表示、検索/ソート/ページネーションを提供しています
- 実装した理由
- 参加可能なグループを探索・参加操作を一元化するためです
- 大変だった実装
- 「参加中/未参加/すべて」切替の URL 同期と、参加ボタンの活性/非活性制御の安全化を実装しました
- 工夫・意識したこと
- 参加アクションはアラートダイアログで再確認しました。大きな画面でもボタン位置・可視性を最適化しました
- 概要
- My Group 一覧(
/dashboard/my-group)- 概要
- 所属グループの一覧と脱退モーダル、デポジット期間・評価方法・保有/残高ポイントの表示、検索/ソート/ページネーションを提供しています
- 実装した理由
- 所属グループの状態確認と離脱操作を安全に提供するためです
- 大変だった実装
- 脱退アクションの確認ダイアログ(誤操作防止)と脱退後のキャッシュ無効化/再フェッチを実装しました
- 工夫・意識したこと
- アイテム数/ソート変更時は
page: 1にリセットし、UI 齟齬を防止しました
- アイテム数/ソート変更時は
- 概要
- Group 詳細(
/dashboard/group/[id])- 概要
- グループ内のタスク一覧を表示します。ステータス変更(コンボボックス)、編集モーダル、削除確認、貢献度/入札額列、オークション導線、検索/絞り込み/ソート/ページネーションを提供しています
- 実装した理由
- グループ管理者/メンバーがタスクの状態を俯瞰・更新できるようにするためです
- 大変だった実装
- Owner/非 Owner で編集/削除可否が変わるため、権限チェックと UI 制御を分離しました
- 工夫・意識したこと
- 画面内の複数操作(変更/削除/編集)を共通 Table
API(editTask/deleteTask/statusCombobox)で表現し責務を明確化しました
- 画面内の複数操作(変更/削除/編集)を共通 Table
- 概要
- My Task 一覧(
- 概要
-
ドメイン購入
- 概要
- Cloudflare で購入しました
- 大変だった点
- 英語圏の方式で住所などを入力が必要になるので手間取りました
- 工夫・意識したこと
- 安さと安心を両立できるドメインとして
.appを選びました
- 安さと安心を両立できるドメインとして
- 概要
-
アプリ内通知
- 概要
- 通知モデルに JSONB
isReadを持ち、既読/未読を端末横断で管理しています。タブ UI で分類表示し、ショートカット操作に対応しています
- 通知モデルに JSONB
- 実装した理由
- アプリ内での迅速なフィードバックと履歴参照を可能にするためです
- 大変だった実装
- JSONB の索引運用、未読集計の最適化、モーダル閉鎖時以外のサーバーアクセス抑制(クライアント state 管理)を実装しました
- 工夫・意識したこと
- UI をオシャレにしました
- ショートカットでタブ切り替えや通知を開けるようにしました
- サーバー負荷の回避と重くならないように Modal を閉じるとき以外はサーバーアクセスせず、state でステータスの管理を行う実装にしました
- 既読未読の管理は JSONB 型を使用して、レコードを増やさず管理しやすくしました
- JSONB 型を使用する場合は PrismaORM でも生 SQL で記載が必要だと勘違いして生 SQL を実行しています
- 概要
-
通知作成フォーム
- 概要
- ダッシュボードから通知を作成・送信するフォームを提供しています
- 対象や本文、スケジュールを指定して予約送信が可能です
- 実装した理由
- 管理者が運用時に一斉/対象限定の通知を手動で送るユースケースを想定したためです
- 大変だった実装
- API 経由(
/api/notifications)とサーバーアクションの役割整理、認証ガードの適用とエラーハンドリングの統一を実装しました
- API 経由(
- 工夫・意識したこと
- 失敗時のトースト通知・入力保持、成功時のクリア/遷移を整理しました
- 概要
-
メール通知
- 概要
- Resend を使用し、React Email テンプレートで本文を生成します
- ユーザー設定に基づき送信可否を制御しています
- 実装した理由
- プッシュ不可環境や重要通知の冗長化のためです
- 大変だった実装
- 本番鍵の管理、到達性/エラー時の再送制御を実装しました。環境整備前は実送信コードを無効化し安全運用しました
- 工夫・意識したこと
- 送信対象ユーザーの抽出・設定尊重・スケジュール送信を
sendGeneralNotificationで一元化しました - API Route(
src/app/api/notifications)からの作成/送信にも対応しました
- 送信対象ユーザーの抽出・設定尊重・スケジュール送信を
- 概要
-
CI/CD
- 概要
- GitHub Actions を想定し、運用スクリプト(入札開始/終了・デポジット返還・予約通知送信)を定期実行します
- 実装した理由
- バッチ運用の自動化と運用コストの最小化のためです
- 大変だった実装
- スケジュールジョブの冪等性・失敗時の再実行・通知整合を実装しました
- 工夫・意識したこと
- スクリプトにテストを付与し回帰を防止しました
- 概要
-
PWA
- 概要
-
next-pwaによるオフライン対応と SW 連携(service-worker.js)を実装しています - アイコン/マニフェストを整備し、Push と共存しています
-
- 実装した理由
- モバイル中心の利用で回線断時も最低限の利用を可能にし、再訪体験を高めるためです
- 大変だった実装
- キャッシュ戦略の調整、SW 更新のリスク管理、Push/API との整合を実装しました
- 概要
-
オフラインページ
- 概要
- オフライン時の専用ページを用意し、接続断でもユーザーに明確な状態と再試行を案内します
- 実装した理由
- PWA/キャッシュ運用において、ネットワーク不通時の体験を劣化させないためです
- 大変だった実装
- メタデータ/テストの整備、Next のキャッシュ制御との整合を実装しました
- 概要
-
設定・プロフィール
- 概要
- ユーザー設定の閲覧/更新を提供します。Push/Email 通知の ON/OFF、ユーザー名/ライフゴールの更新、現在状態の表示が可能です
- 実装した理由
- 通知チャネルの自己決定と、プロフィールの基本情報をユーザーが自律的に管理できるようにするためです
- 大変だった実装
- セッション取得と CSR の初期レンダ制御、トグル ON 時の Service Worker 登録/VAPID 鍵処理/購読作成の順序管理を実装しました
- 工夫・意識したこと
- React Hook Form + Zod で型安全なフォーム、成功/失敗のトースト、Query キャッシュの無効化と再フェッチ制御を実装しました
- 概要
-
レビュー検索
- 概要
- タブ(検索/自分が行った/自身への)とサジェストを備えたレビュー検索を実装しています
- URL 同期(
nuqs)と TanStack Query によるキャッシュで高速化しています
- 実装した理由
- 貢献の可視化と検索性向上のためです
- 工夫・意識したこと
- クエリ/サジェストを別 QueryKey で管理し、
staleTime/gcTimeを最適化しました
- クエリ/サジェストを別 QueryKey で管理し、
- 概要
-
画像アップロード(Cloudflare R2)
- 概要
- サーバーで署名付き URL を発行し(有効 15 分)、クライアントから直接 R2 に PUT する安全なアップロードフローを実装しています
- 実装した理由
- 大容量ファイルでもアプリサーバーを経由せず安全にアップロードするためです
- 工夫・意識したこと
- 機能トグル(
ENABLE_IMAGE_UPLOAD)で環境依存を排除しました - 対応 MIME のみ許可する制限機能を実装しました
- UI は
image-upload-areaでドラッグ&ドロップ対応しました
- 機能トグル(
- 概要
-
CSV アップロード
- 概要
- CSV をモーダルから取り込み、グループ/タスク関連のデータを一括登録・更新(権限/必須列検証・進捗表示・エラー集計)を行います
- 実装した理由
- 運用時の一括メンテナンス効率化のためです
- 工夫・意識したこと
- 型安全な必須列検証と段階的バリデーション、アップロードタイプごとの権限確認を実装しました
-
Papaparse,react-dropzoneライブラリを使用して、ドラッグ&ドロップの CSV アップロード対応しました - 入力必須のカラムのデータが足りない場合は、『「カラム名」の「OO 行目」のデータが不足しています。』とモーダルで表示するようにしました
- 概要
-
ダッシュボード/ナビゲーション
- 概要
- Group 一覧/作成
- Task 作成/一覧
- 通知作成
- レビュー検索
- オークション一覧/履歴
- GitHub API 変換(プレースホルダー)
- My Info(参加 Group 一覧/Task 一覧)
- Settings 等をサイドバーから遷移
- 実装した理由
- 業務フローを一元化し、権限ベースで安全に操作するためです
- 工夫・意識したこと
- レイアウト固定(Header/Sidebar)とスクロール領域分離、保護ルートはミドルウェアで強制サインインするようにしました
- 概要
-
入札・落札履歴
- 概要
- 自身の入札/落札の履歴を可視化し、過去の入札動向/結果を参照できます
- 実装した理由
- 取引後の追跡とナレッジ化のためです
- 概要
機能以外の実装・工夫
-
UI
- レスポンシブ対応
- メニューバーが sm 未満の場合はハンバーガーメニューを表示して、基本は閉じるように実装しました
- メディアクエリを使い、レスポンシブ対応しました
- テーマ/ダークモード
-
src/components/provider/providers.tsxでnext-themesのThemeProviderを使用しました。attribute: "class"、defaultTheme: "system"により OS 設定と同期しています
-
- アクセシビリティ/UI 基盤
- コード規約として ESLint の
eslint-plugin-jsx-a11yを有効化し、Radix UI/shadcn/uiを土台に Tailwind で統一した UI を構築しました
- コード規約として ESLint の
- ローディング/スケルトン
- ローディング状態の視覚化(
src/app/loading.tsxなど)とスケルトン UI を適所に配置し、知覚性能を向上しました
- ローディング状態の視覚化(
- レスポンシブ対応
-
コーディング規約の設定
- 概要
-
eslint.config.mjsと Prettier による一貫整形。@ianvs/prettier-plugin-sort-imports+prettier-plugin-tailwindcssで import とユーティリティの順序最適化 - ルール例
-
import/no-default-exportを原則禁止(page.tsx/layout.tsx等 Next 予約ファイルは許可) -
unicorn/filename-case: kebabCaseでファイル名を強制 -
@typescript-eslint/consistent-type-imports、no-misused-promises(attributes を安全化)など型/非同期の厳格運用 -
@tanstack/eslint-plugin-queryを採用し、React Query のアンチパターンを静的検出
-
-
- 実装した理由
- 可読性とレビュー効率の最大化、バグ温床になりやすい import/非同期/フック周りの逸脱を早期検知するためです
- 大変だった実装
- Next.js 予約ファイルの default export 例外と、ファイル名の kebab-case 強制の両立をルールで表現しました
- 工夫・意識したこと
-
pnpm format:fixで Prettier + Prisma フォーマットを一括実行、pnpm lint:fixで ESLint 修正と Prisma 検証を同時実行するようにしました - ルール/プラグインは最小限にせず、実害の出やすい領域(型/非同期/アクセシビリティ/Query)を厚めにカバーしました
-
- 概要
-
テスト
- 概要
-
vitest+@testing-library/react - カバレッジ閾値: Lines 90% / Functions 85% / Branches 80%(
vitest.config.ts) - include/exclude を厳密化し、UI の土台や設定ファイル、型定義は対象外
-
- 実装した理由
- 新機能の実装やリファクタリングのたびにバグが発生して、テストに時間が取られるためです
- 大変だった実装
- NextAuth/Prisma/TanStack Query の初期化順とモックの整合、
Server-Sent-Events/Push など環境依存部分のテスト分離を実装しました
- NextAuth/Prisma/TanStack Query の初期化順とモックの整合、
- 工夫・意識したこと
- UI は Testing Library、API/アクションは MSW/直呼びで段階別にテストするようにしました
- 概要
-
TanStack Query v5でクライアント側のキャッシュ- 概要
-
idb-keyvalにより IndexedDB 永続化 -
PersistQueryClientProviderでQueryClientを永続化し、Devtools も導入 - デフォルト値:
staleTime/gcTime: Infinity、refetchOn*は有効、throwOnError: true - Query/Mutation でトースト通知を集中管理
- キャッシュキーをファクトリー関数にまとめる
-
- 実装した理由
- オフライン/再訪時の体験向上と、ネットワーク往復を最小化するためです
- 大変だった実装
- 安定した QueryKey 設計(
queryCacheKeysファクトリ)と invalidate の一元化(meta.invalidateCacheKeys)を実装しました
- 安定した QueryKey 設計(
- 工夫・意識したこと
-
NuqsAdapterによる URL 同期とキャッシュの整合、トースト多重発火の抑制(QueryCache/MutationCache経由)を実装しました
-
- 概要
-
PWA / Push / Service Worker
- 概要
-
public/service-worker.jsでinstall/activate/push/notificationclick/pushsubscriptionchangeを実装 -
src/hooks/notification/use-push-notification.tsで SW 登録・VAPID 鍵処理・購読/再購読・端末識別の一連を管理
-
- 実装した理由
- オフライン時の最低限の体験と、入札/通知イベントの即時把握を両立するためです
- 工夫・意識したこと
-
clients.claim()で新 SW の即時制御、registration.readyを待ってから購読するようにしました -
pushsubscriptionchangeでクライアント通知 or API へフォールバックするようにしました
-
- 概要
-
セキュリティ/ミドルウェア
- 概要
-
/dashboard/*を認証必須化(未認証はauth/signinへリダイレクト) - 応答に
X-Frame-Options/X-Content-Type-Options/HSTS/Referrer-Policyを付与
-
- 概要
- 環境変数の型安全
- 概要
-
@t3-oss/env-nextjs+zodで server/client を分離定義し、emptyStringAsUndefined・skipValidationを設定 - VAPID/Supabase/Cloudflare R2 等を厳格検証
-
- 概要
-
コミットフロー/自動整形
- 概要
- Conventional Commits を
commitlintで強制、commitizen(pnpm commit)で対話入力、husky+lint-stagedで整形/静的検査をコミット前実行
- Conventional Commits を
- 概要
-
Supabase と GitHub CI/CD の設定
- GitHub CI/CD を通じて、Supabase にアクセスできる環境を設定しました
- 大変だった内容
- GitHub CI/CD から Supabase にアクセスできない問題の解決
- GitHub CI/CD の DIRECT_URL に登録する URL は、Supabase の Direct Connection ではなく、セッション Pooler の URL を登録する必要がありました
- Supabase の Direct Connection は IPv6 で動作しますが、GitHub は IPv4 しか対応していません
また、セッション Pooler はほぼ同じ機能ですが IPv4 で動作します
- GitHub CI/CD から Supabase にアクセスできない問題の解決
-
パフォーマンス向上
-
useMemo(),useCallback()でリレンダリングを抑えました -
"use cache",TanStack Query のuseQuery,useMutationを使用して、サーバー側とクライアント側でそれぞれキャッシュしました
-
-
Seeding
- テストデータを生成するために、
Faker.jsを使用しました - すべてのデータベースに、それぞれに適したテストデータを生成するコードを実装しました
- コマンド 1 つでデータの削除と再生成ができるように実装しました
- テストデータを生成するために、

