SpringBoot 　管理者ページの作成 MVCモデル設定の一連の流れ

やりたいことのイメージ図
![スクリーンショット 2024-02-22 17.17.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3570384/73b561ec-5b66-69fa-02de-969fd2a61180.png)



## テーブルの作成・データの追加
![スクリーンショット 2024-02-22 17.21.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3570384/debd29c9-2299-8c9c-37fd-f4f548498102.png)

![スクリーンショット 2024-02-22 10.45.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3570384/182ef70e-b94b-5cbb-c292-8072f93b6273.png)
`src/main/resources`直下に**2つのSQL文**を作成。ファイルが2つに分かれている理由の1つが、ファイルを分けることで、実行順序を明示し、テーブルが適切に作成された後にレコードが追加されるようにするため。
この2つのSQL文はアプリが立ち上がるたびに自動で処理されるため、各SQL文でエラー回避の工夫が必要。
役割は以下の通り。
**1.`schema.sql`**
テーブルを作成する。
```sql
CREATE TABLE IF NOT EXISTS houses (
    id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(50) NOT NULL,  
    image_name VARCHAR(255),
    description VARCHAR(255) NOT NULL,
    price INT NOT NULL,
    capacity INT NOT NULL,
    postal_code VARCHAR(50) NOT NULL,
    address VARCHAR(255) NOT NULL,
    phone_number VARCHAR(50) NOT NULL,
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
```
ポイント：`CREATE TABLE IF NOT EXISTS　テーブル名`で「もしその名前のテーブルが存在しなければ」という条件をつけている。これは作成済みのテーブルを再度作成しようとしたときに発生するエラーを防ぐ。

