본문 바로가기

PYTHON-BACK

#파이썬 33일차_이커머스 클론코딩3

728x90
  • Terminal
    • User 생성하기
python manage.py shell

from users.models import User

User.objects.filter(username="pystagram")
User.objects.filter(username="pystagram").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)

4.6.4 SignupForm 내부에서 데이터 유효성 검사

  • users/forms.py
    • clean_username 메서드 작성
from django import forms
from django.core.exceptions import ValidationError

from users.models import User

class SignupForm(forms.Form):
    ...
    def clean_username(self):
        username = self.cleaned_data["username"]
        if User.objects.filter(username=username).exists():
            raise ValidationError(f"입력한 사용자명({username})은 이미 사용중입니다")
        return username
  • users/forms.py
    • clean 메서드로 password1, password2 검증
class SignupForm(forms.Form):
    def clean_username(self):
        ...

    def clean(self):
        password1 = self.cleaned_data["password1"]
        password2 = self.cleaned_data["password2"]
        if password1 != password2:
            self.add_error("password2", "비밀번호와 비밀번호 확인란의 값이 다릅니다")
  • users/views.py
    • View 함수와 SignupForm 리팩토링
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"]
            prifile_image = form.cleaned_data["profile_image"]
            short_description = form.cleaned_data["short_description"]

            user = User.objects.create_user(
                username = username
                password = password1
                prifile_image = prifile_image
                short_description = short_description
            )

            login(request, user)
            return redirect("/posts/feeds/")
        else:
            context = {"form": form}
            return render(request, "users/signup.html", context)
    else:
        form = SignupForm()
        context = {"form": form}
        return render(request, "users/signup.html", context)
  • users/forms.py
    • View 함수와 SignupForm 리팩토링
class SignupForm(forms.Form):
    def clean(self):
        ...

    def save(self):
        username = self.cleaned_data["username"]
        password1 = self.cleaned_data["password1"]
        profile_image = self.cleaned_data["profile_image"]
        short_description = self.cleaned_data["short_description"]
        user = User.objects.create_user(
            username=username,
            password=password1,
            profile_image=profile_image,
            short_description=short_description,
        )
        return user
  • users/views.py
    • SignupForm으로 이동시킨 save() 함수 삭제
    • 새로 만든 save()함수를 사용하도록 변경
def signup(request):
    if request.method == "POST":
        form = SignupForm(data=request.POST, files=request.FILES)

        if form.is_valid():
            user = form.save()
            login(request, user)
            return redirect("/posts/feeds/")

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

    else:
        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():
            user = form.save()
            login(request, user)
            return redirect("/posts/feeds/")

    else:
        form = SignupForm()

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

4.6.5 View 함수에서 진행되는 케이스

  • GET 요청
    • SignupForm()으로 생성된 빈 form을 사용자에게 보여줌
def signup(request):
    if request.method == "POST":
        # 해당 없음

    else:
        form = SignupForm()

    # context에 빈 Form이 전달됨
    context = {"form": form}
    return render(request, "users/signup.html", context)
  • POST 요청이며, 데이터를 받은 SignupForm이 유효한 경우
    • SignupForm(data=...)으로 생성된 form의 save() 메서드로 User 생성, redirect로 경로가 변경됨
def signup(request):
    # POST 요청 시 form이 유효하다면 최종적으로 rediret 처리됨
    if request.method == "POST":
        form = SignupForm(data=request.POST, files=request.FILES)

        if form.is_valid():
            user = form.save()
            login(request, user)
            return redirect("/posts/feeds/")

    # 이후 로직은 실행되지 않음
  • POST 요청이며, 데이터를 받은 SignupForm이 유효하지 않은 경우
    • SignupForm(data=...)으로 생성된 form에는 error가 추가되며, 그 form을 사용자에게 보여줌
def signup(request):
    if request.method == "POST":
        form = SignupForm(data=request.POST, files=request.FILES)

        if form.is_valid():
            # 검증에 실패하여 이 영역으로 들어오지 못함

    # context에 error를 포함한 form이 전달됨
    context = {"form": form}
    return render(request, "users/signup.html", context)

4.6.6 Template 스타일링과 구조 리팩토링

  • templates/users/signup.html
{% load static %}
<!document html>
<html lang="ko">
<head>
    <link rel="stylesheet" href="{% static 'css/style.css' %}">
