clerk 조직 기능 활성화

clerk.com 이용해서 클라우드 서비스 이용하는 경우 조직 기능을 사용 할 수 있어요. clerk 대부분 기능을 NextAuth로 대신 할 수 있을텐데 조직은 clerk에만 있어요. 물론 다 스스로 개발하면 조직도 할 수 있겠지만 얼마나 편하고 빠르게 하 르수 있는지에 대한 것이겠죠

Organizations Settings 메뉴에서 활성화 할 수 있어요. 무료에서 3명까지 사용 할 수 있나봐요.

Enable Organizations 눌러서 활성화 합니다.

마지막으로 보튼 누르면 활성화 끝나요. 세부 설정하는 화면 나오는데 아무것도 변경할 필요 없어요.

조직 선택하는 화면을 만들어봅시다.

/app/(platform)/(clerk)/select-org/[[…select-org]]/page.tsx

import { OrganizationList } from "@clerk/nextjs";

export default function CreateOrganizationPage() {
  return (
    <OrganizationList 
    />
  )
}

이렇게 만들면 조직 만드는 화면으로 접속 할 수 있어요. 주소는 http://localhost:3000/select-org 이 됩니다.

CRETAE ORGANIZATION 메뉴로 생성해보면 생성 이후 아무 반응 없을 수 있는데 생성이후 또는 선택 이후 화면을 정의해주지 않아서 그렇습니다. 기존 코드에 옵션을 추가 합니다. 개인 기본 조직은 표시되지 않도록 하는 옵션도 추가합니다.

/app/(platform)/(clerk)/select-org/[[…select-org]]/page.tsx

import { OrganizationList } from "@clerk/nextjs";

export default function CreateOrganizationPage() {
  return (
    <OrganizationList 
      hidePersonal
      afterSelectOrganizationUrl="/organization/:id"
      afterCreateOrganizationUrl="/organization/:id"
    />
  )
}

조직 선택되면 나오는 페이지를 생성합니다.

/app/(platform)/(dashboard)/organization/[organizationId]/page.tsx

const OrganizationIdPage = () => {
  return (
    <div>
      Organization Page!
    </div>
  )
}

export default OrganizationIdPage;

조직을 변경할 수 있는 기능을 추가 할 수 도 있어요.

import { OrganizationSwitcher } from "@clerk/nextjs";

const OrganizationIdPage = () => {
  return (
    <div>
      <OrganizationSwitcher />
    </div>
  )
}

export default OrganizationIdPage;

이제 layout.tsx 파일을 통해서 구조화 해보겠습니다. 여기서부터는 보너스라고 보면 되겠네요. 기본 특을 만들고 나서 저장하고 Navbar를 추가합니다.

/app/(platform)/(dashboard)/layout.tsx

const DashboardLayout = ({
  children
}:{
  children: React.ReactNode
}) => {
  return (
    <div className="h-full">
      {children}
    </div>
  )
}

export default DashboardLayout;

간단한 네비게이션 화면 콤포넌트를 만들고 layout.tsx 파일에 추가해서 확인합니다.

/app/(platform)/(dashboard)/_components/navbar.tsx/navbar.tsx

export const Navbar = () => {
  return (
    <div className="fixed z-50 top-0 w-full h-14 border-b shadow-sm bg-white flex items-center">
      Navbar!
    </div>
  )
}

/app/(platform)/(dashboard)/layout.tsx 파일에 Navbar 추가한 화면입니다.

import { Navbar } from "./_components/navbar";

const DashboardLayout = ({
  children
}:{
  children: React.ReactNode
}) => {
  return (
    <div className="h-full">
      <Navbar />      
      {children}
    </div>
  )
}

export default DashboardLayout;

Navbar를 더 멋지게 추가해봅니다. 모바일과 PC에서 다르게 나오게 하면서도 조직과 유저 아이콘을 추가합니다.

