【Rails】診断機能を実装（徹底解説）

今回は診断機能を実装していこうと思います
[こちらの記事](https://x.gd/bFoP4)を応用したものになりますので、必要があればご参照ください。



# 目次
① 診断機能の実装イメージ
② 実装の下準備（診断内容の考察）
③ モデル・テーブルの作成
④ 診断するページを作成する
⑤ コントローラーを記述する
⑥ 診断結果を表示するページを作成する
おまけ① 診断結果を保存したい場合

※すぐに実装を始めたたいという人は、①・②をとばし、「③ モデル・テーブルの作成」から始めてください

###### 実装環境
ruby 3.0.4p191 (2021-07-07 revision a21a3b7d23) [x64-mingw32]
Rails 6.1.5

## ① 診断機能の実装イメージ

診断機能とはどういうものか、イメージを明確にしておきます

① 以下のサイトで実際に診断機能を動かしてみましょう




② 診断機能のプログラムの構造を図解します

まずは以下の写真を見てください
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2429739/ebc33720-0f74-1972-5ae7-f95187fbda6f.png)

診断するページでは、各質問に対する各解答が、数値としてコントローラーに送られるようになっています

例えばあなたが「20代」「男」「めがねをかけている」と回答した場合は*212*が、「40代」「女」「めがねをかけていない」と回答した場合は*421*がコントローラーへ送信されます

コントローラーに送られるこの数値と条件式if文を用いることにより、診断した内容と診断した結果を結びつける恩です。


今回はシンプルに単語をつなげただけの診断結果が表示されていますが、これは自分の好みでどのような結果でも表示することができるのでご安心ください。



## ② 実装の下準備（診断内容の考察）

早速診断機能を作っていきましょう。。。

とその前に、診断機能実装にあたり、一番の山場が早々にやってきました💦

その山場とは、診断機能の下準備、つまりどういう設問をつくるか、また設問の回答に対して、どのような結果を用意するのかというのを考えるフェーズです。

例えば自分にあった洋服を診断してくれる診断機能を実装したいと思う場合の設計の一例を紹介します


① ペルソナを意識しつつ、質問と回答を考える

>質問１：性別
>回答１：男・女
>
>質問２：年代
>回答２：10代・20代・30代...etc
>
>質問３：シチュエーション
>回答３：学校・さんぽ・買い物・デート・遊びに行く...etc


② 回答に対する結果を考える

>女性・20代・デートと答えた人にはこんな服を紹介しよう
>男性・10代・学校と答えた人にはこんな服を紹介しよう...etc

今回は回答の数が
性別×２、年代×３、シチュエーション×５
なので、回答の通り数（＝つまり用意すべき結果の数）は
２×３×５で30通り
となります

大変ですね(笑)

設問と回答が増えるにつれて、結果の数は掛け算で増えていくので、ここが診断機能実装の際の一番の正念場だと思います💦
がんばってください！！


③ おまけ：はいの個数に応じて診断結果を表示したいという場合

例えば各設問に対し「はい」「いいえ」に２択でこたえていき、最終的に「はい」と答えた回数に応じて診断結果を表示したいという場合もあるでしょう

その場合も構わず、まずは最初に設問を、そして回答された「はい」の個数に応じてどのように結果を表示するのかを考えておいてください！


## ③ モデル・テーブルの作成

それでは早速プログラムの記述に移っていきます
まずは診断結果を格納するテーブルを作成します

