"텔레그램_연동_기능_구현_및_설정_스키마_수정"

This commit is contained in:
2026-02-03 01:02:04 +09:00
parent 3ef50f9b3a
commit 675026d51e
17 changed files with 4386 additions and 10 deletions

View File

@@ -31,7 +31,7 @@ class HoldingSchema(BaseModel):
class Config: class Config:
from_attributes = True from_attributes = True
@router.get("/summary", response_model=AccountStatusSchema) @router.get("/balance", response_model=AccountStatusSchema)
async def get_account_summary(db: AsyncSession = Depends(get_db)): async def get_account_summary(db: AsyncSession = Depends(get_db)):
stmt = select(AccountStatus).where(AccountStatus.id == 1) stmt = select(AccountStatus).where(AccountStatus.id == 1)
result = await db.execute(stmt) result = await db.execute(stmt)

View File

@@ -16,7 +16,19 @@ class SettingsSchema(BaseModel):
appKey: str | None = None appKey: str | None = None
appSecret: str | None = None appSecret: str | None = None
accountNumber: str | None = None accountNumber: str | None = None
# Integrations
useTelegram: bool | None = None
telegramToken: str | None = None
telegramChatId: str | None = None
useNaverNews: bool | None = None
naverClientId: str | None = None
naverClientSecret: str | None = None
# Configs
kisApiDelayMs: int | None = None kisApiDelayMs: int | None = None
newsScrapIntervalMin: int | None = None
class Config: class Config:
from_attributes = True from_attributes = True
@@ -69,7 +81,18 @@ async def update_settings(payload: SettingsSchema, db: AsyncSession = Depends(ge
if payload.accountNumber is not None: if payload.accountNumber is not None:
settings.accountNumber = encrypt_str(payload.accountNumber) settings.accountNumber = encrypt_str(payload.accountNumber)
# Integrations
if payload.useTelegram is not None: settings.useTelegram = payload.useTelegram
if payload.telegramToken is not None: settings.telegramToken = payload.telegramToken
if payload.telegramChatId is not None: settings.telegramChatId = payload.telegramChatId
if payload.useNaverNews is not None: settings.useNaverNews = payload.useNaverNews
if payload.naverClientId is not None: settings.naverClientId = payload.naverClientId
if payload.naverClientSecret is not None: settings.naverClientSecret = payload.naverClientSecret
# Configs
if payload.kisApiDelayMs is not None: settings.kisApiDelayMs = payload.kisApiDelayMs if payload.kisApiDelayMs is not None: settings.kisApiDelayMs = payload.kisApiDelayMs
if payload.newsScrapIntervalMin is not None: settings.newsScrapIntervalMin = payload.newsScrapIntervalMin
await db.commit() await db.commit()
await db.refresh(settings) await db.refresh(settings)

View File

@@ -57,13 +57,13 @@ async def get_trade_history(limit: int = 100, db: AsyncSession = Depends(get_db)
result = await db.execute(stmt) result = await db.execute(stmt)
return result.scalars().all() return result.scalars().all()
@router.get("/reserved-orders", response_model=List[ReservedOrderSchema]) @router.get("/", response_model=List[ReservedOrderSchema])
async def get_reserved_orders(db: AsyncSession = Depends(get_db)): async def get_reserved_orders(db: AsyncSession = Depends(get_db)):
stmt = select(ReservedOrder) stmt = select(ReservedOrder)
result = await db.execute(stmt) result = await db.execute(stmt)
return result.scalars().all() return result.scalars().all()
@router.post("/reserved-orders") @router.post("/")
async def create_reserved_order(req: CreateReservedOrderRequest, db: AsyncSession = Depends(get_db)): async def create_reserved_order(req: CreateReservedOrderRequest, db: AsyncSession = Depends(get_db)):
import uuid import uuid
new_id = str(uuid.uuid4()) new_id = str(uuid.uuid4())
@@ -92,7 +92,7 @@ async def create_reserved_order(req: CreateReservedOrderRequest, db: AsyncSessio
await db.commit() await db.commit()
return {"id": new_id, "status": "MONITORING"} return {"id": new_id, "status": "MONITORING"}
@router.delete("/reserved-orders/{order_id}") @router.delete("/{order_id}")
async def delete_reserved_order(order_id: str, db: AsyncSession = Depends(get_db)): async def delete_reserved_order(order_id: str, db: AsyncSession = Depends(get_db)):
from sqlalchemy import delete from sqlalchemy import delete
await db.execute(delete(ReservedOrder).where(ReservedOrder.id == order_id)) await db.execute(delete(ReservedOrder).where(ReservedOrder.id == order_id))

View File

@@ -38,7 +38,7 @@ class UpdateGroupRequest(BaseModel):
# --- Endpoints --- # --- Endpoints ---
@router.get("/", response_model=List[WatchlistGroupSchema]) @router.get("/groups", response_model=List[WatchlistGroupSchema])
async def get_watchlists(db: AsyncSession = Depends(get_db)): async def get_watchlists(db: AsyncSession = Depends(get_db)):
# Load groups with items (Need relationship setup? # Load groups with items (Need relationship setup?
# Current models.py WatchlistGroup doesn't have `items` relationship defined explicitly in snippet provided? # Current models.py WatchlistGroup doesn't have `items` relationship defined explicitly in snippet provided?

Binary file not shown.

View File

@@ -6,6 +6,7 @@ from app.db.models import ApiSettings
from app.core.config import settings from app.core.config import settings
from app.services.kis_auth import kis_auth from app.services.kis_auth import kis_auth
from app.services.sync_service import sync_service from app.services.sync_service import sync_service
from app.services.telegram_service import telegram_service
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -48,12 +49,17 @@ async def run_startup_sequence():
logger.error(f" [FAILED] Authentication Failed: {e}") logger.error(f" [FAILED] Authentication Failed: {e}")
logger.error(" Please check your AppKey/Secret and ensure KIS API Server is reachable.") logger.error(" Please check your AppKey/Secret and ensure KIS API Server is reachable.")
# Phase 2.5: Telegram (Placeholder) # Phase 2.5: Telegram Integration
if settings_obj.useTelegram and settings_obj.telegramToken: if settings_obj.useTelegram and settings_obj.telegramToken and settings_obj.telegramChatId:
logger.info(">> [Phase 2] Telegram Token Found. Sending Startup Message...") logger.info(">> [Phase 2.5] Telegram Integration Enabled. Sending Startup Notification...")
# TODO: Implement Telegram Sender msg = "🚀 <b>BatchuKis 배취키스</b> 시스템이 시작되었습니다.\n자동매매 엔진이 가동 중입니다."
await telegram_service.send_message(
settings_obj.telegramToken,
settings_obj.telegramChatId,
msg
)
else: else:
logger.info(">> [Phase 2] Telegram Disabled or Token missing.") logger.info(">> [Phase 2.5] Telegram Disabled or Token/ChatID missing.")
# Phase 3: Data Sync (Master Stocks & Account) # Phase 3: Data Sync (Master Stocks & Account)
logger.info(">> [Phase 3-1] Syncing Account Data...") logger.info(">> [Phase 3-1] Syncing Account Data...")

View File

@@ -0,0 +1,33 @@
import httpx
import logging
from typing import Optional
logger = logging.getLogger(__name__)
class TelegramService:
def __init__(self):
self.base_url = "https://api.telegram.org/bot"
async def send_message(self, token: str, chat_id: str, text: str) -> bool:
if not token or not chat_id:
logger.warning("Telegram: Token or Chat ID is missing.")
return False
url = f"{self.base_url}{token}/sendMessage"
payload = {
"chat_id": chat_id,
"text": text,
"parse_mode": "HTML"
}
try:
async with httpx.AsyncClient() as client:
resp = await client.post(url, json=payload, timeout=10.0)
resp.raise_for_status()
logger.info(f"Telegram: Message sent successfully to {chat_id}")
return True
except Exception as e:
logger.error(f"Telegram: Failed to send message: {e}")
return False
telegram_service = TelegramService()

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.