初めての成果物の振り返り

# はじめに
**この記事は，3ヶ月ほどかけて作成したwebアプリケーションの紹介記事です．**  

公開したwebアプリケーションの概要や制作背景，使用技術などを紹介します．独学での学習を通して開発してきたものであり，誰かの参考や励みになれば幸いです．

# 作成者(投稿時点:2023/10/18)
地方私立大学院生命情報系修士1年
**プログラミングに関わる経歴**
- Ruby / Ruby on Railsを使用してwebアプリケーション開発(学部3年)
- 研究室でC, Pythonを使用(継続中)
- PHP / Laravelを使用してwebアプリケーション開発(修士1年：本記事)

今回紹介するwebアプリケーションを作成する前のスキル：
- 実務経験なし，Ruby / Ruby on Railsを使用して個人開発経験あり
- HTML/CSSは経験している．JavaScriptは経験なし．
- C言語/Pythonを用いて研究

# 目次
[1.アプリケーションの紹介](#アプリケーションの紹介)
[2.選定技術の採用理由](#選定技術の採用理由)
[3.振り返り](#振り返り)
[4.制作過程](#制作過程)
[5.反省点](#反省点)
[6.おわりに](#おわりに)

# アプリケーションの紹介
### CoffSpot / コーヒー検索サービス
![アプリケーションイメージ図.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3458920/03289f20-37d7-bb55-5bed-ea3e9e3997a6.png)

### サービスの概要
CoffSpotは，「好みのコーヒーに手軽に出会える場を提供したい」という想いから作成した，コーヒー検索サービスです．  

苦味・酸味・コク・甘味・香りの5項目の数値を設定するだけであなたの好みのコーヒーを簡単に検索することができます．

### サービスURL
https://coffee-search-app-7a8565b25739.herokuapp.com/login
レスポンシブ対応のため，PCでもスマートフォンでも利用可能です．

**ゲストログイン**
ゲーストユーザーでのログインも可能となっています．
※ゲストユーザーでのログインの場合は，アカウントの編集ができない仕様となっています．

### GitHubのリポジトリ
https://github.com/ok66ym/coffeeApp.git

### 制作背景
自身の好みのコーヒーを探すために以下のような課題があると考えました．

- 多くの種類があるコーヒーの中から自身が飲みたいコーヒーを探すには手間と時間がかかる点．
- コーヒーを選ぶ基準が多く，手軽にコーヒーを探すことが難しい点．
- コーヒーの評価基準がサイトや販売元によってバラバラである点．  

そこで，多岐に渡るコーヒーの中から評価基準を苦味・酸味・コク・甘味・香りの5項目に限定し，この5項目の値をユーザーが自由に設定することで求めている好みのコーヒーに手軽に出会える場を提供したいと考えました． また，ユーザー同士でのコーヒーの情報を共有できるように，自分自身や他のユーザーが見つけたコーヒーを投稿してその中から好みのコーヒーを探すことができるようにしたいと考えました．

### メイン機能の使い方
**ユーザーが出会ったコーヒーの情報を投稿：**
スライダーを動かして，出会ったコーヒーの苦味・酸味・コク・甘味・香りを評価できます．
![コーヒー投稿のGIF](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3458920/323270ed-1c61-80be-b854-1cea4f17cb13.gif)


**あらかじめ登録されているコーヒー情報とユーザーが投稿したコーヒー情報を検索：**
苦味・酸味・コク・甘味・香りの5項目の数値をスライダーを動かして，好みに合わせて設定するだけでコーヒーを検索できます．
![コーヒー検索のGIF![searchHistory_圧縮.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3458920/40474b66-b175-f095-08d6-890abea1cb2f.gif)
](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3458920/a54608a3-e6b6-625e-fe19-b9085a0fb411.gif)

**ユーザーの検索履歴から直接コーヒーを再検索：**
ユーザーの検索履歴を保存し，ワンクリックで再検索できます．
![コーヒー再検索のGIF](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3458920/59ac4092-2aed-5f8e-b0bc-cc7c07291504.gif)

### 実装機能
**機能一覧**
- コーヒー投稿機能(CRUD)・投稿一覧機能
- お気に入り機能・お気に入り一覧機能
- ページネーション機能
- 画像アップロード機能(Cloudinary)
- コーヒー検索機能(データベースに保存されたコーヒー情報，投稿されたコーヒー情報)
- 検索履歴保存機能
- 検索履歴際検索機能
- レスポンシブ設定

**認証機能(Laravel Breeze)**
- ユーザー登録 / ログイン / ログアウト
- ゲストログイン(アカウント情報編集，パスワード変更，アカウント削除については不可)
- パスワード変更
- アカウント情報編集(ユーザー名，メールアドレス(ID)，一言)
- アカウント削除

### 使用技術
**フロントエンド**
- HTML
- CSS
- Tailwind CSS 3.3.3
- JavaScript

**バックエンド**
- PHP 8.0.29
- Laravel 9.52.10

**環境**
- AWS(EC2+Cloud9)
- MySQL(MariaDB) 10.2.38
- Composer 2.5.8
- Git 2.40.1 / GitHub
- Cloudinary

**デプロイ**
- Heroku

### ER図
![ER図](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3458920/005340a6-a652-a9ad-a81d-3edeca7bd9d7.png)


# 選定技術の採用理由
ここでは，作成したアプリケーションで使用した主な技術について採用した理由を説明します．
**バックエンド： PHP / Laravel**
採用理由：
- 調べた限りでは初学者でも理解しやすく，求人数が比較的多い言語のため
- Ruby / Ruby on Railsを以前学習した経験から，新しい言語に挑戦してみたかったため

**フロントエンド： Tailwind CSS**
採用理由：
- 認証機能に使用したLaravel Breezeのビュー層ではTailwind CSSが採用されているため
- CSSファイルを作成する必要がなく，管理が容易になるため

**画像アップロード： Cloudinary**
採用理由：
- デプロイにはHerokuを採用しており，HerokuアドオンであるCloudinaryを採用した


# 振り返り
### 工夫点
- **ユーザーが体験したコーヒーを自身の評価で投稿する機能**
ユーザーが体験したコーヒーの情報やコーヒーの味などをユーザー自身で評価して投稿できるようにしました．  
これにより，検索するだけでなく，体験したコーヒーを記録することができるようにしました．

- **苦味・酸味・コク・甘味・香りの5項目で評価されたコーヒーを，ユーザーの好みの値に合わせて検索するアルゴリズムの実装．**
ここでは，ユーザーが苦味・酸味・コク・甘味・香りの5項目の数値を入力し，その入力に応じてコーヒーを検索できるようにしています．
具体的には，~~5項目それぞれに対して重み付けを行いました(現状，全ての項目を'1'に設定)~~．そして，**ユーザーが入力した数値が大きい項目ほど，ユーザーがコーヒーに求めている要素**であると考えました．そこで，ユーザーが入力した数値が大きいほど重要度が高いため広い範囲でデータを取り出し，ユーザーが入力した数値が小さいほど重要度が低いため狭い範囲でデータを取り出すように実装しました．各項目ごとに
入力した数値が3より大きい場合は
$$入力した数値±0.5$$
のデータを取り出す．
入力した数値が3以下の場合は
$$入力した数値±1.0$$
のデータを取り出す．
そして，ユーザーが入力した値とデータベースのコーヒーデータの差に対応する重みをかけた和を計算して，コーヒーの一致度の計算に使用しました．
```php:App\Http\Controllers
<?php
namespace App\Http\Controllers;

//省略

class SearchController extends Controller
{
    public function dbresult(Request $request, SearchStore $searchstore) {
        
        //各評価項目に入力された数値$bitter, $acidty…に代入する
        $bitter = $request->input('db.bitter');
        $acidity = $request->input('db.acidity');
        $rich = $request->input('db.rich');
        $sweet = $request->input('db.sweet');
        $smell = $request->input('db.smell');
        
        //検索アルゴリズム
        //重みを設定
         $weights = [
            'bitter' => 1.0,
            'acidity' => 1.0,
            'rich' => 1.0,
            'sweet' => 1.0,
            'smell' => 1.0,
        ];
        
        $coffees = Coffee::query();

        // 3より大きい：±0,5の範囲でデータ取り出す(狭くデータを取り出す)．3以下：±1.0の範囲でデータを取り出す(広くデータを取り出す)
        $inputs = ['bitter' => $bitter, 'acidity' => $acidity, 'rich' => $rich, 'sweet' => $sweet, 'smell' => $smell];
        foreach ($inputs as $item => $input) {
            if($input <= 3) {
                $coffees = $coffees->whereBetween($item, [$input - 1, $input + 1]);
            } else {
                $coffees = $coffees->whereBetween($item, [$input - 0.5, $input + 0.5]);
            }
        }
            
        //検索情報を保持
        $dbsearchInfo = $inputs;

        //取り出したデータの合計を重み付きで計算
        $dbsearches = $coffees
            ->select("*")
            ->selectRaw("((bitter - ?) * ? + (acidity - ?) * ? + (rich - ?) * ? + (sweet - ?) * ? + (smell - ?) * ?) as score", [
                $request->input('bitter'),
                $weights['bitter'],
                $request->input('acidity'),
                $weights['acidity'],
                $request->input('rich'),
                $weights['rich'],
                $request->input('sweet'),
                $weights['sweet'],
                $request->input('smell'),
                $weights['smell'],
            ])
            ->orderBy('score', 'asc')  // スコアが小さい順に並べ替え
            ->get();
            
        //　　検索情報の一致度を計算
        // 5段階評価で完全に一致している(100%)の場合：5．0　90％以上100未満：4.5　80％以上90未満：4.0　70％以上80未満：3.5
        // 50％以上70未満：3.0　40％以上50未満：2.5　30％以上40未満：2.0　20％以上30未満：1.5　10％以上20未満：1.0　10％より小さい：0.0
            
        $dbsearches = $dbsearches->map(function ($dbsearch) use ($dbsearchInfo) {
            $input_sum = $dbsearchInfo['bitter'] + $dbsearchInfo['acidity'] + $dbsearchInfo['rich'] + $dbsearchInfo['sweet'] + $dbsearchInfo['smell'];
            $db_sum = $dbsearch->bitter + $dbsearch->acidity + $dbsearch->rich + $dbsearch->sweet + $dbsearch->smell;
            $match_percentage = ($input_sum / $db_sum) * 100;
    
            $dbsearch->match_percentage = $match_percentage;
            
            if ($match_percentage == 100) {
                $dbsearch->rating = 5;
            } elseif ($match_percentage >= 90 && $match_percentage < 100) {
                $dbsearch->rating = 4.5;
            } elseif ($match_percentage >= 80 && $match_percentage < 90) {
                $dbsearch->rating = 4;
            } elseif ($match_percentage >= 70 && $match_percentage < 80) {
                $dbsearch->rating = 3.5;
            } elseif ($match_percentage >= 60 && $match_percentage < 70) {
                $dbsearch->rating = 3;
            } elseif ($match_percentage >= 50 && $match_percentage < 60) {
                $dbsearch->rating = 2.5;
            } elseif ($match_percentage >= 40 && $match_percentage < 50) {
                $dbsearch->rating = 2;
            } elseif ($match_percentage >= 30 && $match_percentage < 40) {
                $dbsearch->rating = 1.5;
            } elseif ($match_percentage >= 20 && $match_percentage < 30) {
                $dbsearch->rating = 1;
            } elseif ($match_percentage >= 10 && $match_percentage < 20) {
                $dbsearch->rating = 0.5;
            } elseif ($match_percentage < 10) {
                $dbsearch->rating = 0;
            } elseif ($match_percentage > 100 && $match_percentage <= 110) {
                $dbsearch->rating = 4.5;
            } elseif ($match_percentage > 110 && $match_percentage <= 120) {
                $dbsearch->rating = 4;
            } elseif ($match_percentage > 120 && $match_percentage <= 130) {
                $dbsearch->rating = 3.5;
            } elseif ($match_percentage > 130 && $match_percentage <= 140) {
                $dbsearch->rating = 3;
            } elseif ($match_percentage > 140 && $match_percentage <= 150) {
                $dbsearch->rating = 2.5;
            } elseif ($match_percentage > 150 && $match_percentage <= 160) {
                $dbsearch->rating = 2;
            } elseif ($match_percentage > 160 && $match_percentage <= 170) {
                $dbsearch->rating = 1.5;
            } elseif ($match_percentage > 170 && $match_percentage <= 180) {
                $dbsearch->rating = 1;
            } elseif ($match_percentage > 180 && $match_percentage <= 190) {
                $dbsearch->rating = 0.5;
            } else {
                $dbsearch->rating = 0;
            }
            
            return $dbsearch;
        });
            
        //検索値をセッションに保存
        //検索数値を検索後も保持
        $request->session()->put('search_values', $request->input('db'));
        // 検索履歴保存ためにseachstoresテーブルに情報を保存
        $inputs['user_id'] = Auth::id();
        $searchstore->fill($inputs)->save();
        
        return view('searches.dbresults')->with(['dbsearches' => $dbsearches, 'dbsearchInfo' => $dbsearchInfo]);
    }

    //省略

}
```

- **レスポンシブ設定によりスマートフォンでの利用を可能にした点**
ユーザーには，**手軽にコーヒーを検索することができるサービス**を提供したかったので日常生活で使うことが多いスマートフォンでの利用が可能になるようにしました．

- **ゲストログインの実装**
わざわざメールアドレスとパスワードなどを用意することなく，サービスを体験できるようにゲストユーザーでのログイン機能を実装しました．  

アカウント情報の変更はできない仕様となっていますが，ゲストユーザーでのログインによってサービスの一通りの機能を試すことができるようにしました．

### 苦労した点
- **ユーザーが設定した値から，好みのコーヒーを検索できるアルゴリズムの実装**
検索部分がこのアプリケーションにおける主となる機能であり，コーヒーの検索機能は時間をかけて取り組みました．
アルゴリズムの学習を行ったことが少なく，アルゴリズムの考え方や実装方法がわからず苦労しました．特に，単純な文字列の検索ではなく複数項目の数値の組み合わせによる検索に関して，わからない部分が多く実装の仕方がわかりませんでした．
最終的には，ユーザーによって重視する項目は異なるのではないかという考えと入力した数値が大きいほどユーザーが重要視している項目であるという考えから，入力された数値に基準を設けて，1つ1つの項目ごとにデータを取り出すアルゴリズムを採用しました．
その結果，入力した数値に応じて最低限ユーザーが求めている検索機能を実装できました．

- **UI/UXの調整(レスポンシブ設定・Tailwind CSS)**
ユーザーが手軽にコーヒーを検索することができるアプリケーションを目指したので，できる限りシンプルかつ使いやすいようにデザインを行いました．
しかし，デザインを考えることが苦手であることに加え，初めて触る自由度の高いTailwind CSSを利用には慣れるまで時間がかかりました．また，レスポンシブデザインに取り組んだことは初めてで，ヘッダー・フッターの位置や検索画面のレイアウト，ページネーションなどに独自性を持たせながら，使いやすく調整するのは大変でした．
しかし，根気強く・デザインの修正などを繰り返し，妥協せずにレスポンシブデザインに挑戦し，様々なデバイスでの利用を可能にしました．

### 今後の課題
- **非同期通信でのお気に入り機能**
現在のお気に入り機能は，非同期通信に対応しておらず，お気に入り登録ボタンを押すとページの読み込み時間を要する同期通信という仕様になっています．そこで，Ajaxを使用して非同期通信を行うことで，よりスムーズな通信を実現したいです．

- **Google Map API**
今回の開発では，**開発期間と就職活動までの期間を考慮にいれながら実現したいことに対して必要最低限な機能**を中心に開発を行ってきました．その中で，APIを利用した実装は行っていません．その中で，ユーザー自身が体験したコーヒーの情報を投稿する機能では，購入店舗のURLを投稿することができますが，webサイトを持っていない店舗には対応できていません．そこで，Google Map APIを利用することでユーザーが投稿するコーヒーはどこで購入したかを公開することができるので，より幅広くコーヒーの情報を紹介あるいは記録することができるようになると考えています．

- **検索アルゴリズムの修正**
現状の検索アルゴリズムでは，入力した数値の組み合わせによって大量の検索結果が表示されてしまうという課題があります．そこで，検索アルゴリズムの修正を行うことでより検索結果を限定的にし，ユーザーが求めているコーヒーをピンポイントで提供できるようにしたいと考えています．

# 制作過程
簡単にですが，制作過程とその内容を紹介します．参考にしてください．

HTML / CSSの学習(1週間)
↓
PHP, Laravelの学習(3週間)
↓
アイデア出し，機能の洗い出し，テーブル設計，画面設計など(1週間)
↓
バックエンドの開発(1ヶ月半程度)
↓
Tailwind CSSの学習と並行してフロントエンドの開発(2週間)
↓
細かい調整(1週間程度)
↓
現在

### 学習・開発時に工夫・意識したこと
- **ユーザー視点での開発**
ユーザー視点での開発は，特に意識して取り組みました．どのようなユーザーを対象にどのような機能が必要なのか，どのような課題があるのかなど開発を開始する前，そして開発中も常に意識していました．
実際に使用するのはユーザーであり，ユーザーのニーズを満たすことはそのサービスの継続や意味につながるものであると思います．今回の開発を通して，ユーザー視点での開発を意識することができたと思います．


- **目標を設定し，スケジュールを立てて開発**
Notionを使用して，１週間単位で作業内容を決めて，それに合わせて学習や開発を進めていきました．また，1日1日の進捗や振り返りを行いながら次の日の作業内容の詳細を決め，１週間のスケジュールの調整を行なっていきました．
スケジュールを立てる上で意識したのは，必ずしもスケジュール通り進むわけではないので，気落ちせずに柔軟に計画を修正しながら学習や開発に取り組んでいきました．

- **設計に時間を割く**
Ruby / Ruby on Railsを使用した初めてのwebアプリケーションの開発で，設計や制作背景などをしっかりと考えずに開発を進めてしまったことがありました．その時，必要な機能の洗い出しが不十分であり，テーブル設計を再度行うといった非常に非効率な開発を行なってしまったという反省点がありました．
そこで，今回の開発では開発前の機能の洗い出しやテーブル設計，画面設計に時間を割くことで効率的に開発を行うことができました．

# 反省点
PHPやLaravelなど基本的な仕様や書き方を学習したのちに開発を通してより深い知識を身につけるようにしました．しかし，深い理解より開発を優先してしまい，しっかりとした知識を身につけることができたとは言い難い結果になってしまいました．なので，開発とともに言語や技術への本質的な理解も並行して意識しながら取り組むべきであったと反省しています．
また，開発を通して得た知識をX(旧：Twitter)やQiitaなどにアウトプットすればよかったと思っています．頭の中で理解していることを文章や図に落とし込むことで理解できていない部分を明らかにできるのではないかと思うので，アウトプットを並行して行うべきであったと考えています．

# おわりに
拙い文章にも関わらず，最後まで読んでいただきありがとうございました．
今回の開発を通して，web開発の難しさや自身の技術力の無さを痛感しました．しかし，それを上回る開発の楽しさ・やりがいを感じることができました．また，確かな技術を持ってユーザーに本質的な価値を提供できるエンジニアになりたいという気持ちが大きくなりました．
もし何か，間違っている点等がございましたら，教えていただけると幸いです．