156 lines
3.2 KiB
Go
156 lines
3.2 KiB
Go
package services
|
|
|
|
import (
|
|
"encoding/json"
|
|
"log"
|
|
"strings"
|
|
"sync"
|
|
"unicode"
|
|
)
|
|
|
|
// StockItem 검색용 종목 정보
|
|
type StockItem struct {
|
|
Code string `json:"code"`
|
|
Name string `json:"name"`
|
|
Market string `json:"market"`
|
|
}
|
|
|
|
// SearchService 종목 검색 서비스 (메모리 캐시)
|
|
type SearchService struct {
|
|
kiwoom *KiwoomClient
|
|
mu sync.RWMutex
|
|
stocks []StockItem // 전체 종목 리스트 캐시
|
|
}
|
|
|
|
var (
|
|
searchSvcOnce sync.Once
|
|
searchSvc *SearchService
|
|
)
|
|
|
|
// GetSearchService 싱글턴 반환
|
|
func GetSearchService() *SearchService {
|
|
searchSvcOnce.Do(func() {
|
|
searchSvc = &SearchService{kiwoom: GetKiwoomClient()}
|
|
})
|
|
return searchSvc
|
|
}
|
|
|
|
// Load 코스피·코스닥 전체 종목 목록을 키움 ka10099로 로딩
|
|
// 서버 시작 시 1회 호출 (고루틴)
|
|
func (s *SearchService) Load() {
|
|
go func() {
|
|
stocks, err := s.fetchAll()
|
|
if err != nil {
|
|
log.Printf("종목 리스트 로딩 실패: %v", err)
|
|
return
|
|
}
|
|
s.mu.Lock()
|
|
s.stocks = stocks
|
|
s.mu.Unlock()
|
|
log.Printf("종목 리스트 로딩 완료: %d개", len(stocks))
|
|
}()
|
|
}
|
|
|
|
// Search 종목명 또는 코드로 검색 (최대 10건)
|
|
func (s *SearchService) Search(q string) []StockItem {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
|
|
q = strings.TrimSpace(q)
|
|
if q == "" || len(s.stocks) == 0 {
|
|
return nil
|
|
}
|
|
|
|
qLower := strings.ToLower(q)
|
|
isDigit := isAllDigit(q)
|
|
|
|
var results []StockItem
|
|
for _, st := range s.stocks {
|
|
var match bool
|
|
if isDigit {
|
|
// 숫자면 코드 전방일치
|
|
match = strings.HasPrefix(st.Code, q)
|
|
} else {
|
|
// 문자면 종목명 포함검색
|
|
match = strings.Contains(strings.ToLower(st.Name), qLower)
|
|
}
|
|
if match {
|
|
results = append(results, st)
|
|
if len(results) >= 10 {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return results
|
|
}
|
|
|
|
// fetchAll ka10099 연속조회로 코스피+코스닥 전체 종목 수집
|
|
func (s *SearchService) fetchAll() ([]StockItem, error) {
|
|
var all []StockItem
|
|
for _, mrkt := range []struct{ tp, name string }{
|
|
{"0", "KOSPI"},
|
|
{"10", "KOSDAQ"},
|
|
} {
|
|
items, err := s.fetchMarket(mrkt.tp, mrkt.name)
|
|
if err != nil {
|
|
log.Printf("종목 리스트 조회 실패 (%s): %v", mrkt.name, err)
|
|
continue
|
|
}
|
|
all = append(all, items...)
|
|
}
|
|
return all, nil
|
|
}
|
|
|
|
// fetchMarket 특정 시장의 전체 종목을 연속조회로 수집
|
|
func (s *SearchService) fetchMarket(mrktTp, marketName string) ([]StockItem, error) {
|
|
body := map[string]string{"mrkt_tp": mrktTp}
|
|
|
|
var items []StockItem
|
|
contYn := "N"
|
|
nextKey := ""
|
|
|
|
for {
|
|
raw, respContYn, respNextKey, err := s.kiwoom.postPaged("ka10099", "/api/dostk/stkinfo", body, contYn, nextKey)
|
|
if err != nil {
|
|
return items, err
|
|
}
|
|
|
|
var resp struct {
|
|
List []struct {
|
|
Code string `json:"code"`
|
|
Name string `json:"name"`
|
|
State string `json:"state"`
|
|
} `json:"list"`
|
|
}
|
|
if err := json.Unmarshal(raw, &resp); err != nil {
|
|
return items, err
|
|
}
|
|
|
|
for _, it := range resp.List {
|
|
items = append(items, StockItem{
|
|
Code: it.Code,
|
|
Name: it.Name,
|
|
Market: marketName,
|
|
})
|
|
}
|
|
|
|
// 연속조회 종료 조건
|
|
if respContYn != "Y" || respNextKey == "" {
|
|
break
|
|
}
|
|
contYn = "Y"
|
|
nextKey = respNextKey
|
|
}
|
|
|
|
return items, nil
|
|
}
|
|
|
|
func isAllDigit(s string) bool {
|
|
for _, r := range s {
|
|
if !unicode.IsDigit(r) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|