Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

게시글 통계 컴포넌트 제작_Feat/#65 #75

Merged
merged 29 commits into from
Jul 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
bfe9599
feat: (#65) msw를 위한 mock 게시물 상세정보 생성
chsua Jul 18, 2023
f3ab0a8
feat: (#65) 한 게시물 상세정보 fetch mocking
chsua Jul 18, 2023
f49693f
feat: (#65) 데이터, 로딩, 에러 정보 전달하는 fetch훅 생성
chsua Jul 18, 2023
cd6d10b
feat: (#65) 게시물 하나 상세정보 fetch 함수 구현
chsua Jul 18, 2023
b21e204
refactor: (#65) mockData 수정
chsua Jul 18, 2023
97a167a
feat: (#65) 통계정보 get하는 api msw작성
chsua Jul 18, 2023
8d781de
feat: (#65) 전체통계정보, 선택지 통계정보 get하는 fetch함수 작성
chsua Jul 18, 2023
7570726
refactor: (#65) useFetch 내 데이터이름 범용성 향성을 위해 수정
chsua Jul 18, 2023
ec2e730
feat: (#65) 통계그래프를 포함한 선택지 컴포넌트 생성
chsua Jul 18, 2023
ae8e0b6
test: (#65) 통계그래프를 포함한 선택지 컴포넌트 테스트 구현
chsua Jul 18, 2023
61c93b4
feat: (#65) 게시글 투표결과 통계 페이지 구현
chsua Jul 18, 2023
149fa4f
test: (#65) 게시글 투표결과 통계 페이지 테스트 구현
chsua Jul 18, 2023
aafb9f4
feat: (#65) 로딩컴포넌트 구현
chsua Jul 18, 2023
3c98e25
test: (#65) 로딩컴포넌트 크기별 테스트
chsua Jul 18, 2023
d196ebc
refactor: 선택지변경 api 인자 interface 리팩터링
chsua Jul 18, 2023
f75d9ca
style: (#65) 사용하지 않는 스타일컴포넌트 삭제 및 코드 정리
chsua Jul 18, 2023
b7044c1
feat: (#65) 통계컴포넌트에 로딩스피너 적용
chsua Jul 18, 2023
108f37f
fix: (#65) 라디오 name속성이 공통되어 생긴 오작동 오류 수정
chsua Jul 18, 2023
0bd5883
refactor: ($65) map에 키 값 부여
chsua Jul 18, 2023
d90c2c6
fix: 라디오에서 발생하는 checked 관련 오류 해결
chsua Jul 18, 2023
fc12501
feat: (#65) 모바일 화면 외 크기에서는 헤더 감추기
chsua Jul 18, 2023
40d2c92
style: (#65) 로딩스피너 오타수정
chsua Jul 18, 2023
bfdab79
style: (#65) css 컨벤션에 따라 순서 수정
chsua Jul 18, 2023
6057aa1
refactor: (#65) 불필요한 코드 정리
chsua Jul 18, 2023
69668bf
refactor: (#65) 목적에 맞지 않는 선택지 통계 토글 함수명 수정
chsua Jul 18, 2023
0848b3d
feat: (#54) 헤더에 있는 이전페이지로 가기 버튼 navigate 연결
chsua Jul 18, 2023
60cbf71
refactor: (#65) font-size를 var로 수정
chsua Jul 18, 2023
d320212
refactor: (#65) 통계 컴포넌트 라디오 상태명 변경
chsua Jul 19, 2023
232a23b
refactor: (#65) 대소문자/오탈자 수정
chsua Jul 19, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 14 additions & 6 deletions frontend/src/api/sua/post.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
import { patchFetch, postFetch } from '@utils/fetch';
import { PostInfo } from '@type/post';

import { getFetch, 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 }
) => {
interface OptionData {
inyeong-kang marked this conversation as resolved.
Show resolved Hide resolved
originOptionId: number;
newOptionId: number;
}

export const changeVotedOption = async (postId: number, optionData: OptionData) => {
return await patchFetch(
`/posts/${postId}/options?source=${originOptionId}&target=${newOptionId}`
`/posts/${postId}/options?source=${optionData.originOptionId}&target=${optionData.newOptionId}`
);
};

export const getVoteDetail = async (postId: number): Promise<PostInfo> => {
return await getFetch<PostInfo>(`/posts/${postId}`);
};
17 changes: 17 additions & 0 deletions frontend/src/api/sua/voteResult.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { VoteResult } from '@components/VoteStatistics/type';

import { getFetch } from '@utils/fetch';

export const getPostStatistics = async (postId: number): Promise<VoteResult> => {
return await getFetch<VoteResult>(`/posts/${postId}/options`);
};

export const getOptionStatistics = async ({
postId,
optionId,
}: {
postId: number;
optionId: number;
}): Promise<VoteResult> => {
return await getFetch<VoteResult>(`/posts/${postId}/options/${optionId}`);
};
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export default function OneLineGraph({ voteResult, size }: GraphProps) {
const amount = Math.floor((voteResultFilteredByAge.total / maxVoteAmount) * 100);

return (
<S.OptionContainer $size={size}>
<S.OptionContainer key={option} $size={size}>
<span aria-label="투표한 인원">{voteResultFilteredByAge.total}</span>
<S.OptionLength $amount={amount} />
<span aria-label="투표한 나이대">{voteResultFilteredByAge.name}</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export default function TwoLineGraph({ voteResult, size }: GraphProps) {
const voteResultFilteredByAge = voteResult.age[option];

return (
<S.OptionContainer $size={size}>
<S.OptionContainer key={option} $size={size}>
<S.DataWrapper>
<S.OptionLengthWrapper $gender="female">
<span aria-label="투표한 여자수">{voteResultFilteredByAge.female}</span>
Expand Down
16 changes: 9 additions & 7 deletions frontend/src/components/VoteStatistics/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,36 +18,38 @@ const radioMode: RadioMode = {
type RadioCategory = keyof RadioMode;

export default function VoteStatistics({ voteResult, size }: GraphProps) {
const [nowRadioMode, setNowRadioMode] = useState<RadioCategory>('all');
const [currentRadioMode, setCurrentRadioMode] = 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);
setCurrentRadioMode(targetCategory);
};

const random = Date.now();

return (
<S.Container>
<S.CategoryWrapper>
{radioModeKey.map(mode => {
return (
<S.RadioLabel>
<S.RadioLabel key={mode}>
<input
type="radio"
name="radio-category"
name={`radio-category-${random}`}
inyeong-kang marked this conversation as resolved.
Show resolved Hide resolved
value={mode}
checked={mode === nowRadioMode}
defaultChecked={mode === currentRadioMode}
onClick={changeMode}
/>
{radioMode[mode]}
</S.RadioLabel>
);
})}
</S.CategoryWrapper>
{nowRadioMode === 'all' && <OneLineGraph size={size} voteResult={voteResult} />}
{nowRadioMode === 'gender' && <TwoLineGraph size={size} voteResult={voteResult} />}
{currentRadioMode === 'all' && <OneLineGraph size={size} voteResult={voteResult} />}
{currentRadioMode === 'gender' && <TwoLineGraph size={size} voteResult={voteResult} />}
</S.Container>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { Meta, StoryObj } from '@storybook/react';

import LoadingSpinner from '.';

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

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

export const SizeS: Story = {
render: () => <LoadingSpinner size="sm" />,
};

export const sizeM: Story = {
render: () => <LoadingSpinner size="md" />,
};

export const sizeL: Story = {
render: () => <LoadingSpinner size="lg" />,
};
17 changes: 17 additions & 0 deletions frontend/src/components/common/LoadingSpinner/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Size } from '../AddButton/type';

import * as S from './style';

interface LoadingSpinnerProps {
size: Size;
}

export default function LoadingSpinner({ size }: LoadingSpinnerProps) {
return (
<S.Container $size={size}>
<S.unit $size={size} />
<S.unit $size={size} />
<S.unit $size={size} />
</S.Container>
);
}
44 changes: 44 additions & 0 deletions frontend/src/components/common/LoadingSpinner/style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { keyframes, styled } from 'styled-components';

import { Size } from '../AddButton/type';

interface LoadingSpinnerProps {
$size: Size;
}

const size = {
sm: '10px',
md: '15px',
lg: '30px',
};

const Animation = keyframes`
to {
transform: translate(0, -15px);
}
`;

export const Container = styled.div<LoadingSpinnerProps>`
display: flex;
justify-content: center;
align-items: center;

& > :nth-child(2) {
animation-delay: 0.1s;
margin: 0 ${props => size[props.$size]};
}

& > :nth-child(3) {
animation-delay: 0.2s;
}
`;

export const unit = styled.div<LoadingSpinnerProps>`
width: ${props => size[props.$size]};
height: ${props => size[props.$size]};
border-radius: 50%;

background-color: #747474;

animation: ${Animation} 0.5s ease-in-out infinite alternate;
`;
2 changes: 1 addition & 1 deletion frontend/src/components/common/NarrowMainHeader/style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const Container = styled.div`
background-color: #1f1f1f;
& :nth-child(2) {
& > :nth-child(2) {
margin-right: auto;
height: 60%;
}
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/common/Post/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ export default function Post({ postInfo, isPreview }: PostProps) {
<S.Wrapper>
<S.Writer>{writer.nickname}</S.Writer>
<S.Wrapper>
<S.Time>{startTime}</S.Time>
<S.Time>{endTime}</S.Time>
<span>{startTime}</span>
<span>{endTime}</span>
Comment on lines +40 to +41
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

불필요한 스타일 컴포넌트 코드를 줄이셨네요 👍👍👍

</S.Wrapper>
</S.Wrapper>
<S.Content $isPreview={isPreview}>{content}</S.Content>
Expand Down
4 changes: 1 addition & 3 deletions frontend/src/components/common/Post/style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export const Wrapper = styled.div`
font-size: 1.2rem;
:nth-child(2) {
& > :nth-child(2) {
margin-left: 10px;
}
Expand All @@ -59,8 +59,6 @@ export const Wrapper = styled.div`
}
`;

export const Time = styled.span``;

export const Content = styled.p<{ $isPreview: boolean }>`
display: -webkit-box;
Expand Down
28 changes: 28 additions & 0 deletions frontend/src/hooks/useFetch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { useEffect, useState } from 'react';

export const useFetch = <T>(fetchFn: () => Promise<T>) => {
inyeong-kang marked this conversation as resolved.
Show resolved Hide resolved
const [data, setData] = useState<T>();
const [errorMessage, setErrorMessage] = useState<string>();
const [isLoading, setIsLoading] = useState(true);

useEffect(() => {
fetchFn()
.then(res => {
setData(res);
})
.catch(rej => {
setErrorMessage(rej.message);
})
.finally(() => {
setIsLoading(false);
inyeong-kang marked this conversation as resolved.
Show resolved Hide resolved
});

return (() => {
setData(undefined);
setIsLoading(true);
setErrorMessage(undefined);
})();
}, []);

return { data, errorMessage, isLoading };
};
3 changes: 2 additions & 1 deletion frontend/src/mocks/handlers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { example } from './example/get';
import { getVoteDetailTest } from './sua/getVoteDetail';
import { votePostTest } from './sua/vote';

export const handlers = [...example, ...votePostTest];
export const handlers = [...example, ...votePostTest, ...getVoteDetailTest];
56 changes: 56 additions & 0 deletions frontend/src/mocks/mockData/voteDetail.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
{
inyeong-kang marked this conversation as resolved.
Show resolved Hide resolved
"postId": 1,
"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": 2,
"allPeopleCount": 123,
"options": [
{
"id": 12,
"text": "당선",
"peopleCount": 30,
"percent": 30
},
{
"id": 123,
"text": "votogether",
"peopleCount": 40,
"percent": 40
},
{
"id": 1234,
"text": "인스타그램, 블라인드와 같은 SNS의 형식을 차용합니다. 누군가는 글을 쓰고, 누군가는 반응합니다. 다만, 댓글은 없습니다. 투표로 자신의 의견을 표현하고 이를 사람들에게 보여줍니다.",
"peopleCount": 20,
"percent": 20
},
{
"id": 2,
"text": "fun from choice, 오늘도 즐거운 한 표 ",
"imageUrl": "https://source.unsplash.com/random",
"peopleCount": 10,
"percent": 10
}
]
}
}
19 changes: 19 additions & 0 deletions frontend/src/mocks/sua/getVoteDetail.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { rest } from 'msw';

import { mockVoteResult } from '@components/VoteStatistics/mockData';

import voteDetail from '../mockData/voteDetail.json';

export const getVoteDetailTest = [
rest.get(`/posts/:postId`, (req, res, ctx) => {
return res(ctx.status(200), ctx.delay(1000), ctx.json(voteDetail));
}),

rest.get(`/posts/:postId/options`, (req, res, ctx) => {
return res(ctx.status(200), ctx.delay(1000), ctx.json(mockVoteResult));
}),

rest.get(`/posts/:postId/options/:optionId`, (req, res, ctx) => {
return res(ctx.status(200), ctx.delay(1000), ctx.json(mockVoteResult));
}),
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { Meta, StoryObj } from '@storybook/react';

import OptionStatistics from '.';

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

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

const MOCK_MAX_VOTE_OPTION = {
id: 2,
text: '',
imageUrl: 'https://source.unsplash.com/random',
peopleCount: 10,
percent: 10,
};

export const defaultPage: Story = {
render: () => (
<OptionStatistics
postId={1}
voteOption={MOCK_MAX_VOTE_OPTION}
isSelectedOption={true}
size="sm"
/>
),
};
Loading