【個人開発】プレゼントして欲しいものを謎解きで伝えることができるアプリを開発しました

## はじめに
はじめまして、[sami](https://twitter.com/sammy_study)と申します。
未経験からWebエンジニアへの転職を目指して、プログラミングスクールで学習しています。
この度、プレゼントして欲しいものを謎解きで伝えることができるWebアプリ「ギフトリクエスト」をリリースいたしました。
## サービス名
[**ギフトリクエスト**](https://www.gift-request.com/)

ゲストログイン機能を実装したので、会員登録せずにお試しいただくこともできます。

[![Image from Gyazo](https://i.gyazo.com/eb785f4709dbf556bd5bdc3a226c196e.png)](https://www.gift-request.com/)

**サービスURL**

https://www.gift-request.com/

**Github**

https://github.com/sami-0085/gift_request

## サービス概要
プレゼントして欲しいものがあるけど直接リクエストするのは言いづらいという方に。
自分がプレゼントして欲しいものを登録すると、それを元にAIが謎解きを生成します。
相手に謎を解いてもらうことで欲しいものを伝えることができる、プレゼント伝達アプリです。

## このサービスへの思い・作りたい理由
誕生日やクリスマス、ちょっとしたお礼として、プレゼントを貰う機会は意外と多くあります。
また相手に送るものも、毎年となるとネタも尽き、ワクワクとしていたはずのプレゼント探しがいつしか悩みの種になっていました。
重複したものを渡してしまったり貰ったりしたことも。
しかし、直接欲しいものを言うのは気恥ずかしいという気持ちもあります。
ギフトリクエストは、こういったミスマッチやマンネリ化を解消し、楽しくプレゼント交換ができたらと思い考えました。

## サービスの差別化ポイント
アプリに自分の欲しいものを登録しておき、ギフトを贈る人は、そのアプリ内で購入までできるアプリはありますが、プレゼントして欲しいものを直接知らせるのではなく、謎解きにしたことが最大の違いです。
プレゼント選びという手間と時間のかかる作業を、謎解きの面白さと組み合わせることで（閃いた時や謎が解けた時の嬉しさ、非日常感）その作業までも楽しめるような仕組みにしています。

## 機能紹介
- 🔶がついているものは、ログイン限定機能です。<br>
- ギフトリクエストでは、作成した謎解きのことを「Re:QUEST」と呼びます。

|トップ画面| ログイン画面 |
|:-:|:-:|
|[![Image from Gyazo](https://i.gyazo.com/6bb75cbe7f80c4957e1b38c30768a803.gif)](https://gyazo.com/6bb75cbe7f80c4957e1b38c30768a803)|[![Image from Gyazo](https://i.gyazo.com/8f07278a6c5150649334d51a9e6f5975.jpg)](https://gyazo.com/8f07278a6c5150649334d51a9e6f5975)|
|メイン機能が一目でわかるようにし、すぐに機能がを使えるような動線にしました。？マークを押すと、詳細な説明を見ることができます。|多様なログイン方法を用意しました。ログインが必要な機能を触った時にログイン画面へ誘導するようにし、ログインするハードルを低くしました。|

|みんなのRe:QUEST一覧| Re:QUEST挑戦 |
|:-:|:-:|
|[![Image from Gyazo](https://i.gyazo.com/977bea1268b7facff64403e22c7ef80e.gif)](https://gyazo.com/977bea1268b7facff64403e22c7ef80e)|[![Image from Gyazo](https://i.gyazo.com/d4b107c743133e555a653d615356d467.gif)](https://gyazo.com/d4b107c743133e555a653d615356d467)|
|すべてのRe:QUESTを見ることができます。タイトルを選ぶと、謎解きに挑戦できます。タグを選ぶと、同じタグをつけたものを一覧で見ることがきます。|謎解きに挑戦できます。XやLINEで共有すると、挑戦したRe:QUESTのリンクに設定されます。|

| 🔶 Re:QUEST作成| 🔶 Re:QUEST詳細 |
|:-:|:-:|
|[![Image from Gyazo](https://i.gyazo.com/3b411c093c4980b568d0a936a1418c6d.gif)](https://gyazo.com/3b411c093c4980b568d0a936a1418c6d)|[![Image from Gyazo](https://i.gyazo.com/907906068eb40271f8108324830a2780.gif)](https://gyazo.com/907906068eb40271f8108324830a2780)|
|欲しいものをもとにOpenAIでRe:QUESTを作成します。作成中と分かるようにモーダルが表示されます。1日5つまで作成できます。|Re:QUESTの詳細内容を見ることができます。気になった箇所は、その場で編集できます。XやLINEで共有すると、作成したRe:QUEST挑戦画面へのリンクが設定されます。|

|🔶 フォロー機能|🔶 挑戦したRe:QUESTの背景色|
|:-:|:-:|
|[![Image from Gyazo](https://i.gyazo.com/cc658be33528419a1293a3825449cae2.gif)](https://gyazo.com/cc658be33528419a1293a3825449cae2)|[![Image from Gyazo](https://i.gyazo.com/0854ac65276e840e0e96562235eff90a.png)](https://gyazo.com/0854ac65276e840e0e96562235eff90a)|
|他ユーザーをフォローできます。フォロー、フォロワーを一覧で確認できます。|正解したRe:QUESTは背景が自動変更されます。挑戦したかどうか一目でわかります。また正解した時の難易度で色が変わります(難しい→ピンク、簡単→黄色)|

## 技術構成
### 使用技術
|カテゴリ|技術|
|:-------------|:------------|
|フロントエンド|JavaScript, TailwindCSS, Rails 7.0.8(Hotwire/Turbo)|
|バックエンド|Ruby 3.2.2, Rails 7.0.8|
|データベース|PostgreSQL|
|環境構築|Docker|
|インフラ|Render|
|Web API| Open AI API(GPT-4), LINE Login, Google Sign-In |

### 画面遷移図
[画面遷移図](https://www.figma.com/file/gEFMyEx0QNwnjENitaVhOm/gift_request?type=design&node-id=0%3A1&mode=design&t=lq3JcScmWoDpdYl2-1)

### ER図
```mermaid
erDiagram
    Users {
        int user_id PK
        string name
        string email
        string crypted_password
        string salt
        string avatar
        date birth_date
        string self_introduction
        datetime created_at
        datetime updated_at
    }

    Requests {
        int id PK
        int user_id FK
        string name
        datetime desired_date
        text remarks
        int style
        string title
        text quest
        datetime created_at
        datetime updated_at
    }

    QuestCompletions {
        int id PK
        int user_id FK
        int request_id FK
        int difficulty_level
        datetime created_at
        datetime updated_at
    }

    Choices {
        int id PK
        int request_id FK
        int number
        text content
        datetime created_at
        datetime updated_at
    }

    Hints {
        int id PK
        int request_id FK
        int number
        text content
        datetime created_at
        datetime updated_at
    }

    Relationships {
        int follower_id
        int followed_id
        datetime created_at
        datetime updated_at
    }

    Tags {
        int tag_id PK
        string name
        datetime created_at
        datetime updated_at
        int taggings_count
    }

    Taggings {
        int taggable_id
        string taggable_type
        int tagger_id
        string tagger_type
        string context
        int tag_id FK
        datetime created_at
    }

    Users ||--o{ Requests : "1:n"
    Users ||--o{ QuestCompletions : "1:n"
    Users ||--o{ Relationships : "1:n"
    Requests ||--o{ QuestCompletions : "1:n"
    Requests ||--o| Choices : "1:n"
    Requests ||--o| Hints : "1:n"
    Requests ||--o{ Taggings : "1:n"
    Tags ||--o{ Taggings : "1:n"

```

## こだわった点
### 謎解きの内容
謎解きは、正解後に達成感を味わえるようにストーリー仕立てで考えました。ストーリーとその背景はChat-GPTで作りました。ユーザー毎に変化するのは、作中登場する「詩」「手がかり」「選択肢」です。挑戦者には「詩」の中に隠されたユーザーの欲しいものを読み解いて貰います。

|Story1| Story2・3 | Epilogue |
|:-:|:-:|:-:|
|[![Image from Gyazo](https://i.gyazo.com/32509395c3848c38b53a9cff09a95d88.webp)](https://gyazo.com/32509395c3848c38b53a9cff09a95d88)|[![Image from Gyazo](https://i.gyazo.com/ac06a93219a954a571ef53592a47c2c9.webp)](https://gyazo.com/ac06a93219a954a571ef53592a47c2c9)|[![Image from Gyazo](https://i.gyazo.com/6dde0cea67028bc1fd899fb8975aa9e6.webp)](https://gyazo.com/6dde0cea67028bc1fd899fb8975aa9e6)|
|ノートが落ちる|ノートを読む|ノートを戻す|

「詩」「手がかり」「選択肢」をAIで生成する際は、以下のポイントでプロンプトを考えました。また、生成したいデータは一度に出力し、抽出して各カラムに保存しています。
- トークン数を最小限にし費用を抑えること
- 欲しい情報が正確に得られること

<details><summary>実際のコード（一部抜粋）</summary>

```ruby:app/services/openai_quest_generation_service.rb
def call
    client = OpenAI::Client.new(access_token: @api_key)

      response = client.chat(
        parameters: {
          model: "gpt-4",
          messages: [
            { role: "system", content: "You are a professional lyricist." },
            { role: "user", content: generate_prompt(@query) }
          ],
        }
      )


private

  def generate_prompt(query)
    "Please create lyrics based on the following conditions.

    # Conditions
    Theme: #{query}
    Do not use the theme word #{query} in the lyrics.
    Taste: longing
    Plese answer in Japanese.
    Output should be less than 300 tokens

    # Required Output
    - タイトル: Please provide a title within 20 characters, enclosed in quotation marks (「」).
    - 歌詞: Write the lyrics within 300 characters.
    - テーマのヒント: Provide 3 hints related to the theme, each within 30 characters, formatted as follows:
      - ヒント1:
      - ヒント2:
      - ヒント3:
    - Theme choices: Provide 2 words  associated with #{query} without explicitly including the word #{query} itself. formatted as follows:
      - 選択肢1:
      - 選択肢2:"
  end
  ```

  ```app/services/create_quest_service.rb
  def process_response(response)
    # 抽出したデータを保存
    match_quest = response.match(/歌詞:([\s\S]+?)- テーマのヒント:/)&.[](1)
    match_title = response.match(/タイトル:(.+?)\n/)&.[](1)
    hints = extract_hints(response)
    match_choices = extract_choices(response)
    # 抽出したデータがすべて存在するかチェック
    if match_quest.blank? || match_title.blank? || hints.values.any?(&:blank?) || match_choices.values.any?(&:blank?)
      return false # データが不完全な場合は失敗を返す
    else
      ActiveRecord::Base.transaction do
        # 抽出したデータを@user_requestに保存
        @user_request.quest = match_quest.strip
        @user_request.title = match_title.strip
        @user_request.save! #save! を使用して明示的に例外を発生
        save_hints(hints)
        save_choices(match_choices)
      end
      true # 保存が成功した場合、trueを返す
    end
  end

  def extract_hints(response)
    {
      1 => response.match(/ヒント1:(.+?)\n/)&.[](1)&.strip,
      2 => response.match(/ヒント2:(.+?)\n/)&.[](1)&.strip,
      3 => response.match(/ヒント3:(.+?)\n/)&.[](1)&.strip
    }
  end

  def extract_choices(response)
    {
      1 => response.match(/選択肢1:(.+?)\n/)&.[](1)&.strip,
      2 => response.match(/選択肢2:(.+)/m)&.[](1)&.strip
    }
  end

  def save_hints(hints)
    hints.each do |number, content|
      @user_request.hints.create(number: number, content: content) if content
    end
  end

  def save_choices(choices)
    choices.each do |number, content|
      @user_request.choices.create(number: number, content: content) if content
    end
  end
  ```

  </details>

  ### ユーザビリティ
  初めてアプリを使うユーザーにも、どのようなアプリかわかるようなイメージ画像、背景色にしました。
  - イメージ画像
  ポップな3Dイラストで、楽しいゲームのような世界観を表現しました
  - 背景色
  謎解きをイメージして（思考が交錯し徐々に統合されていく様子を）グラデーションで表現しました
   <img src="https://i.gyazo.com/418b5db9d1b4f1911bac4b9b7d6d49fe.png" width="50%">
 
  また、直感的に使ってもらえるように、以下の工夫をしました。
  - **多様なログイン機能**
  MVPリリース時、ログインが面倒だというご意見を頂きました。
  そのため、ゲスト、Google、LINEと多様なログイン方法を用意しました。
  また、ログインが必要になった時ログインページへ誘導するようにし、ログインへのハードルをできる限り感じない工夫をしました。
  　<img src="https://i.gyazo.com/8f07278a6c5150649334d51a9e6f5975.jpg" width="50%">
  
  - **各画面にヘルプボタンを設置**
  画面には簡潔な表示を心がけ、知りたい時だけ詳細をモーダルで表示させるようにしました。また、作業工程をステップに分け、分かりやすい説明を心がけました。
  
　　<img src="https://i.gyazo.com/f3c99f0294afbc2cf6a6f93db02ff7cb.gif" width="50%">

  - **前ページに簡単に戻れるパンくず機能**
  どのページからリンクされたのかわかるようにしました。
  
    <img src="https://i.gyazo.com/c20dc0f5bd88cfaedfabb1379a1b231b.gif" width="50%">

## 今後の開発について
さらなるユーザビリティ向上に向け、フォローしている相手がRe:QUESTを作成したら通知が来るなど、LINE通知機能を付けたいと考えています。
また、より分かりやすいコードにするためリファクタリングを行っていきたいです。

## おわりに
一人で１からアプリを作り上げるのは今回が初めてで、何をするにも壁にぶつかり、山を登っても登っても、また高い山が聳え立っているような感覚でしたが、一つ山を超える度に少しずつ成長しているのを感じ、機能が無事動いてくれた時はとても嬉しかったです。

無事リリースできたのは、的確なアドバイスをくださる講師の方々、困っていた時相談に乗ってくれた同期、先輩、後輩、そしてアプリを触ってくださった皆様のおかげです！心から感謝を申し上げます。
まだまだ未熟な部分が多いので、これからも学習を続け、もっとユーザーに寄り添ったサービスを提供できるよう努力します！

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