본문 바로가기
Clone Coding

Jira-clone - Task View Switcher와 Data Filters 구현 정리

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

https://www.youtube.com/watch?v=37v63U7-iG0

2:11:16

 

* 주요기능

3가지 뷰: Table, Kanban, Calendar 전환
4가지 필터: Status, Assignee, Project, Due Date
URL 상태 관리: 새로고침해도 선택한 뷰와 필터 유지
실시간 필터링: 필터 변경 시 즉시 작업 목록 업데이트
로딩 상태: 데이터 로딩 중 스피너 표시

 

기본 Task View Switcher 구현

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

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

export const TaskViewSwitcher = () => {
    // 1. 현재 워크스페이스 ID 가져오기
    const workspaceId = useWorkspaceId()
    
    // 2. 해당 워크스페이스의 작업 목록 가져오기
    const { data:tasks, isLoading: isLoadingTasks } = useGetTasks({workspaceId})

    const {open} = useCreateTaskModal();
    
    return (
        <Tabs className="flex-1 w-full border rounded-lg">
            {/* 탭 영역 */}
            <TabsContent value="table">
                {JSON.stringify(tasks)} {/* tasks 데이터 반환 */}
            </TabsContent>
            <TabsContent value="kanban">
                {JSON.stringify(tasks)}
            </TabsContent>
            <TabsContent value="calendar">
                {JSON.stringify(tasks)}
            </TabsContent>
        </Tabs>
    )
}
```

### 데이터 흐름
```
URL: /workspaces/6901bdd400275a8b8473/projects/6901d9fe0011c25d2854
    ↓
1. useWorkspaceId() → URL에서 워크스페이스 ID 추출
    ↓
2. useGetTasks({workspaceId}) → API로 작업 데이터 요청
    ↓
3. data:tasks → 데이터를 tasks 변수에 저장 (구조 분해 할당 + 이름 변경)
    ↓
4. {JSON.stringify(tasks)} → 화면에 표시

 

Tab Title을 URL로 관리하기

  • URL이 /workspaces/.../projects/...?task-view=table 형태로 관리됨
  • 새로고침해도 선택한 뷰가 유지됨 (URL에 상태 저장)
  • Table → Kanban으로 변경하면 ?task-view=kanban으로 URL 자동 변경
const [view, setView] = useQueryState("task-view", { 
    defaultValue: "table" 
})

<Tabs 
    defaultValue={view} 
    onValueChange={setView} 
    className="flex-1 w-full border rounded-lg"
>

 

 

 

 

 

로딩 상태 추가

데이터 로딩 중일 때 스피너 표시, 완료되면 데이터 표시

{isLoadingTasks ? (
    <div className="w-full border rounded-lg h-[200px] flex flex-col items-center justify-center">
        <Loader className="size-5 animate-spin text-muted-foreground"/>
    </div>
) : (
    <>
        <TabsContent value="table" className="mt-0">
            {JSON.stringify(tasks)}
        </TabsContent>
        {/* ... 나머지 탭들 */}
    </>
)}

 

 

API에 필터 파라미터 추가

src/features/tasks/api/use-get-tasks.ts

작업 목록을 가져올 때 프로젝트, 상태, 담당자, 마감일 등으로 필터링할 수 있게 준비

interface useGetTasksProps{
    workspaceId: string;
    projectId?: string | null;      // 추가
    status?: TaskStatus | null;     // 추가
    search?: string | null;         // 추가
    assigneeId?: string | null;     // 추가
    dueDate?: string | null;        // 추가
}

export const useGetTasks = ({
    workspaceId,
    projectId,
    status,
    search,
    assigneeId,
    dueDate
}: useGetTasksProps) => {
    const query = useQuery({
        queryKey: [
            "tasks", 
            workspaceId,
            projectId,      // 쿼리 키에 추가
            status,
            search,
            assigneeId,
            dueDate
        ],
        queryFn: async () => {
            // API 호출 시 이 파라미터들을 전달
        }
    })
}

 

 

Data Filters 컴포넌트 생성

src/features/tasks/component/data-filters.tsx 생성

import { Select, SelectItem, SelectSeparator, SelectTrigger, SelectValue } from "@/components/ui/select";
import { useGetMembers } from "@/features/members/api/use-get-members";
import { useGetProjects } from "@/features/projects/api/use-get-projects";
import { useWorkspaceId } from "@/features/workspaces/hooks/use-workspace-id"
import { SelectContent, Value } from "@radix-ui/react-select";
import { ListCheckIcon } from "lucide-react";
import { TaskStatus } from "../types";

interface DataFiltersProps {
    hideProjectFilter? :boolean
}

