diff --git a/src/main/java/com/flab/mars/api/controller/StockController.java b/src/main/java/com/flab/mars/api/controller/StockController.java index daf474b..e1c047f 100644 --- a/src/main/java/com/flab/mars/api/controller/StockController.java +++ b/src/main/java/com/flab/mars/api/controller/StockController.java @@ -4,10 +4,11 @@ import com.flab.mars.api.dto.request.StockFluctuationRequestDto; import com.flab.mars.api.dto.response.ResultAPIDto; import com.flab.mars.api.dto.response.StockFluctuationResponseDto; +import com.flab.mars.api.dto.response.StockPriceResponseDto; import com.flab.mars.domain.service.StockService; -import com.flab.mars.domain.vo.StockPrice; import com.flab.mars.domain.vo.TokenInfo; import com.flab.mars.domain.vo.request.StockFluctuationRequestVO; +import com.flab.mars.domain.vo.response.PriceDataResponseVO; import com.flab.mars.domain.vo.response.StockFluctuationResponseVO; import jakarta.servlet.http.HttpSession; import jakarta.validation.Valid; @@ -29,8 +30,8 @@ public class StockController { /** * KIS 토큰 발급 요청, 1분당 하나 가능 - * @param request - * @param session + * @param request 한화투자증권으로부터 발급받은 appKey, appSecret 값을 전달 + * @param session 세션 * @return TokenInfo */ @PostMapping({"/accessToken"}) @@ -52,10 +53,17 @@ public ResponseEntity> getAccessToken(@RequestBody ApiCr return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(ResultAPIDto.res(HttpStatus.INTERNAL_SERVER_ERROR, "Internal Server Error")); } + /** + * 주식현재가 시세를 조회 및 db 에 저장한다. + * @param stockCode ex) 000660 + * @param session 세션 + * @return 주식현재가 , 누적 거래 대금, 누적 거래량, 주식 시가, 주식 최고가, 주식 최저가 등을 반환 + */ @GetMapping("/quotations/inquire-price") - public ResponseEntity> getStockPrice(@RequestParam(name = "stockCode") String stockCode, HttpSession session) { - StockPrice stockPrice = stockService.getStockPrice(stockCode, session); - return ResponseEntity.ok(ResultAPIDto.res(HttpStatus.OK, "Success", stockPrice)); + public ResponseEntity> getStockPrice(@RequestParam(name = "stockCode") String stockCode, HttpSession session) { + PriceDataResponseVO stockPriceResponseVO = stockService.getStockPrice(stockCode, session); + StockPriceResponseDto responseDto = StockPriceResponseDto.from(stockPriceResponseVO); + return ResponseEntity.ok(ResultAPIDto.res(HttpStatus.OK, "Success", responseDto)); } @GetMapping("/domestic-stock/ranking/fluctuation") diff --git a/src/main/java/com/flab/mars/api/dto/response/StockPriceResponseDto.java b/src/main/java/com/flab/mars/api/dto/response/StockPriceResponseDto.java new file mode 100644 index 0000000..bf525d0 --- /dev/null +++ b/src/main/java/com/flab/mars/api/dto/response/StockPriceResponseDto.java @@ -0,0 +1,51 @@ +package com.flab.mars.api.dto.response; + +import com.flab.mars.db.entity.PriceDataType; +import com.flab.mars.domain.vo.response.PriceDataResponseVO; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; +import java.time.LocalDate; + +@NoArgsConstructor +@AllArgsConstructor +@Builder +@Getter +public class StockPriceResponseDto { + + private Long id; + + private String stockId; // StockInfoEntity의 ID와 연결되는 값 + + private PriceDataType dataType; + + private BigDecimal currentPrice; // 현재가 + private BigDecimal openPrice; // 시가 + private BigDecimal closePrice; // 종가 + private BigDecimal highPrice; // 최고가 + private BigDecimal lowPrice; // 최저가 + + private BigDecimal acmlVol; // 누적 거래량 (전체 누적 거래량) + private BigDecimal acmlTrPbmn; // 누적 거래 대금 + + private LocalDate stockBusinessDate; // 주식 영업일자 + + // PriceDataResponseVO 객체를 StockPriceResponseDto로 변환하는 메서드 + public static StockPriceResponseDto from(PriceDataResponseVO responseVO) { + return StockPriceResponseDto.builder() + .id(responseVO.getId()) + .dataType(responseVO.getDataType()) + .currentPrice(responseVO.getCurrentPrice()) + .openPrice(responseVO.getOpenPrice()) + .closePrice(responseVO.getClosePrice()) + .highPrice(responseVO.getHighPrice()) + .lowPrice(responseVO.getLowPrice()) + .acmlVol(responseVO.getAcmlVol()) + .acmlTrPbmn(responseVO.getAcmlTrPbmn()) + .stockBusinessDate(responseVO.getStockBusinessDate()) + .build(); + } +} diff --git a/src/main/java/com/flab/mars/client/KISClient.java b/src/main/java/com/flab/mars/client/KISClient.java index b8d9ff0..26d2033 100644 --- a/src/main/java/com/flab/mars/client/KISClient.java +++ b/src/main/java/com/flab/mars/client/KISClient.java @@ -1,6 +1,5 @@ package com.flab.mars.client; -import com.flab.mars.domain.vo.StockPrice; import com.flab.mars.domain.vo.response.StockFluctuationResponseVO; import lombok.RequiredArgsConstructor; import org.springframework.core.ParameterizedTypeReference; @@ -49,7 +48,7 @@ public String getAccessToken(String appKey, String appSecret, String grantType) } - public StockPrice getStockPrice(String accessToken, String appKey, String appSecret, String stockCode) { + public KisPriceResponseVO getStockPrice(String accessToken, String appKey, String appSecret, String stockCode) { return webClient.get() .uri(uriBuilder -> uriBuilder.path(INQUIRE_PRICE) .queryParam("FID_COND_MRKT_DIV_CODE", "J") @@ -63,7 +62,7 @@ public StockPrice getStockPrice(String accessToken, String appKey, String appSec headers.setContentType(MediaType.APPLICATION_JSON); }) .retrieve() - .bodyToMono(StockPrice.class) + .bodyToMono(KisPriceResponseVO.class) .block(); } diff --git a/src/main/java/com/flab/mars/client/KisPriceResponseVO.java b/src/main/java/com/flab/mars/client/KisPriceResponseVO.java new file mode 100644 index 0000000..4ecba65 --- /dev/null +++ b/src/main/java/com/flab/mars/client/KisPriceResponseVO.java @@ -0,0 +1,53 @@ +package com.flab.mars.client; + + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.flab.mars.domain.vo.response.BaseResponseVO; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + + +@EqualsAndHashCode(callSuper = true) +@Data +@NoArgsConstructor +@AllArgsConstructor +public class KisPriceResponseVO extends BaseResponseVO { + @JsonProperty("output") + private Output output; + + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class Output { + @JsonProperty("stck_prpr") + private String stckPrpr; // 주식 현재가 + + @JsonProperty("prdy_vrss") + private String prdyVrss; // 전일 대비 + + @JsonProperty("prdy_vrss_sign") + private String prdyVrssSign; // 전일 대비 부호 + + @JsonProperty("prdy_ctrt") + private String prdyCtrt; // 전일 대비율 + + @JsonProperty("acml_tr_pbmn") + private String acmlTrPbmn; // 누적 거래 대금 + + @JsonProperty("acml_vol") + private String acmlVol; // 누적 거래량 + + @JsonProperty("stck_oprc") + private String stckOprc; // 주식 시가 + + @JsonProperty("stck_hgpr") + private String stckHgpr; // 주식 최고가 + + @JsonProperty("stck_lwpr") + private String stckLwpr; // 주식 최저가 + + } + +} \ No newline at end of file diff --git a/src/main/java/com/flab/mars/db/entity/PriceDataEntity.java b/src/main/java/com/flab/mars/db/entity/PriceDataEntity.java new file mode 100644 index 0000000..5b77cb4 --- /dev/null +++ b/src/main/java/com/flab/mars/db/entity/PriceDataEntity.java @@ -0,0 +1,39 @@ +package com.flab.mars.db.entity; + +import jakarta.persistence.*; +import lombok.*; + +import java.time.LocalDate; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +@Getter +@Entity +@Table(name = "price_data") +public class PriceDataEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "stock_info_id") + private StockInfoEntity stockInfoEntity; + + @Enumerated(EnumType.STRING) + private PriceDataType dataType; // 데이터 유형 (DAY, WEEK, MONTH, YEAR) + + private String currentPrice; // 현재가 (데이터 유형이 실기간인 경우 사용) + private String openPrice; // 시가 + private String closePrice; // 종가 + private String highPrice; // 최고가 + private String lowPrice; // 최저가 + + private String acmlVol; // 누적 거래량 (전체 누적 거래량) + private String acmlTrPbmn; // 누적 거래 대금 + + @Column(name = "stock_business_date") + private LocalDate stockBusinessDate; // 주식 영업일자 + + +} diff --git a/src/main/java/com/flab/mars/db/entity/PriceDataType.java b/src/main/java/com/flab/mars/db/entity/PriceDataType.java new file mode 100644 index 0000000..cce866d --- /dev/null +++ b/src/main/java/com/flab/mars/db/entity/PriceDataType.java @@ -0,0 +1,9 @@ +package com.flab.mars.db.entity; + +public enum PriceDataType { + REALTIME, // 실시간 데이터 + DAY, // 일별 데이터 + WEEK, // 주별 데이터 + MONTH, // 월별 데이터 + YEAR // 연별 데이터 +} diff --git a/src/main/java/com/flab/mars/db/entity/StockInfoEntity.java b/src/main/java/com/flab/mars/db/entity/StockInfoEntity.java new file mode 100644 index 0000000..a017198 --- /dev/null +++ b/src/main/java/com/flab/mars/db/entity/StockInfoEntity.java @@ -0,0 +1,21 @@ +package com.flab.mars.db.entity; + +import jakarta.persistence.*; +import lombok.*; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +@Getter +@Entity +@Table(name = "stock_info") +public class StockInfoEntity { + @Id + @GeneratedValue + @Column(name = "stock_info_id") + private Long id; + + private String stockCode; // 주식 코드 + private String stockName; // 주식 이름 + +} diff --git a/src/main/java/com/flab/mars/db/repository/PriceDataRepository.java b/src/main/java/com/flab/mars/db/repository/PriceDataRepository.java new file mode 100644 index 0000000..3257011 --- /dev/null +++ b/src/main/java/com/flab/mars/db/repository/PriceDataRepository.java @@ -0,0 +1,7 @@ +package com.flab.mars.db.repository; + +import com.flab.mars.db.entity.PriceDataEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface PriceDataRepository extends JpaRepository { +} diff --git a/src/main/java/com/flab/mars/db/repository/StockInfoRepository.java b/src/main/java/com/flab/mars/db/repository/StockInfoRepository.java new file mode 100644 index 0000000..85ca888 --- /dev/null +++ b/src/main/java/com/flab/mars/db/repository/StockInfoRepository.java @@ -0,0 +1,7 @@ +package com.flab.mars.db.repository; + +import com.flab.mars.db.entity.StockInfoEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface StockInfoRepository extends JpaRepository { +} diff --git a/src/main/java/com/flab/mars/domain/service/StockService.java b/src/main/java/com/flab/mars/domain/service/StockService.java index 45a9bd8..79edd1f 100644 --- a/src/main/java/com/flab/mars/domain/service/StockService.java +++ b/src/main/java/com/flab/mars/domain/service/StockService.java @@ -2,24 +2,33 @@ import com.flab.mars.client.KISClient; import com.flab.mars.client.KISConfig; -import com.flab.mars.domain.vo.StockPrice; +import com.flab.mars.client.KisPriceResponseVO; +import com.flab.mars.db.entity.PriceDataEntity; +import com.flab.mars.db.entity.PriceDataType; +import com.flab.mars.db.repository.PriceDataRepository; import com.flab.mars.domain.vo.TokenInfo; +import com.flab.mars.domain.vo.response.PriceDataResponseVO; import com.flab.mars.domain.vo.response.StockFluctuationResponseVO; import com.flab.mars.exception.AuthException; import com.flab.mars.support.SessionUtil; import jakarta.servlet.http.HttpSession; + import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor +@Transactional(readOnly = true) public class StockService { private final KISClient kisClient; private final KISConfig kisConfig; + private final PriceDataRepository priceDataRepository; + public void getAccessToken(TokenInfo tokenInfo, HttpSession session) { String accessToken = kisClient.getAccessToken(tokenInfo.getAppKey(), tokenInfo.getAppSecret(), kisConfig.getGrantType()); @@ -27,14 +36,36 @@ public void getAccessToken(TokenInfo tokenInfo, HttpSession session) { SessionUtil.setSessionAccessToKenValue(session, tokenInfo); } - public StockPrice getStockPrice(String stockCode, HttpSession session) { + // 실시간 주식 현재가 가져오기 + public PriceDataResponseVO getStockPrice(String stockCode, HttpSession session) { TokenInfo tokenInfo = SessionUtil.getSessionAccessToKenValue(session); + + // TODO userInterceptor 로 빼기 if(tokenInfo == null) { throw new AuthException("로그인에 실패했습니다. ACCESS 토큰을 가져올 수 없습니다."); } + KisPriceResponseVO stockPrice = kisClient.getStockPrice(tokenInfo.getAccessToken(), tokenInfo.getAppKey(), tokenInfo.getAppSecret(), stockCode); + + return insertCurrentStockPrice(stockPrice, stockCode); + } + + @Transactional + public PriceDataResponseVO insertCurrentStockPrice(KisPriceResponseVO stockPrice, String stockCode) { + + PriceDataEntity priceDataEntity = PriceDataEntity.builder() + .dataType(PriceDataType.REALTIME) // 실시간 + .currentPrice(stockPrice.getOutput().getStckPrpr()) // 현재가 + .openPrice(stockPrice.getOutput().getStckOprc()) // 주식 시가 + .highPrice(stockPrice.getOutput().getStckHgpr()) // 주식 최고가 + .lowPrice(stockPrice.getOutput().getStckLwpr()) // 주식 최저가 + .acmlVol(stockPrice.getOutput().getAcmlVol()) // 누적 거래량 + .acmlTrPbmn(stockPrice.getOutput().getAcmlTrPbmn()) // 누적 거래 대금 + .build(); + + PriceDataEntity savedpriceDataEntity = priceDataRepository.save(priceDataEntity); - return kisClient.getStockPrice(tokenInfo.getAccessToken(), tokenInfo.getAppKey(), tokenInfo.getAppSecret(), stockCode); + return PriceDataResponseVO.toVO(savedpriceDataEntity); } public StockFluctuationResponseVO getFluctuationRanking(String url, HttpSession session) { diff --git a/src/main/java/com/flab/mars/domain/vo/response/PriceDataResponseVO.java b/src/main/java/com/flab/mars/domain/vo/response/PriceDataResponseVO.java new file mode 100644 index 0000000..397f3ea --- /dev/null +++ b/src/main/java/com/flab/mars/domain/vo/response/PriceDataResponseVO.java @@ -0,0 +1,52 @@ +package com.flab.mars.domain.vo.response; + +import com.flab.mars.db.entity.PriceDataEntity; +import com.flab.mars.db.entity.PriceDataType; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; +import java.time.LocalDate; + +@NoArgsConstructor +@AllArgsConstructor +@Builder +@Getter +public class PriceDataResponseVO { + + private Long id; + + private String stockInfoId; // StockInfoEntity의 ID와 연결되는 값 + + private PriceDataType dataType; // 데이터 유형 (DAY, WEEK, MONTH, YEAR) + + private BigDecimal currentPrice; // 현재가 + private BigDecimal openPrice; // 시가 + private BigDecimal closePrice; // 종가 + private BigDecimal highPrice; // 최고가 + private BigDecimal lowPrice; // 최저가 + + private BigDecimal acmlVol; // 누적 거래량 (전체 누적 거래량) + private BigDecimal acmlTrPbmn; // 누적 거래 대금 + + private LocalDate stockBusinessDate; // 주식 영업일자 + + + public static PriceDataResponseVO toVO(PriceDataEntity entity) { + return PriceDataResponseVO.builder() + .id(entity.getId()) + //.stockInfoId(entity.getStockInfoEntity().getId().toString()) // TODO : StockPriceInfoEntity의 ID를 문자열로 변환 + .dataType(entity.getDataType()) + .currentPrice(new BigDecimal(entity.getCurrentPrice())) + .openPrice(new BigDecimal(entity.getOpenPrice())) + .closePrice(entity.getClosePrice() != null ? new BigDecimal(entity.getClosePrice()) : BigDecimal.ZERO) // 실시간 가격일 경우 종가 없음 + .highPrice(new BigDecimal(entity.getHighPrice())) + .lowPrice(new BigDecimal(entity.getLowPrice())) + .acmlVol(new BigDecimal(entity.getAcmlVol())) + .acmlTrPbmn(new BigDecimal(entity.getAcmlTrPbmn())) + .stockBusinessDate(entity.getStockBusinessDate()) + .build(); + } +} diff --git a/src/test/java/com/flab/mars/api/controller/StockControllerTest.java b/src/test/java/com/flab/mars/api/controller/StockControllerTest.java index f2fce3c..a43142f 100644 --- a/src/test/java/com/flab/mars/api/controller/StockControllerTest.java +++ b/src/test/java/com/flab/mars/api/controller/StockControllerTest.java @@ -2,12 +2,11 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.flab.mars.api.dto.request.ApiCredentialsRequestDto; +import com.flab.mars.api.dto.response.StockPriceResponseDto; import com.flab.mars.client.KISClient; -import com.flab.mars.client.KISConfig; import com.flab.mars.domain.service.StockService; -import com.flab.mars.domain.vo.StockPrice; import com.flab.mars.domain.vo.TokenInfo; -import com.flab.mars.domain.vo.request.StockFluctuationRequestVO; +import com.flab.mars.domain.vo.response.PriceDataResponseVO; import com.flab.mars.domain.vo.response.StockFluctuationResponseVO; import jakarta.servlet.http.HttpSession; import org.junit.jupiter.api.Test; @@ -47,12 +46,6 @@ class StockControllerTest { @MockitoBean private KISClient kisClient; - @MockitoBean - private KISConfig kisConfig; - - @MockitoBean - private StockFluctuationRequestVO stockFluctuationRequestVO; - @Test public void testGetAccessToken_Success() throws Exception { @@ -84,10 +77,10 @@ public void testGetAccessToken_Success() throws Exception { void testGetStockPrice_Success() throws Exception { // Arrange String stockCode = "12345"; - StockPrice stockPrice = new StockPrice("00", "Success", new StockPrice.Output("100000")); // 예시로 100000 설정 + PriceDataResponseVO priceDataResponseVO = new PriceDataResponseVO(); // Mock stockService.getStockPrice() 메서드 - when(stockService.getStockPrice(eq(stockCode), any(HttpSession.class))).thenReturn(stockPrice); + when(stockService.getStockPrice(eq(stockCode), any(HttpSession.class))).thenReturn(priceDataResponseVO); // Act & Assert mockMvc.perform(get("/api/stock/quotations/inquire-price")