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

feat: 어드민 서비스 TanstackQuery 설정 및 입장 확인, 어드민 로그인 API 연결 #63

Merged
merged 30 commits into from
Jul 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
8745fa4
chore(admin): tanstackQuery, axios 라이브러리 설치
coggiee Jun 29, 2024
bd0cf40
feat(admin): axios instance 생성
coggiee Jun 29, 2024
a0d3ecb
refactor(admin): 기존 Route group의 layout 수정
coggiee Jun 29, 2024
f0e052c
feat(admin): Root Layout에 TanstackQuery Provider 설정
coggiee Jun 29, 2024
7130937
feat(admin): 로그인 및 회원가입 커스텀 쿼리 훅 생성
coggiee Jun 29, 2024
1b16686
feat(admin): 입장 확인 커스텀 쿼리 훅 생성
coggiee Jun 29, 2024
52fd1bd
feat(admin): accessToken 관리 함수 생성
coggiee Jun 29, 2024
a4217fd
chore(admin): zod, react-hook-form 설치
coggiee Jul 8, 2024
be130b9
feat(admin): 로그인 기능에 react-hook-form과 zod 적용
coggiee Jul 8, 2024
91c6c22
chore(admin): 에러 코드에 대응하는 에러 메시지 상수 생성
coggiee Jul 8, 2024
8a85d7f
refactor(admin): API BASE_URL 설정
coggiee Jul 8, 2024
96598f8
refactor(admin): useMutationLogin의 throwOnError 옵션 설정
coggiee Jul 8, 2024
318592c
refactor(admin): 토큰 관리 함수 수정
coggiee Jul 8, 2024
f05af4f
refactor(admin): 로그인 토큰 관리 방식 변경
coggiee Jul 24, 2024
4eaef13
feat(admin): 로그인 시 페이지 이동을 위한 middleware 설정
coggiee Jul 24, 2024
0d6c677
style(admin): 로그인 에러 메시지 스타일 변경
coggiee Jul 24, 2024
716a094
feat(admin): 커스텀 에러 객체 관리 기능 추가
coggiee Jul 24, 2024
6ae6a4d
feat(admin): 토스트 컴포넌트 사용을 위한 Toaster 추가
coggiee Jul 24, 2024
b3265c1
refactor(admin): 입장 확인 API 수정
coggiee Jul 24, 2024
15e0a4f
feat(admin): 입장 확인 API 핸들러 추가
coggiee Jul 24, 2024
aec309c
style(admin): 로고에 Link 추가
coggiee Jul 24, 2024
9c4b771
refactor(admin): 사용하지 않는 커스텀 query 훅 삭제
coggiee Jul 24, 2024
9115e3a
chore: package-lock.json 삭제
coggiee Jul 24, 2024
a086786
refactor(admin): middleware 수정
coggiee Jul 24, 2024
fc066f1
feat: Nav에 로그인 유무를 확인할 수 있는 로그아웃 버튼 추가
coggiee Jul 24, 2024
dab3227
Merge branch 'dev' into feat/#61
coggiee Jul 24, 2024
ed6b5c0
refactor(admin): FormMessage 삭제 및 경고성 메시지 폰트 크기 변경
coggiee Jul 25, 2024
a573ca4
chore(admin): 사용하지 않는 import 삭제
coggiee Jul 25, 2024
c246de7
style(admin): 로그인 폼 스타일 변경
coggiee Jul 27, 2024
82d292e
fix(admin): controlled component 오류 수정
coggiee Jul 27, 2024
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
31 changes: 31 additions & 0 deletions apps/admin/api/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {
LoginRequestParams,
LoginResponse,
SignupRequestParams,
SignupResponse,
} from "@/types/authType";

import { instance } from "./instance";

export const login = async ({ email, password }: LoginRequestParams) => {
const { data } = await instance.post<LoginResponse>(`/auth/login`, {
email,
password,
});

return data;
};

