LoginSignup
19
25

More than 5 years have passed since last update.

さくらのVPSで、https対応かつDjango+uWSGI+nginxなアプリのログイン機能を実装してみた

Last updated at Posted at 2018-07-20

はじめに

  • 簡単なWebアプリをつくるのにBottleを使っていたが、ログイン機能が欲しくなったので、Djangoを使ってみることにした
  • ついでに、手元に転がっていた「さくらのVPS」と「さくらのドメイン(仮称)」のサブドメインで公開するのに、uWSGIとnginxを使ってみることにした
  • 割と苦戦したが、なんとか出来たので、自分のメモとして記録しておく

概要

  • 全体的な流れは以下の通り
  1. Anacondaのインストール(pythonのインストール)
  2. Djangoのインストール
  3. uWSGIのインストール
  4. nginxのインストール
  5. 「さくらのvps」のサブドメインを設定する
  6. Django+uWSGI+nginxの設定と確認
  7. https対応
  8. ログイン機能の実装
  • たった8ステップ! これは簡単ですね!!(そんなわけはない)

用語とか

  • nginxは「エンジンエックス」と読む。読めるか、こんなもん。
  • uWSGIは「ウィスキー」と読む。なめとんのか。
  • Djangoは「ジャンゴ」と読む
  • Anacondaは「アナコンダ」と読む

前提

  • Ubuntu 16.04.4(さくらのVPS(v4) SSD 30 GB メモリ1G CPU2コアTK02)
  • Python 3.6.5
  • Django 2.0.7
  • uwsgi 2.0.17
  • nginx/1.10.3

下準備

  • 環境を最新にしておく
  • 久しぶりにやると長くかかる
  • 途中で、[Y/n]を聞かれたりもする(環境次第)
sudo apt update
sudo apt upgrade

Anacondaのインストール(pythonのインストール)

  • Anacondaのページに行って、linux版の最新版のダウンロードurlを「リンクのアドレスをコピー」で取得
  • あとは、wgetでファイルを取得してインストールコマンドを入力
wget https://repo.anaconda.com/archive/Anaconda3-5.2.0-Linux-x86_64.sh
bash Anaconda3-5.2.0-Linux-x86_64.sh
  • 上記のurlは記事作成時点の最新なので、適宜修正する
  • 最初はライセンスが表示されるので、スペースで読み進める
  • ライセンスに同意するか聞かれるので同意する
Do you accept the license terms? [yes|no]
[no] >>> yes
  • インストール場所を聞かれるので、特にこだわりがなければエンターで進める
  - Press ENTER to confirm the location
  - Press CTRL-C to abort the installation
  - Or specify a different location below

[/home/hoge/anaconda3] >>>
  • 環境変数にAnaconda3のパスをいれるか聞かれるので、好みによって、yesを入力する
  • 迂闊にyesにすると、curlがAnaconda環境のものを優先使用するようになったりするので、各自の判断でどうぞ
  • noを選んでも、「export PATH=(アナコンダをインストールしたパス)/anaconda3/bin:$PATH」でいつでも使用できるので特に気にしない
  • なんなら、あとで、「vi ~/.bashrc」して末尾に、「export PATH=(アナコンダをインストールしたパス)/anaconda3/bin:$PATH」加えて、「source ~/.bashrc」しても同じ
Do you wish the installer to prepend the Anaconda3 install location
to PATH in your /home/ubuntu/.bashrc ? [yes|no]
[no] >>> yes
  • Microsoft VSCodeをインストールするか聞かれるので、不要ならnoを入力する
  • vimで編集していくつもりなので、ここではno
Do you wish to proceed with the installation of Microsoft VSCode? [yes|no]
>>> no
  • 編集した.bashrcを有効にする
source ~/.bashrc
  • 一応、pythonのバージョン確認
python -V
  • 下のように表示されたら、anacondaのpythonが有効になっていて、今のバージョンは3.6.5だと判る
Python 3.6.5 :: Anaconda, Inc.

Djangoのインストール

  • Anacondaをインストールしたら、Djangoはインストールされているはずなので、バージョン確認
python -m django --version
2.0.7
  • 万が一、入っていなければ、pipでインストール
