๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
  • What would life be If we had no courage to attemp anything?
SAFFY Project

Movie World!(22.05.20~05.26)

by DevIseo 2022. 6. 17.

๐ŸํŒ€์› ์ •๋ณด ๋ฐ ์—…๋ฌด ๋ถ„๋‹ด ๋‚ด์—ญ(์•„ํ‚คํ…์ณ: Django์™€ Vanilla Js ์‚ฌ์šฉ)

๐Ÿ˜ƒ์•ˆํฌ์žฌ(ํŒ€์žฅ, back-end): Django๋ฅผ ์ด์šฉํ•œ ์˜ํ™”์ถ”์ฒœ ์›น์„œ๋ฒ„ ๊ตฌ์ถ•

๐Ÿ˜Ž์œ ์ด์„œ(ํŒ€์›, front-end): Vanilla Js์™€ HTML JavaScript bootstrap5 ๋ฅผ ์ด์šฉํ•œ ํ™”๋ฉด ๊ตฌ์„ฑ

 

๐Ÿ๋ชฉํ‘œ


์˜ํ™”์ •๋ณด

  • ๋กœ๊ทธ์ธ ๋œ ์œ ์ €๋Š” ์˜ํ™”์— ๋Œ€ํ•œ ํ‰์  ๋“ฑ๋ก/ ์ˆ˜์ •/์‚ญ์ œ ๋“ฑ์„ ํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•œ๋‹ค.

์ปค๋ฎค๋‹ˆํ‹ฐ

  • ์˜ํ™” ์ •๋ณด์™€ ๊ด€๋ จ๋œ ์†Œํ†ต์„ ํ•  ์ˆ˜ ์žˆ๋Š” ์ปค๋ฎค๋‹ˆํ‹ฐ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•ด์•ผ ํ•œ๋‹ค.
  • ๋กœ๊ทธ์ธ ์‚ฌ์šฉ์ž๋งŒ ๊ธ€์„ ์กฐํšŒ/ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์ž‘์„ฑ์ž ๋ณธ์ธ๋งŒ ๊ธ€์„ ์ˆ˜์ •/ ์‚ญ์ œํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•œ๋‹ค.
  • ์‚ฌ์šฉ์ž๋Š” ์ž‘์„ฑ๋œ ๊ฒŒ์‹œ ๊ธ€์— ๋Œ“๊ธ€์„ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•˜๋ฉฐ ์ž‘์„ฑ์ž ๋ณธ์ธ๋งŒ ๋Œ“๊ธ€์„ ์‚ญ์ œํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•œ๋‹ค.
  • ๊ฐ ๊ฒŒ์‹œ๊ธ€ ๋ฐ ๋Œ“๊ธ€์€ ์ƒ์„ฑ ๋ฐ ์ˆ˜์ • ์‹œ๊ฐ ์ •๋ณด๊ฐ€ ํฌํ•จ๋˜์–ด์•ผ ํ•œ๋‹ค.

๊ธฐํƒ€

  • 5๊ฐœ ์ด์ƒ์˜ URL ํŽ˜์ด์ง€

 

๐Ÿํ•„์ˆ˜ ์š”๊ตฌ ์‚ฌํ•ญ ์‹ค์ œ ๊ตฌํ˜„ ์—ฌ๋ถ€

A. ๊ด€๋ฆฌ์ž ๋ทฐ

  • i. ๊ด€๋ฆฌ์ž ๊ถŒํ•œ์˜ ์œ ์ €๋งŒ ์˜ํ™” ๋“ฑ๋ก / ์ˆ˜์ • / ์‚ญ์ œ ๊ถŒํ•œ์„ ๊ฐ€์ง„๋‹ค ( O )
  • ii. ๊ด€๋ฆฌ์ž ๊ถŒํ•œ์˜ ์œ ์ €๋งŒ ์œ ์ € ๊ด€๋ฆฌ ๊ถŒํ•œ์„ ๊ฐ€์ง„๋‹ค ( O )
  • iii. Django์—์„œ ๊ธฐ๋ณธ์ ์œผ๋กœ ์ œ๊ณตํ•˜๋Š” admin ๊ธฐ๋Šฅ์„ ์ด์šฉํ•˜์—ฌ ๊ตฌํ˜„ํ•œ๋‹ค ( O )

 

B. ์˜ํ™” ์ •๋ณด

  • i. ์˜ํ™” ๋ฐ์ดํ„ฐ๋Š” Database Seeding์„ ํ™œ์šฉํ•˜์—ฌ ์ตœ์†Œ 50๊ฐœ ์ด์ƒ์˜ ๋ฐ์ดํ„ฐ๋ฅผ ๊ตฌ์„ฑํ•ด์•ผ ํ•œ๋‹ค ( O )
    • movie.jsonํŒŒ์ผ์„ ์ž์ฒด์ ์œผ๋กœ ์ œ์ž‘ํ•˜์—ฌ, loaddataํ•˜์—ฌ ๋ฐ์ดํ„ฐ ์ถ”์ถœ
  • ii. ๋กœ๊ทธ์ธ ๋œ ์œ ์ €๋Š” ์˜ํ™”์— ๋Œ€ํ•œ ํ‰์  ๋“ฑ๋ก / ์ˆ˜์ • / ์‚ญ์ œ ๋“ฑ์„ ํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•œ๋‹ค ( O )

 

