first commit

This commit is contained in:
hayato5246
2026-03-31 19:32:59 +09:00
commit d10b794c9f
78 changed files with 1671595 additions and 0 deletions

208
services/index_service.go Normal file
View 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
}