【個人開発】他人が書いた小説の続きを書けるWebサービスを作りました

# はじめに
本業バックエンドエンジニアが、フロントエンドからインフラまで一貫して知見を得るため、個人開発に挑戦しました。
その時に使用した技術や考えたことを記します。

# 作ったもの
突然ですが、2次創作はお好きですか？
- 公式の展開に満足できずにモヤモヤしたり
- 好きな作品の「What If」を創作したい

こういった経験ありますよね？
そんな問題を解決するために
NOVEL VERSE というWebサービスを作りました。

https://novelverse.jp

NOVEL VERSEは誰もが気軽に小説を書く事ができるWebサービスです。
当サービスに投稿された小説は、会員であればその物語の世界観や登場人物、時代背景などを引き継いでその物語の続きを書くことができます。
物語の展開、登場人物の行く末など、誰もが好きなように運命を決めて執筆する事ができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/964196/0ff637d7-a4f5-33bf-b041-4762ead9cccc.png)

## 主な機能
### 小説投稿・編集・削除機能
あらすじ・小説本文を入力するtextareaは改行すると自動で広がります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/964196/8877dde8-b2d3-af19-4089-9e6654f00296.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/964196/b3a36fc4-6603-5ff3-b3f6-5d325b47f266.png)

### 小説検索・フィルタリング機能
ただのLIKE検索です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/964196/045a6e38-9749-9827-8e11-133b459f3021.png)

### いいね・Twitter共有機能
Twitter公式のシェアリンクです

https://publish.twitter.com/#

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/964196/978546d5-b63d-0f80-ecf0-bd98ae26dbc2.png)

### いいね・投稿した小説確認機能
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/964196/a8375118-d524-d1fd-d5dc-ce02b37b7cf9.png)

# アプリケーション
### フロントエンド
- Laravelのbladeテンプレート＆Vue.jsのハイブリッド
基本はbladeテンプレート
小説リーダーはVue.jsのSPA
- CSSフレームワークはBootstrap
- アイコン類はFontAwesomeのFREE枠のみ使用

https://fontawesome.com/search?m=free&s=solid%2Cbrands

### バックエンド
- バックエンド＆APIはLaravelで実装
- ログイン周りの機能はLaravelの標準Authenticationを少し改造して使用

https://reffect.co.jp/laravel/laravel-authentification-by-code-base

# データベース設計
アプリケーションの特性上ツリー構造のデータベースを設計する必要がありました。
RDBでツリー構造のデータを持つ方法については下記の記事を参考にさせていただきました。

https://www.wantedly.com/companies/tutorial/post_articles/299434

結果的に使用した手法は
- 隣接リストモデル
- 閉包テーブルモデル
上記2つのハイブリッド構成にしました。

### ツリー構造DBのイメージ
例えば、下記のような階層の場合
 > エピソード1
 > ┣ エピソード2  
 > ┃  ┗ エピソード3  
 > ┗ エピソード4  
 >　 ┗ エピソード5  

#### エピソードテーブル(隣接リストモデル)
レコードは親IDのみ所持します
|エピソードID|親エピソードID|話数|小説本文|
|:--:|:--:|:--:|:--:|
|1|0|0|...|
|2|1|1|...|
|3|2|2|...|
|4|1|1|...|
|5|4|2|...|

#### 階層管理テーブル(閉包テーブルモデル)
１つのIDが辿ったIDを全て保存します
話数が増えるほどデータが膨大になってしまいます
|エピソードID|辿ったエピソードID|話数|
|:--:|:--:|:--:|
|2|1|1|
|3|1|1|
|3|2|2|
|4|1|1|
|5|1|1|
|5|4|2|

上記のようなイメージです。
このようにすることで、データをエピソードテーブルに保存、
階層管理テーブルを使うことでツリー階層全体の親子関係を管理できるようになります。

# インフラ
コンテナをフル活用したインフラ構成です。
![novelverse-archtecture.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/964196/749d1ea4-b6de-5e60-a929-89e9bc71e3c7.png)
- ホストOSは1台(Amazon Light Sail)
- Docker Composeでコンテナを管理
- ロードバランサ―にtraefikを使用
traefikに関する記事を書いているので、もし気になる方はぜひ見てください。

https://qiita.com/hdk-t/items/e471665a062d516e7d92

- アプリケーションサーバーはUbuntuをベースにした、NginxとPHP-FPM構成の自作Docker Imageを使用
[ubuntu-laravel8 - Docker Hub](https://hub.docker.com/r/thideki/ubuntu-laravel8)
- データベースのMySQLはレプリケーションさせた冗長構成
- コールドスタンバイのNginxを起動すると、全てのアクセスがそのNginxにプロキシされる
一時的にサービスを停止する際のアクセスの逃がし場所にするため   

# 今後やりたい事
### 機能要件
- SEO対策
- 投稿時にTwitterに自動で投稿される機能
- イラスト(画像)アップロード機能
- GoogleAddSenseの導入
- カドカ◯様と提携して、有名ラノベや有名アニメの2次創作作品の掲載を許可してもらう(100%ムリ)

### 非機能要件
- ~~GitHubActionsを使用したCI/CDの導入~~
    ↑ 導入済み

- リファクタリング&テストコードの整理
- コンテナ実行基盤をAWS Fargateへ
- 検索用のElasticSearchの導入

# さいごに
- サーバーもアプリケーションもプロトタイプレベルなので、もしユーザーが増えてきたらマルっとリニューアルする必要がありそうです。
- 最後まで見ていただきありがとうございました。