[Next.js] 중고마켓 앱2

Jungmin Ji·2024년 1월 30일
0

Nextjs

목록 보기
5/9
post-thumbnail

Next Auth 설정하기

Auth 라이브러리 설치

npm install next-auth @prisma/client @next-auth/prisma-adapter
npm install prisma --save-dev

기본 설정 코드 생성

PrismaClient 인스턴스 생성을 하여 데이터에 접근한다. prividers로 어떻게 로그인할건지 정한다. (CredentialsProvider... )

src/pages/api/auth/[...nextauth].js : /api/auth로의 API요청을 Next-Auth가 처리

  • adapter: 데이터를 저장하는데 사용할 데이터베이스 또는 백엔드 시스템에 애플리케이션을 연결
  • privider : 어떠한 로그인을 사용할 것인지, 기본 로그인 혹은 소셜 로그인 사용(구글, 깃허브.... )

Next Auth 설정

src/pages/api/auth/[...nextauth].js

https://authjs.dev/reference/adapter/prisma
공식문서의 코드 가져온다.

import NextAuth from "next-auth"
import GoogleProvider from "next-auth/providers/google"
import { PrismaAdapter } from "@next-auth/prisma-adapter"
import { PrismaClient } from "@prisma/client"

const prisma = new PrismaClient()

export default NextAuth({
  adapter: PrismaAdapter(prisma),
  providers: [
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID, 
      clientSecret: process.env.GOOGLE_CLIENT_SECRET,
    }),
  ],
})

authOptions은 따로 다시 재사용할거라 다음과 같이 수정한다.

import NextAuth, { NextAuthOptions } from "next-auth";
import GoogleProvider from "next-auth/providers/google";
// import { PrismaAdapter } from "@auth/prisma-adapter";
import { PrismaAdapter } from "@next-auth/prisma-adapter";
import { PrismaClient } from "@prisma/client";

const prisma = new PrismaClient();

export const authOptions: NextAuthOptions = {
  adapter: PrismaAdapter(prisma),
  providers: [
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID!, // undefined 아님 명시 
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    }),
  ],
};

export default NextAuth(authOptions);

보통은 app폴더안에 api폴더안에서 처리를 하는데,
현재는 app안에서 작동안돼서 pages폴더를 만들어 사용함.
추후 업데이트 유의

CredentialProvider 작성(이메일과 비밀번호 로그인)

https://next-auth.js.org/providers/credentials

CredentialProvider 작성

src/pages/api/auth/[...nextauth].js

import NextAuth, { NextAuthOptions } from "next-auth";
import GoogleProvider from "next-auth/providers/google";
// import { PrismaAdapter } from "@auth/prisma-adapter";
import { PrismaAdapter } from "@next-auth/prisma-adapter";
import { PrismaClient } from "@prisma/client";
import CredentialsProvider from "next-auth/providers/credentials";

const prisma = new PrismaClient();

export const authOptions: NextAuthOptions = {
  adapter: PrismaAdapter(prisma),
  providers: [
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    }),
    CredentialsProvider({
      name: "Credentials",
      credentials: {
        username: { label: "Username", type: "text", placeholder: "jsmith" },
        password: { label: "Password", type: "password" }
      },
      async authorize(credentials, req) {
        const user = { id: "1", name: "J Smith", email: "jsmith@example.com" }
  
        if (user) {
          return user
        } else {
          return null
        }
      }
    })
  ],
};


export default NextAuth(authOptions);

로그인 후 세션정보 가져오기

클라이언트 컴포넌트에서 세션정보를 가져오기위해서 SesstionProvider를 감싸고 useSession Hook을 이용한다.

layout.tsx를 테스트를 위해서 임시로 client로 만들어놓고 추후에 수정할것.

'use client'
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import './globals.css'
import Link from 'next/link'
import Navbar from '@/components/Navbar';
import { SessionProvider } from 'next-auth/react'

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

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

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body className={inter.className}>
        <SessionProvider>
          <Navbar />
          <main>{children}</main>
        </SessionProvider>
      </body>
    </html>
  );
}

NavItem.tsx

useSession()을 호출해서 콘솔에 찍어보면 data와 status가 나옴.
로그인시 data에 정보를 받고 status가 변경되므로
destructuring해서 풀어준다.
const { data: session, status } = useSession();
console.log({ session }, status);

