Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
-- CreateTable
CREATE TABLE `chat_message_reads` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`messageId` BIGINT NOT NULL,
`accountId` BIGINT NOT NULL,
`read` BOOLEAN NOT NULL DEFAULT false,

PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

-- AddForeignKey
ALTER TABLE `chat_message_reads` ADD CONSTRAINT `chat_message_reads_messageId_fkey` FOREIGN KEY (`messageId`) REFERENCES `chat_messages`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE `chat_message_reads` ADD CONSTRAINT `chat_message_reads_accountId_fkey` FOREIGN KEY (`accountId`) REFERENCES `accounts`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
14 changes: 14 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ model Account {
userCategories UserCategory[]
follows Follow[]
userBadges UserBadge[]
chatMessageReads ChatMessageRead[]

@@unique([provider, oauthId])
@@map("accounts")
Expand Down Expand Up @@ -235,10 +236,23 @@ model ChatMessage {

chatroom Chatroom @relation(fields: [chatroomId], references: [id])
sender User @relation(fields: [senderId], references: [id])
chatMessageReads ChatMessageRead[]

@@map("chat_messages")
}

model ChatMessageRead {
id BigInt @id @default(autoincrement())
messageId BigInt
accountId BigInt
read Boolean @default(false)

message ChatMessage @relation(fields: [messageId], references: [id])
account Account @relation(fields: [accountId], references: [id])

@@map("chat_message_reads")
}

model Follow {
id BigInt @id @default(autoincrement())
artistId BigInt @map("artist_id")
Expand Down
3 changes: 2 additions & 1 deletion src/chat/controller/chatroom.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ export const createChatroom = async (req, res, next) => {
export const getChatroom = async (req, res, next) => {
try {
const dto = new GetChatroomDto({
consumerId: BigInt(req.user.userId)
consumerId: BigInt(req.user.userId),
accountId: BigInt(req.user.accountId),
});

const chatrooms = await ChatroomService.getChatroomsByUserId(dto);
Expand Down
17 changes: 16 additions & 1 deletion src/chat/dto/chatroom.dto.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,26 @@ export class CreateChatroomDto {
}

export class GetChatroomDto {
constructor({ consumerId }) {
constructor({ consumerId, accountId }) {
this.consumerId = BigInt(consumerId);
this.accountId = BigInt(accountId);
}
}

export class ChatroomListResponseDto {
constructor(room, unreadCount = 0) {
this.chatroom_id = room.id;
this.artist_id = room.artist.id;
this.artist_nickname = room.artist.nickname;
this.artist_profile_image = room.artist.profileImage;
this.request_id = room.request.id;
this.request_title = room.request.commission.title;
this.last_message = room.chatMessages[0]?.content || null;
this.last_message_time = room.chatMessages[0]?.createdAt || null;
this.has_unread = unreadCount;
}
}

export class DeleteChatroomDto {
constructor({ chatroomIds, userType, userId }) {
this.chatroomIds = chatroomIds.map(id => BigInt(id));
Expand Down
46 changes: 46 additions & 0 deletions src/chat/repository/chat.repository.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,50 @@ export const ChatRepository = {
}
});
},

async markAsRead(accountId, messageId) {
return await prisma.chatMessageRead.upsert({
where: {
messageId_accountId: {
messageId: BigInt(messageId),
accountId: BigInt(accountId),
},
},
update: { read: true },
create: {
messageId: BigInt(messageId),
accountId: BigInt(accountId),
read: true,
},
});
},

async isMessageRead(accountId, messageId) {
const record = await prisma.chatMessageRead.findUnique({
where: {
messageId_accountId: {
messageId: BigInt(messageId),
accountId: BigInt(accountId),
},
},
});
return record?.read || false;
},

async countUnreadMessages(chatroomId, accountId) {
const count = await prisma.chatMessage.count({
where: {
chatroomId: BigInt(chatroomId),
NOT: {
chatMessageReads: {
some: {
accountId: BigInt(accountId),
read: true,
},
},
},
},
});
return count;
},
};
32 changes: 28 additions & 4 deletions src/chat/repository/chatroom.repository.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,35 @@ export const ChatroomRepository = {
},

async findChatroomsByUser(consumerId) {
return await prisma.chatroom.findMany({
where: {
consumerId: consumerId,
hiddenConsumer: false,
return prisma.chatroom.findMany({
where: { consumerId },
include: {
artist: {
select: {
id: true,
nickname: true,
profileImage: true,
}
},
request: {
select: {
id: true,
commission: {
select: {
title: true
}
}
}
},
chatMessages: {
orderBy: { createdAt: "desc" },
take: 1,
select: {
content: true,
createdAt: true
}
}
}
});
},

Expand Down
8 changes: 8 additions & 0 deletions src/chat/service/chat.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,12 @@ export const ChatService = {
const messages = await ChatRepository.searchByKeyword(dto.keyword, chatroomIds);
return messages;
},

async markMessageAsRead(accountId, messageId) {
return await ChatRepository.markAsRead(accountId, messageId);
},

async getUnreadCount(chatroomId, accountId) {
return await ChatRepository.countUnreadMessages(chatroomId, accountId);
}
};
13 changes: 11 additions & 2 deletions src/chat/service/chatroom.service.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { ChatroomRepository } from "../repository/chatroom.repository.js";
import { ChatRepository } from "../repository/chat.repository.js";
import { UserRepository } from "../../user/repository/user.repository.js";
import { RequestRepository } from "../../request/repository/request.repository.js";
import { UserNotFoundError } from "../../common/errors/user.errors.js";
import { ArtistNotFoundError } from "../../common/errors/artist.errors.js";
import { RequestNotFoundError } from "../../common/errors/request.errors.js";
import { ChatroomNotFoundError } from "../../common/errors/chat.errors.js";
import { ChatroomListResponseDto } from "../dto/chatroom.dto.js";

export const ChatroomService = {
async createChatroom(dto) {
Expand Down Expand Up @@ -52,8 +54,15 @@ export const ChatroomService = {
}

const chatrooms = await ChatroomRepository.findChatroomsByUser(dto.consumerId);

return chatrooms;
console.log(dto.accountId)

const result = [];
for (const room of chatrooms) {
const unreadCount = await ChatRepository.countUnreadMessages(room.id, dto.accountId);
result.push(new ChatroomListResponseDto(room, unreadCount));
}

return result;
},

async softDeleteChatroomsByUser(dto) {
Expand Down
54 changes: 52 additions & 2 deletions src/chat/socket/socket.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { uploadToS3 } from "../../s3.upload.js";
import { Server } from "socket.io";
import { stringifyWithBigInt } from "../../bigintJson.js";
import { PrismaClient } from '@prisma/client';
import { verifyJwt } from '../../jwt.config.js';
import { ChatRepository } from "../repository/chat.repository.js";

const prisma = new PrismaClient();

Expand All @@ -10,13 +12,61 @@ export default function setupSocket(server) {
cors: { origin: "*", methods: ["GET", "POST"] },
});

io.use((socket, next) => {
try {
const token = socket.handshake.auth?.token;
if (!token) {
return next(new Error("Authentication error: Token missing"));
}
const user = verifyJwt(token);
socket.user = user; // userId, role 등 저장
next();
} catch (err) {
next(new Error("Authentication error: Invalid token"));
}
});

io.on("connection", (socket) => {
console.log("User connected:", socket.id);

// 채팅방 join
socket.on("join", (chatroomId) => {
socket.on("join", async (chatroomId) => {
socket.join(chatroomId);
console.log(`User ${socket.id} joined chatroom ${chatroomId}`);
console.log(`User ${socket.user.userId} joined chatroom ${chatroomId}`);

try {
console.log(socket.user)
const accountId = BigInt(socket.user.accountId);

// 해당 채팅방의 모든 미확인 메시지에 대해 읽음 처리
const unreadMessages = await prisma.chatMessage.findMany({
where: {
chatroomId: BigInt(chatroomId),
NOT: {
chatMessageReads: {
some: {
accountId: BigInt(accountId),
read: true,
},
},
},
},
});

// 읽음 상태 기록 업데이트
await Promise.all(
unreadMessages.map((msg) =>
ChatRepository.markAsRead(accountId, msg.id)
)
);

// 필요하면 클라이언트에 읽음 처리 완료 알림 전송
socket.emit("read messages success", { chatroomId });

} catch (err) {
console.error("Error marking messages as read:", err);
socket.emit("error", { message: "읽음 처리 중 오류가 발생했습니다." });
}
});

// 메시지 수신
Expand Down
15 changes: 9 additions & 6 deletions src/common/swagger/chat.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,12 +107,15 @@
"items": {
"type": "object",
"properties": {
"id": { "type": "integer", "example": 10 },
"consumerId": { "type": "integer", "example": 1 },
"artistId": { "type": "integer", "example": 2 },
"requestId": { "type": "integer", "example": 3 },
"createdAt": { "type": "string", "format": "date-time" },
"updatedAt": { "type": "string", "format": "date-time" }
"chatroom_id": { "type": "string", "example": "2" },
"artist_id": { "type": "string", "example": "1" },
"artist_nickname": { "type": "string", "example": "artist_one" },
"artist_profile_image": { "type": "string", "example": "https://example.com/artist1.png" },
"request_id": { "type": "string", "example": "1" },
"request_title": { "type": "string", "example": "테스트 커미션 글" },
"last_message": { "type": ["string", "null"], "example": null },
"last_message_time": { "type": ["string", "null"], "format": "date-time", "example": null },
"has_unread": { "type": "integer", "example": 0 }
}
}
}
Expand Down