mirror of
https://github.com/NomaDamas/k-skill.git
synced 2026-06-24 02:04:11 +00:00
Merge pull request #124 from NomaDamas/fix/issue-99-korean-stock-proxy
Fix Korean stock proxy degraded search and holiday no-data handling
This commit is contained in:
commit
e1bc04bb0d
8 changed files with 231 additions and 28 deletions
|
|
@ -190,7 +190,7 @@ curl -fsS --get 'https://k-skill-proxy.nomadamas.org/v1/mfds/food-safety/search'
|
|||
```bash
|
||||
curl -fsS --get 'https://k-skill-proxy.nomadamas.org/v1/korean-stock/search' \
|
||||
--data-urlencode 'q=삼성전자' \
|
||||
--data-urlencode 'bas_dd=20260404'
|
||||
--data-urlencode 'bas_dd=20260408'
|
||||
```
|
||||
|
||||
한국 주식 기본정보 endpoint:
|
||||
|
|
@ -199,7 +199,7 @@ curl -fsS --get 'https://k-skill-proxy.nomadamas.org/v1/korean-stock/search' \
|
|||
curl -fsS --get 'https://k-skill-proxy.nomadamas.org/v1/korean-stock/base-info' \
|
||||
--data-urlencode 'market=KOSPI' \
|
||||
--data-urlencode 'code=005930' \
|
||||
--data-urlencode 'bas_dd=20260404'
|
||||
--data-urlencode 'bas_dd=20260408'
|
||||
```
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ upstream 참고 구현은 [`jjlabsio/korea-stock-mcp`](https://github.com/jjlabs
|
|||
```bash
|
||||
curl -fsS --get 'https://k-skill-proxy.nomadamas.org/v1/korean-stock/search' \
|
||||
--data-urlencode 'q=삼성전자' \
|
||||
--data-urlencode 'bas_dd=20260404'
|
||||
--data-urlencode 'bas_dd=20260408'
|
||||
```
|
||||
|
||||
## 기본정보 예시
|
||||
|
|
@ -40,7 +40,7 @@ curl -fsS --get 'https://k-skill-proxy.nomadamas.org/v1/korean-stock/search' \
|
|||
curl -fsS --get 'https://k-skill-proxy.nomadamas.org/v1/korean-stock/base-info' \
|
||||
--data-urlencode 'market=KOSPI' \
|
||||
--data-urlencode 'code=005930' \
|
||||
--data-urlencode 'bas_dd=20260404'
|
||||
--data-urlencode 'bas_dd=20260408'
|
||||
```
|
||||
|
||||
## 일별 시세 예시
|
||||
|
|
@ -49,7 +49,7 @@ curl -fsS --get 'https://k-skill-proxy.nomadamas.org/v1/korean-stock/base-info'
|
|||
curl -fsS --get 'https://k-skill-proxy.nomadamas.org/v1/korean-stock/trade-info' \
|
||||
--data-urlencode 'market=KOSPI' \
|
||||
--data-urlencode 'code=005930' \
|
||||
--data-urlencode 'bas_dd=20260404'
|
||||
--data-urlencode 'bas_dd=20260408'
|
||||
```
|
||||
|
||||
## 응답 해석 팁
|
||||
|
|
@ -59,6 +59,7 @@ curl -fsS --get 'https://k-skill-proxy.nomadamas.org/v1/korean-stock/trade-info'
|
|||
- `close_price`, `trading_volume`, `market_cap` 은 숫자로 정규화돼 온다.
|
||||
- `base_date`/`bas_dd` 는 일별 snapshot 날짜다.
|
||||
- 휴장일/장마감 전에는 빈 결과나 `not_found` 가 나올 수 있다.
|
||||
- 일부 시장 upstream 이 실패하면 검색 응답에 `upstream.degraded=true` 와 `failed_markets` 가 붙을 수 있다.
|
||||
|
||||
## 답변 템플릿 권장
|
||||
|
||||
|
|
@ -72,7 +73,8 @@ curl -fsS --get 'https://k-skill-proxy.nomadamas.org/v1/korean-stock/trade-info'
|
|||
|
||||
- 잘못된 `market`, `code`, `bas_dd` 형식은 400
|
||||
- proxy 서버에 `KRX_API_KEY` 가 없으면 503
|
||||
- upstream KRX 오류는 502
|
||||
- 검색 중 일부 시장 upstream 이 실패하면 200 이지만 `upstream.degraded=true` / `failed_markets` 가 함께 온다.
|
||||
- 모든 요청 시장에서 upstream KRX 조회가 실패하면 502
|
||||
- 기준일에 종목을 찾지 못하면 404 `not_found`
|
||||
|
||||
## 참고 링크
|
||||
|
|
|
|||
|
|
@ -132,19 +132,19 @@ korean-law list
|
|||
|
||||
`korean-stock-search` 는 로컬 MCP 설치 대신 **proxy first** 로 사용한다.
|
||||
|
||||
- 가장 빠른 smoke test 는 `curl -fsS --get 'https://k-skill-proxy.nomadamas.org/v1/korean-stock/search' --data-urlencode 'q=삼성전자' --data-urlencode 'bas_dd=20260404'`
|
||||
- 가장 빠른 smoke test 는 `curl -fsS --get 'https://k-skill-proxy.nomadamas.org/v1/korean-stock/search' --data-urlencode 'q=삼성전자' --data-urlencode 'bas_dd=20260408'`
|
||||
- 검색 결과에서 `market`, `code` 를 확인한 뒤 `base-info` 또는 `trade-info` 로 이어간다.
|
||||
- 사용자 쪽 `KRX_API_KEY` 는 필요 없다. self-host proxy 운영자만 서버 환경변수 `KRX_API_KEY` 를 설정한다.
|
||||
|
||||
```bash
|
||||
curl -fsS --get 'https://k-skill-proxy.nomadamas.org/v1/korean-stock/search' \
|
||||
--data-urlencode 'q=삼성전자' \
|
||||
--data-urlencode 'bas_dd=20260404'
|
||||
--data-urlencode 'bas_dd=20260408'
|
||||
|
||||
curl -fsS --get 'https://k-skill-proxy.nomadamas.org/v1/korean-stock/base-info' \
|
||||
--data-urlencode 'market=KOSPI' \
|
||||
--data-urlencode 'code=005930' \
|
||||
--data-urlencode 'bas_dd=20260404'
|
||||
--data-urlencode 'bas_dd=20260408'
|
||||
```
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ upstream 설계 참고는 [`jjlabsio/korea-stock-mcp`](https://github.com/jjlabs
|
|||
|
||||
- "삼성전자 종목코드랑 시장구분 찾아줘"
|
||||
- "005930 기본정보 보여줘"
|
||||
- "SK하이닉스 20260404 종가/거래량 알려줘"
|
||||
- "SK하이닉스 20260408 종가/거래량 알려줘"
|
||||
- "KOSDAQ 에서 알테오젠 시세 확인해줘"
|
||||
|
||||
## When not to use
|
||||
|
|
@ -75,7 +75,7 @@ GET /v1/korean-stock/trade-info?market={KOSPI|KOSDAQ|KONEX}&code={종목코드}&
|
|||
```bash
|
||||
curl -fsS --get 'https://k-skill-proxy.nomadamas.org/v1/korean-stock/search' \
|
||||
--data-urlencode 'q=삼성전자' \
|
||||
--data-urlencode 'bas_dd=20260404'
|
||||
--data-urlencode 'bas_dd=20260408'
|
||||
```
|
||||
|
||||
종목 기본정보:
|
||||
|
|
@ -84,7 +84,7 @@ curl -fsS --get 'https://k-skill-proxy.nomadamas.org/v1/korean-stock/search' \
|
|||
curl -fsS --get 'https://k-skill-proxy.nomadamas.org/v1/korean-stock/base-info' \
|
||||
--data-urlencode 'market=KOSPI' \
|
||||
--data-urlencode 'code=005930' \
|
||||
--data-urlencode 'bas_dd=20260404'
|
||||
--data-urlencode 'bas_dd=20260408'
|
||||
```
|
||||
|
||||
종목 일별 시세:
|
||||
|
|
@ -93,7 +93,7 @@ curl -fsS --get 'https://k-skill-proxy.nomadamas.org/v1/korean-stock/base-info'
|
|||
curl -fsS --get 'https://k-skill-proxy.nomadamas.org/v1/korean-stock/trade-info' \
|
||||
--data-urlencode 'market=KOSPI' \
|
||||
--data-urlencode 'code=005930' \
|
||||
--data-urlencode 'bas_dd=20260404'
|
||||
--data-urlencode 'bas_dd=20260408'
|
||||
```
|
||||
|
||||
## Response shape
|
||||
|
|
@ -113,7 +113,7 @@ curl -fsS --get 'https://k-skill-proxy.nomadamas.org/v1/korean-stock/trade-info'
|
|||
"listed_at": "1975-06-11"
|
||||
}
|
||||
],
|
||||
"query": { "q": "삼성전자", "bas_dd": "20260404", "limit": 10 },
|
||||
"query": { "q": "삼성전자", "bas_dd": "20260408", "limit": 10 },
|
||||
"proxy": { "name": "k-skill-proxy", "cache": { "hit": false, "ttl_ms": 300000 } }
|
||||
}
|
||||
```
|
||||
|
|
@ -135,7 +135,7 @@ curl -fsS --get 'https://k-skill-proxy.nomadamas.org/v1/korean-stock/trade-info'
|
|||
"par_value": 100,
|
||||
"listed_shares": 5969782550
|
||||
},
|
||||
"query": { "market": "KOSPI", "code": "005930", "bas_dd": "20260404" },
|
||||
"query": { "market": "KOSPI", "code": "005930", "bas_dd": "20260408" },
|
||||
"proxy": { "name": "k-skill-proxy", "cache": { "hit": false, "ttl_ms": 300000 } }
|
||||
}
|
||||
```
|
||||
|
|
@ -148,7 +148,7 @@ curl -fsS --get 'https://k-skill-proxy.nomadamas.org/v1/korean-stock/trade-info'
|
|||
"market": "KOSPI",
|
||||
"code": "005930",
|
||||
"standard_code": "KR7005930003",
|
||||
"base_date": "20260404",
|
||||
"base_date": "20260408",
|
||||
"name": "삼성전자",
|
||||
"close_price": 84000,
|
||||
"change_price": 1000,
|
||||
|
|
@ -160,7 +160,7 @@ curl -fsS --get 'https://k-skill-proxy.nomadamas.org/v1/korean-stock/trade-info'
|
|||
"trading_value": 1030000000000,
|
||||
"market_cap": 500000000000000
|
||||
},
|
||||
"query": { "market": "KOSPI", "code": "005930", "bas_dd": "20260404" },
|
||||
"query": { "market": "KOSPI", "code": "005930", "bas_dd": "20260408" },
|
||||
"proxy": { "name": "k-skill-proxy", "cache": { "hit": false, "ttl_ms": 300000 } }
|
||||
}
|
||||
```
|
||||
|
|
@ -168,8 +168,9 @@ curl -fsS --get 'https://k-skill-proxy.nomadamas.org/v1/korean-stock/trade-info'
|
|||
## Response policy
|
||||
|
||||
- 종목명이 모호하면 먼저 `search` 로 시장/종목코드를 좁힌 뒤 `base-info` 또는 `trade-info` 로 들어간다.
|
||||
- 일부 시장 upstream 이 실패하면 `upstream.degraded=true` 와 `failed_markets` 를 보고 부분 장애 여부를 함께 설명한다.
|
||||
- `trade-info` 결과는 일별 snapshot 이다. 실시간 호가/체결처럼 말하지 않는다.
|
||||
- 휴장일/장마감 이전이면 해당 `bas_dd` 에 데이터가 없을 수 있으니 최근 영업일로 재시도한다.
|
||||
- 휴장일/장마감 이전이면 해당 `bas_dd` 에 데이터가 없을 수 있으니 최근 영업일로 재시도한다. 이 경우 `trade-info` 는 502 대신 `not_found` 로 끝날 수 있다.
|
||||
- 숫자는 사람이 읽기 쉬운 단위(원, 주, 억/조)로 짧게 풀어주되 원본 숫자도 유지한다.
|
||||
- 답변 말미에 "KRX 공식 데이터 기준 / 투자 조언 아님" 을 짧게 남긴다.
|
||||
|
||||
|
|
@ -185,7 +186,8 @@ curl -fsS --get 'https://k-skill-proxy.nomadamas.org/v1/korean-stock/trade-info'
|
|||
|
||||
- `q`, `market`, `code`, `bas_dd` 형식이 잘못되면 400 응답
|
||||
- 프록시 서버에 `KRX_API_KEY` 가 없으면 503 응답
|
||||
- upstream KRX 응답 오류면 502 응답
|
||||
- 검색 중 일부 시장 upstream 이 실패하면 200 응답이지만 `upstream.degraded=true` 와 `failed_markets` 를 함께 반환할 수 있다.
|
||||
- 모든 요청 시장에서 upstream KRX 조회가 실패하면 502 응답
|
||||
- 해당 기준일/시장에 종목이 없으면 404 `not_found`
|
||||
|
||||
## Done when
|
||||
|
|
|
|||
|
|
@ -116,7 +116,7 @@ curl -fsS --get 'http://127.0.0.1:4020/v1/mfds/food-safety/search' \
|
|||
```bash
|
||||
curl -fsS --get 'http://127.0.0.1:4020/v1/korean-stock/search' \
|
||||
--data-urlencode 'q=삼성전자' \
|
||||
--data-urlencode 'bas_dd=20260404'
|
||||
--data-urlencode 'bas_dd=20260408'
|
||||
```
|
||||
|
||||
프록시는 내부적으로 `waterlevel/info.json` 으로 관측소를 해석하고, `waterlevel/list/10M/{WLOBSCD}.json` 으로 최신 수위/유량을 조회합니다. 한국 주식 route는 KRX Open API에 `AUTH_KEY` 헤더를 서버 쪽에서만 주입합니다.
|
||||
|
|
|
|||
|
|
@ -165,6 +165,10 @@ async function fetchBaseInfo({ market, basDd = getCurrentKstDate(), codeList = [
|
|||
async function fetchTradeInfo({ market, basDd = getCurrentKstDate(), codeList, apiKey, fetchImpl = global.fetch }) {
|
||||
const tradeItems = await krxRequest(buildUrl(KRX_TRADE_INFO_URLS[market], { basDd }), apiKey, fetchImpl);
|
||||
|
||||
if (tradeItems.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const directlyMatched = tradeItems.filter((item) => matchesCodes(item, codeList));
|
||||
if (directlyMatched.length > 0) {
|
||||
return directlyMatched.map((item) => normalizeTradeItem(item, market));
|
||||
|
|
@ -221,6 +225,14 @@ function buildBaseInfoSnapshotCacheKey({ market, basDd }) {
|
|||
return `krx-base-info:${market}:${basDd}`;
|
||||
}
|
||||
|
||||
function serializeKrxError(error) {
|
||||
return {
|
||||
code: error?.code || "proxy_error",
|
||||
status_code: error?.statusCode || 502,
|
||||
message: error?.message || "Unknown KRX upstream error."
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchBaseInfoSnapshot({
|
||||
market,
|
||||
basDd,
|
||||
|
|
@ -272,13 +284,20 @@ async function searchStocks({
|
|||
const successfulResults = settledResults
|
||||
.filter((result) => result.status === "fulfilled")
|
||||
.map((result) => result.value);
|
||||
const failedResults = settledResults
|
||||
.map((result, index) => ({ result, market: markets[index] }))
|
||||
.filter(({ result }) => result.status === "rejected")
|
||||
.map(({ result, market }) => ({
|
||||
market,
|
||||
...serializeKrxError(result.reason)
|
||||
}));
|
||||
|
||||
if (successfulResults.length === 0) {
|
||||
const firstFailure = settledResults.find((result) => result.status === "rejected");
|
||||
throw firstFailure?.reason || new Error("KRX search failed for every market.");
|
||||
}
|
||||
|
||||
return {
|
||||
const payload = {
|
||||
items: successfulResults
|
||||
.flatMap(({ market: entryMarket, items }) =>
|
||||
items
|
||||
|
|
@ -289,6 +308,17 @@ async function searchStocks({
|
|||
.slice(0, limit)
|
||||
.map(({ score, ...item }) => item)
|
||||
};
|
||||
|
||||
if (failedResults.length > 0) {
|
||||
payload.upstream = {
|
||||
degraded: true,
|
||||
requested_markets: markets,
|
||||
successful_markets: successfulResults.map(({ market: entryMarket }) => entryMarket),
|
||||
failed_markets: failedResults
|
||||
};
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
|
|
|||
|
|
@ -2020,9 +2020,9 @@ function buildServer({ env = process.env, provider = null, now = () => new Date(
|
|||
};
|
||||
}
|
||||
|
||||
let items;
|
||||
let result;
|
||||
try {
|
||||
const result = await searchStocks({
|
||||
result = await searchStocks({
|
||||
query: normalized.q,
|
||||
basDd: normalized.basDd,
|
||||
market: normalized.market,
|
||||
|
|
@ -2031,7 +2031,6 @@ function buildServer({ env = process.env, provider = null, now = () => new Date(
|
|||
cache,
|
||||
cacheTtlMs: config.cacheTtlMs
|
||||
});
|
||||
items = result.items;
|
||||
} catch (error) {
|
||||
reply.code(error.statusCode && error.statusCode >= 400 ? error.statusCode : 502);
|
||||
return {
|
||||
|
|
@ -2041,7 +2040,7 @@ function buildServer({ env = process.env, provider = null, now = () => new Date(
|
|||
}
|
||||
|
||||
const payload = {
|
||||
items,
|
||||
items: result.items,
|
||||
query: {
|
||||
q: normalized.q,
|
||||
bas_dd: normalized.basDd,
|
||||
|
|
@ -2058,7 +2057,13 @@ function buildServer({ env = process.env, provider = null, now = () => new Date(
|
|||
}
|
||||
};
|
||||
|
||||
cache.set(cacheKey, payload, config.cacheTtlMs);
|
||||
if (result.upstream) {
|
||||
payload.upstream = result.upstream;
|
||||
}
|
||||
|
||||
if (!result.upstream?.degraded) {
|
||||
cache.set(cacheKey, payload, config.cacheTtlMs);
|
||||
}
|
||||
return payload;
|
||||
});
|
||||
|
||||
|
|
@ -2442,7 +2447,7 @@ function buildServer({ env = process.env, provider = null, now = () => new Date(
|
|||
reply.code(404);
|
||||
return {
|
||||
error: "not_found",
|
||||
message: `기준일 ${normalized.basDd} 에 ${normalized.market} 시장 종목 ${normalized.code} 의 일별 시세를 찾지 못했습니다.`
|
||||
message: `기준일 ${normalized.basDd} 에 ${normalized.market} 시장 종목 ${normalized.code} 의 일별 시세를 찾지 못했습니다. 휴장일이거나 데이터가 아직 없을 수 있습니다.`
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -172,7 +172,7 @@ test("korean stock search rate limit does not trust spoofed cf-connecting-ip on
|
|||
assert.equal(second.json().error, "rate_limited");
|
||||
});
|
||||
|
||||
test("korean stock search returns healthy market results when another market upstream fails", async (t) => {
|
||||
test("korean stock search surfaces degraded upstream metadata when another market fails", async (t) => {
|
||||
const originalFetch = global.fetch;
|
||||
const fetchCalls = [];
|
||||
global.fetch = async (url, options = {}) => {
|
||||
|
|
@ -249,10 +249,124 @@ test("korean stock search returns healthy market results when another market ups
|
|||
assert.equal(response.json().items[0].market, "KOSPI");
|
||||
assert.equal(response.json().items[0].code, "005930");
|
||||
assert.equal(response.json().items[0].name, "삼성전자");
|
||||
assert.equal(response.json().upstream.degraded, true);
|
||||
assert.deepEqual(response.json().upstream.requested_markets, ["KOSPI", "KOSDAQ", "KONEX"]);
|
||||
assert.deepEqual(response.json().upstream.successful_markets, ["KOSPI", "KONEX"]);
|
||||
assert.deepEqual(response.json().upstream.failed_markets, [
|
||||
{
|
||||
market: "KOSDAQ",
|
||||
code: "upstream_error",
|
||||
status_code: 502,
|
||||
message: "KRX API HTTP 오류 (status: 500): Internal Server Error"
|
||||
}
|
||||
]);
|
||||
assert.equal(fetchCalls.length, 3);
|
||||
assert.ok(fetchCalls.every((entry) => entry.url.startsWith("https://data-dbg.krx.co.kr/")));
|
||||
});
|
||||
|
||||
test("korean stock search does not cache degraded responses and retries a recovered market", async (t) => {
|
||||
const originalFetch = global.fetch;
|
||||
const fetchCalls = [];
|
||||
let kosdaqAttempts = 0;
|
||||
|
||||
global.fetch = async (url, options = {}) => {
|
||||
const text = String(url);
|
||||
fetchCalls.push({ url: text, headers: options.headers });
|
||||
|
||||
if (text.includes("stk_isu_base_info") || text.includes("knx_isu_base_info")) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
OutBlock_1: []
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json;charset=UTF-8" }
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (text.includes("ksq_isu_base_info")) {
|
||||
kosdaqAttempts += 1;
|
||||
|
||||
if (kosdaqAttempts === 1) {
|
||||
return new Response("boom", {
|
||||
status: 500,
|
||||
statusText: "Internal Server Error"
|
||||
});
|
||||
}
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
OutBlock_1: [
|
||||
{
|
||||
ISU_CD: "KR7196170005",
|
||||
ISU_SRT_CD: "196170",
|
||||
ISU_NM: "알테오젠",
|
||||
ISU_ABBRV: "알테오젠",
|
||||
ISU_ENG_NM: "Alteogen",
|
||||
LIST_DD: "20140509",
|
||||
MKT_TP_NM: "KOSDAQ",
|
||||
SECUGRP_NM: "주권",
|
||||
SECT_TP_NM: "제약",
|
||||
KIND_STKCERT_TP_NM: "보통주",
|
||||
PARVAL: "500",
|
||||
LIST_SHRS: "53470829"
|
||||
}
|
||||
]
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json;charset=UTF-8" }
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error(`unexpected URL: ${url}`);
|
||||
};
|
||||
|
||||
const app = buildServer({
|
||||
env: {
|
||||
KRX_API_KEY: "krx-key",
|
||||
KSKILL_PROXY_CACHE_TTL_MS: "60000"
|
||||
}
|
||||
});
|
||||
|
||||
t.after(async () => {
|
||||
global.fetch = originalFetch;
|
||||
await app.close();
|
||||
});
|
||||
|
||||
const first = await app.inject({
|
||||
method: "GET",
|
||||
url: "/v1/korean-stock/search?q=%EC%95%8C%ED%85%8C%EC%98%A4%EC%A0%A0&bas_dd=20260408"
|
||||
});
|
||||
const second = await app.inject({
|
||||
method: "GET",
|
||||
url: "/v1/korean-stock/search?q=%EC%95%8C%ED%85%8C%EC%98%A4%EC%A0%A0&bas_dd=20260408"
|
||||
});
|
||||
|
||||
assert.equal(first.statusCode, 200);
|
||||
assert.equal(first.json().items.length, 0);
|
||||
assert.equal(first.json().proxy.cache.hit, false);
|
||||
assert.equal(first.json().upstream.degraded, true);
|
||||
assert.deepEqual(first.json().upstream.failed_markets, [
|
||||
{
|
||||
market: "KOSDAQ",
|
||||
code: "upstream_error",
|
||||
status_code: 502,
|
||||
message: "KRX API HTTP 오류 (status: 500): Internal Server Error"
|
||||
}
|
||||
]);
|
||||
|
||||
assert.equal(second.statusCode, 200);
|
||||
assert.equal(second.json().proxy.cache.hit, false);
|
||||
assert.equal(second.json().items.length, 1);
|
||||
assert.equal(second.json().items[0].market, "KOSDAQ");
|
||||
assert.equal(second.json().items[0].code, "196170");
|
||||
assert.equal(kosdaqAttempts, 2);
|
||||
assert.equal(fetchCalls.length, 4);
|
||||
});
|
||||
|
||||
test("korean stock search reuses per-market base snapshots across different queries for the same date", async (t) => {
|
||||
const originalFetch = global.fetch;
|
||||
const fetchCalls = [];
|
||||
|
|
@ -538,6 +652,56 @@ test("korean stock trade-info endpoint does not relabel an unmatched single-row
|
|||
assert.ok(fetchCalls.every((entry) => entry.startsWith("https://data-dbg.krx.co.kr/")));
|
||||
});
|
||||
|
||||
test("korean stock trade-info endpoint treats empty trade snapshots as not_found without base-info fallback", async (t) => {
|
||||
const originalFetch = global.fetch;
|
||||
const fetchCalls = [];
|
||||
global.fetch = async (url) => {
|
||||
const text = String(url);
|
||||
fetchCalls.push(text);
|
||||
|
||||
if (text.includes("stk_bydd_trd")) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
OutBlock_1: []
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json;charset=UTF-8" }
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (text.includes("stk_isu_base_info")) {
|
||||
throw new Error("base-info fallback should not run for empty trade snapshots");
|
||||
}
|
||||
|
||||
throw new Error(`unexpected URL: ${url}`);
|
||||
};
|
||||
|
||||
const app = buildServer({
|
||||
env: {
|
||||
KRX_API_KEY: "krx-key"
|
||||
}
|
||||
});
|
||||
|
||||
t.after(async () => {
|
||||
global.fetch = originalFetch;
|
||||
await app.close();
|
||||
});
|
||||
|
||||
const response = await app.inject({
|
||||
method: "GET",
|
||||
url: "/v1/korean-stock/trade-info?market=KOSPI&code=005930&bas_dd=20260404"
|
||||
});
|
||||
|
||||
assert.equal(response.statusCode, 404);
|
||||
assert.equal(response.json().error, "not_found");
|
||||
assert.match(response.json().message, /휴장일이거나 데이터가 아직 없을 수 있습니다/);
|
||||
assert.deepEqual(fetchCalls, [
|
||||
"https://data-dbg.krx.co.kr/svc/apis/sto/stk_bydd_trd?basDd=20260404"
|
||||
]);
|
||||
});
|
||||
|
||||
test("fine dust endpoint stays publicly callable without proxy auth", async (t) => {
|
||||
let providerCalls = 0;
|
||||
const app = buildServer({
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue