Chapter3_Assignment_250123.zip
0.60MB


목록

  1. User 앱
    1. 사용자 모델 구현
    2. 회원가입, 로그인, 로그아웃 기능 구현
      1. 회원가입
      2. 로그인
      3. 로그아웃
    3. 사용자 프로필 페이지 구현
  2. Post 앱 (CRUD)
    1. Post 모델 구현
    2. 게시판 기능
      1. 게시글 목록 보기 (Read - List)
      2. 게시글 상세 보기 (Read - Detail)
      3. 게시글 작성 기능 (Create)
      4. 게시글 수정 기능 (Update)
      5. 게시글 삭제 기능 (Delete)
필수앱 구현
1. View (함수 or 클래스 택 1)
2. 기본 템플릿 (base.html, navbar.html, foodter.html)
3. 데이터베이스 (SQLite3)

평가 기준

  • 코드 구조 및 가독성
  • MTV 패턴 준수
  • 기능의 정확성
  • 에러 처리
  • 코드 재사용성
  • DRF 구현 시 RESTful API 설계 원칙 준수

0. 프로젝트 시작

1. 가상환경 만들기
더보기
python -m venv 가상환경 이름
# 가상환경 활성화
source 가상환경 이름/bin/activate
2. 필요한 pip 설치하기
더보기
django 설치 4.2 버전
# django 설치
pip install django==4.2
ipython 설치
# ipython 설치
pip install ipython

# tap키를 통한 자동 완성 기능
# 여러 줄에 걸린 코드 편집 기능 강화
# 문법에 따른 색상 강조
# 대화형 쉘 내에서 OS쉘의 명령을 즉시 호출
이미지 처리해 줄 pillow 설치
# 이미지 처리
pip install pillow
django의 기본 명령어 확장, django-extensions
# django의 기본 명령들의 기능을 확장해줌
pip install django-extensions
pip freeze
# freeze하여 설치한 목록 저장하기
pip freeze > requirements.txt
3. 프로젝트 시작하기
django-admin startproject 프로젝트 이름
4. App 생성하기
cd my_pjt
python manage.py startapp 앱 이름
5. settings.py에 앱 등록하기
더보기
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    
    'user',
    'post',
    'django_extensions',
]
6. 필요한 폴더 및 파일 생성
더보기
  • media
  • static
  • post/static/post
  • templates
  • templates/post
  • templates/user
  • post/urls.py
  • user/urls.py
7. 기본 템플릿 생성하기 ✅
더보기
 base.html 생성하기

 

17시 방향에 보면 Django HTML 버튼이 있어요

그걸 눌러서

 

 

html을 누르면 그냥 HTML인 애가 있어요

그걸로 눌러주면 느낌표(!) Tab을 하는 순간 HTML 양식이 나와요

 

그러고, 다시 Django HTML로 바꾸어주시면 돼요!

왜 이렇게 해야 하냐면,

그냥 HTML로 해도 상관없지만 Django 관련 자동완성이 안 돼서 Django HTML로 바꿔요


 

이렇게 MyPjt인 제일 바깥 영역에 templates 폴더를 만들고 그 속에 base.html을 만들어주었어요

그리고

html의 body 부분을 많이 쓸 거기 때문에 {% block content %}{% endblock content %}로 뚫어주었어요

 

💝 최종 base.html 모습 💝

 

{% load static %}

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>My Project</title>
    <style>
        .inline-container {
          display: flex; /* Flexbox를 사용해 가로 배치 */
          align-items: center; /* 세로 정렬 중앙 */
          gap: 10px; /* 요소 간 간격 */
        }
        form {
          margin: 0; /* 기본 여백 제거 */
        }
        a {
          text-decoration: none; /* 밑줄 제거 */
          color: blue; /* 링크 색상 설정 */
        }
      </style>