[こちらの記事](https://x.gd/bFoP4)では、viweファイル内に直接if文と結果を書いていましたが、それだと結果の数が増えた時にとてもつらくなるため、今回は結果をテーブルのレコードとして保存する形式をとります

今回はテーブルにnumberカラムとcontentカラムを用意します
numberカラムには診断結果の番号（「121」や「422」等）を、
contentカラムには、表示する診断結果の内容を
それぞれ格納します

もし診断結果に画像を用いたい場合は、imageカラムを追加するなど、各自の実装したいものに合わせて変えてもらって構いません

```ruby:ターミナル
  rails g model Post number:string content:string
```

```ruby:ターミナル
  rails db:migrate
```

テーブルの作成が終わったら、テーブルに診断結果を格納していきます
②の下準備で用意したものを参考に、seedファイルを用いてnumberには番号を、contentには結果を格納していきましょう

seedファイルの細かい使い方や実装は割愛します
分からない方は各自調べてseedファイルを用い、テーブルに診断結果を格納してください


## ④ 診断するページを作成する

診断するページを作っていきます
[こちらの記事](https://x.gd/bFoP4)ではチェックボックスを用い一つのカラムにすべての設問、すべての回答の結果を入れこんでいますが、これだと不具合が生じるため別の方法を用います

具体的に怒る不具合とは、一つの設問に対して２つ以上回答できてしまうことです
つまり最初の例でいうと、「Q.1 あなたは何歳ですか」に対して、「10代」「20代」「30代」「40代」の全ての選択肢を同時に回答できてしまいます

これだと不具合が生じてしまうため、以下の方法をとります

```ruby:views/posts/new.html.erb
  <%= form_tag(posts_path, method: :get) do %>
    <h3>Q.1 あなたは何歳ですか？</h3>
        <label>
            <%= radio_button_tag :age, "1" %> 10代
        </label>
        <label>
            <%= radio_button_tag :age, "2" %> 20代
        </label>
        <label>
            <%= radio_button_tag :age, "3" %> 30代
        </label>
        <label>
            <%= radio_button_tag :age, "4" %> 40代
        </label>

    <h3>Q.2 あなたの性別は何ですか？</h3>
        <label>
            <%= radio_button_tag :sex, "1" %> 男
        </label>
        <label>
            <%= radio_button_tag :sex, "2" %> 女
        </label>

    <h3>Q.3 あなたはメガネをかけていますか？</h3>
        <label>
            <%= radio_button_tag :glasses, "1" %> いいえ
        </label>
        <label>
            <%= radio_button_tag :glasses, "2" %> はい
        </label>

    <br>
    <%= submit_tag '診断する'  %>
 <% end %>
```

見てもらえるとわかるかと思いますが、
①一つひとつの設問に対して、「age」「sex」「glasses」といった回答結果（バリュー）を格納する変数（キー）を用意
②checkboxではなくradio_buttonを使用

radio_buttonを用いることで、checkboxの時のように一つの設問に対して複数の回答が返ってくるという不具合を防いでいます

でふぁ早速、②の下準備で用意した設問と回答、それに結びつく番号をviewファイルに記述してください

注意すべき点としては、

```ruby:views/posts/new.html.erb
  <%= form_tag(posts_path, method: :get) do %>
  #省略
  <% end %>
```

のform_tagの後に書いてあるパス（今回は「posts_path」）ですが、これは診断結果を表示したいページのパスにしておくということくらいです



:::note info
「はい」の回答数に応じて診断したい場合

以下のように記述に変更を加える必要があります

記述の変更点としては、
①radio_buttonで渡す値が「"1"」から「1」となっている（つまり文字列ではなく数値として値を渡している）ことと、
②渡す数値が「１」「２」ではなく「０」「１」となっていること
※はいに「1」を対応させることが重要
です
:::
```ruby:views/posts/new.html.erb
  <%= form_tag(posts_path, method: :get) do %>
   #省略
    <h3>Q.3 あなたはメガネをかけていますか？</h3>
        <label>
            <%= radio_button_tag :glasses, 0 %> いいえ
        </label>
        <label>
            <%= radio_button_tag :glasses, 1 %> はい
        </label>

    <br>
    <%= submit_tag '診断する'  %>
 <% end %>
```


以上で診断ページの記述は終わります


## ⑤ コントローラーを記述する

コントローラーを記述していきます
まず記述の完成形は以下です


```ruby:controllers/posts_controller.rb
  def index
        age = params[:age]
        sex = params[:sex]
        glasses = params[:glasses]

        @result = age + sex + glasses
        @posts = Post.all
    end
```

診断ページでの回答は、それぞれ「age」「sex」「glasses」に直接送られてくるため、paramsを用いて送られてきた値をここに受け取ります

次に個々に受け取った値を、```@result = age + sex + glasses```とすることで、@resultにまとめます
（例えば「age=1」「sex=2」「glasses=1」と送られてきたら、@resultは「121」となります）

※足し算を用いると数値が足しあわされて、上のたとえだと@resultが４になるのではないかと心配する人がいるかもしれませんが、今回は「1」や「2」といった数値を数値データ（integer）としてではなく文字列データ（string）として扱っているため、足し算をすると文字列が接続されるという感じになります

:::note info
「はい」の回答数に応じて診断したい場合

記述に変更を加える必要はありません
viweファイルから文字列（string）ではなく数値（integer）で値を受け取っているため、足し算をすることでそのまま数値が足し合わされ、「はい」と答えた回数が@resultに代入されます
:::


最後に、```@posts = Post.all```ですが、これは言うまでもないでしょう
先ほどpostsテーブルに格納した診断結果をすべて呼び出しています

これでコントローラーの記述は終わりです


## ⑥ 診断結果を表示するページを作成する

最後に診断結果を表示するページを作成していきます
記述の完成形は以下です

```ruby:viwes/post/index.html.erb
  <% @posts.each do |t| %>
    <% if t.number == @result %>
        <div class="tweet">
            あなたは<%= t.content %>です。
        </div>
    <% end %>
<% end %>
```

もはや説明は不要でしょう
eachメソッドとif文を用い、postテーブルのnumberカラムに入っている文字列（「421」など）が@resultに入っている文字列と一致したときにのみ、postテーブルのcontentを表示しています

:::note info
「はい」の回答数に応じて診断したい場合

```<% if t.number >= @result %>```などとif文の条件部分をいじってあげてください
:::


これで診断機能の実装が終わりました

ここから後はおまけです！！


## おまけ① 診断結果を保存したい

一度診断した結果を保存しておきたい場合もあると思います
そんな人向けに診断結果を保存する方法を軽く説明していきます

ログイン機能が実装されていることを前提として話を進めていきます

#### おまけの目次
① 結果を保存するためのテーブルを作る
② view・ルーティング・コントローラーを記述する
③ 保存した診断結果を表示する

#### ① 結果を保存するためのテーブルを作る

```ruby:ターミナル
  rails g model Result number:string user_id:integer
```

```ruby:ターミナル
  rails db:migrate
```

モデルにアソシエーションの記述を行います


```ruby:model/user.rb
  has_many :results, dependent: :destroy  #この一行を追記
```

```ruby:model/result.rb
  belongs_to :user  #この一行を追記
```

これで、ユーザーと診断結果を紐づけて保存できます



#### ② view・ルーティング・コントローラーを記述する

```ruby:viwes/post/index.html.erb
  <% @posts.each do |t| %>
    <% if t.number == @result %>
        <div class="tweet">
            あなたは<%= t.content %>です。
            <br>
            # ここから↓
            <% if user_signed_in? %>
              <%= link_to 'この診断結果を保存する', posts_path(num: @result) , method: :delete %>
            <% end %>
            # ここまで↑
        </div>
    <% end %>
<% end %>
```

link_to のパスにnumというキーを用い、@resultに格納された値をルーティングに送ります
link_to はgetメソッドがデフォルトなので、postメソッドを定義することを忘れてはいけません

```ruby:confg/routes.rb
     post   'posts/:num'          => 'posts#create' # この一行を追記
```

ルーティングでnumに格納された@resultの数値を受け取り、コントローラーに渡します

```ruby:controllers/posts_controller.rb
    # 省略
    def create
       num = params[:num]
       result = Result.new
       result.number = @result
       result.user_id = current_user.id

       if result,save
          @result = num
          @posts = Post.all
          render "posts/index"
       else
          redirect_to root_path
       end

    end
```

コントローラーではnumに格納された値を```params[:num]```でうけとりnumに改めて代入しています
その後は、Resultモデルを呼び出し、resultsテーブルのnumberカラムにnumを、user_idカラムにcurrent_user.idを代入し、保存（save）しています

注意点としては、保存が成功したときのページ遷移を```render "posts/index"```で記述している点です

保存が完了したら再び診断結果を表示したいですが、redirect_toを用いて元のページに遷移してしまうと@resultの値が更新されて空になってしまいます
これでは診断結果のページにはなにも表示ｓれません

そのため、paramsで受け取ったnumの値を@resultに代入し、このまま@resultの値を更新せずにもとのページに戻るためにrenderを用いています

```@posts = Post.all```の記述があるのも、診断結果のページに@postsを渡すためです

ここで詳しくはやりませんが、redirect_toとrenderの違いは以下のようなものです

render　viewファイルを直接指定、viewファイルで用いる変数はrenderの記述があるアクションのもの
コントローラー　→　viewファイル
redirect_to　URLを指定、viewファイルで用いる変数はURLで指定したコントローラーのアクションのもの
コントローラー　→　URL　→　ルーティング　→　コントローラー　→　viewファイル
※↑この方法だと、変数の中身が更新される

以上で診断結果の保存が完了しました


#### ③ 保存した診断結果を表示する

最後に保存された過去の診断結果を表示していきましょう
今回はユーザーマイページに過去の診断結果を表示していきます

```ruby:controllers/users_controller.rb
     def show
        @user = User.find(params[:id]) 
        @posts = Post.all  # この一行を追記
     end
```

いうまでもなく、診断結果を格納したpostsテーブルからすべてのレコードを取得しています

次にviewファイルを編集していきます

```ruby:viwes/users/show.html.erb
  <% @posts.each do |p| %>
     <% @user.results.each do |t| %>
        <%= if p.number == t.number %>
          <%= t.create_at %>の診断
          <%= p.content %>
        <% end %>
     <% end %>
  <% end %>
```

postsテーブルの全てのレコードのうち、resultsテーブルのnumberとpostsテーブルのnumberが一致した場合に、診断を保存した日時と診断の結果を表示しています