Skip to content

Commit

Permalink
[FE] feat: 리뷰 모아보기 페이지의 공통 컴포넌트 구현 및 퍼블리싱 (#790)
Browse files Browse the repository at this point in the history
* feat: Switch 컴포넌트 제작

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

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

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

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

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

* feat: OptionSwitch 반응형 적용

* refactor: ReviewDisplayLayout 반응형 수정

* feat: 공통 Dropdown 컴포넌트 작성

* design: 화살표 버튼 오른쪽 고정 및 옵션을 드래그하지 못하게 수정

* feat: Dropdown 외부 클릭 시 닫히도록 하는 기능 구현

* refactor: Dropdown 로직을 훅으로 분리

* chore: props 명칭 변경 및 선택된 아이템 ellipsis 처리

* feat: Accordion 공통 컴포넌트 작성

* chore: index에 Dropdown, Accordion 추가

* feat: theme에 Dropdown의 z-index 추가

* chore: 누락된 index 추가

* design: Dropdown border 색상 변경

* refactor: Accordion 로직 훅으로 분리

* fix: px을 rem으로 수정

* design: Dropdown 및 Accordion의 margin-bottom 속성 제거

* feat: 초기에 열려있는 Accordion 구현을 위해 prop 추가

* feat: 모아보기 페이지 type 정의

* feat: 모아보기 페이지 목 데이터 작성

* design: Accordion 컴포넌트에서 불필요한 props 제거

* design: Accordion 반응형 구현

* feat: 목 데이터를 사용하여 모아보기 페이지 퍼블리싱

* design: Accordion height 수정 및 스타일 인터페이스 정의

* style: prop명 변경

* design: Dropdown 스타일 인터페이스 정의

* feat: Accordion 제목 왼쪽 정렬 및 애니메이션 추가

* style: 바뀐 prop명 적용

* design: Dropdown이 500px 이하에서 왼쪽 정렬되도록 수정

* merge

---------

Co-authored-by: ImxYJL <allensain14@gmail.com>
  • Loading branch information
2 people authored and skylar1220 committed Nov 5, 2024
1 parent a06f159 commit 2878453
Show file tree
Hide file tree
Showing 14 changed files with 464 additions and 8 deletions.
3 changes: 3 additions & 0 deletions frontend/src/assets/downArrow.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
40 changes: 40 additions & 0 deletions frontend/src/components/common/Accordion/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { useEffect, useRef, useState } from 'react';

import DownArrowIcon from '@/assets/downArrow.svg';
import useAccordion from '@/hooks/useAccordion';
import { EssentialPropsWithChildren } from '@/types';

import * as S from './styles';

interface AccordionProps {
title: string;
isInitiallyOpened?: boolean;
}

const Accordion = ({ title, isInitiallyOpened = false, children }: EssentialPropsWithChildren<AccordionProps>) => {
const { isOpened, handleAccordionButtonClick } = useAccordion({ isInitiallyOpened });
const [contentHeight, setContentHeight] = useState(0);
const contentRef = useRef<HTMLDivElement>(null);

useEffect(() => {
if (contentRef.current) {
setContentHeight(contentRef.current.clientHeight);
}
}, [isOpened]);

return (
<S.AccordionContainer $isOpened={isOpened}>
<S.AccordionButton onClick={handleAccordionButtonClick}>
<S.AccordionTitle>{title}</S.AccordionTitle>
<S.ArrowIcon src={DownArrowIcon} $isOpened={isOpened} alt="" />
</S.AccordionButton>
<S.AccordionContentsWrapper>
<S.AccordionContents $isOpened={isOpened} $contentHeight={contentHeight} ref={contentRef}>
{children}
</S.AccordionContents>
</S.AccordionContentsWrapper>
</S.AccordionContainer>
);
};

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

interface AccordionStyleProps {
$isOpened: boolean;
$contentHeight?: number;
}

export const AccordionContainer = styled.div<AccordionStyleProps>`
display: flex;
flex-direction: column;
gap: ${({ $isOpened }) => ($isOpened ? '2rem' : 0)};
width: 100%;
padding: 1rem;
background-color: ${({ theme, $isOpened }) => ($isOpened ? theme.colors.white : theme.colors.lightGray)};
border: 0.1rem solid ${({ theme }) => theme.colors.placeholder};
border-radius: ${({ theme }) => theme.borderRadius.basic};
&:hover {
border: 0.1rem solid ${({ theme }) => theme.colors.primaryHover};
}
`;

export const AccordionButton = styled.button`
display: flex;
gap: 1rem;
align-items: center;
justify-content: space-between;
width: 100%;
height: fit-content;
min-height: 3rem;
`;

export const AccordionTitle = styled.p`
text-align: left;
::before {
content: 'Q. ';
}
`;

export const ArrowIcon = styled.img<AccordionStyleProps>`
transform: ${({ $isOpened }) => ($isOpened ? 'rotate(180deg)' : 'rotate(0deg)')};
transition: transform 0.3s ease-in-out;
`;

export const AccordionContentsWrapper = styled.div`
overflow: hidden;
`;

export const AccordionContents = styled.div<AccordionStyleProps>`
margin-top: ${({ $isOpened, $contentHeight }) => ($isOpened ? '0' : `-${$contentHeight}px`)};
opacity: ${({ $isOpened }) => ($isOpened ? '1' : '0')};
transition: 0.3s ease;
`;
41 changes: 41 additions & 0 deletions frontend/src/components/common/Dropdown/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import DownArrowIcon from '@/assets/downArrow.svg';
import useDropdown from '@/hooks/useDropdown';

import * as S from './styles';

interface DropdownItem {
text: string;
value: string;
}

interface DropdownProps {
items: DropdownItem[];
selectedItem: string;
handleSelect: (item: string) => void;
}

const Dropdown = ({ items, selectedItem: selectedOption, handleSelect }: DropdownProps) => {
const { isOpened, handleDropdownButtonClick, handleOptionClick, dropdownRef } = useDropdown({ handleSelect });

return (
<S.DropdownContainer ref={dropdownRef}>
<S.DropdownButton onClick={handleDropdownButtonClick}>
<S.SelectedOption>{selectedOption}</S.SelectedOption>
<S.ArrowIcon src={DownArrowIcon} $isOpened={isOpened} alt="" />
</S.DropdownButton>
{isOpened && (
<S.ItemContainer>
{items.map((item) => {
return (
<S.DropdownItem key={item.value} onClick={() => handleOptionClick(item.value)}>
{item.text}
</S.DropdownItem>
);
})}
</S.ItemContainer>
)}
</S.DropdownContainer>
);
};

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

interface DropdownStyleProps {
$isOpened: boolean;
}

export const DropdownContainer = styled.div`
position: relative;
display: flex;
flex-direction: column;
width: 24rem;
`;

export const DropdownButton = styled.button`
display: flex;
gap: 1rem;
justify-content: space-between;
width: 100%;
padding: 1rem;
background-color: ${({ theme }) => theme.colors.white};
border: 0.1rem solid ${({ theme }) => theme.colors.placeholder};
border-radius: ${({ theme }) => theme.borderRadius.basic};
&:hover {
background-color: ${({ theme }) => theme.colors.lightGray};
}
`;

export const SelectedOption = styled.p`
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
`;

export const ArrowIcon = styled.img<DropdownStyleProps>`
transform: ${({ $isOpened }) => ($isOpened ? 'rotate(180deg)' : 'rotate(0deg)')};
transition: transform 0.3s ease-in-out;
`;

export const ItemContainer = styled.ul`
position: absolute;
z-index: ${({ theme }) => theme.zIndex.dropdown};
top: 100%;
overflow: hidden;
width: 100%;
border: 0.1rem solid ${({ theme }) => theme.colors.placeholder};
border-radius: ${({ theme }) => theme.borderRadius.basic};
`;

export const DropdownItem = styled.li`
cursor: pointer;
user-select: none;
display: flex;
align-items: center;
width: 100%;
height: 4rem;
padding: 0 1rem;
background-color: ${({ theme }) => theme.colors.white};
&:hover {
background-color: ${({ theme }) => theme.colors.lightGray};
}
`;
3 changes: 3 additions & 0 deletions frontend/src/components/common/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,8 @@ 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 Accordion } from './Accordion';
export { default as Dropdown } from './Dropdown';

