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 996db6a..3cde0dd 100644 --- a/src/main/java/com/web/SearchWeb/bookmark/controller/BookmarkApiController.java +++ b/src/main/java/com/web/SearchWeb/bookmark/controller/BookmarkApiController.java @@ -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; 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 066ad26..ba13a12 100644 --- a/src/main/java/com/web/SearchWeb/bookmark/dao/BookmarkDao.java +++ b/src/main/java/com/web/SearchWeb/bookmark/dao/BookmarkDao.java @@ -46,4 +46,7 @@ public interface BookmarkDao { // 북마크-태그 연결 일괄 추가 (Bulk Insert) int insertBookmarkTags(Long bookmarkId, List tagIds); + + // 폴더 내 활성 북마크 존재 여부 + boolean existsActiveBookmarkInFolder(Long memberFolderId); } 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 221cdbc..113d01d 100644 --- a/src/main/java/com/web/SearchWeb/bookmark/dao/MybatisBookmarkDao.java +++ b/src/main/java/com/web/SearchWeb/bookmark/dao/MybatisBookmarkDao.java @@ -136,4 +136,9 @@ public List insertAndSelectTags(Long memberId, List public int insertBookmarkTags(Long bookmarkId, List tagIds) { return mapper.insertBookmarkTags(bookmarkId, tagIds); } + + @Override + public boolean existsActiveBookmarkInFolder(Long memberFolderId) { + return mapper.existsActiveBookmarkInFolder(memberFolderId); + } } 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 db718f4..360ea47 100644 --- a/src/main/java/com/web/SearchWeb/bookmark/error/BookmarkErrorCode.java +++ b/src/main/java/com/web/SearchWeb/bookmark/error/BookmarkErrorCode.java @@ -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; 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 d570af1..dd42321 100644 --- a/src/main/java/com/web/SearchWeb/bookmark/service/BookmarkServiceImpl.java +++ b/src/main/java/com/web/SearchWeb/bookmark/service/BookmarkServiceImpl.java @@ -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; diff --git a/src/main/java/com/web/SearchWeb/config/ApiResponse.java b/src/main/java/com/web/SearchWeb/config/common/ApiResponse.java similarity index 92% rename from src/main/java/com/web/SearchWeb/config/ApiResponse.java rename to src/main/java/com/web/SearchWeb/config/common/ApiResponse.java index c1c5027..7bfd29d 100644 --- a/src/main/java/com/web/SearchWeb/config/ApiResponse.java +++ b/src/main/java/com/web/SearchWeb/config/common/ApiResponse.java @@ -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; diff --git a/src/main/java/com/web/SearchWeb/config/BusinessException.java b/src/main/java/com/web/SearchWeb/config/exception/BusinessException.java similarity index 89% rename from src/main/java/com/web/SearchWeb/config/BusinessException.java rename to src/main/java/com/web/SearchWeb/config/exception/BusinessException.java index 42c7578..135353b 100644 --- a/src/main/java/com/web/SearchWeb/config/BusinessException.java +++ b/src/main/java/com/web/SearchWeb/config/exception/BusinessException.java @@ -1,4 +1,4 @@ -package com.web.SearchWeb.config; +package com.web.SearchWeb.config.exception; import lombok.Getter; diff --git a/src/main/java/com/web/SearchWeb/config/CommonErrorCode.java b/src/main/java/com/web/SearchWeb/config/exception/CommonErrorCode.java similarity index 75% rename from src/main/java/com/web/SearchWeb/config/CommonErrorCode.java rename to src/main/java/com/web/SearchWeb/config/exception/CommonErrorCode.java index 30a2e56..8d9ed79 100644 --- a/src/main/java/com/web/SearchWeb/config/CommonErrorCode.java +++ b/src/main/java/com/web/SearchWeb/config/exception/CommonErrorCode.java @@ -1,4 +1,4 @@ -package com.web.SearchWeb.config; +package com.web.SearchWeb.config.exception; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -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", "서버에 오류가 발생했습니다."); diff --git a/src/main/java/com/web/SearchWeb/config/ErrorCode.java b/src/main/java/com/web/SearchWeb/config/exception/ErrorCode.java similarity index 77% rename from src/main/java/com/web/SearchWeb/config/ErrorCode.java rename to src/main/java/com/web/SearchWeb/config/exception/ErrorCode.java index c352033..d155b3e 100644 --- a/src/main/java/com/web/SearchWeb/config/ErrorCode.java +++ b/src/main/java/com/web/SearchWeb/config/exception/ErrorCode.java @@ -1,4 +1,4 @@ -package com.web.SearchWeb.config; +package com.web.SearchWeb.config.exception; import org.springframework.http.HttpStatus; diff --git a/src/main/java/com/web/SearchWeb/config/CustomAuthenticationFailureHandler.java b/src/main/java/com/web/SearchWeb/config/security/CustomAuthenticationFailureHandler.java similarity index 96% rename from src/main/java/com/web/SearchWeb/config/CustomAuthenticationFailureHandler.java rename to src/main/java/com/web/SearchWeb/config/security/CustomAuthenticationFailureHandler.java index d6dc6c7..67dc636 100644 --- a/src/main/java/com/web/SearchWeb/config/CustomAuthenticationFailureHandler.java +++ b/src/main/java/com/web/SearchWeb/config/security/CustomAuthenticationFailureHandler.java @@ -1,4 +1,4 @@ -package com.web.SearchWeb.config; +package com.web.SearchWeb.config.security; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; diff --git a/src/main/java/com/web/SearchWeb/config/CustomAuthenticationSuccessHandler.java b/src/main/java/com/web/SearchWeb/config/security/CustomAuthenticationSuccessHandler.java similarity index 95% rename from src/main/java/com/web/SearchWeb/config/CustomAuthenticationSuccessHandler.java rename to src/main/java/com/web/SearchWeb/config/security/CustomAuthenticationSuccessHandler.java index ded250c..a6eb006 100644 --- a/src/main/java/com/web/SearchWeb/config/CustomAuthenticationSuccessHandler.java +++ b/src/main/java/com/web/SearchWeb/config/security/CustomAuthenticationSuccessHandler.java @@ -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; diff --git a/src/main/java/com/web/SearchWeb/config/GlobalExceptionHandler.java b/src/main/java/com/web/SearchWeb/config/security/GlobalExceptionHandler.java similarity index 83% rename from src/main/java/com/web/SearchWeb/config/GlobalExceptionHandler.java rename to src/main/java/com/web/SearchWeb/config/security/GlobalExceptionHandler.java index 4897c1b..208df0e 100644 --- a/src/main/java/com/web/SearchWeb/config/GlobalExceptionHandler.java +++ b/src/main/java/com/web/SearchWeb/config/security/GlobalExceptionHandler.java @@ -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; diff --git a/src/main/java/com/web/SearchWeb/config/SecurityConfig.java b/src/main/java/com/web/SearchWeb/config/security/SecurityConfig.java similarity index 99% rename from src/main/java/com/web/SearchWeb/config/SecurityConfig.java rename to src/main/java/com/web/SearchWeb/config/security/SecurityConfig.java index afd01bc..3903b80 100644 --- a/src/main/java/com/web/SearchWeb/config/SecurityConfig.java +++ b/src/main/java/com/web/SearchWeb/config/security/SecurityConfig.java @@ -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; diff --git a/src/main/java/com/web/SearchWeb/config/security/SecurityUtils.java b/src/main/java/com/web/SearchWeb/config/security/SecurityUtils.java new file mode 100644 index 0000000..55736c7 --- /dev/null +++ b/src/main/java/com/web/SearchWeb/config/security/SecurityUtils.java @@ -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(); + } + + throw BusinessException.from(CommonErrorCode.UNAUTHORIZED); + } +} diff --git a/src/main/java/com/web/SearchWeb/folder/controller/MemberFolderController.java b/src/main/java/com/web/SearchWeb/folder/controller/MemberFolderController.java index 2fa140e..6475278 100644 --- a/src/main/java/com/web/SearchWeb/folder/controller/MemberFolderController.java +++ b/src/main/java/com/web/SearchWeb/folder/controller/MemberFolderController.java @@ -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; @@ -28,9 +31,10 @@ public class MemberFolderController { // 생성 (201 Created 응답) @PostMapping - public ResponseEntity> create(@RequestBody MemberFolderRequests.Create req) { + public ResponseEntity> 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 @@ -40,17 +44,22 @@ public ResponseEntity> create(@RequestBody MemberFolderRequest .body(ApiResponse.success(folderId)); } - // 단건 조회 + // 폴더 정보 단건 조회 @GetMapping("/{folderId}") - public ResponseEntity> get(@PathVariable Long folderId) { - MemberFolder folder = memberFolderService.get(folderId); + public ResponseEntity> 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>> listRoot(@PathVariable Long ownerMemberId) { - List responses = memberFolderService.listRootFolders(ownerMemberId) + public ResponseEntity>> listRoot( + Authentication authentication, + @PathVariable Long ownerMemberId + ) { + Long loginId = SecurityUtils.extractMemberId(authentication); + List responses = memberFolderService.listRootFolders(loginId, ownerMemberId) .stream() .map(MemberFolderResponses::from) .collect(Collectors.toList()); @@ -61,10 +70,12 @@ public ResponseEntity>> listRoot(@PathVa // 하위 폴더 조회 @GetMapping("/owners/{ownerMemberId}/children/{parentFolderId}") public ResponseEntity>> listChildren( + Authentication authentication, @PathVariable Long ownerMemberId, @PathVariable Long parentFolderId ) { - List responses = memberFolderService.listChildren(ownerMemberId, parentFolderId) + Long loginId = SecurityUtils.extractMemberId(authentication); + List responses = memberFolderService.listChildren(loginId, ownerMemberId, parentFolderId) .stream() .map(MemberFolderResponses::from) .collect(Collectors.toList()); @@ -74,22 +85,27 @@ public ResponseEntity>> listChildren( // 수정 (200 OK) @PutMapping("/{folderId}") - public ResponseEntity> update(@PathVariable Long folderId, @RequestBody MemberFolderRequests.Update req) { - memberFolderService.update(folderId, req.folderName, req.description); + public ResponseEntity> 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> move(@PathVariable Long folderId, @RequestBody MemberFolderRequests.Move req) { - memberFolderService.move(folderId, req.newParentFolderId); + public ResponseEntity> 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> delete(@PathVariable Long folderId) { - memberFolderService.delete(folderId); + public ResponseEntity> delete(Authentication authentication, @PathVariable Long folderId) { + Long loginId = SecurityUtils.extractMemberId(authentication); + memberFolderService.delete(loginId, folderId); return ResponseEntity.ok(ApiResponse.success(null)); } -} \ No newline at end of file +} diff --git a/src/main/java/com/web/SearchWeb/folder/controller/dto/MemberFolderRequests.java b/src/main/java/com/web/SearchWeb/folder/controller/dto/MemberFolderRequests.java index 3e19b7e..dc367ed 100644 --- a/src/main/java/com/web/SearchWeb/folder/controller/dto/MemberFolderRequests.java +++ b/src/main/java/com/web/SearchWeb/folder/controller/dto/MemberFolderRequests.java @@ -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; } /** * 폴더 수정 요청 */ + @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; } } diff --git a/src/main/java/com/web/SearchWeb/folder/dao/MemberFolderJpaDao.java b/src/main/java/com/web/SearchWeb/folder/dao/MemberFolderJpaDao.java index 314f7e7..5807f15 100644 --- a/src/main/java/com/web/SearchWeb/folder/dao/MemberFolderJpaDao.java +++ b/src/main/java/com/web/SearchWeb/folder/dao/MemberFolderJpaDao.java @@ -14,4 +14,10 @@ public interface MemberFolderJpaDao extends JpaRepository { // 사용자 전체 폴더 List findAllByOwnerMemberId(Long ownerMemberId); + + boolean existsByParentFolderId(Long parentFolderId); + + boolean existsByOwnerMemberIdAndParentFolderIdAndFolderName(Long loginId, Long parentFolderId, String normalizedFolderName); + + boolean existsByOwnerMemberIdAndParentFolderIdIsNullAndFolderName(Long loginId, String normalizedFolderName); } diff --git a/src/main/java/com/web/SearchWeb/folder/error/FolderErrorCode.java b/src/main/java/com/web/SearchWeb/folder/error/FolderErrorCode.java index ca76170..5d0e72e 100644 --- a/src/main/java/com/web/SearchWeb/folder/error/FolderErrorCode.java +++ b/src/main/java/com/web/SearchWeb/folder/error/FolderErrorCode.java @@ -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; @@ -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" ,"접근이 제한된 폴더입니다." ), + 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; diff --git a/src/main/java/com/web/SearchWeb/folder/error/FolderException.java b/src/main/java/com/web/SearchWeb/folder/error/FolderException.java index d97d24a..94b9967 100644 --- a/src/main/java/com/web/SearchWeb/folder/error/FolderException.java +++ b/src/main/java/com/web/SearchWeb/folder/error/FolderException.java @@ -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) { super(errorCode); } - - public static class NotFound extends FolderException { - public NotFound() { - super(FolderErrorCode.FOLDER_NOT_FOUND); - } - } } diff --git a/src/main/java/com/web/SearchWeb/folder/service/MemberFolderService.java b/src/main/java/com/web/SearchWeb/folder/service/MemberFolderService.java index ca8b448..745801e 100644 --- a/src/main/java/com/web/SearchWeb/folder/service/MemberFolderService.java +++ b/src/main/java/com/web/SearchWeb/folder/service/MemberFolderService.java @@ -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 listRootFolders(Long ownerMemberId); + List listRootFolders(Long loginId, Long ownerMemberId); - List listChildren(Long ownerMemberId, Long parentFolderId); + List listChildren(Long loginId, Long ownerMemberId, Long parentFolderId); - 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); } diff --git a/src/main/java/com/web/SearchWeb/folder/service/MemberFolderServiceImpl.java b/src/main/java/com/web/SearchWeb/folder/service/MemberFolderServiceImpl.java index 2023694..9e26338 100644 --- a/src/main/java/com/web/SearchWeb/folder/service/MemberFolderServiceImpl.java +++ b/src/main/java/com/web/SearchWeb/folder/service/MemberFolderServiceImpl.java @@ -1,7 +1,9 @@ package com.web.SearchWeb.folder.service; +import com.web.SearchWeb.bookmark.dao.BookmarkDao; import com.web.SearchWeb.folder.dao.MemberFolderJpaDao; import com.web.SearchWeb.folder.domain.MemberFolder; +import com.web.SearchWeb.folder.error.FolderErrorCode; import com.web.SearchWeb.folder.error.FolderException; import java.util.List; import lombok.RequiredArgsConstructor; @@ -13,74 +15,222 @@ public class MemberFolderServiceImpl implements MemberFolderService { private final MemberFolderJpaDao memberFolderJpaRepository; + private final BookmarkDao bookmarkDao; @Override @Transactional - public Long create(Long ownerMemberId, Long parentFolderId, String folderName, String description) { - validateFolderName(folderName); + public Long create(Long loginId, Long parentFolderId, String folderName, String description) { + String normalizedFolderName = normalizeFolderName(folderName); + String normalizedDescription = normalizeDescription(description); + + // 1. 부모 폴더가 있는 경우 검증 + if (parentFolderId != null) { + MemberFolder parentFolder = memberFolderJpaRepository + .findById(parentFolderId) + .orElseThrow(() -> new FolderException(FolderErrorCode.FOLDER_NOT_FOUND)); + + // 2. 부모 폴더 소유자 검증 + if (!parentFolder.getOwnerMemberId().equals(loginId)) { + throw new FolderException(FolderErrorCode.FOLDER_FORBIDDEN); + } + + // 3. 같은 부모 아래 동일 이름 폴더 중복 검증 + boolean exists = memberFolderJpaRepository + .existsByOwnerMemberIdAndParentFolderIdAndFolderName( + loginId, parentFolderId, normalizedFolderName + ); + + if (exists) { + throw new FolderException(FolderErrorCode.DUPLICATE_FOLDER_NAME); + } + } else { + // 4. 루트 폴더일 때 동일 이름 중복 검증 + boolean exists = memberFolderJpaRepository + .existsByOwnerMemberIdAndParentFolderIdIsNullAndFolderName( + loginId, normalizedFolderName + ); + + if (exists) { + throw new FolderException(FolderErrorCode.DUPLICATE_FOLDER_NAME); + } + } MemberFolder folder = MemberFolder.builder() - .ownerMemberId(ownerMemberId) + .ownerMemberId(loginId) .parentFolderId(parentFolderId) - .folderName(folderName) - .description(description) + .folderName(normalizedFolderName) + .description(normalizedDescription) .build(); return memberFolderJpaRepository.save(folder).getMemberFolderId(); } + private String normalizeFolderName(String folderName) { + if (folderName == null) { + throw new FolderException(FolderErrorCode.INVALID_FOLDER_NAME); + } + + String normalizedFolderName = folderName.trim(); + if (normalizedFolderName.isEmpty() || normalizedFolderName.length() > 50) { + throw new FolderException(FolderErrorCode.INVALID_FOLDER_NAME); + } + return normalizedFolderName; + } + + private String normalizeDescription(String description) { + if (description == null) { + return null; + } + + String normalizedDescription = description.trim(); + if (normalizedDescription.isEmpty()) { + return null; + } + + return normalizedDescription; + } + @Override @Transactional(readOnly = true) - public MemberFolder get(Long memberFolderId) { - return memberFolderJpaRepository.findById(memberFolderId) - .orElseThrow(FolderException.NotFound::new); + public MemberFolder get(Long loginId, Long memberFolderId) { + return getOwnedFolder(loginId, memberFolderId); } @Override @Transactional(readOnly = true) - public List listRootFolders(Long ownerMemberId) { + public List listRootFolders(Long loginId, Long ownerMemberId) { + validateOwner(loginId, ownerMemberId); return memberFolderJpaRepository.findAllByOwnerMemberIdAndParentFolderIdIsNull(ownerMemberId); } @Override @Transactional(readOnly = true) - public List listChildren(Long ownerMemberId, Long parentFolderId) { + public List listChildren(Long loginId, Long ownerMemberId, Long parentFolderId) { + validateOwner(loginId, ownerMemberId); + MemberFolder parentFolder = getOwnedFolder(loginId, parentFolderId); + if (!parentFolder.getOwnerMemberId().equals(ownerMemberId)) { + throw new FolderException(FolderErrorCode.FOLDER_FORBIDDEN); + } return memberFolderJpaRepository.findAllByOwnerMemberIdAndParentFolderId(ownerMemberId, parentFolderId); } @Override @Transactional - public void update(Long memberFolderId, String folderName, String description) { - validateFolderName(folderName); - - MemberFolder folder = memberFolderJpaRepository.findById(memberFolderId) - .orElseThrow(FolderException.NotFound::new); + public void update(Long loginId, Long memberFolderId, String folderName, String description) { + MemberFolder folder = getOwnedFolder(loginId, memberFolderId); + + String resolvedFolderName = folderName == null + ? folder.getFolderName() + : normalizeFolderName(folderName); + String resolvedDescription = description == null + ? folder.getDescription() + : normalizeDescription(description); + + if (!folder.getFolderName().equals(resolvedFolderName)) { + boolean exists = folder.getParentFolderId() == null + ? memberFolderJpaRepository.existsByOwnerMemberIdAndParentFolderIdIsNullAndFolderName( + folder.getOwnerMemberId(), + resolvedFolderName + ) + : memberFolderJpaRepository.existsByOwnerMemberIdAndParentFolderIdAndFolderName( + folder.getOwnerMemberId(), + folder.getParentFolderId(), + resolvedFolderName + ); + + if (exists) { + throw new FolderException(FolderErrorCode.DUPLICATE_FOLDER_NAME); + } + } - folder.changeInfo(folderName,description); - // TODO : 수정 로직 좀 더 생각해보기 + folder.changeInfo(resolvedFolderName, resolvedDescription); } @Override @Transactional - public void move(Long memberFolderId, Long newParentFolderId) { - MemberFolder folder = memberFolderJpaRepository.findById(memberFolderId) - .orElseThrow(FolderException.NotFound::new); + public void move(Long loginId, Long memberFolderId, Long newParentFolderId) { + MemberFolder folder = getOwnedFolder(loginId, memberFolderId); + + if ((folder.getParentFolderId() == null && newParentFolderId == null) + || (folder.getParentFolderId() != null && folder.getParentFolderId().equals(newParentFolderId))) { + return; + } + + if (newParentFolderId != null) { + if (memberFolderId.equals(newParentFolderId)) { + throw new FolderException(FolderErrorCode.INVALID_FOLDER_MOVE); + } + + MemberFolder newParentFolder = getOwnedFolder(loginId, newParentFolderId); + + if (!newParentFolder.getOwnerMemberId().equals(folder.getOwnerMemberId())) { + throw new FolderException(FolderErrorCode.FOLDER_FORBIDDEN); + } + + validateNoCycle(folder.getMemberFolderId(), newParentFolder); + + boolean exists = memberFolderJpaRepository.existsByOwnerMemberIdAndParentFolderIdAndFolderName( + folder.getOwnerMemberId(), + newParentFolderId, + folder.getFolderName() + ); + if (exists) { + throw new FolderException(FolderErrorCode.DUPLICATE_FOLDER_NAME); + } + } else { + boolean exists = memberFolderJpaRepository.existsByOwnerMemberIdAndParentFolderIdIsNullAndFolderName( + folder.getOwnerMemberId(), + folder.getFolderName() + ); + if (exists) { + throw new FolderException(FolderErrorCode.DUPLICATE_FOLDER_NAME); + } + } + folder.changeParent(newParentFolderId); } @Override @Transactional - public void delete(Long memberFolderId) { - if (!memberFolderJpaRepository.existsById(memberFolderId)) { - return; // 멱등 삭제 + public void delete(Long loginId, Long memberFolderId) { + getOwnedFolder(loginId, memberFolderId); + + if (memberFolderJpaRepository.existsByParentFolderId(memberFolderId) + || bookmarkDao.existsActiveBookmarkInFolder(memberFolderId)) { + throw new FolderException(FolderErrorCode.FOLDER_NOT_EMPTY); } + memberFolderJpaRepository.deleteById(memberFolderId); } - private void validateFolderName(String folderName) { - if (folderName == null || folderName.isBlank()) { - throw new IllegalArgumentException("folderName must not be blank"); + private MemberFolder getOwnedFolder(Long loginId, Long memberFolderId) { + MemberFolder folder = memberFolderJpaRepository.findById(memberFolderId) + .orElseThrow(() -> new FolderException(FolderErrorCode.FOLDER_NOT_FOUND)); + + validateOwner(loginId, folder.getOwnerMemberId()); + return folder; + } + + private void validateOwner(Long loginId, Long ownerMemberId) { + if (!ownerMemberId.equals(loginId)) { + throw new FolderException(FolderErrorCode.FOLDER_FORBIDDEN); } + } + + private void validateNoCycle(Long folderId, MemberFolder parentFolder) { + MemberFolder current = parentFolder; + while (current != null) { + if (current.getMemberFolderId().equals(folderId)) { + throw new FolderException(FolderErrorCode.INVALID_FOLDER_MOVE); + } + Long nextParentId = current.getParentFolderId(); + if (nextParentId == null) { + return; + } + + current = memberFolderJpaRepository.findById(nextParentId) + .orElseThrow(() -> new FolderException(FolderErrorCode.FOLDER_NOT_FOUND)); + } } -} \ No newline at end of file +} 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 215a849..481fb2b 100644 --- a/src/main/java/com/web/SearchWeb/mypage/controller/MyPageController.java +++ b/src/main/java/com/web/SearchWeb/mypage/controller/MyPageController.java @@ -5,7 +5,7 @@ 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.config.common.ApiResponse; import com.web.SearchWeb.member.domain.Member; import com.web.SearchWeb.member.dto.MemberUpdateDto; import com.web.SearchWeb.member.service.MemberService; diff --git a/src/main/java/com/web/SearchWeb/tag/controller/MemberTagController.java b/src/main/java/com/web/SearchWeb/tag/controller/MemberTagController.java index 026c17a..1449713 100644 --- a/src/main/java/com/web/SearchWeb/tag/controller/MemberTagController.java +++ b/src/main/java/com/web/SearchWeb/tag/controller/MemberTagController.java @@ -1,6 +1,6 @@ package com.web.SearchWeb.tag.controller; -import com.web.SearchWeb.config.ApiResponse; +import com.web.SearchWeb.config.common.ApiResponse; import com.web.SearchWeb.tag.controller.dto.MemberTagDto; import com.web.SearchWeb.tag.domain.MemberTag; import com.web.SearchWeb.tag.service.MemberTagService; diff --git a/src/main/java/com/web/SearchWeb/tag/error/TagErrorCode.java b/src/main/java/com/web/SearchWeb/tag/error/TagErrorCode.java index 16b7732..31ad34a 100644 --- a/src/main/java/com/web/SearchWeb/tag/error/TagErrorCode.java +++ b/src/main/java/com/web/SearchWeb/tag/error/TagErrorCode.java @@ -1,6 +1,6 @@ package com.web.SearchWeb.tag.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; diff --git a/src/main/java/com/web/SearchWeb/tag/error/TagException.java b/src/main/java/com/web/SearchWeb/tag/error/TagException.java index 79c7b22..48b9807 100644 --- a/src/main/java/com/web/SearchWeb/tag/error/TagException.java +++ b/src/main/java/com/web/SearchWeb/tag/error/TagException.java @@ -1,7 +1,7 @@ package com.web.SearchWeb.tag.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 diff --git a/src/main/resources/mapper/bookmark-mapper.xml b/src/main/resources/mapper/bookmark-mapper.xml index 68ae781..ea08638 100644 --- a/src/main/resources/mapper/bookmark-mapper.xml +++ b/src/main/resources/mapper/bookmark-mapper.xml @@ -228,4 +228,13 @@ DO UPDATE SET deleted_at = NULL, updated_at = now() + +