pip install django

django実装場所を作る(プロジェクトの作成)

  • プロジェクトを作成するコマンドを入力する
django-admin startproject mysite
  • mysiteディレクトリが出来ているはずなので移動
cd mysite
  • 基本ファイルは生成されており、以降基本的にこのディレクトリで作業する

Djangoによるwebサイトを確認する(開発サーバの起動)

  • まずアクセス先hostを指定する
vi mysite/settings.py
  • ALLOWED_HOSTに、そのアプリを表示しても良いhost名を書き込む
  • 例えば、さくらのVPSのアドレスがxxx.xxx.xxx.xxxなら、そう書く
  • 本当はデバッグ専用エリアに書き込むのが正しい気がするが、あとでサクッと削除するので今は良しとする
mysite/urls.py
ALLOWED_HOSTS = ['xxx.xxx.xxx.xxx']
  • さくらのVPSのアドレスは、SAKURA internetの[会員ログイン]-[契約情報]-[契約サービスの確認]の「概要」に書いてある
  • 組み込みサーバを起動
  • このときポートを指定(zzzz)
  • このときバインディングを指定することで、特定のサーバ(たとえば自分のIPアドレスyyy.yyy.yyy.yyy)からしか見えないようにするなら、以下のように打つ
python manage.py runserver yyy.yyy.yyy.yyy:zzzz
  • 多分、自分のIPアドレスを調べるのは面倒だろうから、バインディングは全てに許すように設定するとするならば以下のように打つ
python manage.py runserver 000.000.000.000
  • この例だと、「 http://xxx.xxx.xxx.xxx:zzzz/ 」にアクセスすると、「The install worked successfully! Congratulations!」的な画面が表示されているはず
  • 表示されていれば成功

uWSGIのインストール

-uWSGIをインストール

sudo pip install uwsgi
  • 「you need a C compiler to build uWSGI」的なエラーが出た場合は、以下をインストール
sudo apt install build-essential
  • バージョンを確認
uwsgi --version
2.0.17
  • 取り合えず最終的にはこれで動いたのでバージョンはこれで良しとする

nginxのインストール

  • nginxをインストール
sudo apt install nginx
  • ngixのバージョンを確認
sudo nginx -V
nginx version: nginx/1.10.3 (Ubuntu)
  • nginxが無事に動いているか確認
sudo systemctl status nginx
  • 動いてなければ、動かす
sudo systemctl start nginx
  • 再起動時に自動的に立ち上がるように設定する(念のため)
sudo systemctl enable nginx
  • もし、ufwを使っていたら、ufw のポートに穴をあける設定を打つ
sudo ufw allow 'Nginx Full'

「さくらのvpsのサブドメインを設定する

  • 普通のドメインなら、nginxのコンフィグをいじるだけで可能だが、「さくらのvps」でサブドメインを有効にするには、ワンステップが必要である
  • SAKURA internetの[会員ログイン]をクリック
  • [契約情報]をクリック
  • [契約ドメインの確認]をクリック
  • [ドメインメニュー]をクリック
  • [ゾーン編集]をクリック
  • [変更]をクリック
  • 「エントリ」に「サブドメイン名(例えばhoge)」、種別を「別名(CNAME)」、「値」を「@」、「DNSチェック」を「する」、「TTLの指定」をチェックしない状態に設定して、「新規登録」をクリック
  • 「データ送信」をクリック
  • もし、ドメインがmoge.comだったなら、hoge.moge.comが使えるようになったはず

Django+uWSGI+nginxの設定と確認

  • 順番につなげていく
  • まず、Django - uWSGIで接続確認をする
  • そのうえで、Django - uWSGI - nginxで接続確認する

Django - uWSGIで接続確認

  • さっきの「runserver」は終わらせておく。
sudo /home/hoge/anaconda3/bin/uwsgi --http :ZZZZ --module myweb.wsgi
  • この例だと、「 http://xxx.xxx.xxx.xxx:zzzz/ 」にアクセスすると、「The install worked successfully! Congratulations!」的な画面が表示されているはず
  • 表示されていれば成功

