Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

users api 추가 #11

Open
wants to merge 17 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions prisma/schema.prisma
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

소셜 로그인으로 변경되면서 email, password 는 삭제해도 되지 않을까 싶습니다.
그리고 gender가 API 전체에서 쓰이지 않았는데 처음 명세서 계획대로 가면 updateUser에서 수정 가능해야할 것 같습니다!

Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

generator client {
provider = "prisma-client-js"
binaryTargets = ["windows", "darwin", "debian-openssl-1.1.x"]
}

datasource db {
Expand All @@ -13,14 +12,13 @@ datasource db {

model User {
id BigInt @id @default(autoincrement())
email String @unique
email String? @unique
password String?
provider Provider @default(KAKAO)
oauthId String? @unique
nickname String
defaultPhotoId Int @db.TinyInt // 1~6
userPhotoUuid String?
birthYear DateTime?
birthYear Int?
gender Int? @db.TinyInt
isActivated Boolean
createdAt DateTime @default(now())
Expand Down
5 changes: 3 additions & 2 deletions src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import { Module } from "@nestjs/common";
import { AppController } from "./app.controller";
import { AppService } from "./app.service";
import { AppConfigModule } from "./config/app/config.module";
import { AuthModule } from "./modules/auth/auth.module";
import { UsersModule } from "src/modules/users/users.module";
import { AuthModule } from "src/modules/auth/auth.module";

@Module({
imports: [AppConfigModule, AuthModule],
imports: [AppConfigModule, UsersModule, AuthModule],
controllers: [AppController],
providers: [AppService],
})
Expand Down
3 changes: 3 additions & 0 deletions src/common/dtos/success.dto.ts
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 Dto는 사용이 되지 않는 것 같은데 맞나욥?

Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export class SuccessDto {
success: true;
}
7 changes: 7 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ConfigService } from "@nestjs/config";
import { SwaggerModule, DocumentBuilder } from "@nestjs/swagger";
import { AppModule } from "./app.module";
import { winstonLogger } from "./config/logger/winston/logger";
import { ValidationPipe } from "@nestjs/common";
import { PrismaService } from "./config/database/prisma.service";
import * as cookieParser from "cookie-parser";

Expand All @@ -13,6 +14,12 @@ async function bootstrap() {

const appConfig = app.get(ConfigService);

app.useGlobalPipes(
new ValidationPipe({
transform: true,
})
);

// Swagger Setting
const config = new DocumentBuilder()
.setTitle("Foodhub Backend")
Expand Down
1 change: 1 addition & 0 deletions src/modules/auth/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ import { CacheModule } from "@nestjs/cache-manager";
imports: [CacheModule.register()],
controllers: [AuthController],
providers: [AuthService, Oauth2Strategy],
exports: [Oauth2Strategy],
})
export class AuthModule {}
11 changes: 11 additions & 0 deletions src/modules/users/dtos/create-user.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { IsAlphanumeric, IsNotEmpty, IsString, Length } from "class-validator";
import { ApiProperty } from "@nestjs/swagger";

export class CreateUserDto {
@ApiProperty({ description: "유저 닉네임" })
@IsNotEmpty()
@IsString()
@IsAlphanumeric()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

지금 확인해보니까 명세서 상으로는 한글 닉네임이 안된다고 써있었군요..! 한글도 허용하면 좀 복잡해질까요?

@Length(3, 15)
nickname: string;
}
41 changes: 41 additions & 0 deletions src/modules/users/dtos/update-user.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { ApiProperty } from "@nestjs/swagger";
import {
IsAlphanumeric,
IsBoolean,
IsInt,
IsNotEmpty,
IsOptional,
IsString,
Length,
ValidateNested,
} from "class-validator";

class File {
@IsString()
@IsNotEmpty()
uuid: string;
}

export class UpdateUserDto {
@ApiProperty({ description: "유저 닉네임" })
@IsOptional()
@IsString()
@IsAlphanumeric()
@Length(3, 15)
nickname?: string;

@ApiProperty({ description: "유저 생년" })
@IsOptional()
@IsInt()
birthYear?: number;

@ApiProperty({ description: "유저 활성화 여부" })
@IsOptional()
@IsBoolean()
isActivated?: boolean;

@ApiProperty({ description: "유저 프로필 사진 정보" })
@IsOptional()
@ValidateNested({ each: true })
file?: Partial<File>;
}
35 changes: 35 additions & 0 deletions src/modules/users/dtos/user-response.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { File, User } from "@prisma/client";
import { ApiProperty } from "@nestjs/swagger";

export class UserResponseDto {
constructor(user: User & { Files: Partial<File>[] }) {
this.id = Number(user.id);
this.email = user.email;
this.nickname = user.nickname;
this.defaultPhotoId = user.defaultPhotoId;
this.file = user.Files[0];
this.birthYear = user.birthYear;
this.isActivated = user.isActivated;
this.createdAt = user.createdAt;
this.updatedAt = user.updatedAt;
}

@ApiProperty()
id: number;
@ApiProperty({ required: false })
email?: string;
@ApiProperty()
nickname: string;
@ApiProperty()
defaultPhotoId: number;
@ApiProperty({ required: false })
file?: Partial<File>;
@ApiProperty({ required: false })
birthYear?: number;
@ApiProperty()
isActivated: boolean;
@ApiProperty()
createdAt: Date;
@ApiProperty({ required: false })
updatedAt?: Date;
}
87 changes: 87 additions & 0 deletions src/modules/users/users.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import {
Body,
Controller,
Delete,
Get,
HttpCode,
HttpStatus,
Patch,
Post,
UseGuards,
} from "@nestjs/common";
import {
ApiBadRequestResponse,
ApiCreatedResponse,
ApiInternalServerErrorResponse,
ApiOkResponse,
ApiOperation,
ApiTags,
ApiUnauthorizedResponse,
} from "@nestjs/swagger";
import { UsersService } from "src/modules/users/users.service";
import { CreateUserDto } from "src/modules/users/dtos/create-user.dto";
import { UserResponseDto } from "src/modules/users/dtos/user-response.dto";
import { UpdateUserDto } from "src/modules/users/dtos/update-user.dto";
import { Oauth2Guard } from "src/modules/auth/guards/oauth2.guard";
import { CurrentUser } from "src/common/decorators/current-user.decorator";
import { User } from "@prisma/client";

@ApiTags("유저 API")
@Controller("users")
export class UsersController {
constructor(private readonly usersService: UsersService) {}

@Post()
@UseGuards(Oauth2Guard({ strict: true, isSignUp: true }))
@ApiOperation({
summary: "회원가입",
description: "카카오 로그인 정보를 통해 유저를 DB에 추가한다.",
})
@ApiCreatedResponse({ type: UserResponseDto, description: "회원가입 성공" })
@ApiBadRequestResponse({ description: "이미 가입한 소셜 아이디" })
@ApiInternalServerErrorResponse({ description: "서버 내부 오류" })
async createUser(
@Body() createUserDto: CreateUserDto,
@CurrentUser() user: { oauthId: string; email: string }
) {
const userInfo = await this.usersService.createUser(createUserDto, user);
return new UserResponseDto(userInfo);
}

@Get("me")
@UseGuards(Oauth2Guard({ strict: true }))
@ApiOperation({
summary: "본인 정보 조회",
description: "본인의 유저 정보를 조회한다.",
})
@ApiOkResponse({ type: UserResponseDto, description: "본인 정보 조회 성공" })
@ApiUnauthorizedResponse({ description: "인증 실패 (유효한 토큰이 아니거나 토큰 없음)" })
@ApiInternalServerErrorResponse({ description: "서버 내부 오류" })
async getMe(@CurrentUser() user: User) {
const myInfo = await this.usersService.getMe(user);
return new UserResponseDto(myInfo);
}

@Patch("me")
@UseGuards(Oauth2Guard({ strict: true }))
@ApiOperation({
summary: "본인 정보 수정",
description: "본인의 유저 정보를 수정한다.",
})
@ApiOkResponse({ type: UserResponseDto, description: "본인 정보 수정 성공" })
@ApiUnauthorizedResponse({ description: "인증 실패 (유효한 토큰이 아니거나 토큰 없음)" })
@ApiInternalServerErrorResponse({ description: "서버 내부 오류" })
async updateMe(@Body() updateUserDto: UpdateUserDto, @CurrentUser() user: User) {
const updatedMyInfo = await this.usersService.updateMe(updateUserDto, user);
return new UserResponseDto(updatedMyInfo);
}

@Delete("me")
@HttpCode(HttpStatus.NO_CONTENT)
@UseGuards(Oauth2Guard({ strict: true }))
@ApiUnauthorizedResponse({ description: "인증 실패 (유효한 토큰이 아니거나 토큰 없음)" })
@ApiInternalServerErrorResponse({ description: "서버 내부 오류" })
async deleteMe(@CurrentUser() user: User) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기도 일괄적으로 @ApiOperation() 들어가면 좋을 것 같습니다!

await this.usersService.deleteMe(user);
}
}
11 changes: 11 additions & 0 deletions src/modules/users/users.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Module } from "@nestjs/common";
import { UsersController } from "./users.controller";
import { UsersService } from "./users.service";
import { AuthModule } from "src/modules/auth/auth.module";

