NestJS-Follow

jaegeunsong97ยท2024๋…„ 2์›” 23์ผ
0

NestJS

๋ชฉ๋ก ๋ณด๊ธฐ
36/37
post-custom-banner

๐Ÿ–Š๏ธ์ด๋ก 

์ธ์Šคํƒ€๊ทธ๋žจ ๊ธฐ๋ฐ˜์˜ ํŒ”๋กœ์šฐ ์‹œ์Šคํ…œ์„ ์ ์šฉํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

  • Following Many to Many Relation

Mnay To Many ๊ด€๊ณ„์—์„œ๋Š” ์ค‘๊ฐ„ ํ…Œ์ด๋ธ”์„ ๋งŒ๋“ค์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ Follow์˜ ๊ฒฝ์šฐ, User User ํ˜•ํƒœ๊ฐ€ ๋ฉ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋งŒ๋“ค์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

User ํ…Œ์ด๋ธ”์ด Follwer์™€ Follwee ์—ญํ• ์„ ๋‹ด๋‹นํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.


๐Ÿ–Š๏ธFollowers & Followees ํ”„๋กœํผํ‹ฐ ์ƒ์„ฑ

  • users.entity.ts
.
.
// ๋‚ด๊ฐ€ ํŒ”๋กœ์šฐ ํ•˜๊ณ  ์žˆ๋Š” ์‚ฌ๋žŒ๋“ค
@ManyToMany(() => UsersModel, (user) => user.followees)
@JoinTable()
followers: UsersModel[];

// ๋‚˜๋ฅผ ํŒ”๋กœ์šฐ ํ•˜๊ณ  ์žˆ๋Š” ์‚ฌ๋žŒ๋“ค
@ManyToMany(() => UsersModel, (user) => user.followers)
followees: UsersModel[];

pgadmin์œผ๋กœ ํ™•์ธ์„ ํ•˜๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ƒ์„ฑ์ด ๋ฉ๋‹ˆ๋‹ค.


๐Ÿ–Š๏ธFollow ์‹œ์Šคํ…œ ๋กœ์ง ์ž‘์„ฑ ๋ฐ ํ…Œ์ŠคํŠธ

ํŒ”๋กœ์šฐ๋ฅผ ํ•˜๋Š” ๋กœ์ง์„ ์ž‘์„ฑํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

  • users.controller.ts
@Post('follow/:id')
async postFollow(
    @User() user: UsersModel, // follower
    @Param('id', ParseIntPipe) followeeId: number,
) {
    await this.usersService.followUser(
        user.id,
        followeeId
    );
    return true;
}
  • users.service.ts
async followUser(followerId: number, followeeId: number) {
    const user = await this.usersRepository.findOne({
        where: {
          	id: followerId
        },
        relations: {
          	followees: true
        }
    });
    if (!user) throw new BadRequestException(`์กด์žฌํ•˜์ง€ ์•Š๋Š” ํŒ”๋กœ์›Œ ์ž…๋‹ˆ๋‹ค. `);

    await this.usersRepository.save({
        ...user,
        followees: [
            ...user.followees,
            {
              	id: followeeId,
            }
        ]
    })
}

์ด๋ฒˆ์—๋Š” ํŒ”๋กœ์›Œ๋“ค์„ ๊ฐ€์ ธ์˜ค๋Š” API๋ฅผ ๋งŒ๋“ค๊ฒ ์Šต๋‹ˆ๋‹ค.

  • users.controller.ts
@Get('follow/me')
async getFollow(
  	@User() user: UsersModel
) {
  	return this.usersService.getFollowers(user.id);
}
  • users.service.ts
async getFollowers(userId: number): Promise<UsersModel[]> {
    const user = await this.usersRepository.findOne({
        where: {
        	id: userId,
        },
        relations: {
          	followers: true
        }
    });
    return user.followers;
}

ํฌ์ŠคํŠธ๋งจ์œผ๋กœ ํ…Œ์ŠคํŠธ๋ฅผ ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

1๋ฒˆ codefactory๋กœ 2๋ฒˆ codefactory1๋ฅผ ํŒ”๋กœ์šฐ ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค. ๋จผ์ € 1๋ฒˆ ์‚ฌ์šฉ์ž๋กœ ๋กœ๊ทธ์ธ์„ ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค. ํ›„์— 2๋ฒˆ์„ ํŒ”๋กœ์šฐ ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

