diff --git a/src/app.controller.spec.ts b/src/app.controller.spec.ts index d22f389..b404825 100644 --- a/src/app.controller.spec.ts +++ b/src/app.controller.spec.ts @@ -1,22 +1,22 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { AppController } from './app.controller'; -import { AppService } from './app.service'; +// import { Test, TestingModule } from '@nestjs/testing'; +// import { AppController } from './app.controller'; +// import { AppService } from './app.service'; -describe('AppController', () => { - let appController: AppController; +// describe('AppController', () => { +// let appController: AppController; - beforeEach(async () => { - const app: TestingModule = await Test.createTestingModule({ - controllers: [AppController], - providers: [AppService], - }).compile(); +// beforeEach(async () => { +// const app: TestingModule = await Test.createTestingModule({ +// controllers: [AppController], +// providers: [AppService], +// }).compile(); - appController = app.get(AppController); - }); +// appController = app.get(AppController); +// }); - describe('root', () => { - it('should return "Hello World!"', () => { - expect(appController.getHello()).toBe('Hello World!'); - }); - }); -}); +// describe('root', () => { +// it('should return "Hello World!"', () => { +// expect(appController.getHello()).toBe('Hello World!'); +// }); +// }); +// }); diff --git a/src/app.module.ts b/src/app.module.ts index cf0d667..d76bda7 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,6 +1,4 @@ import { Module } from '@nestjs/common'; -import { AppController } from './app.controller'; -import { AppService } from './app.service'; import { PrismaModule } from './prisma/prisma.module'; import { UsersModule } from './users/users.module'; import { EventsModule } from './events/events.module'; @@ -9,6 +7,8 @@ import { MailModule } from './mails/mail.module'; import { ConfigModule } from '@nestjs/config'; import { DataModule } from './data/data.module'; import { SearchesModule } from './searches/searches.module'; +import { AppController } from './app.controller'; +import { AppService } from './app.service'; @Module({ imports: [ diff --git a/src/app.service.ts b/src/app.service.ts index 927d7cc..11db011 100644 --- a/src/app.service.ts +++ b/src/app.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common'; @Injectable() export class AppService { - getHello(): string { - return 'Hello World!'; - } + // getHello(): string { + // return 'Hello World!'; + // } } diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index a7af165..53ce146 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -7,22 +7,20 @@ import { UseGuards, Res, Req, -} from '@nestjs/common'; // Headers 추가 -import { AuthService } from './auth.service'; +} from '@nestjs/common'; import { ApiOkResponse, ApiOperation, ApiResponse, ApiTags, } from '@nestjs/swagger'; +import { AuthGuard } from '@nestjs/passport'; import { LoginDto } from './dto/login.dto'; -import { Request, Response } from 'express'; import { AuthEntity } from './entity/auth.entity'; -import { UsersService } from 'src/users/users.service'; -import { AuthGuard } from '@nestjs/passport'; +import { AuthService } from './auth.service'; +import { Request, Response } from 'express'; interface IOAuthUser { - //interface 설정 user: { name: string; email: string; @@ -34,10 +32,7 @@ interface IOAuthUser { @ApiTags('Auth') @ApiOkResponse({ type: AuthEntity }) export class AuthController { - constructor( - private readonly authService: AuthService, - private readonly usersService: UsersService - ) {} + constructor(private readonly authService: AuthService) {} //-----------------------로그인-----------------------------// @ApiOperation({ summary: '로그인' }) @ApiResponse({ status: 200, description: '로그인에 성공하셨습니다.' }) @@ -46,8 +41,7 @@ export class AuthController { @Post('login') async login( @Body() { email, password }: LoginDto, - @Req() req: Request, - @Res({ passthrough: true }) res: Response // Response 객체 주입 + @Res({ passthrough: true }) res: Response ): Promise { const { accessToken, refreshToken, userId } = await this.authService.login({ email, @@ -60,6 +54,7 @@ export class AuthController { } //-----------------------토큰 재발급-----------------------------// + @Post('refresh') @ApiOperation({ summary: '리프레시 토큰을 사용하여 엑세스 토큰 재발급' }) @ApiResponse({ status: 200, @@ -69,7 +64,6 @@ export class AuthController { status: 401, description: '리프레시 토큰이 유효하지 않습니다.', }) - @Post('refresh') async refreshAccessToken( @Headers('refreshToken') refreshToken: string, // 요청 헤더에서 refresh-token 값을 추출 @Res({ passthrough: true }) res: Response diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index 1ba4011..d1bbba7 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -6,7 +6,6 @@ import { import { PrismaService } from './../prisma/prisma.service'; import { JwtService } from '@nestjs/jwt'; import * as bcrypt from 'bcrypt'; -//import { IAuthServiceLogin } from './interface/auth-service.interface'; import { UsersService } from 'src/users/users.service'; @Injectable() @@ -25,7 +24,7 @@ export class AuthService { }> { // 1. 이메일이 일치하는 유저를 DB에서 찾기 const user = await this.usersService.findByEmail({ email }); - console.log(user); + // 2. 일치하는 유저가 없으면 에러 if (!user) throw new NotFoundException('이메일이 없습니다.'); @@ -41,12 +40,10 @@ export class AuthService { // 4. 리프레시 토큰 생성 const refreshToken = this.setRefreshToken({ user }); - - // 5. 액세스 토큰 및 리프레시 토큰을 반환 const accessToken = this.getAccessToken({ user }); + // 5. 액세스 토큰 및 리프레시 토큰을 반환 res.header('accessToken', accessToken); - res.header('refreshToken', refreshToken); // 6. DB에 리프레시 토큰을 저장한다. @@ -75,7 +72,6 @@ export class AuthService { { sub: user.userId }, { secret: process.env.JWT_REFRESH_KEY, expiresIn: '2w' } ); - return refreshToken; } @@ -92,14 +88,12 @@ export class AuthService { user: { userId }, // 사용자 ID를 전달 // res: null, }); - return newAccessToken; } async OAuthLogin({ req, res }): Promise<{ accessToken: string; refreshToken: string; - // userId: number; }> { // 1. 회원조회 let user = await this.usersService.findByEmail({ email: req.user.email }); // user를 찾아서 @@ -114,11 +108,8 @@ export class AuthService { intro: req.user.intro, profileImg: req.user.profileImg, }; - // console.log('소셜 로그인 회원가입 : ', createUser); // createUser 정보를 콘솔에 출력 user = await this.usersService.create(createUser); - console.log('소셜로그인 회원가입 정보', createUser); } - // 3. 회원가입이 되어 있다면? 로그인(AT, RT를 생성해서 브라우저에 전송)한다 const accessToken = this.getAccessToken({ user }); // res를 전달 const refreshToken = this.setRefreshToken({ user }); // res를 전달 @@ -132,7 +123,6 @@ export class AuthService { console.log('로컬 엑세스 토큰', accessToken); console.log('로컬 리프레시 토큰', refreshToken); - //console.log(user.userId); // 리다이렉션 res.redirect( `http://localhost:5173?accessToken=${encodeURIComponent( @@ -141,9 +131,6 @@ export class AuthService { refreshToken )}&userId=${encodeURIComponent(user.userId)}` ); - //&userId=${encodeURIComponent(user.userId)} - // return res.redirect('http://localhost:5500'); - // return { accessToken, refreshToken }; return { accessToken, refreshToken }; } } diff --git a/src/auth/dto/login.dto.ts b/src/auth/dto/login.dto.ts index d67947f..b9f7e08 100644 --- a/src/auth/dto/login.dto.ts +++ b/src/auth/dto/login.dto.ts @@ -7,7 +7,7 @@ export class LoginDto { @IsNotEmpty() @ApiProperty({ description: 'Email', - example: "abcd1234@naver.com", + example: 'abcd1234@naver.com', }) email: string; diff --git a/src/auth/guards/jwt-auth.guard.ts b/src/auth/guards/jwt-auth.guard.ts index ed7c445..926beb8 100644 --- a/src/auth/guards/jwt-auth.guard.ts +++ b/src/auth/guards/jwt-auth.guard.ts @@ -3,6 +3,5 @@ import { Injectable } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; @Injectable() -// export class JwtAuthGuard extends AuthGuard('jwt') {} export class JwtAccessAuthGuard extends AuthGuard('access') {} export class JwtRefreshAuthGuard extends AuthGuard('refresh') {} diff --git a/src/auth/interface/auth-service.interface.ts b/src/auth/interface/auth-service.interface.ts index b4e977d..735f9a9 100644 --- a/src/auth/interface/auth-service.interface.ts +++ b/src/auth/interface/auth-service.interface.ts @@ -1,9 +1,3 @@ -// export interface IUsersServiceCreate { -// email: string; -// name: string; -// password: string; -// } - import { User } from '@prisma/client'; export interface IUsersServiceFindByEmail { @@ -17,10 +11,10 @@ export interface IAuthServiceLogin { export interface IAuthServiceGetAccessToken { user: User; - res: any; // res 매개 변수 추가 + res: any; } export interface IAuthServiceGetRefereshToken { user: User; - res: any; // res 매개 변수 추가 + res: any; } diff --git a/src/auth/strategies/jwt-refresh.strategy.ts b/src/auth/strategies/jwt-refresh.strategy.ts deleted file mode 100644 index 760233e..0000000 --- a/src/auth/strategies/jwt-refresh.strategy.ts +++ /dev/null @@ -1,46 +0,0 @@ -// import { PassportStrategy } from '@nestjs/passport'; -// import { Strategy, ExtractJwt } from 'passport-jwt'; -// import { Injectable, UnauthorizedException, Request } from '@nestjs/common'; -// import { JwtService } from '@nestjs/jwt'; -// import { UsersService } from '../../users/users.service'; - -// @Injectable() -// export class JwtRefreshStrategy extends PassportStrategy(Strategy, 'refresh') { -// constructor( -// private jwtService: JwtService, -// private usersService: UsersService -// ) { -// super({ -// jwtFromRequest: ExtractJwt.fromHeader('refreshtoken'), // 헤더에서 리프레시 토큰을 추출 -// secretOrKey: process.env.JWT_REFRESH_KEY, -// }); -// } - -// async validate(payload: any, @Request() req: any) { -// // 'refreshtoken' 헤더에서 refreshToken을 추출 -// const refreshToken = req.headers.refreshtoken as string; -// console.log('리프레시토큰 확인', refreshToken); -// if (!refreshToken) { -// throw new UnauthorizedException('Refresh token not provided'); -// } - -// try { -// // 리프레시 토큰의 유효성을 검증 -// const decodedToken = this.jwtService.verify(refreshToken, { -// secret: process.env.JWT_REFRESH_KEY, -// }); - -// // 유효한 사용자를 찾을 때 사용자 서비스를 활용 -// const user = await this.usersService.findById(decodedToken.sub); - -// if (!user) { -// throw new UnauthorizedException('Invalid token'); -// } - -// // 사용자 정보를 반환 -// return { email: user.email, id: user.userId }; -// } catch (error) { -// throw new UnauthorizedException('Invalid token'); -// } -// } -// } diff --git a/src/auth/strategies/jwt-social-google.strategy.ts b/src/auth/strategies/jwt-social-google.strategy.ts index f5b1b0e..96a21b4 100644 --- a/src/auth/strategies/jwt-social-google.strategy.ts +++ b/src/auth/strategies/jwt-social-google.strategy.ts @@ -4,10 +4,11 @@ import * as bcrypt from 'bcrypt'; import { PrismaService } from '../../prisma/prisma.service'; // 프리즈마 서비스 파일 경로를 사용하는 경로로 수정해야 합니다. import { Inject } from '@nestjs/common'; +// @Injectable() export class JwtGoogleStrategy extends PassportStrategy(Strategy, 'google') { constructor( private readonly prisma: PrismaService, - @Inject(PrismaService) private readonly prismaService: PrismaService // 추가 + @Inject(PrismaService) private readonly prismaService: PrismaService ) { super({ clientID: process.env.GOOGLE_CLIENT_ID, @@ -18,16 +19,15 @@ export class JwtGoogleStrategy extends PassportStrategy(Strategy, 'google') { } async validate(accessToken: string, refreshToken: string, profile: any) { - console.log('google 엑세스토큰:', accessToken); - console.log('google 리프레시 토큰:', refreshToken); - console.log('google 프로필:', profile); + // console.log('google 엑세스토큰:', accessToken); + // console.log('google 리프레시 토큰:', refreshToken); + // console.log('google 프로필:', profile); // 비밀번호 암호화 const hashedPassword = await bcrypt.hash(profile.id.toString(), 10); // 고유한 익명 nickname 생성 const nickname = await this.generateUniqueAnonymousName(); - //console.log('닉네임 확인', nickname); return { name: profile.displayName, email: profile.emails[0].value, @@ -53,8 +53,6 @@ export class JwtGoogleStrategy extends PassportStrategy(Strategy, 'google') { const anonymousName = `${anonymousPrefix}${randomString}`; - //return anonymousName; // 밑의 로직이 작동안하면 임시적으로 사용 - // 프리즈마를 사용하여 중복 확인 const existingUser = await this.prisma.userDetail.findUnique({ where: { nickname: anonymousName }, diff --git a/src/auth/strategies/jwt-social-kakao.strategy.ts b/src/auth/strategies/jwt-social-kakao.strategy.ts index 238d299..67fb911 100644 --- a/src/auth/strategies/jwt-social-kakao.strategy.ts +++ b/src/auth/strategies/jwt-social-kakao.strategy.ts @@ -18,10 +18,10 @@ export class JwtKakaoStrategy extends PassportStrategy(Strategy, 'kakao') { } async validate(accessToken: string, refreshToken: string, profile: any) { - console.log('카카오에서 주는 accessToken:' + accessToken); - console.log('카카오에서 주는 refreshToken:' + refreshToken); - console.log('카카오 프로필', profile); - console.log(profile._json.kakao_account.email); + // console.log('카카오에서 주는 accessToken:' + accessToken); + // console.log('카카오에서 주는 refreshToken:' + refreshToken); + // console.log('카카오 프로필', profile); + // console.log(profile._json.kakao_account.email); // 비밀번호 암호화 const hashedPassword = await bcrypt.hash(profile.id.toString(), 10); diff --git a/src/auth/strategies/jwt-social-naver.strategy.ts b/src/auth/strategies/jwt-social-naver.strategy.ts index 8818ca5..7ea37f6 100644 --- a/src/auth/strategies/jwt-social-naver.strategy.ts +++ b/src/auth/strategies/jwt-social-naver.strategy.ts @@ -18,10 +18,10 @@ export class JwtNaverStrategy extends PassportStrategy(Strategy, 'naver') { } async validate(accessToken: string, refreshToken: string, profile: Profile) { - console.log('네이버에서 주는 accessToken:' + accessToken); - console.log('네이버에서 주는 refreshToken:' + refreshToken); - console.log(profile); - console.log(profile.email); + // console.log('네이버에서 주는 accessToken:' + accessToken); + // console.log('네이버에서 주는 refreshToken:' + refreshToken); + // console.log(profile); + // console.log(profile.email); // 비밀번호 암호화 const hashedPassword = await bcrypt.hash(profile.id.toString(), 10); diff --git a/src/auth/strategies/jwt.strategy.ts b/src/auth/strategies/jwt.strategy.ts deleted file mode 100644 index 03d5f19..0000000 --- a/src/auth/strategies/jwt.strategy.ts +++ /dev/null @@ -1,26 +0,0 @@ -// //src/auth/jwt.strategy.ts -// import { Injectable, UnauthorizedException } from '@nestjs/common'; -// import { PassportStrategy } from '@nestjs/passport'; -// import { ExtractJwt, Strategy } from 'passport-jwt'; -// import { jwtSecret } from '../auth.module'; -// import { UsersService } from 'src/users/users.service'; - -// @Injectable() -// export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') { -// constructor(private usersService: UsersService) { -// super({ -// jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), -// secretOrKey: jwtSecret, -// }); -// } - -// async validate(payload: { userId: number }) { -// const user = await this.usersService.findOne(payload.userId); - -// if (!user) { -// throw new UnauthorizedException(); -// } - -// return user; -// } -// } diff --git a/src/data/data.controller.ts b/src/data/data.controller.ts index 9239000..3df0fe4 100644 --- a/src/data/data.controller.ts +++ b/src/data/data.controller.ts @@ -1,8 +1,7 @@ import { Controller, Get, Query } from '@nestjs/common'; import { ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger'; import { DataService } from './data.service'; -import { toss } from './interface/verify'; -import { city } from './interface/city'; +import { city, filter } from './interface/city'; @Controller('data') @ApiTags('Data') @@ -15,11 +14,9 @@ export class DataController { }) @ApiQuery({ name: 'lang', type: String, required: true }) async cityData(@Query() query: any) { - console.log(query.lang); if (query.lang !== 'ko' && query.lang !== 'jp' && query.lang !== 'en') { return { message: '[ko, en, jp] 중 하나를 입력하세요' }; } - const langByCity = city.find((item) => { return item.lang == query.lang; }); @@ -27,16 +24,20 @@ export class DataController { } @Get('gu_name') - @ApiOperation({ summary: 'API 호출시 도, 시를 query 값으로 받아서 하위 구, 군 목록 반환' }) + @ApiOperation({ + summary: 'API 호출시 도, 시를 query 값으로 받아서 하위 구, 군 목록 반환', + }) @ApiQuery({ name: 'doName', type: String, required: true }) async guNameData(@Query() query) { return await this.dataService.guNameData(query); } @Get('toss') - @ApiOperation({ summary: 'API 호출시 카테고리 목록, 아무나, 위치 인증 여부를 반환' }) + @ApiOperation({ + summary: 'API 호출시 카테고리 목록, 아무나, 위치 인증 여부를 반환', + }) categoryData() { - const data = toss; + const data = filter; return data; } } diff --git a/src/data/data.service.ts b/src/data/data.service.ts index 336860a..a48e747 100644 --- a/src/data/data.service.ts +++ b/src/data/data.service.ts @@ -1,17 +1,10 @@ import { Injectable } from '@nestjs/common'; -import { Verify } from 'src/data/interface/verify'; import { PrismaService } from 'src/prisma/prisma.service'; @Injectable() export class DataService { constructor(private prisma: PrismaService) {} - cityData() { - return this.prisma.region.findMany({ - select: { doName: true }, - }); - } - async guNameData(query) { const data = await this.prisma.region.findMany({ where: { doName: query.doName }, diff --git a/src/data/interface/city.ts b/src/data/interface/city.ts index a5ebb40..cc4cdd2 100644 --- a/src/data/interface/city.ts +++ b/src/data/interface/city.ts @@ -66,3 +66,8 @@ export const city = [ ], }, ]; + +export const filter = { + category: ['☕맛집/커피', '🏃‍♂️운동/건강', '🐾애완동물', '📕공부/교육'], + verify: ['🙋‍♀️아무나', '🏡동네만'], +}; diff --git a/src/data/interface/verify.ts b/src/data/interface/verify.ts deleted file mode 100644 index df8c1fd..0000000 --- a/src/data/interface/verify.ts +++ /dev/null @@ -1,10 +0,0 @@ -export interface Verify { - verify: string; -} - -const verifies: Verify[] = [{ verify: '🙋‍♀️아무나' }, { verify: '🏡동네만' }]; - -export const toss = { - category: ['☕맛집/커피', '🏃‍♂️운동/건강', '🐾애완동물', '📕공부/교육'], - verify: ['🙋‍♀️아무나', '🏡동네만'], -}; diff --git a/src/events/dto/create-event.dto.ts b/src/events/dto/create-event.dto.ts index 761190d..f9e867f 100644 --- a/src/events/dto/create-event.dto.ts +++ b/src/events/dto/create-event.dto.ts @@ -10,67 +10,67 @@ import { } from 'class-validator'; export class CreateEventDto { + @IsString() + @IsNotEmpty() @ApiProperty({ example: '같이 산책하실분', }) - @IsNotEmpty() - @IsString() @MaxLength(50) eventName: string; + @IsInt() + @IsNotEmpty() + @Min(1) @ApiProperty({ example: 10, }) - @IsNotEmpty() - @IsInt() - @Min(1) maxSize: number; - @ApiProperty() @IsNotEmpty() + @ApiProperty() eventDate: Date; - @ApiProperty() @IsNotEmpty() + @ApiProperty() signupStartDate: Date; - @ApiProperty() @IsNotEmpty() + @ApiProperty() signupEndDate: Date; + @IsString() + @IsNotEmpty() @ApiProperty({ example: '서울특별시', }) - @IsString() - @IsNotEmpty() eventLocation: string; + @IsString() + @IsNotEmpty() + @MaxLength(200) @ApiProperty({ example: '재밌게 놀아요', }) - @IsNotEmpty() - @IsString() - @MaxLength(200) content: string; + @IsString() + @IsNotEmpty() @ApiProperty({ example: '산책', }) - @IsNotEmpty() - @IsString() category: string; - @ApiProperty({ required: false, default: false }) @IsBoolean() + @ApiProperty({ required: false, default: false }) isDeleted: boolean = false; - @ApiProperty({ required: false, default: '🙋‍♀️아무나' }) - @IsOptional() @IsString() + @IsOptional() + @ApiProperty({ required: false, default: '🙋‍♀️아무나' }) isVerified?: string; - @ApiProperty({ required: false, default: null }) - @IsOptional() @IsString() - eventImg: string; + @IsOptional() + @ApiProperty({ required: false, default: null }) + eventImg?: string; } diff --git a/src/events/dto/update-event.dto.ts b/src/events/dto/update-event.dto.ts index c5df352..5ba8734 100644 --- a/src/events/dto/update-event.dto.ts +++ b/src/events/dto/update-event.dto.ts @@ -9,52 +9,52 @@ import { } from 'class-validator'; export class UpdateEventDto { - @ApiProperty() - @IsNotEmpty() @IsString() + @IsNotEmpty() + @ApiProperty() eventName: string; - @ApiProperty() - @IsNotEmpty() @IsInt() + @IsNotEmpty() + @ApiProperty() @Min(1) maxSize: number; - @ApiProperty() @IsNotEmpty() + @ApiProperty() eventDate: Date; - @ApiProperty() @IsNotEmpty() + @ApiProperty() signupStartDate: Date; - @ApiProperty() @IsNotEmpty() + @ApiProperty() signupEndDate: Date; - @ApiProperty({ example: '경기도' }) - @IsNotEmpty() @IsString() + @IsNotEmpty() + @ApiProperty({ example: '경기도' }) eventLocation: string; - @ApiProperty() - @IsNotEmpty() @IsString() + @IsNotEmpty() @MaxLength(200) + @ApiProperty() content: string; - @ApiProperty({ example: '산책' }) - @IsNotEmpty() @IsString() + @IsNotEmpty() + @ApiProperty({ example: '산책' }) category: string; - @ApiProperty({ default: '🙋‍♀️아무나' }) - @IsNotEmpty() @IsString() + @IsNotEmpty() + @ApiProperty({ default: '🙋‍♀️아무나' }) isVerified: string; - @ApiProperty() - @IsOptional() @IsString() + @IsOptional() + @ApiProperty() eventImg: string; } diff --git a/src/events/events.controller.ts b/src/events/events.controller.ts index 59d7ad1..23e5de8 100644 --- a/src/events/events.controller.ts +++ b/src/events/events.controller.ts @@ -14,9 +14,6 @@ import { UseInterceptors, UnauthorizedException, } from '@nestjs/common'; -import { EventsService } from './events.service'; -import { CreateEventDto } from './dto/create-event.dto'; -import { UpdateEventDto } from './dto/update-event.dto'; import { ApiBearerAuth, ApiCreatedResponse, @@ -26,13 +23,14 @@ import { ApiConsumes, ApiBody, } from '@nestjs/swagger'; +import { FileInterceptor } from '@nestjs/platform-express'; +import { CreateEventDto } from './dto/create-event.dto'; +import { UpdateEventDto } from './dto/update-event.dto'; import { EventEntity } from './entities/event.entity'; import { JwtAccessAuthGuard } from 'src/auth/guards/jwt-auth.guard'; -import { FileInterceptor } from '@nestjs/platform-express'; import { AwsS3Service } from 'src/aws/aws.s3'; -import { RequestWithUser } from './interface/event-controller.interface'; - -// request에 user 객체를 추가하기 위한 인터페이스 +import { EventsService } from './events.service'; +import { RequestWithUser } from '../users/interfaces/users.interface'; @Controller('events') @ApiTags('Events') @@ -42,58 +40,21 @@ export class EventsController { private readonly awsS3Service: AwsS3Service ) {} + // 1. 호스트로 이벤트 생성 @Post() - @UseGuards(JwtAccessAuthGuard) // passport를 사용하여 인증 확인 - @ApiBearerAuth() // Swagger 문서에 Bearer 토큰 인증 추가 @ApiOperation({ summary: '호스트로 Event 생성' }) - @UseInterceptors(FileInterceptor('file')) @ApiCreatedResponse({ type: EventEntity }) + @UseGuards(JwtAccessAuthGuard) + @ApiBearerAuth() async create( @Req() req: RequestWithUser, @Body() createEventDto: CreateEventDto ) { const { userId } = req.user; - return this.eventsService.create(userId, createEventDto); } - @Put(':eventId/upload') - @UseGuards(JwtAccessAuthGuard) - @ApiBearerAuth() - @ApiOperation({ summary: 'Event 이미지 업데이트' }) - @ApiConsumes('multipart/form-data') - @UseInterceptors(FileInterceptor('file')) - @ApiBody({ - description: 'event image', - type: 'multipart/form-data', - required: true, - schema: { - type: 'object', - properties: { - file: { - type: 'string', - format: 'binary', - }, - }, - }, - }) - async uploadFile( - @UploadedFile() file, - @Param('eventId', ParseIntPipe) eventId: number - ) { - // 이미지 파일 aws s3 서버에 저장 - const updatedImg = await this.awsS3Service.uploadEventFile(file) as { - Location: string; - }; - - // 이미지 파일 DB에 URL 형태로 저장 - await this.eventsService.updateImg(eventId, updatedImg.Location); - return { - message: '이미지가 업로드되었습니다', - ImgURL: updatedImg, - }; - } - + // 2. 이벤트 전체 조회 @Get() @ApiOperation({ summary: 'Event 전체 조회' }) @ApiOkResponse({ type: EventEntity, isArray: true }) @@ -103,22 +64,23 @@ export class EventsController { // 전체 조회 시 이벤트 호스트와 참가자 수 반환 const event = events.map((item) => { const { GuestEvents, HostEvents, ...rest } = item; - const hostUser = item.HostEvents[0].User.UserDetail; + const hostUser = HostEvents[0].User.UserDetail; return { event: rest, - guestList: item.GuestEvents.length, + guestList: GuestEvents.length, hostUser: hostUser, }; }); return event; } + // 3. 이벤트 상세 조회 @Get(':eventId') - @UseGuards(JwtAccessAuthGuard) - @ApiBearerAuth() @ApiOperation({ summary: 'Event 상세 조회' }) @ApiOkResponse({ type: EventEntity }) + @UseGuards(JwtAccessAuthGuard) + @ApiBearerAuth() async findOne( @Req() req: RequestWithUser, @Param('eventId', ParseIntPipe) eventId: number @@ -130,14 +92,14 @@ export class EventsController { const confirmJoin = isJoin ? true : false; const event = await this.eventsService.findOne(eventId); + const { GuestEvents, HostEvents, ...rest } = event; + // 조회수 로그 생성 await this.eventsService.createViewLog(eventId); - const { GuestEvents, HostEvents, ...rest } = event; - return { event: rest, - guestList: event.GuestEvents.length, + guestList: GuestEvents.length, hostUser: HostEvents[0].User.UserDetail, guestUser: GuestEvents.map((item) => { return item.User.UserDetail; @@ -146,11 +108,14 @@ export class EventsController { }; } + // 4. 이벤트 참가 @Put(':eventId/join') - @UseGuards(JwtAccessAuthGuard) - @ApiBearerAuth() @ApiOperation({ summary: 'Guest로서 Event 참가신청' }) - @ApiCreatedResponse({ description: `API를 홀수번 호출하면 참석신청 짝수번 신청하면 참석취소` }) + @ApiCreatedResponse({ + description: `API를 홀수번 호출하면 참석신청 짝수번 신청하면 참석취소`, + }) + @UseGuards(JwtAccessAuthGuard) + @ApiBearerAuth() async join( @Param('eventId', ParseIntPipe) eventId: number, @Req() req: RequestWithUser @@ -161,9 +126,11 @@ export class EventsController { const isJoin = await this.eventsService.isJoin(eventId, userId); if (!isJoin) { if (event.maxSize <= event.GuestEvents.length) { - return { message: `참가인원은 최대${event.maxSize}명 입니다 빠꾸입니다` }; + return { + message: `참가인원은 최대${event.maxSize}명 입니다`, + }; } - this.eventsService.join(+eventId, userId); + this.eventsService.join(eventId, userId); this.eventsService.createRsvpLog(eventId, userId, 'applied'); // 참가 신청 로그 생성 return `${eventId}번 모임 참석 신청!`; } @@ -174,12 +141,12 @@ export class EventsController { } } - // 이벤트 수정 + // 5. 이벤트 수정 @Patch(':eventId') - @UseGuards(JwtAccessAuthGuard) // passport를 사용하여 인증 확인 - @ApiBearerAuth() // Swagger 문서에 Bearer 토큰 인증 추가 @ApiOperation({ summary: 'Host로서 Event 수정' }) @ApiOkResponse({ type: EventEntity }) + @UseGuards(JwtAccessAuthGuard) + @ApiBearerAuth() async update( @Param('eventId', ParseIntPipe) eventId: number, @Body() updateEventDto: UpdateEventDto, @@ -187,20 +154,63 @@ export class EventsController { ) { const { userId } = req.user; const event = await this.eventsService.findOne(eventId); + + // 작성자가 호스트인지 확인 후 같지 않은 경우 수정 불가 if (userId !== event.HostEvents[0].HostId) { - throw new UnauthorizedException(`작성자만 수정할 수 있습니다`) + throw new UnauthorizedException(`작성자만 수정할 수 있습니다`); } this.eventsService.update(eventId, updateEventDto); - return {message: '수정이 완료되었습니다'} + return { message: '수정이 완료되었습니다' }; } - // 이벤트 삭제 + // 5-1. 이벤트 이미지 업로드 + @Put(':eventId/upload') + @ApiOperation({ summary: 'Event 이미지 업데이트' }) + @ApiConsumes('multipart/form-data') + @UseInterceptors(FileInterceptor('file')) + @UseGuards(JwtAccessAuthGuard) + @ApiBearerAuth() + @ApiBody({ + description: 'event image', + type: 'multipart/form-data', + required: true, + schema: { + type: 'object', + properties: { + file: { + type: 'string', + format: 'binary', + }, + }, + }, + }) + async uploadFile( + @UploadedFile() file, + @Param('eventId', ParseIntPipe) eventId: number + ) { + // 이미지 파일 aws s3 서버에 저장 + const updatedImg = (await this.awsS3Service.uploadEventFile(file)) as { + Location: string; + }; + + // 이미지 파일 DB에 URL 형태로 저장 + await this.eventsService.updateImg(eventId, updatedImg.Location); + return { + message: '이미지가 업로드되었습니다', + ImgURL: updatedImg, + }; + } + + // 6. 이벤트 삭제 @Delete(':eventId') - @UseGuards(JwtAccessAuthGuard) // passport를 사용하여 인증 확인 - @ApiBearerAuth() // Swagger 문서에 Bearer 토큰 인증 추가 @ApiOperation({ summary: 'Host로서 Event 삭제' }) - @ApiOkResponse({ description: 'soft Delete로 isDelete 필드를 true 바꿔 체킹만 해둔다. 조회는 되지 않음' }) + @ApiOkResponse({ + description: + 'soft Delete로 isDelete 필드를 true 바꿔 체킹만 해둔다. 조회는 되지 않음', + }) + @UseGuards(JwtAccessAuthGuard) + @ApiBearerAuth() async remove( @Param('eventId', ParseIntPipe) eventId: number, @Req() req: RequestWithUser @@ -209,15 +219,14 @@ export class EventsController { const event = await this.eventsService.findOne(eventId); if (userId !== event.HostEvents[0].HostId) throw new UnauthorizedException(`작성자만 삭제할 수 있습니다`); - return this.eventsService.remove(eventId); } - // 관심있는 이벤트 북마크 추가 + // 7-1. 관심있는 이벤트 북마크 추가 @Post(':eventId/bookmark') + @ApiOperation({ summary: 'Event 북마크 추가' }) @UseGuards(JwtAccessAuthGuard) @ApiBearerAuth() - @ApiOperation({ summary: 'Event 북마크 추가' }) async addBookmark( @Param('eventId', ParseIntPipe) eventId: number, @Req() req: RequestWithUser @@ -226,11 +235,11 @@ export class EventsController { return this.eventsService.addBookmark(eventId, userId, 'bookmarked'); } - // 관심있는 이벤트 북마크 제거 + // 7-2. 관심있는 이벤트 북마크 제거 @Delete(':eventId/bookmark') + @ApiOperation({ summary: 'Event 북마크 제거' }) @UseGuards(JwtAccessAuthGuard) @ApiBearerAuth() - @ApiOperation({ summary: 'Event 북마크 제거' }) async removeBookmark( @Param('eventId', ParseIntPipe) eventId: number, @Req() req: RequestWithUser diff --git a/src/events/events.service.ts b/src/events/events.service.ts index 4c5b3f8..0c083a7 100644 --- a/src/events/events.service.ts +++ b/src/events/events.service.ts @@ -35,7 +35,7 @@ export class EventsService { return event; } - // 이벤트 전체 조회 + // 2. 이벤트 전체 조회 findAll() { return this.prisma.event.findMany({ where: { @@ -64,7 +64,7 @@ export class EventsService { }); } - // 이벤트 상세 조회 + // 3. 이벤트 상세 조회 async findOne(eventId: number) { const event = await this.prisma.event.findUnique({ where: { eventId, isDeleted: false }, @@ -103,7 +103,7 @@ export class EventsService { return event; } - // 이벤트 조회 로그 + // 3-1. 이벤트 조회수 로거 async createViewLog(eventId: number) { await this.prisma.viewlog.create({ data: { @@ -113,7 +113,7 @@ export class EventsService { }); } - // 이벤트 참가여부 확인 + // 4-1. 이벤트 참가여부 확인 async isJoin(eventId: number, userId: number) { const isJoin = await this.prisma.guestEvent.findFirst({ where: { @@ -124,7 +124,7 @@ export class EventsService { return isJoin; } - // 이벤트 참가 신청 + // 4. 이벤트 참가 신청 async join(eventId: number, userId: number) { await this.prisma.guestEvent.create({ data: { @@ -134,14 +134,14 @@ export class EventsService { }); } - // 이벤트 참가 취소 + // 4-2. 이벤트 참가 취소 async cancelJoin(guestEventId: number) { await this.prisma.guestEvent.delete({ where: { guestEventId }, }); } - // 이벤트 신청/취소 로그 + // 4-3. 이벤트 참가 신청/취소 로그 async createRsvpLog(eventId: number, userId: number, status: string) { await this.prisma.rsvpLog.create({ data: { @@ -153,15 +153,15 @@ export class EventsService { }); } - // 이벤트 수정 - update(eventId: number, updateEventDto: UpdateEventDto) { - this.prisma.event.update({ + // 5. 이벤트 수정 + async update(eventId: number, updateEventDto: UpdateEventDto) { + await this.prisma.event.update({ where: { eventId }, data: updateEventDto, }); } - // 이벤트 이미지 수정 + // 5-1. 이벤트 이미지 수정 async updateImg(eventId: number, updatedImg: string) { const ImgUrl = await this.prisma.event.update({ where: { eventId }, @@ -170,9 +170,9 @@ export class EventsService { return ImgUrl; } - // 이벤트 삭제 - remove(eventId: number) { - return this.prisma.event.update({ + // 6. 이벤트 삭제 + async remove(eventId: number) { + return await this.prisma.event.update({ where: { eventId }, data: { isDeleted: true, @@ -180,7 +180,7 @@ export class EventsService { }); } - // 관심있는 북마크 추가 + // 7-1. 관심있는 북마크 추가 async addBookmark(eventId: number, userId: number, status: string) { const lastEventInTable = await this.prisma.eventBookmark.findFirst({ where: { @@ -191,7 +191,6 @@ export class EventsService { eventBookmarkId: 'desc', }, }); - console.log('addBookmark:', lastEventInTable); // 이벤트의 북마크가 존재하지 않거나 가장 최신의 북마크 status가 unbookmarked이면 새로운 로그를 생성한다. if (!lastEventInTable || lastEventInTable.status === 'unbookmarked') { @@ -212,7 +211,7 @@ export class EventsService { } } - // 관심있는 이벤트 북마크 제거 + // 7-2. 관심있는 이벤트 북마크 제거 async removeBookmark(eventId: number, userId: number, status: string) { const lastEventInTable = await this.prisma.eventBookmark.findFirst({ where: { diff --git a/src/mails/dto/verify-code.dto.ts b/src/mails/dto/verify-code.dto.ts index 0e5e571..caad78b 100644 --- a/src/mails/dto/verify-code.dto.ts +++ b/src/mails/dto/verify-code.dto.ts @@ -2,7 +2,6 @@ import { IsInt } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; export class VerifyCodeDto { - @ApiProperty() @IsInt() @ApiProperty({ description: 'code', diff --git a/src/main.ts b/src/main.ts index edfbb0d..b9b8cd1 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,10 +1,8 @@ // src/main.ts - import { NestFactory, Reflector } from '@nestjs/core'; import { AppModule } from './app.module'; import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; import { ClassSerializerInterceptor, ValidationPipe } from '@nestjs/common'; -// import { ValidationPipe } from '@nestjs/common'; async function bootstrap() { const app = await NestFactory.create(AppModule); @@ -12,22 +10,26 @@ async function bootstrap() { // 유효성 검사를 위한 ValidationPipe 설정 app.useGlobalPipes(new ValidationPipe()); - app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector))); // 인터셉터를 사용하여 응답 본문에서 비밀번호를 자동으로 제거 + app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector))); const config = new DocumentBuilder() .setTitle('LocalMingle API') .setDescription('The LocalMingle API description') .setVersion('0.1') .addBearerAuth() - .addBearerAuth() .build(); const document = SwaggerModule.createDocument(app, config); SwaggerModule.setup('api', app, document); - //cors 정책 + + // CORS 정책 app.enableCors({ - origin: ['http://localhost:5173','https://d2r603zvpf912o.cloudfront.net','https://totobon.store'], + origin: [ + 'http://localhost:5173', + 'https://d2r603zvpf912o.cloudfront.net', + 'https://totobon.store', + ], methods: 'GET,HEAD,PUT,PATCH,POST,DELETE', credentials: true, // 쿠키를 사용하려면 true로 설정 exposedHeaders: ['accessToken', 'refreshToken'], diff --git a/src/searches/searches.controller.ts b/src/searches/searches.controller.ts index ebf4d55..3dc1484 100644 --- a/src/searches/searches.controller.ts +++ b/src/searches/searches.controller.ts @@ -11,7 +11,7 @@ export class SearchesController { @Get() @ApiOperation({ summary: '이벤트네임 or 콘텐츠 검색' }) async searchByNameOrContent( - @Query('query') query: string // @Query 데코레이터 추가 + @Query('query') query: string ): Promise<{ eventName: string; content: string }[]> { return this.searchesService.searchByNameOrContent(query); } @@ -19,21 +19,19 @@ export class SearchesController { @Get('byLocation') @ApiOperation({ summary: '이벤트 장소별 검색' }) @ApiQuery({ name: 'doName', type: String, required: true }) - searchByLocation(@Query() query:any) { + searchByLocation(@Query() query) { return this.searchesService.searchByLocation(query); } @Get('byCategory') @ApiOperation({ summary: '카테고리별 검색' }) searchByCategory(@Query('query') query: string) { - console.log(query) return this.searchesService.searchByCategory(query); } @Get('byVerify') @ApiOperation({ summary: '동네만 or 아무나 검색' }) searchByVerify(@Query('query') query: string) { - console.log(query) return this.searchesService.searchByVerify(query); } } diff --git a/src/users/dto/create-user.dto.ts b/src/users/dto/create-user.dto.ts index cf5fec7..4ba5586 100644 --- a/src/users/dto/create-user.dto.ts +++ b/src/users/dto/create-user.dto.ts @@ -35,7 +35,7 @@ export class CreateUserDto { @IsString() @IsNotEmpty() @ApiProperty({ - description: 'password', + description: 'password confirm', example: 'abc123456789!', }) confirmPassword: string; @@ -44,23 +44,21 @@ export class CreateUserDto { @IsNotEmpty() @MinLength(2) @MaxLength(8) - //영어 또는 한글이 포함 - @Matches(/^(?=.*[A-Za-z가-힣]).*[A-Za-z가-힣0-9]*$/) + @Matches(/^(?=.*[A-Za-z가-힣]).*[A-Za-z가-힣0-9]*$/) //영어 또는 한글이 포함 @ApiProperty({ description: 'nickname', example: '닉네임', }) nickname: string; - @IsOptional() @IsString() + @IsOptional() @ApiProperty({ description: 'intro', example: '안녕하세요', }) intro?: string; - /* @IsOptional() */ @IsString() @IsOptional() @ApiProperty({ @@ -73,7 +71,7 @@ export class CreateUserDto { @IsOptional() @ApiProperty({ description: 'userLocation', - example: '서울시 강남구 ', + example: '서울시 강남구', }) userLocation?: string; diff --git a/src/users/dto/update-user.dto.ts b/src/users/dto/update-user.dto.ts index b1d60ec..f0c0374 100644 --- a/src/users/dto/update-user.dto.ts +++ b/src/users/dto/update-user.dto.ts @@ -7,20 +7,21 @@ import { MaxLength, MinLength, IsOptional, + IsBoolean, } from 'class-validator'; export class UpdateUserDto { @IsString() + @IsOptional() @IsNotEmpty() @MinLength(2) @MaxLength(8) @Matches(/^(?=.*[A-Za-z가-힣]).*[A-Za-z가-힣0-9]*$/) - @IsOptional() @ApiProperty({ description: 'nickname', example: '닉네임', }) - nickname: string; + nickname?: string; @IsString() @IsOptional() @@ -28,7 +29,7 @@ export class UpdateUserDto { description: 'intro', example: '안녕하세요', }) - intro: string; + intro?: string; @IsString() @IsOptional() @@ -36,8 +37,9 @@ export class UpdateUserDto { description: 'email', example: 'email@email.com', }) - email: string; + email?: string; + @IsBoolean() @ApiProperty({ description: 'nickname changed', example: false, diff --git a/src/users/dto/update-userPassword.dto.ts b/src/users/dto/update-userPassword.dto.ts index c3352c1..995e75e 100644 --- a/src/users/dto/update-userPassword.dto.ts +++ b/src/users/dto/update-userPassword.dto.ts @@ -3,7 +3,7 @@ import { ApiProperty } from '@nestjs/swagger'; export class UpdateUserPasswordDto { @ApiProperty({ - example: '1234', // 예시 값을 설정 + example: '1234', description: 'The password of the user for password update', }) password: string; diff --git a/src/users/interfaces/users-service.interface.ts b/src/users/interfaces/users.interface.ts similarity index 61% rename from src/users/interfaces/users-service.interface.ts rename to src/users/interfaces/users.interface.ts index b10405a..d104826 100644 --- a/src/users/interfaces/users-service.interface.ts +++ b/src/users/interfaces/users.interface.ts @@ -1,3 +1,5 @@ +import { User } from '@prisma/client'; + export interface IUsersServiceCreate { email: string; password: string; @@ -13,3 +15,8 @@ export interface IUsersServiceFindByEmail { export interface IUsersServiceFindByNickname { nickname: string; } + +// request에 user 객체를 추가하기 위한 인터페이스 +export interface RequestWithUser extends Request { + user: User; +} diff --git a/src/users/users.controller.ts b/src/users/users.controller.ts index 22f85a5..d2ab046 100644 --- a/src/users/users.controller.ts +++ b/src/users/users.controller.ts @@ -8,14 +8,11 @@ import { Patch, Param, Delete, - NotFoundException, UseGuards, + UploadedFile, + UseInterceptors, + ParseIntPipe, } from '@nestjs/common'; -import { UsersService } from './users.service'; -import { CreateUserDto } from './dto/create-user.dto'; -import { UpdateUserDto } from './dto/update-user.dto'; -import { UpdateUserPasswordDto } from './dto/update-userPassword.dto'; -import { DeleteUserDto } from './dto/delete-user.dto'; import { ApiBearerAuth, ApiCreatedResponse, @@ -26,19 +23,17 @@ import { ApiBody, ApiConsumes, } from '@nestjs/swagger'; -import { UserEntity } from './entities/user.entity'; +import { FileInterceptor } from '@nestjs/platform-express'; +import { CreateUserDto } from './dto/create-user.dto'; +import { UpdateUserDto } from './dto/update-user.dto'; +import { UpdateUserPasswordDto } from './dto/update-userPassword.dto'; +import { DeleteUserDto } from './dto/delete-user.dto'; import { JwtAccessAuthGuard } from 'src/auth/guards/jwt-auth.guard'; -// import { JwtAccessAuthGuard } from '../auth/guards/jwt-auth.guard'; -import { User } from '@prisma/client'; +import { RequestWithUser } from 'src/users/interfaces/users.interface'; +import { UserEntity } from './entities/user.entity'; +import { UsersService } from './users.service'; import { AwsS3Service } from 'src/aws/aws.s3'; -// import { AwsS3Service } from '../aws/aws.s3'; -import { UploadedFile, UseInterceptors } from '@nestjs/common'; -import { FileInterceptor } from '@nestjs/platform-express'; -// request에 user 객체를 추가하기 위한 인터페이스 -interface RequestWithUser extends Request { - user: User; -} @Controller('users') @ApiTags('Users') export class UsersController { @@ -48,18 +43,19 @@ export class UsersController { ) {} // 1. 유저를 생성한다. (회원가입) - @ApiOperation({ summary: '회원가입' }) - @ApiResponse({ status: 201, description: '회원가입이 성공하였습니다.' }) @Post('/signup') + @ApiOperation({ summary: '회원가입' }) @ApiCreatedResponse({ type: UserEntity }) + @ApiResponse({ status: 201, description: '회원가입이 성공하였습니다.' }) async create(@Body() createUserDto: CreateUserDto) { return new UserEntity(await this.usersService.create(createUserDto)); } - //이메일 중복 검증 + // 1-1 이메일 중복 검증 + // FIXME: HeeDragon - ApiBody 추가, message 개선 @Post('checkEmail') - @ApiBody({}) @ApiOperation({ summary: '이메일 중복 확인' }) + @ApiBody({}) async checkEmail(@Body() { email }: { email: string }) { const existingUser = await this.usersService.findByEmail({ email }); if (existingUser) { @@ -69,10 +65,10 @@ export class UsersController { } } - //닉네임 중복 검증 - @Post('checkNickname') - @ApiBody({}) + // 1-2 닉네임 중복 검증 + // FIXME: HeeDragon - ApiBody 추가, message 개선 @Post('checkNickname') @ApiOperation({ summary: '닉네임 중복 확인' }) + @ApiBody({}) async checkNickname(@Body() { nickname }: { nickname: string }) { const existingNickname = await this.usersService.findByNickname({ nickname, @@ -80,146 +76,118 @@ export class UsersController { if (existingNickname) { return { message: '201' }; } else { - //return res.status(200).json({ message: 'Nickname is available.' }); return { message: '200' }; } } // 2. 전체 유저 리스트를 조회한다. @Get() - @UseGuards(JwtAccessAuthGuard) // passport를 사용하여 인증 확인 - @ApiBearerAuth() // Swagger 문서에 Bearer 토큰 인증 추가 @ApiOperation({ summary: '회원 조회' }) @ApiOkResponse({ type: UserEntity, isArray: true }) - async findAll() { - const users = await this.usersService.findAll(); - if (!users) { - throw new NotFoundException('Users does not exist'); - } - const userEntity = users.map((user) => new UserEntity(user)); - console.log(userEntity); - // return users.map((user) => new UserEntity(user)); - return users; + @UseGuards(JwtAccessAuthGuard) + @ApiBearerAuth() + findAll() { + return this.usersService.findAll(); } - // 유저 자신의 정보를 조회한다. + // 2-1. 유저 본인을 조회한다. @Get('me') - @UseGuards(JwtAccessAuthGuard) // passport를 사용하여 인증 확인 - @ApiBearerAuth() // Swagger 문서에 Bearer 토큰 인증 추가 @ApiOperation({ summary: '유저 본인 조회' }) + @UseGuards(JwtAccessAuthGuard) + @ApiBearerAuth() async findMe(@Req() req: RequestWithUser) { - const { userId } = req.user; // request에 user 객체가 추가되었고 userId에 값 할당 + const { userId } = req.user; const user = await this.usersService.findMe(userId); - if (!user) { - throw new NotFoundException('User does not exist'); - } return user; } - // 3. userId를 통한 유저 조회 - @Get(':id') - @UseGuards(JwtAccessAuthGuard) // passport를 사용하여 인증 확인 - @ApiBearerAuth() // Swagger 문서에 Bearer 토큰 인증 추가 + // 2-2. userId를 통한 유저 조회 + @Get(':userId') @ApiOperation({ summary: 'ID로 회원 조회' }) @ApiResponse({ status: 200, description: '유저 정보 조회 성공' }) - async findOne(@Param('id') id: string) { - const user = this.usersService.findOne(+id); - if (!user) { - throw new NotFoundException('User does not exist'); - } - return user; + @ApiResponse({ status: 404, description: '유저 정보가 존재하지 않습니다' }) + @UseGuards(JwtAccessAuthGuard) + @ApiBearerAuth() + async findOne(@Param('userId', ParseIntPipe) userId: number) { + return this.usersService.findOne(userId); } - // 5. user 정보 수정한다. + // 3. user 정보 수정 @Patch('update') - @UseGuards(JwtAccessAuthGuard) // passport를 사용하여 인증 확인 - @ApiBearerAuth() // Swagger 문서에 Bearer 토큰 인증 추가 @ApiOperation({ summary: '회원 정보 수정' }) @ApiResponse({ status: 200, description: '회원 정보가 수정되었습니다' }) @ApiResponse({ status: 400, description: '중복된 닉네임입니다' }) @ApiResponse({ status: 404, description: '유저 정보가 존재하지 않습니다' }) + @UseGuards(JwtAccessAuthGuard) + @ApiBearerAuth() async update( @Req() req: RequestWithUser, @Body() updateUserDto: UpdateUserDto ) { - const { userId } = req.user; // request에 user 객체가 추가되었고 userId에 값 할당 - const user = await this.usersService.findOne(userId); - if (!user) { - throw new NotFoundException('User does not exist'); - } - + const { userId } = req.user; await this.usersService.update(userId, updateUserDto); return { message: '회원 정보가 수정되었습니다' }; } - // 5.1 user 비밀번호 변경한다 + // 4. user 비밀번호 변경한다 @Patch('updatePassword') - @UseGuards(JwtAccessAuthGuard) // passport를 사용하여 인증 확인 - @ApiBearerAuth() // Swagger 문서에 Bearer 토큰 인증 추가 @ApiOperation({ summary: '비밀번호 변경' }) @ApiResponse({ status: 200, description: '비밀번호가 변경되었습니다' }) @ApiResponse({ status: 404, description: '유저 정보가 존재하지 않습니다' }) + @UseGuards(JwtAccessAuthGuard) + @ApiBearerAuth() async updatePassword( @Req() req: RequestWithUser, @Body() updateUserPasswordDto: UpdateUserPasswordDto ) { const { userId } = req.user; // request에 user 객체가 추가되었고 userId에 값 할당 - const user = await this.usersService.findOne(userId); - - if (!user) { - throw new NotFoundException('User does not exist'); - } - await this.usersService.updatePassword(userId, updateUserPasswordDto); return { message: '비밀번호가 변경되었습니다' }; } - // 6. 회원 탈퇴를 한다. + // 5. 회원 탈퇴를 한다. @Delete('withdrawal') - @UseGuards(JwtAccessAuthGuard) // passport를 사용하여 인증 확인 - @ApiBearerAuth() // Swagger 문서에 Bearer 토큰 인증 추가 @ApiOperation({ summary: '회원 탈퇴' }) + @UseGuards(JwtAccessAuthGuard) + @ApiBearerAuth() async remove( @Req() req: RequestWithUser, @Body() deleteUserDto: DeleteUserDto ) { - const { userId } = req.user; // request에 user 객체가 추가되었고 userId에 값 할당 ) - const user = await this.usersService.findOne(userId); - if (!user) { - throw new NotFoundException('User does not exist'); - } + const { userId } = req.user; await this.usersService.remove(userId, deleteUserDto.password); return { message: '탈퇴되었습니다' }; } - // 7. 사용자가 생성한 모임 리스트를 조회한다. - @Get(':id/hostedEvents') + // 6. 사용자가 생성한 모임 리스트를 조회한다. + @Get(':userId/hostedEvents') @ApiOperation({ summary: '내가 호스트한 이벤트 조회' }) - async findHostedEvents(@Param('id') id: string) { - const hostedEvents = await this.usersService.findHostedEvents(+id); + async findHostedEvents(@Param('userId', ParseIntPipe) userId: number) { + const hostedEvents = await this.usersService.findHostedEvents(userId); return hostedEvents; } - // 8. 사용자가 참가한 모임 리스트를 조회한다. - @Get(':id/joinedEvents') + // 7. 사용자가 참가한 모임 리스트를 조회한다. + @Get(':userId/joinedEvents') @ApiOperation({ summary: '내가 참가한 이벤트 조회' }) - findJoinedEvents(@Param('id') id: string) { - const joinedEvents = this.usersService.findJoinedEvents(+id); + async findJoinedEvents(@Param('userId', ParseIntPipe) userId: number) { + const joinedEvents = await this.usersService.findJoinedEvents(userId); return joinedEvents; } - // 9. 사용자가 북마크한 이벤트 리스트를 조회한다. - @Get(':id/bookmarkedEvents') + // 8. 사용자가 북마크한 이벤트 리스트를 조회한다. + @Get(':userId/bookmarkedEvents') @ApiOperation({ summary: '내가 북마크한 이벤트 조회' }) - async findBookmarkedEvents(@Param('id') id: string) { - return await this.usersService.findBookmarkedEvents(+id); + async findBookmarkedEvents(@Param('userId', ParseIntPipe) userId: number) { + const bookmarkedEvents = this.usersService.findBookmarkedEvents(userId); + return bookmarkedEvents; } - // 10. 사용자 유저 프로필 이미지를 업로드 한다. + // 9. 사용자 유저 프로필 이미지를 업로드 한다. @Post('upload') - @UseGuards(JwtAccessAuthGuard) // passport를 사용하여 인증 확인 - @ApiBearerAuth() // Swagger 문서에 Bearer 토큰 인증 추가 @ApiOperation({ summary: '프로필 이미지 업로드' }) + @UseGuards(JwtAccessAuthGuard) + @ApiBearerAuth() @ApiConsumes('multipart/form-data') @UseInterceptors(FileInterceptor('file')) @ApiBody({ @@ -239,12 +207,7 @@ export class UsersController { async updateProfileImage(@Req() req: RequestWithUser, @UploadedFile() file) { const { userId } = req.user; - const user = await this.usersService.findOne(userId); - if (!user) { - throw new NotFoundException('User does not exist'); - } - - // 이미지를 s3에 업로드한다. + //이미지를 s3에 업로드한다. const uploadedFile = (await this.awsS3Service.uploadFile(file)) as { Location: string; }; diff --git a/src/users/users.service.ts b/src/users/users.service.ts index 70f0127..3a7d88a 100644 --- a/src/users/users.service.ts +++ b/src/users/users.service.ts @@ -1,90 +1,100 @@ -/* eslint-disable prettier/prettier */ // src/users/users.service.ts -import { BadRequestException, ConflictException, Injectable, NotFoundException } from '@nestjs/common'; +import { + BadRequestException, + ConflictException, + Injectable, + NotFoundException, +} from '@nestjs/common'; import { CreateUserDto } from './dto/create-user.dto'; import { UpdateUserDto } from './dto/update-user.dto'; import { UpdateUserPasswordDto } from './dto/update-userPassword.dto'; import { PrismaService } from 'src/prisma/prisma.service'; import * as bcrypt from 'bcrypt'; import { User, UserDetail } from '@prisma/client'; -import { IUsersServiceFindByEmail, IUsersServiceFindByNickname } from './interfaces/users-service.interface'; -// import { PrismaService } from '../prisma/prisma.service'; - - +import { + IUsersServiceFindByEmail, + IUsersServiceFindByNickname, +} from './interfaces/users.interface'; @Injectable() export class UsersService { constructor(private prisma: PrismaService) {} - + // 1. 유저를 생성한다. (회원가입) async create(createUserDto: CreateUserDto): Promise { - const { email, password, nickname, intro, confirmPassword/* , profileImg */ } = createUserDto; - // 리팩토링시 !== 로 변경 - if (password != confirmPassword){ - throw new BadRequestException('비밀번호와 비밀번호 확인이 일치하지 않습니다.'); + const { email, password, nickname, intro, confirmPassword } = createUserDto; + // 비밀번호 매칭 체크 + if (password !== confirmPassword) { + throw new BadRequestException( + '비밀번호와 비밀번호 확인이 일치하지 않습니다.' + ); + } + // 이메일 중복 체크 + const existingUser = await this.findByEmail({ email }); + if (existingUser) { + throw new ConflictException('이미 등록된 이메일입니다.'); } - // 이메일 중복 체크 - const existingUser = await this.findByEmail({ email }); - if (existingUser) { - throw new ConflictException('이미 등록된 이메일입니다.'); - } - // 닉네임 중복 체크 - const existingNickname = await this.prisma.userDetail.findUnique({ - where: { nickname }, - }); - if (existingNickname) { - throw new ConflictException('이미 사용 중인 닉네임입니다.'); - } + // 닉네임 중복 체크 + const existingNickname = await this.prisma.userDetail.findUnique({ + where: { nickname }, + }); + if (existingNickname) { + throw new ConflictException('이미 사용 중인 닉네임입니다.'); + } + + const hashedPassword = await bcrypt.hash(password, 10); + + /** 초기 프로필 이미지 세팅 + * Default 프로필 이미지 리스트 + * 순서: 회색, 하늘색, 주황색, 남색, 네온색, 녹색 + */ + const profileImgList = [ + 'https://s3-image-local-mingle.s3.ap-northeast-2.amazonaws.com/profileImg/1698025763231', + 'https://s3-image-local-mingle.s3.ap-northeast-2.amazonaws.com/profileImg/1698029706605', + 'https://s3-image-local-mingle.s3.ap-northeast-2.amazonaws.com/profileImg/1698029779728', + 'https://s3-image-local-mingle.s3.ap-northeast-2.amazonaws.com/profileImg/1698029799098', + 'https://s3-image-local-mingle.s3.ap-northeast-2.amazonaws.com/profileImg/1698029815362', + 'https://s3-image-local-mingle.s3.ap-northeast-2.amazonaws.com/profileImg/1698029828369', + ]; + // Default 프로필 이미지 리스트 랜덤으로 하나 선택 + const randomProfileImg = + profileImgList[Math.floor(Math.random() * profileImgList.length)]; - const hashedPassword = await bcrypt.hash(password, 10); - - // Default 프로필 이미지 리스트 - // 순서: 회색, 하늘색, 주황색, 남색, 네온색, 녹색 - const profileImgList = - ['https://s3-image-local-mingle.s3.ap-northeast-2.amazonaws.com/profileImg/1698025763231', - 'https://s3-image-local-mingle.s3.ap-northeast-2.amazonaws.com/profileImg/1698029706605', - 'https://s3-image-local-mingle.s3.ap-northeast-2.amazonaws.com/profileImg/1698029779728', - 'https://s3-image-local-mingle.s3.ap-northeast-2.amazonaws.com/profileImg/1698029799098', - 'https://s3-image-local-mingle.s3.ap-northeast-2.amazonaws.com/profileImg/1698029815362', - 'https://s3-image-local-mingle.s3.ap-northeast-2.amazonaws.com/profileImg/1698029828369' - ] - // Default 프로필 이미지 리스트 랜덤으로 하나 선택 - const randomProfileImg = profileImgList[Math.floor(Math.random() * profileImgList.length)]; - - // 트랜잭션을 사용하여 user와 UserDetail 생성 - const [user] = await this.prisma.$transaction([ - this.prisma.user.create({ - data: { - email, - password: hashedPassword, - UserDetail: { - create: { - nickname, - intro, - profileImg: randomProfileImg, // default 프로필 이미지 업로드 + // 트랜잭션을 사용하여 user와 UserDetail 생성 + const [user] = await this.prisma.$transaction([ + this.prisma.user.create({ + data: { + email, + password: hashedPassword, + UserDetail: { + create: { + nickname, + intro, + profileImg: randomProfileImg, // default 프로필 이미지 업로드 + }, }, }, - }, - }), - ]); - - return user; - // return existingUser; // HeeDragon's OAuth code + }), + ]); + return user; } - - //1-1이메일 중복 체크 + + //1-1 이메일 중복 체크 async findByEmail({ email }: IUsersServiceFindByEmail): Promise { return this.prisma.user.findUnique({ where: { email } }); } - - //1-2닉네임 중복 체크 - async findByNickname({ nickname }: IUsersServiceFindByNickname): Promise { + + //1-2 닉네임 중복 체크 + async findByNickname({ + nickname, + }: IUsersServiceFindByNickname): Promise { return this.prisma.userDetail.findUnique({ where: { nickname } }); } - // 사용자 ID로 사용자를 찾는 메서드 추가 - async findById(userId: number): Promise { + //1-3 사용자 ID로 사용자 조회 + // FIXME: HeeDragon 필요한지 + async findById(userId: number): Promise { return this.prisma.user.findUnique({ where: { userId }, }); @@ -93,11 +103,11 @@ export class UsersService { // 2. 전체 유저 리스트를 조회한다. async findAll() { return await this.prisma.user.findMany({ - where : { deletedAt: null }, + where: { deletedAt: null }, }); } - - // 3. 유저 본인 조회 + + // 2-1. 유저 본인을 조회한다. async findMe(userId: number) { const user = await this.prisma.user.findUnique({ where: { userId }, @@ -106,79 +116,81 @@ export class UsersService { return user; } - // 3. userId를 통한 유저 조회 - async findOne(id: number) { + // 2-2. userId를 통한 유저 조회 + async findOne(userId: number) { const user = await this.prisma.user.findUnique({ - where: { userId: id }, - include: { UserDetail: true, HostEvents: true, GuestEvents: true}, + where: { userId }, + include: { UserDetail: true, HostEvents: true, GuestEvents: true }, }); - if (!user || user.deletedAt !== null) { - throw new BadRequestException('삭제된 회원이거나 존재하지 않는 회원입니다.'); + throw new NotFoundException( + '삭제된 회원이거나 존재하지 않는 회원입니다.' + ); } return user; } - - // 5. user 정보 수정한다. - async update(id: number, updateUserDto: UpdateUserDto) { + // 3. user 정보 수정 + async update(userId: number, updateUserDto: UpdateUserDto) { const { nickname, intro, nameChanged, userLocation } = updateUserDto; - + const user = await this.prisma.user.findUnique({ - where: { userId: id }, + where: { userId }, }); if (!user) { - throw new BadRequestException('유저 정보가 존재하지 않습니다.'); + throw new NotFoundException('유저 정보가 존재하지 않습니다.'); } - + if (!nameChanged) { // 자기소개, 유저주소 업데이트 : nameChanged == false 면 닉네임에는 변화가 없다는 것임으로 닉네임을 제외한 나머지 정보만 업데이트 // userdetail page 자기소개 업데이트 const updatedUser = await this.prisma.userDetail.update({ - where: { userDetailId: user.userId}, - data: { + where: { userDetailId: user.userId }, + data: { intro: intro, userLocation: userLocation, }, - }); + }); return updatedUser; - } - else { + } else { // 닉네임, 자기소개 업데이트 : nameChanged = true 면 닉네임을 바꿨다는 거니까 닉네임을 포함해서 업데이트 - + // 중복된 닉네임 확인 const existingNickname = await this.prisma.userDetail.findUnique({ where: { nickname }, }); - + if (existingNickname) { throw new ConflictException('이미 존재하는 닉네임입니다.'); } // userdetail page 닉네임, 자기소개, 업데이트 const updatedUser = await this.prisma.userDetail.update({ - where: { userDetailId: user.userId}, - data: { + where: { userDetailId: user.userId }, + data: { intro: intro, nickname: nickname, userLocation: userLocation, }, - }); + }); return updatedUser; } } - // 5.1 update 유저 정보 수정 - 패스워드 변경 - async updatePassword(id: number, updateUserPasswordDto: UpdateUserPasswordDto) { + // 4. update 유저 정보 수정 - 패스워드 변경 + async updatePassword( + userId: number, + updateUserPasswordDto: UpdateUserPasswordDto + ) { const newPassword = updateUserPasswordDto.password; - + // 패스워드 암호화 const hashedNewPassword = await bcrypt.hash(newPassword, 10); - + // 유저 존재 여부 확인 const user = await this.prisma.user.findUnique({ - where: { userId: id }, + where: { userId }, }); if (!user) { throw new BadRequestException('유저 정보가 존재하지 않습니다.'); @@ -186,40 +198,40 @@ export class UsersService { // password 업데이트 const updatedUserPassword = await this.prisma.user.update({ - where: { userId: id }, + where: { userId }, data: { password: hashedNewPassword }, }); - return updatedUserPassword; + return updatedUserPassword; } - // 6. 회원 탈퇴를 한다. + // 5. 회원 탈퇴를 한다. async remove(userId: number, password: string) { const user = await this.prisma.user.findUnique({ - where: { userId: userId }, + where: { userId }, }); if (!user) { - throw new BadRequestException('회원 정보가 존재하지 않습니다.'); + throw new NotFoundException('회원 정보가 존재하지 않습니다.'); } - + // bcrypt를 사용하여 패스워드를 비교한다. const isPasswordMatching = await bcrypt.compare(password, user.password); if (!isPasswordMatching) { throw new BadRequestException('비밀번호가 일치하지 않습니다.'); } - + // 패스워드가 일치하면 유저 삭제 return await this.prisma.user.update({ - where: { userId: userId }, + where: { userId }, data: { deletedAt: new Date() }, }); } - // 7. 사용자가 생성한 모임(Event) 리스트를 조회한다. HostEvents - async findHostedEvents(id: number) { + // 6. 사용자가 생성한 모임(Event) 리스트를 조회한다. HostEvents + async findHostedEvents(userId: number) { return await this.prisma.user.findUnique({ - where: { userId: id }, - include: { + where: { userId }, + include: { HostEvents: { select: { Event: true, @@ -230,11 +242,11 @@ export class UsersService { }); } - // 8. 사용자가 참가한 모임(Event) 리스트를 조회한다. GuestEvents의 guestId, eventId를 이용하여 Event를 찾는다. - async findJoinedEvents(id: number) { + // 7. 사용자가 참가한 모임(Event) 리스트를 조회한다. GuestEvents의 guestId, eventId를 이용하여 Event를 찾는다. + async findJoinedEvents(userId: number) { return await this.prisma.user.findUnique({ - where: { userId: id }, - include: { + where: { userId }, + include: { GuestEvents: { select: { Event: true, @@ -245,52 +257,52 @@ export class UsersService { }); } -// 10. 사용자가 북마크한 이벤트 리스트를 조회한다. -async findBookmarkedEvents(id: number) { - const events = await this.prisma.eventBookmark.findMany({ - where: { UserId: id }, - include: { Event: true }, - orderBy: { - updatedAt: 'desc', // 가장 최신의 이벤트를 먼저 가져옴 - }, - }); - - if (!events.length) { - throw new NotFoundException('북마크한 이벤트가 없습니다.'); - } - - const latestEventBookmarks = new Map(); // Key: EventId, Value: eventBookmark entry + // 8. 사용자가 북마크한 이벤트 리스트를 조회한다. + async findBookmarkedEvents(userId: number) { + const events = await this.prisma.eventBookmark.findMany({ + where: { UserId: userId }, + include: { Event: true }, + orderBy: { + updatedAt: 'desc', // 가장 최신의 이벤트를 먼저 가져옴 + }, + }); - for (const event of events) { - // 이미 본 EventId가 아니면 Map에 추가 - if (!latestEventBookmarks.has(event.EventId)) { - latestEventBookmarks.set(event.EventId, event); + if (!events.length) { + throw new NotFoundException('북마크한 이벤트가 없습니다.'); } - } - // status가 "bookmarked"인 것만 필터링 - const bookmarkedEvents = Array.from(latestEventBookmarks.values()).filter(event => event.status === 'bookmarked'); - // console.log(bookmarkedEvents); - return bookmarkedEvents; // 각 EventId 당 가장 최신의 'bookmarked' 상태의 eventBookmark 엔트리 배열 반환 -} + const latestEventBookmarks = new Map(); // Key: EventId, Value: eventBookmark entry + for (const event of events) { + // 이미 본 EventId가 아니면 Map에 추가 + if (!latestEventBookmarks.has(event.EventId)) { + latestEventBookmarks.set(event.EventId, event); + } + } + // status가 "bookmarked"인 것만 필터링 + const bookmarkedEvents = Array.from(latestEventBookmarks.values()).filter( + (event) => event.status === 'bookmarked' + ); + + return bookmarkedEvents; // 각 EventId 당 가장 최신의 'bookmarked' 상태의 eventBookmark 엔트리 배열 반환 + } // 9. 프로필 이미지를 업데이트 한다. - async updateProfileImage(id: number, profileImg: string) { + async updateProfileImage(userId: number, profileImg: string) { // 먼저 UserId를 통해 UserDetail을 찾고, UserDetail의 profileImg를 업데이트 한다. const userDetail = await this.prisma.userDetail.findFirst({ - where: { UserId: id }, + where: { UserId: userId }, }); if (!userDetail) { - throw new BadRequestException('회원 상세 정보가 존재하지 않습니다.'); + throw new NotFoundException('회원정보가 존재하지 않습니다.'); } - + // userDetailId를 사용하여 프로필 이미지를 업데이트한다. const updatedProfileImage = await this.prisma.userDetail.update({ where: { userDetailId: userDetail.userDetailId }, data: { profileImg: profileImg }, }); return updatedProfileImage.profileImg; - } + } }