KR_Django - somaz94/python-study GitHub Wiki

Python Django ๊ฐœ๋… ์ •๋ฆฌ


1๏ธโƒฃ Django ๊ธฐ์ดˆ

Django๋Š” ํŒŒ์ด์ฌ์˜ ๋Œ€ํ‘œ์ ์ธ ์›น ํ”„๋ ˆ์ž„์›Œํฌ์ด๋‹ค.

# ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ
django-admin startproject myproject

# ์•ฑ ์ƒ์„ฑ
python manage.py startapp myapp

# ์„œ๋ฒ„ ์‹คํ–‰
python manage.py runserver

# models.py
from django.db import models

class User(models.Model):
    name = models.CharField(max_length=100)
    email = models.EmailField(unique=True)
    created_at = models.DateTimeField(auto_now_add=True)
    
    def __str__(self):
        return self.name
        
    class Meta:
        ordering = ['-created_at']
        verbose_name = '์‚ฌ์šฉ์ž'
        verbose_name_plural = '์‚ฌ์šฉ์ž ๋ชฉ๋ก'

โœ… ํŠน์ง•:

  • MVC ์•„ํ‚คํ…์ฒ˜ (Django์—์„œ๋Š” MTV ํŒจํ„ด)
  • ORM ์ง€์›
  • ๊ด€๋ฆฌ์ž ์ธํ„ฐํŽ˜์ด์Šค
  • ๊ฐ•๋ ฅํ•œ ํ…œํ”Œ๋ฆฟ ์‹œ์Šคํ…œ
  • ํผ ์ฒ˜๋ฆฌ ํ”„๋ ˆ์ž„์›Œํฌ

ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ:

myproject/
โ”œโ”€โ”€ manage.py              # ํ”„๋กœ์ ํŠธ ๊ด€๋ฆฌ ์Šคํฌ๋ฆฝํŠธ
โ”œโ”€โ”€ myproject/             # ํ”„๋กœ์ ํŠธ ์„ค์ • ํŒจํ‚ค์ง€
โ”‚   โ”œโ”€โ”€ __init__.py
โ”‚   โ”œโ”€โ”€ settings.py        # ํ”„๋กœ์ ํŠธ ์„ค์ •
โ”‚   โ”œโ”€โ”€ urls.py            # ๋ฃจํŠธ URL ์„ค์ •
โ”‚   โ”œโ”€โ”€ asgi.py            # ASGI ์„ค์ • (๋น„๋™๊ธฐ)
โ”‚   โ””โ”€โ”€ wsgi.py            # WSGI ์„ค์ • (๋ฐฐํฌ)
โ””โ”€โ”€ myapp/                 # ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ํŒจํ‚ค์ง€
    โ”œโ”€โ”€ __init__.py
    โ”œโ”€โ”€ admin.py           # ๊ด€๋ฆฌ์ž ์ธํ„ฐํŽ˜์ด์Šค
    โ”œโ”€โ”€ apps.py            # ์•ฑ ์„ค์ •
    โ”œโ”€โ”€ migrations/        # ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜
    โ”œโ”€โ”€ models.py          # ๋ฐ์ดํ„ฐ ๋ชจ๋ธ
    โ”œโ”€โ”€ tests.py           # ํ…Œ์ŠคํŠธ
    โ””โ”€โ”€ views.py           # ๋ทฐ ํ•จ์ˆ˜/ํด๋ž˜์Šค


2๏ธโƒฃ URL ํŒจํ„ด๊ณผ ๋ทฐ

Django์˜ URL ๋ผ์šฐํŒ… ์‹œ์Šคํ…œ๊ณผ ๋ทฐ ๊ตฌํ˜„ ๋ฐฉ๋ฒ•์ด๋‹ค.

# urls.py (ํ”„๋กœ์ ํŠธ ๋ ˆ๋ฒจ)
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include('api.urls')),
    path('', include('myapp.urls')),
]

# urls.py (์•ฑ ๋ ˆ๋ฒจ)
from django.urls import path
from . import views

app_name = 'myapp'  # URL ๋„ค์ž„์ŠคํŽ˜์ด์Šค

urlpatterns = [
    path('users/', views.user_list, name='user_list'),
    path('users/<int:pk>/', views.user_detail, name='user_detail'),
    path('users/create/', views.user_create, name='user_create'),
    path('users/<int:pk>/update/', views.user_update, name='user_update'),
    path('users/<int:pk>/delete/', views.user_delete, name='user_delete'),
]