2๋ฒˆ ์‚ฌ์šฉ์ž๋กœ ๋กœ๊ทธ์ธ์„ ๋‹ค์‹œ ํ•˜๊ณ , ๋‚˜์˜ ํŒ”๋กœ์›Œ ๊ฐ€์ ธ์˜ค๋Š” API๋ฅผ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค.

[
    {
        "id": 1,
        "updatedAt": "2024-01-26T05:58:10.800Z",
        "createdAt": "2024-01-26T05:58:10.800Z",
        "nickname": "codefactory",
        "email": "codefactory@codefactory.ai",
        "role": "USER"
    }
]

๋งŒ์•ฝ 3๋ฒˆ ์‚ฌ์šฉ์ž๋กœ 1๋ฒˆ์„ ํŒ”๋กœ์šฐํ•˜๊ณ  1๋ฒˆ ์‚ฌ์šฉ์ž๋กœ ๋กœ๊ทธ์ธ ํ›„ ๋‚˜์˜ ํŒ”๋กœ์›Œ ๊ฐ€์ ธ์˜ค๋Š” API๋ฅผ ํ˜ธ์ถœํ•˜๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋‚˜์˜ค๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

[
    {
        "id": 2,
        "updatedAt": "2024-01-26T06:48:51.110Z",
        "createdAt": "2024-01-26T06:48:51.110Z",
        "nickname": "codefactory1",
        "email": "codefactory1@codefactory.ai",
        "role": "USER"
    },
    {
        "id": 3,
        "updatedAt": "2024-01-27T18:34:48.009Z",
        "createdAt": "2024-01-27T18:34:48.009Z",
        "nickname": "codefactory19",
        "email": "codefactory19@codefactory.ai",
        "role": "ADMIN"
    }
]

๋งค์šฐ ์ •์„์ ์ธ ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค. ์ด๋Ÿฐ ๋ฐฉ๋ฒ•์œผ๋กœ ํ•˜๋ฉด ์ค‘๊ฐ„ ํ…Œ์ด๋ธ”์— ๋ฐ์ดํ„ฐ๋ฅผ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์—†๊ณ  ์˜ค๋กœ์ง€ follower์™€ followee๋งŒ ์ปฌ๋Ÿผ์œผ๋กœ ์ง€์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค๋ฉด ๋‚ด๊ฐ€ ํŒ”๋กœ์šฐ๋ฅผ ํ–ˆ์„ ๋•Œ, ์ƒ๋Œ€๋ฐฉ์ด confirm์„ ํ–ˆ๋Š”์ง€ ์ด๋Ÿฐ ๋ฐ์ดํ„ฐ๋ฅผ ๋„ฃ์„ ์ˆ˜๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.


๐Ÿ–Š๏ธFollow ํ…Œ์ด๋ธ” ์ง์ ‘ ์ƒ์„ฑ

๋”ฐ๋ผ์„œ ์ง์ ‘ ์ค‘๊ฐ„ ํ…Œ์ด๋ธ”์„ ๋งŒ๋“ค๊ฒ ์Šต๋‹ˆ๋‹ค. ์ง์ ‘ ํ…Œ์ด๋ธ”์„ ๊ตฌํ˜„ํ•ด์„œ ManyToMany ํ˜•์‹์œผ๋กœ ๋งŒ๋“ค๊ฒ ์Šต๋‹ˆ๋‹ค.

  • users/entity/user-followers.entity.ts
import { BaseModel } from "src/common/entity/base.entity";
import { UsersModel } from "./users.entity";
import { Column, Entity, ManyToOne } from "typeorm";

@Entity()
export class UserFollowersModel extends BaseModel {

    @ManyToOne(() => UsersModel, (user) => user.followers)
    follower: UsersModel;

    @ManyToOne(() => UsersModel, (user) => user.followees)
    followee: UsersModel;

    @Column({
      	default: false
    })
    isConfirmed: boolean;
}
  • users.entity.ts
.
.
@OneToMany(() => UserFollowersModel, (ufm) => ufm.follower) // ๋ณ€๊ฒฝ
followers: UserFollowersModel[];

@OneToMany(() => UserFollowersModel, (ufm) => ufm.followee) // ๋ณ€๊ฒฝ
followees: UserFollowersModel[];

๊ทธ๋ฆฌ๊ณ  app.module.ts์— UserFollowersModel๋ฅผ ๋“ฑ๋กํ•ฉ๋‹ˆ๋‹ค.

  • app.module.ts
