Files
stocksearch/services/token_service.go
2026-03-31 19:32:59 +09:00

130 lines
3.2 KiB
Go

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)
}
}
}