diff --git a/build.gradle b/build.gradle index 13d3f0a..440527a 100644 --- a/build.gradle +++ b/build.gradle @@ -31,6 +31,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-starter-test' implementation 'org.springframework.boot:spring-boot-devtools' //Lombok diff --git a/src/main/java/com/ripple/BE/global/exception/handler/GlobalExceptionHandler.java b/src/main/java/com/ripple/BE/global/exception/handler/GlobalExceptionHandler.java index 585480c..1efdd88 100644 --- a/src/main/java/com/ripple/BE/global/exception/handler/GlobalExceptionHandler.java +++ b/src/main/java/com/ripple/BE/global/exception/handler/GlobalExceptionHandler.java @@ -75,6 +75,8 @@ public ResponseEntity handleAllException(Exception e, WebRequest request return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); } + e.printStackTrace(); + return handleExceptionInternal(GlobalErrorCode.INTERNAL_SERVER_ERROR); } diff --git a/src/main/java/com/ripple/BE/notification/service/NotificationService.java b/src/main/java/com/ripple/BE/notification/service/NotificationService.java index ccc4b60..b316512 100644 --- a/src/main/java/com/ripple/BE/notification/service/NotificationService.java +++ b/src/main/java/com/ripple/BE/notification/service/NotificationService.java @@ -29,6 +29,9 @@ public class NotificationService { @Transactional public void createCommentNotification(final Post post, final Comment comment) { + if (post.getAuthor() == null) { + return; + } User postAuthor = post.getAuthor(); @@ -50,26 +53,34 @@ public void createReplyNotification(final Post post, final Comment comment) { String content = comment.getContent(); String title = post.getTitle(); - Notification notificationForPostAuthor = - Notification.toNotificationEntity(postAuthor, content, title, NotificationType.REPLY, post); + if (post.getAuthor() != null) { - Notification notificationForCommentAuthor = - Notification.toNotificationEntity( - commentAuthor, content, title, NotificationType.REPLY, post); + Notification notificationForPostAuthor = + Notification.toNotificationEntity( + postAuthor, content, title, NotificationType.REPLY, post); + notificationRepository.save(notificationForPostAuthor); + sendNotification(postAuthor, NotificationDTO.toNotificationDTO(notificationForPostAuthor)); + } + if (comment.getCommenter() != null) { - notificationRepository.save(notificationForPostAuthor); - notificationRepository.save(notificationForCommentAuthor); + Notification notificationForCommentAuthor = + Notification.toNotificationEntity( + commentAuthor, content, title, NotificationType.REPLY, post); - // 게시글 작성자와 댓글 작성자에게 알림 전송 - sendNotification(postAuthor, NotificationDTO.toNotificationDTO(notificationForPostAuthor)); - sendNotification( - commentAuthor, NotificationDTO.toNotificationDTO(notificationForCommentAuthor)); + notificationRepository.save(notificationForCommentAuthor); + + sendNotification( + commentAuthor, NotificationDTO.toNotificationDTO(notificationForCommentAuthor)); + } } @Transactional public void createPopularNotification(final Post post) { User receiver = post.getAuthor(); + if (receiver == null) { + return; + } Notification notification = Notification.toNotificationEntity( diff --git a/src/main/java/com/ripple/BE/post/controller/ToktokController.java b/src/main/java/com/ripple/BE/post/controller/ToktokController.java index 65a6254..9021473 100644 --- a/src/main/java/com/ripple/BE/post/controller/ToktokController.java +++ b/src/main/java/com/ripple/BE/post/controller/ToktokController.java @@ -2,11 +2,12 @@ import com.ripple.BE.global.dto.response.ApiResponse; import com.ripple.BE.post.domain.type.PostSort; -import com.ripple.BE.post.dto.PostDTO; -import com.ripple.BE.post.dto.PostListDTO; +import com.ripple.BE.post.dto.ToktokDTO; +import com.ripple.BE.post.dto.ToktokListDTO; import com.ripple.BE.post.dto.response.ToktokPreviewListResponse; import com.ripple.BE.post.dto.response.ToktokPreviewResponse; import com.ripple.BE.post.dto.response.ToktokResponse; +import com.ripple.BE.post.service.ToktokAdminService; import com.ripple.BE.post.service.ToktokService; import com.ripple.BE.user.domain.CustomUserDetails; import io.swagger.v3.oas.annotations.Operation; @@ -18,6 +19,7 @@ import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @@ -29,14 +31,15 @@ public class ToktokController { private final ToktokService toktokService; + private final ToktokAdminService toktokAdminService; @Operation(summary = "오늘의 경제 톡톡 주제 조회", description = "오늘의 경제 톡톡 주제를 조회합니다.") @GetMapping("/toktok-today") public ResponseEntity> getTodayToktok() { - PostDTO postDTO = toktokService.getTodayToktok(); + ToktokDTO toktokDTO = toktokService.getTodayToktok(); return ResponseEntity.status(HttpStatus.OK) - .body(ApiResponse.from(ToktokPreviewResponse.toToktokPreviewResponse(postDTO))); + .body(ApiResponse.from(ToktokPreviewResponse.toToktokPreviewResponse(toktokDTO))); } @Operation(summary = "경제 톡톡 목록 조회", description = "경제 톡톡 목록을 조회합니다.") @@ -45,10 +48,11 @@ public ResponseEntity> getToktoks( final @AuthenticationPrincipal CustomUserDetails currentUser, final @RequestParam(required = false, defaultValue = "0") @PositiveOrZero int page, final @RequestParam(required = false, defaultValue = "RECENT") PostSort sort) { - PostListDTO postListDTO = toktokService.getToktoks(page, sort, currentUser.getId()); + ToktokListDTO toktokListDTO = toktokService.getToktoks(page, sort, currentUser.getId()); return ResponseEntity.status(HttpStatus.OK) - .body(ApiResponse.from(ToktokPreviewListResponse.toToktokPreviewListResponse(postListDTO))); + .body( + ApiResponse.from(ToktokPreviewListResponse.toToktokPreviewListResponse(toktokListDTO))); } @Operation(summary = "경제 톡톡 게시물 상세 조회", description = "경제 톡톡 게시물을 상세 조회합니다.") @@ -57,9 +61,16 @@ public ResponseEntity> getToktok( final @AuthenticationPrincipal CustomUserDetails currentUser, final @PathVariable("id") long id) { - PostDTO postDTO = toktokService.getToktok(id, currentUser.getId()); + ToktokDTO toktokDTO = toktokService.getToktok(id, currentUser.getId()); return ResponseEntity.status(HttpStatus.OK) - .body(ApiResponse.from(ToktokResponse.toToktokResponse(postDTO))); + .body(ApiResponse.from(ToktokResponse.toToktokResponse(toktokDTO))); + } + + @Operation(summary = "경제 톡톡 게시물 생성 (관리자)", description = "경제 톡톡 게시물을 생성합니다. (관리자 전용)") + @PostMapping("/toktok/excel") + public ResponseEntity> saveToktokByExcel() { + toktokAdminService.createToktokByExcel(); + return ResponseEntity.status(HttpStatus.OK).body(ApiResponse.from(ApiResponse.EMPTY_RESPONSE)); } } diff --git a/src/main/java/com/ripple/BE/post/domain/Post.java b/src/main/java/com/ripple/BE/post/domain/Post.java index 1e0ff87..03c88ad 100644 --- a/src/main/java/com/ripple/BE/post/domain/Post.java +++ b/src/main/java/com/ripple/BE/post/domain/Post.java @@ -4,6 +4,7 @@ import com.ripple.BE.image.domain.Image; import com.ripple.BE.post.domain.type.PostType; import com.ripple.BE.post.dto.PostDTO; +import com.ripple.BE.post.dto.ToktokDTO; import com.ripple.BE.user.domain.User; import jakarta.persistence.CascadeType; import jakarta.persistence.Column; @@ -126,6 +127,15 @@ public static Post toPostEntity(PostDTO postDTO) { .build(); } + public static Post toPostEntity(ToktokDTO toktokDTO) { + return Post.builder() + .title(toktokDTO.title()) + .content(toktokDTO.content()) + .type(toktokDTO.type()) + .imageList(new ArrayList<>()) + .build(); + } + public void update(PostDTO postDTO) { this.title = postDTO.title(); this.content = postDTO.content(); diff --git a/src/main/java/com/ripple/BE/post/dto/ToktokDTO.java b/src/main/java/com/ripple/BE/post/dto/ToktokDTO.java new file mode 100644 index 0000000..66b71e4 --- /dev/null +++ b/src/main/java/com/ripple/BE/post/dto/ToktokDTO.java @@ -0,0 +1,94 @@ +package com.ripple.BE.post.dto; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import com.ripple.BE.image.dto.ImageListDTO; +import com.ripple.BE.post.domain.Post; +import com.ripple.BE.post.domain.type.PostType; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.Map; + +public record ToktokDTO( + Long id, + String title, + String content, + PostType type, + Long likeCount, + Long commentCount, + Long scrapCount, + ImageListDTO imageList, + Boolean isScraped, + Boolean isLiked, + @JsonSerialize(using = LocalDateTimeSerializer.class) + @JsonDeserialize(using = LocalDateTimeDeserializer.class) + LocalDateTime createdDate, + @JsonSerialize(using = LocalDateTimeSerializer.class) + @JsonDeserialize(using = LocalDateTimeDeserializer.class) + LocalDateTime modifiedDate, + @JsonSerialize(using = LocalDateSerializer.class) + @JsonDeserialize(using = LocalDateDeserializer.class) + LocalDate usedDate, + CommentListDTO commentListDTO) { + + private static final String TITLE = "title"; + private static final String CONTENT = "content"; + + public static ToktokDTO toToktokDTO(final Post post) { + return new ToktokDTO( + post.getId(), + post.getTitle(), + post.getContent(), + post.getType(), + post.getLikeCount(), + post.getCommentCount(), + post.getScrapCount(), + ImageListDTO.toImageListDTO(post.getImageList()), + post.getIsScrapped(), + post.getIsLiked(), + post.getCreatedDate(), + post.getModifiedDate(), + post.getUsedDate(), + null); + } + + public static ToktokDTO toToktokDTO(final Post post, final CommentListDTO commentListDTO) { + return new ToktokDTO( + post.getId(), + post.getTitle(), + post.getContent(), + post.getType(), + post.getLikeCount(), + post.getCommentCount(), + post.getScrapCount(), + ImageListDTO.toImageListDTO(post.getImageList()), + post.getIsScrapped(), + post.getIsLiked(), + post.getCreatedDate(), + post.getModifiedDate(), + post.getUsedDate(), + commentListDTO); + } + + public static ToktokDTO toToktokDTO(final Map excelData) { + return new ToktokDTO( + null, + excelData.get(TITLE), + excelData.get(CONTENT), + PostType.ECONOMY_TALK, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null); + } +} diff --git a/src/main/java/com/ripple/BE/post/dto/ToktokListDTO.java b/src/main/java/com/ripple/BE/post/dto/ToktokListDTO.java new file mode 100644 index 0000000..3bf199d --- /dev/null +++ b/src/main/java/com/ripple/BE/post/dto/ToktokListDTO.java @@ -0,0 +1,15 @@ +package com.ripple.BE.post.dto; + +import com.ripple.BE.post.domain.Post; +import java.util.List; +import org.springframework.data.domain.Page; + +public record ToktokListDTO(List toktokDTOList, int totalPage, int currentPage) { + + public static ToktokListDTO toToktokListDTO(Page postPage) { + return new ToktokListDTO( + postPage.getContent().stream().map(ToktokDTO::toToktokDTO).toList(), + postPage.getTotalPages(), + postPage.getNumber()); + } +} diff --git a/src/main/java/com/ripple/BE/post/dto/response/CommentResponse.java b/src/main/java/com/ripple/BE/post/dto/response/CommentResponse.java index cb14a84..865aefb 100644 --- a/src/main/java/com/ripple/BE/post/dto/response/CommentResponse.java +++ b/src/main/java/com/ripple/BE/post/dto/response/CommentResponse.java @@ -1,8 +1,11 @@ package com.ripple.BE.post.dto.response; import com.ripple.BE.global.utils.RelativeTimeFormatter; +import com.ripple.BE.image.domain.Image; +import com.ripple.BE.image.domain.S3Info; import com.ripple.BE.post.dto.CommentDTO; import java.util.List; +import java.util.Optional; public record CommentResponse( Long id, @@ -25,7 +28,10 @@ public static CommentResponse toCommentResponse(CommentDTO commentDTO) { commentDTO.likeCount(), commentDTO.commenter().id(), commentDTO.commenter().nickname(), - commentDTO.commenter().profileImage().getS3Info().getUrl(), + Optional.ofNullable(commentDTO.commenter().profileImage()) + .map(Image::getS3Info) + .map(S3Info::getUrl) + .orElse(null), commentDTO.isDeleted(), commentDTO.isAuthor(), commentDTO.isLiked(), diff --git a/src/main/java/com/ripple/BE/post/dto/response/PostResponse.java b/src/main/java/com/ripple/BE/post/dto/response/PostResponse.java index 88b679a..1cd265d 100644 --- a/src/main/java/com/ripple/BE/post/dto/response/PostResponse.java +++ b/src/main/java/com/ripple/BE/post/dto/response/PostResponse.java @@ -1,10 +1,13 @@ package com.ripple.BE.post.dto.response; import com.ripple.BE.global.utils.RelativeTimeFormatter; +import com.ripple.BE.image.domain.Image; +import com.ripple.BE.image.domain.S3Info; import com.ripple.BE.image.dto.response.ImageResponse; import com.ripple.BE.post.domain.type.PostType; import com.ripple.BE.post.dto.PostDTO; import java.util.List; +import java.util.Optional; public record PostResponse( Long id, @@ -28,7 +31,10 @@ public static PostResponse toPostResponse(PostDTO postDTO) { postDTO.id(), postDTO.title(), postDTO.author().nickname(), - postDTO.author().profileImage().getS3Info().getUrl(), + Optional.ofNullable(postDTO.author().profileImage()) + .map(Image::getS3Info) + .map(S3Info::getUrl) + .orElse(null), postDTO.content(), postDTO.type(), postDTO.likeCount(), diff --git a/src/main/java/com/ripple/BE/post/dto/response/ToktokPreviewListResponse.java b/src/main/java/com/ripple/BE/post/dto/response/ToktokPreviewListResponse.java index 9bb41e4..ca30838 100644 --- a/src/main/java/com/ripple/BE/post/dto/response/ToktokPreviewListResponse.java +++ b/src/main/java/com/ripple/BE/post/dto/response/ToktokPreviewListResponse.java @@ -1,13 +1,13 @@ package com.ripple.BE.post.dto.response; -import com.ripple.BE.post.dto.PostListDTO; +import com.ripple.BE.post.dto.ToktokListDTO; import java.util.List; public record ToktokPreviewListResponse(List toktokPreviewResponseList) { - public static ToktokPreviewListResponse toToktokPreviewListResponse(PostListDTO postListDTO) { + public static ToktokPreviewListResponse toToktokPreviewListResponse(ToktokListDTO toktokListDTO) { return new ToktokPreviewListResponse( - postListDTO.postDTOList().stream() + toktokListDTO.toktokDTOList().stream() .map(ToktokPreviewResponse::toToktokPreviewResponse) .toList()); } diff --git a/src/main/java/com/ripple/BE/post/dto/response/ToktokPreviewResponse.java b/src/main/java/com/ripple/BE/post/dto/response/ToktokPreviewResponse.java index 425f38e..78ea170 100644 --- a/src/main/java/com/ripple/BE/post/dto/response/ToktokPreviewResponse.java +++ b/src/main/java/com/ripple/BE/post/dto/response/ToktokPreviewResponse.java @@ -1,7 +1,7 @@ package com.ripple.BE.post.dto.response; import com.ripple.BE.global.utils.RelativeTimeFormatter; -import com.ripple.BE.post.dto.PostDTO; +import com.ripple.BE.post.dto.ToktokDTO; public record ToktokPreviewResponse( Long id, @@ -12,17 +12,17 @@ public record ToktokPreviewResponse( Boolean isScraped, String createdDate) { - public static ToktokPreviewResponse toToktokPreviewResponse(PostDTO postDTO) { + public static ToktokPreviewResponse toToktokPreviewResponse(ToktokDTO toktokDTO) { return new ToktokPreviewResponse( - postDTO.id(), - postDTO.title(), - postDTO.commentCount(), - postDTO.likeCount(), - postDTO.imageList().imageDTOList().isEmpty() + toktokDTO.id(), + toktokDTO.title(), + toktokDTO.commentCount(), + toktokDTO.likeCount(), + toktokDTO.imageList().imageDTOList().isEmpty() ? null - : postDTO.imageList().imageDTOList().get(0).url(), - postDTO.isScraped(), - RelativeTimeFormatter.formatRelativeTime(postDTO.usedDate().atStartOfDay())); + : toktokDTO.imageList().imageDTOList().get(0).url(), + toktokDTO.isScraped(), + RelativeTimeFormatter.formatRelativeTime(toktokDTO.usedDate().atStartOfDay())); } } diff --git a/src/main/java/com/ripple/BE/post/dto/response/ToktokResponse.java b/src/main/java/com/ripple/BE/post/dto/response/ToktokResponse.java index 9126767..a181682 100644 --- a/src/main/java/com/ripple/BE/post/dto/response/ToktokResponse.java +++ b/src/main/java/com/ripple/BE/post/dto/response/ToktokResponse.java @@ -2,7 +2,7 @@ import com.ripple.BE.global.utils.RelativeTimeFormatter; import com.ripple.BE.image.dto.ImageDTO; -import com.ripple.BE.post.dto.PostDTO; +import com.ripple.BE.post.dto.ToktokDTO; import java.util.List; public record ToktokResponse( @@ -16,17 +16,17 @@ public record ToktokResponse( boolean isLiked, CommentListResponse commentListResponse) { - public static ToktokResponse toToktokResponse(PostDTO postDTO) { + public static ToktokResponse toToktokResponse(ToktokDTO toktokDTO) { return new ToktokResponse( - postDTO.id(), - postDTO.title(), - postDTO.content(), - postDTO.commentCount(), - postDTO.imageList().imageDTOList().stream().map(ImageDTO::url).toList(), - RelativeTimeFormatter.formatRelativeTime(postDTO.usedDate().atStartOfDay()), - postDTO.isScraped(), - postDTO.isLiked(), - CommentListResponse.toCommentListResponse(postDTO.commentListDTO())); + toktokDTO.id(), + toktokDTO.title(), + toktokDTO.content(), + toktokDTO.commentCount(), + toktokDTO.imageList().imageDTOList().stream().map(ImageDTO::url).toList(), + RelativeTimeFormatter.formatRelativeTime(toktokDTO.usedDate().atStartOfDay()), + toktokDTO.isScraped(), + toktokDTO.isLiked(), + CommentListResponse.toCommentListResponse(toktokDTO.commentListDTO())); } } diff --git a/src/main/java/com/ripple/BE/post/exception/errorcode/PostErrorCode.java b/src/main/java/com/ripple/BE/post/exception/errorcode/PostErrorCode.java index 1888275..a13ae4d 100644 --- a/src/main/java/com/ripple/BE/post/exception/errorcode/PostErrorCode.java +++ b/src/main/java/com/ripple/BE/post/exception/errorcode/PostErrorCode.java @@ -14,6 +14,7 @@ public enum PostErrorCode implements ErrorCode { COMMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "Comment not found"), SCRAP_NOT_FOUND(HttpStatus.NOT_FOUND, "Scrap not found"), TOKTOK_NOT_FOUND(HttpStatus.NOT_FOUND, "TodayToktok not found"), + TOKTOK_SAVE_EXCEL_FILE_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "Toktok save excel file failed"), LIKE_ALREADY_EXISTS(HttpStatus.BAD_REQUEST, "Like already exists"), SCRAP_ALREADY_EXISTS(HttpStatus.BAD_REQUEST, "Scrap already exists"), diff --git a/src/main/java/com/ripple/BE/post/repository/post/PostRepository.java b/src/main/java/com/ripple/BE/post/repository/post/PostRepository.java index 6add863..350de7e 100644 --- a/src/main/java/com/ripple/BE/post/repository/post/PostRepository.java +++ b/src/main/java/com/ripple/BE/post/repository/post/PostRepository.java @@ -1,8 +1,11 @@ package com.ripple.BE.post.repository.post; import com.ripple.BE.post.domain.Post; +import com.ripple.BE.post.domain.type.PostType; +import io.lettuce.core.dynamic.annotation.Param; import jakarta.persistence.LockModeType; import java.util.Optional; +import java.util.Set; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Lock; import org.springframework.data.jpa.repository.Query; @@ -14,4 +17,7 @@ public interface PostRepository extends JpaRepository, PostRepositor @Lock(LockModeType.PESSIMISTIC_WRITE) @Query("SELECT p FROM Post p WHERE p.id = :id") Optional findByIdForUpdate(long id); + + @Query("SELECT p.title FROM Post p WHERE p.type = :type") + Set findExistingTitlesByType(@Param("type") PostType type); } diff --git a/src/main/java/com/ripple/BE/post/repository/post/PostRepositoryCustomImpl.java b/src/main/java/com/ripple/BE/post/repository/post/PostRepositoryCustomImpl.java index 1fd0d84..8290251 100644 --- a/src/main/java/com/ripple/BE/post/repository/post/PostRepositoryCustomImpl.java +++ b/src/main/java/com/ripple/BE/post/repository/post/PostRepositoryCustomImpl.java @@ -1,7 +1,5 @@ package com.ripple.BE.post.repository.post; -import static com.ripple.BE.news.domain.QNews.*; -import static com.ripple.BE.news.domain.QNewsScrap.*; import static com.ripple.BE.post.domain.QPost.post; import static com.ripple.BE.post.domain.QPostScrap.*; @@ -65,13 +63,10 @@ public List findPopularPosts() { @Override public Page searchNormalPosts(String keyword, Pageable pageable, long userId) { - BooleanExpression predicate = null; + BooleanExpression predicate = post.type.ne(PostType.ECONOMY_TALK); // 기본 조건 if (keyword != null && !keyword.trim().isEmpty()) { - predicate = - post.type - .ne(PostType.ECONOMY_TALK) - .and(post.title.contains(keyword).or(post.content.contains(keyword))); + predicate = predicate.and(post.title.contains(keyword).or(post.content.contains(keyword))); } List posts = getPostsWithScrapByPageable(pageable, predicate, PostSort.RECENT, userId); @@ -83,14 +78,12 @@ public Page searchNormalPosts(String keyword, Pageable pageable, long user @Override public Page searchUsedToktokPosts(String keyword, Pageable pageable, long userId) { - BooleanExpression predicate = null; + + BooleanExpression predicate = + post.type.eq(PostType.ECONOMY_TALK).and(post.usedDate.isNotNull()); // 기본 조건 if (keyword != null && !keyword.trim().isEmpty()) { - predicate = - post.type - .eq(PostType.ECONOMY_TALK) - .and(post.usedDate.isNotNull()) - .and(post.title.contains(keyword).or(post.content.contains(keyword))); + predicate = predicate.and(post.title.contains(keyword).or(post.content.contains(keyword))); } List posts = getToktoksWithScrapByPageable(pageable, predicate, PostSort.RECENT, userId); diff --git a/src/main/java/com/ripple/BE/post/service/ToktokAdminService.java b/src/main/java/com/ripple/BE/post/service/ToktokAdminService.java new file mode 100644 index 0000000..62ac0a3 --- /dev/null +++ b/src/main/java/com/ripple/BE/post/service/ToktokAdminService.java @@ -0,0 +1,97 @@ +package com.ripple.BE.post.service; + +import static com.ripple.BE.post.exception.errorcode.PostErrorCode.*; +import static com.ripple.BE.user.exception.errorcode.UserErrorCode.*; + +import com.ripple.BE.global.excel.ExcelUtils; +import com.ripple.BE.image.domain.Image; +import com.ripple.BE.image.exception.ImageException; +import com.ripple.BE.image.repository.ImageRepository; +import com.ripple.BE.image.s3.S3Uploader; +import com.ripple.BE.post.domain.Post; +import com.ripple.BE.post.domain.type.PostType; +import com.ripple.BE.post.dto.ToktokDTO; +import com.ripple.BE.post.repository.post.PostRepository; +import com.ripple.BE.term.exception.TermException; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Service +@Slf4j +@Transactional(readOnly = true) +public class ToktokAdminService { + + private static final String FILE_PATH = "static/excel/toktok.xlsx"; + private static final String TITLE_COLUMN = "title"; + private static final String IMAGE_COLUMN = "imageId"; + private static final int TOKTOK_SHEET_INDEX = 0; + + private final PostRepository postRepository; + private final ImageRepository imageRepository; + private final S3Uploader s3Uploader; + + @Transactional + public void createToktokByExcel() { + try { + List newToktokList = parseToktokFromExcel(); + + if (!newToktokList.isEmpty()) { + postRepository.saveAll(newToktokList); // 새로운 게시물만 저장 + } + + } catch (Exception e) { + log.error("경제 톡톡 엑셀 파일 저장 실패", e); + throw new TermException(TOKTOK_SAVE_EXCEL_FILE_FAILED); + } + } + + private List parseToktokFromExcel() throws Exception { + // 기존 '경제 톡톡' 게시물 제목 조회 + Set existingTitles = postRepository.findExistingTitlesByType(PostType.ECONOMY_TALK); + + List> excelDataList = + ExcelUtils.parseExcelFile(FILE_PATH, TOKTOK_SHEET_INDEX); + + return excelDataList.stream() + .map( + excelData -> { + String title = excelData.get(TITLE_COLUMN); + + // 기존 게시물은 무시 + if (existingTitles.contains(title)) { + return null; + } + + // 새로운 게시물 생성 + Post post = Post.toPostEntity(ToktokDTO.toToktokDTO(excelData)); + + // imageId가 존재하면 이미지 조회 및 추가 + String imageIdStr = excelData.get(IMAGE_COLUMN); + if (imageIdStr != null && !imageIdStr.isEmpty()) { + try { + Long imageId = Long.parseLong(imageIdStr); + Image image = + imageRepository + .findById(imageId) + .orElseThrow(() -> new ImageException(IMAGE_NOT_FOUND)); + + post.addImage(image); + } catch (NumberFormatException e) { + log.warn("잘못된 imageId 형식: {}", imageIdStr); + } + } + + return post; + }) + .filter(Objects::nonNull) // 기존 게시물 제외 + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/com/ripple/BE/post/service/ToktokService.java b/src/main/java/com/ripple/BE/post/service/ToktokService.java index d957be9..c3d3dfa 100644 --- a/src/main/java/com/ripple/BE/post/service/ToktokService.java +++ b/src/main/java/com/ripple/BE/post/service/ToktokService.java @@ -6,8 +6,8 @@ import com.ripple.BE.post.domain.Post; import com.ripple.BE.post.domain.type.PostSort; import com.ripple.BE.post.dto.CommentListDTO; -import com.ripple.BE.post.dto.PostDTO; -import com.ripple.BE.post.dto.PostListDTO; +import com.ripple.BE.post.dto.ToktokDTO; +import com.ripple.BE.post.dto.ToktokListDTO; import com.ripple.BE.post.exception.PostException; import com.ripple.BE.post.repository.comment.CommentRepository; import com.ripple.BE.post.repository.post.PostRepository; @@ -41,15 +41,15 @@ public class ToktokService { // 오늘의 경제톡톡 주제 미리보기 @Transactional(readOnly = true) - public PostDTO getTodayToktok() { + public ToktokDTO getTodayToktok() { Post toktok = findTodayToktok(); - return PostDTO.toPostDTO(toktok); + return ToktokDTO.toToktokDTO(toktok); } @Transactional(readOnly = true) - public PostDTO getToktok(final long id, final long userId) { + public ToktokDTO getToktok(final long id, final long userId) { Post post = postRepository.findById(id).orElseThrow(() -> new PostException(POST_NOT_FOUND)); if (post.getUsedDate() == null) { throw new PostException(POST_NOT_FOUND); @@ -61,18 +61,18 @@ public PostDTO getToktok(final long id, final long userId) { CommentListDTO commentListDTO = getCommentList(post, userId); - return PostDTO.toPostDTO(post, commentListDTO); + return ToktokDTO.toToktokDTO(post, commentListDTO); } @Transactional(readOnly = true) - public PostListDTO getToktoks(final int page, final PostSort sort, final long userId) { + public ToktokListDTO getToktoks(final int page, final PostSort sort, final long userId) { Pageable pageable = PageRequest.of(page, PAGE_SIZE); // 게시글 조회 (타입에 따른 필터링) Page postPage = postRepository.findUsedToktokPosts(pageable, sort, userId); - return PostListDTO.toPostListDTO(postPage); + return ToktokListDTO.toToktokListDTO(postPage); } private CommentListDTO getCommentList(final Post post, final long userId) { diff --git a/src/main/java/com/ripple/BE/search/controller/SearchController.java b/src/main/java/com/ripple/BE/search/controller/SearchController.java index 100b8ab..fb378c9 100644 --- a/src/main/java/com/ripple/BE/search/controller/SearchController.java +++ b/src/main/java/com/ripple/BE/search/controller/SearchController.java @@ -4,6 +4,7 @@ import com.ripple.BE.news.dto.NewsListDTO; import com.ripple.BE.news.dto.response.NewsListResponse; import com.ripple.BE.post.dto.PostListDTO; +import com.ripple.BE.post.dto.ToktokListDTO; import com.ripple.BE.post.dto.response.PostListResponse; import com.ripple.BE.post.dto.response.ToktokPreviewListResponse; import com.ripple.BE.search.dto.SearchKeywordListDTO; @@ -52,9 +53,11 @@ public ResponseEntity> searchToktoks( final @RequestParam(value = "keyword", required = false) String keyword, final @RequestParam(value = "page", defaultValue = "0") @PositiveOrZero int page) { - PostListDTO postListDTO = searchService.searchToktoks(keyword, page, currentUser.getId()); + ToktokListDTO toktokListDTO = searchService.searchToktoks(keyword, page, currentUser.getId()); + return ResponseEntity.status(HttpStatus.OK) - .body(ApiResponse.from(ToktokPreviewListResponse.toToktokPreviewListResponse(postListDTO))); + .body( + ApiResponse.from(ToktokPreviewListResponse.toToktokPreviewListResponse(toktokListDTO))); } @Operation(summary = "뉴스 검색", description = "뉴스를 검색합니다.") diff --git a/src/main/java/com/ripple/BE/search/service/SearchService.java b/src/main/java/com/ripple/BE/search/service/SearchService.java index a08b746..509a837 100644 --- a/src/main/java/com/ripple/BE/search/service/SearchService.java +++ b/src/main/java/com/ripple/BE/search/service/SearchService.java @@ -5,6 +5,7 @@ import com.ripple.BE.news.repository.news.NewsRepository; import com.ripple.BE.post.domain.Post; import com.ripple.BE.post.dto.PostListDTO; +import com.ripple.BE.post.dto.ToktokListDTO; import com.ripple.BE.post.repository.post.PostRepository; import com.ripple.BE.search.dto.SearchKeywordListDTO; import com.ripple.BE.term.domain.Term; @@ -52,13 +53,13 @@ public PostListDTO searchPosts(final String keyword, final int page, final long @Cacheable(value = "toktokSearch", key = "#keyword != null ? #keyword + #page : #page") @Transactional(readOnly = true) - public PostListDTO searchToktoks(final String keyword, final int page, final long userId) { + public ToktokListDTO searchToktoks(final String keyword, final int page, final long userId) { Pageable pageable = PageRequest.of(page, PAGE_SIZE); Page postPage = postRepository.searchUsedToktokPosts(keyword, pageable, userId); addRecentSearch(userId, keyword); - return PostListDTO.toPostListDTO(postPage); + return ToktokListDTO.toToktokListDTO(postPage); } @Cacheable(value = "newsSearch", key = "#keyword != null ? #keyword + #page : #page") diff --git a/src/main/resources/static/excel/toktok.xlsx b/src/main/resources/static/excel/toktok.xlsx new file mode 100644 index 0000000..6c0d147 Binary files /dev/null and b/src/main/resources/static/excel/toktok.xlsx differ