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..f6e60403 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 @@ -110,30 +110,34 @@ public SignUpResponse signUp(SignUpRequest signUpRequest) { @Transactional public void recommendUser(String recommendYelloId, String userYelloId) { - if (recommendYelloId != null && !recommendYelloId.isEmpty()) { + if (recommendYelloId!=null && !recommendYelloId.isEmpty()) { User recommendedUser = userRepository.getByYelloId(recommendYelloId); User user = userRepository.getByYelloId(userYelloId); - final Optional recommended = userDataRepository.findByUserIdAndTag(recommendedUser.getId(), - UserDataType.RECOMMENDED); + final Optional recommended = + userDataRepository.findByUserIdAndTag(recommendedUser.getId(), + UserDataType.RECOMMENDED); recommendedUser.addRecommendCount(1L); recommendedUser.addPointBySubscribe(RECOMMEND_POINT); user.addPointBySubscribe(RECOMMEND_POINT); + + final Optional cooldown = + cooldownRepository.findByUserId(recommendedUser.getId()); + cooldown.ifPresent(cooldownRepository::delete); + if (recommended.isEmpty()) { recommendedUser.addTicketCount(1); + notificationService.sendRecommendSignupAndGetTicketNotification(recommendedUser, + user); userDataRepository.save(UserData.of( UserDataType.RECOMMENDED, ZonedDateTime.now(GlobalZoneId).format(ISO_OFFSET_DATE_TIME), recommendedUser )); + return; } - notificationService.sendRecommendNotification(user, recommendedUser); - - final Optional cooldown = - cooldownRepository.findByUserId(recommendedUser.getId()); - cooldown.ifPresent(cooldownRepository::delete); } } @@ -165,22 +169,28 @@ public OnBoardingFriendResponse findOnBoardingFriends(OnBoardingFriendRequest fr return OnBoardingFriendResponse.of(kakaoFriends.size(), pageList); } - public GroupNameSearchResponse findGroupNameContaining(String keyword, UserGroupType userGroupType, + public GroupNameSearchResponse findGroupNameContaining(String keyword, + UserGroupType userGroupType, Pageable pageable) { - int totalCount = userGroupRepository.countDistinctGroupNameContaining(keyword, userGroupType); - final List nameList = userGroupRepository.findDistinctGroupNameContaining(keyword, userGroupType, - pageable) - .stream() - .toList(); + int totalCount = + userGroupRepository.countDistinctGroupNameContaining(keyword, userGroupType); + final List nameList = + userGroupRepository.findDistinctGroupNameContaining(keyword, userGroupType, + pageable) + .stream() + .toList(); return GroupNameSearchResponse.of(totalCount, nameList); } - public DepartmentSearchResponse findGroupDepartmentBySchoolNameContaining(String schoolName, String keyword, + public DepartmentSearchResponse findGroupDepartmentBySchoolNameContaining(String schoolName, + String keyword, UserGroupType userGroupType, Pageable pageable) { - int totalCount = userGroupRepository.countAllByGroupNameContaining(schoolName, keyword, userGroupType); - final List userGroupResult = userGroupRepository.findAllByGroupNameContaining(schoolName, keyword, - userGroupType, pageable); + int totalCount = + userGroupRepository.countAllByGroupNameContaining(schoolName, keyword, userGroupType); + final List userGroupResult = + userGroupRepository.findAllByGroupNameContaining(schoolName, keyword, + userGroupType, pageable); return DepartmentSearchResponse.of(totalCount, userGroupResult); } @@ -205,7 +215,8 @@ public ServiceTokenVO reIssueToken(@NotNull ServiceTokenVO tokens) { public ClassNameSearchResponse getHighSchoolClassName(String schoolName, String className) { UserGroup userGroup = - userGroupRepository.getByGroupNameAndSubGroupName(schoolName, className, UserGroupType.HIGH_SCHOOL); + userGroupRepository.getByGroupNameAndSubGroupName(schoolName, className, + UserGroupType.HIGH_SCHOOL); return ClassNameSearchResponse.of(userGroup); } } diff --git a/src/main/java/com/yello/server/domain/event/service/EventService.java b/src/main/java/com/yello/server/domain/event/service/EventService.java index 09ac1eeb..7378c16b 100644 --- a/src/main/java/com/yello/server/domain/event/service/EventService.java +++ b/src/main/java/com/yello/server/domain/event/service/EventService.java @@ -68,8 +68,8 @@ public class EventService { private final EventRepository eventRepository; private final ObjectMapper objectMapper; - private final UserRepository userRepository; private final UserDataRepository userDataRepository; + private final UserRepository userRepository; public List getEvents(Long userId) throws JsonProcessingException { // exception @@ -99,7 +99,7 @@ public List getEvents(Long userId) throws JsonProcessingException if (!eventTimeList.isEmpty()) { final EventTime eventTime = eventTimeList.get(0); - // 현재 시각이 이벤트 시간에 유효하고, 남은 보상 카운트가 0인 이력 + // 현재 시각이 이벤트 시간에 유효하고, 날짜별 기준, 남은 보상 카운트가 0인 이력 eventInstanceList.addAll( eventRepository.findInstanceAllByEventTimeAndUser( eventTime, user) @@ -107,9 +107,10 @@ public List getEvents(Long userId) throws JsonProcessingException .filter(eventInstance -> eventInstance.getInstanceDate().isAfter(event.getStartDate()) && eventInstance.getInstanceDate().isBefore(event.getEndDate()) + && eventInstance.getInstanceDate().toLocalDate().isEqual(now.toLocalDate()) && nowTime.isAfter(eventInstance.getEventTime().getStartTime()) && nowTime.isBefore(eventInstance.getEventTime().getEndTime()) - && eventInstance.getRemainEventCount()==0 + && eventInstance.getRemainEventCount() == 0 ) .toList() ); @@ -278,7 +279,7 @@ public EventRewardResponse rewardAdmob(Long userId, AdmobRewardRequest request) } // history 있으면 userId로 세팅 - if (eventHistory.get().getUser()!=null) { + if (eventHistory.get().getUser() != null) { throw new EventForbiddenException(DUPLICATE_ADMOB_REWARD_EXCEPTION); } eventHistory.get().update(user); 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..2ab53406 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(), user); + } + 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..83b3f55d 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,8 @@ public enum NotificationType { NEW_VOTE, VOTE_AVAILABLE, NEW_FRIEND, - RECOMMEND + RECOMMEND, + LUNCH_EVENT, + OPEN_VOTE, + FIRST_RECOMMEND } 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..e983aa22 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.FIRST_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..3bb414e8 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 target, User user) { + NotificationMessage notificationMessage = + NotificationMessage.toUserOpenVoteNotificationContent(user); + + if (target.getDeviceToken() != null && !Objects.equals(target.getDeviceToken(), "")) { + final Message message = + fcmManager.createMessage(target.getDeviceToken(), notificationMessage); + fcmManager.send(message); + } + } + + @Override + public void sendRecommendSignupAndGetTicketNotification(User recommendUser, User user) { + NotificationMessage notificationMessage = + NotificationMessage.toUserAndFriendRecommendSignupAndGetTicketNotificationContent(user); + + 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..cfc40446 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 target, User user); + + void sendRecommendSignupAndGetTicketNotification(User recommendUser, 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..a7301786 --- /dev/null +++ b/src/main/java/com/yello/server/infrastructure/scheduler/EventScheduler.java @@ -0,0 +1,46 @@ +package com.yello.server.infrastructure.scheduler; + + +import com.yello.server.global.common.util.ConstantUtil; +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 * * ?", zone = ConstantUtil.GlobalZoneIdLabel) + 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..d0ba4b2f 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(target, user); + } + + @Test + void 추천인_코드_가입하여_열람권_얻은_경우_알림_전송에_성공합니다() { + //given + target.setDeviceToken("test-device-token"); + + // when + // then + notificationService.sendRecommendSignupAndGetTicketNotification(target, user); + } + @Test void 푸시_알림_전송_시_존재하지_않는_유저인_경우에_UserNotFoundException이_발생합니다() { // given