Skip to content

Commit cf77a85

Browse files
authored
Merge pull request #204 from FunD-StockProject/fix/experiment-public-api
Fix/experiment public api
2 parents 28c73c9 + 3aec208 commit cf77a85

File tree

8 files changed

+247
-100
lines changed

8 files changed

+247
-100
lines changed

scripts/stocks_info/overseas_stock_code.py

Lines changed: 44 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -26,26 +26,52 @@
2626
def get_overseas_master_dataframe(base_dir,val):
2727

2828
ssl._create_default_https_context = ssl._create_unverified_context
29-
# urllib.request.urlretrieve(f"https://new.real.download.dws.co.kr/common/master/{val}mst.cod.zip", base_dir + f"\\{val}mst.cod.zip")
30-
urllib.request.urlretrieve(f"https://new.real.download.dws.co.kr/common/master/{val}mst.cod.zip", os.path.join(base_dir, f"{val}mst.cod.zip"))
31-
os.chdir(base_dir)
32-
33-
overseas_zip = zipfile.ZipFile(f'{val}mst.cod.zip')
34-
overseas_zip.extractall()
35-
overseas_zip.close()
36-
37-
# file_name = base_dir + f"\\{val}mst.cod"
38-
file_name = os.path.join(base_dir, f"{val}mst.cod")
39-
columns = ['National code', 'Exchange id', 'Exchange code', 'Exchange name', 'Symbol', 'realtime symbol', 'Korea name', 'English name', 'Security type(1:Index,2:Stock,3:ETP(ETF),4:Warrant)', 'currency', 'float position', 'data type', 'base price', 'Bid order size', 'Ask order size', 'market start time(HHMM)', 'market end time(HHMM)', 'DR 여부(Y/N)', 'DR 국가코드', '업종분류코드', '지수구성종목 존재 여부(0:구성종목없음,1:구성종목있음)', 'Tick size Type', '구분코드(001:ETF,002:ETN,003:ETC,004:Others,005:VIX Underlying ETF,006:VIX Underlying ETN)','Tick size type 상세']
4029

41-
print(f"Downloading...{val}mst.cod")
42-
# df = pd.read_table(base_dir+f"\\{val}mst.cod",sep='\t',encoding='cp949')
43-
df = pd.read_table(os.path.join(base_dir, f"{val}mst.cod"), sep='\t',encoding='cp949')
44-
df.columns = columns
45-
df.to_excel(f'{val}_code.xlsx',index=False) # 현재 위치에 엑셀파일로 저장
46-
30+
# ZIP 파일 경로
31+
zip_file_path = os.path.join(base_dir, f"{val}mst.cod.zip")
32+
# 압축 해제된 파일 경로
33+
cod_file_path = os.path.join(base_dir, f"{val}mst.cod")
4734

48-
return df
35+
try:
36+
# ZIP 파일 다운로드
37+
print(f"Downloading...{val}mst.cod.zip")
38+
urllib.request.urlretrieve(
39+
f"https://new.real.download.dws.co.kr/common/master/{val}mst.cod.zip",
40+
zip_file_path
41+
)
42+
43+
# ZIP 파일이 다운로드되었는지 확인
44+
if not os.path.exists(zip_file_path):
45+
raise FileNotFoundError(f"ZIP file not found after download: {zip_file_path}")
46+
47+
# ZIP 파일 압축 해제
48+
with zipfile.ZipFile(zip_file_path, 'r') as overseas_zip:
49+
overseas_zip.extractall(base_dir)
50+
51+
# 압축 해제된 파일이 존재하는지 확인
52+
if not os.path.exists(cod_file_path):
53+
raise FileNotFoundError(f"Extracted file not found: {cod_file_path}")
54+
55+
# 파일 읽기
56+
columns = ['National code', 'Exchange id', 'Exchange code', 'Exchange name', 'Symbol', 'realtime symbol', 'Korea name', 'English name', 'Security type(1:Index,2:Stock,3:ETP(ETF),4:Warrant)', 'currency', 'float position', 'data type', 'base price', 'Bid order size', 'Ask order size', 'market start time(HHMM)', 'market end time(HHMM)', 'DR 여부(Y/N)', 'DR 국가코드', '업종분류코드', '지수구성종목 존재 여부(0:구성종목없음,1:구성종목있음)', 'Tick size Type', '구분코드(001:ETF,002:ETN,003:ETC,004:Others,005:VIX Underlying ETF,006:VIX Underlying ETN)','Tick size type 상세']
57+
58+
df = pd.read_table(cod_file_path, sep='\t', encoding='cp949')
59+
df.columns = columns
60+
61+
# 엑셀 파일 저장 (선택사항)
62+
excel_path = os.path.join(base_dir, f'{val}_code.xlsx')
63+
df.to_excel(excel_path, index=False)
64+
65+
return df
66+
67+
except Exception as e:
68+
# 에러 발생 시 정리
69+
if os.path.exists(zip_file_path):
70+
try:
71+
os.remove(zip_file_path)
72+
except:
73+
pass
74+
raise e
4975

