Skip to content

Django Admin 인터페이스

Django의 가장 강력한 기능 중 하나는 자동 생성되는 관리자 인터페이스입니다. FastAPI에서는 별도의 관리자 패널을 직접 구현해야 하지만, Django는 모델을 등록하기만 하면 즉시 사용 가능한 관리자 인터페이스를 제공합니다.

1. Django Admin이란?

Django Admin은 모델 데이터를 관리할 수 있는 웹 기반 인터페이스입니다. 데이터 생성, 조회, 수정, 삭제(CRUD) 기능을 자동으로 제공합니다.

FastAPI vs Django Admin

FastAPI: - 관리자 패널을 직접 구현해야 함 - FastAPI-Admin, SQLAdmin 같은 서드파티 패키지 필요 - 커스터마이징은 자유롭지만 초기 구축이 필요

Django: - 프레임워크에 내장된 기능 - 모델 등록만으로 즉시 사용 가능 - 풍부한 커스터마이징 옵션 제공

2. Admin 활성화 및 접속

관리자 계정 생성

python manage.py createsuperuser

# 대화형 프롬프트
Username (leave blank to use 'your_username'): admin
Email address: admin@example.com
Password: ********
Password (again): ********
Superuser created successfully.

Admin 접속

http://127.0.0.1:8000/admin/

3. 모델을 Admin에 등록하기

기본 등록

# blog/admin.py
from django.contrib import admin
from .models import Post, Category, Tag

# 방법 1: 단순 등록
admin.site.register(Post)
admin.site.register(Category)
admin.site.register(Tag)

커스텀 Admin 클래스

# blog/admin.py
from django.contrib import admin
from .models import Post, Category, Tag, Comment

# 방법 2: ModelAdmin 클래스 사용
class PostAdmin(admin.ModelAdmin):
    list_display = ['title', 'author', 'status', 'created_at']
    list_filter = ['status', 'created_at', 'author']
    search_fields = ['title', 'content']
    prepopulated_fields = {'slug': ('title',)}
    date_hierarchy = 'created_at'
    ordering = ['-created_at']

admin.site.register(Post, PostAdmin)

# 방법 3: 데코레이터 사용 (권장)
@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
    list_display = ['name', 'post_count']
    prepopulated_fields = {'slug': ('name',)}

    def post_count(self, obj):
        return obj.posts.count()
    post_count.short_description = '포스트 수'

4. ModelAdmin 옵션 상세

list_display - 목록 페이지 표시 필드

@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    list_display = ['title', 'author', 'status', 'created_at', 'view_count', 'is_featured']

    # 커스텀 필드 추가
    def is_featured(self, obj):
        return obj.view_count > 100
    is_featured.boolean = True  # 체크/X 아이콘으로 표시
    is_featured.short_description = '인기글'

list_filter - 필터 옵션

class PostAdmin(admin.ModelAdmin):
    list_filter = [
        'status',
        'created_at',
        ('author', admin.RelatedOnlyFieldListFilter),  # 관련 필터
    ]

    # 커스텀 필터
    class PublishedFilter(admin.SimpleListFilter):
        title = '공개 상태'
        parameter_name = 'published'

        def lookups(self, request, model_admin):
            return [
                ('yes', '공개됨'),
                ('no', '비공개'),
            ]

        def queryset(self, request, queryset):
            if self.value() == 'yes':
                return queryset.filter(status='published')
            if self.value() == 'no':
                return queryset.exclude(status='published')

    list_filter = ['status', PublishedFilter]

search_fields - 검색 기능

class PostAdmin(admin.ModelAdmin):
    search_fields = [
        'title',  # 제목 검색
        'content',  # 내용 검색
        'author__username',  # 작성자 이름으로 검색
        '=id',  # ID 정확히 일치
        '^title',  # 제목 시작 부분 일치
    ]

인라인 편집

class CommentInline(admin.TabularInline):  # 또는 StackedInline
    model = Comment
    extra = 1  # 빈 폼 개수
    fields = ['author', 'content', 'is_approved']

class PostAdmin(admin.ModelAdmin):
    inlines = [CommentInline]

    # 인라인에서 ManyToMany 관계
    class TagInline(admin.TabularInline):
        model = Post.tags.through
        extra = 1

fieldsets - 필드 그룹화