로그인/로그아웃버튼을 처리해준다.

      {session?.user ? (
        <li className="py-2 text-center border-b-4 cursor-pointer">
          <button onClick={() => signOut()}>Sign out</button>
        </li>
      ) : (
        <li className="py-2 text-center border-b-4 cursor-pointer">
          <button onClick={() => signIn()}>Sign in</button>
        </li>
      )}

next auth에서 제공되는 signOut과 signIn을 호출하면 자동으로 처리를 해주는 페이지로 이동이된다.

[...nextauth].ts에서 providers안에서 label이나 placeholder등을 설정할 수 있다.

리턴하는 user는 NavItem에서 session의 user와 같다.

[...nextauth].ts providers하단에 다음 세션 설정을 추가한다.

  session: {
    // strategy: 'database'
    strategy: 'jwt'
  }

database는 DB에 저장하는것이고, jwt는 토큰에 저장하는것.
세션길이 등을 설정할 수 있다.

그대로 signin을 누르면

signout을 누르면 자동으로 세션쿠키를 삭제해준다.

로그인 한 사람만 특정 경로에 접근에 접근하기

nextjs미들웨어를 사용하기 위해서는 next auth secret을 env에 명시해줘야한다.
.env

...
NEXTAUTH_SECRET=nextAuthScret
NEXTAUTH_URL=http://localhost:3000

src/middleware.ts 생성

export {default} from 'next-auth/middleware'

export const config = { matcher: ["/admin", "/user"] }

비로그인상태에서 user나 admin 들어가면 로그인페이지로 이동한다.


server error페이지가 뜬다면
서버 재시동하거나,
[...nextauth].ts파일에서 secret: process.env.AUTH_SECRET,를 추가한다.

