diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/archive/archive/entity/SharingArchive.java b/src/main/java/org/tuna/zoopzoop/backend/domain/archive/archive/entity/SharingArchive.java index 564107c2..599f4c4d 100644 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/archive/archive/entity/SharingArchive.java +++ b/src/main/java/org/tuna/zoopzoop/backend/domain/archive/archive/entity/SharingArchive.java @@ -8,6 +8,7 @@ import lombok.NoArgsConstructor; import lombok.Setter; import org.tuna.zoopzoop.backend.domain.archive.archive.enums.ArchiveType; +import org.tuna.zoopzoop.backend.domain.archive.folder.entity.Folder; import org.tuna.zoopzoop.backend.domain.space.space.entity.Space; import org.tuna.zoopzoop.backend.global.jpa.entity.BaseEntity; @@ -27,5 +28,9 @@ public class SharingArchive extends BaseEntity { public SharingArchive(Space space) { this.space = space; this.archive = new Archive(ArchiveType.SHARED); + + // πŸ”§ default 폴더 μžλ™ 생성 + Folder defaultFolder = new Folder("default"); + this.archive.addFolder(defaultFolder); } } diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/archive/folder/controller/FolderController.java b/src/main/java/org/tuna/zoopzoop/backend/domain/archive/folder/controller/FolderController.java index a9865c60..7142e46d 100644 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/archive/folder/controller/FolderController.java +++ b/src/main/java/org/tuna/zoopzoop/backend/domain/archive/folder/controller/FolderController.java @@ -10,7 +10,7 @@ import org.tuna.zoopzoop.backend.domain.archive.folder.dto.FolderResponse; import org.tuna.zoopzoop.backend.domain.archive.folder.dto.reqBodyForCreateFolder; import org.tuna.zoopzoop.backend.domain.archive.folder.dto.resBodyForCreateFolder; -import org.tuna.zoopzoop.backend.domain.archive.folder.service.FolderService; +import org.tuna.zoopzoop.backend.domain.archive.folder.service.PersonalArchiveFolderService; import org.tuna.zoopzoop.backend.domain.datasource.dto.FolderFilesDto; import org.tuna.zoopzoop.backend.domain.member.entity.Member; import org.tuna.zoopzoop.backend.global.rsData.RsData; @@ -25,12 +25,10 @@ @Tag(name = "ApiV1Folder", description = "개인 μ•„μΉ΄μ΄λΈŒμ˜ 폴더 CRUD") public class FolderController { - private final FolderService folderService; + private final PersonalArchiveFolderService personalArchiveFolderService; /** * λ‚΄ PersonalArchive μ•ˆμ— μƒˆ 폴더 생성 - * @param rq reqBodyForCreateFolder - * @return resBodyForCreateFolder */ @Operation(summary = "폴더 생성", description = "λ‚΄ PersonalArchive μ•ˆμ— μƒˆ 폴더λ₯Ό μƒμ„±ν•©λ‹ˆλ‹€.") @PostMapping @@ -39,41 +37,30 @@ public RsData createFolder( @AuthenticationPrincipal CustomUserDetails userDetails ) { Member member = userDetails.getMember(); - FolderResponse createFile = folderService.createFolderForPersonal(member.getId(), rq.folderName()); + FolderResponse createFile = personalArchiveFolderService.createFolder(member.getId(), rq.folderName()); resBodyForCreateFolder rs = new resBodyForCreateFolder(createFile.folderName(), createFile.folderId()); - return new RsData<>( - "200", - rq.folderName() + " 폴더가 μƒμ„±λμŠ΅λ‹ˆλ‹€.", - rs - ); + return new RsData<>("200",rq.folderName() + " 폴더가 μƒμ„±λμŠ΅λ‹ˆλ‹€.", rs); } /** * λ‚΄ PersonalArchive μ•ˆμ˜ folder μ‚­μ œ - * @param folderId μ‚­μ œν•  folderId */ @DeleteMapping("/{folderId}") - public ResponseEntity> deleteFolder( + public ResponseEntity> deleteFolder( @PathVariable Integer folderId, @AuthenticationPrincipal CustomUserDetails userDetails ) { - if (folderId == 0) { - var body = new java.util.HashMap(); - body.put("status", 409); - body.put("msg", "default ν΄λ”λŠ” μ‚­μ œν•  수 μ—†μŠ΅λ‹ˆλ‹€."); - body.put("data", null); - return ResponseEntity.badRequest().body(body); - } + if (folderId == 0) + throw new IllegalArgumentException("default ν΄λ”λŠ” μ‚­μ œν•  수 μ—†μŠ΅λ‹ˆλ‹€."); + Member member = userDetails.getMember(); - String deletedFolderName = folderService.deleteFolder(member.getId(), folderId); + String deletedFolderName = personalArchiveFolderService.deleteFolder(member.getId(), folderId); - var body = new java.util.HashMap(); - body.put("status", 200); - body.put("msg", deletedFolderName + " 폴더가 μ‚­μ œλμŠ΅λ‹ˆλ‹€."); - body.put("data", null); - return ResponseEntity.ok(body); + return ResponseEntity.ok( + new RsData<>("200", deletedFolderName + " 폴더가 μ‚­μ œλμŠ΅λ‹ˆλ‹€.", null) + ); } /** @@ -82,28 +69,23 @@ public ResponseEntity> deleteFolder( * @param body μˆ˜μ •ν•  폴더 κ°’ */ @PatchMapping("/{folderId}") - public ResponseEntity> updateFolderName( + public ResponseEntity>> updateFolderName( @PathVariable Integer folderId, @RequestBody Map body, @AuthenticationPrincipal CustomUserDetails userDetails ) { - if (folderId == 0) { - var res = new java.util.HashMap(); - res.put("status", 400); - res.put("msg", "default ν΄λ”λŠ” 이름을 λ³€κ²½ν•  수 μ—†μŠ΅λ‹ˆλ‹€."); - res.put("data", null); - return ResponseEntity.badRequest().body(res); - } + if (folderId == 0) + throw new IllegalArgumentException("default ν΄λ”λŠ” 이름을 λ³€κ²½ν•  수 μ—†μŠ΅λ‹ˆλ‹€."); + Member member = userDetails.getMember(); String newName = body.get("folderName"); - String updatedName = folderService.updateFolderName(member.getId(), folderId, newName); + String updatedName = personalArchiveFolderService.updateFolderName(member.getId(), folderId, newName); - return ResponseEntity.ok(java.util.Map.of( - "status", 200, - "msg", "폴더 이름이 " + updatedName + " 으둜 λ³€κ²½λμŠ΅λ‹ˆλ‹€.", - "data", java.util.Map.of("folderName", updatedName) - )); + return ResponseEntity.ok( + new RsData<>("200", "폴더 이름이 " + updatedName + " 으둜 λ³€κ²½λμŠ΅λ‹ˆλ‹€.", + Map.of("folderName", updatedName)) + ); } /** @@ -112,18 +94,14 @@ public ResponseEntity> updateFolderName( */ @Operation(summary = "폴더 이름 쑰회", description = "λ‚΄ PersonalArchive μ•ˆμ— 이름을 μ „λΆ€ μ‘°νšŒν•©λ‹ˆλ‹€.") @GetMapping - public ResponseEntity getFolders( + public ResponseEntity>> getFolders( @AuthenticationPrincipal CustomUserDetails userDetails ) { Member member = userDetails.getMember(); - List folders = folderService.getFoldersForPersonal(member.getId()); + List folders = personalArchiveFolderService.getFolders(member.getId()); return ResponseEntity.ok( - Map.of( - "status", 200, - "msg", "개인 μ•„μΉ΄μ΄λΈŒμ˜ 폴더 λͺ©λ‘μ„ λΆˆλŸ¬μ™”μŠ΅λ‹ˆλ‹€.", - "data", Map.of("folders", folders) - ) + new RsData<>("200", "개인 μ•„μΉ΄μ΄λΈŒμ˜ 폴더 λͺ©λ‘μ„ λΆˆλŸ¬μ™”μŠ΅λ‹ˆλ‹€.", folders) ); } @@ -138,22 +116,14 @@ public ResponseEntity getFilesInFolder( int memberId = userDetails.getMember().getId(); Integer targetFolderId = (folderId == 0) - ? folderService.getDefaultFolderId(memberId) + ? personalArchiveFolderService.getDefaultFolderId(memberId) : folderId; - FolderFilesDto rs = folderService.getFilesInFolderForPersonal(memberId, targetFolderId); - - return ResponseEntity.ok(Map.of( - "status", 200, - "msg", folderId == 0 ? "κΈ°λ³Έ ν΄λ”μ˜ 파일 λͺ©λ‘μ„ λΆˆλŸ¬μ™”μŠ΅λ‹ˆλ‹€." : "ν•΄λ‹Ή ν΄λ”μ˜ 파일 λͺ©λ‘μ„ λΆˆλŸ¬μ™”μŠ΅λ‹ˆλ‹€.", - "data", Map.of( - "folder", Map.of( - "folderId", rs.folderId(), - "folderName", rs.folderName() - ), - "files", rs.files() - ) - )); + FolderFilesDto rs = personalArchiveFolderService.getFilesInFolder(memberId, targetFolderId); + + return ResponseEntity.ok( + new RsData<>("200","ν•΄λ‹Ή ν΄λ”μ˜ 파일 λͺ©λ‘μ„ λΆˆλŸ¬μ™”μŠ΅λ‹ˆλ‹€.", rs) + ); } } diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/archive/folder/dto/FolderResponse.java b/src/main/java/org/tuna/zoopzoop/backend/domain/archive/folder/dto/FolderResponse.java index e6063343..78d38b04 100644 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/archive/folder/dto/FolderResponse.java +++ b/src/main/java/org/tuna/zoopzoop/backend/domain/archive/folder/dto/FolderResponse.java @@ -1,7 +1,6 @@ package org.tuna.zoopzoop.backend.domain.archive.folder.dto; public record FolderResponse( - int folderId, - String folderName - + String folderName, + int folderId ) {} diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/archive/folder/repository/FolderRepository.java b/src/main/java/org/tuna/zoopzoop/backend/domain/archive/folder/repository/FolderRepository.java index cc2215f4..91b68ca7 100644 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/archive/folder/repository/FolderRepository.java +++ b/src/main/java/org/tuna/zoopzoop/backend/domain/archive/folder/repository/FolderRepository.java @@ -72,4 +72,17 @@ Optional findByIdAndMemberId(@Param("folderId") Integer folderId, where pa.member.id = :memberId and f.isDefault = true """) Optional findDefaultByMemberId(@Param("memberId") Integer memberId); + + Optional findByIdAndArchiveId(Integer folderId, Integer archiveId); + + @Query(""" + select f.name + from Folder f + where f.archive.id = :archiveId + and f.name = :name + and f.id <> :excludeFolderId + """) + List existsNameInArchiveExceptSelf(@Param("archiveId") Integer archiveId, + @Param("name") String name, + @Param("excludeFolderId") Integer excludeFolderId); } diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/archive/folder/service/FolderService.java b/src/main/java/org/tuna/zoopzoop/backend/domain/archive/folder/service/FolderService.java index 6250d622..b5a39118 100644 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/archive/folder/service/FolderService.java +++ b/src/main/java/org/tuna/zoopzoop/backend/domain/archive/folder/service/FolderService.java @@ -6,8 +6,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.tuna.zoopzoop.backend.domain.archive.archive.entity.Archive; -import org.tuna.zoopzoop.backend.domain.archive.archive.entity.PersonalArchive; -import org.tuna.zoopzoop.backend.domain.archive.archive.repository.PersonalArchiveRepository; import org.tuna.zoopzoop.backend.domain.archive.folder.dto.FolderResponse; import org.tuna.zoopzoop.backend.domain.archive.folder.entity.Folder; import org.tuna.zoopzoop.backend.domain.archive.folder.repository.FolderRepository; @@ -16,8 +14,6 @@ import org.tuna.zoopzoop.backend.domain.datasource.entity.DataSource; import org.tuna.zoopzoop.backend.domain.datasource.entity.Tag; import org.tuna.zoopzoop.backend.domain.datasource.repository.DataSourceRepository; -import org.tuna.zoopzoop.backend.domain.member.entity.Member; -import org.tuna.zoopzoop.backend.domain.member.repository.MemberRepository; import java.time.LocalDate; import java.util.HashSet; @@ -29,32 +25,19 @@ @RequiredArgsConstructor public class FolderService { - private final MemberRepository memberRepository; - private final PersonalArchiveRepository personalArchiveRepository; private final FolderRepository folderRepository; private final DataSourceRepository dataSourceRepository; - /** - * ν˜„μž¬ 둜그인 μ‚¬μš©μžμ˜ PersonalArchive에 폴더 생성 - * - 폴더λͺ… 쀑볡 μ‹œ "(n)" μΆ”κ°€ - * - λ™μ‹œμ„± 좩돌 μ‹œ(더블 클릭, λΈŒλΌμš°μ € μž¬μ „μ†‘) μž¬μ‹œλ„ - */ + // ===== 생성 ===== @Transactional - public FolderResponse createFolderForPersonal(Integer currentMemberId, String folderName) { + public FolderResponse createFolder(Archive archive, String folderName) { + if (archive == null) throw new NoResultException("μ•„μΉ΄μ΄λΈŒκ°€ μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€."); if (folderName == null || folderName.trim().isEmpty()) throw new IllegalArgumentException("폴더 이름은 λΉ„μ–΄ μžˆμ„ 수 μ—†μŠ΅λ‹ˆλ‹€."); - Member member = memberRepository.findById(currentMemberId) - .orElseThrow(() -> new NoResultException("멀버λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€.")); - - Archive archive = personalArchiveRepository.findByMemberId(member.getId()) - .map(PersonalArchive::getArchive) - .orElseThrow(() -> new NoResultException("개인 μ•„μΉ΄μ΄λΈŒκ°€ μ—†μŠ΅λ‹ˆλ‹€.")); - final String requested = folderName.trim(); - - // λ™μ‹œμ„± μΆ›λŒμ‹œ 2번 μž¬μ‹œλ„ String unique = generateUniqueFolderName(archive.getId(), requested); + for (int attempt = 0; attempt < 2; attempt++) { try { Folder folder = new Folder(); @@ -63,7 +46,7 @@ public FolderResponse createFolderForPersonal(Integer currentMemberId, String fo folder.setDefault(false); Folder saved = folderRepository.save(folder); - return new FolderResponse( saved.getId(), saved.getName()); + return new FolderResponse(saved.getName(), saved.getId()); } catch (DataIntegrityViolationException e) { unique = generateUniqueFolderName(archive.getId(), requested); } @@ -71,64 +54,20 @@ public FolderResponse createFolderForPersonal(Integer currentMemberId, String fo throw new IllegalStateException("λ™μ‹œμ„± 좩돌둜 폴더 생성에 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€. μž μ‹œ ν›„ λ‹€μ‹œ μ‹œλ„ν•΄μ£Όμ„Έμš”."); } - private static final Pattern SUFFIX_PATTERN = Pattern.compile("^(.*?)(?: \\((\\d+)\\))?$"); - - /** - * κΈ°μ‘΄ file λͺ…κ³Ό κ°™μ§€ μ•Šμ€ μ΅œμ†Ÿκ°’μ˜ 이름 생성 - * β€œν΄λ”λͺ…”, "폴더λͺ… (1)"β†’ "폴더λͺ… (2)" - * "폴더λͺ…", "폴더λͺ… (2)" -> "폴더λͺ… (1)" - */ - private String generateUniqueFolderName(Integer archiveId, String requested) { - NameParts nameParts = NameParts.split(requested); - - // 쀑볡 폴더λͺ… 탐색 - String file = nameParts.base(); - String fileEnd = file + "\uffff"; - - List existing = folderRepository.findNamesForConflictCheck(archiveId, file, fileEnd); - - return pickNextAvailable(file, existing); - } - - /** - * 이미 μ‘΄μž¬ν•˜λŠ” 이름듀 쀑 κ°€μž₯ μž‘μ€ λΉ„μ–΄ μžˆλŠ” 번호 λ°˜ν™˜ - */ - private static String pickNextAvailable(String file, List existing) { - boolean baseUsed = false; - Set used = new HashSet<>(); - Pattern p = Pattern.compile("^" + Pattern.quote(file) + "(?: \\((\\d+)\\))?$"); - - for (String s : existing) { - var m = p.matcher(s); - if (m.matches()) { - if (m.group(1) == null) baseUsed = true; - else used.add(Integer.parseInt(m.group(1))); - } - } - if (!baseUsed) return file; - for (int k = 1; k <= used.size() + 1; k++) { - if (!used.contains(k)) return file + " (" + k + ")"; - } - return file + " (" + (used.size() + 1) + ")"; // fallback - } - - /** - * folderId에 ν•΄λ‹Ήν•˜λŠ” 폴더 영ꡬ μ‚­μ œ - */ + // ===== μ‚­μ œ ===== @Transactional - public String deleteFolder(Integer currentId, Integer folderId) { - // μ†Œμœ ν•œ 폴더인지 확인 - Folder folder = folderRepository.findByIdAndMemberId(folderId, currentId) + public String deleteFolder(Archive archive, Integer folderId) { + Folder folder = folderRepository.findByIdAndArchiveId(folderId, archive.getId()) .orElseThrow(() -> new NoResultException("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ν΄λ”μž…λ‹ˆλ‹€.")); - if (folder.isDefault()) { + if (folder.isDefault()) throw new IllegalArgumentException("default ν΄λ”λŠ” μ‚­μ œν•  수 μ—†μŠ΅λ‹ˆλ‹€."); - } - Folder defaultFolder = folderRepository.findDefaultByMemberId(currentId) + // κΈ°λ³Έ 폴더 확보 (같은 archive) + Folder defaultFolder = folderRepository.findByArchiveIdAndIsDefaultTrue(archive.getId()) .orElseThrow(() -> new IllegalStateException("default 폴더가 μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.")); - // 폴더 λ‚΄ μžλ£Œλ“€μ„ Default둜 이관 + soft delete + // 폴더 λ‚΄ 자료 이관 + soft delete(λ„€ μ •μ±… μœ μ§€) List dataSources = dataSourceRepository.findAllByFolderId(folderId); LocalDate now = LocalDate.now(); for (DataSource ds : dataSources) { @@ -139,71 +78,56 @@ public String deleteFolder(Integer currentId, Integer folderId) { String name = folder.getName(); folderRepository.delete(folder); - return name; } - /** - * folderId에 ν•΄λ‹Ήν•˜λŠ” 이름 λ³€κ²½ - */ + // ===== 이름 λ³€κ²½ ===== @Transactional - public String updateFolderName(Integer currentId, Integer folderId, String newName) { - Folder folder = folderRepository.findByIdAndMemberId(folderId, currentId) + public String updateFolderName(Archive archive, Integer folderId, String newName) { + if (newName == null || newName.trim().isEmpty()) + throw new IllegalArgumentException("폴더 이름은 λΉ„μ–΄ μžˆμ„ 수 μ—†μŠ΅λ‹ˆλ‹€."); + + Folder folder = folderRepository.findByIdAndArchiveId(folderId, archive.getId()) .orElseThrow(() -> new NoResultException("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ν΄λ”μž…λ‹ˆλ‹€.")); - // 같은 μ•„μΉ΄μ΄λΈŒ λ‚΄μ—μ„œ 쀑볡 폴더 이름 확인 - List existingNames = folderRepository.findNamesForConflictCheck( - folder.getArchive().getId(), - newName, - folder.getName() // 자기 μžμ‹ μ€ μ œμ™Έ - ); + if (folder.isDefault()) + throw new IllegalArgumentException("default ν΄λ”λŠ” 이름을 λ³€κ²½ν•  수 μ—†μŠ΅λ‹ˆλ‹€."); - if (!existingNames.isEmpty()) { + // 같은 Archive λ‚΄ 동λͺ… 검사 (자기 μžμ‹  μ œμ™Έ) + List conflict = folderRepository.existsNameInArchiveExceptSelf( + archive.getId(), newName.trim(), folder.getId()); + if (!conflict.isEmpty()) { throw new IllegalArgumentException("이미 μ‘΄μž¬ν•˜λŠ” 폴더λͺ…μž…λ‹ˆλ‹€."); } - folder.setName(newName); + folder.setName(newName.trim()); folderRepository.save(folder); - return newName; + return folder.getName(); } - /** - * Personal Archive의 폴더λͺ… μ „λΆ€ 쑰회 - * @param memberId Personal Archive νšŒμ› Id - */ + // ===== λͺ©λ‘ 쑰회 ===== @Transactional(readOnly = true) - public List getFoldersForPersonal(Integer memberId) { - PersonalArchive personalArchive = personalArchiveRepository.findByMemberId(memberId) - .orElseThrow(() -> new NoResultException("개인 μ•„μΉ΄μ΄λΈŒκ°€ μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.")); - Archive archive = personalArchive.getArchive(); - + public List getFolders(Archive archive) { return folderRepository.findByArchive(archive).stream() - .map(folder -> new FolderResponse(folder.getId(), folder.getName())) + .map(f -> new FolderResponse(f.getName(), f.getId())) .toList(); } - /** - * 폴더 ν•˜μœ„ 파일(datasource) 쑰회 - * @param memberId Personal Archive νšŒμ› Id - * @param folderId μ‘°νšŒν•  folder Id - */ + // ===== 폴더 λ‚΄ 파일 쑰회 ===== @Transactional(readOnly = true) - public FolderFilesDto getFilesInFolderForPersonal(Integer memberId, Integer folderId) { - Folder folder = folderRepository.findByIdAndMemberId(folderId, memberId) + public FolderFilesDto getFilesInFolder(Archive archive, Integer folderId) { + Folder folder = folderRepository.findByIdAndArchiveId(folderId, archive.getId()) .orElseThrow(() -> new NoResultException("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ν΄λ”μž…λ‹ˆλ‹€.")); var files = dataSourceRepository.findAllByFolder(folder).stream() .map(ds -> new FileSummary( ds.getId(), ds.getTitle(), - ds.getDataCreatedDate(), // LocalDate + ds.getDataCreatedDate(), ds.getSummary(), ds.getSourceUrl(), ds.getImageUrl(), - ds.getTags() == null ? List.of() - : ds.getTags().stream() - .map(Tag::getTagName) - .toList(), + ds.getTags() == null ? List.of() : ds.getTags().stream().map(Tag::getTagName).toList(), ds.getCategory() == null ? null : ds.getCategory().name() )) .toList(); @@ -211,19 +135,43 @@ public FolderFilesDto getFilesInFolderForPersonal(Integer memberId, Integer fold return new FolderFilesDto(folder.getId(), folder.getName(), files); } + // ===== κΈ°λ³Έ 폴더 ID 쑰회 (Archive μŠ€μ½”ν”„) ===== + @Transactional(readOnly = true) + public Integer getDefaultFolderId(Archive archive) { + return folderRepository.findByArchiveIdAndIsDefaultTrue(archive.getId()) + .orElseThrow(() -> new NoResultException("default 폴더λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€.")) + .getId(); + } - public Integer getDefaultFolderId(int memberId) { - Folder folder = folderRepository.findDefaultFolderByMemberId(memberId) - .orElseThrow(() -> new NoResultException("default 폴더λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€.")); - return folder.getId(); + // ===== 이름 좩돌 μœ ν‹Έ ===== + private static final Pattern SUFFIX_PATTERN = Pattern.compile("^(.*?)(?: \\((\\d+)\\))?$"); + private String generateUniqueFolderName(Integer archiveId, String requested) { + NameParts nameParts = NameParts.split(requested); + String file = nameParts.base(); + String fileEnd = file + "\uffff"; + List existing = folderRepository.findNamesForConflictCheck(archiveId, file, fileEnd); + return pickNextAvailable(file, existing); + } + + private static String pickNextAvailable(String file, List existing) { + boolean baseUsed = false; + Set used = new HashSet<>(); + Pattern p = Pattern.compile("^" + Pattern.quote(file) + "(?: \\((\\d+)\\))?$"); + for (String s : existing) { + var m = p.matcher(s); + if (m.matches()) { + if (m.group(1) == null) baseUsed = true; + else used.add(Integer.parseInt(m.group(1))); + } + } + if (!baseUsed) return file; + for (int k = 1; k <= used.size() + 1; k++) { + if (!used.contains(k)) return file + " (" + k + ")"; + } + return file + " (" + (used.size() + 1) + ")"; } - /** - * μž…λ ₯된 폴더λͺ…을 (폴더λͺ…, 숫자)둜 λΆ„λ¦¬ν•˜λŠ” μœ ν‹Έ 클래슀 - * β€œν΄λ”λͺ…” β†’ (”폴더λͺ…”, null) - * β€œν΄λ”λͺ…(3)” β†’ (”폴더λͺ…”, 3) - */ private record NameParts(String base, Integer num) { static NameParts split(String name) { var m = SUFFIX_PATTERN.matcher(name.trim()); @@ -236,3 +184,4 @@ static NameParts split(String name) { } } } + diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/archive/folder/service/PersonalArchiveFolderService.java b/src/main/java/org/tuna/zoopzoop/backend/domain/archive/folder/service/PersonalArchiveFolderService.java new file mode 100644 index 00000000..73edaa7d --- /dev/null +++ b/src/main/java/org/tuna/zoopzoop/backend/domain/archive/folder/service/PersonalArchiveFolderService.java @@ -0,0 +1,69 @@ +package org.tuna.zoopzoop.backend.domain.archive.folder.service; + +import jakarta.persistence.NoResultException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.tuna.zoopzoop.backend.domain.archive.archive.entity.Archive; +import org.tuna.zoopzoop.backend.domain.archive.archive.entity.PersonalArchive; +import org.tuna.zoopzoop.backend.domain.archive.archive.repository.PersonalArchiveRepository; +import org.tuna.zoopzoop.backend.domain.archive.folder.dto.FolderResponse; +import org.tuna.zoopzoop.backend.domain.archive.folder.entity.Folder; +import org.tuna.zoopzoop.backend.domain.archive.folder.repository.FolderRepository; +import org.tuna.zoopzoop.backend.domain.datasource.dto.FolderFilesDto; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class PersonalArchiveFolderService { + + private final PersonalArchiveRepository personalArchiveRepository; + private final FolderRepository folderRepository; + private final FolderService folderService; + + @Transactional + public FolderResponse createFolder(Integer memberId, String folderName) { + Archive archive = personalArchiveRepository.findByMemberId(memberId) + .map(PersonalArchive::getArchive) + .orElseThrow(() -> new NoResultException("개인 μ•„μΉ΄μ΄λΈŒκ°€ μ—†μŠ΅λ‹ˆλ‹€.")); + return folderService.createFolder(archive, folderName); + } + + @Transactional + public String deleteFolder(Integer memberId, Integer folderId) { + // 개인 μ „μš© β€œμ†Œμœ  확인” 쿼리둜 λΉ λ₯΄κ²Œ κ°€λ“œ + Folder folder = folderRepository.findByIdAndMemberId(folderId, memberId) + .orElseThrow(() -> new NoResultException("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ν΄λ”μž…λ‹ˆλ‹€.")); + return folderService.deleteFolder(folder.getArchive(), folderId); + } + + @Transactional + public String updateFolderName(Integer memberId, Integer folderId, String newName) { + Folder folder = folderRepository.findByIdAndMemberId(folderId, memberId) + .orElseThrow(() -> new NoResultException("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ν΄λ”μž…λ‹ˆλ‹€.")); + return folderService.updateFolderName(folder.getArchive(), folderId, newName); + } + + @Transactional(readOnly = true) + public List getFolders(Integer memberId) { + Archive archive = personalArchiveRepository.findByMemberId(memberId) + .map(PersonalArchive::getArchive) + .orElseThrow(() -> new NoResultException("개인 μ•„μΉ΄μ΄λΈŒκ°€ μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.")); + return folderService.getFolders(archive); + } + + @Transactional(readOnly = true) + public FolderFilesDto getFilesInFolder(Integer memberId, Integer folderId) { + Folder folder = folderRepository.findByIdAndMemberId(folderId, memberId) + .orElseThrow(() -> new NoResultException("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ν΄λ”μž…λ‹ˆλ‹€.")); + return folderService.getFilesInFolder(folder.getArchive(), folderId); + } + + @Transactional(readOnly = true) + public Integer getDefaultFolderId(Integer memberId) { + Folder folder = folderRepository.findDefaultFolderByMemberId(memberId) + .orElseThrow(() -> new NoResultException("default 폴더λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€.")); + return folder.getId(); + } +} diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/controller/DatasourceController.java b/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/controller/DatasourceController.java index f24466fe..9443e2f8 100644 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/controller/DatasourceController.java +++ b/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/controller/DatasourceController.java @@ -231,6 +231,7 @@ public ResponseEntity search( @RequestParam(required = false) String title, @RequestParam(required = false) String summary, @RequestParam(required = false) String category, + @RequestParam(required = false) String keyword, @RequestParam(required = false) Integer folderId, @RequestParam(required = false) String folderName, @RequestParam(required = false, defaultValue = "true") Boolean isActive, @@ -247,6 +248,7 @@ public ResponseEntity search( .folderId(folderId) .folderName(folderName) .isActive(isActive) + .keyword(keyword) .build(); Page page = dataSourceService.search(memberId, cond, pageable); diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/dto/DataSourceSearchCondition.java b/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/dto/DataSourceSearchCondition.java index 19778980..f05bb2ab 100644 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/dto/DataSourceSearchCondition.java +++ b/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/dto/DataSourceSearchCondition.java @@ -12,4 +12,5 @@ public class DataSourceSearchCondition { private final Integer folderId; private final String folderName; private final Boolean isActive; + private final String keyword; } diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/dto/DataSourceSearchItem.java b/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/dto/DataSourceSearchItem.java index 86a1dc03..d1ef68c0 100644 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/dto/DataSourceSearchItem.java +++ b/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/dto/DataSourceSearchItem.java @@ -13,6 +13,7 @@ public class DataSourceSearchItem { private String title; private LocalDate dataCreatedDate; private String summary; + private String source; private String sourceUrl; private String imageUrl; private List tags; diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/repository/DataSourceQRepositoryImpl.java b/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/repository/DataSourceQRepositoryImpl.java index 5bf745bc..f2d05960 100644 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/repository/DataSourceQRepositoryImpl.java +++ b/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/repository/DataSourceQRepositoryImpl.java @@ -20,6 +20,8 @@ import java.util.*; import java.util.stream.Collectors; +import static org.springframework.util.StringUtils.hasText; + @Repository @RequiredArgsConstructor public class DataSourceQRepositoryImpl implements DataSourceQRepository { @@ -52,6 +54,15 @@ public Page search(Integer memberId, DataSourceSearchCondi if (cond.getCategory() != null && !cond.getCategory().isBlank()) { where.and(ds.category.stringValue().containsIgnoreCase(cond.getCategory())); } + if (hasText(cond.getKeyword())) { + String kw = cond.getKeyword(); + where.and( + ds.title.containsIgnoreCase(kw) + .or(ds.summary.containsIgnoreCase(kw)) + .or(ds.category.stringValue().containsIgnoreCase(kw)) + ); + } + if (cond.getFolderName() != null && !cond.getFolderName().isBlank()) { where.and(ds.folder.name.eq(cond.getFolderName())); } @@ -72,7 +83,7 @@ public Page search(Integer memberId, DataSourceSearchCondi // content JPAQuery contentQuery = queryFactory - .select(ds.id, ds.title, ds.dataCreatedDate, ds.summary, ds.sourceUrl, ds.imageUrl, ds.category) + .select(ds.id, ds.title, ds.dataCreatedDate, ds.summary, ds.source, ds.sourceUrl, ds.imageUrl, ds.category) .from(ds) .join(ds.folder, folder) .join(pa).on(pa.archive.eq(folder.archive)) @@ -110,13 +121,13 @@ public Page search(Integer memberId, DataSourceSearchCondi Collectors.mapping(row -> row.get(tag.tagName), Collectors.toList()) )); - // map to DTO List content = tuples.stream() .map(row -> new DataSourceSearchItem( row.get(ds.id), row.get(ds.title), row.get(ds.dataCreatedDate), row.get(ds.summary), + row.get(ds.source), row.get(ds.sourceUrl), row.get(ds.imageUrl), tagsById.getOrDefault(row.get(ds.id), List.of()), @@ -140,7 +151,7 @@ private List> toOrderSpecifiers(Sort sort) { switch (o.getProperty()) { case "title" -> specs.add(new OrderSpecifier<>(dir, root.getString("title"))); - case "createdAt" -> // μš”μ²­ ν‚€ + case "createdAt" -> specs.add(new OrderSpecifier<>(dir, root.getDate("dataCreatedDate", java.time.LocalDate.class))); default -> { } } diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/service/DataSourceService.java b/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/service/DataSourceService.java index 3c30ef6b..00648e93 100644 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/service/DataSourceService.java +++ b/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/service/DataSourceService.java @@ -12,7 +12,7 @@ import org.tuna.zoopzoop.backend.domain.archive.archive.repository.PersonalArchiveRepository; import org.tuna.zoopzoop.backend.domain.archive.folder.entity.Folder; import org.tuna.zoopzoop.backend.domain.archive.folder.repository.FolderRepository; -import org.tuna.zoopzoop.backend.domain.archive.folder.service.FolderService; +import org.tuna.zoopzoop.backend.domain.archive.folder.service.PersonalArchiveFolderService; import org.tuna.zoopzoop.backend.domain.datasource.dataprocessor.service.DataProcessorService; import org.tuna.zoopzoop.backend.domain.datasource.dto.DataSourceDto; import org.tuna.zoopzoop.backend.domain.datasource.dto.DataSourceSearchCondition; @@ -34,7 +34,7 @@ public class DataSourceService { private final DataSourceRepository dataSourceRepository; private final FolderRepository folderRepository; - private final FolderService folderService; + private final PersonalArchiveFolderService folderService; private final PersonalArchiveRepository personalArchiveRepository; private final TagRepository tagRepository; private final DataProcessorService dataProcessorService; diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/news/service/NewsService.java b/src/main/java/org/tuna/zoopzoop/backend/domain/news/service/NewsService.java index 4ef7b5d2..b6b89221 100644 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/news/service/NewsService.java +++ b/src/main/java/org/tuna/zoopzoop/backend/domain/news/service/NewsService.java @@ -2,7 +2,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import org.tuna.zoopzoop.backend.domain.archive.folder.service.FolderService; +import org.tuna.zoopzoop.backend.domain.archive.folder.service.PersonalArchiveFolderService; import org.tuna.zoopzoop.backend.domain.datasource.dto.FileSummary; import org.tuna.zoopzoop.backend.domain.datasource.dto.FolderFilesDto; @@ -14,10 +14,10 @@ @Service @RequiredArgsConstructor public class NewsService { - private final FolderService folderService; + private final PersonalArchiveFolderService folderService; public List getTagFrequencyFromFiles(Integer memberId, Integer folderId) { - FolderFilesDto folderFilesDto = folderService.getFilesInFolderForPersonal(memberId, folderId); + FolderFilesDto folderFilesDto = folderService.getFilesInFolder(memberId, folderId); List files = folderFilesDto.files(); diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/space/archive/controller/SpaceArchiveFolderController.java b/src/main/java/org/tuna/zoopzoop/backend/domain/space/archive/controller/SpaceArchiveFolderController.java new file mode 100644 index 00000000..d82808ae --- /dev/null +++ b/src/main/java/org/tuna/zoopzoop/backend/domain/space/archive/controller/SpaceArchiveFolderController.java @@ -0,0 +1,126 @@ +package org.tuna.zoopzoop.backend.domain.space.archive.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; +import org.tuna.zoopzoop.backend.domain.archive.folder.dto.FolderResponse; +import org.tuna.zoopzoop.backend.domain.archive.folder.dto.reqBodyForCreateFolder; +import org.tuna.zoopzoop.backend.domain.archive.folder.dto.resBodyForCreateFolder; +import org.tuna.zoopzoop.backend.domain.datasource.dto.FolderFilesDto; +import org.tuna.zoopzoop.backend.domain.member.entity.Member; +import org.tuna.zoopzoop.backend.domain.space.archive.service.SpaceArchiveFolderService; +import org.tuna.zoopzoop.backend.global.rsData.RsData; +import org.tuna.zoopzoop.backend.global.security.jwt.CustomUserDetails; + +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api/v1/spaces/{spaceId}/archive/folder") +@RequiredArgsConstructor +@Tag(name = "SpaceArchiveFolder", description = "곡유 μ•„μΉ΄μ΄λΈŒμ˜ 폴더 CRUD") +public class SpaceArchiveFolderController { + + private final SpaceArchiveFolderService spaceArchiveFolderService; + + /** + * 곡유 μ•„μΉ΄μ΄λΈŒ μ•ˆμ— μƒˆ 폴더 생성 + */ + @Operation(summary = "폴더 생성", description = "ν•΄λ‹Ή 슀페이슀의 곡유 μ•„μΉ΄μ΄λΈŒμ— μƒˆ 폴더λ₯Ό μƒμ„±ν•©λ‹ˆλ‹€.") + @PostMapping + public RsData createFolder( + @PathVariable Integer spaceId, + @Valid @RequestBody reqBodyForCreateFolder rq, + @AuthenticationPrincipal CustomUserDetails userDetails + ) { + Member requester = userDetails.getMember(); + FolderResponse fr = spaceArchiveFolderService.createFolder(spaceId, requester, rq.folderName()); + resBodyForCreateFolder rs = new resBodyForCreateFolder(fr.folderName(), fr.folderId()); + return new RsData<>( + "200", + rq.folderName() + " 폴더가 μƒμ„±λ˜μ—ˆμŠ΅λ‹ˆλ‹€.", + rs + ); + } + + /** + * 곡유 μ•„μΉ΄μ΄λΈŒμ˜ 폴더 μ‚­μ œ + */ + @Operation(summary = "폴더 μ‚­μ œ", description = "ν•΄λ‹Ή 슀페이슀의 곡유 μ•„μΉ΄μ΄λΈŒμ—μ„œ 폴더λ₯Ό μ‚­μ œν•©λ‹ˆλ‹€.") + @DeleteMapping("/{folderId}") + public ResponseEntity> deleteFolder( + @PathVariable Integer spaceId, + @PathVariable Integer folderId, + @AuthenticationPrincipal CustomUserDetails userDetails + ) { + if (folderId == 0) + throw new IllegalArgumentException("default ν΄λ”λŠ” μ‚­μ œν•  수 μ—†μŠ΅λ‹ˆλ‹€."); + + + String deletedFolderName = spaceArchiveFolderService.deleteFolder(spaceId, userDetails.getMember(), folderId); + return ResponseEntity.ok().body( + new RsData<>("200", deletedFolderName + " 폴더가 μ‚­μ œλμŠ΅λ‹ˆλ‹€.", null) + ); + } + + /** + * 곡유 μ•„μΉ΄μ΄λΈŒμ˜ 폴더 이름 μˆ˜μ • + */ + @Operation(summary = "폴더 이름 μˆ˜μ •", description = "ν•΄λ‹Ή 슀페이슀의 곡유 μ•„μΉ΄μ΄λΈŒμ—μ„œ 폴더 이름을 λ³€κ²½ν•©λ‹ˆλ‹€.") + @PatchMapping("/{folderId}") + public ResponseEntity> updateFolderName( + @PathVariable Integer spaceId, + @PathVariable Integer folderId, + @RequestBody Map body, + @AuthenticationPrincipal CustomUserDetails principal + ) { + if (folderId == 0) + throw new IllegalArgumentException("default ν΄λ”λŠ” 이름을 λ³€κ²½ν•  수 μ—†μŠ΅λ‹ˆλ‹€."); + + String updatename = spaceArchiveFolderService.updateFolderName(spaceId, principal.getMember(), folderId, body.get("folderName")); + return ResponseEntity.ok().body( + new RsData<>("200", "폴더 이름이 " + updatename + "(으)둜 λ³€κ²½λμŠ΅λ‹ˆλ‹€.", new FolderResponse(updatename, folderId)) + ); + } + + /** + * 곡유 μ•„μΉ΄μ΄λΈŒμ˜ 폴더 λͺ©λ‘ 쑰회 + */ + @Operation(summary = "폴더 이름 쑰회", description = "ν•΄λ‹Ή 슀페이슀의 곡유 μ•„μΉ΄μ΄λΈŒ 폴더 λͺ©λ‘μ„ μ‘°νšŒν•©λ‹ˆλ‹€.") + @GetMapping + public ResponseEntity>> listFolders( + @PathVariable Integer spaceId, + @AuthenticationPrincipal CustomUserDetails userDetails + ) { + List folders = spaceArchiveFolderService.getFolders(spaceId, userDetails.getMember()); + return ResponseEntity.ok().body( + new RsData<>("200", "곡유 μ•„μΉ΄μ΄λΈŒμ˜ 폴더 λͺ©λ‘μ΄ μ‘°νšŒλ˜μ—ˆμŠ΅λ‹ˆλ‹€.", folders) + ); + } + + /** + * 곡유 μ•„μΉ΄μ΄λΈŒμ˜ νŠΉμ • 폴더 λ‚΄ 파일 λͺ©λ‘ 쑰회 + */ + @Operation(summary = "폴더 λ‚΄ 파일 쑰회", description = "ν•΄λ‹Ή 슀페이슀의 곡유 μ•„μΉ΄μ΄λΈŒμ—μ„œ νŠΉμ • 폴더 λ‚΄ 파일 λͺ©λ‘μ„ μ‘°νšŒν•©λ‹ˆλ‹€.") + @GetMapping("/{folderId}/files") + public ResponseEntity> filesInFolder( + @PathVariable Integer spaceId, + @PathVariable Integer folderId, + @AuthenticationPrincipal CustomUserDetails userDetails + ) { + Integer target = (folderId == 0) + ? spaceArchiveFolderService.getDefaultFolderId(spaceId, userDetails.getMember()) + : folderId; + + FolderFilesDto rs = spaceArchiveFolderService.getFilesInFolder(spaceId, userDetails.getMember(), target); + + return ResponseEntity.ok().body( + new RsData<>("200", "폴더 μ•ˆμ˜ 파일 λͺ©λ‘μ„ λΆˆλŸ¬μ™”μŠ΅λ‹ˆλ‹€.", rs) + ); + } +} + diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/space/archive/service/SpaceArchiveFolderService.java b/src/main/java/org/tuna/zoopzoop/backend/domain/space/archive/service/SpaceArchiveFolderService.java new file mode 100644 index 00000000..bcc51ee9 --- /dev/null +++ b/src/main/java/org/tuna/zoopzoop/backend/domain/space/archive/service/SpaceArchiveFolderService.java @@ -0,0 +1,124 @@ +package org.tuna.zoopzoop.backend.domain.space.archive.service; + +import jakarta.persistence.NoResultException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.tuna.zoopzoop.backend.domain.archive.archive.entity.Archive; +import org.tuna.zoopzoop.backend.domain.archive.archive.entity.SharingArchive; +import org.tuna.zoopzoop.backend.domain.archive.folder.dto.FolderResponse; +import org.tuna.zoopzoop.backend.domain.archive.folder.service.FolderService; +import org.tuna.zoopzoop.backend.domain.datasource.dto.FolderFilesDto; +import org.tuna.zoopzoop.backend.domain.member.entity.Member; +import org.tuna.zoopzoop.backend.domain.space.membership.enums.Authority; +import org.tuna.zoopzoop.backend.domain.space.membership.service.MembershipService; +import org.tuna.zoopzoop.backend.domain.space.space.entity.Space; +import org.tuna.zoopzoop.backend.domain.space.space.service.SpaceService; + +import java.util.List; +import java.util.Optional; + +// package org.tuna.zoopzoop.backend.domain.space.archive.service; + +@Service +@RequiredArgsConstructor +public class SpaceArchiveFolderService { + + private final SpaceService spaceService; + private final MembershipService membershipService; + private final FolderService folderService; + + @Transactional + public FolderResponse createFolder(Integer spaceId, Member requester, String folderName) { + Space space = spaceService.findById(spaceId); + + if (!membershipService.isMemberJoinedSpace(requester, space)) + throw new SecurityException("슀페이슀의 ꡬ성원이 μ•„λ‹™λ‹ˆλ‹€."); + + var m = membershipService.findByMemberAndSpace(requester, space); + var auth = m.getAuthority(); + if (auth == Authority.PENDING || auth == Authority.READ_ONLY) + throw new SecurityException("폴더 생성 κΆŒν•œμ΄ μ—†μŠ΅λ‹ˆλ‹€."); + + Archive archive = Optional.ofNullable(space.getSharingArchive()) + .map(SharingArchive::getArchive) + .orElseThrow(() -> new NoResultException("슀페이슀의 곡유 μ•„μΉ΄μ΄λΈŒκ°€ μ—†μŠ΅λ‹ˆλ‹€.")); + + return folderService.createFolder(archive, folderName); + } + + @Transactional + public String deleteFolder(Integer spaceId, Member requester, Integer folderId) { + Space space = spaceService.findById(spaceId); + if (!membershipService.isMemberJoinedSpace(requester, space)) + throw new SecurityException("슀페이슀의 ꡬ성원이 μ•„λ‹™λ‹ˆλ‹€."); + + var m = membershipService.findByMemberAndSpace(requester, space); + var auth = m.getAuthority(); + if (auth == Authority.PENDING || auth == Authority.READ_ONLY) + throw new SecurityException("폴더 μ‚­μ œ κΆŒν•œμ΄ μ—†μŠ΅λ‹ˆλ‹€."); + + Archive archive = Optional.ofNullable(space.getSharingArchive()) + .map(SharingArchive::getArchive) + .orElseThrow(() -> new NoResultException("슀페이슀의 곡유 μ•„μΉ΄μ΄λΈŒκ°€ μ—†μŠ΅λ‹ˆλ‹€.")); + + return folderService.deleteFolder(archive, folderId); + } + + @Transactional + public String updateFolderName(Integer spaceId, Member requester, Integer folderId, String newName) { + Space space = spaceService.findById(spaceId); + if (!membershipService.isMemberJoinedSpace(requester, space)) + throw new SecurityException("슀페이슀의 ꡬ성원이 μ•„λ‹™λ‹ˆλ‹€."); + + var m = membershipService.findByMemberAndSpace(requester, space); + var auth = m.getAuthority(); + if (auth == Authority.PENDING || auth == Authority.READ_ONLY) + throw new SecurityException("폴더 μˆ˜μ • κΆŒν•œμ΄ μ—†μŠ΅λ‹ˆλ‹€."); + + Archive archive = Optional.ofNullable(space.getSharingArchive()) + .map(SharingArchive::getArchive) + .orElseThrow(() -> new NoResultException("슀페이슀의 곡유 μ•„μΉ΄μ΄λΈŒκ°€ μ—†μŠ΅λ‹ˆλ‹€.")); + + return folderService.updateFolderName(archive, folderId, newName); + } + + @Transactional(readOnly = true) + public List getFolders(Integer spaceId, Member requester) { + Space space = spaceService.findById(spaceId); + if (!membershipService.isMemberInSpace(requester, space)) // 읽기: PENDING도 ν—ˆμš©ν• μ§€ μ •μ±…λŒ€λ‘œ + throw new SecurityException("슀페이슀의 ꡬ성원이 μ•„λ‹™λ‹ˆλ‹€."); + + Archive archive = Optional.ofNullable(space.getSharingArchive()) + .map(SharingArchive::getArchive) + .orElseThrow(() -> new NoResultException("슀페이슀의 곡유 μ•„μΉ΄μ΄λΈŒκ°€ μ—†μŠ΅λ‹ˆλ‹€.")); + + return folderService.getFolders(archive); + } + + @Transactional(readOnly = true) + public FolderFilesDto getFilesInFolder(Integer spaceId, Member requester, Integer folderId) { + Space space = spaceService.findById(spaceId); + if (!membershipService.isMemberInSpace(requester, space)) + throw new SecurityException("슀페이슀의 ꡬ성원이 μ•„λ‹™λ‹ˆλ‹€."); + + Archive archive = Optional.ofNullable(space.getSharingArchive()) + .map(SharingArchive::getArchive) + .orElseThrow(() -> new NoResultException("슀페이슀의 곡유 μ•„μΉ΄μ΄λΈŒκ°€ μ—†μŠ΅λ‹ˆλ‹€.")); + + return folderService.getFilesInFolder(archive, folderId); + } + + @Transactional(readOnly = true) + public Integer getDefaultFolderId(Integer spaceId, Member requester) { + Space space = spaceService.findById(spaceId); + if (!membershipService.isMemberInSpace(requester, space)) + throw new SecurityException("슀페이슀의 ꡬ성원이 μ•„λ‹™λ‹ˆλ‹€."); + + Archive archive = Optional.ofNullable(space.getSharingArchive()) + .map(SharingArchive::getArchive) + .orElseThrow(() -> new NoResultException("슀페이슀의 곡유 μ•„μΉ΄μ΄λΈŒκ°€ μ—†μŠ΅λ‹ˆλ‹€.")); + + return folderService.getDefaultFolderId(archive); + } +} diff --git a/src/test/java/org/tuna/zoopzoop/backend/domain/archive/folder/controller/FolderControllerTest.java b/src/test/java/org/tuna/zoopzoop/backend/domain/archive/folder/controller/FolderControllerTest.java index 604ce50e..59e9e9c8 100644 --- a/src/test/java/org/tuna/zoopzoop/backend/domain/archive/folder/controller/FolderControllerTest.java +++ b/src/test/java/org/tuna/zoopzoop/backend/domain/archive/folder/controller/FolderControllerTest.java @@ -13,8 +13,8 @@ import org.tuna.zoopzoop.backend.domain.archive.folder.dto.FolderResponse; import org.tuna.zoopzoop.backend.domain.archive.folder.dto.reqBodyForCreateFolder; import org.tuna.zoopzoop.backend.domain.archive.folder.entity.Folder; -import org.tuna.zoopzoop.backend.domain.archive.folder.service.FolderService; import org.tuna.zoopzoop.backend.domain.archive.folder.repository.FolderRepository; +import org.tuna.zoopzoop.backend.domain.archive.folder.service.PersonalArchiveFolderService; import org.tuna.zoopzoop.backend.domain.datasource.entity.Category; import org.tuna.zoopzoop.backend.domain.datasource.entity.DataSource; import org.tuna.zoopzoop.backend.domain.datasource.entity.Tag; @@ -32,7 +32,11 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; - +/** + * 개인 μ•„μΉ΄μ΄λΈŒ 폴더 컨트둀러 톡합 ν…ŒμŠ€νŠΈ (MockMvc) + * - μ „μ—­ μ˜ˆμ™Έ ν•Έλ“€λŸ¬λ₯Ό 톡해 RsData(JSON)둜 응닡함 + * - μ‘λ‹΅μ˜ "status" 값은 λ¬Έμžμ—΄("200","400","404",...)둜 검증 + */ @ActiveProfiles("test") @SpringBootTest @AutoConfigureMockMvc @@ -46,12 +50,12 @@ class FolderControllerTest { @Autowired private MemberService memberService; @Autowired private MemberRepository memberRepository; - @Autowired private FolderService folderService; + @Autowired private PersonalArchiveFolderService personalArchiveFolderService; @Autowired private FolderRepository folderRepository; @Autowired private DataSourceRepository dataSourceRepository; - private final String TEST_PROVIDER_KEY = "sc1111"; // WithUserDetails μ—μ„œ μ‚¬μš©λ˜λŠ” provider key ("KAKAO:sc1111") + private final String TEST_PROVIDER_KEY = "sc1111"; private Integer testMemberId; private Integer docsFolderId; @@ -65,13 +69,12 @@ void beforeAll() { .map(BaseEntity::getId) .orElseThrow(); - // GIVEN: ν…ŒμŠ€νŠΈμš© 폴더 및 μƒ˜ν”Œ 자료 μ€€λΉ„ (docs 폴더 + 2개 자료) - FolderResponse fr = folderService.createFolderForPersonal(testMemberId, "docs"); + // given + FolderResponse fr = personalArchiveFolderService.createFolder(testMemberId, "docs"); docsFolderId = fr.folderId(); Folder docsFolder = folderRepository.findById(docsFolderId).orElseThrow(); - // 자료 2건 생성 DataSource d1 = new DataSource(); d1.setFolder(docsFolder); d1.setTitle("spec.pdf"); @@ -81,7 +84,7 @@ void beforeAll() { d1.setDataCreatedDate(LocalDate.now()); d1.setActive(true); d1.setTags(List.of(new Tag("tag1"), new Tag("tag2"))); - d1.setCategory(Category.IT); // enum νƒ€μž… 반영 + d1.setCategory(Category.IT); dataSourceRepository.save(d1); DataSource d2 = new DataSource(); @@ -107,15 +110,13 @@ void afterAll() { } catch (Exception ignored) {} } - // CreateFile + // ---------- Create ---------- @Test @DisplayName("개인 μ•„μΉ΄μ΄λΈŒ 폴더 생성 - 성곡 μ‹œ 200κ³Ό 응닡 DTO λ°˜ν™˜") @WithUserDetails("KAKAO:sc1111") void createFolder_ok() throws Exception { - // Given var req = new reqBodyForCreateFolder("λ³΄κ³ μ„œ"); - // When & Then mockMvc.perform(post("/api/v1/archive/folder") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(req))) @@ -130,40 +131,37 @@ void createFolder_ok() throws Exception { @DisplayName("개인 μ•„μΉ΄μ΄λΈŒ 폴더 생성 - 폴더 이름 λˆ„λ½ μ‹œ 400") @WithUserDetails("KAKAO:sc1111") void createFolder_missingName() throws Exception { - // Given var req = new reqBodyForCreateFolder(null); - // When & Then mockMvc.perform(post("/api/v1/archive/folder") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(req))) - .andExpect(status().isBadRequest()); + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.status").value("400")); } - - // DeleteFile + // ---------- Delete ---------- @Test @DisplayName("개인 μ•„μΉ΄μ΄λΈŒ 폴더 μ‚­μ œ - 성곡 μ‹œ 200κ³Ό μ‚­μ œ λ©”μ‹œμ§€ λ°˜ν™˜") @WithUserDetails("KAKAO:sc1111") void deleteFolder_ok() throws Exception { - // Given: μƒˆ 폴더 생성 ν›„ μ‚­μ œ μ€€λΉ„ - FolderResponse fr = folderService.createFolderForPersonal(testMemberId, "todelete"); + FolderResponse fr = personalArchiveFolderService.createFolder(testMemberId, "todelete"); Integer idToDelete = fr.folderId(); - // When & Then mockMvc.perform(delete("/api/v1/archive/folder/{folderId}", idToDelete)) .andExpect(status().isOk()) - .andExpect(jsonPath("$.status").value(200)) - .andExpect(jsonPath("$.msg").value("todelete 폴더가 μ‚­μ œλμŠ΅λ‹ˆλ‹€.")); + .andExpect(jsonPath("$.status").value("200")) + .andExpect(jsonPath("$.msg").value("todelete 폴더가 μ‚­μ œλμŠ΅λ‹ˆλ‹€.")) + .andExpect(jsonPath("$.data").doesNotExist()); } @Test - @DisplayName("개인 μ•„μΉ΄μ΄λΈŒ 폴더 μ‚­μ œ μ‹€νŒ¨- κΈ°λ³Έ 폴더면 400") + @DisplayName("개인 μ•„μΉ΄μ΄λΈŒ 폴더 μ‚­μ œ μ‹€νŒ¨ - κΈ°λ³Έ 폴더면 400") @WithUserDetails("KAKAO:sc1111") void deleteDefaultFolder_badRequest() throws Exception { mockMvc.perform(delete("/api/v1/archive/folder/{folderId}", 0)) .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.status").value(409)) + .andExpect(jsonPath("$.status").value("400")) .andExpect(jsonPath("$.msg").value("default ν΄λ”λŠ” μ‚­μ œν•  수 μ—†μŠ΅λ‹ˆλ‹€.")) .andExpect(jsonPath("$.data").value(nullValue())); } @@ -172,32 +170,28 @@ void deleteDefaultFolder_badRequest() throws Exception { @DisplayName("개인 μ•„μΉ΄μ΄λΈŒ 폴더 μ‚­μ œ - μ‘΄μž¬ν•˜μ§€ μ•ŠμœΌλ©΄ 404") @WithUserDetails("KAKAO:sc1111") void deleteFolder_notFound() throws Exception { - // When & Then mockMvc.perform(delete("/api/v1/archive/folder/{folderId}", 999999)) .andExpect(status().isNotFound()) .andExpect(jsonPath("$.status").value("404")) .andExpect(jsonPath("$.msg").value("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ν΄λ”μž…λ‹ˆλ‹€.")); } - - // UpdateFile + // ---------- Update ---------- @Test @DisplayName("개인 μ•„μΉ΄μ΄λΈŒ 폴더 이름 λ³€κ²½ - 성곡 μ‹œ 200κ³Ό λ³€κ²½λœ 이름 λ°˜ν™˜") @WithUserDetails("KAKAO:sc1111") void updateFolder_ok() throws Exception { - // Given: rename λŒ€μƒ 폴더 생성 - FolderResponse fr = folderService.createFolderForPersonal(testMemberId, "toRename"); + FolderResponse fr = personalArchiveFolderService.createFolder(testMemberId, "toRename"); Integer id = fr.folderId(); var body = new java.util.HashMap(); body.put("folderName","회의둝"); - // When & Then mockMvc.perform(patch("/api/v1/archive/folder/{folderId}", id) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(body))) .andExpect(status().isOk()) - .andExpect(jsonPath("$.status").value(200)) + .andExpect(jsonPath("$.status").value("200")) .andExpect(jsonPath("$.msg").value("폴더 이름이 회의둝 으둜 λ³€κ²½λμŠ΅λ‹ˆλ‹€.")) .andExpect(jsonPath("$.data.folderName").value("회의둝")); } @@ -213,7 +207,7 @@ void updateDefaultFolder_badRequest() throws Exception { .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(body))) .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.status").value(400)) + .andExpect(jsonPath("$.status").value("400")) .andExpect(jsonPath("$.msg").value("default ν΄λ”λŠ” 이름을 λ³€κ²½ν•  수 μ—†μŠ΅λ‹ˆλ‹€.")) .andExpect(jsonPath("$.data").value(nullValue())); } @@ -222,11 +216,9 @@ void updateDefaultFolder_badRequest() throws Exception { @DisplayName("개인 μ•„μΉ΄μ΄λΈŒ 폴더 이름 λ³€κ²½ - μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” 폴더면 404") @WithUserDetails("KAKAO:sc1111") void updateFolder_notFound() throws Exception { - // Given var body = new java.util.HashMap(); body.put("folderName","회의둝"); - // When & Then mockMvc.perform(patch("/api/v1/archive/folder/{folderId}", 999999) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(body))) @@ -235,33 +227,27 @@ void updateFolder_notFound() throws Exception { .andExpect(jsonPath("$.msg").value("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ν΄λ”μž…λ‹ˆλ‹€.")); } - // ReadFolder - // Read: λ‚΄ 폴더 λͺ©λ‘ + // ---------- Read ---------- @Test @DisplayName("개인 μ•„μΉ΄μ΄λΈŒ 폴더 λͺ©λ‘ 쑰회 - 성곡") @WithUserDetails("KAKAO:sc1111") void getFolders_success() throws Exception { - // When & Then mockMvc.perform(get("/api/v1/archive/folder") .accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) - .andExpect(jsonPath("$.status").value(200)) + .andExpect(jsonPath("$.status").value("200")) .andExpect(jsonPath("$.msg").value("개인 μ•„μΉ΄μ΄λΈŒμ˜ 폴더 λͺ©λ‘μ„ λΆˆλŸ¬μ™”μŠ΅λ‹ˆλ‹€.")) - .andExpect(jsonPath("$.data.folders").isArray()); + .andExpect(jsonPath("$.data").isArray()); } - // Read: 폴더 λ‚΄ 파일 λͺ©λ‘ @Test - @DisplayName("폴더 λ‚΄ 파일 λͺ©λ‘ 쑰회 - 성곡") + @DisplayName("개인 μ•„μΉ΄μ΄λΈŒ 폴더 λ‚΄ 파일 λͺ©λ‘ 쑰회 - 성곡") @WithUserDetails("KAKAO:sc1111") void getFilesInFolder_success() throws Exception { - // Given : @BeforeAll: docsFolderId 및 μƒ˜ν”Œ 파일 쀀비됨 - - // When & Then mockMvc.perform(get("/api/v1/archive/folder/{folderId}/files", docsFolderId) .accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) - .andExpect(jsonPath("$.status").value(200)) + .andExpect(jsonPath("$.status").value("200")) .andExpect(jsonPath("$.msg").value("ν•΄λ‹Ή ν΄λ”μ˜ 파일 λͺ©λ‘μ„ λΆˆλŸ¬μ™”μŠ΅λ‹ˆλ‹€.")) .andExpect(jsonPath("$.data.files").isArray()) .andExpect(jsonPath("$.data.files.length()").value(greaterThanOrEqualTo(2))) @@ -273,25 +259,24 @@ void getFilesInFolder_success() throws Exception { } @Test - @DisplayName("폴더 파일 λͺ©λ‘ 쑰회 - default 폴더 성곡") + @DisplayName("개인 μ•„μΉ΄μ΄λΈŒ κΈ°λ³Έ 폴더 λ‚΄ 파일 λͺ©λ‘ 쑰회 - 성곡") @WithUserDetails("KAKAO:sc1111") void getFilesInDefaultFolder_success() throws Exception { mockMvc.perform(get("/api/v1/archive/folder/{folderId}/files", 0) .accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) - .andExpect(jsonPath("$.status").value(200)) - .andExpect(jsonPath("$.msg").value("κΈ°λ³Έ ν΄λ”μ˜ 파일 λͺ©λ‘μ„ λΆˆλŸ¬μ™”μŠ΅λ‹ˆλ‹€.")) - .andExpect(jsonPath("$.data.folder.folderId").isNumber()) - .andExpect(jsonPath("$.data.folder.folderName").isString()) + .andExpect(jsonPath("$.status").value("200")) + + .andExpect(jsonPath("$.msg").value("ν•΄λ‹Ή ν΄λ”μ˜ 파일 λͺ©λ‘μ„ λΆˆλŸ¬μ™”μŠ΅λ‹ˆλ‹€.")) + .andExpect(jsonPath("$.data.folderId").isNumber()) + .andExpect(jsonPath("$.data.folderName").isString()) .andExpect(jsonPath("$.data.files").isArray()); } - @Test - @DisplayName("폴더 λ‚΄ 파일 λͺ©λ‘ 쑰회 - 폴더가 μ—†μœΌλ©΄ 404") + @DisplayName("개인 μ•„μΉ΄μ΄λΈŒ 폴더 λ‚΄ 파일 λͺ©λ‘ 쑰회 - 폴더가 μ—†μœΌλ©΄ 404") @WithUserDetails("KAKAO:sc1111") void getFilesInFolder_notFound() throws Exception { - // When & Then mockMvc.perform(get("/api/v1/archive/folder/{folderId}/files", 999999) .accept(MediaType.APPLICATION_JSON)) .andExpect(status().isNotFound()) diff --git a/src/test/java/org/tuna/zoopzoop/backend/domain/archive/folder/service/FolderServiceTest.java b/src/test/java/org/tuna/zoopzoop/backend/domain/archive/folder/service/FolderServiceTest.java index fb0f4457..4504d04f 100644 --- a/src/test/java/org/tuna/zoopzoop/backend/domain/archive/folder/service/FolderServiceTest.java +++ b/src/test/java/org/tuna/zoopzoop/backend/domain/archive/folder/service/FolderServiceTest.java @@ -8,24 +8,18 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.test.context.ActiveProfiles; import org.springframework.test.util.ReflectionTestUtils; -import org.springframework.transaction.annotation.Transactional; import org.tuna.zoopzoop.backend.domain.archive.archive.entity.Archive; -import org.tuna.zoopzoop.backend.domain.archive.archive.entity.PersonalArchive; -import org.tuna.zoopzoop.backend.domain.archive.archive.repository.PersonalArchiveRepository; import org.tuna.zoopzoop.backend.domain.archive.folder.dto.FolderResponse; import org.tuna.zoopzoop.backend.domain.archive.folder.entity.Folder; import org.tuna.zoopzoop.backend.domain.archive.folder.repository.FolderRepository; -import org.tuna.zoopzoop.backend.domain.datasource.entity.Category; -import org.tuna.zoopzoop.backend.domain.datasource.dto.FolderFilesDto; import org.tuna.zoopzoop.backend.domain.datasource.dto.FileSummary; +import org.tuna.zoopzoop.backend.domain.datasource.dto.FolderFilesDto; import org.tuna.zoopzoop.backend.domain.datasource.entity.DataSource; import org.tuna.zoopzoop.backend.domain.datasource.entity.Tag; import org.tuna.zoopzoop.backend.domain.datasource.repository.DataSourceRepository; -import org.tuna.zoopzoop.backend.domain.member.entity.Member; -import org.tuna.zoopzoop.backend.domain.member.repository.MemberRepository; +import java.time.LocalDate; import java.util.List; import java.util.Optional; @@ -34,310 +28,198 @@ import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; -/** - * FolderService λ‹¨μœ„ ν…ŒμŠ€νŠΈ - * - memberRepository μŠ€ν…μ€ ν•„μš”ν•œ ν…ŒμŠ€νŠΈμ—λ§Œ μ„ μ–Έ - */ @ExtendWith(MockitoExtension.class) -@Transactional -@ActiveProfiles("test") class FolderServiceTest { - @Mock private MemberRepository memberRepository; - @Mock private PersonalArchiveRepository personalArchiveRepository; @Mock private FolderRepository folderRepository; @Mock private DataSourceRepository dataSourceRepository; @InjectMocks private FolderService folderService; - private Member member; private Archive archive; - private PersonalArchive personalArchive; @BeforeEach void setUp() { - // 곡톡 ν…ŒμŠ€νŠΈ 데이터 μ€€λΉ„ (μŠ€ν…μ€ 각 ν…ŒμŠ€νŠΈμ—μ„œ μ„ μ–Έ) - this.member = new Member(); - ReflectionTestUtils.setField(member, "id", 1); - this.archive = new Archive(); ReflectionTestUtils.setField(archive, "id", 10); - - this.personalArchive = new PersonalArchive(); - ReflectionTestUtils.setField(personalArchive, "id", 100); - personalArchive.setMember(member); - personalArchive.setArchive(archive); } // ---------- Create ---------- @Test - @DisplayName("폴더 생성 성곡(쀑볡 μ—†μŒ)") - void createFolder_success() { - // GIVEN - when(memberRepository.findById(1)).thenReturn(Optional.of(member)); // <- λ°˜λ“œμ‹œ ν•„μš” - when(personalArchiveRepository.findByMemberId(1)).thenReturn(Optional.of(personalArchive)); + @DisplayName("폴더 이름 쀑볡 μ—†μŒ β†’ κ·ΈλŒ€λ‘œ 생성") + void generateUniqueName_noConflict() { when(folderRepository.findNamesForConflictCheck(eq(archive.getId()), anyString(), anyString())) .thenReturn(List.of()); - Folder saved = new Folder(); - saved.setName("λ³΄κ³ μ„œ"); - saved.setArchive(archive); - ReflectionTestUtils.setField(saved, "id", 999); + Folder folder = new Folder(); + folder.setArchive(archive); + folder.setName("λ³΄κ³ μ„œ"); + ReflectionTestUtils.setField(folder, "id", 1); - when(folderRepository.save(any(Folder.class))).thenReturn(saved); + when(folderRepository.save(any(Folder.class))).thenReturn(folder); - // WHEN - FolderResponse result = folderService.createFolderForPersonal(1, "λ³΄κ³ μ„œ"); + FolderResponse rs = folderService.createFolder(archive, "λ³΄κ³ μ„œ"); - // THEN - assertThat(result.folderId()).isEqualTo(999); - assertThat(result.folderName()).isEqualTo("λ³΄κ³ μ„œ"); + assertThat(rs.folderName()).isEqualTo("λ³΄κ³ μ„œ"); } @Test - @DisplayName("폴더 이름 쀑볡 μ‹œ '(1)' λΆ™μ—¬ 생성") - void createFolder_withConflict() { - // given - when(memberRepository.findById(1)).thenReturn(Optional.of(member)); // <- λ°˜λ“œμ‹œ ν•„μš” - when(personalArchiveRepository.findByMemberId(1)).thenReturn(Optional.of(personalArchive)); + @DisplayName("폴더 이름 쀑볡 μ‹œ (1) λΆ™μ—¬ 생성") + void generateUniqueName_withConflict() { when(folderRepository.findNamesForConflictCheck(eq(archive.getId()), eq("λ³΄κ³ μ„œ"), anyString())) .thenReturn(List.of("λ³΄κ³ μ„œ")); - Folder saved = new Folder(); - saved.setName("λ³΄κ³ μ„œ (1)"); - saved.setArchive(archive); - ReflectionTestUtils.setField(saved, "id", 1000); - - when(folderRepository.save(any(Folder.class))).thenReturn(saved); - - // when - FolderResponse result = folderService.createFolderForPersonal(1, "λ³΄κ³ μ„œ"); + Folder folder = new Folder(); + folder.setArchive(archive); + folder.setName("λ³΄κ³ μ„œ (1)"); + ReflectionTestUtils.setField(folder, "id", 2); - // then - assertThat(result.folderName()).isEqualTo("λ³΄κ³ μ„œ (1)"); - assertThat(result.folderId()).isEqualTo(1000); - } + when(folderRepository.save(any(Folder.class))).thenReturn(folder); - @Test - @DisplayName("멀버가 μ—†μœΌλ©΄ μ˜ˆμ™Έ λ°œμƒ") - void createFolder_memberNotFound() { - // given - when(memberRepository.findById(2)).thenReturn(Optional.empty()); + FolderResponse rs = folderService.createFolder(archive, "λ³΄κ³ μ„œ"); - // when & then - assertThrows(NoResultException.class, - () -> folderService.createFolderForPersonal(2, "λ³΄κ³ μ„œ")); + assertThat(rs.folderName()).isEqualTo("λ³΄κ³ μ„œ (1)"); } // ---------- Delete ---------- @Test - @DisplayName("폴더 μ‚­μ œ 성곡 - μžλ£ŒλŠ” default ν΄λ”λ‘œ 이관 + soft delete ν›„ 폴더 μ˜κ΅¬μ‚­μ œ") - void deleteFolder_success() { - // given - // μ‚­μ œ λŒ€μƒ 폴더 - Folder folder = new Folder(); - folder.setName("λ³΄κ³ μ„œ"); - folder.setArchive(archive); - ReflectionTestUtils.setField(folder, "id", 500); - - // κΈ°λ³Έ 폴더 μŠ€ν… (νšŒμ›μ˜ default 폴더) - Folder defaultFolder = new Folder("default"); // μƒμ„±μžμ—μ„œ isDefault=true 섀정이라면 κ·ΈλŒ€λ‘œ μ‚¬μš© + @DisplayName("폴더 μ‚­μ œ μ‹œ μžλ£ŒλŠ” default ν΄λ”λ‘œ 이관 + soft delete") + void deleteFolder_softDeleteAndMove() { + Folder target = new Folder(); + target.setArchive(archive); + target.setName("docs"); + ReflectionTestUtils.setField(target, "id", 100); + + Folder defaultFolder = new Folder(); defaultFolder.setArchive(archive); - ReflectionTestUtils.setField(defaultFolder, "id", 42); + defaultFolder.setName("default"); + defaultFolder.setDefault(true); + ReflectionTestUtils.setField(defaultFolder, "id", 200); - // 폴더 λ‚΄ μžλ£Œλ“€ (이관 + soft deleteκ°€ 적용될 λŒ€μƒ) - DataSource d1 = new DataSource(); ReflectionTestUtils.setField(d1, "id", 1); d1.setFolder(folder); d1.setActive(true); - DataSource d2 = new DataSource(); ReflectionTestUtils.setField(d2, "id", 2); d2.setFolder(folder); d2.setActive(true); + DataSource d1 = new DataSource(); ReflectionTestUtils.setField(d1, "id", 1); d1.setFolder(target); d1.setActive(true); + DataSource d2 = new DataSource(); ReflectionTestUtils.setField(d2, "id", 2); d2.setFolder(target); d2.setActive(true); + when(folderRepository.findByIdAndArchiveId(100, 10)) + .thenReturn(Optional.of(target)); + when(folderRepository.findByArchiveIdAndIsDefaultTrue(10)) + .thenReturn(Optional.of(defaultFolder)); + when(dataSourceRepository.findAllByFolderId(100)) + .thenReturn(List.of(d1, d2)); - when(folderRepository.findByIdAndMemberId(500, 1)).thenReturn(Optional.of(folder)); - when(folderRepository.findDefaultByMemberId(1)).thenReturn(Optional.of(defaultFolder)); + String deleted = folderService.deleteFolder(archive, 100); - when(dataSourceRepository.findAllByFolderId(500)).thenReturn(List.of(d1, d2)); - - - // when - String deletedName = folderService.deleteFolder(1, 500); - - // then - assertThat(deletedName).isEqualTo("λ³΄κ³ μ„œ"); - - // μžλ£Œλ“€μ΄ default ν΄λ”λ‘œ 이관 + soft delete λ˜μ—ˆλŠ”μ§€ 확인 - assertThat(d1.getFolder().getId()).isEqualTo(defaultFolder.getId()); - assertThat(d2.getFolder().getId()).isEqualTo(defaultFolder.getId()); + assertThat(deleted).isEqualTo("docs"); assertThat(d1.isActive()).isFalse(); - assertThat(d2.isActive()).isFalse(); assertThat(d1.getDeletedAt()).isNotNull(); - assertThat(d2.getDeletedAt()).isNotNull(); - - // λ§ˆμ§€λ§‰μ— 폴더 μ‚­μ œ 호좜 - verify(folderRepository, times(1)).delete(folder); + assertThat(d1.getFolder()).isEqualTo(defaultFolder); + verify(folderRepository, times(1)).delete(target); } - - @Test @DisplayName("폴더 μ‚­μ œ μ‹€νŒ¨ - μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” 폴더") void deleteFolder_notFound() { - // given - when(folderRepository.findByIdAndMemberId(999, 1)).thenReturn(Optional.empty()); - - // when & then - assertThrows(NoResultException.class, () -> folderService.deleteFolder(1, 999)); - verify(folderRepository, never()).delete(any(Folder.class)); - } - - @Test - @DisplayName("default ν΄λ”λŠ” μ‚­μ œν•  수 μ—†λ‹€") - void deleteFolder_default_forbidden() { - // given - Folder defaultFolder = new Folder("default"); // isDefault=true - ReflectionTestUtils.setField(defaultFolder, "id", 42); - - when(folderRepository.findByIdAndMemberId(42, 1)).thenReturn(Optional.of(defaultFolder)); + when(folderRepository.findByIdAndArchiveId(999, archive.getId())) + .thenReturn(Optional.empty()); - // when & then - assertThrows(IllegalArgumentException.class, () -> folderService.deleteFolder(1, 42)); - verify(folderRepository, never()).delete(any()); + assertThrows(NoResultException.class, () -> folderService.deleteFolder(archive, 999)); } // ---------- Update ---------- @Test @DisplayName("폴더 이름 λ³€κ²½ 성곡") void updateFolderName_success() { - // given Folder folder = new Folder(); - folder.setName("기쑴이름"); folder.setArchive(archive); - ReflectionTestUtils.setField(folder, "id", 700); + folder.setName("κΈ°μ‘΄"); + ReflectionTestUtils.setField(folder, "id", 300); - when(folderRepository.findByIdAndMemberId(700, 1)).thenReturn(Optional.of(folder)); - when(folderRepository.findNamesForConflictCheck(archive.getId(), "μƒˆμ΄λ¦„", folder.getName())) + when(folderRepository.findByIdAndArchiveId(300, archive.getId())) + .thenReturn(Optional.of(folder)); + when(folderRepository.existsNameInArchiveExceptSelf(archive.getId(), "μƒˆμ΄λ¦„", folder.getId())) .thenReturn(List.of()); - when(folderRepository.save(any(Folder.class))).thenAnswer(invocation -> invocation.getArgument(0)); + when(folderRepository.save(any(Folder.class))).thenAnswer(inv -> inv.getArgument(0)); - // when - String updated = folderService.updateFolderName(1, 700, "μƒˆμ΄λ¦„"); + String updated = folderService.updateFolderName(archive, 300, "μƒˆμ΄λ¦„"); - // then assertThat(updated).isEqualTo("μƒˆμ΄λ¦„"); assertThat(folder.getName()).isEqualTo("μƒˆμ΄λ¦„"); - verify(folderRepository, times(1)).save(folder); - } - - @Test - @DisplayName("폴더 이름 λ³€κ²½ μ‹€νŒ¨ - μ‘΄μž¬ν•˜μ§€ μ•ŠμŒ") - void updateFolderName_notFound() { - // given - when(folderRepository.findByIdAndMemberId(701, 1)).thenReturn(Optional.empty()); - - // when & then - assertThrows(NoResultException.class, () -> folderService.updateFolderName(1, 701, "μ•„λ¬΄κ±°λ‚˜")); - verify(folderRepository, never()).save(any(Folder.class)); } @Test @DisplayName("폴더 이름 λ³€κ²½ μ‹€νŒ¨ - 쀑볡 이름 쑴재") void updateFolderName_conflict() { - // given Folder folder = new Folder(); - folder.setName("기쑴이름"); folder.setArchive(archive); - ReflectionTestUtils.setField(folder, "id", 700); + folder.setName("κΈ°μ‘΄"); + ReflectionTestUtils.setField(folder, "id", 301); - when(folderRepository.findByIdAndMemberId(700, 1)).thenReturn(Optional.of(folder)); - when(folderRepository.findNamesForConflictCheck(archive.getId(), "λ³΄κ³ μ„œ", "기쑴이름")) + when(folderRepository.findByIdAndArchiveId(301, archive.getId())) + .thenReturn(Optional.of(folder)); + when(folderRepository.existsNameInArchiveExceptSelf(archive.getId(), "λ³΄κ³ μ„œ", folder.getId())) .thenReturn(List.of("λ³΄κ³ μ„œ")); - // when & then assertThrows(IllegalArgumentException.class, - () -> folderService.updateFolderName(1, 700, "λ³΄κ³ μ„œ")); + () -> folderService.updateFolderName(archive, 301, "λ³΄κ³ μ„œ")); + } - verify(folderRepository, never()).save(any(Folder.class)); + @Test + @DisplayName("폴더 이름 λ³€κ²½ μ‹€νŒ¨ - 폴더 μ—†μŒ") + void updateFolderName_notFound() { + when(folderRepository.findByIdAndArchiveId(302, archive.getId())) + .thenReturn(Optional.empty()); + + assertThrows(NoResultException.class, + () -> folderService.updateFolderName(archive, 302, "μƒˆμ΄λ¦„")); } - // Read: Personal Archive λ‚΄ 폴더 λͺ©λ‘ + // ---------- Read ---------- @Test - @DisplayName("개인 μ•„μΉ΄μ΄λΈŒ 폴더 λͺ©λ‘ 쑰회 - 성곡") - void getFoldersForPersonal_success() { - // given - Folder f1 = new Folder(); f1.setName("default"); f1.setArchive(archive); ReflectionTestUtils.setField(f1, "id", 1); - Folder f2 = new Folder(); f2.setName("docs"); f2.setArchive(archive); ReflectionTestUtils.setField(f2, "id", 2); + @DisplayName("Archive λ‹¨μœ„ 폴더 λͺ©λ‘ 쑰회 성곡") + void getFolders_success() { + Folder f1 = new Folder(); f1.setArchive(archive); f1.setName("default"); ReflectionTestUtils.setField(f1, "id", 1); + Folder f2 = new Folder(); f2.setArchive(archive); f2.setName("docs"); ReflectionTestUtils.setField(f2, "id", 2); - when(personalArchiveRepository.findByMemberId(1)).thenReturn(Optional.of(personalArchive)); when(folderRepository.findByArchive(archive)).thenReturn(List.of(f1, f2)); - // when - List rs = folderService.getFoldersForPersonal(1); + List rs = folderService.getFolders(archive); - // then assertThat(rs).hasSize(2); - assertThat(rs.get(0).folderId()).isEqualTo(1); assertThat(rs.get(0).folderName()).isEqualTo("default"); assertThat(rs.get(1).folderName()).isEqualTo("docs"); - verify(folderRepository, times(1)).findByArchive(archive); } - // Read: 폴더 λ‚΄ 파일 λͺ©λ‘ @Test - @DisplayName("폴더 λ‚΄ 파일 λͺ©λ‘ 쑰회") - void getFilesInFolderForPersonal_success() { - // given - Integer folderId = 2; - + @DisplayName("Archive λ‹¨μœ„ 폴더 λ‚΄ 파일 λͺ©λ‘ 쑰회 성곡") + void getFilesInFolder_success() { Folder folder = new Folder(); - folder.setName("docs"); folder.setArchive(archive); - ReflectionTestUtils.setField(folder, "id", folderId); - - when(folderRepository.findByIdAndMemberId(folderId, 1)).thenReturn(Optional.of(folder)); - - DataSource d1 = new DataSource(); - ReflectionTestUtils.setField(d1, "id", 10); - d1.setTitle("spec.pdf"); - d1.setFolder(folder); - d1.setSummary("μš”μ•½ A"); - d1.setSourceUrl("http://src/a"); - d1.setImageUrl("http://img/a"); - d1.setTags(List.of(new Tag("tag1"), new Tag("tag2"))); - d1.setCategory(Category.IT); - - DataSource d2 = new DataSource(); - ReflectionTestUtils.setField(d2, "id", 11); - d2.setTitle("notes.txt"); - d2.setFolder(folder); - d2.setSummary("μš”μ•½ B"); - d2.setSourceUrl("http://src/b"); - d2.setImageUrl("http://img/b"); - d2.setTags(List.of()); - d2.setCategory(Category.SCIENCE); - - when(dataSourceRepository.findAllByFolder(folder)).thenReturn(List.of(d1, d2)); - - // when - FolderFilesDto dto = folderService.getFilesInFolderForPersonal(1, folderId); - - // then - assertThat(dto.files()).hasSize(2); - FileSummary f0 = dto.files().getFirst(); - assertThat(f0.dataSourceId()).isEqualTo(10); - assertThat(f0.title()).isEqualTo("spec.pdf"); - assertThat(f0.summary()).isEqualTo("μš”μ•½ A"); - assertThat(f0.sourceUrl()).isEqualTo("http://src/a"); - assertThat(f0.imageUrl()).isEqualTo("http://img/a"); - assertThat(f0.tags()).containsExactly("tag1", "tag2"); + folder.setName("docs"); + ReflectionTestUtils.setField(folder, "id", 400); + + DataSource d1 = new DataSource(); ReflectionTestUtils.setField(d1, "id", 1); + d1.setTitle("spec.pdf"); d1.setSummary("μš”μ•½"); d1.setFolder(folder); + d1.setSourceUrl("http://src/a"); d1.setImageUrl("http://img/a"); + d1.setDataCreatedDate(LocalDate.now()); + d1.setTags(List.of(new Tag("tag1"))); + + when(folderRepository.findByIdAndArchiveId(eq(400), eq(archive.getId()))) + .thenReturn(Optional.of(folder)); + when(dataSourceRepository.findAllByFolder(folder)).thenReturn(List.of(d1)); + + FolderFilesDto dto = folderService.getFilesInFolder(archive, 400); + + assertThat(dto.files()).hasSize(1); + FileSummary f = dto.files().getFirst(); + assertThat(f.title()).isEqualTo("spec.pdf"); } @Test - @DisplayName("폴더 λ‚΄ 파일 λͺ©λ‘ 쑰회 - 폴더가 μ—†μœΌλ©΄ μ˜ˆμ™Έ λ°œμƒ") - void getFilesInFolderForPersonal_notFound() { - // given - Integer folderId = 999; - when(folderRepository.findByIdAndMemberId(folderId, 1)).thenReturn(Optional.empty()); + @DisplayName("Archive λ‹¨μœ„ 폴더 λ‚΄ 파일 λͺ©λ‘ 쑰회 μ‹€νŒ¨ - 폴더 μ—†μŒ") + void getFilesInFolder_notFound() { + when(folderRepository.findByIdAndArchiveId(eq(999), eq(archive.getId()))) + .thenReturn(Optional.empty()); - // when & then assertThrows(NoResultException.class, - () -> folderService.getFilesInFolderForPersonal(1, folderId)); - verify(dataSourceRepository, never()).findAllByFolder(any()); + () -> folderService.getFilesInFolder(archive, 999)); } } diff --git a/src/test/java/org/tuna/zoopzoop/backend/domain/archive/folder/service/PersonalArchiveFolderServiceTest.java b/src/test/java/org/tuna/zoopzoop/backend/domain/archive/folder/service/PersonalArchiveFolderServiceTest.java new file mode 100644 index 00000000..61aa2a10 --- /dev/null +++ b/src/test/java/org/tuna/zoopzoop/backend/domain/archive/folder/service/PersonalArchiveFolderServiceTest.java @@ -0,0 +1,289 @@ +package org.tuna.zoopzoop.backend.domain.archive.folder.service; + +import jakarta.persistence.NoResultException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; +import org.tuna.zoopzoop.backend.domain.archive.archive.entity.Archive; +import org.tuna.zoopzoop.backend.domain.archive.archive.entity.PersonalArchive; +import org.tuna.zoopzoop.backend.domain.archive.archive.repository.PersonalArchiveRepository; +import org.tuna.zoopzoop.backend.domain.archive.folder.dto.FolderResponse; +import org.tuna.zoopzoop.backend.domain.archive.folder.entity.Folder; +import org.tuna.zoopzoop.backend.domain.archive.folder.repository.FolderRepository; +import org.tuna.zoopzoop.backend.domain.datasource.dto.FolderFilesDto; + +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class PersonalArchiveFolderServiceTest { + + @Mock private PersonalArchiveRepository personalArchiveRepository; + @Mock private FolderRepository folderRepository; + @Mock private FolderService folderService; + + @InjectMocks private PersonalArchiveFolderService personalService; + + private Archive mockArchive() { + Archive a = new Archive(); + ReflectionTestUtils.setField(a, "id", 10); + return a; + } + + private PersonalArchive mockPersonalArchive(Archive a) { + PersonalArchive pa = new PersonalArchive(); + ReflectionTestUtils.setField(pa, "id", 100); + pa.setArchive(a); + return pa; + } + + private Folder mockFolder(Archive a, Integer id, String name) { + Folder f = new Folder(); + f.setArchive(a); + f.setName(name); + ReflectionTestUtils.setField(f, "id", id); + return f; + } + + // ---------------------- Create ---------------------- + @Test + @DisplayName("개인 μ•„μΉ΄μ΄λΈŒ 폴더 생성 성곡") + void createFolder_success() { + // given + Integer memberId = 1; + Archive archive = mockArchive(); + PersonalArchive pa = mockPersonalArchive(archive); + + when(personalArchiveRepository.findByMemberId(memberId)).thenReturn(Optional.of(pa)); + when(folderService.createFolder(eq(archive), eq("λ³΄κ³ μ„œ"))) + .thenReturn(new FolderResponse("λ³΄κ³ μ„œ", 999)); + + // when + FolderResponse rs = personalService.createFolder(memberId, "λ³΄κ³ μ„œ"); + + // then + assertThat(rs.folderName()).isEqualTo("λ³΄κ³ μ„œ"); + assertThat(rs.folderId()).isEqualTo(999); + verify(folderService).createFolder(eq(archive), eq("λ³΄κ³ μ„œ")); + } + + @Test + @DisplayName("개인 μ•„μΉ΄μ΄λΈŒ 폴더 생성 μ‹€νŒ¨ - 개인 μ•„μΉ΄μ΄λΈŒ μ—†μŒ") + void createFolder_archiveNotFound() { + // given + Integer memberId = 1; + when(personalArchiveRepository.findByMemberId(memberId)).thenReturn(Optional.empty()); + + // when & then + assertThrows(NoResultException.class, () -> personalService.createFolder(memberId, "λ³΄κ³ μ„œ")); + verify(folderService, never()).createFolder(any(), anyString()); + } + + // ---------------------- Delete ---------------------- + @Test + @DisplayName("개인 μ•„μΉ΄μ΄λΈŒ 폴더 μ‚­μ œ 성곡") + void deleteFolder_success() { + // given + Integer memberId = 1; + Integer folderId = 200; + Archive archive = mockArchive(); + Folder folder = mockFolder(archive, folderId, "todelete"); + + when(folderRepository.findByIdAndMemberId(folderId, memberId)).thenReturn(Optional.of(folder)); + when(folderService.deleteFolder(archive, folderId)).thenReturn("todelete"); + + // when + String deleted = personalService.deleteFolder(memberId, folderId); + + // then + assertThat(deleted).isEqualTo("todelete"); + verify(folderService).deleteFolder(eq(archive), eq(folderId)); + } + + @Test + @DisplayName("개인 μ•„μΉ΄μ΄λΈŒ 폴더 μ‚­μ œ μ‹€νŒ¨ - μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” 폴더") + void deleteFolder_notFound() { + // given + Integer memberId = 1; + Integer folderId = 999; + when(folderRepository.findByIdAndMemberId(folderId, memberId)).thenReturn(Optional.empty()); + + // when & then + assertThrows(NoResultException.class, () -> personalService.deleteFolder(memberId, folderId)); + verify(folderService, never()).deleteFolder(any(), anyInt()); + } + + @Test + @DisplayName("개인 μ•„μΉ΄μ΄λΈŒ 폴더 μ‚­μ œ μ‹€νŒ¨ - default κΈˆμ§€(곡톡 μ„œλΉ„μŠ€ μ˜ˆμ™Έ μ „νŒŒ)") + void deleteFolder_defaultForbidden() { + // given + Integer memberId = 1; + Integer folderId = 123; + Archive archive = mockArchive(); + Folder folder = mockFolder(archive, folderId, "default?"); + when(folderRepository.findByIdAndMemberId(folderId, memberId)).thenReturn(Optional.of(folder)); + when(folderService.deleteFolder(archive, folderId)) + .thenThrow(new IllegalArgumentException("default ν΄λ”λŠ” μ‚­μ œν•  수 μ—†μŠ΅λ‹ˆλ‹€.")); + + // when & then + assertThrows(IllegalArgumentException.class, () -> personalService.deleteFolder(memberId, folderId)); + verify(folderService).deleteFolder(eq(archive), eq(folderId)); + } + + // ---------------------- Update ---------------------- + @Test + @DisplayName("개인 μ•„μΉ΄μ΄λΈŒ 폴더 이름 λ³€κ²½ 성곡") + void updateFolderName_success() { + // given + Integer memberId = 1; + Integer folderId = 300; + Archive archive = mockArchive(); + Folder folder = mockFolder(archive, folderId, "old"); + when(folderRepository.findByIdAndMemberId(folderId, memberId)).thenReturn(Optional.of(folder)); + when(folderService.updateFolderName(archive, folderId, "new")).thenReturn("new"); + + // when + String updated = personalService.updateFolderName(memberId, folderId, "new"); + + // then + assertThat(updated).isEqualTo("new"); + verify(folderService).updateFolderName(eq(archive), eq(folderId), eq("new")); + } + + @Test + @DisplayName("개인 μ•„μΉ΄μ΄λΈŒ 폴더 이름 λ³€κ²½ μ‹€νŒ¨ - 폴더 μ—†μŒ") + void updateFolderName_notFound() { + // given + Integer memberId = 1; + Integer folderId = 301; + when(folderRepository.findByIdAndMemberId(folderId, memberId)).thenReturn(Optional.empty()); + + // when & then + assertThrows(NoResultException.class, + () -> personalService.updateFolderName(memberId, folderId, "new")); + verify(folderService, never()).updateFolderName(any(), anyInt(), anyString()); + } + + @Test + @DisplayName("개인 μ•„μΉ΄μ΄λΈŒ 폴더 이름 λ³€κ²½ μ‹€νŒ¨ - 쀑볡 이름(곡톡 μ„œλΉ„μŠ€ μ˜ˆμ™Έ μ „νŒŒ)") + void updateFolderName_conflict() { + // given + Integer memberId = 1; + Integer folderId = 302; + Archive archive = mockArchive(); + Folder folder = mockFolder(archive, folderId, "old"); + when(folderRepository.findByIdAndMemberId(folderId, memberId)).thenReturn(Optional.of(folder)); + when(folderService.updateFolderName(archive, folderId, "λ³΄κ³ μ„œ")) + .thenThrow(new IllegalArgumentException("이미 μ‘΄μž¬ν•˜λŠ” 폴더λͺ…μž…λ‹ˆλ‹€.")); + + // when & then + assertThrows(IllegalArgumentException.class, + () -> personalService.updateFolderName(memberId, folderId, "λ³΄κ³ μ„œ")); + verify(folderService).updateFolderName(eq(archive), eq(folderId), eq("λ³΄κ³ μ„œ")); + } + + // ---------------------- Read ---------------------- + @Test + @DisplayName("개인 μ•„μΉ΄μ΄λΈŒ 폴더 λͺ©λ‘ 쑰회 성곡") + void getFolders_success() { + // given + Integer memberId = 1; + Archive archive = mockArchive(); + PersonalArchive pa = mockPersonalArchive(archive); + when(personalArchiveRepository.findByMemberId(memberId)).thenReturn(Optional.of(pa)); + when(folderService.getFolders(archive)).thenReturn(List.of( + new FolderResponse("default", 1), + new FolderResponse("docs", 2) + )); + + // when + List rs = personalService.getFolders(memberId); + + // then + assertThat(rs).hasSize(2); + assertThat(rs.getFirst().folderName()).isEqualTo("default"); + verify(folderService).getFolders(eq(archive)); + } + + @Test + @DisplayName("개인 μ•„μΉ΄μ΄λΈŒ 폴더 λͺ©λ‘ 쑰회 μ‹€νŒ¨ - 개인 μ•„μΉ΄μ΄λΈŒ μ—†μŒ") + void getFolders_archiveNotFound() { + // given + Integer memberId = 1; + when(personalArchiveRepository.findByMemberId(memberId)).thenReturn(Optional.empty()); + + // when & then + assertThrows(NoResultException.class, () -> personalService.getFolders(memberId)); + verify(folderService, never()).getFolders(any()); + } + + @Test + @DisplayName("개인 μ•„μΉ΄μ΄λΈŒ 폴더 λ‚΄ 파일 λͺ©λ‘ 쑰회 성곡") + void getFilesInFolder_success() { + // given + Integer memberId = 1; + Integer folderId = 400; + Archive archive = mockArchive(); + Folder folder = mockFolder(archive, folderId, "docs"); + FolderFilesDto dto = new FolderFilesDto(folderId, "docs", List.of()); + when(folderRepository.findByIdAndMemberId(folderId, memberId)).thenReturn(Optional.of(folder)); + when(folderService.getFilesInFolder(archive, folderId)).thenReturn(dto); + + // when + FolderFilesDto rs = personalService.getFilesInFolder(memberId, folderId); + + // then + assertThat(rs.folderId()).isEqualTo(folderId); + assertThat(rs.folderName()).isEqualTo("docs"); + verify(folderService).getFilesInFolder(eq(archive), eq(folderId)); + } + + @Test + @DisplayName("개인 μ•„μΉ΄μ΄λΈŒ 폴더 λ‚΄ 파일 λͺ©λ‘ 쑰회 μ‹€νŒ¨ - 폴더 μ—†μŒ") + void getFilesInFolder_notFound() { + // given + Integer memberId = 1; + Integer folderId = 401; + when(folderRepository.findByIdAndMemberId(folderId, memberId)).thenReturn(Optional.empty()); + + // when & then + assertThrows(NoResultException.class, + () -> personalService.getFilesInFolder(memberId, folderId)); + verify(folderService, never()).getFilesInFolder(any(), anyInt()); + } + + @Test + @DisplayName("default 폴더 ID 쑰회 성곡") + void getDefaultFolderId_success() { + // given + Integer memberId = 1; + Folder defaultFolder = mockFolder(mockArchive(), 42, "default"); + when(folderRepository.findDefaultFolderByMemberId(memberId)).thenReturn(Optional.of(defaultFolder)); + + // when + Integer id = personalService.getDefaultFolderId(memberId); + + // then + assertThat(id).isEqualTo(42); + } + + @Test + @DisplayName("default 폴더 ID 쑰회 μ‹€νŒ¨ - μ—†μŒ") + void getDefaultFolderId_notFound() { + // given + Integer memberId = 1; + when(folderRepository.findDefaultFolderByMemberId(memberId)).thenReturn(Optional.empty()); + + // when & then + assertThrows(NoResultException.class, () -> personalService.getDefaultFolderId(memberId)); + } +} diff --git a/src/test/java/org/tuna/zoopzoop/backend/domain/datasource/controller/DatasourceControllerTest.java b/src/test/java/org/tuna/zoopzoop/backend/domain/datasource/controller/DatasourceControllerTest.java index 9097875a..37a61089 100644 --- a/src/test/java/org/tuna/zoopzoop/backend/domain/datasource/controller/DatasourceControllerTest.java +++ b/src/test/java/org/tuna/zoopzoop/backend/domain/datasource/controller/DatasourceControllerTest.java @@ -19,7 +19,7 @@ import org.tuna.zoopzoop.backend.domain.archive.folder.dto.FolderResponse; import org.tuna.zoopzoop.backend.domain.archive.folder.entity.Folder; import org.tuna.zoopzoop.backend.domain.archive.folder.repository.FolderRepository; -import org.tuna.zoopzoop.backend.domain.archive.folder.service.FolderService; +import org.tuna.zoopzoop.backend.domain.archive.folder.service.PersonalArchiveFolderService; import org.tuna.zoopzoop.backend.domain.datasource.dataprocessor.service.DataProcessorService; import org.tuna.zoopzoop.backend.domain.datasource.dto.DataSourceDto; import org.tuna.zoopzoop.backend.domain.datasource.dto.reqBodyForCreateDataSource; @@ -55,7 +55,7 @@ class DatasourceControllerTest { @Autowired private MemberService memberService; @Autowired private MemberRepository memberRepository; - @Autowired private FolderService folderService; + @Autowired private PersonalArchiveFolderService folderService; @Autowired private FolderRepository folderRepository; @Autowired private DataSourceRepository dataSourceRepository; @@ -116,7 +116,7 @@ void beforeAll() { testMemberId = member.getId(); // docs 폴더 생성 - FolderResponse fr = folderService.createFolderForPersonal(testMemberId, "docs"); + FolderResponse fr = folderService.createFolder(testMemberId, "docs"); docsFolderId = fr.folderId(); Folder docsFolder = folderRepository.findById(docsFolderId).orElseThrow(); @@ -367,7 +367,7 @@ void restore_notFoundIds() throws Exception { @DisplayName("단건 이동 성곡 -> 200") @WithUserDetails(value = "KAKAO:testUser_sc1111", setupBefore = TestExecutionEvent.TEST_METHOD) void moveOne_ok() throws Exception { - FolderResponse newFolder = folderService.createFolderForPersonal(testMemberId, "moveTarget"); + FolderResponse newFolder = folderService.createFolder(testMemberId, "moveTarget"); Integer toId = newFolder.folderId(); var body = new reqBodyForMoveDataSource(toId); @@ -426,7 +426,7 @@ void moveOne_notFound_folder() throws Exception { @DisplayName("자료 닀건 이동 성곡: μ§€μ • 폴더 -> 200") @WithUserDetails(value = "KAKAO:testUser_sc1111", setupBefore = TestExecutionEvent.TEST_METHOD) void moveMany_specific_ok() throws Exception { - FolderResponse newFolder = folderService.createFolderForPersonal(testMemberId, "moveManyTarget"); + FolderResponse newFolder = folderService.createFolder(testMemberId, "moveManyTarget"); Integer toId = newFolder.folderId(); String body = String.format("{\"folderId\":%d,\"dataSourceId\":[%d,%d]}", toId, dataSourceId1, dataSourceId2); diff --git a/src/test/java/org/tuna/zoopzoop/backend/domain/news/service/NewsServiceTest.java b/src/test/java/org/tuna/zoopzoop/backend/domain/news/service/NewsServiceTest.java index a17fc15c..852fafbf 100644 --- a/src/test/java/org/tuna/zoopzoop/backend/domain/news/service/NewsServiceTest.java +++ b/src/test/java/org/tuna/zoopzoop/backend/domain/news/service/NewsServiceTest.java @@ -11,7 +11,7 @@ import org.tuna.zoopzoop.backend.domain.archive.folder.dto.FolderResponse; import org.tuna.zoopzoop.backend.domain.archive.folder.entity.Folder; import org.tuna.zoopzoop.backend.domain.archive.folder.repository.FolderRepository; -import org.tuna.zoopzoop.backend.domain.archive.folder.service.FolderService; +import org.tuna.zoopzoop.backend.domain.archive.folder.service.PersonalArchiveFolderService; import org.tuna.zoopzoop.backend.domain.datasource.entity.Category; import org.tuna.zoopzoop.backend.domain.datasource.entity.DataSource; import org.tuna.zoopzoop.backend.domain.datasource.entity.Tag; @@ -41,7 +41,7 @@ public class NewsServiceTest { private MemberRepository memberRepository; @Autowired - private FolderService folderService; + private PersonalArchiveFolderService folderService; @Autowired private FolderRepository folderRepository; @@ -94,7 +94,7 @@ public void setUp() { Provider.KAKAO ); - FolderResponse folderResponse = folderService.createFolderForPersonal(member.getId(), "newServiceTestFolder"); + FolderResponse folderResponse = folderService.createFolder(member.getId(), "newServiceTestFolder"); newsFolderId = folderResponse.folderId(); Folder folder = folderRepository.findById(folderResponse.folderId()).orElse(null); diff --git a/src/test/java/org/tuna/zoopzoop/backend/domain/space/archive/controller/SpaceArchiveFolderControllerTest.java b/src/test/java/org/tuna/zoopzoop/backend/domain/space/archive/controller/SpaceArchiveFolderControllerTest.java new file mode 100644 index 00000000..63ff5c95 --- /dev/null +++ b/src/test/java/org/tuna/zoopzoop/backend/domain/space/archive/controller/SpaceArchiveFolderControllerTest.java @@ -0,0 +1,385 @@ +package org.tuna.zoopzoop.backend.domain.space.archive.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithUserDetails; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; +import org.tuna.zoopzoop.backend.domain.archive.folder.dto.reqBodyForCreateFolder; +import org.tuna.zoopzoop.backend.domain.archive.folder.entity.Folder; +import org.tuna.zoopzoop.backend.domain.archive.folder.repository.FolderRepository; +import org.tuna.zoopzoop.backend.domain.datasource.entity.Category; +import org.tuna.zoopzoop.backend.domain.datasource.entity.DataSource; +import org.tuna.zoopzoop.backend.domain.datasource.entity.Tag; +import org.tuna.zoopzoop.backend.domain.datasource.repository.DataSourceRepository; +import org.tuna.zoopzoop.backend.domain.member.entity.Member; +import org.tuna.zoopzoop.backend.domain.member.enums.Provider; +import org.tuna.zoopzoop.backend.domain.member.repository.MemberRepository; +import org.tuna.zoopzoop.backend.domain.member.service.MemberService; +import org.tuna.zoopzoop.backend.domain.space.membership.enums.Authority; +import org.tuna.zoopzoop.backend.domain.space.membership.service.MembershipService; +import org.tuna.zoopzoop.backend.domain.space.space.entity.Space; +import org.tuna.zoopzoop.backend.domain.space.space.service.SpaceService; +import org.tuna.zoopzoop.backend.global.jpa.entity.BaseEntity; + +import java.time.LocalDate; +import java.util.List; + +import static org.hamcrest.Matchers.greaterThanOrEqualTo; +import static org.hamcrest.Matchers.nullValue; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@ActiveProfiles("test") +@SpringBootTest +@AutoConfigureMockMvc +@Transactional +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class SpaceArchiveFolderControllerTest { + + @Autowired private MockMvc mockMvc; + @Autowired private ObjectMapper objectMapper; + + @Autowired private MemberService memberService; + @Autowired private MemberRepository memberRepository; + + @Autowired private SpaceService spaceService; + @Autowired private MembershipService membershipService; + + @Autowired private FolderRepository folderRepository; + @Autowired private DataSourceRepository dataSourceRepository; + + private static final String OWNER_PK = "sp1111"; + private static final String READER_PK = "sp2222"; + + private Integer ownerMemberId; + private Integer readerMemberId; + + private Integer spaceId; + private Integer defaultFolderId; + private Integer docsFolderId; + + @BeforeAll + void setUp() { + // μ‚¬μš©μž 생성 + try { memberService.createMember("spaceOwner", "http://img/owner.png", OWNER_PK, Provider.KAKAO); } catch (Exception ignored) {} + try { memberService.createMember("spaceReader", "http://img/reader.png", READER_PK, Provider.KAKAO); } catch (Exception ignored) {} + + ownerMemberId = memberRepository.findByProviderAndProviderKey(Provider.KAKAO, OWNER_PK) + .map(BaseEntity::getId).orElseThrow(); + readerMemberId = memberRepository.findByProviderAndProviderKey(Provider.KAKAO, READER_PK) + .map(BaseEntity::getId).orElseThrow(); + + Member owner = memberRepository.findById(ownerMemberId).orElseThrow(); + Member reader = memberRepository.findById(readerMemberId).orElseThrow(); + + // 슀페이슀 생성 + 멀버십 λΆ€μ—¬ + Space space = spaceService.createSpace("space-folder-test"); + spaceId = space.getId(); + + membershipService.addMemberToSpace(owner, space, Authority.ADMIN); + membershipService.addMemberToSpace(reader, space, Authority.READ_ONLY); + + // 곡유 μ•„μΉ΄μ΄λΈŒμ˜ default 폴더 직접 생성 + // 곡유 μ•„μΉ΄μ΄λΈŒμ˜ default 폴더 idempotent 생성 + var archive = space.getSharingArchive().getArchive(); + + Folder defaultFolder = folderRepository.findByArchiveIdAndIsDefaultTrue(archive.getId()) + .orElseGet(() -> { + Folder f = new Folder("default"); + f.setArchive(archive); + f.setDefault(true); + return folderRepository.saveAndFlush(f); + }); + defaultFolderId = defaultFolder.getId(); + + Folder docsFolder = folderRepository.findByArchiveIdAndName(archive.getId(), "docs") + .orElseGet(() -> { + Folder f = new Folder(); + f.setArchive(archive); + f.setName("docs"); + f.setDefault(false); + return folderRepository.saveAndFlush(f); + }); + docsFolderId = docsFolder.getId(); + + + DataSource d1 = new DataSource(); + d1.setFolder(docsFolder); + d1.setTitle("spec.pdf"); + d1.setSummary("μš”μ•½ A"); + d1.setSourceUrl("http://src/a"); + d1.setImageUrl("http://img/a"); + d1.setDataCreatedDate(LocalDate.now()); + d1.setActive(true); + d1.setTags(List.of(new Tag("tag1"), new Tag("tag2"))); + d1.setCategory(Category.IT); + dataSourceRepository.save(d1); + + DataSource d2 = new DataSource(); + d2.setFolder(docsFolder); + d2.setTitle("notes.txt"); + d2.setSummary("μš”μ•½ B"); + d2.setSourceUrl("http://src/b"); + d2.setImageUrl("http://img/b"); + d2.setDataCreatedDate(LocalDate.now()); + d2.setActive(true); + d2.setTags(List.of()); + d2.setCategory(Category.SCIENCE); + dataSourceRepository.save(d2); + } + + @AfterAll + void tearDown() { + try { + if (docsFolderId != null) { + dataSourceRepository.deleteAll(dataSourceRepository.findAllByFolderId(docsFolderId)); + } + } catch (Exception ignored) {} + } + + // ---------- Create ---------- + @Test + @DisplayName("곡유 μ•„μΉ΄μ΄λΈŒ 폴더 생성 - 성곡 μ‹œ 200κ³Ό 응닡 DTO λ°˜ν™˜") + @WithUserDetails("KAKAO:" + OWNER_PK) + void createFolder_ok() throws Exception { + var req = new reqBodyForCreateFolder("λ³΄κ³ μ„œ"); + + mockMvc.perform(post("/api/v1/spaces/{spaceId}/archive/folder", spaceId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(req))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status").value("200")) + .andExpect(jsonPath("$.msg").value("λ³΄κ³ μ„œ 폴더가 μƒμ„±λ˜μ—ˆμŠ΅λ‹ˆλ‹€.")) + .andExpect(jsonPath("$.data.folderId").isNumber()) + .andExpect(jsonPath("$.data.folderName").value("λ³΄κ³ μ„œ")); + } + + @Test + @DisplayName("곡유 μ•„μΉ΄μ΄λΈŒ 폴더 생성 - 폴더 이름 λˆ„λ½ μ‹œ 400") + @WithUserDetails("KAKAO:" + OWNER_PK) + void createFolder_missingName() throws Exception { + var req = new reqBodyForCreateFolder(null); + + mockMvc.perform(post("/api/v1/spaces/{spaceId}/archive/folder", spaceId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(req))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.status").value("400")); + } + + @Test + @DisplayName("곡유 μ•„μΉ΄μ΄λΈŒ 폴더 생성 μ‹€νŒ¨ - 슀페이슀 ꡬ성원이 μ•„λ‹ˆλ©΄ 403") + @WithUserDetails("KAKAO:" + OWNER_PK) + void createFolder_notMember_forbidden() { + var req = new reqBodyForCreateFolder("x"); + int otherSpaceId = 999999; + // μ‘΄μž¬ν•˜μ§€ μ•ŠμœΌλ©΄ 404인데, μ—¬κΈ°μ„œλŠ” 멀버십 μ‹€νŒ¨λ₯Ό 보기 μœ„ν•΄ μš°μ„  spaceId κ·ΈλŒ€λ‘œ 두고 μ•„λž˜ ν…ŒμŠ€νŠΈλ‘œ λŒ€μ²΄ + // μ‹€μ œλ‘œ 멀버십 μ‹€νŒ¨λ₯Ό μ •ν™•νžˆ 보렀면 별도 슀페이슀λ₯Ό μƒμ„±ν•˜λ˜ ownerλ₯Ό μ΄ˆλŒ€ν•˜μ§€ μ•ŠλŠ” 방법이 ν•„μš” + } + + @Test + @DisplayName("곡유 μ•„μΉ΄μ΄λΈŒ 폴더 생성 μ‹€νŒ¨ - κΆŒν•œ μ—†μŒ(READ_ONLY) 403") + @WithUserDetails("KAKAO:" + READER_PK) + void createFolder_noAuthority_forbidden() throws Exception { + var req = new reqBodyForCreateFolder("μ½κΈ°μ „μš©μ€λͺ»λ§Œλ“¦"); + + mockMvc.perform(post("/api/v1/spaces/{spaceId}/archive/folder", spaceId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(req))) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.status").value("403")) + .andExpect(jsonPath("$.msg").value("폴더 생성 κΆŒν•œμ΄ μ—†μŠ΅λ‹ˆλ‹€.")); + } + + // ---------- Delete ---------- + @Test + @DisplayName("곡유 μ•„μΉ΄μ΄λΈŒ 폴더 μ‚­μ œ - 성곡 μ‹œ 200κ³Ό μ‚­μ œ λ©”μ‹œμ§€ λ°˜ν™˜") + @WithUserDetails("KAKAO:" + OWNER_PK) + void deleteFolder_ok() throws Exception { + var req = new reqBodyForCreateFolder("todelete"); + String content = objectMapper.writeValueAsString(req); + var createRes = mockMvc.perform(post("/api/v1/spaces/{spaceId}/archive/folder", spaceId) + .contentType(MediaType.APPLICATION_JSON) + .content(content)) + .andReturn().getResponse().getContentAsString(); + + Integer toDelete = folderRepository.findByArchiveIdAndName( + folderRepository.findAll().stream().filter(f -> "todelete".equals(f.getName())).findFirst().orElseThrow().getArchive().getId(), + "todelete" + ).orElseThrow().getId(); + + mockMvc.perform(delete("/api/v1/spaces/{spaceId}/archive/folder/{folderId}", spaceId, toDelete)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status").value("200")) + .andExpect(jsonPath("$.msg").value("todelete 폴더가 μ‚­μ œλμŠ΅λ‹ˆλ‹€.")) + .andExpect(jsonPath("$.data").value(nullValue())); + } + + @Test + @DisplayName("곡유 μ•„μΉ΄μ΄λΈŒ 폴더 μ‚­μ œ μ‹€νŒ¨ - κΈ°λ³Έ 폴더면 400") + @WithUserDetails("KAKAO:" + OWNER_PK) + void deleteDefaultFolder_badRequest() throws Exception { + mockMvc.perform(delete("/api/v1/spaces/{spaceId}/archive/folder/{folderId}", spaceId, 0)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.status").value("400")) + .andExpect(jsonPath("$.msg").value("default ν΄λ”λŠ” μ‚­μ œν•  수 μ—†μŠ΅λ‹ˆλ‹€.")) + .andExpect(jsonPath("$.data").value(nullValue())); + } + + @Test + @DisplayName("곡유 μ•„μΉ΄μ΄λΈŒ 폴더 μ‚­μ œ μ‹€νŒ¨ - κΆŒν•œ μ—†μŒ(READ_ONLY) 403") + @WithUserDetails("KAKAO:" + READER_PK) + void deleteFolder_noAuthority_forbidden() throws Exception { + mockMvc.perform(delete("/api/v1/spaces/{spaceId}/archive/folder/{folderId}", spaceId, docsFolderId)) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.status").value("403")) + .andExpect(jsonPath("$.msg").value("폴더 μ‚­μ œ κΆŒν•œμ΄ μ—†μŠ΅λ‹ˆλ‹€.")); + } + + @Test + @DisplayName("곡유 μ•„μΉ΄μ΄λΈŒ 폴더 μ‚­μ œ μ‹€νŒ¨ - 폴더가 μ—†μœΌλ©΄ 404") + @WithUserDetails("KAKAO:" + OWNER_PK) + void deleteFolder_notFound() throws Exception { + mockMvc.perform(delete("/api/v1/spaces/{spaceId}/archive/folder/{folderId}", spaceId, 999999)) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.status").value("404")) + .andExpect(jsonPath("$.msg").value("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ν΄λ”μž…λ‹ˆλ‹€.")); + } + + // ---------- Update ---------- + @Test + @DisplayName("곡유 μ•„μΉ΄μ΄λΈŒ 폴더 이름 λ³€κ²½ - 성곡 μ‹œ 200κ³Ό λ³€κ²½λœ 이름 λ°˜ν™˜") + @WithUserDetails("KAKAO:" + OWNER_PK) + void updateFolder_ok() throws Exception { + var body = new java.util.HashMap(); + body.put("folderName", "회의둝"); + + mockMvc.perform(patch("/api/v1/spaces/{spaceId}/archive/folder/{folderId}", spaceId, docsFolderId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(body))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status").value("200")) + .andExpect(jsonPath("$.msg").value("폴더 이름이 회의둝(으)둜 λ³€κ²½λμŠ΅λ‹ˆλ‹€.")) + .andExpect(jsonPath("$.data.folderName").value("회의둝")); + } + + @Test + @DisplayName("곡유 μ•„μΉ΄μ΄λΈŒ 폴더 이름 λ³€κ²½ μ‹€νŒ¨ - κΈ°λ³Έ 폴더면 400") + @WithUserDetails("KAKAO:" + OWNER_PK) + void updateDefaultFolder_badRequest() throws Exception { + var body = new java.util.HashMap(); + body.put("folderName", "λ¬΄μ‹œλ¨"); + + mockMvc.perform(patch("/api/v1/spaces/{spaceId}/archive/folder/{folderId}", spaceId, 0) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(body))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.status").value("400")) + .andExpect(jsonPath("$.msg").value("default ν΄λ”λŠ” 이름을 λ³€κ²½ν•  수 μ—†μŠ΅λ‹ˆλ‹€.")) + .andExpect(jsonPath("$.data").value(nullValue())); + } + + @Test + @DisplayName("곡유 μ•„μΉ΄μ΄λΈŒ 폴더 이름 λ³€κ²½ μ‹€νŒ¨ - κΆŒν•œ μ—†μŒ(READ_ONLY) 403") + @WithUserDetails("KAKAO:" + READER_PK) + void updateFolder_noAuthority() throws Exception { + var body = new java.util.HashMap(); + body.put("folderName", "λ³€κ²½λΆˆκ°€"); + + mockMvc.perform(patch("/api/v1/spaces/{spaceId}/archive/folder/{folderId}", spaceId, docsFolderId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(body))) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.status").value("403")) + .andExpect(jsonPath("$.msg").value("폴더 μˆ˜μ • κΆŒν•œμ΄ μ—†μŠ΅λ‹ˆλ‹€.")); + } + + @Test + @DisplayName("곡유 μ•„μΉ΄μ΄λΈŒ 폴더 이름 λ³€κ²½ μ‹€νŒ¨ - 폴더가 μ—†μœΌλ©΄ 404") + @WithUserDetails("KAKAO:" + OWNER_PK) + void updateFolder_notFound() throws Exception { + var body = new java.util.HashMap(); + body.put("folderName", "μ–΄λ”¨λ‹ˆ"); + + mockMvc.perform(patch("/api/v1/spaces/{spaceId}/archive/folder/{folderId}", spaceId, 999999) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(body))) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.status").value("404")) + .andExpect(jsonPath("$.msg").value("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ν΄λ”μž…λ‹ˆλ‹€.")); + } + + // ---------- Read ---------- + @Test + @DisplayName("곡유 μ•„μΉ΄μ΄λΈŒ 폴더 λͺ©λ‘ 쑰회 - 성곡") + @WithUserDetails("KAKAO:" + OWNER_PK) + void listFolders_success() throws Exception { + mockMvc.perform(get("/api/v1/spaces/{spaceId}/archive/folder", spaceId) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status").value("200")) + .andExpect(jsonPath("$.msg").value("곡유 μ•„μΉ΄μ΄λΈŒμ˜ 폴더 λͺ©λ‘μ΄ μ‘°νšŒλ˜μ—ˆμŠ΅λ‹ˆλ‹€.")) + .andExpect(jsonPath("$.data").isArray()); + } + + @Test + @DisplayName("곡유 μ•„μΉ΄μ΄λΈŒ 폴더 λͺ©λ‘ 쑰회 μ‹€νŒ¨ - 읽기 κΆŒν•œ μ—†μœΌλ©΄ 403") + @WithUserDetails("KAKAO:" + OWNER_PK) + void listFolders_forbidden_when_not_member() throws Exception { + Space other = spaceService.createSpace("no-membership-space"); + Integer otherSpaceId = other.getId(); + + mockMvc.perform(get("/api/v1/spaces/{spaceId}/archive/folder", otherSpaceId) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.status").value("403")) + .andExpect(jsonPath("$.msg").value("슀페이슀의 ꡬ성원이 μ•„λ‹™λ‹ˆλ‹€.")); + } + + @Test + @DisplayName("곡유 μ•„μΉ΄μ΄λΈŒ νŠΉμ • 폴더 λ‚΄ 파일 λͺ©λ‘ 쑰회 - 성곡") + @WithUserDetails("KAKAO:" + OWNER_PK) + void filesInFolder_success() throws Exception { + mockMvc.perform(get("/api/v1/spaces/{spaceId}/archive/folder/{folderId}/files", spaceId, docsFolderId) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status").value("200")) + .andExpect(jsonPath("$.msg").value("폴더 μ•ˆμ˜ 파일 λͺ©λ‘μ„ λΆˆλŸ¬μ™”μŠ΅λ‹ˆλ‹€.")) + .andExpect(jsonPath("$.data.folderId").value(docsFolderId)) + .andExpect(jsonPath("$.data.folderName").isString()) + .andExpect(jsonPath("$.data.files").isArray()) + .andExpect(jsonPath("$.data.files.length()").value(greaterThanOrEqualTo(2))); + } + + @Test + @DisplayName("곡유 μ•„μΉ΄μ΄λΈŒ κΈ°λ³Έ 폴더 λ‚΄ 파일 λͺ©λ‘ 쑰회 - 성곡") + @WithUserDetails("KAKAO:" + OWNER_PK) + void filesInDefaultFolder_success() throws Exception { + mockMvc.perform(get("/api/v1/spaces/{spaceId}/archive/folder/{folderId}/files", spaceId, 0) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status").value("200")) + .andExpect(jsonPath("$.msg").value("폴더 μ•ˆμ˜ 파일 λͺ©λ‘μ„ λΆˆλŸ¬μ™”μŠ΅λ‹ˆλ‹€.")) + .andExpect(jsonPath("$.data.folderId").isNumber()) + .andExpect(jsonPath("$.data.folderName").isString()) + .andExpect(jsonPath("$.data.files").isArray()); + } + + @Test + @DisplayName("곡유 μ•„μΉ΄μ΄λΈŒ 폴더 λ‚΄ 파일 λͺ©λ‘ 쑰회 μ‹€νŒ¨ - 폴더가 μ—†μœΌλ©΄ 404") + @WithUserDetails("KAKAO:" + OWNER_PK) + void filesInFolder_notFound() throws Exception { + mockMvc.perform(get("/api/v1/spaces/{spaceId}/archive/folder/{folderId}/files", spaceId, 999999) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.status").value("404")) + .andExpect(jsonPath("$.msg").value("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ν΄λ”μž…λ‹ˆλ‹€.")); + } +} diff --git a/src/test/java/org/tuna/zoopzoop/backend/domain/space/archive/service/SpaceArchiveFolderServiceTest.java b/src/test/java/org/tuna/zoopzoop/backend/domain/space/archive/service/SpaceArchiveFolderServiceTest.java new file mode 100644 index 00000000..5a35865a --- /dev/null +++ b/src/test/java/org/tuna/zoopzoop/backend/domain/space/archive/service/SpaceArchiveFolderServiceTest.java @@ -0,0 +1,395 @@ +package org.tuna.zoopzoop.backend.domain.space.archive.service; + +import jakarta.persistence.NoResultException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; +import org.tuna.zoopzoop.backend.domain.archive.archive.entity.Archive; +import org.tuna.zoopzoop.backend.domain.archive.archive.entity.SharingArchive; +import org.tuna.zoopzoop.backend.domain.archive.folder.dto.FolderResponse; +import org.tuna.zoopzoop.backend.domain.archive.folder.service.FolderService; +import org.tuna.zoopzoop.backend.domain.datasource.dto.FolderFilesDto; +import org.tuna.zoopzoop.backend.domain.member.entity.Member; +import org.tuna.zoopzoop.backend.domain.space.membership.entity.Membership; +import org.tuna.zoopzoop.backend.domain.space.membership.enums.Authority; +import org.tuna.zoopzoop.backend.domain.space.membership.service.MembershipService; +import org.tuna.zoopzoop.backend.domain.space.space.entity.Space; +import org.tuna.zoopzoop.backend.domain.space.space.service.SpaceService; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class SpaceArchiveFolderServiceTest { + + @Mock private SpaceService spaceService; + @Mock private MembershipService membershipService; + @Mock private FolderService folderService; + + @InjectMocks private SpaceArchiveFolderService spaceArchiveFolderService; + + // --------- helpers --------- + private Member requester() { + Member m = new Member(); + ReflectionTestUtils.setField(m, "id", 7); + return m; + } + + private Space spaceWithArchive(int spaceId) { + Space s = new Space(); + ReflectionTestUtils.setField(s, "id", spaceId); + + SharingArchive sa = new SharingArchive(); + Archive a = new Archive(); + ReflectionTestUtils.setField(a, "id", 999); + sa.setArchive(a); + sa.setSpace(s); + + s.setSharingArchive(sa); + return s; + } + + private Membership membership(Authority auth) { + Membership ms = new Membership(); + ms.setAuthority(auth); + return ms; + } + + // ---------------------- Create ---------------------- + @Test + @DisplayName("곡유 μ•„μΉ΄μ΄λΈŒ 폴더 생성 성곡 - κΆŒν•œ 있음") + void createFolder_success() { + Integer spaceId = 1; + Member req = requester(); + Space space = spaceWithArchive(spaceId); + + when(spaceService.findById(spaceId)).thenReturn(space); + when(membershipService.isMemberJoinedSpace(req, space)).thenReturn(true); + when(membershipService.findByMemberAndSpace(req, space)).thenReturn(membership(Authority.ADMIN)); + when(folderService.createFolder(any(Archive.class), eq("λ³΄κ³ μ„œ"))) + .thenReturn(new FolderResponse("λ³΄κ³ μ„œ", 123)); + + FolderResponse rs = spaceArchiveFolderService.createFolder(spaceId, req, "λ³΄κ³ μ„œ"); + + assertThat(rs.folderName()).isEqualTo("λ³΄κ³ μ„œ"); + assertThat(rs.folderId()).isEqualTo(123); + verify(folderService).createFolder(eq(space.getSharingArchive().getArchive()), eq("λ³΄κ³ μ„œ")); + } + + @Test + @DisplayName("곡유 μ•„μΉ΄μ΄λΈŒ 폴더 생성 μ‹€νŒ¨ - 슀페이슀 μ—†μŒ") + void createFolder_spaceNotFound() { + Integer spaceId = 999; + Member req = requester(); + + when(spaceService.findById(spaceId)).thenThrow(new NoResultException("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μŠ€νŽ˜μ΄μŠ€μž…λ‹ˆλ‹€.")); + + assertThrows(NoResultException.class, + () -> spaceArchiveFolderService.createFolder(spaceId, req, "λ³΄κ³ μ„œ")); + verifyNoInteractions(folderService); + } + + @Test + @DisplayName("곡유 μ•„μΉ΄μ΄λΈŒ 폴더 생성 μ‹€νŒ¨ - ꡬ성원 μ•„λ‹˜") + void createFolder_notMember() { + Integer spaceId = 1; + Member req = requester(); + Space space = spaceWithArchive(spaceId); + + when(spaceService.findById(spaceId)).thenReturn(space); + when(membershipService.isMemberJoinedSpace(req, space)).thenReturn(false); + + assertThrows(SecurityException.class, + () -> spaceArchiveFolderService.createFolder(spaceId, req, "λ³΄κ³ μ„œ")); + verifyNoInteractions(folderService); + } + + @Test + @DisplayName("곡유 μ•„μΉ΄μ΄λΈŒ 폴더 생성 μ‹€νŒ¨ - κΆŒν•œ μ—†μŒ (PENDING/READ_ONLY)") + void createFolder_noAuthority() { + Integer spaceId = 1; + Member req = requester(); + Space space = spaceWithArchive(spaceId); + + when(spaceService.findById(spaceId)).thenReturn(space); + when(membershipService.isMemberJoinedSpace(req, space)).thenReturn(true); + when(membershipService.findByMemberAndSpace(req, space)).thenReturn(membership(Authority.READ_ONLY)); + + assertThrows(SecurityException.class, + () -> spaceArchiveFolderService.createFolder(spaceId, req, "λ³΄κ³ μ„œ")); + verifyNoInteractions(folderService); + } + + // ---------------------- Delete ---------------------- + @Test + @DisplayName("곡유 μ•„μΉ΄μ΄λΈŒ 폴더 μ‚­μ œ 성곡") + void deleteFolder_success() { + Integer spaceId = 1, folderId = 10; + Member req = requester(); + Space space = spaceWithArchive(spaceId); + + when(spaceService.findById(spaceId)).thenReturn(space); + when(membershipService.isMemberJoinedSpace(req, space)).thenReturn(true); + when(membershipService.findByMemberAndSpace(req, space)).thenReturn(membership(Authority.ADMIN)); + when(folderService.deleteFolder(space.getSharingArchive().getArchive(), folderId)) + .thenReturn("docs"); + + String deleted = spaceArchiveFolderService.deleteFolder(spaceId, req, folderId); + + assertThat(deleted).isEqualTo("docs"); + verify(folderService).deleteFolder(eq(space.getSharingArchive().getArchive()), eq(folderId)); + } + + @Test + @DisplayName("곡유 μ•„μΉ΄μ΄λΈŒ 폴더 μ‚­μ œ μ‹€νŒ¨ - κΆŒν•œ μ—†μŒ") + void deleteFolder_noAuthority() { + Integer spaceId = 1, folderId = 10; + Member req = requester(); + Space space = spaceWithArchive(spaceId); + + when(spaceService.findById(spaceId)).thenReturn(space); + when(membershipService.isMemberJoinedSpace(req, space)).thenReturn(true); + when(membershipService.findByMemberAndSpace(req, space)).thenReturn(membership(Authority.READ_ONLY)); + + assertThrows(SecurityException.class, + () -> spaceArchiveFolderService.deleteFolder(spaceId, req, folderId)); + verifyNoInteractions(folderService); + } + + @Test + @DisplayName("곡유 μ•„μΉ΄μ΄λΈŒ 폴더 μ‚­μ œ μ‹€νŒ¨ - μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” 폴더(곡톡 μ„œλΉ„μŠ€ μ˜ˆμ™Έ μ „νŒŒ)") + void deleteFolder_notFound() { + Integer spaceId = 1, folderId = 999; + Member req = requester(); + Space space = spaceWithArchive(spaceId); + + when(spaceService.findById(spaceId)).thenReturn(space); + when(membershipService.isMemberJoinedSpace(req, space)).thenReturn(true); + when(membershipService.findByMemberAndSpace(req, space)).thenReturn(membership(Authority.ADMIN)); + when(folderService.deleteFolder(space.getSharingArchive().getArchive(), folderId)) + .thenThrow(new NoResultException("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ν΄λ”μž…λ‹ˆλ‹€.")); + + assertThrows(NoResultException.class, + () -> spaceArchiveFolderService.deleteFolder(spaceId, req, folderId)); + verify(folderService).deleteFolder(eq(space.getSharingArchive().getArchive()), eq(folderId)); + } + + @Test + @DisplayName("곡유 μ•„μΉ΄μ΄λΈŒ 폴더 μ‚­μ œ μ‹€νŒ¨ - κΈ°λ³Έ ν΄λ”λŠ” μ‚­μ œ λΆˆκ°€(곡톡 μ„œλΉ„μŠ€ μ˜ˆμ™Έ μ „νŒŒ)") + void deleteFolder_defaultForbidden() { + Integer spaceId = 1, folderId = 0; + Member req = requester(); + Space space = spaceWithArchive(spaceId); + + when(spaceService.findById(spaceId)).thenReturn(space); + when(membershipService.isMemberJoinedSpace(req, space)).thenReturn(true); + when(membershipService.findByMemberAndSpace(req, space)).thenReturn(membership(Authority.ADMIN)); + when(folderService.deleteFolder(space.getSharingArchive().getArchive(), folderId)) + .thenThrow(new IllegalArgumentException("default ν΄λ”λŠ” μ‚­μ œν•  수 μ—†μŠ΅λ‹ˆλ‹€.")); + + assertThrows(IllegalArgumentException.class, + () -> spaceArchiveFolderService.deleteFolder(spaceId, req, folderId)); + verify(folderService).deleteFolder(eq(space.getSharingArchive().getArchive()), eq(folderId)); + } + + // ---------------------- Update ---------------------- + @Test + @DisplayName("곡유 μ•„μΉ΄μ΄λΈŒ 폴더 이름 λ³€κ²½ 성곡") + void updateFolderName_success() { + Integer spaceId = 1, folderId = 22; + Member req = requester(); + Space space = spaceWithArchive(spaceId); + + when(spaceService.findById(spaceId)).thenReturn(space); + when(membershipService.isMemberJoinedSpace(req, space)).thenReturn(true); + when(membershipService.findByMemberAndSpace(req, space)).thenReturn(membership(Authority.ADMIN)); + when(folderService.updateFolderName(space.getSharingArchive().getArchive(), folderId, "μƒˆμ΄λ¦„")) + .thenReturn("μƒˆμ΄λ¦„"); + + String updated = spaceArchiveFolderService.updateFolderName(spaceId, req, folderId, "μƒˆμ΄λ¦„"); + + assertThat(updated).isEqualTo("μƒˆμ΄λ¦„"); + verify(folderService).updateFolderName(eq(space.getSharingArchive().getArchive()), eq(folderId), eq("μƒˆμ΄λ¦„")); + } + + @Test + @DisplayName("곡유 μ•„μΉ΄μ΄λΈŒ 폴더 이름 λ³€κ²½ μ‹€νŒ¨ - 쀑볡 이름 쑴재(곡톡 μ„œλΉ„μŠ€ μ˜ˆμ™Έ μ „νŒŒ)") + void updateFolderName_conflict() { + Integer spaceId = 1, folderId = 22; + Member req = requester(); + Space space = spaceWithArchive(spaceId); + + when(spaceService.findById(spaceId)).thenReturn(space); + when(membershipService.isMemberJoinedSpace(req, space)).thenReturn(true); + when(membershipService.findByMemberAndSpace(req, space)).thenReturn(membership(Authority.ADMIN)); + when(folderService.updateFolderName(space.getSharingArchive().getArchive(), folderId, "λ³΄κ³ μ„œ")) + .thenThrow(new IllegalArgumentException("이미 μ‘΄μž¬ν•˜λŠ” 폴더λͺ…μž…λ‹ˆλ‹€.")); + + assertThrows(IllegalArgumentException.class, + () -> spaceArchiveFolderService.updateFolderName(spaceId, req, folderId, "λ³΄κ³ μ„œ")); + verify(folderService).updateFolderName(eq(space.getSharingArchive().getArchive()), eq(folderId), eq("λ³΄κ³ μ„œ")); + } + + @Test + @DisplayName("곡유 μ•„μΉ΄μ΄λΈŒ 폴더 이름 λ³€κ²½ μ‹€νŒ¨ - 폴더 μ—†μŒ(곡톡 μ„œλΉ„μŠ€ μ˜ˆμ™Έ μ „νŒŒ)") + void updateFolderName_notFound() { + Integer spaceId = 1, folderId = 22; + Member req = requester(); + Space space = spaceWithArchive(spaceId); + + when(spaceService.findById(spaceId)).thenReturn(space); + when(membershipService.isMemberJoinedSpace(req, space)).thenReturn(true); + when(membershipService.findByMemberAndSpace(req, space)).thenReturn(membership(Authority.ADMIN)); + when(folderService.updateFolderName(space.getSharingArchive().getArchive(), folderId, "μƒˆμ΄λ¦„")) + .thenThrow(new NoResultException("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ν΄λ”μž…λ‹ˆλ‹€.")); + + assertThrows(NoResultException.class, + () -> spaceArchiveFolderService.updateFolderName(spaceId, req, folderId, "μƒˆμ΄λ¦„")); + verify(folderService).updateFolderName(eq(space.getSharingArchive().getArchive()), eq(folderId), eq("μƒˆμ΄λ¦„")); + } + + @Test + @DisplayName("곡유 μ•„μΉ΄μ΄λΈŒ 폴더 이름 λ³€κ²½ μ‹€νŒ¨ - κΆŒν•œ μ—†μŒ") + void updateFolderName_noAuthority() { + Integer spaceId = 1, folderId = 22; + Member req = requester(); + Space space = spaceWithArchive(spaceId); + + when(spaceService.findById(spaceId)).thenReturn(space); + when(membershipService.isMemberJoinedSpace(req, space)).thenReturn(true); + when(membershipService.findByMemberAndSpace(req, space)).thenReturn(membership(Authority.READ_ONLY)); + + assertThrows(SecurityException.class, + () -> spaceArchiveFolderService.updateFolderName(spaceId, req, folderId, "μƒˆμ΄λ¦„")); + verifyNoInteractions(folderService); + } + + // ---------------------- Read: λͺ©λ‘/파일 ---------------------- + @Test + @DisplayName("곡유 μ•„μΉ΄μ΄λΈŒ 폴더 λͺ©λ‘ 쑰회 성곡") + void getFolders_success() { + Integer spaceId = 1; + Member req = requester(); + Space space = spaceWithArchive(spaceId); + + when(spaceService.findById(spaceId)).thenReturn(space); + when(membershipService.isMemberInSpace(req, space)).thenReturn(true); + when(folderService.getFolders(space.getSharingArchive().getArchive())) + .thenReturn(List.of(new FolderResponse("default", 1), new FolderResponse("docs", 2))); + + List rs = spaceArchiveFolderService.getFolders(spaceId, req); + + assertThat(rs).hasSize(2); + assertThat(rs.getFirst().folderName()).isEqualTo("default"); + verify(folderService).getFolders(eq(space.getSharingArchive().getArchive())); + } + + @Test + @DisplayName("곡유 μ•„μΉ΄μ΄λΈŒ 폴더 λͺ©λ‘ 쑰회 μ‹€νŒ¨ - κΆŒν•œ μ—†μŒ") + void getFolders_noAuthority() { + Integer spaceId = 1; + Member req = requester(); + Space space = spaceWithArchive(spaceId); + + when(spaceService.findById(spaceId)).thenReturn(space); + when(membershipService.isMemberInSpace(req, space)).thenReturn(false); + + assertThrows(SecurityException.class, + () -> spaceArchiveFolderService.getFolders(spaceId, req)); + verifyNoInteractions(folderService); + } + + @Test + @DisplayName("곡유 μ•„μΉ΄μ΄λΈŒ νŠΉμ • 폴더 λ‚΄ 파일 λͺ©λ‘ 쑰회 성곡") + void getFilesInFolder_success() { + Integer spaceId = 1, folderId = 50; + Member req = requester(); + Space space = spaceWithArchive(spaceId); + FolderFilesDto dto = new FolderFilesDto(folderId, "docs", List.of()); + + when(spaceService.findById(spaceId)).thenReturn(space); + when(membershipService.isMemberInSpace(req, space)).thenReturn(true); + when(folderService.getFilesInFolder(space.getSharingArchive().getArchive(), folderId)).thenReturn(dto); + + FolderFilesDto rs = spaceArchiveFolderService.getFilesInFolder(spaceId, req, folderId); + + assertThat(rs.folderId()).isEqualTo(folderId); + assertThat(rs.folderName()).isEqualTo("docs"); + verify(folderService).getFilesInFolder(eq(space.getSharingArchive().getArchive()), eq(folderId)); + } + + @Test + @DisplayName("곡유 μ•„μΉ΄μ΄λΈŒ 폴더 λ‚΄ 파일 λͺ©λ‘ 쑰회 μ‹€νŒ¨ - 폴더 μ—†μŒ(곡톡 μ„œλΉ„μŠ€ μ˜ˆμ™Έ μ „νŒŒ)") + void getFilesInFolder_notFound() { + Integer spaceId = 1, folderId = 404; + Member req = requester(); + Space space = spaceWithArchive(spaceId); + + when(spaceService.findById(spaceId)).thenReturn(space); + when(membershipService.isMemberInSpace(req, space)).thenReturn(true); + when(folderService.getFilesInFolder(space.getSharingArchive().getArchive(), folderId)) + .thenThrow(new NoResultException("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ν΄λ”μž…λ‹ˆλ‹€.")); + + assertThrows(NoResultException.class, + () -> spaceArchiveFolderService.getFilesInFolder(spaceId, req, folderId)); + verify(folderService).getFilesInFolder(eq(space.getSharingArchive().getArchive()), eq(folderId)); + } + + @Test + @DisplayName("곡유 μ•„μΉ΄μ΄λΈŒ 폴더 λ‚΄ 파일 λͺ©λ‘ 쑰회 μ‹€νŒ¨ - κΆŒν•œ μ—†μŒ") + void getFilesInFolder_noAuthority() { + Integer spaceId = 1, folderId = 50; + Member req = requester(); + Space space = spaceWithArchive(spaceId); + + when(spaceService.findById(spaceId)).thenReturn(space); + when(membershipService.isMemberInSpace(req, space)).thenReturn(false); + + assertThrows(SecurityException.class, + () -> spaceArchiveFolderService.getFilesInFolder(spaceId, req, folderId)); + verifyNoInteractions(folderService); + } + + // ---------------------- Read: default ID ---------------------- + @Test + @DisplayName("곡유 μ•„μΉ΄μ΄λΈŒ default 폴더 ID 쑰회 성곡") + void getDefaultFolderId_success() { + Integer spaceId = 1; + Member req = requester(); + Space space = spaceWithArchive(spaceId); + + when(spaceService.findById(spaceId)).thenReturn(space); + when(membershipService.isMemberInSpace(req, space)).thenReturn(true); + when(folderService.getDefaultFolderId(space.getSharingArchive().getArchive())) + .thenReturn(42); + + Integer id = spaceArchiveFolderService.getDefaultFolderId(spaceId, req); + + assertThat(id).isEqualTo(42); + verify(folderService).getDefaultFolderId(eq(space.getSharingArchive().getArchive())); + } + + @Test + @DisplayName("곡유 μ•„μΉ΄μ΄λΈŒ default 폴더 ID 쑰회 μ‹€νŒ¨ - κΆŒν•œ μ—†μŒ") + void getDefaultFolderId_noAuthority() { + Integer spaceId = 1; + Member req = requester(); + Space space = spaceWithArchive(spaceId); + + when(spaceService.findById(spaceId)).thenReturn(space); + when(membershipService.isMemberInSpace(req, space)).thenReturn(false); + + assertThrows(SecurityException.class, + () -> spaceArchiveFolderService.getDefaultFolderId(spaceId, req)); + verifyNoInteractions(folderService); + } +}