Spring Bootで勤怠表を作ってみる

Spring Bootは実務経験が無いため、数年前にサンプルで作ったものですが、復学のために掲載します。

最低でも以下の画面がないと実用は難しいとは思いますが学習が目的なので今回は必要最小限の開発に留めます。
 ・休日申請画面
 ・勤怠承認画面(勤怠の情報を承認者が確定させる→電子印鑑などの機能があると尚よい)
 ・マスタ画面(管理者権限の対象者のみ)
 ・勤怠稼働集計(法的に違反した労働時間になっていないか)
 ・印刷
 ・勤怠連携データ(給与計算処理を行うためのもの)

この投稿の開発で利用するもの。
・Eclips Version: 2019-03 (4.11.0)Build id: 20190314-1200
・MyBatis 1.4.2
・Spring Tool3
・Posgre 9.1
・JQery 3.3.1

■画面構成/機能

【ログイン】

 ◆機能
  ・ログインができること
  ・ログインできない場合はエラー表示すること
 ◆利用テーブル 
  ・【ユーザマスタ】ログインに利用
  ・【企業マスタ】会社固有情報(創立記念の休日扱いなど)
 

【勤怠画面】

 ◆機能
  ・前月、次月のボタンが表示され、目的のデータが表示される
  ・送信ボタンで登録できる
  ・各日付毎で入力すると自動で就業時間が計算される
  ・休憩時間、就業時間の計が表示される
 ◆利用テーブル
  ・【企業マスタ】会社固有情報(創立記念の休日扱いなど)
  ・【週マスタ】表示に利用(日本語、英語に切り替え易くする)
  ・【勤怠データ】勤怠の実態データ

■テーブル構成/設計

・mst_company【企業マスタ】
  id character(4) NOT NULL,
  name text,
  cutoff_date integer,
  CONSTRAINT mst_company_pkey PRIMARY KEY (id )
・mst_user【ユーザマスタ】
 username character varying(10) NOT NULL,
  password character varying(10),
  name text,
  roles text,
  classno character(1),
  company_id character(4),
  regist_dt date,
  update_dt date,
  update_id text,
  CONSTRAINT "PK" PRIMARY KEY (username )
・mst_week【週マスタ】
  id integer NOT NULL,
  week_name character varying(2),
  CONSTRAINT mst_week_pkey PRIMARY KEY (id )
・attendance【勤怠データ】
  logid character varying(10) NOT NULL,
  yer character(4) NOT NULL,
  month character(2) NOT NULL,
  day character(2) NOT NULL,
  start_time character(5),
  end_time character(5),
  break_time character varying(5),
  status text,
  memo text,
  regist_id text,
  regist_dt timestamp without time zone,
  update_id text,
  update_dt timestamp without time zone,
  CONSTRAINT "Attendance_pkey" PRIMARY KEY (logid , yer , month , day )

■ログイン画面

画面モックを作ります。
※javascriptは直書きしていますが、分離した方が良いかもしれません。
また、ログイン画面はセキュリティーが絡むので、実際に運用する想定の場合はライブラリを利用した方が無難かもしれません。

<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Login</title>
<link href="css/login.css" rel="stylesheet">
<link
	href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css"
	rel="stylesheet"
	integrity="sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M"
	crossorigin="anonymous">
<link
	href="https://getbootstrap.com/docs/4.0/examples/signin/signin.css"
	rel="stylesheet" crossorigin="anonymous" />
<script type="text/javascript" th:src="@{js/jquery-3.4.1.js}"></script>
<script type="text/javascript" th:src="@{js/jquery-3.4.1.min.js}"></script>


<script>
// 初回・リダイレクト時のデータ取得

$(function(){ // .ready() callback, is only executed when the DOM is fully loaded
	if(document.getElementById('hGet').value == 'master'){
		// 管理画面
		location.href = "http://localhost:8080/master" ;
	}else if(document.getElementById('hGet').value == 'user'){
		// 勤務表
		location.href = "http://localhost:8080/attendance" ;
	}
});

</script>
</head>
<body>
	<form method="post" action="/login" style="text-align: center;">
		<input  type="hidden" id="hGet" th:value="${get}">
		<table align="center" style="width: 400px;">
			<tr>
				<td></td>
				<td><h2>Login</h2></td>
				<td></td>
			</tr>
			<tr>
				<td colspan="3" style="color: red"><p th:text="${message}"></p></td>
			</tr>
			<tr>
				<td style="text-align: right;">ログイン名:</td>
				<td><input type="text" name="id" pattern="[0-9A-Za-z]{1,}"
					maxlength='10' placeholder="Username" /><br></td>
				<td></td>
			</tr>
			<tr>
				<td style="text-align: right;">パスワード:</td>
				<td><input type="password" name="pass"
					pattern="[0-9A-Za-z]{1,}" maxlength='10' placeholder="Password" /><br></td>
				<td></td>
			</tr>
			<tr>
				<td></td>
				<td></td>
				<td style="text-align: left;"><input type="submit" value="送信" class="button" style="width: 100px;"/></td>
			</tr>
		</table>
	</form>
</body>
</html>

この画面では「送信」ボタンを押下するアクション先は以下の様に定義しています。
@RequestMapping でコントローラの呼び出し先に紐付けされます。
<form method="post" action="/login" style="text-align: center;">

コントローラー部に画面項目を引き渡す為のクラスを定義します。

package com.example.mybatisdemo.controller.longin;

public class LogInForm {

	private String id;
	private String pass;

	public String getId() {
		return id;
	}

