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/#251 - 주식 컨트롤러 잘못된 경로 매핑 문제 해결 #252

Merged
merged 12 commits into from
Nov 26, 2024
Merged
61 changes: 44 additions & 17 deletions packages/backend/src/chat/chat.gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,34 +18,38 @@ import { WebsocketSessionService } from '@/auth/session/websocketSession.service
import { MEMORY_STORE } from '@/auth/session.module';
import { ChatService } from '@/chat/chat.service';
import { Chat } from '@/chat/domain/chat.entity';
import { ChatScrollQuery, isChatScrollQuery } from '@/chat/dto/chat.request';
import {
ChatMessage,
ChatScrollQuery,
isChatScrollQuery,
} from '@/chat/dto/chat.request';
import { LikeResponse } from '@/chat/dto/like.response';
import { MentionService } from '@/chat/mention.service';
import { WebSocketExceptionFilter } from '@/middlewares/filter/webSocketException.filter';
import { StockService } from '@/stock/stock.service';

interface chatMessage {
room: string;
content: string;
}
import { User } from '@/user/domain/user.entity';

interface chatResponse {
likeCount: number;
message: string;
type: string;
mentioned: boolean;
createdAt: Date;
}

@WebSocketGateway({ namespace: '/api/chat/realtime' })
@UseFilters(WebSocketExceptionFilter)
export class ChatGateway implements OnGatewayConnection {
@WebSocketServer()
server: Server;
websocketSessionService: WebsocketSessionService;
private server: Server;
private websocketSessionService: WebsocketSessionService;
private users = new Map<number, string>();

constructor(
@Inject('winston') private readonly logger: Logger,
private readonly stockService: StockService,
private readonly chatService: ChatService,
private readonly mentionService: MentionService,
@Inject(MEMORY_STORE) sessionStore: MemoryStore,
) {
this.websocketSessionService = new WebsocketSessionService(sessionStore);
Expand All @@ -54,10 +58,10 @@ export class ChatGateway implements OnGatewayConnection {
@UseGuards(WebSocketSessionGuard)
@SubscribeMessage('chat')
async handleConnectStock(
@MessageBody() message: chatMessage,
@MessageBody() message: ChatMessage,
@ConnectedSocket() client: SessionSocket,
) {
const { room, content } = message;
const { room, content, mention } = message;
if (!client.rooms.has(room)) {
client.emit('error', 'You are not in the room');
this.logger.warn(`client is not in the room ${room}`);
Expand All @@ -72,6 +76,17 @@ export class ChatGateway implements OnGatewayConnection {
stockId: room,
message: content,
});
if (mention) {
await this.mentionService.createMention(savedChat.id, mention);
const mentionedSocket = this.users.get(Number(mention));
if (mentionedSocket) {
const chatResponse = this.toResponse(savedChat);
this.server.to(room).except(mentionedSocket).emit('chat', chatResponse);
chatResponse.mentioned = true;
this.server.to(mentionedSocket).emit('chat', chatResponse);
return;
}
}
this.server.to(room).emit('chat', this.toResponse(savedChat));
}

Expand All @@ -86,15 +101,12 @@ export class ChatGateway implements OnGatewayConnection {
const { stockId, pageSize } = await this.getChatScrollQuery(client);
await this.validateExistStock(stockId);
client.join(stockId);
const messages = await this.chatService.scrollChat(
{
stockId,
pageSize,
},
user?.id,
);
const messages = await this.scrollChat(stockId, user, pageSize);
this.logger.info(`client joined room ${stockId}`);
client.emit('chat', messages);
if (user) {
this.users.set(user.id, client.id);
}
} catch (e) {
const error = e as Error;
this.logger.warn(error.message);
Expand All @@ -103,6 +115,20 @@ export class ChatGateway implements OnGatewayConnection {
}
}

private async scrollChat(
stockId: string,
user: User | null,
pageSize?: number,
) {
return await this.chatService.scrollChat(
{
stockId,
pageSize,
},
user?.id,
);
}

private async validateExistStock(stockId: string): Promise<void> {
if (!(await this.stockService.checkStockExist(stockId))) {
throw new Error(`Stock does not exist: ${stockId}`);
Expand All @@ -126,6 +152,7 @@ export class ChatGateway implements OnGatewayConnection {
likeCount: chat.likeCount,
message: chat.message,
type: chat.type,
mentioned: false,
createdAt: chat.date?.createdAt || new Date(),
};
}
Expand Down
10 changes: 8 additions & 2 deletions packages/backend/src/chat/chat.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,18 @@ 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 { Mention } from '@/chat/domain/mention.entity';
import { LikeService } from '@/chat/like.service';
import { MentionService } from '@/chat/mention.service';
import { StockModule } from '@/stock/stock.module';

@Module({
imports: [TypeOrmModule.forFeature([Chat, Like]), StockModule, SessionModule],
imports: [
TypeOrmModule.forFeature([Chat, Like, Mention]),
StockModule,
SessionModule,
],
controllers: [ChatController],
providers: [ChatGateway, ChatService, LikeService],
providers: [ChatGateway, ChatService, LikeService, MentionService],
})
export class ChatModule {}
49 changes: 37 additions & 12 deletions packages/backend/src/chat/chat.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,27 +102,41 @@ export class ChatService {
userId?: number,
order: Order = ORDER.LATEST,
) {
const queryBuilder = this.dataSource.createQueryBuilder(Chat, 'chat');
const { stockId, latestChatId, pageSize } = chatScrollQuery;
const size = pageSize ? pageSize : DEFAULT_PAGE_SIZE;
const queryBuilder = await this.buildInitialChatScrollQuery(
stockId,
size,
userId,
);
if (order === ORDER.LIKE) {
return this.buildLikeCountQuery(queryBuilder, latestChatId);
}
return this.buildLatestChatIdQuery(queryBuilder, latestChatId);
}

queryBuilder
private async buildInitialChatScrollQuery(
stockId: string,
size: number,
userId?: number,
) {
console.log('stockId', stockId);
return this.dataSource
.createQueryBuilder(Chat, 'chat')
.leftJoinAndSelect('chat.likes', 'like', 'like.user_id = :userId', {
userId,
})
.leftJoinAndSelect(
'chat.mentions',
'mention',
'mention.user_id = :userId',
{
userId,
},
)
.leftJoinAndSelect('chat.user', 'user')
.where('chat.stock_id = :stockId', { stockId })
.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(
Expand Down Expand Up @@ -150,4 +164,15 @@ export class ChatService {
}
return queryBuilder;
}

private async buildLatestChatIdQuery(
queryBuilder: SelectQueryBuilder<Chat>,
latestChatId?: number,
) {
queryBuilder.orderBy('chat.id', 'DESC');
if (latestChatId) {
queryBuilder.andWhere('chat.id < :latestChatId', { latestChatId });
}
return queryBuilder;
}
}
4 changes: 4 additions & 0 deletions packages/backend/src/chat/domain/chat.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ 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';
import { Mention } from '@/chat/domain/mention.entity';

@Entity()
export class Chat {
Expand Down Expand Up @@ -41,4 +42,7 @@ export class Chat {

@Column(() => DateEmbedded, { prefix: '' })
date: DateEmbedded;

@OneToMany(() => Mention, (mention) => mention.chat)
mentions: Mention[];
}
2 changes: 1 addition & 1 deletion packages/backend/src/chat/domain/like.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,6 @@ export class Like {
@JoinColumn({ name: 'user_id' })
user: User;

@CreateDateColumn({ name: 'created_at' })
@CreateDateColumn({ name: 'created_at', type: 'timestamp' })
createdAt: Date;
}
28 changes: 28 additions & 0 deletions packages/backend/src/chat/domain/mention.entity.ts
Original file line number Diff line number Diff line change
@@ -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'])
@Entity()
export class Mention {
@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', type: 'timestamp' })
createdAt: Date;
}
9 changes: 6 additions & 3 deletions packages/backend/src/chat/dto/chat.request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,20 @@ export function isChatScrollQuery(object: unknown): object is ChatScrollQuery {
if (typeof object !== 'object' || object === null) {
return false;
}

if (!('stockId' in object) || typeof object.stockId !== 'string') {
return false;
}

if (
'latestChatId' in object &&
!Number.isInteger(Number(object.latestChatId))
) {
return false;
}

return !('pageSize' in object && !Number.isInteger(Number(object.pageSize)));
}

export interface ChatMessage {
room: string;
content: string;
mention?: number;
}
2 changes: 2 additions & 0 deletions packages/backend/src/chat/dto/chat.response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ interface ChatResponse {
type: string;
liked: boolean;
nickname: string;
mentioned: boolean;
createdAt: Date;
}

Expand Down Expand Up @@ -43,6 +44,7 @@ export class ChatScrollResponse {
type: chat.type,
createdAt: chat.date!.createdAt,
liked: !!(chat.likes && chat.likes.length > 0),
mentioned: chat.mentions && chat.mentions.length > 0,
nickname: chat.user.nickname,
}));
this.hasMore = hasMore;
Expand Down
35 changes: 35 additions & 0 deletions packages/backend/src/chat/mention.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Injectable } from '@nestjs/common';
import { DataSource, EntityManager } from 'typeorm';
import { Chat } from '@/chat/domain/chat.entity';
import { Mention } from '@/chat/domain/mention.entity';
import { User } from '@/user/domain/user.entity';

@Injectable()
export class MentionService {
constructor(private readonly dataSource: DataSource) {}

async createMention(chatId: number, userId: number) {
return this.dataSource.transaction(async (manager) => {
if (!(await this.existsChatAndUser(chatId, userId, manager))) {
return null;
}
return await manager.save(Mention, {
chat: { id: chatId },
user: { id: userId },
});
});
}

async existsChatAndUser(
chatId: number,
userId: number,
manager: EntityManager,
) {
if (!(await manager.exists(User, { where: { id: userId } }))) {
return false;
}
return await manager.exists(Chat, {
where: { id: chatId },
});
}
}
Loading