【Spring Boot入門】Eclipseで作ったToDoアプリを改修する

SpringBoot

はじめに

前回作成したtodoアプリを改修したいと思い、今回は要件を考えてから改修するという流れで取り組んでみました。実際の開発でも、客先から改修要望が来て、人工と工数を考えてから予算を決めて受託するという流れだと思います。

以下がキータを参考にして作成したToDoアプリです。こちらをベースにして改修をしています。

完成デモ

top画面

タスク作成

タスクを更新

タスクを完了

タスクを未完了に戻す

完了したタスクを削除する

バリデーション

改修要件

  1. 完了済みのチェックボックスをチェックして更新すると、未完了に戻すことができる
  2. 新しいタスクを空欄で追加をするとエラーメッセージを表示し入力させない
  3. 新しいタスクの日付を指定する時、過去の日付で入力させない
  4. 2.3の編集でも同様の処理を実装する
  5. 本日の日付を表示させること
  6. タスクの目標日付があと何日かを視認できるようにしたい
  7. タスク表示は目標日付を昇順で表示したい
  8. レイアウトを綺麗に整えたい
完了済みのチェックボックスをチェックして更新すると、未完了に戻すことができる
HTML
<input type="checkbox" name="done_flg" value="0" th:checked="${todo.done_flg == 0}" />

チェックボックスにレ点を入れるとvalueが1入るので完了の扱いです、チェックしてない状態が0で未完了の扱いです。

条件式はチェックボックスにレ点が入ってなければチェックボックスにチェックを付ける仕様にする事で、valueに1が入り、未完了になります。

