diff --git a/src/main/java/postman/bottler/global/response/code/ErrorStatus.java b/src/main/java/postman/bottler/global/response/code/ErrorStatus.java index 4e2d26c8..f6d63ca0 100644 --- a/src/main/java/postman/bottler/global/response/code/ErrorStatus.java +++ b/src/main/java/postman/bottler/global/response/code/ErrorStatus.java @@ -30,6 +30,8 @@ public enum ErrorStatus { REPLY_LETTER_VALIDATION_ERROR(HttpStatus.BAD_REQUEST, "REPLY_LETTER4001"), INVALID_LETTER_TYPE(HttpStatus.BAD_REQUEST, "LETTER_TYPE4000"), DUPLICATE_REPLY_LETTER(HttpStatus.BAD_REQUEST, "REPLY_LETTER4002"), + LETTER_AUTHOR_MISMATCH(HttpStatus.BAD_REQUEST, "LETTER4004"), + UNAUTHORIZED_LETTER_ACCESS(HttpStatus.UNAUTHORIZED, "LETTER4010"), // 추천 에러 TEMP_RECOMMENDATIONS_NOT_FOUND(HttpStatus.NOT_FOUND, "TEMP_RECOMMENDATIONS_4040"), diff --git a/src/main/java/postman/bottler/keyword/dto/response/KeywordResponseDTO.java b/src/main/java/postman/bottler/keyword/dto/response/KeywordResponseDTO.java index 1e0b852a..6546e47f 100644 --- a/src/main/java/postman/bottler/keyword/dto/response/KeywordResponseDTO.java +++ b/src/main/java/postman/bottler/keyword/dto/response/KeywordResponseDTO.java @@ -8,16 +8,13 @@ public record KeywordResponseDTO( List categories ) { - // 정적 팩토리 메서드로 변환 로직 처리 public static KeywordResponseDTO from(List keywordList) { - // 카테고리별로 키워드 그룹화 Map> groupedByCategory = keywordList.stream() .collect(Collectors.groupingBy( Keyword::getCategory, Collectors.mapping(Keyword::getKeyword, Collectors.toList()) )); - // 카테고리 DTO 리스트 생성 List categories = groupedByCategory.entrySet().stream() .map(entry -> new CategoryKeywordsDTO(entry.getKey(), entry.getValue())) .toList(); diff --git a/src/main/java/postman/bottler/keyword/service/RedisLetterService.java b/src/main/java/postman/bottler/keyword/service/RedisLetterService.java index dca08918..3b8e0a57 100644 --- a/src/main/java/postman/bottler/keyword/service/RedisLetterService.java +++ b/src/main/java/postman/bottler/keyword/service/RedisLetterService.java @@ -41,7 +41,7 @@ public RecommendNotificationRequestDTO updateRecommendationsFromTemp(Long userId Long recommendId = findFirstValidLetter(tempRecommendations); updateActiveRecommendations(recommendId, activeRecommendations, activeKey); saveLetterToBox(userId, recommendId); - + redisTemplate.delete(tempKey); return RecommendNotificationRequestDTO.of(userId, recommendId); diff --git a/src/main/java/postman/bottler/letter/controller/LetterBoxController.java b/src/main/java/postman/bottler/letter/controller/LetterBoxController.java index 580f0165..ef1682f3 100644 --- a/src/main/java/postman/bottler/letter/controller/LetterBoxController.java +++ b/src/main/java/postman/bottler/letter/controller/LetterBoxController.java @@ -15,7 +15,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import postman.bottler.global.response.ApiResponse; -import postman.bottler.letter.dto.request.LetterDeleteRequestDTO; +import postman.bottler.letter.dto.LetterDeleteDTO; import postman.bottler.letter.dto.request.PageRequestDTO; import postman.bottler.letter.dto.response.LetterHeadersResponseDTO; import postman.bottler.letter.dto.response.PageResponseDTO; @@ -39,6 +39,7 @@ public class LetterBoxController { @Operation( summary = "보관된 모든 편지 조회", description = "페이지네이션을 사용하여 보관된 모든 편지의 제목, 라벨이미지, 작성날짜 정보를 조회합니다." + + "\nPage Default: page(1) size(9) sort(createAt)" ) @GetMapping public ApiResponse> getAllLetters( @@ -54,7 +55,8 @@ public ApiResponse> getAllLetters( @Operation( summary = "보낸 편지 조회", - description = "페이지네이션을 사용하여 보관된 보낸 편지의 헤더 정보를 조회합니다." + description = "페이지네이션을 사용하여 보관된 보낸 편지의 제목, 라벨이미지, 작성날짜 정보를 조회합니다." + + "\nPage Default: page(1) size(9) sort(createAt)" ) @GetMapping("/sent") public ApiResponse> getSentLetters( @@ -71,6 +73,7 @@ public ApiResponse> getSentLetters( @Operation( summary = "받은 편지 조회", description = "페이지네이션을 사용하여 보관된 받은 편지의 제목, 라벨이미지, 작성날짜 정보를 조회합니다." + + "\nPage Default: page(1) size(9) sort(createAt)" ) @GetMapping("/received") public ApiResponse> getReceivedLetters( @@ -90,9 +93,10 @@ public ApiResponse> getReceivedLetters ) @DeleteMapping public ApiResponse deleteSavedLetter( - @RequestBody @Valid List letterDeleteRequestDTOS + @RequestBody @Valid List letterDeleteDTOS, + @AuthenticationPrincipal CustomUserDetails userDetails ) { - deleteManagerService.deleteLetters(letterDeleteRequestDTOS); + deleteManagerService.deleteLetters(letterDeleteDTOS, userDetails.getUserId()); return ApiResponse.onSuccess("편지 보관을 취소했습니다."); } diff --git a/src/main/java/postman/bottler/letter/controller/LetterController.java b/src/main/java/postman/bottler/letter/controller/LetterController.java index 52acba86..7873d175 100644 --- a/src/main/java/postman/bottler/letter/controller/LetterController.java +++ b/src/main/java/postman/bottler/letter/controller/LetterController.java @@ -18,12 +18,14 @@ import postman.bottler.keyword.service.LetterKeywordService; import postman.bottler.keyword.service.RedisLetterService; import postman.bottler.letter.domain.Letter; +import postman.bottler.letter.dto.LetterDeleteDTO; import postman.bottler.letter.dto.request.LetterDeleteRequestDTO; import postman.bottler.letter.dto.request.LetterRequestDTO; import postman.bottler.letter.dto.response.LetterDetailResponseDTO; import postman.bottler.letter.dto.response.LetterRecommendHeadersResponseDTO; import postman.bottler.letter.exception.InvalidLetterRequestException; import postman.bottler.letter.service.DeleteManagerService; +import postman.bottler.letter.service.LetterBoxService; import postman.bottler.letter.service.LetterService; import postman.bottler.letter.utiil.ValidationUtil; import postman.bottler.user.auth.CustomUserDetails; @@ -39,6 +41,7 @@ public class LetterController { private final LetterKeywordService letterKeywordService; private final ValidationUtil validationUtil; private final RedisLetterService redisLetterService; + private final LetterBoxService letterBoxService; @Operation( summary = "키워드 편지 생성", @@ -57,13 +60,14 @@ public ApiResponse createLetter( @Operation( summary = "키워드 편지 삭제", - description = "키워드 편지ID, 편지타입(LETTER, REPLY_LETTER), 송수신 타입(SEND, RECEIVE)을 기반으로 키워드 편지를 삭제합니다." + description = "키워드 편지ID, BoxType 송수신(SEND, RECEIVE)을 기반으로 키워드 편지를 삭제합니다." ) @DeleteMapping public ApiResponse deleteLetter( - @RequestBody @Valid LetterDeleteRequestDTO letterDeleteRequestDTO + @RequestBody @Valid LetterDeleteRequestDTO letterDeleteRequestDTO, + @AuthenticationPrincipal CustomUserDetails userDetails ) { - deleteManagerService.deleteLetters(List.of(letterDeleteRequestDTO)); + deleteManagerService.deleteLetter(LetterDeleteDTO.fromLetter(letterDeleteRequestDTO), userDetails.getUserId()); return ApiResponse.onSuccess("키워드 편지를 삭제했습니다."); } @@ -75,6 +79,7 @@ public ApiResponse deleteLetter( public ApiResponse getLetter( @PathVariable Long letterId, @AuthenticationPrincipal CustomUserDetails userDetails ) { + letterBoxService.validateLetterInUserBox(letterId, userDetails.getUserId()); List keywords = letterKeywordService.getKeywords(letterId); LetterDetailResponseDTO result = letterService.getLetterDetail(letterId, keywords, userDetails.getUserId()); return ApiResponse.onSuccess(result); diff --git a/src/main/java/postman/bottler/letter/controller/ReplyLetterController.java b/src/main/java/postman/bottler/letter/controller/ReplyLetterController.java index e9020e19..262716a6 100644 --- a/src/main/java/postman/bottler/letter/controller/ReplyLetterController.java +++ b/src/main/java/postman/bottler/letter/controller/ReplyLetterController.java @@ -3,7 +3,6 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; -import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Page; @@ -17,8 +16,9 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import postman.bottler.global.response.ApiResponse; -import postman.bottler.letter.dto.request.LetterDeleteRequestDTO; +import postman.bottler.letter.dto.LetterDeleteDTO; import postman.bottler.letter.dto.request.PageRequestDTO; +import postman.bottler.letter.dto.request.ReplyLetterDeleteRequestDTO; import postman.bottler.letter.dto.request.ReplyLetterRequestDTO; import postman.bottler.letter.dto.response.PageResponseDTO; import postman.bottler.letter.dto.response.ReplyLetterHeadersResponseDTO; @@ -26,6 +26,7 @@ import postman.bottler.letter.exception.InvalidPageRequestException; import postman.bottler.letter.exception.InvalidReplyLetterRequestException; import postman.bottler.letter.service.DeleteManagerService; +import postman.bottler.letter.service.LetterBoxService; import postman.bottler.letter.service.ReplyLetterService; import postman.bottler.letter.utiil.ValidationUtil; import postman.bottler.user.auth.CustomUserDetails; @@ -40,6 +41,7 @@ public class ReplyLetterController { private final ReplyLetterService letterReplyService; private final DeleteManagerService deleteManagerService; private final ValidationUtil validationUtil; + private final LetterBoxService letterBoxService; @Operation( summary = "키워드 편지 생성", @@ -61,6 +63,7 @@ public ApiResponse createReply( @Operation( summary = "특정 키워드 편지에 대한 답장 목록 조회", description = "지정된 편지 ID에 대한 답장들의 제목, 라벨이미지, 작성날짜를 페이지네이션 형태로 반환합니다." + + "\nPage Default: page(1) size(9) sort(createAt)" ) @GetMapping("/{letterId}") public ApiResponse> getReplyForLetter( @@ -69,6 +72,7 @@ public ApiResponse> getReplyForLe BindingResult bindingResult, @AuthenticationPrincipal CustomUserDetails userDetails ) { + letterBoxService.validateLetterInUserBox(letterId, userDetails.getUserId()); validatePageRequest(bindingResult); Page result = letterReplyService.getReplyLetterHeadersById(letterId, pageRequestDTO, userDetails.getUserId()); @@ -81,21 +85,25 @@ public ApiResponse> getReplyForLe ) @GetMapping("/detail/{replyLetterId}") public ApiResponse getReplyLetter( - @PathVariable Long replyLetterId + @PathVariable Long replyLetterId, + @AuthenticationPrincipal CustomUserDetails userDetails ) { + letterBoxService.validateLetterInUserBox(replyLetterId, userDetails.getUserId()); ReplyLetterResponseDTO result = letterReplyService.getReplyLetterDetail(replyLetterId); return ApiResponse.onSuccess(result); } @Operation( summary = "답장 편지 삭제", - description = "답장 편지ID, 편지타입(LETTER, REPLY_LETTER, 송수신 타입(SEND, RECEIVE)을 기반으로 답장 편지를 삭제합니다." + description = "답장 편지ID, 송수신 타입(SEND, RECEIVE)을 기반으로 답장 편지를 삭제합니다." ) @DeleteMapping public ApiResponse deleteReplyLetter( - @RequestBody @Valid LetterDeleteRequestDTO letterDeleteRequestDTO + @RequestBody @Valid ReplyLetterDeleteRequestDTO replyLetterDeleteRequestDTO, + @AuthenticationPrincipal CustomUserDetails userDetails ) { - deleteManagerService.deleteLetters(List.of(letterDeleteRequestDTO)); + deleteManagerService.deleteLetter(LetterDeleteDTO.fromReplyLetter(replyLetterDeleteRequestDTO), + userDetails.getUserId()); return ApiResponse.onSuccess("success"); } diff --git a/src/main/java/postman/bottler/letter/dto/LetterDeleteDTO.java b/src/main/java/postman/bottler/letter/dto/LetterDeleteDTO.java new file mode 100644 index 00000000..5bc4c33e --- /dev/null +++ b/src/main/java/postman/bottler/letter/dto/LetterDeleteDTO.java @@ -0,0 +1,29 @@ +package postman.bottler.letter.dto; + +import jakarta.validation.constraints.NotNull; +import postman.bottler.letter.domain.BoxType; +import postman.bottler.letter.domain.LetterType; +import postman.bottler.letter.dto.request.LetterDeleteRequestDTO; +import postman.bottler.letter.dto.request.ReplyLetterDeleteRequestDTO; + +public record LetterDeleteDTO( + @NotNull(message = "Letter ID는 필수입니다.") Long letterId, + @NotNull(message = "Letter Type은 필수입니다.") LetterType letterType, + @NotNull(message = "Box Type은 필수입니다.") BoxType boxType +) { + public static LetterDeleteDTO fromLetter(LetterDeleteRequestDTO letterDeleteRequestDTO) { + return new LetterDeleteDTO( + letterDeleteRequestDTO.letterId(), + LetterType.LETTER, + letterDeleteRequestDTO.boxType() + ); + } + + public static LetterDeleteDTO fromReplyLetter(ReplyLetterDeleteRequestDTO replyLetterDeleteRequestDTO) { + return new LetterDeleteDTO( + replyLetterDeleteRequestDTO.letterId(), + LetterType.REPLY_LETTER, + replyLetterDeleteRequestDTO.boxType() + ); + } +} diff --git a/src/main/java/postman/bottler/letter/dto/request/LetterDeleteRequestDTO.java b/src/main/java/postman/bottler/letter/dto/request/LetterDeleteRequestDTO.java index 5ca100d0..17fbb9c8 100644 --- a/src/main/java/postman/bottler/letter/dto/request/LetterDeleteRequestDTO.java +++ b/src/main/java/postman/bottler/letter/dto/request/LetterDeleteRequestDTO.java @@ -2,18 +2,9 @@ import jakarta.validation.constraints.NotNull; import postman.bottler.letter.domain.BoxType; -import postman.bottler.letter.domain.LetterType; public record LetterDeleteRequestDTO( @NotNull(message = "Letter ID는 필수입니다.") Long letterId, - @NotNull(message = "Letter Type은 필수입니다.") LetterType letterType, @NotNull(message = "Box Type은 필수입니다.") BoxType boxType ) { - public static LetterDeleteRequestDTO of(Long letterId, LetterType letterType, BoxType boxType) { - return new LetterDeleteRequestDTO( - letterId, - letterType, - boxType - ); - } } diff --git a/src/main/java/postman/bottler/letter/dto/request/ReplyLetterDeleteRequestDTO.java b/src/main/java/postman/bottler/letter/dto/request/ReplyLetterDeleteRequestDTO.java new file mode 100644 index 00000000..22ccd0a3 --- /dev/null +++ b/src/main/java/postman/bottler/letter/dto/request/ReplyLetterDeleteRequestDTO.java @@ -0,0 +1,16 @@ +package postman.bottler.letter.dto.request; + +import jakarta.validation.constraints.NotNull; +import postman.bottler.letter.domain.BoxType; + +public record ReplyLetterDeleteRequestDTO( + @NotNull(message = "Letter ID는 필수입니다.") Long letterId, + @NotNull(message = "Box Type은 필수입니다.") BoxType boxType +) { + public static ReplyLetterDeleteRequestDTO of(Long letterId, BoxType boxType) { + return new ReplyLetterDeleteRequestDTO( + letterId, + boxType + ); + } +} diff --git a/src/main/java/postman/bottler/letter/exception/LetterAuthorMismatchException.java b/src/main/java/postman/bottler/letter/exception/LetterAuthorMismatchException.java new file mode 100644 index 00000000..25f59d2c --- /dev/null +++ b/src/main/java/postman/bottler/letter/exception/LetterAuthorMismatchException.java @@ -0,0 +1,7 @@ +package postman.bottler.letter.exception; + +public class LetterAuthorMismatchException extends RuntimeException { + public LetterAuthorMismatchException(String message) { + super(message); + } +} diff --git a/src/main/java/postman/bottler/letter/exception/LetterExceptionHandler.java b/src/main/java/postman/bottler/letter/exception/LetterExceptionHandler.java index fa5cf895..f7cf31db 100644 --- a/src/main/java/postman/bottler/letter/exception/LetterExceptionHandler.java +++ b/src/main/java/postman/bottler/letter/exception/LetterExceptionHandler.java @@ -5,6 +5,7 @@ import static postman.bottler.global.response.code.ErrorStatus.INVALID_SORT_FIELD; import static postman.bottler.global.response.code.ErrorStatus.LETTER_ACCESS_DENIED; import static postman.bottler.global.response.code.ErrorStatus.LETTER_ALREADY_SAVED; +import static postman.bottler.global.response.code.ErrorStatus.LETTER_AUTHOR_MISMATCH; import static postman.bottler.global.response.code.ErrorStatus.LETTER_DELETE_VALIDATION_ERROR; import static postman.bottler.global.response.code.ErrorStatus.LETTER_NOT_FOUND; import static postman.bottler.global.response.code.ErrorStatus.LETTER_UNKNOWN_VALIDATION_ERROR; @@ -12,6 +13,7 @@ import static postman.bottler.global.response.code.ErrorStatus.PAGINATION_VALIDATION_ERROR; import static postman.bottler.global.response.code.ErrorStatus.REPLY_LETTER_VALIDATION_ERROR; import static postman.bottler.global.response.code.ErrorStatus.TEMP_RECOMMENDATIONS_NOT_FOUND; +import static postman.bottler.global.response.code.ErrorStatus.UNAUTHORIZED_LETTER_ACCESS; import java.util.Map; import lombok.extern.slf4j.Slf4j; @@ -114,4 +116,19 @@ public ResponseEntity> handleInvalidLetterTypeException( .status(TEMP_RECOMMENDATIONS_NOT_FOUND.getHttpStatus()) .body(ApiResponse.onFailure(TEMP_RECOMMENDATIONS_NOT_FOUND.getCode(), e.getMessage(), null)); } + + @ExceptionHandler(LetterAuthorMismatchException.class) + public ResponseEntity> handleLetterAuthorMismatchException(LetterAuthorMismatchException e) { + return ResponseEntity + .status(LETTER_AUTHOR_MISMATCH.getHttpStatus()) + .body(ApiResponse.onFailure(LETTER_AUTHOR_MISMATCH.getCode(), e.getMessage(), null)); + } + + @ExceptionHandler(UnauthorizedLetterAccessException.class) + public ResponseEntity> handleUnauthorizedLetterAccessException( + UnauthorizedLetterAccessException e) { + return ResponseEntity + .status(UNAUTHORIZED_LETTER_ACCESS.getHttpStatus()) + .body(ApiResponse.onFailure(UNAUTHORIZED_LETTER_ACCESS.getCode(), e.getMessage(), null)); + } } diff --git a/src/main/java/postman/bottler/letter/exception/UnauthorizedLetterAccessException.java b/src/main/java/postman/bottler/letter/exception/UnauthorizedLetterAccessException.java new file mode 100644 index 00000000..5bfef366 --- /dev/null +++ b/src/main/java/postman/bottler/letter/exception/UnauthorizedLetterAccessException.java @@ -0,0 +1,7 @@ +package postman.bottler.letter.exception; + +public class UnauthorizedLetterAccessException extends RuntimeException { + public UnauthorizedLetterAccessException(String message) { + super(message); + } +} diff --git a/src/main/java/postman/bottler/letter/infra/LetterBoxJdbcRepository.java b/src/main/java/postman/bottler/letter/infra/LetterBoxJdbcRepository.java index 44ff58f2..d7b0a778 100644 --- a/src/main/java/postman/bottler/letter/infra/LetterBoxJdbcRepository.java +++ b/src/main/java/postman/bottler/letter/infra/LetterBoxJdbcRepository.java @@ -42,4 +42,11 @@ public void saveAll(List letterBoxes) { jdbcTemplate.batchUpdate(sql, params); } + + public boolean existsByUserIdAndLetterId(Long letterId, Long userId) { + String sql = "SELECT COUNT(*) FROM letter_box WHERE user_id = ? AND letter_id = ?"; + + Integer count = jdbcTemplate.queryForObject(sql, Integer.class, userId, letterId); + return count > 0; + } } diff --git a/src/main/java/postman/bottler/letter/infra/LetterBoxQueryRepository.java b/src/main/java/postman/bottler/letter/infra/LetterBoxQueryRepository.java index d8c38644..f20cd5d8 100644 --- a/src/main/java/postman/bottler/letter/infra/LetterBoxQueryRepository.java +++ b/src/main/java/postman/bottler/letter/infra/LetterBoxQueryRepository.java @@ -86,4 +86,17 @@ public void deleteByCondition(List letterIds, LetterType letterType, BoxTy ) .execute(); } + + public void deleteByConditionAndUserId(List letterIds, LetterType letterType, BoxType boxType, Long userId) { + QLetterBoxEntity letterBox = QLetterBoxEntity.letterBoxEntity; + + queryFactory.delete(letterBox) + .where( + letterIds != null ? letterBox.letterId.in(letterIds) : null, + letterType != LetterType.UNKNOWN ? letterBox.letterType.eq(letterType) : null, + boxType != BoxType.UNKNOWN ? letterBox.boxType.eq(boxType) : null, + letterBox.userId.eq(userId) + ) + .execute(); + } } diff --git a/src/main/java/postman/bottler/letter/infra/LetterBoxRepositoryImpl.java b/src/main/java/postman/bottler/letter/infra/LetterBoxRepositoryImpl.java index b8d58b00..1a9af7e9 100644 --- a/src/main/java/postman/bottler/letter/infra/LetterBoxRepositoryImpl.java +++ b/src/main/java/postman/bottler/letter/infra/LetterBoxRepositoryImpl.java @@ -52,14 +52,19 @@ public void deleteByCondition(List letterIds, LetterType letterType, BoxTy letterBoxQueryRepository.deleteByCondition(letterIds, letterType, boxType); } + @Override + public void deleteByConditionAndUserId(List letterIds, LetterType letterType, BoxType boxType, Long userId) { + letterBoxQueryRepository.deleteByConditionAndUserId(letterIds, letterType, boxType, userId); + } + @Override public List getReceivedLettersById(Long userId) { return letterBoxQueryRepository.getReceivedLettersById(userId); } @Override - public void saveRecommendedLetters(List letterBoxes) { - letterBoxJdbcRepository.saveAll(letterBoxes); + public boolean existsByLetterIdAndUserId(Long letterId, Long userId) { + return letterBoxJdbcRepository.existsByUserIdAndLetterId(letterId, userId); } private List fetchLetters(Long userId, BoxType boxType, Pageable pageable) { diff --git a/src/main/java/postman/bottler/letter/service/DeleteManagerService.java b/src/main/java/postman/bottler/letter/service/DeleteManagerService.java index a949fcdf..0f4fb019 100644 --- a/src/main/java/postman/bottler/letter/service/DeleteManagerService.java +++ b/src/main/java/postman/bottler/letter/service/DeleteManagerService.java @@ -10,7 +10,7 @@ import postman.bottler.keyword.service.LetterKeywordService; import postman.bottler.letter.domain.BoxType; import postman.bottler.letter.domain.LetterType; -import postman.bottler.letter.dto.request.LetterDeleteRequestDTO; +import postman.bottler.letter.dto.LetterDeleteDTO; @Slf4j @Service @@ -22,27 +22,42 @@ public class DeleteManagerService { private final LetterKeywordService letterKeywordService; @Transactional - public void deleteLetters(List letterDeleteRequestDTOs) { - Map>> groupedByTypeAndBox = letterDeleteRequestDTOs.stream() + public void deleteLetter(LetterDeleteDTO letterDeleteDTO, Long userId) { + Map>> groupedByTypeAndBox = Map.of( + letterDeleteDTO.letterType(), + Map.of( + letterDeleteDTO.boxType(), + List.of(letterDeleteDTO.letterId()) + ) + ); + processGroupedLetters(groupedByTypeAndBox, userId); + } + + @Transactional + public void deleteLetters(List letterDeleteDTOS, Long userId) { + Map>> groupedByTypeAndBox = letterDeleteDTOS.stream() .collect(Collectors.groupingBy( - LetterDeleteRequestDTO::letterType, + LetterDeleteDTO::letterType, Collectors.groupingBy( - LetterDeleteRequestDTO::boxType, - Collectors.mapping(LetterDeleteRequestDTO::letterId, Collectors.toList()) + LetterDeleteDTO::boxType, + Collectors.mapping(LetterDeleteDTO::letterId, Collectors.toList()) ) )); + processGroupedLetters(groupedByTypeAndBox, userId); + } + private void processGroupedLetters(Map>> groupedByTypeAndBox, Long userId) { if (groupedByTypeAndBox.containsKey(LetterType.LETTER)) { Map> letterBoxMap = groupedByTypeAndBox.get(LetterType.LETTER); if (letterBoxMap.containsKey(BoxType.SEND)) { List letterIds = letterBoxMap.get(BoxType.SEND); - letterService.deleteLetters(letterIds); // LetterService에서 삭제 + letterService.deleteLetters(letterIds, userId); letterKeywordService.markKeywordsAsDeleted(letterIds); letterBoxService.deleteByIdsAndType(letterIds, LetterType.LETTER, BoxType.UNKNOWN); } if (letterBoxMap.containsKey(BoxType.RECEIVE)) { List letterIds = letterBoxMap.get(BoxType.RECEIVE); - letterBoxService.deleteByIdsAndType(letterIds, LetterType.LETTER, BoxType.RECEIVE); + letterBoxService.deleteByIdsAndTypeAndUserId(letterIds, LetterType.LETTER, BoxType.RECEIVE, userId); } } @@ -50,13 +65,15 @@ public void deleteLetters(List letterDeleteRequestDTOs) Map> replyLetterBoxMap = groupedByTypeAndBox.get(LetterType.REPLY_LETTER); if (replyLetterBoxMap.containsKey(BoxType.SEND)) { List replyLetterIds = replyLetterBoxMap.get(BoxType.SEND); - replyLetterService.deleteReplyLetters(replyLetterIds); // ReplyLetterService에서 삭제 + replyLetterService.deleteReplyLetters(replyLetterIds, userId); letterBoxService.deleteByIdsAndType(replyLetterIds, LetterType.REPLY_LETTER, BoxType.UNKNOWN); } if (replyLetterBoxMap.containsKey(BoxType.RECEIVE)) { List replyLetterIds = replyLetterBoxMap.get(BoxType.RECEIVE); - letterBoxService.deleteByIdsAndType(replyLetterIds, LetterType.REPLY_LETTER, BoxType.RECEIVE); + letterBoxService.deleteByIdsAndTypeAndUserId(replyLetterIds, LetterType.REPLY_LETTER, BoxType.RECEIVE, + userId); } } } + } diff --git a/src/main/java/postman/bottler/letter/service/LetterBoxRepository.java b/src/main/java/postman/bottler/letter/service/LetterBoxRepository.java index 20f6002d..b0e1eb86 100644 --- a/src/main/java/postman/bottler/letter/service/LetterBoxRepository.java +++ b/src/main/java/postman/bottler/letter/service/LetterBoxRepository.java @@ -19,7 +19,9 @@ public interface LetterBoxRepository { void deleteByCondition(List letterIds, LetterType letterType, BoxType boxType); + void deleteByConditionAndUserId(List letterIds, LetterType letterType, BoxType boxType, Long userId); + List getReceivedLettersById(Long userId); - void saveRecommendedLetters(List letterBoxes); + boolean existsByLetterIdAndUserId(Long letterId, Long userId); } diff --git a/src/main/java/postman/bottler/letter/service/LetterBoxService.java b/src/main/java/postman/bottler/letter/service/LetterBoxService.java index b5967775..bc5b7a70 100644 --- a/src/main/java/postman/bottler/letter/service/LetterBoxService.java +++ b/src/main/java/postman/bottler/letter/service/LetterBoxService.java @@ -11,6 +11,7 @@ import postman.bottler.letter.dto.LetterBoxDTO; import postman.bottler.letter.dto.request.PageRequestDTO; import postman.bottler.letter.dto.response.LetterHeadersResponseDTO; +import postman.bottler.letter.exception.UnauthorizedLetterAccessException; @Slf4j @Service @@ -44,9 +45,22 @@ public void deleteByIdsAndType(List letterIds, LetterType letterType, BoxT letterBoxRepository.deleteByCondition(letterIds, letterType, boxType); } + @Transactional + public void deleteByIdsAndTypeAndUserId(List letterIds, LetterType letterType, BoxType boxType, Long userId) { + letterBoxRepository.deleteByConditionAndUserId(letterIds, letterType, boxType, userId); + } + + @Transactional(readOnly = true) public List getLettersByUserId(Long userId) { return letterBoxRepository.getReceivedLettersById(userId); } + @Transactional(readOnly = true) + public void validateLetterInUserBox(Long letterId, Long userId) { + boolean isLetterInUserBox = letterBoxRepository.existsByLetterIdAndUserId(letterId, userId); + if (!isLetterInUserBox) { + throw new UnauthorizedLetterAccessException("사용자가 해당 편지에 접근할 권한이 없습니다."); + } + } } diff --git a/src/main/java/postman/bottler/letter/service/LetterService.java b/src/main/java/postman/bottler/letter/service/LetterService.java index 216d71c7..a96c9fd1 100644 --- a/src/main/java/postman/bottler/letter/service/LetterService.java +++ b/src/main/java/postman/bottler/letter/service/LetterService.java @@ -13,6 +13,7 @@ import postman.bottler.letter.dto.request.LetterRequestDTO; import postman.bottler.letter.dto.response.LetterDetailResponseDTO; import postman.bottler.letter.dto.response.LetterRecommendHeadersResponseDTO; +import postman.bottler.letter.exception.LetterAuthorMismatchException; import postman.bottler.letter.exception.LetterNotFoundException; import postman.bottler.notification.dto.request.NotificationLabelRequestDTO; import postman.bottler.user.service.UserService; @@ -36,7 +37,12 @@ public Letter createLetter(LetterRequestDTO letterRequestDTO, Long userId) { } @Transactional - public void deleteLetters(List letterIds) { + public void deleteLetters(List letterIds, Long userId) { + letterIds.forEach(letterId -> { + if (!findLetter(letterId).getUserId().equals(userId)) { + throw new LetterAuthorMismatchException("요청자와 작성자가 일치하지 않습니다."); + } + }); letterRepository.deleteByIds(letterIds); } diff --git a/src/main/java/postman/bottler/letter/service/ReplyLetterService.java b/src/main/java/postman/bottler/letter/service/ReplyLetterService.java index 6beb3c8a..1e1f614e 100644 --- a/src/main/java/postman/bottler/letter/service/ReplyLetterService.java +++ b/src/main/java/postman/bottler/letter/service/ReplyLetterService.java @@ -17,6 +17,7 @@ import postman.bottler.letter.dto.response.ReplyLetterHeadersResponseDTO; import postman.bottler.letter.dto.response.ReplyLetterResponseDTO; import postman.bottler.letter.exception.DuplicateReplyLetterException; +import postman.bottler.letter.exception.LetterAuthorMismatchException; import postman.bottler.letter.exception.LetterNotFoundException; import postman.bottler.reply.dto.ReplyType; @@ -80,17 +81,20 @@ public ReplyLetterResponseDTO getReplyLetterDetail(Long replyLetterId) { } @Transactional - public void deleteReplyLetters(List letterIds) { + public void deleteReplyLetters(List letterIds, Long userId) { + letterIds.forEach(letterId -> { + if (!findReplyLetter(letterId).getSenderId().equals(userId)) { + throw new LetterAuthorMismatchException("요청자와 작성자가 일치하지 않습니다."); + } + }); + letterIds.forEach(this::deleteRecentReply); replyLetterRepository.deleteByIds(letterIds); - for (Long letterId : letterIds) { - deleteRecentReply(letterId); - } } @Transactional public Long blockLetter(Long replyLetterId) { - replyLetterRepository.blockReplyLetterById(replyLetterId); ReplyLetter replyLetter = findReplyLetter(replyLetterId); + replyLetterRepository.blockReplyLetterById(replyLetterId); return replyLetter.getSenderId(); }