본문 바로가기
FrameWork/pinterest clone

9. articleapp만들기

by mansoorrr 2023. 7. 22.

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

먼저 만들 앱은 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>

 

<결과화면>

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

'FrameWork > pinterest clone' 카테고리의 다른 글

11. Subscriptionapp 만들기  (0) 2023.07.25
10. Projectapp 만들기  (0) 2023.07.24
8. profileapp 만들기  (0) 2023.07.22
7. accountapp 만들기  (0) 2023.07.20
6. 기초 스타일링(2)  (0) 2023.07.19