๐ŸŽ‰ berenickt ๋ธ”๋กœ๊ทธ์— ์˜จ ๊ฑธ ํ™˜์˜ํ•ฉ๋‹ˆ๋‹ค. ๐ŸŽ‰
Back
NestJs
36-Follow System

1. Follow System ์ด๋ก 

  • ์ธ์Šคํƒ€๊ทธ๋žจ ํŒ”๋กœ์šฐ ์‹œ์Šคํ…œ = ์š”์ฒญ์ด ๋‹จ๋ฑกํ–ฅ์ธ ํŒ”๋กœ์šฐ ์‹œ์Šคํ…œ
  • ์‚ฌ์šฉ์ž A์™€ B๊ฐ€ ์žˆ๋‹ค๊ณ  ๊ฐ€์ •ํ•˜๋ฉด,
    • A๋Š” B๋ฅผ ํŒ”๋กœ์šฐํ•  ์ˆ˜ ์žˆ๊ณ , B๋Š” A๋ฅผ ํŒ”๋กœ์šฐ์•ˆํ•  ์ˆ˜๋„ ์žˆ๋‹ค.
    • ๋ฐ˜๋Œ€์˜ ๊ฒฝ์šฐ๋„ ๋งˆ์ฐฌ๊ฐ€์ง€.
    • ์ฆ‰, ๋‹ค์ˆ˜์˜ ํŒ”๋กœ์›Œ๋ฅผ ๊ฐ€์งˆ ์ˆ˜๋„ ์žˆ๊ณ , ๋‹ค์ˆ˜์˜ ํŒ”๋กœ์šฐ๋„ ํ•  ์ˆ˜ ์žˆ๋‹ค.
    • ๋‹ค์‹œ ๋งํ•ด, Many to Many ๊ด€๊ณ„์ด๋‹ค.
  • ๊ทธ๋Ÿฐ๋ฐ User๋ฅผ ๋‹ค๋Œ€๋‹ค ๊ด€๊ณ„๋กœ ํ• ๋ ค๋Š”๋ฐ, ๋‘˜ ๋‹ค ์œ ์ € ํ…Œ์ด๋ธ”์„ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋‹ค.
  • ์ด๋Ÿด ๋•Œ, ํŒ”๋กœ์šฐ ID์™€ ํŒ”๋กœ์›Œ ID๋ฅผ ๊ฐ€์ง„ User Follow ํ…Œ์ด๋ธ”์ด๋ž€ ์ค‘๊ฐ„ ํ…Œ์ด๋ธ”์„ ๋งŒ๋“ค๋ฉด ๋œ๋‹ค.

๋‹ค์Œ ํ‘œ ์˜ˆ์‹œ๋ฅผ ๋ณด๋ฉด,

follower_idfollowee_id
12
21
34
24
  • 1๋ฒˆ ์‚ฌ์šฉ์ž๊ฐ€ 2๋ฒˆ์„ ํŒ”๋กœ์šฐํ•˜๊ณ  ์žˆ๋‹ค.
  • 2๋ฒˆ ์‚ฌ์šฉ์ž๊ฐ€ 1๋ฒˆ์„ ํŒ”๋กœ์šฐํ•˜๊ณ  ์žˆ๋‹ค. (์ด ๊ฒฝ์šฐ 1๋ฒˆ๊ณผ 2๋ฒˆ์€ ์„œ๋กœ ๋งžํŒ”๋กœ์šฐํ•œ ์ƒํƒœ)
  • 3๋ฒˆ ์‚ฌ์šฉ์ž๊ฐ€ 4๋ฒˆ์„ ํŒ”๋กœ์šฐํ•˜๊ณ  ์žˆ๋‹ค.
  • 4๋ฒˆ ์‚ฌ์šฉ์ž๋Š” ์•„๋ฌด๋„ ํŒ”๋กœ์šฐํ•˜๊ณ  ์žˆ์ง€์•Š๋‹ค. (ํŒ”๋กœ์›Œ๊ฐ€ 3๋ฒˆ, 2๋ฒˆ ํ•ด์„œ 2๋ช… ์žˆ๋‹ค)

2. Followers & Followees ํ”„๋กœํผํ‹ฐ ์ƒ์„ฑ

