Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 15 additions & 13 deletions .github/workflows/code-analyze-sonarqube.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<Asset> assets = loadAssets();
updateIfIconAbsentInDB(assets);
assetService.setAssetCache(assets);
}

private void updateIfIconAbsentInDB(List<Asset> assets) {
for(Asset asset : assets){
if(asset.getIcon() != null) continue;
byte[] encodedIconBytes = loadEncodedIcon(asset.getTicker());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Asset, String> {
@Override
List<Asset> findAll();

@Query("SELECT a.name FROM Asset a WHERE a.ticker = :ticker")
String findNameById(String ticker);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,35 @@
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;

private final ConcurrentHashMap<String, Double> currentPriceCache = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, Asset> assetCache = new ConcurrentHashMap<>();

public void setAssetCache(List<Asset> 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<Asset> assetOpt = getAsset(ticker);
Expand All @@ -33,10 +50,6 @@ public List<AssetInfo> getAllAssetInfos(){
return assetRepository.findAll().stream().map(AssetInfo::from).toList();
}

public List<String> getAllAssetTickers(){
return assetRepository.findAll().stream().map(Asset::getTicker).toList();
}

public boolean isAssetExist(String ticker){
if(assetCacheRepository.isAssetExists(ticker)) return true;

Expand All @@ -49,4 +62,16 @@ public boolean isAssetExist(String ticker){
protected Optional<Asset> getAsset(String ticker){
return assetRepository.findById(ticker);
}
}

public Double getCurrentPrice(String ticker) {
return currentPriceCache.computeIfAbsent(ticker, t -> {
Trade recentTrade = tradeRepository.findFirstByTickerOrderByTradeTimeDesc(t);
return recentTrade == null ? null : recentTrade.getPrice();
});
}

public void updateCurrentPrice(String ticker, double price) {
currentPriceCache.put(ticker, price);
}

}
Original file line number Diff line number Diff line change
@@ -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());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -24,4 +23,6 @@ public interface TradeRepository extends JpaRepository<Trade, Integer> {
List<Trade> findBySellUserIdAndTicker(Integer sellUserId, String ticker);
List<Trade> findTop10ByTickerOrderByTradeTimeDesc(String ticker);
List<Trade> findByTickerAndTradeTimeGreaterThanEqualOrderByTradeTimeDesc(String ticker, LocalDateTime lastTime);
Trade findFirstByTickerOrderByTradeTimeDesc(String ticker);

}
Original file line number Diff line number Diff line change
@@ -1,20 +1,65 @@
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 com.cleanengine.coin.user.info.presentation.UserWalletDTO;
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;
private final AccountRepository accountRepository;
private final WalletRepository walletRepository;
private final OAuthRepository oAuthRepository;
private final AssetService assetService;

@Transactional(readOnly = true)
public UserInfoDTO retrieveUserInfoByUserId(Integer userId) {
Account account = accountRepository.findByUserId(userId)
.orElseThrow(() -> new IllegalArgumentException("계좌를 찾을 수 없습니다. userId: " + userId));
OAuth oauth = oAuthRepository.findByUserId(userId)
.orElseThrow(() -> new IllegalStateException("OAuth 정보를 찾을 수 없습니다. userId: " + userId));

// TODO : 모든 종목에 대해 없는 지갑은 생성... 근데 어디서?
List<Wallet> wallets = walletRepository.findByAccountId(account.getId());

public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
// 총 자산 계산 (현금 + (각 코인 보유량 * 현재가))
double totalWalletValue = wallets.stream()
.mapToDouble(wallet ->
wallet.getSize() * assetService.getCurrentPrice(wallet.getTicker()))
.sum();
double totalCash = account.getCash() + totalWalletValue;

List<UserWalletDTO> userWalletDTOs = convertToDTO(wallets);
return UserInfoDTO.of(userId, oauth.getEmail(), oauth.getNickname(), oauth.getProvider(), account.getCash(), userWalletDTOs, totalCash);
}

public UserInfoDTO retrieveUserInfoByUserId(Integer userId) {
return userRepository.retrieveUserInfoByUserId(userId);
private List<UserWalletDTO> convertToDTO(List<Wallet> wallets) {
return wallets.stream()
.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();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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, Long> {

OAuth findByProviderAndProviderUserId(String provider, String providerUserId);

Optional<OAuth> findByUserId(Integer userId);

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -24,20 +23,4 @@ public interface UserRepository extends JpaRepository<User, Integer> {
""")
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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,46 +3,33 @@
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 io.swagger.v3.oas.annotations.Operation;
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;
}

@Operation(summary = "쿠키의 유저ID를 통해 유저 정보와 보유 자산을 불러옵니다.")
@GetMapping("/api/userinfo")
public ApiResponse<UserInfoDTO> 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<Wallet> wallets = walletService.findByAccountId(account.getId());
userInfoDTO.setWallets(wallets);

return ApiResponse.success(userInfoDTO, HttpStatus.OK);
}
Expand Down
Loading