Skip to content

Commit f789993

Browse files
authored
Merge pull request #393 from PromptPlace/feat/#387
[FEAT] 채팅 HTTP API 개발
2 parents ef7bdf2 + 0d39ebf commit f789993

File tree

6 files changed

+530
-7
lines changed

6 files changed

+530
-7
lines changed

src/chat/controllers/chat.controller.ts

Lines changed: 138 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export const createOrGetChatRoom = async (req: Request, res: Response): Promise<
2626
} catch (err: any) {
2727
console.error(err);
2828
res.fail({
29-
error: err.name || "InternalServerError",
29+
error: err.error || "InternalServerError",
3030
message: err.message || "채팅방 생성/반환 중 오류가 발생했습니다.",
3131
statusCode: err.statusCode || 500,
3232
});
@@ -61,7 +61,7 @@ export const getChatRoomDetail = async (req: Request, res: Response): Promise<vo
6161
} catch (err: any) {
6262
console.error(err);
6363
res.fail({
64-
error: err.name || "InternalServerError",
64+
error: err.error || "InternalServerError",
6565
message: err.message || "채팅방 상세 조회 중 오류가 발생했습니다.",
6666
statusCode: err.statusCode || 500,
6767
});
@@ -99,9 +99,143 @@ export const getChatRoomList = async(req: Request, res: Response) => {
9999
} catch (err: any) {
100100
console.error(err);
101101
res.fail({
102-
error: err.name || "InternalServerError",
102+
error: err.error || "InternalServerError",
103103
message: err.message || "채팅방 목록 조회 중 오류가 발생했습니다.",
104104
statusCode: err.statusCode || 500,
105105
});
106106
}
107-
}
107+
}
108+
109+
// == 상대방 차단
110+
export const blockUser = async(req: Request, res: Response) => {
111+
if (!req.user) {
112+
res.fail({
113+
statusCode: 401,
114+
error: "no user",
115+
message: "로그인이 필요합니다.",
116+
});
117+
return;
118+
}
119+
try {
120+
const blockerId = (req.user as { user_id: number }).user_id;
121+
const { blocked_user_id } = req.body as { blocked_user_id: number };
122+
123+
if (!blocked_user_id || isNaN(blocked_user_id)) {
124+
res.fail({ statusCode: 400, error: "BadRequest", message: "올바른 blocked_user_id가 필요합니다." });
125+
return;
126+
}
127+
128+
if (blockerId === blocked_user_id) {
129+
res.fail({ statusCode: 400, error: "BadRequest", message: "자기 자신을 차단할 수 없습니다." });
130+
return;
131+
}
132+
133+
await chatService.blockUserService(blockerId, blocked_user_id);
134+
135+
res.success(null, "상대방을 성공적으로 차단했습니다.");
136+
} catch (err: any) {
137+
console.error(err);
138+
res.fail({
139+
error: err.error || "InternalServerError",
140+
message: err.message || "상대방 차단 중 오류가 발생했습니다.",
141+
statusCode: err.statusCode || 500,
142+
});
143+
}
144+
};
145+
146+
// == 채팅방 나가기
147+
export const leaveChatRoom = async(req: Request, res: Response) => {
148+
if (!req.user) {
149+
res.fail({
150+
statusCode: 401,
151+
error: "no user",
152+
message: "로그인이 필요합니다.",
153+
});
154+
return;
155+
}
156+
try {
157+
const userId = (req.user as { user_id: number }).user_id;
158+
const roomId = Number(req.params.roomId);
159+
console.log("🍀roomId:", roomId);
160+
161+
if (isNaN(roomId)) {
162+
res.fail({ statusCode: 400, error: "BadRequest", message: "올바른 roomId가 필요합니다." });
163+
return;
164+
}
165+
await chatService.leaveChatRoomService(roomId, userId);
166+
167+
res.success(null, "채팅방을 성공적으로 나갔습니다.");
168+
} catch (err: any) {
169+
console.error(err);
170+
res.fail({
171+
error: err.error || "InternalServerError",
172+
message: err.message || "채팅방 나가기 중 오류가 발생했습니다.",
173+
statusCode: err.statusCode || 500,
174+
});
175+
}
176+
};
177+
// == S3 presigned URL 발급
178+
export const getPresignedUrl = async(req: Request, res: Response) => {
179+
if (!req.user) {
180+
res.fail({
181+
statusCode: 401,
182+
error: "no user",
183+
message: "로그인이 필요합니다.",
184+
});
185+
return;
186+
}
187+
try {
188+
const rawFiles = req.body.files;
189+
190+
if (!rawFiles || !Array.isArray(rawFiles) || rawFiles.length === 0) {
191+
res.fail({ statusCode: 400, error: "BadRequest", message: "업로드할 파일 정보가 필요합니다." });
192+
return;
193+
}
194+
195+
const files = rawFiles.map((file: any) => ({
196+
fileName: file.name,
197+
contentType: file.content_type
198+
}));
199+
200+
const result = await chatService.getPresignedUrlService(files);
201+
202+
res.success(result, "presign을 성공적으로 발급했습니다.");
203+
} catch (err: any) {
204+
console.error(err);
205+
res.fail({
206+
error: err.error || "InternalServerError",
207+
message: err.message || "presigned URL 생성 중 오류가 발생했습니다.",
208+
statusCode: err.statusCode || 500,
209+
});
210+
}
211+
};
212+
213+
// == 채팅방 고정 토글
214+
export const togglePinChatRoom = async(req: Request, res: Response) => {
215+
if (!req.user) {
216+
res.fail({
217+
statusCode: 401,
218+
error: "no user",
219+
message: "로그인이 필요합니다.",
220+
});
221+
return;
222+
}
223+
try {
224+
const userId = (req.user as { user_id: number }).user_id;
225+
const roomId = Number(req.params.roomId);
226+
if (isNaN(roomId)) {
227+
res.fail({ statusCode: 400, error: "BadRequest", message: "올바른 roomId가 필요합니다." });
228+
return;
229+
}
230+
const isPinned = await chatService.togglePinChatRoomService(roomId, userId);
231+
res.success(isPinned, "채팅방 고정을 성공적으로 토글했습니다.");
232+
}
233+
catch (err: any) {
234+
console.error(err);
235+
res.fail({
236+
error: err.error || "InternalServerError",
237+
message: err.message || "채팅방 고정 토글 중 오류가 발생했습니다.",
238+
statusCode: err.statusCode || 500,
239+
});
240+
}
241+
}

