From 6fbadd42a8b155f61842b8de8f43e9223f6989ca Mon Sep 17 00:00:00 2001 From: YongHwan Kim Date: Sun, 22 Sep 2024 15:43:01 +0900 Subject: [PATCH] Release 0.0.10 (#475) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [feat] 종목 현재가 갱신 시스템 구현 (#462) * feat: Kis 웹소켓 서비스 구현 * feat: 웹소켓 접근키 저장소 구현 * test: 웹소켓 접근키 저장소의 테스트 추가 * feat: tr-id 프로퍼티 추가 * fix: 웹소켓 접속 문제 해결 * feat: approvalKey에 대한 null 처리 추가 * feat: 정적 팩토리 추가 * test: 테스트 검증문 수정 * test: KisWebSocketClient connect 테스트 추가 * test: onClose 테스트 추가 * test: add sendMessage test * feat: oauth SecurityFilterChain 순서 변경 - 변경 이유 : 테스트용 SecurityFilterChain이 순서상 앞에 두기 위해서 * test: 테스트용 웹소켓 서버 설정 추가 * feat: 웹소켓 클라이언트에 실시간 종목 체결가 핸들링 메서드 구현 * test: 웹소켓으로 실시간 체결가를 조회하여 레디스에 저장하는 테스트 구현 * style: 투두 추가 및 메인 애플리케이션 이름 변경 * test: 테스트 서포트 클래스 support 패키지로 이동 * feat: StockPrice push 서비스 구현 * feat: WebClientConfig global 패키지로 이동 * fix: api로 요청하는 방식이 아닌 price 모듈에서 가져오는 방식으로 변경 * feat: StockPriceWebSocketClient 구현 * feat: connect 예외 처리 * rename: 패키지명 변경 * style: 코드 정리 * feat: 웹소켓 현재가 조회 API 구현 * feat: StockPriceDispatcher 구현 * feat: add log * test: solve test fail * feat: StockPriceWebSocket 스케줄러 구현 - 오전 8시30분에 웹소켓 재연결 - 오후 16시에 웹소켓 연결 해제 * refactor: extract method * feat: add stream filter * style: 코드 정리 * style: 코드 정리 * test: 테스트 추가 * test: 테스트 검증 수정 * feat: 포트폴리오 캐시 기능 추가 * feat: 캐시 TTL 설정 추가 * feat: 포트폴리오 종목 서비스에 캐시 로직 추가 * test: 캐시 관련 검증문 추가 * test: mock 설정 추가 * feat: 스케줄러 삭제 * feat: cors 프로파일별 설정 클래스 추가 (#464) * fix: test 프로파일 설정값 추가 및 클래스명 변경 * feat: appkey, secretkey 변경 (#466) * [feat] 로깅 추가 (#468) * feat: appkey, secretkey 변경 * feat: 로깅 추가 * feat: ssl 갱신 * feat: ssl release 추가 * [feat] 더미 데이터 생성 기능 구현 (#471) * feat: local db username root로 변경 - bulk insert 수행시 권한 문제로 인하여 로컬 실행시 root 권한으로 실행 * feat: dummy csv 파일 생성 클래스 구현 * fix: MemberRole csv 파일 생성 메서드 삭제 * fix: 회원 샘플수 조정 * feat: 조건문 추가 * fix: 조건문 제거 * feat: 퐁 데이터 송신 구현 (#473) * feat: 종목 현재가 갱신 스케줄러 추가 웹소켓을 이용한 서버가 완성될때까지 임시로 추가 --- .gitignore | 1 + docker-compose.yml | 2 +- secret | 2 +- .../kis/scheduler/KisProductionScheduler.java | 33 ++++++ .../client/StockPriceWebSocketClient.java | 4 +- .../client/StockPriceWebSocketHandler.java | 17 +++ .../StockPriceWebSocketScheduler.java | 3 +- src/main/resources/application-local.yml | 3 +- .../fineants/data/DummyDataCsvGenerator.java | 100 ++++++++++++++++++ 9 files changed, 158 insertions(+), 7 deletions(-) create mode 100644 src/main/java/co/fineants/api/domain/kis/scheduler/KisProductionScheduler.java create mode 100644 src/test/java/co/fineants/data/DummyDataCsvGenerator.java diff --git a/.gitignore b/.gitignore index 02a04b5a2..02e29e5c4 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ /src/main/resources/secret/ /src/main/generated/ /htmlReport/ +/src/main/resources/db/ diff --git a/docker-compose.yml b/docker-compose.yml index b11bd0a5c..a5ad5138a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,7 +13,7 @@ services: DB_HOST: fineAnts_db DB_PORT: 3306 DB_DATABASE: fineAnts - DB_USERNAME: admin + DB_USERNAME: root DB_PASSWORD: password1234! REDIS_HOST: fineAnts_redis REDOS_PORT: 6379 diff --git a/secret b/secret index af5c3d6cc..5f1a0af80 160000 --- a/secret +++ b/secret @@ -1 +1 @@ -Subproject commit af5c3d6cc6a5357b81ab3e1dae5e6c6decaf420f +Subproject commit 5f1a0af80421ff02a9f26516cf973fbc0345037e diff --git a/src/main/java/co/fineants/api/domain/kis/scheduler/KisProductionScheduler.java b/src/main/java/co/fineants/api/domain/kis/scheduler/KisProductionScheduler.java new file mode 100644 index 000000000..b402b7503 --- /dev/null +++ b/src/main/java/co/fineants/api/domain/kis/scheduler/KisProductionScheduler.java @@ -0,0 +1,33 @@ +package co.fineants.api.domain.kis.scheduler; + +import java.time.LocalDate; + +import org.springframework.context.annotation.Profile; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import co.fineants.api.domain.kis.repository.HolidayRepository; +import co.fineants.api.domain.kis.service.KisService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Profile(value = "production") +@Slf4j +@RequiredArgsConstructor +@Service +public class KisProductionScheduler { + + private final HolidayRepository holidayRepository; + private final KisService kisService; + + @Scheduled(cron = "0/5 * 9-15 ? * MON,TUE,WED,THU,FRI") + @Transactional + public void refreshCurrentPrice() { + // 휴장일인 경우 실행하지 않음 + if (holidayRepository.isHoliday(LocalDate.now())) { + return; + } + kisService.refreshAllStockCurrentPrice(); + } +} diff --git a/src/main/java/co/fineants/price/domain/stockprice/client/StockPriceWebSocketClient.java b/src/main/java/co/fineants/price/domain/stockprice/client/StockPriceWebSocketClient.java index 0e63458f0..3be5eaedb 100644 --- a/src/main/java/co/fineants/price/domain/stockprice/client/StockPriceWebSocketClient.java +++ b/src/main/java/co/fineants/price/domain/stockprice/client/StockPriceWebSocketClient.java @@ -127,9 +127,9 @@ private String createCurrentPriceRequest(String ticker) { return ObjectMapperUtil.serialize(requestMap); } - public void disconnect() { + public void disconnect(CloseStatus status) { try { - this.session.close(CloseStatus.NORMAL); + this.session.close(status); } catch (IOException e) { log.warn("StockPriceWebSocketClient fail close, error message is {}", e.getMessage()); } diff --git a/src/main/java/co/fineants/price/domain/stockprice/client/StockPriceWebSocketHandler.java b/src/main/java/co/fineants/price/domain/stockprice/client/StockPriceWebSocketHandler.java index 64759194d..d12072544 100644 --- a/src/main/java/co/fineants/price/domain/stockprice/client/StockPriceWebSocketHandler.java +++ b/src/main/java/co/fineants/price/domain/stockprice/client/StockPriceWebSocketHandler.java @@ -1,5 +1,7 @@ package co.fineants.price.domain.stockprice.client; +import java.io.IOException; + import org.jetbrains.annotations.NotNull; import org.springframework.stereotype.Component; import org.springframework.web.socket.CloseStatus; @@ -37,6 +39,21 @@ public void handleMessage(@NotNull WebSocketSession session, WebSocketMessage handleStockTextMessage(payload); } else { log.info("Received Message : {}", message.getPayload()); + sendPongData(session, message); + } + } + + private void sendPongData(@NotNull WebSocketSession session, WebSocketMessage message) { + // send pong data + try { + session.sendMessage(message); + } catch (IOException e) { + log.error("StockPriceWebStockClient fail send pong data, errorMessage={}", e.getMessage()); + try { + session.close(CloseStatus.SERVER_ERROR); + } catch (IOException ex) { + log.error("session can not close, errorMessage={}", ex.getMessage()); + } } } diff --git a/src/main/java/co/fineants/price/domain/stockprice/scheduler/StockPriceWebSocketScheduler.java b/src/main/java/co/fineants/price/domain/stockprice/scheduler/StockPriceWebSocketScheduler.java index 6695491b9..4f3da46f1 100644 --- a/src/main/java/co/fineants/price/domain/stockprice/scheduler/StockPriceWebSocketScheduler.java +++ b/src/main/java/co/fineants/price/domain/stockprice/scheduler/StockPriceWebSocketScheduler.java @@ -2,6 +2,7 @@ import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; +import org.springframework.web.socket.CloseStatus; import co.fineants.price.domain.stockprice.client.StockPriceWebSocketClient; import co.fineants.price.domain.stockprice.repository.StockPriceRepository; @@ -24,7 +25,7 @@ public void openWebSocketClient() { @Scheduled(cron = "0 0 16 * * MON,TUE,WED,THU,FRI") public void closeWebSocketClient() { log.info("close the StockPriceWebSocketClient"); - client.disconnect(); + client.disconnect(CloseStatus.NORMAL); repository.clear(); } } diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index 1a8361cdc..ad8790c89 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -8,7 +8,7 @@ spring: database: mysql database-platform: org.hibernate.dialect.MySQL8Dialect hibernate: - ddl-auto: create + ddl-auto: update properties: hibernate: format_sql: true @@ -18,7 +18,6 @@ spring: sql: init: mode: always - data-locations: classpath*:db/mysql/data.sql data: redis: host: ${REDIS_HOST:localhost} diff --git a/src/test/java/co/fineants/data/DummyDataCsvGenerator.java b/src/test/java/co/fineants/data/DummyDataCsvGenerator.java new file mode 100644 index 000000000..b4bff3c7c --- /dev/null +++ b/src/test/java/co/fineants/data/DummyDataCsvGenerator.java @@ -0,0 +1,100 @@ +package co.fineants.data; + +import java.io.FileWriter; +import java.io.IOException; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.csv.CSVFormat; +import org.apache.commons.csv.CSVPrinter; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class DummyDataCsvGenerator { + + public static void main(String[] args) { + DummyDataCsvGenerator dummyDataCsvGenerator = new DummyDataCsvGenerator(); + dummyDataCsvGenerator.writeMemberFile(); + dummyDataCsvGenerator.writePortfolioFile(); + } + + public void writeMemberFile() { + String fileName = "src/main/resources/db/mysql/member.csv"; + CSVFormat csvFormat = CSVFormat.Builder.create() + .setHeader("id", "email", "nickname", "provider", "password", "profileUrl", "create_at") + .setSkipHeaderRecord(false) + .build(); + List members = createMemberDummyData(); + boolean result = writeCsvFile(fileName, csvFormat, members); + if (result) { + log.info("success writing the member csv file"); + } else { + log.info("fail writing the member csv file"); + } + } + + private List createMemberDummyData() { + int recordCount = 5_000; + List result = new ArrayList<>(); + for (long i = 1; i <= recordCount; i++) { + String id = String.valueOf(i); + String email = String.format("antuser%d@gmail.com", i); + String nickname = String.format("antuser%d", i); + String provider = "local"; + String password = "$2a$10$zT6g60wI9rup2EvGbDRKa.D9N3RB5wMoFTlIGAaoZMxqX7R80pPQq"; + String profileUrl = null; + String createAt = LocalDateTime.now().toString(); + result.add(new String[] {id, email, nickname, provider, password, profileUrl, createAt}); + } + return result; + } + + public boolean writeCsvFile(String fileName, CSVFormat csvFormat, List data) { + try (FileWriter out = new FileWriter(fileName)) { + CSVPrinter printer = new CSVPrinter(out, csvFormat); + printer.printRecords(data); + } catch (IOException e) { + log.error(e.getMessage()); + return false; + } + return true; + } + + private void writePortfolioFile() { + String fileName = "src/main/resources/db/mysql/portfolio.csv"; + CSVFormat csvFormat = CSVFormat.Builder.create() + .setHeader("id", "name", "securitiesFirm", "budget", "targetGain", "maximumLoss", "targetGainIsActive", + "maximumLossIsActive", "createAt", "member_id") + .setSkipHeaderRecord(false) + .build(); + List portfolios = createPortfolioDummyData(); + boolean result = writeCsvFile(fileName, csvFormat, portfolios); + if (result) { + log.info("success writing the portfolio csv file"); + } else { + log.info("fail writing the portfolio csv file"); + } + } + + private List createPortfolioDummyData() { + int recordCount = 5_000; + List result = new ArrayList<>(); + for (long i = 1; i <= recordCount; i++) { + String id = String.valueOf(i); + String name = String.format("portfolio%d", i); + String securitiesFirm = "토스증권"; + String budget = "1000000"; + String targetGain = "1500000"; + String maximumLoss = "900000"; + String targetGainIsActive = "true"; + String maximumLossIsActive = "true"; + String createAt = LocalDateTime.now().toString(); + String memberIdString = "1"; + result.add(new String[] {id, name, securitiesFirm, budget, targetGain, maximumLoss, targetGainIsActive, + maximumLossIsActive, createAt, memberIdString}); + } + return result; + } +}