【Bootstrap】【Thymeleaf】モーダルを別htmlに切り出す

### 概要
* Thymeleafのfragment属性を使ってモーダルを子要素として切り出す。
* モーダルはBootstrap4を使用して作成する。

### 完成イメージ
宿泊プラン一覧
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2521820/92e007c0-ae7a-0996-b358-eee4d7d0b3c0.png)
↓
「空室確認」を押すとその宿泊プランの空室状況が表示される
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2521820/39db021d-ae03-beab-242c-f5f178be1cb3.png)

### 環境
Spring Boot
maven
Thymleaf
Postgresql

### プロジェクトの構成
```
sample-qiita
│ pom.xml
└─src
    └─main
        ├─java
        │  └─com
        │      └─example
        │          └─domain
        │          │        Plan.java
        │          │        Room.java
        │          │        Calender.java
        │          └─controller
        │          │        PlanListController.java
        │          └─service
        │          │        PlanListService.java
        │          │        VacancyRoomService.java
        │          └─repository 
        │                  PlanMstRepository.java
        │                  CalenderRepository.java
        └─resources
            │  application.yml
            ├─db
            │  └─migration
            │       V1__createTable.sql     
            │       V2__insertData.sql     
            ├─static
            │      └─css
            │      │   common.css
            │      └─img
            │        └─rooms
            │              room1.jpg
            │              room2.jpg
            │              room3.jpg    
            └─templates
                    plans_list.html
                    modal_vacancy_room.html
```
### 1.サーバー側
#### pom.xml
<details><summary>pom.xml</summary>

```pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.6.7</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.example</groupId>
	<artifactId>sample-qiita</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>sample-qiita</name>
	<description>Demo project for Spring Boot</description>
	<properties>
		<java.version>17</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-jdbc</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-thymeleaf</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-validation</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.flywaydb</groupId>
			<artifactId>flyway-core</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
			<scope>runtime</scope>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.postgresql</groupId>
			<artifactId>postgresql</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
		    <groupId>org.projectlombok</groupId>
		    <artifactId>lombok</artifactId>
		    <version>1.18.20</version>
		    <scope>provided</scope>
 		</dependency>
	</dependencies>
	<build>
		<plugins>
			<plugin>
			<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>
</project>
```
</details>

#### db
<details><summary>V1__createTable.sql</summary>

```V1__createTable.sql
CREATE TABLE PLAN_MST (
 plan_id serial  
 ,plan_name varchar(100) NOT NULL
 , plan_contents varchar(1000) NOT NULL 
 , meal_type char(1) NOT NULL
 , plan_fee int NOT NULL
 , room_id int NOT NULL
 , image_photo varchar(100) NOT NULL
 , create_user int NOT NULL
 , create_date date NOT NULL
 , update_user int NOT NULL 
 , update_date date NOT NULL 
 , del_flg char(1) NOT NULL 
 , PRIMARY KEY (plan_id)
) ;

CREATE TABLE ROOM_MST (
 room_id serial   
 ,room_type char(1) NOT NULL
 ,room_rank char(1) NOT NULL
 ,smoking_type char(1) NOT NULL
 ,bathroom_type char(1) NOT NULL
 , guest_capacity int NOT NULL
 , create_user int NOT NULL
 , create_date date NOT NULL
 , update_user int NOT NULL 
 , update_date date NOT NULL 
 , del_flg char(1) NOT NULL 
 , PRIMARY KEY (room_id)
) ;

CREATE TABLE CALENDER_TABLE (
 date date  
 ,date_type char(1) NOT NULL
 ,room_id int NOT NULL 
 ,total_rooms int NOT NULL
 ,vacancy_rooms int NOT NULL
 , room_fee int NOT NULL
 , create_user int NOT NULL
 , create_date date NOT NULL
 , update_user int NOT NULL 
 , update_date date NOT NULL 
 , del_flg char(1) NOT NULL 
 , PRIMARY KEY (date, room_id)
) ;
```

</details>
<details><summary>V2__insertData.sql</summary>

