-
Notifications
You must be signed in to change notification settings - Fork 1
[feat] 프로필 수정 api 연결 #69
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
The head ref may contain hidden characters: "feat/#65/\uD504\uB85C\uD544\uC218\uC815-api-\uC5F0\uACB0"
Changes from 2 commits
a7f9d3f
9a688ff
0820d68
3504df7
9ea73ad
507d8b4
a03133a
5d49725
4be92fc
86e457c
ce2ee9c
e7af571
e2af8c5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| import api from '@/api/api'; | ||
| import type { UserProfile } from '@/types/user'; | ||
| import type { ApiError } from '@/types/api-response'; | ||
|
|
||
| export const getUserProfile = async (): Promise<UserProfile> => { | ||
| try { | ||
| const response = await api.get<UserProfile>('/profile'); | ||
| return response.data; | ||
| } catch (err) { | ||
| const apiError = err as ApiError; | ||
| console.error('프로필 로딩 에러:', apiError); | ||
| throw apiError; | ||
| } | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| import api from '@/api/api'; | ||
| import type { UpdateUserProfileRequest, UserProfileResponse } from '@/types/userEdit'; | ||
|
|
||
| export const updateUserProfile = async ( | ||
| data: UpdateUserProfileRequest | ||
| ): Promise<UserProfileResponse> => { | ||
| const res = await api.put('/api/v1/profile', data); | ||
|
||
| return res.data.data; | ||
|
||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| import { useQuery } from '@tanstack/react-query'; | ||
| import api from '@/api/api'; | ||
|
|
||
| export interface UserProfile { | ||
| nickname: string; | ||
| imageUrl: string | null; | ||
| genres: string[]; | ||
| auditoriums: string[]; | ||
| } | ||
|
|
||
| const fetchUserProfile = async (): Promise<UserProfile> => { | ||
| const res = await api.get('/user/profile'); | ||
| return res.data.data; | ||
| }; | ||
|
|
||
| export const useUserProfileQuery = () => { | ||
| return useQuery<UserProfile>({ | ||
| queryKey: ['userProfile'], | ||
| queryFn: fetchUserProfile, | ||
| staleTime: 1000 * 60 * 5, // 5분 캐시 | ||
| }); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,24 +1,67 @@ | ||
| import { useNavigate } from 'react-router-dom'; | ||
| import { useNavigate, useLocation } from 'react-router-dom'; | ||
| import { useEffect, useState } from 'react'; | ||
| import { Button, Header } from '@/components'; | ||
| import { MyProfileEditIcon, PencilIcon } from '@/assets'; | ||
| import { updateUserProfile } from '@/api/user/userEdit.api'; | ||
| import { useToastStore } from '@/store'; | ||
| import { useUserProfileQuery } from '@/hooks/queries/useUserProfileQuery'; | ||
|
|
||
|
|
||
| export default function ProfileEdit() { | ||
| const navigate = useNavigate(); | ||
| const location = useLocation(); | ||
| const { show: showToast } = useToastStore(); | ||
| const { data: user, isLoading } = useUserProfileQuery(); | ||
| const [nickname, setNickname] = useState(''); | ||
| const [genres, setGenres] = useState<string[]>([]); | ||
| const [auditoriums, setAuditoriums] = useState<string[]>([]); | ||
|
|
||
| useEffect(() => { | ||
| if (!user) return; | ||
|
|
||
| const state = location.state; | ||
|
|
||
| setNickname(state?.nickname ?? user.nickname); | ||
|
|
||
| setGenres( | ||
| Array.isArray(state?.genres) && state.genres.length > 0 | ||
| ? state.genres | ||
| : user.genres | ||
| ); | ||
|
|
||
| setAuditoriums( | ||
| Array.isArray(state?.auditoriums) && state.auditoriums.length > 0 | ||
| ? state.auditoriums | ||
| : user.auditoriums | ||
| ); | ||
| }, [user, location.state]); | ||
| const handleNavigateToGenreSelect = () => { | ||
| navigate('/my/select-genre'); | ||
| navigate('/my/select-genre', { state: { nickname, auditoriums } }); | ||
| }; | ||
|
|
||
| const handleNavigateToCinemaChoice = () => { | ||
| navigate('/my/cinema-choice'); | ||
| navigate('/my/cinema-choice', { state: { nickname, genres } }); | ||
| }; | ||
|
|
||
| const handleSave = async () => { | ||
| try { | ||
| await updateUserProfile({ | ||
| nickname, | ||
| genres, | ||
| auditoriums, | ||
| }); | ||
| showToast('프로필이 저장되었습니다.', 3000); | ||
| navigate('/my'); | ||
| } catch (error) { | ||
| showToast('저장에 실패했습니다.', 3000); | ||
|
||
| console.error(error); | ||
| } | ||
| }; | ||
|
|
||
| return ( | ||
| <div className="font-suit flex h-screen flex-col text-white"> | ||
| {/* Header */} | ||
| <Header leftSection="BACK" rightSection="KEBAB" className="bg-gray-900" /> | ||
| {/* Main Content */} | ||
| <main className="flex-grow overflow-y-auto px-4 pt-[68px]"> | ||
| {/* 프로필 정보 섹션 */} | ||
| <main className="flex-grow overflow-y-auto px-4 pt-[49px]"> | ||
| <section className="mt-4 flex flex-col items-center rounded-xl bg-gray-800/30 px-4 py-6"> | ||
| <button className="mb-6" aria-label="프로필 사진 변경"> | ||
| <MyProfileEditIcon className="h-20 w-20" /> | ||
|
|
@@ -30,50 +73,46 @@ export default function ProfileEdit() { | |
| <input | ||
| id="nickname" | ||
| type="text" | ||
| defaultValue="김씨잇" | ||
| value={nickname} | ||
| onChange={(e) => setNickname(e.target.value)} | ||
| className="mt-2 h-[46px] w-full rounded-md border border-gray-800 bg-gray-900 px-4 py-3 text-white placeholder-gray-500 focus:outline-none" | ||
| /> | ||
| </div> | ||
| </section> | ||
|
|
||
| <div className="mx-auto mt-10 flex h-[168px] w-[330px] flex-col justify-around"> | ||
| <button | ||
| onClick={handleNavigateToGenreSelect} | ||
| className="flex w-full items-start justify-between text-left" | ||
| > | ||
| <div className="mx-auto mt-2 flex h-[168px] w-[330px] flex-col justify-around gap-y-2"> | ||
| <button onClick={handleNavigateToGenreSelect} className="flex w-full justify-between text-left"> | ||
| <div> | ||
| <h2 className="text-title-3 text-white">선호 장르</h2> | ||
| <p className="text-caption-2 mt-2 text-red-300">호러, SF, 로맨스</p> | ||
| <p className="text-caption-2 mt-2 text-red-300">{genres.join(', ')}</p> | ||
| </div> | ||
| <PencilIcon className="h-6 w-6 flex-shrink-0" /> | ||
| <PencilIcon className="h-6 w-6" /> | ||
| </button> | ||
|
|
||
| {/* 즐겨찾는 영화관 섹션 */} | ||
| <button | ||
| onClick={handleNavigateToCinemaChoice} | ||
| className="flex w-full items-start justify-between text-left" | ||
| > | ||
| <button onClick={handleNavigateToCinemaChoice} className="flex w-full justify-between text-left"> | ||
| <div> | ||
| <h2 className="text-title-3 text-white">즐겨찾는 영화관</h2> | ||
| <div className="text-caption-2 mt-2 space-y-2 text-red-300"> | ||
| <p>남양주현대아울렛 스페이스원</p> | ||
| <p>용산아이파크몰 (용아맥)</p> | ||
| {auditoriums.map((a: string) => ( | ||
| <p key={a}>{a}</p> | ||
| ))} | ||
| </div> | ||
| </div> | ||
| <PencilIcon className="h-6 w-6 flex-shrink-0" /> | ||
| <PencilIcon className="h-6 w-6" /> | ||
| </button> | ||
| </div> | ||
| </main> | ||
|
|
||
| {/* Footer */} | ||
| <footer className="flex-shrink-0 bg-gray-900 px-4 py-3"> | ||
| <footer className="bg-gray-900 px-4 py-3"> | ||
| <Button | ||
| variant="primary" | ||
| color="red" | ||
| size="lg" | ||
| rounded="lg" | ||
| className="w-full" | ||
| fontType="title-3" | ||
| onClick={handleSave} | ||
| disabled={isLoading} | ||
| > | ||
| 저장하기 | ||
| </Button> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,12 @@ | ||
| export interface User { | ||
| userId: number; | ||
| nickname: string; | ||
| profileImageUrl: string; | ||
| profileImageUrl: string | null; | ||
| } | ||
|
|
||
| export interface UserProfile extends User { | ||
| level: number; | ||
| progress: number; | ||
| preferredGenres: string[]; | ||
| favoriteTheaters: string[]; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| export interface UpdateUserProfileRequest { | ||
| nickname: string; | ||
| imageUrl?: string | null; | ||
| genres?: string[]; | ||
| auditoriums?: string[]; | ||
| } | ||
|
|
||
| export interface UserProfileResponse { | ||
| nickname: string; | ||
| imageUrl: string | null; | ||
| genres: string[]; | ||
| auditoriums: string[]; | ||
| } |


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.
ApiResponse 타입을 활용하면 좋을 것 같습니당