「おまえは今まで食ったパンの枚数をおぼえているのか?」→「はい」

<img width="100%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/986141/3e685542-2762-b91d-3173-3afa07e1ba0d.png">

[「ジョジョの奇妙な冒険」より](https://x.com/jojocomic_app/status/984249747404935169)


# 絶体絶命の時にパンの数をスッと言えるアプリをつくりました
## 今まで食ったパンカウンター
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/986141/bf20a904-ac61-e76f-6b2b-cbcf0c032db1.png)

サイトは[こちら](http://breadcounter-bucket.s3-website-ap-northeast-1.amazonaws.com/)です。

(12/2)また再開するかもしれませんが、現在はカウントの更新を止めています


# つくり方
パンの数を保持する部分はデータベースを用意し、パンの絵文字をタップするごとにAPIリクエストを行います。
アーキテクチャとそれぞれの役割は以下の通りです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/986141/67c96c18-2c49-f928-9690-9e1b6fef1f4d.png)

:::note warn
1. S3: 静的なWebコンテンツ(今回はHTML)のホスティング

2. CloudFront: S3のコンテンツをキャッシュし高速配信
※こちらはなくても構築できます

3. Lambda: API Gatewayからのリクエストを処理し、DynamoDBを操作

4. API Gateway: フロントエンドとバックエンドをつなぐAPIエンドポイント

5. Amazon DynamoDB: ユーザーのカウントデータの保存
:::

基本的なAWSのコンポーネントを使いこなします。

## S3
S3バケットを作成します。名前は`breadcounter-bucket`としました。

![S3backet.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/986141/40364b25-ca2f-f011-fe7a-44496c9bcefb.png)

ここに`html.index`をアップロードします。

![s3upload0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/986141/ca69614c-e70e-1335-19db-26a40c3a43a8.png)

のちに作るAPI GatewayのURLをこのファイル内に追加します。記事上は行き来することになってしまうのでソースコード上では`"https://your-api-gateway-url"`としています。

```HTML
<script>
        const apiUrl = "https://your-api-gateway-url";
```

<details><summary>ソースコード</summary>

```HTML
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>お前が今まで食ったパンの枚数</title>
    <style>
        body {
            background-color: black;
            color: white;
            font-family: Arial, sans-serif;
            text-align: center;
            margin: 0;
            padding: 0;
        }
        h1 {
            margin-top: 20px;
        }
        #emoji-container {
            display: flex;
            justify-content: center;
            gap: 20px;
            margin: 40px auto;
            flex-wrap: wrap;
        }
        .emoji {
            font-size: 50px;
            background: none;
            border: none;
            cursor: pointer;
            padding: 10px;
        }
        .emoji:focus {
            outline: none;
        }
        #results {
            margin-top: 20px;
            font-size: 20px;
        }
    </style>
</head>
<body>
    <h1>お前が今まで食ったパンの枚数</h1>
    <div id="emoji-container">
        <button class="emoji" data-id="bread">🍞</button>
    </div>
    <div id="results">
        パンの絵文字を押したの回数がここに表示されます。
    </div>
    <script>
        const apiUrl = "https://your-api-gateway-url";

        async function updateCount(emojiId) {
            const response = await fetch(`${apiUrl}/count`, {
                method: "POST",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify({ emojiId }),
            });
            const data = await response.json();
            displayResults(data);
        }

        async function getCounts() {
            const response = await fetch(`${apiUrl}/counts`);
            const data = await response.json();
            displayResults(data);
        }

        function displayResults(data) {
            const resultsDiv = document.getElementById("results");
            resultsDiv.innerHTML = "";
            const breadCount = data["bread"] || 0; // パンのカウントを取得
            resultsDiv.innerHTML = `<p>🍞: ${breadCount}回</p>`;
        }

        document.getElementById("emoji-container").addEventListener("click", (event) => {
            if (event.target.classList.contains("emoji")) {
                const emojiId = event.target.dataset.id;
                updateCount(emojiId);
            }
        });

        getCounts(); // ページロード時にカウントを取得
    </script>
</body>
</html>

```
</details>

アップロードできたらこちらは完了です。

![s3upload.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/986141/1984718a-8047-4eae-e3e3-07f7318a8f77.png)

## Lambda
データベースを操作する関数をPythonで書きます。
関数名は`func-breadcounter`としました。ファイル名は何でも良いです。

![lambda.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/986141/950744ae-45e0-34e4-e9d2-e518144420ef.png)


<details><summary>ソースコード</summary>

```Python
import json
import boto3
from boto3.dynamodb.conditions import Key

dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('EmojiCounts')

def lambda_handler(event, context):
    if event['httpMethod'] == 'POST':
        body = json.loads(event['body'])
        emoji_id = body['emojiId']
        response = table.update_item(
            Key={'emojiId': emoji_id},
            UpdateExpression="ADD count :increment",
            ExpressionAttributeValues={':increment': 1},
            ReturnValues="UPDATED_NEW"
        )
        return {
            'statusCode': 200,
            'body': json.dumps({'emojiId': emoji_id, 'count': response['Attributes']['count']})
        }
    elif event['httpMethod'] == 'GET':
        response = table.scan()
        counts = {item['emojiId']: item['count'] for item in response['Items']}
        return {
            'statusCode': 200,
            'body': json.dumps(counts)
        }

```
</details>

こちらをデータベースのテーブル名として使います。
```python
table = dynamodb.Table('EmojiCounts')
```

デプロイをしたらこちらは完了です。
![lambdadeploy.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/986141/4fa79b1e-a15e-465a-4d3a-02bd976ab163.png)


## API Gateway
「APIを作成」からREST APIを作成します。

![restapi.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/986141/16b90c29-8297-652d-f03f-ce2ba41dca69.png)

エンドポイントを以下のように設定します。

:::note 
- GETメソッド：/counts
データベースから現在のカウントを取得
- POSTメソッド：/count
カウントを更新
:::

ここで先ほど作ったLambdaと統合します。

![get.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/986141/da9348f9-604d-b723-6d42-b720ac4f7854.png)


こちらもデプロイしたら完了です。ステージ名は`breadcounter`にしています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/986141/791cf692-f919-6ab2-653e-3626e08bce94.png)

サイドバーの「ステージ」にAPIのURLがあるので、これを上で作成した`index.html`の`"https://your-api-gateway-url"`に入れて完了です。

## DynamoDB
Lambdaで作成したコードに合わせてキーを設定していきます。
「テーブルを作成」からテーブル名とパーティションキー、ソートキーを入れます。

![dynamodb.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/986141/b2432cd4-7f35-71c0-0782-186969edca8b.png)

データベースはこれだけです！

## できたものを確認
WebサイトをホスティングしたS3バケットのエンドポイントを使います。
形式は次の通りです。
```
http://<バケット名>.s3-website-<リージョン>.amazonaws.com
```

正しくパンの枚数がカウントできているかを確認する方法はいろいろとあります。
1. **API Gatewayのログ**
「ステージ」→「ログ/トレース」→CloudWatchのログを確認
1. **DynamoDBを直接確認**
「テーブル」→「項目」→レコードを確認
1. **Lambdaのログを確認**
「モニタリング」→CloudWatchのログを確認
1. **Postmanなどのテストツール**
例えばPOSTメソッドならJSONデータを送信しカウントを更新します

```bash
curl -X POST https://your-api-gateway-url/count \
-H "Content-Type: application/json" \
-d '{"emojiId": "bread"}'
```

## 参考

https://x.com/jojocomic_app/status/984249747404935169

つくり方は違いますがこちらを参考にしました。

https://punchyourclient.netlify.app/?ref=producthunt