Django와 Python으로 개인 일기 만들기

2023. 12. 25. 20:16python/basic

프로젝트 개요

  1. Django 일기 프로젝트 설정
  2. 백엔드에 항목 만들기
  3. 프런트 엔드에 항목 표시
  4. 스타일링 추가
  5. 프런트 엔드에서 항목 관리
  6. 사용자 경험 개선
  7. 인증 구현

전제조건

1단계: Django 일기 설정

n [ ]:
!python -m pip install Django==3.2.1
 
 
!mkdir my-diary
%cd my-diary
!python3 -m venv .venv
source .venv/bin/activate

장고 초기화

In [ ]:
!pip install django
In [ ]:
!django-admin startproject diary .

위 명령 끝에 점(.)을 추가하는 것을 잊지 마세요. 점은 Django가 일기 프로젝트를 위한 다른 디렉토리를 생성하는 것을 방지합니다.

Django는 방금 manage.py 파일과 5개의 파일이 포함된 diary 폴더를 만들었습니다.

파일튜토리얼 중에 편집되었나요?
manage.py
diary/init.py
diary/asgi.py
diary/settings.py
diary/urls.py
diary/wsgi.py

데이터베이스 생성

Django는 여러 데이터베이스를 지원하며 SQLite와 함께 작동합니다. 다른 데이터베이스 구성이 제공되지 않은 경우 기본적으로 SQLite 데이터베이스입니다.

In [ ]:
!python manage.py migrate

프로젝트 디렉토리를 보면 db.sqlite3 파일이 보일 것입니다.

슈퍼유저가 되세요

In [ ]:
!python manage.py createsuperuser
 

Username (leave blank to use 'root'): admin
Email address: admin@example.com
Password: RealPyth0n
Password (again): RealPyth0n
Superuser created successfully.

개발 웹 서버 실행

In [ ]:
!python manage.py runserver

서버가 실행 중인 상태에서 http://127.0.0.1:8000 또는 http://localhost:8000를 사용하여 브라우저에서 Django 프로젝트를 방문할 수 있습니다.

중요: 브라우저에서 일기 프로젝트를 방문할 때마다 로컬 개발 웹 서버가 아직 실행되고 있지 않은 경우 먼저 시작해야 합니다.

http://localhost:8000/admin을 방문하고 자격 증명으로 로그인하여 이 튜토리얼의 첫 번째 단계를 완료하세요.

2단계: 백엔드에 일기 항목 추가

항목 앱 연결

터미널에서 Django 개발 웹 서버가 계속 실행 중일 수 있습니다. 터미널에서 Ctrl+C를 눌러 중지하세요.

팁: 한 창에서 서버를 제어하려면 두 번째 터미널 창을 열고 다음에서 다른 하나 명령을 실행하세요.

In [ ]:
!python manage.py startapp entries
파일튜토리얼 중에 편집되었나요?
entries/init.py
entries/admin.py
entries/apps.py
entries/models.py
entries/tests.py
entries/views.py

지금까지 Django는 방금 만든 앱을 알지 못합니다. entries 앱을 Django 일기 프로젝트에 연결하려면 diary/settings.py의 INSTALLED_APPS 목록 시작 부분에 구성 클래스 경로를 추가하세요.

In [ ]:
# diary/settings.py

INSTALLED_APPS = [  
  "entries.apps.EntriesConfig",  
  "django.contrib.admin",  
  "django.contrib.auth",  
  "django.contrib.contenttypes",  
  "django.contrib.sessions",  
  "django.contrib.messages",  
  "django.contrib.staticfiles",  
]

이제 entries 앱이 diary 프로젝트에 연결되고 Django가 해당 구성을 찾습니다.

항목 모델 만들기

이미 데이터베이스를 생성했습니다. 이제 일기 항목이 저장될 데이터베이스 테이블을 정의할 차례입니다. Django에서는 모델 클래스를 사용하여 이를 수행합니다. Python의 기존 클래스와 마찬가지로 모델 이름은 단수형이어야 하며 대문자여야 합니다. 앱이 entries 호출되는 동안 모델은 Entry 호출됩니다.

