diff --git a/packages/backend/src/chat/chat.controller.ts b/packages/backend/src/chat/chat.controller.ts index d295f0c9..35ce3a3b 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) @@ -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); + } } 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..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() @@ -23,18 +30,33 @@ export class ChatService { }); } - async scrollFirstChat(chatScrollQuery: ChatScrollQuery, userId?: number) { + async scrollChat(chatScrollQuery: ChatScrollQuery, userId?: number) { this.validatePageSize(chatScrollQuery); - const result = await this.findFirstChatScroll(chatScrollQuery, userId); + const result = await this.findChatScroll(chatScrollQuery, userId); return await this.toScrollResponse(result, chatScrollQuery.pageSize); } - async scrollNextChat(chatScrollQuery: ChatScrollQuery, userId?: number) { + async scrollChatByLike(chatScrollQuery: ChatScrollQuery, userId?: number) { this.validatePageSize(chatScrollQuery); - const result = await this.findChatScroll(chatScrollQuery, userId); + const result = await this.findChatScrollOrderByLike( + chatScrollQuery, + userId, + ); return await this.toScrollResponse(result, chatScrollQuery.pageSize); } + async findChatScrollOrderByLike( + chatScrollQuery: ChatScrollQuery, + userId?: number, + ) { + const queryBuilder = await this.buildChatScrollQuery( + chatScrollQuery, + userId, + ORDER.LIKE, + ); + return queryBuilder.getMany(); + } + private validatePageSize(chatScrollQuery: ChatScrollQuery) { const { pageSize } = chatScrollQuery; if (pageSize && pageSize > 100) { @@ -55,51 +77,63 @@ export class ChatService { chatScrollQuery: ChatScrollQuery, userId?: number, ) { - if (!chatScrollQuery.latestChatId) { - return await this.findFirstChatScroll(chatScrollQuery, userId); - } else { - return await this.findNextChatScroll(chatScrollQuery); - } + const queryBuilder = await this.buildChatScrollQuery( + chatScrollQuery, + userId, + ); + return queryBuilder.getMany(); } - private async findFirstChatScroll( + private async buildChatScrollQuery( chatScrollQuery: ChatScrollQuery, userId?: number, + order: Order = ORDER.LATEST, ) { 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(); + .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 findNextChatScroll( - chatScrollQuery: ChatScrollQuery, - userId?: number, + private async buildLikeCountQuery( + queryBuilder: SelectQueryBuilder, + latestChatId?: number, ) { - const queryBuilder = this.dataSource.createQueryBuilder(Chat, 'chat'); - if (!chatScrollQuery.pageSize) { - chatScrollQuery.pageSize = DEFAULT_PAGE_SIZE; + queryBuilder + .orderBy('chat.likeCount', 'DESC') + .addOrderBy('chat.id', 'DESC'); + 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, + }, + ); + } } - 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; } } 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; diff --git a/packages/backend/src/stock/stock.controller.ts b/packages/backend/src/stock/stock.controller.ts index 5aaf6707..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 { @@ -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; 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 }); }),