Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FE] 작성한 리뷰를 확인할 수 있는 반응형 레이아웃 #1038

Open
wants to merge 20 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
ea223e3
feat: 현재 미디어 쿼리 상태와 디바이스 종류(boolean)를 리턴하는 훅
ImxYJL Dec 30, 2024
437ba4c
feat: 작성한 리뷰 페이지의 분할 레이아웃을 담당하는 WrittenReviewItem 레이아웃 컴포넌트
ImxYJL Dec 30, 2024
b50b5f1
feat: 임시 WrittenReviewList 컴포넌트
ImxYJL Dec 30, 2024
13ca3f9
feat: 임시 DetailedWrittenReview 컴포넌트
ImxYJL Dec 30, 2024
e8dcc78
feat: 임시 작성한 리뷰 확인 페이지
ImxYJL Dec 30, 2024
a8d2246
feat: 작성한 리뷰 페이지에 대한 임시 라우팅
ImxYJL Dec 30, 2024
eb106d6
feat: 임시 레이아웃, 반응형 적용
ImxYJL Jan 2, 2025
4474aeb
feat: 선택한 리뷰가 없을 때의 컴포넌트 추가
ImxYJL Jan 2, 2025
e3a57a1
refactor: 페이지 레이아웃 이름을 더 직관적이고 단순하게 수정
ImxYJL Jan 4, 2025
5f719b0
chore: WrittenReviewPage의 layout 폴더 위치를 component 하위로 변경
ImxYJL Jan 5, 2025
f2073d4
refactor: 작성한 리뷰 확인 페이지의 이름을 WrittenReviewPage로 간략하게 변경
ImxYJL Jan 9, 2025
117735a
refactor: 반응형 레이아웃을 위해 queryString 도입 (+변경된 페이지명에 따른 추가 변경사항)
ImxYJL Jan 9, 2025
aa8344a
chore: amplitude 페이지 정보에 작성한 리뷰 확인 페이지 추가
ImxYJL Jan 9, 2025
2b0aa9d
refactor: 작성한 리뷰 확인 페이지에 early return 스타일 적용
ImxYJL Jan 9, 2025
d3eb2d5
refactor: useSearchParamAndQuery의 매개변수 paramKey를 optional로 변경
ImxYJL Jan 9, 2025
ff3e520
refactor: 미디어 쿼리 관련 훅 리팩토링 - mediaType 대신 breakpoint로 명시
ImxYJL Jan 9, 2025
b0e6234
chore: 간단한 변수명 수정
ImxYJL Jan 9, 2025
a15cd98
chore: Breakpoints 타입 분리
ImxYJL Jan 9, 2025
aee47a5
chore: 경로 수정
ImxYJL Jan 9, 2025
699dde2
refactor: resize 함수에 debounce 추가
ImxYJL Jan 9, 2025
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
4 changes: 4 additions & 0 deletions frontend/src/assets/slideArrows.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions frontend/src/components/ReviewListItem/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// 임시 컴포넌트! 작성한 리뷰 확인 && 받은 리뷰 확인 아이템

import * as S from './styles';

interface ReviewListItemProps {
handleClick: () => void;
}

const ReviewListItem = ({ handleClick }: ReviewListItemProps) => {
return <S.ReviewListItem onClick={handleClick}>리뷰 목록 아이템입니다</S.ReviewListItem>;
};

export default ReviewListItem;
19 changes: 19 additions & 0 deletions frontend/src/components/ReviewListItem/styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import styled from '@emotion/styled';

import media from '@/utils/media';