Django - uWSGI - nginxで接続確認

  • uWSGIとnginxはsocketでつなぐ
  • まず、nginxのuwsgiモジュール用の設定を行う
vi uwsgi_params
  • 内容は以下の通り編集する
uwsgi_param  QUERY_STRING       $query_string;
uwsgi_param  REQUEST_METHOD     $request_method;
uwsgi_param  CONTENT_TYPE       $content_type;
uwsgi_param  CONTENT_LENGTH     $content_length;

uwsgi_param  REQUEST_URI        $request_uri;
uwsgi_param  PATH_INFO          $document_uri;
uwsgi_param  DOCUMENT_ROOT      $document_root;
uwsgi_param  SERVER_PROTOCOL    $server_protocol;
uwsgi_param  REQUEST_SCHEME     $scheme;
uwsgi_param  HTTPS              $https if_not_empty;

uwsgi_param  REMOTE_ADDR        $remote_addr;
uwsgi_param  REMOTE_PORT        $remote_port;
uwsgi_param  SERVER_PORT        $server_port;
uwsgi_param  SERVER_NAME        $server_name;
  • つぎに、uWSGIの設定ファイルを作る
vi uwsgi.ini
  • 内容は以下の通り編集する
  • なお、ユーザ名は「hoge」とする
uwsgi.ini
[uwsgi]
uid = www-data
gid = www-data

chdir = /home/hoge/mysite
wsgi-file = /home/hoge/mysite/mysite/wsgi.py
module = mysite.wsgi.application
logto = /home/hoge/mysite/uwsgi-my_app.log

master = true
vacuum = true
pidfile = /var/run/uwsgi.webapppackage/master.pid
socket = /var/run/uwsgi.webapppackage/master.sock

processes = 5
die-on-term = true

touch-reload=/home/hoge/mysite/reload.trigger
lazy-apps = true
  • uidとgidは実行ユーザであり、www-dataにしないと後後ろくなことがない
  • socketが受け渡しに使われる情報で、こいつの権限がroot/rootになったりするとやっかいなので、uidとgidを設定した
  • touch-reloadを設定しておかないと、uWSGIにファイル更新のお知らせが出来なくて苦労するから作っておいた方が良い
  • さらに、nginxの設定ファイルを作る
vi mysite_nginx.conf
  • 内容は以下のように編集する
mysite_nginx.conf
server {
    listen zzzzz;
    server_name hoge.moge.com;
    charset     utf-8;

    error_log /home/hoge/mysite/mysite_error.log ;

    location /static {
        alias /home/hoge/mysite/static;
    }

    location / {
        include     /home/hoge/mysite/uwsgi_params;
        uwsgi_pass  unix:/var/run/uwsgi.webapppackage/master.sock;
    }
}
  • nginxは起動時に、ディレクトリ/etc/nginx/conf.d/以下の設定ファイルを読み込む
  • 先ほどのファイルをシンボリックファイルで、さも、前期ディレクトリ以下にあるような顔をさせる
sudo ln -s ~/mysite/mysite_nginx.conf /etc/nginx/conf.d/
  • Djangoのstaticファイルを/home/hoge/mysite/staticにしたので、mysite/settings.pyにもそう教える
vi mysite/settings.py
  • 以下のように編集(どっちかだけでいい気はする……)
mysite/urls.py
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, "static/")
  • staticの設定を反映する
python manage.py collectstatic
  • さらに、表示をゆるすhostを追加する
vi mysite/settings.py
  • 以下のように編集(xxx.xxx.xxx.xxxはけしても良い)
mysite/urls.py
ALLOWED_HOSTS = ['xxx.xxx.xxx.xxx','hoge.moge.com']
  • nginxとuWSGIを読み直す
sudo systemctl restart nginx
sudo /home/hoge/anaconda3/bin/uwsgi --ini uwsgi.ini
  • この例だと、「 http://hoge.moge.com:zzzz/ 」にアクセスすると、「The install worked successfully! Congratulations!」的な画面が表示されているはず
  • 表示されていれば成功
  • - ソケットを使っているので、もう「 http://xxx.xxx.xxx.xxx:zzzz/ 」にアクセスしても見えないはずなのがポイント

https対応

  • gitをインストール
