Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
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;
Expand All @@ -14,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;


/**
Expand All @@ -34,142 +33,161 @@ public BookmarkApiController(BookmarkService bookmarkService) {
this.bookmarkService = bookmarkService;
}

/**
* 북마크 확인
*/
@GetMapping("/check")
public ResponseEntity<Boolean> 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);
}


/**
* 북마크 추가
*/
@PostMapping
public ResponseEntity<Map<String, Object>> insertBookmark(
public ResponseEntity<ApiResponse<Long>> insertBookmark(
@AuthenticationPrincipal Object currentUser,
@RequestBody BookmarkDto bookmarkDto,
@RequestParam String url) {

Map<String, Object> 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,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate URL in create requests before calling service

Moving URL from required query param to request.url means create calls can reach the service with null/blank URL, which is no longer rejected at request binding time. In that case insertBookmark proceeds to link creation and can fail with a server-side data error (the link table requires non-null URL columns), returning a 5xx instead of a client 4xx validation error.

Useful? React with 👍 / 👎.

request.memberFolderId,
request.displayTitle,
request.note,
request.primaryCategoryId,
request.tags
);

return ResponseEntity
.status(HttpStatus.CREATED)
.body(ApiResponse.success(bookmarkId));
}


/**
* 북마크 목록 조회
* 북마크 단일 조회
*/
@GetMapping
public ResponseEntity<List<Bookmark>> selectBookmarkList(
@GetMapping("/{bookmarkId}")
public ResponseEntity<ApiResponse<Bookmark>> selectBookmark(
@AuthenticationPrincipal Object currentUser,
@RequestParam(required = false) Long folderId,
@RequestParam(defaultValue = "Newest") String sort,
@RequestParam(required = false) String query,
@RequestParam(required = false) Long categoryId) {

@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<Bookmark> bookmarks = bookmarkService.selectBookmarkList(memberId, folderId, sort, query, categoryId);
return ResponseEntity.ok(bookmarks);

Bookmark bookmark = bookmarkService.selectBookmark(memberId, bookmarkId);
return ResponseEntity.ok(ApiResponse.success(bookmark));
}


/**
* 북마크 단일 조회
* 북마크 목록 조회
*/
@GetMapping("/{bookmarkId}")
public ResponseEntity<Bookmark> selectBookmark(
@GetMapping
public ResponseEntity<ApiResponse<List<Bookmark>>> 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(bookmark);
List<Bookmark> bookmarks = bookmarkService.selectBookmarkList(searchDto.toCommand(memberId));
return ResponseEntity.ok(ApiResponse.success(bookmarks));
}


/**
* 북마크 수정
*/
@PutMapping("/{bookmarkId}")
public ResponseEntity<Map<String, Object>> updateBookmark(
public ResponseEntity<ApiResponse<Long>> updateBookmark(
@AuthenticationPrincipal Object currentUser,
@PathVariable Long bookmarkId,
@RequestBody BookmarkDto bookmarkDto) {
@RequestBody BookmarkRequests.UpdateDto request) {

Map<String, Object> 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));
}


/**
* 북마크 삭제
*/
@DeleteMapping("/{bookmarkId}")
public ResponseEntity<Map<String, Object>> deleteBookmark(
public ResponseEntity<ApiResponse<Long>> deleteBookmark(
@AuthenticationPrincipal Object currentUser,
@PathVariable Long bookmarkId) {

Map<String, Object> 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));
}


/**
* 북마크 확인
*/
@GetMapping("/check")
public ResponseEntity<Boolean> 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);
}


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
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 계층에는 개별 파라미터로 전달
*/
public class BookmarkRequests {

/**
* 북마크 생성 요청 Dto
*/
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; // 태그 문자열 (공백/콤마 구분)
}