# views.py (ํ•จ์ˆ˜ ๊ธฐ๋ฐ˜ ๋ทฐ)
from django.shortcuts import render, get_object_or_404, redirect
from .models import User
from .forms import UserForm

def user_list(request):
    users = User.objects.all()
    return render(request, 'myapp/user_list.html', {'users': users})

def user_detail(request, pk):
    user = get_object_or_404(User, pk=pk)
    return render(request, 'myapp/user_detail.html', {'user': user})

# views.py (ํด๋ž˜์Šค ๊ธฐ๋ฐ˜ ๋ทฐ)
from django.views.generic import ListView, DetailView, CreateView, UpdateView, DeleteView
from django.urls import reverse_lazy

class UserListView(ListView):
    model = User
    template_name = 'myapp/user_list.html'
    context_object_name = 'users'
    paginate_by = 10
    
class UserDetailView(DetailView):
    model = User
    template_name = 'myapp/user_detail.html'
    context_object_name = 'user'

โœ… ํŠน์ง•:

  • URL ํŒจํ„ด ๋งค์นญ
  • ํ•จ์ˆ˜ ๊ธฐ๋ฐ˜ ๋ทฐ์™€ ํด๋ž˜์Šค ๊ธฐ๋ฐ˜ ๋ทฐ
  • ํ…œํ”Œ๋ฆฟ ๋ Œ๋”๋ง
  • URL ๋„ค์ž„์ŠคํŽ˜์ด์Šค
  • ๋งค๊ฐœ๋ณ€์ˆ˜ ์บก์ฒ˜

ํ…œํ”Œ๋ฆฟ ์˜ˆ์ œ:

<!-- templates/myapp/user_list.html -->
{% extends 'base.html' %}