모델의 필드Entry는 일기 항목에 포함되는 요소입니다. 전면에는 해당 필드가 양식으로 표시됩니다. 뒤쪽에는 Entry 데이터베이스 테이블의 열이 됩니다. 이 튜토리얼의 일기 항목에는 세 가지 필드가 포함되어 있습니다:

  1. title은 헤드라인이다.
  2. content은 본문입니다.
  3. date_created은 생성 날짜 및 시간입니다.

entries/models.py에서 먼저 django.utils에서 timezone 가져오기 입니다. 그런 다음 아래와 같이 동일한 파일에 Entry 클래스를 만듭니다.

In [ ]:
# entries/models.py

from django.db import models
from django.utils import timezone

class Entry(models.Model):
    title = models.CharField(max_length=200)
    content = models.TextField()
    date_created = models.DateTimeField(default=timezone.now)

    def __str__(self):
        return self.title

    class Meta:
        verbose_name_plural = "Entries"

timezone 모듈을 가져오면 timezone.now를 default 인수로 사용할 수 있습니다.

엔트리 모델 등록

Django 관리 사이트에서 Entry 모델을 보려면 entries/admin.py:에 등록해야 합니다.

In [ ]:
# entries/admin.py

from django.contrib import admin
from .models import Entry

admin.site.register(Entry)

엔트리 모델 마이그레이션

In [ ]:
!python manage.py makemigrations
!python manage.py migrate

마이그레이션이 완료되면 개발 웹 서버를 실행하고 브라우저로 이동한 후 Django 관리 사이트(http://localhost:8000/admin:)를 방문하세요.

현재 나열된 항목이 없습니다. 항목 추가를 클릭하여 일기에 하나 이상의 항목을 작성하여 이 단계를 완료하세요.

3단계: 프런트 엔드에 일기 항목 표시

목록 및 세부정보 보기 만들기

Django에는 두 가지 종류의 함수 기반 뷰 및 클래스 기반 뷰가 있습니다. 둘 다 웹 요청을 받고 웹 응답을 반환합니다. 일반적으로 함수 기반 뷰는 더 많은 제어 기능을 제공하지만 더 많은 작업을 수행합니다. 클래스 기반 뷰를 사용하면 통제력이 줄어들고 작업량이 줄어듭니다.

In [ ]:
# entries/views.py

from django.views.generic import (
    ListView,
    DetailView,
)

from .models import Entry

class EntryListView(ListView):
    model = Entry
    queryset = Entry.objects.all().order_by("-date_created")

class EntryDetailView(DetailView):
    model = Entry

12행의 Entry.objects.all() 쿼리는 기본 키를 기준으로 정렬된 모든 항목을 반환합니다.

템플릿 만들기

In [ ]:
!mkdir -p entries/templates/entries

먼저 entry_list.html 생성하고 다음 콘텐츠를 추가하세요.

# HTML {% for entry in entry_list %} <article> <h2 class="{{ entry.date_created|date:'l' }}"> {{ entry.date_created|date:'Y-m-d H:i' }} </h2> <h3> {{ entry.title }} </h3> </article> {% endfor %}

Django 템플릿에서는 CSS 클래스를 동적으로 참조할 수도 있습니다. 5행의 <h2>을 보면 class="{{ entry.date_created|date:'l' }}"가 추가된 것을 볼 수 있습니다. 이는 특수 형식이 적용된 타임스탬프를 보여줍니다. 이렇게 하면 <h2> 요소에 평일이 클래스로 포함되며 나중에 CSS에서 평일마다 고유한 색상을 지정할 수 있습니다.

entries/templates/entries/에서 파일 이름을 entry_detail.html로 지정하고 다음 콘텐츠를 추가하세요.

# HTML <article> <h2>{{ entry.date_created|date:'Y-m-d H:i' }}</h2> <h3>{{ entry.title }}</h3> <p>{{ entry.content }}</p> </article>

뷰에 경로 추가

작동 중인 템플릿을 보려면 보기를 URL에 연결해야 합니다. Django는 urls.py 파일을 사용하여 브라우저에서 사용자로부터 들어오는 요청을 전달합니다. 이와 같은 파일이 diary 프로젝트 폴더에 이미 존재합니다. 항목 앱의 경우 먼저 entries/urls.py에서 만들고 EntryListView 및 EntryDetailView에 경로를 추가해야 합니다.

In [ ]:
# entries/urls.py

from django.urls import path

from . import views

urlpatterns = [
    path(
        "",
        views.EntryListView.as_view(),
        name="entry-list"
    ),
    path(
        "entry/<int:pk>",
        views.EntryDetailView.as_view(),
        name="entry-detail"
    ),
]

8행과 13행의 path() 함수에는 두 개 이상의 인수가 있어야 합니다.

  1. URL 패턴을 포함하는 경로 문자열 패턴
  2. 클래스 기반 뷰를 위한 as_view()함수인 view에 대한 참조
In [ ]:
# diary/urls.py

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path("admin/", admin.site.urls),
    path("", include("entries.urls")),
]

