첫 커밋

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,4 @@
export const convertCurrencyStringToNumber = (currencyString: string): number => {
const cleanedString = currencyString.replace(/[^\d]/g, '');
return parseInt(cleanedString, 10);
};

1
src/shared/lib/delay.ts Normal file
View File

@@ -0,0 +1 @@
export const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

11
src/shared/lib/error.ts Normal file
View File

@@ -0,0 +1,11 @@
import axios from 'axios';
import { CBDCAxiosError } from '@/shared/@types/error';
import { IS_MOCK_PHASE } from '@/shared/constants/environment';
// CBDCAxiosError가 아니라면 상위 Error Boundary로 위임
export const checkIsAxiosError = (error: Error): error is CBDCAxiosError => axios.isAxiosError(error);
export const checkIsKickOutError = (error: CBDCAxiosError) => {
const status = error.response?.status.toString();
return !IS_MOCK_PHASE && (status === '401' || status === '403');
};

View File

@@ -0,0 +1,16 @@
export const formatKoreanNumber = (num: number): string => {
const units = ['', '만', '억', '조', '경', '해'];
let result = '';
let unitIndex = 0;
while (num > 0) {
const part = num % 10000;
if (part != 0) {
result = `${part.toLocaleString()}${units[unitIndex]}${result}`;
}
num = Math.floor(num / 10000);
unitIndex++;
}
return result;
};

View File

@@ -0,0 +1,12 @@
export * from './use-navigate';
export * from './use-app-version';
export * from './use-change-bg-color';
export * from './use-device-uid';
export * from './use-fix-scroll-view-safari';
export * from './use-has-bio-hardware';
export * from './use-navigate';
export * from './use-location-permission';
export * from './use-window-focus-change';
export * from './use-router-listener';
export * from './use-scroll-to-top';
export * from './use-app-page-speed';

View File

@@ -0,0 +1,15 @@
import { useEffectOnce } from 'react-use';
import useLocalStorageState from 'use-local-storage-state';
import { StorageKeys } from '@/shared/constants/local-storage';
import { setAfterPageRendered } from '@/shared/lib';
export const useAppPagingSpeed = () => {
return useLocalStorageState(StorageKeys.AppPagingSpeed, { defaultValue: '250' });
};
export const useEffectOnceAfterPageRendered = (callback: () => void) => {
useEffectOnce(() => {
setAfterPageRendered(callback);
});
};

View File

@@ -0,0 +1,17 @@
import { useMemo } from 'react';
import useLocalStorageState from 'use-local-storage-state';
import { config } from '@/shared/configs';
import { StorageKeys } from '@/shared/constants/local-storage';
export const DEFAULT_APP_VERSION = '1.0';
export const useAppVersion = () => {
const [appVersion] = useLocalStorageState(StorageKeys.AppVersion, { defaultValue: DEFAULT_APP_VERSION });
return appVersion;
};
export const webVersion = config.WEB_VERSION.replace(/\b(\d+)(?:\.0\.0)?\b/g, '$1');
export const useFullVersion = () => {
const appVersion = useAppVersion();
const fullVersion = useMemo(() => `${appVersion}.${webVersion}`, [appVersion]);
return { fullVersion };
};

View File

@@ -0,0 +1,10 @@
import { useBlocker } from '@use-blocker';
export const useBlockBack = (blockPaths: string[]) => {
useBlocker(({ nextLocation }) => {
if (blockPaths.includes(nextLocation.pathname)) {
return true;
}
return false;
});
};

View File

@@ -0,0 +1,2 @@
import { useBlocker as _useBlocker } from 'react-router';
export const useBlocker = _useBlocker;

View File

