From 4a2feb16eeed3db2af673a2f9024dd7d7e1ff2cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=97=B0=EC=A7=84?= Date: Tue, 12 Aug 2025 11:13:50 +0900 Subject: [PATCH 01/18] =?UTF-8?q?feat:=202=EC=B0=A8=20=ED=94=BC=EB=93=9C?= =?UTF-8?q?=EB=B0=B1=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- eslint.config.mjs | 1 + src/App.tsx | 9 +- src/components/dateCourse/dateCourse.tsx | 1 - .../dateCourseSearchFilterOption.tsx | 2 +- src/firebase/firebase.ts | 44 +++++-- src/hooks/alarm/useDeviceToken.ts | 39 ------ src/hooks/alarm/usePostDeviceToken.ts | 8 ++ src/hooks/customQuery.ts | 54 +++++++- src/pages/home/HomePage.tsx | 19 ++- src/providers/deviceTokenProvider.tsx | 122 ++++++++++++++++++ src/queryKey/queryKey.ts | 17 ++- src/types/common/common.ts | 23 +++- 12 files changed, 273 insertions(+), 66 deletions(-) delete mode 100644 src/hooks/alarm/useDeviceToken.ts create mode 100644 src/hooks/alarm/usePostDeviceToken.ts create mode 100644 src/providers/deviceTokenProvider.tsx diff --git a/eslint.config.mjs b/eslint.config.mjs index 3787778..2dc3211 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -34,6 +34,7 @@ export default [ { rules: { 'prettier/prettier': 'warn', + 'no-console': ['error', { allow: ['warn', 'error'] }], }, }, tsConfig, diff --git a/src/App.tsx b/src/App.tsx index d4db07f..b1442b9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,10 +2,17 @@ import './App.css'; import { RouterProvider } from 'react-router-dom'; +import { DeviceTokenProvider } from './providers/deviceTokenProvider'; +import { alarmKeys } from './queryKey/queryKey'; + import router from '@/routes/routes'; function App() { - return ; + return ( + + + + ); } export default App; diff --git a/src/components/dateCourse/dateCourse.tsx b/src/components/dateCourse/dateCourse.tsx index cc2b5f5..f144982 100644 --- a/src/components/dateCourse/dateCourse.tsx +++ b/src/components/dateCourse/dateCourse.tsx @@ -20,7 +20,6 @@ function DateCourse({ defaultOpen = false }: { defaultOpen?: boolean }) { } else { setIsBookmarked(!isBookmarked); } - // console.log('북마크 해제'); }; useEffect(() => { diff --git a/src/components/dateCourse/dateCourseSearchFilterOption.tsx b/src/components/dateCourse/dateCourseSearchFilterOption.tsx index 38b8c80..44eab4b 100644 --- a/src/components/dateCourse/dateCourseSearchFilterOption.tsx +++ b/src/components/dateCourse/dateCourseSearchFilterOption.tsx @@ -88,7 +88,7 @@ export default function DateCourseSearchFilterOption({ options, type, value, onC mode="search" onSearchClick={handleSearch} placeholder="ex: 서울시 강남구" - className="w-full" + className="!w-full min-w-full" value={inputValue} onChange={handleInputChange} /> diff --git a/src/firebase/firebase.ts b/src/firebase/firebase.ts index 2762064..afefeff 100644 --- a/src/firebase/firebase.ts +++ b/src/firebase/firebase.ts @@ -1,6 +1,7 @@ // src/firebase/firebase.ts import { initializeApp } from 'firebase/app'; -import { getMessaging, getToken } from 'firebase/messaging'; +import type { Messaging } from 'firebase/messaging'; +import { deleteToken, getMessaging, getToken, isSupported } from 'firebase/messaging'; const firebaseConfig = { apiKey: import.meta.env.VITE_FIREBASE_API_KEY, @@ -13,22 +14,34 @@ const firebaseConfig = { }; const app = initializeApp(firebaseConfig); -const messaging = getMessaging(app); +export let messaging: Messaging | null = null; +(async () => { + if (await isSupported()) { + messaging = getMessaging(app); + } +})(); + +export async function generateToken(): Promise { + if (!(await isSupported())) return null; + if (!messaging) messaging = getMessaging(app); + + // 권한 요청 (이미 허용/거부된 상태면 브라우저가 적절히 동작) + if ('Notification' in window && Notification.permission !== 'granted') { + const perm = await Notification.requestPermission(); + if (perm !== 'granted') return null; + } -export const generateToken = async () => { try { const token = await getToken(messaging, { vapidKey: import.meta.env.VITE_FIREBASE_VAPID_KEY, + serviceWorkerRegistration: await navigator.serviceWorker.getRegistration(), }); - if (!token) { - console.warn('FCM 토큰 생성에 실패했습니다. 알림 권한을 확인해주세요.'); - } - return token; - } catch (error) { - console.error('FCM 토큰 생성 중 오류 발생:', error); + return token ?? null; + } catch (e) { + console.error('FCM getToken 실패:', e); return null; } -}; +} export const registerServiceWorker = async () => { try { @@ -39,3 +52,14 @@ export const registerServiceWorker = async () => { console.error('Service Worker registration failed:', err); } }; + +export async function deleteFcmToken(): Promise { + if (!(await isSupported())) return false; + if (!messaging) messaging = getMessaging(app); + try { + return await deleteToken(messaging); + } catch (e) { + console.error('FCM deleteToken 실패:', e); + return false; + } +} diff --git a/src/hooks/alarm/useDeviceToken.ts b/src/hooks/alarm/useDeviceToken.ts deleted file mode 100644 index 73523c4..0000000 --- a/src/hooks/alarm/useDeviceToken.ts +++ /dev/null @@ -1,39 +0,0 @@ -// src/hooks/alarm/useDeviceToken.ts -import { useEffect } from 'react'; -import { isSupported } from 'firebase/messaging'; - -import { postDeviceToken } from '@/api/alarm/alarm'; // 서버에 FCM 토큰 전송하는 API 함수 -import { generateToken, registerServiceWorker } from '@/firebase/firebase'; - -export const useDeviceToken = () => { - useEffect(() => { - const setupFCM = async () => { - if (!(await isSupported())) { - console.warn('FCM은 현재 브라우저에서 지원되지 않습니다.'); - return; - } - - await registerServiceWorker(); - const token = await generateToken(); - - if (token) { - try { - await postDeviceToken({ deviceToken: token }); // 서버에 전송 - } catch (err) { - console.error('디바이스 토큰 서버 전송 실패:', err); - } - } - }; - - const handleClick = () => { - setupFCM(); - window.removeEventListener('click', handleClick); - }; - - window.addEventListener('click', handleClick); - - return () => { - window.removeEventListener('click', handleClick); - }; - }, []); -}; diff --git a/src/hooks/alarm/usePostDeviceToken.ts b/src/hooks/alarm/usePostDeviceToken.ts new file mode 100644 index 0000000..5034847 --- /dev/null +++ b/src/hooks/alarm/usePostDeviceToken.ts @@ -0,0 +1,8 @@ +import { useCoreMutation } from '../customQuery'; + +import { postDeviceToken } from '@/api/alarm/alarm'; + +export function useFirebase() { + const usePostDeviceToken = useCoreMutation(postDeviceToken); + return { usePostDeviceToken }; +} diff --git a/src/hooks/customQuery.ts b/src/hooks/customQuery.ts index c5b6ace..e5600ab 100644 --- a/src/hooks/customQuery.ts +++ b/src/hooks/customQuery.ts @@ -1,9 +1,10 @@ import { type MutationFunction, type QueryFunction, type QueryKey, useMutation, useQuery, type UseQueryResult } from '@tanstack/react-query'; import type { AxiosError } from 'axios'; -import { toast } from 'sonner'; import type { TUseMutationCustomOptions, TUseQueryCustomOptions } from '@/types/common/common'; +import { queryClient } from '@/api/queryClient'; + export function useCoreQuery( keyName: QueryKey, query: QueryFunction, @@ -17,12 +18,53 @@ export function useCoreQuery( }); } -export function useCoreMutation(mutation: MutationFunction, options?: TUseMutationCustomOptions) { - return useMutation({ +export function useCoreMutation( + mutation: MutationFunction, + options?: TUseMutationCustomOptions, { prevData?: unknown }>, +) { + const qc = queryClient; + + const { + optimisticUpdate, + invalidateKeys, + userOnError, + userOnSuccess, + ...rest // retry, gcTime 등 표준 옵션 + } = options ?? {}; + + return useMutation, TVariables, { prevData?: unknown }>({ mutationFn: mutation, - onError: (error) => { - toast.error(error.response?.data.message || 'An error occurred.'); + + onMutate: async (vars) => { + if (!optimisticUpdate) return {}; + await qc.cancelQueries({ queryKey: optimisticUpdate.key }); + const prevData = qc.getQueryData(optimisticUpdate.key); + qc.setQueryData(optimisticUpdate.key, (old: any) => optimisticUpdate.updateFn(old, vars)); + return { prevData }; }, - ...options, + + onError: (error, vars, ctx) => { + // 롤백 + if (optimisticUpdate && ctx?.prevData !== undefined) { + qc.setQueryData(optimisticUpdate.key, ctx.prevData); + } + + // 사용자 콜백 위임 + userOnError?.(error, vars, ctx); + }, + + onSuccess: async (data, vars, ctx) => { + // invalidate + if (invalidateKeys?.length) { + for (const key of invalidateKeys) { + await qc.invalidateQueries({ queryKey: key }); + } + } + // 사용자 콜백 위임 + userOnSuccess?.(data, vars, ctx); + }, + + // 나머지 표준 옵션 주입 + ...rest, }); } diff --git a/src/pages/home/HomePage.tsx b/src/pages/home/HomePage.tsx index 619d3b3..e153233 100644 --- a/src/pages/home/HomePage.tsx +++ b/src/pages/home/HomePage.tsx @@ -1,4 +1,4 @@ -import { useDeviceToken } from '@/hooks/alarm/useDeviceToken'; +import { useEffect } from 'react'; import Banner from '@/components/home/banner'; import DateCourseStore from '@/components/home/dateCourseStore'; @@ -9,8 +9,23 @@ import MainInfo from '@/components/home/info'; import Level from '@/components/home/level'; import WordCloudCard from '@/components/home/wordCloud'; +import { useDeviceTokenContext } from '@/providers/deviceTokenProvider'; + function Home() { - useDeviceToken(); + const { requestAndRegister } = useDeviceTokenContext(); + + useEffect(() => { + const fire = () => { + requestAndRegister().catch((err) => { + console.error('Device token 등록 실패:', err); + }); + }; + + window.addEventListener('pointerdown', fire, { once: true }); + return () => { + window.removeEventListener('pointerdown', fire); + }; + }, [requestAndRegister]); return (
diff --git a/src/providers/deviceTokenProvider.tsx b/src/providers/deviceTokenProvider.tsx new file mode 100644 index 0000000..37baf1a --- /dev/null +++ b/src/providers/deviceTokenProvider.tsx @@ -0,0 +1,122 @@ +// src/providers/DeviceTokenProvider.tsx +import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; +import { type QueryKey } from '@tanstack/react-query'; +import { isSupported, onMessage } from 'firebase/messaging'; + +import { useFirebase } from '@/hooks/alarm/usePostDeviceToken'; + +import { queryClient } from '@/api/queryClient'; +import { deleteFcmToken, generateToken, messaging, registerServiceWorker } from '@/firebase/firebase'; + +type TDeviceTokenContextValue = { + token: string | null; + supported: boolean | null; + permission: NotificationPermission | null; + requestAndRegister: () => Promise; + unregisterToken: () => Promise; +}; + +const DeviceTokenContext = createContext(null); + +type TProps = { + children: React.ReactNode; + refetchKeys?: QueryKey[]; + onForegroundMessage?: (payload: unknown) => void; +}; + +export function DeviceTokenProvider({ children, refetchKeys = [], onForegroundMessage }: TProps) { + const qc = queryClient; + const [token, setToken] = useState(null); + const [supported, setSupported] = useState(null); + const [permission, setPermission] = useState(null); + const messageUnsubRef = useRef<(() => void) | null>(null); + const initOnceRef = useRef(false); + const { usePostDeviceToken } = useFirebase(); + const { mutate: usePostDeviceTokenMutate } = usePostDeviceToken; + useEffect(() => { + (async () => { + const ok = await isSupported().catch(() => false); + setSupported(ok); + setPermission(typeof window !== 'undefined' && 'Notification' in window ? Notification.permission : null); + })(); + }, []); + + const wireOnMessage = useCallback(() => { + if (!messaging || messageUnsubRef.current) return; + const unsub = onMessage(messaging, (payload) => { + refetchKeys.forEach((key) => { + qc.invalidateQueries({ queryKey: key }); + }); + onForegroundMessage?.(payload); + }); + messageUnsubRef.current = unsub; + }, [onForegroundMessage, qc, refetchKeys]); + + const requestAndRegister = useCallback(async () => { + if (supported === false) { + console.warn('FCM은 현재 브라우저에서 지원되지 않습니다.'); + return; + } + if (initOnceRef.current) return; + initOnceRef.current = true; + + try { + await registerServiceWorker(); + const newToken = await generateToken(); + setPermission(typeof window !== 'undefined' ? Notification.permission : null); + + if (newToken) { + setToken(newToken); + usePostDeviceTokenMutate( + { deviceToken: newToken }, + { + onError: () => { + console.warn('FCM 토큰 등록 실패'); + initOnceRef.current = false; + }, + onSuccess: () => { + initOnceRef.current = true; + }, + }, + ); + wireOnMessage(); + } else { + console.warn('FCM 토큰 발급 실패 또는 권한 거부.'); + initOnceRef.current = false; + } + } catch (err) { + console.error('FCM 초기화 실패:', err); + initOnceRef.current = false; + } + }, [supported, wireOnMessage]); + + const unregisterToken = useCallback(async () => { + try { + await deleteFcmToken().catch(() => {}); + } finally { + setToken(null); + initOnceRef.current = false; + for (const key of refetchKeys) qc.invalidateQueries({ queryKey: key }); + } + }, [qc, refetchKeys]); + + useEffect(() => { + return () => { + messageUnsubRef.current?.(); + messageUnsubRef.current = null; + }; + }, []); + + const value = useMemo( + () => ({ token, supported, permission, requestAndRegister, unregisterToken }), + [token, supported, permission, requestAndRegister, unregisterToken], + ); + + return {children}; +} + +export function useDeviceTokenContext() { + const ctx = useContext(DeviceTokenContext); + if (!ctx) throw new Error('useDeviceTokenContext must be used within DeviceTokenProvider'); + return ctx; +} diff --git a/src/queryKey/queryKey.ts b/src/queryKey/queryKey.ts index c964258..2726c3b 100644 --- a/src/queryKey/queryKey.ts +++ b/src/queryKey/queryKey.ts @@ -1,9 +1,22 @@ import { createQueryKeys } from '@lukemorales/query-key-factory'; +import type { UseQueryOptions } from '@tanstack/react-query'; export const regionKeys = createQueryKeys('region', { - search: (keyword: string) => [keyword], + all: () => ({ + queryKey: ['region'], + }), + search: (keyword: string) => + ({ + queryKey: ['region', 'search', keyword] as const, + }) satisfies UseQueryOptions, }); export const alarmKeys = createQueryKeys('alarm', { - getAlarm: (size: number, cursor?: number) => [size, cursor], + all: () => ({ + queryKey: ['alarm'], + }), + getAlarm: (size: number, cursor?: number) => + ({ + queryKey: ['alarm', 'getAlarm', size, cursor] as const, + }) satisfies UseQueryOptions, }); diff --git a/src/types/common/common.ts b/src/types/common/common.ts index 535d46e..953e57a 100644 --- a/src/types/common/common.ts +++ b/src/types/common/common.ts @@ -14,10 +14,25 @@ export type TResponseError = AxiosError<{ error: string; }>; -export type TUseMutationCustomOptions = Omit< - UseMutationOptions, - 'mutationFn' ->; +export type TOptimisticUpdate = { + key: QueryKey; // 예: ['post', postId] + updateFn: (old: TCache | undefined, vars: TVariables) => TCache; +}; + +export type TUseMutationCustomOptions< + TData = unknown, + TVariables = void, + TError = AxiosError<{ message?: string }>, // message 타입 명시 + TContext = { prevData?: unknown }, +> = Omit, 'mutationFn' | 'onMutate' | 'onError' | 'onSuccess'> & { + optimisticUpdate?: TOptimisticUpdate; + invalidateKeys?: QueryKey[]; + silentError?: boolean; + + /** 사용자 정의 콜백 — 내부 기본 동작 후 호출 */ + userOnError?: UseMutationOptions['onError']; + userOnSuccess?: UseMutationOptions['onSuccess']; +}; export type TUseQueryCustomOptions = Omit< UseQueryOptions, From 2df07eb7132899df979fedee9190db1dc3d629fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=97=B0=EC=A7=84?= Date: Wed, 13 Aug 2025 01:18:01 +0900 Subject: [PATCH 02/18] =?UTF-8?q?fix:=20=EC=BD=98=EC=86=94=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/axiosInstance.ts | 9 ++++----- src/components/common/PasswordEdit.tsx | 2 -- src/components/modal/dateCourseSearchFilterModal.tsx | 1 - src/components/settingTab/InfoSetting.tsx | 2 +- src/pages/TestInputPage.tsx | 6 ++---- src/pages/auth/LoginPage.tsx | 2 +- src/pages/auth/UserSetting.tsx | 2 +- src/pages/notice/Notice.tsx | 2 -- src/pages/question/Question.tsx | 2 +- 9 files changed, 10 insertions(+), 18 deletions(-) diff --git a/src/api/axiosInstance.ts b/src/api/axiosInstance.ts index 14b53f7..115242f 100644 --- a/src/api/axiosInstance.ts +++ b/src/api/axiosInstance.ts @@ -34,7 +34,6 @@ axiosInstance.interceptors.response.use( const refreshResponse = await refresh(); if (refreshResponse.code === '200') { - console.log('refreshToken이 재발급 되었습니다'); isRedirecting = false; return axiosInstance(error.config); @@ -43,20 +42,20 @@ axiosInstance.interceptors.response.use( if (axios.isAxiosError(errors)) { const refreshError = error as AxiosError; if (refreshError.response?.data.message === 'The token is null.') { - console.log('refreshToken이 없습니다. 로그인 페이지로 이동합니다.'); + console.error('refreshToken이 없습니다. 로그인 페이지로 이동합니다.'); } else if (refreshError.response?.data.message === 'The token is invalid.') { - console.log('refreshToken이 만료되었습니다. 로그인 페이지로 이동합니다.'); + console.error('refreshToken이 만료되었습니다. 로그인 페이지로 이동합니다.'); logout(); } else { if (refreshError.response?.data.message === 'Incorrect password.') { alert('Your email or password is incorrect.'); } else { - console.log('알 수 없는 오류가 발생했습니다', errors); + console.error('알 수 없는 오류가 발생했습니다', errors); logout(); } } } else { - console.log('알 수 없는 오류가 발생했습니다', errors); + console.error('알 수 없는 오류가 발생했습니다', errors); logout(); } diff --git a/src/components/common/PasswordEdit.tsx b/src/components/common/PasswordEdit.tsx index 19bd430..bfba95a 100644 --- a/src/components/common/PasswordEdit.tsx +++ b/src/components/common/PasswordEdit.tsx @@ -57,8 +57,6 @@ export default function PasswordEditSection() { setErrors(newErrors); return; } - - console.log('비밀번호 변경 요청:', { currentPw, newPw }); setIsEditing(false); setErrors({}); }; diff --git a/src/components/modal/dateCourseSearchFilterModal.tsx b/src/components/modal/dateCourseSearchFilterModal.tsx index 17f7163..c98fd77 100644 --- a/src/components/modal/dateCourseSearchFilterModal.tsx +++ b/src/components/modal/dateCourseSearchFilterModal.tsx @@ -43,7 +43,6 @@ export default function DateCourseSearchFilterModal({ onClose }: TDateCourseSear const [errorMessages, setErrorMessages] = useState(Array(7).fill('')); const handleSearch = () => { - console.log('선택된 필터:', answers); onClose(); }; diff --git a/src/components/settingTab/InfoSetting.tsx b/src/components/settingTab/InfoSetting.tsx index aa29684..e1dce8a 100644 --- a/src/components/settingTab/InfoSetting.tsx +++ b/src/components/settingTab/InfoSetting.tsx @@ -27,7 +27,7 @@ export default function InfoSetting() { value={nickname} onChange={(e) => setNickname(e.target.value)} onCancel={() => setNickname('')} - onSubmit={() => console.log('닉네임 저장:', nickname)} + onSubmit={() => {}} /> {/* 이메일 - 수정 불가능 */} setSearch(e.target.value)} // 입력 상태 저장 - onSearchClick={() => { - console.log('검색 실행:', search); // 검색 아이콘 클릭 시 예시 - }} + onSearchClick={() => {}} />
@@ -38,7 +36,7 @@ export default function TestInputPage() { value={nickname} onChange={(e) => setNickname(e.target.value)} onCancel={() => setNickname('')} - onSubmit={() => console.log('닉네임 저장:', nickname)} + onSubmit={() => {}} /> diff --git a/src/pages/auth/LoginPage.tsx b/src/pages/auth/LoginPage.tsx index d7db6cd..d1efcce 100644 --- a/src/pages/auth/LoginPage.tsx +++ b/src/pages/auth/LoginPage.tsx @@ -55,7 +55,7 @@ export default function Login() { navigate('/home'); }, onError: (err) => { - console.log(err.response?.data.message); + console.error(err.response?.data.message); setError('잘못된 정보를 입력하였습니다.'); }, }, diff --git a/src/pages/auth/UserSetting.tsx b/src/pages/auth/UserSetting.tsx index 22102e9..728d534 100644 --- a/src/pages/auth/UserSetting.tsx +++ b/src/pages/auth/UserSetting.tsx @@ -62,7 +62,7 @@ export default function User() { navigate('/home'); }, onError: (err) => { - console.log(err); + console.error(err); setError(err.response?.data.message!); }, }, diff --git a/src/pages/notice/Notice.tsx b/src/pages/notice/Notice.tsx index 884070a..070f26c 100644 --- a/src/pages/notice/Notice.tsx +++ b/src/pages/notice/Notice.tsx @@ -37,8 +37,6 @@ export default function Notice() { size: itemsPerPage, }); - console.log('API 응답:', response); - // 공지 목록과 페이지 수 설정 (빈 배열도 허용) setNoticeList(response.result.noticeList ?? []); setTotalPages(response.result.totalPages ?? 1); diff --git a/src/pages/question/Question.tsx b/src/pages/question/Question.tsx index 559084d..b453671 100644 --- a/src/pages/question/Question.tsx +++ b/src/pages/question/Question.tsx @@ -67,7 +67,7 @@ export default function Question() { setSearchValue(e.target.value); setCurrentPage(1); }} - onSearchClick={() => console.log('검색 실행:', searchValue)} + onSearchClick={() => {}} placeholder="내용을 검색하세요" className="mb-8" /> From ba6e8a6a791eefc7751d1ac8ed23685054a6e0dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=97=B0=EC=A7=84?= Date: Wed, 13 Aug 2025 01:23:48 +0900 Subject: [PATCH 03/18] =?UTF-8?q?fix:=20=EC=BD=98=EC=86=94=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/notice/Notice.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/notice/Notice.tsx b/src/pages/notice/Notice.tsx index 070f26c..4a553b0 100644 --- a/src/pages/notice/Notice.tsx +++ b/src/pages/notice/Notice.tsx @@ -43,7 +43,7 @@ export default function Notice() { } catch (err) { // 오류 처리 setError('공지사항을 불러오는 데 실패했습니다.'); - console.log(err); + console.error(err); } finally { setLoading(false); } @@ -64,7 +64,7 @@ export default function Notice() { mode="search" value={searchValue} onChange={(e) => setSearchValue(e.target.value)} - onSearchClick={() => console.log('검색 실행:', searchValue)} + onSearchClick={() => {}} placeholder="찾으시는 내용을 입력해주세요." className="mb-8" /> From d8e066b3d754507e53564c3063229e16f8ba892c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=97=B0=EC=A7=84?= Date: Wed, 13 Aug 2025 13:34:40 +0900 Subject: [PATCH 04/18] =?UTF-8?q?fix:=20=EC=BD=94=EB=93=9C=20=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=ED=94=BC=EB=93=9C=EB=B0=B1=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 4 +++- src/api/axiosInstance.ts | 9 +++------ src/firebase/firebase.ts | 2 +- src/hooks/alarm/usePostDeviceToken.ts | 4 ++-- src/hooks/customQuery.ts | 14 +++++--------- src/pages/auth/UserSetting.tsx | 2 +- src/providers/deviceTokenProvider.tsx | 10 ++++++---- src/queryKey/queryKey.ts | 14 ++++++++------ src/types/common/common.ts | 12 +++++------- 9 files changed, 34 insertions(+), 37 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index b1442b9..ecbe19e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,6 @@ import './App.css'; +import { useMemo } from 'react'; import { RouterProvider } from 'react-router-dom'; import { DeviceTokenProvider } from './providers/deviceTokenProvider'; @@ -8,8 +9,9 @@ import { alarmKeys } from './queryKey/queryKey'; import router from '@/routes/routes'; function App() { + const refetchKeys = useMemo(() => [alarmKeys.all().queryKey], []); return ( - + ); diff --git a/src/api/axiosInstance.ts b/src/api/axiosInstance.ts index 115242f..d3192a1 100644 --- a/src/api/axiosInstance.ts +++ b/src/api/axiosInstance.ts @@ -43,16 +43,13 @@ axiosInstance.interceptors.response.use( const refreshError = error as AxiosError; if (refreshError.response?.data.message === 'The token is null.') { console.error('refreshToken이 없습니다. 로그인 페이지로 이동합니다.'); + logout(); } else if (refreshError.response?.data.message === 'The token is invalid.') { console.error('refreshToken이 만료되었습니다. 로그인 페이지로 이동합니다.'); logout(); } else { - if (refreshError.response?.data.message === 'Incorrect password.') { - alert('Your email or password is incorrect.'); - } else { - console.error('알 수 없는 오류가 발생했습니다', errors); - logout(); - } + console.error('알 수 없는 오류가 발생했습니다', errors); + logout(); } } else { console.error('알 수 없는 오류가 발생했습니다', errors); diff --git a/src/firebase/firebase.ts b/src/firebase/firebase.ts index afefeff..1414eae 100644 --- a/src/firebase/firebase.ts +++ b/src/firebase/firebase.ts @@ -34,7 +34,7 @@ export async function generateToken(): Promise { try { const token = await getToken(messaging, { vapidKey: import.meta.env.VITE_FIREBASE_VAPID_KEY, - serviceWorkerRegistration: await navigator.serviceWorker.getRegistration(), + serviceWorkerRegistration: await navigator.serviceWorker.ready, }); return token ?? null; } catch (e) { diff --git a/src/hooks/alarm/usePostDeviceToken.ts b/src/hooks/alarm/usePostDeviceToken.ts index 5034847..94509c4 100644 --- a/src/hooks/alarm/usePostDeviceToken.ts +++ b/src/hooks/alarm/usePostDeviceToken.ts @@ -3,6 +3,6 @@ import { useCoreMutation } from '../customQuery'; import { postDeviceToken } from '@/api/alarm/alarm'; export function useFirebase() { - const usePostDeviceToken = useCoreMutation(postDeviceToken); - return { usePostDeviceToken }; + const postDeviceTokenMutation = useCoreMutation(postDeviceToken); + return { postDeviceTokenMutation }; } diff --git a/src/hooks/customQuery.ts b/src/hooks/customQuery.ts index e5600ab..93d875e 100644 --- a/src/hooks/customQuery.ts +++ b/src/hooks/customQuery.ts @@ -22,8 +22,6 @@ export function useCoreMutation( mutation: MutationFunction, options?: TUseMutationCustomOptions, { prevData?: unknown }>, ) { - const qc = queryClient; - const { optimisticUpdate, invalidateKeys, @@ -37,16 +35,16 @@ export function useCoreMutation( onMutate: async (vars) => { if (!optimisticUpdate) return {}; - await qc.cancelQueries({ queryKey: optimisticUpdate.key }); - const prevData = qc.getQueryData(optimisticUpdate.key); - qc.setQueryData(optimisticUpdate.key, (old: any) => optimisticUpdate.updateFn(old, vars)); + await queryClient.cancelQueries({ queryKey: optimisticUpdate.key }); + const prevData = queryClient.getQueryData(optimisticUpdate.key); + queryClient.setQueryData(optimisticUpdate.key, (old: unknown) => optimisticUpdate.updateFn(old, vars)); return { prevData }; }, onError: (error, vars, ctx) => { // 롤백 if (optimisticUpdate && ctx?.prevData !== undefined) { - qc.setQueryData(optimisticUpdate.key, ctx.prevData); + queryClient.setQueryData(optimisticUpdate.key, ctx.prevData); } // 사용자 콜백 위임 @@ -56,9 +54,7 @@ export function useCoreMutation( onSuccess: async (data, vars, ctx) => { // invalidate if (invalidateKeys?.length) { - for (const key of invalidateKeys) { - await qc.invalidateQueries({ queryKey: key }); - } + await Promise.all(invalidateKeys.map((key) => queryClient.invalidateQueries({ queryKey: key }))); } // 사용자 콜백 위임 userOnSuccess?.(data, vars, ctx); diff --git a/src/pages/auth/UserSetting.tsx b/src/pages/auth/UserSetting.tsx index 728d534..b7c494e 100644 --- a/src/pages/auth/UserSetting.tsx +++ b/src/pages/auth/UserSetting.tsx @@ -63,7 +63,7 @@ export default function User() { }, onError: (err) => { console.error(err); - setError(err.response?.data.message!); + setError(err.response?.data.message || '회원가입 중 문제가 발생했습니다.'); }, }, ); diff --git a/src/providers/deviceTokenProvider.tsx b/src/providers/deviceTokenProvider.tsx index 37baf1a..6bb983f 100644 --- a/src/providers/deviceTokenProvider.tsx +++ b/src/providers/deviceTokenProvider.tsx @@ -31,8 +31,8 @@ export function DeviceTokenProvider({ children, refetchKeys = [], onForegroundMe const [permission, setPermission] = useState(null); const messageUnsubRef = useRef<(() => void) | null>(null); const initOnceRef = useRef(false); - const { usePostDeviceToken } = useFirebase(); - const { mutate: usePostDeviceTokenMutate } = usePostDeviceToken; + const { postDeviceTokenMutation } = useFirebase(); + const { mutate: postDeviceToken } = postDeviceTokenMutation; useEffect(() => { (async () => { const ok = await isSupported().catch(() => false); @@ -63,11 +63,11 @@ export function DeviceTokenProvider({ children, refetchKeys = [], onForegroundMe try { await registerServiceWorker(); const newToken = await generateToken(); - setPermission(typeof window !== 'undefined' ? Notification.permission : null); + setPermission(typeof window !== 'undefined' && 'Notification' in window ? Notification.permission : null); if (newToken) { setToken(newToken); - usePostDeviceTokenMutate( + postDeviceToken( { deviceToken: newToken }, { onError: () => { @@ -96,6 +96,8 @@ export function DeviceTokenProvider({ children, refetchKeys = [], onForegroundMe } finally { setToken(null); initOnceRef.current = false; + messageUnsubRef.current?.(); + messageUnsubRef.current = null; for (const key of refetchKeys) qc.invalidateQueries({ queryKey: key }); } }, [qc, refetchKeys]); diff --git a/src/queryKey/queryKey.ts b/src/queryKey/queryKey.ts index 2726c3b..a53cbf8 100644 --- a/src/queryKey/queryKey.ts +++ b/src/queryKey/queryKey.ts @@ -2,9 +2,10 @@ import { createQueryKeys } from '@lukemorales/query-key-factory'; import type { UseQueryOptions } from '@tanstack/react-query'; export const regionKeys = createQueryKeys('region', { - all: () => ({ - queryKey: ['region'], - }), + all: () => + ({ + queryKey: ['region'] as const, + }) satisfies UseQueryOptions, search: (keyword: string) => ({ queryKey: ['region', 'search', keyword] as const, @@ -12,9 +13,10 @@ export const regionKeys = createQueryKeys('region', { }); export const alarmKeys = createQueryKeys('alarm', { - all: () => ({ - queryKey: ['alarm'], - }), + all: () => + ({ + queryKey: ['alarm'] as const, + }) satisfies UseQueryOptions, getAlarm: (size: number, cursor?: number) => ({ queryKey: ['alarm', 'getAlarm', size, cursor] as const, diff --git a/src/types/common/common.ts b/src/types/common/common.ts index 953e57a..5f553c5 100644 --- a/src/types/common/common.ts +++ b/src/types/common/common.ts @@ -19,13 +19,11 @@ export type TOptimisticUpdate = { updateFn: (old: TCache | undefined, vars: TVariables) => TCache; }; -export type TUseMutationCustomOptions< - TData = unknown, - TVariables = void, - TError = AxiosError<{ message?: string }>, // message 타입 명시 - TContext = { prevData?: unknown }, -> = Omit, 'mutationFn' | 'onMutate' | 'onError' | 'onSuccess'> & { - optimisticUpdate?: TOptimisticUpdate; +export type TUseMutationCustomOptions = Omit< + UseMutationOptions, + 'mutationFn' | 'onMutate' | 'onError' | 'onSuccess' +> & { + optimisticUpdate?: TOptimisticUpdate; invalidateKeys?: QueryKey[]; silentError?: boolean; From eabac484bc23f8e7f08ac76faf429c01ab1a3fb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=97=B0=EC=A7=84?= Date: Wed, 13 Aug 2025 13:44:16 +0900 Subject: [PATCH 05/18] =?UTF-8?q?fix:=20import=20=EC=98=A4=EB=A5=98=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/home/HomePage.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/home/HomePage.tsx b/src/pages/home/HomePage.tsx index b4c72c9..b2a80d9 100644 --- a/src/pages/home/HomePage.tsx +++ b/src/pages/home/HomePage.tsx @@ -2,7 +2,6 @@ import { useEffect } from 'react'; import { Navigate } from 'react-router-dom'; import ClipLoader from 'react-spinners/ClipLoader'; -import { useDeviceToken } from '@/hooks/alarm/useDeviceToken'; import { useUserGrade } from '@/hooks/home/useUserGrade'; import Banner from '@/components/home/banner'; From 172eaebdb8baca3de2af1062123727e67a83c191 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=97=B0=EC=A7=84?= Date: Wed, 13 Aug 2025 14:21:00 +0900 Subject: [PATCH 06/18] =?UTF-8?q?feat:=20queryInvalidate=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/modal/regionModal.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/modal/regionModal.tsx b/src/components/modal/regionModal.tsx index cf51cb8..5e92203 100644 --- a/src/components/modal/regionModal.tsx +++ b/src/components/modal/regionModal.tsx @@ -6,6 +6,9 @@ import { useUserRegion } from '@/hooks/home/useUserRegion'; import EditableInputBox from '@/components/common/EditableInputBox'; import Modal from '@/components/common/modal'; +import { queryClient } from '@/api/queryClient'; +import { HomeKeys } from '@/queryKey/queryKey'; + interface IRegionModalProps { onClose: () => void; } @@ -23,6 +26,7 @@ function RegionModal({ onClose }: IRegionModalProps) { }, { onSuccess: () => { + queryClient.invalidateQueries({ queryKey: HomeKeys.userRegion().queryKey }); onClose(); }, }, From e4da2287e023feed7a24e275a04af5ea048a08e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=97=B0=EC=A7=84?= Date: Wed, 13 Aug 2025 14:52:01 +0900 Subject: [PATCH 07/18] =?UTF-8?q?fix:=20=EC=BD=94=EB=93=9C=20=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/axiosInstance.ts | 10 +++---- src/components/modal/regionModal.tsx | 2 +- src/firebase/firebase.ts | 17 +++++++---- src/hooks/customQuery.ts | 41 ++++++++++++++----------- src/pages/auth/UserSetting.tsx | 2 +- src/providers/deviceTokenProvider.tsx | 2 +- src/queryKey/queryKey.ts | 43 ++++++++++----------------- src/types/common/common.ts | 16 +++++----- 8 files changed, 65 insertions(+), 68 deletions(-) diff --git a/src/api/axiosInstance.ts b/src/api/axiosInstance.ts index d3192a1..c621bd6 100644 --- a/src/api/axiosInstance.ts +++ b/src/api/axiosInstance.ts @@ -40,20 +40,20 @@ axiosInstance.interceptors.response.use( } } catch (errors) { if (axios.isAxiosError(errors)) { - const refreshError = error as AxiosError; + const refreshError = errors as AxiosError; if (refreshError.response?.data.message === 'The token is null.') { console.error('refreshToken이 없습니다. 로그인 페이지로 이동합니다.'); - logout(); + void logout(); } else if (refreshError.response?.data.message === 'The token is invalid.') { console.error('refreshToken이 만료되었습니다. 로그인 페이지로 이동합니다.'); - logout(); + void logout(); } else { console.error('알 수 없는 오류가 발생했습니다', errors); - logout(); + void logout(); } } else { console.error('알 수 없는 오류가 발생했습니다', errors); - logout(); + void logout(); } return Promise.reject(errors); diff --git a/src/components/modal/regionModal.tsx b/src/components/modal/regionModal.tsx index 5e92203..04700e8 100644 --- a/src/components/modal/regionModal.tsx +++ b/src/components/modal/regionModal.tsx @@ -26,7 +26,7 @@ function RegionModal({ onClose }: IRegionModalProps) { }, { onSuccess: () => { - queryClient.invalidateQueries({ queryKey: HomeKeys.userRegion().queryKey }); + void queryClient.invalidateQueries({ queryKey: HomeKeys.userRegion().queryKey }); onClose(); }, }, diff --git a/src/firebase/firebase.ts b/src/firebase/firebase.ts index 1414eae..cb0dfe4 100644 --- a/src/firebase/firebase.ts +++ b/src/firebase/firebase.ts @@ -15,6 +15,11 @@ const firebaseConfig = { const app = initializeApp(firebaseConfig); export let messaging: Messaging | null = null; +async function ensureMessaging() { + if (!(await isSupported())) return null; + if (!messaging) messaging = getMessaging(app); + return messaging; +} (async () => { if (await isSupported()) { messaging = getMessaging(app); @@ -22,8 +27,8 @@ export let messaging: Messaging | null = null; })(); export async function generateToken(): Promise { - if (!(await isSupported())) return null; - if (!messaging) messaging = getMessaging(app); + const m = await ensureMessaging(); + if (!m) return null; // 권한 요청 (이미 허용/거부된 상태면 브라우저가 적절히 동작) if ('Notification' in window && Notification.permission !== 'granted') { @@ -32,7 +37,7 @@ export async function generateToken(): Promise { } try { - const token = await getToken(messaging, { + const token = await getToken(m, { vapidKey: import.meta.env.VITE_FIREBASE_VAPID_KEY, serviceWorkerRegistration: await navigator.serviceWorker.ready, }); @@ -54,10 +59,10 @@ export const registerServiceWorker = async () => { }; export async function deleteFcmToken(): Promise { - if (!(await isSupported())) return false; - if (!messaging) messaging = getMessaging(app); + const m = await ensureMessaging(); + if (!m) return false; try { - return await deleteToken(messaging); + return await deleteToken(m); } catch (e) { console.error('FCM deleteToken 실패:', e); return false; diff --git a/src/hooks/customQuery.ts b/src/hooks/customQuery.ts index 8a14779..91176a3 100644 --- a/src/hooks/customQuery.ts +++ b/src/hooks/customQuery.ts @@ -18,45 +18,52 @@ export function useCoreQuery( }); } -export function useCoreMutation( - mutation: MutationFunction, - options?: TUseMutationCustomOptions, { prevData?: unknown }>, -) { +export function useCoreMutation< + TData, + TVariables, + TError = AxiosError<{ message?: string }>, // 필요 시 TResponseError 등으로 대체 + TContext extends { prevData?: unknown } = { prevData?: unknown }, + TCache = unknown, +>(mutation: MutationFunction, options?: TUseMutationCustomOptions) { const { - optimisticUpdate, + optimisticUpdate, // { key: QueryKey; updateFn: (old: TCache | undefined, vars: TVariables) => TCache } invalidateKeys, userOnError, userOnSuccess, - ...rest // retry, gcTime 등 표준 옵션 + ...rest // retry, gcTime 등 표준 옵션(UseMutationOptions 호환) } = options ?? {}; - return useMutation, TVariables, { prevData?: unknown }>({ + return useMutation({ mutationFn: mutation, - onMutate: async (vars) => { - if (!optimisticUpdate) return {}; + // 중요: onMutate는 반드시 TContext | undefined를 반환해야 함 + onMutate: async (vars): Promise => { + if (!optimisticUpdate) return undefined; + await queryClient.cancelQueries({ queryKey: optimisticUpdate.key }); - const prevData = queryClient.getQueryData(optimisticUpdate.key); - queryClient.setQueryData(optimisticUpdate.key, (old: unknown) => optimisticUpdate.updateFn(old, vars)); - return { prevData }; + + const prevData = queryClient.getQueryData(optimisticUpdate.key); + + // 캐시 타입 안전하게 업데이트 + queryClient.setQueryData(optimisticUpdate.key, (old) => optimisticUpdate.updateFn(old as TCache | undefined, vars)); + + // prevData를 컨텍스트로 보관 + return { prevData } as TContext; }, onError: (error, vars, ctx) => { // 롤백 if (optimisticUpdate && ctx?.prevData !== undefined) { - queryClient.setQueryData(optimisticUpdate.key, ctx.prevData); + queryClient.setQueryData(optimisticUpdate.key, ctx.prevData as TCache); } - - // 사용자 콜백 위임 userOnError?.(error, vars, ctx); }, onSuccess: async (data, vars, ctx) => { - // invalidate + // 꼭 invalidate가 필요한 키만 if (invalidateKeys?.length) { await Promise.all(invalidateKeys.map((key) => queryClient.invalidateQueries({ queryKey: key }))); } - // 사용자 콜백 위임 userOnSuccess?.(data, vars, ctx); }, diff --git a/src/pages/auth/UserSetting.tsx b/src/pages/auth/UserSetting.tsx index b7c494e..927ff1b 100644 --- a/src/pages/auth/UserSetting.tsx +++ b/src/pages/auth/UserSetting.tsx @@ -63,7 +63,7 @@ export default function User() { }, onError: (err) => { console.error(err); - setError(err.response?.data.message || '회원가입 중 문제가 발생했습니다.'); + setError(err.response?.data.message ?? '회원가입 중 문제가 발생했습니다.'); }, }, ); diff --git a/src/providers/deviceTokenProvider.tsx b/src/providers/deviceTokenProvider.tsx index 6bb983f..a8329cb 100644 --- a/src/providers/deviceTokenProvider.tsx +++ b/src/providers/deviceTokenProvider.tsx @@ -88,7 +88,7 @@ export function DeviceTokenProvider({ children, refetchKeys = [], onForegroundMe console.error('FCM 초기화 실패:', err); initOnceRef.current = false; } - }, [supported, wireOnMessage]); + }, [supported, wireOnMessage, postDeviceToken]); const unregisterToken = useCallback(async () => { try { diff --git a/src/queryKey/queryKey.ts b/src/queryKey/queryKey.ts index cea6f52..dff904f 100644 --- a/src/queryKey/queryKey.ts +++ b/src/queryKey/queryKey.ts @@ -1,41 +1,28 @@ import { createQueryKeys } from '@lukemorales/query-key-factory'; -import type { UseQueryOptions } from '@tanstack/react-query'; export const regionKeys = createQueryKeys('region', { - all: () => - ({ - queryKey: ['region'] as const, - }) satisfies UseQueryOptions, - search: (keyword: string) => - ({ - queryKey: ['region', 'search', keyword] as const, - }) satisfies UseQueryOptions, + all: null, + search: (keyword: string) => ['search', keyword], }); export const alarmKeys = createQueryKeys('alarm', { - all: () => - ({ - queryKey: ['alarm'] as const, - }) satisfies UseQueryOptions, - getAlarm: (size: number, cursor?: number) => - ({ - queryKey: ['alarm', 'getAlarm', size, cursor] as const, - }) satisfies UseQueryOptions, + all: null, + getAlarm: (size: number, cursor?: number) => ['getAlarm', size, cursor], }); export const HomeKeys = createQueryKeys('home', { - all: () => ['home'], - getUserGrade: () => ['home', 'user', 'grade'], - dateCourseSave: () => ['home', 'date-courses', 'saved-count'], - weather: (startDate, regionId) => ['home', 'weather', 'forecast', startDate, regionId], - rainyInfo: (startDate, regionId) => ['home', 'rainy', 'forecast', startDate, regionId], - keywords: () => ['home', 'keywords'], - dateTimes: () => ['home', 'dateTimes'], - monthlyPlaceStates: () => ['home', 'monthlyPlaceStates'], - userRegion: () => ['home', 'user', 'region'], + all: null, + getUserGrade: () => ['user', 'grade'], + dateCourseSave: () => ['date-courses', 'saved-count'], + weather: (startDate, regionId) => ['weather', 'forecast', startDate, regionId], + rainyInfo: (startDate, regionId) => ['rainy', 'forecast', startDate, regionId], + keywords: () => ['keywords'], + dateTimes: () => ['dateTimes'], + monthlyPlaceStates: () => ['monthlyPlaceStates'], + userRegion: () => ['user', 'region'], }); export const NoticeKeys = createQueryKeys('notice', { - all: () => ['notice'], - getAllNotices: (page: number, size: number, noticeCategory: 'SERVICE' | 'SYSTEM') => ['notice', page, size, noticeCategory], + all: null, + getAllNotices: (page: number, size: number, noticeCategory: 'SERVICE' | 'SYSTEM') => [page, size, noticeCategory], }); diff --git a/src/types/common/common.ts b/src/types/common/common.ts index 5f553c5..0b5f44a 100644 --- a/src/types/common/common.ts +++ b/src/types/common/common.ts @@ -14,22 +14,20 @@ export type TResponseError = AxiosError<{ error: string; }>; -export type TOptimisticUpdate = { - key: QueryKey; // 예: ['post', postId] +export type TOptimisticUpdate = { + key: QueryKey; updateFn: (old: TCache | undefined, vars: TVariables) => TCache; }; -export type TUseMutationCustomOptions = Omit< +export type TUseMutationCustomOptions = Omit< UseMutationOptions, 'mutationFn' | 'onMutate' | 'onError' | 'onSuccess' > & { - optimisticUpdate?: TOptimisticUpdate; + optimisticUpdate?: TOptimisticUpdate; invalidateKeys?: QueryKey[]; - silentError?: boolean; - - /** 사용자 정의 콜백 — 내부 기본 동작 후 호출 */ - userOnError?: UseMutationOptions['onError']; - userOnSuccess?: UseMutationOptions['onSuccess']; + // 사용자 콜백 분리(선택): 원한다면 UseMutationOptions의 onError/onSuccess를 감싸 별도 이름으로 사용 + userOnError?: (error: TError, variables: TVariables, context: TContext | undefined) => void; + userOnSuccess?: (data: TData, variables: TVariables, context: TContext | undefined) => void; }; export type TUseQueryCustomOptions = Omit< From 564993d0bc8d4f8ad82816f954a03a6b8d65e59e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=97=B0=EC=A7=84?= Date: Wed, 13 Aug 2025 14:54:45 +0900 Subject: [PATCH 08/18] =?UTF-8?q?fix:=20=EB=B9=8C=EB=93=9C=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/App.tsx b/src/App.tsx index ecbe19e..8cdccb6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -9,7 +9,7 @@ import { alarmKeys } from './queryKey/queryKey'; import router from '@/routes/routes'; function App() { - const refetchKeys = useMemo(() => [alarmKeys.all().queryKey], []); + const refetchKeys = useMemo(() => [alarmKeys.all.queryKey], []); return ( From 1abe0e9526e1925ce0451260922a288177de3191 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=97=B0=EC=A7=84?= Date: Wed, 13 Aug 2025 14:55:20 +0900 Subject: [PATCH 09/18] =?UTF-8?q?fix:=20=EC=A0=88=EB=8C=80=20=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 8cdccb6..0e31161 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,11 +1,10 @@ -import './App.css'; +import '@/App.css'; import { useMemo } from 'react'; import { RouterProvider } from 'react-router-dom'; -import { DeviceTokenProvider } from './providers/deviceTokenProvider'; -import { alarmKeys } from './queryKey/queryKey'; - +import { DeviceTokenProvider } from '@/providers/deviceTokenProvider'; +import { alarmKeys } from '@/queryKey/queryKey'; import router from '@/routes/routes'; function App() { From 4953ef70be9057584e5550df287ce5282690cc09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=97=B0=EC=A7=84?= Date: Thu, 14 Aug 2025 17:58:42 +0900 Subject: [PATCH 10/18] =?UTF-8?q?fix:=20=EB=B9=8C=EB=93=9C=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/common/PasswordEdit.tsx | 31 +++++----- src/components/settingTab/AlarmSetting.tsx | 5 ++ src/components/settingTab/InfoSetting.tsx | 66 +++++++++++----------- src/hooks/auth/useAccount.ts | 21 ++----- src/hooks/settingAlarm/useAlarms.ts | 12 +--- src/pages/setting/DeleteConfirmPage.tsx | 27 +++++---- src/types/auth/account.ts | 6 +- 7 files changed, 81 insertions(+), 87 deletions(-) diff --git a/src/components/common/PasswordEdit.tsx b/src/components/common/PasswordEdit.tsx index 0a0443a..5424593 100644 --- a/src/components/common/PasswordEdit.tsx +++ b/src/components/common/PasswordEdit.tsx @@ -28,16 +28,7 @@ export default function PasswordEditSection() { setIsEditing(false); }; - const { mutate: changePw, isPending } = useChangePassword({ - onSuccess: () => { - alert('비밀번호가 변경되었습니다.'); - handleCancel(); - }, - onError: (err: any) => { - const msg = (err as any)?.response?.data?.message ?? '비밀번호 변경에 실패했습니다.'; - alert(msg); - }, - }); + const { mutate: changePw, isPending } = useChangePassword(); // 제출 const handleSubmit = () => { @@ -58,10 +49,22 @@ export default function PasswordEditSection() { if (Object.keys(nextErrors).length > 0) return; // 제출 - changePw({ - currentPassword: currentPw, - newPassword: newPw, - }); + changePw( + { + currentPassword: currentPw, + newPassword: newPw, + }, + { + onSuccess: () => { + alert('비밀번호가 변경되었습니다.'); + handleCancel(); + }, + onError: (err: any) => { + const msg = (err as any)?.response?.data?.message ?? '비밀번호 변경에 실패했습니다.'; + alert(msg); + }, + }, + ); }; // 공통 인풋 스타일 diff --git a/src/components/settingTab/AlarmSetting.tsx b/src/components/settingTab/AlarmSetting.tsx index f650568..42a6f08 100644 --- a/src/components/settingTab/AlarmSetting.tsx +++ b/src/components/settingTab/AlarmSetting.tsx @@ -4,6 +4,8 @@ import { useGetAlarmSettings, usePatchAlarmSettings } from '@/hooks/settingAlarm import ToggleSwitch from '@/components/common/ToggleSwitch'; +import { queryClient } from '@/api/queryClient'; + type TAlarmType = 'email' | 'push' | 'sms'; interface IAlarmSettingState { @@ -47,6 +49,9 @@ export default function AlarmSetting() { smsAlarm: next.sms, }, { + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['alarmSettings'] }); + }, onError: () => setAlarmSetting(prev), }, ); diff --git a/src/components/settingTab/InfoSetting.tsx b/src/components/settingTab/InfoSetting.tsx index 3d1d182..87f1c0e 100644 --- a/src/components/settingTab/InfoSetting.tsx +++ b/src/components/settingTab/InfoSetting.tsx @@ -1,6 +1,5 @@ import { useEffect, useState } from 'react'; import { Link } from 'react-router-dom'; -import { useQueryClient } from '@tanstack/react-query'; import { TERMS_URL } from '@/constants/policies'; @@ -9,12 +8,12 @@ import { QUERY_KEYS, useAccount } from '@/hooks/auth/useAccount'; import EditableInputBox from '../common/EditableInputBox'; import PasswordEditSection from '../common/PasswordEdit'; +import { queryClient } from '@/api/queryClient'; import ChevronForward from '@/assets/icons/default_arrows/chevron_forward.svg?react'; const getApiErrorMessage = (err: any, fallback: string) => err?.response?.data?.message ?? (err?.response?.status === 401 ? '로그인이 필요합니다.' : fallback); export default function InfoSetting() { - const qc = useQueryClient(); const { useGetMemberInfo, useChangeNickname, useResetPreferences } = useAccount(); const { data: memberData, isLoading: infoLoading, isError: infoError } = useGetMemberInfo(); @@ -32,40 +31,34 @@ export default function InfoSetting() { } }, [apiNickname]); - const { mutate: changeNickname, isPending: nickPending } = useChangeNickname({ - onSuccess: (res) => { - if (res?.isSuccess) { - const next = res.result.username; - setNickname(next); - setInitialNickname(next); - localStorage.setItem('nickname', next); - - qc.invalidateQueries({ queryKey: QUERY_KEYS.memberInfo }); - qc.invalidateQueries({ queryKey: QUERY_KEYS.memberGrade }); - qc.setQueryData(['userGrade'], (old: any) => (old ? { ...old, result: { ...old.result, username: next } } : old)); - } else { - alert(res?.message ?? '닉네임 변경에 실패했습니다.'); - } - }, - onError: (err: any) => alert(getApiErrorMessage(err, '닉네임 변경에 실패했습니다.')), - }); - - const { mutate: resetPref, isPending: resetPending } = useResetPreferences({ - onSuccess: (res) => { - if (res?.isSuccess) { - alert('취향 데이터가 초기화되었습니다.'); - } else { - alert(res?.message ?? '초기화에 실패했습니다.'); - } - }, - onError: (err: any) => alert(getApiErrorMessage(err, '초기화에 실패했습니다.')), - }); + const { mutate: changeNickname, isPending: nickPending } = useChangeNickname(); + + const { mutate: resetPref, isPending: resetPending } = useResetPreferences(); const handleSubmitNickname = () => { const trimmed = nickname.trim(); if (!trimmed) return alert('닉네임을 입력해 주세요.'); if (trimmed === initialNickname || nickPending) return; - changeNickname({ username: trimmed }); + changeNickname( + { username: trimmed }, + { + onSuccess: (res: any) => { + if (res?.isSuccess) { + const next = res.result.username; + setNickname(next); + setInitialNickname(next); + localStorage.setItem('nickname', next); + + queryClient.invalidateQueries({ queryKey: QUERY_KEYS.memberInfo }); + queryClient.invalidateQueries({ queryKey: QUERY_KEYS.memberGrade }); + queryClient.setQueryData(['userGrade'], (old: any) => (old ? { ...old, result: { ...old.result, username: next } } : old)); + } else { + alert(res?.message ?? '닉네임 변경에 실패했습니다.'); + } + }, + onError: (err: any) => alert(getApiErrorMessage(err, '닉네임 변경에 실패했습니다.')), + }, + ); }; const handleCancelNickname = () => { @@ -75,7 +68,16 @@ export default function InfoSetting() { const handleResetPreferences = () => { if (resetPending) return; if (!confirm('정말 초기화할까요? 되돌릴 수 없습니다.')) return; - resetPref(); + resetPref({ + onSuccess: (res: any) => { + if (res?.isSuccess) { + alert('취향 데이터가 초기화되었습니다.'); + } else { + alert(res?.message ?? '초기화에 실패했습니다.'); + } + }, + onError: (err: any) => alert(getApiErrorMessage(err, '초기화에 실패했습니다.')), + }); }; return ( diff --git a/src/hooks/auth/useAccount.ts b/src/hooks/auth/useAccount.ts index d278367..6aafa5b 100644 --- a/src/hooks/auth/useAccount.ts +++ b/src/hooks/auth/useAccount.ts @@ -1,14 +1,3 @@ -import type { - TChangeNicknameMutationOptions, - TChangeNicknameMutationResult, - TChangeNicknamePayload, - TChangeNicknameResponse, - TChangePasswordMutationOptions, - TChangePasswordMutationResult, - TChangePasswordPayload, -} from '@/types/auth/account'; -import type { TResetPreferencesResponse } from '@/types/dates/preferences'; - import { useCoreMutation, useCoreQuery } from '@/hooks/customQuery'; import { changeNickname, changePassword, deleteMember, getMemberGrade, getMemberInfo } from '@/api/auth/account'; @@ -21,13 +10,13 @@ export const QUERY_KEYS = { export function useAccount() { // 비밀번호 변경 - function useChangePassword(options?: TChangePasswordMutationOptions): TChangePasswordMutationResult { - return useCoreMutation(changePassword, options); + function useChangePassword() { + return useCoreMutation(changePassword); } // 닉네임 변경 - function useChangeNickname(options?: TChangeNicknameMutationOptions): TChangeNicknameMutationResult { - return useCoreMutation(changeNickname, options); + function useChangeNickname() { + return useCoreMutation(changeNickname); } // 회원 탈퇴 @@ -47,7 +36,7 @@ export function useAccount() { // 취향 데이터 초기화 function useResetPreferences() { - return useCoreMutation(resetPreferences); + return useCoreMutation(resetPreferences); } return { useChangePassword, useChangeNickname, useDeleteMember, useGetMemberInfo, useGetMemberGrade, useResetPreferences }; diff --git a/src/hooks/settingAlarm/useAlarms.ts b/src/hooks/settingAlarm/useAlarms.ts index bac45af..14566d6 100644 --- a/src/hooks/settingAlarm/useAlarms.ts +++ b/src/hooks/settingAlarm/useAlarms.ts @@ -1,7 +1,4 @@ // hooks/settingAlarm/useAlarms.ts -import { useQueryClient } from '@tanstack/react-query'; - -import type { TAlarmSettings, TGetAlarmSettingsResp, TPatchAlarmSettingsResp } from '@/types/settingAlarm/alarm'; import { useCoreMutation, useCoreQuery } from '@/hooks/customQuery'; @@ -9,7 +6,7 @@ import { getAlarmSettings, patchAlarmSettings } from '@/api/settingAlarm/alarm'; // 조회 export function useGetAlarmSettings() { - return useCoreQuery(['alarmSettings'], getAlarmSettings, { + return useCoreQuery(['alarmSettings'], getAlarmSettings, { select: (resp) => resp.result, refetchOnWindowFocus: false, }); @@ -17,10 +14,5 @@ export function useGetAlarmSettings() { // 업데이트 export function usePatchAlarmSettings() { - const qc = useQueryClient(); - return useCoreMutation(patchAlarmSettings, { - onSuccess: () => { - qc.invalidateQueries({ queryKey: ['alarmSettings'] }); - }, - }); + return useCoreMutation(patchAlarmSettings); } diff --git a/src/pages/setting/DeleteConfirmPage.tsx b/src/pages/setting/DeleteConfirmPage.tsx index 4e69f46..de534db 100644 --- a/src/pages/setting/DeleteConfirmPage.tsx +++ b/src/pages/setting/DeleteConfirmPage.tsx @@ -34,24 +34,27 @@ export default function DeleteConfirmPage() { const { data: memberData, isLoading: infoLoading, isError: infoError } = useGetMemberInfo(); const userEmail = memberData?.result?.email ?? ''; // ← 여기서 이메일 사용 - const { mutate: deleteAccount, isPending } = useDeleteMember({ - onSuccess: () => { - alert('회원 탈퇴가 완료되었습니다.'); - localStorage.removeItem('accessToken'); - navigate('/', { replace: true }); - }, - onError: (error) => { - const msg = error?.response?.data?.message || '회원 탈퇴에 실패했습니다.'; - alert(msg); - }, - }); + const { mutate: deleteAccount, isPending } = useDeleteMember(); const allAgreed = checked.every(Boolean); const handleDelete = () => { if (!allAgreed) return alert('유의사항에 모두 동의해 주세요.'); if (!confirm('정말 탈퇴하시겠습니까?')) return; - deleteAccount(); + deleteAccount( + {}, + { + onSuccess: () => { + alert('회원 탈퇴가 완료되었습니다.'); + localStorage.removeItem('accessToken'); + navigate('/', { replace: true }); + }, + onError: (error) => { + const msg = error?.response?.data?.message || '회원 탈퇴에 실패했습니다.'; + alert(msg); + }, + }, + ); }; const toggleCheckbox = (index: number) => { diff --git a/src/types/auth/account.ts b/src/types/auth/account.ts index 581856f..619ea88 100644 --- a/src/types/auth/account.ts +++ b/src/types/auth/account.ts @@ -1,7 +1,7 @@ import type { UseMutationResult } from '@tanstack/react-query'; import type { AxiosError } from 'axios'; -import type { TCommonResponse, TUseMutationCustomOptions } from '@/types/common/common'; +import type { TCommonResponse } from '@/types/common/common'; export type TChangePasswordPayload = { currentPassword: string; @@ -17,11 +17,11 @@ export type TChangeNicknameResponse = { }; // 비밀번호 변경 훅 타입 -export type TChangePasswordMutationOptions = TUseMutationCustomOptions; + export type TChangePasswordMutationResult = UseMutationResult; // 닉네임 변경 훅 타입 -export type TChangeNicknameMutationOptions = TUseMutationCustomOptions; + export type TChangeNicknameMutationResult = UseMutationResult; // 사용자 정보 타입 From ed67324c67a7e445075c64bf6b98f763f6bd3ac1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=97=B0=EC=A7=84?= Date: Thu, 14 Aug 2025 18:19:50 +0900 Subject: [PATCH 11/18] =?UTF-8?q?fix:=20=EC=BD=94=EB=93=9C=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/common/PasswordEdit.tsx | 3 ++- src/components/modal/regionModal.tsx | 4 ++-- src/components/settingTab/AlarmSetting.tsx | 3 ++- src/components/settingTab/InfoSetting.tsx | 13 ++++++++----- src/hooks/auth/useAccount.ts | 10 +++------- src/hooks/home/useDateCourseStates.ts | 4 ++-- src/hooks/home/useDatePlaceStates.ts | 4 ++-- src/hooks/home/useDateTimes.ts | 4 ++-- src/hooks/home/useKeywordStates.ts | 4 ++-- src/hooks/home/useUserGrade.ts | 4 ++-- src/hooks/home/useUserRegion.ts | 4 ++-- src/hooks/home/useWeather.ts | 9 +++++---- src/hooks/settingAlarm/useAlarms.ts | 3 ++- src/pages/setting/DeleteConfirmPage.tsx | 8 +++++++- src/providers/deviceTokenProvider.tsx | 9 ++++----- src/queryKey/queryKey.ts | 14 ++++++++++---- 16 files changed, 57 insertions(+), 43 deletions(-) diff --git a/src/components/common/PasswordEdit.tsx b/src/components/common/PasswordEdit.tsx index 5424593..ff16cab 100644 --- a/src/components/common/PasswordEdit.tsx +++ b/src/components/common/PasswordEdit.tsx @@ -1,4 +1,5 @@ import { useState } from 'react'; +import type { AxiosError } from 'axios'; import { z } from 'zod'; import { useAccount } from '@/hooks/auth/useAccount'; @@ -59,7 +60,7 @@ export default function PasswordEditSection() { alert('비밀번호가 변경되었습니다.'); handleCancel(); }, - onError: (err: any) => { + onError: (err: AxiosError) => { const msg = (err as any)?.response?.data?.message ?? '비밀번호 변경에 실패했습니다.'; alert(msg); }, diff --git a/src/components/modal/regionModal.tsx b/src/components/modal/regionModal.tsx index 04700e8..b8771ca 100644 --- a/src/components/modal/regionModal.tsx +++ b/src/components/modal/regionModal.tsx @@ -7,7 +7,7 @@ import EditableInputBox from '@/components/common/EditableInputBox'; import Modal from '@/components/common/modal'; import { queryClient } from '@/api/queryClient'; -import { HomeKeys } from '@/queryKey/queryKey'; +import { homeKeys } from '@/queryKey/queryKey'; interface IRegionModalProps { onClose: () => void; @@ -26,7 +26,7 @@ function RegionModal({ onClose }: IRegionModalProps) { }, { onSuccess: () => { - void queryClient.invalidateQueries({ queryKey: HomeKeys.userRegion().queryKey }); + void queryClient.invalidateQueries({ queryKey: homeKeys.userRegion().queryKey }); onClose(); }, }, diff --git a/src/components/settingTab/AlarmSetting.tsx b/src/components/settingTab/AlarmSetting.tsx index 42a6f08..2c974e8 100644 --- a/src/components/settingTab/AlarmSetting.tsx +++ b/src/components/settingTab/AlarmSetting.tsx @@ -5,6 +5,7 @@ import { useGetAlarmSettings, usePatchAlarmSettings } from '@/hooks/settingAlarm import ToggleSwitch from '@/components/common/ToggleSwitch'; import { queryClient } from '@/api/queryClient'; +import { alarmKeys } from '@/queryKey/queryKey'; type TAlarmType = 'email' | 'push' | 'sms'; @@ -50,7 +51,7 @@ export default function AlarmSetting() { }, { onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['alarmSettings'] }); + queryClient.invalidateQueries({ queryKey: alarmKeys.alarmSettings().queryKey }); }, onError: () => setAlarmSetting(prev), }, diff --git a/src/components/settingTab/InfoSetting.tsx b/src/components/settingTab/InfoSetting.tsx index 87f1c0e..dbeeccb 100644 --- a/src/components/settingTab/InfoSetting.tsx +++ b/src/components/settingTab/InfoSetting.tsx @@ -1,17 +1,20 @@ import { useEffect, useState } from 'react'; import { Link } from 'react-router-dom'; +import type { TResponseError } from '@/types/common/common'; import { TERMS_URL } from '@/constants/policies'; -import { QUERY_KEYS, useAccount } from '@/hooks/auth/useAccount'; +import { useAccount } from '@/hooks/auth/useAccount'; import EditableInputBox from '../common/EditableInputBox'; import PasswordEditSection from '../common/PasswordEdit'; import { queryClient } from '@/api/queryClient'; import ChevronForward from '@/assets/icons/default_arrows/chevron_forward.svg?react'; +import { memberKeys } from '@/queryKey/queryKey'; -const getApiErrorMessage = (err: any, fallback: string) => err?.response?.data?.message ?? (err?.response?.status === 401 ? '로그인이 필요합니다.' : fallback); +const getApiErrorMessage = (err: TResponseError, fallback: string) => + err?.response?.data?.message ?? (err?.response?.status === 401 ? '로그인이 필요합니다.' : fallback); export default function InfoSetting() { const { useGetMemberInfo, useChangeNickname, useResetPreferences } = useAccount(); @@ -49,8 +52,8 @@ export default function InfoSetting() { setInitialNickname(next); localStorage.setItem('nickname', next); - queryClient.invalidateQueries({ queryKey: QUERY_KEYS.memberInfo }); - queryClient.invalidateQueries({ queryKey: QUERY_KEYS.memberGrade }); + queryClient.invalidateQueries({ queryKey: memberKeys.all.queryKey }); + queryClient.setQueryData(['userGrade'], (old: any) => (old ? { ...old, result: { ...old.result, username: next } } : old)); } else { alert(res?.message ?? '닉네임 변경에 실패했습니다.'); @@ -68,7 +71,7 @@ export default function InfoSetting() { const handleResetPreferences = () => { if (resetPending) return; if (!confirm('정말 초기화할까요? 되돌릴 수 없습니다.')) return; - resetPref({ + resetPref(undefined, { onSuccess: (res: any) => { if (res?.isSuccess) { alert('취향 데이터가 초기화되었습니다.'); diff --git a/src/hooks/auth/useAccount.ts b/src/hooks/auth/useAccount.ts index 6aafa5b..355e07f 100644 --- a/src/hooks/auth/useAccount.ts +++ b/src/hooks/auth/useAccount.ts @@ -2,11 +2,7 @@ import { useCoreMutation, useCoreQuery } from '@/hooks/customQuery'; import { changeNickname, changePassword, deleteMember, getMemberGrade, getMemberInfo } from '@/api/auth/account'; import { resetPreferences } from '@/api/dates/preferences'; - -export const QUERY_KEYS = { - memberInfo: ['memberInfo'] as const, - memberGrade: ['memberGrade'] as const, -} as const; +import { memberKeys } from '@/queryKey/queryKey'; export function useAccount() { // 비밀번호 변경 @@ -26,12 +22,12 @@ export function useAccount() { // 사용자 정보 조회 function useGetMemberInfo() { - return useCoreQuery(QUERY_KEYS.memberInfo, getMemberInfo); + return useCoreQuery(memberKeys.memberInfo().queryKey, getMemberInfo); } // 사용자 등급 조회 function useGetMemberGrade() { - return useCoreQuery(QUERY_KEYS.memberGrade, getMemberGrade); + return useCoreQuery(memberKeys.memberGrade().queryKey, getMemberGrade); } // 취향 데이터 초기화 diff --git a/src/hooks/home/useDateCourseStates.ts b/src/hooks/home/useDateCourseStates.ts index 39e50c2..73c7a63 100644 --- a/src/hooks/home/useDateCourseStates.ts +++ b/src/hooks/home/useDateCourseStates.ts @@ -1,11 +1,11 @@ import { useCoreQuery } from '@/hooks/customQuery'; import { getDateCourseSavedCount } from '@/api/home/dateCourse'; -import { HomeKeys } from '@/queryKey/queryKey'; +import { homeKeys } from '@/queryKey/queryKey'; // 데이트 코스 저장 횟수 훅 export const useDateCourseSavedCount = () => { - return useCoreQuery(HomeKeys.dateCourseSave().queryKey, getDateCourseSavedCount, { + return useCoreQuery(homeKeys.dateCourseSave().queryKey, getDateCourseSavedCount, { gcTime: 15 * 60 * 1000, // 15분 retry: 3, }); diff --git a/src/hooks/home/useDatePlaceStates.ts b/src/hooks/home/useDatePlaceStates.ts index 35c60df..4246d8a 100644 --- a/src/hooks/home/useDatePlaceStates.ts +++ b/src/hooks/home/useDatePlaceStates.ts @@ -1,10 +1,10 @@ import { useCoreQuery } from '../customQuery'; import { getMonthlyDatePlaceStates } from '@/api/home/dateTimes'; -import { HomeKeys } from '@/queryKey/queryKey'; +import { homeKeys } from '@/queryKey/queryKey'; export const useMonthlyPlaceStates = () => { - return useCoreQuery(HomeKeys.monthlyPlaceStates().queryKey, () => getMonthlyDatePlaceStates(), { + return useCoreQuery(homeKeys.monthlyPlaceStates().queryKey, () => getMonthlyDatePlaceStates(), { gcTime: 15 * 60 * 1000, retry: 3, }); diff --git a/src/hooks/home/useDateTimes.ts b/src/hooks/home/useDateTimes.ts index 225e46a..be8b40c 100644 --- a/src/hooks/home/useDateTimes.ts +++ b/src/hooks/home/useDateTimes.ts @@ -1,8 +1,8 @@ import { useCoreQuery } from '@/hooks/customQuery'; import { getDateTimeStates } from '@/api/home/dateTimes'; -import { HomeKeys } from '@/queryKey/queryKey'; +import { homeKeys } from '@/queryKey/queryKey'; export const useDateTimeStates = () => { - return useCoreQuery(HomeKeys.dateTimes().queryKey, () => getDateTimeStates(), { staleTime: 5 * 60 * 1000, gcTime: 15 * 60 * 1000, retry: 3 }); + return useCoreQuery(homeKeys.dateTimes().queryKey, () => getDateTimeStates(), { staleTime: 5 * 60 * 1000, gcTime: 15 * 60 * 1000, retry: 3 }); }; diff --git a/src/hooks/home/useKeywordStates.ts b/src/hooks/home/useKeywordStates.ts index 3cc4d4d..a815169 100644 --- a/src/hooks/home/useKeywordStates.ts +++ b/src/hooks/home/useKeywordStates.ts @@ -1,11 +1,11 @@ import { useCoreQuery } from '@/hooks/customQuery'; import { getWeeklyKeywords } from '@/api/home/keyword'; -import { HomeKeys } from '@/queryKey/queryKey'; +import { homeKeys } from '@/queryKey/queryKey'; // 이번 주 인기 키워드 훅 export const useWeeklyKeywords = () => { - return useCoreQuery(HomeKeys.keywords().queryKey, getWeeklyKeywords, { + return useCoreQuery(homeKeys.keywords().queryKey, getWeeklyKeywords, { gcTime: 30 * 60 * 1000, retry: 3, }); diff --git a/src/hooks/home/useUserGrade.ts b/src/hooks/home/useUserGrade.ts index bc79b16..bc94eec 100644 --- a/src/hooks/home/useUserGrade.ts +++ b/src/hooks/home/useUserGrade.ts @@ -1,11 +1,11 @@ import { useCoreQuery } from '@/hooks/customQuery'; import { getUserGrade } from '@/api/home/level'; -import { HomeKeys } from '@/queryKey/queryKey'; +import { homeKeys } from '@/queryKey/queryKey'; // 사용자 등급 정보 훅 export const useUserGrade = () => { - return useCoreQuery(HomeKeys.getUserGrade().queryKey, () => getUserGrade(), { + return useCoreQuery(homeKeys.getUserGrade().queryKey, () => getUserGrade(), { gcTime: 15 * 60 * 1000, // 15분 retry: 3, }); diff --git a/src/hooks/home/useUserRegion.ts b/src/hooks/home/useUserRegion.ts index bcb7ac9..0f8542b 100644 --- a/src/hooks/home/useUserRegion.ts +++ b/src/hooks/home/useUserRegion.ts @@ -1,12 +1,12 @@ import { useCoreMutation, useCoreQuery } from '../customQuery'; import { getUserRegion, patchUserRegion } from '@/api/home/region'; -import { HomeKeys } from '@/queryKey/queryKey'; +import { homeKeys } from '@/queryKey/queryKey'; export function useUserRegion() { return useCoreMutation(patchUserRegion); } export function useGetUserRegion() { - return useCoreQuery(HomeKeys.userRegion().queryKey, getUserRegion); + return useCoreQuery(homeKeys.userRegion().queryKey, getUserRegion); } diff --git a/src/hooks/home/useWeather.ts b/src/hooks/home/useWeather.ts index cb9cc3c..0e61681 100644 --- a/src/hooks/home/useWeather.ts +++ b/src/hooks/home/useWeather.ts @@ -1,18 +1,19 @@ +import { useCoreQuery } from '@/hooks/customQuery'; + import { getPrecipitation, getWeeklyWeatherRecommendation } from '../../api/home/weather'; -import { useCoreQuery } from '../customQuery'; -import { HomeKeys } from '@/queryKey/queryKey'; +import { homeKeys } from '@/queryKey/queryKey'; // 주간 날씨 추천 훅 export const useWeatherForecast = ({ startDate, regionId }: { startDate: string; regionId: number }) => { - return useCoreQuery(HomeKeys.weather(startDate, regionId).queryKey, () => getWeeklyWeatherRecommendation({ startDate, regionId: regionId! }), { + return useCoreQuery(homeKeys.weather(startDate, regionId).queryKey, () => getWeeklyWeatherRecommendation({ startDate, regionId: regionId! }), { staleTime: 1000 * 60 * 30, enabled: !!startDate && !!regionId, }); }; export const useRainyInfo = ({ startDate, regionId }: { startDate: string; regionId: number }) => { - return useCoreQuery(HomeKeys.rainyInfo(startDate, regionId).queryKey, () => getPrecipitation({ startDate, regionId: regionId! }), { + return useCoreQuery(homeKeys.rainyInfo(startDate, regionId).queryKey, () => getPrecipitation({ startDate, regionId: regionId! }), { staleTime: 1000 * 60 * 30, enabled: !!startDate && !!regionId, }); diff --git a/src/hooks/settingAlarm/useAlarms.ts b/src/hooks/settingAlarm/useAlarms.ts index 14566d6..de1096d 100644 --- a/src/hooks/settingAlarm/useAlarms.ts +++ b/src/hooks/settingAlarm/useAlarms.ts @@ -3,10 +3,11 @@ import { useCoreMutation, useCoreQuery } from '@/hooks/customQuery'; import { getAlarmSettings, patchAlarmSettings } from '@/api/settingAlarm/alarm'; +import { alarmKeys } from '@/queryKey/queryKey'; // 조회 export function useGetAlarmSettings() { - return useCoreQuery(['alarmSettings'], getAlarmSettings, { + return useCoreQuery(alarmKeys.alarmSettings().queryKey, getAlarmSettings, { select: (resp) => resp.result, refetchOnWindowFocus: false, }); diff --git a/src/pages/setting/DeleteConfirmPage.tsx b/src/pages/setting/DeleteConfirmPage.tsx index de534db..0297afa 100644 --- a/src/pages/setting/DeleteConfirmPage.tsx +++ b/src/pages/setting/DeleteConfirmPage.tsx @@ -6,7 +6,9 @@ import { useAccount } from '@/hooks/auth/useAccount'; import CommonAuthInput from '@/components/auth/commonAuthInput'; import Header from '@/components/layout/Header'; +import { queryClient } from '@/api/queryClient'; import ArrowLeftCircle from '@/assets/icons/Arrow_left_circle.svg?react'; +import useAuthStore from '@/store/useAuthStore'; // 탈퇴 안내 배열 const withdrawNotices = [ @@ -32,6 +34,7 @@ export default function DeleteConfirmPage() { // 사용자 정보 가져오기 const { data: memberData, isLoading: infoLoading, isError: infoError } = useGetMemberInfo(); + const { setEmail, setPassword, setSocialId } = useAuthStore(); const userEmail = memberData?.result?.email ?? ''; // ← 여기서 이메일 사용 const { mutate: deleteAccount, isPending } = useDeleteMember(); @@ -46,7 +49,10 @@ export default function DeleteConfirmPage() { { onSuccess: () => { alert('회원 탈퇴가 완료되었습니다.'); - localStorage.removeItem('accessToken'); + setEmail(''); + setPassword(''); + setSocialId(-1); + queryClient.clear(); navigate('/', { replace: true }); }, onError: (error) => { diff --git a/src/providers/deviceTokenProvider.tsx b/src/providers/deviceTokenProvider.tsx index a8329cb..fd4edfc 100644 --- a/src/providers/deviceTokenProvider.tsx +++ b/src/providers/deviceTokenProvider.tsx @@ -25,7 +25,6 @@ type TProps = { }; export function DeviceTokenProvider({ children, refetchKeys = [], onForegroundMessage }: TProps) { - const qc = queryClient; const [token, setToken] = useState(null); const [supported, setSupported] = useState(null); const [permission, setPermission] = useState(null); @@ -45,12 +44,12 @@ export function DeviceTokenProvider({ children, refetchKeys = [], onForegroundMe if (!messaging || messageUnsubRef.current) return; const unsub = onMessage(messaging, (payload) => { refetchKeys.forEach((key) => { - qc.invalidateQueries({ queryKey: key }); + void queryClient.invalidateQueries({ queryKey: key }); }); onForegroundMessage?.(payload); }); messageUnsubRef.current = unsub; - }, [onForegroundMessage, qc, refetchKeys]); + }, [onForegroundMessage, queryClient, refetchKeys]); const requestAndRegister = useCallback(async () => { if (supported === false) { @@ -98,9 +97,9 @@ export function DeviceTokenProvider({ children, refetchKeys = [], onForegroundMe initOnceRef.current = false; messageUnsubRef.current?.(); messageUnsubRef.current = null; - for (const key of refetchKeys) qc.invalidateQueries({ queryKey: key }); + for (const key of refetchKeys) void queryClient.invalidateQueries({ queryKey: key }); } - }, [qc, refetchKeys]); + }, [queryClient, refetchKeys]); useEffect(() => { return () => { diff --git a/src/queryKey/queryKey.ts b/src/queryKey/queryKey.ts index dff904f..6745d8e 100644 --- a/src/queryKey/queryKey.ts +++ b/src/queryKey/queryKey.ts @@ -8,21 +8,27 @@ export const regionKeys = createQueryKeys('region', { export const alarmKeys = createQueryKeys('alarm', { all: null, getAlarm: (size: number, cursor?: number) => ['getAlarm', size, cursor], + alarmSettings: () => ['alarmSettings'], }); -export const HomeKeys = createQueryKeys('home', { +export const homeKeys = createQueryKeys('home', { all: null, getUserGrade: () => ['user', 'grade'], dateCourseSave: () => ['date-courses', 'saved-count'], - weather: (startDate, regionId) => ['weather', 'forecast', startDate, regionId], - rainyInfo: (startDate, regionId) => ['rainy', 'forecast', startDate, regionId], + weather: (startDate: string, regionId: number) => ['weather', 'forecast', startDate, regionId], + rainyInfo: (startDate: string, regionId: number) => ['rainy', 'forecast', startDate, regionId], keywords: () => ['keywords'], dateTimes: () => ['dateTimes'], monthlyPlaceStates: () => ['monthlyPlaceStates'], userRegion: () => ['user', 'region'], }); -export const NoticeKeys = createQueryKeys('notice', { +export const noticeKeys = createQueryKeys('notice', { all: null, getAllNotices: (page: number, size: number, noticeCategory: 'SERVICE' | 'SYSTEM') => [page, size, noticeCategory], }); +export const memberKeys = createQueryKeys('member', { + all: null, + memberInfo: () => ['memberInfo'], + memberGrade: () => ['memberGrade'], +}); From b125dfbf8cdf2bc5ee24834bb6ada3f7b51fc3e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=97=B0=EC=A7=84?= Date: Thu, 14 Aug 2025 18:47:55 +0900 Subject: [PATCH 12/18] =?UTF-8?q?fix:=20=EC=BD=94=EB=93=9C=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/settingTab/AlarmSetting.tsx | 33 +++++++++-------- src/components/settingTab/InfoSetting.tsx | 5 +-- src/hooks/home/useDatePlaceStates.ts | 4 +-- src/hooks/home/useDateTimes.ts | 2 +- src/hooks/home/useWeather.ts | 7 ++-- src/pages/setting/PaymentHistory.tsx | 23 ++++++------ src/queryKey/queryKey.ts | 6 ++-- src/routes/routes.tsx | 41 +++++----------------- 8 files changed, 48 insertions(+), 73 deletions(-) diff --git a/src/components/settingTab/AlarmSetting.tsx b/src/components/settingTab/AlarmSetting.tsx index 2c974e8..e839750 100644 --- a/src/components/settingTab/AlarmSetting.tsx +++ b/src/components/settingTab/AlarmSetting.tsx @@ -39,25 +39,24 @@ export default function AlarmSetting() { // 토글 핸들러 const handleToggle = (key: TAlarmType) => { - const prev = alarmSetting; - const next = { ...prev, [key]: !prev[key] }; - setAlarmSetting(next); - - patchAlarm( - { - emailAlarm: next.email, - pushAlarm: next.push, - smsAlarm: next.sms, - }, - { - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: alarmKeys.alarmSettings().queryKey }); + setAlarmSetting((prev) => { + const next = { ...prev, [key]: !prev[key] }; + patchAlarm( + { + emailAlarm: next.email, + pushAlarm: next.push, + smsAlarm: next.sms, + }, + { + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: alarmKeys.alarmSettings().queryKey }); + }, + onError: () => setAlarmSetting(prev), }, - onError: () => setAlarmSetting(prev), - }, - ); + ); + return next; + }); }; - const items: { label: string; key: TAlarmType }[] = [ { label: 'Email 알람', key: 'email' }, { label: '푸쉬 알람', key: 'push' }, diff --git a/src/components/settingTab/InfoSetting.tsx b/src/components/settingTab/InfoSetting.tsx index dbeeccb..6d58594 100644 --- a/src/components/settingTab/InfoSetting.tsx +++ b/src/components/settingTab/InfoSetting.tsx @@ -53,8 +53,9 @@ export default function InfoSetting() { localStorage.setItem('nickname', next); queryClient.invalidateQueries({ queryKey: memberKeys.all.queryKey }); - - queryClient.setQueryData(['userGrade'], (old: any) => (old ? { ...old, result: { ...old.result, username: next } } : old)); + queryClient.setQueryData(memberKeys.memberGrade().queryKey, (old: any) => + old ? { ...old, result: { ...old.result, username: next } } : old, + ); } else { alert(res?.message ?? '닉네임 변경에 실패했습니다.'); } diff --git a/src/hooks/home/useDatePlaceStates.ts b/src/hooks/home/useDatePlaceStates.ts index 4246d8a..9f865c0 100644 --- a/src/hooks/home/useDatePlaceStates.ts +++ b/src/hooks/home/useDatePlaceStates.ts @@ -1,10 +1,10 @@ -import { useCoreQuery } from '../customQuery'; +import { useCoreQuery } from '@/hooks/customQuery'; import { getMonthlyDatePlaceStates } from '@/api/home/dateTimes'; import { homeKeys } from '@/queryKey/queryKey'; export const useMonthlyPlaceStates = () => { - return useCoreQuery(homeKeys.monthlyPlaceStates().queryKey, () => getMonthlyDatePlaceStates(), { + return useCoreQuery(homeKeys.monthlyPlaceStates.queryKey, getMonthlyDatePlaceStates, { gcTime: 15 * 60 * 1000, retry: 3, }); diff --git a/src/hooks/home/useDateTimes.ts b/src/hooks/home/useDateTimes.ts index be8b40c..0d61189 100644 --- a/src/hooks/home/useDateTimes.ts +++ b/src/hooks/home/useDateTimes.ts @@ -4,5 +4,5 @@ import { getDateTimeStates } from '@/api/home/dateTimes'; import { homeKeys } from '@/queryKey/queryKey'; export const useDateTimeStates = () => { - return useCoreQuery(homeKeys.dateTimes().queryKey, () => getDateTimeStates(), { staleTime: 5 * 60 * 1000, gcTime: 15 * 60 * 1000, retry: 3 }); + return useCoreQuery(homeKeys.dateTimes.queryKey, getDateTimeStates); }; diff --git a/src/hooks/home/useWeather.ts b/src/hooks/home/useWeather.ts index 0e61681..203c4cc 100644 --- a/src/hooks/home/useWeather.ts +++ b/src/hooks/home/useWeather.ts @@ -1,19 +1,18 @@ import { useCoreQuery } from '@/hooks/customQuery'; -import { getPrecipitation, getWeeklyWeatherRecommendation } from '../../api/home/weather'; - +import { getPrecipitation, getWeeklyWeatherRecommendation } from '@/api/home/weather'; import { homeKeys } from '@/queryKey/queryKey'; // 주간 날씨 추천 훅 export const useWeatherForecast = ({ startDate, regionId }: { startDate: string; regionId: number }) => { - return useCoreQuery(homeKeys.weather(startDate, regionId).queryKey, () => getWeeklyWeatherRecommendation({ startDate, regionId: regionId! }), { + return useCoreQuery(homeKeys.weather(startDate, regionId).queryKey, () => getWeeklyWeatherRecommendation({ startDate, regionId }), { staleTime: 1000 * 60 * 30, enabled: !!startDate && !!regionId, }); }; export const useRainyInfo = ({ startDate, regionId }: { startDate: string; regionId: number }) => { - return useCoreQuery(homeKeys.rainyInfo(startDate, regionId).queryKey, () => getPrecipitation({ startDate, regionId: regionId! }), { + return useCoreQuery(homeKeys.rainyInfo(startDate, regionId).queryKey, () => getPrecipitation({ startDate, regionId }), { staleTime: 1000 * 60 * 30, enabled: !!startDate && !!regionId, }); diff --git a/src/pages/setting/PaymentHistory.tsx b/src/pages/setting/PaymentHistory.tsx index c81dba2..6500183 100644 --- a/src/pages/setting/PaymentHistory.tsx +++ b/src/pages/setting/PaymentHistory.tsx @@ -1,12 +1,11 @@ import { useMemo, useState } from 'react'; import { useNavigate } from 'react-router-dom'; +import GraySvgButton from '@/components/common/graySvgButton'; import Navigator from '@/components/common/navigator'; import Header from '@/components/layout/Header'; import PaymentRow from '@/components/payment/PaymentRow'; -import ArrowLeftCircle from '@/assets/icons/Arrow_left_circle.svg?react'; - const dummyData = [ { orderNumber: '202219486', date: '2024.01.15', amount: '₩2,900', method: '카카오페이', status: '환불처리' }, { orderNumber: '202219487', date: '2023.05.15', amount: '₩2,900', method: '신한카드', status: '결제 완료' }, @@ -31,18 +30,16 @@ export default function PaymentHistory() { }, [currentPage, itemsPerPage]); return ( -
+
{/* 헤더 */}
-
- {/* 뒤로가기*/} - - - {/* 제목 */} -

