Mini Project: Pystagram 만들기

  • 코드출처: 이한영의 Django 입문(디지털북스)

부가 기능 구현

1. 동적 URL

1.1 URL 경로 변경

  • URL 경로를 변경할 때 생기는 중복작업
    • users/urls.py

        urlpatterns = [
            ...
            path("login2/", login_view),
            ...
        ]
      
    • templates/users/signup.html

        { % raw %}
        <div id="signup">
            <form method="POST" enctype="multipart/form-data">
                ...
                <a href="{ % url '/users/login2/' %}">로그인 페이지로 이동</a>
            </form>
        </div>
        { % endraw %}
      
    • 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 변경

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

      app_name = "users"
      urlpatterns = [
          ...
      ]
    
  • posts/urls.py

      app_name = "posts"
      urlpatterns = [
          ...
      ]
    
  • Template을 위한 { % raw %}{ % url %}{ % endraw %} 태그

    • { % raw %}{ % url “URL pattern name” %}{ % endraw %} 태그는 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

        { % raw %}
        <a href="{ % url 'users:login' %}">로그인 페이지로 이동</a>
        { % endraw %}
      
    • 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"),
        ]
      
  • { % raw %} { % url %}{ % endraw %} 태그를 사용하도록 기존 Template 코드 수정

    • 로그인 페이지
      • templates/users/login.html

          { % raw %}
          <a href="{ % url 'users:signup' %}">회원가입 페이지로 이동</a>
          { % endraw %}
        
    • 회원가입 페이지
      • templates/users/signup.html

          { % raw %}
          <a href="{ % url 'users:login' %}">로그인 페이지로 이동</a>
          { % endraw %}
        
    • 글 작성 페이지
      • templates/posts/post_add.html

          { % raw %}
          <nav>
              <h1>
                  <a href="{ % url 'posts:feeds' %}">Pystagram</a>
              </h1>
              <a href="{ % url 'users:logout' %}">Logout</a>
          </nav>
          { % endraw %}
        
    • 피드 페이지 - 내비게이션 바 부분
      • templates/posts/feeds.html

          { % raw %}
          <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>
          { % endraw %}
        
    • 댓글 삭제 부분
      • templates/posts/feeds.html

          { % raw %}
          <div class="post-comments">
              ...
              { % if user == comment.user %}
                  <form method="POST" action="{ % url 'posts:comment_delete' comment_id=comment.id %}">
          { % endraw %}
        
    • 댓글 생성 부분
      • templates/posts/feeds.html

          { % raw %}
          <div class="post-comment-create">
              <form method="POST" action="{ % url 'posts:comment_add' %}">
          { % endraw %}
        

1.3 View의 동적 URL 변경

  • View를 위한 reverse 함수

    • Terminal
      • Template에서 { % raw %} { % url %}{ % endraw %} 태그를 사용하듯 View에서는 reverse 함수로 동적 URL을 생성할 수 있음

          python manage.py shell
        
          from django.urls import reverse
        
          reverse('users:login')
          reverse('posts:feeds')
        
          # 추가 인수를 dict에 담아 키워드 인수로 전달
          reverse('posts:comment_delete', kwarg={'comment_id': 1})
        
          # 추가 인수를 list에 담아 위치 인수로 전달
          reverse('posts:comment_delete', args=[1])
        
  • 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 = reverse("posts:feeds") + f"#post-{comment.post.id}"
                    return HttpResponseRedirect(url)
            ...
      
        def post_add(request):
            if request.method == "POST":
                ...
                if form.is_valid():
                    ...
                    url = reverse("posts:feeds") + f"#post-{post.id}"
                    return HttpResponseRedirect(url)
            ...
      
    • users/urls.py

        app_name = "users"
        urlpatterns = [
            path("login/", login_view, name="login"),
            ...
      

2. 해시태그

2.1 다대다 관계 모델

  • 다대다 관계모델

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

    • 학생 테이블

      id이름
      1김럭스
      2최이즈리얼
      3박룰루
    • 수업 테이블

      id수업명
      1협곡
      2칼바람
      3TFT
    • 학생과 수업을 중개하는 테이블

      학생수업
      11
      12
      21
      22
      23
  • 해시태그 모델 생성, 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
      
        tags = models.ManyToManyField("posts.HashTag", verbose_name="해시태그 목록", blank=True)
      
        python manage.py makemigrations
        python manage.py migrate
      

2.2 다대다 모델 admin

  • 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/models.py

        from django.db.models import ManyToManyField
        from django.forms import CheckboxSelectMultiple
        ...
        @admin.register(Post)
        class PostAdmin(admin.ModelAdmin):
            ...
            formfield_overrides = {
                ManyToManyField: {"widget": checkboxSelectMultiple},
            }
      
  • 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 해시태그 검색

  • 해시태그의 사용 예시

  • 기본 구조
    • 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

        { % raw %}
        { % 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">
                    <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 %}
        { % endraw %}
      
  • 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)
      
  • Post 목록만큼 Grid 랜더링, tag_name 표시
    • templates/posts/tags.html

        { % raw %}
        ...
        </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>
        { % endraw %}
      
  • 각각의 Post가 가진 첫 번째 이미지 보여주기
    • templates/posts/tags.html

        { % raw %}
        <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>
        { % endraw %}
      
    • 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

        { % raw %}
        <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>
        { % endraw %}
      
  • 피드 페이지의 글에서 해시태그 링크 생성
    • templates/posts/feeds.html

        { % raw %}
        <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>
        { % endraw %}
      

2.4 해시태그 생성

  • 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()
      
  • 해시태그 추가 Input 구현
    • templates/posts/post_add.html

        { % raw %}
        <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>
        { % endraw %}
      
  • 쉼표로 구분된 문자열 처리
    • 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)
            ...