본문 바로가기

PYTHON-BACK

#파이썬 32일차_이커머스 클론코딩2

728x90

인스타그램 비슷하게 동작하는 프로그램을 만들어보자

러스트 프레임워크 말고 장고 내부 프레임워크만을 이용해서 만들것

 

Pystagram Project

1. 기능 설정

  • 인증 시스템
  • 피드 페이지
  • 글과 댓글
  • 동적 URL
  • 해시 태크
  • 글 상세 페이지
  • 좋아요 기능
  • 팔로우/팔로잉 기능

 

2. 환경 설정

 
  • 가상환경 생성(Terminal)
python -m venv pystagram
cd pystagram
source ./bin/activate

 

  • 라이브러리 설치(Terminal)
pip install django Pillow

 

  • 프로젝트 생성(Terminal)
django-admin startproject config .
  • 기능별 디렉토리 설정(Terminal)
mkdir templates
mkdir static
  • 환경설정(config/settings.py)
TEMPLATES_DIR = BASE_DIR / "templates"
...
TEMPLATES = [
    {
        ...
        "DIRS": [TEMPLATES_DIR],
    }
]
STATIC_URL = "static/"
STATICFILES_DIRS = [BASE_DIR / "static"]

MEDIA_URL = "media/"
MEDIA_ROOT = BASE_DIR / "media"
  • 환경설정(config/urls.py)
from django.conf import settings
from django.conf.urls.static import static

# 기존에 등록된 urlpatterns에 추가 설정
urlpatterns += static(
    prefix=settings.MEDIA_URL,
    document_root=settings.MEDIA_ROOT,
)

3. 인덱스 페이지 구성

 
  • localhost:8000 뒤에 아무런 경로 추가 없이 기본적으로 보여 줄 인덱스 페이지 구성
  • config/views.py
from django.shortcuts import render

def index(request):
    return render(request, "index.html")
  • templates/index.html
<!doctype html>
<html lang="ko">
<body>
    <h1>pystagram</h>
</body>
</html>
  • config/urls.py
from django.contrib import admin
from django.urls import path

from config.views import index

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

urlpatterns += static(
    prefix=settings.MEDIA_URL,
    document_root=settings.MEDIA_ROOT,
)
  • Terminal
python manage.py migrate
python manage.py runserver

 

4. 인증 시스템

 

4.1 CustomUser 모델 설정

4.1.1 App 생성

  • Terminal
python manage.py startapp users
  • config/settings.py
INSTALLED_APPS = [
    "users",
    ...
]

4.1.2 User 모델 생성

 
  • users/models.py
from django.contrib.auth.models import AbstractUser
from django.db import models

class User(AbstractUser):
    pass
  • AbstractUser 모델의 필드
    • username (사용자명, 로그인 할 때의 아이디)
    • password (비밀번호)
    • first_name (이름)
    • last_name (성)
    • emil (이메일)
    • is_staff (관리자 여부)
    • is_active (활성화 여부)
    • date_joined (가입일시)
    • last_login (마지막 로그인 일시)
  • config/settings.py
from pathlib import Path

AUTH_USER_MODEL = "users.User"
  • Terminal
# Migration 생성 및 적용
python manage.py makemigrations
python manage.py migrate
# 관리자 생성
python manage.py createsuperuser
# 서버 실행
python manage.py runserver

4.1.3 관리자 페이지에 모델 등록

  • users/admin.py
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin

from users.models import User

@admin.register(User)
class CustomUserAdmin(UserAdmin):
    pass
  • config/settings.py
# 관리자 페이지 한글 적용
LANGUAGE_CODE = "ko-kr"
TIME_ZONE = "Asia/Seoul"

4.1.4 CustomUser 모델에 Field 추가

  • users/models.py
class User(AbstractUser):
    profile_image = models.ImageField("프로필 이미지", upload_to="users/profile", blank=True)
    short_description = models.TextField("소개글", blank=True)
  • Terminal
# Migration 생성 및 적용
python manage.py makemigrations
python manage.py migrate
  • users/admin.py
