diff --git a/backend/app/api/endpoints/settings.py b/backend/app/api/endpoints/settings.py
index 700606a..8e9746a 100644
--- a/backend/app/api/endpoints/settings.py
+++ b/backend/app/api/endpoints/settings.py
@@ -6,6 +6,7 @@ from pydantic import BaseModel
from app.db.database import get_db
from app.db.models import ApiSettings
from app.services.kis_auth import kis_auth
+from app.workers.scheduler import service_watchdog_job
import logging
router = APIRouter()
@@ -97,6 +98,9 @@ async def update_settings(payload: SettingsSchema, db: AsyncSession = Depends(ge
await db.commit()
await db.refresh(settings)
+ # Trigger Startup-like sequence immediately
+ await service_watchdog_job()
+
# Return masked object check
# We return what was saved, but masked?
# Usually convention is to return updated state.
diff --git a/backend/app/core/startup.py b/backend/app/core/startup.py
index f7e75f0..d3650e1 100644
--- a/backend/app/core/startup.py
+++ b/backend/app/core/startup.py
@@ -10,14 +10,40 @@ from app.services.telegram_service import telegram_service
logger = logging.getLogger(__name__)
+async def check_kis_connectivity(db_session, settings_obj):
+ if not settings_obj.appKey or not settings_obj.appSecret:
+ logger.warning(">> [KIS] Credentials NOT FOUND in DB.")
+ return False
+
+ logger.info(">> [KIS] Attempting Authentication...")
+ try:
+ token = await kis_auth.get_access_token(db_session)
+ masked_token = token[:10] + "..." if token else "None"
+ logger.info(f" [OK] Access Token Valid ({masked_token})")
+ return True
+ except Exception as e:
+ logger.error(f" [FAILED] KIS Auth Failed: {e}")
+ return False
+
+async def check_telegram_connectivity(settings_obj, is_startup=False):
+ if settings_obj.useTelegram and settings_obj.telegramToken and settings_obj.telegramChatId:
+ logger.info(">> [Telegram] Integration Enabled.")
+ if is_startup:
+ msg = "π BatchuKis λ°°μ·¨ν€μ€ μμ€ν
μ΄ μμλμμ΅λλ€.\nμλλ§€λ§€ μμ§μ΄ κ°λ μ€μ
λλ€."
+ await telegram_service.send_message(settings_obj.telegramToken, settings_obj.telegramChatId, msg)
+ return True
+ else:
+ # logger.debug(">> [Telegram] Disabled or missing config.")
+ return False
+
async def run_startup_sequence():
"""
- Executes the Phase 1~4 startup sequence defined in ReadMe.md.
+ Executes the Phase 1~4 startup sequence.
"""
logger.info("=== Starting System Initialization Sequence ===")
async with SessionLocal() as db_session:
- # Phase 1: DB & Settings Load
+ # Phase 1: Load Settings
stmt = select(ApiSettings).where(ApiSettings.id == 1)
result = await db_session.execute(stmt)
settings_obj = result.scalar_one_or_none()
@@ -28,48 +54,15 @@ async def run_startup_sequence():
await db_session.commit()
logger.info("Created Default ApiSettings.")
- # Phase 2: KIS Connectivity
- if not settings_obj.appKey or not settings_obj.appSecret:
- logger.warning(">> [Phase 2] KIS Credentials (appKey/Secret) NOT FOUND in DB.")
- logger.warning(" Please configure them via the Settings Page.")
- logger.warning(" Skipping Token Issue & Realtime Connection.")
- else:
- logger.info(">> [Phase 2] KIS Credentials Found. Attempting Authentication...")
- try:
- # 1. Access Token
- token = await kis_auth.get_access_token(db_session)
- masked_token = token[:10] + "..." if token else "None"
- logger.info(f" [OK] Access Token Valid (Starts with: {masked_token})")
-
- # 2. Approval Key (Optional, lazy load usually, but good to check)
- # approval_key = await kis_auth.get_approval_key(db_session)
- # logger.info(" [OK] WebSocket Approval Key Issued.")
-
- except Exception as e:
- logger.error(f" [FAILED] Authentication Failed: {e}")
- logger.error(" Please check your AppKey/Secret and ensure KIS API Server is reachable.")
+ # Phase 2: KIS & Telegram
+ await check_kis_connectivity(db_session, settings_obj)
+ await check_telegram_connectivity(settings_obj, is_startup=True)
- # Phase 2.5: Telegram Integration
- if settings_obj.useTelegram and settings_obj.telegramToken and settings_obj.telegramChatId:
- logger.info(">> [Phase 2.5] Telegram Integration Enabled. Sending Startup Notification...")
- msg = "π BatchuKis λ°°μ·¨ν€μ€ μμ€ν
μ΄ μμλμμ΅λλ€.\nμλλ§€λ§€ μμ§μ΄ κ°λ μ€μ
λλ€."
- await telegram_service.send_message(
- settings_obj.telegramToken,
- settings_obj.telegramChatId,
- msg
- )
- else:
- logger.info(">> [Phase 2.5] Telegram Disabled or Token/ChatID missing.")
-
- # Phase 3: Data Sync (Master Stocks & Account)
- logger.info(">> [Phase 3-1] Syncing Account Data...")
+ # Phase 3: Data Sync
+ logger.info(">> [Phase 3] Syncing Account & Master Data...")
await sync_service.sync_account(db_session)
- logger.info(">> [Phase 3-2] Syncing Master Data (This may take a while)...")
from app.services.master_service import master_service
await master_service.sync_master_data(db_session)
- # Phase 4: Scheduler
- # (Scheduler is started in main.py)
-
logger.info("=== System Initialization Complete ===")
diff --git a/backend/app/workers/scheduler.py b/backend/app/workers/scheduler.py
index 0b83e9c..7505039 100644
--- a/backend/app/workers/scheduler.py
+++ b/backend/app/workers/scheduler.py
@@ -3,15 +3,60 @@ from app.core.market_schedule import market_schedule
from app.services.kis_client import kis_client
from app.services.realtime_manager import realtime_manager
from app.db.database import SessionLocal
-from app.db.models import StockItem, ReservedOrder
-from sqlalchemy import select, update
-import logging
-from datetime import datetime
+from app.core.startup import check_kis_connectivity, check_telegram_connectivity
+from app.db.models import StockItem, ReservedOrder, ApiSettings
logger = logging.getLogger(__name__)
scheduler = AsyncIOScheduler()
+class ServiceState:
+ last_keys_hash: str = ""
+ telegram_active: bool = False
+ naver_active: bool = False
+
+ @classmethod
+ def get_keys_hash(cls, settings: ApiSettings):
+ return f"{settings.appKey}{settings.appSecret}{settings.accountNumber}"
+
+async def service_watchdog_job():
+ """
+ Periodically checks settings and initiates services if they were newly enabled or changed.
+ """
+ async with SessionLocal() as db_session:
+ stmt = select(ApiSettings).where(ApiSettings.id == 1)
+ result = await db_session.execute(stmt)
+ settings_obj = result.scalar_one_or_none()
+
+ if not settings_obj: return
+
+ # 1. KIS Connectivity Watchdog
+ current_hash = ServiceState.get_keys_hash(settings_obj)
+ if current_hash != ServiceState.last_keys_hash and settings_obj.appKey:
+ logger.info("Watchdog: KIS Credentials Changed or Initialized. Re-authenticating...")
+ success = await check_kis_connectivity(db_session, settings_obj)
+ if success:
+ ServiceState.last_keys_hash = current_hash
+
+ # 2. Telegram Watchdog
+ if settings_obj.useTelegram and not ServiceState.telegram_active:
+ if settings_obj.telegramToken and settings_obj.telegramChatId:
+ logger.info("Watchdog: Telegram newly enabled. Sending activation message.")
+ from app.services.telegram_service import telegram_service
+ msg = "π μλ¦Ό μλΉμ€κ° νμ±νλμμ΅λλ€.\nμ΄μ λΆν° μ£Όμ κ±°λ μλ¦Όμ μ΄κ³³μΌλ‘ 보λ΄λ립λλ€."
+ await telegram_service.send_message(settings_obj.telegramToken, settings_obj.telegramChatId, msg)
+ ServiceState.telegram_active = True
+ elif not settings_obj.useTelegram:
+ ServiceState.telegram_active = False
+
+ # 3. Naver News Watchdog
+ if settings_obj.useNaverNews and not ServiceState.naver_active:
+ if settings_obj.naverClientId and settings_obj.naverClientSecret:
+ logger.info("Watchdog: Naver News newly enabled. (Logic placeholder)")
+ ServiceState.naver_active = True
+ elif not settings_obj.useNaverNews:
+ ServiceState.naver_active = False
+
async def market_check_job():
"""
Periodic check to ensure Realtime Manager is connected when market is open.
@@ -167,6 +212,7 @@ def start_scheduler():
scheduler.add_job(market_check_job, 'interval', minutes=5)
scheduler.add_job(news_scrap_job, 'interval', minutes=10)
scheduler.add_job(auto_trade_scan_job, 'interval', minutes=1)
+ scheduler.add_job(service_watchdog_job, 'interval', minutes=1) # Check settings every 1 min
scheduler.add_job(persist_market_data_job, 'interval', seconds=10) # Persist every 10s
scheduler.start()