両替計算アプリを作成してみた

## はじめに
初めましての人も、そうでない人も、こんにちは！

少し前の話になるんですけど、とある企業の面接を受けているときに面接官から、「あなた、Qiitaを始めた最初のきっかけが就活目的なんですね。」と聞かれて、びっくりしました！

いや！確かに結構前のQiita記事に書いたよ！！
書いたけど、そこまでしっかりみてくれているとなんか・・・恥ずかしいです！

しかし、あの時の面接官様、ありがとうございます！
今までそこそこ面接をしてきたんですけど、見ていない人、タイトルだけ見た人、内容を少し見ている人と様々でしたが、ここまでしっかりみてくださった面接官は先ほどの人のみで、恥ずかしさもありながら少し嬉しかったですね！
ちなみに、その記事は以下に載せておくので、ぜひご覧ください！(この記事書いたの2024年6月かぁ。時が経つの早いですね)

https://qiita.com/FrohleinYoshie/items/36a52525ac2f4dcdd3d8

## 今回の開発物について
今回は、両替計算Webアプリを作成したいと思います！

私事ではありますが、とある組織の会計を担当させていただいています！
そして、つい先日、銀行からお金を引き出して、とある3人の人物に渡そうとしたんですけど、あまりに金額が大きすぎてお金を崩さないといけなくなりました！
正直、崩すための計算はできるんですけど、正直めんどくさくないですか？
それに、多分あと3回は同じことをしないといけないため、辛いです！

なので今回は、現在の金額の状況から、どのように両替を行えばいいのか計算するアプリを作成しました！
ぜひ最後までご覧ください！
(正直PayPayとか電子でやりとりしたい・・・)

## 今回の開発物に関して欲しい機能
- 所持金入力機能
- 両替希望金額入力機能
- 金額一致チェック機能
- 両替結果表示機能

## 使用技術
- React + TypeScript (フロントエンド)
- Go (バックエンド)

## 主なディレクトリ構成
```
ryougae/
│
├── frontend
│    ├── src
│    │   ├── App.tsx
│    │   ├── App.css
│    │   └── ...
│    └── ...
└── backend
     ├── main.go
     ├── go.sum
     └── go.mod
```

それでは作成していきたいと思います！

## 作成する
**フロントエンド**
まずはReactと使うための環境を作ります！
```
npx create-react-app frontend --template typescript
cd frontend
npm install axios
```
次に`srcフォルダ`にある`app.tsx`を編集します！

