Skip to content

Commit 54ab59a

Browse files
Merge pull request #78 from dnd-side-project/feature/#76/mycd-edit
2 parents c9e94f5 + c64d86f commit 54ab59a

File tree

18 files changed

+292
-102
lines changed

18 files changed

+292
-102
lines changed

β€Žsrc/app/providers/ToastProvider.tsxβ€Ž

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@ import styled from 'styled-components'
44

55
import { Toast } from '@/shared/ui'
66

7-
type ToastType = 'LINK' | 'IMAGE' | 'REPORT' | 'COMMENT'
7+
type ToastType = 'LINK' | 'IMAGE' | 'REPORT' | 'COMMENT' | 'AUTH_EXPIRED'
88

99
const TOAST_MESSAGES: Record<ToastType, string> = {
1010
LINK: '링크가 λ³΅μ‚¬λμ–΄μš”',
1111
IMAGE: '이미지가 μ €μž₯λμ–΄μš”',
1212
REPORT: 'μ‹ κ³ κ°€ μ ‘μˆ˜λμ–΄μš”',
1313
COMMENT: 'λŒ“κΈ€μ΄ μ‚­μ œλμ–΄μš”',
14+
AUTH_EXPIRED: '둜그인 정보가 λ§Œλ£Œλ˜μ—ˆμ–΄μš”',
1415
}
1516

1617
type ToastContextType = {

β€Žsrc/entities/playlist/api/playlist.tsβ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export const deleteMyPagePlaylist = (playlistId: number) => {
3737

3838
// λŒ€ν‘œ ν”Œλ ˆμ΄λ¦¬μŠ€νŠΈ μ„€μ •
3939
export const setPrimaryPlaylist = (playlistId: number) => {
40-
return api.patch<string | null>(`/main/mypage/me/${playlistId}/representative`)
40+
return api.patch<string | null>(`/main/mypage/playlists/me/${playlistId}/representative`)
4141
}
4242

4343
export const getShufflePlaylists = (params: PlaylistParams) => {

β€Žsrc/entities/playlist/model/usePlaylists.tsβ€Ž

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
postPlaylistStart,
1515
} from '@/entities/playlist/api/playlist'
1616
import type { Cursor, PlaylistResponse } from '@/entities/playlist/types/playlist'
17+
import { useAuthStore } from '@/features/auth/store/authStore'
1718

1819
export const useShufflePlaylists = (size: number = 5) => {
1920
return useInfiniteQuery<
@@ -37,6 +38,7 @@ export const useShufflePlaylists = (size: number = 5) => {
3738
}
3839
return undefined
3940
},
41+
enabled: !!useAuthStore.getState().accessToken || !!localStorage.getItem('anonymous_token'),
4042
})
4143
}
4244

β€Žsrc/features/auth/store/authStore.tsβ€Ž

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { create } from 'zustand'
22
import { persist } from 'zustand/middleware'
33

44
import type { AuthState } from '@/features/auth/types/auth'
5+
import type { ProfileResponse } from '@/features/profile/types/profile'
56

