Django insta pj (3)

Haks.·2025년 2월 4일
0

Study

목록 보기
48/65

📝 태그 기능

  • 많은 양의 데이터를 불러오는 것은 부하가 심해 태그에 해당하는 것을 불러오는 것은 속도가 빨라지는 기능이 있다.
  1. 태그를 수정할때 CreateView와 UpdateView에 둘다 수정해 주자, Model의 def save 함수를 수정해도 되지만 새로운 방법 시도
  2. @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)
  1. 내용 수정 눌러서 본문에 #django #python 넣어보기, db.browser로 잘들어가나 확인, 정규표현식으로 처리했기에 띄워쓰기 허용 X

📝 댓글 기능

  1. list.html content 아래쪽
  2. font awesome -> comment 검색 -> 둘중 클릭하고 html에있는것 복사 -> div에 붙어넣기, like 도 바로밑에 붙여넣기, 코멘트 유저넣기
    {{ 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>
  1. 댓글 적는기능 , 보통 댓글 위에 있음 post/forms.py CommnetForm 제작
# post/forms.py
class CommentForm(BootstrapModelForm):
    class Meta:
        model = Comment
        fields = ('content',)
  1. post 목록을 가져올때 같이 가져오면 된다. ListView 에서 context 가져오는 곳에 comment 도 가져오게 추가
# 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
  1. list.html 가서 form 제작, 그리고 댓글 생성하는 것도 로그인이 되어있어야 가능하게끔 제작
    <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');
})
  1. 댓글 View 제작, url 연결, include 사용, list.html에 action url 작성
# 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">
  1. post 저장 안해줘서 같이 안됨 어디에 댓글달아야 할지 지정, + comments 역참조를 미리 해줘야 db에 부하가 덜함
# 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 %}
  1. 최종 python,html
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 문자열에서 해시태그의 내용(단어 부분만)을 추출하는 역할을 합니다.


✅ 정규 표현식(r'#(\w{1,100})(?=\s|$)') 분석

1️⃣ #

  • 해시태그(#)로 시작하는 부분을 찾습니다.

2️⃣ (\w{1,100})

  • \w알파벳(A-Z, a-z), 숫자(0-9), 밑줄(_) 중 하나를 의미합니다.
  • {1,100} → 최소 1자, 최대 100자의 문자(단어)를 포함합니다.
  • 괄호 ()캡처 그룹을 사용하여 해시태그 기호(#)를 제외한 실제 단어만 추출합니다.

3️⃣ (?=\s|$)

  • 긍정형 전방탐색(Lookahead): 특정 패턴이 뒤에 오는 경우만 매칭됩니다.
  • \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에서 해시태그를 찾아 리스트로 반환
👉 # 기호 없이 태그 내용만 리스트에 저장


📝 좋아요 기능 제작

  1. model 제작, migrate
# 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}'
  1. 이또한 역참조 listview에서 등록
    queryset = Post.objects.all().select_related('user').prefetch_related('images','comments', 'likes')
  1. 좋아요 부분 html 작성
<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>
  1. 버튼 생성 했으니 클릭 했을때 처리는 기능 제작 like_views.py 생성 해도 되지만 기능이 많지 않으니 post/views.py 에 그냥 제작
# 이 뷰에서만 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 가 나올것임
  1. html 에서 이제 pk와 함께 보내주도록 수정
<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')
        }
    })
})
  1. url 연결
# config/urls.py
    path('like/', post_views.toggle_like, name='toggle_like'), # 함수내에서 post_pk 받도록 설정해놓음
  1. 색상 바꾸는것까지 전부다한 html
{% 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 %}
  1. 좋아요 빨간색 눌러져있는거 게속 표기하기 위해 Python Package 폴더 templatetags 생성, list.html 수정
# 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 }}">

📝 유저 상세페이지 제작

  1. members/views.py 에 제작 DetialView, str타입 slug를 인자로 받아 사용
