package seriessetmaster import ( "bytes" "encoding/base64" "fmt" "image" _ "image/gif" _ "image/jpeg" "image/png" "io" "net/http" "os" "path/filepath" "strconv" "strings" "cardverse/internal/pkg/response" "cardverse/internal/pkg/utils" "cardverse/internal/pkg/validator" "github.com/chai2010/webp" "github.com/gin-gonic/gin" ) type Handler struct { service Service } func NewHandler(service Service) *Handler { return &Handler{service: service} } func (h *Handler) GetAllSeries(c *gin.Context) { var query ListSeriesQuery if err := c.ShouldBindQuery(&query); err != nil { response.Error(c, http.StatusBadRequest, "query parameter tidak valid", validator.TranslateError(err)) return } list, err := h.service.GetAllSeries(query) if err != nil { response.Error(c, http.StatusInternalServerError, "gagal mengambil data series", err.Error()) return } response.Success(c, http.StatusOK, "berhasil mengambil data series", ToSeriesSetMasterResponseList(list)) } func (h *Handler) GetAllSets(c *gin.Context) { var query ListSetQuery if err := c.ShouldBindQuery(&query); err != nil { response.Error(c, http.StatusBadRequest, "query parameter tidak valid", validator.TranslateError(err)) return } list, err := h.service.GetAllSets(query) if err != nil { response.Error(c, http.StatusInternalServerError, "gagal mengambil data set", err.Error()) return } response.Success(c, http.StatusOK, "berhasil mengambil data set", ToSeriesSetMasterResponseList(list)) } func (h *Handler) GetByID(c *gin.Context) { id, err := parseID(c) if err != nil { response.Error(c, http.StatusBadRequest, "id tidak valid", []validator.FieldError{ {Field: "id", Message: "id harus berupa angka"}, }) return } m, err := h.service.GetByID(id) if err != nil { response.Error(c, http.StatusNotFound, err.Error(), nil) return } response.Success(c, http.StatusOK, "berhasil mengambil data", ToSeriesSetMasterResponse(*m)) } func (h *Handler) GetSetByCode(c *gin.Context) { code := c.Param("code") if code == "" { response.Error(c, http.StatusBadRequest, "set_code tidak boleh kosong", nil) return } m, err := h.service.GetSetByCode(code) if err != nil { response.Error(c, http.StatusNotFound, err.Error(), nil) return } response.Success(c, http.StatusOK, "berhasil mengambil data set", ToSeriesSetMasterResponse(*m)) } func (h *Handler) CreateSeries(c *gin.Context) { var req CreateSeriesRequest contentType := c.Request.Header.Get("Content-Type") if strings.Contains(contentType, "multipart/form-data") { req.Name = c.PostForm("name") if req.Name == "" { response.Error(c, http.StatusBadRequest, "name wajib diisi", nil) return } fileHeader, err := c.FormFile("image") if err == nil && fileHeader != nil { if fileHeader.Size > 5*1024*1024 { response.Error(c, http.StatusBadRequest, "ukuran file gambar maksimal 5MB", nil) return } srcFile, err := fileHeader.Open() if err == nil { defer srcFile.Close() img, _, err := image.Decode(srcFile) if err == nil { slug := utils.UnderscoreString(req.Name) uploadDir := "./public/images/series" _ = os.MkdirAll(uploadDir, os.ModePerm) filename := fmt.Sprintf("%s.webp", slug) filepathDst := filepath.Join(uploadDir, filename) out, err := os.Create(filepathDst) if err == nil { defer out.Close() if err := webp.Encode(out, img, &webp.Options{Lossless: false, Quality: 90}); err == nil { req.Image = fmt.Sprintf("/images/series/%s", filename) } } } } } } else { if err := c.ShouldBindJSON(&req); err != nil { response.Error(c, http.StatusBadRequest, "input tidak valid", validator.TranslateError(err)) return } } created, err := h.service.CreateSeries(req) if err != nil { response.Error(c, http.StatusBadRequest, err.Error(), nil) return } response.Success(c, http.StatusCreated, "series berhasil dibuat", ToSeriesSetMasterResponse(*created)) } func (h *Handler) CreateSet(c *gin.Context) { var req CreateSetRequest contentType := c.Request.Header.Get("Content-Type") if strings.Contains(contentType, "multipart/form-data") { if parentIDStr := c.PostForm("parent_id"); parentIDStr != "" { if pID, err := strconv.ParseUint(parentIDStr, 10, 64); err == nil { req.ParentID = uint(pID) } } req.SetCode = c.PostForm("set_code") req.SetName = c.PostForm("set_name") if req.ParentID == 0 || req.SetCode == "" || req.SetName == "" { response.Error(c, http.StatusBadRequest, "parent_id, set_code, dan set_name wajib diisi", nil) return } parentSeries, err := h.service.GetByID(req.ParentID) if err != nil || parentSeries == nil || parentSeries.ParentID != nil { response.Error(c, http.StatusBadRequest, "parent_id tidak ditemukan atau bukan merupakan series", nil) return } seriesSlug := utils.Slugify(parentSeries.SeriesName) uploadDir := fmt.Sprintf("./public/images/%s/%s", seriesSlug, req.SetCode) // Handle file 'image' fileHeader, err := c.FormFile("image") if err == nil && fileHeader != nil { if fileHeader.Size <= 5*1024*1024 { srcFile, err := fileHeader.Open() if err == nil { defer srcFile.Close() img, _, err := image.Decode(srcFile) if err == nil { _ = os.MkdirAll(uploadDir, os.ModePerm) imgSlug := utils.UnderscoreString(req.SetName) filename := fmt.Sprintf("%s.webp", imgSlug) filepathDst := filepath.Join(uploadDir, filename) out, err := os.Create(filepathDst) if err == nil { defer out.Close() if err := webp.Encode(out, img, &webp.Options{Lossless: false, Quality: 90}); err == nil { req.Image = fmt.Sprintf("/images/%s/%s/%s", seriesSlug, req.SetCode, filename) } } } } } } // Handle file 'logo' logoHeader, err := c.FormFile("logo") if err == nil && logoHeader != nil { if logoHeader.Size <= 5*1024*1024 { _ = os.MkdirAll(uploadDir, os.ModePerm) origFilename := logoHeader.Filename ext := filepath.Ext(origFilename) baseName := strings.TrimSuffix(origFilename, ext) if baseName == "" { baseName = req.SetCode } var logoFilename string if (len(req.SetCode) > 5 || len(baseName) > 5) && (strings.Contains(req.SetCode, "-P") || strings.Contains(baseName, "-P")) { logoFilename = "PROMO.svg" } else { logoFilename = fmt.Sprintf("%s.svg", baseName) } filepathDstLogo := filepath.Join(uploadDir, logoFilename) srcLogo, err := logoHeader.Open() if err == nil { defer srcLogo.Close() logoBytes, err := io.ReadAll(srcLogo) if err == nil { checkLen := len(logoBytes) if checkLen > 512 { checkLen = 512 } isSVG := strings.EqualFold(ext, ".svg") || strings.Contains(strings.ToLower(string(logoBytes[:checkLen])), "`, w, h, w, h, w, h, b64) if err := os.WriteFile(filepathDstLogo, []byte(svgContent), 0644); err == nil { req.Logo = fmt.Sprintf("/images/%s/%s/%s", seriesSlug, req.SetCode, logoFilename) } } } else { if err := c.SaveUploadedFile(logoHeader, filepathDstLogo); err == nil { req.Logo = fmt.Sprintf("/images/%s/%s/%s", seriesSlug, req.SetCode, logoFilename) } } } } } } } } else { if err := c.ShouldBindJSON(&req); err != nil { response.Error(c, http.StatusBadRequest, "input tidak valid", validator.TranslateError(err)) return } } created, err := h.service.CreateSet(req) if err != nil { response.Error(c, http.StatusBadRequest, err.Error(), nil) return } response.Success(c, http.StatusCreated, "set berhasil dibuat", ToSeriesSetMasterResponse(*created)) } func (h *Handler) UpdateSeries(c *gin.Context) { id, err := parseID(c) if err != nil { response.Error(c, http.StatusBadRequest, "id tidak valid", []validator.FieldError{ {Field: "id", Message: "id harus berupa angka"}, }) return } var req UpdateSeriesRequest contentType := c.Request.Header.Get("Content-Type") if strings.Contains(contentType, "multipart/form-data") { req.Name = c.PostForm("name") fileHeader, err := c.FormFile("image") if err == nil && fileHeader != nil { if fileHeader.Size > 5*1024*1024 { response.Error(c, http.StatusBadRequest, "ukuran file gambar maksimal 5MB", nil) return } srcFile, err := fileHeader.Open() if err != nil { response.Error(c, http.StatusInternalServerError, "gagal membaca file gambar series", err.Error()) return } defer srcFile.Close() img, _, err := image.Decode(srcFile) if err != nil { response.Error(c, http.StatusBadRequest, "file yang diunggah bukan format gambar yang valid", err.Error()) return } seriesName := req.Name if seriesName == "" { m, err := h.service.GetByID(id) if err == nil && m != nil { seriesName = m.SeriesName } } slug := utils.UnderscoreString(seriesName) uploadDir := "./public/images/series" if err := os.MkdirAll(uploadDir, os.ModePerm); err != nil { response.Error(c, http.StatusInternalServerError, "gagal membuat direktori penyimpanan series", err.Error()) return } filename := fmt.Sprintf("%s.webp", slug) filepathDst := filepath.Join(uploadDir, filename) out, err := os.Create(filepathDst) if err != nil { response.Error(c, http.StatusInternalServerError, "gagal membuat file gambar series", err.Error()) return } defer out.Close() if err := webp.Encode(out, img, &webp.Options{Lossless: false, Quality: 90}); err != nil { response.Error(c, http.StatusInternalServerError, "gagal mengompresi gambar ke format WebP", err.Error()) return } req.Image = fmt.Sprintf("/images/series/%s", filename) } } else { if err := c.ShouldBindJSON(&req); err != nil { response.Error(c, http.StatusBadRequest, "input tidak valid", validator.TranslateError(err)) return } } updated, err := h.service.UpdateSeries(id, req) if err != nil { response.Error(c, http.StatusBadRequest, err.Error(), nil) return } response.Success(c, http.StatusOK, "series berhasil diupdate", ToSeriesSetMasterResponse(*updated)) } func (h *Handler) UpdateSet(c *gin.Context) { id, err := parseID(c) if err != nil { response.Error(c, http.StatusBadRequest, "id tidak valid", []validator.FieldError{ {Field: "id", Message: "id harus berupa angka"}, }) return } mOld, err := h.service.GetByID(id) if err != nil || mOld == nil { response.Error(c, http.StatusNotFound, "data set tidak ditemukan", nil) return } var req UpdateSetRequest contentType := c.Request.Header.Get("Content-Type") if strings.Contains(contentType, "multipart/form-data") { req.SetCode = c.PostForm("set_code") req.SetName = c.PostForm("set_name") if parentIDStr := c.PostForm("parent_id"); parentIDStr != "" { if pID, err := strconv.ParseUint(parentIDStr, 10, 64); err == nil { uintPID := uint(pID) req.ParentID = &uintPID } } setCode := req.SetCode if setCode == "" { setCode = mOld.SetCode } setName := req.SetName if setName == "" { setName = mOld.SetName } seriesName := mOld.SeriesName if req.ParentID != nil { parentSeries, err := h.service.GetByID(*req.ParentID) if err == nil && parentSeries != nil && parentSeries.ParentID == nil { seriesName = parentSeries.SeriesName } } seriesSlug := utils.Slugify(seriesName) uploadDir := fmt.Sprintf("./public/images/%s/%s", seriesSlug, setCode) // 1. Handle file 'image' (WebP format, nama file = utils.UnderscoreString(setName) + ".webp") fileHeader, err := c.FormFile("image") if err == nil && fileHeader != nil { if fileHeader.Size > 5*1024*1024 { response.Error(c, http.StatusBadRequest, "ukuran file gambar maksimal 5MB", nil) return } srcFile, err := fileHeader.Open() if err == nil { defer srcFile.Close() img, _, err := image.Decode(srcFile) if err == nil { _ = os.MkdirAll(uploadDir, os.ModePerm) imgSlug := utils.UnderscoreString(setName) filename := fmt.Sprintf("%s.webp", imgSlug) filepathDst := filepath.Join(uploadDir, filename) out, err := os.Create(filepathDst) if err == nil { defer out.Close() if err := webp.Encode(out, img, &webp.Options{Lossless: false, Quality: 90}); err == nil { req.Image = fmt.Sprintf("/images/%s/%s/%s", seriesSlug, setCode, filename) } } } } } // 2. Handle file 'logo' (Format SVG: file SVG disimpan langsung, JPG/PNG/GIF/WebP dikonversi ke SVG XML) logoHeader, err := c.FormFile("logo") if err == nil && logoHeader != nil { if logoHeader.Size > 5*1024*1024 { response.Error(c, http.StatusBadRequest, "ukuran file logo maksimal 5MB", nil) return } _ = os.MkdirAll(uploadDir, os.ModePerm) origFilename := logoHeader.Filename ext := filepath.Ext(origFilename) baseName := strings.TrimSuffix(origFilename, ext) if baseName == "" { baseName = setCode } var logoFilename string if (len(setCode) > 5 || len(baseName) > 5) && (strings.Contains(setCode, "-P") || strings.Contains(baseName, "-P")) { logoFilename = "PROMO.svg" } else { logoFilename = fmt.Sprintf("%s.svg", baseName) } filepathDstLogo := filepath.Join(uploadDir, logoFilename) srcLogo, err := logoHeader.Open() if err == nil { defer srcLogo.Close() logoBytes, err := io.ReadAll(srcLogo) if err == nil { checkLen := len(logoBytes) if checkLen > 512 { checkLen = 512 } isSVG := strings.EqualFold(ext, ".svg") || strings.Contains(strings.ToLower(string(logoBytes[:checkLen])), "`, w, h, w, h, w, h, b64) if err := os.WriteFile(filepathDstLogo, []byte(svgContent), 0644); err == nil { req.Logo = fmt.Sprintf("/images/%s/%s/%s", seriesSlug, setCode, logoFilename) } } } else { if err := c.SaveUploadedFile(logoHeader, filepathDstLogo); err == nil { req.Logo = fmt.Sprintf("/images/%s/%s/%s", seriesSlug, setCode, logoFilename) } } } } } } } else { if err := c.ShouldBindJSON(&req); err != nil { response.Error(c, http.StatusBadRequest, "input tidak valid", validator.TranslateError(err)) return } } updated, err := h.service.UpdateSet(id, req) if err != nil { response.Error(c, http.StatusBadRequest, err.Error(), nil) return } response.Success(c, http.StatusOK, "set berhasil diupdate", ToSeriesSetMasterResponse(*updated)) } func (h *Handler) Delete(c *gin.Context) { id, err := parseID(c) if err != nil { response.Error(c, http.StatusBadRequest, "id tidak valid", []validator.FieldError{ {Field: "id", Message: "id harus berupa angka"}, }) return } if err := h.service.Delete(id); err != nil { response.Error(c, http.StatusNotFound, err.Error(), nil) return } response.Success(c, http.StatusOK, "berhasil menghapus data series/set", nil) } func parseID(c *gin.Context) (uint, error) { idParam := c.Param("id") id, err := strconv.ParseUint(idParam, 10, 64) if err != nil { return 0, err } return uint(id), nil }