-
Notifications
You must be signed in to change notification settings - Fork 2
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
백엔드 서버 Pre-Signed URL 반환하는 API 구현 / 프론트에서 해당 API 를 이용해서 이미지 upload 함수 구현 #106
Changes from all commits
1313384
756de2f
360c30d
f0b9bb5
d1a9d20
0054596
eecf689
b6f950d
2bcde7d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import { IsNotEmpty, IsString } from 'class-validator'; | ||
import { IsImageFile } from '../storage.validator'; | ||
|
||
export class PreSignedPostRequest { | ||
@IsString() | ||
@IsNotEmpty() | ||
dirName: string; | ||
|
||
@IsString() | ||
@IsNotEmpty() | ||
@IsImageFile() | ||
extension: string; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. p3. 여기도 검증 추가할 수 있겠네요. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 오 좋습니다.. |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import { BaseException } from '../../common/exception/BaseException'; | ||
import { HttpStatus } from '@nestjs/common'; | ||
|
||
export class CloudFunctionsFetchException extends BaseException { | ||
constructor(error: Error) { | ||
super({ | ||
code: 777, | ||
message: `cloud function 과 fetch 중 오류가 발생했습니다. : ${error.message}`, | ||
status: HttpStatus.CONFLICT, | ||
}); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
export const IMAGE_EXTENSIONS = new Set([ | ||
'jpg', | ||
'jpeg', | ||
'png', | ||
'gif', | ||
'bmp', | ||
'tiff', | ||
'tif', | ||
'webp', | ||
'svg', | ||
'heic', | ||
'raw', | ||
'cr2', | ||
'nef', | ||
'arw', | ||
'dng', | ||
'ico', | ||
]); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import { Body, Controller, Post, UseGuards } from '@nestjs/common'; | ||
import { StorageService } from './storage.service'; | ||
import { JwtAuthGuard } from '../auth/JwtAuthGuard'; | ||
import { PreSignedPostRequest } from './dto/PreSignedPostRequest'; | ||
import { Throttle } from '@nestjs/throttler'; | ||
|
||
@Controller('storage') | ||
export class StorageController { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. storage! 괜찮네요 ㅎㅎ There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 감사합니다! |
||
constructor(private readonly storageService: StorageService) {} | ||
|
||
@Throttle({ default: { limit: 10, ttl: 60000 } }) | ||
@Post('/preSignedPost') | ||
@UseGuards(JwtAuthGuard) | ||
async getPreSignedPost(@Body() preSignedPostRequest: PreSignedPostRequest) { | ||
const { dirName, extension } = preSignedPostRequest; | ||
return await this.storageService.generatePreSignedPost(dirName, extension); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import { Module } from '@nestjs/common'; | ||
import { StorageController } from './storage.controller'; | ||
import { StorageService } from './storage.service'; | ||
|
||
@Module({ | ||
controllers: [StorageController], | ||
providers: [StorageService], | ||
}) | ||
export class StorageModule {} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import { Injectable } from '@nestjs/common'; | ||
import { ConfigService } from '@nestjs/config'; | ||
import { CloudFunctionsFetchException } from './exception/CloudFunctionsFetchException'; | ||
|
||
@Injectable() | ||
export class StorageService { | ||
private preSignedPost: string; | ||
|
||
constructor(private configService: ConfigService) { | ||
this.preSignedPost = this.configService.get<string>('PRE_SIGNED_POST'); | ||
} | ||
|
||
async generatePreSignedPost(dirName: string, extension: string) { | ||
return fetch(this.preSignedPost, { | ||
method: 'POST', | ||
headers: { | ||
'Content-Type': 'application/json', | ||
}, | ||
body: JSON.stringify({ | ||
dirName: dirName, | ||
extension: extension, | ||
}), | ||
}) | ||
.then((res) => res.json()) | ||
.then((data) => { | ||
return data; | ||
}) | ||
.catch((err) => { | ||
throw new CloudFunctionsFetchException(err); | ||
}); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import { | ||
registerDecorator, | ||
ValidationArguments, | ||
ValidatorOptions, | ||
} from 'class-validator'; | ||
import { IMAGE_EXTENSIONS } from './storage.constants'; | ||
|
||
export function IsImageFile(validationOptions?: ValidatorOptions) { | ||
return function (object: object, propertyName: string) { | ||
registerDecorator({ | ||
name: 'IsImageFile', | ||
target: object.constructor, | ||
propertyName: propertyName, | ||
options: validationOptions, | ||
validator: { | ||
validate(value: any) { | ||
return IMAGE_EXTENSIONS.has(value); | ||
}, | ||
defaultMessage(validationArguments?: ValidationArguments): string { | ||
return `${validationArguments.value} 는 이미지 확장자가 아닙니다.`; | ||
}, | ||
}, | ||
}); | ||
}; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
import { axiosInstance } from '../axiosInstance'; | ||
import { PreSignedURLResponse } from '../../types'; | ||
import { END_POINTS, IMAGE_EXTENSIONS } from '@/constants/api'; | ||
import { THREE_MB } from '../../constants/api'; | ||
|
||
export const generatePreSignedPost = async ( | ||
dirName: string, | ||
extension: string, | ||
) => { | ||
const { data } = await axiosInstance.post<PreSignedURLResponse>( | ||
END_POINTS.PRE_SIGNED_POST, | ||
{ | ||
dirName: dirName, | ||
extension: extension, | ||
}, | ||
); | ||
return data; | ||
}; | ||
|
||
export const getExtensionByFile = (file: File) => { | ||
const extension = file.name.split('.').pop(); | ||
return extension ? extension.toLowerCase() : null; | ||
}; | ||
|
||
export const validateFile = (file: File, extension: string | null) => { | ||
return !( | ||
!extension || | ||
!IMAGE_EXTENSIONS.has(extension) || | ||
file.size > THREE_MB | ||
); | ||
}; | ||
|
||
export const uploadImage = async (file: File, dirName: string) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. q. 여긴 다 function 키워드 대신 익명함수네요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
const extension = getExtensionByFile(file); | ||
if (!validateFile(file, extension)) { | ||
throw new Error( | ||
'지원되지 않는 파일 형식이거나 파일 크기가 3MB를 초과합니다.', | ||
); | ||
} | ||
const preSignedPost = await generatePreSignedPost(dirName, extension!); | ||
const formData = new FormData(); | ||
Object.entries(preSignedPost.fields).forEach(([key, value]) => { | ||
formData.append(key, value); | ||
}); | ||
formData.append('file', file); | ||
return fetch(preSignedPost.url, { | ||
method: 'POST', | ||
body: formData, | ||
}) | ||
.then(() => { | ||
return preSignedPost.uploadedUrl; | ||
}) | ||
.catch((err) => { | ||
throw new Error(err); | ||
}); | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이거 이렇게도 가능해요!
사실 이걸 생각하고 리뷰 남긴건데 똑같이 동작하니 괜찮습니다 ㅎㅎ 학습했다 치죠!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
아하... 그러게요 엄청 어려웠네요..