Django 구현 기초-Todo List 웹서비스

1. Todo List 웹서비스 시작하기

  • 코드출처: 백엔드를 위한 Django REST Framework with 파이썬(영진닷컴)

1.1 프로젝트 기능 정리

  • Todo List 전체 조회하기
  • Todo List 상세 조회하기
  • Todo 생성하기
    • 데이터 입력을 위한 Form 필요
  • Todo 수정하기
    • 데이터 수정을 위한 Form 필요
    • Todo 생성용 Form 활용 가능
  • Todo 완료 처리하기

1.2 프로젝트 생성

  • 가상환경 설정
python -m venv django_todolist
cd todolist
source ./bin/activate
# Windows의 경우: ./Scripts/activate
  • Django 설치 및 확인
pip install django
python -m django --version
python --version
  • Project 시작
django-admin startproject todoweb .
  • 앱 추가
python manage.py startapp todolist
  • 웹서버 실행
python manage.py runserver
  • 웹 브라우저에서 웹서버 시작 확인
    • https://127.0.0.1:8000

1.3 프로젝트 설정

  • 웹서버 설정 수정
#//file: "./todoweb/settings.py"

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'todolist',    # 추가할 것
]

TIME_ZONE = 'Asia/Seoul'    # 시간대를 한국으로 바꿀 것
  • 웹서버 설정 URL 확인
#//file: "./todoweb/urls.py"

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

urlpatterns = [
    path('admin/', admin.site.urls),
]
  • Migration 수행
python manage.py migrate
  • 계정 만들기
python manage.py createsuperuser

1.4 Todo 모델 생성

  • Model 만들기
#//file: "./todolist/models.py"

from django.db import models

# Create your models here.
class Todo(models.Model):
    title = models.CharField(max_length=100)
    description = models.TextField(blank=True)
    created = models.DateTimeField(auto_now_add=True)
    complete = models.BooleanField(default=False)
    important = models.BooleanField(default=False)

    def __str__(self):
        return self.title
  • Model 적용시키기
python manage.py makemigrations
python manage.py migrate
  • Model을 Admin 페이지에 적용시키기
#//file: "./todolist/admin.py"

from django.contrib import admin
from todolist.models import Todo

# Register your models here.
admin.site.register(Todo)

2. Todo 전체 조회 기능 만들기

2.1 Todo 전체 조회 기능 컨셉

  • 첫 페이지에서 Todo List를 보여줌
  • 완료되지 않은 항목만 보여줌

2.2 Bootstrap으로 좀 더 멋진 템플릿 만들기

  • Bootstrap
    • 웹 개발을 쉽게 할 수 있도록 도와주는 HTML, CSS, JavaScript 프레임워크
    • Twitter의 개발자들이 처음 만들었으며, 현재는 오픈 소스로 제공 중
    • 특징
      • 반응형 디자인: 다양한 기기(모바일, 태블릿, 데스크탑)에서 잘 작동하도록 설계됨
      • 미리 정의된 스타일: 버튼, 폼, 네비게이션 바 등 다양한 UI 요소에 대한 스타일 제공
      • 확장성: 필요에 따라 커스터마이징이 가능하며, 다양한 플러그인과 함께 사용할 수 있음
      • CDN 지원: 별도의 설치 없이 CDN 링크를 통해 쉽게 사용할 수 있음

2.3 Todo 전체 조회 템플릿 만들기

<!--//file: "./todolist/templates/todo_list.html"-->