```V2__insertData.sql
/*プラン*/
INSERT INTO PLAN_MST (plan_id,plan_name,plan_contents,meal_type,plan_fee,room_id,image_photo,create_user,create_date,update_user,update_date,del_flg) VALUES (1,'【スウィートルーム】禁煙','７０平米以上のスイートルームでゆったりとお寛ぎください。','3',30000,'1','room1.jpg',0,'2023/05/01',0,'2023/05/01','0');
INSERT INTO PLAN_MST (plan_id,plan_name,plan_contents,meal_type,plan_fee,room_id,image_photo,create_user,create_date,update_user,update_date,del_flg) VALUES (2,'【スウィートルーム】喫煙','７０平米以上のスイートルームでゆったりとお寛ぎください。','3',30000,'2','room2.jpg',0,'2023/05/01',0,'2023/05/01','0');
INSERT INTO PLAN_MST (plan_id,plan_name,plan_contents,meal_type,plan_fee,room_id,image_photo,create_user,create_date,update_user,update_date,del_flg) VALUES (3,'【スタンダードプラン】洋室　素泊まり','ビジネス・観光・一人旅にご利用いただいているベーシックな宿泊プランです。','0',0,'3','room3.jpg',0,'2023/05/01',0,'2023/05/01','0');
INSERT INTO PLAN_MST (plan_id,plan_name,plan_contents,meal_type,plan_fee,room_id,image_photo,create_user,create_date,update_user,update_date,del_flg) VALUES (4,'【スタンダードプラン】洋室　朝食付','ご夫婦・ご家族・一人旅の観光旅行におすすめのプランです。','1',2000,'3','room3.jpg',0,'2023/05/01',0,'2023/05/01','0');

/*部屋*/
INSERT INTO ROOM_MST (room_id,room_type,room_rank,smoking_type,bathroom_type,guest_capacity,create_user,create_date,update_user,update_date,del_flg) VALUES (1,'1','S','0','0',2,0,'2023/05/01',0,'2023/05/01','0');
INSERT INTO ROOM_MST (room_id,room_type,room_rank,smoking_type,bathroom_type,guest_capacity,create_user,create_date,update_user,update_date,del_flg) VALUES (2,'1','S','1','0',2,0,'2023/05/01',0,'2023/05/01','0');
INSERT INTO ROOM_MST (room_id,room_type,room_rank,smoking_type,bathroom_type,guest_capacity,create_user,create_date,update_user,update_date,del_flg) VALUES (3,'0','A','0','0',2,0,'2023/05/01',0,'2023/05/01','0');

/*カレンダー*/
INSERT INTO CALENDER_TABLE (date,date_type,room_id,total_rooms,vacancy_rooms,room_fee,create_user,create_date,update_user,update_date,del_flg) VALUES ('2023/05/01','0',1,10,5,20000,0,'2023/05/01',0,'2023/05/01','0');
INSERT INTO CALENDER_TABLE (date,date_type,room_id,total_rooms,vacancy_rooms,room_fee,create_user,create_date,update_user,update_date,del_flg) VALUES ('2023/05/02','0',1,10,5,20000,0,'2023/05/01',0,'2023/05/01','0');
INSERT INTO CALENDER_TABLE (date,date_type,room_id,total_rooms,vacancy_rooms,room_fee,create_user,create_date,update_user,update_date,del_flg) VALUES ('2023/05/03','1',1,10,5,25000,0,'2023/05/01',0,'2023/05/01','0');
INSERT INTO CALENDER_TABLE (date,date_type,room_id,total_rooms,vacancy_rooms,room_fee,create_user,create_date,update_user,update_date,del_flg) VALUES ('2023/05/04','1',1,10,5,25000,0,'2023/05/01',0,'2023/05/01','0');
INSERT INTO CALENDER_TABLE (date,date_type,room_id,total_rooms,vacancy_rooms,room_fee,create_user,create_date,update_user,update_date,del_flg) VALUES ('2023/05/05','1',1,10,5,25000,0,'2023/05/01',0,'2023/05/01','0');
INSERT INTO CALENDER_TABLE (date,date_type,room_id,total_rooms,vacancy_rooms,room_fee,create_user,create_date,update_user,update_date,del_flg) VALUES ('2023/05/06','1',1,10,10,25000,0,'2023/05/01',0,'2023/05/01','0');
…省略

INSERT INTO CALENDER_TABLE (date,date_type,room_id,total_rooms,vacancy_rooms,room_fee,create_user,create_date,update_user,update_date,del_flg) VALUES ('2023/05/01','0',2,10,5,20000,0,'2023/05/01',0,'2023/05/01','0');
INSERT INTO CALENDER_TABLE (date,date_type,room_id,total_rooms,vacancy_rooms,room_fee,create_user,create_date,update_user,update_date,del_flg) VALUES ('2023/05/02','0',2,10,5,20000,0,'2023/05/01',0,'2023/05/01','0');
INSERT INTO CALENDER_TABLE (date,date_type,room_id,total_rooms,vacancy_rooms,room_fee,create_user,create_date,update_user,update_date,del_flg) VALUES ('2023/05/03','1',2,10,5,25000,0,'2023/05/01',0,'2023/05/01','0');
INSERT INTO CALENDER_TABLE (date,date_type,room_id,total_rooms,vacancy_rooms,room_fee,create_user,create_date,update_user,update_date,del_flg) VALUES ('2023/05/04','1',2,10,5,25000,0,'2023/05/01',0,'2023/05/01','0');
INSERT INTO CALENDER_TABLE (date,date_type,room_id,total_rooms,vacancy_rooms,room_fee,create_user,create_date,update_user,update_date,del_flg) VALUES ('2023/05/05','1',2,10,5,25000,0,'2023/05/01',0,'2023/05/01','0');
INSERT INTO CALENDER_TABLE (date,date_type,room_id,total_rooms,vacancy_rooms,room_fee,create_user,create_date,update_user,update_date,del_flg) VALUES ('2023/05/06','1',2,10,10,25000,0,'2023/05/01',0,'2023/05/01','0');
…省略

INSERT INTO CALENDER_TABLE (date,date_type,room_id,total_rooms,vacancy_rooms,room_fee,create_user,create_date,update_user,update_date,del_flg) VALUES ('2023/05/01','0',3,10,5,15000,0,'2023/05/01',0,'2023/05/01','0');
INSERT INTO CALENDER_TABLE (date,date_type,room_id,total_rooms,vacancy_rooms,room_fee,create_user,create_date,update_user,update_date,del_flg) VALUES ('2023/05/02','0',3,10,5,15000,0,'2023/05/01',0,'2023/05/01','0');
INSERT INTO CALENDER_TABLE (date,date_type,room_id,total_rooms,vacancy_rooms,room_fee,create_user,create_date,update_user,update_date,del_flg) VALUES ('2023/05/03','1',3,10,5,20000,0,'2023/05/01',0,'2023/05/01','0');
INSERT INTO CALENDER_TABLE (date,date_type,room_id,total_rooms,vacancy_rooms,room_fee,create_user,create_date,update_user,update_date,del_flg) VALUES ('2023/05/04','1',3,10,5,20000,0,'2023/05/01',0,'2023/05/01','0');
INSERT INTO CALENDER_TABLE (date,date_type,room_id,total_rooms,vacancy_rooms,room_fee,create_user,create_date,update_user,update_date,del_flg) VALUES ('2023/05/05','1',3,10,5,20000,0,'2023/05/01',0,'2023/05/01','0');
INSERT INTO CALENDER_TABLE (date,date_type,room_id,total_rooms,vacancy_rooms,room_fee,create_user,create_date,update_user,update_date,del_flg) VALUES ('2023/05/06','1',3,10,10,20000,0,'2023/05/01',0,'2023/05/01','0');
…省略
```