export const authOptions: NextAuthOptions = {
  adapter: PrismaAdapter(prisma),
  secret: process.env.AUTH_SECRET,
  providers: [

잘 들어가진다.
이러면 안된다!!!!

로그인페이지로 리다이렉트가 되어야한다.
이건 middleware.ts에서 url 설정을 다음과 같이 하면된다.

export {default} from 'next-auth/middleware'

export const config = { matcher: ["/admin/:path*", "/user/:path"] };

관리자나 유저의 어떤 하위페이지도 접근이 안되고
로그인페이지로 redirect되는것을 처리하였다.

세션 데이터에 토큰 데이터 넣어주기

로그인시 session.user에 exp, iat, jti(jwt id) 등 토큰을 넣어주는 작업
[...nextauth].ts

  callbacks: {
    async jwt({ token, user }) {
        console.log("token", token)
        console.log("user", user)
        return { ...token, ...user }
    },
    async session({ session, token }) {
        session.user = token;
        return session;
    }
  }


잘 들어감.

NavItem컴포넌트로 돌아와서,
session.user.을 작성할때 id도 auto complete됐으면 한다.

이건 typescript로 처리해야한다.

src/types/next-auth.d.ts

import { DefaultSession } from "next-auth";

declare module "next-auth" {
    interface Session {
        user?: {
            id?: string;
            role?: string;
        } & DefaultSession["user"];
    }
}

user정보에 id와 role를 추가해준것.

권한에 따른 인가 처리하기(authorization)

로그인시 role이 관리자인경우에만 admin 메뉴를 노출할것이다.

유저타입 추가

db스키마에서 유저타입을 추가한다.
schema.prisma

enum UserType {
  User
  Admin
}

model User {
  id             String    @id @default(cuid())
  name           String?
  hashedPassword String?
  email          String?   @unique
  emailVerified  DateTime?
  image          String?
  accounts       Account[]
  sessions       Session[]

  userType UserType @default(User)
}

DB 싱크맞추기
npx prisma db push (할때마다 포트 연결이 안돼서.. 할때마다 postgreSQL을 서비스 재시작을 해야된다..)

[...nextauth].ts에서 로그인 정보에 role을 임시로 추가해준다.


세션에 role이 들어왔다.

권한에 따른 인가 처리하기

로그인된 유저만 접근가능,
어드민 유저만 접근가능,
로그인된 유저는 로그인, 회원가입 페이지에 접근 X(/로 이동)
처리하기

jwt 설정추가, env에 jwt secret 변수추가

[...nextauth].ts에서 session아래 jwt설정 코드를 추가 작성한다.

  session: {
    // strategy: 'database'
    strategy: "jwt",
  },
  jwt: {
    secret: 'secret111',
    maxAge: 30 * 24 * 60 * 60 // 30days
  },

미들웨어에서 사용하는 jwt의 secret과 [...nextauth].ts에서 secret은 같아야한다.
.env파일에서 JWT_SECRET="jwt-secret"로 변수를 추가하고 사용한다.
사용할때는 process.env.JWT_SECRET라고 작성.

미들웨어 작성

export async function middleware(req: NextRequest) {
    const session = await getToken({ req, secret: process.env.JWT_SECRET });
    console.log('session', session)
    console.log("req.nextUrl.pathname", req.nextUrl.pathname);
    
}

콘솔결과:

이 pathname과 session을 통해서 인가처리를 해준다.
middleware.ts

import { getToken } from 'next-auth/jwt';
import { NextRequest, NextResponse } from 'next/server';

export {default} from 'next-auth/middleware'

export async function middleware(req: NextRequest) {
    const session = await getToken({ req, secret: process.env.JWT_SECRET });
    // console.log('session', session)
    // console.log("req.nextUrl.pathname", req.nextUrl.pathname);

    const pathname = req.nextUrl.pathname;

    // 로그인된 유저만 접근가능
    if(pathname.startsWith('/user') && !session) {
        return NextResponse.redirect(new URL("/auth/login", req.url));
    }

    // 어드민 유저만 접근가능
    if(pathname.startsWith('/admin') && (session?.role !== 'admin')) {
        return NextResponse.redirect(new URL("/", req.url));
    }
    // 로그인된 유저는 회원가입, 로그인페이지 접근 불가
    if(pathname.startsWith('/auth') && session) {
        return NextResponse.redirect(new URL("/", req.url));
    }

    return NextResponse.next(); //통과
}

//  admin과 user 하위페이지 전체 redirect처리: 테스트를 위해 주석처리
// export const config = { matcher: ["/admin/:path*", "/user/:path"] }; 

로그인, 회원가입 UI 생성하기(커스텀)

커스텀 페이지 사용설정

[...nextauth].ts에 signIn페이지 따로 지정하는 설정 추가

  session: {
    // strategy: 'database'
    strategy: "jwt",
  },
  jwt: {
    secret: process.env.JWT_SECRET,
    maxAge: 30 * 24 * 60 * 60 // 30days
  },
  pages: {
    signIn: '/auth/login',
  },

Input 컴포넌트 생성하기

인풋은 자주사용하므로 재사용가능한 컴포넌트로 만든다.
유효성 체크와 최적화등을 위해 react-hook-form 라이브러리를 사용한다.
https://react-hook-form.com/
npm i react-hook-form

src/app/components/Input.tsx

import { format } from 'path';
import React from 'react'
import { FieldErrors, FieldValues, UseFormRegister } from 'react-hook-form';

interface InputProps {
    id: string;
    label: string;
    type?: string;
    disabled?: boolean;
    formatPrice?: boolean;
    required?: boolean;
    register: UseFormRegister<FieldValues>;
    errors: FieldErrors;
}
const Input: React.FC<InputProps> = ({
    id,
    label,
    type = "text",
    disabled, 
    formatPrice,
    register,
    required, 
    errors,
}) => {
  return (
    <div className="relative w-full">
      {formatPrice && (
        <span className="absolute text-neutral-700 top-5 left-2"></span>
      )}
      <input 
        id={id}
        disabled={disabled}
        {...register(id, { required })}
        placeholder=''
        type={type}
        className={`
            w-full
            p-4
            pt-6
            font-light
            border-2
            bg-white
            rounded-md
            outline-none
            transition
            disabled:opacity-70
            disabled:cursor-not-allowed
            ${formatPrice? 'pl-9': 'pl-4'}
            ${errors[id] ? 'border-rose-500' : 'border-neutral-300'}
            ${errors[id] ? 'focus:border-rose-500' : 'focus:border-black'}
        `}
      />
      <label className={`
        absolute
        text-md
        duration-150
        transform
        -translate-y-3
        top-5
        z-10
        origin-[0]
        ${formatPrice ? 'left-9': 'left-4'}
        peer-placeholder-shown:scale-100
        peer-placeholder-shown:translate-y-0
        peer-focus:scale-75
        peer-focus:-translate-y-4
        ${errors[id] ? 'text-rose-500' : 'text-zinc-400'}
      `}>
        {label}
      </label>
    </div>
  );
}

export default Input

Button 컴포넌트 생성하기

아이콘 설치 npm i react-icons
src/app/components/Button.tsx

import React from 'react'
import { IconType } from 'react-icons';

interface ButtonProps {
    label: string;
    onClick?: (e:React.MouseEvent<HTMLButtonElement>) => void;
    disabled?: boolean;
    outline?: boolean;
    small?: boolean;
    icon?: IconType
}
const Button: React.FC<ButtonProps> = ({
    label,
    onClick,
    disabled,
    outline,
    small,
    icon: Icon
}) => {
  return (
    <button
        type="submit"
        disabled={disabled}
        onClick={onClick}
        className={`
            relative
            disabled:opacity-70
            disabled:cursor-not-allowed
            rounded-lg
            hover:opacity-80
            transition
            w-full
            ${outline ? 'bg-white': 'bg-orange-500'}
            ${outline ? 'bolder-black': 'border-orange-500'}
            ${small ? 'text-sm' : 'text-md'}
            ${small ? 'py-1' : 'py-3'}
            ${small ? 'font-light' : 'font-semibold'}
            ${small ? 'bolder-[1px]' : 'border-2'}
        `}
    >
        {Icon && (
            <Icon 
                size={24}
                className='absolute left-4 top-3'
            />
        )}
        {label}
    </button>
  )
}

export default Button

register page UI

src/app/auth/register/page.tsx

"use client";
import Button from "@/components/Button";
import Input from "@/components/Input";
import axios from "axios";
import Link from "next/link";
import { useRouter } from "next/navigation";
import React, { useState } from "react";
import { FieldValues, SubmitHandler, useForm } from "react-hook-form";

const RegisterPage = () => {
  const [isLoading, setIsLoading] = useState(false);
  const router = useRouter();

  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<FieldValues>({
    defaultValues: {
      name: "",
      email: "",
      password: "",
    },
  });

  const onSubmit: SubmitHandler<FieldValues> = async (body) => {
    setIsLoading(true);
    try {
        const {data} = await axios.post('/api/register', body)
        console.log(data)
        router.push("/auth/login")
    } catch(error) {
        console.log(error);
    } finally {
        setIsLoading(false);
    }
  };

  return (
    <section className="grid h-[calc(100vh_-_56px)] place-items-center">
      <form
        className="flex flex-col justify-center gap-4 min-w-[350px]"
        onSubmit={handleSubmit(onSubmit)}
      >
        <h1 className="text-2xl">가입하기</h1>
        <Input
          id="email"
          label="Email"
          disabled={isLoading}
          register={register}
          errors={errors}
        />
        <Input
          id="name"
          label="name"
          disabled={isLoading}
          register={register}
          errors={errors}
        />
        <Input
          id="password"
          label="password"
          type="password"
          disabled={isLoading}
          register={register}
          errors={errors}
        />

        <Button label="가입하기" />
        <div className="text-center">
          <p className="text-gray-400">
            이미 회원이신가요?&nbsp;
            <Link href="/auth/login" className="text-black hover:underline">
              로그인하기
            </Link>
          </p>
        </div>
      </form>
    </section>
  );
};

export default RegisterPage;

login page UI

src/app/auth/login/page.tsx

"use client";
import Button from "@/components/Button";
import Input from "@/components/Input";
import axios from "axios";
import { signIn } from "next-auth/react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import React, { useState } from "react";
import { FieldValues, SubmitHandler, useForm } from "react-hook-form";

const LoginPage = () => {
  const [isLoading, setIsLoading] = useState(false);
  const router = useRouter();

  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<FieldValues>({
    defaultValues: {
      email: "",
      password: "",
    },
  });

  const onSubmit: SubmitHandler<FieldValues> = async (body) => {
    setIsLoading(true);
    try {
      const data = signIn('credentials', body);
      console.log(data);
    //   router.push("/");
    } catch (error) {
      console.log(error);
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <section className="grid h-[calc(100vh_-_56px)] place-items-center">
      <form
        className="flex flex-col justify-center gap-4 min-w-[350px]"
        onSubmit={handleSubmit(onSubmit)}
      >
        <h1 className="text-2xl">로그인</h1>
        <Input
          id="email"
          label="Email"
          disabled={isLoading}
          register={register}
          errors={errors}
        />
        <Input
          id="password"
          label="password"
          type="password"
          disabled={isLoading}
          register={register}
          errors={errors}
        />

        <Button label="로그인" />
        <div className="text-center">
          <p className="text-gray-400">
            회원이 아니신가요?
            <Link href="/auth/register" className="text-black hover:underline">
              가입하기
            </Link>
          </p>
        </div>
      </form>
    </section>
  );
};

export default LoginPage;

UI작업은 끝났는데
signIn시 fake 데이터를 실제데이터로 바꾸고 기능을 구현할것이다.

회원가입 기능 구현하기(Route Handlers)

api

테스트
src/app/api/hello/route.ts

export async function GET(request: Request) {
    return new Response('Hello nextjs')
}

app/api 폴더안에 route.ts파일에 route handlers를 생성하면된다.
HTTP Methods가 지원된다.

  • GET, POST, PATCH, DELETE, HEAD, OPTIONS

회원가입 api

할일 :
POST메소드 생성,
파라미터 받고 패스워드 해싱,
db에 user 넣기(prisma client객체 만들어서)

prisma client 인스턴스는 어디서든지 재사용하기위해 따로 생성한다.

prisma client 파일만들기

src/helpers/prismadb.ts
폴더는 helpers나 lib로 네이밍한다.

import { PrismaClient } from "@prisma/client";

declare global {
    var prisma: PrismaClient | undefined;
}

const prisma = globalThis.prisma || new PrismaClient();

if(process.env.NODE_ENV !== 'production') globalThis.prisma = prisma;

export default prisma;

[...nextauth].ts에서도 PrismaClient를 새로 생성하지 않고 import하여 사용한다.

import prisma from '@/helpers/prismadb'

// const prisma = new PrismaClient();
export const authOptions: NextAuthOptions = {
  adapter: PrismaAdapter(prisma),

db에 저장할때 이 PrismaClient를 이용한다.

api 진짜 작성

hashedPassword를 처리하기위해서 bcrypt를 설치한다.
npm i bcryptjs
npm i --save-dev @types/bcryptjs

src/app/api/register/route.ts

import bcrypt from "bcryptjs";
import prisma from '@/helpers/prismadb'
import { NextResponse } from "next/server";

export async function POST(request: Request) {

    const body = await request.json();

    const {
        email,
        name,
        password
    } = body;

    const hashedPassword = await bcrypt.hash(password, 12); // 12: salt

    const user = await prisma.user.create({
        data: {
            email,
            name,
            hashedPassword
        }
    })

    return NextResponse.json(user);
}

셀렉트 쿼리 입력해서 돌려주면 데이터 아웃풋 잘 나옴.
유저타입도 기본으로 User로 들어간다.


쿼리/데이터 아웃풋 패널 안보이면 Tools에서 쿼리툴 열어주면된다.

로그인 시 실제 유저 세션 데이터 생성하기


임시 로그인 데이터를 지우고 실제 유저 데이터를 받아서 로그인하는 것을 구현한다.

데이터베이스에서 이메일과 패스워드 둘중 하나라도 없다면 error throw
데이터베이스에서 이메일로 유저를 찾고
유저가 없거나 유저의 hashedPassword가 없으면 error throw
유저가 있다면 받은 플레인패스워드와 해싱된 패스워드를 bcrypt.compare()로 비교

[...nextauth].ts

import bcrypt from 'bcryptjs'
...
    CredentialsProvider({
      name: "Credentials",
      credentials: {
        email: { label: "Email", type: "text" },
        password: { label: "Password", type: "password" },
      },
      async authorize(credentials, req) {
        if(!credentials?.email || !credentials?.password) {
          throw new Error('Invalid credentials')
        }

        const user = await prisma.user.findUnique({
          where: {
            email: credentials.email
          }
        })

        if(!user || !user?.hashedPassword) {
          throw new Error("Invalid credentials");
        }

        const isCorrectPassword = await bcrypt.compare(
          credentials.password,
          user.hashedPassword
        )

        if(!isCorrectPassword) {
          throw new Error("Invalid credentials");
        }

        return user;
      },
    }),

없는 유저 로그인시

있는 유저 로그인시

getServerSession

현재는 클라이언트 컴포넌트에서 useSession을 이용하는데
서버컴포넌트에서도 세션데이터 사용할 수 있게 한다.

테스트

src/app/user/page.tsx

import { authOptions } from '@/pages/api/auth/[...nextauth]'
import { getServerSession } from 'next-auth'
import React from 'react'

const UserPage = async () => {

  const session = await getServerSession(authOptions);
  console.log("server session", session)
  return (
    <div>로그인된 사용자만 볼 수있는 페이지</div>
  )
}

export default UserPage

유저 세션 모듈화

세션에 저장된 유저정보를 불러오는 함수 getCurrentUser를 생성하여 모듈화한다.

src/app/actions/getCurrentUser.ts

import { authOptions } from "@/pages/api/auth/[...nextauth]";
import { getServerSession } from "next-auth";
import prisma from '@/helpers/prismadb'

export async function getSession() {
    return await getServerSession(authOptions);
}

export default async function getCurrentUser() {
    try {
        const session = await getSession();

        if(!session?.user?.email) {
            return null;
        }

        const currentUser = await prisma?.user.findUnique({
            where: {
                email: session.user.email
            }
        })

        if(!currentUser) {
            return null;
        }
  
  		return currentUser;
    } catch(error) {
        return null;
    }
}

유저페이지에서 확인

  const userData = await getCurrentUser();
  console.log("유저데이터2",userData)

User 테이블에 createdAt, updatedAt 컬럼생성

schema.prisma

model User {
  id             String    @id @default(cuid())
  name           String?
  hashedPassword String?
  email          String?   @unique
  emailVerified  DateTime?
  image          String?
  accounts       Account[]
  sessions       Session[]
  createdAt      DateTime  @default(now())
  updatedAt      DateTime  @updatedAt

  userType UserType @default(User)
}

스키마파일에 두 행 추가하고
회원가입 해뒀던 유저데이터는 pgAdmin에서 삭제하고
재가입한다.


두 컬럼의 데이터가 정상적으로 출력된다.

최상위 컴포넌트에서 currentUser사용하고, props로 내려주기

useSession()과 sessionProvider 등을 삭제하고,
서버컴포넌트로 변경한 layout.tsx에서 currentUser를 받고
Navbar와 NavbarItem에 props로 내려준다.

Next.js 13.3.x버전에서는 서버컴포넌트와 클라이언트컴포넌트 사이에서 props를 주고받는것이 지원이 되지 않았지만 13.4.x이후부터는 가능해졌다.

getCurrentUser에서 date부분을 다음과 같이 string으로 변환 해야했다. 지금은 변환안해도 됨..
createdAt: currentUser.createdAt.toISOString()

layout.tsx

import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import './globals.css'
import Navbar from '@/components/Navbar';
import getCurrentUser from './actions/getCurrentUser';

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

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

export default async function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  const currentUser = await getCurrentUser();
  return (
    <html lang="en">
      <body className={inter.className}>
        <Navbar currentUser={currentUser} />
        <main>{children}</main>
      </body>
    </html>
  );
}

Navbar.tsx

'use client'

import Link from "next/link";
import { useState } from "react";
import NavItem from "./NavItem";

interface NavbarProps {
  currentUser?: User | null;
}
const Navbar = ({ currentUser }: NavbarProps) => {
  const [menu, setMenu] = useState(false);

  const handleMenu = () => setMenu(!menu);
  return (
    <nav className="relataive z-10 w-full bg-orange-500 text-white">
      <div className="flex items-center justify-between mx-5 sm:mx-10 lg:mx-20">
        <div className="flex items-center text-2xl h-14">
          <Link href="/">Logo</Link>
        </div>

        {/* sm보다 클때 */}
        <div className="text-2xl sm:hidden">
          {menu === false ? (
            <button onClick={handleMenu}>+</button>
          ) : (
            <button onClick={handleMenu}>-</button>
          )}
        </div>

        <div className="hidden sm:block">
          <NavItem currentUser={currentUser} />
        </div>
      </div>
      <div className="block sm:hidden">
        {menu === false ? null : <NavItem mobile />}
      </div>
    </nav>
  );
}

export default Navbar;

NavItem.tsx

import { User } from '@prisma/client';
import { signIn, signOut } from 'next-auth/react';
import Link from 'next/link'
import React from 'react'

interface NavItemProps {
  mobile?: boolean;
  currentUser?: User | null;
}
const NavItem = ({ mobile, currentUser }: NavItemProps) => {
  return (
    <ul
      className={`text-md justify-center flex gap-4 w-full items-center ${
        mobile && "flex-col h-full"
      }`}
    >
      {currentUser?.userType === "Admin" && (
        <li className="py-2 text-center border-b-4 cursor-pointer">
          <Link href="/admin">Admin</Link>
        </li>
      )}
      {currentUser?.userType === "User" && (
      <li className="py-2 text-center border-b-4 cursor-pointer">
        <Link href="/user">{currentUser?.name}</Link>
      </li>
      )} 
      {currentUser ? (
        <li className="py-2 text-center border-b-4 cursor-pointer">
          <button onClick={() => signOut()}>Sign out</button>
        </li>
      ) : (
        <li className="py-2 text-center border-b-4 cursor-pointer">
          <button onClick={() => signIn()}>Sign in</button>
        </li>
      )}
    </ul>
  );
};

export default NavItem
profile
FE DEV/디블리셔

0개의 댓글

관련 채용 정보