From 0ce20e4bf1992a0ebaefe7a7faa17c7ce70e9cc9 Mon Sep 17 00:00:00 2001 From: lunaticbum Date: Tue, 23 Sep 2025 15:37:51 +0900 Subject: [PATCH] .. --- .../lunaticbum/back/lun/configs/AppConfig.kt | 2 +- .../back/lun/controllers/BlogController.kt | 186 ++++++++++-- .../kr/lunaticbum/back/lun/model/Post.kt | 10 +- src/main/resources/static/css/main.css | 101 ++++++- src/main/resources/static/js/common.js | 270 +++++++++++++++++- .../templates/content/bookmarks.html | 89 +++++- .../templates/fragments/includes.html | 3 +- .../templates/layout/default_layout.html | 22 +- 8 files changed, 634 insertions(+), 49 deletions(-) diff --git a/src/main/kotlin/kr/lunaticbum/back/lun/configs/AppConfig.kt b/src/main/kotlin/kr/lunaticbum/back/lun/configs/AppConfig.kt index b1b61ec..59457b1 100644 --- a/src/main/kotlin/kr/lunaticbum/back/lun/configs/AppConfig.kt +++ b/src/main/kotlin/kr/lunaticbum/back/lun/configs/AppConfig.kt @@ -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" ) } diff --git a/src/main/kotlin/kr/lunaticbum/back/lun/controllers/BlogController.kt b/src/main/kotlin/kr/lunaticbum/back/lun/controllers/BlogController.kt index 22af19d..1490320 100644 --- a/src/main/kotlin/kr/lunaticbum/back/lun/controllers/BlogController.kt +++ b/src/main/kotlin/kr/lunaticbum/back/lun/controllers/BlogController.kt @@ -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().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> { 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> { // 기존 서비스 메서드를 그대로 사용하여 사용자 권한에 맞는 북마크 목록을 가져옴 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, + @AuthenticationPrincipal userDetails: UserDetails? + ): ResponseEntity<*> { + if (userDetails == null || uploadPath.isNullOrBlank()) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build() + } + + var bookmark = bookmarkService.findById(id).awaitSingleOrNull() + ?: return ResponseEntity.notFound().build() + + if (bookmark.userId != userDetails.username) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).build() + } + + // [신규] 기존 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() + } + + var bookmark = bookmarkService.findById(id).awaitSingleOrNull() + ?: return ResponseEntity.notFound().build() + + if (bookmark.userId != userDetails.username) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).build() + } + + // [신규] 마이그레이션 로직 추가 + 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 { + 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) + } + } diff --git a/src/main/kotlin/kr/lunaticbum/back/lun/model/Post.kt b/src/main/kotlin/kr/lunaticbum/back/lun/model/Post.kt index cdd4e1b..384146d 100644 --- a/src/main/kotlin/kr/lunaticbum/back/lun/model/Post.kt +++ b/src/main/kotlin/kr/lunaticbum/back/lun/model/Post.kt @@ -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 = emptyList(), + @Deprecated("Use images list instead") + var contentUrls: List = emptyList(), // 이전 버전과의 호환성을 위해 남겨둡니다. + var images: List = emptyList(), // URL과 'isVisible' 상태를 함께 저장하는 새 필드 + var title: String? = null, // 페이지 제목 var description: String? = null, // 페이지 요약 (메타 태그) var thumbnailUrl: String? = null, // 페이지 썸네일 (메타 태그) diff --git a/src/main/resources/static/css/main.css b/src/main/resources/static/css/main.css index 4b11e8d..47701fe 100644 --- a/src/main/resources/static/css/main.css +++ b/src/main/resources/static/css/main.css @@ -4082,4 +4082,103 @@ a.btn_layerClose:hover { /* 기존 CSS에서 불필요한 부분 제거 또는 통합 */ .where_item { /* 이전 코드의 클래스. 새 클래스로 대체되었으므로 필요 없으면 제거 */ /* display: none; */ -} \ No newline at end of file +} + +/* --- [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; /* 숨겨진 아이템을 다시 보이게 하는 버튼은 초록색으로 표시 */ +} diff --git a/src/main/resources/static/js/common.js b/src/main/resources/static/js/common.js index 1546f13..10dc7c0 100644 --- a/src/main/resources/static/js/common.js +++ b/src/main/resources/static/js/common.js @@ -1840,9 +1840,26 @@ function renderStagedBookmarkCategory() { area.innerHTML = stagedBookmarkCategory ? `${stagedBookmarkCategory} X` : '선택된 카테고리 없음'; } function applyBookmarkCategory() { - document.getElementById(bookmarkPopupTargets.inputId).value = stagedBookmarkCategory; - document.getElementById(bookmarkPopupTargets.displayId).innerHTML = stagedBookmarkCategory ? `${stagedBookmarkCategory}` : '카테고리 선택'; - 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 = `${stagedBookmarkCategory}`; + } else { + // 선택한 카테고리가 없으면 기본 텍스트로 복원 + displayEl.innerHTML = `카테고리 선택`; + } + } + + // 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 => `#${tag}`).join(' ') || '태그 선택'; - closePopup(); -} \ No newline at end of file + + // 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 => `#${tag}`).join(' '); + } else { + // 선택한 태그가 없으면 기본 텍스트로 복원 + displayEl.innerHTML = `태그 선택`; + } + } + + // 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 ? `${category}` : '카테고리 선택'; + + const tags = bookmark.tags || []; + document.getElementById('edit-bookmark-tags').value = tags.join(','); + document.getElementById('edit-bookmark-tags-display').innerHTML = tags.map(t => `#${t}`).join(' ') || '태그 선택'; + + // 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 = ` + Bookmark Image + + `; + 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'); + } +} diff --git a/src/main/resources/templates/content/bookmarks.html b/src/main/resources/templates/content/bookmarks.html index 15a9e39..bc51cc2 100644 --- a/src/main/resources/templates/content/bookmarks.html +++ b/src/main/resources/templates/content/bookmarks.html @@ -7,11 +7,52 @@ Bookmarks @@ -67,20 +108,26 @@
-
- Bookmark Image +
+ + Bookmark Image +
- @@ -90,6 +137,14 @@

북마크 제목

+
+
+ +
+
+ +
+

@@ -104,6 +159,12 @@ +