export const signup = async ({
email,
password,
name,
}: SignupRequestParams) => {
const { data } = await instance.post<SignupResponse>(`/auth/signup`, {
email,
password,
name,
});

return data;
};
44 changes: 44 additions & 0 deletions apps/admin/api/instance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import axios from "axios";

import { getAccessToken } from "@/utils/handleToken";

const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL;
const SERVER_VERSION = "/admin/v1";

const AUTH_REQUIRED_PATH: string[] = [];
const DYNAMIC_AUTH_REQUIRED_PATH = [/\/ticket\/[^/]+\/enter/];

const isDynamicUrlMatched = (url: string): boolean => {
return DYNAMIC_AUTH_REQUIRED_PATH.some(path => path.test(url));
};

export const instance = axios.create({
baseURL: `${BASE_URL}${SERVER_VERSION}`,
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
});

instance.interceptors.request.use(async config => {
if (
config.url &&
(AUTH_REQUIRED_PATH.includes(config.url) || isDynamicUrlMatched(config.url))
) {
const accessToken = await getAccessToken();

config.headers.Authorization = `Bearer ${accessToken}`;
}

return config;
});

// TODO: interceptor 에러 발생 시 처리 로직 추가
instance.interceptors.response.use(
async response => {
return response;
},
async error => {
return Promise.reject(error);
},
);
11 changes: 11 additions & 0 deletions apps/admin/api/ticket.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { TicketQrCodeResponse } from "@/types/ticketType";

import { instance } from "./instance";

export const scanQrCode = async (token: string | null) => {
const { data } = await instance.get<TicketQrCodeResponse>(
`/ticket/${token}/enter`,
);

return data;
};
82 changes: 63 additions & 19 deletions apps/admin/app/(auth)/_components/AuthSection.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,76 @@
"use client";

import React from "react";
import { Label } from "@ui/components/ui/label";
import { Input } from "@ui/components/ui/input";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
} from "@ui/components/ui/form";
import { Button } from "@ui/components/ui/button";

import { useLoginForm } from "@/hooks/useLoginForm";

function AuthSection() {
const { form, onSubmit, error } = useLoginForm();

return (
<section className="flex grow flex-col justify-center gap-11">
<section className="mt-16 flex grow flex-col gap-11">
<header>
<h1 className="text-2xl font-black">관리자 로그인</h1>
</header>
<main className="flex flex-col gap-10">
<div className="flex flex-col gap-3">
<div className="grid w-full max-w-sm items-center gap-1.5">
<Label htmlFor="id" className="text-[#5E5E6E]">
ID
</Label>
<Input id="id" placeholder="아이디" />
</div>
<div className="grid w-full max-w-sm items-center gap-1.5">
<Label htmlFor="pw" className="text-[#5E5E6E]">
PW
</Label>
<Input id="pw" type="password" placeholder="비밀번호" />
</div>
</div>
<Button className="bg-brand hover:bg-brandHover w-full rounded-lg py-6 text-base md:max-w-24">
로그인
</Button>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="flex flex-col gap-5"
>
<div className="flex flex-col gap-3">
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem className="grid w-full items-center gap-1.5 sm:max-w-xs">
<FormLabel className="text-[#5E5E6E]">ID</FormLabel>
<FormControl>
<Input id="id" placeholder="아이디" {...field} />
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem className="grid w-full items-center gap-1.5 sm:max-w-xs">
<FormLabel className="text-[#5E5E6E]">PW</FormLabel>
<FormControl>
<Input
id="pw"
type="password"
placeholder="비밀번호"
{...field}
/>
</FormControl>
</FormItem>
)}
/>
</div>
<div className="space-y-4">
<p className="h-5 text-center text-sm text-[#FD7250] sm:text-left">
{error && error}
</p>
<Button
type="submit"
className="bg-brand hover:bg-brandHover w-full rounded-lg py-6 text-base md:max-w-24"
>
로그인
</Button>
</div>
</form>
</Form>
</main>
</section>
);
Expand Down
29 changes: 7 additions & 22 deletions apps/admin/app/(auth)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,17 @@
import type { Metadata } from "next";

import { Inter } from "next/font/google";

