package handlers import ( "encoding/json" "net/http" "stocksearch/models" "stocksearch/services" "strings" ) // StockHandler JSON REST API 핸들러 type StockHandler struct { stockService *services.StockService scannerService *services.ScannerService indexService *services.IndexService searchService *services.SearchService dartService *services.DartService newsService *services.NewsService themeService *services.ThemeService kospi200Service *services.Kospi200Service watchlistSvc *services.WatchlistService } // NewStockHandler 핸들러 초기화 func NewStockHandler(watchlistSvc *services.WatchlistService) *StockHandler { return &StockHandler{ stockService: services.GetStockService(), scannerService: services.GetScannerService(), indexService: services.GetIndexService(), searchService: services.GetSearchService(), dartService: services.GetDartService(), newsService: services.GetNewsService(), themeService: services.GetThemeService(), kospi200Service: services.GetKospi200Service(), watchlistSvc: watchlistSvc, } } // GetCurrentPrice GET /api/stock/{code} - 주식 현재가 JSON 반환 func (h *StockHandler) GetCurrentPrice(w http.ResponseWriter, r *http.Request) { code := strings.TrimPrefix(r.PathValue("code"), "") if code == "" { jsonError(w, "종목코드가 필요합니다.", http.StatusBadRequest) return } price, err := h.stockService.GetCurrentPrice(code) if err != nil { jsonError(w, err.Error(), http.StatusInternalServerError) return } jsonOK(w, price) } // GetChart GET /api/stock/{code}/chart?period=daily|minute1|minute5 - 차트 데이터 JSON 반환 func (h *StockHandler) GetDailyChart(w http.ResponseWriter, r *http.Request) { code := r.PathValue("code") if code == "" { jsonError(w, "종목코드가 필요합니다.", http.StatusBadRequest) return } period := r.URL.Query().Get("period") var ( candles []models.CandleData err error ) switch period { case "minute1": candles, err = h.stockService.GetMinuteChart(code, 1) case "minute5": candles, err = h.stockService.GetMinuteChart(code, 5) default: candles, err = h.stockService.GetDailyChart(code) } if err != nil { jsonError(w, err.Error(), http.StatusInternalServerError) return } jsonOK(w, candles) } // GetScannerStatus GET /api/scanner/status - 스캐너 활성화 상태 반환 func (h *StockHandler) GetScannerStatus(w http.ResponseWriter, r *http.Request) { jsonOK(w, map[string]bool{"enabled": h.scannerService.IsEnabled()}) } // ToggleScanner POST /api/scanner/toggle - 스캐너 ON/OFF 토글 후 새 상태 반환 func (h *StockHandler) ToggleScanner(w http.ResponseWriter, r *http.Request) { next := !h.scannerService.IsEnabled() h.scannerService.SetEnabled(next) jsonOK(w, map[string]bool{"enabled": next}) } // GetSignals GET /api/signal - 체결강도 상승 감지 시그널 종목 JSON 반환 func (h *StockHandler) GetSignals(w http.ResponseWriter, r *http.Request) { signals := h.scannerService.GetSignals() jsonOK(w, signals) } // GetWatchlistSignals GET /api/watchlist-signal?codes=005930,000660,... - 관심종목 복합 분석 JSON 반환 func (h *StockHandler) GetWatchlistSignals(w http.ResponseWriter, r *http.Request) { raw := r.URL.Query().Get("codes") if raw == "" { jsonOK(w, []services.SignalStock{}) return } parts := strings.Split(raw, ",") // 최대 20개 제한 codes := make([]string, 0, 20) for _, c := range parts { c = strings.TrimSpace(c) if len(c) == 6 { codes = append(codes, c) } if len(codes) >= 20 { break } } if len(codes) == 0 { jsonOK(w, []services.SignalStock{}) return } signals := h.scannerService.AnalyzeWatchlist(codes) if signals == nil { signals = []services.SignalStock{} } jsonOK(w, signals) } // Search GET /api/search?q=... - 종목명·코드 검색 결과 JSON 반환 func (h *StockHandler) Search(w http.ResponseWriter, r *http.Request) { q := r.URL.Query().Get("q") results := h.searchService.Search(q) if results == nil { results = []services.StockItem{} } jsonOK(w, results) } // GetIndices GET /api/indices - 코스피·코스닥·다우·나스닥 지수 JSON 반환 func (h *StockHandler) GetIndices(w http.ResponseWriter, r *http.Request) { quotes, err := h.indexService.GetIndices() if err != nil { jsonError(w, err.Error(), http.StatusInternalServerError) return } jsonOK(w, quotes) } // GetNews GET /api/news?name={stockName} - 종목 관련 최근 뉴스 JSON 반환 func (h *StockHandler) GetNews(w http.ResponseWriter, r *http.Request) { name := r.URL.Query().Get("name") if name == "" { jsonError(w, "종목명이 필요합니다.", http.StatusBadRequest) return } news, err := h.newsService.GetNews(name) if err != nil { jsonError(w, err.Error(), http.StatusInternalServerError) return } jsonOK(w, news) } // GetDisclosures GET /api/disclosure?code={stockCode} - DART 최근 공시 JSON 반환 func (h *StockHandler) GetDisclosures(w http.ResponseWriter, r *http.Request) { code := r.URL.Query().Get("code") if len(code) != 6 { jsonError(w, "올바른 종목코드를 입력해주세요.", http.StatusBadRequest) return } disclosures, err := h.dartService.GetDisclosures(code) if err != nil { jsonError(w, err.Error(), http.StatusInternalServerError) return } jsonOK(w, disclosures) } // GetKospi200 GET /api/kospi200 - 코스피200 구성종목 JSON 반환 func (h *StockHandler) GetKospi200(w http.ResponseWriter, r *http.Request) { stocks, err := h.kospi200Service.GetStocks() if err != nil { jsonError(w, err.Error(), http.StatusInternalServerError) return } jsonOK(w, stocks) } // GetThemes GET /api/themes?date=1&sort=3 - 테마그룹 목록 JSON 반환 // sort: 3=상위등락률(기본), 1=상위기간수익률 func (h *StockHandler) GetThemes(w http.ResponseWriter, r *http.Request) { dateTp := r.URL.Query().Get("date") if dateTp == "" { dateTp = "1" } sortTp := r.URL.Query().Get("sort") if sortTp == "" { sortTp = "3" } groups, err := h.themeService.GetThemes(dateTp, sortTp) if err != nil { jsonError(w, err.Error(), http.StatusInternalServerError) return } jsonOK(w, groups) } // GetThemeStocks GET /api/themes/{code}?date=1 - 테마구성종목 JSON 반환 func (h *StockHandler) GetThemeStocks(w http.ResponseWriter, r *http.Request) { code := r.PathValue("code") if code == "" { jsonError(w, "테마코드가 필요합니다.", http.StatusBadRequest) return } dateTp := r.URL.Query().Get("date") if dateTp == "" { dateTp = "1" } detail, err := h.themeService.GetThemeStocks(code, dateTp) if err != nil { jsonError(w, err.Error(), http.StatusInternalServerError) return } jsonOK(w, detail) } // GetWatchlist GET /api/watchlist — 관심종목 목록 JSON 반환 func (h *StockHandler) GetWatchlist(w http.ResponseWriter, r *http.Request) { list := h.watchlistSvc.GetAll() if list == nil { list = []services.WatchlistItem{} } jsonOK(w, list) } // AddWatchlist POST /api/watchlist — body: {code, name} 관심종목 추가 func (h *StockHandler) AddWatchlist(w http.ResponseWriter, r *http.Request) { var body struct { Code string `json:"code"` Name string `json:"name"` } if err := json.NewDecoder(r.Body).Decode(&body); err != nil { jsonError(w, "잘못된 요청입니다.", http.StatusBadRequest) return } if len(body.Code) != 6 { jsonError(w, "올바른 종목코드를 입력해주세요.", http.StatusBadRequest) return } if err := h.watchlistSvc.Add(body.Code, body.Name); err != nil { jsonError(w, err.Error(), http.StatusConflict) return } jsonOK(w, map[string]bool{"ok": true}) } // RemoveWatchlist DELETE /api/watchlist/{code} — 관심종목 삭제 func (h *StockHandler) RemoveWatchlist(w http.ResponseWriter, r *http.Request) { code := r.PathValue("code") if len(code) != 6 { jsonError(w, "올바른 종목코드를 입력해주세요.", http.StatusBadRequest) return } if err := h.watchlistSvc.Remove(code); err != nil { jsonError(w, err.Error(), http.StatusInternalServerError) return } jsonOK(w, map[string]bool{"ok": true}) } // --- JSON 응답 헬퍼 --- func jsonOK(w http.ResponseWriter, data interface{}) { w.Header().Set("Content-Type", "application/json; charset=utf-8") json.NewEncoder(w).Encode(data) } func jsonError(w http.ResponseWriter, message string, status int) { w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(status) json.NewEncoder(w).Encode(map[string]string{"error": message}) }