API 요청
- 로그인
- 요청 형태: POST
- 엔드포인트:
/api/signin
- 요청 본문:
- email: 로그인 시도하는 사용자의 이메일 주소
- password: 로그인 시도하는 사용자의 비밀번호
- 응답:
- 200 OK: 로그인에 성공한 경우. 응답 본문에는 1시간의 유효 기간을 가진 인증 토큰이나 사용자 정보를 포함할 수 있습니다.
- 401 Unauthorized: 잘못된 패스워드를 입력한 경우
- 404 Not Found: 없는 사용자의 경우
- 400 Bad Request, 500 Internal Server Error 등의 오류 응답
- 설명: 사용자가 입력한 이메일과 패스워드를 확인하여 로그인을 처리합니다. 로그인에 성공하면 인증 토큰이나 사용자 정보를 반환할 수 있습니다. 이 토큰은 1시간 동안만 유효합니다. 없는 사용자나 잘못된 패스워드인 경우에는 적절한 오류 메시지를 반환합니다.
- 토큰 검증
- 요청 형태: GET/POST
- 엔드포인트:
/api/validate-token
- 요청 헤더:
- Authorization: "Bearer {토큰}"
- 응답:
- 200 OK: 토큰이 유효한 경우 (토큰의 남은 유효 시간이 1시간 미만일 수 있음)
- 401 Unauthorized: 토큰이 유효하지 않거나 만료된 경우
- 설명: 클라이언트는 주기적으로 이 엔드포인트를 호출하여 토큰의 유효성을 검사할 수 있습니다. 만약 토큰이 만료되었거나 유효하지 않다면, 클라이언트는 사용자를 로그아웃 상태로 변경합니다.
- 로그아웃
- 요청 형태: POST
- 엔드포인트:
/api/signout
- 요청 헤더:
- Authorization: "Bearer {토큰}"
- 응답:
- 200 OK: 로그아웃에 성공한 경우
- 401 Unauthorized: 유효하지 않은 토큰의 경우
- 400 Bad Request, 500 Internal Server Error 등의 오류 응답
- 설명: 사용자 로그아웃 요청을 처리합니다. 서버는 이 요청을 받으면 사용자의 현재 세션을 종료하고, 토큰을 무효화합니다. 클라이언트는 이후로는 해당 토큰을 사용할 수 없게 됩니다.
- 사용자 등록 (Sign Up)
- 요청 형태: POST
- 엔드포인트:
/api/signup
- 요청 본문:
- email: 사용자의 이메일 주소
- password: 사용자의 비밀번호
- nickname: 사용자의 닉네임
- 응답:
- 201 Created: 사용자가 성공적으로 등록된 경우
- 400 Bad Request, 409 Conflict (이미 존재하는 이메일 또는 닉네임의 경우), 500 Internal Server Error 등의 오류 응답
- 설명: 새로운 사용자를 등록합니다.
- 이메일 중복 검사
- 요청 형태: GET
- 엔드포인트(다른 네이밍해주셔도 됩니다!):
/api/check-email?email={email}
- 파라미터:
- 응답:
- 200 OK: { exists: true } 또는 { exists: false }
- 400 Bad Request, 500 Internal Server Error 등의 오류 응답
- 설명: 주어진 이메일 주소가 이미 등록되어 있는지 검사합니다. 이미 존재하는 경우
exists: true, 그렇지 않은 경우 **exists: false**를 반환합니다.
- 닉네임 중복 검사
- 요청 형태: GET
- 엔드포인트(다른 네이밍해주셔도 됩니다!):
/api/check-nickname?nickname={nickname}
- 파라미터:
- 응답:
- 200 OK: { exists: true } 또는 { exists: false }
- 400 Bad Request, 500 Internal Server Error 등의 오류 응답
- 설명: 주어진 닉네임이 이미 등록되어 있는지 검사합니다. 이미 존재하는 경우
exists: true, 그렇지 않은 경우 **exists: false**를 반환합니다.
유효성 체크
//validationRules.ts
interface IValidationRule {
required: string;
maxLength?: {
value: number;
message: string;
};
pattern?: {
value: RegExp;
message: string;
};
minLength?: {
value: number;
message: string;
};
}
export const signupValidationRules: { [key: string]: IValidationRule } = {
email: {
required: 'Email is required',
maxLength: {
value: 50,
message: 'Email should be less than 50 characters',
},
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i,
message: 'Invalid email address',
},
},
password: {
required: 'Password is required',
minLength: {
value: 8,
message: 'Password should be at least 8 characters',
},
maxLength: {
value: 15,
message: 'Password should be less than 15 characters',
},
},
confirmPassword: {
required: 'Confirm Password is required',
},
nickname: {
required: 'Nickname is required',
minLength: {
value: 1,
message: 'Nickname should be at least 1 character',
},
maxLength: {
value: 15,
message: 'Nickname should be less than 15 characters',
},
pattern: {
value: /^[A-Za-z가-힣\s]{1,15}$/,
message:
'Nickname should contain only English and Korean characters without leading or trailing spaces',
},
},
};
export const signinValidationRules = {
email: {
required: 'Email is required',
maxLength: {
value: 50,
message: 'Email should be less than 50 characters',
},
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i,
message: 'invalid email address',
},
},
password: {
required: 'Password is required',
minLength: {
value: 8,
message: 'Password should be at least 8 characters',
},
maxLength: {
value: 15,
message: 'Password should be less than 15 characters',
},
},
};
로그인, 회원가입 모달창(어떻게 적용하시는지 궁금하실까봐…)
//SignUpModal.tsx
import React, { MouseEventHandler, useCallback, useEffect, useRef, useState } from 'react';
import Modal from './Modal';
import { Computer } from '@react95/icons';
import AuthInput from '../inputs/AuthInput';
import Button from '../buttons/Button';
import { useForm } from 'react-hook-form';
import { signupValidationRules } from '../../utils/validationRules';
interface ISignUpModalProps {
open: boolean;
style: React.CSSProperties;
onModalClick: MouseEventHandler<HTMLDivElement>;
onClose: MouseEventHandler<HTMLButtonElement>;
onMinimize: MouseEventHandler<HTMLButtonElement>;
handleSignInModalOpen: MouseEventHandler<HTMLLIElement>;
}
interface IFormData {
email: string;
password: string;
nickname: string;
confirmPassword: string;
}
interface IErrorMessages {
email: string | null;
password: string | null;
confirmPassword: string | null;
nickname: string | null;
}
// TODO:: 더미 데이터 (실제로는 IndexedDB에서 가져온 데이터나 API 응답을 사용해야 함)
const fakeDB = {
emails: ['[email protected]', '[email protected]'],
nicknames: ['testNickname', 'exampleNickname'],
};
const SignUpModal: React.FC<ISignUpModalProps> = ({
open,
style,
onClose,
onMinimize,
onModalClick,
handleSignInModalOpen,
}) => {
const signUpModalRef = useRef<HTMLDivElement | null>(null);
const {
register,
handleSubmit,
formState: { errors },
} = useForm<IFormData>();
const [email, setEmail] = useState<string>('');
const [password, setPassword] = useState<string>('');
const [nickname, setNickname] = useState<string>('');
const [confirmPassword, setConfirmPassword] = useState<string>('');
const [passwordSafety, setPasswordSafety] = useState<string>('');
const [errorMessages, setErrorMessages] = useState<IErrorMessages>({
email: null,
password: null,
confirmPassword: null,
nickname: null,
});
// TODO:: 더미 데이터 (실제로는 IndexedDB에서 가져온 데이터나 API 응답을 사용해야 함)
const checkEmailInDB = (email: string) => {
return fakeDB.emails.includes(email);
};
const checkNicknameInDB = (nickname: string) => {
return fakeDB.nicknames.includes(nickname);
};
const getPasswordStrengthMessage = (safetyLevel: string) => {
switch (safetyLevel) {
case 'high':
return { message: 'Strong password', borderColor: 'border-green' };
case 'medium':
return { message: 'Medium strength', borderColor: 'border-blue' };
case 'low':
return { message: 'Weak password', borderColor: 'border-red' };
default:
return { message: '', borderColor: '' };
}
};
const checkPasswordMatch = () => {
if (confirmPassword) {
setErrorMessages((prev) => ({
...prev,
confirmPassword: password !== confirmPassword ? 'Passwords do not match!' : null,
}));
}
};
const checkPasswordSafety = () => {
const hasLowercase = /[a-z]/.test(password);
const hasUppercase = /[A-Z]/.test(password);
const hasDigits = /\d/.test(password);
const hasSpecialCharacters = /[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]+/.test(password);
if (hasLowercase && hasUppercase && hasDigits && hasSpecialCharacters) {
setPasswordSafety('high');
} else if ((hasLowercase || hasUppercase) && hasDigits) {
setPasswordSafety('medium');
} else {
setPasswordSafety('low');
}
};
const onSubmit = useCallback((data: IFormData) => {
console.log(data);
alert('회원가입이 완료되었습니다! 현재 회원가입 기능은 테스트 중입니다.');
}, []);
const isSignupDisabled = () => {
const isEmailValid = !checkEmailInDB(email) && email.length > 0;
const isPasswordValid = passwordSafety !== 'low' && password.length > 0;
const isNicknameValid = !checkNicknameInDB(nickname) && nickname.length > 0;
const isConfirmPasswordValid = confirmPassword === password;
return !isEmailValid || !isPasswordValid || !isNicknameValid || !isConfirmPasswordValid;
};
useEffect(() => {
if (password.length > 0) {
checkPasswordSafety();
} else {
setPasswordSafety('');
}
checkPasswordMatch();
}, [password, confirmPassword]);
// TODO:: 더미 데이터 (실제로는 IndexedDB에서 가져온 데이터나 API 응답을 사용해야 함)
useEffect(() => {
const passwordStrength = getPasswordStrengthMessage(passwordSafety);
setErrorMessages((prev) => ({
...prev,
email: checkEmailInDB(email) ? 'This email is already in use.' : null,
nickname: checkNicknameInDB(nickname) ? 'This nickname is already in use.' : null,
password: password.length > 0 ? passwordStrength.message : null,
}));
}, [email, nickname, password, passwordSafety]);
useEffect(() => {
const emailError = checkEmailInDB(email) ? 'This email is already in use.' : null;
const nicknameError = checkNicknameInDB(nickname)
? 'This nickname is already in use.'
: null;
setErrorMessages((prev) => ({
...prev,
email: emailError,
nickname: nicknameError,
}));
}, [email, nickname]);
return (
<Modal
className="absolute"
open={open}
onClose={onClose}
onMinimize={onMinimize}
onModalClick={onModalClick}
icon={<Computer className="w-auto" />}
title="SignUp"
modalRef={signUpModalRef}
style={style}
>
<div className="px-[16px] py-[26px]">
<h2 className="flex justify-center pb-[26px]">
<Computer className="w-[66px]" />
</h2>
<form onSubmit={handleSubmit(onSubmit)}>
<div className="pb-[26px] w-[250px]">
<div className="w-full mb-[10px]">
<AuthInput
placeholder="email"
word="email"
type="email"
autoFocus={true}
inputProps={register('email', signupValidationRules.email)}
customOnChange={(e) => setEmail(e.target.value)}
error={!!errorMessages.email}
/>
{errorMessages.email && (
<p className="text-xs text-red">{errorMessages.email}</p>
)}
</div>
<div className="w-full mb-[10px]">
<AuthInput
placeholder="password"
word="password"
type="password"
passwordSafety={passwordSafety}
inputProps={register('password', signupValidationRules.password)}
customOnChange={(e) => setPassword(e.target.value)}
/>
{errorMessages.password && (
<p
className={`text-xs ${
passwordSafety === 'high'
? 'text-green'
: passwordSafety === 'medium'
? 'text-blue'
: 'text-red'
}`}
>
{errorMessages.password}
</p>
)}
<AuthInput
placeholder="confirm password"
type="password"
inputProps={register('confirmPassword')}
customOnChange={(e) => setConfirmPassword(e.target.value)}
error={!!errorMessages.confirmPassword}
/>
{errorMessages.confirmPassword && (
<p className="text-xs text-red">{errorMessages.confirmPassword}</p>
)}
</div>
<div className="w-full">
<AuthInput
placeholder="nickname"
word="nickname"
inputProps={register('nickname', signupValidationRules.nickname)}
customOnChange={(e) => setNickname(e.target.value)}
/>
{errors.nickname && (
<p className="text-xs text-red">{errors.nickname.message}</p>
)}
</div>
</div>
<div className="flex justify-center mb-[7px]">
<Button disabled={isSignupDisabled()} className="px-[18px] py-[3px] w-full">
SignUp
</Button>
</div>
</form>
<div className="flex items-center justify-center">
<span className="mr-1 text-xs opacity-50">Already have an account?</span>
<Button
onClick={handleSignInModalOpen}
className="text-xs underline border-none opacity-80"
>
Sign Up
</Button>
</div>
</div>
</Modal>
);
};
export default SignUpModal;
//SignInModal.tsx
import React, { MouseEvent, MouseEventHandler, useCallback, useRef, useState } from 'react';
import Modal from './Modal';
import { Keys } from '@react95/icons';
import AuthInput from '../inputs/AuthInput';
import Button from '../buttons/Button';
import { useForm } from 'react-hook-form';
import { signinValidationRules } from '../../utils/validationRules';
interface ISignInModalProps {
open: boolean;
style: React.CSSProperties;
onModalClick: MouseEventHandler<HTMLDivElement>;
handleSignUpModalOpen: (event: MouseEvent<HTMLButtonElement, globalThis.MouseEvent>) => void;
onClose: MouseEventHandler<HTMLButtonElement>;
onMinimize: MouseEventHandler<HTMLButtonElement>;
}
interface IFormData {
email: string;
password: string;
}
const SignInModal = ({
open,
style,
onClose,
onMinimize,
onModalClick,
handleSignUpModalOpen,
}: ISignInModalProps) => {
const signInModalRef = useRef(null);
const {
register,
handleSubmit,
formState: { errors },
} = useForm<IFormData>();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const onSubmit = useCallback((data: IFormData) => {
// TODO:: 더미 데이터 (실제로는 IndexedDB에서 가져온 데이터나 API 응답을 사용해야 함)
const dummyUsers = [{ email: '[email protected]', password: 'Test1234!' }];
const isValidUser = dummyUsers.some(
(user) => user.email === data.email && user.password === data.password,
);
if (!isValidUser) {
alert('아이디/비밀번호를 확인해주세요.');
return;
}
console.log(data);
alert('로그인에 성공했습니다! 현재 로그인 기능은 테스트 중입니다.');
}, []);
return (
<Modal
className="absolute"
open={open}
onClose={onClose}
onMinimize={onMinimize}
onModalClick={onModalClick}
icon={<Keys className="w-auto" />}
title="Sign In"
modalRef={signInModalRef}
style={style}
>
<div className="px-[16px] py-[26px]">
<h2 className="flex justify-center pb-[26px]">
<Keys className="w-[66px]" />
</h2>
<form onSubmit={handleSubmit(onSubmit)}>
<div className="pb-[26px] w-[250px]">
<div className="mb-[10px] w-full">
<AuthInput
placeholder="email"
word="email"
type="email"
autoFocus={true}
inputProps={register('email', signinValidationRules.email)}
customOnChange={(e) => setEmail(e.target.value)}
error={!!errors.email}
/>
{errors.email && (
<p className="text-xs text-red">{errors.email.message}</p>
)}
</div>
<div className="w-full">
<AuthInput
placeholder="password"
word="password"
type="password"
inputProps={register('password', signinValidationRules.password)}
customOnChange={(e) => setPassword(e.target.value)}
error={!!errors.password}
/>
{errors.password && (
<p className="text-xs text-red">{errors.password.message}</p>
)}
</div>
</div>
<div className="flex justify-center mb-[7px] w-full">
<Button
disabled={!email || !password}
className="w-full px-[18px] py-[3px]"
>
Sign In
</Button>
</div>
</form>
<div className="flex items-center justify-center">
<span className="mr-1 text-xs opacity-50">{"Don't have an account?"}</span>
<Button
onClick={handleSignUpModalOpen}
className="text-xs underline border-none opacity-80"
>
Sign Up
</Button>
</div>
</div>
</Modal>
);
};
export default SignInModal;