sudo apt install git
  • gitはバージョンによっては最低限の設定をしないとgit clone出来ないので最低限の設定をする
git config --global user.name "First-name Family-name"
git config --global user.email "username@example.com"
  • gitでコミットした時に起動するエディタがnanoなのは初心者には不便なので、vimにする
  • ソースを書くときは出来る限りコミットは細かくして、コミット理由も日本語で良いので書いた方がいい
git config --global core.editor 'vim -c "set fenc=utf-8"'
  • ついでに、色も付けておく
  • 不要なら設定しなくても良い
git config --global color.diff auto
git config --global color.status auto
  • git cloneしてくる
git clone https://github.com/certbot/certbot /usr/local/bin/certbot
  • コマンド実行してSSL証明書を取得する
  • 自分のメールアドレスはhoge@moge.comと仮定
  • nginxは止めておく
sudo systemctl stop nginx
sudo /usr/local/bin/certbot/certbot-auto certonly --standalone -d hoge.moge.com -m hoge@moge.com --agree-tos -n
  • nginxの設定ファイルをSSL対応に書き換える
  • httpからhttpsに転送する設定も入れる
vi mysite_nginx.conf
  • 内容は以下のように編集する
mysite_nginx.conf
server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name hoge.moge.com;
    charset     utf-8;

    ssl_protocols TLSv1.2;
    ssl_ciphers HIGH:!MEDIUM:!LOW:!aNULL:!NULL:!SHA;
    ssl_prefer_server_ciphers on;
    ssl_session_cache shared:SSL:10m;

    ssl_certificate     /etc/letsencrypt/live/hoge.moge.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/hoge.moge.com/privkey.pem;

    eerror_log /home/hoge/mysite/mysite_error.log ;

    location /static {
        alias /home/hoge/mysite/static;
    }

    location / {
        include     /home/hoge/mysite/uwsgi_params;
        uwsgi_pass  unix:/var/run/uwsgi.webapppackage/master.sock;
    }
}

server {
    listen 80;
    listen [::]:80;
    server_name hoge.moge.com;
    location / { return 301 https://$host$request_uri; }
}
  • nginxとuWSGIを読み直す
sudo systemctl restart nginx
sudo /home/hoge/anaconda3/bin/uwsgi --ini uwsgi.ini
  • この例だと、「 https://hoge.moge.com/ 」にアクセスすると、「The install worked successfully! Congratulations!」的な画面が表示されているはず
  • 表示されていれば成功

ログイン機能の実装

アプリの作成

  • ダミーアプリと、ログイン機能だけのアプリを作成する
python manage.py startapp accounts
python manage.py startapp memo

アプリの登録

  • Djangoに、先ほど作成したアプリを登録する
vi mysite/settings.py
  • 以下のように編集
mysite/urls.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'memo',       #this one
    'accounts.apps.AccountsConfig',  #this one
]

ログインurlとログイン時のリダイレクト先の登録

  • ログインurlとログイン時のリダイレクト先も設定する
vi mysite/settings.py
  • 以下のように編集
  • urlはネームスペース+ネーム形式にした(意味は後述)
mysite/urls.py
LOGIN_URL          = 'register:login'
LOGIN_REDIRECT_URL = 'memo:index'

テンプレート置き場の登録

  • テンプレート置き場も設定する
vi mysite/settings.py
  • 設定しなくても大丈夫みたいだが、一応
mysite/urls.py
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR, 'templates')], #this one
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

基本ディスパッチャの登録

  • 基本ディスパッチャに設定する
vi mysite/urls.py
  • 必要なライブラリを読み込み、必要なパスを設定する
mysite/urls.py
from django.contrib import admin
from django.urls import path, include #1

urlpatterns = [
    path('admin/', admin.site.urls),
    path('accounts/', include('accounts.urls')), #2
    path('accounts/', include('django.contrib.auth.urls')),#3
    path('',include('memo.urls', namespace='memo')),#4
]
  • #1:ライブラリの読み込み
  • #2:サインイン用アプリのパス
  • #3: 組み込みログイン機能を利用するためのパス
  • #4: memoアプリ用のパス