{% block content %}
  <h1>์‚ฌ์šฉ์ž ๋ชฉ๋ก</h1>
  <ul>
    {% for user in users %}
      <li>
        <a href="{% url 'myapp:user_detail' user.pk %}">
          {{ user.name }} ({{ user.email }})
        </a>
      </li>
    {% empty %}
      <li>๋“ฑ๋ก๋œ ์‚ฌ์šฉ์ž๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.</li>
    {% endfor %}
  </ul>
  
  <a href="{% url 'myapp:user_create' %}" class="btn btn-primary">
    ์ƒˆ ์‚ฌ์šฉ์ž ๋“ฑ๋ก
  </a>
  
  {# ํŽ˜์ด์ง€๋„ค์ด์…˜ #}
  {% if is_paginated %}
    <div class="pagination">
      {% if page_obj.has_previous %}
        <a href="?page={{ page_obj.previous_page_number }}">์ด์ „</a>
      {% endif %}
      
      <span class="current">
        {{ page_obj.number }} / {{ page_obj.paginator.num_pages }}
      </span>
      
      {% if page_obj.has_next %}
        <a href="?page={{ page_obj.next_page_number }}">๋‹ค์Œ</a>
      {% endif %}
    </div>
  {% endif %}
{% endblock %}


3๏ธโƒฃ ํผ๊ณผ ๋ชจ๋ธ ํผ

Django ํผ ์‹œ์Šคํ…œ์„ ์‚ฌ์šฉํ•œ ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ๊ณผ ์ฒ˜๋ฆฌ ๋ฐฉ๋ฒ•์ด๋‹ค.

# forms.py
from django import forms
from .models import User

class UserForm(forms.ModelForm):
    confirm_email = forms.EmailField(label='์ด๋ฉ”์ผ ํ™•์ธ')
    
    class Meta:
        model = User
        fields = ['name', 'email']
        widgets = {
            'name': forms.TextInput(attrs={'class': 'form-control'}),
            'email': forms.EmailInput(attrs={'class': 'form-control'}),
        }
        help_texts = {
            'email': '์œ ํšจํ•œ ์ด๋ฉ”์ผ ์ฃผ์†Œ๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”.',
        }
    
    def clean_email(self):
        email = self.cleaned_data['email']
        if User.objects.filter(email=email).exists():
            if self.instance.pk is None or self.instance.email != email:
                raise forms.ValidationError('์ด๋ฏธ ์‚ฌ์šฉ ์ค‘์ธ ์ด๋ฉ”์ผ์ž…๋‹ˆ๋‹ค.')
        return email
    
    def clean(self):
        cleaned_data = super().clean()
        email = cleaned_data.get('email')
        confirm_email = cleaned_data.get('confirm_email')
        
        if email and confirm_email and email != confirm_email:
            self.add_error('confirm_email', '์ด๋ฉ”์ผ ์ฃผ์†Œ๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.')
            
        return cleaned_data

# views.py (ํผ ์‚ฌ์šฉ)
def user_create(request):
    if request.method == 'POST':
        form = UserForm(request.POST)
        if form.is_valid():
            user = form.save()
            return redirect('myapp:user_detail', pk=user.pk)
    else:
        form = UserForm()
    
    return render(request, 'myapp/user_form.html', {'form': form})

โœ… ํŠน์ง•:

  • ํผ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ
  • CSRF ๋ณดํ˜ธ
  • ๋ชจ๋ธ ์—ฐ๋™
  • ์œ„์ ฏ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•
  • ํด๋ฆฐ ๋ฉ”์†Œ๋“œ ํ™œ์šฉ

ํ…œํ”Œ๋ฆฟ์—์„œ ํผ ๋ Œ๋”๋ง:

<!-- templates/myapp/user_form.html -->
{% extends 'base.html' %}

{% block content %}
  <h1>{{ user.pk|yesno:'์‚ฌ์šฉ์ž ์ˆ˜์ •,์ƒˆ ์‚ฌ์šฉ์ž ๋“ฑ๋ก' }}</h1>
  
  <form method="post" novalidate>
    {% csrf_token %}
    
    {% if form.non_field_errors %}
      <div class="alert alert-danger">
        {% for error in form.non_field_errors %}
          {{ error }}
        {% endfor %}
      </div>
    {% endif %}
    
    {% for field in form %}
      <div class="form-group">
        {{ field.label_tag }}
        {{ field }}
        
        {% if field.help_text %}
          <small class="form-text text-muted">
            {{ field.help_text }}
          </small>
        {% endif %}
        
        {% if field.errors %}
          <div class="invalid-feedback">
            {% for error in field.errors %}
              {{ error }}
            {% endfor %}
          </div>
        {% endif %}
      </div>
    {% endfor %}
    
    <button type="submit" class="btn btn-primary">์ €์žฅ</button>
    <a href="{% url 'myapp:user_list' %}" class="btn btn-secondary">์ทจ์†Œ</a>
  </form>
{% endblock %}

4๏ธโƒฃ ์ธ์ฆ๊ณผ ๊ถŒํ•œ

Django์˜ ์‚ฌ์šฉ์ž ์ธ์ฆ ์‹œ์Šคํ…œ๊ณผ ๊ถŒํ•œ ๊ด€๋ฆฌ ๋ฐฉ๋ฒ•์ด๋‹ค.

# settings.py
INSTALLED_APPS = [
    # ...
    'django.contrib.auth',
    'django.contrib.contenttypes',
    # ...
]

AUTH_USER_MODEL = 'myapp.CustomUser'

# models.py
from django.contrib.auth.models import AbstractUser, BaseUserManager, Permission
from django.db import models

class CustomUserManager(BaseUserManager):
    def create_user(self, email, password=None, **extra_fields):
        if not email:
            raise ValueError('์ด๋ฉ”์ผ์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค')
        email = self.normalize_email(email)
        user = self.model(email=email, **extra_fields)
        user.set_password(password)
        user.save(using=self._db)
        return user
        
    def create_superuser(self, email, password=None, **extra_fields):
        extra_fields.setdefault('is_staff', True)
        extra_fields.setdefault('is_superuser', True)
        
        return self.create_user(email, password, **extra_fields)

class CustomUser(AbstractUser):
    username = None  # ๊ธฐ๋ณธ username ํ•„๋“œ ์ œ๊ฑฐ
    email = models.EmailField('์ด๋ฉ”์ผ ์ฃผ์†Œ', unique=True)
    phone = models.CharField('์ „ํ™”๋ฒˆํ˜ธ', max_length=15, blank=True)
    
    USERNAME_FIELD = 'email'  # ๋กœ๊ทธ์ธ์— ์‚ฌ์šฉํ•  ํ•„๋“œ
    REQUIRED_FIELDS = []  # createsuperuser ๋ช…๋ น ์‹คํ–‰ ์‹œ ์š”๊ตฌํ•˜๋Š” ํ•„๋“œ
    
    objects = CustomUserManager()
    
    def __str__(self):
        return self.email

# views.py (๋กœ๊ทธ์ธ๊ณผ ๋กœ๊ทธ์•„์›ƒ)
from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.decorators import login_required
from django.shortcuts import render, redirect

def login_view(request):
    if request.method == 'POST':
        email = request.POST.get('email')
        password = request.POST.get('password')
        user = authenticate(request, email=email, password=password)
        
        if user is not None:
            login(request, user)
            return redirect('home')
        else:
            error_message = '์ด๋ฉ”์ผ ๋˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค'
    
    return render(request, 'myapp/login.html', locals())

@login_required
def logout_view(request):
    logout(request)
    return redirect('login')

โœ… ํŠน์ง•:

  • ์‚ฌ์šฉ์ž ์ธ์ฆ ์‹œ์Šคํ…œ
  • ์ปค์Šคํ…€ ์‚ฌ์šฉ์ž ๋ชจ๋ธ
  • ๊ถŒํ•œ ๊ด€๋ฆฌ
  • ์„ธ์…˜ ์ฒ˜๋ฆฌ
  • ๋น„๋ฐ€๋ฒˆํ˜ธ ํ•ด์‹ฑ

๊ถŒํ•œ๊ณผ ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ:

# permissions.py
from django.contrib.auth.mixins import UserPassesTestMixin, LoginRequiredMixin
from django.core.exceptions import PermissionDenied

class StaffRequiredMixin(UserPassesTestMixin):
    def test_func(self):
        return self.request.user.is_staff

# ํ•จ์ˆ˜ ๊ธฐ๋ฐ˜ ๋ทฐ์—์„œ ๊ถŒํ•œ ํ™•์ธ
from django.contrib.auth.decorators import login_required, permission_required

@login_required
def profile_view(request):
    return render(request, 'myapp/profile.html')

@permission_required('myapp.change_user')
def user_update(request, pk):
    # ์‚ฌ์šฉ์ž ์—…๋ฐ์ดํŠธ ๋กœ์ง
    pass

# ํด๋ž˜์Šค ๊ธฐ๋ฐ˜ ๋ทฐ์—์„œ ๊ถŒํ•œ ํ™•์ธ
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin

class UserUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
    model = User
    form_class = UserForm
    template_name = 'myapp/user_form.html'
    permission_required = 'myapp.change_user'
    
    def get_success_url(self):
        return reverse_lazy('myapp:user_detail', kwargs={'pk': self.object.pk})


5๏ธโƒฃ REST API์™€ ์บ์‹œ

Django์—์„œ RESTful API๋ฅผ ๊ตฌํ˜„ํ•˜๊ณ  ์„ฑ๋Šฅ์„ ์ตœ์ ํ™”ํ•˜๋Š” ๋ฐฉ๋ฒ•์ด๋‹ค.

# settings.py
INSTALLED_APPS = [
    # ...
    'rest_framework',
    'django.contrib.staticfiles',
]

# REST Framework ์„ค์ •
REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.IsAuthenticated',
    ],
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework.authentication.SessionAuthentication',
        'rest_framework.authentication.TokenAuthentication',
    ],
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
    'PAGE_SIZE': 10,
}

