diff --git a/src/main/java/com/daon/onjung/account/domain/Store.java b/src/main/java/com/daon/onjung/account/domain/Store.java index 81a4339..c862104 100644 --- a/src/main/java/com/daon/onjung/account/domain/Store.java +++ b/src/main/java/com/daon/onjung/account/domain/Store.java @@ -74,7 +74,7 @@ public class Store { /* -------------------------------------------- */ /* One To One Mapping ------------------------ */ /* -------------------------------------------- */ - @OneToOne + @OneToOne(fetch = FetchType.LAZY) @JoinColumn(name = "owner_id") private Owner owner; diff --git a/src/main/java/com/daon/onjung/core/listener/AppEventListener.java b/src/main/java/com/daon/onjung/core/listener/AppEventListener.java index 4e2e018..703f054 100644 --- a/src/main/java/com/daon/onjung/core/listener/AppEventListener.java +++ b/src/main/java/com/daon/onjung/core/listener/AppEventListener.java @@ -5,6 +5,9 @@ import com.daon.onjung.core.exception.type.CommonException; import com.daon.onjung.event.repository.redis.ScheduledEventJobRepository; import com.daon.onjung.event.application.controller.consumer.EventSchedulerConsumerV1Controller; +import com.daon.onjung.suggestion.application.controller.consumer.BoardSchedulerConsumerV1Controller; +import com.daon.onjung.suggestion.domain.redis.ScheduledBoardJob; +import com.daon.onjung.suggestion.repository.redis.ScheduledBoardJobRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.quartz.*; @@ -21,6 +24,7 @@ public class AppEventListener { private final Scheduler scheduler; private final ScheduledEventJobRepository scheduledEventJobRepository; + private final ScheduledBoardJobRepository scheduledBoardJobRepository; @EventListener public void handleEventScheduled(ScheduledEventJob scheduledEventJob) { @@ -45,4 +49,27 @@ public void handleEventScheduled(ScheduledEventJob scheduledEventJob) { } } + @EventListener + public void handleBoardScheduled(ScheduledBoardJob scheduledBoardJob) { + JobDetail jobDetail = JobBuilder.newJob(BoardSchedulerConsumerV1Controller.class) + .withIdentity("boardJob-" + scheduledBoardJob.getJobId(), "boardGroup") + .usingJobData("boardId", scheduledBoardJob.getBoardId()) + .build(); + log.info("Job 등록 완료. boardId: {}", scheduledBoardJob.getBoardId()); + + Trigger trigger = TriggerBuilder.newTrigger() + .withIdentity("boardTrigger-" + scheduledBoardJob.getJobId(), "boardGroup") + .startAt(Timestamp.valueOf(scheduledBoardJob.getScheduledTime())) + .build(); + log.info("boardId {}에 대한 Trigger 등록 완료.", scheduledBoardJob.getBoardId()); + log.info("Trigger 시작 시간: {}", trigger.getStartTime()); + + try { + scheduler.scheduleJob(jobDetail, trigger); + scheduledBoardJobRepository.save(scheduledBoardJob); + } catch (SchedulerException e) { + throw new CommonException(ErrorCode.SCHEDULER_ERROR); + } + } + } diff --git a/src/main/java/com/daon/onjung/core/listener/SchedulerRecoveryListener.java b/src/main/java/com/daon/onjung/core/listener/SchedulerRecoveryListener.java index 2cf65bb..31612a2 100644 --- a/src/main/java/com/daon/onjung/core/listener/SchedulerRecoveryListener.java +++ b/src/main/java/com/daon/onjung/core/listener/SchedulerRecoveryListener.java @@ -3,6 +3,9 @@ import com.daon.onjung.event.domain.redis.ScheduledEventJob; import com.daon.onjung.event.repository.redis.ScheduledEventJobRepository; import com.daon.onjung.event.application.controller.consumer.EventSchedulerConsumerV1Controller; +import com.daon.onjung.suggestion.application.controller.consumer.BoardSchedulerConsumerV1Controller; +import com.daon.onjung.suggestion.domain.redis.ScheduledBoardJob; +import com.daon.onjung.suggestion.repository.redis.ScheduledBoardJobRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.quartz.*; @@ -20,36 +23,59 @@ public class SchedulerRecoveryListener implements CommandLineRunner { private final ScheduledEventJobRepository scheduledEventJobRepository; private final Scheduler scheduler; + private final ScheduledBoardJobRepository scheduledBoardJobRepository; @Override public void run(String... args) throws Exception { LocalDateTime now = LocalDateTime.now(); - List pendingJobs = scheduledEventJobRepository.findAll(); - log.info("미처리 Job 조회 완료. 조회된 Job 수: {}", pendingJobs.size()); - if (pendingJobs.isEmpty()) { + List pendingEventJobs = scheduledEventJobRepository.findAll(); + log.info("미처리 Event Job 조회 완료. 조회된 Job 수: {}", pendingEventJobs.size()); + if (!pendingEventJobs.isEmpty()) { + for (ScheduledEventJob job : pendingEventJobs) { + if (job == null) { + log.warn("조회된 Job 중 null 객체가 존재합니다."); + continue; + } + if (job.getScheduledTime() == null) { + log.warn("Job의 scheduledTime 값이 null입니다. eventId: {}", job.getEventId()); + continue; + } + if (job.getScheduledTime().isAfter(now)) { + log.info("미래 작업 스케줄러에 재등록. eventId: {}", job.getEventId()); + scheduleEventJob(job); + } else { + executeEventJobImmediately(job); + } + } + } + + List pendingBoardJobs = scheduledBoardJobRepository.findAll(); + log.info("미처리 Board Job 조회 완료. 조회된 Job 수: {}", pendingBoardJobs.size()); + + if (pendingBoardJobs.isEmpty()) { return; } - for (ScheduledEventJob job : pendingJobs) { + for (ScheduledBoardJob job : pendingBoardJobs) { if (job == null) { log.warn("조회된 Job 중 null 객체가 존재합니다."); continue; } if (job.getScheduledTime() == null) { - log.warn("Job의 scheduledTime 값이 null입니다. eventId: {}", job.getEventId()); + log.warn("Job의 scheduledTime 값이 null입니다. boardId: {}", job.getBoardId()); continue; } if (job.getScheduledTime().isAfter(now)) { - log.info("미래 작업 스케줄러에 재등록. eventId: {}", job.getEventId()); - scheduleJob(job); + log.info("미래 작업 스케줄러에 재등록. boardId: {}", job.getBoardId()); + scheduleBoardJob(job); } else { - executeJobImmediately(job); + executeBoardJobImmediately(job); } } } - private void scheduleJob(ScheduledEventJob job) throws SchedulerException { + private void scheduleEventJob(ScheduledEventJob job) throws SchedulerException { JobDetail jobDetail = JobBuilder.newJob(EventSchedulerConsumerV1Controller.class) - .withIdentity("eventJob-" + job.getEventId(), "eventGroup") + .withIdentity("eventJob-" + job.getJobId(), "eventGroup") .usingJobData("eventId", job.getEventId()) .build(); @@ -61,7 +87,7 @@ private void scheduleJob(ScheduledEventJob job) throws SchedulerException { scheduler.scheduleJob(jobDetail, trigger); } - private void executeJobImmediately(ScheduledEventJob scheduledEventJob) { + private void executeEventJobImmediately(ScheduledEventJob scheduledEventJob) { try { JobDetail jobDetail = JobBuilder.newJob(EventSchedulerConsumerV1Controller.class) .withIdentity("eventJob-" + scheduledEventJob.getJobId(), "eventGroup") @@ -86,4 +112,44 @@ private void executeJobImmediately(ScheduledEventJob scheduledEventJob) { // 실패 시 삭제하지 않음. 필요하면 재시도 큐에 추가하는 로직 고려 } } + + private void scheduleBoardJob(ScheduledBoardJob job) throws SchedulerException { + JobDetail jobDetail = JobBuilder.newJob(BoardSchedulerConsumerV1Controller.class) + .withIdentity("boardJob-" + job.getJobId(), "boardGroup") + .usingJobData("boardId", job.getBoardId()) + .build(); + + Trigger trigger = TriggerBuilder.newTrigger() + .withIdentity("trigger-" + job.getJobId(), "boardGroup") + .startAt(Timestamp.valueOf(job.getScheduledTime())) + .build(); + + scheduler.scheduleJob(jobDetail, trigger); + } + + private void executeBoardJobImmediately(ScheduledBoardJob scheduledBoardJob) { + try { + JobDetail jobDetail = JobBuilder.newJob(BoardSchedulerConsumerV1Controller.class) + .withIdentity("boardJob-" + scheduledBoardJob.getJobId(), "boardGroup") + .usingJobData("boardId", scheduledBoardJob.getBoardId()) + .build(); + log.info("Job 등록 완료. boardId: {}", scheduledBoardJob.getBoardId()); + + Trigger trigger = TriggerBuilder.newTrigger() + .withIdentity("boardTrigger-" + scheduledBoardJob.getJobId(), "boardGroup") + .startNow() + .build(); + + // 작업 스케줄링 + scheduler.scheduleJob(jobDetail, trigger); + + // 실행 성공 시 작업 삭제 + scheduledBoardJobRepository.delete(scheduledBoardJob); + + log.info("이전에 등록했지만 실행안된, 기간이 지난 Job을 즉시 실행함. boardId: {}", scheduledBoardJob.getBoardId()); + } catch (SchedulerException e) { + log.error("Job 즉시실행에 실패. boardId: {}", scheduledBoardJob.getBoardId(), e); + // 실패 시 삭제하지 않음. 필요하면 재시도 큐에 추가하는 로직 고려 + } + } } diff --git a/src/main/java/com/daon/onjung/event/application/controller/consumer/EventSchedulerConsumerV1Controller.java b/src/main/java/com/daon/onjung/event/application/controller/consumer/EventSchedulerConsumerV1Controller.java index 409189e..fd93367 100644 --- a/src/main/java/com/daon/onjung/event/application/controller/consumer/EventSchedulerConsumerV1Controller.java +++ b/src/main/java/com/daon/onjung/event/application/controller/consumer/EventSchedulerConsumerV1Controller.java @@ -1,6 +1,5 @@ package com.daon.onjung.event.application.controller.consumer; -import com.daon.onjung.event.repository.redis.ScheduledEventJobRepository; import com.daon.onjung.event.application.usecase.ProcessCompletedEventUseCase; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -13,18 +12,12 @@ public class EventSchedulerConsumerV1Controller implements Job { private final ProcessCompletedEventUseCase processCompletedEventUseCase; - private final ScheduledEventJobRepository scheduledEventJobRepository; @Override public void execute(JobExecutionContext context) throws JobExecutionException { // JobDataMap을 통해 eventId를 가져온다. Long eventId = context.getJobDetail().getJobDataMap().getLong("eventId"); - // eventId를 통해 ScheduledEventJob을 삭제 - scheduledEventJobRepository.findByEventId(eventId) - .ifPresent(scheduledEventJobRepository::delete); - log.info("ScheduledEventJob 삭제 완료. eventId: {}", eventId); - processCompletedEventUseCase.execute(eventId); } } diff --git a/src/main/java/com/daon/onjung/event/application/service/ProcessCompletedEventService.java b/src/main/java/com/daon/onjung/event/application/service/ProcessCompletedEventService.java index f25b90c..ccce5d6 100644 --- a/src/main/java/com/daon/onjung/event/application/service/ProcessCompletedEventService.java +++ b/src/main/java/com/daon/onjung/event/application/service/ProcessCompletedEventService.java @@ -4,7 +4,6 @@ import com.daon.onjung.account.domain.User; import com.daon.onjung.account.domain.type.EBankName; import com.daon.onjung.account.repository.mysql.UserRepository; -import com.daon.onjung.event.domain.service.ScheduledEventJobService; import com.daon.onjung.core.dto.CreateVirtualAccountResponseDto; import com.daon.onjung.core.exception.error.ErrorCode; import com.daon.onjung.core.exception.type.CommonException; @@ -16,9 +15,11 @@ import com.daon.onjung.event.domain.mysql.Event; import com.daon.onjung.event.domain.mysql.Ticket; import com.daon.onjung.event.domain.service.EventService; +import com.daon.onjung.event.domain.service.ScheduledEventJobService; import com.daon.onjung.event.domain.service.TicketService; import com.daon.onjung.event.repository.mysql.EventRepository; import com.daon.onjung.event.repository.mysql.TicketRepository; +import com.daon.onjung.event.repository.redis.ScheduledEventJobRepository; import com.daon.onjung.onjung.repository.mysql.DonationRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -40,6 +41,7 @@ public class ProcessCompletedEventService implements ProcessCompletedEventUseCas private final UserRepository userRepository; private final TicketRepository ticketRepository; private final DonationRepository donationRepository; + private final ScheduledEventJobRepository scheduledEventJobRepository; private final EventService eventService; private final TicketService ticketService; @@ -55,6 +57,11 @@ public class ProcessCompletedEventService implements ProcessCompletedEventUseCas @Transactional public void execute(Long eventId) { + // eventId를 통해 ScheduledEventJob을 삭제 + scheduledEventJobRepository.findByEventId(eventId) + .ifPresent(scheduledEventJobRepository::delete); + log.info("ScheduledEventJob 삭제 완료. eventId: {}", eventId); + // 이벤트 조회 Event currentEvent = eventRepository.findById(eventId) .orElseThrow(() -> new CommonException(ErrorCode.NOT_FOUND_RESOURCE)); diff --git a/src/main/java/com/daon/onjung/suggestion/application/controller/consumer/BoardSchedulerConsumerV1Controller.java b/src/main/java/com/daon/onjung/suggestion/application/controller/consumer/BoardSchedulerConsumerV1Controller.java new file mode 100644 index 0000000..3d4255d --- /dev/null +++ b/src/main/java/com/daon/onjung/suggestion/application/controller/consumer/BoardSchedulerConsumerV1Controller.java @@ -0,0 +1,24 @@ +package com.daon.onjung.suggestion.application.controller.consumer; + +import com.daon.onjung.suggestion.application.usecase.ProcessCompletedBoardUseCase; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.quartz.Job; +import org.quartz.JobExecutionContext; + +@RequiredArgsConstructor +@Slf4j +public class BoardSchedulerConsumerV1Controller implements Job { + + private final ProcessCompletedBoardUseCase processCompletedBoardUseCase; + + @Override + public void execute(JobExecutionContext context) { + // JobDataMap을 통해 boardId를 가져온다. + Long boardId = context.getJobDetail().getJobDataMap().getLong("boardId"); + + processCompletedBoardUseCase.execute(boardId); + } + + +} diff --git a/src/main/java/com/daon/onjung/suggestion/application/controller/consumer/CommentV1Consumer.java b/src/main/java/com/daon/onjung/suggestion/application/controller/consumer/CommentConsumerV1Controller.java similarity index 96% rename from src/main/java/com/daon/onjung/suggestion/application/controller/consumer/CommentV1Consumer.java rename to src/main/java/com/daon/onjung/suggestion/application/controller/consumer/CommentConsumerV1Controller.java index 7f2ada4..5a2b974 100644 --- a/src/main/java/com/daon/onjung/suggestion/application/controller/consumer/CommentV1Consumer.java +++ b/src/main/java/com/daon/onjung/suggestion/application/controller/consumer/CommentConsumerV1Controller.java @@ -6,8 +6,8 @@ import com.daon.onjung.core.exception.type.CommonException; import com.daon.onjung.suggestion.application.dto.request.CommentMessage; import com.daon.onjung.suggestion.application.dto.response.CreateCommentResponseDto; -import com.daon.onjung.suggestion.domain.Board; -import com.daon.onjung.suggestion.domain.Comment; +import com.daon.onjung.suggestion.domain.mysql.Board; +import com.daon.onjung.suggestion.domain.mysql.Comment; import com.daon.onjung.suggestion.domain.service.BoardService; import com.daon.onjung.suggestion.domain.service.CommentService; import com.daon.onjung.suggestion.repository.mysql.BoardRepository; @@ -21,7 +21,7 @@ @Service @RequiredArgsConstructor -public class CommentV1Consumer { +public class CommentConsumerV1Controller { private final BoardRepository boardRepository; private final CommentRepository commentRepository; diff --git a/src/main/java/com/daon/onjung/suggestion/application/controller/consumer/LikeV1Consumer.java b/src/main/java/com/daon/onjung/suggestion/application/controller/consumer/LikeConsumerV1Controller.java similarity index 92% rename from src/main/java/com/daon/onjung/suggestion/application/controller/consumer/LikeV1Consumer.java rename to src/main/java/com/daon/onjung/suggestion/application/controller/consumer/LikeConsumerV1Controller.java index 7222abc..45c623d 100644 --- a/src/main/java/com/daon/onjung/suggestion/application/controller/consumer/LikeV1Consumer.java +++ b/src/main/java/com/daon/onjung/suggestion/application/controller/consumer/LikeConsumerV1Controller.java @@ -5,9 +5,8 @@ import com.daon.onjung.core.exception.error.ErrorCode; import com.daon.onjung.core.exception.type.CommonException; import com.daon.onjung.suggestion.application.dto.request.LikeMessage; -import com.daon.onjung.suggestion.application.dto.response.CreateOrDeleteLikeResponseDto; -import com.daon.onjung.suggestion.domain.Board; -import com.daon.onjung.suggestion.domain.Like; +import com.daon.onjung.suggestion.domain.mysql.Board; +import com.daon.onjung.suggestion.domain.mysql.Like; import com.daon.onjung.suggestion.domain.service.BoardService; import com.daon.onjung.suggestion.domain.service.LikeService; import com.daon.onjung.suggestion.repository.mysql.BoardRepository; @@ -20,7 +19,7 @@ @Service @RequiredArgsConstructor -public class LikeV1Consumer { +public class LikeConsumerV1Controller { private final LikeRepository likeRepository; private final BoardRepository boardRepository; diff --git a/src/main/java/com/daon/onjung/suggestion/application/dto/response/CreateCommentResponseDto.java b/src/main/java/com/daon/onjung/suggestion/application/dto/response/CreateCommentResponseDto.java index 2128fc4..f485d13 100644 --- a/src/main/java/com/daon/onjung/suggestion/application/dto/response/CreateCommentResponseDto.java +++ b/src/main/java/com/daon/onjung/suggestion/application/dto/response/CreateCommentResponseDto.java @@ -3,7 +3,7 @@ import com.daon.onjung.account.domain.User; import com.daon.onjung.core.dto.SelfValidating; import com.daon.onjung.core.utility.DateTimeUtil; -import com.daon.onjung.suggestion.domain.Comment; +import com.daon.onjung.suggestion.domain.mysql.Comment; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Builder; import lombok.Getter; diff --git a/src/main/java/com/daon/onjung/suggestion/application/dto/response/ReadBoardDetailResponseDto.java b/src/main/java/com/daon/onjung/suggestion/application/dto/response/ReadBoardDetailResponseDto.java index 165d952..ca90ec2 100644 --- a/src/main/java/com/daon/onjung/suggestion/application/dto/response/ReadBoardDetailResponseDto.java +++ b/src/main/java/com/daon/onjung/suggestion/application/dto/response/ReadBoardDetailResponseDto.java @@ -3,7 +3,7 @@ import com.daon.onjung.account.domain.User; import com.daon.onjung.core.dto.SelfValidating; import com.daon.onjung.core.utility.DateTimeUtil; -import com.daon.onjung.suggestion.domain.Board; +import com.daon.onjung.suggestion.domain.mysql.Board; import com.fasterxml.jackson.annotation.JsonProperty; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotNull; @@ -11,6 +11,8 @@ import lombok.Builder; import lombok.Getter; +import java.time.LocalDate; + @Getter public class ReadBoardDetailResponseDto extends SelfValidating { @@ -93,10 +95,18 @@ public static class BoardInfoDto extends SelfValidating { @Size(min = 1, max = 500, message = "내용은 1자 이상 500자 이하여야 합니다") private final String content; + @JsonProperty("status") + @NotNull(message = "status는 null일 수 없습니다.") + private final String status; + @JsonProperty("posted_ago") @NotNull(message = "posted_ago는 null일 수 없습니다.") private final String postedAgo; + @JsonProperty("goal_count") + @NotNull(message = "goal_count는 null일 수 없습니다.") + private final Integer goalCount; + @JsonProperty("like_count") @NotNull(message = "like_count는 null일 수 없습니다.") @Min(value = 0, message = "like_count는 0 이상이어야 합니다.") @@ -110,16 +120,33 @@ public static class BoardInfoDto extends SelfValidating { @JsonProperty("is_liked") private final Boolean isLiked; + @JsonProperty("start_date") + @NotNull(message = "start_date는 null일 수 없습니다.") + private final String startDate; + + @JsonProperty("end_date") + @NotNull(message = "end_date는 null일 수 없습니다.") + private final String endDate; + + @JsonProperty("remaining_days") + @NotNull(message = "remaining_days는 null일 수 없습니다.") + private final Integer remainingDays; + @Builder - public BoardInfoDto(Long id, String imgUrl, String title, String content, String postedAgo, Integer likeCount, Integer commentCount, Boolean isLiked) { + public BoardInfoDto(Long id, String imgUrl, String title, String content, String status, String postedAgo, Integer goalCount, Integer likeCount, Integer commentCount, Boolean isLiked, String startDate, String endDate, Integer remainingDays) { this.id = id; this.imgUrl = imgUrl; this.title = title; this.content = content; + this.status = status; this.postedAgo = postedAgo; + this.goalCount = goalCount; this.likeCount = likeCount; this.commentCount = commentCount; this.isLiked = isLiked; + this.startDate = startDate; + this.endDate = endDate; + this.remainingDays = remainingDays; this.validateSelf(); } @@ -133,10 +160,15 @@ public static BoardInfoDto of( .imgUrl(board.getImgUrl()) .title(board.getTitle()) .content(board.getContent()) + .status(board.getStatus().toString()) .postedAgo(DateTimeUtil.calculatePostedAgo(board.getCreatedAt())) + .goalCount(100) // 기획상 100으로 고정 .likeCount(board.getLikeCount()) .commentCount(board.getCommentCount()) .isLiked(isLiked) + .startDate(DateTimeUtil.convertLocalDateToDotSeparatedDateTime(board.getStartDate())) + .endDate(DateTimeUtil.convertLocalDateToDotSeparatedDateTime(board.getEndDate())) + .remainingDays(DateTimeUtil.calculateDaysBetween(LocalDate.now(), board.getEndDate())) .build(); } } diff --git a/src/main/java/com/daon/onjung/suggestion/application/dto/response/ReadBoardOverviewResponseDto.java b/src/main/java/com/daon/onjung/suggestion/application/dto/response/ReadBoardOverviewResponseDto.java index 16f544b..6d31f80 100644 --- a/src/main/java/com/daon/onjung/suggestion/application/dto/response/ReadBoardOverviewResponseDto.java +++ b/src/main/java/com/daon/onjung/suggestion/application/dto/response/ReadBoardOverviewResponseDto.java @@ -2,7 +2,7 @@ import com.daon.onjung.core.dto.SelfValidating; import com.daon.onjung.core.utility.DateTimeUtil; -import com.daon.onjung.suggestion.domain.Board; +import com.daon.onjung.suggestion.domain.mysql.Board; import com.fasterxml.jackson.annotation.JsonProperty; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; @@ -38,13 +38,17 @@ public static class BoardListDto extends SelfValidating { @JsonProperty("img_url") private final String imgUrl; + @JsonProperty("status") + @NotNull(message = "status는 null일 수 없습니다.") + private final String status; + @JsonProperty("title_summary") - @Size(min = 1, max = 18, message = "제목은 1자 이상 18자 이하여야 합니다.") + @Size(min = 1, max = 30, message = "제목은 1자 이상 30자 이하여야 합니다.") @NotNull(message = "제목은 null일 수 없습니다.") private final String titleSummary; @JsonProperty("content_summary") - @Size(min = 1, max = 33, message = "내용은 1자 이상 33자 이하여야 합니다.") + @Size(min = 1, max = 60, message = "내용은 1자 이상 60자 이하여야 합니다.") @NotNull(message = "내용은 null일 수 없습니다.") private final String contentSummary; @@ -61,9 +65,10 @@ public static class BoardListDto extends SelfValidating { private final Integer commentCount; @Builder - public BoardListDto(Long id, String imgUrl, String titleSummary, String contentSummary, String postedAgo, Integer likeCount, Integer commentCount) { + public BoardListDto(Long id, String imgUrl, String status, String titleSummary, String contentSummary, String postedAgo, Integer likeCount, Integer commentCount) { this.id = id; this.imgUrl = imgUrl; + this.status = status; this.titleSummary = titleSummary; this.contentSummary = contentSummary; this.postedAgo = postedAgo; @@ -77,8 +82,9 @@ public static BoardListDto fromEntity(Board board) { return BoardListDto.builder() .id(board.getId()) .imgUrl(board.getImgUrl()) - .titleSummary(board.getTitle().length() > 15 ? board.getTitle().substring(0, 15) + "..." : board.getTitle()) - .contentSummary(board.getContent().length() > 30 ? board.getContent().substring(0, 30) + "..." : board.getContent()) + .status(board.getStatus().toString()) + .titleSummary(board.getTitle().length() > 30 ? board.getTitle().substring(0, 15) : board.getTitle()) + .contentSummary(board.getContent().length() > 30 ? board.getContent().substring(0, 60) : board.getContent()) .postedAgo(DateTimeUtil.calculatePostedAgo(board.getCreatedAt())) .likeCount(board.getLikeCount()) .commentCount(board.getCommentCount()) diff --git a/src/main/java/com/daon/onjung/suggestion/application/dto/response/ReadCommentOverviewResponseDto.java b/src/main/java/com/daon/onjung/suggestion/application/dto/response/ReadCommentOverviewResponseDto.java index 6c6f2e7..4c321d3 100644 --- a/src/main/java/com/daon/onjung/suggestion/application/dto/response/ReadCommentOverviewResponseDto.java +++ b/src/main/java/com/daon/onjung/suggestion/application/dto/response/ReadCommentOverviewResponseDto.java @@ -3,7 +3,7 @@ import com.daon.onjung.account.domain.User; import com.daon.onjung.core.dto.SelfValidating; import com.daon.onjung.core.utility.DateTimeUtil; -import com.daon.onjung.suggestion.domain.Comment; +import com.daon.onjung.suggestion.domain.mysql.Comment; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Builder; import lombok.Getter; diff --git a/src/main/java/com/daon/onjung/suggestion/application/service/CreateBoardService.java b/src/main/java/com/daon/onjung/suggestion/application/service/CreateBoardService.java index c57590e..8b36bfd 100644 --- a/src/main/java/com/daon/onjung/suggestion/application/service/CreateBoardService.java +++ b/src/main/java/com/daon/onjung/suggestion/application/service/CreateBoardService.java @@ -9,14 +9,17 @@ import com.daon.onjung.suggestion.application.dto.request.CreateBoardRequestDto; import com.daon.onjung.suggestion.application.dto.response.CreateBoardResponseDto; import com.daon.onjung.suggestion.application.usecase.CreateBoardUseCase; -import com.daon.onjung.suggestion.domain.Board; +import com.daon.onjung.suggestion.domain.mysql.Board; import com.daon.onjung.suggestion.domain.service.BoardService; +import com.daon.onjung.suggestion.domain.service.ScheduledBoardJobService; import com.daon.onjung.suggestion.repository.mysql.BoardRepository; import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; +import java.time.LocalDateTime; import java.util.UUID; @Service @@ -27,8 +30,10 @@ public class CreateBoardService implements CreateBoardUseCase { private final BoardRepository boardRepository; private final BoardService boardService; + private final ScheduledBoardJobService scheduledBoardJobService; private final S3Util s3Util; + private final ApplicationEventPublisher applicationEventPublisher; @Override @Transactional @@ -53,6 +58,15 @@ public CreateBoardResponseDto execute(UUID accountId, MultipartFile file, Create boardRepository.save(board); } + applicationEventPublisher.publishEvent( + scheduledBoardJobService.createScheduledJob( + board.getId(), +// board.getEndDate().plusDays(1).atStartOfDay() + LocalDateTime.now().plusMinutes(1) // 테스트용 1분 뒤 + ) + ); + + return CreateBoardResponseDto.of(board.getId()); } } diff --git a/src/main/java/com/daon/onjung/suggestion/application/service/CreateCommentService.java b/src/main/java/com/daon/onjung/suggestion/application/service/CreateCommentService.java index d8cd275..9b9a253 100644 --- a/src/main/java/com/daon/onjung/suggestion/application/service/CreateCommentService.java +++ b/src/main/java/com/daon/onjung/suggestion/application/service/CreateCommentService.java @@ -9,7 +9,7 @@ import com.daon.onjung.suggestion.application.dto.request.CreateCommentRequestDto; import com.daon.onjung.suggestion.application.dto.response.CreateCommentResponseDto; import com.daon.onjung.suggestion.application.usecase.CreateCommentUseCase; -import com.daon.onjung.suggestion.domain.Board; +import com.daon.onjung.suggestion.domain.mysql.Board; import com.daon.onjung.suggestion.repository.mysql.BoardRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; diff --git a/src/main/java/com/daon/onjung/suggestion/application/service/ProcessCompletedBoardService.java b/src/main/java/com/daon/onjung/suggestion/application/service/ProcessCompletedBoardService.java new file mode 100644 index 0000000..0b3aeb9 --- /dev/null +++ b/src/main/java/com/daon/onjung/suggestion/application/service/ProcessCompletedBoardService.java @@ -0,0 +1,45 @@ +package com.daon.onjung.suggestion.application.service; + +import com.daon.onjung.core.exception.error.ErrorCode; +import com.daon.onjung.core.exception.type.CommonException; +import com.daon.onjung.suggestion.application.usecase.ProcessCompletedBoardUseCase; +import com.daon.onjung.suggestion.domain.mysql.Board; +import com.daon.onjung.suggestion.domain.service.BoardService; +import com.daon.onjung.suggestion.repository.mysql.BoardRepository; +import com.daon.onjung.suggestion.repository.redis.ScheduledBoardJobRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Slf4j +public class ProcessCompletedBoardService implements ProcessCompletedBoardUseCase { + + private final ScheduledBoardJobRepository scheduledBoardJobRepository; + private final BoardRepository boardRepository; + + private final BoardService boardService; + + @Override + @Transactional + public void execute(Long boardId) { + + // boardId를 통해 ScheduledBoardJob을 삭제 + scheduledBoardJobRepository.findByBoardId(boardId) + .ifPresent(scheduledBoardJobRepository::delete); + log.info("ScheduledBoardJob 삭제 완료. boardId: {}", boardId); + + + // 게시글 조회 + Board board = boardRepository.findById(boardId) + .orElseThrow(() -> new CommonException(ErrorCode.NOT_FOUND_RESOURCE)); + + // 게시글 기간 완료 처리 + board = boardService.processCompletedBoard(board); + boardRepository.save(board); + + log.info("게시글 완료 처리 완료. boardId: {}", boardId); + } +} diff --git a/src/main/java/com/daon/onjung/suggestion/application/service/ReadBoardDetailService.java b/src/main/java/com/daon/onjung/suggestion/application/service/ReadBoardDetailService.java index ed068d1..3dfe468 100644 --- a/src/main/java/com/daon/onjung/suggestion/application/service/ReadBoardDetailService.java +++ b/src/main/java/com/daon/onjung/suggestion/application/service/ReadBoardDetailService.java @@ -6,7 +6,7 @@ import com.daon.onjung.core.exception.type.CommonException; import com.daon.onjung.suggestion.application.dto.response.ReadBoardDetailResponseDto; import com.daon.onjung.suggestion.application.usecase.ReadBoardDetailUseCase; -import com.daon.onjung.suggestion.domain.Board; +import com.daon.onjung.suggestion.domain.mysql.Board; import com.daon.onjung.suggestion.repository.mysql.BoardRepository; import com.daon.onjung.suggestion.repository.mysql.LikeRepository; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/com/daon/onjung/suggestion/application/service/ReadBoardOverviewService.java b/src/main/java/com/daon/onjung/suggestion/application/service/ReadBoardOverviewService.java index 58db165..d0b5fad 100644 --- a/src/main/java/com/daon/onjung/suggestion/application/service/ReadBoardOverviewService.java +++ b/src/main/java/com/daon/onjung/suggestion/application/service/ReadBoardOverviewService.java @@ -2,7 +2,7 @@ import com.daon.onjung.suggestion.application.dto.response.ReadBoardOverviewResponseDto; import com.daon.onjung.suggestion.application.usecase.ReadBoardOverviewUseCase; -import com.daon.onjung.suggestion.domain.Board; +import com.daon.onjung.suggestion.domain.mysql.Board; import com.daon.onjung.suggestion.repository.mysql.BoardRepository; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; diff --git a/src/main/java/com/daon/onjung/suggestion/application/service/ReadCommentOverviewService.java b/src/main/java/com/daon/onjung/suggestion/application/service/ReadCommentOverviewService.java index 3fd8349..637e719 100644 --- a/src/main/java/com/daon/onjung/suggestion/application/service/ReadCommentOverviewService.java +++ b/src/main/java/com/daon/onjung/suggestion/application/service/ReadCommentOverviewService.java @@ -6,7 +6,7 @@ import com.daon.onjung.core.exception.type.CommonException; import com.daon.onjung.suggestion.application.dto.response.ReadCommentOverviewResponseDto; import com.daon.onjung.suggestion.application.usecase.ReadCommentOverviewUseCase; -import com.daon.onjung.suggestion.domain.Comment; +import com.daon.onjung.suggestion.domain.mysql.Comment; import com.daon.onjung.suggestion.repository.mysql.CommentRepository; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; diff --git a/src/main/java/com/daon/onjung/suggestion/application/usecase/ProcessCompletedBoardUseCase.java b/src/main/java/com/daon/onjung/suggestion/application/usecase/ProcessCompletedBoardUseCase.java new file mode 100644 index 0000000..6df1d61 --- /dev/null +++ b/src/main/java/com/daon/onjung/suggestion/application/usecase/ProcessCompletedBoardUseCase.java @@ -0,0 +1,9 @@ +package com.daon.onjung.suggestion.application.usecase; + +import com.daon.onjung.core.annotation.bean.UseCase; + +@UseCase +public interface ProcessCompletedBoardUseCase { + + void execute(Long boardId); +} diff --git a/src/main/java/com/daon/onjung/suggestion/domain/Board.java b/src/main/java/com/daon/onjung/suggestion/domain/mysql/Board.java similarity index 78% rename from src/main/java/com/daon/onjung/suggestion/domain/Board.java rename to src/main/java/com/daon/onjung/suggestion/domain/mysql/Board.java index 959eae7..a080011 100644 --- a/src/main/java/com/daon/onjung/suggestion/domain/Board.java +++ b/src/main/java/com/daon/onjung/suggestion/domain/mysql/Board.java @@ -1,12 +1,14 @@ -package com.daon.onjung.suggestion.domain; +package com.daon.onjung.suggestion.domain.mysql; import com.daon.onjung.account.domain.User; +import com.daon.onjung.suggestion.domain.type.EStatus; import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import java.time.LocalDate; import java.time.LocalDateTime; @Entity @@ -39,6 +41,16 @@ public class Board { @Column(name = "comment_count", nullable = false) private Integer commentCount; + @Column(name = "start_date", nullable = false) + private LocalDate startDate; + + @Column(name = "end_date", nullable = false) + private LocalDate endDate; + + @Column(name = "status", nullable = false) + @Enumerated(EnumType.STRING) + private EStatus status; + @Version // Optimistic Lock 사용 private Long version; @@ -65,6 +77,9 @@ public Board(String title, String content, User user) { this.user = user; this.likeCount = 0; this.commentCount = 0; + this.startDate = LocalDate.now(); + this.endDate = LocalDate.now().plusDays(30); + this.status = EStatus.IN_PROGRESS; this.createdAt = LocalDateTime.now(); } @@ -76,8 +91,12 @@ public void updateLikeCount(Integer likeCount) { this.likeCount = likeCount; } - public void - updateCommentCount(Integer commentCount) { + public void updateCommentCount(Integer commentCount) { this.commentCount = commentCount; } + + public void updateStatus(EStatus status) { + this.status = status; + } + } diff --git a/src/main/java/com/daon/onjung/suggestion/domain/Comment.java b/src/main/java/com/daon/onjung/suggestion/domain/mysql/Comment.java similarity index 97% rename from src/main/java/com/daon/onjung/suggestion/domain/Comment.java rename to src/main/java/com/daon/onjung/suggestion/domain/mysql/Comment.java index 0eb6a7a..073a744 100644 --- a/src/main/java/com/daon/onjung/suggestion/domain/Comment.java +++ b/src/main/java/com/daon/onjung/suggestion/domain/mysql/Comment.java @@ -1,4 +1,4 @@ -package com.daon.onjung.suggestion.domain; +package com.daon.onjung.suggestion.domain.mysql; import com.daon.onjung.account.domain.User; import jakarta.persistence.*; diff --git a/src/main/java/com/daon/onjung/suggestion/domain/Like.java b/src/main/java/com/daon/onjung/suggestion/domain/mysql/Like.java similarity index 96% rename from src/main/java/com/daon/onjung/suggestion/domain/Like.java rename to src/main/java/com/daon/onjung/suggestion/domain/mysql/Like.java index aff8df9..148cc29 100644 --- a/src/main/java/com/daon/onjung/suggestion/domain/Like.java +++ b/src/main/java/com/daon/onjung/suggestion/domain/mysql/Like.java @@ -1,4 +1,4 @@ -package com.daon.onjung.suggestion.domain; +package com.daon.onjung.suggestion.domain.mysql; import com.daon.onjung.account.domain.User; import jakarta.persistence.*; diff --git a/src/main/java/com/daon/onjung/suggestion/domain/redis/ScheduledBoardJob.java b/src/main/java/com/daon/onjung/suggestion/domain/redis/ScheduledBoardJob.java new file mode 100644 index 0000000..1f1f459 --- /dev/null +++ b/src/main/java/com/daon/onjung/suggestion/domain/redis/ScheduledBoardJob.java @@ -0,0 +1,31 @@ +package com.daon.onjung.suggestion.domain.redis; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.Id; +import org.springframework.data.redis.core.RedisHash; +import org.springframework.data.redis.core.index.Indexed; + +import java.time.LocalDateTime; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@RedisHash(value = "ScheduledJob", timeToLive = 60 * 60 * 24 * 30) // 30일 +public class ScheduledBoardJob { + @Id + private String jobId; + + @Indexed + private Long boardId; + + private LocalDateTime scheduledTime; + + @Builder + public ScheduledBoardJob(Long boardId, LocalDateTime scheduledTime) { + this.jobId = boardId.toString(); + this.boardId = boardId; + this.scheduledTime = scheduledTime; + } +} diff --git a/src/main/java/com/daon/onjung/suggestion/domain/service/BoardService.java b/src/main/java/com/daon/onjung/suggestion/domain/service/BoardService.java index 266cc58..55f4bf1 100644 --- a/src/main/java/com/daon/onjung/suggestion/domain/service/BoardService.java +++ b/src/main/java/com/daon/onjung/suggestion/domain/service/BoardService.java @@ -1,7 +1,8 @@ package com.daon.onjung.suggestion.domain.service; import com.daon.onjung.account.domain.User; -import com.daon.onjung.suggestion.domain.Board; +import com.daon.onjung.suggestion.domain.mysql.Board; +import com.daon.onjung.suggestion.domain.type.EStatus; import org.springframework.stereotype.Service; @Service @@ -43,4 +44,16 @@ public Board subtractCommentCount(Board board) { board.updateCommentCount(board.getCommentCount() - 1); return board; } + + public Board processCompletedBoard(Board board) { + Integer likeCount = board.getLikeCount(); + + if (likeCount >= 100) { + board.updateStatus(EStatus.UNDER_REVIEW); + } else { + board.updateStatus(EStatus.EXPIRED); + } + + return board; + } } diff --git a/src/main/java/com/daon/onjung/suggestion/domain/service/CommentService.java b/src/main/java/com/daon/onjung/suggestion/domain/service/CommentService.java index 4fb6ca4..a9367d9 100644 --- a/src/main/java/com/daon/onjung/suggestion/domain/service/CommentService.java +++ b/src/main/java/com/daon/onjung/suggestion/domain/service/CommentService.java @@ -1,8 +1,8 @@ package com.daon.onjung.suggestion.domain.service; import com.daon.onjung.account.domain.User; -import com.daon.onjung.suggestion.domain.Board; -import com.daon.onjung.suggestion.domain.Comment; +import com.daon.onjung.suggestion.domain.mysql.Board; +import com.daon.onjung.suggestion.domain.mysql.Comment; import org.springframework.stereotype.Service; @Service diff --git a/src/main/java/com/daon/onjung/suggestion/domain/service/LikeService.java b/src/main/java/com/daon/onjung/suggestion/domain/service/LikeService.java index e824f3a..bec561c 100644 --- a/src/main/java/com/daon/onjung/suggestion/domain/service/LikeService.java +++ b/src/main/java/com/daon/onjung/suggestion/domain/service/LikeService.java @@ -1,8 +1,8 @@ package com.daon.onjung.suggestion.domain.service; import com.daon.onjung.account.domain.User; -import com.daon.onjung.suggestion.domain.Board; -import com.daon.onjung.suggestion.domain.Like; +import com.daon.onjung.suggestion.domain.mysql.Board; +import com.daon.onjung.suggestion.domain.mysql.Like; import org.springframework.stereotype.Service; @Service diff --git a/src/main/java/com/daon/onjung/suggestion/domain/service/ScheduledBoardJobService.java b/src/main/java/com/daon/onjung/suggestion/domain/service/ScheduledBoardJobService.java new file mode 100644 index 0000000..4ee7441 --- /dev/null +++ b/src/main/java/com/daon/onjung/suggestion/domain/service/ScheduledBoardJobService.java @@ -0,0 +1,20 @@ +package com.daon.onjung.suggestion.domain.service; + +import com.daon.onjung.suggestion.domain.redis.ScheduledBoardJob; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; + +@Service +public class ScheduledBoardJobService { + + public ScheduledBoardJob createScheduledJob( + Long boardId, + LocalDateTime scheduledTime + ) { + return ScheduledBoardJob.builder() + .boardId(boardId) + .scheduledTime(scheduledTime) + .build(); + } +} diff --git a/src/main/java/com/daon/onjung/suggestion/domain/type/.keep b/src/main/java/com/daon/onjung/suggestion/domain/type/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/java/com/daon/onjung/suggestion/domain/type/EStatus.java b/src/main/java/com/daon/onjung/suggestion/domain/type/EStatus.java new file mode 100644 index 0000000..2edb997 --- /dev/null +++ b/src/main/java/com/daon/onjung/suggestion/domain/type/EStatus.java @@ -0,0 +1,31 @@ +package com.daon.onjung.suggestion.domain.type; + +import com.daon.onjung.core.exception.error.ErrorCode; +import com.daon.onjung.core.exception.type.CommonException; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum EStatus { + IN_PROGRESS("진행중", "IN_PROGRESS"), + EXPIRED("만료", "EXPIRED"), + UNDER_REVIEW("검토중", "UNDER_REVIEW"), + REGISTERED("등록됨", "REGISTERED"), + REGISTRATION_FAILED("등록 실패", "REGISTRATION_FAILED") + ; + + private final String krName; + private final String enName; + + public static EStatus fromString(String value) { + return switch (value.toUpperCase()) { + case "IN_PROGRESS" -> IN_PROGRESS; + case "EXPIRED" -> EXPIRED; + case "UNDER_REVIEW" -> UNDER_REVIEW; + case "REGISTERED" -> REGISTERED; + case "REGISTRATION_FAILED" -> REGISTRATION_FAILED; + default -> throw new CommonException(ErrorCode.INVALID_ARGUMENT); + }; + } +} diff --git a/src/main/java/com/daon/onjung/suggestion/repository/mysql/BoardRepository.java b/src/main/java/com/daon/onjung/suggestion/repository/mysql/BoardRepository.java index c282514..c712976 100644 --- a/src/main/java/com/daon/onjung/suggestion/repository/mysql/BoardRepository.java +++ b/src/main/java/com/daon/onjung/suggestion/repository/mysql/BoardRepository.java @@ -1,6 +1,6 @@ package com.daon.onjung.suggestion.repository.mysql; -import com.daon.onjung.suggestion.domain.Board; +import com.daon.onjung.suggestion.domain.mysql.Board; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; diff --git a/src/main/java/com/daon/onjung/suggestion/repository/mysql/CommentRepository.java b/src/main/java/com/daon/onjung/suggestion/repository/mysql/CommentRepository.java index 42a2d7e..9f08d54 100644 --- a/src/main/java/com/daon/onjung/suggestion/repository/mysql/CommentRepository.java +++ b/src/main/java/com/daon/onjung/suggestion/repository/mysql/CommentRepository.java @@ -1,7 +1,7 @@ package com.daon.onjung.suggestion.repository.mysql; -import com.daon.onjung.suggestion.domain.Board; -import com.daon.onjung.suggestion.domain.Comment; +import com.daon.onjung.suggestion.domain.mysql.Board; +import com.daon.onjung.suggestion.domain.mysql.Comment; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; diff --git a/src/main/java/com/daon/onjung/suggestion/repository/mysql/LikeRepository.java b/src/main/java/com/daon/onjung/suggestion/repository/mysql/LikeRepository.java index 1d84903..cb8d6dc 100644 --- a/src/main/java/com/daon/onjung/suggestion/repository/mysql/LikeRepository.java +++ b/src/main/java/com/daon/onjung/suggestion/repository/mysql/LikeRepository.java @@ -1,8 +1,8 @@ package com.daon.onjung.suggestion.repository.mysql; import com.daon.onjung.account.domain.User; -import com.daon.onjung.suggestion.domain.Board; -import com.daon.onjung.suggestion.domain.Like; +import com.daon.onjung.suggestion.domain.mysql.Board; +import com.daon.onjung.suggestion.domain.mysql.Like; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; diff --git a/src/main/java/com/daon/onjung/suggestion/repository/redis/ScheduledBoardJobRepository.java b/src/main/java/com/daon/onjung/suggestion/repository/redis/ScheduledBoardJobRepository.java new file mode 100644 index 0000000..73e7f34 --- /dev/null +++ b/src/main/java/com/daon/onjung/suggestion/repository/redis/ScheduledBoardJobRepository.java @@ -0,0 +1,15 @@ +package com.daon.onjung.suggestion.repository.redis; + +import com.daon.onjung.suggestion.domain.redis.ScheduledBoardJob; +import org.jetbrains.annotations.NotNull; +import org.springframework.data.repository.CrudRepository; + +import java.util.List; +import java.util.Optional; + +public interface ScheduledBoardJobRepository extends CrudRepository { + + @NotNull List findAll(); + + Optional findByBoardId(Long boardId); +}