import { Plus } from "lucide-react"
import { Logo } from "@/components/logo"
import { Button } from "@/components/ui/button"
import { OrganizationSwitcher, UserButton } from "@clerk/nextjs"

export const Navbar = () => {
  return (
    <nav className="fixed z-50 px-4 top-0 w-full h-14 border-b shadow-sm bg-white flex items-center">
      {/* Mobile Sidebar */}
      <div className="flex items-center gap-x-4">
        <div className="hidden md:flex">
          <Logo />
        </div>
        <Button size="sm" className="rounded-sm hidden md:block h-auto py-1.5 px-2">
          Create
        </Button>
        <Button size="sm" className="rounded-sm block md:hidden">
          <Plus className="h-4 w-4" />
        </Button>
      </div>
      <div className="ml-auto flex items-center gap-x-2">
        <OrganizationSwitcher 
          hidePersonal
          afterCreateOrganizationUrl="/organization/:id"
          afterSelectOrganizationUrl="/organization/:id"
          afterLeaveOrganizationUrl="/select-org"
          appearance={{
            elements: {
              rootBox: {
                display: "flex",
                justifyContent: "center",
                alignItems: "center"
              }
            }
          }}
        />
        <UserButton 
          afterSignOutUrl="/"
          appearance={{
            elements: {
              avatarBox: {
                height: 30,
                width: 30
              }
            }
          }}
        />
      </div>      
    </nav>
  )
}

로그인하고 로그인 하지 않고 조직 선택 상태에 따라서 페이지로 적절한 페이지로 이동하도록 미들웨어 부분을 코드를 추가합니다.

/middleware.ts

import { authMiddleware, redirectToSignIn } from "@clerk/nextjs";
import { NextResponse } from "next/server";
 
// This example protects all routes including api/trpc routes
// Please edit this to allow other routes to be public as needed.
// See https://clerk.com/docs/references/nextjs/auth-middleware for more information about configuring your Middleware
export default authMiddleware({
  publicRoutes: ["/"],
  afterAuth(auth, req) {
    if(auth.userId && auth.isPublicRoute) {
      let path = '/select-org';

      if (auth.orgId) {
        path = `/organization/${auth.orgId}`;
      }

      const orgSelection = new URL(path, req.url);
      return NextResponse.redirect(orgSelection);
    }

    // 로그인 하지 않았는데 로그인해야 볼 수 있는 페이지에 접근하는 경우
    // 로그인 페이지로 넘기면서 요청 url로 넘겨서 로그인 후 요청 페이지로 이동하게 한다.
    if (!auth.userId && !auth.isPublicRoute) {
      return redirectToSignIn({ returnBackUrl: req.url})
    }

    // 로그인 했지만 조직이 없고 조직 선택화면이 아닌경우 조적 선택 화면으로 이동
    if (!auth.userId && !auth.orgId && req.nextUrl.pathname != '/select-org') {
      const orgSelection = new URL("/select-org", req.url);
      return NextResponse.redirect(orgSelection);
    }
  }
});
 
export const config = {
  matcher: ['/((?!.+\\.[\\w]+$|_next).*)', '/', '/(api|trpc)(.*)'],
};
 

마지막으로 버튼 색상을 추가하기 위해서 shadcn으로 추가했던 버튼에 색상을 추가해봐요.

link 바로 아래 primary 추가합니다.

import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"

import { cn } from "@/lib/utils"

