장고 예제) 게시판 기능 만들기

작성자: [관리자] 하얀설표

2025.06.15 17:26 (KST) 작성됨

2025.06.17 16:44 (KST) 수정됨






(06.17) 수정됨.

장고에서 글을 추가하고 변경하려면 이를 관리할 app을 추가해야 한다.

app을 추가하고 등록하는 방법을 알아보자.

 

콘솔 실행하기

예제를 따라하기 위해서는 cmd 콘솔을 실행해야 한다.

콘솔 실행 방법을 모른다면 패키지 설치와 프로젝트 설치 및 실행 방법을 보고 오자.

 

app 추가하기

"python manage.py startapp {app name}" 명령으로 app을 추가할 수 있다.

"board"라는 이름의 app을 추가해보자.

(django-sample) C:\WhiteSeolpyo\django-sample\django_sample>python manage.py startapp board

 

트리 구조

board app을 추가했다면 다음과 같이 장고 프로젝트 폴더에 board 폴더가 추가된다.

...
|- manage.py
|- db.sqlite3
|- config
|   |- ...
|- user
|   |- ...
|- templates
|   |- ...
|- board
    |- __init__.py
    |- admin.py
    |- apps.py
    |- models.py
    |- tests.py
    |- views.py
    |- migrations
        |- __init__.py

 

model 작성하기

models.py의 내용을 다음과 같이 변경해보자.

# board/models.py

from django.contrib.auth.models import User
from django.db import models
from django.shortcuts import resolve_url


class Post(models.Model):
    class Meta:
        # pk를 기준으로 내림차순 정렬
        ordering = ['-pk']

    author = models.ForeignKey(to=User, on_delete=models.DO_NOTHING, verbose_name='작성자', null=False, editable=False)
    title = models.CharField(verbose_name='제목', max_length=20, blank=False)
    content = models.TextField(verbose_name='내용', blank=False)
    created_at = models.DateTimeField(verbose_name='작성시간', auto_now_add=True)
    updated_at = models.DateTimeField(verbose_name='수정시간', auto_now=True)

    def get_absolute_url(self):
        return resolve_url('board:detail', pk=self.pk)


 

author

작성자 정보를 저장한다. ForeignKey 필드로 설정되었으며, 다른 모델과 연결되었다는 것을 뜻한다. on_delete 옵션은 연결된 모델이 삭제되었을 때 수행하는 작업을 설정한다.

editable 옵션은 일반 이용자가 해당 값을 변경할 수 있는지 여부를 결정한다. 작성자 정보는 임의로 변경되어선 안되기 때문에 False를 선언한다.

 

title

글의 제목을 저장한다. CharField로 설정되었으며, max_length를 필수로 설정해야 하며, 이를 통해 최대 글자수를 제한한다. blank=False 옵션으로 공란으로 입력될 수 없도록 했다.

 

content

글의 내용을 저장한다. TextField로 설정되었으며, CharField와 달리 max_length를 설정하지 않아도 되지만, 최대 글자수를 설정하는 것은 가능하다. blank=False 옵션으로 공란으로 입력될 수 없도록 했다.

 

created_at

글의 작성 시간을 저장한다. DateTimeField로 설정되었으며, auto_now_add 옵션으로 object 최초 생성시 시간을 기록한다.

 

updated_at

글의 수정 시간을 저장한다. DateTimeField로 설정되었으며, auto_now 옵션으로 object가 변경될 때마다 값이 갱신된다.

 

get_absolute_url

object의 고유 주소를 설정하는데 사용할 method다.

 

ordering

모델의 Meta class를 통해 여러가지 설정을 더할 수 있는데, ordering은 데이터를 가져올 때 정렬을 하는 기준을 설정한다.

"pk"는 pk를 기준 오름차순으로, "-pk"는 pk를 기준 내림차순으로 정렬한다.

pk는 Pirmary Key의 약자로, pk값을 따로 설정하지 않았다면 데이터 생성시 자동으로 숫자 id가 부여된다.

 

apps.py 수정하기

