ハンズオン資料 - Spring Boot 2とAngularでアプリ作成 (2/2)

#はじめに

こんにちは！
Spring Boot 2とAngularを用いて、アプリを作っていこう第二回です。
第一回はこちらです。
[ハンズオン資料 - Spring Boot 2とAngularでアプリ作成 (1/2)](https://qiita.com/s_kozake/items/0ab687c7cae0f2f95f11)

本ハンズオンの趣旨はそちらをみてくださいね！
第二回はフロントエンドに触れていきます。

#事前準備

ハンズオンを進める上で、次の事前準備が必要です。

・Java 8のインストール
・Gitのインストール
・Node.jsのインストール

また、必須ではないですが、次のIDEとエディタがインストールされていると望ましいです。

・IntelliJ IDEA
・Visual Studio Code

###Visual Studio Codeについて

Visual Studio Codeは、マイクロソフトにより開発されたソースコードエディタです。Windows、Linux、macOS上で動作します。JavaScript、TypeScript、Node.jsのサポートが豊富で、フロントエンドの開発者に人気の開発ツールです。

#大まかな流れ

ハンズオンは計２回に分けて行います。
第１回がサーバーサイド側のWeb API実装で、第２回が今回のフロントエンド側です。[^1]

今回のハンズオンでは、次の流れでアプリを作成していきます。

* プロジェクトの用意
* サーバサイドのフロントエンド対応作業
* OpenAPI GeneratorによるAPIクライアントの自動生成
* IonicのインストールとIonicプロジェクト作成
* 画面の雛形作成
* レシピ選択画面の実装
* 必要素材画面の実装
* ログイン機能の実装
* レシピ編集画面の実装
* 本番ビルド

出来上がりのソースコードはGitHubにあります。

https://github.com/kozake/kajitool-handson

では、張り切っていきましょう！

#プロジェクトの用意

第一回のハンズオン環境がある方は、その環境をそのまま使用できます。
ない方は、適当なディレクトリで次のコマンドを実行してプロジェクトを用意してください。

```
git clone -b 0.0.1 https://github.com/kozake/kajitool-handson.git
```

なお、プロキシ環境の方はこちらです。

```
git clone -b 0.0.1-proxy https://github.com/kozake/kajitool-handson.git
```

cloneが完了したら、ブランチを作成しましょう！

```
> cd kajitool-handson
kajitool-handson> git checkout -b 0.0.2
```

ハンズオンで作成するアプリの認証機能はGithubを用います。
第一回のハンズオンをしていない方は、以下のリンクを参考にGithubを用いたOAuth2セキュリティ認証の設定を行ってください。

[Githubを用いたOAuth2セキュリティ認証](https://qiita.com/s_kozake/items/0ab687c7cae0f2f95f11#github%E3%82%92%E7%94%A8%E3%81%84%E3%81%9Foauth2%E3%82%BB%E3%82%AD%E3%83%A5%E3%83%AA%E3%83%86%E3%82%A3%E8%AA%8D%E8%A8%BC)


#サーバサイド側のフロントエンド対応作業

では、フロントエンド対応の為にサーバーサイド側を修正していきます。
決して第一回でやり忘れたわけではないぞ！[^2]

##ApiOperationアノテーションの定義

ApiOperationアノテーションの定義を追加することで、APIスペックの情報を補充します。
この作業は、後々OpenAPI GeneratorでAPIクライアントを自動生成するために必要な作業です。

```java:kajitool-web/src/main/java/kajitool/web/controller/MaterialResource.java
    @GetMapping("/")
    @ApiOperation(value="素材を返します。", nickname="material_getAll")
    public ResponseEntity<List<Material>> getAll() {
```

```java:kajitool-web/src/main/java/kajitool/web/controller/RecipeListViewResource.java
    @GetMapping("")
    @ApiOperation(value="レシピ一覧を返します。", nickname="recipeListView_getAll")
    public ResponseEntity<List<RecipeListView>> getAll() {

```

```java:kajitool-web/src/main/java/kajitool/web/controller/RecipeResource.java
    @PostMapping("/create")
    @ApiOperation(value="レシピを作成します。", nickname="recipe_create")
    public ResponseEntity<Void> create(@RequestBody final Recipe recipe) 
       :
    @GetMapping("/{id}")
    @ApiOperation(value="IDのレシピを返します。", nickname="recipe_get")
    public ResponseEntity<Recipe> get(@PathVariable final long id) {
       :
    @PutMapping("/save")
    @ApiOperation(value="レシピを更新します。", nickname="recipe_save")
    public ResponseEntity<Void> save(@RequestBody final Recipe recipe) {
       :
    @DeleteMapping("/{id}")
    @ApiOperation(value="IDのレシピを削除します。", nickname="recipe_remove")
    public ResponseEntity<Void> remove(
```

valueプロパティでそのAPIの説明を記述します。
nicknameはAPIスペックのoperationIdとなります。

operationIdはAPI全体で一意にする必要があります。OpenAPI GeneratorはoperationIdからAPIクライアントのメソッド名を求めるのですが、これが重複すると、メソッド名が不正な名前（get、get1、get2のように自動で連番が振られてしまう）になります。ですので、クラス名を前に付与して一意になるように工夫しています。

ここまで出来たらコミットしましょう！

```
kajitool-handson> git add .
kajitool-handson> git commit -m "add ApiOperation"
```

##Accountサービスの追加

ログインアカウントの情報を取得するWeb APIを作成します。
今回は簡単にログインユーザの名前のみを返します。
このWeb APIは、フロント側からシステムにログインしているかどうかを判断する際に使用します。

まずはアカウントの情報を保持するクラスを作成します。

```java:kajitool-web/src/main/java/kajitool/web/domain/model/Account.java
package kajitool.web.domain.model;

public class Account {
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
```

次に、ログインユーザのアカウント情報を返すサービスを作成します。

```java:kajitool-web/src/main/java/kajitool/web/service/account/AccountService.java
package kajitool.web.service.account;

import kajitool.web.domain.model.Account;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;

import java.util.Optional;

@Service
public class AccountService {
    public Optional<Account> getCurrentUserLogin() {
        SecurityContext securityContext = SecurityContextHolder.getContext();
        return Optional.ofNullable(securityContext.getAuthentication())
                .map(this::authenticationToAccount);
    }
    private Account authenticationToAccount(final Authentication authentication) {
        if (authentication.getPrincipal() instanceof OAuth2User) {
            OAuth2User user = (OAuth2User) authentication.getPrincipal();
            Account account = new Account();
            account.setName((String) user.getAttributes().get("login"));
            return account;
        }
        return null;
    }
}
```

ユーザ名は、`login`属性から取得しています。
ここら辺の内容は、認証サーバにより異なり、これ以外にも様々な情報を取得することは出来ます。

ログインユーザのアカウント情報を返すコントローラを作成します。
ログインユーザが存在しない場合はエラーとしています。
このメソッドは認証していないと呼べないようにセキュリティ設定するので、これで問題ありません。

```java:kajitool-web/src/main/java/kajitool/web/controller/AccountResource.java
package kajitool.web.controller;

import io.swagger.annotations.ApiOperation;
import kajitool.web.domain.model.Account;
import kajitool.web.service.account.AccountService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/v1/acount")
public class AccountResource {
    private final AccountService service;
    public AccountResource(AccountService service) {
        this.service = service;
    }
    @GetMapping("/")
    @ApiOperation(value="ログインアカウントを返します。", nickname="account_get")
    public ResponseEntity<Account> get() {
        return ResponseEntity.ok()
                .body(service.getCurrentUserLogin().orElseThrow(
                        () -> new RuntimeException("Account could not be found")));
    }
}
```

セキュリティを設定します[^3]。

```java:kajitool-web/src/main/java/kajitool/web/config/WebSecurityConfig.java
        http.csrf().csrfTokenRepository(
                CookieCsrfTokenRepository.withHttpOnlyFalse())
+       .and()
+           .exceptionHandling()
+           .authenticationEntryPoint((request, response, authException) -> {
+               // SPAとの連携を考慮し、認証エラー時は302ではなく401を返すようにする
+               response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+           })
        .and().oauth2Login()
+               // SPAとの連携を考慮し、認証成功時のURLは固定にする
+               .defaultSuccessUrl("/", true)
        .and().authorizeRequests()
+           .mvcMatchers("/api/v1/acount").authenticated()
            .mvcMatchers(HttpMethod.POST, "/api/**/*").authenticated()

```

通常の設定では、認証エラー時はログインURLへリダイレクト（ステータス302）が返却されます。サーバサイドレンダリングの従来のWebアプリケーションの場合はそれでいいのですが、SPAの場合はAJAX通信をするのでリダイレクトを返されても困ります。そのため、認証不足（ステータス401）を返却するよう変更しています。
また、認証成功時の設定も変更しています。通常の設定では、前回認証不足でアクセスに失敗したURLへリダイレクトされるのですが、これも困ります（JSONデータが表示されても困る）。ですので、画面に遷移するよう、認証成功時のURLを固定しています。

では、`kajitool-web`を起動しましょう。次のコマンドを実行してください。

```
kajitool-handson> gradlew :kajitool-web:bootrun
```

次のURLにブラウザでアクセスしてGitHub認証した後、

http://localhost:8080/oauth2/authorization/github

次のURLにアクセスしてアカウント情報が取得できればOKです。

http://localhost:8080/api/v1/acount/

<img src="https://qiita-image-store.s3.amazonaws.com/0/101517/41502214-dca8-e450-7c74-a10cbaddde00.png" width=50%>

ここまで出来たら、コミットしましょう！

```
kajitool-handson> git add .
kajitool-handson> git commit -m "Account Service"
```

##SQLの修正

フロント側の画面を作成するにあたり、前もっていくつかのデータを事前に用意しておきたいと思います。
次のとおり、SQLを修正してください。

```sql:kajitool-flyway/src/main/resources/db/migration/V1_3__Create_data.sql
insert into MATERIAL values(1, 'どうのこうせき');
insert into MATERIAL values(2, 'てつのこうせき');
insert into MATERIAL values(3, 'ぎんのこうせき');

insert into RECIPE values(RECIPE__ID_SEQ.NEXTVAL, 'どうのつるぎ', 1, SYSDATE);
insert into RECIPE_DETAIL values(RECIPE_DETAIL__ID_SEQ.NEXTVAL, RECIPE__ID_SEQ.CURRVAL, 1, 3);

insert into RECIPE values(RECIPE__ID_SEQ.NEXTVAL, 'てつのつるぎ', 1, SYSDATE);
insert into RECIPE_DETAIL values(RECIPE_DETAIL__ID_SEQ.NEXTVAL, RECIPE__ID_SEQ.CURRVAL, 1, 2);
insert into RECIPE_DETAIL values(RECIPE_DETAIL__ID_SEQ.NEXTVAL, RECIPE__ID_SEQ.CURRVAL, 2, 3);

insert into RECIPE values(RECIPE__ID_SEQ.NEXTVAL, 'せいどうのつるぎ', 1, SYSDATE);
insert into RECIPE_DETAIL values(RECIPE_DETAIL__ID_SEQ.NEXTVAL, RECIPE__ID_SEQ.CURRVAL, 1, 3);
insert into RECIPE_DETAIL values(RECIPE_DETAIL__ID_SEQ.NEXTVAL, RECIPE__ID_SEQ.CURRVAL, 2, 1);
```

その後、次のコマンドを実行してください。その際には、`kajitool-web`は停止しておいてくださいね！

```
kajitool-handson> gradlew :kajitool-flyway:flywayClean
kajitool-handson> gradlew :kajitool-flyway:flywayMigrate
```

次のURLにアクセスして、レシピ一覧の情報が取得できればOKです。

http://localhost:8080/api/v1/recipe_list_view

<img src="https://qiita-image-store.s3.amazonaws.com/0/101517/2d907ea0-3395-12a8-8e08-bcd6bdd208fd.png" width=50%>

ここまで出来たら、コミットしましょう！

```
kajitool-handson> git add .
kajitool-handson> git commit -m "create data"
```

#OpenAPI GeneratorによるAPIクライアントの自動生成

では、いよいよフロント側の作業です！
OpenAPI Generatorを用いてAPIクライアントを自動生成します。
OpenAPI Generatorとは、OpenAPI Spec（2.0と3.0の両方をサポート）からAPIクライアントライブラリを自動生成するOSSツールです。生成するプログラムは様々な言語に対応しています。

詳細は、次のリンク先を参考にしてください。

https://github.com/OpenAPITools/openapi-generator

###Gradle pluginによるOpenAPI Generatorの準備

まずは、`kajitool-api`プロジェクトを作成しましょう。

```
kajitool-handson> mkdir kajitool-api
```

OpenAPI Generatorを使うには、CLI、Maven Plugin、Gradle Pluginといくつか方法がありますが、ここではGradle Pluginを用います。次の`build.gradle`を作成してください。

```gradle:kajitool-api/build.gradle
buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath "org.openapitools:openapi-generator-gradle-plugin:3.3.4"
    }
}

apply plugin: 'org.openapi.generator'

task downloadApiSpec doLast {
    def file = new File("$projectDir/api-spec.json")
    file.delete()
    file.setText(groovy.json.JsonOutput.prettyPrint(
        new URL('http://localhost:8080/v2/api-docs').getText('UTF-8')), 'UTF-8')
}

task clean(type: Delete) {
    delete"$buildDir"
}

task genTypescriptAngular(type: org.openapitools.generator.gradle.plugin.tasks.GenerateTask) {
    generatorName = "typescript-angular"
    inputSpec = "$projectDir/api-spec.json".toString()
    outputDir = "$buildDir/typescript-angular".toString()
    additionalProperties = [
            npmName: "@kajitool/kajitool-api",
            npmVersion: "1.0.0",
            ngVersion: "7.2.2",
            providedInRoot: "true"
    ]
    removeOperationIdPrefix = true
}
genTypescriptAngular.dependsOn clean
```

その後、`kajitool-api`をサブプロジェクトに追加します。

```gradle:settings.gradle
include 'kajitool-web', 'kajitool-flyway', 'kajitool-dao', 'kajitool-api'
```

`downloadApiSpec`タスクは、`kajitool-web`の次のURLにアクセスして、OpenAPI Specをダウンロードするタスクです。

http://localhost:8080/v2/api-docs

ダウンロードしたファイルは、`api-spec.json`というファイルに見やすい形で整形してから保存して、gitで管理します。

では、ダウンロードしてみましょう。`kajitool-web`がローカルで起動している状態で、次のコマンドを実行してください。

```
kajitool-handson> gradlew :kajitool-api:downloadApiSpec
```

`api-spec.json`というファイルが`kajitool-api`ディレクトリに作成されていたら成功です。

次に、`genTypescriptAngular`タスクを実行します。
`genTypescriptAngular`タスクは、OpenAPI Generatorを実行します。

それぞれの設定については、次のリンク先を参考にしてください。

[Configuration](https://github.com/OpenAPITools/openapi-generator/blob/master/modules/openapi-generator-gradle-plugin/README.adoc#configuration)

`removeOperationIdPrefix`について、少しだけ補足します。
このオプションを有効にすることで、operationIdの接頭辞を削除できます。例えば、`config_getId`は、`getId`となります。
`ApiOperation`アノテーションを定義した際、operationIdが重複しないよう、nicknameにクラス名の接頭辞をつけました。このままソースコードを生成すると、クラス名もメソッド名に入ってしまいますので、それを防ぐために、このオプションを有効にしています。

では、実行しましょう。次のコマンドを実行してください。

```
kajitool-handson> gradlew :kajitool-api:genTypescriptAngular
```

`kajitool-api\build`ディレクトリ配下にソースコードが生成されていれば成功です。

一旦、ここでコミットしましょう！

```
kajitool-handson> git add .
kajitool-handson> git commit -m "kajitool-api"
```

##APIクライアントのビルド

では、自動生成したAPIクライアントをビルドします。ですが・・・
このままだと、失敗するんですね～＞＜。
OpenAPI Generatorの不具合だと思います。OpenAPI Generatorは様々な言語を有志がサポートしていることもあり、割とこういう系の不具合は多いです。経験上、フロントエンドの開発はこういうことが多いです。頑張って乗り切りましょう！

`kajitool-api\build\typescript-angular`にカレントディレクトリを移します。

```
cd kajitool-api\build\typescript-angular
```

次のコマンドを実行してください。

```
kajitool-handson\kajitool-api\build\typescript-angular> npm i --save-dev tsickle
```

次に、`package.json`のtypescriptのバージョンを修正してください。[^3]

```json:kajitool-api/build/typescript-angular/package.json
   "devDependencies": {
         :
-    "typescript": ">=2.1.5 <2.8"
+    "typescript": "~3.1.6",
         :
   }
```

これで準備完了です！
次のコマンドを実行してビルドしてください。

```
kajitool-handson\kajitool-api\build\typescript-angular> npm install
kajitool-handson\kajitool-api\build\typescript-angular> npm run build
```

`Built Angular Package!`と表示されれば成功です。

自動生成できたことを祝ってコミットしましょう！・・・っとその前に。
作成されたビルドファイルはgit対象外にします。
次の修正を加えてください。

```:kajitool-api/.gitignore
build/typescript-angular/dist.tgz
```

では、改めてコミットしましょう！

```
kajitool-handson> git add .
kajitool-handson> git commit -m "build api"
```

#IonicのインストールとIonicプロジェクト作成

ようやくSPAを作成する下準備が整いました。長かった。。。

フロント画面はIonicで作成していきます。
あれ？Angularじゃないの？と思われる方もいるかもしれませんので~~言い訳~~説明を。

IonicはAngularベースにアプリ開発が出来るアプリケーションフレームワークです。
AngularのみでWebアプリケーションを作成する場合、色々と下準備が必要ですが、Ionicはアプリケーション作成に必要な部品が一式揃っています。モバイル向けのWebアプリケーションを素早く構築するのであれば、Ionicが向いてると思います。

||個人的な所感|
|---|---|
|Angular|コンポーネント指向でSPAを作るための開発キット|
|Ionic|Angularでモバイル向けWebアプリケーションを作成するためのアプリケーション基盤|

実際に、当初はAngularのみで作成しようとしましたが、色々と面倒くさくてIonic使うことにしました。Ionic便利ですね！

Ionicを学びたい方は、こちらの書籍などいいのではないかと思います。
IonicV3の書籍ですが、V4を学ぶ上でも有効だと思います。

<img src="https://qiita-image-store.s3.amazonaws.com/0/101517/da9ef4f4-a96d-ca8b-895f-398bf6cd822f.png" width=50%>
[Ionicで作る モバイルアプリ制作入門〈Web/iPhone/Android対応](https://www.amazon.co.jp/exec/obidos/ASIN/4863542364/)

###Ionicのインストール

では、はじめましょう！
まずはIonicをインストールします。次のコマンドを実行してください。

```
kajitool-handson> npm install -g ionic
```

インストールに失敗した場合は、以下のリンク先の情報に従い、npmのプロキシ設定を見直してください。

[Using a Proxy](https://ionicframework.com/docs/cli/using-a-proxy)

次のコマンドを実行し、バージョンが表示されれば成功です。[^4]

```
kajitool-handson> ionic --version
```

###Ionicプロジェクト作成

では、Ionicプロジェクトを作成します。

次のコマンドを実行してください。
このコマンドにより、最小構成（blank）のIonicプロジェクトのテンプレートが生成されます。

```
kajitool-handson> ionic start kajitool-ui blank
```

`Error: getaddrinfo EAI_AGAIN ～`のような接続エラーで失敗した場合は、以下のリンク先の情報に従い、Ionicのプロキシ設定を実施してください。

[Using a Proxy](https://ionicframework.com/docs/cli/using-a-proxy)

一旦、コミットしましょう！

```
kajitool-handson> git add .
kajitool-handson> git commit -m "ionic start"
```

次に、先ほど生成したAPIクライアントを追加します。
kajitool-uiディレクトリにカレントディレクトリを移し、次のコマンドを実行してください。

```
kajitool-handson> cd kajitool-ui
kajitool-handson\kajitool-ui> npm i ../kajitool-api/build/typescript-angular/dist.tgz
```

実行確認をしましょう！
次のコマンドで画面が立ち上がるかを確認してください。

```
kajitool-handson\kajitool-ui> npm run start
```

次のURLにアクセスし、ブランク画面が表示されれば成功です。

http://localhost:4200/home

<img src="https://qiita-image-store.s3.amazonaws.com/0/101517/a0795f80-99ec-a363-1689-0e4a794f8c89.png" width=50%>


画面が無事起動できることが確認出来たら、コミットしましょう！

```
kajitool-handson\kajitool-ui> git add .
kajitool-handson\kajitool-ui> git commit -m "install api"
```

#画面の雛形作成

では、画面の雛形を作成していきます！
以降の操作は、カレントディレクトリが`kajitool-ui`にある状態で行います。

次のコマンドを実行してください。

```
kajitool-handson\kajitool-ui> ionic g page pages/selectRecipe
kajitool-handson\kajitool-ui> ionic g page pages/needMaterial
kajitool-handson\kajitool-ui> ionic g page pages/EditRecipe
```

画面の生成に成功したら、一旦コミットします。

```
kajitool-handson\kajitool-ui> git add .
kajitool-handson\kajitool-ui> git commit -m "generate pages"
```

###画面のルート構成

画面のルート構成を変更します。
次のファイルを修正してください。[^3]

```typescript:kajitool-ui/src/app/app-routing.module.ts
 const routes: Routes = [
-  { path: 'select-recipe', loadChildren: './pages/select-recipe/select-recipe.module#SelectRecipePageModule' },
-  { path: 'need-material', loadChildren: './pages/need-material/need-material.module#NeedMaterialPageModule' },
+  // { path: 'select-recipe', loadChildren: './pages/select-recipe/select-recipe.module#SelectRecipePageModule' },
+  // { path: 'need-material', loadChildren: './pages/need-material/need-material.module#NeedMaterialPageModule' },
   { path: 'edit-recipe', loadChildren: './pages/edit-recipe/edit-recipe.module#EditRecipePageModule' },
 ];
```

次に、ホーム画面のルーティングに、先ほどコメントアウトした画面のルートを追加します。

```typescript:kajitool-ui/src/app/home/home.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { IonicModule } from '@ionic/angular';
import { FormsModule } from '@angular/forms';
import { Routes, RouterModule } from '@angular/router';

import { HomePage } from './home.page';

const routes: Routes = [
  {
    path: '',
    component: HomePage,
    children: [
      {
        path: 'select-recipe',
        children: [
          {
            path: '',
            loadChildren: '../pages/select-recipe/select-recipe.module#SelectRecipePageModule'
          }
        ]
      },
      {
        path: 'need-material',
        children: [
          {
            path: '',
            loadChildren: '../pages/need-material/need-material.module#NeedMaterialPageModule'
          }
        ]
      },
      {
        path: '',
        redirectTo: '/home/select-recipe',
        pathMatch: 'full'
      }
    ]
  }
];

@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    IonicModule,
    RouterModule.forChild(routes)
  ],
  declarations: [HomePage]
})
export class HomePageModule {}
```

ホーム画面を次のとおりに置き換えます。

```html:kajitool-ui/src/app/home/home.page.html
<ion-tabs>

  <ion-tab-bar slot="bottom">
    <ion-tab-button tab="select-recipe">
      <ion-icon name="checkmark"></ion-icon>
      <ion-label>武器を選択</ion-label>
    </ion-tab-button>

    <ion-tab-button tab="need-material">
      <ion-icon name="list"></ion-icon>
      <ion-label>必要な素材</ion-label>
    </ion-tab-button>
  </ion-tab-bar>

</ion-tabs>
```

これで、ホーム画面でレシピ選択画面と必要素材画面をタブ切り替え出来るようになりました！
画面を起動して、修正が反映されているのを確認してください。

<img src="https://qiita-image-store.s3.amazonaws.com/0/101517/710ee646-db4c-efb3-d14f-80c22ee1981b.png" width=50%>

ここまで出来たら、コミットしましょう！

```
kajitool-handson\kajitool-ui> git add .
kajitool-handson\kajitool-ui> git commit -m "page route"
```

##サイドメニュー

画面にサイドメニューを追加します。
次の修正を加えてください。

```html:kajitool-ui/src/app/app.component.html
<ion-app>
  <ion-split-pane>
    <ion-menu>
      <ion-header>
        <ion-toolbar>
          <ion-title>メニュー</ion-title>
        </ion-toolbar>
      </ion-header>
      <ion-content>
        <ion-list>
          <ion-menu-toggle auto-hide="false">
            <ion-item>
              <ion-icon slot="start" name="log-in"></ion-icon>
              <ion-label>
                ログイン
              </ion-label>
            </ion-item>
          </ion-menu-toggle>
        </ion-list>
      </ion-content>
    </ion-menu>
    <ion-router-outlet main></ion-router-outlet>
  </ion-split-pane>
</ion-app>
```

```html:kajitool-ui/src/app/pages/select-recipe/select-recipe.page.html
 <ion-header>
   <ion-toolbar>
     <ion-buttons slot="start">
       <ion-menu-button></ion-menu-button>
     </ion-buttons>
     <ion-title>武器鍛冶ツール</ion-title>
   </ion-toolbar>
 </ion-header>
```

```html:kajitool-ui/src/app/pages/need-material/need-material.page.html
 <ion-header>
   <ion-toolbar>
     <ion-buttons slot="start">
       <ion-menu-button></ion-menu-button>
     </ion-buttons>
     <ion-title>武器鍛冶ツール</ion-title>
   </ion-toolbar>
 </ion-header>
```

`ion-menu-button`は、PCサイズの場合は表示されず、タブレットサイズの際に表示されます。
このボタンを押下することで、サイドメニューが表示されます。

画面を起動して、修正が反映されているのを確認しましょう。

<img src="https://qiita-image-store.s3.amazonaws.com/0/101517/53f9b14d-26d5-3c95-c223-e98abe27ceec.png" width=50%>
<img src="https://qiita-image-store.s3.amazonaws.com/0/101517/23ca1058-852e-bf0d-703a-ccc0ec03bda4.png" width=50%>

ここまで出来たら、コミットしましょう！

```
kajitool-handson\kajitool-ui> git add .
kajitool-handson\kajitool-ui> git commit -m "side menu"
```

#レシピ選択画面の実装

###レシピサービス

では、レシピ選択画面を実装していきます！
まずは、レシピを扱うサービスクラスを作成します。

```
kajitool-handson\kajitool-ui> ionic g service services/Recipe
```

サービスクラスを次のとおり修正します。

```typescript:kajitool-ui/src/app/services/recipe.service.ts
import { Injectable } from '@angular/core';
import { RecipeListView, RecipeListViewResourceService } from '@kajitool/kajitool-api';

export interface RecipeListViewModel extends RecipeListView {
  selected: boolean;
  orderQuantity: number;
}

@Injectable({
  providedIn: 'root'
})
export class RecipeService {
  recipeList: RecipeListViewModel[] = [];

  constructor(
    private recipeListViewResource: RecipeListViewResourceService,
  ) { }

  async init(): Promise<RecipeListViewModel[]> {
    const list: RecipeListView[] = await this.recipeListViewResource.getAll().toPromise();
    this.recipeList = [];
    list.forEach(recipe => {
      this.recipeList.push({selected: false, orderQuantity: 1, ...recipe});
    });
    return this.recipeList;
  }
}
```

Angualr 6より、`@Injectable`に`providedIn`を指定できます。
`provideIn： 'root'`はサービスがルートインジェクターに提供されるべきであることを指定しています。これにより、このサービスはAngularのDI フレームワークの仕組みによりroot 所属のサービスとして提供されます。root 所属のサービスは、全てのモジュールで同じインスタンスが利用されます（シングルトンです）。

レシピサービスは自動生成したAPIライブラリを使用します。
APIライブラリのサービスを有効にするために、ルートモジュールで`ApiModule`と`HttpClientModule `をimportsします。[^3]

```typescript:kajitool-ui/src/app/app.module.ts
       :
  import { RouteReuseStrategy } from '@angular/router';
+ import { HttpClientModule } from '@angular/common/http';
+ import { ApiModule, Configuration } from '@kajitool/kajitool-api';
       :
+ export function apiConfiguration(): Configuration {
+   return new Configuration({basePath: ' '});
+ }

  @NgModule({
    declarations: [AppComponent],
    entryComponents: [],
-   imports: [BrowserModule, IonicModule.forRoot(), AppRoutingModule],
+   imports: [
+     BrowserModule,
+     IonicModule.forRoot(),
+     AppRoutingModule,
+     HttpClientModule,
+     ApiModule.forRoot(apiConfiguration)
+   ],
    providers: [
      StatusBar,
       :
```

最後に、アプリ起動時にレシピサービスの初期処理を実行するコードを追加します。
これにより、アプリ起動時にレシピの一覧をWeb API呼び出しにより取得します。
次の修正を加えてください。[^3]

```typescript:kajitool-ui/src/app/app.component.ts
    constructor(
      private platform: Platform,
      private splashScreen: SplashScreen,
-     private statusBar: StatusBar
+     private statusBar: StatusBar,
+     public recipeService: RecipeService,
    ) {
      this.initializeApp();
    }

    initializeApp() {
      this.platform.ready().then(() => {
        this.statusBar.styleDefault();
        this.splashScreen.hide();
+ 
+       this.recipeService.init();
      });
    }
```

###レシピ選択画面

次に、レシピ画面を修正します。レシピサービスをレシピ選択画面にインジェクションします。[^3]

```typescript:kajitool-ui/src/app/pages/select-recipe/select-recipe.page.ts
  import { Component, OnInit } from '@angular/core';
+ import { RecipeService } from 'src/app/services/recipe.service';

       :
  export class SelectRecipePage implements OnInit {

-   constructor() { }
+   constructor(
+     public recipeService: RecipeService
+   ) { }
       :
```

そして、レシピ選択画面のテンプレートを次の通り修正します。

```html:kajitool-ui/src/app/pages/select-recipe/select-recipe.page.html
       :
<ion-content padding>
  <ion-list>
    <ion-item-sliding *ngFor="let recipe of recipeService.recipeList">
      <ion-item>
        <ion-label>
          <h2>{{recipe.name}}</h2>
          <p>素材：{{recipe.materialCount}}つ</p>
        </ion-label>
        <ion-checkbox
          slot="start"
          [(ngModel)]="recipe.selected">
        </ion-checkbox>
        <ion-input
          slot="end"
          type="number"
          inputmode="numeric"
          class="text-right"
          [(ngModel)]="recipe.orderQuantity"
          min="1"
          max="99"
        >
        </ion-input>
      </ion-item>
      <ion-item-options side="end">
        <ion-item-option>
          <div>
            編集
          </div>
        </ion-item-option>
      </ion-item-options>
    </ion-item-sliding>
  </ion-list>
  <ion-fab vertical="bottom" horizontal="end" slot="fixed">
    <ion-fab-button>
      <ion-icon name="add"></ion-icon>
    </ion-fab-button>
  </ion-fab>
</ion-content>
```

注文数の入力テキストについては、右寄せにしたいのでCSSを定義してください。

```scss:kajitool-ui/src/app/pages/select-recipe/select-recipe.page.scss
.text-right {
    text-align: right;
}
```

###プロキシの設定

Angularには、`ng serve`というCLIが用意されています。このコマンドにより、ソースコードがビルドされ、テスト用サーバ上で画面の動作確認が出来ます。
このテスト用サーバは4200番ポートで起動して動作するのですが、このサーバにWebAPIを呼び出しても、当然のことながらHTTPステータス404(Not Found)が返って来ます。
そこで、テスト用サーバへのAPI呼び出しを`kajitool-web`にプロキシする設定を行います。

<img src="https://qiita-image-store.s3.amazonaws.com/0/101517/581dfba3-ef5d-1e2a-15a7-f458fc467200.png" width=80%>

`proxy.conf.json`ファイルを追加してください。

```json:kajitool-ui/proxy.conf.json
{
    "/api": {
        "target": "http://localhost:8080"
    },
    "/oauth2": {
        "target": "http://localhost:8080"
    },
    "/login": {
        "target": "http://localhost:8080"
    }
}
```
`/api`から始まるリクエストは、`"http://localhost:8080"`にプロキシするよう設定しています。
その他の設定はOAuth2認証のためです。

最後に、プロキシを有効にして`ng serve`を起動するスクリプトを登録しましょう。

```json:kajitool-ui/package.json
  "scripts": {
       :
    "serve": "ng serve --proxy-config proxy.conf.json -o",
       :
```

`--proxy-config`で先ほど作成したプロキシ設定のjsonファイルを指定します。`-o`オプションを指定することで、開発サーバ起動時にデフォルトブラウザが起動します。

では、実際に起動しましょう。`kajitool-web`を起動させた後に、次のコマンドで開発用サーバを起動してください。

```
kajitool-handson\kajitool-ui> npm run serve
```

画面が起動して、レシピ一覧が表示されれば成功です！

<img src="https://qiita-image-store.s3.amazonaws.com/0/101517/07adac6c-2119-d0e6-0042-51bbbe6e3f5f.png" width=50%>

ここまで出来たら、コミットしましょう！

```
kajitool-handson\kajitool-ui> git add .
kajitool-handson\kajitool-ui> git commit -m "select recipe page"
```

#必要素材画面の実装

では、武器を作るのに必要な素材を表示する画面を作成します。
まずはサービスの作成から。

###マテリアルサービス

次のコマンドを実行して、マテリアルサービスを作成してください。

```
kajitool-handson\kajitool-ui> ionic g service services/Material
```

次の修正をマテリアルサービスに加えてください。

```typescript:kajitool-ui/src/app/services/material.service.ts
import { Injectable } from '@angular/core';
import { Material, MaterialResourceService } from '@kajitool/kajitool-api';

@Injectable({
  providedIn: 'root'
})
export class MaterialService {
  mateials: Material[] = [];

  constructor(
    private materialResource: MaterialResourceService
  ) { }

  async init(): Promise<Material[]> {
    this.mateials = await this.materialResource.getAll().toPromise();
    return this.mateials;
  }

  getMateial(id: number): Material {
    return this.mateials.find(m => m.id === id);
  }

  getMateials(): Material[] {
    return this.mateials;
  }
}
```
レシピサービス同様、素材（マテリアル）の一覧をアプリ起動時に取得します。
次の修正を加えてください。[^3]

```typescript:kajitool-ui/src/app/app.component.ts
       :
  import { StatusBar } from '@ionic-native/status-bar/ngx';
+ import { MaterialService } from './services/material.service';
  import { RecipeService } from './services/recipe.service';
       :
  export class AppComponent {
    constructor(
       :
+     public materialService: MaterialService,
      public recipeService: RecipeService,
       :
  initializeApp() {
       :
+       this.materialService.init();
        this.recipeService.init();
       :
```

選択したレシピから必要な素材を求める処理をレシピサービスに追加します。
コードが長いので、レシピサービス全部のソースコードを載せます。

```typescript:kajitool-ui/src/app/services/recipe.service.ts
import { Injectable } from '@angular/core';
import { Observable, merge } from 'rxjs';
import { toArray } from 'rxjs/operators';
import {
  Material,
  Recipe,
  RecipeListView,
  RecipeListViewResourceService,
  RecipeResourceService
} from '@kajitool/kajitool-api';
import { MaterialService } from './material.service';

export interface RecipeListViewModel extends RecipeListView {
  selected: boolean;
  orderQuantity: number;
}

export interface NeedMaterialModel {
  material: Material;
  needQuantity: number;
}

@Injectable({
  providedIn: 'root'
})
export class RecipeService {
  recipeList: RecipeListViewModel[] = [];
  needMaterial: NeedMaterialModel[] = [];

  constructor(
    private recipeResource: RecipeResourceService,
    private recipeListViewResource: RecipeListViewResourceService,
    private materialService: MaterialService
  ) { }

  async init(): Promise<RecipeListViewModel[]> {
    const list: RecipeListView[] = await this.recipeListViewResource.getAll().toPromise();
    this.recipeList = [];
    list.forEach(recipe => {
      this.recipeList.push({selected: false, orderQuantity: 1, ...recipe});
    });
    return this.recipeList;
  }

  async getNeedMaterial(): Promise<NeedMaterialModel[]> {

    const obRecipes: Observable<Recipe>[] = this.recipeList
    .filter(recipe => recipe.selected && recipe.orderQuantity > 0)
    .map(recipe => {
      return this.recipeResource.get(recipe.id);
    });
    const recipes: Recipe[] = await merge(...obRecipes).pipe(toArray()).toPromise();

    this.needMaterial = this.buildNeedMaterial(recipes);
    return this.needMaterial;
  }

  private buildNeedMaterial(recipes: Recipe[]): NeedMaterialModel[] {

    const needMaterial: NeedMaterialModel[] = [];
    recipes.forEach(recipe => {
      const selectRecipe = this.recipeList.find(r => r.id === recipe.id);
      if (!selectRecipe) {
        return;
      }
      recipe.recipeDetails.forEach(recipeDetail => {
        const material = needMaterial.find(m => m.material.id === recipeDetail.materialId);
        if (material) {
          material.needQuantity += selectRecipe.orderQuantity * recipeDetail.quantity;
        } else {
          needMaterial.push({
            material: this.materialService.getMateial(recipeDetail.materialId),
            needQuantity: selectRecipe.orderQuantity * recipeDetail.quantity
          });
        }
      });
    });
    return needMaterial;
  }
}
```

`getNeedMaterial`メソッドで必要な素材を求めます。
選択されたレシピを求め、そのレシピ情報をサーバーに平行してリクエストし、その応答がすべて揃ったところでレシピの注文数に応じて素材ごとに必要な数を求めるという複雑なことをしています。

> このような複雑なコードになったのは、Web APIの設計が汎用的だったからです。Web APIは、広く多数に公開するものと、公開範囲が狭く特定の用途で使うものがあります。前者の場合、汎用的な設計が用いられますが、後者の場合はそれに特化した設計がされます。書籍などでは、前者の設計方法について多く記載されていますが、後者のほうが、より簡易に作成できて効率的になる可能性があります。Web APIを設計する際は、前者であるか後者であるかを意識してみてください。


##画面

画面を修正します。次の修正を加えてください。[^3]

```typescript:kajitool-ui/src/app/pages/need-material/need-material.page.ts
  import { Component, OnInit } from '@angular/core';
+ import { RecipeService } from 'src/app/services/recipe.service';
       :
  export class NeedMaterialPage implements OnInit {

-   constructor() { }
+   constructor(public recipeService: RecipeService) { }

    ngOnInit() {
+     this.recipeService.getNeedMaterial();
    }

+   ionViewWillEnter() {
+     this.recipeService.getNeedMaterial();
+   }
}
```

`ionViewWillEnter`メソッドは、Ionicのライフサイクルをハンドルします。
画面が表示される度に、このメソッドが呼ばれるので、画面表示の際に必要素材を計算しています。

画面のテンプレートも修正します。

```typescript:kajitool-ui/src/app/pages/need-material/need-material.page.html
       :
<ion-content padding>
  <ion-item-group>
    <ion-item-divider>
      <ion-label>作る武器</ion-label>
    </ion-item-divider>

    <ng-container *ngFor="let r of recipeService.recipeList">
      <ion-item *ngIf="r.selected && r.orderQuantity > 0">
        <ion-label>{{r.name}}</ion-label>
        <ion-note slot="end" class="quantity">×{{r.orderQuantity}}</ion-note>
      </ion-item>
    </ng-container>
  </ion-item-group>
      
  <ion-item-group>
    <ion-item-divider>
      <ion-label>必要な素材</ion-label>
    </ion-item-divider>

    <ng-container *ngFor="let m of recipeService.needMaterial">
      <ion-item>
        <ion-label>{{m.material.name}}</ion-label>
        <ion-note slot="end" class="quantity">×{{m.needQuantity}}</ion-note>
      </ion-item>
    </ng-container>
  </ion-item-group>
</ion-content>
```

レシピサービス同様に、CSSも追加してください。

```scss:kajitool-ui/src/app/pages/need-material/need-material.page.scss
.quantity {
    font-size: inherit;
}
```
では、実際に起動しましょう。`kajitool-web`を起動させた後に、次のコマンドで開発用サーバを起動してください。

```
kajitool-handson\kajitool-ui> npm run serve
```

画面が起動して、レシピ一覧で選択した武器の必要素材が、必要素材画面に表示されれば成功です！

<img src="https://qiita-image-store.s3.amazonaws.com/0/101517/29926e28-dc08-9565-85b5-a9c980f4ddf7.png" width=50%>

ここまで出来たら、コミットしましょう！

```
kajitool-handson\kajitool-ui> git add .
kajitool-handson\kajitool-ui> git commit -m "need material page"
```

#ログイン機能の実装

ログイン機能を実装していきます。
ログイン機能は、Githubを認証サーバとしたOAuth2認証で実現しています。

まずはアカウントサービスを作成しましょう！

###アカウントサービス

次のコマンドを実行して、アカウントサービスを作成してください。

```
kajitool-handson\kajitool-ui> ionic g service services/Account
```

次の修正をアカウントサービスに加えてください。


```typescript:kajitool-ui/src/app/services/account.service.ts
import { Injectable } from '@angular/core';
import { Account, AccountResourceService } from '@kajitool/kajitool-api';

@Injectable({
  providedIn: 'root'
})
export class AccountService {

  account: Account;
  isLogined = false;

  constructor(private accountResource: AccountResourceService) { }

  async init() {
    try {
      this.account = await this.accountResource.get().toPromise();
      this.isLogined = true;
      console.log(`${this.account.name} login.`);
    } catch (e) {
      console.log('no login.');
    }
  }
}
```

Web APIを発行して、アカウント情報を取得するだけの簡単な処理です。
accountResourceの呼び出しは、認証済みでない場合はHTTPステータス401(Unauthorized)が返却されるため、`catch`ブロックに処理が移ります。
このWeb API呼び出しが出来るかどうかで、認証済みかどうかを判断できます。

アカウントサービスをアプリ起動時に呼び出します。
次の修正を加えてください。[^3]

```typescript:kajitool-ui/src/app/app.component.ts
       :
+ import { AccountService } from './services/account.service';
  import { MaterialService } from './services/material.service';
       :
  export class AppComponent {
    constructor(
       :
+     public accountService: AccountService,
      public materialService: MaterialService,
       :
    initializeApp() {
      this.platform.ready().then(() => {
        this.statusBar.styleDefault();
        this.splashScreen.hide();

+       this.accountService.init();
        this.materialService.init();
        this.recipeService.init();
      });
    }

+   goLogin() {
+     location.href = 'oauth2/authorization/github';
+   }
}
```

`goLogin`メソッドを追加しました。
このメソッドを呼び出すことで、Githubの認証画面に遷移します。
メニューのログインをクリックした際に、Githubの認証画面に遷移するよう、テンプレートを修正しましょう。[^3]

##画面

```html:kajitool-ui/src/app/app.component.html
       :
-     <ion-label>
+     <ion-label (click)="goLogin()">
        ログイン
+       <span *ngIf="accountService.isLogined">
+         ({{accountService.account.name}})
+       </span>
      </ion-label>
       :
```

レシピ編集画面には認証済みのユーザのみが遷移できるよう、レシピ選択画面を修正します。[^3]

```typescript:kajitool-ui/src/app/pages/select-recipe/select-recipe.page.ts
  import { Component, OnInit } from '@angular/core';
+ import { AccountService } from 'src/app/services/account.service';
  import { RecipeService } from 'src/app/services/recipe.service';
       :
  export class SelectRecipePage implements OnInit {

    constructor(
+     public accountService: AccountService,
      public recipeService: RecipeService
    ) { }
       :
```

レシピ選択画面のテンプレートで、ログイン時のみ遷移する部品が表示されるようにします。[^3]

```html:kajitool-ui/src/app/pages/select-recipe/select-recipe.page.html
       :
-       <ion-item-options side="end">
+       <ion-item-options side="end" *ngIf="accountService.isLogined">
          <ion-item-option>
-           <div>
+           <div [routerLink]="['/edit-recipe/' + recipe.id]">
              編集
            </div>
          </ion-item-option>
        </ion-item-options>
      </ion-item-sliding>
    </ion-list>
-   <ion-fab vertical="bottom" horizontal="end" slot="fixed">
-     <ion-fab-button>
+   <ion-fab vertical="bottom" horizontal="end" slot="fixed" *ngIf="accountService.isLogined">
+     <ion-fab-button [routerLink]="['/edit-recipe/new']">
        <ion-icon name="add"></ion-icon>
      </ion-fab-button>
    </ion-fab>
  </ion-content>
```

では、起動して認証を試してみましょう！。。。とその前に。
GithubのOAuth Appsの設定を修正する必要があります。`Authorization callback URL`の設定が現在、`http://localhost:8080/`になっていると思います。開発サーバは4200番ポートで動作するので、このままだとうまく動きません。`http://localhost:4200/`に修正してください。

<img src="https://qiita-image-store.s3.amazonaws.com/0/101517/bd28e9c3-ea66-83ee-3d70-9815d4a6797d.png" width=50%>

では、実際に起動しましょう。`kajitool-web`を起動させた後に、次のコマンドで開発用サーバを起動してください。

```
kajitool-handson\kajitool-ui> npm run serve
```

画面が起動して、ログイン後のサイドメニューに名前がが表示されれば成功です！

<img src="https://qiita-image-store.s3.amazonaws.com/0/101517/396566a5-c6d3-029c-7e6b-4cbff17b0f11.png" width=50%>


ここまで出来たら、コミットしましょう！

```
kajitool-handson\kajitool-ui> git add .
kajitool-handson\kajitool-ui> git commit -m "login future"
```

#レシピ編集画面の実装

いよいよラストです。レシピ編集画面の実装です。
まずはルート設定を修正してください。[^3]

```typescript:kajitool-ui/src/app/app-routing.module.ts
const routes: Routes = [
       :
- { path: 'edit-recipe', loadChildren: './pages/edit-recipe/edit-recipe.module#EditRecipePageModule' },
+ { path: 'edit-recipe/:id', loadChildren: './pages/edit-recipe/edit-recipe.module#EditRecipePageModule' },
];
```

##レシピサービス

レシピサービスに編集に必要な処理を追加します。

```typescript:kajitool-ui/src/app/services/recipe.service.ts
       :
export class RecipeService {
       :
// ここから追加箇所
  newRecipe(): Recipe {
    return {
      id: null,
      name: '',
      recipeDetails: [],
      updatedAt: null,
      version: null
    };
  }

  async get(id: number): Promise<Recipe> {
    return this.recipeResource.get(id).toPromise();
  }

  async create(recipe: Recipe): Promise<any> {
    return await this.recipeResource.create(recipe).toPromise();
  }

  async save(recipe: Recipe): Promise<any> {
    return await this.recipeResource.save(recipe).toPromise();
  }

  async remove(recipe: Recipe): Promise<any> {
    await this.recipeResource.remove(recipe.id, recipe.version).toPromise();
    const index = this.recipeList.findIndex(v => v.id === recipe.id);
    if (index !== -1) {
      this.recipeList.splice(index, 1);
    }
  }
// ここまで追加箇所
       :
}
```

##レシピ編集画面

レシピ編集画面を修正します。
修正箇所が多いので、すべてのソースコードを載せています。

```typescript:kajitool-ui/src/app/pages/edit-recipe/edit-recipe.page.ts
import { MaterialService } from './../../services/material.service';
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { NgForm } from '@angular/forms';
import { LoadingController } from '@ionic/angular';
import { Recipe, Material } from '@kajitool/kajitool-api';
import { RecipeService } from 'src/app/services/recipe.service';

@Component({
  selector: 'app-edit-recipe',
  templateUrl: './edit-recipe.page.html',
  styleUrls: ['./edit-recipe.page.scss'],
})
export class EditRecipePage implements OnInit {
  id: string;
  isNew: boolean;
  recipe: Recipe;
  materials: Material[] = [];

  constructor(
    private route: ActivatedRoute,
    private router: Router,
    private loadingController: LoadingController,
    public materialService: MaterialService,
    public recipeService: RecipeService
  ) {
    this.recipe = this.recipeService.newRecipe();
  }

  async ngOnInit() {
    this.id = this.route.snapshot.paramMap.get('id');
    this.isNew = this.id === 'new';
    if (!this.isNew) {
      this.recipe = await this.recipeService.get(Number(this.id));
    }
    this.materials = this.materialService.getMateials();
  }

  onAddMaterial() {
    this.recipe.recipeDetails.push({
      quantity: 1
    });
  }

  onRemoveMaterial(i: number) {
    this.recipe.recipeDetails.splice(i, 1);
  }

  canRegist(recipeForm: NgForm): boolean {
    return !recipeForm.invalid && this.recipe.recipeDetails.length > 0;
  }

  async onCreate() {
    const loading = await this.loadingController.create({
      message: 'processing...'
    });
    await loading.present();

    try {
      await this.recipeService.create(this.recipe);
      await this.recipeService.init();
    } finally {
      await loading.dismiss();
    }
    this.router.navigate(['/home']);
  }

  async onSave() {
    const loading = await this.loadingController.create({
      message: 'processing...'
    });
    await loading.present();

    try {
      await this.recipeService.save(this.recipe);
      await this.recipeService.init();
    } finally {
      await loading.dismiss();
    }
    this.router.navigate(['/home']);
  }

  async onRemove() {
    const loading = await this.loadingController.create({
      message: 'processing...'
    });
    await loading.present();

    try {
      await this.recipeService.remove(this.recipe);
      await this.recipeService.init();
    } finally {
      await loading.dismiss();
    }
    this.router.navigate(['/home']);
  }
}
```
レシピ編集画面のテンプレートを修正します。
修正箇所が多いので、すべてのソースコードを載せています。

```html:kajitool-ui/src/app/pages/edit-recipe/edit-recipe.page.html
<ion-header>
  <ion-toolbar>
    <ion-buttons slot="start">
      <ion-back-button defaultHref="home"></ion-back-button>
    </ion-buttons>
    <ion-title>レシピ編集</ion-title>
  </ion-toolbar>
</ion-header>

<ion-content padding>
  <form ngForm #recipeForm="ngForm">
    <ion-list>
      <ion-item>
        <ion-label>
          武器名
        </ion-label>
        <ion-input
          name="recipeName"
          type="text"
          placeholder="Input text"
          [(ngModel)]="recipe.name"
          required
        ></ion-input>
      </ion-item>
    </ion-list>

    <ion-list>
      <ion-item-group *ngFor="let detail of recipe.recipeDetails; index as i">
        <ion-item-divider>
          <ion-label> {{ i + 1 }}つめの素材 </ion-label>
          <ion-button slot="end" fill="clear" (click)="onRemoveMaterial(i)">
            <ion-icon name="trash"></ion-icon>
          </ion-button>
        </ion-item-divider>

        <ion-item>
          <ion-label>
            素材名
          </ion-label>
          <ion-select 
            [name]="'materialId-' + i"
            placeholder="Select one"
            [(ngModel)]="detail.materialId"
            required>
            <ng-container *ngFor="let material of materials">
              <ion-select-option 
                [value]="material.id" 
                [selected]="material.id === detail.materialId">
                {{material.name}}
              </ion-select-option>
            </ng-container>
          </ion-select>
        </ion-item>
        <ion-item>
          <ion-label>
            数量
          </ion-label>
          <ion-input
            [name]="'quantity-' + i"
            type="number"
            inputmode="numeric"
            class="text-right"
            placeholder="Input number"
            [(ngModel)]="detail.quantity"
            required
            min="1"
            max="99"
          ></ion-input>
        </ion-item>
      </ion-item-group>
    </ion-list>
  </form>
  <ion-toolbar>
    <ion-buttons slot="end">
      <ion-button color="primary" (click)="onAddMaterial()">
        <ion-icon name="add"></ion-icon>
        素材を追加
      </ion-button>
    </ion-buttons>
  </ion-toolbar>
</ion-content>

<ion-footer>
  <ion-toolbar>
    <ion-buttons slot="end">
      <ion-button 
        *ngIf="this.isNew"
        color="primary" 
        [disabled]="!canRegist(recipeForm)"
        (click)="onCreate()">
        <ion-icon name="add"></ion-icon>
        レシピを追加
      </ion-button>
      <ion-button
        *ngIf="!this.isNew"
        color="primary" 
        [disabled]="!canRegist(recipeForm)"
        (click)="onSave()">
        <ion-icon name="add"></ion-icon>
        レシピを更新
      </ion-button>
      <ion-button
        *ngIf="!this.isNew"
        color="danger" 
        (click)="onRemove()">
        <ion-icon name="remove"></ion-icon>
        レシピを削除
      </ion-button>
    </ion-buttons>
  </ion-toolbar>
</ion-footer>
```

scssも修正しましょう！

```scss:kajitool-ui/src/app/pages/edit-recipe/edit-recipe.page.scss
.text-right {
    text-align: right;
}
```
では、実際に起動しましょう。`kajitool-web`を起動させた後に、次のコマンドで開発用サーバを起動してください。

```
kajitool-handson\kajitool-ui> npm run serve
```

画面が起動して、レシピが追加・編集・削除できることを確認しましょう！

<img src="https://qiita-image-store.s3.amazonaws.com/0/101517/5315416d-53cd-d7dc-dec6-9c710901915d.png" width=50%>

ここまで出来たら、コミットしましょう！

```
kajitool-handson\kajitool-ui> git add .
kajitool-handson\kajitool-ui> git commit -m "edit recipe page"
```

#本番ビルド

最後に、本番ビルドの方法です。
`ng build`で`--prod=true`を指定することで、ビルド構成をプロダクションビルドに設定出来ます。プロダクションビルドは時間がかかりますが、最適化が施されて性能面が改善できます。アプリをリリースする際は、必ずprod=trueを有効にしましょう！
また、`--outputPath`で出力先を`kajitool-web`の静的Webコンテンツのフォルダを指定しています。これにより、`kajitool-web`のコンテンツとして含めることが出来るようになります。

```json:kajitool-ui/package.json
  "scripts": {
       :
    "build:prod": "ng build --prod=true --outputPath=../kajitool-web/src/main/resources/public",
       :
```

#まとめ

2回に分けて、Spring Boot 2とAngular（Ionic）という技術を用いてハンズオンを実施してきました。
如何でしょうか？かなりのボリュームがあったかと思います。

今回のハンズオンで作成したアプリは、簡易な要件にも関わらず、これだけのボリュームがありました。それでもなお、アプリにはいくつもの不具合があり（ハンズオンでは無視しました）、テストもされておらず、ビルドの自動化やデプロイ、運用後に必要な様々な機能（ログ出力すらない）も考慮されていません。

実際のシステム開発は、この何十、何百倍ものボリュームがあり、かつ複雑性も比ではありません。

**今気づいたけど、本当システム開発って難しすぎますね！！**

実際のプロジェクトでは、このようなシステムの開発にチームで取り組む必要があります。ほとんどのプロジェクトにおいて、充分な開発期間、チームメンバーのスキル、予算、ステークホルダーの理解、自身のスキルと、その全てが完全に揃うことはないでしょう。完璧なプロジェクトなどなく、全てのプロジェクトで有効な手法もありません。そのような中、システムアーキテクトはこれらの前提条件の中で、最適解を模索し、開発リーダーとしてプロジェクトを引っ張っていく責務があります。

外部環境をコントロールすることは難しいですが、自身のスキルはそれに比べるとコントロールしやすいです。
このハンズオンが、皆さんの様々な技術スキル習得のきっかけになれば幸いです。

**長いハンズオン、おつかれさまでした！！ 🍻(´∀｀*v)**

[^1]: 無事完成してよかったよぉーー。
[^2]: やり忘れました。
[^3]: 変更箇所が分かるように、追加箇所には`+`記号、削除箇所には`-`記号をソースコードの先頭に付与しています。
[^4]: このハンズオン資料を作成時点でのバージョンは、`4.11.0`でした。