2025年人気No.1のNext.jsで、最新ツールとウェブを作る！ | エピソード7 フォームと認証

## 目標
- Next.jsでフォームを扱う方法を学ぶ。
- `react-hook-form`を使って効率的にフォームを作成する。
- NextAuth.jsで認証（Googleログインなど）を統合する。
- 管理者用のページを保護する。

---

## 1. フォームライブラリの導入
フォーム処理を簡単にするために、`react-hook-form`を使用します。

### インストール：
ターミナルで実行：
```
npm install react-hook-form
```

---

## 2. 投稿追加フォームの作成
Supabaseに投稿を追加するフォームを実装します。

### `app/add-post/page.tsx`の作成：
```tsx
'use client';

import { useForm } from 'react-hook-form';
import { supabase } from '../../lib/supabase';

type FormData = {
  title: string;
  content: string;
};

export default function AddPost() {
  const { register, handleSubmit, reset } = useForm<FormData>();

  const onSubmit = async (data: FormData) => {
    const { error } = await supabase.from('posts').insert([data]);
    if (error) {
      console.error('エラー:', error);
    } else {
      alert('投稿が追加されました！');
      reset();
    }
  };

  return (
    <div className="max-w-2xl mx-auto p-6">
      <h1 className="text-3xl font-bold text-blue-600 mb-6">新しい投稿を追加</h1>
      <form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
        <div>
          <label className="block text-gray-700">タイトル</label>
          <input
            {...register('title', { required: true })}
            className="w-full p-2 border rounded"
            placeholder="投稿のタイトル"
          />
        </div>
        <div>
          <label className="block text-gray-700">内容</label>
          <textarea
            {...register('content', { required: true })}
            className="w-full p-2 border rounded"
            placeholder="投稿の内容"
            rows={5}
          />
        </div>
        <button type="submit" className="bg-blue-500 text-white p-2 rounded hover:bg-blue-600">
          投稿する
        </button>
      </form>
    </div>
  );
}
```

**注意**：
- `'use client'`を追加して、クライアント側で動作するコンポーネントに指定。

---

## 3. NextAuth.jsでの認証設定
Googleログインを追加して、認証を導入します。

### インストール：
```
npm install next-auth
```

### `app/api/auth/[...nextauth]/route.ts`の作成：
```ts
import NextAuth from 'next-auth';
import GoogleProvider from 'next-auth/providers/google';

export const authOptions = {
  providers: [
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID || '',
      clientSecret: process.env.GOOGLE_CLIENT_SECRET || '',
    }),
  ],
};

const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };
```

### 環境変数の設定：
`.env.local`に以下を追加：
```
GOOGLE_CLIENT_ID=あなたのGoogleクライアントID
GOOGLE_CLIENT_SECRET=あなたのGoogleクライアントシークレット
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=ランダムな文字列（例：openssl rand -base64 32で生成）
```

**Google設定**：
- Google Cloud ConsoleでOAuth 2.0クライアントを作成し、IDとシークレットを入手。

---

## 4. 認証状態の管理と保護
ログイン状態を確認し、管理者ページを保護します。

### ログイン状態の表示：
`app/layout.tsx`を編集してヘッダーにログイン状態を追加：
```tsx
import './globals.css';
import Link from 'next/link';
import { auth } from 'next-auth';
import { signIn, signOut } from 'next-auth/react';

export const metadata = {
  title: 'Next.js 2025シリーズ',
  description: '2025年のウェブ開発を学ぶ',
};

export default async function RootLayout({ children }) {
  const session = await auth();

  return (
    <html lang="ja">
      <body className="font-sans antialiased">
        <header className="bg-blue-800 text-white p-4">
          <nav className="max-w-4xl mx-auto flex justify-between items-center">
            <Link href="/" className="text-xl font-bold">ホーム</Link>
            <div className="space-x-4">
              <Link href="/about" className="hover:underline">About</Link>
              <Link href="/add-post" className="hover:underline">投稿追加</Link>
              {session ? (
                <>
                  <span>{session.user?.name}</span>
                  <button onClick={() => signOut()} className="hover:underline">ログアウト</button>
                </>
              ) : (
                <button onClick={() => signIn('google')} className="hover:underline">Googleでログイン</button>
              )}
            </div>
          </nav>
        </header>
        <main>{children}</main>
        <footer className="bg-gray-200 p-4 text-center text-gray-600">
          © 2025 Next.js学習シリーズ
        </footer>
      </body>
    </html>
  );
}
```

### ページの保護：
`app/add-post/page.tsx`を保護：
```tsx
'use client';

import { useForm } from 'react-hook-form';
import { supabase } from '../../lib/supabase';
import { useSession } from 'next-auth/react';
import { redirect } from 'next/navigation';

type FormData = {
  title: string;
  content: string;
};

export default function AddPost() {
  const { data: session } = useSession();

  if (!session) {
    redirect('/api/auth/signin');
  }

  const { register, handleSubmit, reset } = useForm<FormData>();

  const onSubmit = async (data: FormData) => {
    const { error } = await supabase.from('posts').insert([data]);
    if (error) {
      console.error('エラー:', error);
    } else {
      alert('投稿が追加されました！');
      reset();
    }
  };

  return (
    <div className="max-w-2xl mx-auto p-6">
      <h1 className="text-3xl font-bold text-blue-600 mb-6">新しい投稿を追加</h1>
      <form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
        <div>
          <label className="block text-gray-700">タイトル</label>
          <input
            {...register('title', { required: true })}
            className="w-full p-2 border rounded"
            placeholder="投稿のタイトル"
          />
        </div>
        <div>
          <label className="block text-gray-700">内容</label>
          <textarea
            {...register('content', { required: true })}
            className="w-full p-2 border rounded"
            placeholder="投稿の内容"
            rows={5}
          />
        </div>
        <button type="submit" className="bg-blue-500 text-white p-2 rounded hover:bg-blue-600">
          投稿する
        </button>
      </form>
    </div>
  );
}
```

---

## 実践：認証付きブログ
- **フォーム**: ログインしたユーザーが投稿を追加可能。
- **認証**: Googleログインでアクセスを管理。
- **保護**: 未ログインの場合は投稿追加ページにアクセス不可。

**確認**：
- `/add-post`にアクセスし、ログインを試みて投稿を追加。

---

## エピソード7の終了
- フォームと認証をNext.jsに統合しました。
- ユーザーインタラクションを強化しました。

**次回のエピソード**：パフォーマンスとSEOを最適化し、ウェブサイトを完成させます。

---

この記事が役に立ったと思ったら、ぜひ「いいね」を押して、ストックしていただければ嬉しいです！次回のエピソードもお楽しみに！