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
44 changes: 38 additions & 6 deletions src/frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,30 @@
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
import * as Sentry from '@sentry/react';
import '@/App.css';
import { Layout } from '@/pages/Layout';
import { LoginPage } from '@/pages/LoginPage';
import { RegisterPage } from '@/pages/RegisterPage';
import { NotFoundPage } from '@/pages/NotFoundPage';
import { HomePage } from '@/pages/HomePage';
import { Room } from '@/pages/RoomPage';
import { RoomPage } from '@/pages/RoomPage';
import { FriendPage } from '@/pages/FriendPage';
import { SettingPage } from '@/pages/SettingPage';
import { MyRoomPage } from '@/pages/MyRoomPage';
import { PasswordResetPage } from '@/pages/PasswordResetPage';
import { SearchPage } from '@/pages/SearchPage';
import { QueryClientProvider } from '@tanstack/react-query';
import { queryClient } from '@/lib/react-query';
import { useUserStore } from './stores/useUserStore';

const ProtectedRoute = ({ children }: { children: JSX.Element }) => {
const { user } = useUserStore();

if (!user) {
return <Navigate to="/login" replace />;
}

return children;
};

function App() {
const SentryRoutes = Sentry.withSentryReactRouterV6Routing(Routes);
Expand All @@ -27,11 +38,32 @@ function App() {
<Route path="/password-reset" element={<PasswordResetPage />} />
<Route path="/" element={<Layout />}>
<Route index element={<HomePage />} />
<Route path="my-room" element={<MyRoomPage />} />
<Route path="room" element={<Room />} />
<Route path="friend" element={<FriendPage />} />
<Route
path="my-room"
element={
<ProtectedRoute>
<MyRoomPage />
</ProtectedRoute>
}
/>
<Route path="room" element={<RoomPage />} />
<Route
path="friend"
element={
<ProtectedRoute>
<FriendPage />
</ProtectedRoute>
}
/>
<Route path="search" element={<SearchPage />} />
<Route path="setting" element={<SettingPage />} />
<Route
path="setting"
element={
<ProtectedRoute>
<SettingPage />
</ProtectedRoute>
}
/>
</Route>
<Route path="/*" element={<NotFoundPage />} />
</SentryRoutes>
Expand Down
10 changes: 8 additions & 2 deletions src/frontend/src/api/endpoints/auth/auth.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,14 @@ export const authApi = {

// 토큰 갱신
refreshToken: async () => {
const { data } = await instance.post('/auth/token/refresh');
return data;
try {
const { data } = await instance.post('/auth/token/refresh');
return data;
} catch (error) {
console.error('토큰 갱신 실패:', error);
useAuthStore.getState().clear();
throw error;
}
},

// 토큰 검증
Expand Down
4 changes: 2 additions & 2 deletions src/frontend/src/api/endpoints/room/room.api.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import instance from '@/api/axios.instance';
import { PlaylistDto, RoomRequestDto, MyRoomDto, RoomDto } from './room.interface';
import { PlaylistDto, RoomRequestDto, MyRoomDto, RoomDto, CurrentRoomDto } from './room.interface';

export const roomApi = {
// 방 생성
Expand All @@ -25,7 +25,7 @@ export const roomApi = {

// 방 입장
joinRoom: async (roomCode: string) => {
const { data } = await instance.post(`/rooms/join`, { roomCode });
const { data } = await instance.post<CurrentRoomDto>(`/rooms/join`, { roomCode });
return data;
},

Expand Down
31 changes: 31 additions & 0 deletions src/frontend/src/api/endpoints/room/room.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,34 @@ export interface RoomDto {
userCount: number;
playlistUrl?: string;
}

export interface CurrentRoomUserDto {
userId: number;
role: number;
nickname: string;
profileImageUrl: string;
}

export interface CurrentRoomInfoDto {
roomId: number;
code: string;
title: string;
description: string;
userCount: number;
creator: string;
profileImageUrl: string;
}

export interface CurrentRoomPlaylistDto {
url: string;
order: number;
}

export interface CurrentRoomDto {
myRole: number;
roomDetails: {
userList: CurrentRoomUserDto[];
roomInfo: CurrentRoomInfoDto[];
playlist: CurrentRoomPlaylistDto[];
};
}
66 changes: 37 additions & 29 deletions src/frontend/src/components/RoomDetail/index.css.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,10 @@
import styled from 'styled-components';

export const Container = styled.div`
display: flex;
flex-direction: row;
justify-content: space-between;
gap: 20px;
padding-right: 16px;
`;

export const Wrapper = styled.div`
width: 100%;
display: flex;
flex-direction: column;
gap: 16px;
`;

export const TitleContainer = styled.header`
export const Container = styled.header`
width: 100%;
display: flex;
gap: 8px;
justify-content: space-between;
> div {
display: flex;
gap: 8px;
}
gap: 8px;
`;

export const TitleContainer_Img = styled.img`
Expand All @@ -32,14 +13,31 @@ export const TitleContainer_Img = styled.img`
border-radius: 10px;
`;

export const TextContainer = styled.div`
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 8px;
`;

export const Title = styled.div`
font-size: 18px;
font-weight: 700;
margin-top: 4px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
word-break: break-all;
line-height: 1.2em;
max-height: 2.4em;
`;

export const Username = styled.div`
color: var(--palette-font-gray-strong);
margin-top: 8px;
font-size: 14px;
`;

export const DescriptionContainer = styled.div`
Expand All @@ -48,28 +46,38 @@ export const DescriptionContainer = styled.div`

export const Description = styled.div<{ $isExpanded: boolean }>`
color: var(--palette-font-gray-strong);
margin-top: 16px;
font-size: 14px;
margin-top: 0.5rem;
max-height: ${({ $isExpanded }) => ($isExpanded ? 'none' : '4.5em')};
overflow: hidden;
line-height: 1.5em;
display: -webkit-box;
-webkit-line-clamp: ${({ $isExpanded }) => ($isExpanded ? 'none' : '3')};
-webkit-box-orient: vertical;
text-overflow: ellipsis;
word-break: break-all;
`;

export const TitleContainer_MemberNum = styled.div`
width: 6rem;
export const MemberCount = styled.div`
flex-shrink: 0;
display: flex;
background-color: var(--palette-font-gray-strong);
align-items: center;
background: rgba(0, 0, 0, 0.5);
color: white;
padding: 8px;
padding: 0.5rem 0.75rem;
border-radius: 16px;
height: 28px;
align-items: center;
font-size: 14px;
font-weight: 500;
white-space: nowrap;
`;

export const Circle = styled.div`
width: 5px;
height: 5px;
border-radius: 100px;
background-color: red;
background-color: var(--palette-status-negative);
margin-right: 4px;
`;

export const MoreButton = styled.button`
Expand Down
64 changes: 36 additions & 28 deletions src/frontend/src/components/RoomDetail/index.tsx
Original file line number Diff line number Diff line change
@@ -1,60 +1,68 @@
import { useState } from 'react';
import { useEffect, useRef, useState } from 'react';

import {
TitleContainer,
Container,
TitleContainer_Img,
TextContainer,
Title,
Username,
DescriptionContainer,
Description,
TitleContainer_MemberNum,
MemberCount,
MoreButton,
Circle,
} from './index.css';

const roomInfoExample = {
imgUrl:
'https://yt3.ggpht.com/YHA2HDj6dt07Qrtbfdyi94eKId71Bmoz6z9LfcLCbCv_RRnxUTYSn0p_IkW9k6Qkp_04Lq-56A=s88-c-k-c0x00ffffff-no-rj',
title: '너무 조용하지도, 너무 들뜨지도 않아 듣기 좋은 재즈',
creator: '유자네임',
description:
'설명이 들어갈 것이면 그렇다면 여기에 많은 내용이 담기겠죠\n심지어 단을 나눠서 설명이 필요할 수도 있겠죠\n#안정적 #음악 #코지\n#유료광고포함 #숨겨둠',
people: '500',
};
import { useCurrentRoomStore } from '@/stores/useCurrentRoomStore';
import DefaultProfile from '@/assets/img/DefaultProfile.svg';

export const RoomDetail = () => {
const { currentRoom } = useCurrentRoomStore();
const roomInfo = currentRoom?.roomDetails.roomInfo[0];

const handleDescription = () => {
if (roomInfo?.description == null) return '';
return roomInfo?.description;
};

return (
<TitleContainer>
<div>
<TitleContainer_Img src={roomInfoExample.imgUrl} />
<div>
<Title>{roomInfoExample.title}</Title>
<Username>{roomInfoExample.creator}</Username>
<DescriptionText description={roomInfoExample.description} />
</div>
</div>
<TitleContainer_MemberNum>
<Container>
<TitleContainer_Img src={roomInfo?.profileImageUrl ?? DefaultProfile} />
<TextContainer>
<Title>{roomInfo?.title}</Title>
<Username>{roomInfo?.creator}</Username>
<DescriptionText description={handleDescription()} />
</TextContainer>
<MemberCount>
<Circle />
201 / {roomInfoExample.people}
</TitleContainer_MemberNum>
</TitleContainer>
{roomInfo?.userCount}명
</MemberCount>
</Container>
);
};

const DescriptionText = (props: { description: string }) => {
const [isExpanded, setIsExpanded] = useState(false);
const [isOverflowing, setIsOverflowing] = useState(false);
const textRef = useRef<HTMLDivElement>(null);

useEffect(() => {
const element = textRef.current;
if (element) {
setIsOverflowing(element.scrollHeight > element.clientHeight);
}
}, [props.description]);

return (
<DescriptionContainer>
<Description $isExpanded={isExpanded}>
<Description ref={textRef} $isExpanded={isExpanded}>
{props.description.split('\n').map((line, index) => (
<span key={index}>
{line}
<br />
</span>
))}
</Description>
{!isExpanded && props.description.split('\n').length > 3 && (
{!isExpanded && isOverflowing && (
<MoreButton onClick={() => setIsExpanded(true)}>더보기</MoreButton>
)}
{isExpanded && <MoreButton onClick={() => setIsExpanded(false)}>접기</MoreButton>}
Expand Down
25 changes: 18 additions & 7 deletions src/frontend/src/components/TopNavBar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,34 @@
import { RoomCreateModal } from '@/components/Modal/RoomCreateModal';
import { NotificationModal } from '@/components/Modal/NotificationModal';
import { SearchBar } from '@/components/Search/SearchBar';
import { ProfileModal } from '@/components/Modal/ProfileModal';
import { useUserStore } from '@/stores/useUserStore';
import { useAuthStore } from '@/stores/useAuthStore';
import { ProfileModal } from '@/components/Modal/ProfileModal';
import { useMyRoomsStore } from '@/stores/useMyRoomsStore';
import { useWebSocketStore } from '@/stores/useWebSocketStore';
export const TopNavBar = () => {
const { user, fetchMyProfile, clear } = useUserStore();
const { user, fetchMyProfile, clearProfile } = useUserStore();
const { fetchMyRooms } = useMyRoomsStore();
const { connect } = useWebSocketStore();
const [isRoomCreateModalOpen, setIsRoomCreateModalOpen] = useState(false);
const [isNotificationModalOpen, setIsNotificationModalOpen] = useState(false);
const [isProfileModalOpen, setIsProfileModalOpen] = useState(false);
const accessToken = useAuthStore(state => state.accessToken);

useEffect(() => {
if (accessToken) {
fetchMyProfile();
} else {
clear();
connect();

if (!accessToken) {
clearProfile();
return;
}
}, [accessToken, fetchMyProfile, clear]);

const initializeUser = async () => {
await fetchMyProfile();
await fetchMyRooms();
};
initializeUser();
}, [accessToken]);

Check warning on line 45 in src/frontend/src/components/TopNavBar/index.tsx

View workflow job for this annotation

GitHub Actions / frontend-ci

React Hook useEffect has missing dependencies: 'clearProfile', 'connect', 'fetchMyProfile', and 'fetchMyRooms'. Either include them or remove the dependency array

const clickNotification = (e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation();
Expand Down
10 changes: 10 additions & 0 deletions src/frontend/src/hooks/queries/useCurrentRoom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { useQuery } from '@tanstack/react-query';
import { roomApi } from '@/api/endpoints/room/room.api';

export const useCurrentRoom = (code: string | null) => {
return useQuery({
queryKey: ['currentRoom', code],
queryFn: () => roomApi.joinRoom(code!),
enabled: !!code, // code가 있을 때만 쿼리 실행
});
};
Loading
Loading