From 0ab32335143714a10852529c72e55265371818cc Mon Sep 17 00:00:00 2001 From: kimminsu Date: Mon, 18 Nov 2024 22:08:49 +0900 Subject: [PATCH 1/4] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=A2=8B=EC=95=84?= =?UTF-8?q?=EC=9A=94=20=EC=97=94=ED=8B=B0=ED=8B=B0=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/chat/chat.module.ts | 3 +- .../backend/src/chat/domain/chat.entity.ts | 5 ++++ .../backend/src/chat/domain/like.entity.ts | 28 +++++++++++++++++++ .../backend/src/stock/domain/stock.entity.ts | 10 +++++-- 4 files changed, 42 insertions(+), 4 deletions(-) create mode 100644 packages/backend/src/chat/domain/like.entity.ts diff --git a/packages/backend/src/chat/chat.module.ts b/packages/backend/src/chat/chat.module.ts index 089b37e4..4a1a27b5 100644 --- a/packages/backend/src/chat/chat.module.ts +++ b/packages/backend/src/chat/chat.module.ts @@ -5,10 +5,11 @@ import { ChatController } from '@/chat/chat.controller'; import { ChatGateway } from '@/chat/chat.gateway'; import { ChatService } from '@/chat/chat.service'; import { Chat } from '@/chat/domain/chat.entity'; +import { Like } from '@/chat/domain/like.entity'; import { StockModule } from '@/stock/stock.module'; @Module({ - imports: [TypeOrmModule.forFeature([Chat]), StockModule, SessionModule], + imports: [TypeOrmModule.forFeature([Chat, Like]), StockModule, SessionModule], controllers: [ChatController], providers: [ChatGateway, ChatService], }) diff --git a/packages/backend/src/chat/domain/chat.entity.ts b/packages/backend/src/chat/domain/chat.entity.ts index 13800bff..a1bab9ca 100644 --- a/packages/backend/src/chat/domain/chat.entity.ts +++ b/packages/backend/src/chat/domain/chat.entity.ts @@ -3,9 +3,11 @@ import { Entity, JoinColumn, ManyToOne, + OneToMany, PrimaryGeneratedColumn, } from 'typeorm'; import { ChatType } from '@/chat/domain/chatType.enum'; +import { Like } from '@/chat/domain/like.entity'; import { DateEmbedded } from '@/common/dateEmbedded.entity'; import { Stock } from '@/stock/domain/stock.entity'; import { User } from '@/user/domain/user.entity'; @@ -23,6 +25,9 @@ export class Chat { @JoinColumn({ name: 'stock_id' }) stock: Stock; + @OneToMany(() => Like, (like) => like.chat) + likes?: Like[]; + @Column() message: string; diff --git a/packages/backend/src/chat/domain/like.entity.ts b/packages/backend/src/chat/domain/like.entity.ts new file mode 100644 index 00000000..84238b15 --- /dev/null +++ b/packages/backend/src/chat/domain/like.entity.ts @@ -0,0 +1,28 @@ +import { + CreateDateColumn, + Entity, + Index, + JoinColumn, + ManyToOne, + PrimaryGeneratedColumn, +} from 'typeorm'; +import { Chat } from '@/chat/domain/chat.entity'; +import { User } from '@/user/domain/user.entity'; + +@Index('chat_user_unique', ['chat', 'user'], { unique: true }) +@Entity() +export class Like { + @PrimaryGeneratedColumn() + id: number; + + @ManyToOne(() => Chat, (chat) => chat.id) + @JoinColumn({ name: 'chat_id' }) + chat: Chat; + + @ManyToOne(() => User, (user) => user.id) + @JoinColumn({ name: 'user_id' }) + user: User; + + @CreateDateColumn({ name: 'created_at' }) + createdAt: Date; +} diff --git a/packages/backend/src/stock/domain/stock.entity.ts b/packages/backend/src/stock/domain/stock.entity.ts index 033833bf..3645b33e 100644 --- a/packages/backend/src/stock/domain/stock.entity.ts +++ b/packages/backend/src/stock/domain/stock.entity.ts @@ -1,6 +1,4 @@ import { Column, Entity, OneToMany, PrimaryColumn } from 'typeorm'; -import { DateEmbedded } from '@/common/dateEmbedded.entity'; -import { UserStock } from '@/stock/domain/userStock.entity'; import { StockDaily, StockMinutely, @@ -8,6 +6,9 @@ import { StockWeekly, StockYearly, } from './stockData.entity'; +import { Like } from '@/chat/domain/like.entity'; +import { DateEmbedded } from '@/common/dateEmbedded.entity'; +import { UserStock } from '@/stock/domain/userStock.entity'; @Entity() export class Stock { @@ -26,8 +27,11 @@ export class Stock { @Column({ name: 'group_code' }) groupCode?: string; + @OneToMany(() => Like, (like) => like.chat) + likes?: Like[]; + @Column(() => DateEmbedded, { prefix: '' }) - dare?: DateEmbedded; + date?: DateEmbedded; @OneToMany(() => UserStock, (userStock) => userStock.stock) userStocks?: UserStock[]; From 74cbf7b5dac3767bafa1f7d02019651b345e2817 Mon Sep 17 00:00:00 2001 From: kimminsu Date: Mon, 18 Nov 2024 22:11:06 +0900 Subject: [PATCH 2/4] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20typeORM=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=ED=8C=8C=EC=9D=BC=20=EC=9D=B4=EB=A6=84=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/app.module.ts | 2 +- .../src/configs/{devTypeormConfig.ts => typeormConfig.ts} | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) rename packages/backend/src/configs/{devTypeormConfig.ts => typeormConfig.ts} (99%) diff --git a/packages/backend/src/app.module.ts b/packages/backend/src/app.module.ts index d0c5f81d..f4735ad2 100644 --- a/packages/backend/src/app.module.ts +++ b/packages/backend/src/app.module.ts @@ -10,7 +10,7 @@ import { ChatModule } from '@/chat/chat.module'; import { typeormDevelopConfig, typeormProductConfig, -} from '@/configs/devTypeormConfig'; +} from '@/configs/typeormConfig'; import { logger } from '@/configs/logger.config'; import { StockModule } from '@/stock/stock.module'; import { UserModule } from '@/user/user.module'; diff --git a/packages/backend/src/configs/devTypeormConfig.ts b/packages/backend/src/configs/typeormConfig.ts similarity index 99% rename from packages/backend/src/configs/devTypeormConfig.ts rename to packages/backend/src/configs/typeormConfig.ts index 2641d8d9..a8c70a18 100644 --- a/packages/backend/src/configs/devTypeormConfig.ts +++ b/packages/backend/src/configs/typeormConfig.ts @@ -24,4 +24,3 @@ export const typeormDevelopConfig: TypeOrmModuleOptions = { //logging: true, synchronize: true, }; - From 1b49e9cf0214d42cc04c7a42f9a17401199210bd Mon Sep 17 00:00:00 2001 From: kimminsu Date: Tue, 19 Nov 2024 12:02:06 +0900 Subject: [PATCH 3/4] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=A2=8B=EC=95=84?= =?UTF-8?q?=EC=9A=94=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/chat/chat.module.ts | 3 +- .../backend/src/chat/dto/like.response.ts | 31 +++++++++++++ packages/backend/src/chat/like.service.ts | 46 +++++++++++++++++++ 3 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 packages/backend/src/chat/dto/like.response.ts create mode 100644 packages/backend/src/chat/like.service.ts diff --git a/packages/backend/src/chat/chat.module.ts b/packages/backend/src/chat/chat.module.ts index 4a1a27b5..62dc1c29 100644 --- a/packages/backend/src/chat/chat.module.ts +++ b/packages/backend/src/chat/chat.module.ts @@ -6,11 +6,12 @@ import { ChatGateway } from '@/chat/chat.gateway'; import { ChatService } from '@/chat/chat.service'; import { Chat } from '@/chat/domain/chat.entity'; import { Like } from '@/chat/domain/like.entity'; +import { LikeService } from '@/chat/like.service'; import { StockModule } from '@/stock/stock.module'; @Module({ imports: [TypeOrmModule.forFeature([Chat, Like]), StockModule, SessionModule], controllers: [ChatController], - providers: [ChatGateway, ChatService], + providers: [ChatGateway, ChatService, LikeService], }) export class ChatModule {} diff --git a/packages/backend/src/chat/dto/like.response.ts b/packages/backend/src/chat/dto/like.response.ts new file mode 100644 index 00000000..94fafde6 --- /dev/null +++ b/packages/backend/src/chat/dto/like.response.ts @@ -0,0 +1,31 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class LikeResponse { + @ApiProperty({ + type: Number, + description: '좋아요를 누른 채팅의 ID', + example: 1, + }) + chatId: number; + + @ApiProperty({ + type: Number, + description: '채팅의 좋아요 수', + example: 45, + }) + likeCount: number; + + @ApiProperty({ + type: String, + description: '결과 메시지', + example: 'like chat', + }) + message: string; + + @ApiProperty({ + type: Date, + description: '좋아요를 누른 시간', + example: '2021-08-01T00:00:00', + }) + date: Date; +} diff --git a/packages/backend/src/chat/like.service.ts b/packages/backend/src/chat/like.service.ts new file mode 100644 index 00000000..a58a0104 --- /dev/null +++ b/packages/backend/src/chat/like.service.ts @@ -0,0 +1,46 @@ +import { BadRequestException, Injectable } from '@nestjs/common'; +import { DataSource, EntityManager } from 'typeorm'; +import { Chat } from '@/chat/domain/chat.entity'; +import { Like } from '@/chat/domain/like.entity'; +import { LikeResponse } from '@/chat/dto/like.response'; + +@Injectable() +export class LikeService { + constructor(private readonly dataSource: DataSource) {} + + async toggleLike(userId: number, chatId: number) { + return await this.dataSource.transaction(async (manager) => { + const chat = await this.findChat(chatId, manager); + return await this.saveLike(manager, chat, userId); + }); + } + + private async findChat(chatId: number, manager: EntityManager) { + const chat = await manager.findOne(Chat, { where: { id: chatId } }); + if (!chat) { + throw new BadRequestException('Chat not found'); + } + return chat; + } + + private async saveLike( + manager: EntityManager, + chat: Chat, + userId: number, + ): Promise { + chat.likeCount += 1; + await Promise.all([ + manager.save(Like, { + user: { id: userId }, + chat, + }), + manager.save(Chat, chat), + ]); + return { + likeCount: chat.likeCount, + message: 'like chat', + chatId: chat.id, + date: chat.date.updatedAt, + }; + } +} From 011f4f0379ab6869e4f76218dbb0e31cf3e132d8 Mon Sep 17 00:00:00 2001 From: kimminsu Date: Tue, 19 Nov 2024 12:03:18 +0900 Subject: [PATCH 4/4] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=A2=8B=EC=95=84?= =?UTF-8?q?=EC=9A=94=20=EC=97=94=EB=93=9C=20=ED=8F=AC=EC=9D=B8=ED=8A=B8=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/chat/chat.controller.ts | 20 ++++++++++-- .../src/chat/decorator/like.decorator.ts | 31 +++++++++++++++++++ packages/backend/src/chat/dto/like.request.ts | 13 ++++++++ 3 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 packages/backend/src/chat/decorator/like.decorator.ts create mode 100644 packages/backend/src/chat/dto/like.request.ts diff --git a/packages/backend/src/chat/chat.controller.ts b/packages/backend/src/chat/chat.controller.ts index f2a5452d..4e2c9371 100644 --- a/packages/backend/src/chat/chat.controller.ts +++ b/packages/backend/src/chat/chat.controller.ts @@ -1,16 +1,25 @@ -import { Controller, Get, Query } from '@nestjs/common'; +import { Body, Controller, Get, Post, Query, UseGuards } from '@nestjs/common'; import { ApiBadRequestResponse, ApiOkResponse, ApiOperation, } from '@nestjs/swagger'; +import SessionGuard from '@/auth/session/session.guard'; import { ChatService } from '@/chat/chat.service'; +import { ToggleLikeApi } from '@/chat/decorator/like.decorator'; import { ChatScrollRequest } from '@/chat/dto/chat.request'; import { ChatScrollResponse } from '@/chat/dto/chat.response'; +import { LikeRequest } from '@/chat/dto/like.request'; +import { LikeService } from '@/chat/like.service'; +import { GetUser } from '@/common/decorator/user.decorator'; +import { User } from '@/user/domain/user.entity'; @Controller('chat') export class ChatController { - constructor(private readonly chatService: ChatService) {} + constructor( + private readonly chatService: ChatService, + private readonly likeService: LikeService, + ) {} @ApiOperation({ summary: '채팅 스크롤 조회 API', @@ -36,4 +45,11 @@ export class ChatController { request.pageSize, ); } + + @UseGuards(SessionGuard) + @ToggleLikeApi() + @Post('like') + async toggleChatLike(@Body() request: LikeRequest, @GetUser() user: User) { + return await this.likeService.toggleLike(user.id, request.chatId); + } } diff --git a/packages/backend/src/chat/decorator/like.decorator.ts b/packages/backend/src/chat/decorator/like.decorator.ts new file mode 100644 index 00000000..e1a1ea6b --- /dev/null +++ b/packages/backend/src/chat/decorator/like.decorator.ts @@ -0,0 +1,31 @@ +import { applyDecorators } from '@nestjs/common'; +import { + ApiBadRequestResponse, + ApiCookieAuth, + ApiOkResponse, + ApiOperation, +} from '@nestjs/swagger'; +import { LikeResponse } from '@/chat/dto/like.response'; + +// eslint-disable-next-line @typescript-eslint/naming-convention +export function ToggleLikeApi() { + return applyDecorators( + ApiCookieAuth(), + ApiOperation({ + summary: '채팅 좋아요 토글 API', + description: '채팅 좋아요를 토글한다.', + }), + ApiOkResponse({ + description: '좋아요 성공', + type: LikeResponse, + }), + ApiBadRequestResponse({ + description: '채팅이 존재하지 않음', + example: { + message: 'Chat not found', + error: 'Bad Request', + statusCode: 400, + }, + }), + ); +} diff --git a/packages/backend/src/chat/dto/like.request.ts b/packages/backend/src/chat/dto/like.request.ts new file mode 100644 index 00000000..02feec50 --- /dev/null +++ b/packages/backend/src/chat/dto/like.request.ts @@ -0,0 +1,13 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNumber } from 'class-validator'; + +export class LikeRequest { + @ApiProperty({ + required: true, + type: Number, + description: '좋아요를 누를 채팅의 ID', + example: 1, + }) + @IsNumber() + chatId: number; +}