diff --git a/backend/resources/scripts/get-presigned-post-function.js b/backend/resources/scripts/get-presigned-post-function.js new file mode 100644 index 00000000..93dd202b --- /dev/null +++ b/backend/resources/scripts/get-presigned-post-function.js @@ -0,0 +1,59 @@ +function main(params) { + const AWS = require('aws-sdk'); + + const ENDPOINT_URL = 'https://kr.object.ncloudstorage.com'; + const endpoint = new AWS.Endpoint('https://kr.object.ncloudstorage.com'); + const region = 'kr-standard'; + const accessKey = params.access; + const secretKey = params.secret; + + const bucketName = 'ogil-public'; + const baseDirname = 'uploads'; + + const objectName = path.join( + 'post', + baseDirname, + params.dirname, + getUUIDName(params.extension), + ); + + const signedUrlExpireSeconds = 300; + const contentType = 'image/*'; + const ACL = 'public-read'; + + const maxFileSize = 3 * 1024 * 1024; + + const S3 = new AWS.S3({ + endpoint: endpoint, + region: region, + credentials: { + accessKeyId: accessKey, + secretAccessKey: secretKey, + }, + signatureVersion: 'v4', + }); + + const post = S3.createPresignedPost({ + Bucket: bucketName, + Conditions: [['content-length-range', 0, maxFileSize]], + ContentType: contentType, + Expires: signedUrlExpireSeconds, + Fields: { + key: objectName, + 'Content-Type': contentType, + acl: ACL, + }, + }); + + const uploadedUrl = path.join(ENDPOINT_URL, bucketName, objectName); + + console.log(post); + console.log(`${uploadedUrl}에 업로드 됩니다`); + + return { ...post, uploadedUrl }; +} + +function getUUIDName(extension) { + const { v4: uuidv4 } = require('uuid'); + return uuidv4().substring(0, 13).replace('-', '') + '.' + extension; +} diff --git a/backend/resources/scripts/get-presigned-url-function.js b/backend/resources/scripts/get-presigned-url-function.js new file mode 100644 index 00000000..e7f0d08b --- /dev/null +++ b/backend/resources/scripts/get-presigned-url-function.js @@ -0,0 +1,50 @@ +function main(params) { + const AWS = require('aws-sdk'); + + const ENDPOINT_URL = 'https://kr.object.ncloudstorage.com'; + const endpoint = new AWS.Endpoint('https://kr.object.ncloudstorage.com'); + const region = 'kr-standard'; + const accessKey = params.access; + const secretKey = params.secret; + + const bucketName = 'ogil-public'; + const baseDirname = 'uploads'; + + const objectName = path.join( + baseDirname, + params.dirname, + getUUIDName(params.extension), + ); + + const signedUrlExpireSeconds = 300; + const contentType = 'image/*'; + const ACL = 'public-read'; + + const S3 = new AWS.S3({ + endpoint: endpoint, + region: region, + credentials: { + accessKeyId: accessKey, + secretAccessKey: secretKey, + }, + signatureVersion: 'v4', + }); + + const url = S3.getSignedUrl('putObject', { + Bucket: bucketName, + Key: objectName, + Expires: signedUrlExpireSeconds, + ContentType: contentType, + ACL, + }); + + const uploadedUrl = path.join(ENDPOINT_URL, bucketName, objectName); + + console.log({ url, uploadedUrl }); + return { url, uploadedUrl }; +} + +function getUUIDName(extension) { + const { v4: uuidv4 } = require('uuid'); + return uuidv4().substring(0, 13).replace('-', '') + '.' + extension; +} diff --git a/backend/resources/sql/DDL.sql b/backend/resources/sql/DDL.sql index 014abd29..23db01fd 100644 --- a/backend/resources/sql/DDL.sql +++ b/backend/resources/sql/DDL.sql @@ -64,12 +64,13 @@ CREATE TABLE MAP CREATE TABLE MAP_PLACE ( id INT PRIMARY KEY AUTO_INCREMENT, - place_id INT NOT NULL, - map_id INT NOT NULL, + place_id INT NOT NULL, + map_id INT NOT NULL, description TEXT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - deleted_at TIMESTAMP NULL, + color VARCHAR(20) DEFAULT 'RED' NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + deleted_at TIMESTAMP NULL, FOREIGN KEY (place_id) REFERENCES PLACE (id) ON DELETE CASCADE, FOREIGN KEY (map_id) REFERENCES MAP (id) ON DELETE CASCADE ); diff --git a/backend/src/common/exception/filter/GlobalExceptionFilter.ts b/backend/src/common/exception/filter/GlobalExceptionFilter.ts index 1e97de0d..64c1831f 100644 --- a/backend/src/common/exception/filter/GlobalExceptionFilter.ts +++ b/backend/src/common/exception/filter/GlobalExceptionFilter.ts @@ -8,52 +8,57 @@ import { import { Response } from 'express'; import { BaseException } from '../BaseException'; -@Catch() -export class GlobalExceptionFilter implements ExceptionFilter { - catch(exception: unknown, host: ArgumentsHost) { +@Catch(BaseException) +export class BaseExceptionFilter implements ExceptionFilter { + catch(exception: BaseException, host: ArgumentsHost) { const ctx = host.switchToHttp(); const response = ctx.getResponse(); - if (exception instanceof BaseException) { - return this.sendErrorResponse( - response, - exception.getCode(), - exception.getStatus(), - exception.getMessage(), - ); - } - - if (exception instanceof HttpException) { - console.log(exception); - return this.sendErrorResponse( - response, - 9999, - exception.getStatus(), - exception.message, - ); - } + return response.status(exception.getStatus()).json({ + code: exception.getCode(), + message: exception.getMessage(), + }); + } +} - console.log(exception); - return this.sendErrorResponse( - response, - -1, - HttpStatus.INTERNAL_SERVER_ERROR, - this.getDefaultErrorMessage(exception), - ); +@Catch(HttpException) +export class HttpExceptionFilter implements ExceptionFilter { + catch(exception: HttpException, host: ArgumentsHost) { + const ctx = host.switchToHttp(); + const response = ctx.getResponse(); + + const exceptionResponse = exception.getResponse(); + + const errorMessage = this.isValidationError(exceptionResponse) + ? (exceptionResponse as any).message.join(', ') + : exception.message; + + return response.status(exception.getStatus()).json({ + code: 9999, + message: errorMessage, + }); } - private sendErrorResponse( - response: Response, - code: number, - status: number, - message: string, - ) { - response.status(status).json({ code, message }); + private isValidationError(exceptionResponse: unknown): boolean { + return ( + typeof exceptionResponse === 'object' && + exceptionResponse !== null && + 'message' in exceptionResponse && + Array.isArray((exceptionResponse as any).message) + ); } +} - private getDefaultErrorMessage(exception: unknown) { - return exception instanceof Error - ? 'Internal server error: ' + exception.message - : 'Internal server error'; +@Catch() +export class UnknownExceptionFilter implements ExceptionFilter { + catch(exception: unknown, host: ArgumentsHost) { + const ctx = host.switchToHttp(); + const response = ctx.getResponse(); + + console.log(exception); + return response.status(HttpStatus.INTERNAL_SERVER_ERROR).json({ + code: -1, + message: 'Internal server error', + }); } } diff --git a/backend/src/course/course.service.ts b/backend/src/course/course.service.ts index 8cec9aa2..572a36f9 100644 --- a/backend/src/course/course.service.ts +++ b/backend/src/course/course.service.ts @@ -118,7 +118,7 @@ export class CourseService { const course = await this.courseRepository.findById(id); if (!course) throw new CourseNotFoundException(id); - await this.checkPlacesExist( + await this.validatePlacesForCourse( setPlacesOfCourseRequest.places.map((p) => p.placeId), ); @@ -131,7 +131,7 @@ export class CourseService { }; } - private async checkPlacesExist(placeIds: number[]) { + private async validatePlacesForCourse(placeIds: number[]) { const notExistsPlaceIds = await Promise.all( placeIds.map(async (placeId) => { const exists = await this.placeRepository.existById(placeId); diff --git a/backend/src/main.ts b/backend/src/main.ts index c00eefb0..2aa91c1e 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -1,11 +1,19 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { ValidationPipe } from '@nestjs/common'; -import { GlobalExceptionFilter } from './common/exception/filter/GlobalExceptionFilter'; +import { + BaseExceptionFilter, + HttpExceptionFilter, + UnknownExceptionFilter, +} from './common/exception/filter/GlobalExceptionFilter'; async function bootstrap() { const app = await NestFactory.create(AppModule); - app.useGlobalFilters(new GlobalExceptionFilter()); + app.useGlobalFilters( + new UnknownExceptionFilter(), + new HttpExceptionFilter(), + new BaseExceptionFilter(), + ); app.useGlobalPipes(new ValidationPipe({ transform: true })); await app.listen(8080); } diff --git a/backend/src/map/dto/AddPlaceToMapRequest.ts b/backend/src/map/dto/AddPlaceToMapRequest.ts index 0d220b05..6daebe9a 100644 --- a/backend/src/map/dto/AddPlaceToMapRequest.ts +++ b/backend/src/map/dto/AddPlaceToMapRequest.ts @@ -1,4 +1,5 @@ -import { IsNumber, IsString } from 'class-validator'; +import { IsNumber, IsString, IsEnum } from 'class-validator'; +import { Color } from '../../place/color.enum'; export class AddPlaceToMapRequest { @IsNumber() @@ -6,4 +7,7 @@ export class AddPlaceToMapRequest { @IsString() comment?: string; + + @IsEnum(Color) + color: Color; } diff --git a/backend/src/map/dto/MapDetailResponse.ts b/backend/src/map/dto/MapDetailResponse.ts index 53c16401..4b7d30b6 100644 --- a/backend/src/map/dto/MapDetailResponse.ts +++ b/backend/src/map/dto/MapDetailResponse.ts @@ -22,6 +22,7 @@ export class MapDetailResponse { return { ...PlaceListResponse.from(place.place), comment: place.comment, + color: place.color, }; }); diff --git a/backend/src/map/entity/map-place.entity.ts b/backend/src/map/entity/map-place.entity.ts index 6b9c301f..dad38f23 100644 --- a/backend/src/map/entity/map-place.entity.ts +++ b/backend/src/map/entity/map-place.entity.ts @@ -2,6 +2,7 @@ import { Entity, Column, ManyToOne, JoinColumn } from 'typeorm'; import { BaseEntity } from '../../common/BaseEntity'; import { Place } from '../../place/entity/place.entity'; import { Map } from './map.entity'; +import { Color } from '../../place/color.enum'; @Entity() export class MapPlace extends BaseEntity { @@ -22,9 +23,13 @@ export class MapPlace extends BaseEntity { @Column('text', { nullable: true }) description?: string; - static of(placeId: number, map: Map, description?: string) { + @Column() + color: Color; + + static of(placeId: number, map: Map, color: Color, description?: string) { const place = new MapPlace(); place.map = map; + place.color = color; place.placeId = placeId; place.place = Promise.resolve({ id: placeId } as Place); place.description = description; diff --git a/backend/src/map/entity/map.entity.ts b/backend/src/map/entity/map.entity.ts index bc58ccda..a74b0334 100644 --- a/backend/src/map/entity/map.entity.ts +++ b/backend/src/map/entity/map.entity.ts @@ -2,6 +2,7 @@ import { Entity, Column, ManyToOne, JoinColumn, OneToMany } from 'typeorm'; import { BaseEntity } from '../../common/BaseEntity'; import { User } from '../../user/entity/user.entity'; import { MapPlace } from './map-place.entity'; +import { Color } from '../../place/color.enum'; @Entity() export class Map extends BaseEntity { @@ -46,8 +47,8 @@ export class Map extends BaseEntity { return this.mapPlaces.length; } - addPlace(placeId: number, description: string) { - this.mapPlaces.push(MapPlace.of(placeId, this, description)); + addPlace(placeId: number, color: Color, description: string) { + this.mapPlaces.push(MapPlace.of(placeId, this, color, description)); } async deletePlace(placeId: number) { @@ -63,6 +64,7 @@ export class Map extends BaseEntity { this.mapPlaces.map(async (mapPlace) => ({ place: await mapPlace.place, comment: mapPlace.description, + color: mapPlace.color, })), ); } diff --git a/backend/src/map/map.controller.ts b/backend/src/map/map.controller.ts index 82e44150..186168df 100644 --- a/backend/src/map/map.controller.ts +++ b/backend/src/map/map.controller.ts @@ -51,8 +51,8 @@ export class MapController { @Param('id') id: number, @Body() addPlaceToMapRequest: AddPlaceToMapRequest, ) { - const { placeId, comment } = addPlaceToMapRequest; - return await this.mapService.addPlace(id, placeId, comment); + const { placeId, color, comment } = addPlaceToMapRequest; + return await this.mapService.addPlace(id, placeId, color, comment); } @Delete('/:id/places/:placeId') diff --git a/backend/src/map/map.service.ts b/backend/src/map/map.service.ts index 6f93ed70..da7c8b01 100644 --- a/backend/src/map/map.service.ts +++ b/backend/src/map/map.service.ts @@ -11,6 +11,7 @@ import { DuplicatePlaceToMapException } from './exception/DuplicatePlaceToMapExc import { PlaceRepository } from '../place/place.repository'; import { InvalidPlaceToMapException } from './exception/InvalidPlaceToMapException'; import { Map } from './entity/map.entity'; +import { Color } from '../place/color.enum'; @Injectable() export class MapService { @@ -103,21 +104,27 @@ export class MapService { throw new MapNotFoundException(id); } - async addPlace(id: number, placeId: number, comment?: string) { + async addPlace( + id: number, + placeId: number, + color = Color.RED, + comment?: string, + ) { const map = await this.mapRepository.findById(id); if (!map) throw new MapNotFoundException(id); - await this.checkPlaceCanAddToMap(placeId, map); + await this.validatePlacesForMap(placeId, map); - map.addPlace(placeId, comment); + map.addPlace(placeId, color, comment); await this.mapRepository.save(map); return { savedPlaceId: placeId, - comment: comment, + comment, + color, }; } - private async checkPlaceCanAddToMap(placeId: number, map: Map) { + private async validatePlacesForMap(placeId: number, map: Map) { if (!(await this.placeRepository.existById(placeId))) { throw new InvalidPlaceToMapException(placeId); } diff --git a/backend/src/place/color.enum.ts b/backend/src/place/color.enum.ts new file mode 100644 index 00000000..95641cfd --- /dev/null +++ b/backend/src/place/color.enum.ts @@ -0,0 +1,8 @@ +export enum Color { + RED = 'RED', + ORANGE = 'ORANGE', + YELLOW = 'YELLOW', + GREEN = 'GREEN', + BLUE = 'BLUE', + PURPLE = 'PURPLE', +} diff --git a/backend/src/place/place.service.ts b/backend/src/place/place.service.ts index 323c1761..5cfe01b3 100644 --- a/backend/src/place/place.service.ts +++ b/backend/src/place/place.service.ts @@ -29,9 +29,6 @@ export class PlaceService { ) : await this.placeRepository.findAll(page, pageSize); - if (!result.length) { - throw new PlaceNotFoundException(); - } return result.map(PlaceSearchResponse.from); } diff --git a/backend/src/user/dto/CreateUserRequest.ts b/backend/src/user/dto/CreateUserRequest.ts index e6e0a1a5..41485094 100644 --- a/backend/src/user/dto/CreateUserRequest.ts +++ b/backend/src/user/dto/CreateUserRequest.ts @@ -1,6 +1,6 @@ import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; import { User } from '../entity/user.entity'; -import { userInfoWithProvider } from '../userType'; +import { userInfoWithProvider } from '../user.type'; export class CreateUserRequest { @IsString() diff --git a/backend/src/user/user-role.ts b/backend/src/user/role.enum.ts similarity index 100% rename from backend/src/user/user-role.ts rename to backend/src/user/role.enum.ts diff --git a/backend/src/user/userType.ts b/backend/src/user/user.type.ts similarity index 100% rename from backend/src/user/userType.ts rename to backend/src/user/user.type.ts