/**
* 북마크 목록 조회 요청 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();
}
}


/**
* 북마크 수정 요청 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; // 태그 문자열 (공백/콤마 구분)
}

}
31 changes: 20 additions & 11 deletions src/main/java/com/web/SearchWeb/bookmark/dao/BookmarkDao.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,39 +2,48 @@

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.request.BookmarkSearchRequestDto;
import com.web.SearchWeb.bookmark.dto.MemberTagResultDto;
import com.web.SearchWeb.bookmark.service.command.BookmarkSearchCommand;

import java.util.List;

public interface BookmarkDao {
//북마크 링크 중복 확인 (동일 폴더에 동일 링크)
int checkBookmarkExists(Long memberId, Long folderId, Long linkId);

//북마크 추가
int insertBookmark(BookmarkDto bookmark, Long linkId);
int insertBookmark(Bookmark bookmark);

//북마크 단일 조회
Bookmark selectBookmark(Long memberId, Long bookmarkId);

//북마크 목록 조회
List<Bookmark> selectBookmarkList(BookmarkSearchRequestDto searchRequest);
List<Bookmark> selectBookmarkList(BookmarkSearchCommand searchCommand);

//북마크 수정
int updateBookmark(BookmarkDto bookmarkDto, Long bookmarkId);
int updateBookmark(Bookmark bookmark);

//북마크 삭제
int deleteBookmark(Long memberId, Long bookmarkId);

//북마크 태그 연결 삭제
int deleteBookmarkTags(Long bookmarkId, Long memberId);

//북마크 삭제 (Link ID 기반 - soft delete)
int deleteBookmarkByLink(Long memberId, Long linkId);

//링크 조회 (canonical_url로)
Link selectLinkByCanonicalUrl(String canonicalUrl);
//북마크 링크 중복 확인 (동일 폴더에 동일 링크)
int checkBookmarkExists(Long memberId, Long folderId, Long linkId);

//링크 조회 (url로)
Link selectLinkByUrl(String url);

//링크 추가 (link 테이블)
int insertLink(Link link);

//URL 기반 북마크 존재 여부 확인 (Board Bridge용)
int checkBookmarkExistsByUrl(Long memberId, String url);

// 태그 등록 및 조회 (Insert & Select)
List<MemberTagResultDto> insertAndSelectTags(Long memberId, List<String> tagNames);

// 북마크-태그 연결 일괄 추가 (Bulk Insert)
int insertBookmarkTags(Long bookmarkId, List<Long> tagIds);
Comment on lines +44 to +48
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check if `@Param` is used elsewhere in DAO interfaces and if mybatis config enables useActualParamName

echo "=== Checking `@Param` usage in DAO interfaces ==="
rg -n '@Param' --type=java -g '*Dao.java' -A1

echo ""
echo "=== Checking if any *Dao.java files exist and their method signatures ==="
fd '*Dao.java' -x cat -n {}

echo ""
echo "=== Checking MyBatis configuration in application properties ==="
fd -e yml -e yaml -e properties | xargs grep -i 'mybatis\|actual-param' 2>/dev/null | head -20

echo ""
echo "=== Checking pom.xml for compiler configuration ==="
fd 'pom.xml' -x cat {} | grep -A10 'maven-compiler-plugin\|mybatis-spring-boot' | head -30

echo ""
echo "=== Checking build.gradle if exists ==="
fd 'build.gradle' -x cat {} 2>/dev/null | head -50

Repository: Searchweb-Dev/Searchweb-Back

Length of output: 3094


🏁 Script executed:

#!/bin/bash
echo "=== BookmarkDao.java current state ==="
fd 'BookmarkDao.java' -x cat -n {}

echo ""
echo "=== BookmarkDao mapper XML file ==="
fd 'BookmarkDao.xml' -o 'BookmarkMapper.xml' -x cat -n {}

Repository: Searchweb-Dev/Searchweb-Back

Length of output: 6044


🏁 Script executed:

#!/bin/bash
echo "=== Searching for BookmarkDao mapper XML ==="
fd -e xml | grep -i bookmark

echo ""
echo "=== Checking mapper directory contents ==="
fd -e xml -type f | head -20

echo ""
echo "=== Looking for parameter usage in bookmark mapper XML ==="
fd -e xml | xargs grep -l 'insertAndSelectTags\|insertBookmarkTags' 2>/dev/null

Repository: Searchweb-Dev/Searchweb-Back

Length of output: 500


🏁 Script executed:

#!/bin/bash
echo "=== Reading bookmark-mapper.xml ==="
cat -n src/main/resources/mapper/bookmark-mapper.xml | grep -A15 -B2 'insertAndSelectTags\|insertBookmarkTags'

Repository: Searchweb-Dev/Searchweb-Back

Length of output: 1415


🏁 Script executed:

#!/bin/bash
echo "=== Full application.properties content ==="
cat -n src/main/resources/application.properties

echo ""
echo "=== Checking other DAO interfaces for `@Param` patterns ==="
fd 'Dao.java' --type f -x grep -l 'interface' {} \; | xargs -I {} sh -c 'echo "=== {} ===" && head -50 {}'

echo ""
echo "=== Checking if build.gradle has -parameters flag ==="
grep -i 'parameters\|compilerArgs' build.gradle

Repository: Searchweb-Dev/Searchweb-Back

Length of output: 9316


새 메서드에 @Param 어노테이션 추가가 필요합니다.

프로젝트의 다른 DAO인 BoardDao.java에서 다중 파라미터 메서드에 명시적으로 @Param 어노테이션을 사용하고 있으며, 현재 application.properties에 mybatis.configuration.use-actual-param-name 설정이 없고 build.gradle에 -parameters 컴파일 옵션이 없습니다. 따라서 insertAndSelectTagsinsertBookmarkTags 메서드에서 MyBatis 파라미터 바인딩이 실패할 수 있습니다.

🛡️ 권장 수정
     // 태그 등록 및 조회 (Insert & Select)
-    List<MemberTagResultDto> insertAndSelectTags(Long memberId, List<String> tagNames);
+    List<MemberTagResultDto> insertAndSelectTags(`@Param`("memberId") Long memberId, `@Param`("tagNames") List<String> tagNames);

     // 북마크-태그 연결 일괄 추가 (Bulk Insert)
-    int insertBookmarkTags(Long bookmarkId, List<Long> tagIds);
+    int insertBookmarkTags(`@Param`("bookmarkId") Long bookmarkId, `@Param`("tagIds") List<Long> tagIds);
🤖 Prompt for AI Agents
In `@src/main/java/com/web/SearchWeb/bookmark/dao/BookmarkDao.java` around lines
42 - 46, The new DAO methods insertAndSelectTags(Long memberId, List<String>
tagNames) and insertBookmarkTags(Long bookmarkId, List<Long> tagIds) need
explicit MyBatis `@Param` annotations on their parameters to ensure proper
parameter binding; update the method signatures in BookmarkDao (add
`@Param`("memberId") and `@Param`("tagNames") to insertAndSelectTags, and
`@Param`("bookmarkId") and `@Param`("tagIds") to insertBookmarkTags) so MyBatis can
resolve the parameters without relying on -parameters compilation or
use-actual-param-name settings.

}
Loading