export const ReviewListItem = styled.li`
display: flex;
flex-direction: column;

min-width: ${({ theme }) => theme.writtenReviewLayoutSize.width};
min-height: 20rem;

border: 0.2rem solid ${({ theme }) => theme.colors.placeholder};
border-radius: ${({ theme }) => theme.borderRadius.basic};

${media.small} {
min-width: 30rem;
min-height: 18rem;
}
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { PageContentLayout } from '../../layouts';
import { NoSelectedReviewGuide } from '../index';

import * as S from './styles';

interface DetailedWrittenReviewProps {
$isMobile: boolean;
selectedReviewId: number | null;
}

// 라우팅으로 들어오는 경우 queryParam으로 reviewId를 가져올 수 있음
// -> 그렇다면 라우터에서 이 컴포넌트를 별도의 props 없이 호출 가능, selectedReviewId는 optional 처리
// but 일단은 props로 id를 무조건 받도록 구현해둔 상태
const DetailedWrittenReview = ({ $isMobile, selectedReviewId }: DetailedWrittenReviewProps) => {
// 추후 이곳에서 직접 상세 리뷰 데이터 호출

// 라우팅으로 넘어온 경우 무조건 isMobile은 true
return (
<S.DetailedWrittenReview $isMobile={$isMobile}>
<PageContentLayout title="작성한 리뷰 상세보기">
<S.Outline>
{selectedReviewId ? <div style={{ height: '120vh' }}>있다</div> : <NoSelectedReviewGuide />}
</S.Outline>
</PageContentLayout>
</S.DetailedWrittenReview>
);
};

export default DetailedWrittenReview;
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import styled from '@emotion/styled';

import media from '@/utils/media';

interface DetailedWrittenReviewStyleProps {
$isMobile: boolean;
}

export const DetailedWrittenReview = styled.div<DetailedWrittenReviewStyleProps>`
${media.xSmall} {
${({ $isMobile }) =>
$isMobile
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

데스크탑에서 내가 쓴 상세 리뷰 url로 바로 들어오면, 상세보기만 보이겠네요.

회의때 모바일에서 내가 쓴 상세 리뷰를 따로 페이지로 관리하자고 했는데 이러면 url 에 따른 화면 관리가 까다롭겠네요. 상세 보기 페이지에서 뒤로 가기를 하면, 데스크탑에서 목록과 상세보기가 같이 보이는 화면이 나타나는게 이상하기도 하구요. (이전에는 상세보기만 보여줬다가 이제는 목록과 상세보기가 같이 보여진다?)
기존 내가 받은 리뷰 목록, 상세처럼 아예 페이지를 분리하는게 나을 것 같아요. 노트북으로 볼 때 지금 목록 크기 대비 두 개가 같이 있을 때 쓴 목록과 상세보기가 생각보다 작게 보일 수 도 있을 것 같네요.

이 부분은 프론트 전체 회의가 필요할 것 같네요

? `
display: block;
width: 100%;
`
: `
display: none;
`}
}
`;

export const Outline = styled.div`
display: flex;
align-items: center;

min-width: ${({ theme }) => theme.writtenReviewLayoutSize.width};
min-height: ${({ theme }) => theme.writtenReviewLayoutSize.height};

border: 0.2rem solid ${({ theme }) => theme.colors.lightGray};
border-radius: ${({ theme }) => theme.borderRadius.basic};
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import SlideArrowsIcon from '@/assets/slideArrows.svg';

import * as S from './styles';

const NoSelectedReviewGuide = () => {
return (
<S.NoSelectedReview>
<img src={SlideArrowsIcon} alt="" />
<p>확인할 리뷰를 선택해주세요!</p>
</S.NoSelectedReview>
);
};

export default NoSelectedReviewGuide;
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import styled from '@emotion/styled';

import media from '@/utils/media';

