반응형
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 삭제 후 테스트




반응형
'Clone Coding' 카테고리의 다른 글
| Jira-clone - Task 관리 기능 구현 (삭제/수정/네비게이션) (0) | 2025.11.21 |
|---|---|
| Jira-clone - TanStack Table로 작업 관리 테이블 만들기 (0) | 2025.11.19 |
| Jira-clone - Task 생성 모달 및 폼 구현 정리 (0) | 2025.11.05 |
| Jira-clone - Tasks 기능 구현 (0) | 2025.11.05 |
| Jira-clone - Project ID 페이지 구현 및 에러/로딩 처리 정리 (0) | 2025.11.05 |