Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

비디오 업로드 예외처리 #164

Merged
merged 8 commits into from
Dec 1, 2023
4 changes: 4 additions & 0 deletions server/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
30 changes: 27 additions & 3 deletions server/src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 { 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';
Expand All @@ -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,
) {}

/**
* 회원가입
Expand Down Expand Up @@ -60,4 +65,23 @@ export class AuthController {
): Promise<RefreshResponseDto> {
return this.authService.refresh(refreshRequestDto);
}

/**
* 회원가입 시 프로필 이미지를 PUT하는 url 발급
*/
@Get('signup/presigned-url/profile')
Comment on lines +69 to +72
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이렇게 프로필이미지 put presigned Url 발급을 나눈게 굉장히 좋은 거 같아요!
프로필 변경시 발급에서는 본인 Id만 발급가능하도록 할 수 있고, 회원가입시 발급에서는 DB에 없는 Id만 발급가능하게 해서 예외처리 할 수 있으니까요

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ㅎㅎㅎ

@ApiSuccessResponse(
200,
'프로필 이미지를 업로드하는 url 발급 성공',
PresignedUrlResponseDto,
)
async putProfilePresignedUrl(
@Query() query: SignupProfilePresignedUrlRequestDto,
) {
await this.authService.checkUserConflict(query.uuid);
return this.presignedUrlService.putProfilePresignedUrl(
query.uuid,
query.profileExtension,
);
}
}
3 changes: 2 additions & 1 deletion server/src/auth/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -17,6 +18,6 @@ import { AuthService } from './auth.service';
}),
],
controllers: [AuthController],
providers: [AuthService],
providers: [AuthService, PresignedUrlService],
})
export class AuthModule {}
8 changes: 6 additions & 2 deletions server/src/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,15 @@ export class AuthService {
private jwtService: JwtService,
) {}