새 템플릿을 생성한 후 Django 개발 웹 서버를 수동으로 다시 시작하세요. 그런 다음 http://localhost:8000을 방문하여 감상해 보세요.

4단계: Django 일기장을 멋지게 보이게 만들기

{% load static %}

Dear diary …

{% block content %}{% endblock %}

템플릿 상속을 사용하면 템플릿에서 마크업을 반복할 필요가 없습니다. 대신 하위 템플릿을 확장합니다. 그런 다음 Django는 뷰에 서비스를 제공할 때 자동으로 병합합니다.

스타일시트 추가

템플릿 파일 시작 부분에 {% load static %}을 삽입하면 {% static %} 템플릿 태그와 CSS 파일의 상대 경로를 사용하여 정적 파일을 참조할 수 있습니다. entries/static/css/ 내에 diary.css를 만들고 아래 상자를 확장하여 파일에 추가할 CSS 코드를 확인하세요

# diary.css의 내용 /* entries/static/css/diary.css */ * { box-sizing: border-box; } body { font-family: sans-serif; font-size: 18px; } a { color: inherit; } a:hover { opacity: 0.7; } h1 { font-size: 2.8em; } h1 a { text-decoration: none; } h2, h3 { font-size: 1.4em; margin: 0; display: inline-block; padding: 0.5rem 1rem; vertical-align: top; } h2 { background-color: aquamarine; } .mark { background-color: gainsboro; } .mark a { text-decoration: none; } article { margin-bottom: 0.5rem; } p { font-size: 1.2em; padding-left: 1rem; line-height: 1.3em; max-width: 36rem; color: dimgray; } em { font-style: normal; font-weight: bold; } /* Form */ label { display: block; } button, textarea, input { font-size: inherit; min-height: 2.5em; padding: 0 1rem; } input[type="text"], textarea { width: 100%; } textarea { padding: 0.5rem 1rem; font-family: sans-serif; } button, input[type="submit"] { margin: 0 1rem 2px 1rem; cursor: pointer; font-weight: bold; min-width: 8rem; } /* Day coloring */ .Saturday, .Sunday { background-color: lightsalmon; }

하위 템플릿 확장

이제 하위 템플릿을 base.html 상위 템플릿과 연결할 차례입니다. entries/templates/entries/entry_list.html 다음과 같이 업데이트하세요.

In [ ]:
<!-- entries/templates/entries/entry_list.html -->

{% extends "entries/base.html" %}