# serializers.py
from rest_framework import serializers
from .models import User

class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ['id', 'name', 'email', 'created_at']
        read_only_fields = ['created_at']
        
    def validate_email(self, value):
        if User.objects.filter(email=value).exists():
            if self.instance is None or self.instance.email != value:
                raise serializers.ValidationError('์ด๋ฏธ ์‚ฌ์šฉ ์ค‘์ธ ์ด๋ฉ”์ผ์ž…๋‹ˆ๋‹ค.')
        return value

# views.py (API ๋ทฐ)
from rest_framework import viewsets, permissions, filters
from rest_framework.decorators import action
from rest_framework.response import Response
from django.core.cache import cache
from django.views.decorators.cache import cache_page
from django.utils.decorators import method_decorator

class UserViewSet(viewsets.ModelViewSet):
    queryset = User.objects.all()
    serializer_class = UserSerializer
    permission_classes = [permissions.IsAuthenticated]
    filter_backends = [filters.SearchFilter, filters.OrderingFilter]
    search_fields = ['name', 'email']
    ordering_fields = ['name', 'created_at']
    
    @method_decorator(cache_page(60 * 15))  # 15๋ถ„ ์บ์‹œ
    def list(self, request, *args, **kwargs):
        return super().list(request, *args, **kwargs)
    
    @action(detail=True, methods=['get'])
    def profile(self, request, pk=None):
        user = self.get_object()
        data = {
            'id': user.id,
            'name': user.name,
            'email': user.email,
            'created_at': user.created_at,
        }
        return Response(data)

