From 8502b95a7d8bc25b5b56f57a1c9e78f237425aad Mon Sep 17 00:00:00 2001 From: woozu-shin Date: Sun, 12 May 2024 23:08:10 +0900 Subject: [PATCH] Implement slack --- build.gradle | 8 ++- gradle/wrapper/gradle-wrapper.properties | 4 ++ jib.gradle | 28 ++++++++ .../datasource/H2ConsoleConfiguration.java | 2 +- .../configuration/web/WebConfiguration.java | 28 ++++++++ .../SlackEventSubscriptionAPIController.java | 65 ++++++++++++++++++ .../controller/TestAPIController.java | 16 ++++- .../dto/feed/v1/SubscribedKeywordDTO.java | 20 ++++++ .../slack/v2/SlackEventSubscriptionDTO.java | 35 ++++++++++ .../dto/slack/v2/SlackSlashCommandDTO.java | 22 ++++++ .../handler/ArticleUpsertEventListener.java | 4 +- .../repository/v2/AppUserRepository.java | 3 + .../v2/SubscribedKeywordRepository.java | 3 + .../service/AppUserCommandService.java | 31 +++++++++ .../shopping/service/AppUserQueryService.java | 9 +++ .../SubscribedKeywordCacheService.java | 8 +-- .../SubscribedKeywordCommandService.java | 46 +++++++++++++ .../shopping/service/UserNotifyService.java | 34 ---------- .../service/slack/RegisterCommandHandler.java | 33 +++++++++ .../service/slack/ShoppingCommandHandler.java | 34 ++++++++++ .../slack/ShoppingCommandProcessor.java | 10 +++ .../service/slack/SlackCommandHandler.java | 4 ++ .../service/slack/UserNotifyService.java | 33 +++++++++ .../KeywordShoppingCommandProcessor.java | 67 +++++++++++++++++++ .../ListShoppingCommandProcessor.java | 25 +++++++ .../resources/application-development.yml | 12 ---- .../src/main/resources/application-prod.yml | 23 +++++++ .../main/resources/application-production.yml | 6 -- .../src/main/resources/application.yml | 3 +- .../src/main/resources/logback-spring.xml | 10 +-- ...ogback-development.xml => logback-dev.xml} | 3 - ...ogback-production.xml => logback-prod.xml} | 3 - ...ackEventSubscriptionAPIControllerTest.java | 33 +++++++++ .../support/dto/constant/CrawlTarget.java | 18 ++++- 34 files changed, 606 insertions(+), 77 deletions(-) create mode 100644 jib.gradle create mode 100644 shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/configuration/web/WebConfiguration.java create mode 100644 shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/controller/SlackEventSubscriptionAPIController.java create mode 100644 shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/dto/feed/v1/SubscribedKeywordDTO.java create mode 100644 shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/dto/slack/v2/SlackEventSubscriptionDTO.java create mode 100644 shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/dto/slack/v2/SlackSlashCommandDTO.java create mode 100644 shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/service/AppUserCommandService.java create mode 100644 shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/service/SubscribedKeywordCommandService.java delete mode 100644 shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/service/UserNotifyService.java create mode 100644 shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/service/slack/RegisterCommandHandler.java create mode 100644 shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/service/slack/ShoppingCommandHandler.java create mode 100644 shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/service/slack/ShoppingCommandProcessor.java create mode 100644 shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/service/slack/SlackCommandHandler.java create mode 100644 shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/service/slack/UserNotifyService.java create mode 100644 shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/service/slack/shopping/KeywordShoppingCommandProcessor.java create mode 100644 shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/service/slack/shopping/ListShoppingCommandProcessor.java delete mode 100644 shopping-crawler/src/main/resources/application-development.yml create mode 100644 shopping-crawler/src/main/resources/application-prod.yml delete mode 100644 shopping-crawler/src/main/resources/application-production.yml rename shopping-crawler/src/main/resources/logback/{logback-development.xml => logback-dev.xml} (77%) rename shopping-crawler/src/main/resources/logback/{logback-production.xml => logback-prod.xml} (77%) create mode 100644 shopping-crawler/src/test/java/com/myoa/engineering/crawl/shopping/controller/SlackEventSubscriptionAPIControllerTest.java diff --git a/build.gradle b/build.gradle index 118203c..8d5f629 100644 --- a/build.gradle +++ b/build.gradle @@ -3,6 +3,7 @@ plugins { id 'idea' id 'org.springframework.boot' version '2.5.4' id 'io.spring.dependency-management' version '1.0.11.RELEASE' + id 'com.google.cloud.tools.jib' version '3.4.0' } group = 'com.myoa.engineering.crawl.shopping' @@ -19,14 +20,16 @@ repositories { mavenCentral() } -allprojects { +subprojects { group = 'com.myoa.engineering.crawl.shopping' version = '1.1.1' + sourceCompatibility = JavaVersion.VERSION_17 apply plugin: 'java' apply plugin: 'idea' apply plugin: 'org.springframework.boot' apply plugin: 'io.spring.dependency-management' + apply plugin: "com.google.cloud.tools.jib" repositories { mavenCentral() @@ -37,8 +40,11 @@ allprojects { ext { set('springCloudVersion', "2020.0.4") + set("BASE_IMAGE_REGISTRY_URL", "192.168.0.10:10001") } + apply from: "${project.rootDir}/jib.gradle" + dependencyManagement { imports { mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 6677206..99abc9f 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -4,3 +4,7 @@ distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists +org.gradle.caching=true +org.gradle.parallel=true +org.gradle.parallel.threads=4 +org.gradle.daemon=true \ No newline at end of file diff --git a/jib.gradle b/jib.gradle new file mode 100644 index 0000000..1a5b3df --- /dev/null +++ b/jib.gradle @@ -0,0 +1,28 @@ +jib.setAllowInsecureRegistries(true) + +jib { + from { + image = "eclipse-temurin:17-jdk" + platforms { + platform { + architecture = "amd64" + os = "linux" + } + } + } + to { + image = "${BASE_IMAGE_REGISTRY_URL}/${project.name}" + tags = ["latest"] + } + container { +// entrypoint = ["INHERIT"] + creationTime = "USE_CURRENT_TIMESTAMP" + environment = [ + "TZ": "Asia/Seoul", + ] + jvmFlags = [ + "--add-opens=java.base/java.time=ALL-UNNAMED" + ] + } + +} diff --git a/shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/configuration/datasource/H2ConsoleConfiguration.java b/shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/configuration/datasource/H2ConsoleConfiguration.java index 9f9ab8b..30991f9 100644 --- a/shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/configuration/datasource/H2ConsoleConfiguration.java +++ b/shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/configuration/datasource/H2ConsoleConfiguration.java @@ -24,7 +24,7 @@ public class H2ConsoleConfiguration { @EventListener(ContextRefreshedEvent.class) public void start() throws SQLException { log.info("starting h2 console"); - this.webServer = Server.createWebServer("-webPort", port, "-tcpAllowOthers").start(); + this.webServer = Server.createWebServer("-webPort", port, "-tcpAllowOthers", "-webAllowOthers").start(); } @EventListener(ContextClosedEvent.class) diff --git a/shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/configuration/web/WebConfiguration.java b/shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/configuration/web/WebConfiguration.java new file mode 100644 index 0000000..c17af15 --- /dev/null +++ b/shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/configuration/web/WebConfiguration.java @@ -0,0 +1,28 @@ +package com.myoa.engineering.crawl.shopping.configuration.web; + +import org.springframework.context.annotation.Configuration; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.nio.charset.StandardCharsets; +import java.util.List; + +@Configuration +public class WebConfiguration implements WebMvcConfigurer { + /* + @Override + public void configureMessageConverters(List> converters) { + WebMvcConfigurer.super.configureMessageConverters(converters); + addJsonFormUrlEncodedConverter(converters); + } + + private void addJsonFormUrlEncodedConverter(List> converters) { + JsonFormUrlEncodedConverter converter = new JsonFormUrlEncodedConverter<>(); + MediaType mediaType = new MediaType(MediaType.APPLICATION_FORM_URLENCODED, StandardCharsets.UTF_8); + converter.setSupportedMediaTypes(List.of(mediaType)); + converters.add(converter); + } + */ + +} diff --git a/shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/controller/SlackEventSubscriptionAPIController.java b/shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/controller/SlackEventSubscriptionAPIController.java new file mode 100644 index 0000000..0fffd10 --- /dev/null +++ b/shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/controller/SlackEventSubscriptionAPIController.java @@ -0,0 +1,65 @@ +package com.myoa.engineering.crawl.shopping.controller; + +import com.myoa.engineering.crawl.shopping.dto.slack.v2.SlackEventSubscriptionDTO; +import com.myoa.engineering.crawl.shopping.dto.slack.v2.SlackSlashCommandDTO; +import com.myoa.engineering.crawl.shopping.service.slack.RegisterCommandHandler; +import com.myoa.engineering.crawl.shopping.service.slack.ShoppingCommandHandler; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@Slf4j +@RestController +@RequestMapping("/api/v1/slack") +public class SlackEventSubscriptionAPIController { + + private final ShoppingCommandHandler shoppingCommandHandler; + private final RegisterCommandHandler registerCommandHandler; + + public SlackEventSubscriptionAPIController(ShoppingCommandHandler shoppingCommandHandler, + RegisterCommandHandler registerCommandHandler) { + this.shoppingCommandHandler = shoppingCommandHandler; + this.registerCommandHandler = registerCommandHandler; + } + + @PostMapping(value = {"/subscriptions"}) + private ResponseEntity slackEventSubscription(@RequestBody SlackEventSubscriptionDTO requestDTO) { + log.info("requestDTO: {}", requestDTO); + return ResponseEntity.ok(requestDTO.getChallenge()); + } + + @PostMapping(value = {"/commands/help"}, consumes = {"application/x-www-form-urlencoded"}) + private ResponseEntity handleCommandHelp(SlackSlashCommandDTO requestDTO) { + log.info("requestDTO: {}", requestDTO); + return ResponseEntity.ok(""" + /쇼핑 목록 + /쇼핑 키워드 추가 (쇼핑몰명) (키워드명) + /쇼핑 키워드 삭제 (쇼핑몰명) (키워드명) + /쇼핑 키워드 목록 + /등록 (사용자명) + """); + } + + @PostMapping(value = {"/commands/shopping"}, consumes = {"application/x-www-form-urlencoded"}) + private ResponseEntity handleAddSubscription(SlackSlashCommandDTO requestDTO) { + log.info("requestDTO: {}", requestDTO); + + List results = shoppingCommandHandler.handle(requestDTO); + + return ResponseEntity.ok(String.join("\n", results)); + } + + @PostMapping(value = {"/commands/register"}, consumes = {"application/x-www-form-urlencoded"}) + private ResponseEntity handleRegisterUser(SlackSlashCommandDTO requestDTO) { + log.info("requestDTO: {}", requestDTO); + + List results = registerCommandHandler.handle(requestDTO); + + return ResponseEntity.ok(String.join("\n", results)); + } +} diff --git a/shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/controller/TestAPIController.java b/shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/controller/TestAPIController.java index 7992a38..02a666c 100644 --- a/shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/controller/TestAPIController.java +++ b/shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/controller/TestAPIController.java @@ -1,22 +1,36 @@ package com.myoa.engineering.crawl.shopping.controller; import com.myoa.engineering.crawl.shopping.crawlhandler.PpomppuCrawlDomesticHandler; +import com.slack.api.methods.MethodsClient; +import com.slack.api.methods.SlackApiException; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import java.io.IOException; + @RestController @RequestMapping("/api/v1/exploit") public class TestAPIController { private final PpomppuCrawlDomesticHandler ppomppuCrawlDomesticHandler; + private final MethodsClient methodsClient; - public TestAPIController(PpomppuCrawlDomesticHandler ppomppuCrawlDomesticHandler) { + public TestAPIController(PpomppuCrawlDomesticHandler ppomppuCrawlDomesticHandler, + MethodsClient methodsClient) { this.ppomppuCrawlDomesticHandler = ppomppuCrawlDomesticHandler; + this.methodsClient = methodsClient; } @GetMapping("/triggers") public void triggerExploit() { ppomppuCrawlDomesticHandler.handle(); } + + @GetMapping("/test-message") + public void testMessage() throws SlackApiException, IOException { + methodsClient.chatPostMessage(req -> req + .channel("notify_shopping") + .text("Hello, World!")); + } } diff --git a/shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/dto/feed/v1/SubscribedKeywordDTO.java b/shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/dto/feed/v1/SubscribedKeywordDTO.java new file mode 100644 index 0000000..99675cc --- /dev/null +++ b/shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/dto/feed/v1/SubscribedKeywordDTO.java @@ -0,0 +1,20 @@ +package com.myoa.engineering.crawl.shopping.dto.feed.v1; + +import com.myoa.engineering.crawl.shopping.support.dto.constant.CrawlTarget; +import lombok.*; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SubscribedKeywordDTO { + + private String userId; + private CrawlTarget crawlTarget; + private String keyword; + + + public String toMessage() { + return crawlTarget.getAlias() + " | " + keyword; + } +} diff --git a/shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/dto/slack/v2/SlackEventSubscriptionDTO.java b/shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/dto/slack/v2/SlackEventSubscriptionDTO.java new file mode 100644 index 0000000..113a161 --- /dev/null +++ b/shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/dto/slack/v2/SlackEventSubscriptionDTO.java @@ -0,0 +1,35 @@ +package com.myoa.engineering.crawl.shopping.dto.slack.v2; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.util.Map; + +@ToString +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class SlackEventSubscriptionDTO { + + private String token; + private String challenge; + private String type; + + @JsonProperty("team_id") + private String teamId; + + @JsonProperty("api_app_id") + private String apiAppId; + + private Map event; + + @JsonProperty("event_id") + private String eventId; + + @JsonProperty("event_time") + private long eventTime; + +} diff --git a/shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/dto/slack/v2/SlackSlashCommandDTO.java b/shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/dto/slack/v2/SlackSlashCommandDTO.java new file mode 100644 index 0000000..9d8dfff --- /dev/null +++ b/shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/dto/slack/v2/SlackSlashCommandDTO.java @@ -0,0 +1,22 @@ +package com.myoa.engineering.crawl.shopping.dto.slack.v2; + +import lombok.*; + +@ToString +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class SlackSlashCommandDTO { + + private String token; + private String command; + private String text; + private String response_url; + private String trigger_id; + private String user_id; + private String user_name; + private String team_id; + private String api_app_id; + +} diff --git a/shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/event/handler/ArticleUpsertEventListener.java b/shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/event/handler/ArticleUpsertEventListener.java index af261e5..b49f828 100644 --- a/shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/event/handler/ArticleUpsertEventListener.java +++ b/shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/event/handler/ArticleUpsertEventListener.java @@ -7,7 +7,7 @@ import com.myoa.engineering.crawl.shopping.domain.model.v2.SubscribedKeywordAggr import com.myoa.engineering.crawl.shopping.event.ArticleUpsertEvent; import com.myoa.engineering.crawl.shopping.service.AppUserQueryService; import com.myoa.engineering.crawl.shopping.service.SubscribedKeywordCacheService; -import com.myoa.engineering.crawl.shopping.service.UserNotifyService; +import com.myoa.engineering.crawl.shopping.service.slack.UserNotifyService; import com.myoa.engineering.crawl.shopping.support.dto.constant.CrawlTarget; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; @@ -44,7 +44,7 @@ public class ArticleUpsertEventListener { .filter(AppUserModel::getEnabled) .map(user -> { List filteredArticles = handleAhoCorasick(articleMap) - .apply(subscribedKeywordCacheService.getSubscribedKeywordsCached(user.getName())); + .apply(subscribedKeywordCacheService.getSubscribedKeywordsCached(user.getSlackId())); return UserNotifyModel.of(user.getSlackId(), filteredArticles); }) .forEach(this::notifyMessage); diff --git a/shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/infra/repository/v2/AppUserRepository.java b/shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/infra/repository/v2/AppUserRepository.java index 6579b4a..77ca0c6 100644 --- a/shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/infra/repository/v2/AppUserRepository.java +++ b/shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/infra/repository/v2/AppUserRepository.java @@ -4,6 +4,9 @@ import com.myoa.engineering.crawl.shopping.domain.entity.v2.AppUser; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +import java.util.Optional; + @Repository public interface AppUserRepository extends JpaRepository { + Optional findBySlackId(String userId); } diff --git a/shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/infra/repository/v2/SubscribedKeywordRepository.java b/shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/infra/repository/v2/SubscribedKeywordRepository.java index 90028c1..fec56bb 100644 --- a/shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/infra/repository/v2/SubscribedKeywordRepository.java +++ b/shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/infra/repository/v2/SubscribedKeywordRepository.java @@ -6,6 +6,7 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import java.util.List; +import java.util.Optional; @Repository public interface SubscribedKeywordRepository extends JpaRepository { @@ -20,4 +21,6 @@ public interface SubscribedKeywordRepository extends JpaRepository findByUserIdAndCrawlTarget(String userId, CrawlTarget crawlTarget); List findByUserId(String userId); + + Optional findByUserIdAndCrawlTargetAndKeyword(String userId, CrawlTarget crawlTarget, String keyword); } diff --git a/shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/service/AppUserCommandService.java b/shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/service/AppUserCommandService.java new file mode 100644 index 0000000..fd8235c --- /dev/null +++ b/shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/service/AppUserCommandService.java @@ -0,0 +1,31 @@ +package com.myoa.engineering.crawl.shopping.service; + +import com.myoa.engineering.crawl.shopping.domain.entity.v2.AppUser; +import com.myoa.engineering.crawl.shopping.infra.repository.v2.AppUserRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class AppUserCommandService { + private final AppUserRepository appUserRepository; + + public AppUserCommandService(AppUserRepository appUserRepository) { + this.appUserRepository = appUserRepository; + } + + @Transactional + public void register(String slackId, String userName) { + appUserRepository.findBySlackId(slackId) + .ifPresent(user -> { + throw new IllegalArgumentException("이미 등록된 사용자입니다."); + }); + + AppUser appUser = AppUser.builder() + .enabled(true) + .slackId(slackId) + .name(userName) + .build(); + + appUserRepository.save(appUser); + } +} diff --git a/shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/service/AppUserQueryService.java b/shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/service/AppUserQueryService.java index c2f1d01..ae13f96 100644 --- a/shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/service/AppUserQueryService.java +++ b/shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/service/AppUserQueryService.java @@ -3,6 +3,7 @@ package com.myoa.engineering.crawl.shopping.service; import com.myoa.engineering.crawl.shopping.domain.model.v2.AppUserModel; import com.myoa.engineering.crawl.shopping.infra.repository.v2.AppUserRepository; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.util.List; @@ -15,6 +16,7 @@ public class AppUserQueryService { this.appUserRepository = appUserRepository; } + @Transactional(readOnly = true) public List findAll() { return appUserRepository.findAll() .stream() @@ -22,4 +24,11 @@ public class AppUserQueryService { .toList(); } + @Transactional(readOnly = true) + public AppUserModel findByUserId(String userId) { + return appUserRepository.findBySlackId(userId) + .map(AppUserModel::from) + .orElseThrow(); + } + } diff --git a/shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/service/SubscribedKeywordCacheService.java b/shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/service/SubscribedKeywordCacheService.java index a0fd644..5b2b332 100644 --- a/shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/service/SubscribedKeywordCacheService.java +++ b/shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/service/SubscribedKeywordCacheService.java @@ -27,15 +27,15 @@ public class SubscribedKeywordCacheService { return SubscribedKeywordAggregatedModel.of(userId, crawlTarget, keywords); } - @Cacheable(cacheNames = "subscribe.keywords", key = "#userId") - public Map getSubscribedKeywordsCached(String userId) { + @Cacheable(cacheNames = "subscribe.keywords", key = "#slackId") + public Map getSubscribedKeywordsCached(String slackId) { System.out.println("getSubscribedKeywordsCached"); - return subscribedKeywordQueryService.findByUser(userId) + return subscribedKeywordQueryService.findByUser(slackId) .stream() .collect(Collectors.groupingBy(SubscribedKeyword::getCrawlTarget, Collectors.mapping(SubscribedKeyword::getKeyword, Collectors.toList()))) .entrySet().stream() - .collect(Collectors.toMap(Map.Entry::getKey, e -> SubscribedKeywordAggregatedModel.of(userId, e.getKey(), e.getValue()))); + .collect(Collectors.toMap(Map.Entry::getKey, e -> SubscribedKeywordAggregatedModel.of(slackId, e.getKey(), e.getValue()))); } } diff --git a/shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/service/SubscribedKeywordCommandService.java b/shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/service/SubscribedKeywordCommandService.java new file mode 100644 index 0000000..c149590 --- /dev/null +++ b/shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/service/SubscribedKeywordCommandService.java @@ -0,0 +1,46 @@ +package com.myoa.engineering.crawl.shopping.service; + +import com.myoa.engineering.crawl.shopping.domain.entity.v2.SubscribedKeyword; +import com.myoa.engineering.crawl.shopping.dto.feed.v1.SubscribedKeywordDTO; +import com.myoa.engineering.crawl.shopping.infra.repository.v2.SubscribedKeywordRepository; +import com.myoa.engineering.crawl.shopping.support.dto.constant.CrawlTarget; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +public class SubscribedKeywordCommandService { + + private final SubscribedKeywordRepository subscribedKeywordRepository; + + public SubscribedKeywordCommandService(SubscribedKeywordRepository subscribedKeywordRepository) { + this.subscribedKeywordRepository = subscribedKeywordRepository; + } + + @Transactional + public void add(SubscribedKeyword subscribedKeyword) { + subscribedKeywordRepository.save(subscribedKeyword); + } + + @Transactional + public void delete(String userId, CrawlTarget crawlTarget, String keyword) { + SubscribedKeyword subscribedKeyword = subscribedKeywordRepository.findByUserIdAndCrawlTargetAndKeyword(userId, crawlTarget, keyword) + .orElseThrow(() -> new IllegalArgumentException("Not found keyword: " + keyword)); + + subscribedKeywordRepository.delete(subscribedKeyword); + } + + @Transactional(readOnly = true) + public List list(String userId) { + return subscribedKeywordRepository.findByUserId(userId) + .stream() + .map(e -> SubscribedKeywordDTO.builder() + .keyword(e.getKeyword()) + .userId(e.getUserId()) + .crawlTarget(e.getCrawlTarget()) + .build()) + .toList(); + } + +} diff --git a/shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/service/UserNotifyService.java b/shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/service/UserNotifyService.java deleted file mode 100644 index c31a589..0000000 --- a/shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/service/UserNotifyService.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.myoa.engineering.crawl.shopping.service; - -import com.myoa.engineering.crawl.shopping.configuration.slack.properties.SlackSecretProperties; -import com.myoa.engineering.crawl.shopping.dto.slack.v1.SlackMessageDTO; -import com.myoa.engineering.crawl.shopping.infra.client.slack.SlackAPIClient; -import org.springframework.stereotype.Service; - -@Service -public class UserNotifyService { - - private static final String SLACK_PROPERTIES_UNIT_NAME = "shopping-crawler"; -// private static final String NOTIFY_CHANNEL_ID = "notify_shopping"; -// private static final String NOTIFY_ICON_EMOJI = ":monge_big:"; -// private static final String NOTIFY_BOT_NAME = "몽이 탈호구봇"; - - private final SlackAPIClient slackAPIClient; - private final SlackSecretProperties.SlackSecretPropertiesUnit slackSecretProperties; - - public UserNotifyService(SlackAPIClient slackAPIClient, - SlackSecretProperties slackSecretProperties) { - this.slackAPIClient = slackAPIClient; - this.slackSecretProperties = slackSecretProperties.find(SLACK_PROPERTIES_UNIT_NAME); - } - - public void notify(String message) { - SlackMessageDTO slackMessageDTO = SlackMessageDTO.builder() - .channel(slackSecretProperties.getChannel()) - .text(message) - .iconEmoji(slackSecretProperties.getIconEmoji()) - .username(slackSecretProperties.getUsername()) - .build(); - slackAPIClient.sendMessage(slackMessageDTO, slackSecretProperties.getToken()); - } -} diff --git a/shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/service/slack/RegisterCommandHandler.java b/shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/service/slack/RegisterCommandHandler.java new file mode 100644 index 0000000..802d89b --- /dev/null +++ b/shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/service/slack/RegisterCommandHandler.java @@ -0,0 +1,33 @@ +package com.myoa.engineering.crawl.shopping.service.slack; + +import com.myoa.engineering.crawl.shopping.dto.slack.v2.SlackSlashCommandDTO; +import com.myoa.engineering.crawl.shopping.service.AppUserCommandService; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class RegisterCommandHandler implements SlackCommandHandler { + + private final AppUserCommandService appUserCommandService; + + public RegisterCommandHandler(AppUserCommandService appUserCommandService) { + this.appUserCommandService = appUserCommandService; + } + + public List handle(SlackSlashCommandDTO requestDTO) { + if (!requestDTO.getCommand().equals("/등록")) { + throw new IllegalArgumentException("Invalid command: " + requestDTO.getCommand()); + } + + final String userName = requestDTO.getText(); + + try { + appUserCommandService.register(requestDTO.getUser_id(), userName); + return List.of("등록 완료"); + } catch (Exception e) { + return List.of(e.getMessage()); + } + + } +} diff --git a/shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/service/slack/ShoppingCommandHandler.java b/shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/service/slack/ShoppingCommandHandler.java new file mode 100644 index 0000000..6588577 --- /dev/null +++ b/shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/service/slack/ShoppingCommandHandler.java @@ -0,0 +1,34 @@ +package com.myoa.engineering.crawl.shopping.service.slack; + +import com.myoa.engineering.crawl.shopping.dto.slack.v2.SlackSlashCommandDTO; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class ShoppingCommandHandler implements SlackCommandHandler { + + private final List processors; + + public ShoppingCommandHandler(List processors) { + this.processors = processors; + } + + public List handle(SlackSlashCommandDTO requestDTO) { + String[] commands = requestDTO.getText().split("\\s+", 2); + + if (!requestDTO.getCommand().equals("/쇼핑")) { + throw new IllegalArgumentException("Invalid command: " + requestDTO.getCommand()); + } + + final String action = commands[0]; + + return processors.stream() + .filter(processor -> processor.isAssignable(action)) + .map(processor -> processor.process(requestDTO)) + .toList(); + + } + + +} diff --git a/shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/service/slack/ShoppingCommandProcessor.java b/shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/service/slack/ShoppingCommandProcessor.java new file mode 100644 index 0000000..bf81840 --- /dev/null +++ b/shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/service/slack/ShoppingCommandProcessor.java @@ -0,0 +1,10 @@ +package com.myoa.engineering.crawl.shopping.service.slack; + +import com.myoa.engineering.crawl.shopping.dto.slack.v2.SlackSlashCommandDTO; + +public interface ShoppingCommandProcessor { + + boolean isAssignable(String menuContext); + + String process(SlackSlashCommandDTO requestDTO); +} diff --git a/shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/service/slack/SlackCommandHandler.java b/shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/service/slack/SlackCommandHandler.java new file mode 100644 index 0000000..7f2e80d --- /dev/null +++ b/shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/service/slack/SlackCommandHandler.java @@ -0,0 +1,4 @@ +package com.myoa.engineering.crawl.shopping.service.slack; + +public interface SlackCommandHandler { +} diff --git a/shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/service/slack/UserNotifyService.java b/shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/service/slack/UserNotifyService.java new file mode 100644 index 0000000..249f424 --- /dev/null +++ b/shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/service/slack/UserNotifyService.java @@ -0,0 +1,33 @@ +package com.myoa.engineering.crawl.shopping.service.slack; + +import com.myoa.engineering.crawl.shopping.configuration.slack.properties.SlackSecretProperties; +import com.slack.api.methods.MethodsClient; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +public class UserNotifyService { + + private static final String SLACK_PROPERTIES_UNIT_NAME = "shopping-crawler"; + + private final SlackSecretProperties.SlackSecretPropertiesUnit slackSecretProperties; + private final MethodsClient methodsClient; + + public UserNotifyService(SlackSecretProperties slackSecretProperties, + MethodsClient methodsClient) { + this.slackSecretProperties = slackSecretProperties.find(SLACK_PROPERTIES_UNIT_NAME); + this.methodsClient = methodsClient; + } + + public void notify(String message) { + try { + methodsClient.chatPostMessage(req -> req + .channel(slackSecretProperties.getChannel()) + .username(slackSecretProperties.getUsername()) + .text(message)); + } catch (Exception e) { + log.warn("Failed. message: {}", message, e); + } + } +} diff --git a/shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/service/slack/shopping/KeywordShoppingCommandProcessor.java b/shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/service/slack/shopping/KeywordShoppingCommandProcessor.java new file mode 100644 index 0000000..63aa0ad --- /dev/null +++ b/shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/service/slack/shopping/KeywordShoppingCommandProcessor.java @@ -0,0 +1,67 @@ +package com.myoa.engineering.crawl.shopping.service.slack.shopping; + +import com.myoa.engineering.crawl.shopping.domain.entity.v2.SubscribedKeyword; +import com.myoa.engineering.crawl.shopping.dto.feed.v1.SubscribedKeywordDTO; +import com.myoa.engineering.crawl.shopping.dto.slack.v2.SlackSlashCommandDTO; +import com.myoa.engineering.crawl.shopping.service.SubscribedKeywordCommandService; +import com.myoa.engineering.crawl.shopping.service.slack.ShoppingCommandProcessor; +import com.myoa.engineering.crawl.shopping.support.dto.constant.CrawlTarget; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.stream.Collectors; + +@Component +public class KeywordShoppingCommandProcessor implements ShoppingCommandProcessor { + + private final SubscribedKeywordCommandService subscribedKeywordCommandService; + + public KeywordShoppingCommandProcessor(SubscribedKeywordCommandService subscribedKeywordCommandService) { + this.subscribedKeywordCommandService = subscribedKeywordCommandService; + } + + @Override + public boolean isAssignable(String menuContext) { + return menuContext.equals("키워드"); + } + + @Override + public String process(SlackSlashCommandDTO requestDTO) { + // "키워드 추가 (쇼핑몰명) (키워드명)" + final String[] splited = requestDTO.getText().split("\\s+", 4); + + final String action = splited[1]; + + switch (action) { + case "추가" -> { + final CrawlTarget crawlTarget = CrawlTarget.fromAlias(splited[2]); + final String keyword = splited[3]; + SubscribedKeyword subscribedKeyword = + SubscribedKeyword.builder() + .crawlTarget(crawlTarget) + .userId(requestDTO.getUser_id()) + .keyword(keyword) + .build(); + subscribedKeywordCommandService.add(subscribedKeyword); + return "keyword: [" + keyword + "] 추가완료"; + } + case "삭제" -> { + final CrawlTarget crawlTarget = CrawlTarget.fromAlias(splited[2]); + final String keyword = splited[3]; + subscribedKeywordCommandService.delete(requestDTO.getUser_id(), crawlTarget, keyword); + return "keyword: [" + keyword + "] 삭제완료"; + } + case "목록" -> { + List keywords = subscribedKeywordCommandService.list(requestDTO.getUser_id()); + if (keywords.isEmpty()) { + return "등록된 키워드가 없습니다."; + } + return keywords.stream().map(SubscribedKeywordDTO::toMessage) + .collect(Collectors.joining("\n")); + } + default -> { + return "지원하지 않는 명령어입니다."; + } + } + } +} diff --git a/shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/service/slack/shopping/ListShoppingCommandProcessor.java b/shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/service/slack/shopping/ListShoppingCommandProcessor.java new file mode 100644 index 0000000..17e8da7 --- /dev/null +++ b/shopping-crawler/src/main/java/com/myoa/engineering/crawl/shopping/service/slack/shopping/ListShoppingCommandProcessor.java @@ -0,0 +1,25 @@ +package com.myoa.engineering.crawl.shopping.service.slack.shopping; + +import com.myoa.engineering.crawl.shopping.dto.slack.v2.SlackSlashCommandDTO; +import com.myoa.engineering.crawl.shopping.service.slack.ShoppingCommandProcessor; +import com.myoa.engineering.crawl.shopping.support.dto.constant.CrawlTarget; +import org.springframework.stereotype.Component; + +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@Component +public class ListShoppingCommandProcessor implements ShoppingCommandProcessor { + + @Override + public boolean isAssignable(String menuContext) { + return menuContext.equals("목록"); + } + + @Override + public String process(SlackSlashCommandDTO requestDTO) { + return Stream.of(CrawlTarget.values()) + .map(e -> e.getAlias() + " | " + e.isAvailable()) + .collect(Collectors.joining("\n")); + } +} diff --git a/shopping-crawler/src/main/resources/application-development.yml b/shopping-crawler/src/main/resources/application-development.yml deleted file mode 100644 index e383370..0000000 --- a/shopping-crawler/src/main/resources/application-development.yml +++ /dev/null @@ -1,12 +0,0 @@ -spring: - config: - activate: - on-profile: development - import: - - "configserver:http://192.168.0.100:11080" - - -server: - port: 20081 - - # import: optional:configserver:http://localhost:11080 # can be start up even config server was not found. \ No newline at end of file diff --git a/shopping-crawler/src/main/resources/application-prod.yml b/shopping-crawler/src/main/resources/application-prod.yml new file mode 100644 index 0000000..d88c46c --- /dev/null +++ b/shopping-crawler/src/main/resources/application-prod.yml @@ -0,0 +1,23 @@ +spring: + config: + activate: + on-profile: prod + import: + - classpath:/datasource/prod.yml + - classpath:/slack/prod.yml + devtools: + livereload: + enabled: false + restart: + enabled: false + +server: + port: 20080 + + # import: optional:configserver:http://localhost:11080 # can be start up even config server was not found. + +feign: + client: + config: + default: + loggerLevel: FULL diff --git a/shopping-crawler/src/main/resources/application-production.yml b/shopping-crawler/src/main/resources/application-production.yml deleted file mode 100644 index 4ff32ed..0000000 --- a/shopping-crawler/src/main/resources/application-production.yml +++ /dev/null @@ -1,6 +0,0 @@ -spring: - config: - activate: - on-profile: production - import: - - "configserver:http://ppn-config-server:20080" diff --git a/shopping-crawler/src/main/resources/application.yml b/shopping-crawler/src/main/resources/application.yml index bfd63e5..2ea2f07 100644 --- a/shopping-crawler/src/main/resources/application.yml +++ b/shopping-crawler/src/main/resources/application.yml @@ -7,8 +7,7 @@ spring: active: ${SPRING_ACTIVE_PROFILE:local} group: local: "local,datasource-local,webclient-local" - development: "development,datasource-development,webclient-development" - production: "production,datasource-production,webclient-production" + prod: "prod" freemarker: enabled: false cloud: diff --git a/shopping-crawler/src/main/resources/logback-spring.xml b/shopping-crawler/src/main/resources/logback-spring.xml index 907a4f5..b591e19 100644 --- a/shopping-crawler/src/main/resources/logback-spring.xml +++ b/shopping-crawler/src/main/resources/logback-spring.xml @@ -2,14 +2,14 @@ - + - - + + - - + + \ No newline at end of file diff --git a/shopping-crawler/src/main/resources/logback/logback-development.xml b/shopping-crawler/src/main/resources/logback/logback-dev.xml similarity index 77% rename from shopping-crawler/src/main/resources/logback/logback-development.xml rename to shopping-crawler/src/main/resources/logback/logback-dev.xml index 458e3d8..a6edb75 100644 --- a/shopping-crawler/src/main/resources/logback/logback-development.xml +++ b/shopping-crawler/src/main/resources/logback/logback-dev.xml @@ -3,15 +3,12 @@ - - - diff --git a/shopping-crawler/src/main/resources/logback/logback-production.xml b/shopping-crawler/src/main/resources/logback/logback-prod.xml similarity index 77% rename from shopping-crawler/src/main/resources/logback/logback-production.xml rename to shopping-crawler/src/main/resources/logback/logback-prod.xml index f824e41..f71a555 100644 --- a/shopping-crawler/src/main/resources/logback/logback-production.xml +++ b/shopping-crawler/src/main/resources/logback/logback-prod.xml @@ -3,15 +3,12 @@ - - - diff --git a/shopping-crawler/src/test/java/com/myoa/engineering/crawl/shopping/controller/SlackEventSubscriptionAPIControllerTest.java b/shopping-crawler/src/test/java/com/myoa/engineering/crawl/shopping/controller/SlackEventSubscriptionAPIControllerTest.java new file mode 100644 index 0000000..e642b0b --- /dev/null +++ b/shopping-crawler/src/test/java/com/myoa/engineering/crawl/shopping/controller/SlackEventSubscriptionAPIControllerTest.java @@ -0,0 +1,33 @@ +package com.myoa.engineering.crawl.shopping.controller; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class SlackEventSubscriptionAPIControllerTest { + + @Test + void test_handleAddSubscription_1() { + // given + String sut = "/쇼핑 키워드 추가 키워드1"; + + // when + String actual = sut.replaceFirst("/.+? ", ""); + + // then + Assertions.assertEquals("키워드 추가 키워드1", actual); + } + + @Test + void test_handleAddSubscription_2() { + // given + String sut = "키워드 추가 키워드 1입니 다"; + + // when + String[] actual = sut.split("\\s+", 2); + + // then + Assertions.assertEquals(2, actual.length); + Assertions.assertEquals("키워드", actual[0]); + Assertions.assertEquals("추가 키워드 1입니 다", actual[1]); + } +} diff --git a/support/src/main/java/com/myoa/engineering/crawl/shopping/support/dto/constant/CrawlTarget.java b/support/src/main/java/com/myoa/engineering/crawl/shopping/support/dto/constant/CrawlTarget.java index 11ba3e5..fb2b552 100644 --- a/support/src/main/java/com/myoa/engineering/crawl/shopping/support/dto/constant/CrawlTarget.java +++ b/support/src/main/java/com/myoa/engineering/crawl/shopping/support/dto/constant/CrawlTarget.java @@ -6,8 +6,20 @@ import lombok.Getter; @Getter @AllArgsConstructor public enum CrawlTarget { - PPOMPPU_DOMESTIC, - PPOMPPU_OVERSEA, - FMKOREA, + PPOMPPU_DOMESTIC("뽐뿌국내", true), + PPOMPPU_OVERSEA("뽐뿌해외", false), + FMKOREA("펨코", false), ; + + private final String alias; + private final boolean available; + + public static CrawlTarget fromAlias(String alias) { + for (CrawlTarget crawlTarget : values()) { + if (crawlTarget.getAlias().equals(alias)) { + return crawlTarget; + } + } + throw new IllegalArgumentException("Not found alias: " + alias); + } }