この教材では「自分だけのメモ」を扱います。あなたのアプリでは、ユーザーごとに分かれていなければならないデータを 1 つ選んでください(予約・日報・在庫など)。
問い合わせフォーム・LINE 連携付きの企業サイトを作る
5 ページの企業サイトに問い合わせフォームを付け、送信をメールと LINE 公式アカウントへ通知する。トークンはコードに書かない。
約180分 Claude CodeCursor 検証 2026.10.21
メール + パスワードのログインと、自分のデータだけを扱うダッシュボードを Next.js と Supabase(Auth + Postgres + RLS)で作る。
サインアップ・ログイン・ログアウト、ログイン後のダッシュボードでメモの追加・編集・削除ができる Web アプリ。他のユーザーのメモは API を直接叩いても見えない。Vercel で公開済み。
動作確認環境: Node.js 20 / Next.js 15(App Router、Server Actions) / @supabase/ssr / Supabase(Postgres + Auth、2026年10月時点の無料枠) / Playwright(E2E)
この教材では「自分だけのメモ」を扱います。あなたのアプリでは、ユーザーごとに分かれていなければならないデータを 1 つ選んでください(予約・日報・在庫など)。
確認: テーブルにする対象が 1 つ決まった
AI に SQL を出してもらい、Supabase の SQL エディタで実行します。RLS を有効にする前にデータを入れないでください。
Supabase(Postgres)で「ユーザーごとのメモ」を扱うテーブルを設計してください。 要件: - テーブル notes: id (uuid, 既定値 gen_random_uuid()), user_id (auth.users への参照), title (text, 1〜120 文字), body (text, 最大 5000 文字), created_at, updated_at - RLS を有効にし、select / insert / update / delete のすべてで auth.uid() = user_id だけを許可するポリシー - updated_at を自動更新するトリガー 出力: Supabase の SQL エディタにそのまま貼れる SQL と、各ポリシーが何を防ぐかの 1 行説明。service role key を使う前提の設計にはしないこと。
create table if not exists public.notes (
id uuid primary key default gen_random_uuid(),
user_id uuid not null references auth.users (id) on delete cascade,
title text not null check (char_length(title) between 1 and 120),
body text not null default '' check (char_length(body) <= 5000),
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
alter table public.notes enable row level security;
create policy "own notes: select" on public.notes for select using (auth.uid() = user_id);
create policy "own notes: insert" on public.notes for insert with check (auth.uid() = user_id);
create policy "own notes: update" on public.notes for update using (auth.uid() = user_id) with check (auth.uid() = user_id);
create policy "own notes: delete" on public.notes for delete using (auth.uid() = user_id);
確認: Table Editor で notes が見え、RLS が ON になっている
npx create-next-app@latest でプロジェクトを作り、.env.local に URL と anon key を入れます。.env.example はプレースホルダーだけです。
# 公開してよいのはこの 2 つだけ。service role key はここにも書かない
NEXT_PUBLIC_SUPABASE_URL=<ここに貼る:Supabase プロジェクトの URL>
NEXT_PUBLIC_SUPABASE_ANON_KEY=<ここに貼る:anon key(RLS があるので公開可)>
# E2E テスト用(ローカルと CI のみ)
E2E_EMAIL=<ここに貼る:テスト用ユーザーのメール>
E2E_PASSWORD=<ここに貼る:テスト用ユーザーのパスワード>
確認: git ls-files .env.local が空
サインアップ・ログイン・ログアウトと、/dashboard の保護を作ります。確認メールが届いてからログインできることを確かめます。
Next.js(App Router)と @supabase/ssr で、メール + パスワードの認証を作ってください。 画面: - /signup: メール・パスワード(8 文字以上)・確認。登録後は確認メールの案内 - /login: メール・パスワード。失敗時は理由を表示(存在しないメールか間違いかは区別しない) - ログアウトはヘッダーのボタン - /dashboard は未ログインなら /login へ 302(middleware で保護) 条件: - クライアントには NEXT_PUBLIC_SUPABASE_URL と NEXT_PUBLIC_SUPABASE_ANON_KEY だけを渡す。service role key は使わない - セッションは cookie(@supabase/ssr の推奨どおり) - フォームは aria-live でエラーを読み上げる - 環境変数は .env.example にキー名とプレースホルダー(<ここに貼る:…>)だけ書く
import { NextResponse, type NextRequest } from 'next/server';
import { updateSession } from '@/lib/supabase/middleware';
export async function middleware(request: NextRequest) {
const { response, user } = await updateSession(request);
if (!user && request.nextUrl.pathname.startsWith('/dashboard')) {
const url = request.nextUrl.clone();
url.pathname = '/login';
return NextResponse.redirect(url);
}
return response;
}
export const config = { matcher: ['/dashboard/:path*'] };
確認: 未ログインで /dashboard を開くと /login に行く
一覧・追加・編集・削除を Server Actions で作ります。user_id は必ずサーバーで付けます。
/dashboard にメモの一覧・追加・編集・削除を作ってください。 条件: - Server Actions で insert / update / delete を行い、必ずログインユーザーの user_id を付ける(クライアントから user_id を受け取らない) - 一覧はサーバーコンポーネントで取得し、空のときは「まだメモがありません」 - 追加フォームは送信中に無効化、完了後にリストを再検証(revalidatePath) - 削除は確認ダイアログ付き - 読み込み中はスケルトン、完了時は小さなトーストで知らせる - 375px で 1 カラムになる
'use server';
import { revalidatePath } from 'next/cache';
import { createClient } from '@/lib/supabase/server';
export async function addNote(formData: FormData) {
const supabase = await createClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) return { ok: false, message: 'ログインが必要です' };
const title = String(formData.get('title') ?? '').trim();
if (title.length < 1 || title.length > 120) return { ok: false, message: 'タイトルは 1〜120 文字です' };
// user_id はサーバーで付ける(クライアントからは受け取らない)
const { error } = await supabase.from('notes').insert({ title, user_id: user.id });
if (error) return { ok: false, message: '保存できませんでした' };
revalidatePath('/dashboard');
return { ok: true };
}
確認: 追加したメモが一覧に出て、削除で消える
2 つ目のユーザーでログインし、1 つ目のメモが見えないこと、id を指定した更新・削除も失敗することを確かめます。これが RLS の効果です。
このアプリについて、次を確認して結果を表にしてください。 - RLS が notes で有効で、4 つの操作すべてにポリシーがある - Server Action が user_id をクライアントから受け取っていない - service role key がクライアント側のコードと .env.example に無い - 未ログインで /dashboard を開くと /login に行く - 別ユーザーのメモ id を指定して update / delete を呼んでも失敗する(curl の手順も書く) - エラーメッセージに他ユーザーの情報やスタックトレースが出ない 問題があれば修正案も出してください。
確認: 別ユーザーのメモは一覧にも API にも出ない
ログイン保護・追加と削除・ログアウト後の保護を Playwright で自動化します。テスト用ユーザーの情報は環境変数から読みます。
Playwright で次の 3 本の E2E テストを書いてください。テスト用ユーザーは環境変数 E2E_EMAIL / E2E_PASSWORD から読み、コードに書かないこと。 1. 未ログインで /dashboard を開くと /login に遷移する 2. ログインしてメモを追加すると一覧に表示され、削除すると消える 3. ログアウト後に /dashboard を開くと /login に戻る npm run test:e2e で動くようにし、CI で動かす場合の注意点を最後に 3 行で書いてください。
確認: npm run test:e2e が 3 本とも通る
Vercel に環境変数(公開してよい 2 つ)を登録してデプロイします。Supabase の Auth 設定で、公開 URL を Site URL と Redirect URLs に追加します。
確認: 公開 URL でサインアップから削除まで一通りできる
チェックはこのブラウザにだけ保存されます。
| 済 | 項目 | やり方 | 期待結果 |
|---|---|---|---|
| ログアウトした状態で /dashboard を開く | /login に 302 で戻る | ||
| 2 ユーザーで交互にログインして一覧を見る | 相手のメモは一覧に出ない | ||
| ユーザー B のセッションで、ユーザー A のメモ id を指定して update / delete を呼ぶ(Supabase REST を curl) | 0 行が対象になり、A のメモは変わらない | ||
| 追加フォームの送信に user_id を混ぜて送る | 無視され、ログインユーザーの id で保存される | ||
| ビルド後の JS を SUPABASE_SERVICE_ROLE で検索する | 見つからない | ||
| /login の「パスワードを忘れた」から再設定する | メールが届き、新しいパスワードでログインできる | ||
| npm run test:e2e | 3 本すべて成功 | ||
| 端末エミュレーションで /login と /dashboard を見る | 1 カラムになり、横スクロールが出ない |
「<ここに貼る:…>」の形で書かれた値は、環境変数など公開されない場所に置きます。コード・リポジトリ・このページには入れません。
原因: cookie ベースのセッションが middleware で更新されていない、または Supabase の Site URL / Redirect URLs に公開 URL が無い
対処: @supabase/ssr の updateSession を middleware で呼ぶ。Supabase の URL Configuration に公開 URL を追加する
原因: insert は成功しているが revalidatePath を呼んでいない、または RLS の select ポリシーが無い
対処: Server Action の最後で revalidatePath('/dashboard') を呼ぶ。4 つのポリシーが揃っているか確認する
原因: insert の with check が auth.uid() = user_id なのに、user_id を付けていない
対処: Server Action でログインユーザーの id を user_id に入れる
原因: RLS を有効化していない(alter table … enable row level security を実行していない)
対処: RLS を有効化し、ポリシーを作り直す。公開前に別ユーザーで必ず確認する
原因: CI に E2E_EMAIL / E2E_PASSWORD が無い、または確認メール待ちのユーザーを使っている
対処: CI の Secrets に設定し、確認済みのテスト用ユーザーを使う
この教材の手順・プロンプト・検証・安全性を 1 枚にまとめた Markdown です。コピーして Claude Code や Codex に渡します。
# 実装指示: Next.js でログインとダッシュボード付きの Web アプリを作る
このファイルの指示に従って実装してください。分からない点は実装前に質問してください。
## 概要
- 教材: https://omochix.com/learn/practice/nextjs-login-dashboard/
- 難易度: 中級 / 所要 約240分
- 対応AI: Claude Code / Cursor
- 使用技術: Next.js / Supabase / TypeScript / Vercel
- 最終検証日: 2026-10-21
## 作るもの
サインアップ・ログイン・ログアウト、ログイン後のダッシュボードでメモの追加・編集・削除ができる Web アプリ。他のユーザーのメモは API を直接叩いても見えない。Vercel で公開済み。
## 前提条件
- GitHub アカウントと Vercel アカウント
- Supabase の無料プロジェクト(Auth のメール確認を有効にする)
- Node.js 20 以上、Claude Code か Cursor
- 先に読む: ログイン機能を追加する / データベースを追加する(Learn Tutorials)
## 動作確認環境
- Node.js 20
- Next.js 15(App Router、Server Actions)
- @supabase/ssr
- Supabase(Postgres + Auth、2026年10月時点の無料枠)
- Playwright(E2E)
## 実装手順
### STEP 1: 作るものを決める
この教材では「自分だけのメモ」を扱います。あなたのアプリでは、ユーザーごとに分かれていなければならないデータを 1 つ選んでください(予約・日報・在庫など)。
- 確認: テーブルにする対象が 1 つ決まった
### STEP 2: Supabase でテーブルと RLS を作る
AI に SQL を出してもらい、Supabase の SQL エディタで実行します。RLS を有効にする前にデータを入れないでください。
AI に渡すプロンプト(データ設計と RLS ポリシー):
```text
Supabase(Postgres)で「ユーザーごとのメモ」を扱うテーブルを設計してください。
要件:
- テーブル notes: id (uuid, 既定値 gen_random_uuid()), user_id (auth.users への参照), title (text, 1〜120 文字), body (text, 最大 5000 文字), created_at, updated_at
- RLS を有効にし、select / insert / update / delete のすべてで auth.uid() = user_id だけを許可するポリシー
- updated_at を自動更新するトリガー
出力: Supabase の SQL エディタにそのまま貼れる SQL と、各ポリシーが何を防ぐかの 1 行説明。service role key を使う前提の設計にはしないこと。
```
コード例(supabase/notes.sql):
```sql
create table if not exists public.notes (
id uuid primary key default gen_random_uuid(),
user_id uuid not null references auth.users (id) on delete cascade,
title text not null check (char_length(title) between 1 and 120),
body text not null default '' check (char_length(body) <= 5000),
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
alter table public.notes enable row level security;
create policy "own notes: select" on public.notes for select using (auth.uid() = user_id);
create policy "own notes: insert" on public.notes for insert with check (auth.uid() = user_id);
create policy "own notes: update" on public.notes for update using (auth.uid() = user_id) with check (auth.uid() = user_id);
create policy "own notes: delete" on public.notes for delete using (auth.uid() = user_id);
```
- 確認: Table Editor で notes が見え、RLS が ON になっている
### STEP 3: プロジェクトを作り、環境変数を置く
npx create-next-app@latest でプロジェクトを作り、.env.local に URL と anon key を入れます。.env.example はプレースホルダーだけです。
コード例(.env.example):
```bash
# 公開してよいのはこの 2 つだけ。service role key はここにも書かない
NEXT_PUBLIC_SUPABASE_URL=<ここに貼る:Supabase プロジェクトの URL>
NEXT_PUBLIC_SUPABASE_ANON_KEY=<ここに貼る:anon key(RLS があるので公開可)>
# E2E テスト用(ローカルと CI のみ)
E2E_EMAIL=<ここに貼る:テスト用ユーザーのメール>
E2E_PASSWORD=<ここに貼る:テスト用ユーザーのパスワード>
```
- 確認: git ls-files .env.local が空
### STEP 4: 認証 UI を作る
サインアップ・ログイン・ログアウトと、/dashboard の保護を作ります。確認メールが届いてからログインできることを確かめます。
AI に渡すプロンプト(認証 UI(サインアップ / ログイン / ログアウト)):
```text
Next.js(App Router)と @supabase/ssr で、メール + パスワードの認証を作ってください。
画面:
- /signup: メール・パスワード(8 文字以上)・確認。登録後は確認メールの案内
- /login: メール・パスワード。失敗時は理由を表示(存在しないメールか間違いかは区別しない)
- ログアウトはヘッダーのボタン
- /dashboard は未ログインなら /login へ 302(middleware で保護)
条件:
- クライアントには NEXT_PUBLIC_SUPABASE_URL と NEXT_PUBLIC_SUPABASE_ANON_KEY だけを渡す。service role key は使わない
- セッションは cookie(@supabase/ssr の推奨どおり)
- フォームは aria-live でエラーを読み上げる
- 環境変数は .env.example にキー名とプレースホルダー(<ここに貼る:…>)だけ書く
```
コード例(middleware.ts):
```typescript
import { NextResponse, type NextRequest } from 'next/server';
import { updateSession } from '@/lib/supabase/middleware';
export async function middleware(request: NextRequest) {
const { response, user } = await updateSession(request);
if (!user && request.nextUrl.pathname.startsWith('/dashboard')) {
const url = request.nextUrl.clone();
url.pathname = '/login';
return NextResponse.redirect(url);
}
return response;
}
export const config = { matcher: ['/dashboard/:path*'] };
```
- 確認: 未ログインで /dashboard を開くと /login に行く
### STEP 5: ダッシュボードのメモ CRUD を作る
一覧・追加・編集・削除を Server Actions で作ります。user_id は必ずサーバーで付けます。
AI に渡すプロンプト(ダッシュボードのメモ CRUD):
```text
/dashboard にメモの一覧・追加・編集・削除を作ってください。
条件:
- Server Actions で insert / update / delete を行い、必ずログインユーザーの user_id を付ける(クライアントから user_id を受け取らない)
- 一覧はサーバーコンポーネントで取得し、空のときは「まだメモがありません」
- 追加フォームは送信中に無効化、完了後にリストを再検証(revalidatePath)
- 削除は確認ダイアログ付き
- 読み込み中はスケルトン、完了時は小さなトーストで知らせる
- 375px で 1 カラムになる
```
コード例(app/dashboard/actions.ts):
```typescript
'use server';
import { revalidatePath } from 'next/cache';
import { createClient } from '@/lib/supabase/server';
export async function addNote(formData: FormData) {
const supabase = await createClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) return { ok: false, message: 'ログインが必要です' };
const title = String(formData.get('title') ?? '').trim();
if (title.length < 1 || title.length > 120) return { ok: false, message: 'タイトルは 1〜120 文字です' };
// user_id はサーバーで付ける(クライアントからは受け取らない)
const { error } = await supabase.from('notes').insert({ title, user_id: user.id });
if (error) return { ok: false, message: '保存できませんでした' };
revalidatePath('/dashboard');
return { ok: true };
}
```
- 確認: 追加したメモが一覧に出て、削除で消える
### STEP 6: 別ユーザーで分離を確かめる
2 つ目のユーザーでログインし、1 つ目のメモが見えないこと、id を指定した更新・削除も失敗することを確かめます。これが RLS の効果です。
AI に渡すプロンプト(セキュリティのセルフチェック):
```text
このアプリについて、次を確認して結果を表にしてください。
- RLS が notes で有効で、4 つの操作すべてにポリシーがある
- Server Action が user_id をクライアントから受け取っていない
- service role key がクライアント側のコードと .env.example に無い
- 未ログインで /dashboard を開くと /login に行く
- 別ユーザーのメモ id を指定して update / delete を呼んでも失敗する(curl の手順も書く)
- エラーメッセージに他ユーザーの情報やスタックトレースが出ない
問題があれば修正案も出してください。
```
- 確認: 別ユーザーのメモは一覧にも API にも出ない
### STEP 7: E2E テストを 3 本書く
ログイン保護・追加と削除・ログアウト後の保護を Playwright で自動化します。テスト用ユーザーの情報は環境変数から読みます。
AI に渡すプロンプト(E2E テスト 3 本):
```text
Playwright で次の 3 本の E2E テストを書いてください。テスト用ユーザーは環境変数 E2E_EMAIL / E2E_PASSWORD から読み、コードに書かないこと。
1. 未ログインで /dashboard を開くと /login に遷移する
2. ログインしてメモを追加すると一覧に表示され、削除すると消える
3. ログアウト後に /dashboard を開くと /login に戻る
npm run test:e2e で動くようにし、CI で動かす場合の注意点を最後に 3 行で書いてください。
```
- 確認: npm run test:e2e が 3 本とも通る
### STEP 8: Vercel に公開する
Vercel に環境変数(公開してよい 2 つ)を登録してデプロイします。Supabase の Auth 設定で、公開 URL を Site URL と Redirect URLs に追加します。
- 確認: 公開 URL でサインアップから削除まで一通りできる
## 検証
- 未ログインで保護される — やり方: ログアウトした状態で /dashboard を開く / 期待結果: /login に 302 で戻る
- 自分のメモだけ見える — やり方: 2 ユーザーで交互にログインして一覧を見る / 期待結果: 相手のメモは一覧に出ない
- API を直接叩いても他人のデータは触れない — やり方: ユーザー B のセッションで、ユーザー A のメモ id を指定して update / delete を呼ぶ(Supabase REST を curl) / 期待結果: 0 行が対象になり、A のメモは変わらない
- user_id を偽装できない — やり方: 追加フォームの送信に user_id を混ぜて送る / 期待結果: 無視され、ログインユーザーの id で保存される
- service role key がクライアントに無い — やり方: ビルド後の JS を SUPABASE_SERVICE_ROLE で検索する / 期待結果: 見つからない
- パスワードリセットが動く — やり方: /login の「パスワードを忘れた」から再設定する / 期待結果: メールが届き、新しいパスワードでログインできる
- E2E テストが通る — やり方: npm run test:e2e / 期待結果: 3 本すべて成功
- 375px で崩れない — やり方: 端末エミュレーションで /login と /dashboard を見る / 期待結果: 1 カラムになり、横スクロールが出ない
## 安全性・秘密情報
- anon key は公開前提だが、RLS が有効で正しいポリシーがあることが前提。RLS なしで公開しない
- service role key はサーバーの秘密。この教材では一切使わず、.env.example にも書かない
- user_id はサーバー(Server Action)で付け、クライアントからの値を信用しない
- エラーメッセージに他ユーザーの情報やスタックトレースを出さない
- テスト用ユーザーの認証情報は環境変数に置き、コードに書かない
- パスワードは 8 文字以上、確認メールを有効にする
- 「<ここに貼る:…>」の形のプレースホルダーは、環境変数など公開されない場所に実際の値を置き、コードやリポジトリに書かない
## 公開手順
- Vercel の Environment Variables に NEXT_PUBLIC_SUPABASE_URL と NEXT_PUBLIC_SUPABASE_ANON_KEY を登録する
- Supabase の Authentication → URL Configuration に公開 URL を Site URL と Redirect URLs として追加する
- 公開 URL でサインアップ → 確認メール → ログイン → メモ追加 → 削除を 1 周する
- 独自ドメインを設定し、HTTPS を確認する
## よくあるエラー
- ログイン後に /dashboard を開いても /login に戻される — 原因: cookie ベースのセッションが middleware で更新されていない、または Supabase の Site URL / Redirect URLs に公開 URL が無い / 対処: @supabase/ssr の updateSession を middleware で呼ぶ。Supabase の URL Configuration に公開 URL を追加する
- メモを追加しても一覧に出ない — 原因: insert は成功しているが revalidatePath を呼んでいない、または RLS の select ポリシーが無い / 対処: Server Action の最後で revalidatePath('/dashboard') を呼ぶ。4 つのポリシーが揃っているか確認する
- insert が「new row violates row-level security policy」で失敗する — 原因: insert の with check が auth.uid() = user_id なのに、user_id を付けていない / 対処: Server Action でログインユーザーの id を user_id に入れる
- 他人のメモが見えてしまう — 原因: RLS を有効化していない(alter table … enable row level security を実行していない) / 対処: RLS を有効化し、ポリシーを作り直す。公開前に別ユーザーで必ず確認する
- E2E が CI だけ落ちる — 原因: CI に E2E_EMAIL / E2E_PASSWORD が無い、または確認メール待ちのユーザーを使っている / 対処: CI の Secrets に設定し、確認済みのテスト用ユーザーを使う
## 参照した公式仕様
- Supabase Row Level Security https://supabase.com/docs/guides/database/postgres/row-level-security
- Supabase Auth with Next.js (SSR) https://supabase.com/docs/guides/auth/server-side/nextjs
- Next.js Server Actions https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations
- Playwright Getting started https://playwright.dev/docs/intro
## 受け入れ条件
- 上の検証項目がすべて期待結果どおりになる
- 秘密情報がコード・設定ファイル・公開ページに含まれていない
- スマートフォン幅でも表示が崩れず、キーボードだけで操作できる
# データ設計と RLS ポリシー Supabase(Postgres)で「ユーザーごとのメモ」を扱うテーブルを設計してください。 要件: - テーブル notes: id (uuid, 既定値 gen_random_uuid()), user_id (auth.users への参照), title (text, 1〜120 文字), body (text, 最大 5000 文字), created_at, updated_at - RLS を有効にし、select / insert / update / delete のすべてで auth.uid() = user_id だけを許可するポリシー - updated_at を自動更新するトリガー 出力: Supabase の SQL エディタにそのまま貼れる SQL と、各ポリシーが何を防ぐかの 1 行説明。service role key を使う前提の設計にはしないこと。 --- # 認証 UI(サインアップ / ログイン / ログアウト) Next.js(App Router)と @supabase/ssr で、メール + パスワードの認証を作ってください。 画面: - /signup: メール・パスワード(8 文字以上)・確認。登録後は確認メールの案内 - /login: メール・パスワード。失敗時は理由を表示(存在しないメールか間違いかは区別しない) - ログアウトはヘッダーのボタン - /dashboard は未ログインなら /login へ 302(middleware で保護) 条件: - クライアントには NEXT_PUBLIC_SUPABASE_URL と NEXT_PUBLIC_SUPABASE_ANON_KEY だけを渡す。service role key は使わない - セッションは cookie(@supabase/ssr の推奨どおり) - フォームは aria-live でエラーを読み上げる - 環境変数は .env.example にキー名とプレースホルダー(<ここに貼る:…>)だけ書く --- # ダッシュボードのメモ CRUD /dashboard にメモの一覧・追加・編集・削除を作ってください。 条件: - Server Actions で insert / update / delete を行い、必ずログインユーザーの user_id を付ける(クライアントから user_id を受け取らない) - 一覧はサーバーコンポーネントで取得し、空のときは「まだメモがありません」 - 追加フォームは送信中に無効化、完了後にリストを再検証(revalidatePath) - 削除は確認ダイアログ付き - 読み込み中はスケルトン、完了時は小さなトーストで知らせる - 375px で 1 カラムになる --- # セキュリティのセルフチェック このアプリについて、次を確認して結果を表にしてください。 - RLS が notes で有効で、4 つの操作すべてにポリシーがある - Server Action が user_id をクライアントから受け取っていない - service role key がクライアント側のコードと .env.example に無い - 未ログインで /dashboard を開くと /login に行く - 別ユーザーのメモ id を指定して update / delete を呼んでも失敗する(curl の手順も書く) - エラーメッセージに他ユーザーの情報やスタックトレースが出ない 問題があれば修正案も出してください。 --- # E2E テスト 3 本 Playwright で次の 3 本の E2E テストを書いてください。テスト用ユーザーは環境変数 E2E_EMAIL / E2E_PASSWORD から読み、コードに書かないこと。 1. 未ログインで /dashboard を開くと /login に遷移する 2. ログインしてメモを追加すると一覧に表示され、削除すると消える 3. ログアウト後に /dashboard を開くと /login に戻る npm run test:e2e で動くようにし、CI で動かす場合の注意点を最後に 3 行で書いてください。