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 && (
+
+ )}