Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Refactor] Board 의 불필요한 재렌더링 줄이기 및 롱폴링 종료 조건 수정 #224

Merged
merged 9 commits into from
Dec 2, 2024
13 changes: 12 additions & 1 deletion apps/client/src/features/project/board/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,18 @@ import { EventResponse, TasksResponse, UpdateDto } from '@/features/project/boar
export const boardAPI = {
getTasks: async (projectId: number) => {
const response = await axiosInstance.get<TasksResponse>(`/task?projectId=${projectId}`);
return response.data.result;

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

항상 필요한 로직이라 분리해두었습니다.

const { version, project } = response.data.result;

return {
version,
sections: project.map((section) => {
return {
...section,
tasks: section.tasks.sort((a, b) => a.position.localeCompare(b.position)),
};
}),
};
},

getEvent: async (projectId: number, version: number, config: AxiosRequestConfig = {}) => {
Expand Down
236 changes: 121 additions & 115 deletions apps/client/src/features/project/board/components/KanbanBoard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -191,128 +191,134 @@ export function KanbanBoard({ projectId }: KanbanBoardProps) {
};

return (
<div className="spazce-x-2 flex h-[calc(100vh-110px)] gap-2 overflow-x-auto p-4">
<AnimatePresence mode="popLayout">
{sections.map((section) => (
<Section
key={section.id}
className={cn(
'flex h-full w-[352px] flex-shrink-0 flex-col items-center',
'bg-transparent',
section.id === belowSectionId && belowTaskId === -1 && 'border-2 border-blue-400'
)}
>
<SectionHeader className="flex w-full items-center justify-between gap-2 space-y-0">
<div className="flex items-center gap-2">
<SectionTitle className="text-xl">{section.name}</SectionTitle>
<SectionCount>{section.tasks.length}</SectionCount>
</div>
<SectionDropdownMenu>
<div className="relative h-full overflow-hidden">
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

외부에 있던 스타일을 위한 div 를 KanbanBoard 내부로 가져오기만 했습니다.

<div className="spazce-x-2 flex h-[calc(100vh-110px)] gap-2 overflow-x-auto p-4">
<AnimatePresence mode="popLayout">
{sections.map((section) => (
<Section
key={section.id}
className={cn(
'flex h-full w-[352px] flex-shrink-0 flex-col items-center',
'bg-transparent',
section.id === belowSectionId && belowTaskId === -1 && 'border-2 border-blue-400'
)}
>
<SectionHeader className="flex w-full items-center justify-between gap-2 space-y-0">
<div className="flex items-center gap-2">
<SectionTitle className="text-xl">{section.name}</SectionTitle>
<SectionCount>{section.tasks.length}</SectionCount>
</div>
<SectionDropdownMenu>
<Button
type="button"
variant="ghost"
className="text-blac hover:text-primary w-full border-none px-0 hover:bg-white"
onClick={() => handleCreateTask(section.id)}
>
<PlusIcon />
Add Task
</Button>
</SectionDropdownMenu>
</SectionHeader>
<SectionContent
key={section.id}
className="flex w-full flex-1 flex-col items-center gap-2 overflow-y-auto pt-1"
onDragOver={(e) => handleDragOver(e, section.id)}
onDragLeave={handleDragLeave}
onDrop={(e) => handleDrop(e, section.id)}
onDragEnd={handleDragEnd}
>
{section.tasks.map((task) => (
<motion.div
key={task.id}
layout
layoutId={task.id.toString()}
draggable
initial={{ opacity: 1, zIndex: 1 }}
animate={{
zIndex: task.id === belowTaskId ? 50 : 1,
scale: task.id === belowTaskId ? 1.02 : 1,
}}
transition={{
layout: { duration: 0.3 },
scale: { duration: 0.2 },
}}
style={{ position: 'relative' }}
onDragStart={(e) =>
handleDragStart(
e as unknown as DragEvent<HTMLDivElement>,
section.id,
task.id
)
}
onDrop={(e) => handleDrop(e, section.id)}
onDragOver={(e) => {
e.preventDefault();
e.stopPropagation();
handleDragOver(e, section.id, task.id);
}}
onDragLeave={handleDragLeave}
>
<Card
className={cn(
'w-56 border bg-white transition-all duration-300 md:w-80',
task.id === belowTaskId && 'border-2 border-blue-400',
'hover:shadow-md'
)}
>
<CardHeader className="flex flex-row items-start gap-2 space-y-0">
<TaskTextarea
taskId={task.id}
initialTitle={task.title}
onTitleChange={handleTitleChange}
/>
<Button
variant="ghost"
type="button"
asChild
className="hover:text-primary px-2 hover:bg-transparent"
>
<Link to={`/${projectId}/board/${task.id}`} className="p-0">
<PanelLeftOpen className="h-6 w-6" />
</Link>
</Button>
</CardHeader>
<CardContent className="flex items-end justify-between">
<div className="flex flex-wrap gap-1">
{task.labels.map((label) => (
<Badge key={label.id} style={{ backgroundColor: label.color }}>
{label.name}
</Badge>
))}
</div>
<AssigneeAvatars assignees={task.assignees} />
</CardContent>
{task.subtasks.total > 0 && (
<CardFooter className="flex items-center justify-between space-y-0">
<SubtaskProgress
total={task.subtasks.total}
completed={task.subtasks.completed}
/>
</CardFooter>
)}
</Card>
</motion.div>
))}
</SectionContent>
<SectionFooter className="w-full">
<Button
type="button"
variant="ghost"
className="text-blac hover:text-primary w-full border-none px-0 hover:bg-white"
className="w-full border-none px-0 text-black"
onClick={() => handleCreateTask(section.id)}
>
<PlusIcon />
Add Task
</Button>
</SectionDropdownMenu>
</SectionHeader>
<SectionContent
key={section.id}
className="flex w-full flex-1 flex-col items-center gap-2 overflow-y-auto pt-1"
onDragOver={(e) => handleDragOver(e, section.id)}
onDragLeave={handleDragLeave}
onDrop={(e) => handleDrop(e, section.id)}
onDragEnd={handleDragEnd}
>
{section.tasks.map((task) => (
<motion.div
key={task.id}
layout
layoutId={task.id.toString()}
draggable
initial={{ opacity: 1, zIndex: 1 }}
animate={{
zIndex: task.id === belowTaskId ? 50 : 1,
scale: task.id === belowTaskId ? 1.02 : 1,
}}
transition={{
layout: { duration: 0.3 },
scale: { duration: 0.2 },
}}
style={{ position: 'relative' }}
onDragStart={(e) =>
handleDragStart(e as unknown as DragEvent<HTMLDivElement>, section.id, task.id)
}
onDrop={(e) => handleDrop(e, section.id)}
onDragOver={(e) => {
e.preventDefault();
e.stopPropagation();
handleDragOver(e, section.id, task.id);
}}
onDragLeave={handleDragLeave}
>
<Card
className={cn(
'w-56 border bg-white transition-all duration-300 md:w-80',
task.id === belowTaskId && 'border-2 border-blue-400',
'hover:shadow-md'
)}
>
<CardHeader className="flex flex-row items-start gap-2 space-y-0">
<TaskTextarea
taskId={task.id}
initialTitle={task.title}
onTitleChange={handleTitleChange}
/>
<Button
variant="ghost"
type="button"
asChild
className="hover:text-primary px-2 hover:bg-transparent"
>
<Link to={`/${projectId}/board/${task.id}`} className="p-0">
<PanelLeftOpen className="h-6 w-6" />
</Link>
</Button>
</CardHeader>
<CardContent className="flex items-end justify-between">
<div className="flex flex-wrap gap-1">
{task.labels.map((label) => (
<Badge key={label.id} style={{ backgroundColor: label.color }}>
{label.name}
</Badge>
))}
</div>
<AssigneeAvatars assignees={task.assignees} />
</CardContent>
{task.subtasks.total > 0 && (
<CardFooter className="flex items-center justify-between space-y-0">
<SubtaskProgress
total={task.subtasks.total}
completed={task.subtasks.completed}
/>
</CardFooter>
)}
</Card>
</motion.div>
))}
</SectionContent>
<SectionFooter className="w-full">
<Button
variant="ghost"
className="w-full border-none px-0 text-black"
onClick={() => handleCreateTask(section.id)}
>
<PlusIcon />
Add Task
</Button>
</SectionFooter>
</Section>
))}
</AnimatePresence>
</SectionFooter>
</Section>
))}
</AnimatePresence>
</div>
</div>
);
}
Expand Down
38 changes: 30 additions & 8 deletions apps/client/src/features/project/board/useBoardStore.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import { create } from 'zustand';
import { Section as TSection, Task, TaskEvent, TaskEventType } from './types';
import { findTask } from './utils';
import { Assignee, Label } from '@/features/types.ts';

interface BoardState {
version: number;
setVersion: (version: number) => void;

export interface BoardState {
sections: TSection[];
setSections: (sections: TSection[]) => void;

Expand All @@ -15,13 +13,13 @@ interface BoardState {
createTask: (sectionId: number, task: Task) => void;
restoreState: (sections: TSection[]) => void;
deleteTask: (taskId: number) => void;

updateTaskAssignees: (taskId: number, assignees: Assignee[]) => void;
updateTaskLabels: (taskId: number, labels: Label[]) => void;
updateTaskSubtasks: (taskId: number, subtasks: { total: number; completed: number }) => void;
}

export const useBoardStore = create<BoardState>((set) => ({
version: 0,

setVersion: (version) => set({ version }),

sections: [] as TSection[],

setSections: (sections) => set({ sections }),
Expand Down Expand Up @@ -125,6 +123,30 @@ export const useBoardStore = create<BoardState>((set) => ({
})),

restoreState: (sections: TSection[]) => set({ sections }),

updateTaskAssignees: (taskId: number, assignees: Assignee[]) =>
set((state) => ({
sections: state.sections.map((section) => ({
...section,
tasks: section.tasks.map((t) => (t.id === taskId ? { ...t, assignees } : t) as Task),
})),
})),

updateTaskLabels: (taskId: number, labels: Label[]) =>
set((state) => ({
sections: state.sections.map((section) => ({
...section,
tasks: section.tasks.map((t) => (t.id === taskId ? { ...t, labels } : t) as Task),
})),
})),

updateTaskSubtasks: (taskId: number, subtasks: { total: number; completed: number }) =>
set((state) => ({
sections: state.sections.map((section) => ({
...section,
tasks: section.tasks.map((t) => (t.id === taskId ? { ...t, subtasks } : t)),
})),
})),
}));

const handleTaskCreated = (sections: TSection[], event: TaskEvent): TSection[] => {
Expand Down
Loading