Django | 인스타그램 클론 코딩(4) - 게시물 등록

Sua·2021년 2월 10일
3

Django

목록 보기
11/23
post-thumbnail

🙆‍♀️ 게시물 등록

이전 시간에는 장고로 인스타그램 회원가입 기능과 로그인을 구현하고, bcrypt와 JWT로 인증 과정을 구현하였습니다. 이번에는 게시물을 등록하는 기능을 구현하도록 하겠습니다.

🔎 게시물 등록 기능 분석

  1. 게시물에는 작성자, 이미지, 내용, 생성 시간이 필요합니다.
  2. 게시물의 내용은 있어도 되고 없어도 됩니다.
  3. 게시물의 사진은 여러 개 등록할 수 있습니다.
  4. 게시물의 작성자는 이미 서비스에 가입된 유저여야 합니다.

🛠 App 생성

게시물 정보를 관리할 수 있는 posting이라는 앱을 만들도록 하겠습니다.

python manage.py startapp posting

Django에서는 주로 다루는 데이터의 종류가 달라지는 시점에서 앱을 분리합니다. 인스타그램의 게시물은 이용자 데이터와는 그 성질이 달라 데이터베이스에서 테이블을 따로 관리합니다. 따라서, 주로 다루는 테이블이 달라지므로 앱을 분리하는 것이 좋습니다.

settings.pyINSTALLED_APPS에 생성한 앱을 등록합니다.

# westagram/settings.py

INSTALLED_APPS = [
    # 'django.contrib.admin',
    # 'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'corsheaders',
    'user',
    'posting',   ---> 입력!
]    

🖌 Model 작성

# posting/models.py

from django.db   import models
from user.models import User

class Posting(models.Model):
    content    = models.CharField(max_length=2000, null=True)
    created_at = models.DateTimeField(auto_now_add=True)
    user       = models.ForeignKey('user.User', on_delete=models.CASCADE)

    class Meta:
        db_table = 'postings'

class Image(models.Model):
    image_url = models.URLField(max_length=2000)
    posting   = models.ForeignKey('Posting', on_delete=models.CASCADE)

    class Meta:
        db_table = 'images'

Q. contentnull=True를 지정한 이유는?
content는 게시물 내용을 의미합니다. 이미지가 아닌 내용은 꼭 작성되지 않아도 되므로 null=True로 지정하였습니다.

Q. Image 클래스를 따로 만든 이유는?
하나의 게시물에 이미지는 여러 개 등록될 수 있습니다. 즉, PostingImage는 One-to-many(1:N)의 관계입니다. 따라서 Posting을 참조하는 Image 클래스를 따로 만들고, ForeignKey 필드로 연결해주었습니다.

Q. User 모델 클래스를 import한 이유는?
게시물과 댓글의 작성자는 이미 서비스에 가입된 유저여야 합니다. 이용자 데이터를 관리하고 있는 user 앱의 model에 있는 User 클래스를 사용하기 위해서 import했습니다.

Q. 어떻게 다른 앱에 있는 클래스를 참조할 수 있는지?
여기서 어떻게 posting 앱의 Posting 클래스가 user 앱의 User 클래스를 참조할 수 있을까요?
(우선 user 앱의 User 클래스를 import한 상태입니다.)

일반적으로 하듯이 ForeignKey 필드에User 클래스를 인자로 받았습니다.

user = models.ForeignKey('User', on_delete=models.CASCADE)

그리고 makemigrations를 시도해봅시다.

🚨 에러가 발생합니다! User 클래스가 user 앱 출신이라는 것을 알려주지 않아서 발생하는 에러입니다.
Useruser.User로 변경합니다.

user = models.ForeignKey('user.User', on_delete=models.CASCADE)

이제 user앱의 User 클래스와 posting앱의 Posting 클래스의 One-to-many 관계가 결성되었습니다.

makemigrations & migrate

Model 작성이 완료되었으면 makemigrations로 migration 파일을 만들어줍니다.

python manage.py makemigrations user

에러가 나지 않고 정상적으로 migarion 파일이 만들어졌으면 migrate로 실제 DB에 변경 사항을 적용해봅시다.

python manage.py migrate user

💣 View 작성

게시물 등록 기능

Part1. 필요한 정보 받아오기

먼저 게시물 등록에 필요한 user(작성자), content(내용), image_url(이미지) 정보를 받아옵니다.

  • user 변수에는 User 클래스에서 가져온 객체를 담습니다.
  • image_url_list라는 변수에는 리스트 형태로 받아온 image_url을 담습니다. 이후에 반복문으로 리스트 안의 값들을 하나씩 빼올 것입니다.
data = json.loads(request.body)

user           = User.objects.get(username=data.get('user', None))
content        = data.get('content', None)
image_url_list = data.get('image_url', None)

Part2. KeyError 처리하기

content는 없어도 되는 정보이므로 userimage_url_list에만 값이 잘 담겨져있는지 확인합니다. 값이 없을 경우에는 KeyError가 발생하지 않도록 처리합니다.

if not (user and image_url_list):
    return JsonResponse({'message':'KEY_ERROR'}, status=400)

Part3. 게시물 생성하기

usercontentcreate()를 사용하여 바로 데이터를 생성할 수 있습니다.

posting = Posting.objects.create(
    user    = user,
    content = content
)

하지만 image_url_list는 이미지 정보가 담긴 리스트입니다. for문을 사용하여 리스트의 각 값들을 하나씩 가져오면서, for문 안에서 create()로 데이터를 생성합니다. 특히, image_urlforeign key인 posting 도 함께 생성해야 합니다.

for image_url in:
    Image.objects.create(
        image_url = image_url,
        posting   = posting
    )

