Skip to content

Commit bbc1659

Browse files
committed
게시글 리스트 조회 기능 수정
2 parents ee13189 + 3896bf7 commit bbc1659

25 files changed

+736
-27
lines changed

package.json

+3
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,12 @@
2525
"@nestjs/jwt": "^10.2.0",
2626
"@nestjs/passport": "^10.0.3",
2727
"@nestjs/platform-express": "^10.3.2",
28+
"@nestjs/platform-socket.io": "^10.4.6",
2829
"@nestjs/swagger": "^7.4.2",
2930
"@nestjs/typeorm": "^10.0.2",
31+
"@nestjs/websockets": "^10.4.6",
3032
"@types/passport-kakao": "^1.0.3",
33+
"@types/socket.io": "^3.0.2",
3134
"class-transformer": "^0.5.1",
3235
"class-validator": "^0.14.1",
3336
"dayjs": "^1.11.13",

src/auth/auth.service.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export class AuthService {
4242
payload: JwtPayload,
4343
): Promise<JwtPayload | undefined> {
4444
return await this.userSerivce.findByFields({
45-
where: { id: payload.id },
45+
where: { id: payload.id, status: 'activated' },
4646
});
4747
}
4848
}

src/chat-message/chat-message.service.ts

+41
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,47 @@ export class ChatMessageService {
1212
private readonly chatMessageRepository: Repository<ChatMessage>,
1313
) {}
1414

