Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
8 changes: 8 additions & 0 deletions next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,13 @@ const nextConfig = {
},
];
},
async rewrites() {
return [
{
source: '/:path*',
destination: `${process.env.NEXT_PUBLIC_API_BASE_URL}/:path*`,
},
];
},
};
export default nextConfig;
12 changes: 9 additions & 3 deletions src/_apis/auth/auth-apis.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,33 @@ import { fetchApi } from '@/src/utils/api';
import { LoginRequest, LoginResponse, SignupRequest, SignupResponse, User } from '@/src/types/auth';

export function signupUser(data: SignupRequest): Promise<{ data: SignupResponse }> {
return fetchApi<{ data: SignupResponse }>('/signup', {
return fetchApi<{ data: SignupResponse; headers: Headers }>('/auths/signup', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
}).then((response) => {
const token = response.headers.get('Authorization');
return { data: { token } };
Comment on lines +5 to +13
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

토큰 μΆ”μΆœ μ‹œ 였λ₯˜ 처리 둜직이 ν•„μš”ν•©λ‹ˆλ‹€

ν˜„μž¬ κ΅¬ν˜„μ—μ„œλŠ” λ‹€μŒκ³Ό 같은 잠재적인 λ¬Έμ œκ°€ μžˆμŠ΅λ‹ˆλ‹€:

  1. Authorization 헀더가 μ—†λŠ” κ²½μš°μ— λŒ€ν•œ μ²˜λ¦¬κ°€ μ—†μŠ΅λ‹ˆλ‹€
  2. 응닡 λ°μ΄ν„°μ˜ μœ νš¨μ„± 검증이 μ—†μŠ΅λ‹ˆλ‹€

λ‹€μŒκ³Ό 같이 κ°œμ„ ν•˜λŠ” 것을 μ œμ•ˆν•©λ‹ˆλ‹€:

  }).then((response) => {
    const token = response.headers.get('Authorization');
+   if (!token) {
+     throw new Error('인증 토큰이 μ—†μŠ΅λ‹ˆλ‹€');
+   }
    return { data: { token } };
  });
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return fetchApi<{ data: SignupResponse; headers: Headers }>('/auths/signup', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
}).then((response) => {
const token = response.headers.get('Authorization');
return { data: { token } };
return fetchApi<{ data: SignupResponse; headers: Headers }>('/auths/signup', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
}).then((response) => {
const token = response.headers.get('Authorization');
if (!token) {
throw new Error('인증 토큰이 μ—†μŠ΅λ‹ˆλ‹€');
}
return { data: { token } };

});
}

export function loginUser(data: LoginRequest): Promise<{ data: LoginResponse }> {
return fetchApi<{ data: LoginResponse }>('/login', {
return fetchApi<{ data: LoginResponse; headers: Headers }>('/auths/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
}).then((response) => {
const token = response.headers.get('Authorization');
return { data: { token } };
Comment on lines +18 to +26
Copy link

Choose a reason for hiding this comment

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

πŸ› οΈ Refactor suggestion

μ½”λ“œ 쀑볡을 μ œκ±°ν•˜κ³  였λ₯˜ 처리λ₯Ό κ°œμ„ ν•΄μ•Ό ν•©λ‹ˆλ‹€

loginUser와 signupUser ν•¨μˆ˜κ°€ 맀우 μœ μ‚¬ν•œ λ‘œμ§μ„ κ°€μ§€κ³  μžˆμŠ΅λ‹ˆλ‹€. λ˜ν•œ 토큰 μΆ”μΆœ μ‹œ 였λ₯˜ μ²˜λ¦¬κ°€ λˆ„λ½λ˜μ–΄ μžˆμŠ΅λ‹ˆλ‹€.

λ‹€μŒκ³Ό 같은 κ°œμ„ μ„ μ œμ•ˆν•©λ‹ˆλ‹€:

  1. 토큰 μΆ”μΆœ λ‘œμ§μ„ 곡톡 ν•¨μˆ˜λ‘œ 뢄리:
const extractAuthToken = (headers: Headers) => {
  const token = headers.get('Authorization');
  if (!token) {
    throw new Error('인증 토큰이 μ—†μŠ΅λ‹ˆλ‹€');
  }
  return token;
};
  1. 각 ν•¨μˆ˜μ—μ„œ 곡톡 ν•¨μˆ˜ μ‚¬μš©:
  }).then((response) => {
-   const token = response.headers.get('Authorization');
+   const token = extractAuthToken(response.headers);
    return { data: { token } };
  });

});
}

