관리자 페이지 추가 및 버그 수정
- 관리자 대시보드 추가 (/admin/) - 통계: 총 포스트, 공개/비공개, 삭제된 포스트, 회원 수 - 포스트 관리 추가 (/admin/posts) - 목록, 검색, 필터링, 페이지네이션 - 포스트 수정, 삭제, 복구 기능 - 회원 관리 추가 (/admin/members) - 회원 목록, 추가, 수정, 삭제 - 비밀번호 재설정 - 버그 수정 - g.is_login, g.user_info 기본값 설정 - index 페이지 빈 포스트 처리 - 관리자 권한: admin, wixon, javamon - README.md 프로젝트 문서 추가 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
251
app.py
251
app.py
@@ -9,6 +9,8 @@ import uuid
|
||||
import re
|
||||
from markupsafe import Markup
|
||||
from jinja2 import filters
|
||||
from functools import wraps
|
||||
import math
|
||||
|
||||
UPLOAD_FOLDER = 'static/upload/img' # 경로를 Flask 앱 루트 기준으로 수정
|
||||
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}
|
||||
@@ -25,6 +27,8 @@ def remove_html_tags(text):
|
||||
|
||||
@app.before_request
|
||||
def load_user():
|
||||
g.is_login = False
|
||||
g.user_info = None
|
||||
if 'user_info' in session:
|
||||
g.is_login = True
|
||||
g.user_info = session['user_info']
|
||||
@@ -110,7 +114,8 @@ def index():
|
||||
r, posts = sql_execute(query, (), is_data=True)
|
||||
r, random_post = sql_execute(r_query, (), is_data=True)
|
||||
|
||||
posts.append(random_post[0])
|
||||
if random_post:
|
||||
posts.append(random_post[0])
|
||||
|
||||
# 태그 제거 후 150자로 제한
|
||||
for post in posts:
|
||||
@@ -261,5 +266,249 @@ def delete_post(id):
|
||||
return jsonify(success=False, message='Could not delete the post'), 500
|
||||
|
||||
|
||||
# ============================================
|
||||
# 관리자 페이지
|
||||
# ============================================
|
||||
|
||||
def admin_required(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if 'username' not in session or session['username'] not in ['admin', 'wixon', 'javamon']:
|
||||
flash('관리자 권한이 필요합니다.')
|
||||
return redirect(url_for('login'))
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
|
||||
|
||||
@app.route('/admin/')
|
||||
@admin_required
|
||||
def admin_dashboard():
|
||||
stats = {
|
||||
'total_posts': 0,
|
||||
'public_posts': 0,
|
||||
'private_posts': 0,
|
||||
'deleted_posts': 0,
|
||||
'total_members': 0
|
||||
}
|
||||
|
||||
r, data = sql_execute("SELECT COUNT(*) as cnt FROM blog WHERE use_yn='Y'", (), is_data=True)
|
||||
if r and data:
|
||||
stats['total_posts'] = data[0]['cnt']
|
||||
|
||||
r, data = sql_execute("SELECT COUNT(*) as cnt FROM blog WHERE use_yn='Y' AND public_yn='Y'", (), is_data=True)
|
||||
if r and data:
|
||||
stats['public_posts'] = data[0]['cnt']
|
||||
|
||||
r, data = sql_execute("SELECT COUNT(*) as cnt FROM blog WHERE use_yn='Y' AND public_yn='N'", (), is_data=True)
|
||||
if r and data:
|
||||
stats['private_posts'] = data[0]['cnt']
|
||||
|
||||
r, data = sql_execute("SELECT COUNT(*) as cnt FROM blog WHERE use_yn='N'", (), is_data=True)
|
||||
if r and data:
|
||||
stats['deleted_posts'] = data[0]['cnt']
|
||||
|
||||
r, data = sql_execute("SELECT COUNT(*) as cnt FROM member", (), is_data=True)
|
||||
if r and data:
|
||||
stats['total_members'] = data[0]['cnt']
|
||||
|
||||
return render_template('admin/dashboard.html', stats=stats)
|
||||
|
||||
|
||||
@app.route('/admin/posts')
|
||||
@admin_required
|
||||
def admin_posts():
|
||||
page = request.args.get('page', 1, type=int)
|
||||
per_page = 20
|
||||
offset = (page - 1) * per_page
|
||||
|
||||
category = request.args.get('category', '')
|
||||
public_yn = request.args.get('public_yn', '')
|
||||
use_yn = request.args.get('use_yn', 'Y')
|
||||
search = request.args.get('search', '')
|
||||
|
||||
where_clauses = []
|
||||
params = []
|
||||
|
||||
if category:
|
||||
where_clauses.append("blog.category = %s")
|
||||
params.append(category)
|
||||
if public_yn:
|
||||
where_clauses.append("blog.public_yn = %s")
|
||||
params.append(public_yn)
|
||||
if use_yn:
|
||||
where_clauses.append("blog.use_yn = %s")
|
||||
params.append(use_yn)
|
||||
if search:
|
||||
where_clauses.append("blog.title LIKE %s")
|
||||
params.append(f"%{search}%")
|
||||
|
||||
where_sql = " AND ".join(where_clauses) if where_clauses else "1=1"
|
||||
|
||||
count_query = f"SELECT COUNT(*) as cnt FROM blog INNER JOIN member ON blog.user_id = member.mb_idx WHERE {where_sql}"
|
||||
r, count_data = sql_execute(count_query, tuple(params), is_data=True)
|
||||
total = count_data[0]['cnt'] if r and count_data else 0
|
||||
total_pages = math.ceil(total / per_page) if total > 0 else 1
|
||||
|
||||
query = f"""
|
||||
SELECT blog.*, member.mb_name, member.mb_id
|
||||
FROM blog
|
||||
INNER JOIN member ON blog.user_id = member.mb_idx
|
||||
WHERE {where_sql}
|
||||
ORDER BY blog.add_date DESC
|
||||
LIMIT %s OFFSET %s
|
||||
"""
|
||||
params.extend([per_page, offset])
|
||||
r, posts = sql_execute(query, tuple(params), is_data=True)
|
||||
|
||||
pagination = {
|
||||
'page': page,
|
||||
'per_page': per_page,
|
||||
'total': total,
|
||||
'total_pages': total_pages,
|
||||
'has_prev': page > 1,
|
||||
'has_next': page < total_pages,
|
||||
'prev_num': page - 1,
|
||||
'next_num': page + 1
|
||||
}
|
||||
|
||||
return render_template('admin/posts.html', posts=posts if posts else [], pagination=pagination)
|
||||
|
||||
|
||||
@app.route('/admin/posts/<int:post_id>', methods=['GET', 'POST'])
|
||||
@admin_required
|
||||
def admin_post_detail(post_id):
|
||||
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')
|
||||
|
||||
soup = BeautifulSoup(contents, 'html.parser')
|
||||
first_image = soup.find('img')
|
||||
thumbnail_img = first_image['src'] if first_image else None
|
||||
|
||||
query = "UPDATE blog SET title=%s, category=%s, public_yn=%s, contents=%s, thumbnail_img=%s WHERE id=%s"
|
||||
res = sql_execute(query, (title, category, public_yn, contents, thumbnail_img, post_id))
|
||||
|
||||
if res:
|
||||
flash('포스트가 수정되었습니다.')
|
||||
return redirect(url_for('admin_posts'))
|
||||
else:
|
||||
flash('포스트 수정에 실패했습니다.')
|
||||
|
||||
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"
|
||||
r, data = sql_execute(query, (post_id,), is_data=True)
|
||||
|
||||
if r and data:
|
||||
return render_template('admin/post_detail.html', post=data[0])
|
||||
else:
|
||||
flash('포스트를 찾을 수 없습니다.')
|
||||
return redirect(url_for('admin_posts'))
|
||||
|
||||
|
||||
@app.route('/admin/posts/<int:post_id>/delete', methods=['DELETE'])
|
||||
@admin_required
|
||||
def admin_post_delete(post_id):
|
||||
query = "UPDATE blog SET use_yn='N' WHERE id=%s"
|
||||
res = sql_execute(query, (post_id,))
|
||||
if res:
|
||||
return jsonify(success=True, message='포스트가 삭제되었습니다.')
|
||||
return jsonify(success=False, message='삭제 실패'), 500
|
||||
|
||||
|
||||
@app.route('/admin/posts/<int:post_id>/restore', methods=['POST'])
|
||||
@admin_required
|
||||
def admin_post_restore(post_id):
|
||||
query = "UPDATE blog SET use_yn='Y' WHERE id=%s"
|
||||
res = sql_execute(query, (post_id,))
|
||||
if res:
|
||||
return jsonify(success=True, message='포스트가 복구되었습니다.')
|
||||
return jsonify(success=False, message='복구 실패'), 500
|
||||
|
||||
|
||||
@app.route('/admin/members')
|
||||
@admin_required
|
||||
def admin_members():
|
||||
query = "SELECT mb_idx, mb_id, mb_name FROM member ORDER BY mb_idx DESC"
|
||||
r, members = sql_execute(query, (), is_data=True)
|
||||
return render_template('admin/members.html', members=members if members else [])
|
||||
|
||||
|
||||
@app.route('/admin/members/add', methods=['GET', 'POST'])
|
||||
@admin_required
|
||||
def admin_member_add():
|
||||
if request.method == 'POST':
|
||||
mb_id = request.form.get('mb_id')
|
||||
mb_name = request.form.get('mb_name')
|
||||
password = request.form.get('password').encode('utf-8')
|
||||
hashed_pw = bcrypt.hashpw(password, bcrypt.gensalt()).decode('utf-8')
|
||||
|
||||
r, existing = sql_execute("SELECT mb_id FROM member WHERE mb_id=%s", (mb_id,), is_data=True)
|
||||
if r and existing:
|
||||
flash('이미 존재하는 아이디입니다.')
|
||||
return render_template('admin/member_form.html', member=None)
|
||||
|
||||
query = "INSERT INTO member (mb_id, mb_passwd, mb_name) VALUES (%s, %s, %s)"
|
||||
res = sql_execute(query, (mb_id, hashed_pw, mb_name))
|
||||
if res:
|
||||
flash('회원이 추가되었습니다.')
|
||||
return redirect(url_for('admin_members'))
|
||||
flash('회원 추가에 실패했습니다.')
|
||||
return render_template('admin/member_form.html', member=None)
|
||||
|
||||
|
||||
@app.route('/admin/members/<int:mb_idx>', methods=['GET', 'POST'])
|
||||
@admin_required
|
||||
def admin_member_detail(mb_idx):
|
||||
if request.method == 'POST':
|
||||
mb_name = request.form.get('mb_name')
|
||||
query = "UPDATE member SET mb_name=%s WHERE mb_idx=%s"
|
||||
res = sql_execute(query, (mb_name, mb_idx))
|
||||
if res:
|
||||
flash('회원 정보가 수정되었습니다.')
|
||||
return redirect(url_for('admin_members'))
|
||||
flash('회원 정보 수정에 실패했습니다.')
|
||||
|
||||
query = "SELECT mb_idx, mb_id, mb_name FROM member WHERE mb_idx=%s"
|
||||
r, member = sql_execute(query, (mb_idx,), is_data=True)
|
||||
|
||||
if r and member:
|
||||
return render_template('admin/member_form.html', member=member[0])
|
||||
else:
|
||||
flash('회원을 찾을 수 없습니다.')
|
||||
return redirect(url_for('admin_members'))
|
||||
|
||||
|
||||
@app.route('/admin/members/<int:mb_idx>/delete', methods=['DELETE'])
|
||||
@admin_required
|
||||
def admin_member_delete(mb_idx):
|
||||
if session.get('user_info', {}).get('mb_idx') == mb_idx:
|
||||
return jsonify(success=False, message='자기 자신은 삭제할 수 없습니다.'), 400
|
||||
|
||||
query = "DELETE FROM member WHERE mb_idx=%s"
|
||||
res = sql_execute(query, (mb_idx,))
|
||||
if res:
|
||||
return jsonify(success=True, message='회원이 삭제되었습니다.')
|
||||
return jsonify(success=False, message='삭제 실패'), 500
|
||||
|
||||
|
||||
@app.route('/admin/members/<int:mb_idx>/reset-password', methods=['POST'])
|
||||
@admin_required
|
||||
def admin_member_reset_password(mb_idx):
|
||||
data = request.get_json()
|
||||
new_password = data.get('password', '').encode('utf-8')
|
||||
|
||||
if len(new_password) < 4:
|
||||
return jsonify(success=False, message='비밀번호는 4자 이상이어야 합니다.'), 400
|
||||
|
||||
hashed_pw = bcrypt.hashpw(new_password, bcrypt.gensalt()).decode('utf-8')
|
||||
|
||||
query = "UPDATE member SET mb_passwd=%s WHERE mb_idx=%s"
|
||||
res = sql_execute(query, (hashed_pw, mb_idx))
|
||||
if res:
|
||||
return jsonify(success=True, message='비밀번호가 재설정되었습니다.')
|
||||
return jsonify(success=False, message='비밀번호 재설정 실패'), 500
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=8899)
|
||||
|
||||
Reference in New Issue
Block a user