@Module({
imports: [AuthModule],
controllers: [UsersController],
providers: [UsersService],
})
export class UsersModule {}
101 changes: 101 additions & 0 deletions src/modules/users/users.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { BadRequestException, Injectable } from "@nestjs/common";
import { CreateUserDto } from "src/modules/users/dtos/create-user.dto";
import { UpdateUserDto } from "src/modules/users/dtos/update-user.dto";
import { User } from "@prisma/client";
import { PrismaService } from "src/config/database/prisma.service";

@Injectable()
export class UsersService {
constructor(private readonly prisma: PrismaService) {}

async createUser(createUserDto: CreateUserDto, user: { oauthId: string; email: string }) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

email은 해당 메서드 내에서 쓰임이 없는 것 같은데 확인 부탁드립니다!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

email을 받아보려고 시도하던 흔적이 남아있네요....ㅎ CurrentUser의 리턴 타입에도 없어서 날리겠습니다

return this.prisma.$transaction(async (tx) => {
const existingUser = await tx.user.findUnique({
where: {
oauthId: user.oauthId,
},
});
if (existingUser) {
throw new BadRequestException("이미 가입한 소셜아이디입니다.");
}
return tx.user.create({
data: {
nickname: createUserDto.nickname,
provider: "KAKAO",
isActivated: true,
defaultPhotoId: Math.floor(Math.random() / 6 + 1),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

defaultPhotoId: Math.floor(Math.random() * 6) + 1

로 수정해야할 것 같습니다!

oauthId: user.oauthId,
},
include: {
Files: {
select: {
uuid: true,
name: true,
mimeType: true,
size: true,
createdAt: true,
updatedAt: true,
},
},
},
});
});
}

