유명현

Merge branch 'main' into feature/line_bot

FROM golang:1.17.3-alpine AS builder
WORKDIR /src
COPY . /src
RUN go build -o bunjang_api_server
FROM alpine
WORKDIR /src
COPY --from=builder /src/bunjang_api_server /src/bunjang_api_server
EXPOSE 8080
CMD ["./bunjang_api_server"]
package controller
import (
"bunjang/service"
"net/http"
"github.com/labstack/echo/v4"
)
func Search(c echo.Context) error {
keyword := c.Param("keyword")
items, err := service.GetItemByKeyword(keyword)
if err != nil {
return err
}
return c.JSON(http.StatusOK, items)
}
#!/usr/bin/env bash
docker build -t bunjang-api-server .
docker-compose up -d
\ No newline at end of file
version: '3'
services:
joongna_api:
image: bunjang-api-server
restart: always
container_name: bunjang-api-server-container
ports:
- '18082:8080'
\ No newline at end of file
module bunjang
go 1.17
require (
github.com/PuerkitoBio/goquery v1.8.0 // indirect
github.com/andybalholm/cascadia v1.3.1 // indirect
github.com/go-rod/rod v0.106.8 // indirect
github.com/labstack/echo/v4 v4.7.2 // indirect
github.com/labstack/gommon v0.3.1 // indirect
github.com/mattn/go-colorable v0.1.11 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.1 // indirect
github.com/ysmood/goob v0.4.0 // indirect
github.com/ysmood/gson v0.7.1 // indirect
github.com/ysmood/leakless v0.7.0 // indirect
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f // indirect
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b // indirect
golang.org/x/text v0.3.7 // indirect
)
github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U=
github.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI=
github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c=
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-rod/rod v0.106.8 h1:pVMVz0jMtLVyx8FhJEEA6l+EY9Iw/nJTDYT/he4+UJc=
github.com/go-rod/rod v0.106.8/go.mod h1:xkZOchuKqTOkMOBkrzb7uJpbKZRab1haPCWDvuZkS2U=
github.com/labstack/echo/v4 v4.7.2 h1:Kv2/p8OaQ+M6Ex4eGimg9b9e6icoxA42JSlOR3msKtI=
github.com/labstack/echo/v4 v4.7.2/go.mod h1:xkCDAdFCIf8jsFQ5NnbK7oqaF/yU1A1X20Ltm0OvSks=
github.com/labstack/gommon v0.3.1 h1:OomWaJXm7xR6L1HmEtGyQf26TEn7V6X88mktX9kee9o=
github.com/labstack/gommon v0.3.1/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
github.com/mattn/go-colorable v0.1.11 h1:nQ+aFkoE2TMGc0b68U2OKSexC+eq46+XwZzWXHRmPYs=
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4=
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/ysmood/goob v0.4.0 h1:HsxXhyLBeGzWXnqVKtmT9qM7EuVs/XOgkX7T6r1o1AQ=
github.com/ysmood/goob v0.4.0/go.mod h1:u6yx7ZhS4Exf2MwciFr6nIM8knHQIE22lFpWHnfql18=
github.com/ysmood/got v0.29.1/go.mod h1:pE1l4LOwOBhQg6A/8IAatkGp7uZjnalzrZolnlhhMgY=
github.com/ysmood/gotrace v0.6.0/go.mod h1:TzhIG7nHDry5//eYZDYcTzuJLYQIkykJzCRIo4/dzQM=
github.com/ysmood/gson v0.7.1 h1:zKL2MTGtynxdBdlZjyGsvEOZ7dkxaY5TH6QhAbTgz0Q=
github.com/ysmood/gson v0.7.1/go.mod h1:3Kzs5zDl21g5F/BlLTNcuAGAYLKt2lV5G8D1zF3RNmg=
github.com/ysmood/leakless v0.7.0 h1:XCGdaPExyoreoQd+H5qgxM3ReNbSPFsEXpSKwbXbwQw=
github.com/ysmood/leakless v0.7.0/go.mod h1:R8iAXPRaG97QJwqxs74RdwzcRHT1SWCGTNqY8q0JvMQ=
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ=
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f h1:OfiFi4JbukWwe3lzw+xunroH1mnC1e2Gy5cxNJApiSY=
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b h1:1VkfZQv42XQlA/jchYumAnv1UPo6RgF9rJFkTgZIxO4=
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
package main
import (
"bunjang/router"
"github.com/labstack/echo/v4"
)
func main() {
e := echo.New()
router.Init(e)
e.Logger.Fatal(e.Start(":8080"))
}
package model
type ApiResponse struct {
Result string `json:"result"`
NoResult bool `json:"no_result"`
Items []ApiResponseItem `json:"list"`
}
type ApiResponseItem struct {
Name string `json:"name"`
Pid string `json:"pid"`
Price string `json:"price"`
ProductImage string `json:"product_image"`
}
package model
type Item struct {
Platform string `json:"platform"`
Name string `json:"name"`
Price int `json:"price"`
ThumbnailUrl string `json:"thumbnailUrl"`
ItemUrl string `json:"itemUrl"`
ExtraInfo string `json:"extraInfo"`
}
package router
import (
"bunjang/controller"
"github.com/labstack/echo/v4"
)
const (
API = "/api/v2"
APIBunJang = API + "/bunjang"
APIKeyword = APIBunJang + "/:keyword"
)
func Init(e *echo.Echo) {
e.GET(APIKeyword, controller.Search)
}
package service
import (
"bunjang/model"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"net/url"
"strconv"
"strings"
"sync"
)
func GetItemByKeyword(keyword string) ([]model.Item, error) {
var items []model.Item
wg := sync.WaitGroup{}
responseItems, err := getApiResponseItems(keyword)
if err != nil {
return nil, err
}
for _, responseItem := range responseItems {
wg.Add(1)
go func(responseItem model.ApiResponseItem) {
defer wg.Done()
extraInfo, err := getItemExtraInfo(responseItem.Pid)
if err != nil {
log.Fatal(err)
}
item := model.Item{
Platform: "번개장터",
Name: responseItem.Name,
Price: priceStringToInt(responseItem.Price),
ThumbnailUrl: responseItem.ProductImage,
ItemUrl: "https://m.bunjang.co.kr/products/" + responseItem.Pid,
ExtraInfo: extraInfo,
}
items = append(items, item)
}(responseItem)
}
wg.Wait()
return items, nil
}
func getApiResponseItems(keyword string) ([]model.ApiResponseItem, error) {
encText := url.QueryEscape(keyword)
apiUrl := fmt.Sprintf("https://api.bunjang.co.kr/api/1/find_v2.json?q=%s&order=score&n=6", encText)
response, err := getResponse(apiUrl)
if err != nil {
return nil, err
}
var apiResponse model.ApiResponse
err = json.Unmarshal(response, &apiResponse)
if err != nil {
return nil, err
}
return apiResponse.Items, nil
}
func getItemExtraInfo(pid string) (string, error) {
apiUrl := fmt.Sprintf("https://api.bunjang.co.kr/api/1/product/%s/detail_info.json", pid)
response, err := getResponse(apiUrl)
if err != nil {
return "", err
}
var itemInfo map[string]interface{}
err = json.Unmarshal(response, &itemInfo)
if err != nil {
return "", err
}
extraInfo := itemInfo["item_info"].(map[string]interface{})["description_for_detail"].(string)
return extraInfo, nil
}
func getResponse(url string) ([]byte, error) {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
log.Fatal(err)
}
}(resp.Body)
response, _ := ioutil.ReadAll(resp.Body)
return response, nil
}
func priceStringToInt(priceString string) int {
strings.TrimSpace(priceString)
if priceString == "" {
return 0
}
priceString = strings.ReplaceAll(priceString, "원", "")
priceString = strings.ReplaceAll(priceString, ",", "")
price, err := strconv.Atoi(priceString)
if err != nil {
log.Fatal(err)
}
return price
}
#!/usr/bin/env bash
docker-compose down
docker image rm bunjang-api-server
\ No newline at end of file
FROM python:3
FROM python:alpine
WORKDIR /usr/src/app
......
#!/usr/bin/env bash
docker build -t daangn-api-server ./daangn/
docker build -t joongna-api-server ./joongna/
docker build -t bunjang-api-server ./bunjang/
docker build -t mamuri-db ./database/
docker-compose up -d
\ No newline at end of file
version: '3'
services:
daangn_api:
image: daangn-api-server
restart: always
container_name: daangn-api-server-container
ports:
- '18080:8080'
joongna_api:
image: joongna-api-server
restart: always
container_name: joongna-api-server-container
ports:
- '18081:8080'
bunjang_api:
image: bunjang-api-server
restart: always
container_name: bunjang-api-server-container
ports:
- '18082:8080'
db:
image: mamuri-db
restart: always
container_name: mamuri-db-container
ports:
- '13060:3306'
env_file:
- "./database/mysql_init/.env"
\ No newline at end of file
......@@ -11,7 +11,5 @@ WORKDIR /src
COPY --from=builder /src/joongna_api_server /src/joongna_api_server
COPY --from=builder /src/config/.env /src/config/.env
RUN apk add chromium
EXPOSE 8080
CMD ["./joongna_api_server"]
......
......@@ -12,6 +12,11 @@ type Config struct {
CLIENTID string `env:"SECRET.CLIENTID"`
CLIENTSECRET string `env:"SECRET.CLIENTSECRET"`
}
Header struct {
Cookie string `env:"HEADER.COOKIE"`
UserAgent string `env:"HEADER.USERAGENT"`
}
}
var Cfg *Config
......
package model
type ApiResponse struct {
LastBuildDate string `json:"lastBuildDate"`
Total uint `json:"total"`
Start uint `json:"start"`
Display uint `json:"display"`
Items []ApiResponseItem `json:"items"`
CafeId int `json:"cafeId"`
ArticelCount int `json:"articleCount"`
Query string `json:"query"`
Items []ApiResponseItem `json:"articleList"`
}
type ApiResponseItem struct {
Title string `json:"title"`
Link string `json:"link"`
Description string `json:"description"`
CafeName string `json:"cafename"`
ArticleId int `json:"articleId"`
Title string `json:"subject"`
ExtraInfo string `json:"summary"`
ThumbnailUrl string `json:"thumbnailImageUrl"`
ProductSale ApiResponseItemSale `json:"productSale"`
}
type ApiResponseItemSale struct {
SaleStatus string `json:"saleStatue"`
Cost string `json:"cost"`
}
......
package service
import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"joongna/config"
......@@ -13,36 +13,35 @@ import (
"strconv"
"strings"
"sync"
"time"
"github.com/PuerkitoBio/goquery"
"github.com/go-rod/rod"
"github.com/go-rod/rod/lib/launcher"
)
func GetItemByKeyword(keyword string) ([]model.Item, error) {
var items []model.Item
wg := sync.WaitGroup{}
itemsInfo, err := getItemsInfoByKeyword(keyword)
responseItems, err := getItemsInfoByKeyword(keyword)
if err != nil {
return nil, err
}
for _, itemInfo := range itemsInfo {
itemUrl := itemInfo.Link
if itemInfo.CafeName != "중고나라" {
continue
}
for _, responseItem := range responseItems {
wg.Add(1)
go func(itemUrl string) {
go func(responseItem model.ApiResponseItem) {
defer wg.Done()
item, err := crawlingNaverCafe(itemUrl)
if err != nil {
log.Fatal(err)
}
items = append(items, *item)
}(itemUrl)
item := model.Item{
Platform: "중고나라",
Name: responseItem.Title,
Price: priceStringToInt(responseItem.ProductSale.Cost),
ThumbnailUrl: responseItem.ThumbnailUrl,
ItemUrl: fmt.Sprintf("https://m.cafe.naver.com/ca-fe/web/cafes/10050146/articles/%d", responseItem.ArticleId),
ExtraInfo: responseItem.ExtraInfo,
}
items = append(items, item)
}(responseItem)
}
wg.Wait()
......@@ -50,8 +49,8 @@ func GetItemByKeyword(keyword string) ([]model.Item, error) {
}
func getItemsInfoByKeyword(keyword string) ([]model.ApiResponseItem, error) {
encText := url.QueryEscape("중고나라 " + keyword + " 판매중")
apiUrl := "https://openapi.naver.com/v1/search/cafearticle.json?query=" + encText + "&sort=sim"
encText := url.QueryEscape(keyword)
apiUrl := fmt.Sprintf("https://apis.naver.com/cafe-web/cafe-mobile/CafeMobileWebArticleSearchListV3?cafeId=10050146&query=%s&searchBy=0&sortBy=sim&page=1&perPage=10&adUnit=MW_CAFE_BOARD", encText)
req, err := http.NewRequest("GET", apiUrl, nil)
if err != nil {
......@@ -59,6 +58,8 @@ func getItemsInfoByKeyword(keyword string) ([]model.ApiResponseItem, error) {
}
req.Header.Add("X-Naver-Client-Id", config.Cfg.Secret.CLIENTID)
req.Header.Add("X-Naver-Client-Secret", config.Cfg.Secret.CLIENTSECRET)
req.Header.Add("Cookie", config.Cfg.Header.Cookie)
req.Header.Add("User-agent", config.Cfg.Header.UserAgent)
client := &http.Client{}
resp, err := client.Do(req)
......@@ -73,55 +74,26 @@ func getItemsInfoByKeyword(keyword string) ([]model.ApiResponseItem, error) {
}(resp.Body)
response, _ := ioutil.ReadAll(resp.Body)
var apiResponse model.ApiResponse
err = json.Unmarshal(response, &apiResponse)
var apiResult map[string]interface{}
err = json.Unmarshal(response, &apiResult)
if err != nil {
log.Fatal(err)
return nil, err
}
return apiResponse.Items, nil
}
func crawlingNaverCafe(cafeUrl string) (*model.Item, error) {
path, _ := launcher.LookPath()
u := launcher.New().Bin(path).MustLaunch()
browser := rod.New().ControlURL(u).MustConnect()
defer func(browser *rod.Browser) {
err := browser.Close()
if err != nil {
log.Fatal(err)
}
}(browser)
frame := browser.MustPage(cafeUrl).MustElement("iframe#cafe_main")
time.Sleep(time.Second * 2)
source := frame.MustFrame().MustHTML()
html, err := goquery.NewDocumentFromReader(bytes.NewReader([]byte(source)))
result := apiResult["message"].(map[string]interface{})["result"]
resultJson, err := json.Marshal(result)
if err != nil {
return nil, err
}
title := html.Find("h3.title_text").Text()
sold := html.Find("div.sold_area").Text()
price := priceStringToInt(html.Find(".ProductPrice").Text())
thumbnailUrl, _ := html.Find("div.product_thumb img").Attr("src")
extraInfo := html.Find(".se-module-text").Text()
title = strings.TrimSpace(title)
sold = strings.TrimSpace(sold)
thumbnailUrl = strings.TrimSpace(thumbnailUrl)
extraInfo = strings.TrimSpace(extraInfo)
item := model.Item{
Platform: "중고나라",
Name: title,
Price: price,
ThumbnailUrl: thumbnailUrl,
ItemUrl: cafeUrl,
ExtraInfo: extraInfo,
var apiResponse model.ApiResponse
err = json.Unmarshal(resultJson, &apiResponse)
if err != nil {
return nil, err
}
return &item, nil
return apiResponse.Items, nil
}
func priceStringToInt(priceString string) int {
......
#!/usr/bin/env bash
docker-compose down
docker image rm daangn-api-server
docker image rm joongna-api-server
docker image rm bunjang-api-server
docker image rm mamuri-db