FrameWork/pinterest clone

9. articleapp만들기

mansoorrr 2023. 7. 22. 23:11

계정관련된 앱을 모두 완료했고 이제 게시물을 만든다.

먼저 만들 앱은 articleapp이다. articleapp을 만들기 전에 pinterest사이트를 보면 다음과같이 되어있다.

카드들이 사진 크기에 맞춰 사이즈가 다른채로 엇갈려서 배치되어있고 창 사이즈가 줄어들면 그에따라 구조가 변경된다.

이를 위해 javascript magicgrid를 사용한다.

 

사용할 magicgrid는 아래 url에서 확인할 수 있다. 위의 url는 magicgrid github 아래 url은 readme에 있는 jsfiddle이다.

magicgrid코드는 github/dist/magic-grid.cjs.js파일을 사용한다.

 

GitHub - e-oj/Magic-Grid: A simple, lightweight Javascript library for dynamic grid layouts.

A simple, lightweight Javascript library for dynamic grid layouts. - GitHub - e-oj/Magic-Grid: A simple, lightweight Javascript library for dynamic grid layouts.

github.com

 

Magic Grid - JSFiddle - Code Playground

 

jsfiddle.net

 

 

먼저 시작에 앞서 python manage.py startapp articleapp을 실시한다. 이후 settings, pinterest_clone/settings.py, pinterest_clone/urls.py, articleapp/urls.py, aticleapp/models.py, migration을 세팅한다.

 

<articleapp/models.py>

Article의 경우 작성자, 게시글 제목, 이미지, 내용, 작성날짜를 필드로 갖는다.

작성자는 회원인 사람만 작성할 수 있으므로 User의 Foreignkey가 된다.

from django.contrib.auth.models import User
from django.db import models

# Create your models here.
class Article(models.Model):
    writer = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, related_name='article')
    title = models.CharField(max_length=200, null=True)
    image = models.ImageField(upload_to='article/', null=False)
    content = models.TextField(null=True)
    created_at = models.DateField(auto_now_add=True, null=True)

 

<CreateView>

본격적으로 CRUD를 실시하기 위해 CreateView를 작성한다.

 

<views.py>

from django.shortcuts import render
from django.views.generic import CreateView

from articleapp.forms import ArticleCreationForm
from articleapp.models import Article


# Create your views here.
class ArticleCreateView(CreateView):
    model = Article # 사용할 모델
    form_class = ArticleCreationForm #사용할 폼 --> 새로 생성
    template_name = 'articleapp/create.html' #사용할 html
    # success_url =

    def form_valid(self, form): #폼 에러나지 않게 writer 서버로부터 받기
        temp_article = form.save(commit=False)
        temp_article.writer = self.request.user
        temp_article.save()
        return super().form_valid(form)

 

<forms.py 생성>

from django.forms import ModelForm

from articleapp.models import Article


class ArticleCreationForm(ModelForm):
    class Meta:
        model = Article
        fields = ['image', 'title', 'content']

 

<templates/articleapp/create.html 생성>

{% extends 'base.html' %}
{% load bootstrap4 %}

{% block content %}

<div class="account_create">
  <div>
    <h4>Create Article</h4>
  </div>
  <form action="{% url 'articleapp:create' %}" method="post" enctype="multipart/form-data">
    {% csrf_token %}
    {% bootstrap_form form %}
    <input type="submit" class="btn btn-dark rounded-pill col-6 mt-3">
  </form>
</div>

{% endblock %}

 

<결과화면>

화면이 잘 나온다. 위의  views.py에서 success_url은 주석처리 되어있다. 왜냐하면 이 화면에서 제출을 눌렀을때 어디로 향하게 할지 정하지 않았기 때문이다. 생각해보면 제출을 눌렀을때 article들이 모여있는 페이지로 가면 된다. 이를위해 ArticleListView를 만든다. CreateView의 success_url은 ArticleListView를 만든 후 커스터마이징을 통해 def get_success_url을 추가할 것이다.

 

