저번 시간에는 회원가입과 로그인 기능들을 다듬어주고, 게시글과 해당 게시글의 댓글 기능을 추가해주었다.
오늘은 해당 프로젝트에서 데이터베이스 관계에 대해 공부를 해보고, 팔로우 기능과 회원가입 및 로그인 시 미입력, 중복 에러를 설정해보고자 한다.
우선 데이터베이스 관계에 대해 알아보도록 하자.
오늘 알아볼 데이터베이스 관계에는 One-to-One, One-to-Many, Many-to-Many 관계가 있다.
데이터베이스 관계
: One-to-One : 1:1 :
A 테이블의 한 요소와 B 테이블의 한 요소에만 연결되어 있는 관계
: One-to-Many : 1:N :
A 테이블의 한 요소에 B 테이블의 다수의 요소가 연결되어 있는 관계
: Many-to-Many : N:N :
두 테이블의 여러 요소가 서로 관계되어 있는 것, 다대다(N:N) 관계 지정을 위해 중간 테이블(Pivot Table)이 필요하다.
이 중에서 Many-to-Many 관계를 연습해보고자 했기 때문에, 연습을 위한 shop이라는 앱을 추가해주었다.

이후 shop 앱의 models.py에 해당 내용을 추가해준 뒤 수정된 내용을 알려주고 반영해주도록 하였다.
from django.db import models
# Create your models here.
class Menu(models.Model):
class Meta:
db_table = "menu"
def __str__(self):
return self.menu_name
menu_name = models.CharField(max_length=100)
class ShopName(models.Model):
class Meta:
db_table = "shop"
def __str__(self):
return self.shop_name
shop_name = models.CharField(max_length=100)
shop_menu = models.ManyToManyField(Menu)

이렇게 해주면 다음과 같이 데이터베이스가 생성되게 된다.

그리고 admin 페이지에 들어가 해당 테이블에 데이터를 추가해주기 위해 우선적으로 admin 계정을 하나 생성해주었다.

하지만 shop_admin 계정을 생성하고 admin 페이지로 접속하여 데이터를 생성해주려고 했지만 다음과 같은 화면처럼 에러가 발생해버렸다.

해당 오류는 유저 권한이 이상하게 꼬여버려서 나타나는 것이라는데, 아마 이전 데이터베이스를 생성할 때 만들어 두었던 jinu 계정과 방금 만든 shop_admin 계정이 서로 문제를 일으킨 것 같다.
그래서 해당 admin 정보들이 담긴 모든 데이터베이스를 삭제하고 다시 설정해주기로 하였다.




해당 데이터베이스를 삭제해주었기 때문에 지워진 모델들을 makemigrations와 migrate를 통해 수정 및 반영을 해주었고, createsuperuser를 통해 하나의 admin 계정을 생성해주었다.
설정이 끝난 뒤 admin 페이지로 들어가 추가하고자 했던 데이터들을 입력해주면 정상적으로 데이터가 들어가는 것을 확인할 수 있었다.

이제 해당 프로젝트에 팔로우 기능을 추가해보고자 한다.
우선 user의 models.py에 follow를 추가해주기로 하였다.
class UserModel(AbstractUser): # 장고에서 제공하는 auth_user 테이블과 연동
class Meta:
db_table = "my_user" # 테이블 이름
bio = models.CharField(max_length=256, default='')
follow = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='followee')
해당 models.py에 대한 정보가 바뀌었으니 makemigrations와 migrate를 통해 해당 모델의 정보가 수정되었고 반영을 해주겠다는 것을 알려주었다.

