1424 lines
72 KiB
Markdown
1424 lines
72 KiB
Markdown
# 部署管理機能 実装手順書(研修生用)
|
||
|
||
この手順書は、`README.md` の課題「部署テーブルの CRUD 機能追加」を、**未経験でも一歩ずつ進められる**ようにした道しるべです。
|
||
|
||
- コードは **「写して終わり」にならないよう、考えてほしい部分を `/* ★考える */` で空けてあります**。空欄は、既存の社員機能(お手本)を読めば埋められます。
|
||
- 各ステップの最後に **「できたかの確認」** があります。確認できるまで次へ進まないでください。
|
||
- 詰まったら、先に「**詰まったとき**」(第10章) を読み、それでも解決しない場合は **30分** 悩んだ時点で相談してください。相談するときは「やりたいこと/期待した結果/実際の結果(エラー全文)/試したこと/自分の考え」をそろえてください。
|
||
|
||
---
|
||
|
||
## 目次
|
||
|
||
0. 全体像(何を作るのか・進め方)
|
||
1. 準備(環境・DB・起動確認)
|
||
2. 実装前に読むもの(読解ワーク)
|
||
3. 作業の基本ルール(ファイルの置き場所・名前)
|
||
4. 第1段階:検索
|
||
5. 第2段階:削除
|
||
6. 第3段階:更新(ここまでが提出可能ライン)
|
||
7. 第4段階:新規登録(チャレンジ)
|
||
8. 第5段階:アクセス制御アノテーション(チャレンジその2)
|
||
9. テストの進め方(テスト仕様書の使い方)
|
||
10. 詰まったとき(エラー別の調べ方)
|
||
11. 提出前チェックリスト
|
||
|
||
---
|
||
|
||
## 0. 全体像
|
||
|
||
### 0.1 作るもの
|
||
|
||
管理者が「部署」を **検索・削除・更新・新規登録** できる画面です。すでにある「社員」の機能と同じ作りで作ります。
|
||
|
||
| 段階 | 内容 | 到達目標 |
|
||
|---|---|---|
|
||
| 1 | 検索 | メニューから部署検索画面を開き、部署ID(完全一致)・部署名(部分一致)で検索できる |
|
||
| 2 | 削除 | 検索結果の各行から削除できる |
|
||
| 3 | 更新 | 検索結果の各行から部署名を更新できる(**提出可能ライン**) |
|
||
| 4 | 新規登録 | 入力 → 確認 → 登録 → 完了 の3画面で登録できる |
|
||
| 5 | アクセス制御 | 未ログイン・一般ユーザーは部署画面に入れない |
|
||
|
||
### 0.2 プログラムの構造(これだけは覚える)
|
||
|
||
画面でボタンを押してから、DBに届くまでの流れです。
|
||
|
||
```
|
||
[ブラウザ] ボタン押下
|
||
│ ① POST で送信(例:/selectDepartment)
|
||
▼
|
||
[Controller] presentation/controller/…Controller.java
|
||
│ 受け取る・どの画面に行くか決める・メッセージを用意する
|
||
│ ② Form ⇄ Model に詰め替え
|
||
▼
|
||
[Service] domain/service/…(インタフェース) と impl/…Impl.java(実装)
|
||
│ 業務の処理(検索する・削除する、など)
|
||
▼
|
||
[Repository] infra/repository/…Repository.java ←すでに用意済み
|
||
▼
|
||
[MySQL] department テーブル
|
||
▲
|
||
└ 結果は逆向きに戻り、Controller が [HTML(Thymeleaf)] に渡して画面になる
|
||
```
|
||
|
||
| 部品 | 役割 | 部署で使うもの |
|
||
|---|---|---|
|
||
| Form | 画面とControllerの間で値を運ぶ箱 | `DepartmentForm`(既存)、他は自作 |
|
||
| Model | 業務で使うデータ | `Department`、`SearchConditionDepartment`(既存) |
|
||
| Entity | DBの1行(infra層専用) | `DepartmentEntity`(既存) |
|
||
|
||
### 0.3 すでにあるもの・作るもの
|
||
|
||
| すでにある(触らない) | 自分で作る |
|
||
|---|---|
|
||
| `DepartmentEntity`、`DepartmentRepository` | Form 3つ |
|
||
| `Department`、`SearchConditionDepartment` | Service:削除・更新(IF+実装)、登録(**実装のみ**) |
|
||
| `DepartmentForm` | Controller 7つ |
|
||
| `SearchDepartment`(検索サービス) | HTML 5つ |
|
||
| `RegisterDepartment`(**インタフェースだけ**) | `menu.html` にリンク、定数の追記 |
|
||
| アノテーション `@SessionCheck` `@PermissionCheck` | |
|
||
|
||
### 0.4 進め方のコツ
|
||
|
||
1. **お手本(社員機能)を必ず動かして、コードを読んでから**書く。
|
||
2. 一度に全部書かない。**1ファイルできたらビルド・起動して確認**する。
|
||
3. エラーは**一番下の `Caused by` から**読む(第10章)。
|
||
4. 変更は**1つずつ**。直らなかったら元に戻して別の仮説を試す。
|
||
5. 第1〜4段階では `@PermissionCheck` / `@SessionCheck` は**付けない**(第5段階で付けます)。
|
||
|
||
---
|
||
|
||
## 1. 準備
|
||
|
||
### 1.1 必要なもの
|
||
|
||
| 項目 | 内容 | 確認方法 |
|
||
|---|---|---|
|
||
| JDK **17** | **17以外は不可**(JDK 25 では Lombok が動かずビルドエラーになります) | コマンドで `java -version` |
|
||
| MySQL 8 | 起動していること | 接続できる |
|
||
| Eclipse | README の手順でプロジェクトをインポート済み | 赤い×が出ていない |
|
||
|
||
### 1.2 DBを初期状態にする(README「課題に取りかかる前に」)
|
||
|
||
1. 未作成なら MySQL で DB とユーザーを作る。`設計書/start-db-ddl-dml.sql` の先頭に **コメントアウトされた** 次の3行があります。必要ならコメントを外して実行します。
|
||
```sql
|
||
CREATE DATABASE rezodb;
|
||
CREATE USER 'rezouser'@'localhost' IDENTIFIED BY 'Rezo_0000';
|
||
GRANT ALL ON rezodb.* TO 'rezouser'@'localhost';
|
||
```
|
||
2. A5:SQL Mk-2 等で `設計書/start-db-ddl-dml.sql` を**全部**実行する。
|
||
- このファイルの文字コードは **Shift-JIS** です。UTF-8のエディタで開くと文字化けします(A5M2 から実行してください)。
|
||
3. 確認:`department` に6件、`employee` に10件。
|
||
|
||
| id_department | nm_department |
|
||
|---|---|
|
||
| 10 | 役員 |
|
||
| 11 | 人事・総務部 |
|
||
| 12 | 経理部 |
|
||
| 13 | システム開発事業部 |
|
||
| 14 | 教育事業部 |
|
||
| 15 | 営業企画部 |
|
||
|
||
> テーブルが無いまま起動すると、ログインの時点で `Table 'rezodb.employee' doesn't exist` のエラーになります。
|
||
|
||
### 1.3 ビルドと起動
|
||
|
||
README のとおり、Eclipse で プロジェクトを右クリック → 実行 → **maven clean** → **maven install**。またはプロジェクト直下の `build.cmd`。起動は `run.cmd`(または Eclipse からアプリを実行)。
|
||
|
||
ブラウザで `http://localhost:8080/` を開き、ログインできることを確認します。
|
||
|
||
| 種類 | メールアドレス | パスワード | ログイン後 |
|
||
|---|---|---|---|
|
||
| 管理者 | `yamada@hoge.jp` | `aaa` | メニュー画面 |
|
||
| 一般 | `jtaka@hoge.jp` | `bbb` | 個人情報更新画面 |
|
||
|
||
### 1.4 できたかの確認
|
||
|
||
- [ ] 管理者でログインし、メニューが表示された
|
||
- [ ] 「社員検索」「社員登録」を一通り操作できた(**お手本の動きを体験**する)
|
||
|
||
---
|
||
|
||
## 2. 実装前に読むもの(読解ワーク)
|
||
|
||
**書き始める前に**必ず次を読み、下の「課題理解シート」を埋めてください(README にも「何を作るか・どこまで作るか理解してから作業を始める」とあります)。
|
||
|
||
1. `README.md`、`ヒント.md`
|
||
2. `設計書/【管理者】部署情報検索更新削除機能-詳細設計書.xlsx`
|
||
3. `設計書/【管理者】部署情報登録機能-詳細設計書.xlsx`
|
||
4. `設計書/【管理者】メニュー機能-詳細設計書.xlsx`
|
||
5. `テスト仕様書/単体テスト仕様書.xlsx` の「部署管理テスト仕様書」シート
|
||
6. 既存の社員機能のコード(第3章の表の「お手本」)
|
||
|
||
### 課題理解シート(ノート等に書く)
|
||
|
||
```
|
||
1. 作るもの(1行):
|
||
2. 画面の一覧(画面名/何をする画面か):
|
||
3. 画面ごとのボタンと、押したときの動き:
|
||
4. 画面に出すメッセージ(文言/どんなとき/どう出す):
|
||
5. 使うテーブルと項目(型・桁数):
|
||
6. 作るファイルの一覧と、それぞれのお手本にする既存ファイル:
|
||
7. 分からないこと:
|
||
```
|
||
|
||
### 設計書を読むときのポイント
|
||
|
||
- 「画面項目定義」=画面にある部品の一覧。「イベント処理」=ボタンを押したときの処理手順。
|
||
- **メッセージの文言は一字一句そのまま**使う(テスト仕様書で確認されます)。
|
||
- 設計書には、社員機能からのコピー由来と思われる**誤記**がいくつかあります(例:部署の設計書に「社員登録画面」「社員マスタに登録」など)。**おかしいと思った所はメモして質問**してください。
|
||
- **テーブル定義書と実際のDDLで、部署名の桁数が食い違っています**(定義書は50、DDLは `VARCHAR(10)`)。第7章で実際に確認します。気づいたらメモしておいてください。
|
||
|
||
### メッセージ一覧(設計書・テスト仕様書より)
|
||
|
||
| 場面 | 文言 | 出し方 |
|
||
|---|---|---|
|
||
| 検索結果0件 | `検索結果はありません。` | 画面内の文字 |
|
||
| 削除ボタン | `本当に削除しますか?` | 確認ダイアログ |
|
||
| 削除:対象IDが不明 | `削除対象のIDが不明です。` | エラー表示 |
|
||
| 削除:社員が所属している | `1名以上が所属している部署は削除出来ません。` | エラー表示 |
|
||
| 削除成功 | `削除しました。` | アラート |
|
||
| 更新ボタン | `本当によろしいですか?` | 確認ダイアログ |
|
||
| 更新:対象がない | `更新対象のデータが見つかりません。` | エラー表示 |
|
||
| 更新:同名あり/変更なし | `既に同名の部署が存在するか、値が変更されていません。` | エラー表示 |
|
||
| 更新:空欄 | `項目が空欄です。` | エラー表示 |
|
||
| 更新成功 | `更新が完了しました。` | アラート |
|
||
| 登録:部署名が不正 | `部署名が不正です。` | エラー表示 |
|
||
| 登録:同名あり | `既に同名の部署が存在します。` | エラー表示 |
|
||
| 登録成功 | `部署登録が完了しました` | 完了画面の見出し |
|
||
|
||
---
|
||
|
||
## 3. 作業の基本ルール
|
||
|
||
### 3.1 ファイルの置き場所
|
||
|
||
ベースは `src/main/java/com/example/rhizome/` です(以下 `…/` と略します)。
|
||
|
||
| 種類 | 置き場所 | 作るファイル |
|
||
|---|---|---|
|
||
| Form | `…/app/presentation/form/` | `SearchConditionDepartmentForm`、`UpdateDepartmentForm`、`InsertDepartmentForm` |
|
||
| Controller | `…/app/presentation/controller/` | `SelectDepartmentController`、`DeleteDepartmentController`、`UpdateDepartmentController`、`UpdateDepartmentResultController`、`InsertDepartmentInputController`、`InsertDepartmentConfirmController`、`InsertDepartmentController` |
|
||
| Service(IF) | `…/app/domain/service/` | `DeleteDepartment`、`UpdateDepartment` |
|
||
| Service(実装) | `…/app/domain/service/impl/` | `DeleteDepartmentImpl`、`UpdateDepartmentImpl`、`RegisterDepartmentImpl` |
|
||
| 定数 | `…/app/presentation/controller/pageproperty/` | 既存の `TransitionTargetPageNameKeyword`、`PageReturnAttributeKeyword` に**追記** |
|
||
| HTML | `src/main/resources/templates/` | `selectDepartment`、`updateDepartmentInput`、`insertDepartmentInput`、`insertDepartmentConfirm`、`insertDepartmentResult`(拡張子 `.html`)、既存 `menu.html` に追記 |
|
||
|
||
> **パッケージの注意**:すべて `com.example.rhizome` の配下に置きます。外に置くと Spring が見つけられず、起動時に `NoSuchBeanDefinitionException` になります。
|
||
|
||
### 3.2 お手本(社員機能)との対応
|
||
|
||
| 作るもの | お手本 |
|
||
|---|---|
|
||
| `SelectDepartmentController` | `SelectEmployeeController` |
|
||
| `SearchConditionDepartmentForm` | `SearchConditionEmployeeForm` |
|
||
| `selectDepartment.html` | `selectEmployee.html` |
|
||
| `DeleteDepartmentController`、`DeleteDepartment(Impl)` | `DeleteEmployeeController`、`DeleteEmployee(Impl)` |
|
||
| `UpdateDepartmentController` / `…ResultController` | `UpdateEmployeeController` / `UpdateEmployeeResultController` |
|
||
| `UpdateDepartmentForm`、`updateDepartmentInput.html` | `UpdateEmployeeInfoForm`、`updateEmployeeInput.html` |
|
||
| `UpdateDepartment(Impl)` | `UpdateEmployee(Impl)` |
|
||
| `InsertDepartment…Controller`、`InsertDepartmentForm`、`insertDepartment…html` | `InsertEmployee…Controller`、`InsertEmployeeForm`、`insertEmployee…html` |
|
||
| `RegisterDepartmentImpl` | `RegisterEmployeeImpl` |
|
||
|
||
**お手本は「構造を学ぶ」ために使います。** 名前を置換してコピーするだけでは、部署に不要な処理(部署のプルダウン、パスワード等)が残って動かなかったり、意味が分からないままになります。**各行が何をしているか説明できる状態**で写してください。
|
||
|
||
### 3.3 コードの書き方の約束(既存に合わせる)
|
||
|
||
- 変数・フィールド名:部署ID=`idDepartment`、部署名=`nmDepartment`
|
||
- クラスは `@Controller` / `@Service` / Lombok の `@Data` 等を、お手本と同じように使う
|
||
- 画面名・URL・画面に渡すキーは**文字列を直書きせず定数にする**(`TransitionTargetPageNameKeyword` / `PageReturnAttributeKeyword`)
|
||
- Controller の処理は、お手本と同様に `try { … } catch (Exception e) { … }` で囲む
|
||
|
||
---
|
||
|
||
## 4. 第1段階:検索
|
||
|
||
### 4.0 ゴールと完成の定義
|
||
|
||
メニューの「部署検索」から部署検索画面に行き、**部署ID=完全一致/部署名=部分一致** で検索できる。検索後も入力した条件が画面に残る。0件なら `検索結果はありません。`。
|
||
→ **単体テスト #1〜#8** が期待どおりになれば完成。
|
||
|
||
完成後の流れ:
|
||
|
||
```
|
||
menu.html「部署検索」 ──POST /selectDepartment──▶ SelectDepartmentController
|
||
└──▶ selectDepartment.html(検索条件+結果)
|
||
```
|
||
|
||
### ステップ 1-1:お手本を動かして、コードで追う
|
||
|
||
1. 社員検索を実行する。
|
||
2. `SelectEmployeeController` の `select` メソッドの最初の行に**ブレークポイント**を置いて、デバッグ実行し、ステップ実行で追う。
|
||
3. 次の質問に自分の言葉で答える(答えられなければコードを読み直す)。
|
||
- Form に値が入るのは、どの時点・誰の仕事?
|
||
- `select` メソッドが**2つ**あるのはなぜ?(片方は引数に検索条件の Form を持つ、片方は持たない。この違いは第2段階で効いてきます)
|
||
- 検索サービスは最終的にどこでSQLを作っている?(`EmployeeInfoRepository` の `Specification`)
|
||
|
||
**できたかの確認**:ホワイトボードやノートに、§0.2 のような流れ図を自分で描ける。
|
||
|
||
### ステップ 1-2:定数を追加する
|
||
|
||
`TransitionTargetPageNameKeyword.java` には、すでに `// Department` と書かれた空の枠があります。ここに追記します。
|
||
|
||
- **HTML名**:`templates` のファイル名(拡張子なし)。先頭に `/` は付けない。
|
||
- **URL**:Controller のURL。**先頭に `/` を付ける**。
|
||
|
||
```java
|
||
// Department
|
||
// HTML
|
||
public static final String SELECT_DEPARTMENT_HTML = "selectDepartment";
|
||
|
||
// Controller
|
||
public static final String SELECT_DEPARTMENT = "/selectDepartment";
|
||
```
|
||
|
||
> `menu.html` にある既存のJavaScript(`selectDepartmentSubmit()`)は、`selectDepartment` という名前に送信します。上の定数はそれに合わせています。
|
||
|
||
**できたかの確認**:ビルドが通る(まだ画面は動かなくてよい)。
|
||
|
||
### ステップ 1-3:検索条件の Form を作る
|
||
|
||
`…/presentation/form/SearchConditionDepartmentForm.java`
|
||
|
||
```java
|
||
package com.example.rhizome.app.presentation.form;
|
||
|
||
import com.example.rhizome.app.domain.model.SearchConditionDepartment;
|
||
import lombok.AllArgsConstructor;
|
||
import lombok.Data;
|
||
import lombok.NoArgsConstructor;
|
||
|
||
/**
|
||
* 画面から渡ってきた部署の検索条件をまとめ、再度画面に表示する為のクラス
|
||
*/
|
||
@Data
|
||
@AllArgsConstructor
|
||
@NoArgsConstructor
|
||
public class SearchConditionDepartmentForm {
|
||
|
||
/** 部署ID */
|
||
private Integer idDepartment;
|
||
/** 部署名 */
|
||
private String nmDepartment;
|
||
|
||
/**
|
||
* Form から business 層の SearchConditionDepartment に変換する
|
||
*/
|
||
public static SearchConditionDepartment convertTo(SearchConditionDepartmentForm form) {
|
||
return new SearchConditionDepartment(/* ★考える:Form の2つの値を渡す */);
|
||
}
|
||
}
|
||
```
|
||
|
||
考えるポイント:
|
||
- 部署IDが `int` ではなく `Integer` なのはなぜ?(未入力=`null` を表したい)
|
||
- `@NoArgsConstructor` が無いと何が困る?(画面の値を自動で詰める時に、空のインスタンスが必要)
|
||
|
||
**できたかの確認**:ビルドが通る。「Form/Model/Entity の違い」を3行で説明できる。
|
||
|
||
### ステップ 1-4:検索サービスの使い方を調べる(コードは書かない)
|
||
|
||
`SearchDepartment`(インタフェース)には3つのメソッドがあります。
|
||
|
||
| メソッド | 動き |
|
||
|---|---|
|
||
| `selectAll()` | 全件取得 |
|
||
| `searchBy(条件)` | 部署ID=完全一致、部署名=**実質完全一致** |
|
||
| `searchByWithLike(条件)` | 部署ID=完全一致、部署名=**部分一致** |
|
||
|
||
> `searchBy` は内部で `like` を使っていますが、`%` を付けないので**完全一致と同じ**です。メソッド名に惑わされず、`SearchDepartmentImpl` と `DepartmentRepository` を読んで確認してください。
|
||
|
||
**課題**:今回の仕様「部署名は部分一致」を満たすのはどちらか。実際に両方試して(部署名に `営業` と入れて)確認し、理由をノートに書く。
|
||
|
||
### ステップ 1-5:検索 Controller を作る
|
||
|
||
`…/presentation/controller/SelectDepartmentController.java`(お手本:`SelectEmployeeController`)
|
||
|
||
```java
|
||
package com.example.rhizome.app.presentation.controller;
|
||
|
||
import com.example.rhizome.app.domain.service.SearchDepartment;
|
||
import com.example.rhizome.app.presentation.controller.pageproperty.PageReturnAttributeKeyword;
|
||
import com.example.rhizome.app.presentation.controller.pageproperty.TransitionTargetPageNameKeyword;
|
||
import com.example.rhizome.app.presentation.form.DepartmentForm;
|
||
import com.example.rhizome.app.presentation.form.SearchConditionDepartmentForm;
|
||
import org.springframework.beans.factory.annotation.Autowired;
|
||
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 javax.servlet.http.HttpSession;
|
||
import java.util.logging.Level;
|
||
import java.util.logging.Logger;
|
||
|
||
@Controller
|
||
public class SelectDepartmentController {
|
||
|
||
private final static Logger LOGGER = Logger.getLogger(SelectDepartmentController.class.getName());
|
||
private final static String SEVERE_ERROR_MESSAGE = "エラーが発生しました!";
|
||
|
||
HttpSession httpSession;
|
||
SearchDepartment searchDepartment;
|
||
|
||
@Autowired
|
||
public SelectDepartmentController(HttpSession httpSession, SearchDepartment searchDepartment) {
|
||
this.httpSession = httpSession;
|
||
this.searchDepartment = searchDepartment;
|
||
}
|
||
|
||
/** 空の検索条件Formを、この Controller の画面用に用意する */
|
||
@ModelAttribute
|
||
public SearchConditionDepartmentForm searchConditionDepartmentForm() {
|
||
return new SearchConditionDepartmentForm();
|
||
}
|
||
|
||
@RequestMapping(value = TransitionTargetPageNameKeyword.SELECT_DEPARTMENT, method = RequestMethod.POST)
|
||
public String select(SearchConditionDepartmentForm searchConditionDepartmentForm, Model model) {
|
||
try {
|
||
// ★考える:①検索サービスを呼ぶ(Form→Model変換 → 部分一致のメソッド)
|
||
// ②結果(List<Department>)を DepartmentForm.convertFrom(...) で画面用に変換
|
||
// ③ model に PageReturnAttributeKeyword.DEPARTMENT_FORM_LIST という名前で載せる
|
||
|
||
return TransitionTargetPageNameKeyword.SELECT_DEPARTMENT_HTML;
|
||
|
||
} catch (Exception e) {
|
||
httpSession.invalidate();
|
||
e.printStackTrace();
|
||
LOGGER.log(Level.SEVERE, SEVERE_ERROR_MESSAGE, e);
|
||
return TransitionTargetPageNameKeyword.ERROR_HTML;
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
知っておくこと:
|
||
- `@ModelAttribute` のメソッドが返した Form は、`searchConditionDepartmentForm`(クラス名の先頭小文字)という名前で画面に渡ります。これは **この Controller クラスの中だけ**で有効です。
|
||
- `select(SearchConditionDepartmentForm form, …)` の引数 Form は、自動的に同じ名前で画面に渡されます。**だから検索後も入力値が画面に残ります**。
|
||
- メニューからの遷移は**パラメータ無しのPOST**です。条件が `null` や空だと全件が返ります(`Specification` で条件が外れる)。
|
||
|
||
**できたかの確認**:ビルドが通る。
|
||
|
||
### ステップ 1-6:検索画面 HTML を作る
|
||
|
||
`src/main/resources/templates/selectDepartment.html`(お手本:`selectEmployee.html`)
|
||
|
||
お手本の次の部分は**そのまま**使えます:`<head>`(CSS・JS読み込み)、見出しとログイン者名・ログアウトボタン、メッセージ表示、「メニューに戻る」リンク。
|
||
|
||
骨格(`★` の所を、お手本を見て完成させます):
|
||
|
||
```html
|
||
<!DOCTYPE html>
|
||
<html xmlns:th="http://www.thymeleaf.org">
|
||
<head>
|
||
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type">
|
||
<script th:src="@{/js/common.js}"></script>
|
||
<link rel="stylesheet" th:href="@{/css/all.css}">
|
||
<link rel=stylesheet th:href="@{/css/style.css}" type="text/css">
|
||
<title>部署検索</title>
|
||
</head>
|
||
<body>
|
||
<h1>
|
||
部署検索条件入力
|
||
<div class="rightSection">
|
||
<span class="loginInfo">ログイン者名:<span th:text="${session.loginUser.nmEmployee}"></span></span>
|
||
<button class="logoutButton" onclick="logoutSubmit()">ログアウト</button>
|
||
</div>
|
||
</h1>
|
||
|
||
<form action="selectDepartment" method="post" th:object="${searchConditionDepartmentForm}">
|
||
<div class="msgBox error" th:if="${messageError} != null">
|
||
<span class="error" th:text="${messageError.message}"></span>
|
||
</div>
|
||
<table>
|
||
<tr>
|
||
<th class="header">部署ID</th>
|
||
<td><input placeholder="部署ID" th:field="*{idDepartment}" type="number"/></td>
|
||
</tr>
|
||
<tr>
|
||
<th class="header">部署名</th>
|
||
<td><!-- ★部署名の入力欄(type="text")を作る --></td>
|
||
</tr>
|
||
<tr>
|
||
<td class="btnArea" colspan="2"><input type="submit" value="検索"/></td>
|
||
<td class="btnArea" colspan="2"><button onclick="clearInput()">クリア</button></td>
|
||
</tr>
|
||
</table>
|
||
</form>
|
||
|
||
<a href="showMenu">メニューに戻る</a>
|
||
<h1><span>検索結果</span></h1>
|
||
|
||
<!-- ★検索結果が1件以上:表(部署ID・部署名)を th:each で繰り返す。変数は departmentFormList -->
|
||
<!-- ★0件:「検索結果はありません。」を表示 -->
|
||
|
||
</body>
|
||
<script type="text/javascript">
|
||
function clearInput() {
|
||
document.getElementById('idDepartment').value = '';
|
||
// ★部署名の入力欄も空にする(id は th:field が自動で作る)
|
||
}
|
||
</script>
|
||
</html>
|
||
```
|
||
|
||
ヒント:
|
||
- `th:if="${departmentFormList != null and departmentFormList.size() > 0}"` で「1件以上」を判定できます(お手本参照)。
|
||
- `th:each="departmentForm : ${departmentFormList}"` と `th:text="${departmentForm.idDepartment}"`。
|
||
- `th:field="*{idDepartment}"` が何を作るかは、ブラウザで右クリック →「ページのソースを表示」で確認できます(`id` `name` `value` が作られる)。
|
||
|
||
**考えるポイント(クリアボタン)**:`<button>` は `type` を省略すると**送信ボタン**になります。`onclick` で値を空にした**あとに**フォームが送信されます。つまり「クリア=条件を消して全件検索」になります(テスト #35 は「クリア後に全件表示」を期待)。値を消すだけにしたい場合は `type="button"` を付けます。どちらがテスト仕様に合うか、自分で判断して理由をノートに書いてください。
|
||
|
||
### ステップ 1-7:メニューにリンクを追加
|
||
|
||
`menu.html` の `<table class="menuClass">` に、「社員登録」の行の次へ追加します。**JavaScript の関数(`selectDepartmentSubmit()`)は、すでにファイル下部にあります**。
|
||
|
||
```html
|
||
<tr>
|
||
<td><a class="menuClass" href="javascript:selectDepartmentSubmit()"><i class="fas fa-search"></i> 部署検索 </a></td>
|
||
<td class="description">登録済みの部署一覧表示と</br>更新、削除を行います。</td>
|
||
</tr>
|
||
```
|
||
|
||
なぜ `<a href>` で直接飛ばず、JavaScriptでフォームを作って送っているのか考えてください(Controller が POST 専用だから。リンクは GET で送られるので 405 になる)。
|
||
|
||
**できたかの確認**:再ビルド・再起動 → 管理者ログイン → メニュー →「部署検索」で全件が表示される。
|
||
|
||
### ステップ 1-8:動作確認(単体テスト #1〜#8)
|
||
|
||
| # | 操作 | 期待 |
|
||
|---|---|---|
|
||
| 1 | `yamada@hoge.jp` でログイン | メニューに部署検索が出る |
|
||
| 2 | 部署検索を押す | 検索条件と全件の結果 |
|
||
| 3 | 右上のログアウト | ログイン画面 |
|
||
| 4 | クリア → 部署ID `10` → 検索 | `役員` のみ。入力した `10` が残る |
|
||
| 5 | クリア → 部署ID `1000` → 検索 | `検索結果はありません。`+条件が残る |
|
||
| 6 | クリア → 部署名 `営業` → 検索 | `営業企画部` |
|
||
| 7 | クリア → 部署名 `開発` → 検索 | `システム開発事業部` |
|
||
| 8 | 「メニューに戻る」 | メニュー画面 |
|
||
|
||
※更新・削除ボタンは第2・3段階で追加するので、今は表示されなくて構いません(テスト #2 のボタン確認は後で実施)。
|
||
|
||
### 4.1 第1段階の振り返り(説明できればOK)
|
||
|
||
- [ ] 画面→Controller→Service→Repository の流れを説明できる
|
||
- [ ] 部分一致/完全一致をどのメソッドで実現し、なぜそれを選んだか説明できる
|
||
- [ ] `@ModelAttribute` が何のためにあるか説明できる
|
||
|
||
---
|
||
|
||
## 5. 第2段階:削除
|
||
|
||
### 5.0 ゴールと完成の定義
|
||
|
||
検索結果の各行の「削除」→ `本当に削除しますか?` → OK で削除。**その部署に所属する社員が1人でもいれば削除できず**エラー。削除後は一覧(全件)に戻る。
|
||
→ **単体テスト #32〜#35**。
|
||
|
||
### ステップ 2-1:処理の手順をフローチャートにする
|
||
|
||
設計書「イベント処理・部署検索・削除」の⑥を読み、次を埋めてください。
|
||
|
||
```
|
||
削除ボタン押下(部署IDを送信)
|
||
├ 部署IDが無い/その部署が存在しない → ???(メッセージ)→ 一覧へ
|
||
├ その部署に所属する社員が1人以上 → ???(メッセージ)→ 一覧へ
|
||
└ 削除する → ???(メッセージ)→ 一覧へ
|
||
```
|
||
|
||
**考えるポイント**:
|
||
- DBに外部キー制約が無い(DDLに `FOREIGN KEY` が無い)ので、所属社員がいても DB は削除を止めてくれません。**アプリ側で確認**する必要があります。
|
||
- 「存在しないID」はあり得ます(別の画面で削除済み、ブラウザの戻る→再送信 など)。
|
||
|
||
### ステップ 2-2:削除サービスを作る
|
||
|
||
**① インタフェース** `…/domain/service/DeleteDepartment.java`
|
||
|
||
```java
|
||
package com.example.rhizome.app.domain.service;
|
||
|
||
public interface DeleteDepartment {
|
||
void deleteDepartment(Integer idDepartment);
|
||
}
|
||
```
|
||
|
||
**② 実装** `…/domain/service/impl/DeleteDepartmentImpl.java`(お手本:`DeleteEmployeeImpl`)
|
||
|
||
```java
|
||
package com.example.rhizome.app.domain.service.impl;
|
||
|
||
import com.example.rhizome.app.domain.service.DeleteDepartment;
|
||
import com.example.rhizome.app.infra.repository.DepartmentRepository;
|
||
import org.springframework.beans.factory.annotation.Autowired;
|
||
import org.springframework.stereotype.Service;
|
||
import org.springframework.transaction.annotation.Transactional;
|
||
|
||
@Service
|
||
@Transactional
|
||
public class DeleteDepartmentImpl implements DeleteDepartment {
|
||
|
||
DepartmentRepository departmentRepository;
|
||
|
||
@Autowired
|
||
public DeleteDepartmentImpl(DepartmentRepository departmentRepository) {
|
||
this.departmentRepository = departmentRepository;
|
||
}
|
||
|
||
public void deleteDepartment(Integer idDepartment) {
|
||
// ★考える:Repository のどのメソッドで「IDを指定して削除」できる?
|
||
// (お手本の DeleteEmployeeImpl を見る)
|
||
}
|
||
}
|
||
```
|
||
|
||
> **注意**:`@Service` を付け忘れると、Controller で使うとき起動時エラー(`No qualifying bean…`)になります。
|
||
|
||
### ステップ 2-3:削除 Controller を作る
|
||
|
||
`…/presentation/controller/DeleteDepartmentController.java`(お手本:`DeleteEmployeeController`)
|
||
|
||
まず、定数を追加します。
|
||
|
||
```java
|
||
// TransitionTargetPageNameKeyword の Department(Controller) の枠に追記
|
||
public static final String DELETE_DEPARTMENT = "/deleteDepartment";
|
||
```
|
||
|
||
骨格:
|
||
|
||
```java
|
||
@Controller
|
||
public class DeleteDepartmentController {
|
||
|
||
private final static Logger LOGGER = Logger.getLogger(DeleteDepartmentController.class.getName());
|
||
private final static String SEVERE_ERROR_MESSAGE = "エラーが発生しました!";
|
||
|
||
HttpSession httpSession;
|
||
DeleteDepartment deleteDepartment;
|
||
SearchDepartment searchDepartment;
|
||
SearchEmployee searchEmployee; // 所属社員の確認に使う(既存)
|
||
|
||
@Autowired
|
||
public DeleteDepartmentController(HttpSession httpSession, DeleteDepartment deleteDepartment,
|
||
SearchDepartment searchDepartment, SearchEmployee searchEmployee) {
|
||
this.httpSession = httpSession;
|
||
this.deleteDepartment = deleteDepartment;
|
||
this.searchDepartment = searchDepartment;
|
||
this.searchEmployee = searchEmployee;
|
||
}
|
||
|
||
@RequestMapping(value = TransitionTargetPageNameKeyword.DELETE_DEPARTMENT, method = RequestMethod.POST)
|
||
public String deleteDepartment(@RequestParam(value = "idDepartment", required = false) Integer idDepartment, Model model) {
|
||
|
||
final String UNKNOWN_ID = "削除対象のIDが不明です。";
|
||
final String HAS_EMPLOYEE = "1名以上が所属している部署は削除出来ません。";
|
||
final String DELETE_COMPLETE = "削除しました。";
|
||
|
||
try {
|
||
// ★① idDepartment が null、または searchDepartment.searchBy(new SearchConditionDepartment(idDepartment, null)) が空
|
||
// → MESSAGE_ERROR に UNKNOWN_ID を載せて一覧へ
|
||
// ★② searchEmployee.searchEmployeeByIdDepartment(idDepartment) が空でない
|
||
// → MESSAGE_ERROR に HAS_EMPLOYEE を載せて一覧へ
|
||
// ★③ deleteDepartment.deleteDepartment(idDepartment) → MESSAGE_INFO に DELETE_COMPLETE を載せて一覧へ
|
||
|
||
return "一覧へ戻る(ステップ 2-5 で決める)";
|
||
} catch (Exception e) {
|
||
httpSession.invalidate();
|
||
e.printStackTrace();
|
||
LOGGER.log(Level.SEVERE, SEVERE_ERROR_MESSAGE, e);
|
||
return TransitionTargetPageNameKeyword.ERROR_HTML;
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
メッセージの載せ方(お手本のとおり):
|
||
```java
|
||
model.addAttribute(PageReturnAttributeKeyword.MESSAGE_ERROR, new MessageForm(UNKNOWN_ID));
|
||
```
|
||
**確認の順序は「存在 → 所属社員 → 削除」**です(削除してから確認しても意味がありません)。
|
||
|
||
### ステップ 2-4:「削除後にどの画面へ戻るか」を考える(重要)
|
||
|
||
**課題**:削除後は検索一覧(全件)を表示したい。でも、`return "selectDepartment";`(画面名で返す)だと画面が壊れます。なぜでしょう?
|
||
|
||
調べるヒント:
|
||
- 検索画面の `th:object="${searchConditionDepartmentForm}"` は、**どの Controller が用意している**?
|
||
- `@ModelAttribute` はどのクラスで有効だった?(ステップ 1-5)
|
||
|
||
→ そこで、**検索 Controller に処理を引き継ぐ `forward:`** を使います(お手本の `DeleteEmployeeController` を参照)。
|
||
|
||
```java
|
||
return "forward:" + TransitionTargetPageNameKeyword.【どの定数?】;
|
||
```
|
||
|
||
**ここに罠があります**:`forward:` は**元のリクエストのパラメータを引き継ぎます**。削除ボタンは `idDepartment=10` を送っているので、これを `/selectDepartment` に forward すると、「部署ID=10 で検索」が実行されてしまいます(実際に確認済みの動作です)。
|
||
|
||
解決方法は、お手本が `SelectEmployeeController` に**2つ目の `select`(引数に Form を持たない版)**を持っている理由と同じです。部署でも、**パラメータを一切使わず全件を返す、もう1つの入口**を作ります。
|
||
|
||
1. 定数を追加:
|
||
```java
|
||
public static final String SELECT_DEPARTMENT_FORM_EMPTY = "/selectDepartmentAll";
|
||
```
|
||
2. `SelectDepartmentController` に2つ目のメソッドを追加:
|
||
```java
|
||
@RequestMapping(value = TransitionTargetPageNameKeyword.SELECT_DEPARTMENT_FORM_EMPTY, method = RequestMethod.POST)
|
||
public String select(Model model) {
|
||
try {
|
||
// ★全件取得 → DepartmentForm に変換 → DEPARTMENT_FORM_LIST で model へ
|
||
return TransitionTargetPageNameKeyword.SELECT_DEPARTMENT_HTML;
|
||
} catch (Exception e) {
|
||
httpSession.invalidate();
|
||
e.printStackTrace();
|
||
LOGGER.log(Level.SEVERE, SEVERE_ERROR_MESSAGE, e);
|
||
return TransitionTargetPageNameKeyword.ERROR_HTML;
|
||
}
|
||
}
|
||
```
|
||
3. 削除 Controller の戻り先を `forward:/selectDepartmentAll`(定数 `SELECT_DEPARTMENT_FORM_EMPTY` を使う)にする。
|
||
|
||
**なぜ `forward:` ならメッセージが画面に出るのか**:model に入れた値は request の属性として公開され、forward でも同じリクエストなので残るためです。
|
||
|
||
### ステップ 2-5:一覧にメッセージ表示と削除ボタンを足す
|
||
|
||
`selectDepartment.html` に追加します。
|
||
|
||
**① 情報メッセージ(アラート)**:お手本の `selectEmployee.html` の次の部分を、同じように入れる(`</h1>` の「検索結果」見出しの後あたり)。
|
||
|
||
```html
|
||
<th:block th:if="${messageInfo != null}">
|
||
<script th:inline="javascript">
|
||
alert([[${messageInfo.message}]]);
|
||
</script>
|
||
</th:block>
|
||
```
|
||
|
||
**② 結果テーブルに「操作」列と削除ボタン**:
|
||
|
||
```html
|
||
<th style="width: 150px;">操作</th> <!-- thead に追加 -->
|
||
|
||
<td> <!-- tbody の各行に追加 -->
|
||
<form action="deleteDepartment" method="post" onsubmit="return confirm('本当に削除しますか?');"
|
||
style="display: inline;">
|
||
<input name="idDepartment" th:value="${departmentForm.idDepartment}" type="hidden">
|
||
<input type="submit" value="削除">
|
||
</form>
|
||
</td>
|
||
```
|
||
|
||
考えるポイント:
|
||
- なぜ `<a>` ではなく `<form method="post">`?(削除のような「データを変える操作」を、URLを開くだけで実行される GET にしない)
|
||
- `hidden` の `name="idDepartment"` と、Controller の `@RequestParam("idDepartment")` は**一致している必要がある**。
|
||
|
||
### ステップ 2-6:動作確認
|
||
|
||
| # | 操作 | 期待 |
|
||
|---|---|---|
|
||
| 32 | 部署名`テスト`で検索 → 対象の削除を押す(※テスト用の部署が無ければ、第4段階の後にやるか、DBに直接 `INSERT INTO department (nm_department) VALUES ('テスト部署2');` を1行追加) | `本当に削除しますか?` |
|
||
| 33 | 〃 → キャンセル | 何も起きない |
|
||
| 34 | 〃 → OK | 削除され `削除しました。` |
|
||
| 35 | クリア(全件)→ `役員` の削除 → OK | `1名以上が所属している部署は削除出来ません。` |
|
||
|
||
追加で自分で確認(異常系):
|
||
- ブラウザの開発者ツール(F12)で hidden の値を `999` に書き換えて削除 →「削除対象のIDが不明です。」
|
||
- 削除直後にブラウザの「戻る」→ 再送信 → エラー画面にならず、メッセージが出る
|
||
|
||
### 5.1 第2段階の振り返り
|
||
|
||
- [ ] なぜ所属社員の確認をアプリで行う必要があるか説明できる
|
||
- [ ] なぜ戻り先を `forward:` の全件用エンドポイントにしたか(パラメータ引き継ぎ)説明できる
|
||
- [ ] 削除を POST にする理由を説明できる
|
||
|
||
---
|
||
|
||
## 6. 第3段階:更新(提出可能ライン)
|
||
|
||
### 6.0 ゴールと完成の定義
|
||
|
||
一覧の「更新」→ 更新画面(部署IDは変更不可)→ 部署名を変更 →「本当によろしいですか?」→ 成功時 `更新が完了しました。` で一覧へ。
|
||
→ **単体テスト #9〜#20**。
|
||
|
||
### ステップ 3-1:画面と処理を分解する
|
||
|
||
```
|
||
[一覧]「更新」押下 ─ POST /showUpdateDepartment(idDepartment)
|
||
├ 対象が存在しない → 「更新対象のデータが見つかりません。」→ 一覧
|
||
└ 存在する → [更新画面] に現在の値を表示(部署IDは変更不可)
|
||
|
||
[更新画面]「更新」押下 → 確認ダイアログ → POST /updateDepartmentResult
|
||
├ 部署名が空欄 → 「項目が空欄です。」→ 更新画面
|
||
├ 同じ名前の部署がある(自分と同じ名前=変更なし も含む)
|
||
│ → 「既に同名の部署が存在するか、値が変更されていません。」→ 更新画面
|
||
├ 更新対象が存在しない → 「更新対象のデータが見つかりません。」→ 一覧
|
||
└ 更新 → 「更新が完了しました。」(アラート) → 一覧
|
||
```
|
||
|
||
### ステップ 3-2:定数を追加
|
||
|
||
```java
|
||
// TransitionTargetPageNameKeyword
|
||
public static final String UPDATE_DEPARTMENT_INPUT_HTML = "updateDepartmentInput"; // HTML
|
||
public static final String SHOW_UPDATE_DEPARTMENT = "/showUpdateDepartment"; // Controller
|
||
public static final String UPDATE_DEPARTMENT_RESULT = "/updateDepartmentResult";
|
||
|
||
// PageReturnAttributeKeyword
|
||
public static final String UPDATE_DEPARTMENT_FORM = "updateDepartmentForm";
|
||
```
|
||
|
||
### ステップ 3-3:更新 Form
|
||
|
||
`…/presentation/form/UpdateDepartmentForm.java`
|
||
|
||
```java
|
||
package com.example.rhizome.app.presentation.form;
|
||
|
||
import com.example.rhizome.app.domain.model.Department;
|
||
import lombok.AllArgsConstructor;
|
||
import lombok.Data;
|
||
import lombok.NoArgsConstructor;
|
||
|
||
@Data
|
||
@AllArgsConstructor
|
||
@NoArgsConstructor
|
||
public class UpdateDepartmentForm {
|
||
|
||
private Integer idDepartment;
|
||
private String nmDepartment;
|
||
|
||
/** Model(Department) → Form */
|
||
public static UpdateDepartmentForm convertFrom(Department department) {
|
||
return new UpdateDepartmentForm(department.getIdDepartment(), department.getNmDepartment());
|
||
}
|
||
|
||
/** Form → Model(Department) */
|
||
public static Department convertTo(UpdateDepartmentForm form) {
|
||
return new Department(form.getIdDepartment(), form.getNmDepartment());
|
||
}
|
||
|
||
/** 必須チェック。true:OK/false:部署ID か部署名が空 */
|
||
public boolean hasRequiredValue() {
|
||
// ★考える:idDepartment が null でなく、nmDepartment が null でも空白だけでもない
|
||
return false;
|
||
}
|
||
}
|
||
```
|
||
|
||
> `String` の空チェックには `isBlank()`(空白だけも空扱い)が便利です。
|
||
|
||
### ステップ 3-4:更新サービス
|
||
|
||
`…/domain/service/UpdateDepartment.java`
|
||
|
||
```java
|
||
package com.example.rhizome.app.domain.service;
|
||
|
||
import com.example.rhizome.app.domain.model.Department;
|
||
|
||
public interface UpdateDepartment {
|
||
void updateDepartment(Department department);
|
||
}
|
||
```
|
||
|
||
`…/domain/service/impl/UpdateDepartmentImpl.java`
|
||
|
||
```java
|
||
@Service
|
||
@Transactional
|
||
public class UpdateDepartmentImpl implements UpdateDepartment {
|
||
|
||
DepartmentRepository departmentRepository;
|
||
|
||
@Autowired
|
||
public UpdateDepartmentImpl(DepartmentRepository departmentRepository) {
|
||
this.departmentRepository = departmentRepository;
|
||
}
|
||
|
||
public void updateDepartment(Department department) {
|
||
// ★考える:Model を Entity に変換(DepartmentEntity.convertFrom)して、Repository の save に渡す
|
||
}
|
||
}
|
||
```
|
||
|
||
import 文は削除サービスを参考に自分で書いてください(IDE の「インポートの整理」が使えます)。
|
||
|
||
> **重要な落とし穴**:`save` は**IDがDBに無い(または null)だと更新ではなく新規追加(INSERT)**をします。後のステップで「存在確認してから呼ぶ」ようにします。
|
||
|
||
### ステップ 3-5:更新画面を表示する Controller
|
||
|
||
`…/presentation/controller/UpdateDepartmentController.java`(お手本:`UpdateEmployeeController`)
|
||
|
||
```java
|
||
@Controller
|
||
public class UpdateDepartmentController {
|
||
|
||
// LOGGER、SEVERE_ERROR_MESSAGE、httpSession、searchDepartment、コンストラクタ は他と同じ作り
|
||
|
||
@RequestMapping(value = TransitionTargetPageNameKeyword.SHOW_UPDATE_DEPARTMENT, method = RequestMethod.POST)
|
||
public String showUpdateDepartment(@RequestParam("idDepartment") Integer idDepartment, Model model) {
|
||
final String UPDATE_TARGET_ID_NOT_FOUND = "更新対象のデータが見つかりません。";
|
||
|
||
try {
|
||
// ★① searchDepartment.searchBy(new SearchConditionDepartment(idDepartment, null)) で対象を検索
|
||
// ★② 結果が空なら MESSAGE_ERROR を載せて "forward:" + SELECT_DEPARTMENT_FORM_EMPTY
|
||
// ★③ あれば UpdateDepartmentForm.convertFrom(結果の先頭) を
|
||
// UPDATE_DEPARTMENT_FORM という名前で model に載せ、UPDATE_DEPARTMENT_INPUT_HTML を返す
|
||
|
||
return "…";
|
||
} catch (Exception e) {
|
||
// 他と同じ
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### ステップ 3-6:更新画面 HTML
|
||
|
||
`updateDepartmentInput.html`(お手本:`updateEmployeeInput.html`)
|
||
|
||
```html
|
||
<!-- head・見出し・ログイン者名・ログアウトは他の画面と同じ。title は「部署情報更新」 -->
|
||
<form action="updateDepartmentResult"
|
||
method="post"
|
||
onsubmit="return confirm('本当によろしいですか?');"
|
||
th:object="${updateDepartmentForm}">
|
||
|
||
<div class="msgBox error" th:if="${messageError != null}">
|
||
<span class="error" th:text="${messageError.message}"></span>
|
||
</div>
|
||
<table>
|
||
<tr>
|
||
<th class="header">部署ID</th>
|
||
<td>
|
||
<input class="readOnly" readonly="readonly" th:field="*{idDepartment}" type="text"/>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<th class="header">部署名</th>
|
||
<td>
|
||
<input maxlength="50" placeholder="部署名" required="required"
|
||
th:field="*{nmDepartment}" type="text"/>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td class="btnArea" colspan="2"><input type="submit" value="更新"/></td>
|
||
</tr>
|
||
</table>
|
||
</form>
|
||
<a href="javascript:selectDepartmentSubmit()">一覧に戻る</a>
|
||
<script type="text/javascript">
|
||
function selectDepartmentSubmit() {
|
||
const form = document.createElement('form');
|
||
form.setAttribute('method', 'post');
|
||
form.setAttribute('action', 'selectDepartment');
|
||
document.body.appendChild(form);
|
||
form.submit();
|
||
}
|
||
</script>
|
||
```
|
||
|
||
考えるポイント:
|
||
- **`readonly` と `disabled` の違い**:`readonly` は編集不可だが**送信される**。`disabled` は**送信されない**。部署IDを `disabled` にすると ID が `null` で届き、更新が新規追加になってしまいます。開発者ツール(F12)の Network タブで、実際に送られる値を比べてみてください。
|
||
- 「一覧に戻る」は、検索の Controller が POST 専用なのでメニューと同じ方法(JavaScriptでフォームを作って送信)にしています。
|
||
- `action` は `updateDepartmentResult`(先頭に `/` を付けない相対パス)。
|
||
|
||
### ステップ 3-7:更新を実行する Controller(最大の山場)
|
||
|
||
`…/presentation/controller/UpdateDepartmentResultController.java`(お手本:`UpdateEmployeeResultController`)
|
||
|
||
```java
|
||
@Controller
|
||
public class UpdateDepartmentResultController {
|
||
|
||
// LOGGER、SEVERE_ERROR_MESSAGE、httpSession、
|
||
// searchDepartment(SearchDepartment)、updateDepartment(UpdateDepartment) とコンストラクタ
|
||
|
||
@RequestMapping(value = TransitionTargetPageNameKeyword.UPDATE_DEPARTMENT_RESULT, method = RequestMethod.POST)
|
||
public String departmentUpdateResult(@ModelAttribute UpdateDepartmentForm updateDepartmentForm, Model model) {
|
||
|
||
final String NOTHING = "項目が空欄です。";
|
||
final String DUPLICATION = "既に同名の部署が存在するか、値が変更されていません。";
|
||
final String NO_TARGET = "更新対象のデータが見つかりません。";
|
||
final String UPDATE_COMPLETE = "更新が完了しました。";
|
||
|
||
try {
|
||
// ★ここに「チェック→更新」を、下の【チェックの順序】どおりに書く
|
||
return "…";
|
||
} catch (Exception e) {
|
||
// 他と同じ
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
**【チェックの順序】**(順番が大切です。理由も考えてください)
|
||
|
||
| 順 | チェック | NGの場合 | 戻り先 |
|
||
|---|---|---|---|
|
||
| 1 | 必須(`hasRequiredValue()`) | `NOTHING` | 更新画面(`UPDATE_DEPARTMENT_INPUT_HTML`) |
|
||
| 2 | 更新後の部署名が既に存在する(`searchBy(new SearchConditionDepartment(null, 更新後の部署名))` が空でない) | `DUPLICATION` | 更新画面 |
|
||
| 3 | 更新対象IDが存在する(`searchBy(new SearchConditionDepartment(ID, null))` が空でない) | `NO_TARGET` | 一覧(forward) |
|
||
| 4 | 更新を実行 → `MESSAGE_INFO` に `UPDATE_COMPLETE` | | 一覧(forward) |
|
||
|
||
**なぜ必須チェックが先か**:部署名が空のまま「同名検索」を行うと、条件が無視されて**全件ヒット**してしまい、「同名の部署が存在する」と誤判定されるからです。(設計書は重複確認→必須確認の順に書かれていますが、この順で実装すると上記の問題が出ます。)
|
||
|
||
**「変更なし」も重複エラーになる理由**:変更しない(自分と同じ名前)で検索すると、自分自身がヒットします。そのため、同名あり・変更なしを1つの判定でまとめて捕まえられます(設計書のメッセージが1つにまとまっている理由)。
|
||
|
||
**エラー時に更新画面へ戻すとき**は、入力済みの値を残すために、次の2つを載せます:
|
||
|
||
```java
|
||
model.addAttribute(PageReturnAttributeKeyword.MESSAGE_ERROR, new MessageForm(DUPLICATION));
|
||
model.addAttribute(PageReturnAttributeKeyword.UPDATE_DEPARTMENT_FORM, updateDepartmentForm);
|
||
return TransitionTargetPageNameKeyword.UPDATE_DEPARTMENT_INPUT_HTML;
|
||
```
|
||
|
||
**ブラウザの `required` があるのに、サーバー側でも必須チェックする理由**:ブラウザの検証は、開発者ツールで簡単に回避できるためです。
|
||
|
||
### ステップ 3-8:一覧に「更新」ボタンを追加
|
||
|
||
`selectDepartment.html` の操作列(削除ボタンと同じ `<td>` の中)に追加:
|
||
|
||
```html
|
||
<form action="showUpdateDepartment" method="post" style="display: inline;">
|
||
<input name="idDepartment" th:value="${departmentForm.idDepartment}" type="hidden">
|
||
<input type="submit" value="更新">
|
||
</form>
|
||
```
|
||
|
||
### ステップ 3-9:動作確認(単体テスト #9〜#20)
|
||
|
||
| # | 操作 | 期待 |
|
||
|---|---|---|
|
||
| 9 | 部署ID `10` の「更新」 | 部署ID変更不可で `10`/`役員` |
|
||
| 10 | 何も変えずに更新 → OK | `既に同名の部署が存在するか、値が変更されていません。` |
|
||
| 12 | 部署名を `役員改` にして更新 → OK | `更新が完了しました。` → 一覧に反映 |
|
||
| 14 | 部署名を `人事・総務部` にして更新 → OK | 同名エラー |
|
||
| 15 | 「一覧に戻る」 | 変更されていない一覧 |
|
||
| 17 | 部署名を空欄で更新 | ブラウザの「このフィールドを入力してください。」(画面遷移なし) |
|
||
| 18 | 「一覧に戻る」 | 検索画面 |
|
||
| 19 | 更新画面で右上のログアウト | ログイン画面 |
|
||
| 20 | 部署名を `役員` に戻して更新 | `更新が完了しました。` |
|
||
|
||
追加で自分で確認(異常系):
|
||
- F12 で `required` を削除して空欄のまま送信 →「項目が空欄です。」
|
||
- F12 で ID を `999` に書き換えて送信 →「更新対象のデータが見つかりません。」(**DBに新しい行が増えていないこと**)
|
||
|
||
### 6.1 結合テスト(部署更新)
|
||
|
||
結合テスト仕様書の「ログインから部署検索、部署更新まで」:部署名を `役員` → `役員改` に変更し、社員検索画面で、もともと `役員` だった社員の所属部署が `役員改` と表示される。(社員テーブルは部署名ではなく部署IDだけを持ち、表示するときに部署テーブルと結合しているためです。)
|
||
※手順3の「社員ID `10`」は「部署ID `10`」の誤記と思われます。
|
||
|
||
### 6.2 ここまでが提出可能ライン — 振り返り
|
||
|
||
- [ ] 単体テスト #1〜#20、#32〜#35 が期待どおり
|
||
- [ ] 更新の検証順序の理由を説明できる
|
||
- [ ] `save` が新規追加になる条件を説明できる
|
||
- [ ] `readonly` と `disabled` の違いを説明できる
|
||
|
||
---
|
||
|
||
## 7. 第4段階:新規登録(チャレンジ)
|
||
|
||
### 7.0 ゴールと完成の定義
|
||
|
||
メニュー「部署登録」→ 入力 → 登録確認 → 登録 → 完了画面(`部署登録が完了しました`)。確認画面の「登録内容の編集」で、値を保ったまま入力画面に戻れる。
|
||
→ **単体テスト #21〜#31**、結合テストの登録シナリオ。
|
||
|
||
画面フロー:
|
||
|
||
```
|
||
[入力] 部署ID(自動で設定されます)/部署名 ──「登録確認」──▶ POST /insertDepartmentConfirm
|
||
[確認] すべて変更不可 ──「登録」──▶ POST /insertDepartment ──▶ [完了]
|
||
──「登録内容の編集」──▶ POST /insertDepartmentInput(値を保持)
|
||
[完了]「続けて登録する」──▶ POST /insertDepartmentInput(空)/「メニューに戻る」
|
||
```
|
||
|
||
### ステップ 4-1:定数を追加
|
||
|
||
```java
|
||
// TransitionTargetPageNameKeyword
|
||
public static final String INSERT_DEPARTMENT_INPUT_HTML = "insertDepartmentInput";
|
||
public static final String INSERT_DEPARTMENT_CONFIRM_HTML = "insertDepartmentConfirm";
|
||
public static final String INSERT_DEPARTMENT_RESULT_HTML = "insertDepartmentResult";
|
||
public static final String INSERT_DEPARTMENT_INPUT = "/insertDepartmentInput";
|
||
public static final String INSERT_DEPARTMENT_CONFIRM = "/insertDepartmentConfirm";
|
||
public static final String INSERT_DEPARTMENT = "/insertDepartment";
|
||
|
||
// PageReturnAttributeKeyword
|
||
public static final String INSERT_DEPARTMENT_FORM = "insertDepartmentForm";
|
||
```
|
||
|
||
### ステップ 4-2:登録 Form
|
||
|
||
`…/presentation/form/InsertDepartmentForm.java`
|
||
|
||
```java
|
||
@Data
|
||
@AllArgsConstructor
|
||
@NoArgsConstructor
|
||
public class InsertDepartmentForm {
|
||
|
||
private String nmDepartment;
|
||
|
||
/** Form → Model。部署IDは自動採番なので null を渡す */
|
||
public static Department convertTo(InsertDepartmentForm form) {
|
||
return new Department(null, form.getNmDepartment());
|
||
}
|
||
|
||
/** 入力チェック。エラーのあった項目名のリストを返す(なければ空のリスト) */
|
||
public List<String> validateParameter() {
|
||
List<String> error = new ArrayList<>();
|
||
// ★部署名が null または空白だけなら error.add("部署名");
|
||
return error;
|
||
}
|
||
}
|
||
```
|
||
|
||
`InsertEmployeeForm.validateParameter()` が書き方のお手本です(`java.util.List` と `ArrayList` の import も忘れずに)。
|
||
|
||
### ステップ 4-3:登録サービス(起動エラーを体験・解決する)
|
||
|
||
`RegisterDepartment`(インタフェース)は**すでにありますが、実装クラスがありません**。この状態で Controller に使うと、**アプリの起動に失敗**し、次のようなエラーが出ます。
|
||
|
||
```
|
||
No qualifying bean of type '…RegisterDepartment' available
|
||
```
|
||
|
||
**考える**:このエラーは何を伝えている?(Spring が `RegisterDepartment` を実装した `@Service` を探したが無かった)
|
||
|
||
`…/domain/service/impl/RegisterDepartmentImpl.java`(お手本:`RegisterEmployeeImpl`)
|
||
|
||
```java
|
||
@Service
|
||
@Transactional
|
||
public class RegisterDepartmentImpl implements RegisterDepartment {
|
||
|
||
DepartmentRepository departmentRepository;
|
||
|
||
@Autowired
|
||
public RegisterDepartmentImpl(DepartmentRepository departmentRepository) {
|
||
this.departmentRepository = departmentRepository;
|
||
}
|
||
|
||
public void registerDepartment(Department department) {
|
||
// ★考える:Entity に変換して save。ID が null のとき save は何をする?(自動採番して追加)
|
||
}
|
||
}
|
||
```
|
||
|
||
### ステップ 4-4:3つの Controller
|
||
|
||
共通の約束:`@ModelAttribute` で空の `InsertDepartmentForm` を用意し(`insertDepartmentForm()` メソッド)、各メソッドの引数に `InsertDepartmentForm` を取ります。
|
||
|
||
**① `InsertDepartmentInputController`**(`/insertDepartmentInput`)
|
||
|
||
```java
|
||
@RequestMapping(value = TransitionTargetPageNameKeyword.INSERT_DEPARTMENT_INPUT, method = RequestMethod.POST)
|
||
public String insertInput(InsertDepartmentForm insertDepartmentForm, Model model) {
|
||
model.addAttribute(PageReturnAttributeKeyword.INSERT_DEPARTMENT_FORM, insertDepartmentForm);
|
||
return TransitionTargetPageNameKeyword.INSERT_DEPARTMENT_INPUT_HTML;
|
||
}
|
||
```
|
||
|
||
なぜ引数の Form を model に載せ直す?(確認画面の「登録内容の編集」から戻ったとき、入力済みの値を残すため。メニューから来たときは空の Form)
|
||
|
||
**② `InsertDepartmentConfirmController`**(`/insertDepartmentConfirm`)
|
||
|
||
| 順 | チェック | NGのとき |
|
||
|---|---|---|
|
||
| 1 | `insertDepartmentForm.validateParameter()` が空でない | メッセージ `String.join(", ", error) + "が不正です。"`(=`部署名が不正です。`)→ 入力画面 |
|
||
| 2 | 同名の部署がある(`searchBy(new SearchConditionDepartment(null, 部署名))` が空でない) | `既に同名の部署が存在します。` → 入力画面 |
|
||
| 3 | OK | 確認画面 `INSERT_DEPARTMENT_CONFIRM_HTML` |
|
||
|
||
エラー時も OK 時も、`INSERT_DEPARTMENT_FORM` で Form を model に載せます(値を画面に残すため)。コンストラクタは `HttpSession` と `SearchDepartment` を受け取ります。
|
||
|
||
**③ `InsertDepartmentController`**(`/insertDepartment`)
|
||
|
||
①と同じチェック(入力・重複)を**もう一度**行い、問題なければ `registerDepartment.registerDepartment(InsertDepartmentForm.convertTo(form))` を呼んで、`INSERT_DEPARTMENT_RESULT_HTML` を返します。コンストラクタは `HttpSession`、`RegisterDepartment`、`SearchDepartment`。
|
||
|
||
**考える**:確認画面でチェック済みなのに、なぜ登録時に再チェック?(確認画面を通らずに直接送られる/確認と登録の間に別の人が同名を登録することがある)。完了画面でブラウザを更新(F5)すると、もう一度送信されます。再チェックが無いと**二重登録**になってしまいます。
|
||
|
||
### ステップ 4-5:3つの HTML
|
||
|
||
お手本の `insertEmployeeInput.html` / `insertEmployeeConfirm.html` / `insertEmployeeResult.html` を見ながら作ります。**部署に不要な項目(社員名・パスワード・権限・所属部署など)はすべて取り除いてください。**
|
||
|
||
**`insertDepartmentInput.html`**(要点)
|
||
|
||
```html
|
||
<form action="insertDepartmentConfirm" method="post" th:object="${insertDepartmentForm}">
|
||
<!-- エラーメッセージ表示(他の画面と同じ) -->
|
||
<table>
|
||
<tr>
|
||
<th class="header">部署ID</th>
|
||
<td><input class="readOnly" readonly="readonly" type="text" value="自動で設定されます"/></td>
|
||
</tr>
|
||
<tr>
|
||
<th class="header">部署名</th>
|
||
<td><input maxlength="50" placeholder="部署名" required="required"
|
||
th:field="*{nmDepartment}" type="text"/></td>
|
||
</tr>
|
||
<tr><td class="btnArea" colspan="2"><input type="submit" value="登録確認"/></td></tr>
|
||
</table>
|
||
</form>
|
||
<a href="showMenu">メニューに戻る</a>
|
||
```
|
||
|
||
**`insertDepartmentConfirm.html`**(要点)
|
||
|
||
```html
|
||
<form action="insertDepartment" id="insertForm" method="post" th:object="${insertDepartmentForm}">
|
||
<table>
|
||
<tr>
|
||
<th class="header">部署ID</th>
|
||
<td><input class="readOnly" readonly="readonly" type="text" value="自動で設定されます"/></td>
|
||
</tr>
|
||
<tr>
|
||
<th class="header">部署名</th>
|
||
<td><input class="readOnly" name="nmDepartment" readonly="readonly"
|
||
th:value="*{nmDepartment}" type="text"/></td>
|
||
</tr>
|
||
<tr><td class="btnArea" colspan="2"><input type="submit" value="登録"/></td></tr>
|
||
</table>
|
||
<a href="javascript:onClickEdit()">登録内容の編集</a> | <a href="showMenu">メニューに戻る</a>
|
||
</form>
|
||
<script type="text/javascript">
|
||
function onClickEdit() {
|
||
const form = document.getElementById('insertForm');
|
||
form.setAttribute('method', 'post');
|
||
form.setAttribute('action', 'insertDepartmentInput');
|
||
form.submit();
|
||
}
|
||
</script>
|
||
```
|
||
|
||
`name="nmDepartment"` が付いているので、画面の値がそのまま次の POST で送られます。「登録内容の編集」は、同じフォームの送信先だけを `insertDepartmentInput` に切り替えているので、値が入力画面に戻ります。
|
||
|
||
**`insertDepartmentResult.html`**(要点)
|
||
|
||
```html
|
||
<h2>部署登録が完了しました</h2>
|
||
<a href="javascript:insertSubmit()">続けて登録する</a> | <a href="showMenu">メニューに戻る</a>
|
||
<script type="text/javascript">
|
||
function insertSubmit() {
|
||
const form = document.createElement('form');
|
||
form.setAttribute('method', 'post');
|
||
form.setAttribute('action', 'insertDepartmentInput');
|
||
document.body.appendChild(form);
|
||
form.submit();
|
||
}
|
||
</script>
|
||
```
|
||
|
||
(`<head>` は他の画面と同じ内容にしてください。)
|
||
|
||
### ステップ 4-6:メニューにリンク追加
|
||
|
||
```html
|
||
<tr>
|
||
<td><a class="menuClass" href="javascript:insertDepartmentSubmit()"><i class="fas fa-plus"></i> 部署登録 </a></td>
|
||
<td class="description">新規で部署追加を行います。</td>
|
||
</tr>
|
||
```
|
||
|
||
### ステップ 4-7:部署名の桁数を確認する(重要)
|
||
|
||
**実験**:部署名に **11文字以上**(例:`ABCDEFGHIJK`)を入れて登録してみてください。
|
||
|
||
- 期待されるのは、設計書の「1〜50文字」で登録できること。
|
||
- しかし、DDL は `nm_department VARCHAR(10)` のため、MySQL のエラー(`Data too long for column 'nm_department'`)になり、**エラー画面に移動してログアウト状態**になります(Controller の `catch` がセッションを破棄するため)。
|
||
|
||
**これは設計書と実際のDBの食い違い**です。あなたの実装のミスではありません。README の「問題が出そうな場合は、必ずルールにしたがって適切な相手に相談してください」に従い、**講師に報告して判断を仰いでください**。報告のしかた(例):
|
||
|
||
> 「部署名に11文字以上を入れると登録でDBエラーになります。テーブル定義書は `varchar(50)`、DDL は `VARCHAR(10)` です。再現手順は…、ログの該当行は…。どちらに合わせればよいでしょうか」
|
||
|
||
(講師から DDL の変更や `maxlength` の指示があれば、それに従ってください。)
|
||
|
||
### ステップ 4-8:動作確認(単体テスト #21〜#31)
|
||
|
||
| # | 操作 | 期待 |
|
||
|---|---|---|
|
||
| 21 | メニュー「部署登録」 | 部署IDは「自動で設定されます」、部署名は空欄 |
|
||
| 22, 23 | メニューに戻る/ログアウト | それぞれ遷移する |
|
||
| 24 | `テスト部署1` → 登録確認 | 確認画面に表示される |
|
||
| 25 | 「登録内容の編集」 | 入力画面に `テスト部署1` が残っている |
|
||
| 26 | `役員` → 登録確認 | `既に同名の部署が存在します。` |
|
||
| 27 | 確認画面で「メニューに戻る」 | メニュー画面 |
|
||
| 28 | `テスト部署1` → 登録確認 →「登録」 | `部署登録が完了しました` |
|
||
| 29 | 「続けて登録する」 | 入力画面(空) |
|
||
| 30 | `テスト部署1` → 登録確認 | 同名エラー |
|
||
| 31 | `テスト部署2` を登録 → 完了 → メニューに戻る | 登録できる |
|
||
|
||
### 7.1 結合テスト
|
||
|
||
結合テスト仕様書の部署関連シナリオ(部署の削除と登録、部署を使った社員登録、社員の異動後の部署削除)を実施します。
|
||
※結合テスト1件目の「`sugi@hoge.jp` でログイン」は、`sugi` が一般ユーザーのため社員登録ができません。管理者(`yamada@hoge.jp`)で実施してください。
|
||
|
||
### 7.2 第4段階の振り返り
|
||
|
||
- [ ] 確認画面から「登録内容の編集」で値が戻る仕組みを説明できる
|
||
- [ ] 登録時に再チェックする理由を説明できる
|
||
- [ ] 起動エラー(`No qualifying bean`)の意味を説明できる
|
||
|
||
---
|
||
|
||
## 8. 第5段階:アクセス制御アノテーション(チャレンジその2)
|
||
|
||
### 8.0 ゴール
|
||
|
||
第1〜4段階のすべての Controller メソッドに既存の `@SessionCheck` と `@PermissionCheck` を付け、**未ログインや一般ユーザーが部署画面に入れない**ようにします。
|
||
|
||
### ステップ 5-1:既存の仕組みを読み解く(README:機能を解析する)
|
||
|
||
次の順に読み、それぞれ**自分の言葉で**ノートに説明を書いてください。
|
||
|
||
1. `aop/aspect/SessionCheck.java` と `PermissionCheck.java`:アノテーションの定義(`@Target(METHOD)`、`@Retention(RUNTIME)` の意味)
|
||
2. `aop/aspect/SessionCheckAspect.java`:`@Pointcut`、`@Before`。ログイン情報がセッションに無いとどうなる?
|
||
3. `aop/aspect/PermissionCheckAspect.java`:リクエストのURLを何と比べている?(`TransitionTargetPageNameKeyword` の `getAdminPageList()` / `getUserPageList()`)
|
||
4. `aop/Handler/GlobalExceptionHandler.java`:投げられた例外を受けて、どの画面に何を表示している?
|
||
5. 既存の `MenuController`:アノテーションがどこに付いている?
|
||
|
||
> **README の注意「アノテーションをコピペしない」** の意味:アノテーションのクラス自体を別の場所にコピーして使うと、チェックが**一切動作しません(エラーも出ません)**。`SessionCheckAspect` の `@Pointcut` は「`com.example.rhizome.aop.aspect.SessionCheck` というクラスのアノテーション」を指定しているためです。**必ず既存のものを `import` して使ってください。**
|
||
|
||
### ステップ 5-2:付与する
|
||
|
||
全 Controller の `@RequestMapping` メソッドに、お手本(社員機能)と同じように2つを付けます(検索の2つの `select` メソッドも両方)。
|
||
|
||
```java
|
||
import com.example.rhizome.aop.aspect.PermissionCheck;
|
||
import com.example.rhizome.aop.aspect.SessionCheck;
|
||
|
||
@PermissionCheck
|
||
@SessionCheck
|
||
@RequestMapping(value = …, method = RequestMethod.POST)
|
||
public String …
|
||
```
|
||
|
||
### ステップ 5-3:URLを許可リストに登録する
|
||
|
||
付けた直後に管理者で操作してみてください。**ログイン画面に戻され「不正な遷移です。」と表示される**はずです。
|
||
|
||
**原因を自分で調べる**:コンソールのどの例外?(`InsufficientPermissionException`)それを投げているのは?(`PermissionCheckAspect`)何と何を比較している?
|
||
|
||
**対処**:`TransitionTargetPageNameKeyword.getAdminPageList()` の `Set.of(…)` に、**部署の Controller の URL をすべて**追加します。
|
||
|
||
```java
|
||
SELECT_DEPARTMENT,
|
||
SELECT_DEPARTMENT_FORM_EMPTY,
|
||
DELETE_DEPARTMENT,
|
||
SHOW_UPDATE_DEPARTMENT,
|
||
UPDATE_DEPARTMENT_RESULT,
|
||
INSERT_DEPARTMENT_INPUT,
|
||
INSERT_DEPARTMENT_CONFIRM,
|
||
INSERT_DEPARTMENT
|
||
```
|
||
|
||
> **特に重要**:`forward:` で到達する `SELECT_DEPARTMENT_FORM_EMPTY`(`/selectDepartmentAll`)も必要です。`forward:` で呼ばれた先でも権限チェックが動き、そのときのURLは **forward 先のURL** になるためです。登録を忘れると、**削除や更新はDBに反映されるのに、画面には「不正な遷移です。」が出ます**。
|
||
|
||
### ステップ 5-4:3パターンで確認する
|
||
|
||
| パターン | 操作 | 期待 |
|
||
|---|---|---|
|
||
| 未ログイン | ログインせずに `POST /selectDepartment` を送る | ログイン画面+`不正な遷移です。` |
|
||
| 一般ユーザー | `jtaka@hoge.jp`/`bbb` でログインし、`POST /selectDepartment` を送る | 同上 |
|
||
| 管理者 | `yamada@hoge.jp` で部署の全機能を操作 | すべて正常 |
|
||
|
||
POST を直接送る方法(Windows は `curl.exe`):
|
||
|
||
```bash
|
||
curl -s -c cj.txt -b cj.txt -X POST http://localhost:8080/doLogin -d "mailAddress=jtaka@hoge.jp&password=bbb"
|
||
curl -s -c cj.txt -b cj.txt -X POST http://localhost:8080/selectDepartment
|
||
```
|
||
|
||
**対照実験**:いったんアノテーションを外すと、一般ユーザーでも部署画面が見えてしまうことを確認し、付けるとブロックされることを確認してください。「メニューにリンクを出さなければ十分」ではない理由を説明できるようにします(URLを直接送れば画面に入れてしまうため)。
|
||
|
||
### 8.1 第5段階の振り返り
|
||
|
||
- [ ] AOP の仕組み(アノテーション → Aspect → 例外 → ControllerAdvice)を図で説明できる
|
||
- [ ] 許可リストに forward 先のURLも必要な理由を説明できる
|
||
- [ ] 3パターンの確認結果を記録した
|
||
|
||
---
|
||
|
||
## 9. テストの進め方(テスト仕様書の使い方)
|
||
|
||
1. `テスト仕様書/単体テスト仕様書.xlsx` の「部署管理テスト仕様書」を、**各段階の終わりに**、該当する項番を上から順に実施します。
|
||
- 検索:#1〜#8/削除:#32〜#35/更新:#9〜#20/登録:#21〜#31
|
||
2. 実施日と結果(OK/NG)、気づいたことを**備考欄に記入**します。
|
||
3. NG は原因を調べて修正し、**最初から**やり直します(一部だけ直して終わりにしない)。
|
||
4. テスト順の注意:#10・#12・#14・#17・#20 は、**前の項番の結果(部署名が `役員改` になっている等)を前提**にしています。上から順に実施してください。やり直すときはDBを初期状態に戻します(§1.2)。
|
||
5. `テスト仕様書` の「表示データ」シートは社員用のデータで、部署の期待値がそのまま書かれていません。期待される結果は、§1.2 の初期データとテスト項目の文言から判断します(分からなければ質問してください)。
|
||
|
||
> Maven のビルド(`mvn install`)では、プロジェクトの設定上、JUnit のテストは実行されません。テスト仕様書の確認は**画面を操作する手動テスト**で行います。
|
||
|
||
---
|
||
|
||
## 10. 詰まったとき
|
||
|
||
### 10.1 エラーの読み方(5ステップ)
|
||
|
||
1. **症状を言葉にする**:何をして、どうなった?
|
||
2. **Eclipse のコンソール**を開き、赤い文字のうち、**一番下の `Caused by:` の行**を探す。
|
||
3. 自分のクラス名(`com.example.rhizome…`)が出ている**最初の行**を見る(その行番号が原因の手がかり)。
|
||
4. ブラウザの **F12 → Network** タブで、送信先URLとパラメータを確認する。
|
||
5. Controller の先頭に**ブレークポイント**を置いてデバッグ実行し、値を確認する。
|
||
|
||
> Controller の `catch` はすべて、**セッションを破棄してエラー画面を出す**作りです。画面が「エラー」になったら、**コンソールのスタックトレースを必ず見て**ください。
|
||
|
||
### 10.2 症状別の早見表
|
||
|
||
| 症状 | 考えられる原因 | 確認すること |
|
||
|---|---|---|
|
||
| ビルドで「シンボルを見つけられません getXxx()」が大量に出る | JDK が17ではない/Lombok が効いていない | `java -version`、Eclipse の JRE 設定 |
|
||
| 起動時 `Table 'rezodb.employee' doesn't exist` | DDLを実行していない | DB のテーブルを確認 |
|
||
| 起動時 `Access denied` / `Unknown database` | DB・ユーザー未作成 | §1.2、`application.properties` |
|
||
| 起動時 `NoSuchBeanDefinitionException` / `UnsatisfiedDependencyException` | `@Service` の付け忘れ/実装クラスが無い/パッケージの置き場所 | 例外の最後の `Caused by` に書かれた型 |
|
||
| 画面が `エラー` になる | Controller 内で例外が発生(握りつぶされている) | コンソールの `Caused by` |
|
||
| 画面に「不正なリクエストです。」+ 405 | POSTのURLを GET で呼んだ(`<a href>` で直接遷移した等) | F12 Network のメソッド |
|
||
| 404 Not Found | URLの綴り違い/先頭の `/` の有無 | Network のURLと `@RequestMapping` の値を並べる |
|
||
| `Error resolving template [xxx]` | 返した画面名と `templates` のファイル名が不一致 | 戻り値の文字列とファイル名 |
|
||
| `Neither BindingResult nor plain target object for bean name 'xxxForm'` | `th:object` の Form が model に無い(`@ModelAttribute` が無い/別の Controller から画面名で返した) | その Form を誰が model に載せているか |
|
||
| `Exception evaluating SpringEL expression` | HTML 内の項目名の綴り違い | Form のフィールド名と `*{…}` |
|
||
| 検索で条件が効かない・全件になる | `name` 属性とFormのフィールド名の不一致 | HTML の `th:field` と Form |
|
||
| 削除・更新のあと、1件しか表示されない | `forward:` で元のパラメータが検索条件に使われた | 戻り先が全件用のURLか |
|
||
| 更新したのに行が**増えた** | IDが送られていない(`disabled`にした等)/ID `null` の `save` は追加 | F12 Network の送信値、DBの行 |
|
||
| 管理者なのに「不正な遷移です。」 | 許可リストにURL(特に forward 先)が無い | §8 ステップ 5-3 |
|
||
| `Data too long for column 'nm_department'` | DDLが `VARCHAR(10)` | §7 ステップ 4-7(講師に報告) |
|
||
| クリアを押したら検索が走る | `<button>` は `type` 省略で送信ボタン | §4 ステップ 1-6 |
|
||
| 日本語が文字化け | ファイルの文字コードが UTF-8 ではない | Eclipse のエンコーディング設定 |
|
||
| ポート8080が使用中 | 前回起動したプロセスが残っている | 停止してから再起動 |
|
||
|
||
### 10.3 相談の型(質問は次の4点をそろえて)
|
||
|
||
1. やりたいこと(設計書のどの項目か)
|
||
2. 期待した結果/実際の結果(画面の状態、**エラーの該当ログ**)
|
||
3. 試したこと(調べたキーワード、変更した内容)
|
||
4. 自分の考え(原因はこうだと思う、理由は…)
|
||
|
||
---
|
||
|
||
## 11. 提出前チェックリスト
|
||
|
||
**コード**
|
||
- [ ] ビルドが通り、起動できる
|
||
- [ ] 画面名・URL・modelのキーが定数になっている(文字列の直書きがない)
|
||
- [ ] 社員機能からの不要な処理(パスワード、部署プルダウン等)が残っていない
|
||
- [ ] 使っていない import・コメントアウトしたコード・デバッグ用の出力が残っていない
|
||
- [ ] メッセージの文言が設計書・テスト仕様書と一字一句同じ
|
||
- [ ] 既存のファイルを不要に書き換えていない(変更は定数・メニューへの追記のみ)
|
||
|
||
**テスト**
|
||
- [ ] 単体テスト仕様書(部署管理)の実施日・結果を記入(第3段階までは必須)
|
||
- [ ] 異常系を自分で追加確認した(存在しないID、`required` を外した送信 など)
|
||
- [ ] 結合テスト仕様書(部署関連)を実施できた範囲で記入
|
||
|
||
**理解**(口頭で説明できること)
|
||
- [ ] 画面からDBまでのリクエストの流れ
|
||
- [ ] `forward:` と全件用エンドポイントを使う理由
|
||
- [ ] 更新の検証順序の理由
|
||
- [ ] (第5段階まで行った人)AOPの仕組みと許可リストの役割
|
||
|
||
**気づいたことのメモ**(加点対象)
|
||
- [ ] 設計書の誤記や、設計書とDBの食い違い(桁数など)を報告した
|
||
|
||
**合格ライン**:第3段階まで(検索・削除・更新)とテストの実施記録。第4・5段階はチャレンジです。
|