320 lines
17 KiB
HTML
320 lines
17 KiB
HTML
<!DOCTYPE html>
|
|
<html xmlns:th="http://www.thymeleaf.org"
|
|
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
|
|
xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
|
|
layout:decorate="~{layout/default_layout}">
|
|
|
|
<th:block layout:fragment="head">
|
|
<style>
|
|
.tabs { display: flex; border-bottom: 2px solid #ddd; margin-bottom: 1.5em; flex-wrap: wrap; }
|
|
.tab-link { padding: 10px 20px; cursor: pointer; border: 1px solid transparent; border-bottom: 0; color: #777; }
|
|
.tab-link.active { border-color: #ddd; border-bottom-color: white; background: white; margin-bottom: -2px; font-weight: bold; color: #333; }
|
|
.tab-content { display: none; }
|
|
.tab-content.active { display: block; }
|
|
.user-list, .post-list { list-style: none; padding-left: 0; }
|
|
.user-list li, .post-list li { display: flex; justify-content: space-between; align-items: center; padding: 10px; border-bottom: 1px solid #eee; }
|
|
.user-list li:last-child, .post-list li:last-child { border-bottom: none; }
|
|
.button.small { margin-left: 0.5em; }
|
|
</style>
|
|
</th:block>
|
|
|
|
<th:block layout:fragment="content">
|
|
<section class="wrapper style1">
|
|
<div class="container">
|
|
<header class="major">
|
|
<h2 th:text="${isAdmin} ? '관리자 대시보드' : (${user.user_id} + '님의 정보')"></h2>
|
|
<p>가입일: <span th:text="${joinDate}"></span></p>
|
|
</header>
|
|
|
|
<div class="tabs">
|
|
<div class="tab-link active" onclick="openTab(event, 'myInfo')">내 정보</div>
|
|
<div class="tab-link" onclick="openTab(event, 'myPosts')">내가 쓴 글</div>
|
|
<div class="tab-link" onclick="openTab(event, 'myComments')">내가 쓴 댓글</div>
|
|
<div class="tab-link" onclick="openTab(event, 'myRanks')">내 게임 랭킹</div>
|
|
<th:block sec:authorize="hasRole('ADMIN')">
|
|
<div class="tab-link" onclick="openTab(event, 'userManagement')">회원 관리</div>
|
|
<div class="tab-link" onclick="openTab(event, 'postManagement')">게시물 관리</div>
|
|
<div class="tab-link" onclick="openTab(event, 'bannerManagement')">배너 관리</div>
|
|
</th:block>
|
|
</div>
|
|
|
|
<div id="myInfo" class="tab-content active">
|
|
<div class="box">
|
|
<h3>기본 정보</h3>
|
|
<ul>
|
|
<li><strong>아이디:</strong> <span th:text="${user.user_id}"></span></li>
|
|
<li><strong>이메일:</strong> <span th:text="${user.user_email}"></span></li>
|
|
<li>
|
|
<strong>현재 권한:</strong> <span th:id="'user-role-status-' + ${user.user_id}" th:text="${user.getRole().name()}"></span>
|
|
<th:block th:if="${user.getRole().name() == 'READ' and !user.writePermissionRequested}">
|
|
<a href="javascript:requestWritePermission()" id="request-perm-btn" class="button small alt" style="margin-left: 1em;">글쓰기 권한 요청</a>
|
|
</th:block>
|
|
<span th:if="${user.writePermissionRequested}" id="request-perm-status" style="margin-left: 1em; color: #007bff;">(권한 요청 처리 중)</span>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="myPosts" class="tab-content">
|
|
<div class="box">
|
|
<ul class="post-list">
|
|
<li th:each="post : ${myPosts}">
|
|
<a th:href="@{'/blog/viewer/' + ${post.id}}" th:text="${post.title}">게시물 제목</a>
|
|
<span th:text="${#dates.format(post.modifyTime, 'yyyy-MM-dd HH:mm')}"></span>
|
|
</li>
|
|
<li th:if="${#lists.isEmpty(myPosts)}">작성한 글이 없습니다.</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="myComments" class="tab-content">
|
|
<div class="box">
|
|
<ul class="post-list">
|
|
<li th:each="comment : ${myComments}">
|
|
<span th:text="${comment.content}">댓글 내용</span>
|
|
<a th:href="@{'/blog/viewer/' + ${comment.postId} + '#comment-' + ${comment.id}}">원문보기</a>
|
|
</li>
|
|
<li th:if="${#lists.isEmpty(myComments)}">작성한 댓글이 없습니다.</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="myRanks" class="tab-content">
|
|
<div class="box">
|
|
<ul class="post-list">
|
|
<li th:if="${#lists.isEmpty(myRanks)}">플레이한 게임 기록이 없습니다.</li>
|
|
<li th:each="rank : ${myRanks}">
|
|
<div>
|
|
<span th:switch="${rank.gameType.name()}">
|
|
<strong th:case="'GAME_2048'">2048</strong>
|
|
<strong th:case="'SUDOKU'">스도쿠</strong>
|
|
<strong th:case="'SPIDER'">스파이더 카드</strong>
|
|
<strong th:case="'NONOGRAM'">노노그램</strong>
|
|
<strong th:case="*">기타 게임</strong>
|
|
</span>
|
|
<span style="margin-left: 1em;" th:switch="${rank.gameType.name()}">
|
|
<span th:case="'SUDOKU'" th:text="|기록: ${rank.primaryScore / 60}분 ${rank.primaryScore % 60}초|"></span>
|
|
<span th:case="'NONOGRAM'" th:text="|기록: ${rank.primaryScore / 60}분 ${rank.primaryScore % 60}초|"></span>
|
|
<span th:case="'SPIDER'" th:text="|기록: ${rank.primaryScore} moves|"></span>
|
|
<span th:case="*" th:text="|점수: ${#numbers.formatInteger(rank.primaryScore, 3, 'COMMA')}|"></span>
|
|
</span>
|
|
</div>
|
|
<span th:text="${#temporals.format(rank.timestamp, 'yyyy-MM-dd HH:mm')}"></span>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="userManagement" class="tab-content" sec:authorize="hasRole('ADMIN')">
|
|
<div class="box">
|
|
<h4>권한 요청</h4>
|
|
<ul id="permission-requests-list" class="user-list">
|
|
<li th:each="reqUser : ${permissionRequests}" th:id="'request-row-' + ${reqUser.user_id}">
|
|
<span>
|
|
<strong th:text="${reqUser.user_id}"></strong> (<span th:text="${reqUser.user_email}"></span>)
|
|
</span>
|
|
<div>
|
|
<button class="button small primary" th:onclick="handlePermission('[[${reqUser.user_id}]]', 'approve')">승인</button>
|
|
<button class="button small alt" th:onclick="handlePermission('[[${reqUser.user_id}]]', 'reject')">거절</button>
|
|
</div>
|
|
</li>
|
|
<li th:if="${#lists.isEmpty(permissionRequests)}">새로운 권한 요청이 없습니다.</li>
|
|
</ul>
|
|
</div>
|
|
<div class="box" style="margin-top: 2em;">
|
|
<h4>전체 회원</h4>
|
|
<ul id="all-users-list" class="user-list">
|
|
<li th:each="u : ${allUsers}">
|
|
<span>
|
|
<strong th:text="${u.user_id}"></strong> - 현재 권한: <span th:id="'user-role-' + ${u.user_id}" th:text="${u.getRole().name()}"></span>
|
|
</span>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="postManagement" class="tab-content" sec:authorize="hasRole('ADMIN')">
|
|
<div class="box">
|
|
<h4>최신 글 관리</h4>
|
|
<ul class="post-list">
|
|
<li th:each="post : ${allRecentPosts}" th:id="'post-row-' + ${post.id}">
|
|
<a th:href="@{'/blog/viewer/' + ${post.id}}" th:text="${post.title}">게시물 제목</a>
|
|
<div>
|
|
<span th:if="${post.isBlocked}" style="color: red; margin-right: 1em;">(차단됨)</span>
|
|
<button th:if="${!post.isBlocked}" class="button small alt" th:onclick="handleContent('[[${post.id}]]', 'block')">차단</button>
|
|
<button th:if="${post.isBlocked}" class="button small" th:onclick="handleContent('[[${post.id}]]', 'unblock')">차단 해제</button>
|
|
</div>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
<div id="bannerManagement" class="tab-content" sec:authorize="hasRole('ADMIN')">
|
|
<div class="box">
|
|
<h4>배너 이미지 관리</h4>
|
|
<ul class="post-list">
|
|
<li th:each="image : ${allImages}" th:id="'image-row-' + ${image.id}">
|
|
<div style="display: flex; align-items: center; gap: 1em;">
|
|
<img th:src="@{'/api/images/' + ${image.fileName}}" alt="Image Thumbnail" style="width: 100px; height: 60px; object-fit: cover; border-radius: 4px;"/>
|
|
<div>
|
|
<strong th:text="${image.fileName}"></strong><br>
|
|
<span th:if="${image.isBannerCandidate}" style="color: #2a9d8f; font-weight: bold;">(배너로 사용 중)</span>
|
|
<span th:unless="${image.isBannerCandidate}" style="color: #888;">(배너로 사용 안 함)</span>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<button th:if="${!image.isBannerCandidate}" class="button small primary" th:onclick="handleBannerPermission('[[${image.id}]]', 'approve')">배너로 승인</button>
|
|
<button th:if="${image.isBannerCandidate}" class="button small alt" th:onclick="handleBannerPermission('[[${image.id}]]', 'revoke')">승인 해제</button>
|
|
</div>
|
|
</li>
|
|
<li th:if="${#lists.isEmpty(allImages)}">업로드된 이미지가 없습니다.</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</section>
|
|
|
|
<script th:inline="javascript">
|
|
// CSRF 토큰을 meta 태그에서 읽어옴 (POST 요청 시 필요)
|
|
const csrfToken = document.querySelector('meta[name="_csrf"]')?.getAttribute('content');
|
|
const csrfHeader = document.querySelector('meta[name="_csrf_header"]')?.getAttribute('content');
|
|
|
|
/**
|
|
* 탭 클릭 시 해당 탭 콘텐츠를 보여주는 함수
|
|
*/
|
|
function openTab(evt, tabName) {
|
|
// 모든 탭 콘텐츠 숨기기
|
|
document.querySelectorAll('.tab-content').forEach(tab => tab.style.display = 'none');
|
|
// 모든 탭 링크에서 'active' 클래스 제거
|
|
document.querySelectorAll('.tab-link').forEach(link => link.classList.remove('active'));
|
|
|
|
// 클릭된 탭 콘텐츠 보이기
|
|
document.getElementById(tabName).style.display = 'block';
|
|
// 클릭된 탭 링크에 'active' 클래스 추가
|
|
evt.currentTarget.classList.add('active');
|
|
}
|
|
|
|
/**
|
|
* '글쓰기 권한 요청'을 서버에 전송하는 함수
|
|
*/
|
|
function requestWritePermission() {
|
|
if (!confirm('글쓰기 권한을 요청하시겠습니까? 관리자 승인 후 적용됩니다.')) return;
|
|
|
|
fetch('/user/request-write', {
|
|
method: 'POST',
|
|
headers: { [csrfHeader]: csrfToken }
|
|
})
|
|
.then(response => {
|
|
if (response.ok) {
|
|
alert('요청이 성공적으로 접수되었습니다.');
|
|
// 버튼을 '처리 중' 텍스트로 변경
|
|
document.getElementById('request-perm-btn').style.display = 'none';
|
|
const statusSpan = document.getElementById('request-perm-status');
|
|
if(statusSpan) {
|
|
statusSpan.innerText = '(권한 요청 처리 중)';
|
|
} else {
|
|
// span이 없는 경우 새로 만들어 추가
|
|
const newStatusSpan = document.createElement('span');
|
|
newStatusSpan.id = 'request-perm-status';
|
|
newStatusSpan.innerText = '(권한 요청 처리 중)';
|
|
newStatusSpan.style = 'margin-left: 1em; color: #007bff;';
|
|
document.getElementById('request-perm-btn').parentElement.appendChild(newStatusSpan);
|
|
}
|
|
} else {
|
|
alert('요청에 실패했습니다. 잠시 후 다시 시도해주세요.');
|
|
}
|
|
})
|
|
.catch(error => console.error('Error:', error));
|
|
}
|
|
|
|
/**
|
|
* (관리자) 사용자 권한 요청을 처리하는 함수
|
|
* @param {string} userId - 대상 사용자 ID
|
|
* @param {'approve' | 'reject'} action - 수행할 작업
|
|
*/
|
|
function handlePermission(userId, action) {
|
|
const url = `/user/${action}-writer/${userId}`;
|
|
fetch(url, {
|
|
method: 'POST',
|
|
headers: { [csrfHeader]: csrfToken }
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data && data.user_id) {
|
|
alert(`'${userId}'님의 권한 요청을 ${action === 'approve' ? '승인' : '거절'}했습니다.`);
|
|
// UI에서 해당 항목 제거
|
|
document.getElementById(`request-row-${userId}`).remove();
|
|
// 전체 사용자 목록의 권한 정보 업데이트
|
|
const userRoleSpan = document.getElementById(`user-role-${userId}`);
|
|
if (userRoleSpan) {
|
|
userRoleSpan.innerText = data.role.name;
|
|
}
|
|
} else {
|
|
alert('작업에 실패했습니다.');
|
|
}
|
|
})
|
|
.catch(error => console.error('Error:', error));
|
|
}
|
|
|
|
/**
|
|
* (관리자) 콘텐츠(게시물)를 차단하거나 해제하는 함수
|
|
* @param {string} postId - 대상 게시물 ID
|
|
* @param {'block' | 'unblock'} action - 수행할 작업
|
|
*/
|
|
function handleContent(postId, action) {
|
|
const cleanpostId = postId.replace(/^"|"$/g, '');
|
|
const url = `/blog/post/${cleanpostId}/${action}`;
|
|
fetch(url, {
|
|
method: 'POST',
|
|
headers: { [csrfHeader]: csrfToken }
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data && data.id) {
|
|
alert(`게시물을 ${action === 'block' ? '차단' : '차단 해제'}했습니다.`);
|
|
location.reload(); // 페이지를 새로고침하여 상태 업데이트
|
|
} else {
|
|
alert('작업에 실패했습니다.');
|
|
}
|
|
})
|
|
.catch(error => console.error('Error:', error));
|
|
}
|
|
|
|
/**
|
|
* (관리자) 이미지의 배너 사용 권한을 승인하거나 해제하는 함수
|
|
* @param {string} imageId - 대상 이미지의 ID
|
|
* @param {'approve' | 'revoke'} action - 수행할 작업
|
|
*/
|
|
function handleBannerPermission(imageId, action) {
|
|
const cleanImageId = imageId.replace(/^"|"$/g, '');
|
|
|
|
// [수정] 깨끗하게 정리된 cleanImageId를 URL에 사용합니다.
|
|
const url = `/api/images/${cleanImageId}/${action === 'approve' ? 'approve-banner' : 'revoke-banner'}`;
|
|
|
|
|
|
fetch(url, {
|
|
method: 'POST',
|
|
headers: { [csrfHeader]: csrfToken }
|
|
})
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
throw new Error('Server returned an error.');
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(data => {
|
|
if (data && data.id) {
|
|
alert(`이미지 상태를 성공적으로 변경했습니다.`);
|
|
location.reload(); // 페이지를 새로고침하여 상태(버튼, 텍스트)를 즉시 반영
|
|
} else {
|
|
alert('작업에 실패했습니다.');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error:', error);
|
|
alert('작업 중 오류가 발생했습니다.');
|
|
});
|
|
}
|
|
</script>
|
|
</th:block>
|
|
</html> |