Skip to content
Open
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
Expand Up @@ -4,7 +4,7 @@
import com.web.SearchWeb.bookmark.controller.dto.BookmarkRequests;
import com.web.SearchWeb.bookmark.domain.Bookmark;
import com.web.SearchWeb.bookmark.service.BookmarkService;
import com.web.SearchWeb.config.ApiResponse;
import com.web.SearchWeb.config.common.ApiResponse;
import com.web.SearchWeb.member.dto.CustomOAuth2User;
import com.web.SearchWeb.member.dto.CustomUserDetails;
import org.springframework.beans.factory.annotation.Autowired;
Expand Down
3 changes: 3 additions & 0 deletions src/main/java/com/web/SearchWeb/bookmark/dao/BookmarkDao.java
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,7 @@ public interface BookmarkDao {

// 북마크-태그 연결 일괄 추가 (Bulk Insert)
int insertBookmarkTags(Long bookmarkId, List<Long> tagIds);

// 폴더 내 활성 북마크 존재 여부
boolean existsActiveBookmarkInFolder(Long memberFolderId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -136,4 +136,9 @@ public List<MemberTagResultDto> insertAndSelectTags(Long memberId, List<String>
public int insertBookmarkTags(Long bookmarkId, List<Long> tagIds) {
return mapper.insertBookmarkTags(bookmarkId, tagIds);
}

@Override
public boolean existsActiveBookmarkInFolder(Long memberFolderId) {
return mapper.existsActiveBookmarkInFolder(memberFolderId);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.web.SearchWeb.bookmark.error;

import com.web.SearchWeb.config.ErrorCode;
import com.web.SearchWeb.config.exception.ErrorCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
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.config.exception.BusinessException;
import com.web.SearchWeb.config.exception.CommonErrorCode;

import com.web.SearchWeb.bookmark.dto.MemberTagResultDto;
import java.net.URI;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.web.SearchWeb.config;
package com.web.SearchWeb.config.common;

import com.web.SearchWeb.config.exception.ErrorCode;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.web.SearchWeb.config;
package com.web.SearchWeb.config.exception;

import lombok.Getter;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.web.SearchWeb.config;
package com.web.SearchWeb.config.exception;

import lombok.Getter;
import lombok.RequiredArgsConstructor;
Expand All @@ -7,6 +7,7 @@
@Getter
@RequiredArgsConstructor
public enum CommonErrorCode implements ErrorCode{
UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "C001", "로그인된 사용자만 접근 가능합니다."),
// 500 Internal Server Error: 서버 내부 오류
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "C006", "서버에 오류가 발생했습니다.");

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.web.SearchWeb.config;
package com.web.SearchWeb.config.exception;

import org.springframework.http.HttpStatus;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.web.SearchWeb.config;
package com.web.SearchWeb.config.security;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.web.SearchWeb.config;
package com.web.SearchWeb.config.security;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package com.web.SearchWeb.config;
package com.web.SearchWeb.config.security;

import com.web.SearchWeb.config.common.ApiResponse;
import com.web.SearchWeb.config.exception.BusinessException;
import com.web.SearchWeb.config.exception.CommonErrorCode;
import com.web.SearchWeb.config.exception.ErrorCode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.web.SearchWeb.config;
package com.web.SearchWeb.config.security;

import com.web.SearchWeb.member.service.CustomOAuth2UserService;
import org.springframework.context.annotation.Bean;
Expand Down
35 changes: 35 additions & 0 deletions src/main/java/com/web/SearchWeb/config/security/SecurityUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.web.SearchWeb.config.security;

import com.web.SearchWeb.config.exception.BusinessException;
import com.web.SearchWeb.config.exception.CommonErrorCode;
import com.web.SearchWeb.member.dto.CustomOAuth2User;
import com.web.SearchWeb.member.dto.CustomUserDetails;
import org.springframework.security.core.Authentication;

public final class SecurityUtils {

private SecurityUtils() {
}

public static Long extractMemberId(Authentication authentication) {
if (authentication == null || !authentication.isAuthenticated()) {
throw BusinessException.from(CommonErrorCode.UNAUTHORIZED);
}

Object principal = authentication.getPrincipal();

if ("anonymousUser".equals(principal)) {
throw BusinessException.from(CommonErrorCode.UNAUTHORIZED);
}

if (principal instanceof CustomUserDetails userDetails) {
return userDetails.getMemberId();
}

if (principal instanceof CustomOAuth2User oauth2User) {
return oauth2User.getMemberId();
Comment on lines +25 to +30
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

memberId가 null인 경우도 여기서 차단하세요.

CustomUserDetails#getMemberId()CustomOAuth2User#getMemberId()가 값을 그대로 반환하므로, 인증 객체가 만들어졌더라도 memberId가 null이면 이후 권한 체크가 500으로 깨질 수 있습니다. 이 메서드에서 fail-closed로 UNAUTHORIZED를 던지는 편이 안전합니다.

🔒 제안 수정
         if (principal instanceof CustomUserDetails userDetails) {
-            return userDetails.getMemberId();
+            Long memberId = userDetails.getMemberId();
+            if (memberId != null) {
+                return memberId;
+            }
+            throw BusinessException.from(CommonErrorCode.UNAUTHORIZED);
         }

         if (principal instanceof CustomOAuth2User oauth2User) {
-            return oauth2User.getMemberId();
+            Long memberId = oauth2User.getMemberId();
+            if (memberId != null) {
+                return memberId;
+            }
+            throw BusinessException.from(CommonErrorCode.UNAUTHORIZED);
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (principal instanceof CustomUserDetails userDetails) {
return userDetails.getMemberId();
}
if (principal instanceof CustomOAuth2User oauth2User) {
return oauth2User.getMemberId();
if (principal instanceof CustomUserDetails userDetails) {
Long memberId = userDetails.getMemberId();
if (memberId != null) {
return memberId;
}
throw BusinessException.from(CommonErrorCode.UNAUTHORIZED);
}
if (principal instanceof CustomOAuth2User oauth2User) {
Long memberId = oauth2User.getMemberId();
if (memberId != null) {
return memberId;
}
throw BusinessException.from(CommonErrorCode.UNAUTHORIZED);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/com/web/SearchWeb/config/security/SecurityUtils.java` around
lines 25 - 30, In SecurityUtils (the method that extracts memberId from the
authentication principal), ensure you fail-closed by validating the returned
memberId from CustomUserDetails.getMemberId() and
CustomOAuth2User.getMemberId(): if either is null throw an UNAUTHORIZED response
(e.g. throw ResponseStatusException(HttpStatus.UNAUTHORIZED) or your app's
equivalent) instead of returning null; also handle the fallback branch (when
principal is not one of those types) to throw UNAUTHORIZED so no null memberId
propagates.

}

throw BusinessException.from(CommonErrorCode.UNAUTHORIZED);
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
package com.web.SearchWeb.folder.controller;

import com.web.SearchWeb.config.ApiResponse;
import com.web.SearchWeb.config.common.ApiResponse;
import com.web.SearchWeb.config.security.SecurityUtils;
import com.web.SearchWeb.folder.controller.dto.MemberFolderRequests;
import com.web.SearchWeb.folder.controller.dto.MemberFolderResponses;
import com.web.SearchWeb.folder.domain.MemberFolder;
import com.web.SearchWeb.folder.service.MemberFolderService;
import jakarta.validation.Valid;
import java.util.List;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
Expand All @@ -28,9 +31,10 @@ public class MemberFolderController {

// 생성 (201 Created 응답)
@PostMapping
public ResponseEntity<ApiResponse<Long>> create(@RequestBody MemberFolderRequests.Create req) {
public ResponseEntity<ApiResponse<Long>> create(Authentication authentication, @Valid @RequestBody MemberFolderRequests.Create req) {
Long loginId = SecurityUtils.extractMemberId(authentication);
Long folderId = memberFolderService.create(
req.ownerMemberId,
loginId,
req.parentFolderId,
req.folderName,
req.description
Expand All @@ -40,17 +44,22 @@ public ResponseEntity<ApiResponse<Long>> create(@RequestBody MemberFolderRequest
.body(ApiResponse.success(folderId));
}

// 단건 조회
// 폴더 정보 단건 조회
@GetMapping("/{folderId}")
public ResponseEntity<ApiResponse<MemberFolderResponses>> get(@PathVariable Long folderId) {
MemberFolder folder = memberFolderService.get(folderId);
public ResponseEntity<ApiResponse<MemberFolderResponses>> get(Authentication authentication, @PathVariable Long folderId) {
Long loginId = SecurityUtils.extractMemberId(authentication);
MemberFolder folder = memberFolderService.get(loginId, folderId);
return ResponseEntity.ok(ApiResponse.success(MemberFolderResponses.from(folder)));
}

// 루트 폴더 조회
@GetMapping("/owners/{ownerMemberId}/root")
public ResponseEntity<ApiResponse<List<MemberFolderResponses>>> listRoot(@PathVariable Long ownerMemberId) {
List<MemberFolderResponses> responses = memberFolderService.listRootFolders(ownerMemberId)
public ResponseEntity<ApiResponse<List<MemberFolderResponses>>> listRoot(
Authentication authentication,
@PathVariable Long ownerMemberId
) {
Long loginId = SecurityUtils.extractMemberId(authentication);
List<MemberFolderResponses> responses = memberFolderService.listRootFolders(loginId, ownerMemberId)
.stream()
.map(MemberFolderResponses::from)
.collect(Collectors.toList());
Expand All @@ -61,10 +70,12 @@ public ResponseEntity<ApiResponse<List<MemberFolderResponses>>> listRoot(@PathVa
// 하위 폴더 조회
@GetMapping("/owners/{ownerMemberId}/children/{parentFolderId}")
public ResponseEntity<ApiResponse<List<MemberFolderResponses>>> listChildren(
Authentication authentication,
@PathVariable Long ownerMemberId,
@PathVariable Long parentFolderId
) {
List<MemberFolderResponses> responses = memberFolderService.listChildren(ownerMemberId, parentFolderId)
Long loginId = SecurityUtils.extractMemberId(authentication);
List<MemberFolderResponses> responses = memberFolderService.listChildren(loginId, ownerMemberId, parentFolderId)
.stream()
.map(MemberFolderResponses::from)
.collect(Collectors.toList());
Expand All @@ -74,22 +85,27 @@ public ResponseEntity<ApiResponse<List<MemberFolderResponses>>> listChildren(

// 수정 (200 OK)
@PutMapping("/{folderId}")
public ResponseEntity<ApiResponse<Void>> update(@PathVariable Long folderId, @RequestBody MemberFolderRequests.Update req) {
memberFolderService.update(folderId, req.folderName, req.description);
public ResponseEntity<ApiResponse<Void>> update(Authentication authentication, @PathVariable Long folderId,
@Valid @RequestBody MemberFolderRequests.Update req) {
Long loginId = SecurityUtils.extractMemberId(authentication);
memberFolderService.update(loginId, folderId, req.folderName, req.description);
return ResponseEntity.ok(ApiResponse.success(null));
}

// 이동(부모 변경)
@PutMapping("/{folderId}/move")
public ResponseEntity<ApiResponse<Void>> move(@PathVariable Long folderId, @RequestBody MemberFolderRequests.Move req) {
memberFolderService.move(folderId, req.newParentFolderId);
public ResponseEntity<ApiResponse<Void>> move(Authentication authentication, @PathVariable Long folderId,
@Valid @RequestBody MemberFolderRequests.Move req) {
Long loginId = SecurityUtils.extractMemberId(authentication);
memberFolderService.move(loginId, folderId, req.newParentFolderId);
return ResponseEntity.ok(ApiResponse.success(null));
}

// 삭제
@DeleteMapping("/{folderId}")
public ResponseEntity<ApiResponse<Void>> delete(@PathVariable Long folderId) {
memberFolderService.delete(folderId);
public ResponseEntity<ApiResponse<Void>> delete(Authentication authentication, @PathVariable Long folderId) {
Long loginId = SecurityUtils.extractMemberId(authentication);
memberFolderService.delete(loginId, folderId);
return ResponseEntity.ok(ApiResponse.success(null));
}
}
}
Original file line number Diff line number Diff line change
@@ -1,29 +1,53 @@
package com.web.SearchWeb.folder.controller.dto;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Positive;
import jakarta.validation.constraints.Size;
import lombok.Getter;
import lombok.NoArgsConstructor;

public class MemberFolderRequests {

/**
* 폴더 생성 요청
*/

@Getter
@NoArgsConstructor
public static class Create {
public Long ownerMemberId;
public Long parentFolderId; // null이면 루트
// null이면 루트 폴더
@Positive(message = "parentFolderId는 양수여야 합니다.")
public Long parentFolderId;

@NotBlank(message = "folderName은 비어 있을 수 없습니다.")
@Size(max = 50, message = "folderName은 최대 50자까지 가능합니다.")
public String folderName;

@Size(max = 200, message = "description은 최대 200자까지 가능합니다.")
public String description;
Comment on lines 17 to 27
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

요청 DTO 필드는 private로 두세요.

@Getter를 붙였는데 필드가 여전히 public이라 검증/바인딩 이후에도 외부 코드가 값을 직접 바꿀 수 있습니다. Jackson + Bean Validation 조합에서는 기본 생성자와 private 필드만으로 충분합니다.

♻️ 제안 수정
     public static class Create {
         // null이면 루트 폴더
         `@Positive`(message = "parentFolderId는 양수여야 합니다.")
-        public Long parentFolderId;
+        private Long parentFolderId;

         `@NotBlank`(message = "folderName은 비어 있을 수 없습니다.")
         `@Size`(max = 50, message = "folderName은 최대 50자까지 가능합니다.")
-        public String folderName;
+        private String folderName;

         `@Size`(max = 200, message = "description은 최대 200자까지 가능합니다.")
-        public String description;
+        private String description;
     }
@@
     public static class Update {
         `@Size`(max = 50, message = "folderName은 최대 50자까지 가능합니다.")
-        public String folderName;
+        private String folderName;

         `@Size`(max = 200, message = "description은 최대 200자까지 가능합니다.")
-        public String description;
+        private String description;
     }
@@
     public static class Move {
         // null이면 루트로 이동
         `@Positive`(message = "newParentFolderId는 양수여야 합니다.")
-        public Long newParentFolderId;
+        private Long newParentFolderId;
     }

Also applies to: 35-40, 48-51

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/web/SearchWeb/folder/controller/dto/MemberFolderRequests.java`
around lines 17 - 27, Change the DTO fields from public to private so external
code cannot mutate them after validation; in the nested request class Create
make parentFolderId, folderName, and description private and rely on the
existing `@Getter` (and default constructor) for Jackson/Bean Validation, and
apply the same change to the other nested request classes in this file (e.g.,
Update/other request classes that currently have public fields).

}

/**
* 폴더 수정 요청
*/
@Getter
@NoArgsConstructor
public static class Update {
@Size(max = 50, message = "folderName은 최대 50자까지 가능합니다.")
public String folderName;

@Size(max = 200, message = "description은 최대 200자까지 가능합니다.")
public String description;
}

/**
* 폴더 이동 요청
*/
@Getter
@NoArgsConstructor
public static class Move {
public Long newParentFolderId; // null이면 루트로 이동
// null이면 루트로 이동
@Positive(message = "newParentFolderId는 양수여야 합니다.")
public Long newParentFolderId;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,10 @@ public interface MemberFolderJpaDao extends JpaRepository<MemberFolder, Long> {

// 사용자 전체 폴더
List<MemberFolder> findAllByOwnerMemberId(Long ownerMemberId);

boolean existsByParentFolderId(Long parentFolderId);

boolean existsByOwnerMemberIdAndParentFolderIdAndFolderName(Long loginId, Long parentFolderId, String normalizedFolderName);

boolean existsByOwnerMemberIdAndParentFolderIdIsNullAndFolderName(Long loginId, String normalizedFolderName);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.web.SearchWeb.folder.error;

import com.web.SearchWeb.config.ErrorCode;
import com.web.SearchWeb.config.exception.ErrorCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
Expand All @@ -9,7 +9,11 @@
@RequiredArgsConstructor
public enum FolderErrorCode implements ErrorCode {
FOLDER_NOT_FOUND(HttpStatus.NOT_FOUND, "F001", "폴더를 찾을 수 없습니다."),
DUPLICATE_FOLDER_NAME(HttpStatus.BAD_REQUEST, "F002", "이미 존재하는 폴더명입니다.");
DUPLICATE_FOLDER_NAME(HttpStatus.BAD_REQUEST, "F002", "이미 존재하는 폴더명입니다."),
FOLDER_FORBIDDEN(HttpStatus.FORBIDDEN,"Foo3" ,"접근이 제한된 폴더입니다." ),
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

에러 코드 값 오타를 수정하세요.

FOLDER_FORBIDDEN의 코드가 Foo3로 들어가 있습니다. 이 값이 응답 계약에 노출되면 클라이언트 분기와 문서가 모두 어긋나므로 F003로 바로잡아야 합니다.

🐛 제안 수정
-    FOLDER_FORBIDDEN(HttpStatus.FORBIDDEN,"Foo3" ,"접근이 제한된 폴더입니다." ),
+    FOLDER_FORBIDDEN(HttpStatus.FORBIDDEN, "F003", "접근이 제한된 폴더입니다."),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
FOLDER_FORBIDDEN(HttpStatus.FORBIDDEN,"Foo3" ,"접근이 제한된 폴더입니다." ),
FOLDER_FORBIDDEN(HttpStatus.FORBIDDEN, "F003", "접근이 제한된 폴더입니다."),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/com/web/SearchWeb/folder/error/FolderErrorCode.java` at line
13, The enum constant FOLDER_FORBIDDEN in FolderErrorCode currently uses the
incorrect code string "Foo3"; update the enum entry for FOLDER_FORBIDDEN to use
the correct code "F003" so the exposed error code matches the response contract
and client documentation.

INVALID_FOLDER_NAME(HttpStatus.BAD_REQUEST,"F004","폴더명으로 적절하지 않습니다" ),
INVALID_FOLDER_MOVE(HttpStatus.BAD_REQUEST, "F005", "유효하지 않은 폴더 이동입니다."),
FOLDER_NOT_EMPTY(HttpStatus.BAD_REQUEST, "F006", "하위 폴더 또는 북마크가 남아 있어 삭제할 수 없습니다.");

private final HttpStatus status;
private final String code;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,13 @@
package com.web.SearchWeb.folder.error;

import com.web.SearchWeb.config.BusinessException;
import com.web.SearchWeb.config.ErrorCode;
import com.web.SearchWeb.config.exception.BusinessException;
import com.web.SearchWeb.config.exception.ErrorCode;
import lombok.Getter;

@Getter
public class FolderException extends BusinessException {

private FolderException(ErrorCode errorCode) {
public FolderException(ErrorCode errorCode) {
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

FolderException 생성자는 FolderErrorCode로 좁히는 편이 안전합니다.

지금 시그니처면 폴더 도메인 예외에 다른 도메인의 ErrorCode를 실수로 태워도 컴파일이 통과합니다. 현재 사용 패턴도 전부 FolderErrorCode라서 타입을 좁혀 두는 쪽이 API 경계를 더 명확하게 만듭니다.

♻️ 제안 수정
-import com.web.SearchWeb.config.exception.ErrorCode;
 import lombok.Getter;

 `@Getter`
 public class FolderException extends BusinessException {

-    public FolderException(ErrorCode errorCode) {
+    public FolderException(FolderErrorCode errorCode) {
         super(errorCode);
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/com/web/SearchWeb/folder/error/FolderException.java` at line
10, The FolderException constructor currently accepts the generic ErrorCode
which allows non-folder error codes to be passed; change the constructor
signature in class FolderException to accept FolderErrorCode instead (replace
ErrorCode parameter with FolderErrorCode) and update all call sites that
construct new FolderException(...) to pass a FolderErrorCode (adjust
imports/usages as needed) so the API boundary is type-safe; keep the rest of
FolderException behavior unchanged.

super(errorCode);
}

public static class NotFound extends FolderException {
public NotFound() {
super(FolderErrorCode.FOLDER_NOT_FOUND);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,17 @@

public interface MemberFolderService {

Long create(Long ownerMemberId, Long parentFolderId, String folderName, String description);
Long create(Long loginId, Long parentFolderId, String folderName, String description);

MemberFolder get(Long memberFolderId);
MemberFolder get(Long loginId, Long memberFolderId);

List<MemberFolder> listRootFolders(Long ownerMemberId);
List<MemberFolder> listRootFolders(Long loginId, Long ownerMemberId);

List<MemberFolder> listChildren(Long ownerMemberId, Long parentFolderId);
List<MemberFolder> listChildren(Long loginId, Long ownerMemberId, Long parentFolderId);
Comment on lines +13 to +15
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd 'MemberFolder(Controller|ServiceImpl)\.java$' src/main/java -x sh -c '
  echo "== $1 ==";
  rg -n -C3 "listRootFolders|listChildren|ownerMemberId|loginId" "$1";
' sh {}

Repository: Searchweb-Dev/Searchweb-Back

Length of output: 8312


조회 API의 ownerMemberId 파라미터는 이미 검증이 적용되어 있습니다.

listRootFolders/listChildrenownerMemberId를 받지만, validateOwner(loginId, ownerMemberId) 호출 시 ownerMemberIdloginId가 일치하는지 검증합니다. 일치하지 않으면 즉시 FolderException(FOLDER_FORBIDDEN)을 발생시키므로, 컨트롤러가 요청값을 그대로 넘겨도 인증 경계는 보호됩니다.

다만 API 설계상 ownerMemberId 파라미터를 제거하고 loginId에서만 소유자를 취득하면 의도가 더 명확해질 수 있습니다. 예를 들어 /root/children/{parentFolderId} 엔드포인트로 단순화하면 URL 경로에서 명시적 검증 로직이 필요 없게 됩니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/com/web/SearchWeb/folder/service/MemberFolderService.java`
around lines 13 - 15, Remove the redundant ownerMemberId parameter from the
MemberFolderService API: change the signatures of listRootFolders and
listChildren to accept only Long loginId (and Long parentFolderId for
listChildren), and update all implementations to derive the owner from loginId
instead of using ownerMemberId; also remove or adapt calls to
validateOwner(loginId, ownerMemberId) so they validate ownership using loginId
alone (e.g., validateOwner(loginId) or inline owner resolution) and adjust any
controllers/routes to stop passing ownerMemberId.


void update(Long memberFolderId, String folderName, String description);
void update(Long loginId, Long memberFolderId, String folderName, String description);

void move(Long memberFolderId, Long newParentFolderId);
void move(Long loginId, Long memberFolderId, Long newParentFolderId);

void delete(Long memberFolderId);
void delete(Long loginId, Long memberFolderId);
}
Loading