entities: [
    PostsModel,
    UsersModel,
    ImageModel,
    ChatsModel,
    MessagesModel,
    CommentsModel,
    UserFollowersModel // ์ถ”๊ฐ€
],

์ •์„์ ์ธ ๋ฐฉ๋ฒ•์œผ๋กœ ํ•˜๋ฉด ์ปฌ๋Ÿผ์€ 2๊ฐœ๋กœ ๊ณ ์ •์ด ๋˜์ง€๋งŒ, ์ง์ ‘ ํ…Œ์ด๋ธ”์„ ์ƒ์„ฑํ•˜๋ฉด ์—ฌ๋Ÿฌ ์ปฌ๋Ÿผ์„ ๋„ฃ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


๐Ÿ–Š๏ธCustom Table์— ๋งž์ถฐ์„œ ๋กœ์ง ๋ณ€๊ฒฝ

์ด์ „ ํŒ”๋กœ์šฐ๋ฅผ ํ•˜๋Š” ๊ธฐ๋Šฅ์—์„œ๋Š” relation์ด ManyToMany๋ผ๋Š” ๊ฐ€์ •ํ•˜์— ์ง์ ‘ followee๋ฅผ ๋„ฃ์—ˆ์Šต๋‹ˆ๋‹ค.

  • users.service.ts
async followUser(followerId: number, followeeId: number) {
    const user = await this.usersRepository.findOne({
        where: {
          	id: followerId
        },
        relations: {
          	followees: true
        }
    });
    if (!user) throw new BadRequestException(`์กด์žฌํ•˜์ง€ ์•Š๋Š” ํŒ”๋กœ์›Œ ์ž…๋‹ˆ๋‹ค. `);

    await this.usersRepository.save({
        ...user,
        followees: [
            ...user.followees,
            {
              	id: followeeId,
            }
      ]
    })
}

๋”ฐ๋ผ์„œ ๊ธฐ์กด ์ฝ”๋“œ๋ฅผ ์‚ญ์ œํ•˜๊ณ  ์ƒˆ๋กœ์šด ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

constructor(
    @InjectRepository(UsersModel)
    private readonly usersRepository: Repository<UsersModel>,
    @InjectRepository(UserFollowersModel)
    private readonly userFollowersRepository: Repository<UserFollowersModel>,
) {}
.
.
async followUser(followerId: number, followeeId: number) {
    const result = await this.userFollowersRepository.save({
        follower: {
          	id: followerId
        },
        followee: {
          	id: followeeId
        }
    });

    return true;
}

async getFollowers(userId: number): Promise<UsersModel[]> {
    const result = await this.userFollowersRepository.find({
        where: {
            // ํŒ”๋กœ์šฐํ•˜๋Š” ๋Œ€์ƒ
            followee: {
            	id: userId
            }
        },
        relations: {
            follower: true,
            followee: true
        }
    });
    return result.map((x) => x.follower); // follower๋งŒ ๋ฝ‘์•„์„œ ๋ฆฌ์ŠคํŠธ๋งŒ๋“ค๊ธฐ
}

ํฌ์ŠคํŠธ๋งจ์œผ๋กœ ํ…Œ์ŠคํŠธ๋ฅผ ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

2๋ฒˆ, 3๋ฒˆ ์‚ฌ์šฉ์ž๋กœ 1๋ฒˆ ์‚ฌ์šฉ์ž๋ฅผ ํŒ”๋กœ์šฐ ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

[
    {
        "id": 3,
        "updatedAt": "2024-01-27T18:34:48.009Z",
        "createdAt": "2024-01-27T18:34:48.009Z",
        "nickname": "codefactory19",
        "email": "codefactory19@codefactory.ai",
        "role": "ADMIN"
    },
    {
        "id": 2,
        "updatedAt": "2024-01-26T06:48:51.110Z",
        "createdAt": "2024-01-26T06:48:51.110Z",
        "nickname": "codefactory1",
        "email": "codefactory1@codefactory.ai",
        "role": "USER"
    }
]

๐Ÿ–Š๏ธConfirm Follow ๋กœ์ง ์ถ”๊ฐ€

  • users.controller.ts
@Patch('follow/:id/confirm') // ๋‚˜๋ฅผ ํŒ”๋กœ์šฐ ํ•˜๋ ค๋Š” ์ƒ๋Œ€ id
async patchFollowConfirm(
    @User() user: UsersModel,
    @Param('id', ParseIntPipe) followerId: number,
) {
    await this.usersService.confirmFollow(followerId, user.id);
    return true;
}
  • users.service.ts
