diff --git a/api/build.gradle b/api/build.gradle deleted file mode 100644 index 5e2a72f..0000000 --- a/api/build.gradle +++ /dev/null @@ -1,21 +0,0 @@ -plugins { - id 'java' - id 'org.springframework.boot' version '3.3.4' - id 'io.spring.dependency-management' version '1.1.6' -} - -dependencies { - implementation project(':common') - implementation project(':service') - implementation project(':collector') - - implementation 'org.springframework.boot:spring-boot-starter' - implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'org.springframework.boot:spring-boot-starter-data-jpa' - - implementation 'org.springframework.boot:spring-boot-starter-security' - testImplementation 'org.springframework.boot:spring-boot-starter-test' - - - -} \ No newline at end of file diff --git a/api/src/main/java/com/whalewatch/WhaleWatchApplication.java b/api/src/main/java/com/whalewatch/WhaleWatchApplication.java deleted file mode 100644 index 2122ded..0000000 --- a/api/src/main/java/com/whalewatch/WhaleWatchApplication.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.whalewatch; - -import com.whalewatch.service.WebSocketManager; -import org.springframework.boot.CommandLineRunner; -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.Bean; - -@SpringBootApplication -public class WhaleWatchApplication { - public static void main(String[] args) { - SpringApplication.run(WhaleWatchApplication.class, args); - } - - @Bean - public CommandLineRunner run(WebSocketManager webSocketManager) { - return args -> { - webSocketManager.startAll(); - }; - } - -} diff --git a/api/src/main/java/com/whalewatch/controller/AlertController.java b/api/src/main/java/com/whalewatch/controller/AlertController.java deleted file mode 100644 index 03bc160..0000000 --- a/api/src/main/java/com/whalewatch/controller/AlertController.java +++ /dev/null @@ -1,73 +0,0 @@ -package com.whalewatch.controller; - -import com.whalewatch.domain.AlertSetting; -import com.whalewatch.dto.AlertSettingsDto; -import com.whalewatch.dto.UserAlertDto; -import com.whalewatch.mapper.AlertSettingsMapper; -import com.whalewatch.mapper.UserAlertMapper; -import com.whalewatch.service.AlertService; -import com.whalewatch.service.UserAlertService; -import org.springframework.web.bind.annotation.*; - -import java.util.List; -import java.util.stream.Collectors; - -@RestController -@RequestMapping("/api/alerts") -public class AlertController { - private final AlertService alertService; - private final AlertSettingsMapper alertSettingsMapper; - - private final UserAlertService userAlertService; - private final UserAlertMapper userAlertMapper; - - - public AlertController(AlertService alertService, - AlertSettingsMapper alertSettingsMapper, - UserAlertService userAlertService, - UserAlertMapper userAlertMapper) { - this.alertService = alertService; - this.alertSettingsMapper = alertSettingsMapper; - this.userAlertService = userAlertService; - this.userAlertMapper = userAlertMapper; - } - - @GetMapping() - public List getAlert() { - return alertService.getAllAlerts().stream() - .map(alertSettingsMapper::toDto) - .collect(Collectors.toList()); - } - - //알림 생성 - @PutMapping() - public AlertSettingsDto createAlert(@RequestBody AlertSettingsDto settings){ - AlertSetting entity = alertSettingsMapper.toEntity(settings); //Dto -> entity - AlertSetting saved = alertService.createAlert(entity); - return alertSettingsMapper.toDto(saved); //entity -> dto - } - - @PostMapping("/{id}") - public AlertSettingsDto updateAlert(@PathVariable int id,@RequestBody AlertSettingsDto settings){ - AlertSetting entity = alertSettingsMapper.toEntity(settings); - AlertSetting updated = alertService.updateAlert(id,entity); - return alertSettingsMapper.toDto(updated); - } - - - //user가 설정한 임계값을 넘는 코인 조회 - - @GetMapping("/history") - public List getAllUserAlerts() { - return userAlertService.getAllAlerts().stream() - .map(userAlertMapper::toDto) - .collect(Collectors.toList()); - } - - @GetMapping("/history/{userId}") - public List getUserAlerts(@PathVariable Integer userId) { - return userAlertService.getAlertsByUserId(userId).stream() - .map(userAlertMapper::toDto) - .collect(Collectors.toList()); - } -} diff --git a/api/src/test/java/com/whalewatch/controller/AlertControllerTest.java b/api/src/test/java/com/whalewatch/controller/AlertControllerTest.java deleted file mode 100644 index e783fc0..0000000 --- a/api/src/test/java/com/whalewatch/controller/AlertControllerTest.java +++ /dev/null @@ -1,96 +0,0 @@ -package com.whalewatch.controller; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.whalewatch.domain.AlertSetting; -import com.whalewatch.repository.AlertRepository; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.http.MediaType; -import org.springframework.test.web.servlet.MockMvc; - -import java.util.List; - -import static org.hamcrest.Matchers.is; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; - -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -@AutoConfigureMockMvc -class AlertControllerTest { - - @Autowired - private MockMvc mockMvc; - - @Autowired - private AlertRepository alertRepository; - - @Autowired - private ObjectMapper objectMapper; - - @BeforeEach - void setup() { - alertRepository.deleteAll(); - - alertRepository.save(new AlertSetting("BTC", 30000, true)); - } - - @Test - @DisplayName("GET /api/alerts") - void testGetAlerts() throws Exception { - // given - - // when & then - mockMvc.perform(get("/api/alerts")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$[0].coin", is("BTC"))) - .andExpect(jsonPath("$[0].threshold", is(30000))) - .andExpect(jsonPath("$[0].notifyByEmail", is(true))); - } - - @Test - @DisplayName("PUT /api/alerts ") - void testCreateAlert() throws Exception { - // given - AlertSetting request = new AlertSetting("ETH", 2000, true); - - // when - mockMvc.perform(put("/api/alerts") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.coin").value("ETH")) - .andExpect(jsonPath("$.threshold").value(2000)) - .andExpect(jsonPath("$.notifyByEmail").value(true)); - - // then - DB 확인 - List all = alertRepository.findAll(); - assertEquals(2, all.size()); - AlertSetting savedAlert = all.get(1); // 저장된 객체 가져오기 - assertEquals("ETH", savedAlert.getCoin()); // coin 값 검증 - assertEquals(2000, savedAlert.getThreshold()); // threshold 값 검증 - assertEquals(true, savedAlert.isNotifyByEmail()); // notifyByEmail 값 검증 - } - - @Test - @DisplayName("POST /api/alerts/{id}") - void testUpdateAlert() throws Exception { - // given - - AlertSetting first = alertRepository.findAll().get(0); //BTC - AlertSetting request = new AlertSetting("DOGE", 1000, true); - - // when & then - mockMvc.perform(post("/api/alerts/" + first.getId()) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.coin").value("DOGE")) - .andExpect(jsonPath("$.threshold").value(1000)) - .andExpect(jsonPath("$.notifyByEmail").value(true)); - } -} diff --git a/api/src/test/java/com/whalewatch/controller/PostControllerTest.java b/api/src/test/java/com/whalewatch/controller/PostControllerTest.java deleted file mode 100644 index 9b53efe..0000000 --- a/api/src/test/java/com/whalewatch/controller/PostControllerTest.java +++ /dev/null @@ -1,89 +0,0 @@ -package com.whalewatch.controller; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.whalewatch.domain.Post; -import com.whalewatch.repository.PostRepository; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.http.MediaType; -import org.springframework.test.web.servlet.MockMvc; - -import java.util.List; - -import static org.hamcrest.Matchers.is; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -@AutoConfigureMockMvc -public class PostControllerTest { - - @Autowired - private MockMvc mockMvc; - - @Autowired - private PostRepository postRepository; - - @Autowired - private ObjectMapper objectMapper; - - @BeforeEach - void setup() { - postRepository.deleteAll(); // DB 초기화 - - postRepository.save(new Post("Title1", "Content1")); - } - - @Test - void testGetPosts() throws Exception { - // given - - // when & then - mockMvc.perform(get("/api/posts")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$[0].title", is("Title1"))) - .andExpect(jsonPath("$[0].content", is("Content1"))); - } - - @Test - void testCreatePost() throws Exception { - // given - Post request = new Post("New Title", "New Content"); - - // when - mockMvc.perform(post("/api/posts") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.title").value("New Title")) - .andExpect(jsonPath("$.content").value("New Content")); - - // then - List all = postRepository.findAll(); - assertEquals(2, all.size()); - assertEquals("New Title", all.get(1).getTitle()); - assertEquals("New Content", all.get(1).getContent()); - } - - @Test - void testUpdatePost() throws Exception { - // given - Post first = postRepository.findAll().get(0); - Post updateRequest = new Post("Updated Title", "Updated Content"); - - // when & then - mockMvc.perform(post("/api/posts/" + first.getId()) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(updateRequest))) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.title").value("Updated Title")) - .andExpect(jsonPath("$.content").value("Updated Content")); - - } -} diff --git a/api/src/test/java/com/whalewatch/controller/TransactionControllerTest.java b/api/src/test/java/com/whalewatch/controller/TransactionControllerTest.java deleted file mode 100644 index 41a0bf8..0000000 --- a/api/src/test/java/com/whalewatch/controller/TransactionControllerTest.java +++ /dev/null @@ -1,60 +0,0 @@ -package com.whalewatch.controller; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.whalewatch.domain.Transaction; -import com.whalewatch.repository.TransactionRepository; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.web.servlet.MockMvc; - -import java.util.List; - -import static org.hamcrest.Matchers.is; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -@AutoConfigureMockMvc -public class TransactionControllerTest { - - @Autowired - private MockMvc mockMvc; - - @Autowired - private TransactionRepository transactionRepository; - - @Autowired - private ObjectMapper objectMapper; - - @BeforeEach - void setup() { - transactionRepository.deleteAll(); - // 기본 데이터 2개 삽입 - transactionRepository.save(new Transaction("0xabc123", "BTC", 20000)); - transactionRepository.save(new Transaction("0xdef456", "ETH", 15000)); - } - - @Test - void testGetAllTransactions() throws Exception { - mockMvc.perform(get("/api/transactions/list")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$[0].hash", is("0xabc123"))) - .andExpect(jsonPath("$[1].coin", is("ETH"))); - } - - @Test - void testGetTransactionById() throws Exception { - List list = transactionRepository.findAll(); - Transaction first = list.get(0); - - mockMvc.perform(get("/api/transactions/" + first.getId())) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.hash", is("0xabc123"))) - .andExpect(jsonPath("$.coin", is("BTC"))); - } - -} diff --git a/api/src/test/java/com/whalewatch/controller/UserControllerTest.java b/api/src/test/java/com/whalewatch/controller/UserControllerTest.java deleted file mode 100644 index a664bfb..0000000 --- a/api/src/test/java/com/whalewatch/controller/UserControllerTest.java +++ /dev/null @@ -1,152 +0,0 @@ -package com.whalewatch.controller; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.whalewatch.domain.User; -import com.whalewatch.dto.TokenResponseDto; -import com.whalewatch.dto.UserDto; -import com.whalewatch.repository.JwtTokenRepository; -import com.whalewatch.repository.UserRepository; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.http.MediaType; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.test.context.TestPropertySource; -import org.springframework.test.web.servlet.MockMvc; - -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -@AutoConfigureMockMvc -@TestPropertySource(properties = { - "jwt.secret-key=zTjEp7AUmDS+bUZKV5OFIVUtFL7EQCMflxiZ3gxpxo0=", - "jwt.access-token-validity-in-seconds=600", - "jwt.refresh-token-validity-in-seconds=1209600" -}) -public class UserControllerTest { - - @Autowired - private MockMvc mockMvc; - - @Autowired - private UserRepository userRepository; - - @Autowired - private ObjectMapper objectMapper; - - @Autowired - private JwtTokenRepository jwtTokenRepository; - - @Autowired - private PasswordEncoder passwordEncoder; - - @BeforeEach - void setup() { - jwtTokenRepository.deleteAll(); - userRepository.deleteAll(); - - String hashed = passwordEncoder.encode("1234"); - userRepository.save(new User("test@test.com", "tester", hashed)); - } - - @Test - void testRegisterUser() throws Exception { - // given - UserDto request = new UserDto(); - request.setEmail("test2@test.com"); - request.setUsername("tester2"); - request.setPassword("1234"); - - // when - mockMvc.perform(post("/api/users") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.id").exists()) - .andExpect(jsonPath("$.email").value("test2@test.com")) - .andExpect(jsonPath("$.username").value("tester2")); - - List all = userRepository.findAll(); - assertEquals(2, all.size()); - } - - @Test - void testLoginUser() throws Exception { - // given - UserDto request = new UserDto(); - request.setEmail("test@test.com"); - request.setPassword("1234"); - - // when - mockMvc.perform(post("/api/users/login") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.accessToken").exists()) - .andExpect(jsonPath("$.refreshToken").exists()); - } - - @Test - void testGetUserInfo() throws Exception { - // given - User user = userRepository.findAll().get(0); - - // 로그인 - UserDto loginRequest = new UserDto(); - loginRequest.setEmail("test@test.com"); - loginRequest.setPassword("1234"); - - String loginResponse = mockMvc.perform(post("/api/users/login") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(loginRequest))) - .andExpect(status().isOk()) - .andReturn().getResponse().getContentAsString(); - - TokenResponseDto tokens = objectMapper.readValue(loginResponse, TokenResponseDto.class); - String accessToken = tokens.getAccessToken(); - - // when & then - mockMvc.perform(get("/api/users/" + user.getId()) - .header("Authorization", "Bearer " + accessToken)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.email").value("test@test.com")) - .andExpect(jsonPath("$.username").value("tester")); - } - - @Test - void testRefreshToken() throws Exception { - //given - UserDto loginRequest = new UserDto(); - loginRequest.setEmail("test@test.com"); - loginRequest.setPassword("1234"); - - //when - String loginResponse = mockMvc.perform(post("/api/users/login") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(loginRequest))) - .andExpect(status().isOk()) - .andReturn().getResponse().getContentAsString(); - - TokenResponseDto tokens = objectMapper.readValue(loginResponse, TokenResponseDto.class); - String refreshToken = tokens.getRefreshToken(); - - //given - accesstoken 재발급 - TokenResponseDto refreshRequest = new TokenResponseDto("", refreshToken); - - // then - mockMvc.perform(post("/api/users/refresh") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(refreshRequest))) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.accessToken").exists()) - .andExpect(jsonPath("$.refreshToken").value(refreshToken)); - } -} diff --git a/collector/build.gradle b/collector/build.gradle index 99f1e41..e53c610 100644 --- a/collector/build.gradle +++ b/collector/build.gradle @@ -5,7 +5,7 @@ plugins { } dependencies { - implementation project(':service') + implementation project(':common') // SpringBoot , Websocket implementation 'org.springframework.boot:spring-boot-starter-websocket' @@ -19,6 +19,9 @@ dependencies { //JsonPath implementation 'com.jayway.jsonpath:json-path:2.9.0' + + //Kafka + implementation 'org.springframework.kafka:spring-kafka' } bootJar { diff --git a/collector/src/main/java/com/whalewatch/CollectorApplication.java b/collector/src/main/java/com/whalewatch/CollectorApplication.java index e157576..f5f04f0 100644 --- a/collector/src/main/java/com/whalewatch/CollectorApplication.java +++ b/collector/src/main/java/com/whalewatch/CollectorApplication.java @@ -1,11 +1,21 @@ package com.whalewatch; +import com.whalewatch.service.WebSocketManager; +import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; @SpringBootApplication public class CollectorApplication { public static void main(String[] args) { SpringApplication.run(CollectorApplication.class, args); } + + @Bean + public CommandLineRunner run(WebSocketManager webSocketManager) { + return args -> { + webSocketManager.startAll(); + }; + } } \ No newline at end of file diff --git a/collector/src/main/java/com/whalewatch/ExchangesProperties.java b/collector/src/main/java/com/whalewatch/config/ExchangesProperties.java similarity index 98% rename from collector/src/main/java/com/whalewatch/ExchangesProperties.java rename to collector/src/main/java/com/whalewatch/config/ExchangesProperties.java index a3e5d2f..1e4a7ea 100644 --- a/collector/src/main/java/com/whalewatch/ExchangesProperties.java +++ b/collector/src/main/java/com/whalewatch/config/ExchangesProperties.java @@ -1,4 +1,4 @@ -package com.whalewatch; +package com.whalewatch.config; import org.springframework.boot.context.properties.ConfigurationProperties; diff --git a/collector/src/main/java/com/whalewatch/TradeDto.java b/collector/src/main/java/com/whalewatch/dto/TradeDto.java similarity index 98% rename from collector/src/main/java/com/whalewatch/TradeDto.java rename to collector/src/main/java/com/whalewatch/dto/TradeDto.java index 9c782e2..5a44927 100644 --- a/collector/src/main/java/com/whalewatch/TradeDto.java +++ b/collector/src/main/java/com/whalewatch/dto/TradeDto.java @@ -1,4 +1,4 @@ -package com.whalewatch; +package com.whalewatch.dto; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/collector/src/main/java/com/whalewatch/kafka/TransactionProducer.java b/collector/src/main/java/com/whalewatch/kafka/TransactionProducer.java new file mode 100644 index 0000000..71395db --- /dev/null +++ b/collector/src/main/java/com/whalewatch/kafka/TransactionProducer.java @@ -0,0 +1,24 @@ +package com.whalewatch.kafka; + +import com.whalewatch.dto.TransactionEventDto; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.stereotype.Component; + +@Component +public class TransactionProducer { + private static final Logger log = LoggerFactory.getLogger(TransactionProducer.class); + + private final KafkaTemplate kafkaTemplate; + + public TransactionProducer(KafkaTemplate kafkaTemplate) { + this.kafkaTemplate = kafkaTemplate; + } + + public void sendTransactionEvent(TransactionEventDto event) { + String topicName = "transaction_event"; + kafkaTemplate.send(topicName, event); + log.info("[TransactionProducer] Sent event to Kafka topic '{}': {}", topicName, event); + } +} diff --git a/collector/src/main/java/com/whalewatch/service/FilteringService.java b/collector/src/main/java/com/whalewatch/service/FilteringService.java index ced9e80..78d3b21 100644 --- a/collector/src/main/java/com/whalewatch/service/FilteringService.java +++ b/collector/src/main/java/com/whalewatch/service/FilteringService.java @@ -1,9 +1,9 @@ package com.whalewatch.service; -import com.whalewatch.ExchangesProperties; -import com.whalewatch.TradeDto; -import com.whalewatch.domain.Transaction; -import com.whalewatch.transaction.TransactionService; +import com.whalewatch.config.ExchangesProperties; +import com.whalewatch.dto.TradeDto; +import com.whalewatch.dto.TransactionEventDto; +import com.whalewatch.kafka.TransactionProducer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; @@ -12,12 +12,14 @@ public class FilteringService { private static final Logger log = LoggerFactory.getLogger(FilteringService.class); + private final ExchangesProperties exchangesProperties; - private final TransactionService transactionService; + private final TransactionProducer transactionProducer; - public FilteringService(TransactionService transactionService, ExchangesProperties exchangesProperties) { - this.transactionService = transactionService; + public FilteringService(ExchangesProperties exchangesProperties, + TransactionProducer transactionProducer) { this.exchangesProperties = exchangesProperties; + this.transactionProducer = transactionProducer; } public void adminFiltering(TradeDto dto) { @@ -37,14 +39,16 @@ public void adminFiltering(TradeDto dto) { if (dto.getTradeVolume() > threshold) { log.info("[ADMIN][{}] coin={}, volume={} > threshold({}) => Save DB", dto.getExchange(), dto.getCode(), dto.getTradeVolume(), threshold); - Transaction tx = new Transaction( + TransactionEventDto eventDto = new TransactionEventDto( + 0, dto.getCode(), dto.getTradePrice(), dto.getTradeVolume(), dto.getAskBid(), dto.getTradeTimestamp() ); - transactionService.createTransaction(tx); + + transactionProducer.sendTransactionEvent(eventDto); } } } diff --git a/collector/src/main/java/com/whalewatch/service/ParsingService.java b/collector/src/main/java/com/whalewatch/service/ParsingService.java index 7fd977a..853aaf8 100644 --- a/collector/src/main/java/com/whalewatch/service/ParsingService.java +++ b/collector/src/main/java/com/whalewatch/service/ParsingService.java @@ -1,11 +1,10 @@ package com.whalewatch.service; -import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.jayway.jsonpath.JsonPath; import com.jayway.jsonpath.PathNotFoundException; -import com.whalewatch.ExchangesProperties; -import com.whalewatch.TradeDto; +import com.whalewatch.config.ExchangesProperties; +import com.whalewatch.dto.TradeDto; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; diff --git a/collector/src/main/java/com/whalewatch/service/WebSocketManager.java b/collector/src/main/java/com/whalewatch/service/WebSocketManager.java index ab257e8..051d6da 100644 --- a/collector/src/main/java/com/whalewatch/service/WebSocketManager.java +++ b/collector/src/main/java/com/whalewatch/service/WebSocketManager.java @@ -1,6 +1,6 @@ package com.whalewatch.service; -import com.whalewatch.ExchangesProperties; +import com.whalewatch.config.ExchangesProperties; import org.springframework.stereotype.Service; import java.util.Map; diff --git a/collector/src/main/java/com/whalewatch/service/WebsocketService.java b/collector/src/main/java/com/whalewatch/service/WebsocketService.java index 21f7ab1..14f0fc7 100644 --- a/collector/src/main/java/com/whalewatch/service/WebsocketService.java +++ b/collector/src/main/java/com/whalewatch/service/WebsocketService.java @@ -1,6 +1,6 @@ package com.whalewatch.service; -import com.whalewatch.ExchangesProperties; +import com.whalewatch.config.ExchangesProperties; import org.springframework.web.socket.WebSocketHandler; import java.util.Map; diff --git a/collector/src/main/resources/application.yml b/collector/src/main/resources/application.yml new file mode 100644 index 0000000..c50c020 --- /dev/null +++ b/collector/src/main/resources/application.yml @@ -0,0 +1,68 @@ +server: + port: 8081 + +spring: + datasource: + url: ${SPRING_DATASOURCE_URL} + username: ${SPRING_DATASOURCE_USERNAME} + password: ${SPRING_DATASOURCE_PASSWORD} + driverClassName: com.mysql.cj.jdbc.Driver + + kafka: + bootstrap-servers: ${SPRING_KAFKA_BOOTSTRAP_SERVERS:localhost:9093} + producer: + key-serializer: org.apache.kafka.common.serialization.StringSerializer + value-serializer: org.springframework.kafka.support.serializer.JsonSerializer + + +exchanges: + exchanges: + binance: + enabled: true + websocketType: text + url: "wss://stream.binance.com:9443/stream?streams=btcusdt@trade/ethusdt@trade/solusdt@trade" + mapping: + type: "data.e" + code: "data.s" + tradePrice: "data.p" + tradeVolume: "data.q" + tradeTimestamp: "data.T" + askBid: "data.m" + threshold: + BTC: 0.8 + ETH: 22.0 + SOL: 300.0 + upbit: + enabled: true + websocketType: binary + url: "wss://api.upbit.com/websocket/v1" + mapping: + type: "type" + code: "code" + tradePrice: "trade_price" + tradeVolume: "trade_volume" + tradeTimestamp: "trade_timestamp" + askBid: "ask_bid" + threshold: + BTC: 0.8 + ETH: 22.0 + SOL: 300.0 + bithumb: + enabled: true + websocketType: binary + url: "wss://ws-api.bithumb.com/websocket/v1" + mapping: + type: "type" + code: "code" + tradePrice: "trade_price" + tradeVolume: "trade_volume" + tradeTimestamp: "trade_timestamp" + askBid: "ask_bid" + threshold: + BTC: 0.8 + ETH: 22.0 + SOL: 300.0 + +logging: + level: + com.whalewatch: DEBUG diff --git a/common/build.gradle b/common/build.gradle index 93ad05b..e14af54 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -5,14 +5,6 @@ plugins { } dependencies { - // Spring Security - implementation 'org.springframework.boot:spring-boot-starter-security' - - // JWT 설정 - implementation 'io.jsonwebtoken:jjwt-api:0.11.5' - runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' - runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5' - implementation 'org.springframework.boot:spring-boot-starter-web' testImplementation 'org.springframework.boot:spring-boot-starter-test' diff --git a/common/src/main/java/com/whalewatch/config/RedisConfig.java b/common/src/main/java/com/whalewatch/config/RedisConfig.java index af82c31..bc96199 100644 --- a/common/src/main/java/com/whalewatch/config/RedisConfig.java +++ b/common/src/main/java/com/whalewatch/config/RedisConfig.java @@ -28,4 +28,5 @@ public RedisTemplate redisTemplate(RedisConnectionFactory connec template.setValueSerializer(serializer); return template; } -} \ No newline at end of file +} + diff --git a/common/src/main/java/com/whalewatch/dto/ThresholdEventDto.java b/common/src/main/java/com/whalewatch/dto/ThresholdEventDto.java new file mode 100644 index 0000000..99d16d2 --- /dev/null +++ b/common/src/main/java/com/whalewatch/dto/ThresholdEventDto.java @@ -0,0 +1,39 @@ +package com.whalewatch.dto; + +import java.io.Serializable; + +public class ThresholdEventDto implements Serializable { + private Long chatId; + private String coin; + private Double threshold; + + public ThresholdEventDto() { + } + + public ThresholdEventDto(Long chatId, String coin, Double threshold) { + this.chatId = chatId; + this.coin = coin; + this.threshold = threshold; + } + + public Long getChatId() { + return chatId; + } + public void setChatId(Long chatId) { + this.chatId = chatId; + } + + public String getCoin() { + return coin; + } + public void setCoin(String coin) { + this.coin = coin; + } + + public Double getThreshold() { + return threshold; + } + public void setThreshold(Double threshold) { + this.threshold = threshold; + } +} diff --git a/common/src/main/java/com/whalewatch/dto/UserOtpEventDto.java b/common/src/main/java/com/whalewatch/dto/UserOtpEventDto.java new file mode 100644 index 0000000..67bb41f --- /dev/null +++ b/common/src/main/java/com/whalewatch/dto/UserOtpEventDto.java @@ -0,0 +1,32 @@ +package com.whalewatch.dto; + +import java.io.Serializable; + +public class UserOtpEventDto implements Serializable { + private Long chatId; + private String message; + + public UserOtpEventDto() { + } + + public UserOtpEventDto(Long chatId, String message) { + this.chatId = chatId; + this.message = message; + } + + public Long getChatId() { + return chatId; + } + + public void setChatId(Long chatId) { + this.chatId = chatId; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } +} diff --git a/common/src/main/java/com/whalewatch/dto/UserRegistrationEventDto.java b/common/src/main/java/com/whalewatch/dto/UserRegistrationEventDto.java new file mode 100644 index 0000000..f68164b --- /dev/null +++ b/common/src/main/java/com/whalewatch/dto/UserRegistrationEventDto.java @@ -0,0 +1,38 @@ +package com.whalewatch.dto; + +import java.io.Serializable; + +public class UserRegistrationEventDto implements Serializable { + private Long chatId; + private String email; + private String username; + + public UserRegistrationEventDto() { } + + public UserRegistrationEventDto(Long chatId, String email, String username) { + this.chatId = chatId; + this.email = email; + this.username = username; + } + + public Long getChatId() { + return chatId; + } + public void setChatId(Long chatId) { + this.chatId = chatId; + } + + public String getEmail() { + return email; + } + public void setEmail(String email) { + this.email = email; + } + + public String getUsername() { + return username; + } + public void setUsername(String username) { + this.username = username; + } +} diff --git a/common/src/main/java/com/whalewatch/dto/UserRegistrationResultEventDto.java b/common/src/main/java/com/whalewatch/dto/UserRegistrationResultEventDto.java new file mode 100644 index 0000000..4c22051 --- /dev/null +++ b/common/src/main/java/com/whalewatch/dto/UserRegistrationResultEventDto.java @@ -0,0 +1,38 @@ +package com.whalewatch.dto; + +import java.io.Serializable; + +public class UserRegistrationResultEventDto implements Serializable { + private Long chatId; + private boolean success; + private String message; + + public UserRegistrationResultEventDto() { } + + public UserRegistrationResultEventDto(Long chatId, boolean success, String message) { + this.chatId = chatId; + this.success = success; + this.message = message; + } + + public Long getChatId() { + return chatId; + } + public void setChatId(Long chatId) { + this.chatId = chatId; + } + + public boolean isSuccess() { + return success; + } + public void setSuccess(boolean success) { + this.success = success; + } + + public String getMessage() { + return message; + } + public void setMessage(String message) { + this.message = message; + } +} diff --git a/api/src/main/java/com/whalewatch/exception/ErrorResponse.java b/common/src/main/java/com/whalewatch/exception/ErrorResponse.java similarity index 100% rename from api/src/main/java/com/whalewatch/exception/ErrorResponse.java rename to common/src/main/java/com/whalewatch/exception/ErrorResponse.java diff --git a/api/src/main/java/com/whalewatch/exception/GlobalExceptionHandler.java b/common/src/main/java/com/whalewatch/exception/GlobalExceptionHandler.java similarity index 100% rename from api/src/main/java/com/whalewatch/exception/GlobalExceptionHandler.java rename to common/src/main/java/com/whalewatch/exception/GlobalExceptionHandler.java diff --git a/service/build.gradle b/notification-service/build.gradle similarity index 62% rename from service/build.gradle rename to notification-service/build.gradle index 09bf17f..06152c5 100644 --- a/service/build.gradle +++ b/notification-service/build.gradle @@ -3,23 +3,19 @@ plugins { id 'org.springframework.boot' version '3.3.4' id 'io.spring.dependency-management' } + dependencies { - // common 모듈(Mapper에서 DTO 참조) implementation project(':common') + // Spring Boot, JPA implementation 'org.springframework.boot:spring-boot-starter-web' - - //JPA + DB implementation 'org.springframework.boot:spring-boot-starter-data-jpa' - // MapStruct - implementation 'org.mapstruct:mapstruct:1.5.5.Final' - annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final' - - //Security - implementation 'org.springframework.boot:spring-boot-starter-security' + //Kafka + implementation 'org.springframework.kafka:spring-kafka' - testImplementation 'org.springframework.boot:spring-boot-starter-test' + // Redis + implementation 'org.springframework.boot:spring-boot-starter-data-redis' //telegramBot implementation 'org.telegram:telegrambots-spring-boot-starter:6.9.7.1' @@ -29,13 +25,10 @@ dependencies { implementation 'org.glassfish.jaxb:jaxb-runtime:2.3.1' implementation 'javax.ws.rs:javax.ws.rs-api:2.1.1' - //Kafka 설정 - implementation 'org.springframework.kafka:spring-kafka' - - // Redis - implementation 'org.springframework.boot:spring-boot-starter-data-redis' + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1' } -bootJar { - archiveFileName = 'whalewatch-service.jar' +test { + useJUnitPlatform() } \ No newline at end of file diff --git a/notification-service/src/main/java/com/whalewatch/NotificationServiceApplication.java b/notification-service/src/main/java/com/whalewatch/NotificationServiceApplication.java new file mode 100644 index 0000000..d22a4b5 --- /dev/null +++ b/notification-service/src/main/java/com/whalewatch/NotificationServiceApplication.java @@ -0,0 +1,11 @@ +package com.whalewatch; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class NotificationServiceApplication { + public static void main(String[] args) { + SpringApplication.run(NotificationServiceApplication.class, args); + } +} diff --git a/service/src/main/java/com/whalewatch/domain/AlertSetting.java b/notification-service/src/main/java/com/whalewatch/domain/AlertSetting.java similarity index 86% rename from service/src/main/java/com/whalewatch/domain/AlertSetting.java rename to notification-service/src/main/java/com/whalewatch/domain/AlertSetting.java index 7f2fc53..50b1db9 100644 --- a/service/src/main/java/com/whalewatch/domain/AlertSetting.java +++ b/notification-service/src/main/java/com/whalewatch/domain/AlertSetting.java @@ -12,11 +12,13 @@ public class AlertSetting { private Integer userId; + private Long chatId; + private String coin; private double threshold; private boolean notifyByEmail; - protected AlertSetting() {} + public AlertSetting() {} public AlertSetting(String coin, double threshold, boolean notifyByEmail) { this.coin = coin; @@ -59,4 +61,12 @@ public Integer getUserId() { public void setUserId(Integer userId) { this.userId = userId; } + + public Long getChatId() { + return chatId; + } + + public void setChatId(Long chatId) { + this.chatId = chatId; + } } diff --git a/service/src/main/java/com/whalewatch/domain/UserAlert.java b/notification-service/src/main/java/com/whalewatch/domain/UserAlert.java similarity index 100% rename from service/src/main/java/com/whalewatch/domain/UserAlert.java rename to notification-service/src/main/java/com/whalewatch/domain/UserAlert.java diff --git a/common/src/main/java/com/whalewatch/dto/AlertSettingsDto.java b/notification-service/src/main/java/com/whalewatch/dto/AlertSettingsDto.java similarity index 100% rename from common/src/main/java/com/whalewatch/dto/AlertSettingsDto.java rename to notification-service/src/main/java/com/whalewatch/dto/AlertSettingsDto.java diff --git a/common/src/main/java/com/whalewatch/dto/UserAlertDto.java b/notification-service/src/main/java/com/whalewatch/dto/UserAlertDto.java similarity index 100% rename from common/src/main/java/com/whalewatch/dto/UserAlertDto.java rename to notification-service/src/main/java/com/whalewatch/dto/UserAlertDto.java diff --git a/notification-service/src/main/java/com/whalewatch/kafka/UserOtpConsumer.java b/notification-service/src/main/java/com/whalewatch/kafka/UserOtpConsumer.java new file mode 100644 index 0000000..f432660 --- /dev/null +++ b/notification-service/src/main/java/com/whalewatch/kafka/UserOtpConsumer.java @@ -0,0 +1,37 @@ +package com.whalewatch.kafka; + +import com.whalewatch.dto.UserOtpEventDto; +import com.whalewatch.telegram.TelegramWebhookUserBot; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.kafka.support.Acknowledgment; +import org.springframework.stereotype.Component; + +@Component +public class UserOtpConsumer { + + private static final Logger log = LoggerFactory.getLogger(UserOtpConsumer.class); + + private final TelegramWebhookUserBot telegramBot; + + public UserOtpConsumer(TelegramWebhookUserBot telegramBot) { + this.telegramBot = telegramBot; + } + + @KafkaListener(topics = "user_otp_topic", groupId = "whalewatch_otp") + public void onUserOtp(ConsumerRecord record, Acknowledgment ack) { + UserOtpEventDto event = record.value(); + log.info("[UserOtpConsumer] Received OTP event: {}", event); + + try { + telegramBot.sendTextMessage(event.getChatId(), event.getMessage()); + log.info("[UserOtpConsumer] OTP message sent to chatId={}", event.getChatId()); + } catch (Exception e) { + log.error("Failed to send OTP via telegramBot: {}", e.getMessage(), e); + } finally { + ack.acknowledge(); + } + } +} \ No newline at end of file diff --git a/notification-service/src/main/java/com/whalewatch/kafka/UserRegistrationResultConsumer.java b/notification-service/src/main/java/com/whalewatch/kafka/UserRegistrationResultConsumer.java new file mode 100644 index 0000000..05dc0d0 --- /dev/null +++ b/notification-service/src/main/java/com/whalewatch/kafka/UserRegistrationResultConsumer.java @@ -0,0 +1,32 @@ +package com.whalewatch.kafka; + + +import com.whalewatch.dto.UserRegistrationResultEventDto; +import com.whalewatch.telegram.TelegramWebhookUserBot; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.stereotype.Component; + +@Component +public class UserRegistrationResultConsumer { + private static final Logger log = LoggerFactory.getLogger(UserRegistrationResultConsumer.class); + private final TelegramWebhookUserBot telegramWebhookUserBot; + + public UserRegistrationResultConsumer(TelegramWebhookUserBot telegramWebhookUserBot) { + this.telegramWebhookUserBot = telegramWebhookUserBot; + } + + @KafkaListener(topics = "user_registration_result_topic", groupId = "whalewatch_registration_result") + public void onRegistrationResult(UserRegistrationResultEventDto event) { + log.info("Received UserRegistrationResultEvent: {}", event); + + String msg; + if (event.isSuccess()) { + msg = "Registration success: " + event.getMessage(); + } else { + msg = "Registration failed: " + event.getMessage(); + } + telegramWebhookUserBot.sendTextMessage(event.getChatId(), msg); + } +} diff --git a/notification-service/src/main/java/com/whalewatch/kafka/UserThresholdConsumer.java b/notification-service/src/main/java/com/whalewatch/kafka/UserThresholdConsumer.java new file mode 100644 index 0000000..bc2cf8a --- /dev/null +++ b/notification-service/src/main/java/com/whalewatch/kafka/UserThresholdConsumer.java @@ -0,0 +1,44 @@ +package com.whalewatch.kafka; + +import com.whalewatch.domain.AlertSetting; +import com.whalewatch.dto.ThresholdEventDto; +import com.whalewatch.repository.AlertRepository; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.kafka.support.Acknowledgment; +import org.springframework.stereotype.Component; + +@Component +public class UserThresholdConsumer { + + private static final Logger log = LoggerFactory.getLogger(UserThresholdConsumer.class); + + private final AlertRepository alertRepository; + + public UserThresholdConsumer(AlertRepository alertRepository) { + this.alertRepository = alertRepository; + } + + @KafkaListener(topics = "user_threshold_topic", groupId = "whalewatch_threshold") + public void onUserThreshold(ConsumerRecord record, Acknowledgment ack) { + ThresholdEventDto event = record.value(); + log.info("[UserThresholdConsumer] Received threshold event: {}", event); + + try { + AlertSetting setting = new AlertSetting(); + setting.setCoin(event.getCoin()); + setting.setThreshold(event.getThreshold()); + setting.setChatId(event.getChatId()); + + AlertSetting saved = alertRepository.save(setting); + log.info("[UserThresholdConsumer] AlertSetting created: id={}, coin={}, threshold={}", + saved.getId(), saved.getCoin(), saved.getThreshold()); + } catch (Exception e) { + log.error("Error saving threshold: {}", e.getMessage(), e); + } finally { + ack.acknowledge(); + } + } +} diff --git a/service/src/main/java/com/whalewatch/service/RedisStateService.java b/notification-service/src/main/java/com/whalewatch/redis/RedisStateService.java similarity index 97% rename from service/src/main/java/com/whalewatch/service/RedisStateService.java rename to notification-service/src/main/java/com/whalewatch/redis/RedisStateService.java index 1966407..758bd32 100644 --- a/service/src/main/java/com/whalewatch/service/RedisStateService.java +++ b/notification-service/src/main/java/com/whalewatch/redis/RedisStateService.java @@ -1,4 +1,4 @@ -package com.whalewatch.service; +package com.whalewatch.redis; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; diff --git a/service/src/main/java/com/whalewatch/service/RegistrationData.java b/notification-service/src/main/java/com/whalewatch/redis/RegistrationData.java similarity index 93% rename from service/src/main/java/com/whalewatch/service/RegistrationData.java rename to notification-service/src/main/java/com/whalewatch/redis/RegistrationData.java index 011ea19..afcc4ec 100644 --- a/service/src/main/java/com/whalewatch/service/RegistrationData.java +++ b/notification-service/src/main/java/com/whalewatch/redis/RegistrationData.java @@ -1,4 +1,4 @@ -package com.whalewatch.service; +package com.whalewatch.redis; import java.io.Serializable; diff --git a/service/src/main/java/com/whalewatch/service/ThresholdSettingData.java b/notification-service/src/main/java/com/whalewatch/redis/ThresholdSettingData.java similarity index 93% rename from service/src/main/java/com/whalewatch/service/ThresholdSettingData.java rename to notification-service/src/main/java/com/whalewatch/redis/ThresholdSettingData.java index 55f8965..e268b19 100644 --- a/service/src/main/java/com/whalewatch/service/ThresholdSettingData.java +++ b/notification-service/src/main/java/com/whalewatch/redis/ThresholdSettingData.java @@ -1,4 +1,4 @@ -package com.whalewatch.service; +package com.whalewatch.redis; import java.io.Serializable; diff --git a/service/src/main/java/com/whalewatch/repository/AlertRepository.java b/notification-service/src/main/java/com/whalewatch/repository/AlertRepository.java similarity index 100% rename from service/src/main/java/com/whalewatch/repository/AlertRepository.java rename to notification-service/src/main/java/com/whalewatch/repository/AlertRepository.java diff --git a/service/src/main/java/com/whalewatch/repository/UserAlertRepository.java b/notification-service/src/main/java/com/whalewatch/repository/UserAlertRepository.java similarity index 100% rename from service/src/main/java/com/whalewatch/repository/UserAlertRepository.java rename to notification-service/src/main/java/com/whalewatch/repository/UserAlertRepository.java diff --git a/service/src/main/java/com/whalewatch/service/AlertService.java b/notification-service/src/main/java/com/whalewatch/service/AlertService.java similarity index 100% rename from service/src/main/java/com/whalewatch/service/AlertService.java rename to notification-service/src/main/java/com/whalewatch/service/AlertService.java diff --git a/service/src/main/java/com/whalewatch/service/UserAlertService.java b/notification-service/src/main/java/com/whalewatch/service/UserAlertService.java similarity index 100% rename from service/src/main/java/com/whalewatch/service/UserAlertService.java rename to notification-service/src/main/java/com/whalewatch/service/UserAlertService.java diff --git a/service/src/main/java/com/whalewatch/telegram/TelegramAlert.java b/notification-service/src/main/java/com/whalewatch/telegram/TelegramAlert.java similarity index 56% rename from service/src/main/java/com/whalewatch/telegram/TelegramAlert.java rename to notification-service/src/main/java/com/whalewatch/telegram/TelegramAlert.java index 599ce5f..5f2b512 100644 --- a/service/src/main/java/com/whalewatch/telegram/TelegramAlert.java +++ b/notification-service/src/main/java/com/whalewatch/telegram/TelegramAlert.java @@ -5,7 +5,6 @@ import com.whalewatch.dto.TransactionEventDto; import com.whalewatch.repository.AlertRepository; import com.whalewatch.service.UserAlertService; -import com.whalewatch.service.UserService; import org.apache.kafka.clients.consumer.ConsumerRecord; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -22,22 +21,19 @@ public class TelegramAlert { private final AlertRepository alertRepository; private final UserAlertService userAlertService; - private final UserService userService; private final TelegramWebhookUserBot telegramWebhookUserBot; public TelegramAlert(AlertRepository alertRepository, UserAlertService userAlertService, - UserService userService, TelegramWebhookUserBot telegramWebhookUserBot) { this.alertRepository = alertRepository; this.userAlertService = userAlertService; - this.userService = userService; this.telegramWebhookUserBot = telegramWebhookUserBot; } @KafkaListener( topics = "transaction_event", - groupId = "whalewatch_group", + groupId = "whalewatch_alert", containerFactory = "kafkaListenerContainerFactory" ) public void consume(ConsumerRecord record, Acknowledgment ack) { @@ -54,31 +50,23 @@ public void consume(ConsumerRecord record, Acknowle for (AlertSetting setting : alertSettings) { if (event.getTradeVolume() >= setting.getThreshold()) { - // DB에 사용자 알림 기록 저장 - UserAlert userAlert = new UserAlert( - setting.getUserId(), - event.getCoin(), - event.getTradePrice(), - event.getTradeVolume(), - event.getTradeTimestamp() + // 사용자 알림 기록 + UserAlert savedAlert = userAlertService.createUserAlert( + new UserAlert(setting.getUserId(), event.getCoin(), + event.getTradePrice(), event.getTradeVolume(), + event.getTradeTimestamp()) ); - UserAlert savedAlert = userAlertService.createUserAlert(userAlert); - log.info("User alert created for userId {} for coin {} with alertId {}", - setting.getUserId(), event.getCoin(), savedAlert.getId()); + log.info("User alert created for userId {} with alertId {}", + setting.getUserId(), savedAlert.getId()); - // UserService를 통해 사용자의 Telegram Chat ID 조회 후 알림 전송 - try { - Long chatId = userService.getUserInfo(setting.getUserId()).getTelegramChatId(); - if (chatId != null) { - String message = String.format("Alert ID [%d]: A trade of at least %.2f occurred for %s", - savedAlert.getId(), event.getTradeVolume(), event.getCoin()); - telegramWebhookUserBot.sendTextMessage(chatId, message); - log.info("Telegram alert sent to chatId {}: {}", chatId, message); - } else { - log.warn("No Telegram chat ID for userId {}", setting.getUserId()); - } - } catch (Exception e) { - log.error("Error sending Telegram alert for userId {}: {}", setting.getUserId(), e.getMessage()); + Long chatId = setting.getChatId(); + if (chatId != null) { + String message = String.format("Alert ID [%d]: A trade of at least %.2f occurred for %s", + savedAlert.getId(), event.getTradeVolume(), event.getCoin()); + telegramWebhookUserBot.sendTextMessage(chatId, message); + log.info("Telegram alert sent to chatId {}: {}", chatId, message); + } else { + log.warn("AlertSetting {} has no chatId for userId {}", setting.getId(), setting.getUserId()); } } } diff --git a/service/src/main/java/com/whalewatch/telegram/TelegramBotConfig.java b/notification-service/src/main/java/com/whalewatch/telegram/TelegramBotConfig.java similarity index 80% rename from service/src/main/java/com/whalewatch/telegram/TelegramBotConfig.java rename to notification-service/src/main/java/com/whalewatch/telegram/TelegramBotConfig.java index 9ec0d6a..bb36b9c 100644 --- a/service/src/main/java/com/whalewatch/telegram/TelegramBotConfig.java +++ b/notification-service/src/main/java/com/whalewatch/telegram/TelegramBotConfig.java @@ -1,21 +1,20 @@ package com.whalewatch.telegram; -import com.whalewatch.config.TelegramBotProperties; -import jakarta.annotation.PostConstruct; -import org.springframework.beans.factory.annotation.Value; + + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.telegram.telegrambots.meta.TelegramBotsApi; import org.telegram.telegrambots.meta.api.methods.updates.SetWebhook; -import org.telegram.telegrambots.meta.generics.BotSession; -import org.telegram.telegrambots.meta.generics.Webhook; import org.telegram.telegrambots.updatesreceivers.DefaultBotSession; -import org.telegram.telegrambots.updatesreceivers.DefaultWebhook; @Configuration public class TelegramBotConfig { + private static final Logger log = LoggerFactory.getLogger(TelegramWebhookUserBot.class); private final TelegramBotProperties telegramBotProperties; private final TelegramWebhookUserBot telegramWebhookUserBot; @@ -37,6 +36,7 @@ public TelegramBotsApi telegramBotsApi() throws Exception { SetWebhook setWebhook = SetWebhook.builder().url(webhookUrl).build(); botsApi.registerBot(telegramWebhookUserBot, setWebhook); + log.info("registerBot called with webhookUrl: {}", webhookUrl); return botsApi; } } \ No newline at end of file diff --git a/common/src/main/java/com/whalewatch/config/TelegramBotProperties.java b/notification-service/src/main/java/com/whalewatch/telegram/TelegramBotProperties.java similarity index 95% rename from common/src/main/java/com/whalewatch/config/TelegramBotProperties.java rename to notification-service/src/main/java/com/whalewatch/telegram/TelegramBotProperties.java index a40b88f..a872e9d 100644 --- a/common/src/main/java/com/whalewatch/config/TelegramBotProperties.java +++ b/notification-service/src/main/java/com/whalewatch/telegram/TelegramBotProperties.java @@ -1,4 +1,4 @@ -package com.whalewatch.config; +package com.whalewatch.telegram; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; diff --git a/service/src/main/java/com/whalewatch/telegram/TelegramMessageEvent.java b/notification-service/src/main/java/com/whalewatch/telegram/TelegramMessageEvent.java similarity index 100% rename from service/src/main/java/com/whalewatch/telegram/TelegramMessageEvent.java rename to notification-service/src/main/java/com/whalewatch/telegram/TelegramMessageEvent.java diff --git a/notification-service/src/main/java/com/whalewatch/telegram/TelegramWebhookController.java b/notification-service/src/main/java/com/whalewatch/telegram/TelegramWebhookController.java new file mode 100644 index 0000000..f6f98ae --- /dev/null +++ b/notification-service/src/main/java/com/whalewatch/telegram/TelegramWebhookController.java @@ -0,0 +1,25 @@ +package com.whalewatch.telegram; + +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.RestController; +import org.telegram.telegrambots.meta.api.objects.Update; + +@RestController +public class TelegramWebhookController { + + private final TelegramWebhookUserBot telegramWebhookUserBot; + + public TelegramWebhookController(TelegramWebhookUserBot telegramWebhookUserBot) { + this.telegramWebhookUserBot = telegramWebhookUserBot; + } + + @PostMapping("/telegram/webhook") + public ResponseEntity onUpdateReceived(@RequestBody Update update) { + // TelegramWebhookUserBot 내부의 onWebhookUpdateReceived()를 호출 + telegramWebhookUserBot.onWebhookUpdateReceived(update); + // Telegram은 HTTP 200 OK , 빈 200 응답을 반환합니다. + return ResponseEntity.ok().build(); + } +} diff --git a/notification-service/src/main/java/com/whalewatch/telegram/TelegramWebhookUserBot.java b/notification-service/src/main/java/com/whalewatch/telegram/TelegramWebhookUserBot.java new file mode 100644 index 0000000..7ddc051 --- /dev/null +++ b/notification-service/src/main/java/com/whalewatch/telegram/TelegramWebhookUserBot.java @@ -0,0 +1,161 @@ +package com.whalewatch.telegram; + +import com.whalewatch.domain.AlertSetting; +import com.whalewatch.dto.ThresholdEventDto; +import com.whalewatch.dto.UserRegistrationEventDto; +import com.whalewatch.redis.RedisStateService; +import com.whalewatch.redis.RegistrationData; +import com.whalewatch.redis.ThresholdSettingData; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.stereotype.Component; +import org.telegram.telegrambots.bots.TelegramWebhookBot; +import org.telegram.telegrambots.meta.api.methods.BotApiMethod; +import org.telegram.telegrambots.meta.api.methods.send.SendMessage; +import org.telegram.telegrambots.meta.api.objects.Update; +import org.telegram.telegrambots.meta.exceptions.TelegramApiException; + + +@Component +public class TelegramWebhookUserBot extends TelegramWebhookBot { + private static final Logger log = LoggerFactory.getLogger(TelegramWebhookUserBot.class); + + private final TelegramBotProperties telegramBotProperties; + private final RedisStateService redisStateService; + private final KafkaTemplate kafkaTemplate; + + public TelegramWebhookUserBot(TelegramBotProperties telegramBotProperties, + RedisStateService redisStateService, + KafkaTemplate kafkaTemplate) { + this.telegramBotProperties = telegramBotProperties; + this.redisStateService = redisStateService; + this.kafkaTemplate = kafkaTemplate; + } + + @Override + public String getBotUsername() { + return telegramBotProperties.getUsername(); + } + + @Override + public String getBotToken() { + return telegramBotProperties.getToken(); + } + + @Override + public String getBotPath() { + return "/telegram/webhook"; + } + + @Override + public BotApiMethod onWebhookUpdateReceived(Update update) { + try { + if (!update.hasMessage() || !update.getMessage().hasText()) { + log.warn("Received update without text or message"); + return null; + } + String messageText = update.getMessage().getText().trim(); + Long chatId = update.getMessage().getChatId(); + log.info("Received update from chatId {}: {}", chatId, messageText); + + if (messageText.equalsIgnoreCase("/start")) { + redisStateService.deleteRegistrationData(chatId); + redisStateService.deleteThresholdData(chatId); + + RegistrationData regData = new RegistrationData(); + redisStateService.saveRegistrationData(chatId, regData); + sendTextMessage(chatId, "Welcome! Please enter your email to sign up."); + log.info("Processed /start command for chatId {}", chatId); + return null; + } else if (messageText.equalsIgnoreCase("/set_threshold")) { + redisStateService.deleteRegistrationData(chatId); + redisStateService.deleteThresholdData(chatId); + + // threshold 설정 진행 + ThresholdSettingData thresholdData = new ThresholdSettingData(); + redisStateService.saveThresholdData(chatId, thresholdData); + sendTextMessage(chatId, "Enter Coins (BTC, ETH, SOL):"); + return null; + } + + // 회원가입 처리 + RegistrationData regData = redisStateService.getRegistrationData(chatId); + if (regData != null) { + if (regData.getEmail() == null) { + regData.setEmail(messageText); + redisStateService.saveRegistrationData(chatId, regData); + sendTextMessage(chatId, "Email received. Now, please enter your username."); + } else if (regData.getUsername() == null) { + regData.setUsername(messageText); + // Kafka에 "user_registration_topic" 전송 + kafkaTemplate.send("user_registration_topic", + new UserRegistrationEventDto( + chatId, + regData.getEmail(), + regData.getUsername() + ) + ); + sendTextMessage(chatId, "Registration request sent! Please wait for confirmation."); + redisStateService.deleteRegistrationData(chatId); + } + return null; + } + + // 임계값 설정 처리 + ThresholdSettingData thresholdData = redisStateService.getThresholdData(chatId); + if (thresholdData != null) { + if (thresholdData.getCoin() == null) { + String coinInput = messageText.toUpperCase(); + if (!("BTC".equals(coinInput) || "ETH".equals(coinInput) || "SOL".equals(coinInput))) { + sendTextMessage(chatId, "Enter a valid coin (BTC, ETH, SOL):"); + return null; + } + thresholdData.setCoin(coinInput); + redisStateService.saveThresholdData(chatId, thresholdData); + sendTextMessage(chatId, "Please enter a threshold:"); + return null; + } else if (thresholdData.getThreshold() == null) { + try { + double thresholdVal = Double.parseDouble(messageText); + thresholdData.setThreshold(thresholdVal); + // Kafka에 "user_threshold_topic" 전송 + kafkaTemplate.send("user_threshold_topic", + new ThresholdEventDto( + chatId, + thresholdData.getCoin(), + thresholdVal + ) + ); + sendTextMessage(chatId, "Threshold request sent! Please wait for confirmation."); + } catch (NumberFormatException e) { + sendTextMessage(chatId, "Enter the threshold again:"); + return null; + } finally { + redisStateService.deleteThresholdData(chatId); + } + return null; + } + } + + // 기본 안내 메시지 전송 + sendTextMessage(chatId, "Please type /start to begin registration or /set_threshold to set alert threshold."); + return null; + } catch (Exception e) { + log.error("Error in onWebhookUpdateReceived: {}", e.getMessage(), e); + return null; // Telegram은 200 OK 응답을 기대하므로, 예외가 발생해도 null 반환 + } + } + + public void sendTextMessage(Long chatId, String text) { + SendMessage message = new SendMessage(); + message.setChatId(chatId.toString()); + message.setText(text); + try { + execute(message); + } catch (TelegramApiException e) { + log.error("Error sending message to chatId {}: {}", chatId, e.getMessage(), e); + e.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/notification-service/src/main/resources/application.yml b/notification-service/src/main/resources/application.yml new file mode 100644 index 0000000..9538d6a --- /dev/null +++ b/notification-service/src/main/resources/application.yml @@ -0,0 +1,27 @@ +server: + port: 8084 + +spring: + datasource: + url: ${SPRING_DATASOURCE_URL} + username: ${SPRING_DATASOURCE_USERNAME} + password: ${SPRING_DATASOURCE_PASSWORD} + driverClassName: com.mysql.cj.jdbc.Driver + + kafka: + bootstrap-servers: ${SPRING_KAFKA_BOOTSTRAP_SERVERS:localhost:9093} + + jpa: + hibernate: + ddl-auto: update + show-sql: false + +telegram: + bot: + username: ${TELEGRAM_USERNAME} + token: ${TELEGRAM_TOKEN} + webhookUrl: ${TELEGRAM_WEBHOOKURL} + +logging: + level: + com.whalewatch.notificationservice: DEBUG diff --git a/post-service/build.gradle b/post-service/build.gradle new file mode 100644 index 0000000..3069e1f --- /dev/null +++ b/post-service/build.gradle @@ -0,0 +1,20 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '3.3.4' + id 'io.spring.dependency-management' +} + +dependencies { + implementation project(':common') + + // Spring Boot, JPA + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1' +} + +test { + useJUnitPlatform() +} \ No newline at end of file diff --git a/service/src/main/java/com/whalewatch/ServiceApplication.java b/post-service/src/main/java/com/whalewatch/PostServiceApplication.java similarity index 67% rename from service/src/main/java/com/whalewatch/ServiceApplication.java rename to post-service/src/main/java/com/whalewatch/PostServiceApplication.java index cd0afb1..98dbaf5 100644 --- a/service/src/main/java/com/whalewatch/ServiceApplication.java +++ b/post-service/src/main/java/com/whalewatch/PostServiceApplication.java @@ -4,8 +4,8 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication -public class ServiceApplication { +public class PostServiceApplication { public static void main(String[] args) { - SpringApplication.run(ServiceApplication.class, args); + SpringApplication.run(PostServiceApplication.class, args); } -} +} \ No newline at end of file diff --git a/api/src/main/java/com/whalewatch/controller/PostController.java b/post-service/src/main/java/com/whalewatch/controller/PostController.java similarity index 100% rename from api/src/main/java/com/whalewatch/controller/PostController.java rename to post-service/src/main/java/com/whalewatch/controller/PostController.java diff --git a/service/src/main/java/com/whalewatch/domain/Post.java b/post-service/src/main/java/com/whalewatch/domain/Post.java similarity index 100% rename from service/src/main/java/com/whalewatch/domain/Post.java rename to post-service/src/main/java/com/whalewatch/domain/Post.java diff --git a/common/src/main/java/com/whalewatch/dto/PostDto.java b/post-service/src/main/java/com/whalewatch/dto/PostDto.java similarity index 100% rename from common/src/main/java/com/whalewatch/dto/PostDto.java rename to post-service/src/main/java/com/whalewatch/dto/PostDto.java diff --git a/service/src/main/java/com/whalewatch/mapper/PostMapper.java b/post-service/src/main/java/com/whalewatch/mapper/PostMapper.java similarity index 100% rename from service/src/main/java/com/whalewatch/mapper/PostMapper.java rename to post-service/src/main/java/com/whalewatch/mapper/PostMapper.java diff --git a/service/src/main/java/com/whalewatch/repository/PostRepository.java b/post-service/src/main/java/com/whalewatch/repository/PostRepository.java similarity index 100% rename from service/src/main/java/com/whalewatch/repository/PostRepository.java rename to post-service/src/main/java/com/whalewatch/repository/PostRepository.java diff --git a/service/src/main/java/com/whalewatch/service/PostService.java b/post-service/src/main/java/com/whalewatch/service/PostService.java similarity index 100% rename from service/src/main/java/com/whalewatch/service/PostService.java rename to post-service/src/main/java/com/whalewatch/service/PostService.java diff --git a/post-service/src/main/resources/application.yml b/post-service/src/main/resources/application.yml new file mode 100644 index 0000000..43aa0d9 --- /dev/null +++ b/post-service/src/main/resources/application.yml @@ -0,0 +1,18 @@ +server: + port: 8085 + +spring: + datasource: + url: ${SPRING_DATASOURCE_URL} + username: ${SPRING_DATASOURCE_USERNAME} + password: ${SPRING_DATASOURCE_PASSWORD} + driverClassName: com.mysql.cj.jdbc.Driver + + jpa: + hibernate: + ddl-auto: update + show-sql: false + +logging: + level: + com.whalewatch.postservice: DEBUG diff --git a/service/Dockerfile b/service/Dockerfile deleted file mode 100644 index c4d1f93..0000000 --- a/service/Dockerfile +++ /dev/null @@ -1,8 +0,0 @@ -# service/Dockerfile -FROM openjdk:17-slim - -COPY build/libs/whalewatch-service.jar /app/service.jar - -EXPOSE 8080 - -ENTRYPOINT ["java", "-jar", "/app/service.jar"] diff --git a/service/src/main/java/com/whalewatch/mapper/AlertSettingsMapper.java b/service/src/main/java/com/whalewatch/mapper/AlertSettingsMapper.java deleted file mode 100644 index 22eb66d..0000000 --- a/service/src/main/java/com/whalewatch/mapper/AlertSettingsMapper.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.whalewatch.mapper; - -import com.whalewatch.dto.AlertSettingsDto; -import com.whalewatch.domain.AlertSetting; -import org.mapstruct.Mapper; -import org.mapstruct.Mapping; - -@Mapper(componentModel = "spring") -public interface AlertSettingsMapper { - - // Entity -> DTO - @Mapping(target = "id", source = "id") - @Mapping(target = "coin", source = "coin") - @Mapping(target = "threshold", source = "threshold") - @Mapping(target = "notifyByEmail", source = "notifyByEmail") - AlertSettingsDto toDto(AlertSetting entity); - - // DTO -> Entity - @Mapping(target = "id", ignore = true) - AlertSetting toEntity(AlertSettingsDto dto); -} diff --git a/service/src/main/java/com/whalewatch/mapper/UserAlertMapper.java b/service/src/main/java/com/whalewatch/mapper/UserAlertMapper.java deleted file mode 100644 index 2bfd747..0000000 --- a/service/src/main/java/com/whalewatch/mapper/UserAlertMapper.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.whalewatch.mapper; - -import com.whalewatch.domain.UserAlert; -import com.whalewatch.dto.UserAlertDto; -import org.mapstruct.Mapper; -import org.mapstruct.Mapping; - -@Mapper(componentModel = "spring") -public interface UserAlertMapper { - // Entity -> DTO - @Mapping(target = "id", source = "id") - @Mapping(target = "userId", source = "userId") - @Mapping(target = "coin", source = "coin") - @Mapping(target = "tradePrice", source = "tradePrice") - @Mapping(target = "tradeVolume", source = "tradeVolume") - @Mapping(target = "tradeTimestamp", source = "tradeTimestamp") - @Mapping(target = "alertedAt", source = "alertedAt") - UserAlertDto toDto(UserAlert entity); - - // DTO -> Entity - @Mapping(target = "id", ignore = true) - @Mapping(target = "alertedAt", ignore = true) - UserAlert toEntity(UserAlertDto dto); -} diff --git a/service/src/main/java/com/whalewatch/telegram/TelegramWebhookUserBot.java b/service/src/main/java/com/whalewatch/telegram/TelegramWebhookUserBot.java deleted file mode 100644 index 6b1a577..0000000 --- a/service/src/main/java/com/whalewatch/telegram/TelegramWebhookUserBot.java +++ /dev/null @@ -1,164 +0,0 @@ -package com.whalewatch.telegram; - -import com.whalewatch.config.TelegramBotProperties; -import com.whalewatch.domain.AlertSetting; -import com.whalewatch.domain.User; -import com.whalewatch.service.*; -import org.springframework.stereotype.Component; -import org.telegram.telegrambots.bots.TelegramWebhookBot; -import org.telegram.telegrambots.meta.api.methods.BotApiMethod; -import org.telegram.telegrambots.meta.api.methods.send.SendMessage; -import org.telegram.telegrambots.meta.api.objects.Update; -import org.telegram.telegrambots.meta.exceptions.TelegramApiException; - - -@Component -public class TelegramWebhookUserBot extends TelegramWebhookBot { - - private final UserService userService; - private final AlertService alertService; - private final TelegramBotProperties telegramBotProperties; - private final RedisStateService redisStateService; - - public TelegramWebhookUserBot(UserService userService, - AlertService alertService, - TelegramBotProperties telegramBotProperties, - RedisStateService redisStateService) { - this.userService = userService; - this.alertService = alertService; - this.telegramBotProperties = telegramBotProperties; - this.redisStateService = redisStateService; - } - - @Override - public String getBotUsername() { - return telegramBotProperties.getUsername(); - } - - @Override - public String getBotToken() { - return telegramBotProperties.getToken(); - } - - @Override - public String getBotPath() { - return "/telegram/webhook"; - } - - @Override - public BotApiMethod onWebhookUpdateReceived(Update update) { - if (!update.hasMessage() || !update.getMessage().hasText()) { - return null; - } - String messageText = update.getMessage().getText().trim(); - Long chatId = update.getMessage().getChatId(); - - if (messageText.equalsIgnoreCase("/start")) { - // /start를 시작하면 모두 초기화 - redisStateService.deleteRegistrationData(chatId); - redisStateService.deleteThresholdData(chatId); - - try { - userService.findByTelegramChatId(chatId); - sendTextMessage(chatId, "You are already registered."); - } catch (RuntimeException e) { - RegistrationData regData = new RegistrationData(); - redisStateService.saveRegistrationData(chatId, regData); - sendTextMessage(chatId, "Welcome! Please enter your email to sign up."); - } - return null; - } else if (messageText.equalsIgnoreCase("/set_threshold")) { - // /set_threshold 실행시 모두 초기화 - redisStateService.deleteRegistrationData(chatId); - redisStateService.deleteThresholdData(chatId); - - try { - // 등록된 사용자만 임계값 설정 가능 - userService.findByTelegramChatId(chatId); - ThresholdSettingData thresholdData = new ThresholdSettingData(); - redisStateService.saveThresholdData(chatId, thresholdData); - sendTextMessage(chatId, "Enter Coins (BTC, ETH, SOL):"); - } catch (RuntimeException e) { - sendTextMessage(chatId, "Please register first using /start."); - } - return null; - } - - // 회원가입 먼저 - RegistrationData regData = redisStateService.getRegistrationData(chatId); - if (regData != null) { - if (regData.getEmail() == null) { - regData.setEmail(messageText); - redisStateService.saveRegistrationData(chatId, regData); - sendTextMessage(chatId, "Email received. Now, please enter your username."); - } else if (regData.getUsername() == null) { - regData.setUsername(messageText); - User newUser = new User(regData.getEmail(), regData.getUsername()); - newUser.setTelegramChatId(chatId); - userService.registerUser(newUser); - sendTextMessage(chatId, "Registration completed! You can request an OTP to log in."); - redisStateService.deleteRegistrationData(chatId); - } - return null; - } - - // 임계값 설정 - ThresholdSettingData thresholdData = redisStateService.getThresholdData(chatId); - if (thresholdData != null) { - if (thresholdData.getCoin() == null) { - String coinInput = messageText.toUpperCase(); - if (!("BTC".equals(coinInput) || "ETH".equals(coinInput) || "SOL".equals(coinInput))) { - sendTextMessage(chatId, "Enter a valid coin (BTC, ETH, SOL):"); - return null; - } - thresholdData.setCoin(coinInput); - redisStateService.saveThresholdData(chatId, thresholdData); - sendTextMessage(chatId, "Please enter a threshold:"); - return null; - } else if (thresholdData.getThreshold() == null) { - try { - double threshold = Double.parseDouble(messageText); - thresholdData.setThreshold(threshold); - User user = userService.findByTelegramChatId(chatId); - if (user == null) { - sendTextMessage(chatId, "You must register first using /start before setting a threshold."); - redisStateService.deleteThresholdData(chatId); - return null; - } - AlertSetting alertSetting = new AlertSetting(thresholdData.getCoin(), threshold, false); - alertSetting.setUserId(user.getId()); - alertService.createAlert(alertSetting); - sendTextMessage(chatId, "Threshold set: Coin: " + thresholdData.getCoin() + ", Threshold: " + threshold); - } catch (NumberFormatException e) { - sendTextMessage(chatId, "Enter the threshold again:"); - return null; - } finally { - redisStateService.deleteThresholdData(chatId); - } - return null; - } - } - - // 기본 안내 메시지 전송 - sendTextMessage(chatId, "Please type /start to begin registration or /set_threshold to set alert threshold."); - return null; - } - - - - public void sendTextMessage(Long chatId, String text) { - SendMessage message = new SendMessage(); - message.setChatId(chatId.toString()); - message.setText(text); - try { - execute(message); - } catch (TelegramApiException e) { - e.printStackTrace(); - } - } - - @org.springframework.context.event.EventListener - public void handleTelegramMessageEvent(TelegramMessageEvent event) { - sendTextMessage(event.getChatId(), event.getMessage()); - } -} \ No newline at end of file diff --git a/service/src/test/java/com/whalewatch/service/AlertServiceTest.java b/service/src/test/java/com/whalewatch/service/AlertServiceTest.java deleted file mode 100644 index b8463cb..0000000 --- a/service/src/test/java/com/whalewatch/service/AlertServiceTest.java +++ /dev/null @@ -1,94 +0,0 @@ -package com.whalewatch.service; - -import com.whalewatch.domain.AlertSetting; -import com.whalewatch.repository.AlertRepository; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.*; -import org.springframework.test.context.junit.jupiter.SpringExtension; - -import java.util.Arrays; -import java.util.List; -import java.util.Optional; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.BDDMockito.*; - -@ExtendWith(SpringExtension.class) -class AlertServiceTest { - - @Mock - private AlertRepository alertRepository; - - @InjectMocks - private AlertService alertService; - - @Test - void getAllAlerts() { - // given - AlertSetting a1 = new AlertSetting("BTC", 30000, true); - AlertSetting a2 = new AlertSetting("ETH", 2000, false); - - given(alertRepository.findAll()).willReturn(Arrays.asList(a1, a2)); - - // when - List alerts = alertService.getAllAlerts(); - - // then - assertEquals(2, alerts.size()); // 리스트 크기 검증 - - // 첫 번째 객체 검증 - assertEquals("BTC", alerts.get(0).getCoin()); - assertEquals(30000, alerts.get(0).getThreshold()); - assertTrue(alerts.get(0).isNotifyByEmail()); - - // 두 번째 객체 검증 - assertEquals("ETH", alerts.get(1).getCoin()); - assertEquals(2000, alerts.get(1).getThreshold()); - assertFalse(alerts.get(1).isNotifyByEmail()); - } - - @Test - void createAlert() { - // given - AlertSetting input = new AlertSetting("BTC", 30000, true); - AlertSetting saved = new AlertSetting("BTC", 30000, true); - - given(alertRepository.save(input)).willReturn(saved); - - // when - AlertSetting result = alertService.createAlert(input); - - // then - assertNotNull(result); - assertEquals("BTC", result.getCoin()); - assertEquals(30000, result.getThreshold()); - assertTrue(result.isNotifyByEmail()); - } - - @Test - void updateAlert() { - // given - int alertId = 1; - AlertSetting existing = new AlertSetting("BTC", 10000, false); - given(alertRepository.findById(alertId)).willReturn(Optional.of(existing)); - - AlertSetting updated = new AlertSetting("ETH", 2000, true); - given(alertRepository.save(existing)).willAnswer(invocation -> { - AlertSetting toUpdate = invocation.getArgument(0); - toUpdate.setCoin(updated.getCoin()); - toUpdate.setThreshold(updated.getThreshold()); - toUpdate.setNotifyByEmail(updated.isNotifyByEmail()); - return toUpdate; - }); - - // when - AlertSetting result = alertService.updateAlert(alertId, updated); - - // then - assertEquals("ETH", result.getCoin()); // 업데이트된 코인 이름 검증 - assertEquals(2000, result.getThreshold()); // 업데이트된 임계값 검증 - assertTrue(result.isNotifyByEmail()); // 업데이트된 이메일 알림 여부 검증 - } - -} diff --git a/service/src/test/java/com/whalewatch/service/JwtServiceTest.java b/service/src/test/java/com/whalewatch/service/JwtServiceTest.java deleted file mode 100644 index ad31f6e..0000000 --- a/service/src/test/java/com/whalewatch/service/JwtServiceTest.java +++ /dev/null @@ -1,91 +0,0 @@ -package com.whalewatch.service; - -import com.whalewatch.config.JwtTokenProvider; -import com.whalewatch.domain.JwtToken; -import com.whalewatch.domain.User; -import com.whalewatch.dto.TokenResponseDto; -import com.whalewatch.repository.JwtTokenRepository; -import com.whalewatch.repository.UserRepository; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.test.context.junit.jupiter.SpringExtension; - -import java.time.LocalDateTime; -import java.util.Optional; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.verify; - -@ExtendWith(SpringExtension.class) -public class JwtServiceTest { - - @Mock - private UserRepository userRepository; - @Mock - private JwtTokenRepository refreshTokenRepository; - @Mock - private JwtTokenProvider jwtTokenProvider; - @Mock - private PasswordEncoder passwordEncoder; - - @InjectMocks - private JwtService jwtService; - - @Test - void loginSuccess() { - // given - String email = "test@test.com"; - String Password = "test"; - String encodedPassword = "$2a$10$ABCD123..."; // bcrypt 해싱된 값 가정 - User user = new User(email, "logtester", encodedPassword); - - given(userRepository.findByEmail(email)).willReturn(Optional.of(user)); - // 비밀번호 매칭 - given(passwordEncoder.matches(Password, encodedPassword)).willReturn(true); - - // JWT 생성 - given(jwtTokenProvider.generateAccessToken(email)).willReturn("access-token"); - given(jwtTokenProvider.generateRefreshToken(email)).willReturn("refresh-token"); - - // when - TokenResponseDto result = jwtService.login(email, Password); - - // then - assertNotNull(result); - assertEquals("access-token", result.getAccessToken()); - assertEquals("refresh-token", result.getRefreshToken()); - verify(refreshTokenRepository).deleteByEmail(email); - verify(refreshTokenRepository).save(any(JwtToken.class)); - } - - @Test - void refreshAccessTokenSuccess() { - // given - String refreshToken = "valid-refresh-token"; - JwtToken stored = new JwtToken(refreshToken, "refresh@test.com", LocalDateTime.now().plusDays(1)); - given(refreshTokenRepository.findByToken(refreshToken)).willReturn(Optional.of(stored)); - - // 토큰 서명/만료 검증 - given(jwtTokenProvider.validateToken(refreshToken)).willReturn(true); - - // 토큰에서 이메일 추출 - given(jwtTokenProvider.getEmailFromToken(refreshToken)).willReturn("refresh@test.com"); - - // 새 Access Token 발급 - given(jwtTokenProvider.generateAccessToken("refresh@test.com")).willReturn("new-access-token"); - - // when - TokenResponseDto result = jwtService.refreshAccessToken(refreshToken); - - // then - assertNotNull(result); - assertEquals("new-access-token", result.getAccessToken()); - assertEquals("valid-refresh-token", result.getRefreshToken()); - } -} diff --git a/service/src/test/java/com/whalewatch/service/PostServiceTest.java b/service/src/test/java/com/whalewatch/service/PostServiceTest.java deleted file mode 100644 index c48b0f1..0000000 --- a/service/src/test/java/com/whalewatch/service/PostServiceTest.java +++ /dev/null @@ -1,88 +0,0 @@ -package com.whalewatch.service; - -import com.whalewatch.domain.Post; -import com.whalewatch.repository.PostRepository; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.*; -import org.springframework.test.context.junit.jupiter.SpringExtension; - -import java.util.Arrays; -import java.util.List; -import java.util.Optional; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.BDDMockito.*; - -@ExtendWith(SpringExtension.class) -class PostServiceTest { - - @Mock - private PostRepository postRepository; - - @InjectMocks - private PostService postService; - - @Test - void getAllPosts() { - // given - Post p1 = new Post("Title1", "Content1"); - Post p2 = new Post("Title2", "Content2"); - given(postRepository.findAll()).willReturn(Arrays.asList(p1, p2)); - - // when - List result = postService.getAllPosts(); - - // then - assertEquals(2, result.size()); // 리스트 크기 검증 - - // 첫 번째 객체 검증 - assertEquals("Title1", result.get(0).getTitle()); - assertEquals("Content1", result.get(0).getContent()); - - // 두 번째 객체 검증 - assertEquals("Title2", result.get(1).getTitle()); - assertEquals("Content2", result.get(1).getContent()); - } - - @Test - void createPost() { - // given - Post input = new Post("New Title", "New Content"); - Post saved = new Post("New Title", "New Content"); - - given(postRepository.save(input)).willReturn(saved); - - // when - Post result = postService.createPost(input); - - // then - assertNotNull(result); - assertEquals("New Title", result.getTitle()); // 제목 검증 - assertEquals("New Content", result.getContent()); // 내용 검증 - } - - @Test - void updatePost() { - // given - int postId = 1; - Post existing = new Post("Old Title", "Old Content"); - given(postRepository.findById(postId)).willReturn(Optional.of(existing)); - - Post updated = new Post("Updated Title", "Updated Content"); - given(postRepository.save(existing)).willAnswer(invocation -> { - Post toUpdate = invocation.getArgument(0); - toUpdate.setTitle(updated.getTitle()); - toUpdate.setContent(updated.getContent()); - return toUpdate; - }); - - // when - Post result = postService.updatePost(postId, updated); - - // then - assertEquals("Updated Title", result.getTitle()); // 업데이트된 제목 검증 - assertEquals("Updated Content", result.getContent()); // 업데이트된 내용 검증 - } - -} diff --git a/service/src/test/java/com/whalewatch/service/TransactionServiceTest.java b/service/src/test/java/com/whalewatch/service/TransactionServiceTest.java deleted file mode 100644 index 744a6e5..0000000 --- a/service/src/test/java/com/whalewatch/service/TransactionServiceTest.java +++ /dev/null @@ -1,84 +0,0 @@ -package com.whalewatch.service; - -import com.whalewatch.domain.Transaction; -import com.whalewatch.repository.TransactionRepository; -import com.whalewatch.transaction.TransactionService; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.*; -import org.springframework.test.context.junit.jupiter.SpringExtension; - -import java.util.Arrays; -import java.util.List; -import java.util.Optional; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.BDDMockito.*; - -@ExtendWith(SpringExtension.class) -class TransactionServiceTest { - - @Mock - private TransactionRepository transactionRepository; - - @InjectMocks - private TransactionService transactionService; - - @Test - void getAllTransactions() { - // given - Transaction t1 = new Transaction("0xabc123", "BTC", 10000); - Transaction t2 = new Transaction("0xdef456", "ETH", 15000); - given(transactionRepository.findAll()).willReturn(Arrays.asList(t1, t2)); - - // when - List result = transactionService.getAllTransactions(); - - // then - assertEquals(2, result.size()); // 리스트 크기 검증 - - // 첫 번째 트랜잭션 검증 - assertEquals("BTC", result.get(0).getCoin()); - assertEquals("0xabc123", result.get(0).getHash()); - assertEquals(10000, result.get(0).getAmount()); - - // 두 번째 트랜잭션 검증 - assertEquals("ETH", result.get(1).getCoin()); - assertEquals("0xdef456", result.get(1).getHash()); - assertEquals(15000, result.get(1).getAmount()); - } - - @Test - void getTransactionById() { - // given - int txId = 1; - Transaction t1 = new Transaction("0xabc123", "BTC", 10000); - - given(transactionRepository.findById(txId)).willReturn(Optional.of(t1)); - - // when - Transaction result = transactionService.getTransactionById(txId); - - // then - assertNotNull(result); // 반환값이 null이 아님을 확인 - assertEquals("BTC", result.getCoin()); // 코인 이름 검증 - assertEquals("0xabc123", result.getHash()); // 트랜잭션 해시 검증 - assertEquals(10000, result.getAmount()); // 금액 검증 - } - - @Test - void createTransaction() { - // given - Transaction input = new Transaction("0x123", "DOGE", 5000); - given(transactionRepository.save(input)).willReturn(input); - - // when - Transaction result = transactionService.createTransaction(input); - - // then - assertNotNull(result); // 반환값이 null이 아님을 확인 - assertEquals("DOGE", result.getCoin()); // 코인 이름 검증 - assertEquals("0x123", result.getHash()); // 트랜잭션 해시 검증 - assertEquals(5000, result.getAmount()); // 금액 검증 - } -} diff --git a/service/src/test/java/com/whalewatch/service/UserServiceTest.java b/service/src/test/java/com/whalewatch/service/UserServiceTest.java deleted file mode 100644 index 192b103..0000000 --- a/service/src/test/java/com/whalewatch/service/UserServiceTest.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.whalewatch.service; - -import com.whalewatch.domain.User; -import com.whalewatch.repository.UserRepository; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.*; -import org.springframework.test.context.junit.jupiter.SpringExtension; - -import java.util.Optional; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.BDDMockito.*; - -@ExtendWith(SpringExtension.class) -class UserServiceTest { - - @Mock - private UserRepository userRepository; - - @InjectMocks - private UserService userService; - - @Test - void registerUser() { - // given - User input = new User("test@test.com", "tester", "1234"); - User saved = new User("test@test.com", "tester", "1234"); - - given(userRepository.save(input)).willReturn(saved); - - // when - User result = userService.registerUser(input); - - // then - assertNotNull(result); // 반환값이 null이 아님을 확인 - assertEquals("test@test.com", result.getEmail()); // 이메일 검증 - assertEquals("tester", result.getUsername()); // 이름 검증 - assertEquals("1234", result.getPassword()); // 비밀번호 검증 - } - - @Test - void getUserInfo() { - // given - int userId = 1; - User user = new User("info@test.com", "infotester", "1234"); - - given(userRepository.findById(userId)).willReturn(Optional.of(user)); - - // when - User result = userService.getUserInfo(userId); - - // then - assertNotNull(result); // 반환값이 null이 아님을 확인 - assertEquals("info@test.com", result.getEmail()); // 이메일 검증 - assertEquals("infotester", result.getUsername()); // 이름 검증 - assertEquals("1234", result.getPassword()); // 비밀번호 검증 - } - - -} diff --git a/settings.gradle b/settings.gradle index aa4fed1..1e86c64 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,3 +1,2 @@ rootProject.name = 'WhaleWatch' -include 'common', 'api', 'service', 'collector' - +include 'common', 'collector', 'transaction-service', 'notification-service', 'user-service', 'post-service' diff --git a/transaction-service/build.gradle b/transaction-service/build.gradle new file mode 100644 index 0000000..4f36ab1 --- /dev/null +++ b/transaction-service/build.gradle @@ -0,0 +1,27 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '3.3.4' + id 'io.spring.dependency-management' +} + +dependencies { + implementation project(':common') + + // Spring Boot, JPA + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + + // MapStruct + implementation 'org.mapstruct:mapstruct:1.5.5.Final' + annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final' + + //Kafka + implementation 'org.springframework.kafka:spring-kafka' + + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1' +} + +test { + useJUnitPlatform() +} \ No newline at end of file diff --git a/transaction-service/src/main/java/com/whalewatch/TransactionServiceApplication.java b/transaction-service/src/main/java/com/whalewatch/TransactionServiceApplication.java new file mode 100644 index 0000000..ee16559 --- /dev/null +++ b/transaction-service/src/main/java/com/whalewatch/TransactionServiceApplication.java @@ -0,0 +1,11 @@ +package com.whalewatch; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class TransactionServiceApplication { + public static void main(String[] args) { + SpringApplication.run(TransactionServiceApplication.class, args); + } +} diff --git a/api/src/main/java/com/whalewatch/controller/TransactionController.java b/transaction-service/src/main/java/com/whalewatch/controller/TransactionController.java similarity index 96% rename from api/src/main/java/com/whalewatch/controller/TransactionController.java rename to transaction-service/src/main/java/com/whalewatch/controller/TransactionController.java index 4350fa9..3d3db26 100644 --- a/api/src/main/java/com/whalewatch/controller/TransactionController.java +++ b/transaction-service/src/main/java/com/whalewatch/controller/TransactionController.java @@ -3,7 +3,7 @@ import com.whalewatch.domain.Transaction; import com.whalewatch.dto.TransactionDto; import com.whalewatch.mapper.TransactionMapper; -import com.whalewatch.transaction.TransactionService; +import com.whalewatch.service.TransactionService; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; diff --git a/service/src/main/java/com/whalewatch/domain/Transaction.java b/transaction-service/src/main/java/com/whalewatch/domain/Transaction.java similarity index 100% rename from service/src/main/java/com/whalewatch/domain/Transaction.java rename to transaction-service/src/main/java/com/whalewatch/domain/Transaction.java diff --git a/common/src/main/java/com/whalewatch/dto/TransactionDto.java b/transaction-service/src/main/java/com/whalewatch/dto/TransactionDto.java similarity index 100% rename from common/src/main/java/com/whalewatch/dto/TransactionDto.java rename to transaction-service/src/main/java/com/whalewatch/dto/TransactionDto.java diff --git a/service/src/main/java/com/whalewatch/transaction/TransactionKafkaConsumer.java b/transaction-service/src/main/java/com/whalewatch/kafka/TransactionKafkaConsumer.java similarity index 96% rename from service/src/main/java/com/whalewatch/transaction/TransactionKafkaConsumer.java rename to transaction-service/src/main/java/com/whalewatch/kafka/TransactionKafkaConsumer.java index c80602a..4093f2d 100644 --- a/service/src/main/java/com/whalewatch/transaction/TransactionKafkaConsumer.java +++ b/transaction-service/src/main/java/com/whalewatch/kafka/TransactionKafkaConsumer.java @@ -1,4 +1,4 @@ -package com.whalewatch.transaction; +package com.whalewatch.kafka; import com.whalewatch.domain.Transaction; import com.whalewatch.dto.TransactionEventDto; @@ -35,7 +35,6 @@ public void persistTransaction(ConsumerRecord recor private Transaction convertToTransaction(TransactionEventDto event) { Transaction tx = new Transaction(); - tx.setId(event.getId()); tx.setCoin(event.getCoin()); tx.setTradePrice(event.getTradePrice()); tx.setTradeVolume(event.getTradeVolume()); diff --git a/service/src/main/java/com/whalewatch/mapper/TransactionMapper.java b/transaction-service/src/main/java/com/whalewatch/mapper/TransactionMapper.java similarity index 100% rename from service/src/main/java/com/whalewatch/mapper/TransactionMapper.java rename to transaction-service/src/main/java/com/whalewatch/mapper/TransactionMapper.java diff --git a/service/src/main/java/com/whalewatch/repository/TransactionRepository.java b/transaction-service/src/main/java/com/whalewatch/repository/TransactionRepository.java similarity index 100% rename from service/src/main/java/com/whalewatch/repository/TransactionRepository.java rename to transaction-service/src/main/java/com/whalewatch/repository/TransactionRepository.java diff --git a/service/src/main/java/com/whalewatch/transaction/TransactionService.java b/transaction-service/src/main/java/com/whalewatch/service/TransactionService.java similarity index 78% rename from service/src/main/java/com/whalewatch/transaction/TransactionService.java rename to transaction-service/src/main/java/com/whalewatch/service/TransactionService.java index 8e2868c..3d0dcc5 100644 --- a/service/src/main/java/com/whalewatch/transaction/TransactionService.java +++ b/transaction-service/src/main/java/com/whalewatch/service/TransactionService.java @@ -1,10 +1,8 @@ -package com.whalewatch.transaction; +package com.whalewatch.service; import com.whalewatch.domain.Transaction; import com.whalewatch.dto.TransactionEventDto; -import com.whalewatch.repository.AlertRepository; import com.whalewatch.repository.TransactionRepository; -import com.whalewatch.service.UserAlertService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.kafka.core.KafkaTemplate; @@ -17,19 +15,13 @@ public class TransactionService { private static final Logger log = LoggerFactory.getLogger(TransactionService.class); private final TransactionRepository transactionRepository; - private final AlertRepository alertRepository; - private final UserAlertService userAlertService; private final KafkaTemplate kafkaTemplate; private final String TRANSACTION_EVENT = "transaction_event"; public TransactionService(TransactionRepository transactionRepository, - AlertRepository alertRepository, - UserAlertService userAlertService, KafkaTemplate kafkaTemplate) { this.transactionRepository = transactionRepository; - this.alertRepository = alertRepository; - this.userAlertService = userAlertService; this.kafkaTemplate = kafkaTemplate; } diff --git a/transaction-service/src/main/resources/application.yml b/transaction-service/src/main/resources/application.yml new file mode 100644 index 0000000..b88612c --- /dev/null +++ b/transaction-service/src/main/resources/application.yml @@ -0,0 +1,21 @@ +server: + port: 8083 + +spring: + datasource: + url: ${SPRING_DATASOURCE_URL} + username: ${SPRING_DATASOURCE_USERNAME} + password: ${SPRING_DATASOURCE_PASSWORD} + driverClassName: com.mysql.cj.jdbc.Driver + + kafka: + bootstrap-servers: ${SPRING_KAFKA_BOOTSTRAP_SERVERS:localhost:9093} + + jpa: + hibernate: + ddl-auto: update + show-sql: false + +logging: + level: + com.whalewatch.transactionservice: DEBUG diff --git a/user-service/build.gradle b/user-service/build.gradle new file mode 100644 index 0000000..878be58 --- /dev/null +++ b/user-service/build.gradle @@ -0,0 +1,37 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '3.3.4' + id 'io.spring.dependency-management' +} + + +dependencies { + implementation project(':common') + + // Spring Boot, JPA + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + + // MapStruct + implementation 'org.mapstruct:mapstruct:1.5.5.Final' + annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final' + + //Security + implementation 'org.springframework.boot:spring-boot-starter-security' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + + // JWT 설정 + implementation 'io.jsonwebtoken:jjwt-api:0.11.5' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5' + + //Kafka + implementation 'org.springframework.kafka:spring-kafka' + + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1' +} + +test { + useJUnitPlatform() +} \ No newline at end of file diff --git a/user-service/src/main/java/com/whalewatch/UserServiceApplication.java b/user-service/src/main/java/com/whalewatch/UserServiceApplication.java new file mode 100644 index 0000000..62bb1da --- /dev/null +++ b/user-service/src/main/java/com/whalewatch/UserServiceApplication.java @@ -0,0 +1,11 @@ +package com.whalewatch; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class UserServiceApplication { + public static void main(String[] args) { + SpringApplication.run(UserServiceApplication.class, args); + } +} \ No newline at end of file diff --git a/common/src/main/java/com/whalewatch/config/JwtAuthentication.java b/user-service/src/main/java/com/whalewatch/config/JwtAuthentication.java similarity index 100% rename from common/src/main/java/com/whalewatch/config/JwtAuthentication.java rename to user-service/src/main/java/com/whalewatch/config/JwtAuthentication.java diff --git a/common/src/main/java/com/whalewatch/config/JwtProperties.java b/user-service/src/main/java/com/whalewatch/config/JwtProperties.java similarity index 100% rename from common/src/main/java/com/whalewatch/config/JwtProperties.java rename to user-service/src/main/java/com/whalewatch/config/JwtProperties.java diff --git a/common/src/main/java/com/whalewatch/config/JwtTokenProvider.java b/user-service/src/main/java/com/whalewatch/config/JwtTokenProvider.java similarity index 100% rename from common/src/main/java/com/whalewatch/config/JwtTokenProvider.java rename to user-service/src/main/java/com/whalewatch/config/JwtTokenProvider.java diff --git a/common/src/main/java/com/whalewatch/config/SecurityConfig.java b/user-service/src/main/java/com/whalewatch/config/SecurityConfig.java similarity index 100% rename from common/src/main/java/com/whalewatch/config/SecurityConfig.java rename to user-service/src/main/java/com/whalewatch/config/SecurityConfig.java diff --git a/api/src/main/java/com/whalewatch/controller/UserController.java b/user-service/src/main/java/com/whalewatch/controller/UserController.java similarity index 100% rename from api/src/main/java/com/whalewatch/controller/UserController.java rename to user-service/src/main/java/com/whalewatch/controller/UserController.java diff --git a/service/src/main/java/com/whalewatch/domain/JwtToken.java b/user-service/src/main/java/com/whalewatch/domain/JwtToken.java similarity index 100% rename from service/src/main/java/com/whalewatch/domain/JwtToken.java rename to user-service/src/main/java/com/whalewatch/domain/JwtToken.java diff --git a/service/src/main/java/com/whalewatch/domain/User.java b/user-service/src/main/java/com/whalewatch/domain/User.java similarity index 100% rename from service/src/main/java/com/whalewatch/domain/User.java rename to user-service/src/main/java/com/whalewatch/domain/User.java diff --git a/common/src/main/java/com/whalewatch/dto/TokenResponseDto.java b/user-service/src/main/java/com/whalewatch/dto/TokenResponseDto.java similarity index 100% rename from common/src/main/java/com/whalewatch/dto/TokenResponseDto.java rename to user-service/src/main/java/com/whalewatch/dto/TokenResponseDto.java diff --git a/common/src/main/java/com/whalewatch/dto/UserDto.java b/user-service/src/main/java/com/whalewatch/dto/UserDto.java similarity index 100% rename from common/src/main/java/com/whalewatch/dto/UserDto.java rename to user-service/src/main/java/com/whalewatch/dto/UserDto.java diff --git a/user-service/src/main/java/com/whalewatch/kafka/UserRegistrationConsumer.java b/user-service/src/main/java/com/whalewatch/kafka/UserRegistrationConsumer.java new file mode 100644 index 0000000..2527051 --- /dev/null +++ b/user-service/src/main/java/com/whalewatch/kafka/UserRegistrationConsumer.java @@ -0,0 +1,66 @@ +package com.whalewatch.kafka; + +import com.whalewatch.domain.User; +import com.whalewatch.dto.UserRegistrationEventDto; +import com.whalewatch.dto.UserRegistrationResultEventDto; +import com.whalewatch.repository.UserRepository; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.kafka.support.Acknowledgment; +import org.springframework.stereotype.Component; + +@Component +public class UserRegistrationConsumer { + + private static final Logger log = LoggerFactory.getLogger(UserRegistrationConsumer.class); + + private final UserRepository userRepository; + private final KafkaTemplate kafkaTemplate; + + public UserRegistrationConsumer(UserRepository userRepository, + KafkaTemplate kafkaTemplate) { + this.userRepository = userRepository; + this.kafkaTemplate = kafkaTemplate; + } + + @KafkaListener( + topics = "user_registration_topic", + groupId = "whalewatch_registration" // 원하는 groupId + ) + public void onUserRegistration(ConsumerRecord record, + Acknowledgment ack) { + UserRegistrationEventDto event = record.value(); + log.info("[UserRegistrationConsumer] Received: {}", event); + + try { + User user = new User(event.getEmail(), event.getUsername()); + user.setTelegramChatId(event.getChatId()); + + // DB에 저장 + userRepository.save(user); + log.info("User saved. email={}, username={}, chatId={}", + user.getEmail(), user.getUsername(), user.getTelegramChatId()); + + // 성공 결과 이벤트 발행 + UserRegistrationResultEventDto successEvent = new UserRegistrationResultEventDto( + event.getChatId(), + true, + "Registration succeeded! Username: " + event.getUsername() + ); + kafkaTemplate.send("user_registration_result_topic", successEvent); + } catch (Exception e) { + log.error("User registration failed: {}", e.getMessage(), e); + // 실패 결과 이벤트 발행 + UserRegistrationResultEventDto failEvent = new UserRegistrationResultEventDto( + event.getChatId(), + false, + "Registration failed: " + e.getMessage() + ); + kafkaTemplate.send("user_registration_result_topic", failEvent); + } + ack.acknowledge(); + } +} \ No newline at end of file diff --git a/service/src/main/java/com/whalewatch/mapper/UserMapper.java b/user-service/src/main/java/com/whalewatch/mapper/UserMapper.java similarity index 100% rename from service/src/main/java/com/whalewatch/mapper/UserMapper.java rename to user-service/src/main/java/com/whalewatch/mapper/UserMapper.java diff --git a/service/src/main/java/com/whalewatch/repository/JwtTokenRepository.java b/user-service/src/main/java/com/whalewatch/repository/JwtTokenRepository.java similarity index 100% rename from service/src/main/java/com/whalewatch/repository/JwtTokenRepository.java rename to user-service/src/main/java/com/whalewatch/repository/JwtTokenRepository.java diff --git a/service/src/main/java/com/whalewatch/repository/UserRepository.java b/user-service/src/main/java/com/whalewatch/repository/UserRepository.java similarity index 100% rename from service/src/main/java/com/whalewatch/repository/UserRepository.java rename to user-service/src/main/java/com/whalewatch/repository/UserRepository.java diff --git a/service/src/main/java/com/whalewatch/service/JwtService.java b/user-service/src/main/java/com/whalewatch/service/JwtService.java similarity index 100% rename from service/src/main/java/com/whalewatch/service/JwtService.java rename to user-service/src/main/java/com/whalewatch/service/JwtService.java diff --git a/service/src/main/java/com/whalewatch/service/UserService.java b/user-service/src/main/java/com/whalewatch/service/UserService.java similarity index 84% rename from service/src/main/java/com/whalewatch/service/UserService.java rename to user-service/src/main/java/com/whalewatch/service/UserService.java index 58c239f..23819cc 100644 --- a/service/src/main/java/com/whalewatch/service/UserService.java +++ b/user-service/src/main/java/com/whalewatch/service/UserService.java @@ -1,9 +1,10 @@ package com.whalewatch.service; import com.whalewatch.domain.User; +import com.whalewatch.dto.UserOtpEventDto; import com.whalewatch.repository.UserRepository; -import com.whalewatch.telegram.TelegramMessageEvent; import org.springframework.context.ApplicationEventPublisher; +import org.springframework.kafka.core.KafkaTemplate; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; @@ -11,14 +12,14 @@ public class UserService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; - private final ApplicationEventPublisher eventPublisher; + private final KafkaTemplate kafkaTemplate; public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder, - ApplicationEventPublisher eventPublisher) { + KafkaTemplate kafkaTemplate) { this.userRepository = userRepository; this.passwordEncoder = passwordEncoder; - this.eventPublisher = eventPublisher; + this.kafkaTemplate = kafkaTemplate; } public User registerUser(User user) { @@ -46,7 +47,8 @@ public void requestLoginOtp(String email) { userRepository.save(user); if (user.getTelegramChatId() != null) { - eventPublisher.publishEvent(new TelegramMessageEvent(user.getTelegramChatId(), "Your login OTP: " + otp)); + UserOtpEventDto event = new UserOtpEventDto(user.getTelegramChatId(), "Your login OTP: " + otp); + kafkaTemplate.send("user_otp_topic", event); } else { throw new RuntimeException("User is not registered with Telegram"); } diff --git a/user-service/src/main/resources/application.yml b/user-service/src/main/resources/application.yml new file mode 100644 index 0000000..4f9b832 --- /dev/null +++ b/user-service/src/main/resources/application.yml @@ -0,0 +1,23 @@ +server: + port: 8082 + +spring: + datasource: + url: ${SPRING_DATASOURCE_URL} + username: ${SPRING_DATASOURCE_USERNAME} + password: ${SPRING_DATASOURCE_PASSWORD} + driverClassName: com.mysql.cj.jdbc.Driver + + jpa: + hibernate: + ddl-auto: update + show-sql: false + +jwt: + secret-key: "${JWT_SECRET_KEY}" + access-token-validity-in-seconds: 600 + refresh-token-validity-in-seconds: 1209600 + +logging: + level: + com.whalewatch.userservice: DEBUG