윤준석

Merge branch 'feature/220510_joongna_api' into 'main'

Feature/220510 joongna api

# 중고나라 키워드 검색 api

1. `api/v2/joongna/{keyword}` GET 요청하려 keyword 전달

2. 네이버 카페 검색 api를 통해 유사도 높은 순서로 검색

3. 검색 결과의 카페 링크를 통해 세부 항목 크롤링 (selenium)

4. 세부 항목 담아서 response

See merge request !6
1 +FROM golang:1.17.3
2 +
3 +RUN apt-get -y update
4 +RUN apt-get install -y wget xvfb gnupg
5 +
6 +RUN wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add -
7 +RUN sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google-chrome.list'
8 +RUN apt-get -y update
9 +RUN apt-get install -y google-chrome-stable
10 +
11 +RUN apt-get install -yqq unzip
12 +RUN wget -O /tmp/chromedriver.zip http://chromedriver.storage.googleapis.com/`curl -sS chromedriver.storage.googleapis.com/LATEST_RELEASE`/chromedriver_linux64.zip
13 +RUN unzip /tmp/chromedriver.zip chromedriver -d /usr/local/bin/
14 +
15 +ENV Xvfb :99
16 +ENV DISPLAY=:99
1 +package controller
2 +
3 +import (
4 + "joongna/service"
5 + "net/http"
6 +
7 + "github.com/labstack/echo/v4"
8 +)
9 +
10 +func Search(c echo.Context) error {
11 + keyword := c.Param("keyword")
12 + items, err := service.GetItemByKeyword(keyword)
13 + if err != nil {
14 + return err
15 + }
16 + return c.JSON(http.StatusOK, items)
17 +}
...@@ -3,6 +3,23 @@ module joongna ...@@ -3,6 +3,23 @@ module joongna
3 go 1.17 3 go 1.17
4 4
5 require ( 5 require (
6 + github.com/PuerkitoBio/goquery v1.8.0 // indirect
7 + github.com/andybalholm/cascadia v1.3.1 // indirect
8 + github.com/blang/semver v3.5.1+incompatible // indirect
9 + github.com/bunsenapp/go-selenium v0.1.0 // indirect
6 github.com/caarlos0/env/v6 v6.9.1 // indirect 10 github.com/caarlos0/env/v6 v6.9.1 // indirect
11 + github.com/fedesog/webdriver v0.0.0-20180606182539-99f36c92eaef // indirect
7 github.com/joho/godotenv v1.4.0 // indirect 12 github.com/joho/godotenv v1.4.0 // indirect
13 + github.com/labstack/echo/v4 v4.7.2 // indirect
14 + github.com/labstack/gommon v0.3.1 // indirect
15 + github.com/mattn/go-colorable v0.1.11 // indirect
16 + github.com/mattn/go-isatty v0.0.14 // indirect
17 + github.com/tebeka/selenium v0.9.9 // indirect
18 + github.com/valyala/bytebufferpool v1.0.0 // indirect
19 + github.com/valyala/fasttemplate v1.2.1 // indirect
20 + golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect
21 + golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f // indirect
22 + golang.org/x/sys v0.0.0-20211103235746-7861aae1554b // indirect
23 + golang.org/x/text v0.3.7 // indirect
24 + sourcegraph.com/sourcegraph/go-selenium v0.0.0-20170113155244-3da7d00aac9c // indirect
8 ) 25 )
......
This diff is collapsed. Click to expand it.
1 package main 1 package main
2 2
3 import ( 3 import (
4 - "fmt" 4 + "joongna/router"
5 - "io/ioutil" 5 +
6 - "joongna/config" 6 + "github.com/labstack/echo/v4"
7 - "log"
8 - "net/http"
9 - url2 "net/url"
10 ) 7 )
11 8
12 func main() { 9 func main() {
13 - keyword := "m1 pro 맥북 프로 16인치" 10 + e := echo.New()
14 - encText := url2.QueryEscape("중고나라" + keyword)
15 - url := "https://openapi.naver.com/v1/search/cafearticle.json?query=" + encText + "&sort=sim"
16 -
17 - req, err := http.NewRequest("GET", url, nil)
18 - if err != nil {
19 - log.Fatal(err)
20 - }
21 - req.Header.Add("X-Naver-Client-Id", config.Cfg.Secret.CLIENTID)
22 - req.Header.Add("X-Naver-Client-Secret", config.Cfg.Secret.CLIENTSECRET)
23 11
24 - client := &http.Client{} 12 + router.Init(e)
25 - resp, err := client.Do(req)
26 - if err != nil {
27 - log.Fatal(err)
28 - }
29 - defer resp.Body.Close()
30 13
31 - bytes, _ := ioutil.ReadAll(resp.Body) 14 + e.Logger.Fatal(e.Start(":8080"))
32 - str := string(bytes)
33 - fmt.Println(str)
34 } 15 }
......
1 +package model
2 +
3 +type ApiResponse struct {
4 + LastBuildDate string `json:"lastBuildDate"`
5 + Total uint `json:"total"`
6 + Start uint `json:"start"`
7 + Display uint `json:"display"`
8 + Items []ApiResponseItem `json:"items"`
9 +}
10 +
11 +type ApiResponseItem struct {
12 + Title string `json:"title"`
13 + Link string `json:"link"`
14 + Description string `json:"description"`
15 + CafeName string `json:"cafename"`
16 +}
1 +package model
2 +
3 +type Item struct {
4 + Platform string `json:"platform"`
5 + Name string `json:"name"`
6 + Price int `json:"price"`
7 + ThumbnailUrl string `json:"thumbnailUrl"`
8 + ItemUrl string `json:"itemUrl"`
9 + ExtraInfo string `json:"extraInfo"`
10 +}
1 +package router
2 +
3 +import (
4 + "joongna/controller"
5 +
6 + "github.com/labstack/echo/v4"
7 +)
8 +
9 +const (
10 + API = "/api/v2"
11 + APIJoongNa = API + "/JoongNa"
12 + APIKeyword = APIJoongNa + "/:keyword"
13 +)
14 +
15 +func Init(e *echo.Echo) {
16 + e.GET(APIKeyword, controller.Search)
17 +}
1 +package service
2 +
3 +import (
4 + "bytes"
5 + "encoding/json"
6 + "io"
7 + "io/ioutil"
8 + "joongna/config"
9 + "joongna/model"
10 + "log"
11 + "net/http"
12 + "net/url"
13 + "strconv"
14 + "strings"
15 + "time"
16 +
17 + "github.com/PuerkitoBio/goquery"
18 + "github.com/fedesog/webdriver"
19 +)
20 +
21 +func GetItemByKeyword(keyword string) ([]model.Item, error) {
22 + var items []model.Item
23 +
24 + itemsInfo := getItemsInfoByKeyword(keyword)
25 + for _, itemInfo := range itemsInfo {
26 + if itemInfo.CafeName != "중고나라" {
27 + continue
28 + }
29 + itemUrl := itemInfo.Link
30 + sold, price, thumbnailUrl, extraInfo := crawlingNaverCafe(itemUrl)
31 +
32 + if sold == "판매 완료" {
33 + continue
34 + }
35 +
36 + item := model.Item{
37 + Platform: "중고나라",
38 + Name: itemInfo.Title,
39 + Price: price,
40 + ThumbnailUrl: thumbnailUrl,
41 + ItemUrl: itemUrl,
42 + ExtraInfo: extraInfo,
43 + }
44 + items = append(items, item)
45 + }
46 + return items, nil
47 +}
48 +
49 +func getItemsInfoByKeyword(keyword string) []model.ApiResponseItem {
50 + encText := url.QueryEscape("중고나라 " + keyword + " 판매중")
51 + apiUrl := "https://openapi.naver.com/v1/search/cafearticle.json?query=" + encText + "&sort=sim"
52 +
53 + req, err := http.NewRequest("GET", apiUrl, nil)
54 + if err != nil {
55 + log.Fatal(err)
56 + }
57 + req.Header.Add("X-Naver-Client-Id", config.Cfg.Secret.CLIENTID)
58 + req.Header.Add("X-Naver-Client-Secret", config.Cfg.Secret.CLIENTSECRET)
59 +
60 + client := &http.Client{}
61 + resp, err := client.Do(req)
62 + if err != nil {
63 + log.Fatal(err)
64 + }
65 + defer func(Body io.ReadCloser) {
66 + err := Body.Close()
67 + if err != nil {
68 + log.Fatal(err)
69 + }
70 + }(resp.Body)
71 +
72 + response, _ := ioutil.ReadAll(resp.Body)
73 + var apiResponse model.ApiResponse
74 + err = json.Unmarshal(response, &apiResponse)
75 + if err != nil {
76 + log.Fatal(err)
77 + }
78 + return apiResponse.Items
79 +}
80 +
81 +func crawlingNaverCafe(cafeUrl string) (string, int, string, string) {
82 + driver := webdriver.NewChromeDriver("./chromedriver")
83 + err := driver.Start()
84 + if err != nil {
85 + log.Println(err)
86 + }
87 + desired := webdriver.Capabilities{"Platform": "Linux"}
88 + required := webdriver.Capabilities{}
89 + session, err := driver.NewSession(desired, required)
90 + if err != nil {
91 + log.Println(err)
92 + }
93 + err = session.Url(cafeUrl)
94 + if err != nil {
95 + log.Println(err)
96 + }
97 + time.Sleep(time.Second * 1)
98 + err = session.FocusOnFrame("cafe_main")
99 + if err != nil {
100 + log.Fatal(err)
101 + }
102 + resp, err := session.Source()
103 +
104 + html, err := goquery.NewDocumentFromReader(bytes.NewReader([]byte(resp)))
105 + if err != nil {
106 + log.Fatal(err)
107 + }
108 +
109 + sold := html.Find("div.sold_area").Text()
110 + price := priceStringToInt(html.Find(".ProductPrice").Text())
111 + thumbnailUrl, _ := html.Find("div.product_thumb img").Attr("src")
112 + extraInfo := html.Find(".se-module-text").Text()
113 +
114 + sold = strings.TrimSpace(sold)
115 + thumbnailUrl = strings.TrimSpace(thumbnailUrl)
116 + extraInfo = strings.TrimSpace(extraInfo)
117 +
118 + return sold, price, thumbnailUrl, extraInfo
119 +}
120 +
121 +func priceStringToInt(priceString string) int {
122 + strings.TrimSpace(priceString)
123 +
124 + priceString = strings.ReplaceAll(priceString, "원", "")
125 + priceString = strings.ReplaceAll(priceString, ",", "")
126 +
127 + price, err := strconv.Atoi(priceString)
128 + if err != nil {
129 + log.Fatal(err)
130 + }
131 + return price
132 +}