<html>
  <head>
    <title>TODO 목록 앱</title>
    <link
      rel="stylesheet"
      href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"
    />
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.1/font/bootstrap-icons.css">
  </head>
  <body>
  <div class="container">
    <h1>TODO 목록 앱</h1>
    <p>
      <a href=""><i class="bi-plus"></i>Add Todo</a>
      <a href="" class="btn btn-primary" style="float:right">완료한 TODO 목록</a>
    </p>
    <ul class="list-group">

      <li class="list-group-item">
        <a href="">{ { todo.title }}</a>
        <div style="float:right">
          <a href="" class="btn btn-danger">완료</a>
          <a href="" class="btn btn-outline-primary">수정하기</a>
        </div>
      </li>

      <li class="list-group-item">
        <a href="">{ { todo.title }}</a>
          <span class="badge badge-danger">!</span>
        <div style="float:right">
          <a href="" class="btn btn-danger">완료</a>
          <a href="" class="btn btn-outline-primary">수정하기</a>
        </div>
      </li>

      <li class="list-group-item">
        <a href="">{ { todo.title }}</a>
        <div style="float:right">
          <a href="" class="btn btn-danger">완료</a>
          <a href="" class="btn btn-outline-primary">수정하기</a>
        </div>
      </li>

    </ul>
  </body>
  </div>
</html>

2.4 Todo 전체 조회 뷰 만들기

#//file: "./todolist/views.py"

from django.shortcuts import render, redirect
from todolist.models import Todo

def todo_list(request):
    todos = Todo.objects.filter(complete=False)
    return render(request, 'todolist/todo_list.html', {'todos': todos})

2.5 Todo 전체 조회 템플릿 수정하기

<!--//file: "./todolist/templates/todo_list.html"-->

<html>
  <head>
    <title>TODO 목록 앱</title>
    <link
      rel="stylesheet"
      href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"
    />
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.1/font/bootstrap-icons.css">
  </head>
  <body>
  <div class="container">
    <h1>TODO 목록 앱</h1>
    <p>
      <a href=""><i class="bi-plus"></i>Add Todo</a>
      <a href="" class="btn btn-primary" style="float:right">완료한 TODO 목록</a>
    </p>
    <ul class="list-group">
      { % for todo in todos %}
      <li class="list-group-item">
        <a href="">{ { todo.title }}</a>
        { % if todo.important %}
          <span class="badge badge-danger">!</span>
        { % endif %}
        <div style="float:right">
          <a href="" class="btn btn-danger">완료</a>
          <a href="" class="btn btn-outline-primary">수정하기</a>
        </div>
      </li>
      { % endfor %}
    </ul>
  </body>
  </div>
</html>

2.6 Todo 전체 조회 URL 연결하기

#//file: "./todolist/urls.py"

from django.urls import path
from . import views

urlpatterns = [
    path('', views.todo_list, name='todo_list'),
]
#//file: "./todoweb/urls.py"

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

urlpatterns = [
    path('admin/', admin.site.urls),
    path('todolist/', include('todolist.urls')),
]

3. Todo 상세 조회 기능 만들기

3.1 Todo 상세 조회 기능 컨셉

  • Todo List에서 Todo를 선택했을 때 조회
  • Todo의 제목과 설명을 보여줌

3.2 Todo 상세 조회 템플릿 만들기

<!--//file: "./todolist/templates/todo_detail.html"-->

<html>
  <head>
    <title>TODO 목록 앱</title>
    <link
      rel="stylesheet"
      href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"
    />
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.1/font/bootstrap-icons.css">
  </head>
  <body>
  <div class="container">
    <h1>TODO 상세보기</h1>
    <div class="container">
      <div class="row">
        <div class="col-md-12">
          <div class="card">
            <div class="card-body">
              <h5 class="card-title">{ { todo.title }}</h5>
              </h5>
              <p class="card-text">{ { todo.description }}</p>
              <a href="{ % url 'todo_list' %}" class="btn btn-primary">목록으로</a>
            </div>
          </div>
        </div>
      </div>
    </div>
  </body>
  </div>
</html>

3.3 Todo 상세 조회 뷰 만들기

#//file: "./todolist/views.py"

from django.shortcuts import render, redirect
from .models import Todo

def todo_list(request):
    todos = Todo.objects.filter(complete=False)
    return render(request, 'todolist/todo_list.html', {'todos': todos})

def todo_detail(request, pk):
    todo = Todo.objects.get(id=pk)
    return render(request, 'todolist/todo_detail.html', {'todo': todo})

3.4 Todo 전체 조회 템플릿 수정하기

  • 상세 조회 페이지로 들어오기 위한 링크 걸기
