Răsfoiți Sursa

first commit

X\choro 4 luni în urmă
comite
06ab782e1a

+ 137 - 0
config/constants.go

@@ -0,0 +1,137 @@
+package config
+
+/**
+ * structs
+ **/
+
+const (
+	LOCAL = "local"
+	DEV   = "dev"
+
+	// JSON 설정 파일
+	ENV_PATH             = "./env.json"
+	CONFIG_PATH_DATABASE = "./config/database.json"
+	CONFIG_PATH_MOVIE    = "./config/movie.json"
+	CONFIG_PATH_G2A      = "./config/g2a.json"
+
+	// 로그 파일 경로
+	ERROR_LOG_PATH_KOBIS = "./log/kobis/error.txt"
+	ERROR_LOG_PATH_G2A   = "./log/g2a/error.txt"
+	LAST_PAGE_PATH_KOBIS = "./log/kobis/page.txt"
+
+	// DB 목록
+	DB_MOVIEW  = "moview"
+	DB_CRAWLER = "crawler"
+	DB_PLAYR   = "playr"
+
+	// 데이터 단위
+	BYTE     = 1.0
+	KILOBYTE = 1024 * BYTE
+	MEGABYTE = 1024 * KILOBYTE
+	GIGABYTE = 1024 * MEGABYTE
+	TERABYTE = 1024 * GIGABYTE
+
+	// GeneralLog Action 구분값
+	GL_ACTION_WRITE  = 1
+	GL_ACTION_MODIFY = 2
+	GL_ACTION_DELETE = 3
+	GL_ACTION_SELECT = 4
+
+	// 영화진흥윈원회
+	KOBIS_DOMAIN = "www.kobis.or.kr"
+	KOBIS_HOST   = "https://www.kobis.or.kr"
+
+	// 영화 목록 API
+	MOVIE_LIST = "http://www.kobis.or.kr/kobisopenapi/webservice/rest/movie/searchMovieList.json"
+
+	// 영화 상세 정보 API
+	MOVIE_INFO = "http://www.kobis.or.kr/kobisopenapi/webservice/rest/movie/searchMovieInfo.json"
+
+	// 영화 상세 정보 Page
+	MOVIE_DETAIL = "https://www.kobis.or.kr/kobis/business/mast/mvie/searchMovieDtl.do"
+
+	// 영화 일별 박스오피스
+	MOVIE_DAILY_BOX_OFFICE_LIST = "http://www.kobis.or.kr/kobisopenapi/webservice/rest/boxoffice/searchDailyBoxOfficeList.json"
+
+	// 영화 주간/주말 박스오피스
+	MOVIE_WEEK_BOX_OFFICE_LIST = "http://www.kobis.or.kr/kobisopenapi/webservice/rest/boxoffice/searchWeeklyBoxOfficeList.json"
+
+	// 영화 박스오피스 (통계)
+	MOVIE_BOX_OFFICE_STATS = "http://api.kcisa.kr/openapi/service/rest/meta5/getKFCC0502"
+
+	// 암호화, 복호화 Key, IV
+	ENCRYPT_KEY = "@@20120726CHrong"
+	ENCRYPT_IV  = "6c2df00f75470f67"
+)
+
+// 환경설정값
+var (
+	EnvKey = "DEVELOPER_ENV" // 윈도우, 리눅스에 선언된 환경변수 이름
+
+	Env   *Environment
+	DB    *DBConfig
+	Movie *MovieConfig
+	G2A   *G2AConfig
+)
+
+type Environment struct {
+	DeveloperEnv string `json:"developerEnv"`
+	IdentityKey  string `json:"identityKey"`
+	IsDebug      bool   `json:"isDebug"`
+	IsLive       bool   `json:"isLive"`
+	APP          struct {
+		Port           string `json:"port"`
+		SecretKey      string `json:"secretKey"`
+		ReadTimeout    int    `json:"readTimeout"`
+		WriteTimeout   int    `json:"writeTimeout"`
+		MaxHeaderBytes int    `json:"maxHeaderBytes"`
+	} `json:"app"`
+}
+
+type DBConfig struct {
+	MaxLifetime int `json:"maxLifetime"`
+	MaxIdleTime int `json:"maxIdleTime"`
+	MaxIdleConn int `json:"maxIdleConn"`
+	MaxOpenConn int `json:"maxOpenConn"`
+	DBServer
+}
+
+type DBServer struct {
+	Local DBAccount `json:"local"`
+	Dev   DBAccount `json:"dev"`
+}
+
+type DBAccount struct {
+	Driver   string `json:"driver"`
+	User     string `json:"user"`
+	Password string `json:"password"`
+	Address  string `json:"address"`
+	Name     string `json:"name"`
+}
+
+// 영화진흥위원회 API
+type MovieConfig struct {
+	Kobis struct {
+		ApiKey_1 string `json:"apiKey_1"`
+		ApiKey_2 string `json:"apiKey_2"`
+		ApiKey_3 string `json:"apiKey_3"`
+	} `json:"kobis"`
+}
+
+// G2A.com API
+type G2AConfig struct {
+	Import  G2ACredential `json:"import"`
+	Export  G2ACredential `json:"export"`
+	Sandbox G2ACredential `json:"sandbox"`
+}
+
+type G2ACredential struct {
+	API    string `json:"api"`
+	Email  string `json:"email"`
+	Client struct {
+		ID     string `json:"id"`
+		Secret string `json:"secret"`
+	} `json:"client"`
+	Hash   string `json:"hash"`
+	IsTest int    `json:"isTest"`
+}

+ 20 - 0
config/database.json

@@ -0,0 +1,20 @@
+{
+    "maxLifetime": 3600,
+    "maxIdleTime": 3600,
+    "maxIdleConn": 20,
+    "maxOpenConn": 30,
+    "local": {
+        "driver": "mysql",
+        "user": "admin",
+        "password": "bluescreen!!",
+        "address": "192.168.0.100:3306",
+        "name": "movie"
+    },
+    "dev": {
+        "driver": "mysql",
+        "user": "admin",
+        "password": "bluescreen!!",
+        "address": "192.168.0.100:3306",
+        "name": "movie"
+    }
+}

+ 32 - 0
config/g2a.json

@@ -0,0 +1,32 @@
+{
+	"import": {
+		"api": "https://api.g2a.com/v3",
+		"email": "playedcompany@gmail.com",
+		"client": {
+			"id": "YCPMFlEMjJYrbNJq",
+			"secret": "DrQmvzAJYqlhWJKvvLLoLNCVKgNlMcOY",
+			"hash": ""
+		},
+		"isTest": 0
+	},
+	"export": {
+		"api": "https://api.g2a.com/v1",
+		"email": "playedcompany@gmail.com",
+		"client": {
+			"id": "YCPMFlEMjJYrbNJq",
+			"secret": "rQXylqJSUkWcrTwtTvXjGeqWKLhuWzNL",
+			"hash": ""
+		},
+		"isTest": 0
+	},
+	"sandbox": {
+		"api": "https://sandboxapi.g2a.com/v1",
+		"email": "sandboxapitest@g2a.com",
+		"client": {
+			"id": "qdaiciDiyMaTjxMt",
+			"secret": "b0d293f6-e1d2-4629-8264-fd63b5af3207b0d293f6-e1d2-4629-8264-fd63b5af3207",
+			"hash": ""
+		},
+		"isTest": 1
+	}
+}

+ 7 - 0
config/movie.json

@@ -0,0 +1,7 @@
+{
+	"kobis": {
+		"apiKey_1": "d470c8a1fa881cd5945b747a6dfadd53",
+		"apiKey_2": "52c8406a33ffe497f041868b2c3be552",
+		"apiKey_3": "f5eef3421c602c6cb7ea224104795888"
+	}
+}

+ 533 - 0
controller/cron.go

@@ -0,0 +1,533 @@
+package controller
+
+import (
+	"crawler/config"
+	"crawler/model"
+	"crawler/service"
+	"crawler/utility"
+	"fmt"
+	"github.com/gin-gonic/gin"
+	"github.com/gocolly/colly"
+	"log"
+	"net"
+	"net/http"
+	"os"
+	"strconv"
+	"time"
+)
+
+type CronController interface {
+	List(c *gin.Context)
+	Info(c *gin.Context)
+	Detail(c *gin.Context)
+	GetKey(c *gin.Context) string
+	SetLastPage(page int)
+	GetLastPage() int
+}
+
+type Cron struct {
+	MovieListModel   model.MovieListModel
+	MovieInfoModel   model.MovieInfoModel
+	MovieDetailModel model.MovieDetailModel
+	MovieStatsModel  model.MovieStatsModel
+	Kobis            model.Kobis
+	Rest             service.Rest
+}
+
+/**
+ * 영화진흥위원회 영화 목록
+ */
+func (this *Cron) List(c *gin.Context) {
+	var (
+		start                                 = time.Now()
+		page                                  = GetLastPage()
+		perPage                               = 100
+		total, errors, insertRows, updateRows = 0, 0, 0, 0
+		output                                = func(n ...int) string {
+			s := fmt.Sprintf("Total : %d\n", n[0])
+			s += fmt.Sprintf("Error: %d\n", n[1])
+			s += fmt.Sprintf("InsertRows: %d\n", n[2])
+			s += fmt.Sprintf("UpdateRows: %d\n", n[3])
+			s += fmt.Sprintf("소요시간: %f초\n", time.Since(start).Seconds())
+			return s
+		}
+		key = GetKey(c)
+	)
+
+	//defer func() {
+	//	if r := recover(); r != nil {
+	//		msg := fmt.Sprintf("[영화 목록 수집 오류 발생]\n")
+	//		msg += output(total, errors, insertRows, updateRows)
+	//		utility.SendMessage(msg)
+	//	}
+	//}()
+
+	for {
+
+		var (
+			req                    = this.MovieListModel.SearchMovieListParams
+			insertData, updateData []model.MovieListInfo
+		)
+
+		req.Key = key
+		req.CurPage = page
+		req.ItemPerPage = perPage
+		data, err := this.Kobis.MovieListAPI(req)
+
+		if err != nil {
+			c.JSON(http.StatusBadRequest, err.Error())
+			break
+		}
+
+		// 더 이상 값이 없다면 중지
+		if data.MovieListResult.TotCnt <= 0 {
+			errors++
+			break
+		}
+
+		// 입력할 값과 수정할 값 구분
+		for _, row := range data.MovieListResult.MovieList {
+			if this.MovieListModel.IsExists(row.MovieCd) == true {
+				updateData = append(updateData, row)
+				updateRows++
+			} else {
+				insertData = append(insertData, row)
+				insertRows++
+			}
+			total++
+		}
+
+		if insertRows > 0 {
+			if err = this.MovieListModel.Insert(insertData); err != nil {
+				errors++
+			}
+		}
+		if updateRows > 0 {
+			if err = this.MovieListModel.Update(updateData); err != nil {
+				errors++
+			}
+		}
+
+		fmt.Println(output(total, errors, insertRows, updateRows))
+
+		SetLastPage(page)
+		page++
+	}
+
+	//msg := "[영화 목록 수집 종료]\n"
+	//msg += output(total, errors, insertRows, updateRows)
+	//utility.SendMessage(msg)
+
+	c.JSON(http.StatusOK, gin.H{
+		"total":      total,
+		"errors":     errors,
+		"page":       page,
+		"perPage":    perPage,
+		"insertRows": insertRows,
+		"updateRows": updateRows,
+	})
+}
+
+/**
+ * 영화진흥위원회 영화 기본 정보
+ */
+func (this *Cron) Info(c *gin.Context) {
+	var (
+		start                                 = time.Now()
+		codes                                 = this.MovieListModel.MovieInfoExcludeCodes()
+		total, errors, insertRows, updateRows = 0, 0, 0, 0
+		output                                = func(n ...int) string {
+			s := fmt.Sprintf("Total : %d\n", n[0])
+			s += fmt.Sprintf("Error: %d\n", n[1])
+			s += fmt.Sprintf("InsertRows: %d\n", n[2])
+			s += fmt.Sprintf("UpdateRows: %d\n", n[3])
+			s += fmt.Sprintf("소요시간: %f초\n", time.Since(start).Seconds())
+			return s
+		}
+		key = GetKey(c)
+	)
+
+	//defer func() {
+	//	if r := recover(); r != nil {
+	//		msg := fmt.Sprintf("[영화 기본 정보 수집 오류 발생]\n")
+	//		msg += output(total, errors, insertRows, updateRows)
+	//		utility.SendMessage(msg)
+	//	}
+	//}()
+
+	for _, movieCd := range codes {
+		var (
+			req = this.MovieInfoModel.SearchMovieInfoParams
+		)
+
+		req.Key = key
+		req.MovieCd = movieCd
+		data, err := this.Kobis.MovieInfoAPI(req)
+
+		if err != nil {
+			c.JSON(http.StatusBadRequest, err.Error())
+			break
+		}
+
+		row := data.MovieInfoResult.MovieInfo
+
+		if row.MovieCd == "" {
+			errors++
+			break
+		}
+
+		if this.MovieInfoModel.IsExists(row.MovieCd) == true {
+			if err = this.MovieInfoModel.Update(row); err == nil {
+				updateRows++
+			} else {
+				errors++
+			}
+		} else {
+			if err = this.MovieInfoModel.Insert(row); err == nil {
+				insertRows++
+			} else {
+				errors++
+			}
+		}
+
+		fmt.Println(output(total, errors, insertRows, updateRows))
+
+		total++
+	}
+
+	//msg := "[영화 기본 정보 수집 종료]\n"
+	//msg += output(total, errors, insertRows, updateRows)
+	//utility.SendMessage(msg)
+
+	c.JSON(http.StatusOK, gin.H{
+		"total":      total,
+		"error":      errors,
+		"insertRows": insertRows,
+		"updateRows": updateRows,
+	})
+}
+
+/*
+ * 호출 순서
+ * OnRequest -> OnError -> OnResponseHeaders -> OnResponse -> OnHTML -> OnXML -> OnScraped
+ */
+func (this *Cron) Detail(c *gin.Context) {
+	var (
+		start                                                  = time.Now()
+		codes                                                  = this.MovieListModel.MovieDetailExcludeCodes()
+		total, scraped, errors, insertRows, updateRows, target = 0, 0, 0, 0, 0, len(codes)
+		output                                                 = func(n ...int) string {
+			s := fmt.Sprintf("Total : %d\n", n[0])
+			s += fmt.Sprintf("Scraped: %d\n", n[1])
+			s += fmt.Sprintf("Error: %d\n", n[2])
+			s += fmt.Sprintf("InsertRows: %d\n", n[3])
+			s += fmt.Sprintf("UpdateRows: %d\n", n[4])
+			s += fmt.Sprintf("Target : %d\n", n[5])
+			s += fmt.Sprintf("소요시간: %f초\n", time.Since(start).Seconds())
+			return s
+		}
+		c1 = colly.NewCollector(
+			colly.AllowedDomains(config.KOBIS_DOMAIN),
+			colly.IgnoreRobotsTxt(),
+			colly.Async(false),
+		)
+	)
+
+	c1.WithTransport(&http.Transport{
+		DialContext: (&net.Dialer{
+			Timeout:   30 * time.Second,
+			KeepAlive: 30 * time.Second,
+		}).DialContext,
+		MaxIdleConns:          0,
+		MaxIdleConnsPerHost:   100,
+		IdleConnTimeout:       30 * time.Second,
+		TLSHandshakeTimeout:   30 * time.Second,
+		ExpectContinueTimeout: 30 * time.Second,
+		DisableCompression:    false,
+	})
+
+	//var c2 = c1.Clone()
+
+	c1.OnRequest(func(r *colly.Request) {
+		r.Headers.Set("User-Agent", utility.RandomString())
+		r.Headers.Set("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8")
+	})
+
+	c1.OnError(func(_ *colly.Response, err error) {
+		log.Printf("Error(c1) : %s\n", err.Error())
+		errors++
+	})
+
+	c1.OnScraped(func(r *colly.Response) {
+		scraped++
+	})
+
+	/*
+	 관객 수, 누적 매출액 조회
+	*/
+	//c2.OnRequest(func(r *colly.Request) {
+	//	r.Headers.Set("User-Agent", utility.RandomString())
+	//	r.Headers.Set("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8")
+	//})
+	//
+	//c2.OnError(func(_ *colly.Response, err error) {
+	//	log.Printf("Error(c2) : %s\n", err.Error())
+	//	errors++
+	//})
+	//
+	//c2.OnScraped(func(r *colly.Response) {
+	//	scraped++
+	//})
+
+	// 복구처리
+	defer func() {
+		if e := recover(); e != nil {
+			//msg := "[영화 상세 정보 수집 오류 발생]\n"
+			//msg += output(total, scraped, errors, insertRows, updateRows, len(codes))
+			//utility.SendMessage(msg)
+		}
+	}()
+
+	for i, movieCd := range codes {
+		if movieCd == "" {
+			continue
+		}
+		movieDetail := this.MovieDetailModel.MovieDetail
+		movieDetail.MovieCd = movieCd
+
+		c1.OnHTML(".item_tab.basic", func(e *colly.HTMLElement) {
+			var host = config.KOBIS_HOST
+			movieDetail.MainImg = e.ChildAttr("a.fl.thumb", "href")
+			if movieDetail.MainImg != "" && movieDetail.MainImg != "#" {
+				movieDetail.MainImg = host + movieDetail.MainImg
+			}
+			movieDetail.ThumbImg = e.ChildAttr("a.fl.thumb > img", "src")
+			if movieDetail.ThumbImg != "" && movieDetail.ThumbImg != "#" {
+				movieDetail.ThumbImg = host + movieDetail.ThumbImg
+			}
+			movieDetail.Synopsis = e.ChildText("div.info.info2 p.desc_info")
+
+			e.ForEach("div#post > input", func(_ int, ee *colly.HTMLElement) {
+				movieDetail.Poster = append(movieDetail.Poster, model.Poster{
+					Thumb:  host + ee.Attr("thn_img"),
+					Origin: host + ee.Attr("img"),
+				})
+			})
+			e.ForEach("div#stl > input", func(_ int, ee *colly.HTMLElement) {
+				movieDetail.StillCut = append(movieDetail.StillCut, model.StillCut{
+					Thumb:  host + ee.Attr("thn_img"),
+					Origin: host + ee.Attr("img"),
+				})
+			})
+		})
+
+		//c2.OnHTML("body", func(e *colly.HTMLElement) {
+		//	var (
+		//		tr      = e.DOM.Find(".info").Eq(0).Find("table tbody tr").Eq(1)
+		//		saleAcc = utility.RemoveSpecialChar(strings.Replace(tr.Find("td").Eq(2).Text(), "(100%)", "", 1))
+		//		audiAcc = utility.RemoveSpecialChar(strings.Replace(tr.Find("td").Eq(3).Text(), "(100%)", "", 1))
+		//	)
+		//	SaleAcc, _ := strconv.Atoi(saleAcc)
+		//	AudiAcc, _ := strconv.Atoi(audiAcc)
+		//
+		//	movieDetail.SaleAcc = SaleAcc
+		//	movieDetail.AudiAcc = AudiAcc
+		//})
+
+		err := c1.Post(config.MOVIE_DETAIL, map[string]string{
+			"code":       movieCd,
+			"sType":      "",
+			"titleYN":    "Y",
+			"etcParam":   "",
+			"isOuterReq": "false",
+		})
+		if err != nil {
+			errors++
+			continue
+		}
+
+		//if this.Rest.Check(err) {
+		//	errors++
+		//	continue
+		//}
+
+		//err = c2.Post(config.MOVIE_DETAIL, map[string]string{
+		//	"code":  movieCd,
+		//	"sType": "stat",
+		//})
+		//if err != nil {
+		//	errors++
+		//	continue
+		//}
+		//
+		//if this.Rest.Check(err) {
+		//	errors++
+		//	continue
+		//}
+
+		if this.MovieDetailModel.IsExists(movieCd) == true {
+			if err = this.MovieDetailModel.Update(movieDetail); err == nil {
+				updateRows++
+			} else {
+				errors++
+			}
+		} else {
+			if err = this.MovieDetailModel.Insert(movieDetail); err == nil {
+				insertRows++
+			} else {
+				errors++
+			}
+		}
+
+		fmt.Println(output(total, scraped, errors, insertRows, updateRows, target))
+
+		codes[i] = ""
+		target--
+		total++
+	}
+
+	//msg := "[영화 상세 정보 수집 종료]\n"
+	//msg += output(total, scraped, insertRows, updateRows, errors)
+	//utility.SendMessage(msg)
+
+	c.JSON(http.StatusOK, gin.H{
+		"total":      total,
+		"insertRows": insertRows,
+		"updateRows": updateRows,
+	})
+}
+
+/**
+ * 영화진흥위원회 박스오피스 (통계 조회)
+ */
+/*
+func (this *Cron) Stats(c *gin.Context) {
+	var (
+		start                                       = time.Now()
+		total, errors, insertRows, updateRows, page = 0, 0, 0, 0, 1
+		output                                      = func(n ...int) string {
+			s := fmt.Sprintf("Total : %d\n", n[0])
+			s += fmt.Sprintf("Error: %d\n", n[1])
+			s += fmt.Sprintf("InsertRows: %d\n", n[2])
+			s += fmt.Sprintf("UpdateRows: %d\n", n[3])
+			s += fmt.Sprintf("Page: %d\n", n[4])
+			s += fmt.Sprintf("소요시간: %f초\n", time.Since(start).Seconds())
+			return s
+		}
+	)
+
+	req := this.MovieStatsModel.SearchBoxOfficeParams
+	req.ServiceKey = config.Env.Movie.Kcisa.BoxOfficeKey
+	req.NumOfRows = 2000
+	req.PageNo = 1
+
+	for {
+
+		var (
+			insertData = make([]model.BoxOfficeInfo, 0)
+			updateData = make([]model.BoxOfficeInfo, 0)
+		)
+		req.PageNo = page
+		data, err := this.Kobis.MovieBoxOfficeAPI(req)
+
+		if err != nil {
+			c.JSON(http.StatusBadRequest, err.Error())
+			break
+		}
+
+		list := data.Response.Body.Items.Item
+
+		if len(list) <= 0 {
+			break
+		}
+
+		for _, row := range list {
+			query, err := url.ParseQuery(row.Url)
+			if err != nil {
+				errors++
+				continue
+			}
+			movieCd := query.Get("dtCd")
+
+			if this.MovieStatsModel.IsExists(movieCd) == true {
+				updateData = append(updateData, row)
+				updateRows++
+			} else {
+				insertData = append(insertData, row)
+				insertRows++
+			}
+		}
+
+		if insertRows > 0 {
+			if err = this.MovieStatsModel.Insert(insertData); err != nil {
+				errors++
+			}
+		}
+		if updateRows > 0 {
+			if err = this.MovieStatsModel.Update(updateData); err != nil {
+				errors++
+			}
+		}
+
+		fmt.Println(output(total, errors, insertRows, updateRows, page))
+
+		page++
+		total++
+	}
+
+	msg := "[영화 통계 정보 수집 종료]\n"
+	msg += output(total, errors, insertRows, updateRows, page)
+	utility.SendMessage(msg)
+
+	c.JSON(http.StatusOK, gin.H{
+		"total":      total,
+		"error":      errors,
+		"insertRows": insertRows,
+		"updateRows": updateRows,
+		"page":       page,
+	})
+}
+*/
+
+func GetKey(c *gin.Context) string {
+	if c.Query("key") == "1" {
+		return config.Movie.Kobis.ApiKey_1
+	} else if c.Query("key") == "2" {
+		return config.Movie.Kobis.ApiKey_2
+	} else {
+		return "f5eef3421c602c6cb7ea224104795888"
+	}
+}
+
+// 마지막 호출 Page 저장
+func SetLastPage(page int) {
+	data, err := os.Create(config.LAST_PAGE_PATH_KOBIS)
+	if err != nil {
+		fmt.Println(err)
+	}
+	defer func() {
+		if data.Close() != nil {
+			fmt.Println(err)
+		}
+	}()
+
+	_, _ = data.WriteString(strconv.FormatInt(int64(page), 10))
+
+	fmt.Printf("Set last page: %d\n", page)
+}
+
+// 마지막 호출 Page 조회
+func GetLastPage() int {
+	byte, err := os.ReadFile(config.LAST_PAGE_PATH_KOBIS)
+	if err != nil {
+		fmt.Println(err)
+	}
+
+	page, _ := strconv.Atoi(string(byte))
+
+	if page == 0 {
+		page = 1
+	}
+
+	return page
+}

+ 491 - 0
controller/g2a.go

