// src\auth\auth.middleware.ts
// authorization토큰 인증
import {
Injectable,
NestMiddleware,
UnauthorizedException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt'; // JWT 토큰을 생성하고 검증하는 데 사용하는 NestJS 서비스.
import { NextFunction, Request, Response } from 'express';
import { ConfigService } from '@nestjs/config'; // 환경 변수에서(.env) JWT 시크릿 키(JWT_SECRET_KEY)를 가져오는 데 사용
// Request 인터페이스 확장
// 토큰 검증 후 사용자 정보를 요청 객체에 저장
interface TokenRequest extends Request {
user?: any; // user 프로퍼티를 추가
}
@Injectable() // 의존성 주입(DI)
export class AuthMiddleware implements NestMiddleware {
constructor(
private jwtService: JwtService,
private configService: ConfigService,
) {}
// use 메서드는 NestJS의 미들웨어에서 필수적으로 구현해야 하는 메서드
use(req: TokenRequest, res: Response, next: NextFunction) {
const authHeader = req.headers.authorization;
// 토큰 유무 확인
if (!authHeader || !authHeader.startsWith('Bearer ')) {
throw new UnauthorizedException('토큰이 필요합니다.');
}
const token = authHeader.split(' ')[1]; // Beare 뒤의 토큰값 가져오기
try {
// jwtService.verify(token, {비밀키}) 를 이용해 토큰 검증
const decoded = this.jwtService.verify(token, {
// 환경변수 가져오기
secret: this.configService.get<string>('JWT_SECRET_KEY'),
});
req.user = decoded; // 요청 객체에 사용자 정보 저장
next();
} catch (error) {
throw new UnauthorizedException('만료되었거나 유효하지 않은 토큰입니다.');
}
}
}
인증 미들웨어를 사용할 때
1. auth서비스, 컨트롤러, 모듈, jwt.전략? 파일을 만들어 항목별 컨트롤러에서 지정해 쓰는 방법과
@Patch('me')
@UseGuards(AuthGuard('jwt')) // 토큰 인증
async updateUser(@Body() updateUserDto: UpdateUserDto, @Request() req) {
const userPayload = req.user;
return this.userService.update(updateUserDto, userPayload);
}
2. 사용하려는 항목의 모듈에서 사용하는 url을 지정하는 방법이 있습니다.
// user 모듈에서 export할 때 예외 사항 지정
export class UserModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(AuthMiddleware)
.exclude(
{ path: 'user/login', method: RequestMethod.POST },
{ path: 'user/signup', method: RequestMethod.POST },
{ path: 'user/refresh', method: RequestMethod.GET },
{ path: 'user/users', method: RequestMethod.GET },
)
.forRoutes(UserController);
}
}
토큰 발행
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config'; // .env 파일이나 환경 변수를 쉽게 불러올 수 있도록 도와주는 NestJS의 서비스.
...
@Injectable()
export class UserService {
constructor(
@InjectRepository(User)
private userRepository: Repository<User>,
private jwtService: JwtService, // JWT 토큰 생성을 위해 주입한 서비스
private configService: ConfigService, // .env 파일에 있는 환경 변수 불러오기
) {}
...
// 로그인 ...
// JWT 토큰 생성
const payload = { id: user.id, email: user.email };
const accessToken = this.createAccessToken(payload);
const refreshToken = this.createRefreshToken(payload);
return { accessToken, refreshToken };
}
/** 액세스 토큰 생성 */
private createAccessToken(payload: object): string {
return this.jwtService.sign(payload, {
secret: this.configService.get<string>('JWT_SECRET_KEY'),
// ACCESS_TOKEN_EXPIRY값이 없으면 15분으로 지정
expiresIn: this.configService.get<string>('ACCESS_TOKEN_EXPIRY', '15m'),
});
}
/** 리프레시 토큰 생성 */
private createRefreshToken(payload: object): string {
return this.jwtService.sign(payload, {
secret: this.configService.get<string>('JWT_SECRET_KEY'),
expiresIn: this.configService.get<string>('REFRESH_TOKEN_EXPIRY', '7d'),
});
}
토큰 재발행(재발행할 근거가 되는 리프레시토큰이 유효해야함)
로그아웃(사용못하는 토큰 발급)
1. Redis를 사용해 블랙리스트(로그아웃인 상태의 유저)에 추가하는 방법(여러 파일을 추가해야함)
2. 사용불가능한 토큰 발행 ( 토큰이 자동으로 해더에 추가되어야 함)
유효기한을 1초로 주거나
payload에 id를 안넣거나
아무 의미없는 값(ex. 0)을 토큰값으로 주기
비밀번호 암호화
// 비밀번호 해싱 함수
private async hashPassword(password: string): Promise<string> {
// .env 파일에서 BCRYPT_SALT_ROUNDS 값을 가져오고, 없으면 기본값 10을 사용.
const saltRounds = Number(
this.configService.get('BCRYPT_SALT_ROUNDS', '10'),
);
console.log(`Salt Rounds: ${saltRounds}, Type: ${typeof saltRounds}`); // 디버깅용 로그
return bcrypt.hash(password, saltRounds); // 비밀번호를 해싱해서 안전하게 저장.
}
...
// 비밀번호 해싱
createUserDto.password = await this.hashPassword(createUserDto.password);
비밀번호 검증
password : 로그인 바디로 입력받은 비번
user.password : user테이블에 저장된 암호화된 비번
bcrypt.compare(정상값, 해싱된 값) : 암호화 된 값을 해독해 정상값과 같은지 검증하는 코드
※ 해싱은 입력값(비밀번호)을 고정된 길이의 난수화된 문자열(해시 값) 로 변환하는 과정
※ 일반적으로 비밀번호 저장 시 Bcrypt 같은 느린 해시 알고리즘을 사용하는 것이 안전
// 비밀번호 검증
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) {
throw new UnauthorizedException(
`${email} 유저의 비밀번호가 올바르지 않습니다.`,
);
}
유저 모듈
// user.module.ts
import {
Module,
MiddlewareConsumer,
NestModule,
RequestMethod,
} from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt'; // WT 관련 기능을 제공하는 모듈. 이를 imports 배열에 추가하여 사용
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigService } from '@nestjs/config';
import { User } from './entities/user.entity';
import { UserController } from './user.controller';
import { UserService } from './user.service';
import { AuthMiddleware } from '../auth/auth.middleware'; // 분리된 미들웨어
@Module({
imports: [
TypeOrmModule.forFeature([User]),
JwtModule.registerAsync({
useFactory: (config: ConfigService) => ({
secret: config.get<string>('JWT_SECRET_KEY'),
}),
inject: [ConfigService],
}),
],
controllers: [UserController],
providers: [UserService],
exports: [UserService, JwtModule],
})
// export class UserModule {}
export class UserModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(AuthMiddleware)
.exclude(
{ path: 'user/login', method: RequestMethod.POST },
{ path: 'user/signup', method: RequestMethod.POST },
{ path: 'user/refresh', method: RequestMethod.GET },
{ path: 'user/users', method: RequestMethod.GET },
)
.forRoutes(UserController);
}
}
유저 서비스
// 유저 서비스
import _ from 'lodash';
import {
BadRequestException,
Injectable,
ConflictException,
NotFoundException,
UnauthorizedException,
Req,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config'; // .env 파일이나 환경 변수를 쉽게 불러올 수 있도록 도와주는 NestJS의 서비스.
import * as bcrypt from 'bcrypt';
import { CreateUserDto } from './dto/create-user.dto';
import { LoginUserDto } from './dto/login-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { DeleteUserDto } from './dto/delete-user.dto';
import { User } from './entities/user.entity';
@Injectable()
export class UserService {
constructor(
@InjectRepository(User)
private userRepository: Repository<User>,
private jwtService: JwtService, // JWT 토큰 생성을 위해 주입한 서비스
private configService: ConfigService, // .env 파일에 있는 환경 변수 불러오기
) {}
// 비밀번호 해싱 함수
private async hashPassword(password: string): Promise<string> {
// .env 파일에서 BCRYPT_SALT_ROUNDS 값을 가져오고, 없으면 기본값 10을 사용.
const saltRounds = Number(
this.configService.get('BCRYPT_SALT_ROUNDS', '10'),
);
console.log(`Salt Rounds: ${saltRounds}, Type: ${typeof saltRounds}`); // 디버깅용 로그
return bcrypt.hash(password, saltRounds); // 비밀번호를 해싱해서 안전하게 저장.
}
//POST 회원가입 /user/signup
async create(createUserDto: CreateUserDto) {
if (createUserDto.verifyPassword !== createUserDto.password) {
throw new UnauthorizedException(`비밀번호가 서로 다릅니다.`);
}
const existUser = await this.userRepository.findOne({
where: { email: createUserDto.email },
});
if (existUser) {
throw new ConflictException(
`이미 가입된 ID입니다. ID: ${createUserDto.email}`,
);
}
// verifyPassword 삭제
delete createUserDto.verifyPassword;
if (!createUserDto.password?.trim()) {
throw new NotFoundException(`비밀번호를 입력하세요`);
}
// 비밀번호 해싱 후 저장
console.log(createUserDto.password); // pw1234
// 여기서 막힘
createUserDto.password = await this.hashPassword(createUserDto.password);
console.log('회원가입 비번해싱 :', createUserDto.password);
const newUser = this.userRepository.create(createUserDto);
await this.userRepository.save(newUser);
console.log('회원가입 서비스 - 저장성공');
return {
message: '회원가입이 완료되었습니다.',
email: createUserDto.email,
};
}
//POST 로그인 /user/login
async login(loginUserDto: LoginUserDto) {
const { email, password } = loginUserDto;
const user = await this.userRepository.findOne({
where: { email, deletedAt: null },
select: ['id', 'email', 'password'],
});
if (!user) {
throw new NotFoundException(`${email} 유저를 찾을 수 없습니다.`);
}
// 비밀번호 검증
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) {
throw new UnauthorizedException(
`${email} 유저의 비밀번호가 올바르지 않습니다.`,
);
}
// JWT 토큰 생성
const payload = { id: user.id, email: user.email };
const accessToken = this.createAccessToken(payload);
const refreshToken = this.createRefreshToken(payload);
return { accessToken, refreshToken };
}
/** 액세스 토큰 생성 */
private createAccessToken(payload: object): string {
return this.jwtService.sign(payload, {
secret: this.configService.get<string>('JWT_SECRET_KEY'),
expiresIn: this.configService.get<string>('ACCESS_TOKEN_EXPIRY', '15m'),
});
}
/** 리프레시 토큰 생성 */
private createRefreshToken(payload: object): string {
return this.jwtService.sign(payload, {
secret: this.configService.get<string>('JWT_SECRET_KEY'),
expiresIn: this.configService.get<string>('REFRESH_TOKEN_EXPIRY', '7d'),
});
}
// accessToken 재발행
/** 토큰 재발행 */
async refreshToken(refreshToken: string) {
try {
const decoded = this.jwtService.verify(refreshToken, {
secret: this.configService.get<string>('JWT_SECRET_KEY'),
});
const payload = { id: decoded.id, email: decoded.email };
const newAccessToken = this.createAccessToken(payload);
//const newRefreshToken = this.createRefreshToken(payload);
return { accessToken: newAccessToken }; //;, refreshToken: newRefreshToken
} catch (error) {
throw new UnauthorizedException('유효하지 않은 리프레시 토큰입니다.');
}
}
//POST 로그아웃 /user/logout
/** 로그아웃 (토큰 유효 시간 1초 미만으로 설정) */
async logout(user) {
const payload = { id: user.id, email: user.email };
console.log('로그아웃 서비스');
// 만료 시간이 1초 미만인 액세스 토큰과 리프레시 토큰 생성
const expiredAccessToken = this.jwtService.sign(payload, {
secret: this.configService.get<string>('JWT_SECRET_KEY'),
expiresIn: '1s',
});
const expiredRefreshToken = this.jwtService.sign(payload, {
secret: this.configService.get<string>('JWT_SECRET_KEY'),
expiresIn: '1s',
});
return {
message: '로그아웃이 성공적으로 완료되었습니다.',
accessToken: expiredAccessToken,
refreshToken: expiredRefreshToken,
};
}
// test용 전체조회
async findAll() {
const user = await this.userRepository.find({});
if (!user) {
throw new NotFoundException(`유저를 찾을 수 없습니다.`);
}
return user;
}
//GET 회원 정보 조회 /user/:userId
async findOne(userId: number) {
if (!userId) {
throw new UnauthorizedException('찾으려는 userId를 입력하세요');
}
const user = await this.userRepository.findOne({
where: { id: userId },
select: ['name', 'email'],
});
if (!user) {
throw new NotFoundException(`유저를 찾을 수 없습니다.`);
}
return user;
}
//GET 내 정보 상세조회 /user/me
async findMe(userPayload: any) {
const user = await this.userRepository.findOne({
where: { id: userPayload.id },
select: ['name', 'email', 'password'],
});
if (!user) {
throw new NotFoundException(`유저 정보를 찾을 수 없습니다.`);
}
return user;
}
//PATCH 회원 정보 수정 /user/me
// 이름, 비번 수정
async update(updateUserDto: UpdateUserDto, userPayload: any) {
if (!updateUserDto) {
throw new UnauthorizedException('수정할 정보를 입력하세요');
}
// 조회
const myInfo = await this.userRepository.findOne({
where: { id: userPayload.id },
select: ['password', 'name'],
});
if (!myInfo) {
throw new NotFoundException(`내 계정정보를 확인할 수 없습니다.`);
}
// 비번 비교 (bcrypt 사용)
// bcrypt.compare() : bcrypt 라이브러리에서 제공하는 함수로, 해시된 비밀번호와 사용자로부터 입력받은 평문 비밀번호를 비교할 때 사용
const isMatch = await bcrypt.compare(
updateUserDto.password,
myInfo.password,
);
if (!isMatch) {
throw new UnauthorizedException('비밀번호가 일치하지 않습니다.');
}
// 업데이트할 데이터 생성
const updatedData: Partial<User> = {
name: updateUserDto.name || myInfo.name,
password: updateUserDto.newPassword
? await this.hashPassword(updateUserDto.newPassword)
: myInfo.password,
};
// updateUserDto 삭제
if (updateUserDto.newPassword) {
delete updateUserDto.newPassword;
}
// 데이터베이스 업데이트
// await this.userRepository.update({ id: userPayload.id }, updatedData);
//save()는 데이터가 존재하면 업데이트하고, 존재하지 않으면 삽입(INSERT)한다.
await this.userRepository.save({ id: userPayload.id, ...updatedData });
const newUser = await this.userRepository.findOne({
where: { id: userPayload.id },
select: ['id', 'name', 'email'],
});
if (!newUser) {
throw new NotFoundException(`${userPayload.id} 유저를 찾을 수 없습니다.`);
}
// JWT 토큰 생성
const payload = { id: newUser.id, email: newUser.email };
const accessToken = this.createAccessToken(payload);
const refreshToken = this.createRefreshToken(payload);
return { accessToken, refreshToken, newUser };
}
//DELETE 회원 탈퇴 /user/me
async remove(deleteUserDto: DeleteUserDto, userPayload: any) {
if (!deleteUserDto) {
throw new UnauthorizedException('수정할 정보를 입력하세요');
}
// 조회
const myInfo = await this.userRepository.findOne({
where: { id: userPayload.id },
select: ['password'],
});
if (!myInfo) {
throw new NotFoundException(`내 계정정보를 확인할 수 없습니다.`);
}
// 비번 비교
const isMatch = await bcrypt.compare(
deleteUserDto.password,
myInfo.password,
);
if (!isMatch) {
throw new UnauthorizedException('비밀번호가 일치하지 않습니다.');
}
// 소프트 삭제 수행
const result = await this.userRepository.softDelete({ id: userPayload.id });
if (result.affected === 0) {
throw new BadRequestException('회원을 찾지 못했습니다.');
}
const deletedUser = await this.userRepository.findOne({
where: { id: userPayload.id },
withDeleted: true,
});
if (!deletedUser?.deletedAt) {
throw new BadRequestException('회원 탈퇴에 실패했습니다.');
}
return { message: '회원 탈퇴가 완료되었습니다.' };
}
}
유저 컨트롤러
// 유저 컨트롤러
import {
Controller,
Headers,
Get,
Post,
Body,
Req,
Patch,
Param,
Delete,
Request,
BadRequestException,
UseGuards,
UnauthorizedException,
} from '@nestjs/common';
import { UserService } from './user.service';
import { CreateUserDto } from './dto/create-user.dto';
import { LoginUserDto } from './dto/login-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { DeleteUserDto } from './dto/delete-user.dto';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { AuthGuard } from '@nestjs/passport';
@Controller('user')
export class UserController {
constructor(private readonly userService: UserService) {}
/**POST 회원가입 /user/signup
POST 로그인 /user/login
POST 로그아웃 /user/logout
GET 회원 정보 조회 /user/:userId
GET 내 정보 상세조회 /user/me
PATCH 회원 정보 수정 /user/me
DELETE 회원 탈퇴 /user/me */
@Post('/signup')
async signUp(@Body() createUserDto: CreateUserDto) {
console.log('회원가입 컨트롤러');
return this.userService.create(createUserDto);
}
/** 로그인 시 토큰 발급 */
@Post('/login')
async login(@Body() loginUserDto: LoginUserDto) {
return this.userService.login(loginUserDto);
}
/** 토큰 재발행 */
@Get('/refresh')
async refreshAccessToken(@Headers('RefreshToken') refreshToken: string) {
if (!refreshToken || !refreshToken.startsWith('Bearer ')) {
throw new UnauthorizedException('리프레시 토큰이 없습니다.');
}
const myRefreshToken = refreshToken.split(' ')[1];
return this.userService.refreshToken(myRefreshToken);
}
/** 로그아웃 (토큰 무효화) */
@Post('/logout')
// @UseGuards(AuthGuard('jwt')) // ✅ 토큰 필수
async logout(@Request() req) {
console.log('로그아웃 컨트롤 - ', req.user);
return this.userService.logout(req.user);
}
@Get('/users')
async users() {
return this.userService.findAll();
}
@Get('userId/:userId')
async getUserInfo(@Param('userId') userId: string, @Request() req) {
//const userPayload = req.user;
return this.userService.findOne(+userId);
}
@Get('me')
async getMyInfo(@Request() req) {
const userPayload = req.user;
return this.userService.findMe(userPayload);
}
@Patch('me')
async updateUser(@Body() updateUserDto: UpdateUserDto, @Request() req) {
const userPayload = req.user;
return this.userService.update(updateUserDto, userPayload);
}
@Delete('me')
async deleteUser(@Body() deleteUserDto: DeleteUserDto, @Request() req) {
const userPayload = req.user;
return this.userService.remove(deleteUserDto, userPayload);
}
}
DTO
./dto/create-user.dto
import { PickType } from '@nestjs/mapped-types'; // 타입 가져오기
import { User } from '../entities/user.entity';
import { ApiProperty } from '@nestjs/swagger';
import {
IsDate,
IsNotEmpty,
IsNumber,
IsOptional,
IsString,
Length,
} from 'class-validator'; // 데코레이터 가져오기
export class CreateUserDto extends PickType(User, [
'email',
'name',
'password',
]) {
@ApiProperty({ example: '청게 장조림' })
@IsNotEmpty({ message: '이름을 입력해주세요' })
@IsString()
name: string;
@ApiProperty({ example: 'email1234@gmail.com' })
@IsNotEmpty({ message: 'email을 입력해주세요' })
@IsString()
email: string;
@ApiProperty({ example: 'pw1234' })
@IsNotEmpty({ message: '비밀번호를 입력해주세요' })
@IsString()
@Length(1, 8, { message: '비밀번호는 1자 이상 8자 이하로 입력해주세요' })
password: string;
@ApiProperty({ example: 'pw1234' })
@IsNotEmpty({ message: '비밀번호를 확인해주세요' })
@IsString()
@Length(1, 30, { message: '비밀번호는 1자 이상 30자 이하로 입력해주세요' })
verifyPassword: string;
}
./dto/login-user.dto
import { PickType } from '@nestjs/mapped-types'; // 타입 가져오기
import { User } from '../entities/user.entity';
import { ApiProperty } from '@nestjs/swagger';
import {
IsDate,
IsNotEmpty,
IsNumber,
IsOptional,
IsString,
} from 'class-validator'; // 데코레이터 가져오기
export class LoginUserDto extends PickType(User, ['email', 'password']) {
@ApiProperty({ example: 'email1234@gmail.com' })
@IsNotEmpty({ message: 'email을 입력해주세요' })
@IsString()
email: string;
@ApiProperty({ example: 'pw1234' })
@IsNotEmpty({ message: '비밀번호를 입력해주세요' })
@IsString()
password: string;
}
./dto/update-user.dto'
import { PickType } from '@nestjs/mapped-types'; // 타입 가져오기
import { User } from '../entities/user.entity';
import { ApiProperty } from '@nestjs/swagger';
import {
IsDate,
IsNotEmpty,
IsNumber,
IsOptional,
IsString,
} from 'class-validator'; // 데코레이터 가져오기
export class UpdateUserDto extends PickType(User, ['name', 'password']) {
@ApiProperty({ example: '청게 장조림' })
@IsString()
name: string;
@ApiProperty({ example: 'pw1234' })
@IsString()
newPassword: string;
@ApiProperty({ example: 'pw1234' })
@IsNotEmpty({ message: '비밀번호를 입력해주세요' })
@IsString()
password: string;
}
./dto/delete-user.dto';
import { PickType } from '@nestjs/mapped-types'; // 타입 가져오기
import { User } from '../entities/user.entity';
import { ApiProperty } from '@nestjs/swagger';
import {
IsDate,
IsNotEmpty,
IsNumber,
IsOptional,
IsString,
} from 'class-validator'; // 데코레이터 가져오기
export class DeleteUserDto extends PickType(User, ['password']) {
@ApiProperty({ example: 'pw1234' })
@IsNotEmpty({ message: '비밀번호를 입력해주세요' })
@IsString()
password: string;
}
user.entity.ts
import { Board } from 'src/board/entities/board.entity';
import { Member } from 'src/member/entities/member.entity';
import {
Column,
CreateDateColumn,
DeleteDateColumn,
Entity,
OneToMany,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity({
name: 'users',
})
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column({ type: 'varchar', unique: true, nullable: false })
email: string;
@Column({ type: 'varchar', nullable: false, length: 15 })
name: string;
@Column({ type: 'varchar', length: 255, nullable: false }) // 해싱하면 길어지니 한계 없애기
password: string;
// 예시 : '$2b$10$1dcY95A77vNF5RgUl8MwYeee3ED2KnmnIUlCx51eHY7v5kW7n7igi'
@CreateDateColumn()
createAt: Date;
@UpdateDateColumn()
updatedAt: Date;
@DeleteDateColumn()
deletedAt: Date;
@OneToMany(() => Member, (member) => member.user)
member: Member[];
@OneToMany(() => Board, (board) => board.user)
board: Board[];
}
db 마이그레이션
db에 마이그레이션 테이블이 생김.
마이그레이션에서 이전기록을 보는 방법은 아직 모르겠음
TypeORM 및 관련 패키지 설치
npm install typeorm @nestjs/typeorm mysql2
type ORM 버전 확인
npx typeorm -v
package.json에 추가
"scripts": {
"typeorm": "typeorm-ts-node-commonjs"
}
마이그레이션 실행
npx typeorm migration:generate -n UpdateUserNameLength
# .env
DB_USERNAME = 'root'
DB_PASSWORD = 'aaaa1234'
DB_HOST = 'localhost'
DB_PORT = 3306
DB_NAME = 'Trello'
DB_SYNC = false
# .env 파일의 값은 기본적으로 문자열입니다.
# Number로 사용하려면 환경변수를 가져가서 숫자타입으로 바꾸기
JWT_SECRET_KEY="gerolly10Legs"
ACCESS_TOKEN_EXPIRY=15m # 15분
REFRESH_TOKEN_EXPIRY=7d # 7일
BCRYPT_SALT_ROUNDS=10 # 암호화 2¹⁰(= 1024)번 연산
// app.module.ts
import Joi from 'joi';
import { SnakeNamingStrategy } from 'typeorm-naming-strategies';
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule, TypeOrmModuleOptions } from '@nestjs/typeorm';
import { BoardModule } from './board/board.module';
import { CommentModule } from './comment/comment.module';
import { ColumnModule } from './column/column.module'; // 컬럼 모듈 가져오기
import { CardModule } from './card/card.module';
import { AlarmModule } from './alarm/alarm.module';
import { MemberModule } from './member/member.module';
import { UserModule } from './user/user.module';
import { FileModule } from './file/file.module';
const typeOrmModuleOptions = {
useFactory: async (
configService: ConfigService,
): Promise<TypeOrmModuleOptions> => ({
namingStrategy: new SnakeNamingStrategy(),
type: 'mysql',
username: configService.get('DB_USERNAME'),
password: configService.get('DB_PASSWORD'),
host: configService.get('DB_HOST'),
port: configService.get('DB_PORT'),
database: configService.get('DB_NAME'),
entities: [__dirname + '/**/entities/*.{ts,js}'], // 이곳에서 자신의 작업물의 엔티티 등록 - 경로 잘못??
// entities: [
// process.env.NODE_ENV === 'production'
// ? 'dist/**/*.entity.js' // 배포 환경에서는 dist 폴더 사용
// : 'src/**/*.entity.ts', // 개발 환경에서는 src 사용
// ],
// synchronize은 테이블 만들 때만 쓰고 migration 사용하기(데이터 초기화 방지)
synchronize: configService.get('DB_SYNC'), // true - 기존 테이블이 있다면 자동으로 수정됨
migrationsRun: true, // 앱 실행 시 마이그레이션 적용
logging: true,
}),
inject: [ConfigService],
};
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
validationSchema: Joi.object({
JWT_SECRET_KEY: Joi.string().required(),
DB_USERNAME: Joi.string().required(),
DB_PASSWORD: Joi.string().required(),
DB_HOST: Joi.string().required(),
DB_PORT: Joi.number().required(),
DB_NAME: Joi.string().required(),
DB_SYNC: Joi.boolean().required(),
}),
}),
TypeOrmModule.forRootAsync(typeOrmModuleOptions),
BoardModule,
CommentModule,
ColumnModule,
UserModule,
MemberModule,
AlarmModule,
CardModule,
FileModule,
],
controllers: [],
providers: [],
})
export class AppModule {}
서버 빌드, 실행
서버 빌드
npm run build
서버 실행
npm run start:dev
package.json의 scripts
"scripts": {
"build": "nest build",
"start:dev": "nest start --watch"
},
package.json
{
"name": "trello-project",
"version": "0.0.1",
"description": "",
"author": "",
"private": true,
"license": "UNLICENSED",
"scripts": {
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json",
"typeorm": "typeorm-ts-node-commonjs"
},
"dependencies": {
"@nestjs/common": "^11.0.1",
"@nestjs/config": "^4.0.0",
"@nestjs/core": "^11.0.1",
"@nestjs/jwt": "^11.0.0",
"@nestjs/mapped-types": "^2.1.0",
"@nestjs/passport": "^11.0.4",
"@nestjs/platform-express": "^11.0.1",
"@nestjs/swagger": "^11.0.3",
"@nestjs/typeorm": "^11.0.0",
"@types/lodash": "^4.17.14",
"@types/papaparse": "^5.3.15",
"@types/passport-jwt": "^4.0.1",
"bcrypt": "^5.1.1",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"ioredis": "^5.4.2",
"joi": "^17.13.3",
"lodash": "^4.17.21",
"migration": "^0.3.0",
"multer": "^1.4.5-lts.1",
"mysql2": "^3.12.0",
"papaparse": "^5.5.1",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
"typeorm": "^0.3.20",
"typeorm-naming-strategies": "^4.1.0",
"uuid": "^11.0.5"
},
"devDependencies": {
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.18.0",
"@nestjs/cli": "^11.0.0",
"@nestjs/schematics": "^11.0.0",
"@nestjs/testing": "^11.0.1",
"@swc/cli": "^0.6.0",
"@swc/core": "^1.10.7",
"@types/bcrypt": "^5.0.2",
"@types/express": "^5.0.0",
"@types/jest": "^29.5.14",
"@types/multer": "^1.4.12",
"@types/node": "^22.13.0",
"@types/supertest": "^6.0.2",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-prettier": "^5.2.2",
"globals": "^15.14.0",
"jest": "^29.7.0",
"prettier": "^3.4.2",
"source-map-support": "^0.5.21",
"supertest": "^7.0.0",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.1",
"ts-jest": "^29.2.5",
"ts-loader": "^9.5.2",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.7.3",
"typescript-eslint": "^8.20.0"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}
'내일배움 과제 > 트렐로' 카테고리의 다른 글
jest 테스트 추가하기 (0) | 2025.02.05 |
---|---|
user CRUD API 구현하기 (0) | 2025.01.24 |
2차회의 (0) | 2025.01.23 |
1차회의 - 250122 (0) | 2025.01.22 |