</details>

#### domain
<details><summary>Plan.java</summary>

```Plan.java
package com.example.domain;

import lombok.Data;

@Data
public class Plan {
	private Integer planId;
	private String planName;
	private String planContents;
	private String mealType;
	private Integer planFee;
	private Room room;
	private String ImagePhoto;
}
```

</details>
<details><summary>Room.java</summary>

```Room.java
package com.example.domain;

import java.util.List;

import lombok.Data;

@Data
public class Room {
	private Integer roomId;
	private String roomType;
	private String roomRank;
	private String smokingType;
	private String bathroomType;
	private Integer guestCapacity;
	private List<Calender> vacancyRoomCalender;
}
```

</details>
<details><summary>Calender.java</summary>

```Calender.java
package com.example.domain;

import java.util.Date;

import lombok.Data;

@Data
public class Calender {
	private Date date;
	private String dateType;
	private Integer roomId;
	private Integer totalRooms;
	private Integer vacancyRooms;
	private Integer roomFee;
	private Integer accomodationFee;
}

```

</details>

#### repository
<details><summary>PlanMstRepository.java</summary>

```PlanMstRepository.java
package com.example.repository;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.stereotype.Repository;

import com.example.domain.Plan;
import com.example.domain.Room;

@Repository
public class PlanMstRepository {
	@Autowired
	private NamedParameterJdbcTemplate template;	
	
	private final static RowMapper<Plan> PLAN_ROOM_ROW_MAPPER=
			(rs,i) -> {
				Plan plan = new Plan();
				plan.setPlanId(rs.getInt("plan_id"));
				plan.setPlanName(rs.getString("plan_name"));
				plan.setPlanContents(rs.getString("plan_contents"));
				plan.setMealType(rs.getString("meal_type"));
				plan.setPlanFee(rs.getInt("plan_fee"));
				plan.setImagePhoto(rs.getString("image_photo"));
				
				Room room = new Room();
				room.setRoomId(rs.getInt("room_id"));
				room.setRoomType(rs.getString("room_type"));
				room.setRoomRank(rs.getString("room_rank"));
				room.setSmokingType(rs.getString("smoking_type"));
				room.setBathroomType(rs.getString("bathroom_type"));
				room.setGuestCapacity(rs.getInt("guest_capacity"));
				plan.setRoom(room);
				return plan;
			};
			
	/**
	 * プラン全件select
	 * @param planId
	 * @return　plan
	 */
	public List<Plan> selectAllPlan() {
		String sql="SELECT "+
						"p.plan_id, "+
						"p.plan_name, "+
						"p.plan_contents, "+
						"CASE p.meal_type "+
						"WHEN '0' THEN '素泊まり' "+
						"WHEN '1' THEN '朝食付' "+
						"WHEN '2' THEN '夕食付' "+
						"WHEN '3' THEN '朝夕付' "+
						"ELSE '登録なし' "+
						"END as meal_type, "+
						"p.plan_fee, "+
						"p.image_photo, "+
						"r.room_id, "+
						"CASE r.room_type "+
						"WHEN '0' THEN '和室' "+
						"WHEN '1' THEN '洋室' "+
						"ELSE '登録なし' "+
						"END as room_type, "+
						"CASE r.bathroom_type "+
						"WHEN '0' THEN '客室露天風呂付' "+
						"WHEN '1' THEN '客室露天風呂なし' "+
						"ELSE '登録なし' "+
						"END as bathroom_type, "+
						"CASE r.smoking_type "+
						"WHEN '0' THEN '禁煙' "+
						"WHEN '1' THEN '喫煙' "+
						"ELSE '登録なし' "+
						"END as smoking_type, "+
						"CASE r.room_rank "+
						"WHEN 'S' THEN 'スイート' "+
						"WHEN 'A' THEN 'スタンダード' "+
						"ELSE '登録なし' "+
						"END as room_rank, "+
						"r.guest_capacity "+
					"FROM PLAN_MST as p "+
					"JOIN ROOM_MST as r ON p.room_id = r.room_id "+
					"WHERE p.del_flg='0' AND r.del_flg='0' "+
					"ORDER BY p.plan_id";
		return template.query(sql,PLAN_ROOM_ROW_MAPPER);
	}
}

```
</details>

