프로그래밍 일기/Python & Flask

Python Flask 게시판 CRUD 웹 프로젝트 구현, 기본 구조 이해

MakeMe 2024. 3. 5. 11:55
반응형

가상환경 만들기

여러 파이썬 프로젝트를 작업할 경우 각 프로젝트마다 파이썬 버전, 사용하는 라이브러리 등이 다르기 때문에 가상환경을 통해 프로젝트를 구현하는게 좋습니다.

터미널을 통해 프로젝트 폴더로 이동 후

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

해당 명령어를 통해 프로젝트를 구동할 수 있습니다.

반응형