본문 바로가기
Clone Coding

Jira-clone - 워크스페이스 삭제(Delete) 기능 구현

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

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

7:12:05

 

1. DELETE API 엔드포인트

  • 워크스페이스 삭제 DELETE 메서드

src/features/workspaces/server/route.ts

 

  • 권한 검증: ADMIN만 삭제 가능 → 무단 삭제 방지
  • 캐스케이드 삭제 예정: 워크스페이스 삭제 시 연관된 멤버, 프로젝트, 작업도 함께 삭제 (현재 TODO)
  • 문서 삭제: 데이터베이스에서 워크스페이스 문서 제거

 

.delete(
    "/:workspaceId",
    sessionMiddleware,
    async (c) => {
        const databases = c.get("databases");
        const user = c.get("user");

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

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

        if(!member || member.role !== MemberRole.ADMIN){
            return c.json({error:"Unauthorized"}, 401)
        }
        
        // TODO: Delete members, projects, and tasks
        
        // 2. 워크스페이스 삭제
        await databases.deleteDocument(
            DATABASE_ID,
            WORKSPACES_ID,
            workspaceId,
        )

        return c.json({data:{ $id: workspaceId}});
    }
)

 

 

2. 클라이언트 훅 - useDeleteWorkspace

  • React Query를 사용한 mutation 훅

src/features/workspaces/api/use-delete-workspace.ts

 

  • 삭제 성공 시 두 가지 쿼리 캐시 무효화
  • 삭제된 워크스페이스는 목록에서 제거됨

 

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

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

type ResponseType = InferResponseType<typeof client.api.workspaces[":workspaceId"]['$delete'], 200>
type RequestType = InferRequestType<typeof client.api.workspaces[":workspaceId"]['$delete']>


