Skip to content

Commit

Permalink
[FE] feat: 리뷰 목록/리뷰 모아보기의 공통 UI 및 스위치 컴포넌트, 새 URL 및 디자인 적용 (#776)
Browse files Browse the repository at this point in the history
* feat: Switch 컴포넌트 제작

* chore: Switch 컴포넌트 이름 변경

* feat: 리뷰 모아보기에 대한 라우팅 추가 및 임시 페이지 구현

* feat: 리뷰 모아보기와 리뷰 목록 페이지의 공통 레이아웃 제작

* refactor: 공통 레이아웃 제작에 따른 ReviewList 리팩토링

* feat: 리뷰 목록 반응형 적용

* feat: OptionSwitch 반응형 적용

* refactor: ReviewDisplayLayout 반응형 수정

* fix: px을 rem으로 수정

* fix: 미디어 쿼리 기준 단위를 px으로 수정

* refactor: 스타일을 위한 속성에 $처리 및 스타일 전용 인터페이스 생성

* fix: 불필요한 상속 제거

* refactor: OptionSwitch 리팩토링 - option 배열을 통한 렌더링 및 시맨틱 태그를사용

* refactor: OptionSwitch 컴포넌트 리팩토링에 따른 ReviewDisplayLayout 컴포넌트 리팩토링 및 훅 분리

* feat: 선택하지 않은 option을 hover한 경우의 추가 배경색 지정

* fix: 리뷰 작성 완료 페이지에서 breadCrumb으로 리뷰 작성 페이지로 이동하는 경로를 절대 URL 경로로 수정
  • Loading branch information
ImxYJL authored and skylar1220 committed Nov 5, 2024
1 parent d3f1eaa commit a06f159
Show file tree
Hide file tree
Showing 20 changed files with 294 additions and 81 deletions.
25 changes: 11 additions & 14 deletions frontend/src/components/ReviewCard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,21 @@ interface ReviewCardProps {
handleClick: () => void;
}

const ReviewCard = ({ projectName, createdAt, contentPreview, categories, handleClick }: ReviewCardProps) => {
const ReviewCard = ({ createdAt, contentPreview, categories, handleClick }: ReviewCardProps) => {
return (
<S.Layout onClick={handleClick}>
<S.Header>
<S.HeaderContent>
<div>
<S.Title>{projectName}</S.Title>
<S.SubTitle>{createdAt}</S.SubTitle>
</div>
</S.HeaderContent>
</S.Header>
<S.LeftLineBorder />

<S.Main>
<span>{contentPreview}</span>
<S.Keyword>
{categories.map((category) => (
<div key={category.optionId}>{category.content}</div>
))}
</S.Keyword>
<S.Footer>
<S.Keyword>
{categories.map((category) => (
<div key={category.optionId}>{category.content}</div>
))}
</S.Keyword>
<S.Date>{createdAt}</S.Date>
</S.Footer>
</S.Main>
</S.Layout>
);
Expand Down
46 changes: 23 additions & 23 deletions frontend/src/components/ReviewCard/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,47 +4,35 @@ import media from '@/utils/media';

export const Layout = styled.div`
display: flex;
flex-direction: column;
border: 0.1rem solid ${({ theme }) => theme.colors.lightGray};
border-radius: 0.8rem;
border-radius: 1rem;
&:hover {
cursor: pointer;
border: 0.1rem solid ${({ theme }) => theme.colors.lightPurple};
border: 0.15rem solid ${({ theme }) => theme.colors.primaryHover};
& > div:first-of-type {
background-color: ${({ theme }) => theme.colors.lightPurple};
}
}
`;

export const Header = styled.div`
display: flex;
justify-content: space-between;
height: 6rem;
padding: 1rem 3rem;
export const LeftLineBorder = styled.div`
width: 5rem;
background-color: ${({ theme }) => theme.colors.lightGray};
border-radius: 0.8rem 0.8rem 0 0;
`;

export const HeaderContent = styled.div`
display: flex;
gap: 1rem;
img {
width: 4rem;
}
border-radius: 1rem 0 0 1rem;
`;

export const Title = styled.div`
font-size: 1.6rem;
font-weight: 700;
`;

export const SubTitle = styled.div`
font-size: 1.2rem;
export const Date = styled.p`
height: fit-content;
padding: 0 1rem;
font-size: 1.3rem;
background-color: ${({ theme }) => theme.colors.lightGray};
`;

export const Visibility = styled.div`
Expand Down Expand Up @@ -74,6 +62,18 @@ export const Main = styled.div`
}
`;

export const Footer = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
${media.small} {
flex-direction: column;
gap: 1.2rem;
align-items: flex-start;
}
`;

export const Keyword = styled.div`
display: flex;
flex-wrap: wrap;
Expand All @@ -83,7 +83,7 @@ export const Keyword = styled.div`
font-size: 1.4rem;
${media.small} {
gap: 1.6rem;
gap: 1.2rem;
}
div {
Expand Down
36 changes: 36 additions & 0 deletions frontend/src/components/common/OptionSwitch/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import * as S from './styles';

export interface OptionSwitchStyleProps {
$isChecked: boolean;
}

export interface OptionSwitchOption {
label: string;
isChecked: boolean;
handleOptionClick: () => void;
}

interface OptionSwitchProps {
options: OptionSwitchOption[];
}

const OptionSwitch = ({ options }: OptionSwitchProps) => {
const handleSwitchClick = (index: number) => {
const clickedOption = options[index];
if (clickedOption) clickedOption.handleOptionClick();
};

return (
<S.OptionSwitchContainer>
{options.map((option, index) => (
<S.CheckboxWrapper key={option.label} $isChecked={option.isChecked} onClick={() => handleSwitchClick(index)}>
<S.CheckboxButton type="button" $isChecked={option.isChecked}>
{option.label}
</S.CheckboxButton>
</S.CheckboxWrapper>
))}
</S.OptionSwitchContainer>
);
};

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

import media from '@/utils/media';

import { OptionSwitchStyleProps } from './index';

export const OptionSwitchContainer = styled.ul`
cursor: pointer;
display: flex;
justify-content: space-between;
width: 15rem;
height: 4.5rem;
padding: 0.7rem;
background-color: ${({ theme }) => theme.colors.lightGray};
border-radius: ${({ theme }) => theme.borderRadius.basic};
${media.small} {
height: 3.5rem;
font-size: 1.2rem;
}
`;

export const CheckboxWrapper = styled.li<OptionSwitchStyleProps>`
display: flex;
align-items: center;
justify-content: center;
width: 50%;
height: 100%;
background-color: ${({ $isChecked, theme }) => ($isChecked ? theme.colors.white : theme.colors.lightGray)};
border-radius: ${({ theme }) => theme.borderRadius.basic};
transition: background-color 0.2s ease-out;
&:hover {
background-color: ${({ $isChecked, theme }) => ($isChecked ? theme.colors.white : theme.colors.lightPurple)};
}
`;

export const CheckboxButton = styled.button<OptionSwitchStyleProps>`
user-select: none;
font-size: 1.2rem;
color: ${({ $isChecked, theme }) => ($isChecked ? theme.colors.primary : theme.colors.black)};
`;
1 change: 1 addition & 0 deletions frontend/src/components/common/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ export { default as Checkbox } from './Checkbox';
export { default as CheckboxItem } from './CheckboxItem';
export { default as EyeButton } from './EyeButton';
export { default as Carousel } from './Carousel';
export { default as OptionSwitch } from './OptionSwitch';
export * from './modals';
1 change: 0 additions & 1 deletion frontend/src/components/layouts/PageLayout/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { TopButton } from '@/components/common';
import Breadcrumb from '@/components/common/Breadcrumb';
import useBreadcrumbPaths from '@/hooks/useBreadcrumbPaths';
import { EssentialPropsWithChildren } from '@/types';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { calculateParticle } from '@/utils';

import * as S from './styles';

export interface ReviewInfoSectionProps {
revieweeName: string;
isReviewList: boolean;
projectName: string;
reviewCount?: number;
}

const ReviewInfoSection = ({ projectName, revieweeName, reviewCount, isReviewList }: ReviewInfoSectionProps) => {
const revieweeNameWithParticle = calculateParticle({
target: revieweeName,
particles: { withFinalConsonant: '이', withoutFinalConsonant: '가' },
});

const getReviewInfoMessage = () => {
return isReviewList
? `${revieweeNameWithParticle} 받은 ${reviewCount}개의 리뷰 목록이에요`
: `${revieweeNameWithParticle} 받은 리뷰를 질문별로 모아봤어요`;
};

return (
<S.ReviewInfoContainer>
<S.ProjectName>{projectName}</S.ProjectName>
<S.RevieweeInfoWrapper>
<S.RevieweeName>{revieweeName}</S.RevieweeName>
{getReviewInfoMessage()}
</S.RevieweeInfoWrapper>
</S.ReviewInfoContainer>
);
};

export default ReviewInfoSection;
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ import media from '@/utils/media';
export const ReviewInfoContainer = styled.div`
display: flex;
flex-direction: column;
margin: 2rem 0 3rem 1rem;
justify-content: flex-end;
margin: 2rem 0 3rem 0;
${media.small} {
margin-bottom: 1.8rem;
margin-bottom: 1rem;
}
`;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { useLocation, useNavigate } from 'react-router';

import { OptionSwitchOption } from '@/components/common/OptionSwitch';
import { ROUTE } from '@/constants/route';
import { useSearchParamAndQuery } from '@/hooks';

const useReviewDisplayLayoutOptions = () => {
const { pathname } = useLocation();
const navigate = useNavigate();

const { param: reviewRequestCode } = useSearchParamAndQuery({
paramKey: 'reviewRequestCode',
});

const isReviewCollection = pathname.includes(ROUTE.reviewCollection);

const reviewDisplayLayoutOptions: OptionSwitchOption[] = [
{
label: '목록보기',
isChecked: !isReviewCollection,
handleOptionClick: () => navigate(`/${ROUTE.reviewList}/${reviewRequestCode}`),
},
{
label: '모아보기',
isChecked: isReviewCollection,
handleOptionClick: () => navigate(`/${ROUTE.reviewCollection}/${reviewRequestCode}`),
},
];

return [...reviewDisplayLayoutOptions];
};

export default useReviewDisplayLayoutOptions;
34 changes: 34 additions & 0 deletions frontend/src/components/layouts/ReviewDisplayLayout/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { TopButton, OptionSwitch } from '@/components/common';
import { EssentialPropsWithChildren } from '@/types';

import ReviewInfoSection, { ReviewInfoSectionProps } from './components/ReviewInfoSection';
import useReviewDisplayLayoutOptions from './hooks/useReviewDisplayLayoutOptions';
import * as S from './styles';

const ReviewDisplayLayout = ({
revieweeName,
projectName,
reviewCount,
isReviewList,
children,
}: EssentialPropsWithChildren<ReviewInfoSectionProps>) => {
const reviewDisplayLayoutOptions = useReviewDisplayLayoutOptions();

return (
<S.ReviewDisplayLayout>
<S.Container>
<ReviewInfoSection
revieweeName={revieweeName}
projectName={projectName}
reviewCount={reviewCount}
isReviewList={isReviewList}
/>
<OptionSwitch options={reviewDisplayLayoutOptions} />
</S.Container>
<TopButton />
{children}
</S.ReviewDisplayLayout>
);
};

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

export const ReviewDisplayLayout = styled.div`
display: flex;
flex-direction: column;
width: 90%;
min-height: inherit;
`;

export const Container = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
@media screen and (max-width: 500px) {
flex-direction: column;
align-items: flex-start;
margin-bottom: 2.5rem;
}
`;
1 change: 1 addition & 0 deletions frontend/src/constants/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ export const ROUTE = {
reviewWritingComplete: 'user/review-writing-complete',
detailedReview: 'user/detailed-review',
reviewZone: 'user/review-zone',
reviewCollection: 'user/review-collection',
};
Loading

0 comments on commit a06f159

Please sign in to comment.