4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【個人開発】野菜の摂取を促進するアプリを作成しました

Posted at

はじめに

こんにちは、プログラミングスクールRUNTEQにて学習しておりますEriと申します。

この度、忙しい生活の中で不足しがちな野菜の摂取をお応援するアプリ「おやさいUP」を開発いたしました。

Image from Gyazo

開発した理由

自身の妊娠や子育てを通し、栄養バランスに対する意識が上がりましたが、毎日毎食バランスのよい食事を用意するのは気をつけていてもなかなか難しいと感じています。特にランチなど自分1人分の食事を用意する時には、パンやおにぎり、うどんなど用意が簡単な炭水化物のみで済ませてしまいがちです。 かと言って毎食何gの野菜を摂取したかを記録するのは手間がかかる上、1日の必要野菜摂取量を毎日達成しようと意識しすぎると、義務感や達成できていない罪悪感で食事が楽しくなくなってしまいます。

もっと気軽に楽しく、野菜を食べることへのモチベーションになるようなサービスを作りたいと考え、野菜を食べたかどうかを自己判断で「ゆるっと」記録するという発想に至りました。

メイン機能(⭐︎はログインが必要な機能)

トップ画面 投稿一覧・検索機能
Image from Gyazo Image from Gyazo
アプリの使用場面とログイン後に使える機能を紹介しています 投稿の一覧から条件を設定し検索できます
投稿共有の動的OGP AIによるレシピ提案
Image from Gyazo Image from Gyazo
各投稿のシェアを行うとその投稿の写真が動的OGPとして表示されます 投稿されたレシピの食材を入れ替えた代用品レシピをAIが提案します
投稿作成・編集⭐︎ おやさいLogへの記録⭐︎
Image from Gyazo Image from Gyazo
レシピを載せるかは選択制です。下書き保存して後から編集することも可能です。 その日のご飯で野菜をどれだけ食べたか、直感で記録します。記録に基づき、カレンダーが緑色に色づきます。
LINE通知⭐︎ ブックマーク⭐︎
Image from Gyazo Image from Gyazo
おやさいUPのLINE公式アカウントと友達になると、毎日20時におやさいLogのリマインダが受け取れます 気に入った投稿はブックマークに保存し、後で確認できます
プロフィール編集⭐︎ AIの提案レシピを下書き保存⭐︎
Image from Gyazo Image from Gyazo
ユーザー名、アバター画像の変更ができます AIに生成してもらったレシピを下書きに保存しておくことができます。

主な使用技術

カテゴリー 使用技術
フロントエンド Rails 7.2.1.2 / TailwindCSS / DaisyUI / Javascript
バックエンド Rails 7.2.1.2 (ruby 3.2.3)
インフラ heroku / AmazonS3
DB MySQL
開発環境 Docker
認証 devise, LINE認証
CI/CD GitHub Actions
Web API OpenAI API, LINE Messaging API

工夫した点

おやさいReport投稿機能

JavaScriptによる動的フォームの実装

投稿フォームでは、レシピの有無が選択できます。

レシピ付きを選択した場合は、材料、作り方の入力欄の増減操作が可能です。

Image from Gyazo

下書き保存機能の実装

Image from Gyazo
下書き保存ボタンを押すと、タイトルのみ入力されていれば保存が可能です。

下書き保存ボタンのname属性をname="draft"と設定しておき、コントローラーではparams[:draft]の有無で処理を分けています。

# フォームの送信ボタン抜粋
<%= f.submit t("form.publish"), class: '…' %>
<%= f.submit t("form.draft"), name: "draft", class: '…' %>

# 下のsubmitを押すと、Parameters: {"authenticity_token"=>"[FILTERED]", "post_form"=>{…省略}, "draft"=>"下書き保存"}のようにparamsが送られる

ちなみにこちらの投稿フォームはForm Objectを利用しています。Form Objectに関しては、以下の記事にもまとめていますので、よろしければご覧ください!

おやさいLog機能

記録用モーダル

「おやさいLog」は、朝、昼、夜ごはんで野菜をどれだけ食べたか、直感で記録する機能です。

記録用フォームをモーダルとして設置することで、ページ遷移なく手早く入力できるようにしました。

Image from Gyazo

おやさいLog記録の仕組み

おやさいLogのデータは、各ユーザーにつき1日1件です。朝、昼、夜ごはんそれぞれの入力された値をcreate、update時に合計し、totalの値も保存します。

