From b4376b79ccb6834a36c1c08f5cea953ea7790434 Mon Sep 17 00:00:00 2001 From: JokerTrickster Date: Thu, 14 Nov 2024 17:17:35 +0900 Subject: [PATCH] =?UTF-8?q?{feat}=20-=20=ED=94=84=EB=A1=9C=ED=95=84=20?= =?UTF-8?q?=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=A0=80=EC=9E=A5=ED=95=98?= =?UTF-8?q?=EA=B8=B0=20api=20=EA=B5=AC=ED=98=84=20\n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/docs/docs.go | 57 ++++++++++++++++ src/docs/swagger.json | 57 ++++++++++++++++ src/docs/swagger.yaml | 50 ++++++++++++++ src/features/user/handler/index.go | 1 + .../user/handler/updateProfileUserHandler.go | 65 +++++++++++++++++++ .../model/entity/updateProfileUserEntity.go | 8 +++ .../user/model/interface/IUserHandler.go | 4 ++ .../user/model/interface/IUserRepository.go | 4 ++ .../user/model/interface/IUserUseCase.go | 4 ++ src/features/user/model/response/getUser.go | 2 + .../user/model/response/updateProfileUser.go | 5 ++ src/features/user/repository/repository.go | 4 ++ .../repository/updateProfileUserRepository.go | 27 ++++++++ .../user/usecase/updateProfileUserUseCase.go | 47 ++++++++++++++ src/features/user/usecase/usecase.go | 14 +++- src/utils/aws/init.go | 1 + src/utils/aws/s3.go | 8 +++ src/utils/db/mysql/gormDB.go | 1 + src/utils/db/mysql/table.sql | 1 + 19 files changed, 359 insertions(+), 1 deletion(-) create mode 100644 src/features/user/handler/updateProfileUserHandler.go create mode 100644 src/features/user/model/entity/updateProfileUserEntity.go create mode 100644 src/features/user/model/response/updateProfileUser.go create mode 100644 src/features/user/repository/updateProfileUserRepository.go create mode 100644 src/features/user/usecase/updateProfileUserUseCase.go diff --git a/src/docs/docs.go b/src/docs/docs.go index 45eb1ac..9401f43 100644 --- a/src/docs/docs.go +++ b/src/docs/docs.go @@ -1061,6 +1061,49 @@ const docTemplate = `{ } } }, + "/v0.1/users/profiles/image": { + "post": { + "description": "■ errCode with 400\nPARAM_BAD : 파라미터 오류\nUSER_NOT_FOUND : 유저가 존재하지 않음\n■ errCode with 401\nINVALID_AUTH_CODE : 인증 코드 검증 실패\nTOKEN_BAD : 잘못된 토큰\nINVALID_ACCESS_TOKEN : 잘못된 액세스 토큰\n\n■ errCode with 500\nINTERNAL_SERVER : 내부 로직 처리 실패\nINTERNAL_DB : DB 처리 실패\nPLAYER_STATE_CHANGE_FAILED : 플레이어 상태 변경 실패", + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "유저 프로필 이미지 저장하기", + "parameters": [ + { + "type": "string", + "description": "accessToken", + "name": "tkn", + "in": "header", + "required": true + }, + { + "type": "file", + "description": "프로필 이미지 파일", + "name": "image", + "in": "formData" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.ResUpdateProfileUser" + } + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, "/v0.1/users/{userID}": { "get": { "description": "■ errCode with 400\nPARAM_BAD : 파라미터 오류\nUSER_NOT_FOUND : 유저가 존재하지 않음\n■ errCode with 401\nINVALID_AUTH_CODE : 인증 코드 검증 실패\nTOKEN_BAD : 잘못된 토큰\nINVALID_ACCESS_TOKEN : 잘못된 액세스 토큰\n\n■ errCode with 500\nINTERNAL_SERVER : 내부 로직 처리 실패\nINTERNAL_DB : DB 처리 실패\nPLAYER_STATE_CHANGE_FAILED : 플레이어 상태 변경 실패", @@ -1722,9 +1765,15 @@ const docTemplate = `{ "email": { "type": "string" }, + "image": { + "type": "string" + }, "name": { "type": "string" }, + "push": { + "type": "boolean" + }, "sex": { "type": "string" } @@ -1866,6 +1915,14 @@ const docTemplate = `{ } } }, + "response.ResUpdateProfileUser": { + "type": "object", + "properties": { + "image": { + "type": "string" + } + } + }, "response.ResV02GoogleOauth": { "type": "object", "properties": { diff --git a/src/docs/swagger.json b/src/docs/swagger.json index 0c4674c..c13e367 100644 --- a/src/docs/swagger.json +++ b/src/docs/swagger.json @@ -1050,6 +1050,49 @@ } } }, + "/v0.1/users/profiles/image": { + "post": { + "description": "■ errCode with 400\nPARAM_BAD : 파라미터 오류\nUSER_NOT_FOUND : 유저가 존재하지 않음\n■ errCode with 401\nINVALID_AUTH_CODE : 인증 코드 검증 실패\nTOKEN_BAD : 잘못된 토큰\nINVALID_ACCESS_TOKEN : 잘못된 액세스 토큰\n\n■ errCode with 500\nINTERNAL_SERVER : 내부 로직 처리 실패\nINTERNAL_DB : DB 처리 실패\nPLAYER_STATE_CHANGE_FAILED : 플레이어 상태 변경 실패", + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "유저 프로필 이미지 저장하기", + "parameters": [ + { + "type": "string", + "description": "accessToken", + "name": "tkn", + "in": "header", + "required": true + }, + { + "type": "file", + "description": "프로필 이미지 파일", + "name": "image", + "in": "formData" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.ResUpdateProfileUser" + } + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, "/v0.1/users/{userID}": { "get": { "description": "■ errCode with 400\nPARAM_BAD : 파라미터 오류\nUSER_NOT_FOUND : 유저가 존재하지 않음\n■ errCode with 401\nINVALID_AUTH_CODE : 인증 코드 검증 실패\nTOKEN_BAD : 잘못된 토큰\nINVALID_ACCESS_TOKEN : 잘못된 액세스 토큰\n\n■ errCode with 500\nINTERNAL_SERVER : 내부 로직 처리 실패\nINTERNAL_DB : DB 처리 실패\nPLAYER_STATE_CHANGE_FAILED : 플레이어 상태 변경 실패", @@ -1711,9 +1754,15 @@ "email": { "type": "string" }, + "image": { + "type": "string" + }, "name": { "type": "string" }, + "push": { + "type": "boolean" + }, "sex": { "type": "string" } @@ -1855,6 +1904,14 @@ } } }, + "response.ResUpdateProfileUser": { + "type": "object", + "properties": { + "image": { + "type": "string" + } + } + }, "response.ResV02GoogleOauth": { "type": "object", "properties": { diff --git a/src/docs/swagger.yaml b/src/docs/swagger.yaml index 843406b..d8fba91 100644 --- a/src/docs/swagger.yaml +++ b/src/docs/swagger.yaml @@ -329,8 +329,12 @@ definitions: type: string email: type: string + image: + type: string name: type: string + push: + type: boolean sex: type: string type: object @@ -421,6 +425,11 @@ definitions: refreshToken: type: string type: object + response.ResUpdateProfileUser: + properties: + image: + type: string + type: object response.ResV1RecommendFood: properties: foodNames: @@ -1562,6 +1571,47 @@ paths: summary: 유저 프로필 저장하기 tags: - user + /v0.1/users/profiles/image: + post: + description: |- + ■ errCode with 400 + PARAM_BAD : 파라미터 오류 + USER_NOT_FOUND : 유저가 존재하지 않음 + ■ errCode with 401 + INVALID_AUTH_CODE : 인증 코드 검증 실패 + TOKEN_BAD : 잘못된 토큰 + INVALID_ACCESS_TOKEN : 잘못된 액세스 토큰 + + ■ errCode with 500 + INTERNAL_SERVER : 내부 로직 처리 실패 + INTERNAL_DB : DB 처리 실패 + PLAYER_STATE_CHANGE_FAILED : 플레이어 상태 변경 실패 + parameters: + - description: accessToken + in: header + name: tkn + required: true + type: string + - description: 프로필 이미지 파일 + in: formData + name: image + type: file + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.ResUpdateProfileUser' + "400": + description: Bad Request + schema: {} + "500": + description: Internal Server Error + schema: {} + summary: 유저 프로필 이미지 저장하기 + tags: + - user /v0.2/auth/google: post: description: |- diff --git a/src/features/user/handler/index.go b/src/features/user/handler/index.go index d4708d7..cadfa5e 100644 --- a/src/features/user/handler/index.go +++ b/src/features/user/handler/index.go @@ -13,4 +13,5 @@ func NewUserHandler(c *echo.Echo) { NewUpdateUserHandler(c, usecase.NewUpdateUserUseCase(repository.NewUpdateUserRepository(mysql.GormMysqlDB), mysql.DBTimeOut)) NewDeleteUserHandler(c, usecase.NewDeleteUserUseCase(repository.NewDeleteUserRepository(mysql.GormMysqlDB), mysql.DBTimeOut)) NewMessageUserHandler(c, usecase.NewMessageUserUseCase(repository.NewMessageUserRepository(mysql.GormMysqlDB), mysql.DBTimeOut)) + NewUpdateProfileUserHandler(c, usecase.NewUpdateProfileUserUseCase(repository.NewUpdateProfileUserRepository(mysql.GormMysqlDB), mysql.DBTimeOut)) } diff --git a/src/features/user/handler/updateProfileUserHandler.go b/src/features/user/handler/updateProfileUserHandler.go new file mode 100644 index 0000000..08211b7 --- /dev/null +++ b/src/features/user/handler/updateProfileUserHandler.go @@ -0,0 +1,65 @@ +package handler + +import ( + "main/features/user/model/entity" + _interface "main/features/user/model/interface" + + mw "main/middleware" + "main/utils" + "net/http" + + "github.com/labstack/echo/v4" +) + +type UpdateProfileUserHandler struct { + UseCase _interface.IUpdateProfileUserUseCase +} + +func NewUpdateProfileUserHandler(c *echo.Echo, useCase _interface.IUpdateProfileUserUseCase) _interface.IUpdateProfileUserHandler { + handler := &UpdateProfileUserHandler{ + UseCase: useCase, + } + c.POST("/v0.1/users/profiles/image", handler.UpdateProfile, mw.TokenChecker) + return handler +} + +// 유저 프로필 이미지 저장하기 +// @Router /v0.1/users/profiles/image [post] +// @Summary 유저 프로필 이미지 저장하기 +// @Description +// @Description ■ errCode with 400 +// @Description PARAM_BAD : 파라미터 오류 +// @Description USER_NOT_FOUND : 유저가 존재하지 않음 +// @Description ■ errCode with 401 +// @Description INVALID_AUTH_CODE : 인증 코드 검증 실패 +// @Description TOKEN_BAD : 잘못된 토큰 +// @Description INVALID_ACCESS_TOKEN : 잘못된 액세스 토큰 +// @Description +// @Description ■ errCode with 500 +// @Description INTERNAL_SERVER : 내부 로직 처리 실패 +// @Description INTERNAL_DB : DB 처리 실패 +// @Description PLAYER_STATE_CHANGE_FAILED : 플레이어 상태 변경 실패 +// @Param tkn header string true "accessToken" +// @Param image formData file false "프로필 이미지 파일" +// @Produce json +// @Success 200 {object} response.ResUpdateProfileUser +// @Failure 400 {object} error +// @Failure 500 {object} error +// @Tags user +func (d *UpdateProfileUserHandler) UpdateProfile(c echo.Context) error { + ctx, uID, _ := utils.CtxGenerate(c) + file, err := c.FormFile("image") + if err != nil { + return err + } + e := &entity.UpdateProfileUserEntity{ + UserID: uID, + Image: file, + } + res, err := d.UseCase.UpdateProfile(ctx, e) + if err != nil { + return err + } + + return c.JSON(http.StatusOK, res) +} diff --git a/src/features/user/model/entity/updateProfileUserEntity.go b/src/features/user/model/entity/updateProfileUserEntity.go new file mode 100644 index 0000000..af16805 --- /dev/null +++ b/src/features/user/model/entity/updateProfileUserEntity.go @@ -0,0 +1,8 @@ +package entity + +import "mime/multipart" + +type UpdateProfileUserEntity struct { + Image *multipart.FileHeader `json:"image"` + UserID uint `json:"userID"` +} diff --git a/src/features/user/model/interface/IUserHandler.go b/src/features/user/model/interface/IUserHandler.go index 1318c34..f4edf47 100644 --- a/src/features/user/model/interface/IUserHandler.go +++ b/src/features/user/model/interface/IUserHandler.go @@ -16,3 +16,7 @@ type IDeleteUserHandler interface { type IMessageUserHandler interface { Message(c echo.Context) error } + +type IUpdateProfileUserHandler interface { + UpdateProfile(c echo.Context) error +} diff --git a/src/features/user/model/interface/IUserRepository.go b/src/features/user/model/interface/IUserRepository.go index 4aae8e8..6493a4c 100644 --- a/src/features/user/model/interface/IUserRepository.go +++ b/src/features/user/model/interface/IUserRepository.go @@ -20,3 +20,7 @@ type IDeleteUserRepository interface { type IMessageUserRepository interface { FindOnePushToken(ctx context.Context, uID uint) (string, error) } + +type IUpdateProfileUserRepository interface { + UpdateProfileImage(ctx context.Context, uID uint, fileName string) error +} diff --git a/src/features/user/model/interface/IUserUseCase.go b/src/features/user/model/interface/IUserUseCase.go index bb2d858..d30dfc8 100644 --- a/src/features/user/model/interface/IUserUseCase.go +++ b/src/features/user/model/interface/IUserUseCase.go @@ -21,3 +21,7 @@ type IDeleteUserUseCase interface { type IMessageUserUseCase interface { Message(c context.Context, uID uint, req *request.ReqMessageUser) error } + +type IUpdateProfileUserUseCase interface { + UpdateProfile(c context.Context, e *entity.UpdateProfileUserEntity) (response.ResUpdateProfileUser, error) +} diff --git a/src/features/user/model/response/getUser.go b/src/features/user/model/response/getUser.go index 1c834a5..97d9e83 100644 --- a/src/features/user/model/response/getUser.go +++ b/src/features/user/model/response/getUser.go @@ -5,4 +5,6 @@ type ResGetUser struct { Birth string `json:"birth"` Sex string `json:"sex"` Email string `json:"email"` + Push *bool `json:"push"` + Image string `json:"image"` } diff --git a/src/features/user/model/response/updateProfileUser.go b/src/features/user/model/response/updateProfileUser.go new file mode 100644 index 0000000..c01f306 --- /dev/null +++ b/src/features/user/model/response/updateProfileUser.go @@ -0,0 +1,5 @@ +package response + +type ResUpdateProfileUser struct { + Image string `json:"image"` +} diff --git a/src/features/user/repository/repository.go b/src/features/user/repository/repository.go index 772f746..5a681b5 100644 --- a/src/features/user/repository/repository.go +++ b/src/features/user/repository/repository.go @@ -16,3 +16,7 @@ type DeleteUserRepository struct { type MessageUserRepository struct { GormDB *gorm.DB } + +type UpdateProfileUserRepository struct { + GormDB *gorm.DB +} diff --git a/src/features/user/repository/updateProfileUserRepository.go b/src/features/user/repository/updateProfileUserRepository.go new file mode 100644 index 0000000..d530f7f --- /dev/null +++ b/src/features/user/repository/updateProfileUserRepository.go @@ -0,0 +1,27 @@ +package repository + +import ( + "context" + _errors "main/features/user/model/errors" + _interface "main/features/user/model/interface" + "main/utils" + "main/utils/db/mysql" + + "gorm.io/gorm" +) + +func NewUpdateProfileUserRepository(gormDB *gorm.DB) _interface.IUpdateProfileUserRepository { + return &UpdateProfileUserRepository{GormDB: gormDB} +} + +func (d *UpdateProfileUserRepository) UpdateProfileImage(ctx context.Context, userID uint, filename string) error { + user := &mysql.Users{} + result := d.GormDB.Model(&user).Where("id = ?", userID).Update("image", filename) + if result.Error != nil { + return utils.ErrorMsg(ctx, utils.ErrInternalServer, utils.Trace(), utils.HandleError(result.Error.Error(), user), utils.ErrFromInternal) + } + if result.RowsAffected == 0 { + return utils.ErrorMsg(ctx, utils.ErrUserNotFound, utils.Trace(), utils.HandleError(_errors.ErrUserNotFound.Error(), user), utils.ErrFromClient) + } + return nil +} diff --git a/src/features/user/usecase/updateProfileUserUseCase.go b/src/features/user/usecase/updateProfileUserUseCase.go new file mode 100644 index 0000000..4edc09e --- /dev/null +++ b/src/features/user/usecase/updateProfileUserUseCase.go @@ -0,0 +1,47 @@ +package usecase + +import ( + "context" + "main/features/user/model/entity" + _interface "main/features/user/model/interface" + "main/features/user/model/response" + "main/utils/aws" + "time" +) + +type UpdateProfileUserUseCase struct { + Repository _interface.IUpdateProfileUserRepository + ContextTimeout time.Duration +} + +func NewUpdateProfileUserUseCase(repo _interface.IUpdateProfileUserRepository, timeout time.Duration) _interface.IUpdateProfileUserUseCase { + return &UpdateProfileUserUseCase{Repository: repo, ContextTimeout: timeout} +} + +func (d *UpdateProfileUserUseCase) UpdateProfile(c context.Context, e *entity.UpdateProfileUserEntity) (response.ResUpdateProfileUser, error) { + ctx, cancel := context.WithTimeout(c, d.ContextTimeout) + defer cancel() + //s3 이미지 업로드 한다. + filename := aws.FileNameGenerateRandom() + err := aws.ImageUpload(ctx, e.Image, filename, aws.ImgTypeProfile) + if err != nil { + return response.ResUpdateProfileUser{}, err + } + + //유저 정보를 업데이트 한다. + err = d.Repository.UpdateProfileImage(ctx, e.UserID, filename) + if err != nil { + return response.ResUpdateProfileUser{}, err + } + + //s3 이미지 url을 응답한다. + url, err := aws.ImageGetSignedURL(ctx, filename, aws.ImgTypeProfile) + if err != nil { + return response.ResUpdateProfileUser{}, err + } + res := response.ResUpdateProfileUser{ + Image: url, + } + + return res, nil +} diff --git a/src/features/user/usecase/usecase.go b/src/features/user/usecase/usecase.go index 0364526..b3d055d 100644 --- a/src/features/user/usecase/usecase.go +++ b/src/features/user/usecase/usecase.go @@ -1,8 +1,11 @@ package usecase import ( + "context" + "fmt" "main/features/user/model/entity" "main/features/user/model/response" + "main/utils/aws" "main/utils/db/mysql" "gorm.io/gorm" @@ -38,11 +41,20 @@ func CreateUpdateUserDTO(entity *entity.UpdateUserEntity) (*mysql.Users, error) } func CreateResGetUser(user *mysql.Users) response.ResGetUser { + //유저 정보를 가져올 때 사용할 DTO를 생성한다. - return response.ResGetUser{ + res := response.ResGetUser{ Name: user.Name, Email: user.Email, Sex: user.Sex, Birth: user.Birth, + Push: user.Push, + } + imageUrl, err := aws.ImageGetSignedURL(context.TODO(), user.Image, aws.ImgTypeProfile) + if err == nil { + fmt.Println(err) } + res.Image = imageUrl + + return res } diff --git a/src/utils/aws/init.go b/src/utils/aws/init.go index 1a09354..57925db 100644 --- a/src/utils/aws/init.go +++ b/src/utils/aws/init.go @@ -25,6 +25,7 @@ type ImgType uint8 const ( ImgTypeFood = ImgType(0) ImgTypeCategory = ImgType(1) + ImgTypeProfile = ImgType(2) ) type imgMetaStruct struct { diff --git a/src/utils/aws/s3.go b/src/utils/aws/s3.go index 47f915c..c105488 100644 --- a/src/utils/aws/s3.go +++ b/src/utils/aws/s3.go @@ -31,6 +31,14 @@ var imgMeta = map[ImgType]imgMetaStruct{ height: 62, expireTime: 2 * time.Hour, }, + ImgTypeProfile: { + bucket: func() string { return "dev-food-recommendation" }, + domain: func() string { return "dev-food-recommendation.s3.ap-northeast-2.amazonaws.com" }, + path: "profiles", + width: 128, + height: 128, + expireTime: 2 * time.Hour, + }, } func ImageUpload(ctx context.Context, file *multipart.FileHeader, filename string, imgType ImgType) error { diff --git a/src/utils/db/mysql/gormDB.go b/src/utils/db/mysql/gormDB.go index 3727f13..ba77f69 100644 --- a/src/utils/db/mysql/gormDB.go +++ b/src/utils/db/mysql/gormDB.go @@ -132,6 +132,7 @@ type Users struct { Sex string `json:"sex" gorm:"column:sex"` Provider string `json:"provider" gorm:"column:provider"` Push *bool `json:"push" gorm:"column:push"` + Image string `json:"image" gorm:"column:image"` } type Foods struct { diff --git a/src/utils/db/mysql/table.sql b/src/utils/db/mysql/table.sql index d880689..c560c49 100644 --- a/src/utils/db/mysql/table.sql +++ b/src/utils/db/mysql/table.sql @@ -11,6 +11,7 @@ CREATE TABLE users ( sex varchar(50), provider VARCHAR(50), push BOOLEAN DEFAULT TRUE, + image varchar(1000) DEFAULT 'profile_default.png', role varchar(200) );