first commit

This commit is contained in:
hayato5246
2026-03-31 19:32:59 +09:00
commit d10b794c9f
78 changed files with 1671595 additions and 0 deletions

288
handlers/stock_handler.go Normal file
View File

@@ -0,0 +1,288 @@
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})
}