memoアプリの設定:ディスパッチャ

  • memoアプリのディスパッチャは`memo/urls.py'
  • 無いから作る
vi memo/urls.py
  • 中身を編集する
memo/urls.py
from django.urls import path #1
from . import views  #2
app_name = 'memo'  #3
urlpatterns = [
    path('', views.IndexVew, name='index'),  #4
]
  • #1:ライブラリの読み込み
  • #2: 同じディレクトリのビューファイルをインポート
  • #3: アプリのネームスペースをmemoに設定
  • #4: 名前空間memoの、''に名前indexを設定し、それはビューファイルのIndexViewクラスを呼んで実行する
  • 次に#4で呼ぶべきビューファイルを作る

memoアプリの設定:ビュー

  • memoアプリのビューは`memo/views.py'
  • あるけど、「# Create your views here.」としか書かれていないので、作り直す
vi memo/views.py
  • 中身を編集する
memo/views.py
from django.shortcuts import render #1
from django.views.generic import TemplateView  #2
from django.contrib.auth.mixins import LoginRequiredMixin  #3

class userlist(LoginRequiredMixin, TemplateView): #4
    template_name = "memo/index.html"  #5

    def get(self, request, *args, **kwargs):
        context = super(userlist, self).get_context_data(**kwargs)
        context['text'] = 'Login OK!!' #6
        return render(self.request, self.template_name, context)
  • #1,#2,#3:ライブラリの読み込み
  • #4: LoginRequiredMixinを読み込むことで、ログインしないと見えないページに設定する
  • #5: テンプレート名
  • #6: テンプレートに渡すデータを設定
  • 次に#4で呼ぶべきテンプレートファイルを作る
  • なお、クラスではなく関数でページ表示する場合に、ログインしないと見えないページに設定するには「@login_required」修飾子を使う
  • 例は以下の通り
from django.http.response import HttpResponse
from django.contrib.auth.decorators import login_required

@login_required
def index(request):
    return HttpResponse('Login OK!!')

memoアプリの設定:テンプレート

  • いま設定したmemo/index.htmlを適切な位置に置かないとエラーになるので作る
  • templatesディレクトリは存在しないので掘る
  • memoが2回出てきて怪しいが、2回目のmemoはネームスペースを表す
  • Djangoは、アプリ中のtemplatesディレクトリ以下を捜査してヒットしたファイルを採用するので、例えば別のアプリでindex.htmlを使おうとしたら訳が分からなくなるので、このように書く
