diff --git a/frontend/.storybook/preview.tsx b/frontend/.storybook/preview.tsx index d35ff88c1..c573afc2e 100644 --- a/frontend/.storybook/preview.tsx +++ b/frontend/.storybook/preview.tsx @@ -5,12 +5,14 @@ import { initialize, mswDecorator } from 'msw-storybook-addon'; import { GlobalStyle } from '../src/styles/globalStyle'; import React from 'react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { handlers } from '../src/mocks/handlers'; const queryClient = new QueryClient(); initialize(); const preview: Preview = { parameters: { + msw: handlers, actions: { argTypesRegex: '^on[A-Z].*' }, controls: { matchers: { diff --git a/frontend/src/CONSTATNS/index.ts b/frontend/src/CONSTATNS/index.ts new file mode 100644 index 000000000..143371efb --- /dev/null +++ b/frontend/src/CONSTATNS/index.ts @@ -0,0 +1,3 @@ +export const POST = { + NOT_VOTE: 0, +}; diff --git a/frontend/src/api/sua/post.ts b/frontend/src/api/sua/post.ts new file mode 100644 index 000000000..bb0d9f4c7 --- /dev/null +++ b/frontend/src/api/sua/post.ts @@ -0,0 +1,14 @@ +import { patchFetch, postFetch } from '@utils/fetch'; + +export const votePost = async (postId: number, optionId: number) => { + return await postFetch(`/posts/${postId}/options/${optionId}`, ''); +}; + +export const changeVotedOption = async ( + postId: number, + { originOptionId, newOptionId }: { originOptionId: number; newOptionId: number } +) => { + return await patchFetch( + `/posts/${postId}/options?source=${originOptionId}&target=${newOptionId}` + ); +}; diff --git a/frontend/src/components/common/Post/Post.stories.tsx b/frontend/src/components/common/Post/Post.stories.tsx new file mode 100644 index 000000000..b3751cea2 --- /dev/null +++ b/frontend/src/components/common/Post/Post.stories.tsx @@ -0,0 +1,29 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { mockNotVotedPost, mockVotedPost } from './mockData'; + +import Post from '.'; + +const meta: Meta = { + component: Post, + decorators: [storyFn =>
{storyFn()}
], +}; + +export default meta; +type Story = StoryObj; + +export const PreviewNotVotedPost: Story = { + render: () => , +}; + +export const PreviewVotedPost: Story = { + render: () => , +}; + +export const NotVotedPost: Story = { + render: () => , +}; + +export const VotedPost: Story = { + render: () => , +}; diff --git a/frontend/src/components/common/Post/index.tsx b/frontend/src/components/common/Post/index.tsx new file mode 100644 index 000000000..3e81b346a --- /dev/null +++ b/frontend/src/components/common/Post/index.tsx @@ -0,0 +1,53 @@ +import { POST } from 'CONSTATNS'; + +import { PostInfo } from '@type/post'; + +import { changeVotedOption, votePost } from '@api/sua/post'; + +import WrittenVoteOptionList from '@components/optionList/WrittenVoteOptionList'; + +import * as S from './style'; + +interface PostProps { + postInfo: PostInfo; + isPreview: boolean; +} + +export default function Post({ postInfo, isPreview }: PostProps) { + const { postId, category, title, writer, startTime, endTime, content, voteInfo } = postInfo; + + const handleVoteClick = (newOptionId: number) => { + if (voteInfo.selectedOptionId === newOptionId) return; + + if (voteInfo.selectedOptionId === POST.NOT_VOTE) { + votePost(postId, newOptionId); + return; + } + + changeVotedOption(postId, { + originOptionId: voteInfo.selectedOptionId, + newOptionId, + }); + }; + + return ( + + {category.map(category => category.name).join(' | ')} + {title} + + {writer.nickname} + + {startTime} + {endTime} + + + {content} + + + ); +} diff --git a/frontend/src/components/common/Post/mockData.ts b/frontend/src/components/common/Post/mockData.ts new file mode 100644 index 000000000..4a098edc1 --- /dev/null +++ b/frontend/src/components/common/Post/mockData.ts @@ -0,0 +1,119 @@ +import { PostInfo } from '@type/post'; + +export const mockNotVotedPost: PostInfo = { + postId: 1111111, + title: + '어느 곳에서 정보를 찾아야 할지도 막막한 사람들을 위한, 심심풀이로 나의 취향과 남의 취향을 비교해보고 싶은 사람들을 위한 프로젝트', + writer: { + id: 121212212, + nickname: '우아한 잔치국수', + }, + content: + '이는 사람들에게 재미와 정보, 두 가지를 줄 수 있습니다. 사람들은 MBTI, 밸런스게임처럼 나와 같은 사람들을 찾고, 나와 다른 사람들과 비교하는 것을 즐깁니다. 이를 컨텐츠화하여 보다 빠르게 질문하고 답변하며, 사람들의 반응을 확인할 수 있다면 사람들은 충분한 즐거움을 느낄 것입니다. 또한 20대가 많은 대학가에 창업을 하고 싶지만 20대의 의견을 모르겠을 때, 확실한 답은 아닐지라도 어느 정도의 가이드를 줄 수 있을 것입니다. 질문자에게 제공되는 성별/나이대별 투표 결과 정보는 질문자가 하고자 하는 의사결정의 근거가 될 수 있을 것입니다.', + category: [ + { + id: 76767, + name: '개발', + }, + { + id: 74632, + name: '연애', + }, + { + id: 71347, + name: '상담', + }, + ], + startTime: '2023-07-12 12:40', + endTime: '2023-07-13 18:40', + voteInfo: { + selectedOptionId: 0, + allPeopleCount: 100, + options: [ + { + id: 12312, + text: '당선', + peopleCount: -1, + percent: -1, + }, + { + id: 12314, + text: 'votogether', + peopleCount: -1, + percent: -1, + }, + { + id: 123152, + text: '인스타그램, 블라인드와 같은 SNS의 형식을 차용합니다. 누군가는 글을 쓰고, 누군가는 반응합니다. 다만, 댓글은 없습니다. 투표로 자신의 의견을 표현하고 이를 사람들에게 보여줍니다.', + peopleCount: -1, + percent: -1, + }, + { + id: 123122, + text: 'fun from choice, 오늘도 즐거운 한 표 ', + imageUrl: 'https://source.unsplash.com/random', + peopleCount: -1, + percent: -1, + }, + ], + }, +}; + +export const mockVotedPost: PostInfo = { + postId: 1111112, + title: + '어느 곳에서 정보를 찾아야 할지도 막막한 사람들을 위한, 심심풀이로 나의 취향과 남의 취향을 비교해보고 싶은 사람들을 위한 프로젝트', + writer: { + id: 12121221, + nickname: '우아한 잔치국수', + }, + content: + '이는 사람들에게 재미와 정보, 두 가지를 줄 수 있습니다. 사람들은 MBTI, 밸런스게임처럼 나와 같은 사람들을 찾고, 나와 다른 사람들과 비교하는 것을 즐깁니다. 이를 컨텐츠화하여 보다 빠르게 질문하고 답변하며, 사람들의 반응을 확인할 수 있다면 사람들은 충분한 즐거움을 느낄 것입니다. 또한 20대가 많은 대학가에 창업을 하고 싶지만 20대의 의견을 모르겠을 때, 확실한 답은 아닐지라도 어느 정도의 가이드를 줄 수 있을 것입니다. 질문자에게 제공되는 성별/나이대별 투표 결과 정보는 질문자가 하고자 하는 의사결정의 근거가 될 수 있을 것입니다.', + category: [ + { + id: 76767, + name: '개발', + }, + { + id: 74632, + name: '연애', + }, + { + id: 71347, + name: '상담', + }, + ], + startTime: '2023-07-12 12:40', + endTime: '2023-07-13 18:40', + voteInfo: { + selectedOptionId: 12312, + allPeopleCount: 123, + options: [ + { + id: 12312, + text: '당선', + peopleCount: 30, + percent: 30, + }, + { + id: 12314, + text: 'votogether', + peopleCount: 40, + percent: 40, + }, + { + id: 123152, + text: '인스타그램, 블라인드와 같은 SNS의 형식을 차용합니다. 누군가는 글을 쓰고, 누군가는 반응합니다. 다만, 댓글은 없습니다. 투표로 자신의 의견을 표현하고 이를 사람들에게 보여줍니다.', + peopleCount: 20, + percent: 20, + }, + { + id: 123122, + text: 'fun from choice, 오늘도 즐거운 한 표 ', + imageUrl: 'https://source.unsplash.com/random', + peopleCount: 10, + percent: 10, + }, + ], + }, +}; diff --git a/frontend/src/components/common/Post/style.ts b/frontend/src/components/common/Post/style.ts new file mode 100644 index 000000000..920803dba --- /dev/null +++ b/frontend/src/components/common/Post/style.ts @@ -0,0 +1,79 @@ +import { styled } from 'styled-components'; + +export const Container = styled.li` + display: flex; + flex-direction: column; + gap: 10px; + + font-size: 1.2rem; + letter-spacing: 0.5px; + line-height: 1.5; + + @media (min-width: 576px) { + font-size: 1.4rem; + } +`; + +export const Category = styled.span` + font-size: 1.2rem; + + @media (min-width: 576px) { + font-size: 1.4rem; + } +`; + +export const Title = styled.p<{ $isPreview: boolean }>` + display: -webkit-box; + + font-size: 2rem; + text-overflow: ellipsis; + word-break: break-word; + + overflow: hidden; + + -webkit-line-clamp: ${props => props.$isPreview && '2'}; + -webkit-box-orient: vertical; + + @media (min-width: 576px) { + font-size: 2.2rem; + } +`; + +export const Writer = styled.span``; + +export const Wrapper = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + + font-size: 1.2rem; + + :nth-child(2) { + margin-left: 10px; + } + + @media (min-width: 576px) { + font-size: 1.4rem; + } +`; + +export const Time = styled.span``; + +export const Content = styled.p<{ $isPreview: boolean }>` + display: -webkit-box; + + font-size: 1.4rem; + text-overflow: ellipsis; + word-break: break-word; + + margin: 10px 0; + + overflow: hidden; + + -webkit-line-clamp: ${props => props.$isPreview && '10'}; + -webkit-box-orient: vertical; + + @media (min-width: 576px) { + font-size: 1.6rem; + } +`; diff --git a/frontend/src/components/optionList/WrittenVoteOptionList/WrittenVoteOption/ProgressBar/index.tsx b/frontend/src/components/optionList/WrittenVoteOptionList/WrittenVoteOption/ProgressBar/index.tsx index e1baca71a..332c6403d 100644 --- a/frontend/src/components/optionList/WrittenVoteOptionList/WrittenVoteOption/ProgressBar/index.tsx +++ b/frontend/src/components/optionList/WrittenVoteOptionList/WrittenVoteOption/ProgressBar/index.tsx @@ -10,7 +10,7 @@ interface ProgressBarProps { 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 index 63fd43d4f..79fd92f58 100644 --- a/frontend/src/components/optionList/WrittenVoteOptionList/WrittenVoteOption/ProgressBar/style.ts +++ b/frontend/src/components/optionList/WrittenVoteOptionList/WrittenVoteOption/ProgressBar/style.ts @@ -8,11 +8,11 @@ export const Container = styled.div` background-color: rgba(0, 0, 0, 0.3); `; -export const Bar = styled.div<{ progress: number; isSelected: boolean }>` +export const Bar = styled.div<{ progress: number; $isSelected: boolean }>` border-radius: 4px; width: ${({ progress }) => `${progress}%`}; height: 8px; - background-color: ${({ isSelected }) => (isSelected ? '#ff7877' : '#9F9F9F')}; + background-color: ${({ $isSelected }) => ($isSelected ? '#ff7877' : '#9F9F9F')}; `; diff --git a/frontend/src/components/optionList/WrittenVoteOptionList/WrittenVoteOption/index.tsx b/frontend/src/components/optionList/WrittenVoteOptionList/WrittenVoteOption/index.tsx index e687d0729..89e4b15bf 100644 --- a/frontend/src/components/optionList/WrittenVoteOptionList/WrittenVoteOption/index.tsx +++ b/frontend/src/components/optionList/WrittenVoteOptionList/WrittenVoteOption/index.tsx @@ -25,7 +25,7 @@ export default function WrittenVoteOption({ imageUrl, }: WrittenVoteOptionProps) { return ( - + {!isPreview && imageUrl && } {isPreview ? ( {text} diff --git a/frontend/src/components/optionList/WrittenVoteOptionList/WrittenVoteOption/style.ts b/frontend/src/components/optionList/WrittenVoteOptionList/WrittenVoteOption/style.ts index 7fe8e210f..d26b33e24 100644 --- a/frontend/src/components/optionList/WrittenVoteOptionList/WrittenVoteOption/style.ts +++ b/frontend/src/components/optionList/WrittenVoteOptionList/WrittenVoteOption/style.ts @@ -1,11 +1,11 @@ import { styled } from 'styled-components'; -export const Container = styled.li<{ isSelected: boolean }>` +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: ${({ $isSelected }) => + $isSelected ? '2px solid #ff7877' : '1px solid rgba(0, 0, 0, 0.1)'}; border-radius: 4px; padding: 15px 20px; diff --git a/frontend/src/components/optionList/WrittenVoteOptionList/index.tsx b/frontend/src/components/optionList/WrittenVoteOptionList/index.tsx index 92ea3eea5..475fa53a8 100644 --- a/frontend/src/components/optionList/WrittenVoteOptionList/index.tsx +++ b/frontend/src/components/optionList/WrittenVoteOptionList/index.tsx @@ -1,25 +1,17 @@ -import React from 'react'; +import { POST } from 'CONSTATNS'; + +import { WrittenVoteOptionType } from '@type/post'; 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; + handleVoteClick: (newOptionId: number) => void; } -const NOT_VOTED = 0; - export default function WrittenVoteOptionList({ isPreview, voteOptionList, @@ -33,7 +25,7 @@ export default function WrittenVoteOptionList({ key={voteOption.id} {...voteOption} isPreview={isPreview} - isVoted={selectedOptionId !== NOT_VOTED} + isVoted={selectedOptionId !== POST.NOT_VOTE} isSelected={selectedOptionId === voteOption.id} handleVoteClick={() => handleVoteClick(voteOption.id)} /> diff --git a/frontend/src/mocks/handlers.ts b/frontend/src/mocks/handlers.ts index f6e89fef8..5368f1e53 100644 --- a/frontend/src/mocks/handlers.ts +++ b/frontend/src/mocks/handlers.ts @@ -1,3 +1,4 @@ import { example } from './example/get'; +import { votePostTest } from './sua/vote'; -export const handlers = [...example]; +export const handlers = [...example, ...votePostTest]; diff --git a/frontend/src/mocks/sua/vote.ts b/frontend/src/mocks/sua/vote.ts new file mode 100644 index 000000000..f2e620954 --- /dev/null +++ b/frontend/src/mocks/sua/vote.ts @@ -0,0 +1,43 @@ +import { rest } from 'msw'; + +export const votePostTest = [ + //투표 + rest.post(`/posts/1111111/options/12312`, (req, res, ctx) => { + console.log('투표 ㅇㅋ'); + return res(ctx.status(200)); + }), + + rest.post(`/posts/1111111/options/12314`, (req, res, ctx) => { + console.log('투표 ㅇㅋ'); + return res(ctx.status(200)); + }), + + rest.post(`/posts/1111111/options/123152`, (req, res, ctx) => { + console.log('투표 ㅇㅋ'); + return res(ctx.status(200)); + }), + + rest.post(`/posts/1111111/options/123122`, (req, res, ctx) => { + console.log('투표 ㅇㅋ'); + return res(ctx.status(200)); + }), + + //선택지 수정 + rest.patch(`/posts/1111112/options?source=12312&target=12314`, (req, res, ctx) => { + console.log('투표 변경ㅇㅋ'); + + return res(ctx.status(200)); + }), + + rest.patch(`/posts/1111112/options?source=12312&target=123152`, (req, res, ctx) => { + console.log('투표 변경ㅇㅋ'); + + return res(ctx.status(200)); + }), + + rest.patch(`/posts/1111112/options?source=12312&target=123122`, (req, res, ctx) => { + console.log('투표 변경ㅇㅋ'); + + return res(ctx.status(200)); + }), +]; diff --git a/frontend/src/types/post.ts b/frontend/src/types/post.ts new file mode 100644 index 000000000..bcb96f59a --- /dev/null +++ b/frontend/src/types/post.ts @@ -0,0 +1,22 @@ +export interface WrittenVoteOptionType { + id: number; + text: string; + peopleCount: number; + percent: number; + imageUrl?: string; +} + +export interface PostInfo { + postId: number; + title: string; + writer: { id: number; nickname: string }; + content: string; + category: { id: number; name: string }[]; + startTime: string; + endTime: string; + voteInfo: { + selectedOptionId: number; + allPeopleCount: number; + options: WrittenVoteOptionType[]; + }; +}