diff --git a/internal/common/techbloghub.paging.go b/internal/common/techbloghub.paging.go new file mode 100644 index 0000000..0d89879 --- /dev/null +++ b/internal/common/techbloghub.paging.go @@ -0,0 +1,45 @@ +package common + +import "strconv" + +const ( + DEFAULT_SIZE = 20 + MAXIMUM_SIZE = 100 + + CURSOR_DEFAULT = 0 +) + +type TechbloghubPaging struct { + Cursor int + Size int +} + +func GenerateTechPaging(cursorStr string, sizeStr string) TechbloghubPaging { + return TechbloghubPaging{ + Cursor: toCursor(cursorStr), + Size: toSize(sizeStr), + } +} + +func toCursor(cursorStr string) int { + cursor, err := strconv.Atoi(cursorStr) + if err != nil { + return CURSOR_DEFAULT + } + return cursor +} + +func toSize(sizeStr string) int { + size, err := strconv.Atoi(sizeStr) + if err != nil { + return DEFAULT_SIZE + } + if size >= MAXIMUM_SIZE { + return MAXIMUM_SIZE + } + return size +} + +func (t TechbloghubPaging) HasNextPage(size int) bool { + return t.Size < size +} diff --git a/internal/http/handler/postinghandler.go b/internal/http/handler/postinghandler.go new file mode 100644 index 0000000..eb77c41 --- /dev/null +++ b/internal/http/handler/postinghandler.go @@ -0,0 +1,95 @@ +package handler + +import ( + "net/http" + "time" + + "github.com/gin-gonic/gin" + "github.com/techbloghub/server/ent" + "github.com/techbloghub/server/ent/posting" + "github.com/techbloghub/server/internal/common" +) + +type TitleSearchResponse struct { + ID int `json:"posting_id"` + Title string `json:"title"` + Url string `json:"url"` + Company string `json:"company"` + Logo string `json:"logo"` + Tags []string `json:"tags"` + CreateTime time.Time `json:"create_time"` + UpdateTime time.Time `json:"update_time"` + PublishedTime time.Time `json:"published_time"` +} + +type PostingSearchResponses struct { + Count int `json:"count"` + Postings []TitleSearchResponse `json:"postings"` + HasNextPage bool `json:"has_next_page"` +} + +func GetPostings(client *ent.Client) gin.HandlerFunc { + return func(c *gin.Context) { + titleSearchParam := c.DefaultQuery("title", "") + paging := common.GenerateTechPaging(c.Query("cursor"), c.Query("size")) + + totalCount, err := countTotalPostings(client, titleSearchParam, c) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + postings, err := fetchPostings(client, titleSearchParam, paging, c) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, PostingSearchResponses{ + Count: totalCount, + Postings: convertToTitleSearchResponse(postings), + HasNextPage: paging.HasNextPage(totalCount), + }) + } +} + +func fetchPostings(client *ent.Client, title string, paging common.TechbloghubPaging, c *gin.Context) ([]*ent.Posting, error) { + query := client.Posting.Query().WithCompany() + if title != "" { + query = query.Where(posting.TitleContainsFold(title)) + } + if paging.Cursor > 0 { + query = query.Where(posting.IDLT(paging.Cursor)) + } + + return query.Order( + ent.Desc(posting.FieldPublishedAt), + ent.Desc(posting.FieldID), + ).Limit(paging.Size).All(c) +} + +func countTotalPostings(client *ent.Client, title string, c *gin.Context) (int, error) { + query := client.Posting.Query() + if title != "" { + query = query.Where(posting.TitleContainsFold(title)) + } + return query.Count(c) +} + +func convertToTitleSearchResponse(postings []*ent.Posting) []TitleSearchResponse { + responses := make([]TitleSearchResponse, len(postings)) + for i, posting := range postings { + responses[i] = TitleSearchResponse{ + ID: posting.ID, + Title: posting.Title, + Url: posting.URL.String(), + Company: posting.Edges.Company.Name, + Logo: posting.Edges.Company.LogoURL.String(), + Tags: posting.Tags.ToStringSlice(), + CreateTime: posting.CreateTime, + UpdateTime: posting.UpdateTime, + PublishedTime: posting.PublishedAt, + } + } + return responses +} diff --git a/internal/http/router/router.go b/internal/http/router/router.go index 2cfb6c6..c016bd1 100644 --- a/internal/http/router/router.go +++ b/internal/http/router/router.go @@ -16,4 +16,7 @@ func InitRouter(r *gin.Engine, client *ent.Client) { // 태그 전체 목록 조회 r.GET("/tags", handler.GetTags(client)) + + // 포스팅(게시글 조회) + r.GET("/postings", handler.GetPostings(client)) } diff --git a/internal/schemasupport/posting_tags.go b/internal/schemasupport/posting_tags.go index ea5f197..5d23e37 100644 --- a/internal/schemasupport/posting_tags.go +++ b/internal/schemasupport/posting_tags.go @@ -8,10 +8,29 @@ import ( type PostingTags []string -func (s PostingTags) Value() (driver.Value, error) { - return pq.Array(s).Value() +func (t *PostingTags) Scan(value interface{}) error { + if value == nil { + *t = []string{} + return nil + } + + var arr []string + if err := pq.Array(&arr).Scan(value); err != nil { + return err + } + + *t = arr + return nil +} + +func (t PostingTags) Value() (driver.Value, error) { + return pq.Array(t).Value() } -func (s *PostingTags) Scan(value interface{}) error { - return pq.Array(s).Scan(value) +func (t *PostingTags) ToStringSlice() []string { + tags := make([]string, len(*t)) + for i, tag := range *t { + tags[i] = tag + } + return tags }