# urls.py (API URL)
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from . import views

router = DefaultRouter()
router.register(r'users', views.UserViewSet)

urlpatterns = [
    path('', include(router.urls)),
    path('auth/', include('rest_framework.urls')),
]

โœ… ํŠน์ง•:

  • RESTful API ์„ค๊ณ„
  • ์ง๋ ฌํ™” ๋ฐ ์—ญ์ง๋ ฌํ™”
  • ์บ์‹œ ์‹œ์Šคํ…œ
  • ์„ฑ๋Šฅ ์ตœ์ ํ™”
  • API ๊ถŒํ•œ ๊ด€๋ฆฌ

์บ์‹œ ํ™œ์šฉ ์˜ˆ์ œ:

# ์ €์ˆ˜์ค€ ์บ์‹œ API
from django.core.cache import cache

def get_active_users():
    # ์บ์‹œ์—์„œ ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ ์‹œ๋„
    active_users = cache.get('active_users')
    
    if active_users is None:
        # ์บ์‹œ์— ์—†์œผ๋ฉด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ๊ฐ€์ ธ์™€ ์บ์‹œ์— ์ €์žฅ
        active_users = User.objects.filter(is_active=True)
        cache.set('active_users', active_users, 60 * 5)  # 5๋ถ„๊ฐ„ ์บ์‹œ
    
    return active_users

# ์บ์‹œ ํ…œํ”Œ๋ฆฟ ์กฐ๊ฐ
from django.core.cache import cache
from django.template.loader import render_to_string

def render_user_list():
    # ์บ์‹œ๋œ HTML ์กฐ๊ฐ ๊ฐ€์ ธ์˜ค๊ธฐ ์‹œ๋„
    fragment = cache.get('user_list_html')
    
    if fragment is None:
        users = User.objects.all()[:10]
        fragment = render_to_string('fragments/user_list.html', {'users': users})
        cache.set('user_list_html', fragment, 60 * 10)  # 10๋ถ„๊ฐ„ ์บ์‹œ
    
    return fragment

# ๋ทฐ ๋ ˆ๋ฒจ ์บ์‹œ
from django.views.decorators.cache import cache_page

@cache_page(60 * 15)  # 15๋ถ„ ์บ์‹œ
def cached_view(request):
    return render(request, 'cached_template.html')

6๏ธโƒฃ ๋น„๋™๊ธฐ ์ž‘์—…๊ณผ ๊ด€๋ฆฌ์ž

Django์—์„œ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ž‘์—…์„ ์ฒ˜๋ฆฌํ•˜๊ณ  ๊ด€๋ฆฌ์ž ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌ์„ฑํ•˜๋Š” ๋ฐฉ๋ฒ•์ด๋‹ค.

# ๋น„๋™๊ธฐ ์ž‘์—… ์„ค์ • (Celery)
# celery.py
import os
from celery import Celery

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')

app = Celery('myproject')
app.config_from_object('django.conf:settings', namespace='CELERY')
app.autodiscover_tasks()

# tasks.py
from celery import shared_task
from django.core.mail import send_mail
from django.template.loader import render_to_string

@shared_task
def send_notification_email(user_id):
    from .models import User  # ์ˆœํ™˜ ์ฐธ์กฐ ๋ฐฉ์ง€๋ฅผ ์œ„ํ•ด ๋‚ด๋ถ€์—์„œ ์ž„ํฌํŠธ
    user = User.objects.get(id=user_id)
    
    context = {
        'user': user,
        'site_name': 'My Django Site',
    }
    
    html_message = render_to_string('emails/notification.html', context)
    plain_message = render_to_string('emails/notification.txt', context)
    
    send_mail(
        subject='์ƒˆ๋กœ์šด ์•Œ๋ฆผ',
        message=plain_message,
        from_email='[email protected]',
        recipient_list=[user.email],
        html_message=html_message,
        fail_silently=False,
    )
    
    return f"Notification sent to {user.email}"

# ๊ด€๋ฆฌ์ž ์ธํ„ฐํŽ˜์ด์Šค ์„ค์ •
# admin.py
from django.contrib import admin
from django.utils.html import format_html
from .models import User