	public void setId(String id) {
		this.id = id;
	}

	public String getPass() {
		return pass;
	}

	public void setPass(String pass) {
		this.pass = pass;
	}

	LogInForm() {
		this.id = id;
	}
}

ログイン情報を保持する為のセッションを定義します。

package com.example.mybatisdemo;

import java.io.Serializable;

import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.stereotype.Component;

import lombok.Data;

@Data
@Component
@Scope(value="session", proxyMode=ScopedProxyMode.TARGET_CLASS)
public class SessionData implements Serializable{
    private static final long serialVersionUID = 1L;
    String userId;
    String name;
    String companyName;
    String yer;
    String month;
    String roles;
    String company_id;
    String su;
}

mybatisのデータ取得マッパーを作成します。
ここではユーザマスタと企業マスタを紐づけて取得します。

package com.example.mybatisdemo.mapper;

import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Options;
import org.apache.ibatis.annotations.Select;

import com.example.mybatisdemo.domain.SelectMstUser;

@Mapper
public interface MstUserMapper {

	@Insert("INSERT INTO mst_user (password, name, classno, company_id, regist_dt, update_dt, update_id) VALUES (#{password}, #{classno}, #{company_id}, #{regist_dt}, #{update_dt}, #{update_id})")
	@Options(useGeneratedKeys = true, keyProperty = "username")
	void insert(MstUserMapper logMst);

	@Select("SELECT "
			+ "  mst_user.username,"
			+ "  mst_user.password,"
			+ "  mst_user.name, "
			+ "  mst_user.roles, "
			+ "  mst_user.classno, "
			+ "  mst_user.company_id, "
			+ "  mst_company.name as company_name ,"
			+ "  mst_company.cutoff_date,"
			+ "  mst_user.regist_dt,"
			+ "  mst_user.update_dt, "
			+ "  mst_user.update_id "
			+ "FROM "
			+ "  mst_user, "
			+ "  mst_company "
			+ "WHERE "
			+ "  username = #{username} "
			+ "  AND mst_company.id = mst_user.company_id")
	SelectMstUser select(String username);

}


コントローラ(処理の実体)を実装します。

package com.example.mybatisdemo.controller.longin;

import java.time.LocalDateTime;
import java.util.Locale;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.ModelAndView;

import com.example.mybatisdemo.SessionData;
import com.example.mybatisdemo.domain.SelectMstUser;
import com.example.mybatisdemo.mapper.MstUserMapper;

@Controller
public class LogInController {

	@Autowired
	SessionData sessionData;

	@Autowired
	private MstUserMapper mstLogMapper;

	@Autowired
	private MessageSource messageSource;

	private ModelAndView mv;

	@ModelAttribute // (1)
	public LogInForm setUpEchoForm() {
		LogInForm form = new LogInForm();
		return form;
	}

  // 初回リクエスト
	@RequestMapping(value = "/", method = RequestMethod.GET)
	public ModelAndView index(LogInForm form) {
		mv = new ModelAndView();

		mv.setViewName("login");
		return mv;
	}

  // 送信ボタンイベント
	@RequestMapping(value = "/login", method = RequestMethod.POST)
	//	@Transactional
	public String postLongin(LogInForm form, Model model) {
		String ret = null;
		String message = "";
		ret = "login";

		SelectMstUser loadedMstLog = mstLogMapper.select(form.getId()); // インサートしたTodoを取得して標準出力する

		if (loadedMstLog == null || (!loadedMstLog.getPassword().equals(form.getPass()))) {

			message = messageSource.getMessage("err.login", null, Locale.JAPANESE);

			model.addAttribute("message", message);
		} else {

			//ページロード初回のみセッションに年月設定
			if (sessionData.getYer() == null) {
				sessionData.setUserId(loadedMstLog.getUsername());
				sessionData.setName(loadedMstLog.getName());
				// 勤務表用
				sessionData.setCompanyName(loadedMstLog.getCompany_name());
				sessionData.setYer(String.valueOf(LocalDateTime.now().getYear()));
				sessionData.setMonth(String.format("%02d", LocalDateTime.now().getMonth().getValue()));
				// マスター画面用
				sessionData.setRoles(loadedMstLog.getRoles());
				sessionData.setCompany_id(loadedMstLog.getCompany_id());
			}

			if (messageSource.getMessage("roles.master", null, Locale.JAPANESE).equals(loadedMstLog.getRoles())) {
				// マスター画面遷移
				model.addAttribute("get", "master");
			} else if (messageSource.getMessage("roles.user", null, Locale.JAPANESE).equals(loadedMstLog.getRoles())) {
				// 勤務表画面遷移
				model.addAttribute("get", "user");

			}

			model.addAttribute("userName", sessionData.getName());
			model.addAttribute("companyName", sessionData.getCompanyName());
			model.addAttribute("cutoffDate", loadedMstLog.getCutoff_date());

		}

		return ret;
	}

}

メッセージプロパティを設定します。
※同じ文言の利用頻度が高い場合は煩雑になりますが、練習用なら個別に埋め込んでも良いかと思います。

err.login = IDまたはパスワードが一致しません。
err.session = セッションが切れました。
err.set = 登録できませんでした。
kbn.syukin   = 出勤
kbn.kyusyutu = 休出
kbn.yukyu    = 有給
kbn.daikyu   = 代休
kbn.kekkin   = 欠勤
kbn.keityou  = 慶弔
msg.ok = 登録しました。
roles.master = 1
roles.user = 2
format.ymd = yyyy-MM-dd HH:mm:ss.SSS