board의 apps.py를 다음과 같이 변경한다.

class명을 변경하는 이유는 app 생성시 "{app name}Config" 형식으로 class명이 정해지는데, 이걸 하나하나 확인하는 것이 번거롭기 때문이다.

이 class명은 나중에 str로 직접 입력해야 하는 값이기 때문에 Config로 통일한다.

# board/apps.py

from django.apps import AppConfig


class Config(AppConfig):
    default_auto_field = 'django.db.models.BigAutoField'
    name = 'board'

 

app 추가하기

config/settings/local/apps.py에 다음과 같이 board app을 추가한다.

# config/settings/local/apps.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',

    'user.apps.Config',
    # board app 추가
    'board.apps.Config',
]

 

마이그레이션 생성하고 적용하기

"python manage.py makemigrations"와 "python manage.py migrate" 명령을 순서대로 입력해 마이그레이션을 적용한다.

(django-sample) C:\WhiteSeolpyo\django-sample\django_sample>python manage.py makemigrations
Migrations for 'board':
  board\migrations\0001_initial.py
    + Create model Post

(django-sample) C:\WhiteSeolpyo\django-sample\django_sample>python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, board, contenttypes, sessions
Running migrations:
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying board.0001_initial... OK

 

무엇이 달라졌을까?

데이터베이스를 확인해보면 "board" 테이블이 추가되어있다.

현재 설정으로는 데이터베이스는 "db.sqlite3" 파일이며, sqlite 파일 조회를 위해서는 SQLite 프로그램을 설치한 상태여야 한다.

 

템플릿 작성하기

게시물 정보를 저장할 준비가 되었으니 개별 페이지와 기능을 만들어야 한다.

 

list.html

글 목록 페이지 템플릿을 다음과 같이 작성한다.

# board/templates/board/list.html

{% extends 'base.html' %}

{% block content %}

<a href="{% url 'board:create' %}">글 작성</a>

<table>
    <thead>
        <tr>
            <th>번호</th>
            <th>제목</th>
            <th>작성자</th>
            <th>작성시간</th>
        </tr>
    </thead>
    <tbody>
        {% for object in object_list %}
        <tr>
            <td>{{ object.pk }}</td>
            <td style="min-width: 200px;"><a href="{{ object.get_absolute_url }}">{{ object.title }}</a></td>
            <td>{{ object.author }}</td>
            <td>{{ object.created_at }}</td>
        </tr>
        {% empty %}
        <tr>
            <td colspan="4">
                게시물이 없습니다.
            </td>
        </tr>
        {% endfor %}
    </tbody>
</table>

{% if is_paginated and 1 < paginator.num_pages %}
    <div>
        {% if page_obj.has_previous %}
            <a href="?page=1">처음</a>
            {% if 2 < page_obj.number %}
                ...
            {% endif %}
            <a href="?page={{ page_obj.previous_page_number }}">{{ page_obj.previous_page_number }}</a>
        {% endif %}

        <strong>{{ page_obj.number }}</strong>

        {% if page_obj.has_next %}
            <a href="?page={{ page_obj.next_page_number }}">{{ page_obj.next_page_number }}</a>
            {% if page_obj.number < paginator.num_pages|add:-1 %}
                ...
            {% endif %}
            <a href="?page={{ paginator.num_pages }}">끝</a>
        {% endif %}
    </div>
{% endif %}

{% endblock %}

 

detail.html

개별 글을 조회할 때 사용할 페이지 템플릿을 다음과 같이 작성한다.

# board/templates/board/detail.html

{% extends 'base.html' %}

{% block content %}

<a href="{% url 'board:list' %}">글 목록</a>
<hr>

{% if object.author == request.user %}
<a href="{% url 'board:update' pk=object.pk %}">수정하기</a>
<form method="post" action="{% url 'board:delete' pk=object.pk %}" style="display: inline-block;">
    {% csrf_token %}
    <input type="submit" value="삭제하기">
</form>
{% endif %}
<div>
    <h2>{{ object.title }}</h2>
    <p>작성시간 : {{ object.created_at }}</p>
    <p>수정시간 : {{ object.updated_at }}</p>
    <p>작성자 : {{ object.author }}</p>
