【個人開発】世の中のWebサービスを探す手間が省けるアプリを、未経験者が開発してみた【Rails7】

# はじめに

はじめまして、[@takutaku_hiro](https://twitter.com/takutaku_hiro)です。
私は現在、スクールに通いプログラミングの勉強を行っています。その中で、初めての個人開発として『WebAppDiary』というWebアプリを作成しました。
まだまだプログラミング学習中の未経験エンジニアであり、技術的な内容などは誤りを含む可能性があります。
そのため、おかしな記述などがあればコメント等で教えていただけたら幸いです🙇

# 作成したアプリ：[WebAppDiary](https://www.webappdiary.com/)

![ogp_image](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3510937/a236be69-1c5c-93e6-cf85-a3e75e19f848.png)

サービス URL：[https://www.webappdiary.com/](https://www.webappdiary.com/)

GitHub URL：[https://github.com/hiroshi-okimura/web_app_diary](https://github.com/hiroshi-okimura/web_app_diary)

ちなみに今後のアップデート情報とかは以下のアカウントで告知していく予定です！よければこちらも見てみてください！

https://twitter.com/takutaku_hiro/status/1702299128712351830

## サービス概要

個人開発の参考として、デザイン・機能・導線などUI/UXの情報収集を継続的に行いたい人のための、 1日1Webサービス提供＆記録ができるWebアプリです。
日めくりカレンダーのような形式で、1日に1つのWebアプリを紹介していきます。

## 開発の背景

プログラミング学習を開始した当初、スクール側の主催で、Webアプリ開発について勉強会が開かれ、講師の方が次のことをおっしゃっていました。

**「世の中に出ているサービスを、1日1つからでいいので利用しましょう」**

これはどういうことかというと、、、

> 良いサービス・使われるサービスというのは、世界観が良い・デザインが綺麗などだけではなく、
> 
> - **「first impressionでどんなサイトかわかる」**
> - **「ユーザーが迷子にならない導線」**
> 
> ということもかなり重要になります。
> しかし、自分で開発していくと自分のアプリが当たり前になってくるので、自分が思っていることと初見ユーザーの印象に乖離が生まれてきます。そうならないためにも、自分が今後開発するのであれば、まずは世の中に出ているサービスを知っていきましょう。
> 

というお話でした。

このお話を聞いた時、「もちろん大事、だけど続く人っていないだろうな」って正直思いました。この状況をチャンスと捉えた自分は、個人開発Webアプリの作成に向けて、**「毎日必ず1つのアプリ・サービスを利用して、UI/UXなどWebアプリについて自身の引き出しを誰よりも増やそう」** と決意しました。

そしていざ蓋を開けてみると、、、**全く続きませんでした笑**
あれだけカッコつけて意気込んでいたのに、情けない:weary:

続かなかった理由としては、そもそも世の中にあるWebアプリ・サービスが多すぎて、どれを利用するか決めるのに時間がかかってしまい、頭の中では「やらなければ」とわかっていても面倒だと感じてしまったからでした。

以上のような自身の体験からヒントを得て、毎日1つのWebサービスを自動的に提供してくれて、そのサービスの利用体験を記録できるアプリがあると便利だなと思い、「WebAppDiary」を開発しました。

## メインターゲットユーザー

**個人開発を行う予定があるプログラミング初学者**

- **【ユーザーが抱える課題】** ~ 個人開発等の参考としてUI/UXの情報収集を行うために、世の中のWebサービスを継続的に体験して記録を残したいが、「Webサービス・アプリが多すぎて探すのが面倒」「単純に忘れる」等の理由から続かない。
- **【解決方法】** ~ 毎日1つのWebサービスを提供し、「ユーザビリティ」「オリジナリティ」「デザイン」について5段階評価で記録・コメントができる。また、ユーザーが任意で通知タイミングを設定することで、記録忘れを防止する。

# 使い方

WebAppDiaryはとてもシンプルなアプリです！ざっくりと使い方・機能について説明します。

まずは新規登録からログイン、もしくはLINEログインすると、「今日のWebアプリ」ページへ遷移します。（ゲストログインも用意しています）

![image](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3510937/1fd5ac6e-481c-a68f-b10b-75b8d772af23.png)
![image](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3510937/aaece6b2-3899-68c3-1541-e66fb9ecc5a6.png)

### 1.　レビュー機能:star:

「レビューする」ボタンから、「オリジナリティ」「ユーザビリティ」「デザイン」の5段階評価と、任意でコメントを残すことができます。

![image](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3510937/1ea1923f-409e-2830-58db-dcd870d9cf7b.png)

### 2.　ブックマーク機能:triangular_flag_on_post:

個人開発の参考にしたいと思ったWebサービスについては、ブックマークボタンを押すことで、右上メニューバーの「ブックマーク」からいつでも確認することができます。

![image](https://i.gyazo.com/141e082973ca4c18da75abc9e06472ed.gif)

### 3.　LINE通知機能:alarm_clock:

#### ※LINEログイン・公式LINEを友達追加することで利用できる機能となっています。:bow_tone1:
右上メニューバーの「通知設定」より、1時間単位の任意のタイミングでLINE通知を受け取ることができます。
![image](https://i.gyazo.com/7069906aa7813293ca44bf476c1a7340.gif)

実際のLINE通知がこちら:point_down:
<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3510937/1693d7c1-eb4b-c98d-9bbe-c8341588a91b.jpeg">


# 工夫した点

### ①各Webサービスのサイトトップページが一目でわかる
[「開発の背景」](https://qiita.com/drafts/e6640f4f8cb15d148c00/edit#%E9%96%8B%E7%99%BA%E3%81%AE%E8%83%8C%E6%99%AF)でも述べていますが、Webサービスにとって **「first impressionでどんなサイトかわかる」「ユーザーが迷子にならない導線」** は非常に重要です。これらの要点を最も効果的に伝えるページは、ご存知の通りトップページやランディングページ（LP）だと思いました。
そこで、各Webサービスの詳細ページにサイトトップページのフルスクリーンショットを掲載することで、ユーザーがそのWebサービスのトップページがどのようなものかを一目で理解できるようにしました。
[![Image from Gyazo](https://i.gyazo.com/6b5395c2646be2702d214ad1b4523c41.gif)](https://gyazo.com/6b5395c2646be2702d214ad1b4523c41)
このフルスクリーンショットの取得には、**[「screenshotlayer API」](https://screenshotlayer.com/)** を使用しました。
フルスクリーンショットを取得できるAPIは他にも存在しますが、screenshotlayer APIは無料で月100枚$^※$までショットが取得可能で、その機能も十分だったため、このAPIを選択しました。
また、ドキュメントに言語別の実装例としてRubyがあったのも、選択した理由の1つです。

:::note info
※現在は無料で月1000枚まで取得可能になったみたいです！！
:::

実際は以下の流れの通りに実装しました。
1. 管理画面からWebサービスの情報を登録
2. 登録したWebサービスのURLを元に、ScreenshotServiceを使用してフルスクリーンショットのURLを取得
3. 取得したフルスクリーンショットのURLの内容を、**CarrierWave**を使用して**Amazon S3**にアップロード

```ruby:app/controllers/admin/web_apps_controller.rb
class Admin::WebAppsController < Admin::BaseController

(省略)

  def create
    @web_app = WebApp.new(web_app_params) # ストロングパラメータの中にurl属性が含まれている。

    # screenshotlayer APIの仕様に応じて、optionsを設定
    options = {
      fullpage: 1,
      width: "",
      viewport: "",
      format: "",
      css_url: "",
      delay: 3,
      ttl: "",
      force: "",
      placeholder: "",
      user_agent: "",
      accept_lang: "",
      export: ""
    }

    screenshot_url = ScreenshotService.screenshotlayer(@web_app.url, options)

    （省略：エラーハンドリング実装）

        @web_app.remote_screenshot_url = screenshot_url

    if @web_app.save
      redirect_to admin_web_apps_path, success: t('defaults.message.registed', item: WebApp.model_name.human)
    else
      flash.now[:danger] = t('defaults.message.not_registed', item: WebApp.model_name.human)
      render :new, status: :unprocessable_entity
    end
  end
 
  private

  def web_app_params
    params.require(:web_app).permit(:site_name, :url, :ogp_title, :ogp_description, :ogp_image, :offer_date)
  end
end
```
```ruby:app/services/screenshot_service.rb
class ScreenshotService
  require 'cgi' unless defined?(CGI)
  require 'digest' unless defined?(Digest)

  def self.screenshotlayer(url, options = {})
    # set access key
    access_key = ENV['SCREENSHOTLAYER_ACCESS_KEY']

    # set secret keyword (defined in account dashboard)
    secret_keyword = ENV['SCREENSHOTLAYER_SECRET_KEYWORD']

    # define parameters
    parameters = {
      :url          => url,
      :fullpage     => options[:fullpage],
      :width        => options[:width],
      :viewport     => options[:viewport],
      :format       => options[:format],
      :css_url      => options[:css_url],
      :delay        => options[:delay],
      :ttl          => options[:ttl],
      :force        => options[:force],
      :placeholder  => options[:placeholder],
      :user_agent   => options[:user_agent],
      :accept_lang  => options[:accept_lang],
      :export       => options[:export]
    }

    query = parameters.
      sort_by { |s| s[0].to_s }.
      select { |s| s[1] }.
      map { |s| s.map { |v| CGI::escape(v.to_s) }.join('=') }.
      join('&')

    # generate md5 secret key
    secret_key = Digest::MD5.hexdigest(url + secret_keyword)

    # スクリーンショットのURLが発行される
    "https://api.screenshotlayer.com/api/capture?access_key=#{access_key}&secret_key=#{secret_key}&#{query}"
  end
end
```

### ②ユーザーの好きなタイミングでLINE通知を受け取れる
事前に行ったアンケートで、多くのユーザーから通知機能の要望があったため、[LINE Messaging API](https://developers.line.biz/ja/services/messaging-api/)を使用してLINE通知機能を実装しました。
![スクリーンショット 2023-09-18 17.43.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3510937/83ce4f22-31d6-11d5-1726-75244fb5cc72.png)
![スクリーンショット 2023-09-18 17.44.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3510937/c7bfd1e5-8d81-c290-603f-38a946f80c0b.png)
![スクリーンショット 2023-09-18 17.44.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3510937/2fd72e07-d7e9-6a69-5dab-c1d024145df6.png)
LINE通知のタイミングは様々ですが、このアプリでLINE通知機能を導入した主な目的は、ユーザーの記録忘れを防ぐためです。全員に同じタイミングで通知（例えば新しいWebサービスが提供されたタイミング等）するのではなく、ユーザーがそれぞれ好きなタイミングで通知を受け取れるようにしたいと思いました。
そこで、```User```モデルに```notify_time```カラムを追加し、ユーザーが1時間単位で通知時刻を自由に設定できるように実装しました。
このアプリは**Heroku**を使用しているので、流れとしては以下のようになります。
1. [Heroku Scheduler](https://devcenter.heroku.com/ja/articles/scheduler)というアドオンを利用してrakeタスク```send_line_message```を1時間間隔​で実行する。
1. 現在時刻に```notify_time```を設定しているユーザーを取得する。
1. 対象のユーザーに、LINEメッセージを送信する。

```ruby:lib/tasks/line_notify.rake
namespace :line_notify do
  desc "LINE通知メッセージ送信"
  task send_line_message: :environment do
    client = Line::Bot::Client.new do |config|
      config.channel_secret = ENV['LINE_CHANNEL_SECRET']
      config.channel_token = ENV['LINE_CHANNEL_TOKEN']
    end

    # UTC時間に9時間を加算して日本時間を取得
    current_hour_jst = (Time.current.utc.hour + 9) % 24

    # 日本時間をUTCに変換
    current_hour_utc = (current_hour_jst - 9) % 24

    users_to_notify = User.where("EXTRACT(HOUR FROM notify_time AT TIME ZONE 'UTC') = ?", current_hour_utc)
    users_to_notify.each do |user|
      next unless user.uid # user.uidがnilや空の場合は次のループへ

      message = {
        type: 'text',
        text: "新しいWebアプリが提供されています!\n確認しましょう!\n(通知を止めたい場合は、「通知設定」より設定解除してください)\n\nhttps://www.webappdiary.com/web_apps/todayapp"
      }
      response = client.push_message(user.uid, message)
      p response
    end
  end
end
```
意外と苦労した点として、Heroku上で日本時間に設定していても、Heroku SchedulerはUTCで動作するので、その時差を考慮する必要がありました。最終的なコードは、

```ruby
# UTC時間に9時間を加算して日本時間を取得
current_hour_jst = (Time.current.utc.hour + 9) % 24

# 日本時間をUTCに変換
current_hour_utc = (current_hour_jst - 9) % 24
```

となりましたが、ここまで来るのにかなり時間がかかってしまいました笑

# 主な使用技術
#### **バックエンド**
- Ruby on Rails (7.0.6)
- Ruby (3.2.0)
- PostgreSQL
- gem
  - devise
  - omniauth-line
  - carrierwave
  - kaminari
  - simple_calendar
- API
  - LINE Messaging API
  - screenshotlayer API
#### **フロントエンド**
- Tailwind CSS
- Hotwire
#### **インフラ**
- Heroku
- Amazon S3

## 技術選定について
私が通っているプログラミングスクールのカリキュラムはRailsを中心にしているため、使い慣れているRailsを選択しました。また、できるだけ短期間での開発を目指したため、技術スタックはシンプルに保ちました。
ただ、その中でも多少の融通を利かせたかったため、Tailwind CSSを使用しました。Tailwind CSSの取り扱いには初めは苦労しましたが、レスポンシブデザインの実装が容易であるため、非常におすすめです。

# ER図
```mermaid
erDiagram
  Users ||--o{ Reviews : has
	Users ||--o{ Bookmarks : has
  WebApps ||--o{ Reviews : has
	WebApps ||--o{ Bookmarks : has
  Users {
	bigint id PK
	int character_id FK
	string name
	string email
	string encrypted_password
	enum role
	datetime notify_time
	string reset_password_token
	datetime reset_password_sent_at
	datetime remember_created_at
	string provider
	string uid
  }
  WebApps {
	bigint id PK
	string site_name
	string url
	text ogp_description
	string ogp_image
	string screenshot
	date offer_date
  }
  Reviews {
	bigint id PK
	int user_id FK
	int web_app_id FK
	text comment
	int originality_score
	int usability_score
	int design_score
	date webapp_offer_date
  }
  Bookmarks {
	bigint id PK
	bigint user_id FK
	bigint web_app_id FK
  }
```

# 開発期間
MVPリリースまで約１ヶ月
本リリースまで1ヶ月

# 今後の展望
- フロントエンドにJavaScriptの追加
- 当サービス内でのWebアプリ検索機能（オートコンプリート機能）
- OGP画像の動的表示

# おわりに
まだまだ改善すべき点は多々ありますが、今回初めて自分で0からサービスを作りあげ、またこのように初めて技術記事を投稿することができました。まさか自分がWebアプリを作ることができる日が来るとは、、、開発することの楽しさを味わうことができましたし、もちろんたくさんの学びを得ることもできました。

この記事が、少しでも誰かのお役に立てば大変幸いです。
拙い記事でしたが、最後までお読みいただきありがとうございました！