export const DataFilters = ({hideProjectFilter}: DataFiltersProps) => {

    const workspaceId = useWorkspaceId();

    const {data: projects, isLoading: isLoadingProjects} = useGetProjects({workspaceId})
    const {data: members, isLoading: isLoadingMembers} =useGetMembers({workspaceId})

    const isLoading = isLoadingProjects || isLoadingMembers;

    const projectOptions = projects?.documents.map((project) => ({
        value: project.$id,
        label: project.name
    }))

    const memberOptions = members?.documents.map((member)=>({
        Value: member.$id,
        label: member.name 
    }))

    if(isLoading) return null;

    return(
        <div className="flex flex-col lg:flex-row gap-2">
            <Select
                defaultValue={undefined}
                onValueChange={()=>{}}
            >
                <SelectTrigger className="w-full lg:w-auto h-8">
                    <div className="flex items-center pr-2">
                        <ListCheckIcon className="size-4 mr-2"/>
                        <SelectValue placeholder="All statuses"/>
                    </div>
                </SelectTrigger>
                <SelectContent className="bg-white">
                    <SelectItem value="al">All statuses</SelectItem>
                    <SelectSeparator/>
                    <SelectItem value={TaskStatus.BACKLOT}>Backlog</SelectItem>
                    <SelectItem value={TaskStatus.IN_PROGRESS}>In Progress</SelectItem>
                    <SelectItem value={TaskStatus.IN_PREVIEW}>In Review</SelectItem>
                    <SelectItem value={TaskStatus.TODO}>Todo</SelectItem>
                    <SelectItem value={TaskStatus.DONE}>Done</SelectItem>
                </SelectContent>
            </Select>
        </div>
    )
}

 

Task View Switcher에 DataFilters 적용

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

 

<DottedSeparator className="my-4"/>
    <DataFilters/>
<DottedSeparator className="my-4"/>

 

 

 

 

필터 상태를 URL로 관리하기

src/features/tasks/hooks/use-task-filters.ts

- 모든 필터 값을 URL 쿼리 파라미터로 관리

import { parseAsString, parseAsStringEnum, useQueryStates } from "nuqs"

import { TaskStatus } from "../types"

export const useTaskFilters = () => {
    return useQueryStates({
        projectId: parseAsString,
        status: parseAsStringEnum(Object.values(TaskStatus)),
        assigneeId: parseAsString,
        search: parseAsString,
        dueDate: parseAsString
    })
}

 

DataFilters에서 필터 상태 연결

src/features/tasks/component/data-filters.tsx

 

  • Select를 선택하면 URL에 필터 값이 추가됨
  • 예: http://localhost:3000/workspaces/.../projects/...?task-view=table&status=IN_PROGRESS
  • 새로고침해도 선택한 필터 값을 유지함

 

// 수정한 부분

const [{
    status,
    assigneeId,
    projectId,
    dueDate
}, setFilters] = useTaskFilters();

const onStatusChange = (value:string) => {
    setFilters({ status: value === "all" ? null : value as TaskStatus})
}

...

<Select
    defaultValue={status ?? undefined}
    onValueChange={(value)=> onStatusChange(value)}
>
    ...
</Select>

 

 

 

완성된 DataFilters 컴포넌트

 

 

src/features/tasks/component/data-filters.tsx

export const DataFilters = ({hideProjectFilter}: DataFiltersProps) => {
    // 현재 URL의 필터 값들 가져오기
    const [{status, assigneeId, projectId, dueDate}, setFilters] = useTaskFilters();

    // 각 필터 변경 핸들러
    const onStatusChange = (value:string) => {
        setFilters({ status: value === "all" ? null : value as TaskStatus})
    }
    const onAssigneeChange = (value:string) => {
        setFilters({ assigneeId: value === "all" ? null : value as string})
    }
    const onProjectChange = (value:string) => {
        setFilters({ projectId: value === "all" ? null : value as string})
    }

    return (
        <div className="flex flex-col lg:flex-row gap-2">
            {/* 1. Status 필터 */}
            <Select defaultValue={status ?? undefined} onValueChange={onStatusChange}>
                <SelectItem value="all">All statuses</SelectItem>
                <SelectItem value={TaskStatus.BACKLOT}>Backlog</SelectItem>
                {/* ... */}
            </Select>

            {/* 2. Assignee 필터 */}
            <Select defaultValue={assigneeId ?? undefined} onValueChange={onAssigneeChange}>
                <SelectItem value="all">All assignees</SelectItem>
                {memberOptions?.map((member) => (
                    <SelectItem key={member.value} value={member.value}>
                        {member.label}
                    </SelectItem>
                ))}
            </Select>

            {/* 3. Project 필터 */}
            <Select defaultValue={projectId ?? undefined} onValueChange={onProjectChange}>
                <SelectItem value="all">All projects</SelectItem>
                {projectOptions?.map((project) => (
                    <SelectItem key={project.value} value={project.value}>
                        {project.label}
                    </SelectItem>
                ))}
            </Select>

            {/* 4. Due Date 필터 */}
            <DatePicker
                value={dueDate ? new Date(dueDate) : undefined}
                onChange={(date) => {
                    setFilters({ dueDate: date ? date.toISOString() : null})
                }}
            />
        </div>
    )
}
```

---

##  최종 결과

### 전체 동작 흐름
```
1. 사용자가 필터 선택 (예: Status → "In Progress")
   ↓
2. URL 자동 업데이트: ?status=IN_PROGRESS
   ↓
3. useTaskFilters()가 URL 변경 감지
   ↓
4. useGetTasks()가 새로운 필터 값으로 API 재호출
   ↓
5. 필터링된 작업 목록 표시

 

appwrite의 모든 projects, task 삭제 후 테스트

 

 

 

 

반응형