Skip to content

Commit d2ba2b0

Browse files
authored
Merge pull request #161 from umc-commit/feat/159-ci-test
[REFACTOR] chat api 수정
2 parents b26bc1a + 6899f5b commit d2ba2b0

File tree

7 files changed

+138
-79
lines changed

7 files changed

+138
-79
lines changed

.github/workflows/cd.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,4 +90,4 @@ jobs:
9090
echo "❌ Health check failed. Rolling back..."
9191
docker rm -f node-app-$TARGET_COLOR || true
9292
exit 1
93-
EOF
93+
EOF

public/client-test.html

Lines changed: 61 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -2,84 +2,97 @@
22
<html lang="en">
33
<head>
44
<meta charset="UTF-8" />
5-
<title>Socket.IO 채팅</title>
5+
<title>Socket.IO 채팅 (JWT 토큰 입력)</title>
66
<script src="https://cdn.socket.io/4.6.1/socket.io.min.js"></script>
77
</head>
88
<body>
9-
<h2>실시간 채팅</h2>
9+
<h2>실시간 채팅 (JWT 인증)</h2>
1010

1111
<div>
12-
<label for="senderIdInput">사용자 ID 입력:</label>
13-
<input type="text" id="senderIdInput" placeholder="사용자 ID를 입력하세요" />
14-
<button id="joinBtn">채팅방 입장</button>
12+
<label for="tokenInput">JWT 토큰 입력:</label>
13+
<input type="text" id="tokenInput" placeholder="JWT 토큰을 입력하세요" style="width: 400px;" />
14+
<button id="connectBtn">서버 연결 및 채팅방 입장</button>
1515
</div>
16-
17-
<div id="chatroom" style="display:none;">
16+
17+
<div id="chatroom" style="display:none; margin-top: 20px;">
1818
<div id="messages" style="border:1px solid #ccc; height:300px; overflow-y:scroll; margin-bottom:10px; padding:5px;"></div>
1919

2020
<input type="text" id="messageInput" placeholder="메시지 입력 (최대 255자)" maxlength="255" style="width:60%;" />
2121
<input type="file" id="imageInput" accept="image/*" />
2222
<button id="sendBtn">전송</button>
2323
</div>
24-
24+
2525
<script>
26-
// 서버 소켓 연결 (URL 바꾸기)
27-
const socket = io('http://localhost:3000');
26+
let socket = null;
2827
const chatroomId = '1'; // 실제 채팅방 아이디 동적으로 받아야 함
2928

30-
let senderId = null;
29+
const tokenInput = document.getElementById('tokenInput');
30+
const connectBtn = document.getElementById('connectBtn');
3131

32-
const joinBtn = document.getElementById('joinBtn');
33-
const senderIdInput = document.getElementById('senderIdInput');
3432
const chatroomDiv = document.getElementById('chatroom');
35-
3633
const messagesDiv = document.getElementById('messages');
3734
const messageInput = document.getElementById('messageInput');
3835
const imageInput = document.getElementById('imageInput');
3936
const sendBtn = document.getElementById('sendBtn');
4037

