백엔드 전체 구현 완료: 내부 서비스(Auth, Client, Realtime), API 엔드포인트 및 스케줄러 구현

This commit is contained in:
2026-02-02 23:55:07 +09:00
parent 03027d2206
commit 4f0cc05f39
22 changed files with 1279 additions and 23 deletions

View File

@@ -0,0 +1,117 @@
import httpx
from datetime import datetime, timedelta
from sqlalchemy import select
from app.db.database import SessionLocal
from app.db.models import ApiSettings
class KisAuth:
BASE_URL_REAL = "https://openapi.koreainvestment.com:9443"
# BASE_URL_VIRTUAL = "https://openapivts.koreainvestment.com:29443"
def __init__(self):
pass
async def get_access_token(self, db_session=None) -> str:
"""
Returns valid access token. Issues new one if expired or missing.
"""
local_session = False
if not db_session:
db_session = SessionLocal()
local_session = True
try:
# 1. Get Settings
stmt = select(ApiSettings).where(ApiSettings.id == 1)
result = await db_session.execute(stmt)
settings_obj = result.scalar_one_or_none()
if not settings_obj or not settings_obj.appKey or not settings_obj.appSecret:
raise ValueError("KIS API Credentials not configured.")
# 2. Check Expiry (Buffer 10 mins)
if settings_obj.accessToken and settings_obj.tokenExpiry:
if settings_obj.tokenExpiry > datetime.now() + timedelta(minutes=10):
return settings_obj.accessToken
# 3. Issue New Token
token_data = await self._issue_token(settings_obj.appKey, settings_obj.appSecret)
# 4. Save to DB
settings_obj.accessToken = token_data['access_token']
# expires_in is seconds (usually 86400)
settings_obj.tokenExpiry = datetime.now() + timedelta(seconds=int(token_data['expires_in']))
await db_session.commit()
return settings_obj.accessToken
except Exception as e:
await db_session.rollback()
raise e
finally:
if local_session:
await db_session.close()
async def _issue_token(self, app_key: str, app_secret: str) -> dict:
url = f"{self.BASE_URL_REAL}/oauth2/tokenP"
payload = {
"grant_type": "client_credentials",
"appkey": app_key,
"appsecret": app_secret
}
async with httpx.AsyncClient() as client:
resp = await client.post(url, json=payload, headers={"Content-Type": "application/json"})
resp.raise_for_status()
return resp.json()
async def get_approval_key(self, db_session=None) -> str:
"""
Returns WebSocket Approval Key. Issues new one if missing.
"""
local_session = False
if not db_session:
db_session = SessionLocal()
local_session = True
try:
stmt = select(ApiSettings).where(ApiSettings.id == 1)
result = await db_session.execute(stmt)
settings_obj = result.scalar_one_or_none()
if not settings_obj or not settings_obj.appKey or not settings_obj.appSecret:
raise ValueError("KIS API Credentials not configured.")
if settings_obj.websocketApprovalKey:
return settings_obj.websocketApprovalKey
# Issue New Key
approval_key = await self._issue_approval_key(settings_obj.appKey, settings_obj.appSecret)
settings_obj.websocketApprovalKey = approval_key
await db_session.commit()
return approval_key
except Exception as e:
await db_session.rollback()
raise e
finally:
if local_session:
await db_session.close()
async def _issue_approval_key(self, app_key: str, app_secret: str) -> str:
url = f"{self.BASE_URL_REAL}/oauth2/Approval"
payload = {
"grant_type": "client_credentials",
"appkey": app_key,
"secretkey": app_secret # Note: Parameter name difference
}
async with httpx.AsyncClient() as client:
resp = await client.post(url, json=payload, headers={"Content-Type": "application/json"})
resp.raise_for_status()
data = resp.json()
return data['approval_key']
kis_auth = KisAuth()