2025-09-15 17:18:44 +09:00
<!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 : 2 px solid #ddd ; margin-bottom : 1.5 em ; flex-wrap : wrap ; }
. tab-link { padding : 10 px 20 px ; cursor : pointer ; border : 1 px solid transparent ; border-bottom : 0 ; color : #777 ; }
. tab-link . active { border-color : #ddd ; border-bottom-color : white ; background : white ; margin-bottom : -2 px ; 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 : 10 px ; border-bottom : 1 px solid #eee ; }
. user-list li : last-child , . post-list li : last-child { border-bottom : none ; }
. button . small { margin-left : 0.5 em ; }
</ 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 >
2025-09-16 18:42:55 +09:00
< div class = "tab-link" onclick = "openTab(event, 'myRanks')" > 내 게임 랭킹</ div >
2025-09-15 17:18:44 +09:00
< th:block sec:authorize = "hasRole('ADMIN')" >
< div class = "tab-link" onclick = "openTab(event, 'userManagement')" > 회원 관리</ div >
2025-09-16 18:42:55 +09:00
< div class = "tab-link" onclick = "openTab(event, 'postManagement')" > 게시물 관리</ div >
< div class = "tab-link" onclick = "openTab(event, 'bannerManagement')" > 배너 관리</ div >
2025-09-15 17:18:44 +09:00
</ 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 >
2025-09-16 18:42:55 +09:00
< 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 >
2025-09-15 17:18:44 +09:00
< 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 >
2025-09-16 18:42:55 +09:00
< div id = "postManagement" class = "tab-content" sec:authorize = "hasRole('ADMIN')" >
2025-09-15 17:18:44 +09:00
< 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 >
2025-09-16 18:42:55 +09:00
< 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 >
2025-09-15 17:18:44 +09:00
</ 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 ) {
2025-09-16 18:42:55 +09:00
const cleanpostId = postId . replace ( /^"|"$/g , '' );
const url = `/blog/post/ ${ cleanpostId } / ${ action } ` ;
2025-09-15 17:18:44 +09:00
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 ));
}
2025-09-16 18:42:55 +09:00
/**
* (관리자) 이미지의 배너 사용 권한을 승인하거나 해제하는 함수
* @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 ( '작업 중 오류가 발생했습니다.' );
});
}
2025-09-15 17:18:44 +09:00
</ script >
</ th:block >
</ html >