Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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: 4 additions & 2 deletions src/components/home/study-matching-toggle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
usePatchAutoMatchingMutation,
useUserProfileQuery,
} from '@/entities/user/model/use-user-profile-query';
import { usePhoneVerificationStore } from '@/features/phone-verification/model/store';
import { usePhoneVerificationStatus } from '@/features/phone-verification/model/use-phone-verification-status';
import PhoneVerificationModal from '@/features/phone-verification/ui/phone-verification-modal';
import StartStudyModal from '@/features/study/participation/ui/start-study-modal';
import { useAuth } from '@/hooks/common/use-auth';
Expand All @@ -20,7 +20,9 @@ export default function StudyMatchingToggle() {
const { mutate: patchAutoMatching, isPending } =
usePatchAutoMatchingMutation();

const { isVerified, setVerified } = usePhoneVerificationStore();
const { isVerified, setVerified } = usePhoneVerificationStatus(
memberId ?? undefined,
);
const [isVerificationModalOpen, setIsVerificationModalOpen] = useState(false);

const [enabled, setEnabled] = useState(false);
Expand Down
2 changes: 1 addition & 1 deletion src/entities/user/model/use-user-profile-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export const usePatchAutoMatchingMutation = () => {
const prev = qc.getQueryData(['userProfile', memberId]);
if (prev && typeof prev === 'object') {
qc.setQueryData(['userProfile', memberId], {
...(prev as any),
...prev,
autoMatching,
});
}
Expand Down
128 changes: 128 additions & 0 deletions src/features/phone-verification/model/use-phone-verification-status.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
'use client';

import { useEffect, useMemo } from 'react';
import { useUserProfileQuery } from '@/entities/user/model/use-user-profile-query';
import { useAuth } from '@/hooks/common/use-auth';
import { usePhoneVerificationStore } from './store';

/**
* @deprecated usePhoneVerificationStore를 직접 사용하지 마세요.
* 대신 usePhoneVerificationStatus 훅을 사용하세요.
*
* 이유:
* - usePhoneVerificationStore는 localStorage 기반으로 서버 상태와 불일치할 수 있음
* - usePhoneVerificationStatus는 서버 상태를 단일 진실 공급원으로 사용
*
* @see usePhoneVerificationStatus
*/
export { usePhoneVerificationStore } from './store';

/**
* 본인인증 상태를 서버 데이터와 동기화하여 반환하는 훅
*
* 문제:
* - Zustand의 localStorage 기반 상태는 다른 브라우저/기기에서의 인증 상태를 반영하지 못함
* - localStorage가 삭제되면 인증이 풀린 것처럼 보임
* - 다른 계정이 같은 번호로 인증하면 기존 계정의 인증이 해제되지만 클라이언트는 모름
*
* 해결:
* - 서버의 userProfile.isVerified 또는 memberProfile.tel을 단일 진실 공급원으로 사용
* - Zustand 상태는 UI 반응성을 위한 캐시로만 활용
* - 서버 데이터로 Zustand 상태를 자동 동기화
*
* @param overrideMemberId - 특정 memberId로 조회할 때 사용 (선택)
*/
export function usePhoneVerificationStatus(overrideMemberId?: number) {
const { data: authData } = useAuth();
const memberId = overrideMemberId ?? authData?.memberId ?? null;

const { data: userProfile, isLoading: isProfileLoading } =
useUserProfileQuery(memberId ?? 0);

const {
isVerified: zustandIsVerified,
phoneNumber: zustandPhoneNumber,
setVerified,
reset,
} = usePhoneVerificationStore();

// 서버 데이터 기반 인증 상태 계산
const serverIsVerified = userProfile?.isVerified ?? false;
const serverPhoneNumber = userProfile?.memberProfile?.tel ?? null;

// 서버 데이터를 우선시하여 최종 인증 상태 결정
// 서버에 tel이 있으면 인증된 것으로 간주 (isVerified보다 tel 존재 여부가 더 확실한 지표)
const isVerified = useMemo(() => {
// 프로필 로딩 중일 때는 Zustand 상태를 임시로 사용 (UI 깜빡임 방지)
if (isProfileLoading && !userProfile) {
return zustandIsVerified;
}

// 서버에 전화번호가 있으면 인증 완료
if (serverPhoneNumber) {
return true;
}

// 서버의 isVerified 플래그 확인
if (serverIsVerified) {
return true;
}

// 서버 데이터가 없으면 미인증
return false;
}, [
isProfileLoading,
userProfile,
zustandIsVerified,
serverPhoneNumber,
serverIsVerified,
]);

const phoneNumber = useMemo(() => {
if (isProfileLoading && !userProfile) {
return zustandPhoneNumber;
}

return serverPhoneNumber ?? null;
}, [isProfileLoading, userProfile, zustandPhoneNumber, serverPhoneNumber]);

// 서버 데이터와 Zustand 상태 동기화
useEffect(() => {
if (isProfileLoading || !userProfile) return;

if (serverPhoneNumber) {
// 서버에 전화번호가 있으면 Zustand도 동기화
if (!zustandIsVerified || zustandPhoneNumber !== serverPhoneNumber) {
setVerified(serverPhoneNumber);
}
} else if (!serverIsVerified) {
// 서버에서 인증이 해제되었으면 Zustand도 초기화
if (zustandIsVerified) {
reset();
}
}
}, [
isProfileLoading,
userProfile,
serverPhoneNumber,
serverIsVerified,
zustandIsVerified,
zustandPhoneNumber,
setVerified,
reset,
]);

return {
isVerified,
phoneNumber,
isLoading: isProfileLoading,
// 원본 상태들 (디버깅용)
_serverIsVerified: serverIsVerified,
_serverPhoneNumber: serverPhoneNumber,
_zustandIsVerified: zustandIsVerified,
_zustandPhoneNumber: zustandPhoneNumber,
// Zustand 액션 (인증 완료 시 호출용)
setVerified,
reset,
};
}
5 changes: 3 additions & 2 deletions src/features/study/group/ui/apply-group-study-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { useController, useForm } from 'react-hook-form';
import Button from '@/components/ui/button';
import Checkbox from '@/components/ui/checkbox';
import { Modal } from '@/components/ui/modal';
import { usePhoneVerificationStore } from '@/features/phone-verification/model/store';
import { usePhoneVerificationStatus } from '@/features/phone-verification/model/use-phone-verification-status';
import PhoneVerificationModal from '@/features/phone-verification/ui/phone-verification-modal';
import { GroupStudyDetailResponse } from '../api/group-study-types';
import {
Expand All @@ -33,7 +33,8 @@ export default function ApplyGroupStudyModal({
onSuccess,
}: ApplyGroupStudyModalProps) {
const [open, setOpen] = useState<boolean>(false);
const { isVerified, setVerified } = usePhoneVerificationStore();
// 서버 상태와 동기화된 인증 상태 사용
const { isVerified, setVerified } = usePhoneVerificationStatus();
const [isVerificationModalOpen, setIsVerificationModalOpen] = useState(false);

const handleOpenChange = (isOpen: boolean) => {
Expand Down
5 changes: 3 additions & 2 deletions src/features/study/group/ui/group-study-form-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { useEffect, useState } from 'react';

import { GroupStudyFullResponseDto } from '@/api/openapi';
import { Modal } from '@/components/ui/modal';
import { usePhoneVerificationStore } from '@/features/phone-verification/model/store';
import { usePhoneVerificationStatus } from '@/features/phone-verification/model/use-phone-verification-status';
import PhoneVerificationModal from '@/features/phone-verification/ui/phone-verification-modal';
import GroupStudyForm from './group-study-form';

Expand Down Expand Up @@ -57,7 +57,8 @@ export default function GroupStudyFormModal({
refetch: refetchGroupStudyInfo,
} = useGroupStudyDetailQuery(groupStudyId!);

const { isVerified, setVerified } = usePhoneVerificationStore();
// 서버 상태와 동기화된 인증 상태 사용
const { isVerified, setVerified } = usePhoneVerificationStatus();
const [isVerificationModalOpen, setIsVerificationModalOpen] = useState(false);

const handleVerificationComplete = (phoneNumber: string) => {
Expand Down
7 changes: 5 additions & 2 deletions src/features/study/participation/ui/reservation-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
useUserProfileQuery,
} from '@/entities/user/model/use-user-profile-query';
import ProfileDefault from '@/entities/user/ui/icon/profile-default.svg';
import { usePhoneVerificationStore } from '@/features/phone-verification/model/store';
import { usePhoneVerificationStatus } from '@/features/phone-verification/model/use-phone-verification-status';
import PhoneVerificationModal from '@/features/phone-verification/ui/phone-verification-modal';
import ReservationCard from '@/features/study/participation/ui/reservation-user-card';
import StartStudyModal from '@/features/study/participation/ui/start-study-modal';
Expand Down Expand Up @@ -36,7 +36,10 @@ export default function ReservationList({
const { data: userProfile } = useUserProfileQuery(memberId ?? 0);
const autoMatching = userProfile?.autoMatching ?? false;

const { isVerified, setVerified } = usePhoneVerificationStore();
// 서버 상태와 동기화된 인증 상태 사용
const { isVerified, setVerified } = usePhoneVerificationStatus(
memberId ?? undefined,
);
const [isVerificationModalOpen, setIsVerificationModalOpen] = useState(false);

const firstMemberId = useMemo(
Expand Down
5 changes: 3 additions & 2 deletions src/features/study/participation/ui/start-study-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {
useUpdateUserProfileInfoMutation,
} from '@/features/my-page/model/use-update-user-profile-mutation';

import { usePhoneVerificationStore } from '@/features/phone-verification/model/store';
import { usePhoneVerificationStatus } from '@/features/phone-verification/model/use-phone-verification-status';
import PhoneVerificationModal from '@/features/phone-verification/ui/phone-verification-modal';

import {
Expand Down Expand Up @@ -99,7 +99,8 @@ export default function StartStudyModal({
open,
onOpenChange,
}: StartStudyModalProps) {
const { isVerified, setVerified } = usePhoneVerificationStore();
// 서버 상태와 동기화된 인증 상태 사용
const { isVerified, setVerified } = usePhoneVerificationStatus(memberId);
const [isVerificationModalOpen, setIsVerificationModalOpen] = useState(false);

const [internalOpen, setInternalOpen] = useState(false);
Expand Down
4 changes: 2 additions & 2 deletions src/widgets/home/todo-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { useState } from 'react';
import Button from '@/components/ui/button';
import { usePhoneVerificationStore } from '@/features/phone-verification/model/store';
import { usePhoneVerificationStatus } from '@/features/phone-verification/model/use-phone-verification-status';
import PhoneVerificationModal from '@/features/phone-verification/ui/phone-verification-modal';
import CheckIcon from 'public/icons/check.svg';

Expand All @@ -17,7 +17,7 @@ const todoItems = [
] as const;

export default function TodoList({ statusList }: TodoListProps) {
const { isVerified, setVerified } = usePhoneVerificationStore();
const { isVerified, setVerified } = usePhoneVerificationStatus();
const [isVerificationModalOpen, setIsVerificationModalOpen] = useState(false);

const handleVerificationComplete = (phoneNumber: string) => {
Expand Down
Loading