티스토리 뷰

 

게시글에 대한 앱 'contents' 을 만든다. settings.py INSTALLED_APPS 에 추가하는 것도 잊지 않기

python manage.py startapp contents

 

 

 

contents / models.py

인스타그램에 게시하는 게시물들 즉 컨텐츠에 대한 모델을 생성한다.

생성시간, 수정시간에 대한 추상화모델을 BaseModel로 만든다.

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


class BaseModel(models.Model):
    created_at = models.DateTimeField(auto_now_add = True)
    modified_at = models.DateTimeField(auto_now = True)

    class Meta:
        abstract = True

 

 

 

컨텐츠에 대한 모델 Content 을 생성한다.

BaseModel을 상속받기 때문에, 생성시간, 수정시간에 대한 값을 자동으로 가져올 수 있다.

user : 작성자는 User에서 ForeignKey 외래키로 가져온다.

text : 작성한 글은 TextField로 가져온다. 이미지에 대한 값은 따로 모델을 생성해야한다.

class Content(BaseModel):
    user = models.ForeignKey(User, on_delete = models.CASCADE)
    text = models.TextField(default = '')

    class Meta:
        ordering = ['-created_at']
        verbose_name_plural = "컨텐츠"

 

 

 

컨텐츠에서 이미지에 대한 모델 Image 을 생성한다.

UPLOAD_PATH : 게시물에 올린 이미지가 저장되는 경로의 이름 설정

content : 작성자와 게시글에 대한 정보가 저장된 Content 모델을 가져와서 Image 모델에서 하나의 필드값 content 로 저장될 수 있도록 한다.

image : 게시물에 올린 이미지에 대해서 저장되는 경로를 uploade_to 값으로 설정한다. image_upload_to 라는 함수에서 저장 경로를 만들도록 설정했다.

order : 저장되는 이미지의 넘버링 1,2,3,4.....

 

image_upload_to고유 파일 이름을 생성하여 정해진 경로에 저장되도록 하는 함수

ext : 확장자가 들어가게 된다. 맨뒤에서부터 점이 나올때까지므로 jpg, png 와 같은 확장자이름이 저장된다.

"%s.%s" %(uuid.uuid4(), ext) : 16자리의 고유한 아이디가 생성된다.

Image 클래스에서 지정한 저장 경로 UPLOAD_PATH에 파일이 저장된다.

def image_upload_to(instance, filename):
    ext = filename.split('.')[-1]
    return os.path.join(instance.UPLOAD_PATH, "%s.%s" % (uuid.uuid4(), ext))


class Image(BaseModel):
    UPLOAD_PATH = 'user-upload'
    content = models.ForeignKey(Content, on_delete = models.CASCADE)
    image = models.ImageField(upload_to = image_upload_to)
    order = models.SmallIntegerField()

    class Meta:
        unique_together = ['content', 'order']
        ordering = ['order']

 

 

아래와 같이 파일이 저장된 곳에서 가져오는 경로를 설정해야 이미지가 제대로 업로드된다.

urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

 

 

 

 

 

 

templates / home.html

 

메인 화면을 구성하는 템플릿.

 

헤더 부분으로 상단 메뉴바를 구성한다.

html 코드가 끝나고 가장 아래에 script 태그에 logoutButton 을 클릭했을 때에 수행해야하는 액션을 javascript 코드로 구성한다. 

상단바에는 홈화면, 친구목록, 로그아웃 각각의 페이지로 이동할 수 있도록 메뉴바를 만든다.

그 아래에는 현재 로그인한 사용자의 이름을 출력한다.

여기까지하면 아래와 같이 메뉴 상단바가 완성된다.

{% extends 'base.html' %}

{% block head %}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/dropzone@5.5.1/dist/dropzone.min.css">
{% endblock %}

{% block body %}
<!--Header-->
<nav class = "fast-nav navbar navbar-expand-lg navbar-light bg-white" style = "width : 1000px; margin : 0 auto;">
    <a class = "navbar-brand text-success col-sm-8" href = "{% url 'home' %}"><b>jiheestagram</b></a>
    <div class = "collapse navbar-collapse col-sm-4" id = "navbarText" style = "display:inline-block !important; text-align:right;">
        <span class = "navbar-text col-sm-3">
            <a href = "{%url 'relation' %}">친구 목록</a>
        </span>
        <span class = "navbar-text col-sm-3">
            <a href = "javascript:void(0);" id = "logoutButton">로그아웃</a>
        </span>
    </div>
</nav>

<!--username-->
<div style = "width:600px; margin:0 auto;">
    <br><br>
    <h2 class = "text-success">{{request.user}}</h2><br>
</div>

<script 
  src="https://code.jquery.com/jquery-3.4.1.min.js"
  integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo="
  crossorigin="anonymous"> </script>
