
์ญ๋ ์ต๊ณ ์ ์ํ 10ํธ์ ๊ผฝ๋ ์น์ฌ์ดํธ
๐ ์ ์ ์ฌํญ
- ํํ์ด์ง์์ ํ 10 ์ํ ๋ชฉ๋ก์ ์นด๋ ํ์์ผ๋ก ๋ณผ ์ ์๊ฒ ๋ง๋ค๊ธฐ
- SQL ์์ผ๋ฏธ๋ฅผ ์ฌ์ฉํ์ฌ SQ ๋ผ์ดํธ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์์ฑ (ํ ์ด๋ฆ: Movie)
- ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ํ๋์ฝ๋ฉ์ผ๋ก ์ ํญ๋ชฉ ์ถ๊ฐ (์ฝ๋๋ฅผ ํ ๋ฒ ์คํ ํ์๋ ์ญ์ ํด์ผ ์ค๋ฅ ๋ฐ์ ์ ํจ)
- ์นด๋ ์๋ฉด์ ์ํ ํฌ์คํฐ ์ด๋ฏธ์ง, ๋ท๋ฉด์ ์ํ ์ ๋ชฉ(์ฐ๋), ๋ณ์ , ๋ฆฌ๋ทฐ, ์ค๋ช
๐ ์ ์ ์ฌํญ
- ์ํ ์นด๋ ๋ท๋ฉด์ ์ ๋ฐ์ดํธ ๋ฒํผ์ ๋๋ฌ ํ์ ๊ณผ ๋ฆฌ๋ทฐ๋ฅผ ๋ณ๊ฒฝํ๋๋ก ํ๊ธฐ
- WTForms๋ก edit.html์์ ๋ ๋๋งํ Quick Form์ ์์ฑ
- ํผ์ ์ ์ถํ๊ณ ์ ํจ์ฑ์ ๊ฒ์ฌํ ๋ค์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ํด๋น ์ํ ํญ๋ชฉ์ ์ ๋ฐ์ดํธ ์ฌํญ์ ์ถ๊ฐ
๐ ์ ์ ์ฌํญ
- ์ํ ์นด๋ ๋ท๋ฉด์ ์ญ์ ๋ฒํผ์ ํด๋ฆญํ๋ฉด ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ์ํ ํญ๋ชฉ์ ์ญ์
๐ ์ ์ ์ฌํญ
- Add Movie ๋ฒํผ์ ํด๋ฆญํ๋ฉด ์ถ๊ฐ ํ์ด์ง๊ฐ ๋ ๋๋ง๋๋๋ก ์์
(์ํ ์ ๋ชฉ ํ๋๋ง ํฌํจ๋ WTF quick form์ด ํ์๋์ด์ผ ํ๋ค)- ์ํ ์ ๋ชฉ ์ ๋ ฅ ํ ๋ฒํผ์ ๋๋ฅด๋ฉด ํ๋ผํฌ์ค ์๋ฒ๊ฐ ํด๋น ์ํ ์ ๋ชฉ์ ์์ ํ๋๋ก ๋ณ๊ฒฝ
- requests๋ก ํด๋น ์ ๋ชฉ๊ณผ ์ผ์นํ๋ ๋ชจ๋ ์ํ์ ๋ํด The Movie Database API๋ฅผ ์์ฒญ ๋ฐ ๊ฒ์
- ๋ฌด๋ฃ ๊ฐ์ ํ
์ค์ โAPI ํค ์์ฒญโDeveloperโ ์์ ๊ธฐ์ ํ ์์ฑ- ์ฐธ๊ณ : ์ ํ๋ฆฌ์ผ์ด์ ์ค๋ช ๋์ ๋๋ฌด ์งง๊ฒ ์ ์ผ๋ฉด ์ค๋ฅ๊ฐ ๋ฐ์ํ๋ ๊ฒ ๊ฐ์
- ์ฐธ๊ณ : API_KEY ๋์ ACCESS_TOKEN ๋ง ๋๋ ๊ฒ ๊ฐ์
- ๊ฒ์ ์ฟผ๋ฆฌ๋ฅผ ๋ง๋ค์ด API๋ก ์ํ ๋ฐ์ดํฐ๋ฅผ ์์ฒญ
- ๊ฒ์๋ ์ํ ๋ฆฌ์คํธ ์ค ํ๋๋ฅผ ์ ํํ๋ฉด
- ํด๋น ์ํ ID๋ฅผ ์ฌ์ฉํ์ฌ ์ํ ๋ฐ์ดํฐ๋ฒ ์ด์ค API์ ๋ค๋ฅธ ๊ฒฝ๋ก๊ฐ ์์ฒญ๋์ด์ผ ํ๋ค
- ํด๋น ์ํ์ ๋ํ ์ ๋ชฉ, ์ด๋ฏธ์งurl, ๊ฐ๋ด์ฐ๋, ์ค๋ช ๊ฐ์ ธ์ค๊ธฐ
- edit.html ํ์ด์ง๋ก ๋ฆฌ๋๋ ์ ํด์ ํ์ ๊ณผ ํ ์ค ๋ฆฌ๋ทฐ๋ฅผ ์ถ๊ฐํ๊ธฐ
๐ ์ ์ ์ฌํญ
- ์ถ๊ฐํ ์ํ๋ค์ ํ์ ์ ๋ฐ๋ผ ์๋์ผ๋ก ๋ฑ๊ธ์ ๋งค๊ธฐ๋๋ก ๋ณ๊ฒฝํ๊ธฐ
- ๋ณ์ ์ด ๊ฐ์ฅ ๋ฎ์ ์์ผ๋ก ์์์๋ถํฐ ์ ๋ ฌ
โจ๏ธ main.py
from flask import Flask, render_template, redirect, url_for, request
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from sqlalchemy import String
from flask_bootstrap import Bootstrap
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired
import requests
TMDB_SEARCH_URL = "https://api.themoviedb.org/3/search/movie"
TMDB_DETAILS_URL = "https://api.themoviedb.org/3/movie/"
TMDB_IMAGE_BASE_URL = "https://image.tmdb.org/t/p/original"
TMDB_API_ACCESS_TOKEN = "๊ฐ์ธ ํ ํฐ"
headers = {
"accept": "application/json",
"Authorization": f"Bearer {TMDB_API_ACCESS_TOKEN}"
}
app = Flask(__name__)
app.config['SECRET_KEY'] = '8BYkEfBA6O6donzWlSihBXox7C0sKR6b'
Bootstrap(app)
class RateMovieForm(FlaskForm):
rating = StringField("Your Rating Out of 10", validators=[DataRequired()])
review = StringField("Your Review", validators=[DataRequired()])
submit = SubmitField("Done")
class FindMovieForm(FlaskForm):
title = StringField("Movie Title", validators=[DataRequired()])
submit = SubmitField("Add Movie")
# CREATE DB
class Base(DeclarativeBase):
pass
db = SQLAlchemy(model_class=Base)
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///movie.db"
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db.init_app(app)
# CREATE TABLE
class Movie(db.Model):
id: Mapped[int] = mapped_column(primary_key=True)
title: Mapped[str] = mapped_column(String(250), unique=True, nullable=False)
year: Mapped[int] = mapped_column(nullable=False)
description: Mapped[str] = mapped_column(String(500), nullable=False)
rating: Mapped[float] = mapped_column(nullable=True)
review: Mapped[str] = mapped_column(String(250), nullable=True)
ranking: Mapped[int] = mapped_column(nullable=True)
img_url: Mapped[str] = mapped_column(nullable=False)
with app.app_context():
db.create_all()
# # ํ๋์ฝ๋ฉ์ผ๋ก ์ํ ์ถ๊ฐ (ํ ๋ฒ ์ถ๊ฐํ ๋ฐ์ดํฐ๋ ์ฝ๋์์ ์ญ์ ํ๊ธฐ)
# new_movie = Movie(
# title="Phone Booth",
# year=2002,
# description="Publicist Stuart Shepard finds himself trapped in a phone booth, pinned down by an extortionist's sniper rifle. Unable to leave or receive outside help, Stuart's negotiation with the caller leads to a jaw-dropping climax.",
# rating=7.3,
# ranking=10,
# review="My favourite character was the caller.",
# img_url="https://image.tmdb.org/t/p/w500/tjrX2oWRCM3Tvarz38zlZM7Uc10.jpg"
# )
# db.session.add(new_movie)
# db.session.commit()
@app.route("/")
def home():
# Movie ํ
์ด๋ธ์ ๋ฐ์ดํฐ๋ฅผ rating ๊ธฐ์ค์ผ๋ก ์ ๋ ฌํ ํ db.session.execute()๋ก ์ฟผ๋ฆฌ ์คํ
result = db.session.execute(db.select(Movie).order_by(Movie.rating))
# result.scalars()๋ก ScalarResult ๊ฐ์ฒด์์ ์ค์ ๋ฐ์ดํฐ๋ฅผ ์ถ์ถํ ํ ํ์ด์ฌ ๋ฆฌ์คํธ๋ก ๋ณํ
all_movies = result.scalars().all()
for i in range(len(all_movies)):
all_movies[i].ranking = len(all_movies) - i
db.session.commit()
return render_template("index.html", movies=all_movies)
@app.route("/edit", methods=["GET", "POST"])
def rate_movie():
form = RateMovieForm()
movie_id = request.args.get("id")
movie = Movie.query.get(movie_id)
if form.validate_on_submit():
# .data ์์ฑ์ผ๋ก ์
๋ ฅ๊ฐ์ ์ถ์ถ
movie.rating = float(form.rating.data)
movie.review = form.review.data
db.session.commit()
return redirect(url_for('home'))
return render_template("edit.html", movie=movie, form=form)
@app.route("/delete")
def delete():
movie_id = request.args.get("id")
movie_to_delete = db.get_or_404(Movie, movie_id)
db.session.delete(movie_to_delete)
db.session.commit()
return redirect(url_for('home'))
@app.route("/add", methods=["GET", "POST"])
def add():
form = FindMovieForm()
if form.validate_on_submit():
movie_title = form.title.data
params = {
"query": movie_title,
"include_adult": "false",
"language": "en-US",
}
response = requests.get(TMDB_SEARCH_URL, params=params, headers=headers)
data = response.json()["results"]
return render_template("select.html", options=data)
return render_template("add.html", form=form)
@app.route("/find")
def find():
movie_api_id = request.args.get("id")
if movie_api_id:
params = {"language": "en-US"}
response = requests.get(TMDB_DETAILS_URL + f"{movie_api_id}", params=params, headers=headers)
data = response.json()
new_movie = Movie(
title= data["title"],
year=data["release_date"].split("-")[0],
img_url=f"{TMDB_IMAGE_BASE_URL}{data['poster_path']}",
description=data["overview"]
)
db.session.add(new_movie)
db.session.commit()
return redirect(url_for('rate_movie', id=new_movie.id))
if __name__ == '__main__':
app.run(debug=True)
๐๏ธ base.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
{% block styles %}
<!-- Load Bootstrap-Flask CSS here -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
<!-- Link to the styles.css here to apply styling to all the child templates.-->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Nunito+Sans:ital,opsz,wght@0,6..12,200..1000;1,6..12,200..1000&family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap" rel="stylesheet">
<script src="https://kit.fontawesome.com/cfaaaf681d.js" crossorigin="anonymous"></script>
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}"/>
{% endblock %}
<title>{% block title %}{% endblock %}</title>
</head>
<body>
{% block content %}{% endblock %}
</body>
</html>
๐๏ธ index.html
{% extends 'base.html' %}
{% block title %}My Top 10 Movies{% endblock %}
{% block content %}
<div class="container">
<h1 class="heading">My Top 10 Movies</h1>
<p class="description">These are my all-time favourite movies.</p>
{% for movie in movies %}
<div class="card" >
<div class="front" style="background-image: url('{{ movie.img_url }}');">
<p class="large">{{ movie.ranking }}</p>
</div>
<div class="back">
<div>
<div class="title">{{ movie.title }} <span class="release_date">({{ movie.year }})</span></div>
<div class="rating">
<label>{{ movie.rating }}</label>
<i class="fas fa-star star"></i>
</div>
<p class="review">"{{ movie.review }}"</p>
<p class="overview">{{ movie.description }}</p>
<a href="{{ url_for('rate_movie', id=movie.id) }}" class="button">Update</a>
<a href="{{ url_for('delete', id=movie.id) }}" class="button delete-button">Delete</a>
</div>
</div>
</div>
{% endfor %}
</div>
<div class="container text-center add">
<a href="{{ url_for('add') }}" class="button">Add Movie</a>
</div>
{% endblock %}
๐๏ธ edit.html
{% extends 'base.html' %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Edit Movies{% endblock %}
{% block content %}
<div class="content">
<h1 class="heading">{{ movie.title }}</h1>
<p class="description">Edit Movie Rating</p>
{{ wtf.quick_form(form, novalidate=True) }}
</div>
{% endblock %}
๐๏ธ add.html
{% extends 'base.html' %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Add Movie{% endblock %}
{% block content %}
<div class="content">
<h1 class="heading">Add a Movie</h1>
{{ wtf.quick_form(form, novalidate=True) }}
</div>
{% endblock %}
๐๏ธ select.html
{% extends 'base.html' %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Select Movie{% endblock %}
{% block content %}
<div class="container">
<h1 class="heading">Select Movie</h1>
{% for movie in options %}
<p>
<a href="{{ url_for('find', id=movie.id) }}">
{{ movie.title }} - {{ movie.release_date }}
</a>
</p>
{% endfor %}
</div>
{% endblock %}