■勤怠画面

画面モックを作ります。

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org/">
<!-- a -->
<head>
<meta charset="UTF-8" />
<title>勤怠表</title>
<link href="css/attendance.css" rel="stylesheet">
<script type="text/javascript" th:src="@{/webjars/jquery/3.3.1/jquery.min.js}"></script>
<script type="text/javascript" th:src="@{js/test.js}"></script>

<script>
	// 初回・リダイレクト時のデータ取得
	jQuery(document).ready(function() {
		getData();

	});


	// データ取得
	function getData() {

		var temp_worktime = 0;

		// リロード時の初期設定
		for (var i = 0; i < $("#scheduleTable tbody").children().length; i++) {
			// 稼働時間自動計算編集
			temp_worktime = getWork_time(getVal('end_time', i),
										getVal('start_time', i),
										getVal('break_time', i));
			$('input[name="work_time[' + i + ']"]').prop('disabled', false);
			$('input[name="work_time[' + i + ']"]').val(temp_worktime);
			$('input[name="work_time[' + i + ']"]').prop('disabled', true);
		}

		// 合計初期化
		setTotalReset();

		// 合計の再計算
		setTotal();


		// 入力イベント
		$('input').change(function(e) {
			var rowNum = $(this).closest('tr').index();
			var sum = "";

			// 時間入力時の稼働時間自動計算編集
			if (getVal('start_time', rowNum) != '' & getVal('end_time', rowNum) != '') {

				//nullの場合、昼休憩の1時間休憩を挿入
				if (getVal('break_time', rowNum) == '') {
					$('input[name="break_time[' + rowNum + ']"]').val('01:00');
				}

				// 稼働時間自動計算編集
				sum = getWork_time(getVal('end_time', rowNum),
						getVal('start_time', rowNum),
						getVal('break_time', rowNum));

			}

			//入力ロックを一時的に外して計算結果を反映
			$('input[name="work_time[' + rowNum + ']"]').prop('disabled', false);
			$('input[name="work_time[' + rowNum + ']"]').val(sum);
			$('input[name="work_time[' + rowNum + ']"]').prop('disabled', true);


			// 合計初期化
			setTotalReset();

			// 合計の再計算
			setTotal();

		});
	}

	// データ登録
	function setData() {

		var chkCount = 0;

		// 入力チェック
		chkCount = chkUpLoad();

		// 入力変更が無かった場合は終了
		if(chkCount <= 0){
			if(chkCount == 0){
				alert('変更点がありません。');
			}
			return;
		}

		   // サブミットするフォームを取得
		   var f = document.forms['myform'];

		   f.method = 'POST'; // method(GET or POST)を設定する
		   f.action = "http://localhost:8080/setJsonData" ; // action(遷移先URL)を設定する
		   f.submit(); // submit する

	}

	// 入力チェック
	function chkUpLoad() {
		var chkCount = 0;

		// フォームデータをアップロードするためのデータ再作成
		for (var i = 0; i < $("#scheduleTable tbody").children().length; i++) {

			var selectElements = document.getElementsByName('status[' + i + ']'),
            optionElements = selectElements[0].options;

			// 入力チェック
			if(getVal('hStatus', i) != optionElements[selectElements[0].selectedIndex].value
					|| getVal('hStart_time', i) != getVal('start_time', i)
					|| getVal('hEnd_time', i) != getVal('end_time', i)
					|| getVal('hBreak_time', i) != getVal('break_time', i)
					|| getVal('hMemo', i) != getVal('memo', i)){
				// 入力に変更が生じた場合カウント
				chkCount += 1;

				// 入力チェック
				if('' == optionElements[selectElements[0].selectedIndex].value
						|| '' == getVal('start_time', i)
						|| '' == getVal('end_time', i)
						|| '' == getVal('break_time', i)){

					if('' == optionElements[selectElements[0].selectedIndex].value){
						setFocus('status', i);
					}else if('' == getVal('start_time', i)){
						setFocus('start_time', i);
					}else if('' == getVal('end_time', i)){
						setFocus('end_time', i);
					}else if('' == getVal('break_time', i)){
						setFocus('break_time', i);
					}

					alert('入力がありません。');
					return -1;
				}

				if('' == getVal('hRegist_id', i)){
					$('input[name="hInsUpdChekVal[' + i + ']"]').val('INS');
				}else{
					$('input[name="hInsUpdChekVal[' + i + ']"]').val('UPD');
				}

			}

		}
		return chkCount;
	}

	//前月次月ボタン押下処理
	function onClickMonth(v_num) {

		if ($('#hMonthId').val() == '12' || $('#hMonthId').val() == '01') {

			if (v_num > 0) {
				//次月
				if ($('#hMonthId').val() == '12') {
					//12月
					$('#hYerId').val(Number($('#hYerId').val()) + v_num);
					$('#hMonthId').val(('0' + v_num).slice(-2));
				} else {
					//1月
					$('#hMonthId').val(('0' + (Number($('#hMonthId').val()) + v_num)).slice(-2));
				}
			} else {
				//前月
				if ($('#hMonthId').val() == '12') {
					//12月
					$('#hMonthId').val(('0' + (Number($('#hMonthId').val()) + v_num)).slice(-2));
				} else {
					//1月
					$('#hYerId').val(Number($('#hYerId').val()) + v_num);
					$('#hMonthId').val(('0' + 12).slice(-2));
				}
			}

		} else {
			$('#hMonthId').val(('0' + (Number($('#hMonthId').val()) + v_num)).slice(-2));

		}


		var f = document.forms['myform'];
		f.method = 'POST'; // method(GET or POST)を設定する
		f.action = "http://localhost:8080/getPostData" ;
		f.submit(); // submit する
		return;
	};

	//ログアウトボタン押下処理
	function setSessionClear() {

		$('#result').after(
						'<div id="submit_result" class="section__block section__block--notification"><p>セッション情報を破棄しました。</p></div>');
		//セッション情報クリア
		window.sessionStorage.clear();
	}

	//登録ボタン押下処理
	function setSubmit() {
		setData();
	}
