- 많은 양의 데이터를 불러오는 것은 부하가 심해 태그에 해당하는 것을 불러오는 것은 속도가 빨라지는 기능이 있다.
@receiver(post_save, sender=Post)
저장한 후에 무엇을 만들 것이냐# Post라는 모델이 저장된 이후에 sender 는 모델임, 이후에 receiver 함수가 호출되는것
@receiver(post_save, sender=Post) # pre_save: 세이브하기 전에, post save : 세이브 이후에
def post_post_save(sender, instance, created, **kwargs):
# # 장고 , 이런 텍스트를 분류 하기 위하여
hashtags = re.findall(r'#(\w{1,100})(?=\s|$)', instance.content) # 본문에서 해쉬태그 글자를 찾는거임
instance.tags.clear() # related_name을 tags 라 한것을 전부 비워버림
if hashtags:
tags = [
Tag.objects.get_or_create(tag=hashtag) # 태그가 있으면 가져오고 없으면 만들라는 것, Ture면 생성을 한것이고 False 면 있던것을 가져온것
for hashtag in hashtags
# tags=[ # 이런식으로 나오는 거임
# [Tag,False]
# [Tag,False
# ]
]
tags = [tag for tag, _ in tags] # 위와 같이 False, True 이걸 제외하고 tag 내용들만 리스트로 저장하기
instance.tags.add(*tags)
{{ post.content | linebreaksbr }}
</div>
<i class="fa-regular fa-comment"></i>
<i class="fa-solid fa-heart"></i>
</div>
<div>
{% for comment in post.comment_set.all %}
<p>
{{ comment.user }} | {{ comment.content | linebreaksbr }}
</p>
{% endfor %}
</div>
# post/forms.py
class CommentForm(BootstrapModelForm):
class Meta:
model = Comment
fields = ('content',)
# post/views.py
class PostListView(ListView):
queryset = Post.objects.all().select_related('user').prefetch_related('images')
template_name = 'post/list.html'
paginate_by = 5
ordering = ['-created_at']
def get_context_data(self, *args, **kwargs):
data = super().get_context_data(*args, **kwargs)
data['comment_form'] = CommentForm()
return data
<div>
<button class="add-comment">댓글 작성</button>
</div>
</div>
<div>
<!-- 댓글 작성 버튼 누르면 나오게 하려고 -->
<div class="comment_form d-none">
{% if request.user.is_authenticated %}
<form action="" method ="post">
{% csrf_token %}
{{ comment_form.as_p }}
<button class="btn btn-primary btn-sm">생성</button>
</form>
{% endif %}
</div>
// 이부분 추가 script 젤밑에
$('.add-comment').on('click', function(){
$(this).parents('.infinite-item').find('.comment-form').toggleClass('d-none');
})
# post/comment_views.py , comment urls 생성
class CommentCreateView(LoginRequiredMixin, CreateView):
model = Comment
form_class = CommentForm
def form_valid(self, form):
self.object = form.save(commit=False)
self.object.user = self.request.user
self.object.save()
return HttpResponseRedirect(reverse('main'))
# post/comment_urls.py
from django.urls import path
import post.comment_views as views
app_name = "comment"
urlpatterns= [
path('create/', views.CommentCreateView.as_view(), name='create'),
]
# config/urls.py
path('comment/', include('post.comment_urls')),
<form action="{% url 'comment:create' %}" method ="post">
# comment_urls.py
path('create/<int:post_pk>/', views.CommentCreateView.as_view(), name='create'),
# post/views.py 수정
def form_valid(self, form):
self.object = form.save(commit=False)
self.object.user = self.request.user # 유저는 입력 부분이 없을테니까
post = Post.objects.get(pk=self.kwargs.get('post_pk'))
self.object.post = post
self.object.save()
# post/views.py
class PostListView(ListView):
queryset = Post.objects.all().select_related('user').prefetch_related('images','comments')
<form action="{% url 'comment:create' post.pk %}" method ="post">
{% for comment in post.comment_set.all %} -> {% for comment in post.comments.all %}
class CommentCreateView(LoginRequiredMixin, CreateView):
model = Comment
form_class = CommentForm
def form_valid(self, form):
self.object = form.save(commit=False)
self.object.user = self.request.user
post = Post.objects.get(pk=self.kwargs.get('post_pk'))
self.object.post = post
self.object.save()
return HttpResponseRedirect(reverse('main'))
{% extends 'base.html' %}
{% load static %}
{% block style %}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.css"/>
<style>
.post-image{
aspect-ratio: 1 / 1;
object-fit: cover;
}
</style>
{% endblock %}
{% block content %}
<div class="row">
<div class="text-end col-10 offset-1 col-lg-6 offset-lg-3">
<a class="btn btn-sm btn-info" href="{% url 'create' %}">생성</a>
</div>
<div class="col-10 offset-1 col-lg-6 offset-lg-3 infinite-container">
{% for post in object_list %}
<div class="border-bottom my-4 pb-2 infinite-item">
<div class="mb-2">
<span class="p-2 border rounded-circle me-2">
<i class="fa-solid fa-user" style="width: 16px; padding-left: 3px;"></i>
</span>
<!-- 작성자 -->
{{ post.user.nickname }}
{% if post.user == request.user %}
<a href="{% url 'update' post.pk %}" class="btn btn-warning btn-sm float-end">
수정
</a>
{% endif %}
</div>
<div class="swiper" style="max-height: 500px;"> <!-- 메인 swiper -->
<div class="border-1 swiper-wrapper"> <!-- 이미지가 여러개 있을거니까 여기에 wrapper -->
{% for image in post.images.all %}
<div class="swiper-slide">
<img class="img-fluid post-image" src="{{ image.image.url }}" alt="">
</div>
{% endfor %}
</div>
<div class="swiper-pagination"></div>
</div>
<div>
{# TODO : like #}
</div>
<div class="my-2">
{{ post.content | linebreaksbr }}
</div>
<div>
<i class="fa-regular fa-comment"></i>
<i class="fa-solid fa-heart"></i>
<div>
<button class="add-comment btn btn-primary btn-sm">댓글 작성</button>
</div>
</div>
<div class="mt-2">
<!-- 댓글 작성 버튼 누르면 나오게 하려고 -->
<div class="comment-form d-none">
{% if request.user.is_authenticated %}
<form action="{% url 'comment:create' post.pk %}" method ="post">
{% csrf_token %}
{{ comment_form.as_p }}
<button class="btn btn-primary btn-sm">생성</button>
</form>
{% endif %}
</div>
{% for comment in post.comments.all %}
<p>
<span class="px-1 py-0 border rounded-circle me-2">
<i class="fa-solid fa-user fa-xs" style="width: 8px; padding-left: 1px;"></i>
</span>
<strong>{{ comment.user }}</strong> | {{ comment.content | linebreaksbr }}
</p>
{% endfor %}
</div>
</div>
{% endfor %}
<!-- has_next : 다음페이지가 있는지 확인하는 기능 -->
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}" class="infinite-more-link d-none"></a>
{% endif %}
</div>
</div>
{% endblock %}
{% block js %}
<script src="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.js"></script>
<script src="{% static 'js/jquery.min.js' %}"></script>
<script src="{% static 'waypoints/jquery.waypoints.min.js' %}"></script>
<script src="{% static 'waypoints/infinite.min.js' %}"></script>
<!-- swiper Initialize Swiper 부분 가져오기 -->
<script>
const swiper = new Swiper('.swiper', {
// Optional parameters
direction: 'horizontal',
loop: false,
// If we need pagination
pagination: {
el: '.swiper-pagination',
},
});
let infinite = new Waypoint.Infinite({
element: $('.infinite-container')[0],
offset: 'bottom-in-view'
})
$('.add-comment').on('click', function(){
$(this).parents('.infinite-item').find('.comment-form').toggleClass('d-none');
})
</script>
{% endblock %}
re.findall(r'#(\w{1,100})(?=\s|$)', instance.content)
코드 분석hashtags = re.findall(r'#(\w{1,100})(?=\s|$)', instance.content) # 본문에서 해시태그 글자를 찾는 코드
이 코드는 정규 표현식(Regex)을 사용하여 instance.content
문자열에서 해시태그의 내용(단어 부분만)을 추출하는 역할을 합니다.
#
#
)로 시작하는 부분을 찾습니다.(\w{1,100})
\w
→ 알파벳(A-Z, a-z), 숫자(0-9), 밑줄(_) 중 하나를 의미합니다.{1,100}
→ 최소 1자, 최대 100자의 문자(단어)를 포함합니다.()
→ 캡처 그룹을 사용하여 해시태그 기호(#
)를 제외한 실제 단어만 추출합니다.(?=\s|$)
\s
→ 공백(스페이스, 탭, 줄바꿈) $
→ 문자열의 끝instance.content
에서 해시태그(#태그
형태의 단어)를 찾아 리스트로 반환합니다.#
기호는 제외하고, 순수한 단어(태그명)만 리스트에 포함합니다.import re
content = "오늘은 #Python 공부 중! #Django #Web_Dev"
hashtags = re.findall(r'#(\w{1,100})(?=\s|$)', content)
print(hashtags)
['Python', 'Django', 'Web_Dev']
✅ #
기호 없이 태그명만 리스트로 추출됨!
📌 정규 표현식의 의미:
👉 #
로 시작하고, 최대 100자의 단어를 포함하며, 뒤에 공백 또는 문자열 끝이 올 경우만 매칭
👉 ()
그룹을 사용하여 해시태그 기호(#)를 제외한 단어만 추출
📌 사용 목적:
👉 instance.content
에서 해시태그를 찾아 리스트로 반환
👉 #
기호 없이 태그 내용만 리스트에 저장
# post/models.py
class Like(TimestampModel):
post = models.ForeignKey(Post, related_name='likes', on_delete=models.CASCADE)
user = models.ForeignKey(User, related_name='likes', on_delete=models.CASCADE)
def __str__(self):
return f'[like]{self.post} | {self.user}'
queryset = Post.objects.all().select_related('user').prefetch_related('images','comments', 'likes')
<div class="mt-1">
<button class="border-0 bg-transparent rounded-3">
<i class="fa-solid fa-heart"></i>
</button>
<button class="add-comment border-0 bg-transparent rounded-3">
<i class="fa-regular fa-comment"></i>
</button>
</div>
# 이 뷰에서만 csrf 무시
@csrf_exempt
@login_required()
def toggle_like(request, post_pk):
post_pk = request.POST.get('post_pk')
if not post_pk:
raise Http404
post = get_object_or_404(Post, pk=post_pk)
user = request.user
like, created = Like.objects.get_or_create(user=user, post=post)
if not created:
like.delete()
# 페이지 전환을 하는것이 아닌 자바스크립트로 전화해서 요청할 것이다
return JsonResponse({'created': created}) # 생성했으면 True, 안했으면 Fasle 가 나올것임
<button class="border-0 bg-transparent rounded-3 like-btn" data-post_pk="{{ post.pk }}">
$('.like-btn').on('click', function () {
$.ajax({
url: '{% url "toggle_like" %}',
method: 'post',
data: {
'post_pk': $(this).data('post_pk')
},
success: function (){
console.log('success')
},
error: function (){
console.log('error')
}
})
})
# config/urls.py
path('like/', post_views.toggle_like, name='toggle_like'), # 함수내에서 post_pk 받도록 설정해놓음
{% extends 'base.html' %}
{% load static %}
{% load custom_tag %}
{% block style %}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.css"/>
<style>
.post-image{
aspect-ratio: 1 / 1;
object-fit: cover;
}
</style>
{% endblock %}
{% block content %}
<div class="row">
<div class="text-end col-10 offset-1 col-lg-6 offset-lg-3">
<a class="btn btn-sm btn-info" href="{% url 'create' %}">생성</a>
</div>
<div class="col-10 offset-1 col-lg-6 offset-lg-3 infinite-container">
{% for post in object_list %}
<div class="border-bottom my-4 pb-2 infinite-item">
<div class="mb-2">
<span class="p-2 border rounded-circle me-2">
<i class="fa-solid fa-user" style="width: 16px; padding-left: 3px;"></i>
</span>
<!-- 작성자 -->
{{ post.user.nickname }}
{% if post.user == request.user %}
<a href="{% url 'update' post.pk %}" class="btn btn-warning btn-sm float-end">
수정
</a>
{% endif %}
</div>
<div class="swiper" style="max-height: 500px;"> <!-- 메인 swiper -->
<div class="border-1 swiper-wrapper"> <!-- 이미지가 여러개 있을거니까 여기에 wrapper -->
{% for image in post.images.all %}
<div class="swiper-slide">
<img class="img-fluid post-image" src="{{ image.image.url }}" alt="">
</div>
{% endfor %}
</div>
<div class="swiper-pagination"></div>
</div>
<div>
</div>
<div class="mt-1">
<button class="border-0 bg-transparent rounded-3 like-btn{% add_like_class request.user post.likes.all %}" data-post_pk="{{ post.pk }}">
<i class="fa-solid fa-heart"></i>
</button>
<button class="add-comment border-0 bg-transparent rounded-3">
<i class="fa-regular fa-comment"></i>
</button>
</div>
<div>
{{ post.likes.count }} likes
</div>
<div class="my-2">
{{ post.content | linebreaksbr }}
</div>
<div>
</div>
<div class="mt-2">
<!-- 댓글 작성 버튼 누르면 나오게 하려고 -->
<div class="comment-form d-none">
{% if request.user.is_authenticated %}
<form action="{% url 'comment:create' post.pk %}" method ="post">
{% csrf_token %}
{{ comment_form.as_p }}
<button class="btn btn-primary btn-sm">생성</button>
</form>
{% endif %}
</div>
{% for comment in post.comments.all %}
<p>
<span class="px-1 py-0 border rounded-circle me-2">
<i class="fa-solid fa-user fa-xs" style="width: 8px; padding-left: 1px;"></i>
</span>
<strong>{{ comment.user }}</strong> | {{ comment.content | linebreaksbr }}
</p>
{% endfor %}
</div>
</div>
{% endfor %}
<!-- has_next : 다음페이지가 있는지 확인하는 기능 -->
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}" class="infinite-more-link d-none"></a>
{% endif %}
</div>
</div>
{% endblock %}
{% block js %}
<script src="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.js"></script>
<script src="{% static 'js/jquery.min.js' %}"></script>
<script src="{% static 'waypoints/jquery.waypoints.min.js' %}"></script>
<script src="{% static 'waypoints/infinite.min.js' %}"></script>
<!-- swiper Initialize Swiper 부분 가져오기 -->
<script>
const swiper = new Swiper('.swiper', {
// Optional parameters
direction: 'horizontal',
loop: false,
// If we need pagination
pagination: {
el: '.swiper-pagination',
},
});
let infinite = new Waypoint.Infinite({
element: $('.infinite-container')[0],
offset: 'bottom-in-view'
})
$('.add-comment').on('click', function(){
$(this).parents('.infinite-item').find('.comment-form').toggleClass('d-none');
})
$('.like-btn').on('click', function () {
const this_btn = $(this);
$.ajax({
url: '{% url "toggle_like" %}',
method: 'post',
data: {
'post_pk': $(this).data('post_pk')
},
success: function (res){
if(res.created) {
this_btn.addClass('text-danger')
} else {
this_btn.removeClass('text-danger')
}
},
error: function (){
console.log('error')
}
})
})
</script>
{% endblock %}
# post/templatetags/custom_tag.py
from django import template
register=template.Library()
@register.simple_tag()
def add_like_class(user, likes):
for like_obj in likes:
if like_obj.user == user:
return ' text-danger'
return ''
<button class="border-0 bg-transparent rounded-3 like-btn{% add_like_class request.user post.likes.all %}" data-post_pk="{{ post.pk }}">
# 보통 pk를 쓰지만 안쓰고 profile/'slug' 으로 가는 것을 제작
class UserProfileView(DetailView):
model = User
template_name = 'profile/detail.html'
slug_field = 'nickname' # 어느 컬럼에서 찾을건지 (unique 한것)
slug_url_kwarg = 'slug' # 닉네임이라는 키를 받아, 매칭해서 가져오는 것 pk 와 같이, 이걸 안적어 놓으면 기본키 pk를 가져옴
# urls 에 slug로 입력하면 됨
# DB 부하 줄이기위해 한번에 불러오기
queryset = User.objects.all().prefetch_related('post_set','post_set__images') # post 의 foreginkey 같은 것을 가져올떄 __ 를 사용하면 알아서 처리됨
# member/urls.py
from django.urls import path
from . import views
app_name= 'profile'
urlpatterns=[
path('<str:slug>/',views.UserProfileView.as_view(), name='detail'),
]
# config/urls.py
path('profile/', include('member.urls')),
{% extends 'base.html' %}
{% block style %}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.css"/>
<style>
.post-image{
aspect-ratio: 1 / 1;
object-fit: cover;
}
</style>
{% endblock %}
{% block content %}
<div class="row">
<div class="col-10 offset-1 col-lg-6 offset-lg-3 infinite-container">
<span class="p-2 border rounded-circle me-2">
<i class="fa-solid fa-user" style="width: 16px; padding-left: 3px;"></i>
</span>
<div>
{{ object.nickname }}
<button class="btn btn-primary brn-sm mt-3">Follow</button>
</div>
<div class="row">
<div class="col-4 text-center">{{ object.post_set.count }}</div>
<div class="col-4 text-center">0 followers</div>
<div class="col-4 text-center">0 follwing</div>
</div>
<div class="row mt-2">
{% for post in object.post_set.all %}
<div class="col-4">
<!-- swiper 로 만들어 줄거임 post/list.html 부분 가져오자-->
<div class="swiper"> <!-- 메인 swiper -->
<div class="border-1 swiper-wrapper"> <!-- 이미지가 여러개 있을거니까 여기에 wrapper -->
{% for image in post.images.all %}
<div class="swiper-slide">
<img class="img-fluid post-image" src="{{ image.image.url }}" alt="">
</div>
{% endfor %}
</div>
<div class="swiper-pagination"></div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
{% endblock %}
{% block js %}
<script src="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.js"></script>
<script>
const swiper = new Swiper('.swiper', {
// Optional parameters
direction: 'horizontal',
loop: false,
// If we need pagination
pagination: {
el: '.swiper-pagination',
},
});
</script>
{% endblock %}
queryset = User.objects.all().prefetch_related('post_set','post_set__images')
분석post_set
이란?Django에서 역참조(Reverse Relation) 를 사용할 때, related_name을 명시적으로 설정하지 않으면 기본적으로 모델명_set 형태로 자동 생성됩니다.
class User(AbstractUser): pass # 유저 모델
class Post(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE) # User 모델과 연결
content = models.TextField()
* post.user -> 해당 게시물을 작성한 사용자 가져오기(User 객체)
* user.post_set.all() 해당 사용자가 작성한 Post 리스트 가져오기
즉, post_set은 User → Post 간의 관계에서, 사용자가 작성한 모든 게시물을 가져옴.
👉 post_set = User가 작성한 모든 Post 리스트
#### `post_set_image` 란?
> 만약 Post 모델이 다른 모델(예: Image)과 관계를 맺고 있다면, __를 사용하여 중첩 관계를 가져올 수 있습니다.
```python
class Image(models.Model):
post = models.ForeignKey(Post, related_name="images", on_delete=models.CASCADE) # Post와 1:N 관계
image = models.ImageField(upload_to='images/')
즉, post_set__images는 특정 사용자가 작성한 모든 게시물에 포함된 모든 이미지 데이터를 가져오는 것입니다.
class User(AbstractBaseUser): # 기존에 있는것 말고 커스터마이징 할 것 작성
email = models.EmailField(verbose_name='email', unique=True)
is_active = models.BooleanField(default=False) # is actvie 가 활성화되어 있지 않으면 로그인이 안됨 email 이 인증되면 로그인 활성화하는 작업
is_admin = models.BooleanField(default=False) # admin 사이트에 들어갈수 있는지
nickname= models.CharField('nickname', max_length=20, unique=True)
# 나를 팔로우 하는 사람 팔로워
# 내가 팔로우 하는 사람 팔로잉
# symmetrical = True : a <=> b, symmetrical = False : a => b
# User N : N User, # 본인 상속시에는 self를 넣어야 동작함
following = models.ManyToManyField(
'self', symmetrical=False, related_name='followers',
# 특정 모델을 통해 이사람과 팔로잉 된지 얼마를 처리해야함 => 또하나의 클래스 생성
through='UserFollowing', # 중간모델 직접 정의 # 현 시점 클래스가 만들어지지 않아 오류가 날땐 ' ' 안에 넣어주면 된다.
through_fields=('from_user','to_user') # 팔로잉은 내가 팔로우 하고 있는 사람이기에 내가 from_user다 , 내가 참조하고 있는 사람
)
# 팔로잉 시간 처리를 위한 모델
class UserFollowing(TimestampModel):
# 한테이블에서 같은 foreignkey 를 같은 테이블로 참조하면 어느 것으로 처리해야할지 몰라서 오류가 발생, 그래서 realted_name 을 넣어줘야함
to_user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='user_followers') # 나를 팔로워 하는사람
from_user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='user_following') # 내가 팔로잉 하는 사람
In [1]: u = User.objects.get(email='admin@admin.com')
In [6]: u2 = User.objects.filter(email='admin2@admin.com')
In [11]: UserFollowing.objects.create(to_user=u, from_user=u2)
Out[11]: <UserFollowing: UserFollowing object (1)>
# 나를 팔로우하는 사람 (u를 팔로우하는 유저 수)
In [12]: u.followers.count()
Out[12]: 1
# 내가 팔로우하는 사람 (u2가 팔로우하는 유저 수)
In [14]: u2.following.count()
Out[14]: 1
# u2를 팔로우하는 사람 (u2의 followers 수)
In [16]: u2.followers.count()
Out[16]: 0
# ✔ u.following.all() → u가 팔로우하는 유저들
# ✔ u.followers.all() → u를 팔로우하는 유저들
<a href="{% url 'profile:detail' post.user.nickname %}", class="text text-decoration-none text-black">
<!-- 작성자 -->
{{ post.user.nickname }}
</a>
'django.contrib.humanize',
, profile/detail.html {% load humanize %}
추가, html 추가<div class="col-4 text-center">{{ object.post_set.count | intcomma }} posts</div>
<div class="col-4 text-center">{{ object.post_set.count | intcomma }} posts</div>
<div class="col-4 text-center">{{ object.followers.count | intcomma }} followers</div>
<div class="col-4 text-center">{{ object.following.count | intcomma }} following</div>
queryset = User.objects.all().prefetch_related('post_set','post_set__images', 'following', 'followers') # post 의 foreginkey 같은 것을 가져올떄 __ 를 사용하면 알아서 처리됨
class UserFollowingView(LoginRequiredMixin, View):
def post(self, *args, **kwargs):
# 해당유저의 pk 가있으면 가져오기 편함
pk = kwargs.get('pk', 0)
to_user = get_object_or_404(User, pk=pk)
# 스스로 팔로잉 할수 없으니까
if to_user == self.request.user:
raise Http404
# 있으면 가져오고 없으면 만들어준 다음
following, created = UserFollowing.objects.get_or_create(
to_user=to_user,
from_user=self.request.user
)
# 이미 존재해서 가져온 경우는 팔로잉 취소
if not created:
following.delete()
return HttpResponseRedirect(reverse('profile:detail', kwargs={'slug': to_user.nickname}))
# if following.exists():
# following.delete()
# else: # 중복으로 2개가 생기는것 방지하기위해 model에 추가
# UserFollowing.objects.create(
# to_user=to_user,
# fromm_user=self.request.user
# )
# member/models.py
class UserFollowing(TimestampModel):
# 한테이블에서 같은 foreignkey 를 같은 테이블로 참조하면 어느 것으로 처리해야할지 몰라서 오류가 발생, 그래서 realted_name 을 넣어줘야함
to_user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='user_followers') # 나를 팔로워 하는사람
from_user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='user_following') # 내가 팔로잉 하는 사람
class Meta:
unique_together = ('to_user', 'from_user')
# to_user 1, from_user 2
# to_user 1, from_user 3
# to_user 1, from_user 4
# to_user 1 , from_user 2 오류발생
# member/urls.py
path('<int:pk>/follow/', views.UserFollowingView.as_view(), name='follow'),
{% if object != request.user %}
<form action="{% url 'profile:follow' object.pk %}" method="post" class="d-inline">
{% csrf_token %}
<button class="btn btn-primary btn-sm ms-3">Follow</button>
</form>
{% endif %}
# UserProfileView 수정
def get_context_data(self, **kwargs):
data = super().get_context_data(**kwargs)
if self.request.user.is_authenticated:
data['is_follow'] = UserFollowing.objects.filter(
to_user=self.object,
from_user=self.request.user
)
return data
<!-- profile/detail.html -->
<button class="btn btn-primary btn-sm ms-3">
{% if is_follow %}
Unfollow
{% else %}
Follow
{% endif %}
</button>
related_name
이 자동 생성되는 원리 및 역할related_name
을 설정하지 않았을 때 어떻게 되는가?Django에서 ForeignKey
또는 ManyToManyField
를 사용할 때, related_name
을 명시적으로 지정하지 않으면 Django가 기본적으로 역참조 이름을 자동으로 생성합니다.
related_name
을 지정하지 않은 경우class Post(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE) # related_name을 지정하지 않음
위와 같이 related_name
을 지정하지 않으면 Django는 자동으로 모델명_set
형식의 역참조 이름을 생성합니다.
✔ user.post_set.all()
→ 특정 유저가 작성한 모든 게시물을 가져올 수 있음.
related_name
이 하는 역할Django에서 related_name
은 역참조(Reverse Lookup) 를 위한 이름을 설정하는 역할을 합니다.
related_name
을 지정한 경우class Post(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='posts')
이 경우 user.posts.all()
을 사용하여 특정 사용자가 작성한 모든 Post
를 가져올 수 있습니다.
✔ user.posts.all()
→ 특정 유저가 작성한 게시물 조회
✔ user.post_set.all()
→ ❌ 이제 동작하지 않음 (related_name
이 직접 설정되었기 때문)
related_name
이 자동으로 생성되는 경우ManyToManyField
에서 related_name
을 설정하지 않았을 때class User(models.Model):
following = models.ManyToManyField('self', symmetrical=False)
🚨 여기서 related_name
을 설정하지 않으면, Django는 기본적으로 user_set
이라는 역참조 필드를 자동 생성합니다.
즉, 내가 팔로우하는 사람을 가져오려면:
user.following.all() # 내가 팔로우하는 사람들
그러나 나를 팔로우하는 사람들을 가져오려고 하면 user_set
을 사용해야 합니다.
user.user_set.all() # 나를 팔로우하는 사람들
✅ 따라서 related_name='followers'
을 직접 설정하면 user_set
대신 user.followers.all()
을 사용할 수 있음.
through
를 사용한 경우 related_name
자동 생성 규칙through
를 사용하여 중간 테이블을 직접 설정한 경우, Django는 자동으로 역참조 필드를 생성하지 않습니다.
이럴 때는 반드시 related_name
을 명시해야 합니다.
through
를 사용하여 중간 테이블을 명시한 경우class UserFollowing(models.Model):
from_user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='user_following')
to_user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='user_followers')
위처럼 related_name
을 설정하면:
✔ user.user_following.all()
→ 내가 팔로우한 사람 목록
✔ user.user_followers.all()
→ 나를 팔로우한 사람 목록
🚨 그런데 related_name
을 지정하지 않으면?
class UserFollowing(models.Model):
from_user = models.ForeignKey(User, on_delete=models.CASCADE)
to_user = models.ForeignKey(User, on_delete=models.CASCADE)
이렇게 하면 Django가 자동으로 userfollowing_set
을 생성합니다.
✔ user.userfollowing_set.all()
→ ❌ 가독성이 떨어짐
✔ user.user_following.all()
→ ✅ related_name='user_following'
을 직접 설정하면 더 직관적
related_name
없이 사용할 때의 단점모델명_set
형식이 자동 생성되므로 가독성이 떨어짐user.post_set.all() # ❌ 의미가 명확하지 않음
user.posts.all() # ✅ related_name='posts'를 사용하면 직관적
class UserFollowing(models.Model):
from_user = models.ForeignKey(User, on_delete=models.CASCADE)
to_user = models.ForeignKey(User, on_delete=models.CASCADE)
user.userfollowing_set.all()
을 사용할 수 있지만 의미가 모호함.related_name='user_following'
과 related_name='user_followers'
을 설정하면 각각 역할이 분명해짐.설정 방식 | related_name 자동 생성 | 예제 |
---|---|---|
ForeignKey(User, on_delete=models.CASCADE) | post_set 자동 생성 | user.post_set.all() |
ForeignKey(User, on_delete=models.CASCADE, related_name='posts') | posts | user.posts.all() |
ManyToManyField('self') | user_set 자동 생성 | user.user_set.all() |
ManyToManyField('self', related_name='followers') | followers | user.followers.all() |
ForeignKey(User, on_delete=models.CASCADE) (중간 테이블 through 사용) | userfollowing_set 자동 생성 | user.userfollowing_set.all() |
ForeignKey(User, on_delete=models.CASCADE, related_name='user_following') | user_following | user.user_following.all() |
✔ 즉, related_name
을 설정하지 않으면 Django가 기본적으로 모델명_set
을 자동 생성한다!
✔ 하지만 직접 related_name
을 설정하면, 더 직관적인 코드 작성이 가능하고 Django의 역참조 기능을 쉽게 활용할 수 있다! 🚀🔥
이제 related_name
이 자동으로 만들어지는지, 언제 직접 설정해야 하는지 확실히 이해됐을 거야!
혹시 더 궁금한 점 있으면 질문 줘! 😊🔥