Skip to content

Commit

Permalink
Merge pull request #5 from tscenping/yubin
Browse files Browse the repository at this point in the history
- closed #4
  • Loading branch information
yubinquitous authored Nov 14, 2023
2 parents 2bfa619 + c979121 commit d4f2bc1
Show file tree
Hide file tree
Showing 14 changed files with 288 additions and 12 deletions.
2 changes: 1 addition & 1 deletion BE-config
8 changes: 2 additions & 6 deletions src/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,7 @@ import userConfig from 'src/config/user.config';
import { User } from 'src/users/entities/user.entity';
import { UserRepository } from './../users/users.repository';
import { User42Dto } from './dto/user-42.dto';

type UserFindreturn = {
user: User;
mfaCode?: string;
};
import { UserFindReturnDto } from './dto/user-find-return.dto';

@Injectable()
export class AuthService {
Expand All @@ -28,7 +24,7 @@ export class AuthService {
return Promise.resolve('mfaCode'); // TODO: 2FA 코드 생성
}

async findOrCreateUser(userData: User42Dto): Promise<UserFindreturn> {
async findOrCreateUser(userData: User42Dto): Promise<UserFindReturnDto> {
const user = await this.userRepository.findOne({
where: { email: userData.email },
});
Expand Down
6 changes: 6 additions & 0 deletions src/auth/dto/user-find-return.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { User } from 'src/users/entities/user.entity';

export type UserFindReturnDto = {
user: User;
mfaCode?: string;
};
2 changes: 2 additions & 0 deletions src/common/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// pagenation에서 한 페이지에 보여줄 데이터 개수
export const DATA_PER_PAGE = 10;
5 changes: 5 additions & 0 deletions src/common/enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export enum UserStatus {
ONLINE = 'ONLINE',
OFFLINE = 'OFFLINE',
IN_GAME = 'IN_GAME',
}
6 changes: 6 additions & 0 deletions src/users/dto/friend-info.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export type FriendInfoDto = {
id: number;
nickname: string;
avatar: string;
status: string;
};
7 changes: 7 additions & 0 deletions src/users/dto/friend-response.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { FriendInfoDto } from './friend-info.dto';

export type FriendResponseDto = {
friends: FriendInfoDto[];

totalItemCount: number;
};
5 changes: 5 additions & 0 deletions src/users/dto/page-info.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export type PageInfoDto = {
total: number;
page: number;
lastPage: number;
};
15 changes: 15 additions & 0 deletions src/users/entities/friend.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { IsNumber, IsPositive } from 'class-validator';
import { BaseEntity } from 'src/common/base-entity';
import { Column, Entity } from 'typeorm';
@Entity()
export class Friend extends BaseEntity {
@Column()
@IsNumber()
@IsPositive()
fromUserId: number;

@Column()
@IsNumber()
@IsPositive()
toUserId: number;
}
16 changes: 15 additions & 1 deletion src/users/entities/user.entity.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import {
IsBoolean,
IsNotEmpty,
IsNumber,
IsOptional,
IsString,
Length,
Matches,
} from 'class-validator';
import { BaseEntity } from 'src/common/base-entity';
import { UserStatus } from 'src/common/enum';
import { Column, Entity, Unique } from 'typeorm';

@Entity()
Expand All @@ -15,6 +17,7 @@ export class User extends BaseEntity {
@Column({ unique: true })
@IsString()
@Length(1, 10)
@IsNotEmpty()
@Matches(/^[ㄱ-ㅎ가-힣a-zA-Z0-9!]+$/)
nickname: string;

Expand All @@ -23,26 +26,32 @@ export class User extends BaseEntity {
avatar: string;

@Column()
@IsNotEmpty()
@IsString()
email: string;

@Column({ default: false })
@IsNotEmpty()
@IsBoolean()
isMfaEnabled: boolean;

@Column({ default: 1000 }) // TODO: 기본점수 검사
@Column({ default: 1200 })
@IsNotEmpty()
@IsNumber()
ladderScore: number;

@Column({ default: 1000 })
@IsNotEmpty()
@IsNumber()
ladderMaxScore: number;

@Column({ default: 0 })
@IsNotEmpty()
@IsNumber()
winCount: number;

@Column({ default: 0 })
@IsNotEmpty()
@IsNumber()
loseCount: number;

Expand All @@ -68,4 +77,9 @@ export class User extends BaseEntity {
@IsOptional()
@Matches(/^[ㄱ-ㅎ가-힣a-zA-Z0-9]+$/)
statusMessage: string;

@Column({ default: UserStatus.OFFLINE })
@IsNotEmpty()
@IsString()
status: UserStatus;
}
39 changes: 39 additions & 0 deletions src/users/friends.repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { InjectRepository } from '@nestjs/typeorm';
import { Friend } from './entities/friend.entity';
import { Repository, DataSource } from 'typeorm';
import { FriendInfoDto } from './dto/friend-info.dto';
import { DATA_PER_PAGE } from 'src/common/constants';

export class FriendRepository extends Repository<Friend> {
constructor(@InjectRepository(Friend) private dataSource: DataSource) {
super(Friend, dataSource.manager);
}

async findFriend(fromUserId: number, toUserId: number) {
return await this.findOne({
where: {
fromUserId,
toUserId,
},
});
}

async findFriendInfos(
userId: number,
page: number,
): Promise<FriendInfoDto[]> {
// raw query
const friends = await this.dataSource.query(
`
SELECT u.id, u.nickname, u.avatar, u.status
FROM friend f
JOIN "user" u
ON u.id = f."toUserId"
WHERE f."fromUserId" = $1
LIMIT $2 OFFSET $3
`,
[userId, DATA_PER_PAGE, (page - 1) * DATA_PER_PAGE],
);
return friends;
}
}
128 changes: 128 additions & 0 deletions src/users/friends.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { BadRequestException, Injectable, Logger } from '@nestjs/common';
import { FriendRepository } from './friends.repository';
import { UserRepository } from './users.repository';
import { FriendResponseDto } from './dto/friend-response.dto';

@Injectable()
export class FriendsService {
private readonly logger = new Logger(FriendsService.name);

constructor(
private readonly friendRepository: FriendRepository,
private readonly userRepository: UserRepository,
) {}

/**
* 친구 추가
* @param fromUserId 친구요청을 보낸 유저의 id
* @param toUserId 친구요청을 받은 유저의 id
* @returns
*/
async createFriend(fromUserId: number, toUserId: number) {
// 본인에게 친구요청을 보내는지 확인
this.checkSelfFriendship(fromUserId, toUserId);

// 친구요청을 받은 유저가 존재하는지 확인
await this.validateUserExists(toUserId);

// 이미 친구인지 확인
await this.checkAlreadyFriends(fromUserId, toUserId);

// 친구 추가
const friend = this.friendRepository.create({
fromUserId,
toUserId,
});
this.logger.log('friend: ', friend);
await this.friendRepository.save(friend);
}

/**
* 친구 삭제
* @param fromUserId 친구 삭제 요청을 보낸 유저의 id
* @param toUserId 친구 삭제 요청을 받은 유저의 id
*/
async deleteFriend(fromUserId: number, toUserId: number) {
// 본인에게 친구 삭제 요청을 보내는지 확인
this.checkSelfFriendship(fromUserId, toUserId);

// 친구 삭제 요청을 받은 유저가 존재하는지 확인
await this.validateUserExists(toUserId);

// 친구인지 확인
const friend = await this.friendRepository.findFriend(fromUserId, toUserId);
if (!friend) {
throw new BadRequestException(`Not friend with ${toUserId}`);
}

// 친구 삭제
const result = await this.friendRepository.softDelete(friend.id);
if (result.affected !== 1) {
throw new BadRequestException(`Failed to delete friend with ${toUserId}`);
}
this.logger.log('result: ', result);
}

/**
* 친구 목록 조회
* @param userId 친구 목록을 조회할 유저의 id
* @param page 페이지 번호
* @returns 친구 목록
*/
async findFriendsWithPage(
userId: number,
page: number,
): Promise<FriendResponseDto> {
// 친구 목록 조회
const friends = await this.friendRepository.findFriendInfos(userId, page);

// 페이지 정보 조회
const totalItemCount = await this.friendRepository.count({
where: {
fromUserId: userId,
},
});

this.logger.log('friends: ', friends);
this.logger.log('pageInfo: ', totalItemCount);

return { friends, totalItemCount };
}

/**
* 유저가 존재하는지 확인
*/
async validateUserExists(userId: number) {
const user = await this.userRepository.findOne({
where: {
id: userId,
},
});

if (!user) {
throw new BadRequestException(`User with id ${userId} doesn't exist`);
}
}

/**
* 본인에게 친구/친구삭제 요청을 보내는지 확인
*/
private checkSelfFriendship(fromUserId: number, toUserId: number) {
if (fromUserId === toUserId) {
throw new BadRequestException(`Can't be friend with yourself`);
}
}

/**
* 이미 친구인지 확인
*/
private async checkAlreadyFriends(fromUserId: number, toUserId: number) {
const isExistFriend = await this.friendRepository.findFriend(
fromUserId,
toUserId,
);
if (isExistFriend) {
throw new BadRequestException(`Already friends`);
}
}
}
54 changes: 52 additions & 2 deletions src/users/users.controller.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,57 @@
import { Controller } from '@nestjs/common';
import {
Body,
Controller,
Delete,
Get,
Param,
ParseIntPipe,
Post,
UseGuards,
} from '@nestjs/common';
import { GetUser } from 'src/auth/get-user.decorator';
import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard';
import { PositiveIntPipe } from 'src/common/pipes/positiveInt.pipe';
import { User } from './entities/user.entity';
import { FriendsService } from './friends.service';
import { UsersService } from './users.service';

@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
constructor(
private readonly usersService: UsersService,
private readonly friendsService: FriendsService,
) {}

@Post('/friends')
@UseGuards(JwtAuthGuard)
async createFriend(
@GetUser() user: User,
@Body('friendId', ParseIntPipe, PositiveIntPipe) toUserId: number,
) {
await this.friendsService.createFriend(user.id, toUserId);
// TODO: 친구요청을 받은 유저에게 알림 보내기
}

@Delete('/friends')
@UseGuards(JwtAuthGuard)
async deleteFriend(
@GetUser() user: User,
@Body('friendId', ParseIntPipe, PositiveIntPipe) toUserId: number,
) {
await this.friendsService.deleteFriend(user.id, toUserId);
}

@Get('/friends/:page')
@UseGuards(JwtAuthGuard)
async findFriendsWithPage(
@GetUser() user: User,
@Param('page', ParseIntPipe, PositiveIntPipe) page: number,
) {
const friendResponseDto = await this.friendsService.findFriendsWithPage(
user.id,
page,
);

return friendResponseDto;
}
}
7 changes: 5 additions & 2 deletions src/users/users.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@ import { UsersController } from './users.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './entities/user.entity';
import { UserRepository } from './users.repository';
import { Friend } from './entities/friend.entity';
import { FriendRepository } from './friends.repository';
import { FriendsService } from './friends.service';

@Module({
imports: [TypeOrmModule.forFeature([User])],
imports: [TypeOrmModule.forFeature([User, Friend])],
controllers: [UsersController],
providers: [UsersService, UserRepository],
providers: [UsersService, FriendsService, UserRepository, FriendRepository],
exports: [UsersService],
})
export class UsersModule {}

0 comments on commit d4f2bc1

Please sign in to comment.