mkdir -p memo/templates/memo/
vi memo/templates/memo/index.html
memo/templates/memo/index.html
{% extends "base.html" %}
{% block title %}memo - index -{% endblock %}
{% block content %}
<div><h1>memo index</h1></div>
<div>{{ text }}</div>
{% endblock %}
  • {% extends "base.html" %}はベースファイルの読み込み
  • {% block title %}~{% endblock %}はベースファイルの{# block title %}{% endblock %}に置き換える値
  • {% block content %}~{% endblock %}はベースファイルの{# block content %}{% endblock %}に置き換える値
  • {{ text }}は、memo/views.pyで指定した、textを代入する部分
  • ついでに、base.htmlも作成する
mkdir templates
vi templates/base.html
  • 以下のように編集
{% load staticfiles %}
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <title>{% block title %}{% endblock %}</title>
  </head>
  <body>
    {% block content %}{% endblock %}
    {% include 'menu.html' %}
  </body>
</html>
  • {% load staticfiles %}はスタティックファイルを読み込む処理で、お約束で書く
  • {% include 'menu.html' %}は別のhtmlを読み込む処理
  • その、menu.htmlも作成する
vi templates/menu.html
  • 以下のように編集
{% load staticfiles %}
<ul>
  <li><a href="{% url 'account:signup' %}">signup</a></li>
  <li><a href="{% url 'login' %}">login</a></li>
  <li><a href="{% url 'logout' %}">logout</a></li>
  <li><a href="{% url 'memo:index' %}">top</a></li>
</ul>

accountsアプリの設定:ディスパッチャ

  • accountsアプリのディスパッチャは`accounts/urls.py'
  • 無いから作る
vi accounts/urls.py
  • 中身を編集する
accounts/urls.py
from django.urls import path
from . import views
app_name = 'accounts' 
urlpatterns = [
    path('signup/', views.SignUpView.as_view(), name='signup'),
]

accountsアプリの設定:ビュー

  • accountsアプリのビューは`accounts/views.py'
  • あるけど、「# Create your views here.」としか書かれていないので、作り直す
vi accounts/views.py
  • 中身を編集する
accounts/views.py
from django.contrib.auth.forms import UserCreationForm
from django.urls import reverse_lazy
from django.views import generic

class SignUpView(generic.CreateView): 
    form_class = UserCreationForm  
    success_url = reverse_lazy('login')  #1
    template_name = 'accounts/signup.html' 
  • #1: ユーザ作成成功時に、無名名前空間のloginに飛ぶことを表す(名前loginはデフォルトで設定されていて、include('gjango.contrib.auth.urls')を設定したパスの後ろにloginを付けたパスになる)

accountsアプリの設定:テンプレート

  • いま設定したaccounts/signup.htmlを適切な位置に置かないとエラーになるので作る
  • templatesディレクトリは存在しないので掘る
mkdir -p accounts/templates/accounts/
vi accounts/templates/accounts/signup.html
  • 中身を編集する
accounts/templates/accounts/signup.html
{% extends "base.html" %}
{% block title %}sign up{% endblock %}
{% block content %}
<h1>Sign up</h1>
<section class="common-form">
    <form method="post">
        {% csrf_token %}
        {{ form.as_p }}
        <button type="submit" class="submit">Sign up</button>
    </form>
</section>
{% endblock %}
  • {% csrf_token %}はCSRF(クロスサイトスクリプションリクエストフォージェリ)対策でformの後ろには必須
  • {{ form.as_p }}は accounts/views.pyのform_class で指定した値が入る

login/logoutのテンプレート

  • ログイン/ログアウト機能は、Django標準機能をそのまま使うので、urls.pyとviews.pyは不要
  • ただし、デフォルトで指定されている値に合わせてテンプレートを置く必要がある
  • デフォルト位置は templates/registrationである
mkdir -p  templates/registration
vi templates/registration/login.html

  • 中身を編集する
templates/registration/login.html
{% extends "base.html" %}
{% block title %}login{% endblock title %}
{% block content %}
{% if form.errors %}
<p>Your username and password didn't match. Please try again.</p>
{% endif %}

<form method="post" action="{% url 'login' %}">
{% csrf_token %}
<table>
<tr>
    <td>{{ form.username.label_tag }}</td>
    <td>{{ form.username }}</td>
</tr>
<tr>
    <td>{{ form.password.label_tag }}</td>
    <td>{{ form.password }}</td>
</tr>
</table>

<input type="submit" value="login" />
<input type="hidden" name="next" value="{{ next }}" />
</form>
{% endblock %}
  • logout用テンプレートも作成する
vi templates/registration/login.html

  • 中身を編集する
  • logged_out.htmlなので注意が必要
templates/registration/logged_out.html
{% extends 'base.html' %}
{% block title %}Logout{% endblock %}
<div><h1>Logged Out</h1></div>
<div>
<p>Thanks for spending some quality time with the Web site today.</p>
<p><a href="{% url 'login' %}">Log in again</a></p>
</div>
{% endblock %}

マイグレーションする

  • DB(今回はDjango標準のままなのでsqlite3)にモデルを反映したりするために、マイグレーションが必要
  • 今回はモデルを更新していないけど、組み込みのUserを使っているのでやっぱりマイグレーションは必要
  • モデルを変更したら、またmakemigrationsする必要がある
python manage.py makemigrations
  • 続いてマイグレーション
  • 構成変更したらまたこのコマンドをたたく必要がある
python manage.py migrate

管理者の作成

  • 一応、/admin/ではアクセスできる管理者ページは居れるユーザを作成する
python manage.py createsuperuser
  • usernameとpasswordを要求される
  • passwordは2回要求され、usernameと同じとかだと通らない

おわりに

  • 長かったケドやりたいことは出来た
  • 乱分乱筆なので、あとで修正をするかも
19
25
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
19
25