Skip to content

Commit 8e54838

Browse files
authored
Merge pull request #17 from oodd-team/OD-55
유저 차단 기능 수정
2 parents 591bcf7 + 7bc1d4a commit 8e54838

27 files changed

+12825
-204
lines changed

package-lock.json

+12,122
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+5-3
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,16 @@
1919
"test:e2e": "jest --config ./test/jest-e2e.json"
2020
},
2121
"dependencies": {
22-
"@nestjs/common": "^10.3.2",
22+
"@nestjs/common": "^10.4.5",
2323
"@nestjs/config": "^3.2.3",
2424
"@nestjs/core": "^10.3.2",
2525
"@nestjs/jwt": "^10.2.0",
2626
"@nestjs/passport": "^10.0.3",
2727
"@nestjs/platform-express": "^10.3.2",
28-
"@nestjs/platform-socket.io": "^10.4.6",
28+
"@nestjs/platform-socket.io": "^10.4.7",
2929
"@nestjs/swagger": "^7.4.2",
3030
"@nestjs/typeorm": "^10.0.2",
31-
"@nestjs/websockets": "^10.4.6",
31+
"@nestjs/websockets": "^10.4.7",
3232
"@types/passport-kakao": "^1.0.3",
3333
"@types/socket.io": "^3.0.2",
3434
"class-transformer": "^0.5.1",
@@ -42,6 +42,7 @@
4242
"passport-naver": "^1.0.6",
4343
"reflect-metadata": "^0.2.1",
4444
"rxjs": "^7.8.1",
45+
"socket.io": "^4.8.1",
4546
"typeorm": "^0.3.20"
4647
},
4748
"devDependencies": {
@@ -53,6 +54,7 @@
5354
"@types/express": "^4.17.21",
5455
"@types/jest": "^29.5.12",
5556
"@types/node": "^20.11.16",
57+
"@types/passport-jwt": "^4.0.1",
5658
"@types/supertest": "^6.0.2",
5759
"@typescript-eslint/eslint-plugin": "^6.21.0",
5860
"@typescript-eslint/parser": "^6.21.0",

src/common/entities/post-report.entity.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { Post } from './post.entity';
66
@Entity('PostReport')
77
export class PostReport extends BaseEntity {
88
@ManyToOne(() => User, (user) => user.postReports)
9-
@JoinColumn({ name: 'reporterId' })
9+
@JoinColumn({ name: 'requesterId' })
1010
reporter!: User;
1111

1212
@ManyToOne(() => Post, (post) => post.postReports)

src/common/entities/user-report.entity.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@ import { User } from './user.entity';
55
@Entity('UserReport')
66
export class UserReport extends BaseEntity {
77
@ManyToOne(() => User)
8-
@JoinColumn({ name: 'fromUserId' })
8+
@JoinColumn({ name: 'requesterId' })
99
fromUser!: User;
1010

1111
@ManyToOne(() => User)
12-
@JoinColumn({ name: 'toUserId' })
12+
@JoinColumn({ name: 'targetId' })
1313
toUser!: User;
1414

1515
@Column({ type: 'varchar', length: 500 })

src/common/exception/error/error.code.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,14 @@ export const UNAUTHORIZED = new ErrorCodeVo(
3535
'UNAUTHORIZED',
3636
);
3737

38-
export const FORBIDDEN = new ErrorCodeVo(403, 'Forbidden', 'FORBIDDEN');
38+
export const FORBIDDEN = new ErrorCodeVo(
39+
403, 'Forbidden',
40+
'FORBIDDEN'
41+
);
3942

4043
export const INTERNAL_SERVER_ERROR = new ErrorCodeVo(
4144
500,
4245
'Internal Server Error',
4346
'INTERNAL_SERVER_ERROR',
4447
);
48+

src/common/exception/service.exception.ts

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export const InternalServerException = (message?: string): ServiceException => {
2929
return new ServiceException(INTERNAL_SERVER_ERROR, message);
3030
};
3131

32+
3233
export class ServiceException extends Error {
3334
readonly errorCode: ErrorCode;
3435

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { ApiProperty } from '@nestjs/swagger';
2+
import { PostLikeResponseDto } from './post-like.response';
3+
4+
export class GetPostLikesResponseDto {
5+
@ApiProperty({ example: 3 })
6+
totalLikes!: number;
7+
8+
@ApiProperty({ type: [PostLikeResponseDto] })
9+
likes!: PostLikeResponseDto[];
10+
}
+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { ApiProperty } from '@nestjs/swagger';
2+
3+
export class PostLikeResponseDto {
4+
@ApiProperty({ example: 28 })
5+
id!: number;
6+
7+
@ApiProperty({ example: 8, description: '좋아요를 누른 유저ID'})
8+
userId!: number;
9+
10+
@ApiProperty({ example: 78 })
11+
postId!: number;
12+
13+
@ApiProperty({ example: '2024-08-13T06:59:09.000Z' })
14+
createdAt!: Date;
15+
16+
@ApiProperty({ example: 'activated', description: '좋아요 누르기/취소 여부' })
17+
status!: 'activated' | 'deactivated';
18+
19+
@ApiProperty({ example: '2024-08-13T08:16:28.000Z' })
20+
updatedAt!: Date;
21+
}

src/post-like/post-like.controller.ts

+23-8
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,41 @@
1-
import { Controller, Get, Post } from '@nestjs/common';
1+
import { Controller, Get, Param, Post, Req, UseGuards } from '@nestjs/common';
22
import { PostLikeService } from './post-like.service';
33
import {
44
CreatePostLikeSwagger,
55
GetPostLikesSwagger,
66
} from './post-like.swagger';
77
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
8+
import { PostLikeResponseDto } from './dtos/post-like.response';
9+
import { BaseResponse } from 'src/common/response/dto';
10+
import { GetPostLikesResponseDto } from './dtos/get-post-like.response.dto';
11+
import { AuthGuard } from 'src/auth/guards/jwt.auth.guard';
12+
import { Request } from 'express';
813

914
@ApiBearerAuth('Authorization')
1015
@Controller('post-like')
16+
@UseGuards(AuthGuard)
1117
@ApiTags('[서비스] 게시글 좋아요')
1218
export class PostLikeController {
1319
constructor(private readonly postLikeService: PostLikeService) {}
1420

1521
@Get()
1622
@GetPostLikesSwagger('게시글 좋아요 리스트 조회 API')
17-
getPostLikes() {
18-
// return this.userService.getHello();
23+
async getPostLikes(@Req() req: Request): Promise<BaseResponse<GetPostLikesResponseDto>> {
24+
const userId = req.user.id;
25+
const likesData = await this.postLikeService.getUserLikes(userId);
26+
27+
return new BaseResponse<GetPostLikesResponseDto>(true, 'SUCCESS', likesData);
1928
}
2029

21-
@Post()
22-
@CreatePostLikeSwagger('게시글 좋아요 생성 및 삭제 API')
23-
createPostLike() {
24-
// return this.userService.getHello();
30+
@Post(":postId")
31+
@CreatePostLikeSwagger('게시글 좋아요 생성 및 삭제 API')
32+
async togglePostLike(
33+
@Param('postId') postId: number,
34+
@Req() req: Request
35+
): Promise<BaseResponse<PostLikeResponseDto>> {
36+
const userId = req.user.id;
37+
const postLikeResponse = await this.postLikeService.toggleLike(postId, userId);
38+
39+
return new BaseResponse<PostLikeResponseDto>(true, 'SUCCESS', postLikeResponse);
2540
}
26-
}
41+
}

src/post-like/post-like.module.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1-
import { Module } from '@nestjs/common';
1+
import { forwardRef, Module } from '@nestjs/common';
22
import { PostLikeController } from './post-like.controller';
33
import { PostLikeService } from './post-like.service';
44
import { TypeOrmModule } from '@nestjs/typeorm';
55
import { PostLike } from 'src/common/entities/post-like.entity';
6+
import { PostModule } from 'src/post/post.module';
67

78
@Module({
8-
imports: [TypeOrmModule.forFeature([PostLike])],
9+
imports: [
10+
TypeOrmModule.forFeature([PostLike]),
11+
forwardRef(() => PostModule),
12+
],
913
controllers: [PostLikeController],
1014
providers: [PostLikeService],
1115
exports: [PostLikeService],

src/post-like/post-like.service.ts

+84-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,22 @@
1-
import { Injectable } from '@nestjs/common';
1+
import { forwardRef, Inject, Injectable } from '@nestjs/common';
22
import { PostLike } from 'src/common/entities/post-like.entity';
33
import { QueryRunner } from 'typeorm';
4+
import { InjectRepository } from '@nestjs/typeorm';
5+
import { Repository } from 'typeorm';
6+
import { PostService } from '../post/post.service';
7+
import { PostLikeResponseDto } from './dtos/post-like.response';
8+
import { DataNotFoundException } from 'src/common/exception/service.exception';
9+
import { GetPostLikesResponseDto } from './dtos/get-post-like.response.dto';
410

511
@Injectable()
612
export class PostLikeService {
13+
constructor(
14+
@InjectRepository(PostLike)
15+
private readonly postLikeRepository: Repository<PostLike>,
16+
@Inject(forwardRef(() => PostService))
17+
private readonly postService: PostService,
18+
) {}
19+
720
async deletePostLikeByPostId(
821
postId: number,
922
queryRunner: QueryRunner,
@@ -20,4 +33,73 @@ export class PostLikeService {
2033
}),
2134
);
2235
}
23-
}
36+
37+
// 유저가 좋아요 누른 게시물들 조회
38+
async getUserLikes(userId: number): Promise<GetPostLikesResponseDto> {
39+
const userLikes = await this.postLikeRepository.find({
40+
where: { user: { id: userId }, status: 'activated' },
41+
relations: ['post', 'user'],
42+
});
43+
44+
const likes = userLikes.map((like) => ({
45+
id: like.id,
46+
userId: like.user.id,
47+
postId: like.post.id,
48+
status: like.status,
49+
createdAt: like.createdAt,
50+
updatedAt: like.updatedAt,
51+
}));
52+
53+
return {
54+
totalLikes: likes.length,
55+
likes: likes,
56+
};
57+
}
58+
59+
// 좋아요 상태값
60+
async toggleLike(postId: number, userId: number): Promise<PostLikeResponseDto> {
61+
const post = await this.postService.findByFields({ where: { id: postId } });
62+
if (!post) {
63+
throw DataNotFoundException('게시글을 찾을 수 없습니다.');
64+
}
65+
66+
const existingLike = await this.postLikeRepository.findOne({
67+
where: { post: { id: postId }, user: { id: userId } },
68+
relations: ['user', 'post'],
69+
});
70+
71+
if (existingLike) {
72+
// 좋아요 actiavated인 경우 -> 좋아요 취소
73+
// 좋아요 deactivated인 경우 -> 다시 좋아요 누름
74+
existingLike.status = existingLike.status === 'deactivated' ? 'activated' : 'deactivated';
75+
existingLike.updatedAt = new Date();
76+
await this.postLikeRepository.save(existingLike);
77+
return {
78+
id: existingLike.id,
79+
userId: existingLike.user.id,
80+
postId: existingLike.post.id,
81+
createdAt: existingLike.createdAt,
82+
status: existingLike.status,
83+
updatedAt: existingLike.updatedAt,
84+
};
85+
} else {
86+
// 좋아요 생성 (처음 좋아요 눌림)
87+
const newLike = this.postLikeRepository.create({
88+
user: { id: userId },
89+
post: post,
90+
status: 'activated',
91+
createdAt: new Date(),
92+
updatedAt: new Date(),
93+
});
94+
await this.postLikeRepository.save(newLike);
95+
return {
96+
id: newLike.id,
97+
userId: newLike.user.id,
98+
postId: newLike.post.id,
99+
createdAt: newLike.createdAt,
100+
status: newLike.status,
101+
updatedAt: newLike.updatedAt,
102+
};
103+
}
104+
}
105+
}

src/post-like/post-like.swagger.ts

+63-4
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,70 @@
1-
import { ApiOperation } from '@nestjs/swagger';
1+
import { ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger';
2+
import { BaseResponse } from 'src/common/response/dto';
3+
import { PostLikeResponseDto } from './dtos/post-like.response';
24

35
// 게시글 좋아요 리스트 조회 API Swagger
46
export function GetPostLikesSwagger(apiSummary: string) {
57
return ApiOperation({ summary: apiSummary });
68
}
79

8-
// 게시글 좋아요 생성 API Swagger
10+
// 게시글 좋아요 생성 및 삭제 API Swagger
911
export function CreatePostLikeSwagger(apiSummary: string) {
10-
return ApiOperation({ summary: apiSummary });
11-
}
12+
return (
13+
ApiOperation({ summary: apiSummary }),
14+
ApiParam({
15+
name: 'postId',
16+
required: true,
17+
description: '좋아요를 추가하거나 삭제할 게시글 ID',
18+
example: 78,
19+
}),
20+
ApiResponse({
21+
status: 201,
22+
description: '좋아요가 성공적으로 생성된 경우',
23+
type: BaseResponse<PostLikeResponseDto>,
24+
schema: {
25+
example: {
26+
isSuccess: true,
27+
code: 'SUCCESS',
28+
data: {
29+
id: 28,
30+
userId: 1,
31+
postId: 78,
32+
createdAt: '2024-08-13T06:59:09.000Z',
33+
status: 'activated',
34+
updatedAt: '2024-08-13T08:16:28.000Z',
35+
},
36+
},
37+
},
38+
}),
39+
ApiResponse({
40+
status: 200,
41+
description: '좋아요가 성공적으로 취소된 경우',
42+
type: BaseResponse<PostLikeResponseDto>,
43+
schema: {
44+
example: {
45+
isSuccess: true,
46+
code: 'SUCCESS',
47+
data: {
48+
id: 28,
49+
userId: 1,
50+
postId: 78,
51+
createdAt: '2024-08-13T06:59:09.000Z',
52+
status: 'deactivated',
53+
updatedAt: '2024-08-13T08:16:28.000Z',
54+
},
55+
},
56+
},
57+
}),
58+
ApiResponse({
59+
status: 404,
60+
description: '게시글을 찾을 수 없는 경우',
61+
schema: {
62+
example: {
63+
statusCode: 404,
64+
message: '게시글을 찾을 수 없습니다.',
65+
error: 'Not Found',
66+
},
67+
},
68+
})
69+
);
70+
}
+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { ApiProperty } from '@nestjs/swagger';
2+
import { IsNumber, IsString, MinLength } from 'class-validator';
3+
4+
export class PostReportDto {
5+
@ApiProperty({ example: 1, description: '신고하는 사용자 ID' })
6+
@IsNumber()
7+
requesterId!: number;
8+
9+
@ApiProperty({ example: 3, description: '신고할 게시글 ID' })
10+
@IsNumber()
11+
postId!: number;
12+
13+
@ApiProperty({ example: '윽 이상한 글', description: '신고 사유' })
14+
@IsString()
15+
@MinLength(5)
16+
reason!: string;
17+
}

0 commit comments

Comments
 (0)