package repository import ( "context" "with_you/internal/model" "with_you/internal/pkg/cursor" "gorm.io/gorm" ) type TradeRepository interface { Create(ctx context.Context, item *model.TradeItem, imageUrls []string) error GetByID(ctx context.Context, id string) (*model.TradeItem, error) Update(ctx context.Context, item *model.TradeItem) error UpdateWithImages(ctx context.Context, item *model.TradeItem, imageUrls *[]string) error Delete(ctx context.Context, id string) error UpdateStatus(ctx context.Context, id string, status model.TradeItemStatus) error ListByCursor(ctx context.Context, tradeType *model.TradeType, category *model.TradeCategory, status *model.TradeItemStatus, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.TradeItem], error) SearchByCursor(ctx context.Context, keyword string, tradeType *model.TradeType, category *model.TradeCategory, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.TradeItem], error) ListByUserByCursor(ctx context.Context, userID string, status *model.TradeItemStatus, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.TradeItem], error) IncrementViews(ctx context.Context, id string) error IncrementFavorites(ctx context.Context, id string) error DecrementFavorites(ctx context.Context, id string) error CreateFavorite(ctx context.Context, tradeItemID, userID string) error DeleteFavorite(ctx context.Context, tradeItemID, userID string) error IsFavorited(ctx context.Context, tradeItemID, userID string) (bool, error) IsFavoritedBatch(ctx context.Context, tradeItemIDs []string, userID string) (map[string]bool, error) } type tradeRepository struct { db *gorm.DB } func NewTradeRepository(db *gorm.DB) TradeRepository { return &tradeRepository{db: db} } func (r *tradeRepository) getDB(ctx context.Context) *gorm.DB { if tx := GetTxFromContext(ctx); tx != nil { return tx } return r.db } func (r *tradeRepository) Create(ctx context.Context, item *model.TradeItem, imageUrls []string) error { db := r.getDB(ctx).WithContext(ctx) return db.Transaction(func(tx *gorm.DB) error { if err := tx.Create(item).Error; err != nil { return err } for i, url := range imageUrls { img := model.TradeImage{ TradeItemID: item.ID, URL: url, SortOrder: i, } if err := tx.Create(&img).Error; err != nil { return err } } return nil }) } func (r *tradeRepository) GetByID(ctx context.Context, id string) (*model.TradeItem, error) { db := r.getDB(ctx).WithContext(ctx) var item model.TradeItem if err := db.Preload("User").Preload("Images", func(db *gorm.DB) *gorm.DB { return db.Order("sort_order ASC") }).First(&item, "id = ?", id).Error; err != nil { return nil, err } return &item, nil } func (r *tradeRepository) Update(ctx context.Context, item *model.TradeItem) error { db := r.getDB(ctx).WithContext(ctx) return db.Save(item).Error } func (r *tradeRepository) UpdateWithImages(ctx context.Context, item *model.TradeItem, imageUrls *[]string) error { db := r.getDB(ctx).WithContext(ctx) return db.Transaction(func(tx *gorm.DB) error { if err := tx.Save(item).Error; err != nil { return err } if imageUrls != nil { if err := tx.Where("trade_item_id = ?", item.ID).Delete(&model.TradeImage{}).Error; err != nil { return err } for i, url := range *imageUrls { img := model.TradeImage{ TradeItemID: item.ID, URL: url, SortOrder: i, } if err := tx.Create(&img).Error; err != nil { return err } } } return nil }) } func (r *tradeRepository) Delete(ctx context.Context, id string) error { db := r.getDB(ctx).WithContext(ctx) return db.Transaction(func(tx *gorm.DB) error { if err := tx.Where("trade_item_id = ?", id).Delete(&model.TradeImage{}).Error; err != nil { return err } if err := tx.Where("trade_item_id = ?", id).Delete(&model.TradeFavorite{}).Error; err != nil { return err } return tx.Delete(&model.TradeItem{}, "id = ?", id).Error }) } func (r *tradeRepository) UpdateStatus(ctx context.Context, id string, status model.TradeItemStatus) error { db := r.getDB(ctx).WithContext(ctx) return db.Model(&model.TradeItem{}).Where("id = ?", id).Update("status", status).Error } func (r *tradeRepository) ListByCursor(ctx context.Context, tradeType *model.TradeType, category *model.TradeCategory, status *model.TradeItemStatus, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.TradeItem], error) { db := r.getDB(ctx).WithContext(ctx) query := db.Model(&model.TradeItem{}) if tradeType != nil { query = query.Where("trade_type = ?", *tradeType) } if category != nil { query = query.Where("category = ?", *category) } defaultStatus := model.TradeItemActive if status != nil { defaultStatus = *status } query = query.Where("status = ?", defaultStatus) builder := cursor.NewBuilder(query, cursor.SortByCreatedAtDesc). WithCursor(req.Cursor, req.Direction). WithPageSize(req.PageSize) if builder.Error() != nil { return cursor.NewCursorPageResult([]*model.TradeItem{}, "", "", false), nil } var items []*model.TradeItem q := builder.Build().Preload("User").Preload("Images", func(db *gorm.DB) *gorm.DB { return db.Order("sort_order ASC") }) if err := q.Find(&items).Error; err != nil { return nil, err } pageSize := builder.GetPageSize() hasMore := cursor.HasMore(len(items), pageSize) if hasMore { items = items[:pageSize] } var nextCursor, prevCursor string if len(items) > 0 { if hasMore { last := items[len(items)-1] nextCursor = cursor.NewCursor(cursor.FormatTime(last.CreatedAt), last.ID, cursor.SortByCreatedAtDesc).Encode() } first := items[0] prevCursor = cursor.NewCursor(cursor.FormatTime(first.CreatedAt), first.ID, cursor.SortByCreatedAtDesc).Encode() } return cursor.NewCursorPageResult(items, nextCursor, prevCursor, hasMore), nil } func (r *tradeRepository) SearchByCursor(ctx context.Context, keyword string, tradeType *model.TradeType, category *model.TradeCategory, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.TradeItem], error) { db := r.getDB(ctx).WithContext(ctx) query := db.Model(&model.TradeItem{}).Where("status = ?", model.TradeItemActive) if tradeType != nil { query = query.Where("trade_type = ?", *tradeType) } if category != nil { query = query.Where("category = ?", *category) } if keyword != "" { searchPattern := "%" + keyword + "%" query = query.Where("title LIKE ? OR content LIKE ?", searchPattern, searchPattern) } builder := cursor.NewBuilder(query, cursor.SortByCreatedAtDesc). WithCursor(req.Cursor, req.Direction). WithPageSize(req.PageSize) if builder.Error() != nil { return cursor.NewCursorPageResult([]*model.TradeItem{}, "", "", false), nil } var items []*model.TradeItem q := builder.Build().Preload("User").Preload("Images", func(db *gorm.DB) *gorm.DB { return db.Order("sort_order ASC") }) if err := q.Find(&items).Error; err != nil { return nil, err } pageSize := builder.GetPageSize() hasMore := cursor.HasMore(len(items), pageSize) if hasMore { items = items[:pageSize] } var nextCursor, prevCursor string if len(items) > 0 { if hasMore { last := items[len(items)-1] nextCursor = cursor.NewCursor(cursor.FormatTime(last.CreatedAt), last.ID, cursor.SortByCreatedAtDesc).Encode() } first := items[0] prevCursor = cursor.NewCursor(cursor.FormatTime(first.CreatedAt), first.ID, cursor.SortByCreatedAtDesc).Encode() } return cursor.NewCursorPageResult(items, nextCursor, prevCursor, hasMore), nil } func (r *tradeRepository) ListByUserByCursor(ctx context.Context, userID string, status *model.TradeItemStatus, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.TradeItem], error) { db := r.getDB(ctx).WithContext(ctx) query := db.Model(&model.TradeItem{}).Where("user_id = ?", userID) if status != nil { query = query.Where("status = ?", *status) } builder := cursor.NewBuilder(query, cursor.SortByCreatedAtDesc). WithCursor(req.Cursor, req.Direction). WithPageSize(req.PageSize) if builder.Error() != nil { return cursor.NewCursorPageResult([]*model.TradeItem{}, "", "", false), nil } var items []*model.TradeItem q := builder.Build().Preload("User").Preload("Images", func(db *gorm.DB) *gorm.DB { return db.Order("sort_order ASC") }) if err := q.Find(&items).Error; err != nil { return nil, err } pageSize := builder.GetPageSize() hasMore := cursor.HasMore(len(items), pageSize) if hasMore { items = items[:pageSize] } var nextCursor, prevCursor string if len(items) > 0 { if hasMore { last := items[len(items)-1] nextCursor = cursor.NewCursor(cursor.FormatTime(last.CreatedAt), last.ID, cursor.SortByCreatedAtDesc).Encode() } first := items[0] prevCursor = cursor.NewCursor(cursor.FormatTime(first.CreatedAt), first.ID, cursor.SortByCreatedAtDesc).Encode() } return cursor.NewCursorPageResult(items, nextCursor, prevCursor, hasMore), nil } func (r *tradeRepository) IncrementViews(ctx context.Context, id string) error { db := r.getDB(ctx).WithContext(ctx) return db.Model(&model.TradeItem{}).Where("id = ?", id).UpdateColumn("views_count", gorm.Expr("views_count + 1")).Error } func (r *tradeRepository) IncrementFavorites(ctx context.Context, id string) error { db := r.getDB(ctx).WithContext(ctx) return db.Model(&model.TradeItem{}).Where("id = ?", id).UpdateColumn("favorites_count", gorm.Expr("favorites_count + 1")).Error } func (r *tradeRepository) DecrementFavorites(ctx context.Context, id string) error { db := r.getDB(ctx).WithContext(ctx) return db.Model(&model.TradeItem{}).Where("id = ? AND favorites_count > 0", id).UpdateColumn("favorites_count", gorm.Expr("favorites_count - 1")).Error } func (r *tradeRepository) CreateFavorite(ctx context.Context, tradeItemID, userID string) error { db := r.getDB(ctx).WithContext(ctx) fav := model.TradeFavorite{ TradeItemID: tradeItemID, UserID: userID, } return db.Create(&fav).Error } func (r *tradeRepository) DeleteFavorite(ctx context.Context, tradeItemID, userID string) error { db := r.getDB(ctx).WithContext(ctx) return db.Where("trade_item_id = ? AND user_id = ?", tradeItemID, userID).Delete(&model.TradeFavorite{}).Error } func (r *tradeRepository) IsFavorited(ctx context.Context, tradeItemID, userID string) (bool, error) { db := r.getDB(ctx).WithContext(ctx) var count int64 if err := db.Model(&model.TradeFavorite{}).Where("trade_item_id = ? AND user_id = ?", tradeItemID, userID).Count(&count).Error; err != nil { return false, err } return count > 0, nil } func (r *tradeRepository) IsFavoritedBatch(ctx context.Context, tradeItemIDs []string, userID string) (map[string]bool, error) { db := r.getDB(ctx).WithContext(ctx) result := make(map[string]bool, len(tradeItemIDs)) if len(tradeItemIDs) == 0 { return result, nil } for _, id := range tradeItemIDs { result[id] = false } var favorites []model.TradeFavorite if err := db.Where("trade_item_id IN ? AND user_id = ?", tradeItemIDs, userID).Find(&favorites).Error; err != nil { return nil, err } for _, fav := range favorites { result[fav.TradeItemID] = true } return result, nil }