{% block content %}
    {% for entry in entry_list %}
        <article>
            <h2 class="{{ entry.date_created|date:'l' }}">
                {{ entry.date_created|date:'Y-m-d H:i' }}
            </h2>
            <h3>
                <a href="{% url 'entry-detail' entry.id %}">
                    {{ entry.title }}
                </a>
            </h3>
        </article>
    {% endfor %}
{% endblock %}

동일한 작업을 entries/templates/entries/entries_detail.html에 하세요.

In [ ]:
<!-- entries/templates/entries/entries_detail.html -->

{% extends "entries/base.html" %}

{% block content %}
    <article>
        <h2>{{ entry.date_created|date:'Y-m-d H:i' }}</h2>
        <h3>{{ entry.title }}</h3>
        <p>{{ entry.content }}</p>
    </article>
{% endblock %}

5단계: 프런트 엔드에서 일기 항목 관리하기

웹 앱을 구축하고 사용할 때 항상 수행하는 네 가지 기본 작업이 있습니다. 이러한 작업은 매우 일반적이어서 CRUD 약어로 참조되는 경우가 많습니다.

  • Create
  • Read
  • Update
  • Delete

뷰 추가

In [ ]:
# entries/views.py

from django.urls import reverse_lazy
from django.views.generic import (
    ListView,
    DetailView,
    CreateView,
    UpdateView,
    DeleteView,
)

하위 클래스 3개를 entries/views.py 하단에 추가하세요.

In [ ]:
# entries/views.py

class EntryCreateView(CreateView):
    model = Entry
    fields = ["title", "content"]
    success_url = reverse_lazy("entry-list")

class EntryUpdateView(UpdateView):
    model = Entry
    fields = ["title", "content"]

    def get_success_url(self):
        return reverse_lazy(
            "entry-detail",
            kwargs={"pk": self.entry.id}
        )

class EntryDeleteView(DeleteView):
    model = Entry
    success_url = reverse_lazy("entry-list")

템플릿 만들기

Django는 특정 이름을 가진 템플릿을 찾습니다.

  • EntryDeleteView의 경우 entry_confirm_delete.html입니다.
  • EntryCreateView의 경우 entry_form.html입니다.
  • EntryUpdateView의 경우는 entry_update_form.html일 것입니다 .

Django가 entry_update_form.html를 찾지 못하면 대체 방법으로 entry_form.html를 시도합니다. entries/templates/entries/에서 두 보기를 모두 처리하는 템플릿을 만들고 기본 제출 양식을 추가하면 이 기능을 활용할 수 있습니다.

In [ ]:
# HTML

<!-- entries/templates/entries/entry_form.html -->

{% extends "entries/base.html" %}
{% block content %}
    <form method="post">
        {% csrf_token %}
        {{ form.as_p }}
        <input type="submit" value="Save">
    </form>
    {% if entry %}
        <a href="{% url 'entry-detail' entry.id %}">
            <button>Cancel</button>
        </a>
    {% else %}
        <a href="{% url 'entry-list' %}">
            <button>Cancel</button>
        </a>
    {% endif %}
{% endblock %}

템플릿에서 양식을 렌더링하는 방법에는 여러 가지가 있습니다. 7행의 {{ form.as_p }}을 사용하면 Django는 단락으로 묶인 뷰에 정의한 필드를 표시합니다. Django 양식에 콘텐츠를 게시할 때마다 6행에 {% csrf_token %} 템플릿 태그도 포함해야 합니다. 이는 교차 사이트 요청 위조를 방지하기 위한 보안 조치입니다.
다른 템플릿과 마찬가지로 3행에 {% extends "entries/base.html" %}을 추가하여 기본 템플릿을 확장합니다. 그런 다음 4행과 18행 사이의 block content 태그에 포함할 내용을 정의합니다. entries/templates/entries/의 entry_confirm_delete.html에 대해 동일한 패턴을 사용합니다.

In [ ]:
# HTML

<!-- entries/templates/entries/entry_confirm_delete.html -->

