....
This commit is contained in:
@@ -29,11 +29,10 @@
|
||||
<p th:if="${srcPost.writeTime != null and srcPost.writeTime > 0}"
|
||||
th:text="${#temporals.format(T(java.time.Instant).ofEpochMilli(srcPost.writeTime).atZone(T(java.time.ZoneId).systemDefault()).toLocalDateTime(), 'yyyy-MM-dd HH:mm:ss')}"></p>
|
||||
</header>
|
||||
<div style="text-align: right; margin-bottom: 2em;">
|
||||
<label for="post-published-switch" style="font-weight: bold; color: #555; vertical-align: middle; margin-right: 10px;">
|
||||
게시물 공개
|
||||
</label>
|
||||
<input type="checkbox" id="post-published-switch" name="posting" th:checked="${srcPost.posting}" />
|
||||
<div style="text-align: right; margin-bottom: 2em; display: flex; align-items: center; justify-content: flex-end;">
|
||||
<span style="font-weight: bold; color: #555; margin-right: 10px;">게시물 공개</span>
|
||||
<input type="checkbox" id="post-published-switch" name="posting" th:checked="${srcPost.posting}" class="custom-checkbox" />
|
||||
<label for="post-published-switch" class="custom-label"></label>
|
||||
</div>
|
||||
<div class="write_controllbox" style="margin-top: -1em; margin-bottom: 2em;">
|
||||
<div class="write_option btn-example controlbox-category" to="#popLayer1">
|
||||
@@ -54,8 +53,13 @@
|
||||
<th:block sec:authorize="isAuthenticated()">
|
||||
<div class="container">
|
||||
<div id="editor"></div>
|
||||
|
||||
<button id="save" class="button fit" style="margin-top: 1em;" onclick="save()">저장하기</button>
|
||||
<div style="display: flex; gap: 1em; margin-top: 1em;">
|
||||
<button id="save" class="button fit primary" onclick="save()">저장하기</button>
|
||||
<button id="delete" class="button fit alt"
|
||||
th:attr="data-post-id=${srcPost.id}"
|
||||
onclick="deleteCurrentPost(this)"
|
||||
th:if="${srcPost.id != null}">삭제하기</button>
|
||||
</div>
|
||||
</div>
|
||||
</th:block>
|
||||
</section>
|
||||
|
||||
@@ -7,7 +7,11 @@
|
||||
<th:block layout:fragment="content" id="content">
|
||||
<section id="banner"
|
||||
th:styleappend="${randomBannerImage != null} ? |background-image: url('${apiBaseUrl}${randomBannerImage}');| : ''">
|
||||
<header>
|
||||
<header th:if="${gibberish != null}">
|
||||
<h2>Bum's Gibberish: <em>[[${gibberish}]]</em></h2>
|
||||
<a th:href="@{/blog/viewer/{id}(id=${gibberishId})}" class="button">코멘트 남기기<br>[Leave a Comment]</a>
|
||||
</header>
|
||||
<header th:if="${gibberish == null}">
|
||||
<h2>Bum's: <em>짧은 헛소리 혹은 기사?! 링크 있으면 링크까지</a></em></h2>
|
||||
<a href="#" class="button">더 읽으쉴?!<br>[Read More Gibberish]</a>
|
||||
</header>
|
||||
@@ -102,10 +106,19 @@
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="wrapper style1" sec:authorize="isAuthenticated()">
|
||||
<div class="container">
|
||||
<div id="gibberish-form" class="box post">
|
||||
<h4>오늘의 Gibberish 남기기 (100자 이내)</h4>
|
||||
<textarea id="gibberish-content" rows="3" maxlength="100" placeholder="문득 떠오른 생각을 적어보세요..."></textarea>
|
||||
<button class="button" style="margin-top: 1em;" onclick="submitGibberish()">등록</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section id="cta2" class="wrapper style3">
|
||||
<div class="container">
|
||||
<header>
|
||||
<h2>Are you ready to continue your quest?</h2>
|
||||
<!-- <h2>Are you ready to continue your quest?</h2>-->
|
||||
</header>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<!doctype html>
|
||||
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.w3.org/1999/xhtml" layout:decorate="~{layout/default_layout}">
|
||||
<head>
|
||||
<link href="https://cdn.jsdelivr.net/npm/quill@2/dist/quill.snow.css" rel="stylesheet" />
|
||||
<link href="https://cdn.jsdelivr.net/npm/quill-table-better@1/dist/quill-table-better.css" rel="stylesheet" />
|
||||
<link href="https://cdn.jsdelivr.net/npm/quill@2/dist/quill.snow.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/quill-table-better@1/dist/quill-table-better.css" rel="stylesheet">
|
||||
<script src="https://cdn.jsdelivr.net/npm/quill@2/dist/quill.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/quill-table-better@1/dist/quill-table-better.js"></script>
|
||||
</head>
|
||||
@@ -11,7 +11,7 @@
|
||||
<section class="wrapper style1">
|
||||
<div class="container">
|
||||
<div id="license-content-container">
|
||||
<p>#lun ##Dependency License Report <em>2025-09-15 13:34:42 KST</em></p>
|
||||
<p>#lun ##Dependency License Report <em>2025-09-24 15:26:44 KST</em></p>
|
||||
<h2>Apache 2.0</h2>
|
||||
<p><strong>1</strong> <strong>Group:</strong> <code>com.google.android</code> <strong>Name:</strong> <code>annotations</code> <strong>Version:</strong> <code>4.1.1.4</code></p>
|
||||
<blockquote>
|
||||
@@ -41,7 +41,7 @@
|
||||
<li><strong>POM License</strong>: Apache 2.0 - <a href="http://www.apache.org/licenses/LICENSE-2.0.txt">http://www.apache.org/licenses/LICENSE-2.0.txt</a></li>
|
||||
</ul>
|
||||
</blockquote>
|
||||
<p><strong>5</strong> <strong>Group:</strong> <code>com.google.errorprone</code> <strong>Name:</strong> <code>error_prone_annotations</code> <strong>Version:</strong> <code>2.27.0</code></p>
|
||||
<p><strong>5</strong> <strong>Group:</strong> <code>com.google.errorprone</code> <strong>Name:</strong> <code>error_prone_annotations</code> <strong>Version:</strong> <code>2.26.1</code></p>
|
||||
<blockquote>
|
||||
<ul>
|
||||
<li><strong>Manifest Project URL</strong>: <a href="https://errorprone.info/error_prone_annotations">https://errorprone.info/error_prone_annotations</a></li>
|
||||
@@ -766,10 +766,10 @@
|
||||
<li><strong>POM License</strong>: Apache-2.0 - <a href="https://www.apache.org/licenses/LICENSE-2.0.txt">https://www.apache.org/licenses/LICENSE-2.0.txt</a></li>
|
||||
</ul>
|
||||
</blockquote>
|
||||
<p><strong>100</strong> <strong>Group:</strong> <code>com.google.code.gson</code> <strong>Name:</strong> <code>gson</code> <strong>Version:</strong> <code>2.11.0</code></p>
|
||||
<p><strong>100</strong> <strong>Group:</strong> <code>com.google.code.gson</code> <strong>Name:</strong> <code>gson</code> <strong>Version:</strong> <code>2.10.1</code></p>
|
||||
<blockquote>
|
||||
<ul>
|
||||
<li><strong>Manifest Project URL</strong>: <a href="https://github.com/google/gson">https://github.com/google/gson</a></li>
|
||||
<li><strong>Manifest Project URL</strong>: <a href="https://github.com/google/gson/gson">https://github.com/google/gson/gson</a></li>
|
||||
<li><strong>Manifest License</strong>: "Apache-2.0";link="https://www.apache.org/licenses/LICENSE-2.0.txt" (Not Packaged)</li>
|
||||
<li><strong>POM License</strong>: Apache-2.0 - <a href="https://www.apache.org/licenses/LICENSE-2.0.txt">https://www.apache.org/licenses/LICENSE-2.0.txt</a></li>
|
||||
</ul>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<div id="content_inner">
|
||||
<article>
|
||||
<th:block th:each="post, iterStat : ${postsPage.content}">
|
||||
<section>
|
||||
<section th:id="'post-section-' + ${post.id}">
|
||||
<div class="box post" th:id="${post.id}">
|
||||
<a href="javascript:void(0);" th:onclick="'goToViewer(this.parentNode)'" class="image left">
|
||||
<img th:src="${post.thumb != null and not #strings.isEmpty(post.thumb)} ? ${apiBaseUrl + post.thumb} : @{/images/pic01.jpg}" alt="Post Thumbnail" />
|
||||
@@ -40,7 +40,8 @@
|
||||
<footer sec:authorize="isAuthenticated()"
|
||||
th:if="${#authentication.name == post.writer or #authorization.expression('hasRole(''ADMIN'')')}"
|
||||
style="text-align: right; margin-top: 1em;">
|
||||
<a th:href="@{/blog/edit/{postId}(postId=${post.id})}" class="button small alt">수정</a>
|
||||
<a th:href="@{/blog/edit/{postId}(postId=${post.id})}" class="button small">수정</a>
|
||||
<button class="button small alt" th:onclick="handleDeletePostInList([[${post.id}]], event)">삭제</button>
|
||||
</footer>
|
||||
</div>
|
||||
</section>
|
||||
@@ -93,5 +94,51 @@
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script th:inline="javascript">
|
||||
// CSRF 토큰을 meta 태그에서 읽어옴 (POST, DELETE 요청 시 필요)
|
||||
const csrfToken = document.querySelector('meta[name="_csrf"]')?.getAttribute('content');
|
||||
const csrfHeader = document.querySelector('meta[name="_csrf_header"]')?.getAttribute('content');
|
||||
|
||||
/**
|
||||
* 게시물 목록에서 게시물을 삭제하는 함수
|
||||
* @param {string} postId - 삭제할 게시물의 ID
|
||||
* @param {Event} event - 클릭 이벤트 객체
|
||||
*/
|
||||
function handleDeletePostInList(postId, event) {
|
||||
// 이벤트 버블링을 막아 게시물 상세보기로 이동하는 것을 방지합니다.
|
||||
event.stopPropagation();
|
||||
|
||||
const cleanPostId = postId.replace(/^"|"$/g, '');
|
||||
if (confirm('이 게시물을 정말로 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.')) {
|
||||
fetch(`/blog/post/${cleanPostId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
[csrfHeader]: csrfToken
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
return response.json();
|
||||
} else {
|
||||
// 서버에서 에러 메시지를 보냈을 경우를 대비
|
||||
return response.json().then(err => { throw new Error(err.message || '삭제에 실패했습니다.') });
|
||||
}
|
||||
})
|
||||
.then(data => {
|
||||
alert(data.message || '게시물이 성공적으로 삭제되었습니다.');
|
||||
// UI에서 해당 게시물 섹션을 제거하여 즉시 반영
|
||||
const postSection = document.getElementById(`post-section-${cleanPostId}`);
|
||||
if (postSection) {
|
||||
postSection.remove();
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('삭제 처리 중 오류가 발생했습니다: ' + error.message);
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</th:block>
|
||||
</html>
|
||||
@@ -246,6 +246,15 @@
|
||||
</div>
|
||||
</div>
|
||||
<div id="userManagement" class="tab-content" sec:authorize="hasRole('ADMIN')">
|
||||
<div class="box" style="border: 2px solid #e67e22; background: #fff3e0;">
|
||||
<h4>데이터 마이그레이션 (주의!)</h4>
|
||||
<p>
|
||||
게시물 데이터 구조를 새 버전으로 업그레이드합니다.
|
||||
<strong>이 작업은 시스템 전체에 영향을 미치며, 반드시 한 번만 실행해야 합니다.</strong>
|
||||
</p>
|
||||
<button id="run-migration-btn" class="button alt">게시물 데이터 마이그레이션 실행</button>
|
||||
<div id="migration-result" style="margin-top: 1em; font-weight: bold;"></div>
|
||||
</div>
|
||||
<div class="box">
|
||||
<h4>권한 요청</h4>
|
||||
<ul id="permission-requests-list" class="user-list">
|
||||
@@ -281,8 +290,13 @@
|
||||
<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>
|
||||
|
||||
<a th:href="@{/user/admin/posts/{postId}/history(postId=${post.id})}" class="button small">히스토리</a>
|
||||
|
||||
<button th:if="${!post.isBlocked}" class="button small" th:onclick="handleContent('[[${post.id}]]', 'block')">차단</button>
|
||||
<button th:if="${post.isBlocked}" class="button small" th:onclick="handleContent('[[${post.id}]]', 'unblock')">차단 해제</button>
|
||||
|
||||
<button class="button small alt" th:onclick="handleDeletePost('[[${post.id}]]')">삭제</button>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -418,6 +432,43 @@
|
||||
.catch(error => console.error('Error:', error));
|
||||
}
|
||||
|
||||
|
||||
const migrationBtn = document.getElementById('run-migration-btn');
|
||||
if (migrationBtn) {
|
||||
migrationBtn.addEventListener('click', async function() {
|
||||
if (!await showConfirm('경고', '정말로 데이터 마이그레이션을 실행하시겠습니까? 이 작업은 되돌릴 수 없으며, 반드시 한 번만 실행해야 합니다.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const resultDiv = document.getElementById('migration-result');
|
||||
resultDiv.innerHTML = '마이그레이션 진행 중... 페이지를 닫지 마세요.';
|
||||
migrationBtn.disabled = true;
|
||||
|
||||
try {
|
||||
const response = await fetch('/user/admin/migrate-posts', {
|
||||
method: 'POST',
|
||||
headers: { [csrfHeader]: csrfToken }
|
||||
});
|
||||
|
||||
const report = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
resultDiv.innerHTML = `✅ 마이그레이션 성공!<br>
|
||||
- 처리된 게시물 그룹: ${report.processedGroups}<br>
|
||||
- 최신 글로 이전됨: ${report.latestPostsMigrated}<br>
|
||||
- 히스토리로 이전됨: ${report.historyPostsMigrated}`;
|
||||
resultDiv.style.color = 'green';
|
||||
} else {
|
||||
throw new Error(report.errors.join(', '));
|
||||
}
|
||||
} catch (error) {
|
||||
resultDiv.innerHTML = `❌ 마이그레이션 실패: ${error.message}`;
|
||||
resultDiv.style.color = 'red';
|
||||
migrationBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* (관리자) 콘텐츠(게시물)를 차단하거나 해제하는 함수
|
||||
* @param {string} postId - 대상 게시물 ID
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org"
|
||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
|
||||
layout:decorate="~{layout/default_layout}">
|
||||
|
||||
<th:block layout:fragment="content">
|
||||
<section class="wrapper style1">
|
||||
<div class="container">
|
||||
<header class="major">
|
||||
<h2 th:text="${pageTitle}">게시물 수정 히스토리</h2>
|
||||
<p><strong>원본글 ID:</strong> <span th:text="${currentPost.id}"></span></p>
|
||||
</header>
|
||||
|
||||
<div class="box">
|
||||
<h4>현재 버전</h4>
|
||||
<p><strong>수정일:</strong> <span th:text="${#dates.format(currentPost.modifyTime, 'yyyy-MM-dd HH:mm')}"></span></p>
|
||||
<a th:href="@{/blog/viewer/{postId}(postId=${currentPost.id})}" class="button small">현재 글 보기</a>
|
||||
<a th:href="@{/blog/edit/{postId}(postId=${currentPost.id})}" class="button small alt">현재 글 수정</a>
|
||||
</div>
|
||||
|
||||
<div class="box" style="margin-top: 2em;">
|
||||
<h4>과거 버전 (히스토리)</h4>
|
||||
<div class="table-wrapper">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>보관된 시간</th>
|
||||
<th>제목</th>
|
||||
<th>작성자</th>
|
||||
<th>상태</th>
|
||||
<th>관리</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr th:each="h : ${historyList}">
|
||||
<td th:text="${#dates.format(h.archivedAt, 'yyyy-MM-dd HH:mm:ss')}"></td>
|
||||
<td th:text="${h.title}"></td>
|
||||
<td th:text="${h.writer}"></td>
|
||||
<td>
|
||||
<span th:if="${h.posting}" class="tag-item" style="background: #a8dadc;">공개</span>
|
||||
<span th:unless="${h.posting}" class="tag-item" style="background: #f1faee;">비공개</span>
|
||||
</td>
|
||||
<td>
|
||||
<a href="#" class="button small primary">내용 보기</a>
|
||||
<a href="#" class="button small alt">이 버전으로 복구</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr th:if="${#lists.isEmpty(historyList)}">
|
||||
<td colspan="5" style="text-align: center;">저장된 히스토리가 없습니다.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</th:block>
|
||||
</html>
|
||||
Reference in New Issue
Block a user