はじめに
今回で、学習時間管理アプリ#4となります。
作成している過程で、日付と曜日の取得やAjaxを用いたサーバーからのリアルタイムで時間を取得するやり方などを解説しています。
ハンズオンで作成したい方は過去の記事をご覧ください。
改修要件を洗い出す
1.打刻画面
- 現在の日付と曜日を取得
- 現在の時刻をリアルタイムで表示
- 学習ボタンの状態管理を実装
開始:A 終了:B とし ボタンを押している状態を活性とする
Aを活性の場合は、Bは非活性とし、Aが非活性の場合はBを活性とする
2.一覧画面(5章で実装します!)
- 選択した表示年月のみを画面に表示する事
- 今月の学習累計時間を画面に表示する事
- 先月の学習累計時間を画面に表示する事
- 詳細画面の処理…JavaScriptの日付を自動に取得する処理をJST取得にする事
※現在はUTCになっているため使用できるが実用できない
コーディング
現在の日付と曜日を取得・現在の時刻をリアルタイムで表示(打刻画面)
現在のアプリの打刻画面は以下です。
では、日付と時間と曜日を表示できるようにしてみます。
「StudyTimeController.java」を編集します。
@GetMapping("/new")
public String newStudyTimeForm(Model model) {
// ベースとなる日付を取得する この場合はJST取得の日付 現在の時刻取得用
LocalDateTime currentDateTime = LocalDateTime.now(ZoneId.of("Asia/Tokyo"));
// どういう型で用意するかを定義
DateTimeFormatter dateFormateer = DateTimeFormatter.ofPattern("MM/dd"); // 日付で形成する
DateTimeFormatter timeFormateer = DateTimeFormatter.ofPattern("HH:mm:ss"); // 時間で形成する
// 現在の時刻にあてはめる
String date = currentDateTime.format(dateFormateer);
String time = currentDateTime.format(timeFormateer);
model.addAttribute("studyTimeObject", new StudyTime());
model.addAttribute("date", date);
model.addAttribute("time",time);
// 曜日を取得する
model.addAttribute("currentDayOfWeek", currentDateTime.getDayOfWeek().getDisplayName(TextStyle.FULL, Locale.JAPAN));
return "studytime/studyTimeForm";
}
「studyTimeForm.html」を編集します。
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>学習時間の入力</title>
</head>
<body>
<h1>学習時間の入力</h1>
<div>
<p th:text="${date}"></p>
<p th:text="${currentDayOfWeek}"></p>
<p th:text="${time}"></p>
</div>
<form th:action="@{/studytime}" method="post">
<button type="submit" name="action" value="start">学習開始</button>
<button type="submit" name="action" value="end">学習終了</button>
</form>
<a href="/studytime">戻る</a>
</body>
</html>
ブラウザで確認してみましょう!日付と時間と曜日が表示されたはずです。
しかし、秒針が止まっているのが個人的に気に入らないので、リアルタイムで秒針を刻んで時刻を更新出来るように修正します。以下のように「studyTimeForm.html」を修正します。
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>学習時間の入力</title>
<script>
function updateTime() {
// 現在の時刻を取得する
const now = new Date();
const options = { hour: '2-digit', minute: '2-digit', second: '2-digit', timeZone: 'Asia/Tokyo', hour12: false};
const formattedTime = now.toLocaleTimeString('ja-JP', options);
document.getElementById('currentTime').textContent = formattedTime;
}
// 1秒ごとにupdateTime関数を呼び出す
setInterval(updateTime, 1000);
</script>
</head>
<body>
<h1>学習時間の入力</h1>
<div>
<p th:text="${date}"></p>
<p th:text="${currentDayOfWeek}"></p>
<p id="currentTime"></p>
</div>
<form th:action="@{/studytime}" method="post">
<button type="submit" name="action" value="start">学習開始</button>
<button type="submit" name="action" value="end">学習終了</button>
</form>
<a href="/studytime">戻る</a>
</body>
</html>
スクショなので、秒針を動きを確認出来ませんが、画面は秒針が動いています。ただ、今度は画面を再読み込みすると、一瞬時刻の表示が画面から消えてしまいます。この動きも納得がいかないので、修正します。
初学者の方はちょっと難しいコードになりますが、ここは丁寧に解説します。
修正内容は以下が概要です。
1.Ajaxによる時刻取得
- fetch関数を使用してサーバーから現在の時刻を取得します
- HH:mm:ss形式で現在の時刻を返す
2.非同期で更新
- 1秒ごとにupdateTime関数を呼び出して更新しているため、随時サーバーから最新の時刻を取得しています。
「StudyTimeController.java」を編集します。
@GetMapping("/new")
public String newStudyTimeForm(Model model) {
// ベースとなる日付を取得する この場合はJST取得の日付 現在の時刻取得用
LocalDateTime currentDateTime = LocalDateTime.now(ZoneId.of("Asia/Tokyo"));
// どういう型で用意するかを定義
DateTimeFormatter dateFormateer = DateTimeFormatter.ofPattern("MM/dd"); // 日付で形成する
DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("HH:mm:ss");
// 現在の時刻にあてはめる
String date = currentDateTime.format(dateFormateer);
String time = currentDateTime.format(timeFormatter);
model.addAttribute("studyTimeObject", new StudyTime());
model.addAttribute("date", date);
model.addAttribute("time", time); //ページが読み込まれた時の初期表示の時刻表示用
// 曜日を取得する
model.addAttribute("currentDayOfWeek", currentDateTime.getDayOfWeek().getDisplayName(TextStyle.FULL, Locale.JAPAN));
return "studytime/studyTimeForm";
}
// 時刻を返すエンドポイント
@GetMapping("/current-time")
@ResponseBody
public String getCurrentTime() {
LocalDateTime currentDateTime = LocalDateTime.now(ZoneId.of("Asia/Tokyo"));
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss");
return currentDateTime.format(formatter);
}
「studyTimeForm.html」を修正します。
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>学習時間の入力</title>
<script>
function updateTime() {
// Ajaxでサーバーから現在の時刻を取得
fetch('/studytime/current-time')
.then(response => response.text())
.then(data => {
document.getElementById('currentTime').textContent = data; // 取得した時刻を表示
})
.catch(error => console.error('Error:', error));
}
// 1秒ごとにupdateTime関数を呼び出す
setInterval(updateTime, 1000);
</script>
</head>
<body>
<h1>学習時間の入力</h1>
<div>
<p th:text="${date}"></p>
<p th:text="${currentDayOfWeek}"></p>
<p id="currentTime" th:text="${time}"></p>
</div>
<form th:action="@{/studytime}" method="post">
<button type="submit" name="action" value="start">学習開始</button>
<button type="submit" name="action" value="end">学習終了</button>
</form>
<a href="/studytime">戻る</a>
</body>
</html>
fetchメソッドはJavaScriptの関数で指定したURLにHTTPリクエストを投げる。リクエストを投げて帰ってきたデータが受け取れていれば次の.thenへ移る
fetch('/studytime/current-time')
fetchメソッドは非同期処理の結果を表現するオブジェクトを返すのですが、そのオブジェクトの名称はPromiseと呼ばれます。この場合は時刻がレスポンスとして返ってきているため、テキストとして取得している状況です。
.then(response => response.text())
上記で受け取ったテキストが取得出来ていれば次の.thenへ移ります。その際、上記で渡されたパラメーターは「data」として渡されます。そのため下記では、「data」を使用して、currentTime要素のテキストとして設定します。
.then(data => {
document.getElementById('currentTime').textContent = data; // 取得した時刻を表示
})
<p id="currentTime" th:text="${time}"></p>
これにより、非同期で時刻を取得できるようになります。再読み込みしても消える事なく表示が可能となります。
学習ボタンの状態管理を実装
まず初めにこちらを実装するにあたって、かなり難航しました。というのもJavaScriptのクリックイベントで実装出来ると思っていたのですが、思ったような動きにならず…。
処理内容を改めて考え、少し時間を要しました。結果的にスキーマから練り直せば、そこまで処理が複雑にならず済むのでは?という考えのもと、実装しております。
「学習中フラグ」を追加し、具体的には学習が「進行中」か「終了」を判別するために使用します。スキーマに以下のカラムを追加します。
カラム名 | 属性 | DEFAULT |
---|---|---|
is_learning | BOOLEAN | FALSE |
-- カラムを追加する
ALTER TABLE study_times
ADD COLUMN is_learning BOOLEAN DEFAULT FALSE;
スキーマにカラムが追加出来たので、Entityを修正していきます。
「StudyTime.java」
package com.app.progTrack.entity;
import java.sql.Timestamp;
import java.time.LocalDateTime;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Data;
@Entity
@Table(name = "study_times")
@Data
public class StudyTime {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "study_times_id")
private Long studyTimesId;
@Column(name = "start_time")
private LocalDateTime startTime;
@Column(name = "end_time")
private LocalDateTime endTime;
@Column(name = "created_at", insertable = false, updatable = false)
private Timestamp createdAt;
@Column(name = "updated_at", insertable = false, updatable = false)
private Timestamp updatedAt;
@Column(name = "is_learning")
private Boolean isLearning;
}
「StudyTimeService.java」にisLiarningActiveメソッドを追加します。
package com.app.progTrack.service;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.List;
import org.springframework.stereotype.Service;
import com.app.progTrack.entity.StudyTime;
import com.app.progTrack.repository.StudyTimeRepository;
@Service
public class StudyTimeService {
private final StudyTimeRepository studyTimeRepository;
public StudyTimeService(StudyTimeRepository studyTimeRepository) {
this.studyTimeRepository = studyTimeRepository;
}
// 学習時間の打刻セット
public void saveStudyTime(StudyTime studyTime) {
studyTimeRepository.save(studyTime);
}
// 最後のレコードを取得する
public StudyTime getLastStudyTime() {
List<StudyTime> studyTimes = studyTimeRepository.findAll();
return studyTimes.isEmpty() ? null : studyTimes.get(studyTimes.size() - 1);
}
// UTCからJSTへ変換する処理
public LocalDateTime convertToJST(LocalDateTime utcDateTime) {
// LocalDateTimeをUTCのZoneTimeに変換
ZonedDateTime utcZoned = utcDateTime.atZone(ZoneId.of("UTC"));
// UTCからJSTへ同じ瞬間の時間を取得
ZonedDateTime jstZonde = utcZoned.withZoneSameInstant(ZoneId.of("Asia/Tokyo"));
// LocalDateTimeに変換する
return jstZonde.toLocalDateTime();
}
// 学習中の判定をする
public boolean isLiarningActive() {
StudyTime lastStudyTime = getLastStudyTime(); // 最後のレコードを取得
return lastStudyTime != null && lastStudyTime.getIsLearning();
}
// 一覧ページ表示用
public List<StudyTime> getAllStudyTimes() {
return studyTimeRepository.findAll();
}
// 編集用
public StudyTime getStudyTime(Long id) {
return studyTimeRepository.findById(id).orElse(null);
}
// 削除用
public void deleteStudyTime(Long id) {
studyTimeRepository.deleteById(id);
}
}
このメソッドは既に定義済みであるgetLastStudyTimeメソッドで最後のレコードを取得し、そのレコードの状態によって、trueかfalseを返す処理となっています。
// 最後のレコードを取得する
public StudyTime getLastStudyTime() {
List<StudyTime> studyTimes = studyTimeRepository.findAll();
return studyTimes.isEmpty() ? null : studyTimes.get(studyTimes.size() - 1);
}
// 途中のコードを割愛
// 学習中の判定をする
public boolean isLiarningActive() {
StudyTime lastStudyTime = getLastStudyTime(); // 最後のレコードを取得
return lastStudyTime != null && lastStudyTime.getIsLearning();
}
学習中の場合はtrue 学習していない場合はfalseという前提です。そのためDBでもデフォルトはfalseになっています。※最初は学習していない状態
lastStudyTimeがnullではない 且つ lastStudyTime.getIsLearning()メソッドで取得した真偽値がtrue つまり学習中であれば、メソッドの戻り値はtrueが返ります。
逆にlastStudyTimeがnullでないという事は最後のレコードが埋まっている状態(値が入っている)状態を指すので、終了時間の打刻がされているという事になるので、falseが戻り値で返ります。
「StudyTimeController.java」を編集します。以下は全文です。
追加したのは以下の57行目、76行目、84行目です。
package com.app.progTrack.controller;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.TextStyle;
import java.util.List;
import java.util.Locale;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import com.app.progTrack.entity.StudyTime;
import com.app.progTrack.service.StudyTimeService;
@Controller
@RequestMapping("/studytime")
public class StudyTimeController {
private final StudyTimeService studyTimeService;
public StudyTimeController(StudyTimeService studyTimeService) {
this.studyTimeService = studyTimeService;
}
@GetMapping
public String listStudyTimes(Model model) {
List<StudyTime> studyTimes = studyTimeService.getAllStudyTimes();
model.addAttribute("studyTimes", studyTimes);
return "studytime/studyTimeList";
}
@GetMapping("/new")
public String newStudyTimeForm(Model model) {
// ベースとなる日付を取得する この場合はJST取得の日付 現在の時刻取得用
LocalDateTime currentDateTime = LocalDateTime.now(ZoneId.of("Asia/Tokyo"));
// どういう型で用意するかを定義
DateTimeFormatter dateFormateer = DateTimeFormatter.ofPattern("MM/dd"); // 日付で形成する
DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("HH:mm:ss");
// 現在の時刻にあてはめる
String date = currentDateTime.format(dateFormateer);
String time = currentDateTime.format(timeFormatter);
model.addAttribute("studyTimeObject", new StudyTime());
model.addAttribute("date", date);
model.addAttribute("time", time); //ページが読み込まれた時の初期表示の時刻表示用
// 曜日を取得する
model.addAttribute("currentDayOfWeek", currentDateTime.getDayOfWeek().getDisplayName(TextStyle.FULL, Locale.JAPAN));
// 学習中フラグの状態を追加する
model.addAttribute("isLearningActive", studyTimeService.isLiarningActive());
return "studytime/studyTimeForm";
}
// 時刻を返すエンドポイント
@GetMapping("/current-time")
@ResponseBody
public String getCurrentTime() {
LocalDateTime currentDateTime = LocalDateTime.now(ZoneId.of("Asia/Tokyo"));
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss");
return currentDateTime.format(formatter);
}
@PostMapping
public String createStudyTime(@ModelAttribute StudyTime studyTime, @RequestParam String action) {
if ("start".equals(action)) {
studyTime.setStartTime(studyTimeService.convertToJST(LocalDateTime.now(ZoneId.of("UTC"))));
studyTime.setEndTime(null);
studyTime.setIsLearning(true);
studyTimeService.saveStudyTime(studyTime);
} else if ("end".equals(action)) {
// 直近の学習セッションを取得する
StudyTime lastStudyTime = studyTimeService.getLastStudyTime();
if (lastStudyTime != null) {
lastStudyTime.setEndTime(studyTimeService.convertToJST(LocalDateTime.now(ZoneId.of("UTC"))));
lastStudyTime.setIsLearning(false);
studyTimeService.saveStudyTime(lastStudyTime);
}
}
return "redirect:/studytime";
}
@GetMapping("/{id}/edit")
public String editStudyTimeForm(@PathVariable Long id, Model model) {
StudyTime studyTime = studyTimeService.getStudyTime(id);
model.addAttribute("studyTimeObject", studyTime);
return "studytime/edit";
}
@PostMapping("/{id}")
public String updateStudyTime(@PathVariable Long id, @ModelAttribute StudyTime studyTime) {
studyTime.setStudyTimesId(id);
studyTimeService.saveStudyTime(studyTime);
return "redirect:/studytime";
}
@PostMapping("/{id}/delete")
public String deleteStudyTime(@PathVariable Long id) {
studyTimeService.deleteStudyTime(id);
return "redirect:/studytime";
}
}
学習時間の打刻画面での状態を取得し、その状態をフロントエンドへ渡す形です。
model.addAttribute("isLearningActive", studyTimeService.isLiarningActive());
学習開始の処理です。trueに設定する事で学習中にフラグを切り替えます。
studyTime.setIsLearning(true);
学習終了の処理です。falseにする事で学習終了にフラグを切り替えます。
lastStudyTime.setIsLearning(false);
「studyTimeForm.html」の31行目と32行目を編集しました。
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>学習時間の入力</title>
<script>
function updateTime() {
// Ajaxでサーバーから現在の時刻を取得
fetch('/studytime/current-time')
.then(response => response.text())
.then(data => {
document.getElementById('currentTime').textContent = data; // 取得した時刻を表示
})
.catch(error => console.error('Error:', error));
}
// 1秒ごとにupdateTime関数を呼び出す
setInterval(updateTime, 1000);
</script>
</head>
<body>
<h1>学習時間の入力</h1>
<div>
<p th:text="${date}"></p>
<p th:text="${currentDayOfWeek}"></p>
<p id="currentTime" th:text="${time}"></p>
</div>
<form th:action="@{/studytime}" method="post">
<button type="submit" name="action" value="start" th:disabled="${isLearningActive}">学習開始</button>
<button type="submit" name="action" value="end" th:disabled="${!isLearningActive}">学習終了</button>
</form>
<a href="/studytime">戻る</a>
</body>
</html>
以下のコードは「value=”start”」が学習開始を示しています。上記のコントローラーで学習開始の場合にtrueを代入していたと思います。「”${isLearningActive}”」がtrueの場合にボタンは非活性されます。※学習中だから学習開始ボタンは押せないようにする
<button type="submit" name="action" value="start" th:disabled="${isLearningActive}">学習開始</button>
以下のコードは「value=”end”」が学習終了を示しています。上記のコントローラーで学習終了の場合にfalseを代入していたと思います。「”${isLearningActive}”」がfalseの場合はボタンは活性化されます。押せないようにするため「!」を付与しfalseからtrueへ反転させてボタンを押すことを出来なくしています。
<button type="submit" name="action" value="end" th:disabled="${!isLearningActive}">学習終了</button>
それではブラウザで確認してみましょう!!
まずは初期画面です。
学習開始します。
学習終了します。
なんとか実装できました。
おわりに
今回の実装は難しかったですねー。まさかのボタンの処理にここまで手こずるとは思っていなかったです。ただ、かなり勉強になりました。次は一覧ページの改修要件を実装します。レイアウトは最後になると思います。
出来ればJunitを使用したテストとかも出来ればよいのですが…あとデプロイとか…。そこまでやるかは未定です。それでは、また次回お会いしましょう!!
コメント