비밀번호 변경 기능 개선 및 검증 강화
- 비밀번호 변경 페이지에 확인 Dialog 추가 (로그인/거래취소) - 비밀번호 에러 코드별 상세 메시지 처리 * INVALID_PASSWORD, UPDATE_PASSWORD_FAIL, PREVIOUS_PASSWORD * MERCHANT_INFO_MATCH_PASSWORD, PASSWORD_LENGHT * DISALLOWED_CHARACTERS_INCLUDED, DISALLOWED_WHITE_SPACE * NOT_ENOUGH_COMPLEXITY, REPEATED_CHARACTER_SEQUENCE * COMMON_PASSWORD_DETECTED - 비밀번호 입력 검증 로직 통합 (validatePassword) - 이메일/전화번호 마스킹 기능 추가 - 사용자 추가 페이지 에러 처리 개선 - 다국어 메시지 추가 (한국어/영어) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,8 @@ import { useUserCreateMutation } from '@/entities/user/api/use-user-create-mutat
|
||||
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';
|
||||
|
||||
export const UserAddAccountPage = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -22,11 +24,36 @@ export const UserAddAccountPage = () => {
|
||||
const { mid } = location.state || {};
|
||||
|
||||
const { mutateAsync: userCreate, isPending } = useUserCreateMutation({
|
||||
onSuccess: () => {
|
||||
snackBar(t('account.userAddedSuccessfully'));
|
||||
},
|
||||
onError: (error) => {
|
||||
snackBar(error?.response?.data?.message || t('account.userAddFailed'));
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -158,22 +185,15 @@ export const UserAddAccountPage = () => {
|
||||
setNewPhones(updated);
|
||||
};
|
||||
|
||||
// 비밀번호 검증 함수
|
||||
const validatePassword = (password: string) => {
|
||||
if (!password.trim()) {
|
||||
return t('account.pleaseEnterPassword');
|
||||
} else if (password.length < 8) {
|
||||
return t('account.pleaseEnter8OrMoreCharacters');
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
// 폼 입력 핸들러
|
||||
const handleInputChange = (field: string, value: string) => {
|
||||
setFormData(prev => ({ ...prev, [field]: value }));
|
||||
|
||||
// 사용자 ID의 경우 디바운스 적용하여 자동 검증
|
||||
// 사용자 ID의 경우 영문과 숫자만 허용
|
||||
if (field === 'usrid') {
|
||||
// 영문과 숫자가 아닌 문자 제거
|
||||
const filteredValue = value.replace(/[^a-zA-Z0-9]/g, '');
|
||||
setFormData(prev => ({ ...prev, [field]: filteredValue }));
|
||||
|
||||
// 기존 타이머 클리어
|
||||
if (debounceTimeout.current) {
|
||||
clearTimeout(debounceTimeout.current);
|
||||
@@ -183,7 +203,7 @@ export const UserAddAccountPage = () => {
|
||||
setErrors(prev => ({ ...prev, usrid: '' }));
|
||||
|
||||
// 값이 있으면 500ms 후 검증 실행
|
||||
if (value.trim().length > 0) {
|
||||
if (filteredValue.trim().length > 0) {
|
||||
setIsCheckingUsrid(true);
|
||||
debounceTimeout.current = setTimeout(() => {
|
||||
setShouldCheckUsrid(true);
|
||||
@@ -192,6 +212,7 @@ export const UserAddAccountPage = () => {
|
||||
setIsCheckingUsrid(false);
|
||||
}
|
||||
} else {
|
||||
setFormData(prev => ({ ...prev, [field]: value }));
|
||||
// 다른 필드는 에러만 초기화
|
||||
if (errors[field as keyof typeof errors]) {
|
||||
setErrors(prev => ({ ...prev, [field]: '' }));
|
||||
@@ -201,10 +222,11 @@ export const UserAddAccountPage = () => {
|
||||
|
||||
// 비밀번호 blur 핸들러
|
||||
const handlePasswordBlur = () => {
|
||||
const passwordError = validatePassword(formData.password);
|
||||
if (passwordError) {
|
||||
setErrors(prev => ({ ...prev, password: passwordError }));
|
||||
}
|
||||
const result = validatePassword(formData.password, t);
|
||||
setErrors(prev => ({
|
||||
...prev,
|
||||
password: result.isValid ? '' : result.errorMessage
|
||||
}));
|
||||
};
|
||||
|
||||
// 컴포넌트 언마운트 시 타이머 정리
|
||||
@@ -281,8 +303,9 @@ export const UserAddAccountPage = () => {
|
||||
if (errors.usrid) return false;
|
||||
if (isCheckingUsrid || isUserExistsLoading) return false;
|
||||
|
||||
// 2. 비밀번호가 8자리 이상
|
||||
if (!formData.password.trim() || formData.password.length < 8) 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());
|
||||
@@ -324,11 +347,9 @@ export const UserAddAccountPage = () => {
|
||||
}
|
||||
|
||||
// 비밀번호 검증
|
||||
if (!formData.password.trim()) {
|
||||
newErrors.password = t('account.pleaseEnterPassword');
|
||||
isValid = false;
|
||||
} else if (formData.password.length < 8) {
|
||||
newErrors.password = t('account.pleaseEnter8OrMoreCharacters');
|
||||
const passwordResult = validatePassword(formData.password, t);
|
||||
if (!passwordResult.isValid) {
|
||||
newErrors.password = passwordResult.errorMessage;
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
@@ -382,21 +403,57 @@ export const UserAddAccountPage = () => {
|
||||
|
||||
const response = await userCreate(request);
|
||||
|
||||
if (response.status) {
|
||||
if (response?.status) {
|
||||
// 성공 시 사용자 관리 페이지로 이동
|
||||
snackBar(t('account.userAddedSuccessfully'));
|
||||
navigate(PATHS.account.user.manage);
|
||||
} else if (response.error) {
|
||||
} else if (response?.error) {
|
||||
// 에러 처리
|
||||
if (response.error.errKey === 'USER_DUPLICATE') {
|
||||
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 {
|
||||
// 기타 에러 처리
|
||||
console.error('User creation failed:', response.error.message);
|
||||
snackBar(error.message || t('account.userAddFailed'));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('User creation error:', error);
|
||||
// catch된 에러는 onError에서 처리됨
|
||||
}
|
||||
};
|
||||
|
||||
@@ -486,25 +543,36 @@ export const UserAddAccountPage = () => {
|
||||
disabled={!isEmailAddButtonEnabled()}
|
||||
></button>
|
||||
</div>
|
||||
{newEmails.map((email, index) => (
|
||||
<div className="ua-input-row" key={index}>
|
||||
<input
|
||||
className="wid-100"
|
||||
type="text"
|
||||
placeholder="example@domain.com"
|
||||
value={email}
|
||||
onChange={(e) => handleNewEmailChange(index, e.target.value)}
|
||||
readOnly={readOnlyEmails.has(index) || index !== editableEmailIndex}
|
||||
/>
|
||||
<button
|
||||
className="icon-btn minus"
|
||||
type="button"
|
||||
aria-label={t('common.delete')}
|
||||
onClick={() => handleRemoveNewEmail(index)}
|
||||
disabled={!isDeleteButtonEnabled()}
|
||||
></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={() => 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">
|
||||
@@ -518,25 +586,36 @@ export const UserAddAccountPage = () => {
|
||||
disabled={!isPhoneAddButtonEnabled()}
|
||||
></button>
|
||||
</div>
|
||||
{newPhones.map((phone, index) => (
|
||||
<div className="ua-input-row" key={index}>
|
||||
<input
|
||||
className="wid-100"
|
||||
type="tel"
|
||||
placeholder="01012345678"
|
||||
value={phone}
|
||||
onChange={(e) => handleNewPhoneChange(index, e.target.value)}
|
||||
readOnly={readOnlyPhones.has(index) || index !== editablePhoneIndex}
|
||||
/>
|
||||
<button
|
||||
className="icon-btn minus"
|
||||
type="button"
|
||||
aria-label={t('common.delete')}
|
||||
onClick={() => handleRemoveNewPhone(index)}
|
||||
disabled={!isDeleteButtonEnabled()}
|
||||
></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}>
|
||||
<input
|
||||
className="wid-100"
|
||||
type="tel"
|
||||
placeholder="01012345678"
|
||||
value={displayValue}
|
||||
onChange={(e) => handleNewPhoneChange(index, e.target.value)}
|
||||
onFocus={() => 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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user