</script>

</head>
<body>
	<form name="myform"
		style="text-align: center;">
		<header style="background: #FFEEEE;">
			<div style="text-align: left;">
			    <div class="block_header">会社名</div>
			    <div class="block__element" th:text="${companyName}"></div>
			    <div class="block_header">氏名</div>
			    <div class="block__element" th:text="${userName}"></div>
		  	</div>
		</header>


		<table id="scheduleTable" class="list" border="1" style="border-collapse: collapse">
			<div style="text-align: left;">
				<input type="hidden" th:id="hYerId" th:name="'hYer'" th:value="${yer}">
				<input type="hidden" th:id="hMonthId" th:name="'hMonth'"th:value="${month}">

			    <div class="block__element"><p th:name="'yermon'" th:text=" ${yer} + '年 ' + ${month} + '月'">
			    </div>
			    <div class="block__element">
				    <input type="button" id="bMonth" value="前月" onclick='onClickMonth(-1);'>
				    <input type="button" id="aMonth" value="次月" onclick='onClickMonth(1);'>
			    </div>
		  	</div>

			<thead>
				<tr>
					<th style="width: 20px;">日</th>
					<th style="width: 40px;">曜日</th>
					<th style="width: 60px;">区分</th>
					<th style="width: 60px;">出社</th>
					<th style="width: 60px;">退社</th>
					<th style="width: 50px;">休憩<br>時間
					</th>
					<th style="width: 60px;">就業<br>時間
					</th>
					<th style="width: 200px;">備考</th>
				</tr>
			</thead>
			<tbody>
				<tr th:each="row, stat : ${attendance}" th:style="'background-color:' + @{(${row.week == '1' || row.week == '7'} ? '#dda0dd' : '#FFFFFF')} + ''">
					<td th:text="${row.day}"></td>
					<td th:text="${row.week_name}"></td>
					<td>
						<input type="hidden" th:id="hLogid"
							th:name="'hLogid[' + ${stat.index} + ']'"
							th:value="${row.logid}">
						<input type="hidden" th:id="hDay"
							th:name="'hDay[' + ${stat.index} + ']'"
							th:value="${row.day}">
						<select id="singleSelectId"
							th:id="'singleSelect[' + ${stat.index} + ']'" name="status"
							th:name="'status[' + ${stat.index} + ']'"
							>
							<option th:value="${row.status}"></option>
							<option th:each="item : ${statusItems}" th:value="${item.key}"
								th:text="${item.value}"
								th:selected="${item.key} == *{row.status}"></option>
						</select>
						<input type="hidden" th:id="hStatusId"
								th:name="'hStatus[' + ${stat.index} + ']'"
								th:value="${row.status}">
					</td>
					<td><input type="time"
						name="start_time" th:name="'start_time[' + ${stat.index} + ']'"
						th:value="${row.start_time}" pattern="[0-9:]{1,}"
						onblur="myFnc(' + ${stat.index} + ');">
						<input type="hidden" th:id="hStart_timeId"
								th:name="'hStart_time[' + ${stat.index} + ']'"
								th:value="${row.start_time}">
						</td>
					<td><input type="time"
						name="end_time" th:name="'end_time[' + ${stat.index} + ']'"
						th:value="${row.end_time}" pattern="[0-9:]{1,}"
						onblur="myFnc(' + ${stat.index} + ');">
						<input type="hidden" th:id="hEnd_timeId"
								th:name="'hEnd_time[' + ${stat.index} + ']'"
								th:value="${row.end_time}">
						</td>
					<td><input type="time"
						name="break_time" th:name="'break_time[' + ${stat.index} + ']'"
						th:value="${row.break_time}" pattern="[0-9:]{1,}"
						onblur="myFnc(' + ${stat.index} + ');">
						<input type="hidden" th:id="hBreak_timeId"
								th:name="'hBreak_time[' + ${stat.index} + ']'"
								th:value="${row.break_time}">
						</td>
					<td><input type="text" style="width: 60px;" maxlength='5'
						name="work_time" th:name="'work_time[' + ${stat.index} + ']'"
						 pattern="[0-9:]{1,}">
						<input type="hidden" th:id="hWork_timeId"
								th:name="'hWork_time[' + ${stat.index} + ']'" >
						</td>
					<td><input type="text" style="width: 180px;" maxlength='10'
						name="memo" th:name="'memo[' + ${stat.index} + ']'"
						th:value="${row.memo}">
						<input type="hidden" th:id="hMemoId"
							th:name="'hMemo[' + ${stat.index} + ']'"
							th:value="${row.memo}">

						<!-- hiddnChk -->
						<input type="hidden" th:id="hRegist_id"
							th:name="'hRegist_id[' + ${stat.index} + ']'"
							th:value="${row.regist_id}">
						<input type="hidden" th:id="hRegist_dt"
							th:name="'hRegist_dt[' + ${stat.index} + ']'"
							th:value="${row.regist_dt}">
						<input type="hidden" th:id="hUpdate_id"
							th:name="'hUpdate_id[' + ${stat.index} + ']'"
							th:value="${row.update_id}">
						<input type="hidden" th:id="hUpdate_dt"
							th:name="'hUpdate_dt[' + ${stat.index} + ']'"
							th:value="${row.update_dt}">
						<input type="hidden" th:id="hInsUpdChekVal"
							th:name="'hInsUpdChekVal[' + ${stat.index} + ']'"
							th:value="SKIP">
						</td>
				</tr>
			</tbody>
		</table>

		<table class="list" border="1">
			<tr>
				<th style="width: 245px;"></th>
				<th style="width: 50px;">休憩<br>時間
				</th>
				<th style="width: 50px;">就業<br>時間
				</th>

			</tr>
			<tr>
				<td></td>
				<td id="total_breakId" style="width: 100px;"></td>
				<td id="total_WorkId" style="width: 100px;"></td>

			</tr>

		</table>
		<input type="button" name="send" value="送信" onclick='setSubmit();'>
	</form>