async getMe(user: User) {
return this.prisma.user.findUnique({
where: { oauthId: user.oauthId },
include: {
Files: {
select: {
uuid: true,
name: true,
mimeType: true,
size: true,
createdAt: true,
updatedAt: true,
},
},
},
});
}

async updateMe(updateUserDto: UpdateUserDto, user: User) {
return await this.prisma.$transaction(async (tx) => {
if (updateUserDto.file) {
await tx.file.update({
where: { uuid: updateUserDto.file.uuid },
data: { userId: user.id },
});
await tx.file.deleteMany({
where: { userId: user.id, NOT: { uuid: updateUserDto.file.uuid } },
});
}
return tx.user.update({
where: { oauthId: user.oauthId },
data: {
nickname: updateUserDto.nickname,
birthYear: updateUserDto.birthYear,
isActivated: updateUserDto.isActivated,
Comment on lines +77 to +79
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

리퀘스트 바디에 값이 없으면 자동으로 업데이트를 패스해주는군요...! 하나 배워 갑니다 👍

},
include: {
Files: {
select: {
uuid: true,
name: true,
mimeType: true,
size: true,
createdAt: true,
updatedAt: true,
},
},
},
});
});
}

async deleteMe(user: User) {
await this.prisma.user.delete({ where: { id: user.id } });
return true;
}
}