5076
# 자동 실행 코드 주석 처리 (테스트를 위해)
5177
# cmd = input("1:전부 다운로드, 2:1개의 시장을 다운로드 \n")

src/main/java/com/fund/stockProject/experiment/dto/ExperimentStatusDetailResponse.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.fund.stockProject.experiment.dto;
22

3+
import com.fund.stockProject.stock.domain.COUNTRY;
34
import java.time.LocalDateTime;
45
import java.util.List;
56
import lombok.Builder;
@@ -17,6 +18,7 @@ public class ExperimentStatusDetailResponse {
1718
private Integer buyPrice; // 매수 가격
1819
private Integer currentPrice; // 현재 가격
1920
private LocalDateTime buyAt; // 매수일
21+
private COUNTRY country; // 국가
2022

2123
@Getter
2224
public static class TradeInfo {

src/main/java/com/fund/stockProject/experiment/service/ExperimentService.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,9 @@ public ExperimentStatusDetailResponse getExperimentStatusDetail(final Integer ex
287287
// 최종 수익률 계산: 현재 가격 기준
288288
double roi = ((currentPrice - experiment.getBuyPrice()) / experiment.getBuyPrice()) * 100;
289289

290+
// country 설정
291+
final COUNTRY country = stockInfo != null ? stockInfo.getCountry() : getCountryFromExchangeNum(stock.getExchangeNum());
292+
290293
return ExperimentStatusDetailResponse.builder()
291294
.tradeInfos(new ArrayList<>())
292295
.roi(roi)
@@ -297,6 +300,7 @@ public ExperimentStatusDetailResponse getExperimentStatusDetail(final Integer ex
297300
.buyPrice(experiment.getBuyPrice().intValue())
298301
.currentPrice(currentPrice)
299302
.buyAt(experiment.getBuyAt())
303+
.country(country)
300304
.build();
301305
}
302306

@@ -322,6 +326,9 @@ public ExperimentStatusDetailResponse getExperimentStatusDetail(final Integer ex
322326
);
323327
}
324328

329+
// country 설정
330+
final COUNTRY country = stockInfo != null ? stockInfo.getCountry() : getCountryFromExchangeNum(stock.getExchangeNum());
331+
325332
return ExperimentStatusDetailResponse.builder()
326333
.tradeInfos(tradeInfos)
327334
.roi(roi)
@@ -332,6 +339,7 @@ public ExperimentStatusDetailResponse getExperimentStatusDetail(final Integer ex
332339
.buyPrice(experiment.getBuyPrice().intValue())
333340
.currentPrice(currentPrice)
334341
.buyAt(experiment.getBuyAt())
342+
.country(country)
335343
.build();
336344
}
337345

src/main/java/com/fund/stockProject/global/config/SecurityConfig.java

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -77,11 +77,7 @@ public class SecurityConfig {
7777
"/stock/{id}/info/{country}",
7878
"/stock/category/{category}/{country}",
7979
"/stock/rankings/hot",
80-
"/stock/summary/{symbol}/{country}",
81-
"/experiment/status",
82-
"/experiment/{id}/buy/{country}",
83-
"/experiment/status/{id}/detail",
84-
"/experiment/report"
80+
"/stock/summary/{symbol}/{country}"
8581
};
8682

8783
@Bean
Lines changed: 3 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,17 @@
11
package com.fund.stockProject.global.scheduler;
22

3-
import com.fund.stockProject.stock.service.StockImportService;
3+
import com.fund.stockProject.stock.service.StockMasterUpdateService;
44
import lombok.RequiredArgsConstructor;
55
import lombok.extern.slf4j.Slf4j;
66
import org.springframework.scheduling.annotation.Scheduled;
77
import org.springframework.stereotype.Component;
88

9-
import java.io.BufferedReader;
10-
import java.io.File;
11-
import java.io.InputStreamReader;
12-
139
@Slf4j
1410
@Component
1511
@RequiredArgsConstructor
1612
public class StockUpdateScheduler {
1713

18-
private final StockImportService stockImportService;
14+
private final StockMasterUpdateService stockMasterUpdateService;
1915

2016
/**
2117
* 매주 목요일 새벽 3시에 종목 마스터 데이터 업데이트
@@ -24,71 +20,7 @@ public class StockUpdateScheduler {
2420
@Scheduled(cron = "0 0 3 * * THU", zone = "Asia/Seoul") // 매주 목요일 3시 실행
2521
public void updateStockMaster() {
2622
log.info("Starting weekly stock master update scheduler");
27-
28-
try {
29-
// 1. Python 스크립트 실행하여 종목 데이터 수집
30-
String scriptPath = "scripts/import_stocks.py";
31-
File scriptFile = new File(scriptPath);
32-
33-
// Docker 환경에서는 /app 경로 기준으로 실행
34-
if (!scriptFile.exists()) {
35-
scriptPath = "/app/scripts/import_stocks.py";
36-
scriptFile = new File(scriptPath);
37-
}
38-
39-
if (!scriptFile.exists()) {
40-
log.error("Stock import script not found: {}", scriptPath);
41-
return;
42-
}
43-
44-
log.info("Executing Python script: {}", scriptPath);
45-
// 스크립트 파일명만 사용 (작업 디렉토리를 스크립트 디렉토리로 설정하므로)
46-
String scriptFileName = scriptFile.getName();
47-
File scriptDir = scriptFile.getParentFile();
48-
49-
ProcessBuilder processBuilder = new ProcessBuilder("python3", scriptFileName);
50-
// 스크립트가 있는 디렉토리를 작업 디렉토리로 설정
51-
processBuilder.directory(scriptDir != null ? scriptDir : new File("."));
52-
processBuilder.redirectErrorStream(true);
53-
54-
Process process = processBuilder.start();
55-
56-
// 스크립트 출력 읽기
57-
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
58-
String line;
59-
while ((line = reader.readLine()) != null) {
60-
log.info("Python script output: {}", line);
61-
}
62-
63-
int exitCode = process.waitFor();
64-
if (exitCode != 0) {
65-
log.error("Python script execution failed with exit code: {}", exitCode);
66-
return;
67-
}
68-
69-
log.info("Python script executed successfully");
70-
71-
// 2. 생성된 JSON 파일을 DB에 반영
72-
String jsonFilePath = "scripts/stocks_data.json";
73-
File jsonFile = new File(jsonFilePath);
74-
// Docker 환경에서는 /app 경로 기준으로 확인
75-
if (!jsonFile.exists()) {
76-
jsonFilePath = "/app/scripts/stocks_data.json";
77-
jsonFile = new File(jsonFilePath);
78-
}
79-
if (!jsonFile.exists()) {
80-
log.error("Generated JSON file not found: {}", jsonFilePath);
81-
return;
82-
}
83-
log.info("Importing stocks from JSON file: {}", jsonFilePath);
84-
stockImportService.importStocksFromJson(jsonFilePath);
85-
86-
log.info("Weekly stock master update completed successfully");
87-
88-
} catch (Exception e) {
89-
log.error("Weekly stock master update scheduler failed", e);
90-
throw new RuntimeException("Failed to update stock master", e);
91-
}
23+
stockMasterUpdateService.updateStockMaster();
9224
}
9325
}
9426

src/main/java/com/fund/stockProject/stock/controller/StockImportController.java

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
package com.fund.stockProject.stock.controller;
22

3+
import com.fund.stockProject.security.principle.CustomUserDetails;
34
import com.fund.stockProject.stock.service.StockImportService;
5+
import com.fund.stockProject.stock.service.StockMasterUpdateService;
46
import io.swagger.v3.oas.annotations.Operation;
7+
import io.swagger.v3.oas.annotations.Parameter;
58
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
69
import io.swagger.v3.oas.annotations.tags.Tag;
710
import lombok.RequiredArgsConstructor;
811
import lombok.extern.slf4j.Slf4j;
12+
import org.springframework.http.HttpStatus;
913
import org.springframework.http.ResponseEntity;
14+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
1015
import org.springframework.web.bind.annotation.*;
1116

1217
import java.util.Map;
@@ -20,12 +25,28 @@
2025
public class StockImportController {
2126

2227
private final StockImportService stockImportService;
28+
private final StockMasterUpdateService stockMasterUpdateService;
2329

2430
@PostMapping("/import")
2531
@Operation(summary = "종목 데이터 임포트", description = "JSON 파일에서 종목 데이터를 읽어서 DB에 저장합니다. 기본 경로: scripts/stocks_data.json")
2632
public ResponseEntity<Map<String, String>> importStocks(
27-
@RequestParam(required = false, defaultValue = "scripts/stocks_data.json") String jsonFilePath) {
33+
@RequestParam(required = false, defaultValue = "scripts/stocks_data.json") String jsonFilePath,
34+
@Parameter(hidden = true) @AuthenticationPrincipal CustomUserDetails customUserDetails) {
2835
try {
36+
// ADMIN 권한 체크
37+
if (customUserDetails == null) {
38+
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
39+
.body(Map.of("error", "Authentication required"));
40+
}
41+
42+
boolean isAdmin = customUserDetails.getAuthorities().stream()
43+
.anyMatch(authority -> authority.getAuthority().equals("ROLE_ADMIN"));
44+
45+
if (!isAdmin) {
46+
return ResponseEntity.status(HttpStatus.FORBIDDEN)
47+
.body(Map.of("error", "Only ADMIN users can import stocks"));
48+
}
49+
2950
stockImportService.importStocksFromJson(jsonFilePath);
3051
return ResponseEntity.ok(Map.of("message", "Stocks imported successfully"));
3152
} catch (Exception e) {
@@ -34,5 +55,36 @@ public ResponseEntity<Map<String, String>> importStocks(
3455
.body(Map.of("error", "Failed to import stocks: " + e.getMessage()));
3556
}
3657
}
58+
59+
@PostMapping("/update-master")
60+
@Operation(
61+
summary = "종목 마스터 업데이트 (배치 작업)",
62+
description = "Python 스크립트를 실행하여 최신 종목 데이터를 수집하고 DB에 반영합니다. ADMIN만 가능합니다."
63+
)
64+
public ResponseEntity<Map<String, String>> updateStockMaster(
65+
@Parameter(hidden = true) @AuthenticationPrincipal CustomUserDetails customUserDetails) {
66+
try {
67+
// ADMIN 권한 체크
68+
if (customUserDetails == null) {
69+
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
70+
.body(Map.of("error", "Authentication required"));
71+
}
72+
73+
boolean isAdmin = customUserDetails.getAuthorities().stream()
74+
.anyMatch(authority -> authority.getAuthority().equals("ROLE_ADMIN"));
75+
76+
if (!isAdmin) {
77+
return ResponseEntity.status(HttpStatus.FORBIDDEN)
78+
.body(Map.of("error", "Only ADMIN users can trigger stock master update"));
79+
}
80+
81+
stockMasterUpdateService.updateStockMaster();
82+
return ResponseEntity.ok(Map.of("message", "Stock master update completed successfully"));
83+
} catch (Exception e) {
84+
log.error("Error updating stock master", e);
85+
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
86+
.body(Map.of("error", "Failed to update stock master: " + e.getMessage()));
87+
}
88+
}
3789
}
3890

0 commit comments

Comments
 (0)