【個人開発/未経験】睡眠の質を高めるためのサポートをするアプリを開発してみた

# はじめに
はじめまして！@tomoya4819と申します。

突然ですが、皆様は質の良い睡眠を取り、スッキリとした朝を迎えられていますか？
私は夜遅くに寝て、朝ギリギリに起きるという生活を繰り返していた、朝が苦手な人間でした。

そこで、睡眠の質が良くなる行動をいくつか調べ、実践してみると夜早く寝付けるようになり、朝も余裕を持って起きることができました。
しかし、一番大切なのはその行動を続けること。つまり続けやすくなるようなアプリを作ればより睡眠の質改善に繋がるのでは！？と思い、本アプリ作成へと至りました。

※私は現在プログラミング学習中の未経験エンジニアであり、技術的な内容などは誤りを含む可能性があります。
そのため、間違っている点があればコメント等で教えていただけると幸いです。:bow_tone1:

# サービス名：[ZzzCrafter](https://zzzcrafter.fly.dev)
![ogp.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3185454/e4a1a43b-c9ef-7619-75f2-2e54988fc5a3.png)

サービス URL：https://zzzcrafter.com
GitHub URL :https://github.com/hosodatomoya41/ZzzCrafter

# サービス概要
ZzzCrafterは、睡眠の質が悪く悩んでいる方のための睡眠の質改善サービスです。
本アプリでは医学的に効果のある行動から抜粋した、睡眠に役立つ様々なルーティーンをご紹介しております。
その中からご自身に合うものを見つけ、継続していくことで睡眠の質の向上をサポートします。

# 使い方
本アプリはレスポンシブ対応していますが、ご利用にあたってLINE登録をお願いしておりますのでスマートフォンからのご利用を推奨いたします。

**1.睡眠時間の設定**
起床時間と就寝時間を設定していただくと、適切なタイミングでLINE通知が届くようになり、睡眠の記録も確認できるようになります。

**2.ルーティーンの登録**
おすすめの実践時間別に様々なルーティーンをご紹介しております。
気になったルーティーンを登録してみてください。

**3.翌朝に調子を記録**
翌朝の設定した起床時間にLINE通知が届くので、起きたときの調子を3段階評価で記録できます。
実践しやすく、効果を感じるルーティーンが見つかるまで色々試してみてください！