그리고 user 앱의 views.py에 사용자들을 불러오는 함수와 팔로우 관련 함수를 추가해주었다.
# 사용자 리스트 불러오기, 로그인한 사용자 제외
@login_required()
def user_view(request):
if request.method == 'GET':
# 사용자 불러오기
# exclude : 해당 데이터에서 제외하겠다.
# request.user.username을 사용하여 로그인한 사용자(나) 제외
user_list = UserModel.objects.all().exclude(username=request.user.username)
return render(request, 'user/user_list.html', {'user_list': user_list})
# 팔로우를 위한 유저를 나누기 (로그인한 유저, 팔로우 관련 유저)
@login_required()
def user_follow(request, id):
me = request.user # 로그인한 사용자
click_user = UserModel.objects.get(id=id) # 팔로우 관련 사용자
if me in click_user.followee.all(): # 팔로우 하고 있을 경우
click_user.followee.remove(request.user)
else: # 팔로우가 안되어 있는 경우
click_user.followee.add(request.user)
return redirect('/user')
user_view 함수에서는 로그인된 사용자(나)를 제외한 나머지 유저들을 보여주기 위해 사용한 것이고, user_follow 함수에서는 내가 팔로우한 사람, 나를 팔로우한 사람을 불러오도록 만들어주었다.
이렇게 views.py 설정이 끝났다면 해당 함수를 연결해주기 위해 urls.py 안에 해당 내용들을 추가해주었다.
from django.urls import path
from . import views
urlpatterns = [
path('sign-up/', views.sign_up_view, name='sign-up'),
path('sign-in/', views.sign_in_view, name='sign-in'),
path('logout/', views.logout, name='logout'),
path('user/', views.user_view, name='user-list'),
path('user/follow/<int:id>', views.user_follow, name='user-follow'),
]
마지막으로 '친구'를 누르면 해당 팔로우 목록을 보여주는 html을 불러올 수 있도록 base.html의 '친구'를 <a> 태그로 변경해주고 팔로우 목록을 보여주는 user_list.html을 추가해주었다.
base.html
<li class="nav-item active">
<a class="nav-link" href="/user"> 친구 <span class="sr-only"></span></a>
</li>
user_list.html
{% extends 'base.html' %}
{% block title %}
사용자 리스트
{% endblock %}
{% block content %}
<div class="container timeline-container">
<div class="row">
{# 왼쪽 칼럼 #}
<div class="col-md-3">
<div class="card">
<div class="card-body">
<h5 class="card-title">{{ user.username }}</h5>
<p class="card-text"> {{ user.bio }}</p>
</div>
</div>
</div>
{# 오른쪽 칼럼 #}
<div class="col-md-7">
<div class="row">
<div class="alert alert-success" role="alert">
나를 팔로우 하는 사람 수 : {{ user.followee.count }} 명 / 내가 팔로우 하는 사람 수 : {{ user.follow.count }} 명
</div>
</div>
<div class="row">
{# 사용자 리스트 반복문 #}
{% for ul in user_list %}
<div class="card">
<div class="card-body">
<h5 class="card-title">{{ ul.username }}</h5>
<h6 class="card-subtitle mb-2 text-muted">{{ ul.email }}</h6>
<p class="card-text">
{{ ul.bio }}
</p>
<p class="card-text">
팔로잉 {{ ul.follow.count }} 명 / 팔로워 {{ ul.followee.count }} 명
</p>
{% if ul in user.follow.all %}
<a href="/user/follow/{{ ul.id }}" class="card-link">[팔로우 취소]</a>
{% else %}
<a href="/user/follow/{{ ul.id }}" class="card-link">[팔로우]</a>
{% endif %}
</div>
</div>
<hr>
{% endfor %}
</div>
</div>
<div class="col-md-2"></div>
</div>
</div>
{% endblock %}
이렇게 해준다면 팔로우 기능도 정상적으로 작동하는 것을 볼 수 있었다.

이제 전에 만들어두었던 회원가입과 로그인 기능들에 대해 조금 수정을 해보고자 한다.
이전 회원가입과 로그인에는 빈칸을 입력하거나 중복되는 값을 입력해도 해당 데이터가 데이터베이스에 들어가거나 Django 오류 페이지로 넘어가는 모습을 볼 수 있었다.
그래서 회원가입과 로그인에 빈칸 및 중복이 일어나면 에러를 알리도록 설정을 해보도록 하였다.
우선 회원가입부터 만들어보았다.
def sign_up_view(request):
if request.method == 'GET':
user = request.user.is_authenticated
if user:
return redirect('/')
else:
return render(request, 'user/signup.html')
elif request.method == 'POST':
username = request.POST.get('username', '')
password = request.POST.get('password', '')
password2 = request.POST.get('password2', '')
bio = request.POST.get('bio', '')
if password != password2:
return render(request, 'user/signup.html', {'error': '비밀번호 확인이 필요합니다.'})
else:
if username == '':
return render(request, 'user/signup.html', {'error': '이름을 적어주세요.'})
elif password == '':
return render(request, 'user/signup.html', {'error': '비밀번호를 적어주세요.'})
# filter : 장고에서 테이블 조회 시 필요한 데이터만 조회한다.
fail_user = get_user_model().objects.filter(username=username)
# 사용자가 존재하면 페이지를 다시 로드한다.
if fail_user:
return render(request, 'user/signup.html', {'error': '사용자가 존재합니다.'})
else:
UserModel.objects.create_user(username=username, password=password, bio=bio)
return redirect('/sign-in')
여기서 바뀐 부분은 username, password, password2, bio의 None 부분이 ''로 바뀐 것이다.
원래는 해당 변수들에 저장되는 값을 None으로 설정해주었기 때문에 오류가 발생했던 것이어서 빈 문자열인 ''를 사용해주었다.
그리고 조건문(if)을 사용하여 해당 부분이 빈 문자열이라면 에러 메시지를 출력하고 해당 html을 다시 보여주게끔 만들어주었다.
기존에 있던 부분들도 에러 메시지를 출력하도록 바꿔주고 해당 html을 불러올 수 있도록 바꿔주었다.
그리고 signup.html에 해당 html 코드를 추가해줌으로써 에러가 발생하면 해당 부분이 생기도록 만들어주었다.
{% if error %}
<div class="alert alert-danger" role="alert">
{{ error }}
</div>
{% endif %}
이렇게 해주면 빈칸 및 중복이 일어날 시 해당 에러 메시지들이 만들어지는 것을 볼 수 있었다.

이제 로그인을 수정해보도록 하자.
로그인도 회원가입과 크게 다르지 않다.
def sign_in_view(request):
if request.method == 'POST':
username = request.POST.get('username', '')
password = request.POST.get('password', '')
# authenticate : 암호화된 비밀번호와 입력된 비밀번호가 일치하는지, 추가로 사용자와 맞는지 확인해준다.
me = auth.authenticate(request, username=username, password=password)
if me is not None: # 위에서 이미 authenticate로 체크했기 때문에 있는지 없는지만 확인하면 된다.
auth.login(request, me) # 장고가 알아서 관리할 수 있도록 로그인 정보를 넣어준다.
return redirect('/')
else:
if username == '':
return render(request, 'user/signin.html', {'error': '이름을 적어주세요.'})
elif password == '':
return render(request, 'user/signin.html', {'error': '비밀번호를 적어주세요.'})
else:
return render(request, 'user/signin.html', {'error': '이름 또는 비밀번호를 확인해주세요.'})
elif request.method == 'GET':
user = request.user.is_authenticated
if user:
return redirect('/')
else:
return render(request, 'user/signin.html')
return render(request, 'user/signin.html')
우선 username과 password를 None이 아닌 빈 문자열 ''로 바꿔주었고, 조건문(if)을 통해 빈칸 및 에러 체크가 가능하도록 고쳐주었다.
그리고 회원가입과 마찬가지로 signin.html에 {% if error %} 부분을 추가해주었다.

회원가입과 로그인 기능들에 대해 빈칸 및 중복 에러 처리에 대한 기능들을 추가해주었다.
이제 추가적으로 로그인을 한 뒤 게시글을 작성할 때 게시글이 빈칸이라면 에러 메시지가 출력되도록 추가 설정을 해주었다.
이 또한 위에서 만들어준 회원가입과 로그인에 크게 벗어나지 않는다.
elif request.method == 'POST':
user = request.user # 현재 로그인되어 있는 사용자의 모든 정보를 가져온다.
content = request.POST.get('my-content', '')
if content == '':
all_tweet = TweetModel.objects.all().order_by('-created_at')
return render(request, 'tweet/home.html', {'error': '글을 적어주세요.', 'tweet': all_tweet})
else:
my_tweet = TweetModel.objects.create(author=user, content=content)
my_tweet.save()
return redirect('/tweet')
tweet 앱의 views.py의 tweet 함수에 있는 POST 부분을 해당 코드와 같이 고쳐주었다.
회원가입과 로그인과 동일하게 None이 아닌 빈 문자열 ''로 바꿔주었고, 글이 비어있으면 에러가 발생하도록 만들어주었다.
그리고 원래 my_tweet.author, my_tweet.content와 같이 코드가 길게 구현되어 있었는데 my_tweet = TweetModel.objects.create(author=user, content=content)를 통해 해당 값들을 저장할 수 있도록 바꿔주었다.
마지막으로 해당 에러가 발생하면 에러 발생 문구를 보여줄 수 있도록 home.html에 {% if error %} 부분을 추가해주었다.

해당 기능들을 구현하고 웹 페이지를 살펴보던 도중 로그인 페이지에서 이상한 점을 하나 발견하였다.
바로 로그인을 하지 않았는데도 로그인 페이지에서 친구 목록이 보이는 것이었다.

로그인 화면에 있다는 것은 로그인된 유저의 정보가 없는 소리인데 친구 버튼이 활성화가 되어있는 것을 볼 수 있었다.
해당 기능을 사용하지 못하게 하기 위해 base.html에 {% if user.is_authenticated %}를 추가하여 로그인 인증이 된 사용자에게만 표시할 수 있도록 바꿔주었다.
{% if user.is_authenticated %}
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav mr-auto">
<li class="nav-item active">
<a class="nav-link" href="/user"> 친구 <span class="sr-only"></span></a>
</li>
</ul>
</div>
{% endif %}

마지막으로 SNS에서 사용되고 있는 태그(Tag) 기능들을 추가해보고자 했다.
해당 태그 기능은 장고에서 해당 기능들에 관련된 모듈이 준비되어 있기 때문에 내가 해줘야 할 것은 해당 모듈을 다운로드하고 사용하기만 하면 된다.

pip install django-taggit과 pip install django-taggit-templatetags2를 터미널 창에서 적어주면 해당 사진과 같이 다운로드가 되는 모습을 볼 수 있다.
여기서 나오는 WARNING 메시지는 현재 사용하고 있는 pip 버전 말고 업그레이드 버전을 사용할 수 있다는 얘기이니 크게 신경 쓰지 않아도 된다.
이렇게 정상적으로 모듈이 설치되었다면 settings.py에서 해당 모듈들에 대한 정보를 추가해주어야 한다.

INSTALLED_APPS 안에 해당 태그 모듈들에 대한 정보를 넣어주었고, 바로 밑에는 태그 모듈에 필요한 정보인 TAGGIT_CASE_INSENSITIVE와 TAGGIT_LIMIT를 추가해주었다.
이제 model을 수정해보도록 하자.
태그 기능은 글에 관련된 기능이니 tweet 앱의 models.py에 들어가서 해당 코드를 추가해주었다.
from django.db import models
from user.models import UserModel
from taggit.managers import TaggableManager # 태그를 추가할 수 있게 하는 기능
# Create your models here.
# 게시글
class TweetModel(models.Model):
class Meta:
db_table = "tweet"
author = models.ForeignKey(UserModel, on_delete=models.CASCADE)
content = models.CharField(max_length=256)
tags = TaggableManager(blank=True) # blank=True : 해당 데이터가 비어있어도 작동할 수 있음
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
이렇게 모델 부분이 수정되었으니 makemigrations와 migrate를 통해 수정 및 반영을 해주어야 한다.

모델을 적용해준 뒤 tweet의 views.py에 해당 태그 기능들에 대한 코드를 추가하고 urls.py에 연결해주면 태그 기능들에 대한 설정이 끝나게 된다.
from django.views.generic import ListView, TemplateView
~
def tweet(request):
if request.method == 'GET':
user = request.user.is_authenticated
if user:
# TweetModel.objects.all() : TweetModel에 있는 모든 데이터를 불러온다.
# order_by() : 데이터 생성된 시간을 역순으로 출력
all_tweet = TweetModel.objects.all().order_by('-created_at') # - : 역순 정렬
return render(request, 'tweet/home.html', {'tweet': all_tweet})
else:
return redirect('/sign-in')
elif request.method == 'POST':
user = request.user # 현재 로그인되어 있는 사용자의 모든 정보를 가져온다.
content = request.POST.get('my-content', '')
tags = request.POST.get('tag', '').split(',')
if content == '':
all_tweet = TweetModel.objects.all().order_by('-created_at')
return render(request, 'tweet/home.html', {'error': '글을 적어주세요.', 'tweet': all_tweet})
else:
my_tweet = TweetModel.objects.create(author=user, content=content)
for tag in tags:
tag = tag.strip()
if tag != '':
my_tweet.tags.add(tag)
my_tweet.save()
return redirect('/tweet')
~
# 태그들을 모아놓은 태그 클라우드
class TagCloudTV(TemplateView):
template_name = 'taggit/tag_cloud_view.html'
# 작성된 태그들을 모아 화면에 전달하는 역할
class TaggedObjectLV(ListView):
template_name = 'taggit/tag_with_post.html'
model = TweetModel
def get_queryset(self):
return TweetModel.objects.filter(tags__name=self.kwargs.get('tag'))
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['tagname'] = self.kwargs['tag']
return context
from django.urls import path
from . import views
urlpatterns = [
path('', views.home, name='home'),
path('tweet/', views.tweet, name='tweet'),
path('tweet/delete/<int:id>', views.delete_tweet, name='delete-tweet'),
path('tweet/<int:id>', views.detail_tweet, name='detail-tweet'),
path('tweet/comment/<int:id>', views.write_comment, name='write-comment'),
path('tweet/comment/delete/<int:id>', views.delete_comment, name='delete-comment'),
path('tag/', views.TagCloudTV.as_view(), name='tag_cloud'),
path('tag/<str:tag>/', views.TaggedObjectLV.as_view(), name='tagged_object_list'),
]
해당 views.py에 있는 class의 TagCloudTV와 TaggedObjectLV은 django-taggit 모듈에서 기본적으로 제공해주는 것이니 현재 상태로는 바꿔줄 필요가 없다.
이제 views.py와 urls.py도 수정이 끝이 났다.
해당 태그 기능을 보여주기 위한 html 파일들을 만들어서 화면에 보여주도록 만들어주었다.
우선 글쓰기 기능이 있는 home.html에 태그를 적어줄 수 있는 칸과 해당 태그들이 표시되는 칸을 추가해주었다.
# 게시글 작성 부분
<div class="mt-3 row">
<label for="tag"
class="col-sm-2 col-form-label">이 글의 태그</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="tag" id="tag"
placeholder="콤마(,)로 구분">
</div>
</div>
# 작성된 게시글을 보여주는 부분
{% if tw.tags.all %}
{% for tag in tw.tags.all %}
<a style="text-decoration: none" href="{% url 'tagged_object_list' tag.name %}">
<span class="badge rounded-pill bg-success">
{{ tag.name }}
</span>
</a>
{% endfor %}
-<a style="text-decoration: none" href="{% url 'tag_cloud' %}">TagCloud</a>
{% endif %}
그리고 게시글 상세 페이지에서도 해당 태그들을 보여줄 수 있도록 tweet_detail.html 부분도 추가해주기로 하였다.
{# 해당 글에 태그가 있으면 태그를 보여준다. #}
{% if tweet.tags.all %}
{% for tag in tweet.tags.all %}
<a style="text-decoration: none"
href="{% url 'tagged_object_list' tag.name %}">
<span class="badge rounded-pill bg-success">
{{ tag.name }}
</span>
</a>
{% endfor %}
-<a style="text-decoration: none"
href="{% url 'tag_cloud' %}">TagCloud</a>
{% endif %}
마지막으로 각 태그들이 어떤 게시글에 쓰였는지 보여주는 tag_with_post.html 페이지와 모든 태그들이 몇 번 사용되었는지 보여주는 tag_cloud_view.html 페이지를 만들어주었다.

tag_with_post.html
{% extends "base.html" %}
{% block title %}태그 글 리스트{% endblock %}
{% block content %}
<div class="container">
<h3 class="mt-2">Posts for tag - {{ tagname }}</h3>
<hr>
<div class="card">
<div class="card-body">
{% for tweet in object_list %}
<h4>
<a href="/tweet/{{ tweet.id }}">{{ tweet.content }}</a>
</h4>
{{ tweet.updated_at|timesince }}
<p> {{ tweet.author }}</p>
{% endfor %}
</div>
</div>
</div>
{% endblock %}
tag_cloud_view.html
{% extends "base.html" %}
{% block title %}태그 클라우드{% endblock %}
{% block content %}
<div class="container timeline-container">
<div class="tag-cloud">
{% load taggit_templatetags2_tags %}
{% get_tagcloud as tags %} <!--모든 태그 추출해서 tags변수에 할당-->
{% for tag in tags %}
<a style="text-decoration: none" href="{% url 'tagged_object_list' tag.name %}">
<span class="badge rounded-pill bg-primary">
{{ tag.name }}({{ tag.num_times }})
</span>
</a>
{% endfor %}
</div>
</div>
{% endblock %}
이렇게 만들어준 뒤 해당 코드를 실행하면 다음과 같은 모습을 볼 수 있었다.

-
장고를 이용하여 웹 페이지를 만들어보는 시간을 가졌다.
짧은 시간이었지만 해당 코드들을 구현하면서 장고에 대한 기초를 다지는 시간을 가질 수 있었던 것 같다.
처음에는 views.py와 urls.py를 연결하는 과정에서 다소 어려움을 겪었지만, 코드를 계속 구현해보니 조금씩 이해되는 것 같았다.
아직은 많이 부족하니 장고를 사용하여 코드를 계속 구현해보고 연습하는 계기를 가져야 익숙해지는 결과를 볼 수 있을 것 같다.
:D
'TIL 및 WIL > TIL (Today I Learned)' 카테고리의 다른 글
| [TIL] 2022.06.07 (Django 추천 시스템 팀 프로젝트2) (0) | 2022.06.07 |
|---|---|
| [TIL] 2022.06.02 (Django 추천 시스템 팀 프로젝트1) (0) | 2022.06.03 |
| [TIL] 2022.05.30 (Django 기초2) (0) | 2022.05.30 |
| [TIL] 2022.05.27 (Django 기초1) (0) | 2022.05.27 |
| [TIL] 2022.05.26 (Django 기초0, Python 문법 복습) (1) | 2022.05.26 |