Spring BootとMySQLでメモアプリを開発しました #3【駆け出し】

# Spring Bootでの開発

Spring Bootで作成した簡易メモアプリにスタイルを当てていなかったため、
CSSを少し学んだ後、アウトプットのためにアプリケーションにスタイルを追加していく過程です。

【 前回の記事 】
https://qiita.com/Takatsuna/items/7be62afa19cb2cd92b88


## 作ったもの
#### 簡易メモアプリ
リポジトリ: https://github.com/takatsuna113/spring-memo-app

[ 前回 ]　Home画面と詳細画面のみ

[ 今回 ]　<strong>編集・新規作成・ゴミ箱の画面を追加</strong> ← new



## 使用技術
- Spring Boot 2.7.12
- Java 11
- Maven(ビルドツール)
- MySQL(DB)
- Mybatis(O/Rマッパー)

## テーブル定義
[こちら](https://qiita.com/Takatsuna/items/31af8b007a44f2c3a900)の記事を参照ください。


## 成果物(画面)
### メモの編集画面
![スクリーンショット 2023-08-14 22.21.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2523157/a1900e9c-f5b0-5196-2367-7ba65b3e588f.png)

動作イメージ
![1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2523157/49e393c1-ca3c-8835-988a-d1a60edd9ac0.gif)

特にこだわりはなく、シンプルな感じになりました。


### メモの新規作成画面
![スクリーンショット 2023-08-14 22.26.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2523157/6e46180c-dede-3a61-26ec-c162c85b347a.png)

動作イメージ
![2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2523157/cda14fc8-ebe6-aa71-4137-28d9aa2f0929.gif)


こちらは編集ページとほぼ同じスタイルです。

### ゴミ箱ページ
![スクリーンショット 2023-08-14 22.30.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2523157/ca5890ac-43ed-fa30-a069-49ab45e85f3f.png)

動作イメージ

① 元に戻す
![1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2523157/a29d0c3f-e987-c231-2010-f0487ed1153d.gif)

こちらは、削除フラグをtrueからfalseにすることでゴミ箱と一覧とを切り替えている




② 完全に削除
![1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2523157/58c2ff20-44f6-45f9-bfdd-eb5bf083466e.gif)

こちらは、<strong>一番苦労した</strong>ところです。

モーダルの表示後の、削除のhref属性が一番上の要素固定になってしまい、どの「完全に削除」を押しても一番上のメモが消えてしまうという現象が発生しました。

試行錯誤の末、


① 各メモをquerySelectorAllで取得しておく
② 削除ボタンにclass属性に"メモのid"を付与しておく
③ ①の要素の中から「完全に削除」をクリックした場所の"メモのid"をモーダル表示関数に渡す
④ 渡ってきた"メモのID"を「削除」のhref属性に追記


といった流れでなんとか正常に動作するようになりました。

以下、モーダル表示に関わるソース(一部)

html

```html
  <div class="container">
    <div class="garbage-list">
      <div class="garbage-item" th:each="memo : ${deletedMemoList}" th:object="${memo}">
        <p>[[${memo.title}]]</p>
        <div class="action">
          <a class="cancel" th:href="@{/memo/cancelDelete/{id}(id=${memo.id})}">元に戻す</a>
          <!-- ここに"メモのID"を付与 -->
          <a class="remove" th:classappend="${memo.id}">完全に削除</a>
        </div>
      </div>
    </div>
  </div>

  <!-- 個別の完全削除モーダル -->
  <div class="remove-confirm">
    <div class="confirm-modal">
      <div class="confirm-title">
        <a onclick="rmModalClose()"><i class="bi bi-x"></i></a>
      </div>
      <div class="confirm-content">
        <p>本当に削除しますか？</p>
      </div>
      <div class="delete-action">
        <a>削除</a>
        <a onclick="rmModalClose()">キャンセル</a>
      </div>
    </div>
  </div>
```

javascript
```javascript
// 個別の完全削除モーダル表示
if (location.href.includes('garbage')) {
  const memoList = document.querySelectorAll('.remove');
  memoList.forEach((memo) => {
    memo.addEventListener('click', () => {
      // 「完全に削除」が押された箇所のメモIDをモーダルに渡す
      rmModalOpen(memo.classList[1]);
    })
  });
  // モーダル表示
  const rmModalOpen = (memoId) => {    
    const rmModal = document.querySelector('.remove-confirm');
    // href属性(リクエストURL)を書き換えるaタグを取得
    const rmTargetAnchor = rmModal.children[0].children[2].children[0];
    // href属性にメモのIDを追記
    rmTargetAnchor.setAttribute('href', `remove/${memoId}`);
    // モーダルを非表示から表示へ
    rmModal.style.visibility = 'visible';
    rmModal.style.zIndex = '100';
  }
}
```

css
```css
/* 個別の完全削除モーダル */
.remove-confirm {
    visibility: hidden;
    position: absolute;
    top: 0;
    left: 0;
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100%;
    width: 100%;
    z-index: -1;
    background: rgba(0, 0, 0, 0.6);
}

.remove-confirm .confirm-modal {
    position: absolute;
    top: 35%;
    left: 35%;
    height: 30%;
    width: 30%;
    display: flex;
    justify-content: space-between;
    align-items: center;
    flex-direction: column;
    background: #faf9f9;
    border: solid 1px var(--primary-text-color);
    border-radius: 4px;
}

.remove-confirm .confirm-modal .confirm-title {
    width: 100%;
    text-align: right;
    padding-right: 0.8rem;
}

.remove-confirm .confirm-modal .confirm-title a {
    cursor: pointer;
    font-size: 1.5rem;
    border-radius: 50%;
    transition: .3s;
}

.remove-confirm .confirm-modal .confirm-title a:hover {
    background: #ecebeb;
}

.remove-confirm .confirm-modal .confirm-content {
    width: 100%;
    text-align: center;
    color: var(--primary-text-color);
}

.remove-confirm .confirm-modal .delete-action {
    display: flex;
    justify-content: center;
    align-items: center;
    height: 20%;
    width: 100%;
    padding-bottom: 0.9rem;
}

.remove-confirm .confirm-modal .delete-action a:nth-child(1) {
    padding: 0.2rem 0.5rem;
    border-radius: 10px;
    box-shadow: 0 0 3px rgba(0, 0, 0, 0.35);
    background: #fedfdc;
    text-decoration: none;
    color: var(--primary-text-color);
    margin-right: 0.7rem;
    width: 25%;
    text-align: center;
    transition: .3s;
}

.remove-confirm .confirm-modal .delete-action a:nth-child(1):hover {
    background: #febab4;
    box-shadow: none;
}

.remove-confirm .confirm-modal .delete-action a:nth-child(2) {
    padding: 0.2rem 0.4rem;
    border-radius: 5px;
    text-decoration: none;
    color: var(--primary-text-color);
    background: #ededed;
    box-shadow: 0 0 3px rgba(0, 0, 0, 0.2);
    cursor: pointer;
    transition: .3s;
}

.remove-confirm .confirm-modal .delete-action a:nth-child(2):hover {
    background: #d4d4d4;
    box-shadow: none;
}
```

③ 空にする
![1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2523157/c6463f63-0ed1-d65b-26c9-db8dfdc35e66.gif)

こちらは個別の完全削除とは違い、比較的簡単に実装できました。

### Homeページ(検索欄)
![スクリーンショット 2023-08-15 21.53.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2523157/e4c10552-cc1e-de09-1e3c-b18a15a9d24e.png)

動作イメージ
![1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2523157/b850836e-f8a3-a37e-9b9f-eadb3eeff40a.gif)



検索バーに入力した「Enter」を押すと検索結果に"タイトル"もしくは"内容"が部分一致するメモが表示される仕組みです。
検索処理の度にリロードされるので見応えはありませんが、動作自体はうまくいったように思います。

html
```html
  <header class="header">
    <a class="app-title" th:href="@{/memo/home}">メモ一覧</a>
    <div class="search">
      <form th:action="@{/memo/home}">
        <input type="text" placeholder="メモを検索">
        <i class='bx bx-search'></i>
      </form>
    </div>
    <nav>
      <a th:href="@{/memo/create}"><i class='bx bx-edit'></i>new</a>
      <a th:href="@{/memo/garbage}"><i class='bx bxs-box'></i>ゴミ箱</a>
    </nav>
  </header>
```

javascript
```javascript
// 検索欄に入力した文字列でリクエストURLを生成
if (location.href.includes('home') || location.href.includes('detail')) {
  const form = document.querySelector('form');
  const baseUrl = form.getAttribute('action') + '/'
  const searchArea = document.querySelector('input');
  // 検索欄に入力がある度にリクエストURLに入力値を追記
  searchArea.addEventListener('input', () => {
    const requestUrl = baseUrl + searchArea.value;
    form.action = requestUrl;
  })
}
```


## こだわった箇所

- モーダルの表示
画面の動作イメージ②で記載した通り、削除したい対象のIDをリクエストURLに動的に割り当てるのが苦労しましたが
普通のconfirmメソッドとは違うというところがこだわりです。


## 現時点での反省点
- <strong>バリデーションの対応をしていない</strong>
バックエンドの方のバリデーションを全く実装できていないので、今後実装をする

- <strong>登録・編集・検索エラーが発生した時の画面表示をしていない</strong>
１つ目と少し重なるが、各種エラー時に画面上にポップアップ？のようなものを表示できていないのでこちらも今後実装をする

- <strong>javascriptのリファクタリング</strong>
モーダルの表示など、おそらく処理を共通化できる部分があるため




## これから
これである程度、それっぽいアプリケーションができた。
フロントはReactを使うなどすれば、もっといろんなことができそうなのでReactをもっと学びたい。

しかし、今の会社ではReact案件なんて聞いたことないのでJavaの理解をもっと深めてJava案件に参画したいという思いもある...。

あとはデプロイ周りも学ばないといけない気がする。
デプロイやLinuxサーバ構築の参考書や資料でおすすめがあったら教えてください。