Skip to content
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

πŸ”€ 역할에 λ”°λ₯Έ url 처리 진행 #77

Merged
merged 4 commits into from
Jan 16, 2025
Merged
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
32 changes: 32 additions & 0 deletions src/app/api/admin/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,35 @@ export async function GET() {
return NextResponse.json({ error: message }, { status });
}
}

export async function DELETE() {
const cookieStore = cookies();
const accessToken = cookieStore.get('accessToken')?.value;

if (!accessToken) {
return NextResponse.json(
{ error: 'Access token not found' },
{ status: 401 },
);
}

try {
await apiClient.delete('/admin', {
headers: {
Authorization: `Bearer ${accessToken}`,
},
});

const response = NextResponse.json({ success: true });
response.cookies.set('accessToken', '', { maxAge: 0 });
response.cookies.set('refreshToken', '', { maxAge: 0 });
return response;
} catch (error) {
if (error instanceof AxiosError) {
const status = error.response?.status || 500;
const message =
error.response?.data?.message || 'delete user account failed';
return NextResponse.json({ error: message }, { status });
}
}
}
Comment on lines +28 to +58
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

catch λΈ”λ‘μ—μ„œ κΈ°λ³Έ 응닡이 λˆ„λ½λ˜μ—ˆμŠ΅λ‹ˆλ‹€.

errorκ°€ AxiosError μΈμŠ€ν„΄μŠ€κ°€ μ•„λ‹Œ κ²½μš°μ— λŒ€ν•œ 응닡이 λˆ„λ½λ˜μ–΄ μžˆμŠ΅λ‹ˆλ‹€. μ΄λŠ” 예기치 μ•Šμ€ 였λ₯˜κ°€ λ°œμƒν•  수 μžˆμŠ΅λ‹ˆλ‹€.

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

  } catch (error) {
    if (error instanceof AxiosError) {
      const status = error.response?.status || 500;
      const message =
        error.response?.data?.message || 'delete user account failed';
      return NextResponse.json({ error: message }, { status });
    }
+   return NextResponse.json(
+     { error: 'Internal server error' },
+     { status: 500 }
+   );
  }