<!--//file: "./todolist/templates/todo_list.html"-->

<html>
  <head>
    <title>TODO 목록 앱</title>
    <link
      rel="stylesheet"
      href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"
    />
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.1/font/bootstrap-icons.css">
  </head>
  <body>
  <div class="container">
    <h1>TODO 목록 앱</h1>
    <p>
      <a href=""><i class="bi-plus"></i>Add Todo</a>
      <a href="" class="btn btn-primary" style="float:right">완료한 TODO 목록</a>
    </p>
    <ul class="list-group">
      { % for todo in todos %}
      <li class="list-group-item">
        <a href="{ % url 'todo_detail' pk=todo.pk %}">{ { todo.title }}</a>
        { % if todo.important %}
          <span class="badge badge-danger">!</span>
        { % endif %}
        <div style="float:right">
          <a href="" class="btn btn-danger">완료</a>
          <a href="" class="btn btn-outline-primary">수정하기</a>
        </div>
      </li>
      { % endfor %}
    </ul>
  </body>
  </div>
</html>

3.5 Todo 상세 조회 URL 연결하기

#//file: "./todolist/urls.py"

from django.urls import path
from . import views

urlpatterns = [
    path('', views.todo_list, name='todo_list'),
    path('<int:pk>/', views.todo_detail, name='todo_detail'),
]

4. Todo 생성 기능 만들기

4.1 Todo 생성 기능 컨셉

  • 제목, 설명, 중요도 입력
  • Form 필요 → ./todolist/forms.py

4.2 Todo 생성 템플릿 만들기

4.2.1 Todo 내용 입력을 위한 Form 만들기

#//file: "./todolist/forms.py"

from django import forms
from .models import Todo

class TodoForm(forms.ModelForm):
    class Meta:
        model = Todo
        fields = ('title', 'description', 'important')

4.2.2 템플릿 만들기

<!--//file: "./todolist/templates/todo_post.html"-->

<html>
  <head>
    <title>TODO 목록 앱</title>
    <link
      rel="stylesheet"
      href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"
    />
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.1/font/bootstrap-icons.css">
  </head>
  <body>
  <div class="container">
    <h1>TODO 추가하기</h1>
    <div class="container">
      <div class="row">
        <div class="col-md-12">
          <div class="card">
            <div class="card-body">
              <form method="POST">
                { % csrf_token %} { { form.as_p }}
                <button type="submit" class="btn btn-primary">등록</button>
              </form>
            </div>
          </div>
        </div>
      </div>
    </div>
  </body>
  </div>
</html>

4.3 Todo 생성 뷰 만들기

#//file: "./todolist/views.py

from django.shortcuts import render, redirect
from .models import Todo
from .forms import TodoForm


def todo_list(request):
    todos = Todo.objects.filter(complete=False)
    return render(request, 'todolist/todo_list.html', {'todos': todos})


def todo_detail(request, pk):
    todo = Todo.objects.get(id=pk)
    return render(request, 'todolist/todo_detail.html', {'todo': todo})


def todo_post(request):
    if request.method == "POST":
        form = TodoForm(request.POST)
        if form.is_valid():
            todo = form.save(commit=False)
            todo.save()
            return redirect('todo_list')
    else:
        form = TodoForm()
    return render(request, 'todolist/todo_post.html', {'form': form})

4.4 Todo 전체 조회 템플릿 수정하기

  • Todo 생성을 위한 링크 걸기
<!--//file: "./todolist/templates/todo_list.html"-->

<html>
  <head>
    <title>TODO 목록 앱</title>
    <link
      rel="stylesheet"
      href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"
    />
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.1/font/bootstrap-icons.css">
  </head>
  <body>
  <div class="container">
    <h1>TODO 목록 앱</h1>
    <p>
      <a href="{ % url 'todo_post' %}"><i class="bi-plus"></i>Add Todo</a>
      <a href="" class="btn btn-primary" style="float:right">완료한 TODO 목록</a>
    </p>
    <ul class="list-group">
      { % for todo in todos %}
      <li class="list-group-item">
        <a href="{ % url 'todo_detail' pk=todo.pk %}">{ { todo.title }}</a>
        { % if todo.important %}
          <span class="badge badge-danger">!</span>
        { % endif %}
        <div style="float:right">
          <a href="" class="btn btn-danger">완료</a>
          <a href="" class="btn btn-outline-primary">수정하기</a>
        </div>
      </li>
      { % endfor %}
    </ul>
  </body>
  </div>