async confirmFollow(followerId: number, followeeId: number) {
    // ์ค‘๊ฐ„ํ…Œ์ด๋ธ”์— ๋ฐ์ดํ„ฐ๊ฐ€ ์กด์žฌํ•˜๋Š”์ง€ ํ™•์ธ
    const existing = await this.userFollowersRepository.findOne({
        where: {
            follower: {
              	id: followerId
            },
            followee: {
              	id: followeeId
            }
        },
        relations: {
            follower: true,
            followee: true
        },
    });
    if (!existing) throw new BadRequestException(`์กด์žฌํ•˜์ง€ ์•Š๋Š” ํŒ”๋กœ์šฐ ์š”์ฒญ์ž…๋‹ˆ๋‹ค. `);

    // save๊ฐ’์„ ๋„ฃ์œผ๋ฉด, ๋ณ€๊ฒฝ๋œ ๋ถ€๋ถ„๋งŒ updateํ•œ๋‹ค.
    await this.userFollowersRepository.save({
        ...existing,
        isConfirmed: true,
    });
    return true;
}

๊ทธ๋ฆฌ๊ณ  ํŒ”๋กœ์šฐ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” API์—์„œ๋Š” isConfirmed๊ฐ€ true์ธ๊ฒƒ๋งŒ ๊ฐ€์ ธ์™€์•ผํ•ฉ๋‹ˆ๋‹ค.

async getFollowers(userId: number): Promise<UsersModel[]> {
    const result = await this.userFollowersRepository.find({
        where: {
            followee: {
            	id: userId,
        	},
        	isConfirmed: true, // ์ถ”๊ฐ€
        },
        relations: {
            follower: true,
            followee: true,
        },
    });
  return result.map((x) => x.follower);
}

ํฌ์ŠคํŠธ๋งจ์œผ๋กœ ํ™•์ธ์„ ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค. 1๋ฒˆ ์‚ฌ์šฉ์ž๋กœ ๋กœ๊ทธ์ธ์„ ํ•˜๊ณ  ํŒ”๋กœ์›Œ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๊ธฐ๋Šฅ์„ ํ˜ธ์ถœํ•˜๋ฉด ์•„๋ฌด๊ฒƒ๋„ ๋‚˜์˜ค์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

์™œ๋ƒํ•˜๋ฉด ์šฐ๋ฆฌ๊ฐ€ ํ—ˆ๊ฐ€ํ•œ ํŒ”๋กœ์šฐ ์š”์ฒญ์ด ์—†๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋ฉด 2๋ฒˆ๊ณผ 3๋ฒˆ์ด ํŒ”๋กœ์šฐ ์š”์ฒญ์„ ํ•œ ๊ฒƒ์„ confirm์„ ํ•˜๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

2๋ฒˆ ์‚ฌ์šฉ์ž๊ฐ€ ํŒ”๋กœ์šฐ ์š”์ฒญํ•œ ๊ฒƒ์„ ํ—ˆ๊ฐ€๋ฅผ ํ•˜๋ฉด ํ™•์ธ์ด ๋˜๋Š” ๊ฒƒ์„ ์•Œ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

[
    {
        "id": 2,
        "updatedAt": "2024-01-26T06:48:51.110Z",
        "createdAt": "2024-01-26T06:48:51.110Z",
        "nickname": "codefactory1",
        "email": "codefactory1@codefactory.ai",
        "role": "USER"
    }
]

์ด๋ฒˆ์—๋Š” getFollow()๋ฅผ ํŒ”๋กœ์šฐ ์š”์ฒญ์ด ์˜จ ๊ฒƒ๋“ค์„ ํฌํ•จํ•ด์„œ ๋ณด์—ฌ์ฃผ๋Š” ๊ฒƒ๊ณผ ํฌํ•จํ•˜์ง€ ์•Š๊ณ  ๋ณด์—ฌ์ฃผ๋Š” ์ฝ”๋“œ๋กœ ์ˆ˜์ •ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

  • users.controller.ts
@Get('follow/me')
async getFollow(
    @User() user: UsersModel,
    @Query('includeNotConfirmed', new DefaultValuePipe(false), ParseBoolPipe) includeNotConfirmed: boolean
) {
  	return this.usersService.getFollowers(user.id, includeNotConfirmed);
}
  • users.service.ts
