【TypeScript × PHP】ReactとLaravelで経路共有アプリを作った話

# 🚙 はじめに
ReactとPHPを用いてwebアプリを開発したので制作記録として解説を掲載したいと思います．また筆者はフルスタック開発初心者であるため，至らぬ点があるかと思われますがご了承ください．

# 🚙 自己紹介
初めまして．趣味でweb開発を勉強している273*(ﾂﾅｻﾝﾄﾞ) / Kei.と申します．関西の大学生です．最近はフルスタック開発を行なっています．初心者です．

# 🚙 完成品
以下のリンクからwebアプリにアクセスできます．
開発期間は2ヶ月です．

https://github.com/273Do/Route8

~~https://route8-c22ee15e5fa9.herokuapp.com~~
公開を終了しました．

# 🚙 制作に至った経緯
私は趣味で友人とサイクリングやドライブによく行くのですが，経路を選定する際に他の人の経路を参考にしたいと感じたことが多々ありました．「経路を共有できるサービス」は既に存在しますが，私調べではただ経路を共有するだけのものばかりでした．そこで，独自のサービスを開発したいと思いました．走行時の感想や天候，移動手段なども併せて投稿できるようにすることで，より詳しく経路を選定することができるようになるのではないかと考えました．また，チャット機能を導入することにより，感想を伝えたり細かな情報を伝えてもらえたりすることができると考えました．

# 🚙 サービス名について
本サービスは「経路は無限大(∞)」をモットーにしています．∞→8というように形が似ているので「Route8」になりました．

# 🚙 仕様
### 1. ログイン，新規登録
メールアドレスとパスワードを用いてログインします．新規登録をする場合は名前，メールアドレス，パスワードを用いて行います．

![login_screen.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3530539/620a24f7-dd42-2f1e-50f8-68c7fae710d4.png)
![register_screen.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3530539/a2f3c17f-e38c-b9ee-2ba6-53ba8e373a26.png)

### 2. 経路表示・各アイコン

#### 経路表示

ログインした際に現れる画面がメイン画面となります．
投稿されている経路がカード形式で表示されます．
下書き保存した投稿は自身の投稿アイコンからアクセスできます．

![main_screen.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3530539/c97ba50b-9870-0f61-5d11-3f94e54e7392.png)

各投稿をクリックすると，より詳細な経路の情報を閲覧できます．

![route_screen.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3530539/e46909c2-abef-5a5c-a1d8-53eaf3995504.png)

経路詳細画面の投稿者名をクリックするとそのユーザーが投稿した経路を表示できます．
![スクリーンショット 2023-11-29 17.28.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3530539/f6853e51-875f-87df-5640-e62cf51b9199.png)

ページネーションも実装しています．1ページ当たり10個の投稿まで表示されます．
下のページ番号から移動することができます．

![スクリーンショット 2023-12-08 18.01.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3530539/e62e1496-f068-c644-d181-e1ab03731287.png)

<hr>

#### 各アイコン

<table>
  <caption>
  経路状態アイコン<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3530539/301e68b1-47f8-4eff-83f0-168a68d7d3e4.png" style="border-radius:10px;"></caption>
  <tr>
    <td>投稿ジャンル(未走行の場合は表示されません)</td>
    <td>走行時の天候(前，後)</td>
    <td>走行手段</td>
  </tr>
</table>

<table>
  <caption>
  メイン画面<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3530539/02299839-7e0e-bda2-5d84-891058b3c1b4.png" style="border-radius:10px;"></caption>
  <tr>
    <td>経路作成</td>
    <td>自身の投稿表示(下書きを含む)</td>
    <td>リコメンド機能</td>
    <td>ブックマークリスト</td>
  </tr>
</table>

<table>
  <caption>
  経路詳細画面(自身の投稿)<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3530539/ca68c40a-d43a-4eac-3ca3-0d7b7aca41cc.png" style="border-radius:10px;">
</caption>
  <tr>
    <td>メイン画面表示</td>
    <td>自身の投稿表示(下書きを含む)</td>
    <td>リコメンド機能</td>
    <td>投稿編集機能</td>
    <td>投稿削除ボタン</td>
    <td>投稿ブックマーク機能</td>
  </tr>
</table>

メイン画面(カード)
<table>
  <caption>
  <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3530539/b502e0ff-24b9-42d9-68cb-2978eff30b7c.png" style="border-radius:10px;">
  </caption>
  
  <tr>
    <td>投稿編集機能</td>
    <td>投稿削除ボタン</td>
    <td>リコメンド機能</td>
  </tr>