</html>

4.5 Todo 생성 URL 연결하기

#//file: "./todolist/urls.py"

from django.urls import path
from . import views

urlpatterns = [
    path('', views.todo_list, name='todo_list'),
    path('<int:pk>/', views.todo_detail, name='todo_detail'),
    path('post/', views.todo_post, name='todo_post'),
]

5. Todo 수정 기능 만들기

5.1 Todo 수정 기능 컨셉

  • Todo 생성 기능과 거의 동일
  • 선택된 Todo의 내용이 Form에 미리 입력되어 있음

5.2 Todo 수정 뷰 만들기

#//file: "./todolist/views.py"

from django.shortcuts import render, redirect
from .models import Todo
from .forms import TodoForm


def todo_list(request):
    todos = Todo.objects.filter(complete=False)
    return render(request, 'todolist/todo_list.html', {'todos': todos})

def todo_detail(request, pk):
    todo = Todo.objects.get(id=pk)
    return render(request, 'todolist/todo_detail.html', {'todo': todo})

def todo_post(request):
    if request.method == "POST":
        form = TodoForm(request.POST)
        if form.is_valid():
            todo = form.save(commit=False)
            todo.save()
            return redirect('todo_list')
    else:
        form = TodoForm()
    return render(request, 'todolist/todo_post.html', {'form': form})

def todo_edit(request, pk):
    todo = Todo.objects.get(id=pk)
    if request.method == "POST":
        form = TodoForm(request.POST, instance=todo)
        if form.is_valid():
            todo = form.save(commit=False)
            todo.save()
            return redirect('todo_list')
    else:
        form = TodoForm(instance=todo)
    return render(request, 'todolist/todo_post.html', {'form': form})

5.4 Todo 전체 조회 템플릿 수정하기

  • Todo 수정을 위한 링크 걸기
<!--//file: "./todolist/templates/todo_list.html"-->

<html>
  <head>
    <title>TODO 목록 앱</title>
    <link
      rel="stylesheet"
      href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"
    />
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.1/font/bootstrap-icons.css">
  </head>
  <body>
  <div class="container">
    <h1>TODO 목록 앱</h1>
    <p>
      <a href="{ % url 'todo_post' %}"><i class="bi-plus"></i>Add Todo</a>
      <a href="" class="btn btn-primary" style="float:right">완료한 TODO 목록</a>
    </p>
    <ul class="list-group">
      { % for todo in todos %}
      <li class="list-group-item">
        <a href="{ % url 'todo_detail' pk=todo.pk %}">{ { todo.title }}</a>
        { % if todo.important %}
          <span class="badge badge-danger">!</span>
        { % endif %}
        <div style="float:right">
          <a href="" class="btn btn-danger">완료</a>
          <a href="{ % url 'todo_edit' pk=todo.pk %" class="btn btn-outline-primary">수정하기</a>
        </div>
      </li>
      { % endfor %}
    </ul>
  </body>
  </div>
</html>

5.4 Todo 수정 URL 연결하기

#//file: "./todolist/urls.py"

from django.urls import path
from . import views

urlpatterns = [
    path('', views.todo_list, name='todo_list'),
    path('<int:pk>/', views.todo_detail, name='todo_detail'),
    path('post/', views.todo_post, name='todo_post'),
    path('<int:pk>/edit/', views.todo_edit, name='todo_edit'),
]

6. Todo 완료 기능 만들기

6.1 Todo 완료 기능 컨셉

  • 완료 버튼을 눌렀을 때 Todo의 complete 값을 True로 설정
  • 완료 Todo 조회 기능: 완료된 Todo List만 보여줌

6.2 Todo 완료 템플릿 만들기