async getFollowers(userId: number, includeNotConfirmed: boolean): Promise<UsersModel[]> {
    const where = {
        // ํŒ”๋กœ์šฐํ•˜๋Š” ๋Œ€์ƒ
        followee: {
        	id: userId
        }
  	};

    // ํ—ˆ๊ฐ€ ํ•œ๊ฒƒ๋“ค๋งŒ ์ถ”๊ฐ€๋ฅผ ํ•ด๋ผ!
    if (!includeNotConfirmed) {
      	where['isConfirmed'] = true;
    }

    const result = await this.userFollowersRepository.find({
        where,
        relations: {
            follower: true,
            followee: true
        }
    });
    return result.map((x) => x.follower);
}

ํฌ์ŠคํŠธ๋งจ์œผ๋กœ ํ…Œ์ŠคํŠธ๋ฅผ ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

[
    {
        "id": 3,
        "updatedAt": "2024-01-27T18:34:48.009Z",
        "createdAt": "2024-01-27T18:34:48.009Z",
        "nickname": "codefactory19",
        "email": "codefactory19@codefactory.ai",
        "role": "ADMIN"
    },
    {
        "id": 2,
        "updatedAt": "2024-01-26T06:48:51.110Z",
        "createdAt": "2024-01-26T06:48:51.110Z",
        "nickname": "codefactory1",
        "email": "codefactory1@codefactory.ai",
        "role": "USER"
    }
]

์‘๋‹ต ๋ฐ์ดํ„ฐ ํ˜•ํƒœ๋ฅผ ๋ฐ”๊พธ๊ฒ ์Šต๋‹ˆ๋‹ค.

async getFollowers(userId: number, includeNotConfirmed: boolean): Promise<UsersModel[]> {
    const where = {
        followee: {
        	id: userId
        }
  	};

    if (!includeNotConfirmed) {
      	where['isConfirmed'] = true;
    }

    const result = await this.userFollowersRepository.find({
        where,
        relations: {
            follower: true,
            followee: true
        }
    });
    return result.map((x) => ({
        id: x.follower.id,
        nickname: x.follower.nickname,
        email: x.follower.email,
        isConfirmed: x.isConfirmed,
    })); // follower๋งŒ ๋ฝ‘์•„์„œ ๋ฆฌ์ŠคํŠธ๋งŒ๋“ค๊ธฐ
}

[
    {
        "id": 3,
        "nickname": "codefactory19",
        "email": "codefactory19@codefactory.ai",
        "isConfirmed": false
    },
    {
        "id": 2,
        "nickname": "codefactory1",
        "email": "codefactory1@codefactory.ai",
        "isConfirmed": true
    }
]

[
    {
        "id": 2,
        "nickname": "codefactory1",
        "email": "codefactory1@codefactory.ai",
        "isConfirmed": true
    }
]

๐Ÿ–Š๏ธFollow ์ทจ์†Œ ์š”์ฒญ ์ž‘์—…

  • users.controller.ts
@Delete('follow/:id')
async deleteFollow(
    @User() user: UsersModel,
    @Param('id', ParseIntPipe) followeeId: number, // ๋‚ด๊ฐ€ ํŒ”๋กœ์šฐํ•˜๋Š” ์ƒ๋Œ€
) {
    await this.usersService.deleteFollow(user.id, followeeId);
    return true;
}
  • users.service.ts
async deleteFollow(followerId: number, followeeId: number) {
    await this.userFollowersRepository.delete({
        follower: {
          	id: followerId,
        },
        followee: {
          	id: followeeId,
        },
    });
    return true;
}

ํฌ์ŠคํŠธ๋งจ์œผ๋กœ ํ…Œ์ŠคํŠธํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค. 2๋ฒˆ ์‚ฌ์šฉ์ž๋Š” 1๋ฒˆ ์‚ฌ์šฉ์ž๋ฅผ ํŒ”๋กœ์šฐ ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

2๋ฒˆ ์‚ฌ์šฉ์ž๋กœ ๋กœ๊ทธ์ธ์„ ํ•ด์„œ 1๋ฒˆ ์‚ฌ์šฉ์ž๋ฅผ ์–ธํŒ”๋กœ์šฐ ํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

[
    {
        "id": 3,
        "nickname": "codefactory19",
        "email": "codefactory19@codefactory.ai",
        "isConfirmed": false
    }
]
profile
๋ธ”๋กœ๊ทธ ์ด์ „ : https://medium.com/@jaegeunsong97
post-custom-banner

0๊ฐœ์˜ ๋Œ“๊ธ€