Skip to content
Open
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
3 changes: 3 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": ["next/core-web-vitals", "next/typescript", "prettier"]
}
41 changes: 41 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# env files (can opt-in for committing if needed)
.env*

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
12 changes: 12 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"trailingComma": "es5",
"printWidth": 80,
"tabWidth": 2,
"semi": true,
"singleQuote": true,
"bracketSameLine": true,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 지현님도 true 파신가요??!!
저도 >태그 아래로 떨어지는거 싫어해서 꼭 넣는 편입니다ㅋㅋㅋㅋㅋ

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저도 아름님처럼 똑같이 싫어서 넣었습니다ㅎㅎ

"bracketSpacing": true,
"arrowParens": "always",
"endOfLine": "lf",
"plugins": ["prettier-plugin-tailwindcss"]
}
Binary file added app/favicon.ico
Binary file not shown.
1 change: 1 addition & 0 deletions app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@import "tailwindcss";
21 changes: 21 additions & 0 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { Metadata } from 'next';
import { ToastContainer } from 'react-toastify';
import './globals.css';

export const metadata: Metadata = {
title: 'Refactoring',
description: '안티 패턴 분리하기',
};

export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="ko">
<ToastContainer />
<body>{children}</body>
</html>
);
}
95 changes: 95 additions & 0 deletions app/login/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
'use client';

import { useState } from 'react';
import { useLogin } from '@/features/auth/hooks/useLogin';
import Head from 'next/head';

const LoginPage = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const { login, isLoading, error } = useLogin();

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();

if (!email || !password) {
Comment on lines +8 to +15
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

필드별로 각각 state를 작성하시는군요!
필드가 많아져도(닉네임, 이메일, 비밀번호, 비밀번호 확인...등등등.....) 각각 작성하시는 편이신가요?!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

많지 않으면 필드별로 작성하려고 합니다!
로그인 페이지는 이메일, 회원가입 2개의 필드만 있어서 각 필드별로 state를 작성했는데,
회원가입 페이지처럼 3개 이상이라면 객체 형태로 묶어서 state를 관리하는 방법을 고려할 것 같습니다!

return;
}

try {
await login({ email, password });
} catch {
// 에러는 useLogin에서 처리됨
}
};

return (
<>
<Head>
<title>로그인</title>
</Head>
<div className="flex min-h-screen items-center justify-center bg-gray-50">
<div className="w-full max-w-md space-y-8 rounded-lg border bg-white p-8">
<div>
<h2 className="text-center text-3xl font-bold text-gray-900">
로그인
</h2>
</div>

<form onSubmit={handleSubmit} className="mt-8 space-y-6">
<div className="space-y-4">
<div>
<label
htmlFor="email"
className="block text-sm font-medium text-gray-700">
이메일
</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="[email protected]"
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 focus:border-blue-500 focus:ring-blue-500 focus:outline-none"
required
/>
</div>

<div>
<label
htmlFor="password"
className="block text-sm font-medium text-gray-700">
비밀번호
</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 focus:border-blue-500 focus:ring-blue-500 focus:outline-none"
required
/>
</div>
</div>

{error && (
<div className="rounded bg-red-50 p-3 text-center text-sm text-red-600">
{error}
</div>
)}

<button
type="submit"
disabled={isLoading}
className="flex w-full justify-center rounded-md border border-transparent bg-blue-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-blue-700 focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50">
{isLoading ? '로그인 중...' : '로그인'}
</button>
</form>
</div>
</div>
</>
);
};

export default LoginPage;
61 changes: 61 additions & 0 deletions app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
'use client';

import { useAuth } from '@/features/auth/hooks/useAuth';
import { useAuthStore } from '@/features/auth/store/authStore';
import { authService } from '@/features/auth/services/authService';
import { useRouter } from 'next/navigation';

const Home = () => {
const router = useRouter();
const { isLoading } = useAuth();
const user = useAuthStore((state) => state.user);
const clearUser = useAuthStore((state) => state.clearUser);

const handleLogout = () => {
// 1. 클라이언트 측 토큰 삭제
authService.clearAuth();

// 2. Zustand 사용자 정보 삭제
clearUser();

// 3. 로그인 페이지로 이동
router.push('/login');
};

if (isLoading) {
return (
<div className="flex min-h-screen items-center justify-center">
<div className="text-gray-600">인증 확인 중...</div>
</div>
);
}

return (
<div className="min-h-screen bg-gray-50">
<div className="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
<div className="rounded-lg border bg-white p-8">
<div className="mb-8 flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-gray-900">메인 페이지</h1>
{user && (
<p className="mt-2 text-lg text-gray-600">
닉네임 :&nbsp;
<span className="font-semibold text-blue-600">
{user.name}
</span>
</p>
)}
</div>
<button
onClick={handleLogout}
className="rounded-md bg-red-600 px-4 py-2 text-white hover:bg-red-700 focus:ring-2 focus:ring-red-500 focus:outline-none">
로그아웃
</button>
</div>
</div>
</div>
</div>
);
};

