반응형
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)
반응형
'Clone Coding' 카테고리의 다른 글
| Jira-clone - Task View Switcher와 Data Filters 구현 정리 (0) | 2025.11.13 |
|---|---|
| Jira-clone - Task 생성 모달 및 폼 구현 정리 (0) | 2025.11.05 |
| Jira-clone - Project ID 페이지 구현 및 에러/로딩 처리 정리 (0) | 2025.11.05 |
| Jira-clone - Projects 기능 구현 (0) | 2025.10.29 |
| Jira-clone - 멤버 api 빌드 (0) | 2025.10.17 |