From 16e7592c16e302d41ce8521de12632ec6f06ed68 Mon Sep 17 00:00:00 2001 From: kimminsu Date: Wed, 20 Nov 2024 11:16:29 +0900 Subject: [PATCH 1/8] =?UTF-8?q?=E2=9C=85=20test:=20datasource=20mock=20?= =?UTF-8?q?=ED=83=80=EC=9E=85=20=EC=97=90=EB=9F=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/user/user.service.spec.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/backend/src/user/user.service.spec.ts b/packages/backend/src/user/user.service.spec.ts index a0a3f19d..df80a82c 100644 --- a/packages/backend/src/user/user.service.spec.ts +++ b/packages/backend/src/user/user.service.spec.ts @@ -1,9 +1,9 @@ /* eslint-disable max-lines-per-function */ -import { BadRequestException, NotFoundException } from '@nestjs/common'; -import { DataSource, EntityManager } from 'typeorm'; -import { User } from './domain/user.entity'; -import { OauthType } from '@/user/domain/ouathType'; -import { UserService } from '@/user/user.service'; +import { BadRequestException, NotFoundException } from "@nestjs/common"; +import { DataSource, EntityManager } from "typeorm"; +import { User } from "./domain/user.entity"; +import { OauthType } from "@/user/domain/ouathType"; +import { UserService } from "@/user/user.service"; export function createDataSourceMock( managerMock?: Partial, @@ -15,7 +15,7 @@ export function createDataSourceMock( }; return { - getRepository: managerMock.getRepository, + getRepository: managerMock?.getRepository, transaction: jest.fn().mockImplementation(async (work) => { return work({ ...defaultManagerMock, ...managerMock }); }), From 5160dc5e151fc3915f27bbef9388325262e83f6f Mon Sep 17 00:00:00 2001 From: kimminsu Date: Wed, 20 Nov 2024 11:16:56 +0900 Subject: [PATCH 2/8] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20=EC=B1=84?= =?UTF-8?q?=ED=8C=85=20=EC=8A=A4=ED=81=AC=EB=A1=A4=20=EC=A4=91=EB=B3=B5=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/chat/chat.controller.ts | 2 +- packages/backend/src/chat/chat.gateway.ts | 2 +- .../backend/src/chat/chat.service.spec.ts | 5 +- packages/backend/src/chat/chat.service.ts | 54 +++++-------------- 4 files changed, 17 insertions(+), 46 deletions(-) diff --git a/packages/backend/src/chat/chat.controller.ts b/packages/backend/src/chat/chat.controller.ts index d295f0c9..0670c9fa 100644 --- a/packages/backend/src/chat/chat.controller.ts +++ b/packages/backend/src/chat/chat.controller.ts @@ -53,7 +53,7 @@ export class ChatController { @Req() req: Express.Request, ) { const user = req.user as User; - return await this.chatService.scrollNextChat(request, user?.id); + return await this.chatService.scrollChat(request, user?.id); } @UseGuards(SessionGuard) diff --git a/packages/backend/src/chat/chat.gateway.ts b/packages/backend/src/chat/chat.gateway.ts index 77db725d..65474edb 100644 --- a/packages/backend/src/chat/chat.gateway.ts +++ b/packages/backend/src/chat/chat.gateway.ts @@ -86,7 +86,7 @@ export class ChatGateway implements OnGatewayConnection { const { stockId, pageSize } = await this.getChatScrollQuery(client); await this.validateExistStock(stockId); client.join(stockId); - const messages = await this.chatService.scrollFirstChat( + const messages = await this.chatService.scrollChat( { stockId, pageSize, diff --git a/packages/backend/src/chat/chat.service.spec.ts b/packages/backend/src/chat/chat.service.spec.ts index 24c77fae..1057ffed 100644 --- a/packages/backend/src/chat/chat.service.spec.ts +++ b/packages/backend/src/chat/chat.service.spec.ts @@ -8,9 +8,8 @@ describe('ChatService 테스트', () => { const chatService = new ChatService(dataSource as DataSource); await expect(() => - chatService.scrollNextChat({ + chatService.scrollChat({ stockId: 'A005930', - latestChatId: 1, pageSize: 101, }), ).rejects.toThrow('pageSize should be less than 100'); @@ -21,7 +20,7 @@ describe('ChatService 테스트', () => { const chatService = new ChatService(dataSource as DataSource); await expect(() => - chatService.scrollFirstChat({ stockId: 'A005930', pageSize: 101 }), + chatService.scrollChat({ stockId: 'A005930', pageSize: 101 }), ).rejects.toThrow('pageSize should be less than 100'); }); }); diff --git a/packages/backend/src/chat/chat.service.ts b/packages/backend/src/chat/chat.service.ts index 569a44ef..3b9ae9d3 100644 --- a/packages/backend/src/chat/chat.service.ts +++ b/packages/backend/src/chat/chat.service.ts @@ -23,13 +23,7 @@ export class ChatService { }); } - async scrollFirstChat(chatScrollQuery: ChatScrollQuery, userId?: number) { - this.validatePageSize(chatScrollQuery); - const result = await this.findFirstChatScroll(chatScrollQuery, userId); - return await this.toScrollResponse(result, chatScrollQuery.pageSize); - } - - async scrollNextChat(chatScrollQuery: ChatScrollQuery, userId?: number) { + async scrollChat(chatScrollQuery: ChatScrollQuery, userId?: number) { this.validatePageSize(chatScrollQuery); const result = await this.findChatScroll(chatScrollQuery, userId); return await this.toScrollResponse(result, chatScrollQuery.pageSize); @@ -55,51 +49,29 @@ export class ChatService { chatScrollQuery: ChatScrollQuery, userId?: number, ) { - if (!chatScrollQuery.latestChatId) { - return await this.findFirstChatScroll(chatScrollQuery, userId); - } else { - return await this.findNextChatScroll(chatScrollQuery); - } + const queryBuilder = this.buildChatScrollQuery(chatScrollQuery, userId); + return queryBuilder.getMany(); } - private async findFirstChatScroll( + private buildChatScrollQuery( chatScrollQuery: ChatScrollQuery, userId?: number, ) { const queryBuilder = this.dataSource.createQueryBuilder(Chat, 'chat'); - if (!chatScrollQuery.pageSize) { - chatScrollQuery.pageSize = DEFAULT_PAGE_SIZE; - } - const { stockId, pageSize } = chatScrollQuery; - return queryBuilder + const { stockId, latestChatId, pageSize } = chatScrollQuery; + const size = pageSize ? pageSize : DEFAULT_PAGE_SIZE; + + queryBuilder .leftJoinAndSelect('chat.likes', 'like', 'like.user_id = :userId', { userId, }) .where('chat.stock_id = :stockId', { stockId }) .orderBy('chat.id', 'DESC') - .take(pageSize + 1) - .getMany(); - } - - private async findNextChatScroll( - chatScrollQuery: ChatScrollQuery, - userId?: number, - ) { - const queryBuilder = this.dataSource.createQueryBuilder(Chat, 'chat'); - if (!chatScrollQuery.pageSize) { - chatScrollQuery.pageSize = DEFAULT_PAGE_SIZE; + .take(size + 1); + if (latestChatId) { + queryBuilder.andWhere('chat.id < :latestChatId', { latestChatId }); } - const { stockId, latestChatId, pageSize } = chatScrollQuery; - return queryBuilder - .leftJoinAndSelect('chat.likes', 'like', 'like.user_id = :userId', { - userId, - }) - .where('chat.stock_id = :stockId and chat.id < :latestChatId', { - stockId, - latestChatId, - }) - .orderBy('chat.id', 'DESC') - .take(pageSize + 1) - .getMany(); + + return queryBuilder; } } From 8f5f7729fd612a129774320378c2be3bf2d6fb98 Mon Sep 17 00:00:00 2001 From: kimminsu Date: Wed, 20 Nov 2024 11:38:37 +0900 Subject: [PATCH 3/8] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=A2=8B=EC=95=84?= =?UTF-8?q?=EC=9A=94=20=EC=88=9C=EC=9C=BC=EB=A1=9C=20=EC=B1=84=ED=8C=85=20?= =?UTF-8?q?=EC=A0=95=EB=A0=AC=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.service.ts | 55 +++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/packages/backend/src/chat/chat.service.ts b/packages/backend/src/chat/chat.service.ts index 3b9ae9d3..95412659 100644 --- a/packages/backend/src/chat/chat.service.ts +++ b/packages/backend/src/chat/chat.service.ts @@ -29,6 +29,26 @@ export class ChatService { return await this.toScrollResponse(result, chatScrollQuery.pageSize); } + async scrollChatByLike(chatScrollQuery: ChatScrollQuery, userId?: number) { + this.validatePageSize(chatScrollQuery); + const result = await this.findChatScrollOrderByLike( + chatScrollQuery, + userId, + ); + return await this.toScrollResponse(result, chatScrollQuery.pageSize); + } + + async findChatScrollOrderByLike( + chatScrollQuery: ChatScrollQuery, + userId?: number, + ) { + const queryBuilder = await this.buildChatScrollByLikeQuery( + chatScrollQuery, + userId, + ); + return queryBuilder.getMany(); + } + private validatePageSize(chatScrollQuery: ChatScrollQuery) { const { pageSize } = chatScrollQuery; if (pageSize && pageSize > 100) { @@ -53,6 +73,41 @@ export class ChatService { return queryBuilder.getMany(); } + private async buildChatScrollByLikeQuery( + chatScrollQuery: ChatScrollQuery, + userId?: number, + ) { + const queryBuilder = this.dataSource.createQueryBuilder(Chat, 'chat'); + const { stockId, latestChatId, pageSize } = chatScrollQuery; + const size = pageSize ? pageSize : DEFAULT_PAGE_SIZE; + + queryBuilder + .leftJoinAndSelect('chat.likes', 'like', 'like.user_id = :userId', { + userId, + }) + .where('chat.stock_id = :stockId', { stockId }) + .orderBy('chat.likeCount', 'DESC') + .addOrderBy('chat.id', 'DESC') + .take(size + 1); + if (latestChatId) { + const chat = await this.dataSource.manager.findOne(Chat, { + where: { id: latestChatId }, + select: ['likeCount'], + }); + if (chat) { + queryBuilder.andWhere( + 'chat.likeCount < :likeCount or (chat.likeCount = :likeCount and chat.id < :latestChatId)', + { + likeCount: chat.likeCount, + latestChatId, + }, + ); + } + } + + return queryBuilder; + } + private buildChatScrollQuery( chatScrollQuery: ChatScrollQuery, userId?: number, From cdcaf84a5b20446a82972053b47fd374f9066dfd Mon Sep 17 00:00:00 2001 From: kimminsu Date: Wed, 20 Nov 2024 11:39:11 +0900 Subject: [PATCH 4/8] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=A2=8B=EC=95=84?= =?UTF-8?q?=EC=9A=94=20=EC=88=9C=20=EC=B1=84=ED=8C=85=20=EC=8A=A4=ED=81=AC?= =?UTF-8?q?=EB=A1=A4=20=EC=97=94=EB=93=9C=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 | 25 ++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/packages/backend/src/chat/chat.controller.ts b/packages/backend/src/chat/chat.controller.ts index 0670c9fa..35ce3a3b 100644 --- a/packages/backend/src/chat/chat.controller.ts +++ b/packages/backend/src/chat/chat.controller.ts @@ -64,4 +64,29 @@ export class ChatController { this.chatGateWay.broadcastLike(result); return result; } + + @ApiOperation({ + summary: '채팅 스크롤 조회 API(좋아요 순)', + description: '좋아요 순으로 채팅을 스크롤하여 조회한다.', + }) + @ApiOkResponse({ + description: '스크롤 조회 성공', + type: ChatScrollResponse, + }) + @ApiBadRequestResponse({ + description: '스크롤 크기 100 초과', + example: { + message: 'pageSize should be less than 100', + error: 'Bad Request', + statusCode: 400, + }, + }) + @Get('/like') + async findChatListByLike( + @Query() request: ChatScrollQuery, + @Req() req: Express.Request, + ) { + const user = req.user as User; + return await this.chatService.scrollChatByLike(request, user?.id); + } } From 8e27f1307ddbe1f951776ef7d6c8baba82ba0580 Mon Sep 17 00:00:00 2001 From: kimminsu Date: Wed, 20 Nov 2024 11:41:08 +0900 Subject: [PATCH 5/8] =?UTF-8?q?=E2=9C=A8=20feat:=20chat=20likeCount=20?= =?UTF-8?q?=EC=9D=B8=EB=8D=B1=EC=8A=A4=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/chat/domain/chat.entity.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/backend/src/chat/domain/chat.entity.ts b/packages/backend/src/chat/domain/chat.entity.ts index a1bab9ca..2a5ab380 100644 --- a/packages/backend/src/chat/domain/chat.entity.ts +++ b/packages/backend/src/chat/domain/chat.entity.ts @@ -1,6 +1,7 @@ import { Column, Entity, + Index, JoinColumn, ManyToOne, OneToMany, @@ -34,6 +35,7 @@ export class Chat { @Column({ type: 'enum', enum: ChatType, default: ChatType.NORMAL }) type: ChatType = ChatType.NORMAL; + @Index() @Column({ name: 'like_count', default: 0 }) likeCount: number = 0; From 0dd1b70f9b90a9b32bd3bb4313bcfc12e0f6af19 Mon Sep 17 00:00:00 2001 From: kimminsu Date: Wed, 20 Nov 2024 12:02:56 +0900 Subject: [PATCH 6/8] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20=EC=B1=84?= =?UTF-8?q?=ED=8C=85=20=EC=8A=A4=ED=81=AC=EB=A1=A4=20=EC=BF=BC=EB=A6=AC=20?= =?UTF-8?q?=EB=B9=8C=EB=8D=94=20=EC=A4=91=EB=B3=B5=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/chat/chat.service.ts | 67 +++++++++++++---------- 1 file changed, 37 insertions(+), 30 deletions(-) diff --git a/packages/backend/src/chat/chat.service.ts b/packages/backend/src/chat/chat.service.ts index 95412659..c9431a8d 100644 --- a/packages/backend/src/chat/chat.service.ts +++ b/packages/backend/src/chat/chat.service.ts @@ -1,5 +1,5 @@ import { BadRequestException, Injectable } from '@nestjs/common'; -import { DataSource } from 'typeorm'; +import { DataSource, SelectQueryBuilder } from 'typeorm'; import { Chat } from '@/chat/domain/chat.entity'; import { ChatScrollQuery } from '@/chat/dto/chat.request'; import { ChatScrollResponse } from '@/chat/dto/chat.response'; @@ -9,6 +9,13 @@ export interface ChatMessage { stockId: string; } +const ORDER = { + LIKE: 'like', + LATEST: 'latest', +} as const; + +export type Order = (typeof ORDER)[keyof typeof ORDER]; + const DEFAULT_PAGE_SIZE = 20; @Injectable() @@ -42,9 +49,10 @@ export class ChatService { chatScrollQuery: ChatScrollQuery, userId?: number, ) { - const queryBuilder = await this.buildChatScrollByLikeQuery( + const queryBuilder = await this.buildChatScrollQuery( chatScrollQuery, userId, + ORDER.LIKE, ); return queryBuilder.getMany(); } @@ -69,13 +77,17 @@ export class ChatService { chatScrollQuery: ChatScrollQuery, userId?: number, ) { - const queryBuilder = this.buildChatScrollQuery(chatScrollQuery, userId); + const queryBuilder = await this.buildChatScrollQuery( + chatScrollQuery, + userId, + ); return queryBuilder.getMany(); } - private async buildChatScrollByLikeQuery( + private async buildChatScrollQuery( chatScrollQuery: ChatScrollQuery, userId?: number, + order: Order = ORDER.LATEST, ) { const queryBuilder = this.dataSource.createQueryBuilder(Chat, 'chat'); const { stockId, latestChatId, pageSize } = chatScrollQuery; @@ -86,9 +98,26 @@ export class ChatService { userId, }) .where('chat.stock_id = :stockId', { stockId }) - .orderBy('chat.likeCount', 'DESC') - .addOrderBy('chat.id', 'DESC') .take(size + 1); + + if (order === ORDER.LIKE) { + return this.buildLikeCountQuery(queryBuilder, latestChatId); + } + queryBuilder.orderBy('chat.id', 'DESC'); + if (latestChatId) { + queryBuilder.andWhere('chat.id < :latestChatId', { latestChatId }); + } + + return queryBuilder; + } + + private async buildLikeCountQuery( + queryBuilder: SelectQueryBuilder, + latestChatId?: number, + ) { + queryBuilder + .orderBy('chat.likeCount', 'DESC') + .addOrderBy('chat.id', 'DESC'); if (latestChatId) { const chat = await this.dataSource.manager.findOne(Chat, { where: { id: latestChatId }, @@ -96,7 +125,8 @@ export class ChatService { }); if (chat) { queryBuilder.andWhere( - 'chat.likeCount < :likeCount or (chat.likeCount = :likeCount and chat.id < :latestChatId)', + 'chat.likeCount < :likeCount or' + + ' (chat.likeCount = :likeCount and chat.id < :latestChatId)', { likeCount: chat.likeCount, latestChatId, @@ -104,29 +134,6 @@ export class ChatService { ); } } - - return queryBuilder; - } - - private buildChatScrollQuery( - chatScrollQuery: ChatScrollQuery, - userId?: number, - ) { - const queryBuilder = this.dataSource.createQueryBuilder(Chat, 'chat'); - const { stockId, latestChatId, pageSize } = chatScrollQuery; - const size = pageSize ? pageSize : DEFAULT_PAGE_SIZE; - - queryBuilder - .leftJoinAndSelect('chat.likes', 'like', 'like.user_id = :userId', { - userId, - }) - .where('chat.stock_id = :stockId', { stockId }) - .orderBy('chat.id', 'DESC') - .take(size + 1); - if (latestChatId) { - queryBuilder.andWhere('chat.id < :latestChatId', { latestChatId }); - } - return queryBuilder; } } From 5e7e4b09c3c9974828f4093ef8d53e6d865d8341 Mon Sep 17 00:00:00 2001 From: kimminsu Date: Wed, 20 Nov 2024 12:04:21 +0900 Subject: [PATCH 7/8] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=A3=BC=EC=8B=9D=20?= =?UTF-8?q?=EC=86=8C=EC=9C=A0=20=ED=99=95=EC=9D=B8=20=EC=97=94=EB=93=9C?= =?UTF-8?q?=ED=8F=AC=EC=9D=B8=ED=8A=B8=EB=A5=BC=20=EC=BF=BC=EB=A6=AC?= =?UTF-8?q?=EB=A1=9C=20=EB=B0=9B=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/stock/stock.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/stock/stock.controller.ts b/packages/backend/src/stock/stock.controller.ts index 5aaf6707..6f9e1dd4 100644 --- a/packages/backend/src/stock/stock.controller.ts +++ b/packages/backend/src/stock/stock.controller.ts @@ -140,7 +140,7 @@ export class StockController { }) @Get('user/ownership') async checkOwnership( - @Body() body: UserStockRequest, + @Query() body: UserStockRequest, @Req() request: Request, ) { const user = request.user as User; From 364f833bb9d7b1d3e0130df91657d5869d79030e Mon Sep 17 00:00:00 2001 From: kimminsu Date: Wed, 20 Nov 2024 15:11:10 +0900 Subject: [PATCH 8/8] =?UTF-8?q?=F0=9F=92=84=20style:=20stock=20controller?= =?UTF-8?q?=20import=20=EC=88=9C=EC=84=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/stock/stock.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/stock/stock.controller.ts b/packages/backend/src/stock/stock.controller.ts index 6f9e1dd4..3f6ef023 100644 --- a/packages/backend/src/stock/stock.controller.ts +++ b/packages/backend/src/stock/stock.controller.ts @@ -32,6 +32,7 @@ import { StockDetailService } from './stockDetail.service'; import SessionGuard from '@/auth/session/session.guard'; import { GetUser } from '@/common/decorator/user.decorator'; import { sessionConfig } from '@/configs/session.config'; +import { StockSearchRequest } from '@/stock/dto/stock.request'; import { StockSearchResponse, StockViewsResponse, @@ -46,7 +47,6 @@ import { UserStockResponse, } from '@/stock/dto/userStock.response'; import { User } from '@/user/domain/user.entity'; -import { StockSearchRequest } from '@/stock/dto/stock.request'; @Controller('stock') export class StockController {