Skip to content
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
8 changes: 7 additions & 1 deletion src/frontend/src/api/endpoints/user/user.api.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import instance from '@/api/axios.instance';
import { UpdateUserRequestDto, UserResponseDto } from './user.interface';
import { RegisterDto, UpdateUserRequestDto, UserResponseDto } from './user.interface';

export const userApi = {
// 내 프로필 조회
Expand Down Expand Up @@ -37,4 +37,10 @@ export const userApi = {
const { data } = await instance.get(`users/exists?nickname=${nickname}`);
return data;
},

// 회원가입
register: async (registerDto: RegisterDto) => {
const { data } = await instance.post<UserResponseDto>(`users/register`, registerDto);
return data;
},
};
6 changes: 6 additions & 0 deletions src/frontend/src/api/endpoints/user/user.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,9 @@ export interface UserResponseDto {
profileImages: string[] | null;
stateMessage: string;
}

export interface RegisterDto {
email: string;
password: string;
nickname: string;
}
33 changes: 33 additions & 0 deletions src/frontend/src/components/Modal/RegisterSuccessModal/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Modal } from '@/components/Modal';
import { ButtonColor } from '@/types/enums/ButtonColor';
import { ModalPortal } from '@/components/Modal/ModalPortal';
import { IModal } from '@/components/Modal';
import { useNavigate } from 'react-router-dom';

interface IRegisterSuccessModal {
onCancel: () => void;
}

export const RegisterSuccessModal = ({ onCancel }: IRegisterSuccessModal) => {
const navigate = useNavigate();

const handleConfirm = () => {
navigate('/login');
onCancel();
};

const props: IModal = {
title: '회원가입 완료',
detail: '회원가입이 완료되었습니다.',
confirmText: '로그인 하기',
confirmButtonColor: ButtonColor.ORANGE,
onConfirm: handleConfirm,
onCancel: handleConfirm,
};

return (
<ModalPortal>
<Modal {...props} />
</ModalPortal>
);
};
8 changes: 6 additions & 2 deletions src/frontend/src/pages/RegisterPage/index.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,12 @@ export const IdSaveCheckBox = styled.label`
}
`;