</head>
<body>
    <div id="signup">
        <form method="POST" enctype="multipart/form-data">
            <h1>Pystagram</h1>
            {% csrf_token %}
            {{ form.as_p }}
            <button type="submit" class="btn btn-signup">가입</button>
        </form>
    </div>
</body>
</html>
  • templates/base.html
    • Template을 확장하는 {% extends %} 태그
{% load static %}
<!doctype html>
<html lang="ko">
<head>
    <link rel="stylesheet" href="{% static 'css/style.css' %}">
</head>
<body>
    {% block content %}{% endblock %}
</body>
</html>
  • templates/users/login.html
{% extends 'base.html' %}

{% block content %}
<div id="login">
    <form method="POST">
        <h1>Pystagram</h1>
        {% csrf_token %}
        {{ form.as_p }}
        <button type="submit" class="btn btn-login">로그인</button>
    </form>
</div>
{% endblock %}
  • templates/users/signup.html
{% extends 'base.html' %}

{% block content %}
<div id="signup">
    <form method="POST" enctype="multipart/form-data">
        <h1>Pystagram</h1>
        {% csrf_token %}
        {{ form.as_p }}
        <button type="submit" class="btn btn-signup">가입</button>
    </form>
</div>
{% endblock %}
  • templates/users/signup.html
    • 회원가입과 로그인 페이지 간의 링크 추가
<div id="signup">
    <form method="POST" enctype="multipart/form-data">
        ...
        <button type="submit" class="btn btn-signup">가입</button>
        <a href="/users/login/">로그인 페이지로 이동</a>
    </form>
</div>
  • templates/users/login.html
<div id="login">
    <form method="POST">
        ...
        <button type="submit" class="btn btn-login">로그인</button>
        <a href="/users/sighup/">회원가입 페이지로 이동</a>
    </form>
</div>

Pystagram Project (2)

1. 피드 페이지

 

1.1 글/이미지/댓글 모델링

 

1.1.1 Model 구성

  • posts/models.py
from django.db import models


class Post(models.Model):
    user = models.ForeignKey("users.User", verbose_name="작성자", on_delete=models.CASCADE)
    content = models.TextField("내용")
    created = models.DateTimeField("생성일시", auto_now_add=True)


class PostImage(models.Model):
    post = models.ForeignKey(Post, verbose_name="포스트", on_delete=models.CASCADE)
    photo = models.ImageField("사진", upload_to="post")


class Comment(models.Model):
    user = models.ForeignKey("users.User", verbose_name="작성자", on_delete=models.CASCADE)
    post = models.ForeignKey(Post, verbose_name="포스트", on_delete=models.CASCADE)
    content = models.TextField("내용")
    created = models.DateTimeField("생성일시", auto_now_add=True)
  • Terminal
python manage.py makemigrations
python manage.py migrate

1.1.2 admin 구현

  • posts/admin.py
from django.contrib import admin
from posts.models import Post, PostImage, Comment


@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    list_display = [
        "id",
        "content",
    ]


@admin.register(PostImage)
class PostImageAdmin(admin.ModelAdmin):
    list_display = [
        "id",
        "post",
        "photo",
    ]


@admin.register(Comment)
class CommentAdmin(admin.ModelAdmin):
    list_display = [
        "id",
        "post",
        "content",
    ]

1.2 admin에 연관 객체 표시

1.2.1 ForeignKey로 연결된 객체 확인

  • posts/admin.py
class CommentInline(admin.TabularInline):
    model = Comment
    extra = 1

@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    ...
    inlines = [
        CommentInline,
    ]
  • posts/admin.py
class CommentInline(admin.TabularInline):
    ...

class PostImageInline(admin.TabularInline):
    model = PostImage
    extra = 1

@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    ...
    inlines = [
        CommentInline,
        PostImageInline,
    ]

1.2.2 썸네일 이미지 표시

  • posts/admin.py
    • 직접 admin을 조작해서 썸네일 표시 코드 추가
from django.contrib.admin.widgets import AdminFileWidget
from django.db import models
from django.utils.safestring import mark_safe
...

class CommentInline(admin.TabularInline):
    ...

