From bc8f58740562a8179db92f7035e9eed9d9c1b7a8 Mon Sep 17 00:00:00 2001 From: Jay Sheen Date: Fri, 21 Nov 2025 15:56:11 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B3=84=EC=A0=95=20=EA=B4=80=EB=A6=AC=20?= =?UTF-8?q?=EC=9D=B4=EB=A9=94=EC=9D=BC/=EC=A0=84=ED=99=94=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EC=8B=A4=EC=8B=9C=EA=B0=84=20validation=20?= =?UTF-8?q?=EB=B0=8F=20=EC=98=A4=EB=A5=98=20=ED=91=9C=EC=8B=9C=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 이메일/전화번호 형식 및 중복 검증 기능 추가 - 실시간 validation 오류 메시지 표시 - 입력 중이거나 오류 발생 시 추가 버튼 비활성화 - 공백 trim 처리하여 중복 검사 정확도 향상 - validation 오류 시 편집 모드 유지하여 수정 가능 - 빈 입력란 blur 시 자동 제거 - 삭제 버튼 항상 활성화 - 다국어 지원 (한국어/영어) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../account/ui/user-login-auth-info-wrap.tsx | 189 +++++++++++++++--- src/locales/en.json | 4 + src/locales/ko.json | 4 + src/pages/account/user/add-account-page.tsx | 146 ++++++++++++-- 4 files changed, 297 insertions(+), 46 deletions(-) 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 7a04b63..2a29c19 100644 --- a/src/entities/account/ui/user-login-auth-info-wrap.tsx +++ b/src/entities/account/ui/user-login-auth-info-wrap.tsx @@ -97,7 +97,21 @@ export const UserLoginAuthInfoWrap = ({ const handleAddEmail = () => { // 현재 편집 중인 항목을 읽기전용으로 고정 if (editableEmailIndex >= 0) { - setReadOnlyEmails(prev => new Set([...prev, editableEmailIndex])); + const currentEmail = newEmails[editableEmailIndex]; + + // 유효성 검사: 값이 있고, 형식이 올바르고, 중복이 아닐 때만 readOnly로 변경 + if (currentEmail && currentEmail.trim() && isValidEmail(currentEmail)) { + // 중복 검사 - trim하여 비교 + const trimmedEmail = currentEmail.trim(); + const allEmails = [ + ...(authMethodData?.emails?.map(e => e.content?.trim()) || []), + ...newEmails.map(e => e.trim()).filter((_, i) => i !== editableEmailIndex) + ].filter(e => e); // 빈 문자열 제거 + + if (!allEmails.includes(trimmedEmail)) { + setReadOnlyEmails(prev => new Set([...prev, editableEmailIndex])); + } + } } // 새로운 편집 가능한 항목 추가 @@ -108,7 +122,21 @@ export const UserLoginAuthInfoWrap = ({ const handleAddPhone = () => { // 현재 편집 중인 항목을 읽기전용으로 고정 if (editablePhoneIndex >= 0) { - setReadOnlyPhones(prev => new Set([...prev, editablePhoneIndex])); + const currentPhone = newPhones[editablePhoneIndex]; + + // 유효성 검사: 값이 있고, 형식이 올바르고, 중복이 아닐 때만 readOnly로 변경 + if (currentPhone && currentPhone.trim() && isValidPhone(currentPhone)) { + // 중복 검사 - trim하여 비교 + const trimmedPhone = currentPhone.trim(); + const allPhones = [ + ...(authMethodData?.phones?.map(p => p.content?.trim()) || []), + ...newPhones.map(p => p.trim()).filter((_, i) => i !== editablePhoneIndex) + ].filter(p => p); // 빈 문자열 제거 + + if (!allPhones.includes(trimmedPhone)) { + setReadOnlyPhones(prev => new Set([...prev, editablePhoneIndex])); + } + } } // 새로운 편집 가능한 항목 추가 @@ -193,43 +221,112 @@ export const UserLoginAuthInfoWrap = ({ return phoneRegex.test(phone); }; + // 이메일 오류 메시지 반환 + const getEmailError = (index: number, email: string): string | null => { + // 값이 없으면 오류 표시 안 함 + if (!email || !email.trim()) return null; + + // 형식 검증 + if (!isValidEmail(email)) { + return t('account.invalidEmailFormat') || '이메일 형식이 올바르지 않습니다.'; + } + + // 중복 검증 + const allEmails = [ + ...(authMethodData?.emails?.map(e => e.content) || []), + ...newEmails + ]; + const currentEmailWithoutSelf = allEmails.filter((e, i) => { + const emailIndex = i - (authMethodData?.emails?.length || 0); + return e.trim() && emailIndex !== index; + }); + if (currentEmailWithoutSelf.includes(email)) { + return t('account.duplicateEmail') || '중복된 이메일입니다.'; + } + + return null; + }; + + // 전화번호 오류 메시지 반환 + const getPhoneError = (index: number, phone: string): string | null => { + // 값이 없으면 오류 표시 안 함 + if (!phone || !phone.trim()) return null; + + // 형식 검증 + if (!isValidPhone(phone)) { + return t('account.invalidPhoneFormat') || '전화번호 형식이 올바르지 않습니다. (010으로 시작하는 11자리)'; + } + + // 중복 검증 + const allPhones = [ + ...(authMethodData?.phones?.map(p => p.content) || []), + ...newPhones + ]; + const currentPhoneWithoutSelf = allPhones.filter((p, i) => { + const phoneIndex = i - (authMethodData?.phones?.length || 0); + return p.trim() && phoneIndex !== index; + }); + if (currentPhoneWithoutSelf.includes(phone)) { + return t('account.duplicatePhone') || '중복된 전화번호입니다.'; + } + + return null; + }; + + // 전체 validation 오류 확인 (이메일 + 전화번호) + const hasAnyValidationError = () => { + // 이메일 오류 확인 + const hasEmailError = newEmails.some((email, index) => { + if (!email || !email.trim()) return false; + return getEmailError(index, email) !== null; + }); + + // 전화번호 오류 확인 + const hasPhoneError = newPhones.some((phone, index) => { + if (!phone || !phone.trim()) return false; + return getPhoneError(index, phone) !== null; + }); + + return hasEmailError || hasPhoneError; + }; + // 이메일 추가 버튼 활성화 조건 const isEmailAddButtonEnabled = () => { + // 이메일 또는 전화번호 입력 중이면 비활성화 + if (editableEmailIndex >= 0 || editablePhoneIndex >= 0) return false; + if (newEmails.length === 0) return true; // 처음은 활성화 const lastEmailIndex = newEmails.length - 1; const lastEmail = newEmails[lastEmailIndex]; - // 마지막 항목이 편집 가능하고, 유효한 형식이며, 중복이 없으면 활성화 - return lastEmailIndex >= editableEmailIndex && - lastEmail && - lastEmail.trim() && - isValidEmail(lastEmail) && - !hasDuplicateEmail(); + // 값이 없으면 비활성화 + if (!lastEmail || !lastEmail.trim()) return false; + + // 이메일 또는 전화번호에 validation 오류가 있으면 비활성화 + return !hasAnyValidationError(); }; // 전화번호 추가 버튼 활성화 조건 const isPhoneAddButtonEnabled = () => { + // 이메일 또는 전화번호 입력 중이면 비활성화 + if (editableEmailIndex >= 0 || editablePhoneIndex >= 0) return false; + if (newPhones.length === 0) return true; // 처음은 활성화 const lastPhoneIndex = newPhones.length - 1; const lastPhone = newPhones[lastPhoneIndex]; - // 마지막 항목이 편집 가능하고, 유효한 형식이며, 중복이 없으면 활성화 - return lastPhoneIndex >= editablePhoneIndex && - lastPhone && - lastPhone.trim() && - isValidPhone(lastPhone) && - !hasDuplicatePhone(); + // 값이 없으면 비활성화 + if (!lastPhone || !lastPhone.trim()) return false; + + // 이메일 또는 전화번호에 validation 오류가 있으면 비활성화 + return !hasAnyValidationError(); }; - // 삭제 버튼 활성화 조건 (전체 항목이 1개만 남으면 비활성화) + // 삭제 버튼 활성화 조건 const isDeleteButtonEnabled = () => { - const totalEmailCount = (authMethodData?.emails?.length || 0) + newEmails.length; - const totalPhoneCount = (authMethodData?.phones?.length || 0) + newPhones.length; - const totalCount = totalEmailCount + totalPhoneCount; - - return totalCount > 1; + return true; // 항상 활성화 }; // 중복 이메일 검증 @@ -442,13 +539,25 @@ export const UserLoginAuthInfoWrap = ({ value={displayValue} placeholder="example@domain.com" onChange={(e) => handleNewEmailChange(index, e.target.value)} - onFocus={() => setEditableEmailIndex(index)} - onBlur={() => { - if (email && isValidEmail(email)) { - setEditableEmailIndex(-1); + onFocus={(e) => { + if (isReadOnly) { + e.target.blur(); + } else { + setEditableEmailIndex(index); } }} + onBlur={() => { + // 값이 없거나 공백만 있으면 입력란 제거 + if (!email || !email.trim()) { + handleRemoveNewEmail(index); + } else if (!getEmailError(index, email)) { + // validation 오류가 없을 때만 편집 모드 해제 + setEditableEmailIndex(-1); + } + // validation 오류가 있으면 편집 모드 유지 (편집 가능 상태) + }} readOnly={isReadOnly} + style={isReadOnly ? { cursor: 'default' } : undefined} />