과제
📌 Django Todo 프로젝트: 이미지 업로드 + 썸네일 생성 + Summernote 적용 전체 흐름 정리
Chansman
2025. 5. 12. 18:19
📌 Django Todo 프로젝트: 이미지 업로드 + 썸네일 생성 + Summernote 적용 전체 흐름 정리
1️⃣ 라이브러리 설치 및 설정
poetry add django-summernote
poetry add pillow
poetry add django-cleanup
🔧 settings.py 설정
- INSTALLED_APPS에 django_summernote, django_cleanup 추가
- MEDIA_URL, MEDIA_ROOT 설정
- SUMMERNOTE_CONFIG 설정으로 iframe, 툴바, 보안 세팅 포함
✅ pillow는 Django 앱이 아니므로 INSTALLED_APPS에 추가하지 않음
2️⃣ models.py 수정 - ImageField, thumbnail 생성 및 썸네일 저장 처리
from PIL import Image
from io import BytesIO
from pathlib import Path
class Todo(models.Model):
...
completed_image = models.ImageField(upload_to='todo/completed_images', null=True, blank=True)
thumbnail = models.ImageField(upload_to='todo/thumbnails', default='todo/no_image/NO-IMAGE.gif', null=True, blank=True)
def save(self, *args, **kwargs):
if not self.completed_image:
return super().save(*args, **kwargs)
image = Image.open(self.completed_image)
image.thumbnail((100, 100))
image_path = Path(self.completed_image.name)
thumbnail_filename = f"{image_path.stem}_thumbnail{image_path.suffix}"
file_type = 'JPEG' if image_path.suffix in ['.jpg', '.jpeg'] else 'PNG' if image_path.suffix == '.png' else 'GIF'
temp_thumb = BytesIO()
image.save(temp_thumb, format=file_type)
temp_thumb.seek(0)
self.thumbnail.save(thumbnail_filename, temp_thumb, save=False)
temp_thumb.close()
return super().save(*args, **kwargs)
📌 마이그레이션 필수: makemigrations → migrate
3️⃣ forms.py 수정 - Summernote + Bootstrap 적용
from django_summernote.widgets import SummernoteWidget
class TodoForm(forms.ModelForm):
class Meta:
model = Todo
fields = ['title', 'description', 'start_date', 'end_date']
widgets = {
'description': SummernoteWidget(),
'title': forms.TextInput(attrs={'class': 'form-control'}),
'start_date': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
'end_date': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'})
}
class TodoUpdateForm(forms.ModelForm):
class Meta:
model = Todo
fields = ['title', 'description', 'start_date', 'end_date', 'is_completed', 'completed_image']
widgets = {
'description': SummernoteWidget(),
'completed_image': forms.FileInput(attrs={'class': 'form-control'}),
'is_completed': forms.CheckboxInput(attrs={'class': 'form-check-input'})
}
4️⃣ views.py 수정 - form_class 적용
class TodoCreateView(LoginRequiredMixin, CreateView):
model = Todo
form_class = TodoForm
template_name = 'todo/todo_create.html'
def form_valid(self, form):
self.object = form.save(commit=False)
self.object.user = self.request.user
self.object.save()
return redirect(self.get_success_url())
def get_success_url(self):
return reverse_lazy('cbv_todo_info', kwargs={'pk': self.object.id})
class TodoUpdateView(LoginRequiredMixin, UpdateView):
model = Todo
form_class = TodoUpdateForm
template_name = 'todo/todo_update.html'
def get_object(self, queryset=None):
obj = super().get_object(queryset)
if obj.user != self.request.user and not self.request.user.is_superuser:
raise Http404()
return obj
def get_success_url(self):
return reverse_lazy('cbv_todo_info', kwargs={'pk': self.object.id})
5️⃣ admin.py 설정 - summernote 필드 + 이미지 필드 표시
class TodoAdmin(SummernoteModelAdmin):
summernote_fields = ('description',)
fieldsets = (
('Todo Info', {'fields': ('user', 'title', 'description', 'completed_image', 'is_completed')}),
('Date Range', {'fields': ('start_date', 'end_date')})
)
urls.py 설정
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import path, include
from todo.views import todo_list, todo_info, todo_create, todo_update, todo_delete
from users import views as user_views
urlpatterns = [
path('todo/', todo_list, name='todo_list'),
path('todo/create/', todo_create, name='todo_create'),
path('todo/<int:todo_id>/', todo_info, name='todo_info'),
path('todo/<int:todo_id>/update/', todo_update, name='todo_update'),
path('todo/<int:todo_id>/delete/', todo_delete, name='todo_delete'),
path('admin/', admin.site.urls),
path('accounts/', include('django.contrib.auth.urls')),
path('accounts/login/', user_views.login, name='login'),
path('accounts/signup/', user_views.sign_up, name='signup'),
# CBV URL include
path('cbv/', include('todo.urls')),
# summernote URL include
path('summernote/', include('django_summernote.urls')),
]
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
6️⃣ 템플릿 수정 - todo_list / create / update
todo_list.html
- 썸네일 이미지 출력
- 완료 상태 표시
- Bootstrap list-group 사용
{% extends 'todo/base.html' %}
{% block content %}
<style>
body {
font-family: Arial, sans-serif;
background-color: #f4f4f4;
margin: 0;
padding: 20px;
}
.custom-container {
max-width: 600px;
margin: auto;
background: #fff;
padding: 20px;
border-radius: 10px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
h1 {
text-align: center;
color: #333;
}
input[type='text'] {
height: 2vh;
}
.comment-list-item {
padding: 10px;
margin: 5px 0;
background: #eaeaea;
border-radius: 5px;
transition: background 0.3s;
}
</style>
<div class="mt-lg-5 custom-container">
<div class="d-flex justify-content-between align-items-center mb-3">
<h1>{{ todo.title }}</h1>
<div class="d-flex">
<button class="btn btn-primary me-2" onclick="location.href='{% url 'cbv_todo_update' todo.id %}'">수정하기</button>
<form method="POST" action="{% url 'cbv_todo_delete' todo.id %}">
{% csrf_token %}
<button id="delete-button" type="submit" class="btn btn-danger">삭제하기</button>
</form>
</div>
</div>
<table class="table">
{% for key, value in todo.items %}
{% if key == 'completed_image' and value %}
<div>
<img src="http://127.0.0.1:8000/media/{{ value }}" alt="완료 인증 이미지" class="w-100" style="max-height: 400px; object-fit: cover;">
</div>
{% endif %}
{% if key in 'description start_date end_date is_completed created_at updated_at' %}
<tr>
<th class="bg-light">{{ key }}</th>
{% if key == 'description' %}
<td>{{ value | safe }}</td>
{% else %}
<td>{{ value }}</td>
{% endif %}
</tr>
{% endif %}
{% endfor %}
</table>
</div>
<div class="custom-container mt-lg-2">
<h2>Comment</h2>
<hr>
<form method="POST" action="{% url 'comment_create' todo.id %}" class="d-flex justify-content-evenly align-items-center">
{% csrf_token %}
{{ comment_form.as_p }}
<button class="btn btn-primary">댓글달기</button>
</form>
<ul class="list-unstyled" id="comment_wrapper">
{% for comment in page_obj %}
<li class="comment-list-item container-fluid">
<div class="ps-2 d-flex justify-content-between align-items-center">
<p class="mb-0">{{ comment.user }}</p>
{% if request.user == comment.user or request.user.is_staff %}
<div class="text-decoration-none a-group">
<form method="POST" action="{% url 'comment_delete' comment.id %}">
{% csrf_token %}
<div class="btn-group-sm">
<button class="btn btn-primary" type="button" onclick="modify_view({{ comment.id }})">수정하기</button>
<button type=submit class="btn btn-danger">삭제하기</button>
</div>
</form>
</div>
{% endif %}
</div>
<hr>
<div>
<p class="p-lg-2">{{ comment.message }}</p>
<p class="text-end">{{ comment.created_at }}</p>
</div>
<form id="comment_modify_form_{{ comment.id }}" style="display: none" method="POST" action="{% url 'comment_update' comment.id %}">
{% csrf_token %}
{{ comment_form.as_p }}
<button class="btn btn-primary btn-sm">수정하기</button>
</form>
</li>
{% endfor %}
</ul>
{% include 'todo/pagination.html' with fragment='comment_wrapper' %}
</div>
<script>
function modify_view(commentId) {
const modifyForm = document.getElementById(`comment_modify_form_${commentId}`);
if (modifyForm.style.display === "none") {
modifyForm.style.display = ""
} else if (modifyForm.style.display === "") {
modifyForm.style.display = "none"
}
}
</script>
{% endblock %}
todo_create.html & todo_update.html
- enctype="multipart/form-data" 설정 필수
- Bootstrap form 스타일 적용
<form method="POST" enctype="multipart/form-data" class="form-control">
{% csrf_token %}
{{ form.as_p }}
<button class="btn btn-primary">Create / Update</button>
</form>
7️⃣ 테스트 흐름 정리 (서버 실행 후)
- runserver 실행 후 /cbv/todo/ 접속
- Todo 생성 시 summernote UI 확인
- 이미지 없이 생성 시 썸네일 기본 이미지 표시
- Todo 수정 페이지에서 이미지 업로드 + 완료 체크
- 수정 후 썸네일 자동 변경 확인
- Todo 삭제 시 django-cleanup으로 이미지 자동 삭제 확인
✅ media/ 경로에 실제 이미지와 썸네일이 생성되고, 삭제 시 자동 제거됨
이로써 텍스트 편집기, 이미지 처리, 자동 클린업, UI 개선을 모두 반영한 Django Todo 확장 기능이 완성됩니다.