# 보통 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')),
  1. detail.html 제작
{% 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: User가 작성한 모든 Post 객체
  • post_set__images: 해당 Post에 연결된 모든 Image 객체
즉, post_set__images는 특정 사용자가 작성한 모든 게시물에 포함된 모든 이미지 데이터를 가져오는 것입니다.

📝 팔로우 모델 및 기능 구현

  1. member/models.py, User 모델에 following 필드 추가
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') # 내가 팔로잉 하는 사람
  1. migrate
  2. python manage.py shell_plus를 통해 팔로우 해보기
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를 팔로우하는 유저들
  1. post/list.html 링크 연결
<a href="{% url 'profile:detail' post.user.nickname %}", class="text text-decoration-none text-black">
<!-- 작성자 -->
{{ post.user.nickname }}
</a>
  1. 000자리간격으로 숫자 콤마 찍기위해 installed_app 추가 설치필요 X, 'django.contrib.humanize',, profile/detail.html {% load humanize %} 추가, html 추가
<div class="col-4 text-center">{{ object.post_set.count | intcomma }} posts</div>
  1. follower, following 추가
<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>
  1. prefetched 부착
queryset = User.objects.all().prefetch_related('post_set','post_set__images', 'following', 'followers') # post 의 foreginkey 같은 것을 가져올떄 __ 를 사용하면 알아서 처리됨
  1. following 기능 제작 members/views.py View
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
        #     )
  1. Django에서 기본적으로 ManyToManyField는 중복된 관계를 자동으로 방지하지만, through를 사용하여 중간 테이블을 직접 정의한 경우, 중복을 방지하는 설정이 필요합니다.
    이를 위해 unique_together를 설정하여 동일한 팔로우 관계가 중복 저장되지 않도록 합니다.
# 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 오류발생
  1. url 연결
# member/urls.py
path('<int:pk>/follow/', views.UserFollowingView.as_view(), name='follow'),
  1. html profile/detail.html 수정, 본인일 경우 안보여주기
{% 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 %}
  1. Follow 하고있는 경우 unfollow 뜨게 하기 member/views.py
# 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
  1. 추가한 context 데이터를 통해 detail.html 수정
<!-- profile/detail.html -->
<button class="btn btn-primary btn-sm ms-3">
    {% if is_follow %}
        Unfollow
    {% else %}
        Follow
    {% endif %}
</button>

Django에서 ForeignKey 또는 ManyToManyField를 사용할 때, related_name을 명시적으로 지정하지 않으면 Django가 기본적으로 역참조 이름을 자동으로 생성합니다.

class Post(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)  # related_name을 지정하지 않음

위와 같이 related_name을 지정하지 않으면 Django는 자동으로 모델명_set 형식의 역참조 이름을 생성합니다.

user.post_set.all() → 특정 유저가 작성한 모든 게시물을 가져올 수 있음.


Django에서 related_name역참조(Reverse Lookup) 를 위한 이름을 설정하는 역할을 합니다.

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이 직접 설정되었기 때문)


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를 사용하여 중간 테이블을 직접 설정한 경우, Django는 자동으로 역참조 필드를 생성하지 않습니다.
이럴 때는 반드시 related_name을 명시해야 합니다.

📌 예제 4: 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'을 직접 설정하면 더 직관적


  1. 기본적으로 모델명_set 형식이 자동 생성되므로 가독성이 떨어짐
    user.post_set.all()  # ❌ 의미가 명확하지 않음
    user.posts.all()  # ✅ related_name='posts'를 사용하면 직관적
  2. 같은 모델을 두 개 이상 참조할 경우 Django가 어떤 필드를 참조해야 할지 몰라 오류 발생
    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')postsuser.posts.all()
ManyToManyField('self')user_set 자동 생성user.user_set.all()
ManyToManyField('self', related_name='followers')followersuser.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_followinguser.user_following.all()

즉, related_name을 설정하지 않으면 Django가 기본적으로 모델명_set을 자동 생성한다!
하지만 직접 related_name을 설정하면, 더 직관적인 코드 작성이 가능하고 Django의 역참조 기능을 쉽게 활용할 수 있다! 🚀🔥

이제 related_name이 자동으로 만들어지는지, 언제 직접 설정해야 하는지 확실히 이해됐을 거야!
혹시 더 궁금한 점 있으면 질문 줘! 😊🔥

0개의 댓글

관련 채용 정보