Django-rest-frameworkとreact

## はじめに
drf と react の組み合わせをやっておきたかった。
参考サイトを再現しようとしたらあちこち破綻してて、結局DDDベースでゴリッゴリに修正したらすでに原型をとどめていない...:joy:
まぁキャッチアップとしてはいいか

## 参考サイト
https://zenn.dev/piyopanman/articles/6ccf6404a82727

## Overview
- django のプロジェクト名は `drf_react_backend` にする
  - 実際には `config` というプロジェクトを作成してプロジェクトフォルダ名を `drf_react_backend` に変更する
  - アプリケーション名は `daily_report` にする
  - venv名は `venv311` にする
- react のプロジェクト名は `drf_react_frontend` にする
```mermaid
graph LR
  drf_react_backend
  drf_react_frontend
  drf_react_backend -- venv --> venv311
  drf_react_backend -- django --> config
  drf_react_backend -- django --> daily_report
```

```console:chatgpt
僕: django rest framework と react を組み合わせてアプリをつくりたい。それぞれの最初のコマンドライン（セットアップ部分）をまとめて（django, react）
```
:::note alert
結局ドヤ顔で説明されて振り回された... :weary:
:::

## Django part
### create root directory
```console:console
mkdir drf_react_backend
cd drf_react_backend
```

### venv
```console:console
python -m venv venv311
```

### create project
```console:console
pip install django
pip install djangorestframework
pip install django-markdownx

django-admin startproject config .
```

### create app
```console:console
python manage.py startapp daily_report
```
```diff_python:drf_react_backend/config/settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
+   'rest_framework',
+   'markdownx',
+   'daily_report',
]
+REST_FRAMEWORK = {
+   'DEFAULT_PERMISSION_CLASSES': [
+       'rest_framework.permissions.AllowAny',
+   ]
+}
```
:::note
`DEFAULT_PERMISSION_CLASSES` は「誰にアクセスを許可するか」を指定する。
- 今回は `get` しかない、かつ、取り回しの良さで `AllowAny` だが、これだと誰でも日報を追加したり削除できるので `post` リクエストなどがある場合は `IsAuthenticatedOrReadOnly` に変えるとよい。すると、`get, head, options` リクエストは誰にでも許可されるが、それ以外の `post, delete` などのリクエストは認証済みのユーザにしか許可されなくなる。

- https://www.django-rest-framework.org/api-guide/permissions/
:::

### 確認
```console:console
python manage.py runserver
```

### 非公開情報を.envに移す（GitGuardian対策）
```console:console
pip install django-environ
```
```env:drf_react_backend/.env
SECRET_KEY=djaXXXXX
DEBUG=True
DB_ENGINE=django.db.backends.mysql
DB_NAME=drf_react_db
DB_USER=python
DB_PASSWORD=python123
```
```diff_python:drf_react_backend/config/settings.py
from pathlib import Path
+ import environ

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve(strict=True).parent.parent

+ # read at .env
+ env = environ.Env(DEBUG=(bool, False))
+ environ.Env.read_env(Path(BASE_DIR, '.env'))
+ DEBUG = env('DEBUG')  # read DEBUG at .env
+ SECRET_KEY = env('SECRET_KEY')  # read SECRET_KEY at .env

# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/

- # SECURITY WARNING: keep the secret key used in production secret!
- SECRET_KEY = 'djaXXXXX'

- # SECURITY WARNING: don't run with debug turned on in production!
- DEBUG = True
    :
```
:::note alert
ここでいったん pycharm を再起動したり、コンソールの再起動をしないと引きずってえんえんとハマることになる
:::

### MySQL
```console:console
pip install mysqlclient
```
```diff_python:drf_react_backend/config/settings.py
DATABASES = {
    'default': {
-       'ENGINE': 'django.db.backends.sqlite3',
-       'NAME': BASE_DIR / 'db.sqlite3',
+       'ENGINE': env('DB_ENGINE'),
+       'NAME': env('DB_NAME'),
+       'USER': env('DB_USER'),
+       'PASSWORD': env('DB_PASSWORD'),
    }
}
    :
```

