diff --git a/src/entities/account/ui/user-login-auth-info-wrap.tsx b/src/entities/account/ui/user-login-auth-info-wrap.tsx
index a1baefb..7a04b63 100644
--- a/src/entities/account/ui/user-login-auth-info-wrap.tsx
+++ b/src/entities/account/ui/user-login-auth-info-wrap.tsx
@@ -9,6 +9,7 @@ import { useNavigate } from '@/shared/lib/hooks/use-navigate';
import { snackBar } from '@/shared/lib/toast';
import { checkGrant } from '@/shared/lib/check-grant';
import { showAlert } from '@/widgets/show-alert';
+import { maskEmail, maskPhoneNumber } from '@/shared/lib/masking';
export const UserLoginAuthInfoWrap = ({
mid,
@@ -417,7 +418,7 @@ export const UserLoginAuthInfoWrap = ({
@@ -430,24 +431,35 @@ export const UserLoginAuthInfoWrap = ({
>
))}
- {newEmails.map((email, index) => (
-
- handleNewEmailChange(index, e.target.value)}
- readOnly={readOnlyEmails.has(index) || index !== editableEmailIndex}
- />
-
-
- ))}
+ {newEmails.map((email, index) => {
+ const isReadOnly = readOnlyEmails.has(index) || index !== editableEmailIndex;
+ const displayValue = isReadOnly && email ? maskEmail(email) : email;
+
+ return (
+
+ handleNewEmailChange(index, e.target.value)}
+ onFocus={() => setEditableEmailIndex(index)}
+ onBlur={() => {
+ if (email && isValidEmail(email)) {
+ setEditableEmailIndex(-1);
+ }
+ }}
+ readOnly={isReadOnly}
+ />
+
+
+ );
+ })}
diff --git a/src/entities/user/api/use-user-create-mutation.ts b/src/entities/user/api/use-user-create-mutation.ts
index 58b78e3..1cdcbb7 100644
--- a/src/entities/user/api/use-user-create-mutation.ts
+++ b/src/entities/user/api/use-user-create-mutation.ts
@@ -37,6 +37,23 @@ export const userCreate = async (params: UserCreateParams): Promise(API_URL_USER.userCreate(), params, options);
+
+ // 서버 응답이 성공이어도 response.data에 status: false가 있는지 확인
+ const responseData = response.data as any;
+ if (responseData?.status === false && responseData?.error) {
+ return {
+ status: false,
+ error: {
+ root: responseData.error.root || 'USER_CREATE',
+ errKey: responseData.error.errKey || 'UNKNOWN_ERROR',
+ code: responseData.error.code || '500',
+ message: responseData.error.message || 'Unknown error',
+ timestamp: responseData.error.timestamp || new Date().toISOString(),
+ details: responseData.error.details || {}
+ }
+ };
+ }
+
return { status: true, data: response.data };
} catch (error: any) {
return {
diff --git a/src/locales/en.json b/src/locales/en.json
index c5e7a8a..3738521 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -327,6 +327,8 @@
"pleaseEnterId": "Please enter ID",
"pleaseEnterPassword": "Please enter password",
"pleaseEnter8OrMoreCharacters": "Please enter 8 or more characters",
+ "passwordFormatRequirement": "Please set a password with 8 to 20 characters including letters, numbers, and special characters.",
+ "passwordSecurityRequirement": "Password cannot contain consecutive numbers (3+ digits), repeated characters (2+ times), or same characters (3+ times).",
"enterPassword": "Enter password",
"reEnterPassword": "Re-enter password",
"enterPhoneNumber": "Enter phone number",
@@ -339,6 +341,7 @@
"userAddFailed": "Failed to add user.",
"userInfoSavedSuccessfully": "User information saved successfully.",
"userInfoSaveFailed": "Failed to save user information.",
+ "confirmPasswordChange": "Would you like to change your password?",
"passwordChangedSuccessfully": "Password changed successfully.",
"passwordChangeFailed": "Failed to change password.",
"permissionSavedSuccessfully": "Permission saved successfully.",
@@ -357,7 +360,26 @@
"menuPermissions": "Menu Permissions",
"deleteUserConfirm": "Do you want to delete this user?",
"deleteUserSuccess": "User deleted successfully.",
- "deleteUserFailed": "Failed to delete user."
+ "deleteUserFailed": "Failed to delete user.",
+ "passwordNoSpaceAllowed": "Password cannot contain spaces.",
+ "passwordLengthRequirement": "Password must be 8-20 characters long.",
+ "passwordInvalidCharacter": "Password contains invalid characters.",
+ "passwordCombinationRequirement": "Password must include at least 2 of: letters+numbers, letters+special characters, numbers+special characters.",
+ "passwordNoRepeatingCharacters": "Password cannot contain the same character 3 times in a row.",
+ "passwordNoConsecutiveSequence": "Password cannot contain 3 or more consecutive characters or numbers.",
+ "passwordNoKeyboardSequence": "Password cannot contain 3 consecutive characters from keyboard layout.",
+ "errors": {
+ "invalidPassword": "Incorrect password.",
+ "updatePasswordFail": "Password update failed.",
+ "previousPassword": "Cannot use previously used password.",
+ "merchantInfoMatchPassword": "Password cannot match merchant information.",
+ "passwordLength": "Password length error",
+ "disallowedCharactersIncluded": "Contains disallowed characters",
+ "disallowedWhiteSpace": "Contains whitespace characters",
+ "notEnoughComplexity": "Password does not meet complexity requirements",
+ "repeatedCharacterSequence": "Consecutive character/number sequence error",
+ "commonPasswordDetected": "Password validation failed"
+ }
},
"favorite": {
"edit": "Edit",
diff --git a/src/locales/ko.json b/src/locales/ko.json
index 5815dce..5a37048 100644
--- a/src/locales/ko.json
+++ b/src/locales/ko.json
@@ -327,6 +327,8 @@
"pleaseEnterId": "ID를 입력해 주세요",
"pleaseEnterPassword": "비밀번호를 입력해 주세요",
"pleaseEnter8OrMoreCharacters": "8자리 이상 입력해 주세요",
+ "passwordFormatRequirement": "8자리 이상 20자리 이하의 영문,숫자,특수문자를 조합하여 설정해 주시기 바랍니다.",
+ "passwordSecurityRequirement": "패스워드는 3자리 이상의 연속된 숫자, 두 번이상 반복 문자열, 3자리 이상의 동일한 문자열을 사용할 수 없습니다.",
"enterPassword": "비밀번호를 입력하세요",
"reEnterPassword": "비밀번호를 다시 입력하세요",
"enterPhoneNumber": "휴대폰 번호 입력",
@@ -339,6 +341,7 @@
"userAddFailed": "사용자 추가에 실패했습니다.",
"userInfoSavedSuccessfully": "사용자 정보가 성공적으로 저장되었습니다.",
"userInfoSaveFailed": "사용자 정보 저장에 실패했습니다.",
+ "confirmPasswordChange": "비밀번호를 변경하시겠습니까?",
"passwordChangedSuccessfully": "비밀번호가 성공적으로 변경되었습니다.",
"passwordChangeFailed": "비밀번호 변경에 실패했습니다.",
"permissionSavedSuccessfully": "권한이 성공적으로 저장되었습니다.",
@@ -357,7 +360,26 @@
"menuPermissions": "메뉴별 권한 설정",
"deleteUserConfirm": "사용자를 삭제하시겠습니까?",
"deleteUserSuccess": "사용자 삭제를 성공했습니다.",
- "deleteUserFailed": "사용자 삭제를 실패했습니다."
+ "deleteUserFailed": "사용자 삭제를 실패했습니다.",
+ "passwordNoSpaceAllowed": "비밀번호에 공백을 사용할 수 없습니다.",
+ "passwordLengthRequirement": "비밀번호는 8자 이상 20자 이하로 입력해 주세요.",
+ "passwordInvalidCharacter": "허용되지 않는 문자가 포함되어 있습니다.",
+ "passwordCombinationRequirement": "영문+숫자, 영문+특수문자, 숫자+특수문자 중 2가지 이상을 조합해 주세요.",
+ "passwordNoRepeatingCharacters": "동일한 문자를 3번 연속으로 사용할 수 없습니다.",
+ "passwordNoConsecutiveSequence": "연속된 문자 또는 숫자를 3자리 이상 사용할 수 없습니다.",
+ "passwordNoKeyboardSequence": "키보드 배열 순서로 3자리 연속 사용할 수 없습니다.",
+ "errors": {
+ "invalidPassword": "패스워드가 틀립니다.",
+ "updatePasswordFail": "패스워드 업데이트 실패입니다.",
+ "previousPassword": "기존 사용하던 패스워드는 사용할 수 없습니다.",
+ "merchantInfoMatchPassword": "패스워드는 가맹점 정보와 일치할 수 없습니다.",
+ "passwordLength": "비밀번호 길이 오류",
+ "disallowedCharactersIncluded": "입력 비허용 문자 포함",
+ "disallowedWhiteSpace": "공백 문자 포함",
+ "notEnoughComplexity": "패스워드 복잡도 요구사항 미충족",
+ "repeatedCharacterSequence": "연속 문자/숫자 사용 오류",
+ "commonPasswordDetected": "패스워드 검증 실패"
+ }
},
"favorite": {
"edit": "편집하기",
diff --git a/src/pages/account/password/modify-cancel-password-page.tsx b/src/pages/account/password/modify-cancel-password-page.tsx
index 811927b..a0e4f31 100644
--- a/src/pages/account/password/modify-cancel-password-page.tsx
+++ b/src/pages/account/password/modify-cancel-password-page.tsx
@@ -12,6 +12,9 @@ import {
import { useUserChangeCancelPasswordMutation } from '@/entities/user/api/use-user-change-cancel-password-mutation';
import { useStore } from '@/shared/model/store';
import { snackBar } from '@/shared/lib/toast';
+import { overlay } from 'overlay-kit';
+import { Dialog } from '@/shared/ui/dialogs/dialog';
+import { validatePassword } from '@/shared/lib/password-validation';
export const PasswordModifyCancelPasswordPage = () => {
const { t } = useTranslation();
@@ -25,6 +28,26 @@ export const PasswordModifyCancelPasswordPage = () => {
const [mid, setMid] = useState((midItem.length > 0)? userMid: '');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
+ const [passwordError, setPasswordError] = useState('');
+
+ // 허용된 문자만 입력 가능하도록 필터링 (영문, 숫자, 특수문자)
+ const allowedCharsRegex = /^[a-zA-Z0-9!@#$%^&*()\-_+=[\]{}|\\:;"'<>,.?/]*$/;
+
+ const handlePasswordChange = (value: string) => {
+ if (allowedCharsRegex.test(value)) {
+ setPassword(value);
+ // 에러가 있었다면 초기화
+ if (passwordError) {
+ setPasswordError('');
+ }
+ }
+ };
+
+ const handleConfirmPasswordChange = (value: string) => {
+ if (allowedCharsRegex.test(value)) {
+ setConfirmPassword(value);
+ }
+ };
const changeCancelPasswordMutation = useUserChangeCancelPasswordMutation({
onSuccess: () => {
@@ -36,7 +59,45 @@ export const PasswordModifyCancelPasswordPage = () => {
navigate(PATHS.account.password.manage);
},
onError: (error) => {
- snackBar(error?.response?.data?.message || t('account.passwordChangeFailed'));
+ 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;
+
+ // 비밀번호 변경 제한 가맹점 그룹 에러 처리
+ if (errorCode === 'RESTRICTED_MERCHANT_GID') {
+ const errorMessage = responseData?.error?.message || responseData?.message;
+ snackBar(errorMessage || t('account.passwordChangeRestricted'));
+ return;
+ }
+
+ // 에러 코드별 메시지 매핑
+ const errorMessageMap: Record = {
+ INVALID_PASSWORD: t('account.errors.invalidPassword'),
+ UPDATE_PASSWORD_FAIL: t('account.errors.updatePasswordFail'),
+ PREVIOUS_PASSWORD: t('account.errors.previousPassword'),
+ 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]
+ : t('account.passwordChangeFailed');
+
+ snackBar(errorMessage);
}
});
@@ -47,10 +108,16 @@ export const PasswordModifyCancelPasswordPage = () => {
navigate(PATHS.account.password.manage);
});
+ // 비밀번호 blur 핸들러
+ const handlePasswordBlur = () => {
+ const result = validatePassword(password, t);
+ setPasswordError(result.isValid ? '' : result.errorMessage);
+ };
+
// 저장 버튼 활성화 조건 체크
const isFormValid = () => {
return (
- password.length >= 8 &&
+ validatePassword(password, t).isValid &&
confirmPassword.length >= 8 &&
password === confirmPassword
);
@@ -60,9 +127,26 @@ export const PasswordModifyCancelPasswordPage = () => {
const handleSave = () => {
if (!isFormValid()) return;
- changeCancelPasswordMutation.mutate({
- mid,
- password: password
+ overlay.open(({
+ isOpen,
+ close,
+ unmount
+ }) => {
+ return (
+