Skip to content

[재하] 1122(수) 개발기록

박재하 edited this page Nov 22, 2023 · 2 revisions

목표

체크리스트

  • 게시글 본문 암복호화
  • Entity 간 관계 적용 (OneToMany, ManyToOne, OneToOne)
  • AuthGuard 적용
  • POST /board에 author <- nickname 직접 삽입
  • user 외래키 활용하도록 메소드 개선
  • 좋아요 중복 비허용 (조인테이블 생성)
  • S3에 사진 데이터 저장하기

게시글 본문 암복호화

AES 암복호화 모듈

스프린트때 직접 개발해두었던 암복호화 모듈을 활용해 utils에 넣었다. (학습메모 1)

// aes.util.ts
import crypto from 'crypto';
import { aesConfig } from '../config/aes.config';

const encryptAes = (plainText) => {
	const algorithm = 'aes-256-cbc'; // 암호 알고리즘
	const key = crypto.scryptSync(aesConfig.password, aesConfig.salt, 32); // 암호화 키
	const iv = aesConfig.iv; // 초기화 벡터

	const cipher = crypto.createCipheriv(algorithm, key, iv);
	let cipherText = cipher.update(plainText, 'utf8', 'base64');
	cipherText += cipher.final('base64');
	return cipherText;
};

const decryptAes = (cipherText) => {
	const algorithm = 'aes-256-cbc'; // 암호 알고리즘
	const key = crypto.scryptSync(aesConfig.password, aesConfig.salt, 32); // 암호화 키
	const iv = aesConfig.iv; // 초기화 벡터

	const decipher = crypto.createDecipheriv(algorithm, key, iv);
	let plainText = decipher.update(cipherText, 'base64', 'utf8');
	plainText += decipher.final('utf8');
	return plainText;
};

export { encryptAes, decryptAes };
import { configDotenv } from 'dotenv';
configDotenv();

export const aesConfig = {
	password: process.env.AES_PASSWORD,
	salt: process.env.AES_SALT,
	iv: Buffer.alloc(16, 0),
};

config 파일로 .env에 있는 AES_PASSWORD, AES_SALT를 활용하도록 보안처리

적용

이제 POST /board, PATCH /board/:idGET /board에 암복호화 처리를 넣어준다.

// board.service.ts
...
import { encryptAes, decryptAes } from 'src/utils/aes.util';

@Injectable()
export class BoardService {
	...

	async createBoard(createBoardDto: CreateBoardDto): Promise<Board> {
		const { title, content, author } = createBoardDto;

		const board = this.boardRepository.create({
			title,
			content: encryptAes(content), // AES 암호화하여 저장
			author,
		});
		const created: Board = await this.boardRepository.save(board);

		return created;
	}

	async findBoardById(id: number): Promise<Board> {
		const found: Board = await this.boardRepository.findOneBy({ id });
		if (!found) {
			throw new NotFoundException(`Not found board with id: ${id}`);
		}
		if (found.content) {
			found.content = decryptAes(found.content); // AES 복호화하여 반환
		}
		return found;
	}

	async updateBoard(id: number, updateBoardDto: UpdateBoardDto) {
		const board: Board = await this.findBoardById(id);

		// updateBoardDto.content가 존재하면 AES 암호화하여 저장
		if (updateBoardDto.content) {
			updateBoardDto.content = encryptAes(updateBoardDto.content);
		}

		const updatedBoard: Board = await this.boardRepository.save({
			...board,
			...updateBoardDto,
		});
		return updatedBoard;
	}
}

결과 화면

스크린샷 2023-11-22 오후 12 03 14

암호화돼서 잘 저장됨

스크린샷 2023-11-22 오후 12 03 37

GET으로 요청할 땐 복호화되어 반환됨

스크린샷 2023-11-22 오후 12 05 11

DB에 저장될때는 암호문으로! IV나 salt를 컬럼에 있는 값을 조합해서 쓰면 같은 input이라도 다른 암호문이 나오게 할 수 있다. 필요하면 개선하자