@@ -0,0 +1,491 @@
+package controller
+
+import (
+	"bytes"
+	"crawler/config"
+	"crawler/model"
+	"crawler/utility"
+	"crypto/sha256"
+	"encoding/base64"
+	"encoding/json"
+	"fmt"
+	"github.com/gin-gonic/gin"
+	"io"
+	"log"
+	"net/http"
+	"reflect"
+	"strconv"
+	"strings"
+	"time"
+)
+
+type G2AInterface interface {
+	Init()
+	Products(c *gin.Context)
+	Order(c *gin.Context)
+}
+
+type G2A struct {
+	ApiURL          string
+	IsTest          bool
+	Credentials     config.G2ACredential
+	G2AProductModel model.G2AProductModel
+	G2AReportModel  model.G2AReportModel
+	G2AOrderModel   model.G2AOrderModel
+	G2AErrorModel   model.G2AErrorModel
+	G2AErrorResponse
+	OrderDetail model.OrderDetailModel
+	ShopConfig  model.ShopConfigModel
+}
+
+// G2A 오류
+type G2AErrorResponse struct {
+	Status  any
+	Message any
+	Code    any
+}
+
+// G2A 환경설정 조회
+func (this *G2A) Init() {
+	this.IsTest = this.ShopConfig.G2AIsTest()
+
+	if this.IsTest == true {
+		this.Credentials = config.G2A.Sandbox
+	} else {
+		switch config.Env.DeveloperEnv {
+		case config.LOCAL:
+			this.Credentials = config.G2A.Sandbox
+		case config.DEV:
+			this.Credentials = config.G2A.Export
+		default:
+			log.Panic("G2A 설정이 잘못되었습니다.")
+		}
+	}
+
+	var checksum = sha256.Sum256([]byte(fmt.Sprintf("%s%s%s",
+		this.Credentials.Client.ID, this.Credentials.Email, this.Credentials.Client.Secret,
+	)))
+
+	this.Credentials.Hash = fmt.Sprintf("%x", checksum[:])
+}
+
+// G2A 상품 조회
+func (this *G2A) Products(c *gin.Context) {
+	this.Init()
+
+	fmt.Println("G2A 상품 수집을 시작합니다.")
+
+	var (
+		result                             = this.G2AProductModel.G2AProductResult
+		total, insert, update, error, page = 0, 0, 0, 0, 1
+		startTime                          = time.Now()
+		endTime                            float64
+		output                             = func(msg string) string {
+			s := fmt.Sprintf("Total : %d\n", total)
+			s += fmt.Sprintf("Error: %d\n", error)
+			s += fmt.Sprintf("Insert: %d\n", insert)
+			s += fmt.Sprintf("Update: %d\n", update)
+			s += fmt.Sprintf("Page: %d\n", page)
+			s += fmt.Sprintf("소요시간: %f초\n", endTime)
+			return msg + "\n" + s
+		}
+	)
+
+	for {
+
+		this.ApiURL = fmt.Sprintf("%s/products?page=%d", this.Credentials.API, page)
+		receivedDTO := this.HttpGetRequest()
+
+		byteData, _ := json.Marshal(receivedDTO)
+		err := json.Unmarshal(byteData, &result)
+		utility.Check(err, config.ERROR_LOG_PATH_G2A)
+
+		// 상품 수집
+		if len(result.Docs) > 0 {
+			for i, product := range result.Docs {
+				if this.G2AProductModel.IsExists(product.ID) {
+					update++
+				} else {
+					insert++
+				}
+
+				if this.IsTest {
+					result.Docs[i].IsTest = 1
+				} else {
+					result.Docs[i].IsTest = 0
+				}
+			}
+
+			err = this.G2AProductModel.Insert(result.Docs)
+			if err != nil {
+				error++
+				utility.SendMessageToG2AError(output("[G2A 상품수집 중 오류]"))
+				utility.Check(err, config.ERROR_LOG_PATH_G2A)
+				break
+			}
+
+			total += len(result.Docs)
+			page++
+			fmt.Printf("G2A 상품 수집 중... / total: %d, page: %d\n", total, page)
+		} else {
+			endTime = time.Since(startTime).Seconds()
+			fmt.Printf("G2A 상품 수집 종료! / total: %d, page: %d\n", total, page)
+			break
+		}
+	}
+
+	// 통계 저장
+	this.G2AReportModel.Insert(model.G2AReport{
+		TotalCnt:  total,
+		InsertCnt: insert,
+		UpdateCnt: update,
+		ProcessAt: endTime,
+		LastPage:  page,
+	})
+
+	// 텔레그램 알림
+	utility.SendMessageToG2AProduct(output("[G2A 상품수집 완료]"))
+
+	c.JSON(http.StatusOK, gin.H{
+		"total":  total,
+		"insert": insert,
+		"update": update,
+		"page":   page,
+	})
+}
+
+// G2A 품절 확인 후 상품 정보 갱신
+func (this *G2A) CheckOutOfStock(c *gin.Context) {
+	this.Init()
+
+	fmt.Println("G2A 상품 품절 확인")
+
+	var (
+		result    = this.G2AProductModel.G2AProductResult
+		productID = c.Query("id")
+		inStock   = "yes"
+	)
+
+	if productID == "" {
+		c.JSON(http.StatusOK, gin.H{
+			"inStock": "no",
+		})
+		return
+	}
+
+	this.ApiURL = fmt.Sprintf("%s/products?id=%s", this.Credentials.API, productID)
+	receivedDTO := this.HttpGetRequest()
+
+	byteData, _ := json.Marshal(receivedDTO)
+	err := json.Unmarshal(byteData, &result)
+	utility.Check(err, config.ERROR_LOG_PATH_G2A)
+
+	// 상품 정보 확인
+	if len(result.Docs) > 0 {
+		for _, product := range result.Docs {
+			// 조회된 상품 정보를 한번 갱신한다.
+			err = this.G2AProductModel.Update(product)
+			utility.Check(err, config.ERROR_LOG_PATH_G2A)
+
+			if product.Qty <= 0 { // 재고가 0 이하면 품절이다.
+				inStock = "no"
+				break
+			}
+		}
+	}
+
+	c.JSON(http.StatusOK, gin.H{
+		"inStock": inStock,
+	})
+}
+
+// G2A 주문
+// Add an Order -> Pay for an order
+func (this *G2A) Order(c *gin.Context) {
+	var requestKey = c.Query("requestKey")
+	if requestKey == "" {
+		c.JSON(http.StatusForbidden, G2AErrorResponse{
+			Code:    http.StatusForbidden,
+			Message: "잘못된 요청입니다.",
+		})
+		return
+	}
+
+	// base64_decode 처리
+	rawRequestKey, err := base64.URLEncoding.DecodeString(requestKey)
+	utility.Check(err, config.ERROR_LOG_PATH_G2A)
+	requestKey = string(rawRequestKey)
+
+	var (
+		key = utility.MakeMD5(config.ENCRYPT_KEY)
+		iv  = config.ENCRYPT_IV
+	)
+
+	// 복호화
+	decryptCode, err := utility.AesDecrypt(requestKey, key, iv)
+	utility.Check(err, config.ERROR_LOG_PATH_G2A)
+
+	// 이미 주문한 주문건이 있는지 확인
+	if this.G2AOrderModel.IsExists(decryptCode) {
+		c.JSON(http.StatusOK, gin.H{
+			"result": "OK",
+		})
+		return
+	}
+
+	id := strings.Split(decryptCode, "/")
+	var (
+		orderID, _       = strconv.Atoi(id[0])
+		orderDetailID, _ = strconv.Atoi(id[1])
+		productID        = id[2]
+	)
+
+	if this.OrderDetail.IsExists(orderID, orderDetailID) == false {
+		c.JSON(http.StatusNotFound, G2AErrorResponse{
+			Code:    http.StatusNotFound,
+			Message: "비 정상적인 주문서 입니다.",
+		})
+		return
+	}
+
+	//orderDetail, err := this.OrderDetail.Info(orderID, orderDetailID)
+	//utility.Check(err, config.ERROR_LOG_PATH_G2A)
+
+	tmpProductID, _ := strconv.Atoi(productID)
+	G2AProductInfo, err := this.G2AProductModel.Info(tmpProductID)
+	utility.Check(err, config.ERROR_LOG_PATH_G2A)
+
+	var params = this.G2AOrderModel.G2AOrderParams
+	params.ProductID = productID
+	params.Currency = "KRW"
+	params.MaxPrice = G2AProductInfo.RetailMinPrice
+
+	if err := c.ShouldBind(&params); err != nil {
+		c.JSON(http.StatusBadRequest, G2AErrorResponse{
+			Code:    http.StatusBadRequest,
+			Message: err.Error(),
+		})
+		return
+	}
+
+	this.Init()
+
+	// 주문을 요청한다.
+	this.ApiURL = fmt.Sprintf("%s/order", this.Credentials.API)
+	var (
+		orderResult = this.G2AOrderModel.G2AOrderResult
+		receivedDTO = this.HttpPostRequest(params)
+		typeName    = reflect.TypeOf(receivedDTO).Name()
+	)
+
+	if typeName == "G2AErrorModel" {
+		c.JSON(http.StatusBadRequest, receivedDTO)
+		return
+	}
+
+	byteData, _ := json.Marshal(receivedDTO)
+	err = json.Unmarshal(byteData, &orderResult)
+	utility.Check(err, config.ERROR_LOG_PATH_G2A)
+
+	// 주문 번호를 결제 처리한다.
+	this.ApiURL = fmt.Sprintf("%s/order/pay/%s", this.Credentials.API, orderResult.OrderID)
+	var orderPayResult = this.G2AOrderModel.G2AOrderPayResult
+	receivedDTO = this.HttpPutRequest()
+	typeName = reflect.TypeOf(receivedDTO).Name()
+
+	if typeName == "G2AErrorModel" {
+		c.JSON(http.StatusBadRequest, receivedDTO)
+		return
+	}
+
+	byteData, _ = json.Marshal(receivedDTO)
+	err = json.Unmarshal(byteData, &orderPayResult)
+	utility.Check(err, config.ERROR_LOG_PATH_G2A)
+
+	// 결제 확인
+	if orderPayResult.Status == false {
+		c.JSON(http.StatusOK, gin.H{
+			"message": "결제에 실패하였습니다.",
+		})
+		return
+	}
+
+	// 결제 및 주문이 제대로 되었는지 확인한다.
+	this.ApiURL = fmt.Sprintf("%s/order/details/%s", this.Credentials.API, orderResult.OrderID)
+	var orderDetailResult = this.G2AOrderModel.G2AOrderDetailResult
+	receivedDTO = this.HttpGetRequest()
+	typeName = reflect.TypeOf(receivedDTO).Name()
+
+	if typeName == "G2AErrorModel" {
+		c.JSON(http.StatusBadRequest, receivedDTO)
+		return
+	}
+
+	byteData, _ = json.Marshal(receivedDTO)
+	err = json.Unmarshal(byteData, &orderDetailResult)
+	utility.Check(err, config.ERROR_LOG_PATH_G2A)
+
+	time.Sleep(1 * time.Second)
+
+	// 결제 및 주문이 제대로 되었는지 확인한다.
+	this.ApiURL = fmt.Sprintf("%s/order/key/%s", this.Credentials.API, orderResult.OrderID)
+	var orderKeyResult = this.G2AOrderModel.G2AOrderKeyResult
+	receivedDTO = this.HttpGetRequest()
+	typeName = reflect.TypeOf(receivedDTO).Name()
+
+	if typeName == "G2AErrorModel" {
+		c.JSON(http.StatusBadRequest, receivedDTO)
+		return
+	}
+
+	byteData, _ = json.Marshal(receivedDTO)
+	err = json.Unmarshal(byteData, &orderKeyResult)
+	utility.Check(err, config.ERROR_LOG_PATH_G2A)
+
+	// 주문 기록 저장
+	this.G2AOrderModel.Insert(model.G2AOrder{
+		RequestKey:           decryptCode,
+		RequestOrderID:       orderID,
+		RequestOrderDetailID: orderDetailID,
+		Status:               orderDetailResult.Status,
+		OrderID:              orderResult.OrderID,
+		ProductID:            params.ProductID,
+		Code:                 orderKeyResult.Key,
+		Price:                orderDetailResult.Price,
+		Currency:             orderDetailResult.Currency,
+		TransactionID:        orderPayResult.TransactionID,
+	})
+
+	// 텔레그램 알림
+	utility.SendMessageToG2AOrder(func() string {
+		msg := fmt.Sprintf("[G2A 주문완료]\n")
+		msg += fmt.Sprintf("Order ID : %d\n", orderID)
+		msg += fmt.Sprintf("Order Detail ID : %d\n", orderDetailID)
+		msg += fmt.Sprintf("Status : %s\n", orderDetailResult.Status)
+		msg += fmt.Sprintf("Code: %s\n", orderKeyResult.Key)
+		msg += fmt.Sprintf("Price: %f\n", orderDetailResult.Price)
+		msg += fmt.Sprintf("Currency: %s\n", orderDetailResult.Currency)
+		return msg
+	}())
+
+	c.JSON(http.StatusOK, gin.H{
+		"result": "OK",
+	})
+}
+
+// HTTP Get CALL
+func (this *G2A) HttpGetRequest() interface{} {
+	req, err := http.NewRequest("GET", this.ApiURL, nil)
+	utility.Check(err, config.ERROR_LOG_PATH_G2A)
+
+	req.Header.Set("Accept", "application/json")
+	req.Header.Add("Authorization", fmt.Sprintf("%s, %s", this.Credentials.Client.ID, this.Credentials.Hash))
+
+	client := &http.Client{
+		Timeout: 8 * time.Second,
+	}
+	res, err := client.Do(req)
+	utility.Check(err, config.ERROR_LOG_PATH_G2A)
+
+	defer res.Body.Close()
+
+	data, err := io.ReadAll(res.Body)
+	utility.Check(err, config.ERROR_LOG_PATH_G2A)
+
+	if res.StatusCode != 200 {
+		return this.SetG2AError(data, nil)
+	}
+
+	var receivedDTO interface{}
+	err = json.Unmarshal(data, &receivedDTO)
+	utility.Check(err, config.ERROR_LOG_PATH_G2A)
+
+	return receivedDTO
+}
+
+// HTTP Post CALL
+func (this *G2A) HttpPostRequest(params model.G2AOrderParams) interface{} {
+	payloadBytes, err := json.Marshal(params)
+	utility.Check(err, config.ERROR_LOG_PATH_G2A)
+
+	body := bytes.NewReader(payloadBytes)
+
+	req, err := http.NewRequest("POST", this.ApiURL, body)
+	utility.Check(err, config.ERROR_LOG_PATH_G2A)
+
+	req.Header.Set("Content-Type", "application/json")
+	req.Header.Add("Authorization", fmt.Sprintf("%s, %s", this.Credentials.Client.ID, this.Credentials.Hash))
+
+	client := &http.Client{
+		Timeout: 8 * time.Second,
+	}
+	res, err := client.Do(req)
+	utility.Check(err, config.ERROR_LOG_PATH_G2A)
+
+	defer res.Body.Close()
+
+	data, err := io.ReadAll(res.Body)
+	utility.Check(err, config.ERROR_LOG_PATH_G2A)
+
+	if res.StatusCode != 200 {
+		return this.SetG2AError(data, payloadBytes)
+	}
+
+	var receivedDTO interface{}
+	err = json.Unmarshal(data, &receivedDTO)
+	utility.Check(err, config.ERROR_LOG_PATH_G2A)
+
+	return receivedDTO
+}
+
+// HTTP Put CALL
+func (this *G2A) HttpPutRequest() interface{} {
+	req, err := http.NewRequest("PUT", this.ApiURL, nil)
+	utility.Check(err, config.ERROR_LOG_PATH_G2A)
+
+	req.Header.Set("Content-type", "application/json")
+	req.Header.Set("Content-Length", "0")
+	req.Header.Add("Authorization", fmt.Sprintf("%s, %s", this.Credentials.Client.ID, this.Credentials.Hash))
+
+	fmt.Println(fmt.Sprintf("%s, %s", this.Credentials.Client.ID, this.Credentials.Hash))
+
+	client := &http.Client{
+		Timeout: 8 * time.Second,
+	}
+	res, err := client.Do(req)
+	utility.Check(err, config.ERROR_LOG_PATH_G2A)
+	defer res.Body.Close()
+
+	data, err := io.ReadAll(res.Body)
+	utility.Check(err, config.ERROR_LOG_PATH_G2A)
+
+	if res.StatusCode != 200 {
+		return this.SetG2AError(data, nil)
+	}
+
+	var receivedDTO interface{}
+	err = json.Unmarshal(data, &receivedDTO)
+	utility.Check(err, config.ERROR_LOG_PATH_G2A)
+
+	return receivedDTO
+}
+
+// G2A 주문 API 오류 발생 처리
+func (this *G2A) SetG2AError(data []byte, params interface{}) model.G2AErrorModel {
+	var G2AError = this.G2AErrorModel
+	err := json.Unmarshal(data, &G2AError)
+	utility.Check(err, config.ERROR_LOG_PATH_G2A)
+
+	G2AError.URL = this.ApiURL
+	if params != nil {
+		G2AError.Params = string(params.([]byte))
+	}
+	G2AError.Save()
+
+	// 텔레그램 알림
+	if err != nil {
+		utility.SendMessageToG2AError(fmt.Sprintf("[G2A 주문 중 오류]\n%s", err.Error()))
+	}
+
+	return G2AError
+}

+ 568 - 0
controller/movie.go

