본문 바로가기
Clone Coding

Jira-clone - 워크스페이스 멤버십 시스템 만들기

by zzuny-code 2025. 10. 2.
반응형

https://www.youtube.com/watch?v=Av9C7xlV0fA

05:22:41

 

1. Members 테이블 컬럼 구성

Appwrite에서 members 테이블을 생성하고 다음 컬럼들을 추가

  • userId: 사용자 ID (string, 50자, 필수)
  • workspaceId: 워크스페이스 ID (string, 50자, 필수)
  • role: 역할 (Enum: ADMIN/MEMBER, 필수)

 

  • Users와 Workspaces를 연결하는 중간 테이블 (Many-to-Many 관계)
  • 한 사용자가 여러 워크스페이스에 속할 수 있고, 각각 다른 역할을 가질 수 있음

 

 

2. 환경 변수 설정 (.env.local), Config 파일 (src/config.ts)

.env.local

NEXT_PUBLIC_APPWRITE_MEMBERS_ID=members

 

src/config.ts

export const APPWRITE_ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT!;
export const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT!;

export const DATABASE_ID = process.env.NEXT_PUBLIC_APPWRITE_DATABASE_ID!;
export const WORKSPACES_ID = process.env.NEXT_PUBLIC_APPWRITE_WORKSPACES_ID!;
export const MEMBERS_ID = process.env.NEXT_PUBLIC_APPWRITE_MEMBERS_ID!;
export const IMAGES_BUCKET_ID = process.env.NEXT_PUBLIC_APPWRITE_IMAGES_ID!;

 

3. MemberRole Enum (src/features/members/type.ts)

 

  • TypeScript enum으로 역할 타입 정의
  • 문자열 enum: 값 자체가 "ADMIN", "MEMBER"

 

 

export enum MemberRole {
    ADMIN = "ADMIN",
    MEMBER = "MEMBER"
}

 

4. Workspace 생성 API - POST (src/features/workspaces/server/route.ts)

 

  • Workspaces: 워크스페이스 자체의 정보 (이름, 이미지 등)
  • Members: "누가 어떤 워크스페이스의 멤버인가" 관계 정보

 

import { zValidator } from "@hono/zod-validator";
import { Hono } from "hono";
import { createWorkspaceSchema } from "../schemas";
import { sessionMiddleware } from "@/lib/session-middleware";
import { DATABASE_ID, IMAGES_BUCKET_ID, MEMBERS_ID, WORKSPACES_ID } from "@/config";
import { ID } from "node-appwrite";

const app = new Hono()
    .get(
        ...
    )
    .post(
        "/",
        zValidator("form", createWorkspaceSchema),
        sessionMiddleware,
        async (c) => {
            const databases = c.get("databases")
            // storage 불러오고
            const storage = c.get("storage")
            const user = c.get("user")
            
            const {name, image} = c.req.valid("form")

            let uploadedImageUrl: string | undefined;

            console.log('data', image);

            if(image instanceof File){
                const file = await storage.createFile(
                    IMAGES_BUCKET_ID,
                    ID.unique(),
                    image
                );

                uploadedImageUrl = file.$id                
            }

            const workspace = await databases.createDocument(
                DATABASE_ID,
                WORKSPACES_ID,
                ID.unique(),
                {
                    name,
                    userId:user.$id,
                    imageUrl: uploadedImageUrl
                }
            );


            // 워크스페이스 생성자를 자동으로 ADMIN 멤버로 등록
            await databases.createDocument(
               DATABASE_ID,
               MEMBERS_ID,
               ID.unique(),
               {
                   userId: user.$id,           // 생성한 사람
                   workspaceId: workspace.$id, // 방금 만든 워크스페이스
                   role: MemberRole.ADMIN      // ADMIN 역할 부여
               }
           )

            
            

            return c.json({data:workspace})
        }
    )

export default app

 

appwrite workspaces 목록 삭제하고 다시 등록

workspaces목록의 workspacesId와 userId가 members의 column과 동일함

 

 

