Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
211bdb6
feat: 이메일, 닉네임 중복검사 api 함수 제작
Chiman2937 Dec 12, 2025
e2f077c
remove: userService에서 회원탈퇴 api 삭제
Chiman2937 Dec 12, 2025
9b2b305
Merge branch 'main' of https://github.com/WeGo-Together/WeGo_FrontEnd…
Chiman2937 Dec 13, 2025
76e9f79
feat: ImageWithFallback 컴포넌트 제작
Chiman2937 Dec 13, 2025
f0b6908
fix: useGetUser에서 api response의 null 값을 ''으로 교체하도록 수정
Chiman2937 Dec 13, 2025
0aa8047
fix: profile edit modal의 field barrel export 추가
Chiman2937 Dec 13, 2025
0652ddd
fix: next config에 wego s3 버킷 경로 추가
Chiman2937 Dec 13, 2025
6adfb79
feat: user patch api 3종 연결
Chiman2937 Dec 13, 2025
cea998b
feat: profile edit modal onsubmit 동작 설명 주석 추가
Chiman2937 Dec 13, 2025
3278739
fix: ImageWithFallback 컴포넌트에서 unoptimized 대신 e.preventDefault() 적용
Chiman2937 Dec 14, 2025
7770581
fix: userService - updateMyNotification 함수 오타 수정
Chiman2937 Dec 14, 2025
97b6691
feat: follow, unfollow api 연결
Chiman2937 Dec 14, 2025
3ed8765
fix: query params 사용 api 구문 수정(axios params 옵션 사용)
Chiman2937 Dec 14, 2025
4c285c6
fix: userService 함수/타입 네이밍 수정
Chiman2937 Dec 14, 2025
7034542
Merge branch 'main' of https://github.com/WeGo-Together/WeGo_FrontEnd…
Chiman2937 Dec 14, 2025
5210af3
fix: 프로필페이지 테스트코드 수정
Chiman2937 Dec 14, 2025
b917018
fix: ProfileEditModal - onSubmit 구문 Promise.all 방식으로 수정
Chiman2937 Dec 14, 2025
d84d2b5
fix: 프로필 업데이트를 Promise 체이닝 방식에서 try catch 방식으로 수정
Chiman2937 Dec 14, 2025
6439f28
fix: userService 변수 네이밍 정리
Chiman2937 Dec 14, 2025
cdcd943
fix: 팔로우/언팔로우 api return type string으로 지정
Chiman2937 Dec 14, 2025
87a91a7
fix: payload type suffix를 payloads 로 수정
Chiman2937 Dec 14, 2025
12200b3
Merge branch 'main' into chiyoung-fix/mypage-edit
claudia99503 Dec 14, 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
6 changes: 6 additions & 0 deletions next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ const nextConfig: NextConfig = {
protocol: 'https',
hostname: 'plus.unsplash.com',
},
{
protocol: 'https',
hostname: 'we-go-bucket.s3.ap-northeast-2.amazonaws.com',
port: '',
pathname: '/**',
},
],
//imagesSizes, deviceSizes는 기본 설정
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
Expand Down
48 changes: 30 additions & 18 deletions src/api/service/user-service/index.ts
Original file line number Diff line number Diff line change
@@ -1,46 +1,58 @@
import { api } from '@/api/core';
import {
Availability,
FollowParams,
GetEmailAvailabilityParams,
GetNickNameAvailabilityParams,
GetUserParams,
UpdateMePayload,
UpdateMyImagePayload,
UpdateMyNotiParams,
UpdateMyProfileImagePayload,
User,
} from '@/types/service/user';

