Ver2.0 ๐ Python ์น ์ฑ์ผ๋ก ๋ง๋๋ ๋๋ง์ ์ด๊ฐ๋จ ์จ๋ผ์ธ ์ค๋ฌธ ์กฐ์ฌ ์์คํ : ์ฝ๋ ํ ์ค๋ถํฐ ๋ฐฐํฌ๊น์ง! ๐

๐ Python ์น ์ฑ์ผ๋ก ๋ง๋๋ ๋๋ง์ ์ด๊ฐ๋จ ์จ๋ผ์ธ ์ค๋ฌธ ์กฐ์ฌ ์์คํ : ์ฝ๋ ํ ์ค๋ถํฐ ๋ฐฐํฌ๊น์ง! ๐
ํ์ด์ฌ์ ๋ง๋ฒ์ผ๋ก ์ค๋ฌธ์กฐ์ฌ ์์คํ ์ ์ง์ ๋ง๋ค์ด๋ณด๋ ์์ ์ ๋ณต ๊ฐ์ด๋
์๋ ํ์ธ์ ์ฌ๋ฌ๋ถ! ์ค๋์ ์ง์ง ๊ฟ์ผ ์ฃผ์ ๋ก ์ฐพ์์์ด์~ ๐ฏ Python์ผ๋ก ๋๋ง์ ์ค๋ฌธ์กฐ์ฌ ์์คํ ์ ๋๋ฑ ๋ง๋ค์ด๋ณด๋ ์๊ฐ์ ๊ฐ์ ธ๋ณผ๊ฒ์! ์์ฆ ์ค๋ฌธ์กฐ์ฌ ๋ง์ด ํ์์ฃ ? ํ๊ต ๊ณผ์ , ํ์ฌ ํ๋ก์ ํธ, ์ฌ์ง์ด ์น๊ตฌ๋ค ์ทจํฅ ํ์ ํ ๋๋ ์ค๋ฌธ์กฐ์ฌ ํ ๋ฒ์ฉ ๋๋ฆฌ๊ฒ ๋๋๋ฐ... ๊ธฐ์กด ์๋น์ค ์ฐ๊ธฐ ์ง๊ฒน๋ค๋ฉด? ๋ด ๋ง๋๋ก ์ปค์คํ ํ๊ณ ์ถ๋ค๋ฉด? ์ง์ ๋ง๋ค์ด๋ฒ๋ฆฌ๋ ๊ฑฐ ์ด๋จ๊น์? ใ ใ ใ
์ด ๊ธ์์๋ ํ์ด์ฌ ์ด๋ณด์๋ ๋ฐ๋ผํ ์ ์๊ฒ Flask ์น ํ๋ ์์ํฌ๋ฅผ ํ์ฉํด ์ค๋ฌธ์กฐ์ฌ ์์คํ ์ ์ฒ์๋ถํฐ ๋๊น์ง ๋ง๋ค์ด๋ณด๋ ๊ณผ์ ์ ์๋ ค๋๋ฆด๊ฒ์! ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๋๋ถํฐ ์ค์๊ฐ ๊ฒฐ๊ณผ ์๊ฐํ๊น์ง, ์ง์ง ์ค๋ฌด์์ ์ธ ์ ์๋ ์์ค์ผ๋ก์! ๐
์ฐธ๊ณ ๋ก ์ด๋ฐ ๊ฐ๋ฐ ์คํฌ์ ์์ฆ ์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์๋ ์ธ๊ธฐ ๋ง์ ์ด๋ผ๊ณ ํด์. ์ค์ ๋ก ์ฌ๋ฅ๋ท์์ Python ์น ๊ฐ๋ฐ ๊ด๋ จ ์ฌ๋ฅ์ด ๋ง์ด ๊ฑฐ๋๋๋ค๊ณ ํ๋, ์ด ๊ธ์ ํตํด ๋ฐฐ์ด ๋ด์ฉ์ผ๋ก ๋ถ์ ๊น์ง ๋ ธ๋ ค๋ณผ ์ ์๊ฒ ์ฃ ? ์ผ์์ด์กฐ! ๐
๐ ๋ชฉ์ฐจ
- ์ค๋ฌธ์กฐ์ฌ ์์คํ ์ ์ดํด์ ๊ธฐํํ๊ธฐ
- ๊ฐ๋ฐ ํ๊ฒฝ ์ค์ ํ๊ธฐ
- Flask๋ก ์น ์ ํ๋ฆฌ์ผ์ด์ ๋ผ๋ ๋ง๋ค๊ธฐ
- ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ค๊ณ ๋ฐ ์ฐ๋ํ๊ธฐ
- ์ค๋ฌธ ์์ฑ ๊ธฐ๋ฅ ๊ตฌํํ๊ธฐ
- ์๋ต ์์ง ํ์ด์ง ๋ง๋ค๊ธฐ
- ๊ฒฐ๊ณผ ๋ถ์ ๋ฐ ์๊ฐํ ๊ตฌํํ๊ธฐ
- ์ฌ์ฉ์ ์ธ์ฆ ์์คํ ์ถ๊ฐํ๊ธฐ
- ๋ฐฐํฌ ๋ฐ ์ด์ํ๊ธฐ
- ํ์ฅ ๋ฐ ์ปค์คํฐ๋ง์ด์ง ์์ด๋์ด
1. ์ค๋ฌธ์กฐ์ฌ ์์คํ ์ ์ดํด์ ๊ธฐํํ๊ธฐ ๐ฏ
์ผ๋จ ๋ญ๋ ๋ง๋ค๊ธฐ ์ ์ "์ด๊ฒ ๋ญ๋ฐ?" ํ๋ ์ดํด๊ฐ ํ์ํ๊ฒ ์ฃ ? ์ค๋ฌธ์กฐ์ฌ ์์คํ ์ด ์ด๋ค ๊ธฐ๋ฅ์ ํด์ผ ํ๋์ง ํ์คํ ์๊ณ ์์ํด๋ด ์๋ค!
1.1 ์ค๋ฌธ์กฐ์ฌ ์์คํ ์ ํต์ฌ ๊ธฐ๋ฅ
์ค๋ฌธ์กฐ์ฌ ์์คํ ์ ํฌ๊ฒ ์ธ ํํธ๋ก ๋๋ ์ ์์ด์:
- ์ค๋ฌธ ์ ์ ํํธ: ์ง๋ฌธ ๋ง๋ค๊ณ , ์ต์ ์ค์ ํ๊ณ , ์ค๋ฌธ์ง ๋์์ธํ๋ ๋ถ๋ถ
- ์๋ต ์์ง ํํธ: ์ฌ์ฉ์๋ค์ด ์ค๋ฌธ์ ๋ต๋ณํ๋ ์ธํฐํ์ด์ค
- ๊ฒฐ๊ณผ ๋ถ์ ํํธ: ์์ง๋ ๋ฐ์ดํฐ๋ฅผ ๋ถ์ํ๊ณ ์๊ฐํํ๋ ๋ถ๋ถ
์ฐ๋ฆฌ๊ฐ ๋ง๋ค ์์คํ ์ ์ด ์ธ ๊ฐ์ง๋ฅผ ๋ค ํฌํจํ ๊ฑฐ์์. ์ง์ง ๊ตฌ๊ธ ํผ์ค ๋บจ์น๋ ๊ธฐ๋ฅ์ ๊ตฌํํด๋ณผ ๊ฑฐ๋๊น ๊ธฐ๋ํ์ ๋ ์ข์์! ใ ใ ใ
1.2 ์์คํ ๊ธฐํํ๊ธฐ
๊ฐ๋ฐ ์์ ์ ์ ๊ฐ๋จํ ๊ธฐํ์ ํด๋ณผ๊ฒ์. ์ฐ๋ฆฌ ์์คํ ์ ์ด๋ฐ ๊ธฐ๋ฅ๋ค์ ๊ฐ์ง ๊ฑฐ์์:
- ๋ค์ํ ์ง๋ฌธ ์ ํ ์ง์ (๊ฐ๊ด์, ์ฃผ๊ด์, ์ฒดํฌ๋ฐ์ค ๋ฑ)
- ์ค๋ฌธ ๊ฒฐ๊ณผ ์ค์๊ฐ ๋์๋ณด๋
- ๋ชจ๋ฐ์ผ ์นํ์ ๋์์ธ
- ์ฌ์ฉ์ ์ธ์ฆ ์์คํ
- ์ค๋ฌธ ๊ณต์ ๊ธฐ๋ฅ
์ ๋ค์ด์ด๊ทธ๋จ์ ๋ณด๋ฉด ์ฐ๋ฆฌ๊ฐ ๋ง๋ค ์์คํ ์ ์ ์ฒด ๊ตฌ์กฐ๊ฐ ํ๋์ ๋ค์ด์ค์ฃ ? ์ด์ ์ด๊ฑธ ์ค์ ๋ก ๊ตฌํํด๋ณผ ์ฐจ๋ก์์! ๐ค
1.3 ๊ธฐ์ ์คํ ์ ํํ๊ธฐ
์ฐ๋ฆฌ๋ ์ด๋ฐ ๊ธฐ์ ๋ค์ ์ฌ์ฉํ ๊ฑฐ์์:
- ๋ฐฑ์๋: Python + Flask (๊ฐ๋ณ๊ณ ๋ฐฐ์ฐ๊ธฐ ์ฌ์์ ๊ตฟ!)
- ๋ฐ์ดํฐ๋ฒ ์ด์ค: SQLite (๊ฐ๋ฐ์ฉ) / PostgreSQL (์ค์ ๋ฐฐํฌ์ฉ)
- ํ๋ก ํธ์๋: HTML, CSS, JavaScript + Chart.js (์๊ฐํ์ฉ)
- ๋ฐฐํฌ: Heroku ๋๋ PythonAnywhere (์ด๋ณด์ ์นํ์ )
์ด ๊ธฐ์ ์คํ์ ์ด๋ณด์๋ ์ฝ๊ฒ ๋ฐ๋ผํ ์ ์์ผ๋ฉด์๋, ์ค์ ์๋น์ค๋ก ํ์ฅ ๊ฐ๋ฅํ ๊ตฌ์ฑ์ด์์. ๋ฌผ๋ก ๋ ๋ณต์กํ ๊ธฐ๋ฅ์ ์ํ๋ค๋ฉด Django๋ React ๊ฐ์ ๊ฒ๋ ๊ณ ๋ คํด๋ณผ ์ ์์ง๋ง, ์ผ๋จ ๊ธฐ๋ณธ๋ถํฐ ์์ํด๋ด์!
2. ๊ฐ๋ฐ ํ๊ฒฝ ์ค์ ํ๊ธฐ ๐ ๏ธ
์, ์ด์ ์ง์ง ์ฝ๋ฉ์ ์์ํด๋ณผ๊น์? ๋จผ์ ๊ฐ๋ฐ ํ๊ฒฝ๋ถํฐ ์ธํ ํด๋ด ์๋ค!
2.1 Python ์ค์นํ๊ธฐ
๋น์ฐํ Python์ด ํ์ํ๊ฒ ์ฃ ? ์ต์ ๋ฒ์ (3.8 ์ด์)์ ์ค์นํ๋ ๊ฑธ ์ถ์ฒํด์.
Python ๊ณต์ ์ฌ์ดํธ(https://www.python.org/downloads/)์์ ๋ค์ด๋ก๋ ๋ฐ์ผ์ค ์ ์์ด์. ์ค์นํ ๋ 'Add Python to PATH' ์ต์ ๊ผญ ์ฒดํฌํ์ธ์! ์ ๊ทธ๋ฌ๋ฉด ๋์ค์ ๋จธ๋ฆฌ ์ํ์... ใ ใ ใ
2.2 ๊ฐ์ํ๊ฒฝ ๋ง๋ค๊ธฐ
ํ๋ก์ ํธ๋ง๋ค ๋ณ๋์ ํ๊ฒฝ์ ๋ง๋๋ ๊ฑด ์ง์ง ๊ฟํ์ด์์! ๋์ค์ ๋ค๋ฅธ ํ๋ก์ ํธ๋ ํจํค์ง ์ถฉ๋ ์์ด ๊น๋ํ๊ฒ ๊ด๋ฆฌํ ์ ์๊ฑฐ๋ ์.
ํฐ๋ฏธ๋(๋งฅ/๋ฆฌ๋ ์ค) ๋๋ ๋ช ๋ น ํ๋กฌํํธ(์๋์ฐ)๋ฅผ ์ด๊ณ ๋ค์ ๋ช ๋ น์ด๋ฅผ ์คํํด๋ด ์๋ค:
# ํ๋ก์ ํธ ํด๋ ๋ง๋ค๊ธฐ
mkdir survey_app
cd survey_app
# ๊ฐ์ํ๊ฒฝ ๋ง๋ค๊ธฐ
python -m venv venv
# ๊ฐ์ํ๊ฒฝ ํ์ฑํ (์๋์ฐ)
venv\Scripts\activate
# ๊ฐ์ํ๊ฒฝ ํ์ฑํ (๋งฅ/๋ฆฌ๋
์ค)
source venv/bin/activate
๊ฐ์ํ๊ฒฝ์ด ํ์ฑํ๋๋ฉด ํฐ๋ฏธ๋ ์์ (venv)๋ผ๊ณ ํ์๋ ๊ฑฐ์์. ์ด์ ์ด ํ๊ฒฝ ์์์๋ง ํจํค์ง๋ฅผ ์ค์นํ๋ฉด ๋ฉ๋๋ค! ๐
2.3 ํ์ํ ํจํค์ง ์ค์นํ๊ธฐ
์ฐ๋ฆฌ ํ๋ก์ ํธ์ ํ์ํ ํจํค์ง๋ค์ ์ค์นํด๋ณผ๊ฒ์:
# ๊ธฐ๋ณธ ์น ํ๋ ์์ํฌ ๋ฐ ๋ฐ์ดํฐ๋ฒ ์ด์ค ๊ด๋ จ
pip install flask flask-sqlalchemy flask-migrate flask-login
# ํผ ์ฒ๋ฆฌ ๋ฐ ๋ณด์
pip install flask-wtf email_validator
# ์๊ฐํ ๊ด๋ จ
pip install matplotlib pandas
์ด ํจํค์ง๋ค์ด ๋ญํ๋ ๊ฑด์ง ๊ฐ๋จํ ์ค๋ช ๋๋ฆด๊ฒ์:
- Flask: ํ์ด์ฌ ์น ํ๋ ์์ํฌ์ ๋ํ์! ๊ฐ๋ณ๊ณ ํ์ฅ์ฑ ์ข์
- Flask-SQLAlchemy: ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฝ๊ฒ ๋ค๋ฃจ๋ ORM
- Flask-Migrate: DB ์คํค๋ง ๋ณ๊ฒฝ ๊ด๋ฆฌ ๋๊ตฌ
- Flask-Login: ์ฌ์ฉ์ ๋ก๊ทธ์ธ/์ธ์ฆ ๊ด๋ฆฌ
- Flask-WTF: ํผ ์ฒ๋ฆฌ ๋ฐ CSRF ๋ณด์
- Matplotlib & Pandas: ๋ฐ์ดํฐ ๋ถ์ ๋ฐ ์๊ฐํ
์ด ์ ๋๋ฉด ๊ธฐ๋ณธ์ ์ธ ์ค๋ฌธ์กฐ์ฌ ์์คํ ์ ๋ง๋ค๊ธฐ์ ์ถฉ๋ถํด์! ๐
2.4 ํ๋ก์ ํธ ๊ตฌ์กฐ ๋ง๋ค๊ธฐ
์ด์ ํ๋ก์ ํธ ํด๋ ๊ตฌ์กฐ๋ฅผ ๋ง๋ค์ด๋ณผ๊ฒ์. ๊น๋ํ ๊ตฌ์กฐ๋ ๋์ค์ ์ฝ๋ ๊ด๋ฆฌํ ๋ ์ง์ง ์ค์ํด์!
survey_app/
โโโ venv/ # ๊ฐ์ํ๊ฒฝ (์ด๋ฏธ ๋ง๋ค์์ฃ ?)
โโโ app/ # ์ ํ๋ฆฌ์ผ์ด์
์ฝ๋
โ โโโ __init__.py # ์ฑ ์ด๊ธฐํ
โ โโโ models.py # ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ชจ๋ธ
โ โโโ routes.py # ๋ผ์ฐํธ (URL ์ฒ๋ฆฌ)
โ โโโ forms.py # ํผ ํด๋์ค
โ โโโ static/ # CSS, JS, ์ด๋ฏธ์ง ๋ฑ
โ โโโ templates/ # HTML ํ
ํ๋ฆฟ
โโโ migrations/ # ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ง์ด๊ทธ๋ ์ด์
โโโ config.py # ์ค์ ํ์ผ
โโโ run.py # ์ฑ ์คํ ์คํฌ๋ฆฝํธ
โโโ requirements.txt # ํจํค์ง ๋ชฉ๋ก
์ด ๊ตฌ์กฐ๋ฅผ ๋ฐ๋ผ ํด๋์ ํ์ผ๋ค์ ๋ง๋ค์ด์ฃผ์ธ์. ํฐ๋ฏธ๋์์ ๋ค์ ๋ช ๋ น์ด๋ก ๋ง๋ค ์ ์์ด์:
# app ํด๋์ ๊ธฐ๋ณธ ํ์ผ ์์ฑ
mkdir -p app/static app/templates
touch app/__init__.py app/models.py app/routes.py app/forms.py
touch config.py run.py requirements.txt
์, ์ด์ ๊ธฐ๋ณธ ๊ตฌ์กฐ๊ฐ ์์ฑ๋์ด์! ๋ค์์ผ๋ก Flask ์ฑ์ ๋ผ๋๋ฅผ ๋ง๋ค์ด๋ณผ๊ฒ์.
3. Flask๋ก ์น ์ ํ๋ฆฌ์ผ์ด์ ๋ผ๋ ๋ง๋ค๊ธฐ ๐๏ธ
์ด์ ์ง์ง ์ฝ๋ฉ ์์์ด์์! Flask๋ก ์น ์ฑ์ ๊ธฐ๋ณธ ๋ผ๋๋ฅผ ๋ง๋ค์ด๋ด ์๋ค.
3.1 Flask ์ฑ ์ด๊ธฐํํ๊ธฐ
๋จผ์ app/__init__.py ํ์ผ์ Flask ์ฑ์ ์ด๊ธฐํํ๋ ์ฝ๋๋ฅผ ์์ฑํด๋ณผ๊ฒ์:
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_login import LoginManager
from config import Config
# ๋ฐ์ดํฐ๋ฒ ์ด์ค ๊ฐ์ฒด ์์ฑ
db = SQLAlchemy()
migrate = Migrate()
login = LoginManager()
login.login_view = 'auth.login'
def create_app(config_class=Config):
app = Flask(__name__)
app.config.from_object(config_class)
# ํ์ฅ ๊ธฐ๋ฅ ์ด๊ธฐํ
db.init_app(app)
migrate.init_app(app, db)
login.init_app(app)
# ๋ธ๋ฃจํ๋ฆฐํธ ๋ฑ๋ก
from app.main import bp as main_bp
app.register_blueprint(main_bp)
from app.auth import bp as auth_bp
app.register_blueprint(auth_bp, url_prefix='/auth')
from app.survey import bp as survey_bp
app.register_blueprint(survey_bp, url_prefix='/survey')
return app
from app import models # ๋ชจ๋ธ ์ํฌํธ
์ฐ์, ๋ญ๊ฐ ๋ณต์กํด ๋ณด์ด์ฃ ? ใ ใ ใ ํ์ง๋ง ๊ฑฑ์ ๋ง์ธ์! ํ๋์ฉ ์ค๋ช ํด๋๋ฆด๊ฒ์:
- ํ์ํ ํจํค์ง๋ค์ ์ํฌํธํ์ด์
- ๋ฐ์ดํฐ๋ฒ ์ด์ค, ๋ง์ด๊ทธ๋ ์ด์ , ๋ก๊ทธ์ธ ๊ด๋ฆฌ์ ๊ฐ์ฒด๋ฅผ ์์ฑํ์ด์
create_appํจ์๋ก ์ฑ์ ๋ง๋ค๊ณ ์ค์ ์ ์ ์ฉํ์ด์- ๋ธ๋ฃจํ๋ฆฐํธ๋ผ๋ ๊ฑธ ๋ฑ๋กํ๋๋ฐ, ์ด๊ฑด ์ฑ์ ๊ธฐ๋ฅ๋ณ๋ก ๋๋๋ ๋ฐฉ๋ฒ์ด์์
์ด๋ ๊ฒ ์ฑ์ ํจ์๋ก ๋ง๋๋ ํจํด์ '์ ํ๋ฆฌ์ผ์ด์ ํฉํ ๋ฆฌ'๋ผ๊ณ ํด์. ํ ์คํธํ๊ธฐ ์ข๊ณ ํ์ฅ์ฑ๋ ์ข์์ ์ค๋ฌด์์ ๋ง์ด ์ฐ๋ ๋ฐฉ์์ด์์!
3.2 ์ค์ ํ์ผ ๋ง๋ค๊ธฐ
์ด์ config.py ํ์ผ์ ์ฑ ์ค์ ์ ์ถ๊ฐํด๋ณผ๊ฒ์:
import os
from dotenv import load_dotenv
basedir = os.path.abspath(os.path.dirname(__file__))
load_dotenv(os.path.join(basedir, '.env'))
class Config:
SECRET_KEY = os.environ.get('SECRET_KEY') or '์ ๋-๋คํค๋ฉด-์๋๋-๋น๋ฐํค'
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
'sqlite:///' + os.path.join(basedir, 'app.db')
SQLALCHEMY_TRACK_MODIFICATIONS = False
์ด ์ค์ ํ์ผ์ ๋ ๊ฐ์ง ์ค์ํ ์ค์ ์ ๋ด๊ณ ์์ด์:
- SECRET_KEY: ์ธ์ ๋ฐ์ดํฐ ์ํธํ์ ์ฌ์ฉ๋๋ ๋น๋ฐ ํค
- SQLALCHEMY_DATABASE_URI: ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๊ฒฐ ์ ๋ณด
์ค์ ๋ฐฐํฌํ ๋๋ ํ๊ฒฝ ๋ณ์๋ก ์ด๋ฐ ๋ฏผ๊ฐํ ์ ๋ณด๋ฅผ ๊ด๋ฆฌํ๋ ๊ฒ ์ข์์. ์ง๊ธ์ ๊ฐ๋ฐ์ฉ์ผ๋ก ๊ธฐ๋ณธ๊ฐ์ ์ค์ ํด๋์ด์.
3.3 ๋ธ๋ฃจํ๋ฆฐํธ ๊ตฌ์กฐ ๋ง๋ค๊ธฐ
์์์ ๋ธ๋ฃจํ๋ฆฐํธ๋ฅผ ๋ฑ๋กํ๋๋ฐ, ์ด์ ๊ทธ ๊ตฌ์กฐ๋ฅผ ๋ง๋ค์ด๋ณผ๊ฒ์. ๋ธ๋ฃจํ๋ฆฐํธ๋ ์ฑ์ ๊ธฐ๋ฅ๋ณ๋ก ๋ชจ๋ํํ๋ ๋ฐฉ๋ฒ์ด์์.
# ํด๋ ๊ตฌ์กฐ ๋ง๋ค๊ธฐ
mkdir -p app/main app/auth app/survey
touch app/main/__init__.py app/main/routes.py
touch app/auth/__init__.py app/auth/routes.py app/auth/forms.py
touch app/survey/__init__.py app/survey/routes.py app/survey/forms.py
๊ฐ ๋ธ๋ฃจํ๋ฆฐํธ์ __init__.py ํ์ผ์ ๋ค์ ์ฝ๋๋ฅผ ์ถ๊ฐํด์ฃผ์ธ์:
# app/main/__init__.py
from flask import Blueprint
bp = Blueprint('main', __name__)
from app.main import routes
๋ค๋ฅธ ๋ธ๋ฃจํ๋ฆฐํธ๋ ๋น์ทํ๊ฒ ์ค์ ํ๋, ์ด๋ฆ๋ง 'auth'๋ 'survey'๋ก ๋ฐ๊ฟ์ฃผ์ธ์.
3.4 ์คํ ์คํฌ๋ฆฝํธ ๋ง๋ค๊ธฐ
run.py ํ์ผ์ ์ฑ์ ์คํํ๋ ์ฝ๋๋ฅผ ์ถ๊ฐํด๋ณผ๊ฒ์:
from app import create_app, db
from app.models import User, Survey, Question, Option, Response
app = create_app()
@app.shell_context_processor
def make_shell_context():
return {
'db': db,
'User': User,
'Survey': Survey,
'Question': Question,
'Option': Option,
'Response': Response
}
if __name__ == '__main__':
app.run(debug=True)
์ด ์คํฌ๋ฆฝํธ๋ ๋ ๊ฐ์ง ์ญํ ์ ํด์:
- ์ฑ์ ์์ฑํ๊ณ ์คํํด์
- Flask ์์์ ์ฌ์ฉํ ์ ์๋ ์ปจํ ์คํธ๋ฅผ ์ ๊ณตํด์ (๋๋ฒ๊น ํ ๋ ์ ์ฉํด์)
์ด์ ๊ธฐ๋ณธ ๊ตฌ์กฐ๊ฐ ์์ฑ๋์ด์! ๋ค์์ผ๋ก ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ชจ๋ธ์ ์ค๊ณํด๋ณผ๊ฒ์.
4. ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ค๊ณ ๋ฐ ์ฐ๋ํ๊ธฐ ๐พ
์ค๋ฌธ์กฐ์ฌ ์์คํ ์ ์ฌ์ฅ์ ๋ฐ๋ก ๋ฐ์ดํฐ๋ฒ ์ด์ค์์! ์ด๋ค ๋ฐ์ดํฐ๋ฅผ ์ด๋ป๊ฒ ์ ์ฅํ ์ง ์ ์ค๊ณํด์ผ ๋์ค์ ๊ณ ์ ์ ํด์~
4.1 ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ชจ๋ธ ์ค๊ณํ๊ธฐ
์ฐ๋ฆฌ ์ค๋ฌธ์กฐ์ฌ ์์คํ ์ ํ์ํ ๋ฐ์ดํฐ ๋ชจ๋ธ์ ์๊ฐํด๋ณผ๊น์?
- User: ์ฌ์ฉ์ ์ ๋ณด (์ด๋ฉ์ผ, ๋น๋ฐ๋ฒํธ ๋ฑ)
- Survey: ์ค๋ฌธ์ง ์ ๋ณด (์ ๋ชฉ, ์ค๋ช , ์์ฑ์ ๋ฑ)
- Question: ์ง๋ฌธ ์ ๋ณด (์ง๋ฌธ ๋ด์ฉ, ์ ํ ๋ฑ)
- Option: ๊ฐ๊ด์ ์ง๋ฌธ์ ์ ํ์ง
- Response: ์ฌ์ฉ์ ์๋ต ๋ฐ์ดํฐ
์ด ๋ชจ๋ธ๋ค ๊ฐ์ ๊ด๊ณ๋ฅผ ๋ค์ด์ด๊ทธ๋จ์ผ๋ก ํํํด๋ณผ๊ฒ์:
์ด์ ์ด ๋ชจ๋ธ๋ค์ ์ฝ๋๋ก ๊ตฌํํด๋ณผ๊ฒ์! app/models.py ํ์ผ์ ๋ค์ ์ฝ๋๋ฅผ ์์ฑํด์ฃผ์ธ์:
from datetime import datetime
from werkzeug.security import generate_password_hash, check_password_hash
from flask_login import UserMixin
from app import db, login
class User(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), index=True, unique=True)
email = db.Column(db.String(120), index=True, unique=True)
password_hash = db.Column(db.String(128))
surveys = db.relationship('Survey', backref='author', lazy='dynamic')
def set_password(self, password):
self.password_hash = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.password_hash, password)
def __repr__(self):
return f'<User {self.username}>'
@login.user_loader
def load_user(id):
return User.query.get(int(id))
class Survey(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(100), nullable=False)
description = db.Column(db.Text)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
is_active = db.Column(db.Boolean, default=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
questions = db.relationship('Question', backref='survey', lazy='dynamic',
cascade='all, delete-orphan')
def __repr__(self):
return f'<Survey {self.title}>'
class Question(db.Model):
id = db.Column(db.Integer, primary_key=True)
content = db.Column(db.Text, nullable=False)
type = db.Column(db.String(20), nullable=False) # 'text', 'radio', 'checkbox', 'dropdown'
required = db.Column(db.Boolean, default=False)
order = db.Column(db.Integer, default=0)
survey_id = db.Column(db.Integer, db.ForeignKey('survey.id'))
options = db.relationship('Option', backref='question', lazy='dynamic',
cascade='all, delete-orphan')
responses = db.relationship('Response', backref='question', lazy='dynamic',
cascade='all, delete-orphan')
def __repr__(self):
return f'<Question {self.id}: {self.content[:20]}...>'
class Option(db.Model):
id = db.Column(db.Integer, primary_key=True)
text = db.Column(db.String(200), nullable=False)
order = db.Column(db.Integer, default=0)
question_id = db.Column(db.Integer, db.ForeignKey('question.id'))
def __repr__(self):
return f'<Option {self.id}: {self.text}>'
class Response(db.Model):
id = db.Column(db.Integer, primary_key=True)
answer = db.Column(db.Text)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
question_id = db.Column(db.Integer, db.ForeignKey('question.id'))
respondent_id = db.Column(db.String(36)) # UUID for anonymous respondents
def __repr__(self):
return f'<Response {self.id} to question {self.question_id}>'
์ฐ์, ์ฝ๋๊ฐ ์ข ๊ธธ์ฃ ? ใ ใ ใ ํ์ง๋ง ๊ฐ ๋ชจ๋ธ์ด ํ๋ ์ผ์ ๊ฐ๋จํด์:
- User: ์ฌ์ฉ์ ์ ๋ณด์ ๋น๋ฐ๋ฒํธ ํด์ฑ ๊ธฐ๋ฅ
- Survey: ์ค๋ฌธ์ง ๊ธฐ๋ณธ ์ ๋ณด
- Question: ์ง๋ฌธ ๋ด์ฉ๊ณผ ์ ํ
- Option: ๊ฐ๊ด์ ์ง๋ฌธ์ ์ ํ์ง
- Response: ์๋ต์๋ค์ ๋ต๋ณ
SQLAlchemy์ relationship ๊ธฐ๋ฅ์ ์ฌ์ฉํด์ ๋ชจ๋ธ ๊ฐ์ ๊ด๊ณ๋ฅผ ์ค์ ํ์ด์. ์ด๋ ๊ฒ ํ๋ฉด ๋์ค์ 'survey.questions'์ฒ๋ผ ์ฝ๊ฒ ๊ด๋ จ ๋ฐ์ดํฐ์ ์ ๊ทผํ ์ ์์ด์!
4.2 ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ด๊ธฐํํ๊ธฐ
์ด์ ๋ชจ๋ธ์ ๋ง๋ค์์ผ๋ ์ค์ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ์ด๊ธฐํํด๋ณผ๊ฒ์. ํฐ๋ฏธ๋์์ ๋ค์ ๋ช ๋ น์ด๋ฅผ ์คํํด์ฃผ์ธ์:
# Flask ์ฑ ํ๊ฒฝ๋ณ์ ์ค์ (์๋์ฐ)
set FLASK_APP=run.py
# Flask ์ฑ ํ๊ฒฝ๋ณ์ ์ค์ (๋งฅ/๋ฆฌ๋
์ค)
export FLASK_APP=run.py
# ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ด๊ธฐํ
flask db init
flask db migrate -m "Initial migration"
flask db upgrade
์ด ๋ช ๋ น์ด๋ค์ด ํ๋ ์ผ์:
flask db init: ๋ง์ด๊ทธ๋ ์ด์ ์์คํ ์ด๊ธฐํflask db migrate: ๋ชจ๋ธ ๋ณ๊ฒฝ์ฌํญ ๊ฐ์งํด์ ๋ง์ด๊ทธ๋ ์ด์ ์คํฌ๋ฆฝํธ ์์ฑflask db upgrade: ์ค์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ๋ณ๊ฒฝ์ฌํญ ์ ์ฉ
์ด์ app.db ํ์ผ์ด ์์ฑ๋์์ ๊ฑฐ์์. ์ด๊ฒ ๋ฐ๋ก ์ฐ๋ฆฌ์ SQLite ๋ฐ์ดํฐ๋ฒ ์ด์ค์์! ๐
4.3 ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฟผ๋ฆฌ ํ ์คํธํ๊ธฐ
๋ฐ์ดํฐ๋ฒ ์ด์ค๊ฐ ์ ๋๋ก ์ค์ ๋๋์ง ํ ์คํธํด๋ณผ๊น์? Flask ์์ ์ฌ์ฉํ๋ฉด ํธํด์:
# Flask ์ ์คํ
flask shell
# ์์์ ๋ค์ ๋ช
๋ น์ด ์คํ
>>> user = User(username='test_user', email='test@example.com')
>>> user.set_password('password123')
>>> db.session.add(user)
>>> db.session.commit()
>>> User.query.all()
[<User test_user>]
>>> exit()
์ค ์ ๋๋ค์! ์ด์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ค์ ์ด ์๋ฃ๋์ด์. ๋ค์์ผ๋ก ์ค๋ฌธ ์์ฑ ๊ธฐ๋ฅ์ ๊ตฌํํด๋ณผ๊ฒ์!
5. ์ค๋ฌธ ์์ฑ ๊ธฐ๋ฅ ๊ตฌํํ๊ธฐ ๐
์ด์ ์ง์ง ํต์ฌ ๊ธฐ๋ฅ์ธ ์ค๋ฌธ ์์ฑ ๊ธฐ๋ฅ์ ๋ง๋ค์ด๋ณผ๊ฒ์! ์ฌ์ฉ์๊ฐ ๋ค์ํ ์ ํ์ ์ง๋ฌธ์ ์ถ๊ฐํ๊ณ ์ค๋ฌธ์ง๋ฅผ ๋ง๋ค ์ ์๊ฒ ํด๋ด ์๋ค.
5.1 ์ธ์ฆ ์์คํ ๊ตฌํํ๊ธฐ
์ค๋ฌธ์ ๋ง๋ค๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธ์ด ํ์ํ๊ฒ ์ฃ ? app/auth/forms.py ํ์ผ์ ๋ก๊ทธ์ธ/ํ์๊ฐ์
ํผ์ ๋ง๋ค์ด๋ณผ๊ฒ์:
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import DataRequired, Email, EqualTo, ValidationError
from app.models import User
class LoginForm(FlaskForm):
email = StringField('์ด๋ฉ์ผ', validators=[DataRequired(), Email()])
password = PasswordField('๋น๋ฐ๋ฒํธ', validators=[DataRequired()])
remember_me = BooleanField('๋ก๊ทธ์ธ ์ํ ์ ์ง')
submit = SubmitField('๋ก๊ทธ์ธ')
class RegistrationForm(FlaskForm):
username = StringField('์ฌ์ฉ์ ์ด๋ฆ', validators=[DataRequired()])
email = StringField('์ด๋ฉ์ผ', validators=[DataRequired(), Email()])
password = PasswordField('๋น๋ฐ๋ฒํธ', validators=[DataRequired()])
password2 = PasswordField(
'๋น๋ฐ๋ฒํธ ํ์ธ', validators=[DataRequired(), EqualTo('password')])
submit = SubmitField('ํ์๊ฐ์
')
def validate_username(self, username):
user = User.query.filter_by(username=username.data).first()
if user is not None:
raise ValidationError('์ด๋ฏธ ์ฌ์ฉ ์ค์ธ ์ฌ์ฉ์ ์ด๋ฆ์
๋๋ค.')
def validate_email(self, email):
user = User.query.filter_by(email=email.data).first()
if user is not None:
raise ValidationError('์ด๋ฏธ ์ฌ์ฉ ์ค์ธ ์ด๋ฉ์ผ์
๋๋ค.')
์ด์ app/auth/routes.py ํ์ผ์ ๋ก๊ทธ์ธ/ํ์๊ฐ์
๋ผ์ฐํธ๋ฅผ ์ถ๊ฐํด๋ณผ๊ฒ์:
from flask import render_template, redirect, url_for, flash, request
from flask_login import login_user, logout_user, current_user
from werkzeug.urls import url_parse
from app import db
from app.auth import bp
from app.auth.forms import LoginForm, RegistrationForm
from app.models import User
@bp.route('/login', methods=['GET', 'POST'])
def login():
if current_user.is_authenticated:
return redirect(url_for('main.index'))
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(email=form.email.data).first()
if user is None or not user.check_password(form.password.data):
flash('์ด๋ฉ์ผ ๋๋ ๋น๋ฐ๋ฒํธ๊ฐ ์ฌ๋ฐ๋ฅด์ง ์์ต๋๋ค')
return redirect(url_for('auth.login'))
login_user(user, remember=form.remember_me.data)
next_page = request.args.get('next')
if not next_page or url_parse(next_page).netloc != '':
next_page = url_for('main.index')
return redirect(next_page)
return render_template('auth/login.html', title='๋ก๊ทธ์ธ', form=form)
@bp.route('/logout')
def logout():
logout_user()
return redirect(url_for('main.index'))
@bp.route('/register', methods=['GET', 'POST'])
def register():
if current_user.is_authenticated:
return redirect(url_for('main.index'))
form = RegistrationForm()
if form.validate_on_submit():
user = User(username=form.username.data, email=form.email.data)
user.set_password(form.password.data)
db.session.add(user)
db.session.commit()
flash('์ถํํฉ๋๋ค! ํ์๊ฐ์
์ด ์๋ฃ๋์์ต๋๋ค.')
return redirect(url_for('auth.login'))
return render_template('auth/register.html', title='ํ์๊ฐ์
', form=form)
์ด์ app/templates/auth ํด๋๋ฅผ ๋ง๋ค๊ณ ๋ก๊ทธ์ธ/ํ์๊ฐ์
ํ
ํ๋ฆฟ์ ์ถ๊ฐํด์ผ ํด์. ๋จผ์ ๊ธฐ๋ณธ ๋ ์ด์์๋ถํฐ ๋ง๋ค์ด๋ณผ๊ฒ์.
app/templates/base.html:
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% if title %}{{ title }} - {% endif %}์ค๋ฌธ์กฐ์ฌ ์์คํ
</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container">
<a class="navbar-brand" href="{{ url_for('main.index') }}">์ค๋ฌธ์กฐ์ฌ ์์คํ
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link" href="{{ url_for('main.index') }}">ํ</a>
</li>
{% if current_user.is_authenticated %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('survey.create') }}">์ค๋ฌธ ๋ง๋ค๊ธฐ</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('survey.my_surveys') }}">๋ด ์ค๋ฌธ</a>
</li>
{% endif %}
</ul>
<ul class="navbar-nav">
{% if current_user.is_authenticated %}
<li class="nav-item">
<span class="nav-link">{{ current_user.username }}</span>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('auth.logout') }}">๋ก๊ทธ์์</a>
</li>
{% else %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('auth.login') }}">๋ก๊ทธ์ธ</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('auth.register') }}">ํ์๊ฐ์
</a>
</li>
{% endif %}
</ul>
</div>
</div>
</nav>
<div class="container mt-4">
{% with messages = get_flashed_messages() %}
{% if messages %}
<div class="row">
<div class="col-md-12">
{% for message in messages %}
<div class="alert alert-info">{{ message }}</div>
{% endfor %}
</div>
</div>
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
{% block scripts %}{% endblock %}
</body>
</html>
์ด์ ๋ก๊ทธ์ธ ํ
ํ๋ฆฟ์ ๋ง๋ค์ด๋ณผ๊ฒ์. app/templates/auth/login.html:
{% extends "base.html" %}
{% block content %}
<div class="row">
<div class="col-md-6 offset-md-3">
<div class="card">
<div class="card-header">
<h3 class="card-title">๋ก๊ทธ์ธ</h3>
</div>
<div class="card-body">
<form action="" method="post" novalidate>
{{ form.hidden_tag() }}
<div class="mb-3">
{{ form.email.label(class="form-label") }}
{{ form.email(class="form-control") }}
{% for error in form.email.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.password.label(class="form-label") }}
{{ form.password(class="form-control") }}
{% for error in form.password.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3 form-check">
{{ form.remember_me(class="form-check-input") }}
{{ form.remember_me.label(class="form-check-label") }}
</div>
<div class="mb-3">
{{ form.submit(class="btn btn-primary") }}
</div>
</form>
<p>๊ณ์ ์ด ์์ผ์ ๊ฐ์? <a href="{{ url_for('auth.register') }}">ํ์๊ฐ์
</a></p>
</div>
</div>
</div>
</div>
{% endblock %}
ํ์๊ฐ์
ํ
ํ๋ฆฟ๋ ๋น์ทํ๊ฒ ๋ง๋ค์ด์ฃผ์ธ์. ์ด์ ๋ฉ์ธ ํ์ด์ง๋ฅผ ๋ง๋ค์ด๋ณผ๊ฒ์. app/main/routes.py:
from flask import render_template, redirect, url_for
from app.main import bp
@bp.route('/')
@bp.route('/index')
def index():
return render_template('index.html', title='ํ')
app/templates/index.html:
{% extends "base.html" %}
{% block content %}
<div class="jumbotron">
<h1 class="display-4">๋๋ง์ ์ค๋ฌธ์กฐ์ฌ๋ฅผ ๋ง๋ค์ด๋ณด์ธ์!</h1>
<p class="lead">๊ฐ๋จํ๊ณ ๊ฐ๋ ฅํ ์ค๋ฌธ์กฐ์ฌ ์์คํ
์ผ๋ก ๋ฐ์ดํฐ๋ฅผ ์์งํ๊ณ ๋ถ์ํด๋ณด์ธ์.</p>
<hr class="my-4">
<p>์ง๊ธ ๋ฐ๋ก ์์ํ์ธ์!</p>
{% if current_user.is_authenticated %}
<a class="btn btn-primary btn-lg" href="{{ url_for('survey.create') }}">์ค๋ฌธ ๋ง๋ค๊ธฐ</a>
{% else %}
<a class="btn btn-primary btn-lg" href="{{ url_for('auth.register') }}">ํ์๊ฐ์
</a>
<a class="btn btn-outline-primary btn-lg" href="{{ url_for('auth.login') }}">๋ก๊ทธ์ธ</a>
{% endif %}
</div>
<div class="row mt-5">
<div class="col-md-4">
<h2>์ฝ๊ณ ๋น ๋ฅธ ์ค๋ฌธ ์ ์</h2>
<p>๋ค์ํ ์ง๋ฌธ ์ ํ๊ณผ ์ง๊ด์ ์ธ ์ธํฐํ์ด์ค๋ก ๋ช ๋ถ ์์ ์ ๋ฌธ์ ์ธ ์ค๋ฌธ์ ๋ง๋ค ์ ์์ต๋๋ค.</p>
</div>
<div class="col-md-4">
<h2>์ค์๊ฐ ์๋ต ํ์ธ</h2>
<p>์๋ต์ด ๋ค์ด์ค๋ ์ฆ์ ๊ฒฐ๊ณผ๋ฅผ ํ์ธํ๊ณ ์๊ฐ์ ๋์๋ณด๋๋ก ๋ฐ์ดํฐ๋ฅผ ๋ถ์ํ ์ ์์ต๋๋ค.</p>
</div>
<div class="col-md-4">
<h2>์ด๋์๋ ์ ๊ทผ ๊ฐ๋ฅ</h2>
<p>๋ชจ๋ฐ์ผ ์นํ์ ์ธ ๋์์ธ์ผ๋ก ์ด๋ค ๊ธฐ๊ธฐ์์๋ ์ค๋ฌธ์ ์๋ตํ ์ ์์ต๋๋ค.</p>
</div>
</div>
{% endblock %}
5.2 ์ค๋ฌธ ์์ฑ ํผ ๋ง๋ค๊ธฐ
์ด์ ์ค๋ฌธ์ ๋ง๋๋ ํผ์ ๊ตฌํํด๋ณผ๊ฒ์. app/survey/forms.py:
from flask_wtf import FlaskForm
from wtforms import StringField, TextAreaField, SelectField, BooleanField, SubmitField
from wtforms.validators import DataRequired, Length
class SurveyForm(FlaskForm):
title = StringField('์ค๋ฌธ ์ ๋ชฉ', validators=[DataRequired(), Length(max=100)])
description = TextAreaField('์ค๋ฌธ ์ค๋ช
')
is_active = BooleanField('ํ์ฑํ', default=True)
submit = SubmitField('์ ์ฅ')
class QuestionForm(FlaskForm):
content = TextAreaField('์ง๋ฌธ ๋ด์ฉ', validators=[DataRequired()])
type = SelectField('์ง๋ฌธ ์ ํ', choices=[
('text', '์ฃผ๊ด์ (ํ
์คํธ)'),
('radio', '๊ฐ๊ด์ (๋จ์ผ ์ ํ)'),
('checkbox', '๊ฐ๊ด์ (๋ค์ค ์ ํ)'),
('dropdown', '๋๋กญ๋ค์ด')
], validators=[DataRequired()])
required = BooleanField('ํ์ ์๋ต', default=False)
submit = SubmitField('์ง๋ฌธ ์ถ๊ฐ')
class OptionForm(FlaskForm):
text = StringField('์ ํ์ง ํ
์คํธ', validators=[DataRequired()])
submit = SubmitField('์ ํ์ง ์ถ๊ฐ')
์ด์ app/survey/routes.py ํ์ผ์ ์ค๋ฌธ ์์ฑ ๊ด๋ จ ๋ผ์ฐํธ๋ฅผ ์ถ๊ฐํด๋ณผ๊ฒ์:
from flask import render_template, redirect, url_for, flash, request, jsonify
from flask_login import login_required, current_user
from app import db
from app.survey import bp
from app.survey.forms import SurveyForm, QuestionForm, OptionForm
from app.models import Survey, Question, Option
@bp.route('/create', methods=['GET', 'POST'])
@login_required
def create():
form = SurveyForm()
if form.validate_on_submit():
survey = Survey(
title=form.title.data,
description=form.description.data,
is_active=form.is_active.data,
author=current_user
)
db.session.add(survey)
db.session.commit()
flash('์ค๋ฌธ์ด ์์ฑ๋์์ต๋๋ค! ์ด์ ์ง๋ฌธ์ ์ถ๊ฐํด์ฃผ์ธ์.')
return redirect(url_for('survey.edit', id=survey.id))
return render_template('survey/create.html', title='์ค๋ฌธ ๋ง๋ค๊ธฐ', form=form)
@bp.route('/edit/<int:id>', methods=['GET', 'POST'])
@login_required
def edit(id):
survey = Survey.query.get_or_404(id)
if survey.author != current_user:
flash('์ด ์ค๋ฌธ์ ํธ์งํ ๊ถํ์ด ์์ต๋๋ค.')
return redirect(url_for('main.index'))
# ์ค๋ฌธ ๊ธฐ๋ณธ ์ ๋ณด ์์ ํผ
survey_form = SurveyForm(obj=survey)
if survey_form.validate_on_submit():
survey_form.populate_obj(survey)
db.session.commit()
flash('์ค๋ฌธ์ด ์
๋ฐ์ดํธ๋์์ต๋๋ค.')
return redirect(url_for('survey.edit', id=survey.id))
# ์ง๋ฌธ ์ถ๊ฐ ํผ
question_form = QuestionForm()
# ์ง๋ฌธ ๋ชฉ๋ก ๊ฐ์ ธ์ค๊ธฐ
questions = survey.questions.order_by(Question.order).all()
return render_template('survey/edit.html',
title='์ค๋ฌธ ํธ์ง',
survey=survey,
survey_form=survey_form,
question_form=question_form,
questions=questions)
@bp.route('/add_question/<int:survey_id>', methods=['POST'])
@login_required
def add_question(survey_id):
survey = Survey.query.get_or_404(survey_id)
if survey.author != current_user:
flash('์ด ์ค๋ฌธ์ ์ง๋ฌธ์ ์ถ๊ฐํ ๊ถํ์ด ์์ต๋๋ค.')
return redirect(url_for('main.index'))
form = QuestionForm()
if form.validate_on_submit():
# ํ์ฌ ์ง๋ฌธ ๊ฐ์ ํ์ธํด์ ์์ ์ ํ๊ธฐ
order = survey.questions.count()
question = Question(
content=form.content.data,
type=form.type.data,
required=form.required.data,
order=order,
survey=survey
)
db.session.add(question)
db.session.commit()
flash('์ง๋ฌธ์ด ์ถ๊ฐ๋์์ต๋๋ค.')
# ๊ฐ๊ด์ ์ง๋ฌธ์ด๋ฉด ์ ํ์ง ์ถ๊ฐ ํ์ด์ง๋ก ์ด๋
if question.type in ['radio', 'checkbox', 'dropdown']:
return redirect(url_for('survey.add_options', question_id=question.id))
return redirect(url_for('survey.edit', id=survey_id))
@bp.route('/add_options/<int:question_id>', methods=['GET', 'POST'])
@login_required
def add_options(question_id):
question = Question.query.get_or_404(question_id)
survey = question.survey
if survey.author != current_user:
flash('์ด ์ง๋ฌธ์ ์ ํ์ง๋ฅผ ์ถ๊ฐํ ๊ถํ์ด ์์ต๋๋ค.')
return redirect(url_for('main.index'))
form = OptionForm()
if form.validate_on_submit():
# ํ์ฌ ์ ํ์ง ๊ฐ์ ํ์ธํด์ ์์ ์ ํ๊ธฐ
order = question.options.count()
option = Option(
text=form.text.data,
order=order,
question=question
)
db.session.add(option)
db.session.commit()
flash('์ ํ์ง๊ฐ ์ถ๊ฐ๋์์ต๋๋ค.')
return redirect(url_for('survey.add_options', question_id=question.id))
options = question.options.order_by(Option.order).all()
return render_template('survey/add_options.html',
title='์ ํ์ง ์ถ๊ฐ',
question=question,
survey=survey,
form=form,
options=options)
@bp.route('/my_surveys')
@login_required
def my_surveys():
surveys = current_user.surveys.order_by(Survey.created_at.desc()).all()
return render_template('survey/my_surveys.html', title='๋ด ์ค๋ฌธ', surveys=surveys)
์ด์ ์ค๋ฌธ ์์ฑ ํ
ํ๋ฆฟ์ ๋ง๋ค์ด๋ณผ๊ฒ์. app/templates/survey/create.html:
{% extends "base.html" %}
{% block content %}
<div class="row">
<div class="col-md-8 offset-md-2">
<div class="card">
<div class="card-header">
<h3 class="card-title">์ ์ค๋ฌธ ๋ง๋ค๊ธฐ</h3>
</div>
<div class="card-body">
<form action="" method="post" novalidate>
{{ form.hidden_tag() }}
<div class="mb-3">
{{ form.title.label(class="form-label") }}
{{ form.title(class="form-control") }}
{% for error in form.title.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.description.label(class="form-label") }}
{{ form.description(class="form-control", rows=4) }}
{% for error in form.description.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3 form-check">
{{ form.is_active(class="form-check-input") }}
{{ form.is_active.label(class="form-check-label") }}
</div>
<div class="mb-3">
{{ form.submit(class="btn btn-primary") }}
<a href="{{ url_for('main.index') }}" class="btn btn-secondary">์ทจ์</a>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
์ค๋ฌธ ํธ์ง ํ์ด์ง๋ ๋ง๋ค์ด๋ณผ๊ฒ์. app/templates/survey/edit.html:
{% extends "base.html" %}
{% block content %}
<div class="row mb-4">
<div class="col-md-8">
<h2>{{ survey.title }} <small class="text-muted">ํธ์ง</small></h2>
<p>{{ survey.description }}</p>
</div>
<div class="col-md-4 text-end">
<a href="{{ url_for('survey.my_surveys') }}" class="btn btn-outline-secondary">๋ด ์ค๋ฌธ ๋ชฉ๋ก</a>
</div>
</div>
<div class="row">
<div class="col-md-4">
<div class="card mb-4">
<div class="card-header">
<h4 class="card-title">์ค๋ฌธ ์ ๋ณด ์์ </h4>
</div>
<div class="card-body">
<form action="" method="post" novalidate>
{{ survey_form.hidden_tag() }}
<div class="mb-3">
{{ survey_form.title.label(class="form-label") }}
{{ survey_form.title(class="form-control") }}
{% for error in survey_form.title.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ survey_form.description.label(class="form-label") }}
{{ survey_form.description(class="form-control", rows=4) }}
{% for error in survey_form.description.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3 form-check">
{{ survey_form.is_active(class="form-check-input") }}
{{ survey_form.is_active.label(class="form-check-label") }}
</div>
<div class="mb-3">
{{ survey_form.submit(class="btn btn-primary") }}
</div>
</form>
</div>
</div>
<div class="card">
<div class="card-header">
<h4 class="card-title">์ง๋ฌธ ์ถ๊ฐ</h4>
</div>
<div class="card-body">
<form action="{{ url_for('survey.add_question', survey_id=survey.id) }}" method="post" novalidate>
{{ question_form.hidden_tag() }}
<div class="mb-3">
{{ question_form.content.label(class="form-label") }}
{{ question_form.content(class="form-control", rows=3) }}
{% for error in question_form.content.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ question_form.type.label(class="form-label") }}
{{ question_form.type(class="form-select") }}
{% for error in question_form.type.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3 form-check">
{{ question_form.required(class="form-check-input") }}
{{ question_form.required.label(class="form-check-label") }}
</div>
<div class="mb-3">
{{ question_form.submit(class="btn btn-success") }}
</div>
</form>
</div>
</div>
</div>
<div class="col-md-8">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h4 class="card-title mb-0">์ง๋ฌธ ๋ชฉ๋ก</h4>
<span class="badge bg-primary">{{ questions|length }}๊ฐ ์ง๋ฌธ</span>
</div>
<div class="card-body">
{% if questions %}
<div class="list-group">
{% for question in questions %}
<div class="list-group-item">
<div class="d-flex justify-content-between align-items-center">
<h5 class="mb-1">{{ question.content }}</h5>
<span class="badge {% if question.type == 'text' %}bg-info{% elif question.type == 'radio' %}bg-success{% elif question.type == 'checkbox' %}bg-warning{% else %}bg-secondary{% endif %}">
{{ question.type }}
</span>
</div>
<p class="mb-1 text-muted">
{% if question.required %}ํ์ ์๋ต{% else %}์ ํ ์๋ต{% endif %}
</p>
{% if question.type in ['radio', 'checkbox', 'dropdown'] %}
<div class="mt-2">
<strong>์ ํ์ง:</strong>
{% if question.options.count() > 0 %}
<ul class="mb-0">
{% for option in question.options %}
<li>{{ option.text }}</li>
{% endfor %}
</ul>
{% else %}
<p class="text-muted">์ ํ์ง๊ฐ ์์ต๋๋ค.</p>
{% endif %}
<a href="{{ url_for('survey.add_options', question_id=question.id) }}" class="btn btn-sm btn-outline-primary mt-2">์ ํ์ง ๊ด๋ฆฌ</a>
</div>
{% endif %}
</div>
{% endfor %}
</div>
{% else %}
<div class="alert alert-info">
์์ง ์ง๋ฌธ์ด ์์ต๋๋ค. ์ผ์ชฝ ํผ์์ ์ง๋ฌธ์ ์ถ๊ฐํด์ฃผ์ธ์.
</div>
{% endif %}
{% if questions %}
<div class="mt-4">
<a href="{{ url_for('survey.preview', id=survey.id) }}" class="btn btn-primary">์ค๋ฌธ ๋ฏธ๋ฆฌ๋ณด๊ธฐ</a>
<a href="{{ url_for('survey.share', id=survey.id) }}" class="btn btn-success">์ค๋ฌธ ๊ณต์ </a>
</div>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}
์ ํ์ง ์ถ๊ฐ ํ์ด์ง๋ ๋ง๋ค์ด๋ณผ๊ฒ์. app/templates/survey/add_options.html:
{% extends "base.html" %}
{% block content %}
<div class="row mb-4">
<div class="col-md-8">
<h2>์ ํ์ง ๊ด๋ฆฌ <small class="text-muted">{{ question.content }}</small></h2>
<p>
<a href="{{ url_for('survey.edit', id=survey.id) }}">โ ์ค๋ฌธ ํธ์ง์ผ๋ก ๋์๊ฐ๊ธฐ</a>
</p>
</div>
</div>
<div class="row">
<div class="col-md-5">
<div class="card">
<div class="card-header">
<h4 class="card-title">์ ํ์ง ์ถ๊ฐ</h4>
</div>
<div class="card-body">
<form action="" method="post" novalidate>
{{ form.hidden_tag() }}
<div class="mb-3">
{{ form.text.label(class="form-label") }}
{{ form.text(class="form-control") }}
{% for error in form.text.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.submit(class="btn btn-success") }}
</div>
</form>
</div>
</div>
</div>
<div class="col-md-7">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h4 class="card-title mb-0">์ ํ์ง ๋ชฉ๋ก</h4>
<span class="badge bg-primary">{{ options|length }}๊ฐ ์ ํ์ง</span>
</div>
<div class="card-body">
{% if options %}
<div class="list-group">
{% for option in options %}
<div class="list-group-item d-flex justify-content-between align-items-center">
<span>{{ option.text }}</span>
<a href="{{ url_for('survey.delete_option', id=option.id) }}" class="btn btn-sm btn-outline-danger" onclick="return confirm('์ ๋ง ์ญ์ ํ์๊ฒ ์ต๋๊น?')">์ญ์ </a>
</div>
{% endfor %}
</div>
{% else %}
<div class="alert alert-info">
์์ง ์ ํ์ง๊ฐ ์์ต๋๋ค. ์ผ์ชฝ ํผ์์ ์ ํ์ง๋ฅผ ์ถ๊ฐํด์ฃผ์ธ์.
</div>
{% endif %}
{% if options|length >= 2 %}
<div class="mt-4">
<a href="{{ url_for('survey.edit', id=survey.id) }}" class="btn btn-primary">์๋ฃ</a>
</div>
{% else %}
<div class="alert alert-warning mt-4">
์ต์ 2๊ฐ ์ด์์ ์ ํ์ง๋ฅผ ์ถ๊ฐํด์ฃผ์ธ์.
</div>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}
๋ด ์ค๋ฌธ ๋ชฉ๋ก ํ์ด์ง๋ ๋ง๋ค์ด๋ณผ๊ฒ์. app/templates/survey/my_surveys.html:
{% extends "base.html" %}
{% block content %}
<div class="row mb-4">
<div class="col-md-8">
<h2>๋ด ์ค๋ฌธ ๋ชฉ๋ก</h2>
</div>
<div class="col-md-4 text-end">
<a href="{{ url_for('survey.create') }}" class="btn btn-primary">์ ์ค๋ฌธ ๋ง๋ค๊ธฐ</a>
</div>
</div>
<div class="row">
<div class="col-md-12">
{% if surveys %}
<div class="card">
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>์ ๋ชฉ</th>
<th>์์ฑ์ผ</th>
<th>์ง๋ฌธ ์</th>
<th>์ํ</th>
<th>์๋ต ์</th>
<th>์์
</th>
</tr>
</thead>
<tbody>
{% for survey in surveys %}
<tr>
<td>{{ survey.title }}</td>
<td>{{ survey.created_at.strftime('%Y-%m-%d') }}</td>
<td>{{ survey.questions.count() }}</td>
<td>
{% if survey.is_active %}
<span class="badge bg-success">ํ์ฑ</span>
{% else %}
<span class="badge bg-secondary">๋นํ์ฑ</span>
{% endif %}
</td>
<td>
{% set response_count = namespace(value=0) %}
{% for question in survey.questions %}
{% set response_count.value = response_count.value + question.responses.count() %}
{% endfor %}
{{ response_count.value }}
</td>
<td>
<div class="btn-group">
<a href="{{ url_for('survey.edit', id=survey.id) }}" class="btn btn-sm btn-outline-primary">ํธ์ง</a>
<a href="{{ url_for('survey.share', id=survey.id) }}" class="btn btn-sm btn-outline-success">๊ณต์ </a>
<a href="{{ url_for('survey.results', id=survey.id) }}" class="btn btn-sm btn-outline-info">๊ฒฐ๊ณผ</a>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% else %}
<div class="alert alert-info">
์์ง ๋ง๋ ์ค๋ฌธ์ด ์์ต๋๋ค. ์์ '์ ์ค๋ฌธ ๋ง๋ค๊ธฐ' ๋ฒํผ์ ํด๋ฆญํด ์ฒซ ์ค๋ฌธ์ ๋ง๋ค์ด๋ณด์ธ์!
</div>
{% endif %}
</div>
</div>
{% endblock %}
์ด์ ๊ธฐ๋ณธ์ ์ธ ์ค๋ฌธ ์์ฑ ๊ธฐ๋ฅ์ด ์์ฑ๋์ด์! ๋ค์์ผ๋ก ์๋ต ์์ง ํ์ด์ง๋ฅผ ๋ง๋ค์ด๋ณผ๊ฒ์.
6. ์๋ต ์์ง ํ์ด์ง ๋ง๋ค๊ธฐ ๐
์ด์ ๋ง๋ ์ค๋ฌธ์ ์๋ต์ ๋ฐ์ ์ ์๋ ํ์ด์ง๋ฅผ ๋ง๋ค์ด๋ณผ๊ฒ์! ์ด ๋ถ๋ถ์ด ์ง์ง ์ค์ํ ๋ถ๋ถ์ด์ฃ ~ ์ฌ์ฉ์ ๊ฒฝํ์ ์ต๋ํ ์ข๊ฒ ๋ง๋ค์ด๋ด ์๋ค!
6.1 ์ค๋ฌธ ๊ณต์ ๊ธฐ๋ฅ ๊ตฌํํ๊ธฐ
๋จผ์ ์ค๋ฌธ์ ๊ณต์ ํ ์ ์๋ ๊ธฐ๋ฅ๋ถํฐ ๋ง๋ค์ด๋ณผ๊ฒ์. app/survey/routes.py์ ๋ค์ ๋ผ์ฐํธ๋ฅผ ์ถ๊ฐํด์ฃผ์ธ์:
import uuid
from datetime import datetime
@bp.route('/share/<int:id>')
@login_required
def share(id):
survey = Survey.query.get_or_404(id)
if survey.author != current_user:
flash('์ด ์ค๋ฌธ์ ๊ณต์ ํ ๊ถํ์ด ์์ต๋๋ค.')
return redirect(url_for('main.index'))
# ์ค๋ฌธ ์๋ต URL ์์ฑ
response_url = url_for('survey.respond', id=survey.id, _external=True)
return render_template('survey/share.html',
title='์ค๋ฌธ ๊ณต์ ',
survey=survey,
response_url=response_url)
์ด์ ๊ณต์ ํ์ด์ง ํ
ํ๋ฆฟ์ ๋ง๋ค์ด๋ณผ๊ฒ์. app/templates/survey/share.html:
{% extends "base.html" %}
{% block content %}
<div class="row mb-4">
<div class="col-md-8">
<h2>์ค๋ฌธ ๊ณต์ <small class="text-muted">{{ survey.title }}</small></h2>
<p>
<a href="{{ url_for('survey.edit', id=survey.id) }}">โ ์ค๋ฌธ ํธ์ง์ผ๋ก ๋์๊ฐ๊ธฐ</a>
</p>
</div>
</div>
<div class="row">
<div class="col-md-8 offset-md-2">
<div class="card">
<div class="card-header">
<h4 class="card-title">์ค๋ฌธ ์๋ต ๋งํฌ</h4>
</div>
<div class="card-body">
<p>์๋ ๋งํฌ๋ฅผ ๋ณต์ฌํ์ฌ ์๋ต์๋ค์๊ฒ ๊ณต์ ํ์ธ์:</p>
<div class="input-group mb-3">
<input type="text" class="form-control" id="response-url" value="{{ response_url }}" readonly>
<button class="btn btn-outline-primary" type="button" onclick="copyToClipboard()">๋ณต์ฌ</button>
</div>
<div class="alert alert-success mt-4">
<h5>QR ์ฝ๋๋ก ๊ณต์ ํ๊ธฐ</h5>
<p>์ค๋งํธํฐ์ผ๋ก ์ฝ๊ฒ ์ ๊ทผํ ์ ์๋๋ก QR ์ฝ๋๋ฅผ ์ ๊ณตํฉ๋๋ค:</p>
<img src="https://api.qrserver.com/v1/create-qr-code/?size=200x200&data={{ response_url|urlencode }}" alt="QR Code" class="img-thumbnail">
</div>
<div class="alert alert-info mt-4">
<h5>์์
๋ฏธ๋์ด๋ก ๊ณต์ ํ๊ธฐ</h5>
<div class="d-flex gap-2 mt-3">
<a href="https://twitter.com/intent/tweet?url={{ response_url|urlencode }}&text={{ survey.title|urlencode }}" target="_blank" class="btn btn-outline-primary">Twitter</a>
<a href="https://www.facebook.com/sharer/sharer.php?u={{ response_url|urlencode }}" target="_blank" class="btn btn-outline-primary">Facebook</a>
<a href="mailto:?subject={{ survey.title|urlencode }}&body={{ survey.description|urlencode }}%0A%0A{{ response_url|urlencode }}" class="btn btn-outline-primary">Email</a>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
function copyToClipboard() {
var copyText = document.getElementById("response-url");
copyText.select();
copyText.setSelectionRange(0, 99999);
document.execCommand("copy");
alert("๋งํฌ๊ฐ ํด๋ฆฝ๋ณด๋์ ๋ณต์ฌ๋์์ต๋๋ค!");
}
</script>
{% endblock %}
6.2 ์ค๋ฌธ ์๋ต ํ์ด์ง ๊ตฌํํ๊ธฐ
์ด์ ์ค์ ๋ก ์๋ต์ ๋ฐ๋ ํ์ด์ง๋ฅผ ๋ง๋ค์ด๋ณผ๊ฒ์. app/survey/routes.py์ ๋ค์ ๋ผ์ฐํธ๋ฅผ ์ถ๊ฐํด์ฃผ์ธ์:
@bp.route('/respond/<int:id>', methods=['GET', 'POST'])
def respond(id):
survey = Survey.query.get_or_404(id)
# ๋นํ์ฑํ๋ ์ค๋ฌธ์ธ์ง ํ์ธ
if not survey.is_active:
flash('์ด ์ค๋ฌธ์ ํ์ฌ ๋นํ์ฑํ ์ํ์
๋๋ค.')
return redirect(url_for('main.index'))
# ์ง๋ฌธ ๋ชฉ๋ก ๊ฐ์ ธ์ค๊ธฐ
questions = survey.questions.order_by(Question.order).all()
if not questions:
flash('์ด ์ค๋ฌธ์๋ ์ง๋ฌธ์ด ์์ต๋๋ค.')
return redirect(url_for('main.index'))
if request.method == 'POST':
# ์๋ต์ ID ์์ฑ (์ต๋ช
์๋ต์ ์ํ UUID)
respondent_id = request.form.get('respondent_id')
if not respondent_id:
respondent_id = str(uuid.uuid4())
# ๊ฐ ์ง๋ฌธ์ ๋ํ ์๋ต ์ฒ๋ฆฌ
for question in questions:
answer = None
if question.type == 'text':
# ์ฃผ๊ด์ ์๋ต
answer = request.form.get(f'question_{question.id}')
elif question.type == 'radio':
# ๋จ์ผ ์ ํ ์๋ต
answer = request.form.get(f'question_{question.id}')
elif question.type == 'checkbox':
# ๋ค์ค ์ ํ ์๋ต
checkbox_answers = request.form.getlist(f'question_{question.id}')
if checkbox_answers:
answer = ','.join(checkbox_answers)
elif question.type == 'dropdown':
# ๋๋กญ๋ค์ด ์๋ต
answer = request.form.get(f'question_{question.id}')
# ํ์ ์ง๋ฌธ์ธ๋ฐ ์๋ต์ด ์๋ ๊ฒฝ์ฐ
if question.required and not answer:
flash(f'"{question.content}" ์ง๋ฌธ์ ํ์ ์๋ต์
๋๋ค.')
return redirect(url_for('survey.respond', id=survey.id))
# ์๋ต ์ ์ฅ
if answer:
response = Response(
answer=answer,
question_id=question.id,
respondent_id=respondent_id
)
db.session.add(response)
db.session.commit()
return redirect(url_for('survey.thank_you', id=survey.id))
# GET ์์ฒญ ์ฒ๋ฆฌ (์ค๋ฌธ ํผ ํ์)
respondent_id = str(uuid.uuid4()) # ์ ์๋ต์ ID ์์ฑ
return render_template('survey/respond.html',
title=survey.title,
survey=survey,
questions=questions,
respondent_id=respondent_id)
@bp.route('/thank_you/<int:id>')
def thank_you(id):
survey = Survey.query.get_or_404(id)
return render_template('survey/thank_you.html', title='์๋ต ์๋ฃ', survey=survey)
์ด์ ์๋ต ํ์ด์ง ํ
ํ๋ฆฟ์ ๋ง๋ค์ด๋ณผ๊ฒ์. app/templates/survey/respond.html:
{% extends "base.html" %}
{% block content %}
<div class="row mb-4">
<div class="col-md-8 offset-md-2">
<div class="card">
<div class="card-header">
<h2 class="card-title">{{ survey.title }}</h2>
</div>
<div class="card-body">
<p class="lead">{{ survey.description }}</p>
<hr>
<form action="" method="post" novalidate>
<input type="hidden" name="respondent_id" value="{{ respondent_id }}">
{% for question in questions %}
<div class="mb-4 p-3 border rounded {% if question.required %}border-danger{% endif %}">
<h5>
{{ question.content }}
{% if question.required %}
<span class="text-danger">*</span>
{% endif %}
</h5>
{% if question.type == 'text' %}
<!-- ์ฃผ๊ด์ ์ง๋ฌธ -->
<div class="mb-3">
<textarea class="form-control" name="question_{{ question.id }}" rows="3" {% if question.required %}required{% endif %}></textarea>
</div>
{% elif question.type == 'radio' %}
<!-- ๊ฐ๊ด์ ๋จ์ผ ์ ํ ์ง๋ฌธ -->
<div class="mb-3">
{% for option in question.options %}
<div class="form-check">
<input class="form-check-input" type="radio" name="question_{{ question.id }}" id="option_{{ option.id }}" value="{{ option.text }}" {% if question.required %}required{% endif %}>
<label class="form-check-label" for="option_{{ option.id }}">
{{ option.text }}
</label>
</div>
{% endfor %}
</div>
{% elif question.type == 'checkbox' %}
<!-- ๊ฐ๊ด์ ๋ค์ค ์ ํ ์ง๋ฌธ -->
<div class="mb-3">
{% for option in question.options %}
<div class="form-check">
<input class="form-check-input" type="checkbox" name="question_{{ question.id }}" id="option_{{ option.id }}" value="{{ option.text }}">
<label class="form-check-label" for="option_{{ option.id }}">
{{ option.text }}
</label>
</div>
{% endfor %}
</div>
{% elif question.type == 'dropdown' %}
<!-- ๋๋กญ๋ค์ด ์ง๋ฌธ -->
<div class="mb-3">
<select class="form-select" name="question_{{ question.id }}" {% if question.required %}required{% endif %}>
<option value="">-- ์ ํํ์ธ์ --</option>
{% for option in question.options %}
<option value="{{ option.text }}">{{ option.text }}</option>
{% endfor %}
</select>
</div>
{% endif %}
</div>
{% endfor %}
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary btn-lg">์๋ต ์ ์ถํ๊ธฐ</button>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
๊ฐ์ฌ ํ์ด์ง๋ ๋ง๋ค์ด๋ณผ๊ฒ์. app/templates/survey/thank_you.html:
{% extends "base.html" %}
{% block content %}
<div class="row">
<div class="col-md-8 offset-md-2 text-center">
<div class="card">
<div class="card-body">
<h1 class="display-4 text-success">๊ฐ์ฌํฉ๋๋ค!</h1>
<p class="lead">{{ survey.title }} ์ค๋ฌธ์ ์๋ตํด์ฃผ์
์ ๊ฐ์ฌํฉ๋๋ค.</p>
<hr>
<p>์์คํ ์๊ฒฌ์ด ์ฑ๊ณต์ ์ผ๋ก ์ ์ถ๋์์ต๋๋ค.</p>
<div class="mt-4">
<a href="{{ url_for('main.index') }}" class="btn btn-primary">ํ์ผ๋ก ๋์๊ฐ๊ธฐ</a>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
์ด์ ์ค๋ฌธ ์๋ต ๊ธฐ๋ฅ์ด ์์ฑ๋์ด์! ๋ค์์ผ๋ก ๊ฒฐ๊ณผ๋ฅผ ๋ถ์ํ๊ณ ์๊ฐํํ๋ ๊ธฐ๋ฅ์ ๊ตฌํํด๋ณผ๊ฒ์.
7. ๊ฒฐ๊ณผ ๋ถ์ ๋ฐ ์๊ฐํ ๊ตฌํํ๊ธฐ ๐
์ด์ ์์ง๋ ์ค๋ฌธ ๊ฒฐ๊ณผ๋ฅผ ๋ถ์ํ๊ณ ์๊ฐํํ๋ ๊ธฐ๋ฅ์ ๋ง๋ค์ด๋ณผ๊ฒ์! ์ด ๋ถ๋ถ์ด ์ง์ง ๊ฟ์ผ ํํธ์์~ ๋ฐ์ดํฐ๋ฅผ ์์๊ฒ ๋ณด์ฌ์ฃผ๋ฉด ์ฌ์ฉ์๋ค์ด ํํธํ ๊ฑฐ์์! ใ ใ ใ
7.1 ๊ฒฐ๊ณผ ํ์ด์ง ๊ตฌํํ๊ธฐ
app/survey/routes.py์ ๊ฒฐ๊ณผ ํ์ด์ง ๋ผ์ฐํธ๋ฅผ ์ถ๊ฐํด๋ณผ๊ฒ์:
from collections import Counter
import json
@bp.route('/results/<int:id>')
@login_required
def results(id):
survey = Survey.query.get_or_404(id)
if survey.author != current_user:
flash('์ด ์ค๋ฌธ์ ๊ฒฐ๊ณผ๋ฅผ ๋ณผ ๊ถํ์ด ์์ต๋๋ค.')
return redirect(url_for('main.index'))
# ์ง๋ฌธ ๋ชฉ๋ก ๊ฐ์ ธ์ค๊ธฐ
questions = survey.questions.order_by(Question.order).all()
# ๊ฐ ์ง๋ฌธ๋ณ ์๋ต ๋ฐ์ดํฐ ๋ถ์
results_data = []
for question in questions:
question_data = {
'id': question.id,
'content': question.content,
'type': question.type,
'total_responses': question.responses.count()
}
if question.type == 'text':
# ์ฃผ๊ด์ ์๋ต์ ํ
์คํธ ๋ชฉ๋ก์ผ๋ก ํ์
question_data['responses'] = [r.answer for r in question.responses]
elif question.type in ['radio', 'dropdown']:
# ๋จ์ผ ์ ํ ์๋ต์ ๊ฐ ์ต์
๋ณ ๊ฐ์ ๊ณ์ฐ
answers = [r.answer for r in question.responses]
counts = Counter(answers)
# ์ฐจํธ ๋ฐ์ดํฐ ์ค๋น
labels = list(counts.keys())
values = list(counts.values())
question_data['chart_data'] = {
'labels': labels,
'values': values
}
elif question.type == 'checkbox':
# ๋ค์ค ์ ํ ์๋ต์ ์ฝค๋ง๋ก ๋ถ๋ฆฌํ์ฌ ๊ฐ ์ต์
๋ณ ๊ฐ์ ๊ณ์ฐ
all_selections = []
for response in question.responses:
if response.answer:
selections = response.answer.split(',')
all_selections.extend(selections)
counts = Counter(all_selections)
# ์ฐจํธ ๋ฐ์ดํฐ ์ค๋น
labels = list(counts.keys())
values = list(counts.values())
question_data['chart_data'] = {
'labels': labels,
'values': values
}
results_data.append(question_data)
# ์ด ์๋ต์ ์ ๊ณ์ฐ (์ค๋ณต ์ ๊ฑฐ)
respondent_ids = set()
for question in questions:
for response in question.responses:
respondent_ids.add(response.respondent_id)
total_respondents = len(respondent_ids)
return render_template('survey/results.html',
title=f'{survey.title} - ๊ฒฐ๊ณผ',
survey=survey,
results_data=results_data,
total_respondents=total_respondents)
์ด์ ๊ฒฐ๊ณผ ํ์ด์ง ํ
ํ๋ฆฟ์ ๋ง๋ค์ด๋ณผ๊ฒ์. app/templates/survey/results.html:
{% extends "base.html" %}
{% block content %}
<div class="row mb-4">
<div class="col-md-8">
<h2>์ค๋ฌธ ๊ฒฐ๊ณผ <small class="text-muted">{{ survey.title }}</small></h2>
<p>
<a href="{{ url_for('survey.my_surveys') }}">โ ๋ด ์ค๋ฌธ ๋ชฉ๋ก์ผ๋ก ๋์๊ฐ๊ธฐ</a>
</p>
</div>
<div class="col-md-4 text-end">
<a href="{{ url_for('survey.export_results', id=survey.id) }}" class="btn btn-success">๊ฒฐ๊ณผ ๋ด๋ณด๋ด๊ธฐ (CSV)</a>
</div>
</div>
<div class="row mb-4">
<div class="col-md-12">
<div class="card">
<div class="card-body">
<div class="row">
<div class="col-md-4 text-center">
<h3 class="display-4">{{ total_respondents }}</h3>
<p class="text-muted">์ด ์๋ต์ ์</p>
</div>
<div class="col-md-4 text-center">
<h3 class="display-4">{{ survey.questions.count() }}</h3>
<p class="text-muted">์ด ์ง๋ฌธ ์</p>
</div>
<div class="col-md-4 text-center">
<h3 class="display-4">{{ survey.created_at.strftime('%Y-%m-%d') }}</h3>
<p class="text-muted">์ค๋ฌธ ์์ฑ์ผ</p>
</div>
</div>
</div>
</div>
</div>
</div>
{% for data in results_data %}
<div class="row mb-4">
<div class="col-md-12">
<div class="card">
<div class="card-header">
<h4 class="card-title">{{ data.content }}</h4>
<div class="text-muted">
{{ data.type }} ์ ํ | {{ data.total_responses }}๊ฐ ์๋ต
</div>
</div>
<div class="card-body">
{% if data.type == 'text' %}
<!-- ์ฃผ๊ด์ ์๋ต ํ์ -->
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>#</th>
<th>์๋ต ๋ด์ฉ</th>
</tr>
</thead>
<tbody>
{% for response in data.responses %}
<tr>
<td>{{ loop.index }}</td>
<td>{{ response }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% elif data.type in ['radio', 'dropdown', 'checkbox'] %}
<!-- ๊ฐ๊ด์ ์๋ต ์ฐจํธ ํ์ -->
<div class="row">
<div class="col-md-8">
<canvas id="chart_{{ data.id }}" width="400" height="200"></canvas>
</div>
<div class="col-md-4">
<table class="table table-sm">
<thead>
<tr>
<th>์ต์
</th>
<th>์๋ต ์</th>
<th>๋น์จ</th>
</tr>
</thead>
<tbody>
{% for i in range(data.chart_data.labels|length) %}
<tr>
<td>{{ data.chart_data.labels[i] }}</td>
<td>{{ data.chart_data.values[i] }}</td>
<td>
{% if data.total_responses > 0 %}
{{ (data.chart_data.values[i] / data.total_responses * 100)|round(1) }}%
{% else %}
0%
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
</div>
</div>
</div>
</div>
{% endfor %}
{% endblock %}
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
{% for data in results_data %}
{% if data.type in ['radio', 'dropdown', 'checkbox'] and data.chart_data %}
// ์ฐจํธ ์์ฑ
var ctx = document.getElementById('chart_{{ data.id }}').getContext('2d');
var chart = new Chart(ctx, {
type: '{{ 'pie' if data.type in ['radio', 'dropdown'] else 'bar' }}',
data: {
labels: {{ data.chart_data.labels|tojson }},
datasets: [{
label: '์๋ต ์',
data: {{ data.chart_data.values|tojson }},
backgroundColor: [
'rgba(255, 99, 132, 0.7)',
'rgba(54, 162, 235, 0.7)',
'rgba(255, 206, 86, 0.7)',
'rgba(75, 192, 192, 0.7)',
'rgba(153, 102, 255, 0.7)',
'rgba(255, 159, 64, 0.7)',
'rgba(199, 199, 199, 0.7)',
'rgba(83, 102, 255, 0.7)',
'rgba(40, 159, 64, 0.7)',
'rgba(210, 199, 199, 0.7)'
],
borderColor: [
'rgba(255, 99, 132, 1)',
'rgba(54, 162, 235, 1)',
'rgba(255, 206, 86, 1)',
'rgba(75, 192, 192, 1)',
'rgba(153, 102, 255, 1)',
'rgba(255, 159, 64, 1)',
'rgba(199, 199, 199, 1)',
'rgba(83, 102, 255, 1)',
'rgba(40, 159, 64, 1)',
'rgba(210, 199, 199, 1)'
],
borderWidth: 1
}]
},
options: {
responsive: true,
{% if data.type == 'checkbox' %}
scales: {
y: {
beginAtZero: true
}
},
{% endif %}
plugins: {
legend: {
position: 'right',
},
title: {
display: true,
text: '{{ data.content }}'
}
}
}
});
{% endif %}
{% endfor %}
});
</script>
{% endblock %}
7.2 ๊ฒฐ๊ณผ ๋ด๋ณด๋ด๊ธฐ ๊ธฐ๋ฅ ๊ตฌํํ๊ธฐ
CSV ํ์์ผ๋ก ๊ฒฐ๊ณผ๋ฅผ ๋ด๋ณด๋ด๋ ๊ธฐ๋ฅ๋ ์ถ๊ฐํด๋ณผ๊ฒ์. app/survey/routes.py์ ๋ค์ ๋ผ์ฐํธ๋ฅผ ์ถ๊ฐํด์ฃผ์ธ์:
import csv
from io import StringIO
from flask import make_response
@bp.route('/export_results/<int:id>')
@login_required
def export_results(id):
survey = Survey.query.get_or_404(id)
if survey.author != current_user:
flash('์ด ์ค๋ฌธ์ ๊ฒฐ๊ณผ๋ฅผ ๋ด๋ณด๋ผ ๊ถํ์ด ์์ต๋๋ค.')
return redirect(url_for('main.index'))
# CSV ํ์ผ ์์ฑ
output = StringIO()
writer = csv.writer(output)
# ํค๋ ํ ์์ฑ
questions = survey.questions.order_by(Question.order).all()
header = ['์๋ต์ ID', '์๋ต ์๊ฐ']
for question in questions:
header.append(f'Q{question.id}: {question.content}')
writer.writerow(header)
# ์๋ต์ ID ๋ชฉ๋ก ๊ฐ์ ธ์ค๊ธฐ
respondent_ids = set()
for question in questions:
for response in question.responses:
respondent_ids.add(response.respondent_id)
# ๊ฐ ์๋ต์๋ณ ๋ฐ์ดํฐ ์์ฑ
for respondent_id in respondent_ids:
row = [respondent_id]
# ์๋ต ์๊ฐ (ํด๋น ์๋ต์์ ์ฒซ ๋ฒ์งธ ์๋ต ์๊ฐ)
first_response = Response.query.filter_by(respondent_id=respondent_id).order_by(Response.created_at).first()
row.append(first_response.created_at.strftime('%Y-%m-%d %H:%M:%S') if first_response else '')
# ๊ฐ ์ง๋ฌธ์ ๋ํ ์๋ต
for question in questions:
response = Response.query.filter_by(question_id=question.id, respondent_id=respondent_id).first()
row.append(response.answer if response else '')
writer.writerow(row)
# CSV ํ์ผ ์๋ต ์์ฑ
output.seek(0)
response = make_response(output.getvalue())
response.headers['Content-Disposition'] = f'attachment; filename=survey_{survey.id}_results.csv'
response.headers['Content-type'] = 'text/csv'
return response
์ด์ ์ค๋ฌธ ๊ฒฐ๊ณผ๋ฅผ ๋ถ์ํ๊ณ ์๊ฐํํ๋ ๊ธฐ๋ฅ์ด ์์ฑ๋์ด์! ๋ค์์ผ๋ก ์ฌ์ฉ์ ์ธ์ฆ ์์คํ ์ ๋ ๊ฐ์ ํด๋ณผ๊ฒ์.
8. ์ฌ์ฉ์ ์ธ์ฆ ์์คํ ์ถ๊ฐํ๊ธฐ ๐
์ง๊ธ๊น์ง ๊ธฐ๋ณธ์ ์ธ ์ธ์ฆ ์์คํ ์ ๊ตฌํํ์ง๋ง, ์ข ๋ ๊ณ ๊ธ ๊ธฐ๋ฅ์ ์ถ๊ฐํด๋ณผ๊ฒ์! ๋น๋ฐ๋ฒํธ ์ฌ์ค์ , ํ๋กํ ๊ด๋ฆฌ ๋ฑ ์ค์ ์๋น์ค์์ ํ์ํ ๊ธฐ๋ฅ๋ค์ ๊ตฌํํด๋ด ์๋ค.
8.1 ์ฌ์ฉ์ ํ๋กํ ๊ด๋ฆฌ ๊ธฐ๋ฅ ๊ตฌํํ๊ธฐ
์ฌ์ฉ์๊ฐ ์์ ์ ํ๋กํ์ ๊ด๋ฆฌํ ์ ์๋ ๊ธฐ๋ฅ์ ์ถ๊ฐํด๋ณผ๊ฒ์. app/auth/forms.py์ ํ๋กํ ์์ ํผ์ ์ถ๊ฐํด์ฃผ์ธ์:
class ProfileForm(FlaskForm):
username = StringField('์ฌ์ฉ์ ์ด๋ฆ', validators=[DataRequired()])
email = StringField('์ด๋ฉ์ผ', validators=[DataRequired(), Email()])
current_password = PasswordField('ํ์ฌ ๋น๋ฐ๋ฒํธ')
new_password = PasswordField('์ ๋น๋ฐ๋ฒํธ')
new_password2 = PasswordField(
'์ ๋น๋ฐ๋ฒํธ ํ์ธ', validators=[EqualTo('new_password')])
submit = SubmitField('ํ๋กํ ์
๋ฐ์ดํธ')
def __init__(self, original_username, original_email, *args, **kwargs):
super(ProfileForm, self).__init__(*args, **kwargs)
self.original_username = original_username
self.original_email = original_email
def validate_username(self, username):
if username.data != self.original_username:
user = User.query.filter_by(username=username.data).first()
if user is not None:
raise ValidationError('์ด๋ฏธ ์ฌ์ฉ ์ค์ธ ์ฌ์ฉ์ ์ด๋ฆ์
๋๋ค.')
def validate_email(self, email):
if email.data != self.original_email:
user = User.query.filter_by(email=email.data).first()
if user is not None:
raise ValidationError('์ด๋ฏธ ์ฌ์ฉ ์ค์ธ ์ด๋ฉ์ผ์
๋๋ค.')
์ด์ app/auth/routes.py์ ํ๋กํ ๊ด๋ฆฌ ๋ผ์ฐํธ๋ฅผ ์ถ๊ฐํด์ฃผ์ธ์:
@bp.route('/profile', methods=['GET', 'POST'])
@login_required
def profile():
form = ProfileForm(current_user.username, current_user.email)
if form.validate_on_submit():
# ๋น๋ฐ๋ฒํธ ๋ณ๊ฒฝ์ ์ํ๋ ๊ฒฝ์ฐ
if form.current_password.data:
if not current_user.check_password(form.current_password.data):
flash('ํ์ฌ ๋น๋ฐ๋ฒํธ๊ฐ ์ฌ๋ฐ๋ฅด์ง ์์ต๋๋ค.')
return redirect(url_for('auth.profile'))
if form.new_password.data:
current_user.set_password(form.new_password.data)
flash('๋น๋ฐ๋ฒํธ๊ฐ ๋ณ๊ฒฝ๋์์ต๋๋ค.')
# ์ฌ์ฉ์ ์ ๋ณด ์
๋ฐ์ดํธ
current_user.username = form.username.data
current_user.email = form.email.data
db.session.commit()
flash('ํ๋กํ์ด ์
๋ฐ์ดํธ๋์์ต๋๋ค.')
return redirect(url_for('auth.profile'))
elif request.method == 'GET':
form.username.data = current_user.username
form.email.data = current_user.email
return render_template('auth/profile.html', title='ํ๋กํ ๊ด๋ฆฌ', form=form)
์ด์ ํ๋กํ ๊ด๋ฆฌ ํ
ํ๋ฆฟ์ ๋ง๋ค์ด๋ณผ๊ฒ์. app/templates/auth/profile.html:
{% extends "base.html" %}
{% block content %}
<div class="row">
<div class="col-md-8 offset-md-2">
<div class="card">
<div class="card-header">
<h3 class="card-title">ํ๋กํ ๊ด๋ฆฌ</h3>
</div>
<div class="card-body">
<form action="" method="post" novalidate>
{{ form.hidden_tag() }}
<div class="mb-3">
{{ form.username.label(class="form-label") }}
{{ form.username(class="form-control") }}
{% for error in form.username.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.email.label(class="form-label") }}
{{ form.email(class="form-control") }}
{% for error in form.email.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<hr>
<h4>๋น๋ฐ๋ฒํธ ๋ณ๊ฒฝ</h4>
<p class="text-muted">๋น๋ฐ๋ฒํธ๋ฅผ ๋ณ๊ฒฝํ๋ ค๋ฉด ์๋ ํญ๋ชฉ์ ์
๋ ฅํ์ธ์. ๋ณ๊ฒฝํ์ง ์์ผ๋ ค๋ฉด ๋น์๋์ธ์.</p>
<div class="mb-3">
{{ form.current_password.label(class="form-label") }}
{{ form.current_password(class="form-control") }}
{% for error in form.current_password.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.new_password.label(class="form-label") }}
{{ form.new_password(class="form-control") }}
{% for error in form.new_password.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.new_password2.label(class="form-label") }}
{{ form.new_password2(class="form-control") }}
{% for error in form.new_password2.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.submit(class="btn btn-primary") }}
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
8.2 ๋น๋ฐ๋ฒํธ ์ฌ์ค์ ๊ธฐ๋ฅ ๊ตฌํํ๊ธฐ
๋น๋ฐ๋ฒํธ๋ฅผ ์์ด๋ฒ๋ฆฐ ์ฌ์ฉ์๋ฅผ ์ํ ์ฌ์ค์ ๊ธฐ๋ฅ๋ ์ถ๊ฐํด๋ณผ๊ฒ์. ๋จผ์ app/auth/forms.py์ ํผ์ ์ถ๊ฐํด์ฃผ์ธ์:
class ResetPasswordRequestForm(FlaskForm):
email = StringField('์ด๋ฉ์ผ', validators=[DataRequired(), Email()])
submit = SubmitField('๋น๋ฐ๋ฒํธ ์ฌ์ค์ ์์ฒญ')
class ResetPasswordForm(FlaskForm):
password = PasswordField('์ ๋น๋ฐ๋ฒํธ', validators=[DataRequired()])
password2 = PasswordField(
'๋น๋ฐ๋ฒํธ ํ์ธ', validators=[DataRequired(), EqualTo('password')])
submit = SubmitField('๋น๋ฐ๋ฒํธ ์ฌ์ค์ ')
์ด๋ฉ์ผ ์ ์ก ๊ธฐ๋ฅ์ ์ํด app/email.py ํ์ผ์ ๋ง๋ค๊ณ ๋ค์ ์ฝ๋๋ฅผ ์ถ๊ฐํด์ฃผ์ธ์:
from flask import render_template, current_app
from flask_mail import Message
from app import mail
from threading import Thread
def send_async_email(app, msg):
with app.app_context():
mail.send(msg)
def send_email(subject, sender, recipients, text_body, html_body):
msg = Message(subject, sender=sender, recipients=recipients)
msg.body = text_body
msg.html = html_body
# ๋น๋๊ธฐ๋ก ์ด๋ฉ์ผ ์ ์ก (๋ฐฑ๊ทธ๋ผ์ด๋ ์์
)
Thread(target=send_async_email,
args=(current_app._get_current_object(), msg)).start()
def send_password_reset_email(user):
token = user.get_reset_password_token()
send_email(
'[์ค๋ฌธ์กฐ์ฌ ์์คํ
] ๋น๋ฐ๋ฒํธ ์ฌ์ค์ ',
sender=current_app.config['MAIL_DEFAULT_SENDER'],
recipients=[user.email],
text_body=render_template('email/reset_password.txt',
user=user, token=token),
html_body=render_template('email/reset_password.html',
user=user, token=token)
)
app/models.py์ ๋น๋ฐ๋ฒํธ ์ฌ์ค์ ํ ํฐ ๊ด๋ จ ๋ฉ์๋๋ฅผ ์ถ๊ฐํด์ฃผ์ธ์:
from time import time
import jwt
from flask import current_app
class User(UserMixin, db.Model):
# ๊ธฐ์กด ์ฝ๋๋ ๊ทธ๋๋ก ๋๊ณ ์๋ ๋ฉ์๋๋ง ์ถ๊ฐ
def get_reset_password_token(self, expires_in=600):
return jwt.encode(
{'reset_password': self.id, 'exp': time() + expires_in},
current_app.config['SECRET_KEY'],
algorithm='HS256'
)
@staticmethod
def verify_reset_password_token(token):
try:
id = jwt.decode(
token,
current_app.config['SECRET_KEY'],
algorithms=['HS256']
)['reset_password']
except:
return None
return User.query.get(id)
์ด์ app/auth/routes.py์ ๋น๋ฐ๋ฒํธ ์ฌ์ค์ ๋ผ์ฐํธ๋ฅผ ์ถ๊ฐํด์ฃผ์ธ์:
from app.email import send_password_reset_email
@bp.route('/reset_password_request', methods=['GET', 'POST'])
def reset_password_request():
if current_user.is_authenticated:
return redirect(url_for('main.index'))
form = ResetPasswordRequestForm()
if form.validate_on_submit():
user = User.query.filter_by(email=form.email.data).first()
if user:
send_password_reset_email(user)
flash('๋น๋ฐ๋ฒํธ ์ฌ์ค์ ์๋ด ์ด๋ฉ์ผ์ ๋ฐ์กํ์ต๋๋ค.')
return redirect(url_for('auth.login'))
return render_template('auth/reset_password_request.html',
title='๋น๋ฐ๋ฒํธ ์ฌ์ค์ ', form=form)
@bp.route('/reset_password/<token>', methods=['GET', 'POST'])
def reset_password(token):
if current_user.is_authenticated:
return redirect(url_for('main.index'))
user = User.verify_reset_password_token(token)
if not user:
flash('์ ํจํ์ง ์๊ฑฐ๋ ๋ง๋ฃ๋ ํ ํฐ์
๋๋ค.')
return redirect(url_for('main.index'))
form = ResetPasswordForm()
if form.validate_on_submit():
user.set_password(form.password.data)
db.session.commit()
flash('๋น๋ฐ๋ฒํธ๊ฐ ์ฌ์ค์ ๋์์ต๋๋ค.')
return redirect(url_for('auth.login'))
return render_template('auth/reset_password.html', form=form)
์ด์ ํ
ํ๋ฆฟ์ ๋ง๋ค์ด๋ณผ๊ฒ์. app/templates/auth/reset_password_request.html:
{% extends "base.html" %}
{% block content %}
<div class="row">
<div class="col-md-6 offset-md-3">
<div class="card">
<div class="card-header">
<h3 class="card-title">๋น๋ฐ๋ฒํธ ์ฌ์ค์ ์์ฒญ</h3>
</div>
<div class="card-body">
<form action="" method="post" novalidate>
{{ form.hidden_tag() }}
<div class="mb-3">
{{ form.email.label(class="form-label") }}
{{ form.email(class="form-control") }}
{% for error in form.email.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.submit(class="btn btn-primary") }}
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
app/templates/auth/reset_password.html:
{% extends "base.html" %}
{% block content %}
<div class="row">
<div class="col-md-6 offset-md-3">
<div class="card">
<div class="card-header">
<h3 class="card-title">๋น๋ฐ๋ฒํธ ์ฌ์ค์ </h3>
</div>
<div class="card-body">
<form action="" method="post" novalidate>
{{ form.hidden_tag() }}
<div class="mb-3">
{{ form.password.label(class="form-label") }}
{{ form.password(class="form-control") }}
{% for error in form.password.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.password2.label(class="form-label") }}
{{ form.password2(class="form-control") }}
{% for error in form.password2.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.submit(class="btn btn-primary") }}
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
์ด๋ฉ์ผ ํ
ํ๋ฆฟ๋ ๋ง๋ค์ด์ผ ํด์. app/templates/email/reset_password.html:
<p>์๋
ํ์ธ์ {{ user.username }}๋,</p>
<p>
๋น๋ฐ๋ฒํธ ์ฌ์ค์ ์ ์์ฒญํ์
จ์ต๋๋ค. ์๋ ๋งํฌ๋ฅผ ํด๋ฆญํ์ฌ ๋น๋ฐ๋ฒํธ๋ฅผ ์ฌ์ค์ ํ์ธ์:
</p>
<p>
<a href="{{ url_for('auth.reset_password', token=token, _external=True) }}">
๋น๋ฐ๋ฒํธ ์ฌ์ค์ ํ๊ธฐ
</a>
</p>
<p>
์ ๋งํฌ๋ 10๋ถ ๋์๋ง ์ ํจํฉ๋๋ค.
</p>
<p>
๋น๋ฐ๋ฒํธ ์ฌ์ค์ ์ ์์ฒญํ์ง ์์ผ์
จ๋ค๋ฉด ์ด ์ด๋ฉ์ผ์ ๋ฌด์ํ์
๋ ๋ฉ๋๋ค.
</p>
<p>์ค๋ฌธ์กฐ์ฌ ์์คํ
ํ</p>
์ด์ ๋ก๊ทธ์ธ ํ์ด์ง์ ๋น๋ฐ๋ฒํธ ์ฌ์ค์ ๋งํฌ๋ฅผ ์ถ๊ฐํด์ฃผ์ธ์. app/templates/auth/login.html์ ๋ง์ง๋ง ๋ถ๋ถ์ ์์ ํด์ฃผ์ธ์:
<p>๊ณ์ ์ด ์์ผ์ ๊ฐ์? <a href="{{ url_for('auth.register') }}">ํ์๊ฐ์
</a></p>
<p>๋น๋ฐ๋ฒํธ๋ฅผ ์์ผ์
จ๋์? <a href="{{ url_for('auth.reset_password_request') }}">๋น๋ฐ๋ฒํธ ์ฌ์ค์ </a></p>
์ด์ ์ฌ์ฉ์ ์ธ์ฆ ์์คํ ์ด ๋ ์๋ฒฝํด์ก์ด์! ๋ค์์ผ๋ก ๋ฐฐํฌ ๋ฐฉ๋ฒ์ ์์๋ณผ๊ฒ์.
9. ๋ฐฐํฌ ๋ฐ ์ด์ํ๊ธฐ ๐
์ด์ ์ฐ๋ฆฌ๊ฐ ๋ง๋ ๋ฉ์ง ์ค๋ฌธ์กฐ์ฌ ์์คํ ์ ์ค์ ๋ก ๋ฐฐํฌํด๋ณผ ์ฐจ๋ก์์! ๊ฐ๋ฐ ํ๊ฒฝ์์๋ง ๋๋ฆฌ๋ ๊ฑด ์ฌ๋ฏธ์์์์? ใ ใ ใ ์ค์ ์๋ฒ์ ์ฌ๋ ค์ ์น๊ตฌ๋ค์๊ฒ ์๋ํด๋ด ์๋ค!
9.1 ๋ฐฐํฌ ์ค๋นํ๊ธฐ
๋ฐฐํฌํ๊ธฐ ์ ์ ๋ช ๊ฐ์ง ์ค๋น๊ฐ ํ์ํด์:
- ํ๊ฒฝ ๋ณ์ ์ค์ ํ๊ธฐ
- ์์กด์ฑ ๋ชฉ๋ก ๋ง๋ค๊ธฐ
- ํ๋ก๋์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ค์ ํ๊ธฐ
- WSGI ์๋ฒ ์ค์ ํ๊ธฐ
๋จผ์ requirements.txt ํ์ผ์ ์
๋ฐ์ดํธํด๋ณผ๊ฒ์:
# ๊ฐ์ํ๊ฒฝ์์ ๋ค์ ๋ช
๋ น์ด ์คํ
pip freeze > requirements.txt
์ด์ .env ํ์ผ์ ๋ง๋ค์ด ํ๊ฒฝ ๋ณ์๋ฅผ ์ค์ ํด๋ณผ๊ฒ์:
SECRET_KEY=your-super-secret-key-here
DATABASE_URL=sqlite:///app.db
MAIL_SERVER=smtp.gmail.com
MAIL_PORT=587
MAIL_USE_TLS=1
MAIL_USERNAME=your-email@gmail.com
MAIL_PASSWORD=your-email-password
MAIL_DEFAULT_SENDER=your-email@gmail.com
์ค์ ๋ฐฐํฌ ์์๋ SQLite ๋์ PostgreSQL ๊ฐ์ ํ๋ก๋์ ์ฉ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ด ์ข์์.
9.2 Heroku์ ๋ฐฐํฌํ๊ธฐ
Heroku๋ ์ด๋ณด์๋ ์ฝ๊ฒ ๋ฐฐํฌํ ์ ์๋ ํด๋ผ์ฐ๋ ํ๋ซํผ์ด์์. ๋ค์ ๋จ๊ณ๋ฅผ ๋ฐ๋ผ ๋ฐฐํฌํด๋ณผ๊ฒ์:
- Heroku ๊ณ์ ๋ง๋ค๊ธฐ (https://signup.heroku.com/)
- Heroku CLI ์ค์นํ๊ธฐ (https://devcenter.heroku.com/articles/heroku-cli)
- ํ๋ก์ ํธ ๋ฃจํธ์
Procfile๋ง๋ค๊ธฐ
Procfile ๋ด์ฉ:
web: gunicorn run:app
์ด์ requirements.txt์ gunicorn์ ์ถ๊ฐํด์ฃผ์ธ์:
pip install gunicorn
pip freeze > requirements.txt
์ด์ Heroku์ ๋ฐฐํฌํด๋ณผ๊ฒ์:
# Heroku ๋ก๊ทธ์ธ
heroku login
# ์ฑ ์์ฑ
heroku create your-survey-app-name
# Git ์ ์ฅ์ ์ด๊ธฐํ (์์ง ์ ํ๋ค๋ฉด)
git init
git add .
git commit -m "Initial commit"
# Heroku์ ํธ์
git push heroku master
# ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ง์ด๊ทธ๋ ์ด์
heroku run flask db upgrade
# ์ฑ ์ด๊ธฐ
heroku open
ํ๊ฒฝ ๋ณ์๋ ์ค์ ํด์ฃผ์ธ์:
heroku config:set SECRET_KEY=your-super-secret-key-here
heroku config:set MAIL_SERVER=smtp.gmail.com
heroku config:set MAIL_PORT=587
heroku config:set MAIL_USE_TLS=1
heroku config:set MAIL_USERNAME=your-email@gmail.com
heroku config:set MAIL_PASSWORD=your-email-password
heroku config:set MAIL_DEFAULT_SENDER=your-email@gmail.com
์ด์ ์ฌ๋ฌ๋ถ์ ์ค๋ฌธ์กฐ์ฌ ์์คํ ์ด ์ธํฐ๋ท์ ๊ณต๊ฐ๋์์ด์! ๐
9.3 PythonAnywhere์ ๋ฐฐํฌํ๊ธฐ
Heroku ์ธ์๋ PythonAnywhere๋ผ๋ ํ๋ซํผ๋ ์์ด์. ์ด ํ๋ซํผ์ Python ์ฑ ๋ฐฐํฌ์ ํนํ๋์ด ์๊ณ , ๋ฌด๋ฃ ํฐ์ด๋ ์ ๊ณตํด์.
- PythonAnywhere ๊ณ์ ๋ง๋ค๊ธฐ (https://www.pythonanywhere.com/)
- Bash ์ฝ์ ์ด๊ธฐ
- ๊ฐ์ํ๊ฒฝ ์ค์ ๋ฐ ์ฝ๋ ๊ฐ์ ธ์ค๊ธฐ
PythonAnywhere Bash ์ฝ์์์ ๋ค์ ๋ช ๋ น์ด๋ฅผ ์คํํด์ฃผ์ธ์:
# ์ฝ๋ ํด๋ก (GitHub์ ์ฌ๋ ค๋จ๋ค๋ฉด)
git clone https://github.com/yourusername/your-survey-app.git
# ๋๋ ZIP ํ์ผ๋ก ์
๋ก๋ ํ ์์ถ ํด์
unzip your-survey-app.zip
# ๋๋ ํ ๋ฆฌ ์ด๋
cd your-survey-app
# ๊ฐ์ํ๊ฒฝ ์์ฑ ๋ฐ ํ์ฑํ
python -m venv venv
source venv/bin/activate
# ํจํค์ง ์ค์น
pip install -r requirements.txt
์ด์ PythonAnywhere ์น ์ธํฐํ์ด์ค์์ 'Web' ํญ์ผ๋ก ์ด๋ํด ์ ์น ์ฑ์ ์ค์ ํด์ฃผ์ธ์:
- 'Add a new web app' ํด๋ฆญ
- 'Manual configuration' ์ ํ
- Python ๋ฒ์ ์ ํ (3.8 ์ด์)
- ๊ฐ์ํ๊ฒฝ ๊ฒฝ๋ก ์
๋ ฅ:
/home/yourusername/your-survey-app/venv - WSGI ํ์ผ ์์ (์ ๊ณต๋ ๋งํฌ ํด๋ฆญ)
WSGI ํ์ผ์ ๋ค์๊ณผ ๊ฐ์ด ์์ ํด์ฃผ์ธ์:
import sys
import os
path = '/home/yourusername/your-survey-app'
if path not in sys.path:
sys.path.append(path)
from dotenv import load_dotenv
load_dotenv(os.path.join(path, '.env'))
from run import app as application
ํ๊ฒฝ ๋ณ์๋ .env ํ์ผ์ ๋ง๋ค์ด ์ค์ ํ๊ฑฐ๋, PythonAnywhere์ 'Web' ํญ์์ 'Environment variables' ์น์
์ ์ถ๊ฐํ ์ ์์ด์.
๋ง์ง๋ง์ผ๋ก 'Reload' ๋ฒํผ์ ํด๋ฆญํด ์ฑ์ ์์ํ์ธ์!
9.4 ๋๋ฉ์ธ ์ฐ๊ฒฐ ๋ฐ HTTPS ์ค์
์ค์ ์๋น์ค๋ก ์ด์ํ๋ ค๋ฉด ์ปค์คํ ๋๋ฉ์ธ๊ณผ HTTPS๋ฅผ ์ค์ ํ๋ ๊ฒ์ด ์ข์์:
- ๋๋ฉ์ธ ๊ตฌ๋งค (Namecheap, GoDaddy ๋ฑ)
- DNS ์ค์ (ํธ์คํ ์ ์ฒด ์ง์นจ ์ฐธ์กฐ)
- SSL ์ธ์ฆ์ ์ค์ (Let's Encrypt ์ถ์ฒ)
Heroku์์๋ ๋ค์๊ณผ ๊ฐ์ด ์ปค์คํ ๋๋ฉ์ธ์ ์ฐ๊ฒฐํ ์ ์์ด์:
heroku domains:add www.yourdomain.com
๊ทธ๋ฐ ๋ค์ ๋๋ฉ์ธ ์ ๊ณต์ ์ฒด์ DNS ์ค์ ์์ CNAME ๋ ์ฝ๋๋ฅผ ์ถ๊ฐํด์ฃผ์ธ์:
www.yourdomain.com -> your-app-name.herokuapp.com
Heroku์ ์ ๋ฃ ํ๋์ ์ฌ์ฉํ๋ฉด ์๋์ผ๋ก SSL ์ธ์ฆ์๋ฅผ ๊ด๋ฆฌํด์ค์:
heroku certs:auto:enable
์ด์ ์ฌ๋ฌ๋ถ์ ์ค๋ฌธ์กฐ์ฌ ์์คํ ์ด ์ ๋ฌธ์ ์ธ ๋๋ฉ์ธ๊ณผ ๋ณด์ ์ฐ๊ฒฐ๋ก ์ด์๋๊ณ ์์ด์! ๐
10. ํ์ฅ ๋ฐ ์ปค์คํฐ๋ง์ด์ง ์์ด๋์ด ๐ก
๊ธฐ๋ณธ ์ค๋ฌธ์กฐ์ฌ ์์คํ ์ ์์ฑํ์ง๋ง, ์ฌ๊ธฐ์ ๋ ๋ฐ์ ์ํฌ ์ ์๋ ์์ด๋์ด๊ฐ ๋ง์์! ์ฌ๋ฌ๋ถ๋ง์ ๋ ํนํ ๊ธฐ๋ฅ์ ์ถ๊ฐํด๋ณด์ธ์.
10.1 ์ถ๊ฐ ๊ธฐ๋ฅ ์์ด๋์ด
๋ค์์ ์ฌ๋ฌ๋ถ์ ์ค๋ฌธ์กฐ์ฌ ์์คํ ์ ๋ ๋ฉ์ง๊ฒ ๋ง๋ค ์ ์๋ ์์ด๋์ด๋ค์ด์์:
- ํ ๋ง ์ปค์คํฐ๋ง์ด์ง: ์ฌ์ฉ์๊ฐ ์ค๋ฌธ์ ์์, ํฐํธ ๋ฑ์ ๋ณ๊ฒฝํ ์ ์๊ฒ ํด๋ณด์ธ์
- ๋ถ๊ธฐ ๋ก์ง: ์ด์ ์ง๋ฌธ์ ์๋ต์ ๋ฐ๋ผ ๋ค๋ฅธ ์ง๋ฌธ์ ๋ณด์ฌ์ฃผ๋ ๊ธฐ๋ฅ
- ํ์ผ ์ ๋ก๋: ์๋ต์๊ฐ ์ด๋ฏธ์ง๋ ๋ฌธ์๋ฅผ ์ ๋ก๋ํ ์ ์๋ ์ง๋ฌธ ์ ํ ์ถ๊ฐ
- ์์ ๋ก๊ทธ์ธ: Google, Facebook ๋ฑ์ผ๋ก ๋ก๊ทธ์ธํ ์ ์๋ ๊ธฐ๋ฅ
- ์๋ต ์ ํ: IP ์ฃผ์๋ ์ด๋ฉ์ผ๋ก ์ค๋ณต ์๋ต ๋ฐฉ์ง
- ์ค๋ฌธ ํ ํ๋ฆฟ: ์์ฃผ ์ฌ์ฉ๋๋ ์ค๋ฌธ ์ ํ์ ํ ํ๋ฆฟ ์ ๊ณต
- ๊ณ ๊ธ ๋ถ์: ์๋ต ๋ฐ์ดํฐ์ ์๊ด๊ด๊ณ ๋ถ์, ์ธ๊ทธ๋จผํธ๋ณ ํํฐ๋ง ๋ฑ
- ์๋ฆผ ์์คํ : ์ ์๋ต์ด ๋ค์ด์ค๋ฉด ์ด๋ฉ์ผ์ด๋ ํธ์ ์๋ฆผ ์ ์ก
- ํ์ ๊ธฐ๋ฅ: ์ฌ๋ฌ ์ฌ์ฉ์๊ฐ ํจ๊ป ์ค๋ฌธ์ ํธ์งํ ์ ์๋ ๊ธฐ๋ฅ
- API ์ ๊ณต: ์ธ๋ถ ์๋น์ค์ ์ฐ๋ํ ์ ์๋ API ๊ฐ๋ฐ
์ด๋ฐ ๊ธฐ๋ฅ๋ค์ ์ถ๊ฐํ๋ฉด ์ฌ๋ฌ๋ถ์ ์ค๋ฌธ์กฐ์ฌ ์์คํ ์ด ์์ฉ ์๋น์ค์ ๊ฒฌ์ค ๋งํ ์์ค์ด ๋ ๊ฑฐ์์! ๊ทธ๋ฆฌ๊ณ ์ด๋ฐ ๊ฐ๋ฐ ๊ฒฝํ์ ์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์ Python ์น ๊ฐ๋ฐ ๊ด๋ จ ์ฌ๋ฅ์ ํ๋งคํ ๋ ์ข์ ํฌํธํด๋ฆฌ์ค๊ฐ ๋ ์ ์์ด์.
10.2 ์ฝ๋ ์ต์ ํ ๋ฐ ๋ฆฌํฉํ ๋ง
์ค์ ์๋น์ค๋ก ์ด์ํ๋ ค๋ฉด ์ฝ๋ ์ต์ ํ๋ ์ค์ํด์:
- ์บ์ฑ ๋์ : Redis๋ Memcached๋ฅผ ์ฌ์ฉํด ์์ฃผ ์ ๊ทผํ๋ ๋ฐ์ดํฐ ์บ์ฑ
- ๋น๋๊ธฐ ์์ : Celery ๊ฐ์ ๋๊ตฌ๋ก ์ด๋ฉ์ผ ์ ์ก ๋ฑ์ ์์ ์ ๋น๋๊ธฐ๋ก ์ฒ๋ฆฌ
- ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ธ๋ฑ์ฑ: ์์ฃผ ์กฐํํ๋ ํ๋์ ์ธ๋ฑ์ค ์ถ๊ฐ
- ํ์ด์ง๋ค์ด์ : ๋๋์ ๋ฐ์ดํฐ๋ฅผ ํ์ด์ง ๋จ์๋ก ๋๋ ๋ก๋ฉ
- ํ ์คํธ ์ฝ๋ ์์ฑ: ๋จ์ ํ ์คํธ์ ํตํฉ ํ ์คํธ๋ก ์์ ์ฑ ํ๋ณด
์ด๋ฐ ์ต์ ํ๋ ์ฌ์ฉ์๊ฐ ๋ง์์ง์๋ก ๋ ์ค์ํด์ ธ์!
10.3 ์์ตํ ๋ฐฉ์
์ฌ๋ฌ๋ถ์ ์ค๋ฌธ์กฐ์ฌ ์์คํ ์ผ๋ก ์์ต์ ์ฐฝ์ถํ๋ ๋ฐฉ๋ฒ๋ ์์ด์:
- ํ๋ฆฌ๋ฏธ์ ํ๋: ๊ธฐ๋ณธ ๊ธฐ๋ฅ์ ๋ฌด๋ฃ, ๊ณ ๊ธ ๊ธฐ๋ฅ์ ์ ๋ฃ๋ก ์ ๊ณต
- ์๋ต ์ ์ ํ: ๋ฌด๋ฃ ํ๋์ ์ 100๊ฐ ์๋ต๊น์ง, ๊ทธ ์ด์์ ์ ๋ฃ
- ํ์ดํธ ๋ผ๋ฒจ: ๊ธฐ์ ์ด ์์ฌ ๋ธ๋๋๋ก ์๋น์ค๋ฅผ ์ด์ฉํ ์ ์๊ฒ ์ ๊ณต
- ์ค๋ฌธ ํ ํ๋ฆฟ ๋ง์ผํ๋ ์ด์ค: ์ ๋ฌธ๊ฐ๊ฐ ๋ง๋ ํ ํ๋ฆฟ ํ๋งค
- API ์ฌ์ฉ๋ฃ: ์ธ๋ถ ์๋น์ค ์ฐ๋์ ์ํ API ์ฌ์ฉ๋ฃ ๋ถ๊ณผ
์ด๋ฐ ๋น์ฆ๋์ค ๋ชจ๋ธ์ ์ ์ฉํ๋ฉด ์ทจ๋ฏธ ํ๋ก์ ํธ๋ฅผ ์ค์ ์์ต์์ผ๋ก ๋ฐ์ ์ํฌ ์ ์์ด์!
10.4 ๋ง๋ฌด๋ฆฌ
์ด์ ์ฌ๋ฌ๋ถ์ Python๊ณผ Flask๋ฅผ ์ฌ์ฉํด ์์ ํ ์ค๋ฌธ์กฐ์ฌ ์์คํ ์ ๋ง๋๋ ๋ฐฉ๋ฒ์ ๋ฐฐ์ ์ด์! ๐
์ด ํ๋ก์ ํธ๋ฅผ ํตํด ๋ค์๊ณผ ๊ฐ์ ๊ธฐ์ ์ ์ตํ์ด์:
- Flask ์น ํ๋ ์์ํฌ ์ฌ์ฉ๋ฒ
- SQLAlchemy๋ฅผ ์ด์ฉํ ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ชจ๋ธ๋ง
- ์ฌ์ฉ์ ์ธ์ฆ ์์คํ ๊ตฌํ
- ๋์ ํผ ์ฒ๋ฆฌ
- ๋ฐ์ดํฐ ์๊ฐํ
- ์น ์ ํ๋ฆฌ์ผ์ด์ ๋ฐฐํฌ
์ด ๋ชจ๋ ๊ธฐ์ ์ ๋ค์ํ ์น ์ ํ๋ฆฌ์ผ์ด์ ๊ฐ๋ฐ์ ํ์ฉํ ์ ์๋ ๊ท์คํ ์์ฐ์ด์์! ์ด์ ์ฌ๋ฌ๋ถ๋ง์ ์์ด๋์ด๋ฅผ ์ถ๊ฐํด ๋ ๋ฉ์ง ์๋น์ค๋ก ๋ฐ์ ์์ผ๋ณด์ธ์.
Python ์น ๊ฐ๋ฐ์ ์ ๋ง ์ฌ๋ฏธ์๊ณ ๋ฌด๊ถ๋ฌด์งํ ๊ฐ๋ฅ์ฑ์ด ์๋ ๋ถ์ผ์์. ์ด ํํ ๋ฆฌ์ผ์ด ์ฌ๋ฌ๋ถ์ ๊ฐ๋ฐ ์ฌ์ ์ ๋์์ด ๋์๊ธธ ๋ฐ๋๋๋ค! ๐
ํน์ ์ด๋ฐ ๊ฐ๋ฐ ์คํฌ์ ๋ ๋ฐ์ ์ํค๊ณ ์ถ๋ค๋ฉด, ์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์ ๋ค์ํ Python ์น ๊ฐ๋ฐ ๊ด๋ จ ์ฌ๋ฅ์ ์ฐพ์๋ณด๋ ๊ฒ๋ ์ข์ ๋ฐฉ๋ฒ์ด์์. ๋ค๋ฅธ ๊ฐ๋ฐ์๋ค์ ๋ ธํ์ฐ๋ฅผ ๋ฐฐ์ฐ๊ณ , ๋์ค์๋ ์ฌ๋ฌ๋ถ๋ ์์ ๋ง์ ์ฌ๋ฅ์ ๊ณต์ ํ ์ ์์ ๊ฑฐ์์!
๊ด๋ จ ํค์๋
๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.