立命館大学の GPA 計算サイトを作った話

## まえがき

娘「パパ、GPAいくつ？」
父「ちょっと待ってね、今から計算するから。」

一般的にGPAの計算方法は以下の通りなんですが.........

1. GPA計算サイトを作る
1. デプロイする
1. サイトにアクセスする
1. データを入力して計算ボタンを押す

やっぱり最初の"GPA計算サイトを作る"工程が一番難しいんですよね。

誰もが気軽にGPAを計算することができたらいいのに.........
これが筆者の願いです。

## 概要
ということで作りました、立命館大学専用GPA計算サイトです。
まずは使ってみてください。↓↓

https://rits-gpa.vercel.app/

(このプロダクトは筆者が個人的に作ったものであり、立命館大学とは無関係です。)

## 目的
* 個人開発をミニマムに経験する
* Reactの練習
* GPAを計算する(おまけ)

## 要件定義
まずは要件定義をしてみましょう。ゆるゆる個人開発なので厳密には定義していませんが、以下の3項目をプロダクトの条件として設定しました。

### 1. GPAを計算できること
"GPA 計算サイト" なので、GPAの計算は絶対条件です。
### 2. 初めて見た人でも使い方がわかること
これは当たり前のようで実は難しかったりします。
新しいアプリでも私たちは画面をポチポチしながら感覚的にその使い方を理解します。これって結構すごいことだと思うんですけど、どうですか？

### 3. 使っていて楽しいこと
この手の計算サイトは見た目が質素なことが多いので基本的に使っていて楽しいと思うことはないのですが、どうせなら皆さんのGPAを楽しく盛大にお伝えしたいと思いこの項目を設定しました。


## 技術
React & TypeScript で作りました。完全フロント実装です。

## 設計
### データ入力

GPAを計算するために必要な情報は以下の2点です。

* 各科目の評価
* 各科目の単位数

これらのデータのインプットをどのように実装するかで悩みましたが、今回はセレクトタグを採用しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2293026/fd13eddc-b748-d4f4-51ac-e9f740a986da.png)

インプットが計60個もあるので、全体的な視認性のことを考えるとセレクトタグが一番まとまりが良い気がします。