# app/models/vegetable_log.rb
class VegetableLog < ApplicationRecord
  validates :date, presence: true, uniqueness: { scope: :user_id }
  belongs_to :user

  def calculate_total
    total = breakfast + lunch + dinner
    self.update(total: total)
  end
end

# app/controllers/vegetable_logs_controller.rb
class VegetableLogsController < ApplicationController
  def create
    @vegetable_log = current_user.vegetable_logs.build(vegetable_log_params)
    if @vegetable_log.save && @vegetable_log.calculate_total
      redirect_to user_path(current_user), notice: t("defaults.flash_message.vegetable_log.recorded", item: VegetableLog.model_name.human)
    else
      redirect_to user_path(current_user), notice: t("defaults.flash_message.vegetable_log.not_recorded", item: VegetableLog.model_name.human), status: :unprocessable_entity
    end
  end

  def update
    @vegetable_log = current_user.vegetable_logs.find(params[:id])
    if @vegetable_log.update(vegetable_log_params) && @vegetable_log.calculate_total
      redirect_to user_path(current_user), notice: t("defaults.flash_message.vegetable_log.recorded", item: VegetableLog.model_name.human)
    else
      redirect_to user_path(current_user), notice: t("defaults.flash_message.vegetable_log.not_recorded", item: VegetableLog.model_name.human), status: :unprocessable_entity
    end
  end

  private

  def vegetable_log_params
    params.require(:vegetable_log).permit(:breakfast, :lunch, :dinner).merge(date: Time.zone.today)
  end
end

JSライブラリの使用

GitHubのようなヒートマップを表現するため、Cal-HeatmapというJavaScriptライブラリを使用しました。

表示したいデータをJSON形式で渡すことで色づきます。

今回は、表示するユーザーに紐づくおやさいLogのデータをJSON形式で渡します。

# app/controllers/users_controller.rbより抜粋
class UsersController < ApplicationController
  def show
    @user = User.find(params[:id])
    vegetable_logs = VegetableLog.where(user_id: @user.id)
    @vegetable_logs = vegetable_logs.to_json(only: [ :date, :total ])
  end
end

to_jsonメソッドのonlyオプションを使用し、dateとtotalのカラムのみを抽出しています。
表示結果は下の画像のようになります。

Image from Gyazo

API使用時のCSRF対策

openAIのAPIを使用するにあたり、CSRFトークンエラーが発生しました。

ActionController::InvalidAuthenticityToken (Can't verify CSRF token authenticity.)

検索するとprotect_from_forgeryをコントローラーに追記するという解決策が出てきますが、それだとCSRF対策を無効化していることになるようです。

そこで下記の記事を参考に、別途CSRF対策用のメソッドを実装しました。

CSRFトークン方式ではなく、リクエストの出所をOriginヘッダー等で検証しています。

  class CsrfProtectionError < StandardError; end
  protect_from_forgery  # こちらは無効にした上で、
  before_action :check_csrf # 別途CSRF対策メソッドを用意
  
  private
  
  def check_csrf
    # Originが許可されていない場合はエラー
    allowed_origins = ['https://oyasaiup.com', 'https://www.oyasaiup.com']
    origin = request.headers['Origin']

    if origin.blank? || allowed_origins.exclude?(origin)
      raise CsrfProtectionError
    end

    # Sec-Fetch-Siteが存在していて許可されていない場合はエラー
    allowed_values = ['same-origin', 'same-site']
    sec_fetch_site = request.headers['Sec-Fetch-Site']

    if sec_fetch_site.present? && allowed_values.exclude?(sec_fetch_site.downcase)
      raise CsrfProtectionError
    end
  end

今後の展望

  • PWA

    忙しい毎日の合間の時間に使ってもらうことを想定しているアプリのため、スマホからもより使いやすくなるよう、PWAを導入したいと考えています。

  • RSpec

    開発中は一部しかテストが書けていなかったため、テストコードを追加しカバレッジを上げていきたいです。

終わりに

アプリ開発を支えてくださったRUNTEQの講師の皆様、スクール生の皆様、本当にありがとうございます!

自分で考えた機能を実装する過程は、なかなか解決策が見つからず苦労することもありましたが、解決した時の喜びはひとしおでした。

少しでも誰かの役に立てるアプリとして成長させていけるよう、引き続き開発していきたいと思います。

最後までお読みいただき、ありがとうございました!

4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?