@@ -0,0 +1,568 @@
+package controller
+
+import (
+	"crawler/config"
+	"crawler/model"
+	"crawler/service"
+	"crawler/utility"
+	"github.com/gin-gonic/gin"
+	"github.com/gocolly/colly"
+	"log"
+	"net"
+	"net/http"
+	"time"
+)
+
+type MovieController interface {
+	SearchDailyBoxOfficeList(c *gin.Context)
+	SearchWeeklylyBoxOfficeList(c *gin.Context)
+	SearchMovieList(c *gin.Context)
+}
+
+type Movie struct {
+	MovieListModel   model.MovieListModel
+	MovieDailyModel  model.MovieDailyModel
+	MovieWeeklyModel model.MovieWeeklyModel
+	MovieInfoModel   model.MovieInfoModel
+	MovieSearchModel model.MovieSearchModel
+	MovieDetailModel model.MovieDetailModel
+	Kobis            model.Kobis
+	Rest             service.Rest
+}
+
+// GET SearchDailyBoxOfficeList
+func (this *Movie) SearchDailyBoxOfficeList(c *gin.Context) {
+	var req = this.MovieDailyModel.SearchDailyBoxOfficeParams
+
+	if err := c.ShouldBind(&req); err != nil {
+		c.JSON(http.StatusBadRequest, err.Error())
+		return
+	}
+
+	var (
+		total, rows = 0, 0
+		list        = make([]model.MovieDailyTable, 0)
+	)
+
+	// 검색 변수 생성
+	params, err := this.MovieDailyModel.MakeParams(req)
+	if err != nil {
+		c.JSON(http.StatusBadRequest, err.Error())
+		return
+	}
+
+	// 검색값이 있는지 확인
+	ids, err := this.MovieSearchModel.SelectIDs(params)
+	if err != nil {
+		c.JSON(http.StatusBadRequest, err.Error())
+		return
+	}
+
+	// 검색 내역이 있음
+	if ids != "" {
+		// DB에서 먼저 조회해본다.
+		list, err = this.MovieDailyModel.List(ids)
+		if err != nil {
+			c.JSON(http.StatusBadRequest, err.Error())
+			return
+		}
+	}
+
+	// 목록이 없으면 API 호출
+	if len(list) <= 0 {
+		data, err := this.Kobis.MovieDailyBoxOfficeListAPI(req)
+		if err != nil {
+			c.JSON(http.StatusBadRequest, err.Error())
+		}
+
+		// 데이터 새로 입력
+		if len(data.BoxOfficeResult.DailyBoxOfficeList) > 0 {
+			if err = this.MovieDailyModel.Insert(data); err != nil {
+				c.JSON(http.StatusBadRequest, err.Error())
+				return
+			}
+
+			var movieCd []string
+			for _, row := range data.BoxOfficeResult.DailyBoxOfficeList {
+				movieCd = append(movieCd, row.MovieCd)
+			}
+
+			lastInsertIds, _ := this.MovieDailyModel.LastInsertIDs(movieCd, req.TargetDt)
+			if err != nil {
+				c.JSON(http.StatusBadRequest, err.Error())
+				return
+			}
+
+			// 검색 결과 저장
+			movieSearch := this.MovieSearchModel.MovieSearch
+			movieSearch.Params = params
+			movieSearch.IDs = lastInsertIds
+			if err = this.MovieSearchModel.Insert(movieSearch); err != nil {
+				c.JSON(http.StatusBadRequest, err.Error())
+				return
+			}
+
+			if lastInsertIds != "" {
+				list, err = this.MovieDailyModel.List(lastInsertIds)
+				if err != nil {
+					c.JSON(http.StatusBadRequest, err.Error())
+					return
+				}
+			}
+		}
+	}
+
+	// 상세 정보를 조회해서 저장한다.
+	for i, row := range list {
+		row.Detail, _ = this.FindMovieDetail(row.MovieCd)
+		list[i] = row
+		rows++
+	}
+
+	total = this.MovieDailyModel.Total()
+
+	c.JSON(http.StatusOK, gin.H{
+		"total": total,
+		"rows":  rows,
+		"list":  list,
+	})
+}
+
+// GET SearchWeeklylyBoxOfficeList
+func (this *Movie) SearchWeeklyBoxOfficeList(c *gin.Context) {
+	var req = this.MovieWeeklyModel.SearchWeeklyBoxOfficeParams
+
+	if err := c.ShouldBind(&req); err != nil {
+		c.JSON(http.StatusBadRequest, err.Error())
+		return
+	}
+
+	var (
+		total, rows = 0, 0
+		list        = make([]model.MovieWeeklyTable, 0)
+	)
+
+	// 검색 변수 생성
+	params, err := this.MovieWeeklyModel.MakeParams(req)
+	if err != nil {
+		c.JSON(http.StatusBadRequest, err.Error())
+		return
+	}
+
+	// 검색값이 있는지 확인
+	ids, err := this.MovieSearchModel.SelectIDs(params)
+	if err != nil {
+		c.JSON(http.StatusBadRequest, err.Error())
+		return
+	}
+
+	// 검색 내역이 있음
+	if ids != "" {
+		// DB에서 먼저 조회해본다.
+		list, err = this.MovieWeeklyModel.List(ids)
+		if err != nil {
+			c.JSON(http.StatusBadRequest, err.Error())
+			return
+		}
+	}
+
+	// 목록이 없으면 API 호출
+	if len(list) <= 0 {
+		data, err := this.Kobis.MovieWeeklyBoxOfficeListAPI(req)
+
+		if err != nil {
+			c.JSON(http.StatusBadRequest, err.Error())
+		}
+
+		// 데이터 새로 입력
+		if len(data.BoxOfficeResult.WeeklyBoxOfficeList) > 0 {
+			if err = this.MovieWeeklyModel.Insert(data); err != nil {
+				c.JSON(http.StatusBadRequest, err.Error())
+				return
+			}
+
+			var movieCd []string
+			for _, row := range data.BoxOfficeResult.WeeklyBoxOfficeList {
+				movieCd = append(movieCd, row.MovieCd)
+			}
+
+			lastInsertIds, _ := this.MovieWeeklyModel.LastInsertIDs(movieCd, req.TargetDt)
+			if err != nil {
+				c.JSON(http.StatusBadRequest, err.Error())
+				return
+			}
+
+			// 검색 결과 저장
+			movieSearch := this.MovieSearchModel.MovieSearch
+			movieSearch.Params = params
+			movieSearch.IDs = lastInsertIds
+			if err = this.MovieSearchModel.Insert(movieSearch); err != nil {
+				c.JSON(http.StatusBadRequest, err.Error())
+				return
+			}
+
+			if lastInsertIds != "" {
+				list, err = this.MovieWeeklyModel.List(lastInsertIds)
+				if err != nil {
+					c.JSON(http.StatusBadRequest, err.Error())
+					return
+				}
+			}
+		}
+	}
+
+	// 상세 정보를 조회해서 저장한다.
+	for i, row := range list {
+		row.Detail, _ = this.FindMovieDetail(row.MovieCd)
+		list[i] = row
+		rows++
+	}
+
+	total = this.MovieWeeklyModel.Total()
+
+	c.JSON(http.StatusOK, gin.H{
+		"total": total,
+		"rows":  rows,
+		"list":  list,
+	})
+}
+
+// GET SearchMovieList
+func (this *Movie) SearchMovieList(c *gin.Context) {
+	var req = this.MovieListModel.SearchMovieListParams
+
+	if err := c.ShouldBind(&req); err != nil {
+		c.JSON(http.StatusBadRequest, err.Error())
+		return
+	}
+
+	type result = struct {
+		MovieListInfo model.MovieListInfo     `json:"summary"`
+		Info          *model.MovieInfoTable   `json:"info"`
+		Detail        *model.MovieDetailTable `json:"detail"`
+	}
+
+	var (
+		total, rows = 0, 0
+		list        = make([]result, 0)
+	)
+
+	data, err := this.Kobis.MovieListAPI(req)
+	if err != nil {
+		c.JSON(http.StatusBadRequest, err.Error())
+	}
+
+	// 데이터 새로 입력
+	if data.MovieListResult.TotCnt > 0 {
+		if err = this.MovieListModel.Insert(data.MovieListResult.MovieList); err != nil {
+			c.JSON(http.StatusBadRequest, err.Error())
+			return
+		}
+	}
+
+	// 기본, 상세 정보를 조회해서 저장한다.
+	for _, row := range data.MovieListResult.MovieList {
+		info, _ := this.FindMovieInfo(req.Key, row.MovieCd)
+		detail, _ := this.FindMovieDetail(row.MovieCd)
+
+		list = append(list, result{
+			MovieListInfo: row,
+			Info:          info,
+			Detail:        detail,
+		})
+		rows++
+	}
+
+	total = this.MovieListModel.Total()
+
+	c.JSON(http.StatusOK, gin.H{
+		"total": total,
+		"rows":  rows,
+		"list":  list,
+	})
+}
+
+/**
+ * 영화 일별 박스오피스 정보 조회
+ * /movie/searchDailyInfo
+ */
+func (this *Movie) SearchDailyInfo(c *gin.Context) {
+	var req = struct {
+		Key     string `form:"key" binding:"required"`
+		DailyID int    `form:"dailyID" binding:"required"`
+	}{}
+
+	if err := c.ShouldBind(&req); err != nil {
+		c.JSON(http.StatusBadRequest, err.Error())
+		return
+	}
+
+	stats, err := this.MovieDailyModel.Info(req.DailyID)
+	if err != nil {
+		c.JSON(http.StatusBadRequest, err.Error())
+		return
+	}
+
+	if stats.DailyID == 0 {
+		c.JSON(http.StatusBadRequest, "잘못된 요청입니다.")
+		return
+	}
+
+	info, err := this.FindMovieInfo(req.Key, stats.MovieCd)
+	if err != nil {
+		c.JSON(http.StatusBadRequest, err.Error())
+		return
+	}
+
+	detail, err := this.FindMovieDetail(stats.MovieCd)
+	if err != nil {
+		c.JSON(http.StatusBadRequest, err.Error())
+		return
+	}
+
+	c.JSON(http.StatusOK, gin.H{
+		"stats":  stats,
+		"info":   info,
+		"detail": detail,
+	})
+}
+
+/**
+ * 영화 주간/주말 박스오피스 정보 조회
+ * /movie/searchWeeklyInfo
+ */
+func (this *Movie) SearchWeeklyInfo(c *gin.Context) {
+	var req = struct {
+		Key      string `form:"key" binding:"required"`
+		WeeklyID int    `form:"weeklyID" binding:"required"`
+	}{}
+
+	if err := c.ShouldBind(&req); err != nil {
+		c.JSON(http.StatusBadRequest, err.Error())
+		return
+	}
+
+	stats, err := this.MovieWeeklyModel.Info(req.WeeklyID)
+	if err != nil {
+		c.JSON(http.StatusBadRequest, err.Error())
+		return
+	}
+
+	if stats.WeeklyID == 0 {
+		c.JSON(http.StatusBadRequest, "잘못된 요청입니다.")
+		return
+	}
+
+	info, err := this.FindMovieInfo(req.Key, stats.MovieCd)
+	if err != nil {
+		c.JSON(http.StatusBadRequest, err.Error())
+		return
+	}
+
+	detail, err := this.FindMovieDetail(stats.MovieCd)
+	if err != nil {
+		c.JSON(http.StatusBadRequest, err.Error())
+		return
+	}
+
+	c.JSON(http.StatusOK, gin.H{
+		"stats":  stats,
+		"info":   info,
+		"detail": detail,
+	})
+}
+
+/**
+ * 영화 정보 조회
+ * /movie/searchMovieInfo
+ */
+func (this *Movie) SearchMovieInfo(c *gin.Context) {
+	var req = this.MovieInfoModel.SearchMovieInfoParams
+
+	if err := c.ShouldBind(&req); err != nil {
+		c.JSON(http.StatusBadRequest, err.Error())
+		return
+	}
+
+	info, err := this.FindMovieInfo(req.Key, req.MovieCd)
+	if err != nil {
+		c.JSON(http.StatusBadRequest, err.Error())
+		return
+	}
+
+	detail, err := this.FindMovieDetail(req.MovieCd)
+	if err != nil {
+		c.JSON(http.StatusBadRequest, err.Error())
+		return
+	}
+
+	c.JSON(http.StatusOK, gin.H{
+		"info":   info,
+		"detail": detail,
+	})
+}
+
+// 영화 기본 정보 조회
+func (this *Movie) FindMovieInfo(key, movieCd string) (*model.MovieInfoTable, error) {
+	if this.MovieListModel.IsExists(movieCd) == false || this.MovieInfoModel.IsExists(movieCd) == false {
+		req := this.MovieInfoModel.SearchMovieInfoParams
+		req.Key = key
+		req.MovieCd = movieCd
+		data, err := this.Kobis.MovieInfoAPI(req)
+		if err != nil {
+			return nil, err
+		}
+
+		err = this.MovieListModel.Replace(model.MovieListInfo{
+			MovieCd:     data.MovieInfoResult.MovieInfo.MovieCd,
+			MovieNm:     data.MovieInfoResult.MovieInfo.MovieNm,
+			MovieNmEn:   data.MovieInfoResult.MovieInfo.MovieNmEn,
+			PrdtYear:    data.MovieInfoResult.MovieInfo.PrdtYear,
+			OpenDt:      data.MovieInfoResult.MovieInfo.OpenDt,
+			TypeNm:      data.MovieInfoResult.MovieInfo.TypeNm,
+			PrdtStatNm:  data.MovieInfoResult.MovieInfo.PrdtStatNm,
+			NationAlt:   "",
+			GenreAlt:    "",
+			RepNationNm: "",
+			RepGenreNm:  "",
+			Directors:   nil,
+			Companys:    nil,
+		})
+
+		if err != nil {
+			return nil, err
+		}
+
+		// 영화 기본 정보 입력
+		this.MovieInfoModel.Insert(data.MovieInfoResult.MovieInfo)
+
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	movieInfo, err := this.MovieInfoModel.Info(movieCd)
+
+	return &movieInfo, err
+}
+
+// 영화 주요 정보 조회
+func (this *Movie) FindMovieDetail(movieCd string) (*model.MovieDetailTable, error) {
+	if this.MovieDetailModel.IsExists(movieCd) == false {
+		movieDetail := this.MovieDetailModel.MovieDetail
+		movieDetail.MovieCd = movieCd
+
+		// c2 := c1.Clone()
+
+		c1 := colly.NewCollector(
+			colly.AllowedDomains(config.KOBIS_DOMAIN),
+			colly.IgnoreRobotsTxt(),
+			colly.Async(false),
+		)
+
+		c1.WithTransport(&http.Transport{
+			DialContext: (&net.Dialer{
+				Timeout:   30 * time.Second,
+				KeepAlive: 30 * time.Second,
+			}).DialContext,
+			MaxIdleConns:          0,
+			IdleConnTimeout:       10 * time.Second,
+			TLSHandshakeTimeout:   10 * time.Second,
+			ExpectContinueTimeout: 10 * time.Second,
+		})
+
+		c1.OnRequest(func(r *colly.Request) {
+			r.Headers.Set("User-Agent", utility.RandomString())
+			r.Headers.Set("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8")
+		})
+
+		c1.OnError(func(_ *colly.Response, err error) {
+			log.Printf("Error(c1) : %s\n", err.Error())
+		})
+
+		/*
+		 관객 수, 누적 매출액 조회
+		*/
+		/*
+			c2.OnRequest(func(r *colly.Request) {
+				r.Headers.Set("User-Agent", utility.RandomString())
+				r.Headers.Set("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8")
+			})
+
+			c2.OnError(func(_ *colly.Response, err error) {
+				log.Printf("Error(c2) : %s\n", err.Error())
+			})
+		*/
+
+		c1.OnHTML(".item_tab.basic", func(e *colly.HTMLElement) {
+			var host = config.KOBIS_HOST
+			movieDetail.MainImg = e.ChildAttr("a.fl.thumb", "href")
+			if movieDetail.MainImg != "" && movieDetail.MainImg != "#" {
+				movieDetail.MainImg = host + movieDetail.MainImg
+			}
+			movieDetail.ThumbImg = e.ChildAttr("a.fl.thumb > img", "src")
+			if movieDetail.ThumbImg != "" && movieDetail.ThumbImg != "#" {
+				movieDetail.ThumbImg = host + movieDetail.ThumbImg
+			}
+			movieDetail.Synopsis = e.ChildText("div.info.info2 p.desc_info")
+
+			e.ForEach("div#post > input", func(_ int, ee *colly.HTMLElement) {
+				movieDetail.Poster = append(movieDetail.Poster, model.Poster{
+					Thumb:  host + ee.Attr("thn_img"),
+					Origin: host + ee.Attr("img"),
+				})
+			})
+			e.ForEach("div#stl > input", func(_ int, ee *colly.HTMLElement) {
+				movieDetail.StillCut = append(movieDetail.StillCut, model.StillCut{
+					Thumb:  host + ee.Attr("thn_img"),
+					Origin: host + ee.Attr("img"),
+				})
+			})
+		})
+
+		/*
+			c2.OnHTML("body", func(e *colly.HTMLElement) {
+				var (
+					tr      = e.DOM.Find(".info").Eq(0).Find("table tbody tr").Eq(1)
+					saleAcc = utility.RemoveSpecialChar(strings.Replace(tr.Find("td").Eq(2).Text(), "(100%)", "", 1))
+					audiAcc = utility.RemoveSpecialChar(strings.Replace(tr.Find("td").Eq(3).Text(), "(100%)", "", 1))
+				)
+				SaleAcc, _ := strconv.Atoi(saleAcc)
+				AudiAcc, _ := strconv.Atoi(audiAcc)
+
+				movieDetail.SaleAcc = SaleAcc
+				movieDetail.AudiAcc = AudiAcc
+			})
+		*/
+
+		err := c1.Post(config.MOVIE_DETAIL, map[string]string{
+			"code":       movieCd,
+			"sType":      "",
+			"titleYN":    "Y",
+			"etcParam":   "",
+			"isOuterReq": "false",
+		})
+		if this.Rest.Check(err) {
+			return nil, err
+		}
+
+		/*
+			err = c2.Post(config.MOVIE_DETAIL, map[string]string{
+				"code":  movieCd,
+				"sType": "stat",
+			})
+			if this.Rest.Check(err) {
+				return nil, err
+			}
+		*/
+
+		err = this.MovieDetailModel.Insert(movieDetail)
+		if this.Rest.Check(err) {
+			return nil, err
+		}
+	}
+
+	movieDetail, err := this.MovieDetailModel.Info(movieCd)
+
+	return &movieDetail, err
+}


+ 13 - 0
env.json

@@ -0,0 +1,13 @@
+{
+	"developerEnv": "local",
+	"identityKey": "app-key",
+	"isDebug": true,
+	"isLive": false,
+	"app": {
+		"port": "1050",
+		"secretKey": "xuchhsmginyjkssm",
+		"readTimeout": 200,
+		"writeTimeout": 200,
+		"maxHeaderBytes": 10
+	}
+}

+ 63 - 0
go.mod

@@ -0,0 +1,63 @@
+module crawler
+
+go 1.19
+
+require (
+	firebase.google.com/go v3.13.0+incompatible
+	github.com/appleboy/gin-jwt/v2 v2.9.1
+	github.com/aws/aws-sdk-go v1.44.167
+	github.com/gin-gonic/gin v1.8.2
+	github.com/mileusna/useragent v1.2.1
+	golang.org/x/crypto v0.4.0
+	gorm.io/driver/mysql v1.4.4
+	gorm.io/gorm v1.24.2
+)
+
+require (
+	cloud.google.com/go/compute v1.13.0 // indirect
+	cloud.google.com/go/compute/metadata v0.2.2 // indirect
+	github.com/PuerkitoBio/goquery v1.8.0 // indirect
+	github.com/andybalholm/cascadia v1.3.1 // indirect
+	github.com/antchfx/htmlquery v1.2.5 // indirect
+	github.com/antchfx/xmlquery v1.3.13 // indirect
+	github.com/antchfx/xpath v1.2.1 // indirect
+	github.com/gin-contrib/sse v0.1.0 // indirect
+	github.com/go-playground/locales v0.14.0 // indirect
+	github.com/go-playground/universal-translator v0.18.0 // indirect
+	github.com/go-playground/validator/v10 v10.11.1 // indirect
+	github.com/go-sql-driver/mysql v1.6.0 // indirect
+	github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 // indirect
+	github.com/gobwas/glob v0.2.3 // indirect
+	github.com/goccy/go-json v0.10.0 // indirect
+	github.com/gocolly/colly v1.2.0 // indirect
+	github.com/golang-jwt/jwt/v4 v4.4.3 // indirect
+	github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
+	github.com/golang/protobuf v1.5.2 // indirect
+	github.com/google/go-cmp v0.5.9 // indirect
+	github.com/google/go-querystring v1.1.0 // indirect
+	github.com/googleapis/enterprise-certificate-proxy v0.2.0 // indirect
+	github.com/jinzhu/inflection v1.0.0 // indirect
+	github.com/jinzhu/now v1.1.5 // indirect
+	github.com/jmespath/go-jmespath v0.4.0 // indirect
+	github.com/json-iterator/go v1.1.12 // indirect
+	github.com/kennygrant/sanitize v1.2.4 // indirect
+	github.com/leodido/go-urn v1.2.1 // indirect
+	github.com/mattn/go-isatty v0.0.16 // indirect
+	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+	github.com/modern-go/reflect2 v1.0.2 // indirect
+	github.com/pelletier/go-toml/v2 v2.0.6 // indirect
+	github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca // indirect
+	github.com/temoto/robotstxt v1.1.2 // indirect
+	github.com/ugorji/go/codec v1.2.7 // indirect
+	go.opencensus.io v0.24.0 // indirect
+	golang.org/x/net v0.4.0 // indirect
+	golang.org/x/oauth2 v0.3.0 // indirect
+	golang.org/x/sys v0.3.0 // indirect
+	golang.org/x/text v0.5.0 // indirect
+	google.golang.org/api v0.105.0 // indirect
+	google.golang.org/appengine v1.6.7 // indirect
+	google.golang.org/genproto v0.0.0-20221206210731-b1a01be3a5f6 // indirect
+	google.golang.org/grpc v1.51.0 // indirect
+	google.golang.org/protobuf v1.28.1 // indirect
+	gopkg.in/yaml.v2 v2.4.0 // indirect
+)

+ 291 - 0
go.sum

@@ -0,0 +1,291 @@
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go/compute v1.13.0 h1:AYrLkB8NPdDRslNp4Jxmzrhdr03fUAIDbiGFjLWowoU=
+cloud.google.com/go/compute v1.13.0/go.mod h1:5aPTS0cUNMIc1CE546K+Th6weJUNQErARyZtRXDJ8GE=
+cloud.google.com/go/compute/metadata v0.2.2 h1:aWKAjYaBaOSrpKl57+jnS/3fJRQnxL7TvR/u1VVbt6k=
+cloud.google.com/go/compute/metadata v0.2.2/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM=
+firebase.google.com/go v3.13.0+incompatible h1:3TdYC3DDi6aHn20qoRkxwGqNgdjtblwVAyRLQwGn/+4=
+firebase.google.com/go v3.13.0+incompatible/go.mod h1:xlah6XbEyW6tbfSklcfe5FHJIwjt8toICdV5Wh9ptHs=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+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/antchfx/htmlquery v1.2.5 h1:1lXnx46/1wtv1E/kzmH8vrfMuUKYgkdDBA9pIdMJnk4=
+github.com/antchfx/htmlquery v1.2.5/go.mod h1:2MCVBzYVafPBmKbrmwB9F5xdd+IEgRY61ci2oOsOQVw=
+github.com/antchfx/xmlquery v1.3.13 h1:wqhTv2BN5MzYg9rnPVtZb3IWP8kW6WV/ebAY0FCTI7Y=
+github.com/antchfx/xmlquery v1.3.13/go.mod h1:3w2RvQvTz+DaT5fSgsELkSJcdNgkmg6vuXDEuhdwsPQ=
+github.com/antchfx/xpath v1.2.1 h1:qhp4EW6aCOVr5XIkT+l6LJ9ck/JsUH/yyauNgTQkBF8=
+github.com/antchfx/xpath v1.2.1/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
+github.com/appleboy/gin-jwt/v2 v2.9.1 h1:l29et8iLW6omcHltsOP6LLk4s3v4g2FbFs0koxGWVZs=
+github.com/appleboy/gin-jwt/v2 v2.9.1/go.mod h1:jwcPZJ92uoC9nOUTOKWoN/f6JZOgMSKlFSHw5/FrRUk=
+github.com/appleboy/gofight/v2 v2.1.2 h1:VOy3jow4vIK8BRQJoC/I9muxyYlJ2yb9ht2hZoS3rf4=
+github.com/appleboy/gofight/v2 v2.1.2/go.mod h1:frW+U1QZEdDgixycTj4CygQ48yLTUhplt43+Wczp3rw=
+github.com/aws/aws-sdk-go v1.44.167 h1:kQmBhGdZkQLU7AiHShSkBJ15zr8agy0QeaxXduvyp2E=
+github.com/aws/aws-sdk-go v1.44.167/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
+github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
+github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
+github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
+github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
+github.com/gin-gonic/gin v1.8.2 h1:UzKToD9/PoFj/V4rvlKqTRKnQYyz8Sc1MJlv4JHPtvY=
+github.com/gin-gonic/gin v1.8.2/go.mod h1:qw5AYuDrzRTnhvusDsrov+fDIxp9Dleuu12h8nfB398=
+github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
+github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
+github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
+github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
+github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
+github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
+github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
+github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJWXmqUsHwfTRRkQ=
+github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU=
+github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
+github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
+github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc=
+github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8=
+github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
+github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
+github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
+github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA=
+github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
+github.com/gocolly/colly v1.2.0 h1:qRz9YAn8FIH0qzgNUw+HT9UN7wm1oF9OBAilwEWpyrI=
+github.com/gocolly/colly v1.2.0/go.mod h1:Hof5T3ZswNVsOHYmba1u03W65HDWgpV5HifSuueE0EA=
+github.com/golang-jwt/jwt/v4 v4.4.3 h1:Hxl6lhQFj4AnOX6MLrsCb/+7tCj7DxP7VA+2rDIq5AU=
+github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY=
+github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
+github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
+github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
+github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
+github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/googleapis/enterprise-certificate-proxy v0.2.0 h1:y8Yozv7SZtlU//QXbezB6QkpuE6jMD2/gfzk4AftXjs=
+github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg=
+github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
+github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
+github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
+github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
+github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
+github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
+github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
+github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
+github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/kennygrant/sanitize v1.2.4 h1:gN25/otpP5vAsO2djbMhF/LQX6R7+O1TB4yv8NzpJ3o=
+github.com/kennygrant/sanitize v1.2.4/go.mod h1:LGsjYYtgxbetdg5owWB2mpgUL6e2nfw2eObZ0u0qvak=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
+github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
+github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
+github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
+github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
+github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/mileusna/useragent v1.2.1 h1:p3RJWhi3LfuI6BHdddojREyK3p6qX67vIfOVMnUIVr0=
+github.com/mileusna/useragent v1.2.1/go.mod h1:3d8TOmwL/5I8pJjyVDteHtgDGcefrFUX4ccGOMKNYYc=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
+github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU=
+github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
+github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
+github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
+github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
+github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca h1:NugYot0LIVPxTvN8n+Kvkn6TrbMyxQiuvKdEwFdR9vI=
+github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/temoto/robotstxt v1.1.2 h1:W2pOjSJ6SWvldyEuiFXNxz3xZ8aiWX5LbfDiOFd7Fxg=
+github.com/temoto/robotstxt v1.1.2/go.mod h1:+1AmkuG3IYkh1kv0d2qEB9Le88ehNO0zwOr3ujewlOo=
+github.com/tidwall/gjson v1.14.3 h1:9jvXn7olKEHU1S9vwoMGliaT8jq1vJ7IH/n9zD9Dnlw=
+github.com/tidwall/gjson v1.14.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
+github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
+github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
+github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
+github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
+github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
+github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
+github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
+go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8=
+golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200421231249-e086a090c8fd/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
+golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
+golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU=
+golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.3.0 h1:6l90koy8/LaBLmLu8jpHeHexzMwEita0zFfYlggy2F8=
+golang.org/x/oauth2 v0.3.0/go.mod h1:rQrIauxkUhJ6CuwEXwymO2/eh4xz2ZWF1nBkcxS+tGk=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+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-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
+golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM=
+golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/api v0.105.0 h1:t6P9Jj+6XTn4U9I2wycQai6Q/Kz7iOT+QzjJ3G2V4x8=
+google.golang.org/api v0.105.0/go.mod h1:qh7eD5FJks5+BcE+cjBIm6Gz8vioK7EHvnlniqXBnqI=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
+google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
+google.golang.org/genproto v0.0.0-20221206210731-b1a01be3a5f6 h1:AGXp12e/9rItf6/4QymU7WsAUwCf+ICW75cuR91nJIc=
+google.golang.org/genproto v0.0.0-20221206210731-b1a01be3a5f6/go.mod h1:1dOng4TWOomJrDGhpXjfCD35wQC6jnC7HpRmOFRqEV0=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
+google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
+google.golang.org/grpc v1.51.0 h1:E1eGv1FTqoLIdnBCZufiSHgKjlqG6fKFf6pPWtMTh8U=
+google.golang.org/grpc v1.51.0/go.mod h1:wgNDFcnuBGmxLKI/qn4T+m5BtEBYXJPvibbUPsAIPww=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
+google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+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=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gorm.io/driver/mysql v1.4.4 h1:MX0K9Qvy0Na4o7qSC/YI7XxqUw5KDw01umqgID+svdQ=
+gorm.io/driver/mysql v1.4.4/go.mod h1:BCg8cKI+R0j/rZRQxeKis/forqRwRSYOR8OM3Wo6hOM=
+gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
+gorm.io/gorm v1.24.2 h1:9wR6CFD+G8nOusLdvkZelOEhpJVwwHzpQOUM+REd6U0=
+gorm.io/gorm v1.24.2/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

BIN
go_build_crawler.exe


+ 0 - 0
log/g2a/error.txt


+ 44 - 0
log/kobis/error-log.txt

@@ -0,0 +1,44 @@
+2023/01/01 22:05:07 <nil>
+2023/01/01 22:05:07 Error 3140: Invalid JSON text: "The document is empty." at position 0 in value for column 'tb_movie_detail.poster'.
+2023/01/02 17:03:46 <nil>
+2023/01/02 17:03:46 Error 1812: Tablespace is missing for table `moview`.`tb_movie_detail`.
+2023/01/02 17:04:25 <nil>
+2023/01/02 17:04:38 Post "https://www.kobis.or.kr/kobis/business/mast/mvie/searchMovieDtl.do": context deadline exceeded (Client.Timeout exceeded while awaiting headers)
+2023/01/02 17:04:38 <nil>
+2023/01/02 17:04:38 Post "https://www.kobis.or.kr/kobis/business/mast/mvie/searchMovieDtl.do": context deadline exceeded (Client.Timeout exceeded while awaiting headers)
+2023/01/02 17:04:38 <nil>
+2023/01/02 17:04:38 Error 1812: Tablespace is missing for table `moview`.`tb_movie_detail`.
+2023/01/02 17:05:10 <nil>
+2023/01/02 17:05:10 Error 1812: Tablespace is missing for table `moview`.`tb_movie_detail`.
+2023/01/02 17:08:29 <nil>
+2023/01/02 17:08:29 Error 1812: Tablespace is missing for table `moview`.`tb_movie_detail`.
+2023/01/04 00:36:06 <nil>
+2023/01/04 00:36:06 
+2023/01/04 23:27:57 <nil>
+2023/01/04 23:27:57 
+2023/01/04 23:27:57 <nil>
+2023/01/04 23:27:57 
+2023/01/04 23:27:57 <nil>
+2023/01/04 23:27:57 
+2023/01/08 07:44:48 <nil>
+2023/01/08 07:44:48 invalid character '<' looking for beginning of value
+2023/01/08 07:46:13 <nil>
+2023/01/08 07:46:13 invalid character '<' looking for beginning of value
+2023/01/08 07:48:31 <nil>
+2023/01/08 07:48:31 invalid character '<' looking for beginning of value
+2023/01/08 07:50:03 <nil>
+2023/01/08 07:50:03 invalid character '<' looking for beginning of value
+2023/01/08 07:50:34 <nil>
+2023/01/08 07:50:34 invalid character '<' looking for beginning of value
+2023/01/08 07:58:25 <nil>
+2023/01/08 07:58:25 invalid character '<' looking for beginning of value
+2023/01/10 07:09:38 <nil>
+2023/01/10 07:09:38 
+2023/01/10 07:09:44 <nil>
+2023/01/10 07:09:44 
+2023/01/10 07:11:23 <nil>
+2023/01/10 07:11:23 
+2023/01/10 07:15:50 <nil>
+2023/01/10 07:15:50 
+2023/01/10 07:16:12 <nil>
+2023/01/10 07:16:12 

+ 28 - 0
log/kobis/error.txt

@@ -0,0 +1,28 @@
+2023/10/03 17:38:20 <nil>
+2023/10/03 17:38:36 <nil>
+2023/10/03 17:39:21 <nil>
+2023/10/03 17:39:42 <nil>
+2023/10/03 17:40:18 <nil>
+2023/10/03 17:40:34 <nil>
+2023/10/03 17:40:55 <nil>
+2023/10/03 17:41:13 <nil>
+2023/10/03 17:41:28 <nil>
+2023/10/03 17:41:56 <nil>
+2023/10/03 17:42:11 <nil>
+2023/10/03 17:42:55 <nil>
+2023/10/03 17:43:09 <nil>
+2023/10/05 11:34:17 <nil>
+2023/10/05 12:22:33 <nil>
+2023/10/05 12:23:15 <nil>
+2023/10/05 12:23:37 <nil>
+2023/10/05 12:24:00 <nil>
+2023/10/05 13:11:57 <nil>
+2023/10/05 13:12:22 <nil>
+2023/10/05 13:13:23 <nil>
+2023/10/05 13:13:33 <nil>
+2023/10/05 13:14:47 <nil>
+2023/10/05 13:18:12 <nil>
+2023/10/05 13:18:22 <nil>
+2023/10/05 13:20:16 <nil>
+2023/10/10 02:53:01 <nil>
+2023/10/10 02:53:01 <nil>

+ 1 - 0
log/kobis/page.txt

@@ -0,0 +1 @@
+104

+ 48 - 0
main.go

@@ -0,0 +1,48 @@
+package main
+
+import (
+	"crawler/config"
+	"crawler/middleware"
+	"crawler/route"
+	"crawler/service"
+	"crawler/utility"
+	"github.com/gin-gonic/gin"
+	"log"
+	"net/http"
+	"time"
+)
+
+/*
+@author 김국현
+@date 2022.12.25
+*/
+func main() {
+	utility.SetEnviron()
+	utility.SetDebug()
+
+	// MySQL DB 연결
+	db := new(service.DB)
+	service.DB_MOVIEW = db.Connection(config.DB_MOVIEW)
+	service.DB_CRAWLER = db.Connection(config.DB_CRAWLER)
+	service.DB_PLAYR = db.Connection(config.DB_PLAYR)
+
+	app := gin.Default()
+	app.Use(middleware.Access())          // 접속기록
+	app.Use(middleware.GinBodyResponse()) // request 요청 로그
+
+	// Routing
+	route.SetRoute(app)
+
+	var port = config.Env.APP.Port
+
+	server := &http.Server{
+		Addr:           ":" + port,
+		Handler:        app,
+		ReadTimeout:    time.Duration(config.Env.APP.ReadTimeout) * time.Second,
+		WriteTimeout:   time.Duration(config.Env.APP.WriteTimeout) * time.Second,
+		MaxHeaderBytes: config.Env.APP.MaxHeaderBytes << 20,
+	}
+
+	log.Println("Server was started successfully !!")
+	log.Println(server.ListenAndServe())
+}

+ 58 - 0
middleware/body.go

@@ -0,0 +1,58 @@
+package middleware
+
+import (
+	"bytes"
+	"crawler/model"
+	"fmt"
+	"io/ioutil"
+
+	"github.com/gin-gonic/gin"
+)
+
+type BodyLogWriter struct {
+	gin.ResponseWriter
+	body *bytes.Buffer
+}
+
+func (w BodyLogWriter) Write(b []byte) (int, error) {
+	w.body.Write(b)
+	return w.ResponseWriter.Write(b)
+}
+
+func GinBodyResponse() gin.HandlerFunc {
+	return func(c *gin.Context) {
+		var (
+			bodyBytes  []byte
+			processLog = new(model.ProcessLogModel)
+		)
+
+		if c.Request.Body != nil {
+			bodyBytes, _ = ioutil.ReadAll(c.Request.Body)
+		}
+		c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))
+		blw := &BodyLogWriter{body: bytes.NewBufferString(""), ResponseWriter: c.Writer}
+		c.Writer = blw
+
+		c.Next()
+
+		/*
+			fmt.Printf(
+				"Send HTTP response, req uri: %v, method: %v, body: %v, resp code: %v, body: %v",
+				c.Request.RequestURI, c.Request.Method, string(bodyBytes), c.Writer.Status(), blw.body.String(),
+			)
+		*/
+
+		fmt.Printf(
+			"Send HTTP response, req uri: %v, method: %v, resp code: %v",
+			c.Request.RequestURI, c.Request.Method, c.Writer.Status(),
+		)
+
+		processLog.Path = c.Request.RequestURI
+		processLog.Code = c.Writer.Status()
+		processLog.Method = c.Request.Method
+		processLog.RawQuery = c.Request.URL.RawQuery
+		processLog.Response = blw.body.String()
+		processLog.Request = string(bodyBytes)
+		processLog.Save()
+	}
+}

