diff --git a/frontend/src/components/Example/style.ts b/frontend/src/components/Example/style.ts index e1a68fe06..83b743789 100644 --- a/frontend/src/components/Example/style.ts +++ b/frontend/src/components/Example/style.ts @@ -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; `; diff --git a/frontend/src/components/VoteStatistics/GraphStyle.ts b/frontend/src/components/VoteStatistics/GraphStyle.ts new file mode 100644 index 000000000..a06aa11ed --- /dev/null +++ b/frontend/src/components/VoteStatistics/GraphStyle.ts @@ -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}`}; +`; diff --git a/frontend/src/components/VoteStatistics/OneLineGraph/OneLineGraph.stories.tsx b/frontend/src/components/VoteStatistics/OneLineGraph/OneLineGraph.stories.tsx new file mode 100644 index 000000000..227f058d8 --- /dev/null +++ b/frontend/src/components/VoteStatistics/OneLineGraph/OneLineGraph.stories.tsx @@ -0,0 +1,24 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { mockVoteResult } from '../mockData'; + +import OneLineGraph from '.'; + +const meta: Meta = { + component: OneLineGraph, +}; + +export default meta; +type Story = StoryObj; + +export const SizeSm: Story = { + render: () => , +}; + +export const SizeMd: Story = { + render: () => , +}; + +export const SizeLg: Story = { + render: () => , +}; diff --git a/frontend/src/components/VoteStatistics/OneLineGraph/index.tsx b/frontend/src/components/VoteStatistics/OneLineGraph/index.tsx new file mode 100644 index 000000000..65fd6b98e --- /dev/null +++ b/frontend/src/components/VoteStatistics/OneLineGraph/index.tsx @@ -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 ( + + + {AGE_OPTION.map(option => { + const voteResultFilteredByAge = voteResult.age[option]; + const amount = Math.floor((voteResultFilteredByAge.total / maxVoteAmount) * 100); + + return ( + + {voteResultFilteredByAge.total} + + {voteResultFilteredByAge.name} + + ); + })} + + ); +} diff --git a/frontend/src/components/VoteStatistics/OneLineGraph/style.ts b/frontend/src/components/VoteStatistics/OneLineGraph/style.ts new file mode 100644 index 000000000..ef0edbbbf --- /dev/null +++ b/frontend/src/components/VoteStatistics/OneLineGraph/style.ts @@ -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; +`; diff --git a/frontend/src/components/VoteStatistics/TwoLineGraph/TwoLineGraph.stories.tsx b/frontend/src/components/VoteStatistics/TwoLineGraph/TwoLineGraph.stories.tsx new file mode 100644 index 000000000..0fefb3d8b --- /dev/null +++ b/frontend/src/components/VoteStatistics/TwoLineGraph/TwoLineGraph.stories.tsx @@ -0,0 +1,24 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { mockVoteResult } from '../mockData'; + +import TwoLineGraph from '.'; + +const meta: Meta = { + component: TwoLineGraph, +}; + +export default meta; +type Story = StoryObj; + +export const SizeSm: Story = { + render: () => , +}; + +export const SizeMd: Story = { + render: () => , +}; + +export const SizeLg: Story = { + render: () => , +}; diff --git a/frontend/src/components/VoteStatistics/TwoLineGraph/index.tsx b/frontend/src/components/VoteStatistics/TwoLineGraph/index.tsx new file mode 100644 index 000000000..00e2466cd --- /dev/null +++ b/frontend/src/components/VoteStatistics/TwoLineGraph/index.tsx @@ -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 ( + + + {AGE_OPTION.map(option => { + const voteResultFilteredByAge = voteResult.age[option]; + + return ( + + + + {voteResultFilteredByAge.female} + + + + {voteResultFilteredByAge.male} + + + + {voteResultFilteredByAge.name} + + ); + })} + + ); +} diff --git a/frontend/src/components/VoteStatistics/TwoLineGraph/style.ts b/frontend/src/components/VoteStatistics/TwoLineGraph/style.ts new file mode 100644 index 000000000..634d871d9 --- /dev/null +++ b/frontend/src/components/VoteStatistics/TwoLineGraph/style.ts @@ -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')}; +`; diff --git a/frontend/src/components/VoteStatistics/VoteStatistics.stories.tsx b/frontend/src/components/VoteStatistics/VoteStatistics.stories.tsx new file mode 100644 index 000000000..fb187667d --- /dev/null +++ b/frontend/src/components/VoteStatistics/VoteStatistics.stories.tsx @@ -0,0 +1,24 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { mockVoteResult } from './mockData'; + +import VoteStatistics from '.'; + +const meta: Meta = { + component: VoteStatistics, +}; + +export default meta; +type Story = StoryObj; + +export const SizeSm: Story = { + render: () => , +}; + +export const SizeMd: Story = { + render: () => , +}; + +export const SizeLg: Story = { + render: () => , +}; diff --git a/frontend/src/components/VoteStatistics/index.tsx b/frontend/src/components/VoteStatistics/index.tsx new file mode 100644 index 000000000..5931bc056 --- /dev/null +++ b/frontend/src/components/VoteStatistics/index.tsx @@ -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('all'); + + const radioModeKey = Object.keys(radioMode) as RadioCategory[]; + + const changeMode = (e: MouseEvent) => { + const target = e.target as HTMLInputElement; + const targetCategory = target.value as RadioCategory; + setNowRadioMode(targetCategory); + }; + + return ( + + + {radioModeKey.map(mode => { + return ( + + + {radioMode[mode]} + + ); + })} + + {nowRadioMode === 'all' && } + {nowRadioMode === 'gender' && } + + ); +} diff --git a/frontend/src/components/VoteStatistics/mockData.ts b/frontend/src/components/VoteStatistics/mockData.ts new file mode 100644 index 000000000..e1a819c67 --- /dev/null +++ b/frontend/src/components/VoteStatistics/mockData.ts @@ -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대 이상' }, + }, +}; diff --git a/frontend/src/components/VoteStatistics/style.ts b/frontend/src/components/VoteStatistics/style.ts new file mode 100644 index 000000000..87c9be40c --- /dev/null +++ b/frontend/src/components/VoteStatistics/style.ts @@ -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; +`; diff --git a/frontend/src/components/VoteStatistics/type.ts b/frontend/src/components/VoteStatistics/type.ts new file mode 100644 index 000000000..693d42c24 --- /dev/null +++ b/frontend/src/components/VoteStatistics/type.ts @@ -0,0 +1,31 @@ +import { Size } from '@components/common/AddButton/type'; + +export interface GraphProps { + voteResult: VoteResult; + size: Size; +} + +interface VoteDetailResult { + name: string; + total: number; + female: number; + male: number; +} + +export const AGE_OPTION = [ + 'underTeenager', + 'teenager', + 'twenties', + 'thirties', + 'forties', + 'fifties', + 'aboveFifties', +] as const; + +export type AgeCategory = (typeof AGE_OPTION)[number]; + +export type VoteResultAge = Record; + +export interface VoteResult extends VoteDetailResult { + age: VoteResultAge; +} diff --git a/frontend/src/components/common/Post/style.ts b/frontend/src/components/common/Post/style.ts index 920803dba..d43d171b3 100644 --- a/frontend/src/components/common/Post/style.ts +++ b/frontend/src/components/common/Post/style.ts @@ -1,5 +1,7 @@ import { styled } from 'styled-components'; +import { theme } from '@styles/theme'; + export const Container = styled.li` display: flex; flex-direction: column; @@ -9,7 +11,7 @@ export const Container = styled.li` letter-spacing: 0.5px; line-height: 1.5; - @media (min-width: 576px) { + @media (min-width: ${theme.breakpoint.sm}) { font-size: 1.4rem; } `; @@ -17,7 +19,7 @@ export const Container = styled.li` export const Category = styled.span` font-size: 1.2rem; - @media (min-width: 576px) { + @media (min-width: ${theme.breakpoint.sm}) { font-size: 1.4rem; } `; @@ -34,7 +36,7 @@ export const Title = styled.p<{ $isPreview: boolean }>` -webkit-line-clamp: ${props => props.$isPreview && '2'}; -webkit-box-orient: vertical; - @media (min-width: 576px) { + @media (min-width: ${theme.breakpoint.sm}) { font-size: 2.2rem; } `; @@ -52,7 +54,7 @@ export const Wrapper = styled.div` margin-left: 10px; } - @media (min-width: 576px) { + @media (min-width: ${theme.breakpoint.sm}) { font-size: 1.4rem; } `; @@ -73,7 +75,7 @@ export const Content = styled.p<{ $isPreview: boolean }>` -webkit-line-clamp: ${props => props.$isPreview && '10'}; -webkit-box-orient: vertical; - @media (min-width: 576px) { + @media (min-width: ${theme.breakpoint.sm}) { font-size: 1.6rem; } `; diff --git a/frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/style.ts b/frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/style.ts index 5a061ebd6..cc35a73dd 100644 --- a/frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/style.ts +++ b/frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/style.ts @@ -1,5 +1,7 @@ import { styled } from 'styled-components'; +import { theme } from '@styles/theme'; + export const Container = styled.li` display: flex; gap: 10px; @@ -37,7 +39,7 @@ export const ContentTextArea = styled.textarea` resize: none; - @media (min-width: 960px) { + @media (min-width: ${theme.breakpoint.md}) { height: 120px; font-size: 1.6rem; @@ -83,7 +85,7 @@ background-color: #bebebe; cursor: pointer; -@media (min-width: 960px) { +@media (min-width: ${theme.breakpoint.md}) { width:28px; height:28px; } @@ -93,7 +95,7 @@ export const IconImage = styled.img` width: 14px; height: 14px; - @media (min-width: 960px) { + @media (min-width: ${theme.breakpoint.md}) { width: 16px; height: 16px; } diff --git a/frontend/src/components/optionList/WrittenVoteOptionList/WrittenVoteOption/style.ts b/frontend/src/components/optionList/WrittenVoteOptionList/WrittenVoteOption/style.ts index d26b33e24..94ad6fbe0 100644 --- a/frontend/src/components/optionList/WrittenVoteOptionList/WrittenVoteOption/style.ts +++ b/frontend/src/components/optionList/WrittenVoteOptionList/WrittenVoteOption/style.ts @@ -1,5 +1,7 @@ import { styled } from 'styled-components'; +import { theme } from '@styles/theme'; + export const Container = styled.li<{ $isSelected: boolean }>` display: flex; flex-direction: column; @@ -13,7 +15,7 @@ export const Container = styled.li<{ $isSelected: boolean }>` cursor: pointer; - @media (min-width: 960px) { + @media (min-width: ${theme.breakpoint.md}) { padding: 20px 30px; } `; @@ -26,7 +28,7 @@ export const Image = styled.img` aspect-ratio: 1/1; object-fit: cover; - @media (min-width: 960px) { + @media (min-width: ${theme.breakpoint.md}) { margin-bottom: 24px; } `; @@ -44,7 +46,7 @@ export const PreviewContent = styled.p` -webkit-line-clamp: 2; // 원하는 라인수 -webkit-box-orient: vertical; - @media (min-width: 960px) { + @media (min-width: ${theme.breakpoint.md}) { font-size: 1.6rem; } `; @@ -53,7 +55,7 @@ export const DetailContent = styled.p` font-size: 1.4rem; font-weight: 500; - @media (min-width: 960px) { + @media (min-width: ${theme.breakpoint.md}) { font-size: 1.6rem; } `; @@ -61,7 +63,7 @@ export const DetailContent = styled.p` export const ProgressContainer = styled.div` margin-top: 12px; - @media (min-width: 960px) { + @media (min-width: ${theme.breakpoint.md}) { margin-top: 18px; } `; @@ -72,7 +74,7 @@ export const TextContainer = styled.div` text-align: end; font-weight: 500; - @media (min-width: 960px) { + @media (min-width: ${theme.breakpoint.md}) { margin-top: 12px; font-size: 1.6rem; @@ -82,7 +84,7 @@ export const TextContainer = styled.div` export const PeopleText = styled.span` font-size: 1.4rem; - @media (min-width: 960px) { + @media (min-width: ${theme.breakpoint.md}) { font-size: 1.6rem; } `; @@ -94,7 +96,7 @@ export const PercentText = styled.span` opacity: 0.7; - @media (min-width: 960px) { + @media (min-width: ${theme.breakpoint.md}) { font-size: 1.4rem; } `; diff --git a/frontend/src/components/optionList/WrittenVoteOptionList/style.ts b/frontend/src/components/optionList/WrittenVoteOptionList/style.ts index 032f4616a..96e6bb269 100644 --- a/frontend/src/components/optionList/WrittenVoteOptionList/style.ts +++ b/frontend/src/components/optionList/WrittenVoteOptionList/style.ts @@ -1,5 +1,7 @@ import { styled } from 'styled-components'; +import { theme } from '@styles/theme'; + export const VoteOptionListContainer = styled.ul` display: flex; flex-direction: column; @@ -7,7 +9,7 @@ export const VoteOptionListContainer = styled.ul` width: 100%; - @media (min-width: 960px) { + @media (min-width: ${theme.breakpoint.md}) { gap: 18px; } `;