export { default as OptionSwitch } from './OptionSwitch';
export * from './modals';
4 changes: 4 additions & 0 deletions frontend/src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ export { default as useSidebar } from './useSidebar';
export { default as useSearchParamAndQuery } from './useSearchParamAndQuery';
export { default as useEyeButton } from './useEyeButton';
export { default as usePasswordValidation } from './usePasswordValidation';
export { default as useDropdown } from './useDropdown';
export { default as useAccordion } from './useAccordion';
export { default as useBreadcrumbPaths } from './useBreadcrumbPaths';
export { default as useTopButton } from './useTopButton';
export * from './review';
export * from './reviewGroup';
export * from './modal';
20 changes: 20 additions & 0 deletions frontend/src/hooks/useAccordion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { useState } from 'react';

interface UseAccordionProps {
isInitiallyOpened: boolean;
}

const useAccordion = ({ isInitiallyOpened }: UseAccordionProps) => {
const [isOpened, setIsOpened] = useState(isInitiallyOpened);

const handleAccordionButtonClick = () => {
setIsOpened((prev) => !prev);
};

return {
isOpened,
handleAccordionButtonClick,
};
};

export default useAccordion;
38 changes: 38 additions & 0 deletions frontend/src/hooks/useDropdown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { useEffect, useRef, useState } from 'react';

