Django

Chapter 10-2 Django에서 LoginRequiredMixin 완전 정복

Chansman 2025. 5. 15. 15:51

🔐 Django에서 LoginRequiredMixin 완전 정복

LoginRequiredMixin은 Django의 클래스 기반 뷰(CBV)에서 로그인한 사용자만 접근 가능하도록 제한할 때 사용하는 믹스인입니다.


1️⃣ LoginRequiredMixin이란?

  • ✅ 로그인하지 않은 사용자가 접근하면 자동으로 로그인 페이지로 리디렉션됩니다.
  • ✅ CBV(Class-Based View)에서 사용하며, FBV(Function-Based View)에서는 @login_required 데코레이터를 사용합니다.
  • ✅ 로그인 후 리디렉션 URL은 settings.LOGIN_URL로 설정된 경로를 따릅니다.

2️⃣ 실전 사용 예시

# post/comment_views.py
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpResponseRedirect
from django.urls import reverse
from django.views.generic import CreateView

from post.forms import CommentForm
from post.models import Comment, Post

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'))

 

post_list_content.html 

fontawesome 에서 comment,like 검색해서 나온것 삽입

추가적으로 comment 가저오기 전체적인 위치 조정

{# templates/post/post_list_content.html #}
{% load static %}

{% 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 }} {{ post.id }}
      {% 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;">
      <div class="border-1 swiper-wrapper">
        {% for post_image in post.images.all %}
          <div class="swiper-slide">
            <img class="img-fluid post-image" src="{{ post_image.image.url }}" alt="">
          </div>
        {% endfor %}
      </div>
      <div class="swiper-pagination"></div>
    </div>
    <div class="my-2">
      {{ post.content|linebreaksbr }}
    </div>
    <div>
        <i class="fa-solid fa-comment"></i>
        <i class="fa-regular fa-heart"></i>
        <div>
            <button class="add-comment">댓글 작성</button>
        </div>
    </div>
    <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>
    <div class="mt-2">
        {% 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 %}

{% if page_obj.has_next %}
  <a href="?page={{ page_obj.next_page_number }}" class="infinite-more-link"></a>
  <div id="scroll-sentinel" style="height:1px;"></div>
{% endif %}

poast/list.html  JS 추가

{% block js %}

<!-- ✅ [추가] jQuery를 가장 먼저 불러오기 -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>

<!-- 기존 swiper 스크립트 유지 -->
<script src="https://cdn.jsdelivr.net/npm/swiper/swiper-bundle.min.js"></script>

<script>
document.addEventListener('DOMContentLoaded', () => {
  let loading = false;

  // Swiper 초기화
  function initSwipers() {
    document.querySelectorAll('.swiper').forEach(el => {
      new Swiper(el, {
        direction: 'horizontal',
        loop: false,
        pagination: { el: el.querySelector('.swiper-pagination') },
      });
    });
  }
  initSwipers();

  async function loadNextPage() {
    if (loading) return;
    const link = document.querySelector('.infinite-more-link');
    if (!link) return;
    loading = true;

    try {
      const res = await fetch(link.href, {
        headers: { 'X-Requested-With': 'XMLHttpRequest' }
      });
      const html = await res.text();
      const tmp = document.createElement('div');
      tmp.innerHTML = html;

      tmp.querySelectorAll('.infinite-item').forEach(item =>
        document.querySelector('.infinite-container').appendChild(item)
      );
      document.querySelector('.infinite-more-link').remove();
      document.getElementById('scroll-sentinel').remove();

      const newLink = tmp.querySelector('.infinite-more-link');
      if (newLink) {
        document.querySelector('.infinite-container').appendChild(newLink);
        const newSentinel = document.createElement('div');
        newSentinel.id = 'scroll-sentinel';
        document.querySelector('.infinite-container').appendChild(newSentinel);
        observer.observe(newSentinel);
      }
      initSwipers();
    } catch (err) {
      console.error('페이지 로드 실패:', err);
    } finally {
      loading = false;
    }
  }

  const observer = new IntersectionObserver(entries => {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        loadNextPage();
      }
    });
  }, {
    root: null,
    rootMargin: '0px 0px 200px 0px',
    threshold: 0
  });

  const sentinel = document.getElementById('scroll-sentinel');
  if (sentinel) observer.observe(sentinel);
});

// ✅ [이벤트 등록은 그대로 유지]
$(document).ready(function(){
  $(document).on('click', '.add-comment', function(){
    $(this).closest('.infinite-item').find('.comment-form').toggleClass('d-none');
  });
});
</script>
{% endblock %}

 

post/forms.py Comment form 작성

 

class CommentForm(BootstrapModelForm):
    class Meta:
        model = Comment
        fields = {'content', }

 

post/views.py context_data get_context_data 함수 작성 역참조 'comment' 추가

class PostListView(ListView):
    model = Post
    queryset = Post.objects.all().select_related('user').prefetch_related('images', 'comments')
    template_name = 'post/list.html'              # 전체 레이아웃용 템플릿
    context_object_name = 'object_list'
    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

post/comment.urls  추가

from django.urls import path
from . import comment_views as views

app_name= 'comment'

urlpatterns = [

    path('create/<int:post_pk>', views.CommentCreateView.as_view(), name='create'),
    
    ]

config/urls.py에 include  추가

# include
    path('comment/', include('post.comment_urls'))

 

 

 

 

🔍 코드 설명

  • LoginRequiredMixin: 로그인하지 않은 사용자는 접근 불가
  • form_valid: 폼이 유효할 경우 실행
  • post = Post.objects.get(...): URL에서 post_pk를 가져와 연결
  • reverse('main'): 댓글 작성 후 메인 페이지로 이동

3️⃣ reverse vs reverse_lazy

항목 reverse reverse_lazy

평가 시점 즉시 평가 지연 평가 (lazy evaluation)
사용 위치 함수 내부, 뷰에서 직접 호출 CBV 속성 (예: success_url)에서 사용
사용 예 HttpResponseRedirect(reverse('main')) success_url = reverse_lazy('main')

📌 요약

  • reverse: 즉시 문자열로 URL을 반환 → 함수 안에서 사용
  • reverse_lazy: 필요한 시점까지 기다렸다가 평가 → CBV 클래스 속성에서 사용

🧠 팁과 주의사항

  • LoginRequiredMixin은 왼쪽에 먼저 작성해야 합니다. 예:
class MyView(LoginRequiredMixin, ListView):
    ...
  • URL 리디렉션 경로를 커스터마이징하고 싶다면 settings.LOGIN_REDIRECT_URL 또는 next 파라미터 사용
  • CBV에서 reverse_lazy를 쓰지 않고 reverse를 쓰면 AppRegistryNotReady 등의 초기화 오류 발생 가능

✅ 결론 정리

목적 사용 도구

로그인 사용자만 접근 LoginRequiredMixin or @login_required
URL 생성 (즉시) reverse()
URL 생성 (지연) reverse_lazy()

로그인 제어와 리디렉션 URL 처리는 Django 프로젝트에서 보안성과 사용자 경험을 동시에 챙길 수 있는 중요한 포인트입니다 🔐