From 7e8d927d4109d5ec5fba5226a4e4c10cac835b04 Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Thu, 7 May 2026 21:31:56 +0900 Subject: [PATCH 1/2] =?UTF-8?q?[fix]=20=EB=AC=B8=EC=A0=9C=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D=20=EA=B3=BC=EC=A0=95=20folderId=20null=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../problem/service/ProblemService.java | 13 +++++- .../problem/service/ProblemServiceTest.java | 42 +++++++++++++++++++ 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/aisip/OnO/backend/problem/service/ProblemService.java b/src/main/java/com/aisip/OnO/backend/problem/service/ProblemService.java index 3cc8af4a..b1bfaabc 100644 --- a/src/main/java/com/aisip/OnO/backend/problem/service/ProblemService.java +++ b/src/main/java/com/aisip/OnO/backend/problem/service/ProblemService.java @@ -206,8 +206,7 @@ public Long registerProblem(ProblemRegisterDto problemRegisterDto, Long userId) */ @Transactional public Long registerProblemV2(ProblemRegisterV2Dto problemRegisterV2Dto, Long userId) { - Folder folder = folderRepository.findById(problemRegisterV2Dto.folderId()) - .orElseThrow(() -> new ApplicationException(FolderErrorCase.FOLDER_NOT_FOUND)); + Folder folder = resolveRegisterFolder(problemRegisterV2Dto.folderId(), userId); ProblemRegisterDto baseDto = new ProblemRegisterDto( problemRegisterV2Dto.problemId(), @@ -256,6 +255,16 @@ public Long registerProblemV2(ProblemRegisterV2Dto problemRegisterV2Dto, Long us return problem.getId(); } + private Folder resolveRegisterFolder(Long folderId, Long userId) { + if (folderId == null) { + return folderRepository.findByUserIdAndParentFolderIsNull(userId) + .orElseThrow(() -> new ApplicationException(FolderErrorCase.ROOT_FOLDER_NOT_EXIST)); + } + + return folderRepository.findById(folderId) + .orElseThrow(() -> new ApplicationException(FolderErrorCase.FOLDER_NOT_FOUND)); + } + /** * 문제 이미지 비동기 업로드 및 AI 분석 트리거 */ diff --git a/src/test/java/com/aisip/OnO/backend/problem/service/ProblemServiceTest.java b/src/test/java/com/aisip/OnO/backend/problem/service/ProblemServiceTest.java index 73414c04..06db3989 100644 --- a/src/test/java/com/aisip/OnO/backend/problem/service/ProblemServiceTest.java +++ b/src/test/java/com/aisip/OnO/backend/problem/service/ProblemServiceTest.java @@ -8,6 +8,7 @@ import com.aisip.OnO.backend.folder.repository.FolderRepository; import com.aisip.OnO.backend.problem.dto.ProblemImageDataRegisterDto; import com.aisip.OnO.backend.problem.dto.ProblemRegisterDto; +import com.aisip.OnO.backend.problem.dto.ProblemRegisterV2Dto; import com.aisip.OnO.backend.problem.dto.ProblemResponseDto; import com.aisip.OnO.backend.problem.entity.Problem; import com.aisip.OnO.backend.problem.entity.ProblemImageData; @@ -17,6 +18,9 @@ import com.aisip.OnO.backend.problemsolve.entity.AnswerStatus; import com.aisip.OnO.backend.problemsolve.entity.ProblemSolve; import com.aisip.OnO.backend.problemsolve.repository.ProblemSolveRepository; +import com.aisip.OnO.backend.user.dto.UserRegisterDto; +import com.aisip.OnO.backend.user.entity.User; +import com.aisip.OnO.backend.user.repository.UserRepository; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -56,6 +60,9 @@ class ProblemServiceTest { @Autowired private ProblemSolveRepository problemSolveRepository; + @Autowired + private UserRepository userRepository; + @MockBean private FileUploadService fileUploadService; @@ -264,6 +271,41 @@ void registerProblem_folderNotFound() { .hasMessageContaining(FolderErrorCase.FOLDER_NOT_FOUND.getMessage()); } + @Test + @DisplayName("문제 등록 v2 - folderId가 없으면 루트 폴더에 등록") + void registerProblemV2_nullFolderIdUsesRootFolder() { + // Given + User user = userRepository.save(User.from(new UserRegisterDto( + "root-user@example.com", + "root-user", + "root-user-" + System.nanoTime(), + "MEMBER", + null + ))); + Long rootUserId = user.getId(); + Folder rootFolder = folderRepository.save(Folder.from( + new FolderRegisterDto("root", null, null), + rootUserId + )); + ProblemRegisterV2Dto dto = new ProblemRegisterV2Dto( + null, + "memo", + "reference", + null, + LocalDateTime.now(), + List.of("https://example.com/problem.png"), + List.of("https://example.com/answer.png"), + null + ); + + // When + Long problemId = problemService.registerProblemV2(dto, rootUserId); + + // Then + Problem problem = problemRepository.findById(problemId).orElseThrow(); + assertThat(problem.getFolder().getId()).isEqualTo(rootFolder.getId()); + } + @Test @DisplayName("문제 이미지 등록하기") void registerProblemImageData() { From c97d9817ec3d05485e4a7f2f8cd9e16b7c817fb4 Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Thu, 7 May 2026 22:04:13 +0900 Subject: [PATCH 2/2] =?UTF-8?q?[fix]=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EB=A6=AC=EC=86=8C=EC=8A=A4=20=EC=A0=91=EA=B7=BC=20=EC=8B=9C=20?= =?UTF-8?q?=EC=86=8C=EC=9C=A0=EC=9E=90=20=EA=B2=80=EC=A6=9D=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/AdminProblemController.java | 4 +- .../folder/controller/FolderController.java | 9 +- .../backend/folder/service/FolderService.java | 54 +++++++++--- .../controller/PracticeNoteController.java | 4 +- .../exception/PracticeNoteErrorCase.java | 4 +- .../service/PracticeNoteService.java | 43 +++++++--- .../problem/controller/ProblemController.java | 17 ++-- .../ProblemImageDataRepository.java | 3 + .../problem/service/ProblemService.java | 82 ++++++++++++++++--- .../ProblemSolveImageDataRepository.java | 5 +- .../controller/FileUploadController.java | 5 +- .../exception/FileUploadErrorCase.java | 4 +- .../fileupload/service/FileUploadService.java | 41 ++++++++++ .../folder/service/FolderServiceTest.java | 41 ++++++++-- .../service/PracticeNoteServiceTest.java | 45 +++++++++- .../controller/ProblemControllerTest.java | 8 +- .../problem/service/ProblemServiceTest.java | 82 +++++++++++++++++-- 17 files changed, 374 insertions(+), 77 deletions(-) diff --git a/src/main/java/com/aisip/OnO/backend/admin/controller/AdminProblemController.java b/src/main/java/com/aisip/OnO/backend/admin/controller/AdminProblemController.java index 2ef6b3e6..54d6b4e0 100644 --- a/src/main/java/com/aisip/OnO/backend/admin/controller/AdminProblemController.java +++ b/src/main/java/com/aisip/OnO/backend/admin/controller/AdminProblemController.java @@ -70,11 +70,11 @@ public String getAllProblems( @GetMapping("/problem/{problemId}") public String getProblemDetail(@PathVariable(name = "problemId") Long problemId, Model model) { - ProblemResponseDto problem = problemService.findProblem(problemId); + ProblemResponseDto problem = problemService.findProblemForAdmin(problemId); model.addAttribute("problem", problem); // 폴더 및 작성자 정보 조회 - FolderResponseDto folder = folderService.findFolder(problem.folderId()); + FolderResponseDto folder = folderService.findFolderForAdmin(problem.folderId()); UserResponseDto user = userService.findUser(folder.userId()); List problemSolves = problemSolveService.getAdminProblemSolvesByProblemId(problemId); model.addAttribute("folder", folder); diff --git a/src/main/java/com/aisip/OnO/backend/folder/controller/FolderController.java b/src/main/java/com/aisip/OnO/backend/folder/controller/FolderController.java index 40f78c55..75c07803 100644 --- a/src/main/java/com/aisip/OnO/backend/folder/controller/FolderController.java +++ b/src/main/java/com/aisip/OnO/backend/folder/controller/FolderController.java @@ -35,7 +35,7 @@ public CommonResponse getRootFolder() { public CommonResponse getFolder(@PathVariable("folderId") Long folderId) { Long userId = (Long) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); - return CommonResponse.success(folderService.findFolder(folderId)); + return CommonResponse.success(folderService.findFolder(folderId, userId)); } // ✅ V2 API: 커서 기반 하위 폴더 조회 (무한 스크롤) @@ -47,7 +47,7 @@ public CommonResponse> getSubFold Long userId = (Long) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); log.info("userId: {} get subfolders for folderId: {} with cursor: {}, size: {}", userId, folderId, cursor, size); - return CommonResponse.success(folderService.findSubFoldersWithCursor(folderId, cursor, size)); + return CommonResponse.success(folderService.findSubFoldersWithCursor(folderId, userId, cursor, size)); } // ✅ 모든 폴더 조회 @@ -98,7 +98,8 @@ public CommonResponse updateFolderInfo(@RequestBody FolderRegisterDto fo // ✅ 폴더 삭제 기능 @DeleteMapping("") public CommonResponse deleteFoldersWithProblems(@RequestBody FolderDeleteRequestDto folderDeleteRequestDto) { - folderService.deleteFoldersWithProblems(folderDeleteRequestDto.deleteFolderIdList()); + Long userId = (Long) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + folderService.deleteFoldersWithProblems(userId, folderDeleteRequestDto.deleteFolderIdList()); return CommonResponse.success("폴더가 성공적으로 삭제되었습니다."); } @@ -110,4 +111,4 @@ public CommonResponse deleteAllFoldersWithProblems() { return CommonResponse.success("폴더가 성공적으로 삭제되었습니다."); } -} \ No newline at end of file +} diff --git a/src/main/java/com/aisip/OnO/backend/folder/service/FolderService.java b/src/main/java/com/aisip/OnO/backend/folder/service/FolderService.java index 4f08475e..992ec5e7 100644 --- a/src/main/java/com/aisip/OnO/backend/folder/service/FolderService.java +++ b/src/main/java/com/aisip/OnO/backend/folder/service/FolderService.java @@ -59,7 +59,7 @@ public FolderResponseDto findRootFolder(Long userId) { } @Transactional(readOnly = true) - public FolderResponseDto findFolder(Long folderId) { + public FolderResponseDto findFolderForAdmin(Long folderId) { Folder folder = folderRepository.findFolderWithDetailsByFolderId(folderId) .orElseThrow(() -> new ApplicationException(FolderErrorCase.FOLDER_NOT_FOUND)); @@ -68,11 +68,26 @@ public FolderResponseDto findFolder(Long folderId) { } @Transactional(readOnly = true) - public Folder findFolderEntity(Long folderId) { + public FolderResponseDto findFolder(Long folderId, Long userId) { + Folder folder = findFolderWithDetailsOwnedByUser(folderId, userId); + + List problemIdList = folderRepository.findProblemIdsByFolder(folder.getId()); + return FolderResponseDto.from(folder, problemIdList); + } + + @Transactional(readOnly = true) + private Folder findFolderEntity(Long folderId) { return folderRepository.findById(folderId) .orElseThrow(() -> new ApplicationException(FolderErrorCase.FOLDER_NOT_FOUND)); } + @Transactional(readOnly = true) + public Folder findFolderEntity(Long folderId, Long userId) { + Folder folder = findFolderEntity(folderId); + validateFolderOwner(folder, userId); + return folder; + } + @Transactional(readOnly = true) public List findAllUserFolderThumbnails(Long userId) { List folderList = folderRepository.findAllByUserId(userId); @@ -104,7 +119,7 @@ private void createDefaultSubFolder(Folder rootFolder, Long userId) { public Long createFolder(FolderRegisterDto folderRegisterDto, Long userId) { Folder folder = Folder.from(folderRegisterDto, userId); - Folder parentFolder = findFolderEntity(folderRegisterDto.parentFolderId()); + Folder parentFolder = findFolderEntity(folderRegisterDto.parentFolderId(), userId); folder.updateParentFolder(parentFolder); folderRepository.save(folder); @@ -114,11 +129,11 @@ public Long createFolder(FolderRegisterDto folderRegisterDto, Long userId) { } public void updateFolder(FolderRegisterDto folderRegisterDto, Long userId) { - Folder folder = findFolderEntity(folderRegisterDto.folderId()); + Folder folder = findFolderEntity(folderRegisterDto.folderId(), userId); folder.updateFolderInfo(folderRegisterDto); if (folderRegisterDto.parentFolderId() != null && folder.getParentFolder() != null) { - Folder newParentFolder = findFolderEntity(folderRegisterDto.parentFolderId()); + Folder newParentFolder = findFolderEntity(folderRegisterDto.parentFolderId(), userId); folder.updateParentFolder(newParentFolder); } @@ -126,11 +141,11 @@ public void updateFolder(FolderRegisterDto folderRegisterDto, Long userId) { log.info("userId : {} update folder id: {}", userId, folder.getId()); } - public void deleteFoldersWithProblems(List folderIds) { + public void deleteFoldersWithProblems(Long userId, List folderIds) { // 삭제할 모든 폴더의 ID 조회 (하위 폴더 포함) - Set allFolderIds = getAllFolderIdsIncludingSubFolders(folderIds); + Set allFolderIds = getAllFolderIdsIncludingSubFolders(userId, folderIds); - problemService.deleteAllByFolderIds(allFolderIds); + problemService.deleteAllByFolderIds(userId, allFolderIds); deleteAllByFolderIds(allFolderIds); } @@ -142,12 +157,11 @@ public void deleteAllUserFoldersWithProblems(Long userId) { deleteAllUserFolders(userId); } - public Set getAllFolderIdsIncludingSubFolders(List folderIds) { + public Set getAllFolderIdsIncludingSubFolders(Long userId, List folderIds) { Set allFolderIds = new HashSet<>(); for (Long folderId : folderIds) { - Folder folder = folderRepository.findById(folderId) - .orElseThrow(() -> new ApplicationException(FolderErrorCase.FOLDER_NOT_FOUND)); + Folder folder = findFolderEntity(folderId, userId); if (folder.getParentFolder() == null) { throw new ApplicationException(FolderErrorCase.ROOT_FOLDER_CANNOT_REMOVE); @@ -170,7 +184,7 @@ private Set getSubFolderIdsRecursive(Folder folder) { return subFolderIds; } - public void deleteAllByFolderIds(Collection folderIds) { + private void deleteAllByFolderIds(Collection folderIds) { List foldersToDelete = folderRepository.findAllById(folderIds); folderRepository.deleteAll(foldersToDelete); } @@ -190,7 +204,8 @@ public void deleteAllUserFolders(Long userId) { * @return 커서 기반 페이징 응답 */ @Transactional(readOnly = true) - public CursorPageResponse findSubFoldersWithCursor(Long folderId, Long cursor, int size) { + public CursorPageResponse findSubFoldersWithCursor(Long folderId, Long userId, Long cursor, int size) { + findFolderEntity(folderId, userId); List folders = folderRepository.findSubFoldersWithCursor(folderId, cursor, size); boolean hasNext = folders.size() > size; @@ -205,6 +220,19 @@ public CursorPageResponse findSubFoldersWithCursor(L return CursorPageResponse.of(dtoList, nextCursor, hasNext, size); } + private Folder findFolderWithDetailsOwnedByUser(Long folderId, Long userId) { + Folder folder = folderRepository.findFolderWithDetailsByFolderId(folderId) + .orElseThrow(() -> new ApplicationException(FolderErrorCase.FOLDER_NOT_FOUND)); + validateFolderOwner(folder, userId); + return folder; + } + + private void validateFolderOwner(Folder folder, Long userId) { + if (!Objects.equals(folder.getUserId(), userId)) { + throw new ApplicationException(FolderErrorCase.FOLDER_USER_UNMATCHED); + } + } + /** * V2 API: 커서 기반 유저의 모든 폴더 썸네일 조회 * @param userId 유저 ID diff --git a/src/main/java/com/aisip/OnO/backend/practicenote/controller/PracticeNoteController.java b/src/main/java/com/aisip/OnO/backend/practicenote/controller/PracticeNoteController.java index fc671ae0..8f0027a8 100644 --- a/src/main/java/com/aisip/OnO/backend/practicenote/controller/PracticeNoteController.java +++ b/src/main/java/com/aisip/OnO/backend/practicenote/controller/PracticeNoteController.java @@ -26,7 +26,7 @@ public CommonResponse getPracticeDetail(@PathVari Long userId = (Long) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); log.info("userId: {} get problem practice for practice id: {}", userId, practiceId); - return CommonResponse.success(practiceNoteService.findPracticeNoteDetail(practiceId)); + return CommonResponse.success(practiceNoteService.findPracticeNoteDetail(practiceId, userId)); } // ✅ 사용자의 모든 복습 리스트 썸네일 조회 @@ -94,7 +94,7 @@ public CommonResponse updatePractice(@RequestBody PracticeNoteUpdateDto @DeleteMapping("") public CommonResponse deletePractices(@RequestBody PracticeNoteDeleteRequestDto practiceNoteDeleteRequestDto) { Long userId = (Long) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); - practiceNoteService.deletePractices(practiceNoteDeleteRequestDto.deletePracticeIdList()); + practiceNoteService.deletePractices(userId, practiceNoteDeleteRequestDto.deletePracticeIdList()); return CommonResponse.success("선택한 복습 노트가 삭제되었습니다."); } diff --git a/src/main/java/com/aisip/OnO/backend/practicenote/exception/PracticeNoteErrorCase.java b/src/main/java/com/aisip/OnO/backend/practicenote/exception/PracticeNoteErrorCase.java index 46a8a65b..9fe0d3ce 100644 --- a/src/main/java/com/aisip/OnO/backend/practicenote/exception/PracticeNoteErrorCase.java +++ b/src/main/java/com/aisip/OnO/backend/practicenote/exception/PracticeNoteErrorCase.java @@ -8,7 +8,9 @@ @RequiredArgsConstructor public enum PracticeNoteErrorCase implements ErrorCase { - PRACTICE_NOTE_NOT_FOUND(404, 6001, "복습 노트를 찾을 수 없습니다."); + PRACTICE_NOTE_NOT_FOUND(404, 6001, "복습 노트를 찾을 수 없습니다."), + + PRACTICE_NOTE_USER_UNMATCHED(403, 6002, "복습 노트를 소유한 유저가 아닙니다."); private final Integer httpStatusCode; private final Integer errorCode; diff --git a/src/main/java/com/aisip/OnO/backend/practicenote/service/PracticeNoteService.java b/src/main/java/com/aisip/OnO/backend/practicenote/service/PracticeNoteService.java index 62683f5c..a84f5abf 100644 --- a/src/main/java/com/aisip/OnO/backend/practicenote/service/PracticeNoteService.java +++ b/src/main/java/com/aisip/OnO/backend/practicenote/service/PracticeNoteService.java @@ -33,6 +33,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.function.Function; import java.util.stream.Collectors; @@ -56,10 +57,13 @@ public class PracticeNoteService { private final UserRepository userRepository; - private PracticeNote getPracticeEntity(Long practiceId){ + private PracticeNote getPracticeEntity(Long practiceId, Long userId){ - return practiceNoteRepository.findById(practiceId) + PracticeNote practiceNote = practiceNoteRepository.findById(practiceId) .orElseThrow(() -> new ApplicationException(PracticeNoteErrorCase.PRACTICE_NOTE_NOT_FOUND)); + + validatePracticeOwner(practiceNote, userId); + return practiceNote; } public Long registerPractice(PracticeNoteRegisterDto practiceNoteRegisterDto, Long userId) { @@ -69,7 +73,7 @@ public Long registerPractice(PracticeNoteRegisterDto practiceNoteRegisterDto, Lo if (practiceNoteRegisterDto.problemIdList() != null) { practiceNoteRegisterDto.problemIdList().forEach(problemId -> - addProblemToPractice(practiceNote, problemId)); + addProblemToPractice(practiceNote, problemId, userId)); } if(practiceNoteRegisterDto.practiceNotification() != null) { @@ -94,10 +98,11 @@ public void registerPracticeNotification(Long userId, Long practiceId, String pr } @Transactional(readOnly = true) - public PracticeNoteDetailResponseDto findPracticeNoteDetail(Long practiceId){ + public PracticeNoteDetailResponseDto findPracticeNoteDetail(Long practiceId, Long userId){ log.info("find practiceId: {}", practiceId); PracticeNote practiceNote = practiceNoteRepository.findPracticeNoteWithDetails(practiceId) .orElseThrow(() -> new ApplicationException(PracticeNoteErrorCase.PRACTICE_NOTE_NOT_FOUND)); + validatePracticeOwner(practiceNote, userId); List problemIdList = practiceNoteRepository.findProblemIdListByPracticeNoteId(practiceId); @@ -132,7 +137,7 @@ public List findAllPracticesByUser(Long userId){ } public void addPracticeNoteCount(Long userId, Long practiceId) { - PracticeNote practiceNote = getPracticeEntity(practiceId); + PracticeNote practiceNote = getPracticeEntity(practiceId, userId); practiceNote.updatePracticeNoteCount(); // 복습노트 사용 미션 등록 @@ -143,7 +148,7 @@ public void addPracticeNoteCount(Long userId, Long practiceId) { public void updatePracticeInfo(Long userId, PracticeNoteUpdateDto practiceNoteUpdateDto) { Long practiceId = practiceNoteUpdateDto.practiceNoteId(); - PracticeNote practiceNote = getPracticeEntity(practiceId); + PracticeNote practiceNote = getPracticeEntity(practiceId, userId); practiceNote.updateTitle(practiceNoteUpdateDto.practiceTitle()); @@ -151,7 +156,7 @@ public void updatePracticeInfo(Long userId, PracticeNoteUpdateDto practiceNoteUp if (!practiceNoteUpdateDto.addProblemIdList().isEmpty()) { practiceNoteUpdateDto.addProblemIdList().forEach(problemId -> { - addProblemToPractice(practiceNote, problemId); + addProblemToPractice(practiceNote, problemId, userId); }); } @@ -176,7 +181,12 @@ public void updatePracticeNotification(Long userId, Long practiceId, String prac } - public void deletePractice(Long practiceId) { + public void deletePractice(Long practiceId, Long userId) { + getPracticeEntity(practiceId, userId); + deletePracticeWithoutOwnerCheck(practiceId); + } + + private void deletePracticeWithoutOwnerCheck(Long practiceId) { List problemPracticeNoteMappingList = problemPracticeNoteMappingRepository.findAllByPracticeNoteId(practiceId); problemPracticeNoteMappingList.forEach(ProblemPracticeNoteMapping::removeMappingFromProblemAndPractice); @@ -184,24 +194,27 @@ public void deletePractice(Long practiceId) { log.info("practiceId: {} has deleted", practiceId); } - public void deletePractices(List practiceIdList) { - practiceIdList.forEach(this::deletePractice); + public void deletePractices(Long userId, List practiceIdList) { + practiceIdList.forEach(practiceId -> deletePractice(practiceId, userId)); } public void deleteAllPracticesByUser(Long userId) { List practiceIdList = practiceNoteRepository.findAllPracticeIdsByUserId(userId); - deletePractices(practiceIdList); + practiceIdList.forEach(this::deletePracticeWithoutOwnerCheck); log.info("userId: {} has delete all practices", userId); } - private void addProblemToPractice(PracticeNote practiceNote, Long problemId) { + private void addProblemToPractice(PracticeNote practiceNote, Long problemId, Long userId) { boolean exists = practiceNoteRepository.checkProblemAlreadyMatchingWithPractice(practiceNote.getId(), problemId); if (!exists) { Problem problem = problemRepository.findById(problemId) .orElseThrow(() -> new ApplicationException(ProblemErrorCase.PROBLEM_NOT_FOUND)); + if (!Objects.equals(problem.getUserId(), userId)) { + throw new ApplicationException(ProblemErrorCase.PROBLEM_USER_UNMATCHED); + } ProblemPracticeNoteMapping problemPracticeNoteMapping = ProblemPracticeNoteMapping.from(); problemPracticeNoteMapping.addMappingToProblemAndPractice(problem, practiceNote); @@ -221,6 +234,12 @@ public void deleteProblemsFromAllPractice(List deleteProblemIdList) { practiceNoteRepository.deleteProblemsFromAllPractice(deleteProblemIdList); } + private void validatePracticeOwner(PracticeNote practiceNote, Long userId) { + if (!Objects.equals(practiceNote.getUserId(), userId)) { + throw new ApplicationException(PracticeNoteErrorCase.PRACTICE_NOTE_USER_UNMATCHED); + } + } + /** * V2 API: 커서 기반 복습노트 썸네일 조회 * @param userId 유저 ID diff --git a/src/main/java/com/aisip/OnO/backend/problem/controller/ProblemController.java b/src/main/java/com/aisip/OnO/backend/problem/controller/ProblemController.java index 07cbb456..897d213d 100644 --- a/src/main/java/com/aisip/OnO/backend/problem/controller/ProblemController.java +++ b/src/main/java/com/aisip/OnO/backend/problem/controller/ProblemController.java @@ -40,7 +40,7 @@ public void initBinder(WebDataBinder binder) { @GetMapping("/{problemId}") public CommonResponse getProblem(@PathVariable("problemId") Long problemId) { Long userId = (Long) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); - ProblemResponseDto problemResponseDto = problemService.findProblem(problemId); + ProblemResponseDto problemResponseDto = problemService.findProblem(problemId, userId); return CommonResponse.success(problemResponseDto); } @@ -58,7 +58,7 @@ public CommonResponse> getProblemsByUserId() { public CommonResponse> getProblemsByUserId(@PathVariable("folderId") Long folderId) { Long userId = (Long) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); - return CommonResponse.success(problemService.findFolderProblemList(folderId)); + return CommonResponse.success(problemService.findFolderProblemList(folderId, userId)); } // ✅ V2 API: 커서 기반 폴더의 문제 조회 (무한 스크롤) @@ -70,7 +70,7 @@ public CommonResponse> getProblemsWithCur Long userId = (Long) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); log.info("userId: {} get problems for folderId: {} with cursor: {}, size: {}", userId, folderId, cursor, size); - return CommonResponse.success(problemService.findProblemsByFolderWithCursor(folderId, cursor, size)); + return CommonResponse.success(problemService.findProblemsByFolderWithCursor(folderId, userId, cursor, size)); } // ✅ V2 API: 커서 기반 태그의 문제 조회 (무한 스크롤) @@ -119,7 +119,7 @@ public CommonResponse getProblemAnalysis(@PathVariab @PostMapping("/{problemId}/analysis") public CommonResponse requestProblemAnalysis(@PathVariable("problemId") Long problemId) { Long userId = (Long) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); - problemService.analysisProblem(problemId); + problemService.analysisProblem(problemId, userId); return CommonResponse.success("문제 분석 요청이 접수되었습니다."); } @@ -153,7 +153,7 @@ public CommonResponse uploadProblemImages( ) { Long userId = (Long) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); problemService.uploadProblemImages(problemId, userId, problemImages, problemImageTypes); - problemService.analysisProblem(problemId); + problemService.analysisProblem(problemId, userId); return CommonResponse.success("이미지 업로드가 시작되었습니다."); } @@ -164,7 +164,7 @@ public CommonResponse updateProblemAnalysisStatus( @PathVariable("problemId") Long problemId ) { Long userId = (Long) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); - analysisService.updateToNoImage(problemId); + problemService.updateProblemAnalysisToNoImage(problemId, userId); return CommonResponse.success("이미지 업로드가 시작되었습니다."); } @@ -204,7 +204,8 @@ public CommonResponse updateProblemTags( public CommonResponse deleteProblems( @RequestBody ProblemDeleteRequestDto problemDeleteRequestDto ) { - problemService.deleteProblemList(problemDeleteRequestDto.deleteProblemIdList()); + Long userId = (Long) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + problemService.deleteProblemList(userId, problemDeleteRequestDto.deleteProblemIdList()); return CommonResponse.success("문제 삭제가 완료되었습니다."); } @@ -221,7 +222,7 @@ public CommonResponse deleteUserProblems( @DeleteMapping("/imageData") public CommonResponse deleteProblemImageData(@RequestParam("imageUrl") String imageUrl) { Long userId = (Long) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); - problemService.deleteProblemImageData(imageUrl); + problemService.deleteProblemImageData(imageUrl, userId); return CommonResponse.success("문제 이미지 데이터 삭제가 완료되었습니다."); } diff --git a/src/main/java/com/aisip/OnO/backend/problem/repository/ProblemImageDataRepository.java b/src/main/java/com/aisip/OnO/backend/problem/repository/ProblemImageDataRepository.java index 459dc6af..50d5c8ac 100644 --- a/src/main/java/com/aisip/OnO/backend/problem/repository/ProblemImageDataRepository.java +++ b/src/main/java/com/aisip/OnO/backend/problem/repository/ProblemImageDataRepository.java @@ -4,11 +4,14 @@ import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; +import java.util.Optional; public interface ProblemImageDataRepository extends JpaRepository { List findAllByProblemId(Long problemId); + Optional findByImageUrl(String imageUrl); + void deleteAllByProblemId(Long problemId); void deleteByImageUrl(String imageUrl); diff --git a/src/main/java/com/aisip/OnO/backend/problem/service/ProblemService.java b/src/main/java/com/aisip/OnO/backend/problem/service/ProblemService.java index b1bfaabc..a20689a7 100644 --- a/src/main/java/com/aisip/OnO/backend/problem/service/ProblemService.java +++ b/src/main/java/com/aisip/OnO/backend/problem/service/ProblemService.java @@ -81,13 +81,20 @@ public class ProblemService { private final ProblemTagMappingRepository problemTagMappingRepository; @Transactional(readOnly = true) - public ProblemResponseDto findProblem(Long problemId) { + public ProblemResponseDto findProblemForAdmin(Long problemId) { Problem problem = problemRepository.findProblemWithImageData(problemId) .orElseThrow(() -> new ApplicationException(ProblemErrorCase.PROBLEM_NOT_FOUND)); return toProblemResponseDto(problem); } + @Transactional(readOnly = true) + public ProblemResponseDto findProblem(Long problemId, Long userId) { + Problem problem = findProblemEntityWithImageData(problemId, userId); + + return toProblemResponseDto(problem); + } + @Transactional(readOnly = true) public Problem findProblemEntity(Long problemId, Long userId) { Problem problem = problemRepository.findById(problemId) @@ -120,7 +127,9 @@ public List findUserProblems(Long userId) { } @Transactional(readOnly = true) - public List findFolderProblemList(Long folderId) { + public List findFolderProblemList(Long folderId, Long userId) { + validateFolderOwner(folderId, userId); + return toProblemResponseDtos(problemRepository.findAllByFolderId(folderId)); } @@ -185,6 +194,7 @@ public Long registerProblem(ProblemRegisterDto problemRegisterDto, Long userId) Folder folder = folderRepository.findById(problemRegisterDto.folderId()) .orElseThrow(() -> new ApplicationException(FolderErrorCase.FOLDER_NOT_FOUND)); + validateFolderOwner(folder, userId); Problem problem = Problem.from(problemRegisterDto, userId); problem.updateFolder(folder); @@ -262,6 +272,10 @@ private Folder resolveRegisterFolder(Long folderId, Long userId) { } return folderRepository.findById(folderId) + .map(folder -> { + validateFolderOwner(folder, userId); + return folder; + }) .orElseThrow(() -> new ApplicationException(FolderErrorCase.FOLDER_NOT_FOUND)); } @@ -313,7 +327,14 @@ public void uploadProblemImages(Long problemId, Long userId, List * - 이미지 없음: NOT_STARTED → NO_IMAGE */ @Transactional - public void analysisProblem(Long problemId) { + public void analysisProblem(Long problemId, Long userId) { + findProblemEntity(problemId, userId); + + analysisProblemWithoutOwnerCheck(problemId); + } + + @Transactional + private void analysisProblemWithoutOwnerCheck(Long problemId) { // 이미 분석이 완료된 문제는 재요청하지 않음 if (problemAnalysisRepository.findByProblemId(problemId) .map(analysis -> analysis.getStatus() == AnalysisStatus.COMPLETED) @@ -341,6 +362,12 @@ public void analysisProblem(Long problemId) { } } + @Transactional + public void updateProblemAnalysisToNoImage(Long problemId, Long userId) { + findProblemEntity(problemId, userId); + analysisService.updateToNoImage(problemId); + } + @Transactional public void updateProblemInfo(ProblemRegisterDto problemRegisterDto, Long userId) { @@ -359,6 +386,7 @@ public void updateProblemFolder(ProblemRegisterDto problemRegisterDto, Long user if (problemRegisterDto.folderId() != null) { Folder folder = folderRepository.findById(problemRegisterDto.folderId()) .orElseThrow(() -> new ApplicationException(FolderErrorCase.FOLDER_NOT_FOUND)); + validateFolderOwner(folder, userId); problem.updateFolder(folder); @@ -472,7 +500,7 @@ private void syncProblemTags(Problem problem, Long userId, List requestedT * - PracticeNote 매핑 삭제: 동기 (데이터 정합성) */ @Transactional - public void deleteProblem(Long problemId) { + private void deleteProblemWithoutOwnerCheck(Long problemId) { // 1. 이미지 데이터 조회 List imageDataList = problemImageDataRepository.findAllByProblemId(problemId); // 1-1. 태그 매핑 조회 @@ -508,36 +536,51 @@ public void deleteProblem(Long problemId) { } @Transactional - public void deleteProblemImageData(String imageUrl) { + public void deleteProblem(Long problemId, Long userId) { + findProblemEntity(problemId, userId); + deleteProblemWithoutOwnerCheck(problemId); + } + + @Transactional + public void deleteProblemImageData(String imageUrl, Long userId) { + ProblemImageData imageData = problemImageDataRepository.findByImageUrl(imageUrl) + .orElseThrow(() -> new ApplicationException(ProblemErrorCase.PROBLEM_NOT_FOUND)); + if (!Objects.equals(imageData.getProblem().getUserId(), userId)) { + throw new ApplicationException(ProblemErrorCase.PROBLEM_USER_UNMATCHED); + } + fileUploadService.deleteImageFileFromS3(imageUrl); problemImageDataRepository.deleteByImageUrl(imageUrl); } @Transactional - public void deleteProblemList(List problemIdList) { - problemIdList.forEach(this::deleteProblem); + public void deleteProblemList(Long userId, List problemIdList) { + problemIdList.forEach(problemId -> deleteProblem(problemId, userId)); } @Transactional - public void deleteFolderProblems(Long folderId) { + private void deleteFolderProblems(Long folderId) { problemRepository.findAllByFolderId(folderId) .forEach(problem -> { - deleteProblem(problem.getId()); + deleteProblemWithoutOwnerCheck(problem.getId()); }); log.info("problem in folderId: {} has deleted", folderId); } @Transactional - public void deleteAllByFolderIds(Collection folderIds) { - folderIds.forEach(this::deleteFolderProblems); + public void deleteAllByFolderIds(Long userId, Collection folderIds) { + folderIds.forEach(folderId -> { + validateFolderOwner(folderId, userId); + deleteFolderProblems(folderId); + }); } @Transactional public void deleteAllUserProblems(Long userId) { problemRepository.findAllByUserId(userId) .forEach(problem -> { - deleteProblem(problem.getId()); + deleteProblemWithoutOwnerCheck(problem.getId()); }); log.info("userId: {} delete all user problems", userId); @@ -551,7 +594,8 @@ public void deleteAllUserProblems(Long userId) { * @return 커서 기반 페이징 응답 */ @Transactional(readOnly = true) - public CursorPageResponse findProblemsByFolderWithCursor(Long folderId, Long cursor, int size) { + public CursorPageResponse findProblemsByFolderWithCursor(Long folderId, Long userId, Long cursor, int size) { + validateFolderOwner(folderId, userId); List problems = problemRepository.findProblemsByFolderWithCursor(folderId, cursor, size); boolean hasNext = problems.size() > size; @@ -657,6 +701,18 @@ private List toProblemResponseDtos(List problems) { .collect(Collectors.toList()); } + private void validateFolderOwner(Long folderId, Long userId) { + Folder folder = folderRepository.findById(folderId) + .orElseThrow(() -> new ApplicationException(FolderErrorCase.FOLDER_NOT_FOUND)); + validateFolderOwner(folder, userId); + } + + private void validateFolderOwner(Folder folder, Long userId) { + if (!Objects.equals(folder.getUserId(), userId)) { + throw new ApplicationException(FolderErrorCase.FOLDER_USER_UNMATCHED); + } + } + @Transactional(readOnly = true) public ReviewDueResponseDto getReviewDueProblems(Long userId) { LocalDate today = LocalDate.now(java.time.ZoneId.of("Asia/Seoul")); diff --git a/src/main/java/com/aisip/OnO/backend/problemsolve/repository/ProblemSolveImageDataRepository.java b/src/main/java/com/aisip/OnO/backend/problemsolve/repository/ProblemSolveImageDataRepository.java index d3e537a3..db98d7b5 100644 --- a/src/main/java/com/aisip/OnO/backend/problemsolve/repository/ProblemSolveImageDataRepository.java +++ b/src/main/java/com/aisip/OnO/backend/problemsolve/repository/ProblemSolveImageDataRepository.java @@ -4,10 +4,13 @@ import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; +import java.util.Optional; public interface ProblemSolveImageDataRepository extends JpaRepository { List findAllByProblemSolveId(Long problemSolveId); + Optional findByImageUrl(String imageUrl); + void deleteByImageUrl(String imageUrl); -} \ No newline at end of file +} diff --git a/src/main/java/com/aisip/OnO/backend/util/fileupload/controller/FileUploadController.java b/src/main/java/com/aisip/OnO/backend/util/fileupload/controller/FileUploadController.java index 67281d86..80cba693 100644 --- a/src/main/java/com/aisip/OnO/backend/util/fileupload/controller/FileUploadController.java +++ b/src/main/java/com/aisip/OnO/backend/util/fileupload/controller/FileUploadController.java @@ -4,7 +4,7 @@ import com.aisip.OnO.backend.util.fileupload.service.FileUploadService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; @@ -42,7 +42,8 @@ public CommonResponse> uploadMultipleImageFiles( public CommonResponse deleteImageFile( @RequestParam("imageUrl") String imageUrl ) { - fileUploadService.deleteImageFileFromS3(imageUrl); + Long userId = (Long) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + fileUploadService.deleteUserImageFile(imageUrl, userId); return CommonResponse.success("이미지 삭제가 완료되었습니다."); } diff --git a/src/main/java/com/aisip/OnO/backend/util/fileupload/exception/FileUploadErrorCase.java b/src/main/java/com/aisip/OnO/backend/util/fileupload/exception/FileUploadErrorCase.java index 94cb9fbd..d0857280 100644 --- a/src/main/java/com/aisip/OnO/backend/util/fileupload/exception/FileUploadErrorCase.java +++ b/src/main/java/com/aisip/OnO/backend/util/fileupload/exception/FileUploadErrorCase.java @@ -8,7 +8,9 @@ @RequiredArgsConstructor public enum FileUploadErrorCase implements ErrorCase { - FILE_UPLOAD_FAILED(400, 2001, "파일 업로드 중 문제가 발생했습니다."); + FILE_UPLOAD_FAILED(400, 2001, "파일 업로드 중 문제가 발생했습니다."), + + FILE_NOT_FOUND(404, 2002, "파일을 찾을 수 없습니다."); private final Integer httpStatusCode; private final Integer errorCode; diff --git a/src/main/java/com/aisip/OnO/backend/util/fileupload/service/FileUploadService.java b/src/main/java/com/aisip/OnO/backend/util/fileupload/service/FileUploadService.java index 2f944213..8229f6f9 100644 --- a/src/main/java/com/aisip/OnO/backend/util/fileupload/service/FileUploadService.java +++ b/src/main/java/com/aisip/OnO/backend/util/fileupload/service/FileUploadService.java @@ -1,6 +1,12 @@ package com.aisip.OnO.backend.util.fileupload.service; import com.aisip.OnO.backend.common.exception.ApplicationException; +import com.aisip.OnO.backend.problem.entity.ProblemImageData; +import com.aisip.OnO.backend.problem.exception.ProblemErrorCase; +import com.aisip.OnO.backend.problem.repository.ProblemImageDataRepository; +import com.aisip.OnO.backend.problemsolve.entity.ProblemSolveImageData; +import com.aisip.OnO.backend.problemsolve.exception.ProblemSolveErrorCase; +import com.aisip.OnO.backend.problemsolve.repository.ProblemSolveImageDataRepository; import com.aisip.OnO.backend.util.fileupload.exception.FileUploadErrorCase; import com.amazonaws.services.s3.AmazonS3Client; import com.amazonaws.services.s3.model.DeleteObjectRequest; @@ -8,10 +14,12 @@ import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Timer; import java.io.IOException; +import java.util.Objects; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; import java.time.LocalDate; @@ -24,6 +32,9 @@ public class FileUploadService { private final AmazonS3Client amazonS3Client; private final MeterRegistry meterRegistry; + private final ProblemImageDataRepository problemImageDataRepository; + private final ProblemSolveImageDataRepository problemSolveImageDataRepository; + @Value("${cloud.aws.s3.bucket}") private String bucket; @@ -65,6 +76,36 @@ public void deleteImageFileFromS3(String imageUrl) { } } + @Transactional + public void deleteUserImageFile(String imageUrl, Long userId) { + problemImageDataRepository.findByImageUrl(imageUrl) + .ifPresentOrElse( + imageData -> deleteProblemImageData(imageData, imageUrl, userId), + () -> deleteProblemSolveImageData(imageUrl, userId) + ); + } + + private void deleteProblemImageData(ProblemImageData imageData, String imageUrl, Long userId) { + if (!Objects.equals(imageData.getProblem().getUserId(), userId)) { + throw new ApplicationException(ProblemErrorCase.PROBLEM_USER_UNMATCHED); + } + + deleteImageFileFromS3(imageUrl); + problemImageDataRepository.deleteByImageUrl(imageUrl); + } + + private void deleteProblemSolveImageData(String imageUrl, Long userId) { + ProblemSolveImageData imageData = problemSolveImageDataRepository.findByImageUrl(imageUrl) + .orElseThrow(() -> new ApplicationException(FileUploadErrorCase.FILE_NOT_FOUND)); + + if (!Objects.equals(imageData.getProblemSolve().getUserId(), userId)) { + throw new ApplicationException(ProblemSolveErrorCase.PROBLEM_SOLVE_USER_UNMATCHED); + } + + deleteImageFileFromS3(imageUrl); + problemSolveImageDataRepository.deleteByImageUrl(imageUrl); + } + private String createFileName(MultipartFile file) { String originalFilename = file.getOriginalFilename(); String extension = originalFilename.substring(originalFilename.lastIndexOf(".")); // 확장자 추출 diff --git a/src/test/java/com/aisip/OnO/backend/folder/service/FolderServiceTest.java b/src/test/java/com/aisip/OnO/backend/folder/service/FolderServiceTest.java index 2e0e9e0e..66e0acc0 100644 --- a/src/test/java/com/aisip/OnO/backend/folder/service/FolderServiceTest.java +++ b/src/test/java/com/aisip/OnO/backend/folder/service/FolderServiceTest.java @@ -170,7 +170,7 @@ void findFolder() { Long folderId = folderList.get(1).getId(); //when - FolderResponseDto folderResponseDto = folderService.findFolder(folderId); + FolderResponseDto folderResponseDto = folderService.findFolder(folderId, userId); //then assertThat(folderResponseDto.folderId()).isEqualTo(folderList.get(1).getId()); @@ -182,6 +182,19 @@ void findFolder() { assertThat(folderResponseDto.problemIdList().get(1)).isEqualTo(problemList.get(3).problemId()); } + @Test + @DisplayName("다른 유저의 폴더 조회 시 예외") + void findFolder_OtherUserFolder() { + Folder otherUserFolder = folderRepository.save(Folder.from( + new FolderRegisterDto("other folder", null, null), + 2L + )); + + assertThatThrownBy(() -> folderService.findFolder(otherUserFolder.getId(), userId)) + .isInstanceOf(ApplicationException.class) + .hasMessageContaining(FolderErrorCase.FOLDER_USER_UNMATCHED.getMessage()); + } + @Test @DisplayName("folderId를 사용해 특정 폴더 엔티티 조회하기 테스트") void findFolderEntity() { @@ -189,7 +202,7 @@ void findFolderEntity() { Long folderId = folderList.get(0).getId(); //when - Folder folder = folderService.findFolderEntity(folderId); + Folder folder = folderService.findFolderEntity(folderId, userId); //then assertThat(folder.getId()).isEqualTo(folderList.get(0).getId()); @@ -316,6 +329,24 @@ void createFolder_FolderNotExists() { .hasMessageContaining(FolderErrorCase.FOLDER_NOT_FOUND.getMessage()); } + @Test + @DisplayName("다른 유저의 부모 폴더에 폴더 생성 시 예외") + void createFolder_OtherUserParentFolder() { + Folder otherUserParentFolder = folderRepository.save(Folder.from( + new FolderRegisterDto("other parent", null, null), + 2L + )); + FolderRegisterDto folderRegisterDto = new FolderRegisterDto( + "new folder", + null, + otherUserParentFolder.getId() + ); + + assertThatThrownBy(() -> folderService.createFolder(folderRegisterDto, userId)) + .isInstanceOf(ApplicationException.class) + .hasMessageContaining(FolderErrorCase.FOLDER_USER_UNMATCHED.getMessage()); + } + @Test @DisplayName("폴더 수정 테스트 - 폴더 이름 수정") void updateFolder_FolderName() { @@ -396,7 +427,7 @@ void deleteFolders_rootFolder() { doNothing().when(fileUploadService).deleteImageFileFromS3(anyString()); // when & then - assertThatThrownBy(() -> folderService.deleteFoldersWithProblems(folderIdList)) + assertThatThrownBy(() -> folderService.deleteFoldersWithProblems(userId, folderIdList)) .isInstanceOf(ApplicationException.class) .hasMessageContaining(FolderErrorCase.ROOT_FOLDER_CANNOT_REMOVE.getMessage()); @@ -411,7 +442,7 @@ void deleteFolders_AllParentFolder() { doNothing().when(fileUploadService).deleteImageFileFromS3(anyString()); // when - folderService.deleteFoldersWithProblems(folderIdList); + folderService.deleteFoldersWithProblems(userId, folderIdList); // then assertThat(folderRepository.findAllByUserId(userId).size()).isEqualTo(1); @@ -425,7 +456,7 @@ void deleteFolders_InternalFolder() { doNothing().when(fileUploadService).deleteImageFileFromS3(anyString()); // when - folderService.deleteFoldersWithProblems(folderIdList); + folderService.deleteFoldersWithProblems(userId, folderIdList); // then assertThat(folderRepository.findAllByUserId(userId).size()).isEqualTo(3); diff --git a/src/test/java/com/aisip/OnO/backend/practicenote/service/PracticeNoteServiceTest.java b/src/test/java/com/aisip/OnO/backend/practicenote/service/PracticeNoteServiceTest.java index 029fda17..d704bdfa 100644 --- a/src/test/java/com/aisip/OnO/backend/practicenote/service/PracticeNoteServiceTest.java +++ b/src/test/java/com/aisip/OnO/backend/practicenote/service/PracticeNoteServiceTest.java @@ -1,5 +1,6 @@ package com.aisip.OnO.backend.practicenote.service; +import com.aisip.OnO.backend.common.exception.ApplicationException; import com.aisip.OnO.backend.folder.dto.FolderRegisterDto; import com.aisip.OnO.backend.folder.entity.Folder; import com.aisip.OnO.backend.folder.repository.FolderRepository; @@ -10,6 +11,7 @@ import com.aisip.OnO.backend.practicenote.dto.PracticeNotificationRegisterDto; import com.aisip.OnO.backend.practicenote.entity.PracticeNote; import com.aisip.OnO.backend.practicenote.entity.ProblemPracticeNoteMapping; +import com.aisip.OnO.backend.practicenote.exception.PracticeNoteErrorCase; import com.aisip.OnO.backend.practicenote.repository.PracticeNoteRepository; import com.aisip.OnO.backend.practicenote.repository.ProblemPracticeNoteMappingRepository; import com.aisip.OnO.backend.problem.dto.ProblemImageDataRegisterDto; @@ -17,6 +19,7 @@ import com.aisip.OnO.backend.problem.entity.Problem; import com.aisip.OnO.backend.problem.entity.ProblemImageData; import com.aisip.OnO.backend.problem.entity.ProblemImageType; +import com.aisip.OnO.backend.problem.exception.ProblemErrorCase; import com.aisip.OnO.backend.problem.repository.ProblemImageDataRepository; import com.aisip.OnO.backend.problem.repository.ProblemRepository; import org.junit.jupiter.api.AfterEach; @@ -33,6 +36,7 @@ import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; @SpringBootTest @ActiveProfiles("test") @@ -187,7 +191,7 @@ void findPracticeNoteDetail() { Long practiceNoteId = practiceNote.getId(); //when - PracticeNoteDetailResponseDto practiceNoteDetailResponseDto = practiceNoteService.findPracticeNoteDetail(practiceNoteId); + PracticeNoteDetailResponseDto practiceNoteDetailResponseDto = practiceNoteService.findPracticeNoteDetail(practiceNoteId, userId); //then assertThat(practiceNote.getId()).isEqualTo(practiceNoteDetailResponseDto.practiceNoteId()); @@ -198,6 +202,19 @@ void findPracticeNoteDetail() { } } + @Test + @DisplayName("다른 유저의 복습 노트 상세 조회 시 예외") + void findPracticeNoteDetail_OtherUserPractice() { + PracticeNote otherUserPractice = practiceNoteRepository.save(PracticeNote.from( + new PracticeNoteRegisterDto(null, "other practice", List.of(), null), + 2L + )); + + assertThatThrownBy(() -> practiceNoteService.findPracticeNoteDetail(otherUserPractice.getId(), userId)) + .isInstanceOf(ApplicationException.class) + .hasMessageContaining(PracticeNoteErrorCase.PRACTICE_NOTE_USER_UNMATCHED.getMessage()); + } + @Test @DisplayName("유저의 복습 노트 썸네일 리스트 조회 테스트") void findAllPracticeThumbnailsByUser() { @@ -229,6 +246,28 @@ void registerPractice() { assertThat(practiceNoteRepository.findAll().size()).isEqualTo(practiceNoteList.size() + 1); } + @Test + @DisplayName("다른 유저의 문제를 복습 노트에 추가하면 예외") + void registerPractice_OtherUserProblem() { + Problem otherUserProblem = Problem.from( + new ProblemRegisterDto(null, "memo", "reference", null, LocalDateTime.now()), + 2L + ); + otherUserProblem.updateFolder(problemList.get(0).getFolder()); + problemRepository.save(otherUserProblem); + + PracticeNoteRegisterDto practiceNoteRegisterDto = new PracticeNoteRegisterDto( + null, + "new practice", + List.of(otherUserProblem.getId()), + null + ); + + assertThatThrownBy(() -> practiceNoteService.registerPractice(practiceNoteRegisterDto, userId)) + .isInstanceOf(ApplicationException.class) + .hasMessageContaining(ProblemErrorCase.PROBLEM_USER_UNMATCHED.getMessage()); + } + @Test @DisplayName("복습 노트 조회 수 증가") void addPracticeNoteCount() { @@ -279,7 +318,7 @@ void deletePractice() { Long practiceNoteId = practiceNoteList.get(0).getId(); // when - practiceNoteService.deletePractice(practiceNoteId); + practiceNoteService.deletePractice(practiceNoteId, userId); // then Optional optionalPracticeNote = practiceNoteRepository.findById(practiceNoteId); @@ -296,7 +335,7 @@ void deletePractices() { List practiceIdList = List.of(practiceNoteList.get(0).getId(), practiceNoteList.get(1).getId()); // when - practiceNoteService.deletePractices(practiceIdList); + practiceNoteService.deletePractices(userId, practiceIdList); // then for (int i = 0; i < 2; i++) { diff --git a/src/test/java/com/aisip/OnO/backend/problem/controller/ProblemControllerTest.java b/src/test/java/com/aisip/OnO/backend/problem/controller/ProblemControllerTest.java index 0a7c521e..0ad2a864 100644 --- a/src/test/java/com/aisip/OnO/backend/problem/controller/ProblemControllerTest.java +++ b/src/test/java/com/aisip/OnO/backend/problem/controller/ProblemControllerTest.java @@ -97,7 +97,7 @@ void tearDown() { void getProblem() throws Exception { //given Long problemId = 1L; - given(problemService.findProblem(problemId)).willReturn(problemResponseDtoList.get(0)); + given(problemService.findProblem(problemId, 1L)).willReturn(problemResponseDtoList.get(0)); // When & Then mockMvc.perform(get("/api/problems/" + problemId) @@ -152,7 +152,7 @@ void getUserProblemCount() throws Exception { @WithMockCustomUser() void getFolderProblems() throws Exception { //given - given(problemService.findFolderProblemList(1L)).willReturn(problemResponseDtoList); + given(problemService.findFolderProblemList(1L, 1L)).willReturn(problemResponseDtoList); // When & Then mockMvc.perform(get("/api/problems/folder/1") @@ -203,7 +203,7 @@ void registerProblemImageData() throws Exception { .andExpect(status().isOk()); verify(problemService, times(1)).uploadProblemImages(eq(1L), eq(1L), any(), any()); - verify(problemService, times(1)).analysisProblem(eq(1L)); + verify(problemService, times(1)).analysisProblem(eq(1L), eq(1L)); } @Test @@ -264,6 +264,6 @@ void deleteProblemImageData() throws Exception { .param("imageUrl", imageUrl)) .andExpect(status().isOk()); - Mockito.verify(problemService, Mockito.times(1)).deleteProblemImageData(imageUrl); + Mockito.verify(problemService, Mockito.times(1)).deleteProblemImageData(imageUrl, 1L); } } diff --git a/src/test/java/com/aisip/OnO/backend/problem/service/ProblemServiceTest.java b/src/test/java/com/aisip/OnO/backend/problem/service/ProblemServiceTest.java index 06db3989..cf8b9ef5 100644 --- a/src/test/java/com/aisip/OnO/backend/problem/service/ProblemServiceTest.java +++ b/src/test/java/com/aisip/OnO/backend/problem/service/ProblemServiceTest.java @@ -13,6 +13,7 @@ import com.aisip.OnO.backend.problem.entity.Problem; import com.aisip.OnO.backend.problem.entity.ProblemImageData; import com.aisip.OnO.backend.problem.entity.ProblemImageType; +import com.aisip.OnO.backend.problem.exception.ProblemErrorCase; import com.aisip.OnO.backend.problem.repository.ProblemImageDataRepository; import com.aisip.OnO.backend.problem.repository.ProblemRepository; import com.aisip.OnO.backend.problemsolve.entity.AnswerStatus; @@ -140,7 +141,7 @@ void findProblem() { problemSolveRepository.save(ProblemSolve.create(problem, userId, lastSolvedAt, AnswerStatus.WRONG, null, null, null)); // when - ProblemResponseDto problemResponseDto = problemService.findProblem(problemId); + ProblemResponseDto problemResponseDto = problemService.findProblem(problemId, userId); // then assertThat(problemResponseDto.memo()).isEqualTo(problem.getMemo()); @@ -151,6 +152,16 @@ void findProblem() { assertThat(problemResponseDto.lastSolvedAt()).isEqualTo(lastSolvedAt); } + @Test + @DisplayName("다른 유저의 문제 조회 시 예외") + void findProblem_OtherUserProblem() { + Problem otherUserProblem = createOtherUserProblem(); + + assertThatThrownBy(() -> problemService.findProblem(otherUserProblem.getId(), userId)) + .isInstanceOf(ApplicationException.class) + .hasMessageContaining(ProblemErrorCase.PROBLEM_USER_UNMATCHED.getMessage()); + } + @Test @DisplayName("특정 유저의 모든 문제 목록 조회하기") void findUserProblems() { @@ -186,7 +197,7 @@ void findFolderProblemList() { Long folderId = folder.getId(); //when - List problemResponseDtoList = problemService.findFolderProblemList(folderId); + List problemResponseDtoList = problemService.findFolderProblemList(folderId, userId); //then assertThat(problemResponseDtoList.size()).isEqualTo(problemRepository.findAllByFolderId(folderId).size()); @@ -204,6 +215,19 @@ void findFolderProblemList() { assertThat(problemResponseDtoList.get(0).imageUrlList().size()).isEqualTo(2); } + @Test + @DisplayName("다른 유저의 폴더 문제 목록 조회 시 예외") + void findFolderProblemList_OtherUserFolder() { + Folder otherUserFolder = folderRepository.save(Folder.from( + new FolderRegisterDto("other folder", null, null), + 2L + )); + + assertThatThrownBy(() -> problemService.findFolderProblemList(otherUserFolder.getId(), userId)) + .isInstanceOf(ApplicationException.class) + .hasMessageContaining(FolderErrorCase.FOLDER_USER_UNMATCHED.getMessage()); + } + @Test @DisplayName("모든 문제 목록 조회하기") void findAllProblems() { @@ -271,6 +295,22 @@ void registerProblem_folderNotFound() { .hasMessageContaining(FolderErrorCase.FOLDER_NOT_FOUND.getMessage()); } + @Test + @DisplayName("문제 등록하기 - 다른 유저 폴더 예외") + void registerProblem_otherUserFolder() { + Folder otherUserFolder = folderRepository.save(Folder.from( + new FolderRegisterDto("other folder", null, null), + 2L + )); + ProblemRegisterDto dto = new ProblemRegisterDto( + null, "memo", "reference", otherUserFolder.getId(), LocalDateTime.now() + ); + + assertThatThrownBy(() -> problemService.registerProblem(dto, userId)) + .isInstanceOf(ApplicationException.class) + .hasMessageContaining(FolderErrorCase.FOLDER_USER_UNMATCHED.getMessage()); + } + @Test @DisplayName("문제 등록 v2 - folderId가 없으면 루트 폴더에 등록") void registerProblemV2_nullFolderIdUsesRootFolder() { @@ -428,7 +468,7 @@ void deleteProblem_success() { // When doNothing().when(fileUploadService).deleteImageFileFromS3(anyString()); - problemService.deleteProblem(problemId); + problemService.deleteProblem(problemId, userId); // Then verify(fileUploadService, times(2)).deleteImageFileFromS3(anyString()); @@ -462,7 +502,7 @@ void deleteProblems_problemIdList() { // when doNothing().when(fileUploadService).deleteImageFileFromS3(anyString()); - problemService.deleteProblemList(problemIdList); + problemService.deleteProblemList(userId, problemIdList); // then verify(fileUploadService, times(2 * problemIdList.size())).deleteImageFileFromS3(anyString()); @@ -477,7 +517,7 @@ void deleteProblems_folderIdList() { // when int problemCount = problemRepository.findAllByFolderId(folderIdList.get(0)).size(); - problemService.deleteAllByFolderIds(folderIdList); + problemService.deleteAllByFolderIds(userId, folderIdList); // then verify(fileUploadService, times(2 * problemCount)).deleteImageFileFromS3(anyString()); @@ -495,7 +535,7 @@ void deleteProblemImageData() { String imageUrl = problem.getProblemImageDataList().get(0).getImageUrl(); // when - problemService.deleteProblemImageData(imageUrl); + problemService.deleteProblemImageData(imageUrl, userId); // then verify(fileUploadService, times(1)).deleteImageFileFromS3(anyString()); @@ -508,4 +548,34 @@ void deleteProblemImageData() { } } } + + @Test + @DisplayName("다른 유저의 문제 이미지 삭제 시 예외") + void deleteProblemImageData_OtherUserProblem() { + Problem otherUserProblem = createOtherUserProblem(); + ProblemImageData imageData = ProblemImageData.from(new ProblemImageDataRegisterDto( + otherUserProblem.getId(), + "other-user-url", + ProblemImageType.PROBLEM_IMAGE + )); + imageData.updateProblem(otherUserProblem); + problemImageDataRepository.save(imageData); + + assertThatThrownBy(() -> problemService.deleteProblemImageData("other-user-url", userId)) + .isInstanceOf(ApplicationException.class) + .hasMessageContaining(ProblemErrorCase.PROBLEM_USER_UNMATCHED.getMessage()); + } + + private Problem createOtherUserProblem() { + Folder otherUserFolder = folderRepository.save(Folder.from( + new FolderRegisterDto("other folder", null, null), + 2L + )); + Problem otherUserProblem = Problem.from( + new ProblemRegisterDto(null, "memo", "reference", otherUserFolder.getId(), LocalDateTime.now()), + 2L + ); + otherUserProblem.updateFolder(otherUserFolder); + return problemRepository.save(otherUserProblem); + } }