Skip to content

Commit df641da

Browse files
authored
feat(be): chatting list (#208)
* feat: throw error for changing host certification by myself * feat: implements getting chats for infinite scroll
1 parent 2d0b7b6 commit df641da

File tree

6 files changed

+162
-5
lines changed

6 files changed

+162
-5
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { Controller, Get, Param, ParseIntPipe, Query } from '@nestjs/common';
2+
import { ApiTags } from '@nestjs/swagger';
3+
4+
import { ChatsService } from './chats.service';
5+
import { GetChatsSwagger } from './swagger/get-chats.swagger';
6+
7+
import { BaseDto } from '@common/base.dto';
8+
9+
@Controller('chats')
10+
@ApiTags('Chats')
11+
export class ChatsController {
12+
private readonly CHAT_FETCH_LIMIT = 20;
13+
14+
constructor(private readonly chatsService: ChatsService) {}
15+
16+
@Get(':chatId?')
17+
@GetChatsSwagger()
18+
async getChats(@Query() data: BaseDto, @Param('chatId', new ParseIntPipe({ optional: true })) chatId?: number) {
19+
const chats = await this.chatsService.getChatsForInfiniteScroll(data.sessionId, this.CHAT_FETCH_LIMIT, chatId);
20+
return { chats };
21+
}
22+
}

apps/server/src/chats/chats.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Module } from '@nestjs/common';
22

3+
import { ChatsController } from './chats.controller';
34
import { ChatsRepository } from './chats.repository';
45
import { ChatsService } from './chats.service';
56

@@ -9,5 +10,6 @@ import { PrismaModule } from '@prisma-alias/prisma.module';
910
imports: [PrismaModule],
1011
providers: [ChatsRepository, ChatsService],
1112
exports: [ChatsService, ChatsRepository],
13+
controllers: [ChatsController],
1214
})
1315
export class ChatsModule {}

apps/server/src/chats/chats.service.ts

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ import { Injectable } from '@nestjs/common';
22

33
import { ChatsRepository } from './chats.repository';
44