export const WarningMessage = styled.p<{ $isVisible: boolean }>`
export const WarningMessage = styled.p<{ $isVisible: boolean; $color?: string }>`
width: 300px;
height: 0.75rem;
font-size: 0.75rem;
color: var(--palette-status-negative);
color: ${({ $color = 'var(--palette-status-negative)' }) => $color};
opacity: ${({ $isVisible }) => ($isVisible ? 1 : 0)};
text-align: right;
padding-right: 0.5rem;
`;
158 changes: 128 additions & 30 deletions src/frontend/src/pages/RegisterPage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,35 +11,105 @@ import {
WarningMessage,
Wrapper,
} from './index.css';
import { userApi } from '@/api/endpoints/user/user.api';
import { RegisterSuccessModal } from '@/components/Modal/RegisterSuccessModal';

type CheckResult = {
checked: boolean;
isAvailable: boolean;
message: string;
};

export const RegisterPage = () => {
const emailRef = useRef<HTMLInputElement>(null);
const nicknameRef = useRef<HTMLInputElement>(null);
const passwordRef = useRef<HTMLInputElement>(null);
const passwordCheckRef = useRef<HTMLInputElement>(null);
const confirmPasswordRef = useRef<HTMLInputElement>(null);

const [isEmailValid, setIsEmailValid] = useState(false);
const [isNicknameFilled, setIsNicknameFilled] = useState(false);
const [isPasswordFilled, setIsPasswordFilled] = useState(false);
const [isPasswordCheckFilled, setIsPasswordCheckFilled] = useState(false);
const [isPasswordMatched, setIsPasswordMatched] = useState(true);

const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
const [emailServerCheck, setEmailServerCheck] = useState<CheckResult>({
checked: false,
isAvailable: false,
message: '',
});

const [nicknameServerCheck, setNicknameServerCheck] = useState<CheckResult>({
checked: false,
isAvailable: false,
message: '',
});

const [isPasswordValid, setIsPasswordValid] = useState(true);
const [isAgreed, setIsAgreed] = useState(false);
const [onSuccessModal, setOnSuccessModal] = useState(false);

const isFormValid = () => {
return (
isEmailValid &&
emailServerCheck.isAvailable &&
isNicknameFilled &&
nicknameServerCheck.isAvailable &&
isPasswordFilled &&
isPasswordCheckFilled &&
isPasswordMatched &&
isPasswordValid &&
isAgreed
);
};

const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
console.log('submit');
console.log('Email:', emailRef.current?.value);
console.log('Password:', passwordRef.current?.value);
console.log('Password:', passwordCheckRef.current?.value);
if (!isFormValid()) {
return;
}

try {
const result = await userApi.register({
email: emailRef.current?.value || '',
password: passwordRef.current?.value || '',
nickname: nicknameRef.current?.value || '',
});
if (result.userId) {
setOnSuccessModal(true);
}
} catch (_error) {
alert('회원가입에 실패했습니다.');
}
};

const handleEmailCheck = (e: React.MouseEvent<HTMLButtonElement>) => {
const handleEmailCheck = async (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
console.log('email check:', emailRef.current?.value);
const email = emailRef.current?.value || '';

if (isEmailValid && email) {
const result = await userApi.checkEmailExists(email);
console.log(result);
setEmailServerCheck({
checked: true,
isAvailable: result.isAvailable,
message: result.message,
});
}
};

const handleNicknameCheck = (e: React.MouseEvent<HTMLButtonElement>) => {
const handleNicknameCheck = async (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
console.log('nickname check:', nicknameRef.current?.value);
const nickname = nicknameRef.current?.value || '';

if (isNicknameFilled && nickname) {
const result = await userApi.checkNicknameExists(nickname);
console.log(result);
setNicknameServerCheck({
checked: true,
isAvailable: result.isAvailable,
message: result.message,
});
}
};

const handleEmailChange = () => {
Expand All @@ -50,28 +120,40 @@ export const RegisterPage = () => {
const isValid = emailRegex.test(email);

setIsEmailValid(isFilled && isValid);
setEmailServerCheck({
checked: false,
isAvailable: false,
message: '',
});
};

const handleNicknameChange = () => {
const nickname = nicknameRef.current?.value || '';
const isFilled = nickname.trim() !== '';

setIsNicknameFilled(isFilled);
setNicknameServerCheck({
checked: false,
isAvailable: false,
message: '',
});
};

const handlePassword = () => {
const handlePasswordChange = () => {
const password = passwordRef.current?.value || '';
const isFilled = password.trim() !== '';
const passwordRegex = /^(?=.*[a-zA-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,20}$/;

setIsPasswordFilled(isFilled);
setIsPasswordValid(passwordRegex.test(password));
};

const handlePasswordCheck = () => {
const passwordCheck = passwordCheckRef.current?.value || '';
const isFilled = passwordCheck.trim() !== '';
const handleConfirmPasswordChange = () => {
const confirmPassword = confirmPasswordRef.current?.value || '';
const isFilled = confirmPassword.trim() !== '';

setIsPasswordCheckFilled(isFilled);
setIsPasswordMatched(passwordRef.current?.value === passwordCheck);
setIsPasswordMatched(passwordRef.current?.value === confirmPassword);
};

return (
Expand All @@ -94,6 +176,16 @@ export const RegisterPage = () => {
중복 확인
</button>
</InputBox>
<WarningMessage
$color={
emailServerCheck.isAvailable
? 'var(--palette-status-positive)'
: 'var(--palette-status-negative)'
}
$isVisible={emailServerCheck.checked}
>
{emailServerCheck.message}
</WarningMessage>
</div>
<div>
<CommonLabel htmlFor="nickname">닉네임</CommonLabel>
Expand All @@ -111,6 +203,16 @@ export const RegisterPage = () => {
중복 확인
</button>
</InputBox>
<WarningMessage
$color={
nicknameServerCheck.isAvailable
? 'var(--palette-status-positive)'
: 'var(--palette-status-negative)'
}
$isVisible={nicknameServerCheck.checked}
>
{nicknameServerCheck.message}
</WarningMessage>
</div>
<div>
<CommonLabel htmlFor="password">비밀번호</CommonLabel>
Expand All @@ -121,28 +223,31 @@ export const RegisterPage = () => {
autoComplete="new-password"
ref={passwordRef}
$isValid={isPasswordMatched}
onChange={handlePassword}
onChange={handlePasswordChange}
required
/>
<WarningMessage $isVisible={!isPasswordValid}>
8-20자의 영문, 숫자, 특수문자(@$!%*?&)를 포함해야 합니다.
</WarningMessage>
</div>
<div>
<CommonLabel htmlFor="passwordCheck">비밀번호 재확인</CommonLabel>
<CommonLabel htmlFor="confirmPassword">비밀번호 재확인</CommonLabel>
<CommonInput
id="passwordCheck"
id="confirmPassword"
type="password"
placeholder="비밀번호 다시 한번 입력해주세요."
autoComplete="new-password"
ref={passwordCheckRef}
ref={confirmPasswordRef}
$isValid={isPasswordMatched}
onChange={handlePasswordCheck}
onChange={handleConfirmPasswordChange}
required
/>
<WarningMessage $isVisible={!isPasswordMatched}>
비밀번호가 서로 일치하지 않습니다.
</WarningMessage>
</div>
<IdSaveCheckBox>
<input type="checkbox" />
<input type="checkbox" id="agreement" onChange={e => setIsAgreed(e.target.checked)} />
<span>이용약관</span>과 <span>개인정보처리방침</span>에 동의합니다.
</IdSaveCheckBox>
<CommonButton
Expand All @@ -151,19 +256,12 @@ export const RegisterPage = () => {
width="300px"
height="3rem"
borderradius="0.625rem"
disabled={
!(
isEmailValid &&
isNicknameFilled &&
isPasswordFilled &&
isPasswordCheckFilled &&
isPasswordMatched
)
}
disabled={!isFormValid()}
>
가입하기
</CommonButton>
</form>
{onSuccessModal && <RegisterSuccessModal onCancel={() => setOnSuccessModal(false)} />}
</Wrapper>
);
};
Loading