diff --git a/build.gradle b/build.gradle index f06e334..77b4b5f 100644 --- a/build.gradle +++ b/build.gradle @@ -27,11 +27,13 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-webflux' compileOnly 'org.projectlombok:lombok' developmentOnly 'org.springframework.boot:spring-boot-devtools' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + runtimeOnly 'com.h2database:h2' } tasks.named('test') { diff --git a/src/main/java/trademill/apiserver/TrademillApiApplication.java b/src/main/java/trademill/apiserver/TrademillApiApplication.java index d5944c0..5374618 100644 --- a/src/main/java/trademill/apiserver/TrademillApiApplication.java +++ b/src/main/java/trademill/apiserver/TrademillApiApplication.java @@ -2,7 +2,11 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import trademill.apiserver.config.KisProperties; + +@EnableConfigurationProperties(KisProperties.class) @SpringBootApplication public class TrademillApiApplication { diff --git a/src/main/java/trademill/apiserver/broker/BrokerGateway.java b/src/main/java/trademill/apiserver/broker/BrokerGateway.java new file mode 100644 index 0000000..3d0ab10 --- /dev/null +++ b/src/main/java/trademill/apiserver/broker/BrokerGateway.java @@ -0,0 +1,12 @@ +package trademill.apiserver.broker; + +import java.math.BigDecimal; + + +// BrokerGateway : '증권사와 어떻게 대화할지' 계약서(포트) 역할 +public interface BrokerGateway { + String placeMarketBuy(String symbol, BigDecimal quantity); + String placeMarketSell(String symbol, BigDecimal quantity); + String placeLimitBuy(String symbol, BigDecimal quantity, BigDecimal price); + String placeLimitSell(String symbol, BigDecimal quantity, BigDecimal price); +} diff --git a/src/main/java/trademill/apiserver/broker/FakeBrokerGateway.java b/src/main/java/trademill/apiserver/broker/FakeBrokerGateway.java new file mode 100644 index 0000000..d175cc4 --- /dev/null +++ b/src/main/java/trademill/apiserver/broker/FakeBrokerGateway.java @@ -0,0 +1,20 @@ +package trademill.apiserver.broker; + +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +import java.math.BigDecimal; +import java.util.UUID; + +@Profile({"local","default"}) +@Component + +// FakeBrokerGateway : 실제 증권사 호출 대신 로컬 개발/테스트용으로 응답을 꾸며주는 구현체(어댑터) +public class FakeBrokerGateway implements BrokerGateway { + private String id(){ return "SIM-" + UUID.randomUUID(); } + + public String placeMarketBuy(String s, BigDecimal q){ return id(); } + public String placeMarketSell(String s, BigDecimal q){ return id(); } + public String placeLimitBuy(String s, BigDecimal q, BigDecimal p){ return id(); } + public String placeLimitSell(String s, BigDecimal q, BigDecimal p){ return id(); } +} diff --git a/src/main/java/trademill/apiserver/broker/kis/KisBrokerGateway.java b/src/main/java/trademill/apiserver/broker/kis/KisBrokerGateway.java new file mode 100644 index 0000000..76aed4b --- /dev/null +++ b/src/main/java/trademill/apiserver/broker/kis/KisBrokerGateway.java @@ -0,0 +1,136 @@ +package trademill.apiserver.broker.kis; + +import lombok.RequiredArgsConstructor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Profile; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.client.WebClient; +import trademill.apiserver.broker.BrokerGateway; +import trademill.apiserver.config.KisProperties; +import trademill.apiserver.order.OrderSide; +import trademill.apiserver.broker.kis.dto.KisResponse; +import trademill.apiserver.broker.kis.dto.OrderCashOutput; + +import java.math.BigDecimal; +import java.util.LinkedHashMap; +import java.util.Map; + +@Component +@Profile("kis") +@RequiredArgsConstructor +public class KisBrokerGateway implements BrokerGateway { + + private static final Logger log = LoggerFactory.getLogger(KisBrokerGateway.class); + + private final WebClient kisWebClient; + private final KisTokenService tokenService; + private final KisProperties props; + + @Override + public String placeMarketBuy(String symbol, BigDecimal quantity) { + return place(OrderSide.BUY, "MARKET", symbol, quantity, null); + } + + @Override + public String placeLimitBuy(String symbol, BigDecimal quantity, BigDecimal price) { + return place(OrderSide.BUY, "LIMIT", symbol, quantity, price); + } + + @Override + public String placeMarketSell(String symbol, BigDecimal quantity) { + return place(OrderSide.SELL, "MARKET", symbol, quantity, null); + } + + @Override + public String placeLimitSell(String symbol, BigDecimal quantity, BigDecimal price) { + return place(OrderSide.SELL, "LIMIT", symbol, quantity, price); + } + + private String place(OrderSide side, String orderType, String symbol, BigDecimal qty, BigDecimal price) { + log.info("[KIS:{} {}] symbol={}, qty={}, price={}", side, orderType, symbol, qty, price); + + // 0) 토큰 + String token = tokenService.getAccessToken(); + + // 1) KIS 주문 바디 구성 + // ORD_DVSN: 00=지정가, 01=시장가 + final String ordDvsn = "LIMIT".equals(orderType) ? "00" : "01"; + final String ordUnpr = "MARKET".equals(orderType) ? "0" : price.toPlainString(); + + Map body = new LinkedHashMap<>(); + body.put("CANO", props.getCano()); // 계좌번호(앞 8자리) + body.put("ACNT_PRDT_CD", props.getAcntPrdtCd()); // 상품코드(보통 01) + body.put("PDNO", symbol); // 종목코드 + body.put("ORD_DVSN", ordDvsn); + body.put("ORD_QTY", qty.toPlainString()); + body.put("ORD_UNPR", ordUnpr); + + // 2) 해시키 요청 + String hashKey = requestHashKey(token, body); + + // 3) 주문 요청 + String trId = mapTrId(side, props.isVirtual()); // KIS는 현금주문 TR이 매수/매도로 구분됨 + KisResponse resp = kisWebClient.post() + .uri("/uapi/domestic-stock/v1/trading/order-cash") + .contentType(MediaType.APPLICATION_JSON) + .headers(h -> { + h.add("authorization", "Bearer " + token); + h.add("appkey", props.getAppKey()); + h.add("appsecret", props.getAppSecret()); + h.add("tr_id", trId); + h.add("custtype", "P"); // 개인 + h.add("hashkey", hashKey); + }) + .bodyValue(body) + .retrieve() + .bodyToMono(ParameterizedTypes.orderCashResponse()) + .block(); + + if (resp == null) throw new IllegalStateException("KIS response is null"); + if (!"0".equals(resp.getRt_cd())) { + throw new IllegalStateException("KIS error: " + resp.getMsg_cd() + " - " + resp.getMsg1()); + } + + String ordNo = resp.getOutput() != null ? resp.getOutput().getORD_NO() : null; + log.info("[KIS] rt_cd=0, ORD_NO={}", ordNo); + return ordNo != null ? ordNo : "KIS-NO-ORDER-NO"; + } + + private String requestHashKey(String token, Map body) { + Map resp = kisWebClient.post() + .uri("/uapi/hashkey") + .contentType(MediaType.APPLICATION_JSON) + .headers(h -> { + h.add("authorization", "Bearer " + token); + h.add("appkey", props.getAppKey()); + h.add("appsecret", props.getAppSecret()); + }) + .bodyValue(body) + .retrieve() + .bodyToMono(Map.class) + .block(); + + Object hk = resp != null ? resp.get("HASH") : null; + if (hk == null) throw new IllegalStateException("Failed to get hashkey from KIS"); + return hk.toString(); + } + + /** KIS 현금주문 TR 매핑 (모의/실전, 매수/매도) */ + private String mapTrId(OrderSide side, boolean virtual) { + boolean buy = side == OrderSide.BUY; + if (virtual) { + return buy ? "VTTC0802U" : "VTTC0801U"; // 모의: 매수/매도 + } else { + return buy ? "TTTC0802U" : "TTTC0801U"; // 실전: 매수/매도 + } + } + + /** WebClient 제네릭 응답 타입 헬퍼 */ + static class ParameterizedTypes { + static org.springframework.core.ParameterizedTypeReference> orderCashResponse() { + return new org.springframework.core.ParameterizedTypeReference<>() {}; + } + } +} diff --git a/src/main/java/trademill/apiserver/broker/kis/KisTokenService.java b/src/main/java/trademill/apiserver/broker/kis/KisTokenService.java new file mode 100644 index 0000000..01bc600 --- /dev/null +++ b/src/main/java/trademill/apiserver/broker/kis/KisTokenService.java @@ -0,0 +1,50 @@ +package trademill.apiserver.broker.kis; + +import lombok.RequiredArgsConstructor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Profile; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClient; +import trademill.apiserver.config.KisProperties; + +import java.time.Instant; +import java.util.Map; + +@Service +@Profile("kis") +@RequiredArgsConstructor +public class KisTokenService { + private static final Logger log = LoggerFactory.getLogger(KisTokenService.class); + + private final WebClient kisWebClient; + private final KisProperties props; + + private volatile String cachedToken; + private volatile Instant expiresAt = Instant.EPOCH; + + public String getAccessToken() { + if (cachedToken != null && Instant.now().isBefore(expiresAt.minusSeconds(30))) { + return cachedToken; + } + Map resp = kisWebClient.post() + .uri("/oauth2/tokenP") + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(Map.of( + "grant_type", "client_credentials", + "appkey", props.getAppKey(), + "appsecret", props.getAppSecret() + )) + .retrieve() + .bodyToMono(Map.class) + .block(); + + String token = (String) resp.get("access_token"); + Number exp = (Number) resp.getOrDefault("expires_in", 0); + this.cachedToken = token; + this.expiresAt = Instant.now().plusSeconds(exp.longValue()); + log.info("[KIS] access_token issued, expires_in={}s", exp); + return token; + } +} diff --git a/src/main/java/trademill/apiserver/broker/kis/dto/KisResponse.java b/src/main/java/trademill/apiserver/broker/kis/dto/KisResponse.java new file mode 100644 index 0000000..af86c78 --- /dev/null +++ b/src/main/java/trademill/apiserver/broker/kis/dto/KisResponse.java @@ -0,0 +1,11 @@ +package trademill.apiserver.broker.kis.dto; + +import lombok.Data; + +@Data +public class KisResponse { + private String rt_cd; // "0" 성공, "1" 실패 + private String msg_cd; + private String msg1; + private T output; +} diff --git a/src/main/java/trademill/apiserver/broker/kis/dto/OrderCashOutput.java b/src/main/java/trademill/apiserver/broker/kis/dto/OrderCashOutput.java new file mode 100644 index 0000000..9e467ec --- /dev/null +++ b/src/main/java/trademill/apiserver/broker/kis/dto/OrderCashOutput.java @@ -0,0 +1,9 @@ +package trademill.apiserver.broker.kis.dto; + +import lombok.Data; + +@Data +public class OrderCashOutput { + // KIS 주문응답의 주문번호 필드 (문서 기준) + private String ORD_NO; +} diff --git a/src/main/java/trademill/apiserver/config/KisConfig.java b/src/main/java/trademill/apiserver/config/KisConfig.java new file mode 100644 index 0000000..c82ffe5 --- /dev/null +++ b/src/main/java/trademill/apiserver/config/KisConfig.java @@ -0,0 +1,27 @@ +package trademill.apiserver.config; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.http.MediaType; +import org.springframework.web.reactive.function.client.ExchangeStrategies; +import org.springframework.web.reactive.function.client.WebClient; + +@Configuration +@EnableConfigurationProperties(KisProperties.class) +@Profile("kis") +public class KisConfig { + + @Bean + public WebClient kisWebClient(KisProperties props) { + return WebClient.builder() + .baseUrl(props.getBaseUrl()) + .defaultHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE + "; charset=UTF-8") + .defaultHeader("Accept", MediaType.APPLICATION_JSON_VALUE) + .exchangeStrategies(ExchangeStrategies.builder() + .codecs(c -> c.defaultCodecs().maxInMemorySize(4 * 1024 * 1024)) + .build()) + .build(); + } +} diff --git a/src/main/java/trademill/apiserver/config/KisProperties.java b/src/main/java/trademill/apiserver/config/KisProperties.java new file mode 100644 index 0000000..f73da18 --- /dev/null +++ b/src/main/java/trademill/apiserver/config/KisProperties.java @@ -0,0 +1,31 @@ +package trademill.apiserver.config; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@Getter @Setter +@ConfigurationProperties(prefix = "kis") +public class KisProperties { + private String baseUrl; + private String appKey; + private String appSecret; + private String accountNo; // 예: 50151652-01 + + public String getCano() { + if (accountNo == null) return null; + int idx = accountNo.indexOf('-'); + return idx > 0 ? accountNo.substring(0, idx) : accountNo; + } + + public String getAcntPrdtCd() { + if (accountNo == null) return null; + int idx = accountNo.indexOf('-'); + return idx > 0 ? accountNo.substring(idx + 1) : "01"; + } + + public boolean isVirtual() { + // 모의투자 여부 (base-url 에 vts 포함 시 true) + return baseUrl != null && baseUrl.contains("openapivts"); + } +} diff --git a/src/main/java/trademill/apiserver/demo/domain/Demo.java b/src/main/java/trademill/apiserver/demo/domain/Demo.java index dd64d16..298085a 100644 --- a/src/main/java/trademill/apiserver/demo/domain/Demo.java +++ b/src/main/java/trademill/apiserver/demo/domain/Demo.java @@ -18,7 +18,7 @@ public class Demo { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column(nullable = false) + @Column(name = "val") private String value; } diff --git a/src/main/java/trademill/apiserver/order/Order.java b/src/main/java/trademill/apiserver/order/Order.java new file mode 100644 index 0000000..3397efd --- /dev/null +++ b/src/main/java/trademill/apiserver/order/Order.java @@ -0,0 +1,42 @@ +package trademill.apiserver.order; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.math.BigDecimal; +import java.time.OffsetDateTime; + +@Entity +@Table(name = "orders") +@Getter @Setter @NoArgsConstructor +public class Order { + + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private Long userId; + private String symbol; + + @Enumerated(EnumType.STRING) private OrderSide side; + @Enumerated(EnumType.STRING) private OrderType orderType; + + private BigDecimal quantity; + private BigDecimal price; // MARKET이면 null 가능 + + @Enumerated(EnumType.STRING) private OrderStatus status; + private String brokerOrderId; + + private OffsetDateTime createdAt; + private OffsetDateTime updatedAt; + + @PrePersist + void onCreate() { + createdAt = OffsetDateTime.now(); + updatedAt = createdAt; + if (status == null) status = OrderStatus.PENDING; + } + @PreUpdate + void onUpdate() { updatedAt = OffsetDateTime.now(); } +} diff --git a/src/main/java/trademill/apiserver/order/OrderController.java b/src/main/java/trademill/apiserver/order/OrderController.java new file mode 100644 index 0000000..19c157a --- /dev/null +++ b/src/main/java/trademill/apiserver/order/OrderController.java @@ -0,0 +1,33 @@ +package trademill.apiserver.order; + +import jakarta.validation.Valid; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.web.bind.annotation.*; +import trademill.apiserver.order.dto.*; + +@RestController +@RequestMapping("/api/v1/orders") +public class OrderController { + private final OrderService service; + public OrderController(OrderService service){ this.service = service; } + + @PostMapping("/buy") + public OrderResponse buy(@Valid @RequestBody PlaceOrderRequest req){ + return OrderResponse.from(service.placeBuy(req)); + } + + @PostMapping("/sell") + public OrderResponse sell(@Valid @RequestBody PlaceOrderRequest req){ + return OrderResponse.from(service.placeSell(req)); + } + + @GetMapping + public Page list(@RequestParam Long userId, + @RequestParam(defaultValue="0") int page, + @RequestParam(defaultValue="20") int size){ + return service.list(userId, PageRequest.of(page, size, Sort.by("id").descending())) + .map(OrderResponse::from); + } +} diff --git a/src/main/java/trademill/apiserver/order/OrderRepository.java b/src/main/java/trademill/apiserver/order/OrderRepository.java new file mode 100644 index 0000000..4b1967e --- /dev/null +++ b/src/main/java/trademill/apiserver/order/OrderRepository.java @@ -0,0 +1,8 @@ +package trademill.apiserver.order; + +import org.springframework.data.domain.*; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface OrderRepository extends JpaRepository { + Page findByUserId(Long userId, Pageable pageable); +} diff --git a/src/main/java/trademill/apiserver/order/OrderService.java b/src/main/java/trademill/apiserver/order/OrderService.java new file mode 100644 index 0000000..4811958 --- /dev/null +++ b/src/main/java/trademill/apiserver/order/OrderService.java @@ -0,0 +1,56 @@ +package trademill.apiserver.order; + +import org.springframework.data.domain.*; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import trademill.apiserver.broker.BrokerGateway; +import trademill.apiserver.order.dto.PlaceOrderRequest; + +@Service +public class OrderService { + private final OrderRepository orders; + private final BrokerGateway broker; + + public OrderService(OrderRepository orders, BrokerGateway broker) { + this.orders = orders; + this.broker = broker; + } + + @Transactional + public Order placeBuy(PlaceOrderRequest req){ + Order o = baseOf(req, OrderSide.BUY); + String id = switch (req.getOrderType()){ + case MARKET -> broker.placeMarketBuy(req.getSymbol(), req.getQuantity()); + case LIMIT -> broker.placeLimitBuy(req.getSymbol(), req.getQuantity(), req.getPrice()); + }; + o.setBrokerOrderId(id); o.setStatus(OrderStatus.ACCEPTED); + return orders.save(o); + } + + @Transactional + public Order placeSell(PlaceOrderRequest req){ + Order o = baseOf(req, OrderSide.SELL); + String id = switch (req.getOrderType()){ + case MARKET -> broker.placeMarketSell(req.getSymbol(), req.getQuantity()); + case LIMIT -> broker.placeLimitSell(req.getSymbol(), req.getQuantity(), req.getPrice()); + }; + o.setBrokerOrderId(id); o.setStatus(OrderStatus.ACCEPTED); + return orders.save(o); + } + + public Page list(Long userId, Pageable pageable){ + return orders.findByUserId(userId, pageable); + } + + private Order baseOf(PlaceOrderRequest req, OrderSide side){ + Order o = new Order(); + o.setUserId(req.getUserId()); + o.setSymbol(req.getSymbol()); + o.setSide(side); + o.setOrderType(req.getOrderType()); + o.setQuantity(req.getQuantity()); + o.setPrice(req.getPrice()); + o.setStatus(OrderStatus.PENDING); + return o; + } +} diff --git a/src/main/java/trademill/apiserver/order/OrderSide.java b/src/main/java/trademill/apiserver/order/OrderSide.java new file mode 100644 index 0000000..ade2750 --- /dev/null +++ b/src/main/java/trademill/apiserver/order/OrderSide.java @@ -0,0 +1,3 @@ +package trademill.apiserver.order; + +public enum OrderSide {BUY, SELL} diff --git a/src/main/java/trademill/apiserver/order/OrderStatus.java b/src/main/java/trademill/apiserver/order/OrderStatus.java new file mode 100644 index 0000000..46b4257 --- /dev/null +++ b/src/main/java/trademill/apiserver/order/OrderStatus.java @@ -0,0 +1,3 @@ +package trademill.apiserver.order; + +public enum OrderStatus {PENDING, ACCEPTED, PARTIALLY_FILLED, FILLED, REJECTED, CANCELED} diff --git a/src/main/java/trademill/apiserver/order/OrderType.java b/src/main/java/trademill/apiserver/order/OrderType.java new file mode 100644 index 0000000..dd950b8 --- /dev/null +++ b/src/main/java/trademill/apiserver/order/OrderType.java @@ -0,0 +1,3 @@ +package trademill.apiserver.order; + +public enum OrderType {MARKET, LIMIT} diff --git a/src/main/java/trademill/apiserver/order/dto/OrderResponse.java b/src/main/java/trademill/apiserver/order/dto/OrderResponse.java new file mode 100644 index 0000000..7fc0d51 --- /dev/null +++ b/src/main/java/trademill/apiserver/order/dto/OrderResponse.java @@ -0,0 +1,38 @@ +package trademill.apiserver.order.dto; + +import lombok.Data; +import trademill.apiserver.order.*; + +import java.math.BigDecimal; +import java.time.OffsetDateTime; + +@Data +public class OrderResponse { + private Long id; + private Long userId; + private String symbol; + private OrderSide side; + private OrderType orderType; + private BigDecimal quantity; + private BigDecimal price; + private OrderStatus status; + private String brokerOrderId; + private OffsetDateTime createdAt; + private OffsetDateTime updatedAt; + + public static OrderResponse from(Order o) { + OrderResponse r = new OrderResponse(); + r.setId(o.getId()); + r.setUserId(o.getUserId()); + r.setSymbol(o.getSymbol()); + r.setSide(o.getSide()); + r.setOrderType(o.getOrderType()); + r.setQuantity(o.getQuantity()); + r.setPrice(o.getPrice()); + r.setStatus(o.getStatus()); + r.setBrokerOrderId(o.getBrokerOrderId()); + r.setCreatedAt(o.getCreatedAt()); + r.setUpdatedAt(o.getUpdatedAt()); + return r; + } +} diff --git a/src/main/java/trademill/apiserver/order/dto/PlaceOrderRequest.java b/src/main/java/trademill/apiserver/order/dto/PlaceOrderRequest.java new file mode 100644 index 0000000..91b47c5 --- /dev/null +++ b/src/main/java/trademill/apiserver/order/dto/PlaceOrderRequest.java @@ -0,0 +1,17 @@ +package trademill.apiserver.order.dto; + +import jakarta.validation.constraints.*; +import lombok.Getter; +import lombok.Setter; +import trademill.apiserver.order.OrderType; + +import java.math.BigDecimal; + +@Getter @Setter +public class PlaceOrderRequest { + @NotNull private Long userId; + @NotBlank private String symbol; + @NotNull private OrderType orderType; // MARKET or LIMIT + @NotNull @DecimalMin("0.00000001") private BigDecimal quantity; + @DecimalMin("0.0") private BigDecimal price; // LIMIT일 때만 필요 +} diff --git a/src/main/resources/application-kis.yml b/src/main/resources/application-kis.yml new file mode 100644 index 0000000..61b8bd7 --- /dev/null +++ b/src/main/resources/application-kis.yml @@ -0,0 +1,25 @@ +kis: + base-url: ${KIS_BASE_URL} + app-key: ${KIS_APP_KEY} + app-secret: ${KIS_APP_SECRET} + account-no: ${KIS_ACCOUNT_NO} + +spring: + datasource: + driver-class-name: org.h2.Driver + url: jdbc:h2:mem:tm;MODE=PostgreSQL;DB_CLOSE_DELAY=-1;DATABASE_TO_UPPER=false + username: sa + password: + jpa: + hibernate: + ddl-auto: update + properties: + hibernate.jdbc.time_zone: UTC + +logging: + level: + org.springframework.web.reactive.function.client.ExchangeFunctions: DEBUG + reactor.netty.http.client: INFO + +server: + port: 8080