diff --git a/src/api/auth/authClientAPI.ts b/src/api/auth/authClientAPI.ts index dc17ff46..bbd87291 100644 --- a/src/api/auth/authClientAPI.ts +++ b/src/api/auth/authClientAPI.ts @@ -1,16 +1,16 @@ -import { EditInfoParams } from '@/features/profile/types'; import apiClient from '@/lib/utils/apiClient'; export const authClientAPI = { //회원정보 확인 //회원정보 수정 - editInfo: async ({ nickname, image, description }: EditInfoParams) => { - await apiClient.post('auths/user', { - nickname, - image, - description, + editInfo: async (formData: FormData) => { + const response = await apiClient.post('auths/user', formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, }); + return response.data; }, //회원가입 diff --git a/src/api/auth/react-query/customHooks.ts b/src/api/auth/react-query/customHooks.ts index 74958a5a..58834975 100644 --- a/src/api/auth/react-query/customHooks.ts +++ b/src/api/auth/react-query/customHooks.ts @@ -2,12 +2,11 @@ import { useMutation } from '@tanstack/react-query'; import { showToast } from '@/components/toast/toast'; import { authClientAPI } from '../authClientAPI'; import { getUserInfo } from '@/features/auth/api/auth'; -import { EditInfoParams } from '@/features/profile/types'; //프로필 수정하기 -export function useEditInfo() { +export function useEditInfoMutation() { return useMutation({ - mutationFn: (data: EditInfoParams) => authClientAPI.editInfo(data), + mutationFn: (formData: FormData) => authClientAPI.editInfo(formData), onSuccess: () => { getUserInfo(); showToast({ message: '프로필 수정이 완료되었습니다.', type: 'success' }); diff --git a/src/api/book-club/bookClubMainAPI.ts b/src/api/book-club/bookClubMainAPI.ts index 12ad39cc..a37a09e5 100644 --- a/src/api/book-club/bookClubMainAPI.ts +++ b/src/api/book-club/bookClubMainAPI.ts @@ -22,7 +22,7 @@ export const bookClubMainAPI = { //유저가 참가한 북클럽 조회 userJoined: async (userId: number, params?: MyProfileParams) => { - const response = await apiClient.get(`/book-clubs/user/${userId}/joined`, { + const response = await apiClient.get(`/book-clubs/users/${userId}/joined`, { params, }); return response.data; @@ -30,9 +30,12 @@ export const bookClubMainAPI = { //유저가 만든 북클럽 조회 userCreated: async (userId: number, params?: MyProfileParams) => { - const response = await apiClient.get(`/book-clubs/user/${userId}/created`, { - params, - }); + const response = await apiClient.get( + `/book-clubs/users/${userId}/created`, + { + params, + }, + ); return response.data; }, diff --git a/src/components/header/HeaderBar.tsx b/src/components/header/HeaderBar.tsx index 3cfba709..34a905db 100644 --- a/src/components/header/HeaderBar.tsx +++ b/src/components/header/HeaderBar.tsx @@ -7,6 +7,7 @@ import { usePathname, useRouter } from 'next/navigation'; import { useAuthStore } from '@/store/authStore'; import DropDown from '../drop-down/DropDown'; import { logout } from '@/features/auth/api/auth'; +import { showToast } from '../toast/toast'; function HeaderBar() { const pathname = usePathname(); @@ -17,6 +18,7 @@ function HeaderBar() { if (value === 'LOGOUT') { try { await logout(); + showToast({ message: '로그아웃 되었습니다 ', type: 'success' }); router.replace('/bookclub'); } catch (error) { console.error('로그아웃 실패:', error); diff --git a/src/constants/avatar.ts b/src/constants/avatar.ts index 509cb089..3105d6d9 100644 --- a/src/constants/avatar.ts +++ b/src/constants/avatar.ts @@ -5,6 +5,7 @@ export const AVATAR_SIZE = { lg: 'h-[56px] w-[56px]', xl: 'h-[74px] w-[71px]', max: 'h-[74px] w-[74px]', + profile: 'h-[80px] w-[80px]', } as const; export type AvatarSize = keyof typeof AVATAR_SIZE; diff --git a/src/features/chat-room/container/chat-bubble-list/components/ChatMessage.tsx b/src/features/chat-room/container/chat-bubble-list/components/ChatMessage.tsx index ebb1d0b5..21a491a0 100644 --- a/src/features/chat-room/container/chat-bubble-list/components/ChatMessage.tsx +++ b/src/features/chat-room/container/chat-bubble-list/components/ChatMessage.tsx @@ -1,5 +1,8 @@ import ChatBubble from '@/features/chat-room/components/chat-bubble/ChatBubble'; import { ChatMessageType } from '../../../types/chatBubbleList'; +import PopUp from '@/components/pop-up/PopUp'; +import { useState } from 'react'; +import { useRouter } from 'next/navigation'; interface ChatMessageProps { message: ChatMessageType; @@ -16,24 +19,41 @@ function ChatMessage({ isConsecutive, hostId, time, - onProfileClick, + // onProfileClick, }: ChatMessageProps) { const { userId, content, userNickname } = message; + const [isPopUpOpen, setIsPopUpOpen] = useState(false); + const router = useRouter(); - return isMyMessage ? ( - - ) : ( - onProfileClick?.(userId), - isConsecutive, - }} - /> + return ( + <> + {isMyMessage ? ( + + ) : ( + setIsPopUpOpen(true), + isConsecutive, + }} + /> + )} + setIsPopUpOpen(false)} + handlePopUpConfirm={() => { + setIsPopUpOpen(false); + router.push(`/profile/${userId}`); + }} + /> + ); } diff --git a/src/features/club-details/components/ReviewList.tsx b/src/features/club-details/components/ReviewList.tsx index 98b4b411..1d190c21 100644 --- a/src/features/club-details/components/ReviewList.tsx +++ b/src/features/club-details/components/ReviewList.tsx @@ -45,7 +45,7 @@ function ReviewList({ ratingCount={review.rating} comment={review.content} userProfile={{ - profileImage: review.image || '/images/profile.png', + profileImage: review.userImage || '/images/profile.png', userName: review.nickname, createdAt: formatDateForUI(review.createdAt, 'DATE_ONLY'), }} diff --git a/src/features/club-details/mocks/DetailReviewDatas.ts b/src/features/club-details/mocks/DetailReviewDatas.ts index f9a202fb..bb8d8443 100644 --- a/src/features/club-details/mocks/DetailReviewDatas.ts +++ b/src/features/club-details/mocks/DetailReviewDatas.ts @@ -8,7 +8,7 @@ export const mockReviews: DetailReview[] = [ rating: 5, content: '정말 멋진 독서 모임이었어요! 많은 것을 배웠습니다.', nickname: '책사랑꾼', - image: undefined, + userImage: undefined, createdAt: '2025-01-01T10:00:00Z', }, { @@ -18,7 +18,7 @@ export const mockReviews: DetailReview[] = [ rating: 3, content: '그럭저럭 괜찮았어요.', nickname: '평범한독자', - image: 'https://example.com/images/user2.jpg', + userImage: 'https://example.com/images/user2.jpg', createdAt: '2025-01-02T15:30:00Z', }, { @@ -28,7 +28,7 @@ export const mockReviews: DetailReview[] = [ rating: 1, content: '시간 낭비였던 것 같아요.', nickname: '비판적인독자', - image: undefined, + userImage: undefined, createdAt: '2025-01-01T09:00:00Z', }, { @@ -38,7 +38,7 @@ export const mockReviews: DetailReview[] = [ rating: 4, content: '좋은 경험이었지만 약간 서둘러서 진행된 느낌이었습니다.', nickname: '빠른독자', - image: 'https://example.com/images/user4.jpg', + userImage: 'https://example.com/images/user4.jpg', createdAt: '2025-01-03T12:00:00Z', }, { @@ -48,7 +48,7 @@ export const mockReviews: DetailReview[] = [ rating: 5, content: '지금까지 참가한 독서 모임 중 최고였습니다!', nickname: '열정독자', - image: undefined, + userImage: undefined, createdAt: '2025-01-02T08:00:00Z', }, { @@ -58,7 +58,7 @@ export const mockReviews: DetailReview[] = [ rating: 2, content: '조직적으로 더 나아질 필요가 있습니다.', nickname: '정리된혼돈', - image: 'https://example.com/images/user6.jpg', + userImage: 'https://example.com/images/user6.jpg', createdAt: '2025-01-04T14:45:00Z', }, { @@ -68,7 +68,7 @@ export const mockReviews: DetailReview[] = [ rating: 3, content: '평범했어요.', nickname: '평범한조', - image: undefined, + userImage: undefined, createdAt: '2025-01-03T18:20:00Z', }, { @@ -78,7 +78,7 @@ export const mockReviews: DetailReview[] = [ rating: 4, content: '재미있고 흥미로운 시간이었습니다!', nickname: '행복한독자', - image: 'https://example.com/images/user8.jpg', + userImage: 'https://example.com/images/user8.jpg', createdAt: '2025-01-05T09:15:00Z', }, { @@ -88,7 +88,7 @@ export const mockReviews: DetailReview[] = [ rating: 1, content: '추천하고 싶지 않아요.', nickname: '실망한독자', - image: undefined, + userImage: undefined, createdAt: '2025-01-04T11:00:00Z', }, { @@ -98,7 +98,7 @@ export const mockReviews: DetailReview[] = [ rating: 5, content: '정말 즐거운 시간이었습니다!', nickname: '기쁜독자', - image: 'https://example.com/images/user10.jpg', + userImage: 'https://example.com/images/user10.jpg', createdAt: '2025-01-06T10:10:00Z', }, ]; diff --git a/src/features/profile/components/clubs/ProfileWrittenReview.tsx b/src/features/profile/components/clubs/ProfileWrittenReview.tsx index 828f0907..5a10d3b8 100644 --- a/src/features/profile/components/clubs/ProfileWrittenReview.tsx +++ b/src/features/profile/components/clubs/ProfileWrittenReview.tsx @@ -18,14 +18,14 @@ export default function ProfileWrittenReview({ >
diff --git a/src/features/profile/components/info/Info.tsx b/src/features/profile/components/info/Info.tsx index 9681ca11..de4a2677 100644 --- a/src/features/profile/components/info/Info.tsx +++ b/src/features/profile/components/info/Info.tsx @@ -2,19 +2,18 @@ import { useState } from 'react'; import Avatar from '@/components/avatar/Avatar'; -import { useEditInfo } from '@/api/auth/react-query'; import { InfoEditModal } from './index'; import { EditInfoParams, ProfilePageProps } from '../../types'; import IconButton from '@/components/icon-button/IconButton'; import { IcEdit } from '../../../../../public/icons'; +import { useEditInfo } from '../../hooks/useEditInfo'; export default function Info({ user, isMyPage }: ProfilePageProps) { const [isModalOpen, setIsModalOpen] = useState(false); + const { onSubmit } = useEditInfo(); - const { mutate: editInfo } = useEditInfo(); - - const onSubmitEditInfo = (formData: EditInfoParams) => { - editInfo(formData); + const onSubmitEditInfo = async (data: EditInfoParams) => { + await onSubmit(data); setIsModalOpen(false); }; @@ -42,11 +41,11 @@ export default function Info({ user, isMyPage }: ProfilePageProps) { role="content" > {/* 프로필 이미지 */} -
+
{/* 프로필 정보 */} @@ -80,9 +79,11 @@ export default function Info({ user, isMyPage }: ProfilePageProps) { onClose={() => setIsModalOpen(false)} onConfirm={(formData) => onSubmitEditInfo(formData)} infoData={{ - nickname: user?.nickname || '', - description: user?.description || '', - image: user?.image, + image: user?.image || '', + user: { + nickname: user?.nickname || '', + description: user?.description || '', + }, }} /> )} diff --git a/src/features/profile/components/info/InfoEditModal.tsx b/src/features/profile/components/info/InfoEditModal.tsx index a37b06cd..6b69aaf1 100644 --- a/src/features/profile/components/info/InfoEditModal.tsx +++ b/src/features/profile/components/info/InfoEditModal.tsx @@ -10,7 +10,7 @@ import { EditInfoParams } from '../../types'; interface InfoEditModalProps { isOpen: boolean; onClose: () => void; - onConfirm: (updatedData: EditInfoParams) => void; + onConfirm: (formData: EditInfoParams) => void; infoData: EditInfoParams; } @@ -31,7 +31,9 @@ function InfoEditContent({
@@ -63,7 +65,7 @@ function InfoEditContent({ type="text" name="nickname" aria-label="nickname" - value={formData.nickname} + value={formData.user?.nickname} onChange={handleChange} className="w-full rounded-lg bg-gray-light-02 p-2 font-medium" /> @@ -74,7 +76,7 @@ function InfoEditContent({ type="text" name="description" aria-label="description" - value={formData.description} + value={formData.user?.description} onChange={handleChange} className="w-full rounded-lg bg-gray-light-02 p-2 font-medium" /> @@ -93,21 +95,23 @@ export default function InfoEditModal({ }: InfoEditModalProps) { const { user } = useAuthStore(); const [formData, setFormData] = useState({ - nickname: infoData.nickname || user?.name || '', - description: infoData.description || user?.description || '', image: infoData.image || user?.image || '/images/profile.png', + user: { + nickname: infoData.user?.nickname || user?.name || '', + description: infoData.user?.description || user?.description || '', + }, }); const [preview, setPreview] = useState(null); const handleFileChange = (e: React.ChangeEvent) => { - const file = e.target.files ? e.target.files[0] : null; + const file = e.target.files?.[0]; if (file) { const reader = new FileReader(); reader.onloadend = () => { const fileResult = reader.result as string; setPreview(fileResult); - setFormData((prev) => ({ ...prev, image: fileResult })); + setFormData((prev) => ({ ...prev, image: file })); }; reader.readAsDataURL(file); } @@ -115,7 +119,10 @@ export default function InfoEditModal({ const handleChange = (e: React.ChangeEvent) => { const { name, value } = e.target; - setFormData((prev) => ({ ...prev, [name]: value })); + setFormData((prev) => ({ + ...prev, + user: { ...prev.user, [name]: value }, + })); }; const handleConfirm = () => { diff --git a/src/features/profile/container/MyJoinedClubList.tsx b/src/features/profile/container/MyJoinedClubList.tsx index a906eded..6651271e 100644 --- a/src/features/profile/container/MyJoinedClubList.tsx +++ b/src/features/profile/container/MyJoinedClubList.tsx @@ -56,7 +56,6 @@ export default function MyJoinedClubList({ order }: ClubListProps) { const onDelete = async (clubId: number) => { try { const res = await leaveClub(clubId); - console.log(res); if (res) { showToast({ message: '취소된 모임을 삭제하였습니다.', @@ -134,7 +133,7 @@ export default function MyJoinedClubList({ order }: ClubListProps) { { + const { mutateAsync: editInfo, isPending } = useEditInfoMutation(); + + const onSubmit = async (data: EditInfoParams) => { + const formData = toFormData(data); + return await editInfo(formData); + }; + return { onSubmit, isLoading: isPending }; +}; diff --git a/src/features/profile/types/index.ts b/src/features/profile/types/index.ts index e473b60e..bc35e47d 100644 --- a/src/features/profile/types/index.ts +++ b/src/features/profile/types/index.ts @@ -38,7 +38,9 @@ export interface ClubListProps { } export interface EditInfoParams { - nickname?: string; - description?: string; - image?: string | null; + image?: string | File; + user?: { + nickname?: string; + description?: string; + }; } diff --git a/src/features/profile/utils/infoEditFormUtils.tsx b/src/features/profile/utils/infoEditFormUtils.tsx new file mode 100644 index 00000000..93cf606f --- /dev/null +++ b/src/features/profile/utils/infoEditFormUtils.tsx @@ -0,0 +1,23 @@ +import { EditInfoParams } from '../types'; + +export const toFormData = (data: EditInfoParams) => { + const formData = new FormData(); + + const imageFile = data.image instanceof File ? data.image : null; + + if (imageFile) { + formData.append('image', imageFile); + } + const userData = { + nickname: data?.user?.nickname, + description: data?.user?.description, + }; + formData.append( + 'user', + new Blob([JSON.stringify(userData)], { + type: 'application/json', + }), + ); + + return formData; +}; diff --git a/src/mocks/mockDatas.ts b/src/mocks/mockDatas.ts index e7868b83..ed2cb21e 100644 --- a/src/mocks/mockDatas.ts +++ b/src/mocks/mockDatas.ts @@ -253,11 +253,11 @@ export const mockReviews: Review[] = [ rating: 5, content: '이 책클럽은 정말 좋았습니다. 책 선정도 훌륭하고, 참여자들 간의 토론도 활발했어요.', - image: 'https://example.com/images/review1.jpg', + userImage: 'https://example.com/images/review1.jpg', createdAt: '2025-01-10T14:30:00Z', - clubImgUrl: 'https://example.com/images/club1.jpg', + bookClubImageUrl: 'https://example.com/images/club1.jpg', nickname: '진영', - clubName: '문학 사랑 모임', + bookClubTitle: '문학 사랑 모임', bookClubType: 'FREE', }, { @@ -267,11 +267,11 @@ export const mockReviews: Review[] = [ rating: 4, content: '모임 분위기는 좋았지만, 책의 주제가 조금 어려웠어요. 그래도 유익한 시간이었어요.', - image: 'https://example.com/images/review2.jpg', + userImage: 'https://example.com/images/review2.jpg', createdAt: '2025-01-12T16:45:00Z', - clubImgUrl: 'https://example.com/images/club2.jpg', + bookClubImageUrl: 'https://example.com/images/club2.jpg', nickname: '민지', - clubName: '책과 커피 모임', + bookClubTitle: '책과 커피 모임', bookClubType: 'FREE', }, { @@ -282,9 +282,9 @@ export const mockReviews: Review[] = [ content: '책은 좋았지만, 온라인 모임이라 참여자들과의 소통이 부족했던 것 같아요.', createdAt: '2025-01-14T17:00:00Z', - clubImgUrl: 'https://example.com/images/club3.jpg', + bookClubImageUrl: 'https://example.com/images/club3.jpg', nickname: '수연', - clubName: 'SF 소설 모임', + bookClubTitle: 'SF 소설 모임', bookClubType: 'FIXED', }, { @@ -295,9 +295,9 @@ export const mockReviews: Review[] = [ content: '좋은 모임이었지만, 장소가 조금 좁았어요. 그 외엔 정말 유익한 시간이었습니다.', createdAt: '2025-01-15T18:10:00Z', - clubImgUrl: 'https://example.com/images/club4.jpg', + bookClubImageUrl: 'https://example.com/images/club4.jpg', nickname: '지훈', - clubName: '역사 탐방 모임', + bookClubTitle: '역사 탐방 모임', bookClubType: 'FREE', }, { @@ -307,11 +307,11 @@ export const mockReviews: Review[] = [ rating: 5, content: '시집에 대한 다양한 해석을 나누어서 매우 흥미로운 모임이었어요. 다음 모임이 기대됩니다.', - image: 'https://example.com/images/review5.jpg', + userImage: 'https://example.com/images/review5.jpg', createdAt: '2025-01-16T12:00:00Z', - clubImgUrl: 'https://example.com/images/club5.jpg', + bookClubImageUrl: 'https://example.com/images/club5.jpg', nickname: '정민', - clubName: '시집 독서 모임', + bookClubTitle: '시집 독서 모임', bookClubType: 'FIXED', }, { @@ -322,9 +322,9 @@ export const mockReviews: Review[] = [ content: '경제학 토론이 매우 유익했고, 다른 사람들의 의견을 들을 수 있어 좋았습니다.', createdAt: '2025-01-17T13:30:00Z', - clubImgUrl: 'https://example.com/images/club6.jpg', + bookClubImageUrl: 'https://example.com/images/club6.jpg', nickname: '현수', - clubName: '경제학 토론 모임', + bookClubTitle: '경제학 토론 모임', bookClubType: 'FREE', }, { @@ -335,9 +335,9 @@ export const mockReviews: Review[] = [ content: '영화와 책을 비교하는 재미는 있었으나, 일부 영화가 책의 내용을 잘 반영하지 못한 것 같아요.', createdAt: '2025-01-18T19:00:00Z', - clubImgUrl: 'https://example.com/images/club7.jpg', + bookClubImageUrl: 'https://example.com/images/club7.jpg', nickname: '태영', - clubName: '문학과 영화 모임', + bookClubTitle: '문학과 영화 모임', bookClubType: 'FIXED', }, { @@ -347,11 +347,11 @@ export const mockReviews: Review[] = [ rating: 5, content: '여행 사진과 이야기를 나누는 모임은 정말 즐거웠어요. 다른 사람들의 경험을 듣는 것이 너무 흥미로웠습니다.', - image: 'https://example.com/images/review8.jpg', + userImage: 'https://example.com/images/review8.jpg', createdAt: '2025-01-19T20:30:00Z', - clubImgUrl: 'https://example.com/images/club8.jpg', + bookClubImageUrl: 'https://example.com/images/club8.jpg', nickname: '수아', - clubName: '여행 사진 모임', + bookClubTitle: '여행 사진 모임', bookClubType: 'FREE', }, { @@ -362,9 +362,9 @@ export const mockReviews: Review[] = [ content: '디지털 기술에 대한 최신 정보를 나눌 수 있어 좋았고, 많은 토론이 이루어졌습니다.', createdAt: '2025-01-20T21:45:00Z', - clubImgUrl: 'https://example.com/images/club9.jpg', + bookClubImageUrl: 'https://example.com/images/club9.jpg', nickname: '정호', - clubName: '디지털 기술 토론 모임', + bookClubTitle: '디지털 기술 토론 모임', bookClubType: 'FREE', }, { @@ -375,9 +375,9 @@ export const mockReviews: Review[] = [ content: '고전 문학을 다시 한번 되새길 수 있는 좋은 시간이었어요. 다만, 책 선정이 조금 아쉬웠습니다.', createdAt: '2025-01-21T22:30:00Z', - clubImgUrl: 'https://example.com/images/club10.jpg', + bookClubImageUrl: 'https://example.com/images/club10.jpg', nickname: '지은', - clubName: '고전 문학 독서 모임', + bookClubTitle: '고전 문학 독서 모임', bookClubType: 'FREE', }, ]; diff --git a/src/types/review.ts b/src/types/review.ts index 7e116757..d1ac6daf 100644 --- a/src/types/review.ts +++ b/src/types/review.ts @@ -5,20 +5,20 @@ export interface Review { bookClubId: number; rating: number; content: string; - image?: string | undefined; + nickname: string; + userImage?: string | undefined; createdAt: string; - clubImgUrl?: string; - nickname?: string; - clubName: string; + bookClubImageUrl?: string; + bookClubTitle: string; bookClubType: 'FREE' | 'FIXED'; } export interface DetailReview - extends Omit { - nickname: string; + extends Omit { bookClubType?: 'FREE' | 'FIXED'; - clubName?: string; + bookClubTitle?: string; + bookClubImageUrl?: string; } export interface ClubDetailReviewFilters {