【個人開発】3つに厳選した自分の考えを送る「3選」を投稿するサービスを作りました

こんにちは、Qiita初投稿になります。
普段は仕事でWebサービスを開発していますが、今回勉強がてら初めて個人開発してみようということで、Webサービスを作ってみたのでご紹介させてください。
# どんなサービス？
## 背景
日々ネットを見ていると、
「料理で役立つ便利グッズ5選」
「今年を代表するおすすめ曲10選」
など、筆者が厳選した○選についてのサイトや動画を目にすることがあります。

そんなとき自分の場合、おすすめ曲やゲームに関する○選を見ると「自分だったら...」みたいなことを考えることがありました。大体はそこに付いているコメント機能で送れたりもできるのですがあまり目立たないので、もっと他者の考えも目立つ形にできないかと思いました。

なので考えたのは以下のこと。
・○選についてのトピックを誰でも作れるようにする
・他ユーザーの考えも集められるようにする

そして、5選でも10選でもいいのかというところで、見やすさ・投稿しやすさを考えた結果「3選」という枠組みに限定することと決めました。

というわけで3選を投稿するサービス、その名も **「SANSEN」** を作りました。
(あとは、みなさんトピック作成や3選投稿に「参戦」してほしいという意味もちょっと込めてます笑)

https://sansen-web.com

※PCで扱うことを前提としていますが、スマホからでも問題ないようにある程度UI調整しました

## 画面と機能の紹介
### トップ画面
![スクリーンショット 2022-09-04 23.48.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/679010/c0440588-9665-59e6-83eb-300da56fbd85.png)

### トピック作成と3選投稿
トピック作成画面
![スクリーンショット 2022-09-04 23.58.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/679010/b64e131e-e0d1-09b1-0970-62bbdd56c2ce.png)
3選投稿画面(理由・コメント部分は多少装飾ができます)
![スクリーンショット 2022-09-04 23.59.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/679010/fda8d0e2-a09a-8db2-435f-45dbb487b3a7.png)
作られたトピックは一覧として出ます
![スクリーンショット 2022-09-05 0.00.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/679010/da00f813-af72-91e3-0b6d-e11dd1ecfe09.png)
トピック詳細を見ると送られた3選投稿を見ることができます
![スクリーンショット 2022-09-05 0.01.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/679010/612f9cdd-dfb4-f950-5e16-d79f7d61295a.png)
メインとなる機能はもう本当にこの2つだけという感じです。
他にも **トピックのブックマーク** や **投稿へのいいね** 機能も備えています。
サービス内にも使い方・ヘルプ画面を設けているので、詳しくはそちらをご確認ください。

# 使用技術
## フロントエンド
- Nuxt.js 2.15.7
- Vuetify 2.5.5
- VeeValidate 3.4.13
- AWS Amplify 4.3.28
- Cloudinary(画像保存)

など
## バックエンド
- Django 4.0.4
- Python 3.10.3

など
## インフラ等
- AWS(Route53、Cognito、EC2、ECS、ECR、VPC、ACMなど)
- Heroku(APIデプロイ・DB) ※DBはMysqlです

