はじめに
こんにちは、プログラミングスクールRUNTEQにて学習しておりますEriと申します。
この度、忙しい生活の中で不足しがちな野菜の摂取をお応援するアプリ「おやさいUP」を開発いたしました。
開発した理由
自身の妊娠や子育てを通し、栄養バランスに対する意識が上がりましたが、毎日毎食バランスのよい食事を用意するのは気をつけていてもなかなか難しいと感じています。特にランチなど自分1人分の食事を用意する時には、パンやおにぎり、うどんなど用意が簡単な炭水化物のみで済ませてしまいがちです。 かと言って毎食何gの野菜を摂取したかを記録するのは手間がかかる上、1日の必要野菜摂取量を毎日達成しようと意識しすぎると、義務感や達成できていない罪悪感で食事が楽しくなくなってしまいます。
もっと気軽に楽しく、野菜を食べることへのモチベーションになるようなサービスを作りたいと考え、野菜を食べたかどうかを自己判断で「ゆるっと」記録するという発想に至りました。
メイン機能(⭐︎はログインが必要な機能)
トップ画面 | 投稿一覧・検索機能 |
---|---|
アプリの使用場面とログイン後に使える機能を紹介しています | 投稿の一覧から条件を設定し検索できます |
投稿共有の動的OGP | AIによるレシピ提案 |
---|---|
各投稿のシェアを行うとその投稿の写真が動的OGPとして表示されます | 投稿されたレシピの食材を入れ替えた代用品レシピをAIが提案します |
投稿作成・編集⭐︎ | おやさいLogへの記録⭐︎ |
---|---|
レシピを載せるかは選択制です。下書き保存して後から編集することも可能です。 | その日のご飯で野菜をどれだけ食べたか、直感で記録します。記録に基づき、カレンダーが緑色に色づきます。 |
LINE通知⭐︎ | ブックマーク⭐︎ |
---|---|
おやさいUPのLINE公式アカウントと友達になると、毎日20時におやさいLogのリマインダが受け取れます | 気に入った投稿はブックマークに保存し、後で確認できます |
プロフィール編集⭐︎ | AIの提案レシピを下書き保存⭐︎ |
---|---|
ユーザー名、アバター画像の変更ができます | 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による動的フォームの実装
投稿フォームでは、レシピの有無が選択できます。
レシピ付きを選択した場合は、材料、作り方の入力欄の増減操作が可能です。
下書き保存機能の実装
下書き保存ボタンを押すと、タイトルのみ入力されていれば保存が可能です。
下書き保存ボタンの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」は、朝、昼、夜ごはんで野菜をどれだけ食べたか、直感で記録する機能です。
記録用フォームをモーダルとして設置することで、ページ遷移なく手早く入力できるようにしました。
おやさい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のカラムのみを抽出しています。
表示結果は下の画像のようになります。
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の講師の皆様、スクール生の皆様、本当にありがとうございます!
自分で考えた機能を実装する過程は、なかなか解決策が見つからず苦労することもありましたが、解決した時の喜びはひとしおでした。
少しでも誰かの役に立てるアプリとして成長させていけるよう、引き続き開発していきたいと思います。
最後までお読みいただき、ありがとうございました!