비밀번호 변경 기능 개선 및 검증 강화

- 비밀번호 변경 페이지에 확인 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:
Jay Sheen
2025-11-17 20:50:53 +09:00
parent 522ccf7464
commit 2d1dd6f9e7
10 changed files with 748 additions and 128 deletions

View File

@@ -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 = ({
<div className="input-row" key={`existing-email-${index}`}>
<input
type="email"
value={email.content}
value={maskEmail(email.content)}
placeholder="example@domain.com"
readOnly
/>
@@ -430,24 +431,35 @@ export const UserLoginAuthInfoWrap = ({
></button>
</div>
))}
{newEmails.map((email, index) => (
<div className="input-row" key={`new-email-${index}`}>
<input
type="email"
value={email}
placeholder="example@domain.com"
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="input-row" key={`new-email-${index}`}>
<input
type="email"
value={displayValue}
placeholder="example@domain.com"
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="group">
@@ -465,7 +477,7 @@ export const UserLoginAuthInfoWrap = ({
<div className="input-row" key={`existing-phone-${index}`}>
<input
type="tel"
value={phone.content}
value={maskPhoneNumber(phone.content)}
placeholder={t('account.enterPhoneNumber')}
readOnly
/>
@@ -478,24 +490,35 @@ export const UserLoginAuthInfoWrap = ({
></button>
</div>
))}
{newPhones.map((phone, index) => (
<div className="input-row" key={`new-phone-${index}`}>
<input
type="tel"
value={phone}
placeholder={t('account.enterPhoneNumber')}
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="input-row" key={`new-phone-${index}`}>
<input
type="tel"
value={displayValue}
placeholder={t('account.enterPhoneNumber')}
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 className="notice-bar">{t('account.tabChangeResetNotice')}</div>
</div>
</div>

View File

@@ -37,6 +37,23 @@ export const userCreate = async (params: UserCreateParams): Promise<UserCreateMu
};
try {
const response = await axios.post<UserCreateResponse>(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 {