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

[# 176] 마이페이지 서버 연결 #177

Merged
merged 3 commits into from
Sep 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
15 changes: 15 additions & 0 deletions src/apis/user/getMyPageData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { privateAxios } from '@/libs/baseAxios.ts';

export interface GetMyPageDataResponse {
nickname: string;
myPlantCount: number;
alarmCount: number;
alarmStatus: boolean;
alarmTime: number;
}

export const getMyPageData = async () => {
const response = await privateAxios.get<GetMyPageDataResponse>('/users/my');

return response.data;
};
4 changes: 4 additions & 0 deletions src/apis/user/updateAlarmStatus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { privateAxios } from '@/libs/baseAxios.ts';

export const updateAlarmStatus = (alarmStatus: boolean) =>
privateAxios.patch('/users/my/alarm-status', { alarmStatus });
4 changes: 4 additions & 0 deletions src/apis/user/updateAlarmTime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { privateAxios } from '@/libs/baseAxios.ts';

export const updateAlarmTime = (alarmTime: number) =>
privateAxios.patch('/users/my/alarm-time', { alarmTime });
4 changes: 4 additions & 0 deletions src/apis/user/updateNickname.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { privateAxios } from '@/libs/baseAxios.ts';

export const updateNickname = async (nickname: string) =>
privateAxios.patch('users/my/nickname', { nickname });
10 changes: 6 additions & 4 deletions src/components/common/Header/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@ interface HeaderProps {

const Header = ({ title, left, right, className }: HeaderProps) => {
return (
<header className={'relative h-[30px] mt-[30px]'}>
<div className={'absolute left-0 top-1/2 -translate-y-1/2'}>{left}</div>
<h1 className={cn('text-center text-Gray-900 text-small-title font-semibold', className)}>
<header className={'grid grid-cols-3 pt-[30px]'}>
<div className={'w-full flex flex-row justify-start items-center'}>{left}</div>
<h1
className={cn('w-full text-center text-Gray-900 text-small-title font-semibold', className)}
>
{title}
</h1>
<div className={'absolute right-0 top-1/2 -translate-y-1/2'}>{right}</div>
<div className={'w-full flex flex-row justify-end items-center'}>{right}</div>
</header>
);
};
Expand Down
59 changes: 37 additions & 22 deletions src/components/profile/ModifyNickname.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,32 @@ import Screen from '@/layouts/Screen';
import TextField from '../common/TextField';
import CTAButton from '../common/CTAButton';
import useInternalRouter from '@/hooks/useInternalRouter';

const myProfile = {
nickname: '블루밍',
myPlantCount: 2,
alarmCount: 1,
alarmStatus: true,
alarmTime: 1,
};
import { useGetMyPageData } from '@/queries/useGetMyPageData.ts';
import { withAsyncBoundary } from '@toss/async-boundary';
import ErrorPage from '@/pages/ErrorPage.tsx';
import LoadingSpinner from '@/components/LoadingSpinner.tsx';
import { cn } from '@/utils.ts';
import { isFalsy } from '@/utils/validation/isFalsy.ts';
import { useUpdateNickname } from '@/queries/useUpdateNickname.ts';

const ModifyNickname: React.FC = () => {
const initialNickname = myProfile.nickname;
const router = useInternalRouter();
const { data: myProfile, error } = useGetMyPageData();

return (
<Screen className="px-0">
<div className="bg-Gray50">
<div className="flex justify-between px-[24px] pt-[31.31px]">
<img src={backButtonIcon} alt="뒤로가기 버튼 아이콘" onClick={() => router.goBack()} />
<p className="text-[20px] text-Gray900 font-semibold">닉네임 수정</p>
<div className="w-[24px] h-[24px]" />
</div>
if (!myProfile) throw Error(error?.message);

<MyProfile justImg={true} myProfile={myProfile} />
<Nickname initialNickname={initialNickname} />
const initialNickname = myProfile.nickname;

return (
<Screen className="px-0 min-h-dvh bg-Gray50 flex flex-col">
<div className="flex justify-between px-[24px] pt-[31.31px]">
<img src={backButtonIcon} alt="뒤로가기 버튼 아이콘" onClick={() => router.goBack()} />
<p className="text-[20px] text-Gray900 font-semibold">닉네임 수정</p>
<div className="w-[24px] h-[24px]" />
</div>

<MyProfile justImg={true} myProfile={myProfile} />
<Nickname initialNickname={initialNickname} />
</Screen>
);
};
Expand All @@ -40,6 +41,7 @@ interface NicknameProps {

const Nickname: React.FC<NicknameProps> = ({ initialNickname }) => {
const [nickname, setNickname] = useState<string>('');
const { mutate: updateNickname } = useUpdateNickname();

const handleClear = () => {
setNickname('');
Expand All @@ -50,8 +52,12 @@ const Nickname: React.FC<NicknameProps> = ({ initialNickname }) => {
setNickname(value);
};

const handleSubmit = () => {
updateNickname(nickname);
};

return (
<div className="flex flex-col justify-between h-screen">
<div className="flex flex-col justify-between flex-1">
<div>
<p className="pt-[30px] px-[24px] pb-[6px] text-Gray800 text-[13px]">닉네임 수정</p>
<div className="box-border flex w-[calc(100%-40px)] mx-auto items-center justify-between text-[15px] font-medium">
Expand All @@ -68,10 +74,19 @@ const Nickname: React.FC<NicknameProps> = ({ initialNickname }) => {
</div>

<div className="px-[24px] pb-[10px]">
<CTAButton text="확인" className="w-full bg-BloomingGreen500" />
<CTAButton
type="button"
onClick={handleSubmit}
text="확인"
className={cn('w-full', isFalsy(nickname) ? 'bg-Gray300' : 'bg-BloomingGreen500')}
disabled={isFalsy(nickname)}
/>
</div>
</div>
);
};

export default ModifyNickname;
export default withAsyncBoundary(ModifyNickname, {
rejectedFallback: ({ error, reset }) => <ErrorPage error={error} reset={reset} />,
pendingFallback: <LoadingSpinner />,
});
2 changes: 1 addition & 1 deletion src/components/profile/MyProfile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const MyProfile: React.FC<MyProfileProps> = ({ justImg, myProfile }) => {
{justImg === true ? (
''
) : (
<img src={pencil} alt="수정하기 아이콘" onClick={() => push('/profile/edit')} />
<img src={pencil} alt="수정하기 아이콘" onClick={() => push(`/profile/edit`)} />
)}
</div>
</div>
Expand Down
64 changes: 35 additions & 29 deletions src/components/profile/SetNotifications.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { useState, useEffect } from 'react';
import { useEffect, useState } from 'react';
import Toggle from '../common/Toggle';
import pencil from '@/assets/icon/pencil.svg';
import BottomSheet from '@/components/common/BottomSheet';
import { MyProfileProps } from '@/types/profile';
import { useUpdateAlarmStatus } from '@/queries/useUpdateAlarmStatus.ts';
import { useUpdateAlarmTime } from '@/queries/useUpdateAlarmTime.ts';

const timeMap = {
'AM 06-07': 0,
Expand All @@ -26,35 +28,39 @@ const timeMap = {
'AM 12-01': 18,
};

const reverseTimeMap = Object.fromEntries(
Object.entries(timeMap).map(([key, value]) => [value, key]),
);
const getTimeLabel = (alarmTime: number): string => {
const label = Object.entries(timeMap).find((item) => item[1] === alarmTime);

const getTimeLabel = (alarmTime: number | null): string => {
if (alarmTime === null || !(alarmTime in reverseTimeMap)) {
return '시간 선택';
if (!label) {
throw Error('유효하지 않은 알람 시간대 입니다.');
}
return reverseTimeMap[alarmTime];

return label[0];
};

const SetNotifications: React.FC<MyProfileProps> = ({ myProfile }) => {
const [isBottomSheetOpen, setBottomSheetOpen] = useState(false);
const [isChecked, setIsChecked] = useState<boolean>(false);
const [selectedTime, setSelectedTime] = useState<string | null>(null);
const [selectedTime, setSelectedTime] = useState<number | undefined>();
const { mutate: updateAlarmStatus } = useUpdateAlarmStatus();
const { mutate: updateAlarmTime } = useUpdateAlarmTime();

useEffect(() => {
if (myProfile?.alarmStatus !== undefined) {
setIsChecked(myProfile.alarmStatus);
}
setSelectedTime(getTimeLabel(myProfile?.alarmTime ?? null));
}, [myProfile?.alarmStatus, myProfile?.alarmTime]);
const handleCheckedChange = () => {
updateAlarmStatus(!myProfile.alarmStatus);
};

const handleCheckedChange = (checked: boolean) => {
setIsChecked(checked);
const handleTimeSelect = (value: number) => {
setSelectedTime(value);
};

const handleTimeSelect = (time: string) => {
setSelectedTime(time);
useEffect(() => {
setSelectedTime(myProfile.alarmTime);
}, [myProfile.alarmTime]);

const handleAlarmTimeSubmit = () => {
if (!selectedTime) return;

updateAlarmTime(selectedTime);
setBottomSheetOpen(false);
};

return (
Expand All @@ -65,13 +71,13 @@ const SetNotifications: React.FC<MyProfileProps> = ({ myProfile }) => {
<section className="flex flex-col gap-[10px]">
<div className="box-border flex w-[calc(100%-40px)] mx-auto px-[24px] py-[16px] bg-white border border-GrayOpacity100 items-center justify-between rounded-[10px]">
<p className="text-Gray800 text-[15px] font-medium">블루밍 알림 설정</p>
<Toggle checked={isChecked} onCheckedChange={handleCheckedChange} />
<Toggle checked={myProfile.alarmStatus} onCheckedChange={() => handleCheckedChange()} />
</div>
{isChecked && (
{myProfile.alarmStatus && (
<div className="box-border flex w-[calc(100%-40px)] mx-auto px-[24px] py-[16px] bg-white border border-GrayOpacity100 items-center justify-between rounded-[10px] text-[15px] font-medium">
<p className="text-Gray800">알림 시간대</p>
<div className="flex gap-[10px] items-center">
<p className="text-BloomingGreen500">{selectedTime}</p>
<p className="text-BloomingGreen500">{getTimeLabel(myProfile.alarmTime)}</p>
<img
src={pencil}
alt="수정하기 아이콘"
Expand All @@ -91,17 +97,17 @@ const SetNotifications: React.FC<MyProfileProps> = ({ myProfile }) => {
<div className="px-[24px] py-[10px]">
<div className="max-h-[260px] overflow-y-auto">
<ul className="p-0 list-none">
{Object.keys(timeMap).map((time) => (
{Object.entries(timeMap).map(([title, value]) => (
<li
key={time}
key={title}
className={`text-[17px] rounded-[10px] py-[15px] px-[24px] text-center transition-colors ${
selectedTime === time
selectedTime === value
? 'bg-GrayOpacity100 text-black'
: 'text-Gray400 hover:bg-GrayOpacity100 hover:text-black'
} cursor-pointer`}
onClick={() => handleTimeSelect(time)}
onClick={() => handleTimeSelect(value)}
>
{time}
{title}
</li>
))}
</ul>
Expand All @@ -111,7 +117,7 @@ const SetNotifications: React.FC<MyProfileProps> = ({ myProfile }) => {
actions={[
<button
key="confirm"
onClick={() => setBottomSheetOpen(false)}
onClick={handleAlarmTimeSubmit}
className="w-full py-[18px] px-[28px] bg-BloomingGreen500 text-white rounded-[16px]"
>
확인
Expand Down
28 changes: 28 additions & 0 deletions src/mocks/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { regions } from '@/mocks/mockDatas/regions.ts';
import { weathers } from '@/mocks/mockDatas/weatherMessage.ts';
import { refreshAccessTokenResponseData } from '@/mocks/mockDatas/refreshAccessToken.ts';
import { plantGuide } from '@/mocks/mockDatas/guideDetail.ts';
import { myPageData } from '@/mocks/mockDatas/myPageData.ts';

export const handlers = [
http.post(import.meta.env.VITE_API_URL + '/plants', async ({ request }) => {
Expand Down Expand Up @@ -142,4 +143,31 @@ export const handlers = [
await delay(1000);
return HttpResponse.json(plantGuide);
}),

http.get(import.meta.env.VITE_API_URL + '/users/my', async () => {
await delay(1000);
return HttpResponse.json(myPageData);
}),

http.patch(import.meta.env.VITE_API_URL + '/users/my/nickname', async () => {
await delay(1000);

return HttpResponse.json({
message: '수정 되었습니다.',
});
}),

http.patch(import.meta.env.VITE_API_URL + '/users/my/alarm-status', async () => {
await delay(1000);
return HttpResponse.json({
message: '수정 되었습니다.',
});
}),

http.patch(import.meta.env.VITE_API_URL + '/users/my/alarm-time', async () => {
await delay(1000);
return HttpResponse.json({
message: '수정 되었습니다.',
});
}),
];
7 changes: 7 additions & 0 deletions src/mocks/mockDatas/myPageData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const myPageData = {
nickname: '최기환',
myPlantCount: 2,
alarmCount: 2,
alarmStatus: true,
alarmTime: 1,
};
1 change: 0 additions & 1 deletion src/pages/AddPlantPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,6 @@ const AddPlantPage = () => {
onClick={() => {
router.goBack();
}}
className={'absolute right-0 top-1/2 -translate-y-1/2'}
>
<IconXMono />
</button>
Expand Down
17 changes: 9 additions & 8 deletions src/pages/Profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,13 @@ import SetNotifications from '@/components/profile/SetNotifications';
import TopButton from '@/components/profile/TopButton';
import UseInformation from '@/components/profile/UseInformation';
import Screen from '@/layouts/Screen';
import { useGetMyPageData } from '@/queries/useGetMyPageData.ts';
import { withAsyncBoundary } from '@toss/async-boundary';
import ErrorPage from '@/pages/ErrorPage.tsx';
import LoadingSpinner from '@/components/LoadingSpinner.tsx';

const Profile = () => {
const myProfile = {
nickname: '블루밍',
myPlantCount: 2,
alarmCount: 1,
alarmStatus: true,
alarmTime: 8,
};
const { data: myProfile } = useGetMyPageData();

return (
<div className="bg-Gray50">
Expand All @@ -25,4 +23,7 @@ const Profile = () => {
);
};

export default Profile;
export default withAsyncBoundary(Profile, {
rejectedFallback: ({ error, reset }) => <ErrorPage error={error} reset={reset} />,
pendingFallback: <LoadingSpinner />,
});
4 changes: 4 additions & 0 deletions src/queries/keyStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ export const keyStore = createQueryKeyStore({
user: {
register: null,
regions: (query: string) => [query],
getMyPageData: null,
updateNickname: null,
updateAlarmStatus: null,
updateAlarmTime: null,
},
home: {
getHomeData: null,
Expand Down
9 changes: 9 additions & 0 deletions src/queries/useGetMyPageData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { useSuspenseQuery } from '@tanstack/react-query';
import { keyStore } from '@/queries/keyStore.ts';
import { getMyPageData } from '@/apis/user/getMyPageData.ts';

export const useGetMyPageData = () =>
useSuspenseQuery({
queryKey: keyStore.user.getMyPageData.queryKey,
queryFn: getMyPageData,
});
Loading
Loading