@@ -0,0 +1,44 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { useLayoutEffect } from 'react';
import useLocalStorageState from 'use-local-storage-state';
import { DEFAULT_BACKGROUND_COLOR } from '@/shared/constants/colors';
import { StorageKeys } from '@/shared/constants/local-storage';
import { NativeFunction } from '@/shared/constants/native-function';
import { useRouterListener } from './use-router-listener';
export const useChangeBgColor = (color: string) => {
const [, setAppColor] = useAppColor();
useLayoutEffect(() => {
setAppColor(color);
window.webViewBridge.send(NativeFunction.SetAppColor, color);
document.body.style.backgroundColor = color;
}, []);
};
export const useResetBgColor = () => {
const [, setAppColor] = useAppColor();
const defaultColor = DEFAULT_BACKGROUND_COLOR;
useLayoutEffect(() => {
setAppColor(defaultColor);
window.webViewBridge.send(NativeFunction.SetAppColor, defaultColor);
document.body.style.backgroundColor = defaultColor;
}, []);
};
export const useResetBgColorOnLeave = () => {
const [, setAppColor] = useAppColor();
const defaultColor = DEFAULT_BACKGROUND_COLOR;
useRouterListener(() => {
setAppColor(defaultColor);
document.body.style.backgroundColor = defaultColor;
window.webViewBridge.send(NativeFunction.SetAppColor, defaultColor);
});
};
export const useAppColor = () => {
return useLocalStorageState(StorageKeys.AppColor, { defaultValue: DEFAULT_BACKGROUND_COLOR });
};

View File

@@ -0,0 +1,31 @@
/* eslint-disable react-hooks/exhaustive-deps */
/* eslint-disable @cspell/spellchecker */
import { useEffect } from 'react';
import useLocalStorageState from 'use-local-storage-state';
// import { useUpdateUserInfo } from '@/entities/user/lib/use-update-user-info';
import { StorageKeys } from '@/shared/constants/local-storage';
import { useStore } from '@/shared/model/store';
export const useDeviceUid = () => {
let defaultUid = '';
if (!window.ReactNativeWebView) {
defaultUid = '1234567890';
}
const userInfo = useStore((state: any) => state.userSlice.userInfo);
const [deviceUid] = useLocalStorageState(StorageKeys.DeviceUniqueId, { defaultValue: defaultUid });
//const { updateUserInfo } = useUpdateUserInfo();
useEffect(() => {
if (deviceUid.length < 1) {
return;
}
if (userInfo?.appEsntlNo && userInfo?.appEsntlNo?.length > 0) {
return;
}
// updateUserInfo((prev: any) => ({ ...prev, appEsntlNo: deviceUid }));
}, [deviceUid]);
return deviceUid;
};

View File

@@ -0,0 +1,12 @@
import { useEffect } from 'react';
export const useFixScrollViewSafari = () => {
useEffect(() => {
const timeout = setTimeout(() => {
window.scrollTo(0, 1);
}, 100);
return () => {
clearTimeout(timeout);
};
}, []);
};

View File