2. ListView

<urls.py>

from django.urls import path

from articleapp.views import ArticleCreateView, ArticleListView

app_name = "articleapp"

urlpatterns = [
    path('create/', ArticleCreateView.as_view(), name='create'),
    path('list/', ArticleListView.as_view(), name='list'),
]

 

<views.py>

ArticleListview를 작성해 준다.

class ArticleListView(ListView):
    model = Article    
    template_name = 'articleapp/list.html'

 

<list.html>

여기에서 제일 위에 걸어둔 링크가 생긴다. jsfiddle에 보면 html, css, js세 분류로 나뉘어있다. 여기서 css는 style태그로 감싸 안에 넣어준다. html부분은 그대로 복사해서 밑에 넣어준다. 그리고 static/js/magicgrid.js 파일을 경로에 맞게 만들어준다. 그 안에 위의 github링크에서 dist/magic-grid.cjs.js안에 있는 내용을 그대로 붙여넣기 해주고 jsfiddle의 js영역을 복사하여 가장 하단에 붙여넣기 한다. 이후 static폴더 안의 내용을 사용하는 것이므로 {% load static %}을 해주고 하단에 script태그를 사용하여 사용할 static 경로를 작성해 준다.

{% extends 'base.html' %}
{% load static %} # static사용한다고 명시

{% block content %}

<style>
.container div {
  width: 280px;
  height: 500px;
  background-color: antiquewhite;
  display: flex;
  justify-content: center;
  align-items: center;
  border-radius: 8px;
}

.container .item1 { height: 200px; }
.container .item4 { height: 800px; }
.container .item6 { height: 600px; }
.container .item11 { height: 400px; }
</style>

<!DOCTYPE html>
<div class="container">
  <div class="item1">1</div>
  <div class="item2">2</div>
  <div class="item3">3</div>
  <div class="item4">4</div>
  <div class="item5">5</div>
  <div class="item6">6</div>
  <div class="item7">7</div>
  <div class="item8">8</div>
  <div class="item9">9</div>
  <div class="item10">10</div>
  <div class="item11">11</div>
  <div class="item12">12</div>
  <div class="item13">13</div>
  <script src="{% static 'js/magicgrid.js' %}"></script> # static 사용할 경로
</div>

{% endblock %}

 

<static/js/magicgrid.js>

'use strict';

/**
 * @author emmanuelolaojo
 * @since 11/11/18
 */

###### 중략 ######

MagicGrid.prototype.listen = function listen () {
    var this$1 = this;

  if (this.ready()) {
    var timeout;

    window.addEventListener("resize", function () {
      if (!timeout){
        timeout = setTimeout(function () {
          this$1.positionItems();
          timeout = null;
        }, 200);
      }
    });

    this.positionItems();
  }
  else { this.getReady(); }
};

########### jsfiddle 부분

let magicGrid = new MagicGrid({
  container: '.container',
  animate: true,
  gutter: 30,
  static: true,
  useMin: true
});

magicGrid.listen();

###############################

//module.exports = MagicGrid; # 이건 없애준다

 

<화면 결과>

여기까지 진행하고 라우팅 경로로 들어가보면 아래와같이 나타나게 된다. 밑작업이 끝났다.

 

이제 이미지를 넣어본다. 이미지는 아래사이트에서 바로 가져올 수 있다.

 

Lorem Picsum

Lorem Ipsum... but for photos

picsum.photos

 

html상 img태그에서 경로에 아래 주소를 넣어주면 알아서 그림을 가져온다. 이렇게 작성해준다.

<!DOCTYPE html>
<div class="container">
  <div class="item1">
    <img src="https://picsum.photos/200/300" alt=""> # 이렇게 작성하고 화면 본다
  </div>
  <div class="item2">2</div>
  <div class="item3">3</div>
  <div class="item4">4</div>

 

그러면 이렇게 나타나는것을 확인할 수 있다. 이유는 화면이 로드되는 속도와 이미지가 가져와지는 속도가 차이가 있기 때문이다. 이를 방지하기 위해 magicgrid.js파일에 내용을 추가한다.

 

<magicgrid.js 내용 추가>

var masonrys = document.getElementsByTagName("img");

# i가 증가함에 따라
for (let i=0; i < masonrys.length; i++){ 
	# 'load' 라는 event추가
    masonrys[i].addEventListener('load', function() {
        magicGrid.positionItems();
    }, false);
}

이후 list.html도 아래와 같이 수정해준다.

<style>
.container div {
  width: 280px; # height 삭제
  background-color: antiquewhite;
  display: flex;
  justify-content: center;
  align-items: center;
  border-radius: 8px;
}

# 이부분 추가: container클래스 안에 img태그의 스타일
.container img {
  width: 100%;
}

# 하단에 있던 부분들 삭제
</style>

 

<결과화면>

이제 로드된 후 잘 들어가는 것을 확인할 수 있다.

그럼 이제 내가 게시물을 올리면 저자리에 들어가게 하면 된다. 이를위해 ArticleListView를 다음과 같이 수정한다. context_object_name = 'article_list'를 추가해주고 list.html에는 for문을 사용한다. CreateView에도 success_url을 설정한다. 또 로그인 한사람만 게시글을 쓸 수 있으므로 method_decorator를 통해 login_required를 적용한다.

 

<views.py>

@method_decorator(login_required, 'get')
@method_decorator(login_required, 'post')
class ArticleCreateView(CreateView):
    model = Article
    form_class = ArticleCreationForm
    template_name = 'articleapp/create.html'
    success_url = reverse_lazy('articleapp:list') #추가

    def form_valid(self, form):
        temp_article = form.save(commit=False)
        temp_article.writer = self.request.user
        temp_article.save()
        return super().form_valid(form)

class ArticleListView(ListView):
    model = Article
    context_object_name = 'article_list' # 추가
    template_name = 'articleapp/list.html'

 

<list.html 수정>

<!DOCTYPE html>
{% if article_list %} # 게시글이 있으면
<div class="container">
  {% for article in article_list %} # for문으로 하나씩 빼주기
  <div>
    <img src="{{ article.image.url }}" alt=""> #article의 url
  </div>
  {% endfor %}
  <script src="{% static 'js/magicgrid.js' %}"></script>
</div>
{% else %}
<h3>Article Not Yet</h3>
{% endif %}

 

<게시글 등록 전 화면>

<게시글 등록 후 화면>

게시글이 잘 등록되는것을 확인할 수 있다. 이제 게시글을 작성할 수 있는 버튼을 만들어 줘야 한다. 그리고 저 게시글을 클릭하면 저 게시글의 내용을 확인할 수 있게 한다. 게시글 작성 버튼은 로그인 되어 있을때만 활성화되게 할 것이고, 게시글 내용 확인은 ArticleDetailView를 만들어 이동한다. 또한, ArticleDetailView가 만들어지면 ArticleCreatView의 success_url도 변경해 준다. 이를 위해 다음과 같이 작업을 진행한다.

 

<list.html 추가>

{% if user.is_authenticated %}
<div style="text-align:center;">
    <a href="{% url 'articleapp:create' %}" class="btn btn-dark rounded-pill mt-3 mb-3 px-3">
        Create Article
    </a>
</div>
{% endif %}

<로그인 했을때>

 

<head.html변경>

<span> # Articles로 향하도록 변경
  <a href="{% url 'articleapp:list' %}">Articles</a> |
</span>
<span>nav2</span> |
<span>nav3</span> |
<span>nav4</span> |
{% if user.is_authenticated %}
<a href="{% url 'accountapp:detail' pk=user.pk%}">
  <span>MyPage</span> |
