유효하지 않은 KEY 가 설정되어 있어도, 일단 UI 페이지가 나오도록 수정.

This commit is contained in:
2026-07-17 00:37:58 +09:00
parent 5d4b9699ba
commit 4e9f912e5f
40 changed files with 93 additions and 12 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -28,12 +28,21 @@ class TokenManager:
self._rate_limiter = RateLimiter(requests_per_second=1.0)
self._lock = asyncio.Lock()
self._client = httpx.AsyncClient(timeout=10.0)
self._authenticated: bool = False
@property
def is_authenticated(self) -> bool:
return self._authenticated
@property
def is_vps(self) -> bool:
return settings.kis.server_mode == "vps"
async def _request_token(self) -> None:
if not settings.kis.app_key or not settings.kis.app_secret:
logger.debug("API 키가 설정되지 않음")
return
url = f"{get_base_url()}{_TOKEN_URL}"
payload = {
"grant_type": "client_credentials",
@@ -41,15 +50,22 @@ class TokenManager:
"appsecret": settings.kis.app_secret,
}
resp = await self._client.post(url, json=payload)
resp.raise_for_status()
data = resp.json()
if resp.status_code != 200 or "access_token" not in data:
logger.warning("토큰 발급 실패: %s", data.get("message", resp.status_code))
return
self._access_token = data["access_token"]
expires_in = int(data.get("expires_in", 7776000))
self._token_expires_at = datetime.now() + timedelta(seconds=expires_in)
self._authenticated = True
logger.info("REST access_token 발급 완료 (만료: %s)", self._token_expires_at)
async def _request_approval_key(self) -> None:
if not settings.kis.app_key or not settings.kis.app_secret:
return
url = f"{get_base_url()}{_APPROVAL_URL}"
payload = {
"grant_type": "client_credentials",
@@ -57,9 +73,12 @@ class TokenManager:
"secretkey": settings.kis.app_secret,
}
resp = await self._client.post(url, json=payload)
resp.raise_for_status()
data = resp.json()
if resp.status_code != 200 or "approval_key" not in data:
logger.warning("approval_key 발급 실패: %s", data.get("message", resp.status_code))
return
self._approval_key = data["approval_key"]
self._approval_expires_at = datetime.now() + timedelta(hours=23, minutes=50)
logger.info("WebSocket approval_key 발급 완료 (만료: %s)", self._approval_expires_at)
@@ -85,7 +104,8 @@ class TokenManager:
"appsecret": settings.kis.app_secret,
}
resp = await self._client.post(url, json=data, headers=headers)
resp.raise_for_status()
if resp.status_code != 200:
raise RuntimeError(f"hashkey 생성 실패: {resp.status_code}")
return resp.json()["HASH"]
def get_auth_headers(self, tr_id: str) -> dict[str, str]:

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -32,11 +32,20 @@ async def lifespan(app: FastAPI):
try:
await token_manager.get_access_token()
logger.info("KIS 인증 완료")
if token_manager.is_authenticated:
logger.info("KIS 인증 완료")
else:
logger.warning(
"KIS 인증 실패 - API 키를 확인하세요 (.env 파일). "
"UI는 표시되지만 시세 조회/매매 기능은 작동하지 않습니다."
)
except Exception as e:
logger.warning("KIS 인증 실패 (API 키를 확인하세요): %s", e)
logger.warning("KIS 인증 오류: %s", e)
start_scheduler()
if token_manager.is_authenticated:
start_scheduler()
else:
logger.info("인증 실패 - 스케줄러 미시작 (인증 후 수동 시작 가능)")
yield
@@ -67,7 +76,7 @@ templates = Jinja2Templates(directory="app/templates")
@app.get("/", response_class=HTMLResponse)
async def index(request: Request):
return templates.TemplateResponse("index.html", {"request": request})
return templates.TemplateResponse(request, "index.html")
@app.get("/health")

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -3,6 +3,7 @@ from __future__ import annotations
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from app.core.auth import token_manager
from app.core.database import get_db
from app.models.stock import Holding, PriceHistory, Strategy, Trade, Stock
from app.engine.scheduler import scheduler
@@ -31,6 +32,7 @@ def dashboard(db: Session = Depends(get_db)) -> dict:
})
return {
"authenticated": token_manager.is_authenticated,
"summary": {
"total_holdings": len(holdings),
"total_profit": total_profit,

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -20,9 +20,17 @@ class AccountService:
self._client = httpx.AsyncClient(timeout=10.0)
self._rate_limiter = RateLimiter(settings.rate_limit.requests_per_second)
def _check_auth(self) -> bool:
if not token_manager.is_authenticated:
logger.debug("API 인증되지 않음 - 잔고 조회 불가")
return False
return True
async def get_balance(self) -> dict:
if not self._check_auth():
return {"deposits": [], "stocks": []}
await self._rate_limiter.acquire()
token = await token_manager.get_access_token()
tr_id = _BALANCE_TR_IDS.get(settings.kis.server_mode, "VTTC0311R")
headers = token_manager.get_auth_headers(tr_id)
@@ -71,8 +79,10 @@ class AccountService:
return {"deposits": deposits, "stocks": stocks}
async def get_orderable_amount(self, stock_code: str, price: int) -> dict:
if not self._check_auth():
return {"orderable_amount": 0, "orderable_qty": 0}
await self._rate_limiter.acquire()
token = await token_manager.get_access_token()
headers = token_manager.get_auth_headers("VTTC0830R" if settings.kis.server_mode == "vps" else "TTTC0830R")
params = {

View File

@@ -22,9 +22,17 @@ class MarketDataService:
self._client = httpx.AsyncClient(timeout=10.0)
self._rate_limiter = RateLimiter(settings.rate_limit.requests_per_second)
def _check_auth(self) -> bool:
if not token_manager.is_authenticated:
logger.debug("API 인증되지 않음 - 시세 조회 불가")
return False
return True
async def get_current_price(self, stock_code: str) -> dict:
if not self._check_auth():
return {}
await self._rate_limiter.acquire()
token = await token_manager.get_access_token()
headers = token_manager.get_auth_headers("FHKST01010100")
params = {_FID_COND_MRKT_DIV_CODE: "J", _FID_INPUT_ISCD: stock_code}
url = f"{get_base_url()}{_PRICE_URL}"
@@ -54,8 +62,10 @@ class MarketDataService:
async def get_daily_chart(
self, stock_code: str, start_date: str, end_date: str, count: int = 30
) -> list[dict]:
if not self._check_auth():
return []
await self._rate_limiter.acquire()
token = await token_manager.get_access_token()
headers = token_manager.get_auth_headers("FHKST03010200")
params = {
_FID_COND_MRKT_DIV_CODE: "J",
@@ -88,8 +98,10 @@ class MarketDataService:
return result
async def get_orderbook(self, stock_code: str) -> dict:
if not self._check_auth():
return {}
await self._rate_limiter.acquire()
token = await token_manager.get_access_token()
headers = token_manager.get_auth_headers("FHKST01010200")
params = {_FID_COND_MRKT_DIV_CODE: "J", _FID_INPUT_ISCD: stock_code}
url = f"{get_base_url()}{_ORDERBOOK_URL}"

View File

@@ -23,6 +23,12 @@ class TradingService:
self._client = httpx.AsyncClient(timeout=10.0)
self._rate_limiter = RateLimiter(settings.rate_limit.requests_per_second)
def _check_auth(self) -> bool:
if not token_manager.is_authenticated:
logger.debug("API 인증되지 않음 - 매매 불가")
return False
return True
def _get_tr_id(self, side: str) -> str:
mode = settings.kis.server_mode
return _TR_IDS.get(mode, _TR_IDS["vps"]).get(side, "VTTC0802U")
@@ -35,6 +41,9 @@ class TradingService:
price: int = 0,
order_type: str = "00",
) -> dict:
if not self._check_auth():
return {"rt_cd": "-1", "msg": "API 인증되지 않음", "order_no": ""}
await self._rate_limiter.acquire()
tr_id = self._get_tr_id(side)
@@ -86,6 +95,9 @@ class TradingService:
async def modify_order(
self, order_no: str, stock_code: str, qty: int, price: int, order_type: str = "00"
) -> dict:
if not self._check_auth():
return {"rt_cd": "-1", "msg": "API 인증되지 않음"}
await self._rate_limiter.acquire()
tr_id = self._get_tr_id("modify")
@@ -123,6 +135,9 @@ class TradingService:
return result
async def cancel_order(self, order_no: str, stock_code: str, qty: int) -> dict:
if not self._check_auth():
return {"rt_cd": "-1", "msg": "API 인증되지 않음"}
await self._rate_limiter.acquire()
tr_id = self._get_tr_id("cancel")

View File

@@ -41,6 +41,9 @@
#ws-status { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 6px; }
#ws-status.connected { background: #3fb950; }
#ws-status.disconnected { background: #f85149; }
.alert { padding: 12px 16px; border-radius: 6px; margin-bottom: 16px; font-size: 13px; display: none; }
.alert.warning { background: #3a2a00; border: 1px solid #d29922; color: #e3b341; }
.alert.warning.show { display: block; }
</style>
</head>
<body>
@@ -53,6 +56,11 @@
</div>
<div class="container">
<div id="auth-alert" class="alert warning">
API 키가 설정되지 않았거나 유효하지 않습니다. .env 파일에 KIS_APP_KEY와 KIS_APP_SECRET을 입력하세요.
현재 UI 표시만 가능하며, 시세 조회 및 매매 기능은 작동하지 않습니다.
</div>
<div class="grid">
<div class="card">
<h3>총 투자금</h3>
@@ -136,6 +144,11 @@
try {
const res = await fetch(API + '/api/dashboard/');
const data = await res.json();
if (!data.authenticated) {
document.getElementById('auth-alert').classList.add('show');
}
const s = data.summary;
document.getElementById('total-invested').textContent = formatPrice(s.total_invested);