@@ -0,0 +1,45 @@
import { CSSProperties, useEffect, useState } from 'react';
export const useFixedButtonPosition = () => {
const [buttonStyle, setButtonStyle] = useState<CSSProperties | undefined>();
const updateButtonPosition = () => {
const viewport = window.visualViewport;
const viewportHeight = viewport?.height;
const viewportOffsetTop = viewport?.offsetTop ?? 0;
if (viewportHeight && viewportHeight < window.innerHeight) {
setButtonStyle({ bottom: `${window.innerHeight - viewportHeight - viewportOffsetTop + 15}px` });
} else {
setButtonStyle(undefined);
}
};
useEffect(() => {
const handleResize = () => {
updateButtonPosition();
};
visualViewport?.addEventListener('resize', handleResize);
window.addEventListener('resize', handleResize); // fallback for window resize
return () => {
visualViewport?.removeEventListener('resize', handleResize);
window.removeEventListener('resize', handleResize);
};
}, []);
useEffect(() => {
const handleScroll = () => {
updateButtonPosition();
};
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, []);
return buttonStyle;
};

View File

@@ -0,0 +1,14 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable react-hooks/exhaustive-deps */
import { useEffect } from 'react';
import { NativeMessage } from '@/shared/constants/native-message';
import { bridge } from '@/bridge';
export const useHardwareBackPressListener = (callback: () => void) => {
useEffect(() => {
// Subscribe to events from react native.
return bridge.addEventListener(NativeMessage.HardwareBackPress, (message: any) => {
callback?.();
});
}, []);
};

View File

@@ -0,0 +1,7 @@
import useLocalStorageState from 'use-local-storage-state';
import { StorageKeys } from '@/shared/constants/local-storage';
export const useHasBioHardware = () => {
const [hasBioHardware] = useLocalStorageState(StorageKeys.HasBioHardware, { defaultValue: false });
return hasBioHardware;
};

View File

@@ -0,0 +1,46 @@
/* eslint-disable @cspell/spellchecker */
import { useEffect } from 'react';
import { PERMISSION_RESULTS, PermissionResultValues, WebViewBridgeResponse } from '@/shared/@types/webview-bridge';
import { NativeFunction } from '@/shared/constants/native-function';
interface CheckLocationPermissionResponse extends WebViewBridgeResponse {
data: PermissionResultValues;
}
export const useRequestLocationPermission = () => {
useEffect(() => {
window.webViewBridge.send(NativeFunction.LocationPermission, {
success: async () => {
// console.log('res', res);
},
});
}, []);
};
export const useCheckLocationPermission = () => {
useEffect(() => {
window.webViewBridge.send(NativeFunction.CheckLocationPermission, {
success: async (res: CheckLocationPermissionResponse) => {
// console.log('res', res.data);
switch (res.data) {
case PERMISSION_RESULTS.UNAVAILABLE:
console.log('This feature is not available (on this device / in this context)');
break;
case PERMISSION_RESULTS.DENIED:
console.log('The permission has not been requested / is denied but requestable');
break;
case PERMISSION_RESULTS.LIMITED:
console.log('The permission is limited: some actions are possible');
break;
case PERMISSION_RESULTS.GRANTED:
console.log('The permission is granted');
break;
case PERMISSION_RESULTS.BLOCKED:
console.log('The permission is denied and not requestable anymore');
break;
}
},
});
}, []);
};

View File

@@ -0,0 +1,37 @@
import { PathType } from '@/shared/constants/paths';
import {
NavigateOptions,
To,
useNavigate as _useNavigate
} from 'react-router';
export type NavigateTo = PathType | -1 | 0;
export const goBackWebview = (goBack: () => void) => {
if (!window.ReactNativeWebView) {
if (window.history.state?.idx > 0) {
goBack();
return;
} else {
window.close();
}
return;
}
};
export const useNavigate = () => {
const _navigate = _useNavigate();
const navigate = (to: NavigateTo, options?: NavigateOptions): void => {
const path = typeof to === 'number' ? to : to.toString();
_navigate(path as To, options);
};
const reload = async () => {
navigate(0);
};
const navigateBack = () => goBackWebview(() => _navigate(-1));
return { navigate, navigateBack, reload };
};

View File

@@ -0,0 +1,13 @@
import { useEffect } from 'react';
import { NativeFunction } from '@/shared/constants/native-function';
export const useRequestNotification = () => {
useEffect(() => {
window.webViewBridge.send(NativeFunction.RequestNotification, {
success: async () => {
// console.log('res', res);
},
});
}, []);
};

View File

@@ -0,0 +1,20 @@
import { RouterState } from '@remix-run/router';
import { useEffect } from 'react';
import { NavigationType } from 'react-router';
import { router } from '@/shared/configs/sentry';
type IRouterListener = (state: RouterState) => void;
export const useRouterListener = (callback: IRouterListener, actionType?: NavigationType) => {
useEffect(() => {
const unsubscribe = router.subscribe((state: any) => {
if (actionType && state.historyAction !== actionType) return;
if (!actionType || state.historyAction === actionType) {
callback?.(state);
}
});
return unsubscribe;
}, [callback, actionType]);
};

View File

@@ -0,0 +1,37 @@
import { useEffect, useState } from 'react';
export function useScript(src: string) {
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let script = document.querySelector(`script[src="${src}"]`);
const handleLoad = () => setLoading(false);
const handleError = (err: any) => {
setError(err);
setLoading(false);
};
if(!script){
script = document.createElement('script');
(script as any).src = src;
(script as any).type = 'text/javascript';
(script as any).charset = 'utf-8';
script.addEventListener('load', handleLoad);
script.addEventListener('error', handleError);
console.log(script);
// document.getElementsByTagName('head')[0].appendChild(script);
}
else{
setLoading(false);
}
return () => {
script.removeEventListener('load', handleLoad);
script.removeEventListener('error', handleError);
}
}, [src]);
return [loading, error];
}