export default Home;
4 changes: 4 additions & 0 deletions common/constants/storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const STORAGE_KEYS = {
ACCESS_TOKEN: 'accessToken',
REFRESH_TOKEN: 'refreshToken',
} as const;
22 changes: 22 additions & 0 deletions common/errors/AppError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export class AppError extends Error {
constructor(
public code: string,
public message: string,
public status: number = 500
) {
super(message);
this.name = 'AppError';
}

static unauthorized(message = '인증이 필요합니다') {
return new AppError('UNAUTHORIZED', message, 401);
}

static badRequest(message = '잘못된 요청입니다') {
return new AppError('BAD_REQUEST', message, 400);
}

static serverError(message = '서버 오류가 발생했습니다') {
return new AppError('SERVER_ERROR', message, 500);
}
}
18 changes: 18 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";

const eslintConfig = defineConfig([
...nextVitals,
...nextTs,
// Override default ignores of eslint-config-next.
globalIgnores([
// Default ignores of eslint-config-next:
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
]),
]);

export default eslintConfig;
21 changes: 21 additions & 0 deletions features/auth/api/authApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { apiWrapper } from '@/shared/api/client';
import type {
LoginRequest,
LoginResponse,
RefreshTokenResponse,
} from '../types/auth.types';

export const authApi = {
login: async (data: LoginRequest) => {
const response = await apiWrapper.post<LoginResponse>('/auth/signIn', data);
return response;
},

refreshToken: async (refreshToken: string) => {
const response = await apiWrapper.post<RefreshTokenResponse>(
'/auth/refresh-token',
{ refreshToken }
);
return response;
},
};
28 changes: 28 additions & 0 deletions features/auth/hooks/useAuth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
'use client';

import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { authService } from '../services/authService';

export const useAuth = (redirectTo: string = '/login') => {
const router = useRouter();
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [isLoading, setIsLoading] = useState(true);

useEffect(() => {
const checkAuth = () => {
const authenticated = authService.isAuthenticated();
setIsAuthenticated(authenticated);

if (!authenticated) {
router.push(redirectTo);
}

setIsLoading(false);
};
Comment on lines +9 to +22
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

제가 생각했을 때는 isAuthenticated라는 state가 없어도 되지 않을까? 싶은데요!
혹시 추가하신 이유가 있으실까요?!

저는 이렇게 생각했습니다
아래 코드처럼 별도로 상태를 두지 않아도 리다이렉트를 구현할 수 있지 않을까 싶었습니다!!
혹시 제가 놓친 부분이 있다면 말씀해주세요!!!

'use client';

import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { authService } from '../services/authService';

export const useAuth = (redirectTo: string = '/login') => {
  const router = useRouter();
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    const authenticated = authService.isAuthenticated();

    if (!authenticated) {
      router.push(redirectTo);
      return;
    }

    setIsLoading(false);
  }, [router, redirectTo]);

  return { isLoading };
};

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

말씀하신대로 없어도 되는 state입니다.
처음에 코드를 작성하면서 혹시 나중에 사용할까 싶어서 추가한 state인데 결국에는 사용하지 않아서 불필요한 state가 되어버렸습니다...
제가 지웠어야 했는데 미쳐 확인을 못하고 그냥 남겨 두었어요ㅎㅎ


checkAuth();
}, [router, redirectTo]);

return { isAuthenticated, isLoading };
};
42 changes: 42 additions & 0 deletions features/auth/hooks/useLogin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
'use client';

import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { authApi } from '../api/authApi';
import { authService } from '../services/authService';
import { useAuthStore } from '../store/authStore';
import type { LoginRequest } from '../types/auth.types';

export const useLogin = () => {
const router = useRouter();
const setUser = useAuthStore((state) => state.setUser);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);

const login = async (data: LoginRequest) => {
setIsLoading(true);
setError(null);

try {
const response = await authApi.login(data);

// 1. 토큰 저장 (access + refresh)
authService.saveTokens(response.accessToken, response.refreshToken);

// 2. 사용자 정보 Zustand에 저장
setUser(response.user);

// 3. 로그인 성공 후 메인 페이지로 이동
router.push('/');
} catch (err) {
const message =
err instanceof Error ? err.message : '로그인에 실패했습니다';
setError(message);
throw err;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기서 던진 에러는 어디에서 사용되나요??!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useLogin 훅에서 에러를 상태로 처리한 후 다시 throw하고는 있지만, login/page.tsx의 catch 블록에서 하는 처리가 없어서 실질적으로는 에러를 무시하고 있습니다.
원래 코드를 작성할 때는 login/page.tsx의 catch 블록에서 처리하게 하려고 했는데 작성하다보니까 useLogin 훅에서 처리를 해버려서 의미없는 코드가 되어버렸어요....ㅎㅎ😓

} finally {
setIsLoading(false);
}
};

return { login, isLoading, error };
};
Loading