...
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
<!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="head">
|
||||
</th:block>
|
||||
|
||||
<th:block layout:fragment="content" id="content">
|
||||
<section class="wrapper style2">
|
||||
<div class="container">
|
||||
<header class="major">
|
||||
<h2 th:text="${srcPost.title}">소개글 제목</h2>
|
||||
<p>
|
||||
최종 수정일: <span th:text="${#temporals.format(T(java.time.Instant).ofEpochMilli(srcPost.modifyTime).atZone(T(java.time.ZoneId).systemDefault()).toLocalDateTime(), 'yyyy-MM-dd HH:mm')}"></span>
|
||||
</p>
|
||||
</header>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="wrapper style1">
|
||||
<div class="container">
|
||||
<article>
|
||||
<div id="editor"></div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/quill@2/dist/quill.snow.css" rel="stylesheet" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/quill@2/dist/quill.js"></script>
|
||||
<script>
|
||||
// DOM 로드 완료 후 Quill 에디터를 읽기 전용(false)으로 초기화
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
initEditor(false);
|
||||
});
|
||||
</script>
|
||||
</th:block>
|
||||
</html>
|
||||
@@ -0,0 +1,83 @@
|
||||
<!DOCTYPE html>
|
||||
<html
|
||||
xmlns:th="http://www.thymeleaf.org"
|
||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
|
||||
layout:decorate="~{layout/default_layout}">
|
||||
<head>
|
||||
<title>Bookmarks</title>
|
||||
</head>
|
||||
<th:block layout:fragment="content">
|
||||
<section class="wrapper style2">
|
||||
<div class="container">
|
||||
<header class="major">
|
||||
<h2>Bookmarks</h2>
|
||||
<p>다른 사용자들이 저장한 유용한 페이지들을 둘러보세요.</p>
|
||||
</header>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="wrapper style1">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12" th:if="${bookmarksPage.empty}">
|
||||
<p style="text-align: center;">아직 저장된 페이지가 없습니다.</p>
|
||||
</div>
|
||||
<div class="col-4 col-6-medium col-12-small" th:each="bookmark : ${bookmarksPage.content}">
|
||||
<section class="box feature">
|
||||
<a href="javascript:void(0);"
|
||||
th:data-url="${bookmark.url}"
|
||||
th:data-title="${bookmark.title}"
|
||||
onclick="showBookmarkOptions(this)" class="image featured">
|
||||
<img th:src="${bookmark.thumbnailUrl ?: '/images/pic01.jpg'}" alt="Thumbnail" />
|
||||
</a>
|
||||
<div class="inner">
|
||||
<header>
|
||||
<h3 th:text="${bookmark.title}">북마크 제목</h3>
|
||||
<p th:if="${bookmark.userComment}" th:text="${bookmark.userComment}" style="font-style: italic; color: #007bff;"></p>
|
||||
</header>
|
||||
<p th:text="${#strings.abbreviate(bookmark.description, 100)}"></p>
|
||||
|
||||
<div class="bookmark-controls" style="margin-top: 1em; padding-top: 1em; border-top: 1px solid #eee;">
|
||||
<div class="vote-controls" th:data-bookmark-id="${bookmark.id}" style="text-align: center; margin-bottom: 1em;">
|
||||
<button class="button small alt" th:onclick="handleBookmarkVote(this, 'like')">
|
||||
👍 (<span class="like-count" th:text="${bookmark.voteCount}">0</span>)
|
||||
</button>
|
||||
<button class="button small alt" th:onclick="handleBookmarkVote(this, 'unlike')" style="margin-left: 0.5em;">
|
||||
👎 (<span class="unlike-count" th:text="${bookmark.unlikeCount}">0</span>)
|
||||
</button>
|
||||
</div>
|
||||
<a href="javascript:void(0);" th:onclick="toggleCommentSection('[[${bookmark.id}]]')" class="button small fit">댓글 보기</a>
|
||||
<div th:id="'comment-section-' + ${bookmark.id}" class="comment-section" style="display: none; margin-top: 1em;">
|
||||
<div th:id="'comments-list-' + ${bookmark.id}" class="comments-list"></div>
|
||||
<textarea th:id="'comment-input-' + ${bookmark.id}" placeholder="댓글을 입력하세요..." style="margin-top: 1em;"></textarea>
|
||||
<button class="button small" th:onclick="submitBookmarkComment('[[${bookmark.id}]]')">등록</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer style="font-size: 0.8em; color: #888; text-align: right; margin-top: 1em;">
|
||||
by <span th:text="${bookmark.userId}"></span>
|
||||
</footer>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav th:if="${bookmarksPage.totalPages > 1}" style="text-align: center; margin-top: 2.5em;">
|
||||
<ul class="pagination">
|
||||
<li th:classappend="${bookmarksPage.first} ? 'disabled'">
|
||||
<a th:href="@{/bookmarks(page=${bookmarksPage.number - 1})}" class="button alt small">Prev</a>
|
||||
</li>
|
||||
<li th:each="pageNum : ${#numbers.sequence(0, bookmarksPage.totalPages - 1)}">
|
||||
<a th:href="@{/bookmarks(page=${pageNum})}"
|
||||
th:text="${pageNum + 1}"
|
||||
th:class="${pageNum == bookmarksPage.number} ? 'button small' : 'button alt small'"></a>
|
||||
</li>
|
||||
<li th:classappend="${bookmarksPage.last} ? 'disabled'">
|
||||
<a th:href="@{/bookmarks(page=${bookmarksPage.number + 1})}" class="button alt small">Next</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</section>
|
||||
</th:block>
|
||||
</html>
|
||||
@@ -7,11 +7,7 @@
|
||||
>
|
||||
|
||||
<th:block layout:fragment="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" />
|
||||
<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>
|
||||
<script>document.addEventListener('DOMContentLoaded', function() {initEditor(true)});</script>
|
||||
|
||||
</th:block>
|
||||
|
||||
<body>
|
||||
@@ -110,6 +106,11 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
<script>document.addEventListener('DOMContentLoaded', function() {initEditor(true)});</script>
|
||||
</th:block>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,7 +1,10 @@
|
||||
<!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.quilljs.com/1.3.6/quill.snow.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>
|
||||
<body>
|
||||
<th:block layout:fragment="content" id="content">
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
<!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="head">
|
||||
<style>
|
||||
.message-list { list-style: none; padding-left: 0; }
|
||||
.message-item { border: 1px solid #ddd; border-radius: 5px; margin-bottom: 1em; }
|
||||
.message-header { padding: 10px 15px; background: #f7f7f7; cursor: pointer; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; }
|
||||
.message-item.unread .message-header { font-weight: bold; background: #fffbe5; border-left: 3px solid #FFA500; }
|
||||
.message-content { padding: 15px; display: none; border-top: 1px solid #ddd; }
|
||||
.message-content.active { display: block; }
|
||||
.reply-form { margin-top: 1em; }
|
||||
.message-title { flex-grow: 1; }
|
||||
</style>
|
||||
</th:block>
|
||||
|
||||
<th:block layout:fragment="content">
|
||||
<section class="wrapper style1">
|
||||
<div class="container">
|
||||
<header class="major">
|
||||
<h2 th:text="${pageTitle}"></h2>
|
||||
</header>
|
||||
|
||||
<div class="box">
|
||||
<ul class="message-list">
|
||||
<li th:if="${#lists.isEmpty(messages)}">받은 쪽지가 없습니다.</li>
|
||||
<li th:each="msg : ${messages}" class="message-item" th:classappend="${!msg.isRead} ? 'unread'" th:data-message-id="${msg.id}">
|
||||
<div class="message-header" onclick="toggleMessage(this)">
|
||||
<div class="message-title">
|
||||
<span th:text="${msg.title}"></span>
|
||||
<small style="margin-left: 1em; color: #777;" th:text="'보낸 사람: ' + ${msg.senderId}"></small>
|
||||
</div>
|
||||
<small th:text="${#temporals.format(T(java.time.Instant).ofEpochMilli(msg.timestamp).atZone(T(java.time.ZoneId).systemDefault()), 'yyyy-MM-dd HH:mm')}"></small>
|
||||
</div>
|
||||
<div class="message-content">
|
||||
<p style="white-space: pre-wrap;" th:text="${msg.content}"></p>
|
||||
<hr/>
|
||||
<div class="reply-form">
|
||||
<h4>답장 보내기</h4>
|
||||
<form onsubmit="sendMessage(event, this)">
|
||||
<input type="hidden" name="receiverId" th:value="${msg.senderId}" />
|
||||
<div class="row gtr-50">
|
||||
<div class="col-12">
|
||||
<input type="text" name="title" placeholder="제목" th:value="'RE: ' + ${msg.title}" required />
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<textarea name="content" placeholder="내용" rows="4" required></textarea>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<button type="submit" class="button primary">답장 전송</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script th:inline="javascript">
|
||||
const csrfToken = document.querySelector('meta[name="_csrf"]')?.getAttribute('content');
|
||||
const csrfHeader = document.querySelector('meta[name="_csrf_header"]')?.getAttribute('content');
|
||||
|
||||
async function toggleMessage(headerElement) {
|
||||
const messageItem = headerElement.closest('.message-item');
|
||||
const content = messageItem.querySelector('.message-content');
|
||||
const messageId = messageItem.dataset.messageId;
|
||||
|
||||
const isOpening = !content.classList.contains('active');
|
||||
|
||||
// 모든 열린 쪽지 닫기
|
||||
document.querySelectorAll('.message-content.active').forEach(c => {
|
||||
if(c !== content) c.classList.remove('active');
|
||||
});
|
||||
content.classList.toggle('active');
|
||||
|
||||
if (isOpening && messageItem.classList.contains('unread')) {
|
||||
const response = await fetch(`/messages/${messageId}/read`, {
|
||||
method: 'POST',
|
||||
headers: { [csrfHeader]: csrfToken }
|
||||
});
|
||||
if (response.ok) {
|
||||
messageItem.classList.remove('unread');
|
||||
// 헤더의 아이콘도 업데이트 할 수 있지만, 페이지 새로고침 전까지는 유지됩니다.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function sendMessage(event, form) {
|
||||
event.preventDefault();
|
||||
const formData = new FormData(form);
|
||||
const data = {
|
||||
receiverId: formData.get('receiverId'),
|
||||
title: formData.get('title'),
|
||||
content: formData.get('content')
|
||||
};
|
||||
|
||||
const submitButton = form.querySelector('button[type="submit"]');
|
||||
submitButton.disabled = true;
|
||||
submitButton.textContent = '전송 중...';
|
||||
|
||||
const response = await fetch('/messages/send', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', [csrfHeader]: csrfToken },
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
if(response.ok) {
|
||||
alert('답장을 성공적으로 보냈습니다.');
|
||||
form.querySelector('textarea').value = ''; // 내용만 초기화
|
||||
toggleMessage(form.closest('.message-item').querySelector('.message-header')); // 답장 후 창 닫기
|
||||
} else {
|
||||
alert('답장 보내기에 실패했습니다.');
|
||||
}
|
||||
submitButton.disabled = false;
|
||||
submitButton.textContent = '답장 전송';
|
||||
}
|
||||
</script>
|
||||
</th:block>
|
||||
</html>
|
||||
@@ -15,6 +15,49 @@
|
||||
.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; }
|
||||
.custom-radio {
|
||||
display: none; /* 기본 라디오 버튼 숨기기 */
|
||||
}
|
||||
|
||||
.custom-label {
|
||||
position: relative;
|
||||
padding-left: 25px; /* 라벨 왼쪽에 가짜 버튼을 위한 공간 확보 */
|
||||
cursor: pointer;
|
||||
line-height: 20px;
|
||||
display: inline-block;
|
||||
color: #555; /* 라벨 텍스트 색상 */
|
||||
}
|
||||
|
||||
/* 가짜 라디오 버튼 (원) 만들기 */
|
||||
.custom-label::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border: 2px solid #ddd;
|
||||
border-radius: 50%; /* 원 모양 */
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* 선택되었을 때 가짜 라디오 버튼 스타일 변경 */
|
||||
.custom-radio:checked + .custom-label::before {
|
||||
border-color: #FFA500; /* 테두리 색상 변경 (사이트의 포인트 색상) */
|
||||
background: #FFA500; /* 배경 색상 채우기 */
|
||||
}
|
||||
|
||||
/* 선택되었을 때 원 안에 작은 점 추가 */
|
||||
.custom-radio:checked + .custom-label::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 6px;
|
||||
top: 6px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: white;
|
||||
}
|
||||
</style>
|
||||
</th:block>
|
||||
|
||||
@@ -31,10 +74,15 @@
|
||||
<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>
|
||||
<div class="tab-link" onclick="openTab(event, 'myMessages')">쪽지함</div>
|
||||
<div class="tab-link" onclick="openTab(event, 'myBookmarks')">저장한 페이지</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>
|
||||
<div class="tab-link" onclick="openTab(event, 'aboutManagement')">사이트 소개 관리</div>
|
||||
|
||||
</th:block>
|
||||
</div>
|
||||
|
||||
@@ -78,7 +126,57 @@
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div id="myBookmarks" class="tab-content">
|
||||
<div class="box">
|
||||
<h4>새 페이지 저장하기</h4>
|
||||
<div id="bookmark-form">
|
||||
<input type="url" id="bookmark-url-input" placeholder="저장할 페이지 URL을 입력하세요" style="margin-bottom: 1em;">
|
||||
<div id="og-preview" style="display:none; border: 1px solid #ddd; padding: 1em; margin-bottom: 1em; border-radius: 5px;">
|
||||
<img id="og-image" src="" style="max-width: 150px; float: left; margin-right: 1em;">
|
||||
<h5 id="og-title"></h5>
|
||||
<p id="og-description" style="font-size: 0.9em; color: #555;"></p>
|
||||
</div>
|
||||
<textarea id="bookmark-comment-input" placeholder="이 페이지에 대한 나의 생각 (선택)" rows="3"></textarea>
|
||||
<div id="visibility-selector" style="margin-top: 1em; display: flex; align-items: center; flex-wrap: wrap;">
|
||||
<strong style="margin-right: 1.5em;">공개 범위:</strong>
|
||||
|
||||
<div style="display: flex; align-items: center; margin-right: 1.5em;">
|
||||
<input type="radio" name="visibility" id="visibility-private" value="PRIVATE" class="custom-radio" checked>
|
||||
<label for="visibility-private" class="custom-label">비공개</label>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; align-items: center; margin-right: 1.5em;">
|
||||
<input type="radio" name="visibility" id="visibility-members" value="MEMBERS" class="custom-radio">
|
||||
<label for="visibility-members" class="custom-label">회원 공개</label>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; align-items: center;">
|
||||
<input type="radio" name="visibility" id="visibility-public" value="PUBLIC" class="custom-radio">
|
||||
<label for="visibility-public" class="custom-label">전체 공개</label>
|
||||
</div>
|
||||
</div>
|
||||
<button id="save-bookmark-btn" class="button primary" style="margin-top: 1em;">저장하기</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="box" style="margin-top: 2em;">
|
||||
<h4>저장된 목록</h4>
|
||||
<div id="bookmarks-list" class="row">
|
||||
<div class="col-4 col-12-medium">
|
||||
<section class="box feature">
|
||||
<a href="#" class="image featured"><img src="/images/pic01.jpg" alt="" /></a>
|
||||
<div class="inner">
|
||||
<header>
|
||||
<h2>카드 제목</h2>
|
||||
<p>사용자 코멘트가 여기에 들어갑니다.</p>
|
||||
</header>
|
||||
<p style="font-size: 0.8em; color: #888;">원본 페이지 설명...</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="myRanks" class="tab-content">
|
||||
<div class="box">
|
||||
<ul class="post-list">
|
||||
@@ -104,7 +202,27 @@
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div id="myMessages" class="tab-content">
|
||||
<div class="box">
|
||||
<ul class="post-list">
|
||||
<li th:if="${#lists.isEmpty(myMessages)}">주고받은 쪽지가 없습니다.</li>
|
||||
<li th:each="msg : ${myMessages}">
|
||||
<div style="display: flex; align-items: center; gap: 1em;">
|
||||
<span th:if="${msg.senderId == user.user_id}" class="tag-item" style="background: #e0f7fa;">보냄</span>
|
||||
<span th:if="${msg.receiverId == user.user_id}" class="tag-item" style="background: #fffbe5;">받음</span>
|
||||
|
||||
<div style="min-width: 150px;">
|
||||
<strong th:if="${msg.senderId == user.user_id}" th:text="'To: ' + ${msg.receiverId}"></strong>
|
||||
<strong th:if="${msg.receiverId == user.user_id}" th:text="'From: ' + ${msg.senderId}"></strong>
|
||||
</div>
|
||||
|
||||
<a th:href="@{/messages}" th:text="${msg.title}">쪽지 제목</a>
|
||||
</div>
|
||||
<span th:text="${#temporals.format(T(java.time.Instant).ofEpochMilli(msg.timestamp).atZone(T(java.time.ZoneId).systemDefault()), '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>
|
||||
@@ -154,7 +272,8 @@
|
||||
<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;"/>
|
||||
<!-- <img th:src="${post.thumb != null and not #strings.isEmpty(post.thumb)} ? ${apiBaseUrl + post.thumb} : @{/images/pic01.jpg}" alt="Post Thumbnail" />-->
|
||||
<img th:src="${apiBaseUrl + '/api/images/' + image.fileName + '?type=thumbnail'}" 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>
|
||||
@@ -170,6 +289,27 @@
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div id="aboutManagement" class="tab-content" sec:authorize="hasRole('ADMIN')">
|
||||
<div class="box">
|
||||
<h4>사이트 소개글 관리</h4>
|
||||
<p>
|
||||
'사이트 소개' 페이지에 표시될 내용입니다. 글을 수정하면 이전 버전은 히스토리로 여기에 남게 됩니다.
|
||||
</p>
|
||||
<th:block th:with="latestAbout=${!#lists.isEmpty(aboutPostHistory) ? aboutPostHistory[0] : null}">
|
||||
<a th:if="${latestAbout != null}" th:href="@{/blog/edit/{postId}(postId=${latestAbout.id})}" class="button primary">최신 소개글 수정</a>
|
||||
<a th:if="${latestAbout == null}" th:href="@{/blog/edit(type='ABOUT_SITE')}" class="button">새 소개글 작성</a>
|
||||
</th:block>
|
||||
|
||||
<h5 style="margin-top: 2em;">수정 히스토리</h5>
|
||||
<ul class="post-list">
|
||||
<li th:each="post : ${aboutPostHistory}">
|
||||
<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(aboutPostHistory)}">작성된 소개글이 없습니다.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
@@ -315,6 +455,83 @@
|
||||
alert('작업 중 오류가 발생했습니다.');
|
||||
});
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const urlInput = document.getElementById('bookmark-url-input');
|
||||
const preview = document.getElementById('og-preview');
|
||||
|
||||
let ogData = {}; // OG 파싱 결과를 저장할 변수
|
||||
|
||||
// URL 입력 필드에서 포커스가 벗어났을 때(onblur) OG 정보 파싱 API 호출
|
||||
urlInput.addEventListener('blur', async function() {
|
||||
const url = this.value.trim();
|
||||
if (!url) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/og/parse?url=${encodeURIComponent(url)}`);
|
||||
if (!response.ok) throw new Error('파싱 실패');
|
||||
|
||||
ogData = await response.json();
|
||||
|
||||
// 미리보기 UI 업데이트
|
||||
document.getElementById('og-title').textContent = ogData.title || '제목 없음';
|
||||
document.getElementById('og-description').textContent = ogData.description || '';
|
||||
const ogImage = document.getElementById('og-image');
|
||||
if (ogData.thumbnailUrl) {
|
||||
ogImage.src = ogData.thumbnailUrl;
|
||||
ogImage.style.display = 'block';
|
||||
} else {
|
||||
ogImage.style.display = 'none';
|
||||
}
|
||||
preview.style.display = 'block';
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
preview.style.display = 'none';
|
||||
alert('페이지 정보를 가져오는 데 실패했습니다. URL을 확인해주세요.');
|
||||
}
|
||||
});
|
||||
|
||||
// 저장 버튼 클릭 이벤트
|
||||
document.getElementById('save-bookmark-btn').addEventListener('click', async function() {
|
||||
const comment = document.getElementById('bookmark-comment-input').value.trim();
|
||||
|
||||
// [수정] 선택된 공개 범위(visibility) 값을 읽어오는 코드 추가
|
||||
const visibility = document.querySelector('input[name="visibility"]:checked').value;
|
||||
|
||||
const bookmarkData = {
|
||||
url: urlInput.value.trim(),
|
||||
// title, description 등은 ogData에서 가져오는 것은 그대로 유지
|
||||
title: ogData.title,
|
||||
description: ogData.description,
|
||||
thumbnailUrl: ogData.thumbnailUrl,
|
||||
userComment: comment,
|
||||
// [수정] bookmarkData 객체에 visibility 프로퍼티 추가
|
||||
visibility: visibility
|
||||
};
|
||||
|
||||
if (!bookmarkData.url) {
|
||||
alert('URL을 입력해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
// 서버에 북마크 저장 요청 (이하 코드는 동일)
|
||||
const response = await fetch('/user/bookmarks/save', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
[csrfHeader]: csrfToken
|
||||
},
|
||||
body: JSON.stringify(bookmarkData)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
alert('페이지가 저장되었습니다.');
|
||||
location.reload();
|
||||
} else {
|
||||
alert('저장에 실패했습니다.');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</th:block>
|
||||
</html>
|
||||
@@ -5,14 +5,7 @@
|
||||
layout:decorate="~{layout/default_layout}">
|
||||
|
||||
<th:block layout:fragment="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" />
|
||||
<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>
|
||||
<script>document.addEventListener('DOMContentLoaded', function() {
|
||||
initEditor(false)
|
||||
fetchComments(serverData.id);
|
||||
});</script>
|
||||
|
||||
</th:block>
|
||||
|
||||
<th:block layout:fragment="content" id="content">
|
||||
@@ -108,5 +101,14 @@
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<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>
|
||||
<script>document.addEventListener('DOMContentLoaded', function() {
|
||||
initEditor(false)
|
||||
fetchComments(serverData.id);
|
||||
});</script>
|
||||
</th:block>
|
||||
</html>
|
||||
Reference in New Issue
Block a user