Django:제로부터 시작하는 인스타그램 만들기 - clone instagram #5

     

목차 - 2021.09.15 - [Study/python] - Django:제로부터 시작하는 인스타그램 만들기 - clone instagram 목차 

 

github - https://github.com/tkdlek11112/jinstagram/tree/jinstagram-%235

 

 

이미지 업로드 완료 시 화면 변환

 

인스타그램을 보면 이미지를 업로드하는 순간 이미지 옆에 글을 쓸 수 있는 화면으로 변경됩니다. 사용할 때는 몰랐는데 만들려고 하니까 아주 고오오오급 기능이었네요 ㅋㅋ 

 

어떻게 만들어볼지 고민을 해보면.. 모달 창을 두개를 만들어놓고 첫 번째 모달은 이미지를 드래그해서 업로드, 두 번째 모달은 업로드된 이미지가 보이고 글을 쓸 수 있는 input창이 추가된 화면. 사용자가 업로드 버튼을 누르면 첫 번째 모달을 보여주고 이미지를 업로드하면 두 번째 모달을 보여주는 방식으로 하면 되지 않을까요?

 

 

이런 너낌으로 modal 두 개 생성

 

머릿속으로 대충 그려보고, 실제 화면을 그려봅시다. 일단 modal을 만든 html을 복사해서 아래와 같이 만들어줍니다.

 

<div id="modal_add_feed" class="modal modal_overlay">
    <div class="modal_window">
        <div class="modal_title">
            <div class="modal_title_side"></div>
            <div> 새 게시물 </div>
            <div class="modal_title_side">
                <span id="close_modal" class="close_modal material-icons-outlined" style="font-size: 30px">
                    close
                </span>
            </div>
        </div>
        <div class="modal_image_upload">
            <span style="text-align: center"> 사진을 여기에 끌어다 놓으세요. </span>

        </div>
    </div>
</div>

<div id="modal_add_feed_content" class="modal modal_overlay_content">
    <div class="modal_window">
        <div class="modal_title">
            <div class="modal_title_side"></div>
            <div style="margin: 5px"> 새 게시물 </div>
            <div class="modal_title_side">
                <span id="close_modal" class="close_modal material-icons-outlined" style="font-size: 30px">
                    close
                </span>
            </div>
        </div>
        <div class="modal_image_content">
            <div id="input_image" class="modal_image_upload_content">

            </div>
            <div class="modal_content_write">
                <div class="feed_name">
                    <div class="profile_box">
                        <img id="input_profile_image" class="profile_img" src="https://scontent-ssn1-1.xx.fbcdn.net/v/t1.6435-9/s1080x2048/165180104_277246477102900_6106347261862438192_n.jpg?_nc_cat=102&ccb=1-5&_nc_sid=730e14&_nc_ohc=1sN4d8i7rn8AX-7aKYN&_nc_ht=scontent-ssn1-1.xx&oh=5049b7cd176848e330b0f5ea95f28172&oe=615A08D1">
                    </div>
                    <span id="input_user_id" class="feed_name_txt"> jin.99 </span>
                </div>
                <div style="height: 440px">
                    <textarea id="input_content" class="feed_content_textarea form-control col-sm-5" rows="10" placeholder="설명을 입력하세요..."></textarea>
                </div>
                <div style="width: 100%; text-align: center">
                    <button id="button_write_feed" type="button" class="btn btn-primary" style="width: 268px"> 공유하기</button>
                </div>
            </div>
        </div>

    </div>