export const NoSelectedReview = styled.section`
display: flex;
gap: 2rem;
align-items: center;
justify-content: center;

margin: 0 auto;

img {
height: 3rem;

${media.medium} {
height: 2.8rem;
margin-left: 2.5rem;
}
}

p {
font-size: ${({ theme }) => theme.fontSize.mediumSmall};
font-weight: bold;
color: ${({ theme }) => theme.colors.disabled};

${media.medium} {
font-size: ${({ theme }) => theme.fontSize.basic};
}
}
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import ReviewListItem from '@/components/ReviewListItem';

import { PageContentLayout } from '../../layouts';

import * as S from './styles';

interface WrittenReviewListProps {
handleClick: (reviewId: number) => void;
}

const WrittenReviewList = ({ handleClick }: WrittenReviewListProps) => {
// 리뷰 리스트 받아오기
const reviewIdList = [5, 1, 2, 3, 4];

return (
<PageContentLayout title="작성한 리뷰 목록">
<S.WrittenReviewList>
{/** 추후 이벤트 위임 형식으로 변경 가능 */}

{/** TODO: 작성한 리뷰 없을 때의 컴포넌트 추가*/}
{reviewIdList.map((reviewId) => (
<ReviewListItem key={reviewId} handleClick={() => handleClick(reviewId)} />
))}
</S.WrittenReviewList>
</PageContentLayout>
);
};

export default WrittenReviewList;
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import styled from '@emotion/styled';

import media from '@/utils/media';


export const WrittenReviewList = styled.ul`
overflow-x: hidden;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 1.7rem;

max-height: 68vh;

${media.xSmall} {
width: 100%;
}

& > li {
margin-right: 0.5rem;
}
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { default as NoSelectedReviewGuide } from './NoSelectedReviewGuide';
export { default as DetailedWrittenReview } from './DetailedWrittenReview';
export { default as WrittenReviewList } from './WrittenReviewList';
1 change: 1 addition & 0 deletions frontend/src/pages/WrittenReviewConfirmPage/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as useCurrentMediaType } from './useCurrentMediaType';
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { useState, useLayoutEffect } from 'react';

import { breakpoint } from '@/styles/theme';
import { Breakpoints } from '@/utils/media';

interface CurrentDevice {
isMobile: boolean;
isTablet: boolean;
isDesktop: boolean;
}

/**
현재 미디어 쿼리 상태와 디바이스 종류(boolean)를 리턴하는 훅
*/
const useCurrentMediaType = () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍🏻 현재 디바이스에 따라 다르게 동작해야 하는 경우 사용하기 좋겠네요

const [currentMediaType, setCurrentMediaType] = useState<Breakpoints | null>(null);
const breakpointsArray = Object.entries(breakpoint);

const getCurrentDeviceType = (mediaType: Breakpoints | null): CurrentDevice => ({
isMobile: mediaType === 'xSmall' || mediaType === 'xxSmall',
isTablet: mediaType === 'small' || mediaType === 'medium',
isDesktop: mediaType === 'large',
});

useLayoutEffect(() => {
const handleResize = () => {
const currentWidth = window.innerWidth;
const matchedBreakpoint = breakpointsArray.find(([, width]) => currentWidth <= width);

setCurrentMediaType((matchedBreakpoint?.[0] as Breakpoints) ?? null);
};

handleResize();

window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);

return {
currentMediaType,
currentDeviceType: getCurrentDeviceType(currentMediaType),
};
};

export default useCurrentMediaType;
44 changes: 44 additions & 0 deletions frontend/src/pages/WrittenReviewConfirmPage/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';

import { ErrorSuspenseContainer, AuthAndServerErrorFallback } from '@/components';
import { useSearchParamAndQuery } from '@/hooks';

import DetailedWrittenReview from './components/DetailedWrittenReview';
import WrittenReviewList from './components/WrittenReviewList';
import { useCurrentMediaType } from './hooks';
import * as S from './styles'; // TODO: 마지막에 import 경로들, 시맨틱 확인하기!

// refactor(선택): 레이아웃 도입 등으로 이 페이지에서는 에러바운더리 + 탭 + 이하 페이지 Content 요소만 쓰도록 분리

const WrittenReviewConfirmPage = () => {
const [selectedReviewId, setSelectedReviewId] = useState<number | null>(null);

const { param: reviewRequestCode } = useSearchParamAndQuery({
paramKey: 'reviewRequestCode',
});
const navigate = useNavigate();
const { currentDeviceType } = useCurrentMediaType();

const handleClick = (reviewId: number) => {
if (currentDeviceType.isMobile) {
navigate(`/user/written-review-confirm/${reviewRequestCode}/${reviewId}`);
} else {
setSelectedReviewId(reviewId);
}
};

return (
<ErrorSuspenseContainer fallback={AuthAndServerErrorFallback}>
<S.PageContainer>
<WrittenReviewList handleClick={handleClick} />
{/* TODO: 모바일에서 DetailedWrittenReview를 화살표 레이아웃으로 감싸기 */}
{!currentDeviceType.isMobile && (
<DetailedWrittenReview $isMobile={currentDeviceType.isMobile} selectedReviewId={selectedReviewId} />
)}
</S.PageContainer>
</ErrorSuspenseContainer>
);
};

export default WrittenReviewConfirmPage;
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { EssentialPropsWithChildren } from '@/types';

import * as S from './styles';

interface WrittenReviewItemProps {
title: string;
}

const PageContentLayout = ({ title, children }: EssentialPropsWithChildren<WrittenReviewItemProps>) => {
return (
<S.PageContentLayout>
<S.Title>{title}</S.Title>
<S.Content>{children}</S.Content>
</S.PageContentLayout>
);
};

export default PageContentLayout;
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import styled from '@emotion/styled';

import media from '@/utils/media';

export const PageContentLayout = styled.article`
display: flex;
flex-direction: column;
height: 100%;

${media.xSmall} {
margin: 0 auto;
}
`;

export const Title = styled.h2`
margin-top: 4.7rem;
margin-bottom: 2.4rem;
font-size: 1.8rem;
font-weight: bold;
`;

export const Content = styled.section`

`;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as PageContentLayout } from './PageContentLayout';
17 changes: 17 additions & 0 deletions frontend/src/pages/WrittenReviewConfirmPage/styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import styled from '@emotion/styled';

import media from '@/utils/media';

export const PageContainer = styled.div`
display: flex;
gap: 6rem;
justify-content: center;

${media.medium} {
gap: 4rem;
}

${media.small} {
margin: 0 2rem;
}
`;
1 change: 1 addition & 0 deletions frontend/src/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export { default as ReviewWritingPage } from './ReviewWritingPage';
export { default as ReviewWritingCompletePage } from './ReviewWritingCompletePage';
export { default as ReviewZonePage } from './ReviewZonePage';
export { default as ReviewCollectionPage } from './ReviewCollectionPage';
export { default as WrittenReviewConfirmPage } from './WrittenReviewConfirmPage';
9 changes: 9 additions & 0 deletions frontend/src/router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ const ReviewWritingPage = lazy(() => import('@/pages/ReviewWritingPage'));
const ReviewZonePage = lazy(() => import('@/pages/ReviewZonePage'));
const ReviewCollectionPage = lazy(() => import('@/pages/ReviewCollectionPage'));
const LoadingPage = lazy(() => import('@/pages/LoadingPage'));
// 임시
const WrittenReviewConfirmPage = lazy(() => import('@/pages/WrittenReviewConfirmPage'));
const DetailedWrittenReview = lazy(() => import('@/pages/WrittenReviewConfirmPage/components/DetailedWrittenReview'));

import App from './App';
import { ErrorSuspenseContainer } from './components';
Expand Down Expand Up @@ -52,6 +55,12 @@ const router = createBrowserRouter([
),
},
{ path: `${ROUTE.reviewCollection}/:${ROUTE_PARAM.reviewRequestCode}`, element: <ReviewCollectionPage /> },
// NOTE: 임시 라우팅 및 페이지명
{ path: `user/written-review-confirm/:${ROUTE_PARAM.reviewRequestCode}`, element: <WrittenReviewConfirmPage /> },
{
path: `user/written-review-confirm/:${ROUTE_PARAM.reviewRequestCode}/:${ROUTE_PARAM.reviewId}`,
element: <DetailedWrittenReview $isMobile={true} selectedReviewId={Number(ROUTE_PARAM.reviewId)} />,
},
],
},
]);
Expand Down
Loading
Loading