Entity 간 관계 적용 (OneToMany, ManyToOne, OneToOne)

OneToOne 관계 (Board <-> Image)

게시글 하나당 사진 최대 하나이므로 OneToOne. 없을 수도 있으므로 nullable을 넣었다. 학습메모 2 참고. Image Entity에는 별도로 설정이 필요 없다.

// board.entity.ts
@Entity()
export class Board extends BaseEntity {
	...
	@OneToOne(() => Image, { nullable: true })
	@JoinColumn()
	image_id: number;
}

image_id가 FK로 Image 엔티티의 PK인 id를 참조하게 되는 거임.

-- Active: 1694011841232@@192.168.64.2@3306@b1g1
CREATE TABLE `board` (
  ...
  UNIQUE KEY `REL_1e34245d55ca9414293c6e4276` (`imageIdId`),
  ...
  CONSTRAINT `FK_1e34245d55ca9414293c6e4276c` FOREIGN KEY (`imageIdId`) REFERENCES `image` (`id`),
  ...
) ENGINE=InnoDB AUTO_INCREMENT=138 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

board 테이블의 DDL을 확인해보면 image(id)에 대한 외래키 설정이 잘 되어 있는 것을 확인할 수 있다.

ManyToOne, OneToMany 관계 (board <-> user / board가 many)

하나의 작성자가 여러 글을 쓸 수 있으므로 이러한 관계가 정립된다.

@Entity()
export class Board extends BaseEntity {
	...
	@ManyToOne(() => User, (user) => user.boards, { onDelete: 'CASCADE' })
	user: User;
}
@Entity()
export class User {
  ...
	@OneToMany(() => Board, (board) => board.user)
	boards: Board[];
}

author를 외래키로 설정하고 user(nickname)을 참조하고 싶은 상태인데, 그런 설정은 찾아봐도 안나옴. 추후 가능하면 넣자. user.nickname으로 지금도 접근할 수 있다.

ON DELETE CASCADE 문도 삽입

결과화면