interface UseDropdownProps {
handleSelect: (option: string) => void;
}

const useDropdown = ({ handleSelect }: UseDropdownProps) => {
const [isOpened, setIsOpened] = useState(false);

const dropdownRef = useRef<HTMLDivElement>(null);

const handleDropdownButtonClick = () => {
setIsOpened((prev) => !prev);
};

const handleOptionClick = (option: string) => {
handleSelect(option);
setIsOpened(false);
};

const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOpened(false);
}
};

useEffect(() => {
document.addEventListener('mousedown', handleClickOutside);

return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [dropdownRef]);

return { isOpened, handleDropdownButtonClick, handleOptionClick, dropdownRef };
};

export default useDropdown;
66 changes: 66 additions & 0 deletions frontend/src/mocks/mockData/reviewCollection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { GroupedReviews, GroupedSection, ReviewSummary } from '@/types';

export const REVIEW_SUMMARY_MOCK_DATA: ReviewSummary = {
projectName: '리뷰미',
revieweeName: '에프이',
reviewCount: 5,
};

export const GROUPED_SECTION_MOCK_DATA: GroupedSection = {
sections: [
{ id: 0, name: '강점 카테고리' },
{ id: 1, name: '커뮤니케이션, 협업 능력' },
{ id: 2, name: '문제 해결 능력' },
{ id: 3, name: '시간 관리 능력' },
{ id: 4, name: '기술 역량, 전문 지식' },
{ id: 5, name: '성장 마인드셋' },
{ id: 6, name: '단점 피드백' },
{ id: 7, name: '추가 리뷰 및 응원' },
],
};

export const GROUPED_REVIEWS_MOCK_DATA: GroupedReviews = {
reviews: [
{
question: {
name: '커뮤니케이션, 협업 능력에서 어떤 부분이 인상 깊었는지 선택해주세요',
type: 'CHECKBOX',
},
answers: null,
votes: [
{ content: '반대 의견을 내더라도 듣는 사람이 기분 나쁘지 않게 이야기해요', count: 5 },
{ content: '팀원들의 의견을 잘 모아서 회의가 매끄럽게 진행되도록 해요', count: 4 },
{ content: '팀의 분위기를 주도해요', count: 3 },
{ content: '주장을 이야기할 때에는 합당한 근거가 뒤따라요', count: 2 },
{ content: '팀에게 필요한 것과 그렇지 않은 것을 잘 구분해요', count: 2 },
{ content: '팀 내 주어진 요구사항에 우선순위를 잘 매겨요 (커뮤니케이션 능력을 특화하자)', count: 1 },
{ content: '서로 다른 분야간의 소통도 중요하게 생각해요', count: 1 },
],
},
{
question: {
name: '위에서 선택한 사항에 대해 조금 더 자세히 설명해주세요',
type: 'TEXT',
},
answers: [
{
content:
'장의 시작부분은 짧고 직접적이며, 뒤따라 나올 복잡한 정보를 어떻게 해석해야 할 것인지 프레임을 짜주는 역할을 해야 한다. 그러면 아무리 긴 문장이라도 쉽게 읽힌다.',
},
{
content:
'고액공제건강보험과 건강저축계좌를 만들어 노동자와 고용주가 세금공제를 받을 수 있도록 하면 결과적으로 노동자의 의료보험 부담이 커진다.',
},
{
content:
'장의 시작부분은 짧고 직접적이며, 뒤따라 나올 복잡한 정보를 어떻게 해석해야 할 것인지 프레임을 짜주는 역할을 해야 한다. 그러면 아무리 긴 문장이라도 쉽게 읽힌다.',
},
{
content:
'고액공제건강보험과 건강저축계좌를 만들어 노동자와 고용주가 세금공제를 받을 수 있도록 하면 결과적으로 노동자의 의료보험 부담이 커진다.',
},
],
votes: null,
},
],
};
Loading

0 comments on commit 2878453

Please sign in to comment.