C. ์˜ํ™” ์ถ”์ฒœ ์•Œ๊ณ ๋ฆฌ์ฆ˜

  • i. ์˜ํ™”๋ฅผ ํ‰๊ฐ€ํ•œ ์œ ์ €๋Š” ํ•ด๋‹น ์ •๋ณด๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์˜ํ™”๋ฅผ ์ถ”์ฒœ ๋ฐ›์„ ์ˆ˜ ์žˆ์–ด์•ผ ํ•œ๋‹ค ( x )
    • ์œ ์ €๊ฐ€ 8์  ์ด์ƒ์˜ ์ ์ˆ˜๋ฅผ ์ค€ ์˜ํ™”์˜ ์žฅ๋ฅด ์ •๋ณด๋ฅผ ์ €์žฅํ•˜๊ณ ,
    • ๊ทธ ์ •๋ณด๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ํ‰์ ์ด ์ข‹์€ ์ˆœ์œผ๋กœ 10๊ฐœ์”ฉ ์ถ”์ฒœํ•˜๋Š” ์•Œ๊ณ ๋ฆฌ์ฆ˜์„ ์งœ๋ ค๊ณ  ํ•˜์˜€์œผ๋‚˜,
    • ์˜ค๋ฅ˜๋ฅผ ํ•ด๊ฒฐํ•˜์ง€ ๋ชปํ•˜์—ฌ -> ๋‹จ์ˆœ ๋žœ๋ค ์ถ”์ฒœ ์•Œ๊ณ ๋ฆฌ์ฆ˜ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑ
  • ii. ์‚ฌ์šฉ์ž๋Š” ๋ฐ˜๋“œ์‹œ ์ตœ์†Œ 1๊ฐœ ์ด์ƒ์˜ ๋ฐฉ์‹์œผ๋กœ ์˜ํ™”๋ฅผ ์ถ”์ฒœ ๋ฐ›์„ ์ˆ˜ ์žˆ์–ด์•ผ ํ•œ๋‹ค ( O )
  • iii. ์–ด๋– ํ•œ ๋ฐฉ์‹์œผ๋กœ ์ถ”์ฒœ ์‹œ์Šคํ…œ์„ ๊ตฌํ˜„ ํ–ˆ๋Š”์ง€ ๊ธฐ์ˆ ์ ์œผ๋กœ ์„ค๋ช…ํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•œ๋‹ค ( O )

 

D. ์ปค๋ฎค๋‹ˆํ‹ฐ

  • i. ์˜ํ™” ์ •๋ณด์™€ ๊ด€๋ จ๋œ ์†Œํ†ต์„ ํ•  ์ˆ˜ ์žˆ๋Š” ์ปค๋ฎค๋‹ˆํ‹ฐ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•ด์•ผ ํ•œ๋‹ค ( O )
  • ii. ๋กœ๊ทธ์ธ ์‚ฌ์šฉ์ž๋งŒ ๊ธ€์„ ์กฐํšŒ / ์ƒ์„ฑ ํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์ž‘์„ฑ์ž ๋ณธ์ธ๋งŒ ๊ธ€์„ ์ˆ˜์ • / ์‚ญ์ œํ•  ์ˆ˜ ์žˆ๋‹ค ( O )
  • iii. ์‚ฌ์šฉ์ž๋Š” ์ž‘์„ฑ๋œ ๊ฒŒ์‹œ ๊ธ€์— ๋Œ“๊ธ€์„ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์ž‘์„ฑ์ž ๋ณธ์ธ๋งŒ ๋Œ“๊ธ€์„ ์‚ญ์ œํ•  ์ˆ˜ ์žˆ๋‹ค ( O )
  • iv. ๊ฐ ๊ฒŒ์‹œ๊ธ€ ๋ฐ ๋Œ“๊ธ€์€ ์ƒ์„ฑ ๋ฐ ์ˆ˜์ • ์‹œ๊ฐ ์ •๋ณด๊ฐ€ ํฌํ•จ๋˜์–ด์•ผ ํ•œ๋‹ค ( O )

 

E. ๊ธฐํƒ€

  • i. ์ตœ์†Œํ•œ 5๊ฐœ ์ด์ƒ์˜ URL ๋ฐ ํŽ˜์ด์ง€๋ฅผ ๊ตฌ์„ฑํ•ด์•ผ ํ•œ๋‹ค ( O )
  • ii. HTTP Method์™€ HTTP response status codes๋Š” ์ƒํ™ฉ์— ๋งž๊ฒŒ ์ ์ ˆํ•˜๊ฒŒ ๋ฐ˜ํ™˜๋˜์–ด์•ผ ํ•˜๋ฉฐ, ํ•„์š”์— ๋”ฐ๋ผ ๋ฉ”์‹œ์ง€ ํ”„๋ ˆ์ž„์›Œํฌ ๋“ฑ์„ ์‚ฌ์šฉํ•˜์—ฌ ์—๋Ÿฌ ํŽ˜์ด์ง€๋ฅผ ๊ตฌ์„ฑํ•ด์•ผ ํ•œ๋‹ค ( O )

3. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ชจ๋ธ๋ง (ERD)

 

๐Ÿ์‹ค์ œ ๊ตฌํ˜„ ์ •๋„ ๋ฐ ํ•„์ˆ˜ ๊ธฐ๋Šฅ์— ๋Œ€ํ•œ ์ƒ์„ธ ์„ค๋ช… (ํ”„๋ก ํŠธ์—”๋“œ - ๋‚˜)


# ๊ตฌํ˜„ ์‹œ์—ฐ ์˜์ƒ

/movies/index.html

  • Bootstrap5์˜ Card Component๋ฅผ ์ด์šฉํ•˜์—ฌ ํ™”๋ฉด์„ ๊ตฌ์„ฑํ•˜๊ธฐ ์œ„ํ•ด movie์˜ title๊ณผ poster image๊ฐ€ ํ•„์š”
  • views.py์˜ index ๋ทฐํ•จ์ˆ˜๋ฅผ ์ฐธ๊ณ ํ•˜๋ฉด models.py์˜ Movie์—์„œ ๋ชจ๋“  ๊ฒƒ์„ ๋ถˆ๋Ÿฌ์˜ค๊ณ  ์žˆ๊ธฐ ๋•Œ๋ฌธ์— context์—์„œ ์ •์˜ํ•œ movie๋กœ models.py์˜ ์š”์†Œ๋“ค์— ์ ‘๊ทผํ•ด ์ด๋ฅผ ๋ถˆ๋Ÿฌ์™”๋‹ค.
  • ๋˜ํ•œ ์˜ํ™” ์ด๋ฏธ์ง€๋ฅผ ๋ˆ„๋ฅด๋ฉด movie detailํŽ˜์ด์ง€๋กœ ์ด๋™ ํ•ด์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์— aํƒœ๊ทธ๋ฅผ ์ด์šฉํ•ด detail ํŽ˜์ด์ง€์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•˜์˜€๋‹ค.
			<!--bootstrap card code start-->
      <div class="row row-cols-1 row-cols-sm-2 row-cols-md-3 g-4">
        {% for movie in movies %}
          <div class="col">
            <div class="card h-100 text-white bg-dark">
              <a href="{% url 'movies:detail' movie.pk %}"><img src="{{ movie.poster_path }}" class="card-img-top" alt="..."></a>
              <div class="card-body text-center">
                {{ movie.title }}
              </div>
            </div>
          </div>
        {% endfor %}
      </div>
			<!--bootstrap card code end-->

/movies/detail.html

  1. form ์ž‘์„ฑ ์œ ์ €์™€ ํ‰์ ์„ ๋‚จ๊ธด ์œ ์ €๋ฅผ ๋น„๊ตํ•ด ์‚ญ์ œ ๋ฒ„ํŠผ ํ‘œ์‹œ
    • form ํƒœ๊ทธ๋ฅผ POSTํ•˜๊ธฐ ๋•Œ๋ฌธ์— CSRF๋ฅผ ๋ง‰๊ธฐ ์œ„ํ•ด ๋‚œ์ˆ˜๋ฅผ ์ƒ์„ฑํ•ด ์ฃผ๋Š” csrf token์„ ์ž‘์„ฑ
    • ํ‰์ ์ด ์—†๋Š” ๊ฒฝ์šฐ(empty) ํ‰์ ์ด ์ž‘์„ฑ๋˜์ง€ ์•Š์•˜๋‹ค๋Š” ๋ฌธ๊ตฌ ์ถœ๋ ฅ
    {% for rating in ratings %}
        <li>
          {{ rating.user }} / {{ rating.score }}์  - {{ rating.comment }} - {{ rating.created_at }}
          {% if request.user == rating.user %}
            <form action="{% url 'movies:delete_rating' movie.pk rating.pk %}" method="POST" class="d-inline">
              {% csrf_token %}
              <input class="btn rounded-pill" style="font-size:0.75rem; background-color:#F48FB1;" type="submit" value="์‚ญ์ œ">
            </form>
          {% endif %}
        </li>
      {% empty %}
        <p>์•„์ง ์ž‘์„ฑ๋œ ํ‰์ ์ด ์—†์Šต๋‹ˆ๋‹ค! โ•ฏ๏ธฟโ•ฐ</p>
      {% endfor %}
    
  2. form ์ž‘์„ฑ ์œ ์ €๊ฐ€ ์ด๋ฏธ ํ‰์ ์„ ๋‚จ๊ฒผ์„ ๊ฒฝ์šฐ
    • ์ด๋ฏธ ์ž‘์„ฑ ์™„๋ฃŒํ–ˆ๋‹ค๋Š” ๋ฌธ๊ตฌ๋ฅผ ์ถœ๋ ฅ
    • ๋งŒ์•ฝ ํ‰์ ์„ ๋‚จ๊ธฐ์ง€ ์•Š์€ ๊ฒฝ์šฐ is_authenticated๋ฅผ ์ด์šฉํ•ด ๋กœ๊ทธ์ธ ์—ฌ๋ถ€๋ฅผ ์ฒดํฌํ–ˆ๋‹ค.
    {% if request.user.id in user_ids %}
        <strong>ํ‰์ ์„ ์ด๋ฏธ ๋“ฑ๋กํ•˜์…จ์Šต๋‹ˆ๋‹ค!</strong>
      {% else %}
        {% if request.user.is_authenticated %}
          
          <form action="{% url 'movies:create_rating' movie.pk %}" method="POST">
            {% csrf_token %}
            {% bootstrap_form rating_form %}
            <div class="text-left">โ˜…ํ‰์ (1 ~ 10์ )์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”!</div><br>
            <div style="text-align: center;">
              <input class="btn rounded-pill" style="background-color : #B3E5FC;" type="submit" value="๋Œ“๊ธ€ + ">
            </div>
            
          </form>
        {% else %}
          <a href="{% url 'accounts:login' %}" class="btn btn-dark rounded-pill">ํ‰์ ์„ ๋‚จ๊ธฐ์‹œ๋ ค๋ฉด ๋กœ๊ทธ์ธํ•˜์„ธ์š”.</a>
        {% endif %}
      {% endif %}
    