</div>
<hr>

<div>
    {{ object.content }}
</div>
<hr>

{% endblock %}

 

템플릿 경로를 추가하지 않는 이유

장고 튜토리얼) 템플릿 작성과 로그인 구현 예제을 보았다면 이번 app에서는 새로운 템플릿 경로를 만들었음에도 불구하고 경로를 추가하지 않았다는 차이를 눈치챘을 것이다.

장고 설정에서 "TEMPLATES"를 살펴보면 다음과 같이 "APP_DIRS"라는 항목이 있다.

이 값이 True인 경우에는 app 폴더에 존재하는 템플릿 경로는 등록하지 않더라도 사용할 수 있다.

TEMPLATES = [
    {
        ...
        'APP_DIRS': True,
    }

 

view 작성하기

다음과 같이 views.py의 내용을 변경한다.

CBV(Class Base View)를 사용하기 때문에 페이지 구현 관련해서 이런저런 코드를 만들어낼 필요가 없다.

CreateView를 보면 author_id를 request.user.pk로 지정하는데, 글 작성시 작성을 한 사람을 작성자로 지정하는 작업을 서버에서 수행하기 위해 필요하다.

# board/views.py

from django.core.exceptions import PermissionDenied
from django.forms import modelform_factory
from django.views import generic
from django.urls import reverse

from .models import Post


form = modelform_factory(Post, fields='__all__')


class ListView(generic.ListView):
    model = Post
    template_name = 'board/list.html'
    # 한 페이지에 표시할 게시물 수
    paginate_by = 3


class CreateView(generic.CreateView):
    model = Post
    template_name = 'form.html'
    form_class = form

    def dispatch(self, request, *args, **kwargs):
        if self.request.user.is_anonymous:
            raise PermissionDenied('로그인해야 합니다.')
        return super().dispatch(request, *args, **kwargs)

    def form_valid(self, form):
        "object에 글 작성자 정보 부여"
        setattr(form.instance, 'author_id', self.request.user.pk)
        return super().form_valid(form)


class DetailView(generic.DetailView):
    model = Post
    template_name = 'board/detail.html'


class UpdateView(generic.UpdateView):
    model = Post
    template_name = 'form.html'
    form_class = form

    def dispatch(self, request, *args, **kwargs):
        if self.request.user.is_anonymous:
            raise PermissionDenied('로그인해야 합니다.')
        return super().dispatch(request, *args, **kwargs)

    def get_object(self, queryset = ...):
        obj: Post = super().get_object(queryset)
        if obj.author != self.request.user:
            raise PermissionDenied('자신의 게시물만 수정할 수 있습니다.')
        return obj


class DeleteView(generic.DeleteView):
    model = Post
    template_name = 'form.html'

    def dispatch(self, request, *args, **kwargs):
        if self.request.user.is_anonymous:
            raise PermissionDenied('로그인해야 합니다.')
        return super().dispatch(request, *args, **kwargs)

    def get_object(self, queryset = ...):
        obj: Post = super().get_object(queryset)
        if obj.author != self.request.user:
            raise PermissionDenied('자신의 게시물만 삭제할 수 있습니다.')
        return obj

    def get_success_url(self):
        "글 삭제시 글 목록으로 이동"
        return reverse('board:list')

 

url 추가하기

다음과 같이 urls.py의 내용을 변경한다.

# board/urls.py

from django.urls import path

from . import views


app_name = 'board'

urlpatterns = [
    path('', views.ListView.as_view(), name='list'),
    path('create/', views.CreateView.as_view(), name='create'),
    path('<int:pk>/', views.DetailView.as_view(), name='detail'),
    path('<int:pk>/update/', views.UpdateView.as_view(), name='update'),
    path('<int:pk>/delete/', views.DeleteView.as_view(), name='delete'),
]

 

홈페이지 템플릿 변경하기

홈페이지 템플릿에 다음과 같이 board app에 할당된 url로 이동할 수 있는 링크를 추가한다.

{% extends 'base.html' %}

{% block content %}

<div>
    Home Page.
</div>
<div>
    <h2>내부 링크</h2>
    <ul>
        <li>
            <a href="{% url 'board:list' %}">게시판</a>
        </li>
    </ul>
</div>

{% endblock %}

 

확인하기

"python manage.py runserver" 명령으로 프로젝트를 실행하고, 접속해보자.

다음과 같이 게시물 조회 페이지를 이용할 수 있게 되었다.

 

게시물 임의생성하기

페이지 구현을 확인하기 위해서는 다수의 게시물 작성이 필요하다.

cmd 콘솔에서 장고 shell을 통해 다수의 게시물을 생성할 수 있다.

"python manage.py shell" 명령으로 장고 쉘을 실행해 다음과 같이 게시물을 만들고, "Ctrl" + "Z"키를 입력해 쉘을 종료하고 프로젝트를 다시 실행해보자.

(django-sample) C:\WhiteSeolpyo\django-sample\django_sample>python manage.py shell
7 objects imported automatically (use -v 2 for details).

Python 3.12.8 (tags/v3.12.8:2dc476b, Dec  3 2024, 19:30:04) [MSC v.1942 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from board.models import Post
>>> for n, _ in enumerate(range(30), 1):
...  obj, created = Post.objects.get_or_create(pk=n)
...  obj.author_id = 1
...  obj.title = str(n) * 3
...  obj.content = str(n) * 30
...  obj.save()
...
>>> ^Z

now exiting InteractiveConsole...

 

확인하기

다수의 게시물이 성공적으로 생성되었다.

한 페이지에 게시물을 3개씩 노출하기 때문에 pagination이 적용되었는지 확인하기도 쉽다.

페이지가 2개 이상인 경우 페이지 이동 기능도 제대로 작동하는 것을 확인할 수 있다.

 

 






추천 (0)


글 목록

댓글을 달 수 없는 게시물입니다.


"분류없음" 카테고리의 #Django, #Django 예제 관련 게시물

분류없음
해결) 장고 bulk_update의 메모리 누수 문제(django orm bluk_update method memory leak)
수정 07.12 | [관리자] 하얀설표
👍 0
#Python, #Django
🗨️ 0
분류없음
거지같은 subQuery와 outerRef
수정 06.29 | [관리자] 하얀설표
👍 0
#Django
🗨️ 0
분류없음
장고) pk 리스트에 등록된 순서대로 오브젝트를 가져오기 예제
작성 06.29 | [관리자] 하얀설표
👍 0
#Django
🗨️ 0
분류없음
해결) django.db.utils.OperationalError: database is locked
수정 06.18 | [관리자] 하얀설표
👍 0
#Python, #에러해결, #Django
🗨️ 0
썸네일
분류없음
장고 튜토리얼) 템플릿 작성과 로그인 구현 예제
수정 06.17 | [관리자] 하얀설표
👍 0
#Django 튜토리얼, #Django 예제
🗨️ 0
분류없음
장고 튜토리얼) settings.py 분리하기
수정 06.15 | [관리자] 하얀설표
👍 0
#Django, #Django 튜토리얼
🗨️ 0
썸네일
분류없음
장고 튜토리얼) 패키지 설치와 프로젝트 설치 및 실행 방법
수정 06.15 | [관리자] 하얀설표
👍 0
#Django, #Django 튜토리얼
🗨️ 0
분류없음
장고 튜토리얼) 관리자 계정과 일반 계정 만드는 방법
수정 06.15 | [관리자] 하얀설표
👍 0
#Django, #Django 튜토리얼
🗨️ 0
분류없음
장고 튜토리얼) makemigrations와 migrate 알아보기(migration과 database)
수정 06.15 | [관리자] 하얀설표
👍 0
#Django, #Django 튜토리얼
🗨️ 0
분류없음
장고) 게시판 템플릿 쉽게 만드는 방법
수정 06.15 | [관리자] 하얀설표
👍 0
#Django
🗨️ 0