-
Notifications
You must be signed in to change notification settings - Fork 0
[♻️ Refactor] user 코드 개선 - 지현 #4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
cb09b7b
05e2ee3
0e73802
3172d25
2deaa6a
738443a
a436f0a
f06af0d
3e454ce
566ab43
19393b3
de83283
1f9c6d3
f76c90e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| { | ||
| "extends": ["next/core-web-vitals", "next/typescript", "prettier"] | ||
| } |
| 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 |
| 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, | ||
| "bracketSpacing": true, | ||
| "arrowParens": "always", | ||
| "endOfLine": "lf", | ||
| "plugins": ["prettier-plugin-tailwindcss"] | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| @import "tailwindcss"; |
| 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> | ||
| ); | ||
| } |
| 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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 필드별로 각각 state를 작성하시는군요!
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 많지 않으면 필드별로 작성하려고 합니다! |
||
| 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; | ||
| 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"> | ||
| 닉네임 : | ||
| <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; |
| 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; |
| 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); | ||
| } | ||
| } |
| 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; |
| 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; | ||
| }, | ||
| }; |
| 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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 };
};
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 말씀하신대로 없어도 되는 state입니다. |
||
|
|
||
| checkAuth(); | ||
| }, [router, redirectTo]); | ||
|
|
||
| return { isAuthenticated, isLoading }; | ||
| }; | ||
| 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; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 여기서 던진 에러는 어디에서 사용되나요??!
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| } finally { | ||
| setIsLoading(false); | ||
| } | ||
| }; | ||
|
|
||
| return { login, isLoading, error }; | ||
| }; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
오 지현님도 true 파신가요??!!
저도
>태그 아래로 떨어지는거 싫어해서 꼭 넣는 편입니다ㅋㅋㅋㅋㅋThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
저도 아름님처럼 똑같이 싫어서 넣었습니다ㅎㅎ