View File

@@ -0,0 +1,7 @@
import { useEffect } from 'react';
export const useScrollToTop = () => {
useEffect(() => {
document.body.scrollTop = 0;
}, []);
};

View File

@@ -0,0 +1,50 @@
import { overlay } from 'overlay-kit';
import { BottomSheet } from '@/shared/ui/bottom-sheets/bottom-sheet';
import {
SelectTemplate,
ElementType,
RadioChangeProps,
SelectTemplateProps,
} from '@/shared/ui/selects/select-template';
import { JSX } from 'react/jsx-runtime';
export interface SelectBottomSheetProps<T> extends SelectTemplateProps<T> {
title?: string | JSX.Element;
description?: string | JSX.Element;
onClose?: () => void;
}
export const useSelectBottomSheet = <T,>() => {
const openSelectBottomSheet = (selectBottomSheet: SelectBottomSheetProps<T>) => {
const {
values = [],
labels = [],
valueKey = undefined,
selectedLabel = '',
selectedValue = '',
title = '',
description = '',
} = selectBottomSheet;
const handleRadioChange = ({ event, label, value }: RadioChangeProps<ElementType<typeof values>>) => {
selectBottomSheet?.onRadioChange?.({ event, label, value });
};
overlay.open(({ isOpen, close, unmount }) => {
return (
<BottomSheet title={title} description={description} afterLeave={unmount} open={isOpen} onClose={close}>
<SelectTemplate
values={values}
valueKey={valueKey}
labels={labels}
selectedLabel={selectedLabel}
selectedValue={selectedValue}
onRadioChange={handleRadioChange}
/>
</BottomSheet>
);
});
};
return { openSelectBottomSheet, closeBottomSheet: overlay.closeAll };
};

View File

@@ -0,0 +1,28 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { useEffect } from 'react';
interface WindowFocusProps {
onFocus?: () => void;
onBlur?: () => void;
args?: any[];
}
export const useWindowFocusChange = ({ onFocus, onBlur, ...args }: WindowFocusProps) => {
const dependencies: any[] = [];
if (Array.isArray(args)) {
dependencies.push(...args);
}
const handleEvent = () => {
if (document.hidden) {
// the page is hidden
onBlur?.();
} else {
// the page is visible
onFocus?.();
}
};
useEffect(() => {
window.document.addEventListener('visibilitychange', handleEvent);
return () => window.document.removeEventListener('visibilitychange', handleEvent);
}, [onFocus, onBlur, ...dependencies]);
};

5
src/shared/lib/index.ts Normal file
View File

@@ -0,0 +1,5 @@
export * from './error';
export * from './toast';
export * from './web-view-bridge';
export * from './format-korean-number';
export * from './set-after-page-rendered';

View File

