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