diff --git a/src/ai/ai-upload.middleware.ts b/src/ai/ai-upload.middleware.ts new file mode 100644 index 0000000..f7b0f7c --- /dev/null +++ b/src/ai/ai-upload.middleware.ts @@ -0,0 +1,16 @@ +import {Request, Response, NextFunction} from 'express'; +import upload from './ai-upload.js'; + +export const uploadMiddleware = ( + req: Request, + res: Response, + next: NextFunction, +) => { + upload.single('base64_image')(req, res, err => { + if (err) { + next(err); + } else { + next(); + } + }); +}; diff --git a/src/app.ts b/src/app.ts index bc534e3..14f746d 100644 --- a/src/app.ts +++ b/src/app.ts @@ -27,9 +27,7 @@ import {authRouter} from './routers/auth.routers.js'; import {userRouter} from './routers/user.router.js'; import {tagRouter} from './routers/tag.router.js'; import {myPageRouter} from './routers/mypage.routers.js'; -import {imageUploader} from './s3/image.uploader.js'; import {trustRouter} from './routers/trust.router.js'; -import upload from './ai/ai-upload.js'; dotenv.config(); @@ -119,10 +117,6 @@ app.use('/tag', tagRouter); app.use('/trust', trustRouter); app.post('/image/ai', labelDetectionController); -app.post('/memo/text-format/folders', upload.single('base64_image')); -app.patch('/memo/text-format/folders/:folderId', upload.single('base64_image')); -app.post('/memo/image-format/folders', imageUploader.single('image')); -app.post('/memo/image-format/folders/:folderId', imageUploader.single('image')); RegisterRoutes(app); app.get('/', (req: Request, res: Response) => { diff --git a/src/controllers/memo-createFolderOCR.Controller.ts b/src/controllers/memo-createFolderOCR.Controller.ts index d1f517f..f94b06a 100644 --- a/src/controllers/memo-createFolderOCR.Controller.ts +++ b/src/controllers/memo-createFolderOCR.Controller.ts @@ -16,12 +16,14 @@ import { Response, Tags, Post, + Middlewares, } from 'tsoa'; import { ITsoaErrorResponse, ITsoaSuccessResponse, TsoaSuccessResponse, } from '../models/tsoaResponse.js'; +import {uploadMiddleware} from '../ai/ai-upload.middleware.js'; @Route('memo') export class MemoCreateUpdateOCRController extends Controller { @@ -34,6 +36,7 @@ export class MemoCreateUpdateOCRController extends Controller { * @returns 성공 시 폴더를 생성하고 텍스트를 저장한 결과를 반환합니다. */ @Post('/text-format/folders') + @Middlewares(uploadMiddleware) @Tags('memo-ai') @Response(StatusCodes.BAD_REQUEST, '잘못된 요청 데이터', { resultType: 'FAIL', diff --git a/src/controllers/memo-updateFolderOCR.Controller.ts b/src/controllers/memo-updateFolderOCR.Controller.ts index fdf2eb5..8e6be04 100644 --- a/src/controllers/memo-updateFolderOCR.Controller.ts +++ b/src/controllers/memo-updateFolderOCR.Controller.ts @@ -20,8 +20,10 @@ import { SuccessResponse, Example, Patch, + Middlewares, } from 'tsoa'; import {StatusCodes} from 'http-status-codes'; +import {uploadMiddleware} from '../ai/ai-upload.middleware.js'; @Route('memo') export class MemoCreateFolderOCRController extends Controller { @@ -35,6 +37,7 @@ export class MemoCreateFolderOCRController extends Controller { * @returns 성공 시 특정 폴더에 텍스트를 저장한 결과를 반환합니다. */ @Patch('/text-format/folders/:folderId') + @Middlewares(uploadMiddleware) @Tags('memo-ai') @Response(StatusCodes.BAD_REQUEST, '잘못된 요청 데이터', { resultType: 'FAIL', diff --git a/src/controllers/trust.controller.ts b/src/controllers/trust.controller.ts index 2de6946..fee1e58 100644 --- a/src/controllers/trust.controller.ts +++ b/src/controllers/trust.controller.ts @@ -1,10 +1,14 @@ import {Request, Response, NextFunction} from 'express'; import * as trustService from '../services/trust.service.js'; -import {SearchNoResultsError,ServerError,DataValidationError} from 'src/errors.js'; +import {SearchNoResultsError, DataValidationError} from 'src/errors.js'; import {StatusCodes} from 'http-status-codes'; -export const handleImageStatus = async (req: Request, res: Response, next:NextFunction): Promise => { - /* +export const handleImageStatus = async ( + req: Request, + res: Response, + next: NextFunction, +): Promise => { + /* #swagger.tags = ['Trust'] #swagger.summary = '휴지통으로 이미지 이동 API' #swagger.description = '선택한 이미지를 휴지통으로 이동시키는 API입니다.' @@ -89,30 +93,34 @@ export const handleImageStatus = async (req: Request, res: Response, next:NextFu } } */ - try { - const {mediaId} = req.body; - const parsedMediaId = parseInt(mediaId); - if (isNaN(parsedMediaId)) { - throw new SearchNoResultsError({searchKeyword: 'mediaId가 올바르지 않습니다'}); - } - const updatedImage = await trustService.deactivateImages(mediaId); - if (!updatedImage) { - throw new SearchNoResultsError({ searchKeyword: '해당 mediaId에 대한 이미지가 존재하지 않습니다' }); - } - const result = { - mediaId: updatedImage.mediaId, - status: updatedImage.status - }; - res.status(StatusCodes.OK).success(result); - } catch (error) { - next(error); + try { + const {mediaId} = req.body; + const parsedMediaId = parseInt(mediaId); + if (isNaN(parsedMediaId)) { + throw new SearchNoResultsError({ + searchKeyword: 'mediaId가 올바르지 않습니다', + }); } + const updatedImage = await trustService.deactivateImages(mediaId); + if (!updatedImage) { + throw new SearchNoResultsError({ + searchKeyword: '해당 mediaId에 대한 이미지가 존재하지 않습니다', + }); + } + const result = { + mediaId: updatedImage.mediaId, + status: updatedImage.status, + }; + res.status(StatusCodes.OK).success(result); + } catch (error) { + next(error); + } }; export const handleImageRestore = async ( - req: Request, - res: Response, - next: NextFunction + req: Request, + res: Response, + next: NextFunction, ): Promise => { /* #swagger.tags = ['Trust'] @@ -202,31 +210,35 @@ export const handleImageRestore = async ( } } */ - try { - const {mediaIds} = req.body; - if (!Array.isArray(mediaIds)) { - throw new SearchNoResultsError({searchKeyword: 'mediaIds가 올바르지 않습니다'}); - } - const restoredImages = await trustService.restoreImages(mediaIds); - if (!restoredImages.length) { - throw new SearchNoResultsError({ searchKeyword: '해당 mediaIds에 대한 이미지가 존재하지 않습니다' }); - } - const result = restoredImages.map(image => ({ - mediaId: image.mediaId, - status: image.status - })); - res.status(StatusCodes.OK).success(result); - } catch (error) { - next(error); + try { + const {mediaIds} = req.body; + if (!Array.isArray(mediaIds)) { + throw new SearchNoResultsError({ + searchKeyword: 'mediaIds가 올바르지 않습니다', + }); + } + const restoredImages = await trustService.restoreImages(mediaIds); + if (!restoredImages.length) { + throw new SearchNoResultsError({ + searchKeyword: '해당 mediaIds에 대한 이미지가 존재하지 않습니다', + }); } + const result = restoredImages.map(image => ({ + mediaId: image.mediaId, + status: image.status, + })); + res.status(StatusCodes.OK).success(result); + } catch (error) { + next(error); + } }; export const handleImageDelete = async ( req: Request, - res: Response, - next: NextFunction + res: Response, + next: NextFunction, ): Promise => { - /* + /* #swagger.tags = ['Trust'] #swagger.summary = '휴지통에서 이미지 삭제 API' #swagger.description = '선택한 이미지를 휴지통에서 삭제하는 API입니다.' @@ -339,18 +351,22 @@ export const handleImageDelete = async ( } } */ - - try { - const { mediaIds } = req.body; - if (!Array.isArray(mediaIds)) { - throw new SearchNoResultsError({searchKeyword: 'mediaIds가 올바르지 않습니다'}); - } - const deleteable = await trustService.deleteImages(mediaIds); - if(!deleteable) { - throw new DataValidationError({reason: '해당 사진이 휴지통에 존재하지 않습니다'}); - } - res.status(StatusCodes.OK).success(deleteable); - } catch (error) { - next(error); + + try { + const {mediaIds} = req.body; + if (!Array.isArray(mediaIds)) { + throw new SearchNoResultsError({ + searchKeyword: 'mediaIds가 올바르지 않습니다', + }); + } + const deleteable = await trustService.deleteImages(mediaIds); + if (!deleteable) { + throw new DataValidationError({ + reason: '해당 사진이 휴지통에 존재하지 않습니다', + }); } -}; \ No newline at end of file + res.status(StatusCodes.OK).success(deleteable); + } catch (error) { + next(error); + } +}; diff --git a/src/controllers/tsoa.memo-folder.controller.ts b/src/controllers/tsoa.memo-folder.controller.ts index 2680553..e85d79e 100644 --- a/src/controllers/tsoa.memo-folder.controller.ts +++ b/src/controllers/tsoa.memo-folder.controller.ts @@ -30,6 +30,7 @@ import { Patch, Example, FormField, + Middlewares, } from 'tsoa'; import { BodyToMemoFolder, @@ -46,6 +47,7 @@ import { } 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'; @Route('memo') export class MemoFolderController extends Controller { @@ -60,6 +62,7 @@ export class MemoFolderController extends Controller { * */ @Post('/image-format/folders') + @Middlewares(ImageUploadMiddleware) @Tags('memo-folder-controller') @Response( StatusCodes.BAD_REQUEST, diff --git a/src/controllers/tsoa.memo-image.controller.ts b/src/controllers/tsoa.memo-image.controller.ts index b8f369c..ef848a0 100644 --- a/src/controllers/tsoa.memo-image.controller.ts +++ b/src/controllers/tsoa.memo-image.controller.ts @@ -18,6 +18,7 @@ import { Tags, Example, Post, + Middlewares, } from 'tsoa'; import { ITsoaErrorResponse, @@ -32,6 +33,7 @@ import { import {StatusCodes} from 'http-status-codes'; import {BodyToMemoImagesToMove} from '../models/memo-image.model.tsoa.js'; import {PhotoDataNotFoundError} from '../errors.js'; +import {ImageUploadMiddleware} from '../s3/image.uploader.middleware.js'; @Route('memo') export class MemoImageController extends Controller { @@ -44,6 +46,7 @@ export class MemoImageController extends Controller { * @returns 성공 시 사진 저장 결과를 반환합니다. */ @Post('/image-format/folders/:folderId') + @Middlewares(ImageUploadMiddleware) @Tags('memo-image-controller') @Response( StatusCodes.BAD_REQUEST, diff --git a/src/s3/image.uploader.middleware.ts b/src/s3/image.uploader.middleware.ts new file mode 100644 index 0000000..4cdfa94 --- /dev/null +++ b/src/s3/image.uploader.middleware.ts @@ -0,0 +1,16 @@ +import {Request, Response, NextFunction} from 'express'; +import {imageUploader} from './image.uploader.js'; +// import {PhotoValidationError} from '../errors.js'; + +export const ImageUploadMiddleware = ( + req: Request, + res: Response, + next: NextFunction, +) => { + imageUploader.single('image')(req, res, err => { + if (err) { + next(err); + } + next(); + }); +}; diff --git a/swagger/openapi.json b/swagger/openapi.json index dda0c0b..addaa34 100644 --- a/swagger/openapi.json +++ b/swagger/openapi.json @@ -104,66 +104,6 @@ } } }, - "/memo/text-format/folders": { - "post": { - "description": "", - "responses": { - "default": { - "description": "" - } - } - } - }, - "/memo/text-format/folders/{folderId}": { - "patch": { - "description": "", - "parameters": [ - { - "name": "folderId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "default": { - "description": "" - } - } - } - }, - "/memo/image-format/folders": { - "post": { - "description": "", - "responses": { - "default": { - "description": "" - } - } - } - }, - "/memo/image-format/folders/{folderId}": { - "post": { - "description": "", - "parameters": [ - { - "name": "folderId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "default": { - "description": "" - } - } - } - }, "/": { "get": { "description": "", @@ -2464,28 +2404,177 @@ }, "/user/mypage/": { "get": { - "description": "", + "tags": [ + "User" + ], + "summary": "사용자 정보 가져오기 API", + "description": "사용자 정보를 가져오는 API입니다.", "responses": { - "default": { - "description": "" + "200": { + "description": "사용자 정보 가져오기 성공", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "resultType": { + "type": "string", + "example": "SUCCESS" + }, + "error": { + "type": "object", + "nullable": true, + "example": null + }, + "success": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "example": 1 + }, + "email": { + "type": "string", + "example": "yein117@naver.com" + }, + "name": { + "type": "string", + "example": "예인" + }, + "goalCount": { + "type": "integer", + "example": 0 + }, + "createdAt": { + "type": "string", + "example": "2025-01-23T12:26:19.188Z" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + } + } + } + } + } + } + } + }, + "401": { + "description": "잘못된 요청", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "resultType": { + "type": "string", + "example": "FAILURE" + }, + "error": { + "type": "object", + "properties": { + "reason": { + "type": "string", + "example": "User_id를 찾을 수 없습니다" + } + } + }, + "success": { + "type": "object", + "nullable": true, + "example": null + } + } + } + } + } } } }, "patch": { - "description": "", + "tags": [ + "User" + ], + "summary": "회원탈퇴 API", + "description": "사용자 상태를 비활성화하는 API입니다.", "responses": { "204": { - "description": "No Content" + "description": "회원탈퇴 성공" + }, + "401": { + "description": "잘못된 요청", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "resultType": { + "type": "string", + "example": "FAILURE" + }, + "error": { + "type": "object", + "properties": { + "reason": { + "type": "string", + "example": "Session ID가 존재하지 않습니다" + } + } + }, + "success": { + "type": "object", + "nullable": true, + "example": null + } + } + } + } + } } } } }, "/user/mypage/logout": { "get": { - "description": "", + "tags": [ + "User" + ], + "summary": "사용자 로그아웃 API", + "description": "로그아웃 API입니다.", "responses": { "204": { - "description": "No Content" + "description": "사용자 로그아웃 성공" + }, + "401": { + "description": "잘못된 요청", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "resultType": { + "type": "string", + "example": "FAILURE" + }, + "error": { + "type": "object", + "properties": { + "reason": { + "type": "string", + "example": "Session ID가 존재하지 않습니다" + } + } + }, + "success": { + "type": "object", + "nullable": true, + "example": null + } + } + } + } + } } } } @@ -2616,22 +2705,92 @@ }, "/trust/active": { "patch": { - "description": "", + "tags": [ + "Trust" + ], + "summary": "휴지통으로 이미지 이동 API", + "description": "선택한 이미지를 휴지통으로 이동시키는 API입니다.", "responses": { - "default": { - "description": "" + "200": { + "description": "휴지통으로 이동 성공", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "resultType": { + "type": "string", + "example": "SUCCESS" + }, + "error": { + "type": "object", + "nullable": true, + "example": null + }, + "success": { + "type": "object", + "properties": { + "mediaId": { + "type": "integer", + "example": 100001 + }, + "status": { + "type": "integer", + "example": 0 + } + } + } + } + } + } + } + }, + "500": { + "description": "해당 mediaId에 대한 데이터를 찾을 수 없음", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "resultType": { + "type": "string", + "example": "FAILURE" + }, + "error": { + "type": "object", + "properties": { + "reason": { + "type": "string", + "example": "해당 mediaId에 대한 이미지가 존재하지 않습니다" + } + } + }, + "success": { + "type": "object", + "nullable": true, + "example": null + } + } + } + } + } } }, "requestBody": { + "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "mediaId": { - "example": "any" + "type": "integer", + "description": "특정 이미지의 mediaId" } - } + }, + "required": [ + "mediaId" + ] } } } @@ -2650,22 +2809,157 @@ }, "/trust/": { "delete": { - "description": "", + "tags": [ + "Trust" + ], + "summary": "휴지통에서 이미지 삭제 API", + "description": "선택한 이미지를 휴지통에서 삭제하는 API입니다.", "responses": { - "default": { - "description": "" + "200": { + "description": "삭제 성공", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "resultType": { + "type": "string", + "example": "SUCCESS" + }, + "error": { + "type": "object", + "nullable": true, + "example": null + }, + "success": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + } + } + } + } + } + } + } + }, + "400": { + "description": "삭제할 이미지가 존재하지 않음", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "resultType": { + "type": "string", + "example": "FAILURE" + }, + "error": { + "type": "object", + "properties": { + "errorCode": { + "type": "string", + "example": "SRH-400" + }, + "reason": { + "type": "string", + "example": "해당 사진이 휴지통에 존재하지 않습니다" + } + } + }, + "success": { + "type": "object", + "nullable": true, + "example": null + } + } + }, + "examples": { + "삭제할 이미지가 존재하지 않음": { + "summary": "삭제할 이미지가 존재하지 않음", + "description": "전달된 imageIds 중 일부 또는 전체가 존재하지 않는 경우 발생합니다.", + "value": { + "resultType": "FAILURE", + "error": { + "errorCode": "SRH-400", + "reason": "해당 사진이 휴지통에 존재하지 않습니다" + }, + "success": null + } + } + } + } + } + }, + "404": { + "description": "잘못된 요청 (imageIds가 올바르지 않음)", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "resultType": { + "type": "string", + "example": "FAILURE" + }, + "error": { + "type": "object", + "properties": { + "errorCode": { + "type": "string", + "example": "SRH-404" + }, + "reason": { + "type": "string", + "example": "imageIds가 올바르지 않습니다" + } + } + }, + "success": { + "type": "object", + "nullable": true, + "example": null + } + } + }, + "examples": { + "유효하지 않은 imageIds": { + "summary": "유효하지 않은 imageIds", + "description": "전송된 imageIds 값이 배열이 아니거나 올바른 숫자가 아닐 때 발생합니다.", + "value": { + "resultType": "FAILURE", + "error": { + "errorCode": "SRH-404", + "reason": "imageIds가 올바르지 않습니다" + }, + "success": null + } + } + } + } + } } }, "requestBody": { + "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { - "mediaIds": { - "example": "any" + "imageIds": { + "type": "array", + "items": { + "type": "integer" + }, + "description": "삭제할 이미지의 imageId 리스트" } - } + }, + "required": [ + "imageIds" + ] } } }