<details><summary>CalenderTableRepository.java</summary>

```CalenderTableRepository.java
package com.example.repository;

import java.time.LocalDate;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
import org.springframework.stereotype.Repository;

import com.example.domain.Calender;

@Repository
public class CalenderTableRepository {
	@Autowired
	private NamedParameterJdbcTemplate template;
	
	private static final RowMapper<Calender> CALENDER_ROW_MAPPER
	= (rs,i) -> {
		Calender calender = new Calender();
		calender.setDate(rs.getDate("date"));
		calender.setRoomId(rs.getInt("room_id"));
		calender.setVacancyRooms(rs.getInt("vacancy_rooms"));
		calender.setAccomodationFee(rs.getInt("accomondation_fee"));
		return calender;
	};
	
	/**
	 * 各日付の空室状況・料金を取得するSELECT文
	 * @param startDate
	 * @param endDate
	 * @param planId
	 * @return　日付・部屋ID・空室数・宿泊料金
	 */
	public List<Calender> selectByPlanId(LocalDate startDate, LocalDate endDate, Integer planId){
		String sql = "SELECT c.date, "
				+ "c.room_id, "
				+ "c.vacancy_rooms, "
				+ "(c.room_fee + p.plan_fee) as accomondation_fee "
				+ "FROM PLAN_MST as p "
				+ "JOIN (SELECT date, room_id, vacancy_rooms, room_fee "
						+ "FROM CALENDER_TABLE WHERE date BETWEEN :startDate AND :endDate  AND del_flg='0') as c "
				+ "ON p.room_id = c.room_id "
				+"WHERE p.plan_id = :planId  AND p.del_flg='0' ORDER BY c.date";
		
		SqlParameterSource param = new MapSqlParameterSource().addValue("startDate", startDate).addValue("endDate", endDate).addValue("planId", planId);
		return template.query(sql,param,CALENDER_ROW_MAPPER);
	}
}
```
</details>

