130 lines
3.2 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|