### 計算
セレクトタグから取得したデータを元に GPA を計算します。立命館大学の成績評価方法は他大学と少し異なるようなので、GPA の細かい計算方法は [学び支援サイト](https://www.ritsumei.ac.jp/pathways-future/examinations_grades/grade_evaluation.html/) で確認してください。

```math
GPA=\frac{\sum \left(credit_n \cdot grade_n  \right)}{\sum credit_n}
```

今回は全てのデータを二次元配列に格納してから計算を行いました。データが入力されなかったコマには0が格納されます。
```ts:gpa.ts
let data: number[][] = [];

for (let i = 0; i < 30; i++){
    let grade_i = document.getElementById("grade" + String(i)) as HTMLInputElement;
    let credit_i = document.getElementById("credit" + String(i)) as HTMLInputElement;
    data.push([Number(grade_i.value), Number(credit_i.value)]);
}

let sum_credit = 0;
data.map((data) => {sum_credit += data[1]});

const GPA = sum_credit === 0 ? 0 : (() => {
    let sum_score = 0;
    data.map((data) => {sum_score += data[0] * data[1]});
    
    const calculated_gpa = (sum_score / sum_credit);
    return Math.floor(calculated_gpa * Math.pow(10, 2)) / Math.pow(10, 2);
});

console.log(GPA);
```

### 結果の表示
計算された GPA を画面に表示します。結果の表示は一大イベントなのでモーダル画面で表示することにしました。
![スクリーンショット 2022-11-27 22.36.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2293026/cb8cf129-0b23-a8e6-a95a-232cc54dfe89.png)


要件定義にも設定した通り使っていて楽しいものにしたかったので、とりあえず盛大に紙吹雪を散らしています。使用したライブラリは [react-rewards](https://www.npmjs.com/package/react-rewards) です。useEffect を使用することで、モーダルが表示された瞬間に紙吹雪が発動します。

```tsx:Modal.tsx
import {useEffect} from 'react';
import { useReward } from 'react-rewards';

const Modal = ():JSX.Element => {
    const { reward, isAnimating } = useReward('rewardId', 'confetti');

    useEffect(():void => {reward()});

    return(
        <>
            <span id="rewardId" />
        </>
    )
}

export default Modal;
```
紙吹雪が物足りないという方はエンターキーまたはスペースキーで追加できます。筆者からのサプライズ機能です。
![スクリーンショット 2022-11-27 23.26.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2293026/c093533e-bee9-8418-b11f-66f33be72e12.png)

こんなに使っていて楽しい GPA 計算サイトはないでしょう。

GPA を計算したらモーダル画面のツイートボタンから共有してくださいね！！

https://twitter.com/hiroki_int/status/1566572949372489728

## React を使っていて思うこと
筆者はとにかく React が好きなんですが、その理由はコンポーネント指向です。要素を細かく区切ってコンポーネント化することで、設計が管理しやすくなります。

特にGPA計算マシンではコンポーネントの再利用性の恩恵を盛大に享受しています。

例えばここ、月曜1限から金曜他まで合計30コマの時間割がありますが、全部同じ要素を繰り返しているだけですね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2293026/0c3bd47a-5749-4da0-6a1a-03e37b3eea5b.png)

全てのコマが同じ要素なので1度定義したものを30回参照すればよさそうです。Square と定義しておきましょう。
```tsx:Square.tsx
const Square = (props):JSX.Element => {
  return (
    <td>
        <div>
            <select id={"grade"+props.id_number}>
                <option value="0" selected disabled>評価　</option>
                <option value="5">A+</option>
                <option value="4">A</option>
                <option value="3">B</option>
                <option value="2">C</option>
                <option value="0">F</option>
            </select>
        </div>
        <div>
            <select id={"credit"+props.id_number}>
                <option value="0" selected disabled>単位数</option>
                <option>0</option>
                <option>1</option>
                <option>2</option>
                <option>3</option>
                <option>4</option>
            </select>
        </div>
    </td>
  );
}
```
あとは 5 × 6 回参照するだけです。

↓↓全体のコンポーネント設計はこんな感じ。超スマートですね。
※各コンポーネントをクリックするとgithubに飛びます。

```mermaid
graph LR
    a[Home]-->b[Header]
    a-->c[Table]
    a-->d[Data]
    c-->g[TableHead]
    c-->e[Columun]
    subgraph 6 times
        h[Times]
        e-->h
        subgraph 5 times
            f[Square]
        end
        e-->f
    end

    click a "https://github.com/hirokiwa/gpa_calculator/blob/main/src/page/home/Home.tsx"
    click b "https://github.com/hirokiwa/gpa_calculator/blob/main/src/page/header/Header.tsx"
    click c "https://github.com/hirokiwa/gpa_calculator/blob/main/src/page/home/table/Table.tsx"
    click d "https://github.com/hirokiwa/gpa_calculator/blob/main/src/page/home/data/Data.tsx"
    click e "https://github.com/hirokiwa/gpa_calculator/blob/main/src/page/home/table/column/Column.tsx"
    click f "https://github.com/hirokiwa/gpa_calculator/blob/main/src/page/home/table/column/square/Square.tsx"
    click g "https://github.com/hirokiwa/gpa_calculator/blob/main/src/page/home/table/table_head/TableHead.tsx"
    click h "https://github.com/hirokiwa/gpa_calculator/blob/main/src/page/home/table/column/square/Times.tsx"


```

なんとなく比較対象が間違っている気がしますが、React の再利用性の恩恵を肌で感じるために html を手書きしてみました。絶対に真似しないでください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2293026/ee47d4a7-bba3-7bc6-b022-c2925f5dae43.png)


↓長すぎて鬱なので畳んでおきます。

