From f1e23737da9a0ff12fea40e84dc0909178c2c475 Mon Sep 17 00:00:00 2001 From: 5tarry Date: Thu, 30 Nov 2023 17:42:55 +0900 Subject: [PATCH 1/8] =?UTF-8?q?refactor=20:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85/=ED=94=84=EB=A1=9C=ED=95=84=20=EB=B3=80=EA=B2=BD=20?= =?UTF-8?q?=EC=8B=9C=20=ED=94=84=EB=A1=9C=ED=95=84=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EC=97=85=EB=A1=9C=EB=93=9C=20presigned=20url=20?= =?UTF-8?q?=EB=B0=9C=EA=B8=89=ED=95=98=EB=8A=94=20api=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/.gitignore | 4 ++++ server/src/auth/auth.controller.ts | 24 ++++++++++++++++--- server/src/auth/auth.module.ts | 3 ++- .../presigned-url/presigned-url.controller.ts | 2 +- 4 files changed, 28 insertions(+), 5 deletions(-) diff --git a/server/.gitignore b/server/.gitignore index a2e6062a..43d03da3 100644 --- a/server/.gitignore +++ b/server/.gitignore @@ -35,3 +35,7 @@ lerna-debug.log* !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json + +src/test.py +src/video/common.response.dto.ts +src/video/video.decorator.ts \ No newline at end of file diff --git a/server/src/auth/auth.controller.ts b/server/src/auth/auth.controller.ts index a35aac1d..18ad5073 100644 --- a/server/src/auth/auth.controller.ts +++ b/server/src/auth/auth.controller.ts @@ -1,5 +1,4 @@ -import { Controller, Post, Body, UseInterceptors } from '@nestjs/common'; -import { FileInterceptor } from '@nestjs/platform-express'; +import { Controller, Post, Body, Get, Query } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { ApiSuccessResponse } from 'src/decorators/api-succes-response'; import { ApiFailResponse } from 'src/decorators/api-fail-response'; @@ -8,6 +7,9 @@ import { OAuthFailedException } from 'src/exceptions/oauth-failed.exception'; import { LoginFailException } from 'src/exceptions/login-fail.exception'; import { InvalidRefreshTokenException } from 'src/exceptions/invalid-refresh-token.exception'; import { ProfileUploadRequiredException } from 'src/exceptions/profile-upload-required-exception'; +import { PresignedUrlResponseDto } from 'src/presigned-url/dto/presigned-url-response.dto'; +import { ProfilePresignedUrlRequestDto } from 'src/presigned-url/dto/profile-presigned-url-request.dto'; +import { PresignedUrlService } from 'src/presigned-url/presigned-url.service'; import { AuthService } from './auth.service'; import { SignupRequestDto } from './dto/signup-request.dto'; import { SignupResponseDto } from './dto/signup-response.dto'; @@ -18,7 +20,10 @@ import { RefreshResponseDto } from './dto/refresh-response.dto'; @Controller('auth') export class AuthController { - constructor(private authService: AuthService) {} + constructor( + private authService: AuthService, + private presignedUrlService: PresignedUrlService, + ) {} /** * 회원가입 @@ -60,4 +65,17 @@ export class AuthController { ): Promise { return this.authService.refresh(refreshRequestDto); } + + /** + * 회원가입 시 프로필 이미지를 PUT하는 url 발급 + */ + @Get('signup/presigned-url/profile') + @ApiSuccessResponse( + 200, + '프로필 이미지를 업로드하는 url 발급 성공', + PresignedUrlResponseDto, + ) + putProfilePresignedUrl(@Query() query: ProfilePresignedUrlRequestDto) { + return this.presignedUrlService.putProfilePresignedUrl(query); + } } diff --git a/server/src/auth/auth.module.ts b/server/src/auth/auth.module.ts index 5765d4de..c722258e 100644 --- a/server/src/auth/auth.module.ts +++ b/server/src/auth/auth.module.ts @@ -3,6 +3,7 @@ import { MongooseModule } from '@nestjs/mongoose'; import { User, UserSchema } from 'src/user/schemas/user.schema'; import { JwtModule } from '@nestjs/jwt'; import { ConfigModule } from '@nestjs/config'; +import { PresignedUrlService } from 'src/presigned-url/presigned-url.service'; import { AuthController } from './auth.controller'; import { AuthService } from './auth.service'; @@ -17,6 +18,6 @@ import { AuthService } from './auth.service'; }), ], controllers: [AuthController], - providers: [AuthService], + providers: [AuthService, PresignedUrlService], }) export class AuthModule {} diff --git a/server/src/presigned-url/presigned-url.controller.ts b/server/src/presigned-url/presigned-url.controller.ts index 6d9e37d5..76a4e90a 100644 --- a/server/src/presigned-url/presigned-url.controller.ts +++ b/server/src/presigned-url/presigned-url.controller.ts @@ -38,7 +38,7 @@ export class PresignedUrlController { } /** - * 프로필 이미지를 PUT하는 url 발급 + * 프로필 이미지 변경 시 이미지를 PUT하는 url 발급 */ @Get('profile') @ApiSuccessResponse( From 2494fe37acb16d1b8da1ac2d995e07c150362e86 Mon Sep 17 00:00:00 2001 From: 5tarry Date: Thu, 30 Nov 2023 17:59:30 +0900 Subject: [PATCH 2/8] =?UTF-8?q?feat=20:=20=EC=A4=91=EB=B3=B5=EB=90=9C=20?= =?UTF-8?q?=EB=B9=84=EB=94=94=EC=98=A4=20Id=20=EC=98=88=EC=99=B8=EC=B2=98?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/exceptions/enum/exception.enum.ts | 2 ++ server/src/exceptions/video-conflict.exception.ts | 9 +++++++++ server/src/video/video.service.ts | 4 ++++ 3 files changed, 15 insertions(+) create mode 100644 server/src/exceptions/video-conflict.exception.ts diff --git a/server/src/exceptions/enum/exception.enum.ts b/server/src/exceptions/enum/exception.enum.ts index 689ae2e8..3dcf295f 100644 --- a/server/src/exceptions/enum/exception.enum.ts +++ b/server/src/exceptions/enum/exception.enum.ts @@ -1,5 +1,6 @@ enum ErrorCode { UserConflict = 3001, + VideoConflict = 3002, BadRequest = 2000, LoginFail = 1000, TokenExpired = 1001, @@ -21,6 +22,7 @@ enum ErrorCode { const ErrorMessage = { [ErrorCode.UserConflict]: '이미 가입된 회원', + [ErrorCode.VideoConflict]: '중복된 Video Id', [ErrorCode.BadRequest]: '잘못된 요청 형식', [ErrorCode.LoginFail]: '가입되지 않은 회원', [ErrorCode.TokenExpired]: 'AccessToken 만료', diff --git a/server/src/exceptions/video-conflict.exception.ts b/server/src/exceptions/video-conflict.exception.ts new file mode 100644 index 00000000..922bbb23 --- /dev/null +++ b/server/src/exceptions/video-conflict.exception.ts @@ -0,0 +1,9 @@ +import { HttpStatus } from '@nestjs/common'; +import { ErrorCode } from 'src/exceptions/enum/exception.enum'; +import { BaseException } from './base.exception'; + +export class VideoConflictException extends BaseException { + constructor() { + super(ErrorCode.VideoConflict, HttpStatus.CONFLICT); + } +} diff --git a/server/src/video/video.service.ts b/server/src/video/video.service.ts index 4ccc5c72..9b00f7d9 100644 --- a/server/src/video/video.service.ts +++ b/server/src/video/video.service.ts @@ -13,6 +13,7 @@ import axios from 'axios'; import * as _ from 'lodash'; import { ActionService } from 'src/action/action.service'; import { createPresignedUrl } from 'src/ncpAPI/presignedURL'; +import { VideoConflictException } from 'src/exceptions/video-conflict.exception'; import { VideoDto } from './dto/video.dto'; import { Video } from './schemas/video.schema'; import { CategoryEnum } from './enum/category.enum'; @@ -150,6 +151,9 @@ export class VideoService { } async uploadVideo(videoDto: VideoDto, uuid: string, videoId: string) { + const checkDuplicate = await this.VideoModel.findOne({ _id: videoId }); + if (checkDuplicate) throw new VideoConflictException(); + const { videoExtension, thumbnailExtension } = videoDto; const videoName = `${videoId}.${videoExtension}`; const thumbnailName = `${videoId}.${thumbnailExtension}`; From 828450c1a67e8cd97e600aef972e2478608b68b6 Mon Sep 17 00:00:00 2001 From: 5tarry Date: Thu, 30 Nov 2023 18:07:49 +0900 Subject: [PATCH 3/8] =?UTF-8?q?feat=20:=20=EB=B9=84=EB=94=94=EC=98=A4=20?= =?UTF-8?q?=EC=97=85=EB=A1=9C=EB=93=9C=20=EC=8B=9C=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=97=85=EB=A1=9C=EB=93=9C=20=ED=99=95=EC=9D=B8=ED=95=98?= =?UTF-8?q?=EB=8A=94=20=EC=98=88=EC=99=B8=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/video/video.service.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/server/src/video/video.service.ts b/server/src/video/video.service.ts index 9b00f7d9..556e63d5 100644 --- a/server/src/video/video.service.ts +++ b/server/src/video/video.service.ts @@ -3,7 +3,7 @@ /* eslint-disable class-methods-use-this */ import { Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; -import { Model } from 'mongoose'; +import { Model, Types } from 'mongoose'; import { requestEncoding } from 'src/ncpAPI/requestEncoding'; import { User } from 'src/user/schemas/user.schema'; import { deleteObject } from 'src/ncpAPI/deleteObject'; @@ -14,6 +14,9 @@ import * as _ from 'lodash'; import { ActionService } from 'src/action/action.service'; import { createPresignedUrl } from 'src/ncpAPI/presignedURL'; import { VideoConflictException } from 'src/exceptions/video-conflict.exception'; +import { checkUpload } from 'src/ncpAPI/listObjects'; +import { VideoUploadRequiredException } from 'src/exceptions/video-upload-required-exception copy'; +import { ThumbnailUploadRequiredException } from 'src/exceptions/thumbnail-upload-required-exception copy 2'; import { VideoDto } from './dto/video.dto'; import { Video } from './schemas/video.schema'; import { CategoryEnum } from './enum/category.enum'; @@ -151,13 +154,20 @@ export class VideoService { } async uploadVideo(videoDto: VideoDto, uuid: string, videoId: string) { + if (!Types.ObjectId.isValid(videoId)) throw new VideoNotFoundException(); const checkDuplicate = await this.VideoModel.findOne({ _id: videoId }); if (checkDuplicate) throw new VideoConflictException(); const { videoExtension, thumbnailExtension } = videoDto; const videoName = `${videoId}.${videoExtension}`; const thumbnailName = `${videoId}.${thumbnailExtension}`; - // TODO 비디오, 썸네일 업로드 확인 + + if (!(await checkUpload(process.env.INPUT_BUCKET, videoName))) { + throw new VideoUploadRequiredException(); + } + if (!(await checkUpload(process.env.THUMBNAIL_BUCKET, thumbnailName))) { + throw new ThumbnailUploadRequiredException(); + } await requestEncoding(process.env.INPUT_BUCKET, [videoName]); From 04348e53d76d079545f940f24739321ef1987116 Mon Sep 17 00:00:00 2001 From: 5tarry Date: Thu, 30 Nov 2023 18:14:23 +0900 Subject: [PATCH 4/8] =?UTF-8?q?feat=20:=20=EB=B9=84=EB=94=94=EC=98=A4=20?= =?UTF-8?q?=EC=97=85=EB=A1=9C=EB=93=9C=20api=20=EC=98=88=EC=99=B8=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=EC=9D=91=EB=8B=B5=20=EB=AA=85=EC=84=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/video/video.controller.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/server/src/video/video.controller.ts b/server/src/video/video.controller.ts index 66a0259d..91082412 100644 --- a/server/src/video/video.controller.ts +++ b/server/src/video/video.controller.ts @@ -30,6 +30,9 @@ import { ActionService } from 'src/action/action.service'; import { NeverViewVideoException } from 'src/exceptions/never-view-video.exception'; import { IgnoreInterceptor } from 'src/decorators/ignore-interceptor'; import { SeedQueryDto } from 'src/action/dto/manifest-query.dto'; +import { VideoConflictException } from 'src/exceptions/video-conflict.exception'; +import { ThumbnailUploadRequiredException } from 'src/exceptions/thumbnail-upload-required-exception copy 2'; +import { VideoUploadRequiredException } from 'src/exceptions/video-upload-required-exception copy'; import { VideoService } from './video.service'; import { VideoDto } from './dto/video.dto'; import { VideoRatingDTO } from './dto/video-rating.dto'; @@ -77,6 +80,12 @@ export class VideoController { ) @Post(':videoId') @ApiSuccessResponse(201, '비디오 업로드 성공', VideoSummaryResponseDto) + @ApiFailResponse('중복된 비디오 ID', [VideoConflictException]) + @ApiFailResponse('잘못된 비디오 ID', [VideoNotFoundException]) + @ApiFailResponse('비디오가 업로드 되지 않음', [VideoUploadRequiredException]) + @ApiFailResponse('썸네일이 업로드 되지 않음', [ + ThumbnailUploadRequiredException, + ]) uploadVideo( @Body() videoDto: VideoDto, @RequestUser() user: User, From 9f37ee21044c34904117959ae6a26ad7fac7a5d1 Mon Sep 17 00:00:00 2001 From: 5tarry Date: Fri, 1 Dec 2023 19:18:10 +0900 Subject: [PATCH 5/8] =?UTF-8?q?refactor=20:=20=EC=98=88=EC=99=B8=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/exceptions/bad-request-format.exception.ts | 9 +++++++++ server/src/exceptions/enum/exception.enum.ts | 2 ++ server/src/video/video.controller.ts | 3 ++- server/src/video/video.service.ts | 3 ++- 4 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 server/src/exceptions/bad-request-format.exception.ts diff --git a/server/src/exceptions/bad-request-format.exception.ts b/server/src/exceptions/bad-request-format.exception.ts new file mode 100644 index 00000000..58ddd2e2 --- /dev/null +++ b/server/src/exceptions/bad-request-format.exception.ts @@ -0,0 +1,9 @@ +import { HttpStatus } from '@nestjs/common'; +import { ErrorCode } from 'src/exceptions/enum/exception.enum'; +import { BaseException } from './base.exception'; + +export class BadRequestFormatException extends BaseException { + constructor() { + super(ErrorCode.BadRequestFormat, HttpStatus.BAD_REQUEST); + } +} diff --git a/server/src/exceptions/enum/exception.enum.ts b/server/src/exceptions/enum/exception.enum.ts index 3dcf295f..a3024578 100644 --- a/server/src/exceptions/enum/exception.enum.ts +++ b/server/src/exceptions/enum/exception.enum.ts @@ -18,6 +18,7 @@ enum ErrorCode { VideoUploadRequired = 5004, BadVideoFormat = 8000, BadThumbnailFormat = 8100, + BadRequestFormat = 8200, } const ErrorMessage = { @@ -39,6 +40,7 @@ const ErrorMessage = { [ErrorCode.ProfileUploadRequired]: '프로필 이미지를 먼저 업로드 해야합니다.', [ErrorCode.ThumbnailUploadRequired]: '썸네일을 먼저 업로드 해야합니다.', [ErrorCode.VideoUploadRequired]: '비디오를 먼저 업로드 해야합니다.', + [ErrorCode.BadRequestFormat]: '요청 형식이 잘못됨', }; export { ErrorCode, ErrorMessage }; diff --git a/server/src/video/video.controller.ts b/server/src/video/video.controller.ts index 91082412..0ce8442d 100644 --- a/server/src/video/video.controller.ts +++ b/server/src/video/video.controller.ts @@ -33,6 +33,7 @@ import { SeedQueryDto } from 'src/action/dto/manifest-query.dto'; import { VideoConflictException } from 'src/exceptions/video-conflict.exception'; import { ThumbnailUploadRequiredException } from 'src/exceptions/thumbnail-upload-required-exception copy 2'; import { VideoUploadRequiredException } from 'src/exceptions/video-upload-required-exception copy'; +import { BadRequestFormatException } from 'src/exceptions/bad-request-format.exception'; import { VideoService } from './video.service'; import { VideoDto } from './dto/video.dto'; import { VideoRatingDTO } from './dto/video-rating.dto'; @@ -81,7 +82,7 @@ export class VideoController { @Post(':videoId') @ApiSuccessResponse(201, '비디오 업로드 성공', VideoSummaryResponseDto) @ApiFailResponse('중복된 비디오 ID', [VideoConflictException]) - @ApiFailResponse('잘못된 비디오 ID', [VideoNotFoundException]) + @ApiFailResponse('잘못된 비디오 ID', [BadRequestFormatException]) @ApiFailResponse('비디오가 업로드 되지 않음', [VideoUploadRequiredException]) @ApiFailResponse('썸네일이 업로드 되지 않음', [ ThumbnailUploadRequiredException, diff --git a/server/src/video/video.service.ts b/server/src/video/video.service.ts index 556e63d5..17a49905 100644 --- a/server/src/video/video.service.ts +++ b/server/src/video/video.service.ts @@ -17,6 +17,7 @@ import { VideoConflictException } from 'src/exceptions/video-conflict.exception' import { checkUpload } from 'src/ncpAPI/listObjects'; import { VideoUploadRequiredException } from 'src/exceptions/video-upload-required-exception copy'; import { ThumbnailUploadRequiredException } from 'src/exceptions/thumbnail-upload-required-exception copy 2'; +import { BadRequestFormatException } from 'src/exceptions/bad-request-format.exception'; import { VideoDto } from './dto/video.dto'; import { Video } from './schemas/video.schema'; import { CategoryEnum } from './enum/category.enum'; @@ -154,7 +155,7 @@ export class VideoService { } async uploadVideo(videoDto: VideoDto, uuid: string, videoId: string) { - if (!Types.ObjectId.isValid(videoId)) throw new VideoNotFoundException(); + if (!Types.ObjectId.isValid(videoId)) throw new BadRequestFormatException(); const checkDuplicate = await this.VideoModel.findOne({ _id: videoId }); if (checkDuplicate) throw new VideoConflictException(); From 3976412a0df622c9387e307cc783876863586761 Mon Sep 17 00:00:00 2001 From: msjang4 Date: Fri, 1 Dec 2023 22:07:20 +0900 Subject: [PATCH 6/8] =?UTF-8?q?feat=20:=20=ED=94=84=EB=A1=9C=ED=95=84=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20Presigned=20Url=EC=8B=9C=20UUID=20?= =?UTF-8?q?=EB=8C=80=EC=8B=A0=20JWT=EB=A1=9C=20=EA=B2=80=EC=A6=9D-?= =?UTF-8?q?=EB=B3=84=EC=A0=90=200=EC=A0=90=EA=B9=8C=EC=A7=80=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D=EA=B0=80=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/profile-presigned-url-request.dto.ts | 8 -------- server/src/presigned-url/presigned-url.controller.ts | 11 +++++++++-- server/src/presigned-url/presigned-url.service.ts | 8 ++++---- server/src/video/dto/video-rating.dto.ts | 2 +- 4 files changed, 14 insertions(+), 15 deletions(-) diff --git a/server/src/presigned-url/dto/profile-presigned-url-request.dto.ts b/server/src/presigned-url/dto/profile-presigned-url-request.dto.ts index 4b052285..0506dd50 100644 --- a/server/src/presigned-url/dto/profile-presigned-url-request.dto.ts +++ b/server/src/presigned-url/dto/profile-presigned-url-request.dto.ts @@ -1,12 +1,4 @@ -import { IsUUID } from 'class-validator'; - export class ProfilePresignedUrlRequestDto { - /** - * user uuid - */ - @IsUUID() - uuid: string; - /** * 프로필 이미지 확장자 * @example 'webp' diff --git a/server/src/presigned-url/presigned-url.controller.ts b/server/src/presigned-url/presigned-url.controller.ts index 76a4e90a..dc55cf6e 100644 --- a/server/src/presigned-url/presigned-url.controller.ts +++ b/server/src/presigned-url/presigned-url.controller.ts @@ -5,6 +5,7 @@ import { AuthGuard } from 'src/auth/auth.guard'; import { InvalidTokenException } from 'src/exceptions/invalid-token.exception'; import { TokenExpiredException } from 'src/exceptions/token-expired.exception'; import { ApiSuccessResponse } from 'src/decorators/api-succes-response'; +import { RequestUser, User } from 'src/decorators/request-user'; import { PresignedUrlService } from './presigned-url.service'; import { AdvertisementPresignedUrlRequestDto } from './dto/advertisement-presigned-url-request.dto'; import { ProfilePresignedUrlRequestDto } from './dto/profile-presigned-url-request.dto'; @@ -46,8 +47,14 @@ export class PresignedUrlController { '프로필 이미지를 업로드하는 url 발급 성공', PresignedUrlResponseDto, ) - putProfilePresignedUrl(@Query() query: ProfilePresignedUrlRequestDto) { - return this.presignedUrlService.putProfilePresignedUrl(query); + putProfilePresignedUrl( + @Query() query: ProfilePresignedUrlRequestDto, + @RequestUser() user: User, + ) { + return this.presignedUrlService.putProfilePresignedUrl( + user.id, + query.profileExtension, + ); } /** diff --git a/server/src/presigned-url/presigned-url.service.ts b/server/src/presigned-url/presigned-url.service.ts index 9a0a8c9c..7d5012e1 100644 --- a/server/src/presigned-url/presigned-url.service.ts +++ b/server/src/presigned-url/presigned-url.service.ts @@ -41,10 +41,10 @@ export class PresignedUrlService { return { advertisements }; } - async putProfilePresignedUrl({ - uuid, - profileExtension, - }): Promise { + async putProfilePresignedUrl( + uuid: string, + profileExtension: string, + ): Promise { const objectName = `${uuid}.${profileExtension}`; const presignedUrl = await createPresignedUrl( process.env.PROFILE_BUCKET, diff --git a/server/src/video/dto/video-rating.dto.ts b/server/src/video/dto/video-rating.dto.ts index a96fbfaa..7caf8580 100644 --- a/server/src/video/dto/video-rating.dto.ts +++ b/server/src/video/dto/video-rating.dto.ts @@ -6,7 +6,7 @@ export class VideoRatingDTO { * @example 2 */ @IsNumber() - @Min(1) + @Min(0) @Max(5) rating: number; From d5e96c24eebea42578b7bd7bcf3a713edd2f9ed8 Mon Sep 17 00:00:00 2001 From: msjang4 Date: Fri, 1 Dec 2023 22:26:37 +0900 Subject: [PATCH 7/8] =?UTF-8?q?feat=20:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=ED=94=84=EB=A1=9C=ED=95=84=20presignedURL=20?= =?UTF-8?q?=EB=B0=9C=EA=B8=89=20=EC=9A=94=EC=B2=AD=20=EC=8A=A4=ED=82=A4?= =?UTF-8?q?=EB=A7=88=EC=97=90=20=EC=86=8C=EC=85=9C=20accessToken=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80-conflict=20=EC=98=88=EC=99=B8=EC=B2=98?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 5tarry --- server/src/auth/auth.controller.ts | 12 +++++++++--- server/src/auth/auth.service.ts | 8 ++++++-- ...signup-profile-presigned-url-request.dto.ts | 18 ++++++++++++++++++ 3 files changed, 33 insertions(+), 5 deletions(-) create mode 100644 server/src/presigned-url/dto/signup-profile-presigned-url-request.dto.ts diff --git a/server/src/auth/auth.controller.ts b/server/src/auth/auth.controller.ts index 18ad5073..68b16ecb 100644 --- a/server/src/auth/auth.controller.ts +++ b/server/src/auth/auth.controller.ts @@ -8,8 +8,8 @@ import { LoginFailException } from 'src/exceptions/login-fail.exception'; import { InvalidRefreshTokenException } from 'src/exceptions/invalid-refresh-token.exception'; import { ProfileUploadRequiredException } from 'src/exceptions/profile-upload-required-exception'; import { PresignedUrlResponseDto } from 'src/presigned-url/dto/presigned-url-response.dto'; -import { ProfilePresignedUrlRequestDto } from 'src/presigned-url/dto/profile-presigned-url-request.dto'; import { PresignedUrlService } from 'src/presigned-url/presigned-url.service'; +import { SignupProfilePresignedUrlRequestDto } from 'src/presigned-url/dto/signup-profile-presigned-url-request.dto'; import { AuthService } from './auth.service'; import { SignupRequestDto } from './dto/signup-request.dto'; import { SignupResponseDto } from './dto/signup-response.dto'; @@ -75,7 +75,13 @@ export class AuthController { '프로필 이미지를 업로드하는 url 발급 성공', PresignedUrlResponseDto, ) - putProfilePresignedUrl(@Query() query: ProfilePresignedUrlRequestDto) { - return this.presignedUrlService.putProfilePresignedUrl(query); + async putProfilePresignedUrl( + @Query() query: SignupProfilePresignedUrlRequestDto, + ) { + await this.authService.checkUserConflict(query.uuid); + return this.presignedUrlService.putProfilePresignedUrl( + query.uuid, + query.profileExtension, + ); } } diff --git a/server/src/auth/auth.service.ts b/server/src/auth/auth.service.ts index 89ba7e54..344b65f5 100644 --- a/server/src/auth/auth.service.ts +++ b/server/src/auth/auth.service.ts @@ -24,11 +24,15 @@ export class AuthService { private jwtService: JwtService, ) {} - async create(signupRequestDto: SignupRequestDto): Promise { - const { uuid, profileImageExtension } = signupRequestDto; + async checkUserConflict(uuid: string): Promise { if (await this.UserModel.findOne({ uuid })) { throw new UserConflictException(); } + } + + async create(signupRequestDto: SignupRequestDto): Promise { + const { uuid, profileImageExtension } = signupRequestDto; + await this.checkUserConflict(uuid); if ( profileImageExtension && !(await checkUpload( diff --git a/server/src/presigned-url/dto/signup-profile-presigned-url-request.dto.ts b/server/src/presigned-url/dto/signup-profile-presigned-url-request.dto.ts new file mode 100644 index 00000000..ec387dde --- /dev/null +++ b/server/src/presigned-url/dto/signup-profile-presigned-url-request.dto.ts @@ -0,0 +1,18 @@ +import { IsNotEmpty, IsUUID } from 'class-validator'; +import { ProfilePresignedUrlRequestDto } from './profile-presigned-url-request.dto'; + +export class SignupProfilePresignedUrlRequestDto extends ProfilePresignedUrlRequestDto { + /** + * 유저 ID + * @example '550e8400-e29b-41d4-a716-446655440000' + */ + @IsUUID() + uuid: string; + + /** + * 소셜 accessToken + * @example '1/fFAGRNJru1FTz70BzhT3Zg' + */ + @IsNotEmpty() + accessToken: string; +} From 12a6771ed4ad90b79e05d65e15331aabb8d15abb Mon Sep 17 00:00:00 2001 From: msjang4 Date: Fri, 1 Dec 2023 22:55:27 +0900 Subject: [PATCH 8/8] =?UTF-8?q?chore=20:=20Swagger=20=EB=AA=85=EC=84=B8=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EB=B0=8F=20Request=20DTO=20validator=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 5tarry --- server/src/exceptions/enum/exception.enum.ts | 2 +- .../dto/advertisement-presigned-url-request.dto.ts | 4 ++++ .../presigned-url/dto/profile-presigned-url-request.dto.ts | 3 +++ .../presigned-url/dto/reissue-presigned-url-request.dto.ts | 3 ++- .../presigned-url/dto/video-presigned-url-request.dto.ts | 4 ++++ server/src/presigned-url/presigned-url.controller.ts | 3 +++ server/src/user/dto/rated-video-request.dto.ts | 4 +++- server/src/user/dto/uploaded-video-request.dto.ts | 4 +++- server/src/video/video.controller.ts | 7 +++---- 9 files changed, 26 insertions(+), 8 deletions(-) diff --git a/server/src/exceptions/enum/exception.enum.ts b/server/src/exceptions/enum/exception.enum.ts index a3024578..e948621b 100644 --- a/server/src/exceptions/enum/exception.enum.ts +++ b/server/src/exceptions/enum/exception.enum.ts @@ -36,7 +36,7 @@ const ErrorMessage = { [ErrorCode.NeverViewVideo]: '시청한 영상만 별점을 등록할 수 있음', [ErrorCode.VideoNotFound]: '비디오를 찾을 수 없음', [ErrorCode.UserNotFound]: '유저를 찾을 수 없음', - [ErrorCode.ObjectNotFound]: '오브젝트를 찾을 수 없음', + [ErrorCode.ObjectNotFound]: '파일을 찾을 수 없음', [ErrorCode.ProfileUploadRequired]: '프로필 이미지를 먼저 업로드 해야합니다.', [ErrorCode.ThumbnailUploadRequired]: '썸네일을 먼저 업로드 해야합니다.', [ErrorCode.VideoUploadRequired]: '비디오를 먼저 업로드 해야합니다.', diff --git a/server/src/presigned-url/dto/advertisement-presigned-url-request.dto.ts b/server/src/presigned-url/dto/advertisement-presigned-url-request.dto.ts index d817b838..741ab5f2 100644 --- a/server/src/presigned-url/dto/advertisement-presigned-url-request.dto.ts +++ b/server/src/presigned-url/dto/advertisement-presigned-url-request.dto.ts @@ -1,7 +1,11 @@ +import { IsNotEmpty, IsOptional } from 'class-validator'; + export class AdvertisementPresignedUrlRequestDto { /** * 특정 광고 이미지의 presigned url만 받고 싶은 경우 * @example 'test.webp' */ + @IsOptional() + @IsNotEmpty() name?: string; } diff --git a/server/src/presigned-url/dto/profile-presigned-url-request.dto.ts b/server/src/presigned-url/dto/profile-presigned-url-request.dto.ts index 0506dd50..cc059a29 100644 --- a/server/src/presigned-url/dto/profile-presigned-url-request.dto.ts +++ b/server/src/presigned-url/dto/profile-presigned-url-request.dto.ts @@ -1,7 +1,10 @@ +import { IsNotEmpty } from 'class-validator'; + export class ProfilePresignedUrlRequestDto { /** * 프로필 이미지 확장자 * @example 'webp' */ + @IsNotEmpty() profileExtension: string; } diff --git a/server/src/presigned-url/dto/reissue-presigned-url-request.dto.ts b/server/src/presigned-url/dto/reissue-presigned-url-request.dto.ts index 341d81a6..f96e42c9 100644 --- a/server/src/presigned-url/dto/reissue-presigned-url-request.dto.ts +++ b/server/src/presigned-url/dto/reissue-presigned-url-request.dto.ts @@ -1,4 +1,4 @@ -import { IsEnum } from 'class-validator'; +import { IsEnum, IsNotEmpty } from 'class-validator'; enum TypeEnum { thumbnail = 'thumbnail', @@ -17,5 +17,6 @@ export class ReissuePresignedUrlRequestDto { * 이미지 확장자 * @example 'webp' */ + @IsNotEmpty() extension: string; } diff --git a/server/src/presigned-url/dto/video-presigned-url-request.dto.ts b/server/src/presigned-url/dto/video-presigned-url-request.dto.ts index 4e93c1a3..a4fa2f25 100644 --- a/server/src/presigned-url/dto/video-presigned-url-request.dto.ts +++ b/server/src/presigned-url/dto/video-presigned-url-request.dto.ts @@ -1,13 +1,17 @@ +import { IsNotEmpty } from 'class-validator'; + export class VIdeoPresignedUrlRequestDto { /** * 비디오 확장자 * @example 'mp4' */ + @IsNotEmpty() videoExtension: string; /** * 썸네일 이미지 확장자 * @example 'webp' */ + @IsNotEmpty() thumbnailExtension: string; } diff --git a/server/src/presigned-url/presigned-url.controller.ts b/server/src/presigned-url/presigned-url.controller.ts index dc55cf6e..8c8bbfbc 100644 --- a/server/src/presigned-url/presigned-url.controller.ts +++ b/server/src/presigned-url/presigned-url.controller.ts @@ -6,6 +6,7 @@ import { InvalidTokenException } from 'src/exceptions/invalid-token.exception'; import { TokenExpiredException } from 'src/exceptions/token-expired.exception'; import { ApiSuccessResponse } from 'src/decorators/api-succes-response'; import { RequestUser, User } from 'src/decorators/request-user'; +import { ObjectNotFoundException } from 'src/exceptions/object-not-found.exception'; import { PresignedUrlService } from './presigned-url.service'; import { AdvertisementPresignedUrlRequestDto } from './dto/advertisement-presigned-url-request.dto'; import { ProfilePresignedUrlRequestDto } from './dto/profile-presigned-url-request.dto'; @@ -32,6 +33,7 @@ export class PresignedUrlController { '광고 이미지 가져오는 url 발급 성공', AdvertisementPresignedUrlResponseDto, ) + @ApiFailResponse('url 발급 실패', [ObjectNotFoundException]) getAdvertisementPresignedUrl( @Query() query: AdvertisementPresignedUrlRequestDto, ) { @@ -79,6 +81,7 @@ export class PresignedUrlController { description: '썸네일 재발급 시 비디오ID, 프로필 재발급 시 유저 UUID', }) @ApiSuccessResponse(200, 'presigned url 재발급 성공', PresignedUrlResponseDto) + @ApiFailResponse('url 발급 실패', [ObjectNotFoundException]) getImagePresignedUrl( @Param('id') id: string, @Query() query: ReissuePresignedUrlRequestDto, diff --git a/server/src/user/dto/rated-video-request.dto.ts b/server/src/user/dto/rated-video-request.dto.ts index d061e796..08aca164 100644 --- a/server/src/user/dto/rated-video-request.dto.ts +++ b/server/src/user/dto/rated-video-request.dto.ts @@ -1,4 +1,4 @@ -import { IsInt, IsPositive } from 'class-validator'; +import { IsInt, IsPositive, IsOptional, IsNotEmpty } from 'class-validator'; export class UserRatedVideoQueryDto { /** @@ -11,5 +11,7 @@ export class UserRatedVideoQueryDto { /** * 마지막으로 조회한 비디오의 ratedAt */ + @IsOptional() + @IsNotEmpty() lastRatedAt?: string; } diff --git a/server/src/user/dto/uploaded-video-request.dto.ts b/server/src/user/dto/uploaded-video-request.dto.ts index 077457c1..3efc5de9 100644 --- a/server/src/user/dto/uploaded-video-request.dto.ts +++ b/server/src/user/dto/uploaded-video-request.dto.ts @@ -1,4 +1,4 @@ -import { IsInt, IsPositive } from 'class-validator'; +import { IsInt, IsNotEmpty, IsOptional, IsPositive } from 'class-validator'; export class UserUploadedVideoQueryDto { /** @@ -11,5 +11,7 @@ export class UserUploadedVideoQueryDto { /** * 마지막으로 조회한 비디오 ID */ + @IsOptional() + @IsNotEmpty() lastId?: string; } diff --git a/server/src/video/video.controller.ts b/server/src/video/video.controller.ts index 0ce8442d..e67b7ef0 100644 --- a/server/src/video/video.controller.ts +++ b/server/src/video/video.controller.ts @@ -12,6 +12,7 @@ import { Query, } from '@nestjs/common'; import { + ApiBadRequestResponse, ApiBearerAuth, ApiOkResponse, ApiProduces, @@ -37,7 +38,6 @@ import { BadRequestFormatException } from 'src/exceptions/bad-request-format.exc import { VideoService } from './video.service'; import { VideoDto } from './dto/video.dto'; import { VideoRatingDTO } from './dto/video-rating.dto'; -import { FileExtensionPipe } from './video.pipe'; import { RandomVideoQueryDto } from './dto/random-video-query.dto'; import { VideoSummaryResponseDto } from './dto/video-summary-response.dto'; import { VideoInfoDto } from './dto/video-info.dto'; @@ -55,7 +55,6 @@ export class VideoController { constructor( private videoService: VideoService, private actionService: ActionService, - private fileExtensionPipe: FileExtensionPipe, ) {} /** @@ -83,8 +82,8 @@ export class VideoController { @ApiSuccessResponse(201, '비디오 업로드 성공', VideoSummaryResponseDto) @ApiFailResponse('중복된 비디오 ID', [VideoConflictException]) @ApiFailResponse('잘못된 비디오 ID', [BadRequestFormatException]) - @ApiFailResponse('비디오가 업로드 되지 않음', [VideoUploadRequiredException]) - @ApiFailResponse('썸네일이 업로드 되지 않음', [ + @ApiFailResponse('업로드가 필요함', [ + VideoUploadRequiredException, ThumbnailUploadRequiredException, ]) uploadVideo(