관리자 페이지 추가 및 버그 수정
- 관리자 대시보드 추가 (/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:
74
templates/admin/base_admin.html
Normal file
74
templates/admin/base_admin.html
Normal file
@@ -0,0 +1,74 @@
|
||||
<!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/style.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>
|
||||
<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 %}
|
||||
Reference in New Issue
Block a user