#### mysqlにrootで入る
```console:console
mysql -u root -p
```
#### create database
```console:console
mysql> CREATE DATABASE drf_react_db DEFAULT CHARACTER SET utf8mb4;
       Query OK, 1 row affected (0.01 sec)
```
#### create user
```console:console
mysql> CREATE USER 'python'@'localhost' IDENTIFIED BY 'python123';
```
#### create grant
```console:console
mysql> grant CREATE, DROP, SELECT, UPDATE, INSERT, DELETE, ALTER, REFERENCES, INDEX on drf_react_db.* to python@localhost;
```

#### mysqlを出る
```console:console
mysql> exit
```
```console:console（作ったユーザで入れるか確認）
mysql -u python -p
```
```console:console
mysql> exit
```

### models.py
```python:drf_react_backend/daily_report/models.py
from django.db import models
from markdownx.models import MarkdownxField


class Daily(models.Model):
    """
    univ: 大学の課題とか
    study: 勉強したこと
    other: その他
    first_meet: 初めて知ったこと
    wanna_do: やりたいこと
    summary: 1日のまとめ
    evaluation: 1日の評価(外部キー)
    isOpen: 公開/非公開
    """
    date = models.DateField()
    univ = MarkdownxField()
    study = MarkdownxField()
    other = MarkdownxField()
    first_meet = MarkdownxField()
    wanna_do = MarkdownxField()
    summary = MarkdownxField()
    evaluation = models.ForeignKey('Evaluation', on_delete=models.PROTECT)
    isOpen = models.BooleanField(default=True)

    def __str__(self):
        date_str = self.date.__format__('%Y/%m/%d')
        return date_str


class Evaluation(models.Model):
    evaluation = models.CharField(max_length=255)

    def __str__(self):
        return self.evaluation
```

### migrate
```console:console
python manage.py makemigrations daily_report
python manage.py migrate
```

### create superuser
```console:console
python manage.py createsuperuser
  ユーザー名 (leave blank to use 'yoshi'): 
  メールアドレス: yoshiXXXX@gmail.com
  Password: 
  Password (again):
  このパスワードは ユーザー名 と似すぎています。
  Bypass password validation and create user anyway? [y/N]: y
  Superuser created successfully.
```

### fixtures
```console:console
mkdir daily_report/fixtures
```
```json:drf_react_backend/daily_report/fixtures/evaluation.json（新規）
[
  {
    "model": "daily_report.evaluation",
    "pk": 1,
    "fields": {
      "evaluation": "perfect"
    }
  },
  {
    "model": "daily_report.evaluation",
    "pk": 2,
    "fields": {
      "evaluation": "good"
    }
  },
  {
    "model": "daily_report.evaluation",
    "pk": 3,
    "fields": {
      "evaluation": "soso"
    }
  },
  {
    "model": "daily_report.evaluation",
    "pk": 4,
    "fields": {
      "evaluation": "bad"
    }
  }
]
```
```json:drf_react_backend/daily_report/fixtures/daily.json（新規）
[
  {
    "model": "daily_report.daily",
    "pk": 1,
    "fields": {
      "date": "2023-05-01",
      "univ": "大学の課題1",
      "study": "勉強したこと1",
      "other": "その他1",
      "first_meet": "初めて知ったこと1",
      "wanna_do": "やりたいこと1",
      "summary": "1日のまとめ1",
      "evaluation": 1,
      "isOpen": true
    }
  },
  {
    "model": "daily_report.daily",
    "pk": 2,
    "fields": {
      "date": "2023-05-02",
      "univ": "大学の課題2",
      "study": "勉強したこと2",
      "other": "その他2",
      "first_meet": "初めて知ったこと2",
      "wanna_do": "やりたいこと2",
      "summary": "1日のまとめ2",
      "evaluation": 3,
      "isOpen": true
    }
  },
  {
    "model": "daily_report.daily",
    "pk": 3,
    "fields": {
      "date": "2023-05-03",
      "univ": "大学の課題3",
      "study": "勉強したこと3",
      "other": "その他3",
      "first_meet": "初めて知ったこと3",
      "wanna_do": "やりたいこと3",
      "summary": "1日のまとめ3",
      "evaluation": 2,
      "isOpen": true
    }
  },
  {
    "model": "daily_report.daily",
    "pk": 4,
    "fields": {
      "date": "2023-05-04",
      "univ": "大学の課題4",
      "study": "勉強したこと4",
      "other": "その他4",
      "first_meet": "初めて知ったこと4",
      "wanna_do": "やりたいこと4",
      "summary": "1日のまとめ4",
      "evaluation": 1,
      "isOpen": true
    }
  },
  {
    "model": "daily_report.daily",
    "pk": 5,
    "fields": {
      "date": "2023-05-05",
      "univ": "大学の課題5",
      "study": "勉強したこと5",
      "other": "その他5",
      "first_meet": "初めて知ったこと5",
      "wanna_do": "やりたいこと5",
      "summary": "1日のまとめ5",
      "evaluation": 1,
      "isOpen": true
    }
  }
]
```
```console:console
python manage.py loaddata daily_report/fixtures/evaluation.json
python manage.py loaddata daily_report/fixtures/daily.json
```

