diff --git a/NBE_5_7_2_02TEAM/src/main/java/io/twogether/nbe_5_7_2_02team/chat/dao/ChatMemberRepository.java b/NBE_5_7_2_02TEAM/src/main/java/io/twogether/nbe_5_7_2_02team/chat/dao/ChatMemberRepository.java index 213b1e22..60153c41 100644 --- a/NBE_5_7_2_02TEAM/src/main/java/io/twogether/nbe_5_7_2_02team/chat/dao/ChatMemberRepository.java +++ b/NBE_5_7_2_02TEAM/src/main/java/io/twogether/nbe_5_7_2_02team/chat/dao/ChatMemberRepository.java @@ -1,11 +1,13 @@ package io.twogether.nbe_5_7_2_02team.chat.dao; import io.twogether.nbe_5_7_2_02team.chat.domain.ChatMember; +import io.twogether.nbe_5_7_2_02team.chat.domain.ChatMemberStatus; import io.twogether.nbe_5_7_2_02team.chat.domain.ChatRoom; import io.twogether.nbe_5_7_2_02team.member.domain.Member; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Collection; import java.util.List; public interface ChatMemberRepository extends JpaRepository { @@ -14,7 +16,8 @@ public interface ChatMemberRepository extends JpaRepository { List findByChatRoom(ChatRoom chatRoom); - List findByMember(Member member); + List findByMemberAndChatMemberStatusIn( + Member member, Collection chatMemberStatuses); void deleteByChatRoom(ChatRoom chatRoom); diff --git a/NBE_5_7_2_02TEAM/src/main/java/io/twogether/nbe_5_7_2_02team/chat/domain/ChatMember.java b/NBE_5_7_2_02TEAM/src/main/java/io/twogether/nbe_5_7_2_02team/chat/domain/ChatMember.java index 10101a10..6cded568 100644 --- a/NBE_5_7_2_02TEAM/src/main/java/io/twogether/nbe_5_7_2_02team/chat/domain/ChatMember.java +++ b/NBE_5_7_2_02TEAM/src/main/java/io/twogether/nbe_5_7_2_02team/chat/domain/ChatMember.java @@ -17,6 +17,7 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.Setter; @Entity @Getter @@ -35,6 +36,7 @@ public class ChatMember extends BaseEntity { @JoinColumn(name = "member_id") private Member member; + @Setter @Column(name = "status") @Enumerated(EnumType.STRING) private ChatMemberStatus chatMemberStatus; @@ -45,8 +47,4 @@ public ChatMember(ChatRoom chatRoom, Member member, ChatMemberStatus chatMemberS this.member = member; this.chatMemberStatus = chatMemberStatus; } - - public void updateStatus(ChatMemberStatus chatMemberStatus) { - this.chatMemberStatus = chatMemberStatus; - } } diff --git a/NBE_5_7_2_02TEAM/src/main/java/io/twogether/nbe_5_7_2_02team/chat/dto/request/ChatMemberUpdateRequest.java b/NBE_5_7_2_02TEAM/src/main/java/io/twogether/nbe_5_7_2_02team/chat/dto/request/ChatMemberUpdateRequest.java index 1d906f3d..11dcd6b0 100644 --- a/NBE_5_7_2_02TEAM/src/main/java/io/twogether/nbe_5_7_2_02team/chat/dto/request/ChatMemberUpdateRequest.java +++ b/NBE_5_7_2_02TEAM/src/main/java/io/twogether/nbe_5_7_2_02team/chat/dto/request/ChatMemberUpdateRequest.java @@ -1,12 +1,19 @@ package io.twogether.nbe_5_7_2_02team.chat.dto.request; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + import io.twogether.nbe_5_7_2_02team.chat.domain.ChatMemberStatus; import lombok.Getter; -import lombok.RequiredArgsConstructor; @Getter -@RequiredArgsConstructor public class ChatMemberUpdateRequest { private final ChatMemberStatus chatMemberStatus; + + @JsonCreator + public ChatMemberUpdateRequest( + @JsonProperty("chatMemberStatus") ChatMemberStatus chatMemberStatus) { + this.chatMemberStatus = chatMemberStatus; + } } diff --git a/NBE_5_7_2_02TEAM/src/main/java/io/twogether/nbe_5_7_2_02team/chat/service/ChatMemberService.java b/NBE_5_7_2_02TEAM/src/main/java/io/twogether/nbe_5_7_2_02team/chat/service/ChatMemberService.java index f04ba5af..76d1158a 100644 --- a/NBE_5_7_2_02TEAM/src/main/java/io/twogether/nbe_5_7_2_02team/chat/service/ChatMemberService.java +++ b/NBE_5_7_2_02TEAM/src/main/java/io/twogether/nbe_5_7_2_02team/chat/service/ChatMemberService.java @@ -1,7 +1,8 @@ package io.twogether.nbe_5_7_2_02team.chat.service; +import static io.twogether.nbe_5_7_2_02team.chat.domain.ChatMemberStatus.LEFT; +import static io.twogether.nbe_5_7_2_02team.chat.domain.ChatMemberStatus.OFFLINE; import static io.twogether.nbe_5_7_2_02team.chat.domain.ChatMemberStatus.ONLINE; -import static io.twogether.nbe_5_7_2_02team.global.response.error.ErrorCode.CHAT_MEMBER_ALREADY_EXISTS; import static io.twogether.nbe_5_7_2_02team.global.response.error.ErrorCode.CHAT_MEMBER_NOT_ENTER; import static io.twogether.nbe_5_7_2_02team.global.response.error.ErrorCode.CHAT_MEMBER_UNDEFINED_STATUS; import static io.twogether.nbe_5_7_2_02team.global.response.error.ErrorCode.CHAT_ROOM_EMPTY; @@ -23,6 +24,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.Arrays; import java.util.List; @Service @@ -39,7 +41,9 @@ public List getChatRoomListByUser( @AuthenticationPrincipal UserDetails userDetails) { Member member = checkUserLogin.checkUserLogin(userDetails); - List chatMemberList = chatMemberRepository.findByMember(member); + List chatMemberList = + chatMemberRepository.findByMemberAndChatMemberStatusIn( + member, Arrays.asList(ONLINE, OFFLINE)); return chatMemberList.stream() .map(chatMember -> ChatRoomGetResponse.from(chatMember.getChatRoom())) @@ -68,7 +72,11 @@ public Long createChatMember(Long chatroomId, UserDetails userDetails) { ChatMember chatMember = chatMemberRepository.findByChatRoomAndMember(chatRoom, member); if (chatMember != null) { - throw new ErrorException(CHAT_MEMBER_ALREADY_EXISTS); + if (chatMember.getChatMemberStatus() == LEFT) { + chatMember.setChatMemberStatus(ONLINE); + } + + return chatMember.getId(); } Long id = @@ -105,7 +113,7 @@ public Long updateChatMember( throw new ErrorException(CHAT_MEMBER_UNDEFINED_STATUS); } - chatMember.updateStatus(chatMemberStatus); + chatMember.setChatMemberStatus(chatMemberStatus); return chatMemberRepository.save(chatMember).getId(); } } diff --git a/NBE_5_7_2_02TEAM/src/test/java/io/twogether/nbe_5_7_2_02team/chat/service/ChatMemberServiceTest.java b/NBE_5_7_2_02TEAM/src/test/java/io/twogether/nbe_5_7_2_02team/chat/service/ChatMemberServiceTest.java index f197bebf..b300b24b 100644 --- a/NBE_5_7_2_02TEAM/src/test/java/io/twogether/nbe_5_7_2_02team/chat/service/ChatMemberServiceTest.java +++ b/NBE_5_7_2_02TEAM/src/test/java/io/twogether/nbe_5_7_2_02team/chat/service/ChatMemberServiceTest.java @@ -197,19 +197,6 @@ void createChatMemberNoChatRoomTest() { assertEquals(CHAT_ROOM_NOT_FOUND, errorException.getErrorCode()); } - @Test - @DisplayName("채팅방 입장 테스트: 에러 - 이미 참여중") - void createChatMemberAlreadyJoinTest() { - chatMemberService.createChatMember(chatRoomId, userDetails1); - - ErrorException errorException = - assertThrows( - ErrorException.class, - () -> chatMemberService.createChatMember(chatRoomId, userDetails1)); - - assertEquals(CHAT_MEMBER_ALREADY_EXISTS, errorException.getErrorCode()); - } - @Test @DisplayName("멤버 상태 변경 테스트: 성공") void updateChatMemberTest() { diff --git a/frontend/src/components/ChatRoom.tsx b/frontend/src/components/ChatRoom.tsx index d1cbe3d4..2a5100e7 100644 --- a/frontend/src/components/ChatRoom.tsx +++ b/frontend/src/components/ChatRoom.tsx @@ -215,14 +215,14 @@ function ChatRoom({ chatRoomId, postTitle, onBack }: ChatRoomProps) { const client = new Client({ webSocketFactory: () => new SockJS(`${import.meta.env.VITE_BASE_URL}/ws/chatroom`), - debug: (str) => { + debug: () => { }, reconnectDelay: 5000, connectHeaders: { Authorization: `Bearer ${token}` }, }); - client.onConnect = (frame) => { + client.onConnect = () => { stompClientRef.current = client; const topic = `/sub/${chatRoomId}/message`; subscriptionRef.current = client.subscribe(topic, (message: IMessage) => { @@ -267,7 +267,7 @@ function ChatRoom({ chatRoomId, postTitle, onBack }: ChatRoomProps) { client.onStompError = (frame) => { setError(`STOMP Error: ${frame.headers["message"] || "Connection failed"}`); }; - client.onWebSocketError = (event) => { + client.onWebSocketError = () => { setError("WebSocket 연결에 실패했습니다. 네트워크 상태를 확인해주세요."); }; client.onDisconnect = () => { @@ -303,17 +303,40 @@ function ChatRoom({ chatRoomId, postTitle, onBack }: ChatRoomProps) { setShowParticipants((prev) => !prev); }; - const handleChangeParticipantState = (participantId: number) => { + const handleBackButtonPress = () => { + disconnectStomp(); + onBack(); }; - const handleSelfStatusChange = async (newStatus: string) => { - }; + const handleLeaveChatRoom = async () => { + const token = localStorage.getItem("accessToken"); + if (!token) { + alert("로그인이 필요합니다."); + return; + } + + try { + const response = await fetch(`${import.meta.env.VITE_API_BASE_URL}/chatroom/${chatRoomId}/member`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}`, + }, + body: JSON.stringify({ chatMemberStatus: 'LEFT' }), + }); - const currentUserParticipant = participants.find((p) => p.id === currentMemberId); + if (!response.ok) { + const errorData = await response.json().catch(() => ({ message: '서버 응답 오류' })); + throw new Error(errorData.message || `채팅방 나가기 실패: ${response.status}`); + } - const handleBackButtonPress = () => { - disconnectStomp(); - onBack(); + disconnectStomp(); + onBack(); + + } catch (error) { + console.error("Failed to leave chat room:", error); + alert(`채팅방을 나가는 중 오류가 발생했습니다: ${error instanceof Error ? error.message : '알 수 없는 오류'}`); + } }; return ( @@ -417,6 +440,32 @@ function ChatRoom({ chatRoomId, postTitle, onBack }: ChatRoomProps) {

참여중인 멤버

+ +
diff --git a/frontend/src/pages/ChatRoomList.tsx b/frontend/src/pages/ChatRoomList.tsx index c87b461d..da919ce9 100644 --- a/frontend/src/pages/ChatRoomList.tsx +++ b/frontend/src/pages/ChatRoomList.tsx @@ -9,10 +9,8 @@ import { Skeleton } from "@/components/ui/skeleton"; // 2a. 서버 응답 객체의 인터페이스 (스네이크 케이스) interface ServerChatRoom { id: number; - post_id: number; + post_id: number; // 서버에서 오는 필드명 title: string; - member_count: number; - updated_at: Date; } function ChatRoomList() { @@ -23,16 +21,19 @@ function ChatRoomList() { const [selectedRoom, setSelectedRoom] = useState(null); const [selectedRoomTitle, setSelectedRoomTitle] = useState(""); + const fetchData = async () => { setLoading(true); setError(null); - const token = localStorage.getItem("accessToken"); + const token = localStorage.getItem("accessToken"); // 토큰 가져오기 if (!token) { console.error("[fetchData] Authorization token not found in localStorage."); setError("로그인이 필요합니다. 로그인 후 다시 시도해주세요."); setLoading(false); setItems([]); + // 여기서 로그인 페이지로 리디렉션하거나 사용자에게 명확한 안내를 할 수 있습니다. + // 예: router.push('/login'); (Next.js 사용 시) return; } @@ -41,16 +42,18 @@ function ChatRoomList() { cache: "no-store", headers: { "Content-Type": "application/json", - "Authorization": `Bearer ${token}`, + "Authorization": `Bearer ${token}`, // Authorization 헤더 추가 }, }); if (!res.ok) { if (res.status === 401 || res.status === 403) { - const errorText = await res.text(); + const errorText = await res.text(); // 에러 본문을 읽어옴 (JSON 형태일 수도 있음) console.error(`[fetchData] Authentication error (${res.status}):`, errorText); setError(`인증에 실패했습니다 (${res.status}). 세션이 만료되었거나 권한이 없습니다. 다시 로그인해주세요.`); + // 필요하다면 여기서 로그아웃 처리 또는 로그인 페이지로 강제 이동 } else { + // 기타 HTTP 에러 const errorText = await res.text(); console.error(`[fetchData] HTTP error! status: ${res.status}, message: ${errorText}`); throw new Error(`HTTP error! status: ${res.status}, message: ${errorText}`); @@ -63,13 +66,7 @@ function ChatRoomList() { const responseData: ServerChatRoom[] = await res.json(); if (responseData && Array.isArray(responseData)) { - const sortedData = responseData.map(room => ({ - ...room, - updated_at: new Date(room.updated_at) - })).sort((a, b) => { - return b.updated_at.getTime() - a.updated_at.getTime() - }); - setItems(sortedData); + setItems(responseData); } else { setItems([]); console.warn("[fetchData] API 응답에서 data 필드를 찾을 수 없거나 형식이 배열이 아닙니다.", responseData); @@ -77,6 +74,7 @@ function ChatRoomList() { } } catch (e: any) { console.error("[fetchData] 채팅방 목록을 가져오는 중 오류 발생: ", e); + // 네트워크 에러 등의 경우 e.message가 있을 수 있음 setError(e.message || "알 수 없는 오류가 발생했습니다. 네트워크 연결을 확인해주세요."); setItems([]); } finally { @@ -86,7 +84,7 @@ function ChatRoomList() { useEffect(() => { fetchData(); - }, [retryCount]); + }, [retryCount]); // retryCount 변경 시 fetchData 재호출 const handleRetry = () => { setRetryCount((prev) => prev + 1); @@ -100,20 +98,21 @@ function ChatRoomList() { const handleCloseRoom = () => { setSelectedRoom(null); setSelectedRoomTitle(""); + fetchData(); }; return (
-
+
-
-

참여중인 채팅방 목록

+
+

참여중인 채팅방 목록

{selectedRoom && ( @@ -173,8 +172,8 @@ function ChatRoomList() {
)}
-
- ) +
+ ); } function ChatRoomSkeleton() { @@ -186,10 +185,10 @@ function ChatRoomSkeleton() {
- ) + ); } -function RefreshIcon(props: React.SVGProps) { +function RefreshIcon(props: React.SVGProps) { // props 타입 추가 return ( ) { - ) + ); } -function AlertCircleIcon(props: React.SVGProps) { +function AlertCircleIcon(props: React.SVGProps) { // props 타입 추가 return ( ) { - ) + ); } -function ChatBubbleIcon(props: React.SVGProps) { +function ChatBubbleIcon(props: React.SVGProps) { // props 타입 추가 return ( ) { - ) + ); } -export default ChatRoomList +export default ChatRoomList; \ No newline at end of file