**テーブルを作成した後に必須の作業=モデルの作成**
モデルはデータベースの中に作成したテーブルごとに作成しなければいけないので、モデルをこのSQLのテーブルと対応させる作業がいる！
[モデルの設定方法](https://qiita.com/ki_takada/items/461746034bcdf2f56219#%E3%83%A2%E3%83%87%E3%83%ABmodel%E5%87%A6%E7%90%86%E6%8B%85%E5%BD%93)

**2.`data.sql`**
レコードの追加などに使う。
```sql:data.sql
-- housesテーブル
INSERT IGNORE INTO houses (id, name, image_name, description, price, capacity, postal_code, address, phone_number) VALUES (1, 'SAMURAIの宿', 'house01.jpg', '最寄り駅から徒歩10分。自然豊かで閑静な場所にあります。長期滞在も可能です。', 6000, 2, '073-0145', '北海道砂川市西五条南X-XX-XX', '012-345-678');
INSERT IGNORE INTO houses (id, name, image_name, description, price, capacity, postal_code, address, phone_number) VALUES (2, 'ペンション SAMURAI', 'house02.jpg', '最寄り駅から徒歩10分。自然豊かで閑静な場所にあります。長期滞在も可能です。', 7000, 3, '030-0945', '青森県青森市桜川X-XX-XX', '012-345-678');
INSERT IGNORE INTO houses (id, name, image_name, description, price, capacity, postal_code, address, phone_number) VALUES (3, 'SAMURAI荘', 'house03.jpg', '最寄り駅から徒歩10分。自然豊かで閑静な場所にあります。長期滞在も可能です。', 8000, 4, '029-5618', '岩手県和賀郡西和賀町沢内両沢X-XX-XX', '012-345-678');
```
ポイント：`INSERT IGNORE INTO テーブル名`で重複や制約違反が発生した場合でもエラーが発生せず、レコードの追加をスキップする。もし、重複や制約違反が発生しない場合は通常どおりレコードを挿入。
# モデルの作成
別記事に記載
[モデルの設定方法](https://qiita.com/ki_takada/items/461746034bcdf2f56219#%E3%83%A2%E3%83%87%E3%83%ABmodel%E5%87%A6%E7%90%86%E6%8B%85%E5%BD%93)

***
ここまでで、データベースに"house"という名前の1つのテーブルが完成しています。
![スクリーンショット 2024-02-22 12.02.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3570384/e348f88a-8d10-dbcf-c9cf-8c5bfa61dc9c.png)


***
## コントローラの作成
管理者ページのルートパスの基準値を設定します。
![スクリーンショット 2024-02-22 17.24.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3570384/78ec5734-a8ab-e88e-949f-2e2c1322bbaa.png)

下記の場所へ、`AdminHouseController.java`という名前でコントローラ設定ファイルを作成。

![スクリーンショット 2024-02-22 17.29.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3570384/f039569c-f56e-6924-5994-1b9d15624d50.png)`

ファイルの内容↓
```Java:管理者ページのコントローラ設定
package com.example.samuraitravel.controller;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import com.example.samuraitravel.entity.House;
import com.example.samuraitravel.repository.HouseRepository;

@Controller // コントローラの宣言
@RequestMapping("/admin/houses") //このコントローラが"/admin/houses" パス以下のリクエストを処理する
public class AdminHouseController { //クラス定義
    private final HouseRepository houseRepository; //依存先のオブジェクトをfinalで宣言        
    @Autowired //コンストラクタインジェクションで依存性注入
    public AdminHouseController(HouseRepository houseRepository) { 
        this.houseRepository = houseRepository;           
    }	
    
    @GetMapping //index()でhouseRepositoryのデータ取得
    public String index(Model model) {
        List<House> houses = houseRepository.findAll();       
        //ビューに"house"という名前でhouseデータを渡す
        model.addAttribute("houses", houses);             
        
        return "admin/houses/index";
    }  
}
```
ポイント：
1. クラスに`@RequestMapping`アノテーションをつけ、ルートパスの基準値を設定する
1. コンストラクタで依存性の注入（DI）を行う（コンストラクタインジェクション）
1. `HouseRepository`インターフェースの`findAll()`メソッドですべての民宿データを取得する
1. `Model`クラスを使ってビューにデータを渡す。
コントローラからビューにデータを渡す場合、`Model`クラスを使う。

```Java:例
@GetMapping
public String index(Model model) {
    List<House> houses = houseRepository.findAll();       
        
    model.addAttribute("var", houses);             
        
    return "admin/houses/index";
}
```
この例では、
1. index()メソッド内ではHouseRepositoryインターフェースのfindAll()メソッドを使い、すべての民宿データを取得
1. メソッドにModel型の引数を指定する
1. メソッド内でaddAttribute()メソッドを使い、以下の引数を渡す
第1引数：ビュー側から参照する変数名
第2引数：ビューに渡すデータ

admin/houses/index.htmlファイル内でvarという変数を使うことで、コントローラから渡されたhousesというデータの中身を参照できる。


`HouseRepository`インターフェースとは→[リポジトリの設定](`HouseRepository`インターフェース)


## ビューの作成
![スクリーンショット 2024-02-22 18.29.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3570384/bb306e85-8cd8-18cc-b73a-5edf49fe8a71.png)

* ビューファイルはsrc/main/resources/templatesフォルダ以下に配置する。理由はデフォルトでThymeleafのビューリゾルバーによって認識されるから。

##### indexファイルの作成・編集
以下のように`admin`フォルダ新規作成、`houses`フォルダ新規作成、そして`index.html`ファイルを新規作成する。
![スクリーンショット 2024-02-22 18.29.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3570384/f4ccffce-d771-5123-4e2c-368549ddde86.png)
ファイルの作成は以下の通り。
```html:index.html
+<!DOCTYPE html>
+<html xmlns:th="https://www.thymeleaf.org">
+    <head>
+        <meta charset="UTF-8">
+        <meta name="viewport" content="width=device-width, initial-scale=1">    
+             
+        <!-- Bootstrap -->
+        <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous">
+        
+        <!-- Google Fonts -->
+        <link rel="preconnect" href="https://fonts.googleapis.com">
+        <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+        <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;500&display=swap" rel="stylesheet">   
+        
+        <link th:href="@{/css/style.css}" rel="stylesheet" >
+        
+        <title>民宿一覧</title>       
+    </head>
+    <body>
+        <div class="samuraitravel-wrapper">
+            <!-- ヘッダー -->
+            <nav class="navbar navbar-expand-md navbar-light bg-white shadow-sm samuraitravel-navbar">
+                <div class="container samuraitravel-container">
+                    <a class="navbar-brand" th:href="@{/}">                        
+                        <img class="samuraitravel-logo me-1" th:src="@{/images/logo.png}" alt="SAMURAI Travel">                        
+                    </a>                                                                    
+                </div>
+            </nav>
+            
+            <main>
+                <div class="container pt-4 pb-5 samuraitravel-container">
+                    <div class="row justify-content-center">
+                        <div class="col-xxl-9 col-xl-10 col-lg-11">
+                            
+                            <h1 class="mb-4 text-center">民宿一覧</h1>   
+                            
+                            <div class="d-flex justify-content-end">                                
+                                <a href="#" class="btn text-white shadow-sm mb-3 samuraitravel-btn">登録</a>
+                            </div>                                                          
+                            
+                            <table class="table">
+                                <thead>
+                                    <tr>
+                                        <th scope="col">ID</th>
+                                        <th scope="col">民宿名</th>
+                                        <th scope="col">郵便番号</th>
+                                        <th scope="col">住所</th>
+                                        <th scope="col">電話番号</th>
+                                        <th scope="col"></th>                                        
+                                    </tr>
+                                </thead>   
+                                <tbody>                                                     
+                                    <tr th:each="house : ${houses}">
+                                        <td th:text="${house.getId()}"></td>
+                                        <td th:text="${house.getName()}"></td>
+                                        <td th:text="${house.getPostalCode()}"></td>
+                                        <td th:text="${house.getAddress()}"></td>
+                                        <td th:text="${house.getPhoneNumber()}"></td>
+                                        <td><a href="#">詳細</a></td>                                                                                
+                                    </tr>                                      
+                                </tbody>
+                            </table>                                       
+                        </div>
+                    </div>
+                </div>              
+            </main>
+            
+            <!-- フッター -->
+            <footer>
+                <div class="d-flex justify-content-center align-items-center h-100">
+                    <p class="text-center text-muted small mb-0">&copy; SAMURAI Travel All rights reserved.</p>      
+                </div>                 
+            </footer>
+        </div>    
+        
+        <!-- Bootstrap -->
+        <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I4" crossorigin="anonymous"></script>        
+    </body>
+</html>
```
ポイント：
1. CDNでBootstrapを導入する
1. CDNでGoogle Fontsを導入する
1. aタグのth:href属性でリンク先のルートパスを指定する
1. imgタグのth:src属性で表示するロゴ画像のルートパスを指定する
1. th:each属性で繰り返し処理を行う
##### CSSファイルの作成・編集
Spring Bootでは、静的なファイル（CSS、JavaScript、画像など）は通常、src/main/resources/static フォルダ内に配置する。

 ![スクリーンショット 2024-02-22 19.41.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3570384/10f4c989-3cbb-b1e3-3101-959b98cfc50c.png)
th:text属性でtd要素のテキストを表示する
```css
+html {
+  font-size: 14px;
+}
+
+body {
+  font-family: 'Noto Sans JP', sans-serif;  
+}
+
+h1 {
+  font-size: 1.5rem;
+}
+
+a {
+  color: #5196a6;
+  text-decoration: none !important;
+}
+
+a:hover {
+    color: #407784;
+}
+
+footer {
+  width: 100%;
+  height: 60px;
+  position: absolute;
+  bottom: 0;    
+  background-color: #f2f0eb;
+}
+
+.samuraitravel-wrapper {
+  min-height: 100vh;
+  position: relative;
+  padding-bottom: 60px;
+  box-sizing: border-box;
+}
+
+.samuraitravel-navbar {
+  min-height: 60px;
+}
+
+.samuraitravel-logo {
+  height: 30px;  
+}
+
+.samuraitravel-btn {
+  background-color: #5196a6 !important;   
+  transition: 0.1s;
+}
+
+.samuraitravel-btn:hover {
+  opacity: 0.8;   
+}
+
+@media screen and (min-width: 1400px) {
+  .samuraitravel-container {
+    max-width: 1200px;
+  }
+}
```
***
ここまでの作業まとめ
![スクリーンショット 2024-02-22 19.57.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3570384/d4231791-783c-8e18-78c2-af3b30527737.png)
この工程を経て、アプリを起動後、`http://localhost:8080/admin/houses`にアクセスすると下記のようなページが表示されるはず。
![スクリーンショット 2024-02-22 20.00.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3570384/8c4f2f99-b60f-63ad-a705-a797be8824a4.png)