diff --git a/src/backend/auth-server/src/auth/constants/constants.ts b/src/backend/auth-server/src/auth/constants/constants.ts
index 2b25c54e..34b86597 100644
--- a/src/backend/auth-server/src/auth/constants/constants.ts
+++ b/src/backend/auth-server/src/auth/constants/constants.ts
@@ -17,10 +17,10 @@ export const TOKEN_TYPE = {
} as const;
export const TOKEN_EXPIRATION_TIME = {
- ACCESS: 300, // 5분
- REFRESH: 3600, // 1시간
- // ACCESS: 3600, // 1시간
- // REFRESH: 604800, // 7일
+ // ACCESS: 300, // 5분
+ // REFRESH: 3600, // 1시간
+ ACCESS: 3600, // 1시간
+ REFRESH: 604800, // 7일
} as const;
export const RAW_TOKEN_TYPE = {
diff --git a/src/frontend/src/App.tsx b/src/frontend/src/App.tsx
index 648ef893..fb57883a 100644
--- a/src/frontend/src/App.tsx
+++ b/src/frontend/src/App.tsx
@@ -17,7 +17,7 @@ import { queryClient } from '@/lib/react-query';
import { useUserStore } from './stores/useUserStore';
const ProtectedRoute = ({ children }: { children: JSX.Element }) => {
- const { user } = useUserStore();
+ const user = useUserStore(state => state.user);
if (!user) {
return ;
diff --git a/src/frontend/src/api/endpoints/room/room.api.ts b/src/frontend/src/api/endpoints/room/room.api.ts
index 408fc6e9..136d8e5e 100644
--- a/src/frontend/src/api/endpoints/room/room.api.ts
+++ b/src/frontend/src/api/endpoints/room/room.api.ts
@@ -158,6 +158,7 @@ export const roomApi = {
}
},
+ // 엘라스틱서치 검색
searchFromElastic: async (keyword: string) => {
try {
const { data } = await instance.get(`/search`, { params: { keyword } });
@@ -167,4 +168,36 @@ export const roomApi = {
throw error;
}
},
+
+ // 방 초대 수락
+ acceptInvitation: async (roomId: number, me: number, senderId: number, roomCode: string) => {
+ try {
+ const { data } = await instance.post(`/rooms/invitations/accept`, {
+ senderId,
+ receiverId: me,
+ roomId,
+ roomCode,
+ });
+ return data;
+ } catch (error) {
+ logAxiosError(error, ErrorType.ROOM, '방 초대 수락 실패');
+ throw error;
+ }
+ },
+
+ // 방 초대 거절
+ rejectInvitation: async (roomId: number, me: number, senderId: number, roomCode: string) => {
+ try {
+ const { data } = await instance.post(`/rooms/invitations/reject`, {
+ senderId,
+ receiverId: me,
+ roomId,
+ roomCode,
+ });
+ return data;
+ } catch (error) {
+ logAxiosError(error, ErrorType.ROOM, '방 초대 거절 실패');
+ throw error;
+ }
+ },
};
diff --git a/src/frontend/src/components/LeftNavBar/index.css.ts b/src/frontend/src/components/LeftNavBar/index.css.ts
index b8bed02d..43941b7a 100644
--- a/src/frontend/src/components/LeftNavBar/index.css.ts
+++ b/src/frontend/src/components/LeftNavBar/index.css.ts
@@ -30,6 +30,7 @@ export const ButtonWrapper = styled.div`
`;
export const ButtonBox = styled.div<{ $isActive: boolean }>`
+ position: relative;
width: 40px;
height: 40px;
display: flex;
diff --git a/src/frontend/src/components/LeftNavBar/index.tsx b/src/frontend/src/components/LeftNavBar/index.tsx
index f5764111..cce757e0 100644
--- a/src/frontend/src/components/LeftNavBar/index.tsx
+++ b/src/frontend/src/components/LeftNavBar/index.tsx
@@ -6,12 +6,16 @@ import PlaylistLineIcon from '@/assets/img/PlaylistLine.svg';
import UsersFillIcon from '@/assets/img/UsersFill.svg';
import UsersLineIcon from '@/assets/img/UsersLine.svg';
import { ButtonBox, ButtonContainer, ButtonWrapper } from './index.css';
+import { NotificationCount } from '@/ui/Common.css';
+import { useMyRoomsStore } from '@/stores/useMyRoomsStore';
export const LeftNavBar = () => {
const location = useLocation();
const isHome = location.pathname === '/';
const isMyRoom = location.pathname === '/my-room';
const isFriend = location.pathname === '/friend';
+ const newChatCount = useMyRoomsStore((state) => state.newChatCount);
+
return (
@@ -27,6 +31,7 @@ export const LeftNavBar = () => {
+ {newChatCount > 0 && {newChatCount}}
MY
diff --git a/src/frontend/src/components/Modal/RegisterSuccessModal/index.tsx b/src/frontend/src/components/Modal/RegisterSuccessModal/index.tsx
index 7eb3d9af..a269a3ab 100644
--- a/src/frontend/src/components/Modal/RegisterSuccessModal/index.tsx
+++ b/src/frontend/src/components/Modal/RegisterSuccessModal/index.tsx
@@ -12,6 +12,9 @@ export const RegisterSuccessModal = ({ onCancel }: IRegisterSuccessModal) => {
const navigate = useNavigate();
const handleConfirm = () => {
+ if (window.webkit?.messageHandlers?.messageHandler) {
+ window.webkit.messageHandlers.messageHandler.postMessage('LoginSuccess');
+ }
navigate('/login');
onCancel();
};
diff --git a/src/frontend/src/components/RoomDetail/index.tsx b/src/frontend/src/components/RoomDetail/index.tsx
index 6cd2e2ea..58063d5f 100644
--- a/src/frontend/src/components/RoomDetail/index.tsx
+++ b/src/frontend/src/components/RoomDetail/index.tsx
@@ -16,7 +16,7 @@ import { useCurrentRoomStore } from '@/stores/useCurrentRoomStore';
import DefaultProfile from '@/assets/img/DefaultProfile.svg';
export const RoomDetail = () => {
- const { currentRoom } = useCurrentRoomStore();
+ const currentRoom = useCurrentRoomStore(state => state.currentRoom);
const roomInfo = currentRoom?.roomDetails.roomInfo[0];
const handleDescription = () => {
diff --git a/src/frontend/src/components/Sidebar/Chating/index.tsx b/src/frontend/src/components/Sidebar/Chating/index.tsx
index 5a00ea1c..502619ed 100644
--- a/src/frontend/src/components/Sidebar/Chating/index.tsx
+++ b/src/frontend/src/components/Sidebar/Chating/index.tsx
@@ -26,6 +26,7 @@ export const Chat = () => {
if (isInitialLoad) {
chatContainerRef.current.scrollTop = chatContainerRef.current.scrollHeight;
+ setnewMessageCount(0);
setIsInitialLoad(false);
}
}, [messages, isInitialLoad]);
@@ -58,6 +59,7 @@ export const Chat = () => {
top: chatContainerRef.current.scrollHeight,
behavior: 'smooth',
});
+ setnewMessageCount(0);
};
// 메시지 추가
diff --git a/src/frontend/src/pages/MyRoomPage/index.tsx b/src/frontend/src/pages/MyRoomPage/index.tsx
index 33f58ebd..c492071d 100644
--- a/src/frontend/src/pages/MyRoomPage/index.tsx
+++ b/src/frontend/src/pages/MyRoomPage/index.tsx
@@ -11,12 +11,13 @@ import { AlertDescription } from '@/components/common/AlertDescription';
export const MyRoomPage = () => {
const { data } = useMyRooms();
const { user } = useUserStore();
- const { myRooms, setMyRooms, clearMyRooms } = useMyRoomsStore();
+ const { myRooms, setMyRooms, clearMyRooms, resetNewChatCount } = useMyRoomsStore();
const showSkeleton = useDelayedLoading(data);
useEffect(() => {
if (data) {
setMyRooms(data);
+ resetNewChatCount();
} else {
clearMyRooms();
}
diff --git a/src/frontend/src/pages/RoomPage/index.tsx b/src/frontend/src/pages/RoomPage/index.tsx
index c61d9a92..a56d73f4 100644
--- a/src/frontend/src/pages/RoomPage/index.tsx
+++ b/src/frontend/src/pages/RoomPage/index.tsx
@@ -11,6 +11,8 @@ import { useCurrentRoom } from '@/hooks/queries/useCurrentRoom';
import { useVideoStore } from '@/stores/useVideoStore';
import { getVideoQueueFromPlaylist } from '@/utils/playlistUtils';
import { useWebSocketStore } from '@/stores/useWebSocketStore';
+import { useMyRoomsStore } from '@/stores/useMyRoomsStore';
+
export const RoomPage = () => {
const [searchParams] = useSearchParams();
const roomCode = searchParams.get('code');
@@ -18,7 +20,7 @@ export const RoomPage = () => {
console.log('RoomPage:', room);
const { setVideoQueue } = useVideoStore();
const { subscribeRoomPlaylistUpdate } = useWebSocketStore();
- const { roomId } = useCurrentRoomStore();
+ const roomId = useCurrentRoomStore(state => state.roomId);
useEffect(() => {
if (room) {
@@ -35,6 +37,9 @@ export const RoomPage = () => {
}
return () => {
useCurrentRoomStore.getState().clearCurrentRoom();
+ if (roomId) {
+ useMyRoomsStore.getState().subscribeRoom(roomId);
+ }
};
}, [room]);
diff --git a/src/frontend/src/sentry.ts b/src/frontend/src/sentry.ts
index 4c882924..90ad960d 100644
--- a/src/frontend/src/sentry.ts
+++ b/src/frontend/src/sentry.ts
@@ -28,7 +28,11 @@ Sentry.init({
// Set `tracePropagationTargets` to control for which URLs trace propagation should be enabled
// tracePropagationTargets: [/^\//, /^https:\/\/yourserver\.io\/api/],
- tracePropagationTargets: ['localhost'],
+ tracePropagationTargets: [
+ 'localhost',
+ /^http:\/\/kicktube\.site/,
+ /^http:\/\/13\.231\.39\.189:8000\/api/,
+ ],
// Capture Replay for 100% of all sessions,
// plus for 100% of sessions with an error
diff --git a/src/frontend/src/stores/useAuthStore.ts b/src/frontend/src/stores/useAuthStore.ts
index 181c8270..dd8cda0f 100644
--- a/src/frontend/src/stores/useAuthStore.ts
+++ b/src/frontend/src/stores/useAuthStore.ts
@@ -1,6 +1,11 @@
import axios from 'axios';
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
+import { useFriendStore } from './useFriendStore';
+import { useNotificationStore } from './useNotificationStore';
+import { useUserStore } from './useUserStore';
+import { useMyRoomsStore } from './useMyRoomsStore';
+import { useWebSocketStore } from './useWebSocketStore';
const instance = axios.create({
baseURL: import.meta.env.VITE_API_URL,
@@ -31,6 +36,11 @@ export const useAuthStore = create(
},
clear: () => {
set({ accessToken: null });
+ useFriendStore.getState().clear();
+ useNotificationStore.getState().clear();
+ useMyRoomsStore.getState().clearMyRooms();
+ useUserStore.getState().clearProfile();
+ useWebSocketStore.getState().disconnect();
},
}),
{
diff --git a/src/frontend/src/stores/useMyRoomsStore.ts b/src/frontend/src/stores/useMyRoomsStore.ts
index 68e90ce4..83f468da 100644
--- a/src/frontend/src/stores/useMyRoomsStore.ts
+++ b/src/frontend/src/stores/useMyRoomsStore.ts
@@ -9,16 +9,21 @@ import { useCurrentRoomStore } from './useCurrentRoomStore';
interface MyRoomsStore {
myRooms: MyRoomDto[];
+ newChatCount: number;
setMyRooms: (rooms: MyRoomDto[]) => void;
fetchMyRooms: () => Promise;
+ subscribeRoom: (roomId: number) => void;
+ subscribeRooms: (roomIds: number[]) => void;
+ resetNewChatCount: () => void;
clearMyRooms: () => void;
}
export const useMyRoomsStore = create(
persist(
- set => ({
+ (set, get) => ({
myRooms: [],
-
+ newChatCount: 0,
+ newChatRooms: {},
// 내 방 목록 설정
setMyRooms: (rooms: MyRoomDto[]) => set({ myRooms: rooms }),
@@ -30,10 +35,11 @@ export const useMyRoomsStore = create(
console.log('myRooms', myRooms);
const myRoomIds = myRooms.map(room => room.roomId);
const currentRoomId = useCurrentRoomStore.getState().roomId;
- if (currentRoomId) { // 현재 /rooom에서 방을 보고 있다면 구독을 currentRoomStore에서 처리
+ if (currentRoomId) {
+ // 현재 /rooom에서 방을 보고 있다면 구독을 currentRoomStore에서 처리
myRoomIds.splice(myRoomIds.indexOf(currentRoomId), 1);
}
- useWebSocketStore.getState().subscribeRooms(myRoomIds);
+ get().subscribeRooms(myRoomIds);
return myRooms;
} catch (error) {
console.error('Failed to fetch rooms:', error);
@@ -41,8 +47,31 @@ export const useMyRoomsStore = create(
}
},
+ // 방 채팅 구독
+ subscribeRoom: (roomId: number) => {
+ const destination = `/topic/room/${roomId}/chat`;
+ useWebSocketStore.getState().subTopic(destination, message => {
+ console.log('subscribeRoomChat', message);
+ set(state => ({
+ newChatCount: state.newChatCount + 1,
+ }));
+ });
+ },
+
+ // 내가 속한 방 채팅 구독
+ subscribeRooms: (roomIds: number[]) => {
+ if (!roomIds) return;
+ roomIds.forEach(roomId => {
+ get().subscribeRoom(roomId);
+ });
+ },
+
+ resetNewChatCount: () => {
+ set({ newChatCount: 0 });
+ },
+
// 내 방 목록 초기화
- clearMyRooms: () => set({ myRooms: [] }),
+ clearMyRooms: () => set({ myRooms: [], newChatCount: 0 }),
}),
{
name: 'my-rooms-storage',
diff --git a/src/frontend/src/stores/useWebSocketStore.ts b/src/frontend/src/stores/useWebSocketStore.ts
index 0a49c645..9b624ef6 100644
--- a/src/frontend/src/stores/useWebSocketStore.ts
+++ b/src/frontend/src/stores/useWebSocketStore.ts
@@ -25,8 +25,6 @@ interface WebSocketStore {
disconnect: () => void;
subTopic: (destination: string, callback: (message: T) => void) => void;
- subscribeRoom: (roomId: number) => void;
- subscribeRooms: (roomIds: number[]) => void;
subscribeInvitations: (userId: number, callback: (message: T) => void) => void;
subscribeFriendConnection: (userId: number, callback: (message: T) => void) => void;
subscribeRoomUserInfo: (
@@ -45,6 +43,7 @@ interface WebSocketStore {
playlist: { order: number; url: string; title: string; youtuber: string }[];
}) => void,
) => void;
+ unsubscribe: (destination: string) => void;
unsubscribeAll: () => void;
pubTopic: (destination: string, message: string) => void;
}
@@ -173,14 +172,6 @@ export const useWebSocketStore = create((set, get) => ({
get().subTopic(destination, callback);
},
- // 방 채팅 구독
- subscribeRoom: (roomId: number) => {
- const destination = `/topic/room/${roomId}/chat`;
- get().subTopic(destination, message => {
- console.log('subscribeRoomChat', message);
- });
- },
-
// 채팅방 내 신규 유저 정보 구독
subscribeRoomUserInfo: (
roomId: number,
@@ -212,12 +203,13 @@ export const useWebSocketStore = create((set, get) => ({
get().subTopic(destination, callback);
},
- // 내가 속한 방 채팅 구독
- subscribeRooms: (roomIds: number[]) => {
- if (!roomIds) return;
- roomIds.forEach(roomId => {
- get().subscribeRoom(roomId);
- });
+ unsubscribe: (destination: string) => {
+ const { client } = get();
+ if (!client) return;
+
+ const subscription = get().subscriptions.get(destination);
+ subscription?.unsubscribe();
+ get().subscriptions.delete(destination);
},
// 구독 해제
diff --git a/src/frontend/src/types/window.d.ts b/src/frontend/src/types/window.d.ts
new file mode 100644
index 00000000..37a38c74
--- /dev/null
+++ b/src/frontend/src/types/window.d.ts
@@ -0,0 +1,20 @@
+interface WebKitMessageHandler {
+ postMessage: (message: string) => void;
+}
+
+interface WebKitMessageHandlers {
+ closeModal: WebKitMessageHandler;
+ [key: string]: WebKitMessageHandler;
+}
+
+interface WebKit {
+ messageHandlers: WebKitMessageHandlers;
+}
+
+declare global {
+ interface Window {
+ webkit?: WebKit;
+ }
+}
+
+export {};
diff --git a/src/frontend/src/ui/Common.css.ts b/src/frontend/src/ui/Common.css.ts
index 0a5609e5..01ee081a 100644
--- a/src/frontend/src/ui/Common.css.ts
+++ b/src/frontend/src/ui/Common.css.ts
@@ -111,3 +111,19 @@ export const SkeletonBase = styled.div`
export const Divider = styled.div`
height: 1rem;
`;
+
+export const NotificationCount = styled.div`
+ position: absolute;
+ top: 2px;
+ right: 2px;
+ width: 18px;
+ height: 18px;
+ border-radius: 50%;
+ background-color: var(--palette-status-negative);
+ color: var(--palette-static-white);
+ font-size: 0.75rem;
+ font-weight: 600;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+`;