【第四弾】Laravel入門資料

📚 Laravel公式ドキュメント - Pagination

https://laravel.com/docs/12.x/pagination

💡 この講座では、上記の公式ドキュメントを基に解説していきます。
公式ドキュメントは初学者には内容が難しいため、エッセンスを優しく噛み砕いて解説していきます。

ページネーション - 大量データを快適に表示する

商品が100件、1000件と増えてきたとき、全てを一度に表示するとページが重くなります
ページネーション機能を実装して、データを分割表示する方法を学びます。

🎯 この章で学ぶこと

  • ✅ ページネーションの基本 - paginate() の使い方
  • ✅ ページネーションリンクの自動生成 - {{ $products->links() }}
  • ✅ 表示件数のカスタマイズ方法
  • ✅ ソート順の指定方法 - orderBy()
  • ✅ ページネーションとEager Loadingの組み合わせ

💡 前提条件

  • products テーブルと categories テーブルが作成済み
  • ✅ Product モデルと Category モデルが作成済み
  • ✅ リレーション(belongsTo)が設定済み

※ 「ここまでの知識の実践」で作成したテーブルとモデルをそのまま使用します

🗄️ テストデータの準備 - ページネーション動作確認用

ページネーション機能を確認するには、十分なテストデータが必要です。
paginate(10)を使う場合、ページネーションを表示するには11件以上のデータが必要になります。

推奨: 40件のテストデータを作成(公開38件 + 非公開1件 + ソフトデリート1件)
→ 検索時もページネーションが複数ページになり、appends()の動作確認ができます

📝 ProductSeeder.php の編集

database/seeders/ProductSeeder.php

<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;

