....
This commit is contained in:
@@ -103,4 +103,5 @@ spring.webflux.response-timeout=60s
|
||||
api.base-url=ss
|
||||
build.config.run=local
|
||||
jwt.secret=your-very-long-and-super-secret-key-for-jwt-that-is-at-least-64-characters-long
|
||||
jwt.expiration=86400000
|
||||
jwt.expiration=86400000
|
||||
logging.level.org.springframework.security=DEBUG
|
||||
@@ -103,4 +103,5 @@ spring.webflux.response-timeout=60s
|
||||
api.base-url=ss
|
||||
build.config.run=prd
|
||||
jwt.secret=your-very-long-and-super-secret-key-for-jwt-that-is-at-least-64-characters-long
|
||||
jwt.expiration=86400000
|
||||
jwt.expiration=86400000
|
||||
logging.level.org.springframework.security=DEBUG
|
||||
@@ -104,4 +104,5 @@ api.base-url=ss
|
||||
|
||||
build.config.run=local
|
||||
jwt.secret=your-very-long-and-super-secret-key-for-jwt-that-is-at-least-64-characters-long
|
||||
jwt.expiration=86400000
|
||||
jwt.expiration=86400000
|
||||
logging.level.org.springframework.security=DEBUG
|
||||
@@ -1777,4 +1777,135 @@ function openBookmarkInIframe(url, title) {
|
||||
// 팝업과 오버레이 표시
|
||||
overlay.style.display = 'block';
|
||||
popup.style.display = 'block';
|
||||
}
|
||||
|
||||
// 팝업과 폼 필드를 연결하기 위한 전역 변수
|
||||
let bookmarkPopupTargets = {
|
||||
displayId: null,
|
||||
inputId: null
|
||||
};
|
||||
let stagedBookmarkCategory = '';
|
||||
let stagedBookmarkTags = [];
|
||||
|
||||
/**
|
||||
* 북마크 카테고리 팝업을 여는 함수
|
||||
* @param {string} displayId - 선택된 카테고리를 보여줄 div의 ID
|
||||
* @param {string} inputId - 실제 값을 저장할 hidden input의 ID
|
||||
*/
|
||||
async function openBookmarkCategoryPopup(displayId, inputId) {
|
||||
bookmarkPopupTargets = { displayId, inputId }; // 현재 작업 대상 필드를 저장
|
||||
|
||||
const currentCategory = document.getElementById(inputId).value;
|
||||
stagedBookmarkCategory = currentCategory || '';
|
||||
renderStagedBookmarkCategory();
|
||||
|
||||
// 기존 카테고리 목록 불러오기
|
||||
const listEl = document.getElementById('bookmark-category-list');
|
||||
listEl.innerHTML = '로딩...';
|
||||
try {
|
||||
const response = await fetch('/api/bookmarks/categories',{
|
||||
headers: {
|
||||
'Authorization': `Bearer ${serverData.token}` // 헤더에 토큰 추가
|
||||
},
|
||||
});
|
||||
const categories = await response.json();
|
||||
listEl.innerHTML = '';
|
||||
categories.forEach(cat => {
|
||||
const tagEl = document.createElement('span');
|
||||
tagEl.className = 'tag-item';
|
||||
tagEl.textContent = cat;
|
||||
tagEl.onclick = () => {
|
||||
stagedBookmarkCategory = cat;
|
||||
renderStagedBookmarkCategory();
|
||||
};
|
||||
listEl.appendChild(tagEl);
|
||||
});
|
||||
} catch (e) {
|
||||
listEl.innerHTML = '카테고리를 불러오는데 실패했습니다.';
|
||||
}
|
||||
|
||||
const dummyEl = document.createElement('div');
|
||||
dummyEl.setAttribute('to', '#bookmark-category-popup');
|
||||
openPopup(dummyEl);
|
||||
}
|
||||
document.getElementById('new-bookmark-category-input')?.addEventListener('keyup', e => {
|
||||
if (e.key === 'Enter') {
|
||||
stagedBookmarkCategory = e.target.value.trim();
|
||||
renderStagedBookmarkCategory();
|
||||
e.target.value = '';
|
||||
}
|
||||
});
|
||||
function renderStagedBookmarkCategory() {
|
||||
const area = document.getElementById('selected-bookmark-category-area');
|
||||
area.innerHTML = stagedBookmarkCategory ? `<span class="tag-item">${stagedBookmarkCategory} <span class="remove-tag" onclick="stagedBookmarkCategory=''; renderStagedBookmarkCategory();">X</span></span>` : '<i>선택된 카테고리 없음</i>';
|
||||
}
|
||||
function applyBookmarkCategory() {
|
||||
document.getElementById(bookmarkPopupTargets.inputId).value = stagedBookmarkCategory;
|
||||
document.getElementById(bookmarkPopupTargets.displayId).innerHTML = stagedBookmarkCategory ? `<span class="tag-item">${stagedBookmarkCategory}</span>` : '카테고리 선택';
|
||||
closePopup();
|
||||
}
|
||||
|
||||
/**
|
||||
* 북마크 태그 팝업을 여는 함수
|
||||
* @param {string} displayId - 선택된 태그를 보여줄 div의 ID
|
||||
* @param {string} inputId - 실제 값을 저장할 hidden input의 ID
|
||||
*/
|
||||
async function openBookmarkTagPopup(displayId, inputId) {
|
||||
bookmarkPopupTargets = { displayId, inputId };
|
||||
|
||||
const currentTags = document.getElementById(inputId).value;
|
||||
stagedBookmarkTags = currentTags ? currentTags.split(',').map(t => t.trim()) : [];
|
||||
renderStagedBookmarkTags();
|
||||
|
||||
// 기존 태그 목록 불러오기
|
||||
const listEl = document.getElementById('bookmark-tag-list');
|
||||
listEl.innerHTML = '로딩...';
|
||||
try {
|
||||
const response = await fetch('/api/bookmarks/tags',{
|
||||
headers: {
|
||||
'Authorization': `Bearer ${serverData.token}` // 헤더에 토큰 추가
|
||||
},
|
||||
});
|
||||
const tags = await response.json();
|
||||
listEl.innerHTML = '';
|
||||
tags.forEach(tag => {
|
||||
const tagEl = document.createElement('span');
|
||||
tagEl.className = 'tag-item';
|
||||
tagEl.textContent = '#' + tag;
|
||||
tagEl.onclick = () => addStagedBookmarkTag(tag);
|
||||
listEl.appendChild(tagEl);
|
||||
});
|
||||
} catch (e) {
|
||||
listEl.innerHTML = '태그를 불러오는데 실패했습니다.';
|
||||
}
|
||||
|
||||
const dummyEl = document.createElement('div');
|
||||
dummyEl.setAttribute('to', '#bookmark-tag-popup');
|
||||
openPopup(dummyEl);
|
||||
}
|
||||
document.getElementById('new-bookmark-tag-input')?.addEventListener('keyup', e => {
|
||||
if (e.key === 'Enter') {
|
||||
addStagedBookmarkTag(e.target.value.trim());
|
||||
e.target.value = '';
|
||||
}
|
||||
});
|
||||
function addStagedBookmarkTag(tag) {
|
||||
if (tag && !stagedBookmarkTags.includes(tag)) {
|
||||
stagedBookmarkTags.push(tag);
|
||||
renderStagedBookmarkTags();
|
||||
}
|
||||
}
|
||||
function removeStagedBookmarkTag(index) {
|
||||
stagedBookmarkTags.splice(index, 1);
|
||||
renderStagedBookmarkTags();
|
||||
}
|
||||
function renderStagedBookmarkTags() {
|
||||
const area = document.getElementById('selected-bookmark-tags-area');
|
||||
area.innerHTML = stagedBookmarkTags.map((tag, i) => `<span class="tag-item">#${tag} <span class="remove-tag" onclick="removeStagedBookmarkTag(${i})">X</span></span>`).join(' ') || '<i>선택된 태그 없음</i>';
|
||||
}
|
||||
function applyBookmarkTags() {
|
||||
const tagsString = stagedBookmarkTags.join(',');
|
||||
document.getElementById(bookmarkPopupTargets.inputId).value = tagsString;
|
||||
document.getElementById(bookmarkPopupTargets.displayId).innerHTML = stagedBookmarkTags.map(tag => `<span class="tag-item">#${tag}</span>`).join(' ') || '태그 선택';
|
||||
closePopup();
|
||||
}
|
||||
@@ -2,9 +2,35 @@
|
||||
<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}">
|
||||
<head>
|
||||
<title>Bookmarks</title>
|
||||
<style>
|
||||
.scrollable-content {
|
||||
max-height: 500px; /* 콘텐츠 영역의 최대 높이를 지정 */
|
||||
max-width: 500px;
|
||||
overflow-y: auto; /* 세로 내용이 넘칠 경우 스크롤바 자동 생성 */
|
||||
-webkit-overflow-scrolling: touch; /* 모바일에서 부드러운 스크롤 효과 */
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" href="https://unpkg.com/swiper/swiper-bundle.min.css" />
|
||||
<script src="https://unpkg.com/swiper/swiper-bundle.min.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const swiper = new Swiper('.bookmark-swiper', {
|
||||
loop: false,
|
||||
pagination: {
|
||||
el: '.swiper-pagination',
|
||||
clickable: true,
|
||||
},
|
||||
navigation: {
|
||||
nextEl: '.swiper-button-next',
|
||||
prevEl: '.swiper-button-prev',
|
||||
},
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<th:block layout:fragment="content">
|
||||
<section class="wrapper style2">
|
||||
@@ -12,71 +38,103 @@
|
||||
<header class="major">
|
||||
<h2>Bookmarks</h2>
|
||||
<p>다른 사용자들이 저장한 유용한 페이지들을 둘러보세요.</p>
|
||||
<div class="filter-controls" style="margin-bottom: 2em; text-align: center;">
|
||||
<div style="margin-bottom: 1em;">
|
||||
<strong>카테고리:</strong>
|
||||
<a th:href="@{/bookmarks}" th:classappend="${currentCategory == null && currentTag == null} ? 'button small' : 'button alt small'">전체</a>
|
||||
<a th:each="cat : ${allCategories}"
|
||||
th:href="@{/bookmarks(category=${cat})}"
|
||||
th:text="${cat}"
|
||||
th:classappend="${currentCategory == cat} ? 'button small' : 'button alt small'"></a>
|
||||
</div>
|
||||
<div>
|
||||
<strong>태그:</strong>
|
||||
<a th:each="tg : ${allTags}"
|
||||
th:href="@{/bookmarks(tag=${tg})}"
|
||||
th:text="'#' + ${tg}"
|
||||
th:classappend="${currentTag == tg} ? 'button small' : 'button alt small'"></a>
|
||||
</div>
|
||||
</div>
|
||||
</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="swiper bookmark-swiper">
|
||||
<div class="swiper-wrapper">
|
||||
<div class="swiper-slide" th:each="bookmark : ${bookmarksPage.content}">
|
||||
<section class="box feature" style="margin: 0; height: 100%; display: flex; flex-direction: column;">
|
||||
|
||||
<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 th:switch="${bookmark.bookmarkType}">
|
||||
<div th:case="'IMAGE'" class="image-flick-container scrollable-content">
|
||||
<img th:each="imageUrl : ${bookmark.contentUrls}" th:src="${apiBaseUrl + imageUrl}" alt="Bookmark Image" />
|
||||
</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 th:case="'VIDEO'" class="video-container" th:if="${!#lists.isEmpty(bookmark.contentUrls)}">
|
||||
<video controls style="width: 100%;">
|
||||
<source th:src="${apiBaseUrl + bookmark.contentUrls[0]}" type="video/mp4">
|
||||
</video>
|
||||
</div>
|
||||
<a th:case="'URL'"
|
||||
href="javascript:void(0);"
|
||||
th:data-url="${bookmark.url}"
|
||||
th:data-title="${bookmark.title}"
|
||||
onclick="showBookmarkOptions(this)" class="image featured scrollable-content">
|
||||
<img th:each="imageUrl : ${bookmark.contentUrls}" th:src="${apiBaseUrl + imageUrl}" alt="Bookmark Image" />
|
||||
</a>
|
||||
</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 class="inner" style="flex-grow: 1; display: flex; flex-direction: column; justify-content: space-between;">
|
||||
<div>
|
||||
<header>
|
||||
<h3 th:text="${bookmark.title}">북마크 제목</h3>
|
||||
<p th:if="${bookmark.userComment}" th:text="${bookmark.userComment}"></p>
|
||||
</header>
|
||||
<p th:text="${#strings.abbreviate(bookmark.description, 100)}"></p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="vote-controls" style="margin-top: 1em; text-align: center;" th:data-bookmark-id="${bookmark.id}">
|
||||
<button class="button small" onclick="handleBookmarkVote(this, 'like')">
|
||||
👍 Like (<span class="like-count" th:text="${bookmark.voteCount}">0</span>)
|
||||
</button>
|
||||
<button class="button small" onclick="handleBookmarkVote(this, 'unlike')">
|
||||
👎 Unlike (<span class="unlike-count" th:text="${bookmark.unlikeCount}">0</span>)
|
||||
</button>
|
||||
<button class="button alt small" th:onclick="toggleCommentSection([[${bookmark.id}]])">
|
||||
💬 Comments
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<section class="comment-section" th:id="|comment-section-${bookmark.id}|" style="display: none; margin-top: 1em; text-align: left;">
|
||||
|
||||
<th:block sec:authorize="isAuthenticated()">
|
||||
<div class="comment-form-container">
|
||||
<textarea th:id="|comment-input-${bookmark.id}|" placeholder="댓글을 입력하세요..." style="width: 100%;"></textarea>
|
||||
<button th:onclick="submitBookmarkComment([[${bookmark.id}]])" class="button small" style="margin-top: 0.5em;">등록</button>
|
||||
</div>
|
||||
</th:block>
|
||||
|
||||
<div sec:authorize="isAnonymous()" style="padding: 1em; text-align: center; border: 1px dashed #ccc; margin-bottom: 1em;">
|
||||
<p style="margin:0;">댓글을 작성하려면 <a th:href="@{/home.bs(action='login')}">로그인</a>이 필요합니다.</p>
|
||||
</div>
|
||||
|
||||
<div th:id="|comments-list-${bookmark.id}|" style="margin-top: 1em;">
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
<div class="swiper-pagination"></div>
|
||||
<div class="swiper-button-prev"></div>
|
||||
<div class="swiper-button-next"></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 th:if="${bookmarksPage.empty}">
|
||||
<p style="text-align: center;">아직 저장된 페이지가 없습니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</th:block>
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<!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" style="text-align: center; padding: 4em 0;">
|
||||
<header class="major">
|
||||
<h2 th:text="|오류가 발생했습니다 (${statusCode})|">오류가 발생했습니다</h2>
|
||||
<p th:text="${errorMessage}" style="font-size: 1.5em; color: #e85a4f;">오류 메시지</p>
|
||||
</header>
|
||||
|
||||
<div class="box" style="max-width: 600px; margin: 2em auto; text-align: left;">
|
||||
<p th:text="${errorDescription}">
|
||||
오류에 대한 상세 설명입니다. 이 페이지는 접근이 금지되었거나, 요청 처리 중 문제가 발생했을 수 있습니다.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<a th:href="@{/}" class="button primary">홈으로 돌아가기</a>
|
||||
</div>
|
||||
</section>
|
||||
</th:block>
|
||||
|
||||
</html>
|
||||
@@ -155,6 +155,17 @@
|
||||
<label for="visibility-public" class="custom-label">전체 공개</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-control-wrapper" onclick="openBookmarkCategoryPopup('new-bookmark-category-display', 'new-bookmark-category')">
|
||||
<strong>카테고리</strong>
|
||||
<div id="new-bookmark-category-display" class="tag-display-box">카테고리 선택</div>
|
||||
</div>
|
||||
<input type="hidden" id="new-bookmark-category">
|
||||
|
||||
<div class="form-control-wrapper" onclick="openBookmarkTagPopup('new-bookmark-tags-display', 'new-bookmark-tags')">
|
||||
<strong>태그</strong>
|
||||
<div id="new-bookmark-tags-display" class="tag-display-box">태그 선택</div>
|
||||
</div>
|
||||
<input type="hidden" id="new-bookmark-tags">
|
||||
<button id="save-bookmark-btn" class="button primary" style="margin-top: 1em;">저장하기</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -162,15 +173,26 @@
|
||||
<div class="box" style="margin-top: 2em;">
|
||||
<h4>저장된 목록</h4>
|
||||
<div id="bookmarks-list" class="row">
|
||||
<div class="col-4 col-12-medium">
|
||||
<div class="col-12" th:if="${#lists.isEmpty(myBookmarks)}">
|
||||
<p style="text-align: center; padding: 2em 0;">저장한 페이지가 없습니다.</p>
|
||||
</div>
|
||||
|
||||
<div class="col-4 col-12-medium" th:each="bookmark : ${myBookmarks}" th:id="|bookmark-row-${bookmark.id}|">
|
||||
<section class="box feature">
|
||||
<a href="#" class="image featured"><img src="/images/pic01.jpg" alt="" /></a>
|
||||
<a th:href="${bookmark.url}" target="_blank" class="image featured">
|
||||
<img th:src="${apiBaseUrl + bookmark.displayImageUrl}" alt="Bookmark Thumbnail" />
|
||||
</a>
|
||||
<div class="inner">
|
||||
<header>
|
||||
<h2>카드 제목</h2>
|
||||
<p>사용자 코멘트가 여기에 들어갑니다.</p>
|
||||
<h2 th:text="${bookmark.title ?: '제목 없음'}">카드 제목</h2>
|
||||
<p th:if="${!#strings.isEmpty(bookmark.userComment)}" th:text="${bookmark.userComment}">사용자 코멘트</p>
|
||||
</header>
|
||||
<p style="font-size: 0.8em; color: #888;">원본 페이지 설명...</p>
|
||||
<p style="font-size: 0.8em; color: #888;" th:text="${#strings.abbreviate(bookmark.description, 100)}">원본 페이지 설명...</p>
|
||||
|
||||
<div class="actions" style="margin-top: 1em; text-align: right;">
|
||||
<button class="button small" th:onclick="openEditBookmarkModal([[${bookmark.id}]])">수정</button>
|
||||
<button class="button small alt" th:onclick="deleteBookmark([[${bookmark.id}]])">삭제</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
@@ -497,6 +519,8 @@
|
||||
|
||||
// [수정] 선택된 공개 범위(visibility) 값을 읽어오는 코드 추가
|
||||
const visibility = document.querySelector('input[name="visibility"]:checked').value;
|
||||
const category = document.getElementById('new-bookmark-category').value;
|
||||
const tags = document.getElementById('new-bookmark-tags').value;
|
||||
|
||||
const bookmarkData = {
|
||||
url: urlInput.value.trim(),
|
||||
@@ -506,7 +530,9 @@
|
||||
thumbnailUrl: ogData.thumbnailUrl,
|
||||
userComment: comment,
|
||||
// [수정] bookmarkData 객체에 visibility 프로퍼티 추가
|
||||
visibility: visibility
|
||||
visibility: visibility,
|
||||
category: category,
|
||||
tags: tags
|
||||
};
|
||||
|
||||
if (!bookmarkData.url) {
|
||||
@@ -532,6 +558,119 @@
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* [수정] 북마크 수정 팝업을 열고 데이터를 채우는 함수
|
||||
*/
|
||||
async function openEditBookmarkModal(bookmarkId) {
|
||||
try {
|
||||
// /api/** 경로는 JWT 인증 헤더(Authorization)가 필요합니다.
|
||||
// 이는 전역 fetch 인터셉터 등에서 처리된다고 가정합니다.
|
||||
const response = await fetch(`/api/bookmarks/${bookmarkId}`,{
|
||||
headers: {
|
||||
'Authorization': `Bearer ${serverData.token}` // 헤더에 토큰 추가
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`서버 응답: ${response.status}`);
|
||||
}
|
||||
const bookmark = await response.json();
|
||||
|
||||
// 이 부분은 모달 요소가 없어서 실패했었습니다.
|
||||
// 이제 default_layout.html에 요소가 추가되었습니다.
|
||||
document.getElementById('edit-bookmark-id').value = bookmark.id;
|
||||
document.getElementById('edit-bookmark-title').value = bookmark.title || '';
|
||||
document.getElementById('edit-bookmark-comment').value = bookmark.userComment || '';
|
||||
document.getElementById('edit-bookmark-visibility').value = bookmark.visibility || 'PRIVATE';
|
||||
|
||||
document.getElementById('edit-bookmark-category-display').innerHTML = bookmark.category ? `<span class="tag-item">${bookmark.category}</span>` : '카테고리 선택';
|
||||
document.getElementById('edit-bookmark-category').value = bookmark.category || '';
|
||||
|
||||
document.getElementById('edit-bookmark-tags-display').innerHTML = (bookmark.tags || []).map(t => `<span class="tag-item">#${t}</span>`).join(' ') || '태그 선택';
|
||||
document.getElementById('edit-bookmark-tags').value = (bookmark.tags || []).join(',');
|
||||
|
||||
// 공통 openPopup 함수 사용
|
||||
const dummyEl = document.createElement('div');
|
||||
dummyEl.setAttribute('to', '#bookmark-edit-popup');
|
||||
openPopup(dummyEl);
|
||||
|
||||
} catch(error) {
|
||||
console.error("수정 모달 열기 실패:", error);
|
||||
showAlert('오류', '북마크 정보를 불러오는 데 실패했습니다. 로그인 상태를 확인해주세요.', 'error');
|
||||
}
|
||||
}
|
||||
/**
|
||||
* [수정] 북마크 수정 내용을 서버에 제출하는 함수
|
||||
*/
|
||||
async function submitBookmarkUpdate() {
|
||||
const bookmarkId = document.getElementById('edit-bookmark-id').value;
|
||||
const tagsValue = document.getElementById('edit-bookmark-tags').value;
|
||||
|
||||
const updatedData = {
|
||||
title: document.getElementById('edit-bookmark-title').value,
|
||||
userComment: document.getElementById('edit-bookmark-comment').value,
|
||||
visibility: document.getElementById('edit-bookmark-visibility').value,
|
||||
category: document.getElementById('edit-bookmark-category').value,
|
||||
tags: tagsValue ? tagsValue.split(',').map(t => t.trim()).filter(t => t) : []
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/bookmarks/${bookmarkId}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${serverData.token}` // 헤더에 토큰 추가
|
||||
},
|
||||
body: JSON.stringify(updatedData)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
showAlert('성공', '북마크 정보가 수정되었습니다.', 'success');
|
||||
location.reload(); // 변경 사항을 확인하기 위해 페이지 새로고침
|
||||
} else {
|
||||
const errorData = await response.text();
|
||||
showAlert('오류', `수정에 실패했습니다: ${errorData}`, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating bookmark:', error);
|
||||
showAlert('오류', '네트워크 오류로 수정에 실패했습니다.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* [신규] 북마크를 삭제하는 함수.
|
||||
* 'deleteBookmark is not defined' 오류를 해결합니다.
|
||||
*/
|
||||
async function deleteBookmark(bookmarkId) {
|
||||
// common.js의 공통 확인 모달 사용
|
||||
const confirmed = await showConfirm('삭제 확인', '이 북마크를 정말로 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.');
|
||||
if (confirmed) {
|
||||
try {
|
||||
// /api/** 경로는 상태가 없는(stateless) JWT 인증을 사용하므로 CSRF 토큰이 필요 없습니다.
|
||||
const response = await fetch(`/api/bookmarks/${bookmarkId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${serverData.token}` // 헤더에 토큰 추가
|
||||
},
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
showAlert('성공', '북마크가 삭제되었습니다.', 'success');
|
||||
// 페이지에서 삭제된 항목 제거
|
||||
document.getElementById(`bookmark-row-${bookmarkId}`).remove();
|
||||
} else {
|
||||
const errorData = await response.text();
|
||||
showAlert('오류', `삭제에 실패했습니다: ${errorData}`, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting bookmark:', error);
|
||||
showAlert('오류', '네트워크 오류로 삭제에 실패했습니다.', 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
</th:block>
|
||||
</html>
|
||||
@@ -55,7 +55,9 @@
|
||||
unlikeCount: [[${srcPost?.unlikeCount ?: 0}]],
|
||||
// --- Page-specific (not model data) ---
|
||||
enc: /*[[${enc ?: ''}]]*/,
|
||||
keyword: /*[[${keyword ?: ''}]]*/
|
||||
keyword: /*[[${keyword ?: ''}]]*/,
|
||||
// --- [핵심 추가] ---
|
||||
token: /*[[${jwtToken}]]*/
|
||||
};
|
||||
</script>
|
||||
</th:block>
|
||||
|
||||
@@ -73,6 +73,78 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="bookmark-edit-popup" class="pop_layer">
|
||||
<div class="pop_container">
|
||||
<div class="pop_conts">
|
||||
<h2>북마크 수정</h2>
|
||||
<input type="hidden" id="edit-bookmark-id">
|
||||
|
||||
<label for="edit-bookmark-title">제목</label>
|
||||
<input type="text" id="edit-bookmark-title" placeholder="페이지 제목">
|
||||
|
||||
<label for="edit-bookmark-comment">내 코멘트</label>
|
||||
<textarea id="edit-bookmark-comment" placeholder="나의 생각 (선택)" rows="3"></textarea>
|
||||
|
||||
<label for="edit-bookmark-visibility">공개 범위</label>
|
||||
<select id="edit-bookmark-visibility" style="width: 100%; padding: 0.5em; border-radius: 4px; border: 1px solid #ddd;">
|
||||
<option value="PRIVATE">비공개</option>
|
||||
<option value="MEMBERS">회원 공개</option>
|
||||
<option value="PUBLIC">전체 공개</option>
|
||||
</select>
|
||||
|
||||
<div class="form-control-wrapper" onclick="openBookmarkCategoryPopup('edit-bookmark-category-display', 'edit-bookmark-category')">
|
||||
<strong>카테고리</strong>
|
||||
<div id="edit-bookmark-category-display" class="tag-display-box">카테고리 선택</div>
|
||||
</div>
|
||||
<input type="hidden" id="edit-bookmark-category">
|
||||
|
||||
<div class="form-control-wrapper" onclick="openBookmarkTagPopup('edit-bookmark-tags-display', 'edit-bookmark-tags')">
|
||||
<strong>태그</strong>
|
||||
<div id="edit-bookmark-tags-display" class="tag-display-box">태그 선택</div>
|
||||
</div>
|
||||
<input type="hidden" id="edit-bookmark-tags">
|
||||
|
||||
<div style="margin-top: 1.5em; text-align: right;">
|
||||
<button type="button" class="button primary" onclick="submitBookmarkUpdate()">변경사항 저장</button>
|
||||
<a href="#" class="button alt btn_layerClose">취소</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="bookmark-category-popup" class="pop_layer">
|
||||
<div class="pop_container">
|
||||
<div class="pop_conts">
|
||||
<h2>카테고리 선택</h2>
|
||||
<div id="selected-bookmark-category-area" class="selected-items-area"></div>
|
||||
<hr>
|
||||
<div id="bookmark-category-list" class="tag-list"></div>
|
||||
<input type="text" id="new-bookmark-category-input" placeholder="새 카테고리 입력 후 Enter">
|
||||
<div style="margin-top: 1.5em;">
|
||||
<button type="button" class="button primary" onclick="applyBookmarkCategory()">적용</button>
|
||||
<a href="#" class="button alt btn_layerClose">취소</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="bookmark-tag-popup" class="pop_layer">
|
||||
<div class="pop_container">
|
||||
<div class="pop_conts">
|
||||
<h2>태그 선택</h2>
|
||||
<div id="selected-bookmark-tags-area" class="selected-items-area"></div>
|
||||
<hr>
|
||||
<div id="bookmark-tag-list" class="tag-list"></div>
|
||||
<input type="text" id="new-bookmark-tag-input" placeholder="새 태그 입력 후 Enter">
|
||||
<div style="margin-top: 1.5em;">
|
||||
<button type="button" class="button primary" onclick="applyBookmarkTags()">적용</button>
|
||||
<a href="#" class="button alt btn_layerClose">취소</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="iframe-viewer-popup" class="pop_layer" style="width: 90%; height: 90%; max-width: 1400px;">
|
||||
<div class="pop_container" style="height: 100%; display: flex; flex-direction: column;">
|
||||
<div class="pop_header" style="display: flex; justify-content: space-between; align-items: center; padding: 10px 20px; border-bottom: 1px solid #eee; background: #f8f8f8;">
|
||||
|
||||
Reference in New Issue
Block a user