</head>
<body>
    <!--로그인하면 username이 index 화면에 떠용-->
    <div class="navbar">
        <h4>Welcome, {{ request.user.username}}</h4>
        <!--버튼들을 한 줄로 표시하려고 해용-->
        <div class="inline-container">

        <!--만약 user가 로그인된 상태라면-->
        {% if request.user.is_authenticated %}
            <!--로그아웃 버튼이 보여요-->
            <form action="{% url 'user:logout' %}" method="POST">
            {% csrf_token %}
            <input type="submit" value="logout"></input>
            </form>

            <!--회원 탈퇴 버튼도 생겨용-->
            <form action="{% url 'user:delete' %}" method="POST">
                {% csrf_token %}
            <input type="submit" value="Delete Account"></input>
            </form>

            <!--user profile로 가는 버튼이에용-->
            <a href="{% url 'user:user_profile' request.user.id %}"><button>{{ request.user.username }} Profile</button></a>

            <!--게시글 목록으로 가는 버튼이에용-->
            <a href="{% url 'post:post_list'%}"><button>Go to Post List</button></a>

        <!--로그인된 상태가 아니라면-->
        {% else %}
            <!--로그인 버튼이 보여용-->
            <a href="{% url 'user:login' %}"><button>Login</button></a>

            <!--회원가입 버튼이 보여용-->
            <a href="{% url 'user:signup' %}"><button>Sign up</button></a>

        {% endif %}
        </div>
        <hr>
    </div>

    <div class="container">
        {% block content %}
        {% endblock content %}
    </div>
</body>
</html>

navbar.html
<!-- navbar.html -->
<nav class="navbar navbar-expand-lg navbar-light bg-light">
    <div class="container">
        <a class="navbar-brand" href="/">MySite</a>
        <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
            <span class="navbar-toggler-icon"></span>
        </button>
        <div class="collapse navbar-collapse" id="navbarNav">
            <ul class="navbar-nav ms-auto">
                <li class="nav-item">
                    <a class="nav-link" href="/">Home</a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" href="/about">About</a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" href="/contact">Contact</a>
                </li>
                {% if user.is_authenticated %}
                    <li class="nav-item">
                        <a class="nav-link" href="/profile">Profile</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="/logout">Logout</a>
                    </li>
                {% else %}
                    <li class="nav-item">
                        <a class="nav-link" href="/login">Login</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="/signup">Signup</a>
                    </li>
                {% endif %}
            </ul>
        </div>
    </div>
</nav>

footer.html
<!-- footer.html -->
<footer class="bg-light text-center text-lg-start mt-5">
    <div class="container p-4">
        <div class="row">
            <div class="col-lg-6 col-md-12 mb-4 mb-md-0">
                <h5 class="text-uppercase">MySite</h5>
                <p>
                    A brief description of the website or a motivational quote can go here.
                </p>
            </div>

            <div class="col-lg-3 col-md-6 mb-4 mb-md-0">
                <h5 class="text-uppercase">Links</h5>
                <ul class="list-unstyled mb-0">
                    <li>
                        <a href="/" class="text-dark">Home</a>
                    </li>
                    <li>
                        <a href="/about" class="text-dark">About</a>
                    </li>
                    <li>
                        <a href="/contact" class="text-dark">Contact</a>
                    </li>
                </ul>
            </div>

            <div class="col-lg-3 col-md-6 mb-4 mb-md-0">
                <h5 class="text-uppercase">Social</h5>
                <ul class="list-unstyled mb-0">
                    <li>
                        <a href="#" class="text-dark">Facebook</a>
                    </li>
                    <li>
                        <a href="#" class="text-dark">Twitter</a>
                    </li>
                    <li>
                        <a href="#" class="text-dark">Instagram</a>
                    </li>
                </ul>
            </div>
        </div>
    </div>

    <div class="text-center p-3" style="background-color: rgba(0, 0, 0, 0.2);">
        © 2025 MySite. All rights reserved.
    </div>
</footer>
8. settings.py에서 TEMPLATES_DIRS 설정하기
더보기
        'DIRS': [BASE_DIR / "templates"],

 

  • BASE_DIR은 settings.py 상단에 정의내려져 있어요
