first push
This commit is contained in:
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
# 디폴트 무시된 파일
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
# 에디터 기반 HTTP 클라이언트 요청
|
||||
/httpRequests/
|
||||
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<settings>
|
||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||
<version value="1.0" />
|
||||
</settings>
|
||||
</component>
|
||||
4
.idea/misc.xml
generated
Normal file
4
.idea/misc.xml
generated
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.9" project-jdk-type="Python SDK" />
|
||||
</project>
|
||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/wixon_blog.iml" filepath="$PROJECT_DIR$/.idea/wixon_blog.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
19
.idea/wixon_blog.iml
generated
Normal file
19
.idea/wixon_blog.iml
generated
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="Flask">
|
||||
<option name="enabled" value="true" />
|
||||
</component>
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
<component name="TemplatesService">
|
||||
<option name="TEMPLATE_CONFIGURATION" value="Jinja2" />
|
||||
<option name="TEMPLATE_FOLDERS">
|
||||
<list>
|
||||
<option value="$MODULE_DIR$/../wixon_blog\templates" />
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
</module>
|
||||
159
app.py
Normal file
159
app.py
Normal file
@@ -0,0 +1,159 @@
|
||||
from flask import Flask, render_template, request, redirect, url_for, flash, session
|
||||
import pymysql
|
||||
from flask import session, g, jsonify
|
||||
import bcrypt
|
||||
from bs4 import BeautifulSoup
|
||||
from werkzeug.utils import secure_filename
|
||||
import os
|
||||
import uuid
|
||||
|
||||
UPLOAD_FOLDER = 'static/upload/img' # 경로를 Flask 앱 루트 기준으로 수정
|
||||
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}
|
||||
|
||||
app = Flask(__name__)
|
||||
app.secret_key = 'your secret key'
|
||||
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
|
||||
|
||||
|
||||
@app.before_request
|
||||
def load_user():
|
||||
if 'user_info' in session:
|
||||
g.is_login = True
|
||||
g.user_info = session['user_info']
|
||||
|
||||
|
||||
def allowed_file(filename):
|
||||
return '.' in filename and \
|
||||
filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
|
||||
|
||||
|
||||
@app.route('/upload_image', methods=['POST'])
|
||||
def upload_image():
|
||||
if 'file' not in request.files:
|
||||
return jsonify(success=False, message='No file part'), 400
|
||||
|
||||
file = request.files['file']
|
||||
|
||||
if file.filename == '':
|
||||
return jsonify(success=False, message='No selected file'), 400
|
||||
|
||||
if file and allowed_file(file.filename):
|
||||
# Secure the filename and keep its extension
|
||||
filename = secure_filename(file.filename)
|
||||
ext = filename.rsplit('.', 1)[1].lower()
|
||||
|
||||
# Generate a random filename using uuid4
|
||||
random_filename = f'{uuid.uuid4().hex}.{ext}'
|
||||
|
||||
file_path = os.path.join(app.config['UPLOAD_FOLDER'], random_filename)
|
||||
|
||||
# Create directories if not exist
|
||||
os.makedirs(os.path.dirname(file_path), exist_ok=True)
|
||||
|
||||
file.save(file_path)
|
||||
|
||||
# Generate the URL of the saved image file
|
||||
file_url = url_for('static', filename='upload/img/' + random_filename)
|
||||
|
||||
return jsonify(success=True, fileUrl=file_url), 200
|
||||
|
||||
return jsonify(success=False, message='File not allowed'), 400
|
||||
|
||||
|
||||
# MySQL 데이터베이스 연결 설정
|
||||
def connnect_db():
|
||||
return pymysql.connect(
|
||||
host='wxnasso.synology.me',
|
||||
user='wixon5',
|
||||
password='Wixon2022@!',
|
||||
database='wixon',
|
||||
charset="utf8mb4",
|
||||
cursorclass=pymysql.cursors.DictCursor, # DictCursor를 사용하여 딕셔너리 형태로 결과를 반환
|
||||
init_command='SET SQL_SAFE_UPDATES = 0;',
|
||||
)
|
||||
|
||||
|
||||
def sql_execute(q, d, is_data=False, is_last_id=False):
|
||||
data = None
|
||||
last_id = None
|
||||
with connnect_db().cursor(pymysql.cursors.DictCursor) as cursor:
|
||||
try:
|
||||
res = cursor.execute(q, d)
|
||||
data = cursor.fetchall() if is_data else None
|
||||
last_id = cursor.lastrowid if cursor.lastrowid != 0 else None
|
||||
cursor.connection.commit()
|
||||
except Exception as e:
|
||||
print(e)
|
||||
cursor.connection.rollback()
|
||||
res = False
|
||||
|
||||
return (res, last_id if is_last_id else data) if is_data or is_last_id else res
|
||||
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
# MySQL에서 모든 블로그 포스트를 가져옵니다.
|
||||
query = "SELECT `title`, `thumbnail_img`, `contents` FROM `blog` WHERE `use_yn` = 'Y' ORDER BY `add_date` DESC;"
|
||||
r, posts = sql_execute(query, (), is_data=True)
|
||||
|
||||
# `index.html` 템플릿으로 데이터를 전달합니다.
|
||||
return render_template('index.html', posts=posts)
|
||||
|
||||
|
||||
@app.route('/login', methods=['GET', 'POST'])
|
||||
def login():
|
||||
if request.method == 'POST':
|
||||
username = request.form.get('username')
|
||||
password = request.form.get('password').encode('utf-8')
|
||||
|
||||
# DB에서 사용자 정보 가져오기
|
||||
res, user = sql_execute("SELECT * FROM member WHERE mb_id=%s", (username,), is_data=True)
|
||||
if res and user and bcrypt.checkpw(password, user[0]['mb_passwd'].encode('utf-8')):
|
||||
session['username'] = username
|
||||
session['user_info'] = user[0]
|
||||
return redirect(url_for('index'))
|
||||
else:
|
||||
flash('Username or password is incorrect')
|
||||
return render_template('login.html')
|
||||
|
||||
|
||||
@app.route('/logout')
|
||||
def logout():
|
||||
session.clear()
|
||||
g.is_login = False
|
||||
g.user_info = None
|
||||
|
||||
return redirect(url_for('index'))
|
||||
|
||||
|
||||
@app.route('/write', methods=['GET', 'POST'])
|
||||
def write():
|
||||
if request.method == 'POST':
|
||||
title = request.form['title']
|
||||
category = request.form['category']
|
||||
contents = request.form['contents']
|
||||
|
||||
# 본문에서 첫 번째 이미지 추출
|
||||
soup = BeautifulSoup(contents, 'html.parser')
|
||||
first_image = soup.find('img')
|
||||
thumbnail_img = first_image['src'] if first_image else None
|
||||
|
||||
# SQL 쿼리와 삽입할 데이터 설정
|
||||
query = "INSERT INTO blog (title, category, contents, thumbnail_img, use_yn) VALUES (%s, %s, %s, %s, 'Y')"
|
||||
data = (title, category, contents, thumbnail_img)
|
||||
|
||||
# 새로운 글 저장
|
||||
result, last_id = sql_execute(query, data, is_last_id=True)
|
||||
|
||||
if result:
|
||||
# 저장 성공시, 인덱스 페이지로 리다이렉트
|
||||
return redirect(url_for('index'))
|
||||
else:
|
||||
# 저장 실패시, 에러 메시지 반환
|
||||
return "Failed to write post", 500
|
||||
else:
|
||||
return render_template('write.html')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=8899)
|
||||
BIN
static/upload/img/d6dffe69d6f84a709962b653e7f6eb16.png
Normal file
BIN
static/upload/img/d6dffe69d6f84a709962b653e7f6eb16.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
BIN
static/upload/img/e0cb20c7b5a34b46b117e06f94963996.png
Normal file
BIN
static/upload/img/e0cb20c7b5a34b46b117e06f94963996.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 926 B |
BIN
static/upload/img/logo.png
Normal file
BIN
static/upload/img/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 926 B |
75
templates/base.html
Normal file
75
templates/base.html
Normal file
@@ -0,0 +1,75 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Blog</title>
|
||||
<link href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/uikit/3.6.16/css/uikit.min.css"/>
|
||||
<style>
|
||||
.blog-card {
|
||||
width: 300px;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.blog-card img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.grid-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- include libraries(jQuery, bootstrap) -->
|
||||
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
|
||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js"></script>
|
||||
|
||||
<!-- include summernote css/js -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote.min.css" rel="stylesheet">
|
||||
<script src="https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote.min.js"></script>
|
||||
|
||||
<script src="https://unpkg.com/masonry-layout@4/dist/masonry.pkgd.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav class="uk-navbar-container" uk-navbar>
|
||||
<div class="uk-navbar-left">
|
||||
<ul class="uk-navbar-nav">
|
||||
<li class="uk-active"><a href="#">
|
||||
<img src="http://webmail.wixon.co.kr/user_img/logoImage.jpeg" alt="">
|
||||
</a></li>
|
||||
<li><a href="/">목록</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="uk-navbar-right">
|
||||
<ul class="uk-navbar-nav">
|
||||
{% if g.is_login %}
|
||||
<li>
|
||||
<a href="#">{{ g.user_info.mb_name }} <span class="uk-icon"
|
||||
uk-icon="icon: triangle-down"></span></a>
|
||||
<div class="uk-navbar-dropdown">
|
||||
<ul class="uk-nav uk-navbar-dropdown-nav">
|
||||
<li class="uk-active"><a href="/write">포스트 작성</a></li>
|
||||
<li class="uk-active"><a href="/logout">로그아웃</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
{% else %}
|
||||
<li><a href="/login">로그인</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
</nav>
|
||||
|
||||
{% block content %}{% endblock %}
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/uikit/3.6.16/js/uikit.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/uikit/3.6.16/js/uikit-icons.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
32
templates/index.html
Normal file
32
templates/index.html
Normal file
@@ -0,0 +1,32 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="uk-container">
|
||||
<div class="grid-container masonry-grid">
|
||||
{% for post in posts %}
|
||||
<div class="blog-card uk-card uk-card-default">
|
||||
<div class="uk-card-media-top">
|
||||
<img src="{{ post.thumbnail_img }}" alt="{{ post.title }}">
|
||||
</div>
|
||||
<div class="uk-card-body">
|
||||
<h3 class="uk-card-title">{{ post.title }}</h3>
|
||||
<!-- Truncate the contents to 100 characters, removing any HTML tags -->
|
||||
<p>{{ post.contents | striptags | truncate(100) }}</p>
|
||||
<a href="#" class="uk-button uk-button-text">Read more</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
$('.masonry-grid').masonry({
|
||||
// options...
|
||||
itemSelector: '.blog-card',
|
||||
columnWidth: '.blog-card',
|
||||
percentPosition: true
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
32
templates/login.html
Normal file
32
templates/login.html
Normal file
@@ -0,0 +1,32 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="uk-container uk-margin-top">
|
||||
<h1 class="uk-text-center"><span>윅슨 블로그 로그인</span></h1>
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<div class="uk-alert-danger" uk-alert>
|
||||
<a class="uk-alert-close" uk-close></a>
|
||||
{{ messages[0] }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<form class="uk-form-stacked" action="{{ url_for('login') }}" method="post">
|
||||
<div class="uk-margin">
|
||||
<label class="uk-form-label" for="form-stacked-text">Username</label>
|
||||
<div class="uk-form-controls">
|
||||
<input class="uk-input" id="form-stacked-text" type="text" placeholder="Username..." name="username">
|
||||
</div>
|
||||
</div>
|
||||
<div class="uk-margin">
|
||||
<label class="uk-form-label" for="form-stacked-text">Password</label>
|
||||
<div class="uk-form-controls">
|
||||
<input class="uk-input" id="form-stacked-text" type="password" placeholder="Password..." name="password">
|
||||
</div>
|
||||
</div>
|
||||
<div class="uk-margin">
|
||||
<button class="uk-button uk-button-default">Log In</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
65
templates/write.html
Normal file
65
templates/write.html
Normal file
@@ -0,0 +1,65 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="uk-container uk-margin-top">
|
||||
<h1 class="uk-text-center"><span>포스트 작성</span></h1>
|
||||
<form action="/write" method="post" class="uk-form-stacked">
|
||||
<div class="uk-margin">
|
||||
<label class="uk-form-label" for="title">제목</label>
|
||||
<div class="uk-form-controls">
|
||||
<input class="uk-input" id="title" type="text" name="title" placeholder="제목을 입력하세요..." required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="uk-margin">
|
||||
<label class="uk-form-label" for="category">카테고리</label>
|
||||
<div class="uk-form-controls">
|
||||
<select class="uk-select" id="category" name="category" required>
|
||||
<option value="">카테고리를 선택하세요...</option>
|
||||
<option value="IT">IT</option>
|
||||
<option value="NEWS">NEWS</option>
|
||||
<option value="ETC">ETC</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="uk-margin">
|
||||
<label class="uk-form-label" for="contents">내용</label>
|
||||
<div class="uk-form-controls">
|
||||
<textarea class="summernote" id="contents" name="contents" required></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="uk-margin">
|
||||
<button type="submit" class="uk-button uk-button-default">글 작성</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
$('.summernote').summernote({
|
||||
height: 300,
|
||||
callbacks: {
|
||||
onImageUpload: function (files) {
|
||||
var $editor = $(this);
|
||||
var data = new FormData();
|
||||
data.append("file", files[0]);
|
||||
$.ajax({
|
||||
url: "/upload_image",
|
||||
method: "POST",
|
||||
data: data,
|
||||
processData: false,
|
||||
contentType: false,
|
||||
success: function (response) {
|
||||
if (response.success) {
|
||||
var imageUrl = response.fileUrl;
|
||||
$editor.summernote("insertImage", imageUrl);
|
||||
} else {
|
||||
alert(response.message);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user