Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat] 회원가입 페이지 중복확인 로직 구현 #150

Merged
merged 1 commit into from
Jan 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions src/pages/signUp/SignUp.styled.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,6 @@ export const LeftContainer = styled.section`

background-image: url('/images/background_img.png');
background-size: cover;

/* background-image: url('data:image/jpeg;base64,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'); */
border-radius: 2rem 0 0 2rem;
`;

Expand Down
120 changes: 90 additions & 30 deletions src/pages/signUp/SignUp.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { usePostNicknameCheck } from '@apis/users/queries';
import { ImgModalcheck } from '@assets/svgs';
import CircleButton from '@components/button/circleButton/CircleButton';
import { AlterModal } from '@components/modal';
Expand All @@ -14,17 +15,12 @@ import * as S from './SignUp.styled';
const SignUp = () => {
const navigate = useNavigate();
const [nickname, setNickname] = useState('');
const [nicknameState, setNicknameState] = useState<'default' | 'act' | 'error' | 'success'>('default');
const [nicknameMessage, setNicknameMessage] = useState<string>('');
const { mutateAsync: checkMutate } = usePostNicknameCheck();
const [isModalOpen, setIsModalOpen] = useState(false);
const [selectedAffiliation, setSelectedAffiliation] = useState<string | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false); // 중복 클릭 방지용

const handleNicknameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setNickname(e.target.value);
};

const handleCloseModal = () => {
setIsModalOpen(false);
};
const [isSubmitting, setIsSubmitting] = useState(false);

useEffect(() => {
const userString = localStorage.getItem('user');
Expand All @@ -47,36 +43,94 @@ const SignUp = () => {
}
}, [navigate]);

// 닉네임 입력 핸들러
const handleNicknameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setNickname(value);

// 닉네임 변경 시 중복 확인 상태 초기화
if (value.length === 0) {
setNicknameState('default'); // 입력값 없음
} else {
setNicknameState('act'); // 입력값 있음
}
setNicknameMessage('');
};

// 닉네임 중복 확인
const handleNicknameCheck = async () => {
if (!nickname.trim()) {
setNicknameState('error');
setNicknameMessage('닉네임을 입력해주세요.');
return;
}

if (!/^[가-힣a-zA-Z0-9]+$/.test(nickname)) {
setNicknameState('error');
setNicknameMessage('초성 또는 모음만으로 구성된 닉네임은 사용할 수 없어요.');
return;
}

setNicknameState('act');
setNicknameMessage('중복 확인 중...');

try {
const checkResponse = await checkMutate(nickname);
if (checkResponse?.statusCode === 200 && checkResponse?.data) {
setNicknameState('error'); // 중복된 닉네임
setNicknameMessage('이미 사용 중인 닉네임이에요.');
} else {
setNicknameState('success'); // 사용 가능 상태
setNicknameMessage('사용 가능한 닉네임이에요.');
}
} catch (error) {
console.error('닉네임 체크 실패:', error);
setNicknameState('error'); // 사용 불가능 상태
setNicknameMessage('중복 확인 중 오류가 발생했어요.');
}
};

// 회원가입 버튼 활성화 조건
const isCircleBtnActive = nickname.length >= 1 && selectedAffiliation !== null && nicknameState === 'success';

