Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/#104 - 좋아요 순 스크롤 기능 구현 #199

Merged
merged 8 commits into from
Nov 20, 2024
27 changes: 26 additions & 1 deletion packages/backend/src/chat/chat.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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);
}
}
2 changes: 1 addition & 1 deletion packages/backend/src/chat/chat.gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
5 changes: 2 additions & 3 deletions packages/backend/src/chat/chat.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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');
});
});
108 changes: 71 additions & 37 deletions packages/backend/src/chat/chat.service.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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()
Expand All @@ -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) {
Expand All @@ -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<Chat>,
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;
}
}
2 changes: 2 additions & 0 deletions packages/backend/src/chat/domain/chat.entity.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
Column,
Entity,
Index,
JoinColumn,
ManyToOne,
OneToMany,
Expand Down Expand Up @@ -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;

Expand Down
4 changes: 2 additions & 2 deletions packages/backend/src/stock/stock.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 {
Expand Down Expand Up @@ -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;
Expand Down
12 changes: 6 additions & 6 deletions packages/backend/src/user/user.service.spec.ts
Original file line number Diff line number Diff line change
@@ -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<EntityManager>,
Expand All @@ -15,7 +15,7 @@ export function createDataSourceMock(
};

return {
getRepository: managerMock.getRepository,
getRepository: managerMock?.getRepository,
transaction: jest.fn().mockImplementation(async (work) => {
return work({ ...defaultManagerMock, ...managerMock });
}),
Expand Down