#### service
<details><summary>PlanListService.java</summary>

```PlanListService.java
package com.example.service;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.example.domain.Plan;
import com.example.repository.PlanMstRepository;

@Service
@Transactional
public class PlanListService {
	@Autowired
	PlanMstRepository planMstRepository;
	//プラン全件取得
	public List<Plan> search(){
		return planMstRepository.selectAllPlan();
	}
}
```
</details>
<details><summary>VacancyRoomService.java</summary>

```VacancyRoomService.java
package com.example.service;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.example.domain.Calender;
import com.example.repository.CalenderTableRepository;

@Service
@Transactional
public class VacancyRoomService {
	@Autowired
	CalenderTableRepository calenderTableRepository;
	
	 //1か月単位で空室状況を取得する
	public List<Calender> searchCalender(String month, String planId){
		String strStartOfMonth = month +"-01";
		LocalDate startDate = LocalDate.parse(strStartOfMonth, DateTimeFormatter.ofPattern("yyyy-MM-dd"));
		LocalDate endDate = startDate.withDayOfMonth(startDate.lengthOfMonth());
		List<Calender> vacancyRoomCalender = calenderTableRepository.selectByPlanId(startDate, endDate, Integer.parseInt(planId));
		
		//カレンダー表示するために月初の前の空白を追加
		int beforeBlank= startDate.getDayOfWeek().getValue() - 1;//前月の最終日の曜日
		for(int i =0; i<=beforeBlank; i++) {
			Calender calender = null;
			vacancyRoomCalender.add(0, calender);
		}
		return vacancyRoomCalender;
	}
}
```
</details>

#### controller
<details><summary>PlanListController.java</summary>

