블로그 기능 구현
> 글쓰기, 수정, 삭제 구현 > 목록 페이지 벽돌 레이아웃 구성 > 로그인 기능 추가 > 컨텐츠 공개/숨김 기능 추가
This commit is contained in:
115
app.py
115
app.py
@@ -6,6 +6,8 @@ from bs4 import BeautifulSoup
|
||||
from werkzeug.utils import secure_filename
|
||||
import os
|
||||
import uuid
|
||||
from markupsafe import Markup
|
||||
from jinja2 import filters
|
||||
|
||||
UPLOAD_FOLDER = 'static/upload/img' # 경로를 Flask 앱 루트 기준으로 수정
|
||||
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}
|
||||
@@ -66,7 +68,7 @@ def connnect_db():
|
||||
host='wxnasso.synology.me',
|
||||
user='wixon5',
|
||||
password='Wixon2022@!',
|
||||
database='wixon',
|
||||
database='test',
|
||||
charset="utf8mb4",
|
||||
cursorclass=pymysql.cursors.DictCursor, # DictCursor를 사용하여 딕셔너리 형태로 결과를 반환
|
||||
init_command='SET SQL_SAFE_UPDATES = 0;',
|
||||
@@ -92,8 +94,13 @@ def sql_execute(q, d, is_data=False, is_last_id=False):
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
# MySQL에서 모든 블로그 포스트를 가져옵니다.
|
||||
query = "SELECT `title`, `thumbnail_img`, `contents` FROM `blog` WHERE `use_yn` = 'Y' ORDER BY `add_date` DESC;"
|
||||
if 'user_info' in session: # 로그인된 사용자가 있을 경우
|
||||
# 외부 공개 안된 글도 포함하여 모든 블로그 포스트를 가져옵니다.
|
||||
query = "SELECT `id`, `title`, `thumbnail_img`, `contents` FROM `blog` WHERE `use_yn` = 'Y' ORDER BY `add_date` DESC;"
|
||||
else: # 로그인된 사용자가 없을 경우
|
||||
# 외부 공개된 블로그 포스트만 가져옵니다.
|
||||
query = "SELECT `id`, `title`, `thumbnail_img`, `contents` FROM `blog` WHERE `use_yn` = 'Y' and `public_yn` = 'Y' ORDER BY `add_date` DESC;"
|
||||
|
||||
r, posts = sql_execute(query, (), is_data=True)
|
||||
|
||||
# `index.html` 템플릿으로 데이터를 전달합니다.
|
||||
@@ -126,34 +133,122 @@ def logout():
|
||||
return redirect(url_for('index'))
|
||||
|
||||
|
||||
@app.route('/post/<int:post_id>')
|
||||
def post(post_id):
|
||||
# SQL 쿼리와 삽입할 데이터 설정
|
||||
query = "SELECT blog.*, member.mb_name, member.mb_id FROM blog INNER JOIN member ON blog.user_id = member.mb_idx WHERE blog.id = %s"
|
||||
data = (post_id,)
|
||||
|
||||
# Post를 데이터베이스에서 검색
|
||||
result, fetched_data = sql_execute(query, data, is_data=True)
|
||||
|
||||
if result:
|
||||
# 검색 성공시, 포스트 상세 페이지로 이동
|
||||
post = fetched_data[0]
|
||||
|
||||
# 이전과 다음 포스트를 찾기
|
||||
if 'user_info' in session: # 로그인된 사용자가 있을 경우
|
||||
# 외부 공개 안된 글도 포함하여 모든 블로그 포스트를 가져옵니다.
|
||||
prev_query = "SELECT blog.*, member.mb_name, member.mb_id FROM blog INNER JOIN member ON blog.user_id = member.mb_idx WHERE blog.add_date < %s AND blog.use_yn = 'Y' ORDER BY blog.add_date DESC LIMIT 1"
|
||||
next_query = "SELECT blog.*, member.mb_name, member.mb_id FROM blog INNER JOIN member ON blog.user_id = member.mb_idx WHERE blog.add_date > %s AND blog.use_yn = 'Y' ORDER BY blog.add_date ASC LIMIT 1"
|
||||
else: # 로그인된 사용자가 없을 경우
|
||||
# 외부 공개된 블로그 포스트만 가져옵니다.
|
||||
prev_query = "SELECT blog.*, member.mb_name, member.mb_id FROM blog INNER JOIN member ON blog.user_id = member.mb_idx WHERE blog.add_date < %s AND blog.public_yn = 'Y' AND blog.use_yn = 'Y' ORDER BY blog.add_date DESC LIMIT 1"
|
||||
next_query = "SELECT blog.*, member.mb_name, member.mb_id FROM blog INNER JOIN member ON blog.user_id = member.mb_idx WHERE blog.add_date > %s AND blog.public_yn = 'Y' AND blog.use_yn = 'Y' ORDER BY blog.add_date ASC LIMIT 1"
|
||||
|
||||
prev_post = None
|
||||
next_post = None
|
||||
|
||||
prev_result, prev_fetched_data = sql_execute(prev_query, (post['add_date'],), is_data=True)
|
||||
next_result, next_fetched_data = sql_execute(next_query, (post['add_date'],), is_data=True)
|
||||
|
||||
if prev_result and prev_fetched_data:
|
||||
prev_post = prev_fetched_data[0]
|
||||
if next_result and next_fetched_data:
|
||||
next_post = next_fetched_data[0]
|
||||
|
||||
return render_template('post.html', post=post, prev_post=prev_post, next_post=next_post)
|
||||
else:
|
||||
# 검색 실패시, 에러 메시지 반환
|
||||
return "Failed to fetch the post", 500
|
||||
|
||||
|
||||
@app.route('/edit_post/<int:post_id>', methods=['GET', 'POST'])
|
||||
def edit_post(post_id):
|
||||
if 'username' not in session or ('username' in session and session['username'] not in ['admin', 'wixon']):
|
||||
flash('You are not allowed to edit this post.')
|
||||
return redirect(url_for('index'))
|
||||
|
||||
if request.method == 'POST':
|
||||
# 포스트 업데이트 로직
|
||||
title = request.form.get('title')
|
||||
category = request.form.get('category')
|
||||
public_yn = 'Y' if request.form.get('public') == 'on' else 'N'
|
||||
contents = request.form.get('contents')
|
||||
|
||||
query = "UPDATE blog SET title = %s, category = %s, public_yn = %s, contents = %s WHERE id = %s"
|
||||
data = (title, category, public_yn, contents, post_id)
|
||||
|
||||
res = sql_execute(query, data)
|
||||
if res:
|
||||
flash('Post updated successfully.')
|
||||
return redirect(url_for('post', post_id=post_id))
|
||||
else:
|
||||
flash('Failed to update the post.')
|
||||
return redirect(url_for('edit_post', post_id=post_id))
|
||||
|
||||
else:
|
||||
# 포스트 가져오기 로직
|
||||
query = "SELECT * FROM blog WHERE id = %s"
|
||||
data = (post_id,)
|
||||
|
||||
res, fetched_data = sql_execute(query, data, is_data=True)
|
||||
if res and fetched_data:
|
||||
return render_template('edit_post.html', post=fetched_data[0])
|
||||
else:
|
||||
flash('Failed to fetch the post.')
|
||||
return redirect(url_for('index'))
|
||||
|
||||
|
||||
@app.route('/write', methods=['GET', 'POST'])
|
||||
def write():
|
||||
if 'user_info' not in session:
|
||||
flash("You need to login first.")
|
||||
return redirect(url_for('login'))
|
||||
|
||||
if request.method == 'POST':
|
||||
user_id = session['user_info']['mb_idx']
|
||||
title = request.form['title']
|
||||
category = request.form['category']
|
||||
contents = request.form['contents']
|
||||
is_public = 'Y' if request.form.get('public') == 'on' else 'N'
|
||||
|
||||
# 본문에서 첫 번째 이미지 추출
|
||||
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)
|
||||
query = "INSERT INTO blog (user_id, title, category, contents, thumbnail_img, public_yn) VALUES (%s, %s, %s, %s, %s, %s)"
|
||||
data = (user_id, title, category, contents, thumbnail_img, is_public)
|
||||
|
||||
# 새로운 글 저장
|
||||
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')
|
||||
|
||||
|
||||
@app.route('/blog/<int:id>', methods=['DELETE'])
|
||||
def delete_post(id):
|
||||
query = "UPDATE `blog` SET `use_yn` = 'N' WHERE `id` = %s;"
|
||||
res = sql_execute(query, (id,))
|
||||
if res:
|
||||
return jsonify(success=True, message='Post deleted successfully'), 200
|
||||
else:
|
||||
return jsonify(success=False, message='Could not delete the post'), 500
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=8899)
|
||||
|
||||
@@ -2,45 +2,61 @@
|
||||
<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;
|
||||
a:link {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.blog-card img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
a:visited {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.grid-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
a:hover {
|
||||
color: inherit;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #888;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #555;
|
||||
}
|
||||
</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>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery.imagesloaded/4.1.4/imagesloaded.pkgd.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/masonry/4.2.2/masonry.pkgd.min.js"></script>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></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="#">
|
||||
<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>
|
||||
|
||||
@@ -54,6 +70,7 @@
|
||||
<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 target="_blank" href="http://www.wixon.co.kr/backS1te">관리자 페이지</a></li>
|
||||
<li class="uk-active"><a href="/logout">로그아웃</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
73
templates/edit_post.html
Normal file
73
templates/edit_post.html
Normal file
@@ -0,0 +1,73 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="uk-container uk-margin-top">
|
||||
<h1 class="uk-text-center"><span>포스트 수정</span></h1>
|
||||
<form action="/edit_post/{{ post.id }}" 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" value="{{ post.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" {% if post.category == 'IT' %} selected {% endif %}>IT</option>
|
||||
<option value="NEWS" {% if post.category == 'NEWS' %} selected {% endif %}>NEWS</option>
|
||||
<option value="ETC" {% if post.category == 'ETC' %} selected {% endif %}>ETC</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="uk-margin">
|
||||
<label class="uk-form-label" for="public">공개 여부</label>
|
||||
<div class="uk-form-controls">
|
||||
<label><input class="uk-checkbox" id="public" type="checkbox" name="public" {% if post.is_public %}
|
||||
checked {% endif %}> 이 글을 외부 공개합니다</label>
|
||||
</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>{{ post.contents }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="uk-margin">
|
||||
<a style="float: left;" href="{{ url_for('post', post_id=post.id) }}" class="uk-button uk-button-default">뒤로가기</a>
|
||||
<button style="float: right;" type="submit" class="uk-button uk-button-primary">글 수정</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 %}
|
||||
@@ -1,32 +1,37 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<style>
|
||||
.blog-card {
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.blog-card img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
</style>
|
||||
<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 class="uk-grid uk-grid-small uk-child-width-1-4@m uk-child-width-1-2@s" uk-grid>
|
||||
{% if not posts %}
|
||||
<h4 style="margin-top: 30px">작성된 포스트가 없습니다.</h4>
|
||||
{% else %}
|
||||
{% for post in posts %}
|
||||
<div>
|
||||
<div class="uk-card uk-card-default blog-card">
|
||||
<div class="uk-card-media-top">
|
||||
<img src="{{ post.thumbnail_img | default('https://via.placeholder.com/300x200', true) }}"
|
||||
alt="{{ post.title }}" style="max-width: 100%;max-height: 500px;">
|
||||
</div>
|
||||
<div class="uk-card-body">
|
||||
<h3 class="uk-card-title">{{ post.title }}</h3>
|
||||
<a href="{{ url_for('post', post_id=post.id) }}" class="uk-button uk-button-text">Read
|
||||
more</a>
|
||||
</div>
|
||||
</div>
|
||||
</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 %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
$('.masonry-grid').masonry({
|
||||
// options...
|
||||
itemSelector: '.blog-card',
|
||||
columnWidth: '.blog-card',
|
||||
percentPosition: true
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
92
templates/post.html
Normal file
92
templates/post.html
Normal file
@@ -0,0 +1,92 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="uk-container" style="margin-top: 30px">
|
||||
<div class="uk-card uk-card-default uk-margin" style="padding: 10px 30px">
|
||||
<div class="uk-card-body" style="padding: 10px 30px">
|
||||
{% if g.is_login and (g.user_info.mb_id == post.mb_id or g.user_info.mb_id in ['admin', 'wixon']) %}
|
||||
<div style="float: right;color: white">
|
||||
<a href="{{ url_for('edit_post', post_id=post.id) }}" class="uk-button uk-button-primary">수정</a>
|
||||
<a class="uk-button uk-button-danger" href="javascript:blogDelete('{{ post.id }}');">삭제</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<h3 class="uk-card-title">[{{ post.category }}] {{ post.title }}</h3>
|
||||
<p class="uk-text-meta" style="float: right">Posted on {{ post.add_date }}</p>
|
||||
<div style="width: 100%;border-top: 1px solid #eee;padding: 15px">
|
||||
{{ post.contents | safe }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="uk-card uk-card-default uk-margin custom-table">
|
||||
<div class="uk-card-body" style="padding: 0px">
|
||||
<table class="uk-table uk-table-divider">
|
||||
<tbody>
|
||||
{% if prev_post is not none %}
|
||||
<tr>
|
||||
<td style="width: 80px;text-align: center;padding: 8px 6px;">이전</td>
|
||||
<td style="padding: 8px 6px;">
|
||||
<a href="{{ url_for('post', post_id=prev_post.id) }}">{{ prev_post.title }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if next_post is not none %}
|
||||
<tr>
|
||||
<td style="width: 80px;text-align: center;padding: 8px 6px;">다음</td>
|
||||
<td style="padding: 8px 6px;">
|
||||
<a href="{{ url_for('post', post_id=next_post.id) }}">
|
||||
<div style="width: 100%">
|
||||
{{ next_post.title }}
|
||||
</div>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div style="text-align: right;padding: 15px;">
|
||||
<a href="{{ url_for('index') }}" class="uk-button uk-button-default">목록</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function blogDelete(id) {
|
||||
Swal.fire({
|
||||
title: '정말로 삭제하시겠습니까?',
|
||||
text: "이 작업은 되돌릴 수 없습니다!",
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#3085d6',
|
||||
cancelButtonColor: '#d33',
|
||||
confirmButtonText: '네, 삭제합니다!'
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
$.ajax({
|
||||
url: "/blog/" + id,
|
||||
type: "DELETE",
|
||||
success: function (result) {
|
||||
Swal.fire(
|
||||
'삭제 완료!',
|
||||
'포스트가 성공적으로 삭제되었습니다.',
|
||||
'success'
|
||||
);
|
||||
// 포스트를 UI에서 삭제하거나 페이지를 다시 로드합니다.
|
||||
location.href = '/';
|
||||
},
|
||||
error: function (jqXHR, textStatus, errorThrown) {
|
||||
Swal.fire(
|
||||
'오류!',
|
||||
'문제가 발생했습니다: ' + textStatus,
|
||||
'error'
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -21,6 +21,13 @@
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Here is your new checkbox field -->
|
||||
<div class="uk-margin">
|
||||
<label class="uk-form-label" for="public">공개 여부</label>
|
||||
<div class="uk-form-controls">
|
||||
<label><input class="uk-checkbox" id="public" type="checkbox" name="public"> 이 글을 외부 공개합니다</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="uk-margin">
|
||||
<label class="uk-form-label" for="contents">내용</label>
|
||||
<div class="uk-form-controls">
|
||||
@@ -28,7 +35,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="uk-margin">
|
||||
<button type="submit" class="uk-button uk-button-default">글 작성</button>
|
||||
<a href="{{ url_for('index') }}" class="uk-button uk-button-default">목록</a>
|
||||
<button style="float: right" type="submit" class="uk-button uk-button-primary">글 작성</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user