..
This commit is contained in:
parent
d6043543a1
commit
0ce20e4bf1
@ -40,7 +40,7 @@ class AppConfig : WebMvcConfigurer {
|
||||
"/bums/where.bs",
|
||||
"/user/info", // "내 정보" 페이지도 추가하면 좋습니다.
|
||||
"/tlg/repotToMe.bjx",
|
||||
"/user/login.bs", "/user/signup.bs", "/user/login.bjx",
|
||||
"/user/login.bs", "/user/signup.bs", "/user/login.bjx","/bookmarks/**",
|
||||
"/blog/viewer/**", "/blog/posts", "/blog/rankOfViews.bjx", "/blog/recentOfPost.bjx"
|
||||
)
|
||||
}
|
||||
|
||||
@ -889,19 +889,18 @@ class BookmarkController(private val bookmarkService: WebBookmarkService,
|
||||
// [수정] 서비스 호출 시 필터 파라미터 전달
|
||||
val bookmarksPage = bookmarkService.getVisibleBookmarks(userDetails, pageable, category, tag).awaitSingle()
|
||||
|
||||
vm.modelMap["bookmarksPage"] = bookmarksPage.map {
|
||||
|
||||
it.contentUrls = arrayListOf<String>().apply {
|
||||
if (it.thumbnailUrl.isNullOrEmpty() == false) {
|
||||
add(it.thumbnailUrl!!)
|
||||
}
|
||||
if (it.userSelectedImageUrl.isNullOrEmpty() == false) {
|
||||
add(it.userSelectedImageUrl!!)
|
||||
}
|
||||
addAll(it.contentUrls)
|
||||
// [수정] Page 객체의 내용을 변환하는 로직 추가
|
||||
val processedBookmarksPage = bookmarksPage.map { bookmark ->
|
||||
// 'images' 필드가 비어있고 구 버전 'contentUrls'에 데이터가 있다면 변환
|
||||
if (bookmark.images.isEmpty() && bookmark.contentUrls.isNotEmpty()) {
|
||||
bookmark.copy(
|
||||
images = bookmark.contentUrls.map { url -> BookmarkImage(url = url, isVisible = true) }
|
||||
)
|
||||
} else {
|
||||
bookmark
|
||||
}
|
||||
it
|
||||
}
|
||||
vm.modelMap["bookmarksPage"] = processedBookmarksPage
|
||||
|
||||
// [추가] 뷰에서 사용할 필터 목록과 현재 선택된 필터 전달
|
||||
vm.modelMap["allCategories"] = bookmarkService.findAllDistinctCategories().collectList().awaitSingle()
|
||||
@ -972,7 +971,20 @@ class BookmarkController(private val bookmarkService: WebBookmarkService,
|
||||
pageable: Pageable
|
||||
): ResponseEntity<Page<WebBookmark>> {
|
||||
val bookmarksPage = bookmarkService.getVisibleBookmarks(userDetails, pageable,null,null).awaitSingle()
|
||||
return ResponseEntity.ok(bookmarksPage)
|
||||
// val bookmarksPage = bookmarkService.getVisibleBookmarks(userDetails, pageable, category, tag).awaitSingle()
|
||||
|
||||
// [수정] Page 객체의 내용을 변환하는 로직 추가
|
||||
val processedBookmarksPage = bookmarksPage.map { bookmark ->
|
||||
// 'images' 필드가 비어있고 구 버전 'contentUrls'에 데이터가 있다면 변환
|
||||
if (bookmark.images.isEmpty() && bookmark.contentUrls.isNotEmpty()) {
|
||||
bookmark.copy(
|
||||
images = bookmark.contentUrls.map { url -> BookmarkImage(url = url, isVisible = true) }
|
||||
)
|
||||
} else {
|
||||
bookmark
|
||||
}
|
||||
}
|
||||
return ResponseEntity.ok(processedBookmarksPage)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1041,7 +1053,17 @@ class BookmarkApiController(
|
||||
): ResponseEntity<Page<WebBookmark>> {
|
||||
// 기존 서비스 메서드를 그대로 사용하여 사용자 권한에 맞는 북마크 목록을 가져옴
|
||||
val bookmarksPage = bookmarkService.getVisibleBookmarks(userDetails, pageable,null,null).awaitSingle()
|
||||
return ResponseEntity.ok(bookmarksPage)
|
||||
val processedBookmarksPage = bookmarksPage.map { bookmark ->
|
||||
// 'images' 필드가 비어있고 구 버전 'contentUrls'에 데이터가 있다면 변환
|
||||
if (bookmark.images.isEmpty() && bookmark.contentUrls.isNotEmpty()) {
|
||||
bookmark.copy(
|
||||
images = bookmark.contentUrls.map { url -> BookmarkImage(url = url, isVisible = true) }
|
||||
)
|
||||
} else {
|
||||
bookmark
|
||||
}
|
||||
}
|
||||
return ResponseEntity.ok(processedBookmarksPage)
|
||||
}
|
||||
|
||||
data class BookmarkDataDto(
|
||||
@ -1225,7 +1247,7 @@ class BookmarkApiController(
|
||||
@PathVariable id: String,
|
||||
@RequestBody request: BookmarkUpdateRequest,
|
||||
@AuthenticationPrincipal userDetails: UserDetails?
|
||||
): ResponseEntity<*> { // [수정] 반환 타입 변경
|
||||
): ResponseEntity<*> {
|
||||
logService.log("북마크 업데이트 요청: ID=$id, 사용자=${userDetails?.username}")
|
||||
|
||||
// 1. 사용자 인증 정보 확인
|
||||
@ -1247,12 +1269,14 @@ class BookmarkApiController(
|
||||
.body(mapOf("message" to "이 북마크를 수정할 권한이 없습니다."))
|
||||
}
|
||||
|
||||
// 4. 업데이트 실행
|
||||
// 4. [수정됨] 안전한 업데이트 실행
|
||||
val updatedBookmark = existingBookmark.copy(
|
||||
title = request.title ?: existingBookmark.title,
|
||||
userComment = request.userComment,
|
||||
// 요청된 값이 null이 아닐 경우에만 업데이트하도록 변경
|
||||
userComment = request.userComment ?: existingBookmark.userComment,
|
||||
visibility = request.visibility ?: existingBookmark.visibility,
|
||||
category = request.category,
|
||||
// 요청된 값이 null이 아닐 경우에만 업데이트하도록 변경
|
||||
category = request.category ?: existingBookmark.category,
|
||||
tags = request.tags ?: existingBookmark.tags
|
||||
)
|
||||
|
||||
@ -1267,6 +1291,134 @@ class BookmarkApiController(
|
||||
}
|
||||
}
|
||||
|
||||
// [이 함수를 BookmarkApiController 내부에 추가]
|
||||
@PostMapping("/{id}/images", consumes = [MediaType.MULTIPART_FORM_DATA_VALUE])
|
||||
suspend fun addImagesToBookmark(
|
||||
@PathVariable id: String,
|
||||
@RequestPart("files") files: List<MultipartFile>,
|
||||
@AuthenticationPrincipal userDetails: UserDetails?
|
||||
): ResponseEntity<*> {
|
||||
if (userDetails == null || uploadPath.isNullOrBlank()) {
|
||||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build<Unit>()
|
||||
}
|
||||
|
||||
var bookmark = bookmarkService.findById(id).awaitSingleOrNull()
|
||||
?: return ResponseEntity.notFound().build<Unit>()
|
||||
|
||||
if (bookmark.userId != userDetails.username) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).build<Unit>()
|
||||
}
|
||||
|
||||
// [신규] 기존 contentUrls를 새로운 images 형식으로 마이그레이션하는 로직
|
||||
if (bookmark.images.isEmpty() && bookmark.contentUrls.isNotEmpty()) {
|
||||
bookmark = bookmark.copy(
|
||||
images = bookmark.contentUrls.map { BookmarkImage(url = it, isVisible = true) },
|
||||
contentUrls = emptyList() // 이전 필드는 비워줍니다.
|
||||
)
|
||||
}
|
||||
|
||||
val newImages = files.mapNotNull { file ->
|
||||
val uniqueFilename = "${UUID.randomUUID()}_${file.originalFilename}"
|
||||
val targetPath = Paths.get(uploadPath, uniqueFilename)
|
||||
try {
|
||||
file.transferTo(targetPath.toFile())
|
||||
// 새로운 BookmarkImage 객체로 생성
|
||||
BookmarkImage(url = "/api/images/$uniqueFilename", isVisible = true)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
val updatedBookmark = bookmark.copy(
|
||||
images = bookmark.images + newImages
|
||||
)
|
||||
|
||||
val saved = bookmarkService.saveBookmark(updatedBookmark).awaitSingle()
|
||||
return ResponseEntity.ok(saved)
|
||||
}
|
||||
|
||||
data class ImageUrlRequest(val imageUrl: String)
|
||||
// [신규] 이미지 '보임/숨김' 상태 변경을 위한 DTO
|
||||
data class ImageVisibilityRequest(val imageUrl: String)
|
||||
|
||||
|
||||
// [이 함수를 BookmarkApiController 내부에 추가 또는 교체]
|
||||
// 기존 DELETE에서 PUT으로 변경하고, 로직을 '숨김' 처리로 변경합니다.
|
||||
@PutMapping("/{id}/images/visibility")
|
||||
suspend fun updateImageVisibility(
|
||||
@PathVariable id: String,
|
||||
@RequestBody request: ImageVisibilityRequest,
|
||||
@AuthenticationPrincipal userDetails: UserDetails?
|
||||
): ResponseEntity<*> {
|
||||
if (userDetails == null) {
|
||||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build<Unit>()
|
||||
}
|
||||
|
||||
var bookmark = bookmarkService.findById(id).awaitSingleOrNull()
|
||||
?: return ResponseEntity.notFound().build<Unit>()
|
||||
|
||||
if (bookmark.userId != userDetails.username) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).build<Unit>()
|
||||
}
|
||||
|
||||
// [신규] 마이그레이션 로직 추가
|
||||
if (bookmark.images.isEmpty() && bookmark.contentUrls.isNotEmpty()) {
|
||||
bookmark = bookmark.copy(
|
||||
images = bookmark.contentUrls.map { BookmarkImage(url = it, isVisible = true) },
|
||||
contentUrls = emptyList()
|
||||
)
|
||||
}
|
||||
|
||||
val updatedImages = bookmark.images.map {
|
||||
if (it.url == request.imageUrl) {
|
||||
it.copy(isVisible = !it.isVisible) // isVisible 상태를 반전시킵니다.
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
|
||||
val updatedBookmark = bookmark.copy(images = updatedImages)
|
||||
val saved = bookmarkService.saveBookmark(updatedBookmark).awaitSingle()
|
||||
return ResponseEntity.ok(saved)
|
||||
}
|
||||
|
||||
// [이 함수도 BookmarkApiController 내부에 추가]
|
||||
@DeleteMapping("/{id}/images")
|
||||
suspend fun removeImageFromBookmark(
|
||||
@PathVariable id: String,
|
||||
@RequestBody request: ImageUrlRequest,
|
||||
@AuthenticationPrincipal userDetails: UserDetails?
|
||||
): ResponseEntity<Any> {
|
||||
if (userDetails == null) {
|
||||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build()
|
||||
}
|
||||
|
||||
val bookmark = bookmarkService.findById(id).awaitSingleOrNull()
|
||||
?: return ResponseEntity.notFound().build()
|
||||
|
||||
if (bookmark.userId != userDetails.username) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).build()
|
||||
}
|
||||
|
||||
// 물리적 파일 삭제 (선택 사항이지만 권장)
|
||||
try {
|
||||
val filename = request.imageUrl.substringAfterLast("/")
|
||||
val filePath = Paths.get(uploadPath, filename)
|
||||
if (filePath.exists()) {
|
||||
Files.delete(filePath)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logService.log("Failed to delete image file: ${request.imageUrl}, Error: ${e.message}")
|
||||
}
|
||||
|
||||
val updatedBookmark = bookmark.copy(
|
||||
contentUrls = bookmark.contentUrls.filter { it != request.imageUrl }
|
||||
)
|
||||
|
||||
val saved = bookmarkService.saveBookmark(updatedBookmark).awaitSingle()
|
||||
return ResponseEntity.ok(saved)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -1060,6 +1060,11 @@ enum class BookmarkType {
|
||||
IMAGE, // 하나 이상의 이미지
|
||||
VIDEO // 하나 이상의 비디오
|
||||
}
|
||||
data class BookmarkImage(
|
||||
val url: String = "",
|
||||
var isVisible: Boolean = true
|
||||
)
|
||||
|
||||
@Document(collection = "WebBookmark")
|
||||
data class WebBookmark(
|
||||
@BsonId
|
||||
@ -1070,7 +1075,10 @@ data class WebBookmark(
|
||||
// [신규] 북마크 타입 (URL, IMAGE, VIDEO 등)
|
||||
var bookmarkType: String = BookmarkType.URL.name,
|
||||
// [신규] 콘텐츠 URL 목록 (웹페이지는 1개, 이미지는 여러 개 가능)
|
||||
var contentUrls: List<String> = emptyList(),
|
||||
@Deprecated("Use images list instead")
|
||||
var contentUrls: List<String> = emptyList(), // 이전 버전과의 호환성을 위해 남겨둡니다.
|
||||
var images: List<BookmarkImage> = emptyList(), // URL과 'isVisible' 상태를 함께 저장하는 새 필드
|
||||
|
||||
var title: String? = null, // 페이지 제목
|
||||
var description: String? = null, // 페이지 요약 (메타 태그)
|
||||
var thumbnailUrl: String? = null, // 페이지 썸네일 (메타 태그)
|
||||
|
||||
@ -4082,4 +4082,103 @@ a.btn_layerClose:hover {
|
||||
/* 기존 CSS에서 불필요한 부분 제거 또는 통합 */
|
||||
.where_item { /* 이전 코드의 클래스. 새 클래스로 대체되었으므로 필요 없으면 제거 */
|
||||
/* display: none; */
|
||||
}
|
||||
}
|
||||
|
||||
/* --- [NEW] 북마크 수정 팝업 스타일 --- */
|
||||
.images-list-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
background: #f9f9f9;
|
||||
min-height: 80px;
|
||||
margin-bottom: 1em;
|
||||
max-height: 230px; /* 이미지 목록 영역의 최대 높이를 230px로 제한 */
|
||||
overflow-y: auto; /* 내용이 넘칠 경우 세로 스크롤바 자동 표시 */
|
||||
}
|
||||
|
||||
/* 이미지 목록 스크롤바 스타일 (선택 사항) */
|
||||
.images-list-container::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
.images-list-container::-webkit-scrollbar-track {
|
||||
background: #e9e9e9;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.images-list-container::-webkit-scrollbar-thumb {
|
||||
background: #bbb;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.image-preview-item {
|
||||
position: relative;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
.image-preview-item img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.delete-image-btn {
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
right: -5px;
|
||||
background: red;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
line-height: 22px;
|
||||
text-align: center;
|
||||
}
|
||||
.form-control-wrapper {
|
||||
margin-top: 1em;
|
||||
padding: 0.75em;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.tag-display-box {
|
||||
min-height: 24px;
|
||||
}
|
||||
.tag-display-box .tag-item {
|
||||
display: inline-block;
|
||||
background-color: #e9e9e9;
|
||||
border-radius: 5px;
|
||||
padding: 3px 8px;
|
||||
font-size: 0.9em;
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
.image-preview-item.is-hidden img {
|
||||
opacity: 0.3;
|
||||
filter: grayscale(100%);
|
||||
}
|
||||
|
||||
.image-preview-item .toggle-visibility-btn {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
line-height: 28px;
|
||||
text-align: center;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.image-preview-item.is-hidden .toggle-visibility-btn {
|
||||
background: #28a745; /* 숨겨진 아이템을 다시 보이게 하는 버튼은 초록색으로 표시 */
|
||||
}
|
||||
|
||||
@ -1840,9 +1840,26 @@ function renderStagedBookmarkCategory() {
|
||||
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();
|
||||
// 1. 숨겨진 input 필드에 선택한 카테고리 값 저장
|
||||
const inputEl = document.getElementById(bookmarkPopupTargets.inputId);
|
||||
if (inputEl) {
|
||||
inputEl.value = stagedBookmarkCategory;
|
||||
}
|
||||
|
||||
// 2. 메인 수정 팝업의 표시 영역(display)을 업데이트
|
||||
const displayEl = document.getElementById(bookmarkPopupTargets.displayId);
|
||||
if (displayEl) {
|
||||
if (stagedBookmarkCategory) {
|
||||
// 선택한 카테고리가 있으면 태그 아이템으로 표시
|
||||
displayEl.innerHTML = `<span class="tag-item">${stagedBookmarkCategory}</span>`;
|
||||
} else {
|
||||
// 선택한 카테고리가 없으면 기본 텍스트로 복원
|
||||
displayEl.innerHTML = `<span>카테고리 선택</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 카테고리 선택 팝업만 닫기
|
||||
document.getElementById('bookmark-category-popup').style.display = 'none';
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1905,7 +1922,246 @@ function renderStagedBookmarkTags() {
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
||||
// 1. 숨겨진 input 필드에 선택한 태그 값들 저장
|
||||
const inputEl = document.getElementById(bookmarkPopupTargets.inputId);
|
||||
if (inputEl) {
|
||||
inputEl.value = tagsString;
|
||||
}
|
||||
|
||||
// 2. 메인 수정 팝업의 표시 영역(display)을 업데이트
|
||||
const displayEl = document.getElementById(bookmarkPopupTargets.displayId);
|
||||
if (displayEl) {
|
||||
if (stagedBookmarkTags && stagedBookmarkTags.length > 0) {
|
||||
// 선택한 태그가 있으면 각 태그를 아이템으로 만들어 표시
|
||||
displayEl.innerHTML = stagedBookmarkTags.map(tag => `<span class="tag-item">#${tag}</span>`).join(' ');
|
||||
} else {
|
||||
// 선택한 태그가 없으면 기본 텍스트로 복원
|
||||
displayEl.innerHTML = `<span>태그 선택</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 태그 선택 팝업만 닫기
|
||||
document.getElementById('bookmark-tag-popup').style.display = 'none';
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* [수정된 최종 함수] '수정' 버튼 클릭 시 팝업을 열고 기존 북마크 데이터를 불러오는 함수
|
||||
* @param {HTMLElement} buttonElement - 클릭된 버튼 요소 ('this')
|
||||
*/
|
||||
async function openBookmarkEditPopup(buttonElement) {
|
||||
// 1. [핵심 수정] 버튼 요소에서 실제 bookmarkId 값을 추출합니다.
|
||||
const bookmarkId = buttonElement.getAttribute('data-bookmark-id');
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/bookmarks/${bookmarkId}`, {
|
||||
headers: { 'Authorization': `Bearer ${serverData.token}` }
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('북마크 정보를 불러오는 데 실패했습니다.');
|
||||
}
|
||||
|
||||
const bookmark = await response.json();
|
||||
|
||||
// 2. 팝업창의 각 필드에 데이터 채우기
|
||||
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;
|
||||
|
||||
const category = bookmark.category || '';
|
||||
document.getElementById('edit-bookmark-category').value = category;
|
||||
document.getElementById('edit-bookmark-category-display').innerHTML = category ? `<span class="tag-item">${category}</span>` : '<span>카테고리 선택</span>';
|
||||
|
||||
const tags = bookmark.tags || [];
|
||||
document.getElementById('edit-bookmark-tags').value = tags.join(',');
|
||||
document.getElementById('edit-bookmark-tags-display').innerHTML = tags.map(t => `<span class="tag-item">#${t}</span>`).join(' ') || '<span>태그 선택</span>';
|
||||
|
||||
// 3. 이미지 목록 표시하기 (숨김/복구 기능 포함)
|
||||
const imagesListDiv = document.getElementById('edit-bookmark-images-list');
|
||||
imagesListDiv.innerHTML = '';
|
||||
|
||||
let imageList = bookmark.images || [];
|
||||
if (imageList.length === 0 && bookmark.contentUrls && bookmark.contentUrls.length > 0) {
|
||||
imageList = bookmark.contentUrls.map(url => ({ url: url, isVisible: true }));
|
||||
}
|
||||
|
||||
imageList.forEach(image => {
|
||||
const imageItem = document.createElement('div');
|
||||
imageItem.className = `image-preview-item ${!image.isVisible ? 'is-hidden' : ''}`;
|
||||
imageItem.innerHTML = `
|
||||
<img src="${serverData.apiBaseUrl + image.url}" alt="Bookmark Image">
|
||||
<button class="toggle-visibility-btn" onclick="toggleBookmarkImageVisibility(this, '${bookmark.id}', '${image.url}')">
|
||||
${image.isVisible ? '👁️' : '🚫'}
|
||||
</button>
|
||||
`;
|
||||
imagesListDiv.appendChild(imageItem);
|
||||
});
|
||||
|
||||
// 4. 파일 추가 input에 이벤트 리스너 연결
|
||||
const imageInput = document.getElementById('add-bookmark-image-input');
|
||||
const newImageInput = imageInput.cloneNode(true);
|
||||
imageInput.parentNode.replaceChild(newImageInput, imageInput);
|
||||
newImageInput.addEventListener('change', (event) => {
|
||||
uploadBookmarkImages(bookmark.id, event.target.files);
|
||||
});
|
||||
|
||||
// 5. 팝업 열기
|
||||
const dummyEl = document.createElement('div');
|
||||
dummyEl.setAttribute('to', '#bookmark-edit-popup');
|
||||
openPopup(dummyEl);
|
||||
|
||||
} catch (error) {
|
||||
showAlert('오류', error.message, 'error');
|
||||
}
|
||||
}
|
||||
/**
|
||||
* [수정된 함수] 이미지의 '보임/숨김' 상태를 서버에 업데이트하고 UI를 즉시 변경합니다.
|
||||
* @param {HTMLElement} button - 클릭된 버튼 요소 (this)
|
||||
* @param {string} bookmarkId - 북마크 ID
|
||||
* @param {string} imageUrl - 상태를 변경할 이미지의 URL
|
||||
*/
|
||||
async function toggleBookmarkImageVisibility(button, bookmarkId, imageUrl) {
|
||||
try {
|
||||
const response = await fetch(`/api/bookmarks/${bookmarkId}/images/visibility`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${serverData.token}`,
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="_csrf"]').getAttribute('content')
|
||||
},
|
||||
body: JSON.stringify({ imageUrl: imageUrl })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('이미지 상태 변경에 실패했습니다.');
|
||||
}
|
||||
|
||||
const updatedBookmark = await response.json();
|
||||
|
||||
// [핵심 수정] 더 이상 페이지 전체를 새로고침하지 않고,
|
||||
// 전달받은 'button' 요소를 기준으로 직접 UI를 변경합니다.
|
||||
if (button) {
|
||||
const imageItem = button.closest('.image-preview-item');
|
||||
const imageInfo = updatedBookmark.images.find(img => img.url === imageUrl);
|
||||
|
||||
if (imageInfo && imageItem) {
|
||||
// 버튼 아이콘과 부모 div의 'is-hidden' 클래스를 직접 제어합니다.
|
||||
button.innerHTML = imageInfo.isVisible ? '👁️' : '🚫';
|
||||
imageItem.classList.toggle('is-hidden', !imageInfo.isVisible);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
showAlert('오류', error.message, 'error');
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 북마크의 텍스트 정보(메타데이터)를 서버에 저장하는 함수
|
||||
*/
|
||||
async function submitBookmarkUpdate() {
|
||||
const bookmarkId = document.getElementById('edit-bookmark-id').value;
|
||||
|
||||
const dataToUpdate = {
|
||||
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: document.getElementById('edit-bookmark-tags').value.split(',').filter(t => t) // 빈 태그 제거
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/bookmarks/${bookmarkId}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${serverData.token}`,
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="_csrf"]').getAttribute('content')
|
||||
},
|
||||
body: JSON.stringify(dataToUpdate)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('북마크 업데이트에 실패했습니다.');
|
||||
}
|
||||
|
||||
showAlert('성공', '북마크가 성공적으로 업데이트되었습니다.', 'success');
|
||||
closePopup();
|
||||
location.reload(); // 페이지 새로고침하여 변경사항 확인
|
||||
} catch (error) {
|
||||
showAlert('오류', error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 새로운 이미지를 서버에 업로드하고 북마크에 추가하는 함수
|
||||
* @param {string} bookmarkId - 북마크 ID
|
||||
* @param {FileList} files - 사용자가 선택한 파일 목록
|
||||
*/
|
||||
async function uploadBookmarkImages(bookmarkId, files) {
|
||||
if (!files.length) return;
|
||||
|
||||
const formData = new FormData();
|
||||
for (const file of files) {
|
||||
formData.append('files', file);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/bookmarks/${bookmarkId}/images`, { // 새 API 엔드포인트
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${serverData.token}`,
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="_csrf"]').getAttribute('content')
|
||||
},
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('이미지 업로드에 실패했습니다.');
|
||||
}
|
||||
|
||||
// 업로드 성공 후, 팝업 내용을 최신 정보로 다시 로드
|
||||
showAlert('성공', '이미지가 추가되었습니다.', 'success');
|
||||
openBookmarkEditPopup(bookmarkId);
|
||||
|
||||
} catch (error) {
|
||||
showAlert('오류', error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 기존 이미지를 북마크에서 삭제하는 함수
|
||||
* @param {string} bookmarkId - 북마크 ID
|
||||
* @param {string} imageUrl - 삭제할 이미지의 URL
|
||||
* @param {HTMLElement} buttonElement - 클릭된 삭제 버튼
|
||||
*/
|
||||
async function deleteBookmarkImage(bookmarkId, imageUrl, buttonElement) {
|
||||
if (!await showConfirm('확인', '이 이미지를 정말 삭제하시겠습니까?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/bookmarks/${bookmarkId}/images`, { // 새 API 엔드포인트
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${serverData.token}`,
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="_csrf"]').getAttribute('content')
|
||||
},
|
||||
body: JSON.stringify({ imageUrl: imageUrl })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('이미지 삭제에 실패했습니다.');
|
||||
}
|
||||
|
||||
// 화면에서 즉시 이미지 제거
|
||||
buttonElement.parentElement.remove();
|
||||
showAlert('성공', '이미지가 삭제되었습니다.', 'success');
|
||||
|
||||
} catch (error) {
|
||||
showAlert('오류', error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,11 +7,52 @@
|
||||
<head>
|
||||
<title>Bookmarks</title>
|
||||
<style>
|
||||
.scrollable-content {
|
||||
max-height: 500px; /* 콘텐츠 영역의 최대 높이를 지정 */
|
||||
max-width: 500px;
|
||||
overflow-y: auto; /* 세로 내용이 넘칠 경우 스크롤바 자동 생성 */
|
||||
-webkit-overflow-scrolling: touch; /* 모바일에서 부드러운 스크롤 효과 */
|
||||
/* --- [수정] 북마크 이미지 영역 스크롤 및 카드 크기 고정 --- */
|
||||
|
||||
/* 1. 각 북마크 카드의 전체 높이를 720px로 고정합니다. */
|
||||
.swiper-slide .box.feature {
|
||||
height: 720px;
|
||||
padding-bottom: 50px;
|
||||
}
|
||||
|
||||
/* 2. 여러 이미지를 담는 컨테이너의 스타일을 정의합니다. */
|
||||
.image-flick-container {
|
||||
height: 450px; /* 이미지 영역의 높이를 450px로 고정합니다. (값 조절 가능) */
|
||||
overflow-y: auto; /* 이 높이를 넘어가는 이미지는 세로 스크롤됩니다. */
|
||||
border: 1px solid #eee;
|
||||
border-radius: 5px;
|
||||
background-color: #f0f0f0; /* 스크롤 영역 배경색을 살짝 추가 */
|
||||
margin-bottom: 1em; /* 이미지 영역과 텍스트 영역 사이의 간격 */
|
||||
}
|
||||
|
||||
/* 3. 스크롤 영역 내부의 이미지 스타일을 지정합니다. */
|
||||
.image-flick-container img {
|
||||
width: 100%; /* 이미지를 컨테이너 너비에 맞춥니다. */
|
||||
height: auto; /* 이미지 비율을 유지합니다. */
|
||||
display: block;
|
||||
margin-bottom: 10px; /* 이미지들 사이에 약간의 간격을 줍니다. */
|
||||
}
|
||||
|
||||
.image-flick-container img:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* 4. (선택) 스크롤바 디자인을 개선합니다. */
|
||||
.image-flick-container::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.image-flick-container::-webkit-scrollbar-track {
|
||||
background: #e9e9e9;
|
||||
}
|
||||
|
||||
.image-flick-container::-webkit-scrollbar-thumb {
|
||||
background: #bbb;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.image-flick-container::-webkit-scrollbar-thumb:hover {
|
||||
background: #999;
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" href="https://unpkg.com/swiper/swiper-bundle.min.css" />
|
||||
@ -67,20 +108,26 @@
|
||||
<section class="box feature" style="margin: 0; height: 100%; display: flex; flex-direction: column;">
|
||||
|
||||
<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 th:case="'IMAGE'" class="image-flick-container">
|
||||
<th:block th:each="image : ${bookmark.images}" th:if="${image.isVisible}">
|
||||
<img th:src="${apiBaseUrl + image.url}" alt="Bookmark Image" />
|
||||
</th:block>
|
||||
</div>
|
||||
<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 th:case="'VIDEO'" class="video-container bookmark-image-container">
|
||||
<th:block th:each="image : ${bookmark.images}" th:if="${image.isVisible and #lists.size(bookmark.images) > 0}">
|
||||
<video controls style="width: 100%;">
|
||||
<source th:src="${apiBaseUrl + image.url}" type="video/mp4">
|
||||
</video>
|
||||
</th:block>
|
||||
</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" />
|
||||
onclick="showBookmarkOptions(this)" class="bookmark-image-container">
|
||||
<th:block th:each="image : ${bookmark.images}" th:if="${image.isVisible}">
|
||||
<img th:src="${apiBaseUrl + mage.url}" alt="Bookmark Image" />
|
||||
</th:block>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@ -90,6 +137,14 @@
|
||||
<h3 th:text="${bookmark.title}">북마크 제목</h3>
|
||||
<p th:if="${bookmark.userComment}" th:text="${bookmark.userComment}"></p>
|
||||
</header>
|
||||
<div class="bookmark-meta-container" style="margin: 1em 0;">
|
||||
<div th:if="${bookmark.category != null and !#strings.isEmpty(bookmark.category)}">
|
||||
<span class="tag-item" th:text="${bookmark.category}"></span>
|
||||
</div>
|
||||
<div th:if="${bookmark.tags != null and !#lists.isEmpty(bookmark.tags)}">
|
||||
<span th:each="tag : ${bookmark.tags}" class="tag-item" th:text="'#' + ${tag}"></span>
|
||||
</div>
|
||||
</div>
|
||||
<p th:text="${#strings.abbreviate(bookmark.description, 100)}"></p>
|
||||
</div>
|
||||
|
||||
@ -104,6 +159,12 @@
|
||||
<button class="button alt small" th:onclick="toggleCommentSection([[${bookmark.id}]])">
|
||||
💬 Comments
|
||||
</button>
|
||||
<button class="button alt small"
|
||||
th:if="${#authentication.principal != null && #authentication.principal instanceof T(org.springframework.security.core.userdetails.UserDetails) && #authentication.principal.username == bookmark.userId}"
|
||||
th:data-bookmark-id="${bookmark.id}"
|
||||
onclick="openBookmarkEditPopup(this)">
|
||||
✏️ 수정
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<section class="comment-section" th:id="|comment-section-${bookmark.id}|" style="display: none; margin-top: 1em; text-align: left;">
|
||||
@ -116,7 +177,7 @@
|
||||
</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>
|
||||
<p style="margin:0;">댓글을 작성하려면 <a href="javascript:void(0);" class="open-login-popup" to="#loginPopup">로그인</a>이 필요합니다.</p>
|
||||
</div>
|
||||
|
||||
<div th:id="|comments-list-${bookmark.id}|" style="margin-top: 1em;">
|
||||
|
||||
@ -57,7 +57,8 @@
|
||||
enc: /*[[${enc ?: ''}]]*/,
|
||||
keyword: /*[[${keyword ?: ''}]]*/,
|
||||
// --- [핵심 추가] ---
|
||||
token: /*[[${jwtToken}]]*/
|
||||
token: /*[[${jwtToken}]]*/,
|
||||
apiBaseUrl : /*[[${apiBaseUrl}]]*/
|
||||
};
|
||||
</script>
|
||||
</th:block>
|
||||
|
||||
@ -74,7 +74,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="bookmark-edit-popup" class="pop_layer">
|
||||
|
||||
<div id="bookmark-edit-popup" class="pop_layer" style="max-width: 600px;">
|
||||
<div class="pop_container">
|
||||
<div class="pop_conts">
|
||||
<h2>북마크 수정</h2>
|
||||
@ -94,20 +95,27 @@
|
||||
</select>
|
||||
|
||||
<div class="form-control-wrapper" onclick="openBookmarkCategoryPopup('edit-bookmark-category-display', 'edit-bookmark-category')">
|
||||
<strong>카테고리</strong>
|
||||
<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>
|
||||
<strong>태그:</strong>
|
||||
<div id="edit-bookmark-tags-display" class="tag-display-box">태그 선택</div>
|
||||
</div>
|
||||
<input type="hidden" id="edit-bookmark-tags">
|
||||
|
||||
<hr>
|
||||
<label>이미지 관리</label>
|
||||
<div id="edit-bookmark-images-list" class="images-list-container">
|
||||
</div>
|
||||
<input type="file" id="add-bookmark-image-input" multiple accept="image/*" style="display: none;">
|
||||
<button type="button" class="button small" onclick="document.getElementById('add-bookmark-image-input').click();">이미지 추가</button>
|
||||
|
||||
<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>
|
||||
<button type="button" class="button" onclick="submitBookmarkUpdate()">변경사항 저장</button>
|
||||
<button type="button" class="button alt btn_layerClose">취소</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -123,7 +131,7 @@
|
||||
<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>
|
||||
<a href="javascript:void(0);" class="button alt" onclick="this.closest('.pop_layer').style.display='none'">취소</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -139,7 +147,7 @@
|
||||
<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>
|
||||
<a href="javascript:void(0);" class="button alt" onclick="this.closest('.pop_layer').style.display='none'">취소</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user