user ์—”ํ‹ฐํ‹ฐ์— ํŒ”๋กœ์›Œ์™€ ํŒ”๋กœ์œ„ ์†์„ฑ์„ ์ถ”๊ฐ€ํ•œ๋‹ค.

users.entity.ts
1
@Entity()
2
export class UsersModel extends BaseModel {
3
// ์ƒ๋žต
4
5
// ๋‚ด๊ฐ€ ํŒ”๋กœ์šฐ ํ•˜๊ณ  ์žˆ๋Š” ์‚ฌ๋žŒ๋“ค
6
@ManyToMany(() => UsersModel, user => user.followees)
7
@JoinTable()
8
followers: UsersModel[]
9
10
// ๋‚˜๋ฅผ ํŒ”๋กœ์šฐ ํ•˜๊ณ  ์žˆ๋Š” ์‚ฌ๋žŒ๋“ค
11
@ManyToMany(() => UsersModel, user => user.followers)
12
followees: UsersModel[]
13
}

3. Follow ์‹œ์Šคํ…œ ๋กœ์ง ์ž‘์„ฑ ๋ฐ ํ…Œ์ŠคํŠธ

user ์„œ๋น„์Šค์— ํŒ”๋กœ์šฐ ์š”์ฒญ, ํŒ”๋กœ์›Œ ์กฐํšŒ ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•œ๋‹ค.

users.service.ts
1
// **** 4) ํŒ”๋กœ์šฐ ์š”์ฒญ
2
async followUser(followerId: number, followeeId: number) {
3
const user = await this.userRepository.findOne({
4
where: {
5
id: followerId,
6
},
7
relations: {
8
followees: true,
9
},
10
})
11
12
if (!user) {
13
throw new BadRequestException('์กด์žฌํ•˜์ง€ ์•Š๋Š” ํŒ”๋กœ์›Œ์ž…๋‹ˆ๋‹ค.')
14
}
15
16
await this.userRepository.save({
17
...user,
18
followees: [...user.followees, { id: followeeId }],
19
})
20
21
return true
22
}
23
24
// **** 5) ํŒ”๋กœ์›Œ๋“ค ์กฐํšŒ
25
async getFollowers(userId: number): Promise<UsersModel[]> {
26
const user = await this.userRepository.findOne({
27
where: { id: userId },
28
relations: {
29
followers: true,
30
},
31
})
32
33
return user.followers
34
}

user ์ปจํŠธ๋กค๋Ÿฌ์— ํŒ”๋กœ์šฐ ์—”๋“œํฌ์ธํŠธ๋ฅผ ์ถ”๊ฐ€ํ•œ๋‹ค.

users.controller.ts
1
@Get('follow/me')
2
async getFollow(
3
@User() user: UsersModel, //
4
) {
5
return this.usersService.getFollowers(user.id)
6
}
7
8
@Post('follow/:id')
9
async postFollow(
10
@User() user: UsersModel, //
11
@Param('id', ParseIntPipe) followeeId: number,
12
) {
13
await this.usersService.followUser(user.id, followeeId)
14
return true
15
}

4. Follow Table ์ง์ ‘ ์ƒ์„ฑ

์ƒ๋Œ€๋ฐฉ์ด okํ–ˆ๋Š”์ง€, ํŒ”๋กœ์šฐ ์š”์ฒญํ–ˆ์„ ๋–„, ํ—ˆ๊ฐ€๋ฅผ ๋‹ด๊ฑฐ๋‚˜ ์–ธ์ œ ํŒ”๋กœ์šฐ ํ–ˆ๋Š”์ง€์˜ ๋ฐ์ดํ„ฐ๋ฅผ ๋„ฃ๊ณ  ์‹ถ์„ ๋–„๊ฐ€ ์žˆ๋‹ค.

src/users/entities/user-follow.entity.ts ํŒŒ์ผ์„ ๋งŒ๋“ ๋‹ค.

src/users/entities/user-follow.entity.ts
1
import { BaseModel } from 'src/common/entity/base.entity'
2
import { UsersModel } from './users.entity'
3
import { Column, Entity, ManyToOne } from 'typeorm'
4
5
@Entity()
6
export class UserFollowersModel extends BaseModel {
7
@ManyToOne(() => UsersModel, user => user.followers)
8
follower: UsersModel
9
10
@ManyToOne(() => UsersModel, user => user.followees)
11
followee: UsersModel
12
13
@Column({ default: false })
14
isConfirmed: boolean
15
}

