Skip to content

Commit

Permalink
[FE] 작성한 리뷰를 확인할 수 있는 반응형 레이아웃 (#1038)
Browse files Browse the repository at this point in the history
* feat: 현재 미디어 쿼리 상태와 디바이스 종류(boolean)를 리턴하는 훅

* feat: 작성한 리뷰 페이지의 분할 레이아웃을 담당하는 WrittenReviewItem 레이아웃 컴포넌트

* feat: 임시 WrittenReviewList 컴포넌트

* feat: 임시 DetailedWrittenReview 컴포넌트

* feat: 임시 작성한 리뷰 확인 페이지

* feat: 작성한 리뷰 페이지에 대한 임시 라우팅

* feat: 임시 레이아웃, 반응형 적용

* feat: 선택한 리뷰가 없을 때의 컴포넌트 추가

* refactor: 페이지 레이아웃 이름을 더 직관적이고 단순하게 수정

* chore: WrittenReviewPage의 layout 폴더 위치를 component 하위로 변경

* refactor: 작성한 리뷰 확인 페이지의 이름을 WrittenReviewPage로 간략하게 변경

* refactor: 반응형 레이아웃을 위해 queryString 도입 (+변경된 페이지명에 따른 추가 변경사항)

* chore: amplitude 페이지 정보에 작성한 리뷰 확인 페이지 추가

* refactor: 작성한 리뷰 확인 페이지에 early return 스타일 적용

* refactor: useSearchParamAndQuery의 매개변수 paramKey를 optional로 변경

* refactor: 미디어 쿼리 관련 훅 리팩토링 - mediaType 대신 breakpoint로 명시

1. 훅 이름 변경
2. 변수명 변경

* chore: 간단한 변수명 수정

* chore: Breakpoints 타입 분리

* chore: 경로 수정

* refactor: resize 함수에 debounce 추가

* refactor: xSmall 사이즈를 430으로 변경(일단 아이폰 프로 맥스를 위해 430으로 맞춤)

* chore: 에러 바운더리에 적용된 속성 이름 변경

* fix: useDeviceBreakpoints훅에서 large 크기일 때 undefined가 나오던 문제

* refactor: ReviewList와 ReviewListItem 스타일 조정 - large 사이즈에서만 2분할되도록 변경

* refactor: 작성한 리뷰 확인 페이지 반응형 조절 - 2분할은 large 사이즈에서만 이루어지도록 변경

* fix: WrittenReviewList에 고정 width값(Detailed의 Outline과 동일) 부여
  • Loading branch information
ImxYJL authored Jan 15, 2025
1 parent ca790e4 commit 9e09b04
Show file tree
Hide file tree
Showing 26 changed files with 417 additions and 6 deletions.
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;
31 changes: 31 additions & 0 deletions frontend/src/components/ReviewListItem/styles.ts
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 ReviewListItem = styled.li`
display: flex;
flex-direction: column;
min-width: ${({ theme }) => theme.writtenReviewLayoutSize.largeMinWidth};
max-width: ${({ theme }) => theme.writtenReviewLayoutSize.largeMaxWidth};
min-height: 20rem;
max-height: 24rem;
border: 0.2rem solid ${({ theme }) => theme.colors.placeholder};
border-radius: ${({ theme }) => theme.borderRadius.basic};
${media.medium} {
min-width: 62vw;
min-height: 18vh;
}
${media.small} {
min-width: 65vw;
min-height: 14vh;
}
${media.xSmall} {
min-width: 70vw;
min-height: 14vh;
}
`;
1 change: 1 addition & 0 deletions frontend/src/constants/amplitudeEventName.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export const PAGE_VISITED_EVENT_NAME: { [key in Exclude<PageName, undefined>]: s
reviewWriting: '[page] 리뷰 작성 페이지',
reviewWritingComplete: '[page] 리뷰 작성 완료 페이지',
reviewLinks: '[page] 리뷰 링크 관리 페이지',
writtenReview: '[page] 작성한 리뷰 확인 페이지',
};

export const REVIEW_WRITING_EVENT_NAME = {
Expand Down
1 change: 1 addition & 0 deletions frontend/src/constants/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ export const ROUTE = {
reviewZone: 'user/review-zone',
reviewCollection: 'user/review-collection',
reviewLinks: 'user/review-links',
writtenReview: 'user/written-review',
};
4 changes: 2 additions & 2 deletions frontend/src/hooks/useSearchParamAndQuery.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { useLocation, useParams } from 'react-router';

interface UseSearchParamAndQueryProps {
paramKey: string;
paramKey?: string;
queryStringKey?: string;
}
/**
* url에서 원하는 param, queryString의 값을 가져온다.
* @param paramKey: 가져오고 싶은 param의 key
* @param queryStringKey: 가져오고 싶은 queryString의 key (옵셔널)
* @param queryStringKey: 가져오고 싶은 queryString의 key
*/
const useSearchParamAndQuery = ({ paramKey, queryStringKey }: UseSearchParamAndQueryProps) => {
const location = useLocation();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { NoSelectedReviewGuide } from '../index';
import { PageContentLayout } from '../layouts';

import * as S from './styles';

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

const DetailedWrittenReview = ({ $isDisplayable, selectedReviewId }: DetailedWrittenReviewProps) => {
// 추후 이곳에서 직접 상세 리뷰 데이터 호출

return (
<PageContentLayout title="작성한 리뷰 상세보기">
<S.DetailedWrittenReview $isDisplayable={$isDisplayable}>
<S.Outline>{selectedReviewId ? <div>{selectedReviewId} 선택함 </div> : <NoSelectedReviewGuide />}</S.Outline>
</S.DetailedWrittenReview>
</PageContentLayout>
);
};

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

import media from '@/utils/media';

import { DetailedWrittenReviewProps } from '.';

export interface StyleProps extends Pick<DetailedWrittenReviewProps, '$isDisplayable'> {}

export const DetailedWrittenReview = styled.div<StyleProps>`
display: block;
max-width: ${({ theme }) => theme.writtenReviewLayoutSize.largeMaxWidth};
${media.medium} {
${({ $isDisplayable }) =>
$isDisplayable
? `
display: block;
`
: `
display: none;
`}
}
`;

export const Outline = styled.div`
display: flex;
align-items: center;
width: 100%;
min-width: ${({ theme }) => theme.writtenReviewLayoutSize.largeMinWidth};
max-width: ${({ theme }) => theme.writtenReviewLayoutSize.largeMaxWidth};
height: 100%;
min-height: ${({ theme }) => theme.writtenReviewLayoutSize.largeMaxHeight};
border: 0.2rem solid ${({ theme }) => theme.colors.lightGray};
border-radius: ${({ theme }) => theme.borderRadius.basic};
${media.medium} {
min-width: 65vw;
max-width: 65vw;
}
${media.small} {
min-width: 75vw;
max-width: 75vw;
}
`;
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,32 @@
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;
width: 100%;
min-width: ${({ theme }) => theme.writtenReviewLayoutSize.largeMinWidth};
max-width: ${({ theme }) => theme.writtenReviewLayoutSize.largeMaxWidth};
height: 100%;
min-height: ${({ theme }) => theme.writtenReviewLayoutSize.largeMaxHeight};
max-height: ${({ theme }) => theme.writtenReviewLayoutSize.largeMaxHeight};
${media.medium} {
min-width: 65vw;
max-width: 65vw;
}
${media.small} {
min-width: 75vw;
max-width: 75vw;
}
& > li {
margin-right: 0.5rem;
}
`;
3 changes: 3 additions & 0 deletions frontend/src/pages/WrittenReviewPage/components/index.tsx
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';
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,23 @@
import styled from '@emotion/styled';

export const PageContentLayout = styled.article`
display: flex;
flex-direction: column;
justify-content: center;
height: 100%;
margin: 0 auto;
`;

export const Title = styled.h2`
align-self: flex-start;
margin-top: 4.7rem;
margin-bottom: 2.4rem;
font-size: 1.8rem;
font-weight: bold;
text-align: left;
`;

export const Content = styled.section``;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as PageContentLayout } from './PageContentLayout';
1 change: 1 addition & 0 deletions frontend/src/pages/WrittenReviewPage/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as useDeviceBreakpoints } from './useDeviceBreakpoints';
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { useState, useLayoutEffect } from 'react';

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

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

const DEBOUNCE_TIME = 100;

/**
현재 미디어 쿼리 상태와 디바이스 종류(boolean)를 리턴하는 훅
*/
const useDeviceBreakpoints = () => {
const [breakpointType, setBreakPointType] = useState<Breakpoints | null>(null);
const breakpointsArray = Object.entries(breakpoint);

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

const handleResize = debounce(() => {
const currentWidth = window.innerWidth;

// 마지막 breakpoint만 특정 범위 사이의 width 값이 아닌, 해당 기준 이상인 값이므로 따로 처리
const inRangeBreakpoint = breakpointsArray.find(([, width]) => currentWidth <= width);
const upperBoundBreakpoint = breakpointsArray[breakpointsArray.length - 1];

const finalBreakpoint = inRangeBreakpoint || upperBoundBreakpoint;

setBreakPointType((finalBreakpoint[0] as Breakpoints) ?? null);
}, DEBOUNCE_TIME);

useLayoutEffect(() => {
handleResize();
window.addEventListener('resize', handleResize);

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

return {
breakpointType,
deviceType: getDeviceType(breakpointType),
};
};

export default useDeviceBreakpoints;
Loading

0 comments on commit 9e09b04

Please sign in to comment.