Skip to content

Commit 1ddcf27

Browse files
committed
feat: 태스크 드래그 앤 드롭 기능 구현
1 parent 6b05d82 commit 1ddcf27

File tree

4 files changed

+179
-65
lines changed

4 files changed

+179
-65
lines changed

frontend/src/components/backlog/StoryBlock.tsx

Lines changed: 10 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,21 @@
1+
import { MouseEvent } from "react";
12
import { Socket } from "socket.io-client";
23
import { useOutletContext } from "react-router-dom";
3-
import useShowDetail from "../../hooks/pages/backlog/useShowDetail";
4-
import {
5-
BacklogStatusType,
6-
EpicCategoryDTO,
7-
TaskDTO,
8-
} from "../../types/DTO/backlogDTO";
4+
import EpicDropdown from "./EpicDropdown";
95
import BacklogStatusChip from "./BacklogStatusChip";
106
import CategoryChip from "./CategoryChip";
11-
import ChevronDown from "../../assets/icons/chevron-down.svg?react";
12-
import ChevronRight from "../../assets/icons/chevron-right.svg?react";
13-
import TaskContainer from "./TaskContainer";
14-
import TaskHeader from "./TaskHeader";
157
import BacklogStatusDropdown from "./BacklogStatusDropdown";
16-
import useStoryEmitEvent from "../../hooks/pages/backlog/useStoryEmitEvent";
8+
import ConfirmModal from "../common/ConfirmModal";
9+
import useShowDetail from "../../hooks/pages/backlog/useShowDetail";
1710
import useBacklogInputChange from "../../hooks/pages/backlog/useBacklogInputChange";
18-
import { MouseEvent } from "react";
19-
import { MOUSE_KEY } from "../../constants/event";
11+
import useStoryEmitEvent from "../../hooks/pages/backlog/useStoryEmitEvent";
2012
import useDropdownState from "../../hooks/common/dropdown/useDropdownState";
21-
import TrashCan from "../../assets/icons/trash-can.svg?react";
2213
import { useModal } from "../../hooks/common/modal/useModal";
23-
import ConfirmModal from "../common/ConfirmModal";
24-
import EpicDropdown from "./EpicDropdown";
25-
import TaskCreateBlock from "./TaskCreateBlock";
26-
import TaskBlock from "./TaskBlock";
14+
import ChevronRight from "../../assets/icons/chevron-right.svg?react";
15+
import ChevronDown from "../../assets/icons/chevron-down.svg?react";
16+
import TrashCan from "../../assets/icons/trash-can.svg?react";
17+
import { MOUSE_KEY } from "../../constants/event";
18+
import { BacklogStatusType, EpicCategoryDTO } from "../../types/DTO/backlogDTO";
2719

