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 접속
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)
# settings.py
INSTALLED_APPS = [
# ...
'django_otp',
'django_otp.plugins.otp_totp',
]
MIDDLEWARE = [
# ...
'django_otp.middleware.OTPMiddleware',
]
정리
Django Admin의 장점: - 자동 생성: 모델 정의만으로 관리 인터페이스 생성 - 커스터마이징: 다양한 옵션으로 세밀한 조정 가능 - 보안: 권한 시스템과 통합 - 확장성: 커스텀 액션, 뷰, 템플릿 추가 가능 - 생산성: 개발 초기부터 데이터 관리 가능
FastAPI와 비교: - Django는 관리자 인터페이스가 내장됨 - 별도의 프론트엔드 개발 없이 즉시 사용 - 모델과 자동으로 동기화 - 권한 관리가 프레임워크와 통합
Django Admin은 개발 생산성을 크게 향상시키는 강력한 도구입니다. 다음 장에서는 사용자가 보는 화면을 위한 CRUD 구현을 알아보겠습니다!