class ProductSeeder extends Seeder
{
    public function run(): void
    {
        DB::table('products')->insert([
            // 家電カテゴリー (15件)
            ['name' => 'ノートPC', 'description' => '高性能ノートパソコン', 'price' => 89800, 'stock' => 15, 'is_published' => true, 'category_id' => 1, 'created_at' => now(), 'updated_at' => now(), 'deleted_at' => null],
            ['name' => 'デスクトップPC', 'description' => 'ゲーミングPC', 'price' => 120000, 'stock' => 8, 'is_published' => true, 'category_id' => 1, 'created_at' => now(), 'updated_at' => now(), 'deleted_at' => null],
            ['name' => 'マウス', 'description' => 'ワイヤレスマウス', 'price' => 2980, 'stock' => 50, 'is_published' => true, 'category_id' => 1, 'created_at' => now(), 'updated_at' => now(), 'deleted_at' => null],
            ['name' => 'キーボード', 'description' => 'メカニカルキーボード', 'price' => 8900, 'stock' => 30, 'is_published' => true, 'category_id' => 1, 'created_at' => now(), 'updated_at' => now(), 'deleted_at' => null],
            ['name' => 'モニター', 'description' => '27インチ4Kモニター', 'price' => 34800, 'stock' => 12, 'is_published' => true, 'category_id' => 1, 'created_at' => now(), 'updated_at' => now(), 'deleted_at' => null],
            ['name' => 'Webカメラ', 'description' => 'フルHD対応', 'price' => 6800, 'stock' => 25, 'is_published' => true, 'category_id' => 1, 'created_at' => now(), 'updated_at' => now(), 'deleted_at' => null],
            ['name' => 'ヘッドセット', 'description' => 'ノイズキャンセリング', 'price' => 12800, 'stock' => 18, 'is_published' => true, 'category_id' => 1, 'created_at' => now(), 'updated_at' => now(), 'deleted_at' => null],
            ['name' => 'プリンター', 'description' => 'カラーレーザー', 'price' => 28000, 'stock' => 10, 'is_published' => true, 'category_id' => 1, 'created_at' => now(), 'updated_at' => now(), 'deleted_at' => null],
            ['name' => 'スキャナー', 'description' => 'ドキュメント用', 'price' => 18000, 'stock' => 8, 'is_published' => true, 'category_id' => 1, 'created_at' => now(), 'updated_at' => now(), 'deleted_at' => null],
            ['name' => '外付けHDD', 'description' => '2TB', 'price' => 9800, 'stock' => 30, 'is_published' => true, 'category_id' => 1, 'created_at' => now(), 'updated_at' => now(), 'deleted_at' => null],
            ['name' => 'USBメモリ', 'description' => '64GB', 'price' => 1280, 'stock' => 100, 'is_published' => true, 'category_id' => 1, 'created_at' => now(), 'updated_at' => now(), 'deleted_at' => null],
            ['name' => 'スピーカー', 'description' => 'Bluetooth対応', 'price' => 5800, 'stock' => 20, 'is_published' => true, 'category_id' => 1, 'created_at' => now(), 'updated_at' => now(), 'deleted_at' => null],
            ['name' => 'ゲーミングチェア', 'description' => 'リクライニング機能', 'price' => 32000, 'stock' => 5, 'is_published' => true, 'category_id' => 1, 'created_at' => now(), 'updated_at' => now(), 'deleted_at' => null],
            ['name' => 'スマートウォッチ', 'description' => '在庫切れ・非公開テスト用', 'price' => 45000, 'stock' => 0, 'is_published' => false, 'category_id' => 1, 'created_at' => now(), 'updated_at' => now(), 'deleted_at' => null],
            ['name' => 'タブレット', 'description' => 'ソフトデリートテスト用', 'price' => 58000, 'stock' => 5, 'is_published' => true, 'category_id' => 1, 'created_at' => now(), 'updated_at' => now(), 'deleted_at' => now()],

            // 食品カテゴリー (10件)
            ['name' => 'オーガニックコーヒー', 'description' => '香り高いコーヒー豆', 'price' => 1500, 'stock' => 30, 'is_published' => true, 'category_id' => 2, 'created_at' => now(), 'updated_at' => now(), 'deleted_at' => null],
            ['name' => '緑茶', 'description' => '静岡産', 'price' => 980, 'stock' => 50, 'is_published' => true, 'category_id' => 2, 'created_at' => now(), 'updated_at' => now(), 'deleted_at' => null],
            ['name' => 'はちみつ', 'description' => '国産100%', 'price' => 2800, 'stock' => 20, 'is_published' => true, 'category_id' => 2, 'created_at' => now(), 'updated_at' => now(), 'deleted_at' => null],
            ['name' => 'オリーブオイル', 'description' => 'エキストラバージン', 'price' => 1680, 'stock' => 35, 'is_published' => true, 'category_id' => 2, 'created_at' => now(), 'updated_at' => now(), 'deleted_at' => null],
            ['name' => 'パスタ', 'description' => 'デュラムセモリナ100%', 'price' => 380, 'stock' => 100, 'is_published' => true, 'category_id' => 2, 'created_at' => now(), 'updated_at' => now(), 'deleted_at' => null],
            ['name' => 'トマトソース', 'description' => 'イタリア産', 'price' => 480, 'stock' => 60, 'is_published' => true, 'category_id' => 2, 'created_at' => now(), 'updated_at' => now(), 'deleted_at' => null],
            ['name' => 'グラノーラ', 'description' => 'ナッツ&フルーツ', 'price' => 880, 'stock' => 40, 'is_published' => true, 'category_id' => 2, 'created_at' => now(), 'updated_at' => now(), 'deleted_at' => null],
            ['name' => 'ジャム', 'description' => 'いちご', 'price' => 680, 'stock' => 45, 'is_published' => true, 'category_id' => 2, 'created_at' => now(), 'updated_at' => now(), 'deleted_at' => null],
            ['name' => 'チョコレート', 'description' => 'ダークチョコ', 'price' => 580, 'stock' => 80, 'is_published' => true, 'category_id' => 2, 'created_at' => now(), 'updated_at' => now(), 'deleted_at' => null],
            ['name' => 'マグカップ', 'description' => '陶器製', 'price' => 1200, 'stock' => 80, 'is_published' => true, 'category_id' => 2, 'created_at' => now(), 'updated_at' => now(), 'deleted_at' => null],

            // 衣類カテゴリー (10件)
            ['name' => 'Tシャツ', 'description' => 'コットン100%', 'price' => 2900, 'stock' => 100, 'is_published' => true, 'category_id' => 3, 'created_at' => now(), 'updated_at' => now(), 'deleted_at' => null],
            ['name' => 'ジーンズ', 'description' => 'デニムパンツ', 'price' => 5980, 'stock' => 45, 'is_published' => true, 'category_id' => 3, 'created_at' => now(), 'updated_at' => now(), 'deleted_at' => null],
            ['name' => 'スニーカー', 'description' => 'ランニングシューズ', 'price' => 7800, 'stock' => 20, 'is_published' => true, 'category_id' => 3, 'created_at' => now(), 'updated_at' => now(), 'deleted_at' => null],
            ['name' => 'パーカー', 'description' => '裏起毛', 'price' => 4800, 'stock' => 60, 'is_published' => true, 'category_id' => 3, 'created_at' => now(), 'updated_at' => now(), 'deleted_at' => null],
            ['name' => 'キャップ', 'description' => '調整可能', 'price' => 2400, 'stock' => 40, 'is_published' => true, 'category_id' => 3, 'created_at' => now(), 'updated_at' => now(), 'deleted_at' => null],
            ['name' => 'ジャケット', 'description' => '防水加工', 'price' => 12800, 'stock' => 25, 'is_published' => true, 'category_id' => 3, 'created_at' => now(), 'updated_at' => now(), 'deleted_at' => null],
            ['name' => 'マフラー', 'description' => 'ウール100%', 'price' => 3200, 'stock' => 35, 'is_published' => true, 'category_id' => 3, 'created_at' => now(), 'updated_at' => now(), 'deleted_at' => null],
            ['name' => '手袋', 'description' => 'タッチパネル対応', 'price' => 1800, 'stock' => 50, 'is_published' => true, 'category_id' => 3, 'created_at' => now(), 'updated_at' => now(), 'deleted_at' => null],
            ['name' => 'ベルト', 'description' => '本革', 'price' => 4200, 'stock' => 30, 'is_published' => true, 'category_id' => 3, 'created_at' => now(), 'updated_at' => now(), 'deleted_at' => null],
            ['name' => 'ソックス', 'description' => '3足セット', 'price' => 980, 'stock' => 120, 'is_published' => true, 'category_id' => 3, 'created_at' => now(), 'updated_at' => now(), 'deleted_at' => null],

            // 書籍カテゴリー (10件)
            ['name' => 'Laravel入門書', 'description' => '初心者向けの解説書', 'price' => 2800, 'stock' => 20, 'is_published' => true, 'category_id' => 4, 'created_at' => now(), 'updated_at' => now(), 'deleted_at' => null],
            ['name' => 'PHP実践ガイド', 'description' => '中級者向け', 'price' => 3200, 'stock' => 15, 'is_published' => true, 'category_id' => 4, 'created_at' => now(), 'updated_at' => now(), 'deleted_at' => null],
            ['name' => '大学ノート', 'description' => 'B5サイズ', 'price' => 180, 'stock' => 200, 'is_published' => true, 'category_id' => 4, 'created_at' => now(), 'updated_at' => now(), 'deleted_at' => null],
            ['name' => 'ボールペン', 'description' => '黒・赤・青', 'price' => 120, 'stock' => 300, 'is_published' => true, 'category_id' => 4, 'created_at' => now(), 'updated_at' => now(), 'deleted_at' => null],
            ['name' => 'クリアファイル', 'description' => 'A4サイズ', 'price' => 100, 'stock' => 150, 'is_published' => true, 'category_id' => 4, 'created_at' => now(), 'updated_at' => now(), 'deleted_at' => null],
            ['name' => 'マーカーペン', 'description' => '蛍光5色セット', 'price' => 580, 'stock' => 80, 'is_published' => true, 'category_id' => 4, 'created_at' => now(), 'updated_at' => now(), 'deleted_at' => null],
            ['name' => 'ホッチキス', 'description' => '30枚とじ', 'price' => 680, 'stock' => 70, 'is_published' => true, 'category_id' => 4, 'created_at' => now(), 'updated_at' => now(), 'deleted_at' => null],
            ['name' => '付箋', 'description' => '5色セット', 'price' => 320, 'stock' => 150, 'is_published' => true, 'category_id' => 4, 'created_at' => now(), 'updated_at' => now(), 'deleted_at' => null],
            ['name' => 'クリップ', 'description' => '50個入り', 'price' => 200, 'stock' => 200, 'is_published' => true, 'category_id' => 4, 'created_at' => now(), 'updated_at' => now(), 'deleted_at' => null],
            ['name' => '消しゴム', 'description' => 'プラスチック製', 'price' => 80, 'stock' => 500, 'is_published' => true, 'category_id' => 4, 'created_at' => now(), 'updated_at' => now(), 'deleted_at' => null],
        ]);
    }
}

