diff --git a/src/main/java/org/mockInvestment/balance/controller/BalanceController.java b/src/main/java/org/mockInvestment/balance/controller/BalanceController.java new file mode 100644 index 0000000..ff07710 --- /dev/null +++ b/src/main/java/org/mockInvestment/balance/controller/BalanceController.java @@ -0,0 +1,27 @@ +package org.mockInvestment.balance.controller; + +import org.mockInvestment.auth.dto.AuthInfo; +import org.mockInvestment.balance.dto.CurrentBalanceResponse; +import org.mockInvestment.balance.service.BalanceService; +import org.mockInvestment.support.auth.Login; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class BalanceController { + + private final BalanceService balanceService; + + + public BalanceController(BalanceService balanceService) { + this.balanceService = balanceService; + } + + @GetMapping("/balance/me") + public ResponseEntity findBalance(@Login AuthInfo authInfo) { + CurrentBalanceResponse response = balanceService.findBalance(authInfo); + return ResponseEntity.ok(response); + } + +} diff --git a/src/main/java/org/mockInvestment/balance/domain/Balance.java b/src/main/java/org/mockInvestment/balance/domain/Balance.java index 10f4ff4..3036d86 100644 --- a/src/main/java/org/mockInvestment/balance/domain/Balance.java +++ b/src/main/java/org/mockInvestment/balance/domain/Balance.java @@ -36,4 +36,9 @@ public void purchase(Double price) { public void cancelPayment(Double price) { balance += price; } + + public double getBalance() { + return balance; + } + } diff --git a/src/main/java/org/mockInvestment/balance/dto/CurrentBalanceResponse.java b/src/main/java/org/mockInvestment/balance/dto/CurrentBalanceResponse.java new file mode 100644 index 0000000..520aaac --- /dev/null +++ b/src/main/java/org/mockInvestment/balance/dto/CurrentBalanceResponse.java @@ -0,0 +1,4 @@ +package org.mockInvestment.balance.dto; + +public record CurrentBalanceResponse(double balance) { +} diff --git a/src/main/java/org/mockInvestment/balance/repository/BalanceRepository.java b/src/main/java/org/mockInvestment/balance/repository/BalanceRepository.java index 12364d6..34e45d6 100644 --- a/src/main/java/org/mockInvestment/balance/repository/BalanceRepository.java +++ b/src/main/java/org/mockInvestment/balance/repository/BalanceRepository.java @@ -1,7 +1,13 @@ package org.mockInvestment.balance.repository; import org.mockInvestment.balance.domain.Balance; +import org.mockInvestment.member.domain.Member; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + public interface BalanceRepository extends JpaRepository { + + Optional findByMember(Member member); + } diff --git a/src/main/java/org/mockInvestment/balance/service/BalanceService.java b/src/main/java/org/mockInvestment/balance/service/BalanceService.java new file mode 100644 index 0000000..59a38ab --- /dev/null +++ b/src/main/java/org/mockInvestment/balance/service/BalanceService.java @@ -0,0 +1,34 @@ +package org.mockInvestment.balance.service; + +import org.mockInvestment.advice.exception.MemberNotFoundException; +import org.mockInvestment.auth.dto.AuthInfo; +import org.mockInvestment.balance.domain.Balance; +import org.mockInvestment.balance.dto.CurrentBalanceResponse; +import org.mockInvestment.balance.repository.BalanceRepository; +import org.mockInvestment.member.domain.Member; +import org.mockInvestment.member.repository.MemberRepository; +import org.springframework.stereotype.Service; + +@Service +public class BalanceService { + + private final BalanceRepository balanceRepository; + + private final MemberRepository memberRepository; + + + public BalanceService(BalanceRepository balanceRepository, MemberRepository memberRepository) { + this.balanceRepository = balanceRepository; + this.memberRepository = memberRepository; + } + + + public CurrentBalanceResponse findBalance(AuthInfo authInfo) { + Member member = memberRepository.findById(authInfo.getId()) + .orElseThrow(MemberNotFoundException::new); +// Balance balance = balanceRepository.findByMember(member) +// .orElseThrow(); + return new CurrentBalanceResponse(member.getBalance().getBalance()); + } + +} diff --git a/src/main/java/org/mockInvestment/stock/controller/StockPriceController.java b/src/main/java/org/mockInvestment/stock/controller/StockPriceController.java index 5d9f7f7..eea8f96 100644 --- a/src/main/java/org/mockInvestment/stock/controller/StockPriceController.java +++ b/src/main/java/org/mockInvestment/stock/controller/StockPriceController.java @@ -1,5 +1,6 @@ package org.mockInvestment.stock.controller; +import lombok.extern.slf4j.Slf4j; import org.mockInvestment.auth.dto.AuthInfo; import org.mockInvestment.stock.dto.StockPriceCandlesResponse; import org.mockInvestment.stock.dto.StockPricesResponse; @@ -14,6 +15,7 @@ import java.util.List; +@Slf4j @RestController @RequestMapping("/stock-prices") public class StockPriceController { @@ -29,7 +31,8 @@ public class StockPriceController { private final PeriodExtractor fiveYearsPeriodExtractor; - public StockPriceController(StockPriceService stockPriceService, @Qualifier("oneWeekPeriodExtractor") PeriodExtractor oneWeekPeriodExtractor, + public StockPriceController(StockPriceService stockPriceService, + @Qualifier("oneWeekPeriodExtractor") PeriodExtractor oneWeekPeriodExtractor, @Qualifier("threeMonthsPeriodExtractor") PeriodExtractor threeMonthsPeriodExtractor, @Qualifier("oneYearPeriodExtractor") PeriodExtractor oneYearPeriodExtractor, @Qualifier("fiveYearsPeriodExtractor") PeriodExtractor fiveYearsPeriodExtractor) { @@ -48,6 +51,7 @@ public ResponseEntity findStockPrices(@RequestParam("code") @GetMapping(value = "/subscribe", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public ResponseEntity subscribe(@Login AuthInfo authInfo, @RequestParam("code") List stockCodes) { + log.info("StockPriceController - subscribe"); SseEmitter sseEmitter = stockPriceService.subscribeStockPrices(authInfo, stockCodes); return ResponseEntity.ok(sseEmitter); } diff --git a/src/main/java/org/mockInvestment/stock/domain/RecentStockInfo.java b/src/main/java/org/mockInvestment/stock/domain/RecentStockInfo.java new file mode 100644 index 0000000..a524925 --- /dev/null +++ b/src/main/java/org/mockInvestment/stock/domain/RecentStockInfo.java @@ -0,0 +1,19 @@ +package org.mockInvestment.stock.domain; + + +import org.mockInvestment.stock.domain.Stock; +import org.mockInvestment.stock.domain.StockPriceCandle; + +public record RecentStockInfo(String symbol, String name, double base, double close, double curr, double high, double low, double open, long volume) { + + public RecentStockInfo(double base, Stock stock, StockPriceCandle recentPriceCandle) { + this(stock.getSymbol(), stock.getName(), base, + recentPriceCandle.getClose(), + recentPriceCandle.getCurr(), + recentPriceCandle.getHigh(), + recentPriceCandle.getLow(), + recentPriceCandle.getOpen(), + recentPriceCandle.getVolume()); + } + +} \ No newline at end of file diff --git a/src/main/java/org/mockInvestment/stock/domain/StockPrice.java b/src/main/java/org/mockInvestment/stock/domain/StockPrice.java index 0ce3a43..5ff5fca 100644 --- a/src/main/java/org/mockInvestment/stock/domain/StockPrice.java +++ b/src/main/java/org/mockInvestment/stock/domain/StockPrice.java @@ -26,4 +26,5 @@ public StockPrice(Double open, Double high, Double low, Double close, Double cur this.close = close; this.curr = curr; } + } diff --git a/src/main/java/org/mockInvestment/stock/domain/StockPriceCandle.java b/src/main/java/org/mockInvestment/stock/domain/StockPriceCandle.java index 7519d8b..56bc553 100644 --- a/src/main/java/org/mockInvestment/stock/domain/StockPriceCandle.java +++ b/src/main/java/org/mockInvestment/stock/domain/StockPriceCandle.java @@ -32,4 +32,25 @@ public StockPriceCandle(StockPrice price, long volume) { this.price = price; this.volume = volume; } + + public Double getClose() { + return price.getClose(); + } + + public Double getCurr() { + return price.getCurr(); + } + + public Double getOpen() { + return price.getOpen(); + } + + public Double getHigh() { + return price.getHigh(); + } + + public Double getLow() { + return price.getLow(); + } + } diff --git a/src/main/java/org/mockInvestment/stock/dto/LastStockInfo.java b/src/main/java/org/mockInvestment/stock/dto/LastStockInfo.java deleted file mode 100644 index 82652d9..0000000 --- a/src/main/java/org/mockInvestment/stock/dto/LastStockInfo.java +++ /dev/null @@ -1,5 +0,0 @@ -package org.mockInvestment.stock.dto; - - -public record LastStockInfo(String symbol, String name, double base, double close, double curr, double high, double low, double open, long volume) { -} \ No newline at end of file diff --git a/src/main/java/org/mockInvestment/stock/dto/StockPriceResponse.java b/src/main/java/org/mockInvestment/stock/dto/StockPriceResponse.java index 85207de..60a43c2 100644 --- a/src/main/java/org/mockInvestment/stock/dto/StockPriceResponse.java +++ b/src/main/java/org/mockInvestment/stock/dto/StockPriceResponse.java @@ -1,8 +1,10 @@ package org.mockInvestment.stock.dto; +import org.mockInvestment.stock.domain.RecentStockInfo; + public record StockPriceResponse(String code, String name, double base, double curr) { - public static StockPriceResponse of(String code, LastStockInfo lastStockInfo) { - return new StockPriceResponse(code, lastStockInfo.name(), lastStockInfo.base(), lastStockInfo.curr()); + public static StockPriceResponse of(String code, RecentStockInfo recentStockInfo) { + return new StockPriceResponse(code, recentStockInfo.name(), recentStockInfo.base(), recentStockInfo.curr()); } } diff --git a/src/main/java/org/mockInvestment/stock/repository/EmitterRepository.java b/src/main/java/org/mockInvestment/stock/repository/EmitterRepository.java index ad6414a..5ad6dba 100644 --- a/src/main/java/org/mockInvestment/stock/repository/EmitterRepository.java +++ b/src/main/java/org/mockInvestment/stock/repository/EmitterRepository.java @@ -1,49 +1,51 @@ package org.mockInvestment.stock.repository; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Repository; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import java.util.*; import java.util.concurrent.ConcurrentHashMap; +@Slf4j @Repository public class EmitterRepository { private static final Long DEFAULT_TIMEOUT = 60L * 1000; - private final Map emitters = new ConcurrentHashMap<>(); - private final Map> stockIdMappingMemberIds = new ConcurrentHashMap<>(); - - public void createSubscription(long memberId, long stockId) { - SseEmitter emitter = getOrCreateEmitter(memberId); - emitters.put(memberId, emitter); - Set memberIds = stockIdMappingMemberIds.getOrDefault(stockId, new HashSet<>()); - memberIds.add(memberId); - stockIdMappingMemberIds.put(stockId, memberIds); + private final Map emitters = new ConcurrentHashMap<>(); + private final Map> stockIdMappingKeys = new ConcurrentHashMap<>(); + + public void createSubscription(String key, long stockId) { + SseEmitter emitter = getOrCreateEmitter(key); + emitters.put(key, emitter); + Set memberIds = stockIdMappingKeys.getOrDefault(stockId, new HashSet<>()); + memberIds.add(key); + stockIdMappingKeys.put(stockId, memberIds); } - public void deleteSseEmitterByMemberId(long memberId) { - emitters.remove(memberId); - for (Long stockId : stockIdMappingMemberIds.keySet()) { - Set memberIds = stockIdMappingMemberIds.get(stockId); - memberIds.remove(stockId); + public void deleteSseEmitterByKey(String key) { + emitters.remove(key); + for (Long stockId : stockIdMappingKeys.keySet()) { + Set memberIds = stockIdMappingKeys.get(stockId); + memberIds.remove(key); } } - public Optional getSseEmitterByMemberId(long memberId) { - return Optional.ofNullable(emitters.get(memberId)); + public Optional getSseEmitterByKey(String key) { + return Optional.ofNullable(emitters.get(key)); } - public Set getMemberIdsByStockId(long stockId) { - return stockIdMappingMemberIds.getOrDefault(stockId, new HashSet<>()); + public Set getMemberIdsByStockId(long stockId) { + return stockIdMappingKeys.getOrDefault(stockId, new HashSet<>()); } - private SseEmitter getOrCreateEmitter(long memberId) { - SseEmitter emitter = emitters.get(memberId); + private SseEmitter getOrCreateEmitter(String key) { + SseEmitter emitter = emitters.get(key); if (emitter == null) { emitter = new SseEmitter(DEFAULT_TIMEOUT); - emitter.onCompletion(() -> deleteSseEmitterByMemberId(memberId)); - emitter.onTimeout(() -> deleteSseEmitterByMemberId(memberId)); - emitter.onError((error) -> deleteSseEmitterByMemberId(memberId)); + emitter.onCompletion(() -> deleteSseEmitterByKey(key)); + emitter.onTimeout(() -> deleteSseEmitterByKey(key)); + emitter.onError((error) -> deleteSseEmitterByKey(key)); } return emitter; } diff --git a/src/main/java/org/mockInvestment/stock/repository/LastStockInfoCacheRepository.java b/src/main/java/org/mockInvestment/stock/repository/LastStockInfoCacheRepository.java deleted file mode 100644 index 095217b..0000000 --- a/src/main/java/org/mockInvestment/stock/repository/LastStockInfoCacheRepository.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.mockInvestment.stock.repository; - -import org.mockInvestment.stock.dto.LastStockInfo; - -import java.util.Optional; - -public interface LastStockInfoCacheRepository { - - Optional findByStockCode(String code); - -} diff --git a/src/main/java/org/mockInvestment/stock/repository/LastStockInfoRedisRepository.java b/src/main/java/org/mockInvestment/stock/repository/LastStockInfoRedisRepository.java deleted file mode 100644 index ff6150b..0000000 --- a/src/main/java/org/mockInvestment/stock/repository/LastStockInfoRedisRepository.java +++ /dev/null @@ -1,29 +0,0 @@ -package org.mockInvestment.stock.repository; - -import org.mockInvestment.advice.exception.InvalidStockCodeException; -import org.mockInvestment.common.JsonStringMapper; -import org.mockInvestment.stock.dto.LastStockInfo; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.stereotype.Repository; - -import java.util.Optional; - -@Repository -public class LastStockInfoRedisRepository implements LastStockInfoCacheRepository { - - private final RedisTemplate redisTemplate; - - - public LastStockInfoRedisRepository(RedisTemplate redisTemplate) { - this.redisTemplate = redisTemplate; - } - - @Override - public Optional findByStockCode(String code) { - String jsonString = redisTemplate.opsForValue().get(code); - if (jsonString == null) - throw new InvalidStockCodeException(); - return JsonStringMapper.parseJsonString(jsonString, LastStockInfo.class); - } - -} diff --git a/src/main/java/org/mockInvestment/stock/repository/RecentStockInfoCacheRepository.java b/src/main/java/org/mockInvestment/stock/repository/RecentStockInfoCacheRepository.java new file mode 100644 index 0000000..7751a48 --- /dev/null +++ b/src/main/java/org/mockInvestment/stock/repository/RecentStockInfoCacheRepository.java @@ -0,0 +1,13 @@ +package org.mockInvestment.stock.repository; + +import org.mockInvestment.stock.domain.RecentStockInfo; + +import java.util.Optional; + +public interface RecentStockInfoCacheRepository { + + Optional findByStockCode(String code); + + void saveByCode(String code, RecentStockInfo entity); + +} diff --git a/src/main/java/org/mockInvestment/stock/repository/RecentStockInfoRedisRepository.java b/src/main/java/org/mockInvestment/stock/repository/RecentStockInfoRedisRepository.java new file mode 100644 index 0000000..da2d6ea --- /dev/null +++ b/src/main/java/org/mockInvestment/stock/repository/RecentStockInfoRedisRepository.java @@ -0,0 +1,37 @@ +package org.mockInvestment.stock.repository; + +import org.mockInvestment.advice.exception.JsonStringDeserializationFailureException; +import org.mockInvestment.common.JsonStringMapper; +import org.mockInvestment.stock.domain.RecentStockInfo; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Repository; + +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +@Repository +public class RecentStockInfoRedisRepository implements RecentStockInfoCacheRepository { + + private final RedisTemplate redisTemplate; + + + public RecentStockInfoRedisRepository(RedisTemplate redisTemplate) { + this.redisTemplate = redisTemplate; + } + + @Override + public Optional findByStockCode(String code) { + String jsonString = redisTemplate.opsForValue().get(code); + if (jsonString == null) + return Optional.empty(); + return JsonStringMapper.parseJsonString(jsonString, RecentStockInfo.class); + } + + @Override + public void saveByCode(String code, RecentStockInfo entity) { + String jsonString = JsonStringMapper.toJsonString(entity) + .orElseThrow(JsonStringDeserializationFailureException::new); + redisTemplate.opsForValue().set(code, jsonString, 60, TimeUnit.SECONDS); + } + +} diff --git a/src/main/java/org/mockInvestment/stock/repository/StockPriceCandleRepository.java b/src/main/java/org/mockInvestment/stock/repository/StockPriceCandleRepository.java index ff408fa..8c78a59 100644 --- a/src/main/java/org/mockInvestment/stock/repository/StockPriceCandleRepository.java +++ b/src/main/java/org/mockInvestment/stock/repository/StockPriceCandleRepository.java @@ -9,6 +9,8 @@ public interface StockPriceCandleRepository extends JpaRepository { + List findTop2ByStockOrderByDateDesc(Stock stock); + List findAllByStockAndDateBetween(Stock stock, LocalDate startDate, LocalDate endDate); } diff --git a/src/main/java/org/mockInvestment/stock/service/StockInfoService.java b/src/main/java/org/mockInvestment/stock/service/StockInfoService.java index 78c8c3a..b41aa4a 100644 --- a/src/main/java/org/mockInvestment/stock/service/StockInfoService.java +++ b/src/main/java/org/mockInvestment/stock/service/StockInfoService.java @@ -1,28 +1,54 @@ package org.mockInvestment.stock.service; import org.mockInvestment.advice.exception.JsonStringDeserializationFailureException; +import org.mockInvestment.advice.exception.StockNotFoundException; +import org.mockInvestment.stock.domain.RecentStockInfo; +import org.mockInvestment.stock.domain.Stock; +import org.mockInvestment.stock.domain.StockPriceCandle; import org.mockInvestment.stock.dto.*; -import org.mockInvestment.stock.repository.LastStockInfoCacheRepository; +import org.mockInvestment.stock.repository.RecentStockInfoCacheRepository; +import org.mockInvestment.stock.repository.StockPriceCandleRepository; +import org.mockInvestment.stock.repository.StockRepository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.List; + @Service @Transactional(readOnly = true) public class StockInfoService { - private final LastStockInfoCacheRepository lastStockInfoCacheRepository; + private final StockRepository stockRepository; + + private final StockPriceCandleRepository stockPriceCandleRepository; + + private final RecentStockInfoCacheRepository recentStockInfoCacheRepository; - public StockInfoService(LastStockInfoCacheRepository lastStockInfoCacheRepository) { - this.lastStockInfoCacheRepository = lastStockInfoCacheRepository; + public StockInfoService(StockRepository stockRepository, StockPriceCandleRepository stockPriceCandleRepository, RecentStockInfoCacheRepository recentStockInfoCacheRepository) { + this.stockRepository = stockRepository; + this.stockPriceCandleRepository = stockPriceCandleRepository; + this.recentStockInfoCacheRepository = recentStockInfoCacheRepository; } public StockInfoResponse findStockInfo(String stockCode) { - LastStockInfo stockInfo = lastStockInfoCacheRepository.findByStockCode(stockCode) - .orElseThrow(JsonStringDeserializationFailureException::new); + RecentStockInfo stockInfo = recentStockInfoCacheRepository.findByStockCode(stockCode) + .orElseGet(() -> { + RecentStockInfo findStockInfo = findRecentStockInfo(stockCode); + recentStockInfoCacheRepository.saveByCode(stockCode, findStockInfo); + return findStockInfo; + }); double base = stockInfo.base(); double currentPrice = stockInfo.curr(); return new StockInfoResponse(stockInfo.name(), stockInfo.symbol(), base, currentPrice); } + private RecentStockInfo findRecentStockInfo(String stockCode) { + Stock stock = stockRepository.findByCode(stockCode) + .orElseThrow(StockNotFoundException::new); + List stockPriceCandles = stockPriceCandleRepository.findTop2ByStockOrderByDateDesc(stock); + StockPriceCandle recentCandle = stockPriceCandles.get(0); + return new RecentStockInfo(stockPriceCandles.get(1).getClose(), stock, recentCandle); + } + } diff --git a/src/main/java/org/mockInvestment/stock/service/StockPriceService.java b/src/main/java/org/mockInvestment/stock/service/StockPriceService.java index 01238e4..335c44b 100644 --- a/src/main/java/org/mockInvestment/stock/service/StockPriceService.java +++ b/src/main/java/org/mockInvestment/stock/service/StockPriceService.java @@ -1,19 +1,20 @@ package org.mockInvestment.stock.service; -import org.mockInvestment.advice.exception.JsonStringDeserializationFailureException; import org.mockInvestment.advice.exception.StockNotFoundException; import org.mockInvestment.auth.dto.AuthInfo; +import org.mockInvestment.stock.domain.RecentStockInfo; import org.mockInvestment.stock.domain.Stock; import org.mockInvestment.stock.domain.StockPriceCandle; import org.mockInvestment.stock.dto.*; import org.mockInvestment.stock.repository.EmitterRepository; -import org.mockInvestment.stock.repository.LastStockInfoCacheRepository; +import org.mockInvestment.stock.repository.RecentStockInfoCacheRepository; import org.mockInvestment.stock.repository.StockPriceCandleRepository; import org.mockInvestment.stock.repository.StockRepository; import org.mockInvestment.stock.util.PeriodExtractor; import org.mockInvestment.stockOrder.dto.StockCurrentPrice; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import java.io.IOException; @@ -22,20 +23,21 @@ import java.util.Set; @Service +@Transactional(readOnly = true) public class StockPriceService { private final StockRepository stockRepository; - private final LastStockInfoCacheRepository lastStockInfoCacheRepository; + private final RecentStockInfoCacheRepository recentStockInfoCacheRepository; private final StockPriceCandleRepository stockPriceCandleRepository; private final EmitterRepository emitterRepository; - public StockPriceService(StockRepository stockRepository, LastStockInfoCacheRepository lastStockInfoCacheRepository, StockPriceCandleRepository stockPriceCandleRepository, EmitterRepository emitterRepository) { + public StockPriceService(StockRepository stockRepository, RecentStockInfoCacheRepository recentStockInfoCacheRepository, StockPriceCandleRepository stockPriceCandleRepository, EmitterRepository emitterRepository) { this.stockRepository = stockRepository; - this.lastStockInfoCacheRepository = lastStockInfoCacheRepository; + this.recentStockInfoCacheRepository = recentStockInfoCacheRepository; this.stockPriceCandleRepository = stockPriceCandleRepository; this.emitterRepository = emitterRepository; } @@ -43,14 +45,26 @@ public StockPriceService(StockRepository stockRepository, LastStockInfoCacheRepo public StockPricesResponse findStockPrices(List stockCodes) { List responses = new ArrayList<>(); for (String code : stockCodes) { - LastStockInfo stockInfo = lastStockInfoCacheRepository.findByStockCode(code) - .orElseThrow(JsonStringDeserializationFailureException::new); + RecentStockInfo stockInfo = recentStockInfoCacheRepository.findByStockCode(code) + .orElseGet(() -> { + RecentStockInfo findStockInfo = findRecentStockInfo(code); + recentStockInfoCacheRepository.saveByCode(code, findStockInfo); + return findStockInfo; + }); responses.add(StockPriceResponse.of(code, stockInfo)); } return new StockPricesResponse(responses); } + private RecentStockInfo findRecentStockInfo(String stockCode) { + Stock stock = stockRepository.findByCode(stockCode) + .orElseThrow(StockNotFoundException::new); + List stockPriceCandles = stockPriceCandleRepository.findTop2ByStockOrderByDateDesc(stock); + StockPriceCandle recentCandle = stockPriceCandles.get(0); + return new RecentStockInfo(stockPriceCandles.get(1).getClose(), stock, recentCandle); + } + public StockPriceCandlesResponse findStockPriceCandles(String stockCode, PeriodExtractor periodExtractor) { Stock stock = stockRepository.findByCode(stockCode) .orElseThrow(StockNotFoundException::new); @@ -63,18 +77,19 @@ public StockPriceCandlesResponse findStockPriceCandles(String stockCode, PeriodE } public SseEmitter subscribeStockPrices(AuthInfo authInfo, List stockCodes) { + String key = authInfo.getId() + String.valueOf(System.currentTimeMillis()); for (String stockCode : stockCodes) { Stock stock = stockRepository.findByCode(stockCode) .orElseThrow(StockNotFoundException::new); - emitterRepository.createSubscription(authInfo.getId(), stock.getId()); - sendToClient(authInfo.getId(), new StockCurrentPrice(stock.getId(), stockCode, 0.0)); + emitterRepository.createSubscription(key, stock.getId()); + sendToClient(key, new StockCurrentPrice(stock.getId(), stockCode, 0.0)); } - return emitterRepository.getSseEmitterByMemberId(authInfo.getId()) + return emitterRepository.getSseEmitterByKey(key) .orElseThrow(); } - private void sendToClient(long memberId, StockCurrentPrice stockCurrentPrice) { - SseEmitter emitter = emitterRepository.getSseEmitterByMemberId(memberId) + private void sendToClient(String key, StockCurrentPrice stockCurrentPrice) { + SseEmitter emitter = emitterRepository.getSseEmitterByKey(key) .orElseThrow(); try { @@ -83,14 +98,14 @@ private void sendToClient(long memberId, StockCurrentPrice stockCurrentPrice) { .data(stockCurrentPrice)); } catch (IOException e) { emitter.completeWithError(e); - emitterRepository.deleteSseEmitterByMemberId(memberId); + emitterRepository.deleteSseEmitterByKey(key); } } @EventListener protected void publishStockCurrentPrice(StockCurrentPrice stockCurrentPrice) { - Set memberIds = emitterRepository.getMemberIdsByStockId(stockCurrentPrice.stockId()); - for (Long memberId : memberIds) + Set memberIds = emitterRepository.getMemberIdsByStockId(stockCurrentPrice.stockId()); + for (String memberId : memberIds) sendToClient(memberId, stockCurrentPrice); } diff --git a/src/test/java/org/mockInvestment/stock/repository/StockInfoRedisRepositoryTest.java b/src/test/java/org/mockInvestment/stock/repository/StockInfoRedisRepositoryTest.java index 959bdd7..6375d6b 100644 --- a/src/test/java/org/mockInvestment/stock/repository/StockInfoRedisRepositoryTest.java +++ b/src/test/java/org/mockInvestment/stock/repository/StockInfoRedisRepositoryTest.java @@ -5,7 +5,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockInvestment.advice.exception.InvalidStockCodeException; -import org.mockInvestment.stock.dto.LastStockInfo; +import org.mockInvestment.stock.domain.RecentStockInfo; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @@ -29,7 +29,7 @@ class StockInfoRedisRepositoryTest { private RedisTemplate redisTemplate; @InjectMocks - private LastStockInfoRedisRepository stockInfoRedisRepository; + private RecentStockInfoRedisRepository stockInfoRedisRepository; @BeforeEach void setUp() { @@ -54,7 +54,7 @@ void findByStockCode_success_validString() { when(redisTemplate.opsForValue().get(any(String.class))) .thenReturn(validString); - Optional lastStockInfo = stockInfoRedisRepository.findByStockCode("CODE"); + Optional lastStockInfo = stockInfoRedisRepository.findByStockCode("CODE"); assertThat(lastStockInfo.isPresent()).isEqualTo(true); assertThat(lastStockInfo.get().name()).isEqualTo("Apple"); diff --git a/src/test/java/org/mockInvestment/stock/service/StockInfoServiceTest.java b/src/test/java/org/mockInvestment/stock/service/StockInfoServiceTest.java index 5e2ed30..919211d 100644 --- a/src/test/java/org/mockInvestment/stock/service/StockInfoServiceTest.java +++ b/src/test/java/org/mockInvestment/stock/service/StockInfoServiceTest.java @@ -5,9 +5,9 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockInvestment.advice.exception.InvalidStockCodeException; -import org.mockInvestment.stock.dto.LastStockInfo; +import org.mockInvestment.stock.domain.RecentStockInfo; import org.mockInvestment.stock.dto.StockInfoResponse; -import org.mockInvestment.stock.repository.LastStockInfoCacheRepository; +import org.mockInvestment.stock.repository.RecentStockInfoCacheRepository; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @@ -26,19 +26,19 @@ class StockInfoServiceTest { private StockInfoService stockInfoService; @Mock - private LastStockInfoCacheRepository lastStockInfoCacheRepository; + private RecentStockInfoCacheRepository recentStockInfoCacheRepository; - LastStockInfo testStockInfo; + RecentStockInfo testStockInfo; @BeforeEach void setUp() { - testStockInfo = new LastStockInfo("MOCK", "Mock Stock", 0.1, 0.1, 1.0, 1.5, 0.6, 0.1, 10L); + testStockInfo = new RecentStockInfo("MOCK", "Mock Stock", 0.1, 0.1, 1.0, 1.5, 0.6, 0.1, 10L); } @Test @DisplayName("유효한 코드로 특정 주식의 현재 주가에 대한 자세한 정보를 불러온다.") void findStockInfoDetail() { - when(lastStockInfoCacheRepository.findByStockCode(any(String.class))) + when(recentStockInfoCacheRepository.findByStockCode(any(String.class))) .thenReturn(Optional.ofNullable(testStockInfo)); StockInfoResponse response = stockInfoService.findStockInfo("Mock Stock"); @@ -51,7 +51,7 @@ void findStockInfoDetail() { @Test @DisplayName("유효하지 않은 코드로 특정 주식의 현재 주가에 대한 자세한 정보를 요청하면, InvalidStockCodeException 을 발생시킨다.") void findStockInfoDetail_exception_invalidCode() { - when(lastStockInfoCacheRepository.findByStockCode(any(String.class))) + when(recentStockInfoCacheRepository.findByStockCode(any(String.class))) .thenThrow(new InvalidStockCodeException()); assertThatThrownBy(() -> stockInfoService.findStockInfo("XX")) diff --git a/src/test/java/org/mockInvestment/stock/service/StockPriceServiceTest.java b/src/test/java/org/mockInvestment/stock/service/StockPriceServiceTest.java index 4c222c0..0c03944 100644 --- a/src/test/java/org/mockInvestment/stock/service/StockPriceServiceTest.java +++ b/src/test/java/org/mockInvestment/stock/service/StockPriceServiceTest.java @@ -8,9 +8,9 @@ import org.mockInvestment.stock.domain.Stock; import org.mockInvestment.stock.domain.StockPrice; import org.mockInvestment.stock.domain.StockPriceCandle; -import org.mockInvestment.stock.dto.LastStockInfo; +import org.mockInvestment.stock.domain.RecentStockInfo; import org.mockInvestment.stock.dto.StockPriceCandlesResponse; -import org.mockInvestment.stock.repository.LastStockInfoCacheRepository; +import org.mockInvestment.stock.repository.RecentStockInfoCacheRepository; import org.mockInvestment.stock.repository.StockPriceCandleRepository; import org.mockInvestment.stock.repository.StockRepository; import org.mockInvestment.stock.util.PeriodExtractor; @@ -32,7 +32,7 @@ class StockPriceServiceTest { @Mock - private LastStockInfoCacheRepository lastStockInfoCacheRepository; + private RecentStockInfoCacheRepository recentStockInfoCacheRepository; @Mock private StockRepository stockRepository; @@ -46,17 +46,17 @@ class StockPriceServiceTest { @InjectMocks private StockPriceService stockPriceService; - private LastStockInfo testStockInfo; + private RecentStockInfo testStockInfo; @BeforeEach void setUp() { - testStockInfo = new LastStockInfo("MOCK", "Mock Stock", 0.1, 0.1, 1.0, 1.5, 0.6, 0.1, 10L); + testStockInfo = new RecentStockInfo("MOCK", "Mock Stock", 0.1, 0.1, 1.0, 1.5, 0.6, 0.1, 10L); } @Test @DisplayName("유효한 코드로 현재 주가(들)에 대한 간략한 정보를 불러온다.") void findStockInfoSummaries() { - when(lastStockInfoCacheRepository.findByStockCode(any(String.class))) + when(recentStockInfoCacheRepository.findByStockCode(any(String.class))) .thenReturn(Optional.ofNullable(testStockInfo)); List stockCodes = new ArrayList<>(); @@ -68,7 +68,7 @@ void findStockInfoSummaries() { @Test @DisplayName("유효하지 않은 코드로 현재 주가(들)에 대한 간략한 정보를 불려오려고 하면, InvalidStockCodeException 을 발생시킨다.") void findStockCurrentPrice_exception_invalidCodes() { - when(lastStockInfoCacheRepository.findByStockCode(any(String.class))) + when(recentStockInfoCacheRepository.findByStockCode(any(String.class))) .thenThrow(new InvalidStockCodeException()); List stockCodes = new ArrayList<>();