### urls.py
```diff_python:drf_react_backend/config/urls.py
"""
URL configuration for config project.

The `urlpatterns` list routes URLs to views. For more information please see:
    https://docs.djangoproject.com/en/4.2/topics/http/urls/
Examples:
Function views
    1. Add an import:  from my_app import views
    2. Add a URL to urlpatterns:  path('', views.home, name='home')
Class-based views
    1. Add an import:  from other_app.views import Home
    2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')
Including another URLconf
    1. Import the include() function: from django.urls import include, path
    2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
"""
from django.contrib import admin
-from django.urls import path
+from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
+   path('daily_report/', include('daily_report.urls')),
+   path('markdownx/', include('markdownx.urls')),
]
```
```python:drf_react_backend/daily_report/urls.py（新規）
from django.urls import path
from . import views

urlpatterns = [
    path('', views.DailyList.as_view()),
    path('<int:pk>/', views.DailyDetail.as_view()),
]
```
:::note
- `http://127.0.0.1:8000/daily/` というURLがきたら `views.py` の `ListDaily` をみてね
- `http://127.0.0.1:8000/daily/1/` とか `http://127.0.0.1:8000/daily/2/` というURLがきたら `views.py` の`DetailDaily` をみてね
- `http://127.0.0.1:8000/daily/study/` とか `http://127.0.0.1:8000/daily/wanna_do/` というURLがきたら`views.py` の `CategoryDairy` をみてね

ということ。つまりここでは `http://127.0.0.1:8000/daily/` 以降のパスを指定している
:::

### serializers.py
Dailyモデル をベースに、取り扱いたい tuple の Serializer を作る
```python:drf_react_backend/daily_report/serializers.py（新規）
from rest_framework import serializers

from .models import Daily


class DailyListSerializer(serializers.ModelSerializer):
    """
    日報の一覧用シリアライザ
    """
    evaluation = serializers.CharField(source='evaluation.evaluation')

    class Meta:
        model = Daily
        fields = ('id', 'date', 'evaluation')


class DailyDetailSerializer(serializers.ModelSerializer):
    """
    日報の詳細用シリアライザ
    """
    class Meta:
        model = Daily
        fields = ('id', 'univ', 'date', 'study', 'other', 'first_meet', 'wanna_do', 'summary')
```

### views.py
JSONを返す処理を書いていく。Serializer を使うことで、データの詰め替えが（Djangoで `Class-based View` を使うときのように）スマートになっていることがわかる。のちにバリデーションしたときによりスマートさが際立つだろう。