5+
import { DatabaseException } from '@common/exceptions/resource.exception';
6+
import { PrismaService } from '@prisma-alias/prisma.service';
7+
58
export interface ChatSaveDto {
69
sessionId: string;
710
token: string;
@@ -10,11 +13,48 @@ export interface ChatSaveDto {
1013

1114
@Injectable()
1215
export class ChatsService {
13-
constructor(private readonly chatsRepository: ChatsRepository) {}
16+
constructor(
17+
private readonly chatsRepository: ChatsRepository,
18+
private readonly prisma: PrismaService,
19+
) {}
1420

1521
async saveChat(data: ChatSaveDto) {
1622
const chat = await this.chatsRepository.save(data);
1723
const { chattingId, createUserTokenEntity, body: content } = chat;
1824
return { chattingId, nickname: createUserTokenEntity?.user?.nickname || '익명', content };
1925
}
26+
27+
async getChatsForInfiniteScroll(sessionId: string, count: number, chatId?: number) {
28+
try {
29+
const chats = await this.prisma.chatting.findMany({
30+
where: {
31+
sessionId,
32+
...(chatId && { chattingId: { lt: chatId } }),
33+
},
34+
include: {
35+
createUserTokenEntity: {
36+
include: {
37+
user: true,
38+
},
39+
},
40+
},
41+
orderBy: {
42+
chattingId: 'desc',
43+
},
44+
take: count,
45+
});
46+
return chats.map((x) => {
47+
const { createUserTokenEntity, chattingId, body: content } = x;
48+
const { user } = createUserTokenEntity;
49+
const nickname = user?.nickname || '익명';
50+
return {
51+
chattingId,
52+
nickname,
53+
content,
54+
};
55+
});
56+
} catch (error) {
57+
throw DatabaseException.read('chatting');
58+
}
59+
}
2060
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { applyDecorators } from '@nestjs/common';
2+
import { ApiOperation, ApiParam, ApiQuery, ApiResponse } from '@nestjs/swagger';
3+
4+
export const GetChatsSwagger = () =>
5+
applyDecorators(
6+
ApiOperation({
7+
summary: 'Chatting list 조회 API',
8+
description: `
9+
채팅 목록을 가져오는 API입니다. Infinite scroll 구현을 위한 페이지네이션을 지원합니다.
10+
11+
- chatId 파라미터가 없는 경우: 최신 채팅 20개를 반환
12+
- chatId 파라미터가 있는 경우: 해당 chatId보다 이전의 채팅 20개를 반환 (chatId보다 작은 ID를 가진 채팅)`,
13+
}),
14+
ApiParam({
15+
name: 'chatId',
16+
required: false,
17+
description: '기준이 되는 채팅 ID. 이 ID보다 이전의 채팅들을 조회합니다.',
18+
type: 'number',
19+
example: 3592,
20+
}),
21+
ApiQuery({
22+
name: 'sessionId',
23+
required: true,
24+
description: '채팅 세션 ID',
25+
type: 'string',
26+
}),
27+
ApiResponse({
28+
status: 200,
29+
description: '채팅 목록 조회 성공',
30+
schema: {
31+
example: {
32+
chats: [
33+
{
34+
chattingId: 3593,
35+
nickname: '익명',
36+
content: 'gggg',
37+
},
38+
{
39+
chattingId: 3592,
40+
nickname: 'jiho',
41+
content: 'asdfdasd',
42+
},
43+
{
44+
chattingId: 3591,
45+
nickname: '익명',
46+
content: 'd',
47+
},
48+
],
49+
},
50+
properties: {
51+
chats: {
52+
type: 'array',
53+
description: '채팅 목록 (최대 20개)',
54+
items: {
55+
type: 'object',
56+
properties: {
57+
chattingId: {
58+
type: 'number',
59+
description: '채팅 고유 ID (내림차순 정렬)',
60+
},
61+
nickname: {
62+
type: 'string',
63+
description: '사용자 닉네임',
64+
},
65+
content: {
66+
type: 'string',
67+
description: '채팅 내용',
68+
},
69+
},
70+
},
71+
},
72+
},
73+
},
74+
}),
75+
ApiResponse({
76+
status: 400,
77+
description: '잘못된 요청 (chatId가 숫자가 아닌 경우)',
78+
schema: {
79+
example: {
80+
messages: ['Validation failed (numeric string is expected)'],
81+
},
82+
},
83+
}),
84+
);

apps/server/src/questions/questions.service.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,13 @@ export class QuestionsService {
3939

4040
async getQuestionsBySession(data: GetQuestionDto) {
4141
const { sessionId, token } = data;
42-
const questions = await this.questionRepository.findQuestionsWithDetails(sessionId);
43-
const session = await this.sessionRepository.findById(sessionId);
42+
const [questions, session, sessionHostTokens] = await Promise.all([
43+
this.questionRepository.findQuestionsWithDetails(sessionId),
44+
this.sessionRepository.findById(sessionId),
45+
this.sessionAuthRepository.findHostTokensInSession(sessionId),
46+
]);
47+
4448
const expired = session.expiredAt < new Date();
45-
const sessionHostTokens = await this.sessionAuthRepository.findHostTokensInSession(sessionId);
4649
const isHost = sessionHostTokens.some(({ token: hostToken }) => hostToken === token);
4750
const mapLikesAndOwnership = <
4851
T extends {

apps/server/src/sessions-auth/sessions-auth.service.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ForbiddenException, Injectable, NotFoundException } from '@nestjs/common';
1+
import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from '@nestjs/common';
22

33
import { SessionAuthDto } from './dto/session-auth.dto';
44
import { UpdateHostDto } from './dto/update-host.dto';
@@ -39,9 +39,15 @@ export class SessionsAuthService {
3939
if (!(await this.validateSuperHost(sessionId, token)))
4040
throw new ForbiddenException('세션 생성자만이 호스트 권한을 부여 할 수 있습니다.');
4141
const targetToken = await this.sessionsAuthRepository.findTokenByUserId(userId, sessionId);
42+
4243
if (!targetToken) {
4344
throw new NotFoundException('존재하지 않는 userId입니다.');
4445
}
46+
47+
if (token === targetToken) {
48+
throw new BadRequestException('자신의 권한을 변경하려는 요청은 허용되지 않습니다.');
49+
}
50+
4551
const { user, isHost: updatedIsHost } = await this.sessionsAuthRepository.updateIsHost(targetToken, isHost);
4652
return { userId: user.userId, nickname: user.nickname, isHost: updatedIsHost };
4753
}

0 commit comments

Comments
 (0)