본문 바로가기
Clone Coding

Jira-clone - 워크스페이스 초대 링크 기능 구현

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

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

07:35:14

 

* "Join Workspace" 버튼을 누르는 부분까지만 작업

 

1. 초대 코드 생성 API 엔드포인트

  • reset-invite-code POST 메서드

src/features/workspaces/server/route.ts 

 

  • ADMIN 권한 확인 → 초대 코드 생성은 관리자만 가능
  • generateInviteCode(6): 임의의 6자리 코드 생성
  • 기존 초대 코드는 무효화되고 새로운 코드로 업데이트
  • 이전 코드로 가입한 사람들은 더 이상 초대 링크 사용 불가

 

.post(
    "/:workspaceId/reset-invite-code",
    sessionMiddleware,
    async (c) => {
        const databases = c.get("databases");
        const user = c.get("user");

        const {workspaceId} = c.req.param();

		// 1. 권한 확인 (ADMIN만 가능)
        const member = await getMember({
            databases,
            workspaceId,
            userId: user.$id,
        })

        if(!member || member.role !== MemberRole.ADMIN){
            return c.json({error:"Unauthorized"}, 401)
        }

		 // 2. 새로운 초대 코드 생성
        const workspace = await databases.updateDocument(
            DATABASE_ID,
            WORKSPACES_ID,
            workspaceId,
            {
                inviteCode: generateInviteCode(6)
            }
        )

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

 

 

2. 클라이언트 훅 - useResetInviteCode

src/features/workspaces/api/use-reset-invite-code.ts

 

  • 초대 코드 재설정 요청 처리
  • 성공 시 두 가지 캐시 무효화 (전체 워크스페이스 + 특정 워크스페이스)
  • 캐시가 무효화되면 UI 자동 갱신

 

import { toast } from 'sonner';
import { InferRequestType, InferResponseType } from 'hono';
import {useMutation, useQueryClient} from '@tanstack/react-query'

import {client} from '@/lib/rpc'

type ResponseType = InferResponseType<typeof client.api.workspaces[':workspaceId']["reset-invite-code"]['$post'], 200>
type RequestType = InferRequestType<typeof client.api.workspaces[':workspaceId']["reset-invite-code"]['$post']>

export const useResetInviteCode = () => {

    const queryClient = useQueryClient()

    const mutation = useMutation<
        ResponseType,
        Error,
        RequestType
    >({

        mutationFn: async({param}) => {
            const response = await client.api.workspaces[':workspaceId']["reset-invite-code"]['$post']({param});

            if(!response.ok){
                throw new Error("Failed to reset invite code")
            }

            return await response.json()
        },
        onSuccess: ({data}) => {            
            toast.success("Invite code reset")
            queryClient.invalidateQueries({queryKey : ['workspaces']})
            queryClient.invalidateQueries({queryKey : ['workspace', data.$id]})
        },
        onError:()=>{
            toast.error("Failed to reset invite code")
        }
    })
    return mutation
}

 

 

3. EditWorkspaceForm에 초대 기능 통합

 

src/features/workspaces/component/edit-workspace-form.tsx

 

url 구성

 

  • window.location.origin: 현재 도메인 (http://localhost:3000)
  • /workspaces/{workspaceId}: 워크스페이스 ID
  • /join/{inviteCode}: 초대 코드

초대 링크 복사 기능

  • navigator.clipboard API 사용

 

'use client'

... 생략

interface EditWorkspaceFormProps{
    onCancel?: () => void;
    initialValues: Workspace
}

export const EditWorkspaceForm = ({onCancel, initialValues} : EditWorkspaceFormProps) =>{
    const router = useRouter()
    const {mutate: resetInviteCode, isPending:isResettingInviteCode} = useResetInviteCode()

    ... 생략


	// ✅ 초대 코드 재설정 확인 다이얼로그
    const [ResetDialog, confirmReset] = useConfirm(
        "Reset invite link",
        "This will invalidate the current invite link",
        "destructive"
    )

	// 초대 링크 재설정 핸들러
    const handleResetInviteCode = async () => {
        const ok = await confirmReset();

        if(!ok) return;

        resetInviteCode({
            param: {workspaceId:initialValues.$id},
        },{
            onSuccess: () => {
                router.refresh() // 페이지 새로고침하여 새 코드 반영
            }
        })
        
    }

	// ✅ 초대 링크 생성
    const fullInviteLink = `${window.location.origin}/workspaces/${initialValues.$id}/join/${initialValues.inviteCode}`

	// 클립보드에 복사
    const handleCopyInviteLink =() => {
        navigator.clipboard.writeText(fullInviteLink)
            .then(()=> toast.success("Invite link copied to clipboard"))
    }

    return(
        <div className="flex flex-col gap-y-4">
            <DeleteDialog/>
            // 추가
            <ResetDialog/>

            <Card className="w-full h-full border-none shadow-none">
            </Card>

			{/* 초대 멤버 카드 */}
            <Card className="w-full h-full border-none shadow-none">
                <CardContent className="p-7">
                    <div className="flex flex-col">
                        <h3 className="font-bold">Invite Members</h3>
                        <p className="text-sm text-muted-foreground">
                            초대 링크를 사용하여 작업 공간에 멤버를 추가하세요
                        </p>
                        
                        {/* 초대 링크 표시 및 복사 */}
                        <div className="mt-4">
                            <div className="flex items-center gap-x-2">
                                <Input disabled value={fullInviteLink}/>
                                <Button 
                                    onClick={handleCopyInviteLink}
                                    variant="secondary"
                                    className="size-12"
                                >
                                    <CopyIcon className="size-5"/>
                                </Button>
                            </div>
                        </div>
                        <DottedSeparator className="py-7"/>
                        
                        {/* 초대 링크 재설정 버튼 */}
                        <Button
                            className="mt-6 w-fit ml-auto"
                            size="sm"  
                            variant="destructive"
                            type="button"
                            disabled={isPending || isResettingInviteCode}    
                            onClick={handleResetInviteCode}                 
                        >
                            Reset invite link
                        </Button>
                    </div>
                </CardContent>
            </Card>

            <Card className="w-full h-full border-none shadow-none">                
            </Card>
        </div>
    )
}

 

 

핵심 정리

POST 엔드포인트: 권한 확인 후 초대 코드 재생성 (API에서 특정 기능을 수행하는 URL 주소)
useResetInviteCode 훅: 초대 코드 재설정 요청 처리
초대 링크 생성: 워크스페이스 ID + 초대 코드 조합
클립보드 복사: navigator.clipboard API 활용
확인 다이얼로그: 실수로 인한 재설정 방지
router.refresh(): 최소한의 데이터만 새로고침
ADMIN 권한: 초대 기능은 관리자만 사용 가능
UI 구분: destructive 버튼으로 비가역적 작업 표시

반응형