2820
interface StoryBlockProps {
2921
id: number;
@@ -34,9 +26,6 @@ interface StoryBlockProps {
3426
status: BacklogStatusType;
3527
taskExist: boolean;
3628
epicList?: EpicCategoryDTO[];
37-
finished?: boolean;
38-
lastTaskRankValue?: string;
39-
taskList: TaskDTO[];
4029
}
4130

4231
const StoryBlock = ({
@@ -48,9 +37,6 @@ const StoryBlock = ({
4837
status,
4938
taskExist,
5039
epicList,
51-
finished = false,
52-
lastTaskRankValue,
53-
taskList,
5440
}: StoryBlockProps) => {
5541
const { socket }: { socket: Socket } = useOutletContext();
5642
const { showDetail, handleShowDetail } = useShowDetail();
@@ -285,15 +271,6 @@ const StoryBlock = ({
285271
</button>
286272
</div>
287273
)}
288-
{showDetail && (
289-
<TaskContainer>
290-
<TaskHeader />
291-
{...taskList.map((task) => <TaskBlock {...task} />)}
292-
{!finished && (
293-
<TaskCreateBlock storyId={id} {...{ lastTaskRankValue }} />
294-
)}
295-
</TaskContainer>
296-
)}
297274
</>
298275
);
299276
};

frontend/src/components/backlog/DragContainer.tsx renamed to frontend/src/components/backlog/StoryDragContainer.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React, { DragEvent } from "react";
22

3-
interface DragContainerProps {
3+
interface StoryDragContainerProps {
44
index: number;
55
setRef: (index: number) => (element: HTMLDivElement) => void;
66
onDragStart: () => void;
@@ -9,14 +9,14 @@ interface DragContainerProps {
99
children: React.ReactNode;
1010
}
1111

12-
const DragContainer = ({
12+
const StoryDragContainer = ({
1313
index,
1414
setRef,
1515
onDragStart,
1616
onDragEnd,
1717
currentlyDraggedOver,
1818
children,
19-
}: DragContainerProps) => (
19+
}: StoryDragContainerProps) => (
2020
<div
2121
className="relative"
2222
ref={setRef(index)}
@@ -33,4 +33,4 @@ const DragContainer = ({
3333
</div>
3434
);
3535

36-
export default DragContainer;
36+
export default StoryDragContainer;
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { DragEvent } from "react";
2+
3+
interface TaskDragContainerProps {
4+
storyIndex: number;
5+
taskIndex: number;
6+
setRef: (
7+
storyIndex: number,
8+
taskIndex: number
9+
) => (element: HTMLDivElement) => void;
10+
onDragStart: () => void;
11+
onDragEnd: (event: DragEvent) => void;
12+
currentlyDraggedOver: boolean;
13+
children: React.ReactNode;
14+
}
15+
16+
const TaskDragContainer = ({
17+
storyIndex,
18+
taskIndex,
19+
setRef,
20+
onDragStart,
21+
onDragEnd,
22+
currentlyDraggedOver,
23+
children,
24+
}: TaskDragContainerProps) => (
25+
<div
26+
className="relative"
27+
ref={setRef(storyIndex, taskIndex)}
28+
draggable={true}
29+
onDragStart={onDragStart}
30+
onDragEnd={onDragEnd}
31+
>
32+
<div
33+
className={`${
34+
currentlyDraggedOver ? "w-full h-1 bg-blue-400" : ""
35+
} absolute`}
36+
/>
37+
{children}
38+
</div>
39+
);
40+
41+
export default TaskDragContainer;

frontend/src/pages/backlog/UnfinishedStoryPage.tsx

Lines changed: 124 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,29 @@ import changeEpicListToStoryList from "../../utils/changeEpicListToStoryList";
1111
import getDragElementIndex from "../../utils/getDragElementIndex";
1212
import { BacklogSocketData } from "../../types/common/backlog";
1313
import { BacklogDTO } from "../../types/DTO/backlogDTO";
14-
import DragContainer from "../../components/backlog/DragContainer";
14+
import StoryDragContainer from "../../components/backlog/StoryDragContainer";
15+
import TaskBlock from "../../components/backlog/TaskBlock";
16+
import TaskDragContainer from "../../components/backlog/TaskDragContainer";
17+
import TaskContainer from "../../components/backlog/TaskContainer";
18+
import TaskHeader from "../../components/backlog/TaskHeader";
19+
import TaskCreateBlock from "../../components/backlog/TaskCreateBlock";
1520

1621
const UnfinishedStoryPage = () => {
1722
const { socket, backlog }: { socket: Socket; backlog: BacklogDTO } =
1823
useOutletContext();
1924
const { showDetail, handleShowDetail } = useShowDetail();
2025
const [storyElementIndex, setStoryElementIndex] = useState<number>();
26+
const [taskElementIndex, setTaskElementIndex] = useState<{
27+
storyId?: number;
28+
taskIndex?: number;
29+
}>({
30+
storyId: undefined,
31+
taskIndex: undefined,
32+
});
33+
const [draggingStoryId, setDraggingStoryId] = useState<number>();
34+
const [draggingTaskId, setDraggingTaskId] = useState<number>();
2135
const storyComponentRefList = useRef<HTMLDivElement[]>([]);
22-
const draggingComponentIdRef = useRef<number>();
36+
const taskComponentRefList = useRef<HTMLDivElement[][]>([]);
2337
const storyList = useMemo(
2438
() =>
2539
changeEpicListToStoryList(backlog.epicList)
@@ -51,30 +65,44 @@ const UnfinishedStoryPage = () => {
5165
storyComponentRefList.current[index] = element;
5266
};
5367

68+
const setTaskComponentRef =
69+
(storyIndex: number, taskIndex: number) => (element: HTMLDivElement) => {
70+
taskComponentRefList.current[storyIndex][taskIndex] = element;
71+
};
72+
5473
const handleDragOver = (event: DragEvent) => {
74+
if (draggingTaskId) {
75+
return;
76+
}
77+
5578
event.preventDefault();
5679
const index = getDragElementIndex(
5780
storyComponentRefList.current,
58-
draggingComponentIdRef.current,
81+
storyList.findIndex(({ id }) => id === draggingStoryId),
5982
event.clientY
6083
);
6184

6285
setStoryElementIndex(index);
6386
};
6487

6588
const handleDragStart = (id: number) => {
66-
draggingComponentIdRef.current = id;
89+
if (draggingTaskId) {
90+
return;
91+
}
92+
setDraggingStoryId(id);
6793
};
6894

6995
const handleDragEnd = (event: DragEvent) => {
96+
if (draggingTaskId) {
97+
return;
98+
}
99+
70100
event.stopPropagation();
71-
const targetIndex = storyList.findIndex(
72-
({ id }) => id === draggingComponentIdRef.current
73-
);
101+
const targetIndex = storyList.findIndex(({ id }) => id === draggingStoryId);
74102
let rankValue;
75103

76104
if (storyElementIndex === targetIndex) {
77-
draggingComponentIdRef.current = undefined;
105+
setDraggingStoryId(undefined);
78106
setStoryElementIndex(undefined);
79107
return;
80108
}
@@ -96,11 +124,11 @@ const UnfinishedStoryPage = () => {
96124
}
97125

98126
emitStoryUpdateEvent({
99-
id: draggingComponentIdRef.current as number,
127+
id: draggingStoryId as number,
100128
rankValue,
101129
});
102130

103-
draggingComponentIdRef.current = undefined;
131+
setDraggingStoryId(undefined);
104132
setStoryElementIndex(undefined);
105133
};
106134

@@ -113,7 +141,7 @@ const UnfinishedStoryPage = () => {
113141
if (
114142
domain === "story" &&
115143
action === "delete" &&
116-
content.id === draggingComponentIdRef.current
144+
content.id === draggingStoryId
117145
) {
118146
setStoryElementIndex(undefined);
119147
}
@@ -126,6 +154,37 @@ const UnfinishedStoryPage = () => {
126154
};
127155
}, []);
128156

157+
const handleTaskDragOver = (event: DragEvent, storyIndex: number) => {
158+
if (draggingStoryId) {
159+
return;
160+
}
161+
162+
event.preventDefault();
163+
const mouseIndex = storyList[storyIndex].taskList.findIndex(
164+
({ id }) => id === draggingTaskId
165+
);
166+
167+
const index = getDragElementIndex(
168+
taskComponentRefList.current[storyIndex],
169+
mouseIndex,
170+
event.clientY
171+
);
172+
173+
setTaskElementIndex({
174+
storyId: storyList[storyIndex].id,
175+
taskIndex: index,
176+
});
177+
};
178+
179+
const handleTaskDragStart = (taskId: number) => {
180+
setDraggingTaskId(taskId);
181+
};
182+
183+
const handleTaskDragEnd = () => {
184+
setDraggingTaskId(undefined);
185+
setTaskElementIndex({ storyId: undefined, taskIndex: undefined });
186+
};
187+
129188
return (
130189
<div className="flex flex-col items-center gap-4">
131190
<div className="w-full border-b" onDragOver={handleDragOver}>
@@ -138,26 +197,63 @@ const UnfinishedStoryPage = () => {
138197
100
139198
)
140199
: 0;
200+
taskComponentRefList.current[index] = [];
141201

142202
return (
143-
<DragContainer
144-
index={index}
145-
setRef={setStoryComponentRef}
146-
onDragStart={() => handleDragStart(id)}
147-
onDragEnd={handleDragEnd}
148-
currentlyDraggedOver={index === storyElementIndex}
203+
<div
204+
className="relative"
205+
onDragOver={(event) => handleTaskDragOver(event, index)}
149206
>
150-
<StoryBlock
151-
{...{ id, title, point, status, epic, progress, taskList }}
152-
taskExist={taskList.length > 0}
153-
epicList={epicCategoryList}
154-
lastTaskRankValue={
155-
taskList.length
156-
? taskList[taskList.length - 1].rankValue
157-
: undefined
158-
}
159-
/>
160-
</DragContainer>
207+
<StoryDragContainer
208+
index={index}
209+
setRef={setStoryComponentRef}
210+
onDragStart={() => handleDragStart(id)}
211+
onDragEnd={handleDragEnd}
212+
currentlyDraggedOver={index === storyElementIndex}
213+
>
214+
<StoryBlock
215+
key={id}
216+
{...{ id, title, point, status, epic, progress }}
217+
taskExist={taskList.length > 0}
218+
epicList={epicCategoryList}
219+
/>
220+
</StoryDragContainer>
221+
<TaskContainer>
222+
<TaskHeader />
223+
{...taskList.map((task, taskIndex) => (
224+
<TaskDragContainer
225+
storyIndex={index}
226+
taskIndex={taskIndex}
227+
setRef={setTaskComponentRef}
228+
onDragEnd={handleTaskDragEnd}
229+
onDragStart={() => handleTaskDragStart(task.id)}
230+
currentlyDraggedOver={
231+
id === taskElementIndex.storyId &&
232+
taskIndex === taskElementIndex.taskIndex
233+
}
234+
>
235+
<TaskBlock key={task.id} {...task} />
236+
</TaskDragContainer>
237+
))}
238+
<div
239+
ref={setTaskComponentRef(index, taskList.length)}
240+
className={`${
241+
id === taskElementIndex.storyId &&
242+
taskElementIndex.taskIndex === taskList.length
243+
? "w-[67.9rem] h-1 bg-blue-400"
244+
: ""
245+
} absolute`}
246+
/>
247+
<TaskCreateBlock
248+
storyId={id}
249+
lastTaskRankValue={
250+
taskList.length
251+
? taskList[taskList.length - 1].rankValue
252+
: undefined
253+
}
254+
/>
255+
</TaskContainer>
256+
</div>
161257
);
162258
}
163259
)}

0 commit comments

Comments
 (0)