πŸ“ 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
export async function DELETE() {
const cookieStore = cookies();
const accessToken = cookieStore.get('accessToken')?.value;
if (!accessToken) {
return NextResponse.json(
{ error: 'Access token not found' },
{ status: 401 },
);
}
try {
await apiClient.delete('/admin', {
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
const response = NextResponse.json({ success: true });
response.cookies.set('accessToken', '', { maxAge: 0 });
response.cookies.set('refreshToken', '', { maxAge: 0 });
return response;
} catch (error) {
if (error instanceof AxiosError) {
const status = error.response?.status || 500;
const message =
error.response?.data?.message || 'delete user account failed';
return NextResponse.json({ error: message }, { status });
}
}
}
export async function DELETE() {
const cookieStore = cookies();
const accessToken = cookieStore.get('accessToken')?.value;
if (!accessToken) {
return NextResponse.json(
{ error: 'Access token not found' },
{ status: 401 },
);
}
try {
await apiClient.delete('/admin', {
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
const response = NextResponse.json({ success: true });
response.cookies.set('accessToken', '', { maxAge: 0 });
response.cookies.set('refreshToken', '', { maxAge: 0 });
return response;
} catch (error) {
if (error instanceof AxiosError) {
const status = error.response?.status || 500;
const message =
error.response?.data?.message || 'delete user account failed';
return NextResponse.json({ error: message }, { status });
}
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}

6 changes: 6 additions & 0 deletions src/entities/admin/api/deleteUserAccount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import axios from 'axios';

export const deleteUserAccount = async () => {
const response = await axios.delete('/api/admin');
Copy link

Choose a reason for hiding this comment

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

πŸ› οΈ Refactor suggestion

μ—λŸ¬ 처리 둜직 μΆ”κ°€ ν•„μš”

ν˜„μž¬ deleteUserAccount ν•¨μˆ˜λŠ” μš”μ²­ μ‹€νŒ¨ μ‹œμ˜ 처리 둜직이 μ—†μŠ΅λ‹ˆλ‹€. μ„œλ²„λ‘œλΆ€ν„° μ—λŸ¬ 응닡을 λ°›λŠ” 경우λ₯Ό λŒ€λΉ„ν•˜μ—¬ μ˜ˆμ™Έ 처리λ₯Ό μΆ”κ°€ν•˜λŠ” 것이 μ’‹μŠ΅λ‹ˆλ‹€.

λ‹€μŒκ³Ό 같이 try-catch 블둝을 μ‚¬μš©ν•˜μ—¬ μ—λŸ¬λ₯Ό μ²˜λ¦¬ν•˜λ„λ‘ μˆ˜μ •ν•˜μ„Έμš”:

export const deleteUserAccount = async () => {
-   const response = await axios.delete('/api/admin');
-   return response;
+   try {
+     const response = await axios.delete('/api/admin');
+     return response;
+   } catch (error) {
+     // μ—λŸ¬ 처리 둜직 μΆ”κ°€
+     throw error;
+   }
};
πŸ“ 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
const response = await axios.delete('/api/admin');
export const deleteUserAccount = async () => {
try {
const response = await axios.delete('/api/admin');
return response;
} catch (error) {
// μ—λŸ¬ 처리 둜직 μΆ”κ°€
throw error;
}
};

return response;
};
19 changes: 19 additions & 0 deletions src/entities/admin/model/useDeleteUserAccount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useMutation } from '@tanstack/react-query';
import { useRouter } from 'next/navigation';
import { toast } from 'react-toastify';
import { deleteUserAccount } from '../api/deleteUserAccount';

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

return useMutation({
mutationFn: () => deleteUserAccount(),
onSuccess: () => {
toast.success('νƒˆν‡΄κ°€ μ™„λ£Œλ˜μ—ˆμŠ΅λ‹ˆλ‹€.');
router.push('/');
},
onError: () => {
toast.error('μœ μ € νƒˆν‡΄λ₯Ό μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.');
},
});
};
36 changes: 31 additions & 5 deletions src/entities/admin/ui/AdminProfile/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react';
import React, { useState } from 'react';
import { Logout } from '@/shared/assets/icons';
import { useDeleteUserAccount } from '../../model/useDeleteUserAccount';
import { useLogout } from '../../model/useLogout';

const ProfileInfo = ({ label, value }: { label: string; value: string }) => (
Expand All @@ -11,18 +12,43 @@ const ProfileInfo = ({ label, value }: { label: string; value: string }) => (

const AdminProfile = () => {
const { mutate: logout } = useLogout();
const { mutate: deleteAccount } = useDeleteUserAccount();
const [isToggleLogout, setIsToggleLogout] = useState(false);

const handleLogoutClick = () => {
setIsToggleLogout((prev) => !prev);
};

return (
<div className="flex w-full justify-between">
<div className="relative flex w-full justify-between">
<div className="flex items-center gap-[124px] mobile:flex-col mobile:gap-[30px]">
<div className="space-y-[32px]">
<ProfileInfo label="이름" value="김진원" />
<ProfileInfo label="아이디" value="jin1234" />
<ProfileInfo label="이메일" value="[email protected]" />
</div>
</div>
<label onClick={() => logout()}>
Comment on lines +23 to 30
Copy link

Choose a reason for hiding this comment

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

πŸ› οΈ Refactor suggestion

ν•˜λ“œμ½”λ”©λœ μ‚¬μš©μž 정보λ₯Ό propsλ‚˜ μƒνƒœλ‘œ κ΄€λ¦¬ν•˜λŠ” 것이 μ’‹μŠ΅λ‹ˆλ‹€.

ν˜„μž¬ μ‚¬μš©μž 정보가 ν•˜λ“œμ½”λ”©λ˜μ–΄ μžˆμŠ΅λ‹ˆλ‹€. μ΄λŠ” μœ μ§€λ³΄μˆ˜μ„±μ„ μ €ν•˜μ‹œν‚¬ 수 μžˆμŠ΅λ‹ˆλ‹€.

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

+ interface AdminProfileProps {
+   name: string;
+   username: string;
+   email: string;
+ }
- const AdminProfile = () => {
+ const AdminProfile = ({ name, username, email }: AdminProfileProps) => {
  return (
    <div className="relative flex w-full justify-between">
      <div className="flex items-center gap-[124px] mobile:flex-col mobile:gap-[30px]">
        <div className="space-y-[32px]">
-         <ProfileInfo label="이름" value="김진원" />
-         <ProfileInfo label="아이디" value="jin1234" />
-         <ProfileInfo label="이메일" value="[email protected]" />
+         <ProfileInfo label="이름" value={name} />
+         <ProfileInfo label="아이디" value={username} />
+         <ProfileInfo label="이메일" value={email} />
        </div>
      </div>
πŸ“ 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
<div className="relative flex w-full justify-between">
<div className="flex items-center gap-[124px] mobile:flex-col mobile:gap-[30px]">
<div className="space-y-[32px]">
<ProfileInfo label="이름" value="김진원" />
<ProfileInfo label="아이디" value="jin1234" />
<ProfileInfo label="이메일" value="[email protected]" />
</div>
</div>
interface AdminProfileProps {
name: string;
username: string;
email: string;
}
const AdminProfile = ({ name, username, email }: AdminProfileProps) => {
<div className="relative flex w-full justify-between">
<div className="flex items-center gap-[124px] mobile:flex-col mobile:gap-[30px]">
<div className="space-y-[32px]">
<ProfileInfo label="이름" value={name} />
<ProfileInfo label="아이디" value={username} />
<ProfileInfo label="이메일" value={email} />
</div>
</div>

<Logout />
</label>
<div>
<button onClick={handleLogoutClick}>
<Logout />
</button>
{isToggleLogout && (
<div className="absolute right-0 top-[30px] flex h-fit flex-col gap-2 rounded-[6px] border border-gray-200 bg-white p-2 shadow-[0px_4px_4px_0px_rgba(0,_0,_0,_0.25)]">
<button
onClick={() => logout()}
className="w-full rounded-[6px] px-5 py-2 text-body2 text-gray-500 hover:bg-error hover:text-white"
>
λ‘œκ·Έμ•„μ›ƒ
</button>
<button
onClick={() => deleteAccount()}
className="w-full rounded-[6px] px-5 py-2 text-body2 text-gray-500 hover:bg-error hover:text-white"
>
νšŒμ›νƒˆν‡΄
</button>
</div>
)}
</div>
</div>
Comment on lines +31 to +51
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. λ²„νŠΌμ— aria-label이 μ—†μŠ΅λ‹ˆλ‹€.
  2. νšŒμ›νƒˆν‡΄ μ‹œ 확인 λŒ€ν™”μƒμžκ°€ μ—†μŠ΅λ‹ˆλ‹€.
  3. λ“œλ‘­λ‹€μš΄μ΄ μ—΄λ €μžˆμ„ λ•Œ μ™ΈλΆ€ 클릭으둜 λ‹«νžˆμ§€ μ•ŠμŠ΅λ‹ˆλ‹€.

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

  <div>
    <button
+     aria-label="λ‘œκ·Έμ•„μ›ƒ 메뉴"
+     aria-expanded={isToggleLogout}
      onClick={handleLogoutClick}>
      <Logout />
    </button>
    {isToggleLogout && (
      <div className="absolute right-0 top-[30px] flex h-fit flex-col gap-2 rounded-[6px] border border-gray-200 bg-white p-2 shadow-[0px_4px_4px_0px_rgba(0,_0,_0,_0.25)]">
        <button
          onClick={() => logout()}
+         aria-label="λ‘œκ·Έμ•„μ›ƒ μ‹€ν–‰"
          className="w-full rounded-[6px] px-5 py-2 text-body2 text-gray-500 hover:bg-error hover:text-white"
        >
          λ‘œκ·Έμ•„μ›ƒ
        </button>
        <button
-         onClick={() => deleteAccount()}
+         onClick={() => {
+           if (window.confirm('μ •λ§λ‘œ νšŒμ›νƒˆν‡΄λ₯Ό μ§„ν–‰ν•˜μ‹œκ² μŠ΅λ‹ˆκΉŒ?')) {
+             deleteAccount();
+           }
+         }}
+         aria-label="νšŒμ›νƒˆν‡΄ μ‹€ν–‰"
          className="w-full rounded-[6px] px-5 py-2 text-body2 text-gray-500 hover:bg-error hover:text-white"
        >
          νšŒμ›νƒˆν‡΄
        </button>
      </div>
    )}
  </div>
πŸ“ 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
<div>
<button onClick={handleLogoutClick}>
<Logout />
</button>
{isToggleLogout && (
<div className="absolute right-0 top-[30px] flex h-fit flex-col gap-2 rounded-[6px] border border-gray-200 bg-white p-2 shadow-[0px_4px_4px_0px_rgba(0,_0,_0,_0.25)]">
<button
onClick={() => logout()}
className="w-full rounded-[6px] px-5 py-2 text-body2 text-gray-500 hover:bg-error hover:text-white"
>
λ‘œκ·Έμ•„μ›ƒ
</button>
<button
onClick={() => deleteAccount()}
className="w-full rounded-[6px] px-5 py-2 text-body2 text-gray-500 hover:bg-error hover:text-white"
>
νšŒμ›νƒˆν‡΄
</button>
</div>
)}
</div>
<div>
<button
aria-label="λ‘œκ·Έμ•„μ›ƒ 메뉴"
aria-expanded={isToggleLogout}
onClick={handleLogoutClick}>
<Logout />
</button>
{isToggleLogout && (
<div className="absolute right-0 top-[30px] flex h-fit flex-col gap-2 rounded-[6px] border border-gray-200 bg-white p-2 shadow-[0px_4px_4px_0px_rgba(0,_0,_0,_0.25)]">
<button
onClick={() => logout()}
aria-label="λ‘œκ·Έμ•„μ›ƒ μ‹€ν–‰"
className="w-full rounded-[6px] px-5 py-2 text-body2 text-gray-500 hover:bg-error hover:text-white"
>
λ‘œκ·Έμ•„μ›ƒ
</button>
<button
onClick={() => {
if (window.confirm('μ •λ§λ‘œ νšŒμ›νƒˆν‡΄λ₯Ό μ§„ν–‰ν•˜μ‹œκ² μŠ΅λ‹ˆκΉŒ?')) {
deleteAccount();
}
}}
aria-label="νšŒμ›νƒˆν‡΄ μ‹€ν–‰"
className="w-full rounded-[6px] px-5 py-2 text-body2 text-gray-500 hover:bg-error hover:text-white"
>
νšŒμ›νƒˆν‡΄
</button>
</div>
)}
</div>

);
};
Expand Down
65 changes: 56 additions & 9 deletions src/middleware.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
const accessToken = request.cookies.get('accessToken');
const MANAGE_RESTRICTED_PATHS = [
/^\/signIn$/,
/^\/signUp$/,
/^\/application\/.+\/(STANDARD|TRAINEE)$/,
];

const requestHeaders = new Headers(request.headers);
const USER_RESTRICTED_PATHS = [
/^\/admin$/,
/^\/create-exhibition$/,
/^\/expo-manage\/.+$/,
/^\/name-tag\/.+$/,
/^\/sms\/.+\/(STANDARD|TRAINEE)$/,
/^\/program(\/.*)?$/,
];

if (accessToken) {
requestHeaders.set('role', 'manage');
} else {
requestHeaders.set('role', 'user');
}
function handleApiRole(request: NextRequest): NextResponse {
const requestHeaders = new Headers(request.headers);
const role = request.cookies.get('accessToken') ? 'manage' : 'user';
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

κΆŒν•œ μ„€μ • 둜직의 λ³΄μ•ˆ κ°•ν™” ν•„μš”

ν˜„μž¬ accessToken의 쑴재 μ—¬λΆ€λ‘œλ§Œ role을 manage λ˜λŠ” user둜 μ„€μ •ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€. ν† ν°μ˜ μœ νš¨μ„±κ³Ό μ‹€μ œ μ‚¬μš©μžμ˜ μ—­ν• (예: κ΄€λ¦¬μž, 일반 μ‚¬μš©μž)을 κ²€μ¦ν•˜λŠ” 좔가적인 둜직이 ν•„μš”ν•©λ‹ˆλ‹€. μ΄λŠ” λ³΄μ•ˆ κ°•ν™”λ₯Ό μœ„ν•΄ μ€‘μš”ν•©λ‹ˆλ‹€.

accessToken을 κ²€μ¦ν•˜κ³  μ‚¬μš©μž 역할을 μ •ν™•ν•˜κ²Œ νŒλ‹¨ν•˜λŠ” λ‘œμ§μ„ μΆ”κ°€ν•˜μ„Έμš”. 예λ₯Ό λ“€μ–΄, 토큰을 ν•΄μ„ν•˜μ—¬ μ‚¬μš©μž 정보λ₯Ό μΆ”μΆœν•˜κ³ , ν•΄λ‹Ή μ‚¬μš©μžμ˜ κΆŒν•œμ— 따라 역할을 λΆ€μ—¬ν•˜λŠ” 방식을 κ³ λ €ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

requestHeaders.set('role', role);

return NextResponse.next({
request: {
Expand All @@ -19,6 +28,44 @@ export function middleware(request: NextRequest) {
});
}

function isPathMatch(pathname: string, patterns: RegExp[]): boolean {
return patterns.some((pattern) => pattern.test(pathname));
}

export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;

if (pathname === '/api/role') {
return handleApiRole(request);
}

const accessToken = request.cookies.get('accessToken');

if (!accessToken && isPathMatch(pathname, USER_RESTRICTED_PATHS)) {
return NextResponse.redirect(new URL('/', request.url));
}

if (accessToken && isPathMatch(pathname, MANAGE_RESTRICTED_PATHS)) {
return NextResponse.redirect(new URL('/', request.url));
}

return NextResponse.next();
}

export const config = {
matcher: ['/api/role'],
matcher: [
'/api/role',
'/signIn',
'/signUp',
'/admin',
'/create-exhibition',
'/expo-manage/:path*',
'/name-tag/:path*',
'/sms/:path*/STANDARD',
'/sms/:path*/TRAINEE',
'/program/:path*',
'/program/detail/:path*',
'/application/:path*/STANDARD',
'/application/:path*/TRAINEE',
],
};
Loading