diff --git a/frontend/src/components/backlog/StoryBlock.tsx b/frontend/src/components/backlog/StoryBlock.tsx
index 6bc9e0d..fab109e 100644
--- a/frontend/src/components/backlog/StoryBlock.tsx
+++ b/frontend/src/components/backlog/StoryBlock.tsx
@@ -1,24 +1,21 @@
+import { MouseEvent } from "react";
import { Socket } from "socket.io-client";
import { useOutletContext } from "react-router-dom";
-import useShowDetail from "../../hooks/pages/backlog/useShowDetail";
-import { BacklogStatusType, EpicCategoryDTO } from "../../types/DTO/backlogDTO";
+import EpicDropdown from "./EpicDropdown";
import BacklogStatusChip from "./BacklogStatusChip";
import CategoryChip from "./CategoryChip";
-import ChevronDown from "../../assets/icons/chevron-down.svg?react";
-import ChevronRight from "../../assets/icons/chevron-right.svg?react";
-import TaskContainer from "./TaskContainer";
-import TaskHeader from "./TaskHeader";
import BacklogStatusDropdown from "./BacklogStatusDropdown";
-import useStoryEmitEvent from "../../hooks/pages/backlog/useStoryEmitEvent";
+import ConfirmModal from "../common/ConfirmModal";
+import useShowDetail from "../../hooks/pages/backlog/useShowDetail";
import useBacklogInputChange from "../../hooks/pages/backlog/useBacklogInputChange";
-import { MouseEvent } from "react";
-import { MOUSE_KEY } from "../../constants/event";
+import useStoryEmitEvent from "../../hooks/pages/backlog/useStoryEmitEvent";
import useDropdownState from "../../hooks/common/dropdown/useDropdownState";
-import TrashCan from "../../assets/icons/trash-can.svg?react";
import { useModal } from "../../hooks/common/modal/useModal";
-import ConfirmModal from "../common/ConfirmModal";
-import EpicDropdown from "./EpicDropdown";
-import TaskCreateBlock from "./TaskCreateBlock";
+import ChevronRight from "../../assets/icons/chevron-right.svg?react";
+import ChevronDown from "../../assets/icons/chevron-down.svg?react";
+import TrashCan from "../../assets/icons/trash-can.svg?react";
+import { MOUSE_KEY } from "../../constants/event";
+import { BacklogStatusType, EpicCategoryDTO } from "../../types/DTO/backlogDTO";
interface StoryBlockProps {
id: number;
@@ -27,11 +24,8 @@ interface StoryBlockProps {
point: number | null;
progress: number;
status: BacklogStatusType;
- children: React.ReactNode;
taskExist: boolean;
epicList?: EpicCategoryDTO[];
- finished?: boolean;
- lastTaskRankValue?: string;
}
const StoryBlock = ({
@@ -43,9 +37,6 @@ const StoryBlock = ({
status,
taskExist,
epicList,
- finished = false,
- lastTaskRankValue,
- children,
}: StoryBlockProps) => {
const { socket }: { socket: Socket } = useOutletContext();
const { showDetail, handleShowDetail } = useShowDetail();
@@ -280,15 +271,6 @@ const StoryBlock = ({
)}
- {showDetail && (
-
-
- {children}
- {!finished && (
-
- )}
-
- )}
>
);
};
diff --git a/frontend/src/components/backlog/StoryDragContainer.tsx b/frontend/src/components/backlog/StoryDragContainer.tsx
new file mode 100644
index 0000000..8964258
--- /dev/null
+++ b/frontend/src/components/backlog/StoryDragContainer.tsx
@@ -0,0 +1,36 @@
+import React, { DragEvent } from "react";
+
+interface StoryDragContainerProps {
+ index: number;
+ setRef: (index: number) => (element: HTMLDivElement) => void;
+ onDragStart: () => void;
+ onDragEnd: (event: DragEvent) => void;
+ currentlyDraggedOver: boolean;
+ children: React.ReactNode;
+}
+
+const StoryDragContainer = ({
+ index,
+ setRef,
+ onDragStart,
+ onDragEnd,
+ currentlyDraggedOver,
+ children,
+}: StoryDragContainerProps) => (
+
+);
+
+export default StoryDragContainer;
diff --git a/frontend/src/components/backlog/TaskDragContainer.tsx b/frontend/src/components/backlog/TaskDragContainer.tsx
new file mode 100644
index 0000000..3c532d1
--- /dev/null
+++ b/frontend/src/components/backlog/TaskDragContainer.tsx
@@ -0,0 +1,41 @@
+import { DragEvent } from "react";
+
+interface TaskDragContainerProps {
+ storyIndex: number;
+ taskIndex: number;
+ setRef: (
+ storyIndex: number,
+ taskIndex: number
+ ) => (element: HTMLDivElement) => void;
+ onDragStart: () => void;
+ onDragEnd: (event: DragEvent) => void;
+ currentlyDraggedOver: boolean;
+ children: React.ReactNode;
+}
+
+const TaskDragContainer = ({
+ storyIndex,
+ taskIndex,
+ setRef,
+ onDragStart,
+ onDragEnd,
+ currentlyDraggedOver,
+ children,
+}: TaskDragContainerProps) => (
+
+);
+
+export default TaskDragContainer;
diff --git a/frontend/src/hooks/pages/backlog/useBacklogSocket.ts b/frontend/src/hooks/pages/backlog/useBacklogSocket.ts
index b18d3c9..30838fc 100644
--- a/frontend/src/hooks/pages/backlog/useBacklogSocket.ts
+++ b/frontend/src/hooks/pages/backlog/useBacklogSocket.ts
@@ -78,7 +78,7 @@ const useBacklogSocket = (socket: Socket) => {
if (content.epicId) {
setBacklog((prevBacklog) => {
let targetStory: StoryDTO | null = null;
- backlog.epicList.some((epic) => {
+ prevBacklog.epicList.some((epic) => {
const foundStory = epic.storyList.find(
(story) => story.id === content.id
);
@@ -168,6 +168,56 @@ const useBacklogSocket = (socket: Socket) => {
});
break;
case BacklogSocketTaskAction.UPDATE:
+ if (content.storyId) {
+ setBacklog((prevBacklog) => {
+ let targetTask: TaskDTO | null = null;
+ prevBacklog.epicList.some((epic) => {
+ epic.storyList.some((story) => {
+ const foundTask = story.taskList.find(
+ (task) => task.id === content.id
+ );
+
+ if (foundTask) {
+ targetTask = { ...foundTask };
+ return true;
+ }
+
+ return false;
+ });
+
+ if (targetTask) {
+ return true;
+ }
+
+ return false;
+ });
+
+ if (!targetTask) {
+ return prevBacklog;
+ }
+
+ const newEpicList = prevBacklog.epicList.map((epic) => {
+ const newStoryList = epic.storyList.map((story) => {
+ const newTaskList = story.taskList.filter(
+ (task) => task.id !== content.id
+ );
+
+ if (story.id === content.storyId) {
+ newTaskList.push({
+ ...targetTask,
+ ...content,
+ } as TaskDTO);
+ }
+ return { ...story, taskList: newTaskList };
+ });
+
+ return { ...epic, storyList: newStoryList };
+ });
+ return { epicList: newEpicList };
+ });
+ break;
+ }
+
setBacklog((prevBacklog) => {
const newEpicList = prevBacklog.epicList.map((epic) => {
const newStoryList = epic.storyList.map((story) => {
diff --git a/frontend/src/pages/backlog/EpicPage.tsx b/frontend/src/pages/backlog/EpicPage.tsx
index bef7450..5f73559 100644
--- a/frontend/src/pages/backlog/EpicPage.tsx
+++ b/frontend/src/pages/backlog/EpicPage.tsx
@@ -27,7 +27,7 @@ const EpicPage = () => {
{...backlog.epicList.map(
({ id: epicId, name, color, rankValue, storyList }) => (
1}
+ storyExist={storyList.length > 0}
epic={{ id: epicId, name, color, rankValue }}
>
{...storyList.map(({ id, title, point, status, taskList }) => {
@@ -40,19 +40,15 @@ const EpicPage = () => {
: 0;
return (
- 0}
- lastTaskRankValue={
- taskList.length
- ? taskList[taskList.length - 1].rankValue
- : undefined
- }
- >
+ <>
+ 0}
+ />
{...taskList.map((task) => )}
-
+ >
);
})}
{showDetail ? (
diff --git a/frontend/src/pages/backlog/FinishedStoryPage.tsx b/frontend/src/pages/backlog/FinishedStoryPage.tsx
index ec0b765..37dbcb8 100644
--- a/frontend/src/pages/backlog/FinishedStoryPage.tsx
+++ b/frontend/src/pages/backlog/FinishedStoryPage.tsx
@@ -38,16 +38,16 @@ const FinishedStoryPage = () => {
: 0;
return (
- 0}
- epicList={epicCategoryList}
- finished={true}
- >
+ <>
+ 0}
+ epicList={epicCategoryList}
+ />
{...taskList.map((task) => )}
-
+ >
);
})}
diff --git a/frontend/src/pages/backlog/UnfinishedStoryPage.tsx b/frontend/src/pages/backlog/UnfinishedStoryPage.tsx
index 4847143..0cfabff 100644
--- a/frontend/src/pages/backlog/UnfinishedStoryPage.tsx
+++ b/frontend/src/pages/backlog/UnfinishedStoryPage.tsx
@@ -1,28 +1,57 @@
+import { DragEvent, useEffect, useMemo, useRef, useState } from "react";
import { useOutletContext } from "react-router-dom";
import { Socket } from "socket.io-client";
import { LexoRank } from "lexorank";
-import { BacklogDTO } from "../../types/DTO/backlogDTO";
import StoryCreateButton from "../../components/backlog/StoryCreateButton";
import StoryCreateForm from "../../components/backlog/StoryCreateForm";
-import { DragEvent, useEffect, useMemo, useRef, useState } from "react";
-import changeEpicListToStoryList from "../../utils/changeEpicListToStoryList";
import StoryBlock from "../../components/backlog/StoryBlock";
-import TaskBlock from "../../components/backlog/TaskBlock";
import useShowDetail from "../../hooks/pages/backlog/useShowDetail";
import useStoryEmitEvent from "../../hooks/pages/backlog/useStoryEmitEvent";
+import changeEpicListToStoryList from "../../utils/changeEpicListToStoryList";
import getDragElementIndex from "../../utils/getDragElementIndex";
import { BacklogSocketData } from "../../types/common/backlog";
+import { BacklogDTO, TaskDTO } from "../../types/DTO/backlogDTO";
+import StoryDragContainer from "../../components/backlog/StoryDragContainer";
+import TaskBlock from "../../components/backlog/TaskBlock";
+import TaskDragContainer from "../../components/backlog/TaskDragContainer";
+import TaskContainer from "../../components/backlog/TaskContainer";
+import TaskHeader from "../../components/backlog/TaskHeader";
+import TaskCreateBlock from "../../components/backlog/TaskCreateBlock";
+import useTaskEmitEvent from "../../hooks/pages/backlog/useTaskEmitEvent";
const UnfinishedStoryPage = () => {
const { socket, backlog }: { socket: Socket; backlog: BacklogDTO } =
useOutletContext();
const { showDetail, handleShowDetail } = useShowDetail();
const [storyElementIndex, setStoryElementIndex] = useState();
+ const [taskElementIndex, setTaskElementIndex] = useState<{
+ storyId?: number;
+ taskIndex?: number;
+ }>({
+ storyId: undefined,
+ taskIndex: undefined,
+ });
+ const [draggingStoryId, setDraggingStoryId] = useState();
+ const [draggingTaskId, setDraggingTaskId] = useState();
const storyComponentRefList = useRef([]);
- const draggingComponentIdRef = useRef();
+ const taskComponentRefList = useRef([]);
const storyList = useMemo(
() =>
changeEpicListToStoryList(backlog.epicList)
+ .filter(({ status }) => status !== "완료")
+ .map((story) => {
+ const newTaskList = story.taskList.slice();
+ newTaskList.sort((taskA, taskB) => {
+ if (taskA.rankValue < taskB.rankValue) {
+ return -1;
+ }
+ if (taskA.rankValue > taskB.rankValue) {
+ return 1;
+ }
+ return 0;
+ });
+ return { ...story, taskList: newTaskList };
+ })
.sort((storyA, storyB) => {
if (storyA.rankValue < storyB.rankValue) {
return -1;
@@ -31,8 +60,7 @@ const UnfinishedStoryPage = () => {
return 1;
}
return 0;
- })
- .filter(({ status }) => status !== "완료"),
+ }),
[backlog.epicList]
);
const epicCategoryList = useMemo(
@@ -46,16 +74,26 @@ const UnfinishedStoryPage = () => {
[backlog.epicList]
);
const { emitStoryUpdateEvent } = useStoryEmitEvent(socket);
+ const { emitTaskUpdateEvent } = useTaskEmitEvent(socket);
const setStoryComponentRef = (index: number) => (element: HTMLDivElement) => {
storyComponentRefList.current[index] = element;
};
+ const setTaskComponentRef =
+ (storyIndex: number, taskIndex: number) => (element: HTMLDivElement) => {
+ taskComponentRefList.current[storyIndex][taskIndex] = element;
+ };
+
const handleDragOver = (event: DragEvent) => {
+ if (draggingTaskId) {
+ return;
+ }
+
event.preventDefault();
const index = getDragElementIndex(
storyComponentRefList.current,
- draggingComponentIdRef.current,
+ storyList.findIndex(({ id }) => id === draggingStoryId),
event.clientY
);
@@ -63,18 +101,23 @@ const UnfinishedStoryPage = () => {
};
const handleDragStart = (id: number) => {
- draggingComponentIdRef.current = id;
+ if (draggingTaskId) {
+ return;
+ }
+ setDraggingStoryId(id);
};
const handleDragEnd = (event: DragEvent) => {
+ if (draggingTaskId) {
+ return;
+ }
+
event.stopPropagation();
- const targetIndex = storyList.findIndex(
- ({ id }) => id === draggingComponentIdRef.current
- );
+ const targetIndex = storyList.findIndex(({ id }) => id === draggingStoryId);
let rankValue;
if (storyElementIndex === targetIndex) {
- draggingComponentIdRef.current = undefined;
+ setDraggingStoryId(undefined);
setStoryElementIndex(undefined);
return;
}
@@ -96,11 +139,11 @@ const UnfinishedStoryPage = () => {
}
emitStoryUpdateEvent({
- id: draggingComponentIdRef.current as number,
+ id: draggingStoryId as number,
rankValue,
});
- draggingComponentIdRef.current = undefined;
+ setDraggingStoryId(undefined);
setStoryElementIndex(undefined);
};
@@ -113,7 +156,7 @@ const UnfinishedStoryPage = () => {
if (
domain === "story" &&
action === "delete" &&
- content.id === draggingComponentIdRef.current
+ content.id === draggingStoryId
) {
setStoryElementIndex(undefined);
}
@@ -126,6 +169,76 @@ const UnfinishedStoryPage = () => {
};
}, []);
+ const handleTaskDragOver = (event: DragEvent, storyIndex: number) => {
+ if (draggingStoryId) {
+ return;
+ }
+
+ event.preventDefault();
+ const mouseIndex = storyList[storyIndex].taskList.findIndex(
+ ({ id }) => id === draggingTaskId
+ );
+
+ const index = getDragElementIndex(
+ taskComponentRefList.current[storyIndex],
+ mouseIndex,
+ event.clientY
+ );
+
+ setTaskElementIndex({
+ storyId: storyList[storyIndex].id,
+ taskIndex: index,
+ });
+ };
+
+ const handleTaskDragStart = (taskId: number) => {
+ setDraggingTaskId(taskId);
+ };
+
+ const handleTaskDragEnd = () => {
+ const { storyId, taskIndex } = taskElementIndex;
+ const taskList = storyList.find(({ id }) => id === storyId)
+ ?.taskList as TaskDTO[];
+ const targetIndex = taskList?.findIndex(({ id }) => id === draggingTaskId);
+
+ let rankValue;
+
+ if (taskIndex === targetIndex) {
+ setDraggingTaskId(undefined);
+ setTaskElementIndex({ storyId: undefined, taskIndex: undefined });
+ return;
+ }
+
+ if (taskIndex === 0 && !taskList.length) {
+ console.log("아무 것도 없을 때");
+
+ rankValue = LexoRank.middle().toString();
+ } else if (taskIndex === 0) {
+ const firstTaskRank = taskList[0].rankValue;
+ rankValue = LexoRank.parse(firstTaskRank).genPrev().toString();
+ } else if (taskIndex === taskList.length) {
+ const lastTaskRank = taskList[taskList.length - 1].rankValue;
+ rankValue = LexoRank.parse(lastTaskRank).genNext().toString();
+ } else {
+ const prevTaskRank = LexoRank.parse(
+ taskList[(taskIndex as number) - 1].rankValue
+ );
+ const nextTaskRank = LexoRank.parse(
+ taskList[taskIndex as number].rankValue
+ );
+ rankValue = prevTaskRank.between(nextTaskRank).toString();
+ }
+
+ emitTaskUpdateEvent({
+ id: draggingTaskId as number,
+ storyId,
+ rankValue,
+ });
+
+ setDraggingTaskId(undefined);
+ setTaskElementIndex({ storyId: undefined, taskIndex: undefined });
+ };
+
return (
@@ -138,34 +251,62 @@ const UnfinishedStoryPage = () => {
100
)
: 0;
+ taskComponentRefList.current[index] = [];
return (
handleDragStart(id)}
- onDragEnd={handleDragEnd}
+ onDragOver={(event) => handleTaskDragOver(event, index)}
>
-
-
0}
- epicList={epicCategoryList}
- lastTaskRankValue={
- taskList.length
- ? taskList[taskList.length - 1].rankValue
- : undefined
- }
+ handleDragStart(id)}
+ onDragEnd={handleDragEnd}
+ currentlyDraggedOver={index === storyElementIndex}
>
- {...taskList.map((task) => )}
-
+
0}
+ epicList={epicCategoryList}
+ />
+
+
+
+ {...taskList.map((task, taskIndex) => (
+ handleTaskDragStart(task.id)}
+ currentlyDraggedOver={
+ id === taskElementIndex.storyId &&
+ taskIndex === taskElementIndex.taskIndex
+ }
+ >
+
+
+ ))}
+
+
+
);
}