「手軽に、身軽に、行きたいお店」が見つかるサービスを作った話(ハッカソン参加レポート)


## はじめに

当資料はとあるハッカソンでの成果物をまとめたものです。※テクニカルサポーター賞(LINE)いただきました！

初めてのハッカソン参加でしたがいろいろと学びがあったので共有しようと思います。

<details><summary>ハッカソン自体の概要　※運営の方に転載許可いただいています。</summary><div>

# ハッカソン概要
### テーマ：シン・ノーマル時代のHappy
2020年の緊急事態宣言から2年が経過をし、Covid-19の影響も少しずつ変化し、「ニューノーマル」は、それぞれの「新しいノーマル」に形を変えている。
本ハッカソンでは、そのような現在（シン・ノーマル時代）の幸せについて、改めて考察し、Happyな気持ちになるプロダクトを創造頂きます。


### 開催概要
##### 開催日程 
- アイデアソン・チームビルディング（全員参加） ：10/11(火) 10:00-16:00
- LINEハンズオン（任意参加・別途申し込み）：10/26水  14:00-17:00
- M5Stack　ハンズオン（任意参加・別途申し込み）：10/25火　13:00-16:00
- メタデータハンズオン（任意参加・別途申し込み）：10:27木　13:00-16:00
- ローコードハンズオン（任意参加・別途申し込み）：10/28金　13:00-16:00
- ハッカソン（全員参加）：11/7(月) ,11/8(火)　9:00〜18:00


##### チームビルディングのルール
アイデアソンにて、ハッカソンへ一緒に挑む、仲間とアイデアを見つけていただきます。（2-5名）
また、6社共同の機会となりますので、1チームに必ず2社以上の方が参加いただくことをルールといたします。

##### グループ分け
本ハッカソンの参加者は合計80名超となるため、チームビルディング・成果発表などはαグループ・βグループのグループ毎に行います。
（最終日の成果発表も2部屋にわかれて実施）


##### 審査基準
- アイデア　新規性・独創性・オリジナリティ
- チャレンジ　技術へのチャレンジ・こだわり
- 完成度　プロトタイプの完成度・出来栄え

##### 賞
- 最優秀賞　1作品
- 優秀賞　2作品
- テクニカルサポーター賞　　各1作品ずつ
- オーディエンス賞（A部屋1,2位、B部屋1,2位）
- 事務局賞　1作品

※個人賞　参加者が「すごい！」って思ったチームに提供