<!--//file: "./todolist/templates/done_list.html"-->

<html>
  <head>
    <title>TODO 목록 앱</title>
    <link
      rel="stylesheet"
      href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"
    />
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.1/font/bootstrap-icons.css">
  </head>
  <body>
  <div class="container">
    <h1>DONE 목록</h1>
    <p>
      <a href="{ % url 'todo_list' %}" class="btn btn-primary">홈으로</a>
    </p>
    <ul class="list-group">
      { % for done in dones %}
      <li class="list-group-item">
        <a href="{ % url 'todo_detail' pk=done.pk %}">{ { done.title }}</a>
        { % if done.important %}
          <span class="badge badge-danger">!</span>
        { % endif %}
      </li>
      { % endfor %}
    </ul>
  </body>
  </div>
</html>

6.3 Todo 전체 조회 템플릿 수정하기

  • Todo 완료를 위한 링크 걸기
<!--//file: "./todolist/templates/todo_list.html"-->

<html>
  <head>
    <title>TODO 목록 앱</title>
    <link
      rel="stylesheet"
      href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"
    />
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.1/font/bootstrap-icons.css">
  </head>
  <body>
  <div class="container">
    <h1>TODO 목록 앱</h1>
    <p>
      <a href="{ % url 'todo_post' %}"><i class="bi-plus"></i>Add Todo</a>
      <a href="{ % url 'done_list' %}" class="btn btn-primary" style="float:right">완료한 TODO 목록</a>
    </p>
    <ul class="list-group">
      { % for todo in todos %}
      <li class="list-group-item">
        <a href="{ % url 'todo_detail' pk=todo.pk %}">{ { todo.title }}</a>
        { % if todo.important %}
          <span class="badge badge-danger">!</span>
        { % endif %}
        <div style="float:right">
          <a href="{ % url 'todo_done' pk=todo.pk %}" class="btn btn-danger">완료</a>
          <a href="{ % url 'todo_edit' pk=todo.pk %}" class="btn btn-outline-primary">수정하기</a>
        </div>
      </li>
      { % endfor %}
    </ul>
  </body>
  </div>
</html>

6.4 Todo 완료 뷰 만들기

#//file: "./todolist/views.py"

from django.shortcuts import render, redirect
from .models import Todo
from .forms import TodoForm


def todo_list(request):
    todos = Todo.objects.filter(complete=False)
    return render(request, 'todolist/todo_list.html', {'todos': todos})

def todo_detail(request, pk):
    todo = Todo.objects.get(id=pk)
    return render(request, 'todolist/todo_detail.html', {'todo': todo})

def todo_post(request):
    if request.method == "POST":
        form = TodoForm(request.POST)
        if form.is_valid():
            todo = form.save(commit=False)
            todo.save()
            return redirect('todo_list')
    else:
        form = TodoForm()
    return render(request, 'todolist/todo_post.html', {'form': form})

def todo_edit(request, pk):
    todo = Todo.objects.get(id=pk)
    if request.method == "POST":
        form = TodoForm(request.POST, instance=todo)
        if form.is_valid():
            todo = form.save(commit=False)
            todo.save()
            return redirect('todo_list')
    else:
        form = TodoForm(instance=todo)
    return render(request, 'todolist/todo_post.html', {'form': form})

def done_list(request):
    dones = Todo.objects.filter(complete=True)
    return render(request, 'todo/done_list.html', {'dones': dones})

def todo_done(request, pk):
    todo = Todo.objects.get(id=pk)
    todo.complete = True
    todo.save()
    return redirect('todo_list')

6.5 Todo 완료 URL 연결하기

#//file: "./todolist/urls.py"

from django.urls import path
from . import views

urlpatterns = [
    path('', views.todo_list, name='todo_list'),
    path('<int:pk>/', views.todo_detail, name='todo_detail'),
    path('post/', views.todo_post, name='todo_post'),
    path('<int:pk>/edit/', views.todo_edit, name='todo_edit'),
    path('done/', views.done_list, name='done_list'),
    path('done/<int:pk>/', views.todo_done, name='todo_done'),
]