```PlanListController.java
package com.example.controller;

import java.util.HashMap;
import java.util.List;
import java.util.Map;


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 org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

import com.example.domain.Calender;
import com.example.service.PlanListService;
import com.example.service.VacancyRoomService;

@Controller
@RequestMapping("/plan")
public class PlanListController {
	
	@Autowired
	private PlanListService planListService;
	
	@Autowired
	private VacancyRoomService vacancyRoomService;
	
	//プラン一覧を表示する
	@RequestMapping("/")
	public String planPage(Model model) {
		model.addAttribute("planList", planListService.search());
		return "plans_list";
	}

	//空室カレンダーを取得
	@GetMapping("/vacancyRoom/search")
	@ResponseBody
	public Map<String, List<Calender>> search(@RequestParam("month") String month,
            @RequestParam("planId") String planId){
		Map <String, List<Calender>> map = new HashMap<>();
		map.put("calenderList", vacancyRoomService.searchCalender(month, planId));
		return map;
	}
}
```
</details>

### 2.画面
#### 親画面
* 「空室確認」ボタンを押すと、初期値として今月の空室状況が表示される。
```plans_list.html
<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org"
				xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.4.2/css/all.css" integrity="sha384-/rXc/GQVaYpyDdyxK+ecHPVYJSN9bmVFBvjA/9eOB+pb3F2w2N6fc5qB9Ew5yIns" crossorigin="anonymous">
<link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/smoothness/jquery-ui.css">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
<link rel="stylesheet" th:href="@{/css/common.css}" />
<title>プラン検索</title>
</head>
<body>
<main>
<!-- プラン一覧 -->
<table class="plan" th:each="plan:${planList}">
	<tr><th colspan="2" th:text="${plan.planName}"></th></tr>
	<tr><td rowspan="3" class="col-5"><img th:src="'/img/rooms/'+${plan.imagePhoto}"></td>
		<td th:text="${plan.mealType}+'&nbsp;&nbsp;'+${plan.room.roomType}+'&nbsp;&nbsp;'+${plan.room.roomRank}+'&nbsp;&nbsp;'+${plan.room.smokingType}+'&nbsp;&nbsp;'+${plan.room.bathroomType}"></td></tr>
		<tr><td th:text="${plan.planContents}"></td></tr>
		<tr><td><button class="btn btn-vacancy-room" type="button" data-toggle="modal" data-target="#vacancy-room-modal" th:attr="data-whatever=${plan.planId}">空室確認</button></td></tr>
</table>

<!-- 空室確認モーダル -->
<div class="modal fade" id="vacancy-room-modal" tabindex="-1" role="dialog" aria-labelledby="basicModal" aria-hidden="true">
    <div class="modal-dialog modal-lg">
        <div class="modal-content">
            <div th:insert="modal_vacancy_room :: fragment-vacancyRoomCalender"></div>
            <div class="modal-footer">
            <button type="button" class="btn btn-secondary btn-dismiss" data-dismiss="modal">閉じる</button>
            </div>
        </div>
    </div>
</div>
</main>
<script>
window.addEventListener('DOMContentLoaded', function(){
$('#vacancy-room-modal').on('show.bs.modal', function (event) {
	  var button = $(event.relatedTarget) //モーダルを呼び出すときに使われたボタンを取得
	  var planId = button.data('whatever') //data-whatever の値を取得
	
	  //monthに今月を設定
	  var today = new Date();
	  if((today.getMonth()+1) <10){
	  var month = today.getFullYear() +'-0'+ (today.getMonth()+1);//1-9日
	  }else{
	  var month = today.getFullYear() +'-'+ (today.getMonth()+1);
	  }
	
	  var modal = $(this)  //モーダルを取得
	  modal.find('.modal-body input#planId').val(planId);//モーダル内のinputタグid="planId"にplanIdの値を渡す
	  modal.find('.modal-body input#month').val(month);//モーダル内のinputタグid="month"にmonthの値を渡す
	});

$('#vacancy-room-modal').on('shown.bs.modal', function(){
	searchVacancyRoom();//選択したプランの今月の空室状況を表示する。
});	
});
</script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.9.2/dist/umd/popper.min.js" integrity="sha384-IQsoLXl5PILFhosVNubq5LC7Qb9DXgDA9i+tQ8Zj3iwWAwPtgFTxbJ8NT4GN1R8p" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous"></script>
</body>
</html>
```
**説明**
```
<button class="btn btn-vacancy-room" type="button" data-toggle="modal" data-target="#vacancy-room-modal" th:attr="data-whatever=${plan.planId}">空室確認</button>
```
* ``data-toggle="modal" data-target="#vacancy-room-modal"``と記述することで、クリックすると、id="vacancy-room-modal"を付与した部分が表示される。
* data-*属性でモーダルに値を渡すことができる。ここでは「data-whatever」を使用している。
* ``th:attr="data-whatever=${plan.planId}"``と記述することでThymeleafでdata-*属性に値を設定することができる。
```
<div th:insert="modal_vacancy_room :: fragment-vacancyRoomCalender"></div>
```
* **th:insert="子画面 :: 子画面内のth:fragmentの値"** 
* この部分にmodal_vacancy_room.htmlのth:fragment="fragment-vacancyRoomCalender"で囲った部分が挿入される。