</body>
</html>

コントローラー部に画面項目を引き渡す為のクラスを定義します。

package com.example.mybatisdemo.controller.attendance;

import java.sql.Timestamp;

public class AttendanceForm {

    private String logid;
    private String yer;
    private String month;
    private String day;
    private String start_time;
    private String end_time;
    private String break_time;
    private String status;
    private String memo;
    private String regist_id;
    private Timestamp regist_dt;
    private String update_id;
    private Timestamp update_dt;

	public String getLogid() {
		return logid;
	}
	public void setLogid(String logid) {
		this.logid = logid;
	}
	public String getYer() {
		return yer;
	}
	public void setYer(String yer) {
		this.yer = yer;
	}
	public String getMonth() {
		return month;
	}
	public void setMonth(String month) {
		this.month = month;
	}
	public String getDay() {
		return day;
	}
	public void setDay(String day) {
		this.day = day;
	}
	public String getStart_time() {
		return start_time;
	}
	public void setStart_time(String start_time) {
		this.start_time = start_time;
	}
	public String getEnd_time() {
		return end_time;
	}
	public void setEnd_time(String end_time) {
		this.end_time = end_time;
	}
	public String getBreak_time() {
		return break_time;
	}
	public void setBreak_time(String break_time) {
		this.break_time = break_time;
	}
	public String getStatus() {
		return status;
	}
	public void setStatus(String status) {
		this.status = status;
	}
	public String getMemo() {
		return memo;
	}
	public void setMemo(String memo) {
		this.memo = memo;
	}
	public String getRegist_id() {
		return regist_id;
	}
	public void setRegist_id(String regist_id) {
		this.regist_id = regist_id;
	}
	public Timestamp getRegist_dt() {
		return regist_dt;
	}
	public void setRegist_dt(Timestamp regist_dt) {
		this.regist_dt = regist_dt;
	}
	public String getUpdate_id() {
		return update_id;
	}
	public void setUpdate_id(String update_id) {
		this.update_id = update_id;
	}
	public Timestamp getUpdate_dt() {
		return update_dt;
	}
	public void setUpdate_dt(Timestamp update_dt) {
		this.update_dt = update_dt;
	}



}

※作りが悪いので二つクラスが必要です。
本来なら一つで足りますが初回表示用に作成。

package com.example.mybatisdemo.controller.attendance;


public class AttendanceList {

	private String logid;
	private String yer;
	private String month;
	private String day;
	private String week;
	private String week_name;
	private String start_time;
	private String end_time;
	private String break_time;
	private String status;
	private String memo;
	private String regist_id;
	private String regist_dt;
	private String update_id;
	private String update_dt;


	public String getLogid() {
		return logid;
	}
	public void setLogid(String logid) {
		this.logid = logid;
	}
	public String getYer() {
		return yer;
	}
	public void setYer(String yer) {
		this.yer = yer;
	}
	public String getMonth() {
		return month;
	}
	public void setMonth(String month) {
		this.month = month;
	}
	public String getDay() {
		return day;
	}
	public void setDay(String day) {
		this.day = day;
	}
	public String getWeek() {
		return week;
	}
	public void setWeek(String week) {
		this.week = week;
	}
	public String getWeek_name() {
		return week_name;
	}
	public void setWeek_name(String week_name) {
		this.week_name = week_name;
	}
	public String getStart_time() {
		return start_time;
	}
	public void setStart_time(String start_time) {
		this.start_time = start_time;
	}
	public String getEnd_time() {
		return end_time;
	}
	public void setEnd_time(String end_time) {
		this.end_time = end_time;
	}
	public String getBreak_time() {
		return break_time;
	}
	public void setBreak_time(String break_time) {
		this.break_time = break_time;
	}
	public String getStatus() {
		return status;
	}
	public void setStatus(String status) {
		this.status = status;
	}
	public String getMemo() {
		return memo;
	}
	public void setMemo(String memo) {
		this.memo = memo;
	}
	public String getRegist_id() {
		return regist_id;
	}
	public void setRegist_id(String regist_id) {
		this.regist_id = regist_id;
	}
	public String getRegist_dt() {
		return regist_dt;
	}
	public void setRegist_dt(String regist_dt) {
		this.regist_dt = regist_dt;
	}
	public String getUpdate_id() {
		return update_id;
	}
	public void setUpdate_id(String update_id) {
		this.update_id = update_id;
	}
	public String getUpdate_dt() {
		return update_dt;
	}
	public void setUpdate_dt(String update_dt) {
		this.update_dt = update_dt;
	}



}