@admin.register(User)
class UserAdmin(admin.ModelAdmin):
    list_display = ['name', 'email', 'created_at', 'actions_buttons']
    list_filter = ['created_at', 'is_active']
    search_fields = ['name', 'email']
    readonly_fields = ['created_at']
    fieldsets = [
        (None, {'fields': ['name', 'email']}),
        ('์ถ”๊ฐ€ ์ •๋ณด', {'fields': ['created_at'], 'classes': ['collapse']}),
    ]
    
    def actions_buttons(self, obj):
        return format_html(
            '<a class="button" href="{}">๋ณด๊ธฐ</a> '
            '<a class="button" onclick="return confirm(\'์ •๋ง ์ด๋ฉ”์ผ์„ ๋ฐœ์†กํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?\');" href="{}">์ด๋ฉ”์ผ ๋ฐœ์†ก</a>',
            f'/admin/users/{obj.id}/view/',
            f'/admin/users/{obj.id}/send_email/'
        )
    actions_buttons.short_description = '์ž‘์—…'
    
    def send_notification(self, request, queryset):
        for user in queryset:
            send_notification_email.delay(user.id)
        self.message_user(request, f"{queryset.count()}๋ช…์˜ ์‚ฌ์šฉ์ž์—๊ฒŒ ์•Œ๋ฆผ์ด ๋ฐœ์†ก๋˜์—ˆ์Šต๋‹ˆ๋‹ค.")
    send_notification.short_description = "์„ ํƒํ•œ ์‚ฌ์šฉ์ž์—๊ฒŒ ์•Œ๋ฆผ ๋ฐœ์†ก"
    
    actions = ['send_notification']

โœ… ํŠน์ง•:

  • ๋น„๋™๊ธฐ ์ž‘์—… ์ฒ˜๋ฆฌ
  • ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ž‘์—… ํ
  • ๊ด€๋ฆฌ์ž ์ธํ„ฐํŽ˜์ด์Šค ์ปค์Šคํ„ฐ๋งˆ์ด์ง•
  • ์ด๋ฉ”์ผ ํ…œํ”Œ๋ฆฟ ๋ Œ๋”๋ง
  • ๊ด€๋ฆฌ์ž ์•ก์…˜ ์ •์˜

์‚ฌ์šฉ์ž ์ •์˜ ๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€:

# admin_views.py
from django.contrib.admin.views.decorators import staff_member_required
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.urls import path
from django.contrib import messages

from .models import User
from .tasks import send_notification_email

@staff_member_required
def send_user_email(request, user_id):
    user = get_object_or_404(User, id=user_id)
    task = send_notification_email.delay(user.id)
    messages.success(request, f"{user.email}์—๊ฒŒ ์•Œ๋ฆผ ์ด๋ฉ”์ผ์„ ๋ฐœ์†ก ์ค‘์ž…๋‹ˆ๋‹ค. ์ž‘์—… ID: {task.id}")
    return HttpResponseRedirect(request.META.get('HTTP_REFERER', '/admin/'))

# ๊ด€๋ฆฌ์ž URL ํ™•์žฅ
class UserAdmin(admin.ModelAdmin):
    # ... ๊ธฐ์กด ์ฝ”๋“œ ...
    
    def get_urls(self):
        urls = super().get_urls()
        custom_urls = [
            path(
                '<int:user_id>/send_email/',
                self.admin_site.admin_view(send_user_email),
                name='user-send-email',
            ),
        ]
        return custom_urls + urls


7๏ธโƒฃ ํ…Œ์ŠคํŠธ์™€ ๋ฐฐํฌ

Django ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ํ…Œ์ŠคํŠธ์™€ ๋ฐฐํฌ ๋ฐฉ๋ฒ•์ด๋‹ค.

ํ…Œ์ŠคํŠธ ์ž‘์„ฑ:

# tests.py
from django.test import TestCase, Client
from django.urls import reverse
from .models import User

class UserModelTest(TestCase):
    def setUp(self):
        self.user = User.objects.create(
            name="ํ…Œ์ŠคํŠธ ์‚ฌ์šฉ์ž",
            email="[email protected]"
        )
    
    def test_user_creation(self):
        self.assertEqual(self.user.name, "ํ…Œ์ŠคํŠธ ์‚ฌ์šฉ์ž")
        self.assertEqual(self.user.email, "[email protected]")
        self.assertTrue(isinstance(self.user, User))
    
    def test_str_representation(self):
        self.assertEqual(str(self.user), self.user.name)

