Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
d676cdc
๐ŸŽจstyle: ์ปค์Šคํ…€ ํด๋ž˜์Šค ์ถ”๊ฐ€
Insung-Jo Jun 18, 2025
8700381
โœจfeat: PlusIcon ์ปดํฌ๋„ŒํŠธ ๊ตฌํ˜„
Insung-Jo Jun 18, 2025
0644bd4
โœจfeat: ์—”๋“œํฌ์ธํŠธ ์ž‘์„ฑ
Insung-Jo Jun 18, 2025
460e2ef
โœจfeat: ๋งˆ์ดํŽ˜์ด์ง€ ํƒ€์ž… ์ž‘์„ฑ
Insung-Jo Jun 18, 2025
e38aad8
โœจfeat: ๋งˆ์ดํŽ˜์ด์ง€ ์‚ฌ์šฉ์ž ์ •๋ณด ์กฐํšŒ ๋ฐ ์ˆ˜์ • API ํ•จ์ˆ˜ ์ถ”๊ฐ€
Insung-Jo Jun 18, 2025
437dd7a
โœจfeat: ์—”๋“œํฌ์ธํŠธ ์ถ”๊ฐ€
Insung-Jo Jun 18, 2025
392fc77
โœจfeat: ๋งˆ์ดํŽ˜์ด์ง€ ํƒ€์ž… ์ถ”๊ฐ€
Insung-Jo Jun 18, 2025
dab06ee
โœจfeat: ๋งˆ์ดํŽ˜์ด์ง€ ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ API ํ•จ์ˆ˜ ์ถ”๊ฐ€
Insung-Jo Jun 18, 2025
cec5cfc
โœจfeat: ํ”„๋กœํ•„ ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ mutation ํ›… ์ƒ์„ฑ
Insung-Jo Jun 18, 2025
4ae13db
โœจfeat: ํ”„๋กœํ•„ ์—…๋ฐ์ดํŠธ mutation ํ›… ์ƒ์„ฑ
Insung-Jo Jun 18, 2025
110345d
๐Ÿซง modify: PlusIcon ํƒ€์ž… ์ˆ˜์ • ๋ฐ ๋ฐ˜์˜
Insung-Jo Jun 18, 2025
041933b
โœจfeat: WhitePenIcon ์ปดํฌ๋„ŒํŠธ ๊ตฌํ˜„
Insung-Jo Jun 18, 2025
2cec219
โœจfeat: CloseCircleIcon ์ปดํฌ๋„ŒํŠธ ๊ตฌํ˜„
Insung-Jo Jun 18, 2025
666650e
โœจfeat: ๋งˆ์ดํŽ˜์ด์ง€ ์Šคํ‚ค๋งˆ ์ž‘์„ฑ
Insung-Jo Jun 18, 2025
a0b027d
๐ŸŽจstyle: Input ์กฐ๊ฑด๋ถ€ ์Šคํƒ€์ผ ์ถ”๊ฐ€
Insung-Jo Jun 18, 2025
89d13dc
โœจfeat: ์‚ฌ์šฉ์ž ์ •๋ณด ์กฐํšŒ์šฉ useUserQuery ํ›… ์ƒ์„ฑ
Insung-Jo Jun 18, 2025
1cde8db
โœจfeat: ProfileImageUpload ์ปดํฌ๋„ŒํŠธ ๊ตฌํ˜„
Insung-Jo Jun 18, 2025
adb0300
โœจfeat: ProfileEditForm ์ปดํฌ๋„ŒํŠธ ๊ตฌํ˜„
Insung-Jo Jun 18, 2025
34d0251
โœจfeat: ํŽ˜์ด์ง€ ์ ์šฉ
Insung-Jo Jun 18, 2025
118ea68
๐Ÿ›fix: ํ† ํฐ ์•ˆ ๋„˜์–ด์˜ค๋˜ ๋ฌธ์ œ ์ˆ˜์ •
Insung-Jo Jun 18, 2025
296bae0
โ™ป๏ธrefactor: axios ์ธ์Šคํ„ด์Šค ๋„ค์ด๋ฐ์„ authHttpClient๋กœ ๋ช…ํ™•ํ•˜๊ฒŒ ๋ณ€๊ฒฝ
Insung-Jo Jun 18, 2025
5489a48
๐Ÿซงmodify: ์˜คํƒ€ ์ˆ˜์ •
Insung-Jo Jun 18, 2025
76fadb9
๐Ÿ›fix: ์ฝ”๋“œ๋ž˜๋น— ๋ฆฌ๋ทฐ ๋ฐ˜์˜
Insung-Jo Jun 18, 2025
cbdb10e
โœจfeat: ๋น„๋ฐ€๋ฒˆํ˜ธ ํ™•์ธ ํ›… ๊ณตํ†ต์œผ๋กœ ๋ถ„๋ฆฌ ๋ฐ ์ ์šฉ
Insung-Jo Jun 18, 2025
dd14548
โœจfeat: ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ ์—”๋“œ ํฌ์ธํŠธ ์ถ”๊ฐ€
Insung-Jo Jun 18, 2025
d7ab214
โœจfeat: ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ ํƒ€์ž… ์ถ”๊ฐ€
Insung-Jo Jun 18, 2025
dadf4db
โœจfeat: ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ API ์ž‘์„ฑ
Insung-Jo Jun 18, 2025
2b3f632
โœจfeat: ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ Mutation ํ›… ๊ตฌํ˜„
Insung-Jo Jun 18, 2025
a269775
โœจfeat: ๋น„๋ฐ€๋ฒˆํ˜ธ ๊ด€๋ จ ์Šคํ‚ค๋งˆ ์ž‘์„ฑ
Insung-Jo Jun 18, 2025
60f5c41
โœจfeat: ์ƒˆ ๋น„๋ฐ€๋ฒˆํ˜ธ ์ค‘๋ณต ๊ฒ€์ฆ ํ›… ๊ตฌํ˜„
Insung-Jo Jun 18, 2025
232708d
โœจfeat: ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ ์ปดํฌ๋„ŒํŠธ ๊ตฌํ˜„
Insung-Jo Jun 18, 2025
49049ef
โœจfeat: ํŽ˜์ด์ง€ ์ ์šฉ
Insung-Jo Jun 18, 2025
f9c87dc
๐Ÿซงmodify: ์ผ๋ถ€ ๊ตฌ์กฐ ๊ฐœ์„ 
Insung-Jo Jun 18, 2025
254e3f3
๐Ÿซงmodify: ์˜คํƒ€ ์ˆ˜์ •
Insung-Jo Jun 18, 2025
8860060
Merge pull request #78 from CoPlay-FE/feature/mypage-ProfileForm
Insung-Jo Jun 19, 2025
32cba40
Merge pull request #80 from CoPlay-FE/feature/mypage-PasswordChangeForm
Insung-Jo Jun 19, 2025
9f11303
โœจfeat: ์ž„์‹œ ๋ฒ„ํŠผ ์ž‘์„ฑ
Insung-Jo Jun 19, 2025
6957a8b
๐ŸŽจstyle: ๋ถˆํ•„์š”ํ•œ ์Šคํƒ€์ผ ์ œ๊ฑฐ
Insung-Jo Jun 19, 2025
6106654
Merge branch 'develop' into feature/mypage
Insung-Jo Jun 19, 2025
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
12 changes: 9 additions & 3 deletions src/app/features/auth/api/authApi.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
import api from '@/app/shared/lib/axios'
import authHttpClient from '@/app/shared/lib/axios'
import { User as SignupResponse } from '@/app/shared/types/user.type'

