From 2f8a6ea349afd4ccbf5f9840ff9e5f3fcf81c41f Mon Sep 17 00:00:00 2001 From: hayato5246 Date: Wed, 8 Apr 2026 19:53:51 +0900 Subject: [PATCH] =?UTF-8?q?=EC=A0=84=EC=9D=BC=EB=8C=80=EB=B9=84=20?= =?UTF-8?q?=EB=B6=80=ED=98=B8=20=EB=B0=8F=20API=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=ED=8C=8C=EC=8B=B1=20=EB=A1=9C=EC=A7=81=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0:=20-=20`utils.ts`:=20=EC=A0=84=EC=9D=BC=EB=8C=80?= =?UTF-8?q?=EB=B9=84=20=EB=B6=80=ED=98=B8=20=EB=A7=A4=ED=95=91=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81(1=3D=EC=83=81=ED=95=9C=EA=B0=80,=204=3D=ED=95=98?= =?UTF-8?q?=ED=95=9C=EA=B0=80)=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=ED=95=A8=EC=88=98(`sigToArrow`,=20`sigCla?= =?UTF-8?q?ss`)=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8.=20-=20`types.ts`:?= =?UTF-8?q?=20`IndexQuote`=20=EC=9D=B8=ED=84=B0=ED=8E=98=EC=9D=B4=EC=8A=A4?= =?UTF-8?q?=EC=9D=98=20`value`=20=ED=95=84=EB=93=9C=EB=AA=85=EC=9D=84=20`p?= =?UTF-8?q?rice`=EB=A1=9C=20=EC=88=98=EC=A0=95.=20-=20`kiwoom=5Fws=5Fservi?= =?UTF-8?q?ce.go`,=20`theme=5Fservice.go`,=20`kiwoom=5Fservice.go`:=20?= =?UTF-8?q?=EC=A0=84=EC=9D=BC=EB=8C=80=EB=B9=84=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=ED=8C=8C=EC=8B=B1=20=EC=8B=9C=20=EB=B6=80=ED=98=B8?= =?UTF-8?q?=20=EA=B2=B0=EC=A0=95=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?(`signedChange`,=20`signedChangeBySig`)=20=EB=B0=8F=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=ED=95=A8=EC=88=98=20=ED=98=B8=EC=B6=9C=EB=A1=9C=20?= =?UTF-8?q?=EB=8C=80=EC=B2=B4.=20-=20=EA=B8=B0=EC=A1=B4=20`predPre`=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=20=EB=B6=80=EB=B6=84=EC=9D=84=20=EC=96=91?= =?UTF-8?q?=EC=88=98/=EC=9D=8C=EC=88=98=EB=A5=BC=20=EC=A0=95=ED=99=95?= =?UTF-8?q?=ED=9E=88=20=EA=B3=84=EC=82=B0=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD.=20-=20`kospi200=5Fservice.go`:=20`PredPre`?= =?UTF-8?q?=20=ED=95=84=EB=93=9C=20=EB=B6=80=ED=98=B8=20=EC=B2=98=EB=A6=AC?= =?UTF-8?q?=20=EB=A1=9C=EC=A7=81(`signedChangeBySig`)=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?.=20-=20Svelte=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=97=85?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=8A=B8:=20=20=20-=20`+page.svelte`=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=EC=97=90=EC=84=9C=20`value`=20=ED=95=84?= =?UTF-8?q?=EB=93=9C=EB=A5=BC=20`price`=EB=A1=9C=20=EB=8C=80=EC=B2=B4.=20?= =?UTF-8?q?=20=20-=20`theme`=20=EB=B0=8F=20`kospi200`=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=EC=97=90=EC=84=9C=20=EC=A0=84=EC=9D=BC=EB=8C=80?= =?UTF-8?q?=EB=B9=84=20=ED=91=9C=EC=8B=9C=EA=B0=92=20=EC=A0=88=EB=8C=93?= =?UTF-8?q?=EA=B0=92=EC=9C=BC=EB=A1=9C=20=EB=A0=8C=EB=8D=94=EB=A7=81?= =?UTF-8?q?=EB=90=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95=20(`Math.abs`?= =?UTF-8?q?=20=EC=A0=81=EC=9A=A9).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/lib/api/types.ts | 2 +- frontend/src/lib/utils.ts | 10 +++---- frontend/src/routes/(app)/+page.svelte | 2 +- .../src/routes/(app)/kospi200/+page.svelte | 2 +- frontend/src/routes/(app)/theme/+page.svelte | 2 +- services/kiwoom_service.go | 28 ++++++++++++++++--- services/kiwoom_ws_service.go | 20 ++++++++++--- services/kospi200_service.go | 2 +- services/theme_service.go | 6 ++-- 9 files changed, 53 insertions(+), 21 deletions(-) diff --git a/frontend/src/lib/api/types.ts b/frontend/src/lib/api/types.ts index 6948542..0770392 100644 --- a/frontend/src/lib/api/types.ts +++ b/frontend/src/lib/api/types.ts @@ -149,7 +149,7 @@ export interface WatchlistItem { export interface IndexQuote { name: string - value: number + price: number change: number changeRate: number } diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts index 71ef64b..3ceab12 100644 --- a/frontend/src/lib/utils.ts +++ b/frontend/src/lib/utils.ts @@ -39,16 +39,16 @@ export function formatDate(iso: string): string { return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}` } -// 전일대비 부호 문자 (2=상승, 3=보합, 5=하락) +// 전일대비 부호 문자 (1=상한가, 2=상승, 3=보합, 4=하한가, 5=하락) export function sigToArrow(sig: string): string { - if (sig === '2') return '▲' - if (sig === '5') return '▼' + if (sig === '1' || sig === '2') return '▲' + if (sig === '4' || sig === '5') return '▼' return '-' } // 전일대비 부호에 따른 클래스 export function sigClass(sig: string): string { - if (sig === '2') return 'text-red-400' - if (sig === '5') return 'text-blue-400' + if (sig === '1' || sig === '2') return 'text-red-400' + if (sig === '4' || sig === '5') return 'text-blue-400' return 'text-gray-400' } diff --git a/frontend/src/routes/(app)/+page.svelte b/frontend/src/routes/(app)/+page.svelte index 58c2bf1..2b03475 100644 --- a/frontend/src/routes/(app)/+page.svelte +++ b/frontend/src/routes/(app)/+page.svelte @@ -296,7 +296,7 @@ {#each indices as idx}
{idx.name}
-
{idx.value.toLocaleString('ko-KR', { maximumFractionDigits: 2 })}
+
{idx.price.toLocaleString('ko-KR', { maximumFractionDigits: 2 })}
{formatRate(idx.changeRate)}
{/each} diff --git a/frontend/src/routes/(app)/kospi200/+page.svelte b/frontend/src/routes/(app)/kospi200/+page.svelte index 2b8c68a..4fe28e8 100644 --- a/frontend/src/routes/(app)/kospi200/+page.svelte +++ b/frontend/src/routes/(app)/kospi200/+page.svelte @@ -97,7 +97,7 @@ {stock.curPrc.toLocaleString()} - {sigToArrow(stock.predPreSig)} {stock.predPre.toLocaleString()} + {sigToArrow(stock.predPreSig)} {Math.abs(stock.predPre).toLocaleString()} {formatRate(stock.fluRt)} diff --git a/frontend/src/routes/(app)/theme/+page.svelte b/frontend/src/routes/(app)/theme/+page.svelte index ddb0d3e..3598dc5 100644 --- a/frontend/src/routes/(app)/theme/+page.svelte +++ b/frontend/src/routes/(app)/theme/+page.svelte @@ -190,7 +190,7 @@ {stock.curPrc.toLocaleString()} - {sigToArrow(stock.fluSig)}{stock.predPre.toLocaleString()} + {sigToArrow(stock.fluSig)}{Math.abs(stock.predPre).toLocaleString()} {formatRate(stock.fluRt)} diff --git a/services/kiwoom_service.go b/services/kiwoom_service.go index f63dab6..a88338f 100644 --- a/services/kiwoom_service.go +++ b/services/kiwoom_service.go @@ -219,12 +219,11 @@ func (k *KiwoomClient) fetchPrice(stkCd string) (*models.StockPrice, error) { if result.ReturnCode != 0 { return nil, fmt.Errorf("현재가 조회 실패: %s", result.ReturnMsg) } - price := &models.StockPrice{ Code: displayCode, Name: result.StkNm, CurrentPrice: absParseIntSafe(result.CurPrc), - ChangePrice: parseIntSafe(result.PredPre), + ChangePrice: signedChange(result.PredPre, result.FluRt), ChangeRate: parseFloatSafe(result.FluRt), Volume: absParseIntSafe(result.TrdeQty), High: absParseIntSafe(result.HighPric), @@ -451,7 +450,7 @@ func (k *KiwoomClient) GetTopVolumeStocks(market string, count int) ([]models.St Code: row.StkCd, Name: row.StkNm, CurrentPrice: absParseIntSafe(row.CurPrc), - ChangePrice: parseIntSafe(row.PredPre), + ChangePrice: signedChange(row.PredPre, row.FluRt), ChangeRate: parseFloatSafe(row.FluRt), Volume: absParseIntSafe(row.TrdeQty), Market: mktName, @@ -559,7 +558,7 @@ func (k *KiwoomClient) GetTopFluctuation(market string, ascending bool, count in Code: row.StkCd, Name: row.StkNm, CurrentPrice: absParseIntSafe(row.CurPrc), - ChangePrice: parseIntSafe(row.PredPre), + ChangePrice: signedChange(row.PredPre, row.FluRt), ChangeRate: parseFloatSafe(row.FluRt), Volume: absParseIntSafe(row.NowTrdeQty), CntrStr: parseFloatSafe(row.CntrStr), @@ -588,6 +587,27 @@ func absParseIntSafe(s string) int64 { return n } +// signedChange 전일대비를 절댓값으로 파싱 후 등락률 부호에 맞춰 부호 결정 +// 키움 API의 pred_pre 필드는 가격 필드처럼 부호가 방향 표시용이므로 flu_rt 기준으로 부호 결정 +func signedChange(predPre string, fluRt string) int64 { + n := absParseIntSafe(predPre) + rate := parseFloatSafe(fluRt) + if rate < 0 { + return -n + } + return n +} + +// signedChangeBySig 전일대비를 절댓값으로 파싱 후 flu_sig(등락기호) 기준으로 부호 결정 +// flu_sig: 1=상한가, 2=상승, 3=보합, 4=하한가, 5=하락 +func signedChangeBySig(predPre string, fluSig string) int64 { + n := absParseIntSafe(predPre) + if fluSig == "4" || fluSig == "5" { + return -n + } + return n +} + func parseFloatSafe(s string) float64 { s = strings.ReplaceAll(s, ",", "") s = strings.TrimPrefix(s, "+") diff --git a/services/kiwoom_ws_service.go b/services/kiwoom_ws_service.go index 44997a6..d5cc5f4 100644 --- a/services/kiwoom_ws_service.go +++ b/services/kiwoom_ws_service.go @@ -434,11 +434,17 @@ func (k *KiwoomWSClient) reconnect() { func parseRealPrice(code string, v map[string]string) *models.StockPrice { normalized := strings.TrimPrefix(code, "A") normalized = strings.SplitN(normalized, "_", 2)[0] // _NX, _AL 등 접미사 제거 + // 전일대비(v["11"])는 절댓값으로 파싱 후, 등락률(v["12"]) 부호에 맞춰 부호 결정 + changeAbs := absInt(parseWSInt(v["11"])) + changeRate := parseWSFloat(v["12"]) + if changeRate < 0 { + changeAbs = -changeAbs + } return &models.StockPrice{ Code: normalized, CurrentPrice: absInt(parseWSInt(v["10"])), - ChangePrice: parseWSInt(v["11"]), - ChangeRate: parseWSFloat(v["12"]), + ChangePrice: changeAbs, + ChangeRate: changeRate, Volume: absInt(parseWSInt(v["13"])), TradeMoney: absInt(parseWSInt(v["14"])), TradeVolume: absInt(parseWSInt(v["15"])), @@ -458,11 +464,17 @@ func parseRealPrice(code string, v map[string]string) *models.StockPrice { func parseExpectedPrice(code string, v map[string]string) *models.StockPrice { normalized := strings.TrimPrefix(code, "A") normalized = strings.SplitN(normalized, "_", 2)[0] + // 전일대비(v["11"])는 절댓값으로 파싱 후, 등락률(v["12"]) 부호에 맞춰 부호 결정 + expChangeAbs := absInt(parseWSInt(v["11"])) + expChangeRate := parseWSFloat(v["12"]) + if expChangeRate < 0 { + expChangeAbs = -expChangeAbs + } return &models.StockPrice{ Code: normalized, CurrentPrice: absInt(parseWSInt(v["10"])), - ChangePrice: parseWSInt(v["11"]), - ChangeRate: parseWSFloat(v["12"]), + ChangePrice: expChangeAbs, + ChangeRate: expChangeRate, TradeVolume: absInt(parseWSInt(v["15"])), Volume: absInt(parseWSInt(v["13"])), TradeTime: v["20"], diff --git a/services/kospi200_service.go b/services/kospi200_service.go index 47b1d9f..55ba625 100644 --- a/services/kospi200_service.go +++ b/services/kospi200_service.go @@ -85,7 +85,7 @@ func (s *Kospi200Service) GetStocks() ([]models.Kospi200Stock, error) { Name: s.StkNm, CurPrc: absParseIntSafe(s.CurPrc), PredPreSig: s.PredPreSig, - PredPre: parseIntSafe(s.PredPre), + PredPre: signedChangeBySig(s.PredPre, s.PredPreSig), FluRt: parseFloatSafe(s.FluRt), Volume: absParseIntSafe(s.NowTrdeQty), Open: absParseIntSafe(s.OpenPric), diff --git a/services/theme_service.go b/services/theme_service.go index 2fa7932..bb41635 100644 --- a/services/theme_service.go +++ b/services/theme_service.go @@ -109,8 +109,8 @@ func (s *ThemeService) GetThemeStocks(themeCode, dateTp string) (*models.ThemeDe } var result struct { - FluRt string `json:"flu_rt"` - DtPrftRt string `json:"dt_prft_rt"` + FluRt string `json:"flu_rt"` + DtPrftRt string `json:"dt_prft_rt"` ThemaCompStk []struct { StkCd string `json:"stk_cd"` StkNm string `json:"stk_nm"` @@ -136,7 +136,7 @@ func (s *ThemeService) GetThemeStocks(themeCode, dateTp string) (*models.ThemeDe Name: s.StkNm, CurPrc: absParseIntSafe(s.CurPrc), FluSig: s.FluSig, - PredPre: parseIntSafe(s.PredPre), + PredPre: signedChangeBySig(s.PredPre, s.FluSig), FluRt: parseFloatSafe(strings.TrimPrefix(s.FluRt, "+")), }) }