Skip to content

Commit

Permalink
[feat] 사용자 보유 주식 조회 기능 구현
Browse files Browse the repository at this point in the history
close #11
  • Loading branch information
pagh2322 committed Mar 25, 2024
1 parent 46e7d41 commit b7c1d9f
Show file tree
Hide file tree
Showing 24 changed files with 323 additions and 51 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.mockInvestment.advice.exception;

import org.mockInvestment.advice.exception.general.BadRequestException;

public class InvalidStockOrderException extends BadRequestException {

private static final String MESSAGE = "주식 구매 요청이 유효하지 않습니다.";

public InvalidStockOrderException() {
super(MESSAGE);
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.mockInvestment.advice.exception;

import org.mockInvestment.advice.exception.general.BadRequestException;

public class InvalidStockOrderTypeException extends BadRequestException {

private static final String MESSAGE = "주식 구매 요청이 유효하지 않습니다.";

public InvalidStockOrderTypeException() {
super(MESSAGE);
}
}


7 changes: 6 additions & 1 deletion src/main/java/org/mockInvestment/member/domain/Member.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.mockInvestment.balance.domain.Balance;
import org.mockInvestment.stock.domain.MemberOwnStock;
import org.mockInvestment.stockOrder.domain.StockOrder;

import java.util.ArrayList;
Expand All @@ -32,6 +33,9 @@ public class Member {
@OneToMany(mappedBy = "member")
private List<StockOrder> stockOrders = new ArrayList<>();

@OneToMany(mappedBy = "member")
private List<MemberOwnStock> ownStocks = new ArrayList<>();

@OneToOne
private Balance balance;

Expand All @@ -47,7 +51,8 @@ public Member(Long id, String name, String email, String role, String username)
}

public void bidStock(StockOrder stockOrder) {
balance.purchase(stockOrder.totalBidPrice());
if (stockOrder.isBuy())
balance.purchase(stockOrder.totalBidPrice());
stockOrders.add(stockOrder);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
package org.mockInvestment.stock.controller;

import org.mockInvestment.auth.dto.AuthInfo;
import org.mockInvestment.stock.dto.MemberOwnStocksResponse;
import org.mockInvestment.stock.dto.StockInfoResponse;
import org.mockInvestment.stock.service.StockInfoService;
import org.mockInvestment.support.auth.Login;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;


@RestController
public class StockInfoController {

private final StockInfoService stockInfoService;


public StockInfoController(StockInfoService stockInfoService) {
this.stockInfoService = stockInfoService;
}
Expand All @@ -20,4 +25,11 @@ public ResponseEntity<StockInfoResponse> findStockInfo(@PathVariable("code") Str
return ResponseEntity.ok(response);
}

@GetMapping("/member/me/own-stock")
public ResponseEntity<MemberOwnStocksResponse> findMyOwnStocks(@Login AuthInfo authInfo,
@RequestParam(value = "code", defaultValue = "") String stockCode) {
MemberOwnStocksResponse response = stockInfoService.findMyOwnStocks(authInfo, stockCode);
return ResponseEntity.ok(response);
}

}
54 changes: 54 additions & 0 deletions src/main/java/org/mockInvestment/stock/domain/MemberOwnStock.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package org.mockInvestment.stock.domain;

import jakarta.persistence.*;
import lombok.Getter;
import org.mockInvestment.advice.exception.InvalidStockOrderException;
import org.mockInvestment.member.domain.Member;
import org.mockInvestment.stockOrder.domain.StockOrder;

import java.util.List;

@Getter
@Entity
public class MemberOwnStock {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@ManyToOne(fetch = FetchType.LAZY)
private Member member;

@ManyToOne(fetch = FetchType.LAZY)
private Stock stock;

@OneToMany(mappedBy = "memberOwnStock")
private List<StockOrder> stockOrders;

private Long volume;

private double averageCost;


public void apply(double price, long volume, boolean isBuy) {
if (isBuy)
buy(price, volume);
else
sell(price, volume);
}

private void buy(double price, long volume) {
double total = (averageCost * this.volume) + (price * volume);
this.volume += volume;
averageCost = total / (double) volume;
}

private void sell(double price, long volume) {
double total = (averageCost * this.volume) - (price * volume);
if (this.volume - volume < 0)
throw new InvalidStockOrderException();
this.volume -= volume;
averageCost = total / (double) volume;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package org.mockInvestment.stock.domain;

public record UpdateStockCurrentPriceEvent(long stockId, String code, double curr) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package org.mockInvestment.stock.dto;


import org.mockInvestment.stock.domain.MemberOwnStock;

public record MemberOwnStockResponse(long id, double averageCost, long volume, String code, String symbol, String name) {

public static MemberOwnStockResponse of(MemberOwnStock entity) {
return new MemberOwnStockResponse(entity.getId(),
entity.getAverageCost(),
entity.getVolume(),
entity.getStock().getCode(),
entity.getStock().getSymbol(),
entity.getStock().getName());
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package org.mockInvestment.stock.dto;

import java.util.List;

public record MemberOwnStocksResponse(List<MemberOwnStockResponse> stocks) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.mockInvestment.stock.repository;

import org.mockInvestment.member.domain.Member;
import org.mockInvestment.stock.domain.MemberOwnStock;
import org.mockInvestment.stock.domain.Stock;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface MemberOwnStockRepository extends JpaRepository<MemberOwnStock, Long> {

Optional<MemberOwnStock> findByMemberAndStock(Member member, Stock stock);

}
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
package org.mockInvestment.stock.service;

import org.mockInvestment.advice.exception.JsonStringDeserializationFailureException;
import org.mockInvestment.advice.exception.MemberNotFoundException;
import org.mockInvestment.advice.exception.StockNotFoundException;
import org.mockInvestment.auth.dto.AuthInfo;
import org.mockInvestment.member.domain.Member;
import org.mockInvestment.member.repository.MemberRepository;
import org.mockInvestment.stock.dto.MemberOwnStockResponse;
import org.mockInvestment.stock.dto.MemberOwnStocksResponse;
import org.mockInvestment.stock.domain.MemberOwnStock;
import org.mockInvestment.stock.domain.RecentStockInfo;
import org.mockInvestment.stock.domain.Stock;
import org.mockInvestment.stock.domain.StockPriceCandle;
Expand All @@ -12,20 +18,27 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.List;

@Service
@Transactional(readOnly = true)
public class StockInfoService {

private final MemberRepository memberRepository;

private final StockRepository stockRepository;

private final StockPriceCandleRepository stockPriceCandleRepository;

private final RecentStockInfoCacheRepository recentStockInfoCacheRepository;


public StockInfoService(StockRepository stockRepository, StockPriceCandleRepository stockPriceCandleRepository, RecentStockInfoCacheRepository recentStockInfoCacheRepository) {
public StockInfoService(MemberRepository memberRepository,
StockRepository stockRepository,
StockPriceCandleRepository stockPriceCandleRepository,
RecentStockInfoCacheRepository recentStockInfoCacheRepository) {
this.memberRepository = memberRepository;
this.stockRepository = stockRepository;
this.stockPriceCandleRepository = stockPriceCandleRepository;
this.recentStockInfoCacheRepository = recentStockInfoCacheRepository;
Expand All @@ -51,4 +64,19 @@ private RecentStockInfo findRecentStockInfo(String stockCode) {
return new RecentStockInfo(stockPriceCandles.get(1).getClose(), stock, recentCandle);
}

public MemberOwnStocksResponse findMyOwnStocks(AuthInfo authInfo, String stockCode) {
Member member = memberRepository.findById(authInfo.getId())
.orElseThrow(MemberNotFoundException::new);
List<MemberOwnStock> ownStocks = member.getOwnStocks().stream()
.filter((ownStock -> {
if (stockCode.isEmpty())
return true;
return stockCode.equals(ownStock.getStock().getCode());
})).toList();
List<MemberOwnStockResponse> responses = new ArrayList<>();
for (MemberOwnStock ownStock : ownStocks)
responses.add(MemberOwnStockResponse.of(ownStock));
return new MemberOwnStocksResponse(responses);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
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.mockInvestment.stock.domain.UpdateStockCurrentPriceEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand Down Expand Up @@ -82,31 +82,31 @@ public SseEmitter subscribeStockPrices(AuthInfo authInfo, List<String> stockCode
Stock stock = stockRepository.findByCode(stockCode)
.orElseThrow(StockNotFoundException::new);
emitterRepository.createSubscription(key, stock.getId());
sendToClient(key, new StockCurrentPrice(stock.getId(), stockCode, 0.0));
sendToClient(key, new UpdateStockCurrentPriceEvent(stock.getId(), stockCode, 0.0));
}
return emitterRepository.getSseEmitterByKey(key)
.orElseThrow();
}

private void sendToClient(String key, StockCurrentPrice stockCurrentPrice) {
private void sendToClient(String key, UpdateStockCurrentPriceEvent updateStockCurrentPriceEvent) {
SseEmitter emitter = emitterRepository.getSseEmitterByKey(key)
.orElseThrow();

try {
emitter.send(SseEmitter.event()
.name("stock-price")
.data(stockCurrentPrice));
.data(updateStockCurrentPriceEvent));
} catch (IOException e) {
emitter.completeWithError(e);
emitterRepository.deleteSseEmitterByKey(key);
}
}

@EventListener
protected void publishStockCurrentPrice(StockCurrentPrice stockCurrentPrice) {
Set<String> memberIds = emitterRepository.getMemberIdsByStockId(stockCurrentPrice.stockId());
protected void publishStockCurrentPrice(UpdateStockCurrentPriceEvent updateStockCurrentPriceEvent) {
Set<String> memberIds = emitterRepository.getMemberIdsByStockId(updateStockCurrentPriceEvent.stockId());
for (String memberId : memberIds)
sendToClient(memberId, stockCurrentPrice);
sendToClient(memberId, updateStockCurrentPriceEvent);
}

}
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
package org.mockInvestment.stockOrder.controller;

import org.mockInvestment.auth.dto.AuthInfo;
import org.mockInvestment.stockOrder.dto.StockOrderHistoriesResponse;
import org.mockInvestment.support.auth.Login;
import org.mockInvestment.stockOrder.dto.StockPurchaseCancelRequest;
import org.mockInvestment.stockOrder.dto.StockPurchaseRequest;
import org.mockInvestment.stockOrder.dto.NewStockOrderRequest;
import org.mockInvestment.stockOrder.service.StockOrderService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
Expand All @@ -19,18 +20,30 @@ public StockOrderController(StockOrderService stockOrderService) {
this.stockOrderService = stockOrderService;
}

@PostMapping("/stocks/{code}/purchase")
public ResponseEntity<Void> requestStockPurchase(@Login AuthInfo authInfo, @PathVariable("code") String stockCode,
@RequestBody StockPurchaseRequest request) {
stockOrderService.requestStockPurchase(authInfo, stockCode, request);
// System.out.println(authInfo.getId() + " " + stockCode + " " + request.bidPrice());
@PostMapping("/stocks/{code}/order")
public ResponseEntity<Void> createStockOrder(@Login AuthInfo authInfo, @PathVariable("code") String stockCode,
@RequestBody NewStockOrderRequest request) {
stockOrderService.createStockOrder(authInfo, stockCode, request);
return ResponseEntity.status(HttpStatus.CREATED).build();
}

@DeleteMapping("/stocks/purchase")
public ResponseEntity<Void> cancelStockPurchase(@Login AuthInfo authInfo, @RequestBody StockPurchaseCancelRequest request) {
@DeleteMapping("/stocks/order")
public ResponseEntity<Void> cancelStockStockOrder(@Login AuthInfo authInfo, @RequestBody StockPurchaseCancelRequest request) {
stockOrderService.cancelStockPurchase(authInfo, request);
return ResponseEntity.noContent().build();
}

@GetMapping("/stock-orders/histories/me")
public ResponseEntity<StockOrderHistoriesResponse> findMyStockOrderHistories(@Login AuthInfo authInfo) {
StockOrderHistoriesResponse response = stockOrderService.findStockOrderHistories(authInfo);
return ResponseEntity.ok(response);
}

@GetMapping("/stock-orders/histories")
public ResponseEntity<StockOrderHistoriesResponse> findStockOrderHistoriesByCode(@Login AuthInfo authInfo,
@RequestParam("code") String stockCode) {
StockOrderHistoriesResponse response = stockOrderService.findStockOrderHistoriesByCode(authInfo, stockCode);
return ResponseEntity.ok(response);
}

}
Loading

0 comments on commit b7c1d9f

Please sign in to comment.