```python:drf_react_backend/daily_report/views.py
from django.http import Http404
from django.shortcuts import get_object_or_404
from rest_framework import generics, status
from rest_framework.response import Response

from .models import Daily
from .serializers import DailyListSerializer, DailyDetailSerializer


class DailyList(generics.ListAPIView):
    """
    日報の一覧
    isOpen が True になっているものを日付の降順で返す
    """
    queryset = Daily.objects.filter(isOpen=True).order_by('-date')
    serializer_class = DailyListSerializer


class DailyDetail(generics.RetrieveAPIView):
    """
    日報の詳細情報
    """
    queryset = Daily.objects.all()
    serializer_class = DailyDetailSerializer

    def get(self, request, *args, **kwargs):
        try:
            instance = get_object_or_404(self.get_queryset(), pk=self.kwargs['pk'])
            serializer = self.get_serializer(instance)
            return Response(serializer.data)
        except Http404:
            return Response(status=status.HTTP_404_NOT_FOUND)
```

### 確認
- 日報の一覧を取得 `http://127.0.0.1:8000/daily_report/`![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/2a178543-8e42-b0bc-2c2f-6a284fa6f322.png)

- idが `2` の日報の詳細情報を取得 `http://127.0.0.1:8000/daily_report/2/`![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/eb8ef78a-2914-5bcc-acef-f40886e4639e.png)

- `date, univ` というフィールドだけの一覧を取得 `http://127.0.0.1:8000/daily_report/univ/`![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/7c1c458e-6e92-ab39-5d64-17175e247d94.png)


### setup CORS
:::note
今回のようにバックエンドをDjango、フロントをReactでやる場合、CORS(オリジン間リソース共有)の設定をする必要があります。DjangoとReactは異なるオリジンで動作するので、その間のHTTPリクエストを許可する必要があります
:::

```console:console
pip install django-cors-headers
```
```diff_python:drf_react_backend/config/settings.py
INSTALLED_APPS = [
      :
+   'corsheaders',
]
  :
MIDDLEWARE = [
+   'corsheaders.middleware.CorsMiddleware',
+   'django.middleware.common.CommonMiddleware',
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
+CORS_ORIGIN_WHITELIST = (
+   'http://localhost:3000', 
+)
```
<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/6e991c09-3e4a-2d33-8502-b0aab82e6684.jpeg">

### .gitignore
- gitignoreは、gitignore.ioのdjango用のものと migrationsフォルダを指定する

https://www.toptal.com/developers/gitignore/api/django

```drf_react_backend/.gitignore
/daily_report/migrations/
    :
（最下行に追加: gitignore.ioのdjango用のもの）
```

## React part
### create root directory
```console:console
mkdir drf_react_frontend
cd drf_react_frontend
```

### create app
```console:console
npx create-react-app . --template typescript
```

### package.json
```diff_json:drf_react_frontend/package.json
{
    :
  "dependencies": {
      :
+   "react-router-dom": "^6.11.2",
+   "antd": "^5.6.1"
  },
    :
```

```console:console
npm start
```

### Testing
```diff_json:drf_react_frontend/package.json
{
    :
  "dependencies": {
      :
  },
+ "devDependencies": {
+   "eslint": "8.22.0"
+ },
    :
```
:::note warn
eslint は `8.22` がいいみたい。
[ESLint: TypeError: this.libOptions.parse は関数ではありません](https://stackoverflow.com/questions/73509984/eslint-typeerror-this-liboptions-parse-is-not-a-function)
:::

```js:drf_react_frontend/src/services/sum.js
function sum (a, b) {
  return a + b
}
module.exports = sum
```
```js:drf_react_frontend/src/services/__tests__/sum.test.js
const sum = require('../sum')

it('adds 1 + 2 to equal 3', () => {
  expect(sum(1, 2)).toBe(3)
})

it('dataから!nullの値を取得できる条件文のテスト', () => {
  const data = {
    test1: 'a',
    test2: [],
    test3: ['1'],
    test4: '',
    test5: null
  }
  const expected = {
    test1: 'a',
    test3: ['1']
  }
  let actual = {}
  for (let [key, val] of Object.entries(data)) {
    if (val && val.length > 0) {
      actual[key] = val
    }
  }
  expect(actual).toEqual(expected)
})

```
```console:console
npm test
 PASS  src/App.test.tsx
 PASS  src/services/__tests__/sum.test.js

 Test Suites: 2 passed, 2 total
 Tests:       3 passed, 3 total
 Snapshots:   0 total
 Time:        2.746 s
 Ran all test suites.
```
:::note
ぼく: あー、プロダクションコードがあるディレクトリに `__tests__` フォルダを作る必要があるのね？

---

ChatGPT: はい、通常、プロダクションコードと対応するテストコードは同じディレクトリ構造を持つことが推奨されます。そのため、プロダクションコードがあるディレクトリ内に対応するテストコードを格納するために `__tests__` ディレクトリを作成します。

これにより、プロダクションコードとテストコードが密接に関連しており、保守性が高まります。また、テストファイルをプロダクションコードと同じ場所に配置することで、開発者や他のチームメンバーがテストを見つけやすくなります。

ただし、必ずしも `__tests__` ディレクトリを使用する必要はありません。Jestはデフォルトで、プロジェクト内のすべての `*.test.js` または `*.spec.js` という拡張子を持つファイルをテストファイルとして自動的に検出します。そのため、特定のディレクトリに制約されずにテストファイルを配置することも可能です。ただし、一貫性と可読性のために、一般的には `__tests__` ディレクトリを使用することが推奨されます。
:::

### ページ構成をあたまで整理
- 参考サイトを読み解いた結果、DDD風味にしてこんなイメージになった
![IMG20230613224149.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/27436298-47dd-44a7-e88e-a88050e386dd.jpeg)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/8aecd0cb-06c6-cd6a-28f8-01a3e6badc20.png)