##### 外部審査員
αグループ
- 千代田　まどかさん（ [chomado](https://twitter.com/chomado)　）
- 菊地 仁さん（クアドラプル株式会社　代表取締役）
- うこさん（ [ukoq](https://twitter.com/ukokq) ）

βグループ
- 栗原　　一貴さん(津田塾大学学芸学部情報科学科 教授 クーリード株式会社 CTO　　情報理工学)
- 菅原　のびすけさん（プロトタイピング専門スクール「ProtoOutStudio」責任者）
- 木戸　康平さん（株式会社Obniz/Co-founder）


##### 審査・発表のあり方
![スクリーンショット 2022-10-11 14.35.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216883/3396e397-20d9-7252-8f9e-5dbbb3d00a2e.png)

αグループ司会：伴野
βグループ司会：ひげだるま（一般社団法人MA 理事　[masaya3](https://twitter.com/masaya3)）



##### ハッカソン当日の作業場所について
ハッカソン参加時チームで集まって作業頂いても問題ありません。
集まっていただく際の諸注意を以下記載しておりますので、ご確認いただきチームごとにお話ください。

 - 運営側で場所の用意はいたしません。チームメンバーの方のオフィス会議室や作業スペースなどを通常の手続きにてご手配ください（ゲストwi-fiなどがあるか、必ずご確認を頂き手配ください）
 - アナウンスなどはオンラインで行います。オンライン会場にも参加をいただけますようお願いいたします
 - 感染症対策などについても、各社のルールに則って対応をお願いいたします
 - ハウリング対策として、共有で扱えるスピーカーや個別のイヤホンなどをご用意頂いたほうが参加しやすいと思いますご参考ください。



##### イベントのタイムテーブル
**■アイデアソン　10/11(火) 10:00-16:000**
10:00-10:10　オープニング・主催挨拶
10:10-10:25　アイスブレイク
10:25-10:35　進行説明
10:35-11:00　テクニカルインプット（機能説明）
11:00-12:00　アイデア発散（個人）
12:00-12:40　チームビルディング
12:40-14:40　アイデアソン&ランチ（チーム）
14:40-15:20　アイデア発表　2分（90秒発表＋30秒入れ替え）×20チーム程度
15:20-16:00　クロージング、今後の案内


**■ハッカソン　11/7(月) ,11/8(火)　9:00〜18:00** 

ハッカソン1日目9:00-18:00
09:00-09:10　オープニング・シート記入のご案内（10:00ほどまでに最新化）
09:10-09:30　ハッカソン開発tips / ハッカソンテクニカルサポート説明
09:30-09:40　チーム集合写真を撮影し、#01雑談　のSlackチャンネルに投稿
09:30-12:00　ハッカソン時間
12:00-12:45　（適当な時間に）各自ランチ　チーム集合写真を撮影し、#01雑談　のSlackチャンネルに投稿
12:45-13:00　発表順番　抽選会（あみだくじ）
13:00-17:30　ハッカソン時間
17:15-18:00　中間発表・クロージング

ハッカソン2日目　9:00-18:00
09:00-09:10　オープニング・スケジュール確認
09:10-09:20　テクニカルサポート説明
09:20-13:30　ハッカソン時間（各自　ZOOMでの接続確認）
-13:00　　　 作品エントリー〆切　[ProtoPediaに作品登録] (https://protopedia.gitbook.io/helpcenter/registration)　　
　※登録後 #01雑談チャンネルに　URLとチーム名を投稿ください
14:00-15:30　成果発表（4分発表＋3分質疑　1分入れ替え）×各部屋10チーム程度　（α・βそれぞれZのZOOMに移動）
15:30-16:00　審査（参加者はoViceへ）
16:00-17:00　決勝参加組発表・決勝プレゼン・相互投票（oViceにて発表）
17:15-17:30　審査会 兼 交流会
17:30-18:00　賞の発表・クロージング　
18:30-19:30 任意で参加の懇親会


# 利用ツール
本イベントでは主に以下4つのツールを利用します。
開催までにアクセスの確認をお願いいたします。

| ツール名 | 利用方法 |
|:-|:-|
|[oVice](https://ovice.in/) | ヴァーチャルスペースツール。本イベントでのメインツールとして利用します  |
|[Zoom](https://explore.zoom.us/ja/products/meetings/)   | 成果発表・ハンズオンはZoomにて行います。  |
|[Googleスプレッドシート](https://www.google.com/intl/ja_jp/sheets/about/)   | アイデア発散、チーム結成用のツールとして利用いたします  |
|[Slack](https://slack.com/intl/ja-jp/)   | コミュニケーションツール。事務局からのお知らせ、チーム内コミュニケーションにご利用いただきます。  |


### テクニカルサポート情報
今回のハッカソンの特徴は充実した技術サポート。
4つの企業・サービスの方に、ハッカソンアウトプットの技術サポートをいただきます。
新しい技術にチャレンジし、新しいクリエイティブを生み出すきっかけにしましょう！
**※４つのサポート技術のうち、1つ以上使用することをルールとします。**

##### ①メタデータ株式会社
https://protopedia.net/company/metadata

複数の解析系APIを提供頂きます。
- 何時何処何誰API（5W1H API）
「何時何処何誰ＡＰＩ」は、出来事を表すメタデータの5W１H「いつ、どこで、何を、誰が、どのように、いくらほど(数量表現)」などを日本語のべた書きテキストから様々な形で抽出します。
- 感情解析API
　感情解析APIは日本語文章を「好ましい―嫌い」「嬉しい―悲しい」「怒り―怖 れ」の3軸で、6種の感情をそれぞれ3段階 +1 (0=中立) で解析し、これら３つの数値を出力します。 文章に表現された感情を手軽に抽出することができます。

##### ②M5Stack（小型マイコン）
https://protopedia.net/material/540

約5cm×5cmの正方形のケースの中にWi-FiとBluetoothによる無線通信機能を備えたCPU（ESP32）をはじめ、カラー液晶ディスプレイ・プッシュボタン・バッテリーなどの周辺部品がひとつのモジュールにまとまっている、小型のマイコンモジュールです。

▼アイデアソン用情報
https://github.com/1ft-seabass/ma-hackathon-qa-and-knowledge-202210/blob/master/docs/ideathon/README.md


##### ③LINE
https://developers.line.biz/ja/services/messaging-api/

Messaging APIはBotを作るためのAPIです。友だちや家族のように、話しかけると応答してくれるBotを作ることができます。Botの処理は自由にカスタム可能ですので、例えば翻訳をしたり、為替の計算をしたり、Q&Aを作ったりなど、使い慣れたLINEのインターフェース上で便利な機能をユーザーに提供できます。

**[LINEミニアプリ](https://developers.line.biz/ja/docs/line-mini-app/discover/introduction/)**
LINEミニアプリはLINE上で動くアプリケーションを作ることが出来る機能です。LINEの持つ認証、決済等の機能を組み込んだ、ネイティブアプリのようなリッチなインターフェースのアプリケーションを作ることが可能です。

**他**
・[LINE Pay API（サービスに決済を導入できます）](https://pay.line.me/jp/developers/apis/onlineApis?locale=ja_JP)
・[LINE Bot Designer（LINE Botのプロトタイプをブラウザ上で簡単に作ることができます）](https://developers.line.biz/ja/services/bot-designer/)

##### ④enebular（ローコードツール）
プロトタイピングの一つの側面である「すばやくつくる」という点で、プログラムしっかり書かなくても GUI で仕組みが作れるノーコード・ローコードツールが様々な業界で登場し注目されており、ebebularもその一つです。

▼アイデアソン用情報
https://github.com/1ft-seabass/ma-hackathon-qa-and-knowledge-202210/blob/master/docs/ideathon/README.md

参考URL
https://node-red.ia-cloud.com/enebular 


### 非公式テクニカルサポート
テクニカルサポートはありませんが、機器の貸し出し・過去のハンズオン動画の共有を行うと共に、参加者同士で質問をしあい解決していくようなSlackにて相談チャンネルを設けます。

##### ■obniz
https://obniz.io
https://docs.obniz.io
obniz(オブナイズ)は電子回路が一体となったAPIです． APIで電子回路を操作することができます．
APIから電子回路を操作することがで，簡単に実世界の動きとソフトウェアを連携することができます．
たとえば，モーターをobnizに繋げばLineのメッセージで動き出すラジコンを作ることができたり，温度センサを繋げばいつでもAPI経由で部屋の温度を計測したりすることができます．
[ハッカソンに役立つTips](https://blog.obniz.io/en/example/)
[2021年ハンズオン動画](https://youtu.be/gC_3yai9bv4)
※機器の貸し出しは別途受付を行います。


#### ロゴとステッカーについて
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216883/baa0bcde-a626-c26e-5c2d-34d29ffcc853.png" width="300">


本ハッカソンのロゴを添付にて用意をいたしました。
また、こちらのステッカーを各社様事務局宛にお送りしております。
欲しい！　という方は、各社事務局の方にご連絡をお願いいたします。

合わせて、ロゴデータとZOOM用背景を作成しております。
Png形式ではありますが、自由にご使用いただけると幸いです。
（発表資料などに使って頂いても問題ありません）

▼ロゴ・背景
https://drive.google.com/drive/folders/1c3lsyaNn28PxW8jCYAIUh0swq8Zowcfl?usp=sharing




</div></details>


## 作ったもの
「手軽に、身軽に、行きたいお店」が見つかるサービスです。 
![BLACK BIRD logo,dove logo template.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/649608/bac37314-16d7-97ca-b9de-3cc5ec79252a.jpeg)




※友達登録用QRコード（利用状況によってサービスを停止する場合があります。）
<img width="150" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/649608/81f73ad2-4257-ad1d-82c9-1e8ce24b60f5.jpeg">

### 機能紹介

https://www.youtube.com/watch?v=-VjaJZ_a_Fo

<img width="150" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/649608/231c05f6-5f68-e75c-319c-e5552d063e1c.png">


### プレゼン資料

https://www.canva.com/design/DAFQqPQzz4Q/OcBdj-zREPHNSYyhxH_3UA/view?utm_content=DAFQqPQzz4Q&utm_campaign=designshare&utm_medium=link&utm_source=publishpresent

### アピールポイント
#### ３タップ以内！「Fast」なUI設計
- 余計なやり取りを極力排除し、必要な情報に最短でアクセスできるようにしました。
- 1画面に表示する情報量を増やし、多くの画面遷移を行わなくてもいいようにしました。
#### 多機能＆シンプル！リッチメニューの切替
- リッチメニューのサイズを広げるのではなく切り替えとすることで、トークルームに表示される領域を広く確保しました。
#### パーソナライズ！お気に入り、Discover機能
- サーバー上でユーザIDごとにデータを管理し、末永く利用したいと思えるものにしました。
    - お気に入り機能
        - 検索結果の「お気に入り登録/解除」をタップすることで利用でき、最大100件まで保存が可能。
リッチメニュー上の「お気に入り」をタップすることで表示できます。
    - Discover
        - サーバ上に保存したデータから、普段あまり行かないお店を紹介します。（マンネリ解消）



### 運営、参加者の方からいただいたコメント
![コメント.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/649608/4316dda4-5dca-0aa2-b791-ec129487e81d.png)




※ぜひ使っていただいてフィードバックください！

https://forms.gle/H4csL1LHie7GPp2t5

<img width="150" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/649608/9b2d0788-75bf-f3b5-a3dd-aa68df66f3b8.png">


## やったこと
ざっくりスケジュールを決めて役割ごとに開発作業を行いました。
リモートだったので、適宜声を掛け合いながら画面共有をしたりして進めました。
### 想定ターゲットの決定
- 性格
    - めんどくさがり（新しいことは覚えたくない）
        - **操作は使い慣れたLINE行う。**
    - せっかち（欲しい情報に素早くたどり着きたい。時間がないわけではないので、気になったものは深く調べる。）
        - **必要最低限の情報を提示する。詳細はリンク先で確認。**
        - **サクサク動くことを優先。**
        - **同じ画面からすぐにシェア。**

- こだわり
    - できるだけ現金を持ち歩きたくない（荷物を持ちたくない。できればスマホ一つで出かけたい。）
        - **カードが使えるかどうかを提供する情報の中心にする。**
    - 食に特別なこだわりはないが大失敗はしたくない。（高すぎる、おいしくないなど）
        - **価格帯の表示**
        ※評価は取得に時間がかったのでレスポンス優先とし、リンク先で確認としました。

- 最近思っていること
    - ２～３個、同じようなお店でランチを回している（飽きてきた） 
        - **おすすめ機能の搭載　※Discoveryとして実装しました。**
     

### 役割分担
得意・不得意などに応じて誰が何をやるかを決めました。
※私はバックエンドを担当しました。


### システム構成の検討
アイデアをもとにまずは大まかなシステム構成を検討しました。
フロントエンドは「LINE」で決まっていた感じでしたが、バックエンドはハンズオン研修もあった「enebular」でいくか「AWS」で行くかを迷いました。
結局AWSで行くことになったのですが、一応並行でeneularでも作成し、うまくいった方を採用という形にしていました。

#### 構成図
![arch.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/649608/7f77566b-8c6e-fb3c-6bc2-c6ef839ba0a2.png)


### フロントエンド（LINE）
今回フロントエンドは全面的にLINEを利用しました。

#### LINE公式アカウント、MassagingAPI、FlexMessageの作成

公式のドキュメントがとても分かりやすかったので↓に従って作業しました。
（ちなみに、APIのエラーメッセージもすごくわかりやすくて、ドキュメントと合わせてLINEの中の人のやさしさを感じました。）

https://developers.line.biz/ja/docs/messaging-api/building-bot/#set-up-bot-on-line-developers-console

https://developers.line.biz/ja/docs/messaging-api/getting-started/

https://developers.line.biz/flex-simulator/

- リプライは基本的にFlex Messageを利用しています。その他テキストメッセージやスタンプメッセージもPushしています。
- Flex Messageを作成する際はFLEX MESSAGE SIMULATORを利用してベースを作り、動的に変更したい部分を修正するのがオススメです。

#### リッチメニュー

- 多機能かつ、シンプルなUIを実現するためにリッチメニューの切り替えを実装しています。（こちらはLine Official Account Managerからは作成不可なので、[こちら](https://developers.line.biz/ja/docs/messaging-api/using-rich-menus/#switching-between-multiple-rich-menus,こちら)を参考に実装しました。）

https://developers.line.biz/ja/docs/messaging-api/using-rich-menus/#using-rich-menus-introduction

※切り替え機能付きのリッチメニューの反映作業は手作業でやるのはめんどくさいのでPythonでスクリプトを組んで対応していました。（ご参考）

```python:python
from linebot import LineBotApi

from linebot.models import (
    RichMenu,
    RichMenuSize,
    RichMenuArea,
    RichMenuBounds,
    URIAction,
    PostbackAction,
    RichMenuSwitchAction,
)
from linebot.exceptions import LineBotApiError
import json
import requests

CHANNEL_ACCESS_TOKEN = {CHANNEL_ACCESS_TOKENを設定}

# リッチメニューごとにタップ位置とアクションを設定
MENU1=RichMenu(
        # リッチメニューのサイズ
        size=RichMenuSize(width=2500, height=843),
        
        # 初期状態で開くか否か
        selected=True,
        name="MENU1",
        chat_bar_text="MENU1",
        areas=[
            RichMenuArea(
                # タップできる位置の設定
                bounds=RichMenuBounds(x=25, y=90, width=687, height=677),
                # アクションの内容を設定
                action=URIAction(
                    label="aaa", uri="https://line.me/R/oaMessage/@{line_id}/?"
                ),
            ),
            RichMenuArea(
                bounds=RichMenuBounds(x=731, y=90, width=687, height=677),
                action=URIAction(label="bbb", uri="https://line.me/R/nv/location/"),
            ),
            RichMenuArea(
                bounds=RichMenuBounds(x=1433, y=90, width=725, height=728),
                action=PostbackAction(
                    label="ccc",
                    data="view_favorite@dummy",
                    display_text="お気に入り一覧",
                    input_option="closeRichMenu",
                ),
            ),
            RichMenuArea(
                bounds=RichMenuBounds(x=2155, y=90, width=370, height=691),
                action=RichMenuSwitchAction(
                    label="ddd",
                    data="richmenu-2Change@dummy",
                    # 切り替えるリッチメニューのエイリアス名を指定
                    rich_menu_alias_id="richmenu-2",
                ),
            ),
        ],
    )

MENU2=RichMenu(
        size=RichMenuSize(width=2500, height=843),
        selected=True,
        name="MENU2",
        chat_bar_text="MENU2",
        areas=[
            RichMenuArea(
                bounds=RichMenuBounds(x=377, y=90, width=687, height=677),
                action=PostbackAction(
                    label="aaa",
                    data="view_poplar@dummy",
                    display_text="人気のお店",
                    input_option="closeRichMenu",
                ),
            ),
            RichMenuArea(
                bounds=RichMenuBounds(x=1091, y=90, width=687, height=677),
                action=PostbackAction(
                    label="aaa",
                    data="Discover@dummy",
                    display_text="Discover",
                    input_option="closeRichMenu",
                ),
            ),
            RichMenuArea(
                bounds=RichMenuBounds(x=1793, y=90, width=687, height=677),
                action=URIAction(
                    label="ccc", uri="https://line.me/R/home/public/profile?id={line_id}"
                ),
            ),
            RichMenuArea(
                bounds=RichMenuBounds(x=0, y=90, width=370, height=691),
                action=RichMenuSwitchAction(
                    label="ddd",
                    data="richmenu-1Change@dummy",
                    rich_menu_alias_id="richmenu-1",
                ),
            ),
        ],
    )


class Bot:

    line_bot_api = None

    def __init__(self):
        self.line_bot_api = LineBotApi(channel_access_token=CHANNEL_ACCESS_TOKEN)

    def get_rich_menu_id(self):
        rich_menu_list = self.line_bot_api.get_rich_menu_list()
        rich_menu_ids = [value.rich_menu_id for value in rich_menu_list]
        return rich_menu_ids

    def create(self, parms):
        rich_menu_to_create = parms
        rich_menu_id = self.line_bot_api.create_rich_menu(rich_menu=rich_menu_to_create)
        return rich_menu_id

    def delete(self, rich_menu_ids):
        print(f"rich_menu_ids={rich_menu_ids}")
        for rich_menu_id in rich_menu_ids:
            self.line_bot_api.delete_rich_menu(rich_menu_id)
            print(f"{rich_menu_id}クリア")

    def set_image(self, menu_names, rich_menu_ids):
        for menu_name, rich_menu_id in zip(menu_names, rich_menu_ids):
            print(f"./{menu_name}.jpg 転送")
            with open(f"./{menu_name}.jpg", "rb") as f:
                self.line_bot_api.set_rich_menu_image(rich_menu_id, "image/jpeg", f)

    def create_alias(self, menu_ids):
        for no, menu_id in enumerate(menu_ids, 1):
            url_items = "https://api.line.me/v2/bot/richmenu/alias"
            headers = {
                "Authorization": f"Bearer {channel_access_token}",
                "Content-Type": "application/json",
            }
            item_data = {
                "richMenuAliasId": f"richmenu-{no}",
                "richMenuId": menu_id,
            }
            print(f"{menu_id} / richmenu-{no}作成")

            response = json.loads(
                requests.post(url_items, headers=headers, json=item_data).text
            )

    def get_alias(self):

        url_items = "https://api.line.me/v2/bot/richmenu/alias/list"
        headers = {
            "Authorization": f"Bearer {channel_access_token}",
        }

        response = json.loads(requests.get(url_items, headers=headers).text)
        return response

    def delete_alias(self):
        alias_list = self.get_alias()
        print(f'alias_list={alias_list["aliases"]}')
        for alias in alias_list["aliases"]:
            url_items = (
                f'https://api.line.me/v2/bot/richmenu/alias/{alias["richMenuAliasId"]}'
            )
            headers = {"Authorization": f"Bearer {channel_access_token}"}
            json.loads(requests.delete(url_items, headers=headers).text)
            print(f'{alias["richMenuAliasId"]}クリア')

    def activate_menu(self, menu_ids):
        for menu_id in reversed(menu_ids):
            url_items = f"https://api.line.me/v2/bot/user/all/richmenu/{menu_id}"
            headers = {"Authorization": f"Bearer {channel_access_token}"}
            response = json.loads(requests.post(url_items, headers=headers).text)

    def all_delete_rich_menu(self):
        self.delete(rich_menu_ids=self.get_rich_menu_id())
        print("リッチメニューIDクリア")
        self.delete_alias()
        print("エイリアスクリア")
        self.get_rich_menu_id()


def reset_menu():
    bot = Bot()
    menus = [MENU1, MENU2]
    menu_names = ["menu3-1", "menu3-2"]

    # メニューのリセット
    bot.all_delete_rich_menu()
    print("リセット完了")

    # メニューの作成
    menu_ids = [bot.create(value) for value in menus]
    print("作成完了")

    # イメージの転送
    bot.set_image(menu_names, menu_ids)
    print("イメージ作成完了")

    # エイリアスの作成
    bot.create_alias(menu_ids)
    print("エイリアス作成完了")

    # メニューの有効化
    bot.activate_menu(menu_ids)
    print("有効化作成完了")

    # 結果表示
    print(f"作成結果(メニュー)={bot.get_rich_menu_id()}")
    print(f"作成結果(メニュー)={bot.get_alias()}")


if __name__ == "__main__":
    reset_menu()

```


### バックエンド（AWS、各種API）
#### AWS Lambda、グルメサーチAPI
洗い出した機能をフロントエンドからのリクエスト形式に合わせてコツコツ実装してきました。

https://webservice.recruit.co.jp/doc/hotpepper/reference.html

##### グルメサーチAPIで店名検索を行う
```python:python
def gourmet_api_shop_name_search(keyword: str) -> list[dict]:
    """グルメサーチAPIで店名検索を行う

    Args:
        keyword (str): 検索キーワード

    Returns:
        list[dict]: 検索結果の辞書を保持した店舗リスト
    """

    # グルメサーチAPI用のクエリパラメータ設定
    params: dict = {
        "key": GOURMET_API_KEY,  # APIキー
        "keyword": keyword,  # 検索キーワード
        "type": "credit_card",  # 利用可能クレジットカード情報をレスポンスに含める
        "format": "json",  # レスポンスタイプをJsonにする
        "count": 100, # 取得件数を設定
    }

    # グルメサーチAPIより店舗情報を取得
    response: dict = json.loads(requests.get(GOURMET_API_URL, params).text)
    shoplist: list[dict] = response["results"]["shop"]

    return shoplist
```

##### リプライメッセージを送信する。
```python:python
def message_post(message, reply_token) -> None:
    """リプライメッセージを送信する。（メッセージ指定）

    Args:
        message (str) : 送信するメッセージ
        reply_token (str): リプライトークン
    """
    REQUEST_HEADERS = {
    "Authorization": "Bearer " + LINE_CHANNEL_ACCESS_TOKEN,
    "Content-Type": "application/json",
    }

    params = {
        "replyToken": reply_token,
        "messages": [{"type": "text", "altText": "カードが使えるお店を見つけよう。", "text": message}],
    }

    requests.post(REQUEST_URL, json=params, headers=REQUEST_HEADERS)
```

##### pushメッセージを送信する。
```python:python
def push_text(user_id, text) -> None:
    """ pushメッセージを送信する

    Args:
        user_id (str) : ユーザーID
        text (str): 送信するメッセージ
    """
    REQUEST_URL = "https://api.line.me/v2/bot/message/push"

    REQUEST_HEADERS = {
        "Content-Type": "application/json",
        "Authorization": f"Bearer {LINE_CHANNEL_ACCESS_TOKEN}",
    }

    params = {"to": user_id, "messages": [{"type": "text", "text": text}]}
    response = requests.post(REQUEST_URL, json=params, headers=REQUEST_HEADERS)
```
##### スタンプを送る
```python:python
def push_stamp(user_id, package_id, sticker_id) -> None:
    """ スタンプを送る

    Args:
        user_id (str) : ユーザーID
        text (str): 送信するメッセージ
    """
    REQUEST_URL = "https://api.line.me/v2/bot/message/push"

    REQUEST_HEADERS = {
        "Content-Type": "application/json",
        "Authorization": f"Bearer {LINE_CHANNEL_ACCESS_TOKEN}",
    }

    params = {
        "to": user_id,
        "messages": [
            {"type": "sticker", "packageId": package_id, "stickerId": sticker_id}
        ],
    }
    response = requests.post(REQUEST_URL, json=params, headers=REQUEST_HEADERS)
```

##### ポストバックイベントを受け取る
```python:python
def sample(event: dict) -> None:
    """ ポストバックイベントを受け取る

    Args:
        event (dict) : MessagingAPIからのリクエスト内容
    """
    ## イベントタイプを取得
    event_type = json.loads(event["body"])["events"][0]["type"]

    # ポストバックイベント
    if event_type == "postback":
        ## dataには「{postback_type}@{postback_data}」という形式で内容を設定してます。
        postback_body = event["events"][0]["postback"]["data"].split("@")

        postback_type = postback_body[0]
        postback_data = postback_body[1]

        # お気に入り登録/解除
        if postback_type == "modify_favorite":
            shop_id = postback_data
            adding_favorite(user_id, shop_id, reply_token)

            return 0

        # お気に入り表示
        if postback_type == "view_favorite":
            view_favorite(user_id, reply_token)
            return 0

        # 人気のお店表示
        if postback_type == "view_poplar":
            push_text(user_id, "過去7日間で人気のお店です")
            view_poplar(user_id, reply_token)
            return 0

```
#### DynamoDB
LINEの公式アカウントではユーザー単位にユーザーIDが割り当てられ、それがMessagingAPIを通じて連携されます。
これを利用してDB上にユーザーごとの情報を保存するテーブルを作成し、おすすめ機能等を実装しました。

また、人気のお店を表示するために検索ログの保存にも利用しています。
※直近７日分のみ保存するように[TLL](https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/howitworks-ttl.html "TLL")を設定しています。
（今回初めてこの機能を知りました。。unix_timeをTLL用の項目として指定しています。）

```mermaid
%%{init:{'theme':'neutral'}}%%
erDiagram
    user_info {
        string user_id
        list favorite_list
        list genre_count_log
        list recent_search
        int request_count
        int search_count
}
       search_logs {
        string dummy
        int date
        int no
        list shop_list
        int unix_time 
}
```

### 各機能のシーケンス図
※replyTokenの認証等は省略しています。

#### キーワード検索
```mermaid
%%{init:{'theme':'neutral'}}%%

sequenceDiagram
    autonumber

    actor USER
    participant Lambda
    participant グルメサーチAPI
    participant 店舗サーチAPI
    participant DynamoDB

    USER->>+Lambda:キーワード検索/キーワード<br>(リッチメニュー/message)
    
    Lambda->>+グルメサーチAPI:キーワード
    

    グルメサーチAPI-->>-Lambda:店舗情報リスト
    
    Lambda->>+店舗サーチAPI:キーワード
    

    店舗サーチAPI-->>-Lambda:店舗情報リスト
   

    Lambda-->Lambda:店舗情報リストをマージ
    Note over Lambda,店舗サーチAPI:店舗サーチAPIも併用することで、<br>より広く店舗情報を取得できるようにしています。
    Lambda->>DynamoDB: 検索ログ、直近検索ログ登録


    Lambda-->>-USER:店舗情報リスト
    

```


#### 周辺検索


``` mermaid
%%{init:{'theme':'neutral'}}%%

sequenceDiagram
    autonumber

    actor USER
    participant Lambda
    participant グルメサーチAPI
    participant DynamoDB




    USER->>Lambda:周辺検索/位置情報<br>(リッチメニュー/uri)
    activate Lambda
    
    Lambda->>グルメサーチAPI:位置情報（緯度、経度）
    activate グルメサーチAPI

    グルメサーチAPI-->>Lambda:店舗情報リスト
    deactivate グルメサーチAPI

    Lambda->>DynamoDB: 検索ログ、直近検索ログ登録


    Lambda-->>USER:店舗情報リスト


    deactivate Lambda
```



#### もっと見る機能
``` mermaid
%%{init:{'theme':'neutral'}}%%

sequenceDiagram
    autonumber

    actor USER
    participant Lambda
    participant DynamoDB


    USER->>Lambda:もっと見る<br>(カルーセルメッセージ/postback)
    activate Lambda

    Lambda->>DynamoDB: 直近の検索ログ,<br>リクエスト回数取得リクエスト
    activate DynamoDB

    DynamoDB-->>Lambda:直近の検索ログ,<br>リクエスト回数
    deactivate DynamoDB
    
    Lambda-->>Lambda:直近検索ログからリクエスト回数に応じて<br>次に表示する対象を抽出
    Lambda-->>USER:店舗情報リスト
    deactivate Lambda
``` 


#### ここに行く機能
``` mermaid
%%{init:{'theme':'neutral'}}%%

sequenceDiagram
    autonumber

    actor USER
    participant ブラウザ
    participant googlemap

    USER->>ブラウザ:ここに行く<br>(カルーセルメッセージ/uri)
    activate ブラウザ

    ブラウザ->>googlemap: maps urls
    activate googlemap

    googlemap-->>ブラウザ:地図情報
    deactivate googlemap
    
    ブラウザ-->>USER:地図情報

    deactivate ブラウザ
``` 



#### お気に入り登録/解除機能

``` mermaid
%%{init:{'theme':'neutral'}}%%

sequenceDiagram
    autonumber

    actor USER
    participant Lambda
    participant DynamoDB

    USER->>Lambda:お気に入り登録/解除<br>(リッチメニュー/postback)
    activate Lambda

    Lambda->>DynamoDB: お気に入りリスト取得リクエスト
    activate DynamoDB

    DynamoDB-->>Lambda:お気に入りリスト
    deactivate DynamoDB
    alt 店舗IDが登録されていない
    Lambda->>DynamoDB: お気に入り登録
    activate DynamoDB

    DynamoDB-->>Lambda:登録完了
    deactivate DynamoDB

    Lambda-->>USER:登録完了通知


    else 店舗IDが登録されている
    Lambda->>DynamoDB: お気に入り解除
    activate DynamoDB

    DynamoDB-->>Lambda:解除完了
    deactivate DynamoDB

    Lambda-->>USER:解除完了通知
    end
    deactivate Lambda
``` 

#### お気に入り表示機能
``` mermaid
%%{init:{'theme':'neutral'}}%%

sequenceDiagram
    autonumber

    actor USER
    participant Lambda
    participant DynamoDB
    participant グルメサーチAPI


    USER->>Lambda:お気に入り表示<br>(リッチメニュー/postback)
    activate Lambda

    Lambda->>DynamoDB: お気に入りリスト取得リクエスト
    activate DynamoDB

    DynamoDB-->>Lambda:お気に入りリスト
    deactivate DynamoDB
    Lambda->>グルメサーチAPI:店舗ID
    activate グルメサーチAPI

    グルメサーチAPI-->>Lambda:店舗情報リスト
    deactivate グルメサーチAPI


    Lambda-->>USER:お気に入りリスト
    deactivate Lambda

``` 

#### 人気のお店機能
``` mermaid
%%{init:{'theme':'neutral'}}%%

sequenceDiagram
    autonumber



    actor USER
    participant Lambda
    participant DynamoDB

    USER->>Lambda:人気のお店<br>(リッチメニュー/postback)
    activate Lambda

    Lambda->>DynamoDB: 過去7日分の検索ログ取得リクエスト
    activate DynamoDB

    DynamoDB-->>Lambda:検索ログ
    deactivate DynamoDB
    Lambda-->>Lambda:ランキング算出
    Lambda-->>USER:人気のお店リスト
    deactivate Lambda

``` 
## 利用したサービスの紹介
ハッカソンでは限られた時間の中で動くものをつくる必要があります。
今回は以下サービスを活用することで、できる限り高品質な機能とリッチな見た目を目指しました。

### LINE
言わずと知れたコミュニケーションアプリ

- LINE公式アカウント
LINE BOTを作成する際には公式アカウントという形で作成する必要があります。
以下のMessagingAPIを作成する過程で一緒に作成できました。

- Messaging API
LINE BOTで利用できる機能の一つで、LINE画面を通じてユーザーとさまざまなやり取りやサービスを提供できます。

- リッチメニュー
通常の吹き出し型のメッセージでのやりとりとは異なり、固定のメニュー等を作成できる機能です。

https://developers.line.biz/ja/


### AWS
バックエンドの処理はAWSを利用して作成しました。

- AWS Lambda
AWSにおけるFaaS(Function as a Service)で、サーバーなどを用意しなくても任意のコードを実行できます。

今回は一番使い慣れているPythonで実装を行いました。
Lambdaの処理をWebAPIとして公開するにあたっては「Function URLs」を利用しました。
※ハッカソンという限定的な期間での開発、利用のためAPIGatewayは利用せずこちらを使ってみました。

https://aws.amazon.com/jp/

### リクルートWebサービス
リクルート社が保有するデータベースを外部から利用するためのWeb API群です。
無料で利用できますが、APIキー取得のために利用登録が必要です。
また、利用するアプリ等でのクレジット表示も必要となります。

- グルメサーチAPI
ホットペッパーグルメにおける各種情報を取得できるAPIです。(一部情報のみ。今回で言うと電子マネーの対応情報なども取得したかったのですが提供されておらず。。残念。)

- 店舗サーチAPI
こちらも同様のAPIですが、詳細情報が載っていない店舗も検索することができます。
検索結果の補強に利用しています。

https://webservice.recruit.co.jp/index.html


### Google Map
言わずと知れた地図情報サービス。

- Maps URLs
URLへパラメータを指定することでGoogleMapの様々な操作を行うことができます。
今回は検索結果のお店への経路案内のために利用しています。
具体的には`https://www.google.com/maps/search/?api=1&query={店舗名}+{住所}`で指定しています。

https://developers.google.com/maps/documentation/urls/get-started

### アイコン作成
- Shopify Hatchful
ネットショップの開設・構築サービスの 「Shopify」が提供しているロゴ作成サービス。
サービス名と、ジャンルや雰囲気などを選択すると数十～数百のロゴを提案してくれます。
 
https://www.shopify.com/jp/tools/logo-maker

- Canva
オンラインのグラフィックデザインツールです。
テンプレートが豊富で、ロゴもあるのでサービス名を差し替えるだけでいい感じのデザインになります。（今回のロゴはこちらのサービスを利用して作りました。）

https://www.canva.com/

### プレゼン資料、リッチメニュー

- Canva
プレゼン資料もCanvaを利用しました。（参加者の方からとても高評価でした。）
テンプレートがあるので文字を差し替えたりするだけでいい感じのスライドが作れます。また、そのまま公開もできるし、同時編集もできます！
※個人的には配色のパレットを作ってくれるので、全体的に統一感のある色合いにできるのが便利だなと思いました。

https://www.canva.com/

### フィードバックフォーム

- Googleフォーム

５分もあれば集計機能付きのアンケートフォーム等が作れます。素晴らしい！

https://www.google.com/intl/ja_jp/forms/about/

### 構成図

- draw.io(VSCodeの拡張機能を利用)
VSCode内でも使えます。主要なクラウドサービスのアイコンやテンプレートもあり、一からデザインを考えなくて済むので便利です。

https://app.diagrams.net/

### シーケンス図

- Mermaid.js

テキスト形式でいろんな図を作れます。（そしてQiitaやGitHub上でそのまま書けます。）
※個人的にはフローチャートは複雑なものは厳しい印象ですが、シーケンス図はきれいに作れて、処理順に番号を振ってくれるのでいい感じになります。

https://mermaid-js.github.io/mermaid/#/

## あとがき
とにかく楽しかったし、エンジニアやっててよかったなと思いました。
※エンジニアならではの遊びというか、エンジニアやってないとできなかった（出会わなかった）と思うので。

開発作業以外にも、プレゼンに向けてロゴやスライド資料、画面の動画撮影・編集などを行う必要がありました。
自分はそういったセンスがないということがわかっていたので、なんとかならんもんかと調べると前述の通り、「hatchful」「Canva」といったサービスがあって、開発作業に集中することができました。おすすめです。

余談として、ハッカソン１日目の深夜（24時40分！）にバックエンドを作っていた個人のAWSアカウントが攻撃を受けてLambdaが使えなくってしまいました。。（AWS側で自動で保護してくれた。）
チームメンバーでアカウントを持っている人がいたのでそちらに移植して事なきを得ましたが、個人アカウントとはいえ２段階認証などセキュリティ周りはちゃんとしておいた方がいいなと痛感しました。

最後になりましたが、運営の皆様、チームメンバーの皆様、参加者の皆様ありがとうございました。また来年もあれば参加しようと思います。