src/chat/dtos/chat.dto.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,4 +207,9 @@ export class ChatRoomListResponseDto {
207207

208208
return dto;
209209
}
210+
}
211+
212+
// == 채팅방 고정 토글
213+
export interface TogglePinResponseDto {
214+
is_pinned: boolean;
210215
}

src/chat/repositories/chat.repository.ts

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export class ChatRepository {
2929
});
3030
}
3131

32+
// == 채팅방 상세 조회 (참여자 정보 포함)
3233
async findRoomDetailWithParticipant(roomId: number) {
3334
return prisma.chatRoom.findUnique({
3435
where: { room_id: roomId },
@@ -40,6 +41,23 @@ export class ChatRepository {
4041
});
4142
}
4243

44+
// == 안읽은 메세지 초기화
45+
async resetUnreadCount(roomId: number, userId: number, lastMessageId?: number | null) {
46+
return prisma.chatParticipant.update({
47+
where: {
48+
room_id_user_id: {
49+
room_id: roomId,
50+
user_id: userId,
51+
},
52+
},
53+
data: {
54+
unread_count: 0,
55+
last_read_message_id: lastMessageId,
56+
},
57+
});
58+
}
59+
60+
// == 메시지 목록 조회
4361
async findMessagesByRoomId(roomId: number, cursor?: number, limit: number = 20, userId?: number) {
4462
const leftInfo = await prisma.chatParticipant.findFirst({
4563
where: {
@@ -194,6 +212,46 @@ export class ChatRepository {
194212
]);
195213
return {rooms, totalRoom}
196214
};
197-
}
198215

216+
// == 상대방 차단
217+
async blockUser(blockerId: number, blockedId: number) {
218+
return prisma.userBlock.create({
219+
data: {
220+
blocker_id: blockerId,
221+
blocked_id: blockedId,
222+
},
223+
});
224+
}
225+
226+
// == 채팅방 나가기
227+
async leaveChatRoom(roomId: number, userId: number) {
228+
return prisma.chatParticipant.update({
229+
where: {
230+
room_id_user_id: {
231+
room_id: roomId,
232+
user_id: userId,
233+
},
234+
},
235+
data: {
236+
left_at: new Date(),
237+
},
238+
});
239+
};
240+
241+
242+
// == 채팅방 고정 토글
243+
async togglePinChatRoom(roomId: number, userId: number, isPinned: boolean) {
244+
return prisma.chatParticipant.update({
245+
where: {
246+
room_id_user_id: {
247+
room_id: roomId,
248+
user_id: userId,
249+
},
250+
},
251+
data: {
252+
is_pinned: !isPinned, // 토글
253+
},
254+
});
255+
}
256+
}
199257

0 commit comments

Comments
 (0)