diff --git a/backend/internal/http/handlers.go b/backend/internal/http/handlers.go index 54b1079..cd3c21e 100644 --- a/backend/internal/http/handlers.go +++ b/backend/internal/http/handlers.go @@ -17,7 +17,9 @@ func NewRouter(svc service.EpisodeService) *gin.Engine { c.JSON(http.StatusOK, gin.H{"status": "ok"}) }) - r.GET("/current", func(c *gin.Context) { + v1 := r.Group("/v1") + + v1.GET("/current", func(c *gin.Context) { cur, err := svc.GetCurrent(c.Request.Context()) if err != nil { if err == repo.ErrNotFound { @@ -32,10 +34,10 @@ func NewRouter(svc service.EpisodeService) *gin.Engine { type setCurrentReq struct { ID int64 `json:"id" binding:"required"` - StartTime string `json:"start_time" binding:"required"` // HH:MM or HH:MM:SS + StartTime string `json:"start_time" binding:"required"` } - r.POST("/current", func(c *gin.Context) { + v1.POST("/current", func(c *gin.Context) { var req setCurrentReq if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid payload"}) diff --git a/backend/internal/service/episode_service.go b/backend/internal/service/episode_service.go index 5758cae..e046d19 100644 --- a/backend/internal/service/episode_service.go +++ b/backend/internal/service/episode_service.go @@ -3,13 +3,13 @@ package service import ( "context" "errors" - "strings" + "regexp" "time" "watch-party-backend/internal/repo" ) -var ErrInvalidTime = errors.New("invalid start_time (expected HH:MM or HH:MM:SS)") +var ErrInvalidTime = errors.New("invalid start_time (expected HH:MM:SS)") type EpisodeService interface { GetCurrent(ctx context.Context) (repo.Episode, error) @@ -31,32 +31,27 @@ func (s *episodeService) GetCurrent(ctx context.Context) (repo.Episode, error) { } func (s *episodeService) SetCurrent(ctx context.Context, id int64, start string) (repo.Episode, error) { - // normalize and validate time as HH:MM:SS - normalized, err := normalizeHHMMSS(start) - if err != nil { + // Strict HH:MM:SS (24h, zero-padded) — e.g., 20:07:05 + if !isHHMMSS(start) { return repo.Episode{}, ErrInvalidTime } c, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() - // do the transaction - if err := s.repo.SetCurrent(c, id, normalized); err != nil { + if err := s.repo.SetCurrent(c, id, start); err != nil { return repo.Episode{}, err } // return the new current row return s.repo.GetCurrent(c) } -func normalizeHHMMSS(s string) (string, error) { - s = strings.TrimSpace(s) - if len(s) == 5 { // HH:MM → append seconds - s += ":00" +var hhmmss = regexp.MustCompile(`^(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d$`) + +func isHHMMSS(s string) bool { + if !hhmmss.MatchString(s) { + return false } - // Strict parse/format to 24h - t, err := time.Parse("15:04:05", s) - if err != nil { - return "", err - } - return t.Format("15:04:05"), nil + _, err := time.Parse("15:04:05", s) + return err == nil } diff --git a/frontend/src/components/Timer.tsx b/frontend/src/components/Timer.tsx index 6f43aae..de79772 100644 --- a/frontend/src/components/Timer.tsx +++ b/frontend/src/components/Timer.tsx @@ -2,7 +2,7 @@ import { useEffect, useMemo, useRef, useState } from "react"; // ===== Config & fallbacks ===== const TIMEZONE = "Asia/Tokyo"; // JST (UTC+09) -const API_URL = "/api/current"; +const API_URL = "/api/v1/current"; const FALLBACK_START_HOUR = 19; const FALLBACK_START_MINUTE = 25; const FALLBACK_END_SECONDS = 300;