【個人開発】スクール生活をより豊かにするためのアプリを開発しました🤖

## はじめに
私はプログラミングスクール「RUNTEQ」に通い、プログラミング学習をしております。
今回私はスクール生活での小さな悩みを解消するアプリを開発いたしました。

RUNTEQではカリキュラムなどで分からない問題があったときや、
就職活動等の話を受講生同士で相談し合いたい時に、
Discord（ビデオ通話・音声通話でのコミュニケーションツール）を用いて受講生同士で交流しあうことがよくあります。

その際に私は2つの思いがありました。
1. オンラインスクールであるため、気軽にDiscrodで集まる人を募集するハードルが少し高い...もっと気軽に声をかけたい！という思い
2. Xなどの投稿を通じて募集を行う人が多かったのですが、SNSのコメント欄などで、日程調整をすることが少し手間だな...という思い

今もRUNTEQでは交流は盛んでコミュニティによく助けられているのですが、
受講生同士が今よりももっと気軽に学習や就職活動の相談を行い、より交流の輪を広げることができるようになったらいいな...という思いがきっかけで、  
Discordに集まる日程や時間を調整できるアプリとしてこのアプリを作成しました。  

### Link_Disco
![Discordで集まろう (1)](https://github.com/ys1227/link_discord/assets/132570742/711e1f67-b71d-4ce3-8770-602e414acee9)

<br />

### 🚀サービスURL🚀
ゲストログイン機能を実装いたしましたので、RUNTEQスクール生以外でもお試しでお使いいただくことができます。  

サービスURL
www.linkdisco-app.com
GithubURL
https://github.com/ys1227/link_discord/blob/main/README.md

:::note
デスクトップからのご利用を推奨しています。
:::
<br />

## 🚀アプリケーションのイメージ🚀

🤖デモ動画🤖
<br /> 

![demo](https://raw.github.com/wiki/ys1227/link_discord/images/way_of_use.gif)

<br />  
<br />  

## 🚀機能一覧🚀

### ①ログイン機能
RUNTEQ生向けに開発したためRUNTEQ生のみにログインは限定しています。

:::note info
RUNTEQ生以外の方でもゲストログイン機能を実装いたしましたのでお試しで機能をお使いいただけます。
投稿などは5秒後に削除されるようになっています。
:::

| ログイン |　ゲストログイン機能 |
| ---- | ---- |
| ![ログイン画面](https://raw.github.com/wiki/ys1227/link_discord/images/02_login_discord.png) |![ゲストログイン画面](https://raw.github.com/wiki/ys1227/link_discord/images/15_guest_login.png)
| Discordによる外部認証機能を実装しました。 スクール生のみにログインを限定しています。| ゲストログイン機能でスクール生以外の方でもお試しいただけます。 |


### ②募集作成機能
| 募集作成画面 |　募集作成画面 |
| ---- | ---- |
| ![募集作成(カテゴリ設定)画面](https://raw.github.com/wiki/ys1227/link_discord/images/05_post.png) | ![募集作成作成画面](https://raw.github.com/wiki/ys1227/link_discord/images/06_post.png) |
| 投稿時に募集するテーマに沿ったカテゴリ(例: 質問・雑談・アプリ・就活など) を設定することができます。 | 募集を行う際のタイトルや内容などを設定できます。 |

| 募集時間投稿画面 |　優先順位設定画面 |
| ---- | ---- |
| ![募集時間投稿細画面](https://raw.github.com/wiki/ys1227/link_discord/images/07_reservations.png) | ![　優先順位設定画面](https://raw.github.com/wiki/ys1227/link_discord/images/08_rank_posts.png) |
| 募集時間を最大で第三希望まで設定できます。 | 希望時間に対して優先順位を設定できます。 |

| 投稿確認画面 |　投稿一覧画面 |
| ---- | ---- |
| ![投稿確認画面](https://raw.github.com/wiki/ys1227/link_discord/images/09_confirmanation.png) | ![　投稿一覧画面](https://raw.github.com/wiki/ys1227/link_discord/images/10_post.png) |
| 設定した時間と優先順位に関して最終確認を行うことができます。 | 投稿一覧画面から受講生が募集しているテーマを探しに行くことができます。 |


### ③応募機能

| 投票画面 |　集合時間確認画面 |
| ---- | ---- |
| ![投票画面](https://raw.github.com/wiki/ys1227/link_discord/images/11_reservations.png) | ![　集合時間確認画面](https://raw.github.com/wiki/ys1227/link_discord/images/12_matching_time.png) |
| 受講生が参加したい時間に対して投票できるようになっています。 | 締め切り時間到来後に、アプリケーションで決定した集合時間を確認することができます。 |

### その他
| チャット画面|　DiscordへのDM送信画面 |
| ---- | ---- |
| ![チャット画面](https://raw.github.com/wiki/ys1227/link_discord/images/14_chat.png) | ![　DiscordへのDM送信画面](https://raw.github.com/wiki/ys1227/link_discord/images/Discord.png) |
| 双方向通信でチャットを行うことができます。 | 集合時間が決定するとDiscordのDMでお知らせを行います。 |

<br />

## 🚀サービスの利用イメージ🚀

### 1. 募集を行う側の操作


![demo](https://raw.github.com/wiki/ys1227/link_discord/images/how_to_use_01.png)

<br/>

### 2. 参加したい側の操作

![demo](https://raw.github.com/wiki/ys1227/link_discord/images/how_to_use_02.png)

<br/>

### 3. 締め切り時間到来後の流れ

![demo](https://raw.github.com/wiki/ys1227/link_discord/images/how_to_use_03.png)


## こだわったところ

アプリ開発をするにあたって自分なりにこだわったポイントがあります。
以下の3点になります。

![　投稿一覧画面](https://raw.github.com/wiki/ys1227/link_discord/images/quita05.png)

### **1. UI**
### **2. 設計**
### **3. レートリミット対策**

## 　①UI設計
ユーザーにとって使いやすい設計になるように心がけました。
具体的には以下の2点です。

**1. アプリを訪れただけで世界観がイメージできるようなデザインになるように心がける
2. アプリの操作においてユーザーの動作が少なくて済むように工夫した**

## 1-1 アプリを訪れただけで世界観がイメージできるようなデザインになるように心がける
| トップ画面 |投稿一覧画面  |
| ---- | ---- |
| ![Top画面](https://raw.github.com/wiki/ys1227/link_discord/images/01_top.png) | ![　投稿一覧画面](https://raw.github.com/wiki/ys1227/link_discord/images/10_post.png) |

アプリを使ってみたい、また継続して使い続けていきたいと思うかどうかは、
アプリの機能性の他にデザインが大きな要因を占めていると他のサービスなどを実際に使ってみて感じていました。
そのためデザインに世界観が表れるように工夫しました。
私はDiscordには宇宙のイメージがあったので色使いを工夫し、背景に星を入れてみたりとデザインを試行錯誤しました。


## ②アプリの操作においてユーザーの動作が少なくて済むように工夫した

### 2-1　ユーザーによる操作の削減

アプリを使うときに必要な操作が多すぎるとユーザーにとっては負担になると考えたので、できるだけユーザーの操作が少なくなるように設計しました。

以下の操作画面はユーザーが設定した募集時間に対して希望順位を設定する画面です。
第3希望までユーザーは募集時間を登録できるようになっているのですが、１つのリソースに対して3回Postのリクエストを送るのはかなり手間だと考えました。
そのためバックエンドの実装を工夫して一回のPostのリクエストの動作で複数リソースを登録できるように実装しました。
</br>
</br>

![　優先順位設定画面](https://raw.github.com/wiki/ys1227/link_discord/images/08_rank_posts.png) 

</br>

### 2-2　ユーザーによる操作の削減

また、以下の2点をアプリ側で行うことによってユーザーの操作を減らしました。
</br>

**1. 応募締め切り時間を自動設定**
**2. 投票集計後、アプリケーションで集合時間を決定して通知**
</br>
</br>

![　操作削減](https://raw.github.com/wiki/ys1227/link_discord/images/quita01.png) 
</br>
</br>

 **1. 応募締め切り時間を自動設定**
応募の締め切り時間は募集時間を登録すると自動でアプリケーションが設定して募集を開始してくれるようにしました。
また締め切り時間に関してはSidekiq-Cronを採用し監視することによって、
ユーザーが締め切り時間などを気にしなくても勝手に管理が行われるように実装しました。

**2. 投票集計後、アプリケーションで集合時間を決定して通知**
また、集合時間の決定に関してですが、締め切り時間が来たらアプリケーションが投票を集計し、ユーザーが確認しなくても勝手に集合時間が決定するように設計しました。
また、集合時間をDiscordで通知することによってユーザがわざわざアプリを見にいかなくても簡単に集合時間を把握できるように実装しました。


## ②設計

２つ目に工夫した点は、サーバーの役割分担です。  

![　優先順位設定画面](https://raw.github.com/wiki/ys1227/link_discord/images/quita02.png) 

</br>
</br>

主要なCRUD操作を扱うサーバーを設ける一方で、
ユーザーからの直接的なリクエストがないバックグラウンドタスク（例えば、募集締切時刻の自動判定など）は、**Sidekiq**と**Redis**を活用した別のサーバーで処理するようにしました。
これにより、システム全体の負荷を効果的に分散させ、各サーバーの負担を最適化することを心がけました。
</br>
</br>

https://qiita.com/seiyatakahashi/items/cb9ae73e5ba3020f4a89

https://zenn.dev/overflow_offers/articles/20230130-how-to-use-sidekiq

## ③レートリミット制限対策

３番目に工夫したポイントはDiscordの**レートリミット対策**です。
**レートリミット**とは、一定時間内に許可されるリクエストの最大数を制限することで、過度なトラフィックを防ぐ仕組みです。
</br>
</br>

![　レートリミット](https://raw.github.com/wiki/ys1227/link_discord/images/quita03.png) 
</br>

[レートリミットとは](https://www.cloudflare.com/ja-jp/learning/bots/what-is-rate-limiting/)
</br>
</br>

今回私が実装したかったのは、Discordで集合時間を通知する機能の実装でしたが、このプロセスでレートリミットに引っかからないように設計する必要がありました。

具体的には、以下のフローで通知システムを実装しました。

**1. CronJobを使って、締め切り時間を過ぎた投稿がないか監視
2. 締め切り時間を過ぎた投稿に対して、投票と優先順位を考慮して集合時間を決める処理をJobで実装
3. 募集した人と投票した人に通知を送る処理もJobで実装**

問題は、通知を送る3の最終ステップでした。
もともとはeachメソッドを用いて一斉に通知リクエストを送っていましたが、
これだと通知対象が多い場合に一瞬で大量のリクエストが送信され、レートリミットに達してしまいます。
そこで、私は**Sidekiq**と**Redis**を使用して、リクエストをキューに溜めておき、一定のインターバルを空けてから順次送信するようにしました。


### SidekiqとRedisを利用した処理の流れ

![　レートリミット](https://raw.github.com/wiki/ys1227/link_discord/images/quita04.png) 

具体的には以下の処理です。

```ruby
# 通知までの待ち時間を設定
wait_time = 15

# ジョブを待ち時間を設けてActiveJobを用いてキューに登録
ExampleJob.set(wait: wait_time.seconds).perform_later(a, b)

```

これによって、リクエストが一度に集中することなく、Discordサーバーに対して適切なペースで通知を行うことが可能になりました。

https://stateful.com/blog/discord-rate-limiting

## PF作成を通じて学んだこと

アプリ開発において大事であると学んだことがあります。
以下の2点です。

**1. 公式ドキュメントをしっかり読む**
**2. 理解が不十分なまま実装を進めない**


## ①公式ドキュメントをしっかり読む

RubyでDiscord APIを使ったログイン処理や通知処理を実装する際、資料が少ない中で公式ドキュメントが大きな助けになりました。

### 1-1 エラー解決に役立てる
RubyでのDiscord APIの利用において、以下のような認証エラーに直面しました。

```ruby
channel_id = Discordrb::API::User.create_pm(ENV['DISCORD_BOT_TOKEN'], user.uid.to_i)
```
```ruby:エラー
[ERROR] 401: Unauthorized
```
Postmanを使ってエラーの原因を探ってみたものの、解決には至りませんでした。
しかし公式ドキュメントを読み込むことで、エラーの解決策を見つけることができました。

公式ドキュメントを見てみると、トークンを渡す際には「Bot」プレフィックスをつける必要があると記されていましたが見落としてしまっていました。

```ruby: BotのPrefixについて
Authorization: Bot YOUR_BOT_TOKEN
```
この情報を元に、以下のように修正したところ、エラーが解決しました。

```ruby
# 変更前
ENV['DISCORD_BOT_TOKEN']

# 変更後
"Bot #{ENV['DISCORD_BOT_TOKEN']}"
```
https://discord.com/developers/docs/reference#authentication

この経験から、公式ドキュメントの細部にまで目を通すことの大切さを学びました。
開発においては、言語やライブラリの公式ドキュメントがとても重要なリソースであるということを再認識しました。


## ②理解が不十分なまま実装を進めない

ActionCableを用いてWebSocket通信によるチャット機能を実装した際、概念の理解が不十分なまま実装を進めてしまい、失敗をしました。
できるだけオブジェクトなどを生成していたら何が行われているかなどを把握しながら実装することを大切にしよう、と意識するきっかけになりました。

### チャット機能
以下が実際に実装したチャット機能になります。

![　チャット機能](https://raw.github.com/wiki/ys1227/link_discord/images/chat.gif) 

### 実装の流れ

ActionCableを使用してチャット機能を実装した際の流れは以下の通りです。
</br>
</br>

![　レートリミット](https://raw.github.com/wiki/ys1227/link_discord/images/quita_07.png) 
</br>
</br>

**1. チャットルームに入室すると、JavaScriptを介してConsumerオブジェクトが生成され、WebSocket通信のコネクションが確立する。**

```JavaScript
//consumerオブジェクトの初期化
import consumer from "./consumer"
// consumerオブジェクトのsubscriptionsプロパティにアクセス。createメソッドを呼び出している。
//createメソッドによって、subscriptionsプロパティに新しく作成されたサブスクリプションの値(Channel)が追加される。
consumer.subscriptions.create({ channel: "ChatChannel", room: "Best Room" })
```

https://github.com/rails/rails/blob/68b20b6513fe56ca80e4966628c231b4d6113bea/actioncable/app/javascript/action_cable/consumer.js

**2. 入室した際に、特定のパラメータがそのユーザーに割り当てられ、サーバーサイドでのストリームが設定される。**

```ruby
# app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
  # コンシューマーがこのチャネルのサブスクライバになるとこのコードが呼び出される
  # （consumerオブジェクトを作成したタイミングで同じチャンネルの値を持つ他のユーザーとストリームで繋がる。）
  def subscribed
  end
end
```

**3.チャットルームでEnterキーが押されると、割り当てられたパラメータを持つユーザーに対して非同期でメッセージが配信（broadcast）される。**

```ruby
def speak(message)
# 同じチャネル(subscriptionプロパティの値として設定されている)をサブスクライブしている全てのユーザーに対してメッセージを配信する
    ActionCable.server.broadcast(
    # 省略
    )
  end
```

**4. 配信されたメッセージはユーザーによって受け取られ、クライアント側のDOMが更新される。**

```JavaScript
received(data) {
    const messages = document.getElementById("exampleid");
    messages.insertAdjacentHTML('beforeend', data['body']);
  },
```

https://railsguides.jp/v7.0/action_cable_overview.html


しかし、ActionCableに関する十分な理解がないまま実装を進めた結果、以下のような問題が発生しました。

### 起こった問題

</br>
</br>

![　レートリミット](https://raw.github.com/wiki/ys1227/link_discord/images/quita08.png) 

</br>

**1. チャットルームを退室する。
2. その後、投稿作成画面に遷移する。
3. 新たな投稿を作成する。
4. コネクションが切断されずに残ってしまい、投稿内容がチャットルームに配信される。**
</br>
</br>


問題の原因を調査したところ、以下の2点が明らかになりました。

### 問題の原因
**1. Enterキーの検知がチャットルームに限定されていなかった
2. コネクションが切断されずに残っていたため、意図しない配信が発生していた**

### 問題の解決

原因を調査するにあたって、ActionCable実装時に生成していたオブジェクトなどの中身のソースコードを確認してみると以下のようになっていました。

```JavaScript
class Consumer {
    constructor(url) {
      this._url = url;
      this.subscriptions = new Subscriptions(this);
      this.connection = new Connection(this);
      this.subprotocols = [];
    }
```

なんとなくRailsガイドを通じて概念を理解したつもりでいましたが、実際のソースコードを確認してみることでConsumerオブジェクトの生成や通信接続のプロセスについて具体的なイメージを持つように努めました。
</br>

https://github.com/rails/rails/blob/3d132b855541c35236e93b3a676a3ee80d02a38a/actioncable/app/assets/javascripts/actioncable.js#L442

</br>
</br>

基礎知識を頭に入れ、以下のように、
**1.Enterキーの検知時にチャットルーム内にいるかどうかの条件分岐を追加**
**2.コネクションを切断する処理を実装すること**で、問題を解消することができました。
</br>
</br>

```JavaScript:変更後のコード
const input = document.getElementById('post_input');

if(input) {
  input.addEventListener("keypress", function(e) {
    if (e.key=== 'Enter') {
      appRoom.speak(e.target.value);
      e.target.value = '';
      e.preventDefault();
    }
  });
} else {
  console.error("Could not find element with id 'post_input'");
}
```

```JavaScript:変更後のコード
document.addEventListener("turbo:before-visit", () => {
  const tag = document.getElementById('messages');
  if ( appRoom !== null && tag !== null ) {
      consumer.subscriptions.remove(Room);
  };
});

history.replaceState(null, null, null);
window.addEventListener('popstate', function(e) {
  const tag = document.getElementById('messages');
  if ( appRoom !== null && tag !== null ) {
    consumer.subscriptions.remove(Room);
};
});
```


今回は認証機能を外部サービスに委ねていましたが、自サーバーで認証処理を行っていた場合、同様の問題が生じていた可能性が高いです。
この経験から、各オブジェクトの役割やシステムの概念を深く理解してから実装を進めることの重要性を学ぶことができました...。


## 🚀使用技術🚀

### フロントエンド
* JavaScript
* Tailwind CSS
* daisyUI

### バックエンド
* Ruby 3.1.4
* Ruby on Rails 7.0.8
* Action Cable
* Active Job
* Sidekiq
* Sidekiq-Cron
* Redis

### 外部API
* Discord API

### インフラ
* Render

### データベース
* PostgreSQL
* Redis(バックグラウンド処理とActionCableのアダプタに使用)

### 開発環境
* Docker


## 🚀ER図🚀
![ER図](https://raw.github.com/wiki/ys1227/link_discord/images/db.svg)

<br />


## 🚀インフラ構成図🚀

![インフラ構成図](https://raw.github.com/wiki/ys1227/link_discord/images/structure02.drawio.png)

<br />
<br />



## 🚀スケジュール🚀
スクール内でのMVPリリースまでが約1ヶ月半、本リリースまでが約1ヶ月となっています。
開発時間は平均して7hから8hくらいです。

### ①企画・設計(2023年10月中旬〜10月末)
カリキュラムと並行しながらアイデア固め、ER図作成、画面遷移図の作成などに取り掛かかりました。

### ②MVPリリース(2023年11月〜12月12日)
ゲストログイン機能とチャット機能以外の全ての機能を実装しました。

### ③本リリース(2023年12月12日〜2月5日)
独自ドメイン取得、記事作成、Gogoleアナリティクスの追加などをメインで行いました。
またゲストログイン機能とチャット機能を実装しました。
またアルゴリズムやセキュリティに少し興味があったため、このタイミングでアプリ開発と並行して勉強してしまい、少し期間が長くなってしまいました。

## 🚀今後について🚀

今後着手していきたいことは以下の通りです。

**1. 投票してくれる人を増やせるような仕組みづくりの追加**
投稿して募集してくださる方はありがたいことにいるのですが、
投稿に対して投票をしてくれる方が少ない現状となっています。
自分がサービスを使用していて感じたこととしては、募集者の募集時間と自分の参加可能な時間帯が合わず投票できないということが多かったです。
そのため、現在は募集時間の登録を最大で第三希望まで、とユーザに登録する数を委ねていますが、最低限募集時間は２つ以上登録してもらうなどをして、マッチングする可能性を上げていけるように改善したいと思います。

**2. テストコードの追加**
現状テストコードが書けていないので追加していきたいです。

**3. リファクタリング**
コントローラに処理が固まってしまっていたり、まとめられる処理をまとめられていなかったりするので進めていきたいです。
またかなり汚いコードになってしまっている部分も多いので順次改善していきたいと考えています。

**4.SPAヘ対応**
現在はerbでフロントを記述していますが、ユーザー体験を上げるためにReactなどを使ってSPAに対応させていくことを考えています。


## 終わりに
 
かなり長くなってしまいましたが、読んでくださった皆様ありがとうございました。
また、企画や実装で困ったときに、相談にのってくださった講師や受講生の方や、サービスを使ってくれた皆様にこの場をお借りしてお礼を言いたいです。
本当にありがとうございました🙇🏻‍♀️🙇🏻‍♀️

企画、設計、実装などの一連のサービスのリリースを通じ、自分が実力不足であることを痛感するとともに、自分のアイデアが形になっていくことがすごく楽しく、開発を楽しみながら進めることができました。
これからも色々なことを学びより良いサービスを作ることができるように頑張っていこうと思います！！