From e0985627a7582325ce226fb967b34fe2c4d92be8 Mon Sep 17 00:00:00 2001 From: Caniro Date: Tue, 1 Jul 2025 20:30:32 +0900 Subject: [PATCH 1/7] =?UTF-8?q?feat:=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EC=B4=9D=20=EC=9E=90=EC=82=B0=20=ED=95=AD?= =?UTF-8?q?=EB=AA=A9=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../coin/order/application/AssetService.java | 19 +++++++++ .../trade/repository/TradeRepository.java | 3 +- .../user/info/application/UserService.java | 39 +++++++++++++++---- .../coin/user/info/infra/OAuthRepository.java | 4 ++ .../coin/user/info/infra/UserRepository.java | 17 -------- .../info/presentation/UserController.java | 21 ++-------- .../user/info/presentation/UserInfoDTO.java | 14 ++++--- .../user/info/presentation/UserWalletDTO.java | 17 ++++++++ .../info/presentation/UserControllerTest.java | 6 +-- 9 files changed, 87 insertions(+), 53 deletions(-) create mode 100644 src/main/java/com/cleanengine/coin/user/info/presentation/UserWalletDTO.java diff --git a/src/main/java/com/cleanengine/coin/order/application/AssetService.java b/src/main/java/com/cleanengine/coin/order/application/AssetService.java index e013b9dc..8ee42a0c 100644 --- a/src/main/java/com/cleanengine/coin/order/application/AssetService.java +++ b/src/main/java/com/cleanengine/coin/order/application/AssetService.java @@ -5,18 +5,25 @@ import com.cleanengine.coin.order.domain.Asset; import com.cleanengine.coin.order.adapter.out.persistentce.asset.AssetCacheRepository; import com.cleanengine.coin.order.adapter.out.persistentce.asset.AssetRepository; +import com.cleanengine.coin.trade.entity.Trade; +import com.cleanengine.coin.trade.repository.TradeRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.validation.FieldError; import java.util.List; import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; @Service @RequiredArgsConstructor public class AssetService { private final AssetRepository assetRepository; private final AssetCacheRepository assetCacheRepository; + private final TradeRepository tradeRepository; + + // TODO : 체결 시 이 필드 업데이트 + private final ConcurrentHashMap currentPriceCache = new ConcurrentHashMap<>(); public AssetInfo getAssetInfo(String ticker){ Optional assetOpt = getAsset(ticker); @@ -49,4 +56,16 @@ public boolean isAssetExist(String ticker){ protected Optional getAsset(String ticker){ return assetRepository.findById(ticker); } + + public double getCurrentPrice(String ticker) { + Double currentPrice = currentPriceCache.get(ticker); + if (currentPrice == null) { + Trade recentTrade = tradeRepository.findFirstByTickerOrderByTradeTimeDesc(ticker); + currentPrice = recentTrade == null ? 0.0 : recentTrade.getPrice(); + currentPriceCache.put(ticker, currentPrice); + } + + return currentPrice; + } + } diff --git a/src/main/java/com/cleanengine/coin/trade/repository/TradeRepository.java b/src/main/java/com/cleanengine/coin/trade/repository/TradeRepository.java index 30bc3de2..25c381cc 100644 --- a/src/main/java/com/cleanengine/coin/trade/repository/TradeRepository.java +++ b/src/main/java/com/cleanengine/coin/trade/repository/TradeRepository.java @@ -5,7 +5,6 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; -import java.awt.print.Pageable; import java.time.LocalDateTime; import java.util.List; @@ -24,4 +23,6 @@ public interface TradeRepository extends JpaRepository { List findBySellUserIdAndTicker(Integer sellUserId, String ticker); List findTop10ByTickerOrderByTradeTimeDesc(String ticker); List findByTickerAndTradeTimeGreaterThanEqualOrderByTradeTimeDesc(String ticker, LocalDateTime lastTime); + Trade findFirstByTickerOrderByTradeTimeDesc(String ticker); + } diff --git a/src/main/java/com/cleanengine/coin/user/info/application/UserService.java b/src/main/java/com/cleanengine/coin/user/info/application/UserService.java index 625ce8a0..5a8ea924 100644 --- a/src/main/java/com/cleanengine/coin/user/info/application/UserService.java +++ b/src/main/java/com/cleanengine/coin/user/info/application/UserService.java @@ -1,20 +1,45 @@ package com.cleanengine.coin.user.info.application; +import com.cleanengine.coin.order.application.AssetService; +import com.cleanengine.coin.user.domain.Account; +import com.cleanengine.coin.user.domain.OAuth; +import com.cleanengine.coin.user.domain.Wallet; +import com.cleanengine.coin.user.info.infra.AccountRepository; +import com.cleanengine.coin.user.info.infra.OAuthRepository; +import com.cleanengine.coin.user.info.infra.WalletRepository; import com.cleanengine.coin.user.info.presentation.UserInfoDTO; -import com.cleanengine.coin.user.info.infra.UserRepository; +import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import java.util.List; + +@RequiredArgsConstructor @Service public class UserService { - private final UserRepository userRepository; - - public UserService(UserRepository userRepository) { - this.userRepository = userRepository; - } + private final AccountRepository accountRepository; + private final WalletRepository walletRepository; + private final OAuthRepository oAuthRepository; + private final AssetService assetService; + @Transactional(readOnly = true) public UserInfoDTO retrieveUserInfoByUserId(Integer userId) { - return userRepository.retrieveUserInfoByUserId(userId); + Account account = accountRepository.findByUserId(userId) + .orElseThrow(() -> new IllegalArgumentException("계좌를 찾을 수 없습니다. userId: " + userId)); + OAuth oauth = oAuthRepository.findByUserId(userId) + .orElseThrow(() -> new IllegalStateException("OAuth 정보를 찾을 수 없습니다. userId: " + userId)); + + // TODO : 모든 종목에 대해 없는 지갑은 생성... 근데 어디서? + List wallets = walletRepository.findByAccountId(account.getId()); + + // 총 자산 계산 (현금 + (각 코인 보유량 * 현재가)) + double totalWalletValue = wallets.stream() + .mapToDouble(wallet -> wallet.getSize() * assetService.getCurrentPrice(wallet.getTicker())) + .sum(); + double totalCash = account.getCash() + totalWalletValue; + + return UserInfoDTO.of(userId, oauth.getEmail(), oauth.getNickname(), oauth.getProvider(), account.getCash(), wallets, totalCash); } } diff --git a/src/main/java/com/cleanengine/coin/user/info/infra/OAuthRepository.java b/src/main/java/com/cleanengine/coin/user/info/infra/OAuthRepository.java index 6c0955de..a858d37e 100644 --- a/src/main/java/com/cleanengine/coin/user/info/infra/OAuthRepository.java +++ b/src/main/java/com/cleanengine/coin/user/info/infra/OAuthRepository.java @@ -3,8 +3,12 @@ import com.cleanengine.coin.user.domain.OAuth; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + public interface OAuthRepository extends JpaRepository { OAuth findByProviderAndProviderUserId(String provider, String providerUserId); + Optional findByUserId(Integer userId); + } diff --git a/src/main/java/com/cleanengine/coin/user/info/infra/UserRepository.java b/src/main/java/com/cleanengine/coin/user/info/infra/UserRepository.java index e7d947c8..cd3fd6b3 100644 --- a/src/main/java/com/cleanengine/coin/user/info/infra/UserRepository.java +++ b/src/main/java/com/cleanengine/coin/user/info/infra/UserRepository.java @@ -2,7 +2,6 @@ import com.cleanengine.coin.user.domain.User; import com.cleanengine.coin.user.login.infra.UserOAuthDetails; -import com.cleanengine.coin.user.info.presentation.UserInfoDTO; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -24,20 +23,4 @@ public interface UserRepository extends JpaRepository { """) UserOAuthDetails findUserByOAuthProviderAndProviderId(@Param("provider") String provider, @Param("providerUserId") String providerUserId); - - @Query(""" - SELECT new com.cleanengine.coin.user.info.presentation.UserInfoDTO( - u.id, - o.email, - o.nickname, - o.provider, - a.cash, - null - ) - FROM User u - JOIN OAuth o ON u.id = o.userId - LEFT JOIN Account a ON a.userId = u.id - WHERE u.id = :userId - """) - UserInfoDTO retrieveUserInfoByUserId(Integer userId); } diff --git a/src/main/java/com/cleanengine/coin/user/info/presentation/UserController.java b/src/main/java/com/cleanengine/coin/user/info/presentation/UserController.java index 0ad1bbf5..b4752e09 100644 --- a/src/main/java/com/cleanengine/coin/user/info/presentation/UserController.java +++ b/src/main/java/com/cleanengine/coin/user/info/presentation/UserController.java @@ -3,46 +3,31 @@ import com.cleanengine.coin.common.response.ApiResponse; import com.cleanengine.coin.common.response.ErrorResponse; import com.cleanengine.coin.common.response.ErrorStatus; -import com.cleanengine.coin.user.domain.Account; -import com.cleanengine.coin.user.domain.Wallet; -import com.cleanengine.coin.user.info.application.AccountService; -import com.cleanengine.coin.user.info.application.WalletService; import com.cleanengine.coin.user.info.application.UserService; import com.cleanengine.coin.user.login.infra.CustomOAuth2User; +import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; -import java.util.List; - +@RequiredArgsConstructor @RestController public class UserController { private final UserService userService; - private final AccountService accountService; - private final WalletService walletService; - - public UserController(UserService userService, AccountService accountService, WalletService walletService) { - this.userService = userService; - this.accountService = accountService; - this.walletService = walletService; - } @GetMapping("/api/userinfo") public ApiResponse retrieveUserInfo() { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication != null && authentication.getPrincipal() instanceof CustomOAuth2User oAuth2User) { Integer userId = oAuth2User.getUserId(); UserInfoDTO userInfoDTO = userService.retrieveUserInfoByUserId(userId); + if (userInfoDTO == null) { return ApiResponse.fail(ErrorResponse.of(ErrorStatus.UNAUTHORIZED_RESOURCE)); } - Account account = accountService.retrieveAccountByUserId(userId); - List wallets = walletService.findByAccountId(account.getId()); - userInfoDTO.setWallets(wallets); return ApiResponse.success(userInfoDTO, HttpStatus.OK); } diff --git a/src/main/java/com/cleanengine/coin/user/info/presentation/UserInfoDTO.java b/src/main/java/com/cleanengine/coin/user/info/presentation/UserInfoDTO.java index ca91c6ad..7224af17 100644 --- a/src/main/java/com/cleanengine/coin/user/info/presentation/UserInfoDTO.java +++ b/src/main/java/com/cleanengine/coin/user/info/presentation/UserInfoDTO.java @@ -3,12 +3,12 @@ import com.cleanengine.coin.user.domain.Wallet; import com.cleanengine.coin.user.info.application.PlainDoubleSerializer; import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import lombok.*; +import lombok.Getter; +import lombok.NoArgsConstructor; import java.util.List; @Getter -@Setter @NoArgsConstructor public class UserInfoDTO { @@ -23,19 +23,23 @@ public class UserInfoDTO { @JsonSerialize(using = PlainDoubleSerializer.class) private Double cash; + @JsonSerialize(using = PlainDoubleSerializer.class) + private Double totalAssetAmount; // 총 자산(cash + wallets 현재가 * 수량) + private List wallets; - private UserInfoDTO(Integer userId, String email, String nickname, String provider, Double cash, List wallets) { + private UserInfoDTO(int userId, String email, String nickname, String provider, double cash, List wallets, double totalAssetAmount) { this.userId = userId; this.email = email; this.nickname = nickname; this.provider = provider; this.cash = cash; this.wallets = wallets; + this.totalAssetAmount = totalAssetAmount; } - public static UserInfoDTO of(Integer userId, String email, String nickname, String provider, Double cash, List wallets) { - return new UserInfoDTO(userId, email, nickname, provider, cash, wallets); + public static UserInfoDTO of(int userId, String email, String nickname, String provider, double cash, List wallets, double totalCash) { + return new UserInfoDTO(userId, email, nickname, provider, cash, wallets, totalCash); } } diff --git a/src/main/java/com/cleanengine/coin/user/info/presentation/UserWalletDTO.java b/src/main/java/com/cleanengine/coin/user/info/presentation/UserWalletDTO.java new file mode 100644 index 00000000..a8f02f75 --- /dev/null +++ b/src/main/java/com/cleanengine/coin/user/info/presentation/UserWalletDTO.java @@ -0,0 +1,17 @@ +package com.cleanengine.coin.user.info.presentation; + +public class UserWalletDTO { + + private String ticker; + + private Integer accountId; + + private Double size; + + private Double buyPrice; // 매수 평단 + + private Double roi; // 수익률 + + private Double currentPrice; // 현재가(최근 체결가) + +} diff --git a/src/test/java/com/cleanengine/coin/user/info/presentation/UserControllerTest.java b/src/test/java/com/cleanengine/coin/user/info/presentation/UserControllerTest.java index dcf80af7..0917c64b 100644 --- a/src/test/java/com/cleanengine/coin/user/info/presentation/UserControllerTest.java +++ b/src/test/java/com/cleanengine/coin/user/info/presentation/UserControllerTest.java @@ -87,7 +87,7 @@ public void testRetrieveUserInfoSuccess() throws Exception { customOAuth2User, null, authorities ); - UserInfoDTO userInfoDTO = UserInfoDTO.of(userId, email, nickname, provider, cash, null); + UserInfoDTO userInfoDTO = UserInfoDTO.of(userId, email, nickname, provider, cash, null, 0.0); when(userService.retrieveUserInfoByUserId(userId)).thenReturn(userInfoDTO); Account account = Account.of(userId, cash); @@ -98,10 +98,6 @@ public void testRetrieveUserInfoSuccess() throws Exception { .andExpect(MockMvcResultMatchers.status().isOk()) .andExpect(MockMvcResultMatchers.jsonPath("$.isSuccess", is(true))) .andExpect(MockMvcResultMatchers.jsonPath("$.data.cash", is((int) cash))); - - verify(userService, times(1)).retrieveUserInfoByUserId(userId); - verify(accountService, times(1)).retrieveAccountByUserId(userId); - verify(walletService, times(1)).findByAccountId(account.getId()); } @Test From 2f02c23b4a4155fb665cac94e0078f712dd3cc23 Mon Sep 17 00:00:00 2001 From: Caniro Date: Wed, 2 Jul 2025 17:25:27 +0900 Subject: [PATCH 2/7] =?UTF-8?q?feat:=20=EC=A7=80=EA=B0=91=20DTO=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD,=20Swagger=20=EC=84=A4=EB=AA=85=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/info/application/UserService.java | 18 ++++++- .../info/presentation/UserController.java | 2 + .../user/info/presentation/UserInfoDTO.java | 15 ++++-- .../user/info/presentation/UserWalletDTO.java | 54 +++++++++++++++---- 4 files changed, 72 insertions(+), 17 deletions(-) diff --git a/src/main/java/com/cleanengine/coin/user/info/application/UserService.java b/src/main/java/com/cleanengine/coin/user/info/application/UserService.java index 5a8ea924..b6b0aba3 100644 --- a/src/main/java/com/cleanengine/coin/user/info/application/UserService.java +++ b/src/main/java/com/cleanengine/coin/user/info/application/UserService.java @@ -8,6 +8,7 @@ import com.cleanengine.coin.user.info.infra.OAuthRepository; import com.cleanengine.coin.user.info.infra.WalletRepository; import com.cleanengine.coin.user.info.presentation.UserInfoDTO; +import com.cleanengine.coin.user.info.presentation.UserWalletDTO; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -35,11 +36,24 @@ public UserInfoDTO retrieveUserInfoByUserId(Integer userId) { // 총 자산 계산 (현금 + (각 코인 보유량 * 현재가)) double totalWalletValue = wallets.stream() - .mapToDouble(wallet -> wallet.getSize() * assetService.getCurrentPrice(wallet.getTicker())) + .mapToDouble(wallet -> + wallet.getSize() * assetService.getCurrentPrice(wallet.getTicker())) .sum(); double totalCash = account.getCash() + totalWalletValue; - return UserInfoDTO.of(userId, oauth.getEmail(), oauth.getNickname(), oauth.getProvider(), account.getCash(), wallets, totalCash); + List userWalletDTOs = convertToDTO(wallets); + return UserInfoDTO.of(userId, oauth.getEmail(), oauth.getNickname(), oauth.getProvider(), account.getCash(), userWalletDTOs, totalCash); + } + + private List convertToDTO(List wallets) { + return wallets.stream() + .map(w -> UserWalletDTO.of(w.getTicker(), + w.getAccountId(), + w.getSize(), + w.getBuyPrice(), + w.getRoi(), + assetService.getCurrentPrice(w.getTicker()))) + .toList(); } } diff --git a/src/main/java/com/cleanengine/coin/user/info/presentation/UserController.java b/src/main/java/com/cleanengine/coin/user/info/presentation/UserController.java index b4752e09..171378a4 100644 --- a/src/main/java/com/cleanengine/coin/user/info/presentation/UserController.java +++ b/src/main/java/com/cleanengine/coin/user/info/presentation/UserController.java @@ -5,6 +5,7 @@ import com.cleanengine.coin.common.response.ErrorStatus; import com.cleanengine.coin.user.info.application.UserService; import com.cleanengine.coin.user.login.infra.CustomOAuth2User; +import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.security.core.Authentication; @@ -18,6 +19,7 @@ public class UserController { private final UserService userService; + @Operation(summary = "쿠키의 유저ID를 통해 유저 정보와 보유 자산을 불러옵니다.") @GetMapping("/api/userinfo") public ApiResponse retrieveUserInfo() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); diff --git a/src/main/java/com/cleanengine/coin/user/info/presentation/UserInfoDTO.java b/src/main/java/com/cleanengine/coin/user/info/presentation/UserInfoDTO.java index 7224af17..5a732caf 100644 --- a/src/main/java/com/cleanengine/coin/user/info/presentation/UserInfoDTO.java +++ b/src/main/java/com/cleanengine/coin/user/info/presentation/UserInfoDTO.java @@ -1,8 +1,8 @@ package com.cleanengine.coin.user.info.presentation; -import com.cleanengine.coin.user.domain.Wallet; import com.cleanengine.coin.user.info.application.PlainDoubleSerializer; import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; import lombok.NoArgsConstructor; @@ -12,23 +12,30 @@ @NoArgsConstructor public class UserInfoDTO { + @Schema(description = "User ID", example = "3") private Integer userId; + @Schema(description = "이메일", example = "a@a.com") private String email; + @Schema(description = "닉네임", example = "버황") private String nickname; + @Schema(description = "oauth 제공자", example = "kakao") private String provider; + @Schema(description = "예수금", example = "50000000") @JsonSerialize(using = PlainDoubleSerializer.class) private Double cash; + @Schema(description = "총 자산", example = "500000000") @JsonSerialize(using = PlainDoubleSerializer.class) private Double totalAssetAmount; // 총 자산(cash + wallets 현재가 * 수량) - private List wallets; + @Schema(description = "보유 지갑", example = "[{ticker: BTC, size: 10000, buyPrice: 10000000, roi: 0.001}]") + private List wallets; - private UserInfoDTO(int userId, String email, String nickname, String provider, double cash, List wallets, double totalAssetAmount) { + private UserInfoDTO(int userId, String email, String nickname, String provider, double cash, List wallets, double totalAssetAmount) { this.userId = userId; this.email = email; this.nickname = nickname; @@ -38,7 +45,7 @@ private UserInfoDTO(int userId, String email, String nickname, String provider, this.totalAssetAmount = totalAssetAmount; } - public static UserInfoDTO of(int userId, String email, String nickname, String provider, double cash, List wallets, double totalCash) { + public static UserInfoDTO of(int userId, String email, String nickname, String provider, double cash, List wallets, double totalCash) { return new UserInfoDTO(userId, email, nickname, provider, cash, wallets, totalCash); } diff --git a/src/main/java/com/cleanengine/coin/user/info/presentation/UserWalletDTO.java b/src/main/java/com/cleanengine/coin/user/info/presentation/UserWalletDTO.java index a8f02f75..da8a8699 100644 --- a/src/main/java/com/cleanengine/coin/user/info/presentation/UserWalletDTO.java +++ b/src/main/java/com/cleanengine/coin/user/info/presentation/UserWalletDTO.java @@ -1,17 +1,49 @@ package com.cleanengine.coin.user.info.presentation; -public class UserWalletDTO { - - private String ticker; - - private Integer accountId; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Getter; - private Double size; - - private Double buyPrice; // 매수 평단 - - private Double roi; // 수익률 +@Getter +public class UserWalletDTO { - private Double currentPrice; // 현재가(최근 체결가) + @Schema(description = "종목 티커", example = "BTC") + private final String ticker; + + @Schema(description = "계좌 ID", example = "3") + private final Integer accountId; + + @Schema(description = "보유수량", example = "2.5") + private final Double size; + + @Schema(description = "1주 평균 매수 금액", example = "15000") + private final Double buyPrice; // 매수 평단 + + @Schema(description = "수익률", example = "10") + private final Double roi; // 수익률 + + @Schema(description = "현재가(최근 체결가)", example = "16500") + private final Double currentPrice; // 현재가(최근 체결가) + + @Builder + public UserWalletDTO(String ticker, Integer accountId, Double size, Double buyPrice, Double roi, Double currentPrice) { + this.ticker = ticker; + this.accountId = accountId; + this.size = size; + this.buyPrice = buyPrice; + this.roi = roi; + this.currentPrice = currentPrice; + } + + public static UserWalletDTO of(String ticker, Integer accountId, Double size, Double buyPrice, Double roi, Double currentPrice) { + return UserWalletDTO.builder() + .ticker(ticker) + .accountId(accountId) + .size(size) + .buyPrice(buyPrice) + .roi(roi) + .currentPrice(currentPrice) + .build(); + } } From ce28b631423468410d64b6573ee4596e20304869 Mon Sep 17 00:00:00 2001 From: Caniro Date: Wed, 2 Jul 2025 17:42:50 +0900 Subject: [PATCH 3/7] =?UTF-8?q?feat:=20=EC=B2=B4=EA=B2=B0=20=EC=99=84?= =?UTF-8?q?=EB=A3=8C=20=EC=8B=9C=20=EC=A2=85=EB=AA=A9=EB=B3=84=20=EC=B5=9C?= =?UTF-8?q?=EA=B7=BC=20=EC=B2=B4=EA=B2=B0=EA=B0=80=20=EC=A0=80=EC=9E=A5?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../coin/order/application/AssetService.java | 23 ++++++++----------- ...xecutedUpdateAssetCurrentPriceHandler.java | 22 ++++++++++++++++++ 2 files changed, 31 insertions(+), 14 deletions(-) create mode 100644 src/main/java/com/cleanengine/coin/order/application/event/SpringTradeExecutedUpdateAssetCurrentPriceHandler.java diff --git a/src/main/java/com/cleanengine/coin/order/application/AssetService.java b/src/main/java/com/cleanengine/coin/order/application/AssetService.java index 8ee42a0c..998e501a 100644 --- a/src/main/java/com/cleanengine/coin/order/application/AssetService.java +++ b/src/main/java/com/cleanengine/coin/order/application/AssetService.java @@ -22,7 +22,6 @@ public class AssetService { private final AssetCacheRepository assetCacheRepository; private final TradeRepository tradeRepository; - // TODO : 체결 시 이 필드 업데이트 private final ConcurrentHashMap currentPriceCache = new ConcurrentHashMap<>(); public AssetInfo getAssetInfo(String ticker){ @@ -40,10 +39,6 @@ public List getAllAssetInfos(){ return assetRepository.findAll().stream().map(AssetInfo::from).toList(); } - public List getAllAssetTickers(){ - return assetRepository.findAll().stream().map(Asset::getTicker).toList(); - } - public boolean isAssetExist(String ticker){ if(assetCacheRepository.isAssetExists(ticker)) return true; @@ -57,15 +52,15 @@ protected Optional getAsset(String ticker){ return assetRepository.findById(ticker); } - public double getCurrentPrice(String ticker) { - Double currentPrice = currentPriceCache.get(ticker); - if (currentPrice == null) { - Trade recentTrade = tradeRepository.findFirstByTickerOrderByTradeTimeDesc(ticker); - currentPrice = recentTrade == null ? 0.0 : recentTrade.getPrice(); - currentPriceCache.put(ticker, currentPrice); - } + public Double getCurrentPrice(String ticker) { + return currentPriceCache.computeIfAbsent(ticker, t -> { + Trade recentTrade = tradeRepository.findFirstByTickerOrderByTradeTimeDesc(t); + return recentTrade == null ? null : recentTrade.getPrice(); + }); + } - return currentPrice; + public void updateCurrentPrice(String ticker, double price) { + currentPriceCache.put(ticker, price); } -} +} \ No newline at end of file diff --git a/src/main/java/com/cleanengine/coin/order/application/event/SpringTradeExecutedUpdateAssetCurrentPriceHandler.java b/src/main/java/com/cleanengine/coin/order/application/event/SpringTradeExecutedUpdateAssetCurrentPriceHandler.java new file mode 100644 index 00000000..0639ee8e --- /dev/null +++ b/src/main/java/com/cleanengine/coin/order/application/event/SpringTradeExecutedUpdateAssetCurrentPriceHandler.java @@ -0,0 +1,22 @@ +package com.cleanengine.coin.order.application.event; + +import com.cleanengine.coin.order.application.AssetService; +import com.cleanengine.coin.trade.application.TradeExecutedEvent; +import com.cleanengine.coin.trade.entity.Trade; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.transaction.event.TransactionalEventListener; + +@Component +@RequiredArgsConstructor +public class SpringTradeExecutedUpdateAssetCurrentPriceHandler { + + private final AssetService assetService; + + @TransactionalEventListener + public void onTradeExecutedEvent(TradeExecutedEvent tradeExecutedEvent) { + Trade trade = tradeExecutedEvent.getTrade(); + assetService.updateCurrentPrice(trade.getTicker(), trade.getPrice()); + } + +} From 54a103260715429e0dfedb7cec539fc7c199f2c2 Mon Sep 17 00:00:00 2001 From: Caniro Date: Wed, 2 Jul 2025 18:18:12 +0900 Subject: [PATCH 4/7] =?UTF-8?q?feat:=20=EC=9E=90=EC=82=B0=EC=97=90=20?= =?UTF-8?q?=EC=A2=85=EB=AA=A9=EB=AA=85=20=ED=91=9C=EA=B8=B0=EB=90=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD,=20asset=20=EC=9D=B8?= =?UTF-8?q?=EB=A9=94=EB=AA=A8=EB=A6=AC=20=EC=BA=90=EC=8B=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{IconInitializer.java => AssetInitializer.java} | 12 +++++++++--- .../out/persistentce/asset/AssetRepository.java | 4 ++++ .../coin/order/application/AssetService.java | 11 +++++++++++ .../coin/user/info/application/UserService.java | 1 + .../coin/user/info/presentation/UserWalletDTO.java | 9 +++++++-- 5 files changed, 32 insertions(+), 5 deletions(-) rename src/main/java/com/cleanengine/coin/configuration/bootstrap/{IconInitializer.java => AssetInitializer.java} (87%) diff --git a/src/main/java/com/cleanengine/coin/configuration/bootstrap/IconInitializer.java b/src/main/java/com/cleanengine/coin/configuration/bootstrap/AssetInitializer.java similarity index 87% rename from src/main/java/com/cleanengine/coin/configuration/bootstrap/IconInitializer.java rename to src/main/java/com/cleanengine/coin/configuration/bootstrap/AssetInitializer.java index e4387640..71171b7b 100644 --- a/src/main/java/com/cleanengine/coin/configuration/bootstrap/IconInitializer.java +++ b/src/main/java/com/cleanengine/coin/configuration/bootstrap/AssetInitializer.java @@ -1,8 +1,9 @@ package com.cleanengine.coin.configuration.bootstrap; import com.cleanengine.coin.common.annotation.WorkingServerProfile; -import com.cleanengine.coin.order.domain.Asset; import com.cleanengine.coin.order.adapter.out.persistentce.asset.AssetRepository; +import com.cleanengine.coin.order.application.AssetService; +import com.cleanengine.coin.order.domain.Asset; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.ApplicationArguments; @@ -25,14 +26,19 @@ @WorkingServerProfile @Slf4j @RequiredArgsConstructor -public class IconInitializer implements ApplicationRunner { +public class AssetInitializer implements ApplicationRunner { private final AssetRepository assetRepository; + private final AssetService assetService; private final ResourceLoader resourceLoader; @Override - public void run(ApplicationArguments args) throws Exception { + public void run(ApplicationArguments args) { List assets = loadAssets(); + assetService.setAssetCache(assets); + updateIfIconAbsentInDB(assets); + } + private void updateIfIconAbsentInDB(List assets) { for(Asset asset : assets){ if(asset.getIcon() != null) continue; byte[] encodedIconBytes = loadEncodedIcon(asset.getTicker()); diff --git a/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/asset/AssetRepository.java b/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/asset/AssetRepository.java index 4115e9f7..a0d422ac 100644 --- a/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/asset/AssetRepository.java +++ b/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/asset/AssetRepository.java @@ -2,10 +2,14 @@ import com.cleanengine.coin.order.domain.Asset; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import java.util.List; public interface AssetRepository extends JpaRepository { @Override List findAll(); + + @Query("SELECT a.name FROM Asset a WHERE a.ticker = :ticker") + String findNameById(String ticker); } diff --git a/src/main/java/com/cleanengine/coin/order/application/AssetService.java b/src/main/java/com/cleanengine/coin/order/application/AssetService.java index 998e501a..2e6c856b 100644 --- a/src/main/java/com/cleanengine/coin/order/application/AssetService.java +++ b/src/main/java/com/cleanengine/coin/order/application/AssetService.java @@ -23,6 +23,17 @@ public class AssetService { private final TradeRepository tradeRepository; private final ConcurrentHashMap currentPriceCache = new ConcurrentHashMap<>(); + private final ConcurrentHashMap assetCache = new ConcurrentHashMap<>(); + + public void setAssetCache(List assets) { + assets.forEach(a -> assetCache.putIfAbsent(a.getTicker(), a)); + } + + public String getAssetName(String ticker){ + Asset asset = assetCache.get(ticker); + + return asset == null ? assetRepository.findNameById(ticker) : asset.getName(); + } public AssetInfo getAssetInfo(String ticker){ Optional assetOpt = getAsset(ticker); diff --git a/src/main/java/com/cleanengine/coin/user/info/application/UserService.java b/src/main/java/com/cleanengine/coin/user/info/application/UserService.java index b6b0aba3..ea6d0cc7 100644 --- a/src/main/java/com/cleanengine/coin/user/info/application/UserService.java +++ b/src/main/java/com/cleanengine/coin/user/info/application/UserService.java @@ -48,6 +48,7 @@ public UserInfoDTO retrieveUserInfoByUserId(Integer userId) { private List convertToDTO(List wallets) { return wallets.stream() .map(w -> UserWalletDTO.of(w.getTicker(), + assetService.getAssetName(w.getTicker()), w.getAccountId(), w.getSize(), w.getBuyPrice(), diff --git a/src/main/java/com/cleanengine/coin/user/info/presentation/UserWalletDTO.java b/src/main/java/com/cleanengine/coin/user/info/presentation/UserWalletDTO.java index da8a8699..db9be648 100644 --- a/src/main/java/com/cleanengine/coin/user/info/presentation/UserWalletDTO.java +++ b/src/main/java/com/cleanengine/coin/user/info/presentation/UserWalletDTO.java @@ -10,6 +10,9 @@ public class UserWalletDTO { @Schema(description = "종목 티커", example = "BTC") private final String ticker; + @Schema(description = "종목명", example = "비트코인") + private final String name; + @Schema(description = "계좌 ID", example = "3") private final Integer accountId; @@ -26,8 +29,9 @@ public class UserWalletDTO { private final Double currentPrice; // 현재가(최근 체결가) @Builder - public UserWalletDTO(String ticker, Integer accountId, Double size, Double buyPrice, Double roi, Double currentPrice) { + public UserWalletDTO(String ticker, String name, Integer accountId, Double size, Double buyPrice, Double roi, Double currentPrice) { this.ticker = ticker; + this.name = name; this.accountId = accountId; this.size = size; this.buyPrice = buyPrice; @@ -35,9 +39,10 @@ public UserWalletDTO(String ticker, Integer accountId, Double size, Double buyPr this.currentPrice = currentPrice; } - public static UserWalletDTO of(String ticker, Integer accountId, Double size, Double buyPrice, Double roi, Double currentPrice) { + public static UserWalletDTO of(String ticker, String name, Integer accountId, Double size, Double buyPrice, Double roi, Double currentPrice) { return UserWalletDTO.builder() .ticker(ticker) + .name(name) .accountId(accountId) .size(size) .buyPrice(buyPrice) From 751d4e280c19175a2a40ca8b614776bc77422230 Mon Sep 17 00:00:00 2001 From: Caniro Date: Wed, 2 Jul 2025 18:40:27 +0900 Subject: [PATCH 5/7] =?UTF-8?q?feat:=20=EC=9E=90=EC=82=B0=20=EC=88=98?= =?UTF-8?q?=EC=9D=B5=EB=A5=A0=20=EA=B3=84=EC=82=B0=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/info/application/UserService.java | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/cleanengine/coin/user/info/application/UserService.java b/src/main/java/com/cleanengine/coin/user/info/application/UserService.java index ea6d0cc7..f67a0a41 100644 --- a/src/main/java/com/cleanengine/coin/user/info/application/UserService.java +++ b/src/main/java/com/cleanengine/coin/user/info/application/UserService.java @@ -47,13 +47,18 @@ public UserInfoDTO retrieveUserInfoByUserId(Integer userId) { private List convertToDTO(List wallets) { return wallets.stream() - .map(w -> UserWalletDTO.of(w.getTicker(), - assetService.getAssetName(w.getTicker()), - w.getAccountId(), - w.getSize(), - w.getBuyPrice(), - w.getRoi(), - assetService.getCurrentPrice(w.getTicker()))) + .map(w -> { + Double currentPrice = assetService.getCurrentPrice(w.getTicker()); + Double roi = currentPrice == null ? null : (currentPrice / w.getBuyPrice() - 1) * 100; + + return UserWalletDTO.of(w.getTicker(), + assetService.getAssetName(w.getTicker()), + w.getAccountId(), + w.getSize(), + w.getBuyPrice(), + roi, + currentPrice); + }) .toList(); } From 50af29bdf17de24336c95ae308b48badc52bbd4f Mon Sep 17 00:00:00 2001 From: Caniro Date: Wed, 2 Jul 2025 19:01:25 +0900 Subject: [PATCH 6/7] =?UTF-8?q?config:=20AWS=20=EC=84=9C=EB=B2=84=20?= =?UTF-8?q?=EC=A2=85=EB=A3=8C=EB=A1=9C=20=EC=9D=B8=ED=95=9C=20SonarQube=20?= =?UTF-8?q?Github=20Actions=20=EB=B9=84=ED=99=9C=EC=84=B1=ED=99=94=20(?= =?UTF-8?q?=EC=88=98=EB=8F=99=20=EC=8B=A4=ED=96=89=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/code-analyze-sonarqube.yml | 28 +++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/.github/workflows/code-analyze-sonarqube.yml b/.github/workflows/code-analyze-sonarqube.yml index a2bf6f8d..7dbb095e 100644 --- a/.github/workflows/code-analyze-sonarqube.yml +++ b/.github/workflows/code-analyze-sonarqube.yml @@ -3,19 +3,21 @@ name: Code Analyze With SonarQube run-name: Run code analyze triggered by ${{github.actor}} on: - pull_request: - types: [opened, reopened, synchronize] - branches: - - main - - dev - paths: - - 'src/**' - push: - branches: - - main - - dev - paths: - - 'src/**' + workflow_dispatch: + +# pull_request: +# types: [opened, reopened, synchronize] +# branches: +# - main +# - dev +# paths: +# - 'src/**' +# push: +# branches: +# - main +# - dev +# paths: +# - 'src/**' jobs: build: From 58e0f8d60cda30c0fda9eecb3a65c7c840a44db7 Mon Sep 17 00:00:00 2001 From: Caniro Date: Wed, 2 Jul 2025 19:37:20 +0900 Subject: [PATCH 7/7] =?UTF-8?q?chore:=20Asset=20=EC=95=84=EC=9D=B4?= =?UTF-8?q?=EC=BD=98=20=EC=A0=80=EC=9E=A5=20=ED=9B=84=20=EC=BA=90=EC=8B=9C?= =?UTF-8?q?=EB=90=98=EB=8F=84=EB=A1=9D=20=EC=88=9C=EC=84=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../coin/configuration/bootstrap/AssetInitializer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/cleanengine/coin/configuration/bootstrap/AssetInitializer.java b/src/main/java/com/cleanengine/coin/configuration/bootstrap/AssetInitializer.java index 71171b7b..abadb900 100644 --- a/src/main/java/com/cleanengine/coin/configuration/bootstrap/AssetInitializer.java +++ b/src/main/java/com/cleanengine/coin/configuration/bootstrap/AssetInitializer.java @@ -34,8 +34,8 @@ public class AssetInitializer implements ApplicationRunner { @Override public void run(ApplicationArguments args) { List assets = loadAssets(); - assetService.setAssetCache(assets); updateIfIconAbsentInDB(assets); + assetService.setAssetCache(assets); } private void updateIfIconAbsentInDB(List assets) {