+ 96 - 0
middleware/log.go

@@ -0,0 +1,96 @@
+package middleware
+
+import (
+	"crawler/model"
+	"crawler/utility"
+	"fmt"
+	"github.com/gin-gonic/gin"
+	ua "github.com/mileusna/useragent"
+	"time"
+)
+
+// 접속기록
+func Access() gin.HandlerFunc {
+	return func(c *gin.Context) {
+		var (
+			gotParam  gin.LogFormatterParams
+			accessLog = new(model.AccessLogModel)
+			start     = time.Now()
+		)
+
+		c.Next()
+
+		/*
+			duration := utility.GetDurationInMillseconds(start)
+			log.Printf("duration: %g\n", duration)
+		*/
+
+		gin.LoggerWithConfig(gin.LoggerConfig{
+			Formatter: func(param gin.LogFormatterParams) string {
+				gotParam = param
+				return fmt.Sprintf("%s - [%s] \"%s %s %s %d %s \"%s\" %s\"\n",
+					param.ClientIP,
+					param.TimeStamp.Format(time.RFC1123),
+					param.Method,
+					param.Path,
+					param.Request.Proto,
+					param.StatusCode,
+					param.Latency,
+					param.Request.UserAgent(),
+					param.ErrorMessage,
+				)
+			},
+		})
+
+		/*
+			fmt.Printf("Request: %#v\n", gotParam.Request)
+			fmt.Printf("TimeStamp: %s\n", gotParam.TimeStamp)
+			fmt.Printf("StatusCode: %d\n", gotParam.StatusCode)
+			fmt.Printf("Latency: %d\n", gotParam.Latency)
+			fmt.Printf("ClientIP: %#v\n", gotParam.ClientIP)
+			fmt.Printf("Method: %s\n", gotParam.Method)
+			fmt.Printf("Path: %s\n", gotParam.Path)
+			fmt.Printf("ErrorMessage: %s\n", gotParam.ErrorMessage)
+		*/
+
+		var (
+			requestID    = c.Writer.Header().Get("Request-Id")
+			status       = c.Writer.Status() // access the status we are sending
+			referer      = c.Request.Referer()
+			clientIP     = utility.GetClientIP(c)
+			method       = c.Request.Method
+			path         = c.Request.RequestURI
+			errorMessage = gotParam.ErrorMessage
+			userAgent    = c.Request.UserAgent()
+			latency      = time.Since(start).Seconds() // after request
+			useragent    = ua.Parse(userAgent)
+		)
+
+		/*
+			fmt.Printf("RequestID: %#v\n", requestID)
+			fmt.Printf("StatusCode: %d\n", statusCode)
+			fmt.Printf("Referer: %s\n", referer)
+			fmt.Printf("ClientIP: %#v\n", clientIP)
+			fmt.Printf("Method: %s\n", method)
+			fmt.Printf("Path: %s\n", path)
+			fmt.Printf("ErrorMessage: %s\n", errorMessage)
+			fmt.Printf("UserAgent: %s\n", userAgent)
+			fmt.Printf("latency: %d\n", latency)
+			fmt.Printf("status: %d\n", status)
+		*/
+
+		accessLog.RequestID = requestID
+		accessLog.RequestURI = path
+		accessLog.ClientIP = clientIP
+		accessLog.Referer = referer
+		accessLog.UserAgent = userAgent
+		accessLog.Browser = useragent.Name + " / " + useragent.Version
+		accessLog.Os = useragent.OS + " / " + useragent.OSVersion
+		accessLog.Device = useragent.Device
+		accessLog.Method = method
+		accessLog.ErrorMessage = errorMessage
+		accessLog.Latency = fmt.Sprint(latency)
+		accessLog.Status = status
+		accessLog.Save()
+	}
+}

+ 42 - 0
model/G2AError.go

@@ -0,0 +1,42 @@
+package model
+
+import (
+	"crawler/config"
+	"crawler/service"
+)
+
+type G2AErrorModel struct {
+	G2AError
+}
+
+type G2AError struct {
+	Status  string `json:"status"`
+	Message string `json:"message"`
+	Code    string `json:"code"`
+	URL     string `json:"url"`
+	Params  string `json:"params"`
+}
+
+func (this *G2AErrorModel) Save() {
+	var (
+		db   = service.DB_PLAYR
+		conn = db.SQLDB
+	)
+
+	sql := `
+		INSERT INTO tb_g2a_error
+		SET 
+			status = ?,
+			message = ?,
+			code = ?,
+			url = ?,
+			params = ?,
+			created_at = NOW();
+	`
+	_, err := conn.Exec(sql, this.Status, this.Message, this.Code, this.URL, this.Params)
+	if err != nil {
+		db.SetErrorLog(err, sql)
+	}
+
+	db.SetGeneralLog(config.GL_ACTION_WRITE, sql, "insert g2a error")
+}

+ 137 - 0
model/G2AOrder.go

@@ -0,0 +1,137 @@
+package model
+
+import (
+	"crawler/config"
+	"crawler/service"
+	"database/sql"
+	"log"
+)
+
+type G2AOrderModel struct {
+	G2AOrderParams
+	G2AOrderResult
+	G2AOrderDetailResult
+	G2AOrderPayResult
+	G2AOrderKeyResult
+	G2AOrder
+}
+
+type G2AOrderParams struct {
+	ProductID string  `form:"product_id" json:"product_id" binding:"required"`
+	Currency  string  `form:"currency" json:"currency"`
+	MaxPrice  float64 `form:"max_price" json:"max_price"`
+}
+
+// Add an Order
+type G2AOrderResult struct {
+	OrderID  string  `json:"order_id"`
+	Price    float32 `json:"price"`
+	Currency string  `json:"currency"`
+}
+
+// Get Order Detail
+type G2AOrderDetailResult struct {
+	Status   string  `json:"status"`
+	Price    float32 `json:"price"`
+	Currency string  `json:"currency"`
+}
+
+// Get Order Key
+type G2AOrderKeyResult struct {
+	Key string `json:"key"`
+}
+
+// Pay for an order
+type G2AOrderPayResult struct {
+	Status        bool   `json:"status"`
+	TransactionID string `json:"transaction_id"`
+}
+
+type G2AOrder struct {
+	RequestKey           string  `json:"request_key"`
+	RequestOrderID       int     `json:"request_order_id"`
+	RequestOrderDetailID int     `json:"request_order_detail_id"`
+	Status               string  `json:"status"`
+	OrderID              string  `json:"order_id"`
+	ProductID            string  `json:"product_id"`
+	Code                 string  `json:"code"`
+	Price                float32 `json:"price"`
+	Currency             string  `json:"currency"`
+	TransactionID        string  `json:"transaction_id"`
+}
+
+func (this *G2AOrderModel) IsExists(requestKey string) bool {
+	var (
+		db     = service.DB_PLAYR
+		conn   = db.SQLDB
+		query  = "SELECT IF(COUNT(*) <= 0, 0, 1) AS `exists` FROM tb_g2a_order WHERE request_key = ?;"
+		exists = false
+	)
+
+	stmt, err := conn.Prepare(query)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	err = stmt.QueryRow(requestKey).Scan(&exists)
+	if err != nil {
+		db.SetErrorLog(err, query)
+		return exists
+	}
+
+	db.SetGeneralLog(config.GL_ACTION_SELECT, query, "select exists g2a order")
+	return exists
+}
+
+func (this *G2AOrderModel) Info(requestKey string) (G2AOrder, error) {
+	var (
+		db    = service.DB_PLAYR
+		conn  = db.SQLDB
+		query = `SELECT request_key, request_order_id, request_order_detail_id, status, order_id, product_id, code, price, currency, transaction_id FROM tb_g2a_order WHERE request_key = ?;`
+		info  G2AOrder
+	)
+
+	err := conn.QueryRow(query, requestKey).Scan(
+		&info.RequestKey, &info.RequestOrderID, &info.RequestOrderDetailID, &info.Status, &info.OrderID, &info.ProductID, &info.Code, &info.Price, &info.Currency, &info.TransactionID,
+	)
+
+	if err != nil && err != sql.ErrNoRows {
+		db.SetErrorLog(err, query)
+		return info, err
+	}
+
+	db.SetGeneralLog(config.GL_ACTION_SELECT, query, "select g2a info")
+	return info, nil
+}
+
+func (this *G2AOrderModel) Insert(order G2AOrder) {
+	var (
+		db   = service.DB_PLAYR
+		conn = db.SQLDB
+	)
+
+	sql := `
+		INSERT INTO tb_g2a_order
+		SET 
+			request_key = ?,
+			request_order_id = ?,
+			request_order_detail_id = ?,
+			status = ?,
+			order_id = ?,
+			product_id = ?,
+			code = ?,
+			price = ?,
+			currency = ?,
+			transaction_id = ?,
+			created_at = NOW();
+	`
+	_, err := conn.Exec(sql,
+		order.RequestKey, order.RequestOrderID, order.RequestOrderDetailID, order.Status, order.OrderID, order.ProductID, order.Code, order.Price, order.Currency, order.TransactionID,
+	)
+
+	if err != nil {
+		db.SetErrorLog(err, sql)
+	}
+
+	db.SetGeneralLog(config.GL_ACTION_WRITE, sql, "insert g2a order info")
+}

+ 288 - 0
model/G2AProduct.go

@@ -0,0 +1,288 @@
+package model
+
+import (
+	"crawler/config"
+	"crawler/service"
+	"database/sql"
+	"encoding/json"
+	"fmt"
+	"reflect"
+)
+
+type G2AProductModel struct {
+	G2AProductParams
+	G2AProductResult
+	G2AProduct
+}
+
+// 검색 변수들
+type G2AProductParams struct {
+	Page              int    `json:"page"`
+	ID                string `json:"id"`
+	MinQty            int    `json:"minQty"`
+	MinPriceFrom      int    `json:"minPriceFrom"`
+	MinPriceTo        int    `json:"minPriceTo"`
+	IncludeOutOfStock string `json:"includeOutOfStock"`
+	UpdatedAtFrom     string `json:"updatedAtFrom"`
+	UpdatedAtTo       string `json:"updatedAtTo"`
+}
+
+// G2A 상품 조회 결과
+type G2AProductResult struct {
+	Total int          `json:"total"`
+	Page  int          `json:"page"`
+	Docs  []G2AProduct `json:"docs"`
+}
+
+// G2A 상품 정보
+type G2AProduct struct {
+	ID                 string `json:"id"`
+	Name               string `json:"name"`
+	IsTest             int
+	Type               string       `json:"type"`
+	Slug               string       `json:"slug"`
+	Qty                int          `json:"qty"`
+	MinPrice           float64      `json:"minPrice"`
+	RetailMinPrice     float64      `json:"retail_min_price"`
+	RetailMinBasePrice float64      `json:"retailMinBasePrice"`
+	Thumbnail          string       `json:"thumbnail"`
+	SmallImage         string       `json:"smallImage"`
+	CoverImage         string       `json:"coverImage"`
+	Images             []string     `json:"images"`
+	UpdatedAt          string       `json:"updated_at"`
+	ReleaseDate        string       `json:"release_date"`
+	Region             string       `json:"region"`
+	Developer          string       `json:"developer"`
+	Publisher          string       `json:"publisher"`
+	Platform           string       `json:"platform"`
+	PriceLimit         PriceLimit   `json:"priceLimit"`
+	Restrictions       Restrictions `json:"restrictions"`
+	Requirements       Requirements `json:"requirements"`
+	Videos             []Videos     `json:"videos"`
+	Categories         []Categories `json:"categories"`
+}
+
+type PriceLimit struct {
+	Min float64 `json:"min"`
+	Max float64 `json:"max"`
+}
+
+type Restrictions struct {
+	PegiViolence       bool `json:"pegi_violence"`
+	PegiProfanity      bool `json:"pegi_profanity"`
+	PegiDiscrimination bool `json:"pegi_discrimination"`
+	PegiDrugs          bool `json:"pegi_drugs"`
+	PegiFear           bool `json:"pegi_fear"`
+	PegiGambling       bool `json:"pegi_gambling"`
+	PegiOnline         bool `json:"pegi_online"`
+	PegiSex            bool `json:"pegi_sex"`
+}
+
+type Requirements struct {
+	Minimal     Spec `json:"minimal"`
+	Recommended Spec `json:"recommended"`
+}
+
+type Spec struct {
+	ReqProcessor string `json:"reqprocessor"`
+	ReqGraphics  string `json:"reqgraphics"`
+	ReqMemory    string `json:"reqmemory"`
+	ReqDiskSpace string `json:"reqdiskspace"`
+	ReqSystem    string `json:"reqsystem"`
+	ReqOther     string `json:"reqother"`
+}
+
+type Videos struct {
+	Type string `json:"type"`
+	Url  string `json:"url"`
+}
+
+type Categories struct {
+	ID   int    `json:"id"`
+	Name string `json:"name"`
+}
+
+func (this *G2AProductModel) IsExists(id string) bool {
+	var (
+		db     = service.DB_PLAYR
+		conn   = db.SQLDB
+		query  = "SELECT IF(COUNT(*) <= 0, 0, 1) AS `exists` FROM tb_g2a_product WHERE id = ?;"
+		exists = false
+	)
+
+	err := conn.QueryRow(query, id).Scan(&exists)
+	if err != nil {
+		db.SetErrorLog(err, query)
+		return exists
+	}
+
+	db.SetGeneralLog(config.GL_ACTION_SELECT, query, "select exists g2a")
+	return exists
+}
+
+func (this *G2AProductModel) Insert(list []G2AProduct) error {
+	if len(list) == 0 {
+		return fmt.Errorf("Product 상품이 존재하지 않습니다.")
+	}
+
+	var (
+		db    = service.DB_PLAYR
+		conn  = db.SQLDB
+		query = `
+			INSERT INTO tb_g2a_product (
+				id, name, is_test, type, slug, qty, min_price, retail_min_price, retail_min_base_price,
+				thumbnail, small_image, cover_image, images, updated_at, release_date, region, developer,
+				publisher, platform, price_limit, restrictions, requirements, videos, categories, created_at
+			)
+			VALUES 
+		`
+		vals = []interface{}{}
+		dup  string
+	)
+
+	for _, row := range list {
+		query += `(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW()),`
+
+		data := this.DataFilter(row)
+
+		vals = append(vals,
+			data["id"], data["name"], data["IsTest"], data["type"], data["slug"], data["qty"],
+			data["minPrice"], data["retail_min_price"], data["retailMinBasePrice"], data["thumbnail"],
+			data["smallImage"], data["coverImage"], data["images"], data["updated_at"], data["release_date"],
+			data["region"], data["developer"], data["publisher"], data["platform"], data["priceLimit"],
+			data["restrictions"], data["requirements"], data["videos"], data["categories"],
+		)
+
+		dup += `name = VALUES(name), is_test = VALUES(is_test), type = VALUES(type), 
+				slug = VALUES(slug), qty = VALUES(qty), min_price = VALUES(min_price), retail_min_price = VALUES(retail_min_price), 
+				retail_min_base_price = VALUES(retail_min_base_price), thumbnail = VALUES(thumbnail), small_image = VALUES(small_image), 
+				cover_image = VALUES(cover_image), images = VALUES(images), updated_at = VALUES(updated_at), release_date = VALUES(release_date), 
+				region = VALUES(region), developer = VALUES(developer), publisher = VALUES(publisher),
+				platform = VALUES(platform), price_limit = VALUES(price_limit), restrictions = VALUES(restrictions), 
+				requirements = VALUES(requirements), videos = VALUES(videos), categories = VALUES(categories),`
+
+	}
+
+	query = query[0 : len(query)-1]
+	dup = dup[0 : len(dup)-1]
+
+	query += `ON DUPLICATE KEY UPDATE ` + dup
+
+	stmt, err := conn.Prepare(query)
+	if err != nil {
+		db.SetErrorLog(err, query)
+		return err
+	}
+	defer stmt.Close()
+
+	_, err = stmt.Exec(vals...)
+	if err != nil {
+		db.SetErrorLog(err, query)
+		return err
+	}
+
+	db.SetGeneralLog(config.GL_ACTION_WRITE, query, "insert g2a products")
+	return nil
+}
+
+func (this *G2AProductModel) Update(row G2AProduct) error {
+	var (
+		db    = service.DB_PLAYR
+		conn  = db.SQLDB
+		query = `
+			UPDATE tb_g2a_product SET 
+				name = ?, is_test = ?, type = ?, slug = ?, qty = ?, min_price = ?, retail_min_price = ?, retail_min_base_price = ?,
+				thumbnail = ?, small_image = ?, cover_image = ?, images = ?, updated_at = ?, release_date = ?, region = ?, developer = ?,
+				publisher = ?, platform = ?, price_limit = ?, restrictions = ?, requirements = ?, videos = ?, categories = ?, created_at = NOW()
+			WHERE id = ?;
+		`
+		data = this.DataFilter(row)
+	)
+
+	_, err := conn.Exec(query,
+		data["name"], data["IsTest"], data["type"], data["slug"], data["qty"],
+		data["minPrice"], data["retail_min_price"], data["retailMinBasePrice"], data["thumbnail"],
+		data["smallImage"], data["coverImage"], data["images"], data["updated_at"], data["release_date"],
+		data["region"], data["developer"], data["publisher"], data["platform"], data["priceLimit"],
+		data["restrictions"], data["requirements"], data["videos"], data["categories"], data["id"])
+
+	if err != nil {
+		db.SetErrorLog(err, query)
+		return err
+	}
+
+	db.SetGeneralLog(config.GL_ACTION_MODIFY, query, "update g2a product info")
+	return nil
+}
+
+func (this *G2AProductModel) Info(id int) (G2AProduct, error) {
+	var (
+		db    = service.DB_PLAYR
+		conn  = db.SQLDB
+		query = `SELECT id, name, qty, min_price, retail_min_price, retail_min_base_price FROM tb_g2a_product WHERE id = ?;`
+		info  G2AProduct
+	)
+
+	err := conn.QueryRow(query, id).Scan(
+		&info.ID, &info.Name, &info.Qty, &info.MinPrice, &info.RetailMinPrice, &info.RetailMinBasePrice,
+	)
+
+	if err != nil && err != sql.ErrNoRows {
+		db.SetErrorLog(err, query)
+		return info, err
+	}
+
+	db.SetGeneralLog(config.GL_ACTION_SELECT, query, "select g2a product info")
+	return info, nil
+}
+
+func (this *G2AProductModel) DataFilter(product G2AProduct) map[string]interface{} {
+	var row = map[string]any{}
+	data, _ := json.Marshal(product)
+	_ = json.Unmarshal(data, &row)
+
+	for k, v := range row {
+		switch c := v.(type) {
+		case string:
+			if c == "" {
+				row[k] = nil
+			}
+		case int, int32, int64:
+			if c == 0 {
+				row[k] = 0
+			}
+		case float32, float64:
+			if c == 0 || c == nil || c == "" {
+				row[k] = 0.0
+			}
+		case []string:
+			if len(c) == 0 {
+				row[k] = nil
+			} else {
+				s, _ := json.Marshal(c)
+				row[k] = string(s)
+			}
+		case nil:
+			row[k] = nil
+		case []interface{}:
+			if c == nil || len(c) == 0 {
+				row[k] = nil
+			} else {
+				s, _ := json.Marshal(c)
+				row[k] = string(s)
+			}
+		case map[string]interface{}:
+			if c == nil || len(c) == 0 {
+				row[k] = nil
+			} else {
+				s, _ := json.Marshal(c)
+				row[k] = string(s)
+			}
+		default:
+			fmt.Println(k)
+			fmt.Println(reflect.TypeOf(c))
+		}
+	}
+
+	return row
+}

+ 52 - 0
model/G2AReport.go

@@ -0,0 +1,52 @@
+package model
+
+import (
+	"crawler/config"
+	"crawler/service"
+)
+
+type G2AReportInterface interface {
+	Insert(row G2AReport)
+}
+
+type G2AReportModel struct {
+	G2AReport
+}
+
+type G2AReport struct {
+	TotalCnt  int
+	InsertCnt int
+	UpdateCnt int
+	ProcessAt float64
+	LastPage  int
+	CreatedAt string
+}
+
+func (this *G2AReportModel) Insert(row G2AReport) error {
+	var (
+		db   = service.DB_PLAYR
+		conn = db.SQLDB
+	)
+
+	sql := `
+			INSERT INTO tb_g2a_report
+			SET 
+				total_cnt = ?,
+				insert_cnt = ?,
+				update_cnt = ?, 
+				process_at = ?, 
+				last_page = ?, 
+				created_at = NOW();
+	`
+	_, err := conn.Exec(sql,
+		row.TotalCnt, row.InsertCnt, row.UpdateCnt, row.ProcessAt, row.LastPage,
+	)
+
+	if err != nil {
+		db.SetErrorLog(err, sql)
+		return err
+	}
+
+	db.SetGeneralLog(config.GL_ACTION_WRITE, sql, "insert g2a report")
+	return nil
+}

+ 66 - 0
model/accessLog.go

@@ -0,0 +1,66 @@
+package model
+
+import (
+	"crawler/config"
+	"crawler/service"
+)
+
+type AccessLogInterface interface {
+	Save()
+}
+
+type AccessLogModel struct {
+	AccessLog
+}
+
+type AccessLog struct {
+	RequestID    string `json:"request_id"`
+	RequestURI   string `json:"request_uri"`
+	ClientIP     string `json:"client_ip"`
+	Referer      string `json:"referer"`
+	UserAgent    string `json:"useragent"`
+	Browser      string `json:"browser"`
+	Os           string `json:"os"`
+	Device       string `json:"device"`
+	Method       string `json:"method"`
+	ErrorMessage string `json:"error_message"`
+	Latency      string `json:"latency"`
+	Status       int    `json:"status"`
+	CreatedAt    string `json:"created_at"`
+}
+
+// 접속기록
+func (this *AccessLogModel) Save() {
+	var (
+		db   = service.DB_CRAWLER
+		conn = db.SQLDB
+	)
+
+	sql := `
+		INSERT INTO tb_access_log
+		SET 
+			request_id = ?, 
+			request_uri = ?, 
+			client_ip = ?, 
+			referer = ?,
+			useragent = ?, 
+			browser = ?, 
+			os = ?, 
+			device = ?, 
+			method = ?, 
+			error_message = ?, 
+			latency = ?, 
+			status = ?, 
+			created_at = NOW();
+	`
+	_, err := conn.Exec(sql,
+		this.RequestID, this.RequestURI, this.ClientIP, this.Referer,
+		this.UserAgent, this.Browser, this.Os, this.Device, this.Method,
+		this.ErrorMessage, this.Latency, this.Status,
+	)
+	if err != nil {
+		db.SetErrorLog(err, sql)
+	}
+
+	db.SetGeneralLog(config.GL_ACTION_WRITE, sql, "insert access log")
+}

+ 132 - 0
model/kobis.go

@@ -0,0 +1,132 @@
+package model
+
+import (
+	"crawler/config"
+	"crawler/service"
+	"encoding/json"
+	"github.com/google/go-querystring/query"
+)
+
+type Kobis struct {
+	Rest service.Rest
+}
+
+func (this *Kobis) MovieDailyBoxOfficeListAPI(req SearchDailyBoxOfficeParams) (SearchDailyBoxOfficeList, error) {
+	var (
+		url      = config.MOVIE_DAILY_BOX_OFFICE_LIST
+		query, _ = query.Values(req)
+		result   SearchDailyBoxOfficeList
+	)
+
+	if query.Encode() != "" {
+		url += "?" + query.Encode()
+	}
+
+	data, err := this.Rest.CallRestGetAPI(url)
+	if this.Rest.Check(err) {
+		return result, err
+	}
+
+	err = json.Unmarshal(data, &result)
+	if this.Rest.Check(err) {
+		return result, err
+	}
+
+	return result, nil
+}
+
+func (this *Kobis) MovieWeeklyBoxOfficeListAPI(req SearchWeeklyBoxOfficeParams) (SearchWeeklyBoxOfficeList, error) {
+	var (
+		url      = config.MOVIE_WEEK_BOX_OFFICE_LIST
+		query, _ = query.Values(req)
+		result   SearchWeeklyBoxOfficeList
+	)
+
+	if query.Encode() != "" {
+		url += "?" + query.Encode()
+	}
+
+	data, err := this.Rest.CallRestGetAPI(url)
+	if this.Rest.Check(err) {
+		return result, err
+	}
+
+	err = json.Unmarshal(data, &result)
+	if this.Rest.Check(err) {
+		return result, err
+	}
+
+	return result, nil
+}
+
+func (this *Kobis) MovieListAPI(req SearchMovieListParams) (SearchMovieList, error) {
+	var (
+		url      = config.MOVIE_LIST
+		query, _ = query.Values(req)
+		result   SearchMovieList
+	)
+
+	if query.Encode() != "" {
+		url += "?" + query.Encode()
+	}
+
+	data, err := this.Rest.CallRestGetAPI(url)
+	if this.Rest.Check(err) {
+		return result, err
+	}
+
+	err = json.Unmarshal(data, &result)
+	if this.Rest.Check(err) {
+		return result, err
+	}
+
+	return result, nil
+}
+
+func (this *Kobis) MovieInfoAPI(req SearchMovieInfoParams) (SearchMovieInfo, error) {
+	var (
+		url      = config.MOVIE_INFO
+		query, _ = query.Values(req)
+		result   SearchMovieInfo
+	)
+
+	if query.Encode() != "" {
+		url += "?" + query.Encode()
+	}
+
+	data, err := this.Rest.CallRestGetAPI(url)
+	if this.Rest.Check(err) {
+		return result, err
+	}
+
+	err = json.Unmarshal(data, &result)
+	if this.Rest.Check(err) {
+		return result, err
+	}
+
+	return result, nil
+}
+
+func (this *Kobis) MovieBoxOfficeAPI(req SearchBoxOfficeParams) (SearchBoxOfficeList, error) {
+	var (
+		url      = config.MOVIE_BOX_OFFICE_STATS
+		query, _ = query.Values(req)
+		result   SearchBoxOfficeList
+	)
+
+	if query.Encode() != "" {
+		url += "?" + query.Encode()
+	}
+
+	data, err := this.Rest.CallRestGetAPI(url)
+	if this.Rest.Check(err) {
+		return result, err
+	}
+
+	err = json.Unmarshal(data, &result)
+	if this.Rest.Check(err) {
+		return result, err
+	}
+
+	return result, nil
+}

+ 495 - 0
model/movie.go

