From ad5f45980f4a1aeae5d254702ebea1bb1fb16769 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EA=B8=B8/KIM=20YOUNG=20GIL?= <80146176+Gilpop8663@users.noreply.github.com> Date: Wed, 12 Jul 2023 16:10:41 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B2=8C=EC=8B=9C=EA=B8=80=EC=97=90=EC=84=9C?= =?UTF-8?q?=20=EB=B3=B4=EB=8A=94=20=ED=88=AC=ED=91=9C=20=EC=84=A0=ED=83=9D?= =?UTF-8?q?=EC=A7=80=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20UI=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20(#30)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: (#18) 게시글에서 보는 투표 선택지 컴포넌트 UI 구현 * refactor: (#18) 가독성 좋은 코드가 되도록 변수명 수정 및 CSS 속성 변경 글 목록에서 이미지 안 보이도록 수정 * refactor: (#18) 불필요한 코드 삭제 및 퍼센트 소수점 1자리 보이도록 수정 --- .../ProgressBar/ProgressBar.stories.tsx | 18 ++ .../WrittenVoteOption/ProgressBar/index.tsx | 16 ++ .../WrittenVoteOption/ProgressBar/style.ts | 18 ++ .../WrittenVoteOption.stories.tsx | 165 ++++++++++++++++++ .../WrittenVoteOption/index.tsx | 48 +++++ .../WrittenVoteOption/style.ts | 100 +++++++++++ .../WrittenVoteOptionList.stories.tsx | 132 ++++++++++++++ .../WrittenVoteOptionList/index.tsx | 43 +++++ .../optionList/WrittenVoteOptionList/style.ts | 13 ++ 9 files changed, 553 insertions(+) create mode 100644 frontend/src/components/optionList/WrittenVoteOptionList/WrittenVoteOption/ProgressBar/ProgressBar.stories.tsx create mode 100644 frontend/src/components/optionList/WrittenVoteOptionList/WrittenVoteOption/ProgressBar/index.tsx create mode 100644 frontend/src/components/optionList/WrittenVoteOptionList/WrittenVoteOption/ProgressBar/style.ts create mode 100644 frontend/src/components/optionList/WrittenVoteOptionList/WrittenVoteOption/WrittenVoteOption.stories.tsx create mode 100644 frontend/src/components/optionList/WrittenVoteOptionList/WrittenVoteOption/index.tsx create mode 100644 frontend/src/components/optionList/WrittenVoteOptionList/WrittenVoteOption/style.ts create mode 100644 frontend/src/components/optionList/WrittenVoteOptionList/WrittenVoteOptionList.stories.tsx create mode 100644 frontend/src/components/optionList/WrittenVoteOptionList/index.tsx create mode 100644 frontend/src/components/optionList/WrittenVoteOptionList/style.ts diff --git a/frontend/src/components/optionList/WrittenVoteOptionList/WrittenVoteOption/ProgressBar/ProgressBar.stories.tsx b/frontend/src/components/optionList/WrittenVoteOptionList/WrittenVoteOption/ProgressBar/ProgressBar.stories.tsx new file mode 100644 index 000000000..03c0080d7 --- /dev/null +++ b/frontend/src/components/optionList/WrittenVoteOptionList/WrittenVoteOption/ProgressBar/ProgressBar.stories.tsx @@ -0,0 +1,18 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import ProgressBar from '.'; + +const meta: Meta = { + component: ProgressBar, +}; + +export default meta; +type Story = StoryObj; + +export const Selected: Story = { + render: () => , +}; + +export const NotSelected: Story = { + render: () => , +}; diff --git a/frontend/src/components/optionList/WrittenVoteOptionList/WrittenVoteOption/ProgressBar/index.tsx b/frontend/src/components/optionList/WrittenVoteOptionList/WrittenVoteOption/ProgressBar/index.tsx new file mode 100644 index 000000000..e1baca71a --- /dev/null +++ b/frontend/src/components/optionList/WrittenVoteOptionList/WrittenVoteOption/ProgressBar/index.tsx @@ -0,0 +1,16 @@ +import React from 'react'; + +import * as S from './style'; + +interface ProgressBarProps { + percent: number; + isSelected: boolean; +} + +export default function ProgressBar({ percent, isSelected }: ProgressBarProps) { + return ( + + + + ); +} diff --git a/frontend/src/components/optionList/WrittenVoteOptionList/WrittenVoteOption/ProgressBar/style.ts b/frontend/src/components/optionList/WrittenVoteOptionList/WrittenVoteOption/ProgressBar/style.ts new file mode 100644 index 000000000..63fd43d4f --- /dev/null +++ b/frontend/src/components/optionList/WrittenVoteOptionList/WrittenVoteOption/ProgressBar/style.ts @@ -0,0 +1,18 @@ +import { styled } from 'styled-components'; + +export const Container = styled.div` + border-radius: 4px; + + height: 8px; + + background-color: rgba(0, 0, 0, 0.3); +`; + +export const Bar = styled.div<{ progress: number; isSelected: boolean }>` + border-radius: 4px; + + width: ${({ progress }) => `${progress}%`}; + height: 8px; + + background-color: ${({ isSelected }) => (isSelected ? '#ff7877' : '#9F9F9F')}; +`; diff --git a/frontend/src/components/optionList/WrittenVoteOptionList/WrittenVoteOption/WrittenVoteOption.stories.tsx b/frontend/src/components/optionList/WrittenVoteOptionList/WrittenVoteOption/WrittenVoteOption.stories.tsx new file mode 100644 index 000000000..684cb5445 --- /dev/null +++ b/frontend/src/components/optionList/WrittenVoteOptionList/WrittenVoteOption/WrittenVoteOption.stories.tsx @@ -0,0 +1,165 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { styled } from 'styled-components'; + +import WrittenVoteOption from '.'; + +const meta: Meta = { + component: WrittenVoteOption, +}; + +export default meta; +type Story = StoryObj; + +const WrittenVoteWrapper = styled.div` + max-width: 460px; +`; + +export const Select: Story = { + render: () => ( + + {}} + isPreview={true} + text="자유를 찾게 냅둔다" + peopleCount={2} + percent={70.9} + isSelected={true} + isVoted={true} + /> + + ), +}; + +export const NotSelectAndLongText: Story = { + render: () => ( + + {}} + isPreview={true} + text="또는 JavaScript로 컴포넌트의 텍스트를 가져와서 원하는 길이로 자르고, 생략 부호를" + percent={80} + peopleCount={6} + isSelected={false} + isVoted={true} + /> + + ), +}; + +export const ImageAndSelect: Story = { + render: () => ( + + {}} + isPreview={true} + imageUrl="https://source.unsplash.com/random" + text="또는 JavaScript로 컴포넌트의 텍스트를 가져와서 원하는 길이로 자르고, 생략 부호를" + percent={80} + peopleCount={6} + isSelected={true} + isVoted={true} + /> + + ), +}; + +export const NotVote: Story = { + render: () => ( + + {}} + isPreview={true} + text="또는 JavaScript로 컴포넌트의 텍스트를 가져와서 원하는 길이로 자르고, 생략 부호를" + percent={0} + peopleCount={0} + isSelected={false} + isVoted={false} + /> + + ), +}; + +export const ImageAndNotVote: Story = { + render: () => ( + + {}} + isPreview={true} + imageUrl="https://source.unsplash.com/random" + text="또는 JavaScript로 컴포넌트의 텍스트를 가져와서 원하는 길이로 자르고, 생략 부호를" + percent={0} + peopleCount={0} + isSelected={false} + isVoted={false} + /> + + ), +}; + +export const PreviewContent: Story = { + render: () => ( + + {}} + isPreview={true} + imageUrl="https://source.unsplash.com/random" + text="isVote는 변수명으로서는 영문법상으로 볼 때는 어색하진 않습니다. is는 보통 boolean 타입을 나타낼 때 사용되는 접두사이며, Vote는 투표를 의미하는 명사입니다. 따라서 isVote는 투표 여부를 나타내는지를 의미하는 변수명으로 적합합니다. 그러나 개인적인 취향에 따라 다른 변수명을 선호할 수도 있습니다. 예를 들면 hasVoted와 같이 투표를 했는지 여부를 나타내는 변수명을 사용하는 것도 가능합니다. 중요한 것은 코드의 가독성과 일관성을 유지하는 것이며, 개발자들과의 커뮤니케이션을 원활하게 하기 위해 명확하고 이해하기 쉬운 변수명을 선택하는 것이 좋습니다." + percent={0} + peopleCount={0} + isSelected={false} + isVoted={false} + /> + + ), +}; + +export const DetailContent: Story = { + render: () => ( + + {}} + isPreview={false} + imageUrl="https://source.unsplash.com/random" + text="isVote는 변수명으로서는 영문법상으로 볼 때는 어색하진 않습니다. is는 보통 boolean 타입을 나타낼 때 사용되는 접두사이며, Vote는 투표를 의미하는 명사입니다. 따라서 isVote는 투표 여부를 나타내는지를 의미하는 변수명으로 적합합니다. 그러나 개인적인 취향에 따라 다른 변수명을 선호할 수도 있습니다. 예를 들면 hasVoted와 같이 투표를 했는지 여부를 나타내는 변수명을 사용하는 것도 가능합니다. 중요한 것은 코드의 가독성과 일관성을 유지하는 것이며, 개발자들과의 커뮤니케이션을 원활하게 하기 위해 명확하고 이해하기 쉬운 변수명을 선택하는 것이 좋습니다." + percent={0} + peopleCount={0} + isSelected={false} + isVoted={false} + /> + + ), +}; + +export const NoImageAndDetailContent: Story = { + render: () => ( + + {}} + isPreview={false} + text="isVote는 변수명으로서는 영문법상으로 볼 때는 어색하진 않습니다. is는 보통 boolean 타입을 나타낼 때 사용되는 접두사이며, Vote는 투표를 의미하는 명사입니다. 따라서 isVote는 투표 여부를 나타내는지를 의미하는 변수명으로 적합합니다. 그러나 개인적인 취향에 따라 다른 변수명을 선호할 수도 있습니다. 예를 들면 hasVoted와 같이 투표를 했는지 여부를 나타내는 변수명을 사용하는 것도 가능합니다. 중요한 것은 코드의 가독성과 일관성을 유지하는 것이며, 개발자들과의 커뮤니케이션을 원활하게 하기 위해 명확하고 이해하기 쉬운 변수명을 선택하는 것이 좋습니다." + percent={60} + peopleCount={8} + isSelected={true} + isVoted={true} + /> + + ), +}; + +export const ImageAndSelectAndDetailContent: Story = { + render: () => ( + + {}} + isPreview={false} + imageUrl="https://source.unsplash.com/random" + text="isVote는 변수명으로서는 영문법상으로 볼 때는 어색하진 않습니다. is는 보통 boolean 타입을 나타낼 때 사용되는 접두사이며, Vote는 투표를 의미하는 명사입니다. 따라서 isVote는 투표 여부를 나타내는지를 의미하는 변수명으로 적합합니다. 그러나 개인적인 취향에 따라 다른 변수명을 선호할 수도 있습니다. 예를 들면 hasVoted와 같이 투표를 했는지 여부를 나타내는 변수명을 사용하는 것도 가능합니다. 중요한 것은 코드의 가독성과 일관성을 유지하는 것이며, 개발자들과의 커뮤니케이션을 원활하게 하기 위해 명확하고 이해하기 쉬운 변수명을 선택하는 것이 좋습니다." + percent={60} + peopleCount={8} + isSelected={true} + isVoted={true} + /> + + ), +}; diff --git a/frontend/src/components/optionList/WrittenVoteOptionList/WrittenVoteOption/index.tsx b/frontend/src/components/optionList/WrittenVoteOptionList/WrittenVoteOption/index.tsx new file mode 100644 index 000000000..e687d0729 --- /dev/null +++ b/frontend/src/components/optionList/WrittenVoteOptionList/WrittenVoteOption/index.tsx @@ -0,0 +1,48 @@ +import React from 'react'; + +import ProgressBar from './ProgressBar'; +import * as S from './style'; + +interface WrittenVoteOptionProps { + handleVoteClick: () => void; + text: string; + isVoted: boolean; + peopleCount: number; + percent: number; + isSelected: boolean; + isPreview: boolean; + imageUrl?: string; +} + +export default function WrittenVoteOption({ + handleVoteClick, + text, + isVoted, + peopleCount, + percent, + isSelected, + isPreview, + imageUrl, +}: WrittenVoteOptionProps) { + return ( + + {!isPreview && imageUrl && } + {isPreview ? ( + {text} + ) : ( + {text} + )} + {isVoted && ( + <> + + + + + {peopleCount}명 + ({percent.toFixed(1)}%) + + + )} + + ); +} diff --git a/frontend/src/components/optionList/WrittenVoteOptionList/WrittenVoteOption/style.ts b/frontend/src/components/optionList/WrittenVoteOptionList/WrittenVoteOption/style.ts new file mode 100644 index 000000000..7fe8e210f --- /dev/null +++ b/frontend/src/components/optionList/WrittenVoteOptionList/WrittenVoteOption/style.ts @@ -0,0 +1,100 @@ +import { styled } from 'styled-components'; + +export const Container = styled.li<{ isSelected: boolean }>` + display: flex; + flex-direction: column; + + border: ${({ isSelected }) => + isSelected ? '2px solid #ff7877' : '1px solid rgba(0, 0, 0, 0.1)'}; + border-radius: 4px; + padding: 15px 20px; + + color: #5b5b5b; + + cursor: pointer; + + @media (min-width: 960px) { + padding: 20px 30px; + } +`; + +export const Image = styled.img` + border-radius: 4px; + margin-bottom: 12px; + + width: 100%; + + aspect-ratio: 1/1; + object-fit: cover; + @media (min-width: 960px) { + margin-bottom: 24px; + } +`; + +export const PreviewContent = styled.p` + display: -webkit-box; + + font-size: 1.4rem; + font-weight: 500; + text-overflow: ellipsis; + word-break: break-word; + + overflow: hidden; + + -webkit-line-clamp: 2; // 원하는 라인수 + -webkit-box-orient: vertical; + + @media (min-width: 960px) { + font-size: 1.6rem; + } +`; + +export const DetailContent = styled.p` + font-size: 1.4rem; + font-weight: 500; + + @media (min-width: 960px) { + font-size: 1.6rem; + } +`; + +export const ProgressContainer = styled.div` + margin-top: 12px; + + @media (min-width: 960px) { + margin-top: 18px; + } +`; + +export const TextContainer = styled.div` + margin-top: 8px; + + text-align: end; + font-weight: 500; + + @media (min-width: 960px) { + margin-top: 12px; + + font-size: 1.6rem; + } +`; + +export const PeopleText = styled.span` + font-size: 1.4rem; + + @media (min-width: 960px) { + font-size: 1.6rem; + } +`; + +export const PercentText = styled.span` + margin-left: 4px; + + font-size: 1.2rem; + + opacity: 0.7; + + @media (min-width: 960px) { + font-size: 1.4rem; + } +`; diff --git a/frontend/src/components/optionList/WrittenVoteOptionList/WrittenVoteOptionList.stories.tsx b/frontend/src/components/optionList/WrittenVoteOptionList/WrittenVoteOptionList.stories.tsx new file mode 100644 index 000000000..3e04472b1 --- /dev/null +++ b/frontend/src/components/optionList/WrittenVoteOptionList/WrittenVoteOptionList.stories.tsx @@ -0,0 +1,132 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { styled } from 'styled-components'; + +import WrittenVoteOptionList from '.'; + +const meta: Meta = { + component: WrittenVoteOptionList, +}; + +export default meta; +type Story = StoryObj; + +const WrittenVoteWrapper = styled.div` + max-width: 460px; +`; + +const MOCK_NOT_VOTED_DATA = { + selectedOptionId: 0, + allPeopleCount: 123, + options: [ + { + id: 12312, + text: '또는 JavaScript로 컴포넌트의 텍스트를 가져와서 원하는 길이로 자르고, 생략 부호를', + peopleCount: -1, + percent: -1, + }, + { + id: 12314, + text: '자유를 찾게 냅둔다', + peopleCount: -1, + percent: -1, + }, + { + id: 123152, + text: 'isVote는 변수명으로서는 영문법상으로 볼 때는 어색하진 않습니다. is는 보통 boolean 타입을 나타낼 때 사용되는 접두사이며, Vote는 투표를 의미하는 명사입니다. 따라서 isVote는 투표 여부를 나타내는지를 의미하는 변수명으로 적합합니다. 그러나 개인적인 취향에 따라 다른 변수명을 선호할 수도 있습니다. 예를 들면 hasVoted와 같이 투표를 했는지 여부를 나타내는 변수명을 사용하는 것도 가능합니다. 중요한 것은 코드의 가독성과 일관성을 유지하는 것이며, 개발자들과의 커뮤니케이션을 원활하게 하기 위해 명확하고 이해하기 쉬운 변수명을 선택하는 것이 좋습니다.', + peopleCount: -1, + percent: -1, + }, + { + id: 123122, + text: 'isVote는 변수명으로서는 영문법상으로 볼 때는 어색하진 않습니다. is는 보통 boolean 타입을 나타낼 때 사용되는 접두사이며, Vote는 투표를 의미하는 명사입니다. 따라서 isVote는 투표 여부를 나타내는지를 의미하는 변수명으로 적합합니다. 그러나 개인적인 취향에 따라 다른 변수명을 선호할 수도 있습니다. 예를 들면 hasVoted와 같이 투표를 했는지 여부를 나타내는 변수명을 사용하는 것도 가능합니다. 중요한 것은 코드의 가독성과 일관성을 유지하는 것이며, 개발자들과의 커뮤니케이션을 원활하게 하기 위해 명확하고 이해하기 쉬운 변수명을 선택하는 것이 좋습니다.', + imageUrl: 'https://source.unsplash.com/random', + peopleCount: -1, + percent: -1, + }, + ], +}; + +const MOCK_VOTED_DATA = { + selectedOptionId: 123122, + allPeopleCount: 123, + options: [ + { + id: 12312, + text: '또는 JavaScript로 컴포넌트의 텍스트를 가져와서 원하는 길이로 자르고, 생략 부호를', + peopleCount: 7, + percent: 8, + }, + { + id: 12314, + text: '자유를 찾게 냅둔다', + peopleCount: 12, + percent: 15, + }, + { + id: 123152, + text: 'isVote는 변수명으로서는 영문법상으로 볼 때는 어색하진 않습니다. is는 보통 boolean 타입을 나타낼 때 사용되는 접두사이며, Vote는 투표를 의미하는 명사입니다. 따라서 isVote는 투표 여부를 나타내는지를 의미하는 변수명으로 적합합니다. 그러나 개인적인 취향에 따라 다른 변수명을 선호할 수도 있습니다. 예를 들면 hasVoted와 같이 투표를 했는지 여부를 나타내는 변수명을 사용하는 것도 가능합니다. 중요한 것은 코드의 가독성과 일관성을 유지하는 것이며, 개발자들과의 커뮤니케이션을 원활하게 하기 위해 명확하고 이해하기 쉬운 변수명을 선택하는 것이 좋습니다.', + peopleCount: 0, + percent: 0, + }, + { + id: 123122, + text: 'isVote는 변수명으로서는 영문법상으로 볼 때는 어색하진 않습니다. is는 보통 boolean 타입을 나타낼 때 사용되는 접두사이며, Vote는 투표를 의미하는 명사입니다. 따라서 isVote는 투표 여부를 나타내는지를 의미하는 변수명으로 적합합니다. 그러나 개인적인 취향에 따라 다른 변수명을 선호할 수도 있습니다. 예를 들면 hasVoted와 같이 투표를 했는지 여부를 나타내는 변수명을 사용하는 것도 가능합니다. 중요한 것은 코드의 가독성과 일관성을 유지하는 것이며, 개발자들과의 커뮤니케이션을 원활하게 하기 위해 명확하고 이해하기 쉬운 변수명을 선택하는 것이 좋습니다.', + imageUrl: 'https://source.unsplash.com/random', + peopleCount: 85, + percent: 60, + }, + ], +}; + +export const PreviewNotVoted: Story = { + render: () => ( + + {}} + isPreview={true} + voteOptionList={MOCK_NOT_VOTED_DATA.options} + /> + + ), +}; + +export const PreviewVoted: Story = { + render: () => ( + + {}} + isPreview={true} + voteOptionList={MOCK_VOTED_DATA.options} + /> + + ), +}; + +export const DetailNotVoted: Story = { + render: () => ( + + {}} + isPreview={false} + voteOptionList={MOCK_NOT_VOTED_DATA.options} + /> + + ), +}; + +export const DetailVoted: Story = { + render: () => ( + + {}} + isPreview={false} + voteOptionList={MOCK_VOTED_DATA.options} + /> + + ), +}; diff --git a/frontend/src/components/optionList/WrittenVoteOptionList/index.tsx b/frontend/src/components/optionList/WrittenVoteOptionList/index.tsx new file mode 100644 index 000000000..92ea3eea5 --- /dev/null +++ b/frontend/src/components/optionList/WrittenVoteOptionList/index.tsx @@ -0,0 +1,43 @@ +import React from 'react'; + +import * as S from './style'; +import WrittenVoteOption from './WrittenVoteOption'; + +interface WrittenVoteOptionType { + id: number; + text: string; + peopleCount: number; + percent: number; + imageUrl?: string; +} + +interface WrittenVoteOptionListProps { + isPreview: boolean; + selectedOptionId: number; + voteOptionList: WrittenVoteOptionType[]; + handleVoteClick: (voteId: number) => void; +} + +const NOT_VOTED = 0; + +export default function WrittenVoteOptionList({ + isPreview, + voteOptionList, + selectedOptionId, + handleVoteClick, +}: WrittenVoteOptionListProps) { + return ( + + {voteOptionList.map(voteOption => ( + handleVoteClick(voteOption.id)} + /> + ))} + + ); +} diff --git a/frontend/src/components/optionList/WrittenVoteOptionList/style.ts b/frontend/src/components/optionList/WrittenVoteOptionList/style.ts new file mode 100644 index 000000000..032f4616a --- /dev/null +++ b/frontend/src/components/optionList/WrittenVoteOptionList/style.ts @@ -0,0 +1,13 @@ +import { styled } from 'styled-components'; + +export const VoteOptionListContainer = styled.ul` + display: flex; + flex-direction: column; + gap: 12px; + + width: 100%; + + @media (min-width: 960px) { + gap: 18px; + } +`;