【個人開発】韓国コスメでスキンケアのお悩みを解決するサービスを開発しました【Next.js + Rails API】

## はじめに
初めまして！[りか](https://twitter.com/__rk2530)と申します。
未経験からのWebエンジニア転職を目指して、日々学習を行っております。

今回、韓国コスメのスキンケア情報サービス「KoreCare（コリケア）」を開発しました。

## サービス名
[KoreCare](https://korecare.jp/)
お試し機能を実装しましたので、ログインせずにお試しいただくこともできます。

![korecare_image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3467801/d4569a94-2097-fe7b-8b33-2daa7547fd02.png)

**▼サービスURL**
https://korecare.jp/

**▼GitHub URL**
https://github.com/rk0325/korecare

## サービス概要
韓国コスメに特化した、スキンケア情報サービスです。

具体的には、
- ユーザーの肌質やお悩みに合った韓国コスメを提案します。
- お肌の大敵である紫外線や乾燥から、ユーザーのお肌を守るサポートをします。
- スキンケアコスメの使用期限切れを防ぐサポートをします。

### サービスを作成した背景
K-POPを好きになったことがきっかけで韓国コスメを使うようになったのですが、比較的安価でありながら質が高いところに魅力を感じ、韓国コスメに馴染みがない方にも興味を持っていただくきっかけを作りたいと思いました。

韓国コスメに関してすでに知識がある方よりは、

「韓国コスメを使ったことはないけれど、**何となく興味はある**」
「たくさん種類があって、**何から手を出したらいいのかわからない**」
「どのスキンケアコスメを選んだらいいか**迷ってしまう**」

といった、韓国コスメ初心者の方や、スキンケアについてお悩みを抱えている方を主なターゲットとし、韓国コスメ用語の解説ページを作成したり、アプリ内で紹介するコスメの種類を絞ることで、気軽に使っていただけるよう工夫しました。

また、UV指数や湿度の情報・使用期限のLINE通知機能を通して、ユーザーの皆さんに自身のスキンケアについてより関心を持っていただき、皆さんの美肌づくりをサポートできたら嬉しいなと思い、開発に至りました。

リリース後には、

「これを機に韓国コスメ使ってみようかな」
「肌質に合わせておすすめしてくれてありがたい」
「何を使ったらいいのか？は男性あるあるなのでありがたい」
「UV、湿度を教えてくれるの嬉しい」

など、たくさんのご感想をいただき、とっても嬉しかったです！

## 機能一覧
|お試し韓国コスメ検索機能|韓国コスメ検索機能|
|:-------------|:-------------|
|[![Image from Gyazo](https://i.gyazo.com/08b1f4c7475cdd68785d6e401ae69d5e.gif)](https://gyazo.com/08b1f4c7475cdd68785d6e401ae69d5e)|[![Image from Gyazo](https://i.gyazo.com/b4ef7510919f33fbf04ae0e4bc2f68d7.gif)](https://gyazo.com/b4ef7510919f33fbf04ae0e4bc2f68d7)|
|肌質・お悩みを選択すると、それぞれに合った韓国コスメを表示します。|肌質・お悩み・形態・金額を選択すると、それぞれに合った韓国コスメを表示します。|

|レコメンド機能（1）|レコメンド機能（2）|
|:-------------|:-------------|
|[![Image from Gyazo](https://i.gyazo.com/0ed455dd207549f0b99726f8871428ab.gif)](https://gyazo.com/0ed455dd207549f0b99726f8871428ab)|[![Image from Gyazo](https://i.gyazo.com/ec0901d3e4251db42ff66eb021b7922a.gif)](https://gyazo.com/ec0901d3e4251db42ff66eb021b7922a)|
|ユーザーが検索時に選択したキーワードに関連する商品をレコメンドします。|マイページにて設定していただいた肌質・お悩みに関連する商品をレコメンドします。|

|お気に入りコスメ登録機能|お気に入りコスメ一覧機能|
|:-------------|:-------------|
|[![Image from Gyazo](https://i.gyazo.com/f3bfb88df1c3ae63eea25af367604f52.gif)](https://gyazo.com/f3bfb88df1c3ae63eea25af367604f52)|[![Image from Gyazo](https://i.gyazo.com/ee9d2d321b688eebf3770a5d5703a819.gif)](https://gyazo.com/ee9d2d321b688eebf3770a5d5703a819)|
|検索したコスメやレコメンドされたコスメをお気に入りに登録できます。|マイページ内でお気に入りに登録したコスメを確認できます。|

|紫外線/乾燥注意通知機能（設定）|使用期限通知機能（設定）|
|:-------------|:-------------|
|[![Image from Gyazo](https://i.gyazo.com/c6f097ed0fd62cd42e9f4b9f97969bec.gif)](https://gyazo.com/c6f097ed0fd62cd42e9f4b9f97969bec)|[![Image from Gyazo](https://i.gyazo.com/aeece26680ab07af365c5aee65e58967.gif)](https://gyazo.com/aeece26680ab07af365c5aee65e58967)|
|マイページにて設定していただいたお住まいのUV指数と湿度の情報を毎朝10時に通知します。|コスメの製品タイプ、開封日、使用期限を設定していただくと、使用期限の5日前・3日前・前日に通知します。|

|紫外線/乾燥注意通知機能（通知画面）|使用期限通知機能（通知画面）|
|:-------------|:-------------|
|![line_image_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3467801/0dd2d3dd-582b-4463-a03c-e70609a3aff3.png)|![line_image_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3467801/2f5622ac-65e5-3194-d6ea-6cd7522cc373.png)
|実際の通知画面です。|実際の通知画面です。|

|レビュー投稿/一覧機能|レビュー編集/削除機能|
|:-------------|:-------------|
|[![Image from Gyazo](https://i.gyazo.com/f8ad3d501e730ab4abd85e35d7e238d1.gif)](https://gyazo.com/f8ad3d501e730ab4abd85e35d7e238d1)|[![Image from Gyazo](https://i.gyazo.com/09d80963cb51c458a9ad69449af68c2f.gif)](https://gyazo.com/09d80963cb51c458a9ad69449af68c2f)|
|お気に入りに登録したコスメのレビューを投稿できます。|レビューの編集や削除も可能です。|

|レビュー検索機能|ログイン/ログアウト機能|
|:-------------|:-------------|
|[![Image from Gyazo](https://i.gyazo.com/3109c698bc949aa980129ebf71573e27.gif)](https://gyazo.com/3109c698bc949aa980129ebf71573e27)|[![Image from Gyazo](https://i.gyazo.com/4589ca07f793d980521edaee6f670c92.gif)](https://gyazo.com/4589ca07f793d980521edaee6f670c92)|
|レビュー投稿時に付けられたタグ（肌質、お悩み、年代）により、該当するレビューを検索できます。|NextAuth.jsを使用したLINEログインが可能です。|

|現在の天気/UV指数/湿度の表示機能|
|:-------------|
|[![Image from Gyazo](https://i.gyazo.com/000bd1a273cca4baf696a07b7750add4.gif)](https://gyazo.com/000bd1a273cca4baf696a07b7750add4)|[![Image from Gyazo](https://i.gyazo.com/8b77327ffc6ca4acf5cbfd6efef053a8.gif)](https://gyazo.com/8b77327ffc6ca4acf5cbfd6efef053a8)|
|OpenWeatherMap APIを使用し、現在の天気、UV指数、湿度の情報を表示しています。|

|お気に入りコスメのXシェア機能|レビュー投稿のXシェア機能|
|:-------------|:-------------|
|[![Image from Gyazo](https://i.gyazo.com/a1ca63096a3d0f2a335cd86e9c42233b.gif)](https://gyazo.com/a1ca63096a3d0f2a335cd86e9c42233b)|[![Image from Gyazo](https://i.gyazo.com/928db7948693ec6bc5d1b7a2cb273c56.gif)](https://gyazo.com/928db7948693ec6bc5d1b7a2cb273c56)|
|お気に入りコスメのXへのシェアができます。|レビュー投稿のXへのシェアができます。|

## 使用技術
|カテゴリ|技術|
|:-------------|:------------|
|フロントエンド|TypeScript 5.3.3 / React 18.2.0 / Next.js 14.0.4|
|バックエンド|Ruby 3.2.2 / Ruby on Rails 7.0.8（API モード）|
|データベース|PostgreSQL|
|認証|NextAuth.js|
|環境構築|Docker|
|インフラ|Vercel / Render|
|Web API|楽天市場商品検索API / LINE Messaging API / OpenWeatherMap API|
|その他|SWR / shadcn/ui / Sidekiq / Redis / ActiveJob|

## インフラ構成
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3467801/253bf43d-0e54-a7c4-2672-4a0fe2eed546.png)

## 選定理由
### 開発環境
開発環境には、環境ごとの差異を最小限に抑えられるとともに、`docker-compose`を使用することで、各コンテナの起動、停止、再構築をコマンド一つで実行できる点が開発の効率化に繋がると考え、`Docker`を採用しました。

**▼参考にした記事**

https://qiita.com/ippei_jp/items/1163a40a86d07fa691b2

### フロントエンド
フロントエンドには、昨年チーム開発にて`Next.js`を使用した経験を踏まえ、さらに深く掘り下げて学びたいと考え、`Next.js`を採用しました。SSRやCSR、SSGなどレンダリング手法を考慮した上でパフォーマンス設計を行い、ユーザビリティを高められるよう意識しました。

### バックエンド
バックエンドには、スクールで学んできた`Ruby on Rails`を採用することで、実装スピードを上げられると考えました。

### インフラ
`Vercel`は、`Next.js`との相性が良いこと、`GitHub`との連携によりコードをpushすることで自動でデプロイが行われる点が、開発の効率化に繋がると思い採用しました。

`Render`は、過去のアプリ開発での利用経験が豊富であったため、導入コストを抑えられることと、LINE通知機能においてバックエンドで要求されるキューイングシステムの構築に必要な`Redis`を提供していることから採用しました。

## ER図
```mermaid
erDiagram
Users ||--|| Profiles : has
Users ||--o{ FavoriteCosmetics : favorites
Users ||--o{ Reviews : creates
Users ||--o{ CosmeticUsage : has
FavoriteCosmetics ||--o{ Reviews : has
Reviews ||--o{ ReviewTags : has
Tags ||--o{ ReviewTags : has

Users {
  bigint id PK
  string provider "UNIQUE"
  string uid "UNIQUE"
  string name
  string avatar
  boolean receive_notifications_weather
  boolean receive_notifications_expiration_date
  datetime created_at
  datetime updated_at
}

Profiles {
  bigint id PK
  bigint user_id FK
  bigint address_id FK
  string name
  string skin_type
  string skin_trouble
  integer age
  string avatar
  string prefecture
  datetime created_at
  datetime updated_at
}

Addresses {
  bigint id PK
  text address
  float latitude
  float longitude
  datetime created_at
  datetime updated_at
}

FavoriteCosmetics {
  bigint id PK
  bigint user_id FK
  string name
  string price
  string item_url
  string image_url
  string item_code
  datetime created_at
  datetime updated_at
}

CosmeticUsages {
  bigint id PK
  bigint user_id FK
  integer item_type
  date open_date
  date expiry_date
  datetime created_at
  datetime updated_at
}

Reviews {
  bigint id PK
  bigint user_id FK
  bigint favorite_cosmetic_id FK
  integer rating
  string title
  text body
  boolean visibility
  datetime created_at
  datetime updated_at
}

Tags {
  bigint id PK
  string tag_name
  datetime created_at
  datetime updated_at
}

ReviewTags {
  bigint id FK
  bigint review_id FK
  bigint tag_id FK
  datetime created_at
  datetime updated_at
}
```

## 画面遷移図
[画面遷移図](https://www.figma.com/file/re0Sf3bUsSQ52A4nPHaFA0/%E7%94%BB%E9%9D%A2%E9%81%B7%E7%A7%BB%E5%9B%B3?type=design&node-id=0%3A1&mode=design&t=jpVBer32Q4ftPUD8-1)

## 工夫したポイント
### ログイン機能
ユーザー登録のハードルを下げたかったことと、セキュリティ面を考慮し、`NextAuth.js`を使用したLINEログインを実装しました。

<details><summary>一部コード抜粋</summary>

```front/app/api/auth/[...nextauth]/route.ts
import NextAuth from 'next-auth';
import LineProvider from 'next-auth/providers/line';

const apiUrl = process.env.NEXT_PUBLIC_API_URL;

const handler = NextAuth({
  providers: [
    LineProvider({
      id: "line",
      name: "LINE",
      clientId: process.env.NEXT_PUBLIC_LINE_CLIENT_ID || '',
      clientSecret: process.env.NEXT_PUBLIC_LINE_CLIENT_SECRET || '',
      authorization: { params: { scope: "profile openid" } },
      token: "https://api.line.me/oauth2/v2.1/token",
      userinfo: "https://api.line.me/v2/profile",
      profile(profile) {
        return {
          id: profile.sub,
          name: profile.displayName,
          image: profile.pictureUrl,
        };
      },
    }),
  ],
  //　中略
});

export { handler as GET, handler as POST };
```
```front/app/providers/NextAuth.tsx
'use client';

import { SessionProvider } from 'next-auth/react';
import { ReactNode } from 'react';

const NextAuthProvider = ({ children }: { children: ReactNode }) => {
  return <SessionProvider>{children}</SessionProvider>;
};

export default NextAuthProvider;
```
</details>

https://next-auth.js.org/

**▼参考にした記事**

https://qiita.com/ke_sukesakuma/items/4b56b9e81c1788d38440

### LINE通知機能
この機能では、情報を決まった時間に自動的に送信することが目標でした。そのため、`Sidekiq`のスケジューリング機能と、`Redis`をバックエンドに使用する`ActiveJob`を組み合わせて利用しました。さらに、`Sidekiq-Cron`を使って`Cron`式でジョブのスケジューリングを設定し、ジョブ失敗に備えて`Sidekiq`の自動リトライ機能を設定しました。

<details><summary>一部コード抜粋</summary>

```back/app/jobs/weather_notification_job.rb
class WeatherNotificationJob < ApplicationJob
  queue_as :default

  sidekiq_options retry: 1
  # Sidekiqはジョブの実行に失敗した場合、デフォルトで25回リトライするため、
  # 何度も通知が送られないよう、リトライ回数を1回に指定

  sidekiq_retry_in do |_count|
    60
  end
  # ジョブのリトライ間隔を60秒に設定

  def perform
    User.includes(:profile).find_each do |user|
      next unless user.profile
      next unless user.receive_notifications_weather

      prefecture_name = user.profile.prefecture
      next if prefecture_name.blank?

      weather_info = WeatherService.fetch_weather_data(prefecture_name)
      next if weather_info.blank?

      message = WeatherMessageGenerator.generate_message(prefecture_name, weather_info)
      LineNotifyService.send_message(user.uid, message) if user.uid.present?
    end
  end
end
```
```back/config/initializers/sidekiq.rb
require 'sidekiq'
require 'sidekiq-cron'

Sidekiq.configure_server do |config|
  # Sidekiqサーバーの設定（ジョブを実行するサーバー側で適用される）
  schedule_file = 'config/schedule.yml'
  # 定期的に実行するジョブのスケジュールを記述したYAMLファイルのパスを指定
  config.redis = {
    url: ENV['REDIS_URL'],
    connect_timeout: 5,
    read_timeout: 5,
    write_timeout: 5
    # 接続、読み込み、書き込みそれぞれに5秒のタイムアウトを設定
  }

  Sidekiq::Cron::Job.load_from_hash YAML.load_file(schedule_file) if File.exist?(schedule_file)
  # config/schedule.ymlファイルが存在する場合、そのファイルを読み込み、
  # YAMLファイルに記述されたスケジュールに従って定期的なジョブを設定
end

Sidekiq.configure_client do |config|
  # Sidekiqクライアントの設定（ジョブをキューに投入する側で適用される）
  config.redis = {
    url: ENV['REDIS_URL'],
    connect_timeout: 5,
    read_timeout: 5,
    write_timeout: 5
    # サーバー側の設定と同様、RedisサーバーのURLとタイムアウトの設定を行う
  }
end

```
```back/config/schedule.yml
weather_notification_job:
  cron: "0 10 * * *"
  class: "WeatherNotificationJob"
  # 毎朝10時に通知を送るよう設定
```
</details>

LINE通知機能の実装については、別の記事にて詳しく書きたいと思います。

https://github.com/sidekiq/sidekiq

https://github.com/sidekiq-cron/sidekiq-cron

### 説明用のモーダル
画面上にはあまり文章を載せないようにし、補足事項はモーダル内に格納するようにしました。また、ツールチップを使用し、ホバーした際に何の情報が書かれているのかわかるようにしました。

|UV指数と湿度の目安|肌質の目安|
|:-------------|:------------|
[![Image from Gyazo](https://i.gyazo.com/f1f10736d8bbb3813706861d30d03aef.gif)](https://gyazo.com/f1f10736d8bbb3813706861d30d03aef)|[![Image from Gyazo](https://i.gyazo.com/75354c79aea6f06a6883bd8646dbcfe5.gif)](https://gyazo.com/75354c79aea6f06a6883bd8646dbcfe5)|

### 配色
韓国コスメはカラフルなパッケージのものが多いため、韓国コスメ自体が引き立つよう落ち着いた配色を意識しました。カラーコードはノグチデザインさん([@n_seitan](https://x.com/n_seitan?s=20))を参考にさせていただきました。

### レスポンシブデザイン
メニューの表示方法をPCとスマートフォンで変更しました。スマートフォンでの使用が多いと想定し、片手で操作することを考え、手の届きやすいところにメニューボタンを配置しました。

|PC|スマートフォン|
|:-------------|:------------|
[![Image from Gyazo](https://i.gyazo.com/8bdec029721689d46816208ae7b04349.gif)](https://gyazo.com/8bdec029721689d46816208ae7b04349)|[![Image from Gyazo](https://i.gyazo.com/ed319ffa8f5665357ef187b0729924d8.gif)](https://gyazo.com/ed319ffa8f5665357ef187b0729924d8)|

## 今後の開発について
**1.ランキング機能**
お気に入りの数が多い順や、レビュー評価の高い順でのランキング機能を実装したいと考えています。

**2.テスト**
まだまだテストコードが書けていないので、カバレッジ率を上げていきたいです。

## おわりに
今まで簡単なミニアプリは作成したことがありますが、0からテーブル設計などを考えて開発するのは初めてでした。わからないことばかりで大変ではありましたが、自分が思い描いたものを形にできてとても楽しかったです！

まだまだ至らない点ばかりですが、今回の経験を糧に今後も楽しみながら学んでいきたいです。

改めて、開発にあたりご協力いただいた皆さまに感謝申し上げます。

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