first commit
This commit is contained in:
129
services/token_service.go
Normal file
129
services/token_service.go
Normal file
@@ -0,0 +1,129 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user