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 136c7f4..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: 'test1@naver.com', + example: 'abcd1234@naver.com', }) email: string; @@ -15,7 +15,7 @@ export class LoginDto { @IsNotEmpty() @ApiProperty({ description: 'Password', - example: 'abc123456789!', + example: 'abcd1234!', }) password: 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 14fac0c..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 { Verify, toss } from './interface/verify'; -import { city } from './interface/city'; +import { city, filter } from './interface/city'; @Controller('data') @ApiTags('Data') @@ -10,41 +9,35 @@ export class DataController { constructor(private readonly dataService: DataService) {} @Get('city') - @ApiOperation({ summary: '시/도 데이터목록' }) + @ApiOperation({ + summary: 'API 호출시 query 값(언어: ko, en, jp)에 따라 시/도 데이터를 반환', + }) @ApiQuery({ name: 'lang', type: String, required: true }) - async cityData(@Query() query) { + async cityData(@Query() query: any) { + 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 + return item.lang == query.lang; }); - return langByCity; } @Get('gu_name') - @ApiOperation({ summary: '구/군 데이터 목록' }) + @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: '카테고리, 위치인증 여부 목록' }) + @ApiOperation({ + summary: 'API 호출시 카테고리 목록, 아무나, 위치 인증 여부를 반환', + }) categoryData() { - const data = toss; + const data = filter; return data; } - - @Get('filter/city') - @ApiOperation({ summary: '이벤트 필터링(시/도)' }) - @ApiQuery({ name: 'doName', type: String, required: true }) - filteredEventByCity(@Query() query) { - return this.dataService.filteredEventByCity(query); - } - - @Get('filter/verify') - @ApiOperation({ summary: '이벤트 필터링(위치인증)' }) - @ApiQuery({ name: 'verify', type: String, required: true }) - filteredEventByVerify(@Query() query: Verify) { - return this.dataService.filteredEventByVerify(query); - } } diff --git a/src/data/data.service.ts b/src/data/data.service.ts index 8bf203d..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 }, @@ -19,16 +12,4 @@ export class DataService { }); return data; } - - filteredEventByCity(query) { - return this.prisma.event.findMany({ - where: { eventLocation: query.doName }, - }); - } - - filteredEventByVerify(query: Verify) { - return this.prisma.event.findMany({ - where: { isVerified: query.verify }, - }); - } } diff --git a/src/data/interface/city.ts b/src/data/interface/city.ts index 76d6c94..e65520b 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 e9f89d5..f9e867f 100644 --- a/src/events/dto/create-event.dto.ts +++ b/src/events/dto/create-event.dto.ts @@ -2,66 +2,75 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsString, IsInt, - IsDate, IsOptional, IsBoolean, IsNotEmpty, MaxLength, + Min, } 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() maxSize: number; + @IsNotEmpty() @ApiProperty() eventDate: Date; + @IsNotEmpty() @ApiProperty() signupStartDate: Date; + @IsNotEmpty() @ApiProperty() signupEndDate: Date; + @IsString() + @IsNotEmpty() @ApiProperty({ example: '서울특별시', }) - @IsString() eventLocation: string; - @ApiProperty() @IsString() + @IsNotEmpty() @MaxLength(200) + @ApiProperty({ + example: '재밌게 놀아요', + }) content: string; + @IsString() + @IsNotEmpty() @ApiProperty({ example: '산책', }) - @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 3a5c730..5ba8734 100644 --- a/src/events/dto/update-event.dto.ts +++ b/src/events/dto/update-event.dto.ts @@ -2,56 +2,59 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsString, IsInt, - IsDate, IsOptional, - IsBoolean, + Min, + MaxLength, + IsNotEmpty, } from 'class-validator'; export class UpdateEventDto { - @ApiProperty({ required: false }) @IsString() - @IsOptional() - eventName?: string; + @IsNotEmpty() + @ApiProperty() + eventName: string; - @ApiProperty({ required: false }) @IsInt() - @IsOptional() - maxSize?: number; + @IsNotEmpty() + @ApiProperty() + @Min(1) + maxSize: number; - @ApiProperty({ required: false }) - @IsOptional() - eventDate?: Date; + @IsNotEmpty() + @ApiProperty() + eventDate: Date; - @ApiProperty({ required: false }) - @IsOptional() - signupStartDate?: Date; + @IsNotEmpty() + @ApiProperty() + signupStartDate: Date; - @ApiProperty({ required: false }) - @IsOptional() - signupEndDate?: Date; + @IsNotEmpty() + @ApiProperty() + signupEndDate: Date; - @ApiProperty({ required: false, example: '경기도' }) @IsString() - @IsOptional() - eventLocation?: string; + @IsNotEmpty() + @ApiProperty({ example: '경기도' }) + eventLocation: string; - @ApiProperty({ required: false }) @IsString() - @IsOptional() - content?: string; + @IsNotEmpty() + @MaxLength(200) + @ApiProperty() + content: string; - @ApiProperty({ required: false, example: '산책' }) @IsString() - @IsOptional() - category?: string; + @IsNotEmpty() + @ApiProperty({ example: '산책' }) + category: string; - @ApiProperty({ required: false, default: '🙋‍♀️아무나' }) - @IsOptional() @IsString() - isVerified?: string; + @IsNotEmpty() + @ApiProperty({ default: '🙋‍♀️아무나' }) + isVerified: string; - @ApiProperty() - @IsOptional() @IsString() - eventImg?: string; + @IsOptional() + @ApiProperty() + eventImg: string; } diff --git a/src/events/events.controller.ts b/src/events/events.controller.ts index 89ec1cd..23e5de8 100644 --- a/src/events/events.controller.ts +++ b/src/events/events.controller.ts @@ -7,7 +7,6 @@ import { Patch, Param, Delete, - NotFoundException, Put, UseGuards, ParseIntPipe, @@ -15,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, @@ -27,16 +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 { User } from '@prisma/client'; -import { FileInterceptor } from '@nestjs/platform-express'; import { AwsS3Service } from 'src/aws/aws.s3'; - -// request에 user 객체를 추가하기 위한 인터페이스 -interface RequestWithUser extends Request { - user: User; -} +import { EventsService } from './events.service'; +import { RequestWithUser } from '../users/interfaces/users.interface'; @Controller('events') @ApiTags('Events') @@ -46,102 +40,66 @@ 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, + @Body() createEventDto: CreateEventDto ) { - const { userId } = req.user; // request에 user 객체가 추가되었고 userId에 값 할당 - - return this.eventsService.create(userId, createEventDto) - } - - // 이벤트 이미지 업데이트 - @Put(':eventId/upload') - @UseGuards(JwtAccessAuthGuard) // passport를 사용하여 인증 확인 - @ApiBearerAuth() // Swagger 문서에 Bearer 토큰 인증 추가 - @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) { - - const updatedImg = (await this.awsS3Service.uploadEventFile(file)) as { - Location: string; - }; - - await this.eventsService.updateImg(eventId, updatedImg.Location) - return { - message: '이미지가 업로드되었습니다', - ImgURL: updatedImg, - }; + const { userId } = req.user; + return this.eventsService.create(userId, createEventDto); } - // 이벤트 전체 조회 + // 2. 이벤트 전체 조회 @Get() @ApiOperation({ summary: 'Event 전체 조회' }) @ApiOkResponse({ type: EventEntity, isArray: true }) async findAll() { const events = await this.eventsService.findAll(); + + // 전체 조회 시 이벤트 호스트와 참가자 수 반환 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) // passport를 사용하여 인증 확인 - @ApiBearerAuth() // Swagger 문서에 Bearer 토큰 인증 추가 @ApiOperation({ summary: 'Event 상세 조회' }) @ApiOkResponse({ type: EventEntity }) + @UseGuards(JwtAccessAuthGuard) + @ApiBearerAuth() async findOne( @Req() req: RequestWithUser, @Param('eventId', ParseIntPipe) eventId: number ) { const { userId } = req.user; + // 로그인한 유저가 상세조회한 이벤트에 참가여부 확인 const isJoin = await this.eventsService.isJoin(eventId, userId); const confirmJoin = isJoin ? true : false; const event = await this.eventsService.findOne(eventId); - if (!event) throw new NotFoundException(`${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; @@ -150,24 +108,29 @@ export class EventsController { }; } - // 이벤트 참가 신청 + // 4. 이벤트 참가 @Put(':eventId/join') - @UseGuards(JwtAccessAuthGuard) // passport를 사용하여 인증 확인 - @ApiBearerAuth() // Swagger 문서에 Bearer 토큰 인증 추가 @ApiOperation({ summary: 'Guest로서 Event 참가신청' }) - @ApiCreatedResponse({ description: `모임 참석 신청 / 취소` }) + @ApiCreatedResponse({ + description: `API를 홀수번 호출하면 참석신청 짝수번 신청하면 참석취소`, + }) + @UseGuards(JwtAccessAuthGuard) + @ApiBearerAuth() async join( @Param('eventId', ParseIntPipe) eventId: number, @Req() req: RequestWithUser ) { - const event = await this.eventsService.findOne(eventId); - if (!event) throw new NotFoundException(`${eventId}번 이벤트가 없습니다`); - const { userId } = req.user; + const event = await this.eventsService.findOne(eventId); const isJoin = await this.eventsService.isJoin(eventId, userId); if (!isJoin) { - this.eventsService.join(+eventId, userId); + if (event.maxSize <= event.GuestEvents.length) { + return { + message: `참가인원은 최대${event.maxSize}명 입니다`, + }; + } + this.eventsService.join(eventId, userId); this.eventsService.createRsvpLog(eventId, userId, 'applied'); // 참가 신청 로그 생성 return `${eventId}번 모임 참석 신청!`; } @@ -178,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, @@ -192,37 +155,78 @@ export class EventsController { const { userId } = req.user; const event = await this.eventsService.findOne(eventId); - if (!event) throw new NotFoundException(`${eventId}번 이벤트가 없습니다`); - if (userId !== event.HostEvents[0].HostId) - throw new UnauthorizedException(`수정 권한이 없습니다`); + // 작성자가 호스트인지 확인 후 같지 않은 경우 수정 불가 + if (userId !== event.HostEvents[0].HostId) { + throw new UnauthorizedException(`작성자만 수정할 수 있습니다`); + } + + this.eventsService.update(eventId, updateEventDto); + 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; + }; - return this.eventsService.update(eventId, updateEventDto); + // 이미지 파일 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: 'isDeleted: true / soft Delete' }) + @ApiOkResponse({ + description: + 'soft Delete로 isDelete 필드를 true 바꿔 체킹만 해둔다. 조회는 되지 않음', + }) + @UseGuards(JwtAccessAuthGuard) + @ApiBearerAuth() async remove( @Param('eventId', ParseIntPipe) eventId: number, @Req() req: RequestWithUser ) { const { userId } = req.user; const event = await this.eventsService.findOne(eventId); - if (!event) throw new NotFoundException(`${eventId}번 이벤트가 없습니다`); if (userId !== event.HostEvents[0].HostId) - throw new UnauthorizedException(`삭제 권한이 없습니다`); - + 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 @@ -231,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.spec.ts b/src/events/events.service.spec.ts index f26bdfe..5f25bb9 100644 --- a/src/events/events.service.spec.ts +++ b/src/events/events.service.spec.ts @@ -1,10 +1,12 @@ import { Test, TestingModule } from '@nestjs/testing'; import { EventsService } from './events.service'; +import { PrismaService } from 'src/prisma/prisma.service'; describe('EventsService', () => { + let prisma: PrismaService let service: EventsService; - beforeEach(async () => { + beforeAll(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [EventsService], }).compile(); diff --git a/src/events/events.service.ts b/src/events/events.service.ts index a141b4c..0c083a7 100644 --- a/src/events/events.service.ts +++ b/src/events/events.service.ts @@ -17,6 +17,7 @@ export class EventsService { data: createEventDto, }); + // 카테고리 테이블 생성 await this.prisma.category.create({ data: { EventId: event.eventId, @@ -24,6 +25,7 @@ export class EventsService { }, }); + // 호스트 이벤트 테이블(호스트유저와 이벤트를 맵핑해주는 테이블) 생성 await this.prisma.hostEvent.create({ data: { HostId: userId, @@ -33,7 +35,7 @@ export class EventsService { return event; } - // 이벤트 전체 조회 + // 2. 이벤트 전체 조회 findAll() { return this.prisma.event.findMany({ where: { @@ -62,7 +64,7 @@ export class EventsService { }); } - // 이벤트 상세 조회 + // 3. 이벤트 상세 조회 async findOne(eventId: number) { const event = await this.prisma.event.findUnique({ where: { eventId, isDeleted: false }, @@ -94,11 +96,14 @@ export class EventsService { }, }, }); + if (!event) { + throw new NotFoundException(`${eventId}번 이벤트가 없습니다`); + } return event; } - // 이벤트 조회 로그 + // 3-1. 이벤트 조회수 로거 async createViewLog(eventId: number) { await this.prisma.viewlog.create({ data: { @@ -108,7 +113,7 @@ export class EventsService { }); } - // 이벤트 참가여부 확인 + // 4-1. 이벤트 참가여부 확인 async isJoin(eventId: number, userId: number) { const isJoin = await this.prisma.guestEvent.findFirst({ where: { @@ -119,7 +124,7 @@ export class EventsService { return isJoin; } - // 이벤트 참가 신청 + // 4. 이벤트 참가 신청 async join(eventId: number, userId: number) { await this.prisma.guestEvent.create({ data: { @@ -129,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: { @@ -148,26 +153,26 @@ export class EventsService { }); } - // 이벤트 수정 - update(eventId: number, updateEventDto: UpdateEventDto) { - return this.prisma.event.update({ + // 5. 이벤트 수정 + async update(eventId: number, updateEventDto: UpdateEventDto) { + await this.prisma.event.update({ where: { eventId }, data: updateEventDto, }); } - // 이벤트 이미지 수정 - async updateImg(eventId: number, updatedImg:string) { + // 5-1. 이벤트 이미지 수정 + async updateImg(eventId: number, updatedImg: string) { const ImgUrl = await this.prisma.event.update({ - where: {eventId}, - data: {eventImg: updatedImg} - }) - return ImgUrl + where: { eventId }, + data: { eventImg: updatedImg }, + }); + 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, @@ -175,7 +180,7 @@ export class EventsService { }); } - // 관심있는 북마크 추가 + // 7-1. 관심있는 북마크 추가 async addBookmark(eventId: number, userId: number, status: string) { const lastEventInTable = await this.prisma.eventBookmark.findFirst({ where: { @@ -186,7 +191,6 @@ export class EventsService { eventBookmarkId: 'desc', }, }); - console.log('addBookmark:', lastEventInTable); // 이벤트의 북마크가 존재하지 않거나 가장 최신의 북마크 status가 unbookmarked이면 새로운 로그를 생성한다. if (!lastEventInTable || lastEventInTable.status === 'unbookmarked') { @@ -201,11 +205,13 @@ export class EventsService { } // 이미 북마크가 있으면 이미 존재하는 북마크라고 안내를 보낸다. else { - throw new BadRequestException('이미 북마크한 이벤트는 다시 북마크 할 수 없습니다.'); + throw new BadRequestException( + '이미 북마크한 이벤트는 다시 북마크 할 수 없습니다.' + ); } } - // 관심있는 이벤트 북마크 제거 + // 7-2. 관심있는 이벤트 북마크 제거 async removeBookmark(eventId: number, userId: number, status: string) { const lastEventInTable = await this.prisma.eventBookmark.findFirst({ where: { diff --git a/src/events/interface/event-controller.interface.ts b/src/events/interface/event-controller.interface.ts new file mode 100644 index 0000000..2e26d70 --- /dev/null +++ b/src/events/interface/event-controller.interface.ts @@ -0,0 +1,5 @@ +import { User } from '@prisma/client'; + +export interface RequestWithUser extends Request { + user: User; +} 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 23a9852..3dc1484 100644 --- a/src/searches/searches.controller.ts +++ b/src/searches/searches.controller.ts @@ -1,6 +1,6 @@ import { Controller, Get, Injectable, Query } from '@nestjs/common'; import { SearchesService } from './searches.service'; -import { ApiTags, ApiOperation } from '@nestjs/swagger'; +import { ApiTags, ApiOperation, ApiQuery } from '@nestjs/swagger'; @Controller('search') @Injectable() @@ -10,9 +10,28 @@ export class SearchesController { @Get() @ApiOperation({ summary: '이벤트네임 or 콘텐츠 검색' }) - async search( - @Query('query') query: string // @Query 데코레이터 추가 + async searchByNameOrContent( + @Query('query') query: string ): Promise<{ eventName: string; content: string }[]> { - return this.searchesService.searchEvents(query); + return this.searchesService.searchByNameOrContent(query); + } + + @Get('byLocation') + @ApiOperation({ summary: '이벤트 장소별 검색' }) + @ApiQuery({ name: 'doName', type: String, required: true }) + searchByLocation(@Query() query) { + return this.searchesService.searchByLocation(query); + } + + @Get('byCategory') + @ApiOperation({ summary: '카테고리별 검색' }) + searchByCategory(@Query('query') query: string) { + return this.searchesService.searchByCategory(query); + } + + @Get('byVerify') + @ApiOperation({ summary: '동네만 or 아무나 검색' }) + searchByVerify(@Query('query') query: string) { + return this.searchesService.searchByVerify(query); } } diff --git a/src/searches/searches.module.ts b/src/searches/searches.module.ts index fe6bfcd..c6a6b55 100644 --- a/src/searches/searches.module.ts +++ b/src/searches/searches.module.ts @@ -12,4 +12,3 @@ import { SearchesController } from './searches.controller'; }) export class SearchesModule {} -// /search?query=특정 단어 와 같은 URL로 접근 diff --git a/src/searches/searches.service.ts b/src/searches/searches.service.ts index f9ef3ff..d87684c 100644 --- a/src/searches/searches.service.ts +++ b/src/searches/searches.service.ts @@ -5,7 +5,7 @@ import { PrismaService } from '../prisma/prisma.service'; export class SearchesService { constructor(private readonly prisma: PrismaService) {} - async searchEvents( + async searchByNameOrContent( query: string ): Promise<{ eventName: string; content: string }[]> { // 최소 2글자 이상의 검색어 확인 @@ -14,16 +14,31 @@ export class SearchesService { } const events = await this.prisma.event.findMany({ where: { + isDeleted: false, OR: [ { eventName: { contains: query } }, { content: { contains: query } }, ], }, - select: { - eventName: true, - content: true, - }, }); return events; } + + searchByLocation(query: any) { + return this.prisma.event.findMany({ + where: { eventLocation: query.doName, isDeleted: false }, + }); + } + + searchByCategory(query: string) { + return this.prisma.event.findMany({ + where: { category: query, isDeleted: false }, + }); + } + + searchByVerify(query: string) { + return this.prisma.event.findMany({ + where: { isVerified: query, isDeleted: false }, + }); + } } diff --git a/src/users/dto/create-user.dto.ts b/src/users/dto/create-user.dto.ts index d0e9945..4ba5586 100644 --- a/src/users/dto/create-user.dto.ts +++ b/src/users/dto/create-user.dto.ts @@ -25,7 +25,7 @@ export class CreateUserDto { @MinLength(8) @MaxLength(15) //알파벳 포함 , 숫자 포함 , 특수문자 포함 - @Matches(/^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]+$/) + @Matches(/^(?=.*[a-zA-Z])(?=.*[0-9])(?=.*[!@#$%^&*(),.?":{}|<>]).*$/) @ApiProperty({ description: 'password', example: 'abc123456789!', @@ -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 4c39d93..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,30 +37,9 @@ export class UpdateUserDto { description: 'email', example: 'email@email.com', }) - email: string; - - @IsString() - @IsNotEmpty() - @MinLength(8) - @MaxLength(15) - @IsOptional() - //알파벳 포함 , 숫자 포함 , 특수문자 포함 - @Matches(/^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]+$/) - @ApiProperty({ - description: 'password', - example: 'abc123456789!', - }) - password: string; - - @IsString() - @IsNotEmpty() - @IsOptional() - @ApiProperty({ - description: 'password confirm', - example: 'abc123456789!', - }) - confirmPassword: 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 c11f38b..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,147 +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: 401, 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({ @@ -240,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 095d74c..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,93 +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; } + // 3. user 정보 수정 + async update(userId: number, updateUserDto: UpdateUserDto) { + const { nickname, intro, nameChanged, userLocation } = updateUserDto; - // 5. user 정보 수정한다. - async update(id: number, updateUserDto: UpdateUserDto) { - const { nickname, intro, confirmPassword, 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 면 닉네임에는 변화가 없다는 것임으로 닉네임을 제외한 나머지 정보만 업데이트 - // 패스워드, 패스워드 확인 일치 여부 확인 - const isPasswordMatching = await bcrypt.compare(confirmPassword, user.password); - if (!isPasswordMatching) { - throw new BadRequestException('패스워드가 일치하지 않습니다.'); - } + // 자기소개, 유저주소 업데이트 : 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 { - // 닉네임, 자기소개 업데이트 - // nameChanged = true 면 닉네임을 바꿨다는 거니까 닉네임을 포함해서 업데이트 - + } else { + // 닉네임, 자기소개 업데이트 : nameChanged = true 면 닉네임을 바꿨다는 거니까 닉네임을 포함해서 업데이트 + // 중복된 닉네임 확인 const existingNickname = await this.prisma.userDetail.findUnique({ where: { nickname }, }); - + if (existingNickname) { throw new ConflictException('이미 존재하는 닉네임입니다.'); } - // 패스워드, 패스워드 확인 일치 여부 확인 - const isPasswordMatching = await bcrypt.compare(confirmPassword, user.password); - if (!isPasswordMatching) { - throw new BadRequestException('패스워드가 일치하지 않습니다.'); - } - // 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('유저 정보가 존재하지 않습니다.'); @@ -200,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, @@ -244,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, @@ -259,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; - } + } }