Skip to content

Commit 2a84cce

Browse files
Merge pull request #382 from code-zero-to-one/fix/verify
본인인증 풀리는 문제 수정
2 parents d8f8a39 + 7a80cce commit 2a84cce

8 files changed

Lines changed: 149 additions & 13 deletions

File tree

src/components/home/study-matching-toggle.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
usePatchAutoMatchingMutation,
77
useUserProfileQuery,
88
} from '@/entities/user/model/use-user-profile-query';
9-
import { usePhoneVerificationStore } from '@/features/phone-verification/model/store';
9+
import { usePhoneVerificationStatus } from '@/features/phone-verification/model/use-phone-verification-status';
1010
import PhoneVerificationModal from '@/features/phone-verification/ui/phone-verification-modal';
1111
import StartStudyModal from '@/features/study/participation/ui/start-study-modal';
1212
import { useAuth } from '@/hooks/common/use-auth';
@@ -20,7 +20,9 @@ export default function StudyMatchingToggle() {
2020
const { mutate: patchAutoMatching, isPending } =
2121
usePatchAutoMatchingMutation();
2222

23-
const { isVerified, setVerified } = usePhoneVerificationStore();
23+
const { isVerified, setVerified } = usePhoneVerificationStatus(
24+
memberId ?? undefined,
25+
);
2426
const [isVerificationModalOpen, setIsVerificationModalOpen] = useState(false);
2527

2628
const [enabled, setEnabled] = useState(false);

src/entities/user/model/use-user-profile-query.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export const usePatchAutoMatchingMutation = () => {
3737
const prev = qc.getQueryData(['userProfile', memberId]);
3838
if (prev && typeof prev === 'object') {
3939
qc.setQueryData(['userProfile', memberId], {
40-
...(prev as any),
40+
...prev,
4141
autoMatching,
4242
});
4343
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
'use client';
2+
3+
import { useEffect, useMemo } from 'react';
4+
import { useUserProfileQuery } from '@/entities/user/model/use-user-profile-query';
5+
import { useAuth } from '@/hooks/common/use-auth';
6+
import { usePhoneVerificationStore } from './store';
7+
8+
/**
9+
* @deprecated usePhoneVerificationStore를 직접 사용하지 마세요.
10+
* 대신 usePhoneVerificationStatus 훅을 사용하세요.
11+
*
12+
* 이유:
13+
* - usePhoneVerificationStore는 localStorage 기반으로 서버 상태와 불일치할 수 있음
14+
* - usePhoneVerificationStatus는 서버 상태를 단일 진실 공급원으로 사용
15+
*
16+
* @see usePhoneVerificationStatus
17+
*/
18+
export { usePhoneVerificationStore } from './store';
19+
20+
/**
21+
* 본인인증 상태를 서버 데이터와 동기화하여 반환하는 훅
22+
*
23+
* 문제:
24+
* - Zustand의 localStorage 기반 상태는 다른 브라우저/기기에서의 인증 상태를 반영하지 못함
25+
* - localStorage가 삭제되면 인증이 풀린 것처럼 보임
26+
* - 다른 계정이 같은 번호로 인증하면 기존 계정의 인증이 해제되지만 클라이언트는 모름
27+
*
28+
* 해결:
29+
* - 서버의 userProfile.isVerified 또는 memberProfile.tel을 단일 진실 공급원으로 사용
30+
* - Zustand 상태는 UI 반응성을 위한 캐시로만 활용
31+
* - 서버 데이터로 Zustand 상태를 자동 동기화
32+
*
33+
* @param overrideMemberId - 특정 memberId로 조회할 때 사용 (선택)
34+
*/
35+
export function usePhoneVerificationStatus(overrideMemberId?: number) {
36+
const { data: authData } = useAuth();
37+
const memberId = overrideMemberId ?? authData?.memberId ?? null;
38+
39+
const { data: userProfile, isLoading: isProfileLoading } =
40+
useUserProfileQuery(memberId ?? 0);
41+
42+
const {
43+
isVerified: zustandIsVerified,
44+
phoneNumber: zustandPhoneNumber,
45+
setVerified,
46+
reset,
47+
} = usePhoneVerificationStore();
48+
49+
// 서버 데이터 기반 인증 상태 계산
50+
const serverIsVerified = userProfile?.isVerified ?? false;
51+
const serverPhoneNumber = userProfile?.memberProfile?.tel ?? null;
52+
53+
// 서버 데이터를 우선시하여 최종 인증 상태 결정
54+
// 서버에 tel이 있으면 인증된 것으로 간주 (isVerified보다 tel 존재 여부가 더 확실한 지표)
55+
const isVerified = useMemo(() => {
56+
// 프로필 로딩 중일 때는 Zustand 상태를 임시로 사용 (UI 깜빡임 방지)
57+
if (isProfileLoading && !userProfile) {
58+
return zustandIsVerified;
59+
}
60+
61+
// 서버에 전화번호가 있으면 인증 완료
62+
if (serverPhoneNumber) {
63+
return true;
64+
}
65+
66+
// 서버의 isVerified 플래그 확인
67+
if (serverIsVerified) {
68+
return true;
69+
}
70+
71+
// 서버 데이터가 없으면 미인증
72+
return false;
73+
}, [
74+
isProfileLoading,
75+
userProfile,
76+
zustandIsVerified,
77+
serverPhoneNumber,
78+
serverIsVerified,
79+
]);
80+
81+
const phoneNumber = useMemo(() => {
82+
if (isProfileLoading && !userProfile) {
83+
return zustandPhoneNumber;
84+
}
85+
86+
return serverPhoneNumber ?? null;
87+
}, [isProfileLoading, userProfile, zustandPhoneNumber, serverPhoneNumber]);
88+
89+
// 서버 데이터와 Zustand 상태 동기화
90+
useEffect(() => {
91+
if (isProfileLoading || !userProfile) return;
92+
93+
if (serverPhoneNumber) {
94+
// 서버에 전화번호가 있으면 Zustand도 동기화
95+
if (!zustandIsVerified || zustandPhoneNumber !== serverPhoneNumber) {
96+
setVerified(serverPhoneNumber);
97+
}
98+
} else if (!serverIsVerified) {
99+
// 서버에서 인증이 해제되었으면 Zustand도 초기화
100+
if (zustandIsVerified) {
101+
reset();
102+
}
103+
}
104+
}, [
105+
isProfileLoading,
106+
userProfile,
107+
serverPhoneNumber,
108+
serverIsVerified,
109+
zustandIsVerified,
110+
zustandPhoneNumber,
111+
setVerified,
112+
reset,
113+
]);
114+
115+
return {
116+
isVerified,
117+
phoneNumber,
118+
isLoading: isProfileLoading,
119+
// 원본 상태들 (디버깅용)
120+
_serverIsVerified: serverIsVerified,
121+
_serverPhoneNumber: serverPhoneNumber,
122+
_zustandIsVerified: zustandIsVerified,
123+
_zustandPhoneNumber: zustandPhoneNumber,
124+
// Zustand 액션 (인증 완료 시 호출용)
125+
setVerified,
126+
reset,
127+
};
128+
}

src/features/study/group/ui/apply-group-study-modal.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { useController, useForm } from 'react-hook-form';
88
import Button from '@/components/ui/button';
99
import Checkbox from '@/components/ui/checkbox';
1010
import { Modal } from '@/components/ui/modal';
11-
import { usePhoneVerificationStore } from '@/features/phone-verification/model/store';
11+
import { usePhoneVerificationStatus } from '@/features/phone-verification/model/use-phone-verification-status';
1212
import PhoneVerificationModal from '@/features/phone-verification/ui/phone-verification-modal';
1313
import { useToastStore } from '@/stores/use-toast-store';
1414
import { GroupStudyDetailResponse } from '../api/group-study-types';
@@ -34,7 +34,8 @@ export default function ApplyGroupStudyModal({
3434
onSuccess,
3535
}: ApplyGroupStudyModalProps) {
3636
const [open, setOpen] = useState<boolean>(false);
37-
const { isVerified, setVerified } = usePhoneVerificationStore();
37+
// 서버 상태와 동기화된 인증 상태 사용
38+
const { isVerified, setVerified } = usePhoneVerificationStatus();
3839
const [isVerificationModalOpen, setIsVerificationModalOpen] = useState(false);
3940

4041
const handleOpenChange = (isOpen: boolean) => {

src/features/study/group/ui/group-study-form-modal.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { useEffect, useState } from 'react';
88

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

@@ -57,7 +57,8 @@ export default function GroupStudyFormModal({
5757
refetch: refetchGroupStudyInfo,
5858
} = useGroupStudyDetailQuery(groupStudyId!);
5959

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

6364
const handleVerificationComplete = (phoneNumber: string) => {

src/features/study/participation/ui/reservation-list.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
useUserProfileQuery,
99
} from '@/entities/user/model/use-user-profile-query';
1010
import ProfileDefault from '@/entities/user/ui/icon/profile-default.svg';
11-
import { usePhoneVerificationStore } from '@/features/phone-verification/model/store';
11+
import { usePhoneVerificationStatus } from '@/features/phone-verification/model/use-phone-verification-status';
1212
import PhoneVerificationModal from '@/features/phone-verification/ui/phone-verification-modal';
1313
import ReservationCard from '@/features/study/participation/ui/reservation-user-card';
1414
import StartStudyModal from '@/features/study/participation/ui/start-study-modal';
@@ -36,7 +36,10 @@ export default function ReservationList({
3636
const { data: userProfile } = useUserProfileQuery(memberId ?? 0);
3737
const autoMatching = userProfile?.autoMatching ?? false;
3838

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

4245
const firstMemberId = useMemo(

src/features/study/participation/ui/start-study-modal.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import {
2323
useUpdateUserProfileInfoMutation,
2424
} from '@/features/my-page/model/use-update-user-profile-mutation';
2525

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

2929
import {
@@ -100,7 +100,8 @@ export default function StartStudyModal({
100100
open,
101101
onOpenChange,
102102
}: StartStudyModalProps) {
103-
const { isVerified, setVerified } = usePhoneVerificationStore();
103+
// 서버 상태와 동기화된 인증 상태 사용
104+
const { isVerified, setVerified } = usePhoneVerificationStatus(memberId);
104105
const [isVerificationModalOpen, setIsVerificationModalOpen] = useState(false);
105106

106107
const [internalOpen, setInternalOpen] = useState(false);

src/widgets/home/todo-list.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

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

@@ -17,7 +17,7 @@ const todoItems = [
1717
] as const;
1818

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

2323
const handleVerificationComplete = (phoneNumber: string) => {

0 commit comments

Comments
 (0)