class PostAdmin(admin.ModelAdmin):
    fieldsets = [
        ('기본 정보', {
            'fields': ['title', 'slug', 'author', 'category']
        }),
        ('내용', {
            'fields': ['content', 'excerpt']
        }),
        ('상태', {
            'fields': ['status', 'published_at'],
            'classes': ['collapse']  # 접기/펼치기
        }),
        ('메타데이터', {
            'fields': ['tags', 'view_count'],
            'description': '추가 정보를 입력하세요.'
        })
    ]

readonly_fields - 읽기 전용 필드

class PostAdmin(admin.ModelAdmin):
    readonly_fields = ['created_at', 'updated_at', 'view_count', 'get_absolute_url']

    def get_absolute_url(self, obj):
        from django.utils.html import format_html
        return format_html('<a href="{}" target="_blank">포스트 보기</a>', obj.get_absolute_url())
    get_absolute_url.short_description = 'URL'

5. 액션 추가하기

@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    actions = ['make_published', 'make_draft', 'export_as_csv']

    def make_published(self, request, queryset):
        updated = queryset.update(status='published')
        self.message_user(request, f'{updated}개의 포스트를 공개했습니다.')
    make_published.short_description = '선택한 포스트를 공개 상태로 변경'

    def make_draft(self, request, queryset):
        queryset.update(status='draft')
    make_draft.short_description = '선택한 포스트를 초안으로 변경'

    def export_as_csv(self, request, queryset):
        import csv
        from django.http import HttpResponse

        response = HttpResponse(content_type='text/csv')
        response['Content-Disposition'] = 'attachment; filename="posts.csv"'

        writer = csv.writer(response)
        writer.writerow(['Title', 'Author', 'Status', 'Created'])

        for post in queryset:
            writer.writerow([
                post.title,
                post.author.username,
                post.status,
                post.created_at
            ])

        return response
    export_as_csv.short_description = 'CSV로 내보내기'

6. 권한 관리

class PostAdmin(admin.ModelAdmin):
    def get_queryset(self, request):
        qs = super().get_queryset(request)
        # 자신의 포스트만 보기
        if not request.user.is_superuser:
            return qs.filter(author=request.user)
        return qs

    def has_change_permission(self, request, obj=None):
        # 자신의 포스트만 수정 가능
        if obj and not request.user.is_superuser:
            return obj.author == request.user
        return super().has_change_permission(request, obj)

    def save_model(self, request, obj, form, change):
        # 새 포스트 작성시 자동으로 작성자 설정
        if not change:  # 새로 생성하는 경우
            obj.author = request.user
        super().save_model(request, obj, form, change)

7. Admin 사이트 커스터마이징

사이트 헤더와 타이틀 변경

# blog/admin.py 또는 urls.py
admin.site.site_header = "My Blog 관리자"
admin.site.site_title = "My Blog Admin"
admin.site.index_title = "환영합니다"

커스텀 템플릿

templates/
└── admin/
    ├── base_site.html      # 전체 레이아웃
    └── blog/              # 앱별 커스터마이징
        └── post/
            └── change_list.html  # 목록 페이지
<!-- templates/admin/base_site.html -->
{% extends "admin/base.html" %}

{% block title %}{{ title }} | My Blog Admin{% endblock %}

{% block branding %}
<h1 id="site-name">
    <a href="{% url 'admin:index' %}">
        <img src="{% static 'images/logo.png' %}" alt="Logo" height="40">
        My Blog Admin
    </a>
</h1>
{% endblock %}

{% block nav-global %}{% endblock %}

8. 고급 기능

자동완성 필드

class PostAdmin(admin.ModelAdmin):
    autocomplete_fields = ['author', 'tags']

    # 관련 모델에서 search_fields 필요
class UserAdmin(admin.ModelAdmin):
    search_fields = ['username', 'email']

커스텀 폼 사용

from django import forms

class PostAdminForm(forms.ModelForm):
    class Meta:
        model = Post
        fields = '__all__'
        widgets = {
            'content': forms.Textarea(attrs={'class': 'markdown-editor'}),
        }

    def clean_title(self):
        title = self.cleaned_data['title']
        if len(title) < 5:
            raise forms.ValidationError('제목은 5자 이상이어야 합니다.')
        return title

class PostAdmin(admin.ModelAdmin):
    form = PostAdminForm

