diff --git a/backend/go.mod b/backend/go.mod index c539f0a3..cc7bb444 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -73,7 +73,7 @@ require ( github.com/gofiber/utils v1.1.0 // indirect github.com/gorilla/websocket v1.5.1 // indirect github.com/josharian/intern v1.0.0 // indirect - github.com/klauspost/compress v1.17.7 // indirect + github.com/klauspost/compress v1.17.8 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect @@ -95,12 +95,11 @@ require ( github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.52.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect - go.mongodb.org/mongo-driver v1.14.0 // indirect + go.mongodb.org/mongo-driver v1.15.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 // indirect + golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8 // indirect golang.org/x/net v0.24.0 // indirect golang.org/x/sys v0.19.0 // indirect - golang.org/x/text v0.14.0 // indirect golang.org/x/tools v0.20.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/backend/go.sum b/backend/go.sum index 602dea87..7b3b7d2e 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -122,6 +122,8 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg= github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU= +github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -236,6 +238,8 @@ github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaD go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g= go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80= go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= +go.mongodb.org/mongo-driver v1.15.0 h1:rJCKC8eEliewXjZGf0ddURtl7tTVy1TK3bfl0gkUSLc= +go.mongodb.org/mongo-driver v1.15.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= @@ -251,6 +255,8 @@ golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 h1:985EYyeCOxTpcgOTJpflJUwOeEz0CQOdPt73OzpE9F8= golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= +golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8 h1:ESSUROHIBHg7USnszlcdmjBEwdMj9VUvU+OPk4yl2mc= +golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8= golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -288,8 +294,6 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 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.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= diff --git a/backend/handlers/admin_api.go b/backend/handlers/admin_api.go index d0eb8cc2..40b8282a 100644 --- a/backend/handlers/admin_api.go +++ b/backend/handlers/admin_api.go @@ -1,12 +1,25 @@ package handlers import ( + "chulbong-kr/middlewares" "chulbong-kr/services" + "time" "github.com/gofiber/fiber/v2" ) -func ListUnreferencedS3ObjectsHandler(c *fiber.Ctx) error { +// RegisterAdminRoutes sets up the routes for admin handling within the application. +func RegisterAdminRoutes(api fiber.Router) { + api.Post("/chat/ban/:markerID/:userID", middlewares.AdminOnly, banUserHandler) + + adminGroup := api.Group("/admin") + { + adminGroup.Use(middlewares.AdminOnly) + adminGroup.Get("/dead", listUnreferencedS3ObjectsHandler) + } +} + +func listUnreferencedS3ObjectsHandler(c *fiber.Ctx) error { killSwitch := c.Query("kill", "n") dbURLs, err := services.FetchAllPhotoURLsFromDB() @@ -29,3 +42,49 @@ func ListUnreferencedS3ObjectsHandler(c *fiber.Ctx) error { return c.JSON(unreferenced) } + +func banUserHandler(c *fiber.Ctx) error { + // Extract markerID and userID from the path parameters + markerID := c.Params("markerID") + userID := c.Params("userID") + + // assert duration is sent in the request body as JSON + var requestBody struct { + DurationInMinutes int `json:"duration"` + } + if err := c.BodyParser(&requestBody); err != nil { + requestBody = struct { + DurationInMinutes int `json:"duration"` + }{ + DurationInMinutes: 5, // default 5 minutes banned + } + // return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + // "error": "Invalid request format", + // }) + } + + if requestBody.DurationInMinutes < 1 { + requestBody.DurationInMinutes = 5 + } else if requestBody.DurationInMinutes > 15 { + requestBody.DurationInMinutes = 10 // max 10 minutes + } + + // Convert duration to time.Duration + duration := time.Duration(requestBody.DurationInMinutes) * time.Minute + + // Call the BanUser method on the manager instance + err := services.WsRoomManager.BanUser(markerID, userID, duration) + if err != nil { + // Log the error or handle it as needed + // log.Printf("Error banning user %s from room %s: %v", userID, markerID, err) + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "Failed to ban user", + }) + } + + // Return success response + return c.JSON(fiber.Map{ + "message": "User successfully banned", + "time": duration, + }) +} diff --git a/backend/handlers/auth_api.go b/backend/handlers/auth_api.go index 5e255e11..9a9d44f3 100644 --- a/backend/handlers/auth_api.go +++ b/backend/handlers/auth_api.go @@ -2,16 +2,40 @@ package handlers import ( "chulbong-kr/dto" + "chulbong-kr/middlewares" "chulbong-kr/services" "chulbong-kr/utils" "database/sql" "fmt" "log" + "os" "strings" "github.com/gofiber/fiber/v2" + "golang.org/x/oauth2" + "golang.org/x/oauth2/google" ) +// RegisterAuthRoutes sets up the routes for auth handling within the application. +func RegisterAuthRoutes(api fiber.Router) { + oauth2 := generateOAuthConfig() + + api.Get("/google", getGoogleAuthHandler(oauth2)) + authGroup := api.Group("/auth") + { + authGroup.Post("/signup", signUpHandler) + authGroup.Post("/login", loginHandler) + authGroup.Post("/logout", middlewares.AuthMiddleware, logoutHandler) + authGroup.Get("/google/callback", getGoogleCallbackHandler(oauth2)) + authGroup.Post("/verify-email/send", sendVerificationEmailHandler) + authGroup.Post("/verify-email/confirm", validateTokenHandler) + + // Finding password + authGroup.Post("/request-password-reset", requestResetPasswordHandler) + authGroup.Post("/reset-password", resetPasswordHandler) + } +} + // SignUp User godoc // // @Summary Sign up a new user [normal] @@ -31,7 +55,7 @@ import ( // @Failure 409 {object} map[string]interface{} "Email already registered" // @Failure 500 {object} map[string]interface{} "An error occurred while creating the user" // @Router /auth/signup [post] -func SignUpHandler(c *fiber.Ctx) error { +func signUpHandler(c *fiber.Ctx) error { var signUpReq dto.SignUpRequest if err := c.BodyParser(&signUpReq); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Cannot parse JSON, wrong sign up form."}) @@ -80,7 +104,7 @@ func SignUpHandler(c *fiber.Ctx) error { // @Failure 401 {object} map[string]interface{} "Invalid email or password" // @Failure 500 {object} map[string]interface{} "Failed to generate token" // @Router /auth/login [post] -func LoginHandler(c *fiber.Ctx) error { +func loginHandler(c *fiber.Ctx) error { var request dto.LoginRequest if err := c.BodyParser(&request); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) @@ -109,7 +133,7 @@ func LoginHandler(c *fiber.Ctx) error { return c.JSON(response) } -func LogoutHandler(c *fiber.Ctx) error { +func logoutHandler(c *fiber.Ctx) error { // Retrieve user ID from context or session userID, ok := c.Locals("userID").(int) if !ok { @@ -150,7 +174,7 @@ func LogoutHandler(c *fiber.Ctx) error { // @Failure 409 {object} map[string]interface{} "Email already registered" // @Failure 500 {object} map[string]interface{} "An unexpected error occurred" // @Router /auth/send-verification-email [post] -func SendVerificationEmailHandler(c *fiber.Ctx) error { +func sendVerificationEmailHandler(c *fiber.Ctx) error { userEmail := c.FormValue("email") _, err := services.GetUserByEmail(userEmail) if err == nil { @@ -212,7 +236,7 @@ func SendVerificationEmailHandler(c *fiber.Ctx) error { // @Failure 400 {object} map[string]interface{} "Invalid or expired token" // @Failure 500 {object} map[string]interface{} "Error validating token" // @Router /auth/validate-token [post] -func ValidateTokenHandler(c *fiber.Ctx) error { +func validateTokenHandler(c *fiber.Ctx) error { token := c.FormValue("token") email := c.FormValue("email") @@ -245,7 +269,7 @@ func ValidateTokenHandler(c *fiber.Ctx) error { // @Success 200 "Password reset request initiated successfully" // @Failure 500 {object} map[string]interface{} "Failed to request reset password" // @Router /auth/request-reset-password [post] -func RequestResetPasswordHandler(c *fiber.Ctx) error { +func requestResetPasswordHandler(c *fiber.Ctx) error { email := c.FormValue("email") // Generate the password reset token @@ -286,7 +310,7 @@ func RequestResetPasswordHandler(c *fiber.Ctx) error { // @Success 200 "Password reset successfully" // @Failure 500 {object} map[string]interface{} "Failed to reset password" // @Router /auth/reset-password [post] -func ResetPasswordHandler(c *fiber.Ctx) error { +func resetPasswordHandler(c *fiber.Ctx) error { token := c.FormValue("token") newPassword := c.FormValue("password") @@ -297,3 +321,14 @@ func ResetPasswordHandler(c *fiber.Ctx) error { return c.SendStatus(fiber.StatusOK) } + +func generateOAuthConfig() *oauth2.Config { + // OAuth2 Configuration + return &oauth2.Config{ + ClientID: os.Getenv("G_CLIENT_ID"), + ClientSecret: os.Getenv("G_CLIENT_SECRET"), + RedirectURL: os.Getenv("G_REDIRECT"), + Scopes: []string{"https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.profile"}, + Endpoint: google.Endpoint, + } +} diff --git a/backend/handlers/chat_api.go b/backend/handlers/chat_api.go index 7a1bd8b2..deecb0c2 100644 --- a/backend/handlers/chat_api.go +++ b/backend/handlers/chat_api.go @@ -123,49 +123,3 @@ func GetRoomUsersHandler(c *fiber.Ctx) error { return c.JSON(fiber.Map{"connections": connections, "total_users": len(connections)}) } - -func BanUserHandler(c *fiber.Ctx) error { - // Extract markerID and userID from the path parameters - markerID := c.Params("markerID") - userID := c.Params("userID") - - // assert duration is sent in the request body as JSON - var requestBody struct { - DurationInMinutes int `json:"duration"` - } - if err := c.BodyParser(&requestBody); err != nil { - requestBody = struct { - DurationInMinutes int `json:"duration"` - }{ - DurationInMinutes: 5, // default 5 minutes banned - } - // return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - // "error": "Invalid request format", - // }) - } - - if requestBody.DurationInMinutes < 1 { - requestBody.DurationInMinutes = 5 - } else if requestBody.DurationInMinutes > 15 { - requestBody.DurationInMinutes = 10 // max 10 minutes - } - - // Convert duration to time.Duration - duration := time.Duration(requestBody.DurationInMinutes) * time.Minute - - // Call the BanUser method on the manager instance - err := services.WsRoomManager.BanUser(markerID, userID, duration) - if err != nil { - // Log the error or handle it as needed - // log.Printf("Error banning user %s from room %s: %v", userID, markerID, err) - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ - "error": "Failed to ban user", - }) - } - - // Return success response - return c.JSON(fiber.Map{ - "message": "User successfully banned", - "time": duration, - }) -} diff --git a/backend/handlers/comment_api.go b/backend/handlers/comment_api.go index 9b59f31e..8a3be2c6 100644 --- a/backend/handlers/comment_api.go +++ b/backend/handlers/comment_api.go @@ -2,6 +2,7 @@ package handlers import ( "chulbong-kr/dto" + "chulbong-kr/middlewares" "chulbong-kr/services" "chulbong-kr/utils" "strconv" @@ -9,8 +10,21 @@ import ( "github.com/gofiber/fiber/v2" ) -// PostCommentHandler creates a new comment -func PostCommentHandler(c *fiber.Ctx) error { +// RegisterCommentRoutes sets up the routes for comments handling within the application. +func RegisterCommentRoutes(api fiber.Router) { + api.Get("/comments/:markerId/comments", loadCommentsHandler) + + commentGroup := api.Group("/comments") + { + commentGroup.Use(middlewares.AuthMiddleware) + commentGroup.Post("", postCommentHandler) + commentGroup.Patch("/:commentId", updateCommentHandler) + commentGroup.Delete("/:commentId", removeCommentHandler) + } +} + +// postCommentHandler creates a new comment +func postCommentHandler(c *fiber.Ctx) error { userID := c.Locals("userID").(int) var req dto.CommentRequest if err := c.BodyParser(&req); err != nil { @@ -19,7 +33,7 @@ func PostCommentHandler(c *fiber.Ctx) error { containsBadWord, _ := utils.CheckForBadWords(req.CommentText) if containsBadWord { - return c.Status(fiber.StatusBadRequest).SendString("Comment contains inappropriate content.") + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Comment contains inappropriate content."}) } comment, err := services.CreateComment(req.MarkerID, userID, req.CommentText) @@ -29,7 +43,7 @@ func PostCommentHandler(c *fiber.Ctx) error { return c.Status(fiber.StatusOK).JSON(comment) } -func UpdateCommentHandler(c *fiber.Ctx) error { +func updateCommentHandler(c *fiber.Ctx) error { commentID, err := strconv.Atoi(c.Params("commentId")) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid comment ID"}) @@ -47,16 +61,16 @@ func UpdateCommentHandler(c *fiber.Ctx) error { // Call the service function to update the comment if err := services.UpdateComment(commentID, userID, request.CommentText); err != nil { if err.Error() == "comment not found or not owned by user" { - return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": err.Error()}) + return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Failed to update the comment"}) } // Handle other potential errors - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to update comment"}) + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to update the comment"}) } return c.Status(fiber.StatusOK).JSON(fiber.Map{"message": "Comment updated successfully"}) } -func RemoveCommentHandler(c *fiber.Ctx) error { +func removeCommentHandler(c *fiber.Ctx) error { commentID, err := strconv.Atoi(c.Params("commentId")) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid comment ID"}) @@ -75,7 +89,7 @@ func RemoveCommentHandler(c *fiber.Ctx) error { return c.Status(fiber.StatusOK).JSON(fiber.Map{"message": "Comment removed successfully"}) } -func LoadCommentsHandler(c *fiber.Ctx) error { +func loadCommentsHandler(c *fiber.Ctx) error { var params dto.CommentLoadParams if err := c.QueryParser(¶ms); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid query parameters"}) diff --git a/backend/handlers/marker_api.go b/backend/handlers/marker_api.go index 0f0ca635..de74ede6 100644 --- a/backend/handlers/marker_api.go +++ b/backend/handlers/marker_api.go @@ -2,6 +2,7 @@ package handlers import ( "chulbong-kr/dto" + "chulbong-kr/middlewares" "chulbong-kr/models" "chulbong-kr/protos" "chulbong-kr/services" @@ -26,7 +27,55 @@ var ( CacheMutex sync.RWMutex ) -func CreateMarkerWithPhotosHandler(c *fiber.Ctx) error { +// RegisterMarkerRoutes sets up the routes for markers handling within the application. +func RegisterMarkerRoutes(api fiber.Router) { + // Marker routes + // api.Get("/markers2", handlers.GetAllMarkersHandler) + // api.Get("/markers2", handlers.GetAllMarkersProtoHandler) + api.Get("/markers", getAllMarkersLocalHandler) + api.Get("/markers/new", getAllNewMarkersHandler) + + // api.Get("/markers-addr", middlewares.AdminOnly, handlers.GetAllMarkersWithAddrHandler) + // api.Post("/markers-addr", middlewares.AdminOnly, handlers.UpdateMarkersAddressesHandler) + // api.Get("/markers-db", middlewares.AdminOnly, handlers.GetMarkersClosebyAdmin) + + api.Get("/markers/:markerId/details", middlewares.AuthSoftMiddleware, getMarker) + api.Get("/markers/:markerID/facilities", getFacilitiesHandler) + api.Get("/markers/close", findCloseMarkersHandler) + api.Get("/markers/ranking", getMarkerRankingHandler) + api.Get("/markers/unique-ranking", getUniqueVisitorCountHandler) + api.Get("/markers/unique-ranking/all", getAllUniqueVisitorCountHandler) + api.Get("/markers/area-ranking", getCurrentAreaMarkerRankingHandler) + api.Get("/markers/convert", convertWGS84ToWCONGNAMULHandler) + api.Get("/markers/location-check", isInSouthKoreaHandler) + api.Get("/markers/weather", getWeatherByWGS84Handler) + + api.Get("/markers/save-offline", saveOfflineMap2Handler) + + api.Post("/markers/upload", middlewares.AdminOnly, uploadMarkerPhotoToS3Handler) + + markerGroup := api.Group("/markers") + { + markerGroup.Use(middlewares.AuthMiddleware) + + markerGroup.Get("/my", getUserMarkersHandler) + markerGroup.Get("/:markerID/dislike-status", checkDislikeStatus) + // markerGroup.Get("/:markerId", handlers.GetMarker) + + markerGroup.Post("/new", createMarkerWithPhotosHandler) + markerGroup.Post("/facilities", setMarkerFacilitiesHandler) + markerGroup.Post("/:markerID/dislike", leaveDislikeHandler) + markerGroup.Post("/:markerID/favorites", addFavoriteHandler) + + markerGroup.Put("/:markerID", updateMarker) + + markerGroup.Delete("/:markerID", deleteMarkerHandler) + markerGroup.Delete("/:markerID/dislike", undoDislikeHandler) + markerGroup.Delete("/:markerID/favorites", removeFavoriteHandler) + } +} + +func createMarkerWithPhotosHandler(c *fiber.Ctx) error { // go services.ResetCache(services.ALL_MARKERS_KEY) CacheMutex.Lock() MarkersLocalCache = nil @@ -41,12 +90,12 @@ func CreateMarkerWithPhotosHandler(c *fiber.Ctx) error { // Check if latitude and longitude are provided latitude, longitude, err := GetLatLngFromForm(form) if err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Failed to parse latitude/longitude"}) } // Location Must Be Inside South Korea if !utils.IsInSouthKoreaPrecisely(latitude, longitude) { - return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "Operations only allowed within South Korea."}) + return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "Operations are only allowed within South Korea."}) } // Checking if there's a marker close to the latitude and longitude @@ -57,7 +106,7 @@ func CreateMarkerWithPhotosHandler(c *fiber.Ctx) error { // Set default description if it's empty or not provided description := GetDescriptionFromForm(form) if containsBadWord, _ := utils.CheckForBadWords(description); containsBadWord { - return c.Status(fiber.StatusBadRequest).SendString("Comment contains inappropriate content.") + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Comment contains inappropriate content."}) } userId := c.Locals("userID").(int) @@ -79,7 +128,7 @@ func CreateMarkerWithPhotosHandler(c *fiber.Ctx) error { return c.Status(fiber.StatusCreated).JSON(marker) } -func GetAllMarkersHandler(c *fiber.Ctx) error { +func getAllMarkersHandler(c *fiber.Ctx) error { cachedMarkers, cacheErr := services.GetCacheEntry[[]dto.MarkerSimple](services.ALL_MARKERS_KEY) if cacheErr == nil && cachedMarkers != nil { // Cache hit @@ -97,7 +146,7 @@ func GetAllMarkersHandler(c *fiber.Ctx) error { return c.JSON(markers) } -func GetAllMarkersLocalHandler(c *fiber.Ctx) error { +func getAllMarkersLocalHandler(c *fiber.Ctx) error { CacheMutex.RLock() cached := MarkersLocalCache CacheMutex.RUnlock() @@ -130,7 +179,7 @@ func GetAllMarkersLocalHandler(c *fiber.Ctx) error { return c.Send(markersJSON) } -func GetAllMarkersProtoHandler(c *fiber.Ctx) error { +func getAllMarkersProtoHandler(c *fiber.Ctx) error { markers, err := services.GetAllMarkersProto() if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) @@ -150,7 +199,7 @@ func GetAllMarkersProtoHandler(c *fiber.Ctx) error { } // GetAllNewMarkersHandler handles requests to fetch a paginated list of newly added markers. -func GetAllNewMarkersHandler(c *fiber.Ctx) error { +func getAllNewMarkersHandler(c *fiber.Ctx) error { // Extract page and pageSize from query parameters. Provide default values if not specified. page, err := strconv.Atoi(c.Query("page", "1")) // Default to page 1 if not specified if err != nil { @@ -172,7 +221,7 @@ func GetAllNewMarkersHandler(c *fiber.Ctx) error { } // ADMIN -func GetAllMarkersWithAddrHandler(c *fiber.Ctx) error { +func getAllMarkersWithAddrHandler(c *fiber.Ctx) error { markersWithPhotos, err := services.GetAllMarkersWithAddr() if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) @@ -182,7 +231,7 @@ func GetAllMarkersWithAddrHandler(c *fiber.Ctx) error { } // GetMarker handler -func GetMarker(c *fiber.Ctx) error { +func getMarker(c *fiber.Ctx) error { markerID, err := strconv.Atoi(c.Params("markerId")) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid Marker ID"}) @@ -215,7 +264,7 @@ func GetMarker(c *fiber.Ctx) error { } // UpdateMarker updates an existing marker -func UpdateMarker(c *fiber.Ctx) error { +func updateMarker(c *fiber.Ctx) error { markerID, _ := strconv.Atoi(c.Params("markerID")) description := c.FormValue("description") @@ -227,7 +276,7 @@ func UpdateMarker(c *fiber.Ctx) error { } // DeleteMarkerHandler handles the HTTP request to delete a marker. -func DeleteMarkerHandler(c *fiber.Ctx) error { +func deleteMarkerHandler(c *fiber.Ctx) error { // Auth userID := c.Locals("userID").(int) userRole := c.Locals("role").(string) @@ -259,7 +308,7 @@ func DeleteMarkerHandler(c *fiber.Ctx) error { } // UploadMarkerPhotoToS3Handler to upload a file to S3 -func UploadMarkerPhotoToS3Handler(c *fiber.Ctx) error { +func uploadMarkerPhotoToS3Handler(c *fiber.Ctx) error { // Parse the multipart form form, err := c.MultipartForm() if err != nil { @@ -286,35 +335,8 @@ func UploadMarkerPhotoToS3Handler(c *fiber.Ctx) error { return c.JSON(fiber.Map{"urls": urls}) } -// DeleteObjectFromS3Handler handles requests to delete objects from S3. -func DeleteObjectFromS3Handler(c *fiber.Ctx) error { - var requestBody struct { - ObjectURL string `json:"objectUrl"` - } - if err := c.BodyParser(&requestBody); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Request body is not valid"}) - } - - // Ensure the object URL is not empty - if requestBody.ObjectURL == "" { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Object URL is required"}) - } - - // Call the service function to delete the object from S3 - if err := services.DeleteDataFromS3(requestBody.ObjectURL); err != nil { - // Determine if the error should be a 404 not found or a 500 internal server error - if err.Error() == "object not found" { - return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Object not found"}) - } - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to delete object from S3"}) - } - - // Return a success response - return c.SendStatus(fiber.StatusNoContent) -} - // / DISLIKE -func LeaveDislikeHandler(c *fiber.Ctx) error { +func leaveDislikeHandler(c *fiber.Ctx) error { // Auth userID := c.Locals("userID").(int) @@ -335,7 +357,7 @@ func LeaveDislikeHandler(c *fiber.Ctx) error { return c.SendStatus(fiber.StatusOK) } -func UndoDislikeHandler(c *fiber.Ctx) error { +func undoDislikeHandler(c *fiber.Ctx) error { // Auth userID := c.Locals("userID").(int) @@ -355,7 +377,7 @@ func UndoDislikeHandler(c *fiber.Ctx) error { return c.SendStatus(fiber.StatusOK) } -func GetUserMarkersHandler(c *fiber.Ctx) error { +func getUserMarkersHandler(c *fiber.Ctx) error { userID, ok := c.Locals("userID").(int) if !ok { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "User not authenticated"}) @@ -403,7 +425,7 @@ func GetUserMarkersHandler(c *fiber.Ctx) error { } // CheckDislikeStatus handler -func CheckDislikeStatus(c *fiber.Ctx) error { +func checkDislikeStatus(c *fiber.Ctx) error { userID := c.Locals("userID").(int) markerID, err := strconv.Atoi(c.Params("markerID")) if err != nil { @@ -419,7 +441,7 @@ func CheckDislikeStatus(c *fiber.Ctx) error { } // AddFavoriteHandler adds a new favorite marker for the user. -func AddFavoriteHandler(c *fiber.Ctx) error { +func addFavoriteHandler(c *fiber.Ctx) error { userData, err := services.GetUserFromContext(c) if err != nil { return err // fiber err @@ -456,7 +478,7 @@ func AddFavoriteHandler(c *fiber.Ctx) error { }) } -func RemoveFavoriteHandler(c *fiber.Ctx) error { +func removeFavoriteHandler(c *fiber.Ctx) error { userID, ok := c.Locals("userID").(int) if !ok { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "User ID not found"}) @@ -483,7 +505,7 @@ func RemoveFavoriteHandler(c *fiber.Ctx) error { } // GetFacilitiesHandler handles requests to get facilities by marker ID. -func GetFacilitiesHandler(c *fiber.Ctx) error { +func getFacilitiesHandler(c *fiber.Ctx) error { markerID, err := strconv.Atoi(c.Params("markerID")) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid Marker ID"}) @@ -514,7 +536,7 @@ func GetFacilitiesHandler(c *fiber.Ctx) error { return c.JSON(facilities) } -func SetMarkerFacilitiesHandler(c *fiber.Ctx) error { +func setMarkerFacilitiesHandler(c *fiber.Ctx) error { var req dto.FacilityRequest if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Cannot parse request"}) @@ -530,7 +552,7 @@ func SetMarkerFacilitiesHandler(c *fiber.Ctx) error { } // UpdateMarkersAddressesHandler handles the request to update all markers' addresses. -func UpdateMarkersAddressesHandler(c *fiber.Ctx) error { +func updateMarkersAddressesHandler(c *fiber.Ctx) error { updatedMarkers, err := services.UpdateMarkersAddresses() if err != nil { // Log the error and return a generic error message to the client diff --git a/backend/handlers/marker_location_api.go b/backend/handlers/marker_location_api.go index e4ea31ef..3c7fedf3 100644 --- a/backend/handlers/marker_location_api.go +++ b/backend/handlers/marker_location_api.go @@ -31,7 +31,7 @@ import ( // @Failure 404 {object} map[string]interface{} "No markers found within the specified distance" // @Failure 500 {object} map[string]interface{} "Internal server error" // @Router /markers/close [get] -func FindCloseMarkersHandler(c *fiber.Ctx) error { +func findCloseMarkersHandler(c *fiber.Ctx) error { var params dto.QueryParams if err := c.QueryParser(¶ms); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid query parameters"}) @@ -74,7 +74,7 @@ func FindCloseMarkersHandler(c *fiber.Ctx) error { }) } -func GetCurrentAreaMarkerRankingHandler(c *fiber.Ctx) error { +func getCurrentAreaMarkerRankingHandler(c *fiber.Ctx) error { latParam := c.Query("latitude") longParam := c.Query("longitude") limitParam := c.Query("limit", "5") // Default limit @@ -109,7 +109,7 @@ func GetCurrentAreaMarkerRankingHandler(c *fiber.Ctx) error { return c.JSON(markers) } -func GetMarkersClosebyAdmin(c *fiber.Ctx) error { +func getMarkersClosebyAdmin(c *fiber.Ctx) error { markers, err := services.CheckNearbyMarkersInDB() if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to retrieve markers: " + err.Error()}) @@ -118,7 +118,7 @@ func GetMarkersClosebyAdmin(c *fiber.Ctx) error { return c.JSON(markers) } -func GetWeatherByWGS84Handler(c *fiber.Ctx) error { +func getWeatherByWGS84Handler(c *fiber.Ctx) error { latParam := c.Query("latitude") longParam := c.Query("longitude") @@ -139,7 +139,7 @@ func GetWeatherByWGS84Handler(c *fiber.Ctx) error { return c.JSON(result) } -func ConvertWGS84ToWCONGNAMULHandler(c *fiber.Ctx) error { +func convertWGS84ToWCONGNAMULHandler(c *fiber.Ctx) error { latParam := c.Query("latitude") longParam := c.Query("longitude") @@ -158,7 +158,7 @@ func ConvertWGS84ToWCONGNAMULHandler(c *fiber.Ctx) error { return c.JSON(result) } -func IsInSouthKoreaHandler(c *fiber.Ctx) error { +func isInSouthKoreaHandler(c *fiber.Ctx) error { latParam := c.Query("latitude") longParam := c.Query("longitude") @@ -177,7 +177,8 @@ func IsInSouthKoreaHandler(c *fiber.Ctx) error { return c.JSON(fiber.Map{"result": result}) } -func SaveOfflineMapHandler(c *fiber.Ctx) error { +// DEPRECATED: Use version 2 +func saveOfflineMapHandler(c *fiber.Ctx) error { latParam := c.Query("latitude") longParam := c.Query("longitude") @@ -198,7 +199,7 @@ func SaveOfflineMapHandler(c *fiber.Ctx) error { return c.Download(pdf) } -func SaveOfflineMap2Handler(c *fiber.Ctx) error { +func saveOfflineMap2Handler(c *fiber.Ctx) error { latParam := c.Query("latitude") longParam := c.Query("longitude") diff --git a/backend/handlers/marker_report_api.go b/backend/handlers/marker_report_api.go index 76be5155..1e97cc6c 100644 --- a/backend/handlers/marker_report_api.go +++ b/backend/handlers/marker_report_api.go @@ -2,6 +2,7 @@ package handlers import ( "chulbong-kr/dto" + "chulbong-kr/middlewares" "chulbong-kr/services" "chulbong-kr/utils" "strconv" @@ -10,8 +11,18 @@ import ( "github.com/gofiber/fiber/v2" ) +// RegisterReportRoutes sets up the routes for report handling within the application. +func RegisterReportRoutes(api fiber.Router) { + reportGroup := api.Group("/reports") + { + reportGroup.Get("/all", getAllReportsHandler) + reportGroup.Get("/marker/:markerID", getMarkerReportsHandler) + reportGroup.Post("", middlewares.AuthSoftMiddleware, createReportHandler) + } +} + // GetAllReportsHandler retrieves all reports for all markers, grouped by MarkerID. -func GetAllReportsHandler(c *fiber.Ctx) error { +func getAllReportsHandler(c *fiber.Ctx) error { reports, err := services.GetAllReports() if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to get reports"}) @@ -38,7 +49,7 @@ func GetAllReportsHandler(c *fiber.Ctx) error { return c.JSON(response) } -func GetMarkerReportsHandler(c *fiber.Ctx) error { +func getMarkerReportsHandler(c *fiber.Ctx) error { markerID, err := strconv.Atoi(c.Params("markerID")) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid Marker ID"}) @@ -51,7 +62,7 @@ func GetMarkerReportsHandler(c *fiber.Ctx) error { return c.JSON(reports) } -func ReportHandler(c *fiber.Ctx) error { +func createReportHandler(c *fiber.Ctx) error { // Parse the multipart form form, err := c.MultipartForm() if err != nil { diff --git a/backend/handlers/oauth_api.go b/backend/handlers/oauth_api.go index 9c4d09a4..235a0571 100644 --- a/backend/handlers/oauth_api.go +++ b/backend/handlers/oauth_api.go @@ -12,7 +12,7 @@ import ( ) // GetGoogleAuthHandler generates a handler to redirect to Google OAuth2 -func GetGoogleAuthHandler(conf *oauth2.Config) fiber.Handler { +func getGoogleAuthHandler(conf *oauth2.Config) fiber.Handler { return func(c *fiber.Ctx) error { // Generate a state string for CSRF protection state := utils.GenerateState() @@ -28,7 +28,7 @@ func GetGoogleAuthHandler(conf *oauth2.Config) fiber.Handler { } // GetGoogleCallbackHandler generates a handler for the OAuth2 callback from Google -func GetGoogleCallbackHandler(conf *oauth2.Config) fiber.Handler { +func getGoogleCallbackHandler(conf *oauth2.Config) fiber.Handler { return func(c *fiber.Ctx) error { // Validate state state := c.Cookies("oauthstate") diff --git a/backend/handlers/rank_api.go b/backend/handlers/rank_api.go index 95c4e1fd..8687640b 100644 --- a/backend/handlers/rank_api.go +++ b/backend/handlers/rank_api.go @@ -6,13 +6,13 @@ import ( "github.com/gofiber/fiber/v2" ) -func GetMarkerRankingHandler(c *fiber.Ctx) error { +func getMarkerRankingHandler(c *fiber.Ctx) error { ranking := services.GetTopMarkers(10) // []dto.MarkerRank { MarkerID (string), Clicks (int) } return c.JSON(ranking) } -func GetUniqueVisitorCountHandler(c *fiber.Ctx) error { +func getUniqueVisitorCountHandler(c *fiber.Ctx) error { markerID := c.Query("markerId") if markerID == "" { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid Marker ID"}) @@ -23,7 +23,7 @@ func GetUniqueVisitorCountHandler(c *fiber.Ctx) error { return c.JSON(fiber.Map{"markerId": markerID, "visitors": count}) } -func GetAllUniqueVisitorCountHandler(c *fiber.Ctx) error { +func getAllUniqueVisitorCountHandler(c *fiber.Ctx) error { count := services.GetAllUniqueVisitorCounts() return c.JSON(count) } diff --git a/backend/handlers/toss_api.go b/backend/handlers/toss_api.go index 854e91b4..d6a373cc 100644 --- a/backend/handlers/toss_api.go +++ b/backend/handlers/toss_api.go @@ -13,6 +13,16 @@ var ( TOSS_PAYMENT_API_URL = "https://api.tosspayments.com/v1/payments/" ) +// RegisterTossPaymentRoutes sets up the routes for toss payments handling within the application. +func RegisterTossPaymentRoutes(api fiber.Router) { + tossGroup := api.Group("/payments/toss") + { + tossGroup.Post("/confirm", confirmToss) + // tossGroup.Get("/success", handlers.SuccessToss) + // tossGroup.Get("/fail", handlers.FailToss) + } +} + // frontend handles // func SuccessToss(c *fiber.Ctx) error { // // Extract query parameters @@ -65,7 +75,7 @@ var ( // return c.SendString("Failed") // } -func ConfirmToss(c *fiber.Ctx) error { +func confirmToss(c *fiber.Ctx) error { // Extract paymentKey, orderId, and amount from the request body var requestBody struct { PaymentKey string `json:"paymentKey"` diff --git a/backend/handlers/user_api.go b/backend/handlers/user_api.go index 3cc6b6ec..5ccfe9f7 100644 --- a/backend/handlers/user_api.go +++ b/backend/handlers/user_api.go @@ -2,6 +2,7 @@ package handlers import ( "chulbong-kr/dto" + "chulbong-kr/middlewares" "chulbong-kr/models" "chulbong-kr/services" "fmt" @@ -11,8 +12,22 @@ import ( "github.com/gofiber/fiber/v2" ) +// RegisterUserRoutes sets up the routes for user handling within the application. +func RegisterUserRoutes(api fiber.Router) { + userGroup := api.Group("/users") + { + userGroup.Use(middlewares.AuthMiddleware) + userGroup.Get("/me", profileHandler) + userGroup.Get("/favorites", getFavoritesHandler) + userGroup.Get("/reports", getMyReportsHandler) + userGroup.Patch("/me", updateUserHandler) + userGroup.Delete("/me", deleteUserHandler) + userGroup.Delete("/s3/objects", middlewares.AdminOnly, deleteObjectFromS3Handler) + } +} + // UpdateUserHandler -func UpdateUserHandler(c *fiber.Ctx) error { +func updateUserHandler(c *fiber.Ctx) error { userData, err := services.GetUserFromContext(c) if err != nil { return err // fiber err @@ -35,7 +50,7 @@ func UpdateUserHandler(c *fiber.Ctx) error { } // DeleteUserHandler deletes the currently authenticated user -func DeleteUserHandler(c *fiber.Ctx) error { +func deleteUserHandler(c *fiber.Ctx) error { userID, ok := c.Locals("userID").(int) if !ok { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "User ID not found"}) @@ -50,7 +65,7 @@ func DeleteUserHandler(c *fiber.Ctx) error { return c.SendStatus(fiber.StatusNoContent) // 204 for successful deletion with no content in response } -func ProfileHandler(c *fiber.Ctx) error { +func profileHandler(c *fiber.Ctx) error { userData, err := services.GetUserFromContext(c) if err != nil { return err // fiber err @@ -77,7 +92,7 @@ func ProfileHandler(c *fiber.Ctx) error { return c.JSON(user) } -func GetFavoritesHandler(c *fiber.Ctx) error { +func getFavoritesHandler(c *fiber.Ctx) error { userData, err := services.GetUserFromContext(c) if err != nil { return err // fiber err @@ -104,7 +119,7 @@ func GetFavoritesHandler(c *fiber.Ctx) error { } // GetMyReportsHandler handles requests to get all reports submitted by the logged-in user. -func GetMyReportsHandler(c *fiber.Ctx) error { +func getMyReportsHandler(c *fiber.Ctx) error { userID, ok := c.Locals("userID").(int) // Make sure to handle errors and cases where userID might not be set if !ok { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "User ID not found"}) @@ -121,3 +136,30 @@ func GetMyReportsHandler(c *fiber.Ctx) error { return c.JSON(reports) } + +// DeleteObjectFromS3Handler handles requests to delete objects from S3. +func deleteObjectFromS3Handler(c *fiber.Ctx) error { + var requestBody struct { + ObjectURL string `json:"objectUrl"` + } + if err := c.BodyParser(&requestBody); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Request body is not valid"}) + } + + // Ensure the object URL is not empty + if requestBody.ObjectURL == "" { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Object URL is required"}) + } + + // Call the service function to delete the object from S3 + if err := services.DeleteDataFromS3(requestBody.ObjectURL); err != nil { + // Determine if the error should be a 404 not found or a 500 internal server error + if err.Error() == "object not found" { + return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Object not found"}) + } + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to delete object from S3"}) + } + + // Return a success response + return c.SendStatus(fiber.StatusNoContent) +} diff --git a/backend/main.go b/backend/main.go index b58981d6..623a1774 100644 --- a/backend/main.go +++ b/backend/main.go @@ -12,11 +12,9 @@ import ( "fmt" "log" "os" - "path/filepath" "runtime" "runtime/debug" "strconv" - "strings" "time" "github.com/Alfex4936/tzf" @@ -34,18 +32,17 @@ import ( "github.com/gofiber/fiber/v2/middleware/monitor" "github.com/gofiber/fiber/v2/middleware/pprof" "github.com/gofiber/fiber/v2/middleware/requestid" + "github.com/gofiber/swagger" "github.com/redis/go-redis/v9" + "go.uber.org/zap" // "github.com/gofiber/storage/redis/v3" - "github.com/gofiber/swagger" + "github.com/gofiber/template/django/v3" "github.com/joho/godotenv" _ "github.com/joho/godotenv/autoload" // amqp "github.com/rabbitmq/amqp091-go" - "go.uber.org/zap" - "golang.org/x/oauth2" - "golang.org/x/oauth2/google" _ "chulbong-kr/docs" ) @@ -67,73 +64,8 @@ func main() { // Increase GOMAXPROCS runtime.GOMAXPROCS(runtime.NumCPU() * 2) // twice the number of CPUs - // Initialize redis - rdb := redis.NewClient(&redis.Options{ - Addr: os.Getenv("REDIS_HOST") + ":" + os.Getenv("REDIS_PORT"), - Username: os.Getenv("REDIS_USERNAME"), - Password: os.Getenv("REDIS_PASSWORD"), - DB: 0, - PoolSize: 10 * runtime.GOMAXPROCS(0), - MaxRetries: 5, - TLSConfig: &tls.Config{InsecureSkipVerify: true}, - }) - - // Ping the server to check connection - err := rdb.Ping(context.Background()).Err() - if err != nil { - log.Fatalf("Error connecting to Redis: %v", err) - } - - if os.Getenv("DEPLOYMENT") == "production" { - // Flush the Redis database to clear all keys - if err := rdb.FlushDB(context.Background()).Err(); err != nil { - log.Fatalf("Error flushing the Redis database: %v", err) - } else { - log.Println("Redis database flushed successfully.") - } - } - - services.RedisStore = rdb - - finder, err := tzf.NewDefaultFinder() - if err != nil { - log.Fatalf("Failed to initialize timezone finder: %v", err) - } - utils.TimeZoneFinder = finder - - // Message Broker - // connection, err := amqp.Dial(os.Getenv("LAVINMQ_HOST")) - // if err != nil { - // log.Panicf("Failed to connect to LavinMQ") - // } - // services.LavinMQClient = connection - - if err := utils.LoadBadWords("badwords.txt"); err != nil { - log.Fatalf("Failed to load bad words: %v", err) - } - - // Initialize global variables - setTokenExpirationTime() - services.AWS_REGION = os.Getenv("AWS_REGION") - services.S3_BUCKET_NAME = os.Getenv("AWS_BUCKET_NAME") - utils.LOGIN_TOKEN_COOKIE = os.Getenv("TOKEN_COOKIE") - - // Initialize database connection - if err := database.Connect(); err != nil { - panic(err) - } - - // OAuth2 Configuration - conf := &oauth2.Config{ - ClientID: os.Getenv("G_CLIENT_ID"), - ClientSecret: os.Getenv("G_CLIENT_SECRET"), - RedirectURL: os.Getenv("G_REDIRECT"), - Scopes: []string{"https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.profile"}, - Endpoint: google.Endpoint, - } - - // engine := html.New("./views", ".html") - engine := django.New("./views", ".django") + setUpExternalConnections() + setUpGlobals() // Initialize Fiber app app := fiber.New(fiber.Config{ @@ -149,7 +81,7 @@ func main() { JSONDecoder: json.Unmarshal, AppName: "chulbong-kr", Concurrency: 512 * 1024, - Views: engine, + Views: django.New("./views", ".django"), ErrorHandler: func(ctx *fiber.Ctx, err error) error { // Initial status code defaults to 500 code := fiber.StatusInternalServerError @@ -182,8 +114,97 @@ func main() { }) // app.Server().MaxConnsPerIP = 10 - go services.ProcessClickEventsBatch() + // Middlewares + setUpMiddlewares(app) + + // API + app.Get("/ws/:markerID", func(c *fiber.Ctx) error { + // Extract markerID from the parameter + markerID := c.Params("markerID") + reqID := c.Query("request-id") + + // Use GetBanDetails to check if the user is banned and get the remaining ban time + banned, remainingTime, err := services.WsRoomManager.GetBanDetails(markerID, reqID) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Internal server error"}) + } + if banned { + return c.Status(fiber.StatusForbidden).JSON(fiber.Map{ + "error": "User is banned", + "remainingTime": remainingTime.Seconds(), // Respond with remaining time in seconds + }) + } + + // Proceed with WebSocket upgrade if not banned + if websocket.IsWebSocketUpgrade(c) { + return c.Next() + } + return fiber.ErrUpgradeRequired + }, websocket.New(func(c *websocket.Conn) { + // Extract markerID from the parameter again if necessary + markerID := c.Params("markerID") + reqID := c.Query("request-id") + + // Now, the connection is already upgraded to WebSocket, and you've passed the ban check. + handlers.HandleChatRoomHandler(c, markerID, reqID) + }, websocket.Config{ + // Set the handshake timeout to a reasonable duration to prevent slowloris attacks. + HandshakeTimeout: 5 * time.Second, + + Origins: []string{"https://test.k-pullup.com", "https://www.k-pullup.com"}, + + EnableCompression: true, + + RecoverHandler: func(c *websocket.Conn) { + // Custom recover logic. By default, it logs the error and stack trace. + if r := recover(); r != nil { + fmt.Fprintf(os.Stderr, "WebSocket panic: %v\n", r) + debug.PrintStack() + c.WriteMessage(websocket.CloseMessage, []byte{}) + c.Close() + } + }, + })) + + // HTML + app.Get("/main", func(c *fiber.Ctx) error { + return c.Render("login", fiber.Map{}) + }) + + // Setup MY routes + api := app.Group("/api/v1") + handlers.RegisterAdminRoutes(api) + handlers.RegisterAuthRoutes(api) + handlers.RegisterUserRoutes(api) + handlers.RegisterMarkerRoutes(api) + handlers.RegisterCommentRoutes(api) + handlers.RegisterTossPaymentRoutes(api) + handlers.RegisterReportRoutes(api) + + // Cron jobs + services.RunAllCrons() + + // Server settings + serverAddr := fmt.Sprintf("0.0.0.0:%s", os.Getenv("SERVER_PORT")) + + // Check if the DEPLOYMENT is not local + if os.Getenv("DEPLOYMENT") == "production" { + // Send Slack notification + go utils.SendDeploymentSuccessNotification(app.Config().AppName, "fly.io") + + // Random ranking + go services.ResetAndRandomizeClickRanking() + } else { + log.Printf("There are %d APIs available in chulbong-kr", countAPIs(app)) + } + + // Start the Fiber app + if err := app.Listen(serverAddr); err != nil { + panic(err) + } +} +func setUpMiddlewares(app *fiber.App) { logger, _ := zap.NewProduction() app.Use(middlewares.ZapLogMiddleware(logger)) @@ -257,194 +278,67 @@ func main() { // app.Use(logger.New()) app.Get("/swagger/*", middlewares.AdminOnly, swagger.HandlerDefault) +} - app.Get("/ws/:markerID", func(c *fiber.Ctx) error { - // Extract markerID from the parameter - markerID := c.Params("markerID") - reqID := c.Query("request-id") - - // Use GetBanDetails to check if the user is banned and get the remaining ban time - banned, remainingTime, err := services.WsRoomManager.GetBanDetails(markerID, reqID) - if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Internal server error"}) - } - if banned { - return c.Status(fiber.StatusForbidden).JSON(fiber.Map{ - "error": "User is banned", - "remainingTime": remainingTime.Seconds(), // Respond with remaining time in seconds - }) - } - - // Proceed with WebSocket upgrade if not banned - if websocket.IsWebSocketUpgrade(c) { - return c.Next() - } - return fiber.ErrUpgradeRequired - }, websocket.New(func(c *websocket.Conn) { - // Extract markerID from the parameter again if necessary - markerID := c.Params("markerID") - reqID := c.Query("request-id") - - // Now, the connection is already upgraded to WebSocket, and you've passed the ban check. - handlers.HandleChatRoomHandler(c, markerID, reqID) - }, websocket.Config{ - // Set the handshake timeout to a reasonable duration to prevent slowloris attacks. - HandshakeTimeout: 5 * time.Second, - - Origins: []string{"https://test.k-pullup.com", "https://www.k-pullup.com"}, - - EnableCompression: true, - - RecoverHandler: func(c *websocket.Conn) { - // Custom recover logic. By default, it logs the error and stack trace. - if r := recover(); r != nil { - fmt.Fprintf(os.Stderr, "WebSocket panic: %v\n", r) - debug.PrintStack() - c.WriteMessage(websocket.CloseMessage, []byte{}) - c.Close() - } - }, - })) - - // HTML - app.Get("/main", func(c *fiber.Ctx) error { - return c.Render("login", fiber.Map{}) - }) - // Setup routes - api := app.Group("/api/v1") - - api.Get("/google", handlers.GetGoogleAuthHandler(conf)) - api.Get("/admin", middlewares.AdminOnly, func(c *fiber.Ctx) error { return c.JSON("good") }) - api.Post("/chat/ban/:markerID/:userID", middlewares.AdminOnly, handlers.BanUserHandler) - - adminGroup := api.Group("/admin") - { - adminGroup.Use(middlewares.AdminOnly) - adminGroup.Get("/dead", handlers.ListUnreferencedS3ObjectsHandler) +func setUpExternalConnections() { + // Initialize database connection + if err := database.Connect(); err != nil { + panic(err) } - // Authentication routes - authGroup := api.Group("/auth") - { - authGroup.Post("/signup", handlers.SignUpHandler) - authGroup.Post("/login", handlers.LoginHandler) - authGroup.Post("/logout", middlewares.AuthMiddleware, handlers.LogoutHandler) - authGroup.Get("/google/callback", handlers.GetGoogleCallbackHandler(conf)) - authGroup.Post("/verify-email/send", handlers.SendVerificationEmailHandler) - authGroup.Post("/verify-email/confirm", handlers.ValidateTokenHandler) - - // Finding password - authGroup.Post("/request-password-reset", handlers.RequestResetPasswordHandler) - authGroup.Post("/reset-password", handlers.ResetPasswordHandler) - } + // Initialize redis + rdb := redis.NewClient(&redis.Options{ + Addr: os.Getenv("REDIS_HOST") + ":" + os.Getenv("REDIS_PORT"), + Username: os.Getenv("REDIS_USERNAME"), + Password: os.Getenv("REDIS_PASSWORD"), + DB: 0, + PoolSize: 10 * runtime.GOMAXPROCS(0), + MaxRetries: 5, + TLSConfig: &tls.Config{InsecureSkipVerify: true}, + }) - // User routes - userGroup := api.Group("/users") - { - userGroup.Use(middlewares.AuthMiddleware) - userGroup.Get("/me", handlers.ProfileHandler) - userGroup.Get("/favorites", handlers.GetFavoritesHandler) - userGroup.Get("/reports", handlers.GetMyReportsHandler) - userGroup.Patch("/me", handlers.UpdateUserHandler) - userGroup.Delete("/me", handlers.DeleteUserHandler) - userGroup.Delete("/s3/objects", middlewares.AdminOnly, handlers.DeleteObjectFromS3Handler) + // Ping the server to check connection + err := rdb.Ping(context.Background()).Err() + if err != nil { + log.Fatalf("Error connecting to Redis: %v", err) } - // Marker routes - // api.Get("/markers2", handlers.GetAllMarkersHandler) - // api.Get("/markers2", handlers.GetAllMarkersProtoHandler) - api.Get("/markers", handlers.GetAllMarkersLocalHandler) - api.Get("/markers/new", handlers.GetAllNewMarkersHandler) - - // api.Get("/markers-addr", middlewares.AdminOnly, handlers.GetAllMarkersWithAddrHandler) - // api.Post("/markers-addr", middlewares.AdminOnly, handlers.UpdateMarkersAddressesHandler) - // api.Get("/markers-db", middlewares.AdminOnly, handlers.GetMarkersClosebyAdmin) - - api.Get("/markers/:markerId/details", middlewares.AuthSoftMiddleware, handlers.GetMarker) - api.Get("/markers/:markerID/facilities", handlers.GetFacilitiesHandler) - api.Get("/markers/close", handlers.FindCloseMarkersHandler) - api.Get("/markers/ranking", handlers.GetMarkerRankingHandler) - api.Get("/markers/unique-ranking", handlers.GetUniqueVisitorCountHandler) - api.Get("/markers/unique-ranking/all", handlers.GetAllUniqueVisitorCountHandler) - api.Get("/markers/area-ranking", handlers.GetCurrentAreaMarkerRankingHandler) - api.Get("/markers/convert", handlers.ConvertWGS84ToWCONGNAMULHandler) - api.Get("/markers/location-check", handlers.IsInSouthKoreaHandler) - api.Get("/markers/weather", handlers.GetWeatherByWGS84Handler) - - api.Get("/markers/save-offline", handlers.SaveOfflineMap2Handler) - - api.Post("/markers/upload", middlewares.AdminOnly, handlers.UploadMarkerPhotoToS3Handler) - - markerGroup := api.Group("/markers") - { - markerGroup.Use(middlewares.AuthMiddleware) - - markerGroup.Get("/my", handlers.GetUserMarkersHandler) - markerGroup.Get("/:markerID/dislike-status", handlers.CheckDislikeStatus) - // markerGroup.Get("/:markerId", handlers.GetMarker) - - markerGroup.Post("/new", handlers.CreateMarkerWithPhotosHandler) - markerGroup.Post("/facilities", handlers.SetMarkerFacilitiesHandler) - markerGroup.Post("/:markerID/dislike", handlers.LeaveDislikeHandler) - markerGroup.Post("/:markerID/favorites", handlers.AddFavoriteHandler) - - markerGroup.Put("/:markerID", handlers.UpdateMarker) - - markerGroup.Delete("/:markerID", handlers.DeleteMarkerHandler) - markerGroup.Delete("/:markerID/dislike", handlers.UndoDislikeHandler) - markerGroup.Delete("/:markerID/favorites", handlers.RemoveFavoriteHandler) + if os.Getenv("DEPLOYMENT") == "production" { + // Flush the Redis database to clear all keys + if err := rdb.FlushDB(context.Background()).Err(); err != nil { + log.Fatalf("Error flushing the Redis database: %v", err) + } else { + log.Println("Redis database flushed successfully.") + } } - // Comment routes - api.Get("/comments/:markerId/comments", handlers.LoadCommentsHandler) // no auth - - commentGroup := api.Group("/comments") - { - commentGroup.Use(middlewares.AuthMiddleware) - commentGroup.Post("", handlers.PostCommentHandler) - commentGroup.Patch("/:commentId", handlers.UpdateCommentHandler) - commentGroup.Delete("/:commentId", handlers.RemoveCommentHandler) - } + services.RedisStore = rdb - tossGroup := api.Group("/payments/toss") - { - tossGroup.Post("/confirm", handlers.ConfirmToss) - // tossGroup.Get("/success", handlers.SuccessToss) - // tossGroup.Get("/fail", handlers.FailToss) - } + // Message Broker + // connection, err := amqp.Dial(os.Getenv("LAVINMQ_HOST")) + // if err != nil { + // log.Panicf("Failed to connect to LavinMQ") + // } + // services.LavinMQClient = connection - reportGroup := api.Group("/reports") - { - reportGroup.Get("/all", handlers.GetAllReportsHandler) - reportGroup.Get("/marker/:markerID", handlers.GetMarkerReportsHandler) +} - reportGroup.Post("", middlewares.AuthSoftMiddleware, handlers.ReportHandler) +func setUpGlobals() { + finder, err := tzf.NewDefaultFinder() + if err != nil { + log.Fatalf("Failed to initialize timezone finder: %v", err) } + utils.TimeZoneFinder = finder - // app.Get("/example-optional/:param?", handlers.QueryParamsExample) - - // Cron jobs - services.CronCleanUpToken() - services.CronCleanUpPasswordTokens() - services.CronResetClickRanking() - services.StartOrphanedPhotosCleanupCron() - go cleanUpOldDirs(os.TempDir(), 2*time.Minute) - - serverAddr := fmt.Sprintf("0.0.0.0:%s", os.Getenv("SERVER_PORT")) - - // Check if the DEPLOYMENT is not local - if os.Getenv("DEPLOYMENT") == "production" { - // Send Slack notification - go utils.SendDeploymentSuccessNotification("chulbong-kr", "fly.io") - - // Random ranking - go services.ResetAndRandomizeClickRanking() + if err := utils.LoadBadWords("badwords.txt"); err != nil { + log.Fatalf("Failed to load bad words: %v", err) } - // Start the Fiber app - if err := app.Listen(serverAddr); err != nil { - panic(err) - } + // Initialize global variables + setTokenExpirationTime() + services.AWS_REGION = os.Getenv("AWS_REGION") + services.S3_BUCKET_NAME = os.Getenv("AWS_BUCKET_NAME") + utils.LOGIN_TOKEN_COOKIE = os.Getenv("TOKEN_COOKIE") } func setTokenExpirationTime() { @@ -461,33 +355,14 @@ func setTokenExpirationTime() { services.TOKEN_DURATION = time.Duration(durationInt) * time.Hour } -func cleanUpOldDirs(dir string, maxAge time.Duration) { - ticker := time.NewTicker(10 * time.Minute) - defer ticker.Stop() - - for range ticker.C { - files, err := os.ReadDir(dir) - if err != nil { - // log.Printf("Failed to list directories in %s: %v", dir, err) - continue - } - - now := time.Now() - for _, file := range files { - if file.IsDir() && strings.HasPrefix(file.Name(), "chulbongkr-") { - dirPath := filepath.Join(dir, file.Name()) - fileInfo, _ := file.Info() - - if now.Sub(fileInfo.ModTime()) > maxAge { - os.RemoveAll(dirPath) - - // if err := os.RemoveAll(dirPath); err != nil { - // log.Printf("Failed to delete old directory %s: %v", dirPath, err) - // } else { - // log.Printf("Deleted old directory %s", dirPath) - // } - } - } +// countAPIs counts the number of APIs in a Fiber app +func countAPIs(app *fiber.App) int { + numAPIs := 0 + for _, route := range app.GetRoutes(true) { + // Check if the route is for an API (skip middleware routes) + if route.Path[len(route.Path)-1] != '*' { + numAPIs++ } } + return numAPIs } diff --git a/backend/services/scheduler_service.go b/backend/services/scheduler_service.go index d1765789..b7ac7f46 100644 --- a/backend/services/scheduler_service.go +++ b/backend/services/scheduler_service.go @@ -4,14 +4,57 @@ import ( "chulbong-kr/database" "context" "fmt" + "os" + "path/filepath" + "strings" + "sync" + "time" "github.com/robfig/cron/v3" ) +func RunAllCrons() { + CronCleanUpToken() + CronCleanUpPasswordTokens() + CronResetClickRanking() + CronOrphanedPhotosCleanup() + CronCleanUpOldDirs() + CronProcessClickEventsBatch(RANK_UPDATE_TIME) +} + +// CronService holds a reference to a cron scheduler and its related setup. +type CronService struct { + cron *cron.Cron +} + +var ( + instance *CronService + once sync.Once +) + +// GetCronService returns a singleton instance of CronService, creating it if not already present. +func GetCronService() *CronService { + once.Do(func() { + c := cron.New(cron.WithChain( + cron.Recover(cron.DefaultLogger), + )) + instance = &CronService{ + cron: c, + } + instance.cron.Start() + }) + return instance +} + +// Schedule a new job with a cron specification. +func (cs *CronService) Schedule(spec string, cmd func()) (cron.EntryID, error) { + job := cron.FuncJob(cmd) + return cs.cron.AddJob(spec, job) +} + func CronCleanUpPasswordTokens() { - c := cron.New() - _, err := c.AddFunc("@daily", func() { - fmt.Println("Running cleanup job...") + c := GetCronService() + _, err := c.Schedule("@daily", func() { if err := DeleteExpiredPasswordTokens(); err != nil { // Log the error fmt.Printf("Error deleting expired tokens: %v\n", err) @@ -24,13 +67,12 @@ func CronCleanUpPasswordTokens() { fmt.Printf("Error scheduling the token cleanup job: %v\n", err) return } - c.Start() } func CronResetClickRanking() { - c := cron.New() - _, err := c.AddFunc("0 2 * * 1", func() { // 2 AM every Monday + c := GetCronService() + _, err := c.Schedule("0 2 * * 1", func() { // 2 AM every Monday // handlers.CacheMutex.Lock() // handlers.MarkersLocalCache = nil @@ -51,12 +93,39 @@ func CronResetClickRanking() { fmt.Printf("Error scheduling the marker ranking cleanup job: %v\n", err) return } - c.Start() +} + +func CronProcessClickEventsBatch(interval time.Duration) { + c := GetCronService() + var spec string + + switch { + case interval < time.Hour: + // Minutes interval + minutes := int(interval.Minutes()) + spec = fmt.Sprintf("*/%d * * * *", minutes) + case interval >= time.Hour && interval < 24*time.Hour: + // Hourly interval + hours := int(interval.Hours()) + spec = fmt.Sprintf("0 */%d * * *", hours) + default: + // Default to every 10 minutes if the interval is oddly long or unspecified + spec = "*/10 * * * *" + } + + _, err := c.Schedule(spec, func() { + fmt.Println("Processing batch job...") + ProcessClickEventsBatch() + }) + if err != nil { + fmt.Printf("Error setting up cron job: %v\n", err) + return + } } func CronCleanUpToken() { - c := cron.New() - _, err := c.AddFunc("@daily", func() { + c := GetCronService() + _, err := c.Schedule("@daily", func() { if err := DeleteExpiredTokens(); err != nil { // Log the error fmt.Printf("Error deleting expired tokens: %v\n", err) @@ -69,13 +138,12 @@ func CronCleanUpToken() { fmt.Printf("Error scheduling the token cleanup job: %v\n", err) return } - c.Start() } -// StartOrphanedPhotosCleanupCron starts the cron job for cleaning up orphaned photos. -func StartOrphanedPhotosCleanupCron() { - c := cron.New() - _, err := c.AddFunc("@daily", func() { +// CronOrphanedPhotosCleanup starts the cron job for cleaning up orphaned photos. +func CronOrphanedPhotosCleanup() { + c := GetCronService() + _, err := c.Schedule("@daily", func() { if err := deleteOrphanedPhotos(); err != nil { fmt.Printf("Error cleaning up orphaned photos: %v\n", err) } else { @@ -86,7 +154,57 @@ func StartOrphanedPhotosCleanupCron() { fmt.Printf("Error scheduling the orphaned photos cleanup job: %v\n", err) return } - c.Start() +} + +// CronCleanUpOldDirs periodically checks and removes directories older than maxAge. +func CronCleanUpOldDirs() { + c := GetCronService() + tempDir := os.TempDir() + maxAge := 2 * time.Minute + + _, err := c.Schedule("*/10 * * * *", func() { // every 10 minutes + if err := cleanTempDir(tempDir, maxAge); err != nil { + fmt.Printf("Error cleaning up orphaned photos: %v\n", err) + } else { + fmt.Println("Orphaned photos cleanup executed successfully") + } + }) + if err != nil { + fmt.Printf("Error scheduling the orphaned photos cleanup job: %v\n", err) + return + } +} + +// -----HELPER + +// cleanTempDir removes temp directories that are older than the maxAge. +func cleanTempDir(dir string, maxAge time.Duration) error { + files, err := os.ReadDir(dir) + if err != nil { + return fmt.Errorf("failed to list directories in %s: %w", dir, err) + } + + now := time.Now() + for _, file := range files { + if file.IsDir() && strings.HasPrefix(file.Name(), "chulbongkr-") { + dirPath := filepath.Join(dir, file.Name()) + fileInfo, err := file.Info() + if err != nil { + // log.Printf("Failed to get info for directory %s: %v", dirPath, err) + continue + } + + if now.Sub(fileInfo.ModTime()) > maxAge { + os.RemoveAll(dirPath) + //if err := os.RemoveAll(dirPath); err != nil { + // log.Printf("Failed to delete old directory %s: %v", dirPath, err) + //} else { + // log.Printf("Deleted old directory %s", dirPath) + //} + } + } + } + return nil } // deleteOrphanedPhotos checks for photos without a corresponding marker and deletes them. diff --git a/backend/services/user_service.go b/backend/services/user_service.go index 5240742a..74e10af6 100644 --- a/backend/services/user_service.go +++ b/backend/services/user_service.go @@ -187,8 +187,6 @@ func UpdateUserProfile(userID int, updateReq *dto.UpdateUserRequest) (*models.Us return nil, fmt.Errorf("error fetching updated user: %w", err) } - ResetCache("/api/v1/users/me_GET") - return updatedUser, nil } diff --git a/backend/utils/badword_util.go b/backend/utils/badword_util.go index 46ecf0b7..a753ce82 100644 --- a/backend/utils/badword_util.go +++ b/backend/utils/badword_util.go @@ -76,7 +76,7 @@ func RemoveURLs(input string) string { // return false, nil // } -// LoadBadWords loads bad words from a file into memory +// LoadBadWords loads bad words from a file into memory with optimizations. func LoadBadWords(filePath string) error { file, err := os.Open(filePath) if err != nil { @@ -84,7 +84,16 @@ func LoadBadWords(filePath string) error { } defer file.Close() + // Estimate the number of words if known or use a high number. + const estimatedWords = 1000 + badWordsList = make([]string, 0, estimatedWords) + + // Create a buffer and attach it to scanner. scanner := bufio.NewScanner(file) + const maxCapacity = 10 * 1024 // 10KB; + buf := make([]byte, maxCapacity) + scanner.Buffer(buf, maxCapacity) + for scanner.Scan() { word := scanner.Text() badWordsList = append(badWordsList, word) @@ -94,6 +103,9 @@ func LoadBadWords(filePath string) error { return err } - CompileBadWordsPattern() + // Optimize memory usage by shrinking the slice to the actual number of words. + badWordsList = append([]string{}, badWordsList...) + + go CompileBadWordsPattern() // Compile in a goroutine if it's safe to do asynchronously. return nil } diff --git a/backend/utils/map_download_util.go b/backend/utils/map_download_util.go index 697ed136..3e1ad02b 100644 --- a/backend/utils/map_download_util.go +++ b/backend/utils/map_download_util.go @@ -7,6 +7,7 @@ import ( "image/draw" "image/png" "io" + "math" "net/http" "os" "path" @@ -273,3 +274,34 @@ func LoadWebP(filePath string) (image.Image, error) { defer file.Close() return webp.Decode(file) } + +func PlaceMarkerOnImageDynamic(CX, CY, centerCX, centerCY float64, imageWidth, imageHeight, zoomLevel int) (int, int) { + // Base scale for zoom level 0 (fully zoomed out) + const baseScale = 0.3125 + + // full coordinate range visible at zoom level 0 for a standard image size + const baseWidth = 1280 // Standard width at zoom level 0 + const baseHeight = 1080 // Standard height at zoom level 0 + const baseCXRange = 3190.0 // Total CX range at zoom level 0 + const baseCYRange = 3190.0 // Total CY range at zoom level 0 + + // Calculate scale factor based on current zoom level + scale := baseScale * math.Pow(2, float64(zoomLevel)) + + // Calculate the actual number of coordinate units per pixel at the current zoom level and image size + cxUnitsPerPixel := (baseCXRange / float64(baseWidth)) / scale * float64(baseWidth) / float64(imageWidth) + cyUnitsPerPixel := (baseCYRange / float64(baseHeight)) / scale * float64(baseHeight) / float64(imageHeight) + + // Calculate the pixel offset from the center + deltaX := CX - centerCX + deltaY := CY - centerCY + + pixelOffsetX := deltaX / cxUnitsPerPixel + pixelOffsetY := deltaY / cyUnitsPerPixel + + // Calculate the absolute pixel coordinates + markerPosX := (imageWidth / 2) + int(pixelOffsetX) + markerPosY := (imageHeight / 2) - int(pixelOffsetY) + + return markerPosX, markerPosY +} diff --git a/backend/utils/map_util.go b/backend/utils/map_util.go index b2abbfbb..14748ab9 100644 --- a/backend/utils/map_util.go +++ b/backend/utils/map_util.go @@ -55,6 +55,20 @@ const ( const RadiusOfEarthMeters float64 = 6370986 const KoreaTimeZone = "Asia/Seoul" +const ( + // Constants related to the WGS84 ellipsoid. + aWGS84 float64 = 6378137 // Semi-major axis. + flatteningFactor float64 = 0.0033528106647474805 + + // Constants for Korea TM projection. + k0 float64 = 1 // Scale factor. + dx float64 = 500000 // False Easting. + dy float64 = 200000 // False Northing. + lat0 float64 = 38 // Latitude of origin. + lon0 float64 = 127 // Longitude of origin. + scaleFactor float64 = 2.5 +) + var TimeZoneFinder tzf.F // Haversine formula @@ -114,9 +128,10 @@ type WCONGNAMULCoord struct { // ConvertWGS84ToWCONGNAMUL converts coordinates from WGS84 to WCONGNAMUL. func ConvertWGS84ToWCONGNAMUL(lat, long float64) WCONGNAMULCoord { - x, y := transformWGS84ToKoreaTM(6378137, 0.0033528106647474805, 500000, 200000, 1, 38, 127, lat, long) - x = math.Round(x * 2.5) - y = math.Round(y * 2.5) + x, y := transformWGS84ToKoreaTM(aWGS84, flatteningFactor, dx, dy, k0, lat0, lon0, lat, long) + // x, y := transformWGS84ToKoreaTM(aWGS84, flatteningFactor, dx, dy, k0, lat0, lon0, lat, long) + x = math.Round(x * scaleFactor) + y = math.Round(y * scaleFactor) return WCONGNAMULCoord{X: x, Y: y} }