Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
f2836b4
feat: KIS api를 사용하여 주식 종목의 시가, 종가를 조회해서 DB에 저장하는 기능 구현.
DwKwCs May 18, 2026
e452346
feat: kis 엑세스 토큰을 redis에 캐싱 기능 추가.
DwKwCs May 20, 2026
7da4bab
chore: TTL 0초 케이스 예외처리 추가.
DwKwCs May 20, 2026
a6cb008
chore: 누락 커밋.
DwKwCs May 20, 2026
004c11a
feat: krx에서 다운한 코스피 200 엑셀 파일을 S3에 업로드하는 기능, S3에 업로드된 코스피 200 엑셀 파일을 …
DwKwCs May 21, 2026
0179dba
feat: 코스피 200 종목 업로드 및 저장 기능 수정. 전제 종목 pagenation으로 조회 api 추가.
DwKwCs May 21, 2026
b8ba56e
chore: 오타 수정.
DwKwCs May 21, 2026
d189128
Merge branch 'develop' into feat/#19-KIS-api
DwKwCs Jun 1, 2026
ee01d2d
chore: KisController 삭제, importStockFromS3 description 수정.
DwKwCs May 27, 2026
65a431d
refactor: util 관련 함수 리펙터링.
DwKwCs Jun 1, 2026
6d4b3b8
refactor: 코스피 200 엑셀 파일 S3 업로드 uri 방식 수정.
DwKwCs Jun 3, 2026
072f200
refactor: 코스피 200 종목을 업로드 할 때, 필요한 케이스(신규 추가, 삭제, 업데이트)를 추가하여 리팩토링.
DwKwCs Jun 3, 2026
6aab2a4
refactor: 한국투자증권 api를 사용해 종목의 시가, 종가를 조회하는 기능을 WebClient에서 FeignClien…
DwKwCs Jun 3, 2026
c3b9cbe
chore: 자잘한 수정. 한국투자증권 종목 조회 실패 시, 다음 날짜로 진행 수정.
DwKwCs Jun 3, 2026
470c590
chore: 어노테이션 정리. 필요없는 DTO 정리.
DwKwCs Jun 4, 2026
3093e60
refactor: stock 관련 예외처리 handler 리펙터.
DwKwCs Jun 4, 2026
88a3ebb
chore: 예외 처리. 주석 삭제.
DwKwCs Jun 8, 2026
4afb2ad
Merge branch 'develop' into feat/#19-KIS-api
DwKwCs Jun 22, 2026
7478371
Merge pull request #23 from TREAT-st/feat/#19-KIS-api
DwKwCs Jul 5, 2026
3e56ebc
feat: prediction 도메인 엔티티 및 채점 기반 구조 추가
Seona12 Jul 12, 2026
d7f860f
feat: prediction 도메인 서비스(CQRS) 구현
Seona12 Jul 12, 2026
d3864eb
feat: prediction API DTO 및 Converter 구현
Seona12 Jul 12, 2026
fc63665
feat: prediction API UseCase 및 Controller 구현
Seona12 Jul 12, 2026
38afb41
feat : 포인트 규칙 및 스케줄러 관련 api 추가
Seona12 Aug 5, 2026
642e1e8
fix : get user api 디자인에 맞게 수정
Seona12 Aug 5, 2026
5d552f7
feat : 예측 결과 actualRateText(실제 등락률 기반 텍스트) 필드 추가
Seona12 Aug 5, 2026
3ec69b6
트리거 확인
Seona12 Aug 24, 2026
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
2 changes: 1 addition & 1 deletion .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@
- 어떤 작업을 했는지 요약해서 적어주세요.
- ex) Spring Boot Security 설정 변경
# 📸 결과 캡쳐화면
- 실행한 결과를 캡쳐하여 올려주세요.
- 실행한 결과를 캡쳐하여 올려주세요. / 확인
16 changes: 16 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ repositories {
mavenCentral()
}

dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:2025.0.0"
}
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
Expand All @@ -38,6 +44,16 @@ dependencies {
// Redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'

// FeignClient
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'

// Excel
implementation 'org.apache.poi:poi-ooxml:5.5.1'

// AWS S3
implementation 'software.amazon.awssdk:s3:2.25.26'
implementation 'software.amazon.awssdk:url-connection-client:2.25.26'

runtimeOnly 'com.mysql:mysql-connector-j'

compileOnly 'org.projectlombok:lombok'
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/com/example/demo/DemoApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableFeignClients
@EnableScheduling
public class DemoApplication {

public static void main(String[] args) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,5 @@
@Builder
public class FavoriteStockRequestDto {
private String stockCode;
private String ticker;
private Integer days;
private String stockName;
Comment on lines 8 to +10

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

FavoriteStock.days 생성 계약을 복구하세요.

days 입력 제거와 변환기 매핑 제거로 인해 새 FavoriteStock의 null 불가 days 값이 항상 누락됩니다. 관심 종목 생성 요청은 저장 시 실패합니다.

  • src/main/java/com/example/demo/api/favoriteStock/dto/FavoriteStockRequestDto.java#L8-L10: days를 요청 계약에 유지하거나, 해당 필드를 제거하는 도메인 변경을 완료하세요.
  • src/main/java/com/example/demo/api/favoriteStock/mapper/FavoriteStockConverter.java#L13-L19: days를 엔티티에 설정하거나, days 제거 후의 새 영속 모델에 맞게 변환기를 변경하세요.
📍 Affects 2 files
  • src/main/java/com/example/demo/api/favoriteStock/dto/FavoriteStockRequestDto.java#L8-L10 (this comment)
  • src/main/java/com/example/demo/api/favoriteStock/mapper/FavoriteStockConverter.java#L13-L19
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/example/demo/api/favoriteStock/dto/FavoriteStockRequestDto.java`
around lines 8 - 10, Restore the FavoriteStock.days creation contract: in
src/main/java/com/example/demo/api/favoriteStock/dto/FavoriteStockRequestDto.java
lines 8-10, retain the days request field, and in
src/main/java/com/example/demo/api/favoriteStock/mapper/FavoriteStockConverter.java
lines 13-19, map it onto the new FavoriteStock entity so its non-null days value
is populated; alternatively, complete the domain and persistence model change to
remove days consistently from both sites.

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public static class FavoriteStockDto {
private Long favoriteStockId;
private Long userId;
private String stockCode;
private String ticker;
private String stockName;
private Boolean isAlertEnabled;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ public static FavoriteStock toFavoriteStock(User user, FavoriteStockRequestDto r
return FavoriteStock.builder()
.user(user)
.stockCode(request.getStockCode())
.ticker(request.getTicker())
.days(request.getDays())
.stockName(request.getStockName())
.isAlertEnabled(false)
.build();
}
Expand All @@ -25,7 +24,7 @@ public static FavoriteStockDto toFavoriteStockDto(FavoriteStock favoriteStock) {
.favoriteStockId(favoriteStock.getId())
.userId(favoriteStock.getUser().getId())
.stockCode(favoriteStock.getStockCode())
.ticker(favoriteStock.getTicker())
.stockName(favoriteStock.getStockName())
.isAlertEnabled(favoriteStock.getIsAlertEnabled())
.build();
}
Expand Down
32 changes: 32 additions & 0 deletions src/main/java/com/example/demo/api/kis/client/KisFeignClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.example.demo.api.kis.client;

import com.example.demo.api.kis.dto.KisResponseDto;
import com.example.demo.api.kis.dto.KisTokenRequestDto;
import com.example.demo.api.kis.dto.KisTokenResponseDto;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestParam;

@FeignClient(name = "kisClient", url = "${kis.base-url}")
public interface KisFeignClient {

@PostMapping("/oauth2/tokenP")
KisTokenResponseDto issueToken(@RequestBody KisTokenRequestDto request);

@GetMapping("/uapi/domestic-stock/v1/quotations/inquire-daily-itemchartprice")
KisResponseDto getDailyStockPrice(
@RequestHeader("authorization") String authorization,
@RequestHeader("appkey") String appKey,
@RequestHeader("appsecret") String appSecret,
@RequestHeader("tr_id") String trId,
@RequestParam("FID_COND_MRKT_DIV_CODE") String fidCondMrktDivCode,
@RequestParam("FID_INPUT_ISCD") String fidInputIscd,
@RequestParam("FID_INPUT_DATE_1") String fidInputDate1,
@RequestParam("FID_INPUT_DATE_2") String fidInputDate2,
@RequestParam("FID_PERIOD_DIV_CODE") String fidPeriodDivCode,
@RequestParam("FID_ORG_ADJ_PRC") String fidOrgAdjPrc
);
}
34 changes: 34 additions & 0 deletions src/main/java/com/example/demo/api/kis/dto/KisResponseDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.example.demo.api.kis.dto;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.util.List;

@Getter
@NoArgsConstructor
public class KisResponseDto {

@JsonProperty("rt_cd")
private String rtCd;

@JsonProperty("msg1")
private String msg1;

@JsonProperty("output2")
private List<DailyData> output2;

@Getter
@NoArgsConstructor
public static class DailyData {
@JsonProperty("stck_bsop_date")
private String date;

@JsonProperty("stck_oprc")
private String openPrice;

@JsonProperty("stck_clpr")
private String closePrice;
}
}
19 changes: 19 additions & 0 deletions src/main/java/com/example/demo/api/kis/dto/KisTokenRequestDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.example.demo.api.kis.dto;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class KisTokenRequestDto {

@JsonProperty("grant_type")
private String grantType;

@JsonProperty("appkey")
private String appKey;

@JsonProperty("appsecret")
private String appSecret;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.example.demo.api.kis.dto;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class KisTokenResponseDto {

@JsonProperty("access_token")
private String accessToken;

@JsonProperty("token_type")
private String tokenType;

@JsonProperty("expires_in")
private Long expiresIn;

@JsonProperty("access_token_token_expired")
private String accessTokenTokenExpired;
}
57 changes: 57 additions & 0 deletions src/main/java/com/example/demo/api/kis/service/KisService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.example.demo.api.kis.service;

import com.example.demo.api.kis.client.KisFeignClient;
import com.example.demo.api.kis.dto.KisResponseDto;
import com.example.demo.domain.stock.exception.StockHandler;
import feign.FeignException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.util.Objects;

@Slf4j
@Service
@RequiredArgsConstructor
public class KisService {

private final KisFeignClient kisFeignClient;
private final KisTokenService kisTokenService;

@Value("${kis.app-key}")
private String appKey;

@Value("${kis.app-secret}")
private String appSecret;

public KisResponseDto getDailyStockPrice(String stockCode, String startDate, String endDate) {
String token = kisTokenService.getAccessToken();

try {
KisResponseDto response = kisFeignClient.getDailyStockPrice(
"Bearer " + token,
appKey,
appSecret,
"FHKST03010100",
"J",
stockCode,
startDate,
endDate,
"D",
"0"
);

if (response == null || !"0".equals(response.getRtCd())) {
log.error("KIS API 응답 오류. rt_cd={}, msg={}", Objects.requireNonNull(response).getRtCd(), response.getMsg1());
throw StockHandler.kisApiError();
Comment on lines +45 to +47

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

null 응답을 로깅할 때 NullPointerException을 발생시키지 마세요.

response == null이면 이 분기에 들어갑니다. 그러나 Objects.requireNonNull(response)가 먼저 예외를 발생시킵니다. null 안전 로그 값을 사용한 뒤 StockHandler.kisApiError()를 던지세요.

수정 예시
- log.error("KIS API 응답 오류. rt_cd={}, msg={}", Objects.requireNonNull(response).getRtCd(), response.getMsg1());
+ log.error("KIS API 응답 오류. rt_cd={}, msg={}",
+         response == null ? null : response.getRtCd(),
+         response == null ? null : response.getMsg1());
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (response == null || !"0".equals(response.getRtCd())) {
log.error("KIS API 응답 오류. rt_cd={}, msg={}", Objects.requireNonNull(response).getRtCd(), response.getMsg1());
throw StockHandler.kisApiError();
if (response == null || !"0".equals(response.getRtCd())) {
log.error("KIS API 응답 오류. rt_cd={}, msg={}",
response == null ? null : response.getRtCd(),
response == null ? null : response.getMsg1());
throw StockHandler.kisApiError();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/com/example/demo/api/kis/service/KisService.java` around lines
45 - 47, KisService의 응답 오류 분기에서 response가 null이어도 로그 전에 예외가 발생하지 않도록 수정하세요.
`Objects.requireNonNull(response)` 사용을 제거하고 null 안전한 rt_cd 및 msg1 값을 로깅한 뒤 기존처럼
`StockHandler.kisApiError()`를 던지세요.

}

return response;

} catch (FeignException e) {
log.error("KIS API 호출 실패. status={}, stockCode={}", e.status(), stockCode);
throw StockHandler.kisApiError();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package com.example.demo.api.kis.service;

import com.example.demo.api.kis.client.KisFeignClient;
import com.example.demo.api.kis.dto.KisTokenRequestDto;
import com.example.demo.api.kis.dto.KisTokenResponseDto;
import com.example.demo.common.service.RedisService;
import com.example.demo.common.util.RedisUtil;
import com.example.demo.domain.stock.exception.StockHandler;
import feign.FeignException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.time.Duration;

import static com.example.demo.common.consts.StaticVariable.GRANT_TYPE;
import static com.example.demo.common.consts.StaticVariable.TOKEN_KEY;

@Slf4j
@Service
@RequiredArgsConstructor
public class KisTokenService {

private final KisFeignClient kisFeignClient;
private final RedisService redisService;
private final RedisUtil redisUtil;

@Value("${kis.app-key}")
private String appKey;

@Value("${kis.app-secret}")
private String appSecret;

public String getAccessToken() {
log.info("[KIS 토큰] 1. Redis 캐시 조회 시작. key={}", TOKEN_KEY);
String cached = redisService.getValue(TOKEN_KEY);

if (cached != null && !cached.isBlank()) {
log.info("[KIS 토큰] 2. 캐시 히트 - 저장된 토큰 반환");
return cached;
}
log.info("[KIS 토큰] 2. 캐시 미스 - 신규 발급 시작");

log.info("[KIS 토큰] 3. KIS API 토큰 발급 요청. appKey={}", appKey);
try {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

토큰 발급 메서드를 private 메서드로 따로 두는게 어떨까요?

KisTokenResponseDto tokenResponse = kisFeignClient.issueToken(
new KisTokenRequestDto(GRANT_TYPE, appKey, appSecret)
);
log.info("[KIS 토큰] 4. KIS API 응답 수신. response={}", tokenResponse);

if (tokenResponse == null || tokenResponse.getAccessToken() == null
|| tokenResponse.getAccessToken().isBlank()) {
log.error("[KIS 토큰] 5. 응답 토큰 값 없음 (null 또는 blank)");
throw StockHandler.kisApiError();
}
log.info("[KIS 토큰] 5. 응답 토큰 정상 확인. expiredAt={}", tokenResponse.getAccessTokenTokenExpired());

String token = tokenResponse.getAccessToken();

log.info("[KIS 토큰] 6. TTL 계산 시작. expiredAt={}", tokenResponse.getAccessTokenTokenExpired());
Duration ttl = redisUtil.calculateTtl(tokenResponse.getAccessTokenTokenExpired());
log.info("[KIS 토큰] 7. TTL 계산 완료. ttl={}s", ttl.getSeconds());

log.info("[KIS 토큰] 8. Redis 저장 시작. key={}, ttl={}s", TOKEN_KEY, ttl.getSeconds());
redisService.setKisTokenExpiresValueWithTtl(TOKEN_KEY, token, ttl);
log.info("[KIS 토큰] 9. Redis 저장 완료");

return token;

} catch (FeignException e) {
log.error("[KIS 토큰] FeignException 발생. status={}, message={}", e.status(), e.getMessage());
throw StockHandler.kisApiError();
}
}
}
Loading