</table>

:::note warn
他ユーザーの投稿の場合は投稿編集機能と投稿削除ボタンは表示されません．
:::

### 3. 経路作成・編集・削除
GoogleMapで作成した経路を埋め込み，内容や走行時の状況などと合わせて経路を投稿することすることができます．その際経路，施設，景色のカテゴリーを選択することができます．自身の投稿はタイトルバーのアイコンから投稿の編集，削除を行うことができます．
[Google Map]をクリックすると Google Map に遷移するので，経路を表示させた後 html 埋め込みタグをコピペしてください．(マップ上で経路を表示させていなくても投稿はできます．)

![create_screen.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3530539/709ef121-bebb-8ed4-2b4f-109d1cd0c374.png)

削除アイコンで投稿の削除ができます．

![delete.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3530539/d1dc1c6f-92bf-6570-51e7-35d44b61e06f.gif)

### 4. 投稿された経路付近の経路の表示(リコメンド機能)
メイン画面の旗アイコンをクリックすると，自身が最後に投稿した経路に近い投稿を表示します．GoogleMapの埋め込みURLから解析，抽出を行い表示します．右上のバーから1km単位で2~20kmの範囲選択ができます．仕組みは後述のとおりです．

![recommend1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3530539/d6d431ad-f0a5-56ab-61fd-77f59b596df5.png)
![recommend2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3530539/477caf60-9700-5dce-4fae-402c33f91548.png)
![recommend3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3530539/6c03c1cf-42d6-7917-5e19-4a32dc5167ab.png)
![recommend4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3530539/3e03fe17-e1be-14a0-079c-75592a540664.png)

~~これは各経路表示画面でその経路に対してリコメンドできるようにした方が良かったかも．後々実装しようかな．~~
各経路詳細画面にて旗アイコンをクリックすると，その経路に対してリコメンド機能が動作します．

### 5. チャット機能
投稿ごとにチャット機能を設けています．ユーザーとのコミュニケーションやより詳細な情報を聞き出したりと様々なことに利用可能です．投稿者がメッセージを送信した場合，線で囲われます．

![chat.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3530539/508a20ef-6c5c-2150-dabd-f688d1c50d09.png)

### 6. 検索機能
検索方法は二つあり，投稿の各アイコンをクリックするフィルター検索と，ヘッダー部分の検索バーでワード検索を実装しています．後者は，虫眼鏡アイコンをクリックすることで，検索項目を切り替えることが可能です．

#### フィルター検索

![search5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3530539/a8bffa9f-018d-fae5-33d9-fb829312ab5d.png)
![search6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3530539/f444d5c5-ef72-7e97-6563-a1b9a4a62f17.png)
![search7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3530539/f76449bb-a40c-1fa3-5278-7dd37e627951.png)
未走行アイコンは検索できません．

<hr>

#### ワード検索

<P>Title：投稿のタイトルで検索</P>

![search1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3530539/d66da773-07e2-1877-eae7-3450a955d61c.png)
<P>Start：投稿のスタート地点で検索</P>

![search2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3530539/0855cc2f-b421-76b8-80bb-54d6a01f8ffb.png)
<P>Goal：投稿のゴール地点で検索</P>

![search3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3530539/e9446ad5-8fb9-60bd-3f07-1a4daaa285a9.png)
<P>Body：投稿の内容で検索</P>

![search4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3530539/f45c5fdf-8f8a-45c3-911b-b43c234640af.png)


### 7. ブックマーク機能
メイン画面，経路詳細画面のヘッダー部分にブックマークアイコンがあります．クリックすることでブックマークできます．塗りつぶされたようなアイコンに変更されます．ブックマークした経路はメイン画面からソートすることができます．

![bookmark1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3530539/8e1c5f89-7553-3ac8-39ef-4984400560a9.png)
![bookmark2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3530539/02a99c91-42c0-f2a2-1954-dfef1118f82b.png)
![bookmark3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3530539/6478585e-ae14-52ac-19f4-60e416a183f4.png)
![bookmark4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3530539/995ef678-e1a7-e932-becf-c959f86f1d21.png)

### 8. テーマ・エフェクト切り替え機能

フッターのアイコンからライトモードとダークモード，エフェクトを切り替えることができます．
標準ではライトテーマ，エフェクト ON です．
テーマ・エフェクトは保存されるのでどのデバイスでログインしても適応されます．
お好みでどうぞ．

