first commit
This commit is contained in:
208
services/index_service.go
Normal file
208
services/index_service.go
Normal file
@@ -0,0 +1,208 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// IndexQuote 지수 현재가 정보
|
||||
type IndexQuote struct {
|
||||
Name string `json:"name"`
|
||||
Price float64 `json:"price"`
|
||||
Change float64 `json:"change"` // 전일대비
|
||||
ChangeRate float64 `json:"changeRate"` // 등락률(%)
|
||||
}
|
||||
|
||||
// indexCache 지수 캐시 (5초 TTL)
|
||||
type indexCache struct {
|
||||
data []IndexQuote
|
||||
expiresAt time.Time
|
||||
}
|
||||
|
||||
var (
|
||||
indexSvcOnce sync.Once
|
||||
indexSvc *IndexService
|
||||
)
|
||||
|
||||
// IndexService 국내/해외 주요 지수 조회 서비스
|
||||
type IndexService struct {
|
||||
kiwoom *KiwoomClient
|
||||
httpClient *http.Client
|
||||
mu sync.Mutex
|
||||
cache *indexCache
|
||||
}
|
||||
|
||||
// GetIndexService 싱글턴 반환
|
||||
func GetIndexService() *IndexService {
|
||||
indexSvcOnce.Do(func() {
|
||||
indexSvc = &IndexService{
|
||||
kiwoom: GetKiwoomClient(),
|
||||
httpClient: &http.Client{Timeout: 5 * time.Second},
|
||||
}
|
||||
})
|
||||
return indexSvc
|
||||
}
|
||||
|
||||
// GetIndices 코스피·코스닥·다우·나스닥 현재가 반환 (5초 캐시)
|
||||
func (s *IndexService) GetIndices() ([]IndexQuote, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if s.cache != nil && time.Now().Before(s.cache.expiresAt) {
|
||||
return s.cache.data, nil
|
||||
}
|
||||
|
||||
quotes := make([]IndexQuote, 0, 4)
|
||||
|
||||
// 국내 지수: 코스피(001), 코스닥(101)
|
||||
domestic := []struct {
|
||||
name string
|
||||
mrktTp string
|
||||
indsCd string
|
||||
}{
|
||||
{"KOSPI", "0", "001"},
|
||||
{"KOSDAQ", "1", "101"},
|
||||
}
|
||||
for _, d := range domestic {
|
||||
q, err := s.fetchKiwoomIndex(d.name, d.mrktTp, d.indsCd)
|
||||
if err != nil {
|
||||
log.Printf("지수 조회 실패 (%s): %v", d.name, err)
|
||||
quotes = append(quotes, IndexQuote{Name: d.name})
|
||||
continue
|
||||
}
|
||||
quotes = append(quotes, q)
|
||||
}
|
||||
|
||||
// 해외 지수: 다우(^DJI), 나스닥(^IXIC)
|
||||
overseas, err := s.fetchYahooIndices()
|
||||
if err != nil {
|
||||
log.Printf("해외 지수 조회 실패: %v", err)
|
||||
quotes = append(quotes, IndexQuote{Name: "DOW"}, IndexQuote{Name: "NASDAQ"})
|
||||
} else {
|
||||
quotes = append(quotes, overseas...)
|
||||
}
|
||||
|
||||
s.cache = &indexCache{data: quotes, expiresAt: time.Now().Add(5 * time.Second)}
|
||||
return quotes, nil
|
||||
}
|
||||
|
||||
// fetchKiwoomIndex 키움 ka20001로 업종 현재가 조회
|
||||
func (s *IndexService) fetchKiwoomIndex(name, mrktTp, indsCd string) (IndexQuote, error) {
|
||||
body := map[string]string{
|
||||
"mrkt_tp": mrktTp,
|
||||
"inds_cd": indsCd,
|
||||
}
|
||||
raw, err := s.kiwoom.post("ka20001", "/api/dostk/sect", body)
|
||||
if err != nil {
|
||||
return IndexQuote{Name: name}, err
|
||||
}
|
||||
|
||||
var resp struct {
|
||||
CurPrc string `json:"cur_prc"`
|
||||
PredPre string `json:"pred_pre"`
|
||||
FluRt string `json:"flu_rt"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &resp); err != nil {
|
||||
return IndexQuote{Name: name}, fmt.Errorf("파싱 실패: %w", err)
|
||||
}
|
||||
|
||||
return IndexQuote{
|
||||
Name: name,
|
||||
Price: parseIndexFloat(resp.CurPrc),
|
||||
Change: parseIndexFloat(resp.PredPre),
|
||||
ChangeRate: parseIndexFloat(resp.FluRt),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// fetchYahooIndices Yahoo Finance v8 chart API로 다우·나스닥 조회 (순서 보장)
|
||||
func (s *IndexService) fetchYahooIndices() ([]IndexQuote, error) {
|
||||
targets := []struct {
|
||||
name string
|
||||
symbol string
|
||||
}{
|
||||
{"DOW", "^DJI"},
|
||||
{"NASDAQ", "^IXIC"},
|
||||
}
|
||||
|
||||
quotes := make([]IndexQuote, 0, len(targets))
|
||||
for _, t := range targets {
|
||||
q, err := s.fetchYahooChart(t.name, t.symbol)
|
||||
if err != nil {
|
||||
log.Printf("Yahoo Finance %s 조회 실패: %v", t.name, err)
|
||||
quotes = append(quotes, IndexQuote{Name: t.name}) // 실패 시 0값 플레이스홀더
|
||||
continue
|
||||
}
|
||||
quotes = append(quotes, q)
|
||||
}
|
||||
return quotes, nil
|
||||
}
|
||||
|
||||
// fetchYahooChart Yahoo Finance v8 chart API로 단일 지수 조회
|
||||
func (s *IndexService) fetchYahooChart(name, symbol string) (IndexQuote, error) {
|
||||
url := "https://query1.finance.yahoo.com/v8/finance/chart/" + symbol + "?interval=1d&range=1d"
|
||||
req, _ := http.NewRequest("GET", url, nil)
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0")
|
||||
|
||||
resp, err := s.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return IndexQuote{Name: name}, fmt.Errorf("%s 요청 실패: %w", name, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
data, _ := io.ReadAll(resp.Body)
|
||||
|
||||
var result struct {
|
||||
Chart struct {
|
||||
Result []struct {
|
||||
Meta struct {
|
||||
RegularMarketPrice float64 `json:"regularMarketPrice"`
|
||||
ChartPreviousClose float64 `json:"chartPreviousClose"`
|
||||
} `json:"meta"`
|
||||
} `json:"result"`
|
||||
} `json:"chart"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &result); err != nil {
|
||||
return IndexQuote{Name: name}, fmt.Errorf("%s 파싱 실패: %w", name, err)
|
||||
}
|
||||
if len(result.Chart.Result) == 0 {
|
||||
return IndexQuote{Name: name}, fmt.Errorf("%s 데이터 없음", name)
|
||||
}
|
||||
|
||||
meta := result.Chart.Result[0].Meta
|
||||
price := meta.RegularMarketPrice
|
||||
prev := meta.ChartPreviousClose
|
||||
change := price - prev
|
||||
var changeRate float64
|
||||
if prev != 0 {
|
||||
changeRate = (change / prev) * 100
|
||||
}
|
||||
return IndexQuote{
|
||||
Name: name,
|
||||
Price: price,
|
||||
Change: change,
|
||||
ChangeRate: changeRate,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// parseIndexFloat 키움 지수 문자열 → float64 (부호 포함)
|
||||
func parseIndexFloat(s string) float64 {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return 0
|
||||
}
|
||||
neg := strings.HasPrefix(s, "-")
|
||||
s = strings.TrimLeft(s, "+-")
|
||||
s = strings.ReplaceAll(s, ",", "")
|
||||
f, _ := strconv.ParseFloat(s, 64)
|
||||
if neg {
|
||||
return -f
|
||||
}
|
||||
return f
|
||||
}
|
||||
Reference in New Issue
Block a user