package handlers import ( "html/template" "log" "net/http" "stocksearch/middleware" "stocksearch/services" ) // PageHandler HTML 페이지 렌더링 핸들러 type PageHandler struct { // 페이지별 독립 템플릿 세트 (content 블록 충돌 방지) pageTemplates map[string]*template.Template stockService *services.StockService } // NewPageHandler 페이지별 템플릿을 분리 파싱하여 핸들러 초기화 func NewPageHandler() *PageHandler { base := "templates/layout/base.html" pages := []string{"index.html", "stock_detail.html", "theme.html", "kospi200.html", "asset.html", "autotrade.html"} fns := templateFuncs() tmplMap := make(map[string]*template.Template, len(pages)) for _, page := range pages { tmplMap[page] = template.Must( template.New("").Funcs(fns).ParseFiles(base, "templates/pages/"+page), ) } return &PageHandler{ pageTemplates: tmplMap, stockService: services.GetStockService(), } } // IndexPage GET / - 메인 페이지 func (h *PageHandler) IndexPage(w http.ResponseWriter, r *http.Request) { data := withLoggedIn(r, map[string]interface{}{ "Title": "주식 시세", "ActiveMenu": "시세", }) h.render(w, "index.html", data) } // ThemePage GET /theme - 테마 분석 페이지 func (h *PageHandler) ThemePage(w http.ResponseWriter, r *http.Request) { data := withLoggedIn(r, map[string]interface{}{ "Title": "테마 분석 - 주식 시세", "ActiveMenu": "테마", }) h.render(w, "theme.html", data) } // Kospi200Page GET /kospi200 - 코스피200 종목 페이지 func (h *PageHandler) Kospi200Page(w http.ResponseWriter, r *http.Request) { data := withLoggedIn(r, map[string]interface{}{ "Title": "코스피200 - 주식 시세", "ActiveMenu": "코스피200", }) h.render(w, "kospi200.html", data) } // AssetPage GET /asset - 자산 현황 페이지 func (h *PageHandler) AssetPage(w http.ResponseWriter, r *http.Request) { data := withLoggedIn(r, map[string]interface{}{ "Title": "자산 현황 - 주식 시세", "ActiveMenu": "자산", }) h.render(w, "asset.html", data) } // AutoTradePage GET /autotrade - 자동매매 페이지 func (h *PageHandler) AutoTradePage(w http.ResponseWriter, r *http.Request) { data := withLoggedIn(r, map[string]interface{}{ "Title": "자동매매 - 주식 시세", "ActiveMenu": "자동매매", }) h.render(w, "autotrade.html", data) } // StockDetailPage GET /stock/{code} - 종목 상세 페이지 func (h *PageHandler) StockDetailPage(w http.ResponseWriter, r *http.Request) { code := r.PathValue("code") price, err := h.stockService.GetCurrentPrice(code) if err != nil { log.Printf("현재가 조회 실패 [%s]: %v", code, err) http.Error(w, "종목 정보를 불러올 수 없습니다.", http.StatusInternalServerError) return } data := withLoggedIn(r, map[string]interface{}{ "Title": price.Name + " - 주식 시세", "Stock": price, "ActiveMenu": "", }) h.render(w, "stock_detail.html", data) } // withLoggedIn 템플릿 데이터에 LoggedIn 필드 추가 func withLoggedIn(r *http.Request, data map[string]interface{}) map[string]interface{} { data["LoggedIn"] = middleware.IsLoggedIn(r) return data } // render 템플릿 렌더링 헬퍼 func (h *PageHandler) render(w http.ResponseWriter, name string, data interface{}) { tmpl, ok := h.pageTemplates[name] if !ok { http.Error(w, "페이지를 찾을 수 없습니다.", http.StatusNotFound) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") if err := tmpl.ExecuteTemplate(w, "base.html", data); err != nil { log.Printf("템플릿 렌더링 실패 [%s]: %v", name, err) http.Error(w, "페이지를 표시할 수 없습니다.", http.StatusInternalServerError) } } // templateFuncs 템플릿에서 사용할 커스텀 함수 func templateFuncs() template.FuncMap { return template.FuncMap{ // 가격 포맷 (천 단위 콤마) "formatPrice": func(n int64) string { return formatNumber(n) }, // 등락률 부호 포함 문자열 "formatRate": func(f float64) string { if f >= 0 { return "+" + formatFloat(f) + "%" } return formatFloat(f) + "%" }, // 등락에 따른 CSS 색상 클래스 "priceClass": func(f float64) string { if f > 0 { return "text-red-500" // 상승: 빨강 (한국 관행) } else if f < 0 { return "text-blue-500" // 하락: 파랑 (한국 관행) } return "text-gray-500" }, // 체결강도 CSS 클래스 (100 기준) "cntrClass": func(f float64) string { if f > 100 { return "text-red-500" } else if f < 100 && f > 0 { return "text-blue-500" } return "text-gray-400" }, // 체결강도 포맷 "formatCntr": func(f float64) string { return formatFloat(f) }, } } func formatNumber(n int64) string { s := "" negative := n < 0 if negative { n = -n } for i, c := range reverse(itoa(n)) { if i > 0 && i%3 == 0 { s = "," + s } s = string(c) + s } if negative { s = "-" + s } return s } func formatFloat(f float64) string { if f < 0 { f = -f } // 소수점 2자리 result := "" intPart := int64(f) fracPart := int64((f - float64(intPart)) * 100) result = itoa(intPart) if fracPart < 10 { result += ".0" + itoa(fracPart) } else { result += "." + itoa(fracPart) } return result } func itoa(n int64) string { if n == 0 { return "0" } s := "" for n > 0 { s = string(rune('0'+n%10)) + s n /= 10 } return s } func reverse(s string) string { r := []rune(s) for i, j := 0, len(r)-1; i < j; i, j = i+1, j-1 { r[i], r[j] = r[j], r[i] } return string(r) }