67
export const useAuthStore = create<AuthState>()(
78
persist(
@@ -28,6 +29,16 @@ export const useAuthStore = create<AuthState>()(
2829
isLogin: false,
2930
})
3031
},
32+
33+
updateUserInfo: (payload: ProfileResponse) => {
34+
set({
35+
userInfo: {
36+
userId: payload.userId,
37+
username: payload.nickname,
38+
userProfileImageUrl: payload.profileImageUrl,
39+
},
40+
})
41+
},
3142
}),
3243
{
3344
name: 'deulak_auth',

β€Žsrc/features/auth/types/auth.tsβ€Ž

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { ProfileResponse } from '@/features/profile/types/profile'
2+
13
export interface LoginPayload {
24
code: string
35
codeVerifier: string
@@ -19,4 +21,5 @@ export interface AuthState {
1921
isLogin: boolean
2022
setLogin: (data: LoginResponse) => void
2123
setLogout: () => void
24+
updateUserInfo: (data: ProfileResponse) => void
2225
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import type { ProfilePayload, ProfileResponse } from '@/features/profile/types/profile'
2+
import { api } from '@/shared/api/httpClient'
3+
4+
// ν”„λ‘œν•„ μˆ˜μ •
5+
export const patchProfile = (payload: ProfilePayload) => {
6+
const formData = new FormData()
7+
8+
formData.append('nickname', payload.nickname)
9+
if (payload.file instanceof File) {
10+
formData.append('profileImage', payload.file)
11+
}
12+
13+
return api.patch<ProfileResponse>('/main/mypage/playlists/me', formData, {
14+
headers: {
15+
'Content-Type': 'multipart/form-data',
16+
},
17+
})
18+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { useMutation } from '@tanstack/react-query'
2+
3+
import { patchProfile } from '@/features/profile/api/profile'
4+
5+
export const useProfile = () => {
6+
return useMutation({
7+
mutationKey: ['patchProfile'],
8+
mutationFn: patchProfile,
9+
})
10+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export interface ProfilePayload {
2+
nickname: string
3+
file: File | null
4+
profileImage: string | null
5+
}
6+
7+
export interface ProfileResponse {
8+
userId: string
9+
nickname: string
10+
profileImageUrl: string | null
11+
}

β€Žsrc/pages/login/index.tsxβ€Ž

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom'
33

44
import styled from 'styled-components'
55

6+
import { useToast } from '@/app/providers'
67
import { Kakao } from '@/assets/icons'
78
import { LoginBg as LoginBgImg, LoginContent as LoginContentImg } from '@/assets/images'
89
import { generateCodeVerifier, generateCodeChallenge } from '@/features/auth/lib/pkce'
@@ -11,6 +12,7 @@ import { flexColCenter, flexRowCenter } from '@/shared/styles/mixins'
1112

1213
const LoginPage = () => {
1314
const navigate = useNavigate()
15+
const { toast } = useToast()
1416

1517
const onKakaoLoginClick = async () => {
1618
const KAKAO_CLIENT_ID = import.meta.env.VITE_KAKAO_CLIENT_ID
@@ -35,6 +37,13 @@ const LoginPage = () => {
3537
}
3638
}, [])
3739

40+
useEffect(() => {
41+
if (localStorage.getItem('show_expired_toast') === 'true') {
42+
toast('AUTH_EXPIRED')
43+
localStorage.removeItem('show_expired_toast')
44+
}
45+
}, [toast])
46+
3847
return (
3948
<>
4049
<LoginBg />
@@ -45,7 +54,7 @@ const LoginPage = () => {
4554
<Kakao width={20} height={20} />
4655
<span>카카였둜 μ‹œμž‘ν•˜κΈ°</span>
4756
</LoginButton>
48-
<GuestButton type="button" onClick={() => navigate(-1)}>
57+
<GuestButton type="button" onClick={() => navigate('/')}>
4958
λΉ„νšŒμ›μœΌλ‘œ λ‘˜λŸ¬λ³΄κΈ°
5059
</GuestButton>
5160
</CtaContainer>

β€Žsrc/pages/myPage/ui/components/UserProfile.tsxβ€Ž

Lines changed: 112 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -2,115 +2,162 @@ import { useEffect, useRef, useState } from 'react'
22

33
import styled from 'styled-components'
44

5-
import { Profile, Input } from '@shared/ui'
5+
import { Profile, Input, Loading } from '@shared/ui'
66
import type { ProfileUrl } from '@shared/ui/Profile'
77

88
import { Camera } from '@/assets/icons'
99
import { useAuthStore } from '@/features/auth/store/authStore'
10+
import { useProfile } from '@/features/profile/model/useProfile'
1011
import { flexRowCenter } from '@/shared/styles/mixins'
1112

13+
const MAX_FILE_SIZE = 1024 * 1024 * 5 // 5MB
14+
1215
const UserProfile = () => {
13-
const { userInfo } = useAuthStore()
16+
const { userInfo, updateUserInfo } = useAuthStore()
17+
const { mutate, isPending } = useProfile()
1418

1519
const fileInputRef = useRef<HTMLInputElement>(null)
1620

1721
const [isEditMode, setIsEditMode] = useState(false)
22+
const [hasErrorMsg, setHasErrorMsg] = useState('')
23+
1824
const [updatedProfile, setUpdatedProfile] = useState<{
1925
nickname: string
20-
profileImg: ProfileUrl
26+
file: File | null
27+
profileImage: string | null
2128
}>({
2229
nickname: userInfo.username,
23-
profileImg: userInfo?.userProfileImageUrl || null,
30+
profileImage: userInfo?.userProfileImageUrl || null,
31+
file: null,
2432
})
25-
const [isFileError, setIsFileError] = useState(false)
2633

27-
// React μ»΄ν¬λ„ŒνŠΈ 라이프사이클에 맞좰 blob: URL ν•΄μ œ 및 λ©”λͺ¨λ¦¬ λ¦­ λ°©μ§€
28-
useEffect(() => {
29-
return () => {
30-
if (
31-
typeof updatedProfile.profileImg === 'string' &&
32-
updatedProfile.profileImg.startsWith('blob:')
33-
) {
34-
URL.revokeObjectURL(updatedProfile.profileImg)
35-
}
36-
}
37-
}, [updatedProfile.profileImg])
34+
// 화면에 보여쀄 프리뷰 URL
35+
const [previewImage, setPreviewImage] = useState<ProfileUrl>(
36+
userInfo?.userProfileImageUrl || null
37+
)
3838

39+
// ν”„λ‘œν•„ νŽΈμ§‘ λ²„νŠΌ 클릭
3940
const onProfileEditClick = () => {
4041
if (!isEditMode) {
4142
setIsEditMode(true)
4243
return
4344
}
44-
// TODO: ν”„λ‘œν•„ μˆ˜μ • api 연동 & api 성곡 μ‹œ updatedProfile μ΄ˆκΈ°ν™” 둜직 μΆ”κ°€
45+
46+
if (updatedProfile.nickname.length === 0) {
47+
setHasErrorMsg('1자 이상 μž…λ ₯ν•΄μ£Όμ„Έμš”')
48+
return
49+
}
50+
51+
mutate(updatedProfile, {
52+
onSuccess: (response) => {
53+
updateUserInfo(response)
54+
55+
setIsEditMode(false)
56+
setHasErrorMsg('')
57+
setUpdatedProfile({
58+
nickname: response.nickname,
59+
profileImage: response.profileImageUrl,
60+
file: null,
61+
})
62+
setPreviewImage(response.profileImageUrl)
63+
},
64+
})
65+
66+
// μ΄ˆκΈ°ν™”
4567
setIsEditMode(false)
46-
setIsFileError(false)
68+
setHasErrorMsg('')
4769
setUpdatedProfile({
4870
nickname: userInfo.username,
49-
profileImg: userInfo?.userProfileImageUrl || null,
71+
profileImage: userInfo?.userProfileImageUrl || null,
72+
file: null,
5073
})
74+
setPreviewImage(userInfo?.userProfileImageUrl || null)
5175
}
5276

77+
// ν”„λ‘œν•„ 이미지 선택
5378
const onFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
5479
if (!e.target.files || !e.target.files.length) return
5580

5681
const file = e.target.files[0]
57-
const MAX_FILE_SIZE = 1024 * 1024 * 5 // 5MB
58-
5982
if (file.size > MAX_FILE_SIZE) {
60-
setIsFileError(true)
83+
setHasErrorMsg('5MB μ΄ν•˜μ˜ 파일만 μ—…λ‘œλ“œ κ°€λŠ₯ν•΄μš”')
6184
if (fileInputRef.current) {
6285
fileInputRef.current.value = ''
6386
}
64-
setUpdatedProfile((prev) => ({ ...prev, profileImg: userInfo?.userProfileImageUrl || null }))
87+
setUpdatedProfile((prev) => ({
88+
...prev,
89+
profileImage: userInfo?.userProfileImageUrl || null,
90+
file: null,
91+
}))
92+
setPreviewImage(userInfo?.userProfileImageUrl || null)
6593
return
6694
}
6795

68-
setIsFileError(false)
69-
const image = window.URL.createObjectURL(file)
70-
setUpdatedProfile((prev) => ({ ...prev, profileImg: image }))
96+
setHasErrorMsg('')
97+
const blobUrl = URL.createObjectURL(file)
98+
99+
setUpdatedProfile((prev) => ({ ...prev, file, profileImage: null }))
100+
setPreviewImage(blobUrl)
71101
}
72102

103+
// blob URL λ©”λͺ¨λ¦¬ 정리
104+
useEffect(() => {
105+
return () => {
106+
if (previewImage && typeof previewImage === 'string' && previewImage.startsWith('blob:')) {
107+
URL.revokeObjectURL(previewImage)
108+
}
109+
}
110+
}, [previewImage])
111+
73112
return (
74-
<ProfileWrapper>
75-
<ProfileImgContainer>
76-
<Profile
77-
size="L"
78-
profileUrl={
79-
isEditMode ? updatedProfile.profileImg : userInfo?.userProfileImageUrl || null
80-
}
81-
/>
82-
{isEditMode && (
83-
<>
84-
<ProfileImgEditBtn
85-
type="button"
86-
aria-label="ν”„λ‘œν•„ 이미지 μˆ˜μ •"
87-
onClick={() => fileInputRef.current?.click()}
88-
>
89-
<Camera width={14} height={14} />
90-
</ProfileImgEditBtn>
91-
<input type="file" ref={fileInputRef} accept="image/*" onChange={onFileChange} hidden />
92-
</>
113+
<>
114+
{isPending && <Loading isLoading={isPending} />}
115+
<ProfileWrapper>
116+
<ProfileImgContainer>
117+
<Profile size="L" profileUrl={previewImage} />
118+
119+
{isEditMode && (
120+
<>
121+
<ProfileImgEditBtn
122+
type="button"
123+
aria-label="ν”„λ‘œν•„ 이미지 μˆ˜μ •"
124+
onClick={() => fileInputRef.current?.click()}
125+
>
126+
<Camera width={14} height={14} />
127+
</ProfileImgEditBtn>
128+
<input
129+
type="file"
130+
ref={fileInputRef}
131+
accept="image/*"
132+
onChange={onFileChange}
133+
hidden
134+
/>
135+
</>
136+
)}
137+
</ProfileImgContainer>
138+
139+
{hasErrorMsg && <FileErrMsg>{hasErrorMsg}</FileErrMsg>}
140+
141+
{!isEditMode ? (
142+
<ProfileName>{userInfo.username}</ProfileName>
143+
) : (
144+
<Input
145+
type="text"
146+
placeholder="λ‹‰λ„€μž„"
147+
value={updatedProfile.nickname}
148+
maxLength={10}
149+
width="155px"
150+
onChange={(e) =>
151+
setUpdatedProfile({ ...updatedProfile, nickname: e.target.value.trim() })
152+
}
153+
/>
93154
)}
94-
</ProfileImgContainer>
95-
{isFileError && <FileErrMsg>5MB μ΄ν•˜μ˜ 파일만 μ—…λ‘œλ“œ κ°€λŠ₯ν•΄μš”</FileErrMsg>}
96-
{!isEditMode ? (
97-
<ProfileName>{userInfo.username}</ProfileName>
98-
) : (
99-
<Input
100-
type="text"
101-
placeholder="λ‹‰λ„€μž„"
102-
value={updatedProfile.nickname}
103-
maxLength={10}
104-
width="155px"
105-
onChange={(e) =>
106-
setUpdatedProfile({ ...updatedProfile, nickname: e.target.value.trim() })
107-
}
108-
/>
109-
)}
110-
<ProfileEditBtn type="button" onClick={onProfileEditClick}>
111-
{isEditMode ? 'μ €μž₯ν•˜κΈ°' : 'ν”„λ‘œν•„ νŽΈμ§‘'}
112-
</ProfileEditBtn>
113-
</ProfileWrapper>
155+
156+
<ProfileEditBtn type="button" onClick={onProfileEditClick}>
157+
{isEditMode ? 'μ €μž₯ν•˜κΈ°' : 'ν”„λ‘œν•„ νŽΈμ§‘'}
158+
</ProfileEditBtn>
159+
</ProfileWrapper>
160+
</>
114161
)
115162
}
116163

0 commit comments

Comments
Β (0)