diff --git a/pass-batch/build.gradle b/pass-batch/build.gradle index e0fccaec..af76ae03 100644 --- a/pass-batch/build.gradle +++ b/pass-batch/build.gradle @@ -21,6 +21,8 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-batch' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-webflux' + annotationProcessor "org.springframework.boot:spring-boot-configuration-processor" // hibernate-types implementation 'com.vladmihalcea:hibernate-types-52:2.19.2' diff --git a/pass-batch/src/main/java/com/example/passbatch/adapter/message/KakaoTalkMessageAdapter.java b/pass-batch/src/main/java/com/example/passbatch/adapter/message/KakaoTalkMessageAdapter.java new file mode 100644 index 00000000..5640b782 --- /dev/null +++ b/pass-batch/src/main/java/com/example/passbatch/adapter/message/KakaoTalkMessageAdapter.java @@ -0,0 +1,38 @@ +package com.example.passbatch.adapter.message; + +import com.example.passbatch.config.KakaoTalkMessageConfig; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.BodyInserters; +import org.springframework.web.reactive.function.client.WebClient; + +@Service +public class KakaoTalkMessageAdapter { + private final WebClient webClient; + + public KakaoTalkMessageAdapter(KakaoTalkMessageConfig config) { + webClient = WebClient.builder() + .baseUrl(config.getHost()) + .defaultHeaders(h -> { + h.setBearerAuth(config.getToken()); + h.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + }).build(); + + } + + public boolean sendKakaoTalkMessage(final String uuid, final String text) { + KakaoTalkMessageResponse response = webClient.post().uri("/v1/api/talk/friends/message/default/send") + .body(BodyInserters.fromValue(new KakaoTalkMessageRequest(uuid, text))) + .retrieve() + .bodyToMono(KakaoTalkMessageResponse.class) + .block(); + + if (response == null || response.getSuccessfulReceiverUuids() == null) { + return false; + + } + return response.getSuccessfulReceiverUuids().size() > 0; + + } + +} \ No newline at end of file diff --git a/pass-batch/src/main/java/com/example/passbatch/adapter/message/KakaoTalkMessageRequest.java b/pass-batch/src/main/java/com/example/passbatch/adapter/message/KakaoTalkMessageRequest.java new file mode 100644 index 00000000..7a47c0f6 --- /dev/null +++ b/pass-batch/src/main/java/com/example/passbatch/adapter/message/KakaoTalkMessageRequest.java @@ -0,0 +1,55 @@ +package com.example.passbatch.adapter.message; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +import java.util.Collections; +import java.util.List; + +@Getter +@Setter +@ToString +public class KakaoTalkMessageRequest { + @JsonProperty("template_object") + private TemplateObject templateObject; + + @JsonProperty("receiver_uuids") + private List receiverUuids; + + @Getter + @Setter + @ToString + public static class TemplateObject { + @JsonProperty("object_type") + private String objectType; + private String text; + private Link link; + + @Getter + @Setter + @ToString + public static class Link { + @JsonProperty("web_url") + private String webUrl; + + } + + } + + public KakaoTalkMessageRequest(String uuid, String text) { + List receiverUuids = Collections.singletonList(uuid); + + TemplateObject.Link link = new TemplateObject.Link(); + TemplateObject templateObject = new TemplateObject(); + templateObject.setObjectType("text"); + templateObject.setText(text); + templateObject.setLink(link); + + this.receiverUuids = receiverUuids; + this.templateObject = templateObject; + + } + +} \ No newline at end of file diff --git a/pass-batch/src/main/java/com/example/passbatch/adapter/message/KakaoTalkMessageResponse.java b/pass-batch/src/main/java/com/example/passbatch/adapter/message/KakaoTalkMessageResponse.java new file mode 100644 index 00000000..c637b1c8 --- /dev/null +++ b/pass-batch/src/main/java/com/example/passbatch/adapter/message/KakaoTalkMessageResponse.java @@ -0,0 +1,17 @@ +package com.example.passbatch.adapter.message; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +import java.util.List; + +@Getter +@Setter +@ToString +public class KakaoTalkMessageResponse { + @JsonProperty("successful_receiver_uuids") + private List successfulReceiverUuids; + +} \ No newline at end of file diff --git a/pass-batch/src/main/java/com/example/passbatch/config/KakaoTalkMessageConfig.java b/pass-batch/src/main/java/com/example/passbatch/config/KakaoTalkMessageConfig.java new file mode 100644 index 00000000..722d1687 --- /dev/null +++ b/pass-batch/src/main/java/com/example/passbatch/config/KakaoTalkMessageConfig.java @@ -0,0 +1,16 @@ +package com.example.passbatch.config; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + + +@Getter +@Setter +@Component +@ConfigurationProperties(prefix = "kakaotalk") +public class KakaoTalkMessageConfig { + private String host; + private String token; +} \ No newline at end of file diff --git a/pass-batch/src/main/java/com/example/passbatch/job/notification/SendNotificationBeforeClassJobConfig.java b/pass-batch/src/main/java/com/example/passbatch/job/notification/SendNotificationBeforeClassJobConfig.java new file mode 100644 index 00000000..2a8127b8 --- /dev/null +++ b/pass-batch/src/main/java/com/example/passbatch/job/notification/SendNotificationBeforeClassJobConfig.java @@ -0,0 +1,103 @@ +package com.example.passbatch.job.notification; + +import com.example.passbatch.repository.booking.BookingEntity; +import com.example.passbatch.repository.notification.NotificationEntity; +import com.example.passbatch.repository.notification.NotificationEvent; +import com.example.passbatch.repository.notification.NotificationModelMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.configuration.annotation.JobBuilderFactory; +import org.springframework.batch.core.configuration.annotation.StepBuilderFactory; +import org.springframework.batch.item.ItemProcessor; +import org.springframework.batch.item.database.JpaCursorItemReader; +import org.springframework.batch.item.database.JpaItemWriter; +import org.springframework.batch.item.database.JpaPagingItemReader; +import org.springframework.batch.item.database.builder.JpaCursorItemReaderBuilder; +import org.springframework.batch.item.database.builder.JpaItemWriterBuilder; +import org.springframework.batch.item.database.builder.JpaPagingItemReaderBuilder; +import org.springframework.batch.item.support.SynchronizedItemStreamReader; +import org.springframework.batch.item.support.builder.SynchronizedItemStreamReaderBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.task.SimpleAsyncTaskExecutor; + +import javax.persistence.EntityManagerFactory; +import java.util.Map; + +@Configuration +@RequiredArgsConstructor +public class SendNotificationBeforeClassJobConfig { + + private final int CHUNK_SIZE = 10; + + private final JobBuilderFactory jobBuilderFactory; + private final StepBuilderFactory stepBuilderFactory; + private final EntityManagerFactory entityManagerFactory; + private final SendNotificationItemWriter sendNotificationItemWriter; + + @Bean + public Job sendNotificationBeforeClassJob() { + return this.jobBuilderFactory.get("sendNotificationBeforeClassJob") + .start(addNotificationStep()) + .next(sendNotificationStep()) + .build(); + } + + @Bean + public Step addNotificationStep() { + return this.stepBuilderFactory.get("addNotificationStep") + .chunk(CHUNK_SIZE) + .reader(addNotificationItemReader()) + .processor(addNotificationItemProcessor()) + .writer(addNotificationItemWriter()) + .build(); + } + + @Bean + public JpaPagingItemReader addNotificationItemReader() { + return new JpaPagingItemReaderBuilder() + .name("addNotificationItemReader") + .entityManagerFactory(entityManagerFactory) + .pageSize(CHUNK_SIZE) + .queryString("select b from BookingEntity b join fetch b.userEntity where b.status = :status and b.startedAt <= :startedAt order by b.bookingSeq") + .build(); + } + + @Bean + public ItemProcessor addNotificationItemProcessor() { + return bookingEntity -> + NotificationModelMapper.INSTANCE.toNotificationEntity(bookingEntity, NotificationEvent.BEFORE_CLASS); + } + + @Bean + public JpaItemWriter addNotificationItemWriter() { + return new JpaItemWriterBuilder() + .entityManagerFactory(entityManagerFactory) + .build(); + } + + @Bean + public Step sendNotificationStep() { + return this.stepBuilderFactory.get("sendNotificationStep") + .chunk(CHUNK_SIZE) + .reader(sendNotificationItemReader()) + .writer(sendNotificationItemWriter) + .taskExecutor(new SimpleAsyncTaskExecutor()) + .build(); + } + + @Bean + public SynchronizedItemStreamReader sendNotificationItemReader() { + JpaCursorItemReader itemReader = new JpaCursorItemReaderBuilder() + .name("sendNotificationItemReader") + .entityManagerFactory(entityManagerFactory) + .queryString("select n from NotificationEntity n where n.event = :event and n.sent = :sent") + .parameterValues(Map.of("event", NotificationEvent.BEFORE_CLASS, "sent", false)) + .build(); + + return new SynchronizedItemStreamReaderBuilder() + .delegate(itemReader) + .build(); + } +} diff --git a/pass-batch/src/main/java/com/example/passbatch/job/notification/SendNotificationItemWriter.java b/pass-batch/src/main/java/com/example/passbatch/job/notification/SendNotificationItemWriter.java new file mode 100644 index 00000000..4513f726 --- /dev/null +++ b/pass-batch/src/main/java/com/example/passbatch/job/notification/SendNotificationItemWriter.java @@ -0,0 +1,43 @@ +package com.example.passbatch.job.notification; + +import com.example.passbatch.adapter.message.KakaoTalkMessageAdapter; +import com.example.passbatch.repository.notification.NotificationEntity; +import com.example.passbatch.repository.notification.NotificationRepository; +import lombok.extern.slf4j.Slf4j; +import org.springframework.batch.item.ItemWriter; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; +import java.util.List; + +@Slf4j +@Component +public class SendNotificationItemWriter implements ItemWriter { + private final NotificationRepository notificationRepository; + private final KakaoTalkMessageAdapter kakaoTalkMessageAdapter; + + public SendNotificationItemWriter(NotificationRepository notificationRepository, KakaoTalkMessageAdapter kakaoTalkMessageAdapter) { + this.notificationRepository = notificationRepository; + this.kakaoTalkMessageAdapter = kakaoTalkMessageAdapter; + } + + @Override + public void write(List notificationEntities) throws Exception { + int count = 0; + + for (NotificationEntity notificationEntity : notificationEntities) { + boolean successful = kakaoTalkMessageAdapter.sendKakaoTalkMessage(notificationEntity.getUuid(), notificationEntity.getText()); + + if (successful) { + notificationEntity.setSent(true); + notificationEntity.setSentAt(LocalDateTime.now()); + notificationRepository.save(notificationEntity); + count++; + } + + } + log.info("SendNotificationItemWriter - write: 수업 전 알람 {}/{}건 전송 성공", count, notificationEntities.size()); + + } + +}