From a97769bfda72ed78c1cbe028e5736ba604d74c2a Mon Sep 17 00:00:00 2001 From: jin2304 Date: Wed, 11 Feb 2026 22:53:30 +0900 Subject: [PATCH 01/11] =?UTF-8?q?feat/SW-45/refactor(Back):=20=EB=B6=81?= =?UTF-8?q?=EB=A7=88=ED=81=AC=20=EC=B6=94=EA=B0=80=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EB=A6=AC=ED=8C=A9=ED=84=B0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 태그 처리 및 생성 로직 추가: 태그 생성과 조회를 한 번의 쿼리로 처리하는 기능 신규 구현 (CTE 활용) - 북마크-태그 연결 로직 추가: 북마크와 태그를 연결하여 DB에 한 번에 저장하는 Bulk Insert 기능 구현 --- .../SearchWeb/bookmark/dao/BookmarkDao.java | 9 ++- .../bookmark/dao/MybatisBookmarkDao.java | 19 +++++ .../SearchWeb/bookmark/domain/Bookmark.java | 3 + .../SearchWeb/bookmark/dto/BookmarkDto.java | 2 + .../bookmark/dto/MemberTagResultDto.java | 13 +++ .../bookmark/service/BookmarkServiceImpl.java | 80 +++++++++++++++++-- src/main/resources/mapper/bookmark-mapper.xml | 60 +++++++++++++- 7 files changed, 175 insertions(+), 11 deletions(-) create mode 100644 src/main/java/com/web/SearchWeb/bookmark/dto/MemberTagResultDto.java diff --git a/src/main/java/com/web/SearchWeb/bookmark/dao/BookmarkDao.java b/src/main/java/com/web/SearchWeb/bookmark/dao/BookmarkDao.java index 0456267..6b86d83 100644 --- a/src/main/java/com/web/SearchWeb/bookmark/dao/BookmarkDao.java +++ b/src/main/java/com/web/SearchWeb/bookmark/dao/BookmarkDao.java @@ -3,6 +3,7 @@ import com.web.SearchWeb.bookmark.domain.Bookmark; import com.web.SearchWeb.bookmark.domain.Link; import com.web.SearchWeb.bookmark.dto.BookmarkDto; +import com.web.SearchWeb.bookmark.dto.MemberTagResultDto; import com.web.SearchWeb.bookmark.dto.request.BookmarkSearchRequestDto; import java.util.List; @@ -12,7 +13,7 @@ public interface BookmarkDao { int checkBookmarkExists(Long memberId, Long folderId, Long linkId); //북마크 추가 - int insertBookmark(BookmarkDto bookmark, Long linkId); + int insertBookmark(BookmarkDto bookmarkDto, Long linkId); //북마크 단일 조회 Bookmark selectBookmark(Long memberId, Long bookmarkId); @@ -37,4 +38,10 @@ public interface BookmarkDao { //URL 기반 북마크 존재 여부 확인 (Board Bridge용) int checkBookmarkExistsByUrl(Long memberId, String url); + + // 태그 등록 및 조회 (Insert & Select) + List insertAndSelectTags(Long memberId, List tagNames); + + // 북마크-태그 연결 일괄 추가 (Bulk Insert) + int insertBookmarkTags(Long bookmarkId, List tagIds); } diff --git a/src/main/java/com/web/SearchWeb/bookmark/dao/MybatisBookmarkDao.java b/src/main/java/com/web/SearchWeb/bookmark/dao/MybatisBookmarkDao.java index ab8e678..18976e6 100644 --- a/src/main/java/com/web/SearchWeb/bookmark/dao/MybatisBookmarkDao.java +++ b/src/main/java/com/web/SearchWeb/bookmark/dao/MybatisBookmarkDao.java @@ -3,6 +3,7 @@ import com.web.SearchWeb.bookmark.domain.Bookmark; import com.web.SearchWeb.bookmark.domain.Link; import com.web.SearchWeb.bookmark.dto.BookmarkDto; +import com.web.SearchWeb.bookmark.dto.MemberTagResultDto; import com.web.SearchWeb.bookmark.dto.request.BookmarkSearchRequestDto; import org.apache.ibatis.session.SqlSession; import org.springframework.beans.factory.annotation.Autowired; @@ -108,4 +109,22 @@ public int deleteBookmark(Long memberId, Long bookmarkId) { public int deleteBookmarkByLink(Long memberId, Long linkId) { return mapper.deleteBookmarkByLink(memberId, linkId); } + + + /** + * 태그 등록 및 조회 (Insert & Select) + */ + @Override + public List insertAndSelectTags(Long memberId, List tagNames) { + return mapper.insertAndSelectTags(memberId, tagNames); + } + + + /** + * 북마크-태그 연결 일괄 추가 (Bulk Insert) + */ + @Override + public int insertBookmarkTags(Long bookmarkId, List tagIds) { + return mapper.insertBookmarkTags(bookmarkId, tagIds); + } } diff --git a/src/main/java/com/web/SearchWeb/bookmark/domain/Bookmark.java b/src/main/java/com/web/SearchWeb/bookmark/domain/Bookmark.java index 3d8de31..f17164c 100644 --- a/src/main/java/com/web/SearchWeb/bookmark/domain/Bookmark.java +++ b/src/main/java/com/web/SearchWeb/bookmark/domain/Bookmark.java @@ -30,4 +30,7 @@ public class Bookmark extends BaseEntity { // Link 객체 (Association) private Link link; // Link 테이블과 조인된 객체 + + // Tag 목록 (Collection) + private List tags; // 태그 목록 } diff --git a/src/main/java/com/web/SearchWeb/bookmark/dto/BookmarkDto.java b/src/main/java/com/web/SearchWeb/bookmark/dto/BookmarkDto.java index baddaa5..ecf991f 100644 --- a/src/main/java/com/web/SearchWeb/bookmark/dto/BookmarkDto.java +++ b/src/main/java/com/web/SearchWeb/bookmark/dto/BookmarkDto.java @@ -19,10 +19,12 @@ @AllArgsConstructor @Builder public class BookmarkDto { + private Long bookmarkId; // bookmark_id (PK, Insert 시 생성된 키 저장용) private Long memberFolderId; // member_folder_id (저장할 폴더) private String displayTitle; // display_title (사용자가 지정한 제목) private String url; // original_url (link 테이블에 저장) private String note; // note (메모) private Long primaryCategoryId; // primary_category_id (카테고리) private Long createdByMemberId; // created_by_member_id (저장한 회원) + private String tags; // Space-separated tags ex: "dev java spring" } diff --git a/src/main/java/com/web/SearchWeb/bookmark/dto/MemberTagResultDto.java b/src/main/java/com/web/SearchWeb/bookmark/dto/MemberTagResultDto.java new file mode 100644 index 0000000..40c12ef --- /dev/null +++ b/src/main/java/com/web/SearchWeb/bookmark/dto/MemberTagResultDto.java @@ -0,0 +1,13 @@ +package com.web.SearchWeb.bookmark.dto; + +import lombok.Data; + +/** + * 북마크 태그 등록 및 조회(insertAndSelectTags) 결과를 담는 DTO + * - 새로 생성된 태그 또는 기존에 존재하는 태그의 ID와 이름을 다 포함 + */ +@Data +public class MemberTagResultDto { + private Long memberTagId; + private String tagName; +} diff --git a/src/main/java/com/web/SearchWeb/bookmark/service/BookmarkServiceImpl.java b/src/main/java/com/web/SearchWeb/bookmark/service/BookmarkServiceImpl.java index a76737d..11ad5a5 100644 --- a/src/main/java/com/web/SearchWeb/bookmark/service/BookmarkServiceImpl.java +++ b/src/main/java/com/web/SearchWeb/bookmark/service/BookmarkServiceImpl.java @@ -6,14 +6,24 @@ import com.web.SearchWeb.bookmark.dto.BoardBookmarkCheckDto; import com.web.SearchWeb.bookmark.dto.BookmarkDto; import com.web.SearchWeb.bookmark.dto.request.BookmarkSearchRequestDto; + +import lombok.extern.slf4j.Slf4j; + import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import com.web.SearchWeb.bookmark.dto.MemberTagResultDto; import java.net.URI; +import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; @Service +@Slf4j public class BookmarkServiceImpl implements BookmarkService { private final BookmarkDao bookmarkDao; @@ -103,17 +113,32 @@ public boolean checkBookmarkExistsByUrl(Long memberId, String url) { public int insertBookmark(BookmarkDto bookmarkDto, String url) { // 링크 조회 또는 생성 Link link = getOrCreateLink(url, bookmarkDto.getCreatedByMemberId()); - - // 중복 확인 (기본 폴더 등에서) - int exists = bookmarkDao.checkBookmarkExists(bookmarkDto.getCreatedByMemberId(), bookmarkDto.getMemberFolderId(), link.getLinkId()); - if (exists > 0) { - return 0; // 이미 존재함 + + // TODO: 링크 분석 및 폴더 서비스 완성 후 제거 - 임시 기본 폴더 ID 설정 + if (bookmarkDto.getMemberFolderId() == null) { + bookmarkDto.setMemberFolderId(1L); // 임시 하드코딩 값 + log.warn("memberFolderId가 null이어서 임시 기본값(1)을 사용합니다. 링크 분석 및 폴더 서비스 연동 후 제거 필요."); } - + // 북마크 추가 - return bookmarkDao.insertBookmark(bookmarkDto, link.getLinkId()); - } + try { + int result = bookmarkDao.insertBookmark(bookmarkDto, link.getLinkId()); + + // 태그 처리 및 저장 + if (result > 0 && bookmarkDto.getTags() != null && !bookmarkDto.getTags().isEmpty()) { + // MyBatis의 useGeneratedKeys="true" 설정에 의해 insert 성공 시, bookmarkDto.bookmarkId에 생성된 PK(member_saved_link_id)가 자동으로 채워짐 + processAndCreateTags(bookmarkDto.getBookmarkId(), bookmarkDto.getCreatedByMemberId(), bookmarkDto.getTags()); + } + return result; + } catch (DataIntegrityViolationException e) { + // TODO: 글로벌 예외 처리 구현 후, 커스텀에러 던지도록 변경 + // 현재는 임시로 0 반환 (DB UNIQUE 제약 uq_member_saved_link_folder_link 위반 시) + log.warn("북마크 중복 저장 시도: memberId={}, folderId={}, linkId={}", + bookmarkDto.getCreatedByMemberId(), bookmarkDto.getMemberFolderId(), link.getLinkId()); + return 0; + } + } /** * 북마크 수정 @@ -164,6 +189,45 @@ private String extractDomain(String url) { } } + /** + * 태그 문자열 처리 및 저장 + * @param bookmarkId 북마크 ID + * @param memberId 회원 ID + * @param tags 태그 문자열 (띄어쓰기 또는 콤마 구분) + */ + private void processAndCreateTags(Long bookmarkId, Long memberId, String tags) { + if (tags == null || tags.isBlank()) return; + + // 1. 태그 파싱 및 중복 제거 + Set uniqueTags = new HashSet<>(); + String[] splitTags = tags.split("[,\\s]+"); + for (String tag : splitTags) { + if (!tag.isBlank()) { + uniqueTags.add(tag.trim()); + } + } + + if (uniqueTags.isEmpty()) return; + + List tagNames = new ArrayList<>(uniqueTags); + + // 2. 태그 등록 및 조회 (Insert & Select) - CTE를 사용하여 한 번의 쿼리로 처리 + // 새로운 태그는 생성하고, 기존 태그는 조회하여 모든 태그의 ID를 반환함 + List allTags = bookmarkDao.insertAndSelectTags(memberId, tagNames); + + log.info("allTags: {}", allTags); + + // 최종 태그 ID 목록 추출 + List finalTagIds = allTags.stream() + .map(MemberTagResultDto::getMemberTagId) + .collect(Collectors.toList()); + + // 3. 북마크-태그 연결 일괄 추가 (Bulk Insert) + if (!finalTagIds.isEmpty()) { + bookmarkDao.insertBookmarkTags(bookmarkId, finalTagIds); + } + } + // ========== Legacy Board-Bookmark Methods ========== diff --git a/src/main/resources/mapper/bookmark-mapper.xml b/src/main/resources/mapper/bookmark-mapper.xml index c505a40..a1eb3c2 100644 --- a/src/main/resources/mapper/bookmark-mapper.xml +++ b/src/main/resources/mapper/bookmark-mapper.xml @@ -25,6 +25,10 @@ + + + + @@ -117,9 +121,9 @@ - + INSERT INTO member_saved_link (link_id, member_folder_id, display_title, note, primary_category_id, created_by_member_id) - VALUES (#{linkId}, #{bookmark.memberFolderId}, #{bookmark.displayTitle}, #{bookmark.note}, #{bookmark.primaryCategoryId}, #{bookmark.createdByMemberId}) + VALUES (#{linkId}, #{bookmarkDto.memberFolderId}, #{bookmarkDto.displayTitle}, #{bookmarkDto.note}, #{bookmarkDto.primaryCategoryId}, #{bookmarkDto.createdByMemberId}) @@ -148,4 +152,56 @@ AND deleted_at IS NULL + + + + INSERT INTO member_tag (owner_member_id, tag_name) + VALUES (#{memberId}, #{tagName}) + ON CONFLICT (owner_member_id, tag_name) + DO UPDATE SET deleted_at = NULL, updated_at = now() + RETURNING member_tag_id + + + + + + + + + + INSERT INTO member_saved_link_tag (member_saved_link_id, member_tag_id) + VALUES + + (#{bookmarkId}, #{tagId}) + + ON CONFLICT (member_saved_link_id, member_tag_id) DO NOTHING + + From 91836dd0c0090f386a8aee7361eba6aae80bddcb Mon Sep 17 00:00:00 2001 From: jin2304 Date: Wed, 11 Feb 2026 22:53:36 +0900 Subject: [PATCH 02/11] =?UTF-8?q?feat/SW-45/refactor(Front):=20=EB=B6=81?= =?UTF-8?q?=EB=A7=88=ED=81=AC=20=EC=B6=94=EA=B0=80=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EB=A6=AC=ED=8C=A9=ED=84=B0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 태그 처리 및 생성 로직 추가: 태그 생성과 조회를 한 번의 쿼리로 처리하는 기능 신규 구현 (CTE 활용) - 북마크-태그 연결 로직 추가: 북마크와 태그를 연결하여 DB에 한 번에 저장하는 Bulk Insert 기능 구현 --- src/main/resources/templates/mypage/myPage.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/resources/templates/mypage/myPage.html b/src/main/resources/templates/mypage/myPage.html index 5244cef..6615970 100644 --- a/src/main/resources/templates/mypage/myPage.html +++ b/src/main/resources/templates/mypage/myPage.html @@ -420,7 +420,7 @@

북마크 수정

member_memberId: memberId, displayTitle: name, // 웹사이트 이름 -> displayTitle note: description, // 웹사이트 설명 -> note - tag: tag, // 단일 태그 + tags: tag, // 단일 태그 -> tags로 변경 // memberFolderId: null, // 백엔드에서 처리하거나, 나중에 폴더 선택 기능 추가 시 설정 // primaryCategoryId: 1 // 기본 카테고리 }; @@ -678,7 +678,7 @@

${displayTitle}

displayTitle: name, url: url, // DTO has url field note: description, - tag: tag, + tags: tag, memberFolderId: memberFolderId ? parseInt(memberFolderId) : null, primaryCategoryId: primaryCategoryId ? parseInt(primaryCategoryId) : 1, createdByMemberId: memberId // DTO 필드 @@ -745,7 +745,7 @@

${displayTitle}

- + // 사용자 프로필 수정 // 프로필 수정 폼 토글 함수 function toggleUpdateProfileForm() { document.getElementById('updateProfileForm').classList.toggle('hidden'); From 13abaa9ea66fa9ad6623470c435888209b9e1c15 Mon Sep 17 00:00:00 2001 From: jin2304 Date: Wed, 11 Feb 2026 22:55:45 +0900 Subject: [PATCH 03/11] =?UTF-8?q?feat/SW-45/refactor(Back):=20=EC=A0=95?= =?UTF-8?q?=EA=B7=9C=ED=99=94=20=EB=A7=81=ED=81=AC=EB=8C=80=EC=8B=A0=20?= =?UTF-8?q?=EC=9D=BC=EB=B0=98=20=EB=A7=81=ED=81=AC=EB=A1=9C=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=A6=AC=ED=8C=A9?= =?UTF-8?q?=ED=84=B0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/web/SearchWeb/bookmark/dao/BookmarkDao.java | 4 ++-- .../com/web/SearchWeb/bookmark/dao/MybatisBookmarkDao.java | 6 +++--- .../web/SearchWeb/bookmark/service/BookmarkServiceImpl.java | 4 ++-- src/main/resources/mapper/bookmark-mapper.xml | 6 +++--- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/web/SearchWeb/bookmark/dao/BookmarkDao.java b/src/main/java/com/web/SearchWeb/bookmark/dao/BookmarkDao.java index 6b86d83..64d9558 100644 --- a/src/main/java/com/web/SearchWeb/bookmark/dao/BookmarkDao.java +++ b/src/main/java/com/web/SearchWeb/bookmark/dao/BookmarkDao.java @@ -30,8 +30,8 @@ public interface BookmarkDao { //북마크 삭제 (Link ID 기반 - soft delete) int deleteBookmarkByLink(Long memberId, Long linkId); - //링크 조회 (canonical_url로) - Link selectLinkByCanonicalUrl(String canonicalUrl); + //링크 조회 (url로) + Link selectLinkByUrl(String url); //링크 추가 (link 테이블) int insertLink(Link link); diff --git a/src/main/java/com/web/SearchWeb/bookmark/dao/MybatisBookmarkDao.java b/src/main/java/com/web/SearchWeb/bookmark/dao/MybatisBookmarkDao.java index 18976e6..6f00772 100644 --- a/src/main/java/com/web/SearchWeb/bookmark/dao/MybatisBookmarkDao.java +++ b/src/main/java/com/web/SearchWeb/bookmark/dao/MybatisBookmarkDao.java @@ -59,11 +59,11 @@ public List selectBookmarkList(BookmarkSearchRequestDto searchRequest) /** - * 링크 조회 (canonical_url로) + * 링크 조회 (url로) */ @Override - public Link selectLinkByCanonicalUrl(String canonicalUrl) { - return mapper.selectLinkByCanonicalUrl(canonicalUrl); + public Link selectLinkByUrl(String url) { + return mapper.selectLinkByUrl(url); } diff --git a/src/main/java/com/web/SearchWeb/bookmark/service/BookmarkServiceImpl.java b/src/main/java/com/web/SearchWeb/bookmark/service/BookmarkServiceImpl.java index 11ad5a5..0f561b4 100644 --- a/src/main/java/com/web/SearchWeb/bookmark/service/BookmarkServiceImpl.java +++ b/src/main/java/com/web/SearchWeb/bookmark/service/BookmarkServiceImpl.java @@ -69,7 +69,7 @@ public Link getOrCreateLink(String url, Long createdByMemberId) { String canonicalUrl = normalizeUrl(url); // 기존 링크 조회 - Link existingLink = bookmarkDao.selectLinkByCanonicalUrl(canonicalUrl); + Link existingLink = bookmarkDao.selectLinkByUrl(url); if (existingLink != null) { return existingLink; } @@ -96,7 +96,7 @@ public Link getOrCreateLink(String url, Long createdByMemberId) { public boolean checkBookmarkExistsByUrl(Long memberId, String url) { String canonicalUrl = normalizeUrl(url); // Link가 존재하는지 먼저 확인 (최적화) - Link link = bookmarkDao.selectLinkByCanonicalUrl(canonicalUrl); + Link link = bookmarkDao.selectLinkByUrl(url); if (link == null) { return false; } diff --git a/src/main/resources/mapper/bookmark-mapper.xml b/src/main/resources/mapper/bookmark-mapper.xml index a1eb3c2..b9b6d02 100644 --- a/src/main/resources/mapper/bookmark-mapper.xml +++ b/src/main/resources/mapper/bookmark-mapper.xml @@ -105,10 +105,10 @@ - - SELECT * FROM link - WHERE canonical_url = #{canonicalUrl} + WHERE canonical_url = #{url} AND deleted_at IS NULL From 3ba9db4e64ad16c86ee85752764cf5d1b2b31fcd Mon Sep 17 00:00:00 2001 From: jin2304 Date: Wed, 11 Feb 2026 23:15:26 +0900 Subject: [PATCH 04/11] =?UTF-8?q?feat/SW-45/refactor(Back):=20=EB=B6=81?= =?UTF-8?q?=EB=A7=88=ED=81=AC=20=EB=8B=A8=EC=9D=BC/=EB=AA=A9=EB=A1=9D=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EC=8B=9C=20=ED=83=9C=EA=B7=B8=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EB=B3=B4=EA=B8=B0=EC=9C=84=ED=95=B4=20JOIN=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/mapper/bookmark-mapper.xml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/main/resources/mapper/bookmark-mapper.xml b/src/main/resources/mapper/bookmark-mapper.xml index b9b6d02..b913388 100644 --- a/src/main/resources/mapper/bookmark-mapper.xml +++ b/src/main/resources/mapper/bookmark-mapper.xml @@ -65,9 +65,11 @@ - SELECT msl.*, l.original_url, l.canonical_url, l.domain, l.thumbnail_url + SELECT msl.*, l.original_url, l.canonical_url, l.domain, l.thumbnail_url, mt.tag_name FROM member_saved_link msl JOIN link l ON msl.link_id = l.link_id + LEFT JOIN member_saved_link_tag mslt ON msl.member_saved_link_id = mslt.member_saved_link_id + LEFT JOIN member_tag mt ON mslt.member_tag_id = mt.member_tag_id AND mt.deleted_at IS NULL WHERE msl.created_by_member_id = #{memberId} AND msl.deleted_at IS NULL @@ -154,6 +158,7 @@ + INSERT INTO member_tag (owner_member_id, tag_name) VALUES (#{memberId}, #{tagName}) From 44a73f1b2b73e90e857f18bd79f52678382acb4a Mon Sep 17 00:00:00 2001 From: jin2304 Date: Sun, 15 Feb 2026 01:42:06 +0900 Subject: [PATCH 05/11] =?UTF-8?q?refactor/SW-45:=20=EC=BB=A8=EB=B2=A4?= =?UTF-8?q?=EC=85=98=EC=97=90=20=EB=94=B0=EB=9D=BC=20=EB=B6=81=EB=A7=88?= =?UTF-8?q?=ED=81=AC=20=EC=B6=94=EA=B0=80=20=EB=A1=9C=EC=A7=81=20=EB=A6=AC?= =?UTF-8?q?=ED=8C=A9=ED=84=B0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Controller 전용 DTO(BookmarkRequests) 도입 - BookmarkRequests 에서 생성, 조회, 수정, 삭제 등 DTO 그룹화 - Service 내부에서 Builder 패턴을 통한 Bookmark 엔티티 생성 로직 적용 - 커스텀에러 적용 --- .../controller/BookmarkApiController.java | 33 ++++++++----- .../controller/dto/BookmarkRequests.java | 22 +++++++++ .../SearchWeb/bookmark/dao/BookmarkDao.java | 2 +- .../bookmark/dao/MybatisBookmarkDao.java | 4 +- .../SearchWeb/bookmark/domain/Bookmark.java | 6 +++ .../bookmark/error/BookmarkErrorCode.java | 16 +++++++ .../bookmark/service/BookmarkService.java | 3 +- .../bookmark/service/BookmarkServiceImpl.java | 46 ++++++++++++------- .../SearchWeb/config/BusinessException.java | 4 ++ .../mypage/controller/MyPageController.java | 14 +++++- src/main/resources/mapper/bookmark-mapper.xml | 4 +- 11 files changed, 117 insertions(+), 37 deletions(-) create mode 100644 src/main/java/com/web/SearchWeb/bookmark/controller/dto/BookmarkRequests.java create mode 100644 src/main/java/com/web/SearchWeb/bookmark/error/BookmarkErrorCode.java diff --git a/src/main/java/com/web/SearchWeb/bookmark/controller/BookmarkApiController.java b/src/main/java/com/web/SearchWeb/bookmark/controller/BookmarkApiController.java index 1577ea2..6510c06 100644 --- a/src/main/java/com/web/SearchWeb/bookmark/controller/BookmarkApiController.java +++ b/src/main/java/com/web/SearchWeb/bookmark/controller/BookmarkApiController.java @@ -1,9 +1,11 @@ package com.web.SearchWeb.bookmark.controller; +import com.web.SearchWeb.bookmark.controller.dto.BookmarkRequests; import com.web.SearchWeb.bookmark.domain.Bookmark; import com.web.SearchWeb.bookmark.dto.BookmarkDto; import com.web.SearchWeb.bookmark.service.BookmarkService; +import com.web.SearchWeb.config.ApiResponse; import com.web.SearchWeb.member.dto.CustomOAuth2User; import com.web.SearchWeb.member.dto.CustomUserDetails; import org.springframework.beans.factory.annotation.Autowired; @@ -56,27 +58,34 @@ public ResponseEntity checkBookmark(@AuthenticationPrincipal Object cur * 북마크 추가 */ @PostMapping - public ResponseEntity> insertBookmark( + public ResponseEntity> insertBookmark( @AuthenticationPrincipal Object currentUser, - @RequestBody BookmarkDto bookmarkDto, - @RequestParam String url) { - - Map response = new HashMap<>(); - + @RequestBody BookmarkRequests.CreateDto request) { + + // TODO: AOP 처리 // 로그인 되지 않은 경우 if (currentUser == null || "anonymousUser".equals(currentUser)) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(response); + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } Long memberId = getMemberId(currentUser); if (memberId == null) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).body(response); + return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); } - bookmarkDto.setCreatedByMemberId(memberId); - int result = bookmarkService.insertBookmark(bookmarkDto, url); - response.put("success", result > 0); - return ResponseEntity.ok(response); + Long bookmarkId = bookmarkService.insertBookmark( + memberId, + request.url, + request.memberFolderId, + request.displayTitle, + request.note, + request.primaryCategoryId, + request.tags + ); + + return ResponseEntity + .status(HttpStatus.CREATED) + .body(ApiResponse.success(bookmarkId)); } diff --git a/src/main/java/com/web/SearchWeb/bookmark/controller/dto/BookmarkRequests.java b/src/main/java/com/web/SearchWeb/bookmark/controller/dto/BookmarkRequests.java new file mode 100644 index 0000000..b9f8f53 --- /dev/null +++ b/src/main/java/com/web/SearchWeb/bookmark/controller/dto/BookmarkRequests.java @@ -0,0 +1,22 @@ +package com.web.SearchWeb.bookmark.controller.dto; + +/** + * Bookmark Controller 전용 Request DTO + * - Controller에서만 사용하며, Service 계층에는 개별 파라미터로 전달 + */ +public class BookmarkRequests { + + /** + * 북마크 생성 요청 + */ + public static class CreateDto { + public Long bookmarkId; // 북마크 ID (PK, Insert 시 생성된 키 저장용) + public Long memberFolderId; // 폴더 ID (null이면 기본 폴더) + public String displayTitle; // 표시 제목 + public String url; // 저장할 URL + public String note; // 메모 + public Long primaryCategoryId; // 카테고리 ID + public Long createdByMemberId; // 저장한 회원 + public String tags; // 태그 문자열 (공백/콤마 구분) + } +} diff --git a/src/main/java/com/web/SearchWeb/bookmark/dao/BookmarkDao.java b/src/main/java/com/web/SearchWeb/bookmark/dao/BookmarkDao.java index 64d9558..3e18d66 100644 --- a/src/main/java/com/web/SearchWeb/bookmark/dao/BookmarkDao.java +++ b/src/main/java/com/web/SearchWeb/bookmark/dao/BookmarkDao.java @@ -13,7 +13,7 @@ public interface BookmarkDao { int checkBookmarkExists(Long memberId, Long folderId, Long linkId); //북마크 추가 - int insertBookmark(BookmarkDto bookmarkDto, Long linkId); + int insertBookmark(Bookmark bookmark); //북마크 단일 조회 Bookmark selectBookmark(Long memberId, Long bookmarkId); diff --git a/src/main/java/com/web/SearchWeb/bookmark/dao/MybatisBookmarkDao.java b/src/main/java/com/web/SearchWeb/bookmark/dao/MybatisBookmarkDao.java index 6f00772..555cf1e 100644 --- a/src/main/java/com/web/SearchWeb/bookmark/dao/MybatisBookmarkDao.java +++ b/src/main/java/com/web/SearchWeb/bookmark/dao/MybatisBookmarkDao.java @@ -80,8 +80,8 @@ public int insertLink(Link link) { * 북마크 추가 */ @Override - public int insertBookmark(BookmarkDto bookmark, Long linkId) { - return mapper.insertBookmark(bookmark, linkId); + public int insertBookmark(Bookmark bookmark) { + return mapper.insertBookmark(bookmark); } diff --git a/src/main/java/com/web/SearchWeb/bookmark/domain/Bookmark.java b/src/main/java/com/web/SearchWeb/bookmark/domain/Bookmark.java index f17164c..0dd050b 100644 --- a/src/main/java/com/web/SearchWeb/bookmark/domain/Bookmark.java +++ b/src/main/java/com/web/SearchWeb/bookmark/domain/Bookmark.java @@ -4,9 +4,12 @@ import java.util.List; import com.web.SearchWeb.common.domain.BaseEntity; +import lombok.AllArgsConstructor; import lombok.Getter; +import lombok.NoArgsConstructor; import lombok.Setter; import lombok.ToString; +import lombok.experimental.SuperBuilder; /** * Bookmark 도메인 클래스 (MemberSavedLink) @@ -16,6 +19,9 @@ */ @Getter @Setter +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor @ToString(callSuper = true) public class Bookmark extends BaseEntity { private Long bookmarkId; // 북마크 고유 ID (PK, member_saved_link_id) diff --git a/src/main/java/com/web/SearchWeb/bookmark/error/BookmarkErrorCode.java b/src/main/java/com/web/SearchWeb/bookmark/error/BookmarkErrorCode.java new file mode 100644 index 0000000..85830d3 --- /dev/null +++ b/src/main/java/com/web/SearchWeb/bookmark/error/BookmarkErrorCode.java @@ -0,0 +1,16 @@ +package com.web.SearchWeb.bookmark.error; + +import com.web.SearchWeb.config.ErrorCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@Getter +@RequiredArgsConstructor +public enum BookmarkErrorCode implements ErrorCode { + DUPLICATE_BOOKMARK(HttpStatus.CONFLICT, "B001", "이미 존재하는 북마크입니다."); + + private final HttpStatus status; + private final String code; + private final String message; +} diff --git a/src/main/java/com/web/SearchWeb/bookmark/service/BookmarkService.java b/src/main/java/com/web/SearchWeb/bookmark/service/BookmarkService.java index 78bd6ee..5f2a41f 100644 --- a/src/main/java/com/web/SearchWeb/bookmark/service/BookmarkService.java +++ b/src/main/java/com/web/SearchWeb/bookmark/service/BookmarkService.java @@ -10,7 +10,8 @@ public interface BookmarkService { //북마크 추가 - int insertBookmark(BookmarkDto bookmarkDto, String url); + Long insertBookmark(Long memberId, String url, Long memberFolderId, String displayTitle, + String note, Long primaryCategoryId, String tags); //북마크 단일 조회 Bookmark selectBookmark(Long memberId, Long bookmarkId); diff --git a/src/main/java/com/web/SearchWeb/bookmark/service/BookmarkServiceImpl.java b/src/main/java/com/web/SearchWeb/bookmark/service/BookmarkServiceImpl.java index 0f561b4..a482e94 100644 --- a/src/main/java/com/web/SearchWeb/bookmark/service/BookmarkServiceImpl.java +++ b/src/main/java/com/web/SearchWeb/bookmark/service/BookmarkServiceImpl.java @@ -14,6 +14,10 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import com.web.SearchWeb.bookmark.error.BookmarkErrorCode; +import com.web.SearchWeb.config.BusinessException; +import com.web.SearchWeb.config.CommonErrorCode; + import com.web.SearchWeb.bookmark.dto.MemberTagResultDto; import java.net.URI; import java.util.ArrayList; @@ -110,33 +114,43 @@ public boolean checkBookmarkExistsByUrl(Long memberId, String url) { */ @Override @Transactional - public int insertBookmark(BookmarkDto bookmarkDto, String url) { + public Long insertBookmark(Long memberId, String url, Long memberFolderId, String displayTitle, + String note, Long primaryCategoryId, String tags) { // 링크 조회 또는 생성 - Link link = getOrCreateLink(url, bookmarkDto.getCreatedByMemberId()); + Link link = getOrCreateLink(url, memberId); // TODO: 링크 분석 및 폴더 서비스 완성 후 제거 - 임시 기본 폴더 ID 설정 - if (bookmarkDto.getMemberFolderId() == null) { - bookmarkDto.setMemberFolderId(1L); // 임시 하드코딩 값 + if (memberFolderId == null) { + memberFolderId = 1L; // 임시 하드코딩 값 log.warn("memberFolderId가 null이어서 임시 기본값(1)을 사용합니다. 링크 분석 및 폴더 서비스 연동 후 제거 필요."); } + // Entity 생성 + Bookmark bookmark = Bookmark.builder() + .linkId(link.getLinkId()) + .memberFolderId(memberFolderId) + .displayTitle(displayTitle) + .note(note) + .primaryCategoryId(primaryCategoryId) + .createdByMemberId(memberId) + .build(); + // 북마크 추가 try { - int result = bookmarkDao.insertBookmark(bookmarkDto, link.getLinkId()); + int result = bookmarkDao.insertBookmark(bookmark); // 태그 처리 및 저장 - if (result > 0 && bookmarkDto.getTags() != null && !bookmarkDto.getTags().isEmpty()) { - // MyBatis의 useGeneratedKeys="true" 설정에 의해 insert 성공 시, bookmarkDto.bookmarkId에 생성된 PK(member_saved_link_id)가 자동으로 채워짐 - processAndCreateTags(bookmarkDto.getBookmarkId(), bookmarkDto.getCreatedByMemberId(), bookmarkDto.getTags()); + if (result > 0) { + if (tags != null && !tags.isEmpty()) { + // MyBatis의 useGeneratedKeys="true" 설정에 의해 insert 성공 시, bookmark.bookmarkId에 생성된 PK가 자동으로 채워짐 + processAndCreateTags(bookmark.getBookmarkId(), memberId, tags); + } + return bookmark.getBookmarkId(); } - - return result; + throw BusinessException.from(CommonErrorCode.INTERNAL_SERVER_ERROR); } catch (DataIntegrityViolationException e) { - // TODO: 글로벌 예외 처리 구현 후, 커스텀에러 던지도록 변경 - // 현재는 임시로 0 반환 (DB UNIQUE 제약 uq_member_saved_link_folder_link 위반 시) - log.warn("북마크 중복 저장 시도: memberId={}, folderId={}, linkId={}", - bookmarkDto.getCreatedByMemberId(), bookmarkDto.getMemberFolderId(), link.getLinkId()); - return 0; + log.warn("북마크 중복 저장 시도: memberId={}, folderId={}, linkId={}", memberId, memberFolderId, link.getLinkId()); + throw BusinessException.from(BookmarkErrorCode.DUPLICATE_BOOKMARK); } } @@ -215,8 +229,6 @@ private void processAndCreateTags(Long bookmarkId, Long memberId, String tags) { // 새로운 태그는 생성하고, 기존 태그는 조회하여 모든 태그의 ID를 반환함 List allTags = bookmarkDao.insertAndSelectTags(memberId, tagNames); - log.info("allTags: {}", allTags); - // 최종 태그 ID 목록 추출 List finalTagIds = allTags.stream() .map(MemberTagResultDto::getMemberTagId) diff --git a/src/main/java/com/web/SearchWeb/config/BusinessException.java b/src/main/java/com/web/SearchWeb/config/BusinessException.java index 7aa60d6..42c7578 100644 --- a/src/main/java/com/web/SearchWeb/config/BusinessException.java +++ b/src/main/java/com/web/SearchWeb/config/BusinessException.java @@ -10,4 +10,8 @@ public BusinessException(ErrorCode errorCode) { super(errorCode.getMessage()); this.errorCode = errorCode; } + + public static BusinessException from(ErrorCode errorCode) { + return new BusinessException(errorCode); + } } diff --git a/src/main/java/com/web/SearchWeb/mypage/controller/MyPageController.java b/src/main/java/com/web/SearchWeb/mypage/controller/MyPageController.java index 7bd34a4..5667495 100644 --- a/src/main/java/com/web/SearchWeb/mypage/controller/MyPageController.java +++ b/src/main/java/com/web/SearchWeb/mypage/controller/MyPageController.java @@ -77,8 +77,18 @@ public ResponseEntity> insertBookmark(@PathVariable final Lo @RequestParam String url){ Map response = new HashMap<>(); bookmarkDto.setCreatedByMemberId(memberId); - int result = bookmarkService.insertBookmark(bookmarkDto, url); - response.put("success", result > 0); + + Long bookmarkId = bookmarkService.insertBookmark( + memberId, + url, + bookmarkDto.getMemberFolderId(), + bookmarkDto.getDisplayTitle(), + bookmarkDto.getNote(), + bookmarkDto.getPrimaryCategoryId(), + bookmarkDto.getTags() + ); + + response.put("success", bookmarkId != null && bookmarkId > 0); return ResponseEntity.ok(response); } diff --git a/src/main/resources/mapper/bookmark-mapper.xml b/src/main/resources/mapper/bookmark-mapper.xml index b913388..3c0654a 100644 --- a/src/main/resources/mapper/bookmark-mapper.xml +++ b/src/main/resources/mapper/bookmark-mapper.xml @@ -125,9 +125,9 @@ - + INSERT INTO member_saved_link (link_id, member_folder_id, display_title, note, primary_category_id, created_by_member_id) - VALUES (#{linkId}, #{bookmarkDto.memberFolderId}, #{bookmarkDto.displayTitle}, #{bookmarkDto.note}, #{bookmarkDto.primaryCategoryId}, #{bookmarkDto.createdByMemberId}) + VALUES (#{linkId}, #{memberFolderId}, #{displayTitle}, #{note}, #{primaryCategoryId}, #{createdByMemberId}) From a0cec34887b7ad9812ad95270f8c28b51c59375b Mon Sep 17 00:00:00 2001 From: jin2304 Date: Sun, 15 Feb 2026 16:29:38 +0900 Subject: [PATCH 06/11] =?UTF-8?q?refactor/SW-45:=20=EC=BB=A8=EB=B2=A4?= =?UTF-8?q?=EC=85=98=EC=97=90=20=EB=94=B0=EB=9D=BC=20=EB=B6=81=EB=A7=88?= =?UTF-8?q?=ED=81=AC=20=EB=8B=A8=EC=9D=BC/=EB=AA=A9=EB=A1=9D=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EB=A1=9C=EC=A7=81=20=EB=A6=AC=ED=8C=A9=ED=84=B0?= =?UTF-8?q?=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Controller 전용 DTO(BookmarkRequests) 도입 - Service 입력 Command 도입 - 커스텀에러 적용 - 조회 SQL 가독성 및 조건 구조 개선 - 프론트 수정 --- .../controller/BookmarkApiController.java | 17 +++--- .../controller/dto/BookmarkRequests.java | 32 ++++++++++- .../SearchWeb/bookmark/dao/BookmarkDao.java | 4 +- .../bookmark/dao/MybatisBookmarkDao.java | 6 +-- .../bookmark/error/BookmarkErrorCode.java | 3 +- .../bookmark/service/BookmarkService.java | 3 +- .../bookmark/service/BookmarkServiceImpl.java | 19 +++---- .../command/BookmarkSearchCommand.java} | 6 +-- .../mypage/controller/MyPageController.java | 15 +++--- src/main/resources/mapper/bookmark-mapper.xml | 54 ++++++++++++------- .../resources/templates/mypage/myPage.html | 9 ++-- 11 files changed, 107 insertions(+), 61 deletions(-) rename src/main/java/com/web/SearchWeb/bookmark/{dto/request/BookmarkSearchRequestDto.java => service/command/BookmarkSearchCommand.java} (82%) diff --git a/src/main/java/com/web/SearchWeb/bookmark/controller/BookmarkApiController.java b/src/main/java/com/web/SearchWeb/bookmark/controller/BookmarkApiController.java index 6510c06..ba5fb37 100644 --- a/src/main/java/com/web/SearchWeb/bookmark/controller/BookmarkApiController.java +++ b/src/main/java/com/web/SearchWeb/bookmark/controller/BookmarkApiController.java @@ -93,13 +93,11 @@ public ResponseEntity> insertBookmark( * 북마크 목록 조회 */ @GetMapping - public ResponseEntity> selectBookmarkList( + public ResponseEntity>> selectBookmarkList( @AuthenticationPrincipal Object currentUser, - @RequestParam(required = false) Long folderId, - @RequestParam(defaultValue = "Newest") String sort, - @RequestParam(required = false) String query, - @RequestParam(required = false) Long categoryId) { + @ModelAttribute BookmarkRequests.SearchDto searchDto) { + // TODO: AOP 처리 // 로그인 되지 않은 경우 if (currentUser == null || "anonymousUser".equals(currentUser)) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); @@ -110,8 +108,8 @@ public ResponseEntity> selectBookmarkList( return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); } - List bookmarks = bookmarkService.selectBookmarkList(memberId, folderId, sort, query, categoryId); - return ResponseEntity.ok(bookmarks); + List bookmarks = bookmarkService.selectBookmarkList(searchDto.toCommand(memberId)); + return ResponseEntity.ok(ApiResponse.success(bookmarks)); } @@ -119,10 +117,11 @@ public ResponseEntity> selectBookmarkList( * 북마크 단일 조회 */ @GetMapping("/{bookmarkId}") - public ResponseEntity selectBookmark( + public ResponseEntity> selectBookmark( @AuthenticationPrincipal Object currentUser, @PathVariable Long bookmarkId) { + // TODO: AOP 처리 // 로그인 되지 않은 경우 if (currentUser == null || "anonymousUser".equals(currentUser)) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); @@ -134,7 +133,7 @@ public ResponseEntity selectBookmark( } Bookmark bookmark = bookmarkService.selectBookmark(memberId, bookmarkId); - return ResponseEntity.ok(bookmark); + return ResponseEntity.ok(ApiResponse.success(bookmark)); } diff --git a/src/main/java/com/web/SearchWeb/bookmark/controller/dto/BookmarkRequests.java b/src/main/java/com/web/SearchWeb/bookmark/controller/dto/BookmarkRequests.java index b9f8f53..f3bcf96 100644 --- a/src/main/java/com/web/SearchWeb/bookmark/controller/dto/BookmarkRequests.java +++ b/src/main/java/com/web/SearchWeb/bookmark/controller/dto/BookmarkRequests.java @@ -1,5 +1,11 @@ package com.web.SearchWeb.bookmark.controller.dto; +import com.web.SearchWeb.bookmark.service.command.BookmarkSearchCommand; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + /** * Bookmark Controller 전용 Request DTO * - Controller에서만 사용하며, Service 계층에는 개별 파라미터로 전달 @@ -7,7 +13,7 @@ public class BookmarkRequests { /** - * 북마크 생성 요청 + * 북마크 생성 요청 Dto */ public static class CreateDto { public Long bookmarkId; // 북마크 ID (PK, Insert 시 생성된 키 저장용) @@ -19,4 +25,28 @@ public static class CreateDto { public Long createdByMemberId; // 저장한 회원 public String tags; // 태그 문자열 (공백/콤마 구분) } + + + /** + * 북마크 목록 조회 요청 Dto (Search Params) + */ + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class SearchDto { + public Long folderId; + public String sort = "Newest"; // 기본값 설정 + public String query; + public Long categoryId; + + public BookmarkSearchCommand toCommand(Long memberId) { + return BookmarkSearchCommand.builder() + .memberId(memberId) + .folderId(this.folderId) + .sort(this.sort) + .query(this.query) + .categoryId(this.categoryId) + .build(); + } + } } diff --git a/src/main/java/com/web/SearchWeb/bookmark/dao/BookmarkDao.java b/src/main/java/com/web/SearchWeb/bookmark/dao/BookmarkDao.java index 3e18d66..5d2adc2 100644 --- a/src/main/java/com/web/SearchWeb/bookmark/dao/BookmarkDao.java +++ b/src/main/java/com/web/SearchWeb/bookmark/dao/BookmarkDao.java @@ -4,7 +4,7 @@ import com.web.SearchWeb.bookmark.domain.Link; import com.web.SearchWeb.bookmark.dto.BookmarkDto; import com.web.SearchWeb.bookmark.dto.MemberTagResultDto; -import com.web.SearchWeb.bookmark.dto.request.BookmarkSearchRequestDto; +import com.web.SearchWeb.bookmark.service.command.BookmarkSearchCommand; import java.util.List; @@ -19,7 +19,7 @@ public interface BookmarkDao { Bookmark selectBookmark(Long memberId, Long bookmarkId); //북마크 목록 조회 - List selectBookmarkList(BookmarkSearchRequestDto searchRequest); + List selectBookmarkList(BookmarkSearchCommand searchCommand); //북마크 수정 int updateBookmark(BookmarkDto bookmarkDto, Long bookmarkId); diff --git a/src/main/java/com/web/SearchWeb/bookmark/dao/MybatisBookmarkDao.java b/src/main/java/com/web/SearchWeb/bookmark/dao/MybatisBookmarkDao.java index 555cf1e..19feb69 100644 --- a/src/main/java/com/web/SearchWeb/bookmark/dao/MybatisBookmarkDao.java +++ b/src/main/java/com/web/SearchWeb/bookmark/dao/MybatisBookmarkDao.java @@ -4,7 +4,7 @@ import com.web.SearchWeb.bookmark.domain.Link; import com.web.SearchWeb.bookmark.dto.BookmarkDto; import com.web.SearchWeb.bookmark.dto.MemberTagResultDto; -import com.web.SearchWeb.bookmark.dto.request.BookmarkSearchRequestDto; +import com.web.SearchWeb.bookmark.service.command.BookmarkSearchCommand; import org.apache.ibatis.session.SqlSession; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Repository; @@ -53,8 +53,8 @@ public Bookmark selectBookmark(Long memberId, Long bookmarkId) { * 북마크 목록 조회 */ @Override - public List selectBookmarkList(BookmarkSearchRequestDto searchRequest) { - return mapper.selectBookmarkList(searchRequest); + public List selectBookmarkList(BookmarkSearchCommand searchCommand) { + return mapper.selectBookmarkList(searchCommand); } diff --git a/src/main/java/com/web/SearchWeb/bookmark/error/BookmarkErrorCode.java b/src/main/java/com/web/SearchWeb/bookmark/error/BookmarkErrorCode.java index 85830d3..db718f4 100644 --- a/src/main/java/com/web/SearchWeb/bookmark/error/BookmarkErrorCode.java +++ b/src/main/java/com/web/SearchWeb/bookmark/error/BookmarkErrorCode.java @@ -8,7 +8,8 @@ @Getter @RequiredArgsConstructor public enum BookmarkErrorCode implements ErrorCode { - DUPLICATE_BOOKMARK(HttpStatus.CONFLICT, "B001", "이미 존재하는 북마크입니다."); + DUPLICATE_BOOKMARK(HttpStatus.CONFLICT, "B001", "이미 존재하는 북마크입니다."), + BOOKMARK_NOT_FOUND(HttpStatus.NOT_FOUND, "B002", "북마크를 찾을 수 없습니다."); private final HttpStatus status; private final String code; diff --git a/src/main/java/com/web/SearchWeb/bookmark/service/BookmarkService.java b/src/main/java/com/web/SearchWeb/bookmark/service/BookmarkService.java index 5f2a41f..b43d449 100644 --- a/src/main/java/com/web/SearchWeb/bookmark/service/BookmarkService.java +++ b/src/main/java/com/web/SearchWeb/bookmark/service/BookmarkService.java @@ -5,6 +5,7 @@ import com.web.SearchWeb.bookmark.dto.BoardBookmarkCheckDto; import com.web.SearchWeb.bookmark.dto.BookmarkDto; +import com.web.SearchWeb.bookmark.service.command.BookmarkSearchCommand; import java.util.List; @@ -17,7 +18,7 @@ Long insertBookmark(Long memberId, String url, Long memberFolderId, String displ Bookmark selectBookmark(Long memberId, Long bookmarkId); //북마크 목록 조회 - List selectBookmarkList(Long memberId, Long folderId, String sort, String query, Long categoryId); + List selectBookmarkList(BookmarkSearchCommand command); //북마크 수정 int updateBookmark(BookmarkDto bookmarkDto, Long bookmarkId); diff --git a/src/main/java/com/web/SearchWeb/bookmark/service/BookmarkServiceImpl.java b/src/main/java/com/web/SearchWeb/bookmark/service/BookmarkServiceImpl.java index a482e94..1b4e347 100644 --- a/src/main/java/com/web/SearchWeb/bookmark/service/BookmarkServiceImpl.java +++ b/src/main/java/com/web/SearchWeb/bookmark/service/BookmarkServiceImpl.java @@ -5,7 +5,7 @@ import com.web.SearchWeb.bookmark.domain.Link; import com.web.SearchWeb.bookmark.dto.BoardBookmarkCheckDto; import com.web.SearchWeb.bookmark.dto.BookmarkDto; -import com.web.SearchWeb.bookmark.dto.request.BookmarkSearchRequestDto; +import com.web.SearchWeb.bookmark.service.command.BookmarkSearchCommand; import lombok.extern.slf4j.Slf4j; @@ -43,7 +43,11 @@ public BookmarkServiceImpl(BookmarkDao bookmarkDao) { */ @Override public Bookmark selectBookmark(Long memberId, Long bookmarkId) { - return bookmarkDao.selectBookmark(memberId, bookmarkId); + Bookmark bookmark = bookmarkDao.selectBookmark(memberId, bookmarkId); + if (bookmark == null) { + throw BusinessException.from(BookmarkErrorCode.BOOKMARK_NOT_FOUND); + } + return bookmark; } @@ -51,15 +55,8 @@ public Bookmark selectBookmark(Long memberId, Long bookmarkId) { * 북마크 목록 조회 */ @Override - public List selectBookmarkList(Long memberId, Long folderId, String sort, String query, Long categoryId) { - BookmarkSearchRequestDto searchRequest = BookmarkSearchRequestDto.builder() - .memberId(memberId) - .folderId(folderId) - .sort(sort) - .query(query) - .categoryId(categoryId) - .build(); - return bookmarkDao.selectBookmarkList(searchRequest); + public List selectBookmarkList(BookmarkSearchCommand command) { + return bookmarkDao.selectBookmarkList(command); } diff --git a/src/main/java/com/web/SearchWeb/bookmark/dto/request/BookmarkSearchRequestDto.java b/src/main/java/com/web/SearchWeb/bookmark/service/command/BookmarkSearchCommand.java similarity index 82% rename from src/main/java/com/web/SearchWeb/bookmark/dto/request/BookmarkSearchRequestDto.java rename to src/main/java/com/web/SearchWeb/bookmark/service/command/BookmarkSearchCommand.java index c4ffbd8..8ac2ce3 100644 --- a/src/main/java/com/web/SearchWeb/bookmark/dto/request/BookmarkSearchRequestDto.java +++ b/src/main/java/com/web/SearchWeb/bookmark/service/command/BookmarkSearchCommand.java @@ -1,4 +1,4 @@ -package com.web.SearchWeb.bookmark.dto.request; +package com.web.SearchWeb.bookmark.service.command; import lombok.AllArgsConstructor; import lombok.Builder; @@ -6,13 +6,13 @@ import lombok.NoArgsConstructor; /** - * Bookmark 검색 요청 DTO + * Bookmark 검색 요청 Command */ @Getter @NoArgsConstructor @AllArgsConstructor @Builder -public class BookmarkSearchRequestDto { +public class BookmarkSearchCommand { private Long memberId; // created_by_member_id로 필터 private Long folderId; // member_folder_id로 필터 private String sort; // 정렬 기준 (Newest, Oldest) diff --git a/src/main/java/com/web/SearchWeb/mypage/controller/MyPageController.java b/src/main/java/com/web/SearchWeb/mypage/controller/MyPageController.java index 5667495..49ecf89 100644 --- a/src/main/java/com/web/SearchWeb/mypage/controller/MyPageController.java +++ b/src/main/java/com/web/SearchWeb/mypage/controller/MyPageController.java @@ -1,12 +1,15 @@ package com.web.SearchWeb.mypage.controller; import com.web.SearchWeb.aop.OwnerCheck; +import com.web.SearchWeb.bookmark.controller.dto.BookmarkRequests; import com.web.SearchWeb.bookmark.domain.Bookmark; import com.web.SearchWeb.bookmark.dto.BookmarkDto; import com.web.SearchWeb.bookmark.service.BookmarkService; +import com.web.SearchWeb.config.ApiResponse; import com.web.SearchWeb.member.domain.Member; import com.web.SearchWeb.member.dto.MemberUpdateDto; import com.web.SearchWeb.member.service.MemberService; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; @@ -99,13 +102,11 @@ public ResponseEntity> insertBookmark(@PathVariable final Lo */ @GetMapping(value ="/myPage/{memberId}/bookmarks") @OwnerCheck(idParam = "memberId", service = "memberService") - public ResponseEntity> getBookmarks(@PathVariable final Long memberId, - @RequestParam(required = false) String query, - @RequestParam(required = false) Long folderId, - @RequestParam(required = false) Long categoryId, - @RequestParam(defaultValue = "Oldest") String sort) { - List bookmarks = bookmarkService.selectBookmarkList(memberId, folderId, sort, query, categoryId); - return ResponseEntity.ok(bookmarks); + public ResponseEntity>> getBookmarks( + @PathVariable final Long memberId, + @ModelAttribute BookmarkRequests.SearchDto searchDto) { + List bookmarks = bookmarkService.selectBookmarkList(searchDto.toCommand(memberId)); + return ResponseEntity.ok(ApiResponse.success(bookmarks)); } diff --git a/src/main/resources/mapper/bookmark-mapper.xml b/src/main/resources/mapper/bookmark-mapper.xml index 3c0654a..a4c64c1 100644 --- a/src/main/resources/mapper/bookmark-mapper.xml +++ b/src/main/resources/mapper/bookmark-mapper.xml @@ -65,36 +65,50 @@ - + SELECT msl.*, + l.original_url, l.canonical_url, l.domain, l.thumbnail_url, + mt.tag_name + FROM member_saved_link msl + JOIN link l + ON msl.link_id = l.link_id + LEFT JOIN member_saved_link_tag mslt + ON msl.member_saved_link_id = mslt.member_saved_link_id + AND mslt.deleted_at IS NULL + LEFT JOIN member_tag mt + ON mslt.member_tag_id = mt.member_tag_id + AND mt.deleted_at IS NULL + WHERE msl.created_by_member_id = #{memberId} + AND msl.deleted_at IS NULL - AND msl.member_folder_id = #{folderId} + AND msl.member_folder_id = #{folderId} - AND msl.primary_category_id = #{categoryId} + AND msl.primary_category_id = #{categoryId} - AND LOWER(msl.display_title) LIKE '%' || LOWER(#{query}) || '%' + AND LOWER(msl.display_title) LIKE '%' || LOWER(#{query}) || '%' - ORDER BY + ORDER BY msl.updated_at DESC diff --git a/src/main/resources/templates/mypage/myPage.html b/src/main/resources/templates/mypage/myPage.html index 6615970..f5dd0d2 100644 --- a/src/main/resources/templates/mypage/myPage.html +++ b/src/main/resources/templates/mypage/myPage.html @@ -477,9 +477,12 @@

북마크 수정

query: query // 검색어를 서버로 전달 }, success: function(response) { + // ApiResponse 구조 대응 (response.data) + const bookmarks = response.data || []; + // 콜백 함수 실행 (필요한 경우) if (callback) { - callback(response); // 콜백 함수 실행, response를 인자로 전달 + callback(bookmarks); // 콜백 함수 실행, response.data를 인자로 전달 return } @@ -489,12 +492,12 @@

북마크 수정

// 불러온 북마크가 없는 경우 빈 상태 메시지 표시 - if (response.length === 0) { + if (bookmarks.length === 0) { document.getElementById('emptyBookmarkMessage').classList.remove('hidden'); } else { document.getElementById('emptyBookmarkMessage').classList.add('hidden'); // 불러온 북마크를 UI에 추가 - response.forEach(addBookmarkToUI); + bookmarks.forEach(addBookmarkToUI); } From 228f28f3c7a87df3e811a4b8fc531912e8ba73b7 Mon Sep 17 00:00:00 2001 From: jin2304 Date: Sun, 15 Feb 2026 23:49:15 +0900 Subject: [PATCH 07/11] =?UTF-8?q?refactor/SW-45:=20=EC=BB=A8=EB=B2=A4?= =?UTF-8?q?=EC=85=98=EC=97=90=20=EB=94=B0=EB=9D=BC=20=EB=B6=81=EB=A7=88?= =?UTF-8?q?=ED=81=AC=20=EC=88=98=EC=A0=95=20=EB=A1=9C=EC=A7=81=20=EB=A6=AC?= =?UTF-8?q?=ED=8C=A9=ED=84=B0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Controller 전용 DTO(BookmarkRequests) 도입 - 커스텀에러 적용 --- .../controller/BookmarkApiController.java | 27 +++++++++----- .../controller/dto/BookmarkRequests.java | 16 +++++++++ .../SearchWeb/bookmark/dao/BookmarkDao.java | 6 ++-- .../bookmark/dao/MybatisBookmarkDao.java | 14 ++++++-- .../bookmark/service/BookmarkService.java | 3 +- .../bookmark/service/BookmarkServiceImpl.java | 36 +++++++++++++++++-- .../mypage/controller/MyPageController.java | 23 +++++++----- src/main/resources/mapper/bookmark-mapper.xml | 27 +++++++++----- 8 files changed, 119 insertions(+), 33 deletions(-) diff --git a/src/main/java/com/web/SearchWeb/bookmark/controller/BookmarkApiController.java b/src/main/java/com/web/SearchWeb/bookmark/controller/BookmarkApiController.java index ba5fb37..169edfd 100644 --- a/src/main/java/com/web/SearchWeb/bookmark/controller/BookmarkApiController.java +++ b/src/main/java/com/web/SearchWeb/bookmark/controller/BookmarkApiController.java @@ -3,7 +3,6 @@ import com.web.SearchWeb.bookmark.controller.dto.BookmarkRequests; import com.web.SearchWeb.bookmark.domain.Bookmark; -import com.web.SearchWeb.bookmark.dto.BookmarkDto; import com.web.SearchWeb.bookmark.service.BookmarkService; import com.web.SearchWeb.config.ApiResponse; import com.web.SearchWeb.member.dto.CustomOAuth2User; @@ -141,22 +140,32 @@ public ResponseEntity> selectBookmark( * 북마크 수정 */ @PutMapping("/{bookmarkId}") - public ResponseEntity> updateBookmark( + public ResponseEntity> updateBookmark( @AuthenticationPrincipal Object currentUser, @PathVariable Long bookmarkId, - @RequestBody BookmarkDto bookmarkDto) { + @RequestBody BookmarkRequests.UpdateDto request) { - Map response = new HashMap<>(); + // TODO: AOP 처리 + // 로그인 되지 않은 경우 + if (currentUser == null || "anonymousUser".equals(currentUser)) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + } Long memberId = getMemberId(currentUser); if (memberId == null) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).body(response); + return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); } - bookmarkDto.setCreatedByMemberId(memberId); - int result = bookmarkService.updateBookmark(bookmarkDto, bookmarkId); - response.put("success", result > 0); - return ResponseEntity.ok(response); + Long updatedBookmarkId = bookmarkService.updateBookmark( + memberId, + bookmarkId, + request.memberFolderId, + request.displayTitle, + request.note, + request.primaryCategoryId, + request.tags + ); + return ResponseEntity.ok(ApiResponse.success(updatedBookmarkId)); } diff --git a/src/main/java/com/web/SearchWeb/bookmark/controller/dto/BookmarkRequests.java b/src/main/java/com/web/SearchWeb/bookmark/controller/dto/BookmarkRequests.java index f3bcf96..56e248c 100644 --- a/src/main/java/com/web/SearchWeb/bookmark/controller/dto/BookmarkRequests.java +++ b/src/main/java/com/web/SearchWeb/bookmark/controller/dto/BookmarkRequests.java @@ -49,4 +49,20 @@ public BookmarkSearchCommand toCommand(Long memberId) { .build(); } } + + + /** + * 북마크 수정 요청 Dto + */ + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class UpdateDto { + public Long memberFolderId; // 폴더 ID + public String displayTitle; // 표시 제목 + public String note; // 메모 + public Long primaryCategoryId; // 카테고리 ID + public String tags; // 태그 문자열 (공백/콤마 구분) + } + } diff --git a/src/main/java/com/web/SearchWeb/bookmark/dao/BookmarkDao.java b/src/main/java/com/web/SearchWeb/bookmark/dao/BookmarkDao.java index 5d2adc2..a845ffd 100644 --- a/src/main/java/com/web/SearchWeb/bookmark/dao/BookmarkDao.java +++ b/src/main/java/com/web/SearchWeb/bookmark/dao/BookmarkDao.java @@ -2,7 +2,6 @@ import com.web.SearchWeb.bookmark.domain.Bookmark; import com.web.SearchWeb.bookmark.domain.Link; -import com.web.SearchWeb.bookmark.dto.BookmarkDto; import com.web.SearchWeb.bookmark.dto.MemberTagResultDto; import com.web.SearchWeb.bookmark.service.command.BookmarkSearchCommand; @@ -22,7 +21,10 @@ public interface BookmarkDao { List selectBookmarkList(BookmarkSearchCommand searchCommand); //북마크 수정 - int updateBookmark(BookmarkDto bookmarkDto, Long bookmarkId); + int updateBookmark(Bookmark bookmark); + + //북마크 태그 연결 삭제 + int deleteBookmarkTags(Long bookmarkId, Long memberId); //북마크 삭제 int deleteBookmark(Long memberId, Long bookmarkId); diff --git a/src/main/java/com/web/SearchWeb/bookmark/dao/MybatisBookmarkDao.java b/src/main/java/com/web/SearchWeb/bookmark/dao/MybatisBookmarkDao.java index 19feb69..aa11a1a 100644 --- a/src/main/java/com/web/SearchWeb/bookmark/dao/MybatisBookmarkDao.java +++ b/src/main/java/com/web/SearchWeb/bookmark/dao/MybatisBookmarkDao.java @@ -2,7 +2,6 @@ import com.web.SearchWeb.bookmark.domain.Bookmark; import com.web.SearchWeb.bookmark.domain.Link; -import com.web.SearchWeb.bookmark.dto.BookmarkDto; import com.web.SearchWeb.bookmark.dto.MemberTagResultDto; import com.web.SearchWeb.bookmark.service.command.BookmarkSearchCommand; import org.apache.ibatis.session.SqlSession; @@ -89,8 +88,17 @@ public int insertBookmark(Bookmark bookmark) { * 북마크 수정 */ @Override - public int updateBookmark(BookmarkDto bookmarkDto, Long bookmarkId) { - return mapper.updateBookmark(bookmarkDto, bookmarkId); + public int updateBookmark(Bookmark bookmark) { + return mapper.updateBookmark(bookmark); + } + + + /** + * 북마크 태그 연결 삭제 + */ + @Override + public int deleteBookmarkTags(Long bookmarkId, Long memberId) { + return mapper.deleteBookmarkTags(bookmarkId, memberId); } diff --git a/src/main/java/com/web/SearchWeb/bookmark/service/BookmarkService.java b/src/main/java/com/web/SearchWeb/bookmark/service/BookmarkService.java index b43d449..6c5ff5b 100644 --- a/src/main/java/com/web/SearchWeb/bookmark/service/BookmarkService.java +++ b/src/main/java/com/web/SearchWeb/bookmark/service/BookmarkService.java @@ -21,7 +21,8 @@ Long insertBookmark(Long memberId, String url, Long memberFolderId, String displ List selectBookmarkList(BookmarkSearchCommand command); //북마크 수정 - int updateBookmark(BookmarkDto bookmarkDto, Long bookmarkId); + Long updateBookmark(Long memberId, Long bookmarkId, Long memberFolderId, String displayTitle, + String note, Long primaryCategoryId, String tags); //북마크 삭제 int deleteBookmark(Long memberId, Long bookmarkId); diff --git a/src/main/java/com/web/SearchWeb/bookmark/service/BookmarkServiceImpl.java b/src/main/java/com/web/SearchWeb/bookmark/service/BookmarkServiceImpl.java index 1b4e347..9868341 100644 --- a/src/main/java/com/web/SearchWeb/bookmark/service/BookmarkServiceImpl.java +++ b/src/main/java/com/web/SearchWeb/bookmark/service/BookmarkServiceImpl.java @@ -155,8 +155,40 @@ public Long insertBookmark(Long memberId, String url, Long memberFolderId, Strin * 북마크 수정 */ @Override - public int updateBookmark(BookmarkDto bookmarkDto, Long bookmarkId) { - return bookmarkDao.updateBookmark(bookmarkDto, bookmarkId); + @Transactional + public Long updateBookmark(Long memberId, Long bookmarkId, Long memberFolderId, String displayTitle, + String note, Long primaryCategoryId, String tags) { + try { + // 1. Entity 생성 (도메인 생성 로직을 서비스 계층으로 이동) + Bookmark bookmark = Bookmark.builder() + .bookmarkId(bookmarkId) + .memberFolderId(memberFolderId) + .displayTitle(displayTitle) + .note(note) + .primaryCategoryId(primaryCategoryId) + .createdByMemberId(memberId) + .build(); + + // 2. 북마크 기본 정보 수정 + int result = bookmarkDao.updateBookmark(bookmark); + + if (result == 0) { + throw BusinessException.from(BookmarkErrorCode.BOOKMARK_NOT_FOUND); + } + + // 3. 기존 태그 관계 삭제 (Soft Delete) + bookmarkDao.deleteBookmarkTags(bookmarkId, memberId); + + // 4. 새 태그 등록 및 관계 생성/재활성화 + if (tags != null && !tags.isBlank()) { + processAndCreateTags(bookmarkId, memberId, tags); + } + + return bookmarkId; + } catch (DataIntegrityViolationException e) { + log.error("북마크 수정 중 데이터 무결성 위반: bookmarkId={}, memberId={}", bookmarkId, memberId, e); + throw BusinessException.from(BookmarkErrorCode.DUPLICATE_BOOKMARK); + } } diff --git a/src/main/java/com/web/SearchWeb/mypage/controller/MyPageController.java b/src/main/java/com/web/SearchWeb/mypage/controller/MyPageController.java index 49ecf89..879a434 100644 --- a/src/main/java/com/web/SearchWeb/mypage/controller/MyPageController.java +++ b/src/main/java/com/web/SearchWeb/mypage/controller/MyPageController.java @@ -9,7 +9,6 @@ import com.web.SearchWeb.member.domain.Member; import com.web.SearchWeb.member.dto.MemberUpdateDto; import com.web.SearchWeb.member.service.MemberService; -import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; @@ -126,13 +125,21 @@ public ResponseEntity getBookmark(@PathVariable final Long memberId, @ */ @PutMapping("/myPage/{memberId}/bookmark/{bookmarkId}") @OwnerCheck(idParam = "memberId", service = "memberService") - public ResponseEntity> updateBookmark(@PathVariable final Long memberId, - @PathVariable final Long bookmarkId, - @RequestBody BookmarkDto bookmarkDto) { - Map response = new HashMap<>(); - int result = bookmarkService.updateBookmark(bookmarkDto, bookmarkId); - response.put("success", result > 0); - return ResponseEntity.ok(response); + public ResponseEntity> updateBookmark( + @PathVariable final Long memberId, + @PathVariable final Long bookmarkId, + @RequestBody BookmarkRequests.UpdateDto request) { + + Long updatedBookmarkId = bookmarkService.updateBookmark( + memberId, + bookmarkId, + request.memberFolderId, + request.displayTitle, + request.note, + request.primaryCategoryId, + request.tags + ); + return ResponseEntity.ok(ApiResponse.success(updatedBookmarkId)); } diff --git a/src/main/resources/mapper/bookmark-mapper.xml b/src/main/resources/mapper/bookmark-mapper.xml index a4c64c1..9ca2c5d 100644 --- a/src/main/resources/mapper/bookmark-mapper.xml +++ b/src/main/resources/mapper/bookmark-mapper.xml @@ -146,15 +146,25 @@ - + UPDATE member_saved_link SET - display_title = #{bookmarkDto.displayTitle}, - note = #{bookmarkDto.note}, - member_folder_id = #{bookmarkDto.memberFolderId}, - primary_category_id = #{bookmarkDto.primaryCategoryId}, + display_title = #{displayTitle}, + note = #{note}, + member_folder_id = #{memberFolderId}, + primary_category_id = #{primaryCategoryId}, updated_at = now(), - updated_by_member_id = #{bookmarkDto.createdByMemberId} + updated_by_member_id = #{createdByMemberId} + WHERE member_saved_link_id = #{bookmarkId} + AND deleted_at IS NULL + + + + + + UPDATE member_saved_link_tag + SET deleted_at = now(), + deleted_by_member_id = #{memberId} WHERE member_saved_link_id = #{bookmarkId} AND deleted_at IS NULL @@ -213,14 +223,15 @@ AND t.deleted_at IS NULL - + INSERT INTO member_saved_link_tag (member_saved_link_id, member_tag_id) VALUES (#{bookmarkId}, #{tagId}) - ON CONFLICT (member_saved_link_id, member_tag_id) DO NOTHING + ON CONFLICT (member_saved_link_id, member_tag_id) + DO UPDATE SET deleted_at = NULL, updated_at = now() From 152616fb4a6fb7606c90af17a237f7ec9a685184 Mon Sep 17 00:00:00 2001 From: jin2304 Date: Mon, 16 Feb 2026 02:35:49 +0900 Subject: [PATCH 08/11] =?UTF-8?q?refactor/SW-45:=20=EC=BB=A8=EB=B2=A4?= =?UTF-8?q?=EC=85=98=EC=97=90=20=EB=94=B0=EB=9D=BC=20=EB=B6=81=EB=A7=88?= =?UTF-8?q?=ED=81=AC=20=EC=82=AD=EC=A0=9C=20=EB=A1=9C=EC=A7=81=20=EB=A6=AC?= =?UTF-8?q?=ED=8C=A9=ED=84=B0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Controller 전용 DTO(BookmarkRequests) 도입 - 커스텀에러 적용 --- .../controller/BookmarkApiController.java | 19 ++++++++++--------- .../bookmark/service/BookmarkService.java | 2 +- .../bookmark/service/BookmarkServiceImpl.java | 15 +++++++++++++-- .../mypage/controller/MyPageController.java | 8 +++----- 4 files changed, 27 insertions(+), 17 deletions(-) diff --git a/src/main/java/com/web/SearchWeb/bookmark/controller/BookmarkApiController.java b/src/main/java/com/web/SearchWeb/bookmark/controller/BookmarkApiController.java index 169edfd..7b4751c 100644 --- a/src/main/java/com/web/SearchWeb/bookmark/controller/BookmarkApiController.java +++ b/src/main/java/com/web/SearchWeb/bookmark/controller/BookmarkApiController.java @@ -15,9 +15,7 @@ import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.web.bind.annotation.*; -import java.util.HashMap; import java.util.List; -import java.util.Map; /** @@ -173,20 +171,23 @@ public ResponseEntity> updateBookmark( * 북마크 삭제 */ @DeleteMapping("/{bookmarkId}") - public ResponseEntity> deleteBookmark( + public ResponseEntity> deleteBookmark( @AuthenticationPrincipal Object currentUser, @PathVariable Long bookmarkId) { - Map response = new HashMap<>(); - + // TODO: AOP 처리 + // 로그인 되지 않은 경우 + if (currentUser == null || "anonymousUser".equals(currentUser)) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + } + Long memberId = getMemberId(currentUser); if (memberId == null) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).body(response); + return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); } - int result = bookmarkService.deleteBookmark(memberId, bookmarkId); - response.put("success", result > 0); - return ResponseEntity.ok(response); + Long deletedId = bookmarkService.deleteBookmark(memberId, bookmarkId); + return ResponseEntity.ok(ApiResponse.success(deletedId)); } diff --git a/src/main/java/com/web/SearchWeb/bookmark/service/BookmarkService.java b/src/main/java/com/web/SearchWeb/bookmark/service/BookmarkService.java index 6c5ff5b..256235c 100644 --- a/src/main/java/com/web/SearchWeb/bookmark/service/BookmarkService.java +++ b/src/main/java/com/web/SearchWeb/bookmark/service/BookmarkService.java @@ -25,7 +25,7 @@ Long updateBookmark(Long memberId, Long bookmarkId, Long memberFolderId, String String note, Long primaryCategoryId, String tags); //북마크 삭제 - int deleteBookmark(Long memberId, Long bookmarkId); + Long deleteBookmark(Long memberId, Long bookmarkId); // 링크 조회 또는 생성 (URL 정규화) Link getOrCreateLink(String url, Long createdByMemberId); diff --git a/src/main/java/com/web/SearchWeb/bookmark/service/BookmarkServiceImpl.java b/src/main/java/com/web/SearchWeb/bookmark/service/BookmarkServiceImpl.java index 9868341..f2badb2 100644 --- a/src/main/java/com/web/SearchWeb/bookmark/service/BookmarkServiceImpl.java +++ b/src/main/java/com/web/SearchWeb/bookmark/service/BookmarkServiceImpl.java @@ -196,8 +196,19 @@ public Long updateBookmark(Long memberId, Long bookmarkId, Long memberFolderId, * 북마크 삭제 (soft delete) */ @Override - public int deleteBookmark(Long memberId, Long bookmarkId) { - return bookmarkDao.deleteBookmark(memberId, bookmarkId); + @Transactional + public Long deleteBookmark(Long memberId, Long bookmarkId) { + // 1. 북마크 삭제 (Soft Delete) + int result = bookmarkDao.deleteBookmark(memberId, bookmarkId); + + if (result == 0) { + throw BusinessException.from(BookmarkErrorCode.BOOKMARK_NOT_FOUND); + } + + // 2. 관련 태그 관계 삭제 (Soft Delete) + bookmarkDao.deleteBookmarkTags(bookmarkId, memberId); + + return bookmarkId; } diff --git a/src/main/java/com/web/SearchWeb/mypage/controller/MyPageController.java b/src/main/java/com/web/SearchWeb/mypage/controller/MyPageController.java index 879a434..215a849 100644 --- a/src/main/java/com/web/SearchWeb/mypage/controller/MyPageController.java +++ b/src/main/java/com/web/SearchWeb/mypage/controller/MyPageController.java @@ -148,10 +148,8 @@ public ResponseEntity> updateBookmark( */ @DeleteMapping("/myPage/{memberId}/bookmark/{bookmarkId}") @OwnerCheck(idParam = "memberId", service = "memberService") - public ResponseEntity> deleteBookmark(@PathVariable final Long memberId, @PathVariable final Long bookmarkId) { - Map response = new HashMap<>(); - int result = bookmarkService.deleteBookmark(memberId, bookmarkId); - response.put("success", result > 0); - return ResponseEntity.ok(response); + public ResponseEntity> deleteBookmark(@PathVariable final Long memberId, @PathVariable final Long bookmarkId) { + Long deletedId = bookmarkService.deleteBookmark(memberId, bookmarkId); + return ResponseEntity.ok(ApiResponse.success(deletedId)); } } From bef39e4dd7ece1bb92dc40e6cba4d49604a728de Mon Sep 17 00:00:00 2001 From: jin2304 Date: Mon, 16 Feb 2026 18:58:21 +0900 Subject: [PATCH 09/11] =?UTF-8?q?refactor/SW-45:=20=EB=B6=81=EB=A7=88?= =?UTF-8?q?=ED=81=AC=20=EB=8F=84=EB=A9=94=EC=9D=B8=20=EB=A9=94=EC=84=9C?= =?UTF-8?q?=EB=93=9C=20=EC=88=9C=EC=84=9C=20=EC=A1=B0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/BookmarkApiController.java | 72 ++++----- .../SearchWeb/bookmark/dao/BookmarkDao.java | 12 +- .../bookmark/dao/MybatisBookmarkDao.java | 67 +++++---- .../bookmark/service/BookmarkServiceImpl.java | 142 +++++++++--------- 4 files changed, 149 insertions(+), 144 deletions(-) diff --git a/src/main/java/com/web/SearchWeb/bookmark/controller/BookmarkApiController.java b/src/main/java/com/web/SearchWeb/bookmark/controller/BookmarkApiController.java index 7b4751c..996db6a 100644 --- a/src/main/java/com/web/SearchWeb/bookmark/controller/BookmarkApiController.java +++ b/src/main/java/com/web/SearchWeb/bookmark/controller/BookmarkApiController.java @@ -33,24 +33,6 @@ public BookmarkApiController(BookmarkService bookmarkService) { this.bookmarkService = bookmarkService; } - /** - * 북마크 확인 - */ - @GetMapping("/check") - public ResponseEntity checkBookmark(@AuthenticationPrincipal Object currentUser, @RequestParam String url) { - // 로그인 되지 않은 경우 - if (currentUser == null || "anonymousUser".equals(currentUser)) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); - } - - Long memberId = getMemberId(currentUser); - - // 북마크 존재 여부 확인 - boolean exists = bookmarkService.checkBookmarkExistsByUrl(memberId, url); - return ResponseEntity.ok(exists); - } - - /** * 북마크 추가 */ @@ -87,50 +69,50 @@ public ResponseEntity> insertBookmark( /** - * 북마크 목록 조회 + * 북마크 단일 조회 */ - @GetMapping - public ResponseEntity>> selectBookmarkList( + @GetMapping("/{bookmarkId}") + public ResponseEntity> selectBookmark( @AuthenticationPrincipal Object currentUser, - @ModelAttribute BookmarkRequests.SearchDto searchDto) { - + @PathVariable Long bookmarkId) { + // TODO: AOP 처리 // 로그인 되지 않은 경우 if (currentUser == null || "anonymousUser".equals(currentUser)) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } - + Long memberId = getMemberId(currentUser); if (memberId == null) { return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); } - - List bookmarks = bookmarkService.selectBookmarkList(searchDto.toCommand(memberId)); - return ResponseEntity.ok(ApiResponse.success(bookmarks)); + + Bookmark bookmark = bookmarkService.selectBookmark(memberId, bookmarkId); + return ResponseEntity.ok(ApiResponse.success(bookmark)); } /** - * 북마크 단일 조회 + * 북마크 목록 조회 */ - @GetMapping("/{bookmarkId}") - public ResponseEntity> selectBookmark( + @GetMapping + public ResponseEntity>> selectBookmarkList( @AuthenticationPrincipal Object currentUser, - @PathVariable Long bookmarkId) { - + @ModelAttribute BookmarkRequests.SearchDto searchDto) { + // TODO: AOP 처리 // 로그인 되지 않은 경우 if (currentUser == null || "anonymousUser".equals(currentUser)) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } - + Long memberId = getMemberId(currentUser); if (memberId == null) { return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); } - - Bookmark bookmark = bookmarkService.selectBookmark(memberId, bookmarkId); - return ResponseEntity.ok(ApiResponse.success(bookmark)); + + List bookmarks = bookmarkService.selectBookmarkList(searchDto.toCommand(memberId)); + return ResponseEntity.ok(ApiResponse.success(bookmarks)); } @@ -191,6 +173,24 @@ public ResponseEntity> deleteBookmark( } + /** + * 북마크 확인 + */ + @GetMapping("/check") + public ResponseEntity checkBookmark(@AuthenticationPrincipal Object currentUser, @RequestParam String url) { + // 로그인 되지 않은 경우 + if (currentUser == null || "anonymousUser".equals(currentUser)) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + } + + Long memberId = getMemberId(currentUser); + + // 북마크 존재 여부 확인 + boolean exists = bookmarkService.checkBookmarkExistsByUrl(memberId, url); + return ResponseEntity.ok(exists); + } + + /** * 현재 사용자의 memberId 추출 */ diff --git a/src/main/java/com/web/SearchWeb/bookmark/dao/BookmarkDao.java b/src/main/java/com/web/SearchWeb/bookmark/dao/BookmarkDao.java index a845ffd..066ad26 100644 --- a/src/main/java/com/web/SearchWeb/bookmark/dao/BookmarkDao.java +++ b/src/main/java/com/web/SearchWeb/bookmark/dao/BookmarkDao.java @@ -8,9 +8,6 @@ import java.util.List; public interface BookmarkDao { - //북마크 링크 중복 확인 (동일 폴더에 동일 링크) - int checkBookmarkExists(Long memberId, Long folderId, Long linkId); - //북마크 추가 int insertBookmark(Bookmark bookmark); @@ -23,15 +20,18 @@ public interface BookmarkDao { //북마크 수정 int updateBookmark(Bookmark bookmark); - //북마크 태그 연결 삭제 - int deleteBookmarkTags(Long bookmarkId, Long memberId); - //북마크 삭제 int deleteBookmark(Long memberId, Long bookmarkId); + //북마크 태그 연결 삭제 + int deleteBookmarkTags(Long bookmarkId, Long memberId); + //북마크 삭제 (Link ID 기반 - soft delete) int deleteBookmarkByLink(Long memberId, Long linkId); + //북마크 링크 중복 확인 (동일 폴더에 동일 링크) + int checkBookmarkExists(Long memberId, Long folderId, Long linkId); + //링크 조회 (url로) Link selectLinkByUrl(String url); diff --git a/src/main/java/com/web/SearchWeb/bookmark/dao/MybatisBookmarkDao.java b/src/main/java/com/web/SearchWeb/bookmark/dao/MybatisBookmarkDao.java index aa11a1a..221cdbc 100644 --- a/src/main/java/com/web/SearchWeb/bookmark/dao/MybatisBookmarkDao.java +++ b/src/main/java/com/web/SearchWeb/bookmark/dao/MybatisBookmarkDao.java @@ -22,20 +22,11 @@ public MybatisBookmarkDao(SqlSession sqlSession) { /** - * 북마크 중복 확인 - */ - @Override - public int checkBookmarkExists(Long memberId, Long folderId, Long linkId) { - return mapper.checkBookmarkExists(memberId, folderId, linkId); - } - - - /** - * URL 기반 북마크 존재 여부 확인 (Board Bridge용) + * 북마크 추가 */ @Override - public int checkBookmarkExistsByUrl(Long memberId, String url) { - return mapper.checkBookmarkExistsByUrl(memberId, url); + public int insertBookmark(Bookmark bookmark) { + return mapper.insertBookmark(bookmark); } @@ -58,64 +49,74 @@ public List selectBookmarkList(BookmarkSearchCommand searchCommand) { /** - * 링크 조회 (url로) + * 북마크 수정 */ @Override - public Link selectLinkByUrl(String url) { - return mapper.selectLinkByUrl(url); + public int updateBookmark(Bookmark bookmark) { + return mapper.updateBookmark(bookmark); } /** - * 링크 추가 + * 북마크 삭제 (soft delete) */ @Override - public int insertLink(Link link) { - return mapper.insertLink(link); + public int deleteBookmark(Long memberId, Long bookmarkId) { + return mapper.deleteBookmark(memberId, bookmarkId); } /** - * 북마크 추가 + * 북마크 태그 연결 삭제 */ @Override - public int insertBookmark(Bookmark bookmark) { - return mapper.insertBookmark(bookmark); + public int deleteBookmarkTags(Long bookmarkId, Long memberId) { + return mapper.deleteBookmarkTags(bookmarkId, memberId); } /** - * 북마크 수정 + * 북마크 삭제 (Link ID 기반 - soft delete) */ @Override - public int updateBookmark(Bookmark bookmark) { - return mapper.updateBookmark(bookmark); + public int deleteBookmarkByLink(Long memberId, Long linkId) { + return mapper.deleteBookmarkByLink(memberId, linkId); } /** - * 북마크 태그 연결 삭제 + * 북마크 중복 확인 */ @Override - public int deleteBookmarkTags(Long bookmarkId, Long memberId) { - return mapper.deleteBookmarkTags(bookmarkId, memberId); + public int checkBookmarkExists(Long memberId, Long folderId, Long linkId) { + return mapper.checkBookmarkExists(memberId, folderId, linkId); } /** - * 북마크 삭제 (soft delete) + * URL 기반 북마크 존재 여부 확인 (Board Bridge용) */ @Override - public int deleteBookmark(Long memberId, Long bookmarkId) { - return mapper.deleteBookmark(memberId, bookmarkId); + public int checkBookmarkExistsByUrl(Long memberId, String url) { + return mapper.checkBookmarkExistsByUrl(memberId, url); } + /** - * 북마크 삭제 (Link ID 기반 - soft delete) + * 링크 조회 (url로) */ @Override - public int deleteBookmarkByLink(Long memberId, Long linkId) { - return mapper.deleteBookmarkByLink(memberId, linkId); + public Link selectLinkByUrl(String url) { + return mapper.selectLinkByUrl(url); + } + + + /** + * 링크 추가 + */ + @Override + public int insertLink(Link link) { + return mapper.insertLink(link); } diff --git a/src/main/java/com/web/SearchWeb/bookmark/service/BookmarkServiceImpl.java b/src/main/java/com/web/SearchWeb/bookmark/service/BookmarkServiceImpl.java index f2badb2..8758d33 100644 --- a/src/main/java/com/web/SearchWeb/bookmark/service/BookmarkServiceImpl.java +++ b/src/main/java/com/web/SearchWeb/bookmark/service/BookmarkServiceImpl.java @@ -38,74 +38,6 @@ public BookmarkServiceImpl(BookmarkDao bookmarkDao) { } - /** - * 북마크 단일 조회 - */ - @Override - public Bookmark selectBookmark(Long memberId, Long bookmarkId) { - Bookmark bookmark = bookmarkDao.selectBookmark(memberId, bookmarkId); - if (bookmark == null) { - throw BusinessException.from(BookmarkErrorCode.BOOKMARK_NOT_FOUND); - } - return bookmark; - } - - - /** - * 북마크 목록 조회 - */ - @Override - public List selectBookmarkList(BookmarkSearchCommand command) { - return bookmarkDao.selectBookmarkList(command); - } - - - /** - * 링크 조회 또는 생성 (URL 정규화) - */ - @Override - @Transactional - public Link getOrCreateLink(String url, Long createdByMemberId) { - // URL 정규화 (canonical URL 생성) - String canonicalUrl = normalizeUrl(url); - - // 기존 링크 조회 - Link existingLink = bookmarkDao.selectLinkByUrl(url); - if (existingLink != null) { - return existingLink; - } - - // 새 링크 생성 - Link newLink = Link.builder() - .canonicalUrl(canonicalUrl) - .originalUrl(url) - .domain(extractDomain(url)) - .title(url) // 기본값, 나중에 메타데이터 추출로 업데이트 - .primaryCategoryId(1L) // 기본 카테고리 - .createdByMemberId(createdByMemberId) - .build(); - - bookmarkDao.insertLink(newLink); - return newLink; - } - - - /** - * 북마크 존재 여부 확인 (URL 기반) - */ - @Override - public boolean checkBookmarkExistsByUrl(Long memberId, String url) { - String canonicalUrl = normalizeUrl(url); - // Link가 존재하는지 먼저 확인 (최적화) - Link link = bookmarkDao.selectLinkByUrl(url); - if (link == null) { - return false; - } - // Link ID로 북마크 테이블 조회 - return bookmarkDao.checkBookmarkExistsByUrl(memberId, url) > 0; - } - - /** * 북마크 추가 */ @@ -151,6 +83,29 @@ public Long insertBookmark(Long memberId, String url, Long memberFolderId, Strin } } + + /** + * 북마크 단일 조회 + */ + @Override + public Bookmark selectBookmark(Long memberId, Long bookmarkId) { + Bookmark bookmark = bookmarkDao.selectBookmark(memberId, bookmarkId); + if (bookmark == null) { + throw BusinessException.from(BookmarkErrorCode.BOOKMARK_NOT_FOUND); + } + return bookmark; + } + + + /** + * 북마크 목록 조회 + */ + @Override + public List selectBookmarkList(BookmarkSearchCommand command) { + return bookmarkDao.selectBookmarkList(command); + } + + /** * 북마크 수정 */ @@ -212,6 +167,55 @@ public Long deleteBookmark(Long memberId, Long bookmarkId) { } + + + // ========== Helper Methods ========== + /** + * 링크 조회 또는 생성 (URL 정규화) + */ + @Override + @Transactional + public Link getOrCreateLink(String url, Long createdByMemberId) { + // URL 정규화 (canonical URL 생성) + String canonicalUrl = normalizeUrl(url); + + // 기존 링크 조회 + Link existingLink = bookmarkDao.selectLinkByUrl(url); + if (existingLink != null) { + return existingLink; + } + + // 새 링크 생성 + Link newLink = Link.builder() + .canonicalUrl(canonicalUrl) + .originalUrl(url) + .domain(extractDomain(url)) + .title(url) // 기본값, 나중에 메타데이터 추출로 업데이트 + .primaryCategoryId(1L) // 기본 카테고리 + .createdByMemberId(createdByMemberId) + .build(); + + bookmarkDao.insertLink(newLink); + return newLink; + } + + + /** + * 북마크 존재 여부 확인 (URL 기반) + */ + @Override + public boolean checkBookmarkExistsByUrl(Long memberId, String url) { + String canonicalUrl = normalizeUrl(url); + // Link가 존재하는지 먼저 확인 (최적화) + Link link = bookmarkDao.selectLinkByUrl(url); + if (link == null) { + return false; + } + // Link ID로 북마크 테이블 조회 + return bookmarkDao.checkBookmarkExistsByUrl(memberId, url) > 0; + } + + /** * URL 정규화 (canonical URL 생성) */ @@ -280,7 +284,7 @@ private void processAndCreateTags(Long bookmarkId, Long memberId, String tags) { } } - + // ========== Legacy Board-Bookmark Methods ========== /** From 503c2630bf890c74f86b76ba59d6efe2428bcd9b Mon Sep 17 00:00:00 2001 From: jin2304 Date: Tue, 17 Feb 2026 02:25:06 +0900 Subject: [PATCH 10/11] =?UTF-8?q?refactor/SW-45:=20=EB=B6=81=EB=A7=88?= =?UTF-8?q?=ED=81=AC=20=EC=88=98=EC=A0=95=20=EC=8B=9C=20=ED=83=9C=EA=B7=B8?= =?UTF-8?q?=20=EC=9C=A0=EC=8B=A4=20=EB=B0=A9=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../bookmark/service/BookmarkServiceImpl.java | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/web/SearchWeb/bookmark/service/BookmarkServiceImpl.java b/src/main/java/com/web/SearchWeb/bookmark/service/BookmarkServiceImpl.java index 8758d33..d570af1 100644 --- a/src/main/java/com/web/SearchWeb/bookmark/service/BookmarkServiceImpl.java +++ b/src/main/java/com/web/SearchWeb/bookmark/service/BookmarkServiceImpl.java @@ -131,12 +131,15 @@ public Long updateBookmark(Long memberId, Long bookmarkId, Long memberFolderId, throw BusinessException.from(BookmarkErrorCode.BOOKMARK_NOT_FOUND); } - // 3. 기존 태그 관계 삭제 (Soft Delete) - bookmarkDao.deleteBookmarkTags(bookmarkId, memberId); - - // 4. 새 태그 등록 및 관계 생성/재활성화 - if (tags != null && !tags.isBlank()) { - processAndCreateTags(bookmarkId, memberId, tags); + // 3. 태그 수정 + if (tags != null) { + // 기존 태그 관계 삭제 (Soft Delete) + bookmarkDao.deleteBookmarkTags(bookmarkId, memberId); + + // 새 태그 등록 및 관계 생성/재활성화 (빈 문자열이면 모든 태그 제거) + if (!tags.isBlank()) { + processAndCreateTags(bookmarkId, memberId, tags); + } } return bookmarkId; From 7877f9bafbf2c2aeff72b07fcc3a15eb187aa60f Mon Sep 17 00:00:00 2001 From: jin2304 Date: Tue, 17 Feb 2026 02:25:37 +0900 Subject: [PATCH 11/11] =?UTF-8?q?refactor/SW-45:=20=EA=B7=B8=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D=20=EC=8B=9C=20PostgreSQL=20RETURNING=EC=9D=84=20?= =?UTF-8?q?=ED=99=9C=EC=9A=A9=ED=95=98=EC=97=AC=20=EC=A4=91=EB=B3=B5=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EB=B0=8F=20UNION=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/mapper/bookmark-mapper.xml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/main/resources/mapper/bookmark-mapper.xml b/src/main/resources/mapper/bookmark-mapper.xml index 9ca2c5d..68ae781 100644 --- a/src/main/resources/mapper/bookmark-mapper.xml +++ b/src/main/resources/mapper/bookmark-mapper.xml @@ -215,12 +215,6 @@ RETURNING member_tag_id, tag_name ) SELECT member_tag_id as "memberTagId", tag_name as "tagName" FROM inserted_tags - UNION - SELECT t.member_tag_id as "memberTagId", t.tag_name as "tagName" - FROM member_tag t - JOIN input_tags i ON t.tag_name = i.tag_name - WHERE t.owner_member_id = #{memberId} - AND t.deleted_at IS NULL