</a>
<a href="{% url 'accountapp:logout' %}">
  <span>Logout</span>
</a>

<로그인 안하고 바로 사이트 들어갔을때>

<article 추가>

<ListView에 pagenation추가>

화면에 몇개만 보이게 만들것인지 정한다. 그리고 페이지가 생성되게 만들어 준다. 이를위해 templates/snippets/pagination.html파일을 만들어주고 내용을 입력한 뒤 list.html에는 {% include %}를 활용해 넣어준다.

class ArticleListView(ListView):
    model = Article
    context_object_name = 'article_list'
    template_name = 'articleapp/list.html'
    paginate_by = 6 # 페이지당 6개 보여주길 원함

<list.html에 include추가>

<!--  pagination  -->
{% include 'snippets/pagination.html' %} # include구문 추가


{% if user.is_authenticated %}
<div style="text-align:center;">
    <a href="{% url 'articleapp:create' %}" class="btn btn-dark rounded-pill mt-3 mb-3 px-3">
        Create Article
    </a>
</div>
{% endif %}

<snippets/pagination.html>

# 전페이지가 있을때
<div class="text-center mt-3 mb-3">
  {% if page_obj.has_previous %}
  <a href="{% url 'articleapp:list' %}?page={{ page_obj.previous_page_number }}" class="btn btn-secondary rounded-pill">
    {{ page_obj.previous_page_number }}
  </a>
  {% endif %}
  
  # 현재 페이지
  <a href="{% url 'articleapp:list' %}?page={{ page_obj.number }}" class="btn btn-dark rounded-pill">
    {{ page_obj.number }}
  </a>
  
  # 다음페이지 있을때
  {% if page_obj.has_next %}
  <a href="{% url 'articleapp:list' %}?page={{ page_obj.next_page_number }}" class="btn btn-secondary rounded-pill">
    {{ page_obj.next_page_number }}
  </a>
  {% endif %}
</div>

 

<결과>

<Updateview>

게시글 수정 페이지를 만든다. CreateView와 동일하다.

<views.py>

class ArticleUpdateView(UpdateView):
    model = Article
    context_object_name = 'target_article'
    form_class = ArticleCreationForm
    template_name = 'articleapp/update.html'

    def get_success_url(self):
        return reverse('articleapp:detail', kwargs={'pk':self.object.pk})

<update.html>

{% extends 'base.html' %}
{% load bootstrap4 %}

{% block content %}

<div class="account_create">
  <div>
    <h4>Update Article</h4>
  </div>
  <form action="{% url 'articleapp:update' pk=target_article.pk %}" method="post" enctype="multipart/form-data">
    {% csrf_token %}
    {% bootstrap_form form %}
    <input type="submit" class="btn btn-dark rounded-pill col-6 mt-3">
  </form>
</div>

{% endblock %}

update는 article/detail.html에서 진행하므로 update할 수 있는 버튼을 만들어 준다.

<detail.html 수정>

  <div>
    <p>{{ target_article.title }}</p>
    <p>{{ target_article.content }}</p>
  </div>
  
  # 해당내용 추가(update로 향하는 버튼)
  <div>
    <a href="{% url 'articleapp:update' pk=target_article.pk %}" class="btn btn-dark rounded-pill col-6 mt-3">Update</a>
  </div>
</div>

 

<DeleteView>

게시글삭제 페이지를 만든다.

 

<views.py>

class ArticleDeleteView(DeleteView):
    model = Article
    context_object_name = 'target_article'
    template_name = 'articleapp/delete.html'
    success_url = reverse_lazy('articleapp:list')

<urls.py>

from django.urls import path

from articleapp.views import ArticleCreateView, ArticleListView, ArticleDetailView, ArticleUpdateView, ArticleDeleteView

app_name = "articleapp"

