본문 바로가기
관리자

Programming-[Backend]/Django

Django로 Pinterest 따라 만들기-11. MagicGrid 활용, ArticleApp CRUD 완성하기

728x90
반응형

 

1. Articleapp.list 생성 / MagicGrid 적용

 

Articleapp을 만들고 나서 Article list가 반응형 사진 앨범처럼 나오게 해주는 MagicGrid를 적용해볼 것이다.

 

Articleapp 기본 틀 생성

 

-articleapp을 시작한다.

'python manage.py startapp articleapp'

 

-pragmatic/settings.py에 추가

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'accountapp',
    'bootstrap4',
    'profileapp',
    'articleapp',
]

 

-pragmatic/urls.py에 추가

urlpatterns = [
    path('admin/', admin.site.urls),
    path('accounts/', include('accountapp.urls')),
    path('profiles/', include('profileapp.urls')),
    path('articles/', include('articleapp.urls')),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

 

-urls.py : TemplateView 추가

 

articleapp/urls.py를 추가 후, urlpatterns에 TemplateView로 list 페이지를 추가한다. TemplateView는 따로 View를 작성하지 않아도 django에서 제공해주는 기본 View라고 보면 된다.

app_name = 'articleapp'

urlpatterns = [
    path('list/', TemplateView.as_view(template_name='articleapp/list.html'), name='list')
]

 

MagicGrid 추가

이제 magicGrid를 추가할 것이다. MagicGrid를 사용하는 아래 jsFiddle 예시를 보면, HTML, CSS, JS 파일로 구분된다. 여기서 HTML, CSS만 복사하고 JS 로직은 다른 곳에서 받아올 것이다.

https://jsfiddle.net/eolaojo/4pov0rdf/

 

 

 

articleapp/templates/articleapp/list.html 추가

list.html을 추가한다. 여기에 MagicGrid를 적용할 내용을 작성한다. style 태그 아래에 css 파일을 붙여넣기 하였다. 그리고 강의에서 약간의 style 수정을 했다.  <script> 태그로 js 파일을 연결하되, js 내용은 static 디렉토리에 따로 추가해준다.

 

Lorem Picsum에서 이미지 불러오기

Magic grid가 적용된 item들에 image를 넣어본다. Lorem Picsum 이라는 site는 간단한 url만 넣어주면 자동으로 랜덤한 이미지들을 불러와준다.

 

{% extends 'base.html' %}

{% load static %}

{% block content %}

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

        .container img {
            width: 100%;
        }

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

    <div class="container">
        <div>
            <img src="https://picsum.photos/200/290" alt="">
        </div>
        <div>
            <img src="https://picsum.photos/200/420" alt="">
        </div>
        <div>
            <img src="https://picsum.photos/200/370" alt="">
        </div>
        <div>
            <img src="https://picsum.photos/200/320" alt="">
        </div>
        <div>
            <img src="https://picsum.photos/200/240" alt="">
        </div>
        <div>
            <img src="https://picsum.photos/200/400" alt="">
        </div>
        <div>
            <img src="https://picsum.photos/200/380" alt="">
        </div>
        <div>
            <img src="https://picsum.photos/200/290" alt="">
        </div>
    </div>

    <script src="{% static 'js/magicgrid.js' %}"></script>

{% endblock %}

 

pragmatic/static/js/magicgrid.js 추가

 

아래 MagicGrid의 공식 Github에 들어가서 'Magic-Grid/dist/magic-grid.cjs.js'의 코드를 복사해오고, magicgrid.js에 붙여넣는다. 맨 아래 'module.exports = MagicGrid' 부분은 js용 문법이므로 삭제한다. 그리고 MagicGrid의 설정을 하는 코드를 jsFiddle 쪽에서 복사해와서 추가해준다.

 

masonrys(벽돌 쌓기; 핀터레스트의 디자인을 호칭하는 단어)

let magicGrid ... 부분과 magicGrid.listen() 사이에 강의에서 나온 코드를 추가한다. 이 코드는 Lorem Picsum과 통신이 되기 전에 js 코드가 먼저 로딩이 완료되어 이미지들이 magicGrid에 제대로 적용되지 않는 문제를 해결한다. 'img' 태그가 있는 각 요소에 EventListener를 적용하여 .positionItems가 되어야만 js 코드를 완료하도록 설정한다.

 

//...magic-grid.cjs.js 파일 생략

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

---
let masonrys = document.getElementsByTagName("img");
for (let i = 0; i < masonrys.length; i++) {
  masonrys[i].addEventListener('load', function () {
    magicGrid.positionItems();
  }, false);
}
---
magicGrid.listen();

 

 

 

 


 

 

2. ArticleApp 구현

 

 

이제 ArticleApp을 구현한다. migration이나 Modeling 과정이 기존에 학습했던 accountapp이나 profileapp과 매우 유사하다. 한번에 모두 처리하고 기존과 다른 부분만 살펴보자.

 

 

Model Migration

 

articleapp/models.py

 

class Article(models.Model):
    writer = models.ForeignKey(User, on_delete=models.SET_NULL, related_name='article', null=True)

    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)

models.ForeignKey user와 관계를 맺어준다. 이것은 JPA에서의 ManyToOne을 설정하는 것과 같다. 즉, 1:N = user : article 관계의 연관관계의 주인으로서 FK를 갖게 만드는 것이다. 다시 말해 유저 1명이 여러 개의 Article을 작성할 수 있도록 설정해준다.

 

