first commit
This commit is contained in:
169
internal/modules/cardmaster/dto.go
Normal file
169
internal/modules/cardmaster/dto.go
Normal file
@@ -0,0 +1,169 @@
|
||||
package cardmaster
|
||||
|
||||
import (
|
||||
"cardverse/internal/pkg/utils"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ListCardMasterQuery struct {
|
||||
Page int `form:"page,default=1" binding:"omitempty,min=1"`
|
||||
Limit int `form:"limit,default=20" binding:"omitempty,min=1,max=100"`
|
||||
Search string `form:"search"`
|
||||
IDSet uint `form:"id_set" binding:"omitempty"`
|
||||
Element string `form:"element"`
|
||||
Rarity string `form:"rarity"`
|
||||
Stage string `form:"stage"`
|
||||
}
|
||||
|
||||
type CreateCardMasterRequest struct {
|
||||
IDSet uint `json:"id_set" binding:"required"`
|
||||
Name string `json:"name" binding:"required,min=1,max=150"`
|
||||
Number string `json:"number" binding:"omitempty,max=50"`
|
||||
Image string `json:"image" binding:"omitempty,max=255"`
|
||||
Stage string `json:"stage" binding:"omitempty,max=50"`
|
||||
Element string `json:"element" binding:"omitempty,max=50"`
|
||||
ElementImage string `json:"element_image" binding:"omitempty,max=255"`
|
||||
EvolvesFrom string `json:"evolves_from" binding:"omitempty,max=150"`
|
||||
Illustrator string `json:"illustrator" binding:"omitempty,max=150"`
|
||||
Regulation string `json:"regulation" binding:"omitempty,max=10"`
|
||||
Rarity string `json:"rarity" binding:"omitempty,max=50"`
|
||||
IDPriceChartingEng string `json:"id_price_charting_eng" binding:"omitempty,max=50"`
|
||||
LinkPriceChartingEng string `json:"link_price_charting_eng" binding:"omitempty"`
|
||||
IDPriceChartingJpn string `json:"id_price_charting_jpn" binding:"omitempty,max=50"`
|
||||
LinkPriceChartingJpn string `json:"link_price_charting_jpn" binding:"omitempty"`
|
||||
HP *HP `json:"hp" binding:"omitempty"`
|
||||
Attacks []Attack `json:"attacks" binding:"omitempty"`
|
||||
Abilities []Ability `json:"abilities" binding:"omitempty"`
|
||||
Battle *Battle `json:"battle" binding:"omitempty"`
|
||||
EvolutionLine []EvolutionStage `json:"evolution_line" binding:"omitempty"`
|
||||
PokedexInfo *PokedexInfo `json:"pokedex_info" binding:"omitempty"`
|
||||
Effects []Effect `json:"effects" binding:"omitempty"`
|
||||
}
|
||||
|
||||
type UpdateCardMasterRequest struct {
|
||||
IDSet uint `json:"id_set" binding:"omitempty"`
|
||||
Name string `json:"name" binding:"omitempty,min=1,max=150"`
|
||||
Number string `json:"number" binding:"omitempty,max=50"`
|
||||
Image string `json:"image" binding:"omitempty,max=255"`
|
||||
Stage string `json:"stage" binding:"omitempty,max=50"`
|
||||
Element string `json:"element" binding:"omitempty,max=50"`
|
||||
EvolvesFrom string `json:"evolves_from" binding:"omitempty,max=150"`
|
||||
Illustrator string `json:"illustrator" binding:"omitempty,max=150"`
|
||||
Regulation string `json:"regulation" binding:"omitempty,max=10"`
|
||||
Rarity string `json:"rarity" binding:"omitempty,max=50"`
|
||||
IDPriceChartingEng string `json:"id_price_charting_eng" binding:"omitempty,max=50"`
|
||||
LinkPriceChartingEng string `json:"link_price_charting_eng" binding:"omitempty"`
|
||||
IDPriceChartingJpn string `json:"id_price_charting_jpn" binding:"omitempty,max=50"`
|
||||
LinkPriceChartingJpn string `json:"link_price_charting_jpn" binding:"omitempty"`
|
||||
HP *HP `json:"hp" binding:"omitempty"`
|
||||
Attacks []Attack `json:"attacks" binding:"omitempty"`
|
||||
Abilities []Ability `json:"abilities" binding:"omitempty"`
|
||||
Battle *Battle `json:"battle" binding:"omitempty"`
|
||||
EvolutionLine []EvolutionStage `json:"evolution_line" binding:"omitempty"`
|
||||
PokedexInfo *PokedexInfo `json:"pokedex_info" binding:"omitempty"`
|
||||
Effects []Effect `json:"effects" binding:"omitempty"`
|
||||
}
|
||||
|
||||
type ElementResponse struct {
|
||||
Name string `json:"name"`
|
||||
Image string `json:"image"`
|
||||
}
|
||||
|
||||
func GetElementsResponse() []ElementResponse {
|
||||
elements := []string{"Daun", "Api", "Air", "Listrik", "Psikis", "Petarung", "Kegelapan", "Logam", "Naga", "Peri", "Bening"}
|
||||
result := make([]ElementResponse, 0, len(elements))
|
||||
for _, name := range elements {
|
||||
if path, exists := ElementImageMap[name]; exists {
|
||||
result = append(result, ElementResponse{
|
||||
Name: name,
|
||||
Image: utils.FormatMediaURL(path),
|
||||
})
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
type CardMasterResponse struct {
|
||||
ID uint `json:"id"`
|
||||
IDSet uint `json:"id_set"`
|
||||
Name string `json:"name"`
|
||||
Number *string `json:"number"`
|
||||
Image *string `json:"image"`
|
||||
Stage *string `json:"stage"`
|
||||
Element *string `json:"element"`
|
||||
ElementImage *string `json:"element_image"`
|
||||
EvolvesFrom *string `json:"evolves_from"`
|
||||
Illustrator *string `json:"illustrator"`
|
||||
Regulation *string `json:"regulation"`
|
||||
Rarity *string `json:"rarity"`
|
||||
IDPriceChartingEng *string `json:"id_price_charting_eng"`
|
||||
LinkPriceChartingEng *string `json:"link_price_charting_eng"`
|
||||
IDPriceChartingJpn *string `json:"id_price_charting_jpn"`
|
||||
LinkPriceChartingJpn *string `json:"link_price_charting_jpn"`
|
||||
HP JSONB `json:"hp"`
|
||||
Attacks JSONB `json:"attacks"`
|
||||
Abilities JSONB `json:"abilities"`
|
||||
Battle JSONB `json:"battle"`
|
||||
EvolutionLine JSONB `json:"evolution_line"`
|
||||
PokedexInfo JSONB `json:"pokedex_info"`
|
||||
Effects JSONB `json:"effects"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
CreatedBy *uint `json:"created_by,omitempty"`
|
||||
UpdatedBy *uint `json:"updated_by,omitempty"`
|
||||
}
|
||||
|
||||
func ToCardMasterResponse(c CardMaster) CardMasterResponse {
|
||||
return CardMasterResponse{
|
||||
ID: c.ID,
|
||||
IDSet: c.IDSet,
|
||||
Name: c.Name,
|
||||
Number: strPtr(c.Number),
|
||||
Image: strPtrMedia(c.Image),
|
||||
Stage: strPtr(c.Stage),
|
||||
Element: strPtr(c.Element),
|
||||
ElementImage: strPtrMedia(c.ElementImage),
|
||||
EvolvesFrom: strPtr(c.EvolvesFrom),
|
||||
Illustrator: strPtr(c.Illustrator),
|
||||
Regulation: strPtr(c.Regulation),
|
||||
Rarity: strPtr(c.Rarity),
|
||||
IDPriceChartingEng: strPtr(c.IDPriceChartingEng),
|
||||
LinkPriceChartingEng: strPtr(c.LinkPriceChartingEng),
|
||||
IDPriceChartingJpn: strPtr(c.IDPriceChartingJpn),
|
||||
LinkPriceChartingJpn: strPtr(c.LinkPriceChartingJpn),
|
||||
HP: JSONB(utils.FormatJSONBMediaURLs(c.HP)),
|
||||
Attacks: JSONB(utils.FormatJSONBMediaURLs(c.Attacks)),
|
||||
Abilities: JSONB(utils.FormatJSONBMediaURLs(c.Abilities)),
|
||||
Battle: JSONB(utils.FormatJSONBMediaURLs(c.Battle)),
|
||||
EvolutionLine: JSONB(utils.FormatJSONBMediaURLs(c.EvolutionLine)),
|
||||
PokedexInfo: JSONB(utils.FormatJSONBMediaURLs(c.PokedexInfo)),
|
||||
Effects: JSONB(utils.FormatJSONBMediaURLs(c.Effects)),
|
||||
CreatedAt: c.CreatedAt,
|
||||
UpdatedAt: c.UpdatedAt,
|
||||
CreatedBy: c.CreatedBy,
|
||||
UpdatedBy: c.UpdatedBy,
|
||||
}
|
||||
}
|
||||
|
||||
func ToCardMasterResponseList(list []CardMaster) []CardMasterResponse {
|
||||
result := make([]CardMasterResponse, 0, len(list))
|
||||
for _, item := range list {
|
||||
result = append(result, ToCardMasterResponse(item))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func strPtr(s string) *string {
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
return &s
|
||||
}
|
||||
|
||||
func strPtrMedia(s string) *string {
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
formatted := utils.FormatMediaURL(s)
|
||||
return &formatted
|
||||
}
|
||||
314
internal/modules/cardmaster/handler.go
Normal file
314
internal/modules/cardmaster/handler.go
Normal file
@@ -0,0 +1,314 @@
|
||||
package cardmaster
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"image"
|
||||
_ "image/gif"
|
||||
_ "image/jpeg"
|
||||
_ "image/png"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"cardverse/internal/modules/seriessetmaster"
|
||||
"cardverse/internal/pkg/response"
|
||||
"cardverse/internal/pkg/utils"
|
||||
"cardverse/internal/pkg/validator"
|
||||
|
||||
"github.com/chai2010/webp"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
service Service
|
||||
seriesSetRepo seriessetmaster.Repository
|
||||
}
|
||||
|
||||
func NewHandler(service Service, seriesSetRepo seriessetmaster.Repository) *Handler {
|
||||
return &Handler{
|
||||
service: service,
|
||||
seriesSetRepo: seriesSetRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) GetAll(c *gin.Context) {
|
||||
var query ListCardMasterQuery
|
||||
if err := c.ShouldBindQuery(&query); err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "query parameter tidak valid", validator.TranslateError(err))
|
||||
return
|
||||
}
|
||||
|
||||
list, total, err := h.service.GetAllPaginated(query)
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusInternalServerError, "gagal mengambil data kartu", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
pagination := response.NewPagination(query.Page, query.Limit, total)
|
||||
response.SuccessWithPagination(c, http.StatusOK, "berhasil mengambil data kartu", ToCardMasterResponseList(list), pagination)
|
||||
}
|
||||
|
||||
func (h *Handler) GetElements(c *gin.Context) {
|
||||
elements := h.service.GetElements()
|
||||
response.Success(c, http.StatusOK, "berhasil mengambil data elemen", elements)
|
||||
}
|
||||
|
||||
func (h *Handler) GetByID(c *gin.Context) {
|
||||
id, err := parseID(c)
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "id tidak valid", []validator.FieldError{
|
||||
{Field: "id", Message: "id harus berupa angka"},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
cardData, err := h.service.GetByID(id)
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusNotFound, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, http.StatusOK, "berhasil mengambil data kartu", ToCardMasterResponse(*cardData))
|
||||
}
|
||||
|
||||
func (h *Handler) Create(c *gin.Context) {
|
||||
var req CreateCardMasterRequest
|
||||
|
||||
contentType := c.GetHeader("Content-Type")
|
||||
if strings.HasPrefix(contentType, "multipart/form-data") {
|
||||
if jsonData := c.PostForm("data"); jsonData != "" {
|
||||
if err := json.Unmarshal([]byte(jsonData), &req); err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "format data JSON pada multipart tidak valid", nil)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if err := c.ShouldBind(&req); err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "input tidak valid", validator.TranslateError(err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
fileHeader, err := c.FormFile("image")
|
||||
if err == nil && fileHeader != nil {
|
||||
if fileHeader.Size > 10*1024*1024 {
|
||||
response.Error(c, http.StatusBadRequest, "ukuran file gambar maksimal 10MB", nil)
|
||||
return
|
||||
}
|
||||
|
||||
if req.IDSet == 0 {
|
||||
response.Error(c, http.StatusBadRequest, "id_set wajib diisi untuk upload gambar", nil)
|
||||
return
|
||||
}
|
||||
|
||||
setInfo, err := h.seriesSetRepo.FindByID(req.IDSet)
|
||||
if err != nil || setInfo == nil {
|
||||
response.Error(c, http.StatusBadRequest, "set dengan id_set tersebut tidak ditemukan", nil)
|
||||
return
|
||||
}
|
||||
|
||||
seriesSlug := utils.Slugify(setInfo.SeriesName)
|
||||
setCode := setInfo.SetCode
|
||||
|
||||
uploadDir := filepath.Join("./public/images", seriesSlug, setCode, "images")
|
||||
if err := os.MkdirAll(uploadDir, os.ModePerm); err != nil {
|
||||
response.Error(c, http.StatusInternalServerError, "gagal membuat direktori gambar kartu", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
srcFile, err := fileHeader.Open()
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusInternalServerError, "gagal membuka file gambar", err.Error())
|
||||
return
|
||||
}
|
||||
defer srcFile.Close()
|
||||
|
||||
img, _, err := image.Decode(srcFile)
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "file bukan merupakan gambar yang valid", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
filename := formatCardImageFilename(setCode, req.Number)
|
||||
dstPath := filepath.Join(uploadDir, filename)
|
||||
|
||||
out, err := os.Create(dstPath)
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusInternalServerError, "gagal membuat file WebP kartu", err.Error())
|
||||
return
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
if err := webp.Encode(out, img, &webp.Options{Lossless: false, Quality: 90}); err != nil {
|
||||
response.Error(c, http.StatusInternalServerError, "gagal mengonversi gambar ke WebP", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
req.Image = fmt.Sprintf("/images/%s/%s/images/%s", seriesSlug, setCode, filename)
|
||||
}
|
||||
} else {
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "input tidak valid", validator.TranslateError(err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
createdCard, err := h.service.Create(req)
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, http.StatusCreated, "kartu berhasil dibuat", ToCardMasterResponse(*createdCard))
|
||||
}
|
||||
|
||||
func (h *Handler) Update(c *gin.Context) {
|
||||
id, err := parseID(c)
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "id tidak valid", []validator.FieldError{
|
||||
{Field: "id", Message: "id harus berupa angka"},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var req UpdateCardMasterRequest
|
||||
|
||||
contentType := c.GetHeader("Content-Type")
|
||||
if strings.HasPrefix(contentType, "multipart/form-data") {
|
||||
if jsonData := c.PostForm("data"); jsonData != "" {
|
||||
if err := json.Unmarshal([]byte(jsonData), &req); err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "format data JSON pada multipart tidak valid", nil)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if err := c.ShouldBind(&req); err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "input tidak valid", validator.TranslateError(err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
fileHeader, err := c.FormFile("image")
|
||||
if err == nil && fileHeader != nil {
|
||||
if fileHeader.Size > 10*1024*1024 {
|
||||
response.Error(c, http.StatusBadRequest, "ukuran file gambar maksimal 10MB", nil)
|
||||
return
|
||||
}
|
||||
|
||||
existingCard, err := h.service.GetByID(id)
|
||||
if err != nil || existingCard == nil {
|
||||
response.Error(c, http.StatusNotFound, "kartu tidak ditemukan", nil)
|
||||
return
|
||||
}
|
||||
|
||||
targetIDSet := req.IDSet
|
||||
if targetIDSet == 0 {
|
||||
targetIDSet = existingCard.IDSet
|
||||
}
|
||||
|
||||
targetNumber := req.Number
|
||||
if targetNumber == "" {
|
||||
targetNumber = existingCard.Number
|
||||
}
|
||||
|
||||
setInfo, err := h.seriesSetRepo.FindByID(targetIDSet)
|
||||
if err != nil || setInfo == nil {
|
||||
response.Error(c, http.StatusBadRequest, "set dengan id_set tersebut tidak ditemukan", nil)
|
||||
return
|
||||
}
|
||||
|
||||
seriesSlug := utils.Slugify(setInfo.SeriesName)
|
||||
setCode := setInfo.SetCode
|
||||
|
||||
uploadDir := filepath.Join("./public/images", seriesSlug, setCode, "images")
|
||||
if err := os.MkdirAll(uploadDir, os.ModePerm); err != nil {
|
||||
response.Error(c, http.StatusInternalServerError, "gagal membuat direktori gambar kartu", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
srcFile, err := fileHeader.Open()
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusInternalServerError, "gagal membuka file gambar", err.Error())
|
||||
return
|
||||
}
|
||||
defer srcFile.Close()
|
||||
|
||||
img, _, err := image.Decode(srcFile)
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "file bukan merupakan gambar yang valid", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
filename := formatCardImageFilename(setCode, targetNumber)
|
||||
dstPath := filepath.Join(uploadDir, filename)
|
||||
|
||||
out, err := os.Create(dstPath)
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusInternalServerError, "gagal membuat file WebP kartu", err.Error())
|
||||
return
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
if err := webp.Encode(out, img, &webp.Options{Lossless: false, Quality: 90}); err != nil {
|
||||
response.Error(c, http.StatusInternalServerError, "gagal mengonversi gambar ke WebP", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
req.Image = fmt.Sprintf("/images/%s/%s/images/%s", seriesSlug, setCode, filename)
|
||||
}
|
||||
} else {
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "input tidak valid", validator.TranslateError(err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
updatedCard, err := h.service.Update(id, req)
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusNotFound, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, http.StatusOK, "kartu berhasil diupdate", ToCardMasterResponse(*updatedCard))
|
||||
}
|
||||
|
||||
func (h *Handler) Delete(c *gin.Context) {
|
||||
id, err := parseID(c)
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "id tidak valid", []validator.FieldError{
|
||||
{Field: "id", Message: "id harus berupa angka"},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.service.Delete(id); err != nil {
|
||||
response.Error(c, http.StatusNotFound, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, http.StatusOK, "kartu berhasil dihapus", nil)
|
||||
}
|
||||
|
||||
func parseID(c *gin.Context) (uint, error) {
|
||||
idParam := c.Param("id")
|
||||
id, err := strconv.ParseUint(idParam, 10, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return uint(id), nil
|
||||
}
|
||||
|
||||
func formatCardImageFilename(setCode, number string) string {
|
||||
cleanNum := strings.TrimSpace(number)
|
||||
if cleanNum != "" {
|
||||
cleanNum = strings.ReplaceAll(cleanNum, "/", "_")
|
||||
cleanNum = strings.ReplaceAll(cleanNum, "\\", "_")
|
||||
if !strings.HasSuffix(strings.ToLower(cleanNum), ".webp") {
|
||||
cleanNum = cleanNum + ".webp"
|
||||
}
|
||||
return cleanNum
|
||||
}
|
||||
return fmt.Sprintf("%s-1_%d.webp", setCode, time.Now().UnixNano())
|
||||
}
|
||||
161
internal/modules/cardmaster/model.go
Normal file
161
internal/modules/cardmaster/model.go
Normal file
@@ -0,0 +1,161 @@
|
||||
package cardmaster
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
// JSONB merepresentasikan kolom JSONB di PostgreSQL
|
||||
type JSONB json.RawMessage
|
||||
|
||||
func (j JSONB) Value() (driver.Value, error) {
|
||||
if len(j) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return string(j), nil
|
||||
}
|
||||
|
||||
func (j *JSONB) Scan(value interface{}) error {
|
||||
if value == nil {
|
||||
*j = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
switch v := value.(type) {
|
||||
case []byte:
|
||||
*j = append((*j)[0:0], v...)
|
||||
case string:
|
||||
*j = JSONB(v)
|
||||
default:
|
||||
return errors.New("type assertion ke []byte atau string gagal")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (j JSONB) MarshalJSON() ([]byte, error) {
|
||||
if len(j) == 0 {
|
||||
return []byte("null"), nil
|
||||
}
|
||||
return json.RawMessage(j).MarshalJSON()
|
||||
}
|
||||
|
||||
func (j *JSONB) UnmarshalJSON(data []byte) error {
|
||||
if j == nil {
|
||||
return errors.New("JSONB: UnmarshalJSON pada nil pointer")
|
||||
}
|
||||
*j = append((*j)[0:0], data...)
|
||||
return nil
|
||||
}
|
||||
|
||||
// CardMaster merepresentasikan tabel "card_masters" di database
|
||||
type CardMaster struct {
|
||||
ID uint `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
IDSet uint `json:"id_set" gorm:"column:id_set;not null;index"`
|
||||
Name string `json:"name" gorm:"type:varchar(150);not null;index"`
|
||||
Number string `json:"number" gorm:"type:varchar(50)"`
|
||||
Image string `json:"image" gorm:"type:varchar(255)"`
|
||||
Stage string `json:"stage,omitempty" gorm:"type:varchar(50)"`
|
||||
Element string `json:"element,omitempty" gorm:"type:varchar(50);index"`
|
||||
ElementImage string `json:"element_image,omitempty" gorm:"type:varchar(255)"`
|
||||
EvolvesFrom string `json:"evolves_from,omitempty" gorm:"type:varchar(150)"`
|
||||
Illustrator string `json:"illustrator,omitempty" gorm:"type:varchar(150)"`
|
||||
Regulation string `json:"regulation,omitempty" gorm:"type:varchar(10)"`
|
||||
Rarity string `json:"rarity,omitempty" gorm:"type:varchar(50);index"`
|
||||
IDPriceChartingEng string `json:"id_price_charting_eng,omitempty" gorm:"type:varchar(50)"`
|
||||
LinkPriceChartingEng string `json:"link_price_charting_eng,omitempty" gorm:"type:text"`
|
||||
IDPriceChartingJpn string `json:"id_price_charting_jpn,omitempty" gorm:"type:varchar(50)"`
|
||||
LinkPriceChartingJpn string `json:"link_price_charting_jpn,omitempty" gorm:"type:text"`
|
||||
HP JSONB `json:"hp,omitempty" gorm:"type:jsonb"`
|
||||
Attacks JSONB `json:"attacks,omitempty" gorm:"type:jsonb"`
|
||||
Abilities JSONB `json:"abilities,omitempty" gorm:"type:jsonb"`
|
||||
Battle JSONB `json:"battle,omitempty" gorm:"type:jsonb"`
|
||||
EvolutionLine JSONB `json:"evolution_line,omitempty" gorm:"type:jsonb"`
|
||||
PokedexInfo JSONB `json:"pokedex_info,omitempty" gorm:"type:jsonb"`
|
||||
Effects JSONB `json:"effects,omitempty" gorm:"type:jsonb"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
|
||||
CreatedBy *uint `json:"created_by" gorm:"default:null"`
|
||||
UpdatedBy *uint `json:"updated_by" gorm:"default:null"`
|
||||
}
|
||||
|
||||
func (CardMaster) TableName() string {
|
||||
return "card_masters"
|
||||
}
|
||||
|
||||
// Struct di bawah ini digunakan untuk parsing data JSON ke tipe Go yang terstruktur
|
||||
|
||||
type HP struct {
|
||||
Value string `json:"value"`
|
||||
Element string `json:"element"`
|
||||
ElementImage string `json:"element_image,omitempty"`
|
||||
}
|
||||
|
||||
type AttackCost struct {
|
||||
Element string `json:"element"`
|
||||
Image string `json:"image,omitempty"`
|
||||
}
|
||||
|
||||
type Attack struct {
|
||||
Cost []AttackCost `json:"cost"`
|
||||
Name string `json:"name"`
|
||||
Damage string `json:"damage,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
}
|
||||
|
||||
type BattleStat struct {
|
||||
Element string `json:"element,omitempty"`
|
||||
ElementImage string `json:"element_image,omitempty"`
|
||||
Value string `json:"value,omitempty"`
|
||||
}
|
||||
|
||||
type Battle struct {
|
||||
Weakness []BattleStat `json:"weakness"`
|
||||
Resistance []BattleStat `json:"resistance"`
|
||||
Retreat []BattleStat `json:"retreat"`
|
||||
}
|
||||
|
||||
type EvolutionStage struct {
|
||||
Stage string `json:"stage"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type PokedexInfo struct {
|
||||
Number string `json:"number"`
|
||||
Height string `json:"height"`
|
||||
Weight string `json:"weight"`
|
||||
}
|
||||
|
||||
type Ability struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
}
|
||||
|
||||
type Effect struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
}
|
||||
|
||||
// ElementImageMap memetakan nama element (Bahasa Indonesia) ke path gambar di /images/elements/
|
||||
var ElementImageMap = map[string]string{
|
||||
"Daun": "/images/elements/Grass.png",
|
||||
"Api": "/images/elements/Fire.png",
|
||||
"Air": "/images/elements/Water.png",
|
||||
"Listrik": "/images/elements/Lightning.png",
|
||||
"Psikis": "/images/elements/Psychic.png",
|
||||
"Petarung": "/images/elements/Fighting.png",
|
||||
"Kegelapan": "/images/elements/Darkness.png",
|
||||
"Logam": "/images/elements/Metal.png",
|
||||
"Naga": "/images/elements/Dragon.png",
|
||||
"Peri": "/images/elements/Fairy.png",
|
||||
"Bening": "/images/elements/Colorless.png",
|
||||
}
|
||||
|
||||
// GetElementImage mengembalikan path gambar element berdasarkan nama element
|
||||
func GetElementImage(element string) string {
|
||||
if path, exists := ElementImageMap[element]; exists {
|
||||
return path
|
||||
}
|
||||
return ""
|
||||
}
|
||||
158
internal/modules/cardmaster/repository.go
Normal file
158
internal/modules/cardmaster/repository.go
Normal file
@@ -0,0 +1,158 @@
|
||||
package cardmaster
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Repository interface {
|
||||
FindAllPaginated(page, limit int, query ListCardMasterQuery) ([]CardMaster, int64, error)
|
||||
FindByID(id uint) (*CardMaster, error)
|
||||
Create(c *CardMaster) error
|
||||
Update(c *CardMaster, oldIDSet uint) error
|
||||
Delete(id uint, idSet uint) error
|
||||
}
|
||||
|
||||
type repository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewRepository(db *gorm.DB) Repository {
|
||||
return &repository{db: db}
|
||||
}
|
||||
|
||||
func (r *repository) FindAllPaginated(page, limit int, queryParams ListCardMasterQuery) ([]CardMaster, int64, error) {
|
||||
var list []CardMaster
|
||||
var total int64
|
||||
|
||||
query := r.db.Model(&CardMaster{}).
|
||||
Select(
|
||||
"id", "id_set", "name", "number", "image", "stage", "element", "element_image",
|
||||
"evolves_from", "illustrator", "regulation", "rarity",
|
||||
"id_price_charting_eng", "link_price_charting_eng", "id_price_charting_jpn", "link_price_charting_jpn",
|
||||
"hp", "attacks", "abilities", "battle", "evolution_line", "pokedex_info", "effects",
|
||||
"created_at", "updated_at", "created_by", "updated_by",
|
||||
)
|
||||
|
||||
if queryParams.IDSet > 0 {
|
||||
query = query.Where("id_set = ?", queryParams.IDSet)
|
||||
}
|
||||
|
||||
if queryParams.Element != "" {
|
||||
query = query.Where("element ILIKE ?", queryParams.Element)
|
||||
}
|
||||
|
||||
if queryParams.Rarity != "" {
|
||||
query = query.Where("rarity ILIKE ?", queryParams.Rarity)
|
||||
}
|
||||
|
||||
if queryParams.Stage != "" {
|
||||
query = query.Where("stage ILIKE ?", queryParams.Stage)
|
||||
}
|
||||
|
||||
if queryParams.Search != "" {
|
||||
pattern := "%" + queryParams.Search + "%"
|
||||
query = query.Where("name ILIKE ? OR number ILIKE ? OR illustrator ILIKE ?", pattern, pattern, pattern)
|
||||
}
|
||||
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
offset := (page - 1) * limit
|
||||
err := query.Order("id ASC").Limit(limit).Offset(offset).Find(&list).Error
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return list, total, nil
|
||||
}
|
||||
|
||||
func (r *repository) FindByID(id uint) (*CardMaster, error) {
|
||||
var c CardMaster
|
||||
err := r.db.Select(
|
||||
"id", "id_set", "name", "number", "image", "stage", "element", "element_image",
|
||||
"evolves_from", "illustrator", "regulation", "rarity",
|
||||
"id_price_charting_eng", "link_price_charting_eng", "id_price_charting_jpn", "link_price_charting_jpn",
|
||||
"hp", "attacks", "abilities", "battle", "evolution_line", "pokedex_info", "effects",
|
||||
"created_at", "updated_at", "created_by", "updated_by",
|
||||
).First(&c, id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &c, nil
|
||||
}
|
||||
|
||||
func (r *repository) Create(c *CardMaster) error {
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Create(c).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if c.IDSet > 0 {
|
||||
recalculateSetAndSeriesStatsTx(tx, c.IDSet)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (r *repository) Update(c *CardMaster, oldIDSet uint) error {
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Save(c).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if c.IDSet > 0 {
|
||||
recalculateSetAndSeriesStatsTx(tx, c.IDSet)
|
||||
}
|
||||
if oldIDSet > 0 && oldIDSet != c.IDSet {
|
||||
recalculateSetAndSeriesStatsTx(tx, oldIDSet)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (r *repository) Delete(id uint, idSet uint) error {
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Delete(&CardMaster{}, id).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if idSet > 0 {
|
||||
recalculateSetAndSeriesStatsTx(tx, idSet)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func recalculateSetAndSeriesStatsTx(tx *gorm.DB, setID uint) {
|
||||
if setID == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
type seriesSetRef struct {
|
||||
ID uint
|
||||
ParentID *uint
|
||||
}
|
||||
var set seriesSetRef
|
||||
if err := tx.Table("series_set_masters").Select("id", "parent_id").Where("id = ?", setID).First(&set).Error; err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var totalCards int64
|
||||
_ = tx.Table("card_masters").Where("id_set = ?", setID).Count(&totalCards).Error
|
||||
_ = tx.Table("series_set_masters").Where("id = ?", setID).Update("total_cards", int(totalCards)).Error
|
||||
|
||||
if set.ParentID != nil {
|
||||
var expansionCount int64
|
||||
_ = tx.Table("series_set_masters").Where("parent_id = ?", *set.ParentID).Count(&expansionCount).Error
|
||||
|
||||
var cardCount int64
|
||||
_ = tx.Table("card_masters").
|
||||
Where("id_set IN (SELECT id FROM series_set_masters WHERE parent_id = ?)", *set.ParentID).
|
||||
Count(&cardCount).Error
|
||||
|
||||
_ = tx.Table("series_set_masters").
|
||||
Where("id = ? AND parent_id IS NULL", *set.ParentID).
|
||||
Updates(map[string]interface{}{
|
||||
"expansion_count": int(expansionCount),
|
||||
"card_count": int(cardCount),
|
||||
}).Error
|
||||
}
|
||||
}
|
||||
44
internal/modules/cardmaster/repository_mock_test.go
Normal file
44
internal/modules/cardmaster/repository_mock_test.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package cardmaster
|
||||
|
||||
type mockRepository struct {
|
||||
findAllPaginatedFunc func(page, limit int, query ListCardMasterQuery) ([]CardMaster, int64, error)
|
||||
findByIDFunc func(id uint) (*CardMaster, error)
|
||||
createFunc func(c *CardMaster) error
|
||||
updateFunc func(c *CardMaster, oldIDSet uint) error
|
||||
deleteFunc func(id uint, idSet uint) error
|
||||
}
|
||||
|
||||
func (m *mockRepository) FindAllPaginated(page, limit int, query ListCardMasterQuery) ([]CardMaster, int64, error) {
|
||||
if m.findAllPaginatedFunc != nil {
|
||||
return m.findAllPaginatedFunc(page, limit, query)
|
||||
}
|
||||
return nil, 0, nil
|
||||
}
|
||||
|
||||
func (m *mockRepository) FindByID(id uint) (*CardMaster, error) {
|
||||
if m.findByIDFunc != nil {
|
||||
return m.findByIDFunc(id)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *mockRepository) Create(c *CardMaster) error {
|
||||
if m.createFunc != nil {
|
||||
return m.createFunc(c)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockRepository) Update(c *CardMaster, oldIDSet uint) error {
|
||||
if m.updateFunc != nil {
|
||||
return m.updateFunc(c, oldIDSet)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockRepository) Delete(id uint, idSet uint) error {
|
||||
if m.deleteFunc != nil {
|
||||
return m.deleteFunc(id, idSet)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
27
internal/modules/cardmaster/routes.go
Normal file
27
internal/modules/cardmaster/routes.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package cardmaster
|
||||
|
||||
import (
|
||||
"cardverse/internal/middleware"
|
||||
"cardverse/internal/modules/seriessetmaster"
|
||||
"cardverse/internal/modules/user"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func RegisterRoutes(router *gin.RouterGroup, db *gorm.DB) {
|
||||
repo := NewRepository(db)
|
||||
svc := NewService(repo)
|
||||
seriesSetRepo := seriessetmaster.NewRepository(db)
|
||||
handler := NewHandler(svc, seriesSetRepo)
|
||||
|
||||
cards := router.Group("/cards")
|
||||
{
|
||||
cards.GET("", handler.GetAll)
|
||||
cards.GET("/elements", handler.GetElements)
|
||||
cards.GET("/:id", handler.GetByID)
|
||||
cards.POST("", middleware.AuthRequired(), middleware.RequireRoles(user.RoleAdmin, user.RoleSuperAdmin), handler.Create)
|
||||
cards.PUT("/:id", middleware.AuthRequired(), middleware.RequireRoles(user.RoleAdmin, user.RoleSuperAdmin), handler.Update)
|
||||
cards.DELETE("/:id", middleware.AuthRequired(), middleware.RequireRoles(user.RoleAdmin, user.RoleSuperAdmin), handler.Delete)
|
||||
}
|
||||
}
|
||||
291
internal/modules/cardmaster/service.go
Normal file
291
internal/modules/cardmaster/service.go
Normal file
@@ -0,0 +1,291 @@
|
||||
package cardmaster
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"cardverse/internal/pkg/utils"
|
||||
)
|
||||
|
||||
var ErrCardNotFound = errors.New("kartu tidak ditemukan")
|
||||
|
||||
type Service interface {
|
||||
GetAllPaginated(query ListCardMasterQuery) ([]CardMaster, int64, error)
|
||||
GetByID(id uint) (*CardMaster, error)
|
||||
GetElements() []ElementResponse
|
||||
Create(req CreateCardMasterRequest) (*CardMaster, error)
|
||||
Update(id uint, req UpdateCardMasterRequest) (*CardMaster, error)
|
||||
Delete(id uint) error
|
||||
}
|
||||
|
||||
type service struct {
|
||||
repo Repository
|
||||
}
|
||||
|
||||
func NewService(repo Repository) Service {
|
||||
return &service{
|
||||
repo: repo,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *service) GetAllPaginated(query ListCardMasterQuery) ([]CardMaster, int64, error) {
|
||||
page := query.Page
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
limit := query.Limit
|
||||
if limit < 1 {
|
||||
limit = 20
|
||||
}
|
||||
|
||||
return s.repo.FindAllPaginated(page, limit, query)
|
||||
}
|
||||
|
||||
func (s *service) GetByID(id uint) (*CardMaster, error) {
|
||||
c, err := s.repo.FindByID(id)
|
||||
if err != nil {
|
||||
return nil, ErrCardNotFound
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (s *service) GetElements() []ElementResponse {
|
||||
return GetElementsResponse()
|
||||
}
|
||||
|
||||
func (s *service) Create(req CreateCardMasterRequest) (*CardMaster, error) {
|
||||
elementImage := req.ElementImage
|
||||
if elementImage == "" && req.Element != "" {
|
||||
elementImage = GetElementImage(req.Element)
|
||||
}
|
||||
|
||||
c := &CardMaster{
|
||||
IDSet: req.IDSet,
|
||||
Name: req.Name,
|
||||
Number: req.Number,
|
||||
Image: req.Image,
|
||||
Stage: req.Stage,
|
||||
Element: req.Element,
|
||||
ElementImage: elementImage,
|
||||
EvolvesFrom: req.EvolvesFrom,
|
||||
Illustrator: req.Illustrator,
|
||||
Regulation: req.Regulation,
|
||||
Rarity: req.Rarity,
|
||||
IDPriceChartingEng: req.IDPriceChartingEng,
|
||||
LinkPriceChartingEng: req.LinkPriceChartingEng,
|
||||
IDPriceChartingJpn: req.IDPriceChartingJpn,
|
||||
LinkPriceChartingJpn: req.LinkPriceChartingJpn,
|
||||
}
|
||||
|
||||
if req.HP != nil {
|
||||
req.HP.ElementImage = GetElementImage(req.HP.Element)
|
||||
bytes, err := json.Marshal(req.HP)
|
||||
if err == nil {
|
||||
c.HP = JSONB(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
if req.Attacks != nil {
|
||||
for i := range req.Attacks {
|
||||
for j := range req.Attacks[i].Cost {
|
||||
req.Attacks[i].Cost[j].Image = GetElementImage(req.Attacks[i].Cost[j].Element)
|
||||
}
|
||||
}
|
||||
bytes, err := json.Marshal(req.Attacks)
|
||||
if err == nil {
|
||||
c.Attacks = JSONB(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
if req.Abilities != nil {
|
||||
bytes, err := json.Marshal(req.Abilities)
|
||||
if err == nil {
|
||||
c.Abilities = JSONB(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
if req.Battle != nil {
|
||||
for i := range req.Battle.Weakness {
|
||||
req.Battle.Weakness[i].ElementImage = GetElementImage(req.Battle.Weakness[i].Element)
|
||||
}
|
||||
for i := range req.Battle.Resistance {
|
||||
req.Battle.Resistance[i].ElementImage = GetElementImage(req.Battle.Resistance[i].Element)
|
||||
}
|
||||
for i := range req.Battle.Retreat {
|
||||
req.Battle.Retreat[i].ElementImage = GetElementImage(req.Battle.Retreat[i].Element)
|
||||
}
|
||||
bytes, err := json.Marshal(req.Battle)
|
||||
if err == nil {
|
||||
c.Battle = JSONB(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
if req.EvolutionLine != nil {
|
||||
bytes, err := json.Marshal(req.EvolutionLine)
|
||||
if err == nil {
|
||||
c.EvolutionLine = JSONB(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
if req.PokedexInfo != nil {
|
||||
bytes, err := json.Marshal(req.PokedexInfo)
|
||||
if err == nil {
|
||||
c.PokedexInfo = JSONB(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
if req.Effects != nil {
|
||||
bytes, err := json.Marshal(req.Effects)
|
||||
if err == nil {
|
||||
c.Effects = JSONB(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.repo.Create(c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (s *service) Update(id uint, req UpdateCardMasterRequest) (*CardMaster, error) {
|
||||
c, err := s.repo.FindByID(id)
|
||||
if err != nil {
|
||||
return nil, ErrCardNotFound
|
||||
}
|
||||
|
||||
var oldIDSet uint
|
||||
if req.IDSet > 0 && req.IDSet != c.IDSet {
|
||||
oldIDSet = c.IDSet
|
||||
c.IDSet = req.IDSet
|
||||
}
|
||||
|
||||
if req.Name != "" {
|
||||
c.Name = req.Name
|
||||
}
|
||||
if req.Number != "" {
|
||||
c.Number = req.Number
|
||||
}
|
||||
if req.Image != "" {
|
||||
if c.Image != "" && c.Image != req.Image {
|
||||
utils.DeleteLocalFile(c.Image)
|
||||
}
|
||||
c.Image = req.Image
|
||||
}
|
||||
if req.Stage != "" {
|
||||
c.Stage = req.Stage
|
||||
}
|
||||
if req.Element != "" {
|
||||
c.Element = req.Element
|
||||
c.ElementImage = GetElementImage(req.Element)
|
||||
}
|
||||
if req.EvolvesFrom != "" {
|
||||
c.EvolvesFrom = req.EvolvesFrom
|
||||
}
|
||||
if req.Illustrator != "" {
|
||||
c.Illustrator = req.Illustrator
|
||||
}
|
||||
if req.Regulation != "" {
|
||||
c.Regulation = req.Regulation
|
||||
}
|
||||
if req.Rarity != "" {
|
||||
c.Rarity = req.Rarity
|
||||
}
|
||||
if req.IDPriceChartingEng != "" {
|
||||
c.IDPriceChartingEng = req.IDPriceChartingEng
|
||||
}
|
||||
if req.LinkPriceChartingEng != "" {
|
||||
c.LinkPriceChartingEng = req.LinkPriceChartingEng
|
||||
}
|
||||
if req.IDPriceChartingJpn != "" {
|
||||
c.IDPriceChartingJpn = req.IDPriceChartingJpn
|
||||
}
|
||||
if req.LinkPriceChartingJpn != "" {
|
||||
c.LinkPriceChartingJpn = req.LinkPriceChartingJpn
|
||||
}
|
||||
|
||||
if req.HP != nil {
|
||||
req.HP.ElementImage = GetElementImage(req.HP.Element)
|
||||
bytes, err := json.Marshal(req.HP)
|
||||
if err == nil {
|
||||
c.HP = JSONB(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
if req.Attacks != nil {
|
||||
for i := range req.Attacks {
|
||||
for j := range req.Attacks[i].Cost {
|
||||
req.Attacks[i].Cost[j].Image = GetElementImage(req.Attacks[i].Cost[j].Element)
|
||||
}
|
||||
}
|
||||
bytes, err := json.Marshal(req.Attacks)
|
||||
if err == nil {
|
||||
c.Attacks = JSONB(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
if req.Abilities != nil {
|
||||
bytes, err := json.Marshal(req.Abilities)
|
||||
if err == nil {
|
||||
c.Abilities = JSONB(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
if req.Battle != nil {
|
||||
for i := range req.Battle.Weakness {
|
||||
req.Battle.Weakness[i].ElementImage = GetElementImage(req.Battle.Weakness[i].Element)
|
||||
}
|
||||
for i := range req.Battle.Resistance {
|
||||
req.Battle.Resistance[i].ElementImage = GetElementImage(req.Battle.Resistance[i].Element)
|
||||
}
|
||||
for i := range req.Battle.Retreat {
|
||||
req.Battle.Retreat[i].ElementImage = GetElementImage(req.Battle.Retreat[i].Element)
|
||||
}
|
||||
bytes, err := json.Marshal(req.Battle)
|
||||
if err == nil {
|
||||
c.Battle = JSONB(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
if req.EvolutionLine != nil {
|
||||
bytes, err := json.Marshal(req.EvolutionLine)
|
||||
if err == nil {
|
||||
c.EvolutionLine = JSONB(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
if req.PokedexInfo != nil {
|
||||
bytes, err := json.Marshal(req.PokedexInfo)
|
||||
if err == nil {
|
||||
c.PokedexInfo = JSONB(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
if req.Effects != nil {
|
||||
bytes, err := json.Marshal(req.Effects)
|
||||
if err == nil {
|
||||
c.Effects = JSONB(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.repo.Update(c, oldIDSet); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (s *service) Delete(id uint) error {
|
||||
c, err := s.repo.FindByID(id)
|
||||
if err != nil {
|
||||
return ErrCardNotFound
|
||||
}
|
||||
|
||||
idSet := c.IDSet
|
||||
|
||||
if c.Image != "" {
|
||||
utils.DeleteLocalFile(c.Image)
|
||||
}
|
||||
|
||||
return s.repo.Delete(id, idSet)
|
||||
}
|
||||
179
internal/modules/cardmaster/service_test.go
Normal file
179
internal/modules/cardmaster/service_test.go
Normal file
@@ -0,0 +1,179 @@
|
||||
package cardmaster
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestService_GetAllPaginated_Success(t *testing.T) {
|
||||
repo := &mockRepository{
|
||||
findAllPaginatedFunc: func(page, limit int, query ListCardMasterQuery) ([]CardMaster, int64, error) {
|
||||
return []CardMaster{
|
||||
{ID: 1, Name: "Pikachu"},
|
||||
{ID: 2, Name: "Charizard"},
|
||||
}, 2, nil
|
||||
},
|
||||
}
|
||||
svc := NewService(repo)
|
||||
|
||||
list, total, err := svc.GetAllPaginated(ListCardMasterQuery{Page: 1, Limit: 20})
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got: %v", err)
|
||||
}
|
||||
if total != 2 {
|
||||
t.Errorf("expected total 2, got: %d", total)
|
||||
}
|
||||
if len(list) != 2 {
|
||||
t.Errorf("expected list length 2, got: %d", len(list))
|
||||
}
|
||||
}
|
||||
|
||||
func TestService_GetAllPaginated_DefaultNormalization(t *testing.T) {
|
||||
var capturedPage, capturedLimit int
|
||||
|
||||
repo := &mockRepository{
|
||||
findAllPaginatedFunc: func(page, limit int, query ListCardMasterQuery) ([]CardMaster, int64, error) {
|
||||
capturedPage, capturedLimit = page, limit
|
||||
return []CardMaster{{ID: 1, Name: "Bulbasaur"}}, 1, nil
|
||||
},
|
||||
}
|
||||
svc := NewService(repo)
|
||||
|
||||
_, total, err := svc.GetAllPaginated(ListCardMasterQuery{Page: 0, Limit: 0})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got: %v", err)
|
||||
}
|
||||
if total != 1 {
|
||||
t.Errorf("expected total 1, got: %d", total)
|
||||
}
|
||||
if capturedPage != 1 {
|
||||
t.Errorf("expected page normalized to 1, got: %d", capturedPage)
|
||||
}
|
||||
if capturedLimit != 20 {
|
||||
t.Errorf("expected limit normalized to 20, got: %d", capturedLimit)
|
||||
}
|
||||
}
|
||||
|
||||
func TestService_GetByID_Success(t *testing.T) {
|
||||
repo := &mockRepository{
|
||||
findByIDFunc: func(id uint) (*CardMaster, error) {
|
||||
return &CardMaster{ID: id, Name: "Bulbasaur"}, nil
|
||||
},
|
||||
}
|
||||
svc := NewService(repo)
|
||||
|
||||
c, err := svc.GetByID(1)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got: %v", err)
|
||||
}
|
||||
if c.ID != 1 {
|
||||
t.Errorf("expected ID 1, got: %d", c.ID)
|
||||
}
|
||||
if c.Name != "Bulbasaur" {
|
||||
t.Errorf("expected Name 'Bulbasaur', got: '%s'", c.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestService_GetByID_NotFound(t *testing.T) {
|
||||
repo := &mockRepository{
|
||||
findByIDFunc: func(id uint) (*CardMaster, error) {
|
||||
return nil, errors.New("record not found")
|
||||
},
|
||||
}
|
||||
svc := NewService(repo)
|
||||
|
||||
_, err := svc.GetByID(999)
|
||||
if !errors.Is(err, ErrCardNotFound) {
|
||||
t.Fatalf("expected ErrCardNotFound, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestService_Update_Success(t *testing.T) {
|
||||
repo := &mockRepository{
|
||||
findByIDFunc: func(id uint) (*CardMaster, error) {
|
||||
return &CardMaster{ID: id, Name: "Old Name", Element: "Daun"}, nil
|
||||
},
|
||||
updateFunc: func(c *CardMaster, oldIDSet uint) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
svc := NewService(repo)
|
||||
|
||||
req := UpdateCardMasterRequest{
|
||||
Name: "Bulbasaur Mega",
|
||||
Element: "Daun",
|
||||
}
|
||||
|
||||
updated, err := svc.Update(1, req)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got: %v", err)
|
||||
}
|
||||
if updated.Name != "Bulbasaur Mega" {
|
||||
t.Errorf("expected Name 'Bulbasaur Mega', got: '%s'", updated.Name)
|
||||
}
|
||||
if updated.ElementImage != "/images/elements/Grass.png" {
|
||||
t.Errorf("expected ElementImage '/images/elements/Grass.png', got: '%s'", updated.ElementImage)
|
||||
}
|
||||
}
|
||||
|
||||
func TestService_Update_NotFound(t *testing.T) {
|
||||
repo := &mockRepository{
|
||||
findByIDFunc: func(id uint) (*CardMaster, error) {
|
||||
return nil, errors.New("record not found")
|
||||
},
|
||||
}
|
||||
svc := NewService(repo)
|
||||
|
||||
_, err := svc.Update(999, UpdateCardMasterRequest{Name: "New Name"})
|
||||
if !errors.Is(err, ErrCardNotFound) {
|
||||
t.Fatalf("expected ErrCardNotFound, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestService_Update_EnrichElementImage(t *testing.T) {
|
||||
repo := &mockRepository{
|
||||
findByIDFunc: func(id uint) (*CardMaster, error) {
|
||||
return &CardMaster{ID: id, Name: "Charmander"}, nil
|
||||
},
|
||||
updateFunc: func(c *CardMaster, oldIDSet uint) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
svc := NewService(repo)
|
||||
|
||||
req := UpdateCardMasterRequest{
|
||||
HP: &HP{Value: "70", Element: "Api"},
|
||||
Attacks: []Attack{
|
||||
{
|
||||
Name: "Flame Burst",
|
||||
Cost: []AttackCost{
|
||||
{Element: "Api"},
|
||||
},
|
||||
Damage: "30",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
updated, err := svc.Update(1, req)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got: %v", err)
|
||||
}
|
||||
|
||||
var parsedHP HP
|
||||
if err := json.Unmarshal(updated.HP, &parsedHP); err != nil {
|
||||
t.Fatalf("failed to unmarshal HP: %v", err)
|
||||
}
|
||||
if parsedHP.ElementImage != "/images/elements/Fire.png" {
|
||||
t.Errorf("expected HP ElementImage '/images/elements/Fire.png', got: '%s'", parsedHP.ElementImage)
|
||||
}
|
||||
|
||||
var parsedAttacks []Attack
|
||||
if err := json.Unmarshal(updated.Attacks, &parsedAttacks); err != nil {
|
||||
t.Fatalf("failed to unmarshal Attacks: %v", err)
|
||||
}
|
||||
if len(parsedAttacks) != 1 || parsedAttacks[0].Cost[0].Image != "/images/elements/Fire.png" {
|
||||
t.Errorf("expected Attack Cost Image '/images/elements/Fire.png', got: '%s'", parsedAttacks[0].Cost[0].Image)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user