참여 중인 방
- {myRoomList.filter(room => room.creator !== user?.nickname).length > 0 ? (
- myRoomList
+ {myRooms.filter(room => room.creator !== user?.nickname).length > 0 ? (
+ myRooms
.filter(room => room.creator !== user?.nickname)
.map(room =>
)
) : (
diff --git a/src/frontend/src/pages/NotFoundPage/index.css.ts b/src/frontend/src/pages/NotFoundPage/index.css.ts
new file mode 100644
index 00000000..575fddb8
--- /dev/null
+++ b/src/frontend/src/pages/NotFoundPage/index.css.ts
@@ -0,0 +1,25 @@
+import { styled } from 'styled-components';
+
+export const Container = styled.div`
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ height: 100vh;
+ text-align: center;
+`;
+
+export const LogoImg = styled.img`
+ width: 150px;
+ margin-bottom: 2rem;
+`;
+
+export const Title = styled.h1`
+ font-size: 1.5rem;
+ margin-bottom: 1.5rem;
+`;
+
+export const Message = styled.p`
+ margin-bottom: 3rem;
+ color: var(--palette-font-gray);
+`;
diff --git a/src/frontend/src/pages/NotFoundPage/index.tsx b/src/frontend/src/pages/NotFoundPage/index.tsx
index cf1795a9..f19b3712 100644
--- a/src/frontend/src/pages/NotFoundPage/index.tsx
+++ b/src/frontend/src/pages/NotFoundPage/index.tsx
@@ -1,3 +1,25 @@
+import { useNavigate } from 'react-router-dom';
+import Logo from '@/assets/img/Logo.svg';
+import { CommonButton } from '@/components/common/Button';
+import { ButtonColor } from '@/types/enums/ButtonColor';
+import { Container, LogoImg, Title, Message } from './index.css';
+
export const NotFoundPage = () => {
- return
404 Not Found입니다.
;
+ const navigate = useNavigate();
+
+ return (
+
+
+ 원하시는 페이지를 찾을 수 없습니다.
+ 요청하신 페이지가 사라졌거나, 잘못된 경로를 이용하셨습니다.
+ navigate('/')}
+ width="160px"
+ height="40px"
+ color={ButtonColor.ORANGE}
+ >
+ 홈으로 가기
+
+
+ );
};
diff --git a/src/frontend/src/pages/RoomPage/index.tsx b/src/frontend/src/pages/RoomPage/index.tsx
index af4342bc..de893097 100644
--- a/src/frontend/src/pages/RoomPage/index.tsx
+++ b/src/frontend/src/pages/RoomPage/index.tsx
@@ -3,14 +3,34 @@ import { YouTubePlayer } from '@/components/YoutubePlayer';
import { RoomDetail } from '@/components/RoomDetail';
import { Container, Wrapper } from './index.css';
+import { useCurrentRoomStore } from '@/stores/useCurrentRoomStore';
+import { useEffect } from 'react';
+import { useSearchParams } from 'react-router-dom';
+import { useCurrentRoom } from '@/hooks/queries/useCurrentRoom';
+
+export const RoomPage = () => {
+ const [searchParams] = useSearchParams();
+ const roomCode = searchParams.get('code');
+ const { data: room } = useCurrentRoom(roomCode);
+ console.log('RoomPage:', room);
+
+ const { messages } = useCurrentRoomStore(); // NOTE: 채팅 메시지 테스트용
+
+ useEffect(() => {
+ if (room) {
+ useCurrentRoomStore.getState().setCurrentRoom(room);
+ useCurrentRoomStore.getState().subscribeChat(room.roomDetails.roomInfo[0]?.roomId);
+ }
+ }, [room]);
-export const Room = () => {
return (
<>
+ {/* NOTE: 채팅 메시지 테스트 용 */}
+ {messages && messages.map((message, i) => - {message.message}
)}
diff --git a/src/frontend/src/pages/SettingPage/index.tsx b/src/frontend/src/pages/SettingPage/index.tsx
index 84ad2538..2d2a9ae8 100644
--- a/src/frontend/src/pages/SettingPage/index.tsx
+++ b/src/frontend/src/pages/SettingPage/index.tsx
@@ -1,3 +1,24 @@
+import { Wrapper, Container, Title, CommonUl, CommonLi, Copyright } from '@/ui/Common.css';
+import { useAuth } from '@/hooks/queries/useAuth';
+
export const SettingPage = () => {
- return
설정
;
+ const { logout } = useAuth();
+
+ const handleLogout = () => {
+ logout.mutate();
+ };
+
+ return (
+
+
+ 설정
+
+
+ 로그아웃
+
+
+ © 2025. KICKZO. All rights reserved.
+
+
+ );
};
diff --git a/src/frontend/src/stores/useChatStore.ts b/src/frontend/src/stores/useChatStore.ts
deleted file mode 100644
index 5b8da45c..00000000
--- a/src/frontend/src/stores/useChatStore.ts
+++ /dev/null
@@ -1,104 +0,0 @@
-import { create } from 'zustand';
-import SockJS from 'sockjs-client';
-import Stomp, { Client, Message } from 'stompjs';
-import { UserRole } from '@/types/enums/UserRole';
-
-const API_BASE_URL = 'http://localhost:8000';
-
-const initialChatData = Array.from({ length: 100 }, (_, i) => ({
- role: i % 2 === 0 ? UserRole.MEMBER : UserRole.CREATOR,
- nickname: `User${i}`,
- time: `10:${(i % 60).toString().padStart(2, '0')}`,
- text: `This is message number ${i}`,
-}));
-
-interface ChatState {
- chatData: { role: UserRole; nickname: string; time: string; text: string }[];
- stompClient: Client | null;
- status: string;
- userId: string;
- roomId: string;
- connect: () => void;
- disconnect: () => void;
- sendMessage: (message: string) => void;
- addMessage: (message: string, sender?: string) => void;
-}
-
-export const useChatStore = create
((set, get) => ({
- chatData: initialChatData,
- stompClient: null,
- status: 'Disconnected',
- userId: `user${Math.floor(Math.random() * 1000)}`,
- roomId: '1',
-
- // WebSocket 연결
- connect: () => {
- if (get().stompClient) {
- console.warn('웹소켓이 이미 연결되어 있습니다.');
- return;
- }
- const socket = new SockJS(`${API_BASE_URL}/api/chat/ws`);
- const client = Stomp.over(socket);
-
- client.connect({}, () => {
- console.log('✅ WebSocket Connected');
- set({ status: 'Connected', stompClient: client });
-
- const subscription = client.subscribe(`/topic/${get().roomId}`, (message: Message) => {
- try {
- const payload = JSON.parse(message.body);
- get().addMessage(payload.content, payload.userId);
- } catch (error) {
- console.error('❌ 메시지 처리 오류:', error);
- }
- });
-
- client.send(
- '/app/joinRoom',
- {},
- JSON.stringify({ roomId: get().roomId, userId: get().userId }),
- );
-
- return () => {
- subscription.unsubscribe();
- };
- });
- },
-
- // WebSocket 연결 해제
- disconnect: () => {
- const client = get().stompClient;
- if (client && client.connected) {
- client.disconnect(() => {
- console.log('❌ WebSocket Disconnected');
- set({ status: 'Disconnected', stompClient: null });
- });
- } else {
- console.warn('웹소켓이 이미 연결되지 않았습니다.');
- }
- },
-
- // 메시지 전송
- sendMessage: (message: string) => {
- const client = get().stompClient;
- if (!client || !client.connected) {
- console.warn('웹소켓이 연결되지 않아 메시지를 보낼 수 없습니다.');
- return;
- }
- client.send('/app/sendMessage', {}, JSON.stringify({ roomId: get().roomId, message }));
- },
-
- // 새 메시지 추가 (수신 시 또는 사용자가 보낼 때)
- addMessage: (message: string, sender = 'Me') => {
- const newChat = {
- role: sender === 'Me' ? UserRole.MEMBER : UserRole.CREATOR,
- nickname: sender,
- time: new Date().toLocaleTimeString().slice(0, 5),
- text: message,
- };
- if (sender === 'Me') {
- get().sendMessage(message);
- }
- set(state => ({ chatData: [...state.chatData, newChat] }));
- },
-}));
diff --git a/src/frontend/src/stores/useCurrentRoomStore.ts b/src/frontend/src/stores/useCurrentRoomStore.ts
new file mode 100644
index 00000000..45016652
--- /dev/null
+++ b/src/frontend/src/stores/useCurrentRoomStore.ts
@@ -0,0 +1,49 @@
+// TODO: 새로운 사람 등장: user-info
+// TODO: 방 정보 변경: room-update
+// TODO: 영상 시간 변경: playlistTime
+// TODO: 플레이리스트 변경: playlist-update
+// TODO: 역할 변경: role-chage
+
+import { create } from 'zustand';
+import { CurrentRoomDto } from '@/api/endpoints/room/room.interface';
+import { useWebSocketStore } from './useWebSocketStore';
+import { useUserStore } from './useUserStore';
+import { ReceiveMessageDto, SendMessageDto } from '@/types/dto/Message.dto';
+
+interface CurrentRoomStore {
+ currentRoom: CurrentRoomDto | null;
+ messages: ReceiveMessageDto[];
+ sendMessage: (messageDto: SendMessageDto) => void;
+ subscribeChat: (roomId: number) => void;
+ addMessage: (message: ReceiveMessageDto) => void;
+ setCurrentRoom: (room: CurrentRoomDto) => void;
+ clearCurrentRoom: () => void;
+}
+
+export const useCurrentRoomStore = create((set, get) => ({
+ currentRoom: null,
+ messages: [],
+
+ setCurrentRoom: (room: CurrentRoomDto) => set({ currentRoom: room }),
+ clearCurrentRoom: () => set({ currentRoom: null }),
+
+ subscribeChat: (roomId: number) => {
+ const destination = `/topic/room/${roomId}/chat`;
+ useWebSocketStore.getState().subTopic(destination, (message: ReceiveMessageDto) => {
+ get().addMessage(message);
+ });
+ },
+
+ sendMessage: (messageDto: SendMessageDto) => {
+ const { client } = useWebSocketStore.getState();
+ if (!client) return;
+
+ const { user } = useUserStore.getState();
+ if (!user) return;
+
+ client.send(`/app/send-message`, {}, JSON.stringify(messageDto));
+ },
+
+ addMessage: (message: ReceiveMessageDto) =>
+ set(state => ({ messages: [...state.messages, message] })),
+}));
diff --git a/src/frontend/src/stores/useMyRoomsStore.ts b/src/frontend/src/stores/useMyRoomsStore.ts
new file mode 100644
index 00000000..0555d51d
--- /dev/null
+++ b/src/frontend/src/stores/useMyRoomsStore.ts
@@ -0,0 +1,46 @@
+// - [x] 방 들어갔을 때 구독 처리
+
+import { MyRoomDto } from '@/api/endpoints/room/room.interface';
+import { create } from 'zustand';
+import { roomApi } from '@/api/endpoints/room/room.api';
+import { persist } from 'zustand/middleware';
+import { useWebSocketStore } from './useWebSocketStore';
+
+interface MyRoomsStore {
+ myRooms: MyRoomDto[];
+ setMyRooms: (rooms: MyRoomDto[]) => void;
+ fetchMyRooms: () => Promise;
+ clearMyRooms: () => void;
+}
+
+export const useMyRoomsStore = create(
+ persist(
+ set => ({
+ myRooms: [],
+
+ // 내 방 목록 설정
+ setMyRooms: (rooms: MyRoomDto[]) => set({ myRooms: rooms }),
+
+ // 내 방 목록 조회
+ fetchMyRooms: async () => {
+ try {
+ const myRooms = await roomApi.getMyRooms();
+ set({ myRooms });
+ console.log('myRooms', myRooms);
+ const myRoomIds = myRooms.map(room => room.roomId);
+ useWebSocketStore.getState().subscribeRooms(myRoomIds);
+ return myRooms;
+ } catch (error) {
+ console.error('Failed to fetch rooms:', error);
+ return [];
+ }
+ },
+
+ // 내 방 목록 초기화
+ clearMyRooms: () => set({ myRooms: [] }),
+ }),
+ {
+ name: 'my-rooms-storage',
+ },
+ ),
+);
diff --git a/src/frontend/src/stores/useUserStore.ts b/src/frontend/src/stores/useUserStore.ts
index e51e516c..1597fb47 100644
--- a/src/frontend/src/stores/useUserStore.ts
+++ b/src/frontend/src/stores/useUserStore.ts
@@ -10,7 +10,7 @@ interface UserStore {
updateMyProfile: (
updateUserRequestDto: UpdateUserRequestDto,
) => Promise;
- clear: () => void;
+ clearProfile: () => void;
}
export const useUserStore = create(
@@ -33,7 +33,7 @@ export const useUserStore = create(
set({ user: data });
return data;
},
- clear: () => {
+ clearProfile: () => {
set({ user: null });
},
}),
diff --git a/src/frontend/src/stores/useWebSocketStore.ts b/src/frontend/src/stores/useWebSocketStore.ts
new file mode 100644
index 00000000..895fcd09
--- /dev/null
+++ b/src/frontend/src/stores/useWebSocketStore.ts
@@ -0,0 +1,140 @@
+//- [x] 웹소켓 연결 후 서버에 유저 아이디 전송
+//- [x] 내 채팅방 목록 조회 후 구독하기
+//- [x] 초대 구독하기
+//- [x] 친구 접속 알림
+// v2 [ ] dm 받기
+// v2 [ ] dm 보내기
+
+import SockJS from 'sockjs-client';
+import Stomp from 'stompjs';
+import { Client } from 'stompjs';
+import { create } from 'zustand';
+import { useUserStore } from './useUserStore';
+
+interface WebSocketStore {
+ socket: WebSocket | null;
+ client: Client | null;
+ subscriptions: Map; // 구독 관리
+
+ // 연결
+ connect: () => void;
+ disconnect: () => void;
+
+ subTopic: (destination: string, callback: (message: T) => void) => void;
+ subscribeRoom: (roomId: number) => void;
+ subscribeRooms: (roomIds: number[]) => void;
+ subscribeInvitations: (userId: number) => void;
+ subscribeFriendConnection: (userId: number) => void;
+ unsubscribeAll: () => void;
+}
+
+export const useWebSocketStore = create((set, get) => ({
+ socket: null,
+ client: null,
+ subscriptions: new Map(),
+
+ // 연결
+ connect: () => {
+ if (get().client?.connected) {
+ get().disconnect();
+ return;
+ }
+ // 기존 소켓 정리
+ if (get().socket) {
+ get().socket?.close();
+ }
+
+ const socket = new SockJS(import.meta.env.VITE_WEBSOCKET_URL);
+ const client = Stomp.over(socket);
+
+ client.connect(
+ {},
+ () => {
+ console.log('✅ WebSocket Connected');
+ set({ client, socket });
+
+ const userId = useUserStore.getState().user?.userId;
+ if (userId) {
+ client.send('/app/connect', {}, JSON.stringify({ userId }));
+ get().subscribeInvitations(userId);
+ get().subscribeFriendConnection(userId);
+ }
+ },
+ error => {
+ console.error('❌ WebSocket Connection Error:', error);
+ set({ client: null });
+ },
+ );
+ },
+
+ // 연결 해제
+ disconnect: () => {
+ const store = get();
+ if (!store.client) return;
+
+ store.unsubscribeAll();
+
+ store.client.disconnect(() => {
+ set({ client: null });
+ });
+ },
+
+ // 토픽 구독
+ subTopic: (destination: string, callback: (message: T) => void) => {
+ const { client } = get();
+ if (!client) return;
+
+ if (get().subscriptions.has(destination)) {
+ const existingSub = get().subscriptions.get(destination);
+ existingSub?.unsubscribe();
+ get().subscriptions.delete(destination);
+ }
+
+ const subscription = client.subscribe(destination, message => {
+ const receivedMessage = JSON.parse(message.body);
+
+ callback(receivedMessage);
+ });
+
+ get().subscriptions.set(destination, subscription);
+ },
+
+ // 친구 접속 알림 구독
+ subscribeFriendConnection: (userId: number) => {
+ const destination = `/topic/user/${userId}/friend-state`;
+ get().subTopic(destination, message => {
+ console.log('subscribeFriendConnection', message);
+ });
+ },
+
+ // 초대 구독
+ subscribeInvitations: (userId: number) => {
+ const destination = `/topic/user/${userId}/notification`;
+ get().subTopic(destination, message => {
+ console.log('subscribeInvitations', message);
+ });
+ },
+
+ // 방 채팅 구독
+ subscribeRoom: (roomId: number) => {
+ const destination = `/topic/room/${roomId}/chat`;
+ get().subTopic(destination, message => {
+ console.log('subscribeRoom', message);
+ });
+ },
+
+ // 내가 속한 방 채팅 구독
+ subscribeRooms: (roomIds: number[]) => {
+ if (!roomIds) return;
+
+ roomIds.forEach(roomId => {
+ get().subscribeRoom(roomId);
+ });
+ },
+
+ // 구독 해제
+ unsubscribeAll: () => {
+ get().subscriptions.forEach(sub => sub.unsubscribe());
+ set({ subscriptions: new Map() });
+ },
+}));
diff --git a/src/frontend/src/types/dto/Message.dto.ts b/src/frontend/src/types/dto/Message.dto.ts
new file mode 100644
index 00000000..e48073cd
--- /dev/null
+++ b/src/frontend/src/types/dto/Message.dto.ts
@@ -0,0 +1,10 @@
+export interface SendMessageDto {
+ roomId: number;
+ userId: number;
+ content?: string;
+ message?: string;
+}
+
+export interface ReceiveMessageDto extends SendMessageDto {
+ timestamp: string;
+}
diff --git a/src/frontend/src/ui/Common.css.ts b/src/frontend/src/ui/Common.css.ts
new file mode 100644
index 00000000..8134338c
--- /dev/null
+++ b/src/frontend/src/ui/Common.css.ts
@@ -0,0 +1,65 @@
+import { styled } from 'styled-components';
+
+export const Wrapper = styled.div`
+ display: flex;
+ justify-content: center;
+ width: 100%;
+ height: 100%;
+ padding: 1rem;
+`;
+
+export const Container = styled.div`
+ width: 100%;
+ max-width: 720px;
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+`;
+
+export const Title = styled.h1`
+ font-size: 2rem;
+ font-weight: 600;
+ padding: 1.5rem 0;
+`;
+
+export const SubTitle = styled.h1`
+ font-size: 1.5rem;
+ font-weight: 600;
+ padding: 2rem 0 1rem;
+`;
+
+export const CommonParagraph = styled.p`
+ font-size: 1rem;
+`;
+
+export const CommonUl = styled.ul`
+ width: 100%;
+ max-height: 300px;
+ overflow-y: auto;
+ border-radius: 10px;
+ text-align: left;
+ padding: 10px;
+ background-color: var(--palette-static-white);
+ border: 1px solid var(--palette-line-solid-alternative);
+`;
+
+export const CommonLi = styled.li<{ $hoverColor?: string }>`
+ padding: 12px;
+ border-radius: 10px;
+ cursor: pointer;
+ display: flex;
+ transition: all 0.2s ease;
+
+ &:hover {
+ font-weight: 600;
+ color: var(--palette-static-white);
+ background-color: ${({ $hoverColor = 'var(--palette-line-solid-alternative)' }) => $hoverColor};
+ }
+`;
+
+export const Copyright = styled.p`
+ font-size: 0.875rem;
+ color: var(--palette-font-gray);
+ text-align: center;
+ margin-top: 2rem;
+`;
diff --git a/src/infra/stomp-tester/stomp-tester.html b/src/infra/stomp-tester/stomp-tester.html
new file mode 100644
index 00000000..f12d1a57
--- /dev/null
+++ b/src/infra/stomp-tester/stomp-tester.html
@@ -0,0 +1,370 @@
+
+
+
+
+
+ STOMP 테스트
+
+
+
+
+
+
+
+
+
+
WebSocket CONNECT
+
+
+
+
+
+
SUBSCRIBE
+
+
+
+
+
+
자주 사용하는 요청
+
+
+
+
+
SEND MESSAGE
+
+
+
+
+
+
+
+
+
+
+
MESSAGES
+
+
+
+
+
+