import "@uket/ui/globals.css";
import Nav from "@/components/Nav";

const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};

export default function RootLayout({
export default function WithoutNavLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={inter.className}>
<main className="flex h-dvh flex-col">
<header className="container sticky top-0">
<Nav />
</header>
<main className="grow">{children}</main>
</main>
</body>
</html>
<main className="flex h-dvh flex-col">
<header className="container sticky top-0">
<Nav />
</header>
<main className="grow">{children}</main>
</main>
);
}
35 changes: 10 additions & 25 deletions apps/admin/app/(main)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,21 @@
import type { Metadata } from "next";

import { Inter } from "next/font/google";

import "@uket/ui/globals.css";
import Nav from "@/components/Nav";
import Footer from "@/components/Footer";

const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};

export default function RootLayout({
export default function WithNavLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={inter.className}>
<main className="flex h-dvh flex-col">
<header className="container sticky top-0">
<Nav />
</header>
<main className="grow">{children}</main>
<footer>
<Footer />
</footer>
</main>
</body>
</html>
<main className="flex h-dvh flex-col">
<header className="container sticky top-0">
<Nav />
</header>
<main className="grow">{children}</main>
<footer>
<Footer />
</footer>
</main>
);
}
8 changes: 6 additions & 2 deletions apps/admin/app/(main)/qr-scan/_components/QRScanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,22 @@
import React from "react";
import { Scanner } from "@yudiel/react-qr-scanner";

import { useScan } from "@/hooks/useScan";

import QRFinderIcon from "./QRFinderIcon";

// TODO: onScan에 실제 스캔 후 동작할 함수 연결
const QRScanner = () => {
const { handleQRScan } = useScan();

return (
<div className="relative h-full">
<Scanner
formats={["qr_code"]}
onScan={result => result}
onScan={handleQRScan}
classNames={{ video: "object-cover" }}
styles={{ finderBorder: 1 }}
components={{ finder: false }}
allowMultiple
>
<QRFinderIcon />
<h1 className="absolute left-0 top-6 w-full text-2xl font-bold text-white sm:text-3xl">
Expand Down
29 changes: 29 additions & 0 deletions apps/admin/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { Metadata } from "next";

import { Inter } from "next/font/google";
import "@uket/ui/globals.css";
import { Toaster } from "@ui/components/ui/toaster";

import Providers from "./providers";

const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};

export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={inter.className}>
<Providers>{children}</Providers>
<Toaster />
</body>
</html>
);
}
62 changes: 62 additions & 0 deletions apps/admin/app/providers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// In Next.js, this file would be called: app/providers.jsx
"use client";

// Since QueryClientProvider relies on useContext under the hood, we have to put 'use client' on top
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import {
isServer,
HydrationBoundary,
QueryClient,
QueryClientProvider,
dehydrate,
} from "@tanstack/react-query";

function makeQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
// With SSR, we usually want to set some default staleTime
// above 0 to avoid refetching immediately on the client
throwOnError: true,
retry: false,
staleTime: 60 * 1000,
},
mutations: {
throwOnError: true,
},
},
});
}

let browserQueryClient: QueryClient | undefined = undefined;

function getQueryClient() {
if (isServer) {
// Server: always make a new query client
return makeQueryClient();
} else {
// Browser: make a new query client if we don't already have one
// This is very important, so we don't re-make a new client if React
// suspends during the initial render. This may not be needed if we
// have a suspense boundary BELOW the creation of the query client
if (!browserQueryClient) browserQueryClient = makeQueryClient();
return browserQueryClient;
}
}

export default function Providers({ children }: { children: React.ReactNode }) {
// NOTE: Avoid useState when initializing the query client if you don't
// have a suspense boundary between this and the code that may
// suspend because React will throw away the client on the initial
// render if it suspends and there is no boundary
const queryClient = getQueryClient();

return (
<QueryClientProvider client={queryClient}>
<HydrationBoundary state={dehydrate(queryClient)}>
{children}
</HydrationBoundary>
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}
Loading
Loading