648 lines
24 KiB
TypeScript
648 lines
24 KiB
TypeScript
import { useTranslation } from 'react-i18next';
|
|
import { PATHS } from '@/shared/constants/paths';
|
|
import { useNavigate } from '@/shared/lib/hooks/use-navigate';
|
|
import { HeaderType } from '@/entities/common/model/types';
|
|
import {
|
|
useSetHeaderTitle,
|
|
useSetHeaderType,
|
|
useSetFooterMode,
|
|
useSetOnBack
|
|
} from '@/widgets/sub-layout/use-sub-layout';
|
|
import { useState, useEffect, useRef } from 'react';
|
|
import { VerificationItem } from '@/entities/account/model/types';
|
|
import { useUserCreateMutation } from '@/entities/user/api/use-user-create-mutation';
|
|
import { useUserExistsUseridQuery } from '@/entities/user/api/use-user-exists-userid-query';
|
|
import { useLocation } from 'react-router';
|
|
import { snackBar } from '@/shared/lib/toast';
|
|
import { maskEmail, maskPhoneNumber } from '@/shared/lib/masking';
|
|
import { validatePassword } from '@/shared/lib/password-validation';
|
|
import { useKeyboardAware } from '@/shared/lib/hooks';
|
|
|
|
export const UserAddAccountPage = () => {
|
|
const { t } = useTranslation();
|
|
const { navigate } = useNavigate();
|
|
const location = useLocation();
|
|
const { mid } = location.state || {};
|
|
|
|
const { handleInputFocus, keyboardAwarePadding } = useKeyboardAware();
|
|
const { mutateAsync: userCreate, isPending } = useUserCreateMutation({
|
|
onError: (error) => {
|
|
type ErrorDetail = {
|
|
errKey?: string;
|
|
message?: string;
|
|
};
|
|
type ErrorResponse = {
|
|
error?: ErrorDetail;
|
|
errorCode?: string;
|
|
message?: string;
|
|
};
|
|
|
|
const responseData = error?.response?.data as ErrorResponse | undefined;
|
|
const errorCode = responseData?.error?.errKey || responseData?.errorCode;
|
|
|
|
// 에러 코드별 메시지 매핑
|
|
const errorMessageMap: Record<string, string> = {
|
|
MERCHANT_INFO_MATCH_PASSWORD: t('account.errors.merchantInfoMatchPassword'),
|
|
PASSWORD_LENGHT: t('account.errors.passwordLength'),
|
|
DISALLOWED_CHARACTERS_INCLUDED: t('account.errors.disallowedCharactersIncluded'),
|
|
DISALLOWED_WHITE_SPACE: t('account.errors.disallowedWhiteSpace'),
|
|
NOT_ENOUGH_COMPLEXITY: t('account.errors.notEnoughComplexity'),
|
|
REPEATED_CHARACTER_SEQUENCE: t('account.errors.repeatedCharacterSequence'),
|
|
COMMON_PASSWORD_DETECTED: t('account.errors.commonPasswordDetected')
|
|
};
|
|
|
|
const errorMessage = errorCode && errorMessageMap[errorCode]
|
|
? errorMessageMap[errorCode]
|
|
: (error?.response?.data?.message || t('account.userAddFailed'));
|
|
|
|
snackBar(errorMessage);
|
|
}
|
|
});
|
|
|
|
// 폼 상태 관리
|
|
const [formData, setFormData] = useState({
|
|
usrid: '',
|
|
password: '',
|
|
loginRange: 'MID'
|
|
});
|
|
|
|
// 에러 상태 관리
|
|
const [errors, setErrors] = useState({
|
|
usrid: '',
|
|
password: ''
|
|
});
|
|
|
|
// 사용자 ID 검증을 위한 상태
|
|
const [shouldCheckUsrid, setShouldCheckUsrid] = useState(false);
|
|
const [isCheckingUsrid, setIsCheckingUsrid] = useState(false);
|
|
const debounceTimeout = useRef<NodeJS.Timeout | null>(null);
|
|
|
|
// 이메일/전화번호 상태 관리 (기본 1개씩 빈 공란 배치)
|
|
const [newEmails, setNewEmails] = useState<string[]>(['']);
|
|
const [newPhones, setNewPhones] = useState<string[]>(['']);
|
|
const [editableEmailIndex, setEditableEmailIndex] = useState<number>(0);
|
|
const [editablePhoneIndex, setEditablePhoneIndex] = useState<number>(0);
|
|
const [readOnlyEmails, setReadOnlyEmails] = useState<Set<number>>(new Set());
|
|
const [readOnlyPhones, setReadOnlyPhones] = useState<Set<number>>(new Set());
|
|
|
|
// 사용자 ID 존재 여부 확인 쿼리
|
|
const { data: userExistsData, isLoading: isUserExistsLoading } = useUserExistsUseridQuery(
|
|
shouldCheckUsrid && formData.usrid.trim().length > 0 ? formData.usrid : '',
|
|
{
|
|
enabled: shouldCheckUsrid && formData.usrid.trim().length > 0,
|
|
}
|
|
);
|
|
|
|
// Handle user exists query result
|
|
useEffect(() => {
|
|
if (userExistsData && shouldCheckUsrid) {
|
|
setIsCheckingUsrid(false);
|
|
if (userExistsData.exists) {
|
|
setErrors(prev => ({ ...prev, usrid: t('account.duplicateIdExists') }));
|
|
} else {
|
|
setErrors(prev => ({ ...prev, usrid: '' }));
|
|
}
|
|
setShouldCheckUsrid(false);
|
|
}
|
|
}, [userExistsData, shouldCheckUsrid, t]);
|
|
|
|
// 이메일/전화번호 관리 함수들 (user-login-auth-info-wrap 방식)
|
|
const handleAddEmail = () => {
|
|
// 현재 편집 중인 항목을 읽기전용으로 고정
|
|
if (editableEmailIndex >= 0) {
|
|
setReadOnlyEmails(prev => new Set([...prev, editableEmailIndex]));
|
|
}
|
|
|
|
// 새로운 편집 가능한 항목 추가
|
|
setEditableEmailIndex(newEmails.length);
|
|
setNewEmails([...newEmails, '']);
|
|
};
|
|
|
|
const handleAddPhone = () => {
|
|
// 현재 편집 중인 항목을 읽기전용으로 고정
|
|
if (editablePhoneIndex >= 0) {
|
|
setReadOnlyPhones(prev => new Set([...prev, editablePhoneIndex]));
|
|
}
|
|
|
|
// 새로운 편집 가능한 항목 추가
|
|
setEditablePhoneIndex(newPhones.length);
|
|
setNewPhones([...newPhones, '']);
|
|
};
|
|
|
|
const handleRemoveNewEmail = (index: number) => {
|
|
const updatedEmails = newEmails.filter((_, i) => i !== index);
|
|
setNewEmails(updatedEmails);
|
|
|
|
// 읽기전용 인덱스들을 업데이트
|
|
const updatedReadOnlyEmails = new Set<number>();
|
|
readOnlyEmails.forEach(readOnlyIndex => {
|
|
if (readOnlyIndex < index) {
|
|
updatedReadOnlyEmails.add(readOnlyIndex);
|
|
} else if (readOnlyIndex > index) {
|
|
updatedReadOnlyEmails.add(readOnlyIndex - 1);
|
|
}
|
|
});
|
|
setReadOnlyEmails(updatedReadOnlyEmails);
|
|
|
|
// 편집 가능한 인덱스 조정
|
|
if (index === editableEmailIndex) {
|
|
setEditableEmailIndex(-1);
|
|
} else if (index < editableEmailIndex) {
|
|
setEditableEmailIndex(editableEmailIndex - 1);
|
|
}
|
|
};
|
|
|
|
const handleRemoveNewPhone = (index: number) => {
|
|
const updatedPhones = newPhones.filter((_, i) => i !== index);
|
|
setNewPhones(updatedPhones);
|
|
|
|
// 읽기전용 인덱스들을 업데이트
|
|
const updatedReadOnlyPhones = new Set<number>();
|
|
readOnlyPhones.forEach(readOnlyIndex => {
|
|
if (readOnlyIndex < index) {
|
|
updatedReadOnlyPhones.add(readOnlyIndex);
|
|
} else if (readOnlyIndex > index) {
|
|
updatedReadOnlyPhones.add(readOnlyIndex - 1);
|
|
}
|
|
});
|
|
setReadOnlyPhones(updatedReadOnlyPhones);
|
|
|
|
// 편집 가능한 인덱스 조정
|
|
if (index === editablePhoneIndex) {
|
|
setEditablePhoneIndex(-1);
|
|
} else if (index < editablePhoneIndex) {
|
|
setEditablePhoneIndex(editablePhoneIndex - 1);
|
|
}
|
|
};
|
|
|
|
const handleNewEmailChange = (index: number, value: string) => {
|
|
const updated = [...newEmails];
|
|
updated[index] = value;
|
|
setNewEmails(updated);
|
|
};
|
|
|
|
const handleNewPhoneChange = (index: number, value: string) => {
|
|
const updated = [...newPhones];
|
|
updated[index] = value;
|
|
setNewPhones(updated);
|
|
};
|
|
|
|
|
|
// 폼 입력 핸들러
|
|
const handleInputChange = (field: string, value: string) => {
|
|
// 사용자 ID의 경우 영문과 숫자만 허용
|
|
if (field === 'usrid') {
|
|
// 영문과 숫자가 아닌 문자 제거
|
|
const filteredValue = value.replace(/[^a-zA-Z0-9]/g, '');
|
|
setFormData(prev => ({ ...prev, [field]: filteredValue }));
|
|
|
|
// 기존 타이머 클리어
|
|
if (debounceTimeout.current) {
|
|
clearTimeout(debounceTimeout.current);
|
|
}
|
|
|
|
// 에러 초기화
|
|
setErrors(prev => ({ ...prev, usrid: '' }));
|
|
|
|
// 값이 있으면 500ms 후 검증 실행
|
|
if (filteredValue.trim().length > 0) {
|
|
setIsCheckingUsrid(true);
|
|
debounceTimeout.current = setTimeout(() => {
|
|
setShouldCheckUsrid(true);
|
|
}, 500);
|
|
} else {
|
|
setIsCheckingUsrid(false);
|
|
}
|
|
} else {
|
|
setFormData(prev => ({ ...prev, [field]: value }));
|
|
// 다른 필드는 에러만 초기화
|
|
if (errors[field as keyof typeof errors]) {
|
|
setErrors(prev => ({ ...prev, [field]: '' }));
|
|
}
|
|
}
|
|
};
|
|
|
|
// 비밀번호 blur 핸들러
|
|
const handlePasswordBlur = () => {
|
|
const result = validatePassword(formData.password, t);
|
|
setErrors(prev => ({
|
|
...prev,
|
|
password: result.isValid ? '' : result.errorMessage
|
|
}));
|
|
};
|
|
|
|
// 컴포넌트 언마운트 시 타이머 정리
|
|
useEffect(() => {
|
|
return () => {
|
|
if (debounceTimeout.current) {
|
|
clearTimeout(debounceTimeout.current);
|
|
}
|
|
};
|
|
}, []);
|
|
|
|
// 검증 함수들
|
|
const isValidEmail = (email: string) => {
|
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
return emailRegex.test(email);
|
|
};
|
|
|
|
const isValidPhone = (phone: string) => {
|
|
const phoneRegex = /^010\d{8}$/;
|
|
return phoneRegex.test(phone);
|
|
};
|
|
|
|
// 이메일 추가 버튼 활성화 조건
|
|
const isEmailAddButtonEnabled = () => {
|
|
if (newEmails.length === 0) return true;
|
|
|
|
const lastEmailIndex = newEmails.length - 1;
|
|
const lastEmail = newEmails[lastEmailIndex];
|
|
|
|
return lastEmailIndex >= editableEmailIndex &&
|
|
lastEmail &&
|
|
lastEmail.trim() &&
|
|
isValidEmail(lastEmail) &&
|
|
!hasDuplicateEmail();
|
|
};
|
|
|
|
// 전화번호 추가 버튼 활성화 조건
|
|
const isPhoneAddButtonEnabled = () => {
|
|
if (newPhones.length === 0) return true;
|
|
|
|
const lastPhoneIndex = newPhones.length - 1;
|
|
const lastPhone = newPhones[lastPhoneIndex];
|
|
|
|
return lastPhoneIndex >= editablePhoneIndex &&
|
|
lastPhone &&
|
|
lastPhone.trim() &&
|
|
isValidPhone(lastPhone) &&
|
|
!hasDuplicatePhone();
|
|
};
|
|
|
|
// 중복 검증
|
|
const hasDuplicateEmail = () => {
|
|
const validEmails = newEmails.filter(e => e.trim());
|
|
const uniqueEmails = new Set(validEmails);
|
|
return validEmails.length !== uniqueEmails.size;
|
|
};
|
|
|
|
const hasDuplicatePhone = () => {
|
|
const validPhones = newPhones.filter(p => p.trim());
|
|
const uniquePhones = new Set(validPhones);
|
|
return validPhones.length !== uniquePhones.size;
|
|
};
|
|
|
|
// 삭제 버튼 활성화 조건
|
|
const isDeleteButtonEnabled = () => {
|
|
const totalCount = newEmails.length + newPhones.length;
|
|
return totalCount > 1;
|
|
};
|
|
|
|
// 저장 버튼 활성화 조건 체크
|
|
const isSaveButtonEnabled = () => {
|
|
// 1. 사용자 ID가 입력되고 중복되지 않아야 함
|
|
if (!formData.usrid.trim()) return false;
|
|
if (errors.usrid) return false;
|
|
if (isCheckingUsrid || isUserExistsLoading) return false;
|
|
|
|
// 2. 비밀번호 검증
|
|
if (!formData.password.trim()) return false;
|
|
if (!validatePassword(formData.password, t).isValid) return false;
|
|
|
|
// 3. 입력된 모든 이메일과 휴대폰 번호가 유효한 형식이어야 함
|
|
const nonEmptyEmails = newEmails.filter(email => email.trim());
|
|
const nonEmptyPhones = newPhones.filter(phone => phone.trim());
|
|
|
|
// 입력된 이메일이 있으면 모두 유효한 형식이어야 함
|
|
if (nonEmptyEmails.length > 0) {
|
|
const invalidEmails = nonEmptyEmails.filter(email => !isValidEmail(email));
|
|
if (invalidEmails.length > 0) return false;
|
|
}
|
|
|
|
// 입력된 휴대폰 번호가 있으면 모두 유효한 형식이어야 함
|
|
if (nonEmptyPhones.length > 0) {
|
|
const invalidPhones = nonEmptyPhones.filter(phone => !isValidPhone(phone));
|
|
if (invalidPhones.length > 0) return false;
|
|
}
|
|
|
|
// 4. 유효한 이메일 또는 휴대폰 번호가 최소 1개 이상
|
|
const validEmails = nonEmptyEmails.filter(email => isValidEmail(email));
|
|
const validPhones = nonEmptyPhones.filter(phone => isValidPhone(phone));
|
|
|
|
if (validEmails.length === 0 && validPhones.length === 0) return false;
|
|
|
|
// 5. 중복이 없어야 함
|
|
if (hasDuplicateEmail() || hasDuplicatePhone()) return false;
|
|
|
|
return true;
|
|
};
|
|
|
|
// 폼 검증
|
|
const validateForm = () => {
|
|
const newErrors = { usrid: '', password: '' };
|
|
let isValid = true;
|
|
|
|
// 사용자 ID 검증
|
|
if (!formData.usrid.trim()) {
|
|
newErrors.usrid = t('account.pleaseEnterId');
|
|
isValid = false;
|
|
}
|
|
|
|
// 비밀번호 검증
|
|
const passwordResult = validatePassword(formData.password, t);
|
|
if (!passwordResult.isValid) {
|
|
newErrors.password = passwordResult.errorMessage;
|
|
isValid = false;
|
|
}
|
|
|
|
// 이메일/전화번호 중 하나는 필수
|
|
const validEmails = newEmails.filter(email => email.trim() && isValidEmail(email));
|
|
const validPhones = newPhones.filter(phone => phone.trim() && isValidPhone(phone));
|
|
|
|
if (validEmails.length === 0 && validPhones.length === 0) {
|
|
// 최소 하나의 유효한 연락처가 필요
|
|
isValid = false;
|
|
}
|
|
|
|
// 중복이 있으면 비활성화
|
|
if (hasDuplicateEmail() || hasDuplicatePhone()) {
|
|
isValid = false;
|
|
}
|
|
|
|
setErrors(newErrors);
|
|
return isValid;
|
|
};
|
|
|
|
// 저장 핸들러
|
|
const handleSave = async () => {
|
|
if (!validateForm()) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// verifications 배열 생성
|
|
const verifications: VerificationItem[] = [];
|
|
|
|
newEmails.forEach(email => {
|
|
if (email.trim() && isValidEmail(email)) {
|
|
verifications.push({ type: 'EMAIL', contact: email });
|
|
}
|
|
});
|
|
|
|
newPhones.forEach(phone => {
|
|
if (phone.trim() && isValidPhone(phone)) {
|
|
verifications.push({ type: 'PHONE', contact: phone });
|
|
}
|
|
});
|
|
|
|
const request = {
|
|
mid: mid,
|
|
usrid: formData.usrid,
|
|
password: formData.password,
|
|
loginRange: formData.loginRange,
|
|
verifications: verifications
|
|
};
|
|
|
|
const response = await userCreate(request);
|
|
|
|
if (response?.status) {
|
|
// 성공 시 사용자 관리 페이지로 이동
|
|
snackBar(t('account.userAddedSuccessfully'));
|
|
navigate(PATHS.account.user.manage);
|
|
} else if (response?.error) {
|
|
// 에러 처리
|
|
const error = response.error;
|
|
|
|
// 에러 코드별 메시지 매핑
|
|
const errorMessageMap: Record<string, string> = {
|
|
MERCHANT_INFO_MATCH_PASSWORD: t('account.errors.merchantInfoMatchPassword'),
|
|
PASSWORD_LENGHT: t('account.errors.passwordLength'),
|
|
DISALLOWED_CHARACTERS_INCLUDED: t('account.errors.disallowedCharactersIncluded'),
|
|
DISALLOWED_WHITE_SPACE: t('account.errors.disallowedWhiteSpace'),
|
|
NOT_ENOUGH_COMPLEXITY: t('account.errors.notEnoughComplexity'),
|
|
REPEATED_CHARACTER_SEQUENCE: t('account.errors.repeatedCharacterSequence'),
|
|
COMMON_PASSWORD_DETECTED: t('account.errors.commonPasswordDetected')
|
|
};
|
|
|
|
if (error.errKey === 'USER_DUPLICATE') {
|
|
setErrors(prev => ({ ...prev, usrid: t('account.duplicateIdExists') }));
|
|
} else if (error.errKey && errorMessageMap[error.errKey]) {
|
|
// 비밀번호 관련 에러는 password 필드에 표시
|
|
const errorMsg = errorMessageMap[error.errKey];
|
|
if (errorMsg) {
|
|
setErrors(prev => ({ ...prev, password: errorMsg }));
|
|
}
|
|
} else if (error.errKey === 'VALIDATION_ERROR') {
|
|
// 검증 에러 처리
|
|
const details = error.details as Record<string, unknown>;
|
|
const validationErrors = details?.validationErrors as Record<string, string> | undefined;
|
|
if (validationErrors) {
|
|
// 필드별 에러 메시지 설정
|
|
const newErrors = { ...errors };
|
|
if (validationErrors.passwordField) {
|
|
newErrors.password = validationErrors.passwordField;
|
|
}
|
|
if (validationErrors.usridField) {
|
|
newErrors.usrid = validationErrors.usridField;
|
|
}
|
|
setErrors(newErrors);
|
|
}
|
|
snackBar(error.message || t('account.userAddFailed'));
|
|
} else {
|
|
// 기타 에러 처리
|
|
snackBar(error.message || t('account.userAddFailed'));
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('User creation error:', error);
|
|
// catch된 에러는 onError에서 처리됨
|
|
}
|
|
};
|
|
|
|
useSetHeaderTitle(t('account.addUser'));
|
|
useSetHeaderType(HeaderType.LeftArrow);
|
|
useSetFooterMode(false);
|
|
useSetOnBack(() => {
|
|
navigate(PATHS.account.user.manage);
|
|
});
|
|
|
|
return (
|
|
<>
|
|
<main>
|
|
<div className="tab-content">
|
|
<div className="tab-pane sub active">
|
|
<div className="ing-list pb-86">
|
|
<div className="user-add">
|
|
<div className="ua-row">
|
|
<div className="ua-label">{t('account.userId')} <span className="red">*</span></div>
|
|
<div style={{ position: 'relative' }}>
|
|
<input
|
|
className={`wid-100 ${errors.usrid ? 'error' : ''}`}
|
|
type="text"
|
|
placeholder={t('account.pleaseEnterId')}
|
|
value={formData.usrid}
|
|
onChange={(e) => handleInputChange('usrid', e.target.value)}
|
|
/>
|
|
{isCheckingUsrid && (
|
|
<div style={{
|
|
position: 'absolute',
|
|
right: '10px',
|
|
top: '50%',
|
|
transform: 'translateY(-50%)',
|
|
fontSize: '12px',
|
|
color: '#666'
|
|
}}>
|
|
{t('account.checking')}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
{errors.usrid && <div className="ua-help error pt-10">{errors.usrid}</div>}
|
|
{!errors.usrid && formData.usrid && userExistsData && !userExistsData.exists && (
|
|
<div className="ua-help" style={{ color: '#78D197' }}>{t('account.availableId')}</div>
|
|
)}
|
|
|
|
<div className="ua-row">
|
|
<div className="ua-label">{t('account.password')} <span className="red">*</span></div>
|
|
<input
|
|
className={`wid-100 ${errors.password ? 'error' : ''}`}
|
|
type="password"
|
|
placeholder={t('account.pleaseEnter8OrMoreCharacters')}
|
|
value={formData.password}
|
|
onChange={(e) => handleInputChange('password', e.target.value)}
|
|
onBlur={handlePasswordBlur}
|
|
/>
|
|
</div>
|
|
{errors.password && <div className="ua-help error pt-10">{errors.password}</div>}
|
|
|
|
<div className="ua-row">
|
|
<div className="ua-label">{t('account.loginRange')}</div>
|
|
<select
|
|
className="wid-100"
|
|
value={formData.loginRange}
|
|
onChange={(e) => handleInputChange('loginRange', e.target.value)}
|
|
>
|
|
<option value="MID">MID</option>
|
|
<option value="GID">MID + GID</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div className="info-divider"></div>
|
|
<div className="user-add info">
|
|
<div className="ua-desc">
|
|
<div className="ua-title">{t('account.identityVerificationInfo')}</div>
|
|
<p className="ua-note">{t('account.identityVerificationNotice')}</p>
|
|
</div>
|
|
|
|
<div className="ua-group">
|
|
<div className="ua-group-header">
|
|
<div className="ua-group-title">{t('account.emailAddress')}</div>
|
|
<button
|
|
className="ic20 plus"
|
|
type="button"
|
|
aria-label={t('account.addEmail')}
|
|
onClick={handleAddEmail}
|
|
disabled={!isEmailAddButtonEnabled()}
|
|
></button>
|
|
</div>
|
|
{newEmails.map((email, index) => {
|
|
const isReadOnly = readOnlyEmails.has(index) || index !== editableEmailIndex;
|
|
const displayValue = isReadOnly && email ? maskEmail(email) : email;
|
|
|
|
return (
|
|
<div className="ua-input-row" key={index}>
|
|
<input
|
|
className="wid-100"
|
|
type="text"
|
|
placeholder="example@domain.com"
|
|
value={displayValue}
|
|
onChange={(e) => handleNewEmailChange(index, e.target.value)}
|
|
onFocus={(e) => {
|
|
handleInputFocus(e);
|
|
setEditableEmailIndex(index);
|
|
}}
|
|
onBlur={() => {
|
|
if (email && isValidEmail(email)) {
|
|
setEditableEmailIndex(-1);
|
|
}
|
|
}}
|
|
readOnly={isReadOnly}
|
|
/>
|
|
<button
|
|
className="icon-btn minus"
|
|
type="button"
|
|
aria-label={t('common.delete')}
|
|
onClick={() => handleRemoveNewEmail(index)}
|
|
disabled={!isDeleteButtonEnabled()}
|
|
></button>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
<div className="ua-group">
|
|
<div className="ua-group-header">
|
|
<div className="ua-group-title">{t('account.phoneNumber')}</div>
|
|
<button
|
|
className="ic20 plus"
|
|
type="button"
|
|
aria-label={t('account.addPhone')}
|
|
onClick={handleAddPhone}
|
|
disabled={!isPhoneAddButtonEnabled()}
|
|
></button>
|
|
</div>
|
|
{newPhones.map((phone, index) => {
|
|
const isReadOnly = readOnlyPhones.has(index) || index !== editablePhoneIndex;
|
|
const displayValue = isReadOnly && phone ? maskPhoneNumber(phone) : phone;
|
|
|
|
return (
|
|
<div className="ua-input-row" key={index}
|
|
style={keyboardAwarePadding}
|
|
>
|
|
<input
|
|
className="wid-100"
|
|
type="tel"
|
|
placeholder="01012345678"
|
|
value={displayValue}
|
|
onChange={(e) => handleNewPhoneChange(index, e.target.value)}
|
|
onFocus={(e) => {
|
|
handleInputFocus(e);
|
|
setEditablePhoneIndex(index);
|
|
}}
|
|
onBlur={() => {
|
|
if (phone && isValidPhone(phone)) {
|
|
setEditablePhoneIndex(-1);
|
|
}
|
|
}}
|
|
readOnly={isReadOnly}
|
|
/>
|
|
<button
|
|
className="icon-btn minus"
|
|
type="button"
|
|
aria-label={t('common.delete')}
|
|
onClick={() => handleRemoveNewPhone(index)}
|
|
disabled={!isDeleteButtonEnabled()}
|
|
></button>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="apply-row">
|
|
<button
|
|
className="btn-50 btn-blue flex-1"
|
|
type="button"
|
|
onClick={handleSave}
|
|
disabled={!isSaveButtonEnabled() || isPending}
|
|
>
|
|
{isPending ? t('common.saving') : t('common.save')}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</>
|
|
);
|
|
}; |