export function getUser(): Promise<{ data: User }> {
return fetchApi<{ data: User }>('/user/1', {
return fetchApi<{ data: User }>('/auths/user', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Expand Down
20 changes: 11 additions & 9 deletions src/_queries/auth/auth-queries.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,38 @@
import { ApiError } from 'next/dist/server/api-utils';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { getUser, loginUser, signupUser } from '@/src/_apis/auth/auth-apis';
import { useAuthStore } from '@/src/store/use-auth-store';
import { ApiError } from '@/src/utils/api';
import { transformKeysToCamel } from '@/src/utils/transform-keys';
import { LoginRequest, LoginResponse, SignupRequest, SignupResponse, User } from '@/src/types/auth';

export function usePostSignupQuery() {
const queryClient = useQueryClient();
const { login } = useAuthStore();
const { login, setUser } = useAuthStore();

return useMutation<{ data: SignupResponse }, ApiError, SignupRequest>({
mutationFn: signupUser,
onSuccess: async (response) => {
// TODO: tokenκ°’μœΌλ‘œ μˆ˜μ •, const { token } = response.data;
const token = 'dummyToken123';
const token = response.data.token?.replace(/^Bearer\s/, '');
if (token) await login(token);

const user: User = await queryClient.fetchQuery(getUserQuery());
login(user, token);
setUser(user);
},
});
}

export function usePostLoginQuery() {
const queryClient = useQueryClient();
const { login } = useAuthStore();
const { login, setUser } = useAuthStore();
Copy link

Choose a reason for hiding this comment

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

πŸ› οΈ Refactor suggestion

인증 둜직 쀑볡 κ°œμ„ μ΄ ν•„μš”ν•©λ‹ˆλ‹€.

둜그인과 νšŒμ›κ°€μž…μ—μ„œ λ™μΌν•œ 토큰 처리 및 μ‚¬μš©μž μƒνƒœ 관리 둜직이 λ°˜λ³΅λ©λ‹ˆλ‹€.

λ‹€μŒκ³Ό 같이 곡톡 λ‘œμ§μ„ λΆ„λ¦¬ν•˜λŠ” 것을 μ œμ•ˆλ“œλ¦½λ‹ˆλ‹€:

// 곡톡 μœ ν‹Έλ¦¬ν‹° ν•¨μˆ˜
async function handleAuthSuccess(
  response: { data: { token?: string } },
  queryClient: QueryClient,
  login: (token: string) => Promise<void>,
  setUser: (user: User) => void
) {
  const token = response.data.token?.replace(/^Bearer\s/, '');
  if (!token) {
    throw new Error('토큰이 μ—†μŠ΅λ‹ˆλ‹€');
  }

  try {
    await login(token);
    const user: User = await queryClient.fetchQuery(getUserQuery());
    setUser(user);
  } catch (error) {
    console.error('μ‚¬μš©μž μƒνƒœ μ—…λ°μ΄νŠΈ μ‹€νŒ¨:', error);
    throw error;
  }
}

// μ‚¬μš© μ˜ˆμ‹œ
onSuccess: (response) => handleAuthSuccess(response, queryClient, login, setUser)

Also applies to: 31-35


return useMutation<{ data: LoginResponse }, ApiError, LoginRequest>({
mutationFn: loginUser,
onSuccess: async (response) => {
// TODO: tokenκ°’μœΌλ‘œ μˆ˜μ •, const { token } = response.data;
const token = 'dummyToken123';
const token = response.data.token?.replace(/^Bearer\s/, '');
if (token) await login(token);

const user: User = await queryClient.fetchQuery(getUserQuery());
login(user, token);
setUser(user);
},
});
}
Expand Down
16 changes: 7 additions & 9 deletions src/app/(auth)/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,13 @@ export default function LoginPage() {
router.push('/');
},
onError: (error) => {
if (error.statusCode === 404) {
setError('email', {
type: 'manual',
message: '이메일 계정이 μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€',
});
} else if (error.statusCode === 401) {
setError('password', {
type: 'manual',
message: '잘λͺ»λœ λΉ„λ°€λ²ˆν˜Έμž…λ‹ˆλ‹€',
if (error.status === 401) {
const { validationErrors } = error.detail;
Object.keys(validationErrors).forEach((key) => {
setError(key as 'email' | 'password', {
type: 'manual',
message: validationErrors[key],
});
Copy link

Choose a reason for hiding this comment

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

πŸ› οΈ Refactor suggestion

μ—λŸ¬ 처리 κ΅¬ν˜„μ— λŒ€ν•œ κ°œμ„  μ œμ•ˆ

ν˜„μž¬ κ΅¬ν˜„μ€ λ‹€μŒκ³Ό 같은 잠재적인 λ¬Έμ œκ°€ μžˆμŠ΅λ‹ˆλ‹€:

  1. error.detail이 μ‘΄μž¬ν•˜μ§€ μ•Šμ„ 경우 λŸ°νƒ€μž„ μ—λŸ¬κ°€ λ°œμƒν•  수 μžˆμŠ΅λ‹ˆλ‹€
  2. validationErrorsκ°€ μ—†λŠ” κ²½μš°μ— λŒ€ν•œ μ²˜λ¦¬κ°€ λˆ„λ½λ˜μ—ˆμŠ΅λ‹ˆλ‹€
  3. μ˜ˆμƒμΉ˜ λͺ»ν•œ ν•„λ“œλͺ…이 λ“€μ–΄μ˜¬ 경우 νƒ€μž… μ•ˆμ •μ„±μ΄ 보μž₯λ˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€

λ‹€μŒκ³Ό 같이 κ°œμ„ ν•˜λŠ” 것을 μ œμ•ˆλ“œλ¦½λ‹ˆλ‹€:

  if (error.status === 401) {
-   const { validationErrors } = error.detail;
-   Object.keys(validationErrors).forEach((key) => {
-     setError(key as 'email' | 'password', {
-       type: 'manual',
-       message: validationErrors[key],
-     });
-   });
+   const validationErrors = error.detail?.validationErrors;
+   if (validationErrors) {
+     (Object.entries(validationErrors) as [keyof LoginFormValues, string][]).forEach(
+       ([field, message]) => {
+         setError(field, {
+           type: 'manual',
+           message,
+         });
+       }
+     );
+   } else {
+     setError('password', {
+       type: 'manual',
+       message: 'λ‘œκ·ΈμΈμ— μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€. λ‹€μ‹œ μ‹œλ„ν•΄ μ£Όμ„Έμš”.',
+     });
+   }
  }
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (error.status === 401) {
const { validationErrors } = error.detail;
Object.keys(validationErrors).forEach((key) => {
setError(key as 'email' | 'password', {
type: 'manual',
message: validationErrors[key],
});
if (error.status === 401) {
const validationErrors = error.detail?.validationErrors;
if (validationErrors) {
(Object.entries(validationErrors) as [keyof LoginFormValues, string][]).forEach(
([field, message]) => {
setError(field, {
type: 'manual',
message,
});
}
);
} else {
setError('password', {
type: 'manual',
message: 'λ‘œκ·ΈμΈμ— μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€. λ‹€μ‹œ μ‹œλ„ν•΄ μ£Όμ„Έμš”.',
});
}
}

});
}
},
Expand Down
19 changes: 11 additions & 8 deletions src/app/(auth)/signup/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,21 @@ export default function LoginPage() {
const { mutate: postSignup } = usePostSignupQuery();

const handleSubmit = async (data: SignupFormValues) => {
postSignup(data, {
const { confirmPassword, ...requestData } = data;

postSignup(requestData, {
onSuccess: () => {
router.push('/');
},
onError: (error) => {
if (error.statusCode === 400) {
// TODO: parameter 처리 ν›„ message 처리 확인
// const { parameter } = error.parameter;
// setError(parameter, {
// type: 'manual',
// message: error.message,
// });
if (error.status === 400) {
const { validationErrors } = error.detail;
Object.keys(validationErrors).forEach((key) => {
setError(key as 'email', {
type: 'manual',
message: validationErrors[key],
});
});
Comment on lines +24 to +31
Copy link

Choose a reason for hiding this comment

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

πŸ› οΈ Refactor suggestion

μ—λŸ¬ 처리 둜직 κ°œμ„ μ΄ ν•„μš”ν•©λ‹ˆλ‹€

ν˜„μž¬ κ΅¬ν˜„λœ μ—λŸ¬ μ²˜λ¦¬μ— λͺ‡ κ°€μ§€ κ°œμ„ μ΄ ν•„μš”ν•©λ‹ˆλ‹€:

  1. νƒ€μž… μ•ˆμ „μ„±: key as 'email' νƒ€μž… 단언은 μœ„ν—˜ν•  수 μžˆμŠ΅λ‹ˆλ‹€
  2. μ—λŸ¬ λ©”μ‹œμ§€ 처리: μ„œλ²„ 응닡이 μ˜ˆμƒκ³Ό λ‹€λ₯Ό 경우의 μ²˜λ¦¬κ°€ μ—†μŠ΅λ‹ˆλ‹€
  3. μ‚¬μš©μž ν”Όλ“œλ°±: 일반적인 μ„œλ²„ 였λ₯˜μ— λŒ€ν•œ μ²˜λ¦¬κ°€ λˆ„λ½λ˜μ—ˆμŠ΅λ‹ˆλ‹€

λ‹€μŒκ³Ό 같이 κ°œμ„ ν•˜λŠ” 것을 μ œμ•ˆν•©λ‹ˆλ‹€:

 if (error.status === 400) {
   const { validationErrors } = error.detail;
-  Object.keys(validationErrors).forEach((key) => {
-    setError(key as 'email', {
-      type: 'manual',
-      message: validationErrors[key],
-    });
-  });
+  try {
+    Object.entries(validationErrors).forEach(([key, message]) => {
+      if (key in formMethods.getValues()) {
+        setError(key as keyof SignupFormValues, {
+          type: 'manual',
+          message: String(message),
+        });
+      }
+    });
+  } catch (e) {
+    console.error('μœ νš¨μ„± 검사 μ—λŸ¬ 처리 쀑 λ¬Έμ œκ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€:', e);
+    setError('root', {
+      type: 'manual',
+      message: 'νšŒμ›κ°€μž… 처리 쀑 λ¬Έμ œκ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€. λ‹€μ‹œ μ‹œλ„ν•΄ μ£Όμ„Έμš”.',
+    });
+  }
+} else {
+  setError('root', {
+    type: 'manual',
+    message: 'μ„œλ²„ 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€. μž μ‹œ ν›„ λ‹€μ‹œ μ‹œλ„ν•΄ μ£Όμ„Έμš”.',
+  });
 }
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (error.status === 400) {
const { validationErrors } = error.detail;
Object.keys(validationErrors).forEach((key) => {
setError(key as 'email', {
type: 'manual',
message: validationErrors[key],
});
});
if (error.status === 400) {
const { validationErrors } = error.detail;
try {
Object.entries(validationErrors).forEach(([key, message]) => {
if (key in formMethods.getValues()) {
setError(key as keyof SignupFormValues, {
type: 'manual',
message: String(message),
});
}
});
} catch (e) {
console.error('μœ νš¨μ„± 검사 μ—λŸ¬ 처리 쀑 λ¬Έμ œκ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€:', e);
setError('root', {
type: 'manual',
message: 'νšŒμ›κ°€μž… 처리 쀑 λ¬Έμ œκ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€. λ‹€μ‹œ μ‹œλ„ν•΄ μ£Όμ„Έμš”.',
});
}
} else {
setError('root', {
type: 'manual',
message: 'μ„œλ²„ 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€. μž μ‹œ ν›„ λ‹€μ‹œ μ‹œλ„ν•΄ μ£Όμ„Έμš”.',
});
}

}
},
});
Expand Down
7 changes: 4 additions & 3 deletions src/store/use-auth-store.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ interface AuthState {
isAuth: boolean;
user: User | null;
token: string | null;
login: (userData: User, token: string) => void;
login: (token: string) => void;
logout: () => void;
setUser: (user: User) => void;
}

export const useAuthStore = create<AuthState>()(
Expand All @@ -16,10 +17,9 @@ export const useAuthStore = create<AuthState>()(
isAuth: false,
user: null,
token: null,
login: (userData, token) =>
login: (token) =>
set({
isAuth: true,
user: userData,
token,
}),
logout: () =>
Expand All @@ -28,6 +28,7 @@ export const useAuthStore = create<AuthState>()(
user: null,
token: null,
}),
setUser: (user: User) => set((state) => ({ ...state, user })),
}),
{
name: 'auth-storage',
Expand Down
4 changes: 2 additions & 2 deletions src/types/auth.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export interface SignupResponse {
token: string;
token: string | null;
}

export interface SignupRequest {
Expand All @@ -9,7 +9,7 @@ export interface SignupRequest {
}

export interface LoginResponse {
token: string;
token: string | null;
}

export interface LoginRequest {
Expand Down
22 changes: 12 additions & 10 deletions src/utils/api.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { useAuthStore } from '@/src/store/use-auth-store';

// TODO: μΆ”ν›„ API URL μˆ˜μ •
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3009';

export class ApiError extends Error {
detail: { validationErrors: Record<string, string> } = { validationErrors: {} };

constructor(
public status: number,
message: string,
detail?: { validationErrors: Record<string, string> }, // νƒ€μž…μ„ 맞좀
) {
super(message);
this.name = 'ApiError';
if (detail) this.detail = detail;
}
}

Expand All @@ -23,7 +24,6 @@ export async function fetchApi<T>(
const { signal } = controller;
const id = setTimeout(() => controller.abort(), timeout);
const { token } = useAuthStore.getState();

const fetchOptions: RequestInit = {
...options,
signal,
Expand All @@ -35,21 +35,23 @@ export async function fetchApi<T>(
};

try {
const response = await fetch(`${API_BASE_URL}${url}`, fetchOptions); // API μš”μ²­ μ‹€ν–‰
const response = await fetch(`${url}`, fetchOptions); // API μš”μ²­ μ‹€ν–‰
if (!response.ok) {
let errorDetail;
let errorMessage;
try {
const errorData = await response.json();
errorMessage = errorData.message || `HTTP error! status: ${response.status}`;
const { status, message, ...detail } = await response.json();
errorMessage = message || `HTTP error! status: ${response.status}`;
errorDetail = detail;
} catch {
errorMessage = `HTTP error! status: ${response.status}`;
}

throw new ApiError(response.status, errorMessage);
throw new ApiError(response.status, errorMessage, errorDetail);
}

// 응닡 데이터λ₯Ό JSON ν˜•νƒœλ‘œ λ°˜ν™˜
return (await response.json()) as T;
const data = await response.json();
return { ...data, headers: response.headers } as T;
} catch (error) {
if (error instanceof Error) {
if (error.name === 'AbortError') throw new ApiError(408, 'Request timeout');
Expand Down