모든 게시물 표출 기능

게시물을 두 가지 방법으로 가져올 수 있게 구분하였습니다. 바로 모든 게시물을 가져오는 방식과 특정 유저의 게시물만 가져오는 방식입니다.

먼저 모든 게시물을 표출하는 기능부터 구현하겠습니다.

    def get(self, request):
        posting_list = [{
            "username"  : User.objects.get(id=posting.user.id).username,
            "content"   : posting.content,
            "image_url" : [i.image_url for i in Image.objects.filter(posting_id=posting.id)],
            "create_at" : posting.created_at
            } for posting in Posting.objects.all()
        ]

        return JsonResponse({'data':posting_list}, status=200)

Part1. 리스트 컴프리헨션

리스트 컴프리헨션을 사용해서 모든 게시물의 정보가 이쁘게 리스트에 담기도록 했습니다.

우선 Posting.objects.all()은 모든 게시물을 정보를 QuerySet으로 가져옵니다. 그리고 for문으로 QuerySet안에 있는 객체를 하나씩 posting에 넣습니다. 이제 posting는 객체라고 할 수 있겠지요.

Part2. 객체인 posting의 속성값 가져오기

객체가 들어있는 posting을 활용해서 게시물의 속성을 가져올 수 있습니다. posting.user.id, posting.content 이런 식으로 말입니다. 이렇게 필요한 정보들을 잘 뽑아내서 key, value 형식의 딕셔너리에 담아냅니다.

이제 posting_list는 모든 게시물의 각 정보들이 딕셔너리 형태로 담긴 리스트가 되었습니다. 이것을 프론트에게 잘 전달하기만 하면 됩니다.

특정 유저의 게시물 표출 기능(PostingSearchView)

user_id으로 조회해서 특정 유저의 게시물만 표출하는 로직을 구현하기 위해서 PostingSearchView 클래스를 새로 만들었습니다. 세부 정보를 검색하는 기능이라 따로 구분하였습니다.

class PostingSearchView(View):
    def get(self, request, user_id):
        if not User.objects.filter(id=user_id).exists():
            return JsonResponse({'message':'USER_DOES_NOT_EXIST'}, status=404)

        posting_list = [{
            "username"  : User.objects.get(id=user_id).username,
            "content"   : posting.content,
            "image_url" : [i.image_url for i in Image.objects.filter(posting_id=posting.id)],
            "create_at" : posting.created_at
            } for posting in Posting.objects.filter(user_id=user_id)
            ]

        return JsonResponse({'data':posting_list}, status=200)

Part1. user_id 받기

url을 통해 user_id를 받습니다.
만약 user_id에 해당하는 유저가 없을 경우에는 'USER_DOES_NOT_EXIST'라는 메시지와 404 에러코드를 응답합니다.

Part2. filter()로 해당 유저의 정보만 가져오기

user_idPosting.objects.filter(user_id=user_id)를 하여 해당 유저의 정보만 가져옵니다.
그 외 리스트 컴프리헨션을 활용하는 방식은 모든 게시물을 가져오는 방식과 동일하니 생략하겠습니다.

👀 View 전체 코드(PostingView & PostingSearchView)

# posting/views.py

import json
from json.decoder import JSONDecodeError

from django.http import JsonResponse
from django.views import View

from posting.models import Posting, Image
from user.models import User

class PostingView(View):
    def post(self, request):
        try:
            data = json.loads(request.body)

            user           = User.objects.get(username=data.get('user', None))
            content        = data.get('content', None)
            image_url_list = data.get('image_url', None)

            if not (user and image_url_list):
                return JsonResponse({'message':'KEY_ERROR'}, status=400)

            posting = Posting.objects.create(
                user    = user,
                content = content
            )

            for image_url in image_url_list:
                Image.objects.create(
                    image_url = image_url,
                    posting   = posting
                )
                
            return JsonResponse({'message':'SUCCESS'}, status=200)
        
        except JSONDecodeError:
            return JsonResponse({'message': 'JSON_DECODE_ERROR'}, status=400)

    def get(self, request):
        posting_list = [{
            "username"  : User.objects.get(id=posting.user.id).username,
            "content"   : posting.content,
            "image_url" : [i.image_url for i in Image.objects.filter(posting_id=posting.id)],
            "create_at" : posting.created_at
            } for posting in Posting.objects.all()
        ]

        return JsonResponse({'data':posting_list}, status=200)
        
        
class PostingSearchView(View):
    def get(self, request, user_id):
        if not User.objects.filter(id=user_id).exists():
            return JsonResponse({'message':'USER_DOES_NOT_EXIST'}, status=404)

        posting_list = [{
            "username"  : User.objects.get(id=user_id).username,
            "content"   : posting.content,
            "image_url" : [i.image_url for i in Image.objects.filter(posting_id=posting.id)],
            "create_at" : posting.created_at
            } for posting in Posting.objects.filter(user_id=user_id)
            ]

        return JsonResponse({'data':posting_list}, status=200)

📬 url 경로 지정하기

# westagram/urls.py

from django.urls import path, include

urlpatterns = [
    path('user', include('user.urls')),
    path('posting', include('posting.urls'))  --> 추가
]
# posting/urls.py

from django.urls import path
from .views import (
    PostingView, 
    PostingSearchView,
)

urlpatterns = [
    path('', PostingView.as_view()),
    path('/search/<int:user_id>', PostingSearchView.as_view()),
    ]

다음 시간에는 인스타그램에 권한이 있는 유저만이 게시물 등록할 수 있도록 인가 과정을 구현하도록 하겠습니다.

profile
Leave your comfort zone

0개의 댓글