Compare commits
10 Commits
0509b92a59
...
56ee50a29f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
56ee50a29f | ||
|
|
39de8edde4 | ||
|
|
2635aff0ea | ||
|
|
8e759144bf | ||
|
|
7f8b372ed9 | ||
|
|
5c71a6f001 | ||
|
|
196988f494 | ||
|
|
d49a33cc6c | ||
|
|
73a13852bf | ||
|
|
a1000d8c94 |
41
DEPLOY.md
Normal file
41
DEPLOY.md
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# 배포 가이드
|
||||||
|
|
||||||
|
## 자동 배포 (Webhook)
|
||||||
|
|
||||||
|
GitHub에 push하면 자동으로 배포됩니다.
|
||||||
|
|
||||||
|
### 동작 방식
|
||||||
|
1. GitHub push 발생
|
||||||
|
2. Webhook이 서버로 요청 전송
|
||||||
|
3. `/var/lib/jenkins/webhooks/deploy-wixon-blog.sh` 실행
|
||||||
|
4. 기존 프로세스 종료 → git pull → 서버 재시작
|
||||||
|
|
||||||
|
### 배포 스크립트 위치
|
||||||
|
```
|
||||||
|
/var/lib/jenkins/webhooks/deploy-wixon-blog.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### 서버 정보
|
||||||
|
- **도메인**: https://blog.wxn.co.kr/
|
||||||
|
- **포트**: 8899
|
||||||
|
- **프로젝트 경로**: `/var/lib/jenkins/workspace/wixon_blog`
|
||||||
|
- **로그 파일**: `/var/lib/jenkins/workspace/wixon_blog/log`
|
||||||
|
|
||||||
|
## 수동 배포
|
||||||
|
|
||||||
|
서버에서 직접 실행:
|
||||||
|
```bash
|
||||||
|
/var/lib/jenkins/webhooks/deploy-wixon-blog.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## 로그 확인
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tail -f /var/lib/jenkins/workspace/wixon_blog/log
|
||||||
|
```
|
||||||
|
|
||||||
|
## 프로세스 확인
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ps aux | grep "python3.*app.py"
|
||||||
|
```
|
||||||
111
README.md
111
README.md
@@ -1,2 +1,109 @@
|
|||||||
"# wixon_blog" "# wixon_blog"
|
# WIXON Blog
|
||||||
"# wixon_blog"
|
|
||||||
|
Flask 기반 블로그 애플리케이션입니다.
|
||||||
|
|
||||||
|
## 프로젝트 구조
|
||||||
|
|
||||||
|
```
|
||||||
|
wixon_blog/
|
||||||
|
├── app.py # Flask 메인 애플리케이션
|
||||||
|
├── templates/ # Jinja2 템플릿
|
||||||
|
│ ├── base.html # 기본 레이아웃
|
||||||
|
│ ├── index.html # 메인 페이지
|
||||||
|
│ ├── login.html # 로그인 페이지
|
||||||
|
│ ├── post.html # 포스트 상세
|
||||||
|
│ ├── write.html # 포스트 작성
|
||||||
|
│ ├── edit_post.html # 포스트 수정
|
||||||
|
│ └── admin/ # 관리자 페이지
|
||||||
|
│ ├── base_admin.html # 관리자 베이스 템플릿
|
||||||
|
│ ├── dashboard.html # 대시보드
|
||||||
|
│ ├── posts.html # 포스트 관리
|
||||||
|
│ ├── post_detail.html # 포스트 수정
|
||||||
|
│ ├── members.html # 회원 목록
|
||||||
|
│ └── member_form.html # 회원 추가/수정
|
||||||
|
├── static/
|
||||||
|
│ ├── css/
|
||||||
|
│ │ ├── style.css # 메인 스타일
|
||||||
|
│ │ └── admin.css # 관리자 스타일
|
||||||
|
│ ├── images/ # 이미지 파일
|
||||||
|
│ ├── font/ # 웹 폰트
|
||||||
|
│ └── upload/img/ # 업로드 이미지
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## 기술 스택
|
||||||
|
|
||||||
|
- **Backend**: Flask (Python)
|
||||||
|
- **Database**: MySQL
|
||||||
|
- **Frontend**: UIKit 3.6.16, jQuery, Summernote Editor
|
||||||
|
- **인증**: bcrypt 암호화, Session 기반
|
||||||
|
|
||||||
|
## 주요 기능
|
||||||
|
|
||||||
|
### 사용자 기능
|
||||||
|
- 블로그 포스트 조회
|
||||||
|
- 로그인/로그아웃
|
||||||
|
- 포스트 작성 (로그인 필요)
|
||||||
|
- 포스트 수정/삭제 (작성자 또는 관리자)
|
||||||
|
|
||||||
|
### 관리자 기능 (`/admin/`)
|
||||||
|
- **대시보드**: 통계 (총 포스트, 공개/비공개, 회원 수)
|
||||||
|
- **포스트 관리**: 목록, 검색, 필터링, 수정, 삭제, 복구
|
||||||
|
- **회원 관리**: 목록, 추가, 수정, 삭제, 비밀번호 재설정
|
||||||
|
|
||||||
|
## URL 구조
|
||||||
|
|
||||||
|
| URL | 설명 |
|
||||||
|
|-----|------|
|
||||||
|
| `/` | 메인 페이지 |
|
||||||
|
| `/login` | 로그인 |
|
||||||
|
| `/logout` | 로그아웃 |
|
||||||
|
| `/post/<id>` | 포스트 상세 |
|
||||||
|
| `/write` | 포스트 작성 |
|
||||||
|
| `/edit_post/<id>` | 포스트 수정 |
|
||||||
|
| `/admin/` | 관리자 대시보드 |
|
||||||
|
| `/admin/posts` | 포스트 관리 |
|
||||||
|
| `/admin/members` | 회원 관리 |
|
||||||
|
|
||||||
|
## 실행 방법
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 의존성 설치
|
||||||
|
pip install flask pymysql bcrypt beautifulsoup4 markupsafe
|
||||||
|
|
||||||
|
# 앱 실행
|
||||||
|
python3 app.py
|
||||||
|
```
|
||||||
|
|
||||||
|
서버가 `http://0.0.0.0:8899`에서 실행됩니다.
|
||||||
|
|
||||||
|
## 데이터베이스 테이블
|
||||||
|
|
||||||
|
### blog
|
||||||
|
| 컬럼 | 설명 |
|
||||||
|
|------|------|
|
||||||
|
| id | PK |
|
||||||
|
| user_id | 작성자 ID (FK) |
|
||||||
|
| title | 제목 |
|
||||||
|
| category | 카테고리 (IT, NEWS, ETC) |
|
||||||
|
| contents | 내용 (HTML) |
|
||||||
|
| thumbnail_img | 썸네일 URL |
|
||||||
|
| public_yn | 공개 여부 (Y/N) |
|
||||||
|
| use_yn | 사용 여부 (Y/N, 소프트 삭제) |
|
||||||
|
| add_date | 작성일 |
|
||||||
|
|
||||||
|
### member
|
||||||
|
| 컬럼 | 설명 |
|
||||||
|
|------|------|
|
||||||
|
| mb_idx | PK |
|
||||||
|
| mb_id | 로그인 ID |
|
||||||
|
| mb_passwd | 비밀번호 (bcrypt) |
|
||||||
|
| mb_name | 이름 |
|
||||||
|
|
||||||
|
## 관리자 계정
|
||||||
|
|
||||||
|
- 관리자 권한: `admin`, `wixon`, `javamon` 아이디 보유자
|
||||||
|
|
||||||
|
## 라이선스
|
||||||
|
|
||||||
|
WIXON Associates Inc.
|
||||||
|
|||||||
309
app.py
309
app.py
@@ -9,6 +9,8 @@ import uuid
|
|||||||
import re
|
import re
|
||||||
from markupsafe import Markup
|
from markupsafe import Markup
|
||||||
from jinja2 import filters
|
from jinja2 import filters
|
||||||
|
from functools import wraps
|
||||||
|
import math
|
||||||
|
|
||||||
UPLOAD_FOLDER = 'static/upload/img' # 경로를 Flask 앱 루트 기준으로 수정
|
UPLOAD_FOLDER = 'static/upload/img' # 경로를 Flask 앱 루트 기준으로 수정
|
||||||
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}
|
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}
|
||||||
@@ -25,6 +27,8 @@ def remove_html_tags(text):
|
|||||||
|
|
||||||
@app.before_request
|
@app.before_request
|
||||||
def load_user():
|
def load_user():
|
||||||
|
g.is_login = False
|
||||||
|
g.user_info = None
|
||||||
if 'user_info' in session:
|
if 'user_info' in session:
|
||||||
g.is_login = True
|
g.is_login = True
|
||||||
g.user_info = session['user_info']
|
g.user_info = session['user_info']
|
||||||
@@ -110,6 +114,7 @@ def index():
|
|||||||
r, posts = sql_execute(query, (), is_data=True)
|
r, posts = sql_execute(query, (), is_data=True)
|
||||||
r, random_post = sql_execute(r_query, (), is_data=True)
|
r, random_post = sql_execute(r_query, (), is_data=True)
|
||||||
|
|
||||||
|
if random_post:
|
||||||
posts.append(random_post[0])
|
posts.append(random_post[0])
|
||||||
|
|
||||||
# 태그 제거 후 150자로 제한
|
# 태그 제거 후 150자로 제한
|
||||||
@@ -260,6 +265,310 @@ def delete_post(id):
|
|||||||
else:
|
else:
|
||||||
return jsonify(success=False, message='Could not delete the post'), 500
|
return jsonify(success=False, message='Could not delete the post'), 500
|
||||||
|
|
||||||
|
@app.route('/list')
|
||||||
|
@app.route('/list/<string:cate>')
|
||||||
|
def list(cate=None):
|
||||||
|
params = ()
|
||||||
|
cate_condition = ""
|
||||||
|
|
||||||
|
if cate:
|
||||||
|
cate_condition = "AND `category` = %s"
|
||||||
|
params = (cate,)
|
||||||
|
|
||||||
|
if 'user_info' in session: # 로그인된 사용자
|
||||||
|
query = f"""SELECT `id`, `title`, `category`, `thumbnail_img`, `contents`, `add_date`
|
||||||
|
FROM `blog`
|
||||||
|
WHERE `use_yn` = 'Y' {cate_condition}
|
||||||
|
ORDER BY `add_date` DESC LIMIT 10;"""
|
||||||
|
else:
|
||||||
|
query = f"""SELECT `id`, `title`, `category`, `thumbnail_img`, `contents`, `add_date`
|
||||||
|
FROM `blog`
|
||||||
|
WHERE `use_yn` = 'Y' AND `public_yn` = 'Y' {cate_condition}
|
||||||
|
ORDER BY `add_date` DESC LIMIT 10;"""
|
||||||
|
|
||||||
|
r, posts = sql_execute(query, params, is_data=True)
|
||||||
|
|
||||||
|
# 태그 제거 후 150자로 제한
|
||||||
|
for post in posts:
|
||||||
|
post['contents'] = remove_html_tags(post['contents'])[:150]
|
||||||
|
|
||||||
|
return render_template('list.html', posts=posts)
|
||||||
|
|
||||||
|
@app.route('/list_more')
|
||||||
|
def list_more():
|
||||||
|
page = int(request.args.get('page', 1))
|
||||||
|
limit = 10
|
||||||
|
offset = (page - 1) * limit
|
||||||
|
|
||||||
|
cate = request.args.get('cate')
|
||||||
|
cate_condition = ""
|
||||||
|
params = []
|
||||||
|
|
||||||
|
if cate:
|
||||||
|
cate_condition = "AND `category` = %s"
|
||||||
|
params.append(cate)
|
||||||
|
|
||||||
|
query = f"""
|
||||||
|
SELECT `id`, `title`, `category`, `thumbnail_img`, `contents`, `add_date`
|
||||||
|
FROM `blog`
|
||||||
|
WHERE `use_yn` = 'Y'
|
||||||
|
{ "AND `public_yn` = 'Y'" if 'user_info' not in session else "" }
|
||||||
|
{cate_condition}
|
||||||
|
ORDER BY `add_date` DESC
|
||||||
|
LIMIT %s OFFSET %s
|
||||||
|
"""
|
||||||
|
params.extend([limit, offset])
|
||||||
|
r, posts = sql_execute(query, tuple(params), is_data=True)
|
||||||
|
|
||||||
|
for post in posts:
|
||||||
|
post['contents'] = remove_html_tags(post['contents'])[:150]
|
||||||
|
post['add_date_str'] = post['add_date'].strftime("%b %d / %Y")
|
||||||
|
|
||||||
|
return jsonify(posts)
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# 관리자 페이지
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
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__':
|
if __name__ == '__main__':
|
||||||
app.run(host='0.0.0.0', port=8899)
|
app.run(host='0.0.0.0', port=8899)
|
||||||
|
|||||||
639
static/css/admin.css
Normal file
639
static/css/admin.css
Normal file
@@ -0,0 +1,639 @@
|
|||||||
|
/* ============================================
|
||||||
|
WIXON Blog Admin Styles
|
||||||
|
Best Practices Applied
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
/* Reset & Base */
|
||||||
|
.admin-body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
background: #f4f6f9;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-body * {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Admin Layout - Flexbox */
|
||||||
|
.admin__wrap {
|
||||||
|
display: flex;
|
||||||
|
min-height: 100vh;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
Sidebar
|
||||||
|
============================================ */
|
||||||
|
.admin__sidebar {
|
||||||
|
width: 260px;
|
||||||
|
min-width: 260px;
|
||||||
|
background: linear-gradient(180deg, #1e1e2d 0%, #1a1a27 100%);
|
||||||
|
color: #fff;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
z-index: 1000;
|
||||||
|
overflow-y: auto;
|
||||||
|
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin__logo {
|
||||||
|
padding: 25px 20px;
|
||||||
|
text-align: center;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin__logo a {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin__logo img {
|
||||||
|
max-width: 100px;
|
||||||
|
height: auto;
|
||||||
|
filter: brightness(0) invert(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin__logo span {
|
||||||
|
display: block;
|
||||||
|
color: #8a8a9e;
|
||||||
|
font-size: 11px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Navigation Menu */
|
||||||
|
.admin__menu {
|
||||||
|
list-style: none;
|
||||||
|
padding: 20px 15px;
|
||||||
|
margin: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin__menu li {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin__menu li a {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 16px;
|
||||||
|
color: #b5b5c3;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin__menu li a:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin__menu li a.active {
|
||||||
|
background: #901438;
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: 0 4px 15px rgba(144, 20, 56, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin__menu li a .uk-icon {
|
||||||
|
margin-right: 12px;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin__menu li a.active .uk-icon {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* User Section */
|
||||||
|
.admin__user {
|
||||||
|
padding: 20px;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
background: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin__user .user__name {
|
||||||
|
display: block;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin__user .user__links {
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin__user a {
|
||||||
|
color: #8a8a9e;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 13px;
|
||||||
|
transition: color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin__user a:hover {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
Main Content Area
|
||||||
|
============================================ */
|
||||||
|
.admin__content {
|
||||||
|
flex: 1;
|
||||||
|
margin-left: 260px;
|
||||||
|
padding: 30px;
|
||||||
|
background: #f4f6f9;
|
||||||
|
min-height: 100vh;
|
||||||
|
width: calc(100% - 260px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Page Header */
|
||||||
|
.page__header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page__header h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1e1e2d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin__content > h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1e1e2d;
|
||||||
|
margin: 0 0 30px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
Cards & Panels
|
||||||
|
============================================ */
|
||||||
|
.stat__card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 25px;
|
||||||
|
box-shadow: 0 2px 15px rgba(0, 0, 0, 0.04);
|
||||||
|
border: 1px solid #eef0f3;
|
||||||
|
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat__card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat__card h3 {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #7e8299;
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
font-weight: 500;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat__number {
|
||||||
|
font-size: 2.2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1e1e2d;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat__number.green { color: #1bc5bd; }
|
||||||
|
.stat__number.orange { color: #ffa800; }
|
||||||
|
.stat__number.red { color: #f64e60; }
|
||||||
|
.stat__number.blue { color: #3699ff; }
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
Tables
|
||||||
|
============================================ */
|
||||||
|
.admin__table {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 2px 15px rgba(0, 0, 0, 0.04);
|
||||||
|
border: 1px solid #eef0f3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin__table .uk-table {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin__table .uk-table th {
|
||||||
|
background: #f8f9fc;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #7e8299;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
padding: 15px 20px;
|
||||||
|
border-bottom: 1px solid #eef0f3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin__table .uk-table td {
|
||||||
|
padding: 15px 20px;
|
||||||
|
vertical-align: middle;
|
||||||
|
border-bottom: 1px solid #eef0f3;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin__table .uk-table tbody tr:hover {
|
||||||
|
background: #f8f9fc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin__table .uk-table tbody tr:last-child td {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table Links */
|
||||||
|
.post__title__link {
|
||||||
|
color: #1e1e2d;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post__title__link:hover {
|
||||||
|
color: #901438;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table Actions */
|
||||||
|
.admin__table .uk-icon-link {
|
||||||
|
color: #b5b5c3;
|
||||||
|
margin-right: 10px;
|
||||||
|
transition: color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin__table .uk-icon-link:hover {
|
||||||
|
color: #901438;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
Labels & Badges
|
||||||
|
============================================ */
|
||||||
|
.uk-label {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uk-label-success {
|
||||||
|
background: #c9f7f5;
|
||||||
|
color: #1bc5bd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uk-label-warning {
|
||||||
|
background: #fff4de;
|
||||||
|
color: #ffa800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uk-label-danger {
|
||||||
|
background: #ffe2e5;
|
||||||
|
color: #f64e60;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
Filter Form
|
||||||
|
============================================ */
|
||||||
|
.filter__form {
|
||||||
|
background: #fff;
|
||||||
|
padding: 20px 25px;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
box-shadow: 0 2px 15px rgba(0, 0, 0, 0.04);
|
||||||
|
border: 1px solid #eef0f3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter__form .uk-select,
|
||||||
|
.filter__form .uk-input {
|
||||||
|
border: 1px solid #e4e6ef;
|
||||||
|
border-radius: 6px;
|
||||||
|
height: 42px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter__form .uk-select:focus,
|
||||||
|
.filter__form .uk-input:focus {
|
||||||
|
border-color: #901438;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter__form .uk-button {
|
||||||
|
height: 42px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
Pagination
|
||||||
|
============================================ */
|
||||||
|
.admin__pagination {
|
||||||
|
background: #fff;
|
||||||
|
padding: 20px 25px;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-top: 25px;
|
||||||
|
box-shadow: 0 2px 15px rgba(0, 0, 0, 0.04);
|
||||||
|
border: 1px solid #eef0f3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin__pagination .uk-pagination {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin__pagination .uk-pagination > li > a,
|
||||||
|
.admin__pagination .uk-pagination > li > span {
|
||||||
|
padding: 8px 14px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin__pagination .uk-pagination > .uk-active > span {
|
||||||
|
background: #901438;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
Forms
|
||||||
|
============================================ */
|
||||||
|
.admin__form {
|
||||||
|
background: #fff;
|
||||||
|
padding: 35px;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 2px 15px rgba(0, 0, 0, 0.04);
|
||||||
|
border: 1px solid #eef0f3;
|
||||||
|
max-width: 900px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin__form .uk-form-label {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1e1e2d;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin__form .uk-input,
|
||||||
|
.admin__form .uk-select,
|
||||||
|
.admin__form .uk-textarea {
|
||||||
|
border: 1px solid #e4e6ef;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 10px 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin__form .uk-input:focus,
|
||||||
|
.admin__form .uk-select:focus,
|
||||||
|
.admin__form .uk-textarea:focus {
|
||||||
|
border-color: #901438;
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 0 3px rgba(144, 20, 56, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin__form .uk-margin {
|
||||||
|
margin-bottom: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
Buttons
|
||||||
|
============================================ */
|
||||||
|
.uk-button-primary {
|
||||||
|
background: #901438;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uk-button-primary:hover {
|
||||||
|
background: #7a1130;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.uk-button-default {
|
||||||
|
background: #f5f8fa;
|
||||||
|
border: 1px solid #e4e6ef;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #7e8299;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uk-button-default:hover {
|
||||||
|
background: #eef0f3;
|
||||||
|
color: #1e1e2d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uk-button-danger {
|
||||||
|
background: #f64e60;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uk-button-danger:hover {
|
||||||
|
background: #ee2d41;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uk-button-secondary {
|
||||||
|
background: #e4e6ef;
|
||||||
|
color: #7e8299;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uk-button-secondary:hover {
|
||||||
|
background: #d6d8e1;
|
||||||
|
color: #1e1e2d;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
Alerts
|
||||||
|
============================================ */
|
||||||
|
.admin__content .uk-alert {
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 15px 20px;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin__content .uk-alert-primary {
|
||||||
|
background: #e1f0ff;
|
||||||
|
color: #3699ff;
|
||||||
|
border-left: 4px solid #3699ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin__content .uk-alert-success {
|
||||||
|
background: #c9f7f5;
|
||||||
|
color: #1bc5bd;
|
||||||
|
border-left: 4px solid #1bc5bd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin__content .uk-alert-warning {
|
||||||
|
background: #fff4de;
|
||||||
|
color: #ffa800;
|
||||||
|
border-left: 4px solid #ffa800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin__content .uk-alert-danger {
|
||||||
|
background: #ffe2e5;
|
||||||
|
color: #f64e60;
|
||||||
|
border-left: 4px solid #f64e60;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
Quick Links (Dashboard)
|
||||||
|
============================================ */
|
||||||
|
.admin__content .uk-card {
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid #eef0f3;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin__content .uk-card:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin__content .uk-card .uk-icon {
|
||||||
|
color: #901438;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
Summernote Editor Override
|
||||||
|
============================================ */
|
||||||
|
.note-editor.note-frame {
|
||||||
|
border: 1px solid #e4e6ef;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-editor .note-toolbar {
|
||||||
|
background: #f8f9fc;
|
||||||
|
border-bottom: 1px solid #e4e6ef;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
Responsive Design
|
||||||
|
============================================ */
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.admin__sidebar {
|
||||||
|
width: 220px;
|
||||||
|
min-width: 220px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin__content {
|
||||||
|
margin-left: 220px;
|
||||||
|
width: calc(100% - 220px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 960px) {
|
||||||
|
.admin__sidebar {
|
||||||
|
width: 70px;
|
||||||
|
min-width: 70px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin__sidebar .admin__logo {
|
||||||
|
padding: 20px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin__sidebar .admin__logo img {
|
||||||
|
max-width: 35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin__sidebar .admin__logo span {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin__menu {
|
||||||
|
padding: 15px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin__menu li a {
|
||||||
|
justify-content: center;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin__menu li a .uk-icon {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin__menu li a span:not(.uk-icon) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin__user {
|
||||||
|
padding: 15px 8px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin__user .user__name {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin__user a {
|
||||||
|
display: block;
|
||||||
|
margin: 8px 0;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin__content {
|
||||||
|
margin-left: 70px;
|
||||||
|
width: calc(100% - 70px);
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.page__header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter__form .uk-grid > div {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin__table {
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin__table .uk-table {
|
||||||
|
min-width: 700px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat__card {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat__number {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.admin__content {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin__form {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page__header h1 {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -81,8 +81,10 @@
|
|||||||
color:#901438;
|
color:#901438;
|
||||||
cursor:default;
|
cursor:default;
|
||||||
text-align:center;
|
text-align:center;
|
||||||
|
z-index: 9;
|
||||||
|
-webkit-box-sizing: initial !important;
|
||||||
}
|
}
|
||||||
.nav__div .nav__title{padding-top:30px;writing-mode:vertical-lr;}
|
.nav__div .nav__title{padding-top:30px;writing-mode:vertical-lr;font-family: 'SourceHanSansK', sans-serif !important ;}
|
||||||
.nav__div .uk-icon{cursor:pointer;}
|
.nav__div .uk-icon{cursor:pointer;}
|
||||||
|
|
||||||
/* toggle menu */
|
/* toggle menu */
|
||||||
@@ -93,7 +95,7 @@
|
|||||||
width:340px;
|
width:340px;
|
||||||
height:100%;
|
height:100%;
|
||||||
padding:50px 20px;
|
padding:50px 20px;
|
||||||
z-index:1;
|
z-index:999;
|
||||||
display:none;
|
display:none;
|
||||||
}
|
}
|
||||||
.menu__box{background:#fff;height:60%;border-radius:20px;}
|
.menu__box{background:#fff;height:60%;border-radius:20px;}
|
||||||
@@ -104,8 +106,17 @@
|
|||||||
|
|
||||||
/* LOGIN */
|
/* LOGIN */
|
||||||
.login__section{position:absolute;width:100px;left:50%;top:60%;text-align:center;transform:translateX(-50%);}
|
.login__section{position:absolute;width:100px;left:50%;top:60%;text-align:center;transform:translateX(-50%);}
|
||||||
.login__section li{font-size:14px;}
|
.login__section li{font-size:14px;margin: 10px 0;}
|
||||||
.login_nav{display:none;}
|
.login_nav{
|
||||||
|
display:none;
|
||||||
|
width: 100%;
|
||||||
|
background-color: #ffff;
|
||||||
|
padding: 20px 50px;
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
top: 50px;
|
||||||
|
}
|
||||||
.login__btn{border-bottom:1px solid #ccc;}
|
.login__btn{border-bottom:1px solid #ccc;}
|
||||||
|
|
||||||
/* main index */
|
/* main index */
|
||||||
@@ -188,7 +199,7 @@
|
|||||||
.post__lists li:hover .lists__img{box-shadow:1px 1px 5px RGBA(144,20,68,0.2);}
|
.post__lists li:hover .lists__img{box-shadow:1px 1px 5px RGBA(144,20,68,0.2);}
|
||||||
|
|
||||||
/* footer */
|
/* footer */
|
||||||
footer{width:100%;height:430px;background-color:#100e11;position:relative;z-index:9999;}
|
footer{width:100%;height:430px;background-color:#100e11;position:relative;z-index:99;}
|
||||||
.footer__inner{
|
.footer__inner{
|
||||||
width:100%;
|
width:100%;
|
||||||
max-width:1200px;
|
max-width:1200px;
|
||||||
@@ -260,5 +271,70 @@
|
|||||||
.btn__area .btn__edit{border:1px solid #4da7ca;color:#4da7ca;}
|
.btn__area .btn__edit{border:1px solid #4da7ca;color:#4da7ca;}
|
||||||
|
|
||||||
/* login */
|
/* login */
|
||||||
.uk-form-stacked{width:30%;margin:0 auto;}
|
.uk-form-stacked{min-width:640px;width:30%;margin:0 auto;}
|
||||||
.login__btn{width:100%;}
|
.login__btn{width:100%;}
|
||||||
|
|
||||||
|
/* list css */
|
||||||
|
.list__posts{float:left;width:30%;padding-right:3%;}
|
||||||
|
.list__posts li{width:100%;height:auto;}
|
||||||
|
.list__posts li a{display:block;width:100%;height:100%;}
|
||||||
|
.list__posts li:hover{text-shadow:1px 0px 1px RGBA(0,0,0,0.3);}
|
||||||
|
.list__posts li:hover .lists__img{box-shadow:1px 1px 5px RGBA(144,20,68,0.2);}
|
||||||
|
|
||||||
|
@media (max-width: 1023px) {
|
||||||
|
html, body{margin:0;overflow-x:hidden;min-height:100%;}
|
||||||
|
.body__wrap{width:100%;max-width:640px;margin:0 auto;position:relative;}
|
||||||
|
|
||||||
|
.nav__div{height:30px;border:none;padding:20px;position:absolute;}
|
||||||
|
.nav__title{display:none;}
|
||||||
|
|
||||||
|
.menu__wrap{width:calc(100% - 40px);z-index:999;max-width:640px;position:fixed;height:100vh;}
|
||||||
|
.wixon__wrap{width:100%;}
|
||||||
|
header{height:70px;}
|
||||||
|
.header__inner{width:100%;height:70px;}
|
||||||
|
h1{margin:0 !important;}
|
||||||
|
.header__inner h1 img{height:70px;padding:10px 20px;}
|
||||||
|
.container{margin-top:30px;}
|
||||||
|
.content{border:none;}
|
||||||
|
.content__inner{display:none;}
|
||||||
|
.fake__lists{display:block;}
|
||||||
|
.fake__lists li{border-bottom: 1px solid #e7e6e7;padding-top:30px;}
|
||||||
|
footer{height:100%;position:relative;}
|
||||||
|
.footer__inner{display:block;padding:0 20px;width:auto;}
|
||||||
|
.footer__tel{padding-top:30px;}
|
||||||
|
.footer__info{text-align:center;}
|
||||||
|
.footer__info ul{display:none;}
|
||||||
|
footer address{margin-top:10px;}
|
||||||
|
.slogan{display:none;}
|
||||||
|
.footer__logo{margin:0;padding:50px 0;text-align:center;}
|
||||||
|
.footer__logo img{width:70%;}
|
||||||
|
|
||||||
|
.container__post .content{width:auto;padding:0 30px;}
|
||||||
|
.title__div{display:block;height:auto;line-height:1em;padding:10px;width:auto;}
|
||||||
|
.content__title{line-height:1.5em;text-indent:0;}
|
||||||
|
.content__date{padding-top:10px;}
|
||||||
|
.content__desc{padding:10px;word-break:break-word;}
|
||||||
|
.content__desc > div{width:auto !important;}
|
||||||
|
.other__list__way{flex-shrink: 0;}
|
||||||
|
.other__list__title{padding-left:20px;}
|
||||||
|
.other__list__date{display:none;}
|
||||||
|
.btn__area{text-align:center;display:flex;justify-content: space-around;padding-top:30px;padding-bottom:0;}
|
||||||
|
.btn__area a{margin:0;width:30%;}
|
||||||
|
|
||||||
|
.login__container{padding-top:50px;}
|
||||||
|
.login__container .uk-form-stacked{padding-top:50px;width:auto !important;min-width:auto !important;}
|
||||||
|
|
||||||
|
.post__container form{width:auto !important;min-width:auto !important;}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
73
templates/admin/base_admin.html
Normal file
73
templates/admin/base_admin.html
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>WIXON Blog - Admin</title>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/uikit/3.6.16/css/uikit.min.css"/>
|
||||||
|
<link rel="stylesheet" href="/static/css/admin.css"/>
|
||||||
|
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
|
||||||
|
<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>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote-lite.min.css" rel="stylesheet">
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote-lite.min.js"></script>
|
||||||
|
{% block staticfiles %}{% endblock %}
|
||||||
|
</head>
|
||||||
|
<body class="admin-body">
|
||||||
|
<div class="admin__wrap">
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<nav class="admin__sidebar">
|
||||||
|
<div class="admin__logo">
|
||||||
|
<a href="/admin/">
|
||||||
|
<img src="/static/images/logo.png" alt="WIXON Admin" />
|
||||||
|
</a>
|
||||||
|
<span>Admin Panel</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul class="admin__menu">
|
||||||
|
<li>
|
||||||
|
<a href="/admin/" class="{% if request.path == '/admin/' %}active{% endif %}">
|
||||||
|
<span uk-icon="icon: home"></span>
|
||||||
|
<span>대시보드</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="/admin/posts" class="{% if '/admin/posts' in request.path %}active{% endif %}">
|
||||||
|
<span uk-icon="icon: file-text"></span>
|
||||||
|
<span>포스트 관리</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="/admin/members" class="{% if '/admin/members' in request.path %}active{% endif %}">
|
||||||
|
<span uk-icon="icon: users"></span>
|
||||||
|
<span>회원 관리</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="admin__user">
|
||||||
|
<span class="user__name">{{ g.user_info.mb_name }}</span>
|
||||||
|
<a href="/">사이트로 이동</a>
|
||||||
|
<a href="/logout">로그아웃</a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<main class="admin__content">
|
||||||
|
{% with messages = get_flashed_messages() %}
|
||||||
|
{% if messages %}
|
||||||
|
<div class="uk-alert-primary" uk-alert>
|
||||||
|
<a class="uk-alert-close" uk-close></a>
|
||||||
|
{{ messages[0] }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% block scripts %}{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
70
templates/admin/dashboard.html
Normal file
70
templates/admin/dashboard.html
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
{% extends 'admin/base_admin.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>대시보드</h1>
|
||||||
|
|
||||||
|
<!-- Stats Cards -->
|
||||||
|
<div class="uk-grid uk-child-width-1-2@s uk-child-width-1-4@m uk-grid-match" uk-grid>
|
||||||
|
<div>
|
||||||
|
<div class="stat__card">
|
||||||
|
<h3>총 포스트</h3>
|
||||||
|
<p class="stat__number">{{ stats.total_posts }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="stat__card">
|
||||||
|
<h3>공개 포스트</h3>
|
||||||
|
<p class="stat__number green">{{ stats.public_posts }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="stat__card">
|
||||||
|
<h3>비공개 포스트</h3>
|
||||||
|
<p class="stat__number orange">{{ stats.private_posts }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="stat__card">
|
||||||
|
<h3>총 회원 수</h3>
|
||||||
|
<p class="stat__number blue">{{ stats.total_members }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Deleted Posts Alert -->
|
||||||
|
{% if stats.deleted_posts > 0 %}
|
||||||
|
<div class="uk-alert-warning uk-margin-top" uk-alert>
|
||||||
|
<a class="uk-alert-close" uk-close></a>
|
||||||
|
<p>
|
||||||
|
<span uk-icon="icon: warning"></span>
|
||||||
|
삭제된 포스트 <strong>{{ stats.deleted_posts }}개</strong>가 있습니다.
|
||||||
|
<a href="/admin/posts?use_yn=N">삭제된 포스트 보기</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Quick Links -->
|
||||||
|
<div class="uk-margin-large-top">
|
||||||
|
<h2 class="uk-heading-line"><span>바로가기</span></h2>
|
||||||
|
<div class="uk-grid uk-child-width-1-3@m" uk-grid>
|
||||||
|
<div>
|
||||||
|
<a href="/admin/posts" class="uk-card uk-card-default uk-card-body uk-card-hover uk-text-center" style="display: block; text-decoration: none;">
|
||||||
|
<span uk-icon="icon: file-text; ratio: 2"></span>
|
||||||
|
<p class="uk-margin-small-top uk-text-bold">포스트 관리</p>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a href="/admin/members" class="uk-card uk-card-default uk-card-body uk-card-hover uk-text-center" style="display: block; text-decoration: none;">
|
||||||
|
<span uk-icon="icon: users; ratio: 2"></span>
|
||||||
|
<p class="uk-margin-small-top uk-text-bold">회원 관리</p>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a href="/write" class="uk-card uk-card-default uk-card-body uk-card-hover uk-text-center" style="display: block; text-decoration: none;">
|
||||||
|
<span uk-icon="icon: plus-circle; ratio: 2"></span>
|
||||||
|
<p class="uk-margin-small-top uk-text-bold">새 포스트 작성</p>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
108
templates/admin/member_form.html
Normal file
108
templates/admin/member_form.html
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
{% extends 'admin/base_admin.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="page__header">
|
||||||
|
<h1>{% if member %}회원 수정{% else %}회원 추가{% endif %}</h1>
|
||||||
|
<a href="/admin/members" class="uk-button uk-button-default">
|
||||||
|
<span uk-icon="icon: arrow-left"></span> 목록으로
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="admin__form" style="max-width: 500px;">
|
||||||
|
<form action="{% if member %}/admin/members/{{ member.mb_idx }}{% else %}/admin/members/add{% endif %}" method="post" class="uk-form-stacked">
|
||||||
|
|
||||||
|
<div class="uk-margin">
|
||||||
|
<label class="uk-form-label" for="mb_id">아이디</label>
|
||||||
|
<div class="uk-form-controls">
|
||||||
|
{% if member %}
|
||||||
|
<input class="uk-input" id="mb_id" type="text" value="{{ member.mb_id }}" disabled>
|
||||||
|
<p class="uk-text-muted uk-text-small uk-margin-small-top">아이디는 변경할 수 없습니다.</p>
|
||||||
|
{% else %}
|
||||||
|
<input class="uk-input" id="mb_id" type="text" name="mb_id" placeholder="영문, 숫자 조합" required>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="uk-margin">
|
||||||
|
<label class="uk-form-label" for="mb_name">이름</label>
|
||||||
|
<div class="uk-form-controls">
|
||||||
|
<input class="uk-input" id="mb_name" type="text" name="mb_name" value="{{ member.mb_name if member else '' }}" placeholder="회원 이름" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if not member %}
|
||||||
|
<div class="uk-margin">
|
||||||
|
<label class="uk-form-label" for="password">비밀번호</label>
|
||||||
|
<div class="uk-form-controls">
|
||||||
|
<input class="uk-input" id="password" type="password" name="password" placeholder="4자 이상" required minlength="4">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="uk-margin">
|
||||||
|
<label class="uk-form-label">비밀번호</label>
|
||||||
|
<div class="uk-form-controls">
|
||||||
|
<button type="button" class="uk-button uk-button-secondary" onclick="resetPassword({{ member.mb_idx }})">
|
||||||
|
<span uk-icon="icon: lock"></span> 비밀번호 재설정
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<hr class="uk-margin-medium">
|
||||||
|
|
||||||
|
<div class="uk-margin">
|
||||||
|
<button type="submit" class="uk-button uk-button-primary uk-button-large">
|
||||||
|
<span uk-icon="icon: check"></span> {% if member %}수정{% else %}추가{% endif %}
|
||||||
|
</button>
|
||||||
|
<a href="/admin/members" class="uk-button uk-button-default uk-button-large">취소</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
{% if member %}
|
||||||
|
<script>
|
||||||
|
function resetPassword(mb_idx) {
|
||||||
|
Swal.fire({
|
||||||
|
title: '새 비밀번호 입력',
|
||||||
|
input: 'password',
|
||||||
|
inputPlaceholder: '새 비밀번호를 입력하세요 (4자 이상)',
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonColor: '#901438',
|
||||||
|
cancelButtonColor: '#6c757d',
|
||||||
|
confirmButtonText: '변경',
|
||||||
|
cancelButtonText: '취소',
|
||||||
|
inputValidator: (value) => {
|
||||||
|
if (!value) return '비밀번호를 입력해주세요';
|
||||||
|
if (value.length < 4) return '비밀번호는 4자 이상이어야 합니다';
|
||||||
|
}
|
||||||
|
}).then((result) => {
|
||||||
|
if (result.isConfirmed) {
|
||||||
|
$.ajax({
|
||||||
|
url: "/admin/members/" + mb_idx + "/reset-password",
|
||||||
|
type: "POST",
|
||||||
|
contentType: "application/json",
|
||||||
|
data: JSON.stringify({password: result.value}),
|
||||||
|
success: function(res) {
|
||||||
|
if (res.success) {
|
||||||
|
Swal.fire({
|
||||||
|
title: '완료',
|
||||||
|
text: res.message,
|
||||||
|
icon: 'success'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Swal.fire('오류', res.message, 'error');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function(xhr) {
|
||||||
|
var msg = xhr.responseJSON ? xhr.responseJSON.message : '비밀번호 변경 중 오류가 발생했습니다.';
|
||||||
|
Swal.fire('오류', msg, 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
139
templates/admin/members.html
Normal file
139
templates/admin/members.html
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
{% extends 'admin/base_admin.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="page__header">
|
||||||
|
<h1>회원 관리</h1>
|
||||||
|
<a href="/admin/members/add" class="uk-button uk-button-primary">
|
||||||
|
<span uk-icon="icon: plus"></span> 회원 추가
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Members Table -->
|
||||||
|
<div class="admin__table">
|
||||||
|
<table class="uk-table uk-table-striped uk-table-hover uk-table-middle">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="width: 80px;">번호</th>
|
||||||
|
<th>아이디</th>
|
||||||
|
<th>이름</th>
|
||||||
|
<th style="width: 200px;">관리</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% if members %}
|
||||||
|
{% for member in members %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ member.mb_idx }}</td>
|
||||||
|
<td>
|
||||||
|
{{ member.mb_id }}
|
||||||
|
{% if member.mb_id in ['admin', 'wixon'] %}
|
||||||
|
<span class="uk-label uk-label-warning" style="margin-left: 5px;">관리자</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>{{ member.mb_name }}</td>
|
||||||
|
<td>
|
||||||
|
<a href="/admin/members/{{ member.mb_idx }}" class="uk-button uk-button-small uk-button-default">
|
||||||
|
<span uk-icon="icon: pencil; ratio: 0.8"></span> 수정
|
||||||
|
</a>
|
||||||
|
<button type="button" class="uk-button uk-button-small uk-button-secondary" onclick="resetPassword({{ member.mb_idx }})">
|
||||||
|
<span uk-icon="icon: lock; ratio: 0.8"></span> 비밀번호
|
||||||
|
</button>
|
||||||
|
{% if member.mb_id not in ['admin', 'wixon'] %}
|
||||||
|
<button type="button" class="uk-button uk-button-small uk-button-danger" onclick="deleteMember({{ member.mb_idx }}, '{{ member.mb_id }}')">
|
||||||
|
<span uk-icon="icon: trash; ratio: 0.8"></span> 삭제
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="4" class="uk-text-center uk-text-muted">
|
||||||
|
등록된 회원이 없습니다.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
function resetPassword(mb_idx) {
|
||||||
|
Swal.fire({
|
||||||
|
title: '새 비밀번호 입력',
|
||||||
|
input: 'password',
|
||||||
|
inputPlaceholder: '새 비밀번호를 입력하세요 (4자 이상)',
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonColor: '#901438',
|
||||||
|
cancelButtonColor: '#6c757d',
|
||||||
|
confirmButtonText: '변경',
|
||||||
|
cancelButtonText: '취소',
|
||||||
|
inputValidator: (value) => {
|
||||||
|
if (!value) return '비밀번호를 입력해주세요';
|
||||||
|
if (value.length < 4) return '비밀번호는 4자 이상이어야 합니다';
|
||||||
|
}
|
||||||
|
}).then((result) => {
|
||||||
|
if (result.isConfirmed) {
|
||||||
|
$.ajax({
|
||||||
|
url: "/admin/members/" + mb_idx + "/reset-password",
|
||||||
|
type: "POST",
|
||||||
|
contentType: "application/json",
|
||||||
|
data: JSON.stringify({password: result.value}),
|
||||||
|
success: function(res) {
|
||||||
|
if (res.success) {
|
||||||
|
Swal.fire({
|
||||||
|
title: '완료',
|
||||||
|
text: res.message,
|
||||||
|
icon: 'success'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Swal.fire('오류', res.message, 'error');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function(xhr) {
|
||||||
|
var msg = xhr.responseJSON ? xhr.responseJSON.message : '비밀번호 변경 중 오류가 발생했습니다.';
|
||||||
|
Swal.fire('오류', msg, 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteMember(mb_idx, mb_id) {
|
||||||
|
Swal.fire({
|
||||||
|
title: '회원을 삭제하시겠습니까?',
|
||||||
|
html: '<strong>' + mb_id + '</strong> 회원을 삭제합니다.<br><span class="uk-text-danger">이 작업은 되돌릴 수 없습니다!</span>',
|
||||||
|
icon: 'warning',
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonColor: '#dc3545',
|
||||||
|
cancelButtonColor: '#6c757d',
|
||||||
|
confirmButtonText: '삭제',
|
||||||
|
cancelButtonText: '취소'
|
||||||
|
}).then((result) => {
|
||||||
|
if (result.isConfirmed) {
|
||||||
|
$.ajax({
|
||||||
|
url: "/admin/members/" + mb_idx + "/delete",
|
||||||
|
type: "DELETE",
|
||||||
|
success: function(res) {
|
||||||
|
if (res.success) {
|
||||||
|
Swal.fire({
|
||||||
|
title: '삭제 완료',
|
||||||
|
text: res.message,
|
||||||
|
icon: 'success'
|
||||||
|
}).then(() => location.reload());
|
||||||
|
} else {
|
||||||
|
Swal.fire('오류', res.message, 'error');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function(xhr) {
|
||||||
|
var msg = xhr.responseJSON ? xhr.responseJSON.message : '삭제 중 오류가 발생했습니다.';
|
||||||
|
Swal.fire('오류', msg, 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
122
templates/admin/post_detail.html
Normal file
122
templates/admin/post_detail.html
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
{% extends 'admin/base_admin.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="page__header">
|
||||||
|
<h1>포스트 수정</h1>
|
||||||
|
<div>
|
||||||
|
<a href="/post/{{ post.id }}" target="_blank" class="uk-button uk-button-default">
|
||||||
|
<span uk-icon="icon: link"></span> 사이트에서 보기
|
||||||
|
</a>
|
||||||
|
<a href="/admin/posts" class="uk-button uk-button-default">
|
||||||
|
<span uk-icon="icon: arrow-left"></span> 목록으로
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="admin__form">
|
||||||
|
<form action="/admin/posts/{{ 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 }}" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="uk-grid uk-child-width-1-2@m" uk-grid>
|
||||||
|
<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="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>
|
||||||
|
<div>
|
||||||
|
<div class="uk-margin">
|
||||||
|
<label class="uk-form-label">공개 설정</label>
|
||||||
|
<div class="uk-form-controls uk-margin-small-top">
|
||||||
|
<label>
|
||||||
|
<input class="uk-checkbox" type="checkbox" name="public" {% if post.public_yn == 'Y' %}checked{% endif %}>
|
||||||
|
이 글을 외부에 공개합니다
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="uk-margin">
|
||||||
|
<label class="uk-form-label">작성 정보</label>
|
||||||
|
<div class="uk-form-controls">
|
||||||
|
<div class="uk-text-muted" style="padding: 10px 0;">
|
||||||
|
<span uk-icon="icon: user"></span> {{ post.mb_name }} ({{ post.mb_id }})
|
||||||
|
|
|
||||||
|
<span uk-icon="icon: clock"></span> {{ post.add_date.strftime('%Y-%m-%d %H:%M') }}
|
||||||
|
</div>
|
||||||
|
</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 uk-margin-large-top">
|
||||||
|
<button type="submit" class="uk-button uk-button-primary uk-button-large">
|
||||||
|
<span uk-icon="icon: check"></span> 저장
|
||||||
|
</button>
|
||||||
|
<a href="/admin/posts" class="uk-button uk-button-default uk-button-large">취소</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
$(document).ready(function() {
|
||||||
|
$('#contents').summernote({
|
||||||
|
height: 400,
|
||||||
|
lang: 'ko-KR',
|
||||||
|
toolbar: [
|
||||||
|
['style', ['style']],
|
||||||
|
['font', ['bold', 'underline', 'clear']],
|
||||||
|
['fontname', ['fontname']],
|
||||||
|
['color', ['color']],
|
||||||
|
['para', ['ul', 'ol', 'paragraph']],
|
||||||
|
['table', ['table']],
|
||||||
|
['insert', ['link', 'picture', 'video']],
|
||||||
|
['view', ['fullscreen', 'codeview', 'help']]
|
||||||
|
],
|
||||||
|
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) {
|
||||||
|
$editor.summernote("insertImage", response.fileUrl);
|
||||||
|
} else {
|
||||||
|
Swal.fire('오류', response.message, 'error');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function() {
|
||||||
|
Swal.fire('오류', '이미지 업로드에 실패했습니다.', 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
222
templates/admin/posts.html
Normal file
222
templates/admin/posts.html
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
{% extends 'admin/base_admin.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="page__header">
|
||||||
|
<h1>포스트 관리</h1>
|
||||||
|
<a href="/write" class="uk-button uk-button-primary">
|
||||||
|
<span uk-icon="icon: plus"></span> 새 포스트 작성
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filter Form -->
|
||||||
|
<div class="filter__form">
|
||||||
|
<form method="get">
|
||||||
|
<div class="uk-grid uk-grid-small uk-flex-middle" uk-grid>
|
||||||
|
<div class="uk-width-1-5@m">
|
||||||
|
<select class="uk-select" name="category">
|
||||||
|
<option value="">전체 카테고리</option>
|
||||||
|
<option value="IT" {% if request.args.get('category') == 'IT' %}selected{% endif %}>IT</option>
|
||||||
|
<option value="NEWS" {% if request.args.get('category') == 'NEWS' %}selected{% endif %}>NEWS</option>
|
||||||
|
<option value="ETC" {% if request.args.get('category') == 'ETC' %}selected{% endif %}>ETC</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="uk-width-1-5@m">
|
||||||
|
<select class="uk-select" name="public_yn">
|
||||||
|
<option value="">공개 여부</option>
|
||||||
|
<option value="Y" {% if request.args.get('public_yn') == 'Y' %}selected{% endif %}>공개</option>
|
||||||
|
<option value="N" {% if request.args.get('public_yn') == 'N' %}selected{% endif %}>비공개</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="uk-width-1-5@m">
|
||||||
|
<select class="uk-select" name="use_yn">
|
||||||
|
<option value="Y" {% if request.args.get('use_yn', 'Y') == 'Y' %}selected{% endif %}>사용중</option>
|
||||||
|
<option value="N" {% if request.args.get('use_yn') == 'N' %}selected{% endif %}>삭제됨</option>
|
||||||
|
<option value="" {% if request.args.get('use_yn') == '' %}selected{% endif %}>전체</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="uk-width-expand@m">
|
||||||
|
<input class="uk-input" type="text" name="search" placeholder="제목 검색..." value="{{ request.args.get('search', '') }}">
|
||||||
|
</div>
|
||||||
|
<div class="uk-width-auto@m">
|
||||||
|
<button class="uk-button uk-button-primary" type="submit">
|
||||||
|
<span uk-icon="icon: search"></span> 검색
|
||||||
|
</button>
|
||||||
|
<a href="/admin/posts" class="uk-button uk-button-default">초기화</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Posts Table -->
|
||||||
|
<div class="admin__table">
|
||||||
|
<table class="uk-table uk-table-striped uk-table-hover uk-table-middle">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="width: 60px;">ID</th>
|
||||||
|
<th>제목</th>
|
||||||
|
<th style="width: 100px;">카테고리</th>
|
||||||
|
<th style="width: 100px;">작성자</th>
|
||||||
|
<th style="width: 80px;">공개</th>
|
||||||
|
<th style="width: 80px;">상태</th>
|
||||||
|
<th style="width: 100px;">작성일</th>
|
||||||
|
<th style="width: 120px;">관리</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% if posts %}
|
||||||
|
{% for post in posts %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ post.id }}</td>
|
||||||
|
<td>
|
||||||
|
<a href="/admin/posts/{{ post.id }}" class="post__title__link">
|
||||||
|
{{ post.title[:50] }}{% if post.title|length > 50 %}...{% endif %}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td>{{ post.category }}</td>
|
||||||
|
<td>{{ post.mb_name }}</td>
|
||||||
|
<td>
|
||||||
|
{% if post.public_yn == 'Y' %}
|
||||||
|
<span class="uk-label uk-label-success">공개</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="uk-label uk-label-warning">비공개</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if post.use_yn == 'Y' %}
|
||||||
|
<span class="uk-label uk-label-success">사용중</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="uk-label uk-label-danger">삭제됨</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>{{ post.add_date.strftime('%Y-%m-%d') }}</td>
|
||||||
|
<td>
|
||||||
|
<a href="/post/{{ post.id }}" target="_blank" class="uk-icon-link" uk-icon="icon: link" title="사이트에서 보기"></a>
|
||||||
|
<a href="/admin/posts/{{ post.id }}" class="uk-icon-link" uk-icon="icon: pencil" title="수정"></a>
|
||||||
|
{% if post.use_yn == 'Y' %}
|
||||||
|
<a href="javascript:deletePost({{ post.id }})" class="uk-icon-link" uk-icon="icon: trash" title="삭제"></a>
|
||||||
|
{% else %}
|
||||||
|
<a href="javascript:restorePost({{ post.id }})" class="uk-icon-link uk-text-success" uk-icon="icon: refresh" title="복구"></a>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="8" class="uk-text-center uk-text-muted">
|
||||||
|
포스트가 없습니다.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pagination -->
|
||||||
|
{% if pagination.total > 0 %}
|
||||||
|
<div class="admin__pagination">
|
||||||
|
<div class="uk-flex uk-flex-between uk-flex-middle">
|
||||||
|
<div class="uk-text-muted">
|
||||||
|
총 {{ pagination.total }}개 중 {{ ((pagination.page - 1) * pagination.per_page) + 1 }} - {{ [pagination.page * pagination.per_page, pagination.total]|min }}개 표시
|
||||||
|
</div>
|
||||||
|
<ul class="uk-pagination uk-margin-remove">
|
||||||
|
{% if pagination.has_prev %}
|
||||||
|
<li>
|
||||||
|
<a href="?page={{ pagination.prev_num }}&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', '') }}">
|
||||||
|
<span uk-pagination-previous></span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% for page_num in range(1, pagination.total_pages + 1) %}
|
||||||
|
{% if page_num == pagination.page %}
|
||||||
|
<li class="uk-active"><span>{{ page_num }}</span></li>
|
||||||
|
{% elif page_num == 1 or page_num == pagination.total_pages or (page_num >= pagination.page - 2 and page_num <= pagination.page + 2) %}
|
||||||
|
<li>
|
||||||
|
<a href="?page={{ page_num }}&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', '') }}">
|
||||||
|
{{ page_num }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% elif page_num == pagination.page - 3 or page_num == pagination.page + 3 %}
|
||||||
|
<li class="uk-disabled"><span>...</span></li>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% if pagination.has_next %}
|
||||||
|
<li>
|
||||||
|
<a href="?page={{ pagination.next_num }}&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', '') }}">
|
||||||
|
<span uk-pagination-next></span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
function deletePost(id) {
|
||||||
|
Swal.fire({
|
||||||
|
title: '포스트를 삭제하시겠습니까?',
|
||||||
|
text: "삭제된 포스트는 복구할 수 있습니다.",
|
||||||
|
icon: 'warning',
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonColor: '#901438',
|
||||||
|
cancelButtonColor: '#6c757d',
|
||||||
|
confirmButtonText: '삭제',
|
||||||
|
cancelButtonText: '취소'
|
||||||
|
}).then((result) => {
|
||||||
|
if (result.isConfirmed) {
|
||||||
|
$.ajax({
|
||||||
|
url: "/admin/posts/" + id + "/delete",
|
||||||
|
type: "DELETE",
|
||||||
|
success: function(res) {
|
||||||
|
if (res.success) {
|
||||||
|
Swal.fire({
|
||||||
|
title: '삭제 완료',
|
||||||
|
text: res.message,
|
||||||
|
icon: 'success'
|
||||||
|
}).then(() => location.reload());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function() {
|
||||||
|
Swal.fire('오류', '삭제 중 오류가 발생했습니다.', 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function restorePost(id) {
|
||||||
|
Swal.fire({
|
||||||
|
title: '포스트를 복구하시겠습니까?',
|
||||||
|
icon: 'question',
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonColor: '#28a745',
|
||||||
|
cancelButtonColor: '#6c757d',
|
||||||
|
confirmButtonText: '복구',
|
||||||
|
cancelButtonText: '취소'
|
||||||
|
}).then((result) => {
|
||||||
|
if (result.isConfirmed) {
|
||||||
|
$.ajax({
|
||||||
|
url: "/admin/posts/" + id + "/restore",
|
||||||
|
type: "POST",
|
||||||
|
success: function(res) {
|
||||||
|
if (res.success) {
|
||||||
|
Swal.fire({
|
||||||
|
title: '복구 완료',
|
||||||
|
text: res.message,
|
||||||
|
icon: 'success'
|
||||||
|
}).then(() => location.reload());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function() {
|
||||||
|
Swal.fire('오류', '복구 중 오류가 발생했습니다.', 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>WIXON Blog</title>
|
<title>WIXON Blog</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/uikit/3.6.16/css/uikit.min.css"/>
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/uikit/3.6.16/css/uikit.min.css"/>
|
||||||
<link rel="stylesheet" href="/static/css/style.css"/>
|
<link rel="stylesheet" href="/static/css/style.css"/>
|
||||||
<!-- include libraries(jQuery, bootstrap) -->
|
<!-- include libraries(jQuery, bootstrap) -->
|
||||||
@@ -19,6 +20,7 @@
|
|||||||
{% block staticfiles %}{% endblock %}
|
{% block staticfiles %}{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<div class="body__wrap">
|
||||||
<div class="nav__div">
|
<div class="nav__div">
|
||||||
<span class="uk-icon menu__open" uk-icon="icon: menu"></span>
|
<span class="uk-icon menu__open" uk-icon="icon: menu"></span>
|
||||||
<span class="nav__title">WXNNER BLOG</span>
|
<span class="nav__title">WXNNER BLOG</span>
|
||||||
@@ -28,11 +30,11 @@
|
|||||||
<div class="menu__close"><button class="uk-icon" uk-icon="icon: close"></button></div>
|
<div class="menu__close"><button class="uk-icon" uk-icon="icon: close"></button></div>
|
||||||
<div class="menu__box">
|
<div class="menu__box">
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="#a">최신테크뉴스</a></li>
|
<li><a href="/list">최신테크뉴스</a></li>
|
||||||
<li><a href="#a">WXN 뉴스</a></li>
|
<li><a href="/list/NEWS">WXN 뉴스</a></li>
|
||||||
<li><a href="#a">IT 요즘</a></li>
|
<li><a href="/list/IT">IT 요즘</a></li>
|
||||||
<li><a href="#a">IT활용자료(윅슨전용)</a></li>
|
<!-- <li><a href="/list">IT활용자료(윅슨전용)</a></li> -->
|
||||||
<li><a href="#a">ETC</a></li>
|
<li><a href="/list/ETC">ETC</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<ul class="login__section">
|
<ul class="login__section">
|
||||||
@@ -42,7 +44,7 @@
|
|||||||
<div class="login_nav">
|
<div class="login_nav">
|
||||||
<ul>
|
<ul>
|
||||||
<li class="uk-active"><a href="/write">포스트 작성</a></li>
|
<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="/admin/">관리자 페이지</a></li>
|
||||||
<li class="uk-active"><a href="/logout">LOGOUT</a></li>
|
<li class="uk-active"><a href="/logout">LOGOUT</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -56,14 +58,19 @@
|
|||||||
<div class="wixon__wrap">
|
<div class="wixon__wrap">
|
||||||
<header>
|
<header>
|
||||||
<div class="header__inner">
|
<div class="header__inner">
|
||||||
<h1><a href="/"><img src="/static/images/logo.png" alt="WIXON" /></a></h1>
|
<h1><a href="https://wxn.co.kr/"><img src="/static/images/logo.png" alt="WIXON" /></a></h1>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{% block content %}{% endblock %}
|
{% block content %}{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
$(document).ready(function(){
|
$(document).ready(function(){
|
||||||
|
function isMobile() {
|
||||||
|
return $(window).width() <= 1023;
|
||||||
|
}
|
||||||
|
|
||||||
// 로그인 메뉴 활성화
|
// 로그인 메뉴 활성화
|
||||||
let check_state = 0;
|
let check_state = 0;
|
||||||
|
|
||||||
@@ -80,17 +87,29 @@
|
|||||||
|
|
||||||
// 메뉴 토글
|
// 메뉴 토글
|
||||||
$(".menu__open").click(function() {
|
$(".menu__open").click(function() {
|
||||||
|
if(isMobile()){
|
||||||
|
// 모바일용 메뉴 동작
|
||||||
|
$(".menu__wrap").fadeIn(300).css({"right": "0"});
|
||||||
|
}else{
|
||||||
|
// PC용 메뉴 동작
|
||||||
$(".menu__wrap").show();
|
$(".menu__wrap").show();
|
||||||
$(".nav__div").animate({"right":"-68px"}, 300, function(){
|
$(".nav__div").animate({"right": "-68px"}, 300, function(){
|
||||||
$(".menu__wrap").animate({"right":"0"}, 700);
|
$(".menu__wrap").animate({"right": "0"}, 700);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$('.menu__close').click(function() {
|
$('.menu__close').click(function() {
|
||||||
$(".menu__wrap").animate({"right":"-440px"}, 700, function(){
|
if (isMobile()) {
|
||||||
|
// 모바일용 메뉴 닫기
|
||||||
|
$(".menu__wrap").fadeOut(300);
|
||||||
|
} else {
|
||||||
|
// PC용 메뉴 닫기
|
||||||
|
$(".menu__wrap").animate({"right": "-440px"}, 700, function(){
|
||||||
$(".menu__wrap").hide();
|
$(".menu__wrap").hide();
|
||||||
$(".nav__div").animate({"right":"0"}, 300);
|
$(".nav__div").animate({"right": "0"}, 300);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
@@ -14,8 +14,8 @@
|
|||||||
font-size: 16px !important;
|
font-size: 16px !important;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<div class="uk-container uk-margin-top" style="margin-bottom: 30px">
|
<div class="uk-container uk-margin-top post__container" style="margin-bottom: 30px">
|
||||||
<h1 class="uk-text-center"><span>포스트 수정</span></h1>
|
<h2 class="uk-text-center"><span>포스트 수정</span></h2>
|
||||||
<form action="/edit_post/{{ post.id }}" method="post" class="uk-form-stacked">
|
<form action="/edit_post/{{ post.id }}" method="post" class="uk-form-stacked">
|
||||||
<div class="uk-margin">
|
<div class="uk-margin">
|
||||||
<label class="uk-form-label" for="title">제목</label>
|
<label class="uk-form-label" for="title">제목</label>
|
||||||
@@ -82,6 +82,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$(".nav__div").hide();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -40,7 +40,7 @@
|
|||||||
<li>
|
<li>
|
||||||
<a href="{{ url_for('post', post_id=r_post.id) }}">
|
<a href="{{ url_for('post', post_id=r_post.id) }}">
|
||||||
<div class="lists__img">
|
<div class="lists__img">
|
||||||
<img src="{{ r_post.thumbnail_img | default('https://via.placeholder.com/300x200', true) }}" alt="{{ r_post.title }}" />
|
<img src="{{ r_post.thumbnail_img | default('https://via.placeholder.com/300x200', true) }}" alt="{{ r_post.title }}" onerror="this.onerror=null; this.src='https://via.placeholder.com/300x200';" />
|
||||||
</div>
|
</div>
|
||||||
<div class="lists__desc">
|
<div class="lists__desc">
|
||||||
<p class="lists__category">{{ r_post.category }}</p>
|
<p class="lists__category">{{ r_post.category }}</p>
|
||||||
@@ -90,6 +90,16 @@
|
|||||||
console.log(moment().format('ll'));
|
console.log(moment().format('ll'));
|
||||||
|
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
|
function Mobile() {
|
||||||
|
return $(window).width() <= 1023;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if(Mobile()){
|
||||||
|
// 모바일용 메뉴 동작
|
||||||
|
|
||||||
|
}else{
|
||||||
|
// PC용 메뉴 동작
|
||||||
const count__lists = $(".fake__lists li").length;
|
const count__lists = $(".fake__lists li").length;
|
||||||
if (count__lists < 4) {
|
if (count__lists < 4) {
|
||||||
for (let i = 0; i < count__lists; i++) {
|
for (let i = 0; i < count__lists; i++) {
|
||||||
@@ -132,6 +142,7 @@
|
|||||||
$(".post__lists2").append($(".fake__lists li:first-child"));
|
$(".post__lists2").append($(".fake__lists li:first-child"));
|
||||||
}
|
}
|
||||||
$(".fake__lists").html("");
|
$(".fake__lists").html("");
|
||||||
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
161
templates/list.html
Normal file
161
templates/list.html
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container">
|
||||||
|
<div class="content">
|
||||||
|
{% if not posts %}
|
||||||
|
<h4 style="margin-top: 100px">작성된 포스트가 없습니다.</h4>
|
||||||
|
{% else %}
|
||||||
|
<div class="content__inner">
|
||||||
|
<ul class="list__posts list__posts0">
|
||||||
|
</ul>
|
||||||
|
<ul class="list__posts list__posts1">
|
||||||
|
</ul>
|
||||||
|
<ul class="list__posts list__posts2">
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<ul class="fake__lists">
|
||||||
|
{% for r_post in posts %}
|
||||||
|
<li>
|
||||||
|
<a href="{{ url_for('post', post_id=r_post.id) }}">
|
||||||
|
<div class="lists__img">
|
||||||
|
<img src="{{ r_post.thumbnail_img | default('https://via.placeholder.com/300x200', true) }}" alt="{{ r_post.title }}" onerror="this.onerror=null; this.src='https://via.placeholder.com/300x200';" />
|
||||||
|
</div>
|
||||||
|
<div class="lists__desc">
|
||||||
|
<p class="lists__category">{{ r_post.category }}</p>
|
||||||
|
<h3 class="lists__title">{{ r_post.title }}</h3>
|
||||||
|
<p class="lists__content">
|
||||||
|
{{ r_post.contents | safe }}
|
||||||
|
</p>
|
||||||
|
{# <p class="lists__content">{{ r_post.contents | safe }}</p> #}
|
||||||
|
<p class="lists__date">
|
||||||
|
<b>{{ (r_post.add_date).strftime("%b %d") }}</b> / {{ (r_post.add_date).strftime("%Y") }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<footer>
|
||||||
|
<div class="footer__inner">
|
||||||
|
<div class="footer__info">
|
||||||
|
<p class="footer__tel">
|
||||||
|
TEL 02-3141-1305 / 1306 E-mail cser@wixon.co.kr / 기업부설연구소 제 2021154317호
|
||||||
|
</p>
|
||||||
|
<address>
|
||||||
|
서울시 마포구 동교로 215-1 한사 스튜디오 406 (주)윅슨어소시에이츠 / #406 , HANSA Studio, 215-1, Donggyo-ro, Mapo-gu, Seoul, Korea
|
||||||
|
</address>
|
||||||
|
<p class="copywriter">
|
||||||
|
© wixon associates Inc. 2022
|
||||||
|
</p>
|
||||||
|
<p class="slogan">
|
||||||
|
wixon. Who Invariable eXistence On the New era " wixon associates Inc. "
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li><a href="/"><img src="/static/images/opusclam.png" alt="WIXON" /></a></li>
|
||||||
|
<li><a href="/"><img src="/static/images/lpstock.png" alt="WIXON" /></a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<h2 class="footer__logo">
|
||||||
|
<a href="/"><img src="/static/images/logo.png" alt="WIXON" /></a>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
<script>
|
||||||
|
let currentPage = 1;
|
||||||
|
let isLoading = false;
|
||||||
|
|
||||||
|
function Mobile() {
|
||||||
|
return $(window).width() <= 1023;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getShortestColumnIndex() {
|
||||||
|
const lengths = [
|
||||||
|
$(".list__posts0 li").length,
|
||||||
|
$(".list__posts1 li").length,
|
||||||
|
$(".list__posts2 li").length
|
||||||
|
];
|
||||||
|
return lengths.indexOf(Math.min(...lengths));
|
||||||
|
}
|
||||||
|
|
||||||
|
function distributePosts(posts) {
|
||||||
|
posts.forEach((post) => {
|
||||||
|
const postHTML = `
|
||||||
|
<li>
|
||||||
|
<a href="/post/${post.id}">
|
||||||
|
<div class="lists__img">
|
||||||
|
<img src="${post.thumbnail_img || 'https://via.placeholder.com/300x200'}" alt="${post.title}" onerror="this.onerror=null; this.src='https://via.placeholder.com/300x200';" />
|
||||||
|
</div>
|
||||||
|
<div class="lists__desc">
|
||||||
|
<p class="lists__category">${post.category}</p>
|
||||||
|
<h3 class="lists__title">${post.title}</h3>
|
||||||
|
<p class="lists__content">${post.contents}</p>
|
||||||
|
<p class="lists__date"><b>${post.add_date_str}</b></p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (Mobile()) {
|
||||||
|
$(".fake__lists").append(postHTML);
|
||||||
|
} else {
|
||||||
|
const columnIndex = getShortestColumnIndex(); // 가장 짧은 컬럼
|
||||||
|
$(`.list__posts${columnIndex}`).append(postHTML);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadMorePosts() {
|
||||||
|
if (isLoading) return;
|
||||||
|
isLoading = true;
|
||||||
|
currentPage++;
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: "/list_more",
|
||||||
|
type: "GET",
|
||||||
|
data: { page: currentPage },
|
||||||
|
success: function(data) {
|
||||||
|
if (data.length === 0) {
|
||||||
|
$(window).off("scroll");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
distributePosts(data);
|
||||||
|
isLoading = false;
|
||||||
|
},
|
||||||
|
error: function() {
|
||||||
|
console.error("게시물 로딩 실패");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 스크롤 감지
|
||||||
|
$(window).on("scroll", function() {
|
||||||
|
if ($(window).scrollTop() + $(window).height() >= $(document).height() - 200) {
|
||||||
|
loadMorePosts();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!Mobile()) {
|
||||||
|
let initialPosts = [];
|
||||||
|
$(".fake__lists li").each(function() {
|
||||||
|
let $li = $(this);
|
||||||
|
initialPosts.push({
|
||||||
|
id: $li.find("a").attr("href").split("/post/")[1],
|
||||||
|
title: $li.find(".lists__title").text(),
|
||||||
|
category: $li.find(".lists__category").text(),
|
||||||
|
thumbnail_img: $li.find("img").attr("src"),
|
||||||
|
contents: $li.find(".lists__content").text(),
|
||||||
|
add_date_str: $li.find(".lists__date").text()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
distributePosts(initialPosts);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="uk-container uk-margin-top">
|
<div class="uk-container uk-margin-top login__container">
|
||||||
<h1 class="uk-text-center"><span>LOGIN</span></h1>
|
<h1 class="uk-text-center"><span>LOGIN</span></h1>
|
||||||
{% with messages = get_flashed_messages() %}
|
{% with messages = get_flashed_messages() %}
|
||||||
{% if messages %}
|
{% if messages %}
|
||||||
|
|||||||
@@ -13,8 +13,8 @@
|
|||||||
font-size: 16px !important;
|
font-size: 16px !important;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<div class="uk-container uk-margin-top" style="margin-bottom: 30px">
|
<div class="uk-container uk-margin-top post__container" style="margin-bottom: 30px">
|
||||||
<h1 class="uk-text-center"><span>포스트 작성</span></h1>
|
<h2 class="uk-text-center"><span>포스트 작성</span></h2>
|
||||||
<form action="/write" method="post" class="uk-form-stacked">
|
<form action="/write" method="post" class="uk-form-stacked">
|
||||||
<div class="uk-margin">
|
<div class="uk-margin">
|
||||||
<label class="uk-form-label" for="title">제목</label>
|
<label class="uk-form-label" for="title">제목</label>
|
||||||
@@ -79,6 +79,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$(".nav__div").hide();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
# Webhook Test - Wixon Blog
|
# Webhook Test - Wixon Blog
|
||||||
|
|
||||||
테스트 시간: 2025-12-08 17:33 KST
|
테스트 시간: 2025-12-08 17:33 KST
|
||||||
|
업데이트: 2025-12-08 17:38 KST
|
||||||
|
|
||||||
이 파일은 webhook 테스트를 위해 생성되었습니다.
|
이 파일은 webhook 테스트를 위해 생성되었습니다.
|
||||||
|
|
||||||
|
## 앱 재시작 테스트
|
||||||
|
|||||||
Reference in New Issue
Block a user