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: 리워드 내역 페이지 UI 구현 #214

Merged
merged 8 commits into from
Oct 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Binary file removed grass-diary/public/assets/img/mainCharacter.png
Binary file not shown.
Binary file removed grass-diary/src/assets/icon/grass.png
Binary file not shown.
Binary file removed grass-diary/src/assets/icon/grassDiary.png
Binary file not shown.
Binary file removed grass-diary/src/assets/icon/mainCharacter.png
Binary file not shown.
Binary file removed grass-diary/src/assets/icon/subCharacter.png
Binary file not shown.
Binary file added grass-diary/src/assets/image/pot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions grass-diary/src/constants/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export const END_POINT = {
`/shared/diaries/latest?cursorId=${cursorId}&size=12`,
total_reward: (memberId: Id) => `/member/totalReward/${memberId}`,
total_grass: (memberId: Id) => `/grass/${memberId}`,
reward_history: (memberId: Id) => `reward/history/${memberId}`,
grass: (memberId: Id) => `/grass/main-page/${memberId}`,
image: '/image/diary',
comment: (id: Id) => `/comment/${id}`,
Expand Down
34 changes: 34 additions & 0 deletions grass-diary/src/hooks/api/useRewardHistory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { END_POINT } from '@constants/api';
import { CONSOLE_ERROR } from '@constants/message';
import API from '@services/index';
import { useQuery } from '@tanstack/react-query';
import { useUser } from '@state/user/useUser';
import { AxiosError } from 'axios';

export const useRewardHistory = () => {
const memberId = useUser();

const fetchUseRewardHistory = async (): Promise<RewardHistory[]> => {
const res = await API.get(END_POINT.reward_history(memberId));
return res.data;
};

const {
data: history,
isError,
error,
} = useQuery<
RewardHistory[],
AxiosError,
RewardHistory[],
[string, number | undefined]
>({
queryKey: ['rewardHistory', memberId],
queryFn: fetchUseRewardHistory,
enabled: !!memberId,
});

if (isError) console.error(CONSOLE_ERROR.reward.get + error.message);

return { history };
};
17 changes: 17 additions & 0 deletions grass-diary/src/pages/CreateDiary/CreateDiary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,23 @@ const CreateDiary = () => {
};
}, [diaryInfo]);

// 자동 임시 저장
useEffect(() => {
const autoSave = 60000;

const autoSaveDraft = () => {
if (!isContentEmpty) {
handleSaveDraft();
}
};

const saveDataTime = setInterval(autoSaveDraft, autoSave);

return () => {
clearInterval(saveDataTime);
};
}, [diaryInfo, isContentEmpty]);