@admin.register(User)
class CustomUserAdmin(UserAdmin):
    fieldsets = [
        (None, {"fields": ("username", "password")}),
        ("개인정보", {"fields": ("first_name", "last_name", "email")}),
        ("추가필드", {"fields": ("profile_image", "short_description")}),
        ("권한", {"fields": ("is_active", "is_staff", "is_superuser")}),
        ("중요한 일정", {"fields": ("last_login", "date_joined")}),
    ]

4.2 로그인/피드 페이지 기본 구조

4.2.1 로그인 페이지
  • 기본 구조 구성
    • View: login_view
    • Template: templetes/users/login.html
    • URL: /users/login/
  • users/views.py
from django.shortcuts import render

def login_view(request):
    return render(request, "users/login.html")
  • templates/users/login.html
<!doctype html>
<html lang="ko">
<body>
    <h1>Login</h>
</body>
</html>
  • users/urls.py
from django.urls import path
from users.views import login_view

urlpatterns = [
    path("login/", login_view)
]
  • config/urls.py
from django.contrib import admin
from django.urls import path, include

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

urlpatterns += static(
    ...

4.2.2 피드 페이지

 

  • 조건에 따른 View 동작 제어
    • 이미 사용자가 브라우저에서 로그인을 했다면
      • 피드 페이지(새글 목록)으로 이동
    • 사용자가 로그인을 한 적이 없거나 로그아웃을 했다면
      • 로그인 페이지로 이동

 

새글은 post로 들어가야하니 post를 위한 app하나 생성

 

  • Terminal
# App 생성
python manage.py startapp posts
  • config/settings.py
INSTALLED_APPS = [
    "users",
    "posts",
    ...
]
  • posts/views.py
from django.shortcuts import render

def feeds(request):
    return render(request, "posts/feeds.html")
  • posts/urls.py
from django.urls import path
from posts.views import feeds

urlpatterns = [
    path("feeds/", feeds),
]
  • templates/posts/feeds.html
<!doctype html>
<html lang="ko">
<body>
    <h1>Feeds</h>
</body>
</html>
  • config/urls.py
urlpatterns = [
    path("admin/", admin.site.urls),
    path("posts/", include("posts.urls")),
    path("users/", include("users.urls")),
    path("", index),
]

4.3 로그인 여부에 따른 접속 제한

4.3.1 관리자 페이지를 사용한 로그인/로그아웃

  • posts/views.py
from django.shortcuts import render

def feeds(request):
    user = request.user

    is_authenticated = user.is_authenticated

    print("user: ", user)
    print("is_authenticated: ", is_authenticated)

    return render(request, "posts/feeds.html")

4.3.2 로그인 여부에 따라 페이지 이동시키기

  • posts/views.py
from django.shortcuts import render, redirect

def feeds(request):
    if not request.user.is_authenticated:
        return redirect("/users/login/")

    return render(request, "posts/feeds.html")
  • users/views.py
from django.shortcuts import render, redirect

def login_view(request):
    if request.user.is_authenticated:
        return redirect("/posts/feeds/")
    return render(request, "users/login.html")

4.3.3 루트 경로에서 로그인 여부에 따라 페이지 이동

  • config/views.py
from django.shortcuts import redirect


def index(request):
    if request.user.is_authenticated:
        return redirect("/posts/feeds/")
    else:
        return redirect("/users/login/")

4.4 로그인 기능

 

4.4.1 Form 클래스를 사용한 로그인 페이지 구성

  • user/forms.py
from django import forms

class LoginForm(forms.Form):
    username = forms.CharField(min_length=3)
    password = forms.CharField(min_length=4)
  • Terminal
python manage.py shell
  • Terminal - Shell
from users.forms import LoginForm
login_data = {"username": "u", "password": "p"}
form = LoginForm(data=login_data)
form.is_valid()
form.errors

login_data2 = {"username": "Sample username", "password": "Sample password"}
form2 = LoginForm(data=login_data2)
form2.is_valid()
form2.errors
  • users/views.py
from django.shortcuts import render, redirect
from users.forms import LoginForm

def login_view(request):
    if request.user.is_authenticated:
        return redirect("/posts/feeds/")

    form = LoginForm()
    context = {"form": form,}
    return render(request, "users/login.html", context)
  • templates/users/login.html
...
<body>
    <h1>Login</h>
    {{ form.as_p }}
</body>
...

 

  • templates/users/login.html
...
<body>
    <h1>Login</h>
    <form method="POST">
        {% csrf_token %}
        {{ form.as_p }}
        <button type="submit">로그인</button>
    </form>
</body>
...

4.4.2 View에 전달된 데이터를 Form으로 처리하기

  • 예제
<form>
    # name이 food인 Input 3개
    <input name="food" value="햄버거">
    <input name="food" value="제육볶음">
    <input name="food" value="돈까스">
    # name이 drink인 input 1개
    <input name="drink" value="제로콜라">
</form>
  • URL 전달 예제
  • users/views.py
def login_view(request):
    if request.user.is_authenticated:
        return redirect("/posts/feeds/")

    if request.method == "POST":
        form = LoginForm(data=request.POST)
        print("form.is_valid(): ", form.is_valid())
        print("form.cleaned_data(): ", form.cleaned_data())

        context = {"form": form}
        return render(request, "users/login.html", context)
    else:
        form = LoginForm()
        context = {"form": form}
        return render(request, "users/login.html", context)

4.4.3 View에서 로그인 처리

 
  • users/views.py
from django.contrib.auth import authenticate, login
from django.shortcuts import render, redirect
from users.forms import LoginForm

def login_view(request):
    if request.user.is_authenticated:
        return redirect("/posts/feeds/")

    if request.method == "POST":
        form = LoginForm(data=request.POST)
        if form.is_valid():
            username = form.cleaned_data["username"]
            password = form.cleaned_data["password"]
            user = authenticate(username=username, password=password)

            if user:
                login(request, user)
                return redirect("/posts/feeds/")
            else:
                print("로그인에 실패했습니다.")

        context = {"form": form}
        return render(request, "users/login.html", context)
    else:
        form = LoginForm()
        context = {"form": form}
        return render(request, "users/login.html", context)
  • Terminal
# Authenticate
python manage.py shell
from django.contrib.auth import authenticate

user = authenticate(username='a', password='b')
print(user)

user = authenticate(username='pystagram', password='1234')
print(user)
exit()

4.5 로그아웃 구현 및 로그인 개선

4.5.1 로그아웃 기능

  • 로그아웃 기본 구조 구현
  • users/views.py
from django.contrib.auth import authenticate, login, logout

def login_view(request):
    ...

def logout_view(request):
    logout(request)
    return redirect("/users/login/")
  • users/urls.py
from users.views import login_view, logout_view

urlpatterns = [
    path("login/", login_view),
    path("logout/", logout_view),
]
  • templates/posts/feeds.html
<!doctype html>
<html lang="ko">
<body>
    <h1>Feeds</h1>
    <a href="/users/logout/">로그아웃</a>
</body>
</html>

4.5.2 로그인 개선

  • templates/posts/feeds.html
    • 피드 페이지에 로그인 상태 표시
<!doctype html>
<html lang="ko">
<body>
    <h1>Feeds</h1>
    <div>{{ user.username }} (ID: {{ user.id }})</div>
    <a href="/users/logout/">로그아웃</a>
</body>
</html>
  • users/views.py
    • 로그인 실패 시 정보 표시
def login_view(request):
    ...

    if request.method == "POST":
        form = LoginForm(data=request.POST)
        if form.is_valid():
            ...

            if user:
                ...
            else:
                form.add_error(None, "입력한 자격증명에 해당하는 사용자가 없습니다.")
  • templates/users/login.html
    • 로그인 페이지 CSS 스타일링, Form 기능 추가
    • 해당 style.css 와 splide.css 파일 다운받아서 파일에 적용
    • static/css/style.css
    • static/splide/ splide.css
{% load static %} # static에대한 자료 읽어오기
<!doctype html>
<html lang="ko">
<head>
    <link rel="stylesheet" href="{% static 'css/style.css' %}">
</head>
<body>
    <div id="login">
        <form method="POST">
        {% csrt_token %}
        {{ form.as_p }}
        <button type="submit" class="btn btn-login">로그인</button>
        </form>
    </div>
</body>
</html>
  • users/forms.py
from django import forms

class LoginForm(forms.Form):
    username = forms.CharField(
        min_length=3,
        widget=forms.TextInput(
            attrs={"placeholder": "사용자명 (3자리 이상)"},
        ),
    )
    password = forms.CharField(
        min_length=4,
        widget=forms.PasswordInput(
            attrs={"placeholder": "비밀번호 (4자리 이상)"},
        ),
    )

4.6 회원가입

 

4.6.1 기본 구조 생성

  • View: signup
  • URL: /users/signup/
  • Template: templates/users/signup.html
 
  • users/views.py
def signup(request):
    return render(request, "users/signup.html")
  • users/urls.py
from django.urls import path

from users.views import login_view, logout_view, signup

urlpatterns = [
    path("login/", login_view),
    path("logout/", logout_view),
    path("signup/", signup),
]
  • templates/users/signup.html
<!doctype html>
<html lang="ko">
<body>
    <h1>Sign up</h1>
</body>
</html>

4.6.2 SignupForm을 사용한 Tempalte 구성

  • users/forms.py
    • SighupForm 클래스 정의
class SignupForm(forms.Form):
    username = forms.CharField()
    password1 = forms.CharField(widget=forms.PasswordInput)
    password2 = forms.CharField(widget=forms.PasswordInput)
    profile_image = forms.ImageField()
    short_description = forms.CharField()
  • users/views.py
    • View에서 Template에 SignupForm 전달
from users.forms import LoginForm, SignupForm

def signup(request):
    form = SignupForm()
    context = {"form": form}
    return render(request, "users/signup.html", context)
  • templates/signup.html
<!doctype html>
<html lang="ko">
<body>
    <h1>Sign up</h1>
    <form method="POST" enctype="multipart/form-data">
    {% csrf_token %}
    {{ form.as_p }}
    <button type="submit">회원가입</button>
    </form>
</body>
</html>

4.6.3 View에 회원가입 로직 구현

  • Terminal
    • create_user 메서드
python manage.py shell

from users.models import User

user = User.objects.create(username="sample", password="sample")
print(user.id, user.username, user.password)
from django.contrib.auth import authenticate

result = authenticate(username="sample", password="sample")
print(result)
  • users/views.py
    • SignupForm의 데이터 가져오기
def signup(request):
    if request.method == "POST":
        print(request.POST)
        print(request.FILES)
    form = SignupForm()
    context = {"form": form}
    return render(request, "users/signup.html", context)
  • users/views.py
def signup(request):
    if request.method == "POST":
        form = SignupForm(data=request.POST, files=request.FILES)
        if form.is_valid():
            username = form.cleaned_data["username"]
            password1 = form.cleaned_data["password1"]
            password2 = form.cleaned_data["password2"]
            profile_image = form.cleaned_data["profile_image"]
            short_description = form.cleaned_data["short_description"]
            print(username)
            print(password1, password2)
            print(profile_image)
            print(short_description)
            user = form.save()
            login(request, user)
            return redirect("posts:feeds")
        context = {"form": form}
        return render(request, "users/signup.html", context)

    form = SignupForm()
    context = {"form": form}
    return render(request, "users/signup.html", context)
  • Terminal
    • User 생성하기
python manage.py shell

from users.models import User

User.objects.filter(username="root")
User.objects.filter(username="root").exists()

User.objects.filter(username="no_user")
User.objects.filter(username="no_user").exists()
  • users/views.py
from users.models import User

def signup(request):
    if request.method == "POST":
        form = SignupForm(data=request.POST, files=request.FILES)
        if form.is_valid():
            username = form.cleaned_data["username"]
            password1 = form.cleaned_data["password1"]
            password2 = form.cleaned_data["password2"]
            prifile_image = form.cleaned_data["profile_image"]
            short_description = form.cleaned_data["short_description"]

            if password1 != password2:
                form.add_error("password2", "비밀번호와 비밀번호 확인란의 값이 다릅니다.")

            if User.objects.filter(username=username).exists():
                form.add_error("username", "입력한 사용자명은 이미 사용중입니다.")

            if form.errors:
                context = {"form": form}
                return render(request, "users/signup.html", context)
            else:
                user = User.objects.create_user(
                    username = username,
                    password = password1,
                    profile_image = profile_image,
                    short_description = short_description,
                )
                login(request, user)
                return redirect("/posts/feeds/")
    else:
        form = SignupForm()
        context = {"form": form}
        return render(request, "users/signup.html", context)
728x90