mybatisのデータ取得・登録マッパーを作成します。

package com.example.mybatisdemo.mapper;

import java.util.List;

import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Options;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;

import com.example.mybatisdemo.controller.attendance.AttendanceForm;
import com.example.mybatisdemo.controller.attendance.AttendanceList;

@Mapper
public interface AttendanceMapper {

	@Insert("INSERT INTO attendance (" +
			"   logid, " +
			"   yer, " +
			"   month, " +
			"   day, " +
			"   start_time, " +
			"   end_time, " +
			"   break_time, " +
			"   status, " +
			"   memo, " +
			"   regist_id, " +
			"   regist_dt, " +
			"   update_id,   " +
			"   update_dt " +
			") VALUES (" +
			"#{logid}, " +
			"#{yer}, " +
			"#{month}, " +
			"#{day}, " +
			"#{start_time}, " +
			"#{end_time}, " +
			"#{break_time}, " +
			"#{status}, " +
			"#{memo}, " +
			"#{regist_id}, " +
			"#{regist_dt}, " +
			"#{update_id}, " +
			"#{update_dt} )")

	@Options(useGeneratedKeys = true, keyProperty = "logid")
	void insert(AttendanceForm mstBunrui);

	@Update("UPDATE attendance SET " +
			"   start_time = #{start_time} ," +
			"   end_time  = #{end_time}, " +
			"   break_time  = #{break_time}, " +
			"   status    = #{status}, " +
			"   memo      = #{memo}, " +
			"   update_dt = #{update_dt}, " +
			"   update_id = #{update_id}  " +
			"WHERE " +
			"   logid = #{logid} AND" +
			"   yer = #{yer} AND" +
			"   month = #{month} AND " +
			"   day = #{day} ")
	@Options(useGeneratedKeys = true, keyProperty = "logid")
	void update(AttendanceForm mstBunrui);

	@Select("SELECT " +
			"   atd_list.logid," +
			"   cal.yer," +
			"   cal.month," +
			"   cal.day," +
			"   cal.week, " +
			"   cal.week_name, " +
			"   atd_list.start_time," +
			"   atd_list.end_time," +
			"   atd_list.break_time," +
			"   atd_list.status," +
			"   atd_list.memo," +
			"   atd_list.regist_id," +
			"   to_char(atd_list.regist_dt, 'yyyy-mm-dd hh24:MI:ss.US') as regist_dt," +
			"   atd_list.update_id," +
			"   to_char(atd_list.update_dt, 'yyyy-mm-dd hh24:MI:ss.US') as update_dt " +
			"FROM " +
			"(SELECT" +
			"   to_char(arr,'YYYY') as yer," +
			"   to_char(arr,'MM') as month," +
			"   to_char(arr,'DD') as day," +
			"   to_char(arr,'D') as week, " +
			"   mst_week.week_name " +
			"FROM " +
			"   generate_series( cast( concat(#{yer},'/',#{month},'/01') as timestamp) ," +
			"   DATE_TRUNC('month', cast( concat(#{yer},'/',#{month},'/01') as timestamp) + '1 months') + '-1 days','1 days') as arr,"
			+
			"   mst_week " +
			" WHERE " +
			"   to_number(to_char(arr,'D'), '9') = mst_week.id " +
			") as cal" +
			"   LEFT JOIN " +
			"(" +
			"" +
			"SELECT " +
			"   attendance.logid," +
			"   attendance.yer," +
			"   attendance.month," +
			"   attendance.day," +
			"   attendance.start_time," +
			"   attendance.end_time," +
			"   attendance.break_time," +
			"   attendance.status," +
			"   attendance.memo," +
			"   attendance.regist_id," +
			"   attendance.regist_dt," +
			"   attendance.update_id," +
			"   attendance.update_dt " +
			"FROM " +
			"   attendance " +
			"WHERE " +
			"  logid = #{logid} AND" +
			"  yer = #{yer} AND" +
			"  month = #{month} " +
			") as atd_list " +
			"" +
			"ON cal.yer = atd_list.yer AND" +
			"   cal.month = atd_list.month AND" +
			"   cal.day = atd_list.day " +
			"ORDER BY " +
			"   cal.day")
	List<AttendanceList> select(String logid, String yer, String month);

	@Select("SELECT * " +
			"FROM " +
			"   attendance " +
			"WHERE " +
			"  logid = #{logid} AND" +
			"  yer = #{yer} AND" +
			"  month = #{month} " +
			"ORDER BY " +
			"   day")
	List<AttendanceForm> selectChek(String logid, String yer, String month);
}

コントローラ(処理の実体)を実装します。

package com.example.mybatisdemo.controller.attendance;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.stereotype.Controller;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

import com.example.mybatisdemo.SessionData;
import com.example.mybatisdemo.mapper.AttendanceMapper;

@Controller
public class AttendanceController {

	static final String C_INS = "INS";
	static final String C_UPD = "UPD";

	static final String C_UPD_ERR = "UPD_ERR";
	static final String C_SKIP = "SKIP";

	@Autowired
	SessionData sessionData;

	@Autowired
	private AttendanceMapper attendanceMapper;

	@Autowired
	private MessageSource messageSource;