### 勝手に出来上がるファイルについて
#### App.tsx
全体的なレイアウト...か

:::note
ぼく: `App.tsx` ってどんな役割？

---

ChatGPT: `App.tsx` はReactアプリケーションのエントリーポイントとして機能します。このファイルは、アプリケーションのルートコンポーネントであり、アプリケーション全体のレイアウトやルーティングを定義するために使用されます。

`App.tsx` は通常、以下のような役割を果たします：

レイアウトの定義: `App.tsx` はアプリケーションの全体的なレイアウトを定義するために使用されます。例えば、ヘッダーやフッターなど、すべてのページで表示される共通のコンポーネントを配置することができます。

ルーティングの設定: `App.tsx` はルーティングライブラリ（例：React Router）を使用して、アプリケーションの異なるページ間のナビゲーションを設定するために使用されます。ルートコンポーネントとルートパスのマッピングや、URLパラメーターの処理などを行うことができます。

コンテキストの提供: `App.tsx` はReactのコンテキストを使用して、アプリケーション全体で共有するデータや状態を提供するために使用されることもあります。例えば、認証情報やテーマの設定など、多くのコンポーネントで利用される共有データを管理する場合に使用できます。

`App.tsx` はReactアプリケーションの中心的な役割を果たすファイルであり、その他のコンポーネントや機能はこのファイルを基準にして組み込まれます。

:::
#### index.tsx
`App.tsx` をトリガーするもの...か

:::note
ぼく: `index.tsx` はなに？

---

ChatGPT: `index.tsx` はReactアプリケーションのエントリーポイントであり、アプリケーションの起点となるファイルです。このファイルは、Reactの仮想DOM (Virtual DOM) をブラウザの実際のDOMにマウントするための役割を担っています。

具体的には、`index.tsx` は以下のような役割を果たします：

ルートコンポーネントのレンダリング: `index.tsx` はアプリケーションのルートコンポーネントをレンダリングするために使用されます。通常、ルートコンポーネントは `<App />` のような形式で指定され、アプリケーション全体のコンポーネントツリーの起点となります。

ブラウザのDOMへのマウント: `index.tsx` はReactの仮想DOMをブラウザの実際のDOMにマウントする役割を持ちます。`ReactDOM.render()` メソッドを使用して、ルートコンポーネントを指定したDOM要素にレンダリングします。

ホットリロードの設定（開発モードの場合）: 開発モードで `index.tsx` を使用している場合、ホットリロードの設定も行われます。ファイルの変更を検知し、自動的にアプリケーションを再描画することができます。

`index.tsx` はReactアプリケーションのエントリーポイントであり、アプリケーションの起動時に最初に実行されるファイルです。そのため、アプリケーションの初期化や構成の設定など、重要な役割を果たしています。
:::