@@ -0,0 +1,495 @@
+package model
+
+import (
+	"crawler/config"
+	"crawler/service"
+	"database/sql"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"log"
+	"reflect"
+	"strconv"
+	"strings"
+)
+
+type MovieListModel struct {
+	SearchMovieListParams
+	SearchMovieList
+	MovieListInfo
+	Director
+	Company
+	MovieListTable
+}
+
+// 영화 목록 검색 변수
+type SearchMovieListParams struct {
+	Key           string `form:"key" url:"key" binding:"required"`
+	CurPage       int    `form:"curPage" url:"curPage"`
+	ItemPerPage   int    `form:"itemPerPage" url:"itemPerPage,omitempty"`
+	MovieNm       string `form:"movieNm" url:"movieNm,omitempty"`
+	DirectorNm    string `form:"directorNm" url:"directorNm,omitempty"`
+	OpenStartDt   int    `form:"openStartDt" url:"openStartDt,omitempty"`
+	OpenEndDt     int    `form:"openEndDt" url:"openEndDt,omitempty"`
+	PrdtStartYear int    `form:"prdtStartYear" url:"prdtStartYear,omitempty"`
+	PrdtEndYear   int    `form:"prdtEndYear" url:"prdtEndYear,omitempty"`
+	RepNationCd   string `form:"repNationCd" url:"repNationCd,omitempty"`
+	MovieTypeCd   string `form:"movieTypeCd" url:"movieTypeCd,omitempty"`
+}
+
+type SearchMovieList struct {
+	MovieListResult struct {
+		TotCnt    int             `json:"totCnt"`
+		Source    string          `json:"source"`
+		MovieList []MovieListInfo `json:"movieList"`
+	} `json:"movieListResult"`
+}
+
+type MovieListInfo struct {
+	MovieCd     string     `json:"movieCd"`
+	MovieNm     string     `json:"movieNm"`
+	MovieNmEn   string     `json:"movieNmEn"`
+	PrdtYear    string     `json:"prdtYear"`
+	OpenDt      string     `json:"openDt"`
+	TypeNm      string     `json:"typeNm"`
+	PrdtStatNm  string     `json:"prdtStatNm"`
+	NationAlt   string     `json:"nationAlt"`
+	GenreAlt    string     `json:"genreAlt"`
+	RepNationNm string     `json:"repNationNm"`
+	RepGenreNm  string     `json:"repGenreNm"`
+	Directors   []Director `json:"directors,omitempty"`
+	Companys    []Company  `json:"companys,omitempty"`
+}
+
+type Director struct {
+	PeopleNm string `json:"peopleNm,omitempty"`
+}
+
+type Company struct {
+	CompanyCd string `json:"companyCd,omitempty"`
+	CompanyNm string `json:"companyNm,omitempty"`
+}
+
+type MovieListTable struct {
+	MovieID     int
+	MovieCd     string
+	MovieNm     *string
+	MovieNmEn   *string
+	PrdtYear    *int
+	OpenDt      *int
+	TypeNm      *string
+	PrdtStatNm  *string
+	NationAlt   *string
+	GenreAlt    *string
+	RepNationNm *string
+	RepGenreNm  *string
+	Directors   *string
+	Companys    *string
+	UpdatedAt   *string
+	CreatedAt   string
+
+	Detail *MovieDetailTable
+}
+
+func (this *MovieListModel) Total() int {
+	var (
+		db    = service.DB_MOVIEW
+		conn  = db.SQLDB
+		query = "SELECT COUNT(*) FROM tb_movie;"
+		total = 0
+	)
+
+	err := conn.QueryRow(query).Scan(&total)
+	if err != nil {
+		db.SetErrorLog(err, query)
+		return total
+	}
+
+	db.SetGeneralLog(config.GL_ACTION_SELECT, query, "select total movie")
+	return total
+}
+
+func (this *MovieListModel) List(ids string) ([]MovieListTable, error) {
+	var (
+		db    = service.DB_MOVIEW
+		conn  = db.SQLDB
+		query = fmt.Sprintf("SELECT * FROM tb_movie WHERE id IN (%s) ORDER BY open_dt DESC;", ids)
+		list  = make([]MovieListTable, 0)
+	)
+
+	rows, err := conn.Query(query)
+	if err != nil && err != sql.ErrNoRows {
+		db.SetErrorLog(err, query)
+		return list, err
+	}
+	defer rows.Close()
+
+	for rows.Next() {
+		var row MovieListTable
+		if err = rows.Scan(
+			&row.MovieID, &row.MovieCd, &row.MovieNm, &row.MovieNmEn, &row.PrdtYear, &row.OpenDt, &row.TypeNm,
+			&row.PrdtStatNm, &row.NationAlt, &row.GenreAlt, &row.RepNationNm, &row.RepGenreNm,
+			&row.Directors, &row.Companys, &row.UpdatedAt, &row.CreatedAt); err != nil {
+			return list, err
+		}
+
+		list = append(list, row)
+	}
+
+	if err = rows.Err(); err != nil {
+		return list, err
+	}
+
+	db.SetGeneralLog(config.GL_ACTION_SELECT, query, "select movie daily")
+	return list, nil
+}
+
+func (this *MovieListModel) Insert(list []MovieListInfo) error {
+	if len(list) <= 0 {
+		return nil
+	}
+
+	var (
+		db    = service.DB_MOVIEW
+		conn  = db.SQLDB
+		query = `
+			INSERT INTO tb_movie (
+				movie_cd, movie_nm, movie_nm_en, prdt_year, open_dt, 
+			    type_nm, prdt_stat_nm, nation_alt, genre_alt, rep_nation_nm, 
+			    rep_genre_nm, directors, companys, updated_at, created_at
+			)
+			VALUES 
+		`
+		vals = []interface{}{}
+		dup  string
+	)
+
+	for _, row := range list {
+		query += `(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NOW()),`
+
+		data := this.DataFilter(row)
+
+		vals = append(vals,
+			data.MovieCd, data.MovieNm, data.MovieNmEn, data.PrdtYear, data.OpenDt,
+			data.TypeNm, data.PrdtStatNm, data.NationAlt, data.GenreAlt, data.RepNationNm,
+			data.RepGenreNm, data.Directors, data.Companys)
+
+		dup += `movie_cd = VALUES(movie_cd), movie_nm = VALUES(movie_nm), movie_nm_en = VALUES(movie_nm_en),
+				prdt_year = VALUES(prdt_year), open_dt = VALUES(open_dt), type_nm = VALUES(type_nm), 
+				prdt_stat_nm = VALUES(prdt_stat_nm), nation_alt = VALUES(nation_alt), genre_alt = VALUES(genre_alt),
+				rep_nation_nm = VALUES(rep_nation_nm), rep_genre_nm = VALUES(rep_genre_nm), directors = VALUES(directors),
+				companys = VALUES(companys), updated_at = NOW(),`
+
+	}
+
+	query = query[0 : len(query)-1]
+	dup = dup[0 : len(dup)-1]
+
+	query += `ON DUPLICATE KEY UPDATE ` + dup
+
+	stmt, err := conn.Prepare(query)
+	if err != nil {
+		db.SetErrorLog(err, query)
+		return err
+	}
+	defer stmt.Close()
+
+	_, err = stmt.Exec(vals...)
+	if err != nil {
+		db.SetErrorLog(err, query)
+		return err
+	}
+
+	db.SetGeneralLog(config.GL_ACTION_WRITE, query, "insert movie list")
+	return nil
+}
+
+func (this *MovieListModel) IsExists(movieCd string) bool {
+	var (
+		db     = service.DB_MOVIEW
+		conn   = db.SQLDB
+		query  = "SELECT IF(COUNT(*) <= 0, 0, 1) AS `exists` FROM tb_movie WHERE movie_cd = ?;"
+		exists = false
+	)
+
+	err := conn.QueryRow(query, movieCd).Scan(&exists)
+	if err != nil {
+		db.SetErrorLog(err, query)
+		return exists
+	}
+
+	db.SetGeneralLog(config.GL_ACTION_SELECT, query, "select exists movie")
+	return exists
+}
+
+func (this *MovieListModel) Update(list []MovieListInfo) error {
+	if len(list) <= 0 {
+		return nil
+	}
+
+	var (
+		db    = service.DB_MOVIEW
+		conn  = db.SQLDB
+		query = `
+			UPDATE tb_movie SET 
+				movie_nm = ?, movie_nm_en = ?, prdt_year = ?, open_dt = ?, 
+			    type_nm = ?, prdt_stat_nm = ?, nation_alt = ?, genre_alt = ?, 
+			    rep_nation_nm = ?, rep_genre_nm = ?, directors = ?, companys = ?,
+			    updated_at = NOW()
+			WHERE movie_cd = ?
+		`
+	)
+
+	for _, row := range list {
+		data := this.DataFilter(row)
+		_, err := conn.Exec(query,
+			data.MovieNm, data.MovieNmEn, data.PrdtYear, data.OpenDt,
+			data.TypeNm, data.PrdtStatNm, data.NationAlt, data.GenreAlt, data.RepNationNm,
+			data.RepGenreNm, data.Directors, data.Companys, data.MovieCd)
+		if err != nil {
+			db.SetErrorLog(err, query)
+			return err
+		}
+	}
+
+	db.SetGeneralLog(config.GL_ACTION_MODIFY, query, "update movie list")
+	return nil
+}
+
+func (this *MovieListModel) Replace(info MovieListInfo) error {
+	if reflect.ValueOf(info).IsZero() {
+		return nil
+	}
+
+	var (
+		db    = service.DB_MOVIEW
+		conn  = db.SQLDB
+		query = `
+			INSERT tb_movie SET 
+				movie_cd = ?, movie_nm = ?, movie_nm_en = ?, prdt_year = ?, open_dt = ?, 
+			    type_nm = ?, prdt_stat_nm = ?, nation_alt = ?, genre_alt = ?, 
+			    rep_nation_nm = ?, rep_genre_nm = ?, directors = ?, companys = ?,
+			    created_at = NOW()
+			ON DUPLICATE KEY UPDATE 
+			    movie_nm = VALUES(movie_nm), movie_nm_en = VALUES(movie_nm_en), prdt_year = VALUES(prdt_year), open_dt = VALUES(open_dt), 
+			    type_nm = VALUES(type_nm), prdt_stat_nm = VALUES(prdt_stat_nm), nation_alt = VALUES(nation_alt), genre_alt = VALUES(genre_alt), 
+			    rep_nation_nm = VALUES(rep_nation_nm), rep_genre_nm = VALUES(rep_genre_nm), directors = VALUES(directors), companys = VALUES(companys),
+			    updated_at = NOW();
+		`
+	)
+
+	data := this.DataFilter(info)
+	_, err := conn.Exec(query,
+		data.MovieCd, data.MovieNm, data.MovieNmEn, data.PrdtYear, data.OpenDt,
+		data.TypeNm, data.PrdtStatNm, data.NationAlt, data.GenreAlt, data.RepNationNm,
+		data.RepGenreNm, data.Directors, data.Companys)
+
+	if err != nil {
+		db.SetErrorLog(err, query)
+		return err
+	}
+
+	db.SetGeneralLog(config.GL_ACTION_MODIFY, query, "replace movie list")
+	return nil
+}
+
+// 영화 기본 정보에 없는 고유 코드 조회
+func (this *MovieListModel) MovieInfoExcludeCodes() []string {
+	var (
+		db    = service.DB_MOVIEW
+		conn  = db.SQLDB
+		query = `SELECT movie_cd FROM tb_movie WHERE NOT EXISTS(SELECT movie_cd FROM tb_movie_info WHERE tb_movie.movie_cd = tb_movie_info.movie_cd);`
+		code  []string
+	)
+
+	rows, err := conn.Query(query)
+	if err != nil && err != sql.ErrNoRows {
+		db.SetErrorLog(err, query)
+		return code
+	}
+	defer rows.Close()
+
+	for rows.Next() {
+		var cd string
+		if err := rows.Scan(&cd); err != nil {
+			log.Fatal(err)
+		}
+		code = append(code, cd)
+	}
+
+	db.SetGeneralLog(config.GL_ACTION_SELECT, query, "select movie_cd list")
+	return code
+}
+
+// 영화 상세 정보에 없는 고유 코드 조회
+func (this *MovieListModel) MovieDetailExcludeCodes() []string {
+	var (
+		db    = service.DB_MOVIEW
+		conn  = db.SQLDB
+		query = `SELECT movie_cd FROM tb_movie WHERE NOT EXISTS(SELECT movie_cd FROM tb_movie_detail WHERE tb_movie_detail.movie_cd = tb_movie.movie_cd);`
+		//sql = `SELECT movie_cd FROM tb_movie ORDER BY movie_id ASC`
+		code []string
+	)
+
+	rows, err := conn.Query(query)
+	if err != nil && err != sql.ErrNoRows {
+		db.SetErrorLog(err, query)
+		return code
+	}
+	defer rows.Close()
+
+	for rows.Next() {
+		var cd string
+		if err := rows.Scan(&cd); err != nil {
+			log.Fatal(err)
+		}
+		code = append(code, cd)
+	}
+
+	db.SetGeneralLog(config.GL_ACTION_SELECT, query, "select movie_cd list")
+
+	return code
+}
+
+func (this *MovieListModel) DataFilter(row MovieListInfo) MovieListTable {
+	var result MovieListTable
+	result.MovieCd = row.MovieCd
+
+	if row.MovieNm != "" {
+		result.MovieNm = &row.MovieNm
+	} else {
+		result.MovieNm = nil
+	}
+
+	if row.MovieNmEn != "" {
+		result.MovieNmEn = &row.MovieNmEn
+	} else {
+		result.MovieNmEn = nil
+	}
+
+	if row.PrdtYear != "" {
+		prdtYear, _ := strconv.Atoi(row.PrdtYear)
+		result.PrdtYear = &prdtYear
+	} else {
+		result.PrdtYear = nil
+	}
+
+	if row.OpenDt != "" {
+		openDt, _ := strconv.Atoi(row.OpenDt)
+		result.OpenDt = &openDt
+	} else {
+		result.OpenDt = nil
+	}
+
+	if row.TypeNm != "" {
+		result.TypeNm = &row.TypeNm
+	} else {
+		result.TypeNm = nil
+	}
+
+	if row.PrdtStatNm != "" {
+		result.PrdtStatNm = &row.PrdtStatNm
+	} else {
+		result.PrdtStatNm = nil
+	}
+
+	if row.NationAlt != "" {
+		result.NationAlt = &row.NationAlt
+	} else {
+		result.NationAlt = nil
+	}
+
+	if row.GenreAlt != "" {
+		result.GenreAlt = &row.GenreAlt
+	} else {
+		result.GenreAlt = nil
+	}
+
+	if row.RepNationNm != "" {
+		result.RepNationNm = &row.RepNationNm
+	} else {
+		result.RepNationNm = nil
+	}
+
+	if row.RepGenreNm != "" {
+		result.RepGenreNm = &row.RepGenreNm
+	} else {
+		result.RepGenreNm = nil
+	}
+
+	if len(row.Directors) > 0 {
+		directors, _ := json.Marshal(row.Directors)
+		s := string(directors)
+		result.Directors = &s
+	} else {
+		result.Directors = nil
+	}
+
+	if len(row.Companys) > 0 {
+		companys, _ := json.Marshal(row.Companys)
+		s := string(companys)
+		result.Companys = &s
+	} else {
+		result.Companys = nil
+	}
+
+	return result
+}
+
+func (this *MovieListModel) MakeParams(req SearchMovieListParams) (string, error) {
+	params, err := json.Marshal(MovieListParams{
+		CurPage:       req.CurPage,
+		ItemPerPage:   req.ItemPerPage,
+		MovieNm:       req.MovieNm,
+		DirectorNm:    req.DirectorNm,
+		OpenStartDt:   req.OpenStartDt,
+		OpenEndDt:     req.OpenEndDt,
+		PrdtStartYear: req.PrdtStartYear,
+		PrdtEndYear:   req.PrdtEndYear,
+		RepNationCd:   req.RepNationCd,
+		MovieTypeCd:   req.MovieTypeCd,
+	})
+	return string(params), err
+}
+
+func (this *MovieListModel) LastInsertIDs(movieCDs []string) (string, error) {
+	if movieCDs == nil {
+		return "", errors.New("날짜를 지정해주세요.")
+	}
+
+	var (
+		db    = service.DB_MOVIEW
+		conn  = db.SQLDB
+		query = fmt.Sprintf("SELECT id FROM tb_movie WHERE movie_cd IN(%s);",
+			strings.Join(strings.Split(strings.Repeat("?", len(movieCDs)), ""), ", "))
+		params = make([]interface{}, 0)
+		ids    = make([]string, 0)
+	)
+
+	for _, movieCd := range movieCDs {
+		params = append(params, movieCd)
+	}
+
+	rows, err := conn.Query(query, params...)
+
+	if err != nil && err != sql.ErrNoRows {
+		db.SetErrorLog(err, query)
+		return "", err
+	}
+
+	for rows.Next() {
+		var id string
+		if err = rows.Scan(&id); err != nil {
+			return "", err
+		}
+		ids = append(ids, id)
+	}
+
+	if err = rows.Err(); err != nil {
+		return "", err
+	}
+
+	db.SetGeneralLog(config.GL_ACTION_SELECT, query, "select ids movie list")
+
+	return strings.Join(ids, ","), nil
+}

+ 402 - 0
model/movieDaily.go

@@ -0,0 +1,402 @@
+package model
+
+import (
+	"crawler/config"
+	"crawler/service"
+	"database/sql"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"strconv"
+	"strings"
+)
+
+type MovieDailyModel struct {
+	SearchDailyBoxOfficeParams
+	SearchDailyBoxOfficeList
+	DailyBoxOfficeInfo
+}
+
+// 일별 박스오피스 검색 변수
+type SearchDailyBoxOfficeParams struct {
+	Key          string `form:"key" url:"key" binding:"required"`
+	TargetDt     string `form:"targetDt" url:"targetDt" binding:"required"`
+	ItemPerPage  string `form:"itemPerPage" url:"itemPerPage,omitempty"`
+	MultiMovieYn string `form:"multiMovieYn" url:"multiMovieYn,omitempty"`
+	RepNationCd  string `form:"repNationCd" url:"repNationCd,omitempty"`
+	WideAreaCd   string `form:"wideAreaCd" url:"wideAreaCd,omitempty"`
+}
+
+// 일별 박스오피스 응답 변수
+type SearchDailyBoxOfficeList struct {
+	BoxOfficeResult struct {
+		BoxofficeType      string               `json:"boxofficeType"`
+		ShowRange          string               `json:"showRange"`
+		DailyBoxOfficeList []DailyBoxOfficeInfo `json:"dailyBoxOfficeList"`
+	} `json:"boxOfficeResult"`
+}
+
+type DailyBoxOfficeInfo struct {
+	Rnum          string `json:"rnum"`
+	Rank          string `json:"rank"`
+	RankInten     string `json:"rankInten"`
+	RankOldAndNew string `json:"rankOldAndNew"`
+	MovieCd       string `json:"movieCd"`
+	MovieNm       string `json:"movieNm"`
+	OpenDt        string `json:"openDt"`
+	SalesAmt      string `json:"salesAmt"`
+	SalesShare    string `json:"salesShare"`
+	SalesInten    string `json:"salesInten"`
+	SalesChange   string `json:"salesChange"`
+	SalesAcc      string `json:"salesAcc"`
+	AudiCnt       string `json:"audiCnt"`
+	AudiInten     string `json:"audiInten"`
+	AudiChange    string `json:"audiChange"`
+	AudiAcc       string `json:"audiAcc"`
+	ScrnCnt       string `json:"scrnCnt"`
+	ShowCnt       string `json:"showCnt"`
+}
+
+type MovieDailyTable struct {
+	DailyID       int
+	BoxofficeType *string
+	ShowRange     *string
+	Rnum          *int
+	Rank          *int
+	RankInten     *int
+	RankOldAndNew *string
+	MovieCd       string
+	MovieNm       *string
+	OpenDt        *string
+	SalesAmt      *int
+	SalesShare    *float64
+	SalesInten    *int
+	SalesChange   *float64
+	SalesAcc      *int
+	AudiCnt       *int
+	AudiInten     *int
+	AudiChange    *float64
+	AudiAcc       *int
+	ScrnCnt       *int
+	ShowCnt       *int
+	UpdatedAt     *string
+	CreatedAt     string
+
+	Detail *MovieDetailTable
+}
+
+func (this *MovieDailyModel) Total() int {
+	var (
+		db    = service.DB_MOVIEW
+		conn  = db.SQLDB
+		query = "SELECT COUNT(*) FROM tb_movie_daily;"
+		total = 0
+	)
+
+	err := conn.QueryRow(query).Scan(&total)
+	if err != nil {
+		db.SetErrorLog(err, query)
+		return total
+	}
+
+	db.SetGeneralLog(config.GL_ACTION_SELECT, query, "select total movie daily")
+	return total
+}
+
+func (this *MovieDailyModel) List(ids string) ([]MovieDailyTable, error) {
+	var (
+		db    = service.DB_MOVIEW
+		conn  = db.SQLDB
+		query = fmt.Sprintf("SELECT * FROM tb_movie_daily WHERE id IN (%s);", ids)
+		list  = make([]MovieDailyTable, 0)
+	)
+
+	rows, err := conn.Query(query)
+	if err != nil {
+		db.SetErrorLog(err, query)
+		return list, err
+	}
+	defer rows.Close()
+
+	for rows.Next() {
+		var row MovieDailyTable
+		if err = rows.Scan(
+			&row.DailyID, &row.BoxofficeType, &row.ShowRange, &row.Rnum, &row.Rank, &row.RankInten,
+			&row.RankOldAndNew, &row.MovieCd, &row.MovieNm, &row.OpenDt, &row.SalesAmt,
+			&row.SalesShare, &row.SalesInten, &row.SalesChange, &row.SalesAcc, &row.AudiCnt,
+			&row.AudiInten, &row.AudiChange, &row.AudiAcc, &row.ScrnCnt, &row.ShowCnt,
+			&row.UpdatedAt, &row.CreatedAt); err != nil {
+			return list, err
+		}
+		list = append(list, row)
+	}
+
+	if err = rows.Err(); err != nil {
+		return list, err
+	}
+
+	db.SetGeneralLog(config.GL_ACTION_SELECT, query, "select movie daily")
+	return list, nil
+}
+
+func (this *MovieDailyModel) Insert(list SearchDailyBoxOfficeList) error {
+	if len(list.BoxOfficeResult.DailyBoxOfficeList) <= 0 {
+		return nil
+	}
+
+	var (
+		db    = service.DB_MOVIEW
+		conn  = db.SQLDB
+		query = `
+			INSERT INTO tb_movie_daily (
+				box_office_type, show_range, rnum, rank, rank_Inten, 
+			    rank_old_and_new, movie_cd, movie_nm, open_dt, sales_amt, 
+			    sales_share, sales_Inten, sales_change, sales_acc,
+			    audi_cnt, audi_Inten, audi_change, audi_acc,
+			    scrn_cnt, show_cnt, updated_at, created_at
+			)
+			VALUES 
+		`
+		vals = []interface{}{}
+		dup  string
+	)
+
+	for _, row := range list.BoxOfficeResult.DailyBoxOfficeList {
+		query += `(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NOW()),`
+
+		data := this.DataFilter(list.BoxOfficeResult.BoxofficeType, list.BoxOfficeResult.ShowRange, row)
+
+		vals = append(vals,
+			data.BoxofficeType, data.ShowRange, data.Rnum, data.Rank, data.RankInten,
+			data.RankOldAndNew, data.MovieCd, data.MovieNm, data.OpenDt, data.SalesAmt,
+			data.SalesShare, data.SalesInten, data.SalesChange, data.SalesAcc,
+			data.AudiCnt, data.AudiInten, data.AudiChange, data.AudiAcc,
+			data.ScrnCnt, data.ShowCnt)
+
+		dup += `box_office_type = VALUES(box_office_type), show_range = VALUES(show_range), rnum = VALUES(rnum),
+				rank = VALUES(rank), rank_Inten = VALUES(rank_Inten), rank_old_and_new = VALUES(rank_old_and_new), 
+				movie_cd = VALUES(movie_cd), movie_nm = VALUES(movie_nm), open_dt = VALUES(open_dt),
+				sales_amt = VALUES(sales_amt), sales_share = VALUES(sales_share), sales_Inten = VALUES(sales_Inten),
+				sales_change = VALUES(sales_change), sales_acc = VALUES(sales_acc), audi_cnt = VALUES(audi_cnt),
+				audi_Inten = VALUES(audi_Inten), audi_change = VALUES(audi_change), audi_acc = VALUES(audi_acc),
+				scrn_cnt = VALUES(scrn_cnt), show_cnt = VALUES(show_cnt), updated_at = NOW(),`
+	}
+
+	query = query[0 : len(query)-1]
+	dup = dup[0 : len(dup)-1]
+
+	query += `ON DUPLICATE KEY UPDATE ` + dup
+
+	stmt, err := conn.Prepare(query)
+	if err != nil {
+		db.SetErrorLog(err, query)
+		return err
+	}
+	defer stmt.Close()
+
+	_, err = stmt.Exec(vals...)
+	if err != nil {
+		db.SetErrorLog(err, query)
+		return err
+	}
+
+	db.SetGeneralLog(config.GL_ACTION_WRITE, query, "insert movie daily")
+	return nil
+}
+
+func (this *MovieDailyModel) LastInsertIDs(movieCd []string, targetDt string) (string, error) {
+	if movieCd == nil || targetDt == "" {
+		return "", errors.New("날짜를 지정해주세요.")
+	}
+
+	var (
+		db    = service.DB_MOVIEW
+		conn  = db.SQLDB
+		query = fmt.Sprintf("SELECT id FROM tb_movie_daily WHERE show_range = CONCAT(?, '~', ?) AND movie_cd IN (%s);", strings.Join(movieCd, ", "))
+		ids   = make([]string, 0)
+	)
+
+	rows, err := conn.Query(query, targetDt, targetDt)
+	if err != nil && err != sql.ErrNoRows {
+		db.SetErrorLog(err, query)
+		return "", err
+	}
+
+	for rows.Next() {
+		var id string
+		if err = rows.Scan(&id); err != nil {
+			return "", err
+		}
+		ids = append(ids, id)
+	}
+
+	if err = rows.Err(); err != nil {
+		return "", err
+	}
+
+	db.SetGeneralLog(config.GL_ACTION_SELECT, query, "select ids movie daily")
+
+	return strings.Join(ids, ","), nil
+}
+
+func (this *MovieDailyModel) DataFilter(boxofficeType, showRange string, row DailyBoxOfficeInfo) MovieDailyTable {
+	var result MovieDailyTable
+	result.MovieCd = row.MovieCd
+	result.BoxofficeType = &boxofficeType
+	result.ShowRange = &showRange
+
+	if row.Rnum != "" {
+		rnum, _ := strconv.Atoi(row.Rnum)
+		result.Rnum = &rnum
+	} else {
+		result.Rnum = nil
+	}
+
+	if row.Rank != "" {
+		rank, _ := strconv.Atoi(row.Rank)
+		result.Rank = &rank
+	} else {
+		result.Rank = nil
+	}
+
+	if row.RankInten != "" {
+		rankInten, _ := strconv.Atoi(row.RankInten)
+		result.RankInten = &rankInten
+	} else {
+		result.RankInten = nil
+	}
+
+	if row.RankOldAndNew != "" {
+		result.RankOldAndNew = &row.RankOldAndNew
+	} else {
+		result.RankOldAndNew = nil
+	}
+
+	if row.MovieNm != "" {
+		result.MovieNm = &row.MovieNm
+	} else {
+		result.MovieNm = nil
+	}
+
+	if row.OpenDt != "" {
+		result.OpenDt = &row.OpenDt
+	} else {
+		result.OpenDt = nil
+	}
+
+	if row.SalesAmt != "" {
+		salesAmt, _ := strconv.Atoi(row.SalesAmt)
+		result.SalesAmt = &salesAmt
+	} else {
+		result.SalesAmt = nil
+	}
+
+	if row.SalesShare != "" {
+		salesShare, _ := strconv.ParseFloat(row.SalesShare, 8)
+		result.SalesShare = &salesShare
+	} else {
+		result.SalesShare = nil
+	}
+
+	if row.SalesInten != "" {
+		salesInten, _ := strconv.Atoi(row.SalesInten)
+		result.SalesInten = &salesInten
+	} else {
+		result.SalesInten = nil
+	}
+
+	if row.SalesChange != "" {
+		salesChange, _ := strconv.ParseFloat(row.SalesChange, 8)
+		result.SalesChange = &salesChange
+	} else {
+		result.SalesChange = nil
+	}
+
+	if row.SalesAcc != "" {
+		salesAcc, _ := strconv.Atoi(row.SalesAcc)
+		result.SalesAcc = &salesAcc
+	} else {
+		result.SalesAcc = nil
+	}
+
+	if row.AudiCnt != "" {
+		audiCnt, _ := strconv.Atoi(row.AudiCnt)
+		result.AudiCnt = &audiCnt
+	} else {
+		result.AudiCnt = nil
+	}
+
+	if row.AudiInten != "" {
+		audiInten, _ := strconv.Atoi(row.AudiInten)
+		result.AudiInten = &audiInten
+	} else {
+		result.AudiInten = nil
+	}
+
+	if row.AudiChange != "" {
+		audiChange, _ := strconv.ParseFloat(row.AudiChange, 8)
+		result.AudiChange = &audiChange
+	} else {
+		result.AudiChange = nil
+	}
+
+	if row.AudiAcc != "" {
+		audiAcc, _ := strconv.Atoi(row.AudiAcc)
+		result.AudiAcc = &audiAcc
+	} else {
+		result.AudiAcc = nil
+	}
+
+	if row.ScrnCnt != "" {
+		scrnCnt, _ := strconv.Atoi(row.ScrnCnt)
+		result.ScrnCnt = &scrnCnt
+	} else {
+		result.ScrnCnt = nil
+	}
+
+	if row.ShowCnt != "" {
+		showCnt, _ := strconv.Atoi(row.ShowCnt)
+		result.ShowCnt = &showCnt
+	} else {
+		result.ShowCnt = nil
+	}
+
+	return result
+}
+
+func (this *MovieDailyModel) MakeParams(req SearchDailyBoxOfficeParams) (string, error) {
+	params, err := json.Marshal(DailyBoxOfficeParams{
+		TargetDt:     req.TargetDt,
+		ItemPerPage:  req.ItemPerPage,
+		MultiMovieYn: req.MultiMovieYn,
+		RepNationCd:  req.RepNationCd,
+		WideAreaCd:   req.WideAreaCd,
+	})
+	return string(params), err
+}
+
+func (this *MovieDailyModel) Info(dailyID int) (MovieDailyTable, error) {
+	var (
+		db    = service.DB_MOVIEW
+		conn  = db.SQLDB
+		query = "SELECT * FROM tb_movie_daily WHERE id = ?;"
+		info  MovieDailyTable
+	)
+
+	err := conn.QueryRow(query, dailyID).Scan(
+		&info.DailyID, &info.BoxofficeType, &info.ShowRange, &info.Rnum,
+		&info.Rank, &info.RankInten, &info.RankOldAndNew,
+		&info.MovieCd, &info.MovieNm, &info.OpenDt,
+		&info.SalesAmt, &info.SalesShare, &info.SalesInten, &info.SalesChange, &info.SalesAcc,
+		&info.AudiCnt, &info.AudiInten, &info.AudiChange, &info.AudiAcc,
+		&info.ScrnCnt, &info.ShowCnt, &info.UpdatedAt, &info.CreatedAt,
+	)
+
+	if err != nil && err != sql.ErrNoRows {
+		db.SetErrorLog(err, query)
+		return info, err
+	}
+
+	db.SetGeneralLog(config.GL_ACTION_SELECT, query, "select movie daily")
+	return info, nil
+}