	/**
	 * getAttendanceメソッド
	 * 初回のリクエスト取得:
	 * セッション情報が存在すればデータを取得し、勤務表画面を表示
	 * セッションが切れた場合はログイン画面に遷移
	 * @param request
	 * @param model
	 * @return リクエストパラメータ
	 */
	@RequestMapping(value = "/attendance", method = RequestMethod.GET)
	public String getAttendance(HttpServletRequest request, Model model) {
		String ret = null;
		String message = "";

		if (sessionData.getUserId() == null) {
			ret = "login";
			message = messageSource.getMessage("err.login", null, Locale.JAPANESE);

		} else {
			ret = "attendance";

			message = sessionData.getName();

			// hiddnで設定された年月をセッション情報にセット
			if (null != request.getParameter("hYer")) {
				sessionData.setYer(request.getParameter("hYer"));
				sessionData.setMonth(request.getParameter("hMonth"));
			}

			List<AttendanceList> attendanceList = attendanceMapper.select(sessionData.getUserId(),
					sessionData.getYer(),
					sessionData.getMonth());
			model.addAttribute("attendance", attendanceList);
		}

		model.addAttribute("message", message);
		model.addAttribute("userName", sessionData.getName());
		model.addAttribute("companyName", sessionData.getCompanyName());

		model.addAttribute("statusItems", getStatusItems());
		model.addAttribute("yer", sessionData.getYer());
		model.addAttribute("month", sessionData.getMonth());
		return ret;
	}

	/**
	 * getPostDataメソッド
	 * 次月のリクエスト取得:
	 * 受け取ったhiddenの年月をセッションにセットしてリロード(getAttendance)
	 *
	 * @param request
	 * @param model
	 * @return リクエストパラメータ
	 */
	@RequestMapping(value = "/getPostData", method = { RequestMethod.POST })
	public String getPostData(HttpServletRequest request, Model model) {
		String ret = "redirect:login";

		// セッションが切れていなければ処理続行
		if (sessionData.getUserId() != null) {
			ret = "redirect:attendance";

			// hiddnで設定された年月をセッション情報にセット
			if (null != request.getParameter("hYer")) {
				sessionData.setYer(request.getParameter("hYer"));
				sessionData.setMonth(request.getParameter("hMonth"));
			}
		}
		return ret;
	}

	/**
	 * setJsonDataメソッド
	 * データ登録:
	 * postで受け取ってsetJsonData2で処理した後、リロード(getAttendance)
	 *
	 * @param request
	 * @param model
	 * @return リクエストパラメータ
	 * @throws ParseException
	 */
	@RequestMapping(value = "/setJsonData", method = { RequestMethod.POST })
	public String setJsonData(HttpServletRequest request, Model model) throws ParseException {
		String ret = "redirect:login";

		// セッションが切れていなければ処理続行
		if (sessionData.getUserId() != null) {
			ret = "redirect:attendance";

			setData(request, model);
		}
		return ret;
	}

	/**
	 * setDataメソッド
	 * データ登録:
	 * データ登録とトランザクションコミットのため細分化
	 * (ResponseBodyをつけるとリロードしないため)
	 * @param request
	 * @param model
	 * @return なし
	 * @throws ParseException
	 */
	@Transactional
	@ResponseBody
	public void setData(HttpServletRequest request, Model model) throws ParseException {
		java.util.Date rdate = null;
		java.util.Date udate = null;
		RetChkData retChkData = new RetChkData();
		retChkData.setSkipValue(0);

		Map<String, String[]> tempPostData = request.getParameterMap();
		AttendanceForm attendanceForm = new AttendanceForm();
		// 更新前のデータを取得
		List<AttendanceForm> chkAttendance = attendanceMapper.selectChek(
				sessionData.getUserId(),
				sessionData.getYer(),
				sessionData.getMonth());

		SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
		//データ書き込み
		for (int i = 0; i < 31; i++) {
			rdate = null;
			udate = null;

			if (((tempPostData.size() - 1) / 18) <= i) {
				//28日以降のデータが無ければ処理終了
				//配列の最大値-1(年と月のカラム)/18(項目数)= その月の最終日
				break;
			}

			retChkData.setStatus(tempPostData.get("hInsUpdChekVal[" + i + "]")[0]);
			if (C_SKIP.equals(retChkData.getStatus())) {
				// 画面からの入力・変更分以外はスキップ
				continue;
			}

			// 入力チェック
			if (!("".equals(tempPostData.get("start_time[" + i + "]")[0]))) {

				if (0 != tempPostData.get("hRegist_dt[" + i + "]")[0].length()) {
					rdate = sdf.parse(tempPostData.get("hRegist_dt[" + i + "]")[0].substring(0, 23));
				} else {
					rdate = new Date();
				}

				if (0 != tempPostData.get("hUpdate_dt[" + i + "]")[0].length()) {
					udate = sdf.parse(tempPostData.get("hUpdate_dt[" + i + "]")[0].substring(0, 23));
				} else {
					udate = new Date();
				}

				attendanceForm.setLogid(tempPostData.get("hLogid[" + i + "]")[0]);
				attendanceForm.setYer(tempPostData.get("hYer")[0]);
				attendanceForm.setMonth(tempPostData.get("hMonth")[0]);
				attendanceForm.setDay(tempPostData.get("hDay[" + i + "]")[0]);
				attendanceForm.setStart_time(tempPostData.get("start_time[" + i + "]")[0]);
				attendanceForm.setEnd_time(tempPostData.get("end_time[" + i + "]")[0]);
				attendanceForm.setBreak_time(tempPostData.get("break_time[" + i + "]")[0]);
				attendanceForm.setStatus(tempPostData.get("status[" + i + "]")[0]);
				attendanceForm.setMemo(tempPostData.get("memo[" + i + "]")[0]);
				attendanceForm.setRegist_id(tempPostData.get("hRegist_id[" + i + "]")[0]);
				attendanceForm.setRegist_dt(new java.sql.Timestamp(rdate.getTime()));
				attendanceForm.setUpdate_id(tempPostData.get("hUpdate_id[" + i + "]")[0]);
				attendanceForm.setUpdate_dt(new java.sql.Timestamp(udate.getTime()));

				// 楽観ロックのチェック
				if (chkData(chkAttendance, attendanceForm, retChkData)) {
					if (C_INS.equals(retChkData.getStatus())) {
						// 登録
						attendanceForm.setLogid(sessionData.getUserId());
						attendanceForm.setRegist_id(sessionData.getUserId());
						attendanceForm.setRegist_dt(new java.sql.Timestamp(new Date().getTime()));
						attendanceForm.setUpdate_id(sessionData.getUserId());
						attendanceForm.setUpdate_dt(new java.sql.Timestamp(new Date().getTime()));
						attendanceMapper.insert(attendanceForm);

					} else if (C_UPD.equals(retChkData.getStatus())) {
						// 更新
						attendanceForm.setUpdate_id(sessionData.getUserId());
						attendanceForm.setUpdate_dt(new java.sql.Timestamp(new Date().getTime()));
						attendanceMapper.update(attendanceForm);

					}
				} else {
					// データ取得後に他ユーザの変更が生じた場合
					break;
				}
			}
		}
	}

