Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

장소에 색상 추가 #100

Merged
merged 8 commits into from
Nov 12, 2024
59 changes: 59 additions & 0 deletions backend/resources/scripts/get-presigned-post-function.js
Original file line number Diff line number Diff line change
@@ -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;
}
50 changes: 50 additions & 0 deletions backend/resources/scripts/get-presigned-url-function.js
Original file line number Diff line number Diff line change
@@ -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;
}
11 changes: 6 additions & 5 deletions backend/resources/sql/DDL.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
Expand Down
83 changes: 44 additions & 39 deletions backend/src/common/exception/filter/GlobalExceptionFilter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,52 +8,57 @@
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<Response>();

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<Response>();

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<Response>();

console.log(exception);

Check warning on line 58 in backend/src/common/exception/filter/GlobalExceptionFilter.ts

View workflow job for this annotation

GitHub Actions / Lint, Build and Test (backend)

Unexpected console statement
return response.status(HttpStatus.INTERNAL_SERVER_ERROR).json({
code: -1,
message: 'Internal server error',
});
}
}
4 changes: 2 additions & 2 deletions backend/src/course/course.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
);

Expand All @@ -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);
Expand Down
12 changes: 10 additions & 2 deletions backend/src/main.ts
Original file line number Diff line number Diff line change
@@ -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(),
Comment on lines +13 to +15
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

q: BaseExceptionHttpException을 상속받기 때문에 실행 순서가 중요할 것 같은데요. Filter가 순서가 적용되는지 확인을 해야할 것 같습니다. 순서가 적용된다면 순서도 바꿔야 겠네요.

제가 테스트 했을 때는 효과가 없었던 것 같아서 GlobalExceptionFilter에서 if 로 구분했었거든요. 한번 확인해보고 알려주세요!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이거 PR 메시지에 적었는데요,

당연히 좁은 범위를 먼저 선언해야 할 것 같지만
반대로 넓은 범위를 먼저 선언해야 제대로 작동하더라구요

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이거 PR 메시지에 적었는데요,

당연히 좁은 범위를 먼저 선언해야 할 것 같지만 반대로 넓은 범위를 먼저 선언해야 제대로 작동하더라구요

헉 충격적이군요 좋습니다

);
app.useGlobalPipes(new ValidationPipe({ transform: true }));
await app.listen(8080);
}
Expand Down
6 changes: 5 additions & 1 deletion backend/src/map/dto/AddPlaceToMapRequest.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { IsNumber, IsString } from 'class-validator';
import { IsNumber, IsString, IsEnum } from 'class-validator';
import { Color } from '../../place/color.enum';

export class AddPlaceToMapRequest {
@IsNumber()
placeId: number;

@IsString()
comment?: string;

@IsEnum(Color)
color: Color;
}
1 change: 1 addition & 0 deletions backend/src/map/dto/MapDetailResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export class MapDetailResponse {
return {
...PlaceListResponse.from(place.place),
comment: place.comment,
color: place.color,
};
});

Expand Down
7 changes: 6 additions & 1 deletion backend/src/map/entity/map-place.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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;
Expand Down
6 changes: 4 additions & 2 deletions backend/src/map/entity/map.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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) {
Expand All @@ -63,6 +64,7 @@ export class Map extends BaseEntity {
this.mapPlaces.map(async (mapPlace) => ({
place: await mapPlace.place,
comment: mapPlace.description,
color: mapPlace.color,
})),
);
}
Expand Down
4 changes: 2 additions & 2 deletions backend/src/map/map.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
17 changes: 12 additions & 5 deletions backend/src/map/map.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
}
Expand Down
Loading
Loading