첫 커밋

This commit is contained in:
focp212@naver.com
2025-09-05 15:36:48 +09:00
commit 05238b04c1
825 changed files with 176358 additions and 0 deletions

View 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);
};

View 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);
}
};

View 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',
};

View 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',
};

View 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);

View 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;

View 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 />,
},
]);

View 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 };

View File

@@ -0,0 +1,7 @@
export type Env = 'development' | 'production'; // 환경변수로 전달받을 환경값 stage(개발계) production(검증계)
interface Config {
APP_NAME: string;
WEB_VERSION: string; // 웹 버전
}
export type ConfigMap = Config;