/accounts/signup.html

  • bootstrap5๋ฅผ ์ด์šฉํ•ด ์‰ฝ๊ฒŒ form์„ ๊ตฌํ˜„ํ–ˆ๋‹ค.
  • bootstrap5๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด {% load django_bootstrap5 %} ๋ฅผ block content ์•ˆ์— ์ž‘์„ฑํ•˜์˜€๋‹ค.
  • formํƒœ๊ทธ๊ฐ€ POST ์š”์ฒญ ์ด๋ฏ€๋กœ CSRF๋ฅผ ๋ง‰๊ธฐ ์œ„ํ•ด csrft token ์ž‘์„ฑํ•˜์˜€๋‹ค.
{% block content %}
{% load django_bootstrap5 %}
<div style="text-align: center; max-width:500px; margin:4rem auto"> 
  <div> 
    <h2>
      <span class="badge rounded-pill col-10 mt-3" style="background-color : #B3E5FC;">ํšŒ์›๊ฐ€์ž…</span>
    </h2>
  </div> 
  <hr>
  <div> 
    <form action="" method="POST"> 
      {% csrf_token %} 
      <!-- {{ form }} --> 
      {% bootstrap_form form %} 
      <br>
      <hr> 
      <input type="submit" class="btn rounded-pill col-6 mt-3" style="background-color : #E1F5FE;"value="๊ฐ€์ž…ํ•˜๊ธฐ"> 
    </form> 
  </div> 
</div>
{% endblock  %}

/accounts/login.html

  • singup.html๊ณผ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ bootstrap5 form์„ ์‚ฌ์šฉํ•˜๊ฒŒ ์œ„ํ•ด {% load django_bootstrap5 %}
  • formํƒœ๊ทธ๊ฐ€ POST ์š”์ฒญ ์ด๋ฏ€๋กœ CSRF๋ฅผ ๋ง‰๊ธฐ ์œ„ํ•ด csrft token ์ž‘์„ฑํ•˜์˜€๋‹ค.
  • ํšŒ์›์ด ์•„๋‹Œ ๊ฒฝ์šฐ ํšŒ์› ๊ฐ€์ž… ์ฐฝ์„ ์ฐพ๊ธฐ ์‰ฝ๊ฒŒ ์•„๋ž˜์— button์„ ๋งŒ๋“ค์—ˆ๋‹ค.
{% extends 'base.html' %}
{% block content %}
{% load django_bootstrap5 %}
<div style="text-align: center; max-width:500px; margin:4rem auto"> 
  <div> 
    <h2>
      <span class="badge rounded-pill col-10 mt-3" style="background-color : #B3E5FC;">๋กœ๊ทธ์ธ</span>
    </h2>
    <br>
    <hr>
  </div> 
  <br>
  <div> 
    <form action="" method="POST"> 
      {% csrf_token %} 
      <!-- {{ form }} --> 
      {% bootstrap_form form %}  
      <input type="submit" class="btn rounded-pill col-6 mt-3" style="background-color : #E1F5FE;"value="LogIn"> 
    </form> 
  </div>
  <br>
  <hr>
  <p>์•„์ง ํšŒ์›์ด ์•„๋‹ˆ์‹ ๊ฐ€์š”?</p> 
  <a class="btn rounded-pill btn-primary" href="{% url 'accounts:signup' %}">ํšŒ์› ๊ฐ€์ž…ํ•˜๊ธฐ</a>
</div>
{% endblock content %}