新しいタスクを空欄で追加をするとエラーメッセージを表示し入力させない & 新しいタスクの日付を指定する時、過去の日付で入力させない
HTML
<input type="date" name="time_limit" required th:value="${#dates.format(new java.util.Date(), 'yyyy-MM-dd')}" min="${#dates.format(new java.util.Date(), 'yyyy-MM-dd')}"/>
  • requiredはユーザーがこのフィールドに入力しないとフォームを送信できなくする
  • min=”${#dates.format(new java.util.Date(), ‘yyyy-MM-dd’)}”はこのフィールドで選択できる最小の日付を現在の日付に設定する。これにより、過去の日付を選択できなくする
TodoController.java
  // タイトルが空であるか確認する
		if (todo.getTitle() == null || todo.getTitle().trim().isEmpty()) {
			return "redirect:/?error=emptyTitle";
		}
  • trim()は文字列の前後の空白を取り除く、その後に、isEmpty()で空になった文字列かを確認している
  • もしtitleがnullまたは空であれば、エラーメッセージを含むリダイレクトを行う、具体的にはredirect:/?error=emptyTitleでURLにエラーメッセージを追加してインデックスページへ戻す
TodoController.java
		// 日付が過去の日付であるか確認する
		if (todo.getTime_limit() != null && todo.getTime_limit().isBefore(LocalDate.now())) {
			return "redirect:/?error=pastDate";
		}
  • isBifore(LocalDate.now())は指定された日付が今日の日付よりも前であるかどうかを判断する、もし指定された日付が今日よりも過去であれば、無効な入力となる。
  • time_limitがnullでなく、かつ過去の日付であれば、再びリダイレクトを行い、エラーメッセージをURLに追加する
本日の日付を表示させること
TodoController.java
// 本日の日付をモデルに追加する
		model.addAttribute("today", LocalDate.now());
  • そのままだが、本日日付をモデルにセットする、その値をviewに渡す
index.html
	<p>本日の日付: <span th:text="${today}"></span></p>
  • LocalDate.now()の値がtodayに渡されているため、viewでそちらを渡すと本日日付が表示できる
タスクの目標日付があと何日かを視認できるようにしたい
Todo.java
	// 現在の日付 - 目標日付 であと〇日と出力するためのフィールド
	private long diffDays;
  • Entityに新しくフィールドを用意する
Todo.java
// 各Todoのtime_limitとの日数の差分を計算する
		for (Todo todo : list) {
			long diffDays = ChronoUnit.DAYS.between(LocalDate.now(), todo.getTime_limit());
			todo.setDiffDays(diffDays);
		}
  • ChronoUnitは日付や時間の単位を操作するためのユーティリティクラス
  • DAYS.betweenは引数でとった2つの日付の差分を日数で取得する
  • LocalDate.now()とtodo.getTime_limit()の差分が計算されることにより、現在の日付から期限日までの差分の日数が表示できる
タスク表示は目標日付を昇順で表示したい
Todo.java
<select id="selectIncomplete" resultMap="todoResultMap">
    SELECT * FROM todo_items WHERE done_flg = 0 ORDER BY time_limit ASC
</select>
    
<select id="selectComplete" resultMap="todoResultMap">
    SELECT * FROM todo_items WHERE done_flg = 1 ORDER BY time_limit ASC
</select>
  • ORDER BY time_limit ASCにより昇順で取得する
レイアウトを綺麗に整えたい
  • Bootstrapを活用し、レイアウトを調整した

全体のコード

index.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <!-- Bootstrap -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous"> 
    <title>Todo</title>
</head>
<body>
    <div class="container mt-3">
        <h1 class="text-center">ToDoList-タスク管理アプリ</h1>
        <h3 class="text-center mb-5">本日の日付: <span th:text="${#temporals.format(today, 'yyyy年MM月dd日')}"></span></h3>
        <div class="row">
            <div class="col-4">
                <div class="card">
                    <div class="card-body">
                       	<h3>タスク一覧</h3>
                        <form method="post" th:action="@{/update}" th:each="todo : ${todos}">
							<div class="row  mt-3">
								<div class="col-1">
									<input type="checkbox" name="done_flg" value="1" th:checked="${todo.done_flg == 1}" class="form-check-input"/>
		                            <input type="hidden" name="done_flg" value="0" th:if="${todo.done_flg == 0}" />
								</div>
								<div class="col-5">	
		                            <input type="hidden" name="id" th:value="${todo.id}" />
		                            <input type="text" name="title" th:value="${todo.title}" class="form-control"/>
								</div>
								<div class="col-5">
									 <input type="date" name="time_limit" th:value="${todo.time_limit}" class="form-control"/>
									 <span style="font-size: small; color: red;" th:text="'期限まで ' + ${todo.diffDays} + ' 日'"></span> 
								</div>
								<div class="col-4 d-flex justify-content-center">
									<input type="submit" value="完了" class="btn btn-success"/>
								</div>
							</div> 
                        </form>
                    </div>
                </div>
            </div>
            
            <div class="col-4">
                <div class="card">
                    <div class="card-body">
                        <h3>完了済み</h3>
                        <form method="post" th:action="@{/update}" th:each="todo : ${doneTodos}">
							<div class="row mb-3">
								<div class="col-1">
									 <input type="checkbox" name="done_flg" value="0" th:checked="${todo.done_flg == 0}" class="form-check-input"/>
								</div>
								<div class="col-5">
									<input type="hidden" name="id" th:value="${todo.id}" />
                            		<input type="text" name="title" th:value="${todo.title}" style="text-decoration:line-through" class="form-control"/>
								</div>
								<div class="col-5">
									<input type="date" name="time_limit" th:value="${todo.time_limit}" class="form-control"/>
								</div>
							</div>
                            <input type="submit" value="未完了に戻す" class="btn btn-warning"/>
                        </form>
                        <form method="post" th:action="@{/delete}">
                            <input type="submit" value="完了済みを削除" class="btn btn-danger mt-3"/>
                        </form>
                    </div>
                </div>
            </div>
            
            <div class="col-4">
                <div class="card">
                    <div class="card-body">
						<div th:if="${error != null}">
	                        <p style="color:red;">
	                            <span th:if="${error == 'emptyTitle'}">タスクのタイトルを入力してください!</span>
	                            <span th:if="${error == 'pastDate'}">未来の日付を設定してください!</span>
	                        </p>
                        </div>
                        <h3>新しいタスクを追加</h3>
                        <form method="post" th:action="@{/add}">
							<div class="row mb-3">
								<div class="col-7">
									 <input type="text" name="title" placeholder="追加するタスクを入力" class="form-control"/>
								</div>
								<div class="col-5">
									<input type="date" name="time_limit" required th:value="${#dates.format(new java.util.Date(), 'yyyy-MM-dd')}" min="${#dates.format(new java.util.Date(), 'yyyy-MM-dd')}" class="form-control"/>
								</div>
							</div>
                            <input type="submit" value="追加" class="btn btn-info"/>	
                        </form>
                        
                    </div>
                </div>
            </div>
        </div>
    </div>
    
    <!-- Bootstrap -->
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz" crossorigin="anonymous"></script>
</body>
</html>

※コードを添付した際、なぜか乱れています。すみません。

TodoController.java
package com.example.todo.controller;

import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
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.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

import com.example.todo.entity.Todo;
import com.example.todo.mapper.TodoMapper;

@Controller
public class TodoController {
	
	@Autowired
	TodoMapper todoMapper;
	
	@GetMapping("/")
	public String index(Model model, @RequestParam(required = false) String error) {

		List<Todo> list = todoMapper.selectIncomplete();
		List<Todo> doneList = todoMapper.selectComplete();

		model.addAttribute("todos", list);
		model.addAttribute("doneTodos", doneList);
		model.addAttribute("error", error);
		
		// 本日の日付をモデルに追加する
		model.addAttribute("today", LocalDate.now());
		
		// 各Todoのtime_limitとの日数の差分を計算する
		for (Todo todo : list) {
			long diffDays = ChronoUnit.DAYS.between(LocalDate.now(), todo.getTime_limit());
			todo.setDiffDays(diffDays);
		}
		
		return "index";
	}
	
	@PostMapping("/add")
	public String add(Todo todo) {
		
		// タイトルが空であるか確認する
		if (todo.getTitle() == null || todo.getTitle().trim().isEmpty()) {
			return "redirect:/?error=emptyTitle";
		}
		
		// 日付が過去の日付であるか確認する
		if (todo.getTime_limit() != null && todo.getTime_limit().isBefore(LocalDate.now())) {
			return "redirect:/?error=pastDate";
		}
		
		todoMapper.add(todo);
		return "redirect:/";
	}
	
	@PostMapping("/update")
	public String update(Todo todo) {
		
		// タイトルが空であるか確認する
		if (todo.getTitle().trim().isEmpty()) {
			return "redirect:/?error=emptyTitle";
		}
		
		// 日付が過去の日付であるか確認する
		if (todo.getTime_limit().isBefore(LocalDate.now())) {
			return "redirect:/?error=pastDate";
		}

		todoMapper.update(todo);
		
		return "redirect:/";
	}
	
	@PostMapping("/delete")
	public String delete() {
		todoMapper.delete();
		return "redirect:/";
	}
	
}
Todo.java
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
        
<mapper namespace="com.example.todo.mapper.TodoMapper">

	 <!-- resultMapの定義 -->
    <resultMap id="todoResultMap" type="com.example.todo.entity.Todo">
        <result property="id" column="id"/>
        <result property="title" column="title"/>
        <result property="done_flg" column="done_flg" />
        <result property="time_limit" column="time_limit" typeHandler="com.example.todo.LocalDateTypeHandler"/>
    </resultMap>

    <select id="selectAll" resultMap="todoResultMap">
        SELECT * FROM todo_items
    </select>
    
    <select id="selectIncomplete" resultMap="todoResultMap">
    	SELECT * FROM todo_items WHERE done_flg = 0 ORDER BY time_limit ASC
    </select>
    
    <select id="selectComplete" resultMap="todoResultMap">
    	SELECT * FROM todo_items WHERE done_flg = 1 ORDER BY time_limit ASC
    </select>
	
	<insert id="add" parameterType="com.example.todo.entity.Todo">
		INSERT INTO todo_items (title, time_limit)
		VALUES (#{title}, #{time_limit})
	</insert>
	
	<update id="update" parameterType="com.example.todo.entity.Todo">
		UPDATE todo_items 
		SET title = #{title}, time_limit = #{time_limit}, done_flg = #{done_flg}
		WHERE id = #{id}
	</update>
	
	<delete id="delete" parameterType="com.example.todo.entity.Todo">
		DELETE FROM todo_items WHERE done_flg = 1
	</delete>
	
</mapper>
Todo.java
package com.example.todo.entity;
import java.time.LocalDate;

import lombok.Data;

@Data
public class Todo {
	private Integer id;
	private String title;
	private Integer done_flg;
	private LocalDate time_limit;
	// 現在の日付 - 目標日付 であと〇日と出力するためのフィールド
	private long diffDays;
}
TodoController.java
package com.example.todo.mapper;

import java.util.List;

import org.apache.ibatis.annotations.Mapper;

import com.example.todo.entity.Todo;

@Mapper
public interface TodoMapper {
	
	public List<Todo> selectAll();
	
	public List<Todo> selectIncomplete();
	
	public List<Todo> selectComplete();
	
	public void add(Todo todo);
	
	public void update(Todo todo);
	
	public void delete();
}

ディレクトリの構成

※configファイルなどは載せておりません。

おわりに

今回の改修で、大分ORマッパーの使い方わ分かった気がします。JPAでいままで作成していたので視座が広がりました。ORマッパーとJPAが併用できるようなので、もう少しなれたら併用したアプリも作ってみたいなと思いまいた。

コメント