栽培管理アプリを作ってます⑮-3:デモ版の書き込み制御と通知UIをsonnerに統一

目次

はじめに

Next.js + Supabaseで農業向け栽培管理アプリを開発しています。
今回はデモ版の書き込み制御と、通知UIをsonnerに統一した話です。

デモ版の仕様

栽培管理アプリにはデモ版を用意しています。

デモ版の仕様はシンプルで、閲覧はできるけど書き込みはできない、というものです。

ただ最初の実装のままだと、書き込みボタンを押したときに

登録に失敗しました。もう一度お試しください。

という普通のエラーメッセージが出ていました。

ユーザーからすると「システムの不具合なのか」「デモの制限なのか」が分かりません。これはよくない。

そこで今回は2つの改善をしました。

  • 通知UIをsonnerに統一する
  • デモ版の書き込みをフロント側で止める

問題①:通知UIがバラバラだった

最初の実装では、各ページに

const [message, setMessage] = useState("")

のような状態管理を作って、登録成功やエラーをそれぞれ表示していました。

これだとページごとに表示位置もデザインもバラバラになるし、同じような実装を何度も書く羽目になります。

sonnerを導入する

そこでsonnerを導入しました。React向けのトースト通知ライブラリです。

sonnerをインストールする

ターミナルで実行。

npm install sonner

あとはlayout.tsx<Toaster /> を追加するだけで、全ページ共通の通知UIが使えるようになります。

// src/app/layout.tsx
import { Toaster } from "sonner";

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        {children}
        <Toaster position="top-center" richColors />
      </body>
    </html>
  );
}

通知ユーティリティを作る

さらに使いやすくするため、共通のユーティリティファイルを作りました。

// src/utils/notify.ts
import { toast } from "sonner";

export const notify = {
  success: (msg: string) => toast.success(msg),
  error: (msg: string) => toast.error(msg),
  info: (msg: string) => toast(msg),
  demoOnly: () =>
    toast.info(
      "デモ版では閲覧のみご利用いただけます。導入をご検討の方はお問い合わせください。"
    ),
};

これで各ページでは

notify.success("保存しました")
notify.error("エラーが発生しました")
notify.demoOnly()

と書くだけで済みます。表示位置もデザインも統一されました。


問題②:書き込みをDBレベルで止めていた

当初はSupabaseのRLS(Row Level Security)でINSERT・UPDATE・DELETEをブロックしていました。DBレベルで書き込みを禁止する方法です。

ただこの方法だと、ユーザーが保存ボタンを押したとき、RLSが拒否した結果として

登録に失敗しました

という本物のエラーに見えてしまいます。「デモ版の制限です」とは伝わらない。

フロント側でデモ判定する

そこでボタンを押した瞬間にデモかどうかを判定して、デモなら処理を止めてトーストを表示する方式に変えました。

クリック
↓
デモ判定
↓(デモの場合)
処理を止める → notify.demoOnly() を表示
↓(デモでない場合)
通常の保存処理へ

判定用のユーティリティはこれだけです。

// src/utils/demo.ts
import { notify } from "./notify";

export function blockIfDemo() {
  if (process.env.NEXT_PUBLIC_DEMO_MODE === "true") {
    notify.demoOnly();
    return true;
  }
  return false;
}

NEXT_PUBLIC_DEMO_MODE はVercelのデモ用プロジェクトにだけ設定している環境変数です。本番プロジェクトには設定しないので、本番では何も起きません。

使い方はシンプルで、保存処理の前に1行挟むだけです。

if (blockIfDemo()) return;

await createHarvestAction(data);

これでデモ版では保存ボタンを押すと

デモ版では閲覧のみご利用いただけます。導入をご検討の方はお問い合わせください。

というトーストが出て、処理は止まります。エラーではなく「制限です」と伝わる形になりました。


おまけ:デモ確認中に別のバグも見つかった

デモ版の動作確認をしているときに、別の問題も見つかりました。

収穫登録フォームに「収穫量(任意)」という項目があります。入力しなくても保存できるようにしていたつもりだったんですが、空のまま保存するとエラーになる。

本番環境では

Server Components error

というメッセージが出ていました。

調べてみるとSupabaseのテーブル定義が

harvest_amount numeric NOT NULL

になっていました。UIでは任意にしているのに、DBでは必須になっている。典型的な設計の不整合です。

Supabaseの SQL Editor で1行直して解決しました。

alter table cult_harvests
alter column harvest_amount drop not null;

Next.jsの本番で出る Server Components error は原因が分かりにくいことが多いですが、フォームの入力→Server Action→DB制約の流れのどこかで詰まっているケースが多いです。DB定義も確認する癖をつけておくと解決が早いです。


まとめ

今回やったことをまとめると:

  • 通知UIをsonnerに統一 → 表示位置・デザインが揃った
  • デモの書き込み制御をフロントに移した → 「エラー」ではなく「制限」として伝わるようになった
  • 収穫量のNOT NULL制約を外した → 任意入力が本当に任意になった

デモ版は「触ってもらうためのもの」なので、ユーザーが戸惑わない動きになっているかは地味に大切です。今回の改善でだいぶ整った気がします。

目次