package services import ( "encoding/json" "fmt" "stocksearch/models" "time" ) // Kospi200Service 코스피200 종목 조회 서비스 type Kospi200Service struct { kiwoom *KiwoomClient cache *CacheService } var kospi200Svc *Kospi200Service // GetKospi200Service 코스피200 서비스 싱글턴 반환 func GetKospi200Service() *Kospi200Service { if kospi200Svc == nil { kospi200Svc = &Kospi200Service{ kiwoom: GetKiwoomClient(), cache: GetCacheService(), } } return kospi200Svc } // GetStocks ka20002: 코스피200 구성종목 전체 조회 (연속조회, 캐시 1분) func (s *Kospi200Service) GetStocks() ([]models.Kospi200Stock, error) { const cacheKey = "kospi200_stocks" if cached, ok := s.cache.Get(cacheKey); ok { if stocks, ok := cached.([]models.Kospi200Stock); ok { return stocks, nil } } var all []models.Kospi200Stock contYn, nextKey := "N", "" for { body, nextContYn, nextNextKey, err := s.kiwoom.postPaged( "ka20002", "/api/dostk/sect", map[string]string{ "mrkt_tp": "0", // 코스피 "inds_cd": "201", // KOSPI200 "stex_tp": "1", // KRX }, contYn, nextKey, ) if err != nil { return nil, fmt.Errorf("코스피200 조회 실패: %w", err) } var result struct { IndsStkpc []struct { StkCd string `json:"stk_cd"` StkNm string `json:"stk_nm"` CurPrc string `json:"cur_prc"` PredPreSig string `json:"pred_pre_sig"` PredPre string `json:"pred_pre"` FluRt string `json:"flu_rt"` NowTrdeQty string `json:"now_trde_qty"` OpenPric string `json:"open_pric"` HighPric string `json:"high_pric"` LowPric string `json:"low_pric"` } `json:"inds_stkpc"` ReturnCode int `json:"return_code"` ReturnMsg string `json:"return_msg"` } if err := json.Unmarshal(body, &result); err != nil { return nil, fmt.Errorf("코스피200 응답 파싱 실패: %w", err) } if result.ReturnCode != 0 { return nil, fmt.Errorf("코스피200 조회 실패: %s", result.ReturnMsg) } for _, s := range result.IndsStkpc { // 종목코드·종목명이 없는 행은 건너뜀 if s.StkCd == "" || s.StkNm == "" { continue } all = append(all, models.Kospi200Stock{ Code: s.StkCd, Name: s.StkNm, CurPrc: absParseIntSafe(s.CurPrc), PredPreSig: s.PredPreSig, PredPre: parseIntSafe(s.PredPre), FluRt: parseFloatSafe(s.FluRt), Volume: absParseIntSafe(s.NowTrdeQty), Open: absParseIntSafe(s.OpenPric), High: absParseIntSafe(s.HighPric), Low: absParseIntSafe(s.LowPric), }) } // 연속조회 종료 조건 if nextContYn != "Y" || nextNextKey == "" { break } contYn, nextKey = nextContYn, nextNextKey } s.cache.Set(cacheKey, all, time.Minute) return all, nil }