diff --git a/backend/src/main/java/kakaobootcamp/backend/common/properties/KisProperties.java b/backend/src/main/java/kakaobootcamp/backend/common/properties/KisProperties.java index fc37d05..40215e8 100644 --- a/backend/src/main/java/kakaobootcamp/backend/common/properties/KisProperties.java +++ b/backend/src/main/java/kakaobootcamp/backend/common/properties/KisProperties.java @@ -11,10 +11,28 @@ public class KisProperties { private String url; - private String orderTrId; - private String sellTrId; - private String getBalanceTrId; - private String getBalanceRealizedProfitAndLossTrId; - private String getPriceTrId; - private String findDomesticStockPriceChartId; + private Domestic domestic; + private International international; + + @Getter + @Setter + public static class Domestic { + private String buyTrId; + private String sellTrId; + private String findBalanceTrId; + private String findBalanceRealizedProfitAndLossTrId; + private String findPriceTrId; + private String findDomesticStockPriceChartId; + } + + @Getter + @Setter + public static class International { + private String buyTrId; + private String sellTrId; + private String findBalanceTrId; + private String findBalanceRealizedProfitAndLossTrId; + private String findPriceTrId; + private String findDomesticStockPriceChartId; + } } diff --git a/backend/src/main/java/kakaobootcamp/backend/common/scheduling/SchedulingService.java b/backend/src/main/java/kakaobootcamp/backend/common/scheduling/SchedulingService.java index acd6d44..a892fd0 100644 --- a/backend/src/main/java/kakaobootcamp/backend/common/scheduling/SchedulingService.java +++ b/backend/src/main/java/kakaobootcamp/backend/common/scheduling/SchedulingService.java @@ -5,7 +5,7 @@ import org.springframework.transaction.annotation.Transactional; import kakaobootcamp.backend.domains.autoTrade.AutoTradeService; -import kakaobootcamp.backend.domains.stock.service.StockService; +import kakaobootcamp.backend.domains.stock.service.DomesticStockService; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor @@ -13,7 +13,7 @@ public class SchedulingService { private final AutoTradeService autoTradeService; - private final StockService stockService; + private final DomesticStockService domesticStockService; @Async @Scheduled(cron = "0 0 11 * * MON-FRI") @@ -24,6 +24,6 @@ public void doAutoTrade() { @Async @Scheduled(cron = "0 0 20 * * MON-SAT") public void updateSuggestedKeywords() { - stockService.updateDomesticStocks(); + domesticStockService.updateDomesticStocks(); } } diff --git a/backend/src/main/java/kakaobootcamp/backend/domains/autoTrade/AutoTradeService.java b/backend/src/main/java/kakaobootcamp/backend/domains/autoTrade/AutoTradeService.java index 40cb8e6..9e04b2d 100644 --- a/backend/src/main/java/kakaobootcamp/backend/domains/autoTrade/AutoTradeService.java +++ b/backend/src/main/java/kakaobootcamp/backend/domains/autoTrade/AutoTradeService.java @@ -15,10 +15,10 @@ import kakaobootcamp.backend.domains.member.MemberService; import kakaobootcamp.backend.domains.member.domain.AutoTradeState; import kakaobootcamp.backend.domains.member.domain.Member; -import kakaobootcamp.backend.domains.stock.dto.StockDTO.GetStockBalanceResponse; -import kakaobootcamp.backend.domains.stock.dto.StockDTO.GetStockBalanceResponse.Output1; -import kakaobootcamp.backend.domains.stock.dto.StockDTO.OrderStockRequest; -import kakaobootcamp.backend.domains.stock.service.StockService; +import kakaobootcamp.backend.domains.stock.dto.DomesticStockDTO.FindStockBalanceResponse; +import kakaobootcamp.backend.domains.stock.dto.DomesticStockDTO.FindStockBalanceResponse.Output1; +import kakaobootcamp.backend.domains.stock.dto.DomesticStockDTO.OrderStockRequest; +import kakaobootcamp.backend.domains.stock.service.DomesticStockService; import kakaobootcamp.backend.domains.transaction.TransactionService; import kakaobootcamp.backend.domains.transaction.domain.Transaction; import lombok.RequiredArgsConstructor; @@ -29,7 +29,7 @@ public class AutoTradeService { private final AiServerService aiServerService; - private final StockService stockService; + private final DomesticStockService domesticStockService; private final TransactionService transactionService; private final MemberService memberService; private final BrokerService brokerService; @@ -52,7 +52,7 @@ private void executeAutoTradeForTransaction(Transaction transaction) { } // 현재 보유 주식량 및 예수금 조회 - GetStockBalanceResponse stockBalanceResponse = stockService.getStockBalance(member, "", ""); + FindStockBalanceResponse stockBalanceResponse = domesticStockService.findStockBalance(member, "", ""); // 거래 가능 금액 계산 int availableBalance = calculateAvailableBalance( @@ -127,9 +127,9 @@ private void placeOrder(Member member, String productNumber, int quantity, boole .build(); if (isBuy) { - stockService.orderStock(member, request); + domesticStockService.orderStock(member, request, true); } else { - stockService.sellStock(member, request); + domesticStockService.orderStock(member, request, false); } } } diff --git a/backend/src/main/java/kakaobootcamp/backend/domains/stock/controller/StockController.java b/backend/src/main/java/kakaobootcamp/backend/domains/stock/controller/DomesticStockController.java similarity index 80% rename from backend/src/main/java/kakaobootcamp/backend/domains/stock/controller/StockController.java rename to backend/src/main/java/kakaobootcamp/backend/domains/stock/controller/DomesticStockController.java index 93658bb..e2cd4ce 100644 --- a/backend/src/main/java/kakaobootcamp/backend/domains/stock/controller/StockController.java +++ b/backend/src/main/java/kakaobootcamp/backend/domains/stock/controller/DomesticStockController.java @@ -25,31 +25,31 @@ import kakaobootcamp.backend.common.dto.ErrorResponse; import kakaobootcamp.backend.common.util.memberLoader.MemberLoader; import kakaobootcamp.backend.domains.member.domain.Member; -import kakaobootcamp.backend.domains.stock.dto.StockDTO.FindDomesticStockPopularChartResponse; -import kakaobootcamp.backend.domains.stock.dto.StockDTO.FindDomesticStockPriceChartResponse; -import kakaobootcamp.backend.domains.stock.dto.StockDTO.FindSuggestedKeywordResponse; -import kakaobootcamp.backend.domains.stock.dto.StockDTO.GetStockBalanceRealizedProfitAndLossResponse; -import kakaobootcamp.backend.domains.stock.dto.StockDTO.GetStockBalanceResponse; -import kakaobootcamp.backend.domains.stock.dto.StockDTO.GetStockPriceResponse; -import kakaobootcamp.backend.domains.stock.dto.StockDTO.OrderStockRequest; -import kakaobootcamp.backend.domains.stock.service.StockService; +import kakaobootcamp.backend.domains.stock.dto.DomesticStockDTO.FindDomesticStockPopularChartResponse; +import kakaobootcamp.backend.domains.stock.dto.DomesticStockDTO.FindStockBalanceRealizedProfitAndLossResponse; +import kakaobootcamp.backend.domains.stock.dto.DomesticStockDTO.FindStockBalanceResponse; +import kakaobootcamp.backend.domains.stock.dto.DomesticStockDTO.FindStockPriceChartResponse; +import kakaobootcamp.backend.domains.stock.dto.DomesticStockDTO.FindStockPriceResponse; +import kakaobootcamp.backend.domains.stock.dto.DomesticStockDTO.FindSuggestedKeywordResponse; +import kakaobootcamp.backend.domains.stock.dto.DomesticStockDTO.OrderStockRequest; +import kakaobootcamp.backend.domains.stock.service.DomesticStockService; import lombok.RequiredArgsConstructor; -@Tag(name = "STOCK API", description = "주식에 대한 API입니다.") +@Tag(name = "DOMESTIC STOCK API", description = "국내 주식에 대한 API입니다.") @RestController -@RequestMapping("/api/stocks") +@RequestMapping("/api/stocks/domestic") @RequiredArgsConstructor -public class StockController { +public class DomesticStockController { private final MemberLoader memberLoader; - private final StockService stockService; + private final DomesticStockService domesticStockService; // 삭제될 API @PostMapping("/buy") public ResponseEntity> buyStock(OrderStockRequest request) { Member member = memberLoader.getMember(); - stockService.orderStock(member, request); + domesticStockService.orderStock(member, request, true); return ResponseEntity.ok(DataResponse.ok()); } @@ -58,13 +58,13 @@ public ResponseEntity> buyStock(OrderStockRequest request) { public ResponseEntity> sellStock(OrderStockRequest request) { Member member = memberLoader.getMember(); - stockService.sellStock(member, request); + domesticStockService.orderStock(member, request, false); return ResponseEntity.ok(DataResponse.ok()); } @GetMapping("/recommendations") - public ResponseEntity>> getStockRecommendations( + public ResponseEntity>> findStockRecommendations( @RequestParam("size") @Min(value = 1, message = "size는 1이상이어야 합니다.") @Max(value = 10, message = "size는 10이하이어야 합니다.") int size, @RequestParam("page") @Min(value = 0, message = "page는 0이상이어야 합니다.") int page) { Pageable pageable = PageRequest.of(page, size); @@ -117,12 +117,12 @@ public ResponseEntity>> ) } ) - public ResponseEntity> getStockBalance( + public ResponseEntity> findStockBalance( @RequestParam(name = "CTX_AREA_FK100", defaultValue = "") String fk, @RequestParam(name = "CTX_AREA_NK100", defaultValue = "") String nk) { Member member = memberLoader.getMember(); - GetStockBalanceResponse response = stockService.getStockBalance(member, fk, nk); + FindStockBalanceResponse response = domesticStockService.findStockBalance(member, fk, nk); return ResponseEntity.ok(DataResponse.from(response)); } @@ -155,11 +155,11 @@ public ResponseEntity> getStockBalance( ) } ) - public ResponseEntity> getBalanceRealizedProfitAndLoss() { + public ResponseEntity> findBalanceRealizedProfitAndLoss() { Member member = memberLoader.getMember(); - GetStockBalanceRealizedProfitAndLossResponse response = stockService - .getBalanceRealizedProfitAndLoss(member); + FindStockBalanceRealizedProfitAndLossResponse response = domesticStockService + .findBalanceRealizedProfitAndLoss(member); return ResponseEntity.ok(DataResponse.from(response)); } @@ -191,11 +191,11 @@ public ResponseEntity ) } ) - public ResponseEntity> getStockPrice( + public ResponseEntity> findStockPrice( @RequestParam("productNumber") String productNumber) { Member member = memberLoader.getMember(); - GetStockPriceResponse response = stockService.getStockPrice(member, productNumber); + FindStockPriceResponse response = domesticStockService.findStockPrice(member, productNumber); return ResponseEntity.ok(DataResponse.from(response)); } @@ -229,14 +229,14 @@ public ResponseEntity> getStockPrice( ) public ResponseEntity>> findSuggestedKeywords( @RequestParam("keyword") String keyword) { - List responses = stockService.findSuggestedKeywords(keyword); + List responses = domesticStockService.findSuggestedKeywords(keyword); return ResponseEntity.ok(DataResponse.from(responses)); } @GetMapping("/update/removed") public ResponseEntity updateRemovedStocks() { - stockService.updateDomesticStocks(); + domesticStockService.updateDomesticStocks(); return ResponseEntity.ok().build(); } @@ -269,12 +269,12 @@ public ResponseEntity updateRemovedStocks() { ) } ) - public ResponseEntity> findDomesticStockPriceChart( + public ResponseEntity> findStockPriceChart( @RequestParam("productNumber") String productNumber, @Pattern(regexp = "D|W|M|Y", message = "periodCode는 D, W, M, Y 중 하나여야 합니다.") @RequestParam("periodCode") String periodCode) { Member member = memberLoader.getMember(); - FindDomesticStockPriceChartResponse response = stockService.findDomesticStockPriceChart(member, productNumber, + FindStockPriceChartResponse response = domesticStockService.findStockPriceChart(member, productNumber, periodCode); return ResponseEntity.ok(DataResponse.from(response)); diff --git a/backend/src/main/java/kakaobootcamp/backend/domains/stock/controller/InternationalStockController.java b/backend/src/main/java/kakaobootcamp/backend/domains/stock/controller/InternationalStockController.java new file mode 100644 index 0000000..839c44a --- /dev/null +++ b/backend/src/main/java/kakaobootcamp/backend/domains/stock/controller/InternationalStockController.java @@ -0,0 +1,43 @@ +package kakaobootcamp.backend.domains.stock.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import io.swagger.v3.oas.annotations.tags.Tag; +import kakaobootcamp.backend.common.dto.DataResponse; +import kakaobootcamp.backend.common.util.memberLoader.MemberLoader; +import kakaobootcamp.backend.domains.member.domain.Member; +import kakaobootcamp.backend.domains.stock.dto.InternationalStockDTO.InternationalOrderStockRequest; +import kakaobootcamp.backend.domains.stock.service.InternationalStockService; +import lombok.RequiredArgsConstructor; + +@Tag(name = "INTERNATIONAL STOCK API", description = "해외 주식에 대한 API입니다.") +@RestController +@RequestMapping("/api/stock/international") +@RequiredArgsConstructor +public class InternationalStockController { + + private final MemberLoader memberLoader; + private final InternationalStockService internationalStockService; + + // 삭제될 API + @PostMapping("/buy") + public ResponseEntity> buyStock(InternationalOrderStockRequest request) { + Member member = memberLoader.getMember(); + + internationalStockService.orderStock(member, request, true); + + return ResponseEntity.ok(DataResponse.ok()); + } + + @PostMapping("/sell") + public ResponseEntity> sellStock(InternationalOrderStockRequest request) { + Member member = memberLoader.getMember(); + + internationalStockService.orderStock(member, request, false); + + return ResponseEntity.ok(DataResponse.ok()); + } +} diff --git a/backend/src/main/java/kakaobootcamp/backend/domains/stock/dto/StockDTO.java b/backend/src/main/java/kakaobootcamp/backend/domains/stock/dto/DomesticStockDTO.java similarity index 97% rename from backend/src/main/java/kakaobootcamp/backend/domains/stock/dto/StockDTO.java rename to backend/src/main/java/kakaobootcamp/backend/domains/stock/dto/DomesticStockDTO.java index cd74910..6ed74fd 100644 --- a/backend/src/main/java/kakaobootcamp/backend/domains/stock/dto/StockDTO.java +++ b/backend/src/main/java/kakaobootcamp/backend/domains/stock/dto/DomesticStockDTO.java @@ -14,15 +14,13 @@ import lombok.NoArgsConstructor; import lombok.Setter; -public class StockDTO { +public class DomesticStockDTO { @Getter - @Setter @NoArgsConstructor(access = AccessLevel.PRIVATE) - @Builder @AllArgsConstructor(access = AccessLevel.PRIVATE) + @Builder public static class OrderStockRequest { - @JsonProperty("PDNO") private String PDNO; // 종목코드 (6자리, 최대 12자리) @@ -36,15 +34,6 @@ public static class OrderStockRequest { private String ORD_UNPR; // 주문단가 (최대 19자리) } - @Getter - @NoArgsConstructor(access = AccessLevel.PRIVATE) - public static class KisBaseResponse { - - private String rt_cd; // 응답 코드 - private String msg_cd; // 메시지 코드 - private String msg1; // 메시지 내용 - } - @Getter @Setter @NoArgsConstructor(access = AccessLevel.PRIVATE) @@ -94,7 +83,7 @@ public static class Output { @Getter @NoArgsConstructor(access = AccessLevel.PRIVATE) - public static class GetStockBalanceResponse extends KisBaseResponse { + public static class FindStockBalanceResponse extends KisBaseResponse { private String ctx_area_fk100; // 연속조회검색조건100 private String ctx_area_nk100; // 연속조회키100 @@ -164,7 +153,7 @@ public static class Output2 { @Getter @NoArgsConstructor(access = AccessLevel.PRIVATE) - public static class GetStockBalanceRealizedProfitAndLossResponse extends KisBaseResponse { + public static class FindStockBalanceRealizedProfitAndLossResponse extends KisBaseResponse { private List output1; // 응답상세1 (Object Array) private List output2; // 응답상세2 (Object) @@ -233,7 +222,7 @@ public static class Output2 { @Getter @NoArgsConstructor(access = AccessLevel.PRIVATE) - public static class GetStockPriceResponse extends KisBaseResponse { + public static class FindStockPriceResponse extends KisBaseResponse { private Output output; // 응답상세 @@ -411,7 +400,7 @@ public static FindSuggestedKeywordResponse from(DomesticStock domesticStock) { @Getter @NoArgsConstructor(access = AccessLevel.PRIVATE) - public static class FindDomesticStockPriceChartResponse extends KisBaseResponse { + public static class FindStockPriceChartResponse extends KisBaseResponse { private Output1 output1; // 응답 상세 private List output2; // 일별 데이터 배열 diff --git a/backend/src/main/java/kakaobootcamp/backend/domains/stock/dto/InternationalStockDTO.java b/backend/src/main/java/kakaobootcamp/backend/domains/stock/dto/InternationalStockDTO.java new file mode 100644 index 0000000..93daedb --- /dev/null +++ b/backend/src/main/java/kakaobootcamp/backend/domains/stock/dto/InternationalStockDTO.java @@ -0,0 +1,67 @@ +package kakaobootcamp.backend.domains.stock.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.Getter; +import lombok.NoArgsConstructor; + +public class InternationalStockDTO { + + @Data + @AllArgsConstructor(access = AccessLevel.PRIVATE) + @Builder + public static class InternationalOrderStockRequest { + + @JsonProperty("CANO") + private String CANO; // 고객계좌번호 + + @JsonProperty("ACNT_PRDT_CD") + private String ACNT_PRDT_CD; // 계좌상품코드 + + @JsonProperty("OVRS_EXCG_CD") + private String OVRS_EXCG_CD; // 해외거래소코드 + + @JsonProperty("PDNO") + private String PDNO; // 종목코드 (6자리, 최대 12자리) + + @JsonProperty("ORD_QTY") + private String ORD_QTY; // 주문수량 (최대 10자리) + + @JsonProperty("OVRS_ORD_UNPR") + private String OVRS_ORD_UNPR; // 해외주문단가 (최대 10자리) + + @JsonProperty("SLL_TYPE") + private final String SLL_TYPE; + + @JsonProperty("ORD_SVR_DVSN_CD") + private final String ORD_SVR_DVSN_CD; + + @JsonProperty("ORD_DVSN") + private String ORD_DVSN; + } + + @Getter + @NoArgsConstructor(access = AccessLevel.PRIVATE) + public static class OrderStockResponse extends KisBaseResponse { + // 출력 데이터 + private Output output; + + @Getter + @NoArgsConstructor(access = AccessLevel.PRIVATE) + public static class Output { + + // KRX 전달 주문 원본 번호 + private String KRX_FWDG_ORD_ORGNO; + + // 주문 번호 + private String ODNO; + + // 주문 시간 + private String ORD_TMD; + } + } +} diff --git a/backend/src/main/java/kakaobootcamp/backend/domains/stock/dto/KisBaseResponse.java b/backend/src/main/java/kakaobootcamp/backend/domains/stock/dto/KisBaseResponse.java new file mode 100644 index 0000000..75e0b69 --- /dev/null +++ b/backend/src/main/java/kakaobootcamp/backend/domains/stock/dto/KisBaseResponse.java @@ -0,0 +1,11 @@ +package kakaobootcamp.backend.domains.stock.dto; + +import lombok.Getter; + +@Getter +public class KisBaseResponse { + + private String rt_cd; // 응답 코드 + private String msg_cd; // 메시지 코드 + private String msg1; // 메시지 내용 +} diff --git a/backend/src/main/java/kakaobootcamp/backend/domains/stock/service/StockService.java b/backend/src/main/java/kakaobootcamp/backend/domains/stock/service/DomesticStockService.java similarity index 79% rename from backend/src/main/java/kakaobootcamp/backend/domains/stock/service/StockService.java rename to backend/src/main/java/kakaobootcamp/backend/domains/stock/service/DomesticStockService.java index bf5f28d..b2508a7 100644 --- a/backend/src/main/java/kakaobootcamp/backend/domains/stock/service/StockService.java +++ b/backend/src/main/java/kakaobootcamp/backend/domains/stock/service/DomesticStockService.java @@ -1,6 +1,7 @@ package kakaobootcamp.backend.domains.stock.service; import static kakaobootcamp.backend.common.exception.ErrorCode.*; +import static kakaobootcamp.backend.domains.stock.dto.DomesticStockDTO.*; import java.time.DayOfWeek; import java.time.LocalDate; @@ -25,17 +26,8 @@ import kakaobootcamp.backend.domains.member.MemberService; import kakaobootcamp.backend.domains.member.domain.Member; import kakaobootcamp.backend.domains.stock.domain.DomesticStock; -import kakaobootcamp.backend.domains.stock.dto.StockDTO.FindDomesticStockPriceChartResponse; -import kakaobootcamp.backend.domains.stock.dto.StockDTO.FindSuggestedKeywordResponse; -import kakaobootcamp.backend.domains.stock.dto.StockDTO.GetStockBalanceRealizedProfitAndLossResponse; -import kakaobootcamp.backend.domains.stock.dto.StockDTO.GetStockBalanceResponse; -import kakaobootcamp.backend.domains.stock.dto.StockDTO.GetStockPriceResponse; -import kakaobootcamp.backend.domains.stock.dto.StockDTO.GetSuggestedKeywordsDTO; -import kakaobootcamp.backend.domains.stock.dto.StockDTO.GetSuggestedKeywordsDTO.Response.Body.Items.Item; -import kakaobootcamp.backend.domains.stock.dto.StockDTO.KisBaseResponse; -import kakaobootcamp.backend.domains.stock.dto.StockDTO.KisOrderStockRequest; -import kakaobootcamp.backend.domains.stock.dto.StockDTO.OrderStockRequest; -import kakaobootcamp.backend.domains.stock.dto.StockDTO.OrderStockResponse; +import kakaobootcamp.backend.domains.stock.dto.DomesticStockDTO.GetSuggestedKeywordsDTO.Response.Body.Items.Item; +import kakaobootcamp.backend.domains.stock.dto.KisBaseResponse; import kakaobootcamp.backend.domains.stock.repository.DomesticStockRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -43,7 +35,7 @@ @Slf4j @Service @RequiredArgsConstructor -public class StockService { +public class DomesticStockService { private final KisProperties kisProperties; private final KisAccessTokenService kisAccessTokenService; @@ -55,8 +47,8 @@ public class StockService { // 헤더 설정 private Map makeHeaders(Member member, String trId) { - KisAccessToken kisAccessToken = kisAccessTokenService.findKisAccessToken(member.getId()). - orElseThrow(() -> ApiException.from(KIS_ACCESS_TOKEN_NOT_FOUND)); + KisAccessToken kisAccessToken = kisAccessTokenService.findKisAccessToken(member.getId()) + .orElseThrow(() -> ApiException.from(KIS_ACCESS_TOKEN_NOT_FOUND)); String accessToken = kisAccessToken.getAccessToken(); Map headers = new HashMap<>(); @@ -75,37 +67,26 @@ private void checkResponse(KisBaseResponse response) { } } - // 주식 사기 - public void orderStock(Member member, OrderStockRequest request) { - String uri = "/uapi/domestic-stock/v1/trading/order-cash"; - - // 헤더 설정 - Map headers = makeHeaders(member, kisProperties.getOrderTrId()); - - // 본문 설정 - KisOrderStockRequest kisOrderStockRequest = makeKisOrderStockRequestFromOrderStockRequestAndMember(member, - request); - - OrderStockResponse response = webClientUtil.postFromKis( - headers, - uri, - kisOrderStockRequest, - OrderStockResponse.class); - - checkResponse(response); + // TrId 조회 + private String getTrId(boolean isBuy) { + if (isBuy) { + return kisProperties.getDomestic().getBuyTrId(); + } else { + return kisProperties.getDomestic().getSellTrId(); + } } - // 주식 팔기 - public void sellStock(Member member, OrderStockRequest request) { + // 주식 매매 + public void orderStock(Member member, OrderStockRequest request, boolean isBuy) { String uri = "/uapi/domestic-stock/v1/trading/order-cash"; + String trId = getTrId(isBuy); // 헤더 설정 - Map headers = makeHeaders(member, kisProperties.getSellTrId()); + Map headers = makeHeaders(member, trId); // 본문 설정 KisOrderStockRequest kisOrderStockRequest = makeKisOrderStockRequestFromOrderStockRequestAndMember(member, - request - ); + request); OrderStockResponse response = webClientUtil.postFromKis( headers, @@ -130,11 +111,11 @@ private KisOrderStockRequest makeKisOrderStockRequestFromOrderStockRequestAndMem } // 주식 잔고 조회 - public GetStockBalanceResponse getStockBalance(Member member, String fk, String nk) { + public FindStockBalanceResponse findStockBalance(Member member, String fk, String nk) { String uri = "/uapi/domestic-stock/v1/trading/inquire-balance"; // 헤더 설정 - Map headers = makeHeaders(member, kisProperties.getGetBalanceTrId()); + Map headers = makeHeaders(member, kisProperties.getDomestic().getFindBalanceTrId()); // 파라미터 설정 MultiValueMap params = new LinkedMultiValueMap<>(); @@ -150,11 +131,11 @@ public GetStockBalanceResponse getStockBalance(Member member, String fk, String params.add("CTX_AREA_FK100", fk); // 연속 조회 검색 조건 100 (최초 조회 시 공란) params.add("CTX_AREA_NK100", nk); // 연속 조회 키 100 (최초 조회 시 공란) - GetStockBalanceResponse response = webClientUtil.getFromKis( + FindStockBalanceResponse response = webClientUtil.getFromKis( headers, uri, params, - GetStockBalanceResponse.class); + FindStockBalanceResponse.class); checkResponse(response); @@ -162,11 +143,12 @@ public GetStockBalanceResponse getStockBalance(Member member, String fk, String } // 주식잔고조회_실현손익 - public GetStockBalanceRealizedProfitAndLossResponse getBalanceRealizedProfitAndLoss(Member member) { + public FindStockBalanceRealizedProfitAndLossResponse findBalanceRealizedProfitAndLoss(Member member) { String uri = "/uapi/domestic-stock/v1/trading/inquire-balance-rlz-pl"; // 헤더 설정 - Map headers = makeHeaders(member, kisProperties.getGetBalanceRealizedProfitAndLossTrId()); + Map headers = makeHeaders(member, + kisProperties.getDomestic().getFindBalanceRealizedProfitAndLossTrId()); headers.put("custtype", "P"); // 고객타입(P: 개인, B: 기업) // 파라미터 설정 @@ -184,11 +166,11 @@ public GetStockBalanceRealizedProfitAndLossResponse getBalanceRealizedProfitAndL params.add("CTX_AREA_FK100", ""); // 연속조회검색조건100 (최초 조회시 공란) params.add("CTX_AREA_NK100", ""); // 연속조회키100 (최초 조회시 공란) - GetStockBalanceRealizedProfitAndLossResponse response = webClientUtil.getFromKis( + FindStockBalanceRealizedProfitAndLossResponse response = webClientUtil.getFromKis( headers, uri, params, - GetStockBalanceRealizedProfitAndLossResponse.class); + FindStockBalanceRealizedProfitAndLossResponse.class); checkResponse(response); @@ -196,22 +178,22 @@ public GetStockBalanceRealizedProfitAndLossResponse getBalanceRealizedProfitAndL } // 주식 현재가 조회 - public GetStockPriceResponse getStockPrice(Member member, String productNumber) { + public FindStockPriceResponse findStockPrice(Member member, String productNumber) { String uri = "/uapi/domestic-stock/v1/quotations/inquire-price"; // 헤더 설정 - Map headers = makeHeaders(member, kisProperties.getGetPriceTrId()); + Map headers = makeHeaders(member, kisProperties.getDomestic().getFindPriceTrId()); // 파라미터 설정 MultiValueMap params = new LinkedMultiValueMap<>(); params.add("FID_COND_MRKT_DIV_CODE", "J"); // 'J': 주식, ETF, ETN / 'W': ELW params.add("FID_INPUT_ISCD", productNumber); // FID 입력 종목코드: 6자리 종목번호 또는 ETN의 경우 'Q'로 시작하는 코드 - GetStockPriceResponse response = webClientUtil.getFromKis( + FindStockPriceResponse response = webClientUtil.getFromKis( headers, uri, params, - GetStockPriceResponse.class); + FindStockPriceResponse.class); checkResponse(response); @@ -312,7 +294,7 @@ private DomesticStock findDomesticStockByProductNumber(String productNumber) { } // 국내 주식 기간별 시세 조회 - public FindDomesticStockPriceChartResponse findDomesticStockPriceChart(Member member, String productNumber, + public FindStockPriceChartResponse findStockPriceChart(Member member, String productNumber, String periodCode) { String uri = "/uapi/domestic-stock/v1/quotations/inquire-daily-itemchartprice"; LocalDate today = LocalDate.now(); @@ -320,7 +302,8 @@ public FindDomesticStockPriceChartResponse findDomesticStockPriceChart(Member me String endDate = makeDateToString(today); // 헤더 설정 - Map headers = makeHeaders(member, kisProperties.getFindDomesticStockPriceChartId()); + Map headers = makeHeaders(member, + kisProperties.getDomestic().getFindDomesticStockPriceChartId()); // 파라미터 설정 MultiValueMap params = new LinkedMultiValueMap<>(); @@ -331,11 +314,11 @@ public FindDomesticStockPriceChartResponse findDomesticStockPriceChart(Member me params.add("FID_PERIOD_DIV_CODE", periodCode); params.add("FID_ORG_ADJ_PRC", "1"); - FindDomesticStockPriceChartResponse response = webClientUtil.getFromKis( + FindStockPriceChartResponse response = webClientUtil.getFromKis( headers, uri, params, - FindDomesticStockPriceChartResponse.class); + FindStockPriceChartResponse.class); checkResponse(response); diff --git a/backend/src/main/java/kakaobootcamp/backend/domains/stock/service/InternationalStockService.java b/backend/src/main/java/kakaobootcamp/backend/domains/stock/service/InternationalStockService.java new file mode 100644 index 0000000..d8b1684 --- /dev/null +++ b/backend/src/main/java/kakaobootcamp/backend/domains/stock/service/InternationalStockService.java @@ -0,0 +1,80 @@ +package kakaobootcamp.backend.domains.stock.service; + +import static kakaobootcamp.backend.common.exception.ErrorCode.*; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; + +import kakaobootcamp.backend.common.exception.ApiException; +import kakaobootcamp.backend.common.properties.KisProperties; +import kakaobootcamp.backend.common.util.webClient.WebClientUtil; +import kakaobootcamp.backend.domains.broker.KisAccessToken; +import kakaobootcamp.backend.domains.broker.service.KisAccessTokenService; +import kakaobootcamp.backend.domains.member.MemberService; +import kakaobootcamp.backend.domains.member.domain.Member; +import kakaobootcamp.backend.domains.stock.dto.InternationalStockDTO.InternationalOrderStockRequest; +import kakaobootcamp.backend.domains.stock.dto.InternationalStockDTO.OrderStockResponse; +import kakaobootcamp.backend.domains.stock.dto.KisBaseResponse; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class InternationalStockService { + + private final KisProperties kisProperties; + private final KisAccessTokenService kisAccessTokenService; + private final MemberService memberService; + private final WebClientUtil webClientUtil; + + // 헤더 설정 + private Map makeHeaders(Member member, String trId) { + KisAccessToken kisAccessToken = kisAccessTokenService.findKisAccessToken(member.getId()) + .orElseThrow(() -> ApiException.from(KIS_ACCESS_TOKEN_NOT_FOUND)); + String accessToken = kisAccessToken.getAccessToken(); + + Map headers = new HashMap<>(); + headers.put("authorization", accessToken); + headers.put("appkey", memberService.getDecryptedAppKey(member)); + headers.put("appsecret", memberService.getDecryptedSecretKey(member)); + headers.put("tr_id", trId); + + return headers; + } + + // 응답 확인 + private void checkResponse(KisBaseResponse response) { + if (!response.getRt_cd().equals("0")) { + throw ApiException.of(HttpStatus.BAD_REQUEST, response.getMsg1()); + } + } + + // TrId 조회 + private String getTrId(boolean isBuy) { + if (isBuy) { + return kisProperties.getInternational().getBuyTrId(); + } else { + return kisProperties.getInternational().getSellTrId(); + } + } + + // 주식 사기 + public void orderStock(Member member, InternationalOrderStockRequest request, boolean isBuy) { + String uri = "/uapi/overseas-stock/v1/trading/order"; + String trId = getTrId(isBuy); + + // 헤더 설정 + Map headers = makeHeaders(member, trId); + + // 본문 설정 + OrderStockResponse response = webClientUtil.postFromKis( + headers, + uri, + request, + OrderStockResponse.class); + + checkResponse(response); + } +}