【個人開発】身近な人への孝行を投稿するサービスを作りました。

# はじめに
はじめまして、hibi_toshi([@hibi_toshi](https://twitter.com/hibi_toshi))と申します。

WEBエンジニアへの転職を目指して学習しています。
今回はポートフォリオとしてサービスを開発しましたので、それについてアウトプットします。
コンセプトは **「孝行をもっと身近に、手軽に、カジュアルに。」** です。

## 作成したサービス
**サービス名: [Cocoaer](https://web.cocoaer.net/) (Chromeのみ対応しています。)**
![Cocoaer_logo.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2918286/bf70f6bc-e6c3-c662-6982-a497794fcfa2.jpeg)

サービスURL: https://web.cocoaer.net/
GitHub URL: https://github.com/hibi6toshi/cocoaer

# サービスについて
## 開発の背景

父親の定年祝いに何しようと**親孝行について検索したとき、**「一緒に旅行にいく」や「プレゼントを渡す」といいった一般的なものを載せているサイトはたくさん見つかりましたが、**その具体的な例を探すのに苦労しました。**

また実際に実施しようとした場合、**「どういう準備をすればいいか？」「どういう調べ方をすればより良いものが見つけられるか？」「費用はどれくらいだろうか？」** といったことについても自分で調べる必要がありました。

そこで、自分が行った孝行を投稿するサービスがあれば、**同じように孝行をしようと思っている人の助け**になるのではないかと考えました。
それにより、より多くの人が孝行を行うようになり、**世の中が少し温かくなる**のではないかと考えました。

## 主な機能
### 孝行の投稿機能
| 表示  | 投稿/編集 |
| --- | --- |
| ![article_show.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2918286/5ae91b6d-d281-7082-7dbc-9d50d5265508.gif) |![article_create.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2918286/1bdd49c3-9778-4dbc-91d5-daf387c60142.gif)|
| 投稿された孝行を表示できます。 <br> コメント・お気に入り登録できます。  | 孝行を投稿できます。 <br>サムネイル画像を指定できます。<br>ターゲット・カテゴリ・日数・費用を設定できます。  |

### プロジェクトの表示・投稿

| 表示  | 投稿/編集 |
| --- | --- |
| ![project_show.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2918286/4ecbc07c-97b7-5eba-c3b6-196a9705bd36.gif)| ![project_create.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2918286/9b9aa0bc-8158-f1e0-4668-165d1f11f200.gif)|
| 投稿されたプロジェクトを表示できます。 <br> お気に入り登録できます。  | プロジェクトを投稿できます。<br>ターゲット・カテゴリ・目標日・費用を設定できます。<br>タスク・アクションを複数登録できます。 |

### フォーラムの表示・投稿

| 表示  | 投稿/編集 |
| --- | --- |
| ![forum_show.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2918286/8288a461-919c-e9ea-b5a6-8b4b77c59d0e.gif)| ![forum_create.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2918286/744872ee-3aef-ef92-afe1-0e3529184d9f.gif)|
| 投稿されたフォーラムを表示できます。 <br> コメント・お気に入り登録できます。  | フォーラムを投稿できます。<br>ターゲット・カテゴリ・日数・費用を設定できます。 |

## 工夫した点

### 1. サービス機能とハードル

#### 投稿機能とそのハードル 
このサービスを作ろうと企画したとき、最初の機能としては投稿機能がすぐに思い付きました。
これは文字通り、自分が行った孝行を投稿して、シェアするものです。

確かにこの機能は私のやりたいことを叶えてくれるものです。
しかし、**いきなり投稿をするというのはかなりハードルの高いもののように感じられます。**
そもそも**投稿するためには孝行を実施する必要があり、それすらもハードルが高いのではないでしょうか？**
![only_share.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2918286/8bf94f58-4561-eb5d-ce9b-46bb553e45c6.png)



そのために2つの機能を考えました。

#### 小さいハードルを用意する

機能の1つめは**プロジェクト機能** です。
タスクとアクションを記録する機能です。
ある程度やることが決まっていないと使えませんが、実施する際の記録となり、他の人により役立つのではないかともいます。
![plan_and_share.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2918286/4f42e7cf-2d3b-046b-516d-6f3b4111bfc0.png)

機能の2つめは**フォーラム機能** です。
これは文字通り、トピックを指定してそのことについてユーザー他のユーザーの意見を聞ける機能です。
これは孝行しようかなといった段階でも使え、またこの時点では実際に行動に移す必要もないため（ある種の言ったもの勝ちみたいな感じですね。）ハードルは低いと思います。
![form.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2918286/e8984f53-90bb-cd31-f9f4-684166927bc2.png)

またプロジェクト機能・フォーラム機能を持つことで、
**フォーラムでやることを決める　　
→ プロジェクトで実際にタスク管理・やったことを記録 
→ 実施して投稿し、シェア**

と言った分解された**一連のフローを同じサービス内でユーザーに提供できる**ようになりました。
![hop_step_jump.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2918286/34ad7c01-d5fb-c2bf-b916-e824fc40a822.png)

### 2. マスタデータの取得
カテゴリ・ターゲットのマスタデータはフロントでは持たず、DBに保存してあるデータを初回レンダリング時に取得、useContextで全体に提供しています。
これにより、フロント/バックエンドでデグれることのないようにしています。
```PietyCategoryProvider.tsx
const PietyCategoryContext = createContext<{}>({});
export const usePietyCategoryContext = () => useContext(PietyCategoryContext);

const PietyCategoryProvider: React.FC<Props> = ({ children }) => {
  const [pietyCategorys, setPietyCategorys] = useState<PietyCategoryDict>({});

  useEffect(() => {
    const fetchData = async () =>{
      const pietyCategoryDatas = await getPietyCategorys();
      pietyCategoryDatas.forEach((pietyCategoryData: PietyCategory) =>{
        setPietyCategorys(( prevDic => ({...prevDic, [pietyCategoryData.id] : pietyCategoryData.name} )))
      })
    };
    fetchData();
  }, []);

  if (pietyCategorys === null) return <div><Loading /></div>; 

  return (
    <PietyCategoryContext.Provider value={pietyCategorys}>
      {children}
    </PietyCategoryContext.Provider>
  );
};

export default PietyCategoryProvider;
```

またcontextの使用方法はcustom hookに切り出し、共通化しています。

```useCategorys.ts
const useCategorys = () => {
  const pietyCategorys = usePietyCategoryContext() as PietyCategoryDict;

  const getCategoryName = (id: number) =>{
    return pietyCategorys[id];
  };

  const getPietyCategorysDict = () :Option[] => {
    let categoryOptions: Option[] = [];
    Object.keys(pietyCategorys).forEach(key => {
      categoryOptions.push({value: key, label: pietyCategorys[key]})
    });
    return categoryOptions;
  };

  return {
    getCategoryName,
    getPietyCategorysDict
  }

}
 
export default useCategorys;
```

### 3. ポリモーフィックの利用
コメント機能・お気に入り機能を実装しており、Rails側でポリモーフィック関連にすることで、投稿・プロジェクト・フォーラムでそれぞれに対して使えます。

Railsのコントローラーでは2種類の対応の仕方をしています。
1つ目は「何に対してのものか」でエンドポイントを変える方法です。
個人的にはこちらの方が、安全でかつわかりやすいのでいいかなと思いました。
処理内容が同じなら、共通部分をモジュールに切り出し、差分をそれぞれのコントローラーで埋めれば簡単に実装できるので、、、

```routes.rb
  namespace :api do
    namespace :v1 do
      resources :articles, only: %i[index show create edit update destroy] do
        resources :comments, only: %i[index create update destroy], module: :articles
      end
      end
      resources :forums, only: %i[index show create edit update destroy] do
        resources :comments, only: %i[index create update destroy], module: :forums
      end
    end
  end
```

```articles/comments_controller.rb
class Api::V1::Articles::CommentsController < SecuredController
  include Api::Commentable

  skip_before_action :authorize_request, only: %i[index]

  private

  # 差分は「何に対してコメントするか」だったので、それはコントローラーで捌くようする。
  # それによりほとんどを共通化でき、共通部分はモジュールに切り出すことができる。
  def set_commentable
    @commentable = Article.find(params[:article_id])
  end
end

```

２つ目はエンドポイントは一律で、パラメータに「何に対してのものか」を入れる方法です。
こちらの場合は処理が共通していれば単純になりますが、
- 共通処理が少ない場合、複雑になる。
- パラメータの検証をする必要がある。（以下なら、params[:favoritable_type]）
- 共通処理がほとんどならば、１のようにモジュールを使うことで個々のコントローラーは簡単に実装できる。
- RESTぽくない（気がする）
なので、使う利点が少ない方法かなと思いました。

```routes.rb
  namespace :api do
    namespace :v1 do
      resources :favorites, only: %i[index create destroy]
    end
  end
```

```favorites_controller.rb
class Api::V1::FavoritesController < SecuredController
  before_action :valid_param?, :set_favoritable, only: %i[create destroy]

  def create
    @current_user.favorite(@favoritable)

    render json: {
      data: @favoritable.reload.as_json(include: [{ favorited_by_users: { only: [:id, :avatar] } }], methods: :favorited_by_user_ids)
    }
  end

  private

  def valid_param?
    render_404 unless Favorite::FAVORITABLE_TYPES.include?(params[:favoritable_type])
  end

  def set_favoritable
    @favoritable = params[:favoritable_type].constantize.includes(:favorited_by_users).find(params[:favoritable_id])
  end
end

```

合わせてフロント側でもfavoriteについてはcustom hookに一連の処理を切り出して共通化しています。

```useFavorites.tsx
const useFavorites = ({
  initFavoritedUserIds,
  favoritableType,
  favoritableId
}: UseFavoritesProps) => {

  const { user } = useUser();
  const [ favoritedUserIds, setFavoritedUserIds ] = useState(initFavoritedUserIds);
  const { getAccessTokenSilently } = useAuth0();

  const isFavorited = useMemo(()=>(
    favoritedUserIds.includes(Number(user?.id))
  ),[user, favoritedUserIds]);
  
  const toggleFavorite = useCallback(async() => {

    if(!user){
      return ;
    }

    const token = await getAccessTokenSilently();

    if(isFavorited){
      return deleteFavorite(token, favoritableType, favoritableId)
              .then((res) => {
                      setFavoritedUserIds(res.data.data.favorited_by_user_ids);
                      return res;
                    })
              .catch((e: any) => {throw e})
    }else{
      return createFavorite(token, favoritableType, favoritableId)
              .then((res) => {
                      setFavoritedUserIds(res.data.data.favorited_by_user_ids);
                      return res;
                    })
              .catch((e: any) => {throw e})
    }
  }, [user, favoritedUserIds])

  return {
    favoritedUserIds,
    isFavorited,
    setFavoritedUserIds,
    toggleFavorite
  };
}
 
export default useFavorites;
```

## 主な使用技術

**バックエンド**
Ruby 3.1.3
Rails 7.0.4

**フロントエンド**
React 18.2.0
TypeScript 4.9.5

**インフラ**
AWS(S3, CloudFront, ECS)

**認証**
Auth0

# 開発を通して得られた経験
### サービスを考えるは楽しい&開発は楽しい
「自分の考えをコードを通してサービスという形で実現する。」
これは非常にやりがいがあり、楽しいものです！
「どういったサービスを作るか」「どのような機能をつけるか」と言ったものから、「モデル設計はどのようにするか」「処理はどこでどう持たせるか」と言ったことまで、自分で決められるのは少し辛い面もありますが、やりがいや得られるものが多く、楽しいものでした。

また普段利用するサービスへの見え方が変わリます。
このサービスは何が魅力的なのか、この画面の動きをさせたかったらどのように実装したらいいだろうか、どういうようなモデリングをしているんだろうかという観点で見ることが増えました。

### 自信になる
１から設計、実装、デプロイできたのは率直に自信になりました。
また、Reactに関しては独学でキャッチアップできたので、自分でもやればできるんだなと思いました。

### 自分の不足してる部分がわかる/学びが多い
サービスを１から作ることによって、自分の不足している部分がわかりました。
特に今回は、フロントにReactを使ったことで、JavaScriptの知識・考え方とTCPIPについてわからないことがたくさんあることに気づきました。

Railsでアプリを作ったことはありますが、そのときはTCPIP(主にHTTP)について意識することはありませんでした。
しかし、ReactからRailsにリクエストを送る際、いろいろなことを設定する必要がありました。
「Railsはいろんなことを簡単に実現させてくれてるんだな」と実感するとともにまだまだ学ぶべきものが沢山あるなぁと思いました。

# 最後に
ポートフォリオ作成を通じて、「自分はある程度はできるようになったけど、まだまだ学習して身につけることがあるな」と感じました。
学習を続けて力をつけながら転職活動に望みたいと思います。
時間ができたら、勉強の仕方や、個人開発の反省もアウトプットしたいと思います。

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