diff --git a/config/tsoa.json b/config/tsoa.json index a0b45f9..19f4458 100644 --- a/config/tsoa.json +++ b/config/tsoa.json @@ -19,27 +19,6 @@ } ], "paths": { - "/memo/image-format/folders": { - "post": { - "requestBody": { - "required": true, - "content": { - "multipart/form-data": { - "schema": { - "type": "object", - "properties": { - "image": { - "type": "string", - "format": "binary", - "description": "파일 업로드" - } - } - } - } - } - } - } - }, "/memo/image-format/folders/{folderId}": { "post": { "requestBody": { diff --git a/src/controllers/tsoa.memo-folder.controller.ts b/src/controllers/tsoa.memo-folder.controller.ts index e85d79e..2068256 100644 --- a/src/controllers/tsoa.memo-folder.controller.ts +++ b/src/controllers/tsoa.memo-folder.controller.ts @@ -7,14 +7,13 @@ import { listMemoFolder, listMemoTextImage, memoFolderCreate, - memoFolderImageCreate, memoFolderUpdate, memoSearch, memoTextUpdate, } from '../services/memo-folder.service.tsoa.js'; import {memoImageDelete} from '../services/memo-image.service.tsoa.js'; import {bodyToMemoImagesToDelete} from '../dtos/memo-image.dto.tsoa.js'; -import {DataValidationError, PhotoValidationError} from '../errors.js'; +import {DataValidationError} from '../errors.js'; import { Response, Body, @@ -29,8 +28,6 @@ import { Path, Patch, Example, - FormField, - Middlewares, } from 'tsoa'; import { BodyToMemoFolder, @@ -38,7 +35,6 @@ import { MemoFolderListResponseDto, MemoFolderResponseDto, MemoTextImageListResponseDto, - MemoFolderImageResponseDto, } from '../models/memo-folder.model.tsoa.js'; import { ITsoaErrorResponse, @@ -46,126 +42,125 @@ import { TsoaSuccessResponse, } from '../models/tsoaResponse.js'; import {BodyToMemoImagesToDelete} from '../models/memo-image.model.tsoa.js'; -import {Request as ExpressRequest, Express} from 'express'; -import {ImageUploadMiddleware} from '../s3/image.uploader.middleware.js'; +import {Request as ExpressRequest} from 'express'; @Route('memo') export class MemoFolderController extends Controller { - /** - * 폴더 생성과 동시에 파일을 저장하는 API입니다. - * - * @summary 폴더 생성 및 사진 저장 API - * @param req - * @param folderName 생성할 폴더 이름 - * @param image 파일 업로드 - * @returns 성공 시 폴더 생성 및 사진 저장 결과를 반환합니다. - * - */ - @Post('/image-format/folders') - @Middlewares(ImageUploadMiddleware) - @Tags('memo-folder-controller') - @Response( - StatusCodes.BAD_REQUEST, - '유효하지 않은 데이터 에러', - { - resultType: 'FAIL', - success: null, - error: { - errorCode: 'FOL-400', - reason: '폴더 생성 중 오류가 발생했습니다.', - data: {userId: '1', folderName: 'string'}, - }, - }, - ) - @Response( - StatusCodes.BAD_REQUEST, - '유효하지 않은 데이터 에러', - { - resultType: 'FAIL', - error: { - errorCode: 'PHO-400', - reason: '사진 데이터가 유효하지 않습니다.', - data: { - reason: '저장할 사진이 없습니다.', - }, - }, - success: null, - }, - ) - @Response( - StatusCodes.BAD_REQUEST, - '유효하지 않은 데이터 에러', - { - resultType: 'FAIL', - error: { - errorCode: 'MEM-400', - reason: '메모 사진 추가 중 오류가 발생했습니다.', - data: { - folderId: '1', - imageUrl: 'string', - }, - }, - success: null, - }, - ) - @Response( - StatusCodes.BAD_REQUEST, - '유효하지 않은 데이터 에러', - { - resultType: 'FAIL', - error: { - errorCode: 'PHO-400', - reason: '사진 데이터가 유효하지 않습니다.', - data: { - extension: 'string', - }, - }, - success: null, - }, - ) - @Response(StatusCodes.CONFLICT, '중복 데이터 에러', { - resultType: 'FAIL', - success: null, - error: { - errorCode: 'FOL-409', - reason: '이미 존재하는 폴더 이름입니다.', - data: {folderName: 'string'}, - }, - }) - @SuccessResponse(StatusCodes.OK, '폴더 생성 및 사진 저장 성공 응답') - @Example({ - resultType: 'SUCCESS', - error: null, - success: { - folderId: '1', - folderName: 'string', - imageId: '1', - imageUrl: 'string', - }, - }) - public async handlerMemoFolderImageAdd( - @Request() req: ExpressRequest, - @FormField() folderName: string, - ): Promise> { - try { - const userId = BigInt(req.user!.id); + // /** + // * 폴더 생성과 동시에 파일을 저장하는 API입니다. + // * + // * @summary 폴더 생성 및 사진 저장 API + // * @param req + // * @param folderName 생성할 폴더 이름 + // * @param image 파일 업로드 + // * @returns 성공 시 폴더 생성 및 사진 저장 결과를 반환합니다. + // * + // */ + // @Post('/image-format/folders') + // @Middlewares(ImageUploadMiddleware) + // @Tags('memo-folder-controller') + // @Response( + // StatusCodes.BAD_REQUEST, + // '유효하지 않은 데이터 에러', + // { + // resultType: 'FAIL', + // success: null, + // error: { + // errorCode: 'FOL-400', + // reason: '폴더 생성 중 오류가 발생했습니다.', + // data: {userId: '1', folderName: 'string'}, + // }, + // }, + // ) + // @Response( + // StatusCodes.BAD_REQUEST, + // '유효하지 않은 데이터 에러', + // { + // resultType: 'FAIL', + // error: { + // errorCode: 'PHO-400', + // reason: '사진 데이터가 유효하지 않습니다.', + // data: { + // reason: '저장할 사진이 없습니다.', + // }, + // }, + // success: null, + // }, + // ) + // @Response( + // StatusCodes.BAD_REQUEST, + // '유효하지 않은 데이터 에러', + // { + // resultType: 'FAIL', + // error: { + // errorCode: 'MEM-400', + // reason: '메모 사진 추가 중 오류가 발생했습니다.', + // data: { + // folderId: '1', + // imageUrl: 'string', + // }, + // }, + // success: null, + // }, + // ) + // @Response( + // StatusCodes.BAD_REQUEST, + // '유효하지 않은 데이터 에러', + // { + // resultType: 'FAIL', + // error: { + // errorCode: 'PHO-400', + // reason: '사진 데이터가 유효하지 않습니다.', + // data: { + // extension: 'string', + // }, + // }, + // success: null, + // }, + // ) + // @Response(StatusCodes.CONFLICT, '중복 데이터 에러', { + // resultType: 'FAIL', + // success: null, + // error: { + // errorCode: 'FOL-409', + // reason: '이미 존재하는 폴더 이름입니다.', + // data: {folderName: 'string'}, + // }, + // }) + // @SuccessResponse(StatusCodes.OK, '폴더 생성 및 사진 저장 성공 응답') + // @Example({ + // resultType: 'SUCCESS', + // error: null, + // success: { + // folderId: '1', + // folderName: 'string', + // imageId: '1', + // imageUrl: 'string', + // }, + // }) + // public async handlerMemoFolderImageAdd( + // @Request() req: ExpressRequest, + // @FormField() folderName: string, + // ): Promise> { + // try { + // const userId = BigInt(req.user!.id); - if (!req.file) { - throw new PhotoValidationError({reason: '저장할 사진이 없습니다.'}); - } - const imageUrl = (req.file as Express.MulterS3File).key; - const folderId = req.uploadDirectory; - const memoFolderImage = await memoFolderImageCreate( - userId, - folderId, - imageUrl, - folderName, - ); - return new TsoaSuccessResponse(memoFolderImage); - } catch (error) { - throw error; - } - } + // if (!req.file) { + // throw new PhotoValidationError({reason: '저장할 사진이 없습니다.'}); + // } + // const imageUrl = (req.file as Express.MulterS3File).key; + // const folderId = req.uploadDirectory; + // const memoFolderImage = await memoFolderImageCreate( + // userId, + // folderId, + // imageUrl, + // folderName, + // ); + // return new TsoaSuccessResponse(memoFolderImage); + // } catch (error) { + // throw error; + // } + // } /** * 폴더를 생성하는 API입니다. diff --git a/src/routers/tsoaRoutes.ts b/src/routers/tsoaRoutes.ts index 26682ff..32999c6 100644 --- a/src/routers/tsoaRoutes.ts +++ b/src/routers/tsoaRoutes.ts @@ -578,37 +578,6 @@ export function RegisterRoutes(app: Router) { } }); // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa - const argsMemoFolderController_handlerMemoFolderImageAdd: Record = { - req: {"in":"request","name":"req","required":true,"dataType":"object"}, - folderName: {"in":"formData","name":"folderName","required":true,"dataType":"string"}, - }; - app.post('/memo/image-format/folders', - ...(fetchMiddlewares(MemoFolderController)), - ...(fetchMiddlewares(MemoFolderController.prototype.handlerMemoFolderImageAdd)), - - async function MemoFolderController_handlerMemoFolderImageAdd(request: ExRequest, response: ExResponse, next: any) { - - // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa - - let validatedArgs: any[] = []; - try { - validatedArgs = templateService.getValidatedArgs({ args: argsMemoFolderController_handlerMemoFolderImageAdd, request, response }); - - const controller = new MemoFolderController(); - - await templateService.apiHandler({ - methodName: 'handlerMemoFolderImageAdd', - controller, - response, - next, - validatedArgs, - successStatus: 200, - }); - } catch (err) { - return next(err); - } - }); - // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa const argsMemoFolderController_handlerMemoFolderAdd: Record = { req: {"in":"request","name":"req","required":true,"dataType":"object"}, body: {"in":"body","name":"body","required":true,"ref":"BodyToMemoFolder"}, diff --git a/src/s3/image.uploader.middleware.ts b/src/s3/image.uploader.middleware.ts index 4cdfa94..dd24efa 100644 --- a/src/s3/image.uploader.middleware.ts +++ b/src/s3/image.uploader.middleware.ts @@ -1,12 +1,30 @@ import {Request, Response, NextFunction} from 'express'; import {imageUploader} from './image.uploader.js'; -// import {PhotoValidationError} from '../errors.js'; - -export const ImageUploadMiddleware = ( +import {getMemoFolder} from 'src/repositories/memo-folder.repository.tsoa.js'; +import {DataValidationError, FolderNotFoundError} from 'src/errors.js'; +export const ImageUploadMiddleware = async ( req: Request, res: Response, next: NextFunction, ) => { + const userId = BigInt(req.user!.id); + if (req.params === null) { + return next( + new DataValidationError({reason: 'folderId가 유효하지 않습니다.'}), + ); + } + try { + const folderId = req.params.folderId; + const checkFolder = await getMemoFolder(BigInt(folderId)); + + if (!checkFolder || checkFolder.userId !== userId) { + return next(new FolderNotFoundError({folderId: BigInt(folderId)})); + } + const uploadDirectory = folderId; + req.uploadDirectory = BigInt(uploadDirectory); + } catch (error) { + return next(error); + } imageUploader.single('image')(req, res, err => { if (err) { next(err); diff --git a/src/s3/image.uploader.ts b/src/s3/image.uploader.ts index e5feffe..78cc357 100644 --- a/src/s3/image.uploader.ts +++ b/src/s3/image.uploader.ts @@ -4,18 +4,8 @@ import {Request} from 'express'; import {v4 as uuidv4} from 'uuid'; // 고유한 식별자(UUID) 생성 import path from 'path'; // 확장자 처리 import process from 'process'; - import {s3} from './awsS3Client.js'; -import { - createMemoFolder, - getMemoFolder, -} from '../repositories/memo-folder.repository.tsoa.js'; -import {bodyToMemoFolder} from '../dtos/memo-folder.dto.tsoa.js'; -import { - FolderDuplicateError, - FolderNotFoundError, - PhotoValidationError, -} from '../errors.js'; +import {PhotoValidationError} from '../errors.js'; const allowedExtensions = [ '.png', @@ -26,6 +16,7 @@ const allowedExtensions = [ '.JPG', '.webp', ]; // 확장자 검사 목록 + export const imageUploader = multer({ // 파일 업로드 미들웨어 설정 storage: multerS3({ @@ -33,46 +24,19 @@ export const imageUploader = multer({ s3: s3, // AWS S3 객체 설정 bucket: process.env.AWS_S3_BUCKET_NAME, // 업로드할 S3 버킷 이름 contentType: multerS3.AUTO_CONTENT_TYPE, // 업로드 파일의 MIME 타입 자동 설정 - key: async (req: Request, file, callback) => { + key: (req: Request, file, callback) => { // S3 버킷에 저장될 경로와 이름 정의 - const userId = req.user!.id; // 사용자 ID - - const uuid = uuidv4(); // UUID 생성 - const extension = path.extname(file.originalname); // 파일 이름(확장자) 추출 + const extension = path.extname(file.originalname); if (!allowedExtensions.includes(extension)) { - // 업로드 파일의 확장자가 허용 목록에 없을 경우 - return callback(new PhotoValidationError({extension: extension})); + throw new PhotoValidationError({extension: extension}); } - // 디렉토리 path 설정 과정 - let uploadDirectory = null; - if (req.body.folderName) { - const createdMemoFolderId = await createMemoFolder( - bodyToMemoFolder(req.body), - userId, - ); - if (createdMemoFolderId === null) { - return callback( - new FolderDuplicateError({folderName: req.body.folderName}), - ); - } - uploadDirectory = createdMemoFolderId; - req.uploadDirectory = uploadDirectory; // 디렉토리 정보 저장 - } else { - const folderId = req.params.folderId; - const checkFolder = await getMemoFolder(BigInt(folderId)); - if (checkFolder === null || checkFolder.userId !== userId) { - return callback( - new FolderNotFoundError({folderId: BigInt(folderId)}), - ); - } - uploadDirectory = folderId; - } + const userId = req.user!.id; // 사용자 ID + const folderId = req.uploadDirectory; + const uuid = uuidv4(); // UUID 생성 + const s3Key = `${userId}/${folderId}/${uuid}_${file.originalname}`; - callback( - null, - `${userId}/${uploadDirectory}/${uuid}_${file.originalname}`, - ); // S3 버킷에서 파일이 저장될 key + callback(null, s3Key); }, acl: 'private', // 비공개 설정 (업로드 파일을 버킷 소유자만 접근 가능) }), diff --git a/swagger/swagger.json b/swagger/swagger.json index eb5f609..aa16958 100644 --- a/swagger/swagger.json +++ b/swagger/swagger.json @@ -2703,159 +2703,6 @@ "parameters": [] } }, - "/memo/image-format/folders": { - "post": { - "operationId": "HandlerMemoFolderImageAdd", - "responses": { - "200": { - "description": "폴더 생성 및 사진 저장 성공 응답", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ITsoaSuccessResponse_MemoFolderImageResponseDto_" - }, - "examples": { - "Example 1": { - "value": { - "resultType": "SUCCESS", - "error": null, - "success": { - "folderId": "1", - "folderName": "string", - "imageId": "1", - "imageUrl": "string" - } - } - } - } - } - } - }, - "400": { - "description": "유효하지 않은 데이터 에러", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ITsoaErrorResponse" - }, - "examples": { - "Example 1": { - "value": { - "resultType": "FAIL", - "success": null, - "error": { - "errorCode": "FOL-400", - "reason": "폴더 생성 중 오류가 발생했습니다.", - "data": { - "userId": "1", - "folderName": "string" - } - } - } - }, - "Example 2": { - "value": { - "resultType": "FAIL", - "error": { - "errorCode": "PHO-400", - "reason": "사진 데이터가 유효하지 않습니다.", - "data": { - "reason": "저장할 사진이 없습니다." - } - }, - "success": null - } - }, - "Example 3": { - "value": { - "resultType": "FAIL", - "error": { - "errorCode": "MEM-400", - "reason": "메모 사진 추가 중 오류가 발생했습니다.", - "data": { - "folderId": "1", - "imageUrl": "string" - } - }, - "success": null - } - }, - "Example 4": { - "value": { - "resultType": "FAIL", - "error": { - "errorCode": "PHO-400", - "reason": "사진 데이터가 유효하지 않습니다.", - "data": { - "extension": "string" - } - }, - "success": null - } - } - } - } - } - }, - "409": { - "description": "중복 데이터 에러", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ITsoaErrorResponse" - }, - "examples": { - "Example 1": { - "value": { - "resultType": "FAIL", - "success": null, - "error": { - "errorCode": "FOL-409", - "reason": "이미 존재하는 폴더 이름입니다.", - "data": { - "folderName": "string" - } - } - } - } - } - } - } - } - }, - "description": "폴더 생성과 동시에 파일을 저장하는 API입니다.", - "summary": "폴더 생성 및 사진 저장 API", - "tags": [ - "memo-folder-controller" - ], - "security": [], - "parameters": [], - "requestBody": { - "required": true, - "content": { - "multipart/form-data": { - "schema": { - "required": [ - "folderName" - ], - "type": "object", - "properties": { - "folderName": { - "type": "string", - "description": "생성할 폴더 이름" - }, - "image": { - "type": "string", - "format": "binary", - "description": "파일 업로드" - } - } - } - } - } - } - } - }, "/memo/image-format/folders/{folderId}": { "post": { "operationId": "HandlerMemoImageAdd",