diff --git a/src/controllers/challenge.controller.ts b/src/controllers/challenge.controller.ts index 798b1a3..e147fa9 100644 --- a/src/controllers/challenge.controller.ts +++ b/src/controllers/challenge.controller.ts @@ -12,9 +12,11 @@ import { Get, Query, Response, + Post, } from 'tsoa'; import { serviceAcceptChallenge, + serviceChallengeImageUpload, serviceCompleteChallenge, serviceDeleteChallenge, serviceGetByUserId, @@ -438,4 +440,78 @@ export class ChallengeController extends Controller { return new TsoaSuccessResponse(result); } + + /** + * 챌린지의 이미지들을 업로드합니다. + * + * @summary 챌린지 이미지 업로드 API + * @param challengeId 챌린지 ID + * @param req + * @param body 이미지ID의 배열(string[]) + * @returns 업로드 성공 유무 + */ + @Post('/images/upload/:challengeId') + @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: 'CHL-400', + reason: '이미지 업로드 중 문제가 발생했습니다.', + data: null, + }, + success: null, + }) + @Response(StatusCodes.NOT_FOUND, 'Not Found', { + resultType: 'FAIL', + error: { + errorCode: 'CHL-404', + reason: '이미지가 서버에 존재하지 않습니다.', + 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 handleChallengeImageUpload( + @Path('challengeId') challengeId: string, + @Request() req: ExpressRequest, + @Body() body: string[] + ): Promise>{ + if (!req.user) { + throw new DataValidationError({reason: 'req.user 정보가 없습니다.'}); + } + + await serviceChallengeImageUpload(body, challengeId, req.user.id) + .then(() => {return;}) + .catch(err => { + if (!(err instanceof BaseError)) { + throw new ServerError(); + } else { + throw err; + } + }); + + return new TsoaSuccessResponse(`${challengeId}챌린지 이미지 업로드 성공`); + } } diff --git a/src/dtos/challenge.dtos.ts b/src/dtos/challenge.dtos.ts index 8674157..c49763a 100644 --- a/src/dtos/challenge.dtos.ts +++ b/src/dtos/challenge.dtos.ts @@ -83,15 +83,21 @@ export const responseFromGetByUserId = ( ): ResponseFromGetByUserIdReform[] => { return challenges.map((value: ResponseFromGetByUserId) => { const {id, title, context, requiredCount, remainingCount, userId, - createdAt, updatedAt, acceptedAt, completedAt, status, locationChallenge, dateChallenge + createdAt, updatedAt, acceptedAt, completedAt, status, locationChallenge, dateChallenge, images } = value; + const imageList: string[] = images.map((value: {image: {mediaId: bigint}}) => + { + return value.image.mediaId.toString(); + }); + return { id: id.toString(), title, context, challengeLocation: locationChallenge?.challengeLocation, challengeDate: dateChallenge?.challengeDate, + images: imageList, requiredCount, remainingCount, userId: userId.toString(), @@ -128,4 +134,16 @@ export const bodyToWeeklyCreation = (data: BodyToWeeklyCreation, userId: bigint) challengeDate, required }; +}; + +export const challengeImageUplaodBody = (imageIdList: {id: bigint}[], challengeId: bigint): { + imageId: bigint, + challengeId: bigint +}[] => { + return imageIdList.map((value: {id: bigint}) => { + return { + imageId: value.id, + challengeId + }; + }); }; \ No newline at end of file diff --git a/src/errors.ts b/src/errors.ts index 127c2fd..489b17d 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -266,6 +266,20 @@ export class DateChallengeNotFoundError extends BaseError { } } +// 챌린지 이미지 업로드 에러 +export class ChallengeImageUploadError extends BaseError { + constructor(details: {reason: string}) { + super(400, 'CHL-400', '이미지 업로드 중 문제가 발생했습니다.', details); + } +} + +// 챌린지 이미지 존재하지 않음 에러 +export class ChallengeImageMissingError extends BaseError { + constructor(details: {reason: string}) { + super(404, 'CHL-404', '이미지가 서버에 존재하지 않습니다.', details); + } +} + // 네이버 API 관련 에러 export class NaverGeoCodeError extends BaseError { constructor(details: {reason: string}) { diff --git a/src/models/challenge.entities.ts b/src/models/challenge.entities.ts index 574a561..e829b01 100644 --- a/src/models/challenge.entities.ts +++ b/src/models/challenge.entities.ts @@ -90,6 +90,9 @@ export interface ResponseFromGetByUserId { dateChallenge: { challengeDate: Date; } | null; + images: { + image: {mediaId: bigint} + }[]; id: bigint; userId: bigint; @@ -111,6 +114,7 @@ export interface ResponseFromGetByUserIdReform { context: string; challengeLocation: string | undefined; challengeDate: Date | undefined; + images: string[]; requiredCount: number; remainingCount: number; createdAt: Date; diff --git a/src/repositories/challenge.repositories.ts b/src/repositories/challenge.repositories.ts index 1a921a5..e81f88a 100644 --- a/src/repositories/challenge.repositories.ts +++ b/src/repositories/challenge.repositories.ts @@ -1,8 +1,11 @@ +import { challengeImageUplaodBody } from 'src/dtos/challenge.dtos.js'; import {prisma} from '../db.config.js'; import { ChallengeAcceptError, ChallengeCompleteError, ChallengeDeletionError, + ChallengeImageMissingError, + ChallengeImageUploadError, ChallengeUpdateError } from '../errors.js'; import { @@ -39,8 +42,6 @@ export const challengeExist = async (userId: bigint): Promise => { } }); - console.log(isExistChallenge); - if(isExistChallenge){ return true; } @@ -216,8 +217,59 @@ export const getChallengeByUserId = async ( challengeDate: true, }, }, + images: { + select: { + image: { + select: { + mediaId: true + } + } + } + } }, }); + //console.log(challenges[0].images); + return challenges; }; + +export const challengeImageUpload = async ( + imageIdList: bigint[], + challengeId: bigint, + userId: bigint +): Promise<{count: number}> => { + const duplicateChallenge = await prisma.challengeImage.findFirst({ + where: { + challengeId: challengeId + } + }); + + if(duplicateChallenge){ + throw new ChallengeImageUploadError({reason: `${challengeId}챌린지에 이미지가 이미 존재합니다.`}); + } + + const foundImage = await prisma.image.findMany({ + where: { + mediaId: { + in: imageIdList + }, + userId: userId + }, + select: { + id: true + } + }); + + if(foundImage.length !== imageIdList.length){ + throw new ChallengeImageMissingError({reason: '서버에 존재하지 않는 이미지가 있습니다.'}); + } + + const inputData = challengeImageUplaodBody(foundImage, challengeId); + + const upload = await prisma.challengeImage.createMany({ + data: inputData + }); + + return upload; +}; \ No newline at end of file diff --git a/src/routers/tsoaRoutes.ts b/src/routers/tsoaRoutes.ts index f2c50c3..fe10a09 100644 --- a/src/routers/tsoaRoutes.ts +++ b/src/routers/tsoaRoutes.ts @@ -535,6 +535,7 @@ const models: TsoaRoute.Models = { "context": {"dataType":"string","required":true}, "challengeLocation": {"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}],"required":true}, "challengeDate": {"dataType":"union","subSchemas":[{"dataType":"datetime"},{"dataType":"undefined"}],"required":true}, + "images": {"dataType":"array","array":{"dataType":"string"},"required":true}, "requiredCount": {"dataType":"double","required":true}, "remainingCount": {"dataType":"double","required":true}, "createdAt": {"dataType":"datetime","required":true}, @@ -1842,6 +1843,38 @@ 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 argsChallengeController_handleChallengeImageUpload: Record = { + challengeId: {"in":"path","name":"challengeId","required":true,"dataType":"string"}, + req: {"in":"request","name":"req","required":true,"dataType":"object"}, + body: {"in":"body","name":"body","required":true,"dataType":"array","array":{"dataType":"string"}}, + }; + app.post('/challenge/images/upload/:challengeId', + ...(fetchMiddlewares(ChallengeController)), + ...(fetchMiddlewares(ChallengeController.prototype.handleChallengeImageUpload)), + + async function ChallengeController_handleChallengeImageUpload(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: argsChallengeController_handleChallengeImageUpload, request, response }); + + const controller = new ChallengeController(); + + await templateService.apiHandler({ + methodName: 'handleChallengeImageUpload', + 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 // 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 diff --git a/src/services/challenge.services.ts b/src/services/challenge.services.ts index 22cc688..fa46f46 100644 --- a/src/services/challenge.services.ts +++ b/src/services/challenge.services.ts @@ -16,6 +16,7 @@ import { acceptChallenge, getChallengeByUserId, completeChallenge, + challengeImageUpload, } from '../repositories/challenge.repositories.js'; import { ChallengeUpdateError, @@ -107,3 +108,18 @@ export const serviceGetByUserId = async ( throw error; } }; + +export const serviceChallengeImageUpload = async ( + imageIdList: string[], + challengeId: string, + userId: bigint +): Promise => { + const imageList: bigint[] = imageIdList.map((value: string) => {return BigInt(value);}); + const challenge: bigint = BigInt(challengeId); + + const result: {count: number}= await challengeImageUpload(imageList, challenge, userId); + + if(imageIdList.length !== result.count){ + throw new Error('챌린지에 이미지 업로드 중 문제가 생겼습니다.'); + } +}; \ No newline at end of file diff --git a/swagger/swagger.json b/swagger/swagger.json index 1a9b2ff..30e467e 100644 --- a/swagger/swagger.json +++ b/swagger/swagger.json @@ -1464,6 +1464,12 @@ "type": "string", "format": "date-time" }, + "images": { + "items": { + "type": "string" + }, + "type": "array" + }, "requiredCount": { "type": "number", "format": "double" @@ -1501,6 +1507,7 @@ "userId", "title", "context", + "images", "requiredCount", "remainingCount", "createdAt", @@ -1563,7 +1570,7 @@ "description": "Sweepic local server" }, { - "url": "http://3.37.137.212:3000", + "url": "https://sweepic.store", "description": "Sweepic server" } ], @@ -5298,6 +5305,135 @@ ] } }, + "/challenge/images/upload/{challengeId}": { + "post": { + "operationId": "HandleChallengeImageUpload", + "responses": { + "200": { + "description": "챌린지 이미지 업로드 성공 응답", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ITsoaSuccessResponse_string_" + } + } + } + }, + "400": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ITsoaErrorResponse" + }, + "examples": { + "Example 1": { + "value": { + "resultType": "FAIL", + "error": { + "errorCode": "SRH-400", + "reason": "req.user 정보가 없습니다.", + "data": null + }, + "success": null + } + }, + "Example 2": { + "value": { + "resultType": "FAIL", + "error": { + "errorCode": "CHL-400", + "reason": "이미지 업로드 중 문제가 발생했습니다.", + "data": null + }, + "success": null + } + } + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ITsoaErrorResponse" + }, + "examples": { + "Example 1": { + "value": { + "resultType": "FAIL", + "error": { + "errorCode": "CHL-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": null + }, + "success": null + } + } + } + } + } + } + }, + "description": "챌린지의 이미지들을 업로드합니다.", + "summary": "챌린지 이미지 업로드 API", + "tags": [ + "Challenge" + ], + "security": [], + "parameters": [ + { + "description": "챌린지 ID", + "in": "path", + "name": "challengeId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "이미지ID의 배열(string[])", + "required": true, + "content": { + "application/json": { + "schema": { + "items": { + "type": "string" + }, + "type": "array", + "description": "이미지ID의 배열(string[])" + } + } + } + } + } + }, "/memo/image-format/folders/{folderId}": { "post": { "operationId": "HandlerMemoImageAdd",