41-
joinBtn.addEventListener('click', () => {
42-
const inputVal = senderIdInput.value.trim();
43-
if (!inputVal) {
44-
alert('사용자 ID를 입력해주세요.');
38+
connectBtn.addEventListener('click', () => {
39+
const token = tokenInput.value.trim();
40+
if (!token) {
41+
alert('JWT 토큰을 입력해주세요.');
4542
return;
4643
}
47-
senderId = inputVal;
4844

49-
// 채팅방 입장
50-
socket.emit('join', chatroomId);
51-
52-
// UI 변경
53-
senderIdInput.disabled = true;
54-
joinBtn.disabled = true;
55-
chatroomDiv.style.display = 'block';
56-
});
57-
58-
// 메시지 받기
59-
socket.on('chat message', (msg) => {
60-
const div = document.createElement('div');
61-
div.style.borderBottom = '1px solid #eee';
62-
div.style.padding = '5px';
63-
64-
let content = `<b>사용자 ${msg.senderId}:</b> ${msg.content || ''}`;
65-
if (msg.imageUrl) {
66-
content += `<br><img src="${msg.imageUrl}" alt="이미지" style="max-width:200px; max-height:200px;" />`;
45+
// 기존 연결 있으면 끊기
46+
if (socket) {
47+
socket.disconnect();
6748
}
68-
div.innerHTML = content;
69-
70-
messagesDiv.appendChild(div);
71-
messagesDiv.scrollTop = messagesDiv.scrollHeight;
72-
});
7349

74-
// 에러 받기
75-
socket.on('error', (err) => {
76-
alert('에러: ' + err.message);
50+
// 소켓 연결 (토큰을 auth에 담아 보냄)
51+
socket = io('http://localhost:3000', {
52+
auth: {
53+
token: token,
54+
}
55+
});
56+
57+
socket.on('connect', () => {
58+
console.log('서버 연결됨, 채팅방 입장');
59+
socket.emit('join', chatroomId);
60+
61+
chatroomDiv.style.display = 'block';
62+
tokenInput.disabled = true;
63+
connectBtn.disabled = true;
64+
});
65+
66+
socket.on('chat message', (msg) => {
67+
const div = document.createElement('div');
68+
div.style.borderBottom = '1px solid #eee';
69+
div.style.padding = '5px';
70+
71+
let content = `<b>사용자 ${msg.senderId}:</b> ${msg.content || ''}`;
72+
if (msg.imageUrl) {
73+
content += `<br><img src="${msg.imageUrl}" alt="이미지" style="max-width:200px; max-height:200px;" />`;
74+
}
75+
div.innerHTML = content;
76+
77+
messagesDiv.appendChild(div);
78+
messagesDiv.scrollTop = messagesDiv.scrollHeight;
79+
});
80+
81+
socket.on('error', (err) => {
82+
alert('에러: ' + err.message);
83+
});
84+
85+
socket.on('disconnect', () => {
86+
alert('서버 연결이 끊겼습니다.');
87+
chatroomDiv.style.display = 'none';
88+
tokenInput.disabled = false;
89+
connectBtn.disabled = false;
90+
});
7791
});
7892

79-
// 전송 버튼 클릭
8093
sendBtn.addEventListener('click', () => {
81-
if (!senderId) {
82-
alert('먼저 사용자 ID를 입력하고 채팅방에 입장하세요.');
94+
if (!socket || !socket.connected) {
95+
alert('서버에 연결되어 있지 않습니다.');
8396
return;
8497
}
8598

@@ -102,7 +115,6 @@ <h2>실시간 채팅</h2>
102115

103116
socket.emit('chat message', {
104117
chatroomId,
105-
senderId,
106118
content,
107119
imageBase64: base64Image,
108120
});
@@ -111,7 +123,6 @@ <h2>실시간 채팅</h2>
111123
} else {
112124
socket.emit('chat message', {
113125
chatroomId,
114-
senderId,
115126
content,
116127
});
117128
}

src/chat/dto/chatroom.dto.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ export class ChatroomListResponseDto {
2121
this.artist_profile_image = room.artist.profileImage;
2222
this.request_id = room.request.id;
2323
this.request_title = room.request.commission.title;
24-
this.last_message = room.chatMessages[0]?.content || null;
24+
// 이미지가 있으면 이미지 URL, 없으면 텍스트 content
25+
const lastMsg = room.chatMessages[0];
26+
this.last_message = lastMsg?.imageUrl || lastMsg?.content || null;
2527
this.last_message_time = room.chatMessages[0]?.createdAt || null;
2628
this.has_unread = unreadCount;
2729
}

src/chat/repository/chat.repository.js

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -48,20 +48,27 @@ export const ChatRepository = {
4848
},
4949

5050
async markAsRead(accountId, messageId) {
51-
return await prisma.chatMessageRead.upsert({
51+
const existing = await prisma.chatMessageRead.findFirst({
5252
where: {
53-
messageId_accountId: {
54-
messageId: BigInt(messageId),
55-
accountId: BigInt(accountId),
56-
},
57-
},
58-
update: { read: true },
59-
create: {
6053
messageId: BigInt(messageId),
6154
accountId: BigInt(accountId),
62-
read: true,
6355
},
6456
});
57+
58+
if (existing) {
59+
return await prisma.chatMessageRead.update({
60+
where: { id: existing.id },
61+
data: { read: true },
62+
});
63+
} else {
64+
return await prisma.chatMessageRead.create({
65+
data: {
66+
messageId: BigInt(messageId),
67+
accountId: BigInt(accountId),
68+
read: true,
69+
},
70+
});
71+
}
6572
},
6673

6774
async isMessageRead(accountId, messageId) {

src/chat/repository/chatroom.repository.js

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ export const ChatroomRepository = {
2323
},
2424

2525
async findChatroomsByUser(consumerId) {
26-
return prisma.chatroom.findMany({
26+
// 1. 채팅방 기본 정보 + 마지막 메시지(내용, 생성시간, id) 조회
27+
const chatrooms = await prisma.chatroom.findMany({
2728
where: { consumerId },
2829
include: {
2930
artist: {
@@ -38,7 +39,7 @@ export const ChatroomRepository = {
3839
id: true,
3940
commission: {
4041
select: {
41-
title: true
42+
title: true,
4243
}
4344
}
4445
}
@@ -47,12 +48,46 @@ export const ChatroomRepository = {
4748
orderBy: { createdAt: "desc" },
4849
take: 1,
4950
select: {
51+
id: true,
5052
content: true,
51-
createdAt: true
53+
createdAt: true,
5254
}
5355
}
5456
}
5557
});
58+
59+
// 2. 마지막 메시지 ID 목록 수집
60+
const messageIds = chatrooms
61+
.map(room => room.chatMessages[0]?.id)
62+
.filter(Boolean); // null 제외
63+
64+
if (messageIds.length === 0) {
65+
return chatrooms;
66+
}
67+
68+
// 3. 메시지 ID로 이미지 URL 조회
69+
const images = await prisma.image.findMany({
70+
where: {
71+
target: "chat_messages",
72+
targetId: { in: messageIds },
73+
},
74+
});
75+
76+
// 4. 이미지 URL 매핑 (messageId -> imageUrl)
77+
const imageMap = {};
78+
images.forEach(img => {
79+
imageMap[img.targetId.toString()] = img.imageUrl;
80+
});
81+
82+
// 5. 채팅방 객체에 이미지 URL 병합
83+
chatrooms.forEach(room => {
84+
const msg = room.chatMessages[0];
85+
if (msg) {
86+
msg.imageUrl = imageMap[msg.id.toString()] || null;
87+
}
88+
});
89+
90+
return chatrooms;
5691
},
5792

5893
async softDeleteChatrooms(chatroomIds, userType, userId) {

src/chat/socket/socket.js

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ export default function setupSocket(server) {
3131

3232
// 채팅방 join
3333
socket.on("join", async (chatroomId) => {
34-
socket.join(chatroomId);
34+
const room = String(chatroomId);
35+
socket.join(room);
3536
console.log(`User ${socket.user.userId} joined chatroom ${chatroomId}`);
3637

3738
try {
@@ -70,7 +71,8 @@ export default function setupSocket(server) {
7071
});
7172

7273
// 메시지 수신
73-
socket.on("chat message", async ({ chatroomId, senderId, content, imageBase64 }) => {
74+
socket.on("chat message", async ({ chatroomId, content, imageBase64 }) => {
75+
const room = String(chatroomId);
7476
try {
7577
if (content && content.length > 255) {
7678
socket.emit("error", { message: "메시지 길이는 255자를 초과할 수 없습니다." });
@@ -81,19 +83,18 @@ export default function setupSocket(server) {
8183

8284
// 이미지 처리
8385
if (imageBase64) {
84-
const buffer = Buffer.from(imageBase64, 'base64');
85-
imageUrl = await uploadToS3({
86-
buffer,
87-
folderName: "messages",
88-
extension: "png"
89-
});
86+
const base64Data = imageBase64.replace(/^data:image\/\w+;base64,/, '');
87+
const buffer = Buffer.from(base64Data, 'base64');
88+
console.log("Buffer length:", buffer.length);
89+
console.log("Is Buffer:", Buffer.isBuffer(buffer));
90+
imageUrl = await uploadToS3(buffer, "messages", "png");
9091
}
9192

9293
// 메시지 저장
9394
const savedMessage = await prisma.chatMessage.create({
9495
data: {
9596
chatroomId: BigInt(chatroomId),
96-
senderId: BigInt(senderId),
97+
senderId: BigInt(socket.user.accountId),
9798
content,
9899
},
99100
});
@@ -119,7 +120,7 @@ export default function setupSocket(server) {
119120
createdAt: savedMessage.createdAt,
120121
}));
121122

122-
io.to(chatroomId).emit("chat message", safeMessage);
123+
io.to(room).emit("chat message", safeMessage);
123124

124125
} catch (err) {
125126
console.error("Socket message error:", err);

src/common/swagger/chat.json

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,17 @@
3434
"type": "object",
3535
"properties": {
3636
"resultType": { "type": "string", "example": "SUCCESS" },
37-
"error": { "type": "object", "nullable": true },
37+
"error": { "type": "object", "nullable": true, "example": null },
3838
"success": {
3939
"type": "object",
4040
"properties": {
41-
"id": { "type": "integer", "example": 10 },
42-
"consumerId": { "type": "integer", "example": 1 },
43-
"artistId": { "type": "integer", "example": 2 },
44-
"requestId": { "type": "integer", "example": 3 }
41+
"id": { "type": "string", "example": "1" },
42+
"consumerId": { "type": "string", "example": "1" },
43+
"artistId": { "type": "string", "example": "1" },
44+
"requestId": { "type": "string", "example": "1" },
45+
"hiddenArtist": { "type": "boolean", "example": false },
46+
"hiddenConsumer": { "type": "boolean", "example": false },
47+
"createdAt": { "type": "string", "format": "date-time", "example": "2025-08-07T15:35:42.375Z" }
4548
}
4649
}
4750
}

0 commit comments

Comments
 (0)