```
$('#vacancy-room-modal').on('show.bs.modal', function (event) {  …  });

$('#vacancy-room-modal').on('shown.bs.modal', function(){  …  });
```

* ``show.bs.modal``：モーダルが呼び出された時の処理
* ``shown.bs.modal``：モーダルが画面表示された後の処理


#### 子画面（モーダル）
* モーダルを別htmlに作成
* 非同期で空室状況を取得しカレンダー形式で表示している。
```modal_vacancy_room.html
<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>空室確認</title>
</head>
<body>

<div th:fragment="fragment-vacancyRoomCalender">
<div class="modal-body">
<div class="month-form">
<input type="month" name="month" id="month" class="form-control" onchange="searchVacancyRoom()">
</div>
<input type="hidden" name="planId" id="planId">
※1泊のみした場合のおひとり当たりの料金です。
<div id="vacancyRoomCalender"></div>
</div>

<script>
//空室状況を表示する
function searchVacancyRoom(){
	var monthVal =$('#month').val();
	var planIdVal =$('#planId').val();
		$.ajax({
			url : '/plan/vacancyRoom/search',
			type : 'GET',
			data: {
		        month: monthVal,
		        planId: planIdVal,
		    	},
			dataType : 'json'
		}).done(function(data) {
		var vacancyRoomCalender_html ='<table class="vacancy-room"><tr><th class="sunday">日</th><th>月</th><th>火</th><th>水</th><th>木</th><th>金</th><th class="saturday">土</th></tr>';
		$.each(data.calenderList, function(i, calender){
		if(calender != null){
			var date = calender.date;
			var day = date.slice(-2);//カレンダー表示用に日付だけ取り出す
			if(day.slice(0,1)=='0'){
				day = day.slice(-1);//1-9日までは一桁で取り出す
			}
			
			var vacancyRooms = calender.vacancyRooms;//空室数
			if(vacancyRooms == 0){
				var vacancyRooms_html = '<div style="color:red">満室</div>';
				var accomodationFee_html = '';//満室の時は料金を表示しない
			}else{
				var vacancyRooms_html = vacancyRooms+'室';
				var accomodationFee_html = calender.accomodationFee.toLocaleString()+'円';
			}
	
			var today = new Date();
			today.setDate(today.getDate());//今日
		    var yyyy = today.getFullYear();
		    var mm = ("0"+(today.getMonth()+1)).slice(-2);
		    var dd = ("0"+today.getDate()).slice(-2);
		    var todayVal=yyyy+'-'+mm+'-'+dd;//今日の年月日
			if(calender.date<todayVal){
				//昨日以前の空室数、料金は表示しない
				var vacancyRooms_html = '-';
				var accomodationFee_html = '';
			}
				
			if(i%7==0){
			//日曜日
			vacancyRoomCalender_html +='<tr><td class="sunday">'+day+'<br>'+vacancyRooms_html+'<br>'+accomodationFee_html;
			}else if(i%7==6){
			//土曜日
			vacancyRoomCalender_html +='<td class="saturday">'+day+'<br>'+vacancyRooms_html+'<br>'+accomodationFee_html;
			}else{
			//平日
			vacancyRoomCalender_html +='<td>'+day+'<br>'+vacancyRooms_html+'<br>'+accomodationFee_html;
			}
		}else{//前後の空白部分
			if(i%7==0){
			vacancyRoomCalender_html +='<tr><td class="sunday"></td>';
			}else if(i%7==6){
			vacancyRoomCalender_html +='<td class="saturday"></td></tr>';
			}else{
			vacancyRoomCalender_html +='<td></td>';
			}
		}
		});
		vacancyRoomCalender_html +='</table>';
		$('#vacancyRoomCalender').html(vacancyRoomCalender_html);
		});
}
</script>
</div><!-- <div th:fragment="fragment-vacancyRoomCalender>"の最後 -->
</body>
</html>
```
**説明**
* 親画面に挿入したい部分を``<div th:fragment="fragment-vacancyRoomCalender">…</div>``で囲む。

