diff --git a/BE/musicspot/src/common/decorator/objectId.decorator.ts b/BE/musicspot/src/common/decorator/objectId.decorator.ts new file mode 100644 index 0000000..c077e66 --- /dev/null +++ b/BE/musicspot/src/common/decorator/objectId.decorator.ts @@ -0,0 +1,26 @@ +import { + registerDecorator, + ValidationOptions, + ValidationArguments, +} from 'class-validator'; +import { is1DArray, is2DArray } from '../util/coordinate.util'; +import mongoose from 'mongoose'; + +export function IsObjectId(validationOptions?: ValidationOptions) { + return function (object: Object, propertyName: string) { + registerDecorator({ + name: 'isObjectId', + target: object.constructor, + propertyName: propertyName, + options: validationOptions, + validator: { + validate(receiveValue: string, args: ValidationArguments) { + if (!mongoose.Types.ObjectId.isValid(receiveValue)) { + return false; + } + return true; + }, + }, + }); + }; +} diff --git a/BE/musicspot/src/filters/exception.filter.ts b/BE/musicspot/src/filters/exception.filter.ts index da89eb5..3f286ae 100644 --- a/BE/musicspot/src/filters/exception.filter.ts +++ b/BE/musicspot/src/filters/exception.filter.ts @@ -8,7 +8,7 @@ export class AllExceptionFilter implements ExceptionFilter { const req = ctx.getRequest(); const res = ctx.getResponse(); const err = exception; - console.log(err); + console.log(err.message); const { status, response } = err; let json = { method: req.method, diff --git a/BE/musicspot/src/journey/controller/journey.controller.ts b/BE/musicspot/src/journey/controller/journey.controller.ts index 441afbe..0cd7ee9 100644 --- a/BE/musicspot/src/journey/controller/journey.controller.ts +++ b/BE/musicspot/src/journey/controller/journey.controller.ts @@ -7,6 +7,7 @@ import { Get, Query, Param, + Delete, } from '@nestjs/common'; import { JourneyService } from '../service/journey.service'; @@ -33,6 +34,7 @@ import { RecordJourneyResDTO, } from '../dto/journeyRecord/journeyRecord.dto'; import { StartJourneyResDTO } from '../dto/journeyStart/journeyStart.dto'; +import { DeleteJourneyReqDTO } from '../dto/journeyDelete.dto'; @Controller('journey') @ApiTags('journey 관련 API') @@ -125,20 +127,6 @@ export class JourneyController { return await this.journeyService.checkJourney(checkJourneyDTO); } - // @ApiOperation({ - // summary: '여정 조회 API', - // description: '해당 범위 내의 여정들을 반환합니다.', - // }) - // @ApiCreatedResponse({ - // description: '범위에 있는 여정의 기록들을 반환', - // type: CheckJourneyResDTO, - // }) - // @Post('check') - // @UsePipes(ValidationPipe) //유효성 체크 - // async checkPost(@Body() checkJourneyDTO: CheckJourneyReqDTO) { - // return await this.journeyService.checkJourney(checkJourneyDTO); - // } - @ApiOperation({ summary: '최근 여정 조회 API', description: '진행 중인 여정이 있었는 지 확인', @@ -152,8 +140,43 @@ export class JourneyController { return await this.journeyService.loadLastJourney(userId); } + @ApiOperation({ + summary: '여정 조회 API', + description: 'journey id를 통해 여정을 조회', + }) + @ApiCreatedResponse({ + description: 'journey id에 해당하는 여정을 반환', + type: [Journey], + }) @Get(':journeyId') async getJourneyById(@Param('journeyId') journeyId: string) { return await this.journeyService.getJourneyById(journeyId); } + + @ApiOperation({ + summary: '여정 삭제 api', + description: 'journey id에 따른 여정 삭제', + }) + @ApiCreatedResponse({ + description: '삭제된 여정을 반환', + type: Journey, + }) + @Delete('') + async deleteJourneyById(@Body() deleteJourneyDto: DeleteJourneyReqDTO) { + return await this.journeyService.deleteJourneyById(deleteJourneyDto); + } } + +// @ApiOperation({ +// summary: '여정 조회 API', +// description: '해당 범위 내의 여정들을 반환합니다.', +// }) +// @ApiCreatedResponse({ +// description: '범위에 있는 여정의 기록들을 반환', +// type: CheckJourneyResDTO, +// }) +// @Post('check') +// @UsePipes(ValidationPipe) //유효성 체크 +// async checkPost(@Body() checkJourneyDTO: CheckJourneyReqDTO) { +// return await this.journeyService.checkJourney(checkJourneyDTO); +// } diff --git a/BE/musicspot/src/journey/dto/journeyCheck/journeyCheck.dto.ts b/BE/musicspot/src/journey/dto/journeyCheck/journeyCheck.dto.ts index 3cb8a85..a8e76f1 100644 --- a/BE/musicspot/src/journey/dto/journeyCheck/journeyCheck.dto.ts +++ b/BE/musicspot/src/journey/dto/journeyCheck/journeyCheck.dto.ts @@ -1,12 +1,12 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsArray, IsNotEmpty, IsString, IsUUID } from 'class-validator'; +import { IsArray, IsNotEmpty, IsUUID } from 'class-validator'; import { UUID } from 'crypto'; import { IsCoordinate } from 'src/common/decorator/coordinate.decorator'; export class CheckJourneyReqDTO { @IsNotEmpty() @ApiProperty({ - example: '655efda2fdc81cae36d20650', + example: 'ACB46D2C-44D7-444F-84C5-4EF7E81E12E', description: '유저 id', required: true, }) @@ -22,7 +22,10 @@ export class CheckJourneyReqDTO { required: true, }) @IsNotEmpty() - @IsArray() + @IsCoordinate({ + message: + '위치 좌표는 2개의 숫자와 각각의 범위를 만족해야합니다.(-90~90 , -180~180)', + }) readonly minCoordinate: number[]; @ApiProperty({ diff --git a/BE/musicspot/src/journey/dto/journeyDelete.dto.ts b/BE/musicspot/src/journey/dto/journeyDelete.dto.ts new file mode 100644 index 0000000..54a1a4e --- /dev/null +++ b/BE/musicspot/src/journey/dto/journeyDelete.dto.ts @@ -0,0 +1,53 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsString, IsDateString } from 'class-validator'; +import { UUID } from 'crypto'; +export class DeleteJourneyReqDTO { + @ApiProperty({ + example: 'ab4068ef-95ed-40c3-be6d-3db35df866b9', + description: '사용자 id', + required: true, + }) + @IsString() + readonly userId: UUID; + + @ApiProperty({ + example: '6574c8adb08b3d712827385f', + description: '여정 id', + required: true, + }) + @IsString() + readonly journeyId: string; +} + +export class DeleteJourneyResDTO { + @ApiProperty({ + example: [37.555946, 126.972384], + description: '위치 좌표', + required: true, + }) + readonly coordinate: number[]; + + @ApiProperty({ + example: '2023-11-22T12:00:00Z', + description: 'timestamp', + required: true, + }) + @IsDateString() + readonly startTimestamp: string; + + @ApiProperty({ + example: '656f4b55b11c27334d1fd347', + description: '저장한 journey id', + required: true, + }) + @IsString() + readonly journeyId: string; + + // @ApiProperty({ + // example: 'hello@gmail.com', + // description: '이메일', + // required: true, + // }) + // @IsString() + // readonly email: string; +} diff --git a/BE/musicspot/src/journey/dto/journeyEnd/journeyEnd.dto.ts b/BE/musicspot/src/journey/dto/journeyEnd/journeyEnd.dto.ts index 183de74..bb660e6 100644 --- a/BE/musicspot/src/journey/dto/journeyEnd/journeyEnd.dto.ts +++ b/BE/musicspot/src/journey/dto/journeyEnd/journeyEnd.dto.ts @@ -13,6 +13,7 @@ import { } from '../../../common/decorator/coordinate.decorator'; import { Type } from 'class-transformer'; import { SongDTO } from '../song/song.dto'; +import { IsObjectId } from 'src/common/decorator/objectId.decorator'; export class EndJourneyReqDTO { @ApiProperty({ @@ -20,7 +21,7 @@ export class EndJourneyReqDTO { description: '여정 id', required: true, }) - @IsString() + @IsObjectId({ message: 'ObjectId 형식만 유효합니다.' }) readonly journeyId: string; @ApiProperty({ @@ -31,12 +32,15 @@ export class EndJourneyReqDTO { description: '위치 좌표', required: true, }) - @IsCoordinates() + @IsCoordinates({ + message: + '위치 좌표 배열은 2차원 배열이고 각각의 배열은 숫자 2개와 범위를 만족해야합니다.(-90~90, -180~180)', + }) readonly coordinates: number[][]; @ApiProperty({ example: '2023-11-22T12:00:00Z', - description: 'timestamp', + description: '종료 timestamp', required: true, }) @IsDateString() diff --git a/BE/musicspot/src/journey/dto/journeyRecord/journeyRecord.dto.ts b/BE/musicspot/src/journey/dto/journeyRecord/journeyRecord.dto.ts index 9ed0178..f172679 100644 --- a/BE/musicspot/src/journey/dto/journeyRecord/journeyRecord.dto.ts +++ b/BE/musicspot/src/journey/dto/journeyRecord/journeyRecord.dto.ts @@ -4,6 +4,7 @@ import { IsCoordinate, IsCoordinates, } from '../../../common/decorator/coordinate.decorator'; +import { IsObjectId } from 'src/common/decorator/objectId.decorator'; export class RecordJourneyResDTO { @ApiProperty({ example: [ @@ -23,7 +24,7 @@ export class RecordJourneyReqDTO { description: '여정 id', required: true, }) - @IsString() + @IsObjectId({ message: 'ObjectId 형식만 유효합니다.' }) readonly journeyId: string; @ApiProperty({ diff --git a/BE/musicspot/src/journey/dto/journeyStart/journeyStart.dto.ts b/BE/musicspot/src/journey/dto/journeyStart/journeyStart.dto.ts index 93ac68a..73185e4 100644 --- a/BE/musicspot/src/journey/dto/journeyStart/journeyStart.dto.ts +++ b/BE/musicspot/src/journey/dto/journeyStart/journeyStart.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsString, IsDateString, IsArray } from 'class-validator'; +import { IsString, IsDateString, IsArray, IsUUID } from 'class-validator'; import { IsCoordinate } from '../../../common/decorator/coordinate.decorator'; import { UUID } from 'crypto'; export class StartJourneyReqDTO { @@ -16,7 +16,7 @@ export class StartJourneyReqDTO { @ApiProperty({ example: '2023-11-22T12:00:00Z', - description: 'timestamp', + description: '시작 timestamp', required: true, }) @IsDateString() @@ -27,7 +27,7 @@ export class StartJourneyReqDTO { description: '사용자 id', required: true, }) - @IsString() + @IsUUID() readonly userId: UUID; // @ApiProperty({ diff --git a/BE/musicspot/src/journey/schema/journey.schema.ts b/BE/musicspot/src/journey/schema/journey.schema.ts index b984056..7da63a6 100644 --- a/BE/musicspot/src/journey/schema/journey.schema.ts +++ b/BE/musicspot/src/journey/schema/journey.schema.ts @@ -3,6 +3,7 @@ import { HydratedDocument } from 'mongoose'; import { ApiProperty } from '@nestjs/swagger'; import { Song } from './song.schema'; import { JourneyMetadata } from './journeyMetadata.schema'; +import { IsDefined, ValidateNested } from 'class-validator'; export type JourneyDocument = HydratedDocument; @@ -39,9 +40,11 @@ export class Journey { @Prop({ type: [[Number]] }) coordinates?: number[][]; + @ApiProperty({ description: '메타데이터', type: JourneyMetadata }) @Prop({ type: JourneyMetadata }) journeyMetadata?: JourneyMetadata; + @ApiProperty({ description: '음악 정보', type: Song }) @Prop({ type: Song }) song?: Song; } diff --git a/BE/musicspot/src/journey/service/journey.service.ts b/BE/musicspot/src/journey/service/journey.service.ts index 3227bdf..d2d4b9f 100644 --- a/BE/musicspot/src/journey/service/journey.service.ts +++ b/BE/musicspot/src/journey/service/journey.service.ts @@ -1,4 +1,4 @@ -import { Model } from 'mongoose'; +import mongoose, { Model } from 'mongoose'; import { InjectModel } from '@nestjs/mongoose'; import { Injectable } from '@nestjs/common'; @@ -22,6 +22,8 @@ import { EndJourneyReqDTO } from '../dto/journeyEnd/journeyEnd.dto'; import { CheckJourneyReqDTO } from '../dto/journeyCheck/journeyCheck.dto'; import { RecordJourneyReqDTO } from '../dto/journeyRecord/journeyRecord.dto'; import { is1DArray } from 'src/common/util/coordinate.util'; +import { DeleteJourneyReqDTO } from '../dto/journeyDelete.dto'; +import { checkPrimeSync } from 'crypto'; @Injectable() export class JourneyService { @@ -239,4 +241,29 @@ export class JourneyService { }) .lean(); } + + async deleteJourneyById(deletedJourneyDto: DeleteJourneyReqDTO) { + const { userId, journeyId } = deletedJourneyDto; + + const deletedJourney = await this.journeyModel + .findOneAndDelete({ + _id: journeyId, + }) + .lean(); + if (!deletedJourney) { + throw new JourneyNotFoundException(); + } + + const deletedUserData = await this.userModel.findOneAndUpdate( + { userId }, + { $pull: { journeys: new mongoose.Types.ObjectId(journeyId) } }, + { new: true }, + ); + + if (!deletedUserData) { + throw new UserNotFoundException(); + } + + return deletedJourney; + } } diff --git a/BE/musicspot/src/spot/dto/recordSpot.dto.ts b/BE/musicspot/src/spot/dto/recordSpot.dto.ts index ad2d9ca..e400447 100644 --- a/BE/musicspot/src/spot/dto/recordSpot.dto.ts +++ b/BE/musicspot/src/spot/dto/recordSpot.dto.ts @@ -1,6 +1,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsArray, IsDateString, IsString, IsUrl } from 'class-validator'; import { IsCoordinate } from '../../common/decorator/coordinate.decorator'; +import { IsObjectId } from 'src/common/decorator/objectId.decorator'; export class RecordSpotReqDTO { @ApiProperty({ @@ -8,7 +9,7 @@ export class RecordSpotReqDTO { description: '여정 id', required: true, }) - @IsString() + @IsObjectId({ message: 'ObjectId 형식만 유효합니다.' }) readonly journeyId: string; @ApiProperty({