💡 このデータの特徴

  • 40件のデータpaginate(10)で4ページ目まで表示
  • 家電15件、食品10件、衣類10件、書籍10件 → カテゴリー検索でも複数ページになる
  • 1件非公開(is_published = false) → フィルタリング動作確認
  • 1件ソフトデリート(deleted_atあり) → 通常クエリでは除外される
  • appends()の動作確認: カテゴリー検索(例:家電15件)でも2ページ目に移動でき、検索条件が保持されることを確認できる

🚀 シーダー実行コマンド

# データベースをリセットして再シード
php artisan migrate:fresh --seed

# または ProductSeeder のみ実行
php artisan db:seed --class=ProductSeeder

⚠️ 注意

migrate:fresh全データが削除されます。
本番環境では絶対に実行しないでください!

なぜページネーションが必要なのか?

❌ 全件取得の問題点

  • 商品が1000件あると、1000件全てをデータベースから取得
  • メモリを大量に消費
  • ページの表示が遅くなる
  • ユーザーが1000件全てを見ることはほぼない

✅ ページネーションのメリット

  • 必要なデータだけを取得(例: 10件ずつ)
  • メモリ使用量が少ない
  • ページ表示が高速
  • ユーザーが見やすい

ページネーションの基本実装

まずは、最もシンプルなページネーションを実装してみましょう。
get()paginate() に変えるだけです。