15+
async saveMessage(
16+
chatRoomId: number,
17+
toUserId: number,
18+
message: string,
19+
fromUserId: number,
20+
createdAt: string,
21+
) {
22+
return await this.chatMessageRepository.save({
23+
chatRoomId,
24+
toUserId,
25+
message,
26+
fromUserId,
27+
createdAt,
28+
});
29+
}
30+
31+
async getMessagesByChatRoomId(chatRoomId: number) {
32+
const messages = await this.chatMessageRepository.find({
33+
where: { chatRoom: { id: chatRoomId }, status: 'activated' },
34+
order: { createdAt: 'ASC' }, // 오래된 메시지부터 최신 메시지 순으로 정렬
35+
relations: ['fromUser', 'toUser'], // 메시지 발신자, 수신자 정보도 함께 로드
36+
});
37+
38+
return messages.map((message) => ({
39+
id: message.id,
40+
content: message.content,
41+
fromUser: {
42+
id: message.fromUser.id,
43+
nickname: message.fromUser.nickname,
44+
profileUrl: message.fromUser.profilePictureUrl,
45+
},
46+
toUser: {
47+
id: message.toUser.id,
48+
nickname: message.toUser.nickname,
49+
profileUrl: message.toUser.profilePictureUrl,
50+
},
51+
createdAt: message.createdAt,
52+
toUserReadAt: message.toUserReadAt,
53+
}));
54+
}
55+
1556
async createChatMessage(
1657
queryRunner: QueryRunner,
1758
chatRoom: ChatRoom,

src/chat-room/chat-room.controller.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { ChatRoomService } from './chat-room.service';
33
import { GetChatRoomsSwagger, LeaveChatRoomSwagger } from './chat-room.swagger';
44
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
55

6-
@ApiBearerAuth()
6+
@ApiBearerAuth('Authorization')
77
@Controller('chat-room')
88
@ApiTags('[서비스] 채팅방')
99
export class ChatRoomController {

src/chat-room/chat-room.service.ts

+39
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,45 @@ export class ChatRoomService {
1212
private readonly chatRoomRepository: Repository<ChatRoom>,
1313
) {}
1414

15+
async getChatRoomsWithLatestMessage(userId: number) {
16+
const chatRooms = await this.chatRoomRepository
17+
.createQueryBuilder('chatRoom')
18+
.leftJoinAndSelect('chatRoom.fromUser', 'fromUser')
19+
.leftJoinAndSelect('chatRoom.toUser', 'toUser')
20+
.leftJoinAndSelect('chatRoom.chatMessages', 'chatMessages')
21+
.loadRelationIdAndMap('chatRoom.latestMessage', 'chatMessages')
22+
.where('chatRoom.fromUserId = :userId OR chatRoom.toUserId = :userId', {
23+
userId,
24+
})
25+
.andWhere('chatRoom.status = :status', { status: 'activated' })
26+
.orderBy('chatMessages.createdAt', 'DESC') // 최신 메시지 우선 정렬
27+
.getMany();
28+
29+
return chatRooms.map((chatRoom) => {
30+
// 상대방 정보 설정
31+
const otherUser =
32+
chatRoom.fromUser.id === userId ? chatRoom.toUser : chatRoom.fromUser;
33+
34+
// 최신 메시지 설정
35+
const latestMessage = chatRoom.chatMessages[0];
36+
37+
return {
38+
chatRoomId: chatRoom.id,
39+
otherUser: {
40+
id: otherUser.id,
41+
nickname: otherUser.nickname,
42+
profileUrl: otherUser.profilePictureUrl,
43+
},
44+
latestMessage: latestMessage
45+
? {
46+
content: latestMessage.content,
47+
createdAt: latestMessage.createdAt,
48+
}
49+
: null,
50+
};
51+
});
52+
}
53+
1554
async createChatRoom(
1655
queryRunner: QueryRunner,
1756
matching: Matching,

src/common/entities/user.entity.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Entity, OneToMany, Column } from 'typeorm';
1+
import { Entity, OneToMany, Column, OneToOne } from 'typeorm';
22
import { BaseEntity } from './base.entity';
33
import { Post } from './post.entity';
44
import { PostComment } from './post-comment.entity';
@@ -83,5 +83,6 @@ export class User extends BaseEntity {
8383
postReports!: PostReport[];
8484

8585
// 대표 게시물 필드 추가
86+
@OneToOne(() => Post, (post) => post.user)
8687
representativePost?: Post | null;
8788
}

src/eventGateway.ts

+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import {
2+
SubscribeMessage,
3+
WebSocketGateway,
4+
WebSocketServer,
5+
OnGatewayDisconnect,
6+
OnGatewayConnection,
7+
} from '@nestjs/websockets';
8+
import { Server, Socket } from 'socket.io';
9+
import { ChatRoomService } from './chat-room/chat-room.service';
10+
import { ChatMessageService } from './chat-message/chat-message.service';
11+
12+
//클라이언트의 패킷들이 게이트웨이를 통해서 들어오게 됩니다.
13+
@WebSocketGateway({ namespace: '/chatting', cors: { origin: '*' } })
14+
export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect {
15+
// 소켓 서버를 정의합니다.
16+
@WebSocketServer()
17+
server: Server;
18+
// 유저 정보를 레디스에 적재시키기 위해서 레디스 설정이 선행되야합니다.
19+
constructor(
20+
private readonly chatRoomService: ChatRoomService,
21+
private readonly chatMessageService: ChatMessageService,
22+
) {}
23+
24+
afterInit(server: Server) {
25+
console.log('Initialized', server.engine.clientsCount);
26+
}
27+
28+
handleConnection(client: Socket) {
29+
console.log(`Client connected: ${client.id}`);
30+
}
31+
32+
handleDisconnect(client: Socket) {
33+
console.log(`Client disconnected: ${client.id}`);
34+
}
35+
36+
@SubscribeMessage('getChatRooms')
37+
async handleGetChatRooms(client: Socket, userId: number) {
38+
const chatRooms =
39+
await this.chatRoomService.getChatRoomsWithLatestMessage(userId);
40+
client.emit('chatRoomList', chatRooms);
41+
}
42+
43+
@SubscribeMessage('sendMessage')
44+
async handleSendMessage(
45+
client: Socket,
46+
payload: {
47+
chatRoomId: number;
48+
toUserId: number;
49+
message: string;
50+
fromUserId: number;
51+
createdAt: string;
52+
},
53+
) {
54+
const { chatRoomId, toUserId, message, fromUserId, createdAt } = payload;
55+
56+
// 메시지 저장 로직
57+
const newMessage = await this.chatMessageService.saveMessage(
58+
chatRoomId,
59+
toUserId,
60+
message,
61+
fromUserId,
62+
createdAt,
63+
);
64+
65+
// 채팅방 리스트 업데이트
66+
const chatRooms =
67+
await this.chatRoomService.getChatRoomsWithLatestMessage(fromUserId);
68+
client.emit('chatRoomList', chatRooms);
69+
70+
// 해당 채팅방에 있는 모든 사용자에게 메시지 전송
71+
this.server.to(String(chatRoomId)).emit('newMessage', newMessage);
72+
}
73+
74+
@SubscribeMessage('getChatRoomMessages')
75+
async handleGetChatRoomMessages(client: Socket, chatRoomId: number) {
76+
const messages =
77+
await this.chatMessageService.getMessagesByChatRoomId(chatRoomId);
78+
client.emit('chatRoomMessages', messages); // 클라이언트에 메시지 리스트 전송
79+
}
80+
81+
@SubscribeMessage('joinChatRoom')
82+
handleJoinChatRoom(client: Socket, chatRoomId: number) {
83+
client.join(String(chatRoomId));
84+
}
85+
}

src/main.ts

+2
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ import { AppModule } from './app.module';
33
import { setupSwagger } from './utils/swagger';
44
import { ServiceExceptionToHttpExceptionFilter } from './common/exception-filter';
55
import { ValidationPipe } from '@nestjs/common';
6+
import { IoAdapter } from '@nestjs/platform-socket.io';
67

78
async function bootstrap() {
89
const app = await NestFactory.create(AppModule);
910
app.useGlobalFilters(new ServiceExceptionToHttpExceptionFilter());
1011
app.useGlobalPipes(new ValidationPipe());
12+
app.useWebSocketAdapter(new IoAdapter(app));
1113
setupSwagger(app);
1214
await app.listen(process.env.PORT);
1315
console.log(`Application is running on: ${await app.getUrl()}`);
+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { ApiProperty } from '@nestjs/swagger';
2+
import { IsInt, IsString, IsIn } from 'class-validator';
3+
4+
export class PatchMatchingRequestDto {
5+
@ApiProperty({ example: 1, description: '매칭 ID' })
6+
@IsInt()
7+
matchingId: number;
8+
9+
@ApiProperty({ example: 1, description: '신청한 유저 아이디' })
10+
@IsInt()
11+
requesterId: number;
12+
13+
@ApiProperty({ example: 2, description: '매칭 상대 유저 아이디' })
14+
@IsInt()
15+
targetId: number;
16+
17+
@ApiProperty({
18+
example: 'accept',
19+
enum: ['accept', 'reject'],
20+
description: '수락 또는 거절',
21+
})
22+
@IsString()
23+
@IsIn(['accept', 'reject'])
24+
action: 'accept' | 'reject';
25+
}
+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { Type } from 'class-transformer';
2+
import { ApiProperty } from '@nestjs/swagger';
3+
4+
class RequesterResponse {
5+
@ApiProperty({
6+
description: '매칭 요청자의 ID',
7+
example: '19',
8+
})
9+
requesterId: number;
10+
11+
@ApiProperty({
12+
description: '매칭 요청자의 닉네임',
13+
example: '1d1d1d',
14+
})
15+
nickname: string;
16+
17+
@ApiProperty({
18+
description: '매칭 요청자의 프로필 이미지 url',
19+
example: 'https://example.com/image1.jpg',
20+
})
21+
profilePictureUrl: string;
22+
}
23+
24+
class RequesterPostResponse {
25+
@ApiProperty({
26+
description: '매칭 요청자의 게시물 이미지 목록',
27+
type: [String],
28+
example: [
29+
{ url: 'https://example.com/image1.jpg', orderNum: 1 },
30+
{ url: 'https://example.com/image2.jpg', orderNum: 2 },
31+
],
32+
})
33+
postImages: { url: string; orderNum: number }[];
34+
35+
@ApiProperty({
36+
description: '매칭 요청자의 게시물 스타일 태그 목록',
37+
type: [String],
38+
example: ['classic', 'basic'],
39+
})
40+
styleTags: string[];
41+
}
42+
43+
class MatchingResponse {
44+
@ApiProperty({
45+
description: '매칭 요청자 정보',
46+
type: RequesterResponse,
47+
})
48+
@Type(() => RequesterResponse)
49+
requester: RequesterResponse;
50+
51+
@ApiProperty({
52+
description: '매칭 요청자의 대표 게시물이 없을 경우, 가장 최근 게시물 정보',
53+
type: RequesterPostResponse,
54+
})
55+
@Type(() => RequesterPostResponse)
56+
requesterPost: RequesterPostResponse;
57+
}
58+
59+
export class GetMatchingsResponse {
60+
@ApiProperty({
61+
description: '매칭 존재 여부',
62+
example: true,
63+
})
64+
isMatching: boolean;
65+
66+
@ApiProperty({
67+
description: '받은 매칭 수',
68+
example: 10,
69+
})
70+
matchingCount: number;
71+
72+
@ApiProperty({
73+
description: '매칭 정보',
74+
type: [MatchingResponse],
75+
})
76+
@Type(() => MatchingResponse)
77+
matching: MatchingResponse[];
78+
}

0 commit comments

Comments
 (0)