diff --git a/build.gradle b/build.gradle index c9a03190..909410ed 100644 --- a/build.gradle +++ b/build.gradle @@ -48,9 +48,11 @@ dependencies { // Repositories implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.amqp:spring-rabbit:3.1.1' + implementation 'org.hibernate:hibernate-core:6.4.4.Final' + implementation 'mysql:mysql-connector-java:8.0.33' runtimeOnly 'com.h2database:h2' - runtimeOnly 'com.mysql:mysql-connector-j:8.0.31' + runtimeOnly 'com.mysql:mysql-connector-j:8.2.0' // Validations implementation 'org.springframework.boot:spring-boot-starter-validation' @@ -88,6 +90,7 @@ dependencies { testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc' testImplementation 'org.springframework.security:spring-security-test' testImplementation 'org.mockito:mockito-inline:5.2.0' + testImplementation 'org.springframework.batch:spring-batch-test' // jwt decode implementation 'org.bouncycastle:bcprov-jdk15on:1.69' @@ -109,6 +112,10 @@ dependencies { // tink implementation 'com.google.crypto.tink:tink-android:1.4.0-rc1' implementation 'com.google.crypto.tink:apps-rewardedads:1.10.0' + + // spring batch + implementation 'org.springframework.boot:spring-boot-starter-batch' + } asciidoctor { diff --git a/src/main/java/com/yello/server/ServerApplication.java b/src/main/java/com/yello/server/ServerApplication.java index 91b6def6..59d267b5 100644 --- a/src/main/java/com/yello/server/ServerApplication.java +++ b/src/main/java/com/yello/server/ServerApplication.java @@ -1,10 +1,14 @@ package com.yello.server; +import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; +import org.springframework.batch.core.configuration.support.DefaultBatchConfiguration; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; @SpringBootApplication +@ConditionalOnMissingBean(value = DefaultBatchConfiguration.class, annotation = EnableBatchProcessing.class) public class ServerApplication { public static void main(String[] args) { diff --git a/src/main/java/com/yello/server/domain/authorization/service/AuthService.java b/src/main/java/com/yello/server/domain/authorization/service/AuthService.java index 40140383..d995dd79 100644 --- a/src/main/java/com/yello/server/domain/authorization/service/AuthService.java +++ b/src/main/java/com/yello/server/domain/authorization/service/AuthService.java @@ -127,6 +127,7 @@ public void recommendUser(String recommendYelloId, String userYelloId) { ZonedDateTime.now(GlobalZoneId).format(ISO_OFFSET_DATE_TIME), recommendedUser )); + notificationService.sendRecommendSignupAndGetTicketNotification(recommendedUser); } notificationService.sendRecommendNotification(user, recommendedUser); diff --git a/src/main/java/com/yello/server/domain/user/repository/UserJpaRepository.java b/src/main/java/com/yello/server/domain/user/repository/UserJpaRepository.java index 01d64b09..5e6d6df7 100644 --- a/src/main/java/com/yello/server/domain/user/repository/UserJpaRepository.java +++ b/src/main/java/com/yello/server/domain/user/repository/UserJpaRepository.java @@ -133,4 +133,5 @@ List findAllByOtherGroupContainingYelloId(@Param("groupName") String group @Query("select u from User u " + "where LOWER(u.name) like LOWER(CONCAT('%', :name, '%'))") Page findAllByNameContaining(Pageable pageable, @Param("name") String name); + } diff --git a/src/main/java/com/yello/server/domain/user/repository/UserRepository.java b/src/main/java/com/yello/server/domain/user/repository/UserRepository.java index 2ef67768..0c9d9256 100644 --- a/src/main/java/com/yello/server/domain/user/repository/UserRepository.java +++ b/src/main/java/com/yello/server/domain/user/repository/UserRepository.java @@ -75,4 +75,5 @@ List findAllByOtherGroupContainingYelloId(String groupName, String keyword Page findAllByNameContaining(Pageable pageable, String name); void delete(User user); + } diff --git a/src/main/java/com/yello/server/domain/user/repository/UserRepositoryImpl.java b/src/main/java/com/yello/server/domain/user/repository/UserRepositoryImpl.java index aa1e8df4..7a9a85af 100644 --- a/src/main/java/com/yello/server/domain/user/repository/UserRepositoryImpl.java +++ b/src/main/java/com/yello/server/domain/user/repository/UserRepositoryImpl.java @@ -192,4 +192,6 @@ public Page findAllByNameContaining(Pageable pageable, String name) { public void delete(User user) { userJpaRepository.delete(user); } + + } diff --git a/src/main/java/com/yello/server/domain/vote/service/VoteManagerImpl.java b/src/main/java/com/yello/server/domain/vote/service/VoteManagerImpl.java index 37c79d98..a752daf9 100644 --- a/src/main/java/com/yello/server/domain/vote/service/VoteManagerImpl.java +++ b/src/main/java/com/yello/server/domain/vote/service/VoteManagerImpl.java @@ -1,19 +1,5 @@ package com.yello.server.domain.vote.service; -import static com.yello.server.global.common.ErrorCode.DUPLICATE_VOTE_EXCEPTION; -import static com.yello.server.global.common.ErrorCode.INVALID_VOTE_EXCEPTION; -import static com.yello.server.global.common.ErrorCode.LACK_POINT_EXCEPTION; -import static com.yello.server.global.common.ErrorCode.LACK_USER_EXCEPTION; -import static com.yello.server.global.common.factory.WeightedRandomFactory.randomPoint; -import static com.yello.server.global.common.util.ConstantUtil.KEYWORD_HINT_POINT; -import static com.yello.server.global.common.util.ConstantUtil.NAME_HINT_DEFAULT; -import static com.yello.server.global.common.util.ConstantUtil.NAME_HINT_POINT; -import static com.yello.server.global.common.util.ConstantUtil.NO_FRIEND_COUNT; -import static com.yello.server.global.common.util.ConstantUtil.RANDOM_COUNT; -import static com.yello.server.global.common.util.ConstantUtil.VOTE_COUNT; -import static com.yello.server.global.common.util.ConstantUtil.YELLO_FEMALE; -import static com.yello.server.global.common.util.ConstantUtil.YELLO_MALE; - import com.yello.server.domain.friend.dto.response.FriendShuffleResponse; import com.yello.server.domain.friend.entity.Friend; import com.yello.server.domain.friend.exception.FriendException; @@ -33,17 +19,19 @@ import com.yello.server.domain.vote.exception.VoteForbiddenException; import com.yello.server.domain.vote.exception.VoteNotFoundException; import com.yello.server.domain.vote.repository.VoteRepository; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.ThreadLocalRandom; -import java.util.stream.IntStream; import lombok.Builder; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; +import java.util.*; +import java.util.concurrent.ThreadLocalRandom; +import java.util.stream.IntStream; + +import static com.yello.server.global.common.ErrorCode.*; +import static com.yello.server.global.common.factory.WeightedRandomFactory.randomPoint; +import static com.yello.server.global.common.util.ConstantUtil.*; + @Builder @Component @RequiredArgsConstructor @@ -66,27 +54,32 @@ public List createVotes(Long senderId, List voteAnswers) { final User sender = userRepository.getById(senderId); IntStream.range(0, voteAnswers.size()) - .forEach(index -> { - VoteAnswer currentVote = voteAnswers.get(index); + .filter(index -> { + User receiver = userRepository.getById(voteAnswers.get(index).friendId()); + return Objects.isNull(receiver.getDeletedAt()); + }) + .forEach(index -> { + VoteAnswer currentVote = voteAnswers.get(index); + + if (isDuplicatedVote(index, voteAnswers)) { + throw new VoteForbiddenException(DUPLICATE_VOTE_EXCEPTION); + } - if (isDuplicatedVote(index, voteAnswers)) { - throw new VoteForbiddenException(DUPLICATE_VOTE_EXCEPTION); - } + User receiver = userRepository.getById(currentVote.friendId()); + Question question = questionRepository.getById(currentVote.questionId()); - User receiver = userRepository.getById(currentVote.friendId()); - Question question = questionRepository.getById(currentVote.questionId()); - Vote newVote = Vote.createVote( - currentVote.keywordName(), - sender, - receiver, - question, - currentVote.colorIndex() - ); + Vote newVote = Vote.createVote( + currentVote.keywordName(), + sender, + receiver, + question, + currentVote.colorIndex() + ); - Vote savedVote = voteRepository.save(newVote); - votes.add(savedVote); - }); + Vote savedVote = voteRepository.save(newVote); + votes.add(savedVote); + }); return votes; } @@ -97,15 +90,15 @@ public List generateVoteQuestion(User user, List QuestionForVoteResponse.builder() - .friendList(getShuffledFriends(user)) - .keywordList(getShuffledKeywords(question)) - .question(QuestionVO.of(question)) - .questionPoint(randomPoint()) - .subscribe(user.getSubscribe().toString()) - .build()) - .limit(VOTE_COUNT) - .toList(); + .map(question -> QuestionForVoteResponse.builder() + .friendList(getShuffledFriends(user)) + .keywordList(getShuffledKeywords(question)) + .question(QuestionVO.of(question)) + .questionPoint(randomPoint()) + .subscribe(user.getSubscribe().toString()) + .build()) + .limit(VOTE_COUNT) + .toList(); } @Override @@ -146,18 +139,18 @@ public KeywordCheckResponse useKeywordHint(User user, Vote vote) { public void makeGreetingVote(User user) { final User sender = userManager.getOfficialUser(user.getGender()); final Question greetingQuestion = questionRepository.findByQuestionContent( - null, - GREETING_NAME_FOOT, - null, - GREETING_KEYWORD_FOOT + null, + GREETING_NAME_FOOT, + null, + GREETING_KEYWORD_FOOT ).orElseGet(() -> - questionRepository.save( - Question.of( - null, - GREETING_NAME_FOOT, - null, - GREETING_KEYWORD_FOOT) - ) + questionRepository.save( + Question.of( + null, + GREETING_NAME_FOOT, + null, + GREETING_KEYWORD_FOOT) + ) ); voteRepository.save(createFirstVote(sender, user, greetingQuestion)); @@ -177,19 +170,19 @@ public List getShuffledFriends(User user) { if (friends.size() > NO_FRIEND_COUNT && friends.size() < RANDOM_COUNT) { return friendList.stream() - .map(FriendShuffleResponse::of) - .toList(); + .map(FriendShuffleResponse::of) + .toList(); } return friendList.stream() - .map(FriendShuffleResponse::of) - .limit(RANDOM_COUNT) - .toList(); + .map(FriendShuffleResponse::of) + .limit(RANDOM_COUNT) + .toList(); } private boolean isDuplicatedVote(int index, List voteAnswers) { return index > 0 && voteAnswers.get(index - 1).questionId() - .equals(voteAnswers.get(index).questionId()); + .equals(voteAnswers.get(index).questionId()); } private List getShuffledKeywords(Question question) { @@ -198,9 +191,9 @@ private List getShuffledKeywords(Question question) { Collections.shuffle(keywordList); return keywordList.stream() - .map(Keyword::getKeywordName) - .limit(RANDOM_COUNT) - .toList(); + .map(Keyword::getKeywordName) + .limit(RANDOM_COUNT) + .toList(); } private Vote createFirstVote(User sender, User receiver, Question question) { @@ -208,14 +201,14 @@ private Vote createFirstVote(User sender, User receiver, Question question) { final String answer = "널 기다렸어"; return Vote.builder() - .answer(answer) - .nameHint(-3) - .isAnswerRevealed(true) - .isRead(false) - .sender(sender) - .receiver(receiver) - .question(question) - .colorIndex(random.nextInt(12) + 1) - .build(); + .answer(answer) + .nameHint(-3) + .isAnswerRevealed(true) + .isRead(false) + .sender(sender) + .receiver(receiver) + .question(question) + .colorIndex(random.nextInt(12) + 1) + .build(); } } diff --git a/src/main/java/com/yello/server/domain/vote/service/VoteService.java b/src/main/java/com/yello/server/domain/vote/service/VoteService.java index 7c23eb3b..afcdc96a 100644 --- a/src/main/java/com/yello/server/domain/vote/service/VoteService.java +++ b/src/main/java/com/yello/server/domain/vote/service/VoteService.java @@ -45,6 +45,7 @@ import com.yello.server.domain.vote.exception.VoteForbiddenException; import com.yello.server.domain.vote.exception.VoteNotFoundException; import com.yello.server.domain.vote.repository.VoteRepository; +import com.yello.server.infrastructure.firebase.service.NotificationService; import com.yello.server.infrastructure.rabbitmq.service.ProducerService; import java.time.LocalDateTime; import java.util.List; @@ -73,6 +74,7 @@ public class VoteService { private final VoteManager voteManager; private final ProducerService producerService; + private final NotificationService notificationService; public VoteListResponse findAllVotes(Long userId, Pageable pageable) { Integer totalCount = voteRepository.countAllByReceiverUserId(userId); @@ -104,6 +106,10 @@ public VoteDetailResponse findVoteById(Long voteId, Long userId) { final Vote vote = voteRepository.getById(voteId); final User user = userRepository.getById(userId); + if(!vote.getIsRead()) { + notificationService.sendOpenVoteNotification(vote.getSender()); + } + vote.read(); return VoteDetailResponse.of(vote, user); } diff --git a/src/main/java/com/yello/server/infrastructure/batch/ChunkProcessor.java b/src/main/java/com/yello/server/infrastructure/batch/ChunkProcessor.java new file mode 100644 index 00000000..0aae1a7a --- /dev/null +++ b/src/main/java/com/yello/server/infrastructure/batch/ChunkProcessor.java @@ -0,0 +1,20 @@ +package com.yello.server.infrastructure.batch; + + +import com.yello.server.domain.user.entity.User; +import lombok.RequiredArgsConstructor; +import org.springframework.batch.item.ItemProcessor; +import org.springframework.context.annotation.Configuration; + +@RequiredArgsConstructor +@Configuration +public class ChunkProcessor { + public ItemProcessor lunchEventProcessor() { + ItemProcessor item = user -> { + System.out.println(user.getId() + ", " + user.getName() + " dds121212"); + return user; + }; + return item; + } + +} diff --git a/src/main/java/com/yello/server/infrastructure/batch/ChunkReader.java b/src/main/java/com/yello/server/infrastructure/batch/ChunkReader.java new file mode 100644 index 00000000..1351b38e --- /dev/null +++ b/src/main/java/com/yello/server/infrastructure/batch/ChunkReader.java @@ -0,0 +1,105 @@ +package com.yello.server.infrastructure.batch; + +import com.yello.server.domain.user.entity.User; +import com.yello.server.domain.user.repository.UserJpaRepository; +import jakarta.persistence.EntityManagerFactory; +import lombok.RequiredArgsConstructor; +import org.springframework.batch.core.configuration.annotation.StepScope; +import org.springframework.batch.item.ItemReader; +import org.springframework.batch.item.data.RepositoryItemReader; +import org.springframework.batch.item.data.builder.RepositoryItemReaderBuilder; +import org.springframework.batch.item.database.*; +import org.springframework.batch.item.database.builder.JdbcCursorItemReaderBuilder; +import org.springframework.batch.item.database.builder.JdbcPagingItemReaderBuilder; +import org.springframework.batch.item.database.builder.JpaPagingItemReaderBuilder; +import org.springframework.batch.item.database.support.SqlPagingQueryProviderFactoryBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.domain.Sort; +import org.springframework.jdbc.core.BeanPropertyRowMapper; + +import javax.sql.DataSource; +import java.util.Collections; + +@Configuration +@RequiredArgsConstructor +public class ChunkReader { + + private final UserJpaRepository userRepository; + private final EntityManagerFactory entityManagerFactory; + private final DataSource dataSource; + + @Bean + @StepScope + public RepositoryItemReader usersDataRepositoryItemReader() { + + return new RepositoryItemReaderBuilder() + .name("userDataReader") + .repository(userRepository) + .methodName("findAllByPageable") + .pageSize(100) + .sorts(Collections.singletonMap("id", Sort.Direction.ASC)) + .build(); + } + + public JdbcCursorItemReader jdbcCursorItemReader() { + return new JdbcCursorItemReaderBuilder() + .fetchSize(10) + .dataSource(dataSource) + .rowMapper(new BeanPropertyRowMapper<>(User.class)) + .sql("SELECT u.id, u.name FROM user u WHERE u.deleted_at is NULL ORDER BY u.id") + .name("jdbcCursorItemReader") + .build(); + } + + @Bean + @StepScope + public ItemReader userDataItemReader() { + return new JpaPagingItemReaderBuilder() + .name("exampleItemReader") + .entityManagerFactory(this.entityManagerFactory) + .pageSize(10) + .queryString("SELECT u FROM User u") + .build(); + } + + @Bean + @StepScope + public JpaPagingItemReader userDataJpaPagingItemReader() { + + return new JpaPagingItemReaderBuilder() + .name("userDataReader") + .pageSize(100) + .queryString("SELECT u FROM User u WHERE deletedAt is NULL ORDER BY id") + .entityManagerFactory(entityManagerFactory) + .build(); + } + + @Bean + @StepScope + public JdbcPagingItemReader userDataJdbcPagingItemReader() throws Exception { + + return new JdbcPagingItemReaderBuilder() + .pageSize(100) + .fetchSize(100) + .dataSource(dataSource) + .queryProvider(createUserDataQueryProvider()) + .rowMapper(new UserRowMapper()) + .name("jdbcPagingItemReader") + .build(); + } + + @Bean + public PagingQueryProvider createUserDataQueryProvider() throws Exception { + SqlPagingQueryProviderFactoryBean queryProvider = new SqlPagingQueryProviderFactoryBean(); + queryProvider.setDataSource(dataSource); + queryProvider.setSelectClause("*"); + queryProvider.setFromClause("from user"); + queryProvider.setWhereClause("where deleted_at is null"); + + queryProvider.setSortKeys(Collections.singletonMap("id", Order.ASCENDING)); + + return queryProvider.getObject(); + + } +} diff --git a/src/main/java/com/yello/server/infrastructure/batch/ChunkWriter.java b/src/main/java/com/yello/server/infrastructure/batch/ChunkWriter.java new file mode 100644 index 00000000..3e489eb9 --- /dev/null +++ b/src/main/java/com/yello/server/infrastructure/batch/ChunkWriter.java @@ -0,0 +1,22 @@ +package com.yello.server.infrastructure.batch; + +import com.yello.server.domain.user.entity.User; +import com.yello.server.infrastructure.firebase.service.NotificationService; +import lombok.RequiredArgsConstructor; +import org.springframework.batch.core.configuration.annotation.StepScope; +import org.springframework.batch.item.ItemWriter; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + + +@RequiredArgsConstructor +@Configuration +public class ChunkWriter { + private final NotificationService notificationService; + + @Bean + @StepScope + public ItemWriter lunchEventWriter() { + return items -> items.forEach(notificationService::sendLunchEventNotification); + } +} diff --git a/src/main/java/com/yello/server/infrastructure/batch/JobConfiguration.java b/src/main/java/com/yello/server/infrastructure/batch/JobConfiguration.java new file mode 100644 index 00000000..b15623ee --- /dev/null +++ b/src/main/java/com/yello/server/infrastructure/batch/JobConfiguration.java @@ -0,0 +1,26 @@ +package com.yello.server.infrastructure.batch; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.job.builder.JobBuilder; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.PlatformTransactionManager; + +@Slf4j +@Configuration +@RequiredArgsConstructor +public class JobConfiguration { + private final StepConfiguration stepConfiguration; + + @Bean + public Job lunchEventJob(JobRepository jobRepository, PlatformTransactionManager transactionManager) throws Exception { + return new JobBuilder("lunchEventJob", jobRepository) + .start(stepConfiguration.lunchEventAlarmStep(jobRepository, transactionManager)) + .build(); + } + + +} diff --git a/src/main/java/com/yello/server/infrastructure/batch/StepConfiguration.java b/src/main/java/com/yello/server/infrastructure/batch/StepConfiguration.java new file mode 100644 index 00000000..6a29e882 --- /dev/null +++ b/src/main/java/com/yello/server/infrastructure/batch/StepConfiguration.java @@ -0,0 +1,33 @@ +package com.yello.server.infrastructure.batch; + +import com.yello.server.domain.user.entity.User; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.configuration.annotation.JobScope; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.step.builder.StepBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.PlatformTransactionManager; + +@Slf4j +@Configuration +@RequiredArgsConstructor +public class StepConfiguration { + + private final ChunkReader chunkReader; + private final ChunkProcessor chunkProcessor; + private final ChunkWriter chunkWriter; + + @Bean + @JobScope + public Step lunchEventAlarmStep(JobRepository jobRepository, + PlatformTransactionManager transactionManager) throws Exception { + return new StepBuilder("lunchEventStep", jobRepository) + .chunk(100, transactionManager) + .reader(chunkReader.userDataJdbcPagingItemReader()) + .writer(chunkWriter.lunchEventWriter()) + .build(); + } +} diff --git a/src/main/java/com/yello/server/infrastructure/batch/UserRowMapper.java b/src/main/java/com/yello/server/infrastructure/batch/UserRowMapper.java new file mode 100644 index 00000000..02d8abf0 --- /dev/null +++ b/src/main/java/com/yello/server/infrastructure/batch/UserRowMapper.java @@ -0,0 +1,18 @@ +package com.yello.server.infrastructure.batch; + +import com.yello.server.domain.user.entity.User; +import org.springframework.jdbc.core.RowMapper; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class UserRowMapper implements RowMapper { + @Override + public User mapRow(ResultSet rs, int rowNum) throws SQLException { + return User.builder() + .id(rs.getLong("id")) + .name(rs.getString("name")) + .yelloId(rs.getString("yello_id")) + .build(); + } +} diff --git a/src/main/java/com/yello/server/infrastructure/firebase/dto/NotificationType.java b/src/main/java/com/yello/server/infrastructure/firebase/dto/NotificationType.java index 27e7b989..6ab4a915 100644 --- a/src/main/java/com/yello/server/infrastructure/firebase/dto/NotificationType.java +++ b/src/main/java/com/yello/server/infrastructure/firebase/dto/NotificationType.java @@ -4,5 +4,7 @@ public enum NotificationType { NEW_VOTE, VOTE_AVAILABLE, NEW_FRIEND, - RECOMMEND + RECOMMEND, + LUNCH_EVENT, + OPEN_VOTE } diff --git a/src/main/java/com/yello/server/infrastructure/firebase/dto/request/NotificationMessage.java b/src/main/java/com/yello/server/infrastructure/firebase/dto/request/NotificationMessage.java index f452069b..00b0204f 100644 --- a/src/main/java/com/yello/server/infrastructure/firebase/dto/request/NotificationMessage.java +++ b/src/main/java/com/yello/server/infrastructure/firebase/dto/request/NotificationMessage.java @@ -52,6 +52,30 @@ public static NotificationMessage toYelloNotificationContent(Vote vote) { .build(); } + public static NotificationMessage toUserOpenVoteNotificationContent(User user) { + return NotificationMessage.builder() + .title(MessageFormat.format("{0}님이 내가 보낸 쪽지를 확인했어요!", user.getName())) + .message("\uD83D\uDEA8\uD83D\uDC9A 그린라이트입니다.") + .type(NotificationType.OPEN_VOTE) + .build(); + } + + public static NotificationMessage toUserAndFriendRecommendSignupAndGetTicketNotificationContent(User user) { + return NotificationMessage.builder() + .title(MessageFormat.format("{0}님이 나를 추천인으로 가입해 열람권이 지급됐어요!", user.getName())) + .message("지금이다! 날 짝사랑 하는 사람 보러가기") + .type(NotificationType.RECOMMEND) + .build(); + } + + public static NotificationMessage toAllUserLunchEventNotificationContent() { + return NotificationMessage.builder() + .title("우리 학교 선착순 30명 열람권 뿌린다!") + .message("지금부터 14시까지\uD83D\uDD25 사라지기 전에 바로 확인해보세요!") + .type(NotificationType.LUNCH_EVENT) + .build(); + } + public static NotificationMessage toYelloNotificationCustomContent( NotificationCustomMessage message) { diff --git a/src/main/java/com/yello/server/infrastructure/firebase/service/NotificationFcmService.java b/src/main/java/com/yello/server/infrastructure/firebase/service/NotificationFcmService.java index b110e0c3..58aa485c 100644 --- a/src/main/java/com/yello/server/infrastructure/firebase/service/NotificationFcmService.java +++ b/src/main/java/com/yello/server/infrastructure/firebase/service/NotificationFcmService.java @@ -10,12 +10,13 @@ import com.yello.server.infrastructure.firebase.dto.request.NotificationCustomMessage; import com.yello.server.infrastructure.firebase.dto.request.NotificationMessage; import com.yello.server.infrastructure.firebase.manager.FCMManager; -import java.util.Objects; import lombok.Builder; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import org.springframework.stereotype.Service; +import java.util.Objects; + @Log4j2 @Builder @Service @@ -29,11 +30,11 @@ public class NotificationFcmService implements NotificationService { @Override public void sendRecommendNotification(User user, User target) { NotificationMessage notificationMessage = - NotificationMessage.toRecommendNotificationContent(user); + NotificationMessage.toRecommendNotificationContent(user); if (target.getDeviceToken() != null && !Objects.equals(target.getDeviceToken(), "")) { final Message message = - fcmManager.createMessage(target.getDeviceToken(), notificationMessage); + fcmManager.createMessage(target.getDeviceToken(), notificationMessage); fcmManager.send(message); } } @@ -43,13 +44,13 @@ public void sendYelloNotification(Vote vote) { final User receiver = vote.getReceiver(); NotificationMessage notificationMessage = - NotificationMessage.toYelloNotificationContent(vote); + NotificationMessage.toYelloNotificationContent(vote); final String path = "/api/v1/vote/" + vote.getId().toString(); if (receiver.getDeviceToken() != null && !Objects.equals(receiver.getDeviceToken(), "")) { final Message message = - fcmManager.createMessage(receiver.getDeviceToken(), notificationMessage, path); + fcmManager.createMessage(receiver.getDeviceToken(), notificationMessage, path); fcmManager.send(message); } } @@ -60,11 +61,11 @@ public void sendFriendNotification(Friend friend) { final User sender = friend.getUser(); NotificationMessage notificationMessage = - NotificationMessage.toFriendNotificationContent(sender); + NotificationMessage.toFriendNotificationContent(sender); if (receiver.getDeviceToken() != null && !Objects.equals(receiver.getDeviceToken(), "")) { final Message message = - fcmManager.createMessage(receiver.getDeviceToken(), notificationMessage); + fcmManager.createMessage(receiver.getDeviceToken(), notificationMessage); fcmManager.send(message); } } @@ -74,12 +75,12 @@ public void sendVoteAvailableNotification(Long receiverId) { final User receiveUser = userRepository.getById(receiverId); NotificationMessage notificationMessage = - NotificationMessage.toVoteAvailableNotificationContent(); + NotificationMessage.toVoteAvailableNotificationContent(); if (receiveUser.getDeviceToken() != null && !Objects.equals(receiveUser.getDeviceToken(), - "")) { + "")) { final Message message = - fcmManager.createMessage(receiveUser.getDeviceToken(), notificationMessage); + fcmManager.createMessage(receiveUser.getDeviceToken(), notificationMessage); fcmManager.send(message); log.info("[rabbitmq] successfully send notification!"); } @@ -89,19 +90,19 @@ public void sendVoteAvailableNotification(Long receiverId) { public void sendCustomNotification(NotificationCustomMessage request) { request.userIdList().stream() - .forEach(userId -> { - final User receiver = userRepository.getById(userId); + .forEach(userId -> { + final User receiver = userRepository.getById(userId); - NotificationMessage notificationMessage = - NotificationMessage.toYelloNotificationCustomContent(request); + NotificationMessage notificationMessage = + NotificationMessage.toYelloNotificationCustomContent(request); - if (receiver.getDeviceToken() != null && !Objects.equals(receiver.getDeviceToken(), - "")) { - final Message message = - fcmManager.createMessage(receiver.getDeviceToken(), notificationMessage); - fcmManager.send(message); - } - }); + if (receiver.getDeviceToken() != null && !Objects.equals(receiver.getDeviceToken(), + "")) { + final Message message = + fcmManager.createMessage(receiver.getDeviceToken(), notificationMessage); + fcmManager.send(message); + } + }); } @@ -116,4 +117,44 @@ public EmptyObject adminSendCustomNotification(Long adminId, NotificationCustomM return EmptyObject.builder().build(); } + + @Override + public void sendLunchEventNotification(User user) { + final User receiver = userRepository.getById(user.getId()); + + NotificationMessage notificationMessage = + NotificationMessage.toAllUserLunchEventNotificationContent(); + + if (receiver.getDeviceToken() != null && !Objects.equals(receiver.getDeviceToken(), + "")) { + final Message message = + fcmManager.createMessage(receiver.getDeviceToken(), notificationMessage); + fcmManager.send(message); + + } + } + + @Override + public void sendOpenVoteNotification(User sender) { + NotificationMessage notificationMessage = + NotificationMessage.toUserOpenVoteNotificationContent(sender); + + if (sender.getDeviceToken() != null && !Objects.equals(sender.getDeviceToken(), "")) { + final Message message = + fcmManager.createMessage(sender.getDeviceToken(), notificationMessage); + fcmManager.send(message); + } + } + + @Override + public void sendRecommendSignupAndGetTicketNotification(User recommendUser) { + NotificationMessage notificationMessage = + NotificationMessage.toUserAndFriendRecommendSignupAndGetTicketNotificationContent(recommendUser); + + if (recommendUser.getDeviceToken() != null && !Objects.equals(recommendUser.getDeviceToken(), "")) { + final Message message = + fcmManager.createMessage(recommendUser.getDeviceToken(), notificationMessage); + fcmManager.send(message); + } + } } diff --git a/src/main/java/com/yello/server/infrastructure/firebase/service/NotificationService.java b/src/main/java/com/yello/server/infrastructure/firebase/service/NotificationService.java index b0d8f687..e613b4aa 100644 --- a/src/main/java/com/yello/server/infrastructure/firebase/service/NotificationService.java +++ b/src/main/java/com/yello/server/infrastructure/firebase/service/NotificationService.java @@ -19,4 +19,9 @@ public interface NotificationService { void sendCustomNotification(NotificationCustomMessage request); EmptyObject adminSendCustomNotification(Long adminId, NotificationCustomMessage request); + void sendLunchEventNotification(User userList); + + void sendOpenVoteNotification(User user); + + void sendRecommendSignupAndGetTicketNotification(User user); } diff --git a/src/main/java/com/yello/server/infrastructure/scheduler/EventScheduler.java b/src/main/java/com/yello/server/infrastructure/scheduler/EventScheduler.java new file mode 100644 index 00000000..82f43bc8 --- /dev/null +++ b/src/main/java/com/yello/server/infrastructure/scheduler/EventScheduler.java @@ -0,0 +1,45 @@ +package com.yello.server.infrastructure.scheduler; + + +import com.yello.server.infrastructure.batch.JobConfiguration; +import lombok.RequiredArgsConstructor; +import org.springframework.batch.core.JobParameters; +import org.springframework.batch.core.JobParametersBuilder; +import org.springframework.batch.core.JobParametersInvalidException; +import org.springframework.batch.core.launch.JobLauncher; +import org.springframework.batch.core.repository.JobExecutionAlreadyRunningException; +import org.springframework.batch.core.repository.JobInstanceAlreadyCompleteException; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.springframework.transaction.PlatformTransactionManager; + +import java.util.UUID; + +@Component +@RequiredArgsConstructor +public class EventScheduler { + + private final JobLauncher jobLauncher; + private final JobConfiguration jobConfiguration; + private final JobRepository jobRepository; + private final PlatformTransactionManager transactionManager; + + @Scheduled(cron = "0 0 12 * * ?") + public void lunchEventRunJob() { + + //JobParamter의 역할은 반복해서 실행되는 Job의 유일한 ID임, 동일한 값이 세팅되면 두번째부터 실행안됨) + JobParameters jobParameters = new JobParametersBuilder() + .addString("uuid", UUID.randomUUID().toString()) + .toJobParameters(); + + try { + jobLauncher.run(jobConfiguration.lunchEventJob(jobRepository, transactionManager), jobParameters); + } catch (JobExecutionAlreadyRunningException | JobInstanceAlreadyCompleteException + | JobParametersInvalidException | org.springframework.batch.core.repository.JobRestartException e) { + System.out.println(e.getMessage()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/test/java/com/yello/server/domain/vote/small/VoteServiceTest.java b/src/test/java/com/yello/server/domain/vote/small/VoteServiceTest.java index 1d096ebf..36cd3d6c 100644 --- a/src/test/java/com/yello/server/domain/vote/small/VoteServiceTest.java +++ b/src/test/java/com/yello/server/domain/vote/small/VoteServiceTest.java @@ -1,8 +1,5 @@ package com.yello.server.domain.vote.small; -import static com.yello.server.global.common.factory.PaginationFactory.createPageable; -import static org.assertj.core.api.Assertions.assertThat; - import com.yello.server.domain.cooldown.FakeCooldownRepository; import com.yello.server.domain.cooldown.repository.CooldownRepository; import com.yello.server.domain.friend.FakeFriendRepository; @@ -36,32 +33,30 @@ import com.yello.server.domain.vote.FakeVoteRepository; import com.yello.server.domain.vote.dto.request.CreateVoteRequest; import com.yello.server.domain.vote.dto.request.VoteAnswer; -import com.yello.server.domain.vote.dto.response.RevealNameResponse; -import com.yello.server.domain.vote.dto.response.VoteAvailableResponse; -import com.yello.server.domain.vote.dto.response.VoteCreateVO; -import com.yello.server.domain.vote.dto.response.VoteDetailResponse; -import com.yello.server.domain.vote.dto.response.VoteFriendResponse; -import com.yello.server.domain.vote.dto.response.VoteListResponse; -import com.yello.server.domain.vote.dto.response.VoteUnreadCountResponse; +import com.yello.server.domain.vote.dto.response.*; import com.yello.server.domain.vote.entity.Vote; import com.yello.server.domain.vote.repository.VoteRepository; import com.yello.server.domain.vote.service.VoteManager; import com.yello.server.domain.vote.service.VoteService; +import com.yello.server.infrastructure.firebase.FakeFcmManger; +import com.yello.server.infrastructure.firebase.manager.FCMManager; +import com.yello.server.infrastructure.firebase.service.NotificationFcmService; +import com.yello.server.infrastructure.firebase.service.NotificationService; import com.yello.server.infrastructure.rabbitmq.FakeMessageQueueRepository; import com.yello.server.infrastructure.rabbitmq.FakeProducerService; import com.yello.server.infrastructure.rabbitmq.service.ProducerService; import com.yello.server.util.TestDataEntityUtil; import com.yello.server.util.TestDataRepositoryUtil; -import java.util.ArrayList; -import java.util.List; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.*; import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; -import org.junit.jupiter.api.Test; import org.springframework.data.domain.Pageable; +import java.util.ArrayList; +import java.util.List; + +import static com.yello.server.global.common.factory.PaginationFactory.createPageable; +import static org.assertj.core.api.Assertions.assertThat; + @DisplayName("VoteService 에서") @DisplayNameGeneration(ReplaceUnderscores.class) public class VoteServiceTest { @@ -71,35 +66,40 @@ public class VoteServiceTest { private final KeywordRepository keywordRepository = new FakeKeywordRepository(); private final NoticeRepository noticeRepository = new FakeNoticeRepository(); private final ProducerService producerService = - new FakeProducerService(new FakeMessageQueueRepository()); + new FakeProducerService(new FakeMessageQueueRepository()); private final PurchaseRepository purchaseRepository = new FakePurchaseRepository(); private final QuestionRepository questionRepository = new FakeQuestionRepository(); private final QuestionGroupTypeRepository questionGroupTypeRepository = new FakeQuestionGroupTypeRepository( - questionRepository); + questionRepository); private final TestDataEntityUtil testDataEntityUtil = new TestDataEntityUtil(); private final UserDataRepository userDataRepository = new FakeUserDataRepository(); private final UserGroupRepository userGroupRepository = new FakeUserGroupRepository(); private final UserRepository userRepository = new FakeUserRepository(friendRepository); private final UserManager userManager = new FakeUserManager(userRepository); + private final FCMManager fcmManager = new FakeFcmManger(); + private final NotificationService notificationService = NotificationFcmService.builder() + .userRepository(userRepository) + .fcmManager(fcmManager) + .build(); private final VoteRepository voteRepository = new FakeVoteRepository(); private final VoteManager voteManager = new FakeVoteManager( - userRepository, - questionRepository, - voteRepository, - friendRepository, - userManager + userRepository, + questionRepository, + voteRepository, + friendRepository, + userManager ); private final TestDataRepositoryUtil testDataUtil = new TestDataRepositoryUtil( - friendRepository, - noticeRepository, - purchaseRepository, - questionGroupTypeRepository, - questionRepository, - testDataEntityUtil, - userDataRepository, - userGroupRepository, - userRepository, - voteRepository + friendRepository, + noticeRepository, + purchaseRepository, + questionGroupTypeRepository, + questionRepository, + testDataEntityUtil, + userDataRepository, + userGroupRepository, + userRepository, + voteRepository ); private VoteService voteService; private List questionData = new ArrayList<>(); @@ -109,16 +109,17 @@ public class VoteServiceTest { @BeforeEach void init() { this.voteService = VoteService.builder() - .voteRepository(voteRepository) - .friendRepository(friendRepository) - .cooldownRepository(cooldownRepository) - .userRepository(userRepository) - .questionRepository(questionRepository) - .keywordRepository(keywordRepository) - .producerService(producerService) - .voteManager(voteManager) - .questionGroupTypeRepository(questionGroupTypeRepository) - .build(); + .voteRepository(voteRepository) + .friendRepository(friendRepository) + .cooldownRepository(cooldownRepository) + .userRepository(userRepository) + .questionRepository(questionRepository) + .keywordRepository(keywordRepository) + .producerService(producerService) + .voteManager(voteManager) + .questionGroupTypeRepository(questionGroupTypeRepository) + .notificationService(notificationService) + .build(); for (long i = 1; i <= 8; i++) { questionData.add(testDataUtil.generateQuestion(i)); @@ -126,7 +127,7 @@ void init() { for (long i = 1; i <= 8; i++) { QuestionGroupType questionGroupType = testDataUtil.generateQuestionGroupType(i, - questionData.get(Long.valueOf(i).intValue() - 1)); + questionData.get(Long.valueOf(i).intValue() - 1)); questionGroupTypeData.add(questionGroupType); } @@ -203,7 +204,7 @@ void cleanup() { // when VoteFriendResponse result = - voteService.findAllFriendVotes(userId, pageable); // 다시 확인 !! + voteService.findAllFriendVotes(userId, pageable); // 다시 확인 !! // then assertThat(result.totalCount()).isEqualTo(4); @@ -262,17 +263,17 @@ void cleanup() { final List voteAnswerList = new ArrayList<>(); VoteAnswer answer1 = VoteAnswer.builder() - .friendId(2L) - .questionId(1L) - .keywordName("test") - .colorIndex(0) - .build(); + .friendId(2L) + .questionId(1L) + .keywordName("test") + .colorIndex(0) + .build(); voteAnswerList.add(answer1); CreateVoteRequest request = CreateVoteRequest.builder() - .voteAnswerList(voteAnswerList) - .totalPoint(3) - .build(); + .voteAnswerList(voteAnswerList) + .totalPoint(3) + .build(); // when VoteCreateVO result = voteService.createVote(userId, request); diff --git a/src/test/java/com/yello/server/infrastructure/firebase/NotificationFcmServiceTest.java b/src/test/java/com/yello/server/infrastructure/firebase/NotificationFcmServiceTest.java index 9cde6837..a650f5b1 100644 --- a/src/test/java/com/yello/server/infrastructure/firebase/NotificationFcmServiceTest.java +++ b/src/test/java/com/yello/server/infrastructure/firebase/NotificationFcmServiceTest.java @@ -36,7 +36,8 @@ class NotificationFcmServiceTest { private User user; private User target; private User dummy; - + private Vote vote; + private Question question; @BeforeEach void init() { this.notificationService = NotificationFcmService.builder() @@ -74,6 +75,19 @@ void init() { .deletedAt(null).group(userGroup) .groupAdmissionYear(19).email("yello@test.com") .build(); + question = Question.builder() + .id(1L) + .nameHead(null).nameFoot("와") + .keywordHead("멋진").keywordFoot("에서 놀고싶어") + .build(); + vote = Vote.builder() + .id(1L) + .colorIndex(0).answer("test") + .isRead(false).nameHint(-1).isAnswerRevealed(false) + .sender(userRepository.getById(1L)) + .receiver(userRepository.getById(2L)) + .question(question).createdAt(LocalDateTime.now()) + .build(); } @Test @@ -120,6 +134,26 @@ void init() { notificationService.sendFriendNotification(friend); } + @Test + void 친구가_내가_보낸_쪽지_열람시_알림_전송에_성공합니다() { + //given + target.setDeviceToken("test-device-token"); + + // when + // then + notificationService.sendOpenVoteNotification(vote.getSender()); + } + + @Test + void 추천인_코드_가입하여_열람권_얻은_경우_알림_전송에_성공합니다() { + //given + target.setDeviceToken("test-device-token"); + + // when + // then + notificationService.sendRecommendSignupAndGetTicketNotification(target); + } + @Test void 푸시_알림_전송_시_존재하지_않는_유저인_경우에_UserNotFoundException이_발생합니다() { // given