[Node.js][Express]傾斜機能付き割り勘webアプリを作る！

# はじめに
### 制作物
[warican](https://warican.onrender.com/home)
<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952141/ab678605-1b97-82a1-6457-cc83a0ccadbd.png">


:::note warn
現在スマホで操作するとイベント編集ボタンをクリックできないバグが発生しております。現在対応中です
:::


### github（一部抜粋）
[github](https://github.com/Mitachi-H/waricanOpen)
個人情報を含むファイルは削除しています。
### こだわりポイント
* 傾斜を設定できる
* 結果のテキストを保存できる
* グループ作成時にはメンバーを指定する必要がない
* 支払い者が複数のイベントも追加できる
<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952141/f928de9a-dd23-07c8-d2fd-6ac53be66c64.png">

### 自己紹介
神奈川在住の大学二年生。エンジニア系のインターンに受かることを目標に2022年9月からJavascriptの勉強を開始。[独習JavaScript](https://www.amazon.co.jp/独習JavaScript-新版-CodeMafia-外村-将大/dp/479816027X)読破後、[ハンズオンNode-js](https://www.amazon.co.jp/ハンズオンNode-js-今村-謙士/dp/4873119235)でNode.jsの勉強にうつり、10月中盤にwebアプリの基本を抑えたのでwebアプリ制作を開始。HTMLとCSSはもともとぼちぼち知っていた。

### 制作の経緯
[warica](https://walica.jp)とか割り勘webアプリめっちゃ便利だけど、傾斜機能（先輩が多く払うなど）がないから、後輩とかとドライブ行く時とかは結局使えないんだよねー。じゃあ作るか！

### 制作方法
友人と2人で。友人はフロントエンドのデザイン、私はバックエンドとフロントエンドの通信、データ処理部分を担当。僕はデザインが、友人はデータ処理部分が苦手だったので、いい勉強になった。またGitHubでの共同開発についても知見が深まった。

# 制作準備

## 機能を考える
機能を考えるためにまずは以下の用語を定義しておく。
### 用語定義
|用語|定義|
|-|-|
|グループ|割り勘をする集団　メンバーを設定し、イベントを追加していく|
|メンバー|グループに参加する人|
|イベント|お金を立て替えた出来事（居酒屋、カラオケなど）|

### 最低限必要な機能
グループ、メンバー、イベントをそれぞれ追加、編集、削除できる。

### 欲しい機能
* みんなで編集できる
* 結果の共有が簡単
* 傾斜の詳細設定ができる


## 使う技術
### フロントエンド
HTML,CSS,JavaScriptで制作。友人がデザイン面でJQueryを利用していたので、DOM操作にも多少利用した。データのやりとりはfetchを利用した。

### デプロイ方法
本当はAWSで公開したかったが難しそうだったので、githubの連携で簡単に公開できる[render.com](https://render.com)を選択。render.comではpostgresqlを無料で利用できるので、データベースとしてpostgreSQLを選択した。またパッケージマネージャーはrender.comと相性の良いyarnを利用。

### バックエンド
Expressフレームワークを選択。またその他パッケージとして
* cookie-parser (レスポンスでクッキーを含める）
* ejs　(エラーページにエラーメッセージをのせるため）
* pg　（PostgreSQLの操作）

を使用。



## ページ、URL設計
[warica](https://walica.jp)を参考に制作した。
### URL
|パス|メソッド|ページの内容|
|-|-|-|
|/home|GET|ホーム画面　過去の作成したグループの表示|
|/home/addGroup|GET|グループ作成ページ|
|/home/groupHome/:groupId|GET|グループ、割り勘結果の表示|
|/home/groupHome/:groupId/addEvent|GET|イベントの追加|
|/home/groupHome/:groupId/groupSettings|GET|割り勘方法の設定|

### ページ設計
```mermaid
flowchart TB
    _<-->error
    subgraph _
    home-->addGroup
    home-->groupHome
    addGroup-->groupHome
    subgraph group
    groupHome<-->groupSettings
    groupHome<-->addEvent
    end
    end
```

### WebAPiのURL
|パス|メソッド|処理内容|利用方法|
|-|-|-|-|
|/home/addGroup|POST|グループ追加|fetch|
|/home/groupHome/:groupId|POST|グループ編集|fetch|
|/home/groupHome/:groupId|POST|メンバー追加|fetch|
|/home/groupHome/:groupId|POST|メンバー削除|fetch|
|/home/groupHome/:groupId|POST|イベント削除|fetch|
|/home/groupHome/:groupId/showGroup|GET|グループ表示|fetch|
|/home/groupHome/:groupId/addEvent|POST|イベント追加|fetch|
|/home/groupHome/:groupId/addEvent|POST|イベント編集|fetch|
|/home/groupHome/:groupId/groupSettings|POST|グループ編集|fetch|

### 反省
同じパスに複数のメソッドを投げる設計は適切でないかも。

## データベース設計
ユーザーのグループを作成時、groupidを作成して、allgroupにデータが追加する。
#### allgroup
|Column|Type|補足|
|-|-|-|
|groupid|PK varchar(255)|長さ8のランダムなアルファベット列|
|groupname|varchar(255)||
|grouptype|json|割り勘方法や最小単位といったグループの情報|
|lastlogin|timestamp with time zone||

その後groupidをもとに2つのテーブルを作成。（テーブル名にgroupidを含める）
#### (groupid)member
|Column|Type|補足|
|-|-|-|
|memberid|serial integer||
|name|varchar(255)|メンバー名|
|membertype|varchar(255)|男、女などメンバーの属性|

#### (groupid)event
|Column|Type|補足|
|-|-|-|
|eventid|serial integer||
|name|varchar(255)|イベント名|
|payerlist|json[]|支払い者のmemberidと支払い金額情報|
|receiverlist|interger[]|未支払い者のmemberidの配列|

グループで追加したメンバーやイベントはこれらのテーブルに保存。

#### 反省
問題点
* ログインのないグループのデータとテーブルは自動で削除するようにしているが、テーブルがグループ数だけ増えていく仕様になっている。
* 対策用のミドルウェアを実装しているが、table名が動的に変化するためSQLインジェクションの可能性がある。
allmember、alleventというテーブルを制作し、Columnにgroupidを追加する方法で制作した方が良いのかな？？データベース設計についてよくわからないことが多いので、体系的に勉強したい。

# バックエンド制作
## はじめに
### ディレクトリ構造
```
├── app.js
├── db.js
├── node_modules
├── package.json
|   ├──...
├── public
│   ├── debug
│   │   ├──...
│   ├── docs
│   │   ├── css
|   |   |   ├──...
│   │   ├── html
|   |   |   ├──...
│   │   └── js
|   |       ├──...
│   └── img
|       ├──...
|
├── yarn.lock
```
### 必要モジュールインストール
<details>

```JavaScript:app.js
const express = require("express");
const cookie = require("cookie-parser");
const pool = require("./db");
const app = express();

app.use(express.json());
app.use(cookie());
app.use(express.static("public"));
app.set("view engine", "ejs");
```
</details>

## リクエストからレスポンスまでの流れ例
groupidをURLに含む場合
```mermaid
flowchart 
正常時--->errorハンドリングミドルウェア
subgraph 正常時
リクエスト--->groupidチェックミドルウェア--主にGET-->静的ファイル送信
groupidチェックミドルウェア--主にPOST-->SQL操作関数--->SQL結果送信
end

```
## groupidチェックミドルウェア
Router
```JavaScript:app.js
//groupIdをチェックするミドルウェア
app.use("/home/groupHome/:groupId", (req, res, next) => {
  checkGroupId(req.params.groupId, next);
});
```
groupIDをチェックする関数
```JavaScript:app.js
function checkGroupId(groupId, next) {
  //groupIdのインジェクションを防ぐ
  if (!isGroupIdSafe(groupId)) {
    next(new Error("invalidGropuId")); //errorへ
  }
  //groupIdの存在を確認する
  else {
    pool
      .query("select * from allgroup where groupid = $1", [groupId])
      .then((results) => {
        if (!results.rows.length) {
          next(new Error("noGroup")); //errorへ
        } else {
          next();
        }
      });
  }
}

function isGroupIdSafe(groupId) {
  //groupIdのインジェクションを防ぐ
  const alphabet = "abcdefghijklmnopqrstuvwxyz";
  if (groupId.length != 8) {
    return false;
  }
  for (const word of groupId) {
    if (!alphabet.includes(word)) {
      return false;
    }
  }
  return true;
}
```
## リクエストハンドラ
### groupHomeリクエストハンドラ
```JavaScript:app.js
app
  .route("/home/groupHome/:groupId")
  .get((req, res, next) => {
    //静的ファイル送信
    res.sendFile(__dirname + "/public/docs/html/groupHome.html");
  })
  .post((req, res, next) => {
    const obj = req.body;
    //SQL操作関数へ
    if (obj.method === "editGroup") {
      editGroup(req.params.groupId, obj.arguments, res, next);
    }
    if (obj.method === "addMember") {
      addMember(req.params.groupId, obj.arguments, res, next);
    }
    if (obj.method === "deleteMember") {
      deleteMember(req.params.groupId, obj.arguments, res, next);
    }
    if (obj.method === "deleteEvent") {
      deleteEvent(req.params.groupId, obj.arguments, res, next);
    }
  });
```
その他のURLについても同様のため省略

### 反省
先にも述べた通り、ひとつのURLで複数のリクエストを処理するのは良くない設計の可能性がある。

## SQL関連
### SQLとの接続
<details>

```JavaScript:db.js
const Pool = require("pg").Pool;

const connectionString = process.env.PORT
  ? "Internal Database URL"
  : "External Database URL";

const pool = new Pool({
  connectionString: connectionString,
});

module.exports = pool;
```

### 反省
render.comは外部からデータベースにアクセスする時とrender.comでデブロイしたアプリ内部でデータベースにアクセスするときでURLが違うが、それを区別する方法としてより適切な方法がありそう。
</details>

## SQL操作関数
### グループ追加
```JavaScript:app.js
function addGroup({ name }, res, next) {
  //ランダムな8桁の文字列を生成してgroupIdとする
  const alphabet = "abcdefghijklmnopqrstuvwxyz";
  let groupId = "";
  for (let i = 0; i < 8; i++) {
    groupId += alphabet[Math.floor(Math.random() * 26)];
  }

  //alldata table に　nameを追加する
  //table (groupId)member,(groupId)event を作成する
  const createMemberTable =
    "create table " +
    groupId +
    "member (memberid serial,name varchar(255),membertype varchar(255))";
  const createEventTable =
    "create table " +
    groupId +
    `event (eventid serial,name varchar(255),payerlist json[],receiverlist integer[])`;
  Promise.all([
    pool.query(
      "insert into allgroup (groupid,groupname,grouptype,lastlogin) values ($1,$2,$3,$4)",
      [
        groupId,
        name,
        JSON.stringify({
          balance: "equal",
          ratelist: [1, 1.5, 2.0],
          forXYen: 10,
        }),
        new Date(),
      ]
    ),
    pool.query(createMemberTable),
    pool.query(createEventTable),
  ])
    .then(() => {
      //適切に作成されたら、groupIdとクッキーを返す
      res
        .status(200)
        .cookie(groupId, name, { maxAge: 1000 * 3600 * 24 * 4 })
        .json(groupId);
    })
    .catch((err) => {
      //errorハンドリングミドルウェアへ
      next(err);
    });
}
```
### 反省
* groupidをテーブル名に使用している以上、PostgreSQLのuuid型で作成したものは利用できなかった（table名の最初はalphabetのみのため）
groupidの長さは揃えた方がよいためこの形に落ち着いた

* transactionを利用して、一連の操作でエラーが出た場合は操作を取り消す仕組みを追加するべきだった。
* res,nextを引数に入れないほうがシンプルだったかも？

その他のeditGroup,deleteGroup,addMember....関数に関しては同様のため省略。

## errorハンドリング
```JavaScript:app.js
//errorハンドリングミドルウェア
app.use((err, req, res, next) => {
  if (err == "Error: noGroup") {
    errMessage = "お探しのグループは存在しません";
    res.status(404);
  } else if ((err = "Error: invalidGroupuId")) {
    errMessage = "グループIDが無効です";
    res.status(406);
  } else {
    errMessage = "サーバー上で問題が発生しました";
    res.status(500);
  }
  res.render(__dirname + "/public/docs/html/error.ejs", {
    err: errMessage,
  });
});
```
### 反省
errorの分類の仕方が流石にダサすぎる。うまい方法があるんだろうけど思いつかなかった。
## その他の機能
### 更新のないグループのデータの自動削除
```JavaScript:app.js
function deleteOldData() {
  pool.query("select * from allgroup").then((result) => {
    const now = new Date().getTime();
    result.rows.forEach(({ groupid, lastlogin }) => {
      if (now - lastlogin.getTime() > 1000 * 3600 * 24 * 4) {
        //4日間編集なしなら
        deleteGroup(
          groupid,
          {
            status: () => {
              return { json: () => {} };
            },
          },
          console.log
        ); //いなし方雑
      }
    });
  });
}
```
<details><summary>deleteGroup</summary>

```JavaScript:app.js
function deleteGroup(groupId, res, next) {
  //alldata table のgroupId を削除する

  const dropMemberTable = "drop table " + groupId + "member";
  const dropEventTable = "drop table " + groupId + "event";
  Promise.all([
    pool.query("delete from allgroup where groupid = $1", [groupId]),
    pool.query(dropMemberTable),
    pool.query(dropEventTable),
  ])
    .then(() => {
      res.status(200).json("group has deleted");
    })
    .catch((err) => {
      next(err);
    });
}
```
</details>

### 反省
SQL操作関数のdeleteGroupを使用しようとしたら、res,nextを組み込んだ設計だったため、簡単には流用できず、結果的にerrorにならないような適当な関数を用意していなした。雑い...

# フロントエンド制作
:::note warn
以後紹介するのは各ページのJSファイルの一部抜粋です。
:::
### ページ設計(再掲）
```mermaid
flowchart TB
    _<-->error
    subgraph _
    home-->addGroup
    home-->groupHome
    addGroup-->groupHome
    subgraph group
    groupHome<-->groupSettings
    groupHome<-->addEvent
    end
    end
```
## home
<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952141/662b0793-8c83-273e-58a8-e0135bcad6b4.png">

### Cookieから過去に編集したグループのデータを取得し表示
```JavaScript:index.js
//cokkieの情報をゲットする
const oldData = document.cookie.split("; ");
//cookie空確認
if (Boolean(oldData[0])) {
  oldData.forEach((str) => {
    const [groupid, name] = str.split("=");
    const cloneItem = groupListItem.cloneNode(true);
    cloneItem.id = groupid;
    //日本語の文字化けを直す
    cloneItem.querySelector(".group-title").textContent = decodeURI(name);
    groupListContainer.append(cloneItem);
  });
}
groupListItem.remove();

//過去データクリックしたらグループホームへ
$(".group-list-item").on("click", function () {
  location.href = location.origin + "/home/groupHome/" + $(this)[0].id;
});
```
### 反省
この制作ではページ遷移の方法としてlocation.hrefを変える方法を使用しているが、適切であるかわからない。

## addGroup
<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952141/a7b1be1b-1b0b-10a2-7e05-50c6f86125e5.png">

### 入力した名前をグループ名に保存しグループ作成
```JavaScript:createGroup.js
groupButton.addEventListener("click", addGroup);

async function addGroup(event) {
  event.preventDefault();
  const name = groupName.value;
  if (name && name.length < 10) {
    groupButton.removeEventListener("click", addGroup);
    const obj = {
      method: "addGroup",
      arguments: { groupname: name },
    };
    //POST
    fetch(location.pathname, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(obj),
    }).then((result) => {
      result.json().then((groupId) => {
        location.href = location.origin + "/home/groupHome/" + groupId;
      });
    });
    load();
  } else {
    alert("グループ名が不適切です");
  }
}

//ロード画面を表示=>ページ遷移まで
function load() {
  const loaderWrapper = document.querySelector(".loader-wrapper");
  loaderWrapper.classList.add("active");
}

```
## groupHome
<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952141/2f00eded-4e85-b116-97b5-0a3692753182.png">

### groupHomeのロード順
groupHomeは機能が多く、処理順序が重要であるため以下にまとめる。
```mermaid
graph TD;
不適切なイベントチェック--allLoadedイベント発信-->割り勘を計算,表示
不適切なイベントチェック--allLoadedイベント発信-->イベントElementにイベントリスナを追加
subgraph showGroup.js
グループのデータ取得-->セッションストレージに保存-->グループ,イベント,メンバーDOMの作成
グループ,イベント,メンバーDOMの作成-->不適切なイベントチェック
end
subgraph caluculation.js
割り勘を計算,表示
end
subgraph groupHome.js
イベントElementにイベントリスナを追加
end
```

### 不適切なイベントチェック
既に削除されたメンバーを含みイベントをエラー表示する
```JavaScript:showGroup.js
function checkalertEvent() {
  //全てのイベントのpayerlist,receiverlistに削除されたメンバーがいないか確認
  const memberidList = JSON.parse(sessionStorage.getItem("member")).map(
    ({ memberid }) => memberid
  );
  const eventList = JSON.parse(sessionStorage.getItem("event"));
  eventList.forEach(({ eventid, payerlist, receiverlist }) => {
    //payerlist,receiverlistに削除されたメンバーがいないか確認
    if (
      receiverlist.find((memberid) => !memberidList.includes(memberid)) ||
      payerlist.find(({ memberid }) => !memberidList.includes(Number(memberid)))
    ) {
      //eventエレメントの見た目を変更
      const str = "#event-" + eventid;
      const item = document.querySelector(str);
      if (!item.classList.contains("alert")) {
        item.classList.toggle("alert");
        //メッセージ表示
        item.querySelector(".alert-message").hidden = false;
      }
    }
  });
}
```
### 計算の流れ
```mermaid
flowchart TD
subgraph 全イベントについて
そのイベントの男子と女子の支払い金額計算-->そのイベントの各個人の収支を計算
end
subgraph 繰り返す
先頭の支払い者が末尾の受取者にお金を渡す--支払い者が十分支払った-->1つ後ろの支払い者で同じことをやる
先頭の支払い者が末尾の受取者にお金を渡す--受取者が十分受け取った-->1つ前の受取者で同じことをやる
end
セッションストレージからグループデータを取得 --> 全イベントについて --> 全メンバーの合計収支計算
全メンバーの合計収支計算-->収支順に並べ替え-->繰り返す

```
イベントごとに男子と女子の支払い金額を計算しているのは、イベントごとにメンバーが変化する可能性があり、まとめて計算すると不利なひとが生じ得るから。また最後のお金の受け渡しのアルゴリズムによって支払い者がお金を渡す必要がある人数を少なく抑えている。
<details><summary>実際のコード（全文）</summary>

```JavaScript:calculation.js
window.addEventListener("allLoaded", tryCalculation);
window.addEventListener("calculate", tryCalculation);
//クリップボードにコピー用
let results = "";

function tryCalculation() {
  try {
    caluculate();
  } catch (err) {
    console.log(err);
  }
}
function caluculate() {
  //pay初期化
  const memberList = JSON.parse(sessionStorage.getItem("member")).map(
    (memberObj) => {
      return { ...memberObj, pay: 0 };
    }
  );
  const eventList = JSON.parse(sessionStorage.getItem("event"));
  const { groupname, grouptype } = JSON.parse(sessionStorage.getItem("group"));

  //results初期化
  results =
    location.href +
    "\n" +
    (groupname || "グループ") +
    " " +
    (grouptype.balance == "boy+"
      ? "男子少し多め\n"
      : grouptype.balance == "boy++"
      ? "男子かなり多め\n"
      : "") +
    memberList.length +
    "人で計算";
  eventList.forEach(({ payerlist, receiverlist }) => {
    const totalCost = payerlist.reduce(
      (total, { cost }) => total + Number(cost),
      0
    );
    let girlPay = 0;
    let boyPay = 0;
    //girlPay,boyPayを求める
    if (grouptype.balance == "equal") {
      girlPay = totalCost / receiverlist.length;
      boyPay = girlPay;
    } else {
      const rate = grouptype.ratelist[grouptype.balance == "boy++" ? 2 : 1];
      let boyGirl = [0, 0];
      receiverlist.forEach((receiverid) =>
        memberList.find(({ memberid }) => memberid == receiverid).membertype ==
        "male"
          ? boyGirl[0]++
          : boyGirl[1]++
      );
      girlPay = totalCost / (boyGirl[0] * rate + boyGirl[1]);
      boyPay = girlPay * rate;
    }
    //memberのイベントの収支を計算
    memberList.forEach(({ memberid, membertype }, index) => {
      if (receiverlist.includes(memberid)) {
        memberList[index].pay += membertype == "male" ? boyPay : girlPay;
      }
      memberList[index].pay -= Number(
        (
          payerlist.find((payerData) => payerData.memberid == memberid) || {
            cost: 0,
          }
        ).cost
      );
    });
  });

  //memberListをcost順に並び替える
  memberList.sort((a, b) => b.pay - a.pay);
  //割り勘計算
  //ここでのpayerは精算時のpayer(逆に注意)
  let payerIndex = 0;
  let receiverIndex = memberList.length - 1;

  //showCalculationで必要
  const paymentItemContainer = document.querySelector(
    ".payment-item-container"
  );
  const paymentItem = paymentItemContainer.querySelector(".payment-item");
  paymentItemContainer.innerHTML = "";
  const forXYen = grouptype.forXYen;

  while (payerIndex < receiverIndex) {
    if ((memberList[receiverIndex].pay += memberList[payerIndex].pay) >= 0) {
      showCalculation(
        memberList[payerIndex].name,
        memberList[receiverIndex].name,
        Number(memberList[payerIndex].pay - memberList[receiverIndex].pay)
      );
      memberList[payerIndex].pay = memberList[receiverIndex].pay;
      if (memberList[payerIndex].pay == 0) {
        payerIndex++;
      }
      memberList[receiverIndex].pay = 0;
      receiverIndex--;
    } else {
      showCalculation(
        memberList[payerIndex].name,
        memberList[receiverIndex].name,
        memberList[payerIndex].pay
      );
      memberList[payerIndex].pay = 0;
      payerIndex++;
    }
  }
  function showCalculation(payerName, receiverName, money) {
    //何円単位で割り勘を行うかを加味した実際払うお金
    const fixedMoney =
      money - Math.floor(money / forXYen) * forXYen >
      forXYen - (money - Math.floor(money / forXYen) * forXYen)
        ? (1 + Math.floor(money / forXYen)) * forXYen
        : Math.floor(money / forXYen) * forXYen;
    if (fixedMoney) {
      //domでの処理
      const cloneItem = paymentItem.cloneNode(true);
      cloneItem.querySelector(".payer").textContent = payerName;
      cloneItem.querySelector(".receiver").textContent = receiverName;
      cloneItem.querySelector(".payment-cost").textContent = fixedMoney + "円";
      paymentItemContainer.append(cloneItem);
      //resultsに対応
      results +=
        "\n" + payerName + "\n=>" + receiverName + "   " + fixedMoney + "円";
    }
  }
  paymentItem.remove();
}

//クリップボードにコピー
$(".copy-button").on("click", function () {
  if (results[results.length - 1] == "円") {
    //空じゃない
    navigator.clipboard.writeText(results);
    $(this)[0].textContent = "コピーしました!!";
  }
});

```
</details>

### グループ編集処理
```mermaid
flowchart TB
addMember-->memberid取得
deleteMember-->_
editGroup-->__
deleteEvent-->____
subgraph postServer
memberid取得
_
__
____
end
memberid取得--セッションストレージ更新-->メンバーElement作成
_--セッションストレージ更新-->不適切なイベントチェック
__--セッションストレージ更新-->caluculateイベント発信
____--セッションストレージ更新-->caluculateイベント発信
```
### postServer
```JavaScript:groupHome.js
function postServer(obj, callback) {
  //ロード開始
  if (obj.method.includes("Member")) {
    memberLoad();
  } else if (obj.method === "editGroup") {
    groupTitleLoad();
  }
  fetch(location.pathname, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(obj),
  })
    .then((res) => {
      res.json().then((x) => {
        console.log(obj);
        console.log(x);
        //ロード終了
        if (obj.method.includes("Member")) {
          memberLoad();
        } else if (obj.method === "editGroup") {
          groupTitleLoad();
        }
        //addMember deleteMember の場合
        const memberObj = { memberid: x[0].memberid, ...obj.arguments }; //add=>alldata,delete=>memberid
        callback(memberObj);
      });
    })
    .catch((err) => {
      console.log("通信に失敗", err);
    });
}
```
callbackの引数が必要なのがaddmemberのみなので(memberidを受け取る必要がある）、memberObjを渡している。（deletememberの時も便利なので使っている)
### addMember
```JavaScript:groupHome.js
//member追加したときの動作
$("#member-add").on("click", function () {
  const memberNameInput = document.querySelector("#member-name-input");
  const memberSex = document
    .querySelector("#sex-button")
    .className.split(" ")[1];

  if (memberNameInput.value) {
    const obj = {
      method: "addMember",
      arguments: {
        name: memberNameInput.value,
        membertype: memberSex,
      },
    };
    //showMemberし、sessionストレージにデータ追加
    postServer(obj, (obj) => {
      sessionStorage.setItem(
        "member",
        JSON.stringify([...JSON.parse(sessionStorage.getItem("member")), obj])
      );
      //メンバーElement追加
      showMember(obj);
    });
  }
  //更新終わった後にinputを空にする
  memberNameInput.value = "";
});
```
deleteMember,editGroupも同様のため省略

## addEvent
<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952141/f3264c1a-295f-fec2-be05-d3de4337c6d1.png">

かなりごちゃついた
### addEventロード順
```mermaid
flowchart TB
セッションストレージからメンバー情報を取得,表示--イベント編集なら-->過去のイベント情報を反映
```
### イベント編集なら編集前情報を画面に反映
```JavaScript:addEvent.js
function importEvent() {
  //編集ボタンをクリックするとeditEventにeventidが追加される
  //単純なイベント追加だとeditEventは""![スクリーンショット 2022-11-05 23.29.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952141/475fa78b-f5e3-a421-fbc7-9195c0ad647e.png)

  const editeventid = sessionStorage.getItem("editEvent");
  try {
    //編集するイベントのデータを取得
    const { name, payerlist, receiverlist } = JSON.parse(
      sessionStorage.getItem("event")
    ).find(({ eventid }) => eventid == editeventid);
    //nameを代入
    for (const input of document.querySelectorAll(".event-name-input")) {
      input.value = name || "イベント" + editeventid;
    }
    //payerを代入
    if (payerlist.length == 1) {
      //oneのページを表示
      showOneorSeveral("one");
      //one payerに代入
      selectOnePayer(payerlist[0].memberid);
      //one costに代入
      document.querySelector(".cost-input").value = payerlist[0].cost;
    } else {
      //severalのページを表示
      showOneorSeveral("several");
      //several payerとcostを代入
      payerlist.forEach(({ cost, memberid, name }) => {
        selectSeveralPayer(memberid);
        addSeveralPayerCostItem({
          memberid: memberid,
          name: name,
          cost: cost,
        });
      });
    }
    //receiverを代入
    receiverlist.forEach((memberid) => {
      selectReciever(Number(payerlist.length != 1), memberid);
    });
    //最後に追加ボタンを更新ボタンに変更
    for (const button of addEventButtons) {
      button.firstElementChild.textContent = "更新";
    }
  } catch (err) {
    console.log(err);
  }
}
```
支払い者人数を一人から複数に切り替えたとき、入力していた情報を反映する機能や、受け取り手を一度に選択できるボタンを追加したり、使い勝手を意識して制作した。（故にごちゃついたのだが）


## groupSettings
<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952141/88dad1af-72cc-2a16-e2ce-fa3e07c39208.png">
特に難しい処理はないので省略

## error
<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952141/e2a154a7-c56f-b4aa-8d5b-bf283799d9e5.png">

### アクセスしたグループが存在しなかった時にcookieのデータを消す
```JavaScript:error.js
const errorMassage = document.querySelector(".error-massage");
//urlのgroupidを取得
const hrefList = location.href.split("/");
const invalidgroupid = hrefList[hrefList.indexOf("groupHome") + 1];
if (errorMassage.textContent == "お探しのグループは存在しません") {
  const cookie = document.cookie.split("; ").map((str) => str.split("="));
  cookie.forEach(([groupid, groupname]) => {
    if (groupid == invalidgroupid) {
      document.cookie =
        groupid +
        "=" +
        groupname +
        "; expires=Mon, 31 Aug 2020 15:00:00 GMT;path=/";
    }
  });
}
```
expireを過去に書き換えることで削除する
## おわりに
### 制作をおえて
このQiitaを編集していて気がついたのは、このアプリを制作する上で一番大変だったところは、コードを書くところじゃなくて設計すところだったということ。あとコーディングに関しては、（コードぐちゃぐちゃな自分が言うのもなんですが）、やりたいように動かすこと事態はそんなに難しくなくて、どれほどスマートにわかりやすくかけるかが一番大事で一番難しい部分なんだとも感じた。今後は実践経験を積んで、その部分についても勉強しつつ、セキュリティー面やバージョン管理といった難しい部分についても挑戦していきたい。

### 今後やりたいこと
* Reactを使用したSPAでの実装
* AWSを利用したデブロイ
* websocketを利用したリアルタイムで編集が反映されるgroupHome（制作途中でトライしたが、errorハンドリング等共有が難しく、断念した。）
* c#での経験を活かしてバックエンドTypeScriptでの書き直し。

質問があったらどしどし送ってください！！それでは！