Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision

Target

Select target project
  • websystem1/webback
1 result
Select Git revision
Show changes
Commits on Source (27)
......@@ -5,4 +5,6 @@ resources/
app.js
output.log
weblog.log
start.sh
start2.sh
......@@ -13,7 +13,12 @@ exports.createChatRoom = async (params) => {
// 채팅방 목록 조회
exports.getChatRooms = async (req, res) => {
try {
const roomData = await chatService.getChatRooms();
const name = req.user.name; // Google 로그인에서 가져온 email
console.log("name", name);
// 본인이 참가자로 포함된 채팅방만 가져오기
const roomData = await chatService.getChatRooms(name);
res.json(roomData);
} catch (err) {
console.error('Error fetching rooms:', err);
......
......@@ -12,7 +12,7 @@ class friendController {
async sendFriendRequest(req, res) {
try {
return await performanceMonitor.measureAsync('sendFriendRequest', async () => {
const email = req.body;
const { email } = req.body;
const userId = req.user.id;
if (!userId || !email) {
......@@ -97,9 +97,29 @@ class friendController {
async acceptRequest(req, res) {
try {
return await performanceMonitor.measureAsync('acceptFriendRequest', async () => {
if (!req.user || !req.user.id) {
return res.status(401).json({
success: false,
error: {
message: '인증되지 않은 사용자입니다.',
code: 'UNAUTHORIZED'
}
});
}
const userId = req.user.id;
const { friendId } = req.params;
const friendId = parseInt(req.params.friendId, 10);
if (!friendId || isNaN(friendId)) {
return res.status(400).json({
success: false,
error: {
message: '유효하지 않은 친구 ID입니다.',
code: 'INVALID_FRIEND_ID'
}
});
}
const result = await FriendService.acceptFriendRequest(userId, friendId);
return res.status(200).json({
success: true,
......@@ -107,6 +127,7 @@ class friendController {
});
});
} catch (error) {
console.error('Friend request accept error:', error);
return res.status(400).json({
success: false,
error: {
......@@ -162,7 +183,9 @@ class friendController {
return res.status(200).json({
success: true,
data: friends
data: {
...friends
}
});
});
} catch (error) {
......
......@@ -108,8 +108,9 @@ class MeetingController {
*/
async getMeetingDetail(req, res) {
const { meetingId } = req.params;
const userId=req.user.id;
try {
const meetingDetail = await MeetingService.getMeetingDetail(meetingId);
const meetingDetail = await MeetingService.getMeetingDetail(meetingId,userId);
res.status(200).json(meetingDetail);
} catch (err) {
console.error('모임 상세 조회 오류:', err);
......@@ -165,6 +166,24 @@ class MeetingController {
}
}
/**
* 번개 모임 삭제
* DELETE /api/meeting/:meetingId
*/
// controllers/meetingController.js
async deleteMeeting(req, res) {
const { meetingId } = req.params;
const userId = req.user.id;
try {
await MeetingService.deleteMeeting(meetingId, userId);
res.status(200).json({ message: '모임이 삭제되었습니다.' });
} catch (err) {
console.error('모임 삭제 오류:', err);
res.status(500).json({ error: err.message || '모임 삭제 실패' });
}
}
}
module.exports = new MeetingController();
......@@ -22,7 +22,7 @@ class CreateMeetingRequestDTO {
time_idx_start: Joi.number().integer().min(0).required(),
time_idx_end: Joi.number().integer().greater(Joi.ref('time_idx_start')).required(),
location: Joi.string().allow('', null).optional(),
time_idx_deadline: Joi.number().integer().min(0).less(Joi.ref('time_idx_start')).optional(),
time_idx_deadline: Joi.number().integer().min(0).max(Joi.ref('time_idx_start')).optional(),
type: Joi.string().valid('OPEN', 'CLOSE').required(),
created_by: Joi.number().integer().positive().required(),
});
......
......@@ -13,8 +13,8 @@ class MeetingDetailResponseDTO {
this.isScheduleConflict = isScheduleConflict;
this.participants = meeting.participants.map(participant => ({
userId: participant.user_id,
name: participant.participantUser ? participant.participantUser.name : 'Unknown',
email: participant.participantUser ? participant.participantUser.email : 'Unknown'
name: participant.user ? participant.user.name : 'Unknown',
email: participant.user ? participant.user.email : 'Unknown'
}));
}
}
......
......@@ -78,6 +78,7 @@ User.hasMany(Schedule, {
onDelete: 'CASCADE',
});
// Invite 관계 설정
Invite.belongsTo(Meeting, {
foreignKey: 'meeting_id',
......@@ -109,7 +110,17 @@ Meeting.hasMany(Invite, {
as: 'invites',
onDelete: 'CASCADE',
});
FcmToken.belongsTo(User,{
foreignKey:'userId',
as:'user',
onDelete:'CASCADE',
});
User.hasMany(FcmToken,
{
foreignKey:'userId',
as:'fcmTokenList',
onDelete:'CASCADE',
});
module.exports = {
sequelize,
......@@ -119,5 +130,6 @@ module.exports = {
Meeting,
MeetingParticipant,
Friend,
Invite,
FcmToken,
};
......@@ -24,6 +24,7 @@ const Meeting = sequelize.define('Meeting', {
},
time_idx_deadline: {
type: DataTypes.INTEGER,
defaultValue:671,
},
type: {
type: DataTypes.ENUM('OPEN', 'CLOSE'),
......@@ -37,7 +38,7 @@ const Meeting = sequelize.define('Meeting', {
max_num: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 10, // 기본값 설정 (필요에 따라 조정)
defaultValue: 20, // 기본값 설정 (필요에 따라 조정)
},
cur_num: {
type: DataTypes.INTEGER,
......
......@@ -42,7 +42,7 @@ async function startPushServer() {
body: `${sender}: ${messageContent}`,
},
data: {
click_action: `http://localhost:3000/chat/chatRoom/${chatRoomId}`, // 클릭 시 이동할 URL
click_action: `${process.env.FRONT_URL}/mypage`, // 클릭 시 이동할 URL
},
android: { priority: 'high' },
apns: { payload: { aps: { sound: 'default' } } },
......@@ -71,24 +71,42 @@ async function startPushServer() {
channel.consume(meetingQueue, async (msg) => {
if (msg !== null) {
const event = JSON.parse(msg.content.toString());
const { meetingTitle, inviterName, inviteeTokens } = event;
const { meetingTitle, inviterName, inviteeTokens, type } = event;
console.log('Meeting 푸시 알림 요청 수신:', event);
console.log("푸시 알림 보내는 fcmToken", inviteeTokens);
if (inviteeTokens.length > 0) {
const message = {
tokens: inviteeTokens,
notification: {
title: '번개 모임 초대',
body: `${inviterName}님이 ${meetingTitle} 모임에 초대했습니다.`,
},
data: {
click_action: `http://localhost:3000`, // 클릭 시 이동할 URL
},
android: { priority: 'high' },
apns: { payload: { aps: { sound: 'default' } } },
};
let message;
// 이벤트 타입에 따라 알림 내용 구성
if (type === 'invite') {
message = {
tokens: inviteeTokens,
notification: {
title: '번개 모임 초대',
body: `${inviterName}님이 ${meetingTitle} 번개모임에 초대했습니다.`,
},
data: {
click_action: `${process.env.FRONT_URL}/meeting`, // 클릭 시 이동할 URL
},
android: { priority: 'high' },
apns: { payload: { aps: { sound: 'default' } } },
};
} else if (type === 'join') {
message = {
tokens: inviteeTokens,
notification: {
title: `${meetingTitle}`,
body: `${inviterName}님이 ${meetingTitle} 모임에 참가했습니다.`,
},
data: {
click_action: `${process.env.FRONT_URL}/meeting`, // 클릭 시 이동할 URL
},
android: { priority: 'high' },
apns: { payload: { aps: { sound: 'default' } } },
};
}
try {
const response = await admin.messaging().sendEachForMulticast(message);
......
......@@ -4,7 +4,6 @@ const chatController = require('../controllers/chatController');
const { isLoggedIn } = require('../middlewares/auth');
router.post('/create-room', chatController.createChatRoom);
router.get('/rooms', chatController.getChatRooms);
router.post('/update-status', chatController.updateStatus);
router.post('/update-read-status', chatController.updateReadStatus);
router.get('/unread-messages/:nickname', chatController.getUnreadMessages);
......@@ -13,6 +12,7 @@ router.post('/update-status-and-logid', chatController.updateStatusAndLogId);
router.post('/update-read-log-id', chatController.updateReadLogId);
router.use(isLoggedIn);
router.get('/rooms', chatController.getChatRooms);
router.post('/:chatRoomId/notices', chatController.addNotice);
router.get('/:chatRoomId/notices/latest', chatController.getLatestNotice);
......
......@@ -28,6 +28,7 @@ router.get('/:meetingId', MeetingController.getMeetingDetail);
// 번개 모임 탈퇴
router.delete('/:meetingId/leave', MeetingController.leaveMeeting);
// 번개 모임 삭제
router.delete('/:meetingId', MeetingController.deleteMeeting);
module.exports = router;
\ No newline at end of file
......@@ -30,15 +30,18 @@ class ChatService {
}
// 채팅방 목록 조회
async getChatRooms() {
const rooms = await ChatRooms.find({}, { chatRoomId: 1, chatRoomName: 1, messages: { $slice: -1 } });
async getChatRooms(name) {
const rooms = await ChatRooms.find(
{ "participants.name": name },
{ chatRoomId: 1, chatRoomName: 1, messages: { $slice: -1 } }
);
return rooms.map(room => {
const lastMessage = room.messages[0] || {};
return {
chatRoomId: room.chatRoomId,
chatRoomName: room.chatRoomName,
lastMessage: {
sender: lastMessage.sender || '없음',
sender: lastMessage.sender || '알림',
message: lastMessage.message || '메시지 없음',
timestamp: lastMessage.timestamp || null,
},
......
......@@ -39,13 +39,22 @@ class MeetingService {
return totalIdx;
}
async sendMeetingPushNotificationRequest(meetingTitle, inviterName, inviteeTokens) {
// async sendMeetingPushNotificationRequest(meetingTitle, inviterName, inviteeTokens) {
// const event = {
// meetingTitle,
// inviterName,
// inviteeTokens,
// };
// await this.publishToQueue('meeting_push_notifications', event); // meeting_push_notifications 큐에 메시지 발행
// }
async sendMeetingPushNotificationRequest(meetingTitle, inviterName, inviteeTokens, type) {
const event = {
meetingTitle,
inviterName,
inviteeTokens,
type, // 이벤트 타입 ('invite' 또는 'join')
};
await this.publishToQueue('meeting_push_notifications', event); // meeting_push_notifications 큐에 메시지 발행
await this.publishToQueue('meeting_push_notifications', event); // 큐에 메시지 발행
}
......@@ -67,7 +76,7 @@ class MeetingService {
// 사용자와 FCM 토큰 조회
const user = await this._findUserWithFcmTokens(created_by);
console.log("user", user);
console.log("user", user);
const userFcmTokens = user.fcmTokenList.map((fcmToken) => fcmToken.token);
......@@ -119,8 +128,8 @@ class MeetingService {
);
const time_indices = Array.from(
{ length: time_idx_end - time_idx_start + 1 },
(_, i) => time_idx_start + i
{ length: parseInt(time_idx_end) - parseInt(time_idx_start) + 1 },
(_, i) => (parseInt(time_idx_start) + i).toString()
);
await ScheduleService.createSchedules({
userId: created_by,
......@@ -147,7 +156,12 @@ class MeetingService {
// RabbitMQ 메시지 발행 (푸시 알림 요청)
if (inviteeTokens.length > 0) {
await this.sendMeetingPushNotificationRequest(title, user.name, inviteeTokens);
await this.sendMeetingPushNotificationRequest(
title,
user.name,
inviteeTokens,
'invite'
);
}
const chatRoom = await ChatRooms.findOne({ chatRoomId: chatRoomId });
......@@ -215,7 +229,7 @@ class MeetingService {
return availableFriendIds;
}
async joinMeeting(meetingId, userId) {
const meeting = await Meeting.findByPk(meetingId);
console.log(`참여하려는 모임: ${JSON.stringify(meeting)}`);
......@@ -240,81 +254,104 @@ class MeetingService {
// 트랜잭션을 사용하여 참가자 추가 및 스케줄 업데이트를 원자적으로 처리
await sequelize.transaction(async (transaction) => {
if (meeting.cur_num >= meeting.max_num) {
throw new Error("모임 인원이 모두 찼습니다.");
}
// 스케줄 충돌 확인
const hasConflict = await ScheduleService.checkScheduleOverlapByTime(
userId,
meeting.time_idx_start,
meeting.time_idx_end,
transaction
);
console.log(`스케줄 충돌 결과: ${hasConflict}`);
if (hasConflict) {
throw new Error("스케줄이 겹칩니다. 다른 모임에 참가하세요.");
}
await MeetingParticipant.create(
{ meeting_id: meetingId, user_id: userId },
{ transaction }
);
const time_indices = Array.from(
{ length: meeting.time_idx_end - meeting.time_idx_start + 1 },
(_, i) => meeting.time_idx_start + i
);
await ScheduleService.createSchedules({
userId: userId,
title: `번개 모임: ${meeting.title}`,
is_fixed: false,
time_indices: time_indices,
}, transaction);
// 채팅방 참가 (MongoDB)
const user = await User.findOne({
where: { id: userId },
include: [
{
model: FcmToken,
as: 'fcmTokenList', // FCM 토큰 가져오기
attributes: ['token'],
},
],
transaction
});
const userFcmTokens = user.fcmTokenList.map((token) => token.token);
const chatRoom = await ChatRooms.findOne({
chatRoomId: meeting.chatRoomId,
});
console.log("여기까지");
console.log("user.name", user.name);
console.log("참가하는 유저 fcm", userFcmTokens);
if (chatRoom && !chatRoom.participants.includes(user.name)) {
// 참가자 추가
chatRoom.participants.push({
name: user.name,
fcmTokens: userFcmTokens, // FCM 토큰 추가
if (meeting.cur_num >= meeting.max_num) {
throw new Error("모임 인원이 모두 찼습니다.");
}
// 스케줄 충돌 확인
const hasConflict = await ScheduleService.checkScheduleOverlapByTime(
userId,
meeting.time_idx_start,
meeting.time_idx_end,
transaction
);
console.log(`스케줄 충돌 결과: ${hasConflict}`);
if (hasConflict) {
throw new Error("스케줄이 겹칩니다. 다른 모임에 참가하세요.");
}
await MeetingParticipant.create(
{ meeting_id: meetingId, user_id: userId },
{ transaction }
);
const time_indices = Array.from(
{ length: meeting.time_idx_end - meeting.time_idx_start + 1 },
(_, i) => meeting.time_idx_start + i
);
await ScheduleService.createSchedules({
userId: userId,
title: `번개 모임: ${meeting.title}`,
is_fixed: false,
time_indices: time_indices,
}, transaction);
// 채팅방 참가 (MongoDB)
const user = await User.findOne({
where: { id: userId },
include: [
{
model: FcmToken,
as: 'fcmTokenList', // FCM 토큰 가져오기
attributes: ['token'],
},
],
transaction
});
chatRoom.isOnline.set(user.name, true);
chatRoom.lastReadAt.set(user.name, new Date());
chatRoom.lastReadLogId.set(user.name, null);
await chatRoom.save();
}
// 현재 인원 수 증가
await meeting.increment("cur_num", { by: 1, transaction });
const userFcmTokens = user.fcmTokenList.map((token) => token.token);
const chatRoom = await ChatRooms.findOne({
chatRoomId: meeting.chatRoomId,
});
console.log("여기까지");
console.log("user.name", user.name);
console.log("참가하는 유저 fcm", userFcmTokens);
if (chatRoom && !chatRoom.participants.includes(user.name)) {
// 참가자 추가
chatRoom.participants.push({
name: user.name,
fcmTokens: userFcmTokens, // FCM 토큰 추가
});
chatRoom.isOnline.set(user.name, false);
chatRoom.lastReadAt.set(user.name, new Date());
chatRoom.lastReadLogId.set(user.name, null);
const joinMessage = {
message: `${user.name}님이 참가했습니다.`,
timestamp: new Date(),
type: 'join'
};
chatRoom.messages.push(joinMessage);
// 기존 참가자 FCM 토큰 가져오기
const otherParticipants = chatRoom.participants.filter(participant => participant.name !== user.name);
const otherParticipantTokens = otherParticipants.flatMap(participant => participant.fcmTokens);
if (otherParticipantTokens.length > 0) {
// RabbitMQ 메시지 발행
await this.sendMeetingPushNotificationRequest(
meeting.title,
user.name,
otherParticipantTokens,
'join'
);
}
await chatRoom.save();
}
// 현재 인원 수 증가
await meeting.increment("cur_num", { by: 1, transaction });
});
}
async getMeetings(userId, pagination) {
const { limit = 20, offset = 0 } = pagination;
try {
const meetings = await Meeting.findAll({
attributes: [
......@@ -337,7 +374,7 @@ class MeetingService {
offset,
distinct: true
});
const hasNext = meetings.length > limit;
const content = await Promise.all(
meetings.slice(0, limit).map(async (meeting) => {
......@@ -347,13 +384,13 @@ class MeetingService {
user_id: userId
}
});
const hasConflict = await ScheduleService.checkScheduleOverlapByTime(
userId,
meeting.time_idx_start,
meeting.time_idx_end
);
return new MeetingResponseDTO(
meeting,
!!isParticipant,
......@@ -362,7 +399,7 @@ class MeetingService {
);
})
);
return { content, hasNext };
} catch (error) {
console.error('getMeetings error:', error);
......@@ -372,7 +409,7 @@ class MeetingService {
async getMyMeetings(userId, pagination) {
const { limit = 20, offset = 0 } = pagination;
try {
const meetings = await Meeting.findAll({
attributes: [
......@@ -401,7 +438,7 @@ class MeetingService {
offset,
distinct: true
});
const hasNext = meetings.length > limit;
const content = meetings.slice(0, limit).map(meeting => {
return new MeetingResponseDTO(
......@@ -411,14 +448,14 @@ class MeetingService {
meeting.creator?.name || 'Unknown'
);
});
return { content, hasNext };
} catch (error) {
console.error('getMyMeetings error:', error);
throw new Error('Failed to fetch my meetings');
}
}
async getMeetingDetail(meetingId, userId) {
const meeting = await Meeting.findByPk(meetingId, {
include: [
......@@ -440,17 +477,17 @@ class MeetingService {
}
]
});
if (!meeting) {
throw new Error("모임을 찾을 수 없습니다.");
}
const hasConflict = await ScheduleService.checkScheduleOverlapByTime(
userId,
meeting.time_idx_start,
meeting.time_idx_end
);
return new MeetingDetailResponseDTO(meeting, hasConflict);
}
......@@ -526,13 +563,13 @@ class MeetingService {
// 저장
chatRoom.save();
}
async leaveMeeting(meetingId, userId) {
const meeting = await Meeting.findByPk(meetingId);
if (!meeting) {
throw new Error('모임을 찾을 수 없습니다.');
}
await sequelize.transaction(async (transaction) => {
// 참가자 확인
const participant = await MeetingParticipant.findOne({
......@@ -542,16 +579,16 @@ class MeetingService {
},
transaction
});
if (!participant) {
throw new Error('참가하지 않은 모임입니다.');
}
// 생성자는 탈퇴할 수 없음
if (meeting.created_by === userId) {
throw new Error('모임 생성자는 탈퇴할 수 없습니다.');
}
// 참가자 제거
await MeetingParticipant.destroy({
where: {
......@@ -560,7 +597,7 @@ class MeetingService {
},
transaction
});
// 관련 스케줄 삭제
await Schedule.destroy({
where: {
......@@ -572,7 +609,7 @@ class MeetingService {
},
transaction
});
// 채팅방에서 제거
const chatRoom = await ChatRooms.findOne({
chatRoomId: meeting.chatRoomId
......@@ -583,13 +620,61 @@ class MeetingService {
chatRoom.isOnline.delete(user.name);
chatRoom.lastReadAt.delete(user.name);
chatRoom.lastReadLogId.delete(user.name);
const leaveMessage = {
message: `${user.name}님이 퇴장했습니다.`,
timestamp: new Date(),
type: 'leave'
};
chatRoom.messages.push(leaveMessage);
await chatRoom.save();
}
// 현재 인원 수 감소
await meeting.decrement('cur_num', { by: 1, transaction });
});
}
async deleteMeeting(meetingId, userId) {
const meeting = await Meeting.findByPk(meetingId);
if (!meeting) {
throw new Error('모임을 찾을 수 없습니다.');
}
if (meeting.created_by !== userId) {
throw new Error('모임 생성자만 삭제할 수 있습니다.');
}
return await sequelize.transaction(async (transaction) => {
const participants = await MeetingParticipant.findAll({
where: { meeting_id: meetingId },
attributes: ['user_id'],
transaction
});
const participantIds = participants.map(p => p.user_id);
// 모든 참가자의 스케줄 삭제
await Schedule.destroy({
where: {
user_id: { [Op.in]: participantIds },
title: `번개 모임: ${meeting.title}`,
time_idx: {
[Op.between]: [meeting.time_idx_start, meeting.time_idx_end]
}
},
transaction
});
await ChatRooms.deleteOne({ chatRoomId: meeting.chatRoomId });
// 모임 관련 데이터 삭제
await meeting.destroy({ transaction });
});
}
}
module.exports = new MeetingService();
\ No newline at end of file
......@@ -10,11 +10,17 @@ class ScheduleService {
* @param {object} [transaction] - Sequelize 트랜잭션 객체 -> 미팅방에서 쓰기위해 트랜잭션을 넘겨받는걸 추가
*/
async createSchedules({ userId, title, is_fixed, time_indices }, transaction = null) {
const parsedTimeIndices = time_indices.map(idx => parseInt(idx, 10));
if (!userId || !title || !parsedTimeIndices.length) {
throw new Error('Required parameters missing');
}
const overlaps = await Schedule.findAll({
where: {
user_id: userId,
time_idx: {
[Op.in]: time_indices
[Op.in]: parsedTimeIndices
}
},
transaction
......@@ -24,7 +30,7 @@ class ScheduleService {
throw new Error(`Schedule overlaps at time_idx ${overlaps[0].time_idx}`);
}
const scheduleData = time_indices.map(time_idx => ({
const scheduleData = parsedTimeIndices.map(time_idx => ({
user_id: userId,
title,
time_idx,
......@@ -43,7 +49,7 @@ class ScheduleService {
user_id: userId,
title,
is_fixed,
time_indices,
time_indices: parsedTimeIndices,
createdAt: createdSchedules[0].createdAt,
updatedAt: createdSchedules[0].updatedAt
};
......
......@@ -7,16 +7,15 @@ async function syncRdb() {
try {
// 데이터베이스 연결 테스트
await sequelize.authenticate();
console.log('Rdb데이터베이스 연결 성공.');
console.log('Rdb 데이터베이스 연결 성공.');
// 모든 모델 동기화
await sequelize.sync({ force: true });
console.log('모든 모델이 성공적으로 동기화되었습니다.');
await sequelize.sync({alter :true});
console.log('모든 모델이 성공적으로 동기화됨.');
} catch (error) {
console.error('Rdb데이터베이스 연결 실패:', error);
console.error('Rdb 데이터베이스 연결 실패:', error);
}
}
module.exports = syncRdb;
\ No newline at end of file
module.exports = syncRdb;
......@@ -94,6 +94,7 @@ function startWebSocketServer() {
});
server.on('upgrade', (req, socket, head) => {
console.log('WebSocket 업그레이드 요청 수신:', req.headers);
handleWebSocketUpgrade(req, socket);
});
......@@ -103,19 +104,22 @@ function startWebSocketServer() {
}
function handleWebSocketUpgrade(req, socket) {
const key = req.headers['sec-websocket-key'];
const acceptKey = generateAcceptValue(key);
const responseHeaders = [
'HTTP/1.1 101 Switching Protocols',
'Upgrade: websocket',
'Connection: Upgrade',
`Sec-WebSocket-Accept: ${acceptKey}`
];
socket.write(responseHeaders.join('\r\n') + '\r\n\r\n');
// 클라이언트를 clients 배열에 추가
clients.push(socket);
try {
const key = req.headers['sec-websocket-key'];
const acceptKey = generateAcceptValue(key);
const responseHeaders = [
'HTTP/1.1 101 Switching Protocols',
'Upgrade: websocket',
'Connection: Upgrade',
`Sec-WebSocket-Accept: ${acceptKey}`,
`Access-Control-Allow-Origin: ${process.env.FRONT_URL}`, // 환경변수에서 가져옴
'Access-Control-Allow-Credentials: true'
];
socket.write(responseHeaders.join('\r\n') + '\r\n\r\n');
clients.push(socket);
} catch (error) {
console.error('WebSocket 업그레이드 실패:', error);
}
socket.on('data', async buffer => {
try {
......