본문 바로가기
Clone Coding

Jira-clone - Tasks 기능 구현

by zzuny-code 2025. 11. 5.
반응형

https://www.youtube.com/watch?v=37v63U7-iG0&t=4s

1:15:06

 

 

1. TaskViewSwitcher 컴포넌트 생성

src/features/tasks/component/task-view-switcher.tsx

export const TaskViewSwitcher = () => {
    return(
        <div>
            Tasks
        </div>
    )
}

 

2. ProjectId 페이지에 TaskViewSwitcher 추가

src/app/(dashboard)/workspaces/[workspaceId]/projects/[projectId]/page.tsx

return(
    <div className="flex flex-col gap-y-4">
        ...
        <TaskViewSwitcher/>
    </div>
)

 

3. Tabs 컴포넌트 설치 &  class수정

 bunx --bun shadcn@2.1.0 add tabs

 

src/components/ui/tabs.tsx

<TabsPrimitive.List
    ref={ref}
    className={cn(
      "inline-flex h-9 items-center justify-center rounded-lg bg-transparent p-1 text-muted-foreground gap-x-2",
      className
    )}
    {...props}
/>

<TabsPrimitive.Trigger
    ref={ref}
    className={cn(
      "inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm bg-neutral-100 font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-neutral-200 data-[state=active]:text-black",
      className
    )}
    {...props}
/>

 

4. Appwrite Tasks 테이블 생성

Appwrite Console → Create table → tasks

workspaceId string size: 50, Required
name string size: 256, Required
projectId string size: 50, Required
assigneeId string size: 50, Required
description string size: 2048
dueDate datetime Required
status Enum elements: BACKLOT, TODO, IN_PROGRESS, IN_PREVIEW, DONE, Required
position Integer min: 1000, max: 1000000, Required

 

 

5. 환경 변수 설정

.env.local

NEXT_PUBLIC_APPWRITE_TASKS_ID=tasks

 

config.ts

export const TASKS_ID = process.env.NEXT_PUBLIC_APPWRITE_TASKS_ID ?? "fallback-id";

 

 

 

6. Task 타입 정의

src/features/tasks/types.ts

export enum TaskStatus{
    BACKLOT = "BACKLOT", 
    TODO = "TODO", 
    IN_PROGRESS = "IN_PROGRESS", 
    IN_PREVIEW = "IN_PREVIEW",  
    DONE = "DONE", 
}

 

 

7. Task 스키마 정의

src/features/tasks/schemas.ts

import z from "zod";
import { TaskStatus } from "./types";

export const createTaskSchema = z.object({
    name: z.string().trim().min(1, "Required"),
    status: z.enum(TaskStatus, {error: "Required"}),
    workspaceId: z.string().trim().min(1, "Required"),
    projectId : z.string().trim().min(1, "Required"),
    dueDate: z.coerce.date(),
    assigneeId: z.string().trim().min(1, "Required"),
    description: z.string().optional()
})

 

 

 

8. Tasks API 라우트 생성

src/features/tasks/server/route.ts

import { Hono } from "hono";
import { ID, Query } from "node-appwrite";
import { zValidator } from "@hono/zod-validator";

import { getMember } from "@/features/members/utils";
import { Project } from "@/features/projects/type";

import { DATABASE_ID, PROJECTS_ID, TASKS_ID } from "@/config";
import { sessionMiddleware } from "@/lib/session-middleware";

import { createTaskSchema } from "../schemas";
import z from "zod";
import { TaskStatus } from "../types";
import { createAdminClient } from "@/lib/appwrite";

const app = new Hono()
    .get(
        "/",
        sessionMiddleware,
        zValidator(
            "query", 
            z.object({
                workspaceId: z.string(),
                projectId: z.string().nullish(),
                assigneeId: z.string().nullish(),
                status: z.enum(TaskStatus).nullish(),
                search: z.string().nullish(),
                dueDate: z.string().nullish()
            })
        ),

        async (c) => {
            const {users} = await createAdminClient();
            const databases = c.get("databases");
            const user = c.get("user");

            const {
                workspaceId,
                projectId,
                status,
                search,
                assigneeId,
                dueDate,
            } = c.req.valid("query")

            const member = await getMember({
                databases,
                workspaceId,
                userId: user.$id
            })

            if(!member) {
                return c.json({error: "Unauthorized"}, 401)
            }

            const query = [
                Query.equal("workspaceId", workspaceId),
                Query.orderDesc("$createdAt")
            ]

            if(projectId){
                console.log("projectId", projectId);
                query.push(Query.equal("projectId", projectId))
                
            }

            if(status){
                console.log("status", status);
                query.push(Query.equal("status", status))                
            }

            if(assigneeId){
                console.log("assigneeId", assigneeId);
                query.push(Query.equal("assigneeId", assigneeId))                
            }

            if(dueDate){
                console.log("dueDate", dueDate);
                query.push(Query.equal("dueDate", dueDate))                
            }

            if(search){
                console.log("search", search);
                query.push(Query.equal("name", search))                
            }

            const tasks = await databases.listDocuments(
                DATABASE_ID,
                TASKS_ID,
                query
            )

            const projectIds = tasks.documents.map((task)=> task.projectId);
            const assigneeIds = tasks.documents.map((task)=> task.assigneeId);

            const projects = await databases.listDocuments<Project>(
                DATABASE_ID,
                PROJECTS_ID,
                projectIds.length > 0 ? [Query.contains("$id", projectIds)] : []
            )

            const members = await databases.listDocuments<Project>(
                DATABASE_ID,
                PROJECTS_ID,
                assigneeIds.length > 0 ? [Query.contains("$id", assigneeIds)] : []
            )

            const assignees = await Promise.all(
                members.documents.map( async (member) => {
                    const user = await users.get(member.userId);

                    return{
                        ...member,
                        name:user.name,
                        email:user.email
                    }
                })
            )

            const populatedTasks = tasks.documents.map((task)=>{
                const project = projects.documents.find(
                    (project) => project.$id === task.projectId,
                )

                const assignee = assignees.find(
                    (assignee) => assignee.$id === task.assigneeId,
                )

                return {
                    ...task,
                    project,
                    assignee
                }
            })

            return c.json({
                data:{
                    ...tasks,
                    documents: populatedTasks,
                }
            })
        }
    )

    .post(
        "/",
        sessionMiddleware,
        zValidator("json", createTaskSchema),
        async (c) => {
            const user = c.get("user");
            const databases = c.get("databases")
            const {
                name,
                status,
                workspaceId,
                projectId,
                dueDate,
                assigneeId
            } = c.req.valid("json")

            const member = await getMember({
                databases,
                workspaceId,
                userId: user.$id
            });

            if(!member){
                return c.json({error: "Unauthorized"}, 401)
            }

            const highestPositionTask = await databases.listDocuments(
                DATABASE_ID,
                TASKS_ID,
                [
                    Query.equal("status", status),
                    Query.equal("workspaceId", workspaceId),
                    Query.orderAsc("position"),
                    Query.limit(1),                    
                ]
            )

            const newPosition = 
                highestPositionTask.documents.length > 0
                ? highestPositionTask.documents[0].position + 1000
                : 1000;

            const task = await databases.createDocument(
                DATABASE_ID,
                TASKS_ID,
                ID.unique(),
                {
                    name,
                    status,
                    workspaceId,
                    projectId,
                    dueDate,
                    assigneeId,
                    position: newPosition
                }
            )

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

export default app;

 

9. API 라우트에 Tasks 등록

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

import tasks from '@/features/tasks/server/route'.

const routes = app
    ...
    .route("/tasks", tasks)

 

 

반응형