Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export default [
{
rules: {
'prettier/prettier': 'warn',
'no-console': ['error', { allow: ['warn', 'error'] }],
},
},
tsConfig,
Expand Down
12 changes: 10 additions & 2 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
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 router from '@/routes/routes';

function App() {
return <RouterProvider router={router} />;
const refetchKeys = useMemo(() => [alarmKeys.all.queryKey], []);
return (
<DeviceTokenProvider refetchKeys={refetchKeys}>
<RouterProvider router={router} />
</DeviceTokenProvider>
);
}

export default App;
22 changes: 9 additions & 13 deletions src/api/axiosInstance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,30 +34,26 @@ axiosInstance.interceptors.response.use(
const refreshResponse = await refresh();

if (refreshResponse.code === '200') {
console.log('refreshToken이 재발급 되었습니다');
isRedirecting = false;

return axiosInstance(error.config);
}
} catch (errors) {
if (axios.isAxiosError(errors)) {
const refreshError = error as AxiosError<IRefreshResponse>;
const refreshError = errors as AxiosError<IRefreshResponse>;
if (refreshError.response?.data.message === 'The token is null.') {
console.log('refreshToken이 없습니다. 로그인 페이지로 이동합니다.');
console.error('refreshToken이 없습니다. 로그인 페이지로 이동합니다.');
void logout();
} else if (refreshError.response?.data.message === 'The token is invalid.') {
console.log('refreshToken이 만료되었습니다. 로그인 페이지로 이동합니다.');
logout();
console.error('refreshToken이 만료되었습니다. 로그인 페이지로 이동합니다.');
void logout();
} else {
if (refreshError.response?.data.message === 'Incorrect password.') {
alert('Your email or password is incorrect.');
} else {
console.log('알 수 없는 오류가 발생했습니다', errors);
logout();
}
console.error('알 수 없는 오류가 발생했습니다', errors);
void logout();
}
} else {
console.log('알 수 없는 오류가 발생했습니다', errors);
logout();
console.error('알 수 없는 오류가 발생했습니다', errors);
void logout();
}

return Promise.reject(errors);
Expand Down
2 changes: 1 addition & 1 deletion src/components/common/PasswordEdit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export default function PasswordEditSection() {
alert('비밀번호가 변경되었습니다.');
handleCancel();
},
onError: (err) => {
onError: (err: any) => {
const msg = (err as any)?.response?.data?.message ?? '비밀번호 변경에 실패했습니다.';
alert(msg);
},
Expand Down
1 change: 0 additions & 1 deletion src/components/dateCourse/dateCourse.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ function DateCourse({ defaultOpen = false }: { defaultOpen?: boolean }) {
} else {
setIsBookmarked(!isBookmarked);
}
// console.log('북마크 해제');
};

useEffect(() => {
Expand Down
2 changes: 1 addition & 1 deletion src/components/dateCourse/dateCourseSearchFilterOption.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}
/>
Expand Down
1 change: 0 additions & 1 deletion src/components/modal/dateCourseSearchFilterModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ export default function DateCourseSearchFilterModal({ onClose }: TDateCourseSear
const [errorMessages, setErrorMessages] = useState<string[]>(Array(7).fill(''));

const handleSearch = () => {
console.log('선택된 필터:', answers);
onClose();
};

Expand Down
4 changes: 4 additions & 0 deletions src/components/modal/regionModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -23,6 +26,7 @@ function RegionModal({ onClose }: IRegionModalProps) {
},
{
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: HomeKeys.userRegion().queryKey });
onClose();
},
},
Expand Down
51 changes: 40 additions & 11 deletions src/firebase/firebase.ts
Original file line number Diff line number Diff line change
@@ -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: 'AIzaSyAjZqK2lhCOeX_P2Sf-_2IGEFlORchcO5w',
Expand All @@ -13,22 +14,39 @@ const firebaseConfig = {
};

const app = initializeApp(firebaseConfig);
const messaging = getMessaging(app);
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);
}
})();

