diff --git a/app/database/entity/about_us_content_images.entity.go b/app/database/entity/about_us_content_images.entity.go index f5f26bf..973e07c 100644 --- a/app/database/entity/about_us_content_images.entity.go +++ b/app/database/entity/about_us_content_images.entity.go @@ -11,8 +11,7 @@ type AboutUsContentImage struct { CreatedAt time.Time `json:"created_at" gorm:"default:now()"` UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"` - // relation (optional tapi bagus) - AboutUsContent AboutUsContent `json:"about_us_content" gorm:"foreignKey:AboutUsContentID"` + AboutUsContent AboutUsContent `json:"-" gorm:"foreignKey:AboutUsContentID"` } func (AboutUsContentImage) TableName() string { diff --git a/app/database/entity/about_us_contents.entity.go b/app/database/entity/about_us_contents.entity.go index 1f4804a..4cdda88 100644 --- a/app/database/entity/about_us_contents.entity.go +++ b/app/database/entity/about_us_contents.entity.go @@ -12,6 +12,8 @@ type AboutUsContent struct { IsActive *bool `json:"is_active" gorm:"type:bool;default:true"` CreatedAt time.Time `json:"created_at" gorm:"default:now()"` UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"` + + Images []AboutUsContentImage `json:"images,omitempty" gorm:"foreignKey:AboutUsContentID"` } func (AboutUsContent) TableName() string { diff --git a/app/database/entity/hero_contents.entity.go b/app/database/entity/hero_contents.entity.go index 149bdf3..63367f8 100644 --- a/app/database/entity/hero_contents.entity.go +++ b/app/database/entity/hero_contents.entity.go @@ -7,7 +7,7 @@ import ( ) type HeroContents struct { - ID uuid.UUID `json:"id" gorm:"primaryKey;type:uuid;default:uuid_generate_v4()"` + ID uuid.UUID `json:"id" gorm:"primaryKey;type:uuid"` PrimaryTitle string `json:"primary_title" gorm:"type:varchar(255)"` SecondaryTitle string `json:"secondary_title" gorm:"type:varchar(255)"` Description string `json:"description" gorm:"type:text"` diff --git a/app/database/entity/hero_contents_images.entity.go b/app/database/entity/hero_contents_images.entity.go index 6e75a26..492803b 100644 --- a/app/database/entity/hero_contents_images.entity.go +++ b/app/database/entity/hero_contents_images.entity.go @@ -7,7 +7,7 @@ import ( ) type HeroContentImages struct { - ID uuid.UUID `json:"id" gorm:"primaryKey;type:uuid;default:uuid_generate_v4()"` + ID uuid.UUID `json:"id" gorm:"primaryKey;type:uuid"` HeroContentID uuid.UUID `json:"hero_content_id" gorm:"type:uuid;not null"` ImagePath string `json:"image_path" gorm:"type:text"` ImageURL string `json:"image_url" gorm:"type:text"` diff --git a/app/database/entity/our_product_content_images.entity.go b/app/database/entity/our_product_content_images.entity.go index 3868801..6a1e4ea 100644 --- a/app/database/entity/our_product_content_images.entity.go +++ b/app/database/entity/our_product_content_images.entity.go @@ -5,7 +5,7 @@ import ( ) type OurProductContentImage struct { - ID uuid.UUID `json:"id" gorm:"primaryKey;type:uuid;default:uuid_generate_v4()"` + ID uuid.UUID `json:"id" gorm:"primaryKey;type:uuid"` OurProductContentID uuid.UUID `json:"our_product_content_id" gorm:"type:uuid"` ImagePath string `json:"image_path" gorm:"type:varchar(255)"` ImageURL string `json:"image_url" gorm:"type:text"` diff --git a/app/database/entity/our_product_contents.entity.go b/app/database/entity/our_product_contents.entity.go index 1215a0d..f8be114 100644 --- a/app/database/entity/our_product_contents.entity.go +++ b/app/database/entity/our_product_contents.entity.go @@ -7,7 +7,7 @@ import ( ) type OurProductContent struct { - ID uuid.UUID `json:"id" gorm:"primaryKey;type:uuid;default:uuid_generate_v4()"` + ID uuid.UUID `json:"id" gorm:"primaryKey;type:uuid"` PrimaryTitle string `json:"primary_title" gorm:"type:varchar(255)"` SecondaryTitle string `json:"secondary_title" gorm:"type:varchar(255)"` Description string `json:"description" gorm:"type:text"` diff --git a/app/database/entity/partner_contents.entity.go b/app/database/entity/partner_contents.entity.go index b80e01c..050041f 100644 --- a/app/database/entity/partner_contents.entity.go +++ b/app/database/entity/partner_contents.entity.go @@ -7,7 +7,7 @@ import ( ) type PartnerContent struct { - ID uuid.UUID `json:"id" gorm:"primaryKey;type:uuid;default:uuid_generate_v4()"` + ID uuid.UUID `json:"id" gorm:"primaryKey;type:uuid"` PrimaryTitle string `json:"primary_title" gorm:"type:varchar(255)"` ImagePath string `json:"image_path" gorm:"type:varchar(255)"` ImageURL string `json:"image_url" gorm:"type:text"` diff --git a/app/database/index.database.go b/app/database/index.database.go index 833372a..32dfb87 100644 --- a/app/database/index.database.go +++ b/app/database/index.database.go @@ -103,13 +103,13 @@ func Models() []interface{} { entity.Bookmarks{}, entity.Cities{}, entity.Clients{}, + entity.HeroContents{}, + entity.HeroContentImages{}, entity.ClientApprovalSettings{}, entity.CsrfTokenRecords{}, entity.CustomStaticPages{}, entity.Districts{}, entity.Feedbacks{}, - entity.HeroContents{}, - entity.HeroContentImages{}, entity.ForgotPasswords{}, entity.Magazines{}, entity.MagazineFiles{}, @@ -121,7 +121,11 @@ func Models() []interface{} { entity.OneTimePasswords{}, entity.OurProductContent{}, entity.OurProductContentImage{}, + entity.OurServiceContent{}, + entity.OurServiceContentImage{}, entity.PartnerContent{}, + entity.PopupNewsContents{}, + entity.PopupNewsContentImages{}, entity.Subscription{}, entity.Schedules{}, entity.UserLevels{}, diff --git a/app/module/about_us_content_images/about_us_content_images.module.go b/app/module/about_us_content_images/about_us_content_images.module.go index 68e0903..f85dd19 100644 --- a/app/module/about_us_content_images/about_us_content_images.module.go +++ b/app/module/about_us_content_images/about_us_content_images.module.go @@ -46,6 +46,7 @@ func (_i *AboutUsContentImageRouter) RegisterAboutUsContentImageRoutes() { _i.App.Route("/about-us-content-images", func(router fiber.Router) { router.Get("/", aboutUsContentImageController.All) + router.Post("/url", aboutUsContentImageController.SaveRemote) router.Get("/:id", aboutUsContentImageController.Show) // upload image (pakai form-data) diff --git a/app/module/about_us_content_images/controller/about_us_content_images.controller.go b/app/module/about_us_content_images/controller/about_us_content_images.controller.go index af5dfba..f46d9c2 100644 --- a/app/module/about_us_content_images/controller/about_us_content_images.controller.go +++ b/app/module/about_us_content_images/controller/about_us_content_images.controller.go @@ -3,12 +3,14 @@ package controller import ( "strconv" + "web-qudo-be/app/module/about_us_content_images/request" "web-qudo-be/app/module/about_us_content_images/service" "github.com/gofiber/fiber/v2" "github.com/rs/zerolog" utilRes "web-qudo-be/utils/response" + utilVal "web-qudo-be/utils/validator" ) type aboutUsContentImageController struct { @@ -20,6 +22,7 @@ type AboutUsContentImageController interface { All(c *fiber.Ctx) error Show(c *fiber.Ctx) error Save(c *fiber.Ctx) error + SaveRemote(c *fiber.Ctx) error Delete(c *fiber.Ctx) error } @@ -95,6 +98,24 @@ func (_i *aboutUsContentImageController) Save(c *fiber.Ctx) error { }) } +// SaveRemote JSON: public URL for image or video (e.g. CDN .mp4) +func (_i *aboutUsContentImageController) SaveRemote(c *fiber.Ctx) error { + req := new(request.AboutUsContentImageRemoteRequest) + if err := utilVal.ParseAndValidate(c, req); err != nil { + return err + } + result, err := _i.service.SaveRemoteURL(req.AboutUsContentID, req.MediaURL, req.MediaType) + if err != nil { + _i.Log.Error().Err(err).Msg("failed save remote about us media") + return err + } + return utilRes.Resp(c, utilRes.Response{ + Success: true, + Messages: utilRes.Messages{"About us media URL saved"}, + Data: result, + }) +} + // DELETE func (_i *aboutUsContentImageController) Delete(c *fiber.Ctx) error { id, err := strconv.Atoi(c.Params("id")) diff --git a/app/module/about_us_content_images/repository/about_us_content_images.repository.go b/app/module/about_us_content_images/repository/about_us_content_images.repository.go index 5570e85..ddf10eb 100644 --- a/app/module/about_us_content_images/repository/about_us_content_images.repository.go +++ b/app/module/about_us_content_images/repository/about_us_content_images.repository.go @@ -35,9 +35,7 @@ func NewAboutUsContentImageRepository(db *database.Database, log zerolog.Logger) // GET ALL func (_i *aboutUsContentImageRepository) GetAll() (images []*entity.AboutUsContentImage, err error) { - err = _i.DB.DB. - Where("is_active = ?", true). - Find(&images).Error + err = _i.DB.DB.Find(&images).Error return } @@ -52,7 +50,7 @@ func (_i *aboutUsContentImageRepository) FindOne(id uint) (image *entity.AboutUs // GET BY ABOUT US CONTENT ID func (_i *aboutUsContentImageRepository) FindByContentID(contentID uint) (images []*entity.AboutUsContentImage, err error) { err = _i.DB.DB. - Where("about_us_content_id = ? AND is_active = ?", contentID, true). + Where("about_us_content_id = ?", contentID). Find(&images).Error return } diff --git a/app/module/about_us_content_images/request/about_us_content_images.request.go b/app/module/about_us_content_images/request/about_us_content_images.request.go new file mode 100644 index 0000000..c065ffa --- /dev/null +++ b/app/module/about_us_content_images/request/about_us_content_images.request.go @@ -0,0 +1,7 @@ +package request + +type AboutUsContentImageRemoteRequest struct { + AboutUsContentID uint `json:"about_us_content_id" validate:"required"` + MediaURL string `json:"media_url" validate:"required"` + MediaType string `json:"media_type"` +} diff --git a/app/module/about_us_content_images/service/about_us_content_images.service.go b/app/module/about_us_content_images/service/about_us_content_images.service.go index cea0775..0c1b680 100644 --- a/app/module/about_us_content_images/service/about_us_content_images.service.go +++ b/app/module/about_us_content_images/service/about_us_content_images.service.go @@ -4,6 +4,7 @@ import ( "fmt" "mime/multipart" "path/filepath" + "strings" "time" "web-qudo-be/app/database/entity" "web-qudo-be/app/module/about_us_content_images/repository" @@ -22,6 +23,7 @@ type AboutUsContentImageService interface { All() (images []*entity.AboutUsContentImage, err error) Show(id uint) (image *entity.AboutUsContentImage, err error) Save(aboutUsContentId uint, file *multipart.FileHeader) (image *entity.AboutUsContentImage, err error) + SaveRemoteURL(aboutUsContentID uint, mediaURL, mediaType string) (image *entity.AboutUsContentImage, err error) Delete(id uint) error } @@ -50,9 +52,8 @@ func (_i *aboutUsContentImageService) Save(aboutUsContentId uint, file *multipar Str("filename", file.Filename). Msg("upload image") - // validasi file - ext := filepath.Ext(file.Filename) - if ext != ".jpg" && ext != ".jpeg" && ext != ".png" { + ext := filepath.Ext(strings.ToLower(file.Filename)) + if ext != ".jpg" && ext != ".jpeg" && ext != ".png" && ext != ".mp4" && ext != ".webm" { return nil, fmt.Errorf("invalid file type") } @@ -68,10 +69,14 @@ func (_i *aboutUsContentImageService) Save(aboutUsContentId uint, file *multipar } // save ke DB + mt := "image/" + strings.TrimPrefix(ext, ".") + if ext == ".mp4" || ext == ".webm" { + mt = "video/" + strings.TrimPrefix(ext, ".") + } data := &entity.AboutUsContentImage{ AboutUsContentID: aboutUsContentId, MediaPath: filePath, - MediaType: ext, + MediaType: mt, MediaURL: "/uploads/" + filename, } @@ -84,6 +89,27 @@ func (_i *aboutUsContentImageService) Save(aboutUsContentId uint, file *multipar return result, nil } +func (_i *aboutUsContentImageService) SaveRemoteURL(aboutUsContentID uint, mediaURL, mediaType string) (image *entity.AboutUsContentImage, err error) { + if strings.TrimSpace(mediaURL) == "" { + return nil, fmt.Errorf("media_url is required") + } + mt := mediaType + if mt == "" { + lower := strings.ToLower(mediaURL) + if strings.HasSuffix(lower, ".mp4") || strings.Contains(lower, "video") { + mt = "video/mp4" + } else { + mt = "image/url" + } + } + data := &entity.AboutUsContentImage{ + AboutUsContentID: aboutUsContentID, + MediaURL: mediaURL, + MediaType: mt, + } + return _i.Repo.Create(data) +} + func (_i *aboutUsContentImageService) Delete(id uint) error { return _i.Repo.Delete(id) } diff --git a/app/module/about_us_contents/controller/about_us_contents.controller.go b/app/module/about_us_contents/controller/about_us_contents.controller.go index 000e715..36d9cbd 100644 --- a/app/module/about_us_contents/controller/about_us_contents.controller.go +++ b/app/module/about_us_contents/controller/about_us_contents.controller.go @@ -3,6 +3,7 @@ package controller import ( "strconv" + "web-qudo-be/app/module/about_us_contents/request" "web-qudo-be/app/module/about_us_contents/service" "github.com/gofiber/fiber/v2" @@ -71,13 +72,13 @@ func (_i *aboutUsContentController) Show(c *fiber.Ctx) error { // CREATE func (_i *aboutUsContentController) Save(c *fiber.Ctx) error { - req := new(map[string]interface{}) + req := new(request.AboutUsContentCreateRequest) if err := utilVal.ParseAndValidate(c, req); err != nil { return err } - result, err := _i.service.Save(*req) + result, err := _i.service.Save(req.ToEntity()) if err != nil { _i.Log.Error().Err(err).Msg("failed create about us content") return err @@ -97,13 +98,13 @@ func (_i *aboutUsContentController) Update(c *fiber.Ctx) error { return err } - req := new(map[string]interface{}) + req := new(request.AboutUsContentUpdateRequest) if err := utilVal.ParseAndValidate(c, req); err != nil { return err } - err = _i.service.Update(uint(id), *req) + err = _i.service.Update(uint(id), req.ToEntity()) if err != nil { _i.Log.Error().Err(err).Msg("failed update about us content") return err diff --git a/app/module/about_us_contents/repository/about_use_contents.repository.go b/app/module/about_us_contents/repository/about_use_contents.repository.go index a2abcfc..bbc7ac0 100644 --- a/app/module/about_us_contents/repository/about_use_contents.repository.go +++ b/app/module/about_us_contents/repository/about_use_contents.repository.go @@ -34,7 +34,9 @@ func (r *aboutUsContentRepository) GetAll() ([]*entity.AboutUsContent, error) { var results []*entity.AboutUsContent err := r.DB.DB. - Where("is_active = ?", true). + Preload("Images"). + Where("is_active IS NULL OR is_active = ?", true). + Order("id ASC"). Find(&results).Error if err != nil { @@ -69,12 +71,24 @@ func (r *aboutUsContentRepository) Create(data *entity.AboutUsContent) (*entity. return data, nil } -// UPDATE +// Update uses a column map so we never SET primary key / created_at to zero values (breaks FK children). func (r *aboutUsContentRepository) Update(id uint, data *entity.AboutUsContent) error { + updates := map[string]interface{}{ + "primary_title": data.PrimaryTitle, + "secondary_title": data.SecondaryTitle, + "description": data.Description, + "primary_cta": data.PrimaryCta, + "secondary_cta_text": data.SecondaryCtaText, + "updated_at": data.UpdatedAt, + } + if data.IsActive != nil { + updates["is_active"] = data.IsActive + } + err := r.DB.DB. Model(&entity.AboutUsContent{}). Where("id = ?", id). - Updates(data).Error + Updates(updates).Error if err != nil { r.Log.Error().Err(err).Msg("failed update about us content") diff --git a/app/module/about_us_contents/service/about_use_contents.service.go b/app/module/about_us_contents/service/about_use_contents.service.go index 75096e9..4bc0bcc 100644 --- a/app/module/about_us_contents/service/about_use_contents.service.go +++ b/app/module/about_us_contents/service/about_use_contents.service.go @@ -1,6 +1,8 @@ package service import ( + "time" + "github.com/rs/zerolog" "web-qudo-be/app/database/entity" @@ -15,8 +17,8 @@ type aboutUsContentService struct { type AboutUsContentService interface { All() ([]*entity.AboutUsContent, error) Show(id uint) (*entity.AboutUsContent, error) - Save(data map[string]interface{}) (*entity.AboutUsContent, error) - Update(id uint, data map[string]interface{}) error + Save(data *entity.AboutUsContent) (*entity.AboutUsContent, error) + Update(id uint, data *entity.AboutUsContent) error Delete(id uint) error } @@ -53,26 +55,8 @@ func (s *aboutUsContentService) Show(id uint) (*entity.AboutUsContent, error) { } // CREATE -func (s *aboutUsContentService) Save(data map[string]interface{}) (*entity.AboutUsContent, error) { - entityData := &entity.AboutUsContent{} - - if v, ok := data["primary_title"].(string); ok { - entityData.PrimaryTitle = v - } - if v, ok := data["secondary_title"].(string); ok { - entityData.SecondaryTitle = v - } - if v, ok := data["description"].(string); ok { - entityData.Description = v - } - if v, ok := data["primary_cta"].(string); ok { - entityData.PrimaryCta = v - } - if v, ok := data["secondary_cta_text"].(string); ok { - entityData.SecondaryCtaText = v - } - - result, err := s.Repo.Create(entityData) +func (s *aboutUsContentService) Save(data *entity.AboutUsContent) (*entity.AboutUsContent, error) { + result, err := s.Repo.Create(data) if err != nil { s.Log.Error().Err(err).Msg("failed create about us content") return nil, err @@ -82,26 +66,8 @@ func (s *aboutUsContentService) Save(data map[string]interface{}) (*entity.About } // UPDATE -func (s *aboutUsContentService) Update(id uint, data map[string]interface{}) error { - entityData := &entity.AboutUsContent{} - - if v, ok := data["primary_title"].(string); ok { - entityData.PrimaryTitle = v - } - if v, ok := data["secondary_title"].(string); ok { - entityData.SecondaryTitle = v - } - if v, ok := data["description"].(string); ok { - entityData.Description = v - } - if v, ok := data["primary_cta"].(string); ok { - entityData.PrimaryCta = v - } - if v, ok := data["secondary_cta_text"].(string); ok { - entityData.SecondaryCtaText = v - } - - err := s.Repo.Update(id, entityData) +func (s *aboutUsContentService) Update(id uint, data *entity.AboutUsContent) error { + err := s.Repo.Update(id, data) if err != nil { s.Log.Error().Err(err).Msg("failed update about us content") return err @@ -119,6 +85,7 @@ func (s *aboutUsContentService) Delete(id uint) error { isActive := false result.IsActive = &isActive + result.UpdatedAt = time.Now() return s.Repo.Update(id, result) } \ No newline at end of file diff --git a/app/module/hero_content_images/controller/hero_content_images.controller.go b/app/module/hero_content_images/controller/hero_content_images.controller.go index 1bf78eb..4c1f0bc 100644 --- a/app/module/hero_content_images/controller/hero_content_images.controller.go +++ b/app/module/hero_content_images/controller/hero_content_images.controller.go @@ -1,6 +1,8 @@ package controller import ( + "strings" + "github.com/gofiber/fiber/v2" "github.com/google/uuid" "github.com/rs/zerolog" diff --git a/app/module/hero_content_images/service/hero_content_images.service.go b/app/module/hero_content_images/service/hero_content_images.service.go index 066198e..9ef884e 100644 --- a/app/module/hero_content_images/service/hero_content_images.service.go +++ b/app/module/hero_content_images/service/hero_content_images.service.go @@ -1,32 +1,41 @@ package service import ( + "mime/multipart" + "github.com/google/uuid" "github.com/rs/zerolog" "web-qudo-be/app/database/entity" "web-qudo-be/app/module/hero_content_images/repository" + minioStorage "web-qudo-be/config/config" + "web-qudo-be/utils/storage" ) type heroContentImagesService struct { - Repo repository.HeroContentImagesRepository - Log zerolog.Logger + Repo repository.HeroContentImagesRepository + MinioStorage *minioStorage.MinioStorage + Log zerolog.Logger } type HeroContentImagesService interface { FindByHeroID(heroID uuid.UUID) (*entity.HeroContentImages, error) Save(data *entity.HeroContentImages) (*entity.HeroContentImages, error) + SaveWithFile(heroContentID uuid.UUID, file *multipart.FileHeader) (*entity.HeroContentImages, error) Update(id uuid.UUID, data *entity.HeroContentImages) error + UpdateWithFile(id uuid.UUID, file *multipart.FileHeader) error Delete(id uuid.UUID) error } func NewHeroContentImagesService( repo repository.HeroContentImagesRepository, + minio *minioStorage.MinioStorage, log zerolog.Logger, ) HeroContentImagesService { return &heroContentImagesService{ - Repo: repo, - Log: log, + Repo: repo, + MinioStorage: minio, + Log: log, } } @@ -52,6 +61,19 @@ func (s *heroContentImagesService) Save(data *entity.HeroContentImages) (*entity return result, nil } +func (s *heroContentImagesService) SaveWithFile(heroContentID uuid.UUID, file *multipart.FileHeader) (*entity.HeroContentImages, error) { + key, url, err := storage.UploadCMSObject(s.MinioStorage, "hero", file, false) + if err != nil { + return nil, err + } + data := &entity.HeroContentImages{ + HeroContentID: heroContentID, + ImagePath: key, + ImageURL: url, + } + return s.Save(data) +} + func (s *heroContentImagesService) Update(id uuid.UUID, data *entity.HeroContentImages) error { err := s.Repo.Update(id, data) if err != nil { @@ -62,6 +84,17 @@ func (s *heroContentImagesService) Update(id uuid.UUID, data *entity.HeroContent return nil } +func (s *heroContentImagesService) UpdateWithFile(id uuid.UUID, file *multipart.FileHeader) error { + key, url, err := storage.UploadCMSObject(s.MinioStorage, "hero", file, false) + if err != nil { + return err + } + return s.Repo.Update(id, &entity.HeroContentImages{ + ImagePath: key, + ImageURL: url, + }) +} + func (s *heroContentImagesService) Delete(id uuid.UUID) error { err := s.Repo.Delete(id) if err != nil { @@ -70,4 +103,4 @@ func (s *heroContentImagesService) Delete(id uuid.UUID) error { } return nil -} \ No newline at end of file +} diff --git a/app/module/hero_contents/repository/hero_contents.repository.go b/app/module/hero_contents/repository/hero_contents.repository.go index 5f02682..3460d52 100644 --- a/app/module/hero_contents/repository/hero_contents.repository.go +++ b/app/module/hero_contents/repository/hero_contents.repository.go @@ -1,11 +1,13 @@ package repository import ( + "errors" "web-qudo-be/app/database" "web-qudo-be/app/database/entity" "github.com/google/uuid" "github.com/rs/zerolog" + "gorm.io/gorm" ) type heroContentsRepository struct { @@ -35,6 +37,9 @@ func (r *heroContentsRepository) Get() (*entity.HeroContents, error) { First(&data).Error if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil + } r.Log.Error().Err(err).Msg("failed get hero content") return nil, err } diff --git a/app/module/our_product_content_images/controller/our_product_content_images.controller.go b/app/module/our_product_content_images/controller/our_product_content_images.controller.go index c20d068..b61cb0c 100644 --- a/app/module/our_product_content_images/controller/our_product_content_images.controller.go +++ b/app/module/our_product_content_images/controller/our_product_content_images.controller.go @@ -32,7 +32,7 @@ func NewOurProductContentImagesController(service service.OurProductContentImage } func (_i *ourProductContentImagesController) FindByOurProductContentID(c *fiber.Ctx) error { - contentIDStr := c.Params("our_product_content_id") + contentIDStr := c.Params("content_id") contentID, err := uuid.Parse(contentIDStr) if err != nil { diff --git a/app/module/our_product_contents/repository/our_product_contents.repository.go b/app/module/our_product_contents/repository/our_product_contents.repository.go index 0a4e77a..c0ef12e 100644 --- a/app/module/our_product_contents/repository/our_product_contents.repository.go +++ b/app/module/our_product_contents/repository/our_product_contents.repository.go @@ -1,11 +1,13 @@ package repository import ( + "errors" "web-qudo-be/app/database" "web-qudo-be/app/database/entity" "github.com/google/uuid" "github.com/rs/zerolog" + "gorm.io/gorm" ) type ourProductContentRepository struct { @@ -15,6 +17,7 @@ type ourProductContentRepository struct { type OurProductContentRepository interface { Get() (*entity.OurProductContent, error) + GetAll() ([]entity.OurProductContent, error) Create(data *entity.OurProductContent) (*entity.OurProductContent, error) Update(id uuid.UUID, data *entity.OurProductContent) error Delete(id uuid.UUID) error @@ -35,6 +38,9 @@ func (r *ourProductContentRepository) Get() (*entity.OurProductContent, error) { First(&data).Error if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil + } r.Log.Error().Err(err).Msg("failed get our product content") return nil, err } @@ -42,6 +48,19 @@ func (r *ourProductContentRepository) Get() (*entity.OurProductContent, error) { return &data, nil } +func (r *ourProductContentRepository) GetAll() ([]entity.OurProductContent, error) { + var rows []entity.OurProductContent + err := r.DB.DB. + Preload("Images"). + Order("created_at ASC"). + Find(&rows).Error + if err != nil { + r.Log.Error().Err(err).Msg("failed list our product contents") + return nil, err + } + return rows, nil +} + func (r *ourProductContentRepository) Create(data *entity.OurProductContent) (*entity.OurProductContent, error) { data.ID = uuid.New() diff --git a/app/module/our_product_contents/service/our_product_contents.service.go b/app/module/our_product_contents/service/our_product_contents.service.go index 6f2168d..993c2c4 100644 --- a/app/module/our_product_contents/service/our_product_contents.service.go +++ b/app/module/our_product_contents/service/our_product_contents.service.go @@ -16,7 +16,7 @@ type ourProductContentService struct { } type OurProductContentService interface { - Show() (*entity.OurProductContent, error) + Show() ([]entity.OurProductContent, error) Save(data *entity.OurProductContent) (*entity.OurProductContent, error) Update(id uuid.UUID, data *entity.OurProductContent) error Delete(id uuid.UUID) error @@ -34,14 +34,13 @@ func NewOurProductContentService( } } -func (s *ourProductContentService) Show() (*entity.OurProductContent, error) { - data, err := s.Repo.Get() +func (s *ourProductContentService) Show() ([]entity.OurProductContent, error) { + rows, err := s.Repo.GetAll() if err != nil { - s.Log.Error().Err(err).Msg("failed get our product content") + s.Log.Error().Err(err).Msg("failed list our product contents") return nil, err } - - return data, nil + return rows, nil } func (s *ourProductContentService) Save(data *entity.OurProductContent) (*entity.OurProductContent, error) { @@ -67,13 +66,5 @@ func (s *ourProductContentService) Update(id uuid.UUID, data *entity.OurProductC } func (s *ourProductContentService) Delete(id uuid.UUID) error { - result, err := s.Repo.Get() - if err != nil { - return err - } - - isActive := false - result.IsActive = &isActive - - return s.Repo.Update(id, result) + return s.Repo.Delete(id) } \ No newline at end of file diff --git a/app/module/our_service_contents/repository/our_service_contents.repository.go b/app/module/our_service_contents/repository/our_service_contents.repository.go index 05977d1..59553e6 100644 --- a/app/module/our_service_contents/repository/our_service_contents.repository.go +++ b/app/module/our_service_contents/repository/our_service_contents.repository.go @@ -1,10 +1,12 @@ package repository import ( + "errors" "web-qudo-be/app/database" "web-qudo-be/app/database/entity" "github.com/rs/zerolog" + "gorm.io/gorm" ) type ourServiceContentRepository struct { @@ -14,6 +16,7 @@ type ourServiceContentRepository struct { type OurServiceContentRepository interface { Get() (*entity.OurServiceContent, error) + GetAll() ([]entity.OurServiceContent, error) Create(data *entity.OurServiceContent) (*entity.OurServiceContent, error) Update(id uint, data *entity.OurServiceContent) error Delete(id uint) error @@ -34,6 +37,9 @@ func (r *ourServiceContentRepository) Get() (*entity.OurServiceContent, error) { First(&data).Error if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil + } r.Log.Error().Err(err).Msg("failed get our service content") return nil, err } @@ -41,6 +47,19 @@ func (r *ourServiceContentRepository) Get() (*entity.OurServiceContent, error) { return &data, nil } +func (r *ourServiceContentRepository) GetAll() ([]entity.OurServiceContent, error) { + var rows []entity.OurServiceContent + err := r.DB.DB. + Preload("Images"). + Order("created_at ASC"). + Find(&rows).Error + if err != nil { + r.Log.Error().Err(err).Msg("failed list our service contents") + return nil, err + } + return rows, nil +} + func (r *ourServiceContentRepository) Create(data *entity.OurServiceContent) (*entity.OurServiceContent, error) { err := r.DB.DB.Create(data).Error if err != nil { diff --git a/app/module/our_service_contents/service/our_service_contents.service.go b/app/module/our_service_contents/service/our_service_contents.service.go index e242bba..2631fab 100644 --- a/app/module/our_service_contents/service/our_service_contents.service.go +++ b/app/module/our_service_contents/service/our_service_contents.service.go @@ -15,7 +15,7 @@ type ourServiceContentService struct { } type OurServiceContentService interface { - Show() (*entity.OurServiceContent, error) + Show() ([]entity.OurServiceContent, error) Save(data *entity.OurServiceContent) (*entity.OurServiceContent, error) Update(id uint, data *entity.OurServiceContent) error Delete(id uint) error @@ -33,14 +33,13 @@ func NewOurServiceContentService( } } -func (s *ourServiceContentService) Show() (*entity.OurServiceContent, error) { - data, err := s.Repo.Get() +func (s *ourServiceContentService) Show() ([]entity.OurServiceContent, error) { + rows, err := s.Repo.GetAll() if err != nil { - s.Log.Error().Err(err).Msg("failed get our service content") + s.Log.Error().Err(err).Msg("failed list our service contents") return nil, err } - - return data, nil + return rows, nil } func (s *ourServiceContentService) Save(data *entity.OurServiceContent) (*entity.OurServiceContent, error) { @@ -75,13 +74,5 @@ func (s *ourServiceContentService) Update(id uint, data *entity.OurServiceConten } func (s *ourServiceContentService) Delete(id uint) error { - result, err := s.Repo.Get() - if err != nil { - return err - } - - isActive := false - result.IsActive = &isActive - - return s.Repo.Update(id, result) + return s.Repo.Delete(id) } \ No newline at end of file diff --git a/config/config/minio.config.go b/config/config/minio.config.go index 211db47..f17dec8 100644 --- a/config/config/minio.config.go +++ b/config/config/minio.config.go @@ -2,6 +2,7 @@ package config import ( "context" + "fmt" "log" "github.com/minio/minio-go/v7" @@ -69,3 +70,13 @@ func (_minio *MinioStorage) ConnectMinio() (*minio.Client, error) { log.Printf("[MinIO] Successfully connected to MinIO and bucket '%s' is ready", bucketName) return minioClient, nil } + +// PublicObjectURL builds a path-style URL for public reads (bucket policy must allow GET). +func (m *MinioStorage) PublicObjectURL(objectKey string) string { + o := m.Cfg.ObjectStorage.MinioStorage + scheme := "http" + if o.UseSSL { + scheme = "https" + } + return fmt.Sprintf("%s://%s/%s/%s", scheme, o.Endpoint, o.BucketName, objectKey) +} diff --git a/config/toml/config.toml b/config/toml/config.toml index 73a2e73..56c2843 100644 --- a/config/toml/config.toml +++ b/config/toml/config.toml @@ -12,7 +12,7 @@ production = false body-limit = 1048576000 # "100 * 1024 * 1024" [db.postgres] -dsn = "postgresql://medols_user:MedolsDB@2025@38.47.185.79:5432/medols_db" # ://:@:/ +dsn = "postgresql://qudo_user:QudoDB@2026@38.47.185.79:5432/qudo_db" # ://:@:/ log-mode = "ERROR" migrate = true seed = false @@ -28,7 +28,7 @@ endpoint = "is3.cloudhost.id" access-key-id = "YRP1RM617986USRU6NN8" secret-access-key = "vfbwQDYb1m7nfzo4LVEz90BIyOWfBMZ6bfGQbqDO" use-ssl = true -bucket-name = "mikulnews" +bucket-name = "qudo" location = "us-east-1" [middleware.compress] diff --git a/utils/storage/cms_upload.go b/utils/storage/cms_upload.go new file mode 100644 index 0000000..71dc9bf --- /dev/null +++ b/utils/storage/cms_upload.go @@ -0,0 +1,69 @@ +package storage + +import ( + "context" + "fmt" + "mime" + "mime/multipart" + "path/filepath" + "strings" + "time" + + "github.com/google/uuid" + "github.com/minio/minio-go/v7" + + appcfg "web-qudo-be/config/config" +) + +var imageExts = map[string]bool{ + ".jpg": true, ".jpeg": true, ".png": true, ".gif": true, ".webp": true, +} + +var mediaExts = map[string]bool{ + ".jpg": true, ".jpeg": true, ".png": true, ".gif": true, ".webp": true, + ".mp4": true, ".webm": true, +} + +// UploadCMSObject stores a file in MinIO under cms/{folder}/YYYY/MM/{uuid}{ext} and returns object key + public URL. +func UploadCMSObject(ms *appcfg.MinioStorage, folder string, file *multipart.FileHeader, allowVideo bool) (objectKey string, publicURL string, err error) { + if file == nil { + return "", "", fmt.Errorf("file is required") + } + ext := strings.ToLower(filepath.Ext(file.Filename)) + if allowVideo { + if !mediaExts[ext] { + return "", "", fmt.Errorf("unsupported file type (allowed: images, mp4, webm)") + } + } else if !imageExts[ext] { + return "", "", fmt.Errorf("unsupported image type") + } + + client, err := ms.ConnectMinio() + if err != nil { + return "", "", err + } + + src, err := file.Open() + if err != nil { + return "", "", err + } + defer src.Close() + + bucket := ms.Cfg.ObjectStorage.MinioStorage.BucketName + now := time.Now() + objectKey = fmt.Sprintf("cms/%s/%d/%02d/%s%s", folder, now.Year(), int(now.Month()), uuid.New().String(), ext) + + contentType := mime.TypeByExtension(ext) + if contentType == "" { + contentType = "application/octet-stream" + } + + _, err = client.PutObject(context.Background(), bucket, objectKey, src, file.Size, minio.PutObjectOptions{ + ContentType: contentType, + }) + if err != nil { + return "", "", err + } + + return objectKey, ms.PublicObjectURL(objectKey), nil +} diff --git a/web-qudo-be.exe b/web-qudo-be.exe index 162f894..5cf48d9 100644 Binary files a/web-qudo-be.exe and b/web-qudo-be.exe differ