<details><summary>サンプルコード</summary>


```haml:timetable.html
<table>
    <tr>
        <th></th>
        <th>月</th>
        <th>火</th>
        <th>水</th>
        <th>木</th>
        <th>金</th>
    </tr>
    <tr>
        <td>1</td>
        <td>
            <select id="grade0">
                <option value="0" disabled="">評価　</option>
                <option value="5">A+</option>
                <option value="4">A</option>
                <option value="3">B</option>
                <option value="2">C</option>
                <option value="0">F</option>
            </select>
            <select id="credit0">
                <option value="0" disabled="">単位数</option>
                <option>0</option>
                <option>1</option>
                <option>2</option>
                <option>3</option>
                <option>4</option>
            </select>
        </td>
        <td>
            <select id="grade1">
                <option value="0" disabled="">評価　</option>
                <option value="5">A+</option>
                <option value="4">A</option>
                <option value="3">B</option>
                <option value="2">C</option>
                <option value="0">F</option>
            </select>
            <select id="credit1">
                <option value="0" disabled="">単位数</option>
                <option>0</option>
                <option>1</option>
                <option>2</option>
                <option>3</option>
                <option>4</option>
            </select>
        </td>
        <td>
            <select id="grade2">
                <option value="0" disabled="">評価　</option>
                <option value="5">A+</option>
                <option value="4">A</option>
                <option value="3">B</option>
                <option value="2">C</option>
                <option value="0">F</option>
            </select>
            <select id="credit2">
                <option value="0" disabled="">単位数</option>
                <option>0</option>
                <option>1</option>
                <option>2</option>
                <option>3</option>
                <option>4</option>
            </select>
        </td>
        <td>
            <select id="grade3">
                <option value="0" disabled="">評価　</option>
                <option value="5">A+</option>
                <option value="4">A</option>
                <option value="3">B</option>
                <option value="2">C</option>
                <option value="0">F</option>
            </select>
            <select id="credit3">
                <option value="0" disabled="">単位数</option>
                <option>0</option>
                <option>1</option>
                <option>2</option>
                <option>3</option>
                <option>4</option>
            </select>
        </td>
        <td>
            <select id="grade4">
                <option value="0" disabled="">評価　</option>
                <option value="5">A+</option>
                <option value="4">A</option>
                <option value="3">B</option>
                <option value="2">C</option>
                <option value="0">F</option>
            </select>
            <select id="credit4">
                <option value="0" disabled="">単位数</option>
                <option>0</option>
                <option>1</option>
                <option>2</option>
                <option>3</option>
                <option>4</option>
            </select>
        </td>
    </tr>
    <tr>
        <td>2</td>
        <td>
            <select id="grade5">
                <option value="0" disabled="">評価　</option>
                <option value="5">A+</option>
                <option value="4">A</option>
                <option value="3">B</option>
                <option value="2">C</option>
                <option value="0">F</option>
            </select>
            <select id="credit5">
                <option value="0" disabled="">単位数</option>
                <option>0</option>
                <option>1</option>
                <option>2</option>
                <option>3</option>
                <option>4</option>
            </select>
        </td>
        <td>
            <select id="grade6">
                <option value="0" disabled="">評価　</option>
                <option value="5">A+</option>
                <option value="4">A</option>
                <option value="3">B</option>
                <option value="2">C</option>
                <option value="0">F</option>
            </select>
            <select id="credit6">
                <option value="0" disabled="">単位数</option>
                <option>0</option>
                <option>1</option>
                <option>2</option>
                <option>3</option>
                <option>4</option>
            </select>
        </td>
        <td>
            <select id="grade7">
                <option value="0" disabled="">評価　</option>
                <option value="5">A+</option>
                <option value="4">A</option>
                <option value="3">B</option>
                <option value="2">C</option>
                <option value="0">F</option>
            </select>
            <select id="credit7">
                <option value="0" disabled="">単位数</option>
                <option>0</option>
                <option>1</option>
                <option>2</option>
                <option>3</option>
                <option>4</option>
            </select>
        </td>
        <td>
            <select id="grade8">
                <option value="0" disabled="">評価　</option>
                <option value="5">A+</option>
                <option value="4">A</option>
                <option value="3">B</option>
                <option value="2">C</option>
                <option value="0">F</option>
            </select>
            <select id="credit8">
                <option value="0" disabled="">単位数</option>
                <option>0</option>
                <option>1</option>
                <option>2</option>
                <option>3</option>
                <option>4</option>
            </select>
        </td>
        <td>
            <select id="grade9">
                <option value="0" disabled="">評価　</option>
                <option value="5">A+</option>
                <option value="4">A</option>
                <option value="3">B</option>
                <option value="2">C</option>
                <option value="0">F</option>
            </select>
            <select id="credit9">
                <option value="0" disabled="">単位数</option>
                <option>0</option>
                <option>1</option>
                <option>2</option>
                <option>3</option>
                <option>4</option>
            </select>
        </td>
    </tr>
    <tr>
        <td>3</td>
        <td>
            <select id="grade10">
                <option value="0" disabled="">評価　</option>
                <option value="5">A+</option>
                <option value="4">A</option>
                <option value="3">B</option>
                <option value="2">C</option>
                <option value="0">F</option>
            </select>
            <select id="credit10">
                <option value="0" disabled="">単位数</option>
                <option>0</option>
                <option>1</option>
                <option>2</option>
                <option>3</option>
                <option>4</option>
            </select>
        </td>
        <td>
            <select id="grade11">
                <option value="0" disabled="">評価　</option>
                <option value="5">A+</option>
                <option value="4">A</option>
                <option value="3">B</option>
                <option value="2">C</option>
                <option value="0">F</option>
            </select>
            <select id="credit11">
                <option value="0" disabled="">単位数</option>
                <option>0</option>
                <option>1</option>
                <option>2</option>
                <option>3</option>
                <option>4</option>
            </select>
        </td>
        <td>
            <select id="grade12">
                <option value="0" disabled="">評価　</option>
                <option value="5">A+</option>
                <option value="4">A</option>
                <option value="3">B</option>
                <option value="2">C</option>
                <option value="0">F</option>
            </select>
            <select id="credit12">
                <option value="0" disabled="">単位数</option>
                <option>0</option>
                <option>1</option>
                <option>2</option>
                <option>3</option>
                <option>4</option>
            </select>
        </td>
        <td>
            <select id="grade13">
                <option value="0" disabled="">評価　</option>
                <option value="5">A+</option>
                <option value="4">A</option>
                <option value="3">B</option>
                <option value="2">C</option>
                <option value="0">F</option>
            </select>
            <select id="credit13">
                <option value="0" disabled="">単位数</option>
                <option>0</option>
                <option>1</option>
                <option>2</option>
                <option>3</option>
                <option>4</option>
            </select>
        </td>
        <td>
            <select id="grade14">
                <option value="0" disabled="">評価　</option>
                <option value="5">A+</option>
                <option value="4">A</option>
                <option value="3">B</option>
                <option value="2">C</option>
                <option value="0">F</option>
            </select>
            <select id="credit14">
                <option value="0" disabled="">単位数</option>
                <option>0</option>
                <option>1</option>
                <option>2</option>
                <option>3</option>
                <option>4</option>
            </select>
        </td>
    </tr>
    <tr>
        <td>4</td>
        <td>
            <select id="grade15">
                <option value="0" disabled="">評価　</option>
                <option value="5">A+</option>
                <option value="4">A</option>
                <option value="3">B</option>
                <option value="2">C</option>
                <option value="0">F</option>
            </select>
            <select id="credit15">
                <option value="0" disabled="">単位数</option>
                <option>0</option>
                <option>1</option>
                <option>2</option>
                <option>3</option>
                <option>4</option>
            </select>
        </td>
        <td>
            <select id="grade16">
                <option value="0" disabled="">評価　</option>
                <option value="5">A+</option>
                <option value="4">A</option>
                <option value="3">B</option>
                <option value="2">C</option>
                <option value="0">F</option>
            </select>
            <select id="credit16">
                <option value="0" disabled="">単位数</option>
                <option>0</option>
                <option>1</option>
                <option>2</option>
                <option>3</option>
                <option>4</option>
            </select>
        </td>
        <td>
            <select id="grade17">
                <option value="0" disabled="">評価　</option>
                <option value="5">A+</option>
                <option value="4">A</option>
                <option value="3">B</option>
                <option value="2">C</option>
                <option value="0">F</option>
            </select>
            <select id="credit17">
                <option value="0" disabled="">単位数</option>
                <option>0</option>
                <option>1</option>
                <option>2</option>
                <option>3</option>
                <option>4</option>
            </select>
        </td>
        <td>
            <select id="grade18">
                <option value="0" disabled="">評価　</option>
                <option value="5">A+</option>
                <option value="4">A</option>
                <option value="3">B</option>
                <option value="2">C</option>
                <option value="0">F</option>
            </select>
            <select id="credit18">
                <option value="0" disabled="">単位数</option>
                <option>0</option>
                <option>1</option>
                <option>2</option>
                <option>3</option>
                <option>4</option>
            </select>
        </td>
        <td>
            <select id="grade19">
                <option value="0" disabled="">評価　</option>
                <option value="5">A+</option>
                <option value="4">A</option>
                <option value="3">B</option>
                <option value="2">C</option>
                <option value="0">F</option>
            </select>
            <select id="credit19">
                <option value="0" disabled="">単位数</option>
                <option>0</option>
                <option>1</option>
                <option>2</option>
                <option>3</option>
                <option>4</option>
            </select>
        </td>
    </tr>
    <tr>
        <td>5</td>
        <td>
            <select id="grade20">
                <option value="0" disabled="">評価　</option>
                <option value="5">A+</option>
                <option value="4">A</option>
                <option value="3">B</option>
                <option value="2">C</option>
                <option value="0">F</option>
            </select>
            <select id="credit20">
                <option value="0" disabled="">単位数</option>
                <option>0</option>
                <option>1</option>
                <option>2</option>
                <option>3</option>
                <option>4</option>
            </select>
        </td>
        <td>
            <select id="grade21">
                <option value="0" disabled="">評価　</option>
                <option value="5">A+</option>
                <option value="4">A</option>
                <option value="3">B</option>
                <option value="2">C</option>
                <option value="0">F</option>
            </select>
            <select id="credit21">
                <option value="0" disabled="">単位数</option>
                <option>0</option>
                <option>1</option>
                <option>2</option>
                <option>3</option>
                <option>4</option>
            </select>
        </td>
        <td>
            <select id="grade22">
                <option value="0" disabled="">評価　</option>
                <option value="5">A+</option>
                <option value="4">A</option>
                <option value="3">B</option>
                <option value="2">C</option>
                <option value="0">F</option>
            </select>
            <select id="credit22">
                <option value="0" disabled="">単位数</option>
                <option>0</option>
                <option>1</option>
                <option>2</option>
                <option>3</option>
                <option>4</option>
            </select>
        </td>
        <td>
            <select id="grade23">
                <option value="0" disabled="">評価　</option>
                <option value="5">A+</option>
                <option value="4">A</option>
                <option value="3">B</option>
                <option value="2">C</option>
                <option value="0">F</option>
            </select>
            <select id="credit23">
                <option value="0" disabled="">単位数</option>
                <option>0</option>
                <option>1</option>
                <option>2</option>
                <option>3</option>
                <option>4</option>
            </select>
        </td>
        <td>
            <select id="grade24">
                <option value="0" disabled="">評価　</option>
                <option value="5">A+</option>
                <option value="4">A</option>
                <option value="3">B</option>
                <option value="2">C</option>
                <option value="0">F</option>
            </select>
            <select id="credit24">
                <option value="0" disabled="">単位数</option>
                <option>0</option>
                <option>1</option>
                <option>2</option>
                <option>3</option>
                <option>4</option>
            </select>
        </td>
    </tr>
    <tr>
        <td>他</td>
        <td>
            <select id="grade25">
                <option value="0" disabled="">評価　</option>
                <option value="5">A+</option>
                <option value="4">A</option>
                <option value="3">B</option>
                <option value="2">C</option>
                <option value="0">F</option>
            </select>
            <select id="credit25">
                <option value="0" disabled="">単位数</option>
                <option>0</option>
                <option>1</option>
                <option>2</option>
                <option>3</option>
                <option>4</option>
            </select>
        </td>
        <td>
            <select id="grade26">
                <option value="0" disabled="">評価　</option>
                <option value="5">A+</option>
                <option value="4">A</option>
                <option value="3">B</option>
                <option value="2">C</option>
                <option value="0">F</option>
            </select>
            <select id="credit26">
                <option value="0" disabled="">単位数</option>
                <option>0</option>
                <option>1</option>
                <option>2</option>
                <option>3</option>
                <option>4</option>
            </select>
        </td>
        <td>
            <select id="grade27">
                <option value="0" disabled="">評価　</option>
                <option value="5">A+</option>
                <option value="4">A</option>
                <option value="3">B</option>
                <option value="2">C</option>
                <option value="0">F</option>
            </select>
            <select id="credit27">
                <option value="0" disabled="">単位数</option>
                <option>0</option>
                <option>1</option>
                <option>2</option>
                <option>3</option>
                <option>4</option>
            </select>
        </td>
        <td>
            <select id="grade28">
                <option value="0" disabled="">評価　</option>
                <option value="5">A+</option>
                <option value="4">A</option>
                <option value="3">B</option>
                <option value="2">C</option>
                <option value="0">F</option>
            </select>
            <select id="credit28">
                <option value="0" disabled="">単位数</option>
                <option>0</option>
                <option>1</option>
                <option>2</option>
                <option>3</option>
                <option>4</option>
            </select>
        </td>
        <td>
            <select id="grade29">
                <option value="0" disabled="">評価　</option>
                <option value="5">A+</option>
                <option value="4">A</option>
                <option value="3">B</option>
                <option value="2">C</option>
                <option value="0">F</option>
            </select>
            <select id="credit29">
                <option value="0" disabled="">単位数</option>
                <option>0</option>
                <option>1</option>
                <option>2</option>
                <option>3</option>
                <option>4</option>
            </select>
        </td>
    </tr>
</table>
```
</details>

