208 lines
5.1 KiB
Go
208 lines
5.1 KiB
Go
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
|
|
} |