package services import ( "bytes" "encoding/json" "fmt" "io" "log" "net/http" "stocksearch/config" "stocksearch/models" "sync" "time" ) // TokenService 키움증권 액세스 토큰 관리 (싱글턴) type TokenService struct { mu sync.RWMutex token *models.TokenResponse } var tokenSvc *TokenService var tokenOnce sync.Once // GetTokenService 토큰 서비스 싱글턴 반환 func GetTokenService() *TokenService { tokenOnce.Do(func() { tokenSvc = &TokenService{} }) return tokenSvc } // Start 서버 시작 시 토큰 발급 후 자동 갱신 고루틴 실행 func (s *TokenService) Start() error { if err := s.refresh(); err != nil { return fmt.Errorf("초기 토큰 발급 실패: %w", err) } go s.autoRefresh() return nil } // GetToken 현재 유효한 액세스 토큰 반환 func (s *TokenService) GetToken() string { s.mu.RLock() defer s.mu.RUnlock() if s.token == nil { return "" } return s.token.Token } // refresh 키움증권 API로 토큰 발급 func (s *TokenService) refresh() error { cfg := config.App body := map[string]string{ "grant_type": "client_credentials", "appkey": cfg.AppKey, "secretkey": cfg.AppSecret, } data, _ := json.Marshal(body) resp, err := http.Post( cfg.BaseURL+"/oauth2/token", "application/json;charset=UTF-8", bytes.NewReader(data), ) if err != nil { return fmt.Errorf("토큰 발급 요청 실패: %w", err) } defer resp.Body.Close() respBody, _ := io.ReadAll(resp.Body) if resp.StatusCode != http.StatusOK { return fmt.Errorf("토큰 발급 응답 오류: HTTP %d, 응답: %s", resp.StatusCode, string(respBody)) } // HTML 응답 시 서버 점검 중으로 판단 contentType := resp.Header.Get("Content-Type") if len(respBody) > 0 && respBody[0] == '<' || (contentType != "" && !bytes.Contains([]byte(contentType), []byte("json"))) { return fmt.Errorf("키움증권 서버 점검 중 (HTML 응답): %s", contentType) } var tokenResp models.TokenResponse if err := json.Unmarshal(respBody, &tokenResp); err != nil { return fmt.Errorf("토큰 응답 파싱 실패: %w", err) } if tokenResp.ReturnCode != 0 { return fmt.Errorf("토큰 발급 실패: %s", tokenResp.ReturnMsg) } // expires_dt: YYYYMMDDHHmmss 형식 파싱 expiresTime, err := time.ParseInLocation("20060102150405", tokenResp.ExpiresAt, time.Local) if err != nil { // 파싱 실패 시 12시간 후를 기본값으로 설정 expiresTime = time.Now().Add(12 * time.Hour) } tokenResp.ExpiresTime = expiresTime s.mu.Lock() s.token = &tokenResp s.mu.Unlock() log.Printf("토큰 발급 완료 (만료: %s)", expiresTime.Format("2006-01-02 15:04:05")) return nil } // autoRefresh 만료 1시간 전에 자동으로 토큰 갱신 func (s *TokenService) autoRefresh() { for { s.mu.RLock() expiresTime := s.token.ExpiresTime s.mu.RUnlock() // 만료 1시간 전에 갱신 refreshAt := expiresTime.Add(-1 * time.Hour) waitDuration := time.Until(refreshAt) if waitDuration < 0 { waitDuration = 10 * time.Second // 이미 만료됐으면 즉시 재시도 } time.Sleep(waitDuration) if err := s.refresh(); err != nil { log.Printf("토큰 자동 갱신 실패: %v (10초 후 재시도)", err) time.Sleep(10 * time.Second) } } }