## 完成
完成しました。立命館大学専用 GPA 計算ツールです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2293026/7dd2e859-4b08-4e64-7c3a-7a9bb3030fc0.png)


## Google 検索からユーザーを獲得したい
せっかく作ったサイトなのでどうせなら多くの人に利用してもらいたいですよね。ということでデプロイ先の URL を [Google Search Console](https://search.google.com/search-console/about) に登録しました。ここに登録すると、サイトを手っ取り早く Google 検索に引っ掛けることができます。

現在は検索順位が下がってしまいましたが、SEOは大成功でした。
↓↓(2022.10.25)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2293026/9732431e-e70c-2793-c0ac-0457b437b777.png)

Google さんから嬉しいお知らせが届きます。しっかりとGoogle 検索からのユーザーを獲得しているみたいですね。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2293026/e6a12a5f-0c3e-4885-f01d-76509ecc6758.png)

SEOについてはこちらの記事に詳しく書きました！

https://qiita.com/hirokiwa/items/0451cc855ddb2345c676

## まとめ
GPA 計算サイトを作って、無事に GPA を計算することができました。みなさんも GPA を知りたくなった際にはこの記事を参考にして、ぜひGPA計算サイトの開発に取り組んでいただければと思います。

↓↓使ってね〜

https://rits-gpa.vercel.app/