첫 커밋
This commit is contained in:
79
src/shared/configs/axios/index.ts
Normal file
79
src/shared/configs/axios/index.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import * as Sentry from '@sentry/react';
|
||||
import axios, { AxiosError, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
|
||||
import { WHITE_LIST_URLS } from '@/shared/api/urls';
|
||||
import { StorageKeys } from '@/shared/constants/local-storage';
|
||||
import { checkIsAxiosError, getLocalStorage } from '@/shared/lib';
|
||||
import { finalizeConfig, extractAccessToken, extractRequestId } from './utils';
|
||||
import { HEADER_USER_AGENT } from '@/shared/constants/url';
|
||||
|
||||
const onRequestFulfilled = (config: InternalAxiosRequestConfig) => {
|
||||
config.headers['Content-Type'] = 'application/json;charset=UTF-8';
|
||||
config.headers['X-User-Agent'] = HEADER_USER_AGENT;
|
||||
|
||||
if(WHITE_LIST_URLS.includes(config?.url ?? '')){
|
||||
if(config.headers.hasOwnProperty('Authorization')){
|
||||
delete config.headers.Authorization;
|
||||
}
|
||||
}
|
||||
else{
|
||||
const accessToken = getLocalStorage(StorageKeys.AccessToken);
|
||||
const tokenType = getLocalStorage(StorageKeys.TokenType);
|
||||
config.headers.Authorization = `${tokenType} ${accessToken}`;
|
||||
// config.headers['X-Request-id'] = getLocalStorage(StorageKeys.requestId) ?? '';
|
||||
}
|
||||
return finalizeConfig(config);
|
||||
};
|
||||
|
||||
const onRequestRejected = (error: any) => {
|
||||
const { method, url, params, data, headers } = error.config;
|
||||
Sentry.setContext('API Request Detail', {
|
||||
method,
|
||||
url,
|
||||
params,
|
||||
data,
|
||||
headers,
|
||||
});
|
||||
|
||||
return Promise.reject(error);
|
||||
};
|
||||
|
||||
const onResponseFulfilled = (response: AxiosResponse) => {
|
||||
extractAccessToken(response);
|
||||
extractRequestId(response);
|
||||
return {
|
||||
...response,
|
||||
data: response.data.data,
|
||||
};
|
||||
};
|
||||
|
||||
const onResponseRejected = (error: AxiosError) => {
|
||||
if (error?.response) {
|
||||
const { data, status } = error.response;
|
||||
extractRequestId(error?.response);
|
||||
Sentry.setContext('API Response Detail', {
|
||||
status,
|
||||
data,
|
||||
});
|
||||
}
|
||||
return new Promise((_resolve, reject) => {
|
||||
if (checkIsAxiosError(error)) {
|
||||
// iOS의 경우 window에서 unload event가 일어날때 네트워크 에러가 발생하곤 해서 이런 케이스를 방지하기 위해 지연시킴
|
||||
// location.href, location.reload 등으로 unload event가 일어나면 자바스크립트 런타임이 초기화되므로 settimeout으로 인한 네트워크 에러가 트리거 x
|
||||
setTimeout(() => reject(error), 300);
|
||||
} else {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const initAxios = () => {
|
||||
axios.defaults.withCredentials = true;
|
||||
axios.defaults.timeout = 15000;
|
||||
axios.defaults.transitional = {
|
||||
clarifyTimeoutError: true,
|
||||
forcedJSONParsing: true,
|
||||
silentJSONParsing: true,
|
||||
};
|
||||
axios.interceptors.request.use(onRequestFulfilled, onRequestRejected);
|
||||
axios.interceptors.response.use(onResponseFulfilled, onResponseRejected);
|
||||
};
|
||||
28
src/shared/configs/axios/utils.ts
Normal file
28
src/shared/configs/axios/utils.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { InternalAxiosRequestConfig, AxiosResponse } from 'axios';
|
||||
import { StorageKeys } from '@/shared/constants/local-storage';
|
||||
import { setLocalStorage } from '@/shared/lib';
|
||||
|
||||
export const finalizeConfig = (config: InternalAxiosRequestConfig) => {
|
||||
const { params, data } = config;
|
||||
return {
|
||||
...config,
|
||||
params,
|
||||
data,
|
||||
};
|
||||
};
|
||||
|
||||
export const extractAccessToken = (response: AxiosResponse): void => {
|
||||
const authHeader = response?.headers?.['authorization'];
|
||||
if (authHeader) {
|
||||
const accessToken = authHeader.substring(7);
|
||||
setLocalStorage(StorageKeys.Jwt, accessToken);
|
||||
}
|
||||
};
|
||||
|
||||
export const extractRequestId = (response: AxiosResponse): void => {
|
||||
const requestIdHeader = response?.headers?.['x-request-id'];
|
||||
if (requestIdHeader) {
|
||||
const requestId = requestIdHeader.replaceAll(', *', '');
|
||||
setLocalStorage(StorageKeys.RequestId, requestId);
|
||||
}
|
||||
};
|
||||
9
src/shared/configs/config.development.ts
Normal file
9
src/shared/configs/config.development.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { RELEASE_VERSION } from '@/shared/constants/environment';
|
||||
import { ConfigMap } from './types';
|
||||
/**
|
||||
* development
|
||||
*/
|
||||
export const DEVELOPMENT_CONFIG_MAP: ConfigMap = {
|
||||
APP_NAME: 'NICE-Development',
|
||||
WEB_VERSION: RELEASE_VERSION ?? '1',
|
||||
};
|
||||
10
src/shared/configs/config.production.ts
Normal file
10
src/shared/configs/config.production.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { RELEASE_VERSION } from '@/shared/constants/environment';
|
||||
import { ConfigMap } from './types';
|
||||
|
||||
/**
|
||||
* production
|
||||
*/
|
||||
export const PRODUCTION_CONFIG_MAP: ConfigMap = {
|
||||
APP_NAME: 'NICE-Production',
|
||||
WEB_VERSION: RELEASE_VERSION ?? '1',
|
||||
};
|
||||
28
src/shared/configs/index.ts
Normal file
28
src/shared/configs/index.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { Env } from './types';
|
||||
import { RELEASE_VERSION } from '@/shared/constants/environment';
|
||||
import { PRODUCTION_CONFIG_MAP } from './config.production';
|
||||
import { DEVELOPMENT_CONFIG_MAP } from './config.development';
|
||||
|
||||
|
||||
// env와 bankKey에따라 mockup, 개발계(development), 검증계(production)의 은행 설정 가져오기
|
||||
function getConfig(env: Env) {
|
||||
function getCurrEnvConfigMap() {
|
||||
if (env === 'development') return DEVELOPMENT_CONFIG_MAP;
|
||||
if (env === 'production') return PRODUCTION_CONFIG_MAP;
|
||||
return DEVELOPMENT_CONFIG_MAP;
|
||||
}
|
||||
return getCurrEnvConfigMap();
|
||||
}
|
||||
|
||||
/**
|
||||
* 환경변수에서 전달받은 환경값 ( mockup | development | production )
|
||||
* 이 값으로 api 호출 URL 세팅
|
||||
*/
|
||||
const ENV: Env = (import.meta.env.VITE_APP_ENV ?? 'development') as Env;
|
||||
|
||||
const WEB_VERSION = RELEASE_VERSION ?? '1';
|
||||
|
||||
export const config = getConfig(ENV);
|
||||
console.log(`%cENV: %c${ENV}`, 'font-weight:bold;', 'color:red;font-weight:bold;');
|
||||
console.log(`%cWEB_VERSION: %c${WEB_VERSION}`, 'font-weight:bold;', 'color:red;font-weight:bold;');
|
||||
console.log('config', config);
|
||||
20
src/shared/configs/query.ts
Normal file
20
src/shared/configs/query.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { QueryClient } from '@tanstack/react-query';
|
||||
|
||||
const globalQueryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
// 사용자에게 에러상태를 즉시 보여주기 위해 retry false로 지정
|
||||
retry: false,
|
||||
refetchOnWindowFocus: true,
|
||||
throwOnError: true,
|
||||
// suspense: true,
|
||||
networkMode: 'always',
|
||||
},
|
||||
mutations: {
|
||||
throwOnError: true,
|
||||
networkMode: 'always',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const getGlobalQueryClient = (): QueryClient => globalQueryClient;
|
||||
196
src/shared/configs/sentry/index.tsx
Normal file
196
src/shared/configs/sentry/index.tsx
Normal file
@@ -0,0 +1,196 @@
|
||||
/* eslint-disable react-refresh/only-export-components */
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { OverlayProvider } from 'overlay-kit';
|
||||
import React, { lazy } from 'react';
|
||||
import {
|
||||
createRoutesFromChildren,
|
||||
matchRoutes,
|
||||
Navigate,
|
||||
Route,
|
||||
Routes,
|
||||
useLocation,
|
||||
useNavigationType,
|
||||
} from 'react-router';
|
||||
import { createBrowserRouter } from 'react-router-dom';
|
||||
|
||||
import { AppChildren } from '@/app/app-children';
|
||||
import { GlobalAPIErrorBoundary } from '@/widgets/error-boundaries';
|
||||
import { NotFoundError } from '@/widgets/fallbacks';
|
||||
import { ProtectedRoute } from '@/widgets/protected-route';
|
||||
import { PullToRefreshRoute } from '@/widgets/pull-to-refresh/pull-to-refresh-route';
|
||||
import { SubLayout } from '@/widgets/sub-layout';
|
||||
import { IS_PROD } from '@/shared/constants/environment';
|
||||
import { ROUTE_NAMES } from '@/shared/constants/route-names';
|
||||
import { toCamelCase } from '@/shared/lib/to-camel-case';
|
||||
|
||||
export const initSentry = () => {
|
||||
if (IS_PROD) {
|
||||
Sentry.init({
|
||||
environment: import.meta.env.VITE_APP_ENV,
|
||||
dsn: 'https://ddd8755ce025f753e8521af5b1034a93@o4507569039867904.ingest.us.sentry.io/4507569041244160',
|
||||
integrations: [
|
||||
Sentry.browserTracingIntegration(),
|
||||
Sentry.browserProfilingIntegration(),
|
||||
Sentry.replayIntegration(),
|
||||
Sentry.reactRouterV6BrowserTracingIntegration({
|
||||
useEffect: React.useEffect,
|
||||
useLocation,
|
||||
useNavigationType,
|
||||
createRoutesFromChildren,
|
||||
matchRoutes,
|
||||
}),
|
||||
],
|
||||
normalizeDepth: 6,
|
||||
// Performance Monitoring
|
||||
tracesSampleRate: 1.0, // Capture 100% of the transactions
|
||||
// Set 'tracePropagationTargets' to control for which URLs distributed tracing should be enabled
|
||||
tracePropagationTargets: [
|
||||
'http://3.35.79.250:8090'
|
||||
],
|
||||
// Session Replay
|
||||
replaysSessionSampleRate: 0.1, // This sets the sample rate at 10%. You may want to change it to 100% while in development and then sample at a lower rate in production.
|
||||
replaysOnErrorSampleRate: 1.0, // If you're not already sampling the entire session, change the sample rate to 100% when sampling sessions where errors occur.
|
||||
|
||||
profilesSampleRate: 1.0,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const sentryCreateBrowserRouter = Sentry.wrapCreateBrowserRouterV6(createBrowserRouter);
|
||||
|
||||
const modules = import.meta.glob('~/pages/**/*.tsx');
|
||||
|
||||
const lazyLoad = (path: string) => {
|
||||
const module = modules[`${path}.tsx`];
|
||||
if (!module) {
|
||||
throw new Error(`Module not found: ${path}`);
|
||||
}
|
||||
|
||||
const namedExportKebab = path.split('/').pop()?.replace('.tsx', '') as string;
|
||||
const namedExportCamel = toCamelCase(namedExportKebab);
|
||||
return lazy(() => module().then((load: any) => ({
|
||||
default: load[namedExportCamel]
|
||||
})));
|
||||
};
|
||||
|
||||
const HomePage = lazyLoad('/src/pages/home/home-page');
|
||||
const TransactionPages = lazyLoad('/src/pages/transaction/transaction-pages');
|
||||
const SettlementPages = lazyLoad('/src/pages/settlement/settlement-pages');
|
||||
const BusinessMemberPages = lazyLoad('/src/pages/business-member/business-member-pages');
|
||||
const PaymentPages = lazyLoad('/src/pages/payment/payment-pages');
|
||||
const AccountPages = lazyLoad('/src/pages/account/account-pages');
|
||||
const TaxPages = lazyLoad('/src/pages/tax/tax-pages');
|
||||
const AdditionalServicePages = lazyLoad('/src/pages/additional-service/additional-service-pages');
|
||||
const SupportPages = lazyLoad('/src/pages/support/support-pages');
|
||||
const SettingPage = lazyLoad('/src/pages/setting/setting-page');
|
||||
const AlarmPages = lazyLoad('/src/pages/alarm/alarm-pages');
|
||||
/*
|
||||
const IntroPage = lazyLoad('/src/pages/intro/intro-page');
|
||||
const StartPage = lazyLoad('/src/pages/sign-up/start/start-page');
|
||||
const AppAuthPage = lazyLoad('/src/pages/sign-up/app-auth/app-auth-page');
|
||||
const MobileVerificationPage = lazyLoad('/src/pages/sign-up/mobile-verification/mobile-verification-page');
|
||||
const SignUpPages = lazyLoad('/src/pages/sign-up/sign-up-pages');
|
||||
const IssueWalletPages = lazyLoad('/src/pages/issue-wallet/issue-wallet-pages');
|
||||
const MyWalletPages = lazyLoad('/src/pages/my-wallet/my-wallet-pages');
|
||||
const ExchangePages = lazyLoad('/src/pages/exchange/exchange-pages');
|
||||
const MyBankAccountPage = lazyLoad('/src/pages/my-bank-account/my-bank-account-page');
|
||||
const SecurityPage = lazyLoad('/src/pages/security/security-page');
|
||||
const AppVersionPage = lazyLoad('/src/pages/app-version/app-version-page');
|
||||
const SettingFontSizePage = lazyLoad('/src/pages/setting-font-size/setting-font-size-page');
|
||||
const CommunityPages = lazyLoad('/src/pages/community/community-pages');
|
||||
const MenuPage = lazyLoad('/src/pages/menu/menu-page');
|
||||
const LoginPage = lazyLoad('/src/pages/login/login-page');
|
||||
const ReLoginPage = lazyLoad('/src/pages/login/re-login-page');
|
||||
const HomePage = lazyLoad('/src/pages/home/home-page');
|
||||
const ShopListPage = lazyLoad('/src/pages/shop-list/shop-list-page');
|
||||
const NotificationPages = lazyLoad('/src/pages/notification/notification-pages');
|
||||
const PaymentPages = lazyLoad('/src/pages/payment/payment-pages');
|
||||
const VoucherPages = lazyLoad('/src/pages/voucher/voucher-pages');
|
||||
const PaymentGuidePage = lazyLoad('/src/pages/payment/payment-guide-page');
|
||||
const InputShopWalletAddressPage = lazyLoad('/src/pages/payment/input-shop-wallet-address-page');
|
||||
*/
|
||||
|
||||
export const SentryRoutes = Sentry.withSentryReactRouterV6Routing(Routes);
|
||||
const Pages = () => {
|
||||
return (
|
||||
<OverlayProvider>
|
||||
<GlobalAPIErrorBoundary>
|
||||
<AppChildren />
|
||||
<SentryRoutes>
|
||||
<Route element={<SubLayout />}>
|
||||
<Route element={<ProtectedRoute />}>
|
||||
<Route path={ROUTE_NAMES.home} element={<HomePage />} />
|
||||
<Route path={ROUTE_NAMES.transaction.base} element={<TransactionPages />} />
|
||||
<Route path={ROUTE_NAMES.settlement.base} element={<SettlementPages />} />
|
||||
<Route path={ROUTE_NAMES.businessMember.base} element={<BusinessMemberPages />} />
|
||||
<Route path={ROUTE_NAMES.payment.base} element={<PaymentPages />} />
|
||||
<Route path={ROUTE_NAMES.account.base} element={<AccountPages />} />
|
||||
<Route path={ROUTE_NAMES.tax.base} element={<TaxPages />} />
|
||||
<Route path={ROUTE_NAMES.additionalService.base} element={<AdditionalServicePages />} />
|
||||
<Route path={ROUTE_NAMES.support.base} element={<SupportPages />} />
|
||||
<Route path={ROUTE_NAMES.setting} element={<SettingPage />} />
|
||||
<Route path={ROUTE_NAMES.alarm.base} element={<AlarmPages />} />
|
||||
</Route>
|
||||
<Route path="*" element={<NotFoundError />} />
|
||||
</Route>
|
||||
<Route element={<PullToRefreshRoute />}>
|
||||
<Route element={<ProtectedRoute />}>
|
||||
|
||||
|
||||
</Route>
|
||||
</Route>
|
||||
|
||||
|
||||
{
|
||||
/*
|
||||
<Route path={ROUTE_NAMES.intro} element={<IntroPage />} />
|
||||
<Route path={ROUTE_NAMES.start} element={<StartPage />} />
|
||||
<Route path={ROUTE_NAMES.appAuth} element={<AppAuthPage />} />
|
||||
<Route element={<SubLayout />}>
|
||||
<Route path={ROUTE_NAMES.mobileVerification} element={<MobileVerificationPage />} />
|
||||
<Route path={ROUTE_NAMES.signUp.base} element={<SignUpPages />} />
|
||||
<Route element={<ProtectedRoute />}>
|
||||
<Route path={ROUTE_NAMES.issueWallet.base} element={<IssueWalletPages />} />
|
||||
<Route path={ROUTE_NAMES.myWallet.base} element={<MyWalletPages />} />
|
||||
<Route path={ROUTE_NAMES.exchange.base} element={<ExchangePages />} />
|
||||
<Route path={ROUTE_NAMES.myBankAccount} element={<MyBankAccountPage />} />
|
||||
<Route path={ROUTE_NAMES.security} element={<SecurityPage />} />
|
||||
<Route path={ROUTE_NAMES.appVersion} element={<AppVersionPage />} />
|
||||
<Route path={ROUTE_NAMES.settingFontSize} element={<SettingFontSizePage />} />
|
||||
<Route path={ROUTE_NAMES.community.base} element={<CommunityPages />} />
|
||||
<Route path={ROUTE_NAMES.shopList} element={<ShopListPage />} />
|
||||
<Route path={ROUTE_NAMES.notification.base} element={<NotificationPages />} />
|
||||
<Route path={ROUTE_NAMES.payment.base} element={<PaymentPages />} />
|
||||
<Route path={ROUTE_NAMES.menu} element={<MenuPage />} />
|
||||
<Route path={ROUTE_NAMES.voucher.base} element={<VoucherPages />} />
|
||||
</Route>
|
||||
<Route path="*" element={<NotFoundError />} />
|
||||
</Route>
|
||||
<Route path={ROUTE_NAMES.login} element={<LoginPage />} />
|
||||
<Route path={ROUTE_NAMES.reLogin} element={<ReLoginPage />} />
|
||||
<Route element={<PullToRefreshRoute />}>
|
||||
<Route element={<ProtectedRoute />}>
|
||||
<Route path={ROUTE_NAMES.home} element={<HomePage />} />
|
||||
</Route>
|
||||
</Route>
|
||||
<Route path={ROUTE_NAMES.paymentGuide} element={<PaymentGuidePage />} />
|
||||
<Route path={ROUTE_NAMES.inputShopWalletAddr} element={<InputShopWalletAddressPage />} />
|
||||
*/
|
||||
}
|
||||
|
||||
</SentryRoutes>
|
||||
</GlobalAPIErrorBoundary>
|
||||
</OverlayProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export const router = sentryCreateBrowserRouter([
|
||||
{
|
||||
path: '/',
|
||||
element: <Navigate to={ROUTE_NAMES.home} />,
|
||||
},
|
||||
{
|
||||
path: '/*',
|
||||
element: <Pages />,
|
||||
},
|
||||
]);
|
||||
56
src/shared/configs/test/render.tsx
Normal file
56
src/shared/configs/test/render.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
/* eslint-disable react-refresh/only-export-components */
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { render, RenderOptions } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { FC, ReactElement, useEffect } from 'react';
|
||||
import { MemoryRouter, Route, Routes } from 'react-router';
|
||||
|
||||
import { RecoilRoot } from 'recoil';
|
||||
import { SubLayout } from '@/widgets/sub-layout';
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
throwOnError: true,
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
interface RouterParams {
|
||||
path?: string;
|
||||
entry?: string;
|
||||
state?: any;
|
||||
}
|
||||
|
||||
const AllTheProviders: FC<{ children: React.ReactNode; routerParams?: RouterParams }> = ({
|
||||
children,
|
||||
routerParams,
|
||||
}) => {
|
||||
const { path = '/', entry = '/', state } = routerParams ?? {};
|
||||
const initialEntry = { pathname: entry, state };
|
||||
useEffect(() => {
|
||||
return () => queryClient.clear();
|
||||
}, []);
|
||||
return (
|
||||
<RecoilRoot>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<MemoryRouter initialEntries={[initialEntry]}>
|
||||
<Routes>
|
||||
<Route element={<SubLayout />}>
|
||||
<Route path={path} element={<>{children}</>} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
</QueryClientProvider>
|
||||
</RecoilRoot>
|
||||
);
|
||||
};
|
||||
const user = userEvent.setup();
|
||||
const customRender = (ui: ReactElement, routerParams?: RouterParams, options?: Omit<RenderOptions, 'wrapper'>) => {
|
||||
return render(ui, { wrapper: (props) => <AllTheProviders {...props} routerParams={routerParams} />, ...options });
|
||||
};
|
||||
|
||||
export * from '@testing-library/react';
|
||||
export { customRender as render, user };
|
||||
7
src/shared/configs/types.ts
Normal file
7
src/shared/configs/types.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export type Env = 'development' | 'production'; // 환경변수로 전달받을 환경값 stage(개발계) production(검증계)
|
||||
|
||||
interface Config {
|
||||
APP_NAME: string;
|
||||
WEB_VERSION: string; // 웹 버전
|
||||
}
|
||||
export type ConfigMap = Config;
|
||||
Reference in New Issue
Block a user