{% extends "entries/base.html" %}
{% block content %}
    <form method="post">{% csrf_token %}
        <p>
            Are you sure you want to delete
            <em>"{{ entry.title }}"</em>
            created on {{ entry.date_created|date:'Y-m-d' }}?
        </p>
        <input type="submit" value="Confirm">
    </form>
    <a href="{% url 'entry-detail' entry.id %}">
        <button>Cancel</button>
    </a>
{% endblock %}

URL 만들기

뷰와 해당 템플릿을 생성한 후 해당 경로를 생성하여 프런트 엔드에서 액세스하세요. entries/urls.py의 urlpatterns에 세 개의 추가 경로를 추가하십시오.

In [ ]:
# entries/urls.py

urlpatterns = [
    path(
        "",
        views.EntryListView.as_view(),
        name="entry-list"
    ),
    path(
        "entry/<int:pk>",
        views.EntryDetailView.as_view(),
        name="entry-detail"6단계: 사용자 경험 개선
    ),
    path(
        "create",
        views.EntryCreateView.as_view(),
        name="entry-create"
    ),
    path(
        "entry/<int:pk>/update",
        views.EntryUpdateView.as_view(),
        name="entry-update",
    ),
    path(
        "entry/<int:pk>/delete",
        views.EntryDeleteView.as_view(),
        name="entry-delete",
    ),
]

6단계: 사용자 경험 개선

성공을 처리하라

In [ ]:
# entries/views.py

from django.contrib import messages
from django.contrib.messages.views import SuccessMessageMixin
In [ ]:
# entries/views.py

class EntryCreateView(SuccessMessageMixin, CreateView):
    model = Entry
    fields = ["title", "content"]
    success_url = reverse_lazy("entry-list")
    success_message = "Your new entry was created!"

class EntryUpdateView(SuccessMessageMixin, UpdateView):
    model = Entry
    fields = ["title", "content"]
    success_message = "Your entry was updated!"

    def get_success_url(self):
        return reverse_lazy(
            "entry-detail",
            kwargs={"pk": self.object.pk}
        )
In [ ]:
# entries/views.py

class EntryDeleteView(DeleteView):
    model = Entry
    success_url = reverse_lazy("entry-list")
    success_message = "Your entry was deleted!"

    def delete(self, request, *args, **kwargs):
        messages.success(self.request, self.success_message)
        return super().delete(request, *args, **kwargs)

메시지 받기

In [ ]:
# HTML

<!-- entries/base.html -->

<h1><a href="/">Dear diary …</a></h1>

{% if messages %}
    <ul class="messages">
    {% for message in messages %}
        <li class="message">
            {{ message }}
        </li>
    {% endfor %}
    </ul>
{% endif %}

{% block content %}{% endblock %}

탐색 개선

In [ ]:
# HTML

<!-- entries/templates/entries/entry_list.html -->

{% block content %}
<article>
    <h2 class="mark">{% now "Y-m-d H:i" %}</em></h2>
    <a href="{% url 'entry-create' %}"><button>Add new entry</button></a>
</article>

{% for entry in entry_list %}
In [ ]:
# HTML

<!-- entries/templates/entries/entry_detail.html -->

</article>
<p>
    <a href="{% url 'entry-update' entry.id %}">✍️ Edit</a>
    <a href="{% url 'entry-delete' entry.id %}">⛔ Delete</a>
</p>
{% endblock %}

메시지 스타일을 지정하세요

마지막으로 .messages 및 .message를 entries/static/css/diary.css 끝에 추가하여 플래시 메시지의 스타일을 지정합니다.

In [ ]:
# CSS

/* entries/static/css/diary.css */

/* Messages */

.messages {
    padding: 0;
    list-style: none;
}

.message {
    width: 100%;
    background: lightblue;
    padding: 1rem;
    text-align: center;
    margin: 1rem 0;
}

