Skip to content

Commit

Permalink
[FE] fix: 차트에서 0% 비율 제외 및 객관식 질문을 투표수 기준으로 내림차순 정렬 (#837)
Browse files Browse the repository at this point in the history
* fix: preview가 짧을 때 스타일이 깨지던 현상 수정

* chore: 목 데이터 count 값 수정

* fix: 0%인 객관식 질문을 차트에서 제외

* feat: 객관식 질문을 투표수에 따라 내림차순 정렬

* refactor: 차트 비율을 소수점 첫째 자리까지 반올림

* refactor: 차트 관련 상수 정의

* feat: 여러 줄(3줄)에 대한 ellipsis 설정

* chore: 리뷰 목록의 일부 모킹 데이터 수정 - 가짜 말줄임표 제거

* chore: 불필요한 속성 제거

* refactor: 차트 색상 객체의 키 값을 카멜케이스로 수정

---------

Co-authored-by: ImxYJL <allensain14@gmail.com>
  • Loading branch information
soosoo22 and ImxYJL authored Oct 16, 2024
1 parent be8f2f1 commit ff53dbe
Show file tree
Hide file tree
Showing 7 changed files with 124 additions and 45 deletions.
2 changes: 1 addition & 1 deletion frontend/src/components/ReviewCard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const ReviewCard = ({ createdAt, contentPreview, categories, handleClick }: Revi
<S.LeftLineBorder />

<S.Main>
<span>{contentPreview}</span>
<S.ContentPreview>{contentPreview}</S.ContentPreview>
<S.Footer>
<S.Keyword>
{categories.map((category) => (
Expand Down
21 changes: 16 additions & 5 deletions frontend/src/components/ReviewCard/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export const Layout = styled.div`
`;

export const LeftLineBorder = styled.div`
width: 5rem;
width: 2.5rem;
background-color: ${({ theme }) => theme.colors.lightGray};
border-radius: 1rem 0 0 1rem;
`;
Expand All @@ -32,7 +32,6 @@ 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 All @@ -53,19 +52,31 @@ export const Main = styled.div`
flex-direction: column;
gap: 2rem;
width: 100%;
padding: 2rem 3rem;
font-size: 1.6rem;
`;

span {
overflow-wrap: break-word;
}
export const ContentPreview = styled.p`
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
height: 6rem;
padding-right: 2rem;
line-height: 2rem;
text-overflow: ellipsis;
overflow-wrap: break-word;
`;

export const Footer = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
${media.small} {
flex-direction: column;
Expand Down
10 changes: 5 additions & 5 deletions frontend/src/mocks/mockData/reviewCollection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,11 @@ export const GROUPED_REVIEWS_MOCK_DATA: GroupedReviews = {
},
answers: null,
votes: [
{ content: '반대 의견을 내더라도 듣는 사람이 기분 나쁘지 않게 이야기해요', count: 5 },
{ content: '팀원들의 의견을 잘 모아서 회의가 매끄럽게 진행되도록 해요', count: 4 },
{ content: '팀의 분위기를 주도해요', count: 3 },
{ content: '주장을 이야기할 때에는 합당한 근거가 뒤따라요', count: 2 },
{ content: '팀에게 필요한 것과 그렇지 않은 것을 잘 구분해요', count: 2 },
{ content: '반대 의견을 내더라도 듣는 사람이 기분 나쁘지 않게 이야기해요', count: 13 },
{ content: '팀원들의 의견을 잘 모아서 회의가 매끄럽게 진행되도록 해요', count: 0 },
{ content: '팀의 분위기를 주도해요', count: 5 },
{ content: '주장을 이야기할 때에는 합당한 근거가 뒤따라요', count: 3 },
{ content: '팀에게 필요한 것과 그렇지 않은 것을 잘 구분해요', count: 0 },
{ content: '팀 내 주어진 요구사항에 우선순위를 잘 매겨요', count: 1 },
{ content: '서로 다른 분야간의 소통도 중요하게 생각해요', count: 1 },
],
Expand Down
54 changes: 47 additions & 7 deletions frontend/src/mocks/mockData/reviewListMockData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export const REVIEW_LIST: ReviewList = {
{
reviewId: 5,
createdAt: '2024-07-24',
contentPreview: `1. 물론 시중에 출간되어 있는 책들로 공부하는 것도 큰 장점이지만 더 깊은 공부를 하고 싶을 때 공식 문서를 확인해보는 것이 좋기 때문에, 저 개인적인 생각으로는 언어 공부를 아예 처음 입문하시는 분들은 한국에서 출간된 개발 서적으로 공부를 시작하시다가 모르는 부분이.....`,
contentPreview: `1. 나는 짧은 데이터`,
categories: [
{ optionId: 1, content: '🗣️ 커뮤니케이션, 협업 능력' },
{ optionId: 5, content: '🌱 성장 마인드셋' },
Expand All @@ -17,7 +17,7 @@ export const REVIEW_LIST: ReviewList = {
{
reviewId: 2,
createdAt: '2023-08-29',
contentPreview: `2. 하루스터디는 효율적인 공부 방법을 제공하는 학습 진행 도구 서비스입니다. 하루스터디는 목표 설정 단계, 학습 단계, 회고 단계를 반복하는 학습 사이클을 통해 학습 효율을 끌어올립니다. 하루스터디를 사용하게 되면 '학습을 잘 하는 방법'에 대해서...`,
contentPreview: `2. 전해주고 싶어 슬픈 시간이 다 흩어진 후에야 들리지만 눈을 감고 느껴봐 움직이는 마음 너를 향한 내 눈빛을 특별한 기적을 기다리지마 눈 앞에선 우리의 거친 길은 알 수 없는 미래와 벽 바꾸지 않아 포기할 수 없어 변치 않을 사랑으로 지켜줘 상처 입은 내 맘까지 시선 속에서 말은 필요 없어 멈춰져 버린 이 시간 사랑해 널 이 느낌 이대로 그려왔던 헤매임의 끝 이 세상 속에서 반복되는 슬픔 이젠 안녕 수많은 알 수 없는 길 속에 희미한 빛을 난 쫓아가 언제까지라도 함께 하는거야 다시 만난 나의 세계`,
categories: [
{ optionId: 3, content: '⏰ 시간 관리 능력' },
{ optionId: 4, content: '🤓 기술적 역량, 전문 지식' },
Expand All @@ -26,7 +26,20 @@ export const REVIEW_LIST: ReviewList = {
{
reviewId: 3,
createdAt: '2021-08-01',
contentPreview: `3. 공간을 한 눈에, 예약은 한 번에! 맞춤형 공간예약 서비스 제작 플랫폼 찜꽁입니다! 공간 제공자(관리자)는 에디터를 통해 공간을 생성할 수 있습니다! 생성한 공간은 링크를 통해 사용자에게 제공할 수 있으며, 사용자는 링크를 통해 간편하게 공간을 확인하고 예약을...`,
contentPreview: `3. 'Cause, ah-ah, I'm in the stars tonight
So watch me bring the fire and set the night alight
Shoes on, get up in the morn
Cup of milk, let's rock and roll
King Kong, kick the drum, rolling on like a rolling stone
Sing song when I'm walking home
Jump up to the top, LeBron
Ding dong, call me on my phone
Ice tea and a game of ping pong This is getting heavy
Can you hear the bass boom, I'm ready
Life is sweet as honey
Yeah this beat cha ching like money
Disco overload I'm into that I'm good to go
`,
categories: [
{ optionId: 5, content: '🌱 성장 마인드셋' },
{ optionId: 1, content: '🗣️ 커뮤니케이션, 협업 능력' },
Expand All @@ -35,7 +48,18 @@ export const REVIEW_LIST: ReviewList = {
{
reviewId: 4,
createdAt: '2021-08-01',
contentPreview: `4. 공간을 한 눈에, 예약은 한 번에! 맞춤형 공간예약 서비스 제작 플랫폼 찜꽁입니다! 공간 제공자(관리자)는 에디터를 통해 공간을 생성할 수 있습니다! 생성한 공간은 링크를 통해 사용자에게 제공할 수 있으며, 사용자는 링크를 통해 간편하게 공간을 확인하고 예약을...`,
contentPreview: `4. 솔직히, 말할게 많이 기다려 왔어
너도 그랬을 거라 믿어
오늘이 오길 매일같이 달력을 보면서
솔직히, 나에게도, 지금 이 순간은
꿈만 같아, 너와 함께라
오늘을 위해 꽤 많은 걸 준비해 봤어
All about you and I, 다른 건 다 제쳐 두고
Now come with me, take my hand
아름다운 청춘의 한 장 함께 써내려 가자
너와의 추억들로 가득 채울래 (come on!)
아무 걱정도 하지는 마, 나에게 다 맡겨 봐
`,
categories: [
{ optionId: 1, content: '🗣️ 커뮤니케이션, 협업 능력' },
{ optionId: 2, content: '💡 문제 해결 능력' },
Expand All @@ -44,7 +68,17 @@ export const REVIEW_LIST: ReviewList = {
{
reviewId: 1,
createdAt: '2021-08-01',
contentPreview: `5. 공간을 한 눈에, 예약은 한 번에! 맞춤형 공간예약 서비스 제작 플랫폼 찜꽁입니다! 공간 제공자(관리자)는 에디터를 통해 공간을 생성할 수 있습니다! 생성한 공간은 링크를 통해 사용자에게 제공할 수 있으며, 사용자는 링크를 통해 간편하게 공간을 확인하고 예약을...`,
contentPreview: `5. I'm like some kind of supernova
Watch out
Look at me go, 재미 좀 볼
빛의 core, so hot, hot
문이 열려 서로의 존재를 느껴
마치 Discord, 날 닮은 너 (incoming!), 너 누구야? (Drop)
사건은 다가와, ah-oh, ayy
거세게 커져가, ah-oh, ayy
That tick, that tick, tick bomb
That tick, that tick, tick bomb
`,
categories: [
{ optionId: 1, content: '🗣️ 커뮤니케이션, 협업 능력' },
{ optionId: 2, content: '💡 문제 해결 능력' },
Expand All @@ -53,7 +87,13 @@ export const REVIEW_LIST: ReviewList = {
{
reviewId: 6,
createdAt: '2021-08-01',
contentPreview: `6. 공간을 한 눈에, 예약은 한 번에! 맞춤형 공간예약 서비스 제작 플랫폼 찜꽁입니다! 공간 제공자(관리자)는 에디터를 통해 공간을 생성할 수 있습니다! 생성한 공간은 링크를 통해 사용자에게 제공할 수 있으며, 사용자는 링크를 통해 간편하게 공간을 확인하고 예약을...`,
contentPreview: `6. 네가 참 궁금해 그건 너도 마찬가지 이거면 충분해 쫓고 쫓는 이런 놀이 참을 수 없는 이끌림과 호기심 묘한 너와 나 두고 보면 알겠지 Ooh-ooh, ooh-ooh 눈동자 아래로 Ooh-ooh, ooh-ooh 감추고 있는 거 Narcissistic, my God, I love it
서로를 비춘 밤
아름다운 까만 눈빛 더 빠져 깊이
(넌 내게로, 난 네게로)
숨 참고 love dive
Ooh-ooh, ooh-ooh, lalalala-lalala
`,
categories: [
{ optionId: 5, content: '🌱 성장 마인드셋' },
{ optionId: 1, content: '🗣️ 커뮤니케이션, 협업 능력' },
Expand All @@ -62,7 +102,7 @@ export const REVIEW_LIST: ReviewList = {
{
reviewId: 7,
createdAt: '2021-08-01',
contentPreview: `7. 공간을 한 눈에, 예약은 한 번에! 맞춤형 공간예약 서비스 제작 플랫폼 찜꽁입니다! 공간 제공자(관리자)는 에디터를 통해 공간을 생성할 수 있습니다! 생성한 공간은 링크를 통해 사용자에게 제공할 수 있으며, 사용자는 링크를 통해 간편하게 공간을 확인하고 예약을...`,
contentPreview: `7. 나는 짧은 데이터`,
categories: [
{ optionId: 3, content: '⏰ 시간 관리 능력' },
{ optionId: 2, content: '💡 문제 해결 능력' },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,52 +7,62 @@ import DoughnutChartDetails from '../DoughnutChartDetails';
import * as S from './styles';

const DOUGHNUT_COLOR = {
START: `${theme.colors.primary}`,
END: '#e7e3f9',
start: `${theme.colors.primary}`,
end: '#e7e3f9',
};

const CHART_RADIUS = 90;
const SVG_VIEWBOX = '0 0 250 250';
const SVG_SIZE = 250;
const STROKE_WIDTH = 65;

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 circumference = 2 * Math.PI * CHART_RADIUS; // 차트의 둘레
const centerX = SVG_SIZE / 2; // svg의 중앙 x 좌표
const centerY = SVG_SIZE / 2; // svg의 중앙 y 좌표

const nonZeroReviewVotes = reviewVotes.filter((reviewVote) => reviewVote.count > 0);

const total = reviewVotes.reduce((acc, reviewVote) => acc + reviewVote.count, 0);
const ratios = reviewVotes.map((reviewVote) => reviewVote.count / total);
const totalReviewCount = nonZeroReviewVotes.reduce((acc, reviewVote) => acc + reviewVote.count, 0);
const reviewVoteRatios = nonZeroReviewVotes.map((reviewVote) => reviewVote.count / totalReviewCount);

// 누적 값 계산
const acc = reviewVotes.reduce(
const cumulativeVotes = nonZeroReviewVotes.reduce(
(arr, reviewVote) => {
const last = arr[arr.length - 1];
return [...arr, last + reviewVote.count]; // 현재 값과 이전 누적 값을 더해 새로운 배열 반환
arr.push(arr[arr.length - 1] + reviewVote.count);
return arr;
},
[0],
);

// 색상 시작 및 끝값 정의
const colors = generateGradientColors(reviewVotes.length, DOUGHNUT_COLOR.START, DOUGHNUT_COLOR.END);
const chartColors = generateGradientColors({
length: reviewVotes.length,
startHex: DOUGHNUT_COLOR.start,
endHex: DOUGHNUT_COLOR.end,
});

// 각 조각의 중심 좌표를 계산하는 함수
const calculateLabelPosition = (startAngle: number, endAngle: number) => {
const midAngle = (startAngle + endAngle) / 2; // 중간 각도
const labelRadius = radius * 1; // 텍스트가 배치될 반지름 (차트 내부)
const labelRadius = CHART_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;
<svg viewBox={SVG_VIEWBOX} width={SVG_SIZE} height={SVG_SIZE}>
{nonZeroReviewVotes.map((reviewVote, index) => {
const ratio = reviewVote.count / totalReviewCount;
const fillSpace = circumference * ratio;
const emptySpace = circumference - fillSpace;
const offset = (acc[index] / total) * circumference;
const offset = (cumulativeVotes[index] / totalReviewCount) * circumference;

// 시작 각도와 끝 각도를 계산
const startAngle = (acc[index] / total) * 360 + 90;
const endAngle = ((acc[index] + reviewVote.count) / total) * 360 - 90;
const startAngle = (cumulativeVotes[index] / totalReviewCount) * 360 + 90;
const endAngle = ((cumulativeVotes[index] + reviewVote.count) / totalReviewCount) * 360 - 90;

// 비율 레이블의 위치를 계산
const { x, y } = calculateLabelPosition(startAngle, endAngle);
Expand All @@ -62,21 +72,21 @@ const DoughnutChart = ({ reviewVotes }: { reviewVotes: ReviewVotes[] }) => {
<circle
cx={centerX} // 중앙에 배치
cy={centerY}
r={radius}
r={CHART_RADIUS}
fill="none"
stroke={colors[index]}
strokeWidth="65"
stroke={chartColors[index]}
strokeWidth={STROKE_WIDTH}
strokeDasharray={`${fillSpace} ${emptySpace}`} // 조각의 길이와 나머지 길이 설정
strokeDashoffset={-offset} // 시작 위치 설정
/>
<text x={x} y={y} textAnchor="middle" dominantBaseline="middle" fontSize="14">
{Math.floor(ratios[index] * 100)}%
{(reviewVoteRatios[index] * 100).toFixed(1)}%
</text>
</g>
);
})}
</svg>
<DoughnutChartDetails reviewVotes={reviewVotes} colors={colors} />
<DoughnutChartDetails reviewVotes={reviewVotes} colors={chartColors} />
</S.DoughnutChartContainer>
);
};
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/pages/ReviewCollectionPage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ const ReviewCollectionPage = () => {
const [selectedSection, setSelectedSection] = useState<DropdownItem>(dropdownSectionList[0]);
const { data: groupedReviews } = useGetGroupedReviews({ sectionId: selectedSection.value as number });

groupedReviews.reviews.forEach((review) => {
review.votes?.sort((voteA, voteB) => voteB.count - voteA.count);
});

return (
<ErrorSuspenseContainer fallback={AuthAndServerErrorFallback}>
<ReviewDisplayLayout isReviewList={false}>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
const R_SHIFT = 16;
const G_SHIFT = 8;
const RGB_MAX_VALUE = 255;

// Hex 색상을 RGB로 변환하는 함수
const hexToRGB = (hex: string) => {
const bigint = parseInt(hex.slice(1), 16);
return [bigint >> 16, (bigint >> 8) & 255, bigint & 255];
const r = bigint >> R_SHIFT;
const g = (bigint >> G_SHIFT) & RGB_MAX_VALUE;
const b = bigint & RGB_MAX_VALUE;

return [r, g, b];
};

// RGB 색상을 Hex로 변환하는 함수
const rgbToHex = (r: number, g: number, b: number) => {
return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase()}`;
return `#${((1 << 24) + (r << R_SHIFT) + (g << G_SHIFT) + b).toString(16).slice(1).toUpperCase()}`;
};

// 두 색상 사이의 색상을 계산하는 함수
Expand All @@ -15,8 +23,14 @@ const interpolateColor = (start: number[], end: number[], factor: number) => {
return result;
};

interface GradientColorProps {
length: number;
startHex: string;
endHex: string;
}

// reviewVotes 길이에 따라 색상 배열을 생성하는 함수
const generateGradientColors = (length: number, startHex: string, endHex: string) => {
const generateGradientColors = ({ length, startHex, endHex }: GradientColorProps) => {
const startColor = hexToRGB(startHex);
const endColor = hexToRGB(endHex);
const colors = [];
Expand Down

0 comments on commit ff53dbe

Please sign in to comment.