커스텀 뷰 추가

from django.urls import path
from django.shortcuts import render

class PostAdmin(admin.ModelAdmin):
    def get_urls(self):
        urls = super().get_urls()
        custom_urls = [
            path('statistics/', self.admin_site.admin_view(self.statistics_view), name='post_statistics'),
        ]
        return custom_urls + urls

    def statistics_view(self, request):
        context = {
            'total_posts': Post.objects.count(),
            'published_posts': Post.objects.filter(status='published').count(),
            'title': '포스트 통계',
        }
        return render(request, 'admin/blog/post/statistics.html', context)

9. 실용적인 Admin 예제

# blog/admin.py
from django.contrib import admin
from django.utils.html import format_html
from django.db.models import Count
from .models import Post, Category, Tag, Comment

@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    list_display = ['title', 'author_link', 'category', 'status_colored', 'created_at', 'comment_count']
    list_filter = ['status', 'created_at', 'category', 'tags']
    search_fields = ['title', 'content']
    prepopulated_fields = {'slug': ('title',)}
    filter_horizontal = ['tags']
    date_hierarchy = 'created_at'

    fieldsets = [
        ('기본 정보', {
            'fields': ['title', 'slug', 'author', 'category', 'tags']
        }),
        ('내용', {
            'fields': ['content', 'excerpt']
        }),
        ('발행', {
            'fields': ['status', 'published_at'],
        }),
        ('통계', {
            'fields': ['view_count'],
            'classes': ['collapse']
        })
    ]

    def author_link(self, obj):
        url = f"/admin/auth/user/{obj.author.pk}/change/"
        return format_html('<a href="{}">{}</a>', url, obj.author.username)
    author_link.short_description = '작성자'

    def status_colored(self, obj):
        colors = {
            'draft': 'gray',
            'published': 'green',
        }
        return format_html(
            '<span style="color: {};">{}</span>',
            colors.get(obj.status, 'black'),
            obj.get_status_display()
        )
    status_colored.short_description = '상태'

    def comment_count(self, obj):
        return obj.comments.count()
    comment_count.short_description = '댓글'

    def get_queryset(self, request):
        return super().get_queryset(request).annotate(
            comment_count=Count('comments')
        )

@admin.register(Comment)
class CommentAdmin(admin.ModelAdmin):
    list_display = ['post', 'author', 'content_short', 'created_at', 'is_approved']
    list_filter = ['is_approved', 'created_at']
    search_fields = ['content', 'author__username', 'post__title']
    actions = ['approve_comments', 'reject_comments']

    def content_short(self, obj):
        return obj.content[:50] + '...' if len(obj.content) > 50 else obj.content
    content_short.short_description = '내용'

    def approve_comments(self, request, queryset):
        queryset.update(is_approved=True)
    approve_comments.short_description = '선택한 댓글 승인'

    def reject_comments(self, request, queryset):
        queryset.update(is_approved=False)
    reject_comments.short_description = '선택한 댓글 거부'

10. Admin 보안 설정

URL 변경

# urls.py
from django.urls import path
from django.contrib import admin

urlpatterns = [
    path('secret-admin/', admin.site.urls),  # 기본 /admin/ 대신
]

2단계 인증 추가 (django-otp)

pip install django-otp qrcode
# settings.py
INSTALLED_APPS = [
    # ...
    'django_otp',
    'django_otp.plugins.otp_totp',
]

MIDDLEWARE = [
    # ...
    'django_otp.middleware.OTPMiddleware',
]

정리

Django Admin의 장점: - 자동 생성: 모델 정의만으로 관리 인터페이스 생성 - 커스터마이징: 다양한 옵션으로 세밀한 조정 가능 - 보안: 권한 시스템과 통합 - 확장성: 커스텀 액션, 뷰, 템플릿 추가 가능 - 생산성: 개발 초기부터 데이터 관리 가능

FastAPI와 비교: - Django는 관리자 인터페이스가 내장됨 - 별도의 프론트엔드 개발 없이 즉시 사용 - 모델과 자동으로 동기화 - 권한 관리가 프레임워크와 통합

Django Admin은 개발 생산성을 크게 향상시키는 강력한 도구입니다. 다음 장에서는 사용자가 보는 화면을 위한 CRUD 구현을 알아보겠습니다!

Comments