diff --git a/研修生用実装手順書.md b/研修生用実装手順書.md new file mode 100644 index 0000000..d8759de --- /dev/null +++ b/研修生用実装手順書.md @@ -0,0 +1,1423 @@ +# 部署管理機能 実装手順書(研修生用) + +この手順書は、`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)を 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`) + +お手本の次の部分は**そのまま**使えます:``(CSS・JS読み込み)、見出しとログイン者名・ログアウトボタン、メッセージ表示、「メニューに戻る」リンク。 + +骨格(`★` の所を、お手本を見て完成させます): + +```html + + + + + + + + 部署検索 + + +

+ 部署検索条件入力    +
+ ログイン者名: + +
+

+ +
+
+ +
+ + + + + + + + + + + + + +
部署ID
部署名
+
+ +メニューに戻る +

検索結果

+ + + + + + + +``` + +ヒント: +- `th:if="${departmentFormList != null and departmentFormList.size() > 0}"` で「1件以上」を判定できます(お手本参照)。 +- `th:each="departmentForm : ${departmentFormList}"` と `th:text="${departmentForm.idDepartment}"`。 +- `th:field="*{idDepartment}"` が何を作るかは、ブラウザで右クリック →「ページのソースを表示」で確認できます(`id` `name` `value` が作られる)。 + +**考えるポイント(クリアボタン)**:`