class UserViewTest(TestCase):
    def setUp(self):
        self.client = Client()
        self.user = User.objects.create(
            name="ํ…Œ์ŠคํŠธ ์‚ฌ์šฉ์ž",
            email="[email protected]"
        )
        self.list_url = reverse('myapp:user_list')
        self.detail_url = reverse('myapp:user_detail', args=[self.user.id])
    
    def test_user_list_view(self):
        response = self.client.get(self.list_url)
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, "ํ…Œ์ŠคํŠธ ์‚ฌ์šฉ์ž")
        self.assertTemplateUsed(response, 'myapp/user_list.html')
    
    def test_user_detail_view(self):
        response = self.client.get(self.detail_url)
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, "[email protected]")
        self.assertTemplateUsed(response, 'myapp/user_detail.html')

# ํผ ํ…Œ์ŠคํŠธ
class UserFormTest(TestCase):
    def test_valid_form(self):
        data = {'name': '์ƒˆ ์‚ฌ์šฉ์ž', 'email': '[email protected]', 'confirm_email': '[email protected]'}
        form = UserForm(data=data)
        self.assertTrue(form.is_valid())
    
    def test_invalid_email(self):
        # ์ด๋ฉ”์ผ ๋ถˆ์ผ์น˜ ํ…Œ์ŠคํŠธ
        data = {'name': '์ƒˆ ์‚ฌ์šฉ์ž', 'email': '[email protected]', 'confirm_email': '[email protected]'}
        form = UserForm(data=data)
        self.assertFalse(form.is_valid())
        self.assertIn('confirm_email', form.errors)

๋ฐฐํฌ ์„ค์ •:

# settings/production.py
from .base import *

DEBUG = False
ALLOWED_HOSTS = ['www.example.com', 'example.com']

# ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค์ •
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': os.environ.get('DB_NAME'),
        'USER': os.environ.get('DB_USER'),
        'PASSWORD': os.environ.get('DB_PASSWORD'),
        'HOST': os.environ.get('DB_HOST'),
        'PORT': os.environ.get('DB_PORT', '5432'),
    }
}

# ๋ณด์•ˆ ์„ค์ •
SECURE_HSTS_SECONDS = 31536000  # 1๋…„
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
X_FRAME_OPTIONS = 'DENY'

# ์บ์‹œ ์„ค์ •
CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.redis.RedisCache',
        'LOCATION': f"redis://{os.environ.get('REDIS_HOST', 'localhost')}:6379/1",
    }
}

# ์ •์  ํŒŒ์ผ ์ฒ˜๋ฆฌ
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'

# ๋ฏธ๋””์–ด ํŒŒ์ผ ์ฒ˜๋ฆฌ
DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'
AWS_STORAGE_BUCKET_NAME = os.environ.get('AWS_STORAGE_BUCKET_NAME')
AWS_S3_REGION_NAME = os.environ.get('AWS_S3_REGION_NAME')
AWS_S3_CUSTOM_DOMAIN = f"{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com"

Docker ๋ฐฐํฌ:

# Dockerfile
FROM python:3.9-slim

ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

# ๋ฐฐํฌ ์ „ ์Šคํฌ๋ฆฝํŠธ ์‹คํ–‰
RUN python manage.py collectstatic --noinput
RUN python manage.py compress --force

EXPOSE 8000

CMD ["gunicorn", "--bind", "0.0.0.0:8000", "myproject.wsgi:application"]
# docker-compose.yml
version: '3.8'

services:
  web:
    build: .
    restart: always
    env_file:
      - .env
    volumes:
      - static_data:/app/staticfiles
    depends_on:
      - db
      - redis
    networks:
      - app_network

  db:
    image: postgres:13
    volumes:
      - postgres_data:/var/lib/postgresql/data/
    env_file:
      - .env
    networks:
      - app_network

  redis:
    image: redis:6-alpine
    volumes:
      - redis_data:/data
    networks:
      - app_network

  nginx:
    image: nginx:latest
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d
      - ./nginx/ssl:/etc/nginx/ssl
      - static_data:/var/www/static
    depends_on:
      - web
    networks:
      - app_network

  celery:
    build: .
    command: celery -A myproject worker -l INFO
    env_file:
      - .env
    depends_on:
      - db
      - redis
    networks:
      - app_network

volumes:
  postgres_data:
  redis_data:
  static_data:

networks:
  app_network:

