Skip to content

Commit

Permalink
fix: resolve conflict
Browse files Browse the repository at this point in the history
  • Loading branch information
wlgh1553 committed Nov 14, 2024
2 parents 92deb80 + 156f8b9 commit f92aadc
Show file tree
Hide file tree
Showing 67 changed files with 1,039 additions and 504 deletions.
3 changes: 1 addition & 2 deletions apps/client/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
<title>Ask-It</title>
</head>
<body>
<div id="root"></div>
Expand Down
33 changes: 27 additions & 6 deletions apps/client/src/components/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { Link, useNavigate } from '@tanstack/react-router';

import { Button, SignInModal, SignUpModal } from '@/components';
import { logout, useAuthStore } from '@/features/auth';
import { useModal } from '@/features/modal';

function Header() {
const { isLogin, clearAccessToken } = useAuthStore();

const { Modal: SignUp, openModal: openSignUpModal } = useModal(
<SignUpModal />,
);
Expand All @@ -10,22 +15,38 @@ function Header() {
<SignInModal />,
);

const navigate = useNavigate();

const handleLogout = () =>
logout().then(() => {
clearAccessToken();
});

return (
<>
<div className='h-16 w-full bg-white px-4 py-4 shadow'>
<div className='mx-auto flex h-full w-full max-w-[1194px] items-center justify-between px-4'>
<button className='text-2xl font-bold text-indigo-600' type='button'>
<Link to='/' className='text-2xl font-bold text-indigo-600'>
Ask-It
</button>
</Link>
<div className='flex items-center justify-center gap-2.5'>
<Button
className='hover:bg-gray-200 hover:text-white hover:transition-all'
onClick={openSignInModal}
onClick={isLogin() ? handleLogout : openSignInModal}
>
<p className='text-base font-bold text-black'>로그인</p>
<p className='text-base font-bold text-black'>
{isLogin() ? '로그아웃' : '로그인'}
</p>
</Button>
<Button className='bg-indigo-600' onClick={openSignUpModal}>
<p className='text-base font-bold text-white'>회원 가입</p>
<Button
className='bg-indigo-600'
onClick={
isLogin() ? () => navigate({ to: '/my' }) : openSignUpModal
}
>
<p className='text-base font-bold text-white'>
{isLogin() ? '세션 기록' : '회원가입'}
</p>
</Button>
</div>
</div>
Expand Down
8 changes: 4 additions & 4 deletions apps/client/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export { default as FeatureCard } from './FeatureCard';
export { default as SignUpModal } from './modal/SignUpModal';
export { default as SignInModal } from './modal/SignInModal';
export { default as CreateSessionModal } from './modal/CreateSessionModal';
export { default as QuestionList } from '@/components/qna/QuestionList';
export { default as QuestionDetail } from '@/components/qna/QuestionDetail';
export { default as ChattingList } from '@/components/qna/ChattingList';
export { default as CreateQuestionModal } from '@/components/modal/CreateQuestionModal';
export { default as QuestionList } from './qna/QuestionList';
export { default as QuestionDetail } from './qna/QuestionDetail';
export { default as ChattingList } from './qna/ChattingList';
export { default as CreateQuestionModal } from './modal/CreateQuestionModal';
2 changes: 1 addition & 1 deletion apps/client/src/components/modal/InputField.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ValidationStatus, ValidationStatusWithMessage } from '@/features/auth';
import { ValidationStatus, ValidationStatusWithMessage } from '@/shared';

interface InputFieldProps {
label: string;
Expand Down
18 changes: 12 additions & 6 deletions apps/client/src/components/modal/SignInModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,15 @@ import { useModalContext } from '@/features/modal';
function SignInModal() {
const { closeModal } = useModalContext();

const { email, setEmail, password, setPassword } = useSignInForm();
const {
email,
setEmail,
password,
setPassword,
isLoginEnabled,
handleLogin,
loginFailed,
} = useSignInForm();

return (
<Modal>
Expand All @@ -27,14 +35,12 @@ function SignInModal() {
value={password}
onChange={setPassword}
placeholder='비밀번호를 입력해주세요'
validationStatus={loginFailed}
/>
<div className='mt-4 inline-flex items-start justify-start gap-2.5'>
<Button
className='bg-indigo-600'
onClick={() => {
// TODO: 로그인 API 요청
closeModal();
}}
className={`transition-colors duration-200 ${isLoginEnabled ? 'bg-indigo-600' : 'bg-indigo-300'}`}
onClick={() => handleLogin().then(() => closeModal())}
>
<div className='w-[150px] text-sm font-medium text-white'>
로그인
Expand Down
2 changes: 1 addition & 1 deletion apps/client/src/components/modal/SignUpModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import Button from '../Button';

import InputField from '@/components/modal/InputField';
import Modal from '@/components/modal/Modal';
import { createUser, useSignUpForm } from '@/features/auth';
import { useModalContext } from '@/features/modal';
import { createUser, useSignUpForm } from '@/features/user';

function SignUpModal() {
const { closeModal } = useModalContext();
Expand Down
2 changes: 1 addition & 1 deletion apps/client/src/components/my/SessionRecord.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Link } from '@tanstack/react-router';

import { formatDate } from '@/shared/date.utils';
import { formatDate } from '@/shared';

interface SessionRecordProps {
sessionName: string;
Expand Down
23 changes: 12 additions & 11 deletions apps/client/src/features/auth/auth.api.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
import axios from 'axios';

import {
CreateUserDTO,
VerifyEmailDTO,
VerifyNicknameDTO,
LoginRequestDTO,
LoginResponseDTO,
RefreshResponseDTO,
} from '@/features/auth/auth.dto';

const USER_BASE_URL = `/api/users`;
const AUTH_BASE_URL = '/api/auth';

export const createUser = (createUserDTO: CreateUserDTO) =>
axios.post(USER_BASE_URL, createUserDTO);

export const verifyEmail = (email: string) =>
export const login = (loginDTO: LoginRequestDTO) =>
axios
.get<VerifyEmailDTO>(`${USER_BASE_URL}/emails/${email}`)
.post<LoginResponseDTO>(`${AUTH_BASE_URL}/login`, loginDTO)
.then((res) => res.data);

export const verifyNickname = (nickname: string) =>
export const logout = () => axios.post(`${AUTH_BASE_URL}/logout`);

export const refresh = () =>
axios
.get<VerifyNicknameDTO>(`${USER_BASE_URL}/nicknames/${nickname}`)
.post<RefreshResponseDTO>(`${AUTH_BASE_URL}/token`, undefined, {
withCredentials: true,
})
.then((res) => res.data);
17 changes: 12 additions & 5 deletions apps/client/src/features/auth/auth.dto.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import { SuccessDTO } from '@/shared';
import { ErrorDTO, SuccessDTO } from '@/shared';

export interface CreateUserDTO {
export interface LoginRequestDTO {
email: string;
password: string;
nickname: string;
}

export type VerifyEmailDTO = SuccessDTO<{ exists: boolean }>;
export type LoginResponseDTO =
| SuccessDTO<{
accessToken: string;
}>
| ErrorDTO;

export type VerifyNicknameDTO = SuccessDTO<{ exists: boolean }>;
export type RefreshResponseDTO =
| SuccessDTO<{
accessToken: string;
}>
| ErrorDTO;
179 changes: 39 additions & 140 deletions apps/client/src/features/auth/auth.hook.ts
Original file line number Diff line number Diff line change
@@ -1,156 +1,55 @@
import { debounce } from 'es-toolkit';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { isAxiosError } from 'axios';
import { useState } from 'react';

import { verifyEmail, verifyNickname } from '@/features/auth/index';
import { login } from '@/features/auth/auth.api';
import { useAuthStore } from '@/features/auth/auth.store';
import { ValidationStatusWithMessage } from '@/shared';

export type ValidationStatus = 'INITIAL' | 'PENDING' | 'VALID' | 'INVALID';

export interface ValidationStatusWithMessage {
status: ValidationStatus;
message?: string;
}

const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

const validateEmail = (email: string): ValidationStatusWithMessage => {
if (!email) return { status: 'INITIAL' };
if (!emailRegex.test(email))
return { status: 'INVALID', message: '이메일 형식이 올바르지 않습니다.' };
if (email.includes(' '))
return {
status: 'INVALID',
message: '이메일에 공백이 포함될 수 없습니다.',
};
return { status: 'PENDING', message: '중복 여부를 검사 중입니다.' };
};

const validateNickname = (nickname: string): ValidationStatusWithMessage => {
if (!nickname) return { status: 'INITIAL' };
if (nickname.length < 3 || nickname.length > 20)
return {
status: 'INVALID',
message: '닉네임은 3-20자 사이로 입력해주세요.',
};
if (nickname.includes(' '))
return {
status: 'INVALID',
message: '닉네임에 공백이 포함될 수 없습니다.',
};
return { status: 'PENDING', message: '중복 여부를 검사 중입니다.' };
};

const validatePassword = (password: string): ValidationStatusWithMessage => {
if (!password) return { status: 'INITIAL' };
if (password.length < 8 || password.length > 20)
return {
status: 'INVALID',
message: '비밀번호는 8-20자 사이로 입력해주세요.',
};
if (password.includes(' '))
return {
status: 'INVALID',
message: '비밀번호에는 공백이 포함될 수 없습니다.',
};
return { status: 'VALID', message: '사용 가능한 비밀번호입니다.' };
};
export function useSignInForm() {
const { setAccessToken } = useAuthStore();

export function useSignUpForm() {
const [email, setEmail] = useState('');

const [nickname, setNickname] = useState('');

const [password, setPassword] = useState('');

const [emailValidationStatus, setEmailValidationStatus] =
useState<ValidationStatusWithMessage>({ status: 'INITIAL' });

const [nicknameValidationStatus, setNicknameValidationStatus] =
useState<ValidationStatusWithMessage>({ status: 'INITIAL' });

const [passwordValidationStatus, setPasswordValidationStatus] =
useState<ValidationStatusWithMessage>({ status: 'INITIAL' });

const isSignUpEnabled = useMemo(
() =>
emailValidationStatus.status === 'VALID' &&
nicknameValidationStatus.status === 'VALID' &&
passwordValidationStatus.status === 'VALID',
[emailValidationStatus, nicknameValidationStatus, passwordValidationStatus],
);

const checkEmailToVerify = useCallback(
debounce(async (emailToVerify: string) => {
const response = await verifyEmail(emailToVerify);

setEmail(emailToVerify);
setEmailValidationStatus(
response.data.exists
? { status: 'INVALID', message: '이미 사용 중인 이메일입니다.' }
: { status: 'VALID', message: '사용 가능한 이메일입니다.' },
);
}, 500),
[],
);

const checkNicknameToVerify = useCallback(
debounce(async (nicknameToVerify: string) => {
const response = await verifyNickname(nicknameToVerify);

setNickname(nicknameToVerify);
setNicknameValidationStatus(
response.data.exists
? { status: 'INVALID', message: '이미 사용 중인 닉네임입니다.' }
: { status: 'VALID', message: '사용 가능한 닉네임입니다.' },
);
}, 500),
[],
);

useEffect(() => {
const validationStatus = validateEmail(email);
setEmailValidationStatus(validationStatus);
if (validationStatus.status === 'PENDING') checkEmailToVerify(email);
else checkEmailToVerify.cancel();
}, [email, checkEmailToVerify]);

useEffect(() => {
const validationStatus = validateNickname(nickname);
setNicknameValidationStatus(validationStatus);
if (validationStatus.status === 'PENDING') checkNicknameToVerify(nickname);
else checkNicknameToVerify.cancel();
}, [nickname, checkNicknameToVerify]);

useEffect(() => {
setPasswordValidationStatus(validatePassword(password));
}, [password]);

return {
email,
setEmail,
nickname,
setNickname,
password,
setPassword,
emailValidationStatus,
nicknameValidationStatus,
passwordValidationStatus,
isSignUpEnabled,
const isLoginEnabled = email.length > 0 && password.length > 7;

const [loginFailed, setLoginFailed] = useState<ValidationStatusWithMessage>({
status: 'INITIAL',
});

const handleLogin = async () => {
try {
const response = await login({ email, password });

if (response.type === 'success') {
setAccessToken(response.data.accessToken);
}
} catch (e) {
if (isAxiosError(e) && e.response && 'error' in e.response.data) {
if (e.response.status === 400) {
setLoginFailed({
status: 'INVALID',
message: e.response.data.error.messages.shift(),
});
} else if (e.response.status === 401) {
setLoginFailed({
status: 'INVALID',
message: e.response.data.error.message,
});
}
}
throw e;
}
};
}

export function useSignInForm() {
const [email, setEmail] = useState('');

const [password, setPassword] = useState('');

const [isLoginFailed, setIsLoginFailed] = useState(false);

return {
email,
setEmail,
password,
setPassword,
isLoginFailed,
setIsLoginFailed,
isLoginEnabled,
loginFailed,
handleLogin,
};
}
Loading

0 comments on commit f92aadc

Please sign in to comment.