accounts/follow.html

  • follow.html์€ views.py์—์„œ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋“ฏ์ด follow์ƒํƒœ๋ฅผ HttpRequest ์ธ์Šคํ„ด์Šค๋ฅผ ์ธ์ž๋กœ ๋ฆฌํ„ดํ•˜๊ธฐ ์œ„ํ•ด FBV(Function Based View)์˜ ๋ฐฉ๋ฒ• ์ค‘ ํ•˜๋‚˜ ์ธ JSON ํ˜•์‹ response๋ฅผ ํ•ด์ฃผ์—ˆ๋‹ค.
  • ๋”ฐ๋ผ์„œ, follow.html์€ bootstrap5์˜ ๊ธฐ๋Šฅ ์ค‘ ํ•˜๋‚˜ ์ธ jumbotron ์„ ์ด์šฉํ•ด profile.html์—์„œ includeํ•ด ์ถœ๋ ฅํ•˜๋Š” ๋ฐฉ์‹์„ ํƒํ–ˆ๋‹ค.
  • follow ์ƒํƒœ๊ฐ€ ์•„๋‹ˆ๋ผ๋ฉด follow ๋ฒ„ํŠผ์ด ํšŒ์ƒ‰์œผ๋กœ ์ฒ˜๋ฆฌ ๋˜๊ฒŒ ๊ตฌํ˜„ํ–ˆ๋‹ค. (fontawesome CDN ํ™œ์šฉ)
  • ๋‹ค์Œ ๊ตฌํ˜„ ํŽ˜์ด์ง€ ์„ค๋ช…์—์„œ ์ด follow.html์„ JS์˜ axios๋ฅผ ํ†ตํ•ด ๋น„๋™๊ธฐํ™” ํ–ˆ๋‹ค.
<div class="container jumbotron text-left text-black ">
  <h1 class="display-4">@ {{ person.username }}</h1>
  <hr>
  {% with followings=person.followings.all followers=person.followers.all %}
  <div class="d-flex justify-content ">
    <!-- ํŒ”๋กœ์šฐ ๋ฒ„ํŠผ -->
    {% if request.user != person %}
      <form id="follow-form" data-user-id="{{ person.pk }}">
        {% csrf_token %}
        {% if request.user in followers %}
          <button class='btn btn-link btn-outline-light' role="button">
          <i id='follow-{{ person.pk }}' class='fa-solid fa-ticket' style='color:crimson;'> Follow</i>
          </button>
        {% else %}
          <button class='btn btn-link btn-outline-light' role="button">
          <i id='follow-{{ person.pk }}' class='fa-solid fa-ticket' style='color: gray;'> Follow</i>
          </button>
        {% endif %}
      </form>
    {% endif %}
  </div>
  <div class="d-flex justify-content-end ">
    <p class="lead">
      ํŒ”๋กœ์ž‰ : 
      <span id='followings-count-{{ person.pk }}'>{{ followings|length }}</span>
       | ํŒ”๋กœ์›Œ :
       <span id='followers-count-{{ person.pk }}'>{{ followers|length }}</span>       
    </p>
  </div>
  {% endwith %}
</div>

accounts/profile.html

  • follow.html์—์„œ ์ž‘์„ฑ๋œ Bootstrap์˜ jumbtron์„ {% include 'accounts/follow.html' %} ํ†ตํ•ด ์ƒ๋‹จ์— ํ‘œ์‹œํ–ˆ๋‹ค.
  • Bootstrap์˜ Component์ค‘ ํ•˜๋‚˜ ์ธ Accordion์„ ํ™œ์šฉํ•ด ํ”„๋กœํ•„ ์œ ์ €์˜ ํ‰์  ๋ชฉ๋ก๊ณผ ์ž‘์„ฑํ•œ ๊ฒŒ์‹œ๊ธ€ ๋ฐ ๋Œ“๊ธ€, ์ข‹์•„์š” ํ•œ ๊ฒŒ์‹œ๊ธ€์„ ํ‘œํ˜„ํ–ˆ๋‹ค.
  • {% include 'accounts/follow.html' %}ํ•ด์„œ ๋ถˆ๋Ÿฌ์˜จ follow.html์—์„œ follow๋ฒ„ํŠผ์„ ๋ˆŒ๋ €์„ ๋•Œ ์ƒˆ๋กœ ๊ณ ์นจ์ด ์ผ์–ด๋‚˜์ง€ ์•Š๊ฒŒ ํ•˜๊ธฐ ์œ„ํ•ด JS๋ฅผ ์‚ฌ์šฉํ•ด ์ด๋ฅผ ๊ตฌํ˜„ํ–ˆ๋‹ค.
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
  const form = document.querySelector('#follow-form')
  const csrftoken = document.querySelector('[name=csrfmiddlewaretoken]').value

  form.addEventListener('submit', function (event) {
    event.preventDefault()
    console.log(event)
    const userId = event.target.dataset.userId

    axios({
      method: 'post',
      url: `/accounts/follow/${userId}/`,
      headers: {'X-CSRFToken': csrftoken},
      }).then((response) => {
      const followed = response.data.followed
      const followersCount = response.data.followers_count
      const followingsCount = response.data.followings_count
      
      const followButton = document.querySelector(`#follow-${userId}`)
      if (followed) {
        followButton.style.color = 'crimpson'
      } else {
        followButton.style.color = 'gray'
      }
      
      const followingsNum = document.querySelector(`#followings-count-${userId}`)
      followingsNum.innerText = followingsCount
      const followersNum = document.querySelector(`#followers-count-${userId}`)
      followersNum.innerText = followersCount
      
    })
  })