-- Active: 1694011841232@@192.168.64.2@3306@b1g1
CREATE TABLE `board` (
  `id` int NOT NULL AUTO_INCREMENT,
  `title` varchar(255) NOT NULL,
  `content` text,
  `author` varchar(50) NOT NULL,
  `like_cnt` int NOT NULL DEFAULT '0',
  `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
  `updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
  `imageId` int DEFAULT NULL,
  `userId` int DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `REL_6c2a2c2b30fbb895ef2dc41294` (`imageId`),
  KEY `FK_c9951f13af7909d37c0e2aec484` (`userId`),
  CONSTRAINT `FK_6c2a2c2b30fbb895ef2dc412947` FOREIGN KEY (`imageId`) REFERENCES `image` (`id`),
  CONSTRAINT `FK_c9951f13af7909d37c0e2aec484` FOREIGN KEY (`userId`) REFERENCES `user` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=138 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

AuthGuard 적용

대망의 AuthGuard 적용. 잘 동작할까..?

트러블 슈팅 1 : Custom Guard 의존성 문제

스크린샷 2023-11-22 오후 12 57 38

안됨. AuthModule을 board에서 import해도 안되는데, 학습메모 4 참고하면 AuthModule에서 guard 사용에 필요한 PassportModule과 JwtStrategy를 export해주지 않았기 때문인듯.

import { AuthModule } from 'src/auth/auth.module';

@Module({
	imports: [TypeOrmModule.forFeature([Board, Image]), AuthModule],
	controllers: [BoardController],
	providers: [BoardService],
})
export class BoardModule {}
@Module({
	imports: [
		PassportModule.register({ defaultStrategy: 'jwt' }),
		JwtModule.register(jwtConfig),
		TypeOrmModule.forFeature([User]),
	],
	controllers: [AuthController],
	providers: [AuthService, CookieAuthGuard, RedisRepository],
	exports: [CookieAuthGuard, PassportModule],
})
export class AuthModule {}

근데 JwtStrategy가 없다. 거기에 해당하는 게(validate 로직이 들어가는 게) CookieAuthGuard라는 페어분이 만드신 custom guard인데, 이걸 export해줘야 한다. (그래서 strategy 만들었다 지움)

스크린샷 2023-11-22 오후 1 08 37

그래도 안됨. 동작은 하는데 로그인을 해서 쿠키가 있는데도 통과가 안됨 코드도 보고 console.log()로 어디서 Unauthorized가 나오나 봤는데

// cookie auth guard
...

@Injectable()
export class CookieAuthGuard extends AuthGuard('jwt') {
	constructor(
		private readonly jwtService: JwtService,
		private readonly redisRepository: RedisRepository,
	) {
		super();
	}
	...

	async canActivate(context: ExecutionContext): Promise<boolean> {
		...

		const refreshToken = request.cookies['refreshToken'];
		try {
			const { userId, username, nickname } =
				this.jwtService.verify(refreshToken);
			request.user = { userId, username, nickname };
		} catch (error) {
			response.clearCookie(JwtEnum.ACCESS_TOKEN_COOKIE_NAME);
			response.clearCookie(JwtEnum.REFRESH_TOKEN_COOKIE_NAME);
      console.log(2);
			throw new UnauthorizedException('로그인이 필요합니다.');
		}
    ...
  }
}

이 jwtService 파트에서 try catch문으로 에러가 나는거였다. verify가 안된 게 아니라 그냥 jwtService가 정의가 안돼서 그런거임 의존성 주입이 제대로 안돼서!

따라서 저 CookieAuthGuard에서 의존하는 JwtServiceRedisRepository를 함께 export 및 import를 시켜줘야 한다 이말씀. 추가로 이 상황에선 PassportModule모듈은 필요없다.

import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './entities/user.entity';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { jwtConfig } from '../config/jwt.config';
import { RedisRepository } from './redis.repository';
import { CookieAuthGuard } from './cookie-auth.guard';

@Module({
	imports: [
		PassportModule.register({ defaultStrategy: 'jwt' }),
		JwtModule.register(jwtConfig),
		TypeOrmModule.forFeature([User]),
	],
	controllers: [AuthController],
	providers: [AuthService, CookieAuthGuard, RedisRepository],
	exports: [JwtModule, CookieAuthGuard, RedisRepository],
})
export class AuthModule {}

최종적으로 AuthModule에선 [JwtModule, CookieAuthGuard, RedisRepository] export.

스크린샷 2023-11-22 오후 1 33 54

로그인 하면

스크린샷 2023-11-22 오후 1 34 01

이제 Guard로 막혔던 게 잘 접근이 된다.

트러블 슈팅 2 : Sign Out, RefreshToken 삭제 로직 누락됨

스크린샷 2023-11-22 오후 1 55 50

로그아웃하면 다시 안돼야하는데 된다. HTTP 로그를 보니 accessToken만 지워가지고 소스코드를 봤다.

@Get('signout')
async signOut(@Res({ passthrough: true }) res: Response) {
  res.clearCookie('accessToken', { path: '/', httpOnly: true });
  return { message: 'success' };
}

accessToken만 지우고 refreshToken이 살아있으니 현재 인증관리 로직 상 guard를 통과할 때 요청 없이 자동으로 refresh되어서 다시 accessToken을 발급받아 인증이 되어버리는 것.

// auth.controller.ts
@Get('signout')
@UseGuards(CookieAuthGuard)
async signOut(@Req() req, @Res({ passthrough: true }) res: Response) {
	res.clearCookie('accessToken', { path: '/', httpOnly: true });
	res.clearCookie('refreshToken', { path: '/', httpOnly: true });
	await this.authService.signOut(req.user.username);
	return { message: 'success' };
}
// auth.service.ts
async signOut(username: string) {
  // redis에 저장된 refreshToken 삭제
  await this.redisRepository.del(username);
}
// redis.repository.ts
@Injectable()
export class RedisRepository {
	...
	async del(key: string) {
		return this.redisClient.del(key);
	}
}

기능은 다 만들어뒀는데 이러면 로그인 안한 상태에서 로그아웃을 하면 Unauthorized가 뜨긴함. 음..!

스크린샷 2023-11-22 오후 2 17 36 스크린샷 2023-11-22 오후 2 17 42

아무튼 이제 잘 제거되어 다시 UnauthorizedException을 받을 수 있게 된다.

@UseGuard(CookieAuthGuard) 적용

이제 검증이 완료됐으니 게시글 CRUD의 모든 기능에 가드를 넣어준다.

...
@Controller('board')
@ApiTags('게시글 API')
export class BoardController {
	constructor(private readonly boardService: BoardService) {}

	@Post()
	@UseGuards(CookieAuthGuard)
	@UsePipes(ValidationPipe)
	createBoard(@Body() createBoardDto: CreateBoardDto): Promise<Board> {
		return this.boardService.createBoard(createBoardDto);
	}

	@Get()
	@UseGuards(CookieAuthGuard)
	findAllBoards(): Promise<Board[]> {
		return this.boardService.findAllBoards();
	}

	@Get('by-author')
	@UseGuards(CookieAuthGuard)
	findAllBoardsByAuthor(@Query('author') author: string): Promise<Board[]> {
		return this.boardService.findAllBoardsByAuthor(author);
	}

	@Get(':id')
	@UseGuards(CookieAuthGuard)
	findBoardById(@Param('id', ParseIntPipe) id: number): Promise<Board> {
		return this.boardService.findBoardById(id);
	}

	@Patch(':id')
	@UseGuards(CookieAuthGuard)
	@UsePipes(ValidationPipe)
	updateBoard(
		@Param('id', ParseIntPipe) id: number,
		@Body() updateBoardDto: UpdateBoardDto,
	) {
		return this.boardService.updateBoard(id, updateBoardDto);
	}

	@Patch(':id/like')
	@UseGuards(CookieAuthGuard)
	patchLike(@Param('id', ParseIntPipe) id: number): Promise<Partial<Board>> {
		return this.boardService.patchLike(id);
	}

	@Patch(':id/unlike')
	@UseGuards(CookieAuthGuard)
	patchUnlike(@Param('id', ParseIntPipe) id: number): Promise<Partial<Board>> {
		return this.boardService.patchUnlike(id);
	}

	@Delete(':id')
	@UseGuards(CookieAuthGuard)
	deleteBoard(@Param('id', ParseIntPipe) id: number): Promise<void> {
		return this.boardService.deleteBoard(id);
	}

	@Post(':id/image')
	@UseGuards(CookieAuthGuard)
	@UseInterceptors(FileInterceptor('file', { dest: './uploads' }))
	@UsePipes(ValidationPipe)
	uploadFile(
		@Param('id', ParseIntPipe) board_id: number,
		@UploadedFile() file: CreateImageDto,
	): Promise<Board> {
		return this.boardService.uploadFile(board_id, file);
	}
}

결과 화면

로그인 했을 때

스크린샷 2023-11-22 오후 1 33 54 스크린샷 2023-11-22 오후 1 34 01

로그아웃 했을 때

스크린샷 2023-11-22 오후 2 17 36 스크린샷 2023-11-22 오후 2 17 42

POST /board에 author <- nickname 직접 삽입

기능 검증

// cookie-auth.guard.ts
request.user = { userId, username, nickname };

이제 Guard를 통해 req.user에 정보를 넣어주므로

@Get()
@UseGuards(CookieAuthGuard)
findAllBoards(@Req() req): Promise<Board[]> {
  console.log(req.user);
  return this.boardService.findAllBoards();
}

이제 가드만 넣으면 유저 정보를 언제든 얻을 수 있다.

스크린샷 2023-11-22 오후 2 26 28

요렇게! nickname까지!

Guard를 통해 user 정보 얻기

이제 create dto로 author를 입력받는 게 아닌 서버에서 자동 삽입해주도록 바꿔보자.

@Post()
@UseGuards(CookieAuthGuard)
@UsePipes(ValidationPipe)
@ApiOperation({ summary: '게시글 작성', description: '게시글을 작성합니다.' })
@ApiCreatedResponse({ status: 201, description: '게시글 작성 성공' })
@ApiBadRequestResponse({
	status: 400,
	description: '잘못된 요청으로 게시글 작성 실패',
})
createBoard(
	@Req() req,
	@Body() createBoardDto: CreateBoardDto,
): Promise<Board> {
	if (req.user && req.user.nickname)
		createBoardDto.author = req.user.nickname;
	return this.boardService.createBoard(createBoardDto);
}
// 서버에서 직접 삽입해주도록 변경 (validation 제거)
// @IsNotEmpty({ message: '게시글 작성자는 필수 입력입니다.' })
// @IsString({ message: '게시글 작성자는 문자열로 입력해야 합니다.' })
// @MaxLength(50, { message: '게시글 작성자는 50자 이내로 입력해야 합니다.' })
author: string;

validation 데코레이터는 모두 주석처리.

결과 화면

스크린샷 2023-11-22 오후 2 54 29

이제 author는 삽입되어도 무시하고 원래 닉네임으로 잘 들어간다.

user 외래키 활용하도록 메소드 개선

Entity 재정의

{ eager: true } 구문을 사용하지 않아 바로 반영이 안되어 수많은 삽질을 반복했다..

@Entity()
export class User {
	@OneToMany(() => Board, (board) => board.user, { eager: false })
	boards: Board[];
}
@Entity()
export class Board extends BaseEntity {
	@ManyToOne(() => User, (user) => user.boards, {
		eager: true,
		onDelete: 'CASCADE',
	})
	user: User;
}

결과적으로 보드쪽에서 {eager: true}, 유저쪽에서 {eager: false}, 해주면 DB에 잘 반영되더라

User Repository 의존성 주입

어째선진 모르겠지만 TypeORM에서 관계성이 있는 값을 등록할 때, id가 아닌 객체 자체를 삽입해줘야 한다.

따라서 User Repository를 Board에서도 불러와야 한다.

@Module({
	imports: [TypeOrmModule.forFeature([Board, Image, User]), AuthModule],
	controllers: [BoardController],
	providers: [BoardService],
})
export class BoardModule {}

board 모듈에서 TypeOrmModule에 User를 같이 불러와주고

get userdata 커스텀 데코레이터 및 DTO 작성

import { ExecutionContext, createParamDecorator } from '@nestjs/common';

export const GetUser = createParamDecorator((_, ctx: ExecutionContext) => {
	const req = ctx.switchToHttp().getRequest();
	return req.user;
});

@Req로 request 전체를 불러오지 않고 cookie의 jwt 토큰에서 추출한 User에 대한 데이터(id, username, nickname)만 추출하도록 @GetUser() 데코레이터를 새로 만들었다.

또한 이렇게 가져온 값의 유효성을 입증하기 위해 user-data.dto.ts를 board모듈에 추가해 필요한 곳에 타입을 지정하도록 했다.

import { IsInt, IsNotEmpty, IsString } from 'class-validator';

export class UserDataDto {
	@IsNotEmpty()
	@IsInt()
	userId: number;

	@IsNotEmpty()
	@IsString()
	username: string;

	@IsNotEmpty()
	@IsString()
	nickname: string;
}

Validation Pipe도 사용할 수 있게 각종 데코레이터를 추가했다.

POST /board

board의 생성 시엔 author를 집어넣는 대신, 참조된 user 객체의 nickname 항목을 가져다 쓰면 된다.

그렇기 때문에 해당 board entity에서 author를 아예 제거하고, user repository에서 직접 user 객체를 찾아 저장 시 삽입하도록 변경했다.

async createBoard(
	createBoardDto: CreateBoardDto,
	userData: UserDataDto,
): Promise<Board> {
	const { title, content } = createBoardDto;

	const user = await this.userRepository.findOneBy({ id: userData.userId });

	const board = this.boardRepository.create({
		title,
		content: encryptAes(content), // AES 암호화하여 저장
		user,
	});
	const createdBoard: Board = await this.boardRepository.save(board);
	createdBoard.user.password = undefined; // password 제거하여 반환
	return createdBoard;
}
스크린샷 2023-11-22 오후 6 52 34

PATCH /board/:id, DELETE /board/:id

수정, 삭제 시에는 단순 Guard 인증 뿐만이 아니라, 내가 작성한 게시글인 경우에만 처리가 가능해야 한다. 따라서 user data를 활용해 이를 검사한다.

async updateBoard(
	id: number,
	updateBoardDto: UpdateBoardDto,
	userData: UserDataDto,
) {
	const board: Board = await this.findBoardById(id);

	// 게시글 작성자와 수정 요청자가 다른 경우
	if (board.user.id !== userData.userId) {
		throw new BadRequestException('You are not the author of this post');
	}

	// updateBoardDto.content가 존재하면 AES 암호화하여 저장
	if (updateBoardDto.content) {
		updateBoardDto.content = encryptAes(updateBoardDto.content);
	}

	const updatedBoard: Board = await this.boardRepository.save({
		...board,
		...updateBoardDto,
	});
	return updatedBoard;
}

async deleteBoard(id: number, userData: UserDataDto): Promise<void> {
	const board: Board = await this.findBoardById(id);

	// 게시글 작성자와 삭제 요청자가 다른 경우
	if (board.user.id !== userData.userId) {
		throw new BadRequestException('You are not the author of this post');
	}

	const result = await this.boardRepository.delete({ id });
}
스크린샷 2023-11-22 오후 7 22 14

본인인 경우

스크린샷 2023-11-22 오후 7 24 02 스크린샷 2023-11-22 오후 7 24 21

다른 사용자인 경우 잘 차단되는 것 확인

트러블 슈팅

update, delete 간 findBoardById()의 호출로 인해 수정이 발생하지 않았을 때 평문으로 복호화된 레코드가 저장되어버리는 문제 발생

async findBoardById(@Param('id', ParseIntPipe) id: number): Promise<Board> {
	const found = await this.boardService.findBoardById(id);
	// AES 복호화
	if (found.content) {
		found.content = decryptAes(found.content); // AES 복호화하여 반환
	}
	return found;
}

decrypt 로직을 controller단으로 땡겨서 해당 문제를 해결했다.

서비스 내에서는 암호화된 채로만 사용한다.

GET /board/by-author

마지막으로 /board/by-author는 닉네임으로 조회를 하되 author 컬럼이 아닌 user.nickname을 조회하도록 변경했고,

아무런 파라미터가 없으면 본인 닉네임을 삽입하도록 했다.

findAllBoardsByAuthor(
	@Query('author') author: string,
	@GetUser() userData: UserDataDto,
): Promise<Board[]> {
	// 파라미터 없는 경우 로그인한 사용자의 게시글 조회
	author = author ? author : userData.nickname;
	return this.boardService.findAllBoardsByAuthor(author);
}
async findAllBoardsByAuthor(author: string): Promise<Board[]> {
	const boards = await this.boardRepository.findBy({
		user: { nickname: author },
	});
	return boards;
}
스크린샷 2023-11-22 오후 7 38 28

닉네임 입력한 경우

스크린샷 2023-11-22 오후 7 38 47

닉네임 없는 경우

결과 화면

  • POST /board 개선
스크린샷 2023-11-22 오후 6 52 34
  • PATCH /board/:id
스크린샷 2023-11-22 오후 7 22 14 스크린샷 2023-11-22 오후 7 24 02
  • DELETE /board/:id
스크린샷 2023-11-22 오후 7 24 21
  • GET /board/by-author
스크린샷 2023-11-22 오후 7 38 28 스크린샷 2023-11-22 오후 7 38 47

좋아요 중복 비허용 (조인테이블 생성)

Join Table 생성

// board.entity.ts
@ManyToMany(() => User, { eager: true })
@JoinTable()
likes: User[];

@Column({ type: 'int', default: 0 })
like_cnt: number;

Many To Many 및 @JoinTable() 데코레이터로 like에 대한 조인 테이블을 생성한다.

스크린샷 2023-11-22 오후 8 09 04

잘 생성됨

User Data 및 Validation Pipe 적용

@Patch(':id/like')
@UseGuards(CookieAuthGuard)
@UsePipes(ValidationPipe)
patchLike(
	@Param('id', ParseIntPipe) id: number,
	@GetUser() userData: UserDataDto,
): Promise<Partial<Board>> {
	return this.boardService.patchLike(id, userData);
}

@Patch(':id/unlike')
@UseGuards(CookieAuthGuard)
@UsePipes(ValidationPipe)
patchUnlike(
	@Param('id', ParseIntPipe) id: number,
	@GetUser() userData: UserDataDto,
): Promise<Partial<Board>> {
	return this.boardService.patchUnlike(id, userData);
}

컨트롤러 단에서 위에서 작업했던 것과 같은 방식으로 user data를 받아 서비스로 넘긴다.

like 비즈니스 로직 처리

like_cnt를 단순히 더하고 빼는 것이 아닌, jointable에 삽입하고 빼는 과정으로 처리하고, 그 배열의 길이를 like_cnt에 넣는 형식으로 해서 예외 상황을 없앤다.

async patchLike(id: number, userData: UserDataDto): Promise<Partial<Board>> {
	const board = await this.findBoardById(id);
	console.log(board);
	if (board.likes.find((user) => user.id === userData.userId)) {
		throw new BadRequestException('You already liked this post');
	}

	const user = await this.userRepository.findOneBy({ id: userData.userId });
	if (!user) {
		throw new NotFoundException(`Not found user with id: ${userData.userId}`);
	}

	board.likes.push(user);
	board.like_cnt = board.likes.length;

	const updatedBoard = await this.boardRepository.save(board);

	return { like_cnt: updatedBoard.like_cnt };
}

async patchUnlike(
	id: number,
	userData: UserDataDto,
): Promise<Partial<Board>> {
	const board = await this.findBoardById(id);
	if (!board.likes.find((user) => user.id === userData.userId)) {
		throw new BadRequestException('You have not liked this post');
	}

	const user = await this.userRepository.findOneBy({ id: userData.userId });
	if (!user) {
		throw new NotFoundException(`Not found user with id: ${userData.userId}`);
	}

	board.likes = board.likes.filter((user) => user.id !== userData.userId);
	board.like_cnt = board.likes.length;

	const updatedBoard = await this.boardRepository.save(board);
	return { like_cnt: updatedBoard.like_cnt };
}

잘못된 요청이 있으면 400 및 404 에러 처리를 해준다.

결과 화면

  • PATCH /board/:id/like
스크린샷 2023-11-22 오후 8 03 46 스크린샷 2023-11-22 오후 8 05 58
  • PATCH /board/:id/unlike
스크린샷 2023-11-22 오후 8 06 06 스크린샷 2023-11-22 오후 8 06 13

학습메모

  1. AES 암복호화
  2. one-to-one TypeORM 공식문서
  3. one-to-many, many-to-one TypeORM 공식문서
  4. 만들면서 배우는 NestJS 기초
  5. many-to-many TypeORM 공식문서
  6. CORS 허용

소개

규칙

학습 기록

[공통] 개발 기록

[재하] 개발 기록

[준섭] 개발 기록

회의록

스크럼 기록

팀 회고

개인 회고

멘토링 일지

Clone this wiki locally