불필요 내용 수정및 파일 제거
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { RELEASE_VERSION } from '@/shared/constants/environment';
|
import { RELEASE_VERSION } from '@/shared/constants/common';
|
||||||
import { ConfigMap } from './types';
|
import { ConfigMap } from './types';
|
||||||
/**
|
/**
|
||||||
* development
|
* development
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { RELEASE_VERSION } from '@/shared/constants/environment';
|
import { RELEASE_VERSION } from '@/shared/constants/common';
|
||||||
import { ConfigMap } from './types';
|
import { ConfigMap } from './types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Env } from './types';
|
import { Env } from './types';
|
||||||
import { RELEASE_VERSION } from '@/shared/constants/environment';
|
import { RELEASE_VERSION } from '@/shared/constants/common';
|
||||||
import { PRODUCTION_CONFIG_MAP } from './config.production';
|
import { PRODUCTION_CONFIG_MAP } from './config.production';
|
||||||
import { DEVELOPMENT_CONFIG_MAP } from './config.development';
|
import { DEVELOPMENT_CONFIG_MAP } from './config.development';
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1,4 @@
|
|||||||
export const IMAGE_ROOT = '/images';
|
import packageInfo from '../../../package.json';
|
||||||
|
|
||||||
|
export const IMAGE_ROOT = '/images';
|
||||||
|
export const RELEASE_VERSION = packageInfo.version;
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import packageInfo from '../../../package.json';
|
|
||||||
|
|
||||||
// 어플리케이션 running environment (development, production, test)
|
|
||||||
export const IS_LOCAL = import.meta.env.VITE_APP_ENV === 'local';
|
|
||||||
export const IS_TEST = import.meta.env.VITE_APP_ENV === 'test';
|
|
||||||
export const IS_DEV = import.meta.env.VITE_APP_ENV === 'development';
|
|
||||||
export const IS_PROD = import.meta.env.VITE_APP_ENV === 'production';
|
|
||||||
export const IS_STORYBOOK = !!import.meta.env.STORYBOOK;
|
|
||||||
|
|
||||||
export const IS_DEV_PHASE = IS_LOCAL || IS_DEV;
|
|
||||||
|
|
||||||
export const IS_MOCK_PHASE = import.meta.env.VITE_APP_ENV === 'mock';
|
|
||||||
|
|
||||||
export const RELEASE_VERSION = packageInfo.version;
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
import { FieldValues, RegisterOptions } from 'react-hook-form';
|
|
||||||
|
|
||||||
export const createFormOptions = <T extends FieldValues>(overrides?: Partial<Record<string, RegisterOptions<T>>>) => {
|
|
||||||
const defaultOptions: Partial<Record<string, RegisterOptions<T>>> = {
|
|
||||||
NAME: {
|
|
||||||
required: '이름을 입력해 주세요.',
|
|
||||||
pattern: { value: /^[가-힣]{2,}$/, message: '이름을 두 글자 이상 입력해 주세요.' },
|
|
||||||
},
|
|
||||||
BIRTH: {
|
|
||||||
minLength: { value: 8, message: '생년월일을 8자리로 입력해 주세요.' },
|
|
||||||
pattern: {
|
|
||||||
value: /^\d{4}(0[1-9]|1[0-2])(0[1-9]|[12][0-9]|3[01])$/,
|
|
||||||
message: '생년월일 형식에 맞게 입력해 주세요.',
|
|
||||||
},
|
|
||||||
validate: {
|
|
||||||
isBeforeToday: (value: string | null | undefined) => {
|
|
||||||
if (!value) return '생년월일을 입력해 주세요.';
|
|
||||||
const birthDate = new Date(
|
|
||||||
Number(value.slice(0, 4)),
|
|
||||||
Number(value.slice(4, 6)) - 1,
|
|
||||||
Number(value.slice(6, 8)),
|
|
||||||
);
|
|
||||||
const today = new Date();
|
|
||||||
today.setHours(0, 0, 0, 0);
|
|
||||||
return birthDate < today || '오늘 이전 날짜로 입력해 주세요.';
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: '생년월일을 입력해 주세요.',
|
|
||||||
},
|
|
||||||
CARRIER: {
|
|
||||||
required: '',
|
|
||||||
},
|
|
||||||
PHONE: {
|
|
||||||
required: '휴대폰 번호를 입력해 주세요.',
|
|
||||||
pattern: { value: /^01[016789][0-9]{7,8}$/, message: '올바르지 않은 휴대폰 형식입니다.' },
|
|
||||||
},
|
|
||||||
BANK_ACCOUNT: {
|
|
||||||
required: '계좌번호를 입력해 주세요.',
|
|
||||||
pattern: { value: /^[0-9]+$/, message: '숫자만 입력 가능합니다.' },
|
|
||||||
},
|
|
||||||
BANK_CODE: {
|
|
||||||
required: '은행을 선택해 주세요.',
|
|
||||||
},
|
|
||||||
ALIAS_NM: {
|
|
||||||
required: true,
|
|
||||||
pattern: { value: /^[0-9]+$/, message: '숫자만 입력 가능합니다.' },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
return {
|
|
||||||
...defaultOptions,
|
|
||||||
...overrides,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const MAX_INPUT_NUMBER = 999999999999999;
|
|
||||||
@@ -14,5 +14,4 @@ export enum StorageKeys {
|
|||||||
DeviceId = 'DEVICE_ID',
|
DeviceId = 'DEVICE_ID',
|
||||||
AppVersion = 'APP_VERSION',
|
AppVersion = 'APP_VERSION',
|
||||||
LogOut = 'LOGOUT',
|
LogOut = 'LOGOUT',
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { NiceAxiosError } from '@/shared/@types/error';
|
import { NiceAxiosError } from '@/shared/@types/error';
|
||||||
import { IS_MOCK_PHASE } from '@/shared/constants/environment';
|
|
||||||
|
|
||||||
// CBDCAxiosError가 아니라면 상위 Error Boundary로 위임
|
// CBDCAxiosError가 아니라면 상위 Error Boundary로 위임
|
||||||
export const checkIsAxiosError = (error: Error): error is NiceAxiosError => axios.isAxiosError(error);
|
export const checkIsAxiosError = (error: Error): error is NiceAxiosError => axios.isAxiosError(error);
|
||||||
|
|
||||||
export const checkIsKickOutError = (error: NiceAxiosError) => {
|
export const checkIsKickOutError = (error: NiceAxiosError) => {
|
||||||
const status = error.response?.status.toString();
|
const status = error.response?.status.toString();
|
||||||
return !IS_MOCK_PHASE && (status === '401' || status === '403');
|
return status === '401' || status === '403';
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
export * from './use-navigate';
|
export * from './use-navigate';
|
||||||
export * from './use-fix-scroll-view-safari';
|
export * from './use-fix-scroll-view-safari';
|
||||||
export * from './use-navigate';
|
export * from './use-navigate';
|
||||||
export * from './use-router-listener';
|
|
||||||
export * from './use-keyboard-aware';
|
export * from './use-keyboard-aware';
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
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);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
};
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import { RouterState } from '@remix-run/router';
|
|
||||||
import { useEffect } from 'react';
|
|
||||||
import { NavigationType } from 'react-router';
|
|
||||||
|
|
||||||
import { router } from '@/shared/configs/router';
|
|
||||||
|
|
||||||
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]);
|
|
||||||
};
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
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 };
|
|
||||||
};
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import clsx from 'clsx';
|
|
||||||
import { motion, useAnimation } from 'framer-motion';
|
|
||||||
|
|
||||||
interface RefreshButtonProps {
|
|
||||||
onClick: () => void;
|
|
||||||
children?: React.ReactNode;
|
|
||||||
}
|
|
||||||
export const RefreshButton = ({ onClick, children }: RefreshButtonProps) => {
|
|
||||||
const controls = useAnimation();
|
|
||||||
const handleReset = async () => {
|
|
||||||
controls.start({
|
|
||||||
rotate: 360,
|
|
||||||
transition: { duration: 0.4 },
|
|
||||||
});
|
|
||||||
controls.set({ rotate: 0 });
|
|
||||||
onClick?.();
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<button type="button" onClick={handleReset} className="flex flex-row">
|
|
||||||
{children}
|
|
||||||
<motion.div className={clsx('btn reset-btn')} animate={controls}>
|
|
||||||
<span>reset</span>
|
|
||||||
</motion.div>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
import clsx from 'clsx';
|
|
||||||
import { useEffect, useRef } from 'react';
|
|
||||||
|
|
||||||
export type ElementType<T> = T extends (infer U)[] ? U : never;
|
|
||||||
|
|
||||||
export interface RadioChangeProps<ValueType> {
|
|
||||||
event: React.ChangeEvent<HTMLInputElement>;
|
|
||||||
label?: string;
|
|
||||||
value: ValueType;
|
|
||||||
}
|
|
||||||
export interface SelectTemplateProps<ValueType> {
|
|
||||||
values?: ValueType[];
|
|
||||||
valueKey?: keyof ValueType;
|
|
||||||
labels?: string[];
|
|
||||||
selectedLabel?: string;
|
|
||||||
selectedValue?: string;
|
|
||||||
onRadioChange?: ({ event, label, value }: RadioChangeProps<ValueType>) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SelectTemplate = ({
|
|
||||||
values = [],
|
|
||||||
valueKey,
|
|
||||||
labels = [],
|
|
||||||
selectedLabel,
|
|
||||||
selectedValue,
|
|
||||||
onRadioChange,
|
|
||||||
}: SelectTemplateProps<any>) => {
|
|
||||||
const refs = useRef<(HTMLLabelElement | null)[]>([]);
|
|
||||||
|
|
||||||
const handleRadioChange = ({ event, label, value }: RadioChangeProps<ElementType<typeof values>>) => {
|
|
||||||
onRadioChange?.({ event, label, value });
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const scrollIntoView = (array: any[], selected: string | undefined) => {
|
|
||||||
const index = array?.indexOf(selected ?? '');
|
|
||||||
if (index !== -1 && refs.current[index]) {
|
|
||||||
refs.current[index]?.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
scrollIntoView(labels, selectedLabel);
|
|
||||||
scrollIntoView(values, selectedValue);
|
|
||||||
}, [selectedLabel, labels, selectedValue, values]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
style={{ height: '100%', maxHeight: '50dvh', overflowY: 'auto' }}
|
|
||||||
className="show-scrollbar mx-[24px] pb-[30px] pt-[20px]"
|
|
||||||
>
|
|
||||||
{values?.map((value, index) => {
|
|
||||||
return (
|
|
||||||
<label
|
|
||||||
htmlFor={labels?.[index]}
|
|
||||||
key={labels?.[index]}
|
|
||||||
className={clsx('btm-sheet-btn block h-[52px] text-[15px]', {
|
|
||||||
'selected font-bold text-[#366FC4]':
|
|
||||||
selectedLabel === labels?.[index] ||
|
|
||||||
selectedValue === (valueKey ? values?.[index]?.[valueKey] : values?.[index]),
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
className="hidden"
|
|
||||||
type="radio"
|
|
||||||
id={labels?.[index]}
|
|
||||||
// name={name}
|
|
||||||
value={valueKey ? value?.[valueKey] : `${value}`}
|
|
||||||
onChange={(event) => handleRadioChange({
|
|
||||||
event,
|
|
||||||
label: labels?.[index],
|
|
||||||
value
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
{labels?.[index]}
|
|
||||||
<br />
|
|
||||||
</label>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
|
||||||
import { ElementType, Fragment, useEffect, useState } from 'react';
|
|
||||||
import { RegisterOptions, useFormContext, UseFormRegister } from 'react-hook-form';
|
|
||||||
|
|
||||||
import { BottomSheet } from '~/shared/ui/bottom-sheets/bottom-sheet';
|
|
||||||
import { SelectTemplate, RadioChangeProps } from '~/shared/ui/selects/select-template';
|
|
||||||
import { TextInput } from '~/shared/ui/text-input/text-input';
|
|
||||||
|
|
||||||
interface SelectProps {
|
|
||||||
placeholder: string;
|
|
||||||
labels: string[];
|
|
||||||
values: string[];
|
|
||||||
name: string;
|
|
||||||
option?: RegisterOptions<any>;
|
|
||||||
readOnly?: boolean;
|
|
||||||
onClick?: () => void;
|
|
||||||
register: UseFormRegister<any>;
|
|
||||||
classStr?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Select = ({ placeholder, register, name, option, labels, values, classStr }: SelectProps) => {
|
|
||||||
const [isOpenBottomSheet, setIsOpenBottomSheet] = useState(false);
|
|
||||||
|
|
||||||
const handleCloseBottomSheet = () => {
|
|
||||||
setIsOpenBottomSheet(false);
|
|
||||||
if ((!value || value?.length < 0) && option?.required) {
|
|
||||||
setError(name, { type: 'required', message: option?.required.toString() ?? null });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const { setValue, clearErrors, watch, getValues, setError } = useFormContext();
|
|
||||||
const value = watch(name);
|
|
||||||
|
|
||||||
const defaultValue = getValues(name);
|
|
||||||
const selectedValue = watch(name, defaultValue);
|
|
||||||
const selectedLabel = labels[values.indexOf(selectedValue)];
|
|
||||||
|
|
||||||
const onInputClick = (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
|
||||||
event.preventDefault();
|
|
||||||
setIsOpenBottomSheet(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onRadioChange = ({ event }: RadioChangeProps<ElementType<typeof values>>) => {
|
|
||||||
setValue(name, event.target.value);
|
|
||||||
setIsOpenBottomSheet(false);
|
|
||||||
clearErrors(name);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (value?.length > 0) {
|
|
||||||
setValue(name, selectedValue);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Fragment>
|
|
||||||
<TextInput
|
|
||||||
label={placeholder}
|
|
||||||
{...register(name)}
|
|
||||||
readOnly
|
|
||||||
onClick={onInputClick}
|
|
||||||
selectLabel={selectedLabel}
|
|
||||||
selectMode
|
|
||||||
classStr={classStr}
|
|
||||||
/>
|
|
||||||
<BottomSheet title={placeholder} open={isOpenBottomSheet} onClose={handleCloseBottomSheet}>
|
|
||||||
<SelectTemplate values={values} labels={labels} selectedLabel={selectedLabel} onRadioChange={onRadioChange} />
|
|
||||||
</BottomSheet>
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import clsx from 'clsx';
|
|
||||||
|
|
||||||
interface ClearButtonProps {
|
|
||||||
isShow: boolean;
|
|
||||||
isError: boolean;
|
|
||||||
isActiveWithoutError: boolean;
|
|
||||||
onClick: (() => void) | undefined;
|
|
||||||
}
|
|
||||||
export const ClearButton = ({ isShow, isError, isActiveWithoutError, onClick }: ClearButtonProps) => {
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={clsx(`absolute right-0 top-1/2 mt-[-8px] h-4 w-4 bg-[url('../img/ipt_reset.svg')] indent-[-9999px]`, {
|
|
||||||
'opacity-0': isShow,
|
|
||||||
"bg-[url('../img/ipt_reset_onerror.svg')]": isError,
|
|
||||||
"bg-[url('../img/ipt_chk.svg')]/[1]": isActiveWithoutError,
|
|
||||||
})}
|
|
||||||
onClick={onClick}
|
|
||||||
>
|
|
||||||
삭제
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
interface DescriptionProps {
|
|
||||||
description?: string | React.ReactNode;
|
|
||||||
}
|
|
||||||
export const Description = ({ description }: DescriptionProps) => {
|
|
||||||
if (!description) {
|
|
||||||
return <></>;
|
|
||||||
}
|
|
||||||
return <p className="text-[14px] text-[#000]/[.38]">{description}</p>;
|
|
||||||
};
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
interface ErrorContainerProps {
|
|
||||||
isErrorPaddingPresent?: boolean;
|
|
||||||
children: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ErrorContainer = ({ isErrorPaddingPresent, children }: ErrorContainerProps) => {
|
|
||||||
return <div className={isErrorPaddingPresent ? 'h-6' : undefined}>{children}</div>;
|
|
||||||
};
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
interface ErrorItemProps {
|
|
||||||
message: string;
|
|
||||||
}
|
|
||||||
export const ErrorItem = ({ message }: ErrorItemProps) => {
|
|
||||||
return (
|
|
||||||
<div role="alert" className="ipt-desc">
|
|
||||||
<span>{message}</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import { useFormContext } from 'react-hook-form';
|
|
||||||
|
|
||||||
import { ErrorContainer } from './error-container';
|
|
||||||
import { ErrorItem } from './error-item';
|
|
||||||
|
|
||||||
interface ErrorMessageProps {
|
|
||||||
name: string;
|
|
||||||
isErrorPaddingPresent?: boolean;
|
|
||||||
isShowError: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ErrorMessage = ({ isShowError, name, isErrorPaddingPresent = true }: ErrorMessageProps) => {
|
|
||||||
const {
|
|
||||||
formState: { errors },
|
|
||||||
} = useFormContext();
|
|
||||||
|
|
||||||
if (!isShowError) {
|
|
||||||
return <></>;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<ErrorContainer isErrorPaddingPresent={isErrorPaddingPresent}>
|
|
||||||
{errors[name] && <ErrorItem message={errors[name]?.message?.toString() ?? ''} />}
|
|
||||||
</ErrorContainer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import { useFormContext } from 'react-hook-form';
|
|
||||||
|
|
||||||
import { ErrorContainer } from './error-container';
|
|
||||||
import { ErrorItem } from './error-item';
|
|
||||||
|
|
||||||
interface ExternalErrorDisplayProps {
|
|
||||||
isErrorPaddingPresent?: boolean;
|
|
||||||
isShowAll?: boolean;
|
|
||||||
}
|
|
||||||
export const ExternalErrorDisplay = ({
|
|
||||||
isErrorPaddingPresent = true,
|
|
||||||
isShowAll = false,
|
|
||||||
}: ExternalErrorDisplayProps) => {
|
|
||||||
const {
|
|
||||||
formState: { errors },
|
|
||||||
} = useFormContext();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ErrorContainer isErrorPaddingPresent={isErrorPaddingPresent}>
|
|
||||||
{(
|
|
||||||
Object.keys(errors)?.map((key) => <ErrorItem key={key} message={errors?.[key]?.message?.toString() ?? ''} />)
|
|
||||||
) }
|
|
||||||
</ErrorContainer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,166 +0,0 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
|
||||||
import { forwardRef, startTransition, useCallback, useMemo, useRef } from 'react';
|
|
||||||
import { useFormContext, UseFormRegister } from 'react-hook-form';
|
|
||||||
|
|
||||||
export type TextFieldProps = {
|
|
||||||
placeholder?: string;
|
|
||||||
inputMode?: 'text' | 'search' | 'email' | 'tel' | 'url' | 'none' | 'numeric' | 'decimal';
|
|
||||||
readOnly?: boolean;
|
|
||||||
selectLabel?: string;
|
|
||||||
onInputFocus?: (event: React.FocusEvent<HTMLInputElement>) => void;
|
|
||||||
onInputBlur?: (event: React.FocusEvent<HTMLInputElement>) => void;
|
|
||||||
onInputChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
|
||||||
isFocused: boolean;
|
|
||||||
setIsFocused: React.Dispatch<React.SetStateAction<boolean>>;
|
|
||||||
suffix?: string;
|
|
||||||
prefix?: string;
|
|
||||||
textFieldId: string;
|
|
||||||
} & ReturnType<UseFormRegister<any>>;
|
|
||||||
|
|
||||||
export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(function TextField(
|
|
||||||
{
|
|
||||||
name,
|
|
||||||
placeholder,
|
|
||||||
inputMode = 'text',
|
|
||||||
onBlur,
|
|
||||||
disabled,
|
|
||||||
readOnly,
|
|
||||||
onInputFocus,
|
|
||||||
onInputBlur,
|
|
||||||
selectLabel,
|
|
||||||
onChange,
|
|
||||||
onInputChange,
|
|
||||||
maxLength,
|
|
||||||
isFocused,
|
|
||||||
setIsFocused,
|
|
||||||
suffix,
|
|
||||||
prefix,
|
|
||||||
textFieldId,
|
|
||||||
},
|
|
||||||
ref,
|
|
||||||
) {
|
|
||||||
const textRef = useRef<HTMLInputElement>(null);
|
|
||||||
|
|
||||||
const {
|
|
||||||
setValue,
|
|
||||||
formState: { errors },
|
|
||||||
watch,
|
|
||||||
trigger,
|
|
||||||
} = useFormContext();
|
|
||||||
|
|
||||||
const value = watch(name);
|
|
||||||
const hasValue = useMemo(() => value?.length > 0, [value]);
|
|
||||||
const isFocusedAndNoValue = useMemo(() => isFocused && !hasValue, [isFocused, hasValue]);
|
|
||||||
|
|
||||||
const handleBlur = useCallback(
|
|
||||||
(event: React.FocusEvent<HTMLInputElement>) => {
|
|
||||||
if (readOnly) return;
|
|
||||||
startTransition(() => {
|
|
||||||
setIsFocused(false);
|
|
||||||
onBlur(event);
|
|
||||||
onInputBlur?.(event);
|
|
||||||
if (prefix || suffix) {
|
|
||||||
trigger(name);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[setIsFocused, onBlur, onInputBlur],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleFocus = useCallback(
|
|
||||||
(event: React.FocusEvent<HTMLInputElement>) => {
|
|
||||||
if (readOnly) return;
|
|
||||||
setIsFocused(true);
|
|
||||||
onInputFocus?.(event);
|
|
||||||
},
|
|
||||||
[setIsFocused, onInputFocus],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleChange = useCallback(
|
|
||||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
startTransition(() => {
|
|
||||||
// if maxLength is set, do not allow input more than maxLength
|
|
||||||
if (maxLength && event.target.value?.length > maxLength) {
|
|
||||||
setValue(name, event.target.value.substring(0, maxLength));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (prefix || suffix) {
|
|
||||||
const inputValue = event.target.value;
|
|
||||||
const numberPart = inputValue.replace(/[^\d]/g, '');
|
|
||||||
const formattedNumber = Number(numberPart).toLocaleString('ko-KR');
|
|
||||||
setValue(name, numberPart);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
const cursorPosition = (prefix ? prefix.length : 0) + formattedNumber.length;
|
|
||||||
textRef.current?.setSelectionRange(cursorPosition, cursorPosition);
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
onChange(event);
|
|
||||||
onInputChange?.(event);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[maxLength, setValue, name, onChange, onInputChange],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (selectLabel) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<input type="hidden" name={name} ref={ref} id={textFieldId} />
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="h-[28px] w-full rounded-none pr-[19px] text-xl leading-7 tracking-[0]"
|
|
||||||
value={selectLabel}
|
|
||||||
inputMode={inputMode}
|
|
||||||
readOnly={readOnly}
|
|
||||||
onFocus={handleFocus}
|
|
||||||
onBlur={handleBlur}
|
|
||||||
placeholder={isFocusedAndNoValue ? placeholder : ''}
|
|
||||||
aria-invalid={errors[name] ? 'true' : 'false'}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const isNotNullValue = value !== null && value !== undefined && value !== '';
|
|
||||||
|
|
||||||
if (prefix || suffix) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<input type="hidden" name={name} ref={ref} id={textFieldId} />
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={`${isNotNullValue ? (prefix ?? '') + Math.abs(value).toLocaleString('ko-KR') + suffix : ''}`}
|
|
||||||
ref={textRef}
|
|
||||||
className="h-[28px] w-full rounded-none pr-[19px] text-xl leading-7 tracking-[0]"
|
|
||||||
onChange={handleChange}
|
|
||||||
onFocus={handleFocus}
|
|
||||||
onBlur={handleBlur}
|
|
||||||
inputMode={inputMode}
|
|
||||||
disabled={disabled}
|
|
||||||
readOnly={readOnly}
|
|
||||||
placeholder={isFocusedAndNoValue ? placeholder : ''}
|
|
||||||
aria-invalid={errors[name] ? 'true' : 'false'}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name={name}
|
|
||||||
ref={ref}
|
|
||||||
id={textFieldId}
|
|
||||||
className="h-[28px] w-full rounded-none pr-[19px] text-xl leading-7 tracking-[0]"
|
|
||||||
onChange={handleChange}
|
|
||||||
onFocus={handleFocus}
|
|
||||||
onBlur={handleBlur}
|
|
||||||
inputMode={inputMode}
|
|
||||||
disabled={disabled}
|
|
||||||
readOnly={readOnly}
|
|
||||||
placeholder={isFocusedAndNoValue ? placeholder : ''}
|
|
||||||
aria-invalid={errors[name] ? 'true' : 'false'}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import clsx from 'clsx';
|
|
||||||
|
|
||||||
interface TextInputContainerProps {
|
|
||||||
children: React.ReactNode;
|
|
||||||
isError: boolean;
|
|
||||||
disabled?: boolean;
|
|
||||||
isActiveWithoutError: boolean;
|
|
||||||
labelClassStr?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TextInputContainer = ({
|
|
||||||
children,
|
|
||||||
isError,
|
|
||||||
disabled,
|
|
||||||
isActiveWithoutError,
|
|
||||||
labelClassStr,
|
|
||||||
}: TextInputContainerProps) => {
|
|
||||||
const containerClassNames = clsx(`relative border-b h-9 block border-[#000]/[.56]`, {
|
|
||||||
[labelClassStr as string]: labelClassStr,
|
|
||||||
'border-gray-300': disabled,
|
|
||||||
'border-[#ff0000]': isError,
|
|
||||||
'border-[#2196f3]': isActiveWithoutError,
|
|
||||||
});
|
|
||||||
|
|
||||||
return <div className={containerClassNames}>{children}</div>;
|
|
||||||
};
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
|
||||||
import clsx from 'clsx';
|
|
||||||
import { forwardRef, useDeferredValue, useMemo, useState } from 'react';
|
|
||||||
import { useFormContext, UseFormRegister } from 'react-hook-form';
|
|
||||||
|
|
||||||
import { ClearButton } from './clear-button';
|
|
||||||
import { Description } from './description';
|
|
||||||
import { ErrorMessage } from './error-message';
|
|
||||||
import { TextField, TextFieldProps } from './text-field';
|
|
||||||
import { TextInputContainer } from './text-input-container';
|
|
||||||
import { TextLabel } from './text-label';
|
|
||||||
|
|
||||||
type TextInputProps = {
|
|
||||||
label?: string | React.ReactNode;
|
|
||||||
description?: string | React.ReactNode;
|
|
||||||
selectMode?: boolean;
|
|
||||||
isOpenBottomSheet?: boolean;
|
|
||||||
classStr?: string;
|
|
||||||
labelClassStr?: string;
|
|
||||||
isErrorPaddingPresent?: boolean;
|
|
||||||
isShowError?: boolean;
|
|
||||||
onClick?: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
|
|
||||||
} & Omit<TextFieldProps, 'isFocused' | 'setIsFocused' | 'textFieldId'> &
|
|
||||||
ReturnType<UseFormRegister<any>>;
|
|
||||||
|
|
||||||
export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(function TextInput(props, ref) {
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
label,
|
|
||||||
description,
|
|
||||||
disabled,
|
|
||||||
onClick,
|
|
||||||
selectMode,
|
|
||||||
isOpenBottomSheet,
|
|
||||||
classStr,
|
|
||||||
labelClassStr,
|
|
||||||
isErrorPaddingPresent = true,
|
|
||||||
isShowError = true,
|
|
||||||
} = props;
|
|
||||||
const textFieldId = useMemo(() => `text-input-${name}`, [name]);
|
|
||||||
|
|
||||||
const {
|
|
||||||
setFocus,
|
|
||||||
setValue,
|
|
||||||
watch,
|
|
||||||
formState: { errors },
|
|
||||||
clearErrors,
|
|
||||||
} = useFormContext();
|
|
||||||
const value = watch(name);
|
|
||||||
const [isFocused, setIsFocused] = useState(false);
|
|
||||||
|
|
||||||
// selectMode true일 때는, input Focus 상태를 isOpenBottomSheet 상태로 처리
|
|
||||||
const isSelecting = useMemo(() => !!selectMode && !!isOpenBottomSheet, [selectMode, isOpenBottomSheet]);
|
|
||||||
const deferredIsFocused = useDeferredValue(isFocused || isSelecting);
|
|
||||||
|
|
||||||
const hasValue = useMemo(() => !!value && String(value)?.length > 0, [value]);
|
|
||||||
|
|
||||||
const isActiveWithoutError = useMemo(
|
|
||||||
() => deferredIsFocused && hasValue && !errors[name],
|
|
||||||
[hasValue, deferredIsFocused, errors[name]],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleContainerClick = (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
|
||||||
onClick?.(event);
|
|
||||||
};
|
|
||||||
|
|
||||||
// handle clear button
|
|
||||||
const handleClickClearButton = () => {
|
|
||||||
setValue(name, '');
|
|
||||||
clearErrors(name);
|
|
||||||
setFocus(name);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div
|
|
||||||
className={clsx('ipt-form text-input', {
|
|
||||||
'btm-sheet': selectMode,
|
|
||||||
[classStr as string]: classStr,
|
|
||||||
})}
|
|
||||||
onClick={handleContainerClick}
|
|
||||||
>
|
|
||||||
<TextInputContainer
|
|
||||||
labelClassStr={labelClassStr}
|
|
||||||
disabled={disabled}
|
|
||||||
isError={!!errors[name]}
|
|
||||||
isActiveWithoutError={isActiveWithoutError}
|
|
||||||
>
|
|
||||||
<TextLabel
|
|
||||||
isError={!!errors[name]}
|
|
||||||
isFocused={deferredIsFocused}
|
|
||||||
hasValue={hasValue}
|
|
||||||
textFieldId={textFieldId}
|
|
||||||
isActiveWithoutError={isActiveWithoutError}
|
|
||||||
label={label}
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
{...props}
|
|
||||||
textFieldId={textFieldId}
|
|
||||||
ref={ref}
|
|
||||||
isFocused={isFocused || isSelecting}
|
|
||||||
setIsFocused={setIsFocused}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ClearButton
|
|
||||||
isActiveWithoutError={isActiveWithoutError}
|
|
||||||
isError={!!errors[name]}
|
|
||||||
isShow={!hasValue || !deferredIsFocused}
|
|
||||||
onClick={!disabled ? handleClickClearButton : undefined}
|
|
||||||
/>
|
|
||||||
</TextInputContainer>
|
|
||||||
<ErrorMessage name={name} isErrorPaddingPresent={isErrorPaddingPresent} isShowError={isShowError} />
|
|
||||||
</div>
|
|
||||||
<Description description={description} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import clsx from 'clsx';
|
|
||||||
import { ReactNode } from 'react';
|
|
||||||
|
|
||||||
interface TextLabelProps {
|
|
||||||
label: ReactNode;
|
|
||||||
textFieldId: string;
|
|
||||||
isError: boolean;
|
|
||||||
hasValue: boolean;
|
|
||||||
isFocused: boolean;
|
|
||||||
isActiveWithoutError: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TextLabel = ({
|
|
||||||
label,
|
|
||||||
textFieldId,
|
|
||||||
isError,
|
|
||||||
isFocused,
|
|
||||||
hasValue,
|
|
||||||
isActiveWithoutError,
|
|
||||||
}: TextLabelProps) => {
|
|
||||||
const labelClassNames = clsx('absolute left-0 transition-all duration-200 h-9 leading-9', {
|
|
||||||
'top-[-17px] text-[12px] leading-[12px]': isFocused || hasValue,
|
|
||||||
'top-1/2 text-[16px] mt-[-18px] text-black/[.6] ': !isFocused && !hasValue,
|
|
||||||
'text-[#ff0000] ': isError,
|
|
||||||
'text-[#2196f3] ': isActiveWithoutError,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<label className={labelClassNames} htmlFor={textFieldId}>
|
|
||||||
{label}
|
|
||||||
</label>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
Reference in New Issue
Block a user