- 인스타그램 클론 코딩 중 댓글 기능 구현을 위해 AJAX를 써야 했음
- 요즘 제이쿼리는 지양하는 추세라고 해서 바닐라 JS로 작성해봄
DB모델
class Photo(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="photos")
image = models.ImageField(upload_to="media/")
filtered_image = ImageSpecField(source='image', processors=[
ResizeToFill(293, 293)], format="JPEG", options={'quality': 60})
content = models.TextField(max_length=500, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
like = models.IntegerField(default=0)
class Meta:
ordering = ['-created_at']
def get_absolute_url(self):
return resolve_url("photo:detail", self.id)
class Comment(models.Model):
photo = models.ForeignKey(
Photo, on_delete=models.CASCADE, related_name="comments")
user = models.ForeignKey(settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="comments")
text = models.TextField(max_length=500)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
like = models.IntegerField(default=0)
def get_absolute_url(self):
if self.photo:
return resolve_url("photo:list")
- photo 앱 안에 Photo 모델과 Comment모델을 만듦
- Comment 모델은 photo와 user를 외래키로 지정하고 날짜는 자동으로 받음
- 좋아요 수는 0을 기본 값으로 설정
- 따라서 Comment 모델은 댓글 내용 text와 photo, user만 입력하면 DB에 저장 가능
- text는 HTML 댓글 폼에서 받고 photo와 user는 view에서 지정할 것임
URL 설정
app_name = "photo"
urlpatterns = [
path('comment/create/<int:pk>/', # HTML에서 넘겨주는 photo.pk를 받기 위해 <int:pk>지정
views.CommentCreateAjaxView.as_view(), name='comment_create'),]
- 클래스 뷰 사용
- photo를 기준으로 댓글이 달리기 때문에 photo.pk를 url로 받아줄 것임
AJAX로 렌더링할 HTML파일
<!-- photo_list.html-->
<h4 class="photo_content">
<span class="user_nickname">{{ photo.user.profile.nickname }}</span>
{{ photo.content }}
</h4>
<!-- with: 단순히 문자열를 바꿈
ex) "/detail/{{pk}}" -> "/detail/{{photo.id}}" -->
<div class="comment_box">
{% include 'comment/comment_container.html' with comments=photo.comments pk=photo.id%}
</div>
<small class="timezone">{{ photo.timedelta_string }}</small> <!-- 업로드 날짜 -->
<div class="comment_add"> <!-- 댓글 입력 폼 -->
<form class="comment_form" action="{% url 'photo:comment_create' photo.pk %}" method="post">
{% csrf_token %}
{{ form.as_p }}
<input type="hidden" name="next" value="{{ request.path }}">
<input type="submit">
</form>
</div>
- AJAX로 렌더링할 박스 하나 만듦 comment_box
- 그 안에 따로 렌더링할 코드를 적어논 comment_container를 include함
- include할 때 with를 같이 쓰면 변수처럼 지정 할 수 있음
- include한 html파일을 가져올 때 "comments"는 "photo.comments"로 "pk"는 "photo.id"로 바꿈
- photo_list.html은 모델이 Photo이므로 with로 변수를 조정하지 않으면 오류남
<!-- comment_container.html -->
{% if comments.count %} <!-- comments가 1개 이상이면~ -->
<a href="/detail/{{pk}}" class="comment_count"><small>댓글 {{ comments.count }}개 모두 보기</small></a>
<div class="comment">
<h5 class="comment_text">
<span class="user_nickname">{{ comments.last.user.profile.nickname }}</span>
{{ comments.last.text }}
</h5>
<i class="far fa-heart"></i>
</div>
{% endif %}
- 제일 최근에 적은 comment 하나만 보여주고 나머지 댓글을 detail 페이지에서 보여주는 방식
- 이 html파일은 AJAX로 계속해서 렌더링 될 파일임
- 디테일 페이지로 가는 앵커요소에 {% url 'photo:photo_list' photo.id %} 를 하면 AJAX로 댓글을 쓰면 오류남
- 처음 list페이지를 렌더링할 때는 괜찮지만 AJAX로 댓글을 생성할 때마다
- Comment 모델로 만들기 때문에 photo.id를 찾을 수 없음
- include with로 처음 Photo 모델에서 페이지를 렌더링할 때와 나중에 Comment 모델로 렌더링 할 때 변수를 잘 조정해서 오류없도록 맞춤
VIEW 수정
class CommentCreateAjaxView(LoginRequiredMixin, CreateView):
model = Comment
fields = ['text']
template_name = "comment/comment_container.html"
def form_valid(self, form):
comment = form.save(commit=False)
comment.user = self.request.user # 로그인한 유저
comment.photo = get_object_or_404(
Photo, pk=self.kwargs.get("pk")) # url에서 photo.pk 가져와서 일치하는 photo 찾음
comment.save() # DB에 저장
context = { # 다른 페이지로 넘겨줄 데이터 딕셔너리
"comments": Comment.objects.select_related("user").filter(photo=comment.photo).order_by("created_at"),
"pk": Photo.objects.filter(id=comment.photo.id)[0].id
} # 템플릿에서 쓰려면 {{}}안에 key 써야함
# ex) "/detail/{{pk}}" --> "/detail/16" 즉, 중괄호 사라짐
return render(self.request, "comment/comment_container.html", context)
- text는 폼으로 받고 user는 request에서 받음
- photo는 html에서 photo.pk로 넘겨주고 url에서 <int:pk>로 넘겨준 값을 받아서 가져옴
- context에 commet_container.html로 넘겨줄 데이터를 넣음
- comments와 pk라는 key로 해당 포토의 id 상수와 댓글쿼리를 넘겨줌
AJAX JavaScript 작성 - XMLHTTPRequest
<script>
const comment_inputs = document.querySelectorAll(".comment_input"); // 댓글 폼들 지정
for (input of comment_inputs) { // 댓글 폼들에게 이벤트리스너 설정
input.addEventListener("keypress", (event)=> {
if (event.keyCode === 13 || event.which === 13) { // 엔터키가 입력됐을 때
event.preventDefault(); // 엔터키 -> submit 막는 함수
const text = event.target.value; // 댓글 폼에 입력한 값
event.target.value = ""; // 댓글 input value 비우기
if (text === "") return // 아무것도 입력안하고 엔터키 입력했을 경우 그냥 리턴
const url = event.target.closest("form").getAttribute("action"); // event 지점에서 가장 가까운 form의 action을 url로 가져옴
const request = new XMLHttpRequest(); // Request 객체 생성
const data = new FormData(); // Form 객체 생성
data.append("text", text); // Form text에 데이터 삽입
request.open("POST", url, true); // Request를 POST 메소드, url에 async로 설정
request.setRequestHeader('X-CSRFToken', cookies['csrftoken']); // POST에 {{ csrf_token }}
request.send(data); // Send
request.onload = () => {
if (request.status === 200) { // send 정상적으로 완료했을 경우
const result = request.responseText; // comment_container.html 코드
event.target.closest(".comment_wrap").querySelector(".comment_box").innerHTML = result; // 박스 안 HTML코드 교체
} else {
console.error(request.responseText);
}
}
}
})
} ///////////// post csrf_token 설정
function parse_cookies() {
const cookies = {};
if (document.cookie && document.cookie !== '') {
document.cookie.split(';').forEach(function (c) {
const m = c.trim().match(/(\w+)=(.*)/);
if(m !== undefined) {
cookies[m[1]] = decodeURIComponent(m[2]);
}
});
}
return cookies;
}
const cookies = parse_cookies();
</script>
- 폼으로 데이터를 보낼 경우 header를 지정할 필요가 없음
- 여기서는 csrf_token header만 지정함
- 자동으로 Content-type이 multipart-formdata로 됨
- 괜히 header를 지정하면 send 정상적으로 작동 안함
AJAX JavaScript Fetch API
const comment_inputs = document.querySelectorAll(".comment_input");
for (input of comment_inputs) {
input.addEventListener("keypress", (event)=> {
if (event.keyCode === 13 || event.which === 13) {
event.preventDefault();
const text = event.target.value;
event.target.value = "";
if (text === "") return
const url = event.target.closest("form").getAttribute("action");
const data = new FormData(); // Form 객체 생성
data.append("text", text); // Form text에 데이터 삽입
fetch(url, {
method: "POST",
body: data,
headers: {'X-CSRFToken': cookies['csrftoken']}
}).then(res => {
if (res.status === 200) {
res.text().then(text=> {
event.target.closest(".comment_wrap").querySelector(".comment_box").innerHTML = text;
}).catch(err => console.log(err))
}
})
}
})
}
- fetch api로 좀 더 간단하게 표현
- fetch는 promise 객체를 반환
https://www.zerocho.com/category/HTML&DOM/post/594bc4e9991b0e0018fff5ed
https://nearkim.coffee/posts/django-instagram-clone-tutorial-5-4
https://new93helloworld.tistory.com/310
'Backend > Python' 카테고리의 다른 글
[인스타그램클론] 폼 에러메시지 표시 (0) | 2020.07.18 |
---|---|
[인스타그램 클론] 단일 페이지에 모델 폼 추가하기 (0) | 2020.06.17 |
중복값 갯수(collections.Counter) (0) | 2020.05.02 |
[인스타그램 클론] 프로필 페이지 (0) | 2020.04.28 |
form 이미지 업로드 (0) | 2020.04.22 |