on_delete=models.SET_NULL 옵션은 CASCADE와 달리 user가 삭제되면 해당 Article의 writer 정보를 null로 만들어준다는 것이다.

 

related_name은 앞서 공부한 바와 같이, user.article로 엔티티 탐색을 할 때 쓰일 명칭을 의미한다. 

 

models.DateField로 created_at을 설정해주었다. 그리고 auto_now_add=True 옵션을 적용하여 해당 튜플이 생성된 시각을 표시할 수 있도록 해주었다.

 

 

articleapp/forms.py

forms.py도 생성 후 ModelForm을 활용하여 ArticleCreationForm을 생성해준다.

 

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

 

migration을 진행한다.

'python manage.py makemigrations'

'python manage.py migrate'

 

 

ArticleApp 완성하기

 

articleapp/views.py : creation, detail, update, delete View

이전과 거의 유사하다.  본인의 article만 수정할 수 있도록 article_ownership_required 라는 decorator를 추가한다.

 

@method_decorator(login_required, 'get')
@method_decorator(login_required, 'post')
class ArticleCreationView(CreateView):
    model = Article
    form_class = ArticleCreationForm
    template_name = 'articleapp/create.html'

    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)

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

class ArticleDetailView(DetailView):
    model = Article
    context_object_name = 'target_article'
    template_name = 'articleapp/detail.html'

@method_decorator(article_ownership_required, 'get')
@method_decorator(article_ownership_required, 'post')
class ArticleUpdateView(UpdateView):
    model = Article
    context_object_name = 'target_article'
    form_class = ArticleCreationForm
    template_name = 'articleapp/update.html'

    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)

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

@method_decorator(article_ownership_required, 'get')
@method_decorator(article_ownership_required, 'post')
class ArticleDeleteView(DeleteView):
    model = Article
    context_object_name = 'target_article'
    success_url = reverse_lazy('articleapp:list')
    template_name = 'articleapp/delete.html'

 

articleapp/decorators.py

def article_ownership_required(func):
    def decorated(request, *args, **kwargs):
        article = Article.objects.get(pk=kwargs['pk'])
        if not article.writer == request.user:
            return HttpResponseForbidden()
        return func(request, *args, **kwargs)
    return decorated

 

articleapp/urls.py

app_name = 'articleapp'

urlpatterns = [
    path('list/', TemplateView.as_view(template_name='articleapp/list.html'), name='list'),
    path('create/', ArticleCreationView.as_view(), name='create'),
    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')
]

 

html 파일 수정 및 추가

 

list.html 일부

list 페이지에서 create을 할 수 있도록 아래쪽에 버튼을 추가한다.

<div style = "text-align: center">
    <a href="{% url 'articleapp:create' %}" class="btn btn-dark rounded_pill col-3 mt-3 mb-3" >
        Create Article
    </a>
</div>

 

 

header.html 일부

heaeder 부분에서 Article list에 접근가능하도록 navigation 링크를 추가한다.

<div>
    <a href="{% url 'articleapp:list' %}">
        <span>Articles</span>
    </a>

 

 

 

나머지 create, detail, update, delete.html 파일을 완성한다.

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

{% block content %}
    <div style="text-align: center; max-width: 500px; margin: 4rem auto">
    <div class="mb-4">
        <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 %}
{% extends 'base.html' %}
{% load bootstrap4 %}

{% block content %}
    <div style="text-align: center; max-width: 500px; margin: 4rem auto">

        <h1>
            {{ target_article.title }}
        </h1>
        <img src="{{ target_article.image.url }}" alt="">

        <p>
            {{ target_article.content }}
        </p>
        <a href="{% url 'articleapp:update' pk=target_article.pk %}">
            <p>Update Article</p>
        </a>
        <a href="{% url 'articleapp:delete' pk=target_article.pk %}">
            <p>Delete Article</p>
        </a>
    </div>
{% endblock %}
{%  extends 'base.html' %}
{% load bootstrap4 %}

{% block content %}
    <div style="text-align: center; max-width: 500px; margin: 4rem auto">
    <div class="mb-4">
        <h4>Update Article</h4>
    </div>
        <form action="{% url 'articleapp:update' pk=target_article.pk %}" method="post">
            {%  csrf_token %}
            {% bootstrap_form form %}
            <input type="submit" class="btn btn-dark rounded-pill col-6 mt-3">
        </form>
    </div>
{% endblock %}
{%  extends 'base.html' %}
{% load bootstrap4 %}

{% block content %}
    <div style="text-align: center; max-width: 500px; margin: 4rem auto">
    <div class="mb-4">
        <h4>Delete Article</h4>
    </div>
        <form action="{% url 'articleapp:delete' pk=target_article.pk %}" method="post">
            {%  csrf_token %}
            {% bootstrap_form form %}
            <input type="submit" class="btn btn-danger rounded-pill col-6 mt-3">
        </form>
    </div>
{% endblock %}

 

 

 

 

 


 

참조

1. 작정하고 장고! Django로 Pinterest 따라만들기 : 바닥부터 배포까지-박형석님 인프런 강의

https://www.inflearn.com/course/%EC%9E%A5%EA%B3%A0-%ED%95%80%ED%84%B0%EB%A0%88%EC%8A%A4%ED%8A%B8/dashboard

 

 

728x90
반응형