:::note
ぼく: `index.tsx` が最初に呼ばれて、そのときに `App` を呼ぶのでつぎに `App.tsx` が呼ばれる感じか。djangoでいう `base.html` と `index.html` の感じに似ている。

---

ChatGPT: はい、おっしゃる通りです。 `index.tsx` は最初に呼び出され、その中で `<App />` コンポーネントがレンダリングされます。その後、Reactは `<App />` コンポーネント内のコンポーネントツリーを構築し、最終的にブラウザに表示されます。
:::

#### react-app-env.d.ts
環境設定ファイル...か

:::note
ぼく: `react-app-env.d.ts` はなに？

---

ChatGPT: `react-app-env.d.ts` は、Reactアプリケーションの環境設定ファイルです。このファイルは、TypeScriptの型定義やグローバルな型拡張を含むために使用されます。

通常、Reactアプリケーションでは、`.tsx` 拡張子のファイル内でTypeScriptを使用して開発を行います。しかし、プロジェクトのルートディレクトリに `react-app-env.d.ts` ファイルが存在すると、TypeScriptコンパイラはそのファイルを自動的に読み込み、その中に定義された型情報を利用できるようになります。

`react-app-env.d.ts` ファイルは、主に以下のような目的で使用されます：

プロジェクト固有の型定義: プロジェクト内で利用するカスタムな型定義やインターフェースの定義を含むことがあります。これにより、アプリケーション全体で共通の型情報を利用することができます。

グローバルな型拡張: 既存のライブラリやフレームワークの型定義を拡張するための型定義を含むことがあります。これにより、外部ライブラリの型情報を補完したり、より具体的な型情報を提供したりすることができます。

`react-app-env.d.ts` ファイルは、通常、自動生成されます。Reactアプリケーションを作成するときに、`Create React App` などのツールが自動的に生成してくれることがあります。ファイルの中身はプロジェクトの具体的な要件によって異なる場合があります。
:::

#### reportwebvitals.ts
パフォーマンス計測関連...か

:::note
ぼく: `reportwebvitals.ts` はなに？

---

ChatGPT: `reportWebVitals.ts`は、ReactアプリケーションでのWeb Vitals（Webのパフォーマンス指標）の計測とレポートを行うためのファイルです。

Web Vitalsは、ユーザーエクスペリエンスの測定に役立つ重要なパフォーマンス指標のセットです。これには、ページの読み込み時間、反応性、レンダリングの遅延などが含まれます。`reportWebVitals.ts`ファイルは、これらのパフォーマンス指標を計測し、レポートするための関数を提供します。

通常、`reportWebVitals.ts`ファイルは、`Create React App` などのツールによってプロジェクトの初期設定時に自動的に生成されます。このファイルには、`reportWebVitals`という関数が含まれており、この関数を使用してWeb Vitalsの計測とレポートが行われます。

`reportWebVitals`関数は、`web-vitals`というパッケージを使用して、ブラウザのパフォーマンスAPIからWeb Vitalsのデータを収集します。収集されたデータは、開発者が指定した方法でレポートされます。デフォルトでは、コンソールにログが出力されますが、開発者は必要に応じてこの挙動をカスタマイズすることもできます。

`reportWebVitals.ts`ファイルは、Web Vitalsの計測とレポートを行うための重要なツールであり、パフォーマンスの改善やユーザーエクスペリエンスの向上に役立ちます。
:::

### App.tsx
主にルーティングの設定
```tsx:drf_react_frontend/src/App.tsx
import React from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import GeneralHeader from './pages/GeneralHeader';
import GeneralHomeView from './pages/GeneralHomeView';
import HomeView from './pages/daily_report/HomeView';
import DetailView from './pages/daily_report/DetailView';
import ProfileView from './pages/profile/ProfileView';
import Page404 from "./pages/Page404";

const App: React.FC = () => {
  return (
    <div>
      <Router>
        <GeneralHeader />
        <div>
          <Routes>
            <Route path="/" element={<GeneralHomeView />} />
            <Route path="/daily_report" element={<HomeView />} />
            <Route path="/daily_report/:id" element={<DetailView />} />
            <Route path="/profile" element={<ProfileView />} />
            <Route path="*" element={<Page404 />} />
          </Routes>
        </div>
      </Router>
    </div>
  );
};

export default App;
```