# AdminFileWidget은 관리자 페이지에서 '파일 선택' 버튼을 보여주는 부분
# 이 widget을 커스텀하여 <img> 태그를 추가함
class InlineImageWidget(AdminFileWidget):
    def render(self, name, value, attrs=None, renderer=None):
        html = super().render(name, value, attrs, renderer)
        if value and getattr(value, "url", None):
            html = mark_safe(f'<img src="{value.url}" width="150" height="150">') + html
        return html

# ImageField를 표시할 때, AdminFileWidget을 커스텀한 InlineImageWidget을 사용함
class PostImageInline(admin.TabularInline):
    model = PostImage
    extra = 1
    formfield_overrides = {
        models.ImageField: {
            "widget": InlineImageWidget,
        }
    }
  • Terminal
    • 오픈소스 라이브러리를 사용한 썸네일 표시
pip install django-admin-thumbnails
  • posts/admin.py
# 위에서 추가한 코드들은 모두 삭제하고 썸네일 라이브러리를 사용함
import admin_thumbnails

@admin_thumbnails.thumbnail("photo")
class PostImageInline(admin.TabularInline):
    model = PostImage
    extra = 1

1.3 피드 페이지

1.3.1 View 작성

  • posts/views.py
from posts.models import Post

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

    posts = Post.objects.all()
    context = { "posts": posts }
    return render(request, "posts/feeds.html", context)

1.3.2 작성자 정보 표시

  • templates/posts/feeds.html
{% extends 'base.html' %}
{% block content %}
    <nav>
        <h1>Pystagram</h1>
    </nav>
    <div id="feeds" class="post-container">
        {% for post in posts %}
            <article class="post">
                <header class="post-header">
                    {% if post.user.profile_image %}
                        <img src="{{ post.user.profile_image.url }}">
                    {% endif %}
                    <span>{{ post.user.username }}</span>
                </header>
            </article>
        {% endfor %}
    </div>
{% endblock %}

1.3.3 이미지 슬라이더 구현

 

  • templates/base.html
    • 이미지 슬라이드 자바스크립트, CSS 파일 불러오기
{% load static %}
<!doctype html>
<html lang="ko">
<head>
    <link rel="stylesheet" href="{% static 'css/style.css' %}">
    <link rel="stylesheet" href="{% static 'splide/splide.css' %}">
    <script src="{% static 'splide/splide.js' %}"></script>
</head>
<body>
...
  • templates/posts/feeds.html
    • Splide 라이브러리 사용
{% extends 'base.html' %}
{% block content %}
...
    <div id="feeds" class="post-container">
        {% for post in posts %}
            <article class="post">
                <header class="post-header">
                    ...
                </header>

                <!-- 이미지 슬라이드 영역 시작 -->
                <div class="post-images splide">
                    <div class="splide__track">
                        <ul class="splide__list">
                            {% for image in post.postimage_set.all %}
                                {% if image.photo %}
                                    <li class="splide__slide">
                                        <img src="{{ image.photo.url }}">
                                    </li>
                                {% endif %}
                            {% endfor %}
                        </ul>
                    </div>
                </div>
                <!-- 이미지 슬라이드 영역 종료 -->
            </article>
        {% endfor %}
    </div>
{% endblock %}
  • templates/posts/feeds.html
    • 템플릿 하단에 자바스크립트 코드 작성
{% block content %}
    <div id="feeds" class="post-container">
        ...
    </div>
    <!-- content 블록의 최하단에 작성함 -->
    <script>
        const elms = document.getElementsByClassName('splide')
        for (let i = 0; i < elms.length; i++){
            new Splide(elms[i]).mount();
        }
    </script>
{% endblock %}

1.3.4 글 속성 출력

  • templates/posts/feeds.html
    • 글 내용 출력
<article class="post">
    <header class="post-header">...</header>
    <div class="post-images">...</div>
    <div class="post-content">
        {{ post.content|linebreaksbr }}
    </div>
  • templates/posts/feeds.html
    • 좋아요/댓글 버튼 표시
<div class="post-content">...</div>
<div class="post-buttons">
    <button>Likes(0)</button>
    <span>Comments(0)</span>
</div>
  • templates/posts/feeds.html
    • 댓글 목록 표시
<div class="post-buttons">...</div>
<div class="post-comments">
    <ul>
        <1-- 각 Post에 연결된 PostComment들을 순회 -->
        {% for comment in post.comment_set.all %}
            <li>
                <span>{{ comment.user.username }}</span>
                <span>{{ comment.content }}</span>
            </li>
        {% endfor %}
    </ul>
    <button>Likes(0)</button>
    <span>Comments(0)</span>
