diff --git a/BE/musicspot/src/journey/dto/journeyCheck/journeyCheck.dto.ts b/BE/musicspot/src/journey/dto/journeyCheck/journeyCheck.dto.ts index a8e76f1..d34cd9e 100644 --- a/BE/musicspot/src/journey/dto/journeyCheck/journeyCheck.dto.ts +++ b/BE/musicspot/src/journey/dto/journeyCheck/journeyCheck.dto.ts @@ -42,7 +42,7 @@ export class SpotDTO { @ApiProperty({ description: '여정 ID', example: '65649c91380cafcab8869ed2' }) readonly journeyId: string; - @ApiProperty({ description: 'spot 위치', example: [37.555913, 126.972313] }) + @ApiProperty({ description: 'spot 위치', example: '37.555913 126.972313' }) readonly coordinate: number[]; @ApiProperty({ description: '기록 시간', example: '2023-11-22T12:00:00Z' }) diff --git a/BE/musicspot/src/journey/service/journey.service.ts b/BE/musicspot/src/journey/service/journey.service.ts index b0911da..bc71e10 100644 --- a/BE/musicspot/src/journey/service/journey.service.ts +++ b/BE/musicspot/src/journey/service/journey.service.ts @@ -272,7 +272,7 @@ export class JourneyService { } return { - journey: this.parseJourneyFromEntityToDtoV2(lastJourneyData), + journey: lastJourneyData, isRecording: true, }; } diff --git a/BE/musicspot/src/spot/controller/spot.controller.ts b/BE/musicspot/src/spot/controller/spot.controller.ts index 8c13db5..830a96d 100644 --- a/BE/musicspot/src/spot/controller/spot.controller.ts +++ b/BE/musicspot/src/spot/controller/spot.controller.ts @@ -6,6 +6,7 @@ import { UploadedFile, Get, Query, + Version, } from '@nestjs/common'; import { ApiCreatedResponse, ApiOperation, ApiTags } from '@nestjs/swagger'; import { RecordSpotReqDTO } from '../dto/recordSpot.dto'; @@ -13,6 +14,7 @@ import { SpotService } from '../service/spot.service'; import { FileInterceptor } from '@nestjs/platform-express'; import { Spot } from '../schema/spot.schema'; import { SpotDTO } from 'src/journey/dto/journeyCheck/journeyCheck.dto'; +import {RecordSpotReqDTOV2} from "../dto/v2/recordSpot.v2.dto"; @Controller('spot') @ApiTags('spot 관련 API') export class SpotController { @@ -35,6 +37,23 @@ export class SpotController { return await this.spotService.create(file, recordSpotDTO); } + @Version('2') + @ApiOperation({ + summary: 'spot 기록 API', + description: 'spot을 기록합니다.', + }) + @ApiCreatedResponse({ + description: 'spot 생성 데이터 반환', + type: SpotDTO, + }) + @UseInterceptors(FileInterceptor('image')) + @Post('') + async createV2( + @UploadedFile() file: Express.Multer.File, + @Body() recordSpotDTO, + ) { + return await this.spotService.createV2(file, recordSpotDTO); + } @ApiOperation({ summary: 'spot 조회 API', description: 'spotId로 스팟 이미지를 조회합니다.', diff --git a/BE/musicspot/src/spot/dto/v2/recordSpot.v2.dto.ts b/BE/musicspot/src/spot/dto/v2/recordSpot.v2.dto.ts new file mode 100644 index 0000000..a506726 --- /dev/null +++ b/BE/musicspot/src/spot/dto/v2/recordSpot.v2.dto.ts @@ -0,0 +1,63 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsDateString, IsNumber } from 'class-validator'; +import { IsCoordinateV2 } from '../../../common/decorator/coordinate.v2.decorator'; + +export class RecordSpotReqDTOV2 { + @ApiProperty({ + example: '20', + description: '여정 id', + required: true, + }) + @IsNumber() + readonly journeyId: number; + + @ApiProperty({ + example: '37.555946 126.972384', + description: '위치 좌표', + required: true, + }) + @IsCoordinateV2({ + message: + '위치 좌표는 2개의 숫자와 각각의 범위를 만족해야합니다.(-90~90 , -180~180)', + }) + readonly coordinate: string; + + @ApiProperty({ + example: '2023-11-22T12:00:00Z', + description: 'timestamp', + required: true, + }) + @IsDateString() + readonly timestamp: string; +} + +export class RecordSpotResDTOV2 { + @ApiProperty({ + example: 20, + description: '여정 id', + required: true, + }) + readonly journeyId: number; + + @ApiProperty({ + example: '37.555946 126.972384', + description: '위치 좌표', + required: true, + }) + readonly coordinate: string; + + @ApiProperty({ + example: '2023-11-22T12:00:00Z', + description: 'timestamp', + required: true, + }) + readonly timestamp: string; + + @ApiProperty({ + example: + 'https://music-spot-storage.kr.object.ncloudstorage.com/path/name?AWSAccessKeyId=key&Expires=1701850233&Signature=signature', + description: 'presigned url', + required: true, + }) + readonly photoUrl: string; +} diff --git a/BE/musicspot/src/spot/service/spot.service.ts b/BE/musicspot/src/spot/service/spot.service.ts index f556a5c..69bf732 100644 --- a/BE/musicspot/src/spot/service/spot.service.ts +++ b/BE/musicspot/src/spot/service/spot.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@nestjs/common'; -import {Spot} from '../entities/spot.entity' +import { Spot } from '../entities/spot.entity'; import { SpotNotFoundException, SpotRecordFail, @@ -10,99 +10,154 @@ import { makePresignedUrl, } from '../../common/s3/objectStorage'; -import { JourneyNotFoundException, coordinateNotCorrectException } from 'src/filters/journey.exception'; -import { is1DArray, parseCoordinateFromDtoToGeo } from 'src/common/util/coordinate.util'; +import { + JourneyNotFoundException, + coordinateNotCorrectException, +} from 'src/filters/journey.exception'; +import { + is1DArray, + parseCoordinateFromDtoToGeo, parseCoordinateFromGeoToDto, +} from 'src/common/util/coordinate.util'; import { SpotRepository } from '../repository/spot.repository'; import { RecordSpotResDTO } from '../dto/recordSpot.dto'; import { JourneyRepository } from 'src/journey/repository/journey.repository'; +import { + RecordSpotReqDTOV2, + RecordSpotResDTOV2, +} from '../dto/v2/recordSpot.v2.dto'; +import { + isPointString, + parseCoordinateFromDtoToGeoV2, parseCoordinateFromGeoToDtoV2, +} from '../../common/util/coordinate.v2.util'; @Injectable() export class SpotService { constructor( - private spotRepository: SpotRepository, private journeyRepository: JourneyRepository) {} - - - async uploadPhotoToStorage(journeyId, file) { - try{ - const key = `${journeyId}/${Date.now()}`; - const result = await S3.putObject({ - Bucket: bucketName, - Key: key, - Body: file.buffer, - }).promise(); - - return key; - } catch(err){ - throw new SpotRecordFail(); - } - } + private spotRepository: SpotRepository, + private journeyRepository: JourneyRepository, + ) {} + + async uploadPhotoToStorage(journeyId, file) { + try { + const key = `${journeyId}/${Date.now()}`; + const result = await S3.putObject({ + Bucket: bucketName, + Key: key, + Body: file.buffer, + }).promise(); - - async insertToSpot(spotData){ - const point = `POINT(${parseCoordinateFromDtoToGeo(spotData.coordinate)})`; - const data = {...spotData, journeyId :Number(spotData.journeyId), coordinate: point } - - return await this.spotRepository.save(data); + return key; + } catch (err) { + throw new SpotRecordFail(); } - - async updateCoordinatesToJourney(journeyId, coordinate){ - const parsedCoordinate = parseCoordinateFromDtoToGeo(coordinate); - const originalJourney = await this.journeyRepository.findOne({where:{journeyId}}) - const lineStringLen = 'LINESTRING('.length; - - if(!originalJourney){ - throw new JourneyNotFoundException(); - } - originalJourney.coordinates = `LINESTRING(${originalJourney.coordinates.slice(lineStringLen, -1)}, ${parsedCoordinate})` - - - return await this.journeyRepository.save(originalJourney); + } + + async insertToSpotV2(spotData) { + const data = { + ...spotData, + journeyId: Number(spotData.journeyId), + }; + + return await this.spotRepository.save(data); + } + async insertToSpot(spotData) { + const point = `POINT(${parseCoordinateFromDtoToGeo(spotData.coordinate)})`; + const data = { + ...spotData, + journeyId: Number(spotData.journeyId), + coordinate: point, + }; + + return await this.spotRepository.save(data); + } + + async updateCoordinatesToJourney(journeyId, coordinate) { + const parsedCoordinate = parseCoordinateFromDtoToGeo(coordinate); + const originalJourney = await this.journeyRepository.findOne({ + where: { journeyId }, + }); + const lineStringLen = 'LINESTRING('.length; + + if (!originalJourney) { + throw new JourneyNotFoundException(); } - - async create(file, recordSpotDto){ - let parsedCoordinate; - try { - parsedCoordinate = JSON.parse(recordSpotDto.coordinate); - } catch (err) { - throw new coordinateNotCorrectException(); - } - if (!is1DArray(parsedCoordinate)) { - throw new coordinateNotCorrectException(); - } - - const photoKey = await this.uploadPhotoToStorage( - recordSpotDto.journeyId, - file, - ); - const presignedUrl = makePresignedUrl(photoKey); - - const createdSpotData = await this.insertToSpot({ - ...recordSpotDto, - photoKey, - coordinate: parsedCoordinate - }); - const updatedJourneyData = await this.updateCoordinatesToJourney(recordSpotDto.journeyId, parsedCoordinate) - - const returnData:RecordSpotResDTO = { - journeyId : createdSpotData.journeyId, - coordinate : parsedCoordinate, - timestamp : createdSpotData.timestamp, - photoUrl : presignedUrl - - } - - return returnData + originalJourney.coordinates = `LINESTRING(${originalJourney.coordinates.slice( + lineStringLen, + -1, + )}, ${parsedCoordinate})`; + + return await this.journeyRepository.save(originalJourney); + } + + async createV2(file, recordSpotDto) { + const { coordinate } = recordSpotDto; + if (!isPointString(coordinate)) { + throw new coordinateNotCorrectException(); } - - async getSpotImage(spotId: number) { - const spot = await this.spotRepository.findOne({where: {spotId}}); - if (!spot) { - throw new SpotNotFoundException(); - } - - return spot.photoKey; + const photoKey: string = await this.uploadPhotoToStorage( + recordSpotDto.journeyId, + file, + ); + const presignedUrl: string = makePresignedUrl(photoKey); + const createdSpotData = await this.insertToSpotV2({ + ...recordSpotDto, + photoKey, + coordinate: parseCoordinateFromDtoToGeoV2(coordinate), + }); + + const returnData: RecordSpotResDTOV2 = { + journeyId: createdSpotData.journeyId, + coordinate: parseCoordinateFromGeoToDtoV2(createdSpotData.coordinate), + timestamp: createdSpotData.timestamp, + photoUrl: presignedUrl, + }; + + return returnData; + } + async create(file, recordSpotDto) { + let parsedCoordinate; + try { + parsedCoordinate = JSON.parse(recordSpotDto.coordinate); + } catch (err) { + throw new coordinateNotCorrectException(); } -} + if (!is1DArray(parsedCoordinate)) { + throw new coordinateNotCorrectException(); + } + + const photoKey = await this.uploadPhotoToStorage( + recordSpotDto.journeyId, + file, + ); + const presignedUrl = makePresignedUrl(photoKey); + const createdSpotData = await this.insertToSpot({ + ...recordSpotDto, + photoKey, + coordinate: parsedCoordinate, + }); + const updatedJourneyData = await this.updateCoordinatesToJourney( + recordSpotDto.journeyId, + parsedCoordinate, + ); + const returnData: RecordSpotResDTO = { + journeyId: createdSpotData.journeyId, + coordinate: parsedCoordinate, + timestamp: createdSpotData.timestamp, + photoUrl: presignedUrl, + }; + + return returnData; + } + + async getSpotImage(spotId: number) { + const spot = await this.spotRepository.findOne({ where: { spotId } }); + if (!spot) { + throw new SpotNotFoundException(); + } + + return spot.photoKey; + } +}