![effect0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3530539/da8058c3-077e-f0e6-91cf-a10cefd38115.png)

#### テーマ

ライトテーマ
![theme1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3530539/8946a450-01f7-33d3-18d5-c630271e9b75.png)
ダークテーマ
![theme2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3530539/3cc11b2e-5a36-b00f-55e9-dfa4f81c4328.png)

<hr>

#### エフェクト

エフェクト ON
![effect1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3530539/61417332-4fbf-5049-4b93-cfe16a25c53f.png)
エフェクト OFF
![effect2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3530539/4da967d9-c8bc-5f00-29d3-38483a9b5f41.png)


# 🚙 技術スタック

### インフラ
今回はAWSのEC2インスタンスの中で開発を行いました．無料枠の最小スペックです．VSCode上でSSL接続を行い開発を行なっていました．仮想メモリを4GBに設定してようやく快適に開発を行える環境になりました．とりあえずインスタンス構築の方法も知っておきたかっただけので次からは普通にDocker使うかな．他の方のQiita記事を参考にさせていただきました．ありがとうございます🙇‍♂️．補完や整形が使えるのはやっぱりデカいです！

https://qiita.com/teracy164/items/c1dfc09901c7f242a13f

### フロントエンド
コンポーネント単位で管理したかったのでTypeScriptのReact，スタイルはSassを使用しました．TypeScriptの型定義に少々苦労しました．Sassに関してはCSSを書くよりも効率よく綺麗に書くことができました．こちらの導入も他の方のを参考にさせていただきました🙇‍♂️．今回はもう少しCSSについて学びを深めたかったのでBootstrapやTailwind CSSは使用しませんでした．今後勉強して行きたいですね．

https://qiita.com/shomffl/items/644656c4de086d547429

https://qiita.com/pikimaru/items/062533c3cba6dbfe8f42

### バックエンド
PHPのフレームワークであるLaravelを利用しました．RESTやMVCモデルなどの仕組みに最初は戸惑いましたが，慣れてくると非常に開発が快適かつシンプルに行うことができました．ReactとLaravelの連結をするのにinertia.jsを使用しました．認証機能はBreezeを使用しました．バックエンドの基礎を十分に学習できたと思います．

### データベース
データベースにはMariaDBを使用しました．LaravelにはORMが提供されているのでSQL文を書かずに済んで便利ですね(かと言ってSQLを知らないのもよくないね)．

### デプロイ
Herokuを使用しました．GitHubの学生プランに加入しているのでEco Dynoを実質的に無料で使用できるのがありがたいです．アクセスがないと自動で落ちるので接続する時にちょっと時間がかかるのがネック．

# 🚙 デザイン

### ロゴ
ロゴに関しては3DCG制作ツールのBlenderを使用しました．手前にすりガラス風のマテリアルのオブジェクトを，背面に放射マテリアルのオブジェクトを配置することでリッチなデザインに仕上げています．他制作物のロゴも全てこのツールを用いています．統一感が出てて良きです．色は紺色をグラデーションにした感じです．映像制作やってた経験が活かせました☺️．

![スクリーンショット 2023-11-28 23.16.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3530539/c6ad12ea-c8fa-f944-daec-9509f5171636.png)

<hr>

### アプリデザイン / UI / アニメーション
使っていて楽しい，気持ち良いという体験をしてもらいたいと考え，UI設計をしました．丸みを帯びたデザイン，イージング，落ち着いた見た目など．．．アプリデザインはdraw.ioとFigmaを使用しました．はじめにdraw.ioでワイヤーフレームを制作し，それに沿ってFigmaで肉付けを行いました．高度の機能をシンプルなUIに落とし込むのに苦労しました．私のポートフォリオのような角丸でモダンなデザインを採用し，落ち着いた二色の色で統一しています．また，ライトテーマ，ダークテーマにも対応しており，どのデバイスでアクセスしても設定したテーマが適応されます．その他ホバー時の動作やアニメーションアイコンにもこだわっています．UIアニメーションはScrollReveal，ポップアップはSweetAlert2を用いています．Typescript用にコンポーネントを別途作成し適応しています．

![dermo2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3530539/7e5cdd7d-8228-f773-5321-ac8b63ae2c9f.gif)

https://drive.google.com/file/d/1KpsebUX7hLQYesWiSbc1nrqbm-Bdly7a/view?usp=sharing

https://www.figma.com/file/suusJOO1ly4jw8B45bVK3w/Route8?type=design&node-id=0%3A1&mode=design&t=078GnQ3h8X5qGIvc-1