// 회원가입 요청
const handleCircleBtnClick = async () => {
if (!nickname || !selectedAffiliation) {
alert('닉네임과 소속을 모두 입력해주세요.');
return;
}

const userString = localStorage.getItem('user');
if (userString) {
const user = JSON.parse(userString);
if (!user.email) {
alert('이메일 정보가 없습니다. 다시 로그인해주세요.');
return;
}
setIsSubmitting(true); // 버튼 중복 클릭 방지
try {
await signup({
nickname,
positions: selectedAffiliation,
email: user.email,
});
setIsModalOpen(true); // 회원가입 성공 시 모달 열기
} catch (error) {
console.error('회원가입 실패:', error);
} finally {
setIsSubmitting(false);
}
if (!userString) {
alert('로그인이 필요합니다. 다시 시도해주세요.');
return;
}

const user = JSON.parse(userString);
if (!user.email) {
alert('이메일 정보가 없습니다. 다시 로그인해주세요.');
return;
}

setIsSubmitting(true);
try {
await signup({
nickname,
positions: selectedAffiliation,
email: user.email,
});
setIsModalOpen(true); // 회원가입 성공 시 모달 열기
} catch (error) {
console.error('회원가입 실패:', error);
} finally {
setIsSubmitting(false);
}
};

const isCircleBtnActive = nickname.length >= 1 && selectedAffiliation !== null;
// 모달 닫기
const handleCloseModal = () => {
setIsModalOpen(false);
};

const modalProps = {
modalTitle: '회원가입이 완료되었어요.',
Expand Down Expand Up @@ -127,7 +181,13 @@ const SignUp = () => {
</S.AffiliationBtnBox>
</S.AffiliationBox>
<S.NicknameInputBox>
<NamingInput value={nickname} onChange={handleNicknameChange} />
<NamingInput
value={nickname}
state={nicknameState}
description={nicknameMessage}
onChange={handleNicknameChange}
onNicknameCheck={handleNicknameCheck}
/>
</S.NicknameInputBox>
<S.SignUpBtn>
<CircleButton size="mini" disabled={!isCircleBtnActive || isSubmitting} onClick={handleCircleBtnClick}>
Expand Down
13 changes: 13 additions & 0 deletions src/pages/signUp/apis/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,16 @@ const signup = async (requestBody: SignupRequest): Promise<void> => {
};

export default signup;

export const postNicknameCheck = async (
nickname: string,
): Promise<{ statusCode: number; message: string; data: boolean } | undefined> => {
try {
const response = await post<{ statusCode: number; message: string; data: boolean }>(
`users/nickname?nickname=${nickname}`,
);
return response;
} catch (error) {
console.error('Error:', error);
}
};
10 changes: 5 additions & 5 deletions src/pages/signUp/components/namingInput/NamingInput.styled.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ export const InputBox = styled.div`
margin-bottom: 0.6rem;
`;

export const DescriptionBox = styled.div`
export const DescriptionBox = styled.div<{ $description: boolean }>`
display: flex;
flex-direction: row-reverse;
flex-direction: ${({ $description }) => ($description ? '' : 'row-reverse')};
justify-content: space-between;
width: 30.1rem;
height: 1.8rem;
Expand All @@ -40,11 +40,11 @@ export const Input = styled.input<{ state: 'default' | 'act' | 'error' | 'succes
border-color: ${({ state, theme }) => {
switch (state) {
case 'act':
return theme.colors.sys_green;
return theme.colors.gray1;
case 'error':
return theme.colors.sys_red;
case 'success':
return null;
return theme.colors.sys_green;
default:
return theme.colors.gray4;
}
Expand All @@ -60,7 +60,7 @@ export const Description = styled.span<{ state: 'default' | 'act' | 'error' | 's
color: ${({ state, theme }) => {
switch (state) {
case 'act':
return theme.colors.sys_green;
return theme.colors.gray1;
case 'error':
return theme.colors.sys_red;
case 'success':
Expand Down
8 changes: 5 additions & 3 deletions src/pages/signUp/components/namingInput/NamingInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ type NamingInputPropTypes = {
inputRestrictions?: string[];
value: string;
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
onNicknameCheck: () => void; // 닉네임 중복 확인 함수
} & InputHTMLAttributes<HTMLInputElement>;

const NamingInput = ({
Expand All @@ -23,6 +24,7 @@ const NamingInput = ({
],
value,
onChange,
onNicknameCheck,
...props
}: NamingInputPropTypes) => {
const placeholder = state === 'default' ? '닉네임을 입력해주세요.' : '';
Expand All @@ -34,16 +36,16 @@ const NamingInput = ({
}
};

const isActive = count > 0; // 입력값이 1글자 이상일 때만 active
const isActive = count > 0; // 입력값이 1글자 이상일 때만 활성화

return (
<S.InputWrapper>
{label && <S.Label>{label}</S.Label>}
<S.InputBox>
<S.Input state={state} value={value} onChange={handleInputChange} placeholder={placeholder} {...props} />
<ConfirmBtn isActive={isActive} />
<ConfirmBtn isActive={isActive} onClick={onNicknameCheck} />
</S.InputBox>
<S.DescriptionBox>
<S.DescriptionBox $description={!!description}>
{description && <S.Description state={state}>{description}</S.Description>}
<S.LetterCount>{count}/10</S.LetterCount>
</S.DescriptionBox>
Expand Down
Loading