From 5638b5c92d14097af57dcaafe74189e8a0cbec2c Mon Sep 17 00:00:00 2001 From: litsynp Date: Sat, 21 Oct 2023 00:18:45 +0900 Subject: [PATCH] feat: check nickname API --- cmd/server/handler/user_handler.go | 24 +++++++++ cmd/server/main.go | 2 +- cmd/server/router.go | 1 + .../000006_add_users_nickname_uix.down.sql | 3 ++ .../000006_add_users_nickname_uix.up.sql | 4 ++ internal/domain/user/service.go | 5 ++ internal/domain/user/tests/service_test.go | 46 ++++++++++++++++ internal/domain/user/user.go | 1 + internal/domain/user/view.go | 8 +++ internal/postgres/user_store.go | 33 ++++++++++++ pkg/docs/docs.go | 54 ++++++++++++++++++- pkg/docs/swagger.json | 54 ++++++++++++++++++- pkg/docs/swagger.yaml | 35 +++++++++++- 13 files changed, 266 insertions(+), 4 deletions(-) create mode 100644 db/migrations/000006_add_users_nickname_uix.down.sql create mode 100644 db/migrations/000006_add_users_nickname_uix.up.sql diff --git a/cmd/server/handler/user_handler.go b/cmd/server/handler/user_handler.go index f3345001..e341a826 100644 --- a/cmd/server/handler/user_handler.go +++ b/cmd/server/handler/user_handler.go @@ -52,6 +52,30 @@ func (h *UserHandler) RegisterUser(w http.ResponseWriter, r *http.Request) { commonviews.Created(w, nil, res) } +// CheckUserNickname godoc +// @Summary 닉네임 중복 여부를 조회합니다. +// @Description +// @Tags users +// @Accept json +// @Produce json +// @Param request body user.CheckNicknameRequest true "사용자 닉네임 중복 조회 요청" +// @Success 200 {object} user.CheckNicknameView +// @Router /users/check/nickname [post] +func (h *UserHandler) CheckUserNickname(w http.ResponseWriter, r *http.Request) { + var checkUserNicknameRequest user.CheckNicknameRequest + if err := commonviews.ParseBody(w, r, &checkUserNicknameRequest); err != nil { + return + } + + exists, err := h.userService.ExistsByNickname(checkUserNicknameRequest.Nickname) + if err != nil { + commonviews.InternalServerError(w, nil, err.Error()) + return + } + + commonviews.OK(w, nil, user.CheckNicknameView{IsAvailable: !exists}) +} + // FindUserStatusByEmail godoc // @Summary 이메일로 유저의 가입 상태를 조회합니다. // @Description diff --git a/cmd/server/main.go b/cmd/server/main.go index 9e00706b..6d026f9e 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -11,7 +11,7 @@ import ( ) // @title 이웃집멍냥 API 문서 -// @version 0.4.0 +// @version 0.5.0 // @description 이웃집멍냥 백엔드 API 문서입니다. // @termsOfService http://swagger.io/terms/ diff --git a/cmd/server/router.go b/cmd/server/router.go index 6cda3fe8..6a89349d 100644 --- a/cmd/server/router.go +++ b/cmd/server/router.go @@ -91,6 +91,7 @@ func NewRouter(app *firebaseinfra.FirebaseApp) *chi.Mux { }) r.Route("/users", func(r chi.Router) { r.Post("/", userHandler.RegisterUser) + r.Post("/check/nickname", userHandler.CheckUserNickname) r.Post("/status", userHandler.FindUserStatusByEmail) r.Get("/me", userHandler.FindMyProfile) r.Put("/me", userHandler.UpdateMyProfile) diff --git a/db/migrations/000006_add_users_nickname_uix.down.sql b/db/migrations/000006_add_users_nickname_uix.down.sql new file mode 100644 index 00000000..99fd617d --- /dev/null +++ b/db/migrations/000006_add_users_nickname_uix.down.sql @@ -0,0 +1,3 @@ +ALTER TABLE + users + DROP CONSTRAINT users_nickname_uix; diff --git a/db/migrations/000006_add_users_nickname_uix.up.sql b/db/migrations/000006_add_users_nickname_uix.up.sql new file mode 100644 index 00000000..2c9fdced --- /dev/null +++ b/db/migrations/000006_add_users_nickname_uix.up.sql @@ -0,0 +1,4 @@ +ALTER TABLE + users + ADD + CONSTRAINT users_nickname_uix UNIQUE (nickname); diff --git a/internal/domain/user/service.go b/internal/domain/user/service.go index 31b5396e..219b98a4 100644 --- a/internal/domain/user/service.go +++ b/internal/domain/user/service.go @@ -23,6 +23,7 @@ type UserServicer interface { RegisterUser(registerUserRequest *RegisterUserRequest) (*RegisterUserResponse, error) FindUserByEmail(email string) (*UserWithProfileImage, error) FindUserByUID(uid string) (*FindUserResponse, error) + ExistsByNickname(nickname string) (bool, error) FindUserStatusByEmail(email string) (*UserStatus, error) UpdateUserByUID(uid string, nickname string, profileImageID int) (*UserWithProfileImage, error) AddPetsToOwner(uid string, addPetsRequest pet.AddPetsToOwnerRequest) ([]pet.PetView, error) @@ -81,6 +82,10 @@ func (service *UserService) FindUserByUID(uid string) (*FindUserResponse, error) }, nil } +func (service *UserService) ExistsByNickname(nickname string) (bool, error) { + return service.userStore.ExistsByNickname(nickname) +} + func (service *UserService) FindUserStatusByEmail(email string) (*UserStatus, error) { userStatus, err := service.userStore.FindUserStatusByEmail(email) if err != nil { diff --git a/internal/domain/user/tests/service_test.go b/internal/domain/user/tests/service_test.go index a6ff9df3..aa881639 100644 --- a/internal/domain/user/tests/service_test.go +++ b/internal/domain/user/tests/service_test.go @@ -180,6 +180,52 @@ func TestUserService(t *testing.T) { }) }) + t.Run("ExistsByNickname", func(t *testing.T) { + t.Run("사용자의 닉네임이 존재하지 않을 경우 false를 반환한다", func(t *testing.T) { + tearDown := setUp(t) + defer tearDown(t) + + media_service := media.NewMediaService(postgres.NewMediaPostgresStore(db), nil) + + service := user.NewUserService(postgres.NewUserPostgresStore(db), postgres.NewPetPostgresStore(db), media_service) + + exists, _ := service.ExistsByNickname("non-existent") + if exists { + t.Errorf("got %v want %v", exists, false) + } + }) + + t.Run("사용자의 닉네임이 존재할 경우 true를 반환한다", func(t *testing.T) { + tearDown := setUp(t) + defer tearDown(t) + + media_service := media.NewMediaService(postgres.NewMediaPostgresStore(db), nil) + profile_image, _ := media_service.CreateMedia(&media.Media{ + MediaType: media.IMAGE_MEDIA_TYPE, + URL: "http://example.com", + }) + + service := user.NewUserService(postgres.NewUserPostgresStore(db), postgres.NewPetPostgresStore(db), media_service) + + user := &user.RegisterUserRequest{ + Email: "test@example.com", + Nickname: "nickname", + Fullname: "fullname", + ProfileImageID: profile_image.ID, + FirebaseProviderType: "kakao", + FirebaseUID: "uid", + } + + _, _ = service.RegisterUser(user) + + exists, _ := service.ExistsByNickname(user.Nickname) + + if !exists { + t.Errorf("got %v want %v", exists, true) + } + }) + }) + t.Run("FindUserStatusByEmail", func(t *testing.T) { t.Run("사용자의 상태를 반환한다", func(t *testing.T) { tearDown := setUp(t) diff --git a/internal/domain/user/user.go b/internal/domain/user/user.go index 3b01b9c6..36b602d9 100644 --- a/internal/domain/user/user.go +++ b/internal/domain/user/user.go @@ -45,6 +45,7 @@ type UserStore interface { CreateUser(request *RegisterUserRequest) (*User, error) FindUserByEmail(email string) (*UserWithProfileImage, error) FindUserByUID(uid string) (*UserWithProfileImage, error) + ExistsByNickname(nickname string) (bool, error) FindUserStatusByEmail(email string) (*UserStatus, error) UpdateUserByUID(uid string, nickname string, profileImageID int) (*User, error) } diff --git a/internal/domain/user/view.go b/internal/domain/user/view.go index e27aa47a..5f825299 100644 --- a/internal/domain/user/view.go +++ b/internal/domain/user/view.go @@ -29,6 +29,14 @@ type FindUserResponse struct { FirebaseUID string `json:"fbUid"` } +type CheckNicknameRequest struct { + Nickname string `json:"nickname" validate:"required"` +} + +type CheckNicknameView struct { + IsAvailable bool `json:"isAvailable"` +} + type UserStatusRequest struct { Email string `json:"email" validate:"required,email"` } diff --git a/internal/postgres/user_store.go b/internal/postgres/user_store.go index 4d15b8b0..076dbf1e 100644 --- a/internal/postgres/user_store.go +++ b/internal/postgres/user_store.go @@ -146,6 +146,39 @@ func (s *UserPostgresStore) FindUserByUID(uid string) (*user.UserWithProfileImag return user, nil } +func (s *UserPostgresStore) ExistsByNickname(nickname string) (bool, error) { + var exists bool + + tx, _ := s.db.Begin() + err := tx.QueryRow(` + SELECT + CASE + WHEN EXISTS ( + SELECT + 1 + FROM + users + WHERE + nickname = $1 AND + deleted_at IS NULL + ) THEN TRUE + ELSE FALSE + END + `, + nickname, + ).Scan(&exists) + if err != nil { + return false, err + } + + err = tx.Commit() + if err != nil { + return false, err + } + + return exists, nil +} + func (s *UserPostgresStore) FindUserStatusByEmail(email string) (*user.UserStatus, error) { var userStatus user.UserStatus diff --git a/pkg/docs/docs.go b/pkg/docs/docs.go index 66fb87f2..7431f0ef 100644 --- a/pkg/docs/docs.go +++ b/pkg/docs/docs.go @@ -193,6 +193,39 @@ const docTemplate = `{ } } }, + "/users/check/nickname": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "닉네임 중복 여부를 조회합니다.", + "parameters": [ + { + "description": "사용자 닉네임 중복 조회 요청", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/user.CheckNicknameRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/user.CheckNicknameView" + } + } + } + } + }, "/users/me": { "get": { "security": [ @@ -551,6 +584,25 @@ const docTemplate = `{ } } }, + "user.CheckNicknameRequest": { + "type": "object", + "required": [ + "nickname" + ], + "properties": { + "nickname": { + "type": "string" + } + } + }, + "user.CheckNicknameView": { + "type": "object", + "properties": { + "isAvailable": { + "type": "boolean" + } + } + }, "user.FindUserResponse": { "type": "object", "properties": { @@ -731,7 +783,7 @@ const docTemplate = `{ // SwaggerInfo holds exported Swagger Info so clients can modify it var SwaggerInfo = &swag.Spec{ - Version: "0.4.0", + Version: "0.5.0", Host: "", BasePath: "/api", Schemes: []string{}, diff --git a/pkg/docs/swagger.json b/pkg/docs/swagger.json index ed15f38e..59238dc3 100644 --- a/pkg/docs/swagger.json +++ b/pkg/docs/swagger.json @@ -12,7 +12,7 @@ "name": "Apache 2.0", "url": "http://www.apache.org/licenses/LICENSE-2.0.html" }, - "version": "0.4.0" + "version": "0.5.0" }, "basePath": "/api", "paths": { @@ -186,6 +186,39 @@ } } }, + "/users/check/nickname": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "닉네임 중복 여부를 조회합니다.", + "parameters": [ + { + "description": "사용자 닉네임 중복 조회 요청", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/user.CheckNicknameRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/user.CheckNicknameView" + } + } + } + } + }, "/users/me": { "get": { "security": [ @@ -544,6 +577,25 @@ } } }, + "user.CheckNicknameRequest": { + "type": "object", + "required": [ + "nickname" + ], + "properties": { + "nickname": { + "type": "string" + } + } + }, + "user.CheckNicknameView": { + "type": "object", + "properties": { + "isAvailable": { + "type": "boolean" + } + } + }, "user.FindUserResponse": { "type": "object", "properties": { diff --git a/pkg/docs/swagger.yaml b/pkg/docs/swagger.yaml index 058ee320..78efbc99 100644 --- a/pkg/docs/swagger.yaml +++ b/pkg/docs/swagger.yaml @@ -134,6 +134,18 @@ definitions: weight_in_kg: type: number type: object + user.CheckNicknameRequest: + properties: + nickname: + type: string + required: + - nickname + type: object + user.CheckNicknameView: + properties: + isAvailable: + type: boolean + type: object user.FindUserResponse: properties: email: @@ -258,7 +270,7 @@ info: url: http://www.apache.org/licenses/LICENSE-2.0.html termsOfService: http://swagger.io/terms/ title: 이웃집멍냥 API 문서 - version: 0.4.0 + version: 0.5.0 paths: /auth/callback/kakao: get: @@ -370,6 +382,27 @@ paths: summary: 파이어베이스 가입 이후 정보를 입력 받아 유저를 생성합니다. tags: - users + /users/check/nickname: + post: + consumes: + - application/json + parameters: + - description: 사용자 닉네임 중복 조회 요청 + in: body + name: request + required: true + schema: + $ref: '#/definitions/user.CheckNicknameRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/user.CheckNicknameView' + summary: 닉네임 중복 여부를 조회합니다. + tags: + - users /users/me: get: produces: