簡単なマッチングアプリをDjangoとReactを使用して開発したので記録を残します。(※解説は未完成ですが、順次追加して更新していきます)以下にGitHubのリンクを貼っています。
今回アプリを開発するにあたって以下の記事を大いに参考にさせていただいたため、ここで感謝をお伝えすると同時に、まずは以下の記事を読んでいただけると助かります。
WEBアプリ開発の全体像
WEBアプリ開発と聞くと非常に難しそうなイメージがありますが、意外とシンプルです。僕たちが見ているPCの画面はデータがペタペタ貼られているだけです。どこに、どんなデータを貼るかを決めて実装するのがWEBアプリ開発です。もう少し具体的に説明すると、どのURLに対してどの画面(HTML)を対応させるかを決めて、データベースから欲しい情報を持ってきて画面に貼り付けたり、フォームに入力されたデータをデータベースに追加したりしているのです。
コードレベルまで具体化して説明すると、バックエンドでは、データベースの設計とAPIのエンドポイントを定義します。データベースとは、ユーザー情報(メールアドレス、ユーザー名など)を格納するものや、プロフィール(年齢、趣味など)を格納するもの、ダイレクトメッセージの情報(誰が、誰に、どんなメッセージを、いつ送っているのか)を格納するものなどがあって、これらの情報が定義された箱のようなものです。
APIエンドポイントというのは、http://127.0.0.1:8000/api/user/create
のようなものです。例えば、ユーザーの新規登録を行いたいときは、ユーザー情報と登録をしたいということと共にhttp://127.0.0.1:8000/api/user/create
にアクセスしたら、ユーザー情報がデータベースに登録されます。このように、画面の操作とデータベースの懸け橋となってくれています。
フロントエンドは、このAPIエンドポイントを駆使して、どの画面にどの情報をどのように表示させるかを決めたり、どのようなフォームを作ってデータベースに情報を追加するか決めたりしています。
環境構築
仮想環境とは...?
仮想環境というのは、一つのコンピューターに別のコンピューターを作ることです。
仮想環境を作る理由は主に3つあります。
- 各プロジェクトが必要とするライバラりのバージョンを個別に管理できるから
PCに直接ライブラリーをインストールしてバージョン管理をすると、あるプロジェクトではバージョン1.5のままでなければいけないのに、あるプロジェクトではバージョン1.6にアップデートしなくてはいけないといった競合が生じたときに対応が難しくなります。そこで、各プロジェクトごとに仮想環境を作ってバージョンを管理することが必要となってきます。 - 開発環境と本番環境を同じにしたいから
開発環境と本番環境が同じであるとは限りません。そのため、様々な本番環境を仮想的に自分のPCに構築して、正しく動作するかを確認する必要があります。 - 開発環境の共有が楽だから
共同で開発をする際に、自分の環境と全く同じ環境を共同開発仲間に設定してもらうのはめんどくさいです。仮想環境を作って仮想環境を共有するば簡単に同じ環境で共同開発ができます。
仮想環境の種類
詳細の説明は省略します。(今後追加するかも...)
仮想マシン
コンピューター上のコンピューターのイメージです。ハードウェアレベルの仮想化技術です。
コンテナ(docker)
OSレベルの仮想化技術です。
anacondaで仮想環境をつくる
ANACONDA.NAVIGATORのEnviromentsでCreateを押して、Nameを決めて、PackageはPythonを選択してください。
PyCharmと仮想環境の紐づけ
- プロジェクトルートとなるフォルダを作成します
例:C:\Users\ユーザー名\projects\matching-app - PyCharmをインストールして起動します
- Openを押して作成したプロジェクトルートのフォルダを選択します
- ANACONDAで作成した環境を紐づけます
File>Setting>Project>Python Interpreter>Add Interpreter>Add Local Interpreter 作成した仮想環境を選択してください
これでひとまず環境構築は以上です。
バックエンド
それでは、さっそくバックエンドの開発をしていきます。バックエンドでは何をつくるか覚えているでしょうか。データベースとAPIエンドポイントです。このことを頭に置きながら開発を進めていきましょう。
プロジェクト作成とアプリケーションの追加
プロジェクト作成
ルートディレクトリ配下に
backend
とfrontend
というフォルダを作成
PyCharmのターミナルを開いてbackend
とfrontend
のフォルダを作成してください。mkdir backend mkdir frontend
プロジェクトを作成
djangoライブラリーをインストールして、プロジェクトを作成します。
プロジェクトを作成するコマンドは、django-admin startproject <プロジェクト名> <作成するディレクトリ>
です。<作成するディレクトリ>は省略可能ですが、.
を指定することで、カレントディレクトリ直下にプロジェクトを展開してくれるようになります。pip install django django-admin startproject matchingappapi .
アプリケーションの追加
任意の名前(以下ではmyapp
)でアプリを追加します
django-admin startappp myapp
サーバーの起動
- 左下の
manage.py
を右クリックして、run manage
をクリック - 右上の
manage
からEdit Configurations
を選択 parameter
にrunserver
と記述- 右上の再生ボタン(▶)を押すと、サーバーが起動してアプリが立ち上がる。
setting.pyの編集
環境変数の設定
環境変数はPCが上手く(設定通り)動くために必要な変数のことです。例えば、LANG
という環境変数は使用する言語を設定しています。
setting.py
にはSECRET_KEY
という環境変数が記述されています。SECRET_KEY
はユーザーが登録したパスワードを暗号化したりするなどセキュリティを担保するうえで用いられているため、この値が知られると、情報漏洩のリスクが急激に上がってしまいます。
そこで、SECRET_KEY
はハードコーディングせずに、.env
ファイルに記述して、setting.py
では.env
ファイルから呼び出して記述します。.env
を.gitiginore
に記述することで、GitHubにソースコードをアップロードしてもSECRET_KEY
は公開されないという状態を作り出せます。(※.gitignore
に記述されたファイルはGitの管理対象外となります)
(余談:暗号化とハッシュ化の違い→前者は可逆で元データを復元できる、後者は不可逆。)
backend
のフォルダ直下に.env
と.gitignore
ファイルを作成してください。
SECRET_KEY="ここにデフォルトでハードコーディングされていたSECRET_KEYを貼り付けてください"
.env
以外にもGitHubにのせたくないファイルは.gitignore
に記述します。
.env db.sqlite3 myapp/__pycache__/ myapp/migrations/__pycache__/ matchingappapi/__pycache__/
環境変数を扱うライブラリ(django-environ
)をインストールします
pip install django-environ
env = environ.Env() env.read_env(os.path.join(BASE_DIR, '.env')) SECRET_KEY = env('SECRET_KEY')
デフォルトの定数の説明と変更
BASE_DIR
基準となるディレクトリを定義します。ここでは、setting.py
のディレクトリの親の親のディレクトリ、つまり、manage.py
と同じ階層のディレクトリが指定されています。BASE_DIR = Path(__file__).resolve().parent.parent
DEBUG
DEBUG
をTrue
にすると、エラーが発生したときにエラーメッセージが表示されるために、開発環境ではTrue
にします。本番環境でもTrue
にすると、エラーメッセージから情報漏洩する可能性があるため、本番環境においてはFalse
にします。DEBUG = True
ALLOWED_HOSTS
Webサービスを配信するサーバーのドメイン名、IPアドレス、ホスト名をリストで指定します。
DEBUG=True
かつALLOWED_HOSTS=[]
の場合は、ALLOWED_HOSTS=['localhost, '127.0.0.1', '[::1]']
が自動的に有効になるため、空のリストのままのせて位で大丈夫です。
(余談:localhost
はドメイン名であって、自分自身を指すIPアドレス127.0.0.1
(IPv4の場合)、[:11]
(IPv6の場合)を指す。)ALLOWED_HOSTS=[]
INSTALLED_APPS
インストールしたり、作成たりしたアプリを追加。INSTALLED_APPS = [ 'myapp' #←これを追加 ]
ROOT_URLCONF
TEMPLATES
WSGI_APPLICATION
DATABASES
AUTH_PASSWORD_VALIDATORS
LANGUAGE_CODE
日本語に変更。LANGUAGE_CODE = "ja"
TIME_ZONE
東京に変更。TIME_ZONE = "Asia/Tokyo"
USE_I18N
USE_TZ
他にも変更する定数や、新たに追加する定数はありますが、今追加しても何のために必要なのかイメージしずらいと思いますので、必要となった時に適宜追加していきます。
models.pyの編集
モデルとは何か
UserManagerの設定
モデルの設定
コード
from django.contrib.auth.models import ( AbstractBaseUser, BaseUserManager, PermissionsMixin, ) from django.db import models from django.utils import timezone from django.conf import settings from django.core.validators import MinValueValidator, MaxValueValidator # ユーザーの作成、取得、更新、削除などのDB操作を抽象化したクラス class CustomUserManager(BaseUserManager): def create_user(self, email, password=None, **extra_fields): if not email: raise ValueError('The Email must be set') email = self.normalize_email(email) user = self.model(email=email, **extra_fields) user.set_password(password) user.save(using=self._db) return user def create_superuser(self, email, password=None, **extra_fields): extra_fields.setdefault('is_staff', True) extra_fields.setdefault('is_superuser', True) return self.create_user(email, password, **extra_fields) # ユーザー情報のDBの設計 class CustomUser(AbstractBaseUser, PermissionsMixin): email = models.EmailField(unique=True) is_active = models.BooleanField(default=True) is_staff = models.BooleanField(default=False) date_joined = models.DateTimeField(default=timezone.now) objects = CustomUserManager() USERNAME_FIELD = "email" # プロフィールのDB設計 class Profile(models.Model): user = models.OneToOneField(settings.AUTH_USER_MODEL, primary_key=True, on_delete=models.CASCADE, related_name='profile') last_name = models.CharField("姓", default="", max_length=100) first_name = models.CharField("名", default="", max_length=100) is_kyc = models.BooleanField("本人確認", default=False) age = models.PositiveSmallIntegerField( "年齢", default=20, validators=[ MinValueValidator(18, "18歳未満は登録できません"), MaxValueValidator(35, "36歳以上は登録できません") ] ) SEX = [('male', '男性'), ('female', '女性')] sex = models.CharField("性別", max_length=16, choices=SEX, default="") hobby = models.TextField("趣味", max_length=1000) elementary_school = models.CharField("小学校", max_length=100, blank=True, null=True, default="") middle_school = models.CharField("中学校", max_length=100, blank=True, null=True, default="") high_school = models.CharField("高校", max_length=100, blank=True, null=True, default="") university = models.CharField("大学", max_length=100, blank=True, null=True, default="") # 外出ボタンのDB設計 class GoOut(models.Model): user = models.OneToOneField(settings.AUTH_USER_MODEL, primary_key=True, on_delete=models.CASCADE, ) go_out = models.BooleanField("外出ボタン", default=False) # マッチング情報のDB設計 class Matching(models.Model): approaching = models.ForeignKey( settings.AUTH_USER_MODEL, related_name='approaching', default='', on_delete=models.CASCADE, ) approached = models.ForeignKey( settings.AUTH_USER_MODEL, related_name='approached', default='', on_delete=models.CASCADE, ) class Meta: unique_together = (('approaching', 'approached'),) # message情報のDB設計 class DirectMessage(models.Model): sender = models.ForeignKey( settings.AUTH_USER_MODEL, related_name='sender', on_delete=models.CASCADE, default='', ) receiver = models.ForeignKey( settings.AUTH_USER_MODEL, related_name='receiver', on_delete=models.CASCADE, default='', ) message = models.CharField(verbose_name="メッセージ", max_length=200, default='',) created_at = models.DateTimeField("送信時間", auto_now_add=True)
admin.pyの編集
管理画面とは?
コード
from django.contrib import admin from .models import CustomUser, Profile, GoOut ,Matching, DirectMessage class CustomUserAdmin(admin.ModelAdmin): list_display = ( "id", "email", "is_staff", "is_active", "date_joined", ) class ProfileAdmin(admin.ModelAdmin): list_display = ('user', 'last_name', 'first_name', 'is_kyc', 'age', 'sex', 'elementary_school', 'middle_school', 'high_school', 'university', ) class GoOutAdmin(admin.ModelAdmin): list_display = ('user', 'go_out',) class MatchingAdmin(admin.ModelAdmin): list_display = ('approaching', 'approached',) class DirectMessageAdmin(admin.ModelAdmin): list_display = ('sender', 'receiver', 'message', 'created_at') admin.site.register(CustomUser, CustomUserAdmin) admin.site.register(Profile, ProfileAdmin) admin.site.register(GoOut, GoOutAdmin) admin.site.register(Matching, MatchingAdmin) admin.site.register(DirectMessage, DirectMessageAdmin)
urls.pyの編集
URLのルーティングを決めます
コード(matchingappapi/urls.py)
from django.contrib import admin from django.urls import path, include urlpatterns = [ path("admin/", admin.site.urls), path('api/', include('myapp.urls')), path('authen/', include('djoser.urls.jwt')), ]
コード(myapp/urls.py)
from rest_framework.routers import DefaultRouter from django.urls import path, include from .views import (CreateUserView, UserView, ProfileViewSet, EditProfileView, OtherProfileViewSet, FavoriteProfileViewSet, MyGoOutViewSet, MyGoOutEditView, TrueGoOutUserViewSet, ApproachedMeViewSet, MyApproachingViewSet, ApproachingDeleteView, DirectMessageViewSet, InboxListView) app_name = 'myapp' router = DefaultRouter() router.register('profile', ProfileViewSet) router.register('other_profile', OtherProfileViewSet) router.register('favorite_profile', FavoriteProfileViewSet) router.register('goout', MyGoOutViewSet) router.register('true_goout', TrueGoOutUserViewSet) router.register('approached_me', ApproachedMeViewSet) router.register('my_favorite', MyApproachingViewSet) router.register('dm-message', DirectMessageViewSet) router.register('dm-inbox', InboxListView) urlpatterns = [ path('users/create', CreateUserView.as_view(), name='users-create'), path('users/<pk>', UserView.as_view(), name='users'), path('users/edit_profile/<pk>', EditProfileView.as_view(), name='users-profile'), path('users/goout/<pk>', MyGoOutEditView.as_view(), name='edit-goout'), path('delete_favorite', ApproachingDeleteView.as_view(), name='delete-favorite'), path('', include(router.urls)), ]
serializers.pyの編集
コード
from rest_framework import serializers from django.contrib.auth import get_user_model from .models import Profile, GoOut, Matching, DirectMessage class UserSerializer(serializers.ModelSerializer): class Meta: model = get_user_model() fields = ('email', 'password', 'id') extra_kwargs = {'password': {'write_only': True, 'min_length': 8}} def create(self, validated_data): user = get_user_model().objects.create_user(**validated_data) return user def update(self, use_instance, validated_data): for attr, value in validated_data.items(): if attr == 'password': use_instance.set_password(value) else: setattr(use_instance, attr, value) use_instance.save() return use_instance class ProfileSerializer(serializers.ModelSerializer): class Meta: model = Profile fields = ( 'user', 'last_name', 'first_name', 'is_kyc', 'age', 'sex', 'hobby', 'elementary_school', 'middle_school', 'high_school', 'university', ) extra_kwargs = {'user': {'read_only': True}} class GoOutSerializer(serializers.ModelSerializer): class Meta: model = GoOut fields = ('user', 'go_out', ) extra_kwargs = {'user': {'read_only': True}} class MatchingSerializer(serializers.ModelSerializer): class Meta: model = Matching fields = ('id', 'approaching', 'approached',) extra_kwargs = {'approaching': {'read_only': True}} class DirectMessageSerializer(serializers.ModelSerializer): class Meta: model = DirectMessage fields = ('id', 'sender', 'receiver', 'message','created_at', ) extra_kwargs = {'sender': {'read_only': True}}
views.pyの編集
コード
from rest_framework.generics import CreateAPIView, RetrieveUpdateAPIView, DestroyAPIView from rest_framework.permissions import AllowAny from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ReadOnlyModelViewSet from rest_framework.response import Response from rest_framework import status from django.db.models import Q from .models import CustomUser, Profile, GoOut, Matching, DirectMessage from .serializers import UserSerializer, ProfileSerializer, GoOutSerializer, MatchingSerializer, DirectMessageSerializer # ユーザーの登録 class CreateUserView(CreateAPIView): serializer_class = UserSerializer permission_classes = (AllowAny,) # 自分のユーザー情報(メールとパスワード)の取得と更新 class UserView(RetrieveUpdateAPIView): queryset = CustomUser.objects.all() serializer_class = UserSerializer def get_queryset(self): return self.queryset.filter(id=self.request.user.id) # 自分のプロフィールの取得と作成(GoOutはデフォルトでFalseが登録される) class ProfileViewSet(ModelViewSet): queryset = Profile.objects.all() serializer_class = ProfileSerializer def get_queryset(self): return self.queryset.filter(user=self.request.user) def perform_create(self, serializer): profile = serializer.save(user=self.request.user) go_out_defaults = { "user": profile.user, "go_out": False, } GoOut.objects.create(**go_out_defaults) return Response(serializer.data, status=status.HTTP_201_CREATED) # 自分のプロフィールの取得と編集 class EditProfileView(RetrieveUpdateAPIView): queryset = Profile.objects.all() serializer_class = ProfileSerializer def get_queryset(self): return self.queryset.filter(user=self.request.user) # 他の人のプロフィールの表示 class OtherProfileViewSet(ModelViewSet): queryset = Profile.objects.all() serializer_class = ProfileSerializer allowed_methods = ('GET',) def get_queryset(self): return self.queryset.exclude(pk=self.request.user.pk) # LIKEした人のプロフィールの表示 class FavoriteProfileViewSet(ModelViewSet): queryset = Profile.objects.all() serializer_class = ProfileSerializer allowed_methods = ('GET',) def get_queryset(self): # リクエストパラメータからユーザーIDのリストを取得 user_ids = self.request.query_params.getlist('user_ids') # ユーザーIDが存在しない場合は、空のリストを返す if not user_ids: return [] # 指定されたユーザーIDのプロフィールのみを取得 return self.queryset.filter(pk__in=user_ids) # 自分のGoOutの状態と取得と更新 class MyGoOutViewSet(ModelViewSet): queryset = GoOut.objects.all() serializer_class = GoOutSerializer def get_queryset(self): return self.queryset.filter(user=self.request.user) # 自分のGoOut状態の変更と取得 class MyGoOutEditView(RetrieveUpdateAPIView): queryset = GoOut.objects.all() serializer_class = GoOutSerializer def get_queryset(self): return self.queryset.filter(user=self.request.user) # GoOutがTrueなユーザーの取得 class TrueGoOutUserViewSet(ModelViewSet): queryset = GoOut.objects.all() serializer_class = GoOutSerializer def get_queryset(self): return super().get_queryset().filter(go_out=True) # 自分がアプローチされているリストの取得と作成 class ApproachedMeViewSet(ModelViewSet): queryset = Matching.objects.all() serializer_class = MatchingSerializer def get_queryset(self): return self.queryset.filter(approached=self.request.user) def perform_create(self, serializer): # POSTリクエストを送った時にapproachingフィールドに、requestユーザーを登録する serializer.save(approaching=self.request.user) # 自分がアプローチしているリストの取得と作成 class MyApproachingViewSet(ModelViewSet): queryset = Matching.objects.all() serializer_class = MatchingSerializer def get_queryset(self): return self.queryset.filter(approaching=self.request.user) def perform_create(self, serializer): serializer.save(approaching=self.request.user) class ApproachingDeleteView(DestroyAPIView): queryset = Matching.objects.all() serializer_class = MatchingSerializer def get_object(self): # リクエストパラメータから approached を取得 approached = self.request.GET.get('approached') if not approached: return Response({'error': 'approached パラメータが必要です'}, status=400) # リクエストを送信しているユーザーを取得 user = self.request.user # クエリセットをフィルタリング queryset = self.queryset.filter( Q(approaching=user.id) & Q(approached=approached) ) # フィルタリング結果からオブジェクトを取得 obj = queryset.get() return obj def get(self, request, *args, **kwargs): # リクエストパラメータから approached を取得 approached = request.GET.get('approached') if not approached: return Response({'error': 'approached パラメータが必要です'}, status=400) # リクエストを送信しているユーザーを取得 user = request.user # クエリセットをフィルタリング queryset = self.queryset.filter( Q(approaching=user.id) & Q(approached=approached) ) # シリアライザでクエリセットをシリアル化 serializer = self.serializer_class(queryset, many=True) # シリアル化されたデータを返す return Response(serializer.data) class DirectMessageViewSet(ModelViewSet): queryset = DirectMessage.objects.all() serializer_class = DirectMessageSerializer def get_queryset(self): receiver_id = self.request.query_params.get('receiver_id') return self.queryset.filter(Q(sender=self.request.user) & Q(receiver=receiver_id)) def perform_create(self, serializer): serializer.save(sender=self.request.user) class InboxListView(ReadOnlyModelViewSet): queryset = DirectMessage.objects.all() serializer_class = DirectMessageSerializer def get_queryset(self): sender_id = self.request.query_params.get('sender_id') return self.queryset.filter(Q(receiver=self.request.user) & Q(sender=sender_id))
フロントエンド
React環境の構築
App.jsの編集
import React from 'react'; import {BrowserRouter as Router, Route, Routes} from 'react-router-dom'; import SignUp from './components/SignUp'; import Login from './components/Login'; import Home from './components/Home'; import { CookiesProvider, withCookies} from 'react-cookie'; import CreateProfile from "./components/CreateProfile"; import EditProfile from "./components/EditProfile"; import Choice from "./components/Choice"; import Favorite from "./components/Favorite"; import DirectMessage from "./components/DirectMessage"; import Matching from "./components/Matching"; export const apiURL = 'http://127.0.0.1:8000'; const App = () => { return( <> <Router> <CookiesProvider> <Routes> <Route path="/" element={<SignUp />} /> <Route path="profile/create" element={<CreateProfile />} /> <Route path="profile/edit" element={<EditProfile />} /> <Route path="login" element={<Login />} /> <Route path="home" element={<Home />} /> <Route path="choice" element={<Choice />} /> <Route path="favorite" element={<Favorite />} /> <Route path="matching" element={<Matching />} /> <Route path="dm/:userId" element={<DirectMessage />} /> </Routes> </CookiesProvider> </Router> </> ) } export default withCookies(App)
新規登録画面
import React, {useState} from 'react'; import axios from 'axios'; import {Box, Button, Container, TextField, Typography} from '@mui/material' import { apiURL } from '../App' import {withCookies} from "react-cookie"; // ユーザーの新規登録 const SignUp = (props) => { const [ email, setEmail ] = useState(""); const [ password, setPassword ] = useState(""); const handleCreate = (event) => { event.preventDefault(); const form_data = new FormData(); form_data.append('email', email); form_data.append('password', password); axios.post(`${apiURL}/api/users/create`, form_data, { headers: { 'Content-Type': 'application/json' }, }) .then( res => { console.log(res.data) const certificationUri = `${apiURL}/authen/jwt/create`; axios.post(certificationUri, form_data, { headers: { 'Content-Type': 'application/json' }, }) .then( res => { props.cookies.set('token', res.data.access) console.log("success") window.location.href="/profile/create"; }) .catch( () => { console.log("certification error"); }); }) .catch( () => { console.log("error"); }); } return( <Container maxWidth="xs"> <Box sx={{ marginTop: 8, display: "flex", flexDirection: "column", alignItems: "center", }} > <Typography component="h2" variant="h5"> 新規登録 </Typography> <Box component="form" noValidate sx={{mt:1}} onSubmit={handleCreate}> <TextField margin="normal" required fullWidth id="email" label="メールアドレス" name="email" autoComplete="email" autoFocus value={email} onChange={(e) => {setEmail(e.target.value)}} /> <TextField margin="normal" required fullWidth name="password" label="パスワード" type="password" id="password" values={password} onChange={(e) => setPassword(e.target.value)} /> <Button type="submit" fullWidth variant="contained" sx={{mt:3, mb:2}} > 新規登録 </Button> <Button variant="text" href="/login" sx={{ mt: 1 }}> ログインはこちら </Button> </Box> </Box> </Container> ) } export default withCookies(SignUp)
プロフィールの新規作成
import { withCookies } from "react-cookie"; import React, { useState } from "react"; import { apiURL } from "../App"; import axios from "axios"; import {Box, Button, Container, Grid, InputLabel, MenuItem, Paper, Select, TextField} from "@mui/material"; const CreateProfile = (props) => { // Profileの情報を格納 const [last_name, setLast_name] = useState(""); const [first_name, setFirst_name] = useState(""); const [age, setAge] = useState(""); const [sex, setSex] = useState(""); const [hobby, setHobby] = useState(""); const [elementary_school, setElementary_school] = useState(""); const [middle_school, setMiddle_school] = useState(""); const [high_school, setHigh_school] = useState(""); const [university, setUniversity] = useState(""); const handleCreate = (event) => { event.preventDefault(); const form_data = new FormData(); form_data.append("is_kyc", true); form_data.append("last_name", last_name); form_data.append("first_name", first_name); form_data.append("age", age); form_data.append("sex", sex); form_data.append("hobby", hobby); form_data.append("elementary_school", elementary_school); form_data.append("middle_school", middle_school); form_data.append("high_school", high_school); form_data.append("university", university); axios.post(`${apiURL}/api/profile/`, form_data, { headers: { Authorization: `JWT ${props.cookies.get("token")}`, }, }) .then((res) => { console.log(res.data); window.location.href = "/home"; }) .catch((error) => { console.log(error); }); }; return ( <Container> <Box sx={{my:4}} component="h1"> プロフィール作成 </Box> <Paper sx={{p:10}}> <Box component="form" sx={{mt:1}} onSubmit={handleCreate}> <Grid> <TextField margin="normal" label="姓" id="last_name" name="last_name" value={last_name} onChange={(e) => setLast_name(e.target.value)} fullWidth required /> </Grid> <Grid> <TextField margin="normal" label="名前" id="first_name" name="first_name" value={first_name} onChange={(e) => setFirst_name(e.target.value)} fullWidth required /> </Grid> <Grid> <TextField margin="normal" label="年齢" id="age" name="age" value={age} onChange={(e) => setAge(e.target.value)} fullWidth required /> </Grid> <InputLabel id="sex-label">性別</InputLabel> <Select labelId="sex-label" id="sex" name="sex" value={sex} onChange={(e) => setSex(e.target.value)} label="性別" > <MenuItem value="male">男性</MenuItem> <MenuItem value="female">女性</MenuItem> </Select> <Grid> <TextField margin="normal" label="趣味" id="hobby" name="hobby" value={hobby} onChange={(e) => setHobby(e.target.value)} fullWidth required /> </Grid> <Grid> <TextField margin="normal" label="小学校" id="elementary_school" name="elementary_school" value={elementary_school} onChange={(e) => setElementary_school(e.target.value)} fullWidth required /> </Grid> <Grid> <TextField margin="normal" label="中学校" id="middle_school" name="middle_school" value={middle_school} onChange={(e) => setMiddle_school(e.target.value)} fullWidth required /> </Grid> <Grid> <TextField margin="normal" label="高校" id="high_school" name="high_school" value={high_school} onChange={(e) => setHigh_school(e.target.value)} fullWidth required /> </Grid> <Grid> <TextField margin="normal" label="大学" id="university" name="university" value={university} onChange={(e) => setUniversity(e.target.value)} fullWidth required /> </Grid> <Button type="submit" fullWidth variant="contained" sx={{mt:3, mb:2}} > 作成 </Button> <Button variant="text" href="/home" sx={{ mt: 1 }}> ホームに戻る </Button> </Box> </Paper> </Container> ); }; export default withCookies(CreateProfile);
ログイン画面
import React, { useState } from "react"; import axios from "axios"; import { Box, Button, Container, TextField, Typography, } from "@mui/material"; import { apiURL } from "../App"; import { withCookies } from "react-cookie"; // ユーザーのログイン処理 const Login = (props) => { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const handleLogin = (event) => { event.preventDefault(); let form_data = new FormData(); form_data.append("email", email); form_data.append("password", password); axios.post(`${apiURL}/authen/jwt/create`, form_data, { headers: { "Content-Type": "application/json", }, }) .then((res) => { props.cookies.set("token", res.data.access); window.location.href = "/home"; }) .catch(() => { console.log("error"); }); }; return ( <Container maxWidth="xs"> <Box sx={{ marginTop: 8, display: "flex", flexDirection: "column", alignItems: "center" }}> <Typography component="h2" variant="h5"> ログイン </Typography> <Box component="form" noValidate sx={{ mt: 1 }} onSubmit={handleLogin}> <TextField margin="normal" required fullWidth id="email" label="メールアドレス" name="email" autoComplete="email" autoFocus value={email} onChange={(e) => setEmail(e.target.value)} /> <TextField margin="normal" required fullWidth name="password" label="パスワード" type="password" id="password" values={password} onChange={(e) => setPassword(e.target.value)} /> <Button type="submit" fullWidth variant="contained" sx={{ mt: 3, mb: 2 }}> ログイン </Button> <Button variant="text" href="/" sx={{ mt: 1 }}> 新規登録はこちら </Button> </Box> </Box> </Container> ); }; export default withCookies(Login);
ホーム画面
import React, { useState, useEffect } from 'react'; import { withCookies } from 'react-cookie'; import axios from 'axios'; import { apiURL } from '../App'; import {Box, Button, Container, Grid, Link, Paper, Typography} from '@mui/material' const Home = (props) => { const [myProfileList, setMyProfileList] = useState([]); const editProfileDirectory = "/profile/edit" // 自分のプロファイルの取得 useEffect(() => { axios.get(`${apiURL}/api/profile`, { headers: { 'Authorization': `JWT ${props.cookies.get('token')}` } }) .then(res => { setMyProfileList(res.data); }) .catch(error => { console.error(error); }); }, [props.cookies]); return ( <Container maxWidth="sm"> <Box sx={{ my: 4 }}> <Typography variant="h4" component="h1" gutterBottom> Home </Typography> <Paper sx={{ p: 2 }}> {myProfileList.map((profile, index) => ( <Grid container key={index}> <Grid item xs={12}> <Typography variant="h6" component="h2"> こんにちは {profile.last_name} {profile.first_name}さん </Typography> </Grid> <Grid item xs={12} sx={{ mt: 2 }}> <Link href={editProfileDirectory}> <Button variant="outlined">プロフィール編集</Button> </Link> </Grid> </Grid> ))} </Paper> <Box sx={{ mt: 4 }}> <Grid container spacing={2}> <Grid item xs={12} sm={6}> <Link href="/choice"> <Button variant="contained" fullWidth>仲間探し</Button> </Link> </Grid> <Grid item xs={12} sm={6}> <Link href="/matching"> <Button variant="contained" fullWidth>マッチング成立</Button> </Link> </Grid> </Grid> </Box> <Box sx={{ mt:4 }}> <Grid> <Link href="/login"> <Button variant="outlined" fullWidth>ログアウト</Button> </Link> </Grid> </Box> </Box> </Container> ); } export default withCookies(Home);
プロフィールの編集
import { withCookies } from 'react-cookie'; import { Box, Button, Container, Grid, InputLabel, Link, MenuItem, Paper, Select, TextField, Typography } from "@mui/material"; import React, {useEffect, useState} from "react"; import {apiURL} from "../App"; import axios from "axios"; const EditProfile = (props) => { // Profileの情報を格納 const [user, setUser] = useState(""); const [last_name, setLast_name] = useState(""); const [first_name, setFirst_name] = useState(""); const [age, setAge] = useState(""); const [sex, setSex] = useState(""); const [hobby, setHobby] = useState(""); const [elementary_school, setElementary_school] = useState(""); const [middle_school, setMiddle_school] = useState(""); const [high_school, setHigh_school] = useState(""); const [university, setUniversity] = useState(""); const getProfile = () => { axios.get(`${apiURL}/api/profile`, { headers: { 'Authorization': `JWT ${props.cookies.get('token')}` } }) .then(res => { setUser(res.data[0].user) setLast_name(res.data[0].last_name) setFirst_name(res.data[0].first_name) setAge(res.data[0].age) setSex(res.data[0].sex) setHobby(res.data[0].hobby) setElementary_school((res.data[0].elementary_school)) setMiddle_school((res.data[0].middle_school)) setHigh_school((res.data[0].high_school)) setUniversity((res.data[0].university)) console.log(res.data) }) .catch(error => { console.error(error); }); } useEffect(() => { getProfile() }, []); const handleEdit = (event) => { event.preventDefault(); const form_data = new FormData(); form_data.append("is_kyc", true); form_data.append("last_name", last_name); form_data.append("first_name", first_name); form_data.append("age", age); form_data.append("sex", sex); form_data.append("hobby", hobby); form_data.append("elementary_school", elementary_school); form_data.append("middle_school", middle_school); form_data.append("high_school", high_school); form_data.append("university", university); axios.patch(`${apiURL}/api/users/edit_profile/${user}`, form_data, { headers: { Authorization: `JWT ${props.cookies.get("token")}`, }, }) .then((res) => { console.log(res.data); window.location.href = "/home"; }) .catch((error) => { console.log(error); }); } return( <Container maxWidth="md"> <Box sx={{my:4}} component="h1"> プロフィール編集 </Box> <Paper sx={{p:10}}> <Box component="form" sx={{mt:1}} onSubmit={handleEdit}> <Grid> <TextField margin="normal" label="姓" id="last_name" name="last_name" value={last_name} onChange={(e) => setLast_name(e.target.value)} fullWidth required /> </Grid> <Grid> <TextField margin="normal" label="名前" id="first_name" name="first_name" value={first_name} onChange={(e) => setFirst_name(e.target.value)} fullWidth required /> </Grid> <Grid> <TextField margin="normal" label="年齢" id="age" name="age" value={age} onChange={(e) => setAge(e.target.value)} fullWidth required /> </Grid> <InputLabel id="sex-label">性別</InputLabel> <Select labelId="sex-label" id="sex" name="sex" value={sex} onChange={(e) => setSex(e.target.value)} label="性別" > <MenuItem value="male">男性</MenuItem> <MenuItem value="female">女性</MenuItem> </Select> <Grid> <TextField margin="normal" label="趣味" id="hobby" name="hobby" value={hobby} onChange={(e) => setHobby(e.target.value)} fullWidth required /> </Grid> <Grid> <TextField margin="normal" label="小学校" id="elementary_school" name="elementary_school" value={elementary_school} onChange={(e) => setElementary_school(e.target.value)} fullWidth required /> </Grid> <Grid> <TextField margin="normal" label="中学校" id="middle_school" name="middle_school" value={middle_school} onChange={(e) => setMiddle_school(e.target.value)} fullWidth required /> </Grid> <Grid> <TextField margin="normal" label="高校" id="high_school" name="high_school" value={high_school} onChange={(e) => setHigh_school(e.target.value)} fullWidth required /> </Grid> <Grid> <TextField margin="normal" label="大学" id="university" name="university" value={university} onChange={(e) => setUniversity(e.target.value)} fullWidth required /> </Grid> <Button type="submit" fullWidth variant="contained" sx={{mt:3, mb:2}} > 変更 </Button> <Button variant="text" href="/home" sx={{ mt: 1 }}> ホームに戻る </Button> </Box> </Paper> </Container> ) } export default withCookies(EditProfile)
好きな人の選択画面
import { withCookies } from "react-cookie"; import {apiURL} from "../App"; import React, {useEffect, useState} from "react"; import axios from "axios"; import {Box, Button, Card, Container, Grid, Link, Paper, Table, Typography} from "@mui/material"; const Choice = (props) => { const [otherProfileList, setOtherProfileList] = useState([]); const [favoriteList, setFavoriteList] = useState([]) const getOtherProfile = () => { axios.get(`${apiURL}/api/other_profile`, { headers: { 'Authorization': `JWT ${props.cookies.get('token')}` } }) .then(res => { setOtherProfileList(res.data) console.log(res.data) }) .catch(error => { console.error(error); }); } const getFavorite = () => { axios.get(`${apiURL}/api/my_favorite/`, { headers:{ 'Authorization': `JWT ${props.cookies.get('token')}` } }) .then(res => { console.log(res.data) setFavoriteList(res.data.map(item => item.approached)) }) .catch(error => { console.log(error) }) } const handleLike = (profile) => { axios.post(`${apiURL}/api/my_favorite/`, {"approached":profile.user}, { headers:{ 'Authorization': `JWT ${props.cookies.get('token')}` } }) .then(res => { console.log(res.data) getFavorite() }) .catch(error => { console.log(error) }) } useEffect(() => { getOtherProfile() getFavorite() }, []); const handleDelete = (profile) => { axios.delete(`${apiURL}/api/delete_favorite?approached=${profile.user}`, { headers:{ 'Authorization': `JWT ${props.cookies.get('token')}` } }) .then(res => { console.log(res.data) getFavorite() }) .catch(error => { console.log(error) }) }; return ( <Container maxWidth="md"> <Box sx={{ my: 4 }}> <Typography variant="h4" component="h1" gutterBottom> 気になる人にLIKEボタンを押そう! </Typography> {otherProfileList.map((profile, index) => ( <Card sx={{ my:4 }} key={index}> <Grid container justifyContent="center" alignItems="center"> <Grid item xs={6}> <Table sx={{my:4, mx:4}}> <tbody> <tr> <th>名前</th> <td>{profile.last_name} {profile.first_name}</td> </tr> <tr> <th>年齢</th> <td>{profile.age}歳</td> </tr> <tr> <th>性別</th> <td>{profile.sex === "male" ? "男性" : "女性"}</td> </tr> <tr> <th>趣味</th> <td>{profile.hobby}</td> </tr> </tbody> </Table> </Grid> <Grid item xs={6}> {favoriteList.includes(profile.user) ? ( <div> <Button variant="outlined" onClick={() => handleDelete(profile)}> LIKEを取り消す </Button> </div> ) : ( <Button variant="contained" onClick={() => handleLike(profile)}> LIKE </Button> )} </Grid> </Grid> </Card> ))} <Box sx={{ mt: 2 }}> <Link href="/home"> <Button variant="outlined">Homeに戻る</Button> </Link> </Box> </Box> </Container> ) } export default withCookies(Choice)
LIKEを押した人の一覧
import { withCookies } from "react-cookie"; import {Box, Button, Card, Container, Grid, Link, Typography} from "@mui/material"; import React, {useEffect, useState} from "react"; import axios from "axios"; import {apiURL} from "../App"; const Favorite = (props) => { const [favoriteList, setFavoriteList] = useState([]) const [favoriteProfileList, setFavoriteProfileList] = useState([]); const getFavorite = () => { axios.get(`${apiURL}/api/my_favorite/`, { headers:{ 'Authorization': `JWT ${props.cookies.get('token')}` } }) .then(res => { setFavoriteList(res.data.map(item => item.approached)) }) .catch(error => { console.log(error) }) } const getFavoriteProfile = () => { const queryString = favoriteList.map(id => `user_ids=${id}`).join('&'); const getFavoriteUrl = `${apiURL}/api/favorite_profile?${queryString}`; axios.get(getFavoriteUrl, { headers: { 'Authorization': `JWT ${props.cookies.get('token')}` } }) .then(res => { setFavoriteProfileList(res.data) }) .catch(error => { console.error(error); }); } useEffect(() => { getFavorite() getFavoriteProfile() }, [favoriteList]); return( <Container maxWidth="md"> <Box sx={{my:4}}> <Typography variant="h4" component="h1" gutterBottom> LIKEした人 </Typography> </Box> {favoriteProfileList.map((profile, index) => ( <Card sx={{my:4}}> <Typography sx={{my:4, mx:4}}> {profile.last_name} {profile.first_name}さん </Typography> </Card> ))} <Box sx={{ mt: 2 }}> <Link href="/home"> <Button variant="outlined">Homeに戻る</Button> </Link> </Box> </Container> ) } export default withCookies(Favorite)
マッチング可否の画面
import {withCookies} from "react-cookie"; import {Box, Button, Card, Container, Grid, Link, Typography} from "@mui/material"; import React, {useEffect, useState} from "react"; import axios from "axios"; import {apiURL} from "../App"; const Matching = (props) => { const [favoriteList, setFavoriteList] = useState([]) const [approachedMeList, setApproachedMeList] = useState([]) const [matchingList, setMatchingList] = useState([]) const [matchingUserProfileList, setMatchingUserProfileList] = useState([]) // 自分がLIKEを押した人 const getFavorite = () => { axios.get(`${apiURL}/api/my_favorite/`, { headers:{ 'Authorization': `JWT ${props.cookies.get('token')}` } }) .then(res => { setFavoriteList(res.data.map(item => item.approached)) }) .catch(error => { console.log(error) }) } // 自分にLIKEを押してくれた人 const getApproachedMe = () => { axios.get(`${apiURL}/api/approached_me/`, { headers:{ 'Authorization': `JWT ${props.cookies.get('token')}` } }) .then(res => { setApproachedMeList(res.data.map(item => item.approaching)) }) .catch(error => { console.log(error) }) } // 両想いのユーザーのProfile const matchingProfile = () => { const queryString = matchingList.map(id => `user_ids=${id}`).join('&'); const getMatchingUrl = `${apiURL}/api/favorite_profile?${queryString}`; axios.get(getMatchingUrl, { headers: { 'Authorization': `JWT ${props.cookies.get('token')}` } }) .then(res => { setMatchingUserProfileList(res.data) }) .catch(error => { console.error(error); }); } useEffect(() => { getFavorite() getApproachedMe() // 両想いの人のフィルタリング setMatchingList(favoriteList.filter(element => approachedMeList.includes(element))) }, [approachedMeList]); useEffect(() => { matchingProfile() }, [matchingList]); return( <Container> <Box sx={{my:4}}> <Typography variant="h4" component="h4" gutterBottom> マッチングが成立した人 </Typography> {matchingUserProfileList.map((profile, index) => ( <Card sx={{my:4, maxWidth: 400}} key={index}> <Grid container justifyContent="center" alignItems="center"> <Grid item xs={6}> <Typography sx={{my:4, mx:4}}> {profile.last_name} {profile.first_name}さん </Typography> </Grid> <Grid item xa={6}> <Typography sx={{my:4, mx:4}}> <Link href={`/dm/${profile.user}`}> <Button>DM</Button> </Link> </Typography> </Grid> </Grid> </Card> ))} </Box> <Box sx={{ mt: 2 }}> <Link href="/home"> <Button variant="outlined">Homeに戻る</Button> </Link> </Box> </Container> ) } export default withCookies(Matching)
ダイレクトメッセージの画面
import { withCookies } from "react-cookie"; import {Box, Button, Divider, Grid, Link, List, ListItem, TextField, Typography} from "@mui/material"; import React, {useEffect, useRef, useState} from "react"; import { useParams } from "react-router-dom"; import { apiURL } from "../App"; import axios from "axios"; const DirectMessage = (props) => { const { userId } = useParams(); const [friendName, setFriendName] = useState(""); const [messages, setMessages] = useState([]); // Combined messages state const [input, setInput] = useState("") const fetchData = async () => { try { const friendResponse = await axios.get(`${apiURL}/api/favorite_profile?user_ids=${userId}`, { headers: { Authorization: `JWT ${props.cookies.get("token")}`, }, }); setFriendName(friendResponse.data[0].last_name); const sentMessagesResponse = await axios.get(`${apiURL}/api/dm-message?receiver_id=${userId}`, { headers: { Authorization: `JWT ${props.cookies.get("token")}`, }, }); const receivedMessagesResponse = await axios.get(`${apiURL}/api/dm-inbox?sender_id=${userId}`, { headers: { Authorization: `JWT ${props.cookies.get("token")}`, }, }); // Combine messages with proper sender identification const combinedMessages = [ ...sentMessagesResponse.data.map((message) => ({ ...message, isSent: true })), ...receivedMessagesResponse.data.map((message) => ({ ...message, isSent: false })), ]; // Sort messages chronologically by 'created_at' field combinedMessages.sort((a, b) => new Date(a.created_at) - new Date(b.created_at)); setMessages(combinedMessages); } catch (error) { console.error(error); } }; useEffect(() => { fetchData().then(r => console.log(r)); }, [userId]); const handleSendMessage = (event) => { event.preventDefault(); const form_data = new FormData(); form_data.append("message", input); form_data.append("receiver", userId); axios.post(`${apiURL}/api/dm-message/`, form_data, { headers:{ Authorization: `JWT ${props.cookies.get("token")}`, } }) .then(res => { console.log(res.data) fetchData().then(r => console.log(r)); setInput("") }) .catch(error => { console.log("e") }) } const styles = { container: { display: "flex", flexDirection: "column", height: "100vh", }, header: { padding: "16px", backgroundColor: "#f5f5f5", }, messageList: { flex: 1, overflow: "scroll", }, message: { padding: "8px 16px", maxWidth: "80%", borderRadius: "8px", backgroundColor: "#f5f5f5", }, myMessage: { backgroundColor: "#fff", alignSelf: "flex-end", }, inputArea: { padding: "16px", }, input: { width: "95%", padding: "8px 16px", borderRadius: "10px", // border: "1px solid #ccc", }, button: { marginRight: "10px", }, }; return ( <Box sx={styles.container}> <Box sx={styles.header}> <Typography variant="h6">{friendName}さんとのメッセージ</Typography> </Box> <Box sx={styles.messageList}> <List> {messages.map((message, index) => ( <ListItem key={index}> <Box sx={styles.message} className={message.isSent ? styles.myMessage : ""}> {message.isSent ? "あなた:" : `${friendName}:`}{message.message} </Box> <Divider /> </ListItem> ))} </List> </Box> <Box sx={styles.inputArea}> <Grid container justifyContent="center" alignItems="center"> <Grid item xs={10}> <TextField id="message" name="message" value={input} onChange={(e) => setInput(e.target.value)} sx={styles.input} multiline rows={1} /> </Grid> <Grid item xs={2}> <Button variant="contained" type="submit" sx={styles.button} onClick={handleSendMessage}> 送信 </Button> </Grid> </Grid> </Box> <Box sx={{ padding: "16px" }}> <Link href="/home"> <Button variant="outlined">Homeに戻る</Button> </Link> </Box> </Box> ); }; export default withCookies(DirectMessage);