目次
前回のおさらい
これまでの実装で、以下の機能を作成しました:
今回は、流通業者が出荷予定を登録する機能を実装します。流通業者が事前に出荷予定を入力することで、後の工程で生産者が当日の出荷情報を確認できるようになります。
データベース設計(Supabase)
テーブル構造
planned_shipments
テーブルと命名し、以下の構造で設計しました:
sql-- 実際に使用している主要フィールド
id uuid primary key (自動生成)
created_at timestamptz default: now()
created_by uuid 誰が作ったか
destination text 出荷先
shipment_date date 出荷日
quantity int4 数量 (箱)
note_caution text 注意事項
-- その他のフィールド(将来的に使用予定)
image_url, tracking_code, destination_group, quantity_kg,
total_quantity, group_id, price_code, price, status, destination_id
設計の経緯
実は、最初にChatGPTに相談した際に「考えられる項目は全部作っておいた方が困らない」とアドバイスされ、16項目のテーブルを作成しました。
しかし、実際に実装してみると使用しているのは7項目のみ。残りの項目は現時点では未使用です。
これが正解だったかどうかは、プロジェクト完成後に振り返ってみたいと思います。
Supabase RLS設定
セキュリティ面では、SupabaseのRow Level Security(RLS)を使用して権限制御を実装しました。
実装したポリシー
-- 削除:自分が登録したもののみ
CREATE POLICY "DELETE: 自分が登録したもののみ削除可"
ON "public"."planned_shipments"
FOR DELETE
USING ((created_by = auth.uid()));
-- 登録:流通業者・管理者のみ
CREATE POLICY "INSERT: 流通業者・管理者のみ登録可"
ON "public"."planned_shipments"
FOR INSERT
WITH CHECK (
(role() = ANY (ARRAY['trader'::text, 'admin'::text])) AND
(created_by = auth.uid())
);
-- 閲覧:流通業者・管理者のみ
CREATE POLICY "SELECT: 流通業者・管理者のみ閲覧可"
ON "public"."planned_shipments"
FOR SELECT
USING (role() = ANY (ARRAY['trader'::text, 'admin'::text]));
-- 更新:自分が登録したもののみ
CREATE POLICY "UPDATE: 自分が登録したもののみ編集可"
ON "public"."planned_shipments"
FOR UPDATE
USING ((created_by = auth.uid()))
WITH CHECK (
(role() = ANY (ARRAY['trader'::text, 'admin'::text])) AND
(created_by = auth.uid())
);
RLSの利点
- フロントエンドで権限チェック不要:データベースレベルで制御
- セキュリティが確実:APIを直接叩かれても安全
- シンプルな実装:フロントエンドのコードがスッキリ
フォーム実装
レスポンシブ対応
PC版とモバイル版で異なるUIを提供しています:
typescript// カスタムフックで画面サイズを判定
export function useIsMobile(breakpoint = 768) {
const [isMobile, setIsMobile] = useState(false);
useEffect(() => {
if (typeof window === "undefined") return;
const mediaQuery = window.matchMedia(`(max-width: ${breakpoint}px)`);
setIsMobile(mediaQuery.matches);
const handleResize = (e: MediaQueryListEvent) => {
setIsMobile(e.matches);
};
mediaQuery.addEventListener("change", handleResize);
return () => mediaQuery.removeEventListener("change", handleResize);
}, [breakpoint]);
return isMobile;
}
typescript// 使用例
const isMobile = useIsMobile();
return (
<div>
{isMobile ? (
<MobileFlexibleShipmentTable {...props} />
) : (
<FlexibleShipmentTable {...props} />
)}
</div>
);
PC版:表形式で一括入力

- 複数行を表形式で表示
- 「行を追加」ボタンで動的に行を追加
- 一括登録処理
モバイル版:縦並び入力

- 各項目を縦に並べて表示
- スマホでの操作性を重視
- 1件ずつ登録する形式
登録処理の実装
メイン処理
typescriptconst handleSubmit = async () => {
// 入力値のバリデーション
const validRows = rows.filter((r) => r.date && r.destination);
// データベース用のデータ整形
const insertData = validRows.map((row) => ({
created_by: userId,
destination: row.destination,
shipment_date: row.date,
quantity: row.quantity,
note_caution: row.note_caution,
}));
// Supabaseへの一括登録
const { error } = await supabase
.from("planned_shipments")
.insert(insertData);
if (error) {
alert("登録失敗: " + error.message);
} else {
alert("出荷予定を登録しました!");
// フォームをリセット
setRows(
Array(5).fill({ date: "", destination: "", quantity: "", note_caution: "" })
);
}
};
出荷先マスタとの連携
typescript// 出荷先のリストを取得
useEffect(() => {
const fetchDestinations = async () => {
const { data, error } = await supabase
.from("destinations")
.select("name");
if (!error && data) {
setDestinations(data.map((d) => d.name));
}
};
fetchDestinations();
}, []);
③で登録したマスタデータを活用し、プルダウンで出荷先を選択できるようにしています。
実装のポイント
1. バリデーション
- 必須項目チェック:日付と出荷先が入力されている行のみ登録
- 空行の除外:入力されていない行は自動的に除外
2. UX配慮
- エラーハンドリング:登録失敗時にわかりやすいメッセージ
- 成功時の処理:登録完了後にフォームをリセット
- 行の追加:必要に応じて入力行を増やせる
3. 権限制御
- 認証チェック:ログインしていない場合はログイン画面へリダイレクト
- RLS連携:データベースレベルでの権限制御
動作確認
実際に流通業者アカウントでログインし、出荷予定を登録してSupabaseの管理画面で確認しました:
- ✅ 複数行の一括登録が正常に動作
- ✅ RLSによる権限制御が機能
- ✅ PC・モバイル両方でレスポンシブ対応
- ✅ 出荷先マスタとの連携が正常
今後の課題
- バリデーション強化:数量の範囲チェックなど
- 編集機能:登録済みデータの修正
- 検索・フィルタ:大量データの管理
次回予告
今回の実装では、コンポーネント化を積極的に行い、PC用とモバイル用で別々のコンポーネントを作成しました。
しかし、実装してみて「本当にコンポーネント化する必要があったのか?」「ベタ書きでも良かったのでは?」という疑問が浮かんできました。
次回は、React初心者がコンポーネント化してみた結果について、リアルな体験談を踏まえて詳しく検証してみたいと思います。