+ 192 - 0
model/movieDetail.go

@@ -0,0 +1,192 @@
+package model
+
+import (
+	"crawler/config"
+	"crawler/service"
+	"database/sql"
+	"encoding/json"
+)
+
+type MovieDetailModel struct {
+	MovieDetail
+	Poster
+	StillCut
+	MovieDetailTable
+}
+
+type MovieDetail struct {
+	MovieCd  string
+	MainImg  string
+	ThumbImg string
+	Poster   []Poster
+	StillCut []StillCut
+	Synopsis string
+	SaleAcc  int
+	AudiAcc  int
+}
+
+type Poster struct {
+	Thumb  string
+	Origin string
+}
+
+type StillCut struct {
+	Thumb  string
+	Origin string
+}
+
+type MovieDetailTable struct {
+	DetailID  int
+	MovieCd   string
+	MainImg   *string
+	ThumbImg  *string
+	Poster    *string
+	StillCut  *string
+	Synopsis  *string
+	SaleAcc   *int
+	AudiAcc   *int
+	UpdatedAt *string
+	CreatedAt string
+}
+
+func (this *MovieDetailModel) Insert(row MovieDetail) error {
+	var (
+		db    = service.DB_MOVIEW
+		conn  = db.SQLDB
+		query = `
+			INSERT INTO tb_movie_detail (
+				movie_cd, main_img, thumb_img, poster, stillcut, 
+			    synopsis, sale_acc, audi_acc, updated_at, created_at
+			)
+			VALUES 
+				(?, ?, ?, ?, ?, ?, ?, ?, NULL, NOW())
+			ON DUPLICATE KEY UPDATE 
+				main_img = VALUES(main_img), thumb_img = VALUES(thumb_img), 
+				poster = VALUES(poster), stillcut = VALUES(stillcut), 
+				synopsis = VALUES(synopsis), sale_acc = VALUES(sale_acc), 
+				audi_acc = VALUES(audi_acc), updated_at = NOW();
+		`
+		data = this.DataFilter(row)
+	)
+
+	_, err := conn.Exec(query, data.MovieCd, data.MainImg, data.ThumbImg, data.Poster, data.StillCut, data.Synopsis, data.SaleAcc, data.AudiAcc)
+	if err != nil {
+		db.SetErrorLog(err, query)
+		return err
+	}
+
+	db.SetGeneralLog(config.GL_ACTION_WRITE, query, "insert movie detail")
+	return nil
+}
+
+func (this *MovieDetailModel) IsExists(movieCd string) bool {
+	var (
+		db     = service.DB_MOVIEW
+		conn   = db.SQLDB
+		query  = "SELECT IF(COUNT(*) <= 0, 0, 1) AS `exists` FROM tb_movie_detail WHERE movie_cd = ? AND thumb_img IS NOT NULL;"
+		exists = false
+	)
+
+	err := conn.QueryRow(query, movieCd).Scan(&exists)
+	if err != nil {
+		db.SetErrorLog(err, query)
+		return exists
+	}
+
+	db.SetGeneralLog(config.GL_ACTION_SELECT, query, "select exists movie detail")
+	return exists
+}
+
+func (this *MovieDetailModel) Update(row MovieDetail) error {
+	var (
+		db    = service.DB_MOVIEW
+		conn  = db.SQLDB
+		query = `
+			UPDATE tb_movie_detail SET 
+				main_img = ?, thumb_img = ?, poster = ?, stillcut = ?, 
+			    synopsis = ?, sale_acc = ?, audi_acc = ?, updated_at = NOW()
+			WHERE movie_cd = ?;
+		`
+		data = this.DataFilter(row)
+	)
+
+	_, err := conn.Exec(query,
+		data.MainImg, data.ThumbImg, data.Poster, data.StillCut,
+		data.Synopsis, data.SaleAcc, data.AudiAcc, data.MovieCd)
+	if err != nil {
+		db.SetErrorLog(err, query)
+		return err
+	}
+
+	db.SetGeneralLog(config.GL_ACTION_MODIFY, query, "update movie detail")
+	return nil
+}
+
+func (this *MovieDetailModel) DataFilter(row MovieDetail) MovieDetailTable {
+	var result MovieDetailTable
+	result.MovieCd = row.MovieCd
+
+	if row.MainImg != "" && row.MainImg != "#" {
+		result.MainImg = &row.MainImg
+	} else {
+		result.MainImg = nil
+	}
+
+	if row.ThumbImg != "" && row.ThumbImg != "#" {
+		result.ThumbImg = &row.ThumbImg
+	} else {
+		result.ThumbImg = nil
+	}
+
+	if len(row.Poster) > 0 {
+		poster, _ := json.Marshal(row.Poster)
+		s := string(poster)
+		result.Poster = &s
+	}
+
+	if len(row.StillCut) > 0 {
+		stillCut, _ := json.Marshal(row.StillCut)
+		s := string(stillCut)
+		result.StillCut = &s
+	}
+
+	if row.Synopsis != "" {
+		result.Synopsis = &row.Synopsis
+	} else {
+		result.Synopsis = nil
+	}
+
+	if row.SaleAcc != 0 {
+		result.SaleAcc = &row.SaleAcc
+	}
+
+	if row.AudiAcc != 0 {
+		result.AudiAcc = &row.AudiAcc
+	}
+
+	return result
+}
+
+func (this *MovieDetailModel) Info(movieCd string) (MovieDetailTable, error) {
+	var (
+		db     = service.DB_MOVIEW
+		conn   = db.SQLDB
+		query  = "SELECT * FROM tb_movie_detail WHERE movie_cd = ?"
+		detail MovieDetailTable
+	)
+
+	err := conn.QueryRow(query, movieCd).Scan(
+		&detail.DetailID, &detail.MovieCd, &detail.MainImg,
+		&detail.ThumbImg, &detail.Poster, &detail.StillCut,
+		&detail.Synopsis, &detail.SaleAcc, &detail.AudiAcc,
+		&detail.UpdatedAt, &detail.CreatedAt,
+	)
+
+	if err != nil && err != sql.ErrNoRows {
+		db.SetErrorLog(err, query)
+		return detail, err
+	}
+
+	db.SetGeneralLog(config.GL_ACTION_SELECT, query, "select movie detail")
+	return detail, nil
+}

+ 358 - 0
model/movieInfo.go

@@ -0,0 +1,358 @@
+package model
+
+import (
+	"crawler/config"
+	"crawler/service"
+	"database/sql"
+	"encoding/json"
+	"fmt"
+	"strconv"
+)
+
+type MovieInfoModel struct {
+	SearchMovieInfoParams
+	SearchMovieInfo
+	MovieInfo
+	Nations
+	Genres
+	Directors
+	Actors
+	ShowTypes
+	Companys
+	Audits
+	Staffs
+}
+
+// 일별 박스오피스 검색 변수
+type SearchMovieInfoParams struct {
+	Key     string `form:"key" url:"key" binding:"required"`
+	MovieCd string `form:"movieCd" url:"movieCd" binding:"required"`
+}
+
+type SearchMovieInfo struct {
+	MovieInfoResult struct {
+		Source    string    `json:"source"`
+		MovieInfo MovieInfo `json:"movieInfo"`
+	} `json:"movieInfoResult"`
+}
+
+type MovieInfo struct {
+	MovieCd    string      `json:"movieCd"`
+	MovieNm    string      `json:"movieNm"`
+	MovieNmEn  string      `json:"movieNmEn"`
+	MovieNmOg  string      `json:"movieNmOg"`
+	ShowTm     string      `json:"showTm"`
+	PrdtYear   string      `json:"prdtYear"`
+	OpenDt     string      `json:"openDt"`
+	PrdtStatNm string      `json:"prdtStatNm"`
+	TypeNm     string      `json:"typeNm"`
+	Nations    []Nations   `json:"nations"`
+	Genres     []Genres    `json:"genres"`
+	Directors  []Directors `json:"directors"`
+	Actors     []Actors    `json:"actors"`
+	ShowTypes  []ShowTypes `json:"showTypes"`
+	Companys   []Companys  `json:"companys"`
+	Audits     []Audits    `json:"audits"`
+	Staffs     []Staffs    `json:"staffs"`
+}
+
+type Nations struct {
+	NationNm string `json:"nationNm"`
+}
+
+type Genres struct {
+	GenreNm string `json:"genreNm"`
+}
+
+type Directors struct {
+	PeopleNm   string `json:"peopleNm"`
+	PeopleNmEn string `json:"peopleNmEn"`
+}
+
+type Actors struct {
+	PeopleNm   string `json:"peopleNm"`
+	PeopleNmEn string `json:"peopleNmEn"`
+	Cast       string `json:"cast"`
+	CastEn     string `json:"castEn"`
+}
+
+type ShowTypes struct {
+	ShowTypeGroupNm string `json:"showTypeGroupNm"`
+	ShowTypeNm      string `json:"showTypeNm"`
+}
+
+type Companys struct {
+	CompanyCd     string `json:"companyCd"`
+	CompanyNm     string `json:"companyNm"`
+	CompanyNmEn   string `json:"companyNmEn"`
+	CompanyPartNm string `json:"companyPartNm"`
+}
+
+type Audits struct {
+	AuditNo      string `json:"auditNo"`
+	WatchGradeNm string `json:"watchGradeNm"`
+}
+
+type Staffs struct {
+	PeopleNm    string `json:"peopleNm"`
+	PeopleNmEn  string `json:"peopleNmEn"`
+	StaffRoleNm string `json:"staffRoleNm"`
+}
+
+type MovieInfoTable struct {
+	InfoID     int
+	MovieCd    string
+	MovieNm    *string
+	MovieNmEn  *string
+	MovieNmOg  *string
+	PrdtYear   *int
+	ShowTm     *int
+	OpenDt     *int
+	PrdtStatNm *string
+	TypeNm     *string
+	Nations    *string
+	Genres     *string
+	Directors  *string
+	Actors     *string
+	ShowTypes  *string
+	Companys   *string
+	Audits     *string
+	Staffs     *string
+	UpdatedAt  *string
+	CreatedAt  string
+}
+
+func (this *MovieInfoModel) Insert(row MovieInfo) error {
+	var (
+		db    = service.DB_MOVIEW
+		conn  = db.SQLDB
+		query = `
+			INSERT INTO tb_movie_info (
+				movie_cd, movie_nm, movie_nm_en, movie_nm_og, prdt_year, 
+			    show_tm, open_dt, prdt_stat_nm, type_nm, nations, 
+			    genres, directors, actors, show_types, companys, 
+			   	audits, staffs, updated_at, created_at
+			)
+			VALUES 
+				(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NOW())
+			ON DUPLICATE KEY UPDATE 
+			    movie_nm = VALUES(movie_nm), movie_nm_en = VALUES(movie_nm_en), movie_nm_og = VALUES(movie_nm_og), 
+			    prdt_year = VALUES(prdt_year), show_tm = VALUES(show_tm), open_dt = VALUES(open_dt), 
+			    prdt_stat_nm = VALUES(prdt_stat_nm), type_nm = VALUES(type_nm), nations = VALUES(nations), 
+				genres = VALUES(genres), directors = VALUES(directors), actors = VALUES(actors), 
+				show_types = VALUES(show_types), companys = VALUES(companys), audits = VALUES(audits), 
+				staffs = VALUES(staffs), updated_at = NOW();
+		`
+		data = this.DataFilter(row)
+	)
+
+	_, err := conn.Exec(query,
+		data.MovieCd, data.MovieNm, data.MovieNmEn, data.MovieNmOg,
+		data.PrdtYear, data.ShowTm, data.OpenDt, data.PrdtStatNm, data.TypeNm,
+		data.Nations, data.Genres, data.Directors, data.Actors, data.ShowTypes, data.Companys,
+		data.Audits, data.Staffs)
+	if err != nil {
+		db.SetErrorLog(err, query)
+		fmt.Println(err)
+		return err
+	}
+
+	db.SetGeneralLog(config.GL_ACTION_WRITE, query, "insert movie info")
+	return nil
+}
+
+func (this *MovieInfoModel) IsExists(movieCd string) bool {
+	var (
+		db     = service.DB_MOVIEW
+		conn   = db.SQLDB
+		query  = "SELECT IF(COUNT(*) <= 0, 0, 1) AS `exists` FROM tb_movie_info WHERE movie_cd = ?;"
+		exists = false
+	)
+
+	err := conn.QueryRow(query, movieCd).Scan(&exists)
+	if err != nil {
+		db.SetErrorLog(err, query)
+		return exists
+	}
+
+	db.SetGeneralLog(config.GL_ACTION_SELECT, query, "select exists movie info")
+	return exists
+}
+
+func (this *MovieInfoModel) Update(row MovieInfo) error {
+	var (
+		db    = service.DB_MOVIEW
+		conn  = db.SQLDB
+		query = `
+			UPDATE tb_movie_info SET 
+				movie_nm = ?, movie_nm_en = ?, movie_nm_og = ?, prdt_year = ?, 
+			    show_tm = ?, open_dt = ?, prdt_stat_nm = ?, type_nm = ?, nations = ?, 
+			    genres = ?, directors = ?, actors = ?, show_types = ?, companys = ?, 
+			   	audits = ?, staffs = ?, updated_at = NOW()
+			WHERE movie_cd = ?;
+		`
+		data = this.DataFilter(row)
+	)
+
+	_, err := conn.Exec(query,
+		data.MovieNm, data.MovieNmEn, data.MovieNmOg,
+		data.PrdtYear, data.ShowTm, data.OpenDt, data.PrdtStatNm, data.TypeNm,
+		data.Nations, data.Genres, data.Directors, data.Actors, data.ShowTypes, data.Companys,
+		data.Audits, data.Staffs, data.MovieCd)
+	if err != nil {
+		db.SetErrorLog(err, query)
+		return err
+	}
+
+	db.SetGeneralLog(config.GL_ACTION_MODIFY, query, "update movie info")
+	return nil
+}
+
+func (this *MovieInfoModel) DataFilter(row MovieInfo) MovieInfoTable {
+	var result MovieInfoTable
+	result.MovieCd = row.MovieCd
+
+	if row.MovieNm != "" {
+		result.MovieNm = &row.MovieNm
+	} else {
+		result.MovieNm = nil
+	}
+
+	if row.MovieNmEn != "" {
+		result.MovieNmEn = &row.MovieNmEn
+	} else {
+		result.MovieNmEn = nil
+	}
+
+	if row.MovieNmOg != "" {
+		result.MovieNmOg = &row.MovieNmOg
+	} else {
+		result.MovieNmOg = nil
+	}
+
+	if row.PrdtYear != "" {
+		prdtYear, _ := strconv.Atoi(row.PrdtYear)
+		result.PrdtYear = &prdtYear
+	} else {
+		result.PrdtYear = nil
+	}
+
+	if row.ShowTm != "" {
+		showTm, _ := strconv.Atoi(row.ShowTm)
+		result.ShowTm = &showTm
+	} else {
+		result.ShowTm = nil
+	}
+
+	if row.OpenDt != "" {
+		openDt, _ := strconv.Atoi(row.OpenDt)
+		result.OpenDt = &openDt
+	} else {
+		result.OpenDt = nil
+	}
+
+	if row.PrdtStatNm != "" {
+		result.PrdtStatNm = &row.PrdtStatNm
+	} else {
+		result.PrdtStatNm = nil
+	}
+
+	if row.TypeNm != "" {
+		result.TypeNm = &row.TypeNm
+	} else {
+		result.TypeNm = nil
+	}
+
+	if len(row.Nations) > 0 {
+		nations, _ := json.Marshal(row.Nations)
+		s := string(nations)
+		result.Nations = &s
+	} else {
+		result.Nations = nil
+	}
+
+	if len(row.Genres) > 0 {
+		genres, _ := json.Marshal(row.Genres)
+		s := string(genres)
+		result.Genres = &s
+	} else {
+		result.Genres = nil
+	}
+
+	if len(row.Directors) > 0 {
+		directors, _ := json.Marshal(row.Directors)
+		s := string(directors)
+		result.Directors = &s
+	} else {
+		result.Directors = nil
+	}
+
+	if len(row.Actors) > 0 {
+		actors, _ := json.Marshal(row.Actors)
+		s := string(actors)
+		result.Actors = &s
+	} else {
+		result.Actors = nil
+	}
+
+	if len(row.ShowTypes) > 0 {
+		showtypes, _ := json.Marshal(row.ShowTypes)
+		s := string(showtypes)
+		result.ShowTypes = &s
+	} else {
+		result.ShowTypes = nil
+	}
+
+	if len(row.Companys) > 0 {
+		companys, _ := json.Marshal(row.Companys)
+		s := string(companys)
+		result.Companys = &s
+	} else {
+		result.Companys = nil
+	}
+
+	if len(row.Audits) > 0 {
+		audits, _ := json.Marshal(row.Audits)
+		s := string(audits)
+		result.Audits = &s
+	} else {
+		result.Audits = nil
+	}
+
+	if len(row.Staffs) > 0 {
+		staffs, _ := json.Marshal(row.Staffs)
+		s := string(staffs)
+		result.Staffs = &s
+	} else {
+		result.Staffs = nil
+	}
+
+	return result
+}
+
+func (this *MovieInfoModel) Info(movieCd string) (MovieInfoTable, error) {
+	var (
+		db    = service.DB_MOVIEW
+		conn  = db.SQLDB
+		query = "SELECT * FROM tb_movie_info WHERE movie_cd = ? LIMIT 1;"
+		info  MovieInfoTable
+	)
+
+	err := conn.QueryRow(query, movieCd).Scan(
+		&info.InfoID, &info.MovieCd, &info.MovieNm,
+		&info.MovieNmEn, &info.MovieNmOg, &info.PrdtYear,
+		&info.ShowTm, &info.OpenDt, &info.PrdtStatNm,
+		&info.TypeNm, &info.Nations, &info.Genres,
+		&info.Directors, &info.Actors, &info.ShowTypes,
+		&info.Companys, &info.Audits, &info.Staffs,
+		&info.UpdatedAt, &info.CreatedAt,
+	)
+
+	if err != nil && err != sql.ErrNoRows {
+		db.SetErrorLog(err, query)
+		return info, err
+	}
+
+	db.SetGeneralLog(config.GL_ACTION_SELECT, query, "select movie info")
+	return info, nil
+}

+ 96 - 0
model/movieSearch.go

@@ -0,0 +1,96 @@
+package model
+
+import (
+	"crawler/config"
+	"crawler/service"
+	"database/sql"
+)
+
+type MovieSearchModel struct {
+	MovieSearch
+	MovieListParams
+	DailyBoxOfficeParams
+	WeeklyBoxOfficeListParams
+}
+
+type MovieSearch struct {
+	SearchID string
+	Params   string
+	IDs      string
+}
+
+// 영화 목록 검색 변수
+type MovieListParams struct {
+	CurPage       any `json:"curPage"`
+	ItemPerPage   any `json:"itemPerPage"`
+	MovieNm       any `json:"movieNm"`
+	DirectorNm    any `json:"directorNm"`
+	OpenStartDt   any `json:"openStartDt"`
+	OpenEndDt     any `json:"openEndDt"`
+	PrdtStartYear any `json:"prdtStartYear"`
+	PrdtEndYear   any `json:"prdtEndYear"`
+	RepNationCd   any `json:"repNationCd"`
+	MovieTypeCd   any `json:"movieTypeCd"`
+}
+
+// 일별 박스오피스 검색 변수
+type DailyBoxOfficeParams struct {
+	TargetDt     any `json:"targetDt"`
+	ItemPerPage  any `json:"itemPerPage"`
+	MultiMovieYn any `json:"multiMovieYn"`
+	RepNationCd  any `json:"repNationCd"`
+	WideAreaCd   any `json:"wideAreaCd"`
+}
+
+// 주간/주말 박스오피스 검색 변수
+type WeeklyBoxOfficeListParams struct {
+	TargetDt     any `json:"targetDt"`
+	WeekGb       any `json:"weekGb"`
+	ItemPerPage  any `json:"itemPerPage"`
+	MultiMovieYn any `json:"multiMovieYn"`
+	RepNationCd  any `json:"repNationCd"`
+	WideAreaCd   any `json:"wideAreaCd"`
+}
+
+func (this *MovieSearchModel) SelectIDs(params string) (string, error) {
+	var (
+		db    = service.DB_MOVIEW
+		conn  = db.SQLDB
+		ids   string
+		query = `SELECT ids FROM tb_movie_search WHERE params = ?`
+	)
+
+	err := conn.QueryRow(query, params).Scan(&ids)
+	if err != nil && err != sql.ErrNoRows {
+		db.SetErrorLog(err, query)
+		return ids, err
+	}
+
+	db.SetGeneralLog(config.GL_ACTION_SELECT, query, "select ids movie search")
+	return ids, nil
+}
+
+func (this *MovieSearchModel) Insert(row MovieSearch) error {
+	var (
+		db    = service.DB_MOVIEW
+		conn  = db.SQLDB
+		query = `
+			INSERT INTO tb_movie_search (
+				params, ids, created_at
+			)
+			VALUES 
+				(?, ?, NOW())
+			ON DUPLICATE KEY UPDATE 
+				params = VALUES(params), ids = VALUES(ids), created_at = VALUES(created_at);
+		`
+	)
+
+	_, err := conn.Exec(query, row.Params, row.IDs)
+	if err != nil {
+		db.SetErrorLog(err, query)
+		return err
+	}
+
+	db.SetGeneralLog(config.GL_ACTION_WRITE, query, "insert movie search")
+	return nil
+}