urlpatterns = [
    path('create/', ArticleCreateView.as_view(), name='create'),
    path('list/', ArticleListView.as_view(), name='list'),
    path('detail/<int:pk>', ArticleDetailView.as_view(), name="detail"),
    path('update/<int:pk>', ArticleUpdateView.as_view(), name="update"),
    path('delete/<int:pk>', ArticleDeleteView.as_view(), name='delete'),
]

<delete.html>

{% extends 'base.html' %}

{% block content %}

<div class="account_create">
  <div>
    <h4>Quit: {{ target_article.title }}</h4>
  </div>
  <form action="{% url 'articleapp:delete' pk=target_article.pk %}" method="post">
    {% csrf_token %}
    <input type="submit" class="btn btn-dark rounded-pill col-6 mt-3">
  </form>
</div>

{% endblock %}

articleapp/detail.html에 delete 버튼을 만들어 준다.

  <div>
    <a href="{% url 'articleapp:update' pk=target_article.pk %}" class="btn btn-dark rounded-pill col-3 mt-3">Update</a>
    # delete버튼 만들기
    <a href="{% url 'articleapp:delete' pk=target_article.pk %}" class="btn btn-danger rounded-pill col-3 mt-3">Delete</a>
  </div>

 

이렇게 articleapp은 완료가 되었다. 한가지 더 할것이 있다. pinterest에는 마이페이지에 들어가면 내가 작성한 게시글들이 마이페이지 안에 보이게 만들어져 있다. 이를위해 accountapp의 detail페이지에 article이 보여지도록 한다. 그러면 현재 User모델을 사용하고 있는 accountapp에서 Article모델도 사용해야 한다. 이렇게 한 앱에서 두가지 모델을 사용하기 위해 Mixin이라는 개념을 활용한다.

 

<accountapp/views.AccountDetailView수정>

#MultipleObjectMixin: 한개의 앱에서 여러개 모델을 사용할때 사용
class AccountDetailView(DetailView, MultipleObjectMixin):
    model = User
    context_object_name = 'target_user'
    template_name = 'accountapp/detail.html'
    
    pagenate_by = 5 # MultipleObjectMixin을 사용하게 되면 pagenate_by를 사용할 수 있다.
	
    #Article모델에서writer가 user인 article만 가져온다.
    def get_context_data(self, **kwargs):
        object_list = Article.objects.filter(writer=self.get_object())
        return super().get_context_data(object_list=object_list, **kwargs)

<templates/accountapp/detail.html 수정>

</div>

# 이부분을 가장 하단에 추가해준다.
<div>
  {% include 'snippets/list_fragment.html' with article_list=object_list %}
</div>

{% endblock %}



# snippets/list_fragment.html은 articleapp의 list.html을 그대로 복사해 snippets폴더에 list_fragment라는 이름으로 만든것이다.
# 한번이상 사용되고 사용되는 형식도 동일하기 때문에 따로 파일을 만들어서 불러와 사용하였다.
# list_fragment파일은 만들때, header와 footers는 필요 없기 때문에 지워준다.

<snippets/pagination.html수정>

페이지의url을 그대로 받아올 수 있도록 앞에 href속성의 {% url %} 부분을 삭제한다.

# 그 페이지의 url을 받아올 수 있도록 변경하기
<div class="text-center mt-3 mb-3">
  {% if page_obj.has_previous %}
  <a href="?page={{ page_obj.previous_page_number }}" class="btn btn-secondary rounded-pill">
    {{ page_obj.previous_page_number }}
  </a>
  {% endif %}
  
  <a href="?page={{ page_obj.number }}" class="btn btn-dark rounded-pill">
    {{ page_obj.number }}
  </a>
  
  {% if page_obj.has_next %}
  <a href="?page={{ page_obj.next_page_number }}" class="btn btn-secondary rounded-pill">
    {{ page_obj.next_page_number }}
  </a>
  {% endif %}
</div>

 

<결과화면>

프로필과 내가 올린 게시글이 잘 나타나는것을 볼 수 있다.