5. Workspace 목록 조회 API - GET

src/features/workspaces/server/route.ts

 get에서 member list도 불러온다? 가 맞나?

import { zValidator } from "@hono/zod-validator";
import { Hono } from "hono";
import { createWorkspaceSchema } from "../schemas";
import { sessionMiddleware } from "@/lib/session-middleware";
import { DATABASE_ID, IMAGES_BUCKET_ID, MEMBERS_ID, WORKSPACES_ID } from "@/config";
import { ID, Query } from "node-appwrite";
import { MemberRole } from "@/features/members/type";

const app = new Hono()
    .get(
        "/",
        sessionMiddleware,
        async (c) => {
            const user = c.get("user")
            const databases = c.get("databases")

            // 1단계: 현재 사용자의 멤버십 찾기
            const members = await databases.listDocuments(
                DATABASE_ID,
                MEMBERS_ID,
                [Query.equal("userId", user.$id)]
            );

            // 2단계: 멤버십이 없으면 빈 배열 반환
            if(members.total === 0){
                return c.json({data: {documents: [], total:0}})
            }

            // 3단계: 멤버십에서 워크스페이스 ID들 추출
            const workspaceIds = members.documents.map((member) => member.workspaceId)

            // 4단계: 해당 워크스페이스들만 조회
            const workspace = await databases.listDocuments(
                DATABASE_ID,
                WORKSPACES_ID,
                [
                    Query.orderDesc("$createdAt"),      // 최신순 정렬
                    Query.contains("$id", workspaceIds) // ID가 목록에 있는 것만
                ]
            );

            return c.json({data:workspace})
        }
    )
    .post(
        ...
    )

export default app

 

 

  • 사용자 A: 자기가 속한 워크스페이스만 보임
  • 사용자 B: 다른 사람의 워크스페이스는 안 보임 (권한 격리)

 

 

 

6. 로그아웃 시 캐시 무효화 (src/features/auth/api/use-logout.ts)

 

  • React Query 캐시에서 "current" (현재 사용자 정보) 무효화
  • 다음에 current를 조회하면 새로 fetch함
 onSuccess: () => {
    toast.success("Logged out")
    router.refresh()
    queryClient.invalidateQueries({queryKey :["current"]}) // 데이터를 강제로 "무효화"시켜서 다시 가져온다.
    queryClient.invalidateQueries({queryKey :["workspaces"]}) 
    // window.location.reload()
},

 

 

 

7. 초대 코드 생성 유틸 (src/lib/utils.ts)

src/lib/utils.ts

export function generateInviteCode(length: number){
  const characters = 'ABCDEFGHIJKLMOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';

  let result = '';
  for(let i=0; i <length; i++){
    result += characters.charAt(Math.floor(Math.random()*characters.length))
  }
  return result
}

 

8. Workspace 생성 시 초대 코드 추가

src/features/workspaces/server/route.ts

 

  • 워크스페이스 생성할 때 자동으로 10자리 초대 코드 생성
  • 이 코드를 나중에 다른 사람에게 공유하면 워크스페이스에 초대할 수 있음

 

import { zValidator } from "@hono/zod-validator";
import { Hono } from "hono";
import { createWorkspaceSchema } from "../schemas";
import { sessionMiddleware } from "@/lib/session-middleware";
import { DATABASE_ID, IMAGES_BUCKET_ID, MEMBERS_ID, WORKSPACES_ID } from "@/config";
import { ID } from "node-appwrite";

const app = new Hono()
    .get(
        ...
    )
    .post(
        ...
        const workspace = await databases.createDocument(
            DATABASE_ID,
            WORKSPACES_ID,
            ID.unique(),
            {
                name,
                userId:user.$id,
                imageUrl: uploadedImageUrl,
                inviteCode: generateInviteCode(10)  // 추가!
            }
        );
            return c.json({data:workspace})
        }
    )

export default app


appwrite의 workspaces 에 inviteCode column추가 후 members와 workspace목록 삭제.

재등록하면 inviteCode가 생성됨

 

반응형