```tsx:src/ app.tsx
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import './App.css';

interface DenominationCount {
  [key: number]: number;
}

interface ExchangeResult {
  from: DenominationCount;
  to: DenominationCount;
  totalCurrent: number;
  totalTarget: number;
}

const denominations = [10000, 5000, 1000, 500, 100, 50, 10, 5, 1];

const App: React.FC = () => {
  const [currentMoney, setCurrentMoney] = useState<DenominationCount>(
    Object.fromEntries(denominations.map(d => [d, 0]))
  );
  const [targetAmounts, setTargetAmounts] = useState<number[]>([0]);
  const [result, setResult] = useState<ExchangeResult | null>(null);
  const [error, setError] = useState<string | null>(null);
  const [activeTab, setActiveTab] = useState<'current' | 'target'>('current');

  useEffect(() => {
    calculateExchange();
  }, [currentMoney, targetAmounts]);

  const handleCurrentMoneyChange = (denomination: number, value: string) => {
    const numValue = value === '' ? 0 : parseInt(value, 10);
    if (!isNaN(numValue) && numValue >= 0) {
      setCurrentMoney(prev => ({ ...prev, [denomination]: numValue }));
    }
  };

  const handleTargetAmountChange = (index: number, value: string) => {
    const numValue = value === '' ? 0 : parseInt(value, 10);
    if (!isNaN(numValue) && numValue >= 0) {
      setTargetAmounts(prev => {
        const newAmounts = [...prev];
        newAmounts[index] = numValue;
        return newAmounts;
      });
    }
  };

  const addTargetAmountField = () => {
    setTargetAmounts(prev => [...prev, 0]);
  };

  const removeTargetAmountField = (index: number) => {
    setTargetAmounts(prev => prev.filter((_, i) => i !== index));
  };

  const calculateExchange = async () => {
    try {
      const response = await axios.post<ExchangeResult>('http://localhost:8080/calculate-exchange', {
        currentMoney,
        targetAmounts: targetAmounts.filter(amount => amount > 0)
      });
      setResult(response.data);
      setError(null);
    } catch (err) {
      if (axios.isAxiosError(err) && err.response) {
        setError(`計算エラー: ${err.response.data}`);
      } else {
        setError('計算中に予期せぬエラーが発生しました。');
      }
      setResult(null);
    }
  };

  const renderDenominationCount = (counts: DenominationCount) => {
    return (
      <div className="denomination-count">
        {denominations.map(denom => (
          counts[denom] > 0 && (
            <div key={denom} className="denomination-item">
              <span className="denomination-value">{denom}円</span>
              <span className="denomination-count">{counts[denom]}枚</span>
            </div>
          )
        ))}
      </div>
    );
  };

  const getDenominationLabel = (denomination: number) => {
    if (denomination >= 1000) {
      return `${denomination}円札`;
    } else {
      return `${denomination}円硬貨`;
    }
  };

  return (
    <div className="app-container">
      <h1 className="app-title">両替計算機</h1>
      
      <div className="tab-container">
        <button 
          className={`tab-button ${activeTab === 'current' ? 'active' : ''}`}
          onClick={() => setActiveTab('current')}
        >
          現在の金額
        </button>
        <button 
          className={`tab-button ${activeTab === 'target' ? 'active' : ''}`}
          onClick={() => setActiveTab('target')}
        >
          両替したい金額
        </button>
      </div>

      <div className="input-container">
        <div className={`input-section ${activeTab === 'current' ? 'active' : ''}`}>
          <h2>現在の金額（枚数を入力）</h2>
          <p className="input-description">各金種の枚数を入力してください。</p>
          <div className="denomination-inputs">
            {denominations.map(denomination => (
              <div key={denomination} className="denomination-input">
                <label>{getDenominationLabel(denomination)}:</label>
                <div className="input-with-unit">
                  <input
                    type="text"
                    inputMode="numeric"
                    pattern="\d*"
                    value={currentMoney[denomination] || ''}
                    onChange={(e) => handleCurrentMoneyChange(denomination, e.target.value)}
                    placeholder="0"
                  />
                  <span className="input-unit">枚</span>
                </div>
              </div>
            ))}
          </div>
          <p className="total-amount">合計: {result?.totalCurrent.toLocaleString() ?? 0}円</p>
        </div>

        <div className={`input-section ${activeTab === 'target' ? 'active' : ''}`}>
          <h2>両替したい金額</h2>
          <div className="target-amount-inputs">
            {targetAmounts.map((amount, index) => (
              <div key={index} className="target-amount-input">
                <input
                  type="text"
                  inputMode="numeric"
                  pattern="\d*"
                  value={amount || ''}
                  onChange={(e) => handleTargetAmountChange(index, e.target.value)}
                  placeholder="金額を入力"
                />
                {index > 0 && (
                  <button onClick={() => removeTargetAmountField(index)} className="remove-button">
                    削除
                  </button>
                )}
              </div>
            ))}
            <button onClick={addTargetAmountField} className="add-button">
              金額を追加
            </button>
          </div>
          <p className="total-amount">合計: {result?.totalTarget.toLocaleString() ?? 0}円</p>
        </div>
      </div>

      {error && <p className="error-message">{error}</p>}

      {result && (
        <div className="result-section">
          <h2>計算結果</h2>
          <div className="result-content">
            <div className="result-column">
              <h3>元の金額:</h3>
              {renderDenominationCount(result.from)}
            </div>
            <div className="result-column">
              <h3>両替後の金額:</h3>
              {renderDenominationCount(result.to)}
            </div>
          </div>
        </div>
      )}
    </div>
  );
};

export default App;
```
このコードでは、主に情報がない場合と0は同等の扱いにする処理や、両替したい金額の入力フォームを追加・削除、バックエンドに入力データを送信・取得を行っています！

あとは、App.cssを操作して、みなさんのお好みでデザインをしてみてください！

**バックエンド**
それではまずGoを使うための環境を作っていきます！
```
mkdir backend
cd backend
touch main.go
go mod init backend
go get github.com/gorilla/mux
go get github.com/rs/cors
```