import { LoginRequest, LoginResponse, SignupRequest } from '../types/auth.type'
import { AUTH_ENDPOINT } from './authEndpoint'

export const login = async (data: LoginRequest): Promise<LoginResponse> => {
const response = await api.post<LoginResponse>(AUTH_ENDPOINT.LOGIN, data)
const response = await authHttpClient.post<LoginResponse>(
AUTH_ENDPOINT.LOGIN,
data,
)
return response.data
}

export const signup = async (data: SignupRequest): Promise<SignupResponse> => {
const response = await api.post<SignupResponse>(AUTH_ENDPOINT.SIGNUP, data)
const response = await authHttpClient.post<SignupResponse>(
AUTH_ENDPOINT.SIGNUP,
data,
)
return response.data
}
4 changes: 2 additions & 2 deletions src/app/features/auth/components/SignupForm.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
'use client'

import Input from '@components/Input'
import { useConfirmPasswordValidation } from '@hooks/useConfirmPasswordValidation'
import { cn } from '@lib/cn'
import { useState } from 'react'
import { useForm } from 'react-hook-form'

import { useConfirmPasswordValidation } from '../hooks/useConfirmPasswordValidation'
import { useSignupMutation } from '../hooks/useSignupMutation'
import { signupValidation } from '../schemas/signupValidation'
import { SignupFormData } from '../types/auth.type'
Expand All @@ -29,7 +29,7 @@ export default function SignupForm() {

const [isChecked, setIsChecked] = useState(false)
const { mutate: signupMtate, isPending } = useSignupMutation()
const validation = useConfirmPasswordValidation(getValues)
const validation = useConfirmPasswordValidation(() => getValues('password'))

function handleAgree() {
setIsChecked((prev) => !prev)
Expand Down
11 changes: 0 additions & 11 deletions src/app/features/auth/hooks/useConfirmPasswordValidation.ts

This file was deleted.

4 changes: 2 additions & 2 deletions src/app/features/auth/hooks/useLoginMutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ export function useLoginMutation() {
},
onError: (error) => {
if (axios.isAxiosError(error)) {
const severMessage = (
const serverMessage = (
error.response?.data as { message?: string } | undefined
)?.message
const fallback = error.message || '๋กœ๊ทธ์ธ ์‹คํŒจ'
showError(severMessage ?? fallback)
showError(serverMessage ?? fallback)
} else {
showError('์•Œ ์ˆ˜ ์—†๋Š” ์—๋Ÿฌ ๋ฐœ์ƒ')
}
Expand Down
4 changes: 2 additions & 2 deletions src/app/features/auth/hooks/useSignupMutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ export function useSignupMutation() {
},
onError: (error) => {
if (axios.isAxiosError(error)) {
const severMessage = (
const serverMessage = (
error.response?.data as { message?: string } | undefined
)?.message
const fallback = error.message || '๋กœ๊ทธ์ธ ์‹คํŒจ'
showError(severMessage ?? fallback)
showError(serverMessage ?? fallback)
} else {
showError('์•Œ ์ˆ˜ ์—†๋Š” ์—๋Ÿฌ ๋ฐœ์ƒ')
}
Expand Down
43 changes: 43 additions & 0 deletions src/app/features/mypage/api/mypageApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import authHttpClient from '@lib/axios'

import { User as UserDataResponse } from '@/app/shared/types/user.type'

import { PasswordChangeRequest } from '../types/mypage.type'
import {
UpdateProfileRequest,
UploadProfileImageResponse,
} from '../types/mypage.type'
import { MYPAGE_ENDPOINT } from './mypageEndPoint'

export async function loadUser(): Promise<UserDataResponse> {
const response = await authHttpClient.get(MYPAGE_ENDPOINT.USER)
return response.data
}

export async function updateMyProfile(
data: UpdateProfileRequest,
): Promise<UserDataResponse> {
const response = await authHttpClient.put<UserDataResponse>(
MYPAGE_ENDPOINT.USER,
data,
)
return response.data
}

export async function uploadProfileImage(
image: File,
): Promise<UploadProfileImageResponse> {
const formData = new FormData()
formData.append('image', image)
const response = await authHttpClient.post<UploadProfileImageResponse>(
MYPAGE_ENDPOINT.IMAGE,
formData,
)
return response.data
}

export async function changePassword(
data: PasswordChangeRequest,
): Promise<void> {
await authHttpClient.put(MYPAGE_ENDPOINT.CHANGE_PASSWORD, data)
}
5 changes: 5 additions & 0 deletions src/app/features/mypage/api/mypageEndPoint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const MYPAGE_ENDPOINT = {
USER: `/${process.env.NEXT_PUBLIC_TEAM_ID}/users/me`,
IMAGE: `/${process.env.NEXT_PUBLIC_TEAM_ID}/users/me/image`,
CHANGE_PASSWORD: `/${process.env.NEXT_PUBLIC_TEAM_ID}/auth/password`,
}
114 changes: 114 additions & 0 deletions src/app/features/mypage/components/PasswordChangeForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import Input from '@components/Input'
import { useConfirmPasswordValidation } from '@hooks/useConfirmPasswordValidation'
import { cn } from '@lib/cn'
import { useForm } from 'react-hook-form'

import { showSuccess } from '@/app/shared/lib/toast'

import { useChangePasswordMutation } from '../hook/useChangePasswordMutation'
import { useNewPasswordValidation } from '../hook/useNewPasswordValidation'
import { PasswordChangeRequest } from '../types/mypage.type'

interface PasswordChangeFormData extends PasswordChangeRequest {
confirmPassword: string
}

export default function PasswordChangeForm() {
const {
register,
handleSubmit,
trigger,
getValues,
reset,
formState: { errors, isValid },
} = useForm<PasswordChangeFormData>({
mode: 'onBlur',
defaultValues: {
password: '',
newPassword: '',
confirmPassword: '',
},
})

const { mutate: changePassword, isPending } = useChangePasswordMutation()
const newPasswordValidation = useNewPasswordValidation(() =>
getValues('password'),
)
const confirmPasswordValidation = useConfirmPasswordValidation(() =>
getValues('newPassword'),
)

function onSubmit(data: PasswordChangeFormData) {
changePassword(
{
password: data.password,
newPassword: data.newPassword,
},
{
onSuccess: () => {
reset()
showSuccess('๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!')
},
},
)
}

return (
<div className="BG-white flex h-auto w-full max-w-672 flex-col gap-24 rounded-8 p-24">
<h2 className="text-2xl font-bold">๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ</h2>
<form
onSubmit={handleSubmit(onSubmit)}
className="flex flex-col justify-between gap-16"
>
<Input
labelName="ํ˜„์žฌ ๋น„๋ฐ€๋ฒˆํ˜ธ"
type="password"
placeholder="๋น„๋ฐ€๋ฒˆํ˜ธ ์ž…๋ ฅ"
autoComplete="current-password"
{...register('password')}
/>

<Input
labelName="์ƒˆ ๋น„๋ฐ€๋ฒˆํ˜ธ"
type="password"
placeholder="์ƒˆ ๋น„๋ฐ€๋ฒˆํ˜ธ ์ž…๋ ฅ"
autoComplete="new-password"
{...register('newPassword', {
...newPasswordValidation,
onBlur: () => {
trigger('confirmPassword')
},
})}
hasError={!!errors.newPassword}
errorMessage={errors.newPassword?.message}
/>

<Input
labelName="์ƒˆ ๋น„๋ฐ€๋ฒˆํ˜ธ ํ™•์ธ"
type="password"
placeholder="์ƒˆ ๋น„๋ฐ€๋ฒˆํ˜ธ ์ž…๋ ฅ"
autoComplete="new-password"
{...register('confirmPassword', {
...confirmPasswordValidation,
onBlur: () => {
trigger('newPassword')
},
})}
hasError={!!errors.confirmPassword}
errorMessage={errors.confirmPassword?.message}
/>

<button
type="submit"
className={cn(
'mt-8 h-50 w-full rounded-8 text-lg font-medium text-white',
isValid && !isPending ? 'BG-blue' : 'BG-blue-disabled',
)}
disabled={!isValid || isPending}
>
{isPending ? '๋ณ€๊ฒฝ ์ค‘..' : '๋ณ€๊ฒฝ'}
</button>
</form>
</div>
)
}
133 changes: 133 additions & 0 deletions src/app/features/mypage/components/ProfileEditForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
'use client'

import Input from '@components/Input'
import { showError, showSuccess } from '@lib/toast'
import { isAxiosError } from 'axios'
import { useRouter } from 'next/navigation'
import { useEffect, useState } from 'react'
import { Controller, useForm } from 'react-hook-form'

import { useUpdateMyProfileMutation } from '../hook/useUpdateMyProfileMutation'
import { useUploadProfileImageMutation } from '../hook/useUploadProfileImageMutation'
import { useUserQuery } from '../hook/useUserQurey'
import { mypageValidation } from '../schemas/mypageValidation'
import ProfileImageUpload from './ProfileImageUpload'

interface ProfileFormData {
profileImageUrl: string | null
nickname: string
email: string
}

export default function ProfileEditForm() {
const { data: user } = useUserQuery() // get์œผ๋กœ ์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐ ์ตœ์‹ ํ™”
const router = useRouter() // useClientQuery๋ฅผ ์‚ฌ์šฉ ํ•˜์—ฌ ํ•ด๋‹น ๋ถ€๋ถ„์—๋งŒ ๋ Œ๋”๋ง์„ ์ง„ํ–‰ํ•˜๋ ค ํ–ˆ์œผ๋‚˜ ๋‹ค๋ฅธ ๋ถ€๋ถ„๋„ ์—ฐ๋™๋˜๋Š” ๋ถ€๋ถ„์ด ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ๋ผ์šฐํ„ฐ ์‚ฌ์šฉ
const {
control,
register,
handleSubmit,
reset,
formState: { errors },
} = useForm<ProfileFormData>({
mode: 'onChange',
defaultValues: {
profileImageUrl: user?.profileImageUrl,
nickname: user?.nickname,
email: user?.email,
},
})

const [profileImageFile, setProfileImageFile] = useState<File | null>(null)
const { mutateAsync: uploadImage } = useUploadProfileImageMutation()
const { mutateAsync: updateProfile } = useUpdateMyProfileMutation()

// ์œ ์ € ์ •๋ณด๊ฐ€ ๋น„๋™๊ธฐ์ ์œผ๋กœ ๋„˜์–ด์˜ค๊ธฐ ๋•Œ๋ฌธ์— ์œ ์ €๊ฐ€ ๋กœ๋”ฉ๋œ ์‹œ์ ์—์„œ RHF์„ ์ดˆ๊ธฐํ™” ํ•˜๊ธฐ ์œ„ํ•จ (SSR ๋„์ž… ์‹œ ๋ณ€๊ฒฝ ์˜ˆ์ •)
useEffect(() => {
if (user) {
reset({
profileImageUrl: user.profileImageUrl ?? null,
nickname: user.nickname ?? '',
email: user.email ?? '',
})
}
}, [user, reset])

async function onSubmit(data: ProfileFormData) {
try {
// ํ˜„์žฌ ์ด๋ฏธ์ง€ URL์„ ์ดˆ๊ธฐ๊ฐ’์œผ๋กœ ์„ค์ • (๋ณ€๊ฒฝ์ด ์—†์„ ์ˆ˜๋„ ์žˆ๊ธฐ ๋•Œ๋ฌธ)
let imageUrl = data.profileImageUrl

// ์ƒˆ ์ด๋ฏธ์ง€๋ฅผ ์—…๋กœ๋“œํ•œ ๊ฒฝ์šฐ โ†’ ์„œ๋ฒ„์— ์—…๋กœ๋“œ ์š”์ฒญ (POST)
if (profileImageFile) {
const { profileImageUrl } = await uploadImage(profileImageFile)
imageUrl = profileImageUrl
}

// ๋‹‰๋„ค์ž„๊ณผ ์ด๋ฏธ์ง€ URL์„ ํฌํ•จํ•œ ์‚ฌ์šฉ์ž ํ”„๋กœํ•„ ์ •๋ณด ์ƒ์„ฑ
const submitData = {
nickname: data.nickname,
profileImageUrl: imageUrl,
}

// ์„œ๋ฒ„์— ํ”„๋กœํ•„ ์ •๋ณด ์ˆ˜์ • ์š”์ฒญ (PUT)
await updateProfile(submitData)

// ์‚ฌ์šฉ์ž์—๊ฒŒ ์„ฑ๊ณต ์•Œ๋ฆผ + ์ปดํฌ๋„ŒํŠธ ์ตœ์‹ ํ™”
showSuccess('ํ”„๋กœํ•„ ๋ณ€๊ฒฝ์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.')
router.refresh()
} catch (error) {
if (isAxiosError(error)) {
// ์„œ๋ฒ„ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ์šฐ์„  ์ฒ˜๋ฆฌ, ์—†์œผ๋ฉด ๊ธฐ๋ณธ ๋ฉ”์‹œ์ง€
const serverMessage = (
error.response?.data as { message?: string } | undefined
)?.message
const fallback = error.message || 'ํ”„๋กœํ•„ ๋ณ€๊ฒฝ์— ์‹คํŒจํ•˜์˜€์Šต๋‹ˆ๋‹ค.'
showError(serverMessage ?? fallback)
} else {
showError('์•Œ ์ˆ˜ ์—†๋Š” ์—๋Ÿฌ ๋ฐœ์ƒ')
}
}
}
Comment on lines +55 to +90
Copy link

Choose a reason for hiding this comment

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

๐Ÿ› ๏ธ Refactor suggestion

ํŒŒ์ผ ์—…๋กœ๋“œ ๋ณด์•ˆ ๊ฒ€์ฆ ํ•„์š”

ํŒŒ์ผ ์—…๋กœ๋“œ ๋กœ์ง์ด ๊ตฌํ˜„๋˜์–ด ์žˆ์ง€๋งŒ, ํด๋ผ์ด์–ธํŠธ ์ธก ํŒŒ์ผ ํƒ€์ž… ๋ฐ ํฌ๊ธฐ ๊ฒ€์ฆ์ด ์—†์Šต๋‹ˆ๋‹ค.

๋‹ค์Œ๊ณผ ๊ฐ™์€ ํด๋ผ์ด์–ธํŠธ ์ธก ๊ฒ€์ฆ์„ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค:

      // ์ƒˆ ์ด๋ฏธ์ง€๋ฅผ ์—…๋กœ๋“œํ•œ ๊ฒฝ์šฐ โ†’ ์„œ๋ฒ„์— ์—…๋กœ๋“œ ์š”์ฒญ (POST)
      if (profileImageFile) {
+        // ํŒŒ์ผ ํƒ€์ž… ๊ฒ€์ฆ
+        const allowedTypes = ['image/jpeg', 'image/png', 'image/webp']
+        if (!allowedTypes.includes(profileImageFile.type)) {
+          showError('์ง€์›๋˜์ง€ ์•Š๋Š” ํŒŒ์ผ ํ˜•์‹์ž…๋‹ˆ๋‹ค. (JPEG, PNG, WebP๋งŒ ๊ฐ€๋Šฅ)')
+          return
+        }
+        
+        // ํŒŒ์ผ ํฌ๊ธฐ ๊ฒ€์ฆ (์˜ˆ: 5MB)
+        const maxSize = 5 * 1024 * 1024
+        if (profileImageFile.size > maxSize) {
+          showError('ํŒŒ์ผ ํฌ๊ธฐ๋Š” 5MB๋ฅผ ์ดˆ๊ณผํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.')
+          return
+        }
+        
         const { profileImageUrl } = await uploadImage(profileImageFile)
         imageUrl = profileImageUrl
       }
๐Ÿ“ Committable suggestion

โ€ผ๏ธ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async function onSubmit(data: ProfileFormData) {
try {
// ํ˜„์žฌ ์ด๋ฏธ์ง€ URL์„ ์ดˆ๊ธฐ๊ฐ’์œผ๋กœ ์„ค์ • (๋ณ€๊ฒฝ์ด ์—†์„ ์ˆ˜๋„ ์žˆ๊ธฐ ๋•Œ๋ฌธ)
let imageUrl = data.profileImageUrl
// ์ƒˆ ์ด๋ฏธ์ง€๋ฅผ ์—…๋กœ๋“œํ•œ ๊ฒฝ์šฐ โ†’ ์„œ๋ฒ„์— ์—…๋กœ๋“œ ์š”์ฒญ (POST)
if (profileImageFile) {
const { profileImageUrl } = await uploadImage(profileImageFile)
imageUrl = profileImageUrl
}
// ๋‹‰๋„ค์ž„๊ณผ ์ด๋ฏธ์ง€ URL์„ ํฌํ•จํ•œ ์‚ฌ์šฉ์ž ํ”„๋กœํ•„ ์ •๋ณด ์ƒ์„ฑ
const submitData = {
nickname: data.nickname,
profileImageUrl: imageUrl,
}
// ์„œ๋ฒ„์— ํ”„๋กœํ•„ ์ •๋ณด ์ˆ˜์ • ์š”์ฒญ (PUT)
await updateProfile(submitData)
// ์‚ฌ์šฉ์ž์—๊ฒŒ ์„ฑ๊ณต ์•Œ๋ฆผ + ์ปดํฌ๋„ŒํŠธ ์ตœ์‹ ํ™”
showSuccess('ํ”„๋กœํ•„ ๋ณ€๊ฒฝ์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.')
router.refresh()
} catch (error) {
if (isAxiosError(error)) {
// ์„œ๋ฒ„ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ์šฐ์„  ์ฒ˜๋ฆฌ, ์—†์œผ๋ฉด ๊ธฐ๋ณธ ๋ฉ”์‹œ์ง€
const serverMessage = (
error.response?.data as { message?: string } | undefined
)?.message
const fallback = error.message || 'ํ”„๋กœํ•„ ๋ณ€๊ฒฝ์— ์‹คํŒจํ•˜์˜€์Šต๋‹ˆ๋‹ค.'
showError(serverMessage ?? fallback)
} else {
showError('์•Œ ์ˆ˜ ์—†๋Š” ์—๋Ÿฌ ๋ฐœ์ƒ')
}
}
}
async function onSubmit(data: ProfileFormData) {
try {
// ํ˜„์žฌ ์ด๋ฏธ์ง€ URL์„ ์ดˆ๊ธฐ๊ฐ’์œผ๋กœ ์„ค์ • (๋ณ€๊ฒฝ์ด ์—†์„ ์ˆ˜๋„ ์žˆ๊ธฐ ๋•Œ๋ฌธ)
let imageUrl = data.profileImageUrl
// ์ƒˆ ์ด๋ฏธ์ง€๋ฅผ ์—…๋กœ๋“œํ•œ ๊ฒฝ์šฐ โ†’ ์„œ๋ฒ„์— ์—…๋กœ๋“œ ์š”์ฒญ (POST)
if (profileImageFile) {
// ํŒŒ์ผ ํƒ€์ž… ๊ฒ€์ฆ
const allowedTypes = ['image/jpeg', 'image/png', 'image/webp']
if (!allowedTypes.includes(profileImageFile.type)) {
showError('์ง€์›๋˜์ง€ ์•Š๋Š” ํŒŒ์ผ ํ˜•์‹์ž…๋‹ˆ๋‹ค. (JPEG, PNG, WebP๋งŒ ๊ฐ€๋Šฅ)')
return
}
// ํŒŒ์ผ ํฌ๊ธฐ ๊ฒ€์ฆ (์˜ˆ: 5MB)
const maxSize = 5 * 1024 * 1024
if (profileImageFile.size > maxSize) {
showError('ํŒŒ์ผ ํฌ๊ธฐ๋Š” 5MB๋ฅผ ์ดˆ๊ณผํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.')
return
}
const { profileImageUrl } = await uploadImage(profileImageFile)
imageUrl = profileImageUrl
}
// ๋‹‰๋„ค์ž„๊ณผ ์ด๋ฏธ์ง€ URL์„ ํฌํ•จํ•œ ์‚ฌ์šฉ์ž ํ”„๋กœํ•„ ์ •๋ณด ์ƒ์„ฑ
const submitData = {
nickname: data.nickname,
profileImageUrl: imageUrl,
}
// ์„œ๋ฒ„์— ํ”„๋กœํ•„ ์ •๋ณด ์ˆ˜์ • ์š”์ฒญ (PUT)
await updateProfile(submitData)
// ์‚ฌ์šฉ์ž์—๊ฒŒ ์„ฑ๊ณต ์•Œ๋ฆผ + ์ปดํฌ๋„ŒํŠธ ์ตœ์‹ ํ™”
showSuccess('ํ”„๋กœํ•„ ๋ณ€๊ฒฝ์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.')
router.refresh()
} catch (error) {
if (isAxiosError(error)) {
// ์„œ๋ฒ„ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ์šฐ์„  ์ฒ˜๋ฆฌ, ์—†์œผ๋ฉด ๊ธฐ๋ณธ ๋ฉ”์‹œ์ง€
const serverMessage = (
error.response?.data as { message?: string } | undefined
)?.message
const fallback = error.message || 'ํ”„๋กœํ•„ ๋ณ€๊ฒฝ์— ์‹คํŒจํ•˜์˜€์Šต๋‹ˆ๋‹ค.'
showError(serverMessage ?? fallback)
} else {
showError('์•Œ ์ˆ˜ ์—†๋Š” ์—๋Ÿฌ ๋ฐœ์ƒ')
}
}
}
๐Ÿค– Prompt for AI Agents
In src/app/features/mypage/components/ProfileEditForm.tsx between lines 55 and
90, the onSubmit function uploads a profile image without validating the file
type or size on the client side. To fix this, add checks before uploading to
ensure the file is of an allowed type (e.g., image/jpeg, image/png) and within a
size limit (e.g., under 5MB). If the file fails validation, prevent the upload
and show an appropriate error message to the user.


return (
<form
onSubmit={handleSubmit(onSubmit)}
className="BG-white flex h-auto w-full max-w-672 flex-col gap-24 rounded-8 p-24 font-medium"
>
<h2 className="text-2xl font-bold">ํ”„๋กœํ•„</h2>

<div className="flex justify-between gap-42 tablet:flex-col">
<Controller
name="profileImageUrl"
control={control}
render={({ field: { value, onChange } }) => (
<ProfileImageUpload
value={value} // ๋ฏธ๋ฆฌ๋ณด๊ธฐ์šฉ ์ด๋ฏธ์ง€ URL
onChange={onChange} // form ์ƒํƒœ(profileImageUrl) ์—…๋ฐ์ดํŠธ
onFileChange={(file) => setProfileImageFile(file)} // ์„œ๋ฒ„ ์ „์†ก์šฉ ํŒŒ์ผ ์ €์žฅ
/>
)}
/>

<div className="flex flex-grow flex-col gap-16">
<Input labelName="์ด๋ฉ”์ผ" {...register('email')} readOnly />
<Input
labelName="๋‹‰๋„ค์ž„"
type="text"
placeholder="๋‹‰๋„ค์ž„์„ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”"
autoComplete="off"
{...register('nickname', mypageValidation.nickname)}
hasError={!!errors.nickname}
errorMessage={errors.nickname?.message}
/>
<button
type="submit"
className="BG-blue h-50 w-full rounded-8 text-white"
>
์ €์žฅ
</button>
</div>
</div>
</form>
)
}
Loading