async create(signupRequestDto: SignupRequestDto): Promise<SignupResponseDto> {
const { uuid, profileImageExtension } = signupRequestDto;
async checkUserConflict(uuid: string): Promise<void> {
if (await this.UserModel.findOne({ uuid })) {
throw new UserConflictException();
}
}

async create(signupRequestDto: SignupRequestDto): Promise<SignupResponseDto> {
const { uuid, profileImageExtension } = signupRequestDto;
await this.checkUserConflict(uuid);
if (
profileImageExtension &&
!(await checkUpload(
Expand Down
9 changes: 9 additions & 0 deletions server/src/exceptions/bad-request-format.exception.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
6 changes: 5 additions & 1 deletion server/src/exceptions/enum/exception.enum.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
enum ErrorCode {
UserConflict = 3001,
VideoConflict = 3002,
BadRequest = 2000,
LoginFail = 1000,
TokenExpired = 1001,
Expand All @@ -17,10 +18,12 @@ enum ErrorCode {
VideoUploadRequired = 5004,
BadVideoFormat = 8000,
BadThumbnailFormat = 8100,
BadRequestFormat = 8200,
}

const ErrorMessage = {
[ErrorCode.UserConflict]: '이미 가입된 회원',
[ErrorCode.VideoConflict]: '중복된 Video Id',
[ErrorCode.BadRequest]: '잘못된 요청 형식',
[ErrorCode.LoginFail]: '가입되지 않은 회원',
[ErrorCode.TokenExpired]: 'AccessToken 만료',
Expand All @@ -33,10 +36,11 @@ const ErrorMessage = {
[ErrorCode.NeverViewVideo]: '시청한 영상만 별점을 등록할 수 있음',
[ErrorCode.VideoNotFound]: '비디오를 찾을 수 없음',
[ErrorCode.UserNotFound]: '유저를 찾을 수 없음',
[ErrorCode.ObjectNotFound]: '오브젝트를 찾을 수 없음',
[ErrorCode.ObjectNotFound]: '파일을 찾을 수 없음',
[ErrorCode.ProfileUploadRequired]: '프로필 이미지를 먼저 업로드 해야합니다.',
[ErrorCode.ThumbnailUploadRequired]: '썸네일을 먼저 업로드 해야합니다.',
[ErrorCode.VideoUploadRequired]: '비디오를 먼저 업로드 해야합니다.',
[ErrorCode.BadRequestFormat]: '요청 형식이 잘못됨',
Copy link
Collaborator

@msjang4 msjang4 Dec 1, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

리뷰 반영 감사합니다! 👍

};

export { ErrorCode, ErrorMessage };
9 changes: 9 additions & 0 deletions server/src/exceptions/video-conflict.exception.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { IsNotEmpty, IsOptional } from 'class-validator';

export class AdvertisementPresignedUrlRequestDto {
/**
* 특정 광고 이미지의 presigned url만 받고 싶은 경우
* @example 'test.webp'
*/
@IsOptional()
@IsNotEmpty()
name?: string;
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
import { IsUUID } from 'class-validator';
import { IsNotEmpty } from 'class-validator';

export class ProfilePresignedUrlRequestDto {
/**
* user uuid
*/
@IsUUID()
uuid: string;

/**
* 프로필 이미지 확장자
* @example 'webp'
*/
@IsNotEmpty()
profileExtension: string;
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { IsEnum } from 'class-validator';
import { IsEnum, IsNotEmpty } from 'class-validator';

enum TypeEnum {
thumbnail = 'thumbnail',
Expand All @@ -17,5 +17,6 @@ export class ReissuePresignedUrlRequestDto {
* 이미지 확장자
* @example 'webp'
*/
@IsNotEmpty()
extension: string;
}
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { IsNotEmpty } from 'class-validator';

export class VIdeoPresignedUrlRequestDto {
/**
* 비디오 확장자
* @example 'mp4'
*/
@IsNotEmpty()
videoExtension: string;

/**
* 썸네일 이미지 확장자
* @example 'webp'
*/
@IsNotEmpty()
thumbnailExtension: string;
}
16 changes: 13 additions & 3 deletions server/src/presigned-url/presigned-url.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ 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 { 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';
Expand All @@ -31,23 +33,30 @@ export class PresignedUrlController {
'광고 이미지 가져오는 url 발급 성공',
AdvertisementPresignedUrlResponseDto,
)
@ApiFailResponse('url 발급 실패', [ObjectNotFoundException])
getAdvertisementPresignedUrl(
@Query() query: AdvertisementPresignedUrlRequestDto,
) {
return this.presignedUrlService.getAdvertisementPresignedUrl(query.name);
}

/**
* 프로필 이미지를 PUT하는 url 발급
* 프로필 이미지 변경 시 이미지를 PUT하는 url 발급
*/
@Get('profile')
@ApiSuccessResponse(
200,
'프로필 이미지를 업로드하는 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,
);
}

/**
Expand All @@ -72,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,
Expand Down
8 changes: 4 additions & 4 deletions server/src/presigned-url/presigned-url.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,10 @@ export class PresignedUrlService {
return { advertisements };
}

async putProfilePresignedUrl({
uuid,
profileExtension,
}): Promise<PresignedUrlResponseDto> {
async putProfilePresignedUrl(
uuid: string,
profileExtension: string,
): Promise<PresignedUrlResponseDto> {
const objectName = `${uuid}.${profileExtension}`;
const presignedUrl = await createPresignedUrl(
process.env.PROFILE_BUCKET,
Expand Down
4 changes: 3 additions & 1 deletion server/src/user/dto/rated-video-request.dto.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { IsInt, IsPositive } from 'class-validator';
import { IsInt, IsPositive, IsOptional, IsNotEmpty } from 'class-validator';

export class UserRatedVideoQueryDto {
/**
Expand All @@ -11,5 +11,7 @@ export class UserRatedVideoQueryDto {
/**
* 마지막으로 조회한 비디오의 ratedAt
*/
@IsOptional()
@IsNotEmpty()
lastRatedAt?: string;
}
4 changes: 3 additions & 1 deletion server/src/user/dto/uploaded-video-request.dto.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { IsInt, IsPositive } from 'class-validator';
import { IsInt, IsNotEmpty, IsOptional, IsPositive } from 'class-validator';

export class UserUploadedVideoQueryDto {
/**
Expand All @@ -11,5 +11,7 @@ export class UserUploadedVideoQueryDto {
/**
* 마지막으로 조회한 비디오 ID
*/
@IsOptional()
@IsNotEmpty()
lastId?: string;
}
2 changes: 1 addition & 1 deletion server/src/video/dto/video-rating.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export class VideoRatingDTO {
* @example 2
*/
@IsNumber()
@Min(1)
@Min(0)
@Max(5)
rating: number;

Expand Down
13 changes: 11 additions & 2 deletions server/src/video/video.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
Query,
} from '@nestjs/common';
import {
ApiBadRequestResponse,
ApiBearerAuth,
ApiOkResponse,
ApiProduces,
Expand All @@ -30,10 +31,13 @@ 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 { 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';
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';
Expand All @@ -51,7 +55,6 @@ export class VideoController {
constructor(
private videoService: VideoService,
private actionService: ActionService,
private fileExtensionPipe: FileExtensionPipe,
) {}

/**
Expand All @@ -77,6 +80,12 @@ export class VideoController {
)
@Post(':videoId')
@ApiSuccessResponse(201, '비디오 업로드 성공', VideoSummaryResponseDto)
@ApiFailResponse('중복된 비디오 ID', [VideoConflictException])
@ApiFailResponse('잘못된 비디오 ID', [BadRequestFormatException])
@ApiFailResponse('업로드가 필요함', [
VideoUploadRequiredException,
ThumbnailUploadRequiredException,
])
uploadVideo(
@Body() videoDto: VideoDto,
@RequestUser() user: User,
Expand Down
Loading