터미널에서 Ctrl+C를 눌러 개발 웹 서버를 중지하고 다시 시작하세요. 그런 다음 http://localhost:8000을 방문하여 변경사항이 실제로 적용되는 것을 확인하십시오. 메시지에 스타일이 적용되지 않은 것처럼 보이면 브라우저의 캐시를 지워 스타일시트 변경 사항을 다시 로드해야 할 수 있습니다.

7단계: Django Diary에 자물쇠 걸기

Django 관리자 로그인 재사용

Django가 제공하는 인증 시스템은 매우 기본적입니다. 일반 사용자가 참여하는 다른 프로젝트의 경우 사용자 정의를 고려할 수 있습니다. 하지만 Django 일기의 경우 Django 관리 사이트의 로그인을 재사용하는 것으로 충분합니다.

로그아웃 링크 추가

특수 URL을 방문하여 로그아웃했습니다. Django는 Django 관리 사이트에서 로그아웃하기 위해 admin:logout라는 이름의 URL을 이미 제공하고 있습니다. 로그인한 후 프로젝트의 어느 곳에서나 이 URL을 방문하면 Django가 자동으로 로그아웃됩니다. 이 URL에 빠르게 액세스하려면 entries/templates/entries/base.html: 하단에 링크를 추가하세요.

In [ ]:
# HTML

<!-- entries/templates/entries/base.html -->

{% load static %}
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>My Diary</title>
    <link rel="stylesheet" href="{% static 'css/diary.css' %}">
</head>

<body>
<h1><a href="/">Dear diary …</a></h1>

{% if messages %}
    <ul class="messages">
    {% for message in messages %}
        <li class="message">
            {{ message }}
        </li>
    {% endfor %}
    </ul>
{% endif %}

{% block content %}{% endblock %}

<hr>
<a href="{% url 'admin:logout' %}">Logout</a>
</body>

</html>

뷰에 대한 액세스 제한

In [ ]:
# entries/views.py

from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib import messages
from django.contrib.messages.views import SuccessMessageMixin
from django.urls import reverse_lazy
from django.views.generic import (
    ListView,
    DetailView,
    CreateView,
    UpdateView,
    DeleteView,
)

from .models import Entry

뷰 클래스가 LoginRequiredMixin을 사용하면 먼저 성공적인 로그인이 필요합니다. 또한 login_url를 정의해야 Django가 로그인하지 않았을 때 리디렉션할 위치를 알 수 있습니다. 모든 클래스에 대해 개별적으로 이 작업을 수행하는 대신 기본 클래스는 LoginRequiredMixin를 상속하고 entries/views.py 에서 login_url를 정의합니다:

In [ ]:
# entries/views.py

class LockedView(LoginRequiredMixin):
    login_url = "admin:login"

이제 인증된 사용자만 액세스할 수 있는 다른 모든 보기에서 LockedView를 상속할 수 있습니다. 수업이 다음과 같이 보이도록 entries/views.py 수정하세요.

In [ ]:
# entries/views.py

class EntryListView(LockedView, ListView):
    model = Entry
    queryset = Entry.objects.all().order_by("-created_date")

class EntryDetailView(LockedView, DetailView):
    model = Entry

class EntryCreateView(LockedView, SuccessMessageMixin, CreateView):
    model = Entry
    fields = ["title", "content"]
    success_url = reverse_lazy("entry-list")
    success_message = "Your new entry was created!"

class EntryUpdateView(LockedView, SuccessMessageMixin, UpdateView):
    model = Entry
    fields = ["title", "content"]
    success_message = "Your entry was updated!"

    def get_success_url(self):
        return reverse_lazy("entry-detail", kwargs={"pk": self.object.pk})

class EntryDeleteView(LockedView, SuccessMessageMixin, DeleteView):
    model = Entry
    success_url = reverse_lazy("entry-list")
    success_message = "Your entry was deleted!"

    def delete(self, request, *args, **kwargs):
        messages.success(self.request, self.success_message)
        return super().delete(request, *args, **kwargs)