## その他
- Github Projects(タスク管理)
- Google Cloud(Googleログイン)
- Google Analytics(アクセス解析)
- [ソコスト](https://soco-st.com/)(トップ画面のイラストに使わせていただきました)

# 苦労したところ3選
このサービスの特徴である「3選」にちなんで、苦労したところや時間がかかったところを3つあげたいと思います。
## 1. アイデア出し
まず一番初めとなるこの作業に時間がかかりました...。

初めての個人開発ということもあり、まず以下のようなことを考えました。
・収益化は考えない
・課金要素は加えない
・シンプルなもの(単純にGET/POST/PUT/DELETEするもの)
といったことをふまえて **投稿型のサービス** にすることと決めました。

あとは重要視したことに、 **挫折しないでリリースしきること** を掲げました。
そこでちょっとひねった投稿型サービスを考えた結果、3選トピックを作って投稿を送るものとなりました。

## 2. AWS CognitoとDjangoの連携
バックエンドのAPIをDjangoを用いて書いたのですが、トピックの作成や3選投稿などの処理はログインしていないと呼べないようにする必要がありました。(いわゆるパーミッション付与です)

認証基盤はAWS Cognitoで行っているため、そことDjangoを連携させるべく調べて、
[django-cognito-jwt](https://github.com/LabD/django-cognito-jwt)を見つけて使うこととしました。
```python:settings.py
COGNITO_AWS_REGION = env('COGNITO_AWS_REGION')
COGNITO_USER_POOL = env('COGNITO_USER_POOL')
COGNITO_AUDIENCE = env('COGNITO_AUDIENCE')

COGNITO_PUBLIC_KEYS_CACHING_ENABLED  =  True 
COGNITO_PUBLIC_KEYS_CACHING_TIMEOUT  =  60 * 60 * 24

COGNITO_USER_MODEL = 'users.User'

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'django_cognito_jwt.JSONWebTokenAuthentication',
    ],
}
```
settings.pyの内容は上記の通りで、cognitoの認証情報は環境変数で渡しています。
環境変数の設定には[django-environ](https://github.com/joke2k/django-environ)を用いています。

```python:users/models.py
class UserManager(BaseUserManager):
  
    use_in_migrations = True

    def get_or_create_for_cognito(self, payload):
        cognito_id = payload['sub']

        try:
            return self.get(cognito_id=cognito_id)
        except self.model.DoesNotExist:
            pass

        try:
            user = self.create(
                cognito_id=cognito_id,
                email=payload['email'],
                is_active=True)
        except IntegrityError:
            user = self.get(cognito_id=cognito_id)

        return user
```
先ほどのsettings.pyで`COGNITO_USER_MODEL = 'users.User'`と設定したので、users配下のmodels.pyには上記の記述を入れておきます。(そしてusersモデルには別途cognito_idのカラムが必要になります)

```python:core/permissions.py
from rest_framework import permissions

class CorePermission(permissions.IsAuthenticated):

    def has_permission(self, request, view):
        if view.action in ['hoge']:
            return request.user.is_authenticated
        elif view.action in ['fuga']:
            return True
        else:
            return False
```
このサービスでは基本的なAPIはcoreという名前のディレクトリ配下に置いているので、そこにpermissions.pyを作り、hogeとfugaという名前のメソッドがあるとしてこのような記述とします。こうすることで、

hoge→ログインしないと呼ぶことができない
fuga→ログインしなくても呼べる

というようなメソッドごとのパーミッションが設定できます。
メソッド名を書くところは配列になっているので、メソッドが増えればここに足していく仕組みです。
```python:core/views.py
class TopicViewSet(viewsets.ModelViewSet):
    serializer_class = TopicSerializer
    queryset = Topic.objects.all()
    permission_classes = (CorePermission,)
```
最後に、views.pyのviewsetごとに`permission_classes = (CorePermission,)`を書いてあげればその中のメソッド名とpermissions.pyのメソッド名を照らし合わせて処理してくれます。
## 3. AWS Fargateでのデプロイ
フロントエンド側のデプロイをAWS Fargateで行っています。
ここらへんは業務では行ってなかった部分なので、色々難航した部分でもありました。

まずはVPCからサブネット...
DockerイメージをECRにプッシュして...
EC2でロードバランサーを作成して...
ECSでタスク作成しクラスター、それからサービスの作成...

など、いろんなサービスに跨るのだなと実感しました。
あとはポートの設定値なども違っていたりしてそこも苦戦したところでした。
とりあえずデプロイはできましたが、まだ理解しきれてない部分もあるのでこれから学んでいこうと思っています。
# これからの課題
まずは大きな問題が...。
バックエンド側をHerokuでデプロイしているのですが、今話題になっている通り無料枠が今年の11月28日に終了予定です。ここをどうにか別のところに変えてからリリースしようか迷いましたが一旦Herokuのままリリースすることにしました。

:::note warn
なので **11月28日でサービスが休止** するかもしれないのは事前に告知しておきます...。
:::
他のサービス使うかバックエンドもAWSとするかこれから検討です。(AWSにしようとして失敗しました...)
あとはデザインを向上させたり、サービスのリファクタリングや追加機能のアイデアも練っていきたいと考えています。
# おわりに
それでは、ぜひSANSENを使っていただけたらと思います！
不具合等あればサービス内の「お問い合わせ」、または~~なんか張り切って作ってしまった~~[運営Twitter](https://twitter.com/sansen_offi03)までご連絡ください。
あと、技術的なことに関してどんなことをやってきたか、もう少し深く別記事として書くかもしれません。

最後まで見ていただきありがとうございました。