ステップ1: ルーティングの設定

routes/web.php

use App\Http\Controllers\PaginationController;

Route::get('/pagination', [PaginationController::class, 'index']);

ステップ2: コントローラーの作成

ページネーション機能を持つコントローラーを作成します。

php artisan make:controller PaginationController

app/Http/Controllers/PaginationController.php を以下のように編集します。

<?php

namespace App\Http\Controllers;

use App\Models\Product;
use Illuminate\Http\Request;

class PaginationController extends Controller
{
    public function index()
    {
        // 1ページあたり10件でページネーション
        // with('category')でN+1問題を回避(model.htmlで学習済み)
        $products = Product::with('category')
            ->paginate(10);

        return view('pagination.index', compact('products'));
    }
}

💡 paginate()メソッド

paginate(10) は1ページあたり10件のデータを取得します。
自動的に現在のページ番号を検出し、適切なデータを取得してくれます。
get() との違いは、ページネーション用のメタ情報も一緒に返してくれる点です。

💡 N+1問題の復習

with('category') を使うことで、N+1問題を回避しています。
model.htmlで学習した通り、Eager Loadingにより以下のように改善されます:

  • ❌ 使わない場合: 1 + N回のクエリ(N件の商品があれば N+1 回)
  • ✅ 使った場合: 2回のクエリのみ(商品取得1回 + カテゴリー取得1回)

ステップ3: ビューファイルの作成

