diff --git a/api-docs b/api-docs index b68aac3..83ff666 160000 --- a/api-docs +++ b/api-docs @@ -1 +1 @@ -Subproject commit b68aac34729b4dd4dd7be9f1f49f02e2d030b694 +Subproject commit 83ff666d42c48dbb3377742656e8cad49183ee43 diff --git a/src/controllers/feeds.controller.ts b/src/controllers/feeds.controller.ts index 4ac1f04..06067c6 100644 --- a/src/controllers/feeds.controller.ts +++ b/src/controllers/feeds.controller.ts @@ -155,10 +155,10 @@ const deleteFeed = async (req: Request, res: Response) => { const getFeedList = async (req: Request, res: Response) => { const categoryId: number = Number(req.query.categoryId); - const page: number = Number(req.query.page); + const index: number = Number(req.query.index); const limit: number = Number(req.query.limit); - const result = await feedsService.getFeedList(categoryId, page, limit); + const result = await feedsService.getFeedList(categoryId, index, limit); res.status(200).json(result); }; diff --git a/src/services/feeds.service.ts b/src/services/feeds.service.ts index c572644..adaf8a8 100644 --- a/src/services/feeds.service.ts +++ b/src/services/feeds.service.ts @@ -32,35 +32,23 @@ const getTempFeedList = async (userId: number) => { }; // 임시저장 및 게시글 저장 ----------------------------------------------------------- -const createFeed = async ( +const maxTransactionAttempts = 3; +const executeTransactionWithRetry = async ( + attempt: number, feedInfo: TempFeedDto | FeedDto, fileLinks: string[], - options?: FeedOption + options: FeedOption ): Promise => { - if (feedInfo.status === 2) { - feedInfo = plainToInstance(TempFeedDto, feedInfo); - } else { - feedInfo = plainToInstance(FeedDto, feedInfo); - feedInfo.posted_at = new Date(); - } - - await validateOrReject(feedInfo).catch(errors => { - throw { status: 500, message: errors[0].constraints }; - }); - - // transaction으로 묶어주기 const queryRunner = dataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); try { - // feed 저장 const newFeedInstance = plainToInstance(Feed, feedInfo); const newFeed = await queryRunner.manager .withRepository(FeedRepository) .createFeed(newFeedInstance); - // uploadFile에 feed의 ID를 연결해주는 함수 if (fileLinks) { await uploadFileService.updateFileLinks(queryRunner, newFeed, fileLinks); @@ -85,16 +73,53 @@ const createFeed = async ( .getFeed(newFeed.id, options); await queryRunner.commitTransaction(); - return result; - } catch (err) { + } catch (err: any) { await queryRunner.rollbackTransaction(); - throw new Error(`createFeed TRANSACTION error: ${err}`); - } finally { - await queryRunner.release(); + + if ( + err.message.includes('Lock wait timeout') && + attempt < maxTransactionAttempts + ) { + console.log(`createFeed TRANSACTION retry: ${attempt}`); + return await executeTransactionWithRetry( + attempt + 1, + feedInfo, + fileLinks, + options + ); + } else { + throw new Error(`createFeed TRANSACTION error: ${err}`); + } } }; +const createFeed = async ( + feedInfo: TempFeedDto | FeedDto, + fileLinks: string[], + options?: FeedOption +): Promise => { + if (feedInfo.status === 2) { + feedInfo = plainToInstance(TempFeedDto, feedInfo); + } else { + feedInfo = plainToInstance(FeedDto, feedInfo); + feedInfo.posted_at = new Date(); + } + + await validateOrReject(feedInfo).catch(errors => { + throw { status: 500, message: errors[0].constraints }; + }); + + const result = await executeTransactionWithRetry( + 1, + feedInfo, + fileLinks, + options + ); + + return result; +}; + // 임시게시글 및 게시글 수정 ----------------------------------------------------------- const updateFeed = async ( userId: number, @@ -211,7 +236,7 @@ const getFeed = async ( // 게시글 리스트 -------------------------------------------------------------- const getFeedList = async ( categoryId: number, - page: number, + index: number, limit: number ): Promise => { // query로 전달된 categoryId가 0이거나 없을 경우 undefined로 변경 처리 @@ -224,10 +249,10 @@ const getFeedList = async ( limit = 10; } - if (!page) { - page = 1; + if (!index) { + index = 1; } - const startIndex: number = (page - 1) * limit; + const startIndex: number = (index - 1) * limit; return await FeedListRepository.getFeedList(categoryId, startIndex, limit); }; diff --git a/src/services/upload.service.ts b/src/services/upload.service.ts index 2c1af38..16201b1 100644 --- a/src/services/upload.service.ts +++ b/src/services/upload.service.ts @@ -1,14 +1,38 @@ import sharp from 'sharp'; -import { - DeleteObjectsCommand, - GetObjectCommand, - PutObjectCommand, -} from '@aws-sdk/client-s3'; +import { DeleteObjectsCommand, PutObjectCommand } from '@aws-sdk/client-s3'; import { s3 } from '../middleware/uploadToS3'; import dataSource from '../repositories/data-source'; import { UploadFiles } from '../entities/uploadFiles.entity'; import crypto from 'crypto'; import { UserRepository } from '../repositories/user.repository'; +import AWS from 'aws-sdk'; + +const lambda = new AWS.Lambda({ + region: process.env.AWS_REGION, +}); + +const invokeLambda = async (param: { Bucket: string; Key: string }) => { + const lambdaParams = { + FunctionName: 'checkFileAccessLambda', + Payload: JSON.stringify(param), + }; + + console.log(param); + console.log(lambdaParams); + + // FIXME : invokeLambda 함수에서 에러가 발생하면, 에러를 잡아서 처리해야 한다.(작동은 하는것 같은데 예외처리가 안된다. 확인 필요) + + return new Promise((resolve, reject) => { + lambda.invoke(lambdaParams, function (err, data: any) { + if (err) { + reject(err); + } else { + console.log('invokeLambda data: ', data); + resolve(JSON.parse(data.Payload)); + } + }); + }); +}; const uploadFiles = async ( userId: number, @@ -105,6 +129,32 @@ const uploadFiles = async ( } return files_link; }; +// uploadFiles 업로드된 파일을 삭제하는 함수 --------------------------------------------------- + +// mySQL에서 file_link를 통해 uploadFile의 ID를 찾는 함수 +const findFile = async (file_link: string) => { + try { + return await dataSource.manager.findOneOrFail(UploadFiles, { + where: { file_link: file_link }, + }); + } catch (err) { + throw { status: 404, message: 'NOT_FOUND_UPLOAD_FILE' }; + } +}; + +// AWS S3에서 파일의 유무를 확인하는 함수 +// const checkFileAccess = async (param: any) => { +// try { +// await s3.send(new GetObjectCommand(param)); +// } catch (err: any) { +// if (err.Code === 'AccessDenied' || err.$metadata.httpStatusCode === 404) { +// throw { +// status: 404, +// message: `DELETE_UPLOADED_FILE_IS_NOT_EXISTS: ${err}`, +// }; +// } +// } +// }; const deleteUploadFile = async ( userId: number, @@ -127,31 +177,6 @@ const deleteUploadFile = async ( (file_link: string) => file_link !== 'DELETE_FROM_UPLOAD_FILES_TABLE' ); - // mySQL에서 file_link를 통해 uploadFile의 ID를 찾는 함수 - const findFile = async (file_link: string) => { - try { - return await dataSource.manager.findOneOrFail(UploadFiles, { - where: { file_link: file_link }, - }); - } catch (err) { - throw { status: 404, message: 'NOT_FOUND_UPLOAD_FILE' }; - } - }; - - // AWS S3에서 파일의 유무를 확인하는 함수 - const checkFileAccess = async (param: any) => { - try { - await s3.send(new GetObjectCommand(param)); - } catch (err: any) { - if (err.Code === 'AccessDenied' || err.$metadata.httpStatusCode === 404) { - throw { - status: 404, - message: `DELETE_UPLOADED_FILE_IS_NOT_EXISTS: ${err}`, - }; - } - } - }; - const deleteFiles = async (newFileLinks: string[], userId: number) => { const findAndCheckPromises = newFileLinks.map(async file_link => { const findFileResult = await findFile(file_link); @@ -161,7 +186,7 @@ const deleteUploadFile = async ( throw { status: 403, message: 'DELETE_UPLOADED_FILE_IS_NOT_YOURS' }; } - const param = { + const param: { Bucket: string; Key: string } = { Bucket: process.env.AWS_S3_BUCKET, Key: findFileResult.file_link.split('.com/')[1], }; @@ -169,15 +194,14 @@ const deleteUploadFile = async ( keyArray.push({ Key: param.Key }); uploadFileIdArray.push(findFileResult.id); - await checkFileAccess(param); + // FIXME 여기가 지울 파일이 많아지면 병목현상?인지 여튼 오래걸리면서 transaction이 잠기는 현상이 발생한다. + // await checkFileAccess(param); + await invokeLambda(param); }); - await Promise.all(findAndCheckPromises); - - // 이후 작업들을 수행하세요. }; - // 이 함수를 호출하여 파일을 삭제하세요: + // 이 함수를 호출하여 파일을 삭제 await deleteFiles(newFileLinks, userId); const params = { @@ -199,7 +223,16 @@ const deleteUploadFile = async ( // file_links에 'DELETE_FROM_UPLOAD_FILES_TABLE'가 포함되어있으면 mySQL 테이블에서도 개체 삭제 if (file_links.includes('DELETE_FROM_UPLOAD_FILES_TABLE')) { // mySQL에서 개체 삭제 - await dataSource.manager.softDelete(UploadFiles, uploadFileIdArray); + const queryRunner = dataSource.createQueryRunner(); + await queryRunner.connect(); + await queryRunner.startTransaction(); + try { + await dataSource.manager.softDelete(UploadFiles, uploadFileIdArray); + await queryRunner.commitTransaction(); + } catch (err) { + await queryRunner.rollbackTransaction(); + throw new Error(`DELETE_UPLOAD_FILE_FAIL: ${err}`); + } } }; diff --git a/src/utils/swagger.ts b/src/utils/swagger.ts index cc3dc19..88e61c8 100644 --- a/src/utils/swagger.ts +++ b/src/utils/swagger.ts @@ -32,7 +32,7 @@ const options = { }, { description: 'project_review AWS RDS Test API document', - url: 'http://15.164.86.242:8005', + url: 'http://3.38.6.179:8000', }, ], },