Skip to content

Commit

Permalink
투표 상세 통계 컴포넌트 제작_#54 (#56)
Browse files Browse the repository at this point in the history
* feat: (#54) 투표 통계 mockData 생성

* feat: (#54) 투표 통계 관련 type, interface 생성

- 나이대 type
- 투표통계 총결과 interface

* feat: (#54) 그래프 공통 스타일 생성

* feat: (#54) 막대 하나 그래프 구현

* test: (#54) 막대 하나 그래프 사이즈별 테스트 구현

* feat: (#54) 막대 두개 그래프 구현

* test: (#54) 막대 두개 그래프 사이즈별 테스트 구현

* design: (#54) 그래프 공통 스타일 수정

* feat: (#54) 라디오를 포함한 투표 통계 결과 그래프 컴포넌트 구현

* test: (#54) 라디오를 포함한 투표 통계 결과 그래프 컴포넌트 테스트 구현

* refactor: 라디오를 포함한 투표 통계결과 그래프 컴포넌트명/폴더명 변경

- VoteResult -> VoteStatistics
- 수정이유: 투표결과 데이터 타입명과 중복

* style: (#54) styled component 파일 컨벤션에 맞춰 순서 수정

* refactor: (#54) 통계 컴포넌트 타입/인터베이스 오타 수정

* refactor: (#54) 그래프 스타일 상수화하여 코드 정리

* refactor: (#54) 공통된 그래프 프롭스 interface 리팩토링

* refactor: 투표 통계 나이대 속성 type 리팩토링

- 투표 나이대 상수화
- 투표 나이대 상수에서 나이대 type 추출
- VoteDetailResult를 value로 가지는 투표 나이대 객체 type 생성
- 기존 voteResult interface에 투표 나이대 객체 type 연결

* refactor: (#54) 몇몇 컴포넌트 반응형 웹 기준 상수화 적용

 - 게시글, 선택지, 투표 통계 컴포넌트

* refactor: (#54) 몇몇 컴포넌트 반응형 웹 기준 상수화 재적용

 - 게시글, 선택지, 투표 통계 컴포넌트
  • Loading branch information
chsua authored and tjdtls690 committed Sep 12, 2023
1 parent 2a59a20 commit d4a767c
Show file tree
Hide file tree
Showing 17 changed files with 423 additions and 21 deletions.
6 changes: 2 additions & 4 deletions frontend/src/components/Example/style.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { styled } from 'styled-components';

import { theme } from '@styles/theme';

export const Button = styled.button`
width: 80px;
color: ${theme.color.white};
background: ${theme.color.primary};
color: red;
background: black;
font-size: 1rem;
`;
33 changes: 33 additions & 0 deletions frontend/src/components/VoteStatistics/GraphStyle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { styled } from 'styled-components';

import { Size } from '@components/common/AddButton/type';

import { theme } from '@styles/theme';

const size: { [key in Size]: { height: string; linePositionTop: string } } = {
sm: { height: '200px', linePositionTop: '165px' },
md: { height: '230px', linePositionTop: '194px' },
lg: { height: '260px', linePositionTop: '224px' },
};

export const GraphContainer = styled.div<{ $size: Size }>`
display: flex;
height: ${props => `${size[props.$size].height}`};
position: relative;
font-size: 1.2rem;
@media (min-width: ${theme.breakpoint.sm}) {
font-size: 1.4rem;
}
`;

export const Line = styled.div<{ $size: Size }>`
width: 100%;
border-bottom: 2px solid black;
position: absolute;
top: ${props => `${size[props.$size].linePositionTop}`};
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { Meta, StoryObj } from '@storybook/react';

import { mockVoteResult } from '../mockData';

import OneLineGraph from '.';

const meta: Meta<typeof OneLineGraph> = {
component: OneLineGraph,
};

export default meta;
type Story = StoryObj<typeof OneLineGraph>;

export const SizeSm: Story = {
render: () => <OneLineGraph size="sm" voteResult={mockVoteResult} />,
};

export const SizeMd: Story = {
render: () => <OneLineGraph size="md" voteResult={mockVoteResult} />,
};

export const SizeLg: Story = {
render: () => <OneLineGraph size="lg" voteResult={mockVoteResult} />,
};
28 changes: 28 additions & 0 deletions frontend/src/components/VoteStatistics/OneLineGraph/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import * as GS from '../GraphStyle';
import { AGE_OPTION, GraphProps } from '../type';

import * as S from './style';

export default function OneLineGraph({ voteResult, size }: GraphProps) {
const maxVoteAmount = Math.max(
...Object.values(voteResult.age).map(voteResult => voteResult.total)
);

return (
<GS.GraphContainer $size={size}>
<GS.Line $size={size} />
{AGE_OPTION.map(option => {
const voteResultFilteredByAge = voteResult.age[option];
const amount = Math.floor((voteResultFilteredByAge.total / maxVoteAmount) * 100);

return (
<S.OptionContainer $size={size}>
<span aria-label="투표한 인원">{voteResultFilteredByAge.total}</span>
<S.OptionLength $amount={amount} />
<span aria-label="투표한 나이대">{voteResultFilteredByAge.name}</span>
</S.OptionContainer>
);
})}
</GS.GraphContainer>
);
}
34 changes: 34 additions & 0 deletions frontend/src/components/VoteStatistics/OneLineGraph/style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { styled } from 'styled-components';

import { Size } from '@components/common/AddButton/type';

import { theme } from '@styles/theme';

export const OptionContainer = styled.div<{ $size: Size }>`
display: flex;
flex-direction: column;
justify-content: flex-end;
align-items: center;
gap: 5px;
width: ${props => (props.$size === 'sm' ? '30px' : props.$size === 'md' ? '40px' : '50px')};
& > :last-child {
height: 30px;
text-align: center;
word-break: keep-all;
}
@media (min-width: ${theme.breakpoint.sm}) {
width: ${props => (props.$size === 'sm' ? '40px' : props.$size === 'md' ? '50px' : '60px')};
}
`;

export const OptionLength = styled.div<{ $amount: number }>`
height: ${props => `${props.$amount * 0.8}%`};
width: 40%;
border-radius: 5px 5px 0 0;
background-color: #f27676;
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { Meta, StoryObj } from '@storybook/react';

import { mockVoteResult } from '../mockData';

import TwoLineGraph from '.';

const meta: Meta<typeof TwoLineGraph> = {
component: TwoLineGraph,
};

export default meta;
type Story = StoryObj<typeof TwoLineGraph>;

export const SizeSm: Story = {
render: () => <TwoLineGraph size="sm" voteResult={mockVoteResult} />,
};

export const SizeMd: Story = {
render: () => <TwoLineGraph size="md" voteResult={mockVoteResult} />,
};

export const SizeLg: Story = {
render: () => <TwoLineGraph size="lg" voteResult={mockVoteResult} />,
};
41 changes: 41 additions & 0 deletions frontend/src/components/VoteStatistics/TwoLineGraph/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import * as GS from '../GraphStyle';
import { AGE_OPTION, GraphProps } from '../type';

import * as S from './style';

export default function TwoLineGraph({ voteResult, size }: GraphProps) {
const maxVoteAmount = Math.max(
...Object.values(voteResult.age).map(voteResult => Math.max(voteResult.female, voteResult.male))
);

return (
<GS.GraphContainer $size={size}>
<GS.Line $size={size} />
{AGE_OPTION.map(option => {
const voteResultFilteredByAge = voteResult.age[option];

return (
<S.OptionContainer $size={size}>
<S.DataWrapper>
<S.OptionLengthWrapper $gender="female">
<span aria-label="투표한 여자수">{voteResultFilteredByAge.female}</span>
<S.OptionLength
$amount={(voteResultFilteredByAge.female / maxVoteAmount) * 100}
$gender="female"
/>
</S.OptionLengthWrapper>
<S.OptionLengthWrapper $gender="male">
<span aria-label="투표한 남자수">{voteResultFilteredByAge.male}</span>
<S.OptionLength
$amount={(voteResultFilteredByAge.male / maxVoteAmount) * 100}
$gender="male"
/>
</S.OptionLengthWrapper>
</S.DataWrapper>
<span aria-label="투표한 나이대">{voteResultFilteredByAge.name}</span>
</S.OptionContainer>
);
})}
</GS.GraphContainer>
);
}
63 changes: 63 additions & 0 deletions frontend/src/components/VoteStatistics/TwoLineGraph/style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { styled } from 'styled-components';

import { Size } from '@components/common/AddButton/type';

import { theme } from '@styles/theme';

export const OptionContainer = styled.div<{ $size: Size }>`
display: flex;
flex-direction: column;
justify-content: flex-end;
align-items: center;
gap: 5px;
width: ${props => (props.$size === 'sm' ? '30px' : props.$size === 'md' ? '40px' : '50px')};
& > :last-child {
height: 30px;
text-align: center;
word-break: keep-all;
}
@media (min-width: ${theme.breakpoint.sm}) {
width: ${props => (props.$size === 'sm' ? '40px' : props.$size === 'md' ? '50px' : '60px')};
}
`;

export const DataWrapper = styled.div`
display: flex;
justify-content: center;
height: 90%;
width: 50px;
@media (min-width: ${theme.breakpoint.sm}) {
width: 60px;
}
`;

export const OptionLengthWrapper = styled.div<{ $gender: 'female' | 'male' }>`
display: flex;
flex-direction: column;
justify-content: flex-end;
align-items: center;
gap: 5px;
height: 100%;
width: 20%;
& > :first-child {
position: relative;
left: ${props => props.$gender === 'male' && '3px'};
right: ${props => props.$gender === 'female' && '3px'};
}
`;

export const OptionLength = styled.div<{ $amount: number; $gender: 'female' | 'male' }>`
height: ${props => `${props.$amount}% `};
width: 100%;
border-radius: 5px 5px 0 0;
background-color: ${props => (props.$gender === 'female' ? '#853DE1' : '#5AEAA5')};
`;
24 changes: 24 additions & 0 deletions frontend/src/components/VoteStatistics/VoteStatistics.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { Meta, StoryObj } from '@storybook/react';

import { mockVoteResult } from './mockData';

import VoteStatistics from '.';

const meta: Meta<typeof VoteStatistics> = {
component: VoteStatistics,
};

export default meta;
type Story = StoryObj<typeof VoteStatistics>;

export const SizeSm: Story = {
render: () => <VoteStatistics size="sm" voteResult={mockVoteResult} />,
};

export const SizeMd: Story = {
render: () => <VoteStatistics size="md" voteResult={mockVoteResult} />,
};

export const SizeLg: Story = {
render: () => <VoteStatistics size="lg" voteResult={mockVoteResult} />,
};
53 changes: 53 additions & 0 deletions frontend/src/components/VoteStatistics/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { MouseEvent, useState } from 'react';

import OneLineGraph from './OneLineGraph';
import * as S from './style';
import TwoLineGraph from './TwoLineGraph';
import { GraphProps } from './type';

interface RadioMode {
all: string;
gender: string;
}

const radioMode: RadioMode = {
all: '전체보기',
gender: '성별보기',
};

type RadioCategory = keyof RadioMode;

export default function VoteStatistics({ voteResult, size }: GraphProps) {
const [nowRadioMode, setNowRadioMode] = useState<RadioCategory>('all');

const radioModeKey = Object.keys(radioMode) as RadioCategory[];

const changeMode = (e: MouseEvent<HTMLInputElement>) => {
const target = e.target as HTMLInputElement;
const targetCategory = target.value as RadioCategory;
setNowRadioMode(targetCategory);
};

return (
<S.Container>
<S.CategoryWrapper>
{radioModeKey.map(mode => {
return (
<S.RadioLabel>
<input
type="radio"
name="radio-category"
value={mode}
checked={mode === nowRadioMode}
onClick={changeMode}
/>
{radioMode[mode]}
</S.RadioLabel>
);
})}
</S.CategoryWrapper>
{nowRadioMode === 'all' && <OneLineGraph size={size} voteResult={voteResult} />}
{nowRadioMode === 'gender' && <TwoLineGraph size={size} voteResult={voteResult} />}
</S.Container>
);
}
15 changes: 15 additions & 0 deletions frontend/src/components/VoteStatistics/mockData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export const mockVoteResult = {
total: 100,
female: 30,
name: '총합',
male: 70,
age: {
underTeenager: { total: 10, female: 10, male: 0, name: '10대 미만' },
teenager: { total: 20, female: 10, male: 10, name: '10대' },
twenties: { total: 10, female: 2, male: 8, name: '20대' },
thirties: { total: 20, female: 16, male: 4, name: '30대' },
forties: { total: 40, female: 30, male: 10, name: '40대' },
fifties: { total: 2, female: 1, male: 1, name: '50대' },
aboveFifties: { total: 3, female: 2, male: 1, name: '60대 이상' },
},
};
26 changes: 26 additions & 0 deletions frontend/src/components/VoteStatistics/style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { styled } from 'styled-components';

import { theme } from '@styles/theme';

export const Container = styled.div`
display: flex;
flex-direction: column;
align-items: center;
gap: 15px;
font-size: 1.2rem;
@media (min-width: ${theme.breakpoint.sm}) {
font-size: 1.4rem;
}
`;

export const CategoryWrapper = styled.fieldset`
display: flex;
gap: 10px;
`;

export const RadioLabel = styled.label`
display: flex;
gap: 5px;
`;
Loading

0 comments on commit d4a767c

Please sign in to comment.