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'; export const UserAddAccountPage = () => { const { navigate } = useNavigate(); const location = useLocation(); const { mid } = location.state || {}; const { mutateAsync: userCreate, isPending } = useUserCreateMutation({ onSuccess: () => { snackBar('사용자가 성공적으로 추가되었습니다.'); }, onError: (error) => { snackBar(error?.response?.data?.message || '사용자 추가에 실패했습니다.'); } }); // 폼 상태 관리 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(null); // 이메일/전화번호 상태 관리 (기본 1개씩 빈 공란 배치) const [newEmails, setNewEmails] = useState(['']); const [newPhones, setNewPhones] = useState(['']); const [editableEmailIndex, setEditableEmailIndex] = useState(0); const [editablePhoneIndex, setEditablePhoneIndex] = useState(0); const [readOnlyEmails, setReadOnlyEmails] = useState>(new Set()); const [readOnlyPhones, setReadOnlyPhones] = useState>(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: '동일한 ID가 이미 존재합니다.' })); } else { setErrors(prev => ({ ...prev, usrid: '' })); } setShouldCheckUsrid(false); } }, [userExistsData, shouldCheckUsrid]); // 이메일/전화번호 관리 함수들 (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(); 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(); 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 validatePassword = (password: string) => { if (!password.trim()) { return '비밀번호를 입력해 주세요'; } else if (password.length < 8) { return '8자리 이상 입력해 주세요'; } return ''; }; // 폼 입력 핸들러 const handleInputChange = (field: string, value: string) => { setFormData(prev => ({ ...prev, [field]: value })); // 사용자 ID의 경우 디바운스 적용하여 자동 검증 if (field === 'usrid') { // 기존 타이머 클리어 if (debounceTimeout.current) { clearTimeout(debounceTimeout.current); } // 에러 초기화 setErrors(prev => ({ ...prev, usrid: '' })); // 값이 있으면 500ms 후 검증 실행 if (value.trim().length > 0) { setIsCheckingUsrid(true); debounceTimeout.current = setTimeout(() => { setShouldCheckUsrid(true); }, 500); } else { setIsCheckingUsrid(false); } } else { // 다른 필드는 에러만 초기화 if (errors[field as keyof typeof errors]) { setErrors(prev => ({ ...prev, [field]: '' })); } } }; // 비밀번호 blur 핸들러 const handlePasswordBlur = () => { const passwordError = validatePassword(formData.password); if (passwordError) { setErrors(prev => ({ ...prev, password: passwordError })); } }; // 컴포넌트 언마운트 시 타이머 정리 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. 비밀번호가 8자리 이상 if (!formData.password.trim() || formData.password.length < 8) 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 = 'ID를 입력해 주세요'; isValid = false; } // 비밀번호 검증 if (!formData.password.trim()) { newErrors.password = '비밀번호를 입력해 주세요'; isValid = false; } else if (formData.password.length < 8) { newErrors.password = '8자리 이상 입력해 주세요'; 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('사용자가 성공적으로 추가되었습니다.'); navigate(PATHS.account.user.manage); } else if (response.error) { // 에러 처리 if (response.error.errKey === 'USER_DUPLICATE') { setErrors(prev => ({ ...prev, usrid: '동일한 ID가 이미 존재합니다.' })); } else { // 기타 에러 처리 console.error('User creation failed:', response.error.message); } } } catch (error) { console.error('User creation error:', error); } }; useSetHeaderTitle('사용자 추가'); useSetHeaderType(HeaderType.LeftArrow); useSetFooterMode(false); useSetOnBack(() => { navigate(PATHS.account.user.manage); }); return ( <>
사용자ID *
handleInputChange('usrid', e.target.value)} /> {isCheckingUsrid && (
확인 중...
)}
{errors.usrid &&
{errors.usrid}
} {!errors.usrid && formData.usrid && userExistsData && !userExistsData.exists && (
사용 가능한 ID입니다.
)}
비밀번호 *
handleInputChange('password', e.target.value)} onBlur={handlePasswordBlur} />
{errors.password &&
{errors.password}
}
로그인 범위
본인인증용 정보 입력

입력하신 정보는 이후 로그인 및 가맹점 관련 자료 발송에 이용됩니다.

이메일 주소
{newEmails.map((email, index) => (
handleNewEmailChange(index, e.target.value)} readOnly={readOnlyEmails.has(index) || index !== editableEmailIndex} />
))}
휴대폰 번호
{newPhones.map((phone, index) => (
handleNewPhoneChange(index, e.target.value)} readOnly={readOnlyPhones.has(index) || index !== editablePhoneIndex} />
))}
); };