-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[FE] feat: 리뷰 모아보기에서 질문별 통계 차트 구현 (#803)
* 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: 목 데이터를 사용하여 모아보기 페이지 퍼블리싱 * feat: 질문별 통계 차트 구현 * feat: 통계 차트 세부사항 표시 * feat: 질문 길이에 따라 색상을 생성하는 함수 구현 * chore: DoughnutChart 파일 위치 변경 * feat: 차트 애니메이션 적용 및 비율을 텍스트로 시각화 * chore: 비율을 'n표' 형식으로 수정 * chore: 불필요한 텍스트 제거 * feat: ReviewCollectionPage에 DoughnutChart 컴포넌트 적용 * feat: 통계 차트에 반응형 적용 * refactor: theme에 있는 primary 색상 활용 * chore: 불필요한 코드 제거 * refactor: 차트 애니메이션 제거 * chore: Accordion에 넘겨주는 props 변수명 수정 --------- Co-authored-by: ImxYJL <allensain14@gmail.com> Co-authored-by: chysis <chysiss@naver.com>
- Loading branch information
1 parent
2878453
commit 50dc8ed
Showing
7 changed files
with
219 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
84 changes: 84 additions & 0 deletions
84
frontend/src/pages/ReviewCollectionPage/components/DoughnutChart/index.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
import theme from '@/styles/theme'; | ||
import { ReviewVotes } from '@/types'; | ||
|
||
import generateGradientColors from '../../utils/generateGradientColors'; | ||
import DoughnutChartDetails from '../DoughnutChartDetails'; | ||
|
||
import * as S from './styles'; | ||
|
||
const DOUGHNUT_COLOR = { | ||
START: `${theme.colors.primary}`, | ||
END: '#e7e3f9', | ||
}; | ||
|
||
const DoughnutChart = ({ reviewVotes }: { reviewVotes: ReviewVotes[] }) => { | ||
const radius = 90; // 차트의 반지름 | ||
const circumference = 2 * Math.PI * radius; // 차트의 둘레 | ||
const centerX = 125; // svg의 중앙 좌표 (x) | ||
const centerY = 125; // svg의 중앙 좌표 (y) | ||
|
||
const total = reviewVotes.reduce((acc, reviewVote) => acc + reviewVote.count, 0); | ||
const ratios = reviewVotes.map((reviewVote) => reviewVote.count / total); | ||
|
||
// 누적 값 계산 | ||
const acc = reviewVotes.reduce( | ||
(arr, reviewVote) => { | ||
const last = arr[arr.length - 1]; | ||
return [...arr, last + reviewVote.count]; // 현재 값과 이전 누적 값을 더해 새로운 배열 반환 | ||
}, | ||
[0], | ||
); | ||
|
||
// 색상 시작 및 끝값 정의 | ||
const colors = generateGradientColors(reviewVotes.length, DOUGHNUT_COLOR.START, DOUGHNUT_COLOR.END); | ||
|
||
// 각 조각의 중심 좌표를 계산하는 함수 | ||
const calculateLabelPosition = (startAngle: number, endAngle: number) => { | ||
const midAngle = (startAngle + endAngle) / 2; // 중간 각도 | ||
const labelRadius = radius * 1; // 텍스트가 배치될 반지름 (차트 내부) | ||
const x = centerX + labelRadius * Math.cos((midAngle * Math.PI) / 180); | ||
const y = centerY + labelRadius * Math.sin((midAngle * Math.PI) / 180); | ||
return { x, y }; | ||
}; | ||
|
||
return ( | ||
<S.DoughnutChartContainer> | ||
<svg viewBox="0 0 250 250" width="250" height="250"> | ||
{reviewVotes.map((reviewVote, index) => { | ||
const ratio = reviewVote.count / total; | ||
const fillSpace = circumference * ratio; | ||
const emptySpace = circumference - fillSpace; | ||
const offset = (acc[index] / total) * circumference; | ||
|
||
// 시작 각도와 끝 각도를 계산 | ||
const startAngle = (acc[index] / total) * 360 + 90; | ||
const endAngle = ((acc[index] + reviewVote.count) / total) * 360 - 90; | ||
|
||
// 비율 레이블의 위치를 계산 | ||
const { x, y } = calculateLabelPosition(startAngle, endAngle); | ||
|
||
return ( | ||
<g key={index}> | ||
<circle | ||
cx={centerX} // 중앙에 배치 | ||
cy={centerY} | ||
r={radius} | ||
fill="none" | ||
stroke={colors[index]} | ||
strokeWidth="65" | ||
strokeDasharray={`${fillSpace} ${emptySpace}`} // 조각의 길이와 나머지 길이 설정 | ||
strokeDashoffset={-offset} // 시작 위치 설정 | ||
/> | ||
<text x={x} y={y} textAnchor="middle" dominantBaseline="middle" fontSize="14"> | ||
{Math.floor(ratios[index] * 100)}% | ||
</text> | ||
</g> | ||
); | ||
})} | ||
</svg> | ||
<DoughnutChartDetails reviewVotes={reviewVotes} colors={colors} /> | ||
</S.DoughnutChartContainer> | ||
); | ||
}; | ||
|
||
export default DoughnutChart; |
15 changes: 15 additions & 0 deletions
15
frontend/src/pages/ReviewCollectionPage/components/DoughnutChart/styles.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import styled from '@emotion/styled'; | ||
|
||
import media from '@/utils/media'; | ||
|
||
export const DoughnutChartContainer = styled.div` | ||
display: flex; | ||
justify-content: center; | ||
align-items: center; | ||
gap: 5rem; | ||
${media.small} { | ||
flex-direction: column; | ||
} | ||
`; |
26 changes: 26 additions & 0 deletions
26
frontend/src/pages/ReviewCollectionPage/components/DoughnutChartDetails/index.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import { ReviewVotes } from '@/types'; | ||
|
||
import * as S from './styles'; | ||
|
||
interface DoughnutChartDetails { | ||
reviewVotes: ReviewVotes[]; | ||
colors: string[]; | ||
} | ||
|
||
const DoughnutChartDetails = ({ reviewVotes, colors }: DoughnutChartDetails) => { | ||
return ( | ||
<S.DoughnutChartDetailList> | ||
{reviewVotes.map((reviewVote, index) => ( | ||
<S.DetailItem key={reviewVote.content}> | ||
<S.ContentContainer> | ||
<S.ChartColor color={colors[index]}></S.ChartColor> | ||
<S.Description>{reviewVote.content}</S.Description> | ||
</S.ContentContainer> | ||
<S.ReviewVoteResult>{reviewVote.count}표</S.ReviewVoteResult> | ||
</S.DetailItem> | ||
))} | ||
</S.DoughnutChartDetailList> | ||
); | ||
}; | ||
|
||
export default DoughnutChartDetails; |
58 changes: 58 additions & 0 deletions
58
frontend/src/pages/ReviewCollectionPage/components/DoughnutChartDetails/styles.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
import styled from '@emotion/styled'; | ||
|
||
import media from '@/utils/media'; | ||
|
||
export const DoughnutChartDetailList = styled.div` | ||
display: flex; | ||
flex-direction: column; | ||
gap: 2rem; | ||
margin: 2rem; | ||
${media.small} { | ||
margin: 0 1rem; | ||
} | ||
`; | ||
|
||
export const DetailItem = styled.div` | ||
display: flex; | ||
justify-content: space-between; | ||
align-items: center; | ||
gap: 1rem; | ||
`; | ||
|
||
export const ContentContainer = styled.div` | ||
display: flex; | ||
align-items: center; | ||
gap: 1rem; | ||
`; | ||
|
||
export const ChartColor = styled.div<{ color: string }>` | ||
background-color: ${({ color }) => color}; | ||
width: 2rem; | ||
height: 2rem; | ||
border-radius: 0.5rem; | ||
flex-shrink: 0; | ||
${media.small} { | ||
width: 1.6rem; | ||
height: 1.6rem; | ||
} | ||
`; | ||
|
||
export const Description = styled.span` | ||
${media.small} { | ||
font-size: ${({ theme }) => theme.fontSize.small}; | ||
} | ||
`; | ||
|
||
export const ReviewVoteResult = styled.span` | ||
${media.small} { | ||
font-size: ${({ theme }) => theme.fontSize.small}; | ||
} | ||
`; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
33 changes: 33 additions & 0 deletions
33
frontend/src/pages/ReviewCollectionPage/utils/generateGradientColors.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
// Hex 색상을 RGB로 변환하는 함수 | ||
const hexToRGB = (hex: string) => { | ||
const bigint = parseInt(hex.slice(1), 16); | ||
return [bigint >> 16, (bigint >> 8) & 255, bigint & 255]; | ||
}; | ||
|
||
// RGB 색상을 Hex로 변환하는 함수 | ||
const rgbToHex = (r: number, g: number, b: number) => { | ||
return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase()}`; | ||
}; | ||
|
||
// 두 색상 사이의 색상을 계산하는 함수 | ||
const interpolateColor = (start: number[], end: number[], factor: number) => { | ||
const result = start.map((startValue, index) => Math.round(startValue + factor * (end[index] - startValue))); | ||
return result; | ||
}; | ||
|
||
// reviewVotes 길이에 따라 색상 배열을 생성하는 함수 | ||
const generateGradientColors = (length: number, startHex: string, endHex: string) => { | ||
const startColor = hexToRGB(startHex); | ||
const endColor = hexToRGB(endHex); | ||
const colors = []; | ||
|
||
for (let i = 0; i < length; i++) { | ||
const factor = i / (length - 1); | ||
const color = interpolateColor(startColor, endColor, factor); | ||
colors.push(rgbToHex(color[0], color[1], color[2])); | ||
} | ||
|
||
return colors; | ||
}; | ||
|
||
export default generateGradientColors; |