본문 바로가기

PYTHON-BACK

#파이썬 34일차_이커머스 클론코딩4_(2)

728x90

Pystagram Project (3)

 

1. 동적 URL


1.1 URL 경로 변경

1.1.1 URL 경로를 변경할 때 생기는 중복작업

  • users/urls.py
urlpatterns = [
    ...
    path("login2/", login_view),
    ...
]
  • templates/users/signup.html
<div id="signup">
    <form method="POST" enctype="multipart/form-data">
        ...
        <a href="{% url '/users/login2/' %}">로그인 페이지로 이동</a>
    </form>
</div>
  • posts/views.py
def feeds(request):
    ...
    if not request.user.is_authenticated:
        return redirect("/users/login2/")
  • users/views.py
def logout_view(request):
    logout(request)
    return redirect("/users/login2/")
  • config/views.py
from django.shortcuts import redirect

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

1.2 Template의 동적 URL 변경

1.2.1 동적 URL 생성을 위한 요소 추가
  • 동적으로 URL을 생성해서 사용하기 위해서는 app별로 분리된 하위 urls.py에 app_name이라는 속성이 필요함
  • 일반적으로 app의 패키지명(디렉토리명)을 사용함
 
  • users/urls.py
...
app_name = "users"
urlpatterns = [
    ...
]
  • posts/urls.py
...
app_name = "posts"
urlpatterns = [
    ...
]

1.2.2 Template을 위한 {% url %} 태그

{% url "URL pattern name" %} 태그는 Template에서 urls.py의 내용을 이용해 동적으로 URL을 생성함

{urls.py에 있는 app_name}:{path()에 지정된 name}
  • users/urls.py
...
app_name = "users"
urlpatterns = [
    path("login2/", login_view, name="login"),
    path("logout/", logout_view, name="logout"),
    path("signup/", signup, name="sighup"),
]
  • templates/users/signup.html
    • 실제로 동적 링크를 만들어보기
<a href="{% url 'users:login' %}">로그인 페이지로 이동</a>
  • posts/urls.py
app_name - "posts"
urlpatterns = [
    path("feeds/", feeds, name="feeds"),
    path("comment_add/", comment_add, name="comment_add"),
    path("comment_delete/<int:comment_id>/", comment_delete, name="comment_delete"),
    path("post_add/", post_add, name="post_add"),
]

1.2.3 {% url %} 태그를 사용하도록 기존 Template 코드 수정

  • templates/users/login.html
    • 로그인 페이지
<a href="{% url 'users:signup' %}">회원가입 페이지로 이동</a>
  • templates/users/signup.html
    • 회원가입 페이지
<a href="{% url 'users:login' %}">로그인 페이지로 이동</a>
  • templates/posts/post_add.html
    • 글 작성 페이지
<nav>
    <h1>
        <a href="{% url 'posts:feeds' %}">Pystagram</a>
    </h1>
    <a href="{% url 'users:logout' %}">Logout</a>
</nav>
  • templates/posts/feeds.html
    • 피드 페이지 - 내비게이션 바 부분
<nav>
    <h1>
        <a href="{% url 'posts:feeds' %}">Pystagram</a>
    </h1>
    <a href="{% url 'posts:post_add' %}">Add post</a>
    <a href="{% url 'users:logout' %}">Logout</a>
</nav>
  • templates/posts/feeds.html
    • 댓글 삭제 부분
<div class="post-comments">
    ...
    {% if user == comment.user %}
        <form method="POST" action="{% url 'posts:comment_delete' comment_id=comment.id %}">

1.3 View의 동적 URL 변경

1.3.1 View를 위한 reverse 함수
 
  • Terminal
    • Template에서 {% url %} 태그를 사용하듯 View에서는 reverse 함수로 동적 URL을 생성할 수 있음
python manage.py shell

from django.urls import reverse

reverse('users:login')
reverse('posts:feeds')

# 추가 인수를 dict에 담아 키워드 인수로 전달
reverse('posts:comment_delete', kwargs={'comment_id': 1})

# 추가 인수를 list에 담아 위치 인수로 전달
reverse('posts:comment_delete', args=[1])

1.3.2 reverse 함수를 사용하도록 기존 View 코드 수정

  • 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")
  • users/views.py
from django.urls import reverse

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


def logout_view(request):
    logout(request)
    return redirect("users:login")


def signup(request):
    ...
    login(request, user)
    return redirect("posts:feeds")
    ...
  • posts/views.py
from django.urls import reverse

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


def comment_add(request):
    ...
    if form.is_valid():
        comment = form.save(commit=False)
        comment.user = request.user
        comment.save()
        ...
        url_next = reverse("posts:feeds") + f"#post-{comment.post.id}"
        return HttpResponseRedirect(url_next)


def comment_delete(request, comment_id):
    if request.method == "POST":
        comment = Comment.objects.get(id=comment_id)
        if comment.user == request.user:
            comment.delete()
            url_next = reverse("posts:feeds") + f"#post-{comment.post.id}"
            return HttpResponseRedirect(url_next)
    ...