### DailyReport.tsx
- djangoの向き先を取りまとめる

```tsx:drf_react_frontend/src/daily_report/api/DailyReport.tsx
// apiにアクセスして日記のリストを取得します
export const getReportList = async () => {
  const res = await fetch('http://localhost:8000/daily_report/', {
    method: 'GET',
  });
  return await res.json();
};

// apiにアクセスして日記の詳細を取得します
export const getReportDetail = async (id: string) => {
    const res = await fetch( `http://localhost:8000/daily_report/${id}`, {
      method: 'GET',
    });
    if (res.ok) {
      return await res.json();
    } else {
      // 検索結果 0件
      throw new Error(`id: ${id} では日記を取得できませんでした`);
    }
};
```

:::note
- `http://localhost:3000/` というパスが指定されたら `Top` を表示
- `http://localhost:3000/daily` というパスが指定されたら `DailyTop` を表示
- `http://localhost:3000/daily/3` とかのパスが指定されたら `DailyDetail` を表示
- `http://localhost:3000/daily/category/univ` とかのパスが指定されたら `CategoryView` を表示
- どれにも当てはまらなかったら `not found...` と表示
:::

### GeneralHeader.tsx
:::note
gpt: Reactコンポーネントを含むファイルは `.tsx` ファイルを使用し、それ以外のTypeScriptファイルは `.ts` ファイルを使用します。

---

まぁ `.ts` にするとIDEがエラー出すはず
:::
:::note
gpt: `React.FC` を使用することで、関数コンポーネントのpropsの型を指定することができます。
:::

```tsx:drf_react_frontend/src/pages/GeneralHeader.tsx
import React from 'react';
import { Link } from 'react-router-dom';

const GeneralHeader: React.FC = () => {
  return(
    <div>
      <Link to='/'>Top</Link>
      <ul>
        <li><Link to='/daily_report'>日報</Link></li>
        <li><Link to='/profile'>プロフィール</Link></li>
      </ul>
    </div>
  )
}

export default GeneralHeader
```

### GeneralHomeView.tsx
```tsx:drf_react_frontend/src/pages/GeneralHomeView.tsx
import React from 'react';

const GeneralHomeView: React.FC = () => {
  return (
    <div>
      <h1>総合Topです</h1>
      <p>このアプリケーションは...</p>
      <ul>
        <li>あれができます</li>
        <li>これができます</li>
      </ul>
    </div>
  );
};

export default GeneralHomeView
```

### HomeView.tsx
- 記事のリストを持ってきて map 関数でくるくると Conponent に引き渡している
```tsx:drf_react_frontend/src/pages/daily_report/HomeView.tsx
import React, { useState, useEffect } from 'react';
import { getReportList } from '../../daily_report/api/DailyReport';
import { Content } from '../../daily_report/components/home/Content';
import { HomeContentEntity } from "../../daily_report/domain/HomeContentEntity";

const HomeView = () => {
  const [entities, setEntities] = useState<HomeContentEntity[]>([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    getReportList()
      .then((values: HomeContentEntity[]) => {
        setEntities(values);
        setLoading(false);
      })
  }, []);

  return (
    <div>
      { loading ? (
        <h1>loading...</h1>
      ) : (
        <div>
          { entities.map( entity => <Content key={ entity.id } { ...entity } /> ) }
        </div>
      ) }
    </div>
  );
};

export default HomeView
```

