diff --git a/frontend/src/mocks/mockData/reviewCollection.ts b/frontend/src/mocks/mockData/reviewCollection.ts
index a2db044e2..94aa20786 100644
--- a/frontend/src/mocks/mockData/reviewCollection.ts
+++ b/frontend/src/mocks/mockData/reviewCollection.ts
@@ -33,7 +33,7 @@ export const GROUPED_REVIEWS_MOCK_DATA: GroupedReviews = {
{ content: '팀의 분위기를 주도해요', count: 3 },
{ content: '주장을 이야기할 때에는 합당한 근거가 뒤따라요', count: 2 },
{ content: '팀에게 필요한 것과 그렇지 않은 것을 잘 구분해요', count: 2 },
- { content: '팀 내 주어진 요구사항에 우선순위를 잘 매겨요 (커뮤니케이션 능력을 특화하자)', count: 1 },
+ { content: '팀 내 주어진 요구사항에 우선순위를 잘 매겨요', count: 1 },
{ content: '서로 다른 분야간의 소통도 중요하게 생각해요', count: 1 },
],
},
diff --git a/frontend/src/pages/ReviewCollectionPage/components/DoughnutChart/index.tsx b/frontend/src/pages/ReviewCollectionPage/components/DoughnutChart/index.tsx
new file mode 100644
index 000000000..7464323cf
--- /dev/null
+++ b/frontend/src/pages/ReviewCollectionPage/components/DoughnutChart/index.tsx
@@ -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 (
+
+
+
+
+ );
+};
+
+export default DoughnutChart;
diff --git a/frontend/src/pages/ReviewCollectionPage/components/DoughnutChart/styles.ts b/frontend/src/pages/ReviewCollectionPage/components/DoughnutChart/styles.ts
new file mode 100644
index 000000000..af15be420
--- /dev/null
+++ b/frontend/src/pages/ReviewCollectionPage/components/DoughnutChart/styles.ts
@@ -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;
+ }
+`;
diff --git a/frontend/src/pages/ReviewCollectionPage/components/DoughnutChartDetails/index.tsx b/frontend/src/pages/ReviewCollectionPage/components/DoughnutChartDetails/index.tsx
new file mode 100644
index 000000000..93a2e7883
--- /dev/null
+++ b/frontend/src/pages/ReviewCollectionPage/components/DoughnutChartDetails/index.tsx
@@ -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 (
+
+ {reviewVotes.map((reviewVote, index) => (
+
+
+
+ {reviewVote.content}
+
+ {reviewVote.count}표
+
+ ))}
+
+ );
+};
+
+export default DoughnutChartDetails;
diff --git a/frontend/src/pages/ReviewCollectionPage/components/DoughnutChartDetails/styles.ts b/frontend/src/pages/ReviewCollectionPage/components/DoughnutChartDetails/styles.ts
new file mode 100644
index 000000000..899ae23b6
--- /dev/null
+++ b/frontend/src/pages/ReviewCollectionPage/components/DoughnutChartDetails/styles.ts
@@ -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};
+ }
+`;
diff --git a/frontend/src/pages/ReviewCollectionPage/index.tsx b/frontend/src/pages/ReviewCollectionPage/index.tsx
index b5ee31492..3bd098ace 100644
--- a/frontend/src/pages/ReviewCollectionPage/index.tsx
+++ b/frontend/src/pages/ReviewCollectionPage/index.tsx
@@ -5,6 +5,7 @@ import ReviewDisplayLayout from '@/components/layouts/ReviewDisplayLayout';
import { useGetReviewList } from '@/hooks';
import { GROUPED_REVIEWS_MOCK_DATA, GROUPED_SECTION_MOCK_DATA } from '@/mocks/mockData/reviewCollection';
+import DoughnutChart from './components/DoughnutChart';
import * as S from './styles';
const ReviewCollectionPage = () => {
@@ -34,7 +35,7 @@ const ReviewCollectionPage = () => {
return (
{review.question.type === 'CHECKBOX' ? (
- 객관식 통계 차트
+
) : (
{review.answers?.map((answer, index) => {
diff --git a/frontend/src/pages/ReviewCollectionPage/utils/generateGradientColors.ts b/frontend/src/pages/ReviewCollectionPage/utils/generateGradientColors.ts
new file mode 100644
index 000000000..87059804b
--- /dev/null
+++ b/frontend/src/pages/ReviewCollectionPage/utils/generateGradientColors.ts
@@ -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;