diff --git a/internal/handler/article_handler.go b/internal/handler/article_handler.go index d8405f8..bc33c7f 100644 --- a/internal/handler/article_handler.go +++ b/internal/handler/article_handler.go @@ -1,6 +1,8 @@ package handler import ( + "fmt" + "mime/multipart" "strconv" "github.com/gofiber/fiber/v2" @@ -43,7 +45,6 @@ func (h *ArticleHandler) CreateArticle(c *fiber.Ctx) error { } func (h *ArticleHandler) GetAllArticles(c *fiber.Ctx) error { - page, err := strconv.Atoi(c.Query("page", "0")) if err != nil || page < 1 { page = 0 @@ -54,24 +55,17 @@ func (h *ArticleHandler) GetAllArticles(c *fiber.Ctx) error { limit = 0 } - var articles []dto.ArticleResponseDTO - var totalArticles int - - if page == 0 && limit == 0 { - - articles, totalArticles, err = h.ArticleService.GetAllArticles(0, 0) - if err != nil { - return utils.GenericResponse(c, fiber.StatusInternalServerError, "Failed to fetch articles") - } - - return utils.NonPaginatedResponse(c, articles, totalArticles, "Articles fetched successfully") - } - - articles, totalArticles, err = h.ArticleService.GetAllArticles(page, limit) + articles, totalArticles, err := h.ArticleService.GetAllArticles(page, limit) if err != nil { return utils.GenericResponse(c, fiber.StatusInternalServerError, "Failed to fetch articles") } + fmt.Printf("Total Articles: %d\n", totalArticles) + + if page == 0 && limit == 0 { + return utils.NonPaginatedResponse(c, articles, totalArticles, "Articles fetched successfully") + } + return utils.PaginatedResponse(c, articles, page, limit, totalArticles, "Articles fetched successfully") } @@ -83,7 +77,8 @@ func (h *ArticleHandler) GetArticleByID(c *fiber.Ctx) error { article, err := h.ArticleService.GetArticleByID(id) if err != nil { - return utils.GenericResponse(c, fiber.StatusNotFound, err.Error()) + + return utils.GenericResponse(c, fiber.StatusNotFound, "Article not found") } return utils.SuccessResponse(c, article, "Article fetched successfully") @@ -96,7 +91,6 @@ func (h *ArticleHandler) UpdateArticle(c *fiber.Ctx) error { } var request dto.RequestArticleDTO - if err := c.BodyParser(&request); err != nil { return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid body"}}) } @@ -106,6 +100,7 @@ func (h *ArticleHandler) UpdateArticle(c *fiber.Ctx) error { return utils.ValidationErrorResponse(c, errors) } + var coverImage *multipart.FileHeader coverImage, err := c.FormFile("coverImage") if err != nil && err.Error() != "no such file" { return utils.GenericResponse(c, fiber.StatusBadRequest, "Cover image is required") diff --git a/internal/repositories/article_repo.go b/internal/repositories/article_repo.go index 493a136..3f22c6e 100644 --- a/internal/repositories/article_repo.go +++ b/internal/repositories/article_repo.go @@ -1,6 +1,8 @@ package repositories import ( + "fmt" + "github.com/pahmiudahgede/senggoldong/model" "gorm.io/gorm" ) @@ -28,7 +30,10 @@ func (r *articleRepository) FindArticleByID(id string) (*model.Article, error) { var article model.Article err := r.DB.Where("id = ?", id).First(&article).Error if err != nil { - return nil, err + if err == gorm.ErrRecordNotFound { + return nil, fmt.Errorf("article with ID %s not found", id) + } + return nil, fmt.Errorf("failed to fetch article: %v", err) } return &article, nil } @@ -37,21 +42,21 @@ func (r *articleRepository) FindAllArticles(page, limit int) ([]model.Article, i var articles []model.Article var total int64 - err := r.DB.Model(&model.Article{}).Count(&total).Error - if err != nil { - return nil, 0, err + if err := r.DB.Model(&model.Article{}).Count(&total).Error; err != nil { + return nil, 0, fmt.Errorf("failed to count articles: %v", err) } + fmt.Printf("Total Articles Count: %d\n", total) + if page > 0 && limit > 0 { err := r.DB.Offset((page - 1) * limit).Limit(limit).Find(&articles).Error if err != nil { - return nil, 0, err + return nil, 0, fmt.Errorf("failed to fetch articles: %v", err) } } else { - err := r.DB.Find(&articles).Error if err != nil { - return nil, 0, err + return nil, 0, fmt.Errorf("failed to fetch articles: %v", err) } } @@ -59,5 +64,5 @@ func (r *articleRepository) FindAllArticles(page, limit int) ([]model.Article, i } func (r *articleRepository) UpdateArticle(id string, article *model.Article) error { - return r.DB.Save(article).Error + return r.DB.Model(&model.Article{}).Where("id = ?", id).Updates(article).Error } diff --git a/internal/services/article_service.go b/internal/services/article_service.go index 702b290..a50df96 100644 --- a/internal/services/article_service.go +++ b/internal/services/article_service.go @@ -1,6 +1,7 @@ package services import ( + "encoding/json" "fmt" "mime/multipart" "os" @@ -36,8 +37,9 @@ func (s *articleService) CreateArticle(request dto.RequestArticleDTO, coverImage return nil, fmt.Errorf("failed to create directory for cover image: %v", err) } + allowedExtensions := map[string]bool{".jpg": true, ".jpeg": true, ".png": true} extension := filepath.Ext(coverImage.Filename) - if extension != ".jpg" && extension != ".jpeg" && extension != ".png" { + if !allowedExtensions[extension] { return nil, fmt.Errorf("invalid file type, only .jpg, .jpeg, and .png are allowed") } @@ -90,83 +92,77 @@ func (s *articleService) CreateArticle(request dto.RequestArticleDTO, coverImage cacheData := map[string]interface{}{ "data": articleResponseDTO, } - if err := utils.SetJSONData(cacheKey, cacheData, time.Hour*24); err != nil { - fmt.Printf("Error caching article to Redis: %v\n", err) } - articlesCacheKey := "articles:all" - if err := utils.DeleteData(articlesCacheKey); err != nil { - - fmt.Printf("Error deleting articles cache: %v\n", err) + articles, total, err := s.ArticleRepo.FindAllArticles(0, 0) + if err != nil { + fmt.Printf("Error fetching all articles: %v\n", err) } - articles, _, err := s.ArticleRepo.FindAllArticles(0, 0) - if err == nil { - var articleDTOs []dto.ArticleResponseDTO - for _, a := range articles { - createdAt, _ := utils.FormatDateToIndonesianFormat(a.PublishedAt) - updatedAt, _ := utils.FormatDateToIndonesianFormat(a.UpdatedAt) + var articleDTOs []dto.ArticleResponseDTO + for _, a := range articles { + createdAt, _ := utils.FormatDateToIndonesianFormat(a.PublishedAt) + updatedAt, _ := utils.FormatDateToIndonesianFormat(a.UpdatedAt) - articleDTOs = append(articleDTOs, dto.ArticleResponseDTO{ - ID: a.ID, - Title: a.Title, - CoverImage: a.CoverImage, - Author: a.Author, - Heading: a.Heading, - Content: a.Content, - PublishedAt: createdAt, - UpdatedAt: updatedAt, - }) - } + articleDTOs = append(articleDTOs, dto.ArticleResponseDTO{ + ID: a.ID, + Title: a.Title, + CoverImage: a.CoverImage, + Author: a.Author, + Heading: a.Heading, + Content: a.Content, + PublishedAt: createdAt, + UpdatedAt: updatedAt, + }) + } - cacheData = map[string]interface{}{ - "data": articleDTOs, - } - if err := utils.SetJSONData(articlesCacheKey, cacheData, time.Hour*24); err != nil { - - fmt.Printf("Error caching all articles to Redis: %v\n", err) - } - } else { - - fmt.Printf("Error fetching all articles: %v\n", err) + articlesCacheKey := "articles:all" + cacheData = map[string]interface{}{ + "data": articleDTOs, + "total": total, + } + if err := utils.SetJSONData(articlesCacheKey, cacheData, time.Hour*24); err != nil { + fmt.Printf("Error caching all articles to Redis: %v\n", err) } return articleResponseDTO, nil } func (s *articleService) GetAllArticles(page, limit int) ([]dto.ArticleResponseDTO, int, error) { + var cacheKey string - cacheKey := fmt.Sprintf("articles_page:%d_limit:%d", page, limit) + if page == 0 && limit == 0 { + cacheKey = "articles:all" + cachedData, err := utils.GetJSONData(cacheKey) + if err == nil && cachedData != nil { + if data, ok := cachedData["data"].([]interface{}); ok { + var articles []dto.ArticleResponseDTO + for _, item := range data { + articleData, ok := item.(map[string]interface{}) + if ok { + articles = append(articles, dto.ArticleResponseDTO{ + ID: articleData["article_id"].(string), + Title: articleData["title"].(string), + CoverImage: articleData["coverImage"].(string), + Author: articleData["author"].(string), + Heading: articleData["heading"].(string), + Content: articleData["content"].(string), + PublishedAt: articleData["publishedAt"].(string), + UpdatedAt: articleData["updatedAt"].(string), + }) + } + } - cachedData, err := utils.GetJSONData(cacheKey) - if err == nil && cachedData != nil { - var articles []dto.ArticleResponseDTO - - if data, ok := cachedData["data"].([]interface{}); ok { - for _, item := range data { - articleData, ok := item.(map[string]interface{}) - if ok { - - articles = append(articles, dto.ArticleResponseDTO{ - ID: articleData["article_id"].(string), - Title: articleData["title"].(string), - CoverImage: articleData["coverImage"].(string), - Author: articleData["author"].(string), - Heading: articleData["heading"].(string), - Content: articleData["content"].(string), - PublishedAt: articleData["publishedAt"].(string), - UpdatedAt: articleData["updatedAt"].(string), - }) + if total, ok := cachedData["total"].(float64); ok { + fmt.Printf("Cached Total Articles: %f\n", total) + return articles, int(total), nil + } else { + fmt.Println("Total articles not found in cache, using 0 as fallback.") + return articles, 0, nil } } - - total, ok := cachedData["total"].(float64) - if !ok { - return nil, 0, fmt.Errorf("invalid total count in cache") - } - return articles, int(total), nil } } @@ -175,6 +171,8 @@ func (s *articleService) GetAllArticles(page, limit int) ([]dto.ArticleResponseD return nil, 0, fmt.Errorf("failed to fetch articles: %v", err) } + fmt.Printf("Total Articles from Database: %d\n", total) + var articleDTOs []dto.ArticleResponseDTO for _, article := range articles { publishedAt, _ := utils.FormatDateToIndonesianFormat(article.PublishedAt) @@ -192,12 +190,14 @@ func (s *articleService) GetAllArticles(page, limit int) ([]dto.ArticleResponseD }) } + cacheKey = fmt.Sprintf("articles_page:%d_limit:%d", page, limit) cacheData := map[string]interface{}{ "data": articleDTOs, "total": total, } - err = utils.SetJSONData(cacheKey, cacheData, time.Hour*24) - if err != nil { + + fmt.Printf("Setting cache with total: %d\n", total) + if err := utils.SetJSONData(cacheKey, cacheData, time.Hour*24); err != nil { fmt.Printf("Error caching articles to Redis: %v\n", err) } @@ -205,24 +205,15 @@ func (s *articleService) GetAllArticles(page, limit int) ([]dto.ArticleResponseD } func (s *articleService) GetArticleByID(id string) (*dto.ArticleResponseDTO, error) { - cacheKey := fmt.Sprintf("article:%s", id) + cacheKey := fmt.Sprintf("article:%s", id) cachedData, err := utils.GetJSONData(cacheKey) if err == nil && cachedData != nil { - articleData, ok := cachedData["data"].(map[string]interface{}) - if ok { - - article := dto.ArticleResponseDTO{ - ID: articleData["article_id"].(string), - Title: articleData["title"].(string), - CoverImage: articleData["coverImage"].(string), - Author: articleData["author"].(string), - Heading: articleData["heading"].(string), - Content: articleData["content"].(string), - PublishedAt: articleData["publishedAt"].(string), - UpdatedAt: articleData["updatedAt"].(string), + articleResponse := &dto.ArticleResponseDTO{} + if data, ok := cachedData["data"].(string); ok { + if err := json.Unmarshal([]byte(data), articleResponse); err == nil { + return articleResponse, nil } - return &article, nil } } @@ -248,9 +239,7 @@ func (s *articleService) GetArticleByID(id string) (*dto.ArticleResponseDTO, err cacheData := map[string]interface{}{ "data": articleResponseDTO, } - err = utils.SetJSONData(cacheKey, cacheData, time.Hour*24) - if err != nil { - + if err := utils.SetJSONData(cacheKey, cacheData, time.Hour*24); err != nil { fmt.Printf("Error caching article to Redis: %v\n", err) } @@ -258,6 +247,7 @@ func (s *articleService) GetArticleByID(id string) (*dto.ArticleResponseDTO, err } func (s *articleService) UpdateArticle(id string, request dto.RequestArticleDTO, coverImage *multipart.FileHeader) (*dto.ArticleResponseDTO, error) { + article, err := s.ArticleRepo.FindArticleByID(id) if err != nil { return nil, fmt.Errorf("article not found: %v", err) @@ -270,39 +260,10 @@ func (s *articleService) UpdateArticle(id string, request dto.RequestArticleDTO, var coverImagePath string if coverImage != nil { - coverImageDir := "./public/uploads/articles" - if _, err := os.Stat(coverImageDir); os.IsNotExist(err) { - err := os.MkdirAll(coverImageDir, os.ModePerm) - if err != nil { - return nil, fmt.Errorf("failed to create directory for cover image: %v", err) - } - } - - extension := filepath.Ext(coverImage.Filename) - if extension != ".jpg" && extension != ".jpeg" && extension != ".png" { - return nil, fmt.Errorf("invalid file type, only .jpg, .jpeg, and .png are allowed") - } - - coverImageFileName := fmt.Sprintf("%s_cover%s", uuid.New().String(), extension) - coverImagePath = filepath.Join(coverImageDir, coverImageFileName) - - src, err := coverImage.Open() - if err != nil { - return nil, fmt.Errorf("failed to open uploaded file: %v", err) - } - defer src.Close() - - dst, err := os.Create(coverImagePath) - if err != nil { - return nil, fmt.Errorf("failed to create cover image file: %v", err) - } - defer dst.Close() - - _, err = dst.ReadFrom(src) + coverImagePath, err = s.saveCoverImage(coverImage) if err != nil { return nil, fmt.Errorf("failed to save cover image: %v", err) } - article.CoverImage = coverImagePath } @@ -331,15 +292,7 @@ func (s *articleService) UpdateArticle(id string, request dto.RequestArticleDTO, } articleCacheKey := fmt.Sprintf("article:%s", updatedArticle.ID) - err = utils.DeleteData(articleCacheKey) - if err != nil { - fmt.Printf("Error deleting old cache for article: %v\n", err) - } - - cacheData := map[string]interface{}{ - "data": articleResponseDTO, - } - err = utils.SetJSONData(articleCacheKey, cacheData, time.Hour*24) + err = utils.SetJSONData(articleCacheKey, map[string]interface{}{"data": articleResponseDTO}, time.Hour*24) if err != nil { fmt.Printf("Error caching updated article to Redis: %v\n", err) } @@ -371,7 +324,7 @@ func (s *articleService) UpdateArticle(id string, request dto.RequestArticleDTO, }) } - cacheData = map[string]interface{}{ + cacheData := map[string]interface{}{ "data": articleDTOs, } err = utils.SetJSONData(articlesCacheKey, cacheData, time.Hour*24) @@ -382,3 +335,39 @@ func (s *articleService) UpdateArticle(id string, request dto.RequestArticleDTO, return articleResponseDTO, nil } + +func (s *articleService) saveCoverImage(coverImage *multipart.FileHeader) (string, error) { + coverImageDir := "./public/uploads/articles" + if _, err := os.Stat(coverImageDir); os.IsNotExist(err) { + if err := os.MkdirAll(coverImageDir, os.ModePerm); err != nil { + return "", fmt.Errorf("failed to create directory for cover image: %v", err) + } + } + + extension := filepath.Ext(coverImage.Filename) + if extension != ".jpg" && extension != ".jpeg" && extension != ".png" { + return "", fmt.Errorf("invalid file type, only .jpg, .jpeg, and .png are allowed") + } + + coverImageFileName := fmt.Sprintf("%s_cover%s", uuid.New().String(), extension) + coverImagePath := filepath.Join(coverImageDir, coverImageFileName) + + src, err := coverImage.Open() + if err != nil { + return "", fmt.Errorf("failed to open uploaded file: %v", err) + } + defer src.Close() + + dst, err := os.Create(coverImagePath) + if err != nil { + return "", fmt.Errorf("failed to create cover image file: %v", err) + } + defer dst.Close() + + _, err = dst.ReadFrom(src) + if err != nil { + return "", fmt.Errorf("failed to save cover image: %v", err) + } + + return coverImagePath, nil +}