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 }