+ 239 - 0
model/movieStats.go

@@ -0,0 +1,239 @@
+package model
+
+import (
+	"crawler/config"
+	"crawler/service"
+	"net/url"
+	"regexp"
+	"strconv"
+)
+
+type MovieStatsModel struct {
+	SearchBoxOfficeParams
+	SearchBoxOfficeList
+	BoxOfficeInfo
+	MovieStatsTable
+}
+
+// 박스오피스 검색 변수
+type SearchBoxOfficeParams struct {
+	ServiceKey string `form:"serviceKey" url:"serviceKey" binding:"required"`
+	NumOfRows  int    `form:"numOfRows" url:"numOfRows,omitempty"`
+	PageNo     int    `form:"pageNo" url:"pageNo,omitempty"`
+}
+
+// 박스오피스 응답 변수
+type SearchBoxOfficeList struct {
+	Response struct {
+		Header struct {
+			ResultCode string `json:"resultCode"`
+			ResultMsg  string `json:"resultMsg"`
+		} `json:"header"`
+		Body struct {
+			Items struct {
+				Item []BoxOfficeInfo `json:"item"`
+			} `json:"items"`
+			NumOfRows  string `json:"numOfRows"`
+			PageNo     string `json:"pageNo"`
+			TotalCount string `json:"totalCount"`
+		} `json:"body"`
+	} `json:"response"`
+}
+
+type BoxOfficeInfo struct {
+	Title               string `json:"title"`
+	AlternativeTitle    string `json:"alternativeTitle"`
+	Creator             string `json:"creator"`
+	RegDate             string `json:"regDate"`
+	CollectionDb        string `json:"collectionDb"`
+	SubjectCategory     string `json:"subjectCategory"`
+	SubjectKeyword      string `json:"subjectKeyword"`
+	Extent              string `json:"extent"`
+	Description         string `json:"description"`
+	SpatialCoverage     string `json:"spatialCoverage"`
+	Temporal            string `json:"temporal"`
+	Person              string `json:"person"`
+	Language            string `json:"language"`
+	SourceTitle         string `json:"sourceTitle"`
+	ReferenceIdentifier string `json:"referenceIdentifier"`
+	Rights              string `json:"rights"`
+	CopyrightOthers     string `json:"copyrightOthers"`
+	Url                 string `json:"url"`
+	Contributor         string `json:"contributor"`
+}
+
+type MovieStatsTable struct {
+	StatsID   int
+	MovieCd   *string
+	MovieNm   *string
+	ShowDt    *int
+	SaleAcc   int
+	AudiAcc   int
+	ScrnCnt   int
+	ShowCnt   int
+	RegDate   *string
+	CreatedAt string
+}
+
+func (this *MovieStatsModel) Insert(list []BoxOfficeInfo) error {
+	if len(list) <= 0 {
+		return nil
+	}
+
+	var (
+		db    = service.DB_MOVIEW
+		conn  = db.SQLDB
+		query = `
+			INSERT INTO tb_movie_stats (
+				movie_cd, movie_nm, show_dt, sale_acc, audi_acc, scrn_cnt, show_cnt, reg_date, updated_at, created_at
+			)
+			VALUES 
+		`
+		vals = []interface{}{}
+		dup  string
+	)
+
+	for _, row := range list {
+		query += `(?, ?, ?, ?, ?, ?, ?, ?, NULL, NOW()),`
+
+		data := this.DataFilter(row)
+
+		vals = append(vals,
+			data.MovieCd, data.MovieNm, data.ShowDt, data.SaleAcc, data.AudiAcc, data.ScrnCnt, data.ShowCnt, data.RegDate)
+
+		dup += `movie_cd = VALUES(movie_cd), movie_nm = VALUES(movie_nm), show_dt = VALUES(show_dt), 
+				sale_acc = VALUES(sale_acc), audi_acc = VALUES(audi_acc), scrn_cnt = VALUES(scrn_cnt),  
+				show_cnt = VALUES(show_cnt), reg_date = VALUES(reg_date), updated_at = NOW(),`
+	}
+
+	query = query[0 : len(query)-1]
+	dup = dup[0 : len(dup)-1]
+
+	query += ` ON DUPLICATE KEY UPDATE ` + dup
+
+	_, _ = conn.Exec("LOCK TABLE tb_movie_stats READ;")
+
+	stmt, err := conn.Prepare(query)
+	if err != nil {
+		db.SetErrorLog(err, query)
+		return err
+	}
+	defer stmt.Close()
+
+	_, err = stmt.Exec(vals...)
+	if err != nil {
+		db.SetErrorLog(err, query)
+		return err
+	}
+
+	_, _ = conn.Exec("UNLOCK TABLES;")
+
+	db.SetGeneralLog(config.GL_ACTION_WRITE, query, "insert movie stats")
+	return nil
+}
+
+func (this *MovieStatsModel) IsExists(movieCd string) bool {
+	var (
+		db     = service.DB_MOVIEW
+		conn   = db.SQLDB
+		query  = "SELECT IF(COUNT(*) <= 0, 0, 1) AS `exists` FROM tb_movie_stats WHERE movie_cd = ?;"
+		exists = false
+	)
+
+	err := conn.QueryRow(query, movieCd).Scan(&exists)
+	if err != nil {
+		db.SetErrorLog(err, query)
+		return exists
+	}
+
+	db.SetGeneralLog(config.GL_ACTION_SELECT, query, "select exists movie stats")
+	return exists
+}
+
+func (this *MovieStatsModel) Update(list []BoxOfficeInfo) error {
+	if len(list) <= 0 {
+		return nil
+	}
+
+	var (
+		db    = service.DB_MOVIEW
+		conn  = db.SQLDB
+		query = `
+			UPDATE tb_movie_stats SET 
+				movie_cd = ?, movie_nm = ?, show_dt = ?, sale_acc = ?, audi_acc = ?, 
+				scrn_cnt = ?, show_cnt = ?, reg_date = ?, updated_at = NOW()
+			WHERE movie_cd = ?;
+		`
+	)
+
+	for _, row := range list {
+		data := this.DataFilter(row)
+		_, err := conn.Exec(query,
+			data.MovieCd, data.MovieNm, data.ShowDt, data.SaleAcc, data.AudiAcc, data.ScrnCnt, data.ShowCnt, data.RegDate, data.MovieCd)
+		if err != nil {
+			db.SetErrorLog(err, query)
+			return err
+		}
+	}
+
+	db.SetGeneralLog(config.GL_ACTION_MODIFY, query, "update movie stats")
+	return nil
+}
+
+func (this *MovieStatsModel) DataFilter(row BoxOfficeInfo) MovieStatsTable {
+	var result MovieStatsTable
+
+	query, err := url.ParseQuery(row.Url)
+	if err != nil {
+		return result
+	}
+
+	var (
+		movieCd = query.Get("dtCd")
+		showDt  = query.Get("showdt")
+	)
+
+	result.MovieCd = &movieCd
+
+	if row.Title != "" {
+		result.MovieNm = &row.Title
+	} else {
+		result.MovieNm = nil
+	}
+
+	if showDt != "" {
+		s, _ := strconv.Atoi(query.Get("showdt"))
+		result.ShowDt = &s
+	} else {
+		result.ShowDt = nil
+	}
+
+	// 매출액, 관객수, 스크린수, 상영횟수
+	re := regexp.MustCompile("[0-9]+")
+	stats := re.FindAllString(row.Description, -1)
+
+	if stats[0] != "" {
+		s, _ := strconv.Atoi(stats[0])
+		result.SaleAcc = s
+	}
+	if stats[1] != "" {
+		s, _ := strconv.Atoi(stats[1])
+		result.AudiAcc = s
+	}
+	if stats[2] != "" {
+		s, _ := strconv.Atoi(stats[2])
+		result.ScrnCnt = s
+	}
+	if stats[3] != "" {
+		s, _ := strconv.Atoi(stats[3])
+		result.ShowCnt = s
+	}
+
+	if row.RegDate != "" {
+		result.RegDate = &row.RegDate
+	} else {
+		result.RegDate = nil
+	}
+
+	return result
+}

+ 410 - 0
model/movieWeekly.go

@@ -0,0 +1,410 @@
+package model
+
+import (
+	"crawler/config"
+	"crawler/service"
+	"database/sql"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"strconv"
+	"strings"
+	"time"
+)
+
+type MovieWeeklyModel struct {
+	SearchWeeklyBoxOfficeParams
+	SearchWeeklyBoxOfficeList
+	WeeklyBoxOfficeInfo
+}
+
+// 일별 박스오피스 검색 변수
+type SearchWeeklyBoxOfficeParams struct {
+	Key          string `form:"key" url:"key" binding:"required"`
+	TargetDt     string `form:"targetDt" url:"targetDt" binding:"required"`
+	WeekGb       string `form:"weekGb" url:"weekGb,omitempty"`
+	ItemPerPage  string `form:"itemPerPage" url:"itemPerPage,omitempty"`
+	MultiMovieYn string `form:"multiMovieYn" url:"multiMovieYn,omitempty"`
+	RepNationCd  string `form:"repNationCd" url:"repNationCd,omitempty"`
+	WideAreaCd   string `form:"wideAreaCd" url:"wideAreaCd,omitempty"`
+}
+
+// 일별 박스오피스 응답 변수
+type SearchWeeklyBoxOfficeList struct {
+	BoxOfficeResult struct {
+		BoxofficeType       string                `json:"boxofficeType"`
+		ShowRange           string                `json:"showRange"`
+		YearWeekTime        string                `json:"yearWeekTime"`
+		WeeklyBoxOfficeList []WeeklyBoxOfficeInfo `json:"weeklyBoxOfficeList"`
+	} `json:"boxOfficeResult"`
+}
+
+type WeeklyBoxOfficeInfo struct {
+	Rnum          string `json:"rnum"`
+	Rank          string `json:"rank"`
+	RankInten     string `json:"rankInten"`
+	RankOldAndNew string `json:"rankOldAndNew"`
+	MovieCd       string `json:"movieCd"`
+	MovieNm       string `json:"movieNm"`
+	OpenDt        string `json:"openDt"`
+	SalesAmt      string `json:"salesAmt"`
+	SalesShare    string `json:"salesShare"`
+	SalesInten    string `json:"salesInten"`
+	SalesChange   string `json:"salesChange"`
+	SalesAcc      string `json:"salesAcc"`
+	AudiCnt       string `json:"audiCnt"`
+	AudiInten     string `json:"audiInten"`
+	AudiChange    string `json:"audiChange"`
+	AudiAcc       string `json:"audiAcc"`
+	ScrnCnt       string `json:"scrnCnt"`
+	ShowCnt       string `json:"showCnt"`
+}
+
+type MovieWeeklyTable struct {
+	WeeklyID      int
+	BoxofficeType *string
+	ShowRange     *string
+	YearWeekTime  *string
+	Rnum          *int
+	Rank          *int
+	RankInten     *int
+	RankOldAndNew *string
+	MovieCd       string
+	MovieNm       *string
+	OpenDt        *string
+	SalesAmt      *int
+	SalesShare    *float64
+	SalesInten    *int
+	SalesChange   *float64
+	SalesAcc      *int
+	AudiCnt       *int
+	AudiInten     *int
+	AudiChange    *float64
+	AudiAcc       *int
+	ScrnCnt       *int
+	ShowCnt       *int
+	UpdatedAt     *string
+	CreatedAt     string
+
+	Detail *MovieDetailTable
+}
+
+func (this *MovieWeeklyModel) Total() int {
+	var (
+		db    = service.DB_MOVIEW
+		conn  = db.SQLDB
+		query = "SELECT COUNT(*) FROM tb_movie_weekly;"
+		total = 0
+	)
+
+	err := conn.QueryRow(query).Scan(&total)
+	if err != nil {
+		db.SetErrorLog(err, query)
+		return total
+	}
+
+	db.SetGeneralLog(config.GL_ACTION_SELECT, query, "select total movie weekly")
+	return total
+}
+
+func (this *MovieWeeklyModel) List(ids string) ([]MovieWeeklyTable, error) {
+	var (
+		db    = service.DB_MOVIEW
+		conn  = db.SQLDB
+		query = fmt.Sprintf("SELECT * FROM tb_movie_weekly WHERE id IN (%s);", ids)
+		list  = make([]MovieWeeklyTable, 0)
+	)
+
+	rows, err := conn.Query(query)
+	if err != nil && err != sql.ErrNoRows {
+		db.SetErrorLog(err, query)
+		return list, err
+	}
+	defer rows.Close()
+
+	for rows.Next() {
+		var row MovieWeeklyTable
+		if err = rows.Scan(
+			&row.WeeklyID, &row.BoxofficeType, &row.ShowRange, &row.YearWeekTime, &row.Rnum, &row.Rank, &row.RankInten,
+			&row.RankOldAndNew, &row.MovieCd, &row.MovieNm, &row.OpenDt, &row.SalesAmt,
+			&row.SalesShare, &row.SalesInten, &row.SalesChange, &row.SalesAcc, &row.AudiCnt,
+			&row.AudiInten, &row.AudiChange, &row.AudiAcc, &row.ScrnCnt, &row.ShowCnt,
+			&row.UpdatedAt, &row.CreatedAt); err != nil {
+			return list, err
+		}
+		list = append(list, row)
+	}
+
+	if err = rows.Err(); err != nil {
+		return list, err
+	}
+
+	db.SetGeneralLog(config.GL_ACTION_SELECT, query, "select movie week")
+	return list, nil
+}
+
+func (this *MovieWeeklyModel) Insert(list SearchWeeklyBoxOfficeList) error {
+	if len(list.BoxOfficeResult.WeeklyBoxOfficeList) <= 0 {
+		return nil
+	}
+
+	var (
+		db    = service.DB_MOVIEW
+		conn  = db.SQLDB
+		query = `
+			INSERT INTO tb_movie_weekly (
+				box_office_type, show_range, year_week_time, rnum, rank, rank_Inten, 
+			    rank_old_and_new, movie_cd, movie_nm, open_dt, sales_amt, 
+			    sales_share, sales_Inten, sales_change, sales_acc,
+			    audi_cnt, audi_Inten, audi_change, audi_acc,
+			    scrn_cnt, show_cnt, updated_at, created_at
+			)
+			VALUES 
+		`
+		vals = []interface{}{}
+		dup  string
+	)
+
+	for _, row := range list.BoxOfficeResult.WeeklyBoxOfficeList {
+		query += `(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NOW()),`
+
+		data := this.DataFilter(list.BoxOfficeResult.BoxofficeType, list.BoxOfficeResult.ShowRange, list.BoxOfficeResult.YearWeekTime, row)
+
+		vals = append(vals,
+			data.BoxofficeType, data.ShowRange, data.YearWeekTime, data.Rnum, data.Rank, data.RankInten,
+			data.RankOldAndNew, data.MovieCd, data.MovieNm, data.OpenDt, data.SalesAmt,
+			data.SalesShare, data.SalesInten, data.SalesChange, data.SalesAcc,
+			data.AudiCnt, data.AudiInten, data.AudiChange, data.AudiAcc,
+			data.ScrnCnt, data.ShowCnt)
+
+		dup += `box_office_type = VALUES(box_office_type), show_range = VALUES(show_range), year_week_time = VALUES(year_week_time), 
+				rnum = VALUES(rnum), rank = VALUES(rank), rank_Inten = VALUES(rank_Inten), rank_old_and_new = VALUES(rank_old_and_new), 
+				movie_cd = VALUES(movie_cd), movie_nm = VALUES(movie_nm), open_dt = VALUES(open_dt),
+				sales_amt = VALUES(sales_amt), sales_share = VALUES(sales_share), sales_Inten = VALUES(sales_Inten),
+				sales_change = VALUES(sales_change), sales_acc = VALUES(sales_acc), audi_cnt = VALUES(audi_cnt),
+				audi_Inten = VALUES(audi_Inten), audi_change = VALUES(audi_change), audi_acc = VALUES(audi_acc),
+				scrn_cnt = VALUES(scrn_cnt), show_cnt = VALUES(show_cnt), updated_at = NOW(),`
+	}
+
+	query = query[0 : len(query)-1]
+	dup = dup[0 : len(dup)-1]
+
+	query += `ON DUPLICATE KEY UPDATE ` + dup
+
+	stmt, err := conn.Prepare(query)
+	if err != nil {
+		db.SetErrorLog(err, query)
+		return err
+	}
+	defer stmt.Close()
+
+	_, err = stmt.Exec(vals...)
+	if err != nil {
+		db.SetErrorLog(err, query)
+		return err
+	}
+
+	db.SetGeneralLog(config.GL_ACTION_WRITE, query, "insert movie week")
+	return nil
+}
+
+func (this *MovieWeeklyModel) LastInsertIDs(movieCd []string, targetDt string) (string, error) {
+	if movieCd == nil || targetDt == "" {
+		return "", errors.New("날짜를 지정해주세요.")
+	}
+
+	var (
+		db         = service.DB_MOVIEW
+		conn       = db.SQLDB
+		query      = fmt.Sprintf("SELECT id FROM tb_movie_weekly WHERE year_week_time = CONCAT(?, ?) AND movie_cd IN (%s);", strings.Join(movieCd, ", "))
+		ids        = make([]string, 0)
+		t, _       = time.Parse("20060102", targetDt)
+		year, week = t.ISOWeek()
+	)
+
+	rows, err := conn.Query(query, year, fmt.Sprintf("%02d", week))
+	if err != nil && err != sql.ErrNoRows {
+		db.SetErrorLog(err, query)
+		return "", err
+	}
+
+	for rows.Next() {
+		var id string
+		if err = rows.Scan(&id); err != nil {
+			return "", err
+		}
+		ids = append(ids, id)
+	}
+
+	if err = rows.Err(); err != nil {
+		return "", err
+	}
+
+	db.SetGeneralLog(config.GL_ACTION_SELECT, query, "select ids movie week")
+
+	return strings.Join(ids, ","), nil
+}
+
+func (this *MovieWeeklyModel) DataFilter(boxofficeType, showRange, yearWeekTime string, row WeeklyBoxOfficeInfo) MovieWeeklyTable {
+	var result MovieWeeklyTable
+	result.MovieCd = row.MovieCd
+	result.BoxofficeType = &boxofficeType
+	result.ShowRange = &showRange
+	result.YearWeekTime = &yearWeekTime
+
+	if row.Rnum != "" {
+		rnum, _ := strconv.Atoi(row.Rnum)
+		result.Rnum = &rnum
+	} else {
+		result.Rnum = nil
+	}
+
+	if row.Rank != "" {
+		rank, _ := strconv.Atoi(row.Rank)
+		result.Rank = &rank
+	} else {
+		result.Rank = nil
+	}
+
+	if row.RankInten != "" {
+		rankInten, _ := strconv.Atoi(row.RankInten)
+		result.RankInten = &rankInten
+	} else {
+		result.RankInten = nil
+	}
+
+	if row.RankOldAndNew != "" {
+		result.RankOldAndNew = &row.RankOldAndNew
+	} else {
+		result.RankOldAndNew = nil
+	}
+
+	if row.MovieNm != "" {
+		result.MovieNm = &row.MovieNm
+	} else {
+		result.MovieNm = nil
+	}
+
+	if row.OpenDt != "" {
+		result.OpenDt = &row.OpenDt
+	} else {
+		result.OpenDt = nil
+	}
+
+	if row.SalesAmt != "" {
+		salesAmt, _ := strconv.Atoi(row.SalesAmt)
+		result.SalesAmt = &salesAmt
+	} else {
+		result.SalesAmt = nil
+	}
+
+	if row.SalesShare != "" {
+		salesShare, _ := strconv.ParseFloat(row.SalesShare, 8)
+		result.SalesShare = &salesShare
+	} else {
+		result.SalesShare = nil
+	}
+
+	if row.SalesInten != "" {
+		salesInten, _ := strconv.Atoi(row.SalesInten)
+		result.SalesInten = &salesInten
+	} else {
+		result.SalesInten = nil
+	}
+
+	if row.SalesChange != "" {
+		salesChange, _ := strconv.ParseFloat(row.SalesChange, 8)
+		result.SalesChange = &salesChange
+	} else {
+		result.SalesChange = nil
+	}
+
+	if row.SalesAcc != "" {
+		salesAcc, _ := strconv.Atoi(row.SalesAcc)
+		result.SalesAcc = &salesAcc
+	} else {
+		result.SalesAcc = nil
+	}
+
+	if row.AudiCnt != "" {
+		audiCnt, _ := strconv.Atoi(row.AudiCnt)
+		result.AudiCnt = &audiCnt
+	} else {
+		result.AudiCnt = nil
+	}
+
+	if row.AudiInten != "" {
+		audiInten, _ := strconv.Atoi(row.AudiInten)
+		result.AudiInten = &audiInten
+	} else {
+		result.AudiInten = nil
+	}
+
+	if row.AudiChange != "" {
+		audiChange, _ := strconv.ParseFloat(row.AudiChange, 8)
+		result.AudiChange = &audiChange
+	} else {
+		result.AudiChange = nil
+	}
+
+	if row.AudiAcc != "" {
+		audiAcc, _ := strconv.Atoi(row.AudiAcc)
+		result.AudiAcc = &audiAcc
+	} else {
+		result.AudiAcc = nil
+	}
+
+	if row.ScrnCnt != "" {
+		scrnCnt, _ := strconv.Atoi(row.ScrnCnt)
+		result.ScrnCnt = &scrnCnt
+	} else {
+		result.ScrnCnt = nil
+	}
+
+	if row.ShowCnt != "" {
+		showCnt, _ := strconv.Atoi(row.ShowCnt)
+		result.ShowCnt = &showCnt
+	} else {
+		result.ShowCnt = nil
+	}
+
+	return result
+}
+
+func (this *MovieWeeklyModel) MakeParams(req SearchWeeklyBoxOfficeParams) (string, error) {
+	params, err := json.Marshal(WeeklyBoxOfficeListParams{
+		TargetDt:     req.TargetDt,
+		WeekGb:       req.WeekGb,
+		ItemPerPage:  req.ItemPerPage,
+		MultiMovieYn: req.MultiMovieYn,
+		RepNationCd:  req.RepNationCd,
+		WideAreaCd:   req.WideAreaCd,
+	})
+	return string(params), err
+}
+
+func (this *MovieWeeklyModel) Info(dailyID int) (MovieWeeklyTable, error) {
+	var (
+		db    = service.DB_MOVIEW
+		conn  = db.SQLDB
+		query = "SELECT * FROM tb_movie_weekly WHERE id = ?;"
+		info  MovieWeeklyTable
+	)
+
+	err := conn.QueryRow(query, dailyID).Scan(
+		&info.WeeklyID, &info.BoxofficeType, &info.ShowRange, &info.YearWeekTime, &info.Rnum,
+		&info.Rank, &info.RankInten, &info.RankOldAndNew,
+		&info.MovieCd, &info.MovieNm, &info.OpenDt,
+		&info.SalesAmt, &info.SalesShare, &info.SalesInten, &info.SalesChange, &info.SalesAcc,
+		&info.AudiCnt, &info.AudiInten, &info.AudiChange, &info.AudiAcc,
+		&info.ScrnCnt, &info.ShowCnt, &info.UpdatedAt, &info.CreatedAt,
+	)
+
+	if err != nil && err != sql.ErrNoRows {
+		db.SetErrorLog(err, query)
+		return info, err
+	}
+
+	db.SetGeneralLog(config.GL_ACTION_SELECT, query, "select movie weekly")
+	return info, nil
+}

+ 74 - 0
model/orderDetail.go

@@ -0,0 +1,74 @@
+package model
+
+import (
+	"crawler/config"
+	"crawler/service"
+	"database/sql"
+	"log"
+)
+
+type OrderDetailModel struct {
+	OrderDetail
+}
+
+type OrderDetail struct {
+	ID              int    `json:"id"`
+	OrderID         int    `json:"order_id"`
+	ProductID       int    `json:"product_id"`
+	ProductOptionID int    `json:"product_option_id"`
+	GamePos         int    `json:"game_pos"`
+	GameID          int    `json:"game_id"`
+	OrderProductID  string `json:"OrderProductID"`
+}
+
+func (this *OrderDetailModel) IsExists(orderID, orderDetailID int) bool {
+	var (
+		db     = service.DB_PLAYR
+		conn   = db.SQLDB
+		query  = "SELECT IF(COUNT(*) <= 0, 0, 1) AS `exists` FROM tb_order_detail WHERE id = ? AND order_id = ?;"
+		exists = false
+	)
+
+	stmt, err := conn.Prepare(query)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	err = stmt.QueryRow(orderDetailID, orderID).Scan(&exists)
+	if err != nil {
+		db.SetErrorLog(err, query)
+		return exists
+	}
+
+	db.SetGeneralLog(config.GL_ACTION_SELECT, query, "select exists order detail")
+	return exists
+}
+
+func (this *OrderDetailModel) Info(orderID, orderDetailID int) (OrderDetail, error) {
+	var (
+		db    = service.DB_PLAYR
+		conn  = db.SQLDB
+		query = `SELECT id, order_id, product_id, product_option_id, game_pos, game_id,
+					(CASE WHEN game_pos = 1 THEN
+					  (SELECT CWS.code FROM tb_cws_product CWS WHERE CWS.id = game_id LIMIT 1)
+					WHEN game_pos = 2 THEN
+					  (SELECT G2A.id FROM tb_g2a_product G2A WHERE G2A.index = game_id LIMIT 1)
+					ELSE
+					  NULL
+					END) AS OrderProductID
+					FROM tb_order_detail WHERE id = ? AND order_id = ?;`
+		info OrderDetail
+	)
+
+	err := conn.QueryRow(query, orderDetailID, orderID).Scan(
+		&info.ID, &info.OrderID, &info.ProductID, &info.ProductOptionID, &info.GamePos, &info.GameID, &info.OrderProductID,
+	)
+
+	if err != nil && err != sql.ErrNoRows {
+		db.SetErrorLog(err, query)
+		return info, err
+	}
+
+	db.SetGeneralLog(config.GL_ACTION_SELECT, query, "select order detail info")
+	return info, nil
+}

+ 49 - 0
model/processLog.go

