【第四弾】Laravel入門資料
- Windows環境構築
- Mac環境構築
- 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→ パラメータが無くてもOKinteger→ 整数のみ許可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対応)
次のステップ
ページネーションの基本と応用を習得しました。
次は「画像アップロード」タブで、商品画像のアップロード機能を学習しましょう!
✅ ページネーション学習完了です!