Skip to content
Merged
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"jest-fixed-jsdom": "^0.0.11",
"js-cookie": "^3.0.5",
"motion": "^12.23.24",
"next": "16.0.7",
"react": "19.2.1",
Expand All @@ -76,6 +77,7 @@
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"@types/jest": "^30.0.0",
"@types/js-cookie": "^3.0.6",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
Expand Down
17 changes: 17 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 10 additions & 5 deletions src/api/service/auth-service/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,28 +10,33 @@ import {

export const authServiceRemote = () => ({
// 로그인
login: async (payload: LoginRequest): Promise<LoginResponse> => {
login: async (payload: LoginRequest) => {
const data = await api.post<LoginResponse>('/auth/login', payload);

setAccessToken(data.accessToken, data.expiresIn);
return data;
},

// 회원가입
signup: async (payload: SignupRequest): Promise<SignupResponse> =>
api.post<SignupResponse>(`/auth/signup`, payload),
signup: (payload: SignupRequest) => api.post<SignupResponse>(`/auth/signup`, payload),

// 로그아웃
logout: async (): Promise<void> => {
logout: async () => {
await api.post<void>('/auth/logout');
clearAccessToken();
},

// 액세스 토큰 재발급
refresh: async (): Promise<RefreshResponse> => {
refresh: async () => {
const data = await api.post<RefreshResponse>('/auth/refresh');

setAccessToken(data.accessToken, data.expiresIn);
return data;
},

// 회원 탈퇴
withdraw: async () => {
await api.delete<void>('/auth/withdraw');
clearAccessToken();
},
});
18 changes: 18 additions & 0 deletions src/app/login/_temp/login-temp-actions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
'use client';

import { MyPageActionButton } from '@/components/pages/user/mypage/mypage-setting-button';
import { useLogout, useWithdraw } from '@/hooks/use-auth';

const LoginTempActions = () => {
const logout = useLogout();
const withdraw = useWithdraw();

return (
<div className='flex-center'>
<MyPageActionButton onClick={logout}>로그아웃</MyPageActionButton>
<MyPageActionButton onClick={withdraw}>회원탈퇴</MyPageActionButton>
</div>
);
};

export default LoginTempActions;
16 changes: 15 additions & 1 deletion src/app/login/page.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,29 @@
import { cookies } from 'next/headers';

// import { redirect } from 'next/navigation';
import { Icon } from '@/components/icon';
import { LoginForm } from '@/components/pages/login';
import { AuthSwitch } from '@/components/shared';

const LoginPage = () => {
import LoginTempActions from './_temp/login-temp-actions';

const LoginPage = async () => {
const cookieStore = await cookies();
const accessToken = cookieStore.get('accessToken')?.value;

// if (accessToken) {
// redirect('/');
// }

return (
<div className='flex-col-center min-h-[calc(100dvh-113px)] gap-10 overflow-auto bg-gray-100 px-4 py-15'>
<div className='flex-col-center w-full gap-4'>
<Icon id='wego-logo' className='h-19.5 w-45' />
<LoginForm />
</div>
<AuthSwitch type='signup' />
{/* 📜 임시, 삭제 예정 */}
{accessToken && <LoginTempActions />}
</div>
);
};
Expand Down
29 changes: 8 additions & 21 deletions src/components/pages/login/login-form/index.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
'use client';

import { useRouter } from 'next/navigation';

import { type AnyFieldApi, useForm } from '@tanstack/react-form';

import { API } from '@/api';
import { FormInput } from '@/components/shared';
import { Button } from '@/components/ui';
import { useLogin } from '@/hooks/use-auth';
import { loginSchema } from '@/lib/schema/auth';
import { CommonErrorResponse } from '@/types/service/common';

const getHintMessage = (field: AnyFieldApi) => {
const {
Expand All @@ -23,7 +20,7 @@ const getHintMessage = (field: AnyFieldApi) => {
};

export const LoginForm = () => {
const router = useRouter();
const login = useLogin();

const form = useForm({
defaultValues: {
Expand All @@ -35,23 +32,12 @@ export const LoginForm = () => {
onChange: loginSchema,
},
onSubmit: async ({ value, formApi }) => {
try {
const payload = {
email: value.email,
password: value.password,
};

const result = await API.authService.login(payload);
console.log('login success:', result);

formApi.reset();
router.push('/');
} catch (error) {
const err = error as CommonErrorResponse;
const payload = {
email: value.email,
password: value.password,
};

console.error('[LOGIN ERROR]', err.errorCode, err.detail);
alert(err.detail || '로그인에 실패했습니다.');
}
await login(payload, formApi);
},
});

Expand Down Expand Up @@ -93,6 +79,7 @@ export const LoginForm = () => {
hintMessage={hintMessage}
inputProps={{
type: 'password',
autoComplete: 'current-password',
placeholder: '비밀번호를 입력해주세요',
value: field.state.value,
onChange: (e) => field.handleChange(e.target.value),
Expand Down
36 changes: 13 additions & 23 deletions src/components/pages/signup/signup-form/index.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
'use client';

import { useRouter } from 'next/navigation';

import { type AnyFieldApi, useForm } from '@tanstack/react-form';

import { API } from '@/api';
import { FormInput } from '@/components/shared';
import { Button } from '@/components/ui';
import { useSignup } from '@/hooks/use-auth';
import { signupSchema } from '@/lib/schema/auth';
import { CommonErrorResponse } from '@/types/service/common';

const getHintMessage = (field: AnyFieldApi) => {
const {
Expand All @@ -23,7 +20,7 @@ const getHintMessage = (field: AnyFieldApi) => {
};

export const SignupForm = () => {
const router = useRouter();
const signup = useSignup();

const form = useForm({
defaultValues: {
Expand All @@ -37,24 +34,13 @@ export const SignupForm = () => {
onSubmit: signupSchema,
},
onSubmit: async ({ value, formApi }) => {
try {
const payload = {
email: value.email,
password: value.password,
nickName: value.nickname,
};

const result = await API.authService.signup(payload);
console.log('signup success:', result);

formApi.reset();
router.push('/login');
} catch (error) {
const err = error as CommonErrorResponse;

console.error('[SIGNUP ERROR]', err.errorCode, err.detail);
alert(err.detail || '회원가입에 실패했습니다.');
}
const payload = {
email: value.email,
password: value.password,
nickName: value.nickname,
};

await signup(payload, formApi);
},
});

Expand All @@ -76,6 +62,7 @@ export const SignupForm = () => {
hintMessage={hintMessage}
inputProps={{
type: 'email',
autoComplete: 'email',
placeholder: '이메일을 입력해주세요',
value: field.state.value,
onChange: (e) => field.handleChange(e.target.value),
Expand All @@ -94,6 +81,7 @@ export const SignupForm = () => {
<FormInput
hintMessage={hintMessage}
inputProps={{
autoComplete: 'username',
placeholder: '닉네임을 입력해주세요',
value: field.state.value,
onChange: (e) => field.handleChange(e.target.value),
Expand All @@ -113,6 +101,7 @@ export const SignupForm = () => {
hintMessage={hintMessage}
inputProps={{
type: 'password',
autoComplete: 'new-password',
placeholder: '비밀번호를 입력해주세요',
value: field.state.value,
onChange: (e) => field.handleChange(e.target.value),
Expand All @@ -132,6 +121,7 @@ export const SignupForm = () => {
hintMessage={hintMessage}
inputProps={{
type: 'password',
autoComplete: 'new-password',
placeholder: '비밀번호를 한 번 더 입력해주세요',
value: field.state.value,
onChange: (e) => field.handleChange(e.target.value),
Expand Down
4 changes: 4 additions & 0 deletions src/hooks/use-auth/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export { useLogin } from './use-auth-login';
export { useLogout } from './use-auth-logout';
export { useSignup } from './use-auth-signup';
export { useWithdraw } from './use-auth-withdraw';
45 changes: 45 additions & 0 deletions src/hooks/use-auth/use-auth-login/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
'use client';

import { useRouter } from 'next/navigation';

import { AxiosError } from 'axios';
import Cookies from 'js-cookie';

import { API } from '@/api';
import { LoginRequest } from '@/types/service/auth';
import { CommonErrorResponse } from '@/types/service/common';

export const useLogin = () => {
const router = useRouter();

const handleLogin = async (payload: LoginRequest, formApi: { reset: () => void }) => {
try {
const result = await API.authService.login(payload);
// 📜 추후 삭제
console.log('login success:', result);

Cookies.set('userId', String(result.user.userId), {
path: '/',
sameSite: 'lax',
secure: process.env.NODE_ENV === 'production',
});

formApi.reset();
router.push('/');
} catch (error) {
const axiosError = error as AxiosError<CommonErrorResponse>;
const problem = axiosError.response?.data;

// 📜 에러 UI 결정나면 변경
if (problem) {
console.error('[LOGIN ERROR]', problem.errorCode, problem.detail);
alert(problem.detail || '로그인에 실패했습니다.');
} else {
console.error(error);
alert('알 수 없는 오류가 발생했습니다.');
}
}
};

return handleLogin;
};
32 changes: 32 additions & 0 deletions src/hooks/use-auth/use-auth-logout/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
'use client';

import { useRouter } from 'next/navigation';

import { useQueryClient } from '@tanstack/react-query';
import Cookies from 'js-cookie';

import { API } from '@/api';
import { userKeys } from '@/lib/query-key/query-key-user';

export const useLogout = () => {
const router = useRouter();
const queryClient = useQueryClient();

const handleLogout = async () => {
try {
await API.authService.logout();
} catch (error) {
console.error('[LOGOUT ERROR]', error);
} finally {
// 로그인 유저 관련 캐시 정리
queryClient.removeQueries({ queryKey: userKeys.all });
Cookies.remove('userId');

// 로컬 스토리지/추가 상태도 정리???

router.push('/login');
}
};

return handleLogout;
};
Loading