#### css
<details><summary>common.css</summary>

```common.css
table.plan {
  border-collapse: collapse;
  border-spacing: 5px;
  padding: 0;
  width: 80%;
  margin: 1% auto auto auto;
  border-spacing: 10px 10px;
}

table.plan tr{
  border-bottom: solid 2px white;
}

table.plan th{
  position: relative;
  text-align: left;
  background-color: #48d1cc;
  color: white;
  padding: 1% 1% 1% 1%;
}

table.plan td{
  text-align: left;
  background-color: #f0f8ff;
  padding: 1% 1% 1% 1%;
}

.btn-vacancy-room{
	width:100%;
	margin: auto;
	margin-left: auto;
  	margin-right: auto;
	color:black;
	background-color:#ffd700;
	border-color:#ffd700;
}

img{
	width:100%;
	height:100%;
}

table.vacancy-room {
  border-collapse: collapse;
  border-spacing: 5px;
  padding: 0;
  width: 80%;
  margin: 1% auto auto auto;
}

table.vacancy-room tr{
  border: solid 2px #a9a9a9;
}

table.vacancy-room th{
  position: relative;
  text-align: left;
  width: 10%;
  background-color: #90ee90;
  color: white;
  text-align: center;
  padding: 1% 1% 1% 1%;
  border: solid 2px #a9a9a9;
}

table.vacancy-room th.sunday{
  position: relative;
  text-align: left;
  width: 10%;
  background-color: #ff6347;
  color: white;
  text-align: center;
  padding: 1% 1% 1% 1%;
}

table.vacancy-room th.saturday{
  position: relative;
  text-align: left;
  width: 10%;
  background-color: #87cefa;
  color: white;
  text-align: center;
  padding: 1% 1% 1% 1%;
}

table.vacancy-room td{
  text-align: center;
  width: 10%;
  background-color: #ffffff;
  padding: 1% 1% 1% 1%;
  border: solid 2px #a9a9a9;
}

table.vacancy-room td.sunday{
  width: 10%;
  background-color: #ffe4c4;
  padding: 1% 1% 1% 1%;
}
table.vacancy-room td.saturday{
  width: 10%;
  background-color: #f0f8ff;
  padding: 1% 1% 1% 1%;
}

.month-form{
	width: 20%;
}

body {
  width: 100%;
  display: flex;
  flex-flow: column;
  min-height: 100vh;
}
main {
  flex: 1;
}
```
</details>

### 参考
* [【Bootstrap3】モーダルに「data-*」属性でデータを渡す](https://qiita.com/sofpyon/items/1e72426598eed745f031 "タイトル")
* [Modal-Bootstrap4.2- 日本語リファレンス](https://getbootstrap.jp/docs/4.2/components/modal/ "タイトル")
* [Thymeleafでテンプレートを共通化（フラグメント）する方法](https://miruraku.com/java/thymeleaf/fragment/)
* [thymeleafでdata-から始まる属性に値を設定する](http://se-bikou.blogspot.com/2017/08/thymeleafdata.html)