diff --git a/config/tsoa.json b/config/tsoa.json index 19f4458..4128002 100644 --- a/config/tsoa.json +++ b/config/tsoa.json @@ -49,7 +49,7 @@ "schema": { "type": "object", "properties": { - "base64_image": { + "image": { "type": "string", "format": "binary", "description": "OCR 처리를 위한 이미지 업로드" @@ -70,7 +70,7 @@ "schema": { "type": "object", "properties": { - "base64_image": { + "image": { "type": "string", "format": "binary", "description": "OCR 처리를 위한 이미지 업로드" @@ -81,6 +81,27 @@ } } } + }, + "/tags/ai": { + "post": { + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "required": ["image"], + "type": "object", + "properties": { + "image": { + "type": "string", + "format": "binary", + "description": "태그라벨 자동생성을 위한 이미지 업로드" + } + } + } + } + } + } + } } } }, diff --git a/src/ai/ai-upload.middleware.ts b/src/ai/ai-upload.middleware.ts index f7b0f7c..42de369 100644 --- a/src/ai/ai-upload.middleware.ts +++ b/src/ai/ai-upload.middleware.ts @@ -6,7 +6,7 @@ export const uploadMiddleware = ( res: Response, next: NextFunction, ) => { - upload.single('base64_image')(req, res, err => { + upload.single('image')(req, res, err => { if (err) { next(err); } else { diff --git a/src/app.ts b/src/app.ts index b186e89..8352442 100644 --- a/src/app.ts +++ b/src/app.ts @@ -18,16 +18,10 @@ import {BaseError} from './errors.js'; import {sessionAuthMiddleware} from './auth.config.js'; import cookieParser from 'cookie-parser'; import {ValidateError} from 'tsoa'; -import {labelDetectionController} from './controllers/tags-ai.controller.js'; // routers import {RegisterRoutes} from './routers/tsoaRoutes.js'; 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 {trustRouter} from './routers/trust.router.js'; -import upload from './ai/ai-upload.js'; dotenv.config(); @@ -110,13 +104,6 @@ app.use('/oauth2', authRouter); app.use(sessionAuthMiddleware); // 로그인 후 -app.use('/onboarding', userRouter); -//app.use('/challenge', challengeRouter); -app.use('/user/mypage', myPageRouter); -app.use('/tag', tagRouter); -app.use('/trust', trustRouter); -app.post('/image/ai', upload.single('base64_image'), labelDetectionController); - RegisterRoutes(app); app.get('/', (req: Request, res: Response) => { @@ -147,7 +134,7 @@ const errorHandler: ErrorRequestHandler = (err, req, res, next) => { resultType: 'FAIL', error: { errorCode: 'VAL-001', - reason: 'Validation Error', + reason: err.message, data: err.fields, }, success: null, diff --git a/src/controllers/challenge.controller.ts b/src/controllers/challenge.controller.ts index 1f47d3b..798b1a3 100644 --- a/src/controllers/challenge.controller.ts +++ b/src/controllers/challenge.controller.ts @@ -1,6 +1,6 @@ import {Request as ExpressRequest} from 'express'; -import { - Controller, +import { + Controller, Route, Patch, Tags, @@ -11,7 +11,7 @@ import { Path, Get, Query, - Response + Response, } from 'tsoa'; import { serviceAcceptChallenge, @@ -21,52 +21,49 @@ import { serviceUpdateChallenge, } from '../services/challenge.services.js'; import {StatusCodes} from 'http-status-codes'; +import {getReverseGeocode} from '../utils/challenge.utils.js'; import { - getReverseGeocode -} from '../utils/challenge.utils.js'; -import {ResponseFromChallenge, ResponseFromGetByUserIdReform, ResponseFromUpdateChallenge} from '../models/challenge.entities.js'; + ResponseFromChallenge, + ResponseFromGetByUserIdReform, + ResponseFromUpdateChallenge, +} from '../models/challenge.entities.js'; import {BaseError, DataValidationError, ServerError} from '../errors.js'; -import { ITsoaErrorResponse, ITsoaSuccessResponse, TsoaSuccessResponse } from '../models/tsoaResponse.js'; - +import { + ITsoaErrorResponse, + ITsoaSuccessResponse, + TsoaSuccessResponse, +} from '../models/tsoaResponse.js'; @Route('challenge') -export class ChallengeController extends Controller{ +export class ChallengeController extends Controller { /** * 챌린지의 남은 장수와 목표 장수를 수정하는 API입니다. - * + * * @summary 챌린지 수정 API * @param body 챌린지 id, 남은 장수, 목표 장수 * @returns 챌린지 업데이트 결과 */ @Patch('/update') - @Tags('challenge controller') + @Tags('Challenge') @SuccessResponse(StatusCodes.OK, '챌린지 업데이터 성공 응답') - @Response( - StatusCodes.BAD_REQUEST, - 'Not Found', - { - resultType: 'FAIL', - error: { - errorCode: 'SRH-400', - reason: '업데이트 내용이 없습니다.', - data: null, - }, - success: null, + @Response(StatusCodes.BAD_REQUEST, 'Not Found', { + resultType: 'FAIL', + error: { + errorCode: 'SRH-400', + reason: '업데이트 내용이 없습니다.', + data: null, }, - ) - @Response( - StatusCodes.BAD_REQUEST, - 'Not Found', - { - resultType: 'FAIL', - error: { - errorCode: 'CHL-400', - reason: '챌린지 업데이트 실패', - data: null, - }, - success: null, + success: null, + }) + @Response(StatusCodes.BAD_REQUEST, 'Not Found', { + resultType: 'FAIL', + error: { + errorCode: 'CHL-400', + reason: '챌린지 업데이트 실패', + data: null, }, - ) + success: null, + }) @Response( StatusCodes.INTERNAL_SERVER_ERROR, 'Internal Server Error', @@ -80,69 +77,64 @@ export class ChallengeController extends Controller{ success: null, }, ) - public async handleUpdateChallenge ( + public async handleUpdateChallenge( @Request() req: ExpressRequest, - @Body() body: { - id: string, - required: number, - remaining: number - } + @Body() + body: { + id: string; + required: number; + remaining: number; + }, ): Promise> { - if (!body){ + if (!body) { throw new DataValidationError({reason: '업데이트 내용이 없습니다.'}); } - const result: ResponseFromUpdateChallenge = await serviceUpdateChallenge(body) - .then(r => { - return r; - }) - .catch(err => { - if (!(err instanceof BaseError)) { - throw new ServerError(); - } else { - throw err; - } - }); + const result: ResponseFromUpdateChallenge = await serviceUpdateChallenge( + body, + ) + .then(r => { + return r; + }) + .catch(err => { + if (!(err instanceof BaseError)) { + throw new ServerError(); + } else { + throw err; + } + }); return new TsoaSuccessResponse(result); } /** * 챌린지를 삭제하는 API입니다. - * + * * @summary 챌린지 삭제 API * @param deleteId 삭제할 챌린지 ID * @returns 삭제한 챌린지의 ID */ @Delete('/delete/:id') - @Tags('challenge controller') + @Tags('Challenge') @SuccessResponse(StatusCodes.OK, '챌린지 삭제 성공 응답') - @Response( - StatusCodes.BAD_REQUEST, - 'Not Found', - { - resultType: 'FAIL', - error: { - errorCode: 'SRH-400', - reason: '삭제할 챌린지의 id를 입력해주세요.', - data: null, - }, - success: null, + @Response(StatusCodes.BAD_REQUEST, 'Not Found', { + resultType: 'FAIL', + error: { + errorCode: 'SRH-400', + reason: '삭제할 챌린지의 id를 입력해주세요.', + data: null, }, - ) - @Response( - StatusCodes.BAD_REQUEST, - 'Not Found', - { - resultType: 'FAIL', - error: { - errorCode: 'CHL-400', - reason: '챌린지 삭제 실패', - data: null, - }, - success: null, + success: null, + }) + @Response(StatusCodes.BAD_REQUEST, 'Not Found', { + resultType: 'FAIL', + error: { + errorCode: 'CHL-400', + reason: '챌린지 삭제 실패', + data: null, }, - ) + success: null, + }) @Response( StatusCodes.INTERNAL_SERVER_ERROR, 'Internal Server Error', @@ -158,23 +150,25 @@ export class ChallengeController extends Controller{ ) public async handleRemoveChallenge( @Request() req: ExpressRequest, - @Path('id') deleteId: string - ): Promise>{ - if(!deleteId){ + @Path('id') deleteId: string, + ): Promise> { + if (!deleteId) { throw new DataValidationError({ reason: '삭제할 챌린지의 id를 입력해주세요.', }); } await serviceDeleteChallenge(BigInt(deleteId)) - .then(()=>{return;}) - .catch(err => { - if (!(err instanceof BaseError)) { - throw new ServerError(); - } else { - throw err; - } - }); + .then(() => { + return; + }) + .catch(err => { + if (!(err instanceof BaseError)) { + throw new ServerError(); + } else { + throw err; + } + }); return new TsoaSuccessResponse(`${deleteId} 챌린지 삭제`); } @@ -183,41 +177,33 @@ export class ChallengeController extends Controller{ * 챌린지를 수락하는 API입니다. * parameter에 id값을 적으면 challenge의 status가 2로 변합니다. * 1 = 생성된 챌린지, 2 = 수락된 챌린지, 3 = 완료된 챌린지 - * + * * @summary 챌린지 수락 API - * @param req + * @param req * @param acceptId 수락할 챌린지의 ID * @returns 챌린지 정보 */ @Patch('/accept/:id') - @Tags('challenge controller') + @Tags('Challenge') @SuccessResponse(StatusCodes.OK, '챌린지 수락 성공 응답') - @Response( - StatusCodes.BAD_REQUEST, - 'Not Found', - { - resultType: 'FAIL', - error: { - errorCode: 'SRH-400', - reason: '입력된 id가 없습니다.', - data: null, - }, - success: null, + @Response(StatusCodes.BAD_REQUEST, 'Not Found', { + resultType: 'FAIL', + error: { + errorCode: 'SRH-400', + reason: '입력된 id가 없습니다.', + data: null, }, - ) - @Response( - StatusCodes.BAD_REQUEST, - 'Not Found', - { - resultType: 'FAIL', - error: { - errorCode: 'CHL-400', - reason: '챌린지를 수락할 수 없습니다.', - data: null, - }, - success: null, + success: null, + }) + @Response(StatusCodes.BAD_REQUEST, 'Not Found', { + resultType: 'FAIL', + error: { + errorCode: 'CHL-400', + reason: '챌린지를 수락할 수 없습니다.', + data: null, }, - ) + success: null, + }) @Response( StatusCodes.INTERNAL_SERVER_ERROR, 'Internal Server Error', @@ -233,64 +219,58 @@ export class ChallengeController extends Controller{ ) public async handleAcceptChallenge( @Request() req: ExpressRequest, - @Path('id') acceptId: string - ): Promise>{ - if(!acceptId){ + @Path('id') acceptId: string, + ): Promise> { + if (!acceptId) { throw new DataValidationError({reason: '입력된 id가 없습니다.'}); } - const result: ResponseFromChallenge = await serviceAcceptChallenge(BigInt(acceptId)) - .then(r => { - return r; - }) - .catch(err => { - if (!(err instanceof BaseError)) { - throw new ServerError(); - } else { - throw err; - } - }); + const result: ResponseFromChallenge = await serviceAcceptChallenge( + BigInt(acceptId), + ) + .then(r => { + return r; + }) + .catch(err => { + if (!(err instanceof BaseError)) { + throw new ServerError(); + } else { + throw err; + } + }); return new TsoaSuccessResponse(result); } /** * 챌린지를 완료하는 API입니다. - * + * * @summary 챌린지 완료 API - * @param req + * @param req * @param completeId 완료할 챌린지의 ID * @returns 챌린지 정보 */ @Patch('/complete/:id') - @Tags('challenge controller') + @Tags('Challenge') @SuccessResponse(StatusCodes.OK, '챌린지 완료 성공 응답') - @Response( - StatusCodes.BAD_REQUEST, - 'Not Found', - { - resultType: 'FAIL', - error: { - errorCode: 'SRH-400', - reason: '입력된 id가 없습니다.', - data: null, - }, - success: null, + @Response(StatusCodes.BAD_REQUEST, 'Not Found', { + resultType: 'FAIL', + error: { + errorCode: 'SRH-400', + reason: '입력된 id가 없습니다.', + data: null, }, - ) - @Response( - StatusCodes.BAD_REQUEST, - 'Not Found', - { - resultType: 'FAIL', - error: { - errorCode: 'CHL-400', - reason: '해당 챌린지를 완료할 수 없습니다.', - data: null, - }, - success: null, + success: null, + }) + @Response(StatusCodes.BAD_REQUEST, 'Not Found', { + resultType: 'FAIL', + error: { + errorCode: 'CHL-400', + reason: '해당 챌린지를 완료할 수 없습니다.', + data: null, }, - ) + success: null, + }) @Response( StatusCodes.INTERNAL_SERVER_ERROR, 'Internal Server Error', @@ -306,63 +286,57 @@ export class ChallengeController extends Controller{ ) public async handleCompleteChallenge( @Request() req: ExpressRequest, - @Path('id') completeId: string - ): Promise>{ - if(!completeId){ + @Path('id') completeId: string, + ): Promise> { + if (!completeId) { throw new DataValidationError({reason: '입력된 id가 없습니다.'}); } - const result: ResponseFromChallenge = await serviceCompleteChallenge(BigInt(completeId)) - .then(r => { - return r; - }) - .catch(err => { - if (!(err instanceof BaseError)) { - throw new ServerError(); - } else { - throw err; - } - }); + const result: ResponseFromChallenge = await serviceCompleteChallenge( + BigInt(completeId), + ) + .then(r => { + return r; + }) + .catch(err => { + if (!(err instanceof BaseError)) { + throw new ServerError(); + } else { + throw err; + } + }); return new TsoaSuccessResponse(result); } /** * 로그인 되어 있는 유저의 모든 챌린지들을 불러옵니다. - * + * * @summary 유저 챌린지 조회 - * @param req + * @param req * @returns 챌린지 정보 */ @Get('/get') - @Tags('challenge controller') + @Tags('Challenge') @SuccessResponse(StatusCodes.OK, '유저 챌린지 조회 성공') - @Response( - StatusCodes.BAD_REQUEST, - 'Not Found', - { - resultType: 'FAIL', - error: { - errorCode: 'SRH-400', - reason: 'req.user 정보가 없습니다.', - data: null, - }, - success: null, + @Response(StatusCodes.BAD_REQUEST, 'Not Found', { + resultType: 'FAIL', + error: { + errorCode: 'SRH-400', + reason: 'req.user 정보가 없습니다.', + data: null, }, - ) - @Response( - StatusCodes.BAD_REQUEST, - 'Not Found', - { - resultType: 'FAIL', - error: { - errorCode: 'CHL-400', - reason: '해당 유저의 챌린지를 찾을 수 없습니다.', - data: null, - }, - success: null, + success: null, + }) + @Response(StatusCodes.BAD_REQUEST, 'Not Found', { + resultType: 'FAIL', + error: { + errorCode: 'CHL-400', + reason: '해당 유저의 챌린지를 찾을 수 없습니다.', + data: null, }, - ) + success: null, + }) @Response( StatusCodes.INTERNAL_SERVER_ERROR, 'Internal Server Error', @@ -377,63 +351,57 @@ export class ChallengeController extends Controller{ }, ) public async handleGetByUserId( - @Request() req: ExpressRequest - ): Promise>{ - if(!req.user){ + @Request() req: ExpressRequest, + ): Promise> { + if (!req.user) { throw new DataValidationError({reason: 'req.user 정보가 없습니다.'}); } - const result: ResponseFromGetByUserIdReform[] = await serviceGetByUserId(req.user.id) - .then(r => { - return r; - }) - .catch(err => { - if (!(err instanceof BaseError)) { - throw new ServerError(); - } else { - throw err; - } - }); + const result: ResponseFromGetByUserIdReform[] = await serviceGetByUserId( + req.user.id, + ) + .then(r => { + return r; + }) + .catch(err => { + if (!(err instanceof BaseError)) { + throw new ServerError(); + } else { + throw err; + } + }); return new TsoaSuccessResponse(result); } /** * 지오해쉬 값을 입력하면 주소로 변환해줍니다. - * + * * @summary 주소 변환 API * @param hashedLocation 해쉬된 위치 정보 * @returns 주소 정보 */ @Get('getGeoCode') - @Tags('challenge controller') + @Tags('Challenge') @SuccessResponse(StatusCodes.OK, '네이버 API 성공 응답') - @Response( - StatusCodes.BAD_REQUEST, - 'Not Found', - { - resultType: 'FAIL', - error: { - errorCode: 'SRH-400', - reason: 'query문이 비었습니다. hashedLocation을 입력하세요.', - data: null, - }, - success: null, + @Response(StatusCodes.BAD_REQUEST, 'Not Found', { + resultType: 'FAIL', + error: { + errorCode: 'SRH-400', + reason: 'query문이 비었습니다. hashedLocation을 입력하세요.', + data: null, }, - ) - @Response( - StatusCodes.BAD_REQUEST, - 'Not Found', - { - resultType: 'FAIL', - error: { - errorCode: 'CHL-500', - reason: '네이버 API 호출에 문제가 있습니다.', - data: null, - }, - success: null, + success: null, + }) + @Response(StatusCodes.BAD_REQUEST, 'Not Found', { + resultType: 'FAIL', + error: { + errorCode: 'CHL-500', + reason: '네이버 API 호출에 문제가 있습니다.', + data: null, }, - ) + success: null, + }) @Response( StatusCodes.INTERNAL_SERVER_ERROR, 'Internal Server Error', @@ -448,24 +416,26 @@ export class ChallengeController extends Controller{ }, ) public async naverController( - @Query() hashedLocation: string - ): Promise>{ - if(!hashedLocation){ - throw new DataValidationError({reason: 'query문이 비었습니다. hashedLocation을 입력하세요.'}); + @Query() hashedLocation: string, + ): Promise> { + if (!hashedLocation) { + throw new DataValidationError({ + reason: 'query문이 비었습니다. hashedLocation을 입력하세요.', + }); } const result: string = await getReverseGeocode(hashedLocation) - .then(r => { - return r; - }) - .catch(err => { - if (!(err instanceof BaseError)) { - throw new ServerError(); - } else { - throw err; - } - }); + .then(r => { + return r; + }) + .catch(err => { + if (!(err instanceof BaseError)) { + throw new ServerError(); + } else { + throw err; + } + }); return new TsoaSuccessResponse(result); } -} \ No newline at end of file +} diff --git a/src/controllers/challenge.location.controller.ts b/src/controllers/challenge.location.controller.ts index 2ea0479..dcb9f7a 100644 --- a/src/controllers/challenge.location.controller.ts +++ b/src/controllers/challenge.location.controller.ts @@ -13,7 +13,7 @@ import { } from '../models/challenge.entities.js'; import {bodyToLocationCreation} from '../dtos/challenge.dtos.js'; import {BaseError, DataValidationError, ServerError} from '../errors.js'; -import { +import { Controller, Post, Route, @@ -23,49 +23,45 @@ import { Body, Get, Path, - Response + Response, } from 'tsoa'; -import { ITsoaErrorResponse, ITsoaSuccessResponse, TsoaSuccessResponse } from '../models/tsoaResponse.js'; +import { + ITsoaErrorResponse, + ITsoaSuccessResponse, + TsoaSuccessResponse, +} from '../models/tsoaResponse.js'; @Route('challenge') -export class LocationController extends Controller{ +export class LocationController extends Controller { /** * 위치 챌린지를 생성하는 API 입니다. - * + * * @summary 위치 챌린지 생성 API - * @param req + * @param req * @param body 챌린지 생성에 필요한 정보 * @returns 챌린지 정보 */ @Post('/location_challenge/create') - @Tags('location challenge controller') + @Tags('Location-challenge') @SuccessResponse(StatusCodes.OK, '위치 챌린지 성공 응답') - @Response( - StatusCodes.BAD_REQUEST, - 'Not Found', - { - resultType: 'FAIL', - error: { - errorCode: 'SRH-400', - reason: '입력 데이터가 유효하지 않습니다.', - data: null, - }, - success: null, + @Response(StatusCodes.BAD_REQUEST, 'Not Found', { + resultType: 'FAIL', + error: { + errorCode: 'SRH-400', + reason: '입력 데이터가 유효하지 않습니다.', + data: null, }, - ) - @Response( - StatusCodes.BAD_REQUEST, - 'Not Found', - { - resultType: 'FAIL', - error: { - errorCode: 'CHL-400', - reason: '위치 기반 챌린지 생성 중 오류가 발생했습니다.', - data: null, - }, - success: null, + success: null, + }) + @Response(StatusCodes.BAD_REQUEST, 'Not Found', { + resultType: 'FAIL', + error: { + errorCode: 'CHL-400', + reason: '위치 기반 챌린지 생성 중 오류가 발생했습니다.', + data: null, }, - ) + success: null, + }) @Response( StatusCodes.INTERNAL_SERVER_ERROR, 'Internal Server Error', @@ -81,69 +77,68 @@ export class LocationController extends Controller{ ) public async handleNewLocationChallenge( @Request() req: ExpressRequest, - @Body() body: { - context: string, - location: string, - required: number - } - ): Promise>{ - if(!req.user){ - throw new DataValidationError({reason: '유저 정보가 없습니다. 다시 로그인 해주세요.'}); + @Body() + body: { + context: string; + location: string; + required: number; + }, + ): Promise> { + if (!req.user) { + throw new DataValidationError({ + reason: '유저 정보가 없습니다. 다시 로그인 해주세요.', + }); } - const data: LocationChallengeCreation = bodyToLocationCreation(body, req.user.id); - const result: ResponseFromChallenge = await serviceCreateNewLocationChallenge(data) - .then(r => { - return r; - }) - .catch(err => { - if (!(err instanceof BaseError)) { - throw new ServerError(); - } else { - throw err; - } - }); + const data: LocationChallengeCreation = bodyToLocationCreation( + body, + req.user.id, + ); + const result: ResponseFromChallenge = + await serviceCreateNewLocationChallenge(data) + .then(r => { + return r; + }) + .catch(err => { + if (!(err instanceof BaseError)) { + throw new ServerError(); + } else { + throw err; + } + }); return new TsoaSuccessResponse(result); } /** * 위치 챌린지를 id로 조회하는 API입니다. - * + * * @summary 위치 챌린지 조회 API - * @param req + * @param req * @param challengeId 챌린지 ID * @returns 챌린지 정보 */ @Get('/location_challenge/get/:id') - @Tags('location challenge controller') + @Tags('Location-challenge') @SuccessResponse(StatusCodes.OK, '위치 챌린지 조회 성공 응답') - @Response( - StatusCodes.BAD_REQUEST, - 'Not Found', - { - resultType: 'FAIL', - error: { - errorCode: 'SRH-400', - reason: '입력 데이터가 유효하지 않습니다.', - data: null, - }, - success: null, + @Response(StatusCodes.BAD_REQUEST, 'Not Found', { + resultType: 'FAIL', + error: { + errorCode: 'SRH-400', + reason: '입력 데이터가 유효하지 않습니다.', + data: null, }, - ) - @Response( - StatusCodes.BAD_REQUEST, - 'Not Found', - { - resultType: 'FAIL', - error: { - errorCode: 'CHL-404', - reason: '해당 위치 기반 챌린지를 찾을 수 없습니다.', - data: null, - }, - success: null, + success: null, + }) + @Response(StatusCodes.BAD_REQUEST, 'Not Found', { + resultType: 'FAIL', + error: { + errorCode: 'CHL-404', + reason: '해당 위치 기반 챌린지를 찾을 수 없습니다.', + data: null, }, - ) + success: null, + }) @Response( StatusCodes.INTERNAL_SERVER_ERROR, 'Internal Server Error', @@ -159,44 +154,42 @@ export class LocationController extends Controller{ ) public async handleGetLocationChallenge( @Request() req: ExpressRequest, - @Path('id') challengeId: string - ): Promise>{ - if(!challengeId){ + @Path('id') challengeId: string, + ): Promise> { + if (!challengeId) { throw new DataValidationError({ reason: '올바른 parameter 값이 필요합니다.', }); } - const result: ResponseFromLocationChallenge = await serviceGetLocationChallenge(BigInt(challengeId)) - .then(r => { - return r; - }) - .catch(err => { - if (!(err instanceof BaseError)) { - throw new ServerError(); - } else { - throw err; - } - }); + const result: ResponseFromLocationChallenge = + await serviceGetLocationChallenge(BigInt(challengeId)) + .then(r => { + return r; + }) + .catch(err => { + if (!(err instanceof BaseError)) { + throw new ServerError(); + } else { + throw err; + } + }); return new TsoaSuccessResponse(result); } /** * 사진들의 위치 챌린지 조건을 판별하는 로직입니다. - * + * * @summary 위치 챌린지 판별 API - * @param req + * @param req * @param body 사진 데이터 정보 * @returns 사진 데이터 정보 */ - @Post('/location_logic/test') - @Tags('location challenge controller') - @SuccessResponse(StatusCodes.OK, '위치 챌린지 판별 응답') - @Response( - StatusCodes.BAD_REQUEST, - 'Not Found', - { + @Post('/location_logic/test') + @Tags('Location-challenge') + @SuccessResponse(StatusCodes.OK, '위치 챌린지 판별 응답') + @Response(StatusCodes.BAD_REQUEST, 'Not Found', { resultType: 'FAIL', error: { errorCode: 'SRH-400', @@ -204,12 +197,8 @@ export class LocationController extends Controller{ data: null, }, success: null, - }, -) -@Response( - StatusCodes.BAD_REQUEST, - 'Not Found', - { + }) + @Response(StatusCodes.BAD_REQUEST, 'Not Found', { resultType: 'FAIL', error: { errorCode: 'PHO-404', @@ -217,12 +206,8 @@ export class LocationController extends Controller{ data: null, }, success: null, - }, -) -@Response( - StatusCodes.BAD_REQUEST, - 'Not Found', - { + }) + @Response(StatusCodes.BAD_REQUEST, 'Not Found', { resultType: 'FAIL', error: { errorCode: 'PHO-400', @@ -230,47 +215,47 @@ export class LocationController extends Controller{ data: null, }, success: null, - }, -) -@Response( - StatusCodes.INTERNAL_SERVER_ERROR, - 'Internal Server Error', - { - resultType: 'FAIL', - error: { - errorCode: 'SER-001', - reason: '내부 서버 오류입니다.', - data: null, - }, - success: null, - }, -) - public async handleLocationLogic( - @Request() req: ExpressRequest, - @Body() body: { - id: string, - displayName: string, - longitude: number, - latitude: number, - timestamp: Date - }[] - ): Promise>{ - if(!body){ - throw new DataValidationError({reason: '사진 데이터가 없습니다.'}); - } - - const result: PhotoInfo[] = await serviceLocationLogic(body) - .then(r => { - return r; }) - .catch(err => { - if (!(err instanceof BaseError)) { - throw new ServerError(); - } else { - throw err; + @Response( + StatusCodes.INTERNAL_SERVER_ERROR, + 'Internal Server Error', + { + resultType: 'FAIL', + error: { + errorCode: 'SER-001', + reason: '내부 서버 오류입니다.', + data: null, + }, + success: null, + }, + ) + public async handleLocationLogic( + @Request() req: ExpressRequest, + @Body() + body: { + id: string; + displayName: string; + longitude: number; + latitude: number; + timestamp: Date; + }[], + ): Promise> { + if (!body) { + throw new DataValidationError({reason: '사진 데이터가 없습니다.'}); } - }); - return new TsoaSuccessResponse(result); - } -} \ No newline at end of file + const result: PhotoInfo[] = await serviceLocationLogic(body) + .then(r => { + return r; + }) + .catch(err => { + if (!(err instanceof BaseError)) { + throw new ServerError(); + } else { + throw err; + } + }); + + return new TsoaSuccessResponse(result); + } +} diff --git a/src/controllers/challenge.weekly.controller.ts b/src/controllers/challenge.weekly.controller.ts index b5d02e9..425e5c8 100644 --- a/src/controllers/challenge.weekly.controller.ts +++ b/src/controllers/challenge.weekly.controller.ts @@ -10,7 +10,7 @@ import { serviceGetWeeklyChallenge, } from '../services/challenge.weekly.services.js'; import {BaseError, DataValidationError, ServerError} from '../errors.js'; -import { +import { Controller, Route, SuccessResponse, @@ -20,49 +20,45 @@ import { Body, Get, Path, - Response + Response, } from 'tsoa'; -import { ITsoaErrorResponse, ITsoaSuccessResponse, TsoaSuccessResponse } from '../models/tsoaResponse.js'; +import { + ITsoaErrorResponse, + ITsoaSuccessResponse, + TsoaSuccessResponse, +} from '../models/tsoaResponse.js'; @Route('challenge') -export class DateChallengeController extends Controller{ +export class DateChallengeController extends Controller { /** * 날짜 챌린지를 생성하는 API 입니다. - * + * * @summary 날짜 챌린지 생성 API - * @param req + * @param req * @param body 챌린지 생성 정보 * @returns 챌린지 정보 */ @Post('/weekly_challenge/create') - @Tags('date challenge controller') + @Tags('Date-challenge') @SuccessResponse(StatusCodes.OK, '날짜 챌린지 생성 성공 응답') - @Response( - StatusCodes.BAD_REQUEST, - 'Not Found', - { - resultType: 'FAIL', - error: { - errorCode: 'SRH-400', - reason: '입력 데이터가 유효하지 않습니다.', - data: null, - }, - success: null, + @Response(StatusCodes.BAD_REQUEST, 'Not Found', { + resultType: 'FAIL', + error: { + errorCode: 'SRH-400', + reason: '입력 데이터가 유효하지 않습니다.', + data: null, }, - ) - @Response( - StatusCodes.BAD_REQUEST, - 'Not Found', - { - resultType: 'FAIL', - error: { - errorCode: 'CHL-400', - reason: '날짜 기반 챌린지 생성 중 오류가 발생했습니다.', - data: null, - }, - success: null, + success: null, + }) + @Response(StatusCodes.BAD_REQUEST, 'Not Found', { + resultType: 'FAIL', + error: { + errorCode: 'CHL-400', + reason: '날짜 기반 챌린지 생성 중 오류가 발생했습니다.', + data: null, }, - ) + success: null, + }) @Response( StatusCodes.INTERNAL_SERVER_ERROR, 'Internal Server Error', @@ -78,14 +74,17 @@ export class DateChallengeController extends Controller{ ) public async handleNewWeeklyChallenge( @Request() req: ExpressRequest, - @Body() body: { + @Body() + body: { context: string; challengeDate: Date; required: number; - } - ): Promise>{ - if(!req.user){ - throw new DataValidationError({reason: '유저 정보가 없습니다. 다시 로그인 해주세요.'}); + }, + ): Promise> { + if (!req.user) { + throw new DataValidationError({ + reason: '유저 정보가 없습니다. 다시 로그인 해주세요.', + }); } if (!req.body) { @@ -94,59 +93,56 @@ export class DateChallengeController extends Controller{ }); } - const data: WeeklyChallengeCreation = bodyToWeeklyCreation(body, req.user.id); - const result: ResponseFromChallenge = await serviceCreateNewWeeklyChallenge(data) - .then(r => { - return r; - }) - .catch(err => { - if (!(err instanceof BaseError)) { - throw new ServerError(); - } else { - throw err; - } - }); + const data: WeeklyChallengeCreation = bodyToWeeklyCreation( + body, + req.user.id, + ); + const result: ResponseFromChallenge = await serviceCreateNewWeeklyChallenge( + data, + ) + .then(r => { + return r; + }) + .catch(err => { + if (!(err instanceof BaseError)) { + throw new ServerError(); + } else { + throw err; + } + }); return new TsoaSuccessResponse(result); } /** * 날짜 챌린지 조회 API입니다. - * + * * @summary 날짜 챌린지 조회 API - * @param req + * @param req * @param challengeId 챌린지 ID * @returns 챌린지 정보 */ @Get('/weekly_challenge/get/:id') - @Tags('date challenge controller') + @Tags('Date-challenge') @SuccessResponse(StatusCodes.OK, '날짜 챌린지 조회 성공 응답') - @Response( - StatusCodes.BAD_REQUEST, - 'Not Found', - { - resultType: 'FAIL', - error: { - errorCode: 'SRH-400', - reason: '입력 데이터가 유효하지 않습니다.', - data: null, - }, - success: null, + @Response(StatusCodes.BAD_REQUEST, 'Not Found', { + resultType: 'FAIL', + error: { + errorCode: 'SRH-400', + reason: '입력 데이터가 유효하지 않습니다.', + data: null, }, - ) - @Response( - StatusCodes.BAD_REQUEST, - 'Not Found', - { - resultType: 'FAIL', - error: { - errorCode: 'CHL-400', - reason: '해당 날짜 기반 챌린지를 찾을 수 없습니다.', - data: null, - }, - success: null, + success: null, + }) + @Response(StatusCodes.BAD_REQUEST, 'Not Found', { + resultType: 'FAIL', + error: { + errorCode: 'CHL-400', + reason: '해당 날짜 기반 챌린지를 찾을 수 없습니다.', + data: null, }, - ) + success: null, + }) @Response( StatusCodes.INTERNAL_SERVER_ERROR, 'Internal Server Error', @@ -162,26 +158,28 @@ export class DateChallengeController extends Controller{ ) public async handleGetWeeklyChallenge( @Request() req: ExpressRequest, - @Path('id') challengeId: string - ): Promise>{ + @Path('id') challengeId: string, + ): Promise> { if (!req.params.id) { throw new DataValidationError({ reason: '올바른 parameter값이 필요합니다.', }); } - const result: ResponseFromChallenge = await serviceGetWeeklyChallenge(BigInt(challengeId)) - .then(r => { - return r; - }) - .catch(err => { - if (!(err instanceof BaseError)) { - throw new ServerError(); - } else { - throw err; - } - }); + const result: ResponseFromChallenge = await serviceGetWeeklyChallenge( + BigInt(challengeId), + ) + .then(r => { + return r; + }) + .catch(err => { + if (!(err instanceof BaseError)) { + throw new ServerError(); + } else { + throw err; + } + }); return new TsoaSuccessResponse(result); } -} \ No newline at end of file +} diff --git a/src/controllers/memo-createFolderOCR.controller.ts b/src/controllers/memo-createFolderOCR.controller.ts index f94b06a..d412d98 100644 --- a/src/controllers/memo-createFolderOCR.controller.ts +++ b/src/controllers/memo-createFolderOCR.controller.ts @@ -37,7 +37,7 @@ export class MemoCreateUpdateOCRController extends Controller { */ @Post('/text-format/folders') @Middlewares(uploadMiddleware) - @Tags('memo-ai') + @Tags('Memo-AI') @Response(StatusCodes.BAD_REQUEST, '잘못된 요청 데이터', { resultType: 'FAIL', success: null, diff --git a/src/controllers/memo-updateFolderOCR.controller.ts b/src/controllers/memo-updateFolderOCR.controller.ts index 8e6be04..2116379 100644 --- a/src/controllers/memo-updateFolderOCR.controller.ts +++ b/src/controllers/memo-updateFolderOCR.controller.ts @@ -38,7 +38,7 @@ export class MemoCreateFolderOCRController extends Controller { */ @Patch('/text-format/folders/:folderId') @Middlewares(uploadMiddleware) - @Tags('memo-ai') + @Tags('Memo-AI') @Response(StatusCodes.BAD_REQUEST, '잘못된 요청 데이터', { resultType: 'FAIL', success: null, diff --git a/src/controllers/tag.controller.ts b/src/controllers/tag.controller.ts deleted file mode 100644 index 15e7673..0000000 --- a/src/controllers/tag.controller.ts +++ /dev/null @@ -1,83 +0,0 @@ -import {Request, Response} from 'express'; -import {StatusCodes} from 'http-status-codes'; -import {bodyToTag} from '../dtos/tag.dto.js'; -import {tagCreate} from '../services/tag.service.js'; - -async function handleNewTag(req: Request, res: Response): Promise { - /* - #swagger.tags = ['tag-controller'] - #swagger.summary = '태그 생성 API'; - #swagger.description = '태그 생성 API입니다. 중복 이름은 허용하지 않습니다. 카레고리 ID가 없을 경우 에러를 반환합니다.' - #swagger.requestBody = { - required: true, - content: { - "multipart/form-data": { - schema: { - type: "object", - required: ['content', 'tag_category_id'], - properties: { - content: { type: "string", description: "태그 이름" }, - tag_category_id: { type: "integer", description: "태그 카테고리 ID" , example: 1} - } - } - } - } - }; - #swagger.responses[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: "string", example: "1" }, - content: { type: "string", example: "스위픽 단체사진" }, - createdAt: { type: "string", example: "2021-08-31T07:00:00.000Z" }, - updatedAt: { type: "string", example: "2021-08-31T07:00:00.000Z" }, - status: { type: "number", example: 1 }, - tagCategoryId: { type: "string", example: "1" } - } - } - } - } - } - } - }; - #swagger.responses[400] = { - description: "태그 저장 실패 응답", - content: { - "application/json": { - schema: { - type: "object", - properties: { - resultType: { type: "string", example: "FAIL" }, - error: { - type: "object", - properties: { - errorCode: { type: "string", example: "U400" }, - reason: { type: "string", example: "태그 중복 확인 에러" }, - data: { - type: "object", - example: null - } - } - }, - success: { type: "object", nullable: true, example: null } - } - } - } - } - } - }; - */ - - const newImageTag = await tagCreate(bodyToTag(req.body)); - res.status(StatusCodes.OK).success(newImageTag); -} - -export {handleNewTag}; diff --git a/src/controllers/tags-ai.controller.ts b/src/controllers/tags-ai.controller.ts deleted file mode 100644 index 062f499..0000000 --- a/src/controllers/tags-ai.controller.ts +++ /dev/null @@ -1,113 +0,0 @@ -import {NextFunction, Request, Response} from 'express'; -import {detectLabels} from '../services/tags-ai.service.js'; -import {StatusCodes} from 'http-status-codes'; -import {PhotoDataNotFoundError, PhotoValidationError} from '../errors.js'; - -export const labelDetectionController = async ( - req: Request, - res: Response, - next: NextFunction, -): Promise => { - /* - #swagger.tags = ['label-detection'] - #swagger.summary = '이미지 라벨링' - #swagger.description = 'Base64 데이터를 JSON으로 받아 이미지를 분석하여 상위 3개의 라벨과 정확도를 반환합니다.' - #swagger.requestBody = { - required: true, - content: { - "multipart/form-data": { - schema: { - type: "object", - properties: { - base64_image: { - type: "string", - format: "binary", - description: "업로드할 이미지 파일" - } - } - } - } - } - } - #swagger.responses[200] = { - description: "라벨링 결과 반환", - content: { - "application/json": { - schema: { - type: "object", - properties: { - topLabels: { - type: "array", - items: { - type: "object", - properties: { - description: { type: "string", example: "Mountain" }, - score: { type: "number", example: 0.95 } - } - } - } - } - } - } - } - } - #swagger.responses[400] = { - description: "잘못된 요청 데이터", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { type: "string", example: "Base64 이미지 데이터가 제공되지 않았습니다." } - } - } - } - } - } - #swagger.responses[500] = { - description: "서버 내부 오류", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { type: "string", example: "라벨링 중 오류가 발생했습니다." } - } - } - } - } - } - */ - - try { - // Base64 이미지 데이터가 요청에 포함되었는지 확인 - if (!req.file) { - throw new PhotoDataNotFoundError({ - reason: '라벨링을 추출 할 사진이 없습니다', - }); - } - - const base64_image = req.file.buffer.toString('base64'); - - if (!isValidBase64(base64_image)) { - throw new PhotoValidationError({ - reason: '올바른 Base64 이미지 형식이 아닙니다.', - }); - } - - //서비스 호출 - const labels = await detectLabels(base64_image); - - // 라벨 반환 - res.status(StatusCodes.OK).json({topLabels: labels}); - } catch (error) { - next(error); - } -}; - -const isValidBase64 = (base64String: string): boolean => { - // base64 문자열 검증 함수 - const base64Regex = - /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/; - return base64Regex.test(base64String); -}; diff --git a/src/controllers/trash.controller.ts b/src/controllers/trash.controller.ts new file mode 100644 index 0000000..9e516a6 --- /dev/null +++ b/src/controllers/trash.controller.ts @@ -0,0 +1,211 @@ +import {Request as ExpressRequest} from 'express'; +import { + deactivateImage, + deleteImages, + restoreImages, +} from '../services/trash.service.js'; +import {BaseError, ServerError} from 'src/errors.js'; +import {StatusCodes} from 'http-status-codes'; +import { + Body, + Controller, + Delete, + Patch, + Path, + Request, + Response, + Route, + SuccessResponse, + Tags, +} from 'tsoa'; +import { + ITsoaErrorResponse, + ITsoaSuccessResponse, + TsoaSuccessResponse, +} from '../models/tsoaResponse.js'; +import {DeleteImagesDTO, RestoreImagesDTO} from '../dtos/trash.dto.js'; + +@Route('trash') +export class TrustController extends Controller { + /** + * 휴지통에 넣고싶은 imageId를 입력하시면 해당 이미지는 비활성화 됩니다. + * + * @summary 휴지통에 이미지 넣기 + * @param imageId 버릴 이미지ID + * @returns 성공문구 + */ + @Patch('/images/{imageId}') + @Tags('Trash') + @SuccessResponse(201, 'CREATED') + @Response( + StatusCodes.INTERNAL_SERVER_ERROR, + 'Internal Server Error', + { + resultType: 'FAIL', + error: { + errorCode: 'DB-001', + reason: 'DB 에러 입니다.', + data: {reason: '에러원인 메시지'}, + }, + success: null, + }, + ) + @Response( + StatusCodes.INTERNAL_SERVER_ERROR, + 'Internal Server Error', + { + resultType: 'FAIL', + error: { + errorCode: 'SER-001', + reason: '내부 서버 오류입니다.', + data: {reason: '에러원인 메시지'}, + }, + success: null, + }, + ) + public async deactivateImage( + @Path() imageId: string, + ): Promise> { + const imageIdBigint = BigInt(imageId); + await deactivateImage(imageIdBigint).catch(err => { + if (err instanceof BaseError) { + throw err; + } + throw new ServerError({reason: err.message}); + }); + return new TsoaSuccessResponse( + `${imageId} 이미지가 휴지통에 들어갔습니다.`, + ); + } + + /** + * 휴지통에서 선택된 이미지들을 복원하는 API 입니다. + * + * @summary 휴지통 복원 API + * @param body 복구하고 싶은 이미지들의 mediaId + * @param req + * @returns 복구 성공 문구 + */ + @Patch('/active') + @Tags('Trash') + @SuccessResponse(StatusCodes.CREATED, 'CREATED') + @Response(StatusCodes.BAD_REQUEST, 'Bad Request', { + resultType: 'FAIL', + error: { + errorCode: 'VAL-001', + reason: '잘못된 요청입니다.', + data: { + mediaIdList: { + message: '하나이상의 복구할 사진을 선택해주세요.', + value: [], + }, + }, + }, + success: null, + }) + @Response( + StatusCodes.INTERNAL_SERVER_ERROR, + 'Internal Server Error', + { + resultType: 'FAIL', + error: { + errorCode: 'DB-001', + reason: 'DB 에러 입니다.', + data: {reason: '에러원인 메시지'}, + }, + success: null, + }, + ) + @Response( + StatusCodes.INTERNAL_SERVER_ERROR, + 'Internal Server Error', + { + resultType: 'FAIL', + error: { + errorCode: 'SER-001', + reason: '내부 서버 오류입니다.', + data: {reason: '에러원인 메시지'}, + }, + success: null, + }, + ) + public async restoreImages( + @Body() body: {mediaIdList: string[]}, + @Request() req: ExpressRequest, + ): Promise> { + const dto = new RestoreImagesDTO(body.mediaIdList, req.user!.id); + await restoreImages(dto).catch(err => { + if (err instanceof BaseError) { + throw err; + } + throw new ServerError({reason: err.message}); + }); + + return new TsoaSuccessResponse('이미지 복구 성공'); + } + + /** + * 저장된 이미지들의 데이터를 삭제하는 API 입니다. + * + * @summary 이미지데이터 삭제 API + * @param body 삭제하고싶은 이미지들의 mediaId + * @param req + * @returns 삭제 성공 문구 + */ + @Delete('/images') + @Tags('Trash') + @SuccessResponse(StatusCodes.NO_CONTENT, 'DELETED') + @Response(StatusCodes.BAD_REQUEST, 'Bad Request', { + resultType: 'FAIL', + error: { + errorCode: 'VAL-001', + reason: '잘못된 요청입니다.', + data: { + mediaIdList: { + message: '하나이상의 복구할 사진을 선택해주세요.', + value: [], + }, + }, + }, + success: null, + }) + @Response( + StatusCodes.INTERNAL_SERVER_ERROR, + 'Internal Server Error', + { + resultType: 'FAIL', + error: { + errorCode: 'DB-001', + reason: 'DB 에러 입니다.', + data: {reason: '에러원인 메시지'}, + }, + success: null, + }, + ) + @Response( + StatusCodes.INTERNAL_SERVER_ERROR, + 'Internal Server Error', + { + resultType: 'FAIL', + error: { + errorCode: 'SER-001', + reason: '내부 서버 오류입니다.', + data: {reason: '에러원인 메시지'}, + }, + success: null, + }, + ) + public async deleteImages( + @Body() body: {mediaIdList: string[]}, + @Request() req: ExpressRequest, + ): Promise> { + const dto = new DeleteImagesDTO(body.mediaIdList, req.user!.id); + await deleteImages(dto).catch(err => { + if (err instanceof BaseError) { + throw err; + } + throw new ServerError({reason: err.message}); + }); + return new TsoaSuccessResponse('이미지 삭제 성공'); + } +} diff --git a/src/controllers/trust.controller.ts b/src/controllers/trust.controller.ts deleted file mode 100644 index a686987..0000000 --- a/src/controllers/trust.controller.ts +++ /dev/null @@ -1,471 +0,0 @@ -import {Request, Response, NextFunction} from 'express'; -import * as trustService from '../services/trust.service.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 => { - /* - #swagger.tags = ['Trust'] - #swagger.summary = '휴지통으로 이미지 이동 API' - #swagger.description = '선택한 이미지를 휴지통으로 이동시키는 API입니다.' - #swagger.requestBody = { - required: true, - content: { - "application/json": { - schema: { - type: "object", - properties: { - imageId: { - type: "integer", - description: "특정 이미지의 imageId" - } - }, - required: ["imageId"] - } - } - } - } - #swagger.responses[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: { - imageId: { type: "integer", example: 100001 }, - status: { type: "integer", example: 0 } - } - } - } - } - } - } - } - - #swagger.responses[500] = { - description: "잘못된 요청 (imageId가 올바르지 않음)", - content: { - "application/json": { - schema: { - type: "object", - properties: { - resultType: { type: "string", example: "FAIL" }, - error: { - type: "object", - properties: { - errorcode: { type: "string", example: "SRH-404"}, - reason: { type: "string", example: "imageId가 올바르지 않습니다" } - } - }, - success: { type: "object", nullable: true, example: null } - } - } - } - } - } - - #swagger.responses[500] = { - description: "해당 imageId에 대한 데이터를 찾을 수 없음", - content: { - "application/json": { - schema: { - type: "object", - properties: { - resultType: { type: "string", example: "FAIL" }, - error: { - type: "object", - properties: { - errorcode: { type: "string", example: "SRH-404"}, - reason: { type: "string", example: "해당 imageId에에 대한 이미지가 존재하지 않습니다" } - } - }, - success: { type: "object", nullable: true, example: null } - } - } - } - } - } - */ - try { - const {imageId} = req.body; - const parsedMediaId = parseInt(imageId); - if (isNaN(parsedMediaId)) { - throw new SearchNoResultsError({ - searchKeyword: 'imageId가 올바르지 않습니다', - }); - } - const updatedImage = await trustService.deactivateImages(imageId); - if (!updatedImage) { - throw new SearchNoResultsError({ - searchKeyword: '해당 imageId에 대한 이미지가 존재하지 않습니다', - }); - } - const result = { - imageId: updatedImage.imageId, - status: updatedImage.status, - }; - res.status(StatusCodes.OK).success(result); - } catch (error) { - next(error); - } -}; - -export const handleImageRestore = async ( - req: Request, - res: Response, - next: NextFunction, -): Promise => { - /* - #swagger.tags = ['Trust'] - #swagger.summary = '휴지통에서 이미지 복구 API' - #swagger.description = '선택한 이미지를 휴지통에서 복구하는 API입니다.' - #swagger.requestBody = { - required: true, - content: { - "application/json": { - schema: { - type: "object", - properties: { - imageIds: { - type: "array", - items: { type: "integer" }, - description: "복구할 이미지의 imageId 리스트" - } - }, - required: ["imageIds"] - } - } - } - } - - #swagger.responses[200] = { - description: "복구 성공", - content: { - "application/json": { - schema: { - type: "object", - properties: { - resultType: { type: "string", example: "SUCCESS" }, - error: { type: "object", nullable: true, example: null }, - success: { - type: "array", - items: { - type: "object", - properties: { - imageId: { type: "integer", example: 10000001 }, - status: { type: "integer", example: 1 } - } - } - } - } - } - } - } - } - #swagger.responses[404] = { - description: "잘못된 요청 (imageId가 올바르지 않음)", - content: { - "application/json": { - schema: { - type: "object", - properties: { - resultType: { type: "string", example: "FAIL" }, - error: { - type: "object", - properties: { - errorcode: { type: "string", example: "SRH-404"}, - reason: { type: "string", example: "imageId가 올바르지 않습니다" } - } - }, - success: { type: "object", nullable: true, example: null } - } - } - } - } - } - #swagger.responses[404] = { - description: "해당 imageId에 대한 데이터를 찾을 수 없음", - content: { - "application/json": { - schema: { - type: "object", - properties: { - resultType: { type: "string", example: "FAIL" }, - error: { - type: "object", - properties: { - errorcode: { type: "string", example: "SRH-404"}, - reason: { type: "string", example: "해당 imageIds에 대한 이미지가 존재하지 않습니다" } - } - }, - success: { type: "object", nullable: true, example: null } - } - } - } - } - } -*/ - try { - const {imageIds} = req.body; - if (!Array.isArray(imageIds)) { - throw new SearchNoResultsError({ - searchKeyword: 'imageIds가 올바르지 않습니다', - }); - } - const restoredImages = await trustService.restoreImages(imageIds); - if (!restoredImages.length) { - throw new SearchNoResultsError({ - searchKeyword: '해당 imageIds에 대한 이미지가 존재하지 않습니다', - }); - } - const result = restoredImages.map(image => ({ - imageId: image.imageId, - status: image.status, - })); - res.status(StatusCodes.OK).success(result); - } catch (error) { - next(error); - } -}; - -export const handleImageDelete = async ( - req: Request, - res: Response, - next: NextFunction, -): Promise => { - /* - #swagger.tags = ['Trust'] - #swagger.summary = '휴지통에서 이미지 삭제 API' - #swagger.description = '선택한 이미지를 휴지통에서 삭제하는 API입니다.' - - #swagger.requestBody = { - required: true, - content: { - "application/json": { - schema: { - type: "object", - properties: { - imageIds: { - type: "array", - items: { type: "integer" }, - description: "삭제할 이미지의 imageId 리스트" - } - }, - required: ["imageIds"] - } - } - } - } - #swagger.responses[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 }, - } - } - } - } - } - } - } - #swagger.responses[404] = { - description: "잘못된 요청 (imageIds가 올바르지 않음)", - content: { - "application/json": { - schema: { - type: "object", - properties: { - resultType: { type: "string", example: "FAIL" }, - 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: "FAIL", - error: { - errorCode: "SRH-404", - reason: "imageIds가 올바르지 않습니다", - }, - success: null - } - } - } - } - } - } - - #swagger.responses[400] = { - description: "삭제할 이미지가 존재하지 않음", - content: { - "application/json": { - schema: { - type: "object", - properties: { - resultType: { type: "string", example: "FAIL" }, - 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: "FAIL", - error: { - errorCode: "SRH-400", - reason: "해당 사진이 휴지통에 존재하지 않습니다", - }, - success: null - } - } - } - } - } - } - */ - - try { - const {imageIds} = req.body; - if (!Array.isArray(imageIds)) { - throw new SearchNoResultsError({ - searchKeyword: 'imageIds가 올바르지 않습니다', - }); - } - const deleteable = await trustService.deleteImages(imageIds); - if (!deleteable) { - throw new DataValidationError({ - reason: '해당 사진이 휴지통에 존재하지 않습니다', - }); - } - res.status(StatusCodes.OK).success(deleteable); - } catch (error) { - next(error); - } -}; - -export const handleImage = async ( - req: Request, - res: Response, - next: NextFunction - ): Promise => { - /* - #swagger.tags = ['Trust'] - #swagger.summary = '휴지통에 있는 이미지 목록 조회 API' - #swagger.description = '로그인한 사용자의 휴지통에 있는 이미지 정보를 가져오는 API입니다.' - #swagger.responses[200] = { - description: "휴지통 조회 성공", - content: { - "application/json": { - schema: { - type: "object", - properties: { - resultType: { type: "string", example: "SUCCESS" }, - error: { type: "object", nullable: true, example: null }, - success: { - type: "array", - items: { - type: "object", - properties: { - imageId: { type: "integer", example: 100001 }, - mediaId: { type: "integer", example: 500002 } - } - } - } - } - } - } - } - } - #swagger.responses[404] = { - description: "해당 user_id 가 존재하지 않습니다", - content: { - "application/json": { - schema: { - type: "object", - properties: { - resultType: { type: "string", example: "FAIL" }, - error: { - type: "object", - properties: { - errorCode: { type: "string", example: "SRH-400" }, - reason: { type: "string", example: "해당 user_id 가 존재하지 않습니다" } - } - }, - success: { type: "object", nullable: true, example: null } - } - } - } - } - } - #swagger.responses[404] = { - description: "휴지통에 이미지 없음", - content: { - "application/json": { - schema: { - type: "object", - properties: { - resultType: { type: "string", example: "FAIL" }, - error: { - type: "object", - properties: { - errorCode: { type: "string", example: "SRH-400" }, - reason: { type: "string", example: "해당 사진이 휴지통에 존재하지 않습니다" } - } - }, - success: { type: "object", nullable: true, example: null } - } - } - } - } - } - */ - try { - const userId = Number(req.user?.id); - if (!userId) { - throw new DataValidationError({reason: 'user_id가 존재하지 않습니다'}); - } - const images = await trustService.getTrashedImages(userId); - - if (!images.length) { - throw new DataValidationError({ - reason: '해당 사진이 휴지통에 존재하지 않습니다', - }); - } - res.status(StatusCodes.OK).success(images); - } catch (error) { - next(error); - } - }; \ No newline at end of file diff --git a/src/controllers/tsoa.memo-folder.controller.ts b/src/controllers/tsoa.memo-folder.controller.ts index 2068256..1b7663b 100644 --- a/src/controllers/tsoa.memo-folder.controller.ts +++ b/src/controllers/tsoa.memo-folder.controller.ts @@ -171,7 +171,7 @@ export class MemoFolderController extends Controller { * @returns 성공 시 폴더 생성 결과를 반환합니다. */ @Post('/folders') - @Tags('memo-folder-controller') + @Tags('Memo-folder') @Response( StatusCodes.BAD_REQUEST, '유효하지 않은 데이터 에러', @@ -224,7 +224,7 @@ export class MemoFolderController extends Controller { * @returns 성공 시 메모 조회 결과를 반환합니다. */ @Get('/list') - @Tags('memo-folder-controller') + @Tags('Memo-folder') @SuccessResponse(StatusCodes.OK, '메모 조회 성공 응답') @Example({ resultType: 'SUCCESS', @@ -264,7 +264,7 @@ export class MemoFolderController extends Controller { * @returns 성공 시 메모 검색 결과를 반환합니다. */ @Get('/search') - @Tags('memo-folder-controller') + @Tags('Memo-folder') @Response( StatusCodes.BAD_REQUEST, '유효하지 않은 검색 키워드 에러', @@ -325,7 +325,7 @@ export class MemoFolderController extends Controller { * @returns 성공 시 사진 삭제 결과를 반환합니다. */ @Post('/folders/:folderId/images/delete') - @Tags('memo-image-controller') + @Tags('Memo-image') @Response( StatusCodes.NOT_FOUND, '존재하지 않은 데이터 조회 에러', @@ -400,7 +400,7 @@ export class MemoFolderController extends Controller { * @returns 성공 시 특정 메모의 조회 결과를 반환합니다. */ @Get('/folders/:folderId') - @Tags('memo-folder-controller') + @Tags('Memo-folder') @Response( StatusCodes.NOT_FOUND, '존재하지 않은 데이터 조회 에러', @@ -456,7 +456,7 @@ export class MemoFolderController extends Controller { * @returns 성공 시 메모 폴더 이름 수정 결과를 반환합니다. */ @Patch('/folders/:folderId') - @Tags('memo-folder-controller') + @Tags('Memo-folder') @Response( StatusCodes.BAD_REQUEST, '유효하지 않은 데이터 에러', @@ -552,7 +552,7 @@ export class MemoFolderController extends Controller { * @returns 성공 시 메모 텍스트 수정 결과를 반환합니다. */ @Patch('/folders/:folderId/text') - @Tags('memo-folder-controller') + @Tags('Memo-folder') @Response( StatusCodes.NOT_FOUND, '존재하지 않은 데이터 조회 에러', diff --git a/src/controllers/tsoa.memo-image.controller.ts b/src/controllers/tsoa.memo-image.controller.ts index ef848a0..923c215 100644 --- a/src/controllers/tsoa.memo-image.controller.ts +++ b/src/controllers/tsoa.memo-image.controller.ts @@ -47,7 +47,7 @@ export class MemoImageController extends Controller { */ @Post('/image-format/folders/:folderId') @Middlewares(ImageUploadMiddleware) - @Tags('memo-image-controller') + @Tags('Memo-image') @Response( StatusCodes.BAD_REQUEST, '유효하지 않은 데이터 조회 에러', @@ -148,7 +148,7 @@ export class MemoImageController extends Controller { * @returns 성공 시 사진 이동 결과를 반환합니다. */ @Patch('/folders/:folderId/images') - @Tags('memo-image-controller') + @Tags('Memo-image') @Response( StatusCodes.BAD_REQUEST, '유효하지 않은 데이터 조회 에러', @@ -239,7 +239,7 @@ export class MemoImageController extends Controller { * @returns 성공 시 폴더 삭제 결과를 반환합니다. */ @Delete('/folders/:folderId') - @Tags('memo-folder-controller') + @Tags('Memo-folder') @Response( StatusCodes.NOT_FOUND, '존재하지 않은 데이터 조회 에러', diff --git a/src/controllers/tsoaTag.controller.ts b/src/controllers/tsoaTag.controller.ts index e78db15..d8c114b 100644 --- a/src/controllers/tsoaTag.controller.ts +++ b/src/controllers/tsoaTag.controller.ts @@ -8,13 +8,26 @@ import { Response, Request, Path, + Post, + Body, + Middlewares, } from 'tsoa'; import { findTagsByDate, findTagsFromImage, } from '../services/tsoaTag.service.js'; -import {DateToTags, ImageToTags} from '../dtos/tsoaTag.dto.js'; -import {BaseError, ServerError} from '../errors.js'; +import { + DateToTags, + ImageToTags, + RequestBodyCreationTags, + RequestCreationTags, +} from '../dtos/tsoaTag.dto.js'; +import { + BaseError, + PhotoDataNotFoundError, + PhotoValidationError, + ServerError, +} from '../errors.js'; import { ITsoaErrorResponse, ITsoaSuccessResponse, @@ -23,9 +36,13 @@ import { import {StatusCodes} from 'http-status-codes'; import {Request as ExpressRequest} from 'express'; import { + ResponseCreationTags, ResponseTagListFromImage, ResponseTagListWithDate, } from '../models/tsoaTag.model.js'; +import {tagCreate} from '../services/tag.service.js'; +import {uploadMiddleware} from '../ai/ai-upload.middleware.js'; +import {detectLabels} from '../services/tags-ai.service.js'; @Route('tags') export class TagsController extends Controller { @@ -192,4 +209,156 @@ export class TagsController extends Controller { }); return new TsoaSuccessResponse(tags); } + + /** + * 태그를 생성하는 API 입니다. + * + * @summary 태그 생성 + * @param imageId 이미지ID + * @param body 생성할 태그들 + * @returns 생성된 태그 + */ + @Post('/images/{imageId}') + @Tags('Tag') + @SuccessResponse(201, 'CREATED') + @Response( + StatusCodes.INTERNAL_SERVER_ERROR, + 'Internal Server Error', + { + resultType: 'FAIL', + error: { + errorCode: 'DB-001', + reason: 'DB 에러 입니다.', + data: {reason: '에러원인 메시지'}, + }, + success: null, + }, + ) + @Response(500, 'Internal Server Error', { + resultType: 'FAIL', + error: { + errorCode: 'SER-001', + reason: '내부 서버 오류입니다.', + data: {reason: '에러원인 메시지'}, + }, + success: null, + }) + public async postTag( + @Path() imageId: string, + @Body() body: RequestBodyCreationTags, + ): Promise> { + const dto = new RequestCreationTags(imageId, body.tags); + const newImageTag = await tagCreate(dto).catch(err => { + if (err instanceof BaseError) { + throw err; + } + throw new ServerError({reason: err.message}); + }); + return new TsoaSuccessResponse(newImageTag); + } + + /** + * 이미지를 분석하여 태그 추천 라벨을 생성합니다. + * + * @summary 이미지 분석 태그생성 API + * @param req + * @param image 라벨을 생성할 이미지 + * @returns 태그 라벨 + */ + @Post('/ai') + @Middlewares(uploadMiddleware) + @Tags('Tag') + @SuccessResponse(201, 'CREATED') + @Response(StatusCodes.NOT_FOUND, 'Not Found', { + resultType: 'FAIL', + error: { + errorCode: 'PHO-404', + reason: '해당 사진 데이터가 없습니다.', + data: {reason: '라벨링을 추출 할 사진이 없습니다.'}, + }, + success: null, + }) + @Response(StatusCodes.NOT_FOUND, 'Not Found', { + resultType: 'FAIL', + error: { + errorCode: 'LBL-405', + reason: '이미지에서 라벨을 감지하지 못했습니다.', + data: {reason: '해당 이미지는 정보가 부족합니다.'}, + }, + success: null, + }) + @Response(StatusCodes.BAD_REQUEST, 'Bad Request', { + resultType: 'FAIL', + error: { + errorCode: 'PHO-400', + reason: '사진 데이터가 유효하지 않습니다.', + data: {reason: '올바른 Base64 이미지 형식이 아닙니다.'}, + }, + success: null, + }) + @Response( + StatusCodes.INTERNAL_SERVER_ERROR, + 'Internal Server Error', + { + resultType: 'FAIL', + error: { + errorCode: 'LBL-500', + reason: '라벨링 처리 중 오류가 발생했습니다.', + data: {reason: '라벨링 처리 중 오류가 발생했습니다.'}, + }, + success: null, + }, + ) + @Response( + StatusCodes.INTERNAL_SERVER_ERROR, + 'Internal Server Error', + { + resultType: 'FAIL', + error: { + errorCode: 'SER-001', + reason: '내부 서버 오류입니다.', + data: {reason: '에러원인 메시지'}, + }, + success: null, + }, + ) + public async getLableFromImage(@Request() req: ExpressRequest): Promise< + ITsoaSuccessResponse<{ + labels: { + description: string; + score: number; + }[]; + }> + > { + if (!req.file) { + throw new PhotoDataNotFoundError({ + reason: '라벨링을 추출 할 사진이 없습니다.', + }); + } + + const base64_image = req.file.buffer.toString('base64'); + + if (!isValidBase64(base64_image)) { + throw new PhotoValidationError({ + reason: '올바른 Base64 이미지 형식이 아닙니다.', + }); + } + + const labels = { + labels: await detectLabels(base64_image).catch(err => { + if (err instanceof BaseError) { + throw err; + } + throw new ServerError({reason: err.message}); + }), + }; + return new TsoaSuccessResponse(labels); + } } + +const isValidBase64 = (base64String: string): boolean => { + // base64 문자열 검증 함수 + const base64Regex = + /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/; + return base64Regex.test(base64String); +}; diff --git a/src/controllers/user.controller.ts b/src/controllers/user.controller.ts index 9fbfd3b..e005492 100644 --- a/src/controllers/user.controller.ts +++ b/src/controllers/user.controller.ts @@ -1,162 +1,363 @@ -import { Request, Response, NextFunction } from 'express'; -import { updateUserName, updateUserGoalCount } from '../services/user.service.js'; -import { DataValidationError } from '../errors.js'; -import { StatusCodes } from 'http-status-codes'; +import {Request as ExpressRequest} from 'express'; +import { + updateUserGoalCount as updateGoalCount, + updateUserName, +} from '../services/user.service.js'; +import {AuthError, BaseError, ServerError} from '../errors.js'; +import {StatusCodes} from 'http-status-codes'; +import { + Body, + Controller, + Patch, + Request, + Route, + SuccessResponse, + Tags, + ValidateError, + Response, + Get, +} from 'tsoa'; +import { + ITsoaErrorResponse, + ITsoaSuccessResponse, + TsoaSuccessResponse, +} from '../models/tsoaResponse.js'; +import {ResponseUser} from '../models/user.model.js'; +import { + getUserById, + logoutUserService, + removeUser, +} from '../services/mypage.service.js'; +@Route('onboarding') +export class UserController extends Controller { + /** + * 로그인한 사용자의 이름을 변경하는 API입니다. + * + * @summary 사용자 이름 변경 API + * @param req + * @param bodyName 변경할 이름 + * @returns 변경된 유저정보 + */ + @Patch('/name') + @Tags('User') + @SuccessResponse(StatusCodes.CREATED, 'CREATED') + @Response(StatusCodes.BAD_REQUEST, 'Bad Request', { + resultType: 'FAIL', + error: { + errorCode: 'VAL-001', + reason: 'Validation Error', + data: { + name: { + message: 'name is required', + value: null, + }, + }, + }, + success: null, + }) + @Response( + StatusCodes.INTERNAL_SERVER_ERROR, + 'Internal Server Error', + { + resultType: 'FAIL', + error: { + errorCode: 'DB-001', + reason: 'DB 에러 입니다.', + data: {reason: '에러원인 메시지'}, + }, + success: null, + }, + ) + @Response( + StatusCodes.INTERNAL_SERVER_ERROR, + 'Internal Server Error', + { + resultType: 'FAIL', + error: { + errorCode: 'SER-001', + reason: '내부 서버 오류입니다.', + data: {reason: '에러원인 메시지'}, + }, + success: null, + }, + ) + public async updateUserName( + @Request() req: ExpressRequest, + @Body() body: {name: string}, + ): Promise> { + const userId = req.user!.id; -// 사용자 이름 변경 -export const updateUserNameController = async ( - req: Request, - res: Response, - next: NextFunction -): Promise => { -/* - #swagger.tags = ['User'] - #swagger.summary = '사용자 이름 변경 API' - #swagger.description = '로그인한 사용자의 이름을 변경하는 API입니다.' - #swagger.requestBody = { - required: true, - content: { - "application/json": { - schema: { - type: "object", - properties: { - name: { type: "string", description: "변경할 사용자 이름" } - }, - required: ["name"] - } - } - } + const name = body.name.trim(); + if (!name) { + throw new ValidateError( + {name: {message: 'name is required', value: null}}, + '이름이 입력되지 않았습니다.', + ); } - #swagger.responses[200] = { - description: "사용자 이름 변경 성공", - 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 }, - name: { type: "string", example: "John Doe" }, - updatedAt: { type: "string", format: "date-time" } - } - } - } + + const updatedUser = await updateUserName(userId, name).catch(err => { + if (err instanceof BaseError) { + throw err; } - } - #swagger.responses[400] = { - description: "잘못된 요청", - schema: { - type: "object", - properties: { - resultType: { type: "string", example: "FAILURE" }, - error: { - type: "object", - properties: { - reason: { type: "string", example: "Name is required." } - } + throw new ServerError({reason: err.message}); + }); + return new TsoaSuccessResponse(updatedUser); + } + + /** + * 로그인한 사용자의 목표 장수를 변경하는 API입니다. + * + * @summary 사용자 목표 장수 변경 API + * @param req + * @param body 변경할 목표 장수 + * @returns 변경된 유저정보 + */ + @Patch('/goal') + @Tags('User') + @SuccessResponse(StatusCodes.CREATED, 'CREATED') + @Response(StatusCodes.BAD_REQUEST, 'Bad Request', { + resultType: 'FAIL', + error: { + errorCode: 'VAL-001', + reason: 'Validation Error', + data: { + goalCount: { + message: 'goalCount is required', + value: null, + }, + }, + }, + success: null, + }) + @Response(StatusCodes.BAD_REQUEST, 'Bad Request', { + resultType: 'FAIL', + error: { + errorCode: 'VAL-001', + reason: 'Validation Error', + data: { + goalCount: { + message: 'goalCount is unsigned Integer', + value: -8, + }, + }, + }, + success: null, + }) + @Response( + StatusCodes.INTERNAL_SERVER_ERROR, + 'Internal Server Error', + { + resultType: 'FAIL', + error: { + errorCode: 'DB-001', + reason: 'DB 에러 입니다.', + data: {reason: '에러원인 메시지'}, + }, + success: null, + }, + ) + @Response( + StatusCodes.INTERNAL_SERVER_ERROR, + 'Internal Server Error', + { + resultType: 'FAIL', + error: { + errorCode: 'SER-001', + reason: '내부 서버 오류입니다.', + data: {reason: '에러원인 메시지'}, + }, + success: null, + }, + ) + public async updateUserGoalCount( + @Request() req: ExpressRequest, + @Body() body: {goalCount: number}, + ): Promise> { + const userId = req.user!.id; + const goalCount = body.goalCount; + if (goalCount === undefined || goalCount < 0) { + throw new ValidateError( + { + goalCount: { + message: 'goalCount is unsigned Integer', + value: goalCount, }, - success: { type: "object", nullable: true, example: null } - } - } + }, + '잘못된 목표장수 입니다.', + ); } - */ - const userId = req.user?.id; - if (!userId) { - throw new DataValidationError({reason: 'user_id is required.'}); - } - const { name } = req.body; + const updatedUser = await updateGoalCount(userId, goalCount).catch(err => { + if (err instanceof BaseError) { + throw err; + } + throw new ServerError({reason: err.message}); + }); - if (!name) { - throw new DataValidationError({reason: 'Name is required.'}); + return new TsoaSuccessResponse(updatedUser); } +} + +@Route('user/mypage') +export class MypageController extends Controller { + /** + * 사용자 정보를 가져오는 API 입니다. + * + * @summary 사용자 정보 가져오기 + * @param req + * @returns 유저 정보 + */ + @Get('') + @Tags('User') + @SuccessResponse(StatusCodes.OK, 'OK') + @Response(StatusCodes.NOT_FOUND, 'Not Found', { + resultType: 'FAIL', + error: { + errorCode: 'USR-404', + reason: '사용자를 찾을수 없습니다', + data: null, + }, + success: null, + }) + @Response( + StatusCodes.INTERNAL_SERVER_ERROR, + 'Internal Server Error', + { + resultType: 'FAIL', + error: { + errorCode: 'SER-001', + reason: '내부 서버 오류입니다.', + data: {reason: '에러원인 메시지'}, + }, + success: null, + }, + ) + @Response( + StatusCodes.INTERNAL_SERVER_ERROR, + 'Internal Server Error', + { + resultType: 'FAIL', + error: { + errorCode: 'DB-001', + reason: 'DB 에러 입니다.', + data: {reason: '에러원인 메시지'}, + }, + success: null, + }, + ) + public async getUser( + @Request() req: ExpressRequest, + ): Promise> { + const myPageUserId = req.user!.id; + const user = await getUserById(myPageUserId); + const userInfo = { + ...user, + id: user.id.toString(), + }; - try { - const updatedUser = await updateUserName(Number(userId), name); - res.status(StatusCodes.OK).success(updatedUser); - } catch (error) { - next(error); + return new TsoaSuccessResponse(userInfo); } -}; -// 사용자 목표 장수 변경 -export const updateUserGoalCountController = async ( - req: Request, - res: Response, - next: NextFunction -): Promise => { - /* - #swagger.tags = ['User'] - #swagger.summary = '사용자 목표 장수 변경 API' - #swagger.description = '로그인한 사용자의 목표 장수를 변경하는 API입니다.' - #swagger.requestBody = { - required: true, - content: { - "application/json": { - schema: { - type: "object", - properties: { - goalCount: { type: "integer", description: "변경할 목표 장수" } - }, - required: ["goalCount"] - } - } - } - } - #swagger.responses[200] = { - description: "목표 장수 변경 성공", - 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 }, - goalCount: { type: "integer", example: 5 }, - updatedAt: { type: "string", format: "date-time" } - } - } - } - } + /** + * 사용자 로그아웃 API 입니다. + * + * @summary 사용자 로그아웃 API + * @param req + * @returns 로그아웃 성공 메시지 + */ + @Patch('/logout') + @Tags('User') + @SuccessResponse(StatusCodes.NO_CONTENT, 'LOGOUT') + @Response(StatusCodes.NOT_FOUND, 'Not Found', { + resultType: 'FAIL', + error: { + errorCode: 'AUT-401', + reason: '인증 실패', + data: {reason: 'Session ID가 존재하지 않습니다'}, + }, + success: null, + }) + @Response( + StatusCodes.INTERNAL_SERVER_ERROR, + 'Internal Server Error', + { + resultType: 'FAIL', + error: { + errorCode: 'SER-001', + reason: '내부 서버 오류입니다.', + data: {reason: '에러원인 메시지'}, + }, + success: null, + }, + ) + @Response( + StatusCodes.INTERNAL_SERVER_ERROR, + 'Internal Server Error', + { + resultType: 'FAIL', + error: { + errorCode: 'DB-001', + reason: 'DB 에러 입니다.', + data: {reason: '에러원인 메시지'}, + }, + success: null, + }, + ) + public async logOut( + @Request() req: ExpressRequest, + ): Promise> { + const sessionID = req.sessionID; + if (sessionID.trim() === '') { + throw new AuthError({reason: 'Session ID가 존재하지 않습니다'}); } - #swagger.responses[400] = { - description: "잘못된 요청", - schema: { - type: "object", - properties: { - resultType: { type: "string", example: "FAILURE" }, - error: { - type: "object", - properties: { - reason: { type: "string", example: "goal_count is required." } - } - }, - success: { type: "object", nullable: true, example: null } - } - } - } - */ - const userId = req.user?.id; - if (!userId) { - throw new DataValidationError({reason: 'user_id is required.'}); + await logoutUserService(sessionID); + req.session.destroy(() => {}); + return new TsoaSuccessResponse('사용자 로그아웃 성공'); } - const { goalCount } = req.body; - - if (goalCount === undefined || goalCount < 0) { - throw new DataValidationError({reason: 'goal_count is required.'}); + /** + * 사용자 상태를 비활성화하는 API 입니다. + * + * @summary 회원탈퇴 + * @param req + * @returns 탈퇴 성공 메시지 + */ + @Patch() + @Tags('User') + @SuccessResponse(StatusCodes.NO_CONTENT, '회원 탈퇴') + @Response(StatusCodes.NOT_FOUND, 'Not Found', { + resultType: 'FAIL', + error: { + errorCode: 'AUT-401', + reason: '인증 실패', + data: {reason: 'user를 찾을 수 없습니다.'}, + }, + success: null, + }) + @Response( + StatusCodes.INTERNAL_SERVER_ERROR, + 'Internal Server Error', + { + resultType: 'FAIL', + error: { + errorCode: 'DB-001', + reason: 'DB 에러 입니다.', + data: {reason: '에러원인 메시지'}, + }, + success: null, + }, + ) + public async deleteUser( + @Request() req: ExpressRequest, + ): Promise> { + const userId = req.user!.id; + const success = await removeUser(userId); - } + if (!success) { + throw new AuthError({reason: 'user를 찾을 수 없습니다'}); + } - try { - const updatedUser = await updateUserGoalCount(Number(userId), goalCount); - res.status(StatusCodes.OK).success(updatedUser); - } catch (error) { - next(error); + return new TsoaSuccessResponse('회원 탈퇴 성공'); } -}; +} diff --git a/src/controllers/user.mypage.controllers.ts b/src/controllers/user.mypage.controllers.ts deleted file mode 100644 index 1c4e8a8..0000000 --- a/src/controllers/user.mypage.controllers.ts +++ /dev/null @@ -1,168 +0,0 @@ -import {Request, Response, NextFunction} from 'express'; -import * as userService from '../services/mypage.service.js'; -import {AuthError} from '../errors.js'; -import {StatusCodes} from 'http-status-codes'; - -//사용자 정보 가져오기 -export const getUser = async ( - req: Request, - res: Response, - next: NextFunction, -): Promise => { - /* - #swagger.tags = ['User'] - #swagger.summary = '사용자 정보 가져오기 API' - #swagger.description = '사용자 정보를 가져오는 API입니다.' - #swagger.responses[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" } - } - } - } - } - } - } - } - #swagger.responses[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 } - } - } - } - } - } - */ - try { - if (!req.user?.id) { - throw new AuthError({reason: 'User_id를 찾을 수 없습니다'}); - } - const myPageUserId = req.user?.id; - const user = await userService.getUserById(myPageUserId); - res.status(StatusCodes.OK).success(user); - } catch (error) { - next(error); - } -}; -//사용자 로그아웃 -export const logOutUser = async ( - req: Request, - res: Response, - next: NextFunction, -): Promise => { - /* - #swagger.tags = ['User'] - #swagger.summary = '사용자 로그아웃 API' - #swagger.description = '로그아웃 API입니다.' - #swagger.responses[204] = { - description: "사용자 로그아웃 성공", - } - #swagger.responses[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 } - } - } - } - } - } - */ - try { - const sessionId = req.sessionID; - if (!sessionId) { - throw new AuthError({reason: 'Session ID가 존재하지 않습니다'}); - } - await userService.logoutUserService(sessionId); //세션 삭제 처리 - req.session.destroy(() => {}); //express 세션도 삭제 - res.status(204).send(); // No Content; - } catch (error) { - next(error); - } -}; -//회원탈퇴 -export const deleteUser = async ( - req: Request, - res: Response, - next: NextFunction, -) => { - /* - #swagger.tags = ['User'] - #swagger.summary = '회원탈퇴 API' - #swagger.description = '사용자 상태를 비활성화하는 API입니다.' - #swagger.responses[204] = { - description: "회원탈퇴 성공", - } - #swagger.responses[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 } - } - } - } - } - } - */ - try { - if (!req.user?.id) { - throw new AuthError({reason: 'User_id를 찾을 수 없습니다'}); - } - const userId = req.user?.id; - const success = await userService.deleteUser(userId); - - if (!success) { - throw new AuthError({reason: 'user를 찾을 수 없습니다'}); - } - - res.status(204).send(); // No Content - } catch (error) { - next(error); - } -}; diff --git a/src/dtos/trash.dto.ts b/src/dtos/trash.dto.ts new file mode 100644 index 0000000..3fe2544 --- /dev/null +++ b/src/dtos/trash.dto.ts @@ -0,0 +1,43 @@ +import {ValidateError} from 'tsoa'; + +export class RestoreImagesDTO { + mediaIdList: bigint[]; + userId: bigint; + + constructor(mediaIdList: string[], userId: bigint) { + this.userId = userId; + if (mediaIdList.length === 0) { + throw new ValidateError( + { + mediaIdList: { + value: mediaIdList, + message: '하나이상의 복구할 사진을 선택해주세요', + }, + }, + '잘못된 요청입니다.', + ); + } + this.mediaIdList = mediaIdList.map(mediaId => BigInt(mediaId)); + } +} + +export class DeleteImagesDTO { + mediaIdList: bigint[]; + userId: bigint; + + constructor(mediaIdList: string[], userId: bigint) { + this.userId = userId; + if (mediaIdList.length === 0) { + throw new ValidateError( + { + mediaIdList: { + value: mediaIdList, + message: '하나이상의 삭제할 사진을 선택해주세요', + }, + }, + '잘못된 요청입니다.', + ); + } + this.mediaIdList = mediaIdList.map(mediaId => BigInt(mediaId)); + } +} diff --git a/src/dtos/tsoaTag.dto.ts b/src/dtos/tsoaTag.dto.ts index 26117c9..d784e94 100644 --- a/src/dtos/tsoaTag.dto.ts +++ b/src/dtos/tsoaTag.dto.ts @@ -22,3 +22,25 @@ export class ImageToTags { this.mediaId = mediaId; } } + +export interface RequestBodyCreationTags { + tags: { + content: string; + tagCategoryId: string; + }[]; +} + +export class RequestCreationTags { + imageId: bigint; + tags: {content: string; tagCategoryId: bigint}[]; + + constructor( + imageId: string, + tags: {content: string; tagCategoryId: string}[], + ) { + this.imageId = BigInt(imageId); + this.tags = tags.map(tag => { + return {content: tag.content, tagCategoryId: BigInt(tag.tagCategoryId)}; + }); + } +} diff --git a/src/models/tsoaTag.model.ts b/src/models/tsoaTag.model.ts index 25b9c69..ea95485 100644 --- a/src/models/tsoaTag.model.ts +++ b/src/models/tsoaTag.model.ts @@ -11,3 +11,38 @@ export interface ResponseTagListFromImage { }; }[]; } + +export interface ResponseCreationTags { + tags: { + id: string; + createdAt: Date; + updatedAt: Date | null; + status: number; + imageId: string; + tagId: string; + }[]; +} + +export const responseCreationTags = ( + imageTagData: { + id: bigint; + createdAt: Date; + updatedAt: Date | null; + status: number; + imageId: bigint; + tagId: bigint; + }[], +): ResponseCreationTags => { + const responseData = imageTagData.map(data => { + const {id, createdAt, updatedAt, status, imageId, tagId} = data; + return { + id: id.toString(), + createdAt, + updatedAt, + status, + imageId: imageId.toString(), + tagId: tagId.toString(), + }; + }); + return {tags: responseData}; +}; diff --git a/src/models/user.model.ts b/src/models/user.model.ts index 8858f41..62bff10 100644 --- a/src/models/user.model.ts +++ b/src/models/user.model.ts @@ -7,3 +7,13 @@ export interface UserModel { updatedAt: Date | null; status: number; } + +export interface ResponseUser { + id: string; + email: string; + name: string; + goalCount: number; + createdAt: Date; + updatedAt: Date | null; + status: number; +} diff --git a/src/repositories/mypage.repository.ts b/src/repositories/mypage.repository.ts deleted file mode 100644 index 4e58654..0000000 --- a/src/repositories/mypage.repository.ts +++ /dev/null @@ -1,36 +0,0 @@ -import {prisma} from '../db.config.js'; -import {UserModel} from '../models/user.model.js'; - -export const userRepository = { - findUserById: async (user_id: bigint): Promise => { - const user = await prisma.user.findUnique({ - where: { id: Number(user_id) }, - }); - - return user; - }, - - deleteUser: async (user_id: bigint): Promise => { - try { - await prisma.user.update({ - where: { id: Number(user_id) }, - data: { status:0 }, - }); - return true; - } catch (error) { - throw error; - } - }, -}; - -export const logoutUserRepository = async (sessionId: string): Promise => { - try { - await prisma.session.delete({ - where: { - sid: sessionId, - }, - }); - } catch (error) { - throw error; - } -}; \ No newline at end of file diff --git a/src/repositories/tag.repositories.ts b/src/repositories/tag.repositories.ts index 2608d70..9817b31 100644 --- a/src/repositories/tag.repositories.ts +++ b/src/repositories/tag.repositories.ts @@ -1,32 +1,32 @@ +import {ImageTag} from '@prisma/client'; import {prisma} from '../db.config.js'; import {TagNotFound} from '../errors.js'; -import { - ResponseFromTag, - BodyToImageTag, - ResponseFromImageTag, -} from '../models/tag.model.js'; +import {ResponseFromTag} from '../models/tag.model.js'; // 태그가 있을 시, 태그를 반환하고 없을 시, 태그를 생성 export async function addImageTag({ imageId, tags, -}: BodyToImageTag): Promise { +}: { + imageId: bigint; + tags: {content: string; tagCategoryId: bigint}[]; +}): Promise { for (let i = 0; i < tags.length; i++) { const tag = tags[i]; - const {content, tag_category_id} = tag; + const {content, tagCategoryId} = tag; // 태그가 빈 값일 경우, 해당 이미지의 태그 상태를 0으로 변경 if (content === '') { - await inActiveTag(imageId, tag_category_id); + await inActiveTag(imageId, tagCategoryId); } // 태그가 동일한 것이 있을 시, 태그를 반환하고 / 태그가 동일한 것이 없을 시, 태그를 생성 else { - const tagData = await getTag(content, tag_category_id); - // await activeTag(imageId, tag_category_id); - await inActiveTag(imageId, tag_category_id); + const tagData = await getTag(content, tagCategoryId); + // await activeTag(imageId, tagCategoryId); + await inActiveTag(imageId, tagCategoryId); // 태그가 동일한 것이 없으면 생성 if (!tagData) { - await newTag(imageId, content, tag_category_id); + await newTag(imageId, content, tagCategoryId); } // 태그가 동일한 것이 있으면 미생성 else { @@ -245,7 +245,7 @@ export async function getTag( export async function getImageTag( imageId: bigint | number, -): Promise { +): Promise { const imageTagData = await prisma.imageTag.findMany({ where: { imageId, diff --git a/src/repositories/trash.repository.ts b/src/repositories/trash.repository.ts new file mode 100644 index 0000000..b10b96d --- /dev/null +++ b/src/repositories/trash.repository.ts @@ -0,0 +1,41 @@ +import {prisma} from '../db.config.js'; +import {DBError} from '../errors.js'; + +export const updateImageStatus = async ( + mediaIdList: bigint[], + status: number, + userId?: bigint, +): Promise => { + if (userId) { + await prisma.image + .updateMany({ + where: {mediaId: {in: mediaIdList}, userId: userId}, + data: {status}, + }) + .catch(err => { + throw new DBError({reason: err.message}); + }); + } else { + await prisma.image + .updateMany({ + where: {id: {in: mediaIdList}}, + data: {status}, + }) + .catch(err => { + throw new DBError({reason: err.message}); + }); + } +}; + +export const removeImages = async ( + mediaIdList: bigint[], + userId: bigint, +): Promise => { + await prisma.image + .deleteMany({ + where: {mediaId: {in: mediaIdList}, userId: userId, status: 0}, + }) + .catch(err => { + throw new DBError({reason: err.message}); + }); +}; diff --git a/src/repositories/trust.repositories.ts b/src/repositories/trust.repositories.ts deleted file mode 100644 index 443aa00..0000000 --- a/src/repositories/trust.repositories.ts +++ /dev/null @@ -1,38 +0,0 @@ -import {prisma} from '../db.config.js'; - -export const updateImageStatus = async (imageIds: number[], status: number): Promise => { - await prisma.image.updateMany({ - where: { id: { in: imageIds } }, - data: { status } - }); -}; - -export const removeImages = async (imageIds: number[]): Promise => { - await prisma.image.deleteMany({ - where: { id: { in: imageIds }, status: 0 } - }); -}; - -export const getImagesByIds = async (imageIds: number[]): Promise<{ imageId: number; status: number }[]> => { - const images = await prisma.image.findMany({ - where: { id: { in: imageIds } }, - select: { id: true, status: true } - }); - return images.map(({ id, status }) => ({ - imageId: Number(id), // 변환 적용 - status - })); -}; - -export const getTrashedImages = async (userId: number): Promise<{ imageId: number; mediaId: number }[]> => { - const images = await prisma.image.findMany({ - where: { userId, status: 0 }, - select: { id: true, mediaId: true } - }); - - return images.map(({ id, mediaId }) => ({ - imageId: Number(id), - mediaId: Number(mediaId), - })); - }; - \ No newline at end of file diff --git a/src/repositories/tsoaTag.repository.ts b/src/repositories/tsoaTag.repository.ts index 174cf02..7b23d26 100644 --- a/src/repositories/tsoaTag.repository.ts +++ b/src/repositories/tsoaTag.repository.ts @@ -1,6 +1,7 @@ -import {Tag} from '@prisma/client'; +import {Tag, ImageTag} from '@prisma/client'; +import {DBError, TagNotFound} from '../errors.js'; import {prisma} from '../db.config.js'; -import {DBError} from '../errors.js'; +import {ResponseFromTag} from '../models/tag.model.js'; export const selectTagsByDate = async ( userId: bigint, @@ -77,3 +78,256 @@ export const selectTagsFromImage = async ( return tags; }; + +// 태그가 있을 시, 태그를 반환하고 없을 시, 태그를 생성 +export async function addImageTag({ + imageId, + tags, +}: { + imageId: bigint; + tags: {content: string; tagCategoryId: bigint}[]; +}): Promise { + for (let i = 0; i < tags.length; i++) { + const tag = tags[i]; + const {content, tagCategoryId} = tag; + + // 태그가 빈 값일 경우, 해당 이미지의 태그 상태를 0으로 변경 + if (content === '') { + await inActiveTag(imageId, tagCategoryId); + } + // 태그가 동일한 것이 있을 시, 태그를 반환하고 / 태그가 동일한 것이 없을 시, 태그를 생성 + else { + const tagData = await getTag(content, tagCategoryId); + // await activeTag(imageId, tagCategoryId); + await inActiveTag(imageId, tagCategoryId); + // 태그가 동일한 것이 없으면 생성 + if (!tagData) { + await newTag(imageId, content, tagCategoryId); + } + // 태그가 동일한 것이 있으면 미생성 + else { + await updateTag(imageId, tagData); + } + } + } + return; +} + +export async function inActiveTag( + imageId: bigint | number, + tag_category_id: bigint, +): Promise { + // 이미지 태그 테이블에서 image_id가 일치하는 데이터들을 찾아서, + // 해당 tag_id 값과 태그 테이블의 id 값이 같은 데이터를 태그 테이블에서 찾아서, + // tagCategoryID와 tag_category_id가 일치하는 데이터를 찾음. + // 해당 데이터의 status를 0으로 변경 + const imageData = await prisma.imageTag.findMany({ + where: { + imageId, + }, + select: { + id: true, + tagId: true, + }, + }); + + // bigint 타입을 string 타입으로 변경 + const formattedImageData = imageData.map(item => ({ + id: item.id.toString(), + tagId: item.tagId.toString(), + })); + + for (let j = 0; j < imageData.length; j++) { + const tagData = await prisma.tag.findMany({ + where: { + id: BigInt(formattedImageData[j].tagId), + tagCategoryId: tag_category_id, + }, + select: { + id: true, + }, + }); + + if (tagData) { + const formattedTagData = tagData.map(item => ({ + ...item, + id: item.id.toString(), + })); + for (let k = 0; k < tagData.length; k++) { + await prisma.imageTag.update({ + where: { + id: BigInt(formattedImageData[j].id), + tagId: BigInt(formattedTagData[k].id), + }, + data: { + status: 0, + }, + }); + } + } + } +} + +export async function activeTag( + imageId: bigint | number, + tag_category_id: bigint, +): Promise { + const imageData = await prisma.imageTag.findMany({ + where: { + imageId, + }, + select: { + id: true, + tagId: true, + }, + }); + + // bigint 타입을 string 타입으로 변경 + const formattedImageData = imageData.map(item => ({ + ...item, + id: item.id.toString(), + tagId: item.tagId.toString(), + })); + + for (let j = 0; j < imageData.length; j++) { + const tagData = await prisma.tag.findFirst({ + where: { + id: BigInt(formattedImageData[j].tagId), + tagCategoryId: tag_category_id, + }, + select: { + id: true, + }, + }); + + // bigint 타입을 string 타입으로 변경 + + if (tagData) { + const formattedTagDataId = tagData.id.toString(); + await prisma.imageTag.update({ + where: { + id: BigInt(formattedImageData[j].id), + tagId: BigInt(formattedTagDataId), + }, + data: { + status: 1, + }, + }); + } else { + continue; + } + } +} + +export async function newTag( + imageId: bigint | number, + content: string, + tag_category_id: bigint, +): Promise { + const tagData = await addTag(content, tag_category_id); + + if (tagData === null) { + throw new TagNotFound(); + } // 태그 생성 실패 시 에러 반환 + + const formattedTagDataId = tagData.id.toString(); + + await prisma.imageTag.create({ + data: { + imageId, + tagId: BigInt(formattedTagDataId), + }, + }); +} + +export async function updateTag( + imageId: bigint | number, + tagData: ResponseFromTag, +): Promise { + const formattedTagDataId = tagData.id.toString(); + + // 이미지 테이블에 태그 존재 여부 + const imageData = await prisma.imageTag.findFirst({ + where: { + imageId, + tagId: tagData.id, + }, + select: { + id: true, + }, + }); + + // 비활성화 -> 활성화 + if (imageData) { + const formattedImageDataId = imageData.id.toString(); + await prisma.imageTag.update({ + where: { + id: BigInt(formattedImageDataId), + tagId: BigInt(formattedTagDataId), + }, + data: { + status: 1, + }, + }); + } + // 이미지 태그 테이블에 데이터가 없으면 생성 + else { + await prisma.imageTag.create({ + data: { + imageId, + tagId: BigInt(formattedTagDataId), + }, + }); + } +} + +export async function addTag( + content: string, + tag_category_id: bigint, +): Promise { + const created = await prisma.tag.create({ + data: { + content, + tagCategoryId: tag_category_id, + }, + }); // 태그 생성 + + return created; +} + +export async function getTag( + content: string, + tag_category_id: bigint, +): Promise { + await prisma.tagCategory.findFirst({ + where: { + id: tag_category_id, + }, + }); // 태그 카테고리 조회 + + const tag = await prisma.tag.findFirst({ + where: { + content, + tagCategoryId: tag_category_id, + }, + }); // 태그 조회 + + if (tag === null) { + return null; + } // 태그 조회 실패 시 null 반환 + + return tag; +} + +export async function getImageTag( + imageId: bigint | number, +): Promise { + const imageTagData = await prisma.imageTag.findMany({ + where: { + imageId, + status: 1, + }, + }); + + return imageTagData; +} diff --git a/src/repositories/user.repository.ts b/src/repositories/user.repository.ts new file mode 100644 index 0000000..cda7bf9 --- /dev/null +++ b/src/repositories/user.repository.ts @@ -0,0 +1,41 @@ +import {DBError} from 'src/errors.js'; +import {prisma} from '../db.config.js'; +import {UserModel} from '../models/user.model.js'; + +export const findUserById = async ( + userId: bigint, +): Promise => { + const user = await prisma.user + .findUnique({ + where: {id: userId}, + }) + .catch(err => { + throw new DBError({reason: err.message}); + }); + + return user; +}; + +export const deleteUser = async (userId: bigint): Promise => { + await prisma.user + .update({ + where: {id: userId}, + data: {status: 0}, + }) + .catch(err => { + throw new DBError({reason: err.message}); + }); + return true; +}; + +export const deleteSession = async (sessionId: string): Promise => { + await prisma.session + .delete({ + where: { + sid: sessionId, + }, + }) + .catch(err => { + throw new DBError({reason: err.message}); + }); +}; diff --git a/src/routers/mypage.routers.ts b/src/routers/mypage.routers.ts deleted file mode 100644 index 3b883b6..0000000 --- a/src/routers/mypage.routers.ts +++ /dev/null @@ -1,7 +0,0 @@ -import express from 'express'; -export const myPageRouter = express.Router(); -import {getUser, logOutUser, deleteUser} from '../controllers/user.mypage.controllers.js'; - -myPageRouter.get('/', getUser); -myPageRouter.get('/logout', logOutUser); -myPageRouter.patch('/', deleteUser); diff --git a/src/routers/tag.router.ts b/src/routers/tag.router.ts deleted file mode 100644 index 94afe8d..0000000 --- a/src/routers/tag.router.ts +++ /dev/null @@ -1,5 +0,0 @@ -import express from 'express'; -export const tagRouter = express.Router(); -import {handleNewTag} from '../controllers/tag.controller.js'; - -tagRouter.post('/', handleNewTag); // 태그 생성 API diff --git a/src/routers/trust.router.ts b/src/routers/trust.router.ts deleted file mode 100644 index 285c9d7..0000000 --- a/src/routers/trust.router.ts +++ /dev/null @@ -1,13 +0,0 @@ -import express from 'express'; -export const trustRouter = express.Router(); -import { - handleImageStatus, - handleImageRestore, - handleImageDelete, - handleImage, -} from '../controllers/trust.controller.js'; - -trustRouter.patch('/active', handleImageStatus); // 이미지 비활성화 -trustRouter.patch('/restore', handleImageRestore); //이미지 복구 -trustRouter.delete('/', handleImageDelete); // 비활성화된 이미지 삭제 -trustRouter.get('/in', handleImage); //휴지통에 있는 이미지정보 가져오기 diff --git a/src/routers/tsoaRoutes.ts b/src/routers/tsoaRoutes.ts index 9ca1dfe..f2c50c3 100644 --- a/src/routers/tsoaRoutes.ts +++ b/src/routers/tsoaRoutes.ts @@ -4,6 +4,10 @@ import type { TsoaRoute } from '@tsoa/runtime'; import { fetchMiddlewares, ExpressTemplateService } from '@tsoa/runtime'; // 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 +import { UserController } from './../controllers/user.controller.js'; +// 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 +import { MypageController } from './../controllers/user.controller.js'; +// 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 import { TagsController } from './../controllers/tsoaTag.controller.js'; // 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 import { ImagesController } from './../controllers/tsoaImage.controller.js'; @@ -12,6 +16,8 @@ import { MemoImageController } from './../controllers/tsoa.memo-image.controller // 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 import { MemoFolderController } from './../controllers/tsoa.memo-folder.controller.js'; // 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 +import { TrustController } from './../controllers/trash.controller.js'; +// 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 import { MemoCreateFolderOCRController } from './../controllers/memo-updateFolderOCR.controller.js'; // 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 import { MemoCreateUpdateOCRController } from './../controllers/memo-createFolderOCR.controller.js'; @@ -32,20 +38,26 @@ import type { Request as ExRequest, Response as ExResponse, RequestHandler, Rout // 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 models: TsoaRoute.Models = { - "ResponseTagListWithDate": { + "ResponseUser": { "dataType": "refObject", "properties": { - "tags": {"dataType":"array","array":{"dataType":"string"},"required":true}, + "id": {"dataType":"string","required":true}, + "email": {"dataType":"string","required":true}, + "name": {"dataType":"string","required":true}, + "goalCount": {"dataType":"double","required":true}, + "createdAt": {"dataType":"datetime","required":true}, + "updatedAt": {"dataType":"union","subSchemas":[{"dataType":"datetime"},{"dataType":"enum","enums":[null]}],"required":true}, + "status": {"dataType":"double","required":true}, }, "additionalProperties": false, }, // 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 - "ITsoaSuccessResponse_ResponseTagListWithDate_": { + "ITsoaSuccessResponse_ResponseUser_": { "dataType": "refObject", "properties": { "resultType": {"dataType":"string","required":true}, "error": {"dataType":"enum","enums":[null],"required":true}, - "success": {"ref":"ResponseTagListWithDate","required":true}, + "success": {"ref":"ResponseUser","required":true}, }, "additionalProperties": false, }, @@ -72,6 +84,34 @@ const models: TsoaRoute.Models = { "additionalProperties": false, }, // 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 + "ITsoaSuccessResponse_string_": { + "dataType": "refObject", + "properties": { + "resultType": {"dataType":"string","required":true}, + "error": {"dataType":"enum","enums":[null],"required":true}, + "success": {"dataType":"string","required":true}, + }, + "additionalProperties": false, + }, + // 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 + "ResponseTagListWithDate": { + "dataType": "refObject", + "properties": { + "tags": {"dataType":"array","array":{"dataType":"string"},"required":true}, + }, + "additionalProperties": false, + }, + // 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 + "ITsoaSuccessResponse_ResponseTagListWithDate_": { + "dataType": "refObject", + "properties": { + "resultType": {"dataType":"string","required":true}, + "error": {"dataType":"enum","enums":[null],"required":true}, + "success": {"ref":"ResponseTagListWithDate","required":true}, + }, + "additionalProperties": false, + }, + // 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 "ResponseTagListFromImage": { "dataType": "refObject", "properties": { @@ -90,6 +130,42 @@ const models: TsoaRoute.Models = { "additionalProperties": false, }, // 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 + "ResponseCreationTags": { + "dataType": "refObject", + "properties": { + "tags": {"dataType":"array","array":{"dataType":"nestedObjectLiteral","nestedProperties":{"tagId":{"dataType":"string","required":true},"imageId":{"dataType":"string","required":true},"status":{"dataType":"double","required":true},"updatedAt":{"dataType":"union","subSchemas":[{"dataType":"datetime"},{"dataType":"enum","enums":[null]}],"required":true},"createdAt":{"dataType":"datetime","required":true},"id":{"dataType":"string","required":true}}},"required":true}, + }, + "additionalProperties": false, + }, + // 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 + "ITsoaSuccessResponse_ResponseCreationTags_": { + "dataType": "refObject", + "properties": { + "resultType": {"dataType":"string","required":true}, + "error": {"dataType":"enum","enums":[null],"required":true}, + "success": {"ref":"ResponseCreationTags","required":true}, + }, + "additionalProperties": false, + }, + // 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 + "RequestBodyCreationTags": { + "dataType": "refObject", + "properties": { + "tags": {"dataType":"array","array":{"dataType":"nestedObjectLiteral","nestedProperties":{"tagCategoryId":{"dataType":"string","required":true},"content":{"dataType":"string","required":true}}},"required":true}, + }, + "additionalProperties": false, + }, + // 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 + "ITsoaSuccessResponse__labels_58__description-string--score-number_-Array__": { + "dataType": "refObject", + "properties": { + "resultType": {"dataType":"string","required":true}, + "error": {"dataType":"enum","enums":[null],"required":true}, + "success": {"dataType":"nestedObjectLiteral","nestedProperties":{"labels":{"dataType":"array","array":{"dataType":"nestedObjectLiteral","nestedProperties":{"score":{"dataType":"double","required":true},"description":{"dataType":"string","required":true}}},"required":true}},"required":true}, + }, + "additionalProperties": false, + }, + // 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 "ResponseGetImageListFromTag": { "dataType": "refObject", "properties": { @@ -450,16 +526,6 @@ const models: TsoaRoute.Models = { "additionalProperties": false, }, // 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 - "ITsoaSuccessResponse_string_": { - "dataType": "refObject", - "properties": { - "resultType": {"dataType":"string","required":true}, - "error": {"dataType":"enum","enums":[null],"required":true}, - "success": {"dataType":"string","required":true}, - }, - "additionalProperties": false, - }, - // 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 "ResponseFromGetByUserIdReform": { "dataType": "refObject", "properties": { @@ -507,6 +573,158 @@ export function RegisterRoutes(app: Router) { + const argsUserController_updateUserName: Record = { + req: {"in":"request","name":"req","required":true,"dataType":"object"}, + body: {"in":"body","name":"body","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"name":{"dataType":"string","required":true}}}, + }; + app.patch('/onboarding/name', + ...(fetchMiddlewares(UserController)), + ...(fetchMiddlewares(UserController.prototype.updateUserName)), + + async function UserController_updateUserName(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: argsUserController_updateUserName, request, response }); + + const controller = new UserController(); + + await templateService.apiHandler({ + methodName: 'updateUserName', + controller, + response, + next, + validatedArgs, + successStatus: 201, + }); + } 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 argsUserController_updateUserGoalCount: Record = { + req: {"in":"request","name":"req","required":true,"dataType":"object"}, + body: {"in":"body","name":"body","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"goalCount":{"dataType":"double","required":true}}}, + }; + app.patch('/onboarding/goal', + ...(fetchMiddlewares(UserController)), + ...(fetchMiddlewares(UserController.prototype.updateUserGoalCount)), + + async function UserController_updateUserGoalCount(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: argsUserController_updateUserGoalCount, request, response }); + + const controller = new UserController(); + + await templateService.apiHandler({ + methodName: 'updateUserGoalCount', + controller, + response, + next, + validatedArgs, + successStatus: 201, + }); + } 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 argsMypageController_getUser: Record = { + req: {"in":"request","name":"req","required":true,"dataType":"object"}, + }; + app.get('/user/mypage', + ...(fetchMiddlewares(MypageController)), + ...(fetchMiddlewares(MypageController.prototype.getUser)), + + async function MypageController_getUser(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: argsMypageController_getUser, request, response }); + + const controller = new MypageController(); + + await templateService.apiHandler({ + methodName: 'getUser', + 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 argsMypageController_logOut: Record = { + req: {"in":"request","name":"req","required":true,"dataType":"object"}, + }; + app.patch('/user/mypage/logout', + ...(fetchMiddlewares(MypageController)), + ...(fetchMiddlewares(MypageController.prototype.logOut)), + + async function MypageController_logOut(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: argsMypageController_logOut, request, response }); + + const controller = new MypageController(); + + await templateService.apiHandler({ + methodName: 'logOut', + controller, + response, + next, + validatedArgs, + successStatus: 204, + }); + } 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 argsMypageController_deleteUser: Record = { + req: {"in":"request","name":"req","required":true,"dataType":"object"}, + }; + app.patch('/user/mypage', + ...(fetchMiddlewares(MypageController)), + ...(fetchMiddlewares(MypageController.prototype.deleteUser)), + + async function MypageController_deleteUser(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: argsMypageController_deleteUser, request, response }); + + const controller = new MypageController(); + + await templateService.apiHandler({ + methodName: 'deleteUser', + controller, + response, + next, + validatedArgs, + successStatus: 204, + }); + } 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 argsTagsController_getTagListWithDate: Record = { req: {"in":"request","name":"req","required":true,"dataType":"object"}, year: {"in":"query","name":"year","required":true,"dataType":"double"}, @@ -571,6 +789,67 @@ 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 argsTagsController_postTag: Record = { + imageId: {"in":"path","name":"imageId","required":true,"dataType":"string"}, + body: {"in":"body","name":"body","required":true,"ref":"RequestBodyCreationTags"}, + }; + app.post('/tags/images/:imageId', + ...(fetchMiddlewares(TagsController)), + ...(fetchMiddlewares(TagsController.prototype.postTag)), + + async function TagsController_postTag(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: argsTagsController_postTag, request, response }); + + const controller = new TagsController(); + + await templateService.apiHandler({ + methodName: 'postTag', + controller, + response, + next, + validatedArgs, + successStatus: 201, + }); + } 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 argsTagsController_getLableFromImage: Record = { + req: {"in":"request","name":"req","required":true,"dataType":"object"}, + }; + app.post('/tags/ai', + ...(fetchMiddlewares(TagsController)), + ...(fetchMiddlewares(TagsController.prototype.getLableFromImage)), + + async function TagsController_getLableFromImage(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: argsTagsController_getLableFromImage, request, response }); + + const controller = new TagsController(); + + await templateService.apiHandler({ + methodName: 'getLableFromImage', + controller, + response, + next, + validatedArgs, + successStatus: 201, + }); + } 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 argsImagesController_getImageListFromTag: Record = { req: {"in":"request","name":"req","required":true,"dataType":"object"}, tag: {"in":"query","name":"tag","required":true,"dataType":"string"}, @@ -946,6 +1225,98 @@ 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 argsTrustController_deactivateImage: Record = { + imageId: {"in":"path","name":"imageId","required":true,"dataType":"string"}, + }; + app.patch('/trash/images/:imageId', + ...(fetchMiddlewares(TrustController)), + ...(fetchMiddlewares(TrustController.prototype.deactivateImage)), + + async function TrustController_deactivateImage(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: argsTrustController_deactivateImage, request, response }); + + const controller = new TrustController(); + + await templateService.apiHandler({ + methodName: 'deactivateImage', + controller, + response, + next, + validatedArgs, + successStatus: 201, + }); + } 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 argsTrustController_restoreImages: Record = { + body: {"in":"body","name":"body","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"mediaIdList":{"dataType":"array","array":{"dataType":"string"},"required":true}}}, + req: {"in":"request","name":"req","required":true,"dataType":"object"}, + }; + app.patch('/trash/active', + ...(fetchMiddlewares(TrustController)), + ...(fetchMiddlewares(TrustController.prototype.restoreImages)), + + async function TrustController_restoreImages(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: argsTrustController_restoreImages, request, response }); + + const controller = new TrustController(); + + await templateService.apiHandler({ + methodName: 'restoreImages', + controller, + response, + next, + validatedArgs, + successStatus: 201, + }); + } 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 argsTrustController_deleteImages: Record = { + body: {"in":"body","name":"body","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"mediaIdList":{"dataType":"array","array":{"dataType":"string"},"required":true}}}, + req: {"in":"request","name":"req","required":true,"dataType":"object"}, + }; + app.delete('/trash/images', + ...(fetchMiddlewares(TrustController)), + ...(fetchMiddlewares(TrustController.prototype.deleteImages)), + + async function TrustController_deleteImages(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: argsTrustController_deleteImages, request, response }); + + const controller = new TrustController(); + + await templateService.apiHandler({ + methodName: 'deleteImages', + controller, + response, + next, + validatedArgs, + successStatus: 204, + }); + } 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 argsMemoCreateFolderOCRController_updateFolderOCR: Record = { req: {"in":"request","name":"req","required":true,"dataType":"object"}, folderId: {"in":"path","name":"folderId","required":true,"dataType":"double"}, diff --git a/src/routers/user.router.ts b/src/routers/user.router.ts deleted file mode 100644 index 9feeabf..0000000 --- a/src/routers/user.router.ts +++ /dev/null @@ -1,6 +0,0 @@ -import express from 'express'; -export const userRouter = express.Router(); -import { updateUserNameController, updateUserGoalCountController } from '../controllers/user.controller.js'; - -userRouter.patch('/name', updateUserNameController); -userRouter.patch('/goal', updateUserGoalCountController); \ No newline at end of file diff --git a/src/services/memo-ocrService.ts b/src/services/memo-ocrService.ts index dd257cb..2102b63 100644 --- a/src/services/memo-ocrService.ts +++ b/src/services/memo-ocrService.ts @@ -106,7 +106,10 @@ export const performOCR = async (base64_image: string): Promise => { } return annotations[0].description || '텍스트를 찾을 수 없습니다'; - } catch { + } catch (error) { + if (error instanceof PhotoDataNotFoundError) { + throw error; + } throw new OCRProcessError(); } }; diff --git a/src/services/mypage.service.ts b/src/services/mypage.service.ts index e820d6f..e2fc7da 100644 --- a/src/services/mypage.service.ts +++ b/src/services/mypage.service.ts @@ -1,23 +1,30 @@ -import {userRepository, logoutUserRepository} from '../repositories/mypage.repository.js'; +import { + findUserById, + deleteSession, + deleteUser, +} from '../repositories/user.repository.js'; import {UserModel} from '../models/user.model.js'; import {UserNotFoundError} from '../errors.js'; //사용자 정보 가져오기 -export const getUserById = async (user_id: bigint): Promise => { - const user = await userRepository.findUserById(user_id); - if (!user) { - throw new UserNotFoundError(); +export const getUserById = async (user_id: bigint): Promise => { + const user = await findUserById(user_id).then(result => { + if (!result) { + throw new UserNotFoundError(); } - return user; + return result; + }); + + return user; }; -//사용자 회원 탈퇴 -export const deleteUser = async (user_id: bigint): Promise => { - return await userRepository.deleteUser(user_id); +// //사용자 회원 탈퇴 +export const removeUser = async (user_id: bigint): Promise => { + return await deleteUser(user_id); }; //사용자 로그아웃 export const logoutUserService = async (sessionId: string) => { - try { - await logoutUserRepository(sessionId); // Prisma를 통해 DB에서 세션 삭제 - } catch (error) { - throw error; - } -}; \ No newline at end of file + try { + await deleteSession(sessionId); // Prisma를 통해 DB에서 세션 삭제 + } catch (error) { + throw error; + } +}; diff --git a/src/services/tag.service.ts b/src/services/tag.service.ts index 0102679..52b418a 100644 --- a/src/services/tag.service.ts +++ b/src/services/tag.service.ts @@ -1,6 +1,10 @@ -import {responseFromImageTag} from '../dtos/tag.dto.js'; -import {BodyToTag, ResponseFromImageTag} from '../models/tag.model.js'; +import {RequestCreationTags} from 'src/dtos/tsoaTag.dto.js'; import {addImageTag, getImageTag} from '../repositories/tag.repositories.js'; +import { + ResponseCreationTags, + responseCreationTags, +} from 'src/models/tsoaTag.model.js'; +import {DBError} from 'src/errors.js'; /** * 태그 생성 @@ -9,12 +13,16 @@ import {addImageTag, getImageTag} from '../repositories/tag.repositories.js'; async function tagCreate({ imageId, tags, -}: BodyToTag): Promise { - await addImageTag({imageId, tags}); +}: RequestCreationTags): Promise { + await addImageTag({imageId, tags}).catch(err => { + throw new DBError({reason: err.message}); + }); - const imageTagData = await getImageTag(imageId); + const imageTagData = await getImageTag(imageId).catch(err => { + throw new DBError({reason: err.message}); + }); - return responseFromImageTag(imageTagData); + return responseCreationTags(imageTagData); } export {tagCreate}; diff --git a/src/services/trash.service.ts b/src/services/trash.service.ts new file mode 100644 index 0000000..d49904a --- /dev/null +++ b/src/services/trash.service.ts @@ -0,0 +1,20 @@ +import {DeleteImagesDTO, RestoreImagesDTO} from '../dtos/trash.dto.js'; +import { + removeImages, + updateImageStatus, +} from '../repositories/trash.repository.js'; + +export const deactivateImage = async (imageId: bigint): Promise => { + await updateImageStatus([imageId], 0); + return; +}; + +export const restoreImages = async (dto: RestoreImagesDTO): Promise => { + await updateImageStatus(dto.mediaIdList, 1, dto.userId); + return; +}; + +export const deleteImages = async (dto: DeleteImagesDTO): Promise => { + await removeImages(dto.mediaIdList, dto.userId); + return; +}; diff --git a/src/services/trust.service.ts b/src/services/trust.service.ts deleted file mode 100644 index 60ded4e..0000000 --- a/src/services/trust.service.ts +++ /dev/null @@ -1,25 +0,0 @@ -import * as trustRepository from '../repositories/trust.repositories.js'; - -export const deactivateImages = async (imageId: number): Promise <{imageId: number; status: number}> => { - await trustRepository.updateImageStatus([imageId], 0); - const [updatedImage] = await trustRepository.getImagesByIds([imageId]); - return updatedImage; -}; - -export const restoreImages = async (imageIds: number[]): Promise<{ imageId: number; status: number }[]> => { - await trustRepository.updateImageStatus(imageIds, 1); - return trustRepository.getImagesByIds(imageIds); -}; - -export const deleteImages = async (imageIds: number[]): Promise => { - const images = await trustRepository.getImagesByIds(imageIds); - if (!images.length || images.some(({ status }) => status ===1)){ - return false; - } - await trustRepository.removeImages(imageIds); - return true; -}; - -export const getTrashedImages = async (userId: number): Promise<{ imageId: number; mediaId: number }[]> => { - return trustRepository.getTrashedImages(userId); -}; diff --git a/src/services/tsoaTag.service.ts b/src/services/tsoaTag.service.ts index 6cb5218..0a1f68c 100644 --- a/src/services/tsoaTag.service.ts +++ b/src/services/tsoaTag.service.ts @@ -1,6 +1,16 @@ -import {DateToTags, ImageToTags} from '../dtos/tsoaTag.dto.js'; +import { + responseCreationTags, + ResponseCreationTags, +} from '../models/tsoaTag.model.js'; +import { + DateToTags, + ImageToTags, + RequestCreationTags, +} from '../dtos/tsoaTag.dto.js'; import {TagNotFound} from '../errors.js'; import { + addImageTag, + getImageTag, selectTagsByDate, selectTagsFromImage, } from '../repositories/tsoaTag.repository.js'; @@ -51,3 +61,14 @@ export const findTagsFromImage = async ( ); return tags; }; + +export async function tagCreate({ + imageId, + tags, +}: RequestCreationTags): Promise { + await addImageTag({imageId, tags}); + + const imageTagData = await getImageTag(imageId); + + return responseCreationTags(imageTagData); +} diff --git a/src/services/user.service.ts b/src/services/user.service.ts index 169ed4f..bedf634 100644 --- a/src/services/user.service.ts +++ b/src/services/user.service.ts @@ -1,15 +1,46 @@ -import { prisma } from '../db.config.js'; +import {DBError} from '../errors.js'; +import {prisma} from '../db.config.js'; -export const updateUserName = async (id: number, name: string) => { - return await prisma.user.update({ - where: { id }, - data: { name }, +export const updateUserName = async (id: bigint, name: string) => { + return await prisma.user + .update({ + where: {id}, + data: {name}, + }) + .then(result => { + return { + id: result.id.toString(), + email: result.email, + name: result.name, + goalCount: result.goalCount, + createdAt: result.createdAt, + updatedAt: result.updatedAt, + status: result.status, + }; + }) + .catch(err => { + throw new DBError({reason: err.message}); }); - }; - - export const updateUserGoalCount = async (id: number, goalCount: number) => { - return await prisma.user.update({ - where: { id }, - data: { goalCount }, +}; + +export const updateUserGoalCount = async (id: bigint, goalCount: number) => { + return await prisma.user + .update({ + where: {id}, + data: {goalCount}, + }) + .then(result => { + return { + id: result.id.toString(), + email: result.email, + name: result.name, + goalCount: result.goalCount, + createdAt: result.createdAt, + updatedAt: result.updatedAt, + status: result.status, + }; + }) + .catch(err => { + throw new DBError({reason: err.message}); }); - }; \ No newline at end of file +}; diff --git a/swagger/swagger.json b/swagger/swagger.json index dbb3972..1a9b2ff 100644 --- a/swagger/swagger.json +++ b/swagger/swagger.json @@ -7,22 +7,48 @@ "requestBodies": {}, "responses": {}, "schemas": { - "ResponseTagListWithDate": { + "ResponseUser": { "properties": { - "tags": { - "items": { - "type": "string" - }, - "type": "array" + "id": { + "type": "string" + }, + "email": { + "type": "string" + }, + "name": { + "type": "string" + }, + "goalCount": { + "type": "number", + "format": "double" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "status": { + "type": "number", + "format": "double" } }, "required": [ - "tags" + "id", + "email", + "name", + "goalCount", + "createdAt", + "updatedAt", + "status" ], "type": "object", "additionalProperties": false }, - "ITsoaSuccessResponse_ResponseTagListWithDate_": { + "ITsoaSuccessResponse_ResponseUser_": { "properties": { "resultType": { "type": "string" @@ -35,7 +61,7 @@ "nullable": true }, "success": { - "$ref": "#/components/schemas/ResponseTagListWithDate" + "$ref": "#/components/schemas/ResponseUser" } }, "required": [ @@ -197,6 +223,69 @@ "type": "object", "additionalProperties": false }, + "ITsoaSuccessResponse_string_": { + "properties": { + "resultType": { + "type": "string" + }, + "error": { + "type": "number", + "enum": [ + null + ], + "nullable": true + }, + "success": { + "type": "string" + } + }, + "required": [ + "resultType", + "error", + "success" + ], + "type": "object", + "additionalProperties": false + }, + "ResponseTagListWithDate": { + "properties": { + "tags": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "tags" + ], + "type": "object", + "additionalProperties": false + }, + "ITsoaSuccessResponse_ResponseTagListWithDate_": { + "properties": { + "resultType": { + "type": "string" + }, + "error": { + "type": "number", + "enum": [ + null + ], + "nullable": true + }, + "success": { + "$ref": "#/components/schemas/ResponseTagListWithDate" + } + }, + "required": [ + "resultType", + "error", + "success" + ], + "type": "object", + "additionalProperties": false + }, "ResponseTagListFromImage": { "properties": { "tags": { @@ -260,6 +349,152 @@ "type": "object", "additionalProperties": false }, + "ResponseCreationTags": { + "properties": { + "tags": { + "items": { + "properties": { + "tagId": { + "type": "string" + }, + "imageId": { + "type": "string" + }, + "status": { + "type": "number", + "format": "double" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "id": { + "type": "string" + } + }, + "required": [ + "tagId", + "imageId", + "status", + "updatedAt", + "createdAt", + "id" + ], + "type": "object" + }, + "type": "array" + } + }, + "required": [ + "tags" + ], + "type": "object", + "additionalProperties": false + }, + "ITsoaSuccessResponse_ResponseCreationTags_": { + "properties": { + "resultType": { + "type": "string" + }, + "error": { + "type": "number", + "enum": [ + null + ], + "nullable": true + }, + "success": { + "$ref": "#/components/schemas/ResponseCreationTags" + } + }, + "required": [ + "resultType", + "error", + "success" + ], + "type": "object", + "additionalProperties": false + }, + "RequestBodyCreationTags": { + "properties": { + "tags": { + "items": { + "properties": { + "tagCategoryId": { + "type": "string" + }, + "content": { + "type": "string" + } + }, + "required": [ + "tagCategoryId", + "content" + ], + "type": "object" + }, + "type": "array" + } + }, + "required": [ + "tags" + ], + "type": "object", + "additionalProperties": false + }, + "ITsoaSuccessResponse__labels_58__description-string--score-number_-Array__": { + "properties": { + "resultType": { + "type": "string" + }, + "error": { + "type": "number", + "enum": [ + null + ], + "nullable": true + }, + "success": { + "properties": { + "labels": { + "items": { + "properties": { + "score": { + "type": "number", + "format": "double" + }, + "description": { + "type": "string" + } + }, + "required": [ + "score", + "description" + ], + "type": "object" + }, + "type": "array" + } + }, + "required": [ + "labels" + ], + "type": "object" + } + }, + "required": [ + "resultType", + "error", + "success" + ], + "type": "object", + "additionalProperties": false + }, "ResponseGetImageListFromTag": { "properties": { "images": { @@ -1208,30 +1443,6 @@ "type": "object", "additionalProperties": false }, - "ITsoaSuccessResponse_string_": { - "properties": { - "resultType": { - "type": "string" - }, - "error": { - "type": "number", - "enum": [ - null - ], - "nullable": true - }, - "success": { - "type": "string" - } - }, - "required": [ - "resultType", - "error", - "success" - ], - "type": "object", - "additionalProperties": false - }, "ResponseFromGetByUserIdReform": { "properties": { "id": { @@ -1357,16 +1568,16 @@ } ], "paths": { - "/tags/date": { - "get": { - "operationId": "GetTagListWithDate", + "/onboarding/name": { + "patch": { + "operationId": "UpdateUserName", "responses": { - "200": { - "description": "OK", + "201": { + "description": "CREATED", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ITsoaSuccessResponse_ResponseTagListWithDate_" + "$ref": "#/components/schemas/ITsoaSuccessResponse_ResponseUser_" } } } @@ -1386,9 +1597,9 @@ "errorCode": "VAL-001", "reason": "Validation Error", "data": { - "year": { - "message": "invalid float", - "value": "a" + "name": { + "message": "name is required", + "value": null } } }, @@ -1399,8 +1610,8 @@ } } }, - "404": { - "description": "Not Found", + "500": { + "description": "Internal Server Error", "content": { "application/json": { "schema": { @@ -1411,31 +1622,8 @@ "value": { "resultType": "FAIL", "error": { - "errorCode": "TAG-001", - "reason": "태그가 없습니다.", - "data": null - }, - "success": null - } - } - } - } - } - }, - "500": { - "description": "Internal Server Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ITsoaErrorResponse" - }, - "examples": { - "Example 1": { - "value": { - "resultType": "FAIL", - "error": { - "errorCode": "SER-001", - "reason": "내부 서버 오류입니다.", + "errorCode": "DB-001", + "reason": "DB 에러 입니다.", "data": { "reason": "에러원인 메시지" } @@ -1447,8 +1635,8 @@ "value": { "resultType": "FAIL", "error": { - "errorCode": "DB-001", - "reason": "DB 에러 입니다.", + "errorCode": "SER-001", + "reason": "내부 서버 오류입니다.", "data": { "reason": "에러원인 메시지" } @@ -1461,56 +1649,43 @@ } } }, - "description": "사용자가 날짜를 입력하면 해당 날짜에 찍은 사진들의 태그를 검색합니다.\r\n\r\ndate를 입력하지 않으면 해당 월에 찍은 사진들의 태그를 검색합니다.", - "summary": "날짜기반 태그 검색 API", + "description": "로그인한 사용자의 이름을 변경하는 API입니다.", + "summary": "사용자 이름 변경 API", "tags": [ - "Tag" + "User" ], "security": [], - "parameters": [ - { - "description": "년", - "in": "query", - "name": "year", - "required": true, - "schema": { - "format": "double", - "type": "number" - } - }, - { - "description": "월", - "in": "query", - "name": "month", - "required": true, - "schema": { - "format": "double", - "type": "number" - } - }, - { - "description": "일", - "in": "query", - "name": "date", - "required": false, - "schema": { - "format": "double", - "type": "number" + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + } } } - ] + } } }, - "/tags/images/{mediaId}": { - "get": { - "operationId": "GetTagListFromImage", + "/onboarding/goal": { + "patch": { + "operationId": "UpdateUserGoalCount", "responses": { - "200": { - "description": "OK", + "201": { + "description": "CREATED", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ITsoaSuccessResponse_ResponseTagListFromImage_" + "$ref": "#/components/schemas/ITsoaSuccessResponse_ResponseUser_" } } } @@ -1530,34 +1705,27 @@ "errorCode": "VAL-001", "reason": "Validation Error", "data": { - "mediaId": { - "message": "invalid float", - "value": "a" + "goalCount": { + "message": "goalCount is required", + "value": null } } }, "success": null } - } - } - } - } - }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ITsoaErrorResponse" - }, - "examples": { - "Example 1": { + }, + "Example 2": { "value": { "resultType": "FAIL", "error": { - "errorCode": "TAG-001", - "reason": "태그가 없습니다.", - "data": null + "errorCode": "VAL-001", + "reason": "Validation Error", + "data": { + "goalCount": { + "message": "goalCount is unsigned Integer", + "value": -8 + } + } }, "success": null } @@ -1578,8 +1746,8 @@ "value": { "resultType": "FAIL", "error": { - "errorCode": "SER-001", - "reason": "내부 서버 오류입니다.", + "errorCode": "DB-001", + "reason": "DB 에러 입니다.", "data": { "reason": "에러원인 메시지" } @@ -1591,8 +1759,8 @@ "value": { "resultType": "FAIL", "error": { - "errorCode": "DB-001", - "reason": "DB 에러 입니다.", + "errorCode": "SER-001", + "reason": "내부 서버 오류입니다.", "data": { "reason": "에러원인 메시지" } @@ -1605,63 +1773,46 @@ } } }, - "description": "사용자가 mediaId를 제공하면 해당 이미지와 연관된 태그들의 정보를 제공합니다.", - "summary": "이미지의 태그 조회", + "description": "로그인한 사용자의 목표 장수를 변경하는 API입니다.", + "summary": "사용자 목표 장수 변경 API", "tags": [ - "Tag" + "User" ], "security": [], - "parameters": [ - { - "description": "이미지의 고유 mediaId", - "in": "path", - "name": "mediaId", - "required": true, - "schema": { - "format": "double", - "type": "number" + "parameters": [], + "requestBody": { + "description": "변경할 목표 장수", + "required": true, + "content": { + "application/json": { + "schema": { + "properties": { + "goalCount": { + "type": "number", + "format": "double" + } + }, + "required": [ + "goalCount" + ], + "type": "object", + "description": "변경할 목표 장수" + } } } - ] + } } }, - "/images": { + "/user/mypage": { "get": { - "operationId": "GetImageListFromTag", + "operationId": "GetUser", "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ITsoaSuccessResponse_ResponseGetImageListFromTag_" - } - } - } - }, - "400": { - "description": "Bad Request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ITsoaErrorResponse" - }, - "examples": { - "Example 1": { - "value": { - "resultType": "FAIL", - "error": { - "errorCode": "VAL-001", - "reason": "Validation Error", - "data": { - "tag": { - "message": "\"tag\" is required" - } - } - }, - "success": null - } - } + "$ref": "#/components/schemas/ITsoaSuccessResponse_ResponseUser_" } } } @@ -1678,8 +1829,8 @@ "value": { "resultType": "FAIL", "error": { - "errorCode": "PHO-404", - "reason": "<인하대학교> 태그에 해당하는 사진이 존재하지 않습니다.", + "errorCode": "USR-404", + "reason": "사용자를 찾을수 없습니다", "data": null }, "success": null @@ -1728,39 +1879,29 @@ } } }, - "description": "사용자가 tag를 입력하면 해당 태그에 속하는 이미지들을 가져옵니다.", - "summary": "태그기반 이미지 검색", + "description": "사용자 정보를 가져오는 API 입니다.", + "summary": "사용자 정보 가져오기", "tags": [ - "Image" + "User" ], "security": [], - "parameters": [ - { - "description": "태그 내용", - "in": "query", - "name": "tag", - "required": true, - "schema": { - "type": "string" - } - } - ] + "parameters": [] }, - "post": { - "operationId": "CreateImage", + "patch": { + "operationId": "DeleteUser", "responses": { - "201": { - "description": "CREATED", + "204": { + "description": "회원 탈퇴", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ITsoaSuccessResponse_ResponseCreateImage_" + "$ref": "#/components/schemas/ITsoaSuccessResponse_string_" } } } }, - "400": { - "description": "Bad Request", + "404": { + "description": "Not Found", "content": { "application/json": { "schema": { @@ -1771,26 +1912,10 @@ "value": { "resultType": "FAIL", "error": { - "errorCode": "VAL-001", - "reason": "Validation Error", - "data": { - "mediaId": { - "message": "invalid float", - "value": "a" - } - } - }, - "success": null - } - }, - "Example 2": { - "value": { - "resultType": "FAIL", - "error": { - "errorCode": "PHO-400", - "reason": "사진 데이터가 유효하지 않습니다.", + "errorCode": "AUT-401", + "reason": "인증 실패", "data": { - "reason": "이미 휴지통에 있는 사진입니다" + "reason": "user를 찾을 수 없습니다." } }, "success": null @@ -1809,19 +1934,6 @@ }, "examples": { "Example 1": { - "value": { - "resultType": "FAIL", - "error": { - "errorCode": "SER-001", - "reason": "내부 서버 오류입니다.", - "data": { - "reason": "에러원인 메시지" - } - }, - "success": null - } - }, - "Example 2": { "value": { "resultType": "FAIL", "error": { @@ -1839,50 +1951,1105 @@ } } }, - "description": "mediaId에 해당하는 사진데이터가 존재하면 해당 사진데이터의 imageId를 보여줍니다.(조회)\r\n\r\nmediaId에 해당하는 사진데이터가 존재하지 않는다면 사진데이터를 생성하고 생성한 사진데이터의 imageId를 보여줍니다.(생성)", - "summary": "이미지 데이터 생성 및 조회", + "description": "사용자 상태를 비활성화하는 API 입니다.", + "summary": "회원탈퇴", "tags": [ - "Image" + "User" ], "security": [], - "parameters": [], - "requestBody": { - "description": "미디어ID, 사진 생성일", - "required": true, - "content": { - "application/json": { - "schema": { - "properties": { - "timestamp": { - "type": "string", - "format": "date-time" - }, - "mediaId": { + "parameters": [] + } + }, + "/user/mypage/logout": { + "patch": { + "operationId": "LogOut", + "responses": { + "204": { + "description": "LOGOUT", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ITsoaSuccessResponse_string_" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ITsoaErrorResponse" + }, + "examples": { + "Example 1": { + "value": { + "resultType": "FAIL", + "error": { + "errorCode": "AUT-401", + "reason": "인증 실패", + "data": { + "reason": "Session ID가 존재하지 않습니다" + } + }, + "success": null + } + } + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ITsoaErrorResponse" + }, + "examples": { + "Example 1": { + "value": { + "resultType": "FAIL", + "error": { + "errorCode": "SER-001", + "reason": "내부 서버 오류입니다.", + "data": { + "reason": "에러원인 메시지" + } + }, + "success": null + } + }, + "Example 2": { + "value": { + "resultType": "FAIL", + "error": { + "errorCode": "DB-001", + "reason": "DB 에러 입니다.", + "data": { + "reason": "에러원인 메시지" + } + }, + "success": null + } + } + } + } + } + } + }, + "description": "사용자 로그아웃 API 입니다.", + "summary": "사용자 로그아웃 API", + "tags": [ + "User" + ], + "security": [], + "parameters": [] + } + }, + "/tags/date": { + "get": { + "operationId": "GetTagListWithDate", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ITsoaSuccessResponse_ResponseTagListWithDate_" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ITsoaErrorResponse" + }, + "examples": { + "Example 1": { + "value": { + "resultType": "FAIL", + "error": { + "errorCode": "VAL-001", + "reason": "Validation Error", + "data": { + "year": { + "message": "invalid float", + "value": "a" + } + } + }, + "success": null + } + } + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ITsoaErrorResponse" + }, + "examples": { + "Example 1": { + "value": { + "resultType": "FAIL", + "error": { + "errorCode": "TAG-001", + "reason": "태그가 없습니다.", + "data": null + }, + "success": null + } + } + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ITsoaErrorResponse" + }, + "examples": { + "Example 1": { + "value": { + "resultType": "FAIL", + "error": { + "errorCode": "SER-001", + "reason": "내부 서버 오류입니다.", + "data": { + "reason": "에러원인 메시지" + } + }, + "success": null + } + }, + "Example 2": { + "value": { + "resultType": "FAIL", + "error": { + "errorCode": "DB-001", + "reason": "DB 에러 입니다.", + "data": { + "reason": "에러원인 메시지" + } + }, + "success": null + } + } + } + } + } + } + }, + "description": "사용자가 날짜를 입력하면 해당 날짜에 찍은 사진들의 태그를 검색합니다.\r\n\r\ndate를 입력하지 않으면 해당 월에 찍은 사진들의 태그를 검색합니다.", + "summary": "날짜기반 태그 검색 API", + "tags": [ + "Tag" + ], + "security": [], + "parameters": [ + { + "description": "년", + "in": "query", + "name": "year", + "required": true, + "schema": { + "format": "double", + "type": "number" + } + }, + { + "description": "월", + "in": "query", + "name": "month", + "required": true, + "schema": { + "format": "double", + "type": "number" + } + }, + { + "description": "일", + "in": "query", + "name": "date", + "required": false, + "schema": { + "format": "double", + "type": "number" + } + } + ] + } + }, + "/tags/images/{mediaId}": { + "get": { + "operationId": "GetTagListFromImage", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ITsoaSuccessResponse_ResponseTagListFromImage_" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ITsoaErrorResponse" + }, + "examples": { + "Example 1": { + "value": { + "resultType": "FAIL", + "error": { + "errorCode": "VAL-001", + "reason": "Validation Error", + "data": { + "mediaId": { + "message": "invalid float", + "value": "a" + } + } + }, + "success": null + } + } + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ITsoaErrorResponse" + }, + "examples": { + "Example 1": { + "value": { + "resultType": "FAIL", + "error": { + "errorCode": "TAG-001", + "reason": "태그가 없습니다.", + "data": null + }, + "success": null + } + } + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ITsoaErrorResponse" + }, + "examples": { + "Example 1": { + "value": { + "resultType": "FAIL", + "error": { + "errorCode": "SER-001", + "reason": "내부 서버 오류입니다.", + "data": { + "reason": "에러원인 메시지" + } + }, + "success": null + } + }, + "Example 2": { + "value": { + "resultType": "FAIL", + "error": { + "errorCode": "DB-001", + "reason": "DB 에러 입니다.", + "data": { + "reason": "에러원인 메시지" + } + }, + "success": null + } + } + } + } + } + } + }, + "description": "사용자가 mediaId를 제공하면 해당 이미지와 연관된 태그들의 정보를 제공합니다.", + "summary": "이미지의 태그 조회", + "tags": [ + "Tag" + ], + "security": [], + "parameters": [ + { + "description": "이미지의 고유 mediaId", + "in": "path", + "name": "mediaId", + "required": true, + "schema": { + "format": "double", + "type": "number" + } + } + ] + } + }, + "/tags/images/{imageId}": { + "post": { + "operationId": "PostTag", + "responses": { + "201": { + "description": "CREATED", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ITsoaSuccessResponse_ResponseCreationTags_" + } + } + } + }, + "500": { + "description": "Internal Server Error" + } + }, + "description": "태그를 생성하는 API 입니다.", + "summary": "태그 생성", + "tags": [ + "Tag" + ], + "security": [], + "parameters": [ + { + "description": "이미지ID", + "in": "path", + "name": "imageId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "생성할 태그들", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestBodyCreationTags", + "description": "생성할 태그들" + } + } + } + } + } + }, + "/images": { + "get": { + "operationId": "GetImageListFromTag", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ITsoaSuccessResponse_ResponseGetImageListFromTag_" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ITsoaErrorResponse" + }, + "examples": { + "Example 1": { + "value": { + "resultType": "FAIL", + "error": { + "errorCode": "VAL-001", + "reason": "Validation Error", + "data": { + "tag": { + "message": "\"tag\" is required" + } + } + }, + "success": null + } + } + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ITsoaErrorResponse" + }, + "examples": { + "Example 1": { + "value": { + "resultType": "FAIL", + "error": { + "errorCode": "PHO-404", + "reason": "<인하대학교> 태그에 해당하는 사진이 존재하지 않습니다.", + "data": null + }, + "success": null + } + } + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ITsoaErrorResponse" + }, + "examples": { + "Example 1": { + "value": { + "resultType": "FAIL", + "error": { + "errorCode": "SER-001", + "reason": "내부 서버 오류입니다.", + "data": { + "reason": "에러원인 메시지" + } + }, + "success": null + } + }, + "Example 2": { + "value": { + "resultType": "FAIL", + "error": { + "errorCode": "DB-001", + "reason": "DB 에러 입니다.", + "data": { + "reason": "에러원인 메시지" + } + }, + "success": null + } + } + } + } + } + } + }, + "description": "사용자가 tag를 입력하면 해당 태그에 속하는 이미지들을 가져옵니다.", + "summary": "태그기반 이미지 검색", + "tags": [ + "Image" + ], + "security": [], + "parameters": [ + { + "description": "태그 내용", + "in": "query", + "name": "tag", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "post": { + "operationId": "CreateImage", + "responses": { + "201": { + "description": "CREATED", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ITsoaSuccessResponse_ResponseCreateImage_" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ITsoaErrorResponse" + }, + "examples": { + "Example 1": { + "value": { + "resultType": "FAIL", + "error": { + "errorCode": "VAL-001", + "reason": "Validation Error", + "data": { + "mediaId": { + "message": "invalid float", + "value": "a" + } + } + }, + "success": null + } + }, + "Example 2": { + "value": { + "resultType": "FAIL", + "error": { + "errorCode": "PHO-400", + "reason": "사진 데이터가 유효하지 않습니다.", + "data": { + "reason": "이미 휴지통에 있는 사진입니다" + } + }, + "success": null + } + } + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ITsoaErrorResponse" + }, + "examples": { + "Example 1": { + "value": { + "resultType": "FAIL", + "error": { + "errorCode": "SER-001", + "reason": "내부 서버 오류입니다.", + "data": { + "reason": "에러원인 메시지" + } + }, + "success": null + } + }, + "Example 2": { + "value": { + "resultType": "FAIL", + "error": { + "errorCode": "DB-001", + "reason": "DB 에러 입니다.", + "data": { + "reason": "에러원인 메시지" + } + }, + "success": null + } + } + } + } + } + } + }, + "description": "mediaId에 해당하는 사진데이터가 존재하면 해당 사진데이터의 imageId를 보여줍니다.(조회)\r\n\r\nmediaId에 해당하는 사진데이터가 존재하지 않는다면 사진데이터를 생성하고 생성한 사진데이터의 imageId를 보여줍니다.(생성)", + "summary": "이미지 데이터 생성 및 조회", + "tags": [ + "Image" + ], + "security": [], + "parameters": [], + "requestBody": { + "description": "미디어ID, 사진 생성일", + "required": true, + "content": { + "application/json": { + "schema": { + "properties": { + "timestamp": { + "type": "string", + "format": "date-time" + }, + "mediaId": { "type": "string" } }, - "required": [ - "timestamp", - "mediaId" - ], - "type": "object", - "description": "미디어ID, 사진 생성일" + "required": [ + "timestamp", + "mediaId" + ], + "type": "object", + "description": "미디어ID, 사진 생성일" + } + } + } + } + } + }, + "/memo/folders/{folderId}/images": { + "patch": { + "operationId": "HandlerMemoImageMove", + "responses": { + "200": { + "description": "사진 이동 성공 응답", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ITsoaSuccessResponse_MemoTextImageListResponseDto_" + }, + "examples": { + "Example 1": { + "value": { + "resultType": "SUCCESS", + "error": null, + "success": { + "folderId": "1", + "folderName": "string", + "imageText": "string", + "images": [ + { + "imageId": "1", + "imageUrl": "string" + } + ] + } + } + } + } + } + } + }, + "400": { + "description": "유효하지 않은 데이터 조회 에러", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ITsoaErrorResponse" + }, + "examples": { + "Example 1": { + "value": { + "resultType": "FAIL", + "error": { + "errorCode": "MEM-400", + "reason": "메모 사진 이동 중 오류가 발생했습니다.", + "data": { + "folderId": "1", + "imageId": [ + "1" + ] + } + }, + "success": null + } + } + } + } + } + }, + "404": { + "description": "존재하지 않은 데이터 조회 에러", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ITsoaErrorResponse" + }, + "examples": { + "Example 1": { + "value": { + "resultType": "FAIL", + "error": { + "errorCode": "FOL-404", + "reason": "해당 폴더를 찾을 수 없습니다.", + "data": { + "folderId": "1" + } + }, + "success": null + } + }, + "Example 2": { + "value": { + "resultType": "FAIL", + "error": { + "errorCode": "PHO-404", + "reason": "해당 사진 데이터가 없습니다.", + "data": { + "imageId": "1" + } + }, + "success": null + } + } + } + } + } + } + }, + "description": "특정 폴더의 사진을 이동하는 API입니다.", + "summary": "사진 이동 API", + "tags": [ + "Memo-image" + ], + "security": [], + "parameters": [ + { + "description": "현재 폴더 ID", + "in": "path", + "name": "folderId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "이동할 폴더 ID, 이동할 사진 배열", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BodyToMemoImagesToMove", + "description": "이동할 폴더 ID, 이동할 사진 배열" + } + } + } + } + } + }, + "/memo/folders/{folderId}": { + "delete": { + "operationId": "HandlerMemoFolderDelete", + "responses": { + "200": { + "description": "폴더 삭제 성공 응답", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ITsoaSuccessResponse_ResponseMessage_" + }, + "examples": { + "Example 1": { + "value": { + "resultType": "SUCCESS", + "error": null, + "success": { + "message": "string" + } + } + } + } + } + } + }, + "404": { + "description": "존재하지 않은 데이터 조회 에러", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ITsoaErrorResponse" + }, + "examples": { + "Example 1": { + "value": { + "resultType": "FAIL", + "error": { + "errorCode": "FOL-404", + "reason": "해당 폴더를 찾을 수 없습니다.", + "data": { + "folderId": "1" + } + }, + "success": null + } + } + } + } + } + } + }, + "description": "특정 폴더의 사진을 삭제하는 API입니다.", + "summary": "폴더 삭제 API", + "tags": [ + "Memo-folder" + ], + "security": [], + "parameters": [ + { + "description": "폴더 ID", + "in": "path", + "name": "folderId", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "get": { + "operationId": "HandlerMemoTextImageList", + "responses": { + "200": { + "description": "메모 조회 성공 응답", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ITsoaSuccessResponse_MemoTextImageListResponseDto_" + }, + "examples": { + "Example 1": { + "value": { + "resultType": "SUCCESS", + "error": null, + "success": { + "folderId": "1", + "folderName": "string", + "imageText": "string", + "images": [ + { + "imageId": "1", + "imageUrl": "string" + } + ] + } + } + } + } + } + } + }, + "404": { + "description": "존재하지 않은 데이터 조회 에러", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ITsoaErrorResponse" + }, + "examples": { + "Example 1": { + "value": { + "resultType": "FAIL", + "error": { + "errorCode": "FOL-404", + "reason": "해당 폴더를 찾을 수 없습니다.", + "data": { + "folderId": "1" + } + }, + "success": null + } + } + } + } + } + } + }, + "description": "특정 폴더의 모든 메모(텍스트 및 사진)을 조회하는 API입니다.", + "summary": "특정 폴더의 메모 조회 API", + "tags": [ + "Memo-folder" + ], + "security": [], + "parameters": [ + { + "description": "폴더 ID", + "in": "path", + "name": "folderId", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "patch": { + "operationId": "HandlerMemoFolderUpdate", + "responses": { + "200": { + "description": "폴더 이름 수정 성공 응답", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ITsoaSuccessResponse_MemoTextImageListResponseDto_" + }, + "examples": { + "Example 1": { + "value": { + "resultType": "SUCCESS", + "error": null, + "success": { + "folderId": "1", + "folderName": "string", + "imageText": "string", + "images": [ + { + "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": { + "folderId": "1" + } + } + } + }, + "Example 2": { + "value": { + "resultType": "FAIL", + "success": null, + "error": { + "errorCode": "FOL-400", + "reason": "변경 전의 폴더 이름과 같습니다.", + "data": { + "folderName": "string" + } + } + } + } + } + } + } + }, + "404": { + "description": "존재하지 않은 데이터 조회 에러", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ITsoaErrorResponse" + }, + "examples": { + "Example 1": { + "value": { + "resultType": "FAIL", + "error": { + "errorCode": "FOL-404", + "reason": "해당 폴더를 찾을 수 없습니다.", + "data": { + "folderId": "1" + } + }, + "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" + ], + "security": [], + "parameters": [ + { + "description": "폴더 ID", + "in": "path", + "name": "folderId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "폴더 이름 수정 내용", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BodyToMemoFolder", + "description": "폴더 이름 수정 내용" } } } } } }, - "/memo/folders/{folderId}/images": { - "patch": { - "operationId": "HandlerMemoImageMove", + "/memo/folders": { + "post": { + "operationId": "HandlerMemoFolderAdd", "responses": { "200": { - "description": "사진 이동 성공 응답", + "description": "폴더 생성 성공 응답", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ITsoaSuccessResponse_MemoTextImageListResponseDto_" + "$ref": "#/components/schemas/ITsoaSuccessResponse_MemoFolderResponseDto_" }, "examples": { "Example 1": { @@ -1890,15 +3057,8 @@ "resultType": "SUCCESS", "error": null, "success": { - "folderId": "1", - "folderName": "string", - "imageText": "string", - "images": [ - { - "imageId": "1", - "imageUrl": "string" - } - ] + "id": "1", + "folderName": "string" } } } @@ -1907,7 +3067,7 @@ } }, "400": { - "description": "유효하지 않은 데이터 조회 에러", + "description": "유효하지 않은 데이터 에러", "content": { "application/json": { "schema": { @@ -1917,25 +3077,23 @@ "Example 1": { "value": { "resultType": "FAIL", + "success": null, "error": { - "errorCode": "MEM-400", - "reason": "메모 사진 이동 중 오류가 발생했습니다.", + "errorCode": "FOL-400", + "reason": "폴더 생성 중 오류가 발생했습니다.", "data": { - "folderId": "1", - "imageId": [ - "1" - ] + "userId": "1", + "folderName": "string" } - }, - "success": null + } } } } } } }, - "404": { - "description": "존재하지 않은 데이터 조회 에러", + "409": { + "description": "중복 데이터 에러", "content": { "application/json": { "schema": { @@ -1945,27 +3103,14 @@ "Example 1": { "value": { "resultType": "FAIL", + "success": null, "error": { - "errorCode": "FOL-404", - "reason": "해당 폴더를 찾을 수 없습니다.", - "data": { - "folderId": "1" - } - }, - "success": null - } - }, - "Example 2": { - "value": { - "resultType": "FAIL", - "error": { - "errorCode": "PHO-404", - "reason": "해당 사진 데이터가 없습니다.", + "errorCode": "FOL-409", + "reason": "이미 존재하는 폴더 이름입니다.", "data": { - "imageId": "1" + "folderName": "string" } - }, - "success": null + } } } } @@ -1973,47 +3118,37 @@ } } }, - "description": "특정 폴더의 사진을 이동하는 API입니다.", - "summary": "사진 이동 API", + "description": "폴더를 생성하는 API입니다.", + "summary": "폴더 생성 API", "tags": [ - "memo-image-controller" + "Memo-folder" ], "security": [], - "parameters": [ - { - "description": "현재 폴더 ID", - "in": "path", - "name": "folderId", - "required": true, - "schema": { - "type": "string" - } - } - ], + "parameters": [], "requestBody": { - "description": "이동할 폴더 ID, 이동할 사진 배열", + "description": "생성할 폴더 이름", "required": true, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BodyToMemoImagesToMove", - "description": "이동할 폴더 ID, 이동할 사진 배열" + "$ref": "#/components/schemas/BodyToMemoFolder", + "description": "생성할 폴더 이름" } } } } } }, - "/memo/folders/{folderId}": { - "delete": { - "operationId": "HandlerMemoFolderDelete", + "/memo/list": { + "get": { + "operationId": "HandlerMemoFolderList", "responses": { "200": { - "description": "폴더 삭제 성공 응답", + "description": "메모 조회 성공 응답", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ITsoaSuccessResponse_ResponseMessage_" + "$ref": "#/components/schemas/ITsoaSuccessResponse__data-MemoFolderListResponseDto-Array__" }, "examples": { "Example 1": { @@ -2021,67 +3156,44 @@ "resultType": "SUCCESS", "error": null, "success": { - "message": "string" + "data": [ + { + "folderId": "1", + "folderName": "string", + "imageCount": 0, + "imageText": "string", + "firstImageId": "1", + "firstImageUrl": "string", + "createdAt": "2025-01-17T03:50:25.923Z" + } + ] } } } } } } - }, - "404": { - "description": "존재하지 않은 데이터 조회 에러", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ITsoaErrorResponse" - }, - "examples": { - "Example 1": { - "value": { - "resultType": "FAIL", - "error": { - "errorCode": "FOL-404", - "reason": "해당 폴더를 찾을 수 없습니다.", - "data": { - "folderId": "1" - } - }, - "success": null - } - } - } - } - } } }, - "description": "특정 폴더의 사진을 삭제하는 API입니다.", - "summary": "폴더 삭제 API", + "description": "모든 메모를 조회하는 API입니다.", + "summary": "모든 메모 조회 API", "tags": [ - "memo-folder-controller" + "Memo-folder" ], "security": [], - "parameters": [ - { - "description": "폴더 ID", - "in": "path", - "name": "folderId", - "required": true, - "schema": { - "type": "string" - } - } - ] - }, + "parameters": [] + } + }, + "/memo/search": { "get": { - "operationId": "HandlerMemoTextImageList", + "operationId": "HandlerMemoSearch", "responses": { "200": { - "description": "메모 조회 성공 응답", + "description": "메모 검색 성공 응답", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ITsoaSuccessResponse_MemoTextImageListResponseDto_" + "$ref": "#/components/schemas/ITsoaSuccessResponse__data-MemoFolderListResponseDto-Array__" }, "examples": { "Example 1": { @@ -2089,13 +3201,15 @@ "resultType": "SUCCESS", "error": null, "success": { - "folderId": "1", - "folderName": "string", - "imageText": "string", - "images": [ + "data": [ { - "imageId": "1", - "imageUrl": "string" + "folderId": "1", + "folderName": "string", + "imageCount": 0, + "imageText": "string", + "firstImageId": "1", + "firstImageUrl": "string", + "createdAt": "2025-01-17T03:50:25.923Z" } ] } @@ -2105,8 +3219,8 @@ } } }, - "404": { - "description": "존재하지 않은 데이터 조회 에러", + "400": { + "description": "유효하지 않은 검색 키워드 에러", "content": { "application/json": { "schema": { @@ -2116,14 +3230,14 @@ "Example 1": { "value": { "resultType": "FAIL", + "success": null, "error": { - "errorCode": "FOL-404", - "reason": "해당 폴더를 찾을 수 없습니다.", + "errorCode": "SRH-400", + "reason": "입력 데이터가 유효하지 않습니다.", "data": { - "folderId": "1" + "reason": "검색어를 1자 이상 입력하세요." } - }, - "success": null + } } } } @@ -2131,29 +3245,31 @@ } } }, - "description": "특정 폴더의 모든 메모(텍스트 및 사진)을 조회하는 API입니다.", - "summary": "특정 폴더의 메모 조회 API", + "description": "메모를 검색 및 조회하는 API입니다.", + "summary": "메모 검색 API", "tags": [ - "memo-folder-controller" + "Memo-folder" ], "security": [], "parameters": [ { - "description": "폴더 ID", - "in": "path", - "name": "folderId", + "description": "검색 키워드", + "in": "query", + "name": "keyword", "required": true, "schema": { "type": "string" } } ] - }, - "patch": { - "operationId": "HandlerMemoFolderUpdate", + } + }, + "/memo/folders/{folderId}/images/delete": { + "post": { + "operationId": "HandlerMemoImageDelete", "responses": { "200": { - "description": "폴더 이름 수정 성공 응답", + "description": "사진 삭제 성공 응답", "content": { "application/json": { "schema": { @@ -2181,44 +3297,6 @@ } } }, - "400": { - "description": "유효하지 않은 데이터 에러", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ITsoaErrorResponse" - }, - "examples": { - "Example 1": { - "value": { - "resultType": "FAIL", - "success": null, - "error": { - "errorCode": "FOL-400", - "reason": "폴더 업데이트 중 오류가 발생했습니다.", - "data": { - "folderId": "1" - } - } - } - }, - "Example 2": { - "value": { - "resultType": "FAIL", - "success": null, - "error": { - "errorCode": "FOL-400", - "reason": "변경 전의 폴더 이름과 같습니다.", - "data": { - "folderName": "string" - } - } - } - } - } - } - } - }, "404": { "description": "존재하지 않은 데이터 조회 에러", "content": { @@ -2237,32 +3315,20 @@ "folderId": "1" } }, - "success": null - } - } - } - } - } - }, - "409": { - "description": "중복 데이터 에러", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ITsoaErrorResponse" - }, - "examples": { - "Example 1": { + "success": null + } + }, + "Example 2": { "value": { "resultType": "FAIL", - "success": null, "error": { - "errorCode": "FOL-409", - "reason": "이미 존재하는 폴더 이름입니다.", + "errorCode": "PHO-404", + "reason": "해당 사진 데이터가 없습니다.", "data": { - "folderName": "string" + "imageId": "1" } - } + }, + "success": null } } } @@ -2270,10 +3336,10 @@ } } }, - "description": "특정 폴더의 이름을 수정하는 API입니다.", - "summary": "메모 폴더 이름 수정 API", + "description": "특정 폴더의 사진을 삭제하는 API입니다.", + "summary": "사진 삭제 API", "tags": [ - "memo-folder-controller" + "Memo-image" ], "security": [], "parameters": [ @@ -2288,29 +3354,29 @@ } ], "requestBody": { - "description": "폴더 이름 수정 내용", + "description": "이동할 사진 ID 배열", "required": true, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BodyToMemoFolder", - "description": "폴더 이름 수정 내용" + "$ref": "#/components/schemas/BodyToMemoImagesToDelete", + "description": "이동할 사진 ID 배열" } } } } } }, - "/memo/folders": { - "post": { - "operationId": "HandlerMemoFolderAdd", + "/memo/folders/{folderId}/text": { + "patch": { + "operationId": "HandlerMemoTextUpdate", "responses": { "200": { - "description": "폴더 생성 성공 응답", + "description": "메모 텍스트 수정 성공 응답", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ITsoaSuccessResponse_MemoFolderResponseDto_" + "$ref": "#/components/schemas/ITsoaSuccessResponse_MemoTextImageListResponseDto_" }, "examples": { "Example 1": { @@ -2318,34 +3384,15 @@ "resultType": "SUCCESS", "error": null, "success": { - "id": "1", - "folderName": "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" - } + "folderId": "1", + "folderName": "string", + "imageText": "string", + "images": [ + { + "imageId": "1", + "imageUrl": "string" + } + ] } } } @@ -2353,8 +3400,8 @@ } } }, - "409": { - "description": "중복 데이터 에러", + "404": { + "description": "존재하지 않은 데이터 조회 에러", "content": { "application/json": { "schema": { @@ -2366,10 +3413,10 @@ "resultType": "FAIL", "success": null, "error": { - "errorCode": "FOL-409", - "reason": "이미 존재하는 폴더 이름입니다.", + "errorCode": "FOL-404", + "reason": "해당 폴더를 찾을 수 없습니다.", "data": { - "folderName": "string" + "folderId": "1" } } } @@ -2379,109 +3426,53 @@ } } }, - "description": "폴더를 생성하는 API입니다.", - "summary": "폴더 생성 API", + "description": "특정 폴더의 메모 텍스트를 수정하는 API입니다.", + "summary": "특정 폴더의 메모 텍스트 수정 API", "tags": [ - "memo-folder-controller" + "Memo-folder" ], "security": [], - "parameters": [], + "parameters": [ + { + "description": "폴더 ID", + "in": "path", + "name": "folderId", + "required": true, + "schema": { + "type": "string" + } + } + ], "requestBody": { - "description": "생성할 폴더 이름", + "description": "메모 텍스트 수정 내용", "required": true, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BodyToMemoFolder", - "description": "생성할 폴더 이름" + "$ref": "#/components/schemas/BodyToMemoTextToUpdate", + "description": "메모 텍스트 수정 내용" } } } } } }, - "/memo/list": { - "get": { - "operationId": "HandlerMemoFolderList", - "responses": { - "200": { - "description": "메모 조회 성공 응답", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ITsoaSuccessResponse__data-MemoFolderListResponseDto-Array__" - }, - "examples": { - "Example 1": { - "value": { - "resultType": "SUCCESS", - "error": null, - "success": { - "data": [ - { - "folderId": "1", - "folderName": "string", - "imageCount": 0, - "imageText": "string", - "firstImageId": "1", - "firstImageUrl": "string", - "createdAt": "2025-01-17T03:50:25.923Z" - } - ] - } - } - } - } - } - } - } - }, - "description": "모든 메모를 조회하는 API입니다.", - "summary": "모든 메모 조회 API", - "tags": [ - "memo-folder-controller" - ], - "security": [], - "parameters": [] - } - }, - "/memo/search": { - "get": { - "operationId": "HandlerMemoSearch", + "/trash/images/{imageId}": { + "patch": { + "operationId": "DeactivateImage", "responses": { - "200": { - "description": "메모 검색 성공 응답", + "201": { + "description": "CREATED", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ITsoaSuccessResponse__data-MemoFolderListResponseDto-Array__" - }, - "examples": { - "Example 1": { - "value": { - "resultType": "SUCCESS", - "error": null, - "success": { - "data": [ - { - "folderId": "1", - "folderName": "string", - "imageCount": 0, - "imageText": "string", - "firstImageId": "1", - "firstImageUrl": "string", - "createdAt": "2025-01-17T03:50:25.923Z" - } - ] - } - } - } + "$ref": "#/components/schemas/ITsoaSuccessResponse_string_" } } } }, - "400": { - "description": "유효하지 않은 검색 키워드 에러", + "500": { + "description": "Internal Server Error", "content": { "application/json": { "schema": { @@ -2491,14 +3482,27 @@ "Example 1": { "value": { "resultType": "FAIL", - "success": null, "error": { - "errorCode": "SRH-400", - "reason": "입력 데이터가 유효하지 않습니다.", + "errorCode": "DB-001", + "reason": "DB 에러 입니다.", "data": { - "reason": "검색어를 1자 이상 입력하세요." + "reason": "에러원인 메시지" } - } + }, + "success": null + } + }, + "Example 2": { + "value": { + "resultType": "FAIL", + "error": { + "errorCode": "SER-001", + "reason": "내부 서버 오류입니다.", + "data": { + "reason": "에러원인 메시지" + } + }, + "success": null } } } @@ -2506,17 +3510,17 @@ } } }, - "description": "메모를 검색 및 조회하는 API입니다.", - "summary": "메모 검색 API", + "description": "휴지통에 넣고싶은 imageId를 입력하시면 해당 이미지는 비활성화 됩니다.", + "summary": "휴지통에 이미지 넣기", "tags": [ - "memo-folder-controller" + "Trash" ], "security": [], "parameters": [ { - "description": "검색 키워드", - "in": "query", - "name": "keyword", + "description": "버릴 이미지ID", + "in": "path", + "name": "imageId", "required": true, "schema": { "type": "string" @@ -2525,41 +3529,50 @@ ] } }, - "/memo/folders/{folderId}/images/delete": { - "post": { - "operationId": "HandlerMemoImageDelete", + "/trash/active": { + "patch": { + "operationId": "RestoreImages", "responses": { - "200": { - "description": "사진 삭제 성공 응답", + "201": { + "description": "CREATED", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ITsoaSuccessResponse_MemoTextImageListResponseDto_" + "$ref": "#/components/schemas/ITsoaSuccessResponse_string_" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ITsoaErrorResponse" }, "examples": { "Example 1": { "value": { - "resultType": "SUCCESS", - "error": null, - "success": { - "folderId": "1", - "folderName": "string", - "imageText": "string", - "images": [ - { - "imageId": "1", - "imageUrl": "string" + "resultType": "FAIL", + "error": { + "errorCode": "VAL-001", + "reason": "잘못된 요청입니다.", + "data": { + "mediaIdList": { + "message": "하나이상의 복구할 사진을 선택해주세요.", + "value": [] } - ] - } + } + }, + "success": null } } } } } }, - "404": { - "description": "존재하지 않은 데이터 조회 에러", + "500": { + "description": "Internal Server Error", "content": { "application/json": { "schema": { @@ -2570,10 +3583,10 @@ "value": { "resultType": "FAIL", "error": { - "errorCode": "FOL-404", - "reason": "해당 폴더를 찾을 수 없습니다.", + "errorCode": "DB-001", + "reason": "DB 에러 입니다.", "data": { - "folderId": "1" + "reason": "에러원인 메시지" } }, "success": null @@ -2583,10 +3596,10 @@ "value": { "resultType": "FAIL", "error": { - "errorCode": "PHO-404", - "reason": "해당 사진 데이터가 없습니다.", + "errorCode": "SER-001", + "reason": "내부 서버 오류입니다.", "data": { - "imageId": "1" + "reason": "에러원인 메시지" } }, "success": null @@ -2597,72 +3610,82 @@ } } }, - "description": "특정 폴더의 사진을 삭제하는 API입니다.", - "summary": "사진 삭제 API", + "description": "휴지통에서 선택된 이미지들을 복원하는 API 입니다.", + "summary": "휴지통 복원 API", "tags": [ - "memo-image-controller" + "Trash" ], "security": [], - "parameters": [ - { - "description": "폴더 ID", - "in": "path", - "name": "folderId", - "required": true, - "schema": { - "type": "string" - } - } - ], + "parameters": [], "requestBody": { - "description": "이동할 사진 ID 배열", + "description": "복구하고 싶은 이미지들의 mediaId", "required": true, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BodyToMemoImagesToDelete", - "description": "이동할 사진 ID 배열" + "properties": { + "mediaIdList": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "mediaIdList" + ], + "type": "object", + "description": "복구하고 싶은 이미지들의 mediaId" } } } } } }, - "/memo/folders/{folderId}/text": { - "patch": { - "operationId": "HandlerMemoTextUpdate", + "/trash/images": { + "delete": { + "operationId": "DeleteImages", "responses": { - "200": { - "description": "메모 텍스트 수정 성공 응답", + "204": { + "description": "DELETED", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ITsoaSuccessResponse_MemoTextImageListResponseDto_" + "$ref": "#/components/schemas/ITsoaSuccessResponse_string_" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ITsoaErrorResponse" }, "examples": { "Example 1": { "value": { - "resultType": "SUCCESS", - "error": null, - "success": { - "folderId": "1", - "folderName": "string", - "imageText": "string", - "images": [ - { - "imageId": "1", - "imageUrl": "string" + "resultType": "FAIL", + "error": { + "errorCode": "VAL-001", + "reason": "잘못된 요청입니다.", + "data": { + "mediaIdList": { + "message": "하나이상의 복구할 사진을 선택해주세요.", + "value": [] } - ] - } + } + }, + "success": null } } } } } }, - "404": { - "description": "존재하지 않은 데이터 조회 에러", + "500": { + "description": "Internal Server Error", "content": { "application/json": { "schema": { @@ -2672,14 +3695,27 @@ "Example 1": { "value": { "resultType": "FAIL", - "success": null, "error": { - "errorCode": "FOL-404", - "reason": "해당 폴더를 찾을 수 없습니다.", + "errorCode": "DB-001", + "reason": "DB 에러 입니다.", "data": { - "folderId": "1" + "reason": "에러원인 메시지" } - } + }, + "success": null + } + }, + "Example 2": { + "value": { + "resultType": "FAIL", + "error": { + "errorCode": "SER-001", + "reason": "내부 서버 오류입니다.", + "data": { + "reason": "에러원인 메시지" + } + }, + "success": null } } } @@ -2687,31 +3723,32 @@ } } }, - "description": "특정 폴더의 메모 텍스트를 수정하는 API입니다.", - "summary": "특정 폴더의 메모 텍스트 수정 API", + "description": "저장된 이미지들의 데이터를 삭제하는 API 입니다.", + "summary": "이미지데이터 삭제 API", "tags": [ - "memo-folder-controller" + "Trash" ], "security": [], - "parameters": [ - { - "description": "폴더 ID", - "in": "path", - "name": "folderId", - "required": true, - "schema": { - "type": "string" - } - } - ], + "parameters": [], "requestBody": { - "description": "메모 텍스트 수정 내용", + "description": "삭제하고싶은 이미지들의 mediaId", "required": true, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BodyToMemoTextToUpdate", - "description": "메모 텍스트 수정 내용" + "properties": { + "mediaIdList": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "mediaIdList" + ], + "type": "object", + "description": "삭제하고싶은 이미지들의 mediaId" } } } @@ -3243,7 +4280,7 @@ "description": "날짜 챌린지를 생성하는 API 입니다.", "summary": "날짜 챌린지 생성 API", "tags": [ - "date challenge controller" + "Date-challenge" ], "security": [], "parameters": [], @@ -3354,7 +4391,7 @@ "description": "날짜 챌린지 조회 API입니다.", "summary": "날짜 챌린지 조회 API", "tags": [ - "date challenge controller" + "Date-challenge" ], "security": [], "parameters": [ @@ -3445,7 +4482,7 @@ "description": "위치 챌린지를 생성하는 API 입니다.", "summary": "위치 챌린지 생성 API", "tags": [ - "location challenge controller" + "Location-challenge" ], "security": [], "parameters": [], @@ -3555,7 +4592,7 @@ "description": "위치 챌린지를 id로 조회하는 API입니다.", "summary": "위치 챌린지 조회 API", "tags": [ - "location challenge controller" + "Location-challenge" ], "security": [], "parameters": [ @@ -3657,7 +4694,7 @@ "description": "사진들의 위치 챌린지 조건을 판별하는 로직입니다.", "summary": "위치 챌린지 판별 API", "tags": [ - "location challenge controller" + "Location-challenge" ], "security": [], "parameters": [], @@ -3780,7 +4817,7 @@ "description": "챌린지의 남은 장수와 목표 장수를 수정하는 API입니다.", "summary": "챌린지 수정 API", "tags": [ - "challenge controller" + "Challenge" ], "security": [], "parameters": [], @@ -3891,7 +4928,7 @@ "description": "챌린지를 삭제하는 API입니다.", "summary": "챌린지 삭제 API", "tags": [ - "challenge controller" + "Challenge" ], "security": [], "parameters": [ @@ -3982,7 +5019,7 @@ "description": "챌린지를 수락하는 API입니다.\r\nparameter에 id값을 적으면 challenge의 status가 2로 변합니다.\r\n1 = 생성된 챌린지, 2 = 수락된 챌린지, 3 = 완료된 챌린지", "summary": "챌린지 수락 API", "tags": [ - "challenge controller" + "Challenge" ], "security": [], "parameters": [ @@ -4073,7 +5110,7 @@ "description": "챌린지를 완료하는 API입니다.", "summary": "챌린지 완료 API", "tags": [ - "challenge controller" + "Challenge" ], "security": [], "parameters": [ @@ -4164,7 +5201,7 @@ "description": "로그인 되어 있는 유저의 모든 챌린지들을 불러옵니다.", "summary": "유저 챌린지 조회", "tags": [ - "challenge controller" + "Challenge" ], "security": [], "parameters": [] @@ -4245,7 +5282,7 @@ "description": "지오해쉬 값을 입력하면 주소로 변환해줍니다.", "summary": "주소 변환 API", "tags": [ - "challenge controller" + "Challenge" ], "security": [], "parameters": [ @@ -4370,7 +5407,7 @@ "description": "특정 폴더에 사진을 저장하는 API입니다.", "summary": "사진 저장 API", "tags": [ - "memo-image-controller" + "Memo-image" ], "security": [], "parameters": [ @@ -4548,7 +5585,7 @@ "description": "기존 폴더에 이미지를 추가하고, OCR 텍스트를 업데이트하는 API입니다.", "summary": "폴더 업데이트 및 OCR 수행", "tags": [ - "memo-ai" + "Memo-AI" ], "security": [], "parameters": [ @@ -4570,7 +5607,7 @@ "schema": { "type": "object", "properties": { - "base64_image": { + "image": { "type": "string", "format": "binary", "description": "OCR 처리를 위한 이미지 업로드" @@ -4726,7 +5763,7 @@ "description": "새로운 폴더를 생성하고, 이미지에서 OCR 텍스트를 추출하여 이미지와 텍스트를 저장하는 API입니다.", "summary": "폴더 생성 및 OCR 수행", "tags": [ - "memo-ai" + "Memo-AI" ], "security": [], "parameters": [], @@ -4744,7 +5781,7 @@ "type": "string", "description": "생성할 폴더의 이름" }, - "base64_image": { + "image": { "type": "string", "format": "binary", "description": "OCR 처리를 위한 이미지 업로드" @@ -4755,6 +5792,150 @@ } } } + }, + "/tags/ai": { + "post": { + "operationId": "GetLableFromImage", + "responses": { + "201": { + "description": "CREATED", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ITsoaSuccessResponse__labels_58__description-string--score-number_-Array__" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ITsoaErrorResponse" + }, + "examples": { + "Example 1": { + "value": { + "resultType": "FAIL", + "error": { + "errorCode": "PHO-400", + "reason": "사진 데이터가 유효하지 않습니다.", + "data": { + "reason": "올바른 Base64 이미지 형식이 아닙니다." + } + }, + "success": null + } + } + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ITsoaErrorResponse" + }, + "examples": { + "Example 1": { + "value": { + "resultType": "FAIL", + "error": { + "errorCode": "PHO-404", + "reason": "해당 사진 데이터가 없습니다.", + "data": { + "reason": "라벨링을 추출 할 사진이 없습니다." + } + }, + "success": null + } + }, + "Example 2": { + "value": { + "resultType": "FAIL", + "error": { + "errorCode": "LBL-405", + "reason": "이미지에서 라벨을 감지하지 못했습니다.", + "data": { + "reason": "해당 이미지는 정보가 부족합니다." + } + }, + "success": null + } + } + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ITsoaErrorResponse" + }, + "examples": { + "Example 1": { + "value": { + "resultType": "FAIL", + "error": { + "errorCode": "LBL-500", + "reason": "라벨링 처리 중 오류가 발생했습니다.", + "data": { + "reason": "라벨링 처리 중 오류가 발생했습니다." + } + }, + "success": null + } + }, + "Example 2": { + "value": { + "resultType": "FAIL", + "error": { + "errorCode": "SER-001", + "reason": "내부 서버 오류입니다.", + "data": { + "reason": "에러원인 메시지" + } + }, + "success": null + } + } + } + } + } + } + }, + "description": "이미지를 분석하여 태그 추천 라벨을 생성합니다.", + "summary": "이미지 분석 태그생성 API", + "tags": [ + "Tag" + ], + "security": [], + "parameters": [], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "required": [ + "image" + ], + "type": "object", + "properties": { + "image": { + "type": "string", + "format": "binary", + "description": "태그라벨 자동생성을 위한 이미지 업로드" + } + } + } + } + } + } + } } } } \ No newline at end of file