결제 내역 확인

+
+
+ navigate('/home')} type="backward" /> + {/* 제목 */} +

결제 내역 확인

+
{/* 테이블 */}
@@ -71,7 +68,9 @@ export default function PaymentHistory() {
{/* 페이지네이션*/} - +
+ +
); diff --git a/src/queryKey/queryKey.ts b/src/queryKey/queryKey.ts index 6745d8e..5440a9f 100644 --- a/src/queryKey/queryKey.ts +++ b/src/queryKey/queryKey.ts @@ -17,9 +17,9 @@ export const homeKeys = createQueryKeys('home', { dateCourseSave: () => ['date-courses', 'saved-count'], weather: (startDate: string, regionId: number) => ['weather', 'forecast', startDate, regionId], rainyInfo: (startDate: string, regionId: number) => ['rainy', 'forecast', startDate, regionId], - keywords: () => ['keywords'], - dateTimes: () => ['dateTimes'], - monthlyPlaceStates: () => ['monthlyPlaceStates'], + keywords: null, + dateTimes: null, + monthlyPlaceStates: null, userRegion: () => ['user', 'region'], }); diff --git a/src/routes/routes.tsx b/src/routes/routes.tsx index 3dd5cc1..664e299 100644 --- a/src/routes/routes.tsx +++ b/src/routes/routes.tsx @@ -1,5 +1,4 @@ -import type { PropsWithChildren } from 'react'; -import { createBrowserRouter, Navigate } from 'react-router-dom'; +import { createBrowserRouter } from 'react-router-dom'; import ModalProvider from '@/components/common/modalProvider'; @@ -29,17 +28,6 @@ import DeleteConfirmPage from '@/pages/setting/DeleteConfirmPage'; import DeleteReasonPage from '@/pages/setting/DeleteReasonPage.tsx'; import PaymentHistory from '@/pages/setting/PaymentHistory'; -function ProtectedRoute({ children }: PropsWithChildren) { - //추후 실제 로그인 여부로 대체 필요 - const isLoggedIn = true; - - if (!isLoggedIn) { - return ; - } - - return children; -} - const router = createBrowserRouter([ { path: '/', @@ -75,10 +63,10 @@ const router = createBrowserRouter([ { path: '/', element: ( - + <> - + ), errorElement: , children: [ @@ -139,34 +127,23 @@ const router = createBrowserRouter([ { path: '/paymentHistory', element: ( - + <> + - + ), }, { path: '/deleteAccount', - element: ( - - - - ), + element: , }, { path: '/deleteAccount/confirm', - element: ( - - - - ), + element: , }, { path: '/withdraw', - element: ( - - - - ), + element: , errorElement: , }, ]); From 848a1e15c533c55bbc237f2196ee859e365c2d22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=97=B0=EC=A7=84?= Date: Thu, 14 Aug 2025 18:49:37 +0900 Subject: [PATCH 13/18] =?UTF-8?q?fix:=20=EB=B9=8C=EB=93=9C=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/home/useKeywordStates.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/home/useKeywordStates.ts b/src/hooks/home/useKeywordStates.ts index a815169..9333783 100644 --- a/src/hooks/home/useKeywordStates.ts +++ b/src/hooks/home/useKeywordStates.ts @@ -5,7 +5,7 @@ import { homeKeys } from '@/queryKey/queryKey'; // 이번 주 인기 키워드 훅 export const useWeeklyKeywords = () => { - return useCoreQuery(homeKeys.keywords().queryKey, getWeeklyKeywords, { + return useCoreQuery(homeKeys.keywords.queryKey, getWeeklyKeywords, { gcTime: 30 * 60 * 1000, retry: 3, }); From 4ba38b7549a5f53a5c504cd0bd7c78d9a61b194c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=97=B0=EC=A7=84?= Date: Thu, 14 Aug 2025 21:44:49 +0900 Subject: [PATCH 14/18] =?UTF-8?q?feat:=20=EB=A0=88=EC=9D=B4=EC=95=84?= =?UTF-8?q?=EC=9B=83=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/auth/commonAuthInput.tsx | 19 +------- src/layout/minimalLayout.tsx | 14 ++++++ src/pages/auth/UserSetting.tsx | 31 ++++++------ src/pages/common/Error.tsx | 2 +- src/pages/setting/PaymentHistory.tsx | 63 ++++++++++++------------- src/routes/routes.tsx | 40 ++++++++-------- 6 files changed, 83 insertions(+), 86 deletions(-) create mode 100644 src/layout/minimalLayout.tsx diff --git a/src/components/auth/commonAuthInput.tsx b/src/components/auth/commonAuthInput.tsx index 1392e26..02bb98a 100644 --- a/src/components/auth/commonAuthInput.tsx +++ b/src/components/auth/commonAuthInput.tsx @@ -1,8 +1,6 @@ import type { InputHTMLAttributes } from 'react'; import React from 'react'; -import formatInputNumber from '@/utils/formatPhoneNumber'; - import Button from '@/components/common/Button'; import AlertCircle from '@/assets/icons/alert-circle_Fill.svg?react'; @@ -58,21 +56,6 @@ const CommonAuthInput = React.forwardRef { - const rawValue = e.target.value; - const formatted = type === 'phoneNum' ? formatInputNumber(rawValue) : rawValue; - - // 외부에서 넘긴 onChange 핸들러에 적용된 값 전달 - if (rest.onChange) { - rest.onChange({ - ...e, - target: { - ...e.target, - value: formatted, - }, - }); - } - }} {...rest} /> {short &&
} @@ -96,7 +79,7 @@ const CommonAuthInput = React.forwardRef )} - {error &&
{errorMessage}
} + {error &&
{errorMessage}
} {error && }
); diff --git a/src/layout/minimalLayout.tsx b/src/layout/minimalLayout.tsx new file mode 100644 index 0000000..846ca3b --- /dev/null +++ b/src/layout/minimalLayout.tsx @@ -0,0 +1,14 @@ +import { Outlet } from 'react-router-dom'; + +import Header from '@/components/layout/Header'; + +function MinimalLayout() { + return ( +
+
+ +
+ ); +} + +export default MinimalLayout; diff --git a/src/pages/auth/UserSetting.tsx b/src/pages/auth/UserSetting.tsx index 927ff1b..6ebf710 100644 --- a/src/pages/auth/UserSetting.tsx +++ b/src/pages/auth/UserSetting.tsx @@ -8,7 +8,7 @@ import type { TUserSettingFormValues } from '@/types/auth/auth'; import { Gender } from '@/types/auth/auth'; import formatDateInput from '@/utils/formatDateInput'; -import formatInputNumber from '@/utils/formatPhoneNumber'; +import formatPhoneNumber from '@/utils/formatPhoneNumber'; import { userSettingSchema } from '@/utils/validation'; import { useAuth } from '@/hooks/auth/useAuth'; @@ -21,7 +21,6 @@ import Button from '../../components/common/Button'; import useAuthStore from '@/store/useAuthStore'; export default function User() { - const [error, setError] = useState(''); const [gender, setGender] = useState(Gender.MALE); const [agree1, setAgree1] = useState(false); const [agree2, setAgree2] = useState(false); @@ -40,6 +39,7 @@ export default function User() { resolver: zodResolver(userSettingSchema), defaultValues: { gender: Gender.MALE, + phoneNum: '010-', }, }); const { mutate: signupMutate, isPending } = useDefaultSignup; @@ -58,12 +58,12 @@ export default function User() { }, { onSuccess: () => { - setSocialId(-1); - navigate('/home'); + setSocialId(socialId ?? -1); + alert('성공적으로 회원가입되었습니다.'); + navigate('/'); }, onError: (err) => { - console.error(err); - setError(err.response?.data.message ?? '회원가입 중 문제가 발생했습니다.'); + alert(err.response?.data.message ?? '회원가입 중 문제가 발생했습니다.'); }, }, ); @@ -71,11 +71,11 @@ export default function User() { }; return ( -
+
navigate('/Join')} />
-
+
회원가입
@@ -87,7 +87,7 @@ export default function User() { children={'남자'} size="big-32" variant={`${gender == Gender.MALE ? 'mint' : 'white'}`} - className="px-[32px] !py-[16px]" + className="px-[32px] !py-[12px]" />
{ - const formatted = formatInputNumber(e.target.value); - onChange({ ...e, target: { ...e.target, value: formatted } }); + const formatted = formatPhoneNumber(e.target.value); + onChange(formatted); }} ref={ref} placeholder="전화번호 (010-xxxx-xxxx)" title="Phone Number" + error={!!errors.phoneNum?.message} + errorMessage={errors.phoneNum?.message} /> )} />
전화번호는 이메일을 잊었을 때 찾기 위한 용도입니다. 정확하게 기재해주세요. @@ -163,13 +165,12 @@ export default function User() { 이용약관 동의
-
{error}
- ))} -
- - -
+ > + {tab} 설정 + + ))} +
-
- {activeTab === '알람' && } - {activeTab === '멤버십' && } - {activeTab === '정보' && } + +
+ +
+ {activeTab === '알람' && } + {activeTab === '멤버십' && } + {activeTab === '정보' && } +
); diff --git a/src/components/modal/deleteBookmarkModal.tsx b/src/components/modal/deleteBookmarkModal.tsx index ed449cf..669823a 100644 --- a/src/components/modal/deleteBookmarkModal.tsx +++ b/src/components/modal/deleteBookmarkModal.tsx @@ -15,7 +15,6 @@ function DeleteBookmarkModal({ onClose, isOpen, changeState }: TDeleteBookmarkMo }, [isOpen]); const handleDelete = () => { - // Logic to delete the bookmarked date course changeState(false); onClose(); }; diff --git a/src/components/settingTab/AlarmSetting.tsx b/src/components/settingTab/AlarmSetting.tsx index e839750..963b3d8 100644 --- a/src/components/settingTab/AlarmSetting.tsx +++ b/src/components/settingTab/AlarmSetting.tsx @@ -64,9 +64,9 @@ export default function AlarmSetting() { ]; return ( -
+
{items.map(({ label, key }) => ( -
+

{label}

handleToggle(key)} onLabel="ON" offLabel="OFF" />
diff --git a/src/components/settingTab/InfoSetting.tsx b/src/components/settingTab/InfoSetting.tsx index 9a5d7d7..ff36943 100644 --- a/src/components/settingTab/InfoSetting.tsx +++ b/src/components/settingTab/InfoSetting.tsx @@ -81,7 +81,7 @@ export default function InfoSetting() { }; return ( -
+
{/* 닉네임 */} {/* 이메일 */} - {}} - className="pointer-events-none" - placeholder="이메일" - /> +
+

{'이메일'}

+
+ {infoLoading ? '불러오는 중' : infoError ? '' : email} +
+
{/* 비밀번호 변경 */} diff --git a/src/components/settingTab/MembershipSetting.tsx b/src/components/settingTab/MembershipSetting.tsx index 7d2fa0d..3d68009 100644 --- a/src/components/settingTab/MembershipSetting.tsx +++ b/src/components/settingTab/MembershipSetting.tsx @@ -47,14 +47,14 @@ export default function MembershipSetting() { }; return ( -
+

현재 등급은 {gradeText} 입니다.

다음 결제 예정일 : 2025.06.14

-
+

자동 결제

diff --git a/src/components/settingTab/mobileSettingTab.tsx b/src/components/settingTab/mobileSettingTab.tsx new file mode 100644 index 0000000..63acdd3 --- /dev/null +++ b/src/components/settingTab/mobileSettingTab.tsx @@ -0,0 +1,33 @@ +import LogoutSvg from '@/assets/icons/Logout_Blank.svg?react'; + +type TMobileSettingTab = { + setActiveTab: (activeTab: '알람' | '멤버십' | '정보') => void; + activeTab: '알람' | '멤버십' | '정보'; + handleLogout: () => void; + logoutPending: boolean; +}; +export default function MobileSettingTab({ setActiveTab, activeTab, handleLogout, logoutPending }: TMobileSettingTab) { + return ( +
+ {['알람', '멤버십', '정보'].map((tab) => ( + + ))} + + +
+ ); +} From 776ebff4dee8962e2db3f24a988910c68e5e7e9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=97=B0=EC=A7=84?= Date: Fri, 15 Aug 2025 01:16:47 +0900 Subject: [PATCH 17/18] =?UTF-8?q?feat:=20=EC=BF=BC=EB=A6=AC=ED=82=A4=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 2 +- src/components/modal/SettingModal.tsx | 2 +- src/components/settingTab/InfoSetting.tsx | 7 ++----- src/hooks/customQuery.ts | 1 - src/queryKey/queryKey.ts | 6 +----- 5 files changed, 5 insertions(+), 13 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 0e31161..6edceb7 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,7 +8,7 @@ import { alarmKeys } from '@/queryKey/queryKey'; import router from '@/routes/routes'; function App() { - const refetchKeys = useMemo(() => [alarmKeys.all.queryKey], []); + const refetchKeys = useMemo(() => [alarmKeys._def], []); return ( diff --git a/src/components/modal/SettingModal.tsx b/src/components/modal/SettingModal.tsx index 9768ee1..b6cc322 100644 --- a/src/components/modal/SettingModal.tsx +++ b/src/components/modal/SettingModal.tsx @@ -64,7 +64,7 @@ export default function SettingsModal({ onClose, defaultTab = '알람' }: ISetti
-
+
{activeTab === '알람' && } {activeTab === '멤버십' && } {activeTab === '정보' && } diff --git a/src/components/settingTab/InfoSetting.tsx b/src/components/settingTab/InfoSetting.tsx index ff36943..7a43a41 100644 --- a/src/components/settingTab/InfoSetting.tsx +++ b/src/components/settingTab/InfoSetting.tsx @@ -48,10 +48,7 @@ export default function InfoSetting() { setInitialNickname(next); localStorage.setItem('nickname', next); - queryClient.invalidateQueries({ queryKey: memberKeys.all.queryKey }); - queryClient.setQueryData(memberKeys.memberGrade.queryKey, (old: any) => - old?.result ? { ...old, result: { ...old.result, username: next } } : old, - ); + queryClient.invalidateQueries({ queryKey: memberKeys._def }); } else { alert(res?.message ?? '닉네임 변경에 실패했습니다.'); } @@ -81,7 +78,7 @@ export default function InfoSetting() { }; return ( -
+
{/* 닉네임 */} ( queryKey: keyName, queryFn: query, ...options, - staleTime: options?.staleTime ?? 1000 * 60 * 5, }); } diff --git a/src/queryKey/queryKey.ts b/src/queryKey/queryKey.ts index 66f7d82..44372b8 100644 --- a/src/queryKey/queryKey.ts +++ b/src/queryKey/queryKey.ts @@ -1,18 +1,15 @@ import { createQueryKeys } from '@lukemorales/query-key-factory'; export const regionKeys = createQueryKeys('region', { - all: null, search: (keyword: string) => ['search', keyword], }); export const alarmKeys = createQueryKeys('alarm', { - all: null, getAlarm: (size: number, cursor?: number) => ['getAlarm', size, cursor], alarmSettings: () => ['alarmSettings'], }); export const homeKeys = createQueryKeys('home', { - all: null, getUserGrade: () => ['user', 'grade'], dateCourseSave: () => ['date-courses', 'saved-count'], weather: (startDate: string, regionId: number) => ['weather', 'forecast', startDate, regionId], @@ -24,11 +21,10 @@ export const homeKeys = createQueryKeys('home', { }); export const noticeKeys = createQueryKeys('notice', { - all: null, getAllNotices: (page: number, size: number, noticeCategory: 'SERVICE' | 'SYSTEM') => [page, size, noticeCategory], }); export const memberKeys = createQueryKeys('member', { - all: null, memberInfo: null, memberGrade: null, + // memberKeys안에 있는 걸 모두 초기화 하고 싶으면 alarmKeys._def로 호출하면 됩니다! }); From 949c8c8077de5ab818014a5809ffe77d5b31b49f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=97=B0=EC=A7=84?= Date: Fri, 15 Aug 2025 01:18:57 +0900 Subject: [PATCH 18/18] =?UTF-8?q?fix:=20=EC=BD=94=EB=93=9C=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/common/Error.tsx | 2 +- src/routes/routes.tsx | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/pages/common/Error.tsx b/src/pages/common/Error.tsx index cbd181d..a61620c 100644 --- a/src/pages/common/Error.tsx +++ b/src/pages/common/Error.tsx @@ -8,7 +8,7 @@ export default function Error() {

서비스 이용에 불편을 드려 죄송합니다

-

+

요청하신 페이지를 찾을 수 없습니다
경로가 잘못되었거나, 인터넷 연결이 불안정할 수 있습니다

diff --git a/src/routes/routes.tsx b/src/routes/routes.tsx index ded51d1..e042aae 100644 --- a/src/routes/routes.tsx +++ b/src/routes/routes.tsx @@ -131,19 +131,19 @@ const router = createBrowserRouter([ errorElement: , children: [ { - path: '/paymentHistory', + path: 'paymentHistory', element: , }, { - path: '/deleteAccount', + path: 'deleteAccount', element: , }, { - path: '/deleteAccount/confirm', + path: 'deleteAccount/confirm', element: , }, { - path: '/withdraw', + path: 'withdraw', element: , }, ],