package services import ( "encoding/json" "fmt" "stocksearch/models" "strings" "time" ) // ThemeService 테마 분석 서비스 type ThemeService struct { kiwoom *KiwoomClient cache *CacheService } var themeSvc *ThemeService // GetThemeService 테마 서비스 싱글턴 반환 func GetThemeService() *ThemeService { if themeSvc == nil { themeSvc = &ThemeService{ kiwoom: GetKiwoomClient(), cache: GetCacheService(), } } return themeSvc } // GetThemes ka90001: 테마그룹 목록 조회 (캐시 1분) // sortTp: "3"=상위등락률, "1"=상위기간수익률 func (s *ThemeService) GetThemes(dateTp, sortTp string) ([]models.ThemeGroup, error) { cacheKey := fmt.Sprintf("themes:%s:%s", dateTp, sortTp) if cached, ok := s.cache.Get(cacheKey); ok { if groups, ok := cached.([]models.ThemeGroup); ok { return groups, nil } } body, err := s.kiwoom.post("ka90001", "/api/dostk/thme", map[string]string{ "qry_tp": "0", // 전체검색 "stk_cd": "", "date_tp": dateTp, "thema_nm": "", "flu_pl_amt_tp": sortTp, "stex_tp": "1", // KRX }) if err != nil { return nil, fmt.Errorf("테마 목록 조회 실패: %w", err) } var result struct { ThemaGrp []struct { ThemaGrpCd string `json:"thema_grp_cd"` ThemaNm string `json:"thema_nm"` StkNum string `json:"stk_num"` FluSig string `json:"flu_sig"` FluRt string `json:"flu_rt"` RisingStkNum string `json:"rising_stk_num"` FallStkNum string `json:"fall_stk_num"` DtPrftRt string `json:"dt_prft_rt"` MainStk string `json:"main_stk"` } `json:"thema_grp"` ReturnCode int `json:"return_code"` ReturnMsg string `json:"return_msg"` } if err := json.Unmarshal(body, &result); err != nil { return nil, fmt.Errorf("테마 목록 파싱 실패: %w", err) } if result.ReturnCode != 0 { return nil, fmt.Errorf("테마 목록 조회 실패: %s", result.ReturnMsg) } groups := make([]models.ThemeGroup, 0, len(result.ThemaGrp)) for _, g := range result.ThemaGrp { groups = append(groups, models.ThemeGroup{ Code: g.ThemaGrpCd, Name: g.ThemaNm, StockCount: int(parseIntSafe(g.StkNum)), FluSig: g.FluSig, FluRt: parseFloatSafe(strings.TrimPrefix(g.FluRt, "+")), RisingCount: int(parseIntSafe(g.RisingStkNum)), FallCount: int(parseIntSafe(g.FallStkNum)), PeriodRt: parseFloatSafe(strings.TrimPrefix(g.DtPrftRt, "+")), MainStock: g.MainStk, }) } s.cache.Set(cacheKey, groups, time.Minute) return groups, nil } // GetThemeStocks ka90002: 테마구성종목 조회 (캐시 2분) func (s *ThemeService) GetThemeStocks(themeCode, dateTp string) (*models.ThemeDetail, error) { cacheKey := fmt.Sprintf("theme_stocks:%s:%s", themeCode, dateTp) if cached, ok := s.cache.Get(cacheKey); ok { if detail, ok := cached.(*models.ThemeDetail); ok { return detail, nil } } body, err := s.kiwoom.post("ka90002", "/api/dostk/thme", map[string]string{ "thema_grp_cd": themeCode, "date_tp": dateTp, "stex_tp": "1", // KRX }) if err != nil { return nil, fmt.Errorf("테마 구성종목 조회 실패: %w", err) } var result struct { FluRt string `json:"flu_rt"` DtPrftRt string `json:"dt_prft_rt"` ThemaCompStk []struct { StkCd string `json:"stk_cd"` StkNm string `json:"stk_nm"` CurPrc string `json:"cur_prc"` FluSig string `json:"flu_sig"` PredPre string `json:"pred_pre"` FluRt string `json:"flu_rt"` } `json:"thema_comp_stk"` ReturnCode int `json:"return_code"` ReturnMsg string `json:"return_msg"` } if err := json.Unmarshal(body, &result); err != nil { return nil, fmt.Errorf("테마 구성종목 파싱 실패: %w", err) } if result.ReturnCode != 0 { return nil, fmt.Errorf("테마 구성종목 조회 실패: %s", result.ReturnMsg) } stocks := make([]models.ThemeStock, 0, len(result.ThemaCompStk)) for _, s := range result.ThemaCompStk { stocks = append(stocks, models.ThemeStock{ Code: s.StkCd, Name: s.StkNm, CurPrc: absParseIntSafe(s.CurPrc), FluSig: s.FluSig, PredPre: parseIntSafe(s.PredPre), FluRt: parseFloatSafe(strings.TrimPrefix(s.FluRt, "+")), }) } detail := &models.ThemeDetail{ FluRt: parseFloatSafe(strings.TrimPrefix(result.FluRt, "+")), PeriodRt: parseFloatSafe(strings.TrimPrefix(result.DtPrftRt, "+")), Stocks: stocks, } s.cache.Set(cacheKey, detail, 2*time.Minute) return detail, nil }