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
90 changes: 90 additions & 0 deletions src/entities/chat/lib/useChatMessages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { useEffect, useState, useRef } from "react";
import { apiFetch } from "@/shared/api/fetcher";
import { MessagesResponse, MessageProps } from "../model/types";
import { useModalStore } from "@/shared/model/modal.store";

export const useChatMessages = (chatId: number | null) => {
const [messages, setMessages] = useState<MessageProps[]>([]);
const [cursor, setCursor] = useState<number | null>(null);
const [hasNext, setHasNext] = useState(true);
const [isLoading, setIsLoading] = useState(false);
const { openModal, closeModal } = useModalStore();

const scrollContainerRef = useRef<HTMLDivElement | null>(null);
const messagesEndRef = useRef<HTMLDivElement | null>(null);

// 초기 메시지
useEffect(() => {
if (!chatId) return;

async function fetchMessages() {
try {
setIsLoading(true);
const res = await apiFetch<MessagesResponse>(
`/api/chat/${chatId}?size=20`,
{ method: "GET" },
);

setMessages(res.messages.reverse());
setCursor(res.nextCursor);
setHasNext(res.hasNext);

if (messagesEndRef.current) {
messagesEndRef.current.scrollIntoView({ behavior: "auto" });
}
} catch {
openModal("normal", {
message: "메시지 조회에 실패했습니다.",
onClick: () => closeModal(),
});
} finally {
setIsLoading(false);
}
}

fetchMessages();
}, [chatId]);

// 이전 메시지 추가 로드
const fetchMoreMessages = async () => {
if (!chatId || !hasNext || isLoading) return;

const container = scrollContainerRef.current;
const prevHeight = container?.scrollHeight ?? 0;

try {
setIsLoading(true);
const res = await apiFetch<MessagesResponse>(
`/api/chat/${chatId}?cursor=${cursor}&size=20`,
{ method: "GET" },
);
setMessages((prev) => [...[...res.messages].reverse(), ...prev]);
setCursor(res.nextCursor);
setHasNext(res.hasNext);
//스크롤 높이 조정
if (container) {
requestAnimationFrame(() => {
const newHeight = container.scrollHeight;
container.scrollTop += newHeight - prevHeight;
});
}
} catch {
openModal("normal", {
message: "이전 메시지를 불러오는데 실패했습니다.",
onClick: () => closeModal(),
});
} finally {
setIsLoading(false);
}
};

return {
messages,
setMessages,
isLoading,
hasNext,
fetchMoreMessages,
scrollContainerRef,
messagesEndRef,
};
};
33 changes: 33 additions & 0 deletions src/entities/chat/lib/useChatOtherUser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { useEffect, useState } from "react";
import { User } from "@/entities/user/model/types/user";
import { apiFetch } from "@/shared/api/fetcher";
import { useModalStore } from "@/shared/model/modal.store";

export const useChatOtherUser = (otherId: number) => {
const [otherUser, setOtherUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(false);
const { openModal, closeModal } = useModalStore();

useEffect(() => {
async function fetchUser() {
try {
setIsLoading(true);
const res = await apiFetch<User>(`/api/users/${otherId}`, {
method: "GET",
});
setOtherUser(res);
} catch {
openModal("normal", {
message: "상대 유저 정보 조회에 실패했습니다.",
onClick: () => closeModal(),
});
} finally {
setIsLoading(false);
}
}

fetchUser();
}, [otherId]);

return { otherUser, isLoading };
};
33 changes: 33 additions & 0 deletions src/entities/chat/lib/useChatPost.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { useEffect, useState } from "react";
import { PostDetail } from "@/entities/post/model/types/post";
import { apiFetch } from "@/shared/api/fetcher";
import { useModalStore } from "@/shared/model/modal.store";

export const useChatPost = (postingId: number) => {
const [post, setPost] = useState<PostDetail | null>(null);
const [isLoading, setIsLoading] = useState(false);
const { openModal, closeModal } = useModalStore();

useEffect(() => {
async function fetchPost() {
try {
setIsLoading(true);
const res = await apiFetch<PostDetail>(`/api/postings/${postingId}`, {
method: "GET",
});
setPost(res);
} catch {
openModal("normal", {
message: "게시물 정보 조회에 실패했습니다.",
onClick: () => closeModal(),
});
} finally {
setIsLoading(false);
}
}

fetchPost();
}, [postingId]);

return { post, isLoading };
};
83 changes: 83 additions & 0 deletions src/entities/chat/lib/useChatSocket.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { useEffect, useRef } from "react";
import { ChatSocket } from "../model/socket";
import { MessageProps } from "../model/types";

export const useChatSocket = (
chatId: number | null,
setMessages: React.Dispatch<React.SetStateAction<MessageProps[]>>,
scrollToBottom?: () => void,
) => {
const socketRef = useRef<ChatSocket | null>(null);
const isConnectedRef = useRef(false);

useEffect(() => {
if (!chatId) return;

const socket = new ChatSocket(chatId, {
onOpen: () => {
console.log("[Socket] Connected");
isConnectedRef.current = true;
},
onMessage: (msg) => {
setMessages((prev) => [...prev, msg]);
scrollToBottom?.();
},
onSystem: (sys) => console.log("[System]", sys.message),
onClose: (code) => {
console.log("[Socket] Closed:", code);
isConnectedRef.current = false;
},
onError: () => {
isConnectedRef.current = false;
},
});

socket.connect().then(() => {
isConnectedRef.current = true;
});

socketRef.current = socket;

// cleanup
return () => {
socketRef.current?.leaveRoom();
socketRef.current = null;
isConnectedRef.current = false;
};
}, [chatId]);

const waitForConnection = async () => {
const socket = socketRef.current;
if (!socket) return;

// 연결 시도 중이면 connect()의 Promise를 기다려줌
if (!isConnectedRef.current) {
await socket.connect();
isConnectedRef.current = true;
}
};

const sendMessage = async (type: "text" | "image", content: string) => {
await waitForConnection(); // 연결 안 되어있으면 여기서 기다림

const now = new Date();
const sendAt = now.toISOString();

socketRef.current?.sendMessage(type, content);
setMessages((prev) => [
...prev,
{
messageId: Date.now(),
type,
content,
isMine: true,
sendAt,
isRead: true,
},
]);

scrollToBottom?.();
};

return { sendMessage };
};
14 changes: 11 additions & 3 deletions src/entities/chat/model/socket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,17 @@ export class ChatSocket {

leaveRoom() {
if (!this.socket) return;
console.log(this.socket);
this.socket.send(JSON.stringify({ event: "leave_room" }));
this.socket.close(1000, "User left");

const state = this.socket.readyState;

// 아직 연결 중이거나 이미 닫힌 상태에서는 그냥 close()만 호출
if (state === WebSocket.OPEN) {
this.socket.send(JSON.stringify({ event: "leave_room" }));
this.socket.close(1000, "User left");
} else {
this.socket.close(1000, `User left skipped - ${state.toString()}`);
}

this.socket = null;
}
}
Loading