<script src="https://cdn.jsdelivr.net/npm/dropzone@5.5.1/dist/dropzone.min.js"></script>

 

 

 

그 다음에는, 게시글 작성 폼에 대한 코드를 작성한다.

dropzone 이라는 기능을 사용하여 이미지를 업로드한다.

작성하기 버튼을 눌렀을 때에 수행해야하는 동작은 script 태그에서 설정한다.

게시글 작성 폼은 'apis_content_create' 의 경로로 전달된다. 

<br>
<!--Create Content-->
<div id = "formContainer" style = "width:600px; margin:0 auto;">
    <form action = "{% url 'apis_content_create' %}" class = "dropzone" id = "uploader" style = "background-color:whitesmoke;">
        <div class = "dz-message" data-dz-message><span>드래그 드랍하거나 클릭하여 이미지 선택</span></div>
        <div class = "fallback"><input name = "file" type = "file"></div>
    </form>
    <div class = "form-group">
        <label>아래에 글을 입력해주세요.</label>
        <textarea class = "form-control rounded-0" id = "text" rows = "3"></textarea>
    </div>
    <div style = "text-align: right;">
        <button type = "button" class = "btn btn-success" id = "upload">작성하기</button>
    </div>
</div>

 

 

dropzone 에 대한 javascript 코드

작성하기 버튼의 id 인 upload 가 클릭되었을 때에 dropZone 기능이 실행된다.

<script>
    Dropzone.autoDiscover = false;
    var dropZoneUploader = new Dropzone('form#uploader', {
        init: function(){
            var dropzone = this;
            $('#upload').click(function() {
                dropZoneUploader.processQueue();
            });

            dropzone.on("sending", function(file, xhr, formData){
                formData.append("text", $('#text').val());
            });
        },
        parallelUploads : 10,
        autoProcessQueue : false,
        type : 'POST',
        success : function(){
            location.reload();
            toastr.success("<h3>success</h3>");
        },
        error : function(e) {
            console.log(e)
            alert('오류가 발생했습니다. 다시 시도해주세요.');
        },
        acceptedFiles : ".jpeg, .jpg, .png, .gif",
        uploadMultiple : true,
    });
</script>

 

 

메뉴 상단바에 있는 로그아웃 버튼을 클릭했을 때의 액션으로, logout 경로로 이동하여 로그아웃을 수행하고 다시 로그인 페이지로 이동하게끔 한다.

이 때, 로그아웃 경로에서 앞에도 '/' 를 붙여야한다.

그냥 'apis/user/logout/' 으로 해버리면 현재 경로에 'apis/user/logout/' 가 더해진 경로로 이동하게 된다. 생각치도 못하게 로그아웃 기능이 실행되지 않아, 한참을 헤맸다. 

<script>
    $(document).ready(function(){
        $('#logoutButton').click(function(){
            $.get('/apis/user/logout/', {}, function(){
                window.location = "{% url 'login' %}";
            });
        });
    });
</script>

 

 

 

실행 결과

 

 

 

 

 

 

 

apis / views.py

왜 컨텐츠 생성 로직을 contents / views.py 경로에 만들지 않는 것일까?

게시글 생성 API 에 대한 로직이기 때문에 JSON 형태의 데이터로 주고받으려면 BaseView를 상속받아야하기 때문이다.

그래서 그냥 from contents.models import Content 로 필요한 모델만 가져오면 된다.

 

 

home.html 에서 게시글 작성폼을 ContentCreateView에서 넘겨받았다.

 

text : POST 요청된 폼에서 'text' 값을 가져와서 strip으로 양끝 공백을 없앤다.

content : 사용자와 텍스트 필드에 대한 Content 객체를 생성하여 content에 저장한다.

업로드한 이미지를 request.FILES.values으로 불러와 enumerate 함수를 사용하여 인덱스와 파일명을 나눈다.

위에서 저장한 content, 이미지, 인덱스 넘버에 대한 Image 객체를 생성한다.

@method_decorator(login_required, name = "dispatch")
class ContentCreateView(BaseView):
    def post(self, request):
        text = request.POST.get('text', '').strip()
        content = Content.objects.create(user = request.user, text = text)
        for idx, file in enumerate(request.FILES.values()):
            Image.objects.create(content = content, image = file, order = idx)
        return self.response({})

 

 

 

 

 

실행 결과

게시글을 작성한다. 

작성하기 버튼 누르기 전

dropzone 기능이 제대로 실행된다.

 

 

작성하기 버튼 누른 후

ContentCreateView 진행 중

아래와 같이 idx 와 file 값이 저장된다.

 

 

게시글 생성 완료

이미지 업로드도 잘 되고 텍스트 작성자 모두 제대로 저장된다.

 

댓글