@@ -0,0 +1,49 @@
+package model
+
+import (
+	"crawler/config"
+	"crawler/service"
+)
+
+type ProcessLogInterface interface {
+	Save()
+}
+
+type ProcessLogModel struct {
+	ProcessLog
+}
+
+type ProcessLog struct {
+	Path      string `json:"path"`
+	Code      int    `json:"code"`
+	Method    string `json:"method"`
+	Response  string `json:"response"`
+	Request   string `json:"request"`
+	RawQuery  string `json:"rawQuery"`
+	CreatedAt string `json:"createdAt"`
+}
+
+func (this *ProcessLogModel) Save() {
+	var (
+		db   = service.DB_CRAWLER
+		conn = db.SQLDB
+	)
+
+	sql := `
+		INSERT INTO tb_process_log
+		SET 
+			path = ?,
+			code = ?,
+			method = ?,
+			response = ?,
+			request = ?,
+			raw_query = ?, 
+			created_at = NOW();
+	`
+	_, err := conn.Exec(sql, this.Path, this.Code, this.Method, this.Response, this.Request, this.RawQuery)
+	if err != nil {
+		db.SetErrorLog(err, sql)
+	}
+
+	db.SetGeneralLog(config.GL_ACTION_WRITE, sql, "insert process log")
+}

+ 74 - 0
model/shopConfig.go

@@ -0,0 +1,74 @@
+package model
+
+import (
+	"crawler/config"
+	"crawler/service"
+	"database/sql"
+)
+
+type ShopConfigInterface interface {
+	Configs()
+}
+
+type ShopConfigModel struct {
+	ShopConfig
+}
+
+type ShopConfig struct {
+	Key         string
+	Value       *string
+	Description *string
+}
+
+func (this *ShopConfigModel) Configs() ([]ShopConfig, error) {
+	var (
+		db    = service.DB_PLAYR
+		conn  = db.SQLDB
+		query = "SELECT `key`, `value`, `description` FROM tb_shop_config;"
+		list  = make([]ShopConfig, 0)
+	)
+
+	rows, err := conn.Query(query)
+	if err != nil && err != sql.ErrNoRows {
+		db.SetErrorLog(err, query)
+		return list, err
+	}
+	defer rows.Close()
+
+	for rows.Next() {
+		var row ShopConfig
+		if err = rows.Scan(&row.Key, &row.Value, &row.Description); err != nil {
+			return list, err
+		}
+
+		list = append(list, row)
+	}
+
+	if err = rows.Err(); err != nil {
+		return list, err
+	}
+
+	db.SetGeneralLog(config.GL_ACTION_SELECT, query, "select shop config")
+	return list, nil
+}
+
+func (this *ShopConfigModel) G2AIsTest() bool {
+	var (
+		configs, err = this.Configs()
+		isTest       = false
+	)
+
+	if err != nil {
+		return isTest
+	}
+
+	for _, row := range configs {
+		if row.Key == "shop_g2a_is_test" {
+			if *row.Value != "0" && *row.Value != "" {
+				isTest = true
+			}
+		}
+	}
+
+	return isTest
+}

+ 2 - 0
robots.txt

@@ -0,0 +1,2 @@
+User-agent: *
+Disallow:

+ 64 - 0
route/api.go

@@ -0,0 +1,64 @@
+package route
+
+import (
+	"crawler/controller"
+	"fmt"
+	"github.com/gin-gonic/gin"
+)
+
+var (
+	MovieController = new(controller.Movie)
+	CronController  = new(controller.Cron)
+	G2AController   = new(controller.G2A)
+)
+
+// 라우터 설정
+func SetRoute(app *gin.Engine) {
+	NoRoute(app)    // 404
+	CronRoute(app)  // Cron schedule
+	MovieRoute(app) // 영화 API
+	G2ARoute(app)   // G2A API
+
+	defer func() {
+		if c := recover(); c != nil {
+			fmt.Println("Recover route execute !!")
+		}
+	}()
+
+	app.GET("/ping", func(c *gin.Context) {
+		c.JSON(200, gin.H{
+			"message": "pong",
+		})
+	})
+}
+
+func NoRoute(app *gin.Engine) {
+	app.NoRoute(func(c *gin.Context) {
+		c.File("index.html")
+	})
+}
+
+func MovieRoute(app *gin.Engine) {
+	r := app.Group("/movie")
+	r.GET("/searchDailyBoxOfficeList", MovieController.SearchDailyBoxOfficeList)
+	r.GET("/searchWeeklyBoxOfficeList", MovieController.SearchWeeklyBoxOfficeList)
+	r.GET("/searchMovieList", MovieController.SearchMovieList)
+	r.GET("/searchWeeklyInfo", MovieController.SearchWeeklyInfo)
+	r.GET("/searchDailyInfo", MovieController.SearchDailyInfo)
+	r.GET("/searchMovieInfo", MovieController.SearchMovieInfo)
+}
+
+func CronRoute(app *gin.Engine) {
+	r := app.Group("/cron")
+	r.GET("/list", CronController.List)
+	r.GET("/info", CronController.Info)
+	r.GET("/detail", CronController.Detail)
+	//r.GET("/stats", CronController.Stats)
+}
+
+func G2ARoute(app *gin.Engine) {
+	r := app.Group("/g2a")
+	r.GET("/products", G2AController.Products)
+	r.GET("/checkOutOfStock", G2AController.CheckOutOfStock)
+	r.GET("/order", G2AController.Order)
+}

+ 189 - 0
service/db.go

@@ -0,0 +1,189 @@
+package service
+
+import (
+	"crawler/config"
+	"strings"
+	"time"
+
+	"database/sql"
+	"fmt"
+	"gorm.io/driver/mysql"
+	"gorm.io/gorm"
+)
+
+var (
+	DB_MOVIEW  DB
+	DB_CRAWLER DB
+	DB_PLAYR   DB
+)
+
+type DB struct {
+	SQLDB  *sql.DB
+	GORMDB *gorm.DB
+	DSN    string
+	Err    error
+}
+
+func SetConfig() config.DBAccount {
+	var (
+		env     = config.Env
+		db      = config.DB
+		account = config.DBAccount{}
+	)
+
+	// DB 환경설정
+	switch env.DeveloperEnv {
+	case config.LOCAL:
+		account = db.Local
+	case config.DEV:
+		account = db.Dev
+	default:
+		fmt.Println("DB 설정이 잘못되었습니다.")
+	}
+
+	//fmt.Printf("\n%#v\n", account)
+
+	return account
+}
+
+// DB 연결
+func SetDatabase(dbName string) DB {
+	var (
+		env     = config.Env
+		db      = config.DB
+		account = SetConfig()
+		err     error
+	)
+
+	if dbName != "" {
+		account.Name = dbName
+	}
+
+	/*
+	 * sqlDB
+	 */
+	dsn := fmt.Sprintf("%s:%s@tcp(%s)/%s", account.User, account.Password, account.Address, account.Name)
+	fmt.Printf("\nEnv : %s\n", env.DeveloperEnv)
+	fmt.Printf("%s\n", dsn)
+	SQLConnd, err := sql.Open(account.Driver, dsn)
+
+	if err != nil || SQLConnd.Ping() != nil {
+		fmt.Printf("%s\n", account.Driver)
+		fmt.Printf("%s\n", dsn)
+		fmt.Printf("%v\n", SQLConnd)
+		fmt.Printf("sqlDB에 연결에 실패하였습니다.\n %v", err.Error())
+		fmt.Println(err)
+		SQLConnd.Close()
+	}
+
+	SQLConnd.SetConnMaxLifetime(time.Duration(db.MaxLifetime))
+	SQLConnd.SetConnMaxIdleTime(time.Duration(db.MaxIdleTime))
+	SQLConnd.SetMaxIdleConns(db.MaxIdleConn)
+	SQLConnd.SetMaxOpenConns(db.MaxOpenConn)
+
+	/*
+	 * GormDB
+	 */
+	GORMConnd, err := gorm.Open(mysql.New(mysql.Config{
+		Conn: SQLConnd,
+	}), &gorm.Config{})
+	if err != nil {
+		fmt.Printf("GormDB에 연결에 실패하였습니다.\n %v", err.Error())
+		fmt.Println(err)
+	}
+
+	new := new(DB)
+	new.SQLDB = SQLConnd
+	new.GORMDB = GORMConnd
+	new.DSN = dsn
+	new.Err = err
+
+	fmt.Println("Database was opened successfully !!")
+
+	return *new
+}
+
+// DB 연결
+func (db *DB) Connection(dbName string) DB {
+	return SetDatabase(dbName)
+}
+
+// gorm 커넥션 연결
+func (db *DB) GConnection(dbName string) DB {
+	return SetDatabase(dbName)
+}
+
+// DB 오류 입력
+func (db *DB) SetErrorLog(err error, query string) {
+	var (
+		conn         = DB_CRAWLER.SQLDB
+		sql          = `CALL SP_ERROR_LOG(?, ?, ?);`
+		errorMessage = strings.TrimSpace(strings.Split(err.Error(), ":")[1])
+		errorNo      = strings.TrimSpace(strings.Replace(strings.Split(err.Error(), ":")[0], "Error ", "", 1))
+	)
+
+	conn.Exec(sql, errorNo, errorMessage, query)
+	switch errorMessage {
+	case "sql: no rows in result set":
+	case "":
+	default:
+		fmt.Printf("DB error msg : %s\n", err.Error())
+	}
+
+	fmt.Printf("Exe error query : %s\n", query)
+}
+
+// DB SQL 입력
+func (db *DB) SetGeneralLog(action int, query, comment string) {
+	var (
+		conn = DB_CRAWLER.SQLDB
+		sql  = `
+			INSERT INTO tb_general_log
+			SET 
+				action = ?,
+				query = ?,
+				comment = ?, 
+				created_at = NOW();
+		`
+	)
+
+	_, err := conn.Exec(sql, action, query, comment)
+	if err != nil {
+		db.SetErrorLog(err, sql)
+	}
+}
+
+// DB 연결 종료
+func (db *DB) Close() {
+	defer func() {
+		if err := recover(); err != nil {
+			fmt.Printf("Database recovered message: %s\n", err)
+		}
+	}()
+
+	if DB_MOVIEW != (DB{}) {
+		defer DB_MOVIEW.SQLDB.Close()
+		DB_MOVIEW.SQLDB = nil
+		DB_MOVIEW.GORMDB = nil
+		DB_MOVIEW.DSN = ""
+		DB_MOVIEW.Err = nil
+	}
+
+	if DB_CRAWLER != (DB{}) {
+		defer DB_CRAWLER.SQLDB.Close()
+		DB_CRAWLER.SQLDB = nil
+		DB_CRAWLER.GORMDB = nil
+		DB_CRAWLER.DSN = ""
+		DB_CRAWLER.Err = nil
+	}
+
+	if DB_PLAYR != (DB{}) {
+		defer DB_PLAYR.SQLDB.Close()
+		DB_PLAYR.SQLDB = nil
+		DB_PLAYR.GORMDB = nil
+		DB_PLAYR.DSN = ""
+		DB_PLAYR.Err = nil
+	}
+
+	fmt.Println("DB closed")
+}

+ 101 - 0
service/rest.go

@@ -0,0 +1,101 @@
+package service
+
+import (
+	"crawler/config"
+	"fmt"
+	"io"
+	"log"
+	"net/http"
+	"os"
+	"time"
+)
+
+type Rest struct{}
+
+// REST API 호출
+func (this *Rest) CallRestGetAPI(url string) ([]byte, error) {
+	// API 호출
+	req, err := http.NewRequest("GET", url, nil)
+	if this.Check(err) {
+		return nil, err
+	}
+
+	// 웹 수집을 위한 Crawler Header 값 추가
+	req.Header.Set("Accept", "application/json")
+	req.Header.Add("User-Agent", "Crawler")
+
+	client := &http.Client{
+		Timeout: 8 * time.Second,
+	}
+	res, err := client.Do(req)
+	if this.Check(err) {
+		return nil, err
+	}
+
+	defer func(body io.ReadCloser) {
+		this.Check(body.Close())
+	}(res.Body)
+
+	data, err := io.ReadAll(res.Body)
+	if this.Check(err) {
+		return nil, err
+	}
+
+	return data, nil
+}
+
+func (this *Rest) CallRestGet(url string) ([]byte, error) {
+	// API 호출
+	req, err := http.NewRequest("GET", url, nil)
+	if this.Check(err) {
+		return nil, err
+	}
+
+	// 웹 수집을 위한 Crawler Header 값 추가
+	req.Header.Add("User-Agent", "Crawler")
+
+	client := &http.Client{
+		Timeout: 8 * time.Second,
+	}
+	res, err := client.Do(req)
+	if this.Check(err) {
+		return nil, err
+	}
+
+	defer func(body io.ReadCloser) {
+		this.Check(body.Close())
+	}(res.Body)
+
+	data, err := io.ReadAll(res.Body)
+	if this.Check(err) {
+		return nil, err
+	}
+
+	return data, nil
+}
+
+// REST API 오류 확인
+func (this *Rest) Check(err error) bool {
+	if err != nil {
+		this.WriteLog(err.Error())
+		return true
+	}
+	return false
+}
+
+// Kobis 오류 기록
+func (this *Rest) WriteLog(msg string) {
+	data, err := os.OpenFile(config.ERROR_LOG_PATH_KOBIS, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
+	if err != nil {
+		log.Fatalln(err)
+	}
+
+	defer func() {
+		if data.Close() != nil {
+			fmt.Println(err)
+		}
+	}()
+
+	log.SetOutput(data)
+	log.Println(err)
+}

+ 235 - 0
utility/common.go

@@ -0,0 +1,235 @@
+package utility
+
+import (
+	"crawler/config"
+	"encoding/json"
+	"fmt"
+	"github.com/gin-gonic/gin"
+	"log"
+	"math/rand"
+	"net"
+	"net/http"
+	"os"
+	"regexp"
+	"strings"
+	"time"
+)
+
+// 패키지 호출 시 실행
+func SetEnviron() {
+
+	// 변수화 대상 설정 파일
+	var configFiles = [...]string{
+		config.ENV_PATH, config.CONFIG_PATH_DATABASE, config.CONFIG_PATH_MOVIE, config.CONFIG_PATH_G2A,
+	}
+
+	// json 설정 파일을 읽어들인 후 변수화
+	for _, file := range configFiles {
+		SetValueFromJsonFile(file)
+	}
+
+	// 현재 개발환경 변수 확인 (local, dev)
+	var DEVELOPER_ENV = DeveloperEnv()
+	if DEVELOPER_ENV == "" {
+		DEVELOPER_ENV = config.Env.DeveloperEnv
+	}
+
+	if os.Setenv(config.EnvKey, DEVELOPER_ENV) == nil {
+		config.Env.DeveloperEnv = DEVELOPER_ENV
+	}
+}
+
+// json 파일에서 값 추출 후 변수화
+func SetValueFromJsonFile(filePath string) {
+	file, err := os.Open(filePath)
+	defer func() {
+		err := file.Close()
+		if err != nil {
+			log.Fatalln(err)
+		}
+	}()
+	Check(err, nil)
+
+	decoder := json.NewDecoder(file)
+
+	switch filePath {
+	case config.ENV_PATH:
+		err = decoder.Decode(&config.Env)
+	case config.CONFIG_PATH_DATABASE:
+		err = decoder.Decode(&config.DB)
+	case config.CONFIG_PATH_MOVIE:
+		err = decoder.Decode(&config.Movie)
+	case config.CONFIG_PATH_G2A:
+		err = decoder.Decode(&config.G2A)
+	}
+
+	Check(err, nil)
+}
+
+// Gin 디버그 모드 사용할 것인가 여부
+func SetDebug() {
+	if config.Env.IsDebug {
+		gin.SetMode(gin.DebugMode)
+	} else {
+		gin.SetMode(gin.ReleaseMode)
+	}
+}
+
+// 오류 확인
+func Check(err error, path interface{}) {
+	if err != nil {
+		if path == nil || path == "" {
+			path = "./log/error.txt"
+		}
+
+		logFile, err := os.OpenFile(path.(string), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
+		if err != nil {
+			panic(err)
+		}
+		defer logFile.Close()
+
+		logger := log.New(logFile, "INFO: ", log.Ldate|log.Ltime|log.Lshortfile)
+		logger.Printf("%s\n", err.Error())
+	}
+}
+
+// 설정된 개발 환경변수 조회
+func DeveloperEnv() string {
+	return strings.ToLower(os.Getenv(config.EnvKey)) // 소문자로 변환
+}
+
+// 시작시간으로 부터 소요시간(초)
+func GetDurationInMillseconds(start time.Time) float64 {
+	end := time.Now()
+	duration := end.Sub(start)
+	milliseconds := float64(duration) / float64(time.Millisecond)
+	rounded := float64(int(milliseconds*100+.5)) / 100
+	return rounded
+}
+
+func IpAddrFromRemoteAddr(s string) string {
+	idx := strings.LastIndex(s, ":")
+	if idx == -1 {
+		return s
+	}
+	return s[:idx]
+}
+
+// 현재 요청 IP
+func GetClientIP(c *gin.Context) string {
+	// first check the X-Forwarded-For header
+	requester := c.Request.Header.Get("X-Forwarded-For")
+	// if empty, check the Real-IP header
+	if len(requester) == 0 {
+		requester = c.Request.Header.Get("X-Real-IP")
+	}
+	// if the requester is still empty, use the hard-coded address from the socket
+	if len(requester) == 0 {
+		requester = c.Request.RemoteAddr
+	}
+
+	// if requester is a comma delimited list, take the first one
+	// (this happens when proxied via elastic load balancer then again through nginx)
+	if strings.Contains(requester, ",") {
+		requester = strings.Split(requester, ",")[0]
+	}
+
+	return requester
+}
+
+// 현재 서버 IP
+func GetLocalIP() string {
+	addrs, err := net.InterfaceAddrs()
+	if err != nil {
+		return ""
+	}
+	for _, address := range addrs {
+		// check the address type and if it is not a loopback the display it
+		if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
+			if ipnet.IP.To4() != nil {
+				return ipnet.IP.String()
+			}
+		}
+	}
+	return ""
+}
+
+func RequestGetRemoteAddress(r *http.Request) string {
+	hdr := r.Header
+	hdrRealIP := hdr.Get("X-Real-Ip")
+	hdrForwardedFor := hdr.Get("X-Forwarded-For")
+	if hdrRealIP == "" && hdrForwardedFor == "" {
+		return IpAddrFromRemoteAddr(r.RemoteAddr)
+	}
+	if hdrForwardedFor != "" {
+		// X-Forwarded-For is potentially a list of addresses separated with ","
+		parts := strings.Split(hdrForwardedFor, ",")
+		for i, p := range parts {
+			parts[i] = strings.TrimSpace(p)
+		}
+		// TODO: should return first non-local address
+		return parts[0]
+	}
+	return hdrRealIP
+}
+
+// 사용자 useragent 조회
+func UserAgent(w http.ResponseWriter, r *http.Request) {
+	ua := r.UserAgent() //<---- simpler and faster!
+	fmt.Printf("user agent is: %s \n", ua)
+	w.Write([]byte("user agent is " + ua))
+}
+
+// 데이터 단위 변환
+func ByteSize(bytes uint64) string {
+	unit := ""
+	value := float32(bytes)
+
+	switch {
+	case bytes >= config.TERABYTE:
+		unit = "T"
+		value = value / config.TERABYTE
+	case bytes >= config.GIGABYTE:
+		unit = "G"
+		value = value / config.GIGABYTE
+	case bytes >= config.MEGABYTE:
+		unit = "M"
+		value = value / config.MEGABYTE
+	case bytes >= config.KILOBYTE:
+		unit = "K"
+		value = value / config.KILOBYTE
+	case bytes >= config.BYTE:
+		unit = "B"
+	case bytes == 0:
+		return "0"
+	}
+
+	stringValue := fmt.Sprintf("%.1f", value)
+	stringValue = strings.TrimSuffix(stringValue, ".0")
+	return fmt.Sprintf("%s%s", stringValue, unit)
+}
+
+// inArray
+func Find(slice []string, val string) (int, bool) {
+	for i, item := range slice {
+		if item == val {
+			return i, true
+		}
+	}
+	return -1, false
+}
+
+const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
+
+func RandomString() string {
+	b := make([]byte, rand.Intn(10)+10)
+	for i := range b {
+		b[i] = letterBytes[rand.Intn(len(letterBytes))]
+	}
+	return string(b)
+}
+
+// 특수문자 제거
+func RemoveSpecialChar(s string) string {
+	return strings.TrimSpace(regexp.MustCompile(`[\{\}\[\]\/?.,;:|\)*~!^\-_+<>@\#$%&\\\=\(\'\"\n\r]+`).ReplaceAllString(s, ""))
+}

+ 141 - 0
utility/encrypt.go

@@ -0,0 +1,141 @@
+package utility
+
+import (
+	"bytes"
+	"crypto/aes"
+	"crypto/cipher"
+	"crypto/md5"
+	"crypto/rand"
+	"encoding/base64"
+	"encoding/hex"
+	"fmt"
+	"golang.org/x/crypto/sha3"
+	"io"
+	"strings"
+)
+
+// 암호화
+var key = []byte{0x17, 0xc0, 0xcc, 0x00, 0x32, 0x88, 0x11, 0xa1, 0x51, 0xfe, 0xff, 0x81, 0x9c, 0xdc, 0x9f, 0xea, 0x60, 0x2f, 0x71, 0x28, 0x16, 0x1f, 0x41, 0x7a, 0xa5, 0xc4, 0xac, 0xdd, 0x50, 0x78, 0x08, 0x3f}
+
+func HashPassword(pw string) string {
+	bin := sha3.Sum256([]byte(pw))
+	str := hex.EncodeToString(bin[:])
+	return str
+}
+
+func Encrypt(buff []byte) ([]byte, error) {
+	block, err := aes.NewCipher(key)
+	if err != nil {
+		return nil, err
+	}
+
+	ciphertext := make([]byte, aes.BlockSize+len(buff))
+	iv := ciphertext[:aes.BlockSize]
+	if _, err := io.ReadFull(rand.Reader, iv); err != nil {
+		return nil, err
+	}
+
+	cfb := cipher.NewCFBEncrypter(block, iv)
+	cfb.XORKeyStream(ciphertext[aes.BlockSize:], buff)
+	return ciphertext, nil
+}
+
+func EncryptString(s string) (string, error) {
+	result, err := Encrypt([]byte(s))
+	if err != nil {
+		return "", err
+	}
+
+	return hex.EncodeToString(result), nil
+}
+
+func Decrypt(buff []byte) ([]byte, error) {
+	block, err := aes.NewCipher(key)
+	if err != nil {
+		return nil, err
+	}
+	if len(buff) < aes.BlockSize {
+		return nil, fmt.Errorf("ciphertext too short")
+	}
+	iv := buff[:aes.BlockSize]
+	buff = buff[aes.BlockSize:]
+	cfb := cipher.NewCFBDecrypter(block, iv)
+	cfb.XORKeyStream(buff, buff)
+
+	return buff, nil
+}
+
+func DecryptString(s string) (string, error) {
+	buf, err := hex.DecodeString(s)
+	if err != nil {
+		return "", err
+	}
+
+	result, err := Decrypt(buf)
+	if err != nil {
+		return "", err
+	}
+
+	return string(result), nil
+}
+
+func MakeMD5(text string) string {
+	hasher := md5.New()
+	hasher.Write([]byte(text))
+	return string(hasher.Sum(nil))
+}
+
+func AesEncrypt(plainText string, key string, iv string) (string, error) {
+	if strings.TrimSpace(plainText) == "" {
+		return plainText, nil
+	}
+
+	block, err := aes.NewCipher([]byte(key))
+	if err != nil {
+		return "", err
+	}
+
+	encrypter := cipher.NewCBCEncrypter(block, []byte(iv))
+	paddedPlainText := padPKCS7([]byte(plainText), encrypter.BlockSize())
+
+	cipherText := make([]byte, len(paddedPlainText))
+	// CryptBlocks 함수에 데이터(paddedPlainText)와 암호화 될 데이터를 저장할 슬라이스(cipherText)를 넣으면 암호화가 된다.
+	encrypter.CryptBlocks(cipherText, paddedPlainText)
+
+	return base64.StdEncoding.EncodeToString(cipherText), nil
+}
+
+func AesDecrypt(cipherText string, key string, iv string) (string, error) {
+	if strings.TrimSpace(cipherText) == "" {
+		return cipherText, nil
+	}
+
+	decodedCipherText, err := base64.StdEncoding.DecodeString(cipherText)
+	if err != nil {
+		return "", err
+	}
+
+	block, err := aes.NewCipher([]byte(key))
+	if err != nil {
+		return "", err
+	}
+
+	decrypter := cipher.NewCBCDecrypter(block, []byte(iv))
+	plainText := make([]byte, len(decodedCipherText))
+
+	decrypter.CryptBlocks(plainText, decodedCipherText)
+	trimmedPlainText := trimPKCS5(plainText)
+
+	return string(trimmedPlainText), nil
+}
+
+func padPKCS7(plainText []byte, blockSize int) []byte {
+	padding := blockSize - len(plainText)%blockSize
+	padText := bytes.Repeat([]byte{byte(padding)}, padding)
+	return append(plainText, padText...)
+}
+
+func trimPKCS5(text []byte) []byte {
+	padding := text[len(text)-1]
+	return text[:len(text)-int(padding)]
+}

+ 45 - 0
utility/telegram.go

@@ -0,0 +1,45 @@
+package utility
+
+import (
+	"fmt"
+	tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
+	"log"
+)
+
+// G2A 상품수집 결과
+func SendMessageToG2AProduct(msg string) {
+	SendMessage("6236017150:AAGsCufu7QT6-MdEUXdda9_frczPwnGXuL0", 650107127, msg)
+}
+
+// G2A 상품주문 결과
+func SendMessageToG2AOrder(msg string) {
+	SendMessage("5816574764:AAETUt5eIbdORTkKEpE6DkdBG4q18jycUQ4", 650107127, msg)
+}
+
+// G2A 오류 내역
+func SendMessageToG2AError(msg string) {
+	SendMessage("6249107543:AAFJHE3dZl8I0JFgMpyLMUoUS4pUiRhbIc0", 650107127, msg)
+}
+
+func SendMessage(token string, chatID int64, msg string) {
+	defer func() {
+		if e := recover(); e != nil {
+			fmt.Printf("Telegram recover error: %s\n", e)
+		}
+	}()
+
+	bot, err := tgbotapi.NewBotAPI(token)
+	if err != nil {
+		log.Panic(err)
+	}
+
+	bot.Debug = true
+
+	chat, _ := bot.GetChat(tgbotapi.ChatInfoConfig{
+		tgbotapi.ChatConfig{
+			ChatID: chatID,
+		},
+	})
+
+	bot.Send(tgbotapi.NewMessage(chat.ID, msg))
+}