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 = () => { my Room + {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; +`;