diff --git a/src/app/(landing)/layout.tsx b/src/app/(landing)/layout.tsx index e59c25bd..296f2063 100644 --- a/src/app/(landing)/layout.tsx +++ b/src/app/(landing)/layout.tsx @@ -1,10 +1,10 @@ import '../global.css'; -import Clarity from '@microsoft/clarity'; import { GoogleTagManager } from '@next/third-parties/google'; import { clsx } from 'clsx'; import type { Metadata } from 'next'; import localFont from 'next/font/local'; +import ClarityInit from '@/components/analytics/clarity-init'; import PageViewTracker from '@/components/analytics/page-view-tracker'; import MainProvider from '@/providers'; import { getOrganizationSchema, getWebsiteSchema } from '@/utils/seo'; @@ -62,10 +62,6 @@ export default async function LandingPageLayout({ children: React.ReactNode; }>) { const initialAccessToken = await getServerCookie('accessToken'); - if (typeof window !== 'undefined' && CLARITY_PROJECT_ID) { - Clarity.init(CLARITY_PROJECT_ID); - } - const organizationSchema = getOrganizationSchema(); const websiteSchema = getWebsiteSchema(); @@ -94,6 +90,7 @@ export default async function LandingPageLayout({ +
{/** 1400 + 48*2 패딩 양옆 48로 임의적용 */} diff --git a/src/app/(service)/layout.tsx b/src/app/(service)/layout.tsx index fa8b7e2b..fc5d3bd8 100644 --- a/src/app/(service)/layout.tsx +++ b/src/app/(service)/layout.tsx @@ -1,11 +1,11 @@ import '../global.css'; -import Clarity from '@microsoft/clarity'; import { GoogleTagManager } from '@next/third-parties/google'; import { clsx } from 'clsx'; import type { Metadata } from 'next'; import localFont from 'next/font/local'; import React from 'react'; +import ClarityInit from '@/components/analytics/clarity-init'; import PageViewTracker from '@/components/analytics/page-view-tracker'; import MainProvider from '@/providers'; import { getServerCookie } from '@/utils/server-cookie'; @@ -34,15 +34,13 @@ export default async function ServiceLayout({ children: React.ReactNode; }>) { const initialAccessToken = await getServerCookie('accessToken'); - if (typeof window !== 'undefined' && CLARITY_PROJECT_ID) { - Clarity.init(CLARITY_PROJECT_ID); - } return ( {GTM_ID && } +
diff --git a/src/components/analytics/clarity-init.tsx b/src/components/analytics/clarity-init.tsx new file mode 100644 index 00000000..0224ef77 --- /dev/null +++ b/src/components/analytics/clarity-init.tsx @@ -0,0 +1,29 @@ +'use client'; + +import Clarity from '@microsoft/clarity'; +import { useEffect } from 'react'; +import type { ReactElement } from 'react'; + +declare global { + interface Window { + __clarityInitialized?: boolean; + } +} + +interface ClarityInitProps { + projectId?: string; +} + +export default function ClarityInit({ + projectId, +}: ClarityInitProps): ReactElement { + useEffect(() => { + if (!projectId || typeof window === 'undefined') return; + if (window.__clarityInitialized) return; + + Clarity.init(projectId); + window.__clarityInitialized = true; + }, [projectId]); + + return <>; +} diff --git a/src/components/ui/toast.tsx b/src/components/ui/toast.tsx index ffc932de..9eaba48b 100644 --- a/src/components/ui/toast.tsx +++ b/src/components/ui/toast.tsx @@ -5,6 +5,8 @@ import React, { useEffect, useState } from 'react'; import { createPortal } from 'react-dom'; import { cn } from '@/components/ui/(shadcn)/lib/utils'; +const EXIT_DURATION = 200; + interface ToastProps { message: string; isVisible: boolean; @@ -22,7 +24,6 @@ export default function Toast({ }: ToastProps) { const [isRendered, setIsRendered] = useState(isVisible); const [isExiting, setIsExiting] = useState(false); - const exitDuration = 200; useEffect(() => { if (isVisible) { @@ -40,11 +41,11 @@ export default function Toast({ const timer = setTimeout(() => { setIsRendered(false); setIsExiting(false); - }, exitDuration); + }, EXIT_DURATION); return () => clearTimeout(timer); } - }, [isVisible, duration, onClose]); + }, [isVisible, duration, onClose, isRendered]); if (!isRendered) return null; if (typeof document === 'undefined') return null; diff --git a/src/features/phone-verification/model/store.ts b/src/features/phone-verification/model/store.ts index 32eb3afd..74894aae 100644 --- a/src/features/phone-verification/model/store.ts +++ b/src/features/phone-verification/model/store.ts @@ -12,6 +12,11 @@ interface PhoneVerificationState { setHasHydrated: (hasHydrated: boolean) => void; } +type PersistedPhoneVerificationState = Pick< + PhoneVerificationState, + 'isVerified' | 'phoneNumber' | 'verifiedAt' | 'memberId' +>; + export const usePhoneVerificationStore = create()( persist( (set) => ({ @@ -21,12 +26,12 @@ export const usePhoneVerificationStore = create()( memberId: null as number | null, hasHydrated: false, setVerified: (phoneNumber, memberId) => - set({ + set((state) => ({ isVerified: true, phoneNumber, verifiedAt: new Date().toISOString(), - ...(memberId !== undefined ? { memberId } : {}), - }), + memberId: memberId ?? state.memberId, + })), reset: () => set({ isVerified: false, @@ -38,7 +43,7 @@ export const usePhoneVerificationStore = create()( }), { name: 'phone-verification-storage', - partialize: (state) => ({ + partialize: (state): PersistedPhoneVerificationState => ({ isVerified: state.isVerified, phoneNumber: state.phoneNumber, verifiedAt: state.verifiedAt, diff --git a/src/features/phone-verification/model/use-phone-auth-mutation.ts b/src/features/phone-verification/model/use-phone-auth-mutation.ts index 2e0b0a9b..a3b87aae 100644 --- a/src/features/phone-verification/model/use-phone-auth-mutation.ts +++ b/src/features/phone-verification/model/use-phone-auth-mutation.ts @@ -39,12 +39,10 @@ export const useVerifyPhoneCodeMutation = (memberId?: number) => { mutationFn: verifyPhoneCode, onSuccess: async (data, variables) => { if (data.success) { - // 현재 사용자의 memberId 가져오기 (쿠키에서) const currentMemberId = memberId ?? Number(getCookie('memberId')); - // 인증 상태 저장 (memberId 포함) + // 인증 상태 저장 setVerified(variables.phoneNumber, currentMemberId || undefined); - // 프로필 정보 쿼리키 갱신 if (currentMemberId) { await queryClient.invalidateQueries({ diff --git a/src/features/phone-verification/model/use-phone-verification-status.ts b/src/features/phone-verification/model/use-phone-verification-status.ts index c542c2e4..adbbf79c 100644 --- a/src/features/phone-verification/model/use-phone-verification-status.ts +++ b/src/features/phone-verification/model/use-phone-verification-status.ts @@ -83,9 +83,7 @@ export function usePhoneVerificationStatus( // 서버 데이터 기반 인증 상태 계산 const serverHasIsVerified = typeof userProfile?.isVerified === 'boolean'; - const serverIsVerified = serverHasIsVerified - ? userProfile!.isVerified - : false; + const serverIsVerified = serverHasIsVerified ? userProfile.isVerified : false; const serverPhoneNumber = userProfile?.memberProfile?.tel ?? null; // 스토어가 현재 유저 소유인지 확인 (계정 전환 대응) @@ -153,22 +151,22 @@ export function usePhoneVerificationStatus( current.memberId !== memberId || current.phoneNumber !== serverPhoneNumber ) { - store.setVerified(serverPhoneNumber ?? '', memberId); - } - } else { - // 서버가 미인증 → 같은 유저 캐시 제거 - if (current.isVerified && current.memberId === memberId) { - store.reset(); + current.setVerified(serverPhoneNumber ?? '', memberId); } + + return; + } + + // 서버가 미인증 → 같은 유저 캐시 제거 + if (current.isVerified && current.memberId === memberId) { + current.reset(); } - // eslint-disable-next-line react-hooks/exhaustive-deps }, [ isProfileLoading, userProfile, memberId, - serverIsVerified, - serverPhoneNumber, resolvedServerIsVerified, + serverPhoneNumber, ]); // memberId 변경(계정 전환 / 로그아웃) 시 스토어 리셋 @@ -178,17 +176,17 @@ export function usePhoneVerificationStatus( prevMemberIdRef.current = memberId; if (prev !== null && prev !== memberId) { - store.reset(); + usePhoneVerificationStore.getState().reset(); } - // eslint-disable-next-line react-hooks/exhaustive-deps }, [memberId]); // 인증 완료 시 호출 — memberId를 자동 주입 const setVerified = useCallback( (phone: string) => { - store.setVerified(phone, memberId ?? undefined); + usePhoneVerificationStore + .getState() + .setVerified(phone, memberId ?? undefined); }, - // eslint-disable-next-line react-hooks/exhaustive-deps [memberId], ); diff --git a/src/features/study/group/ui/group-study-form-modal.tsx b/src/features/study/group/ui/group-study-form-modal.tsx index f38a8e63..b485fe7e 100644 --- a/src/features/study/group/ui/group-study-form-modal.tsx +++ b/src/features/study/group/ui/group-study-form-modal.tsx @@ -99,8 +99,6 @@ export default function GroupStudyFormModal({ }; const refineStudyDetail = (value: GroupStudyFullResponseDto) => { - if (isGroupStudyLoading) return; - const refinedClassification = value.basicInfo?.classification ?? classification; const originalType = value.basicInfo?.type; @@ -116,7 +114,7 @@ export default function GroupStudyFormModal({ return { classification: refinedClassification, studyLeaderParticipation: - value.basicInfo.studyLeaderParticipation ?? false, + value.basicInfo?.studyLeaderParticipation ?? false, type: refinedType, targetRoles: value.basicInfo?.targetRoles, maxMembersCount: value.basicInfo?.maxMembersCount?.toString() ?? '', @@ -232,6 +230,11 @@ export default function GroupStudyFormModal({ } }, [open, mode]); + const editDefaultValues = + mode === 'edit' && groupStudyInfo + ? refineStudyDetail(groupStudyInfo) + : null; + return ( <> - + {mode === 'create' && ( + + )} + {mode === 'edit' && isGroupStudyLoading && ( + + 스터디 정보를 불러오는 중입니다... + + )} + {mode === 'edit' && !isGroupStudyLoading && editDefaultValues && ( + + )}