BASE_DIR = Path(__file__).resolve().parent.parent

 

이렇게 설정을 해줘야 {% extends "base.html" %}을 쓸 수 있어요 ("navbar.html", "footer.html"도 마찬가지)


1. User 앱

1.1. 사용자 모델 구현

1.1.1. user/models.py

더보기
from django.contrib.auth.models import AbstractUser
from django.db import models

class CustomUser(AbstractUser):
    nickname = models.CharField(max_length=20)
    profile_image = models.ImageField(upload_to='media/', blank=True)
    bio = models.TextField(blank=True, null=True)   # 소개글

    def __str__(self):
        return self.username

 

  • Ctrl을 누른채로 AbstractUser를 클릭하면 ↓ 어떤 기능을 하는지 확인할 수 있다.
  • auth에 있는 model를 데리고 온 이유는 로그인 해야 user의 프로필, 소개글 등을 볼 수 있게 하기 위함이다. 
⬇️ class AbstractUser(AbstractBaseUser, PermissionsMixin):
더보기
class AbstractUser(AbstractBaseUser, PermissionsMixin):
    """
    An abstract base class implementing a fully featured User model with
    admin-compliant permissions.

    Username and password are required. Other fields are optional.
    """

    username_validator = UnicodeUsernameValidator()

    username = models.CharField(
        _("username"),
        max_length=150,
        unique=True,
        help_text=_(
            "Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only."
        ),
        validators=[username_validator],
        error_messages={
            "unique": _("A user with that username already exists."),
        },
    )
    first_name = models.CharField(_("first name"), max_length=150, blank=True)
    last_name = models.CharField(_("last name"), max_length=150, blank=True)
    email = models.EmailField(_("email address"), blank=True)
    is_staff = models.BooleanField(
        _("staff status"),
        default=False,
        help_text=_("Designates whether the user can log into this admin site."),
    )
    is_active = models.BooleanField(
        _("active"),
        default=True,
        help_text=_(
            "Designates whether this user should be treated as active. "
            "Unselect this instead of deleting accounts."
        ),
    )
    date_joined = models.DateTimeField(_("date joined"), default=timezone.now)

    objects = UserManager()

    EMAIL_FIELD = "email"
    USERNAME_FIELD = "username"
    REQUIRED_FIELDS = ["email"]

    class Meta:
        verbose_name = _("user")
        verbose_name_plural = _("users")
        abstract = True

    def clean(self):
        super().clean()
        self.email = self.__class__.objects.normalize_email(self.email)

    def get_full_name(self):
        """
        Return the first_name plus the last_name, with a space in between.
        """
        full_name = "%s %s" % (self.first_name, self.last_name)
        return full_name.strip()

    def get_short_name(self):
        """Return the short name for the user."""
        return self.first_name

    def email_user(self, subject, message, from_email=None, **kwargs):
        """Send an email to this user."""
        send_mail(subject, message, from_email, [self.email], **kwargs)

 

1.1.2. settings.py에 AUTH 모델 추가하기

더보기
AUTH_USER_MODEL = 'user.CustomUser'

 

1.1.3. migrate 하기

더보기
python manage.py makemigrations
python manage.py migrate

 

제대로 migrate가 됐다면, migrations 폴더에 0001로 migrate 파일이 생성될 거예요 👍🏻


1.2. 회원가입, 로그인, 로그아웃 기능 구현

1.2.1. MyPjt/usls.py

더보기
from django.contrib import admin
from django.urls import path, include
from post import views
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', views.index, name="index"),    # 서버 들어가자마자 Index
    path('user/', include('user.urls')),    # user.urls에 있는 애들은 user/로 시작
    path('post/', include('post.urls')),    # post.urls에 있는 애들은 post/로 시작

 

1.2.1. 회원가입 (signup) 및 회원 탈퇴

1.2.1.1. urls.py 추가

더보기
from django.urls import path
from . import views