@@ -0,0 +1,17 @@
import { AxiosPromise } from 'axios';
import { CBDCAxiosError } from '@/shared/@types/error';
export const resultify = async <T = any>(promiseObj: AxiosPromise<T>): Promise<T> => {
try {
const result: any = await promiseObj;
return result?.data !== undefined && result?.data !== null ? result.data : result;
} catch (error: any) {
const axiosError = error as CBDCAxiosError;
console.error(
`${
axiosError.response?.status ? `[${axiosError.response.status}], ` : ''
}${JSON.stringify(axiosError.response?.data)}`,
);
throw error;
}
};

View File

@@ -0,0 +1,9 @@
import { StorageKeys } from '@/shared/constants/local-storage';
import { getLocalStorage } from './web-view-bridge';
export const setAfterPageRendered = (callback: () => void) => {
setTimeout(callback, PAGING_SPEED);
};
export const PAGING_SPEED = getLocalStorage(StorageKeys.AppPagingSpeed)
? Number(getLocalStorage(StorageKeys.AppPagingSpeed))
: 250;

View File

@@ -0,0 +1,6 @@
export const toCamelCase = (str: string) => {
return str
.split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join('');
};

11
src/shared/lib/toast.tsx Normal file
View File

@@ -0,0 +1,11 @@
/* eslint-disable @cspell/spellchecker */
import { toast } from 'react-toastify';
export const snackBar = (text: string) => {
toast.dismiss({ containerId: 'snackbar' });
toast(text, { containerId: 'snackbar' });
};
export const notiBar = (text: string) => {
toast.dismiss({ containerId: 'notibar' });
toast(text);
};

View File

@@ -0,0 +1,79 @@
export const setLocalStorage = (key: string, value: any) => {
if (!key) return false;
if (value) {
if (typeof value === 'object') {
window?.localStorage?.setItem(key, JSON.stringify(value));
} else {
window?.localStorage?.setItem(key, value);
}
}
return true;
};
export const getLocalStorage = (key: string) => {
if (!key) return;
const ret = window?.localStorage?.getItem(key);
try {
return ret && JSON.parse(ret);
} catch (error) {
return ret;
}
};
export const parseValueToDate = (
date: string | number | Date | string[] | number[] | null | undefined,
type?: string,
fixType?: string,
): string => {
if (!date) return '';
let parse = '';
switch (typeof date) {
case 'string':
return date;
case 'number':
case 'object':
let dDate = null;
if (date instanceof Date || typeof date === 'number') {
dDate = new Date(date);
} else if (date instanceof Array) {
if (date.length === 6)
dDate = new Date(
Number(date[0]),
Number(date[1]) - 1,
Number(date[2]),
Number(date[3]),
Number(date[4]),
Number(date[5]),
);
else if (date.length === 5)
dDate = dDate = new Date(
Number(date[0]),
Number(date[1]) - 1,
Number(date[2]),
Number(date[3]),
Number(date[4]),
);
else if (date.length === 4)
dDate = dDate = new Date(Number(date[0]), Number(date[1]) - 1, Number(date[2]), Number(date[3]));
else if (date.length === 3) dDate = dDate = new Date(Number(date[0]), Number(date[1]) - 1, Number(date[2]));
else if (date.length === 2) dDate = dDate = new Date(Number(date[0]), Number(date[1]) - 1);
else if (date.length === 1) dDate = new Date(Number(date[0]));
}
if (dDate) {
if (type !== 'time') {
parse = dDate.getFullYear() + (fixType ? fixType : '-');
parse += String(dDate.getMonth() + 1).padStart(2, '0') + (fixType ? fixType : '-');
parse += String(dDate.getDate()).padStart(2, '0');
}
if (type !== 'date') {
if (type !== 'time') parse += ' ';
parse += String(dDate.getHours()).padStart(2, '0') + ':';
parse += String(dDate.getMinutes()).padStart(2, '0') + ':';
parse += String(dDate.getSeconds()).padStart(2, '0');
}
}
return parse;
}
};