package main import ( "log" "net/http" "stocksearch/config" "stocksearch/handlers" "stocksearch/middleware" "stocksearch/models" "stocksearch/services" ws "stocksearch/websocket" ) func main() { // 환경변수 로딩 config.Load() // 키움증권 토큰 발급 (서버 시작 시 즉시 실행) tokenSvc := services.GetTokenService() if err := tokenSvc.Start(); err != nil { log.Fatalf("토큰 발급 실패: %v\n키움증권 API 키를 .env 파일에 설정해주세요.", err) } // 체결강도 상승 감지 스캐너 시작 (08:00 KST 이후 10초 주기) services.GetScannerService().Start() // 종목 리스트 백그라운드 로딩 (검색용) services.GetSearchService().Load() // WebSocket Hub 시작 (내부에서 키움 WS 클라이언트 초기화) hub := ws.NewHub() go hub.Run() // 키움 WS 실시간 연결 시작 (Hub 이벤트 루프 실행 후) if err := hub.StartKiwoomWS(); err != nil { log.Printf("키움 WS 초기 연결 실패: %v (자동 재연결 시도)", err) } // 서비스 추가 sessionSvc := services.GetSessionService() watchlistSvc := services.GetWatchlistService() autoTradeSvc := services.GetAutoTradeService() // 스캐너 구독 종목 → WebSocket 내부 구독 연결 services.GetScannerService().SetSubscribeCallback(func(codes []string) { hub.SubscribeInternal(codes) }) // 자동매매 로그 → WebSocket 브로드캐스트 연결 autoTradeSvc.SetLogBroadcaster(func(l models.AutoTradeLog) { hub.BroadcastTradeLog(l) }) // 핸들러 초기화 pageHandler := handlers.NewPageHandler() stockHandler := handlers.NewStockHandler(watchlistSvc) wsHandler := handlers.NewWSHandler(hub) authHandler := handlers.NewAuthHandler(sessionSvc) orderHandler := handlers.NewOrderHandler() autoTradeHandler := handlers.NewAutoTradeHandler(autoTradeSvc) // 라우터 설정 (Go 1.22 패턴 매칭) mux := http.NewServeMux() // --- 인증 라우트 --- mux.HandleFunc("GET /login", authHandler.LoginPage) mux.HandleFunc("POST /login", authHandler.Login) mux.HandleFunc("POST /logout", authHandler.Logout) // --- 페이지 라우트 --- mux.HandleFunc("GET /", pageHandler.IndexPage) mux.HandleFunc("GET /theme", pageHandler.ThemePage) mux.HandleFunc("GET /kospi200", pageHandler.Kospi200Page) mux.HandleFunc("GET /asset", pageHandler.AssetPage) mux.HandleFunc("GET /autotrade", pageHandler.AutoTradePage) mux.HandleFunc("GET /stock/{code}", pageHandler.StockDetailPage) // --- REST API 라우트 --- mux.HandleFunc("GET /api/stock/{code}", stockHandler.GetCurrentPrice) mux.HandleFunc("GET /api/stock/{code}/chart", stockHandler.GetDailyChart) mux.HandleFunc("GET /api/scanner/status", stockHandler.GetScannerStatus) mux.HandleFunc("POST /api/scanner/toggle", stockHandler.ToggleScanner) mux.HandleFunc("GET /api/signal", stockHandler.GetSignals) mux.HandleFunc("GET /api/watchlist-signal", stockHandler.GetWatchlistSignals) mux.HandleFunc("GET /api/indices", stockHandler.GetIndices) mux.HandleFunc("GET /api/search", stockHandler.Search) mux.HandleFunc("GET /api/disclosure", stockHandler.GetDisclosures) mux.HandleFunc("GET /api/news", stockHandler.GetNews) mux.HandleFunc("GET /api/kospi200", stockHandler.GetKospi200) mux.HandleFunc("GET /api/themes", stockHandler.GetThemes) mux.HandleFunc("GET /api/themes/{code}", stockHandler.GetThemeStocks) mux.HandleFunc("GET /api/watchlist", stockHandler.GetWatchlist) mux.HandleFunc("POST /api/watchlist", stockHandler.AddWatchlist) mux.HandleFunc("DELETE /api/watchlist/{code}", stockHandler.RemoveWatchlist) // --- 주문/계좌 API 라우트 --- mux.HandleFunc("POST /api/order/buy", orderHandler.Buy) mux.HandleFunc("POST /api/order/sell", orderHandler.Sell) mux.HandleFunc("PUT /api/order/modify", orderHandler.Modify) mux.HandleFunc("DELETE /api/order", orderHandler.Cancel) mux.HandleFunc("GET /api/account/balance", orderHandler.GetBalance) mux.HandleFunc("GET /api/account/pending", orderHandler.GetPending) mux.HandleFunc("GET /api/account/history", orderHandler.GetHistory) mux.HandleFunc("GET /api/account/deposit", orderHandler.GetDeposit) mux.HandleFunc("GET /api/account/orderable", orderHandler.GetOrderable) // --- 자동매매 API 라우트 --- mux.HandleFunc("GET /api/autotrade/status", autoTradeHandler.GetStatus) mux.HandleFunc("GET /api/autotrade/rules", autoTradeHandler.GetRules) mux.HandleFunc("POST /api/autotrade/rules", autoTradeHandler.AddRule) mux.HandleFunc("PUT /api/autotrade/rules/{id}", autoTradeHandler.UpdateRule) mux.HandleFunc("DELETE /api/autotrade/rules/{id}", autoTradeHandler.DeleteRule) mux.HandleFunc("POST /api/autotrade/rules/{id}/toggle", autoTradeHandler.ToggleRule) mux.HandleFunc("GET /api/autotrade/positions", autoTradeHandler.GetPositions) mux.HandleFunc("GET /api/autotrade/logs", autoTradeHandler.GetLogs) mux.HandleFunc("GET /api/autotrade/watch-source", autoTradeHandler.GetWatchSource) mux.HandleFunc("PUT /api/autotrade/watch-source", autoTradeHandler.SetWatchSource) mux.HandleFunc("POST /api/autotrade/start", autoTradeHandler.Start) mux.HandleFunc("POST /api/autotrade/stop", autoTradeHandler.Stop) mux.HandleFunc("POST /api/autotrade/emergency", autoTradeHandler.Emergency) // --- WebSocket 라우트 --- mux.HandleFunc("GET /ws", wsHandler.ServeWS) // --- 정적 파일 --- mux.Handle("GET /static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static")))) // 미들웨어 체인 적용 (Auth → Logger → Recovery 순) handler := middleware.Chain(mux, middleware.Recovery, middleware.Logger, middleware.Auth(sessionSvc)) addr := "0.0.0.0:" + config.App.ServerPort log.Printf("서버 시작: http://%s", addr) if err := http.ListenAndServe(addr, handler); err != nil { log.Fatalf("서버 실행 실패: %v", err) } }