diff --git a/src/entities/user/model/use-user-profile-query.ts b/src/entities/user/model/use-user-profile-query.ts index 374fbcf9..d31a5f19 100644 --- a/src/entities/user/model/use-user-profile-query.ts +++ b/src/entities/user/model/use-user-profile-query.ts @@ -1,13 +1,10 @@ import { sendGTMEvent } from '@next/third-parties/google'; -import { useMutation, useQuery } from '@tanstack/react-query'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { getUserProfile, patchAutoMatching, } from '@/entities/user/api/get-user-profile'; -import type { - GetUserProfileResponse, - PatchAutoMatchingParams, -} from '@/entities/user/api/types'; +import type { GetUserProfileResponse } from '@/entities/user/api/types'; import { hashValue } from '@/shared/lib/hash'; export const useUserProfileQuery = (memberId: number) => { @@ -19,23 +16,62 @@ export const useUserProfileQuery = (memberId: number) => { }); }; +export interface PatchAutoMatchingParams { + memberId: number; + autoMatching: boolean; +} + export const usePatchAutoMatchingMutation = () => { - return useMutation({ + const qc = useQueryClient(); + + return useMutation< + void, + unknown, + PatchAutoMatchingParams, + { prev?: unknown } + >({ mutationFn: patchAutoMatching, - onSuccess: (_, variables) => { - if (variables.autoMatching) { - sendGTMEvent({ - event: 'custom_member_study_toggle_on', - dl_timestamp: new Date().toISOString(), - dl_member_id: hashValue(String(variables.memberId)), - }); - } else { - sendGTMEvent({ - event: 'custom_member_study_toggle_off', - dl_timestamp: new Date().toISOString(), - dl_member_id: hashValue(String(variables.memberId)), + + onMutate: async ({ memberId, autoMatching }) => { + await qc.cancelQueries({ queryKey: ['userProfile', memberId] }); + const prev = qc.getQueryData(['userProfile', memberId]); + if (prev && typeof prev === 'object') { + qc.setQueryData(['userProfile', memberId], { + ...(prev as any), + autoMatching, }); } + + return { prev }; + }, + + onError: (_err, { memberId }, ctx) => { + if (ctx?.prev) { + qc.setQueryData(['userProfile', memberId], ctx.prev); + } + }, + + onSuccess: async (_data, { memberId, autoMatching }) => { + await qc.invalidateQueries({ queryKey: ['userProfile', memberId] }); + + await qc.invalidateQueries({ + predicate: (q) => + Array.isArray(q.queryKey) && q.queryKey[0] === 'weeklyParticipation', + }); + + await qc.invalidateQueries({ + predicate: (q) => + Array.isArray(q.queryKey) && + q.queryKey[0] === 'weeklyReservationMembers', + }); + + sendGTMEvent({ + event: autoMatching + ? 'custom_member_study_toggle_on' + : 'custom_member_study_toggle_off', + dl_timestamp: new Date().toISOString(), + dl_member_id: hashValue(String(memberId)), + }); }, }); }; diff --git a/src/entities/user/ui/user-profile-modal.tsx b/src/entities/user/ui/user-profile-modal.tsx index a9ab7436..041aaa84 100644 --- a/src/entities/user/ui/user-profile-modal.tsx +++ b/src/entities/user/ui/user-profile-modal.tsx @@ -1,6 +1,7 @@ 'use client'; import { XIcon } from 'lucide-react'; +import { useState } from 'react'; import { useUserProfileQuery } from '@/entities/user/model/use-user-profile-query'; import KeywordReview from '@/entities/user/ui/keyword-review'; import ProfileInfoCard from '@/entities/user/ui/profile-info-card'; @@ -23,167 +24,212 @@ export default function UserProfileModal({ memberId, trigger, }: UserProfileModalProps) { + const [open, setOpen] = useState(false); + + return ( + + {trigger} + + {open && ( + + + + setOpen(false)} + /> + + + )} + + ); +} + +function UserProfileBody({ + memberId, + onClose, +}: { + memberId: number; + onClose: () => void; +}) { const { data: profile, isLoading, isError } = useUserProfileQuery(memberId); const { data: positiveKeywordsData } = useUserPositiveKeywordsQuery({ memberId, }); - if (isLoading || isError || !profile || !positiveKeywordsData) return null; - - const positiveKeywords = positiveKeywordsData?.keywords || []; + if (isLoading) { + return ( + <> +
+ 불러오는 중… + + ); + } + + if (isError || !profile || !positiveKeywordsData) { + return ( + <> +
+ 프로필을 불러오지 못했습니다. + + ); + } + + const positiveKeywords = positiveKeywordsData.keywords ?? []; const temperPreset = getSincerityPresetByLevelName( profile.sincerityTemp.levelName, ); return ( - - {trigger} - - - - - - {profile.memberProfile.memberName}님의 프로필 - - - - - - - -
- +
+ + +
+ + +
+
+ {profile.memberProfile.mbti && ( + {profile.memberProfile.mbti} + )} + {profile.memberProfile.interests.slice(0, 4).map((interest) => ( + + {interest.name} + + ))} +
+ +
+
+ {profile.memberProfile.memberName} +
+ +
+ +
+ + t.techStackName) + .join(', ')} + /> + t.label) + .join(', ')} + /> + + +
+ +
+ +
+ + 받은 평가 + + +
+ {/* todo: 기획 fix되면 수정 */} + {/* n명의 유저들이 이런 점이 좋다고 했어요. */} +
    + {positiveKeywords.length > 0 ? ( + positiveKeywords.map((keyword) => ( + + )) + ) : ( + + 아직 받은 평가가 없습니다. + + )} +
+
+
+ + + ); +} -
- -
- - 받은 평가 - - -
- {/* todo: 기획 fix되면 수정 */} - {/* n명의 유저들이 이런 점이 좋다고 했어요. */} - -
    - {positiveKeywords.length > 0 ? ( - positiveKeywords.map((keyword) => ( - - )) - ) : ( - - 아직 받은 평가가 없습니다. - - )} -
-
-
- - - - +function Header({ title, onClose }: { title: string; onClose: () => void }) { + return ( + + + {title} + + + + + + ); +} + +function Field({ icon, value }: { icon: React.ReactNode; value?: string }) { + return ( +
+ {icon} + + {value ?? ''} + +
); } diff --git a/src/features/study/model/use-study-query.ts b/src/features/study/model/use-study-query.ts index ab782631..8976355c 100644 --- a/src/features/study/model/use-study-query.ts +++ b/src/features/study/model/use-study-query.ts @@ -23,6 +23,7 @@ export const useWeeklyParticipation = (params: string) => { queryKey: ['weeklyParticipation', params], queryFn: () => getWeeklyParticipation(params), staleTime: 60 * 1000, + enabled: !!params, }); }; diff --git a/src/features/study/participation/ui/reservation-list.tsx b/src/features/study/participation/ui/reservation-list.tsx index 284e9206..912afdef 100644 --- a/src/features/study/participation/ui/reservation-list.tsx +++ b/src/features/study/participation/ui/reservation-list.tsx @@ -13,20 +13,19 @@ import StartStudyModal from '../../ui/start-study-modal'; import { useInfiniteReservation } from '../model/use-participation-query'; interface ReservationListProps { - isParticipation?: boolean; + studyDate?: string; pageSize?: number; month: number; week: number; } export default function ReservationList({ - isParticipation = false, + studyDate, pageSize = 50, month, week, }: ReservationListProps) { const sentinelRef = useRef(null); - const calledRef = useRef(false); const [memberId, setMemberId] = useState(null); const [isModalOpen, setIsModalOpen] = useState(false); @@ -35,37 +34,20 @@ export default function ReservationList({ setMemberId(id ? Number(id) : null); }, []); + const { data: userProfile } = useUserProfileQuery(memberId ?? 0); + const autoMatching = userProfile?.autoMatching ?? false; + const firstMemberId = useMemo( - () => (isParticipation && memberId !== null ? memberId : null), - [isParticipation, memberId], + () => (autoMatching && memberId !== null ? memberId : null), + [autoMatching, memberId], ); const { data, isLoading, isFetchingNextPage, fetchNextPage, hasNextPage } = - useInfiniteReservation(firstMemberId, pageSize); - - const { data: userProfile } = useUserProfileQuery(memberId ?? 0); + useInfiniteReservation(firstMemberId ?? undefined, pageSize); const { mutate: patchAutoMatching, isPending } = usePatchAutoMatchingMutation(); - useEffect(() => { - if (!memberId || isParticipation || !userProfile) return; - - const { studyApplied, autoMatching } = userProfile; - - if (studyApplied && !autoMatching && !calledRef.current) { - calledRef.current = true; - patchAutoMatching( - { memberId, autoMatching: true }, - { - onError: () => { - calledRef.current = false; - }, - }, - ); - } - }, [memberId, isParticipation, userProfile, patchAutoMatching]); - useEffect(() => { if (!hasNextPage) return; @@ -124,10 +106,14 @@ export default function ReservationList({
{items.map((p) => ( - + ))} - {!isParticipation && ( + {!autoMatching && (
{participant.name}
- {isCurrentUser && 본인} + {isCurrentUser && ( + + 본인 + + )}
{participant.simpleIntroduction} diff --git a/src/features/study/ui/study-card.tsx b/src/features/study/ui/study-card.tsx index 04082f97..453ccd4c 100644 --- a/src/features/study/ui/study-card.tsx +++ b/src/features/study/ui/study-card.tsx @@ -80,7 +80,7 @@ export default function StudyCard() { )}