-
Notifications
You must be signed in to change notification settings - Fork 3
Feat/125/my page profile #134
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
Changes from all commits
ffe3017
15249d9
a0a4aef
79105a1
fe90461
7c7efbd
a54e378
0250543
f867559
92b1af1
20522d6
7a72219
8e373cf
8b87010
f92e139
843083e
47b8c5f
6168d99
d157a7e
2b952f8
8cd3ccd
92f9509
edbb817
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -9,3 +9,44 @@ export function getUser(): Promise<User> { | |||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }).then((response) => response.data); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // ํ์์ ๋ณด ์์ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export async function updateUserProfile(file: File): Promise<void> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const url = `/auths/user`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const formData = new FormData(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| formData.append('file', file); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await fetchApi(url, { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| method: 'PUT', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| body: formData, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export async function fetchUpdatedUser() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return fetchApi<{ data: User }>('/auths/user', { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| method: 'GET', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| headers: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'Content-Type': 'application/json', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'Cache-Control': 'no-cache', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .then((response) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return response.data; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .catch((error) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw error; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // ์ ์ ํ๋กํ ์ด๋ฏธ์ง ์ด๊ธฐํ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export async function resetUserProfileImage(): Promise<void> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const url = `/auths/profile-image/reset`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await fetchApi(url, { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| method: 'PUT', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| headers: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'Content-Type': 'application/json', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+43
to
+50
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ๐ ๏ธ Refactor suggestion ์๋ต ์ฒ๋ฆฌ์ ์ค๋ฅ ์ฒ๋ฆฌ๋ฅผ ๊ฐ์ ํด์ผ ํฉ๋๋ค. ํ๋กํ ์ด๋ฏธ์ง ์ด๊ธฐํ API์ ๋ค์ ๊ฐ์ ์ฌํญ์ด ํ์ํฉ๋๋ค:
๋ค์๊ณผ ๊ฐ์ ์์ ์ ์ ์๋๋ฆฝ๋๋ค: -export async function resetUserProfileImage(): Promise<void> {
+interface ResetProfileResponse {
+ message: string;
+ success: boolean;
+}
+
+export async function resetUserProfileImage(): Promise<ResetProfileResponse> {
const url = `/auths/profile-image/reset`;
- await fetchApi(url, {
+ return fetchApi<ResetProfileResponse>(url, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
- });
+ }).catch((error) => {
+ console.error('ํ๋กํ ์ด๋ฏธ์ง ์ด๊ธฐํ ์คํจ:', error);
+ throw error;
+ });
}๐ Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,103 @@ | ||
| 'use client'; | ||
|
|
||
| import { useEffect, useState } from 'react'; | ||
| import { toast } from 'react-toastify'; | ||
| import { useRouter } from 'next/navigation'; | ||
| import { | ||
| fetchUpdatedUser, | ||
| resetUserProfileImage, | ||
| updateUserProfile, | ||
| } from '@/src/_apis/auth/user-apis'; | ||
| import { useAuthStore } from '@/src/store/use-auth-store'; | ||
| import { User } from '@/src/types/auth'; | ||
| import ProfileCardPresenter from './presenter'; | ||
|
|
||
| export default function ProfileCard() { | ||
| const router = useRouter(); | ||
| const { isAuth, rehydrated, setUser } = useAuthStore(); | ||
| const [user, setLocalUser] = useState<User | null>(null); | ||
| const [isLoading, setIsLoading] = useState(true); | ||
| const [profileImageUrl, setProfileImageUrl] = useState<string>(''); | ||
|
|
||
| useEffect(() => { | ||
| const checkAuthAndLoadUser = async () => { | ||
| if (!rehydrated) return; // ์ํ ๋ณต์์ด ์๋ฃ๋์ง ์์์ผ๋ฉด ๋๊ธฐ | ||
|
|
||
| if (!isAuth) { | ||
| router.push('/login'); // ์ธ์ฆ๋์ง ์์ ๊ฒฝ์ฐ ๋ฆฌ๋๋ ์ | ||
| return; | ||
| } | ||
|
|
||
| setIsLoading(true); | ||
|
|
||
| try { | ||
| const updatedUser = await fetchUpdatedUser(); | ||
| setLocalUser(updatedUser); | ||
| setUser(updatedUser); | ||
| setProfileImageUrl(updatedUser.profileImageUrl); | ||
| } catch { | ||
| toast.error('์ ์ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์ค๋ ๋ฐ ์คํจํ์ต๋๋ค.'); | ||
| } finally { | ||
| setIsLoading(false); | ||
| } | ||
| }; | ||
|
|
||
| checkAuthAndLoadUser(); | ||
| }, [isAuth, rehydrated, router, setUser]); | ||
|
|
||
| if (!rehydrated) return null; | ||
| if (!isAuth) return null; | ||
| if (isLoading) return <div>๋ก๋ฉ ์ค...</div>; | ||
| if (!user) return <div>์ ์ ์ ๋ณด๋ฅผ ๋ถ๋ฌ์ค์ง ๋ชปํ์ต๋๋ค.</div>; | ||
|
|
||
| const handleEdit = () => { | ||
| const input = document.createElement('input'); | ||
| input.type = 'file'; | ||
| input.accept = '.png,.jpg,.jpeg'; | ||
| input.onchange = async (event) => { | ||
| const file = (event.target as HTMLInputElement)?.files?.[0]; | ||
| if (file) { | ||
| if (file.size > 5 * 1024 * 1024) { | ||
| alert('5MB ์ดํ์ ํ์ผ๋ง ์ ๋ก๋ ๊ฐ๋ฅํฉ๋๋ค.'); | ||
| return; | ||
| } | ||
|
|
||
| try { | ||
| await updateUserProfile(file); | ||
|
|
||
| const tempUrl = URL.createObjectURL(file); | ||
| setProfileImageUrl(tempUrl); | ||
|
|
||
| const updatedUser = await fetchUpdatedUser(); | ||
| const newProfileImageUrl = `${updatedUser.profileImageUrl}?timestamp=${new Date().getTime()}`; | ||
| setProfileImageUrl(newProfileImageUrl); | ||
| setUser({ ...updatedUser, profileImageUrl: newProfileImageUrl }); | ||
| } catch (error) { | ||
| toast.error('ํ์ผ ์ ๋ก๋์ ์คํจํ์ต๋๋ค.'); | ||
| } | ||
| } | ||
| }; | ||
| input.click(); | ||
| }; | ||
|
|
||
| const handleDeleteProfile = async () => { | ||
| try { | ||
| await resetUserProfileImage(); | ||
| const updatedUser = await fetchUpdatedUser(); | ||
| setProfileImageUrl(''); // ์ด๊ธฐํ๋ ์ด๋ฏธ์ง ๋ฐ์ | ||
| setLocalUser(updatedUser); | ||
| setUser(updatedUser); | ||
| toast.success('ํ๋กํ ์ด๋ฏธ์ง๊ฐ ์ด๊ธฐํ๋์์ต๋๋ค.'); | ||
| } catch (error) { | ||
| toast.error('ํ๋กํ ์ด๋ฏธ์ง ์ด๊ธฐํ์ ์คํจํ์ต๋๋ค.'); | ||
| } | ||
| }; | ||
|
|
||
| return ( | ||
| <ProfileCardPresenter | ||
| data={{ ...user, profileImageUrl }} | ||
| onEdit={handleEdit} | ||
| onDelete={handleDeleteProfile} | ||
| /> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| 'use client'; | ||
|
|
||
| import { Menu } from '@mantine/core'; | ||
| import { Profile } from '@/src/components/common/profile'; | ||
| import { UserType } from '@/src/types/user'; | ||
|
|
||
| export interface ProfileCardProps { | ||
| data: UserType; | ||
| onEdit: () => void; | ||
| onDelete: () => void; | ||
| } | ||
|
|
||
| export default function ProfileCardPresenter({ data, onEdit, onDelete }: ProfileCardProps) { | ||
| return ( | ||
| <div className="flex items-end justify-between"> | ||
| <div className="flex items-center gap-6.5"> | ||
| <figure className="h-20 w-20 md:h-30 md:w-30 lg:h-30 lg:w-30"> | ||
| <Menu | ||
| shadow="sm" | ||
| width={170} | ||
| offset={-5} | ||
| withArrow | ||
| arrowPosition="side" | ||
| position="bottom-start" | ||
| > | ||
| <Menu.Target> | ||
| <div className="h-full w-full"> | ||
| <Profile editable imageUrl={data?.profileImageUrl ?? ''} /> | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ๐ ๏ธ Refactor suggestion ํ๋กํ ์ด๋ฏธ์ง ๋ก๋ฉ ๋ฐ ์๋ฌ ์ํ ์ฒ๋ฆฌ ํ์ ํ๋กํ ์ด๋ฏธ์ง URL์ด ์๊ฑฐ๋ ๋ก๋ฉ ์ค์ผ ๋์ ์ํ ์ฒ๋ฆฌ๊ฐ ํ์ํฉ๋๋ค. -<Profile editable imageUrl={data?.profileImageUrl ?? ''} />
+<Profile
+ editable
+ imageUrl={data?.profileImageUrl ?? ''}
+ loading={!data}
+ onError={() => {/* ์๋ฌ ์ฒ๋ฆฌ ๋ก์ง */}}
+/>
|
||
| </div> | ||
| </Menu.Target> | ||
| <Menu.Dropdown className="translate-x-16 translate-y-2 transform md:translate-x-24 md:translate-y-0 lg:translate-x-24 lg:translate-y-0"> | ||
| <Menu.Item onClick={onEdit}>ํ๋กํ ์ด๋ฏธ์ง ์์ ํ๊ธฐ</Menu.Item> | ||
| <Menu.Item color="red" onClick={onDelete}> | ||
| ๊ธฐ๋ณธ ํ๋กํ๋ก ๋์๊ฐ๊ธฐ | ||
| </Menu.Item> | ||
| </Menu.Dropdown> | ||
| </Menu> | ||
| </figure> | ||
| <div className="flex flex-col gap-2"> | ||
| <p className="text-xl font-semibold text-gray-900 md:text-2xl lg:text-2xl"> | ||
| {data?.nickname} ๋, ์๋ ํ์ธ์๐ | ||
| </p> | ||
| <dl className="flex text-base font-medium text-gray-700"> | ||
| <dt className="flex-shrink-0 flex-grow-0 basis-20">Email</dt> | ||
| <dd className="flex-1">{data?.email}</dd> | ||
| </dl> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,23 +3,15 @@ | |
| import { useState } from 'react'; | ||
| import { Divider } from '@mantine/core'; | ||
| import { useInfiniteScroll } from '@/src/hooks/use-infinite-scroll'; | ||
| import ProfileCardContainer from '@/src/app/(crew)/mypage/_components/profile-card/container'; | ||
| import { fetchWritableGatheringData } from '@/src/app/(crew)/api/mock-api/writable-gathering'; | ||
| import { fetchMyReviewData } from '@/src/app/api/mock-api/review'; | ||
| import ReviewCardList from '@/src/components/common/review-list/review-card-list'; | ||
| import Tabs from '@/src/components/common/tab'; | ||
| import WritableGatheringCardList from '@/src/components/common/writable-gathering-card/writable-gathering-card-list'; | ||
| import { ReviewInformResponse } from '@/src/types/review'; | ||
| import { WritableGatheringCardInformResponse } from '@/src/types/writable-gathering-card'; | ||
| import { fetchMyReviewData } from '../../api/mock-api/review'; | ||
| import { fetchWritableGatheringData } from '../api/mock-api/writable-gathering'; | ||
|
|
||
| const mockData = { | ||
| id: 1, | ||
| profileImageUrl: '', | ||
| nickname: '์จ์จ', | ||
| email: '[email protected]', | ||
| }; | ||
|
|
||
| export default function MyPage() { | ||
| export default function ReviewSection() { | ||
| const myPageTabs = [ | ||
| { label: '์์ฑ ๊ฐ๋ฅํ ๋ฆฌ๋ทฐ', id: 'available-review' }, | ||
| { label: '์์ฑํ ๋ฆฌ๋ทฐ', id: 'my-review' }, | ||
|
|
@@ -43,15 +35,12 @@ export default function MyPage() { | |
| isFetchingNextPage: isFetchingGatheringNextPage, | ||
| } = useInfiniteScroll<WritableGatheringCardInformResponse>({ | ||
| queryKey: ['crew'], | ||
| queryFn: ({ pageParam = 0 }) => { | ||
| return fetchWritableGatheringData(pageParam, 3); | ||
| }, | ||
| queryFn: ({ pageParam = 0 }) => fetchWritableGatheringData(pageParam, 3), | ||
| getNextPageParam: (lastPage, allPages) => | ||
| lastPage.hasNextPage ? allPages.length + 1 : undefined, | ||
| }); | ||
|
|
||
| const renderTabContent = () => { | ||
| // TODO : ๋ฆฌํด ๊ฐ ์ปดํฌ๋ํธ๋ก ๊ต์ฒด | ||
| switch (currentTab) { | ||
| case 'my-review': | ||
| return ( | ||
|
|
@@ -73,27 +62,20 @@ export default function MyPage() { | |
| ); | ||
| } | ||
| }; | ||
|
|
||
| return ( | ||
| <div className="container mx-auto my-0 min-h-screen max-w-pc bg-gray-50 px-3 py-11 md:px-8 lg:px-11"> | ||
| <div className="lg:gap-4.5 flex flex-col gap-3 md:gap-4"> | ||
| <ProfileCardContainer data={mockData} /> | ||
| </div> | ||
| <div className="mt-12 flex flex-col"> | ||
| <h3 className="text-2xl font-semibold text-gray-900">๋์ ๋ฆฌ๋ทฐ ๋ชจ์๋ณด๊ธฐ</h3> | ||
| <Divider mt={16} mb={24} size={2} /> | ||
| <div className="flex justify-start"> | ||
| <Tabs | ||
| variant="review" | ||
| tabs={myPageTabs} | ||
| activeTab={currentTab} | ||
| onTabClick={(id) => { | ||
| setCurrentTab(id); | ||
| }} | ||
| /> | ||
| </div> | ||
| <div className="pt-12">{renderTabContent()}</div> | ||
| <div className="mt-12 flex flex-col"> | ||
| <h3 className="text-2xl font-semibold text-gray-900">๋์ ๋ฆฌ๋ทฐ ๋ชจ์๋ณด๊ธฐ</h3> | ||
| <Divider mt={16} mb={24} size={2} /> | ||
| <div className="flex justify-start"> | ||
| <Tabs | ||
| variant="review" | ||
| tabs={myPageTabs} | ||
| activeTab={currentTab} | ||
| onTabClick={(id) => setCurrentTab(id)} | ||
| /> | ||
| </div> | ||
| <div /> | ||
| <div className="pt-12">{renderTabContent()}</div> | ||
| </div> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| import ProfileCardContainer from '@/src/app/(crew)/my-page/_components/profile-card/container'; | ||
| import ReviewSection from '@/src/app/(crew)/my-page/_components/review-section'; | ||
|
|
||
| export default function MyPage() { | ||
| return ( | ||
| <div className="container mx-auto my-0 min-h-screen max-w-pc bg-gray-50 px-3 py-11 md:px-8 lg:px-11"> | ||
| <div className="lg:gap-4.5 flex flex-col gap-3 md:gap-4"> | ||
| <ProfileCardContainer /> | ||
| </div> | ||
| <ReviewSection /> | ||
| <div /> | ||
| </div> | ||
| ); | ||
| } |
This file was deleted.
This file was deleted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
๐ ๏ธ Refactor suggestion
์ฝ๋ ์ค๋ณต์ ์ ๊ฑฐํ๊ณ ์ฌ์ฌ์ฉ์ฑ์ ๋์ผ ์ ์์ต๋๋ค.
getUser์fetchUpdatedUserํจ์๊ฐ ์๋น ๋ถ๋ถ ์ค๋ณต๋์ด ์์ต๋๋ค. ๊ณตํต ๋ก์ง์ ์ถ์ถํ์ฌ ์ฌ์ฌ์ฉํ๋ ๊ฒ์ด ์ข๊ฒ ์ต๋๋ค.๋ค์๊ณผ ๊ฐ์ ๋ฆฌํฉํ ๋ง์ ์ ์๋๋ฆฝ๋๋ค:
๐ Committable suggestion