diff --git a/package.json b/package.json index 5a7ae6f..6041dda 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "next-pwa": "^5.6.0", "react": "^18", "react-dom": "^18", + "react-hot-toast": "^2.5.2", "react-router-dom": "^6.24.0", "shadcn-ui": "^0.9.4", "swiper": "^11.2.4", diff --git a/src/app/desktop/admin-inquiry/_components/AdminTable/index.tsx b/src/app/desktop/(nav-bar)/admin-inquiry/_components/AdminTable/index.tsx similarity index 100% rename from src/app/desktop/admin-inquiry/_components/AdminTable/index.tsx rename to src/app/desktop/(nav-bar)/admin-inquiry/_components/AdminTable/index.tsx diff --git a/src/app/desktop/admin-inquiry/page.tsx b/src/app/desktop/(nav-bar)/admin-inquiry/page.tsx similarity index 89% rename from src/app/desktop/admin-inquiry/page.tsx rename to src/app/desktop/(nav-bar)/admin-inquiry/page.tsx index 5ba4e73..a888b64 100644 --- a/src/app/desktop/admin-inquiry/page.tsx +++ b/src/app/desktop/(nav-bar)/admin-inquiry/page.tsx @@ -1,6 +1,6 @@ 'use client'; -import Sidebar from 'src/components/desktop/Sidebar'; +import Sidebar from '@/components/desktop/Sidebar'; import { useState, useEffect } from 'react'; import { Button } from '@/components/ui/button'; import { useMutation, useQuery } from '@tanstack/react-query'; @@ -13,9 +13,10 @@ import { import { Admins } from '@/types/admins'; import { SearchInput } from '@/components/ui/search-input'; import { PageChangeAction } from '@/types/paginationType'; +import toast from 'react-hot-toast'; import TableComponent from './_components/AdminTable'; -export default function PayerInquiryPage() { +export default function AdminInquiryPage() { const [isDeleteModeOriginal, setIsDeleteModeOriginal] = useState(false); const [, setIsDeleteModeAdded] = useState(false); const [selectedOriginal, setSelectedOriginal] = useState([]); @@ -41,19 +42,19 @@ export default function PayerInquiryPage() { const deleteMutation = useMutation({ mutationFn: deleteAdmins, onSuccess: () => { - alert('선택된 관리자 정보가 성공적으로 삭제되었습니다.'); + toast.success('선택된 관리자 정보가 성공적으로 삭제되었습니다.'); refetch(); }, - onError: () => alert('관리자 정보 삭제에 실패했습니다.'), + onError: () => toast.error('관리자 정보 삭제에 실패했습니다.'), }); const mutation = useMutation({ mutationFn: addAdmins, onSuccess: () => { - alert('추가된 관리자 정보가 성공적으로 저장되었습니다.'); + toast.success('추가된 관리자 정보가 성공적으로 저장되었습니다.'); refetch(); }, - onError: () => alert('추가된 관리자 정보 저장에 실패했습니다.'), + onError: () => toast.error('추가된 관리자 정보 저장에 실패했습니다.'), }); useEffect(() => { @@ -104,12 +105,12 @@ export default function PayerInquiryPage() { const handleApply = () => { if (!memberData || !Array.isArray(memberData.members)) { console.error('memberData가 올바른 형식이 아닙니다.', memberData); - alert('데이터를 불러오는 중입니다. 잠시 후 다시 시도해주세요.'); + toast.loading('데이터를 불러오는 중입니다. 잠시 후 다시 시도해주세요.'); return; } if (selectedAdded.length === 0) { - alert('추가할 관리자를 선택해주세요.'); + toast.error('추가할 관리자를 선택해주세요.'); return; } diff --git a/src/app/desktop/item-list/_components/ItemTable/index.tsx b/src/app/desktop/(nav-bar)/item-list/_components/ItemTable/index.tsx similarity index 95% rename from src/app/desktop/item-list/_components/ItemTable/index.tsx rename to src/app/desktop/(nav-bar)/item-list/_components/ItemTable/index.tsx index e000d11..b687a71 100644 --- a/src/app/desktop/item-list/_components/ItemTable/index.tsx +++ b/src/app/desktop/(nav-bar)/item-list/_components/ItemTable/index.tsx @@ -9,14 +9,14 @@ import { } from '@/components/ui/table'; import { Button } from '@/components/ui/button'; import { Checkbox } from '@/components/ui/checkbox'; -import { Item, ItemTableProps } from '@/types/items'; +import { Item, ItemTableProps, ItemTypeText } from '@/types/items'; import Image from 'next/image'; import { PageChangeAction } from '@/types/paginationType'; export default function ItemTable({ items = [], showCheckboxes = true, - headers = ['로고', '물품명', '소모품', '총 수량', '대여 중'], + headers = ['로고', '물품명', '소모품/대여품', '총 수량', '대여 중'], selected, setSelected, currentPage = 1, @@ -91,7 +91,7 @@ export default function ItemTable({ {item.itemName} - {item.itemType ? 'RENTAL' : 'CONSUMPTION'} + {ItemTypeText[item.itemType]} {item.count} diff --git a/src/app/desktop/item-list/page.tsx b/src/app/desktop/(nav-bar)/item-list/page.tsx similarity index 94% rename from src/app/desktop/item-list/page.tsx rename to src/app/desktop/(nav-bar)/item-list/page.tsx index 45eeeb9..cf562f2 100644 --- a/src/app/desktop/item-list/page.tsx +++ b/src/app/desktop/(nav-bar)/item-list/page.tsx @@ -1,6 +1,6 @@ 'use client'; -import Sidebar from 'src/components/desktop/Sidebar'; +import Sidebar from '@/components/desktop/Sidebar'; import { useState, useCallback, useEffect } from 'react'; import { Button } from '@/components/ui/button'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; @@ -8,6 +8,7 @@ import { getItems, addItems, deleteItems } from '@/services/items'; import { SearchInput } from '@/components/ui/search-input'; import Image from 'next/image'; import { PageChangeAction } from '@/types/paginationType'; +import toast from 'react-hot-toast'; import TableComponent from './_components/ItemTable'; export default function ItemListPage() { @@ -28,21 +29,21 @@ export default function ItemListPage() { const mutation = useMutation({ mutationFn: addItems, onSuccess: () => { - alert('물품 등록이 완료되었습니다.'); + toast.success('물품 등록이 완료되었습니다.'); queryClient.invalidateQueries({ queryKey: ['items'] }); }, onError: () => { - alert('물품 등록에 실패했습니다.'); + toast.error('물품 등록에 실패했습니다.'); }, }); const deleteMutation = useMutation({ mutationFn: deleteItems, onSuccess: () => { - alert('선택된 물품이 삭제되었습니다.'); + toast.success('선택된 물품이 삭제되었습니다.'); }, onError: () => { - alert('물품 삭제에 실패했습니다.'); + toast.error('물품 삭제에 실패했습니다.'); }, }); @@ -56,7 +57,7 @@ export default function ItemListPage() { const { itemName, quantity, selectedImage, isConsumable } = formData; if (!itemName || quantity === '' || quantity <= 0) { - alert('모든 정보를 입력하세요.'); + toast.error('모든 정보를 입력하세요.'); return; } @@ -97,11 +98,9 @@ export default function ItemListPage() { }; const handlePageChange = async (pageChangeAction: PageChangeAction) => { - console.log('PageChange:', pageChangeAction); setPage((current) => pageChangeAction === 'NEXT' ? current + 1 : current - 1, ); - console.log(`page: ${page}`); }; // 삭제 모드 토글 const toggleDeleteMode = () => { @@ -112,7 +111,7 @@ export default function ItemListPage() { // 물품 삭제 핸들러 const handleDeleteItem = () => { if (selectedItem === null) { - alert('삭제할 물품을 선택해 주세요.'); + toast.error('삭제할 물품을 선택해 주세요.'); return; } diff --git a/src/app/desktop/(nav-bar)/layout.tsx b/src/app/desktop/(nav-bar)/layout.tsx new file mode 100644 index 0000000..65dce6e --- /dev/null +++ b/src/app/desktop/(nav-bar)/layout.tsx @@ -0,0 +1,20 @@ +'use client'; + +import useAuthRedirect from '@/hooks/useAuthRedirect'; +import React from 'react'; +import NavBar from '@/components/desktop/NavBar/NavBar'; + +interface NavBarLayoutProps { + children: React.ReactNode; +} + +export default function NavBarLayout({ children }: NavBarLayoutProps) { + useAuthRedirect(); + + return ( +
+ + {children} +
+ ); +} diff --git a/src/app/desktop/payer-inquiry/_components/TableComponent/index.tsx b/src/app/desktop/(nav-bar)/payer-inquiry/_components/TableComponent/index.tsx similarity index 100% rename from src/app/desktop/payer-inquiry/_components/TableComponent/index.tsx rename to src/app/desktop/(nav-bar)/payer-inquiry/_components/TableComponent/index.tsx diff --git a/src/app/desktop/payer-inquiry/page.tsx b/src/app/desktop/(nav-bar)/payer-inquiry/page.tsx similarity index 92% rename from src/app/desktop/payer-inquiry/page.tsx rename to src/app/desktop/(nav-bar)/payer-inquiry/page.tsx index 7675979..75c30d5 100644 --- a/src/app/desktop/payer-inquiry/page.tsx +++ b/src/app/desktop/(nav-bar)/payer-inquiry/page.tsx @@ -1,6 +1,6 @@ 'use client'; -import Sidebar from 'src/components/desktop/Sidebar'; +import Sidebar from '@/components/desktop/Sidebar'; import { useState, useEffect } from 'react'; import AddStudentId from '@/components/desktop/AddStudentId'; import { Button } from '@/components/ui/button'; @@ -9,8 +9,9 @@ import { getPayer, addPayer, deletePayer } from '@/services/payers'; import { Payer } from '@/types/payers'; import { SearchInput } from '@/components/ui/search-input'; import { PageChangeAction } from '@/types/paginationType'; +import toast from 'react-hot-toast'; import TableComponent from './_components/TableComponent'; -import AddInput from '../../../components/desktop/AddInput'; +import AddInput from '../../../../components/desktop/AddInput'; export default function PayerInquiryPage() { const [searchQuery, setSearchQuery] = useState(''); @@ -37,24 +38,24 @@ export default function PayerInquiryPage() { const mutation = useMutation({ mutationFn: addPayer, onSuccess: () => { - alert('추가된 납부자 정보가 성공적으로 저장되었습니다.'); + toast.success('추가된 납부자 정보가 성공적으로 저장되었습니다.'); setAddedData([]); refetch(); }, onError: () => { - alert('추가된 납부자 정보 저장에 실패했습니다.'); + toast.error('추가된 납부자 정보 저장에 실패했습니다.'); }, }); const deleteMutation = useMutation({ mutationFn: deletePayer, onSuccess: () => { - alert('선택된 납부자 정보가 성공적으로 삭제되었습니다.'); + toast.success('선택된 납부자 정보가 성공적으로 삭제되었습니다.'); setAddedData([]); refetch(); }, onError: () => { - alert('납부자 정보 삭제에 실패했습니다.'); + toast.error('납부자 정보 삭제에 실패했습니다.'); }, }); @@ -84,13 +85,13 @@ export default function PayerInquiryPage() { const handleAddStudent = () => { if (!newStudentId || !newStudentName) { - alert('이름과 학번을 입력해주세요.'); + toast.error('이름과 학번을 입력해주세요.'); return; } const studentIdPattern = /^\d{8}$/; if (!studentIdPattern.test(newStudentId)) { - alert('학번은 8자리 숫자로 입력해야 합니다.'); + toast.error('학번은 8자리 숫자로 입력해야 합니다.'); return; } @@ -120,11 +121,9 @@ export default function PayerInquiryPage() { }; const handlePageChange = async (pageChangeAction: PageChangeAction) => { - console.log('PageChange:', pageChangeAction); setPage((current) => pageChangeAction === 'NEXT' ? current + 1 : current - 1, ); - console.log(`page: ${page}`); }; const toggleDeleteMode = (mode: 'original' | 'added') => { diff --git a/src/app/desktop/rental-history/_components/RentalsTable/index.tsx b/src/app/desktop/(nav-bar)/rental-history/_components/RentalsTable/index.tsx similarity index 100% rename from src/app/desktop/rental-history/_components/RentalsTable/index.tsx rename to src/app/desktop/(nav-bar)/rental-history/_components/RentalsTable/index.tsx diff --git a/src/app/desktop/rental-history/page.tsx b/src/app/desktop/(nav-bar)/rental-history/page.tsx similarity index 100% rename from src/app/desktop/rental-history/page.tsx rename to src/app/desktop/(nav-bar)/rental-history/page.tsx diff --git a/src/app/desktop/layout.tsx b/src/app/desktop/layout.tsx index 91d4d5d..416a3f3 100644 --- a/src/app/desktop/layout.tsx +++ b/src/app/desktop/layout.tsx @@ -1,14 +1,15 @@ -'use client'; - -import useAuthRedirect from '@/hooks/useAuthRedirect'; import React from 'react'; +import { Toaster } from 'react-hot-toast'; interface DesktopLayoutProps { children: React.ReactNode; } export default function DesktopLayout({ children }: DesktopLayoutProps) { - useAuthRedirect(); - - return
{children}
; + return ( +
+ + {children} +
+ ); } diff --git a/src/app/desktop/login/page.tsx b/src/app/desktop/login/page.tsx index a6f7a4e..2c19c13 100644 --- a/src/app/desktop/login/page.tsx +++ b/src/app/desktop/login/page.tsx @@ -6,6 +6,7 @@ import axios from 'axios'; import { postAdminLogin } from '@/services/admins'; import { handleLoginSuccess } from '@/utils/loginHandler'; import { useRouter } from 'next/navigation'; +import toast from 'react-hot-toast'; export default function Login() { const router = useRouter(); @@ -14,50 +15,76 @@ export default function Login() { const validateLoginForm = (studentId: string, password: string) => { if (!studentId) { - alert('학번을 입력해 주세요!'); + toast.error('학번을 입력해 주세요!'); return false; } if (!password) { - alert('비밀번호를 입력해 주세요!'); + toast.error('비밀번호를 입력해 주세요!'); return false; } const idRegex = /^\d{8}$/; if (!idRegex.test(studentId)) { - alert('학번은 숫자 8자리여야 합니다.'); + toast.error('학번은 숫자 8자리여야 합니다.'); return false; } return true; }; - const handleAdminLogin = async () => { + const handleAdminLogin = async ( + event: React.MouseEvent, + ) => { + event.preventDefault(); + const studentId = studentIdRef?.current?.value || ''; const password = passwordRef?.current?.value || ''; if (!validateLoginForm(studentId, password)) return; - try { - const data = await postAdminLogin({ - studentId, - password, - }); + await toast.promise( + postAdminLogin({ studentId, password }).then((data) => { + handleLoginSuccess(data.accessToken); + router.push('/desktop/rental-history'); + }), + { + loading: '로그인 중...', + success: '로그인에 성공했습니다!', + error: (error) => { + if (axios.isAxiosError(error)) { + const errorResponse = error.response?.data; - handleLoginSuccess(data.accessToken); - router.push('/desktop/rental-history'); - } catch (error) { - if (axios.isAxiosError(error)) { - const errorResponse = error.response?.data; + if (errorResponse) { + return errorResponse.message; + } + } - if (!errorResponse) { - alert('관리자 로그인 중 오류가 발생했습니다!'); - return; - } + return '관리자 로그인 중 오류가 발생했습니다!'; + }, + }, + ); - alert(errorResponse.message); - } - } + // try { + // const data = await postAdminLogin({ + // studentId, + // password, + // }); + // + // handleLoginSuccess(data.accessToken); + // router.push('/desktop/rental-history'); + // } catch (error) { + // if (axios.isAxiosError(error)) { + // const errorResponse = error.response?.data; + // + // if (!errorResponse) { + // toast.error('관리자 로그인 중 오류가 발생했습니다!'); + // return; + // } + // + // toast.error(errorResponse.message); + // } + // } }; return ( diff --git a/src/components/desktop/Header/index.tsx b/src/components/desktop/Header/index.tsx deleted file mode 100644 index e69de29..0000000 diff --git a/src/components/desktop/NavBar/NavBar.tsx b/src/components/desktop/NavBar/NavBar.tsx new file mode 100644 index 0000000..62a0508 --- /dev/null +++ b/src/components/desktop/NavBar/NavBar.tsx @@ -0,0 +1,69 @@ +'use client'; + +import { useRouter } from 'next/navigation'; +import { useState } from 'react'; +import toast from 'react-hot-toast'; + +interface NavBarItem { + name: string; + link: string; +} + +const navBarItems: NavBarItem[] = [ + { + name: '대여내역 조회', + link: '/desktop/rental-history', + }, + { + name: '관리자 관리', + link: '/desktop/admin-inquiry', + }, + { + name: '납부자 관리', + link: '/desktop/payer-inquiry', + }, + { + name: '물품 관리', + link: '/desktop/item-list', + }, + { + name: '로그아웃', + link: '/desktop/login', + }, +]; + +export default function NavBar() { + const router = useRouter(); + const [currentIndex, setCurrentIndex] = useState(0); + + const handleNavBtnClick = (item: NavBarItem, index: number) => { + router.push(`${item.link}`); + + if (item.name === '로그아웃') { + localStorage.clear(); + toast.success('로그아웃에 성공했습니다!'); + } + + console.log('index:', index); + setCurrentIndex(index); + }; + + return ( +
+ {navBarItems.map((navItem: NavBarItem, index: number) => ( + + ))} +
+ ); +} diff --git a/src/hooks/useAuthRedirect.ts b/src/hooks/useAuthRedirect.ts index d75ebd9..b7e9bce 100644 --- a/src/hooks/useAuthRedirect.ts +++ b/src/hooks/useAuthRedirect.ts @@ -6,7 +6,6 @@ import { useRouter, usePathname } from 'next/navigation'; const useAuthRedirect = () => { const router = useRouter(); const pathname = usePathname(); - useEffect(() => { if (typeof window === 'undefined') return; @@ -17,31 +16,33 @@ const useAuthRedirect = () => { currentPage === '/desktop/login'; const userString = localStorage.getItem('user'); - const isLogin = userString && localStorage.getItem('token'); + const isLogin = !!(userString && localStorage.getItem('token')); const user = userString ? JSON.parse(userString) : undefined; if (!isLogin) { - localStorage.clear(); if (!checkCurrentPages) { alert('로그인 후 이용 가능한 페이지입니다.'); - if (currentPage.startsWith('/desktop')) { - router.replace('/desktop/login'); - } else { - router.replace('/mobile/sign-in'); - } - } - } else { - if (currentPage.startsWith('/desktop') && user.role !== 'ADMIN') { - alert('관리자만 이용 가능한 페이지입니다.'); - router.replace('/desktop/login'); + router.replace( + currentPage.startsWith('/desktop') + ? '/desktop/login' + : '/mobile/sign-in', + ); return; } - if (currentPage.startsWith('/mobile/admin') && user.role !== 'ADMIN') { - alert('관리자만 이용 가능한 페이지입니다.'); - router.replace('/mobile/main'); - } + return; + } + + if (currentPage.startsWith('/desktop') && user.role !== 'ADMIN') { + alert('관리자만 이용 가능한 페이지입니다.'); + router.replace('/desktop/login'); + return; + } + + if (currentPage.startsWith('/mobile/admin') && user.role !== 'ADMIN') { + alert('관리자만 이용 가능한 페이지입니다.'); + router.replace('/mobile/main'); } - }, [router, pathname]); + }, []); }; export default useAuthRedirect; diff --git a/src/services/privateAxiosInstance.ts b/src/services/privateAxiosInstance.ts index 5499f07..5fd8f20 100644 --- a/src/services/privateAxiosInstance.ts +++ b/src/services/privateAxiosInstance.ts @@ -25,7 +25,16 @@ PrivateAxiosInstance.interceptors.request.use( (config: InternalAxiosRequestConfig) => { const token = getAccessToken(); if (!token) { - window.location.replace('/mobile/sign-in'); + const redirectPages = ['/desktop/login', '/mobile/sign-in']; + const { pathname } = window.location; + + if (!redirectPages.includes(pathname)) { + window.location.replace( + pathname.startsWith('/desktop') + ? '/desktop/login' + : '/mobile/sign-in', + ); + } return Promise.reject(new AxiosError('No authentication token found')); } diff --git a/src/types/items.ts b/src/types/items.ts index f35fa44..86a6401 100644 --- a/src/types/items.ts +++ b/src/types/items.ts @@ -17,3 +17,8 @@ export interface ItemTableProps extends PaginationProps { setSelected: (selectedIds: number) => void; handleDelete?: (selectedIds: string) => void; } + +export const ItemTypeText: Record = { + CONSUMPTION: '소모품', + RENTAL: '대여품', +} as const; diff --git a/yarn.lock b/yarn.lock index 2b36b72..16fb0b4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4034,7 +4034,7 @@ cssstyle@^2.3.0: dependencies: cssom "~0.3.6" -csstype@^3.0.2: +csstype@^3.0.2, csstype@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== @@ -5298,6 +5298,11 @@ globby@^6.1.0: pify "^2.0.0" pinkie-promise "^2.0.0" +goober@^2.1.16: + version "2.1.16" + resolved "https://registry.yarnpkg.com/goober/-/goober-2.1.16.tgz#7d548eb9b83ff0988d102be71f271ca8f9c82a95" + integrity sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g== + gopd@^1.0.1, gopd@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" @@ -7412,6 +7417,14 @@ react-dom@^18: loose-envify "^1.1.0" scheduler "^0.23.2" +react-hot-toast@^2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/react-hot-toast/-/react-hot-toast-2.5.2.tgz#b55328966a26add56513e2dc1682e2cb4753c244" + integrity sha512-Tun3BbCxzmXXM7C+NI4qiv6lT0uwGh4oAfeJyNOjYUejTsm35mK9iCaYLGv8cBz9L5YxZLx/2ii7zsIwPtPUdw== + dependencies: + csstype "^3.1.3" + goober "^2.1.16" + react-is@^16.13.1, react-is@^16.7.0: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"