export const useDeleteWorkspace = () => {
    const queryClient = useQueryClient()

    const mutation = useMutation<ResponseType, Error, RequestType>({  
        mutationFn: async({param}) => {
            const response = await client.api.workspaces[':workspaceId']['$delete']({param});

            if(!response.ok){
                throw new Error("Failed to delete Workspace")
            }

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

 

 

3. 확인 다이얼로그 커스텀 훅

  • useConfirm 훅 - 재사용 가능한 확인 창( 사용자가 중요한 작업(삭제 등)을 수행하기 전에 확인 창을 띄우는 훅)
// src/hooks/use-confirm.tsx

import { ResponsiveModal } from "@/components/responsive-modal"
import { Button, ButtonProps } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle, CardDescription,} from "@/components/ui/card"
import { useState } from "react"

export const useConfirm = (
    title: string,
    message: string,
    variant: ButtonProps["variant"] = "primary"
): [() => JSX.Element, () => Promise<unknown>] => {

    const [promise, setPromise] = useState<{
        resolve: (value: boolean) => void
    } | null>(null)

    // 새로운 확인 프롬프트 생성
    const confirm = () => {
        return new Promise((resolve) => {
            setPromise({resolve})
        })
    }

    const handleClose = () => {
        setPromise(null)
    }

    const handleConfirm = () => {
        promise?.resolve(true);   // true 반환
        handleClose();
    }

    const handleCancel = () => {
        promise?.resolve(false);  // false 반환
        handleClose();
    }

    // JSX 컴포넌트
    const ConfirmationDialog = () => (
        <ResponsiveModal open={promise !== null} onOpenChange={handleClose}>
            <Card className="w-full h-full border-none shadow-none">
                <CardContent className="pt-8">
                    <CardHeader className="p-0">
                        <CardTitle>{title}</CardTitle>
                        <CardDescription>{message}</CardDescription>
                    </CardHeader>
                    <div className="pt-4 w-full flex gap-y-2 lg:flex-row gap-x-2 items-center justify-end">
                        <Button onClick={handleCancel} variant="outline" className="w-full lg:w-auto">
                            Cancel
                        </Button>
                        <Button onClick={handleConfirm} variant={variant} className="w-full lg:w-auto">
                            Confirm
                        </Button>
                    </div>
                </CardContent>
            </Card>
        </ResponsiveModal>
    )


    return [ConfirmationDialog, confirm]
}

 

 

4. EditWorkspaceForm에 삭제 기능 통합

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

export const EditWorkspaceForm = ({onCancel, initialValues} : EditWorkspaceFormProps) => {
    const router = useRouter()
    const {mutate, isPending} = useUpdateWorkspace();
    const {mutate: deleteWorkspace, isPending: isDeletingWorkspace} = useDeleteWorkspace()

    // ✅ 확인 다이얼로그 생성
    const [DeleteDialog, confirmDelete] = useConfirm(
        "Delete Workspace",
        "This action cannot be undone",
        "destructive"
    )

    const handleDelete = async () => {
        // 1. 사용자 확인 대기
        const ok = await confirmDelete();

        if(!ok) return;

        // 2. 삭제 요청
        deleteWorkspace({
            param: {workspaceId: initialValues.$id},
        }, {
            onSuccess: () => {
                // 3. 워크스페이스 없으면 생성 페이지로 리다이렉트
                window.location.href = "/"
            }
        })
    }

    return(
        <div className="flex flex-col gap-y-4">
            <DeleteDialog/>  {/* 확인 다이얼로그 렌더링 */}
            
            {/* 기본 정보 수정 카드 */}
            <Card>
                {/* ... 이름, 이미지 수정 ... */}
            </Card>

            {/* Danger Zone - 삭제 영역 */}
            <Card className="w-full h-full border-none shadow-none">
                <CardContent className="p-7">
                    <div className="flex flex-col">
                        <h3 className="font-bold">Danger Zone</h3>
                        <p className="text-sm text-muted-foreground">
                            작업 공간 삭제는 되돌릴 수 없으며 연관된 모든 데이터가 제거됩니다.
                        </p>
                        <Button
                            className="mt-6 w-fit ml-auto"
                            size="sm"  
                            variant="destructive"
                            type="button"
                            disabled={isPending || isDeletingWorkspace}    
                            onClick={handleDelete}                
                        >
                            Delete Workspace
                        </Button>
                    </div>
                </CardContent>
            </Card>
        </div>
    )
}

 

 

4. EditWorkspaceForm에 삭제 기능 통합

 

src/features/workspaces/components/edit-workspace-form.tsx 삭제 버튼 구현

export const EditWorkspaceForm = ({onCancel, initialValues} : EditWorkspaceFormProps) => {
    const router = useRouter()
    const {mutate, isPending} = useUpdateWorkspace();
    const {mutate: deleteWorkspace, isPending: isDeletingWorkspace} = useDeleteWorkspace()

    // ✅ 확인 다이얼로그 생성
    const [DeleteDialog, confirmDelete] = useConfirm(
        "Delete Workspace",
        "This action cannot be undone",
        "destructive"
    )

    const handleDelete = async () => {
        // 1. 사용자 확인 대기
        const ok = await confirmDelete();

        if(!ok) return;

        // 2. 삭제 요청
        deleteWorkspace({
            param: {workspaceId: initialValues.$id},
        }, {
            onSuccess: () => {
                // 3. 워크스페이스 없으면 생성 페이지로 리다이렉트
                window.location.href = "/"
            }
        })
    }

    return(
        <div className="flex flex-col gap-y-4">
            <DeleteDialog/>  {/* 확인 다이얼로그 렌더링 */}
            
            {/* 기본 정보 수정 카드 */}
            <Card>
                {/* ... 이름, 이미지 수정 ... */}
            </Card>

            {/* Danger Zone - 삭제 영역 */}
            <Card className="w-full h-full border-none shadow-none">
                <CardContent className="p-7">
                    <div className="flex flex-col">
                        <h3 className="font-bold">Danger Zone</h3>
                        <p className="text-sm text-muted-foreground">
                            작업 공간 삭제는 되돌릴 수 없으며 연관된 모든 데이터가 제거됩니다.
                        </p>
                        <Button
                            className="mt-6 w-fit ml-auto"
                            size="sm"  
                            variant="destructive"
                            type="button"
                            disabled={isPending || isDeletingWorkspace}    
                            onClick={handleDelete}                
                        >
                            Delete Workspace
                        </Button>
                    </div>
                </CardContent>
            </Card>
        </div>
    )
}

 

! window.location.href 와 router.push() 차이

// ❌ router.push()만 사용하면 문제
router.push("/")
// → 캐시된 워크스페이스 데이터가 남아있을 수 있음

// ✅ window.location.href 사용
window.location.href = "/"
// → 페이지 완전히 새로고침
// → 모든 상태/캐시 초기화
// → 워크스페이스 목록이 비어 있으면 생성 페이지로 자동 리다이렉트

 

 

6. Tailwind 설정 업데이트

 

tailwind.config.ts

 

  • use-confirm.tsx 훅 내부의 Tailwind 클래스들이 빌드에 포함되어야 함
  • 이 파일을 스캔하지 않으면 스타일이 누락될 수 있음

 

const config: Config = {
    content: [
        "./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
        "./src/components/**/*.{js,ts,jsx,tsx,mdx}",
        "./src/app/**/*.{js,ts,jsx,tsx,mdx}",
        "./src/features/**/*.{js,ts,jsx,tsx,mdx}",
        "./src/hooks/**/*.{js,ts,jsx,tsx,mdx}",  // ✅ 추가
    ],
    // ...
}

 

 

7. DELETE HTTP 메서드 등록

 

src/app/api/[[...route]]/route.ts

 

  • API 엔드포인트가 존재해도 HTTP 메서드를 export하지 않으면 405 에러 발생
  • GET, POST, PATCH와 함께 DELETE도 명시적으로 내보내야 함

 

export const GET = handle(routes)
export const POST = handle(routes)
export const PATCH = handle(routes)
export const DELETE = handle(routes)  // ✅ DELETE 메서드 추가

export type AppType = typeof routes

 

 

 

흐름

DELETE 엔드포인트: 권한 확인 후 워크스페이스 삭제
useDeleteWorkspace 훅: 삭제 요청 및 캐시 관리
useConfirm 훅: 재사용 가능한 확인 다이얼로그
Promise 기반: async/await로 깔끔한 제어 흐름
Danger Zone UI: 비가역적 작업임을 시각적 강조
window.location.href: 삭제 후 완전한 상태 초기화
Tailwind 설정: 훅 폴더 추가로 스타일 포함
DELETE 메서드: API 라우트에서 명시적 export

반응형