export async function generateToken(): Promise<string | null> {
const m = await ensureMessaging();
if (!m) return null;

// 권한 요청 (이미 허용/거부된 상태면 브라우저가 적절히 동작)
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, {
const token = await getToken(m, {
vapidKey: import.meta.env.VITE_FIREBASE_VAPID_KEY,
serviceWorkerRegistration: await navigator.serviceWorker.ready,
});
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 {
Expand All @@ -39,3 +57,14 @@ export const registerServiceWorker = async () => {
console.error('Service Worker registration failed:', err);
}
};

export async function deleteFcmToken(): Promise<boolean> {
const m = await ensureMessaging();
if (!m) return false;
try {
return await deleteToken(m);
} catch (e) {
console.error('FCM deleteToken 실패:', e);
return false;
}
}
39 changes: 0 additions & 39 deletions src/hooks/alarm/useDeviceToken.ts

This file was deleted.

8 changes: 8 additions & 0 deletions src/hooks/alarm/usePostDeviceToken.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { useCoreMutation } from '../customQuery';

import { postDeviceToken } from '@/api/alarm/alarm';

export function useFirebase() {
const postDeviceTokenMutation = useCoreMutation(postDeviceToken);
return { postDeviceTokenMutation };
}
9 changes: 4 additions & 5 deletions src/hooks/auth/useAccount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import type {
TChangePasswordMutationResult,
TChangePasswordPayload,
} from '@/types/auth/account';
import type { TUseMutationCustomOptions } from '@/types/common/common';
import type { TResetPreferencesResponse } from '@/types/dates/preferences';

import { useCoreMutation, useCoreQuery } from '@/hooks/customQuery';
Expand All @@ -32,8 +31,8 @@ export function useAccount() {
}

// 회원 탈퇴
function useDeleteMember(options?: TUseMutationCustomOptions<void, void>) {
return useCoreMutation<void, void>(deleteMember, options);
function useDeleteMember() {
return useCoreMutation(deleteMember);
}

// 사용자 정보 조회
Expand All @@ -47,8 +46,8 @@ export function useAccount() {
}

// 취향 데이터 초기화
function useResetPreferences(options?: TUseMutationCustomOptions<TResetPreferencesResponse, void>) {
return useCoreMutation<TResetPreferencesResponse, void>(resetPreferences, options);
function useResetPreferences() {
return useCoreMutation<TResetPreferencesResponse, void>(resetPreferences);
}

return { useChangePassword, useChangeNickname, useDeleteMember, useGetMemberInfo, useGetMemberGrade, useResetPreferences };
Expand Down
58 changes: 51 additions & 7 deletions src/hooks/customQuery.ts
Original file line number Diff line number Diff line change
@@ -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<TQueryFnData, TData = TQueryFnData>(
keyName: QueryKey,
query: QueryFunction<TQueryFnData, QueryKey>,
Expand All @@ -17,13 +18,56 @@ export function useCoreQuery<TQueryFnData, TData = TQueryFnData>(
});
}

//options 타입을 제네릭 변경
export function useCoreMutation<T, U>(mutation: MutationFunction<T, U>, options?: TUseMutationCustomOptions<T, U>) {
return useMutation({
export function useCoreMutation<
TData,
TVariables,
TError = AxiosError<{ message?: string }>, // 필요 시 TResponseError 등으로 대체
TContext extends { prevData?: unknown } = { prevData?: unknown },
TCache = unknown,
>(mutation: MutationFunction<TData, TVariables>, options?: TUseMutationCustomOptions<TData, TVariables, TError, TContext, TCache>) {
const {
optimisticUpdate, // { key: QueryKey; updateFn: (old: TCache | undefined, vars: TVariables) => TCache }
invalidateKeys,
userOnError,
userOnSuccess,
...rest // retry, gcTime 등 표준 옵션(UseMutationOptions 호환)
} = options ?? {};

return useMutation<TData, TError, TVariables, TContext>({
mutationFn: mutation,
onError: (error) => {
toast.error(error.response?.data.message || 'An error occurred.');

// 중요: onMutate는 반드시 TContext | undefined를 반환해야 함
onMutate: async (vars): Promise<TContext | undefined> => {
if (!optimisticUpdate) return undefined;

await queryClient.cancelQueries({ queryKey: optimisticUpdate.key });

const prevData = queryClient.getQueryData<TCache>(optimisticUpdate.key);

// 캐시 타입 안전하게 업데이트
queryClient.setQueryData<TCache>(optimisticUpdate.key, (old) => optimisticUpdate.updateFn(old as TCache | undefined, vars));

// prevData를 컨텍스트로 보관
return { prevData } as TContext;
},
...options,

onError: (error, vars, ctx) => {
// 롤백
if (optimisticUpdate && ctx?.prevData !== undefined) {
queryClient.setQueryData<TCache>(optimisticUpdate.key, ctx.prevData as TCache);
}
userOnError?.(error, vars, ctx);
},

onSuccess: async (data, vars, ctx) => {
// 꼭 invalidate가 필요한 키만
if (invalidateKeys?.length) {
await Promise.all(invalidateKeys.map((key) => queryClient.invalidateQueries({ queryKey: key })));
}
userOnSuccess?.(data, vars, ctx);
},

// 나머지 표준 옵션 주입
...rest,
});
}
6 changes: 2 additions & 4 deletions src/pages/TestInputPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,7 @@ export default function TestInputPage() {
placeholder="검색어 입력를 입력하세요"
value={search}
onChange={(e) => setSearch(e.target.value)} // 입력 상태 저장
onSearchClick={() => {
console.log('검색 실행:', search); // 검색 아이콘 클릭 시 예시
}}
onSearchClick={() => {}}
/>
</div>

Expand All @@ -38,7 +36,7 @@ export default function TestInputPage() {
value={nickname}
onChange={(e) => setNickname(e.target.value)}
onCancel={() => setNickname('')}
onSubmit={() => console.log('닉네임 저장:', nickname)}
onSubmit={() => {}}
/>
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/pages/auth/LoginPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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('잘못된 정보를 입력하였습니다.');
},
},
Expand Down
4 changes: 2 additions & 2 deletions src/pages/auth/UserSetting.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ export default function User() {
navigate('/home');
},
onError: (err) => {
console.log(err);
setError(err.response?.data.message!);
console.error(err);
setError(err.response?.data.message ?? '회원가입 중 문제가 발생했습니다.');
},
},
);
Expand Down
Loading
Loading