app_name = "user"
urlpatterns = [
    path('signup/', views.signup, name="signup"),   # 회원가입

 

1.2.1.2. views.py 추가

더보기
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib.auth.forms import AuthenticationForm
from django.contrib.auth import login as auth_login
from django.views.decorators.http import require_POST
from .forms import CustomUserCreationForm

# 회원가입
def signup(request):
    if request.method == "POST":
        # UserCreationForm : username과 password로 새로운 user를 생성해주는 모델
        form = CustomUserCreationForm(request.POST, request.FILES) # 바인딩 form : POST로 채워져서 만들어지는 form
        if form.is_valid(): # POST로 받은 값이 유효하다면,
            user = form.save()  # user에 POST 값 저장
            auth_login(request, user) # user 값으로 자동으로 로그인 하기
            return redirect("index")    # 로그인된 상태로 index로 가기
    else:
        form = CustomUserCreationForm()   # GET일 때는 form으로 보여주기
    context = {'form': form}
    return render(request, "user/signup.html", context)

 

1.2.1.3. signup.html 추가

더보기
<!--회원가입 하기-->

{% extends "base.html" %}

{% block content %}
<h1>Sign up</h1>

<!--회원가입하는 form이에용-->
<form action="{% url "user:signup" %}" method="POST" enctype="multipart/form-data">
    {% csrf_token %}
    {{ form.as_p }}

    <!-- 닉네임 -->
    <label for="nickname">Nickname :</label>
    <input type="text" id="nickname" name="nickname"><br><br>

    <!-- 소개글 넣기 (bio)-->
    <label for="bio">Bio :</label>
    <textarea id="bio" name="bio" rows="4"></textarea><br><br>
    
    <!-- 프로필 사진 넣기 -->
    <label for="profile_image">Profile Picture :</label>
    <input type="file" id="profile_image" name="profile_image" accept="media/*"><br><br>
 

    <button type="submit">Sign up</button>
</form>
{% endblock content %}

 

1.2.2. 회원 탈퇴 

더보기
1️⃣ user/uris.py
path('delete/', views.delete, name="delete"),   # 회원탈퇴

2️⃣ user/views.py

from django.contrib.auth import logout as auth_logout

# 회원탈퇴
# @require_POST : POST일 때만 작동
@require_POST
def delete(request):
    if request.user.is_authenticated:
        request.user.delete()
        auth_logout(request)
    return redirect("index")

1.2.2. 로그인(login)

1.2.2.1. urls.py 추가

더보기
path('login/', views.login, name="login"),  # 로그인

1.2.2.2. views.py 추가

더보기
from django.contrib.auth.forms import AuthenticationForm
from django.contrib.auth import login as auth_login

# 로그인
def login(request):
    if request.method == "POST": 
        form = AuthenticationForm(data=request.POST)    # 로그인을 위한 기본적인 form을 제공해줌
        if form.is_valid(): # form이 POST 값으로 유효하다면,
            auth_login(request, form.get_user())    # 로그인 하기
            next_url = request.GET.get("next") or "index"   # GET 정보가 있으면 next로 가고, 없으면 index로 가자
            return redirect(next_url)   # 이전까지 실행되던 창으로 돌아가자
        else:
            print(form.errors)
    else:
        form = AuthenticationForm() # GET일 경우 form만 출력
    context = {"form": form}
    return render(request, "user/login.html", context)

1.2.2.3. login.html 추가

더보기
<!--로그인하기-->

{% extends "base.html" %}

{% block content %}
<h1>Login</h1>

<!--로그인하는 로직이에용-->
<form action="" method="POST">
    {% csrf_token %}
    {{ form.as_p }}
    <input type="submit" value="Login">
</form>

<!--인덱스로 가는 버튼이에용-->
<a href="{% url "index"%}"><button>Back to index</button></a>
{% endblock content %}

1.2.3. 로그아웃(logout)

1.2.3.1. urls.py 추가

더보기
path('logout/', views.logout, name="logout"),   # 로그아웃

1.2.3.2. views.py 추가

더보기
from django.contrib.auth import logout as auth_logout
from django.views.decorators.http import require_POST

# 로그아웃
# @require_POST : POST일 때만 작동
@require_POST
def logout(request):
    if request.user.is_authenticated:   # user가 로그인된 상태일 때,
        auth_logout(request)    # 로그아웃 하기
    return redirect("index")    # 로그아웃되면 인덱스 화면으로 가용

1.3. 사용자 프로필 페이지 구현 (user_profile)

1.3.1. urls.py 추가

더보기
path('user-profile/<int:id>/', views.user_profile, name="user_profile"),    # user 프로필
path('<int:id>/edit-profile/', views.edit_profile, name="edit_profile"),    # user 프로필 수정하기

 

까먹었던 namespace도 적어주었어요.

1.3.2. views.py 추가

더보기
# 프로필 보여주기
@login_required
def user_profile(request, id):
    user = request.user # 로그인한 user의 profile
    profile = get_object_or_404(CustomUser, id=id) # other user's profile
    
    if request.method == "POST":
        if user != profile: # user와 profile이 다르다면,
            return redirect("index")    # index로 돌아가세욧
    
    if request.method == "POST":
        # 클라이언트가 POST, FILES로 보낸 데이터를 받고, profile 값으로 form을 채워용
        form = CustomUserCreationForm(request.POST, request.FILES, instance=profile)
        if form.is_valid():
            profile = form.save()
            return redirect("user:user_profile", id=user.id) # 수정 후 다시 프로필 페이지로 가세욧
        else:
            print(form.errors)  # form 에러 출력
    else:
        form = CustomUserCreationForm(instance=profile)
    context = {
               "user":user,
               "profile":profile,
               "form":form,
               }
    return render(request, "user/profile.html", context)

1.3.3. profile.html 추가

더보기
<!--프로필 보기-->

{% extends "base.html" %}

{% load static %}

{% block content %}
<!--profile에 있는 username을 불러와줘용-->
<h2><center>Profile of {{ profile.username }}</center></h2>
<div>

<!--만약 mdeia/에 사진이 있다면-->
{% if profile.profile_image %}
    <p><img src="{{ profile.profile_image.url }}" style="max-width: 200px;"></p>

<!--만약 사진이 없다면 사진 없다고 문구 보여줄게용-->
{% else %}
    <p>No profile picture uploaded.</p>
{% endif %}
</div>

<!--유저 이름, 별명, 소개글 보여줘용-->
<p>Username : {{ profile.username }}</p>
<p>Nickname : {{ profile.nickname }}</p>
<p>Bio : {{ profile.bio }}</p>

<hr>
<!--프로필 수정하는 버튼이에용-->
<a href="{% url 'user:edit_profile' user.id %}"><button>Edit Profile</button></a>

<!--인덱스로 돌아가는 버튼이에용-->
<a href="{% url 'index' %}"><button>Back to index</button></a>

{% endblock content %}

 

1.3.4. 프로필 수정하기 

더보기
1️⃣ user/urls.py
path('<int:id>/edit-profile/', views.edit_profile, name="edit_profile"),    # user 프로필 수정하기

2️⃣ user/views.py

# 프로필 수정하기
@login_required(login_url="user:login")
def edit_profile(request, id):
    user = request.user # 현재 로그인된 user
    profile = get_object_or_404(CustomUser, id=user.id)  # 현재 사용자 정보 가져오기
    
    if request.method == "POST":
        form = CustomUserCreationForm(request.POST, request.FILES, instance=profile)
        if form.is_valid():
            profile = form.save()  # 변경된 데이터 저장
            return redirect("user:user_profile", id=user.id)  # 수정 완료 후 프로필 페이지로 이동
        
    else:
        form = CustomUserCreationForm(instance=profile)
    context = {
        "form": form,
        "profile": profile,
    }
    return render(request, "user/edit_profile.html", context)

 

🐾Recent posts