Python Flask 게시판 CRUD 웹 프로젝트 구현, 기본 구조 이해
가상환경 만들기
여러 파이썬 프로젝트를 작업할 경우 각 프로젝트마다 파이썬 버전, 사용하는 라이브러리 등이 다르기 때문에 가상환경을 통해 프로젝트를 구현하는게 좋습니다.
터미널을 통해 프로젝트 폴더로 이동 후
python3.9 -m venv venv
(파이썬 버전이 3.9 이전일 경우 python -m venv venv)
해당 명령어 입력 시 venv 파일이 생성되는 것을 알 수 있습니다.
source venv/bin/activate
(mac)
source venv/script/activate
(window)
해당 명령어로 가상환경을 활성화 시킵니다.
라이브러리 관리하기
requirements.txt 파일 생성
Flask==3.0.2
Flask-Migrate==3.1.0
SQLAlchemy==1.4.25
marshmallow==3.14.1
marshmallow-sqlalchemy==0.27.0
flask-marshmallow==0.14.0
psycopg2==2.9.9
위 내용 입력 후 터미널을 통해
pip install -r requirements.txt
만약 아래와 같은 화면이 나올 경우
pip install --upgrade pip
위 명령어 수행 후 다시 pip install -r requirements.txt 를 입력해주세요.
코딩
GitHub - tm-kr/python-board
Contribute to tm-kr/python-board development by creating an account on GitHub.
github.com
app.py
from board_app import app
app.run(port=5100, debug=True)
config.py
class Config:
# 데이터베이스 연결 정보
SQLALCHEMY_DATABASE_URI = 'postgresql://[username]:[username]@[host]:[port]/[db]'
# 예시) postgresql://kimtaemin:kimtaemin@localhost:5432/test_board
SQLALCHEMY_TRACK_MODIFICATIONS = False
board_app 폴더 생성
__init__.py, apis/, models/, schemas/, static/, templates/, views/ 생성
board_app/__init__.py
from flask import Flask
from config import Config
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_marshmallow import Marshmallow
app = Flask(__name__)
app.config.from_object(Config)
db = SQLAlchemy(app)
migrate = Migrate(app, db)
ma = Marshmallow(app)
from board_app.models import * # 모델 import
from board_app.views import * # 뷰 파일에서 route 모듈을 import
from board_app.apis import *
board_app/apis/__init__.py
from . import api
board_app/apis/api.py
from board_app import app, db
from flask import jsonify, request
from ..schemas.schema import BoardSchema
from ..models.model import Board
from sqlalchemy import desc
@app.route('/board/load')
def loadBoard():
result = BoardSchema().dump(db.session.query(Board).order_by(desc(Board.board_id)).all(), many=True)
return jsonify(result)
@app.route('/board/detail/load')
def loadBoardDetail():
result = BoardSchema().dump(db.session.query(Board).filter(Board.board_id == request.args.get("boardId")).first())
return jsonify(result)
@app.route('/board/add', methods=["POST"])
def addBoard():
data = request.json
new_board = Board(**data)
db.session.add(new_board)
db.session.commit()
return jsonify({"response": "complete"})
@app.route('/board/modify', methods=["POST"])
def modifyBoard():
data = request.json
board_id = data.get('board_id')
board_to_modify = Board.query.get(board_id)
if board_to_modify:
board_to_modify.board_title = data.get('board_title')
board_to_modify.board_contents = data.get('board_contents')
db.session.commit()
return jsonify({"response": "complete"})
else:
return jsonify({"response": "fail"})
@app.route('/board/remove', methods=["POST"])
def removeBoard():
data = request.json
board_id = data.get('board_id')
board_to_delete = Board.query.get(board_id)
if board_to_delete:
db.session.delete(board_to_delete)
db.session.commit()
return jsonify({"response": "complete"})
else:
return jsonify({"response": "fail"})
board_app/models/__init__.py
from . import model
board_app/models/model.py
from datetime import datetime
from flask_sqlalchemy import SQLAlchemy
from board_app import db
class Board(db.Model):
__tablename__ = 'board'
board_id = db.Column(db.Integer, primary_key=True, autoincrement=True)
board_title = db.Column(db.String)
board_contents = db.Column(db.String)
writer = db.Column(db.String)
create_date = db.Column(db.Date, default=datetime.now)
views = db.Column(db.Integer)
board_app/schemas/__init__.py
from . import schema
board_app/schemas/schema.py
from flask_marshmallow import Marshmallow
from marshmallow import fields
ma = Marshmallow() # 이 부분은 삭제합니다.
class BoardSchema(ma.Schema):
board_id = fields.Integer() # Integer 타입으로 수정
board_title = fields.String()
board_contents = fields.String()
writer = fields.String()
create_date = fields.Date() # Date 타입으로 수정
views = fields.Integer() # Integer 타입으로 수정
static 폴더에 js폴더 생성
board_app/static/js/detail.js
$(document).ready(function() {
loadBoardDetail()
})
var urlParams = new URLSearchParams(window.location.search);
var boardId = urlParams.get('id');
function loadBoardDetail() {
$.ajax({
url: "/board/detail/load",
data: {
boardId: boardId
},
dataType: "json",
contentType: "application/json; charset=utf-8",
type: "GET",
success: function (result) {
$('input[name=boardTitle]').val(result.board_title)
$('textarea[name=boardContents]').val(result.board_contents)
$('#boardWriter').html(result.writer)
$('#createDate').html(result.create_date)
},
error: function (result) {
alert('실패')
},
});
}
function modifyFormOn() {
$('.modifyButton').show();
$('.modifyButtonOff').hide();
$('input[name=boardTitle]').attr('readonly', false);
$('textarea[name=boardContents]').attr('readonly', false);
}
function modifyFormOff() {
$('.modifyButtonOff').show();
$('.modifyButton').hide();
$('input[name=boardTitle]').attr('readonly', true);
$('textarea[name=boardContents]').attr('readonly', true);
loadBoardDetail();
}
function modifyBoard() {
$.ajax({
url: "/board/modify",
data: JSON.stringify({
board_id: boardId,
board_title: $('input[name=boardTitle]').val(),
board_contents: $('textarea[name=boardContents]').val(),
}),
dataType: "json",
contentType: "application/json; charset=utf-8",
type: "POST",
success: function () {
alert('수정되었습니다.')
modifyFormOff();
},
error: function () {
alert('실패')
},
});
}
function removeBoard() {
if (!confirm('삭제하시겠습니까?')) {
return;
}
$.ajax({
url: "/board/remove",
data: JSON.stringify({
board_id: boardId
}),
dataType: "json",
contentType: "application/json; charset=utf-8",
type: "POST",
success: function () {
alert('삭제되었습니다.')
location.href = '/'
},
error: function () {
alert('실패')
},
});
}
board_app/static/js/main.js
$(document).ready(function() {
loadBoard()
})
function loadBoard() {
$.ajax({
url: "/board/load",
data: {},
dataType: "json",
contentType: "application/json; charset=utf-8",
type: "GET",
success: function (result) {
var tb = $("#table_tbody");
var html = "";
total = result.length
for (var i = 0 ; i < total; i++) {
item = result[i]
html += `
<tr>
<td>${total - i}</td>
<td>${item.writer}</td>
<td><a href="/detail?id=${item.board_id}">${item.board_title}</a></td>
<td>${item.create_date}</td>
<td>${item.views}</td>
</tr>
`;
}
if (total > 0) {
tb.html(html)
}
},
error: function (result) {
alert('실패')
},
});
}
function addBoard() {
$.ajax({
url: "/board/add",
data: JSON.stringify({
board_title: $('input[name=boardTitle]').val(),
board_contents: $('textarea[name=boardContents]').val(),
writer: '관리자',
views: 0
}),
dataType: "json",
contentType: "application/json; charset=utf-8",
type: "POST",
success: function (result) {
location.reload();
},
error: function (result) {
},
});
}
board_app/templates/detail.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>게시글 상세보기</title>
</head>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<body>
<div class="container" style="max-width: 800px;">
<div class="d-grid d-md-flex justify-content-md-end mt-5">
<button class="btn btn-primary me-md-2 modifyButtonOff" type="button" onclick="modifyFormOn()">수정하기</button>
<button class="btn btn-danger me-md-2 modifyButtonOff" type="button" onclick="removeBoard()">삭제</button>
<button class="btn btn-outline-primary modifyButtonOff" type="button" onclick="location.href='/'">목록으로</button>
<button class="btn btn-primary me-md-2 modifyButton" type="button" style="display: none;" onclick="modifyBoard()">저장</button>
<button class="btn btn-outline-primary modifyButton" type="button" style="display: none;" onclick="modifyFormOff()">취소</button>
</div>
<div class="mb-3">
<label for="formGroupExampleInput" class="form-label">제목</label>
<input name="boardTitle" readonly type="text" class="form-control" id="formGroupExampleInput" placeholder="제목을 입력해주세요.">
</div>
<div class="mb-3">
<label for="formGroupExampleInput2" class="form-label">내용</label>
<textarea name="boardContents" readonly class="form-control" placeholder="내용을 입력해주세요." id="floatingTextarea" style="min-height: 350px;"></textarea>
</div>
<div style="font-size:15px; color:gray">
작성자 : <span id="boardWriter"></span> | 게시일 : <span id="createDate"></span>
</div>
</div>
</body>
</html>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js" integrity="sha512-894YE6QWD5I59HgZOGReFYm4dnWc1Qt5NtvYSaNcOP+u1T9qYdvdihz0PPSiiqn/+/3e7Jo4EaG7TubfWGUrMQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="../static/js/detail.js"></script>
board_app/templates/main.html
<!DOCTYPE html>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>게시판</title>
</head>
<body>
<div class="container" style="max-width: 800px;">
<button class="btn btn-primary mt-5" type="button" style="float: right; margin-bottom: 20px;" data-bs-toggle="modal" data-bs-target="#exampleModal">게시글 등록</button>
<table class="table table-bordered">
<colgroup>
<col width="5%"/>
<col width="15%"/>
<col width="50%"/>
<col width="20%"/>
<col width="10%"/>
</colgroup>
<thead>
<tr>
<th>#</th>
<th>작성자</th>
<th>제목</th>
<th>날짜</th>
<th>조회수</th>
</tr>
</thead>
<tbody id="table_tbody">
<td colspan="5" style="text-align: center;">등록된 글이 없습니다.</td>
</tbody>
</table>
</div>
<div class="modal fade" id="exampleModal" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="exampleModalLabel">게시글 등록</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="formGroupExampleInput" class="form-label">제목</label>
<input name="boardTitle" type="text" class="form-control" id="formGroupExampleInput" placeholder="제목을 입력해주세요.">
</div>
<div class="mb-3">
<label for="formGroupExampleInput" class="form-label">내용</label>
<textarea name="boardContents" class="form-control" placeholder="내용을 입력해주세요." id="floatingTextarea" style="min-height: 350px;"></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" onclick="addBoard()">등록</button>
</div>
</div>
</div>
</div>
</body>
</html>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js" integrity="sha512-894YE6QWD5I59HgZOGReFYm4dnWc1Qt5NtvYSaNcOP+u1T9qYdvdihz0PPSiiqn/+/3e7Jo4EaG7TubfWGUrMQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="../static/js/main.js"></script>
board_app/views/__init__.py
from . import route
board_app/views/route.py
from board_app import app
from flask import render_template
@app.route('/')
def index():
return render_template('main.html')
@app.route('/detail')
def detail():
return render_template('detail.html')
최종 폴더 구조
board/
| app.py
| config.py
| requirements.txt
| migrations/
| venv/
| board_app/
| __init__.py
| models/
| __init__.py
| model.py
| schemas/
| __init__.py
| schema.py
| static/
| css/
| js/
| detail.js
| main.js
| templates/
| detail.html
| main.html
| views/
| __init__.py
| route.py
| apis/
| __init__.py
| api.py
DB 마이그레이션
먼저 프로젝트를 진행하기 위한 DB가 있다는 가정하에 진행하겠습니다. 저는 Postgresql (psql)을 사용할 예정입니다.
먼저 localhost:5432에 test_board 라는 db를 만든 후 프로젝트로 돌아옵니다.
flask db init
해당 명령어를 입력 시 migrations 폴더가 생성된걸 확인할 수 있습니다.
flask db migrate
해당 명령어 입력 시 migrations/versions 폴더 하위에 파이썬 파일이 생기며, test_board 테이블에 alembic_version 테이블이 생긴걸 확인할 수 있습니다.
flask db upgrade
해당 명령어 입력 시 test_board db에 board 테이블이 생성되는걸 확인할 수 있습니다.
프로젝트 실행하기
python app.py
해당 명령어를 통해 프로젝트를 구동할 수 있습니다.