### DetailView.tsx
- url から `id` を取得して 単一の記事を取得して detail/content に引き渡している
```tsx:drf_react_frontend/src/pages/daily_report/DetailView.tsx
import React, { useState, useEffect } from 'react';
import { getReportDetail } from "../../daily_report/api/DailyReport";
import { Content } from "../../daily_report/components/detail/Content";
import { DetailContentEntity } from "../../daily_report/domain/DetailContentEntity";
import { useParams } from "react-router-dom";

const DetailView: React.FC = () => {
  const [entity, setEntity] = useState<DetailContentEntity>({} as DetailContentEntity)
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  // get url param
  const { id } = useParams<{ id: string }>();

  useEffect(() => {
    if (id) {
      getReportDetail(id)
        .then((values: DetailContentEntity) => {
          setEntity(values)
          setLoading(false)
        })
        .catch((e: Error) => {
          setError(e.message);
          setLoading(false);
        });
    }
  },[id])

  if (error) {
    return <h1>{error}</h1>;
  }

  return (
    <div>
      { loading ? (
        <h1>loading....</h1>
      ) : (
        <Content key={ entity.id } { ...entity } />
      ) }
    </div>
  );
};

export default DetailView
```

### ProfileView.tsx
```tsx:drf_react_frontend/src/pages/profile/ProfileView.tsx
import React from 'react';

const ProfileView: React.FC = () => {
  return (
    <div>
      <h1>Profileです</h1>
    </div>
  );
};

export default ProfileView
```

### Page404.tsx
```tsx:drf_react_frontend/src/pages/Page404.tsx
import React from 'react';
import { Link } from 'react-router-dom';

const Page404 = () => {
  return (
    <>
      <h1>404 NOT FOUND</h1>
      <p>お探しのページが見つかりませんでした。</p>
      <Link to="/">Topに戻る</Link>
    </>
  );
};

export default Page404;
```

### home/Content.tsx
```tsx:drf_react_frontend/src/daily_report/components/home/Content.tsx
import React from 'react';
import { Link } from 'react-router-dom';

import img1 from '../../images/home/perfect.png';
import img2 from '../../images/home/good.png';
import img3 from '../../images/home/soso.png';
import img4 from '../../images/home/bad.png';
import { HomeContentEntity } from "../../domain/HomeContentEntity";

export const Content = (entity: HomeContentEntity) => {
  let evaluation_img;
  if (entity.evaluation === 'perfect') {
    evaluation_img = img1;
  } else if (entity.evaluation === 'good') {
    evaluation_img = img2;
  } else if (entity.evaluation === 'soso') {
    evaluation_img = img3;
  } else {
    evaluation_img = img4;
  }

  return (
    <div>
      <Link to={ `${ entity.id }` } >
        <h2>{ entity.date }</h2>
      </Link>
      <img src={ evaluation_img } alt="evaluation" />
    </div>
  );
};
```

### detail/Content.tsx
```tsx:drf_react_frontend/src/daily_report/components/detail/Content.tsx
import React from 'react';

import { DetailContentEntity } from "../../domain/DetailContentEntity";

export const Content = (entity: DetailContentEntity) => {
  return (
    <div>
      <h1>{ entity.date }</h1>
      <h2>大学のこと</h2>
      <div>{ entity.univ }</div>
      <h2>勉強</h2>
      <div>{ entity.study }</div>
      <h2>その他</h2>
      <div>{ entity.other }</div>
      <h2>初めましてだったこと</h2>
      <div>{ entity.first_meet }</div>
      <h2>やりたいこと</h2>
      <div>{ entity.wanna_do }</div>
      <h2>1日のまとめ</h2>
      <div>{ entity.summary }</div>
    </div>
  );
};
```

### HomeContentEntity.tsx
```tsx:drf_react_frontend/src/daily_report/domain/HomeContentEntity.ts
export type HomeContentEntity = {
  id: string,
  date: string,
  evaluation: string,
};
```

### DetailContentEntity.tsx
```tsx:drf_react_frontend/src/daily_report/domain/DetailContentEntity.ts
export type DetailContentEntity = {
  id: string,
  date: string,
  univ: string,
  study: string,
  other: string,
  first_meet: string,
  wanna_do: string,
  summary: string,
};
```

### 確認
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/8eb141c4-8ba3-94fc-523c-2e2f09698c6d.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/fba7ea91-f22c-bc13-2db8-ca28326a1c19.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/e283233d-a046-457b-45be-d29aeded0dcb.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/117082aa-8ede-417b-d885-42aebd0d3fb0.png)