users/entity/users.entity.ts ํŒŒ์ผ์˜ ํŒ”๋กœ์šฐ ๊ด€๊ณ„๋ฅผ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ˆ˜์ •ํ•œ๋‹ค.

users/entities/users.entity.ts
1
// ๋‚ด๊ฐ€ ํŒ”๋กœ์šฐ ํ•˜๊ณ  ์žˆ๋Š” ์‚ฌ๋žŒ๋“ค
2
@OneToMany(() => UserFollowersModel, ufm => ufm.follower)
3
followers: UsersModel[]
4
5
// ๋‚˜๋ฅผ ํŒ”๋กœ์šฐ ํ•˜๊ณ  ์žˆ๋Š” ์‚ฌ๋žŒ๋“ค
6
@OneToMany(() => UserFollowersModel, ufm => ufm.followee)
7
followees: UsersModel[]

app ๋ชจ๋“ˆ์— UserFollowModel์„ ์ถ”๊ฐ€ํ•œ๋‹ค.

app.module.ts
1
// ์ƒ๋žต
2
TypeOrmModule.forRoot({
3
// ์ƒ๋žต
4
// entitiesํด๋”์— ์ž‘์„ฑํ•œ PostsModel ๊ฐ€์ ธ์˜ค๊ธฐ
5
entities: [
6
PostsModel, //
7
UsersModel,
8
ImageModel,
9
ChatsModel,
10
MessagesModel,
11
CommentsModel,
12
UserFollowersModel,
13
],
14
synchronize: true,
15
}),

5. Custom Table์— ๋งž์ถฐ์„œ ๋กœ์ง ๋ณ€๊ฒฝ

users/entity/users.entity.ts ํŒŒ์ผ์˜ ํŒ”๋กœ์šฐ ๊ด€๊ณ„๋ฅผ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ˆ˜์ •ํ•œ๋‹ค.

users/entities/users.entity.ts
1
// ๋‚ด๊ฐ€ ํŒ”๋กœ์šฐ ํ•˜๊ณ  ์žˆ๋Š” ์‚ฌ๋žŒ๋“ค
2
@OneToMany(() => UserFollowersModel, ufm => ufm.follower)
3
followers: UserFollowersModel[]
4
5
// ๋‚˜๋ฅผ ํŒ”๋กœ์šฐ ํ•˜๊ณ  ์žˆ๋Š” ์‚ฌ๋žŒ๋“ค
6
@OneToMany(() => UserFollowersModel, ufm => ufm.followee)
7
followees: UserFollowersModel[]

users ์„œ๋น„์Šค์— ๊ธฐ๋Šฅ์„ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ˆ˜์ •ํ•œ๋‹ค.

users.service.ts
1
@Injectable()
2
export class UsersService {
3
constructor(
4
@InjectRepository(UsersModel)
5
private readonly userRepository: Repository<UsersModel>,
6
@InjectRepository(UserFollowersModel)
7
private readonly userFollowersRepository: Repository<UserFollowersModel>,
8
) {}
9
10
// ์ƒ๋žต
11
12
// **** 4) ํŒ”๋กœ์šฐ ์š”์ฒญ
13
async followUser(followerId: number, followeeId: number) {
14
return await this.userFollowersRepository.save({
15
follower: {
16
id: followerId,
17
},
18
followee: {
19
id: followeeId,
20
},
21
})
22
}
23
24
// **** 5) ํŒ”๋กœ์›Œ๋“ค ์กฐํšŒ
25
async getFollowers(userId: number): Promise<UsersModel[]> {
26
const result = await this.userFollowersRepository.find({
27
where: {
28
followee: {
29
id: userId,
30
},
31
},
32
relations: {
33
follower: true,
34
},
35
})
36
37
return result.map(userFollow => userFollow.follower)
38
}
39
}

users ๋ชจ๋“ˆ์— UserFollowersModel์„ importํ•œ๋‹ค.

src/users/users.module.ts
1
@Module({
2
// ์ด ๋ชจ๋“ˆ ์•ˆ์—์„œ UsersModel์„ ์–ด๋””์„œ๋“  ์‚ฌ์šฉ ๊ฐ€๋Šฅ
3
imports: [
4
TypeOrmModule.forFeature([
5
UsersModel, //
6
UserFollowersModel,
7
]),
8
],
9
exports: [UsersService],
10
controllers: [UsersController],
11
providers: [UsersService],
12
})
13
export class UsersModule {}