resources/views/pagination/index.blade.php を作成します。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>商品一覧(ページネーション)</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <style>
        body { font-family: sans-serif; padding: 20px; }
        table { width: 100%; border-collapse: collapse; margin: 20px 0; }
        th, td { border: 1px solid #ddd; padding: 12px; text-align: left; }
        th { background-color: #4CAF50; color: white; }
        .pagination { display: flex; gap: 10px; margin-top: 20px; }
        .pagination a, .pagination span {
            padding: 8px 12px;
            border: 1px solid #ddd;
            text-decoration: none;
            color: #333;
        }
        .pagination .active { background-color: #4CAF50; color: white; }
    </style>
</head>
<body>
    <h1>商品一覧(ページネーション)</h1>

    <table>
        <thead>
            <tr>
                <th>ID</th>
                <th>商品名</th>
                <th>カテゴリー</th>
                <th>価格</th>
                <th>在庫</th>
            </tr>
        </thead>
        <tbody>
            @foreach($products as $product)
            <tr>
                <td>{{ $product->id }}</td>
                <td>{{ $product->name }}</td>
                <td>{{ $product->category->name }}</td>
                <td>¥{{ number_format($product->price) }}</td>
                <td>{{ $product->stock }}</td>
            </tr>
            @endforeach
        </tbody>
    </table>

    {{-- ページネーションリンク --}}
    {{ $products->links() }}
</body>
</html>

💡 重要なポイント

{{ $products->links() }} この1行だけで、ページネーションのリンクが自動生成されます。
前へ・次へボタン、ページ番号リンクなど、すべてLaravelが作ってくれます。

💡 ページネーションリンクの日本語化

デフォルトでは "Previous", "Next" などの英語表記になります。
config/app.php'locale' => 'ja' に設定すると日本語化されます。

'locale' => 'ja',  // 'en' から 'ja' に変更

laravel-lang/common パッケージがインストール済みの場合のみ有効です。

ステップ4: 確認

ブラウザで /pagination にアクセスして確認します。

商品が10件ずつ表示され、下部にページ番号のリンクが表示されているはずです。

✅ 基本的なページネーション機能が完成しました!

クエリパラメータを使った動的な設定

ページネーションでは、ページ番号や表示件数などをクエリパラメータで渡します。

💡 クエリパラメータとは?

URLの?以降に付けるパラメータのことです。
例: /pagination?per_page=20&page=2

  • ? の後にパラメータを記述
  • キー=値 の形式
  • • 複数のパラメータは & で繋ぐ
  • page はLaravelが自動で処理

クエリパラメータの取得

// コントローラーでクエリパラメータを取得
$perPage = $request->input('per_page', 10);  // デフォルト: 10

// URLが /pagination?per_page=20 の場合 → $perPage は 20
// URLが /pagination の場合 → $perPage は 10(デフォルト値)

✅ クエリパラメータを使うメリット

  • • URLに設定が表示される → ブックマーク可能
  • • URLをシェアできる(「20件表示のページを見て!」)
  • • ブラウザの戻るボタンが使える
  • • サーバー側で状態を保持しなくていい

表示件数を変更できるようにする

ユーザーが表示件数を選択できるように、プルダウンで10件/20件/50件を切り替える機能を実装します。

コントローラーの更新

app/Http/Controllers/PaginationController.php

<?php

namespace App\Http\Controllers;

use App\Models\Product;
use Illuminate\Http\Request;

class PaginationController extends Controller
{
    public function index(Request $request)
    {
        // バリデーション:Laravelの標準的な方法
        $validated = $request->validate([
            'per_page' => 'nullable|integer|in:10,20,50'
        ]);

        // バリデーション済みの値を取得(デフォルト: 10件)
        $perPage = $validated['per_page'] ?? 10;

        // ページネーション
        $products = Product::with('category')
            ->paginate($perPage);

        return view('pagination.index', compact('products', 'perPage'));
    }
}

💡 バリデーションの解説

validate()Laravelの標準的なバリデーション方法です。

バリデーションルール:

  • nullable → パラメータが無くてもOK
  • integer → 整数のみ許可
  • in:10,20,50 → 10, 20, 50のいずれかのみ許可(ホワイトリスト)

⚠️ なぜバリデーションが必要?(セキュリティ)

❌ バリデーションなし

// ユーザーが悪意を持って...
?per_page=999999999

// 結果
→ 9億件取得を試みる
→ メモリ不足
→ サーバーダウン 💥

✅ バリデーションあり

// ユーザーが悪意を持って...
?per_page=999999999

// 結果
→ バリデーションエラー
→ 10件にフォールバック
→ サーバー安全 ✨

本番環境では必須のセキュリティ対策です。
中〜大規模プロジェクトでは、このようなバリデーションがないとセキュリティ監査で指摘されます。

ビューの更新

表示件数選択プルダウンを追加します。

<h1>商品一覧(ページネーション)</h1>

{{-- 表示件数選択 --}}
<form method="GET" action="/pagination" style="margin-bottom: 20px;">
    <label>表示件数: </label>
    <select name="per_page" onchange="this.form.submit()">
        <option value="10" {{ $perPage == 10 ? 'selected' : '' }}>10件</option>
        <option value="20" {{ $perPage == 20 ? 'selected' : '' }}>20件</option>
        <option value="50" {{ $perPage == 50 ? 'selected' : '' }}>50件</option>
    </select>
</form>

<p>全{{ $products->total() }}件中 {{ $products->firstItem() }}〜{{ $products->lastItem() }}件を表示</p>

<table>
    {{-- テーブル内容 --}}
</table>

{{-- ページネーションリンク(per_pageを保持) --}}
{{ $products->appends(['per_page' => $perPage])->links() }}

💡 重要なポイント

  • onchange="this.form.submit()" - プルダウン変更時に自動送信
  • {{ $perPage == 10 ? 'selected' : '' }} - 選択状態を保持
  • appends(['per_page' => $perPage]) - ページ移動時も件数を保持
  • $products->total() - 総件数を表示
  • $products->firstItem() - 現在ページの最初のアイテム番号
  • $products->lastItem() - 現在ページの最後のアイテム番号

実際の動作

URLの変化

  • 10件選択時: /pagination?per_page=10
  • 20件選択時: /pagination?per_page=20
  • 2ページ目に移動: /pagination?per_page=20&page=2

→ クエリパラメータで表示件数とページ番号の両方を保持

✅ ユーザーが表示件数を選択できるようになりました!

ソート順を追加する

価格の高い順、安い順などでソートする機能も追加してみましょう。

コントローラーの更新

public function index(Request $request)
{
    // バリデーション
    $validated = $request->validate([
        'per_page' => 'nullable|integer|in:10,20,50',
        'sort_by' => 'nullable|string|in:created_at,price,name,stock',
        'sort_order' => 'nullable|string|in:asc,desc'
    ]);

    // バリデーション済みの値を取得(デフォルト値あり)
    $perPage = $validated['per_page'] ?? 10;
    $sortBy = $validated['sort_by'] ?? 'created_at';
    $sortOrder = $validated['sort_order'] ?? 'desc';

    // ページネーション
    $products = Product::with('category')
        ->orderBy($sortBy, $sortOrder)
        ->paginate($perPage);

    return view('pagination.index', compact('products', 'perPage', 'sortBy', 'sortOrder'));
}

✅ 複数パラメータのバリデーション

複数のパラメータを一度にバリデーションできます。
すべてのユーザー入力をホワイトリストで検証することで、安全なアプリケーションになります。

ビューの更新

{{-- ソート選択 --}}
<form method="GET" action="/pagination" style="margin-bottom: 20px;">
    <input type="hidden" name="per_page" value="{{ $perPage }}">

    <label>並び順: </label>
    <select name="sort_by" onchange="this.form.submit()">
        <option value="created_at" {{ $sortBy == 'created_at' ? 'selected' : '' }}>登録日</option>
        <option value="price" {{ $sortBy == 'price' ? 'selected' : '' }}>価格</option>
        <option value="name" {{ $sortBy == 'name' ? 'selected' : '' }}>商品名</option>
        <option value="stock" {{ $sortBy == 'stock' ? 'selected' : '' }}>在庫数</option>
    </select>

    <select name="sort_order" onchange="this.form.submit()">
        <option value="asc" {{ $sortOrder == 'asc' ? 'selected' : '' }}>昇順</option>
        <option value="desc" {{ $sortOrder == 'desc' ? 'selected' : '' }}>降順</option>
    </select>
</form>

{{-- ページネーションリンク(全てのパラメータを保持) --}}
{{ $products->appends([
    'per_page' => $perPage,
    'sort_by' => $sortBy,
    'sort_order' => $sortOrder
])->links() }}

💡 複数パラメータの保持

appends() に配列で複数のパラメータを渡すことで、
ページ移動時にすべての設定(表示件数・ソート順)を保持できます。

✅ ソート機能も追加できました!

まとめ

ページネーション機能の実装を学びました!

学んだこと

  • paginate(10) で簡単にページネーション実装
  • {{ $products->links() }} でページ番号リンクを自動生成
  • with('category') でN+1問題を回避(モデルで学習済み)
  • ✅ クエリパラメータで表示件数を動的に変更
  • validate() + inルールでセキュリティ対策(ホワイトリスト検証)
  • orderBy() でソート順をカスタマイズ
  • appends() で複数パラメータを保持
  • vendor:publishでページネーションビューをカスタマイズ(Tailwind対応)

次のステップ

ページネーションの基本と応用を習得しました。
次は「画像アップロード」タブで、商品画像のアップロード機能を学習しましょう!

✅ ページネーション学習完了です!