</script>
  • addEventListener์„ ํ†ตํ•ด follow ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜๋ฉด POST ์š”์ฒญ์ด ๊ฐ€๋„๋ก ์ž‘์„ฑํ–ˆ๋‹ค.
  form.addEventListener('submit', function (event) {
    event.preventDefault()
    console.log(event)
    const userId = event.target.dataset.userId
	...

POST์š”์ฒญ์ด๊ธฐ ๋•Œ๋ฌธ์— CSRF๋ฅผ ๋ง‰๊ธฐ ์œ„ํ•ด csrf token์„ ์‚ฌ์šฉํ•ด์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์— axios์— ์ง์ ‘ csrf token์„ ์ง€์ •ํ•ด ์ฃผ์—ˆ๋‹ค. (์ „์—ญ ๋ณ€์ˆ˜๋กœ csrf token์„ ์ž‘์„ฑํ•˜๋Š” ๋ฐฉ๋ฒ•๋„ ์žˆ๋‹ค.)

    axios({
      method: 'post',
      url: `/accounts/follow/${userId}/`,
      headers: {'X-CSRFToken': csrftoken},
      }).then((response) => {
      ...
  • follow ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜๋ฉด ๋ฒ„ํŠผ์€ ํŒ”๋กœ์šฐ ์ƒํƒœ๊ฐ€ ๋˜๊ณ (ํšŒ์ƒ‰์—์„œ ๋‹ค๋ฅธ ์ƒ‰์œผ๋กœ), ํŒ”๋กœ์šฐ ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅธ ์‚ฌ๋žŒ์˜ ํŒ”๋กœ์ž‰์ด +1 ๋˜๊ฒŒ ํ•˜๊ณ , ํŒ”๋กœ์›Œ๊ฐ€ ์ƒ๊ธด ์‚ฌ๋žŒ์€ ํŒ”๋กœ์›Œ๊ฐ€ +1 ๋˜๊ฒŒ ๊ตฌํ˜„ํ•˜์˜€๋‹ค.
  • ๋ฐ˜๋Œ€์˜ ๊ฒฝ์šฐ์—” -1์ด ๋˜๊ณ  ๋ฒ„ํŠผ ์ƒ‰๋„ ๋‹ค์‹œ ํšŒ์ƒ‰์ด ๋˜๊ฒŒ ๋œ๋‹ค.
.then((response) => {
      const followed = response.data.followed
      const followersCount = response.data.followers_count
      const followingsCount = response.data.followings_count
      
      const followButton = document.querySelector(`#follow-${userId}`)
      if (followed) {
        followButton.style.color = 'crimson'
      } else {
        followButton.style.color = 'gray'
      }
      
      const followingsNum = document.querySelector(`#followings-count-${userId}`)
      followingsNum.innerText = followingsCount
      const followersNum = document.querySelector(`#followers-count-${userId}`)
      followersNum.innerText = followersCount
      
    })

community/index.html

  • ๊ธ€์„ ์ž‘์„ฑํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ๋กœ๊ทธ์ธ์„ ํ•˜๋„๋ก is_authenticated ํ•ด ๋กœ๊ทธ์ธ ์—ฌ๋ถ€๋ฅผ ์ฒดํฌํ•ด ์ฃผ์—ˆ๋‹ค.
  • ๋งŒ์•ฝ ๋กœ๊ทธ์ธ์ด ๋˜์–ด ์žˆ๋‹ค๋ฉด +new ๋ฒ„ํŠผ์ด ๋ณด์ด๋„๋ก ์„ค์ •ํ–ˆ๋‹ค.
{% if user.is_authenticated %}
  <a class="btn rounded-pill text-black" style="background-color : #E1F5FE;" href="{% url 'community:create' %}">+ New</a>
{% else %}
  <a href="{% url 'accounts:login' %}">์ƒˆ ๊ธ€์„ ์ž‘์„ฑํ•˜๋ ค๋ฉด ๋กœ๊ทธ์ธํ•˜์„ธ์š”</a>
{% endif %}
  • ์˜ํ™” ์ œ๋ชฉ, ์ž‘์„ฑ์ž, ํ‰์ , ์ž‘์„ฑ ์‹œ๊ฐ„, ์ข‹์•„์š” ๋ฒ„ํŠผ, ์ข‹์•„์š” ํ•œ ์‚ฌ๋žŒ์˜ ์ˆ˜๊ฐ€ ๋‚˜์˜ค๋„๋ก ๊ตฌํ˜„ํ–ˆ๋‹ค.
  • ์—ฌ๊ธฐ๋„ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ POST ์š”์ฒญ์ด๊ธฐ ๋•Œ๋ฌธ์— csrf token์„ ์ž‘์„ฑํ–ˆ๋‹ค.
  • ์ข‹์•„์š” ๋ฒ„ํŠผ์„ ํ•˜ํŠธ๋กœ ๋ฐ”๊พธ์—ˆ์œผ๋ฉฐ ์ด๋ฅผ ๋ˆ„๋ฅด๋ฉด ํ•˜ํŠธ๊ฐ€ ๋นจ๊ฐ„์ƒ‰(์ข‹์•„์š” ์ƒํƒœ), ๋‹ค์‹œ ํ•œ๋ฒˆ ๋” ๋ˆ„๋ฅด๋ฉด ๊ฒ€์€์ƒ‰(์ข‹์•„์š” ์ทจ์†Œ ์ƒํƒœ)์œผ๋กœ ๋‚˜ํƒ€๋‚˜๊ฒŒ ํ•˜์˜€๋‹ค.
  • ๋”๋ถˆ์–ด ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅผ ๋•Œ ์ข‹์•„ํ•˜๋Š” ์‚ฌ๋žŒ์˜ ์ˆ˜๊ฐ€ ์ฆ๊ฐ๋˜๋„๋ก ํ–ˆ๋‹ค.
{% for review in reviews %}
      <a class="fw-bold text-decoration link-dark" href="{% url 'community:detail' review.pk %}">{{ review.title }}</a>
      <br>
      <a>์˜ํ™” ์ œ๋ชฉ : {{ review.movie_title }}</a>
      <br>
      ์ž‘์„ฑ์ž : <a class=" link-secondary" href="{% url 'accounts:profile' review.user.username %}">{{ review.user }}</a>
      <br>
      <a>ํ‰์  : {{review.rank}}</a>
      <br>
      <a>์ž‘์„ฑ์‹œ๊ฐ„ : {{review.created_at}}</a>
      
      <!-- ์ข‹์•„์š” start -->
      <div class="d-flex justify-content-end ">
        <form class="like_form" data-id='{{ review.pk }}'>
          {% csrf_token %}
          {% if user in review.like_users.all %}
            <button class="btn btn-link">
              <i id="like-{{ review.pk }}" class="fas fa-heart fa-lg" style="color:crimson;"></i>
            </button>
          {% else %}
            <button class="btn btn-link">
              <i id="like-{{ review.pk }}" class="fas fa-heart fa-lg" style="color:black;"></i>
            </button>
          {% endif %}
        </form>
        <span id="like-count-{{ review.pk }}">{{ review.like_users.all|length }}</span>๋ช…์ด ์ด ๊ธ€์„ ์ข‹์•„ํ•ฉ๋‹ˆ๋‹ค.
      </div>
      <hr>
    {% endfor %}
  • accounts/profile.html๊ณผ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ์ข‹์•„์š” ๋ฒ„ํŠผ์„ axios์ฒ˜๋ฆฌํ•˜๋Š” ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ–ˆ๋‹ค.
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
  <script>
    const forms = document.querySelectorAll('.like_form')
    const csrftoken = document.querySelector('[name=csrfmiddlewaretoken]').value

    forms.forEach(function (form) {
      form.addEventListener('submit', function (event) {
        event.preventDefault()
        const reviewId = event.target.dataset.id
  
        axios.post(`http://127.0.0.1:8000/community/${reviewId}/like/`, {},
        {headers: {'X-CSRFToken': csrftoken}
        }).then(function (response) {
          const liked = response.data.liked
          const count = response.data.count

          const likeButton = document.querySelector(`#like-${reviewId}`)
          if (liked) {
            likeButton.style.color = 'crimson'
          } else {
            likeButton.style.color = 'black'
          }
          const likeCount = document.querySelector(`#like-count-${reviewId}`)
          likeCount.innerText = `${count}`
        })
      })
    })
  </script>

 

community/create.html

  • create.html ํŽ˜์ด์ง€๋Š” bootstrap5์˜ form์„ ํ™œ์šฉํ•˜๋Š” ๋ฐฉ์‹์„ ํƒํ–ˆ๋‹ค.
{% extends 'base.html' %}

{% block content %}
{% load django_bootstrap5 %}
<div style="max-width:500px; margin:4rem auto">
  <h1 class="text-center">Create</h1>
  <br>
  <form action="" method="POST">
    {% csrf_token %}
    {% bootstrap_form form %}
    <div style="text-align: right;">
      <input class="btn rounded-pill " style="background-color : #B3E5FC;" type="submit" value="create!">
      <hr>
      <a class="btn  rounded-pill " style="background-color : #E1F5FE;" href="{% url 'community:index' %}">Back</a>
    </div>
  </form>
</div>
{% endblock %}

community/detail.html

  • ๋กœ๊ทธ์ธ ๋œ ์œ ์ €์™€ ๋ฆฌ๋ทฐ์ž‘์„ฑ์ž๊ฐ€ ์ผ์น˜ํ•˜๋ฉด ์‚ญ์ œ ๋ฒ„ํŠผ์ด ๋ณด์ด๊ณ  ์‚ญ์ œ๋„ ๊ฐ€๋Šฅํ•˜๊ฒŒ ๊ตฌํ˜„ํ–ˆ๋‹ค.
{% if request.user == review.user %}
    <a href="{% url 'community:update' review.pk %}" class="btn rounded-pill" style="font-size:0.75rem; background-color: #BBDEFB;">์ˆ˜์ •</a>
      <form action="{% url 'community:delete' review.pk %}" method="POST">
        {% csrf_token %}
        <button class="btn rounded-pill" style="font-size:0.75rem; background-color:#F48FB1;">์‚ญ์ œ</button>
      </form>
  {% endif %}

community/update.html

  • ๋‹ค๋ฅธ ํŽ˜์ด์ง€์™€ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ update.html๋„ bootstrap5์˜ form์„ ํ™œ์šฉํ•˜๋Š” ๋ฐฉ์‹์„ ํƒํ–ˆ๋‹ค.
{% extends 'base.html' %}

{% block content %}
{% load django_bootstrap5 %}
  <div style="max-width:500px; margin:4rem auto">
    <h1>Update</h1>
    <br>
    
  <form action="" method="POST">
    {% csrf_token %}
    <div style="text-align: left;">
      {% bootstrap_form form %}
    </div>
    <div style="text-align: right;">
      <input class="btn rounded-pill " style="background-color : #B3E5FC;" type="submit" value="update!">
      <hr>
      <a class="btn  rounded-pill " style="background-color : #E1F5FE;" href="{% url 'community:detail' review.pk %}">Back</a>
    </div>
  </form>
</div>
  {% endblock %}

 

ํ›„๊ธฐ

์ง€๋‚œ ํŽ˜์–ด ํ”„๋กœ์ ํŠธ ์ดํ›„๋กœ ๋‘ ๋ฒˆ์งธ๋กœ ๋‹ค๋ฅธ ์‚ฌ๋žŒ๊ณผ ํ˜‘์—…ํ•˜์—ฌ ์ง„ํ–‰ํ•˜๋Š” ํ”„๋กœ์ ํŠธ์˜€๋Š”๋ฐ, 
์ข‹์€ ํŒ€์›์„ ๋งŒ๋‚˜์„œ ์ˆ˜์›”ํ•˜๊ฒŒ ํ”„๋กœ์ ํŠธ๋ฅผ ์™„์„ฑํ•ด๋‚ธ ๊ฑฐ ๊ฐ™์Šต๋‹ˆ๋‹ค. 
ํ”„๋กœ์ ํŠธ๋ฅผ ๊ตฌ์ƒํ•  ๋•Œ ๋ชฉํ‘œํ•˜๋Š” ๋ฐ”์™€ ๊ตฌํ˜„ ํŽ˜์ด์ง€ ๊ตฌ์ƒ์— ๋Œ€ํ•œ ์˜๊ฒฌ์ด ๋น„์Šทํ•ด์„œ ๋น ๋ฅด๊ฒŒ ์ง„ํ–‰ํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. 
๋˜ํ•œ ์‹ธํ”ผ ๊ณผ์ •์—์„œ ๋ฐฐ์› ๋˜ ๊ฒƒ์„ ์ด์šฉํ•ด ํ•ด๋ณด๋Š” ์ ์—์„œ ๋œป๊นŠ์—ˆ์Šต๋‹ˆ๋‹ค.
์ง€๋‚œ ๊ด€ํ†ต ํ”„๋กœ์ ํŠธ๋“ค์—์„œ๋Š” ํ•˜๋ฃจ ๋งŒ์— ๊ธฐ๋ณธ ๊ธฐ๋Šฅ๋งŒ์„ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐ์— ํž˜ ์Ÿ์•„ 
์–ด๋”˜๊ฐ€ ์—‰์„ฑํ•˜๊ณ  ๊ฐ€๋…์„ฑ ์—†๋Š” ํŽ˜์ด์ง€๋“ค์„ ๋งŒ๋“ค์—ˆ์—ˆ๋Š”๋ฐ, 
์ด๋ฒˆ์—๋Š” ๊ธฐ๋Šฅ๋“ค์„ ๋ฉฐ์น ๊ฐ„ ํŽ˜์–ด์™€ ๋‚˜๋ˆ„์–ด ๊ตฌํ˜„ํ•˜๊ณ  ํ™ˆํŽ˜์ด์ง€๋ฅผ 
์ข€ ๋” ๊ฐ€๋…์„ฑ ์žˆ๊ณ  ์‹ฌํ”Œํ•˜๊ฒŒ ๊ตฌํ˜„ํ•ด ๋‚ผ ์ˆ˜ ์žˆ์–ด์„œ ๋ฟŒ๋“ฏํ–ˆ์Šต๋‹ˆ๋‹ค! 
๋˜ํ•œ ํ˜ผ์ž๊ฐ€ ์•„๋‹Œ ๋‹ค๋ฅธ ์‚ฌ๋žŒ๊ณผ ํ•จ๊ป˜ ํ”„๋กœ์ ํŠธ๋ฅผ ํ•ด๋‚ด์–ด ์˜๋ฏธ์žˆ๋Š” ๊ฒฐ๊ณผ๋ฅผ ๋งŒ๋“ค์—ˆ๋‹ค๋Š” ์ ์—์„œ 
5๊ฐœ์›” ๋™์•ˆ ๋งŽ์€ ์„ฑ์žฅ์„ ํ–ˆ๋‹ค๊ณ  ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค๐Ÿ˜ƒ

 

 

 

๋Œ“๊ธ€