From 51797ddad22de33abf81413ba8de902118eb180e Mon Sep 17 00:00:00 2001 From: 5tarry Date: Wed, 29 Nov 2023 11:57:48 +0900 Subject: [PATCH 01/14] =?UTF-8?q?feat=20:=20Object=20Storage=EC=9D=98=20Pr?= =?UTF-8?q?esigned=20URL=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/package.json | 1 + server/src/ncpAPI/presignedURL.ts | 32 +++++++++++++++++++++++++++++++ server/yarn.lock | 27 ++++++++++++++++++++------ 3 files changed, 54 insertions(+), 6 deletions(-) create mode 100644 server/src/ncpAPI/presignedURL.ts diff --git a/server/package.json b/server/package.json index a16e4a62..ccba76ca 100644 --- a/server/package.json +++ b/server/package.json @@ -34,6 +34,7 @@ "class-validator": "^0.14.0", "crypto-js": "^4.2.0", "express": "^4.18.2", + "fluent-ffmpeg": "^2.1.2", "lodash": "^4.17.21", "mongoose": "^8.0.0", "reflect-metadata": "^0.1.13", diff --git a/server/src/ncpAPI/presignedURL.ts b/server/src/ncpAPI/presignedURL.ts new file mode 100644 index 00000000..6b21e956 --- /dev/null +++ b/server/src/ncpAPI/presignedURL.ts @@ -0,0 +1,32 @@ +import { HttpRequest } from '@smithy/protocol-http'; +import { S3RequestPresigner } from '@aws-sdk/s3-request-presigner'; +import { parseUrl } from '@smithy/url-parser'; +import { formatUrl } from '@aws-sdk/util-format-url'; +import { Hash } from '@smithy/hash-node'; + +export const createPresignedUrlWithoutClient = async ({ + bucketName, + objectName, + method, +}) => { + const region = 'kr-standard'; + const endPoint = 'https://kr.object.ncloudstorage.com'; + const canonicalURI = `/${bucketName}/${objectName}`; + const apiUrl = `${endPoint}${canonicalURI}`; + const url = parseUrl(apiUrl); + + const presigner = new S3RequestPresigner({ + credentials: { + accessKeyId: process.env.ACCESS_KEY, + secretAccessKey: process.env.SECRET_KEY, + }, + region, + sha256: Hash.bind(null, 'sha256'), + }); + + const signedUrlObject = await presigner.presign( + new HttpRequest({ ...url, method }), + { expiresIn: 100 }, + ); + return formatUrl(signedUrlObject); +}; diff --git a/server/yarn.lock b/server/yarn.lock index 447c065d..1ccd6381 100644 --- a/server/yarn.lock +++ b/server/yarn.lock @@ -1585,6 +1585,11 @@ asap@^2.0.0: resolved "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz" integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA== +async@>=0.2.9: + version "3.2.5" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.5.tgz#ebd52a8fdaf7a2289a24df399f8d8485c8a46b66" + integrity sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg== + asynckit@^0.4.0: version "0.4.0" resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz" @@ -2868,6 +2873,14 @@ flatted@^3.2.9: resolved "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz" integrity sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ== +fluent-ffmpeg@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/fluent-ffmpeg/-/fluent-ffmpeg-2.1.2.tgz#c952de2240f812ebda0aa8006d7776ee2acf7d74" + integrity sha512-IZTB4kq5GK0DPp7sGQ0q/BWurGHffRtQQwVkiqDgeO6wYJLLV5ZhgNOQ65loZxxuPMKZKZcICCUnaGtlxBiR0Q== + dependencies: + async ">=0.2.9" + which "^1.1.1" + follow-redirects@^1.15.0: version "1.15.3" resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz" @@ -4171,16 +4184,11 @@ lodash.once@^4.0.0: resolved "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz" integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg== -lodash@4.17.21: +lodash@4.17.21, lodash@^4.17.21: version "4.17.21" resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== -lodash@^4.17.21: - version "4.17.21" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" - integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== - log-symbols@^4.1.0: version "4.1.0" resolved "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz" @@ -5913,6 +5921,13 @@ which-typed-array@^1.1.11, which-typed-array@^1.1.13: gopd "^1.0.1" has-tostringtag "^1.0.0" +which@^1.1.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + which@^2.0.1: version "2.0.2" resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz" From 9a5204c152e500c179e4c54bde360b6b3b202a60 Mon Sep 17 00:00:00 2001 From: 5tarry Date: Wed, 29 Nov 2023 18:23:13 +0900 Subject: [PATCH 02/14] =?UTF-8?q?feat=20:=20=EA=B4=91=EA=B3=A0=20=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=20presigned=20url=20=EB=B0=98=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/auth/auth.controller.ts | 18 ++++++++--- server/src/auth/auth.service.ts | 32 +++++++++++++++++++ ...advertisement-presigned-url-request.dto.ts | 7 ++++ server/src/ncpAPI/presignedURL.ts | 10 +++--- 4 files changed, 58 insertions(+), 9 deletions(-) create mode 100644 server/src/auth/dto/advertisement-presigned-url-request.dto.ts diff --git a/server/src/auth/auth.controller.ts b/server/src/auth/auth.controller.ts index da04568e..aa4c9ec4 100644 --- a/server/src/auth/auth.controller.ts +++ b/server/src/auth/auth.controller.ts @@ -4,6 +4,8 @@ import { Body, UseInterceptors, UploadedFile, + Get, + Query, } from '@nestjs/common'; import { FileInterceptor } from '@nestjs/platform-express'; import { ApiConsumes, ApiTags } from '@nestjs/swagger'; @@ -20,16 +22,17 @@ import { SigninResponseDto } from './dto/signin-response.dto'; import { SigninRequestDto } from './dto/signin-request.dto'; import { RefreshRequestDto } from './dto/refresh-request.dto'; import { RefreshResponseDto } from './dto/refresh-response.dto'; +import { AdvertisementPresignedUrlRequestDto } from './dto/advertisement-presigned-url-request.dto'; @ApiTags('COMPLETE') -@Controller('auth') +@Controller() export class AuthController { constructor(private authService: AuthService) {} /** * 회원가입 */ - @Post('signup') + @Post('auth/signup') @ApiConsumes('multipart/form-data') @ApiSuccessResponse(201, '회원가입 성공', SignupResponseDto) @ApiFailResponse('인증 실패', [OAuthFailedException]) @@ -45,7 +48,7 @@ export class AuthController { /** * 로그인 */ - @Post('login') + @Post('auth/login') @ApiSuccessResponse(201, '로그인 성공', SigninResponseDto) @ApiFailResponse('인증 실패', [LoginFailException, OAuthFailedException]) signin( @@ -57,7 +60,7 @@ export class AuthController { /** * 토큰 재발급 */ - @Post('refresh') + @Post('auth/refresh') @ApiSuccessResponse(201, '토큰 재발급 성공', RefreshResponseDto) @ApiFailResponse('인증 실패', [InvalidRefreshTokenException]) refresh( @@ -65,4 +68,11 @@ export class AuthController { ): Promise { return this.authService.refresh(refreshRequestDto); } + + @Get('presigned-url/advertisements') + getAdvertisementPresignedUrl( + @Query() query: AdvertisementPresignedUrlRequestDto, + ) { + return this.authService.getAdvertisementPresignedUrl(query.name); + } } diff --git a/server/src/auth/auth.service.ts b/server/src/auth/auth.service.ts index 8cee683d..06bb7ac7 100644 --- a/server/src/auth/auth.service.ts +++ b/server/src/auth/auth.service.ts @@ -8,6 +8,10 @@ import { JwtService } from '@nestjs/jwt'; import { LoginFailException } from 'src/exceptions/login-fail.exception'; import { InvalidRefreshTokenException } from 'src/exceptions/invalid-refresh-token.exception'; import { UserInfoDto } from 'src/user/dto/user-info.dto'; +import { createPresignedUrlWithoutClient } from 'src/ncpAPI/presignedURL'; +import * as _ from 'lodash'; +import { listObjects } from 'src/ncpAPI/listObjects'; +import { xml2js } from 'xml-js'; import { SignupRequestDto } from './dto/signup-request.dto'; import { JwtDto } from './dto/jwt.dto'; import { SignupResponseDto } from './dto/signup-response.dto'; @@ -104,4 +108,32 @@ export class AuthService { (loginInfo) => new SigninResponseDto(loginInfo), ); } + + async getAdvertisementPresignedUrl(adName: string) { + if (adName) + return this.getPresignedUrl(process.env.ADVERTISEMENT_BUCKET, adName); + + const xmlData = await listObjects(process.env.ADVERTISEMENT_BUCKET); + const jsonData: any = xml2js(xmlData, { compact: true }); + + const adList = _.map(jsonData.ListBucketResult.Contents, 'Key._text'); + const advertisements = await Promise.all( + adList.map(async (advertisement: string) => { + return this.getPresignedUrl( + process.env.ADVERTISEMENT_BUCKET, + advertisement, + ); + }), + ); + return { advertisements }; + } + + async getPresignedUrl(bucketName: string, name: string) { + const presignedUrl = await createPresignedUrlWithoutClient( + bucketName, + name, + 'GET', + ); + return { name, presignedUrl }; + } } diff --git a/server/src/auth/dto/advertisement-presigned-url-request.dto.ts b/server/src/auth/dto/advertisement-presigned-url-request.dto.ts new file mode 100644 index 00000000..d817b838 --- /dev/null +++ b/server/src/auth/dto/advertisement-presigned-url-request.dto.ts @@ -0,0 +1,7 @@ +export class AdvertisementPresignedUrlRequestDto { + /** + * 특정 광고 이미지의 presigned url만 받고 싶은 경우 + * @example 'test.webp' + */ + name?: string; +} diff --git a/server/src/ncpAPI/presignedURL.ts b/server/src/ncpAPI/presignedURL.ts index 6b21e956..c4c682cf 100644 --- a/server/src/ncpAPI/presignedURL.ts +++ b/server/src/ncpAPI/presignedURL.ts @@ -4,11 +4,11 @@ import { parseUrl } from '@smithy/url-parser'; import { formatUrl } from '@aws-sdk/util-format-url'; import { Hash } from '@smithy/hash-node'; -export const createPresignedUrlWithoutClient = async ({ - bucketName, - objectName, - method, -}) => { +export const createPresignedUrlWithoutClient = async ( + bucketName: string, + objectName: string, + method: string, +) => { const region = 'kr-standard'; const endPoint = 'https://kr.object.ncloudstorage.com'; const canonicalURI = `/${bucketName}/${objectName}`; From 6e5ca6d945fa9d9241708e6a88ce81275399fffe Mon Sep 17 00:00:00 2001 From: 5tarry Date: Wed, 29 Nov 2023 18:48:00 +0900 Subject: [PATCH 03/14] =?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=20presigned=20url=20=EB=B0=98?= =?UTF-8?q?=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/auth/auth.controller.ts | 6 ++++++ server/src/auth/auth.service.ts | 20 +++++++++---------- .../dto/profile-presigned-url-request.dto.ts | 15 ++++++++++++++ server/src/ncpAPI/presignedURL.ts | 4 ++-- 4 files changed, 33 insertions(+), 12 deletions(-) create mode 100644 server/src/auth/dto/profile-presigned-url-request.dto.ts diff --git a/server/src/auth/auth.controller.ts b/server/src/auth/auth.controller.ts index aa4c9ec4..18dbdeaf 100644 --- a/server/src/auth/auth.controller.ts +++ b/server/src/auth/auth.controller.ts @@ -23,6 +23,7 @@ import { SigninRequestDto } from './dto/signin-request.dto'; import { RefreshRequestDto } from './dto/refresh-request.dto'; import { RefreshResponseDto } from './dto/refresh-response.dto'; import { AdvertisementPresignedUrlRequestDto } from './dto/advertisement-presigned-url-request.dto'; +import { ProfilePresignedUrlRequestDto } from './dto/profile-presigned-url-request.dto'; @ApiTags('COMPLETE') @Controller() @@ -75,4 +76,9 @@ export class AuthController { ) { return this.authService.getAdvertisementPresignedUrl(query.name); } + + @Get('presigned-url/profile') + getProfilePresignedUrl(@Query() query: ProfilePresignedUrlRequestDto) { + return this.authService.getProfilePresignedUrl(query); + } } diff --git a/server/src/auth/auth.service.ts b/server/src/auth/auth.service.ts index 06bb7ac7..e3bfb9aa 100644 --- a/server/src/auth/auth.service.ts +++ b/server/src/auth/auth.service.ts @@ -8,7 +8,7 @@ import { JwtService } from '@nestjs/jwt'; import { LoginFailException } from 'src/exceptions/login-fail.exception'; import { InvalidRefreshTokenException } from 'src/exceptions/invalid-refresh-token.exception'; import { UserInfoDto } from 'src/user/dto/user-info.dto'; -import { createPresignedUrlWithoutClient } from 'src/ncpAPI/presignedURL'; +import { getPresignedUrl } from 'src/ncpAPI/presignedURL'; import * as _ from 'lodash'; import { listObjects } from 'src/ncpAPI/listObjects'; import { xml2js } from 'xml-js'; @@ -111,7 +111,7 @@ export class AuthService { async getAdvertisementPresignedUrl(adName: string) { if (adName) - return this.getPresignedUrl(process.env.ADVERTISEMENT_BUCKET, adName); + return getPresignedUrl(process.env.ADVERTISEMENT_BUCKET, adName, 'GET'); const xmlData = await listObjects(process.env.ADVERTISEMENT_BUCKET); const jsonData: any = xml2js(xmlData, { compact: true }); @@ -119,21 +119,21 @@ export class AuthService { const adList = _.map(jsonData.ListBucketResult.Contents, 'Key._text'); const advertisements = await Promise.all( adList.map(async (advertisement: string) => { - return this.getPresignedUrl( + return getPresignedUrl( process.env.ADVERTISEMENT_BUCKET, advertisement, + 'GET', ); }), ); return { advertisements }; } - async getPresignedUrl(bucketName: string, name: string) { - const presignedUrl = await createPresignedUrlWithoutClient( - bucketName, - name, - 'GET', - ); - return { name, presignedUrl }; + async getProfilePresignedUrl({ uuid, profileExtension }) { + const objectName = `${uuid}.${profileExtension}`; + const presignedUrl = ( + await getPresignedUrl(process.env.PROFILE_BUCKET, objectName, 'PUT') + ).url; + return { presignedUrl }; } } diff --git a/server/src/auth/dto/profile-presigned-url-request.dto.ts b/server/src/auth/dto/profile-presigned-url-request.dto.ts new file mode 100644 index 00000000..4b052285 --- /dev/null +++ b/server/src/auth/dto/profile-presigned-url-request.dto.ts @@ -0,0 +1,15 @@ +import { IsUUID } from 'class-validator'; + +export class ProfilePresignedUrlRequestDto { + /** + * user uuid + */ + @IsUUID() + uuid: string; + + /** + * 프로필 이미지 확장자 + * @example 'webp' + */ + profileExtension: string; +} diff --git a/server/src/ncpAPI/presignedURL.ts b/server/src/ncpAPI/presignedURL.ts index c4c682cf..f0b47537 100644 --- a/server/src/ncpAPI/presignedURL.ts +++ b/server/src/ncpAPI/presignedURL.ts @@ -4,7 +4,7 @@ import { parseUrl } from '@smithy/url-parser'; import { formatUrl } from '@aws-sdk/util-format-url'; import { Hash } from '@smithy/hash-node'; -export const createPresignedUrlWithoutClient = async ( +export const getPresignedUrl = async ( bucketName: string, objectName: string, method: string, @@ -28,5 +28,5 @@ export const createPresignedUrlWithoutClient = async ( new HttpRequest({ ...url, method }), { expiresIn: 100 }, ); - return formatUrl(signedUrlObject); + return { name: objectName, url: formatUrl(signedUrlObject) }; }; From 40f452fd7a8576924ee6aeb160e8d58fcca08ba6 Mon Sep 17 00:00:00 2001 From: 5tarry Date: Wed, 29 Nov 2023 19:15:51 +0900 Subject: [PATCH 04/14] =?UTF-8?q?refactor=20:=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=ED=95=84=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=ED=8C=8C=EC=9D=BC?= =?UTF-8?q?=EC=9D=84=20url=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/user/dto/profile-response.dto.ts | 19 ++++++++++++++++ server/src/user/user.controller.ts | 3 ++- server/src/user/user.service.ts | 25 ++++++++++++--------- 3 files changed, 36 insertions(+), 11 deletions(-) create mode 100644 server/src/user/dto/profile-response.dto.ts diff --git a/server/src/user/dto/profile-response.dto.ts b/server/src/user/dto/profile-response.dto.ts new file mode 100644 index 00000000..ec0fed97 --- /dev/null +++ b/server/src/user/dto/profile-response.dto.ts @@ -0,0 +1,19 @@ +export class ProfileResponseDto { + /** + * 닉네임 + * @example 'honux' + */ + nickname: string; + + /** + * 상태 메세지 + * @example 'web be 마스터' + */ + statusMessage: string; + + /** + * 프로필 이미지 가져오는 presigned url + * @example 'https://ncloud.com/' + */ + profileImageUrl: string; +} diff --git a/server/src/user/user.controller.ts b/server/src/user/user.controller.ts index 4907522e..ba63c05c 100644 --- a/server/src/user/user.controller.ts +++ b/server/src/user/user.controller.ts @@ -24,6 +24,7 @@ import { UploadedVideoResponseDto } from './dto/uploaded-video-response.dto'; import { UserUploadedVideoQueryDto } from './dto/uploaded-video-request.dto'; import { UserRatedVideoQueryDto } from './dto/rated-video-request.dto'; import { RatedVideoResponseDto } from './dto/rated-video-response.dto'; +import { ProfileResponseDto } from './dto/profile-response.dto'; @ApiBearerAuth() @UseGuards(AuthGuard) @@ -37,7 +38,7 @@ export class UserController { */ @Get(':userId/profile') @ApiTags('COMPLETE') - @ApiSuccessResponse(200, '프로필 조회 성공', ProfileDto) + @ApiSuccessResponse(200, '프로필 조회 성공', ProfileResponseDto) @ApiFailResponse('프로필 조회 실패', [UserNotFoundException]) getProfile(@Param('userId') userId: string) { return this.userService.getProfile(userId); diff --git a/server/src/user/user.service.ts b/server/src/user/user.service.ts index 899d1ce1..83750722 100644 --- a/server/src/user/user.service.ts +++ b/server/src/user/user.service.ts @@ -9,10 +9,12 @@ import * as _ from 'lodash'; import { deleteObject } from 'src/ncpAPI/deleteObject'; import { getBucketImage } from 'src/ncpAPI/getBucketImage'; import { VideoService } from 'src/video/video.service'; +import { getPresignedUrl } from 'src/ncpAPI/presignedURL'; import { UploadedVideoResponseDto } from './dto/uploaded-video-response.dto'; import { User } from './schemas/user.schema'; import { ProfileDto } from './dto/profile.dto'; import { RatedVideoResponseDto } from './dto/rated-video-response.dto'; +import { ProfileResponseDto } from './dto/profile-response.dto'; @Injectable() export class UserService { @@ -22,23 +24,27 @@ export class UserService { private videoService: VideoService, ) {} - async getProfile(userId: string) { + async getProfile(userId: string): Promise { const user = await this.UserModel.findOne({ uuid: userId }); if (!user) { throw new UserNotFoundException(); } const { uuid, profileImageExtension, nickname, statusMessage } = user; - const profileImage = await getBucketImage( - process.env.PROFILE_BUCKET, - profileImageExtension, - uuid, - ); - return new ProfileDto({ + const profileImageUrl = profileImageExtension + ? ( + await getPresignedUrl( + process.env.PROFILE_BUCKET, + `${uuid}.${profileImageExtension}`, + 'GET', + ) + ).url + : null; + return { nickname, statusMessage, - ...(profileImage && { profileImage }), - }); + ...(profileImageUrl && { profileImageUrl }), + }; } async patchProfile( @@ -180,7 +186,6 @@ export class UserService { if (!data.length) throw new UserNotFoundException(); const { actions, ...rater } = data.pop(); - console.log(actions); const videos = await Promise.all( actions.map(async (action) => { const videoData = await this.VideoModel.findOne({ From 3931c77aaf87db9a867b917cf7ad7e7f4f8786a4 Mon Sep 17 00:00:00 2001 From: 5tarry Date: Wed, 29 Nov 2023 20:12:01 +0900 Subject: [PATCH 05/14] =?UTF-8?q?refactor=20:=20=EB=9E=9C=EB=8D=A4=20?= =?UTF-8?q?=EB=B9=84=EB=94=94=EC=98=A4=20=EC=9D=91=EB=8B=B5,=20=EC=8D=B8?= =?UTF-8?q?=EB=84=A4=EC=9D=BC=20=ED=81=B4=EB=A6=AD=20=EC=8B=9C=20=EB=B9=84?= =?UTF-8?q?=EB=94=94=EC=98=A4=20=EC=A0=95=EB=B3=B4=20=EB=B0=98=ED=99=98=20?= =?UTF-8?q?api=20=EC=8D=B8=EB=84=A4=EC=9D=BC,=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=ED=95=84=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20url=EB=A1=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/user/dto/uploader-response.dto.ts | 6 ++-- server/src/user/user.service.ts | 24 ++++++++++----- server/src/video/dto/video-response.dto.ts | 6 ++-- server/src/video/video.service.ts | 32 ++++++++++++++------ 4 files changed, 45 insertions(+), 23 deletions(-) diff --git a/server/src/user/dto/uploader-response.dto.ts b/server/src/user/dto/uploader-response.dto.ts index 35fbcd96..f57d1a8f 100644 --- a/server/src/user/dto/uploader-response.dto.ts +++ b/server/src/user/dto/uploader-response.dto.ts @@ -21,8 +21,8 @@ export class UploaderResponseDto { statusMessage: string; /** - * 업로더 이미지 - * @example "�PNG\r\n\u001a\n\u0000\u0000\u0000\" + * 업로더 프로필 이미지를 가져오는 presigned url + * @example 'https://ncloud.com/ */ - profileImage: string; + profileImageUrl: string; } diff --git a/server/src/user/user.service.ts b/server/src/user/user.service.ts index 83750722..7790e15c 100644 --- a/server/src/user/user.service.ts +++ b/server/src/user/user.service.ts @@ -119,14 +119,24 @@ export class UserService { } async getUploaderInfo(uuid: string, uploaderData) { - // eslint-disable-next-line prettier/prettier - const { _id: uploaderId, profileImageExtension, ...uploaderInfo } = uploaderData; - const profileImage = await getBucketImage( - process.env.PROFILE_BUCKET, + const { + _id: uploaderId, profileImageExtension, - uuid, - ); - const uploader = { ...uploaderInfo, ...(profileImage && { profileImage }) }; + ...uploaderInfo + } = uploaderData; + const profileImageUrl = profileImageExtension + ? ( + await getPresignedUrl( + process.env.PROFILE_BUCKET, + `${uuid}.${profileImageExtension}`, + 'GET', + ) + ).url + : null; + const uploader = { + ...uploaderInfo, + ...(profileImageUrl && { profileImageUrl }), + }; return { uploader, uploaderId }; } diff --git a/server/src/video/dto/video-response.dto.ts b/server/src/video/dto/video-response.dto.ts index d7077bcc..ad71143b 100644 --- a/server/src/video/dto/video-response.dto.ts +++ b/server/src/video/dto/video-response.dto.ts @@ -48,8 +48,8 @@ export class VideoResponseDto { manifest: string; /** - * 썸네일 이미지 - * @example "�PNG\r\n\u001a\n\u0000\u0000\u0000\" + * 썸네일 이미지를 가져오는 presigned url + * @example "https://ncloud.com/" */ - thumbnailImage: string; + thumbnailImageUrl: string; } diff --git a/server/src/video/video.service.ts b/server/src/video/video.service.ts index 4f0038f3..08868006 100644 --- a/server/src/video/video.service.ts +++ b/server/src/video/video.service.ts @@ -10,9 +10,9 @@ import { User } from 'src/user/schemas/user.schema'; import { deleteObject } from 'src/ncpAPI/deleteObject'; import { VideoNotFoundException } from 'src/exceptions/video-not-found.exception'; import { NotYourVideoException } from 'src/exceptions/not-your-video.exception'; -import { getBucketImage } from 'src/ncpAPI/getBucketImage'; import axios from 'axios'; import { ActionService } from 'src/action/action.service'; +import { getPresignedUrl } from 'src/ncpAPI/presignedURL'; import { VideoDto } from './dto/video.dto'; import { Video } from './schemas/video.schema'; import { CategoryEnum } from './enum/category.enum'; @@ -68,22 +68,34 @@ export class VideoService { const { profileImageExtension, uuid, ...uploaderInfo } = '_doc' in uploaderId ? uploaderId._doc : uploaderId; // uploaderId가 model인경우 _doc을 붙여줘야함 - const [profileImage, thumbnailImage] = await Promise.all([ - getBucketImage(process.env.PROFILE_BUCKET, profileImageExtension, uuid), - getBucketImage( - process.env.THUMBNAIL_BUCKET, - videoInfo.thumbnailExtension, - videoInfo._id, - ), + const [profileImageUrl, thumbnailImageUrl] = await Promise.all([ + profileImageExtension + ? ( + await getPresignedUrl( + process.env.PROFILE_BUCKET, + `${uuid}.${profileImageExtension}`, + 'GET', + ) + ).url + : null, + videoInfo.thumbnailExtension + ? ( + await getPresignedUrl( + process.env.THUMBNAIL_BUCKET, + `${videoInfo._id}.${videoInfo.thumbnailExtension}`, + 'GET', + ) + ).url + : null, ]); const uploader = { ...uploaderInfo, - ...(profileImage && { profileImage }), + ...(profileImageUrl && { profileImageUrl }), uuid, }; return { - video: { ...videoInfo, manifest, rating, thumbnailImage }, + video: { ...videoInfo, manifest, rating, thumbnailImageUrl }, uploader, }; } From bd1aa22561474bb04cd4f9c7462868e31d3aa7a8 Mon Sep 17 00:00:00 2001 From: 5tarry Date: Wed, 29 Nov 2023 20:45:46 +0900 Subject: [PATCH 06/14] =?UTF-8?q?refactor=20:=20=ED=9A=8C=EC=9B=90?= =?UTF-8?q?=EA=B0=80=EC=9E=85=20api=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EC=97=85=EB=A1=9C=EB=93=9C=20=EB=B6=80?= =?UTF-8?q?=EB=B6=84=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/auth/auth.controller.ts | 4 +--- server/src/auth/auth.service.ts | 19 ++++--------------- server/src/user/dto/profile.dto.ts | 4 +++- server/src/user/dto/user-info.dto.ts | 4 +++- server/src/user/dto/user.dto.ts | 14 +++++--------- 5 files changed, 16 insertions(+), 29 deletions(-) diff --git a/server/src/auth/auth.controller.ts b/server/src/auth/auth.controller.ts index 18dbdeaf..22cf2d1b 100644 --- a/server/src/auth/auth.controller.ts +++ b/server/src/auth/auth.controller.ts @@ -3,7 +3,6 @@ import { Post, Body, UseInterceptors, - UploadedFile, Get, Query, } from '@nestjs/common'; @@ -40,10 +39,9 @@ export class AuthController { @ApiFailResponse('회원가입 실패', [UserConflictException]) @UseInterceptors(FileInterceptor('profileImage')) signUp( - @UploadedFile() profileImage: Express.Multer.File, @Body() signupRequestDto: SignupRequestDto, ): Promise { - return this.authService.create(signupRequestDto, profileImage); + return this.authService.create(signupRequestDto); } /** diff --git a/server/src/auth/auth.service.ts b/server/src/auth/auth.service.ts index e3bfb9aa..4605aa81 100644 --- a/server/src/auth/auth.service.ts +++ b/server/src/auth/auth.service.ts @@ -2,7 +2,6 @@ import { Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { Model } from 'mongoose'; import { UserConflictException } from 'src/exceptions/conflict.exception'; -import { putObject } from 'src/ncpAPI/putObject'; import { User, UserDocument } from 'src/user/schemas/user.schema'; import { JwtService } from '@nestjs/jwt'; import { LoginFailException } from 'src/exceptions/login-fail.exception'; @@ -27,24 +26,14 @@ export class AuthService { private jwtService: JwtService, ) {} - async create( - signupRequestDto: SignupRequestDto, - profileImage: Express.Multer.File, - ): Promise { + async create(signupRequestDto: SignupRequestDto): Promise { const { uuid } = signupRequestDto; if (await this.UserModel.findOne({ uuid })) { throw new UserConflictException(); } - // TODO 프로필 이미지 예외처리 - const profileImageExtension = profileImage - ? profileImage.originalname.split('.').pop() - : null; - if (profileImage) { - putObject( - process.env.PROFILE_BUCKET, - `${uuid}.${profileImageExtension}`, - profileImage.buffer, - ); + const profileImageExtension = signupRequestDto.profileExtension; + if (profileImageExtension) { + // TODO 프로필 이미지 업로드 됐는지 확인 } const newUser = new this.UserModel({ diff --git a/server/src/user/dto/profile.dto.ts b/server/src/user/dto/profile.dto.ts index 2bbac4b2..e8dee07f 100644 --- a/server/src/user/dto/profile.dto.ts +++ b/server/src/user/dto/profile.dto.ts @@ -1,7 +1,9 @@ import { PickType } from '@nestjs/swagger'; import { UserDto } from './user.dto'; -export class ProfileDto extends PickType(UserDto, ['profileImage'] as const) { +export class ProfileDto extends PickType(UserDto, [ + 'profileExtension', +] as const) { constructor(init: ProfileDto) { super(); Object.assign(this, init); diff --git a/server/src/user/dto/user-info.dto.ts b/server/src/user/dto/user-info.dto.ts index 6a37b7b2..ee396de3 100644 --- a/server/src/user/dto/user-info.dto.ts +++ b/server/src/user/dto/user-info.dto.ts @@ -1,7 +1,9 @@ import { OmitType } from '@nestjs/swagger'; import { UserDto } from './user.dto'; -export class UserInfoDto extends OmitType(UserDto, ['profileImage'] as const) { +export class UserInfoDto extends OmitType(UserDto, [ + 'profileExtension', +] as const) { constructor(init: UserInfoDto) { super(); Object.assign(this, init); diff --git a/server/src/user/dto/user.dto.ts b/server/src/user/dto/user.dto.ts index 1ac999d1..41612330 100644 --- a/server/src/user/dto/user.dto.ts +++ b/server/src/user/dto/user.dto.ts @@ -1,4 +1,3 @@ -import { ApiProperty } from '@nestjs/swagger'; import { IsNotEmpty, IsUUID } from 'class-validator'; export class UserDto { @@ -9,14 +8,11 @@ export class UserDto { @IsUUID() uuid: string; - @ApiProperty({ - description: '유저 프로필 사진', - type: 'string', - format: 'binary', - example: '인코딩된 이미지 파일', - }) - // TODO file 유효성검사 - profileImage?: Express.Multer.File; + /** + * 프로필 이미지 확장자 + * @example 'webp' + */ + profileExtension: string; /** * 유저 닉네임 From 7beca204646ff759207b6d088ef88b9cf0706cda Mon Sep 17 00:00:00 2001 From: 5tarry Date: Wed, 29 Nov 2023 21:12:18 +0900 Subject: [PATCH 07/14] =?UTF-8?q?feat=20:=20=EB=B9=84=EB=94=94=EC=98=A4,?= =?UTF-8?q?=20=EC=8D=B8=EB=84=A4=EC=9D=BC=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EC=97=85=EB=A1=9C=EB=93=9C=20=ED=95=98=EB=8A=94=20presigned=20?= =?UTF-8?q?url=20=EB=B0=98=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/package.json | 1 + server/src/auth/auth.controller.ts | 6 +++++ server/src/auth/auth.service.ts | 23 +++++++++++++++++++ .../dto/video-presigned-url-request.dto.ts | 13 +++++++++++ server/yarn.lock | 5 ++++ 5 files changed, 48 insertions(+) create mode 100644 server/src/auth/dto/video-presigned-url-request.dto.ts diff --git a/server/package.json b/server/package.json index ccba76ca..45a45619 100644 --- a/server/package.json +++ b/server/package.json @@ -40,6 +40,7 @@ "reflect-metadata": "^0.1.13", "rxjs": "^7.8.1", "swagger-ui-express": "^5.0.0", + "uuid": "^9.0.1", "xml-js": "1.6.11" }, "devDependencies": { diff --git a/server/src/auth/auth.controller.ts b/server/src/auth/auth.controller.ts index 22cf2d1b..b564dc77 100644 --- a/server/src/auth/auth.controller.ts +++ b/server/src/auth/auth.controller.ts @@ -23,6 +23,7 @@ import { RefreshRequestDto } from './dto/refresh-request.dto'; import { RefreshResponseDto } from './dto/refresh-response.dto'; import { AdvertisementPresignedUrlRequestDto } from './dto/advertisement-presigned-url-request.dto'; import { ProfilePresignedUrlRequestDto } from './dto/profile-presigned-url-request.dto'; +import { VIdeoPresignedUrlRequestDto } from './dto/video-presigned-url-request.dto'; @ApiTags('COMPLETE') @Controller() @@ -79,4 +80,9 @@ export class AuthController { getProfilePresignedUrl(@Query() query: ProfilePresignedUrlRequestDto) { return this.authService.getProfilePresignedUrl(query); } + + @Get('presigned-url/video') + getVideoPresignedUrl(@Query() query: VIdeoPresignedUrlRequestDto) { + return this.authService.getVideoPresignedUrl(query); + } } diff --git a/server/src/auth/auth.service.ts b/server/src/auth/auth.service.ts index 4605aa81..55e44f2a 100644 --- a/server/src/auth/auth.service.ts +++ b/server/src/auth/auth.service.ts @@ -19,6 +19,8 @@ import { SigninRequestDto } from './dto/signin-request.dto'; import { RefreshRequestDto } from './dto/refresh-request.dto'; import { RefreshResponseDto } from './dto/refresh-response.dto'; +const { v4: uuidv4 } = require('uuid'); + @Injectable() export class AuthService { constructor( @@ -125,4 +127,25 @@ export class AuthService { ).url; return { presignedUrl }; } + + async getVideoPresignedUrl({ videoExtension, thumbnailExtension }) { + const videoId = uuidv4(); + const [videoUrl, thumbnailUrl] = await Promise.all([ + ( + await getPresignedUrl( + process.env.INPUT_BUCKET, + `${videoId}.${videoExtension}`, + 'PUT', + ) + ).url, + ( + await getPresignedUrl( + process.env.THUMBNAIL_BUCKET, + `${videoId}.${thumbnailExtension}`, + 'PUT', + ) + ).url, + ]); + return { videoId, videoUrl, thumbnailUrl }; + } } diff --git a/server/src/auth/dto/video-presigned-url-request.dto.ts b/server/src/auth/dto/video-presigned-url-request.dto.ts new file mode 100644 index 00000000..4e93c1a3 --- /dev/null +++ b/server/src/auth/dto/video-presigned-url-request.dto.ts @@ -0,0 +1,13 @@ +export class VIdeoPresignedUrlRequestDto { + /** + * 비디오 확장자 + * @example 'mp4' + */ + videoExtension: string; + + /** + * 썸네일 이미지 확장자 + * @example 'webp' + */ + thumbnailExtension: string; +} diff --git a/server/yarn.lock b/server/yarn.lock index 1ccd6381..12def525 100644 --- a/server/yarn.lock +++ b/server/yarn.lock @@ -5787,6 +5787,11 @@ uuid@9.0.0: resolved "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz" integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg== +uuid@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" + integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== + v8-compile-cache-lib@^3.0.1: version "3.0.1" resolved "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz" From a574d7a4876f9091a69a1530bf372694604e5b55 Mon Sep 17 00:00:00 2001 From: 5tarry Date: Wed, 29 Nov 2023 21:48:45 +0900 Subject: [PATCH 08/14] =?UTF-8?q?refactor=20:=20=EB=B9=84=EB=94=94?= =?UTF-8?q?=EC=98=A4=20=EC=97=85=EB=A1=9C=EB=93=9C=20api=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EB=B6=80=EB=B6=84=20=EC=A0=9C=EA=B1=B0,=20?= =?UTF-8?q?=EB=B9=84=EB=94=94=EC=98=A4ID=20uuid->objectId=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/auth/auth.service.ts | 6 ++---- server/src/video/dto/video.dto.ts | 29 +++++++++++-------------- server/src/video/video.controller.ts | 10 +++------ server/src/video/video.service.ts | 32 +++++++++++----------------- server/yarn.lock | 5 ----- 5 files changed, 30 insertions(+), 52 deletions(-) diff --git a/server/src/auth/auth.service.ts b/server/src/auth/auth.service.ts index 55e44f2a..c8f14f2f 100644 --- a/server/src/auth/auth.service.ts +++ b/server/src/auth/auth.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; -import { Model } from 'mongoose'; +import { Model, Types } from 'mongoose'; import { UserConflictException } from 'src/exceptions/conflict.exception'; import { User, UserDocument } from 'src/user/schemas/user.schema'; import { JwtService } from '@nestjs/jwt'; @@ -19,8 +19,6 @@ import { SigninRequestDto } from './dto/signin-request.dto'; import { RefreshRequestDto } from './dto/refresh-request.dto'; import { RefreshResponseDto } from './dto/refresh-response.dto'; -const { v4: uuidv4 } = require('uuid'); - @Injectable() export class AuthService { constructor( @@ -129,7 +127,7 @@ export class AuthService { } async getVideoPresignedUrl({ videoExtension, thumbnailExtension }) { - const videoId = uuidv4(); + const videoId = new Types.ObjectId(); const [videoUrl, thumbnailUrl] = await Promise.all([ ( await getPresignedUrl( diff --git a/server/src/video/dto/video.dto.ts b/server/src/video/dto/video.dto.ts index d54acadb..30371413 100644 --- a/server/src/video/dto/video.dto.ts +++ b/server/src/video/dto/video.dto.ts @@ -1,28 +1,21 @@ -import { ApiProperty } from '@nestjs/swagger'; import { IsEnum, IsNotEmpty } from 'class-validator'; import { CategoryEnum } from '../enum/category.enum'; export class VideoDto { /** * 비디오 제목 + * @example 제목입니다 */ @IsNotEmpty() title: string; /** * 비디오 내용 + * @example 내용입니다 */ @IsNotEmpty() content: string; - @ApiProperty({ - description: '비디오 파일', - type: 'string', - format: 'binary', - required: true, - }) - video: Express.Multer.File; - /** * 비디오 카테고리 * @example '챌린지' @@ -30,11 +23,15 @@ export class VideoDto { @IsEnum(CategoryEnum) category: CategoryEnum; - @ApiProperty({ - description: '비디오 썸네일 이미지', - type: 'string', - format: 'binary', - required: true, - }) - thumbnail: Express.Multer.File; + /** + * 비디오 확장자 + * @example 'mp4' + */ + videoExtension: string; + + /** + * 썸네일 이미지 확장자 + * @example 'webp' + */ + thumbnailExtension: string; } diff --git a/server/src/video/video.controller.ts b/server/src/video/video.controller.ts index 947c9668..e60d7f08 100644 --- a/server/src/video/video.controller.ts +++ b/server/src/video/video.controller.ts @@ -8,13 +8,11 @@ import { Body, Header, UseInterceptors, - UploadedFiles, UseGuards, Query, } from '@nestjs/common'; import { ApiBearerAuth, - ApiConsumes, ApiOkResponse, ApiProduces, ApiTags, @@ -67,7 +65,6 @@ export class VideoController { /** * 비디오 업로드 */ - @ApiConsumes('multipart/form-data') @UseInterceptors( FileFieldsInterceptor([ { name: 'video', maxCount: 1 }, @@ -75,15 +72,14 @@ export class VideoController { ]), ) @ApiTags('COMPLETE') - @Post() + @Post(':videoId') @ApiSuccessResponse(201, '비디오 업로드 성공', VideoSummaryResponseDto) uploadVideo( - @UploadedFiles() files: Array, @Body() videoDto: VideoDto, @RequestUser() user: User, + @Param('videoId') videoId: string, ) { - this.fileExtensionPipe.transform(files); - return this.videoService.uploadVideo(files, videoDto, user.id); + return this.videoService.uploadVideo(videoDto, user.id, videoId); } /** diff --git a/server/src/video/video.service.ts b/server/src/video/video.service.ts index 08868006..897d0868 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 { putObject } from 'src/ncpAPI/putObject'; import { requestEncoding } from 'src/ncpAPI/requestEncoding'; import { User } from 'src/user/schemas/user.schema'; @@ -100,17 +100,19 @@ export class VideoService { }; } - async uploadVideo(files: any, videoDto: VideoDto, uuid: string) { - const { title, content, category } = videoDto; - const video = files.video.pop(); - const thumbnail = files.thumbnail.pop(); + async uploadVideo(videoDto: VideoDto, uuid: string, videoId: string) { + const { videoExtension, thumbnailExtension } = videoDto; + const videoName = `${videoId}.${videoExtension}`; + const thumbnailName = `${videoId}.${thumbnailExtension}`; + // TODO 비디오, 썸네일 업로드 확인 - const uploader = await this.UserModel.findOne({ uuid }); + await requestEncoding(process.env.INPUT_BUCKET, [videoName]); - const videoExtension = video.originalname.split('.').pop(); - const thumbnailExtension = thumbnail.originalname.split('.').pop(); + const uploader = await this.UserModel.findOne({ uuid }); + const { title, content, category } = videoDto; const newVideo = new this.VideoModel({ + _id: videoId, title, content, category, @@ -118,18 +120,8 @@ export class VideoService { thumbnailExtension, videoExtension, }); - - const videoName = `${newVideo._id}.${videoExtension}`; - const thumbnailName = `${newVideo._id}.${thumbnailExtension}`; - - await Promise.all([ - newVideo.save(), - putObject(process.env.INPUT_BUCKET, videoName, video.buffer), - putObject(process.env.THUMBNAIL_BUCKET, thumbnailName, thumbnail.buffer), - ]); - await requestEncoding(process.env.INPUT_BUCKET, [videoName]); - - return { _id: newVideo._id, ...videoDto }; + await newVideo.save(); + return { videoId, ...videoDto }; } async deleteEncodedVideo(videoId: string) { diff --git a/server/yarn.lock b/server/yarn.lock index 12def525..1ccd6381 100644 --- a/server/yarn.lock +++ b/server/yarn.lock @@ -5787,11 +5787,6 @@ uuid@9.0.0: resolved "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz" integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg== -uuid@^9.0.1: - version "9.0.1" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" - integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== - v8-compile-cache-lib@^3.0.1: version "3.0.1" resolved "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz" From fe98104b87704550343278c8178154109b2f07dd Mon Sep 17 00:00:00 2001 From: 5tarry Date: Wed, 29 Nov 2023 22:43:11 +0900 Subject: [PATCH 09/14] =?UTF-8?q?refactor=20:=20Swagger=20=ED=83=9C?= =?UTF-8?q?=EA=B7=B8=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/package.json | 1 - server/src/app.controller.ts | 2 +- server/src/auth/auth.controller.ts | 24 +++++++++++++++++++----- server/src/auth/auth.service.ts | 4 ++-- server/src/user/user.controller.ts | 4 +--- server/src/video/video.controller.ts | 7 +------ 6 files changed, 24 insertions(+), 18 deletions(-) diff --git a/server/package.json b/server/package.json index 45a45619..ccba76ca 100644 --- a/server/package.json +++ b/server/package.json @@ -40,7 +40,6 @@ "reflect-metadata": "^0.1.13", "rxjs": "^7.8.1", "swagger-ui-express": "^5.0.0", - "uuid": "^9.0.1", "xml-js": "1.6.11" }, "devDependencies": { diff --git a/server/src/app.controller.ts b/server/src/app.controller.ts index 4740f7f4..f52958bc 100644 --- a/server/src/app.controller.ts +++ b/server/src/app.controller.ts @@ -15,7 +15,7 @@ export class AppController { /** * 광고 이미지 응답 */ - @ApiTags('COMPLETE') + @ApiTags('LEGACY') @Get('ads') @ApiSuccessResponse(200, '광고 조회 성공', AdsResponseDto) @ApiFailResponse('인증 실패', [InvalidTokenException, TokenExpiredException]) diff --git a/server/src/auth/auth.controller.ts b/server/src/auth/auth.controller.ts index b564dc77..dd6b3516 100644 --- a/server/src/auth/auth.controller.ts +++ b/server/src/auth/auth.controller.ts @@ -25,7 +25,6 @@ import { AdvertisementPresignedUrlRequestDto } from './dto/advertisement-presign import { ProfilePresignedUrlRequestDto } from './dto/profile-presigned-url-request.dto'; import { VIdeoPresignedUrlRequestDto } from './dto/video-presigned-url-request.dto'; -@ApiTags('COMPLETE') @Controller() export class AuthController { constructor(private authService: AuthService) {} @@ -34,6 +33,7 @@ export class AuthController { * 회원가입 */ @Post('auth/signup') + @ApiTags('AUTH') @ApiConsumes('multipart/form-data') @ApiSuccessResponse(201, '회원가입 성공', SignupResponseDto) @ApiFailResponse('인증 실패', [OAuthFailedException]) @@ -49,6 +49,7 @@ export class AuthController { * 로그인 */ @Post('auth/login') + @ApiTags('AUTH') @ApiSuccessResponse(201, '로그인 성공', SigninResponseDto) @ApiFailResponse('인증 실패', [LoginFailException, OAuthFailedException]) signin( @@ -61,6 +62,7 @@ export class AuthController { * 토큰 재발급 */ @Post('auth/refresh') + @ApiTags('AUTH') @ApiSuccessResponse(201, '토큰 재발급 성공', RefreshResponseDto) @ApiFailResponse('인증 실패', [InvalidRefreshTokenException]) refresh( @@ -69,20 +71,32 @@ export class AuthController { return this.authService.refresh(refreshRequestDto); } + /** + * 광고 이미지를 GET하는 url 발급 + */ @Get('presigned-url/advertisements') + @ApiTags('PRESIGNED URL') getAdvertisementPresignedUrl( @Query() query: AdvertisementPresignedUrlRequestDto, ) { return this.authService.getAdvertisementPresignedUrl(query.name); } + /** + * 프로필 이미지를 PUT하는 url 발급 + */ @Get('presigned-url/profile') - getProfilePresignedUrl(@Query() query: ProfilePresignedUrlRequestDto) { - return this.authService.getProfilePresignedUrl(query); + @ApiTags('PRESIGNED URL') + putProfilePresignedUrl(@Query() query: ProfilePresignedUrlRequestDto) { + return this.authService.putProfilePresignedUrl(query); } + /** + * 비디오, 썸네일 이미지를 PUT하는 url 발급 + */ @Get('presigned-url/video') - getVideoPresignedUrl(@Query() query: VIdeoPresignedUrlRequestDto) { - return this.authService.getVideoPresignedUrl(query); + @ApiTags('PRESIGNED URL') + putVideoPresignedUrl(@Query() query: VIdeoPresignedUrlRequestDto) { + return this.authService.putVideoPresignedUrl(query); } } diff --git a/server/src/auth/auth.service.ts b/server/src/auth/auth.service.ts index c8f14f2f..15cea50c 100644 --- a/server/src/auth/auth.service.ts +++ b/server/src/auth/auth.service.ts @@ -118,7 +118,7 @@ export class AuthService { return { advertisements }; } - async getProfilePresignedUrl({ uuid, profileExtension }) { + async putProfilePresignedUrl({ uuid, profileExtension }) { const objectName = `${uuid}.${profileExtension}`; const presignedUrl = ( await getPresignedUrl(process.env.PROFILE_BUCKET, objectName, 'PUT') @@ -126,7 +126,7 @@ export class AuthService { return { presignedUrl }; } - async getVideoPresignedUrl({ videoExtension, thumbnailExtension }) { + async putVideoPresignedUrl({ videoExtension, thumbnailExtension }) { const videoId = new Types.ObjectId(); const [videoUrl, thumbnailUrl] = await Promise.all([ ( diff --git a/server/src/user/user.controller.ts b/server/src/user/user.controller.ts index ba63c05c..8e6191f4 100644 --- a/server/src/user/user.controller.ts +++ b/server/src/user/user.controller.ts @@ -26,6 +26,7 @@ import { UserRatedVideoQueryDto } from './dto/rated-video-request.dto'; import { RatedVideoResponseDto } from './dto/rated-video-response.dto'; import { ProfileResponseDto } from './dto/profile-response.dto'; +@ApiTags('USER') @ApiBearerAuth() @UseGuards(AuthGuard) @ApiFailResponse('인증 실패', [InvalidTokenException, TokenExpiredException]) @@ -37,7 +38,6 @@ export class UserController { * 프로필 조회 */ @Get(':userId/profile') - @ApiTags('COMPLETE') @ApiSuccessResponse(200, '프로필 조회 성공', ProfileResponseDto) @ApiFailResponse('프로필 조회 실패', [UserNotFoundException]) getProfile(@Param('userId') userId: string) { @@ -48,7 +48,6 @@ export class UserController { * 프로필 변경 */ @ApiConsumes('multipart/form-data') - @ApiTags('COMPLETE') @UseInterceptors(FileInterceptor('profileImage')) @ApiSuccessResponse(200, '프로필 변경 성공', ProfileDto) @Patch('profile') @@ -64,7 +63,6 @@ export class UserController { * 특정 유저가 업로드 한 비디오 정보 반환 */ @Get(':userId/videos/uploaded') - @ApiTags('COMPLETE') @ApiSuccessResponse(200, '비디오 반환 성공', UploadedVideoResponseDto) @ApiFailResponse('조회 실패', [UserNotFoundException]) getUploadedVideos( diff --git a/server/src/video/video.controller.ts b/server/src/video/video.controller.ts index e60d7f08..4848cbce 100644 --- a/server/src/video/video.controller.ts +++ b/server/src/video/video.controller.ts @@ -41,6 +41,7 @@ import { VideoRatingResponseDTO } from './dto/video-rating-response.dto'; import { TopVideoQueryDto } from './dto/top-video-query.dto'; import { VideoListResponseDto } from './dto/video-list-response.dto'; +@ApiTags('VIDEO') @ApiBearerAuth() @UseGuards(AuthGuard) @ApiFailResponse('인증 실패', [InvalidTokenException, TokenExpiredException]) @@ -55,7 +56,6 @@ export class VideoController { /** * 랜덤으로 비디오 응답 */ - @ApiTags('COMPLETE') @Get('random') @ApiSuccessResponse(200, '랜덤 비디오 반환 성공', VideoListResponseDto) getRandomVideo(@Query() query: RandomVideoQueryDto) { @@ -71,7 +71,6 @@ export class VideoController { { name: 'thumbnail', maxCount: 1 }, ]), ) - @ApiTags('COMPLETE') @Post(':videoId') @ApiSuccessResponse(201, '비디오 업로드 성공', VideoSummaryResponseDto) uploadVideo( @@ -86,7 +85,6 @@ export class VideoController { * 카테고리별 TOP 10 조회 */ @Get('top-rated') - @ApiTags('COMPLETE') @ApiSuccessResponse(200, 'TOP 10 조회 성공', VideoListResponseDto) getTopRatedVideo(@Query() query: TopVideoQueryDto) { return this.videoService.getTopRatedVideo(query.category); @@ -129,7 +127,6 @@ export class VideoController { * 썸네일 클릭 시 비디오 정보 반환 */ @Get(':id') - @ApiTags('COMPLETE') @ApiSuccessResponse(200, '비디오 조회 성공', VideoInfoDto) @ApiFailResponse('비디오를 찾을 수 없음', [VideoNotFoundException]) getVideo(@Param('id') videoId: string) { @@ -140,7 +137,6 @@ export class VideoController { * 비디오 삭제 */ @Delete(':id') - @ApiTags('COMPLETE') @ApiSuccessResponse(200, '비디오 삭제 성공', VideoSummaryResponseDto) @ApiFailResponse('업로더만이 삭제할 수 있음', [NotYourVideoException]) @ApiFailResponse('비디오를 찾을 수 없음', [VideoNotFoundException]) @@ -151,7 +147,6 @@ export class VideoController { /** * 비디오 별점 등록/수정 */ - @ApiTags('COMPLETE') @Put(':id/rating') @ApiSuccessResponse(200, '비디오 별점 등록/수정 성공', VideoRatingResponseDTO) @ApiFailResponse('비디오를 찾을 수 없음', [VideoNotFoundException]) From 78e6f583eda0e3a3a0b5cfeb4ff4114bfc4f1929 Mon Sep 17 00:00:00 2001 From: 5tarry Date: Thu, 30 Nov 2023 00:14:31 +0900 Subject: [PATCH 10/14] =?UTF-8?q?refactor=20:=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=ED=95=84=20=EB=B3=80=EA=B2=BD=20api=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=9D=B4=EB=AF=B8=EC=A7=80=20=ED=8C=8C=EC=9D=BC=20=EB=B6=80?= =?UTF-8?q?=EB=B6=84=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/user/user.controller.ts | 9 ++---- server/src/user/user.service.ts | 49 ++++++++++++++---------------- 2 files changed, 24 insertions(+), 34 deletions(-) diff --git a/server/src/user/user.controller.ts b/server/src/user/user.controller.ts index 8e6191f4..a0262b02 100644 --- a/server/src/user/user.controller.ts +++ b/server/src/user/user.controller.ts @@ -47,16 +47,11 @@ export class UserController { /** * 프로필 변경 */ - @ApiConsumes('multipart/form-data') @UseInterceptors(FileInterceptor('profileImage')) @ApiSuccessResponse(200, '프로필 변경 성공', ProfileDto) @Patch('profile') - patchProfile( - @UploadedFile() profileImage: Express.Multer.File, - @Body() profileDto: ProfileDto, - @RequestUser() user: User, - ) { - return this.userService.patchProfile(profileDto, profileImage, user.id); + patchProfile(@Body() profileDto: ProfileDto, @RequestUser() user: User) { + return this.userService.patchProfile(profileDto, user.id); } /** diff --git a/server/src/user/user.service.ts b/server/src/user/user.service.ts index 7790e15c..3236d0fd 100644 --- a/server/src/user/user.service.ts +++ b/server/src/user/user.service.ts @@ -47,13 +47,10 @@ export class UserService { }; } - async patchProfile( - profileDto: ProfileDto, - profileImage: Express.Multer.File, - uuid: string, - ): Promise { + async patchProfile(profileDto: ProfileDto, uuid: string) { const updateOption = _.omitBy(profileDto, _.isEmpty); // profileDto 중 빈 문자열인 필드 제거 const user = await this.UserModel.findOne({ uuid }); + if (user.nickname === updateOption.nickname) { // 실제로 변경된 필드만 updateOption에 남기기 delete updateOption.nickname; @@ -61,36 +58,34 @@ export class UserService { if (user.statusMessage === updateOption.statusMessage) { delete updateOption.statusMessage; } + // TODO profileExtension, profileImageExtension 중 통일하는게 좋으려나 + updateOption.profileImageExtension = updateOption.profileExtension; + delete updateOption.profileExtension; - let updatedProfileImage; - if (profileImage) { - const profileImageExtension = profileImage.originalname.split('.').pop(); - putObject( - process.env.PROFILE_BUCKET, - `${uuid}.${profileImageExtension}`, - profileImage.buffer, - ); - updateOption.profileImageExtension = profileImageExtension; - updatedProfileImage = profileImage.buffer; - } else if ( - 'profileImage' in profileDto && - user.profileImageExtension !== null - ) { - // profileImage 필드를 빈 문자열로 주었고, 기존 프로필이미지가 있었다면 삭제 + // TODO profileUrl이 null이 아니라면 프로필 이미지가 업로드됐는지 확인 + const profileUrl = profileDto.profileExtension + ? ( + await getPresignedUrl( + process.env.PROFILE_BUCKET, + `${uuid}.${profileDto.profileExtension}`, + 'GET', + ) + ).url + : null; + if (profileDto.profileExtension === '' && user.profileImageExtension) { + // profileExtension 필드를 빈 문자열로 주었고, 기존 프로필이미지가 있었다면 삭제 deleteObject( process.env.PROFILE_BUCKET, `${uuid}.${user.profileImageExtension}`, ); updateOption.profileImageExtension = null; - updatedProfileImage = null; } - await this.UserModel.updateOne({ uuid }, updateOption); - if (updatedProfileImage !== undefined) { - updateOption.profileImage = updatedProfileImage; - delete updateOption.profileImageExtension; - } - return updateOption; + // TODO findOneAndUpdate이 아니라 다른걸 써서 update 결과를 받고 싶음 + const result = ( + await this.UserModel.findOneAndUpdate({ uuid }, updateOption) + ).toObject(); + return { ...result, profileUrl }; } async getUploadedVideos( From 1d1c5f1500b1faee36625b5fb33c98c877eb67ef Mon Sep 17 00:00:00 2001 From: 5tarry Date: Thu, 30 Nov 2023 00:52:27 +0900 Subject: [PATCH 11/14] =?UTF-8?q?feat=20:=20=EB=A7=8C=EB=A3=8C=EB=90=9C=20?= =?UTF-8?q?get=20presigned=20url=20=EC=9E=AC=EB=B0=9C=EA=B8=89=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/auth/auth.controller.ts | 13 +++++++++ server/src/auth/auth.service.ts | 29 +++++++++++++++---- .../dto/reissue-presigned-url-request.dto.ts | 21 ++++++++++++++ server/src/ncpAPI/presignedURL.ts | 2 +- server/src/user/user.controller.ts | 3 +- server/src/user/user.service.ts | 9 +++--- server/src/video/video.service.ts | 9 +++--- 7 files changed, 67 insertions(+), 19 deletions(-) create mode 100644 server/src/auth/dto/reissue-presigned-url-request.dto.ts diff --git a/server/src/auth/auth.controller.ts b/server/src/auth/auth.controller.ts index dd6b3516..ddf8e8c3 100644 --- a/server/src/auth/auth.controller.ts +++ b/server/src/auth/auth.controller.ts @@ -5,6 +5,7 @@ import { UseInterceptors, Get, Query, + Param, } from '@nestjs/common'; import { FileInterceptor } from '@nestjs/platform-express'; import { ApiConsumes, ApiTags } from '@nestjs/swagger'; @@ -24,6 +25,7 @@ import { RefreshResponseDto } from './dto/refresh-response.dto'; import { AdvertisementPresignedUrlRequestDto } from './dto/advertisement-presigned-url-request.dto'; import { ProfilePresignedUrlRequestDto } from './dto/profile-presigned-url-request.dto'; import { VIdeoPresignedUrlRequestDto } from './dto/video-presigned-url-request.dto'; +import { ReissuePresignedUrlRequestDto } from './dto/reissue-presigned-url-request.dto'; @Controller() export class AuthController { @@ -99,4 +101,15 @@ export class AuthController { putVideoPresignedUrl(@Query() query: VIdeoPresignedUrlRequestDto) { return this.authService.putVideoPresignedUrl(query); } + + /** + * 만료된 presigned url 재발급 + */ + @Get('presigned-url/reissue/:id') + getImagePresignedUrl( + @Param('id') id: string, + @Query() query: ReissuePresignedUrlRequestDto, + ) { + return this.authService.getImagePresignedUrl(id, query); + } } diff --git a/server/src/auth/auth.service.ts b/server/src/auth/auth.service.ts index 15cea50c..47799045 100644 --- a/server/src/auth/auth.service.ts +++ b/server/src/auth/auth.service.ts @@ -7,7 +7,7 @@ import { JwtService } from '@nestjs/jwt'; import { LoginFailException } from 'src/exceptions/login-fail.exception'; import { InvalidRefreshTokenException } from 'src/exceptions/invalid-refresh-token.exception'; import { UserInfoDto } from 'src/user/dto/user-info.dto'; -import { getPresignedUrl } from 'src/ncpAPI/presignedURL'; +import { createPresignedUrl } from 'src/ncpAPI/presignedURL'; import * as _ from 'lodash'; import { listObjects } from 'src/ncpAPI/listObjects'; import { xml2js } from 'xml-js'; @@ -100,7 +100,11 @@ export class AuthService { async getAdvertisementPresignedUrl(adName: string) { if (adName) - return getPresignedUrl(process.env.ADVERTISEMENT_BUCKET, adName, 'GET'); + return createPresignedUrl( + process.env.ADVERTISEMENT_BUCKET, + adName, + 'GET', + ); const xmlData = await listObjects(process.env.ADVERTISEMENT_BUCKET); const jsonData: any = xml2js(xmlData, { compact: true }); @@ -108,7 +112,7 @@ export class AuthService { const adList = _.map(jsonData.ListBucketResult.Contents, 'Key._text'); const advertisements = await Promise.all( adList.map(async (advertisement: string) => { - return getPresignedUrl( + return createPresignedUrl( process.env.ADVERTISEMENT_BUCKET, advertisement, 'GET', @@ -121,7 +125,7 @@ export class AuthService { async putProfilePresignedUrl({ uuid, profileExtension }) { const objectName = `${uuid}.${profileExtension}`; const presignedUrl = ( - await getPresignedUrl(process.env.PROFILE_BUCKET, objectName, 'PUT') + await createPresignedUrl(process.env.PROFILE_BUCKET, objectName, 'PUT') ).url; return { presignedUrl }; } @@ -130,14 +134,14 @@ export class AuthService { const videoId = new Types.ObjectId(); const [videoUrl, thumbnailUrl] = await Promise.all([ ( - await getPresignedUrl( + await createPresignedUrl( process.env.INPUT_BUCKET, `${videoId}.${videoExtension}`, 'PUT', ) ).url, ( - await getPresignedUrl( + await createPresignedUrl( process.env.THUMBNAIL_BUCKET, `${videoId}.${thumbnailExtension}`, 'PUT', @@ -146,4 +150,17 @@ export class AuthService { ]); return { videoId, videoUrl, thumbnailUrl }; } + + async getImagePresignedUrl(id: string, { type, extension }) { + // TODO 업로드 된 이미지인지 확인 + const bucketName = { + thumbnail: process.env.THUMBNAIL_BUCKET, + profile: process.env.PROFILE_BUCKET, + }[type]; + const objectName = `${id}.${extension}`; + const presignedUrl = ( + await createPresignedUrl(bucketName, objectName, 'GET') + ).url; + return { presignedUrl }; + } } diff --git a/server/src/auth/dto/reissue-presigned-url-request.dto.ts b/server/src/auth/dto/reissue-presigned-url-request.dto.ts new file mode 100644 index 00000000..341d81a6 --- /dev/null +++ b/server/src/auth/dto/reissue-presigned-url-request.dto.ts @@ -0,0 +1,21 @@ +import { IsEnum } from 'class-validator'; + +enum TypeEnum { + thumbnail = 'thumbnail', + profile = 'profile', +} + +export class ReissuePresignedUrlRequestDto { + /** + * 요청하는 이미지 종류 + * @example 'thumbnail' + */ + @IsEnum(TypeEnum) + type: TypeEnum; + + /** + * 이미지 확장자 + * @example 'webp' + */ + extension: string; +} diff --git a/server/src/ncpAPI/presignedURL.ts b/server/src/ncpAPI/presignedURL.ts index f0b47537..649f217e 100644 --- a/server/src/ncpAPI/presignedURL.ts +++ b/server/src/ncpAPI/presignedURL.ts @@ -4,7 +4,7 @@ import { parseUrl } from '@smithy/url-parser'; import { formatUrl } from '@aws-sdk/util-format-url'; import { Hash } from '@smithy/hash-node'; -export const getPresignedUrl = async ( +export const createPresignedUrl = async ( bucketName: string, objectName: string, method: string, diff --git a/server/src/user/user.controller.ts b/server/src/user/user.controller.ts index a0262b02..2d2ad42f 100644 --- a/server/src/user/user.controller.ts +++ b/server/src/user/user.controller.ts @@ -6,11 +6,10 @@ import { UseGuards, Param, UseInterceptors, - UploadedFile, Query, } from '@nestjs/common'; import { RequestUser, User } from 'src/decorators/request-user'; -import { ApiBearerAuth, ApiConsumes, ApiTags } from '@nestjs/swagger'; +import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; import { AuthGuard } from 'src/auth/auth.guard'; import { ApiFailResponse } from 'src/decorators/api-fail-response'; import { InvalidTokenException } from 'src/exceptions/invalid-token.exception'; diff --git a/server/src/user/user.service.ts b/server/src/user/user.service.ts index 3236d0fd..a5f73429 100644 --- a/server/src/user/user.service.ts +++ b/server/src/user/user.service.ts @@ -3,13 +3,12 @@ import { Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { Document, Model } from 'mongoose'; import { Video } from 'src/video/schemas/video.schema'; -import { putObject } from 'src/ncpAPI/putObject'; import { UserNotFoundException } from 'src/exceptions/user-not-found.exception'; import * as _ from 'lodash'; import { deleteObject } from 'src/ncpAPI/deleteObject'; import { getBucketImage } from 'src/ncpAPI/getBucketImage'; import { VideoService } from 'src/video/video.service'; -import { getPresignedUrl } from 'src/ncpAPI/presignedURL'; +import { createPresignedUrl } from 'src/ncpAPI/presignedURL'; import { UploadedVideoResponseDto } from './dto/uploaded-video-response.dto'; import { User } from './schemas/user.schema'; import { ProfileDto } from './dto/profile.dto'; @@ -33,7 +32,7 @@ export class UserService { const { uuid, profileImageExtension, nickname, statusMessage } = user; const profileImageUrl = profileImageExtension ? ( - await getPresignedUrl( + await createPresignedUrl( process.env.PROFILE_BUCKET, `${uuid}.${profileImageExtension}`, 'GET', @@ -65,7 +64,7 @@ export class UserService { // TODO profileUrl이 null이 아니라면 프로필 이미지가 업로드됐는지 확인 const profileUrl = profileDto.profileExtension ? ( - await getPresignedUrl( + await createPresignedUrl( process.env.PROFILE_BUCKET, `${uuid}.${profileDto.profileExtension}`, 'GET', @@ -121,7 +120,7 @@ export class UserService { } = uploaderData; const profileImageUrl = profileImageExtension ? ( - await getPresignedUrl( + await createPresignedUrl( process.env.PROFILE_BUCKET, `${uuid}.${profileImageExtension}`, 'GET', diff --git a/server/src/video/video.service.ts b/server/src/video/video.service.ts index 897d0868..138675b2 100644 --- a/server/src/video/video.service.ts +++ b/server/src/video/video.service.ts @@ -3,8 +3,7 @@ /* eslint-disable class-methods-use-this */ import { Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; -import { Model, Types } from 'mongoose'; -import { putObject } from 'src/ncpAPI/putObject'; +import { Model } from 'mongoose'; import { requestEncoding } from 'src/ncpAPI/requestEncoding'; import { User } from 'src/user/schemas/user.schema'; import { deleteObject } from 'src/ncpAPI/deleteObject'; @@ -12,7 +11,7 @@ import { VideoNotFoundException } from 'src/exceptions/video-not-found.exception import { NotYourVideoException } from 'src/exceptions/not-your-video.exception'; import axios from 'axios'; import { ActionService } from 'src/action/action.service'; -import { getPresignedUrl } from 'src/ncpAPI/presignedURL'; +import { createPresignedUrl } from 'src/ncpAPI/presignedURL'; import { VideoDto } from './dto/video.dto'; import { Video } from './schemas/video.schema'; import { CategoryEnum } from './enum/category.enum'; @@ -71,7 +70,7 @@ export class VideoService { const [profileImageUrl, thumbnailImageUrl] = await Promise.all([ profileImageExtension ? ( - await getPresignedUrl( + await createPresignedUrl( process.env.PROFILE_BUCKET, `${uuid}.${profileImageExtension}`, 'GET', @@ -80,7 +79,7 @@ export class VideoService { : null, videoInfo.thumbnailExtension ? ( - await getPresignedUrl( + await createPresignedUrl( process.env.THUMBNAIL_BUCKET, `${videoInfo._id}.${videoInfo.thumbnailExtension}`, 'GET', From 8d5cba10eebf6ec8ed10accb41a34a9967fc795e Mon Sep 17 00:00:00 2001 From: 5tarry Date: Thu, 30 Nov 2023 01:09:05 +0900 Subject: [PATCH 12/14] =?UTF-8?q?chore=20:=20presigned=20url=20=EB=AA=A8?= =?UTF-8?q?=EB=93=88=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/app.module.ts | 8 +- server/src/auth/auth.controller.ts | 62 ++------------- server/src/auth/auth.service.ts | 72 +----------------- ...advertisement-presigned-url-request.dto.ts | 0 .../dto/profile-presigned-url-request.dto.ts | 0 .../dto/reissue-presigned-url-request.dto.ts | 0 .../dto/video-presigned-url-request.dto.ts | 0 .../presigned-url/presigned-url.controller.ts | 57 ++++++++++++++ .../src/presigned-url/presigned-url.module.ts | 4 + .../presigned-url/presigned-url.service.ts | 75 +++++++++++++++++++ 10 files changed, 148 insertions(+), 130 deletions(-) rename server/src/{auth => presigned-url}/dto/advertisement-presigned-url-request.dto.ts (100%) rename server/src/{auth => presigned-url}/dto/profile-presigned-url-request.dto.ts (100%) rename server/src/{auth => presigned-url}/dto/reissue-presigned-url-request.dto.ts (100%) rename server/src/{auth => presigned-url}/dto/video-presigned-url-request.dto.ts (100%) create mode 100644 server/src/presigned-url/presigned-url.controller.ts create mode 100644 server/src/presigned-url/presigned-url.module.ts create mode 100644 server/src/presigned-url/presigned-url.service.ts diff --git a/server/src/app.module.ts b/server/src/app.module.ts index 96e1e67a..9607b37f 100644 --- a/server/src/app.module.ts +++ b/server/src/app.module.ts @@ -7,6 +7,9 @@ import { ActionModule } from './action/action.module'; import { UserModule } from './user/user.module'; import { AppController } from './app.controller'; import { AppService } from './app.service'; +import { PresignedUrlController } from './presigned-url/presigned-url.controller'; +import { PresignedUrlService } from './presigned-url/presigned-url.service'; +import { PresignedUrlModule } from './presigned-url/presigned-url.module'; @Module({ imports: [ @@ -16,8 +19,9 @@ import { AppService } from './app.service'; VideoModule, ActionModule, UserModule, + PresignedUrlModule, ], - controllers: [AppController], - providers: [AppService], + controllers: [AppController, PresignedUrlController], + providers: [AppService, PresignedUrlService], }) export class AppModule {} diff --git a/server/src/auth/auth.controller.ts b/server/src/auth/auth.controller.ts index ddf8e8c3..7cabab2d 100644 --- a/server/src/auth/auth.controller.ts +++ b/server/src/auth/auth.controller.ts @@ -1,12 +1,4 @@ -import { - Controller, - Post, - Body, - UseInterceptors, - Get, - Query, - Param, -} from '@nestjs/common'; +import { Controller, Post, Body, UseInterceptors } from '@nestjs/common'; import { FileInterceptor } from '@nestjs/platform-express'; import { ApiConsumes, ApiTags } from '@nestjs/swagger'; import { ApiSuccessResponse } from 'src/decorators/api-succes-response'; @@ -22,19 +14,15 @@ import { SigninResponseDto } from './dto/signin-response.dto'; import { SigninRequestDto } from './dto/signin-request.dto'; import { RefreshRequestDto } from './dto/refresh-request.dto'; import { RefreshResponseDto } from './dto/refresh-response.dto'; -import { AdvertisementPresignedUrlRequestDto } from './dto/advertisement-presigned-url-request.dto'; -import { ProfilePresignedUrlRequestDto } from './dto/profile-presigned-url-request.dto'; -import { VIdeoPresignedUrlRequestDto } from './dto/video-presigned-url-request.dto'; -import { ReissuePresignedUrlRequestDto } from './dto/reissue-presigned-url-request.dto'; -@Controller() +@Controller('auth') export class AuthController { constructor(private authService: AuthService) {} /** * 회원가입 */ - @Post('auth/signup') + @Post('signup') @ApiTags('AUTH') @ApiConsumes('multipart/form-data') @ApiSuccessResponse(201, '회원가입 성공', SignupResponseDto) @@ -50,7 +38,7 @@ export class AuthController { /** * 로그인 */ - @Post('auth/login') + @Post('login') @ApiTags('AUTH') @ApiSuccessResponse(201, '로그인 성공', SigninResponseDto) @ApiFailResponse('인증 실패', [LoginFailException, OAuthFailedException]) @@ -63,7 +51,7 @@ export class AuthController { /** * 토큰 재발급 */ - @Post('auth/refresh') + @Post('refresh') @ApiTags('AUTH') @ApiSuccessResponse(201, '토큰 재발급 성공', RefreshResponseDto) @ApiFailResponse('인증 실패', [InvalidRefreshTokenException]) @@ -72,44 +60,4 @@ export class AuthController { ): Promise { return this.authService.refresh(refreshRequestDto); } - - /** - * 광고 이미지를 GET하는 url 발급 - */ - @Get('presigned-url/advertisements') - @ApiTags('PRESIGNED URL') - getAdvertisementPresignedUrl( - @Query() query: AdvertisementPresignedUrlRequestDto, - ) { - return this.authService.getAdvertisementPresignedUrl(query.name); - } - - /** - * 프로필 이미지를 PUT하는 url 발급 - */ - @Get('presigned-url/profile') - @ApiTags('PRESIGNED URL') - putProfilePresignedUrl(@Query() query: ProfilePresignedUrlRequestDto) { - return this.authService.putProfilePresignedUrl(query); - } - - /** - * 비디오, 썸네일 이미지를 PUT하는 url 발급 - */ - @Get('presigned-url/video') - @ApiTags('PRESIGNED URL') - putVideoPresignedUrl(@Query() query: VIdeoPresignedUrlRequestDto) { - return this.authService.putVideoPresignedUrl(query); - } - - /** - * 만료된 presigned url 재발급 - */ - @Get('presigned-url/reissue/:id') - getImagePresignedUrl( - @Param('id') id: string, - @Query() query: ReissuePresignedUrlRequestDto, - ) { - return this.authService.getImagePresignedUrl(id, query); - } } diff --git a/server/src/auth/auth.service.ts b/server/src/auth/auth.service.ts index 47799045..a70ce15a 100644 --- a/server/src/auth/auth.service.ts +++ b/server/src/auth/auth.service.ts @@ -1,16 +1,12 @@ import { Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; -import { Model, Types } from 'mongoose'; +import { Model } from 'mongoose'; import { UserConflictException } from 'src/exceptions/conflict.exception'; import { User, UserDocument } from 'src/user/schemas/user.schema'; import { JwtService } from '@nestjs/jwt'; import { LoginFailException } from 'src/exceptions/login-fail.exception'; import { InvalidRefreshTokenException } from 'src/exceptions/invalid-refresh-token.exception'; import { UserInfoDto } from 'src/user/dto/user-info.dto'; -import { createPresignedUrl } from 'src/ncpAPI/presignedURL'; -import * as _ from 'lodash'; -import { listObjects } from 'src/ncpAPI/listObjects'; -import { xml2js } from 'xml-js'; import { SignupRequestDto } from './dto/signup-request.dto'; import { JwtDto } from './dto/jwt.dto'; import { SignupResponseDto } from './dto/signup-response.dto'; @@ -97,70 +93,4 @@ export class AuthService { (loginInfo) => new SigninResponseDto(loginInfo), ); } - - async getAdvertisementPresignedUrl(adName: string) { - if (adName) - return createPresignedUrl( - process.env.ADVERTISEMENT_BUCKET, - adName, - 'GET', - ); - - const xmlData = await listObjects(process.env.ADVERTISEMENT_BUCKET); - const jsonData: any = xml2js(xmlData, { compact: true }); - - const adList = _.map(jsonData.ListBucketResult.Contents, 'Key._text'); - const advertisements = await Promise.all( - adList.map(async (advertisement: string) => { - return createPresignedUrl( - process.env.ADVERTISEMENT_BUCKET, - advertisement, - 'GET', - ); - }), - ); - return { advertisements }; - } - - async putProfilePresignedUrl({ uuid, profileExtension }) { - const objectName = `${uuid}.${profileExtension}`; - const presignedUrl = ( - await createPresignedUrl(process.env.PROFILE_BUCKET, objectName, 'PUT') - ).url; - return { presignedUrl }; - } - - async putVideoPresignedUrl({ videoExtension, thumbnailExtension }) { - const videoId = new Types.ObjectId(); - const [videoUrl, thumbnailUrl] = await Promise.all([ - ( - await createPresignedUrl( - process.env.INPUT_BUCKET, - `${videoId}.${videoExtension}`, - 'PUT', - ) - ).url, - ( - await createPresignedUrl( - process.env.THUMBNAIL_BUCKET, - `${videoId}.${thumbnailExtension}`, - 'PUT', - ) - ).url, - ]); - return { videoId, videoUrl, thumbnailUrl }; - } - - async getImagePresignedUrl(id: string, { type, extension }) { - // TODO 업로드 된 이미지인지 확인 - const bucketName = { - thumbnail: process.env.THUMBNAIL_BUCKET, - profile: process.env.PROFILE_BUCKET, - }[type]; - const objectName = `${id}.${extension}`; - const presignedUrl = ( - await createPresignedUrl(bucketName, objectName, 'GET') - ).url; - return { presignedUrl }; - } } diff --git a/server/src/auth/dto/advertisement-presigned-url-request.dto.ts b/server/src/presigned-url/dto/advertisement-presigned-url-request.dto.ts similarity index 100% rename from server/src/auth/dto/advertisement-presigned-url-request.dto.ts rename to server/src/presigned-url/dto/advertisement-presigned-url-request.dto.ts diff --git a/server/src/auth/dto/profile-presigned-url-request.dto.ts b/server/src/presigned-url/dto/profile-presigned-url-request.dto.ts similarity index 100% rename from server/src/auth/dto/profile-presigned-url-request.dto.ts rename to server/src/presigned-url/dto/profile-presigned-url-request.dto.ts diff --git a/server/src/auth/dto/reissue-presigned-url-request.dto.ts b/server/src/presigned-url/dto/reissue-presigned-url-request.dto.ts similarity index 100% rename from server/src/auth/dto/reissue-presigned-url-request.dto.ts rename to server/src/presigned-url/dto/reissue-presigned-url-request.dto.ts diff --git a/server/src/auth/dto/video-presigned-url-request.dto.ts b/server/src/presigned-url/dto/video-presigned-url-request.dto.ts similarity index 100% rename from server/src/auth/dto/video-presigned-url-request.dto.ts rename to server/src/presigned-url/dto/video-presigned-url-request.dto.ts diff --git a/server/src/presigned-url/presigned-url.controller.ts b/server/src/presigned-url/presigned-url.controller.ts new file mode 100644 index 00000000..07f58ef3 --- /dev/null +++ b/server/src/presigned-url/presigned-url.controller.ts @@ -0,0 +1,57 @@ +import { Controller, Get, Param, Query, UseGuards } from '@nestjs/common'; +import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { ApiFailResponse } from 'src/decorators/api-fail-response'; +import { AuthGuard } from 'src/auth/auth.guard'; +import { InvalidTokenException } from 'src/exceptions/invalid-token.exception'; +import { TokenExpiredException } from 'src/exceptions/token-expired.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'; +import { VIdeoPresignedUrlRequestDto } from './dto/video-presigned-url-request.dto'; +import { ReissuePresignedUrlRequestDto } from './dto/reissue-presigned-url-request.dto'; + +@ApiTags('PRESIGNED URL') +@ApiBearerAuth() +@UseGuards(AuthGuard) +@ApiFailResponse('인증 실패', [InvalidTokenException, TokenExpiredException]) +@Controller('presigned-url') +export class PresignedUrlController { + constructor(private presignedUrlService: PresignedUrlService) {} + + /** + * 광고 이미지를 GET하는 url 발급 + */ + @Get('advertisements') + getAdvertisementPresignedUrl( + @Query() query: AdvertisementPresignedUrlRequestDto, + ) { + return this.presignedUrlService.getAdvertisementPresignedUrl(query.name); + } + + /** + * 프로필 이미지를 PUT하는 url 발급 + */ + @Get('profile') + putProfilePresignedUrl(@Query() query: ProfilePresignedUrlRequestDto) { + return this.presignedUrlService.putProfilePresignedUrl(query); + } + + /** + * 비디오, 썸네일 이미지를 PUT하는 url 발급 + */ + @Get('video') + putVideoPresignedUrl(@Query() query: VIdeoPresignedUrlRequestDto) { + return this.presignedUrlService.putVideoPresignedUrl(query); + } + + /** + * 만료된 presigned url 재발급 + */ + @Get('reissue/:id') + getImagePresignedUrl( + @Param('id') id: string, + @Query() query: ReissuePresignedUrlRequestDto, + ) { + return this.presignedUrlService.getImagePresignedUrl(id, query); + } +} diff --git a/server/src/presigned-url/presigned-url.module.ts b/server/src/presigned-url/presigned-url.module.ts new file mode 100644 index 00000000..742bfad5 --- /dev/null +++ b/server/src/presigned-url/presigned-url.module.ts @@ -0,0 +1,4 @@ +import { Module } from '@nestjs/common'; + +@Module({}) +export class PresignedUrlModule {} diff --git a/server/src/presigned-url/presigned-url.service.ts b/server/src/presigned-url/presigned-url.service.ts new file mode 100644 index 00000000..550d2f44 --- /dev/null +++ b/server/src/presigned-url/presigned-url.service.ts @@ -0,0 +1,75 @@ +import { Injectable } from '@nestjs/common'; +import { createPresignedUrl } from 'src/ncpAPI/presignedURL'; +import { listObjects } from 'src/ncpAPI/listObjects'; +import * as _ from 'lodash'; +import { xml2js } from 'xml-js'; +import { Types } from 'mongoose'; + +@Injectable() +export class PresignedUrlService { + async getAdvertisementPresignedUrl(adName: string) { + if (adName) + return createPresignedUrl( + process.env.ADVERTISEMENT_BUCKET, + adName, + 'GET', + ); + + const xmlData = await listObjects(process.env.ADVERTISEMENT_BUCKET); + const jsonData: any = xml2js(xmlData, { compact: true }); + + const adList = _.map(jsonData.ListBucketResult.Contents, 'Key._text'); + const advertisements = await Promise.all( + adList.map(async (advertisement: string) => { + return createPresignedUrl( + process.env.ADVERTISEMENT_BUCKET, + advertisement, + 'GET', + ); + }), + ); + return { advertisements }; + } + + async putProfilePresignedUrl({ uuid, profileExtension }) { + const objectName = `${uuid}.${profileExtension}`; + const presignedUrl = ( + await createPresignedUrl(process.env.PROFILE_BUCKET, objectName, 'PUT') + ).url; + return { presignedUrl }; + } + + async putVideoPresignedUrl({ videoExtension, thumbnailExtension }) { + const videoId = new Types.ObjectId(); + const [videoUrl, thumbnailUrl] = await Promise.all([ + ( + await createPresignedUrl( + process.env.INPUT_BUCKET, + `${videoId}.${videoExtension}`, + 'PUT', + ) + ).url, + ( + await createPresignedUrl( + process.env.THUMBNAIL_BUCKET, + `${videoId}.${thumbnailExtension}`, + 'PUT', + ) + ).url, + ]); + return { videoId, videoUrl, thumbnailUrl }; + } + + async getImagePresignedUrl(id: string, { type, extension }) { + // TODO 업로드 된 이미지인지 확인 + const bucketName = { + thumbnail: process.env.THUMBNAIL_BUCKET, + profile: process.env.PROFILE_BUCKET, + }[type]; + const objectName = `${id}.${extension}`; + const presignedUrl = ( + await createPresignedUrl(bucketName, objectName, 'GET') + ).url; + return { presignedUrl }; + } +} From c09bf4b1f454a746702a34fc55061b67d4b9847b Mon Sep 17 00:00:00 2001 From: 5tarry Date: Thu, 30 Nov 2023 11:12:45 +0900 Subject: [PATCH 13/14] =?UTF-8?q?feat=20:=20presigned=20url=20=EC=9D=91?= =?UTF-8?q?=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 --- ...dvertisement-presigned-url-response.dto.ts | 21 +++++++++++ .../dto/presigned-url-response.dto.ts | 7 ++++ .../dto/video-presigned-url-response.dto.ts | 21 +++++++++++ .../presigned-url/presigned-url.controller.ts | 26 +++++++++++++- .../presigned-url/presigned-url.service.ts | 36 ++++++++++++++----- 5 files changed, 101 insertions(+), 10 deletions(-) create mode 100644 server/src/presigned-url/dto/advertisement-presigned-url-response.dto.ts create mode 100644 server/src/presigned-url/dto/presigned-url-response.dto.ts create mode 100644 server/src/presigned-url/dto/video-presigned-url-response.dto.ts diff --git a/server/src/presigned-url/dto/advertisement-presigned-url-response.dto.ts b/server/src/presigned-url/dto/advertisement-presigned-url-response.dto.ts new file mode 100644 index 00000000..2232c570 --- /dev/null +++ b/server/src/presigned-url/dto/advertisement-presigned-url-response.dto.ts @@ -0,0 +1,21 @@ +/* eslint-disable max-classes-per-file */ +import { ApiProperty } from '@nestjs/swagger'; + +class PresignedUrlDto { + /** + * 파일 이름 + * @example 'test.webp' + */ + name: string; + + /** + * 파일을 가져올 수 있는 presigned url + * @example "https://kr.object.ncloudstorage.com/" + */ + url: string; +} + +export class AdvertisementPresignedUrlResponseDto { + @ApiProperty({ type: [PresignedUrlDto] }) + advertisements: [PresignedUrlDto]; +} diff --git a/server/src/presigned-url/dto/presigned-url-response.dto.ts b/server/src/presigned-url/dto/presigned-url-response.dto.ts new file mode 100644 index 00000000..e24d8d63 --- /dev/null +++ b/server/src/presigned-url/dto/presigned-url-response.dto.ts @@ -0,0 +1,7 @@ +export class PresignedUrlResponseDto { + /** + * presigned url + * @example 'https://kr.object.ncloudstorage.com/' + */ + presignedUrl: string; +} diff --git a/server/src/presigned-url/dto/video-presigned-url-response.dto.ts b/server/src/presigned-url/dto/video-presigned-url-response.dto.ts new file mode 100644 index 00000000..46161c54 --- /dev/null +++ b/server/src/presigned-url/dto/video-presigned-url-response.dto.ts @@ -0,0 +1,21 @@ +import { Types } from 'mongoose'; + +export class VideoPresignedUrlResponseDto { + /** + * 비디오 ID + * @example '6567eb9aa1efacad06e24b81' + */ + videoId: Types.ObjectId; + + /** + * 비디오 업로드 presigned url + * @example 'https://kr.object.ncloudstorage.com' + */ + videoUrl: string; + + /** + * 썸네일 업로드 presigned url + * @example 'https://kr.object.ncloudstorage.com' + */ + thumbnailUrl: string; +} diff --git a/server/src/presigned-url/presigned-url.controller.ts b/server/src/presigned-url/presigned-url.controller.ts index 07f58ef3..6d9e37d5 100644 --- a/server/src/presigned-url/presigned-url.controller.ts +++ b/server/src/presigned-url/presigned-url.controller.ts @@ -1,14 +1,18 @@ import { Controller, Get, Param, Query, UseGuards } from '@nestjs/common'; -import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { ApiBearerAuth, ApiParam, ApiTags } from '@nestjs/swagger'; import { ApiFailResponse } from 'src/decorators/api-fail-response'; 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 { PresignedUrlService } from './presigned-url.service'; import { AdvertisementPresignedUrlRequestDto } from './dto/advertisement-presigned-url-request.dto'; import { ProfilePresignedUrlRequestDto } from './dto/profile-presigned-url-request.dto'; import { VIdeoPresignedUrlRequestDto } from './dto/video-presigned-url-request.dto'; import { ReissuePresignedUrlRequestDto } from './dto/reissue-presigned-url-request.dto'; +import { AdvertisementPresignedUrlResponseDto } from './dto/advertisement-presigned-url-response.dto'; +import { PresignedUrlResponseDto } from './dto/presigned-url-response.dto'; +import { VideoPresignedUrlResponseDto } from './dto/video-presigned-url-response.dto'; @ApiTags('PRESIGNED URL') @ApiBearerAuth() @@ -22,6 +26,11 @@ export class PresignedUrlController { * 광고 이미지를 GET하는 url 발급 */ @Get('advertisements') + @ApiSuccessResponse( + 200, + '광고 이미지 가져오는 url 발급 성공', + AdvertisementPresignedUrlResponseDto, + ) getAdvertisementPresignedUrl( @Query() query: AdvertisementPresignedUrlRequestDto, ) { @@ -32,6 +41,11 @@ export class PresignedUrlController { * 프로필 이미지를 PUT하는 url 발급 */ @Get('profile') + @ApiSuccessResponse( + 200, + '프로필 이미지를 업로드하는 url 발급 성공', + PresignedUrlResponseDto, + ) putProfilePresignedUrl(@Query() query: ProfilePresignedUrlRequestDto) { return this.presignedUrlService.putProfilePresignedUrl(query); } @@ -40,6 +54,11 @@ export class PresignedUrlController { * 비디오, 썸네일 이미지를 PUT하는 url 발급 */ @Get('video') + @ApiSuccessResponse( + 200, + '비디오/썸네일을 업로드하는 url 발급 성공', + VideoPresignedUrlResponseDto, + ) putVideoPresignedUrl(@Query() query: VIdeoPresignedUrlRequestDto) { return this.presignedUrlService.putVideoPresignedUrl(query); } @@ -48,6 +67,11 @@ export class PresignedUrlController { * 만료된 presigned url 재발급 */ @Get('reissue/:id') + @ApiParam({ + name: 'id', + description: '썸네일 재발급 시 비디오ID, 프로필 재발급 시 유저 UUID', + }) + @ApiSuccessResponse(200, 'presigned url 재발급 성공', PresignedUrlResponseDto) getImagePresignedUrl( @Param('id') id: string, @Query() query: ReissuePresignedUrlRequestDto, diff --git a/server/src/presigned-url/presigned-url.service.ts b/server/src/presigned-url/presigned-url.service.ts index 550d2f44..4212c42e 100644 --- a/server/src/presigned-url/presigned-url.service.ts +++ b/server/src/presigned-url/presigned-url.service.ts @@ -4,16 +4,25 @@ import { listObjects } from 'src/ncpAPI/listObjects'; import * as _ from 'lodash'; import { xml2js } from 'xml-js'; import { Types } from 'mongoose'; +import { AdvertisementPresignedUrlResponseDto } from './dto/advertisement-presigned-url-response.dto'; +import { PresignedUrlResponseDto } from './dto/presigned-url-response.dto'; +import { VideoPresignedUrlResponseDto } from './dto/video-presigned-url-response.dto'; @Injectable() export class PresignedUrlService { - async getAdvertisementPresignedUrl(adName: string) { + async getAdvertisementPresignedUrl( + adName: string, + ): Promise { if (adName) - return createPresignedUrl( - process.env.ADVERTISEMENT_BUCKET, - adName, - 'GET', - ); + return { + advertisements: [ + await createPresignedUrl( + process.env.ADVERTISEMENT_BUCKET, + adName, + 'GET', + ), + ], + }; const xmlData = await listObjects(process.env.ADVERTISEMENT_BUCKET); const jsonData: any = xml2js(xmlData, { compact: true }); @@ -31,7 +40,10 @@ export class PresignedUrlService { return { advertisements }; } - async putProfilePresignedUrl({ uuid, profileExtension }) { + async putProfilePresignedUrl({ + uuid, + profileExtension, + }): Promise { const objectName = `${uuid}.${profileExtension}`; const presignedUrl = ( await createPresignedUrl(process.env.PROFILE_BUCKET, objectName, 'PUT') @@ -39,7 +51,10 @@ export class PresignedUrlService { return { presignedUrl }; } - async putVideoPresignedUrl({ videoExtension, thumbnailExtension }) { + async putVideoPresignedUrl({ + videoExtension, + thumbnailExtension, + }): Promise { const videoId = new Types.ObjectId(); const [videoUrl, thumbnailUrl] = await Promise.all([ ( @@ -60,7 +75,10 @@ export class PresignedUrlService { return { videoId, videoUrl, thumbnailUrl }; } - async getImagePresignedUrl(id: string, { type, extension }) { + async getImagePresignedUrl( + id: string, + { type, extension }, + ): Promise { // TODO 업로드 된 이미지인지 확인 const bucketName = { thumbnail: process.env.THUMBNAIL_BUCKET, From 722fa6b2385f7e0285264fd71cba0e9b85e32754 Mon Sep 17 00:00:00 2001 From: 5tarry Date: Thu, 30 Nov 2023 11:22:05 +0900 Subject: [PATCH 14/14] =?UTF-8?q?refactor=20:=20=ED=95=A8=EC=88=98=20?= =?UTF-8?q?=EB=B0=98=ED=99=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/ncpAPI/presignedURL.ts | 2 +- .../presigned-url/presigned-url.service.ts | 66 ++++++++++--------- server/src/user/user.service.ts | 36 +++++----- server/src/video/video.service.ts | 26 +++----- 4 files changed, 62 insertions(+), 68 deletions(-) diff --git a/server/src/ncpAPI/presignedURL.ts b/server/src/ncpAPI/presignedURL.ts index 649f217e..2fd32f04 100644 --- a/server/src/ncpAPI/presignedURL.ts +++ b/server/src/ncpAPI/presignedURL.ts @@ -28,5 +28,5 @@ export const createPresignedUrl = async ( new HttpRequest({ ...url, method }), { expiresIn: 100 }, ); - return { name: objectName, url: formatUrl(signedUrlObject) }; + return formatUrl(signedUrlObject); }; diff --git a/server/src/presigned-url/presigned-url.service.ts b/server/src/presigned-url/presigned-url.service.ts index 4212c42e..63710855 100644 --- a/server/src/presigned-url/presigned-url.service.ts +++ b/server/src/presigned-url/presigned-url.service.ts @@ -16,11 +16,14 @@ export class PresignedUrlService { if (adName) return { advertisements: [ - await createPresignedUrl( - process.env.ADVERTISEMENT_BUCKET, - adName, - 'GET', - ), + { + name: adName, + url: await createPresignedUrl( + process.env.ADVERTISEMENT_BUCKET, + adName, + 'GET', + ), + }, ], }; @@ -30,11 +33,14 @@ export class PresignedUrlService { const adList = _.map(jsonData.ListBucketResult.Contents, 'Key._text'); const advertisements = await Promise.all( adList.map(async (advertisement: string) => { - return createPresignedUrl( - process.env.ADVERTISEMENT_BUCKET, - advertisement, - 'GET', - ); + return { + name: advertisement, + url: await createPresignedUrl( + process.env.ADVERTISEMENT_BUCKET, + advertisement, + 'GET', + ), + }; }), ); return { advertisements }; @@ -45,9 +51,11 @@ export class PresignedUrlService { profileExtension, }): Promise { const objectName = `${uuid}.${profileExtension}`; - const presignedUrl = ( - await createPresignedUrl(process.env.PROFILE_BUCKET, objectName, 'PUT') - ).url; + const presignedUrl = await createPresignedUrl( + process.env.PROFILE_BUCKET, + objectName, + 'PUT', + ); return { presignedUrl }; } @@ -57,20 +65,16 @@ export class PresignedUrlService { }): Promise { const videoId = new Types.ObjectId(); const [videoUrl, thumbnailUrl] = await Promise.all([ - ( - await createPresignedUrl( - process.env.INPUT_BUCKET, - `${videoId}.${videoExtension}`, - 'PUT', - ) - ).url, - ( - await createPresignedUrl( - process.env.THUMBNAIL_BUCKET, - `${videoId}.${thumbnailExtension}`, - 'PUT', - ) - ).url, + await createPresignedUrl( + process.env.INPUT_BUCKET, + `${videoId}.${videoExtension}`, + 'PUT', + ), + await createPresignedUrl( + process.env.THUMBNAIL_BUCKET, + `${videoId}.${thumbnailExtension}`, + 'PUT', + ), ]); return { videoId, videoUrl, thumbnailUrl }; } @@ -85,9 +89,11 @@ export class PresignedUrlService { profile: process.env.PROFILE_BUCKET, }[type]; const objectName = `${id}.${extension}`; - const presignedUrl = ( - await createPresignedUrl(bucketName, objectName, 'GET') - ).url; + const presignedUrl = await createPresignedUrl( + bucketName, + objectName, + 'GET', + ); return { presignedUrl }; } } diff --git a/server/src/user/user.service.ts b/server/src/user/user.service.ts index a5f73429..d870b4bb 100644 --- a/server/src/user/user.service.ts +++ b/server/src/user/user.service.ts @@ -31,13 +31,11 @@ export class UserService { } const { uuid, profileImageExtension, nickname, statusMessage } = user; const profileImageUrl = profileImageExtension - ? ( - await createPresignedUrl( - process.env.PROFILE_BUCKET, - `${uuid}.${profileImageExtension}`, - 'GET', - ) - ).url + ? await createPresignedUrl( + process.env.PROFILE_BUCKET, + `${uuid}.${profileImageExtension}`, + 'GET', + ) : null; return { nickname, @@ -63,13 +61,11 @@ export class UserService { // TODO profileUrl이 null이 아니라면 프로필 이미지가 업로드됐는지 확인 const profileUrl = profileDto.profileExtension - ? ( - await createPresignedUrl( - process.env.PROFILE_BUCKET, - `${uuid}.${profileDto.profileExtension}`, - 'GET', - ) - ).url + ? await createPresignedUrl( + process.env.PROFILE_BUCKET, + `${uuid}.${profileDto.profileExtension}`, + 'GET', + ) : null; if (profileDto.profileExtension === '' && user.profileImageExtension) { // profileExtension 필드를 빈 문자열로 주었고, 기존 프로필이미지가 있었다면 삭제 @@ -119,13 +115,11 @@ export class UserService { ...uploaderInfo } = uploaderData; const profileImageUrl = profileImageExtension - ? ( - await createPresignedUrl( - process.env.PROFILE_BUCKET, - `${uuid}.${profileImageExtension}`, - 'GET', - ) - ).url + ? await createPresignedUrl( + process.env.PROFILE_BUCKET, + `${uuid}.${profileImageExtension}`, + 'GET', + ) : null; const uploader = { ...uploaderInfo, diff --git a/server/src/video/video.service.ts b/server/src/video/video.service.ts index 138675b2..0e76ffec 100644 --- a/server/src/video/video.service.ts +++ b/server/src/video/video.service.ts @@ -69,23 +69,17 @@ export class VideoService { '_doc' in uploaderId ? uploaderId._doc : uploaderId; // uploaderId가 model인경우 _doc을 붙여줘야함 const [profileImageUrl, thumbnailImageUrl] = await Promise.all([ profileImageExtension - ? ( - await createPresignedUrl( - process.env.PROFILE_BUCKET, - `${uuid}.${profileImageExtension}`, - 'GET', - ) - ).url - : null, - videoInfo.thumbnailExtension - ? ( - await createPresignedUrl( - process.env.THUMBNAIL_BUCKET, - `${videoInfo._id}.${videoInfo.thumbnailExtension}`, - 'GET', - ) - ).url + ? await createPresignedUrl( + process.env.PROFILE_BUCKET, + `${uuid}.${profileImageExtension}`, + 'GET', + ) : null, + await createPresignedUrl( + process.env.THUMBNAIL_BUCKET, + `${videoInfo._id}.${videoInfo.thumbnailExtension}`, + 'GET', + ), ]); const uploader = { ...uploaderInfo,