From 7cac52050e8631f88c717ff779ff76526f7932c6 Mon Sep 17 00:00:00 2001 From: iamjooon2 Date: Sun, 16 Feb 2025 23:25:03 +0900 Subject: [PATCH 1/8] =?UTF-8?q?feat:=20router=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/http/handler/postinghandler.go | 12 ++++++++++++ internal/http/router/router.go | 3 +++ 2 files changed, 15 insertions(+) create mode 100644 internal/http/handler/postinghandler.go diff --git a/internal/http/handler/postinghandler.go b/internal/http/handler/postinghandler.go new file mode 100644 index 0000000..3b78bce --- /dev/null +++ b/internal/http/handler/postinghandler.go @@ -0,0 +1,12 @@ +package handler + +import ( + "github.com/gin-gonic/gin" + "github.com/techbloghub/server/ent" +) + +func GetPostings(client *ent.Client) gin.HandlerFunc { + return func(c *gin.Context) { + + } +} 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)) } From bdab47e7d52c0a651502addd0db3651a655db4e5 Mon Sep 17 00:00:00 2001 From: iamjooon2 Date: Sun, 2 Mar 2025 01:11:44 +0900 Subject: [PATCH 2/8] =?UTF-8?q?feat:=20=EA=B3=B5=EC=9A=A9=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=95=20struct=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/common/techbloghub.paging.go | 49 +++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 internal/common/techbloghub.paging.go diff --git a/internal/common/techbloghub.paging.go b/internal/common/techbloghub.paging.go new file mode 100644 index 0000000..7586144 --- /dev/null +++ b/internal/common/techbloghub.paging.go @@ -0,0 +1,49 @@ +package common + +import "strconv" + +const ( + DEFAULT_SIZE = 20 + MAXIMUM_SIZE = 100 + + LAST_VIEW_ID_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(lastViewedIdStr string) int { + lastViewId, err := strconv.Atoi(lastViewedIdStr) + if err != nil { + return LAST_VIEW_ID_DEFAULT + } + return lastViewId +} + +func toSize(sizeStr string) int { + size, err := strconv.Atoi(sizeStr) + if err != nil { + return DEFAULT_SIZE + } + return ceil(size) +} + +func ceil(limit int) int { + if limit >= MAXIMUM_SIZE { + return MAXIMUM_SIZE + } + return limit +} + +func (t TechbloghubPaging) HasNextPage(size int) bool { + return t.Size < size +} From d187e388950dbfb55ed4b475e2fe72093c1a1c9a Mon Sep 17 00:00:00 2001 From: iamjooon2 Date: Sun, 2 Mar 2025 01:36:56 +0900 Subject: [PATCH 3/8] =?UTF-8?q?feat:=20=EC=A0=9C=EB=AA=A9=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=ED=8F=AC=EC=8A=A4=ED=8C=85=20=EC=B0=BE=EB=8A=94=20?= =?UTF-8?q?api=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/http/handler/postinghandler.go | 100 ++++++++++++++++++++++++ internal/schemasupport/posting_tags.go | 4 + 2 files changed, 104 insertions(+) diff --git a/internal/http/handler/postinghandler.go b/internal/http/handler/postinghandler.go index 3b78bce..1e0b40e 100644 --- a/internal/http/handler/postinghandler.go +++ b/internal/http/handler/postinghandler.go @@ -1,12 +1,112 @@ package handler import ( + "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:"createTime"` + UpdateTime time.Time `json:"updateTime"` + PublishedTime time.Time `json:"publishedTime"` +} + +type PostingSearchResponses struct { + Count int `json:"id"` + //Tags []TagSearchResponse `json:"tags"` + Postings []TitleSearchResponse `json:"titles"` + HasNextPage bool `json:"hasNextPage"` +} + 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"), + ) + + query := client.Posting. + Query(). + WithCompany() + if titleSearchParam != "" { + query = query.Where(posting.TitleContainsFold(titleSearchParam)) + } + if paging.Cursor > 0 { + query = query.Where(posting.IDLT(paging.Cursor)) + } + postings, err := query. + Order( + ent.Desc(posting.FieldPublishedAt), + ent.Desc(posting.FieldID), + ). + Limit(paging.Size). + All(c) + postingsByTitle := []TitleSearchResponse{} + if err != nil { + postingsByTitle = make([]TitleSearchResponse, len(postings)) + for i, posting := range postings { + postingsByTitle[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.ToStringArray(), + CreateTime: posting.CreateTime, + UpdateTime: posting.UpdateTime, + PublishedTime: posting.PublishedAt, + } + } + } + c.JSON(200, PostingSearchResponses{ + Count: len(postingsByTitle), + Postings: postingsByTitle, + HasNextPage: paging.HasNextPage(len(postings)), + }) } } + +//tagSearchParam := c.DefaultQuery("tag", "") + +//postingsByTag := []TagSearchResponse{} +//if tagSearchParam != "" { +// result, err := client.Posting. +// Query(). +// Where(schema.TagsContains(tagSearchParam)). +// All(c) +// +// if err != nil { +// postingsByTag = make([]TagSearchResponse, len(result)) +// for i, posting := range result { +// postingsByTag[i] = TagSearchResponse{ +// ID: posting.ID, +// Title: posting.Title, +// Url: posting.URL.String(), +// CreateTime: posting.CreateTime, +// UpdateTime: posting.UpdateTime, +// PublishedTime: posting.PublishedAt, +// } +// } +// } +//} +//type TagSearchResponse struct { +// ID int `json:"posting_id"` +// Title string `json:"title"` +// Url string `json:"url"` +// Company string `json:"company"` +// CreateTime time.Time `json:"createTime"` +// UpdateTime time.Time `json:"updateTime"` +// PublishedTime time.Time `json:"publishedTime"` +//} diff --git a/internal/schemasupport/posting_tags.go b/internal/schemasupport/posting_tags.go index ea5f197..a7d0572 100644 --- a/internal/schemasupport/posting_tags.go +++ b/internal/schemasupport/posting_tags.go @@ -15,3 +15,7 @@ func (s PostingTags) Value() (driver.Value, error) { func (s *PostingTags) Scan(value interface{}) error { return pq.Array(s).Scan(value) } + +func (s PostingTags) ToStringArray() []string { + return s +} From 785e967a034269b57cdca74636238f32c071045c Mon Sep 17 00:00:00 2001 From: iamjooon2 Date: Sun, 2 Mar 2025 01:55:12 +0900 Subject: [PATCH 4/8] =?UTF-8?q?feat:=20=EC=A0=9C=EB=AA=A9=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=ED=8F=AC=EC=8A=A4=ED=8C=85=20=EC=B0=BE=EB=8A=94=20?= =?UTF-8?q?api=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/http/handler/postinghandler.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/internal/http/handler/postinghandler.go b/internal/http/handler/postinghandler.go index 1e0b40e..4e1fbcd 100644 --- a/internal/http/handler/postinghandler.go +++ b/internal/http/handler/postinghandler.go @@ -52,9 +52,8 @@ func GetPostings(client *ent.Client) gin.HandlerFunc { ). Limit(paging.Size). All(c) - postingsByTitle := []TitleSearchResponse{} + postingsByTitle := make([]TitleSearchResponse, len(postings)) if err != nil { - postingsByTitle = make([]TitleSearchResponse, len(postings)) for i, posting := range postings { postingsByTitle[i] = TitleSearchResponse{ ID: posting.ID, @@ -70,10 +69,13 @@ func GetPostings(client *ent.Client) gin.HandlerFunc { } } + totalCount, err := client.Posting.Query(). + Where(posting.TitleContainsFold(titleSearchParam)). + Count(c) c.JSON(200, PostingSearchResponses{ - Count: len(postingsByTitle), + Count: totalCount, Postings: postingsByTitle, - HasNextPage: paging.HasNextPage(len(postings)), + HasNextPage: paging.HasNextPage(totalCount), }) } } From e5142a8e0a7279b976a50feb0d94f7127f19ca9a Mon Sep 17 00:00:00 2001 From: iamjooon2 Date: Sun, 2 Mar 2025 02:01:57 +0900 Subject: [PATCH 5/8] =?UTF-8?q?fix:=20api=20=EB=AA=85=EC=84=B8=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/http/handler/postinghandler.go | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/internal/http/handler/postinghandler.go b/internal/http/handler/postinghandler.go index 4e1fbcd..d5002b5 100644 --- a/internal/http/handler/postinghandler.go +++ b/internal/http/handler/postinghandler.go @@ -16,16 +16,15 @@ type TitleSearchResponse struct { Company string `json:"company"` Logo string `json:"logo"` Tags []string `json:"tags"` - CreateTime time.Time `json:"createTime"` - UpdateTime time.Time `json:"updateTime"` - PublishedTime time.Time `json:"publishedTime"` + CreateTime time.Time `json:"create_time"` + UpdateTime time.Time `json:"update_time"` + PublishedTime time.Time `json:"published_time"` } type PostingSearchResponses struct { - Count int `json:"id"` - //Tags []TagSearchResponse `json:"tags"` - Postings []TitleSearchResponse `json:"titles"` - HasNextPage bool `json:"hasNextPage"` + Count int `json:"count"` + Postings []TitleSearchResponse `json:"postings"` + HasNextPage bool `json:"has_next_page"` } func GetPostings(client *ent.Client) gin.HandlerFunc { From bcb3e4f33d0193583aa2c4c6f08f46670e681a83 Mon Sep 17 00:00:00 2001 From: iamjooon2 Date: Sun, 2 Mar 2025 02:04:52 +0900 Subject: [PATCH 6/8] =?UTF-8?q?refactor:=20=EC=97=90=EB=9F=AC=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=EC=9C=84=ED=95=9C=20=EB=A6=AC=ED=8E=99=ED=84=B0?= =?UTF-8?q?=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/http/handler/postinghandler.go | 68 ++++++++----------------- internal/schemasupport/posting_tags.go | 27 +++++++--- 2 files changed, 43 insertions(+), 52 deletions(-) diff --git a/internal/http/handler/postinghandler.go b/internal/http/handler/postinghandler.go index d5002b5..be864c9 100644 --- a/internal/http/handler/postinghandler.go +++ b/internal/http/handler/postinghandler.go @@ -44,6 +44,7 @@ func GetPostings(client *ent.Client) gin.HandlerFunc { if paging.Cursor > 0 { query = query.Where(posting.IDLT(paging.Cursor)) } + postings, err := query. Order( ent.Desc(posting.FieldPublishedAt), @@ -51,26 +52,34 @@ func GetPostings(client *ent.Client) gin.HandlerFunc { ). Limit(paging.Size). All(c) - postingsByTitle := make([]TitleSearchResponse, len(postings)) if err != nil { - for i, posting := range postings { - postingsByTitle[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.ToStringArray(), - CreateTime: posting.CreateTime, - UpdateTime: posting.UpdateTime, - PublishedTime: posting.PublishedAt, - } + c.JSON(500, gin.H{"error": err.Error()}) + return + } + + postingsByTitle := make([]TitleSearchResponse, len(postings)) + for i, posting := range postings { + postingsByTitle[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, } } totalCount, err := client.Posting.Query(). Where(posting.TitleContainsFold(titleSearchParam)). Count(c) + if err != nil { + c.JSON(500, gin.H{"error": err.Error()}) + return + } + c.JSON(200, PostingSearchResponses{ Count: totalCount, Postings: postingsByTitle, @@ -78,36 +87,3 @@ func GetPostings(client *ent.Client) gin.HandlerFunc { }) } } - -//tagSearchParam := c.DefaultQuery("tag", "") - -//postingsByTag := []TagSearchResponse{} -//if tagSearchParam != "" { -// result, err := client.Posting. -// Query(). -// Where(schema.TagsContains(tagSearchParam)). -// All(c) -// -// if err != nil { -// postingsByTag = make([]TagSearchResponse, len(result)) -// for i, posting := range result { -// postingsByTag[i] = TagSearchResponse{ -// ID: posting.ID, -// Title: posting.Title, -// Url: posting.URL.String(), -// CreateTime: posting.CreateTime, -// UpdateTime: posting.UpdateTime, -// PublishedTime: posting.PublishedAt, -// } -// } -// } -//} -//type TagSearchResponse struct { -// ID int `json:"posting_id"` -// Title string `json:"title"` -// Url string `json:"url"` -// Company string `json:"company"` -// CreateTime time.Time `json:"createTime"` -// UpdateTime time.Time `json:"updateTime"` -// PublishedTime time.Time `json:"publishedTime"` -//} diff --git a/internal/schemasupport/posting_tags.go b/internal/schemasupport/posting_tags.go index a7d0572..5d23e37 100644 --- a/internal/schemasupport/posting_tags.go +++ b/internal/schemasupport/posting_tags.go @@ -8,14 +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 (s *PostingTags) Scan(value interface{}) error { - return pq.Array(s).Scan(value) +func (t PostingTags) Value() (driver.Value, error) { + return pq.Array(t).Value() } -func (s PostingTags) ToStringArray() []string { - return s +func (t *PostingTags) ToStringSlice() []string { + tags := make([]string, len(*t)) + for i, tag := range *t { + tags[i] = tag + } + return tags } From 09e9a9e0f9b1e774b856d505906c33949d24dfd0 Mon Sep 17 00:00:00 2001 From: iamjooon2 Date: Sun, 2 Mar 2025 04:44:50 +0900 Subject: [PATCH 7/8] =?UTF-8?q?refactor:=20=ED=95=A8=EC=88=98=EB=AA=85=20?= =?UTF-8?q?=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/common/techbloghub.paging.go | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/internal/common/techbloghub.paging.go b/internal/common/techbloghub.paging.go index 7586144..0d89879 100644 --- a/internal/common/techbloghub.paging.go +++ b/internal/common/techbloghub.paging.go @@ -6,7 +6,7 @@ const ( DEFAULT_SIZE = 20 MAXIMUM_SIZE = 100 - LAST_VIEW_ID_DEFAULT = 0 + CURSOR_DEFAULT = 0 ) type TechbloghubPaging struct { @@ -21,12 +21,12 @@ func GenerateTechPaging(cursorStr string, sizeStr string) TechbloghubPaging { } } -func toCursor(lastViewedIdStr string) int { - lastViewId, err := strconv.Atoi(lastViewedIdStr) +func toCursor(cursorStr string) int { + cursor, err := strconv.Atoi(cursorStr) if err != nil { - return LAST_VIEW_ID_DEFAULT + return CURSOR_DEFAULT } - return lastViewId + return cursor } func toSize(sizeStr string) int { @@ -34,14 +34,10 @@ func toSize(sizeStr string) int { if err != nil { return DEFAULT_SIZE } - return ceil(size) -} - -func ceil(limit int) int { - if limit >= MAXIMUM_SIZE { + if size >= MAXIMUM_SIZE { return MAXIMUM_SIZE } - return limit + return size } func (t TechbloghubPaging) HasNextPage(size int) bool { From 3c303342068985580647d9406a84ac83d3acd04a Mon Sep 17 00:00:00 2001 From: iamjooon2 Date: Sun, 2 Mar 2025 14:06:59 +0900 Subject: [PATCH 8/8] =?UTF-8?q?refactor:=20=EB=A9=94=EC=84=9C=EB=93=9C=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/http/handler/postinghandler.go | 92 +++++++++++++------------ 1 file changed, 49 insertions(+), 43 deletions(-) diff --git a/internal/http/handler/postinghandler.go b/internal/http/handler/postinghandler.go index be864c9..eb77c41 100644 --- a/internal/http/handler/postinghandler.go +++ b/internal/http/handler/postinghandler.go @@ -1,6 +1,7 @@ package handler import ( + "net/http" "time" "github.com/gin-gonic/gin" @@ -30,60 +31,65 @@ type PostingSearchResponses struct { 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"), - ) + paging := common.GenerateTechPaging(c.Query("cursor"), c.Query("size")) - query := client.Posting. - Query(). - WithCompany() - if titleSearchParam != "" { - query = query.Where(posting.TitleContainsFold(titleSearchParam)) - } - if paging.Cursor > 0 { - query = query.Where(posting.IDLT(paging.Cursor)) - } - - postings, err := query. - Order( - ent.Desc(posting.FieldPublishedAt), - ent.Desc(posting.FieldID), - ). - Limit(paging.Size). - All(c) + totalCount, err := countTotalPostings(client, titleSearchParam, c) if err != nil { - c.JSON(500, gin.H{"error": err.Error()}) + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } - postingsByTitle := make([]TitleSearchResponse, len(postings)) - for i, posting := range postings { - postingsByTitle[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, - } - } - - totalCount, err := client.Posting.Query(). - Where(posting.TitleContainsFold(titleSearchParam)). - Count(c) + postings, err := fetchPostings(client, titleSearchParam, paging, c) if err != nil { - c.JSON(500, gin.H{"error": err.Error()}) + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } - c.JSON(200, PostingSearchResponses{ + c.JSON(http.StatusOK, PostingSearchResponses{ Count: totalCount, - Postings: postingsByTitle, + 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 +}