โœ… ํŠน์ง•:

  • ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ
  • ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ๊ตฌํ˜„
  • ํ”„๋กœ๋•์…˜ ์„ค์ • ๋ถ„๋ฆฌ
  • Docker ์ปจํ…Œ์ด๋„ˆํ™”
  • ์ž๋™ํ™”๋œ ๋ฐฐํฌ ํŒŒ์ดํ”„๋ผ์ธ


์ฃผ์š” ํŒ

โœ… ๋ชจ๋ฒ” ์‚ฌ๋ก€:

  • ORM ์ตœ์ ํ™”: select_related์™€ prefetch_related๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ฟผ๋ฆฌ ์ˆ˜ ์ค„์ด๊ธฐ
  • ์บ์‹ฑ ์ „๋žต: ์ ์ ˆํ•œ ๋ ˆ๋ฒจ์—์„œ ์บ์‹œ ์ ์šฉ (๋ทฐ, ํ…œํ”Œ๋ฆฟ, DB ์ฟผ๋ฆฌ)
  • ๋ณด์•ˆ ์„ค์ •: HTTPS ๊ฐ•์ œ, CSRF ๋ณดํ˜ธ, XSS ๋ฐฉ์ง€ ์„ค์ • ์ ์šฉ
  • ํ…Œ์ŠคํŠธ ์ž‘์„ฑ: ๋ชจ๋ธ, ๋ทฐ, ํผ, API์— ๋Œ€ํ•œ ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค ๊ตฌํ˜„
  • ์„ฑ๋Šฅ ๋ชจ๋‹ˆํ„ฐ๋ง: Django Debug Toolbar, django-silk ๋“ฑ์˜ ๋„๊ตฌ ํ™œ์šฉ
  • ๋ฐฐํฌ ์ „๋žต: ํ™˜๊ฒฝ๋ณ„ ์„ค์ • ๋ถ„๋ฆฌ, CI/CD ํŒŒ์ดํ”„๋ผ์ธ ๊ตฌ์ถ•
  • ๋กœ๊น… ์„ค์ •: ๊ตฌ์กฐํ™”๋œ ๋กœ๊ทธ ํ˜•์‹๊ณผ ์ ์ ˆํ•œ ๋กœ๊ทธ ๋ ˆ๋ฒจ ์‚ฌ์šฉ
  • ๋ฌธ์„œํ™”: ์ฝ”๋“œ ์ฃผ์„๊ณผ API ๋ฌธ์„œ ์ž‘์„ฑ
  • ๋ฏธ๋“ค์›จ์–ด ์ตœ์ ํ™”: ๋ถˆํ•„์š”ํ•œ ๋ฏธ๋“ค์›จ์–ด ์ œ๊ฑฐ, ์ปค์Šคํ…€ ๋ฏธ๋“ค์›จ์–ด ์ž‘์„ฑ
  • ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๊ด€๋ฆฌ: ์„ค์ •๊ฐ’์„ ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋กœ ๋ถ„๋ฆฌํ•˜์—ฌ ๋ณด์•ˆ ๊ฐ•ํ™”
  • ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๊ด€๋ฆฌ: ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํŒŒ์ผ ์ฃผ๊ธฐ์  ์ •๋ฆฌ, ์ถฉ๋Œ ๋ฐฉ์ง€
  • ์ •์  ํŒŒ์ผ ์ตœ์ ํ™”: ๋ณ‘ํ•ฉ, ์••์ถ•, CDN ํ™œ์šฉ์œผ๋กœ ์„ฑ๋Šฅ ํ–ฅ์ƒ
  • ๋น„๋™๊ธฐ ์ž‘์—… ์ „๋žต: ์‚ฌ์šฉ์ž ์‘๋‹ต์— ์˜ํ–ฅ์„ ์ฃผ๋Š” ์ž‘์—…์€ ๋ฐฑ๊ทธ๋ผ์šด๋“œ๋กœ ์ฒ˜๋ฆฌ
  • ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ธ๋ฑ์‹ฑ: ์ž์ฃผ ์กฐํšŒํ•˜๋Š” ํ•„๋“œ์— ์ธ๋ฑ์Šค ์„ค์ •
  • ํผ ๊ฒ€์ฆ ๊ฐ•ํ™”: ์„œ๋ฒ„ ์ธก ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ์™€ ํด๋ผ์ด์–ธํŠธ ์ธก ๊ฒ€์ฆ ๋ณ‘ํ–‰


โš ๏ธ **GitHub.com Fallback** โš ๏ธ