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

refactor: mypage에도 prefetchQuery적용, design : 홈 캐러셀 글자 문구, 디자인 수정 #415

Merged
merged 4 commits into from
Dec 16, 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
5 changes: 4 additions & 1 deletion src/api/queries/categoryQueries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ import { apiClient } from '@/lib/apiClient';
import { FetchCategoryResponse } from '@/types/Category';

// 모든 카테고리 조회 (GET)
export const fetchAllCategories = async (): Promise<FetchCategoryResponse> => {
export const fetchAllCategories = async (
customHeaders?: Record<string, string>,
): Promise<FetchCategoryResponse> => {
return apiClient<FetchCategoryResponse>('/categories/all', {
Copy link
Collaborator

Choose a reason for hiding this comment

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

폐기된 api 같아요~
리딩 리스닝 나뉘어서요!

method: 'GET',
customHeaders,
});
};

Expand Down
5 changes: 4 additions & 1 deletion src/api/queries/userQueries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@ import {
} from '@/types/User';

// 사용자 정보 조회 (GET)
export const fetchUserInfo = async (): Promise<UserResponse> => {
export const fetchUserInfo = async (
customHeaders?: Record<string, string>,
): Promise<UserResponse> => {
return apiClient<UserResponse>('/user/me', {
method: 'GET',
customHeaders,
});
};

Expand Down
253 changes: 253 additions & 0 deletions src/app/(default)/mypage/profile/ProfileClient.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
'use client';

/* eslint-disable jsx-a11y/label-has-associated-control */
import { useState, useMemo } from 'react';
// TODO(@godhyzzang) : 나중에 프로필 업로드 기능 추가
// import { Camera, X } from 'lucide-react';

import { useFetchAllCategories } from '@/api/hooks/useCategories';
import { useUserInfo, useUpdateUserInfo } from '@/api/hooks/useUserInfo';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { useToast } from '@/hooks/use-toast';

export default function UserProfile() {
const { data: userData, refetch: refetchUserInfo } = useUserInfo();
const { data: categoryData } = useFetchAllCategories();
const { toast } = useToast();

// 상태 설정
const [nickname, setNickname] = useState(userData?.data.nickname || '');
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [username, setUsername] = useState(userData?.data.username || '');
const [selectedCategories, setSelectedCategories] = useState<number[]>(
userData?.data.myCategories?.map((category) => category.id) || [],
);

const updateUserInfoMutation = useUpdateUserInfo();

// 닉네임 변경
const handleNicknameChange = () => {
// 닉네임 길이 검증
if (nickname.length < 4 || nickname.length > 12) {
toast({
duration: 1000,
description: '닉네임은 4~12자 사이로 해주세요',
});
return;
}

// 닉네임 패턴 검증
const nicknamePattern = /^[a-zA-Z가-힣0-9]+( [a-zA-Z가-힣0-9]+)*$/;
if (!nicknamePattern.test(nickname)) {
toast({
duration: 1000,
description: '닉네임은 영어, 한글, 숫자 및 단일 공백만 사용해주세요',
});
return;
}

// 서버로 닉네임 변경 요청
updateUserInfoMutation.mutate(
{ nickname },
{
onSuccess: () => {
toast({
duration: 1000,
description: '닉네임이 성공적으로 변경되었어요',
});
refetchUserInfo();
},
onError: () => {
toast({ duration: 1000, description: '닉네임을 변경하지 못했어요' });
setNickname(userData?.data.nickname || '');
},
},
);
};

// 닉네임 변경 여부 확인
const isNicknameChanged = useMemo(() => {
return nickname !== userData?.data.nickname;
}, [nickname, userData?.data.nickname]);

// 카테고리 토글
const toggleCategory = (categoryId: number) => {
setSelectedCategories((prevSelected) => {
if (prevSelected.includes(categoryId)) {
return prevSelected.filter((id) => id !== categoryId);
}
if (prevSelected.length < 5) {
return [...prevSelected, categoryId];
}

toast({
duration: 1000,
description: '카테고리는 최대 5개까지 선택 가능해요',
});

return prevSelected;
});
};

// 카테고리 변경 여부 확인
const isCategoriesChanged = useMemo(() => {
const initialCategories =
userData?.data.myCategories?.map((category) => category.id) || [];
return (
selectedCategories.length !== initialCategories.length ||
selectedCategories.some((id) => !initialCategories.includes(id))
);
}, [selectedCategories, userData?.data.myCategories]);

// 카테고리 변경
const updateCategories = () => {
updateUserInfoMutation.mutate(
{ categories: selectedCategories },
{
onSuccess: () => {
toast({
duration: 1000,
description: '카테고리가 성공적으로 변경되었어요',
});
refetchUserInfo();
},
onError: () => {
toast({
duration: 1000,
description: '카테고리를 변경하지 못했어요',
});
setSelectedCategories(
userData?.data.myCategories?.map((category) => category.id) || [],
);
},
},
);
};

return (
<div className="bg-white min-h-screen">
<header className="flex items-center p-3 border-b">
<h1 className="flex-1 text-center font-semibold">개인정보수정</h1>
</header>
<div className="p-4 max-w-xl flex flex-col justify-center items-center mx-auto">
{/* <div className="flex justify-center mb-6">
<div className="relative">
<img
src="https://upload.wikimedia.org/wikipedia/commons/8/89/Portrait_Placeholder.png"
alt="Profile"
className="w-20 h-20 rounded-full"
/>
<button
type="button"
className="absolute bottom-0 right-0 bg-gray-100 rounded-full p-1"
>
<Camera className="w-4 h-4" />
</button>
<button
type="button"
className="absolute top-0 right-0 bg-gray-100 rounded-full p-1"
>
<X className="w-4 h-4" />
</button>
</div>
</div> */}

<form className="space-y-4 w-full m-8">
<div>
<label
htmlFor="nickname"
className="block text-sm font-medium text-gray-700 mb-1"
>
닉네임 *
</label>
<div className="flex gap-2">
<Input
id="nickname"
placeholder="닉네임"
value={nickname}
onChange={(e) => setNickname(e.target.value)}
className="flex-1"
/>
<Button
type="button" // 새로고침 방지
variant="outline"
size="sm"
disabled={!isNicknameChanged}
onClick={handleNicknameChange}
>
변경하기
</Button>
</div>
</div>

<div>
<label
htmlFor="username"
className="block text-sm font-medium text-gray-700 mb-1"
>
이름 *
</label>
<div className="flex gap-2">
<Input
id="username"
placeholder="이름"
value={username}
className="flex-1"
disabled
/>
</div>
</div>

<div>
<label
htmlFor="email"
className="block text-sm font-medium text-gray-700 mb-1"
>
이메일 *
</label>
<div className="flex gap-2">
<Input
id="email"
readOnly
disabled
value={userData?.data.email || ''}
className="flex-1"
/>
</div>
</div>
</form>

<div className="mt-8 w-full">
<div className="flex justify-between">
<h2 className="text-lg font-semibold mb-4">관심 카테고리</h2>
<Button
variant="outline"
size="sm"
disabled={!isCategoriesChanged}
onClick={updateCategories}
>
변경하기
</Button>
</div>
<div className="flex flex-wrap gap-2">
{categoryData?.data.categoryList?.map((category) => (
<button
key={category.id}
type="button"
onClick={() => toggleCategory(category.id)}
className={`px-3 py-1 rounded-full text-sm ${
selectedCategories.includes(category.id)
? 'bg-primary text-primary-foreground'
: 'bg-gray-200 text-gray-800'
}`}
>
{category.name}
</button>
))}
</div>
</div>
</div>
</div>
);
}
Loading
Loading