const buttonVariants = cva(
  "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
  {
    variants: {
      variant: {
        default: "bg-primary text-primary-foreground hover:bg-primary/90",
        destructive:
          "bg-destructive text-destructive-foreground hover:bg-destructive/90",
        outline:
          "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
        secondary:
          "bg-secondary text-secondary-foreground hover:bg-secondary/80",
        ghost: "hover:bg-accent hover:text-accent-foreground",
        link: "text-primary underline-offset-4 hover:underline",
        primary: "bg-sky-700 text-primary-foreground hover:bg-sky-700/90",
      },
      size: {
        default: "h-10 px-4 py-2",
        sm: "h-9 rounded-md px-3",
        lg: "h-11 rounded-md px-8",
        icon: "h-10 w-10",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  }
)

export interface ButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof buttonVariants> {
  asChild?: boolean
}

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  ({ className, variant, size, asChild = false, ...props }, ref) => {
    const Comp = asChild ? Slot : "button"
    return (
      <Comp
        className={cn(buttonVariants({ variant, size, className }))}
        ref={ref}
        {...props}
      />
    )
  }
)
Button.displayName = "Button"

export { Button, buttonVariants }

이제 navbar.tsx에서 일반 버튼에서 primary 버튼을 사용하도록 속성을 추가합니다.

<Button size="sm" className="rounded-sm hidden md:block h-auto py-1.5 px-2">

-->
<Button variant="primary" size="sm" className="rounded-sm hidden md:block h-auto py-1.5 px-2">

완성된 navbar.tsx 파일입니다.

import { Plus } from "lucide-react"
import { Logo } from "@/components/logo"
import { Button } from "@/components/ui/button"
import { OrganizationSwitcher, UserButton } from "@clerk/nextjs"

export const Navbar = () => {
  return (
    <nav className="fixed z-50 px-4 top-0 w-full h-14 border-b shadow-sm bg-white flex items-center">
      {/* Mobile Sidebar */}
      <div className="flex items-center gap-x-4">
        <div className="hidden md:flex">
          <Logo />
        </div>
        <Button variant="primary" size="sm" className="rounded-sm hidden md:block h-auto py-1.5 px-2">
          Create
        </Button>
        <Button variant="primary" size="sm" className="rounded-sm block md:hidden">
          <Plus className="h-4 w-4" />
        </Button>
      </div>
      <div className="ml-auto flex items-center gap-x-2">
        <OrganizationSwitcher 
          hidePersonal
          afterCreateOrganizationUrl="/organization/:id"
          afterSelectOrganizationUrl="/organization/:id"
          afterLeaveOrganizationUrl="/select-org"
          appearance={{
            elements: {
              rootBox: {
                display: "flex",
                justifyContent: "center",
                alignItems: "center"
              }
            }
          }}
        />
        <UserButton 
          afterSignOutUrl="/"
          appearance={{
            elements: {
              avatarBox: {
                height: 30,
                width: 30
              }
            }
          }}
        />
      </div>      
    </nav>
  )
}

상단 네비게이션을 완성했어요.

조직을 변경하면 주소창에 있는 주소도 같이 변경되는 것을 볼 수 있는데요. 주소를 메모했다가 붙여넣기로 이동하면 조직은 세션에 있는 조직이 보여서 주소에 있는 조직과 맞지 않는 조직이 표시될 수 있어요.

조직 폴더 아래 콤포넌트 폴더를 만들어서 주소를 인색해서 설정하는 모듈을 생성해봅시다.

/app/(platform)/(dashboard)/organization/[organizationId]/_components/org-control.tsx

"use client";

import { useOrganizationList } from "@clerk/nextjs";
import { useParams } from "next/navigation";
import { useEffect } from "react";

export const OrgControl = () => {
  const params = useParams();
  const { setActive } = useOrganizationList();

  useEffect(() => {
    if (!setActive) return;

    setActive({
      organization: params.organizationId as string,
    });
  }, [setActive, params.organizationId]);
  
  return null;
};

이 모듈이 organization 주소에서 실행되도록 layout.tsx 파일을 새로 추가합니다.

/app/(platform)/(dashboard)/organization/[organizationId]/layout.tsx

import { OrgControl } from "./_components/org-control"

const OrganizationIdlayout = ({
  children
}:{
  children:React.ReactNode
}) => {
  return (
    <>
      <OrgControl />
      {children}
    </>
  )
}

export default OrganizationIdlayout

Leave a Comment