export const userServiceRemote = () => ({
// 2. 프로필 편집
// 1. 사용자 팔로우
followUser: async (payload: FollowParams) => {
return api.post<void>(`/follows/${payload.followNickname}`);
},

// 2. 유저 프로필 변경
updateMe: async (payload: UpdateMePayload) => {
return api.patch<User>('/users', payload);
return api.patch<User>('/users/profile', payload);
},

// 3. 프로필 이미지 편집
updateMyImage: async (payload: UpdateMyImagePayload) => {
return api.patch<User>(`/users/profile-image`, payload);
// 3. 프로필 이미지 변경
updateMyProfileImage: async (payload: UpdateMyProfileImagePayload) => {
const formData = new FormData();
formData.append('file', payload.file);
return api.patch<User>(`/users/profile-image`, formData);
},

// 4. 알림 설정 변경
updatMyNotification: async (payload: UpdateMyNotiParams) => {
return api.patch<User>(`/users/notification/${payload.isNotificationEnabled}`);
updatMyNotification: async (params: UpdateMyNotiParams) => {
return api.patch<User>(
`/users/notification?isNotificationEnabled=${params.isNotificationEnabled}`,
);
},
Comment on lines 35 to 40
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find and display the target file and relevant context
cat -n src/api/service/user-service/index.ts | head -50

Repository: WeGo-Together/WeGo_FrontEnd

Length of output: 1850


🏁 Script executed:

# Search for all usages of the function name (both correct and typo variants)
rg -n 'updatMyNotification|updateMyNotification' --type=ts --type=tsx -C2

Repository: WeGo-Together/WeGo_FrontEnd

Length of output: 98


🏁 Script executed:

# Search for all usages of updatMyNotification (the typo version)
rg -n 'updatMyNotification' -C2

Repository: WeGo-Together/WeGo_FrontEnd

Length of output: 1011


🏁 Script executed:

# Also search for updateMyNotification to see if it's used anywhere
rg -n 'updateMyNotification' -C2

Repository: WeGo-Together/WeGo_FrontEnd

Length of output: 53


🏁 Script executed:

# Check the api core to understand how axios is configured
cat -n src/api/core.ts 2>/dev/null || cat -n src/api/core/index.ts 2>/dev/null || find src/api -name "*core*" -type f

Repository: WeGo-Together/WeGo_FrontEnd

Length of output: 3597


Fix function name typo and use proper query parameter handling.

Two issues identified:

  1. Function name has a typo: "updatMyNotification" should be "updateMyNotification"
  2. Query parameters should use axios config instead of string interpolation for consistency and best practices

This change affects at least one call site in src/hooks/use-user/use-user-notification/index.ts (line 10).

Apply this diff:

-  // 4. 알림 설정 변경
-  updatMyNotification: async (params: UpdateMyNotiParams) => {
-    return api.patch<User>(
-      `/users/notification?isNotificationEnabled=${params.isNotificationEnabled}`,
-    );
+  // 4. 알림 설정 변경
+  updateMyNotification: async (params: UpdateMyNotiParams) => {
+    return api.patch<User>('/users/notification', null, {
+      params: { isNotificationEnabled: params.isNotificationEnabled },
+    });
   },

Update the call site in src/hooks/use-user/use-user-notification/index.ts (line 10) to use the corrected function name.

📝 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
// 4. 알림 설정 변경
updatMyNotification: async (payload: UpdateMyNotiParams) => {
return api.patch<User>(`/users/notification/${payload.isNotificationEnabled}`);
updatMyNotification: async (params: UpdateMyNotiParams) => {
return api.patch<User>(
`/users/notification?isNotificationEnabled=${params.isNotificationEnabled}`,
);
},
// 4. 알림 설정 변경
updateMyNotification: async (params: UpdateMyNotiParams) => {
return api.patch<User>('/users/notification', null, {
params: { isNotificationEnabled: params.isNotificationEnabled },
});
},
🤖 Prompt for AI Agents
In src/api/service/user-service/index.ts around lines 32 to 37, rename the
function from "updatMyNotification" to "updateMyNotification" and change the
request to pass query parameters via axios config (use
api.patch<User>('/users/notification', undefined, { params: {
isNotificationEnabled: params.isNotificationEnabled } }) ) instead of string
interpolation; also update the call site in
src/hooks/use-user/use-user-notification/index.ts (around line 10) to call the
new updateMyNotification function name.


// 5. 사용자 단건 조회
// 5. 유저 프로필 조회
getUser: async (payload: GetUserParams) => {
return api.get<User>(`/users/${payload.userId}`);
},

// 1. 사용자 팔로우
followUser: async (payload: FollowParams) => {
return api.post<void>(`/follows/${payload.followNickname}`);
// 6. 닉네임 중복 검사
getNicknameAvailability: async (params: GetNickNameAvailabilityParams) => {
return api.get<Availability>(`/users/nickname/availability?nickname=${params.nickName}`);
},
Comment on lines 47 to 52
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the file structure and the endpoint in question
cat -n src/api/service/user-service/index.ts | head -60

Repository: WeGo-Together/WeGo_FrontEnd

Length of output: 2141


🏁 Script executed:

# Search for API endpoint definitions or tests that might show the correct parameter name
rg "nickname.*availability" -i -A 3 -B 3

Repository: WeGo-Together/WeGo_FrontEnd

Length of output: 1298


🏁 Script executed:

# Look for similar endpoints to understand the pattern used in the codebase
rg "api\.get.*params:" -A 2 -B 2

Repository: WeGo-Together/WeGo_FrontEnd

Length of output: 53


🏁 Script executed:

# Check if there's any API documentation or backend endpoint definition
find . -type f \( -name "*api*" -o -name "*spec*" -o -name "*swagger*" -o -name "*openapi*" \) | grep -E "\.(ts|js|md|json|yaml|yml)$" | head -20

Repository: WeGo-Together/WeGo_FrontEnd

Length of output: 53


🏁 Script executed:

# Look for tests related to this endpoint
rg "getNicknameAvailability" -A 5 -B 2

Repository: WeGo-Together/WeGo_FrontEnd

Length of output: 661


🏁 Script executed:

# Check the axios configuration to understand default behavior with params
rg "api\.get|axios.*get" -A 2 | head -40

Repository: WeGo-Together/WeGo_FrontEnd

Length of output: 1314


🏁 Script executed:

# Look for test files or actual usage of getNicknameAvailability
rg "getNicknameAvailability" -A 5 -B 2

Repository: WeGo-Together/WeGo_FrontEnd

Length of output: 661


🏁 Script executed:

# Check the axios/api core configuration
cat -n src/api/core/index.ts 2>/dev/null || find src/api/core -type f -name "*.ts" | head -5

Repository: WeGo-Together/WeGo_FrontEnd

Length of output: 3597


🏁 Script executed:

# Look for backend API documentation or OpenAPI spec
find . -type f \( -name "*.md" -o -name "*.json" -o -name "*.yaml" -o -name "*.yml" \) | xargs grep -l "nickname.*availability" 2>/dev/null | head -5

Repository: WeGo-Together/WeGo_FrontEnd

Length of output: 53


🏁 Script executed:

# Check if there's environment/config that might specify API endpoint details
find . -type f \( -name ".env*" -o -name "config*" -o -name "*api*config*" \) | head -10

Repository: WeGo-Together/WeGo_FrontEnd

Length of output: 84


🏁 Script executed:

# Look for any comments or documentation about the nickname endpoint
rg "nickname" -B 2 -A 2 | grep -E "(api|endpoint|param|query)" | head -20

Repository: WeGo-Together/WeGo_FrontEnd

Length of output: 709


🏁 Script executed:

# Look for any integration tests or E2E tests that use this endpoint
find . -type f -name "*.test.*" -o -name "*.spec.*" | xargs grep -l "getNicknameAvailability\|nickname.*availability" 2>/dev/null

Repository: WeGo-Together/WeGo_FrontEnd

Length of output: 55


🏁 Script executed:

# Check if there's a backend repository reference or API spec
cat README.md 2>/dev/null | grep -i "api\|backend\|backend-repo" | head -10

Repository: WeGo-Together/WeGo_FrontEnd

Length of output: 410


🏁 Script executed:

# Search for actual usage of these functions to see if they work with special characters
rg "getNicknameAvailability|getEmailAvailability" -A 3 -B 3 | head -30

Repository: WeGo-Together/WeGo_FrontEnd

Length of output: 909


Missing URL encoding for query parameters.

The nickname query parameter is not URL-encoded. Nicknames containing spaces or special characters (e.g., "John Doe", "user@123") will cause malformed URLs and request failures.

The api.get() function supports passing a config object as the second parameter, which allows axios to properly handle and encode query parameters. Apply this fix:

   // 6. 닉네임 중복 검사
   getNicknameAvailability: async (params: GetNickNameAvailabilityParams) => {
-    return api.get<Availability>(`/users/nickname/availability?nickname=${params.nickName}`);
+    return api.get<Availability>('/users/nickname/availability', {
+      params: { nickname: params.nickName },
+    });
   },

Note: The same issue affects the getEmailAvailability endpoint (line 51) and updatMyNotification endpoint (line 35). Consider applying the same fix pattern to those endpoints as well.

📝 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
// 6. 닉네임 중복 검사
getNicknameAvailability: async (params: GetNickNameAvailabilityParams) => {
return api.get<Availability>(`/users/nickname/availability?nickname=${params.nickName}`);
},
// 6. 닉네임 중복 검사
getNicknameAvailability: async (params: GetNickNameAvailabilityParams) => {
return api.get<Availability>('/users/nickname/availability', {
params: { nickname: params.nickName },
});
},
🤖 Prompt for AI Agents
In src/api/service/user-service/index.ts around lines 44 to 47, the nickname
query parameter is concatenated into the URL without URL-encoding; change the
call to pass query parameters via the api.get second-argument config (use {
params: { nickname: params.nickName } }) so axios encodes them properly; apply
the same pattern to getEmailAvailability (around line 51) and
updatMyNotification (around line 35) to avoid malformed URLs for emails,
nicknames or other special-character values.


// 7. 이메일 중복 검사
getEmailAvailability: async (params: GetEmailAvailabilityParams) => {
return api.get<Availability>(`/users/email/availability?email=${params.email}`);
},

// 6. 사용자 언팔로우
// 8. 사용자 언팔로우
unfollowUser: async (payload: FollowParams) => {
return api.delete<void>(`/follows/${payload.followNickname}`);
},

// 7. 회원탈퇴
deleteMe: async () => api.delete<User>(`/users`),

// 8. 사용자 프로필 이미지 변경
});
1 change: 0 additions & 1 deletion src/app/(user)/mypage/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import { useGetUser } from '@/hooks/use-user';
const MyPage = () => {
// const [userId, setUserId] = useState(0);
const [userId, setUserId] = useState(0);
// 여기서 user 정보를 확인해서 undefined이면 로그인페이지로 리다이렉트

const { data: user } = useGetUser({ userId }, { enabled: !!userId });

Expand Down
12 changes: 6 additions & 6 deletions src/components/pages/user/mypage/mypage-setting/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
'use client';
import { useState } from 'react';

import { useUpdateMyNotification } from '@/hooks/use-user/use-user-notification';
import { User } from '@/types/service/user';

import { MyPageActionButton, MyPageToggleButton } from '../mypage-setting-button';
Expand All @@ -10,13 +9,14 @@ interface Props {
}

export const MyPageSetting = ({ user }: Props) => {
console.log(user);
// useState 로직은 추후 삭제 예정
const [isOn, setIsOn] = useState(false);
const { mutate } = useUpdateMyNotification();

return (
<section className='bg-mono-white flex flex-col gap-3 px-3 py-6'>
<MyPageToggleButton value={isOn} onClick={() => setIsOn((prev) => !prev)}>
<MyPageToggleButton
value={user.isNotificationEnabled}
onClick={() => mutate({ isNotificationEnabled: !user.isNotificationEnabled })}
>
알림 받기
</MyPageToggleButton>
<MyPageActionButton onClick={() => console.log('로그아웃')}>로그아웃</MyPageActionButton>
Expand Down
11 changes: 8 additions & 3 deletions src/components/pages/user/profile/profile-card/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import Image from 'next/image';

import { ImageWithFallback } from '@/components/ui';
import { User } from '@/types/service/user';

interface Props {
Expand All @@ -11,7 +10,13 @@ export const ProfileCard = ({ user }: Props) => {
return (
<div className='flex-col-center mb-6'>
<div className='relative mb-3 size-24 overflow-hidden rounded-full'>
<Image className='object-cover' alt='프로필 이미지' fill src={profileImage} />
<ImageWithFallback
className='object-cover'
alt='프로필 이미지'
fill
loading='eager'
src={profileImage}
/>
</div>
<h1 className='text-text-xl-bold text-gray-900'>{nickName}</h1>
<p className='text-text-sm-medium text-gray-600'>{profileMessage}</p>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
import Image from 'next/image';

import { AnyFieldApi } from '@tanstack/react-form';

import { Icon } from '@/components/icon';
import { ImageInput, ImageInputProps } from '@/components/ui';
import { ImageInput, ImageInputProps, ImageWithFallback } from '@/components/ui';
import { cn } from '@/lib/utils';

type ImageUploadPropsWithoutChildren = Omit<ImageInputProps, 'children'>;

interface Props extends ImageUploadPropsWithoutChildren {
field: AnyFieldApi;
}
const ImageField = ({ field, initialImages }: Props) => {
export const ImageField = ({ field, initialImages }: Props) => {
return (
<div className='flex-center py-6'>
<ImageInput
Expand All @@ -23,34 +21,35 @@ const ImageField = ({ field, initialImages }: Props) => {
value={field.state.value}
onChange={field.handleChange}
>
{(images, _onRemoveImageClick, onFileSelectClick) => (
<>
{Object.entries(images).map(([url, _file]) => (
<div key={url} className='relative aspect-square size-24'>
<Image
className='rounded-full border-1 border-[rgba(0,0,0,0.04)]'
alt='프로필 이미지'
fill
src={url}
/>
<button
className={cn(
'flex-center absolute -right-1.75 bottom-0 size-8 cursor-pointer rounded-full border-1 border-gray-300 bg-gray-100',
'hover:scale-110 hover:bg-gray-200',
'transition-all duration-300',
)}
type='button'
onClick={onFileSelectClick}
>
<Icon id='edit' className='size-5 text-gray-600' />
</button>
</div>
))}
</>
)}
{(images, _onRemoveImageClick, onFileSelectClick) => {
const nextImages = Object.keys(images).length > 0 ? images : { '': null };
return (
<>
{Object.entries(nextImages).map(([url, _file]) => (
<div key={url} className='relative aspect-square size-24'>
<ImageWithFallback
className='rounded-full border-1 border-[rgba(0,0,0,0.04)]'
alt='프로필 이미지'
fill
src={url}
/>
<button
className={cn(
'flex-center absolute -right-1.75 bottom-0 size-8 cursor-pointer rounded-full border-1 border-gray-300 bg-gray-100',
'hover:scale-110 hover:bg-gray-200',
'transition-all duration-300',
)}
type='button'
onClick={onFileSelectClick}
>
<Icon id='edit' className='size-5 text-gray-600' />
</button>
</div>
))}
</>
);
}}
</ImageInput>
</div>
);
};

export default ImageField;
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export { ImageField } from './image-field';
export { MBTIField } from './mbti-field';
export { MessageField } from './message-field';
export { NickNameField } from './nickname-field';
40 changes: 30 additions & 10 deletions src/components/pages/user/profile/profile-edit-modal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,11 @@ import {
ModalTitle,
useModal,
} from '@/components/ui';
import { User } from '@/types/service/user';
import { useUpdateUser } from '@/hooks/use-user';
import { useUserImageUpdate } from '@/hooks/use-user/use-user-image-update';
import { UpdateMePayload, User } from '@/types/service/user';

import ImageField from '../profile-edit-fields/image-field';
import { MBTIField } from '../profile-edit-fields/mbti-field';
import { MessageField } from '../profile-edit-fields/message-field';
import { NickNameField } from '../profile-edit-fields/nickname-field';
import { ImageField, MBTIField, MessageField, NickNameField } from '../profile-edit-fields';

interface Props {
user: User;
Expand All @@ -25,21 +24,40 @@ export const ProfileEditModal = ({ user }: Props) => {

const { close } = useModal();

const { mutateAsync: updateUser, isPending: isUserInfoPending } = useUpdateUser();
const { mutateAsync: updateUserImage, isPending: isUserImagePending } = useUserImageUpdate();

const form = useForm({
defaultValues: {
profileImage: {
[image]: null,
} as ImageRecord,
profileImage: { [image]: null } as ImageRecord,
nickName,
profileMessage,
mbti,
},
onSubmit: async ({ value }) => {
console.log(value);
const { profileImage, nickName, profileMessage, mbti } = value;

// 프로필 항목 업데이트 조건 체크
const nextProfileInfo: UpdateMePayload = {
...(user.nickName !== value.nickName && { nickName }),
...(user.profileMessage !== value.profileMessage && { profileMessage }),
...(user.mbti !== value.mbti && { mbti }),
};
if (Object.values(nextProfileInfo).length > 0) {
await updateUser(nextProfileInfo);
}

// 이미지 업데이트 조건 체크
const imageFileObject = Object.values(profileImage)[0];
if (imageFileObject) {
await updateUserImage({ file: imageFileObject });
}
close();
},
Comment on lines 46 to 82
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add error handling for failed mutations.

If either updateUser or updateUserImage fails, the modal closes without user feedback. Users won't know their changes weren't saved.

     onSubmit: async ({ value }) => {
       const { profileImage, nickName, profileMessage, mbti } = value;

       // 프로필 항목 업데이트 조건 체크
       const nextProfileInfo: UpdateMePayload = {
         ...(user.nickName !== value.nickName && { nickName }),
         ...(user.profileMessage !== value.profileMessage && { profileMessage }),
         ...(user.mbti !== value.mbti && { mbti }),
       };
-      if (Object.values(nextProfileInfo).length > 0) {
-        await updateUser(nextProfileInfo);
-      }
-
-      // 이미지 업데이트 조건 체크
-      const imageFileObject = Object.values(profileImage)[0];
-      if (imageFileObject) {
-        await updateUserImage({ file: imageFileObject });
+      try {
+        if (Object.keys(nextProfileInfo).length > 0) {
+          await updateUser(nextProfileInfo);
+        }
+
+        // 이미지 업데이트 조건 체크
+        const imageFileObject = Object.values(profileImage)[0];
+        if (imageFileObject) {
+          await updateUserImage({ file: imageFileObject });
+        }
+        close();
+      } catch (error) {
+        // Consider showing a toast notification here
+        console.error('프로필 업데이트 실패:', error);
       }
-      close();
     },
🤖 Prompt for AI Agents
In src/components/pages/user/profile/profile-edit-modal/index.tsx around lines
37 to 56, the submit handler currently awaits updateUser and updateUserImage but
closes the modal regardless of failures; wrap the mutation calls in a try/catch
so errors are caught, show a user-facing error (e.g., toast or set an error
state) when either updateUser or updateUserImage throws, only call close() when
all required updates succeed, and log the error for debugging; ensure both calls
remain awaited and preserve current behavior when there are no errors.

});

const isPending = isUserInfoPending || isUserImagePending;

return (
<ModalContent className='max-w-82.5'>
<ModalTitle>프로필 수정</ModalTitle>
Expand All @@ -61,7 +79,9 @@ export const ProfileEditModal = ({ user }: Props) => {
<Button variant='tertiary' onClick={close}>
취소
</Button>
<Button type='submit'>수정하기</Button>
<Button disabled={isPending} type='submit'>
{isPending ? '수정 중...' : '수정하기'}
</Button>
</div>
</form>
</ModalContent>
Expand Down
27 changes: 27 additions & 0 deletions src/components/ui/image-with-fallback/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
'use client';

import Image, { ImageProps } from 'next/image';

import { useEffect, useState } from 'react';

interface ImageWithFallbackProps extends Omit<ImageProps, 'src' | 'onError'> {
src: string;
fallbackSrc?: string;
}

export const ImageWithFallback = ({
src,
fallbackSrc = 'https://plus.unsplash.com/premium_photo-1738592736106-a17b897c0ab1?q=80&w=1934&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
...rest
}: ImageWithFallbackProps) => {
Comment on lines +12 to +16
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Use a local fallback image instead of an external URL.

The default fallbackSrc points to an external Unsplash URL, which defeats the purpose of a reliable fallback. If Unsplash is unavailable, rate-limited, or the URL changes, the fallback itself will fail.

Consider using a local placeholder image:

 export const ImageWithFallback = ({
   src,
-  fallbackSrc = 'https://plus.unsplash.com/premium_photo-1738592736106-a17b897c0ab1?q=80&w=1934&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
+  fallbackSrc = '/images/default-profile.png',
   ...rest
 }: ImageWithFallbackProps) => {

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/components/ui/image-with-fallback/index.tsx around lines 12 to 16, the
default fallbackSrc is an external Unsplash URL which is unreliable; replace it
with a local static asset (e.g., import or reference a local placeholder in the
project assets/public folder) and update the component to use that local path as
the default fallbackSrc so the fallback always resolves even if external
services fail.

const [error, setError] = useState(false);

useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect
setError(false);
}, [src]);

const imgSrc = error || !src || src === 'null' ? fallbackSrc : src;

return <Image {...rest} src={imgSrc} unoptimized onError={() => setError(true)} />;
};
1 change: 1 addition & 0 deletions src/components/ui/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export { Button } from './button';
export { Hint } from './hint';
export { ImageWithFallback } from './image-with-fallback';
export type { ImageInputProps, ImageRecord } from './imageinput';
export { ImageInput } from './imageinput';
export { Input } from './input';
Expand Down
1 change: 0 additions & 1 deletion src/hooks/use-user/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export { useDeleteUser } from './use-user-delete';
export { useFollowUser } from './use-user-follow';
export { useGetUser } from './use-user-get';
export { useUnfollowUser } from './use-user-unfollow';
Expand Down
6 changes: 6 additions & 0 deletions src/hooks/use-user/use-user-get/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ export const useGetUser = ({ userId }: GetUserParams, options?: { enabled?: bool
queryKey: userKeys.item(userId),
queryFn: () => API.userService.getUser({ userId }),
...options,
select: (data) => ({
...data,
profileImage: data.profileImage ?? '',
profileMessage: data.profileMessage ?? '',
mbti: data.mbti ?? '',
}),
});
return query;
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,17 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';

import { API } from '@/api';
import { userKeys } from '@/lib/query-key/query-key-user';
import { UpdateMyProfileImagePayload } from '@/types/service/user';

export const useDeleteUser = () => {
export const useUserImageUpdate = () => {
const queryClient = useQueryClient();
const query = useMutation({
mutationFn: () => API.userService.deleteMe(),
mutationFn: (payload: UpdateMyProfileImagePayload) =>
API.userService.updateMyProfileImage(payload),
onSuccess: (data, _variables, _context) => {
queryClient.invalidateQueries({ queryKey: userKeys.item(data.userId) });
console.log('요청 성공');
},
onError: () => {
console.log('요청 실패');
},
onError: () => {},
});
return query;
};
Loading