
요구조건
1. 일반 사용자 → User
2. 사업자 사용자 → Partner
1. Room은 하나의 날짜에 대해서는 하나의 Reservation 만을 가진다.
2. Reservation은 연박을 고려하지 않는다.
3. Reservation은 대실 등의 경우를 고려하지 않고 숙박의 경우만을 다룬다.
1. 날짜
2. Region
3. AccommodationType
Modeling
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id String @id
email String @unique
encryptedPassword String
profile UserProfile?
createdAt DateTime @default(now())
reservations Reservation[]
reviews Review[]
}
model UserProfile {
userId String @id
user User @relation(fields: [userId], references: [id])
nickname String
phoneNumber String?
updatedAt DateTime @updatedAt
}
model Partner {
id String @id
email String @unique
encryptedPassword String
businessName String
phoneNumber String
staffName String
accommodations Accommodation[]
createdAt DateTime @default(now())
}
model Accommodation {
id Int @id @default(autoincrement())
partner Partner @relation(fields: [partnerId], references: [id])
partnerId String
regions Region[]
type AccommodationType
name String
description String @default("")
rooms Room[]
address1 String
address2 String
latitude Float
longitude Float
imgUrl String @default("")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
enum AccommodationType {
hotel
motel
resort
pension
guestHouse
poolVilla
camping
glamping
}
model Region {
id Int @id @default(autoincrement())
name String @unique
label String
accommodations Accommodation[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Room {
id Int @id @default(autoincrement())
accommodation Accommodation @relation(fields: [accommodationId], references: [id])
accommodationId Int
name String
description String @default("")
originalPrice Int
price Int
checkInTime String
checkOutTime String
reservations Reservation[]
reviews Review[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Reservation {
id String @id
room Room @relation(fields: [roomId], references: [id])
roomId Int
date DateTime
reservedBy User? @relation(fields: [reservedById], references: [id])
reservedById String?
reservedAt DateTime?
checkedInAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([roomId, date])
}
model Review {
id Int @id @default(autoincrement())
user User @relation(fields: [userId], references: [id])
userId String
room Room @relation(fields: [roomId], references: [id])
roomId Int
rating Int
content String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
main.ts
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule);
await app.listen(5050);
}
bootstrap();
prisma
npm install prisma @prisma/client
npx prisma init
"prisma": {
"schema": "./src/db/prisma/schema.prisma",
"seed": "ts-node ./src/db/prisma/prisma.seed.ts"
}
import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
@Injectable()
export class PrismaService
extends PrismaClient
implements OnModuleInit, OnModuleDestroy
{
public client: PrismaClient;
async onModuleInit() {
await this.$connect(); //this 자체가 prismaClient 가 됐음.
}
async onModuleDestroy() {
await this.$disconnect();
}
}
account/accommodation CRUD
import {
Body,
Controller,
Get,
Param,
ParseIntPipe,
Patch,
Post,
} from '@nestjs/common';
import { Partner } from '@prisma/client';
import { DPartner } from 'src/decorators/partner.decorator';
import { Private } from 'src/decorators/private.decorator';
import { AccommodationsCreateDto as AccommodationsRegisterDto } from './accommodations.dto';
import { AccommodationsService } from './accommodations.service';
@Controller('accommodations')
export class AccommodationsController {
constructor(private readonly accommodationsService: AccommodationsService) {}
@Post()
@Private('partner')
registerAccommodation(
@DPartner() partner: Partner,
@Body() dto: AccommodationsRegisterDto,
) {
return this.accommodationsService.createAccommodation({
...dto,
partnerId: partner.id,
});
}
@Get()
getAccommodations() {
return this.accommodationsService.getAccommodations();
}
@Get(':accommodationId')
getAccommodation(
@Param('accommodationId', ParseIntPipe) accommodationId: number,
) {
return this.accommodationsService.getAccommodation(+accommodationId);
}
@Patch(':accommodationId')
@Private('partner')
updateAccommodation() {}
}
https://docs.nestjs.com/security/authentication
//private.decorator.ts
import { SetMetadata } from '@nestjs/common';
import { AccountType } from 'src/domains/accounts/accounts.type';
export const Private = (accountType: AccountType) =>
SetMetadata('accountType', accountType);
import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Reflector } from '@nestjs/core';
import { Request } from 'express';
import { JwtPayload, verify } from 'jsonwebtoken';
import { AccountType } from 'src/domains/accounts/accounts.type';
import { PrismaService } from './../db/prisma/prisma.service';
@Injectable()
export class AuthGuard implements CanActivate {
constructor(
private reflector: Reflector,
private readonly configService: ConfigService,
private readonly prismaService: PrismaService,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const accountTypeInDecorator = this.reflector.getAllAndOverride<
'user' | 'partner'
>('accountType', [context.getHandler(), context.getClass()]);
if (!accountTypeInDecorator) {
return true;
}
const request = context.switchToHttp().getRequest<Request>();
const accessToken = this.extractTokenFromHeader(request);
if (!accessToken) throw new UnauthorizedException();
try {
const secret = this.configService.getOrThrow<string>('JWT_SECRET_KEY');
const { sub: id, accountType: accountTypeInAccessToken } = verify(
accessToken,
secret,
) as JwtPayload & { accountType: AccountType }; // 제네릭이 없어서 이렇게 타입 지정
//accessToken 이 유효하다
if (accountTypeInDecorator !== accountTypeInAccessToken)
throw new Error(); // 파트너만 접근할 수 있는 라우트인데 유저 토큰이 들어왔을때
if (accountTypeInDecorator === 'user') {
const user = await this.prismaService.user.findUniqueOrThrow({
where: { id },
});
request.user = user;
} else {
const partner = await this.prismaService.partner.findUniqueOrThrow({
where: { id },
});
request.partner = partner;
}
} catch {
throw new UnauthorizedException();
}
return true;
}
private extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? []; // 만약에 null 이면 빈 배열
return type === 'Bearer' ? token : undefined;
}
}
'accountType' 데코레이터의 값 확인한다.
request 의 헤더에서 토큰을 읽고, 이를 verify 로 검증한다.
토큰의 accountType 과 데코레이터에서 가져온 accountType 이 일치하는지 검사한다.
//설치
npm i @nestjs/config
//app.module.ts
@Module({
imports: [
DomainsModule,
PrismaModule,
ConfigModule.forRoot({
isGlobal: true,
}),
],
https://docs.nestjs.com/custom-decorators
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { Request } from 'express';
export const DPartner = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest<Request>();
return request.partner;
},
);
Gaurd 의 request.partner = Partner; 로 partner 정보를 얻게되고,
@DUser에서 가드에 의해 첨부된 partner 객체를 추출.