</div>
.main_body {
        display: flex;
        justify-content: center;
        padding-top: 50px;
        background-color: #FAFAFA;
    }

    .left_body {
    {#background-color: skyblue;#} margin-right: 100px;
        width: 600px;
        height: 2000px;
        display: flex;
        flex-direction: column;
    }

    .right_body {
    {#background-color: yellow;#} padding-top: 20px;
        width: 300px;
        height: 1000px;
        left: 72%;
        position: fixed
    }

    .feed_box {
        background-color: white;
        width: 580px;
        margin: 10px;
        min-height: auto;
        padding-bottom: 10px;
    }

    .feed_img {
        width: 100%;
        object-fit: contain;
    }

    .feed_content {
        padding: 0px 10px;
    }

    .feed_like {
        padding: 0px 10px;
    }

    .feed_reply {
        padding: 0px 10px;
        display: flex;
        flex-direction: column;
    }


    .feed_txt {
        font-size: 14px;
    }


    .feed_icon {
        padding: 5px 5px 0px 5px;
        display: flex;
        justify-content: space-between;
    }

    span {
        padding-right: 5px;
    }

    .feed_name {
        padding: 10px;
        display: flex;
        align-items: center;
    }

    .feed_name_txt {
        font-size: 14px;
        padding: 0px 10px;
        font-weight: bold;
    }

    .profile_box {
        width: 40px;
        height: 40px;
        border-radius: 70%;
        overflow: hidden;
    }

    .profile_img {
        width: 100%;
        height: 100%;
        object-fit: cover;
    }

    .name_content {
        display: flex;
        flex-direction: column;
    }

    .name_content_txt {
        font-size: 12px;
        padding: 0px 10px;
        font-weight: bold;
        color: lightgray;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
        width: 190px;
    }

    .big_profile_box {
        width: 60px;
        height: 60px;
        border-radius: 70%;
        overflow: hidden;
    }

    .link_txt {
        font-size: 14px;
        font-weight: bold;
        cursor: pointer;
        text-decoration: none;
    }

    .recommend_box {
        display: flex;
        justify-content: space-between;
        padding: 5px;
        font-size: 14px;
        font-weight: bold;
        align-items: center;
    }

    .comment_box {
        margin: 40px 0 0 5px;
        font-size: 12px;
        font-weight: bold;
        color: lightgray;
        display: flex;
        flex-direction: column;
    }

    @media screen and (max-width: 1280px) {
        .right_body {
            display: none;
        }
    }


    .modal {
        width: 100%;
        height: 100%;
        position: absolute;
        left: 0;
        top: 0;
        display: none;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        background: rgba(0, 0, 0, 0.8);
        backdrop-filter: blur(1.5px);
        -webkit-backdrop-filter: blur(1.5px);
    }

    .modal_window {
        background: white;
        backdrop-filter: blur(13.5px);
        -webkit-backdrop-filter: blur(13.5px);
        border-radius: 10px;
        border: 1px solid rgba(255, 255, 255, 0.18);
        width: 800px;
        height: 600px;
        position: relative;
    }

    .modal_title{
        display: flex;
        flex-direction: row;
        justify-content: space-between;
        font-weight: bold;
        font-size: 20px;
        border-bottom: 1px solid rgba(0, 0, 0, 0.18);
    }

    .modal_title_side{
        margin: 5px;
        flex: 0 0 40px;
        text-align: center;
    }

    .modal_image_upload{
        outline: 2px dashed black ;
        outline-offset:-10px;
        transition: all .15s ease-in-out;
        width: 798px;
        height: 548px;
        text-align: center;
        line-height: 548px;
    }

    .modal_image_upload_content{
        outline: 2px dashed black ;
        outline-offset:-10px;
        text-align: center;
        transition: all .15s ease-in-out;
        width:500px;
        height: 548px;
    }

    .modal_image_content{
        display: flex;
        flex-direction: row;
    }

    .modal_content_write{
        display: flex;
        flex-direction: column;
        border-left: 1px solid rgba(0, 0, 0, 0.18);;
    }

    .feed_content_textarea{
        resize: none;
        width: 294px;
        border: none;
    }

 

위에 modal_add_feed가 기존에 이미지를 드래그해서 업로드하는 modal이고, 아래 modal_add_feed_content는 글쓰는 곳까지 추가된 modal입니다. 이제 이미지를 올렸을 때 jquery에서 기존에 modal_add_feed에 이미지 미리보기를 지원해줬는데, 이 부분을 없애고 modal_add_feed를 안 보이게 하고 modal_add_feed_content를 보이게 바꿔줍니다. 사용자에게는 첫 번째 modal에서 두 번째 modal로 넘어가는 것처럼 보이지만 실제로는 modal1과 modal2의 display css 태그 값을 이용해 보이고 안 보이고만 정해주게 됩니다. 

 

jquery 쪽에 uploadFiles라는 함수에서 원래는 modal1에 background-image를 설정해주는 부분을 modal1을 안 보이게 하고 modal2를 보이게 하고, 이미지까지 세팅해주는 코드로 변경합니다.

 

function uploadFiles(e){
        e.stopPropagation();
        e.preventDefault();
        console.log(e.dataTransfer)
        console.log(e.originalEvent.dataTransfer)

        e.dataTransfer = e.originalEvent.dataTransfer;

        files = e.dataTransfer.files;
        if (files.length > 1) {
            alert('하나만 올려라.');
            return;
        }

        if (files[0].type.match(/image.*/)) {
            $('#modal_add_feed_content').css({
                display : 'flex'
            });
            $('.modal_image_upload_content').css({
                "background-image": "url(" + window.URL.createObjectURL(files[0]) + ")",
                "outline": "none",
                "background-size": "contain",
                "background-repeat" : "no-repeat",
                "background-position" : "center"
            });
            $('#modal_add_feed').css({
                display: 'none'
            })
        }else{
            alert('이미지가 아닙니다.');
            return;
        }
    };

 

이전 코드와 변경된 부분은 파일이 이미지일경우 modal_add_feed의 display를 none으로 변경(modal1 끄기) modal_image_upload_content에 background-image 추가, modal_add_feed_content의 display를 flex로 변경하는 코드입니다. modal 하나를 끄고 킨다는 게 뭔가 부자연스럽다고 생각될 수 있는데, 요즘 컴퓨터들은 처리속도가 워낙 빨라서 자연스럽게 modal1에서 modal2로 넘어가는 것처럼 보입니다.

 

마지막으로 modal2에서 공유 버튼을 눌렀을때 실행되는 jquery를 만들면 됩니다. 공유 버튼을 눌렀을 때 실행되는 과정은 2단계로 나눌 수 있는데, 먼저 화면에서 데이터를 끌어오는 단계가 있고, 끌어온 데이터를 백엔드 쪽(views.py)으로 넘기는 단계가 있습니다.

 

데이터를 모으는 단계에서는 input값에 적은 feed의 글내용과 이미지, 글쓴이 정보를 jquery를 이용해 가져옵니다. 그리고 ajax통신을 통해 django에 views.py에 만들어놓은 함수로 보내게 됩니다. 

 

    $('#button_write_feed').on('click', ()=>{
        const image = $('#input_image').css("background-image").replace(/^url\(['"](.+)['"]\)/, '$1');
        const content = $('#input_content').val();
        const profile_image = $('#input_profile_image').attr('src');
        const user_id = $('#input_user_id').text();

        const file = files[0];

        let fd = new FormData();

        fd.append('file', file);
        fd.append('image', image);
        fd.append('content', content);
        fd.append('profile_image', profile_image);
        fd.append('user_id', user_id);

        if(image.length <= 0)
        {
            alert("이미지가 비어있습니다.");
        }
        else if(content.length <= 0)
        {
            alert("설명을 입력하세요");
        }
        else if(profile_image.length <= 0)
        {
            alert("프로필 이미지가 비어있습니다.");
        }
        else if(user_id.length <= 0)
        {
            alert("사용자 id가 없습니다.");
        }
        else{
            writeFeed(fd);
            console.log(files[0]);
        }
    });

    function writeFeed(fd) {
        $.ajax({
            url: "/content/upload",
            data: fd,
            method: "POST",
            processData: false,
            contentType: false,
            success: function (data) {
                console.log("성공");
            },
            error: function (request, status, error) {
                console.log("에러");
            },
            complete: function() {
                console.log("무조건실행");
                closeModal();
                location.reload();
            }
        })
    };

 

코드에서 윗부분에 있는 $('#button_write_feed').on('click' 이 부분이 데이터를 긁어오는 부분입니다. image, content, profile_image, user_id, file을 가져와서 데이터가 비어있는지 채워져 있는지 체크를 합니다. 지금은 image와 content만 사용자가 설정한 데이터고 profile_image와 user_id는 고정된 값으로 올라오게 됩니다. (회원가입을 안 만들었기 때문에 회원정보가 없음)

 

이렇게 input데이터를 검증하는 코드는 프론트에 위치할 수도 있고 백엔드에도 위치할 수 있습니다. django에 비유하면 template에 jquery를 사용해서 input을 검증할 수도있고, views.py에서 python코드로 검증할 수도있습니다. 만약 백엔드와 프론트 팀이 나뉘어 있다면 어디서 할지 role을 정하게 되는데, 웬만하면 양쪽에서 다 하는 게 좋습니다.

 

데이터를 서버로 전송하는 역할은 function writeFeed(fd)라는 함수가 하게 됩니다. ajax를 이용해서 api 호출하게 되는데 fd라는 formdata를 넘기게 됩니다. 아까 데이터를 긁어 오는 곳에서 formdata를 만들게 되는데, 파일을 전송하게 위해서는 formdata를 사용해야 합니다. 파일이 아닌 일반적인 데이터 같은 경우는 json형태로 넘기는 게 보통입니다. (rest 표준)

 

ajax에 success와 error, complete function이 있는데, 각각 성공, 실패, 완료 후 실행되는 callback함수입니다. callback함수는 특정 조건에서 호출되는 함수라는 의미입니다. ajax api 요청이 성공하면 자동으로 success 안에 있는 코드가 실행되며 에러일 경우에는 error가 실행되고 complete는 성공이든 실패든 요청이 끝나면 무조건 실행됩니다. 따라서 성공이든 실패든 modal창을 닫기 위해 complete안에 closeModal();을 넣어 modal을 닫아줍니다. 그리고 업로드한 feed를 메인화면에서 확인할 수 있게 화면은 새로고침해(location.reload();)줍니다. 

 

url을 /content/upload로 만들었기 때문에 해당 경로에 views.py에 함수를 만들어 연결시켜 줍니다. jinstagram/views.py에 class Main아래 UploadFeed를 만들어줍니다.

 

from django.shortcuts import render
from rest_framework.views import APIView
from content.models import Feed
from rest_framework.response import Response
import os
from .settings import MEDIA_ROOT
from uuid import uuid4


class Main(APIView):
    def get(self, request):
        feed_list = Feed.objects.all().order_by('-id')
        return render(request, 'jinstagram/main.html', context=dict(feed_list=feed_list))


class UploadFeed(APIView):
    def post(self, request):
        file = request.FILES['file']
        uuid_name = uuid4().hex
        save_path = os.path.join(MEDIA_ROOT, uuid_name)
        with open(save_path, 'wb+') as destination:
            for chunk in file.chunks():
                destination.write(chunk)
        content = request.data.get('content')
        image = uuid_name
        profile_image = request.data.get('profile_image')
        user_id = request.data.get('user_id')

        Feed.objects.create(content=content, image=image, profile_image=profile_image, user_id=user_id, like_count=0)

        return Response(status=200)

file을 처리하기 위해서는 request.FILES를 통해서 파일을 읽어와야 합니다. 서버에서는 보통 올라온 파일 이름 그대로 저장하지 않고 특정 id값을 만들어 저장합니다. 파일 이름이 한글이거나 이상한 특수문자가 있을지도 모르기 때문이죠. 여기서는 uuid라는 값을 랜덤으로 만들어서 해당 파일의 고유 id값으로 사용하고 있습니다. save_pate에서 MEDIA_ROOT가 있는데 이건 jinstagram/settings.py에서 설정해줘야 합니다.

 

# jinstagram/settings.py

MEDIA_URL = '/media/'

MEDIA_ROOT = os.path.join(BASE_DIR, 'media')

 

웹 서비스를 만들다 보면 static파일과 media파일이라는 개념을 접하게 되는데 간단하게 설명하면 static은 서버를 돌릴 때 필요한 파일들이고, media는 사용자가 올리는 파일들을 관리하는 곳입니다. jinstagram에서 사용자들이 올리는 이미지는 media에 저장되게 됩니다.

 

파일을 제외한 데이터는 request.data.get을 통해 가져올 수 있습니다. 모두 다 가져왔으면 Feed.objects.create를 통해 새로운 Feed를 만들 수 있습니다. like_count의 경우 처음 생성하면 무조건 0이니까 0으로 넣어줍니다. 그리고 클라이언트에게 status=200인 응답(Response)을 줍니다.

 

Views.py에 함수를 만들면 urls.py에서 함수에 url을 할당해줘야 합니다. 추가로 사용자들이 업로드한 이미지를 사용할 수 있도록 media에 대한 url도 추가해줘야 합니다.

 

from django.urls import path
from .views import Main, UploadFeed
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    path('', Main.as_view()),
    path('content/upload', UploadFeed.as_view())
]

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

 

'content/upload'로 접속할 경우 UploadFeed를 실행합니다.

아래 urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 코드가 media경로를 url에 포함하는 코드입니다. 요걸 해줘야 우리가 파일을 올리면 ~/medai/{파일 이름}으로 조회할 수 있습니다.

 

이제 실행을 해볼까요?

 

두 번째 모달이 뜬다.
피드에 나타난다!
media에 파일이 생기고 DB에도 파일명이 들어가야한다.

 

이제 피드에 이미지를 넣으면 글 쓰는 곳이 나오고 글까지 입력하고 공유하기를 누르면 메인화면에 보입니다~!! 프로젝트 폴더를 보면 media 경로에 4ab~~~ 파일이 생긴 것을 볼 수 있고 같은 이름으로 content_feed 테이블에 들어간 것을 확인할 수 있습니다. 아 위에는 pycharm pro버전에서 사용할 수 있는 DB 툴이고, 지난 시간에 사용했던 db browser fro sqlite로 보면 아래와 같습니다.

 

짠~

근데 자세히 보면 jin.99 앞뒤에 공백이 있는 것을 확인할 수 있는데요 (저도 나중에 발견했습니다.) 요건 나중에 수정하겠습니다. ㅎㅎ 아마 html에 있는 값을 그대로 읽어오다 보니 공백까지 같이 들어갔나 봅니다. 앞뒤 공백을 잘라주는(trim) 코드를 추가하면 됩니다.

 

파일의 이동을 살펴보면 처음에 드래그해서 페이지에 올리면 그 페이지를 브라우저가 들고 있다가 ajax를 통해 views.py으로 보내줍니다. views.py에서는 받은 파일을 media/경로에 새로운 이름으로 저장하게 되고 같은 이름으로 DB image 필드에 저장합니다. 그럼 main에서 feed를 읽을 때 파일 이름을 읽어서 앞에 media 경로를 합쳐서 ~/media/{파일 이름} 이렇게 호출합니다. 아 요 부분도 수정을 해야 되는군요?

 

            {% for feed in feed_list %}
            <div class="border feed_box">
                    <div class="feed_name">
                        <div class="profile_box">
                            <img class="profile_img" src="{{ feed.profile_image }}">
                        </div>
                        <span class="feed_name_txt"> {{ feed.user_id }}</span>
                    </div>
                    <img class="feed_img" src="{% get_media_prefix %}{{ feed.image }}">

                    <div class="feed_icon">
                        <div>
                            <span class="material-icons-outlined">
                                favorite_border
                            </span>
                            <span class="material-icons-outlined">
                                mode_comment
                            </span>
                            <span class="material-icons-outlined">
                                send
                            </span>
                        </div>
                        <div>
                            <span class="material-icons-outlined">
                                turned_in_not
                            </span>
                        </div>
                    </div>
                    <div class="feed_like">
                        <p class="feed_txt"><b>좋아요 {{ feed.like_count }}개</b></p>
                    </div>
                    <div class="feed_content">
                        <p class="feed_txt"><b> {{ feed.user_id }} </b> {{ feed.content }}</p>
                    </div>
                    <div class="feed_reply">
                        <span class="feed_txt"> <b> mychew </b> 댓글은 아직 모델을 안만들어서 </span>
                        <span class="feed_txt"> <b> cho </b> 항상 이 두개가 나옵니다 ㅋㅋ </span>
                    </div>
                </div>
            {% endfor %}

이 부분은 main.html에서 feed를 for문을 사용해 뿌려주는 곳입니다. 실제로 해당 feed에 이미지를 보여주는 feed_img를 보시면 <img class="feed_img" src="{% get_media_prefix %}{{ feed.image }}"요렇게 바뀐 게 보이실 겁니다. get_media_prefix는 settings.py에서 정의한 media_url을 출력합니다. 따라서 경로는 ~/medai/{파일 이름} 요렇게 되죠. 실제로 브라우저에서 봐볼까요?

 

개발자 모드를 켜고 feed_img 태그를 보면 src="/media/4ab~~"로 되어있습니다. 마우스를 올리면 더 상세한 경로가 나옵니다. 이제 사용자가 업로드한 이미지가 서버에 어떻게 저장되고 어떻게 화면까지 보이는지 알았죠? 

 

실제로 DB에 이미지가 저장되는 것이 아니라 이미지의 이름만 저장되고 이미지는 서버에 저장돼서 브라우저에 이미지의 경로만 알려주면 서버에 저장된 이미지 파일을 읽어가는 경로입니다.

 

요런 너낌

1. 브라우저(클라이언트)에서 메인 조회

2. API에서 class main 실행. 

3. DB에서 Feed에 있는 내용 전체 내려줌 

4. Feed 내용을 브라우저(클라이언트)로 전달. 이때 이미지는 파일명만 있음

5. 파일명 + media root 경로를 더해서 실제 파일 경로를 조회 ~/media/{파일명}

6. 서버에서 저장된 파일을 찾아서 화면에 내려줌 (정적 파일 조회)

 

 

이러한 일련의 과정을 통해 브라우저에 우리가 올린 이미지가 보이게 됩니다. 이미지를 DB에 저장 안 하고 따로 저장한다는 개념이 처음 보면 상당히 헷갈리는 개념입니다. 항상 DB에 데이터를 저장하고 불러오는 방식을 사용했는데 DB에는 경로를 집어넣고 실제 데이터는 다른 곳에서 불러오다니..? 이런 방식을 컴퓨터 구조에서는 in-direct방식이라고 설명하는데 실제로 프로그래밍 내부에서는 상당히 많이 사용되는 개념입니다. C언어에서는 pointer가 바로 이방식이죠. 주소를 저장하고 주소에 있는 값을 가져다주는..? 너무 깊게 들어갔군요 ㅎㅎ

 

 

전체 소스는 아래 브랜치에서 확인 가능합니다.

https://github.com/tkdlek11112/jinstagram/tree/jinstagram-%235

 

 

 

 

반응형

댓글

Designed by JB FACTORY