Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -21,6 +21,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
73 changes: 47 additions & 26 deletions src/api/service/user-service/index.ts
Original file line number Diff line number Diff line change
@@ -1,46 +1,67 @@
import { api } from '@/api/core';
import {
FollowParams,
GetUserParams,
UpdateMePayload,
UpdateMyImagePayload,
UpdateMyNotiParams,
Availability,
FollowPathParams,
GetEmailAvailabilityQueryParams,
GetNicknameAvailabilityQueryParams,
GetUserPathParams,
UnfollowQueryParams,
UpdateMyImagePayloads,
UpdateMyInfoPayloads,
UpdateMyNotificationQueryParams,
User,
} from '@/types/service/user';

export const userServiceRemote = () => ({
// 2. 프로필 편집
updateMe: async (payload: UpdateMePayload) => {
return api.patch<User>('/users', payload);
// 1. 사용자 팔로우
followUser: async (pathParams: FollowPathParams) => {
return api.post<string>(`/users/follow`, null, {
params: { followNickname: pathParams.followNickname },
});
},

// 3. 프로필 이미지 편집
updateMyImage: async (payload: UpdateMyImagePayload) => {
return api.patch<User>(`/users/profile-image`, payload);
// 2. 유저 프로필 변경
updateMyInfo: async (payloads: UpdateMyInfoPayloads) => {
return api.patch<User>('/users/profile', payloads);
},

// 4. 알림 설정 변경
updatMyNotification: async (payload: UpdateMyNotiParams) => {
return api.patch<User>(`/users/notification/${payload.isNotificationEnabled}`);
// 3. 프로필 이미지 변경
updateMyImage: async (payloads: UpdateMyImagePayloads) => {
const formData = new FormData();
formData.append('file', payloads.file);
return api.patch<User>(`/users/profile-image`, formData);
},

// 5. 사용자 단건 조회
getUser: async (payload: GetUserParams) => {
return api.get<User>(`/users/${payload.userId}`);
// 4. 알림 설정 변경
updateMyNotification: async (queryParams: UpdateMyNotificationQueryParams) => {
return api.patch<User>(
`/users/notification?isNotificationEnabled=${queryParams.isNotificationEnabled}`,
);
},

// 1. 사용자 팔로우
followUser: async (payload: FollowParams) => {
return api.post<void>(`/follows/${payload.followNickname}`);
// 5. 유저 프로필 조회
getUser: async (pathParams: GetUserPathParams) => {
return api.get<User>(`/users/${pathParams.userId}`);
},

// 6. 사용자 언팔로우
unfollowUser: async (payload: FollowParams) => {
return api.delete<void>(`/follows/${payload.followNickname}`);
// 6. 닉네임 중복 검사
getNicknameAvailability: async (queryParams: GetNicknameAvailabilityQueryParams) => {
return api.get<Availability>(`/users/nickname/availability`, {
params: { nickname: queryParams.nickName },
});
},

// 7. 회원탈퇴
deleteMe: async () => api.delete<User>(`/users`),
// 7. 이메일 중복 검사
getEmailAvailability: async (queryParams: GetEmailAvailabilityQueryParams) => {
return api.get<Availability>(`/users/email/availability`, {
params: { email: queryParams.email },
});
},

// 8. 사용자 프로필 이미지 변경
// 8. 사용자 언팔로우
unfollowUser: async (params: UnfollowQueryParams) => {
return api.delete<string>(`/users/unfollow`, {
params: { unFollowNickname: params.unFollowNickname },
});
},
});
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
4 changes: 2 additions & 2 deletions src/app/(user)/profile/[userId]/page.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ describe('프로필 페이지 테스트', () => {
return HttpResponse.json(
createMockSuccessResponse({
...mockUserItems[1],
isFollowing: false,
isFollow: false,
}),
);
}),
Expand All @@ -93,7 +93,7 @@ describe('프로필 페이지 테스트', () => {
return HttpResponse.json(
createMockSuccessResponse({
...mockUserItems[1],
isFollowing: true,
isFollow: true,
}),
);
}),
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';
68 changes: 57 additions & 11 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 { UpdateMyInfoPayloads, 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,66 @@ export const ProfileEditModal = ({ user }: Props) => {

const { close } = useModal();

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

const form = useForm({
defaultValues: {
profileImage: {
[image]: null,
} as ImageRecord,
profileImage: { [image]: null } as ImageRecord,
nickName,
profileMessage,
mbti,
},

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

// 프로필 항목 업데이트 조건 체크
const nextProfileInfo: UpdateMyInfoPayloads = {
...(user.nickName !== value.nickName && { nickName }),
...(user.profileMessage !== value.profileMessage && { profileMessage }),
...(user.mbti !== value.mbti && { mbti }),
};

const promises = [];

// 프로필 정보 업데이트 조건 체크
if (Object.keys(nextProfileInfo).length > 0) {
promises.push(updateUser(nextProfileInfo));
}

// 프로필 이미지 업데이트 조건 체크
const imageFileObject = Object.values(profileImage)[0];
if (imageFileObject) {
promises.push(updateUserImage({ file: imageFileObject }));
}

/*
Promise 체이닝 사용 시 catch를 먹어버리기 때문에 각 mutation의 error가 업데이트 되지않음
따라서 try catch 방식 사용
*/
/*
todo: 이미지 변경과 정보 변경 중 하나라도 실패하면 각 항목에 대한 에러메시지 보여줘야함
*/
try {
await Promise.all(promises);
close();
} catch (error) {
console.log('요청 실패', error);
}
},
});

const isPending = isUserInfoPending || isUserImagePending;

return (
<ModalContent className='max-w-82.5'>
<ModalTitle>프로필 수정</ModalTitle>
Expand All @@ -61,7 +105,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
Loading