diff --git a/BE/src/postings/postings.controller.ts b/BE/src/postings/postings.controller.ts index 8eba7d7..caf99c6 100644 --- a/BE/src/postings/postings.controller.ts +++ b/BE/src/postings/postings.controller.ts @@ -96,8 +96,9 @@ export class PostingsController { description: '검색어와 선택 태그의 교집합에 해당하는 게시글을 반환합니다.', }) @ApiOkResponse({ schema: { example: search_OK } }) - async search(@Query() searchPostingDto: SearchPostingDto) { - return this.postingsService.findAll(searchPostingDto); + async search(@Req() request, @Query() searchPostingDto: SearchPostingDto) { + const userId = request['user'].id; + return this.postingsService.findAll(userId, searchPostingDto); } @Get('titles') diff --git a/BE/src/postings/postings.service.ts b/BE/src/postings/postings.service.ts index 577c2fa..f329d5b 100644 --- a/BE/src/postings/postings.service.ts +++ b/BE/src/postings/postings.service.ts @@ -18,11 +18,13 @@ import { Report } from './entities/report.entity'; import { Period, Season } from './postings.types'; import { BLOCKING_LIMIT } from './postings.constants'; import { StorageService } from 'src/storage/storage.service'; +import { BlockRepository } from 'src/users/block.repository'; @Injectable() export class PostingsService { constructor( private readonly userRepository: UserRepository, + private readonly blockRepository: BlockRepository, private readonly postingsRepository: PostingsRepository, private readonly likedsRepository: LikedsRepository, private readonly reportsRepository: ReportsRepository, @@ -44,8 +46,10 @@ export class PostingsService { return this.postingsRepository.save(posting); } - async findAll(dto: SearchPostingDto) { + async findAll(userId: string, dto: SearchPostingDto) { + const blockedIds = await this.findBlockedIds(userId); const postings = await this.postingsRepository.findAll( + blockedIds, dto.keyword, dto.sorting, dto.offset, @@ -226,4 +230,9 @@ export class PostingsService { reports: posting.reports, }; } + + private async findBlockedIds(blocker: string) { + const blocks = await this.blockRepository.findByBlocker(blocker); + return blocks.map((block) => block.blocked.id); + } } diff --git a/BE/src/postings/repositories/postings.repository.ts b/BE/src/postings/repositories/postings.repository.ts index 7c4c280..732be33 100644 --- a/BE/src/postings/repositories/postings.repository.ts +++ b/BE/src/postings/repositories/postings.repository.ts @@ -36,6 +36,7 @@ export class PostingsRepository { } async findAll( + blockedIds: string[], keyword: string, sorting: Sorting, offset: number, @@ -50,7 +51,14 @@ export class PostingsRepository { withWhos: WithWho[] ) { const conditions = ['p.title LIKE :keyword']; - let params: { [key: string]: string } = { keyword: `%${keyword}%` }; + let params: { [key: string]: string | string[] } = { + keyword: `%${keyword}%`, + }; + + if (blockedIds.length > 0) { + conditions.push('p.writer NOT IN (:...blockedIds)'); + params = { ...params, blockedIds }; + } if (budget) { conditions.push('p.budget = :budget'); diff --git a/BE/src/users/block.repository.ts b/BE/src/users/block.repository.ts new file mode 100644 index 0000000..6a92f95 --- /dev/null +++ b/BE/src/users/block.repository.ts @@ -0,0 +1,38 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Block } from './entities/block.entity'; +import { BLOCK_REPOSITORY } from './users.constants'; +import { Repository } from 'typeorm'; +import { User } from './entities/user.entity'; + +@Injectable() +export class BlockRepository { + constructor( + @Inject(BLOCK_REPOSITORY) + private blockRepository: Repository + ) {} + + save(blocker: User, blocked: User) { + return this.blockRepository.save({ blocker, blocked }); + } + + findByBlocker(blocker: string) { + return this.blockRepository + .createQueryBuilder('b') + .leftJoinAndSelect('b.blocker', 'x') + .leftJoinAndSelect('b.blocked', 'y') + .where('b.blocker = :blocker', { blocker }) + .getMany(); + } + + findByBlockerAndBlocked(blocker: string, blocked: string) { + return this.blockRepository + .createQueryBuilder('b') + .leftJoinAndSelect('b.blocker', 'x') + .leftJoinAndSelect('b.blocked', 'y') + .where('b.blocker = :blocker AND b.blocked = :blocked', { + blocker, + blocked, + }) + .getOne(); + } +} diff --git a/BE/src/users/entities/block.entity.ts b/BE/src/users/entities/block.entity.ts new file mode 100644 index 0000000..8a07192 --- /dev/null +++ b/BE/src/users/entities/block.entity.ts @@ -0,0 +1,16 @@ +import { Entity, PrimaryGeneratedColumn, ManyToOne, JoinColumn } from 'typeorm'; +import { User } from './user.entity'; + +@Entity() +export class Block { + @PrimaryGeneratedColumn('uuid') + id: string; + + @ManyToOne(() => User, (user) => user.blockers, { nullable: false }) + @JoinColumn({ name: 'blocker', referencedColumnName: 'id' }) + blocker: User; + + @ManyToOne(() => User, (user) => user.blockeds, { nullable: false }) + @JoinColumn({ name: 'blocked', referencedColumnName: 'id' }) + blocked: User; +} diff --git a/BE/src/users/entities/user.entity.ts b/BE/src/users/entities/user.entity.ts index 07f2e06..c4145df 100644 --- a/BE/src/users/entities/user.entity.ts +++ b/BE/src/users/entities/user.entity.ts @@ -1,6 +1,7 @@ import { Liked } from 'src/postings/entities/liked.entity'; import { Posting } from 'src/postings/entities/posting.entity'; import { Report } from 'src/postings/entities/report.entity'; +import { Block } from './block.entity'; import { Entity, Column, @@ -49,4 +50,10 @@ export class User { @ManyToOne(() => SocialLogin, (socialLogin) => socialLogin.users) @JoinColumn({ name: 'social_type', referencedColumnName: 'id' }) socials: SocialLogin; + + @OneToMany(() => Block, (block) => block.blocker) + blockers: Block[]; + + @OneToMany(() => Block, (block) => block.blocked) + blockeds: Block[]; } diff --git a/BE/src/users/users.constants.ts b/BE/src/users/users.constants.ts index 3906300..c85ce48 100644 --- a/BE/src/users/users.constants.ts +++ b/BE/src/users/users.constants.ts @@ -1,2 +1,3 @@ export const DATA_SOURCE = 'DATA_SOURCE'; export const USERS_REPOSITORY = 'USERS_REPOSITORY'; +export const BLOCK_REPOSITORY = 'BLOCK_REPOSITORY'; diff --git a/BE/src/users/users.controller.ts b/BE/src/users/users.controller.ts index 41506fe..34564b7 100644 --- a/BE/src/users/users.controller.ts +++ b/BE/src/users/users.controller.ts @@ -8,6 +8,9 @@ import { UploadedFile, UseGuards, Req, + Post, + Param, + ParseUUIDPipe, } from '@nestjs/common'; import { UsersService } from './users.service'; import { @@ -110,4 +113,14 @@ export class UsersController { checkDuplicatedName(@Query('name') name: string) { return this.usersService.checkDuplicatedName(name); } + + @Post(':id/block') + @ApiOperation({ + summary: '특정 회원 차단', + description: '현재 유저가 id에 해당하는 회원을 차단합니다.', + }) + blockUser(@Req() request, @Param('id', ParseUUIDPipe) id: string) { + const userId = request['user'].id; + return this.usersService.blockUser(userId, id); + } } diff --git a/BE/src/users/users.module.ts b/BE/src/users/users.module.ts index 05b2878..a535949 100644 --- a/BE/src/users/users.module.ts +++ b/BE/src/users/users.module.ts @@ -5,11 +5,12 @@ import { usersProvider } from './users.providers'; import { UserRepository } from './users.repository'; import { StorageModule } from '../storage/storage.module'; import { DatabaseModule } from '../database/database.module'; +import { BlockRepository } from './block.repository'; @Module({ imports: [DatabaseModule, StorageModule], controllers: [UsersController], - providers: [UsersService, ...usersProvider, UserRepository], - exports: [UserRepository, UsersService], + providers: [UsersService, ...usersProvider, UserRepository, BlockRepository], + exports: [UserRepository, UsersService, BlockRepository], }) export class UsersModule {} diff --git a/BE/src/users/users.providers.ts b/BE/src/users/users.providers.ts index be2217b..0376ec7 100644 --- a/BE/src/users/users.providers.ts +++ b/BE/src/users/users.providers.ts @@ -1,6 +1,11 @@ +import { Block } from './entities/block.entity'; import { User } from './entities/user.entity'; import { DataSource } from 'typeorm'; -import { DATA_SOURCE, USERS_REPOSITORY } from './users.constants'; +import { + BLOCK_REPOSITORY, + DATA_SOURCE, + USERS_REPOSITORY, +} from './users.constants'; export const usersProvider = [ { @@ -8,4 +13,9 @@ export const usersProvider = [ useFactory: (dataSource: DataSource) => dataSource.getRepository(User), inject: [DATA_SOURCE], }, + { + provide: BLOCK_REPOSITORY, + useFactory: (dataSource: DataSource) => dataSource.getRepository(Block), + inject: [DATA_SOURCE], + }, ]; diff --git a/BE/src/users/users.service.ts b/BE/src/users/users.service.ts index c6fc479..ef963e3 100644 --- a/BE/src/users/users.service.ts +++ b/BE/src/users/users.service.ts @@ -5,11 +5,13 @@ import { UserRepository } from './users.repository'; import { CheckDuplicatedNameResponseDto } from './dto/check-duplicated-name-response.dto'; import { UpdateUserDto } from './dto/update-user.dto'; import { UpdateUserIpDto } from './dto/update-user-ip.dto'; +import { BlockRepository } from './block.repository'; @Injectable() export class UsersService { constructor( private userRepository: UserRepository, + private blockRepository: BlockRepository, private readonly storageService: StorageService ) {} @@ -113,4 +115,26 @@ export class UsersService { async updateUserIp(id: string, updateUserIpDto: UpdateUserIpDto) { return this.userRepository.update(id, updateUserIpDto); } + + async blockUser(blocker: string, blocked: string) { + if (blocker === blocked) { + throw new BadRequestException('자기 자신을 차단할 수 없습니다.'); + } + + const blockedUser = await this.userRepository.findById(blocked); + if (!blockedUser) { + throw new BadRequestException('존재하지 않는 회원을 차단할 수 없습니다.'); + } + + const block = await this.blockRepository.findByBlockerAndBlocked( + blocker, + blocked + ); + if (block) { + throw new BadRequestException('이미 차단한 회원입니다.'); + } + + const blockerUser = await this.userRepository.findById(blocker); + return this.blockRepository.save(blockerUser, blockedUser); + } }