diff --git a/.env.template b/.env.template new file mode 100644 index 0000000..0e393f5 --- /dev/null +++ b/.env.template @@ -0,0 +1,4 @@ +OPENWEATHER_API_KEY= +OPENWEATHER_LAT=37 +OPENWEATHER_LON=126 +NATS_URL= \ No newline at end of file diff --git a/.gitignore b/.gitignore index 047bcf4..6aac5ec 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ /vendor/ /seven-skies /.vscode/launch.json +/.env +/openweather diff --git a/cmd/openweather/handler.go b/cmd/openweather/handler.go new file mode 100644 index 0000000..0c9b492 --- /dev/null +++ b/cmd/openweather/handler.go @@ -0,0 +1,90 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "log" + "time" + + "github.com/neatflowcv/seven-skies/internal/pkg/broker" + "github.com/neatflowcv/seven-skies/internal/pkg/domain" + "github.com/neatflowcv/seven-skies/internal/pkg/openweather" +) + +type Handler struct { + broker broker.Broker + key string + lat float64 + lon float64 +} + +func NewHandler(broker broker.Broker, key string, lat float64, lon float64) *Handler { + return &Handler{ + broker: broker, + key: key, + lat: lat, + lon: lon, + } +} + +func (h *Handler) Handle(ctx context.Context) error { + log.Println("get forecast") + + forecast, err := openweather.Forecast(ctx, h.key, h.lat, h.lon) + if err != nil { + return fmt.Errorf("error in get forecast: %w", err) + } + + log.Println("get forecast successfully", forecast) + + for _, list := range forecast.List { + weatherEvent := domain.WeatherEvent{ + Date: time.Unix(int64(list.Dt), 0), + Condition: decideCondition(list.Weather), + Temperature: &domain.Temperature{Value: domain.Celsius(list.Main.Temp)}, + High: &domain.Temperature{Value: domain.Celsius(list.Main.TempMax)}, + Low: &domain.Temperature{Value: domain.Celsius(list.Main.TempMin)}, + } + + message, err := json.Marshal(&weatherEvent) + if err != nil { + log.Println("error in marshal weather event", err) + + continue + } + + err = h.broker.Publish(ctx, "SEVEN_SKIES_SUBJECT.OPENWEATHER", message) + if err != nil { + log.Println("error in publish weather event", err) + + continue + } + } + + return nil +} + +func decideCondition(weather []openweather.Weather) domain.WeatherCondition { + if len(weather) == 0 { + return domain.WeatherConditionUnknown + } + + main := weather[0].Main + switch main { + case "Thunderstorm", "Drizzle", "Rain": + return domain.WeatherConditionRain + + case "Snow": + return domain.WeatherConditionSnow + + case "Mist", "Smoke", "Haze", "Dust", "Fog", "Sand", "Ash", "Squall", "Tornado", "Clouds": + return domain.WeatherConditionCloudy + + case "Clear": + return domain.WeatherConditionClear + + default: + return domain.WeatherConditionUnknown + } +} diff --git a/cmd/openweather/main.go b/cmd/openweather/main.go new file mode 100644 index 0000000..0109366 --- /dev/null +++ b/cmd/openweather/main.go @@ -0,0 +1,128 @@ +package main + +import ( + "context" + "log" + "os" + "runtime/debug" + "strconv" + + "github.com/go-co-op/gocron/v2" + "github.com/joho/godotenv" + "github.com/neatflowcv/seven-skies/internal/pkg/broker/nats" +) + +func version() string { + info, ok := debug.ReadBuildInfo() + if !ok { + return "unknown" + } + + return info.Main.Version +} + +type Config struct { + Key string + Lat float64 + Lon float64 + NATSURL string +} + +func NewConfig() *Config { + key := os.Getenv("OPENWEATHER_API_KEY") + if key == "" { + log.Panic("OPENWEATHER_API_KEY is not set") + } + + lat := os.Getenv("OPENWEATHER_LAT") + if lat == "" { + log.Panic("OPENWEATHER_LAT is not set") + } + + lon := os.Getenv("OPENWEATHER_LON") + if lon == "" { + log.Panic("OPENWEATHER_LON is not set") + } + + NATS_URL := os.Getenv("NATS_URL") + if NATS_URL == "" { + log.Panic("NATS_URL is not set") + } + + latFloat, err := strconv.ParseFloat(lat, 64) + if err != nil { + log.Panic("error in parse lat", err) + } + + lonFloat, err := strconv.ParseFloat(lon, 64) + if err != nil { + log.Panic("error in parse lon", err) + } + + return &Config{ + Key: key, + Lat: latFloat, + Lon: lonFloat, + NATSURL: NATS_URL, + } +} + +func main() { + log.Println("version", version()) + + err := godotenv.Load() + if err == nil { + log.Println("env loaded") + } + + config := NewConfig() + + ctx := context.Background() + + broker, err := nats.NewBroker(ctx, config.NATSURL, "SEVEN_SKIES_STREAM", []string{"SEVEN_SKIES_SUBJECT.>"}) + if err != nil { + log.Panic("error in create broker", err) + } + defer broker.Close() + + handler := NewHandler(broker, config.Key, config.Lat, config.Lon) + + scheduler, err := gocron.NewScheduler() + if err != nil { + log.Panic("error in create scheduler", err) + } + + defer func() { + err := scheduler.Shutdown() + if err != nil { + log.Println("error in shutdown scheduler", err) + } + }() + + err = handler.Handle(ctx) + if err != nil { + log.Panic("error in task", err) + } + + job, err := scheduler.NewJob( + gocron.CronJob("5 */2 * * *", false), // openweather에서 2시간마다 데이터가 업데이트 된다. + // 5분을 준 이유는 업데이트가 바로 된다는 보장이 없어서 + gocron.NewTask( + handler.Handle, + ), + ) + if err != nil { + log.Panic("error in create job", err) + } + + scheduler.Start() + + nextRun, err := job.NextRun() + if err != nil { + log.Panic("error in get next run", err) + } + + log.Println("next run", nextRun) + + select {} +} diff --git a/go.mod b/go.mod index 132cf72..0cc0b41 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,8 @@ module github.com/neatflowcv/seven-skies go 1.25.5 require ( + github.com/go-co-op/gocron/v2 v2.18.2 + github.com/joho/godotenv v1.5.1 github.com/nats-io/nats.go v1.47.0 github.com/stretchr/testify v1.11.1 resty.dev/v3 v3.0.0-beta.4 @@ -10,10 +12,14 @@ require ( require ( github.com/davecgh/go-spew v1.1.1 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/jonboulle/clockwork v0.5.0 // indirect github.com/klauspost/compress v1.18.2 // indirect + github.com/kr/pretty v0.3.1 // indirect github.com/nats-io/nkeys v0.4.12 // indirect github.com/nats-io/nuid v1.0.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/robfig/cron/v3 v3.0.1 // indirect golang.org/x/crypto v0.45.0 // indirect golang.org/x/net v0.47.0 // indirect golang.org/x/sys v0.38.0 // indirect diff --git a/go.sum b/go.sum index 526f20c..5da393a 100644 --- a/go.sum +++ b/go.sum @@ -1,25 +1,46 @@ +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 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/go-co-op/gocron/v2 v2.18.2 h1:+5VU41FUXPWSPKLXZQ/77SGzUiPCcakU0v7ENc2H20Q= +github.com/go-co-op/gocron/v2 v2.18.2/go.mod h1:Zii6he+Zfgy5W9B+JKk/KwejFOW0kZTFvHtwIpR4aBI= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I= +github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60= github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/nats-io/nats.go v1.47.0 h1:YQdADw6J/UfGUd2Oy6tn4Hq6YHxCaJrVKayxxFqYrgM= github.com/nats-io/nats.go v1.47.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g= github.com/nats-io/nkeys v0.4.12 h1:nssm7JKOG9/x4J8II47VWCL1Ds29avyiQDRn0ckMvDc= github.com/nats-io/nkeys v0.4.12/go.mod h1:MT59A1HYcjIcyQDJStTfaOY6vhy9XTUjOFo+SVsvpBg= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 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/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= resty.dev/v3 v3.0.0-beta.4 h1:2O77oFymtA4NT8AY87wAaSgSGUBk2yvvM1qno9VRXZU= diff --git a/internal/pkg/domain/weather_condition.go b/internal/pkg/domain/weather_condition.go new file mode 100644 index 0000000..dea11f1 --- /dev/null +++ b/internal/pkg/domain/weather_condition.go @@ -0,0 +1,12 @@ +package domain + +type WeatherCondition string + +const ( + WeatherConditionUnknown WeatherCondition = "UNKNOWN" + WeatherConditionClear WeatherCondition = "CLEAR" + WeatherConditionCloudy WeatherCondition = "CLOUDY" + WeatherConditionRain WeatherCondition = "RAIN" + WeatherConditionSnow WeatherCondition = "SNOW" + WeatherConditionRainSnow WeatherCondition = "RAIN_SNOW" +) diff --git a/internal/pkg/domain/weather_event.go b/internal/pkg/domain/weather_event.go new file mode 100644 index 0000000..cccc8d0 --- /dev/null +++ b/internal/pkg/domain/weather_event.go @@ -0,0 +1,17 @@ +package domain + +import "time" + +type Celsius float64 + +type Temperature struct { + Value Celsius `json:"value"` +} + +type WeatherEvent struct { + Date time.Time `json:"date"` + Condition WeatherCondition `json:"condition"` + Temperature *Temperature `json:"temperature,omitempty"` + High *Temperature `json:"high,omitempty"` + Low *Temperature `json:"low,omitempty"` +}