def post_add(request):
    if request.method == "POST":
        ...
        if form.is_valid():
            ...
            url_next = reverse("posts:feeds") + f"#post-{post.id}"
            return HttpResponseRedirect(url_next)
    ...
  • users/urls.py
...
app_name = "users"
urlpatterns = [
    path("login/", login_view, name="login"),
    ...

. 해시태그

2.1 다대다 관계 모델
 

2.1.1 다대다 관계모델

  • 다대일(Many-to-One, N:1) 관계
    • 한 테이블의 한 레코드가 다른 테이블의 여러 레코드와 연관됨을 나타내는 관계
  • 다대다(Many-to-Many, M2M, N:N) 관계
    • 한 테이블의 여러 레코드가 다른 테이블의 여러 레코드와 연관됨을 나타내는 관계
 
  • 예를 들어
    • 학생은 하나의 대학교에만 속할 수 있음: 다대일(학생:학교) 관계
    • 한 학생은 여러 개의 수업을 수강신청할 수 있으며 하나의 수업은 그 수업을 수강신청한 여러 명의 학생을 가질 수 있음: 다대다관계
 

2.1.2 다대다 테이블 구조

2.1.3 해시태그 모델 생성, ManyToMany 연결

  • posts/models.py
class HashTag(models.Model):
    name = models.CharField("태그명", max_length=50)
  • posts/models.py
class Post(models.Model):
    ...
    tags = models.ManyToManyField(HashTag, verbose_name="해시태그 목록", blank=True)
  • Terminal
    • posts/models.py에서 HashTag 클래스를 Post 클래스보다 아래쪽에 선언
    • Post 클래스가 정의될 때는 HashTag 클래스를 알 수 없음
    • ForeignKey, ManyToManyField를 사용할 때 아래쪽에 선언한 모델을 참조하려면 문자열을 사용함
python manage.py runserver

NameError: name 'HashTag' is not defined
tags = models.ManyToManyField("posts.HashTag", verbose_name="해시태그 목록", blank=True)
python manage.py makemigrations
python manage.py migrate

2.2 다대다 모델 admin

2.2.1 admin 구현

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

@admin.register(HashTag)
class HashTagAdmin(admin.ModelAdmin):
    pass
  • posts/models.py
class HashTag(models.Model):
    name = models.CharField("태그명", max_length=50)

    def __str__(self):
        return self.name
  • posts/admins.py
from django.db.models import ManyToManyField
from django.forms import CheckboxSelectMultiple
...
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    ...
    formfield_overrides = {
        ManyToManyField: {"widget": CheckboxSelectMultiple},
    }

2.2.2 Template에 Post의 HashTag 표시

  • templates/posts/feeds.html
<div class="post-content">
    {{ post.content|linebreaksbr }}
    <div class="post-tags">
        {% for tag in post.tags.all %}
            <span>#{{ tag.name }}</span>
        {% endfor %}
    </div>
</div>

2.3 해시태그 검색

2.3.1 해시태그의 사용 예시

2.3.2 기본 구조 구현

  • View: posts/views.py → tags
  • URL: /posts/tags/{tag의 name}/
  • Template: templates/posts/tags.html
  • posts/views.py
...
def tags(request, tag_name):
    return render(request, "posts/tags.html")
  • posts/urls.py
from posts.views import ..., tags
...
urlpatterns = [
    ...
    path("tags/<str:tag_name>/", tags, name="tags"),
]
  • templates/posts/tags.html
{% extends 'base.html' %}

{% block content %}
    <nav>
        <h1>
            <a href="{% url 'posts:feeds' %}">Pystagram</a>
        </h1>
        <a href="{% url 'posts:post_add' %}">Add Post</a>
        <a href="{% url 'users:logout' %}">Logout</a>
    </nav>
    <div id="tags">
        <header class="tags-header">
            <h2>#{{ tag_name }}</h2>
            <div>게시물 1,094</div>
        </header>
        <div class="post-grid-container">
def tags(request, tag_name):
    tag = HashTag.objects.get(name=tag_name)
    posts = Post.objects.filter(tags=tag)

    context = {
        "tag_name": tag_name,
        "posts": posts,
    }
    return render(request, "posts/tags.html", context)
            <div class="post-grid"></div>
            <div class="post-grid"></div>
            <div class="post-grid"></div>
            <div class="post-grid"></div>
            <div class="post-grid"></div>
            <div class="post-grid"></div>
            <div class="post-grid"></div>
            <div class="post-grid"></div>
        </div>
    </div>
{% endblock %}

2.3.3 View에서 해시태그를 찾고 해당하는 Post 목록 돌려주기

  • posts/views.py
from posts.models import Post, Comment, PostImage, HashTag

def tags(request, tag_name):
    tag = HashTag.objects.get(name=tag_name)
    print(tag)

    return render(request, "posts/tags.html")
  • posts/views.py
def tags(request, tag_name):
    tag = HashTag.objects.get(name=tag_name)
    posts = Post.objects.filter(tags=tag)

    context = {
        "tag_name": tag_name,
        "posts": posts,
    }
    return render(request, "posts/tags.html", context)

2.3.4 Post 목록만큼 Grid 랜더링, tag_name 표시

  • templates/posts/tags.html
...
</nav>
<div id="tags">
    <header class="tags-header">
        <h2>#{{ tag_name }}</h2>
        <div>게시물 {{ posts.count }}</div>
    </header>
    <div class="post-grid-container">
        {% for post in posts %}
            <div class="post-grid"></div>
        {% endfor %}
    </div>
</div>

2.3.5 각각의 Post가 가진 첫 번째 이미지 보여주기

  • templates/posts/tags.html
<div class="post-grid-container">
    {% for post in posts %}
        {% if post.postimage_set.first and post.postimage_set.first.photo %}
            <div class="post-grid">
                <img src="{{ post.postimage_set.first.photo.url }}" alt="">
            </div>
        {% endif %}
    {% endfor %}
</div>
  • posts/views.py
def tags(request, tag_name):
    try:
        tag = HashTag.objects.get(name=tag_name)
    except HashTag.DoesNotExist:
        # tag_name에 해당하는 HashTag를 찾지 못한 경우 빈 QuerySet을 돌려준다
        posts = Post.objects.none()
    else:
        posts = Post.objects.filter(tags=tag)

    context = {
        "tag_name": tag_name,
        "posts": posts,
    }
    return render(request, "posts/tags.html", context)
  • templates/posts/tags.html
<div class="post-grid-container">
    {% for post in posts %}
        {% if post.postimage_set.first and post.postimage_set.first.photo %}
            <div class="post-grid">
                <img src="{{ post.postimage_set.first.photo.url }}" alt="">
            </div>
        {% endif %}
    {% empty %}
        <p>검색된 게시물이 없습니다</p>
    {% endfor %}
</div>

2.3.7 피드 페이지의 글에서 해시태그 링크 생성

  • templates/posts/feeds.html
<div class="post-content">
    {{ post.content|linebreaksbr }}
    <div class="post-tags">
        {% for tag in post.tags.all %}
            <a href="{% url 'posts:tags' tag_name=tag.name %}">#{{ tag.name }}</a>
        {% endfor %}
    </div>
</div>

2.4 해시태그 생성

2.4.1 ManyToManyField 항목 추가 실습

  • Terminal
python manage.py shell

from posts.models import Post, HashTag

tag = HashTag.objects.create(name='테스트해시태그')
tag

from users.models import User

user = User.objects.first()
user

post = Post.objects.create(user=user, content='HashTag 테스트용 Post')
post
post.tags.all

post.tags.add(tag)
post.tags.all()

2.4.2 해시태그 추가 Input 구현

  • templates/posts/post_add.html
...
<form method="POST" enctype="multipart/form-data">
    {% csrf_token %}
    <div>
        <!-- label의 for속성에는 가리키는 input의 id값을 입력 -->
        <label for="id_images">이미지</label>
        <input id="id_images" name="images" type="file" multiple>
    </div>
    {{ form.as_p }}
    <div>
        <label for="id_tags">해시태그</label>
        <input id="id_tags" name="tags" type="text" placeholder="쉼표(,)로 구분하여 여러 태그 입력">
    </div>
    <button type="submit">게시</button>
</form>
...

2.4.3 쉼표로 구분된 문자열 처리

  • Terminal
python manage.py shell

tag_string = 'coffee,latte'
tag_string.split(',')

# 중간에 공백이 있는 경우 공백을 처리하지는 못함
tag_string = 'coffee, latte'
tag_string.split(',')

# 좌우 공백을 없애는 함수는 strip()
'   좌우 공백 포함    '.strip()

# list comprehension으로 리스트 내의 공백 문자열 없애기
tag_string = 'coffee,latte'
tag_list = [tag.strip for tag in tag_string.split(',')]
tag_list
  • posts/views.py
def post_add(request):
    if request.method == "POST":
        form = PostForm(request.POST)

        if form.is_valid():
            post = form.save(commit=False)
            ...

            for image_file in request.FILES.getlist("images"):
                ...

            # "tags"에 전달 된 문자열을 분리해 HashTag생성
            tag_string = request.POST.get("tags")
            if tag_string:
                tag_names = [tag_name.strip() for tag_name in tag_string.split(",")]
                for tag_name in tag_names:
                    tag, _ = HashTag.objects.get_or_create(name=tag_name)
                    # get_or_create로 생성하거나 가져온 HashTag객체를 Post의 tags에 추가한다
                    post.tags.add(tag)

            # 모든 PostImage와 Post의 생성이 완료되면
            # 피드페이지로 이동하여 생성된 Post의 위치로 스크롤되도록 한다
            url = reverse("posts:feeds") + f"#post-{post.id}"
            return HttpResponseRedirect(url)
    ...
728x90