```backend/ main.go
package main

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"

	"github.com/gorilla/mux"
	"github.com/rs/cors"
)

type DenominationCount map[int]int

type ExchangeRequest struct {
	CurrentMoney  DenominationCount `json:"currentMoney"`
	TargetAmounts []int             `json:"targetAmounts"`
}

type ExchangeResult struct {
	From         DenominationCount `json:"from"`
	To           DenominationCount `json:"to"`
	TotalCurrent int               `json:"totalCurrent"`
	TotalTarget  int               `json:"totalTarget"`
}

var denominations = []int{10000, 5000, 1000, 500, 100, 50, 10, 5, 1}

func calculateExchange(w http.ResponseWriter, r *http.Request) {
	var req ExchangeRequest
	err := json.NewDecoder(r.Body).Decode(&req)
	if err != nil {
		http.Error(w, fmt.Sprintf("リクエストの解析に失敗しました: %v", err), http.StatusBadRequest)
		return
	}

	result, err := processExchange(req)
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(result)
}

func processExchange(req ExchangeRequest) (ExchangeResult, error) {
	totalCurrent := calculateTotal(req.CurrentMoney)
	totalTarget := sum(req.TargetAmounts)

	if totalCurrent != totalTarget {
		return ExchangeResult{}, fmt.Errorf("現在の金額(%d円)と目標金額(%d円)の合計が一致しません", totalCurrent, totalTarget)
	}

	result := ExchangeResult{
		From:         cleanDenominationCount(req.CurrentMoney),
		To:           make(DenominationCount),
		TotalCurrent: totalCurrent,
		TotalTarget:  totalTarget,
	}

	for _, targetAmount := range req.TargetAmounts {
		remaining := targetAmount
		for _, denom := range denominations {
			count := remaining / denom
			if count > 0 {
				result.To[denom] += count
				remaining -= count * denom
			}
		}
	}

	result.To = cleanDenominationCount(result.To)
	return result, nil
}

func calculateTotal(dc DenominationCount) int {
	total := 0
	for denomination, count := range dc {
		total += denomination * count
	}
	return total
}

func sum(amounts []int) int {
	total := 0
	for _, amount := range amounts {
		total += amount
	}
	return total
}

func cleanDenominationCount(dc DenominationCount) DenominationCount {
	cleaned := make(DenominationCount)
	for _, denom := range denominations {
		if count, exists := dc[denom]; exists && count > 0 {
			cleaned[denom] = count
		}
	}
	return cleaned
}

func main() {
	r := mux.NewRouter()
	r.HandleFunc("/calculate-exchange", calculateExchange).Methods("POST")

	c := cors.New(cors.Options{
		AllowedOrigins: []string{"http://localhost:3000"},
		AllowedMethods: []string{"GET", "POST", "OPTIONS"},
		AllowedHeaders: []string{"Content-Type", "Authorization"},
	})

	handler := c.Handler(r)

	log.Println("Server is running on http://localhost:8080")
	log.Fatal(http.ListenAndServe(":8080", handler))
}
```
ここでは、フロント側から情報を受け取って、金額が一致するのかを確かめています！
そして、最適な両替結果を導き出して、それをJSON形式で返すことを行っています！

## 実行してみよう！
それでは`frontend`と`backend`のディレクトリに移り実行してみてください！
```
npm start
```
```
go run .
```

すると・・・

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3631203/657df36b-6fb0-35bd-a920-f95ea2d8d612.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3631203/5f728d22-d573-451d-1fe0-6b6cbd69e282.png)


このように表示させることができました！

今回は一万円と千円札が2枚ずつと百円が3枚、十円が2枚あります！
それを4973円と11283円と6064円にきっちりと支払えるように両替をしたいです！

それではやっていきましょう！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3631203/181b7dd6-51fb-89cb-ca44-a613de60edf2.png)

こんな感じで入力すると・・・

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3631203/1dd2a3d1-9439-0e55-7536-c05098f22b5a.png)

うまく出力されました！

実際にこの通りに両替を行うときっちりと支払えるようになりました！

## おわりに
本来は、この記事を書く予定は全くありませんでした！
なんなら、今回のような出来事がなければ、作る予定すらありませんでした！

いやー、やっぱり何事にも経験ですね！
これ、デプロイとかして私の後釜の人に継承してもいいレベルでしょ（慢心）。

今回の記事はいかがだったでしょうか？
またどこかの記事でお会いしましょう！

## GithubURL

https://github.com/FrohleinYoshie/ryougae