	/**
	 * chkDataメソッド
	 * データチェック:
	 * 更新前と画面の情報を比較し、登録か更新かを判断する
	 *
	 * @param chkAttendance
	 * @param attendance
	 * @param i
	 * @return Boolean
	 */
	private Boolean chkData(List<AttendanceForm> chkAttendance, AttendanceForm attendance, RetChkData retChkData) {
		Boolean ret = false;
		int chk = 0;

		for (int i = retChkData.getSkipValue(); i < chkAttendance.size(); i++) {

			// 日付が一致した場合
			if (attendance.getDay().equals(chkAttendance.get(i).getDay())) {
				chk++;
				retChkData.setSkipValue(i+1);

				// 楽観ロックのチェック
				if (C_UPD.equals(retChkData.getStatus())
						&& attendance.getRegist_id().equals(chkAttendance.get(i).getRegist_id())
						&& attendance.getRegist_dt().equals(chkAttendance.get(i).getRegist_dt())
						&& attendance.getUpdate_id().equals(chkAttendance.get(i).getUpdate_id())
						&& attendance.getUpdate_dt().equals(chkAttendance.get(i).getUpdate_dt())) {

					ret = true;
					break;
				}
			}
		}

		// 楽観ロックのチェック
		if (C_INS.equals(retChkData.getStatus())) {
			if (0 == chk) {
				ret = true;
			}
		}

		return ret;

	}

	/**
	 * getStatusItemsメソッド
	 * 区分の項目:
	 * 出勤~慶弔の項目を画面に設定するための定義
	 *
	 * @param なし
	 * @return 区分のマップデータ
	 */
	private Map<String, String> getStatusItems() {
		Map<String, String> selectMap = new LinkedHashMap<String, String>();
		selectMap.put("1", messageSource.getMessage("kbn.syukin", null, Locale.JAPANESE));
		selectMap.put("2", messageSource.getMessage("kbn.kyusyutu", null, Locale.JAPANESE));
		selectMap.put("3", messageSource.getMessage("kbn.yukyu", null, Locale.JAPANESE));
		selectMap.put("4", messageSource.getMessage("kbn.daikyu", null, Locale.JAPANESE));
		selectMap.put("5", messageSource.getMessage("kbn.kekkin", null, Locale.JAPANESE));
		selectMap.put("6", messageSource.getMessage("kbn.keityou", null, Locale.JAPANESE));

		return selectMap;
	}

}

★カレンダー作成の肝
良いか悪いか別として、SQLで生成した方が楽だったので今回はこの様に作成。(うるう年とかの計算も面倒)

SELECT
  to_char(arr, 'YYYY') as yer
  , to_char(arr, 'MM') as month
  , to_char(arr, 'DD') as day
  , to_char(arr, 'D') as week
  , mst_week.week_name 
FROM
  generate_series( 
    cast(concat(2014, '/', 07, '/01') as timestamp)
    , DATE_TRUNC( 
      'month'
      , cast(concat(2014, '/', 07, '/01') as timestamp) + '1 months'
    ) + '-1 days'
    , '1 days'
  ) as arr
  , mst_week 
WHERE
  to_number(to_char(arr, 'D'), '9') = mst_week.id 
ORDER BY
  day

楽観ロック用にチェッククラスを定義します。

package com.example.mybatisdemo.controller.attendance;



public class RetChkData {

    private String status;
    private int skipValue;

	public String getStatus() {
		return status;
	}
	public void setStatus(String status) {
		this.status = status;
	}
	public int getSkipValue() {
		return skipValue;
	}
	public void setSkipValue(int skipValue) {
		this.skipValue = skipValue;
	}

}

■感想

わりかし困ったのは対象月のテーブルリストを表示する時のhtml記述でしょうか。入力したものをチェックして保存出来る様にしようとすると意図した動作にならず、サンプルが無かったので難義しました。
(記述形式が理解できず、2か月近く試行錯誤)
とりあえず動くから良しとしますが、Springの実務経験が無いので正解かどうかは不明。

■実際に作ったサンプルソースは以下に保存しています。

※以下の場所にテーブル作成とデータが格納されています。
mybatis-demo\src\main\resources\webapp\tablle

いいなと思ったら応援しよう!