| 1.睡眠時間の設定 | 2.ルーティーンの登録 | 3.翌朝に調子を記録 |
|:--|:--|:--|
| ![sleep_record.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3185454/bf658c98-e4d2-405b-a2c8-bf550ca0e5e1.gif) | ![routine_index](https://github.com/hosodatomoya41/ZzzCrafter/assets/123244117/a5eaf8af-2498-499c-8d80-4cfe16fcab68) | ![morning_condition](https://github.com/hosodatomoya41/ZzzCrafter/assets/123244117/be344269-65bf-4cab-829b-d993320da3cc) |
| 起床時間と就寝時間を設定します。 | 実践しやすいものを選び<br>登録することができます。 | 設定した起床時間に<br>LINE通知が届きます。 |

# 工夫した点
### ①LINE内で一連の流れを行えるように実装
習慣化のためのハードルをなるべく下げるため、LINE Messaging APIを利用し、LINE内で一連の流れを完結できるようにしました。

| 睡眠の記録の確認 | ルーティーンの登録 | おすすめのルーティーンの表示 |
|:--|:--|:--|
| ![line_sleep_record](https://github.com/hosodatomoya41/ZzzCrafter/assets/123244117/1955b002-96e1-4c78-a5ec-338c20892993) | ![routine_index](https://github.com/hosodatomoya41/ZzzCrafter/assets/123244117/f9aa0dec-45bb-4817-ba46-958beef971df) | ![line_recommend_routine](https://github.com/hosodatomoya41/ZzzCrafter/assets/123244117/3d5be285-cffb-446c-9153-a14cb6b68e75) |
| 睡眠の記録を確認できます。<br>起床、就寝時間の設定も行えます。 | ルーティーン一覧を確認し<br>登録することができます。| 実践したルーティーンから<br>最適なものを確認できます。|

以下の流れで実装を行いました。

1.LINEメニューをタップ時の応答メッセージで`callback`メソッドが`handle_message`メソッドを呼び出す
2.event引数で受け取ったメッセージ内容で処理を分岐
3.FlexMessageを構築、ユーザーに送信
4.送信されたメッセージのボタンをユーザーがタップすると`callback`メソッドが`handle_postback`メソッドを呼び出し、ルーティーンの登録等の処理を行う

```ruby:app/controller/linebot_controller.rb
  def callback
    body = request.body.read

    signature = request.env['HTTP_X_LINE_SIGNATURE']
    return head :bad_request unless client.validate_signature(body, signature)

    events = client.parse_events_from(body)
    events.each do |event|
      case event
      when Line::Bot::Event::Postback
        data = event['postback']['data']
        handle_postback(event, data)
      when Line::Bot::Event::Message
        case event.type
        when Line::Bot::Event::MessageType::Text
          handle_message(event)
        end
      end
    end
  end

  private

  def handle_message(event)
    line_user_id = event['source']['userId']
    user = User.find_by(line_user_id: line_user_id)
    received_text = event.message['text']

    if %w[睡眠の記録を見る ルーティーン一覧を見る おすすめのルーティーンを教えて].include?(received_text)
      Richmenu.postback(user, event, received_text)
    elsif received_text =~ /^(\d{4}年)?(1[0-2]|0?[1-9])月$/
      handle_month(user, event, received_text)
    else
      handle_register_routine(user, event, received_text)
    end
  end

  def handle_postback(event, data)
    user_id = event['source']['userId']
    user = User.find_by(line_user_id: user_id)
    # クエリストリング形式で受け取ったデータを解析
    params = Rack::Utils.parse_nested_query(data)
    case params['action']
    when 'good', 'normal', 'bad'
      handle_morning_condition(user, event, params)
    when 'wakeup', 'sleep'
      handle_sleeptime(user, event, params)
    when 'show_routines'
      handle_show_routines(user, event, params)
    when 'add_routine'
      handle_register_routine(user, event, params)
    end
  end
  ```


### ②最適なタイミングでの通知機能
起床する時間に通知を送って起床を促すだけではなく、ルーティーンの最適な実践時間にLINEメッセージを送信しています。そうすることにより、ルーティーンの実践忘れの防止や決められた時間に行えるように習慣化を目指しました。

以下が実装の流れです。
1.`User`モデルに`bedtime`と`notification_time`の２つのカラムを用意し、就寝時間と起床時間を登録できるように設定
2.Fly.ioにデプロイを行っているので時間帯をTokyoに設定するように注意し、rakeタスクのファイルの実装
3.ユーザーに設定された起床時間、ルーティーン実践時間にLINEメッセージの送信

Fly.io上でcrontabのタスクを定期実行するにあたり、Supercronicを活用しました。

https://fly.io/docs/app-guides/supercronic/

```ruby:lib/tasks/routine_notify.rake
namespace :routine_notify do
  desc 'ユーザーに設定されたルーティーンの推奨実践時間にルーティーン実践の催促の通知送信'
  task send_notifications: :environment do
    # 今日ルーティンを登録しているユーザーごとに実行
    User.today_routine.find_each do |user|
      current_time = Time.now.in_time_zone('Tokyo').strftime('%H:%M')
      # ユーザーの就寝時間を取得
      bedtime = user.bedtime.strftime('%H:%M')
      user_routines_to_notify = []

      user.user_routines.includes(:routine).each do |user_routine|
        # ルーティンが今日実行されるものかどうかをチェック
        if user_routine.choose_date == appropriate_date(user_routine, user_routine.routine.recommend_time.to_sym)
          # 通知を送るべき時間を計算
          recommended_notify_time = user.calculate_time(user.bedtime, user_routine.routine.recommend_time.to_sym)
          # 現在時間が通知時間と一致する場合、通知配列にルーティン名を追加
          user_routines_to_notify << user_routine.routine.name if recommended_notify_time == current_time
        end
      end

      Notification.send_routine_notification(user, user_routines_to_notify, current_time, bedtime) unless user_routines_to_notify.empty?
    end
  end

  def appropriate_date(user_routine, recommend_time)
    # 就寝時間が0:00以降でも適切に通知を送るためのメソッド
    # ユーザーの就寝時間を取得
    bedtime = user_routine.user.bedtime.strftime('%H:%M')
  
    if bedtime >= '00:00' && bedtime <= '00:59'
      # ルーティーン実践時間が0:00以降で就寝時間と同じ時間の場合、日程を昨日に設定
      recommend_time == :before0 ? Date.yesterday : Date.today
    elsif bedtime > '00:59' && bedtime < '06:00'
      # 就寝時間が1時以降でルーティーン実践時間が0:00以降の場合、日程を昨日に設定
      [:before0, :before1].include?(recommend_time) ? Date.yesterday : Date.today
    else
      Date.today
    end
  end
end
```

```ruby:lib/tasks/wakeup_notify.rake
namespace :wakeup_notify do
  desc 'ユーザーがルーティーンを登録すると、翌朝の起床時間にLINE通知を送信'
  task send_notifications: :environment do
    today = Date.today

    User.where.not(notification_time: nil).find_each do |user|
      current_time = Time.now.in_time_zone('Tokyo').strftime('%H:%M')
      # ユーザーの通知時間を取得
      notification_time = user.notification_time.strftime('%H:%M')
      # ユーザーがルーティーンを登録しているか確認
      has_registered_routine = user.user_routines.exists?(choose_date: today - 1)

      Notification.send_wakeup_notification(user) if current_time == notification_time && has_registered_routine
    end
  end
end
```

# 主な使用技術
| カテゴリ         | 技術               |
|----------------|-------------------|
| バックエンド     | Ruby on Rails 7.0.6 |
|              | Ruby 3.0.2        |
| フロントエンド   | Hotwire       |
|              | TailwindCSS    |
| データベース    | PostgreSQL        |
| インフラ        | Fly.io            |
| API            | LINE Messaging API|
# ER図
![er_diagram](https://github.com/hosodatomoya41/ZzzCrafter/assets/123244117/5498f8a9-fb24-4db1-b4bf-6452292e812b)

# 今後の展望
今後実装したい機能面については下記となります。

**1.認証機能の拡充**
現状、ログインはLINEログイン機能のみとなっているので、Webブラウザでの利便性も兼ねて認証機能を増やしたいと考えております。

**2.ルーティーン項目の追加**
ご紹介しているルーティーンの数はまだバリエーション豊かとは言えないので、適宜情報を調べつつ増やしていきたいです。
(おすすめのルーティーンなどあれば教えてください！！)

**3.テストコードの追加**
現状ではテストコードがあまり書けていないので、順次書いていくつもりです。

# 終わりに
今回初めてサービスを1から作成しましたが、本当に多くの学びがありました！
課題を解決できるようなサービスを自分で作れる、開発の楽しさを実感しました。
まだまだ改善点もありますので、引き続きアップデートをしていきます。
睡眠に悩んでいる方がいらっしゃいましたら、少しでも触っていただけると嬉しいです！

この記事が、少しでも皆さんの開発における参考となれば幸いです。
拙い文章となりましたが、最後までご覧いただきありがとうございました！