feat: update banner image upload, gallery files, product spec, product, promotion

This commit is contained in:
hanif salafi 2025-11-17 22:30:00 +07:00
parent 85bf2dd456
commit d6aa8b6c2c
23 changed files with 2235 additions and 453 deletions

View File

@ -46,6 +46,7 @@ func (_i *BannersRouter) RegisterBannersRoutes() {
// define routes
_i.App.Route("/banners", func(router fiber.Router) {
router.Get("/", bannersController.All)
router.Get("/viewer/:filename", bannersController.Viewer)
router.Get("/:id", bannersController.Show)
router.Post("/", bannersController.Save)
router.Put("/:id", bannersController.Update)

View File

@ -22,6 +22,7 @@ type BannersController interface {
Save(c *fiber.Ctx) error
Update(c *fiber.Ctx) error
Delete(c *fiber.Ctx) error
Viewer(c *fiber.Ctx) error
}
func NewBannersController(bannersService service.BannersService) BannersController {
@ -102,23 +103,61 @@ func (_i *bannersController) Show(c *fiber.Ctx) error {
// Save Banner
// @Summary Create Banner
// @Description API for creating Banner
// @Description API for creating Banner with file upload
// @Tags Banners
// @Security Bearer
// @Param X-Client-Key header string true "Insert the X-Client-Key"
// @Param payload body request.BannersCreateRequest true "Required payload"
// @Param file formData file false "Upload file"
// @Param title formData string true "Banner title"
// @Param description formData string false "Banner description"
// @Param position formData string false "Banner position"
// @Param status formData string false "Banner status"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /banners [post]
func (_i *bannersController) Save(c *fiber.Ctx) error {
req := new(request.BannersCreateRequest)
if err := utilVal.ParseAndValidate(c, req); err != nil {
return err
// Parse multipart form
form, err := c.MultipartForm()
if err != nil {
return utilRes.Resp(c, utilRes.Response{
Success: false,
Messages: utilRes.Messages{"Failed to parse form data"},
})
}
dataResult, err := _i.bannersService.Create(*req)
// Extract form values
req := request.BannersCreateRequest{
Title: c.FormValue("title"),
}
if description := c.FormValue("description"); description != "" {
req.Description = &description
}
if position := c.FormValue("position"); position != "" {
req.Position = &position
}
if status := c.FormValue("status"); status != "" {
req.Status = &status
}
// Validate required fields
if req.Title == "" {
return utilRes.Resp(c, utilRes.Response{
Success: false,
Messages: utilRes.Messages{"Title is required"},
})
}
// Check if file is uploaded
if len(form.File["file"]) > 0 {
// File will be handled in service
}
dataResult, err := _i.bannersService.Create(c, req)
if err != nil {
return err
}
@ -194,3 +233,19 @@ func (_i *bannersController) Delete(c *fiber.Ctx) error {
Messages: utilRes.Messages{"Banner successfully deleted"},
})
}
// Viewer Banner
// @Summary Viewer Banner
// @Description API for viewing Banner file
// @Tags Banners
// @Security Bearer
// @Param X-Client-Key header string true "Insert the X-Client-Key"
// @Param filename path string true "Banner File Path"
// @Success 200 {file} file
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /banners/viewer/{filename} [get]
func (_i *bannersController) Viewer(c *fiber.Ctx) error {
return _i.bannersService.Viewer(c)
}

View File

@ -21,6 +21,7 @@ type BannersRepository interface {
Create(banner *entity.Banners) (bannerReturn *entity.Banners, err error)
Update(id uint, banner *entity.Banners) (err error)
Delete(id uint) (err error)
FindByThumbnailPath(thumbnailPath string) (banner *entity.Banners, err error)
}
func NewBannersRepository(db *database.Database, log zerolog.Logger) BannersRepository {
@ -90,3 +91,9 @@ func (_i *bannersRepository) Delete(id uint) (err error) {
err = _i.DB.DB.Model(&entity.Banners{}).Where("id = ?", id).Update("is_active", false).Error
return
}
func (_i *bannersRepository) FindByThumbnailPath(thumbnailPath string) (banner *entity.Banners, err error) {
banner = &entity.Banners{}
err = _i.DB.DB.Where("thumbnail_path LIKE ? AND is_active = ?", "%"+thumbnailPath, true).First(banner).Error
return
}

View File

@ -1,14 +1,26 @@
package service
import (
"context"
"errors"
"fmt"
"io"
"jaecoo-be/app/module/banners/mapper"
"jaecoo-be/app/module/banners/repository"
"jaecoo-be/app/module/banners/request"
"jaecoo-be/app/module/banners/response"
"jaecoo-be/config/config"
minioStorage "jaecoo-be/config/config"
"jaecoo-be/utils/paginator"
"math/rand"
"mime"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/gofiber/fiber/v2"
"github.com/minio/minio-go/v7"
"github.com/rs/zerolog"
)
@ -16,21 +28,25 @@ type bannersService struct {
Repo repository.BannersRepository
Log zerolog.Logger
Cfg *config.Config
MinioStorage *minioStorage.MinioStorage
}
type BannersService interface {
GetAll(req request.BannersQueryRequest) (banners []*response.BannersResponse, paging paginator.Pagination, err error)
GetOne(id uint) (banner *response.BannersResponse, err error)
Create(req request.BannersCreateRequest) (banner *response.BannersResponse, err error)
Create(c *fiber.Ctx, req request.BannersCreateRequest) (banner *response.BannersResponse, err error)
Update(id uint, req request.BannersUpdateRequest) (banner *response.BannersResponse, err error)
Delete(id uint) (err error)
UploadFileToMinio(c *fiber.Ctx, fileKey string) (filePath *string, err error)
Viewer(c *fiber.Ctx) (err error)
}
func NewBannersService(repo repository.BannersRepository, log zerolog.Logger, cfg *config.Config) BannersService {
func NewBannersService(repo repository.BannersRepository, log zerolog.Logger, cfg *config.Config, minioStorage *minioStorage.MinioStorage) BannersService {
return &bannersService{
Repo: repo,
Log: log,
Cfg: cfg,
MinioStorage: minioStorage,
}
}
@ -66,7 +82,12 @@ func (_i *bannersService) GetOne(id uint) (banner *response.BannersResponse, err
return
}
func (_i *bannersService) Create(req request.BannersCreateRequest) (banner *response.BannersResponse, err error) {
func (_i *bannersService) Create(c *fiber.Ctx, req request.BannersCreateRequest) (banner *response.BannersResponse, err error) {
// Handle file upload if exists
if filePath, uploadErr := _i.UploadFileToMinio(c, "file"); uploadErr == nil && filePath != nil {
req.ThumbnailPath = filePath
}
bannerEntity := req.ToEntity()
isActive := true
bannerEntity.IsActive = &isActive
@ -82,6 +103,71 @@ func (_i *bannersService) Create(req request.BannersCreateRequest) (banner *resp
return
}
func (_i *bannersService) UploadFileToMinio(c *fiber.Ctx, fileKey string) (filePath *string, err error) {
form, err := c.MultipartForm()
if err != nil {
return nil, err
}
files := form.File[fileKey]
if len(files) == 0 {
return nil, nil // No file uploaded, return nil without error
}
fileHeader := files[0]
// Create minio connection
minioClient, err := _i.MinioStorage.ConnectMinio()
if err != nil {
return nil, err
}
bucketName := _i.MinioStorage.Cfg.ObjectStorage.MinioStorage.BucketName
// Open file
src, err := fileHeader.Open()
if err != nil {
return nil, err
}
defer src.Close()
// Process filename
filename := filepath.Base(fileHeader.Filename)
filename = strings.ReplaceAll(filename, " ", "")
filenameWithoutExt := filepath.Clean(filename[:len(filename)-len(filepath.Ext(filename))])
extension := filepath.Ext(fileHeader.Filename)[1:]
// Generate unique filename
now := time.Now()
rand.New(rand.NewSource(now.UnixNano()))
randUniqueId := rand.Intn(1000000)
newFilenameWithoutExt := filenameWithoutExt + "_" + strconv.Itoa(randUniqueId)
newFilename := newFilenameWithoutExt + "." + extension
// Create object name with path structure
objectName := fmt.Sprintf("banners/upload/%d/%d/%s", now.Year(), now.Month(), newFilename)
_i.Log.Info().Str("timestamp", time.Now().
Format(time.RFC3339)).Str("Service:Resource", "Banners:UploadFileToMinio").
Interface("Uploading file", objectName).Msg("")
// Upload file to MinIO
_, err = minioClient.PutObject(context.Background(), bucketName, objectName, src, fileHeader.Size, minio.PutObjectOptions{})
if err != nil {
_i.Log.Error().Str("timestamp", time.Now().
Format(time.RFC3339)).Str("Service:Resource", "Banners:UploadFileToMinio").
Interface("Error uploading file", err).Msg("")
return nil, err
}
_i.Log.Info().Str("timestamp", time.Now().
Format(time.RFC3339)).Str("Service:Resource", "Banners:UploadFileToMinio").
Interface("Successfully uploaded", objectName).Msg("")
return &objectName, nil
}
func (_i *bannersService) Update(id uint, req request.BannersUpdateRequest) (banner *response.BannersResponse, err error) {
bannerEntity := req.ToEntity()
@ -105,3 +191,79 @@ func (_i *bannersService) Delete(id uint) (err error) {
err = _i.Repo.Delete(id)
return
}
func (_i *bannersService) Viewer(c *fiber.Ctx) (err error) {
filename := c.Params("filename")
// Find banner by thumbnail path
result, err := _i.Repo.FindByThumbnailPath(filename)
if err != nil {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
"error": true,
"msg": "Banner file not found",
})
}
if result.ThumbnailPath == nil || *result.ThumbnailPath == "" {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
"error": true,
"msg": "Banner thumbnail path not found",
})
}
ctx := context.Background()
bucketName := _i.MinioStorage.Cfg.ObjectStorage.MinioStorage.BucketName
objectName := *result.ThumbnailPath
_i.Log.Info().Str("timestamp", time.Now().
Format(time.RFC3339)).Str("Service:Resource", "Banners:Viewer").
Interface("data", objectName).Msg("")
// Create minio connection
minioClient, err := _i.MinioStorage.ConnectMinio()
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": true,
"msg": err.Error(),
})
}
fileContent, err := minioClient.GetObject(ctx, bucketName, objectName, minio.GetObjectOptions{})
if err != nil {
_i.Log.Error().Str("timestamp", time.Now().
Format(time.RFC3339)).Str("Service:Resource", "Banners:Viewer").
Interface("Error getting file", err).Msg("")
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": true,
"msg": "Failed to retrieve file",
})
}
defer fileContent.Close()
// Determine Content-Type based on file extension
contentType := mime.TypeByExtension("." + getFileExtension(objectName))
if contentType == "" {
contentType = "application/octet-stream" // fallback if no MIME type matches
}
c.Set("Content-Type", contentType)
if _, err := io.Copy(c.Response().BodyWriter(), fileContent); err != nil {
return err
}
return
}
func getFileExtension(filename string) string {
// split file name
parts := strings.Split(filename, ".")
// jika tidak ada ekstensi, kembalikan string kosong
if len(parts) == 1 || (len(parts) == 2 && parts[0] == "") {
return ""
}
// ambil ekstensi terakhir
return parts[len(parts)-1]
}

View File

@ -22,6 +22,7 @@ type GalleryFilesController interface {
Save(c *fiber.Ctx) error
Update(c *fiber.Ctx) error
Delete(c *fiber.Ctx) error
Viewer(c *fiber.Ctx) error
}
func NewGalleryFilesController(galleryFilesService service.GalleryFilesService) GalleryFilesController {
@ -101,23 +102,52 @@ func (_i *galleryFilesController) Show(c *fiber.Ctx) error {
// Save GalleryFile
// @Summary Create GalleryFile
// @Description API for creating GalleryFile
// @Description API for creating GalleryFile with file upload
// @Tags GalleryFiles
// @Security Bearer
// @Param X-Client-Key header string true "Insert the X-Client-Key"
// @Param payload body request.GalleryFilesCreateRequest true "Required payload"
// @Param file formData file false "Upload file"
// @Param gallery_id formData int true "Gallery ID"
// @Param title formData string false "Gallery file title"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /gallery-files [post]
func (_i *galleryFilesController) Save(c *fiber.Ctx) error {
req := new(request.GalleryFilesCreateRequest)
if err := utilVal.ParseAndValidate(c, req); err != nil {
return err
// Parse multipart form
form, err := c.MultipartForm()
if err != nil {
return utilRes.Resp(c, utilRes.Response{
Success: false,
Messages: utilRes.Messages{"Failed to parse form data"},
})
}
dataResult, err := _i.galleryFilesService.Create(*req)
// Extract form values
galleryIDStr := c.FormValue("gallery_id")
galleryID, err := strconv.ParseUint(galleryIDStr, 10, 0)
if err != nil {
return utilRes.Resp(c, utilRes.Response{
Success: false,
Messages: utilRes.Messages{"Invalid gallery_id"},
})
}
req := request.GalleryFilesCreateRequest{
GalleryID: uint(galleryID),
}
if title := c.FormValue("title"); title != "" {
req.Title = &title
}
// Check if file is uploaded
if len(form.File["file"]) > 0 {
// File will be handled in service
}
dataResult, err := _i.galleryFilesService.Create(c, req)
if err != nil {
return err
}
@ -193,3 +223,19 @@ func (_i *galleryFilesController) Delete(c *fiber.Ctx) error {
Messages: utilRes.Messages{"GalleryFile successfully deleted"},
})
}
// Viewer GalleryFile
// @Summary Viewer GalleryFile
// @Description API for viewing GalleryFile file
// @Tags GalleryFiles
// @Security Bearer
// @Param X-Client-Key header string true "Insert the X-Client-Key"
// @Param filename path string true "Gallery File Path"
// @Success 200 {file} file
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /gallery-files/viewer/{filename} [get]
func (_i *galleryFilesController) Viewer(c *fiber.Ctx) error {
return _i.galleryFilesService.Viewer(c)
}

View File

@ -46,6 +46,7 @@ func (_i *GalleryFilesRouter) RegisterGalleryFilesRoutes() {
// define routes
_i.App.Route("/gallery-files", func(router fiber.Router) {
router.Get("/", galleryFilesController.All)
router.Get("/viewer/:filename", galleryFilesController.Viewer)
router.Get("/:id", galleryFilesController.Show)
router.Post("/", galleryFilesController.Save)
router.Put("/:id", galleryFilesController.Update)

View File

@ -21,6 +21,7 @@ type GalleryFilesRepository interface {
Create(file *entity.GalleryFiles) (fileReturn *entity.GalleryFiles, err error)
Update(id uint, file *entity.GalleryFiles) (err error)
Delete(id uint) (err error)
FindByImagePath(imagePath string) (file *entity.GalleryFiles, err error)
}
func NewGalleryFilesRepository(db *database.Database, log zerolog.Logger) GalleryFilesRepository {
@ -87,3 +88,9 @@ func (_i *galleryFilesRepository) Delete(id uint) (err error) {
return
}
func (_i *galleryFilesRepository) FindByImagePath(imagePath string) (file *entity.GalleryFiles, err error) {
file = &entity.GalleryFiles{}
err = _i.DB.DB.Where("image_path LIKE ? AND is_active = ?", "%"+imagePath, true).First(file).Error
return
}

View File

@ -1,14 +1,26 @@
package service
import (
"context"
"errors"
"fmt"
"io"
"jaecoo-be/app/module/gallery_files/mapper"
"jaecoo-be/app/module/gallery_files/repository"
"jaecoo-be/app/module/gallery_files/request"
"jaecoo-be/app/module/gallery_files/response"
"jaecoo-be/config/config"
minioStorage "jaecoo-be/config/config"
"jaecoo-be/utils/paginator"
"math/rand"
"mime"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/gofiber/fiber/v2"
"github.com/minio/minio-go/v7"
"github.com/rs/zerolog"
)
@ -16,21 +28,25 @@ type galleryFilesService struct {
Repo repository.GalleryFilesRepository
Log zerolog.Logger
Cfg *config.Config
MinioStorage *minioStorage.MinioStorage
}
type GalleryFilesService interface {
GetAll(req request.GalleryFilesQueryRequest) (files []*response.GalleryFilesResponse, paging paginator.Pagination, err error)
GetOne(id uint) (file *response.GalleryFilesResponse, err error)
Create(req request.GalleryFilesCreateRequest) (file *response.GalleryFilesResponse, err error)
Create(c *fiber.Ctx, req request.GalleryFilesCreateRequest) (file *response.GalleryFilesResponse, err error)
Update(id uint, req request.GalleryFilesUpdateRequest) (file *response.GalleryFilesResponse, err error)
Delete(id uint) (err error)
UploadFileToMinio(c *fiber.Ctx, fileKey string) (filePath *string, err error)
Viewer(c *fiber.Ctx) (err error)
}
func NewGalleryFilesService(repo repository.GalleryFilesRepository, log zerolog.Logger, cfg *config.Config) GalleryFilesService {
func NewGalleryFilesService(repo repository.GalleryFilesRepository, log zerolog.Logger, cfg *config.Config, minioStorage *minioStorage.MinioStorage) GalleryFilesService {
return &galleryFilesService{
Repo: repo,
Log: log,
Cfg: cfg,
MinioStorage: minioStorage,
}
}
@ -66,7 +82,12 @@ func (_i *galleryFilesService) GetOne(id uint) (file *response.GalleryFilesRespo
return
}
func (_i *galleryFilesService) Create(req request.GalleryFilesCreateRequest) (file *response.GalleryFilesResponse, err error) {
func (_i *galleryFilesService) Create(c *fiber.Ctx, req request.GalleryFilesCreateRequest) (file *response.GalleryFilesResponse, err error) {
// Handle file upload if exists
if filePath, uploadErr := _i.UploadFileToMinio(c, "file"); uploadErr == nil && filePath != nil {
req.ImagePath = filePath
}
fileEntity := req.ToEntity()
isActive := true
fileEntity.IsActive = &isActive
@ -82,6 +103,71 @@ func (_i *galleryFilesService) Create(req request.GalleryFilesCreateRequest) (fi
return
}
func (_i *galleryFilesService) UploadFileToMinio(c *fiber.Ctx, fileKey string) (filePath *string, err error) {
form, err := c.MultipartForm()
if err != nil {
return nil, err
}
files := form.File[fileKey]
if len(files) == 0 {
return nil, nil // No file uploaded, return nil without error
}
fileHeader := files[0]
// Create minio connection
minioClient, err := _i.MinioStorage.ConnectMinio()
if err != nil {
return nil, err
}
bucketName := _i.MinioStorage.Cfg.ObjectStorage.MinioStorage.BucketName
// Open file
src, err := fileHeader.Open()
if err != nil {
return nil, err
}
defer src.Close()
// Process filename
filename := filepath.Base(fileHeader.Filename)
filename = strings.ReplaceAll(filename, " ", "")
filenameWithoutExt := filepath.Clean(filename[:len(filename)-len(filepath.Ext(filename))])
extension := filepath.Ext(fileHeader.Filename)[1:]
// Generate unique filename
now := time.Now()
rand.New(rand.NewSource(now.UnixNano()))
randUniqueId := rand.Intn(1000000)
newFilenameWithoutExt := filenameWithoutExt + "_" + strconv.Itoa(randUniqueId)
newFilename := newFilenameWithoutExt + "." + extension
// Create object name with path structure
objectName := fmt.Sprintf("gallery-files/upload/%d/%d/%s", now.Year(), now.Month(), newFilename)
_i.Log.Info().Str("timestamp", time.Now().
Format(time.RFC3339)).Str("Service:Resource", "GalleryFiles:UploadFileToMinio").
Interface("Uploading file", objectName).Msg("")
// Upload file to MinIO
_, err = minioClient.PutObject(context.Background(), bucketName, objectName, src, fileHeader.Size, minio.PutObjectOptions{})
if err != nil {
_i.Log.Error().Str("timestamp", time.Now().
Format(time.RFC3339)).Str("Service:Resource", "GalleryFiles:UploadFileToMinio").
Interface("Error uploading file", err).Msg("")
return nil, err
}
_i.Log.Info().Str("timestamp", time.Now().
Format(time.RFC3339)).Str("Service:Resource", "GalleryFiles:UploadFileToMinio").
Interface("Successfully uploaded", objectName).Msg("")
return &objectName, nil
}
func (_i *galleryFilesService) Update(id uint, req request.GalleryFilesUpdateRequest) (file *response.GalleryFilesResponse, err error) {
fileEntity := req.ToEntity()
@ -105,3 +191,79 @@ func (_i *galleryFilesService) Delete(id uint) (err error) {
err = _i.Repo.Delete(id)
return
}
func (_i *galleryFilesService) Viewer(c *fiber.Ctx) (err error) {
filename := c.Params("filename")
// Find gallery file by image path
result, err := _i.Repo.FindByImagePath(filename)
if err != nil {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
"error": true,
"msg": "Gallery file not found",
})
}
if result.ImagePath == nil || *result.ImagePath == "" {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
"error": true,
"msg": "Gallery image path not found",
})
}
ctx := context.Background()
bucketName := _i.MinioStorage.Cfg.ObjectStorage.MinioStorage.BucketName
objectName := *result.ImagePath
_i.Log.Info().Str("timestamp", time.Now().
Format(time.RFC3339)).Str("Service:Resource", "GalleryFiles:Viewer").
Interface("data", objectName).Msg("")
// Create minio connection
minioClient, err := _i.MinioStorage.ConnectMinio()
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": true,
"msg": err.Error(),
})
}
fileContent, err := minioClient.GetObject(ctx, bucketName, objectName, minio.GetObjectOptions{})
if err != nil {
_i.Log.Error().Str("timestamp", time.Now().
Format(time.RFC3339)).Str("Service:Resource", "GalleryFiles:Viewer").
Interface("Error getting file", err).Msg("")
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": true,
"msg": "Failed to retrieve file",
})
}
defer fileContent.Close()
// Determine Content-Type based on file extension
contentType := mime.TypeByExtension("." + getFileExtension(objectName))
if contentType == "" {
contentType = "application/octet-stream" // fallback if no MIME type matches
}
c.Set("Content-Type", contentType)
if _, err := io.Copy(c.Response().BodyWriter(), fileContent); err != nil {
return err
}
return
}
func getFileExtension(filename string) string {
// split file name
parts := strings.Split(filename, ".")
// jika tidak ada ekstensi, kembalikan string kosong
if len(parts) == 1 || (len(parts) == 2 && parts[0] == "") {
return ""
}
// ambil ekstensi terakhir
return parts[len(parts)-1]
}

View File

@ -22,6 +22,7 @@ type ProductSpecificationsController interface {
Save(c *fiber.Ctx) error
Update(c *fiber.Ctx) error
Delete(c *fiber.Ctx) error
Viewer(c *fiber.Ctx) error
}
func NewProductSpecificationsController(productSpecificationsService service.ProductSpecificationsService) ProductSpecificationsController {
@ -101,23 +102,57 @@ func (_i *productSpecificationsController) Show(c *fiber.Ctx) error {
// Save ProductSpecification
// @Summary Create ProductSpecification
// @Description API for creating ProductSpecification
// @Description API for creating ProductSpecification with file upload
// @Tags ProductSpecifications
// @Security Bearer
// @Param X-Client-Key header string true "Insert the X-Client-Key"
// @Param payload body request.ProductSpecificationsCreateRequest true "Required payload"
// @Param file formData file false "Upload file"
// @Param product_id formData int true "Product ID"
// @Param title formData string true "Product specification title"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /product-specifications [post]
func (_i *productSpecificationsController) Save(c *fiber.Ctx) error {
req := new(request.ProductSpecificationsCreateRequest)
if err := utilVal.ParseAndValidate(c, req); err != nil {
return err
// Parse multipart form
form, err := c.MultipartForm()
if err != nil {
return utilRes.Resp(c, utilRes.Response{
Success: false,
Messages: utilRes.Messages{"Failed to parse form data"},
})
}
dataResult, err := _i.productSpecificationsService.Create(*req)
// Extract form values
productIDStr := c.FormValue("product_id")
productID, err := strconv.ParseUint(productIDStr, 10, 0)
if err != nil {
return utilRes.Resp(c, utilRes.Response{
Success: false,
Messages: utilRes.Messages{"Invalid product_id"},
})
}
req := request.ProductSpecificationsCreateRequest{
ProductID: uint(productID),
Title: c.FormValue("title"),
}
// Validate required fields
if req.Title == "" {
return utilRes.Resp(c, utilRes.Response{
Success: false,
Messages: utilRes.Messages{"Title is required"},
})
}
// Check if file is uploaded
if len(form.File["file"]) > 0 {
// File will be handled in service
}
dataResult, err := _i.productSpecificationsService.Create(c, req)
if err != nil {
return err
}
@ -194,3 +229,18 @@ func (_i *productSpecificationsController) Delete(c *fiber.Ctx) error {
})
}
// Viewer ProductSpecification
// @Summary Viewer ProductSpecification
// @Description API for viewing ProductSpecification file
// @Tags ProductSpecifications
// @Security Bearer
// @Param X-Client-Key header string true "Insert the X-Client-Key"
// @Param filename path string true "Product Specification File Path"
// @Success 200 {file} file
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /product-specifications/viewer/{filename} [get]
func (_i *productSpecificationsController) Viewer(c *fiber.Ctx) error {
return _i.productSpecificationsService.Viewer(c)
}

View File

@ -46,6 +46,7 @@ func (_i *ProductSpecificationsRouter) RegisterProductSpecificationsRoutes() {
// define routes
_i.App.Route("/product-specifications", func(router fiber.Router) {
router.Get("/", productSpecificationsController.All)
router.Get("/viewer/:filename", productSpecificationsController.Viewer)
router.Get("/:id", productSpecificationsController.Show)
router.Post("/", productSpecificationsController.Save)
router.Put("/:id", productSpecificationsController.Update)

View File

@ -21,6 +21,7 @@ type ProductSpecificationsRepository interface {
Create(spec *entity.ProductSpecifications) (specReturn *entity.ProductSpecifications, err error)
Update(id uint, spec *entity.ProductSpecifications) (err error)
Delete(id uint) (err error)
FindByThumbnailPath(thumbnailPath string) (spec *entity.ProductSpecifications, err error)
}
func NewProductSpecificationsRepository(db *database.Database, log zerolog.Logger) ProductSpecificationsRepository {
@ -87,3 +88,9 @@ func (_i *productSpecificationsRepository) Delete(id uint) (err error) {
return
}
func (_i *productSpecificationsRepository) FindByThumbnailPath(thumbnailPath string) (spec *entity.ProductSpecifications, err error) {
spec = &entity.ProductSpecifications{}
err = _i.DB.DB.Where("thumbnail_path LIKE ? AND is_active = ?", "%"+thumbnailPath, true).First(spec).Error
return
}

View File

@ -1,14 +1,26 @@
package service
import (
"context"
"errors"
"fmt"
"io"
"jaecoo-be/app/module/product_specifications/mapper"
"jaecoo-be/app/module/product_specifications/repository"
"jaecoo-be/app/module/product_specifications/request"
"jaecoo-be/app/module/product_specifications/response"
"jaecoo-be/config/config"
minioStorage "jaecoo-be/config/config"
"jaecoo-be/utils/paginator"
"math/rand"
"mime"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/gofiber/fiber/v2"
"github.com/minio/minio-go/v7"
"github.com/rs/zerolog"
)
@ -16,21 +28,25 @@ type productSpecificationsService struct {
Repo repository.ProductSpecificationsRepository
Log zerolog.Logger
Cfg *config.Config
MinioStorage *minioStorage.MinioStorage
}
type ProductSpecificationsService interface {
GetAll(req request.ProductSpecificationsQueryRequest) (specs []*response.ProductSpecificationsResponse, paging paginator.Pagination, err error)
GetOne(id uint) (spec *response.ProductSpecificationsResponse, err error)
Create(req request.ProductSpecificationsCreateRequest) (spec *response.ProductSpecificationsResponse, err error)
Create(c *fiber.Ctx, req request.ProductSpecificationsCreateRequest) (spec *response.ProductSpecificationsResponse, err error)
Update(id uint, req request.ProductSpecificationsUpdateRequest) (spec *response.ProductSpecificationsResponse, err error)
Delete(id uint) (err error)
UploadFileToMinio(c *fiber.Ctx, fileKey string) (filePath *string, err error)
Viewer(c *fiber.Ctx) (err error)
}
func NewProductSpecificationsService(repo repository.ProductSpecificationsRepository, log zerolog.Logger, cfg *config.Config) ProductSpecificationsService {
func NewProductSpecificationsService(repo repository.ProductSpecificationsRepository, log zerolog.Logger, cfg *config.Config, minioStorage *minioStorage.MinioStorage) ProductSpecificationsService {
return &productSpecificationsService{
Repo: repo,
Log: log,
Cfg: cfg,
MinioStorage: minioStorage,
}
}
@ -66,7 +82,12 @@ func (_i *productSpecificationsService) GetOne(id uint) (spec *response.ProductS
return
}
func (_i *productSpecificationsService) Create(req request.ProductSpecificationsCreateRequest) (spec *response.ProductSpecificationsResponse, err error) {
func (_i *productSpecificationsService) Create(c *fiber.Ctx, req request.ProductSpecificationsCreateRequest) (spec *response.ProductSpecificationsResponse, err error) {
// Handle file upload if exists
if filePath, uploadErr := _i.UploadFileToMinio(c, "file"); uploadErr == nil && filePath != nil {
req.ThumbnailPath = filePath
}
specEntity := req.ToEntity()
isActive := true
specEntity.IsActive = &isActive
@ -82,6 +103,71 @@ func (_i *productSpecificationsService) Create(req request.ProductSpecifications
return
}
func (_i *productSpecificationsService) UploadFileToMinio(c *fiber.Ctx, fileKey string) (filePath *string, err error) {
form, err := c.MultipartForm()
if err != nil {
return nil, err
}
files := form.File[fileKey]
if len(files) == 0 {
return nil, nil // No file uploaded, return nil without error
}
fileHeader := files[0]
// Create minio connection
minioClient, err := _i.MinioStorage.ConnectMinio()
if err != nil {
return nil, err
}
bucketName := _i.MinioStorage.Cfg.ObjectStorage.MinioStorage.BucketName
// Open file
src, err := fileHeader.Open()
if err != nil {
return nil, err
}
defer src.Close()
// Process filename
filename := filepath.Base(fileHeader.Filename)
filename = strings.ReplaceAll(filename, " ", "")
filenameWithoutExt := filepath.Clean(filename[:len(filename)-len(filepath.Ext(filename))])
extension := filepath.Ext(fileHeader.Filename)[1:]
// Generate unique filename
now := time.Now()
rand.New(rand.NewSource(now.UnixNano()))
randUniqueId := rand.Intn(1000000)
newFilenameWithoutExt := filenameWithoutExt + "_" + strconv.Itoa(randUniqueId)
newFilename := newFilenameWithoutExt + "." + extension
// Create object name with path structure
objectName := fmt.Sprintf("product-specifications/upload/%d/%d/%s", now.Year(), now.Month(), newFilename)
_i.Log.Info().Str("timestamp", time.Now().
Format(time.RFC3339)).Str("Service:Resource", "ProductSpecifications:UploadFileToMinio").
Interface("Uploading file", objectName).Msg("")
// Upload file to MinIO
_, err = minioClient.PutObject(context.Background(), bucketName, objectName, src, fileHeader.Size, minio.PutObjectOptions{})
if err != nil {
_i.Log.Error().Str("timestamp", time.Now().
Format(time.RFC3339)).Str("Service:Resource", "ProductSpecifications:UploadFileToMinio").
Interface("Error uploading file", err).Msg("")
return nil, err
}
_i.Log.Info().Str("timestamp", time.Now().
Format(time.RFC3339)).Str("Service:Resource", "ProductSpecifications:UploadFileToMinio").
Interface("Successfully uploaded", objectName).Msg("")
return &objectName, nil
}
func (_i *productSpecificationsService) Update(id uint, req request.ProductSpecificationsUpdateRequest) (spec *response.ProductSpecificationsResponse, err error) {
specEntity := req.ToEntity()
@ -105,3 +191,79 @@ func (_i *productSpecificationsService) Delete(id uint) (err error) {
err = _i.Repo.Delete(id)
return
}
func (_i *productSpecificationsService) Viewer(c *fiber.Ctx) (err error) {
filename := c.Params("filename")
// Find product specification by thumbnail path
result, err := _i.Repo.FindByThumbnailPath(filename)
if err != nil {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
"error": true,
"msg": "Product specification file not found",
})
}
if result.ThumbnailPath == nil || *result.ThumbnailPath == "" {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
"error": true,
"msg": "Product specification thumbnail path not found",
})
}
ctx := context.Background()
bucketName := _i.MinioStorage.Cfg.ObjectStorage.MinioStorage.BucketName
objectName := *result.ThumbnailPath
_i.Log.Info().Str("timestamp", time.Now().
Format(time.RFC3339)).Str("Service:Resource", "ProductSpecifications:Viewer").
Interface("data", objectName).Msg("")
// Create minio connection
minioClient, err := _i.MinioStorage.ConnectMinio()
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": true,
"msg": err.Error(),
})
}
fileContent, err := minioClient.GetObject(ctx, bucketName, objectName, minio.GetObjectOptions{})
if err != nil {
_i.Log.Error().Str("timestamp", time.Now().
Format(time.RFC3339)).Str("Service:Resource", "ProductSpecifications:Viewer").
Interface("Error getting file", err).Msg("")
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": true,
"msg": "Failed to retrieve file",
})
}
defer fileContent.Close()
// Determine Content-Type based on file extension
contentType := mime.TypeByExtension("." + getFileExtension(objectName))
if contentType == "" {
contentType = "application/octet-stream" // fallback if no MIME type matches
}
c.Set("Content-Type", contentType)
if _, err := io.Copy(c.Response().BodyWriter(), fileContent); err != nil {
return err
}
return
}
func getFileExtension(filename string) string {
// split file name
parts := strings.Split(filename, ".")
// jika tidak ada ekstensi, kembalikan string kosong
if len(parts) == 1 || (len(parts) == 2 && parts[0] == "") {
return ""
}
// ambil ekstensi terakhir
return parts[len(parts)-1]
}

View File

@ -1,6 +1,7 @@
package controller
import (
"encoding/json"
"jaecoo-be/app/module/products/request"
"jaecoo-be/app/module/products/service"
"jaecoo-be/utils/paginator"
@ -22,6 +23,7 @@ type ProductsController interface {
Save(c *fiber.Ctx) error
Update(c *fiber.Ctx) error
Delete(c *fiber.Ctx) error
Viewer(c *fiber.Ctx) error
}
func NewProductsController(productsService service.ProductsService) ProductsController {
@ -101,23 +103,67 @@ func (_i *productsController) Show(c *fiber.Ctx) error {
// Save Product
// @Summary Create Product
// @Description API for creating Product
// @Description API for creating Product with file upload
// @Tags Products
// @Security Bearer
// @Param X-Client-Key header string true "Insert the X-Client-Key"
// @Param payload body request.ProductsCreateRequest true "Required payload"
// @Param file formData file false "Upload file"
// @Param title formData string true "Product title"
// @Param variant formData string false "Product variant"
// @Param price formData number false "Product price"
// @Param colors formData string false "Product colors (JSON array)"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /products [post]
func (_i *productsController) Save(c *fiber.Ctx) error {
req := new(request.ProductsCreateRequest)
if err := utilVal.ParseAndValidate(c, req); err != nil {
return err
// Parse multipart form
form, err := c.MultipartForm()
if err != nil {
return utilRes.Resp(c, utilRes.Response{
Success: false,
Messages: utilRes.Messages{"Failed to parse form data"},
})
}
dataResult, err := _i.productsService.Create(*req)
// Extract form values
req := request.ProductsCreateRequest{
Title: c.FormValue("title"),
}
if variant := c.FormValue("variant"); variant != "" {
req.Variant = &variant
}
if priceStr := c.FormValue("price"); priceStr != "" {
if price, err := strconv.ParseFloat(priceStr, 64); err == nil {
req.Price = &price
}
}
// Handle colors (JSON array string)
if colorsStr := c.FormValue("colors"); colorsStr != "" {
var colors []string
if err := json.Unmarshal([]byte(colorsStr), &colors); err == nil {
req.Colors = colors
}
}
// Validate required fields
if req.Title == "" {
return utilRes.Resp(c, utilRes.Response{
Success: false,
Messages: utilRes.Messages{"Title is required"},
})
}
// Check if file is uploaded
if len(form.File["file"]) > 0 {
// File will be handled in service
}
dataResult, err := _i.productsService.Create(c, req)
if err != nil {
return err
}
@ -194,3 +240,19 @@ func (_i *productsController) Delete(c *fiber.Ctx) error {
})
}
// Viewer Product
// @Summary Viewer Product
// @Description API for viewing Product file
// @Tags Products
// @Security Bearer
// @Param X-Client-Key header string true "Insert the X-Client-Key"
// @Param filename path string true "Product File Path"
// @Success 200 {file} file
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /products/viewer/{filename} [get]
func (_i *productsController) Viewer(c *fiber.Ctx) error {
return _i.productsService.Viewer(c)
}

View File

@ -46,6 +46,7 @@ func (_i *ProductsRouter) RegisterProductsRoutes() {
// define routes
_i.App.Route("/products", func(router fiber.Router) {
router.Get("/", productsController.All)
router.Get("/viewer/:filename", productsController.Viewer)
router.Get("/:id", productsController.Show)
router.Post("/", productsController.Save)
router.Put("/:id", productsController.Update)

View File

@ -21,6 +21,7 @@ type ProductsRepository interface {
Create(product *entity.Products) (productReturn *entity.Products, err error)
Update(id uint, product *entity.Products) (err error)
Delete(id uint) (err error)
FindByThumbnailPath(thumbnailPath string) (product *entity.Products, err error)
}
func NewProductsRepository(db *database.Database, log zerolog.Logger) ProductsRepository {
@ -87,3 +88,9 @@ func (_i *productsRepository) Delete(id uint) (err error) {
return
}
func (_i *productsRepository) FindByThumbnailPath(thumbnailPath string) (product *entity.Products, err error) {
product = &entity.Products{}
err = _i.DB.DB.Where("thumbnail_path LIKE ? AND is_active = ?", "%"+thumbnailPath, true).First(product).Error
return
}

View File

@ -1,14 +1,26 @@
package service
import (
"context"
"errors"
"fmt"
"io"
"jaecoo-be/app/module/products/mapper"
"jaecoo-be/app/module/products/repository"
"jaecoo-be/app/module/products/request"
"jaecoo-be/app/module/products/response"
"jaecoo-be/config/config"
minioStorage "jaecoo-be/config/config"
"jaecoo-be/utils/paginator"
"math/rand"
"mime"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/gofiber/fiber/v2"
"github.com/minio/minio-go/v7"
"github.com/rs/zerolog"
)
@ -16,21 +28,25 @@ type productsService struct {
Repo repository.ProductsRepository
Log zerolog.Logger
Cfg *config.Config
MinioStorage *minioStorage.MinioStorage
}
type ProductsService interface {
GetAll(req request.ProductsQueryRequest) (products []*response.ProductsResponse, paging paginator.Pagination, err error)
GetOne(id uint) (product *response.ProductsResponse, err error)
Create(req request.ProductsCreateRequest) (product *response.ProductsResponse, err error)
Create(c *fiber.Ctx, req request.ProductsCreateRequest) (product *response.ProductsResponse, err error)
Update(id uint, req request.ProductsUpdateRequest) (product *response.ProductsResponse, err error)
Delete(id uint) (err error)
UploadFileToMinio(c *fiber.Ctx, fileKey string) (filePath *string, err error)
Viewer(c *fiber.Ctx) (err error)
}
func NewProductsService(repo repository.ProductsRepository, log zerolog.Logger, cfg *config.Config) ProductsService {
func NewProductsService(repo repository.ProductsRepository, log zerolog.Logger, cfg *config.Config, minioStorage *minioStorage.MinioStorage) ProductsService {
return &productsService{
Repo: repo,
Log: log,
Cfg: cfg,
MinioStorage: minioStorage,
}
}
@ -66,7 +82,12 @@ func (_i *productsService) GetOne(id uint) (product *response.ProductsResponse,
return
}
func (_i *productsService) Create(req request.ProductsCreateRequest) (product *response.ProductsResponse, err error) {
func (_i *productsService) Create(c *fiber.Ctx, req request.ProductsCreateRequest) (product *response.ProductsResponse, err error) {
// Handle file upload if exists
if filePath, uploadErr := _i.UploadFileToMinio(c, "file"); uploadErr == nil && filePath != nil {
req.ThumbnailPath = filePath
}
productEntity := req.ToEntity()
isActive := true
productEntity.IsActive = &isActive
@ -82,6 +103,71 @@ func (_i *productsService) Create(req request.ProductsCreateRequest) (product *r
return
}
func (_i *productsService) UploadFileToMinio(c *fiber.Ctx, fileKey string) (filePath *string, err error) {
form, err := c.MultipartForm()
if err != nil {
return nil, err
}
files := form.File[fileKey]
if len(files) == 0 {
return nil, nil // No file uploaded, return nil without error
}
fileHeader := files[0]
// Create minio connection
minioClient, err := _i.MinioStorage.ConnectMinio()
if err != nil {
return nil, err
}
bucketName := _i.MinioStorage.Cfg.ObjectStorage.MinioStorage.BucketName
// Open file
src, err := fileHeader.Open()
if err != nil {
return nil, err
}
defer src.Close()
// Process filename
filename := filepath.Base(fileHeader.Filename)
filename = strings.ReplaceAll(filename, " ", "")
filenameWithoutExt := filepath.Clean(filename[:len(filename)-len(filepath.Ext(filename))])
extension := filepath.Ext(fileHeader.Filename)[1:]
// Generate unique filename
now := time.Now()
rand.New(rand.NewSource(now.UnixNano()))
randUniqueId := rand.Intn(1000000)
newFilenameWithoutExt := filenameWithoutExt + "_" + strconv.Itoa(randUniqueId)
newFilename := newFilenameWithoutExt + "." + extension
// Create object name with path structure
objectName := fmt.Sprintf("products/upload/%d/%d/%s", now.Year(), now.Month(), newFilename)
_i.Log.Info().Str("timestamp", time.Now().
Format(time.RFC3339)).Str("Service:Resource", "Products:UploadFileToMinio").
Interface("Uploading file", objectName).Msg("")
// Upload file to MinIO
_, err = minioClient.PutObject(context.Background(), bucketName, objectName, src, fileHeader.Size, minio.PutObjectOptions{})
if err != nil {
_i.Log.Error().Str("timestamp", time.Now().
Format(time.RFC3339)).Str("Service:Resource", "Products:UploadFileToMinio").
Interface("Error uploading file", err).Msg("")
return nil, err
}
_i.Log.Info().Str("timestamp", time.Now().
Format(time.RFC3339)).Str("Service:Resource", "Products:UploadFileToMinio").
Interface("Successfully uploaded", objectName).Msg("")
return &objectName, nil
}
func (_i *productsService) Update(id uint, req request.ProductsUpdateRequest) (product *response.ProductsResponse, err error) {
productEntity := req.ToEntity()
@ -105,3 +191,79 @@ func (_i *productsService) Delete(id uint) (err error) {
err = _i.Repo.Delete(id)
return
}
func (_i *productsService) Viewer(c *fiber.Ctx) (err error) {
filename := c.Params("filename")
// Find product by thumbnail path
result, err := _i.Repo.FindByThumbnailPath(filename)
if err != nil {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
"error": true,
"msg": "Product file not found",
})
}
if result.ThumbnailPath == nil || *result.ThumbnailPath == "" {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
"error": true,
"msg": "Product thumbnail path not found",
})
}
ctx := context.Background()
bucketName := _i.MinioStorage.Cfg.ObjectStorage.MinioStorage.BucketName
objectName := *result.ThumbnailPath
_i.Log.Info().Str("timestamp", time.Now().
Format(time.RFC3339)).Str("Service:Resource", "Products:Viewer").
Interface("data", objectName).Msg("")
// Create minio connection
minioClient, err := _i.MinioStorage.ConnectMinio()
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": true,
"msg": err.Error(),
})
}
fileContent, err := minioClient.GetObject(ctx, bucketName, objectName, minio.GetObjectOptions{})
if err != nil {
_i.Log.Error().Str("timestamp", time.Now().
Format(time.RFC3339)).Str("Service:Resource", "Products:Viewer").
Interface("Error getting file", err).Msg("")
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": true,
"msg": "Failed to retrieve file",
})
}
defer fileContent.Close()
// Determine Content-Type based on file extension
contentType := mime.TypeByExtension("." + getFileExtension(objectName))
if contentType == "" {
contentType = "application/octet-stream" // fallback if no MIME type matches
}
c.Set("Content-Type", contentType)
if _, err := io.Copy(c.Response().BodyWriter(), fileContent); err != nil {
return err
}
return
}
func getFileExtension(filename string) string {
// split file name
parts := strings.Split(filename, ".")
// jika tidak ada ekstensi, kembalikan string kosong
if len(parts) == 1 || (len(parts) == 2 && parts[0] == "") {
return ""
}
// ambil ekstensi terakhir
return parts[len(parts)-1]
}

View File

@ -22,6 +22,7 @@ type PromotionsController interface {
Save(c *fiber.Ctx) error
Update(c *fiber.Ctx) error
Delete(c *fiber.Ctx) error
Viewer(c *fiber.Ctx) error
}
func NewPromotionsController(promotionsService service.PromotionsService) PromotionsController {
@ -100,23 +101,51 @@ func (_i *promotionsController) Show(c *fiber.Ctx) error {
// Save Promotion
// @Summary Create Promotion
// @Description API for creating Promotion
// @Description API for creating Promotion with file upload
// @Tags Promotions
// @Security Bearer
// @Param X-Client-Key header string true "Insert the X-Client-Key"
// @Param payload body request.PromotionsCreateRequest true "Required payload"
// @Param file formData file false "Upload file"
// @Param title formData string true "Promotion title"
// @Param description formData string false "Promotion description"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /promotions [post]
func (_i *promotionsController) Save(c *fiber.Ctx) error {
req := new(request.PromotionsCreateRequest)
if err := utilVal.ParseAndValidate(c, req); err != nil {
return err
// Parse multipart form
form, err := c.MultipartForm()
if err != nil {
return utilRes.Resp(c, utilRes.Response{
Success: false,
Messages: utilRes.Messages{"Failed to parse form data"},
})
}
dataResult, err := _i.promotionsService.Create(*req)
// Extract form values
req := request.PromotionsCreateRequest{
Title: c.FormValue("title"),
}
if description := c.FormValue("description"); description != "" {
req.Description = &description
}
// Validate required fields
if req.Title == "" {
return utilRes.Resp(c, utilRes.Response{
Success: false,
Messages: utilRes.Messages{"Title is required"},
})
}
// Check if file is uploaded
if len(form.File["file"]) > 0 {
// File will be handled in service
}
dataResult, err := _i.promotionsService.Create(c, req)
if err != nil {
return err
}
@ -192,3 +221,19 @@ func (_i *promotionsController) Delete(c *fiber.Ctx) error {
Messages: utilRes.Messages{"Promotion successfully deleted"},
})
}
// Viewer Promotion
// @Summary Viewer Promotion
// @Description API for viewing Promotion file
// @Tags Promotions
// @Security Bearer
// @Param X-Client-Key header string true "Insert the X-Client-Key"
// @Param filename path string true "Promotion File Path"
// @Success 200 {file} file
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /promotions/viewer/{filename} [get]
func (_i *promotionsController) Viewer(c *fiber.Ctx) error {
return _i.promotionsService.Viewer(c)
}

View File

@ -46,6 +46,7 @@ func (_i *PromotionsRouter) RegisterPromotionsRoutes() {
// define routes
_i.App.Route("/promotions", func(router fiber.Router) {
router.Get("/", promotionsController.All)
router.Get("/viewer/:filename", promotionsController.Viewer)
router.Get("/:id", promotionsController.Show)
router.Post("/", promotionsController.Save)
router.Put("/:id", promotionsController.Update)

View File

@ -21,6 +21,7 @@ type PromotionsRepository interface {
Create(promotion *entity.Promotions) (promotionReturn *entity.Promotions, err error)
Update(id uint, promotion *entity.Promotions) (err error)
Delete(id uint) (err error)
FindByThumbnailPath(thumbnailPath string) (promotion *entity.Promotions, err error)
}
func NewPromotionsRepository(db *database.Database, log zerolog.Logger) PromotionsRepository {
@ -83,3 +84,9 @@ func (_i *promotionsRepository) Delete(id uint) (err error) {
return
}
func (_i *promotionsRepository) FindByThumbnailPath(thumbnailPath string) (promotion *entity.Promotions, err error) {
promotion = &entity.Promotions{}
err = _i.DB.DB.Where("thumbnail_path LIKE ? AND is_active = ?", "%"+thumbnailPath, true).First(promotion).Error
return
}

View File

@ -1,14 +1,26 @@
package service
import (
"context"
"errors"
"fmt"
"io"
"jaecoo-be/app/module/promotions/mapper"
"jaecoo-be/app/module/promotions/repository"
"jaecoo-be/app/module/promotions/request"
"jaecoo-be/app/module/promotions/response"
"jaecoo-be/config/config"
minioStorage "jaecoo-be/config/config"
"jaecoo-be/utils/paginator"
"math/rand"
"mime"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/gofiber/fiber/v2"
"github.com/minio/minio-go/v7"
"github.com/rs/zerolog"
)
@ -16,21 +28,25 @@ type promotionsService struct {
Repo repository.PromotionsRepository
Log zerolog.Logger
Cfg *config.Config
MinioStorage *minioStorage.MinioStorage
}
type PromotionsService interface {
GetAll(req request.PromotionsQueryRequest) (promotions []*response.PromotionsResponse, paging paginator.Pagination, err error)
GetOne(id uint) (promotion *response.PromotionsResponse, err error)
Create(req request.PromotionsCreateRequest) (promotion *response.PromotionsResponse, err error)
Create(c *fiber.Ctx, req request.PromotionsCreateRequest) (promotion *response.PromotionsResponse, err error)
Update(id uint, req request.PromotionsUpdateRequest) (promotion *response.PromotionsResponse, err error)
Delete(id uint) (err error)
UploadFileToMinio(c *fiber.Ctx, fileKey string) (filePath *string, err error)
Viewer(c *fiber.Ctx) (err error)
}
func NewPromotionsService(repo repository.PromotionsRepository, log zerolog.Logger, cfg *config.Config) PromotionsService {
func NewPromotionsService(repo repository.PromotionsRepository, log zerolog.Logger, cfg *config.Config, minioStorage *minioStorage.MinioStorage) PromotionsService {
return &promotionsService{
Repo: repo,
Log: log,
Cfg: cfg,
MinioStorage: minioStorage,
}
}
@ -66,7 +82,12 @@ func (_i *promotionsService) GetOne(id uint) (promotion *response.PromotionsResp
return
}
func (_i *promotionsService) Create(req request.PromotionsCreateRequest) (promotion *response.PromotionsResponse, err error) {
func (_i *promotionsService) Create(c *fiber.Ctx, req request.PromotionsCreateRequest) (promotion *response.PromotionsResponse, err error) {
// Handle file upload if exists
if filePath, uploadErr := _i.UploadFileToMinio(c, "file"); uploadErr == nil && filePath != nil {
req.ThumbnailPath = filePath
}
promotionEntity := req.ToEntity()
isActive := true
promotionEntity.IsActive = &isActive
@ -82,6 +103,71 @@ func (_i *promotionsService) Create(req request.PromotionsCreateRequest) (promot
return
}
func (_i *promotionsService) UploadFileToMinio(c *fiber.Ctx, fileKey string) (filePath *string, err error) {
form, err := c.MultipartForm()
if err != nil {
return nil, err
}
files := form.File[fileKey]
if len(files) == 0 {
return nil, nil // No file uploaded, return nil without error
}
fileHeader := files[0]
// Create minio connection
minioClient, err := _i.MinioStorage.ConnectMinio()
if err != nil {
return nil, err
}
bucketName := _i.MinioStorage.Cfg.ObjectStorage.MinioStorage.BucketName
// Open file
src, err := fileHeader.Open()
if err != nil {
return nil, err
}
defer src.Close()
// Process filename
filename := filepath.Base(fileHeader.Filename)
filename = strings.ReplaceAll(filename, " ", "")
filenameWithoutExt := filepath.Clean(filename[:len(filename)-len(filepath.Ext(filename))])
extension := filepath.Ext(fileHeader.Filename)[1:]
// Generate unique filename
now := time.Now()
rand.New(rand.NewSource(now.UnixNano()))
randUniqueId := rand.Intn(1000000)
newFilenameWithoutExt := filenameWithoutExt + "_" + strconv.Itoa(randUniqueId)
newFilename := newFilenameWithoutExt + "." + extension
// Create object name with path structure
objectName := fmt.Sprintf("promotions/upload/%d/%d/%s", now.Year(), now.Month(), newFilename)
_i.Log.Info().Str("timestamp", time.Now().
Format(time.RFC3339)).Str("Service:Resource", "Promotions:UploadFileToMinio").
Interface("Uploading file", objectName).Msg("")
// Upload file to MinIO
_, err = minioClient.PutObject(context.Background(), bucketName, objectName, src, fileHeader.Size, minio.PutObjectOptions{})
if err != nil {
_i.Log.Error().Str("timestamp", time.Now().
Format(time.RFC3339)).Str("Service:Resource", "Promotions:UploadFileToMinio").
Interface("Error uploading file", err).Msg("")
return nil, err
}
_i.Log.Info().Str("timestamp", time.Now().
Format(time.RFC3339)).Str("Service:Resource", "Promotions:UploadFileToMinio").
Interface("Successfully uploaded", objectName).Msg("")
return &objectName, nil
}
func (_i *promotionsService) Update(id uint, req request.PromotionsUpdateRequest) (promotion *response.PromotionsResponse, err error) {
promotionEntity := req.ToEntity()
@ -105,3 +191,79 @@ func (_i *promotionsService) Delete(id uint) (err error) {
err = _i.Repo.Delete(id)
return
}
func (_i *promotionsService) Viewer(c *fiber.Ctx) (err error) {
filename := c.Params("filename")
// Find promotion by thumbnail path
result, err := _i.Repo.FindByThumbnailPath(filename)
if err != nil {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
"error": true,
"msg": "Promotion file not found",
})
}
if result.ThumbnailPath == nil || *result.ThumbnailPath == "" {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
"error": true,
"msg": "Promotion thumbnail path not found",
})
}
ctx := context.Background()
bucketName := _i.MinioStorage.Cfg.ObjectStorage.MinioStorage.BucketName
objectName := *result.ThumbnailPath
_i.Log.Info().Str("timestamp", time.Now().
Format(time.RFC3339)).Str("Service:Resource", "Promotions:Viewer").
Interface("data", objectName).Msg("")
// Create minio connection
minioClient, err := _i.MinioStorage.ConnectMinio()
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": true,
"msg": err.Error(),
})
}
fileContent, err := minioClient.GetObject(ctx, bucketName, objectName, minio.GetObjectOptions{})
if err != nil {
_i.Log.Error().Str("timestamp", time.Now().
Format(time.RFC3339)).Str("Service:Resource", "Promotions:Viewer").
Interface("Error getting file", err).Msg("")
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": true,
"msg": "Failed to retrieve file",
})
}
defer fileContent.Close()
// Determine Content-Type based on file extension
contentType := mime.TypeByExtension("." + getFileExtension(objectName))
if contentType == "" {
contentType = "application/octet-stream" // fallback if no MIME type matches
}
c.Set("Content-Type", contentType)
if _, err := io.Copy(c.Response().BodyWriter(), fileContent); err != nil {
return err
}
return
}
func getFileExtension(filename string) string {
// split file name
parts := strings.Split(filename, ".")
// jika tidak ada ekstensi, kembalikan string kosong
if len(parts) == 1 || (len(parts) == 2 && parts[0] == "") {
return ""
}
// ambil ekstensi terakhir
return parts[len(parts)-1]
}

View File

@ -3331,7 +3331,7 @@ const docTemplate = `{
"Bearer": []
}
],
"description": "API for creating Banner",
"description": "API for creating Banner with file upload",
"tags": [
"Banners"
],
@ -3345,13 +3345,35 @@ const docTemplate = `{
"required": true
},
{
"description": "Required payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/request.BannersCreateRequest"
}
"type": "file",
"description": "Upload file",
"name": "file",
"in": "formData"
},
{
"type": "string",
"description": "Banner title",
"name": "title",
"in": "formData",
"required": true
},
{
"type": "string",
"description": "Banner description",
"name": "description",
"in": "formData"
},
{
"type": "string",
"description": "Banner position",
"name": "position",
"in": "formData"
},
{
"type": "string",
"description": "Banner status",
"name": "status",
"in": "formData"
}
],
"responses": {
@ -3382,6 +3404,62 @@ const docTemplate = `{
}
}
},
"/banners/viewer/{filename}": {
"get": {
"security": [
{
"Bearer": []
}
],
"description": "API for viewing Banner file",
"tags": [
"Banners"
],
"summary": "Viewer Banner",
"parameters": [
{
"type": "string",
"description": "Insert the X-Client-Key",
"name": "X-Client-Key",
"in": "header",
"required": true
},
{
"type": "string",
"description": "Banner File Path",
"name": "filename",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "file"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.BadRequestError"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/response.UnauthorizedError"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.InternalServerError"
}
}
}
}
},
"/banners/{id}": {
"get": {
"security": [
@ -5279,7 +5357,7 @@ const docTemplate = `{
"Bearer": []
}
],
"description": "API for creating GalleryFile",
"description": "API for creating GalleryFile with file upload",
"tags": [
"GalleryFiles"
],
@ -5293,13 +5371,23 @@ const docTemplate = `{
"required": true
},
{
"description": "Required payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/request.GalleryFilesCreateRequest"
}
"type": "file",
"description": "Upload file",
"name": "file",
"in": "formData"
},
{
"type": "integer",
"description": "Gallery ID",
"name": "gallery_id",
"in": "formData",
"required": true
},
{
"type": "string",
"description": "Gallery file title",
"name": "title",
"in": "formData"
}
],
"responses": {
@ -5330,6 +5418,62 @@ const docTemplate = `{
}
}
},
"/gallery-files/viewer/{filename}": {
"get": {
"security": [
{
"Bearer": []
}
],
"description": "API for viewing GalleryFile file",
"tags": [
"GalleryFiles"
],
"summary": "Viewer GalleryFile",
"parameters": [
{
"type": "string",
"description": "Insert the X-Client-Key",
"name": "X-Client-Key",
"in": "header",
"required": true
},
{
"type": "string",
"description": "Gallery File Path",
"name": "filename",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "file"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.BadRequestError"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/response.UnauthorizedError"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.InternalServerError"
}
}
}
}
},
"/gallery-files/{id}": {
"get": {
"security": [
@ -5607,7 +5751,7 @@ const docTemplate = `{
"Bearer": []
}
],
"description": "API for creating ProductSpecification",
"description": "API for creating ProductSpecification with file upload",
"tags": [
"ProductSpecifications"
],
@ -5621,13 +5765,24 @@ const docTemplate = `{
"required": true
},
{
"description": "Required payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/request.ProductSpecificationsCreateRequest"
}
"type": "file",
"description": "Upload file",
"name": "file",
"in": "formData"
},
{
"type": "integer",
"description": "Product ID",
"name": "product_id",
"in": "formData",
"required": true
},
{
"type": "string",
"description": "Product specification title",
"name": "title",
"in": "formData",
"required": true
}
],
"responses": {
@ -5658,6 +5813,62 @@ const docTemplate = `{
}
}
},
"/product-specifications/viewer/{filename}": {
"get": {
"security": [
{
"Bearer": []
}
],
"description": "API for viewing ProductSpecification file",
"tags": [
"ProductSpecifications"
],
"summary": "Viewer ProductSpecification",
"parameters": [
{
"type": "string",
"description": "Insert the X-Client-Key",
"name": "X-Client-Key",
"in": "header",
"required": true
},
{
"type": "string",
"description": "Product Specification File Path",
"name": "filename",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "file"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.BadRequestError"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/response.UnauthorizedError"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.InternalServerError"
}
}
}
}
},
"/product-specifications/{id}": {
"get": {
"security": [
@ -5935,7 +6146,7 @@ const docTemplate = `{
"Bearer": []
}
],
"description": "API for creating Product",
"description": "API for creating Product with file upload",
"tags": [
"Products"
],
@ -5949,13 +6160,35 @@ const docTemplate = `{
"required": true
},
{
"description": "Required payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/request.ProductsCreateRequest"
}
"type": "file",
"description": "Upload file",
"name": "file",
"in": "formData"
},
{
"type": "string",
"description": "Product title",
"name": "title",
"in": "formData",
"required": true
},
{
"type": "string",
"description": "Product variant",
"name": "variant",
"in": "formData"
},
{
"type": "number",
"description": "Product price",
"name": "price",
"in": "formData"
},
{
"type": "string",
"description": "Product colors (JSON array)",
"name": "colors",
"in": "formData"
}
],
"responses": {
@ -5986,6 +6219,62 @@ const docTemplate = `{
}
}
},
"/products/viewer/{filename}": {
"get": {
"security": [
{
"Bearer": []
}
],
"description": "API for viewing Product file",
"tags": [
"Products"
],
"summary": "Viewer Product",
"parameters": [
{
"type": "string",
"description": "Insert the X-Client-Key",
"name": "X-Client-Key",
"in": "header",
"required": true
},
{
"type": "string",
"description": "Product File Path",
"name": "filename",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "file"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.BadRequestError"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/response.UnauthorizedError"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.InternalServerError"
}
}
}
}
},
"/products/{id}": {
"get": {
"security": [
@ -6258,7 +6547,7 @@ const docTemplate = `{
"Bearer": []
}
],
"description": "API for creating Promotion",
"description": "API for creating Promotion with file upload",
"tags": [
"Promotions"
],
@ -6272,13 +6561,23 @@ const docTemplate = `{
"required": true
},
{
"description": "Required payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/request.PromotionsCreateRequest"
}
"type": "file",
"description": "Upload file",
"name": "file",
"in": "formData"
},
{
"type": "string",
"description": "Promotion title",
"name": "title",
"in": "formData",
"required": true
},
{
"type": "string",
"description": "Promotion description",
"name": "description",
"in": "formData"
}
],
"responses": {
@ -6309,6 +6608,62 @@ const docTemplate = `{
}
}
},
"/promotions/viewer/{filename}": {
"get": {
"security": [
{
"Bearer": []
}
],
"description": "API for viewing Promotion file",
"tags": [
"Promotions"
],
"summary": "Viewer Promotion",
"parameters": [
{
"type": "string",
"description": "Insert the X-Client-Key",
"name": "X-Client-Key",
"in": "header",
"required": true
},
{
"type": "string",
"description": "Promotion File Path",
"name": "filename",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "file"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.BadRequestError"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/response.UnauthorizedError"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.InternalServerError"
}
}
}
}
},
"/promotions/{id}": {
"get": {
"security": [
@ -9820,29 +10175,6 @@ const docTemplate = `{
}
}
},
"request.BannersCreateRequest": {
"type": "object",
"required": [
"title"
],
"properties": {
"description": {
"type": "string"
},
"position": {
"type": "string"
},
"status": {
"type": "string"
},
"thumbnail_path": {
"type": "string"
},
"title": {
"type": "string"
}
}
},
"request.BannersUpdateRequest": {
"type": "object",
"properties": {
@ -10027,23 +10359,6 @@ const docTemplate = `{
}
}
},
"request.GalleryFilesCreateRequest": {
"type": "object",
"required": [
"gallery_id"
],
"properties": {
"gallery_id": {
"type": "integer"
},
"image_path": {
"type": "string"
},
"title": {
"type": "string"
}
}
},
"request.GalleryFilesUpdateRequest": {
"type": "object",
"properties": {
@ -10061,24 +10376,6 @@ const docTemplate = `{
}
}
},
"request.ProductSpecificationsCreateRequest": {
"type": "object",
"required": [
"product_id",
"title"
],
"properties": {
"product_id": {
"type": "integer"
},
"thumbnail_path": {
"type": "string"
},
"title": {
"type": "string"
}
}
},
"request.ProductSpecificationsUpdateRequest": {
"type": "object",
"properties": {
@ -10096,32 +10393,6 @@ const docTemplate = `{
}
}
},
"request.ProductsCreateRequest": {
"type": "object",
"required": [
"title"
],
"properties": {
"colors": {
"type": "array",
"items": {
"type": "string"
}
},
"price": {
"type": "number"
},
"thumbnail_path": {
"type": "string"
},
"title": {
"type": "string"
},
"variant": {
"type": "string"
}
}
},
"request.ProductsUpdateRequest": {
"type": "object",
"properties": {
@ -10148,23 +10419,6 @@ const docTemplate = `{
}
}
},
"request.PromotionsCreateRequest": {
"type": "object",
"required": [
"title"
],
"properties": {
"description": {
"type": "string"
},
"thumbnail_path": {
"type": "string"
},
"title": {
"type": "string"
}
}
},
"request.PromotionsUpdateRequest": {
"type": "object",
"properties": {

View File

@ -3320,7 +3320,7 @@
"Bearer": []
}
],
"description": "API for creating Banner",
"description": "API for creating Banner with file upload",
"tags": [
"Banners"
],
@ -3334,13 +3334,35 @@
"required": true
},
{
"description": "Required payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/request.BannersCreateRequest"
}
"type": "file",
"description": "Upload file",
"name": "file",
"in": "formData"
},
{
"type": "string",
"description": "Banner title",
"name": "title",
"in": "formData",
"required": true
},
{
"type": "string",
"description": "Banner description",
"name": "description",
"in": "formData"
},
{
"type": "string",
"description": "Banner position",
"name": "position",
"in": "formData"
},
{
"type": "string",
"description": "Banner status",
"name": "status",
"in": "formData"
}
],
"responses": {
@ -3371,6 +3393,62 @@
}
}
},
"/banners/viewer/{filename}": {
"get": {
"security": [
{
"Bearer": []
}
],
"description": "API for viewing Banner file",
"tags": [
"Banners"
],
"summary": "Viewer Banner",
"parameters": [
{
"type": "string",
"description": "Insert the X-Client-Key",
"name": "X-Client-Key",
"in": "header",
"required": true
},
{
"type": "string",
"description": "Banner File Path",
"name": "filename",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "file"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.BadRequestError"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/response.UnauthorizedError"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.InternalServerError"
}
}
}
}
},
"/banners/{id}": {
"get": {
"security": [
@ -5268,7 +5346,7 @@
"Bearer": []
}
],
"description": "API for creating GalleryFile",
"description": "API for creating GalleryFile with file upload",
"tags": [
"GalleryFiles"
],
@ -5282,13 +5360,23 @@
"required": true
},
{
"description": "Required payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/request.GalleryFilesCreateRequest"
}
"type": "file",
"description": "Upload file",
"name": "file",
"in": "formData"
},
{
"type": "integer",
"description": "Gallery ID",
"name": "gallery_id",
"in": "formData",
"required": true
},
{
"type": "string",
"description": "Gallery file title",
"name": "title",
"in": "formData"
}
],
"responses": {
@ -5319,6 +5407,62 @@
}
}
},
"/gallery-files/viewer/{filename}": {
"get": {
"security": [
{
"Bearer": []
}
],
"description": "API for viewing GalleryFile file",
"tags": [
"GalleryFiles"
],
"summary": "Viewer GalleryFile",
"parameters": [
{
"type": "string",
"description": "Insert the X-Client-Key",
"name": "X-Client-Key",
"in": "header",
"required": true
},
{
"type": "string",
"description": "Gallery File Path",
"name": "filename",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "file"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.BadRequestError"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/response.UnauthorizedError"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.InternalServerError"
}
}
}
}
},
"/gallery-files/{id}": {
"get": {
"security": [
@ -5596,7 +5740,7 @@
"Bearer": []
}
],
"description": "API for creating ProductSpecification",
"description": "API for creating ProductSpecification with file upload",
"tags": [
"ProductSpecifications"
],
@ -5610,13 +5754,24 @@
"required": true
},
{
"description": "Required payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/request.ProductSpecificationsCreateRequest"
}
"type": "file",
"description": "Upload file",
"name": "file",
"in": "formData"
},
{
"type": "integer",
"description": "Product ID",
"name": "product_id",
"in": "formData",
"required": true
},
{
"type": "string",
"description": "Product specification title",
"name": "title",
"in": "formData",
"required": true
}
],
"responses": {
@ -5647,6 +5802,62 @@
}
}
},
"/product-specifications/viewer/{filename}": {
"get": {
"security": [
{
"Bearer": []
}
],
"description": "API for viewing ProductSpecification file",
"tags": [
"ProductSpecifications"
],
"summary": "Viewer ProductSpecification",
"parameters": [
{
"type": "string",
"description": "Insert the X-Client-Key",
"name": "X-Client-Key",
"in": "header",
"required": true
},
{
"type": "string",
"description": "Product Specification File Path",
"name": "filename",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "file"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.BadRequestError"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/response.UnauthorizedError"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.InternalServerError"
}
}
}
}
},
"/product-specifications/{id}": {
"get": {
"security": [
@ -5924,7 +6135,7 @@
"Bearer": []
}
],
"description": "API for creating Product",
"description": "API for creating Product with file upload",
"tags": [
"Products"
],
@ -5938,13 +6149,35 @@
"required": true
},
{
"description": "Required payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/request.ProductsCreateRequest"
}
"type": "file",
"description": "Upload file",
"name": "file",
"in": "formData"
},
{
"type": "string",
"description": "Product title",
"name": "title",
"in": "formData",
"required": true
},
{
"type": "string",
"description": "Product variant",
"name": "variant",
"in": "formData"
},
{
"type": "number",
"description": "Product price",
"name": "price",
"in": "formData"
},
{
"type": "string",
"description": "Product colors (JSON array)",
"name": "colors",
"in": "formData"
}
],
"responses": {
@ -5975,6 +6208,62 @@
}
}
},
"/products/viewer/{filename}": {
"get": {
"security": [
{
"Bearer": []
}
],
"description": "API for viewing Product file",
"tags": [
"Products"
],
"summary": "Viewer Product",
"parameters": [
{
"type": "string",
"description": "Insert the X-Client-Key",
"name": "X-Client-Key",
"in": "header",
"required": true
},
{
"type": "string",
"description": "Product File Path",
"name": "filename",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "file"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.BadRequestError"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/response.UnauthorizedError"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.InternalServerError"
}
}
}
}
},
"/products/{id}": {
"get": {
"security": [
@ -6247,7 +6536,7 @@
"Bearer": []
}
],
"description": "API for creating Promotion",
"description": "API for creating Promotion with file upload",
"tags": [
"Promotions"
],
@ -6261,13 +6550,23 @@
"required": true
},
{
"description": "Required payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/request.PromotionsCreateRequest"
}
"type": "file",
"description": "Upload file",
"name": "file",
"in": "formData"
},
{
"type": "string",
"description": "Promotion title",
"name": "title",
"in": "formData",
"required": true
},
{
"type": "string",
"description": "Promotion description",
"name": "description",
"in": "formData"
}
],
"responses": {
@ -6298,6 +6597,62 @@
}
}
},
"/promotions/viewer/{filename}": {
"get": {
"security": [
{
"Bearer": []
}
],
"description": "API for viewing Promotion file",
"tags": [
"Promotions"
],
"summary": "Viewer Promotion",
"parameters": [
{
"type": "string",
"description": "Insert the X-Client-Key",
"name": "X-Client-Key",
"in": "header",
"required": true
},
{
"type": "string",
"description": "Promotion File Path",
"name": "filename",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "file"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.BadRequestError"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/response.UnauthorizedError"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.InternalServerError"
}
}
}
}
},
"/promotions/{id}": {
"get": {
"security": [
@ -9809,29 +10164,6 @@
}
}
},
"request.BannersCreateRequest": {
"type": "object",
"required": [
"title"
],
"properties": {
"description": {
"type": "string"
},
"position": {
"type": "string"
},
"status": {
"type": "string"
},
"thumbnail_path": {
"type": "string"
},
"title": {
"type": "string"
}
}
},
"request.BannersUpdateRequest": {
"type": "object",
"properties": {
@ -10016,23 +10348,6 @@
}
}
},
"request.GalleryFilesCreateRequest": {
"type": "object",
"required": [
"gallery_id"
],
"properties": {
"gallery_id": {
"type": "integer"
},
"image_path": {
"type": "string"
},
"title": {
"type": "string"
}
}
},
"request.GalleryFilesUpdateRequest": {
"type": "object",
"properties": {
@ -10050,24 +10365,6 @@
}
}
},
"request.ProductSpecificationsCreateRequest": {
"type": "object",
"required": [
"product_id",
"title"
],
"properties": {
"product_id": {
"type": "integer"
},
"thumbnail_path": {
"type": "string"
},
"title": {
"type": "string"
}
}
},
"request.ProductSpecificationsUpdateRequest": {
"type": "object",
"properties": {
@ -10085,32 +10382,6 @@
}
}
},
"request.ProductsCreateRequest": {
"type": "object",
"required": [
"title"
],
"properties": {
"colors": {
"type": "array",
"items": {
"type": "string"
}
},
"price": {
"type": "number"
},
"thumbnail_path": {
"type": "string"
},
"title": {
"type": "string"
},
"variant": {
"type": "string"
}
}
},
"request.ProductsUpdateRequest": {
"type": "object",
"properties": {
@ -10137,23 +10408,6 @@
}
}
},
"request.PromotionsCreateRequest": {
"type": "object",
"required": [
"title"
],
"properties": {
"description": {
"type": "string"
},
"thumbnail_path": {
"type": "string"
},
"title": {
"type": "string"
}
}
},
"request.PromotionsUpdateRequest": {
"type": "object",
"properties": {

View File

@ -281,21 +281,6 @@ definitions:
- title
- typeId
type: object
request.BannersCreateRequest:
properties:
description:
type: string
position:
type: string
status:
type: string
thumbnail_path:
type: string
title:
type: string
required:
- title
type: object
request.BannersUpdateRequest:
properties:
description:
@ -420,17 +405,6 @@ definitions:
title:
type: string
type: object
request.GalleryFilesCreateRequest:
properties:
gallery_id:
type: integer
image_path:
type: string
title:
type: string
required:
- gallery_id
type: object
request.GalleryFilesUpdateRequest:
properties:
gallery_id:
@ -442,18 +416,6 @@ definitions:
title:
type: string
type: object
request.ProductSpecificationsCreateRequest:
properties:
product_id:
type: integer
thumbnail_path:
type: string
title:
type: string
required:
- product_id
- title
type: object
request.ProductSpecificationsUpdateRequest:
properties:
is_active:
@ -465,23 +427,6 @@ definitions:
title:
type: string
type: object
request.ProductsCreateRequest:
properties:
colors:
items:
type: string
type: array
price:
type: number
thumbnail_path:
type: string
title:
type: string
variant:
type: string
required:
- title
type: object
request.ProductsUpdateRequest:
properties:
colors:
@ -499,17 +444,6 @@ definitions:
variant:
type: string
type: object
request.PromotionsCreateRequest:
properties:
description:
type: string
thumbnail_path:
type: string
title:
type: string
required:
- title
type: object
request.PromotionsUpdateRequest:
properties:
description:
@ -3028,19 +2962,34 @@ paths:
tags:
- Banners
post:
description: API for creating Banner
description: API for creating Banner with file upload
parameters:
- description: Insert the X-Client-Key
in: header
name: X-Client-Key
required: true
type: string
- description: Required payload
in: body
name: payload
- description: Upload file
in: formData
name: file
type: file
- description: Banner title
in: formData
name: title
required: true
schema:
$ref: '#/definitions/request.BannersCreateRequest'
type: string
- description: Banner description
in: formData
name: description
type: string
- description: Banner position
in: formData
name: position
type: string
- description: Banner status
in: formData
name: status
type: string
responses:
"200":
description: OK
@ -3175,6 +3124,42 @@ paths:
summary: Update Banner
tags:
- Banners
/banners/viewer/{filename}:
get:
description: API for viewing Banner file
parameters:
- description: Insert the X-Client-Key
in: header
name: X-Client-Key
required: true
type: string
- description: Banner File Path
in: path
name: filename
required: true
type: string
responses:
"200":
description: OK
schema:
type: file
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.BadRequestError'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/response.UnauthorizedError'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.InternalServerError'
security:
- Bearer: []
summary: Viewer Banner
tags:
- Banners
/cities:
get:
description: API for getting all Cities
@ -4267,19 +4252,26 @@ paths:
tags:
- GalleryFiles
post:
description: API for creating GalleryFile
description: API for creating GalleryFile with file upload
parameters:
- description: Insert the X-Client-Key
in: header
name: X-Client-Key
required: true
type: string
- description: Required payload
in: body
name: payload
- description: Upload file
in: formData
name: file
type: file
- description: Gallery ID
in: formData
name: gallery_id
required: true
schema:
$ref: '#/definitions/request.GalleryFilesCreateRequest'
type: integer
- description: Gallery file title
in: formData
name: title
type: string
responses:
"200":
description: OK
@ -4414,6 +4406,42 @@ paths:
summary: Update GalleryFile
tags:
- GalleryFiles
/gallery-files/viewer/{filename}:
get:
description: API for viewing GalleryFile file
parameters:
- description: Insert the X-Client-Key
in: header
name: X-Client-Key
required: true
type: string
- description: Gallery File Path
in: path
name: filename
required: true
type: string
responses:
"200":
description: OK
schema:
type: file
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.BadRequestError'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/response.UnauthorizedError'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.InternalServerError'
security:
- Bearer: []
summary: Viewer GalleryFile
tags:
- GalleryFiles
/product-specifications:
get:
description: API for getting all ProductSpecifications
@ -4476,19 +4504,27 @@ paths:
tags:
- ProductSpecifications
post:
description: API for creating ProductSpecification
description: API for creating ProductSpecification with file upload
parameters:
- description: Insert the X-Client-Key
in: header
name: X-Client-Key
required: true
type: string
- description: Required payload
in: body
name: payload
- description: Upload file
in: formData
name: file
type: file
- description: Product ID
in: formData
name: product_id
required: true
schema:
$ref: '#/definitions/request.ProductSpecificationsCreateRequest'
type: integer
- description: Product specification title
in: formData
name: title
required: true
type: string
responses:
"200":
description: OK
@ -4623,6 +4659,42 @@ paths:
summary: Update ProductSpecification
tags:
- ProductSpecifications
/product-specifications/viewer/{filename}:
get:
description: API for viewing ProductSpecification file
parameters:
- description: Insert the X-Client-Key
in: header
name: X-Client-Key
required: true
type: string
- description: Product Specification File Path
in: path
name: filename
required: true
type: string
responses:
"200":
description: OK
schema:
type: file
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.BadRequestError'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/response.UnauthorizedError'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.InternalServerError'
security:
- Bearer: []
summary: Viewer ProductSpecification
tags:
- ProductSpecifications
/products:
get:
description: API for getting all Products
@ -4685,19 +4757,34 @@ paths:
tags:
- Products
post:
description: API for creating Product
description: API for creating Product with file upload
parameters:
- description: Insert the X-Client-Key
in: header
name: X-Client-Key
required: true
type: string
- description: Required payload
in: body
name: payload
- description: Upload file
in: formData
name: file
type: file
- description: Product title
in: formData
name: title
required: true
schema:
$ref: '#/definitions/request.ProductsCreateRequest'
type: string
- description: Product variant
in: formData
name: variant
type: string
- description: Product price
in: formData
name: price
type: number
- description: Product colors (JSON array)
in: formData
name: colors
type: string
responses:
"200":
description: OK
@ -4832,6 +4919,42 @@ paths:
summary: Update Product
tags:
- Products
/products/viewer/{filename}:
get:
description: API for viewing Product file
parameters:
- description: Insert the X-Client-Key
in: header
name: X-Client-Key
required: true
type: string
- description: Product File Path
in: path
name: filename
required: true
type: string
responses:
"200":
description: OK
schema:
type: file
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.BadRequestError'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/response.UnauthorizedError'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.InternalServerError'
security:
- Bearer: []
summary: Viewer Product
tags:
- Products
/promotions:
get:
description: API for getting all Promotions
@ -4891,19 +5014,26 @@ paths:
tags:
- Promotions
post:
description: API for creating Promotion
description: API for creating Promotion with file upload
parameters:
- description: Insert the X-Client-Key
in: header
name: X-Client-Key
required: true
type: string
- description: Required payload
in: body
name: payload
- description: Upload file
in: formData
name: file
type: file
- description: Promotion title
in: formData
name: title
required: true
schema:
$ref: '#/definitions/request.PromotionsCreateRequest'
type: string
- description: Promotion description
in: formData
name: description
type: string
responses:
"200":
description: OK
@ -5038,6 +5168,42 @@ paths:
summary: Update Promotion
tags:
- Promotions
/promotions/viewer/{filename}:
get:
description: API for viewing Promotion file
parameters:
- description: Insert the X-Client-Key
in: header
name: X-Client-Key
required: true
type: string
- description: Promotion File Path
in: path
name: filename
required: true
type: string
responses:
"200":
description: OK
schema:
type: file
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.BadRequestError'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/response.UnauthorizedError'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.InternalServerError'
security:
- Bearer: []
summary: Viewer Promotion
tags:
- Promotions
/provinces:
get:
description: API for getting all Provinces