return (
<>
<S.Layout>
Expand Down
303 changes: 121 additions & 182 deletions grass-diary/src/pages/RewardPage/RewardPage.tsx
Original file line number Diff line number Diff line change
@@ -1,193 +1,132 @@
import stylex from '@stylexjs/stylex';
import { Button, Container } from '@components/index';
import Swal from 'sweetalert2';
import subCharacter from '@icon/subCharacter.png';
import * as S from '@styles/RewardPage/RewardPage.style';
import { semantic } from '@styles/semantic';
import { ReactComponent as Avatar } from '@svg/avatarBg.svg';
import { ReactComponent as Arrow } from '@svg/chevron_right.svg';
import { useReward } from '@hooks/api/useReward';
import { useGrassRecord } from '@hooks/api/useGrassRecord';
import { useRewardHistory } from '@hooks/api/useRewardHistory';

const RewardPageStyle = stylex.create({
titleBanner: {
backgroundColor: 'rgb(221, 223, 224)',
padding: '40px',
width: '100vw',

display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
},
title: {
width: {
default: '1105px',
'@media (max-width: 1139px)': '95vw',
},
},

rewardInfoContainer: {
display: 'flex',
justifyContent: 'space-around',
alignItems: 'center',
width: {
default: '1105px',
'@media (max-width: 1139px)': '95vw',
},
gap: '900px',
paddingTop: '20px',
},
rewardInfoItem: {
display: 'flex',
gap: '30px',
},
const RewardPage = () => {
const { reward } = useReward();
const { grassQuery } = useGrassRecord();
const { history } = useRewardHistory();

line: {
borderBottom: '1px solid rgb(221, 223, 224)',
width: '1105px',
paddingTop: '20px',
},
const groupedHistory: GroupedHistory | undefined = history?.reduce(
(acc: GroupedHistory, rewardHistory: RewardHistory) => {
const [date] = rewardHistory.rewardedDate.split(' ');
const [year, month] = date.split('-');

rewardListContainer: {
display: 'flex',
flexDirection: 'column',
width: {
default: '1105px',
'@media (max-width: 1139px)': '95vw',
const key = `${year}-${month}`;
if (!acc[key]) {
acc[key] = {
year,
month: parseInt(month, 10),
records: [],
};
}
acc[key].records.push(rewardHistory);
return acc;
},
},
rewardListMonth: {
fontWeight: 'bold',
fontSize: '20px',
padding: '20px 0px 30px 0px',
},
rewardListContent: {
display: 'flex',
alignItems: 'center',
gap: '30px',
},
rewardListBox: {
display: 'flex',
flexDirection: 'column',
padding: '5px',
},
rewardListLine: {
borderBottom: '1px solid rgb(221, 223, 224)',
width: '1105px',
padding: '5px',
},
rewardListDate: {
color: '#8a8f95',
},
rewardListPoint: {
fontSize: '20px',
fontWeight: 'bold',
},
});

const RewardPage = () => {
const modal = () => {
Swal.fire({
title: '테마 상점',
text: '테마 상점 준비중이에요',
imageUrl: subCharacter,
imageWidth: 300,
imageHeight: 300,
imageAlt: 'Custom image',
confirmButtonColor: '#28CA3B',
confirmButtonText: '확인',
});
};
{} as GroupedHistory,
);

return (
<Container>
<section {...stylex.props(RewardPageStyle.titleBanner)}>
<p
style={{ fontSize: '30px', fontWeight: 'bold' }}
{...stylex.props(RewardPageStyle.title)}
>
리워드 내역
</p>
<p {...stylex.props(RewardPageStyle.title)}>
내가 쌓은 포인트를 확인 할 수 있어요
</p>
</section>
<section {...stylex.props(RewardPageStyle.rewardInfoContainer)}>
<article {...stylex.props(RewardPageStyle.rewardInfoItem)}>
<div>
<p>잔디</p>
<div
style={{
display: 'flex',
fontWeight: 'bold',
fontSize: '20px',
paddingTop: '5px',
}}
>
<img
src="https://raw.githubusercontent.com/Tarikul-Islam-Anik/Animated-Fluent-Emojis/master/Emojis/Animals/Seedling.png"
alt="Seedling"
width="20"
height="20"
/>
<p>55</p>
</div>
</div>
<div>
<p>리워드</p>
<div
style={{
display: 'flex',
fontWeight: 'bold',
fontSize: '20px',
paddingTop: '5px',
}}
>
<img
src="https://raw.githubusercontent.com/Tarikul-Islam-Anik/Animated-Fluent-Emojis/master/Emojis/Activities/Party%20Popper.png"
alt="Party Popper"
width="20"
height="20"
<>
<S.Background>
<S.Layout>
<S.TitleContainer>
<S.RewardTitle>리워드 내역</S.RewardTitle>
<S.RewardSubTitle>
내가 쌓은 포인트를 확인할 수 있어요
</S.RewardSubTitle>
</S.TitleContainer>
<S.RewardSection>
<S.RewardContainer>
<S.GrassCountBox>
<img src="../src/assets/image/pot.png" alt="image" />
<S.CountText>{grassQuery?.totalCount}</S.CountText>
<S.CountCaptionText>내가 심은 잔디</S.CountCaptionText>
</S.GrassCountBox>
<S.GrassRewardBox>
<Avatar />
<S.TotalRewardText>{reward.rewardPoint}</S.TotalRewardText>
<S.TotalRewardCaptionText>
내 잔디 리워드
</S.TotalRewardCaptionText>
</S.GrassRewardBox>
</S.RewardContainer>
<S.ThemeBtn>
<S.ThemeBtnText>테마 상점</S.ThemeBtnText>
<Arrow
width={18}
height={18}
fill={semantic.light.accent.solid.hero}
/>
<p>123</p>
</div>
</div>
</article>
<Button
text="테마 상점"
width="110px"
defaultColor="#2d2d2d"
hoverColor="#FFF"
defaultBgColor="#FFFFFF"
hoverBgColor="#111111"
border="1px solid #929292"
onClick={modal}
/>
</section>
<span {...stylex.props(RewardPageStyle.line)}></span>
<section {...stylex.props(RewardPageStyle.rewardListContainer)}>
<p {...stylex.props(RewardPageStyle.rewardListMonth)}>2024 5월</p>

<div {...stylex.props(RewardPageStyle.rewardListBox)}>
<div {...stylex.props(RewardPageStyle.rewardListContent)}>
<p {...stylex.props(RewardPageStyle.rewardListDate)}>5월 15일</p>
<p {...stylex.props(RewardPageStyle.rewardListPoint)}>🔥 +10</p>
</div>
<span {...stylex.props(RewardPageStyle.rewardListLine)}></span>
</div>

<div {...stylex.props(RewardPageStyle.rewardListBox)}>
<div {...stylex.props(RewardPageStyle.rewardListContent)}>
<p {...stylex.props(RewardPageStyle.rewardListDate)}>5월 15일</p>
<p {...stylex.props(RewardPageStyle.rewardListPoint)}>🔥 +10</p>
</div>
<span {...stylex.props(RewardPageStyle.rewardListLine)}></span>
</div>
</S.ThemeBtn>
</S.RewardSection>
<S.HistorySection>
<S.DayContainer>
<S.DayBox>
<S.DayTextBox>
<S.DayText>날짜</S.DayText>
</S.DayTextBox>
<S.GetRewardText>획득 리워드</S.GetRewardText>
</S.DayBox>
<S.DividerBox>
<S.Divider />
</S.DividerBox>
</S.DayContainer>

<div {...stylex.props(RewardPageStyle.rewardListBox)}>
<div {...stylex.props(RewardPageStyle.rewardListContent)}>
<p {...stylex.props(RewardPageStyle.rewardListDate)}>5월 15일</p>
<p {...stylex.props(RewardPageStyle.rewardListPoint)}>🔥 +10</p>
</div>
<span {...stylex.props(RewardPageStyle.rewardListLine)}></span>
</div>
</section>
</Container>
{groupedHistory &&
Object.values(groupedHistory)
.slice()
.reverse()
.map(
(group: {
year: string;
month: number;
records: RewardHistory[];
}) => (
<S.RewardListContainer key={`${group.year}-${group.month}`}>
<S.HistoryYearMonthTextBox>
<S.HistoryYearMonthText>
{`${group.year}년 ${group.month}월`}
</S.HistoryYearMonthText>
</S.HistoryYearMonthTextBox>
<S.RewardListBox>
{group.records
.slice()
.reverse()
Copy link
Collaborator

Choose a reason for hiding this comment

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

요 slice()는 혹시 어떤 목적인가요!? 범위 설정이 따로 없어서 궁금하네용

Copy link
Member Author

Choose a reason for hiding this comment

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

리워드 내역 데이터를 map으로 뿌려주는데 마지막에 들어가는 데이터가 아래로 쌓여서 최신순으로 보여주기 위해 reverse()를 사용해 배열을 뒤집었습니다!

그런데 reverse()는 원본 배열을 변형시키는 파괴적 메서드라고 해서 비파괴 구조인 slice()를 사용해서 배열을 복사했습니다! slice()안에 인자 없이 사용하면 단순히 배열을 복사 해준다고 합니다.

어차피 뒤집힌 데이터를 보여줄 거라 복사를 안 하고 사용해도 문제는 없을 거 같은데 🤔 원본 데이터가 유지된 채 사용하는 게 혹시 모를 에러를 방지하지 않을까 했습니다.

스프레드 연산자랑 같은 역할을 합니다 [...group.records].reverse()

Javascript - 비파괴적 / 파괴적 메서드
파괴적 처리와 비파괴적 처리(destructive and non-destructive)
[javascript] 배열 원소 뒤집기 reverse()
[혼공] 혼자 공부하는 자바스크립트

Copy link
Collaborator

Choose a reason for hiding this comment

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

오호!!! 단순 복사를 위해서였구나 reverse()도 원본 배열 변형시키는 거였군요 ㅎㅎ 감사합니다 😊

.map(rewardHistory => {
const [date] =
rewardHistory.rewardedDate.split(' ');
const [, , day] = date.split('-');
return (
<S.RewardList key={rewardHistory.historyId}>
<S.RewardDate>
<S.RewardDateText>
{`${group.month}월 ${parseInt(day, 10)}일`}
</S.RewardDateText>
</S.RewardDate>
<S.RewardPoint>
<S.RewardPointText>
<Avatar /> +{rewardHistory.rewardPoint}
</S.RewardPointText>
</S.RewardPoint>
</S.RewardList>
);
})}
</S.RewardListBox>
<S.DividerBox>
<S.Divider />
</S.DividerBox>
</S.RewardListContainer>
),
)}
</S.HistorySection>
</S.Layout>
</S.Background>
</>
);
};

Expand Down
Loading
Loading