6. Confirm Follow ๋กœ์ง

users ์ปจํŠธ๋กค๋Ÿฌ ํŒ”๋กœ์šฐ ์š”์ฒญ ์Šน์ธ ์—”๋“œํฌ์ธํŠธ๋ฅผ ์ถ”๊ฐ€ํ•œ๋‹ค.

users.controller.ts
1
// **** ๋‚ด ํŒ”๋กœ์›Œ๋“ค ์กฐํšŒ
2
@Get('follow/me')
3
async getFollow(
4
@User() user: UsersModel, //
5
@Query('includeNotConfirmed', new DefaultValuePipe(false), ParseBoolPipe) includeNotConfirmed: boolean,
6
) {
7
return this.usersService.getFollowers(user.id, includeNotConfirmed)
8
}
9
10
// **** ํŒ”๋กœ์šฐ ์š”์ฒญ ์Šน์ธ
11
@Patch('follow/:id/confirm')
12
async patchFollowConfirm(
13
@User('id') followeeId: number, //
14
@Param('id', ParseIntPipe) followerId: number,
15
) {
16
await this.usersService.confirmFollow(followerId, followeeId)
17
return true
18
}

users ์„œ๋น„์Šค์— ํŒ”๋กœ์šฐ ์š”์ฒญ ์Šน์ธ ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•œ๋‹ค.

users.service.ts
1
// **** 5) ํŒ”๋กœ์›Œ๋“ค ์กฐํšŒ
2
async getFollowers(userId: number, includeNotConfirmed: boolean) {
3
const where = {
4
followee: { id: userId },
5
}
6
7
if (!includeNotConfirmed) {
8
where['isConfirmed'] = true
9
}
10
11
const result = await this.userFollowersRepository.find({
12
where,
13
relations: {
14
follower: true,
15
followee: true,
16
},
17
})
18
19
return result.map(el => ({
20
id: el.follower.id,
21
nickname: el.follower.nickname,
22
email: el.follower.email,
23
isConfirmed: el.isConfirmed,
24
}))
25
}
26
27
// **** ํŒ”๋กœ์šฐ ์š”์ฒญ ์Šน์ธ
28
async confirmFollow(followerId: number, followeeId: number) {
29
const existing = await this.userFollowersRepository.findOne({
30
where: {
31
follower: { id: followerId },
32
followee: { id: followeeId },
33
},
34
relations: {
35
follower: true,
36
followee: true,
37
},
38
})
39
40
if (!existing) {
41
throw new BadRequestException('์กด์žฌํ•˜์ง€ ์•Š๋Š” ํŒ”๋กœ์šฐ ์š”์ฒญ์ž…๋‹ˆ๋‹ค!')
42
}
43
44
await this.userFollowersRepository.save({
45
...existing,
46
isConfirmed: true,
47
})
48
49
return true
50
}

7. ํŒ”๋กœ์šฐ ์ทจ์†Œ ์š”์ฒญ

users ์ปจํŠธ๋กค๋Ÿฌ ํŒ”๋กœ์šฐ ์š”์ฒญ ์ทจ์†Œ ์—”๋“œํฌ์ธํŠธ๋ฅผ ์ถ”๊ฐ€ํ•œ๋‹ค.

users.controller.ts
1
// **** ํŒ”๋กœ์šฐ ์š”์ฒญ ์‚ญ์ œ
2
@Delete('follow/:id')
3
async deleteFollow(
4
@User() user: UsersModel, //
5
@Param('id', ParseIntPipe) followeeId: number,
6
) {
7
await this.usersService.deleteFollow(user.id, followeeId)
8
return true
9
}

users ์„œ๋น„์Šค์— ํŒ”๋กœ์šฐ ์š”์ฒญ ์ทจ์†Œ ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•œ๋‹ค.

users.service.ts
1
// **** ํŒ”๋กœ์šฐ ์š”์ฒญ ์‚ญ์ œ
2
async deleteFollow(followerId: number, followeeId: number) {
3
await this.userFollowersRepository.delete({
4
follower: { id: followerId },
5
followee: { id: followeeId },
6
})
7
8
return true
9
}