오늘 한 일
오늘은 공연 예약 사이트 마무리 한 것을 다듬는 시간을 가졌다.(NestJS + TypeORM) 어제까지 코드를 완성했다 싶었는데 한 번 손을 대기 시작하니까 손 볼 부분이 한 두 군데가 아니어서 시간이 많이 들었던 것 같다. 어제는 완성본이라기 보단 기능 구현에 초점을 맞춘 코드였기 때문에 오늘은 @Guard를 사용해서 로그인한 사람만 마이페이지 조회가 가능하다던지, 로그인 시 Role이 Admin인 사람만 게시글을 올릴 수 있다던지 하는 기능들을 추가해서 넣었다.
배운 부분
로그인한 사용자만 접근하게 하기
@nestjs/common 과 @nestjs/passport 패키지를 다운 받은 뒤 @nestjs/common에서는 UseGuard를 @nestjs/passport에서는 AuthGuard를 import 해온다.
그 후 원하는 경로 위에 데코레이터로 붙여주면 된다.
@UseGuards(AuthGuard('jwt'))
@Get('me/:id')
async getMyInfo(@Param('id') id: number) {
const user = await this.userService.getMyInfo(id);
const { name, point } = user;
return { name, point };
}
관리자(Admin)만 게시글 작성하게 하기
'로그인한 사용자만 접근하게 하기'와 비슷하게 @Guard를 사용하는데 기본적인 가드에서 '역할이 Admin 일 경우'를 상정하여 커스텀 데코레이터를 만들어 사용한다.
// roll.guard/ts
import { Role } from 'src/user/types/userRole.type';
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class RolesGuard extends AuthGuard('jwt') implements CanActivate {
constructor(private reflector: Reflector) {
super();
}
async canActivate(context: ExecutionContext) {
const authenticated = await super.canActivate(context);
if (!authenticated) {
return false;
}
// @Roles(Role.Admin) -> 'roles' -> [Role.Admin, Role.Admin2, Role.Admin3] 중 하나면 true 아니면 false (29번째줄)
const requiredRoles = this.reflector.getAllAndOverride<Role[]>('roles', [
context.getHandler(),
context.getClass(),
]);
if (!requiredRoles) {
return true;
}
const { user } = context.switchToHttp().getRequest();
return requiredRoles.some((role) => user.role === role);
}
}
위와 같이 커스텀 데코레이터를 만들어주고 원하는 위치에 데코레이터를 씌워주면 Admin일 경우만 접근할 수 있게 만들 수 있다.
// performance.controller.ts
@Roles(Role.Admin)
@Post()
async postPerformance(@Body() performanceData: PerformanceDto) {
const postedPerformance =
await this.performanceService.postPerformance(performanceData);
return {
status: 201,
message: '성공적으로 공연 등록이 완료되었습니다.',
date: postedPerformance,
};
}
유저 정보 가져오기
위와 같이 커스텀 데코레이터를 만들어 사용할 수 있다.
// userInfo.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const UserInfo = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.user ? request.user : null;
},
);
// suppor-message.controller.ts
@Post(':teamId')
async createMessage(
@UserInfo() user: User,
@Param('teamId') teamId: number,
@Body() supportMessageDto: SupportMessageDto,
) {
await this.supportMessageService.createMessage(
teamId,
user.id,
supportMessageDto.message,
);
}
테이블 간 관계 설정하기
테이블 간 서로 데이터를 공유하며 관계를 맺는 경우가 생긴다. 이번 프로젝트에서도 유저가 공연을 예약하면 예약 데이터에 유저아이디와 공연아이디가 저장되고 유저 테이블에서 유저의 포인트가 차감됨과 동시에 공연 테이블의 좌석이 차감되는 로직을 만들었다.
이 경우 reservation.service.ts 파일에 3개의 repository를 불러와야 했는데 이런 경우에는 각 테이블을 담당하는 entity 부분에서 각 테이블 간의 관계 설정을 해주어야 한다.
// reservation.service.ts
// 대략적인 앞부분 코드
import {
BadRequestException,
ForbiddenException,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import _ from 'lodash';
import { Repository } from 'typeorm';
import { Reservation } from './entities/reservation.entity';
import { User } from 'src/user/entities/user.entity';
import { Performance } from 'src/performance/entities/performance.entity';
import { ReservationDto } from './dto/reservation.dto';
@Injectable()
export class ReservationService {
constructor(
@InjectRepository(Reservation)
private readonly reservationRepository: Repository<Reservation>,
@InjectRepository(Performance)
private readonly performanceRepository: Repository<Performance>,
@InjectRepository(User)
private readonly userRepository: Repository<User>,
) {}
async reservePerformance(userId: number, reservationData: ReservationDto) {
const { performanceId, datetime, place } = reservationData;
const [date, time] = datetime;
const user = await this.userRepository.findOne({ where: { id: userId } });
const performance = await this.performanceRepository.findOne({
where: { id: performanceId },
});
// performance.entity.ts
@Entity({
name: 'performances',
})
export class Performance {
@PrimaryGeneratedColumn()
id: number;
// ... 생략
@Column({ type: 'varchar', nullable: false })
category: string;
@Column({ type: 'int', default: 30000 })
price: number;
@OneToMany(() => Reservation, (reservation) => reservation.performance, {
cascade: true,
})
@JoinColumn()
reservation: Reservation;
}
// reservation.entity.ts
@Entity({
name: 'reservations',
})
export class Reservation {
@PrimaryGeneratedColumn()
id: number;
@Column({ type: 'int', nullable: false })
UserId: number;
// ... 생략
@ManyToOne(() => Performance)
@JoinColumn()
performance: Performance;
@ManyToMany(() => User)
@JoinColumn()
user: User;
}
// user.entity.ts
@Index('email', ['email'], { unique: true })
@Entity({
name: 'users',
})
export class User {
@PrimaryGeneratedColumn()
id: number;
// ... 생략
@ManyToMany(() => Reservation, { cascade: true })
@JoinColumn()
reservation: Reservation;
}
보다싶이, 테이블 간의 관계에서 1:1 , 1:N , N:1, N:N 과 같은 관계 설정을 해주어야 하고, 테이블 간의 관계를 고려하여 적절하게 cascade 옵션을 달아주어야 한다. (블로그 지난 글에서도 cascade에 대해서 설명해 놓음)
오늘의 에러
있었지만, 콘솔 창을 너무 많이 쓰다보니, 에러 메세지들이 전부 없어졌다. 대략적인 내용은 위에서 적어놓은 reservation.service.ts 파일에서 Performance를 불러올 수 없다는 것이었는데, 어찌보면 간단하게(?) 해결했다.
첫째로, 위에서 설명한 테이블 간의 관계 설정이 안되어 있던 시점이라 테이블 간의 관계를 설정해주었다.
두번째로, Performance 클래스를 사용한 모든 곳에서 Performance 클래스를 정의한 performance.entity.ts 파일을 import 해왔다.
일단 문제로 보면 첫 번째도 문제였고, 두 번째는 Performance는 임포트 해왔지만 ctrl + . 을 이용해서 임포트해와서 그런지 entity파일에서 불러온 Performance가 아니고 영 이상한 곳에서 Performance가 import되고 있었다.
(다른 폴더에서 Performance를 정의해 놓은 게 없다고 생각하는데 왜 자동으로 이상한 경로에서 가져왔는지 모르겠다)
아무튼, 두 가지를 이용해서 문제를 해결했다. 두 번째 해결책은 예방할 수 있는 방법이기에 마음에 새겨둬야겠다.
ctrl + . 으로 자동 import해오더라도 경로를 잘 확인하자. 경로를 잘못 가져왔어도 오류 안뜬다!!! 오류 안뜨니까 import 할때부터 신경쓰자.
'📁Dev Log > TIL' 카테고리의 다른 글
2024_01_08 TIL (1) | 2024.01.09 |
---|---|
2024_01_05 TIL (1) | 2024.01.06 |
2024_01_02 TIL (1) | 2024.01.03 |
2023_12_29 TIL (1) | 2023.12.30 |
2023_12_28 TIL (1) | 2023.12.29 |