</div>
  • templates/posts/feeds.html
    • 작성일자, 댓글 입력창 표시
<div class="post-comments">...</div>
<small>{{ post.created }}</small>
<div class="post-comments-create">
    <input type="text" placeholder="댓글 달기...">
    <button type="submit">게시</button>
</div>

1.3.5 Template에 링크 추가

  • templates/posts/feeds.html
    • 메인 링크 추가
<nav>
    <h1>
        <a href="/posts/feeds/">Pystagram</a>
    </h1>
</nav>
  • templates/posts/feeds.html
    • 로그아웃 버튼 추가
<nav>
    <h1>
        <a href="/posts/feeds/">Pystagram</a>
    </h1>
    <a href="/users/logout/">Logout</a>
</nav>
  • 전체 코드 (feeds.html)
{% extends 'base.html' %}
{% block content %}
    <nav>
        <h1>
            <a href="/posts/feeds/">Pystagram</a>
        </h1>
        <a href="/users/logout/">Logout</a>
        </h1>
    </nav>
    <div id="feeds" class="post-container">
        {% for post in posts %}
            <article class="post">
                <header class="post-header">
                    {% if post.user.profile_image %}
                        <img src="{{ post.user.profile_image.url }}">
                    {% endif %}
                    <span>{{ post.user.username }}</span>
                </header>
                <!-- 이미지 슬라이드 영역 시작 -->
                <div class="post-images splide">
                    <div class="splide__track">
                        <ul class="splide__list">
                            {% for image in post.postimage_set.all %}
                                {% if image.photo %}
                                    <li class="splide__slide">
                                        <img src="{{ image.photo.url }}">
                                    </li>
                                {% endif %}
                            {% endfor %}
                        </ul>
                    </div>
                </div>
                <!-- 이미지 슬라이드 영역 종료 -->
                <div class="post-content">
                    {{ post.content|linebreaksbr }}
                </div>
                <div class="post-buttons">
                    <button>Likes(0)</button>
                    <span>Comments(0)</span>
                </div>
                <div class="post-comments">
                    <ul>
                        <!-- 각 Post에 연결된 PostComment들을 순회 -->
                        {% for comment in post.comment_set.all %}
                            <li>
                                <span>{{ comment.user.username }}</span>
                                <span>{{ comment.content }}</span>
                            </li>
                        {% endfor %}
                    </ul>
                </div>
                <small>{{ post.created }}</small>
                <div class="post-comments-create">
                    <input type="text" placeholder="댓글 달기...">
                    <button type="submit">게시</button>
                </div>                                                
            </article>
        {% endfor %}
    </div>
    <!-- content 블록의 최하단에 작성함 -->
    <script>
        const elms = document.getElementsByClassName('splide')
        for (let i = 0; i < elms.length; i++){
            new Splide(elms[i]).mount();
        }
    </script>
{% endblock %}

 

 

2. 글과 댓글

 

2.1 댓글 작성

 

2.1.1 CommentForm 구현

 
  • posts/forms.py
    • ModelForm
from django import forms
from posts.models import Comment

class CommentForm(forms.ModelForm):
    class Meta:
        model = Comment
        fields = [
            "content",
        ]
  • Terminal
    • 오류 발생
      • posts_comment 테이블의 post_id 필드는 NULL을 허용하지 않는다는 메시지
python manage.py shell

from posts.forms import CommentForm

data = {"content": "SampleContent"}
form = CommentForm(data=data)
form.is_valid()
form.save()
  • Terminal
    • 오류 해결 방법
      • CommentForm으로 Comment 객체를 일단 만들되, 메모리 상에 객체를 만들고 필요한 데이터를 나중에 채우기
      • CommentForm에 NULL을 허용하지 않는 모든 필드를 선언하고 인스턴스 생성 시 유효한 데이터를 전달
    • 첫 번째 방법으로 해결해보기
python manage.py shell

from posts.forms import CommentForm

data = {"content": "SampleContent"}
form = CommentForm(data=data)
form.is_valid()
comment = form.save(commit=False)
print(comment.id)

from users.models import User
from posts.models import Post

user = User.objects.all()[0]
post = Post.objects.all()[0]
print(user)
print(post)

comment.user = user
comment.post = post
comment.save()

comment.id

728x90