https://lordicon.com

https://scrollrevealjs.org

https://sweetalert2.github.io

# 🚙 リコメンド機能について
本機能についてはライブラリ等を使用せずに一からバックエンドで実装しました．フローは以下の通りです．ChatGPT君にも手伝っていただきました．

1. ログインしているユーザーの最新の投稿のGoogleMapのURLを取得し基準とする．
2. ログインしているユーザー以外の投稿のGoogleMapのURLを取得し，配列に格納．比較対象とする．
3. 上で取得したURLから緯度経度を抽出しつつ，Haversine formula(ハバーサイン法)によって2点間の距離を導出する．
4. 3の距離がユーザーが設定した距離範囲内であれば，URLと距離を二次元配列に格納．
5. 3, 4を2で格納した分だけ繰り返す．繰り返した結果配列が空であればnullを返す．
6. 5で得た二次元配列を距離が近い順に並び替える．
7. 6で得た結果をもとにURLのみ返す．
8. 7の結果を使用し投稿をソートしてフロントへ返す．

https://en.wikipedia.org/wiki/Haversine_formula

以下は実装した式です．

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3530539/996c9bc8-d669-06b3-24bf-6c5014f186da.png)

```recommend.php
<?php
namespace App\Library;
use Illuminate\Support\Facades\Facade;
class Recommend extends Facade {
    protected static function getFacadeAccessor() {
        return 'recommend';
    }
    // 引数はベースとなるMapURL，他の全てのURLが格納された配列，閾値(範囲選択)
    public function recommend($baseMapUrl, $otherMapUrls, $thresholdDistanceKm) {
        // URLを解析して緯度と経度を格納
        list($baseLat, $baseLon) = $this->extractLocationFromUrl($baseMapUrl);
        $distances = [];
        foreach ($otherMapUrls as $mapUrl) {
            list($targetLat, $targetLon) = $this->extractLocationFromUrl($mapUrl);
            $distance = $this->calculateDistance($baseLat, $baseLon, $targetLat, $targetLon);
            // 指定の範囲内であれば配列にデータを格納
            if ($distance <= $thresholdDistanceKm * 1) {
                $distances[] = ['url' => $mapUrl, 'distance' => $distance];
            }
        }
        //該当するmapがなければ空を返す
        if (empty($distances)) {
            return null;
        }
        // 距離が近い順に並び替える
        $distancesArray = array_column($distances, 'distance');
        array_multisort($distancesArray, SORT_ASC, $distances);
        foreach ($distances as $distance) {
            $closestMaps[] = $distance['url'];
        }
        return $closestMaps;
    }
    // 2つの座標の距離を計算する関数(Haversine formula)
    protected function calculateDistance($lat1, $lon1, $lat2, $lon2) {
        $R = 6378.137; // 地球の半径(キロメートル)
        $dLat = deg2rad($lat2 - $lat1);
        $dLon = deg2rad($lon2 - $lon1);
        $a = sin($dLat / 2) * sin($dLat / 2) + cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * sin($dLon / 2) * sin($dLon / 2);
        $c = 2 * atan2(sqrt($a), sqrt(1 - $a));
        $distance = $R * $c; // キロメートル単位
        return $distance;
    }
    // URLから緯度・経度を抽出する関数
    protected function extractLocationFromUrl($mapUrl) {
        preg_match('/!2d([\d.]+)!3d([\d.]+)/', $mapUrl, $matches);
        if (count($matches) >= 3) {
            $latitude = floatval($matches[1]);
            $longitude = floatval($matches[2]);
            return [$latitude, $longitude];
        } else {
            return [0.0, 0.0];
        }
    }
}

```

# 🚙 心残り
本サービスはGoogleMapの埋め込みを利用します．本当は独自の経路作成ツールを製作したかったのですが，時間と技術がなく実装することができませんでした．を今後技術が身に付いたら作り直したいと考えています．また，独自ドメインの設定，スマホ向けレスポンシブの調整がうまくできなかったのが心残りですね．

# 🚙 最後に
本サービス開発の経験を経て，簡単ではありますがwebアプリ制作に必要な技術について知識を身につけることができたと感じています．もう少しバックエンドの勉強も行いたいです😉．現在は研究に必要になりそうなツールの制作を計画しているところです．完成しましたらまたQiitaにて記事を投稿したいと思います(笑)．　今後も日常生活が便利になるようなツールをどんどん開発していきたいです．最後まで読んでいただきありがとうございました．それでは！