[Django] widget (5) widget 만들어보기 - 이미지, Clearble File Input
개별적인 내용을 담고있지만 이전 포스팅과 전체적인 맥락은 이어지므로 참고하시면 도움이 되실겁니다. 현재 모델은 다음과 같습니다.
# core/models.py
from django.db import models
from django.core.validators import MinValueValidator, MaxValueValidator
class University(models.Model):
name = models.CharField(max_length=100, unique=True)
def __str__(self):
return self.name
class Student(models.Model):
university = models.ForeignKey(University, on_delete=models.CASCADE)
date_birth = models.DateTimeField()
residencce = models.CharField(max_length=200)
photo = models.ImageField(blank=True)
grade = models.IntegerField(validators=[MinValueValidator(1), MaxValueValidator(5)])
intro = models.TextField()
이번에는 Student 모델에 대해 photo
에 대해 다뤄보겠습니다. 기존의 input 태그의 file 필드를 보면 UI 적으로는 별로인게 사실입니다. 이미지도 확인할 수 가 없고 말이죠. 이 부분을 개선해보도록 하겠습니다.
Clearble File Input
각 field 에는 대응되는 widget이 존재합니다. file, image field 의 경우에는 기본적으로 Clearble File Input 이 사용되는데 이번에는 새로운 라이브러리를 가져오기 보다 이를 이용해서 진행하도록 하겠습니다.
기존에는 이미지를 선택하더라도 다음과 같이 이미지의 이름만을 보여줄 뿐 이미지가 보이지는 않습니다. 우선 이미지가 보이도록 해보도록 개선하겠습니다.
# core/forms.py
from django import forms
from django.urls import reverse_lazy
from .models import University, Student
from .widgets import CounterTextInput, starWidget, AutoCompleteWidget, DatePickerWidget, PreviewFileWidget
# 생략
class StudentForm(forms.ModelForm):
class Meta:
model = Student
fields = '__all__'
widgets = {
'university': AutoCompleteWidget(ajax_url=reverse_lazy('university_list')),
'grade': starWidget,
'date_birth': DatePickerWidget,
'photo': PreviewFileWidget,
}
PreviewFileWidget
이라는 커스텀 widget 을 정의하여 사용하도록 하겠습니다.
# core/widgets.py
class PreviewFileWidget(forms.ClearableFileInput):
template_name = "widgets/preview_file.html"
class Media:
js = [
"//code.jquery.com/jquery-3.4.1.min.js",
]
ClearableFileInput
을 상속받고 CDN 방식으로 jQuery 만 필요합니다. 나머지 기능은 이를 통해 구현하도록 합니다.
<!-- templates/widgets/preview_file.html -->
{% include "admin/widgets/clearable_file_input.html" %}
{{ form.media }}
<script>
$(function(){
$('#{{ widget.attrs.id }}').change(function(){
if (this.files[0]){
var reader = new FileReader();
reader.readAsDataURL(this.files[0]);
reader.onload = function(e){
$('#preview_{{widget.attrs.id}}').attr('src', e.target.result);
};
}
})
})
</script>
기본적으로 clearble_file_input.html
을 include 해주고 media 를 불러왔습니다. file input 이 변할 때마다 함수를 실행시켜줍니다. this.files
를 통해 input 에 들어간 file 들의 list 를 불러올 수 있습니다. 기본적으로 여러 사진을 추가 할 수 있기 때문에 list 형태로 존재하며 지금은 사진을 하나씩 넣고 있기 때문에 index 를 통해 접근이 가능합니다. file 을 읽을 수 있는 FileReader 객체를 만들어주고, 이 파일을 읽어줍니다. onload
를 통해서 파일을 다 읽을 때까지 기다렸다가 함수를 실행합니다.e.target.result
를 통해 파일을 64진수로 표현할 수 있으며 이를 img input 에 넣어줌으로써 사진을 볼 수 있습니다.
<!-- templates/widgets/preview_file.html -->
{{ form.media }}
<div class="img_cotainer" style="display: flex; align-items: center;">
{% if widget.value %}
{{ widget.value.url }}
{% endif %}
<img id="preview_{{ widget.attrs.id }}" style="width: 100px; height:100px; display: none;">
{% include "admin/widgets/clearable_file_input.html" %}
</div>
<script>
$(function(){
$('#{{ widget.attrs.id }}').change(function(){
if (this.files[0]){
var reader = new FileReader();
reader.readAsDataURL(this.files[0]);
reader.onload = function(e){
$('#preview_{{widget.attrs.id}}').attr('src', e.target.result).show();
};
}
})
})
</script>
실습을 따라하고 계시다면 위 사진과 다른 위치에 사진이 보이실 겁니다. 사진 위치를 좀 조절하기 위해서 약간의 style 을 추가하였습니다. 처음에는 사진 공간이 보여지지 않고 사진이 들어가는 순간 show
를 통해 보여지게 됩니다. 또한 위에서의 문제는 file 의 값이 바뀌었을 때는 그 이미지를 바로 띄워주지만 저장한 후 다시 확인하러 들어가면 js 코드가 실행이 안되니 확인 할 수 없습니다. 따라서 widget.value.url
를 통해 해당 이미지를 불러옵니다.
이미지의 이름은 뜨지만 사진은 아직 뜨지 않습니다. 이에 대해서는 media 처리를 해줘야합니다. 이에 대한 자세한 설명은 media 에 대한 이전 포스팅을 참조해주세요.
# config/settings.py
# 생략
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
# config/urls.py
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('core.urls')),
]
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
다음과 같은 설정 후, MEDIA_ROOT
에. 해당하는 경로에 media 폴더를 만들어줍니다. 지금은 manage.py 와 같은 위치가 될 것 입니다.
<!-- templates/widgets/preview_file.html -->
{{ form.media }}
<div class="img_cotainer" style="display: flex; align-items: center;">
{% if widget.value %}
<img src="{{ widget.value.url }}" style="width:100px; height:100px;">
{% endif %}
<img id="preview_{{ widget.attrs.id }}" style="width: 100px; height:100px; display: none;">
{% include "admin/widgets/clearable_file_input.html" %}
</div>
<script>
// 위와 동일하여 생략
</script>
결과적으로 widget.value.url
은 media 파일이 존재하는 경로가 되었으므로 위와 같이 사진을 나타낼 수 있습니다.
하지만 이럴 경우, 아래 이미지와 같은 문제가 발생합니다.
<!-- templates/widgets/preview_file.html -->
{{ form.media }}
<div class="img_cotainer" style="display: flex; align-items: center;">
{% if widget.value %}
<img id="current_{{widget.attrs.id}}" src="{{ widget.value.url }}" style="width:100px; height:100px;">
{% endif %}
<img id="preview_{{ widget.attrs.id }}" style="width: 100px; height:100px; display: none;">
{% include "admin/widgets/clearable_file_input.html" %}
</div>
<script>
$(function(){
$('#{{ widget.attrs.id }}').change(function(){
if (this.files[0]){
var reader = new FileReader();
reader.readAsDataURL(this.files[0]);
reader.onload = function(e){
$('#preview_{{widget.attrs.id}}').attr('src', e.target.result).show();
$('#current_{{widget.attrs.id}}').hide()
};
}
})
})
</script>
따라서 새로운 이미지가 추가되면 기존에 있던 이미지는 hide
로 숨겨주었습니다.
아래 영상과 같이 이미지가 바뀌는걸 확인할 수 있습니다.