From 2a13f26f03d26766a59fab990885681996c4155c Mon Sep 17 00:00:00 2001 From: surinkwon Date: Wed, 31 Jul 2024 22:52:12 +0900 Subject: [PATCH 1/8] =?UTF-8?q?feat:=20=EC=8A=A4=ED=86=A0=EB=A6=AC=20?= =?UTF-8?q?=EB=93=9C=EB=9E=98=EA=B7=B8=20=EC=8B=9C=20UI=20=EB=B3=80?= =?UTF-8?q?=ED=99=94=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/pages/backlog/UnfinishedStoryPage.tsx | 102 ++++++++++++++---- 1 file changed, 79 insertions(+), 23 deletions(-) diff --git a/frontend/src/pages/backlog/UnfinishedStoryPage.tsx b/frontend/src/pages/backlog/UnfinishedStoryPage.tsx index 6a5772a9..7fb35d26 100644 --- a/frontend/src/pages/backlog/UnfinishedStoryPage.tsx +++ b/frontend/src/pages/backlog/UnfinishedStoryPage.tsx @@ -2,7 +2,7 @@ import { useOutletContext } from "react-router-dom"; import { BacklogDTO } from "../../types/DTO/backlogDTO"; import StoryCreateButton from "../../components/backlog/StoryCreateButton"; import StoryCreateForm from "../../components/backlog/StoryCreateForm"; -import { useMemo } from "react"; +import { DragEvent, useMemo, useRef, useState } from "react"; import changeEpicListToStoryList from "../../utils/changeEpicListToStoryList"; import StoryBlock from "../../components/backlog/StoryBlock"; import TaskBlock from "../../components/backlog/TaskBlock"; @@ -11,6 +11,9 @@ import useShowDetail from "../../hooks/pages/backlog/useShowDetail"; const UnfinishedStoryPage = () => { const { backlog }: { backlog: BacklogDTO } = useOutletContext(); const { showDetail, handleShowDetail } = useShowDetail(); + const [beforeElementIndex, setBeforeElementIndex] = useState(); + const storyComponentRefList = useRef([]); + const draggingComponentIndexRef = useRef(); const storyList = useMemo( () => changeEpicListToStoryList(backlog.epicList), [backlog.epicList] @@ -20,30 +23,83 @@ const UnfinishedStoryPage = () => { [backlog.epicList] ); + const setStoryComponentRef = (index: number) => (element: HTMLDivElement) => { + storyComponentRefList.current[index] = element; + }; + + const handleDragOver = (event: DragEvent) => { + event.preventDefault(); + const index = getDragBeforeElement(event.clientY); + setBeforeElementIndex(index); + }; + + const handleDragStart = (index: number) => { + draggingComponentIndexRef.current = index; + }; + + const handleDragEnd = (event: DragEvent) => { + event.stopPropagation(); + draggingComponentIndexRef.current = undefined; + setBeforeElementIndex(undefined); + }; + + function getDragBeforeElement(y: number) { + return storyComponentRefList.current.reduce( + (closest, child, index) => { + const box = child.getBoundingClientRect(); + const offset = y - box.top - box.height / 2; + if (offset < 0 && offset > closest.offset) { + return { offset, index }; + } else { + return closest; + } + }, + { + offset: Number.NEGATIVE_INFINITY, + index: draggingComponentIndexRef.current, + } + ).index; + } + return (
-
- {...storyList.map(({ id, epic, title, point, status, taskList }) => { - const progress = taskList.length - ? Math.round( - (taskList.filter(({ status }) => status === "완료").length / - taskList.length) * - 100 - ) - : 0; - - return ( - 0} - epicList={epicCategoryList} - > - {...taskList.map((task) => )} - - ); - })} +
+ {...storyList.map( + ({ id, epic, title, point, status, taskList }, index) => { + const progress = taskList.length + ? Math.round( + (taskList.filter(({ status }) => status === "완료").length / + taskList.length) * + 100 + ) + : 0; + + return ( +
handleDragStart(index)} + onDragEnd={handleDragEnd} + > +
+ 0} + epicList={epicCategoryList} + > + {...taskList.map((task) => )} + +
+ ); + } + )}
{showDetail ? ( Date: Thu, 1 Aug 2024 20:44:40 +0900 Subject: [PATCH 2/8] =?UTF-8?q?chore:=20lexorank=20=EB=9D=BC=EC=9D=B4?= =?UTF-8?q?=EB=B8=8C=EB=9F=AC=EB=A6=AC=20=EC=84=A4=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/package.json | 1 + frontend/yarn.lock | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/frontend/package.json b/frontend/package.json index 0d375258..ac8c5bab 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,6 +13,7 @@ "dependencies": { "@tanstack/react-query": "^5.28.14", "axios": "^1.6.7", + "lexorank": "^1.0.5", "react": "^18.2.0", "react-dom": "^18.2.0", "react-error-boundary": "^4.0.13", diff --git a/frontend/yarn.lock b/frontend/yarn.lock index d63893af..df3f3eeb 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -4363,6 +4363,11 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" +lexorank@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/lexorank/-/lexorank-1.0.5.tgz#6d0a22efd0dc0a32cf2ec128e3cba48ef58c4201" + integrity sha512-K1B/Yr/gIU0wm68hk/yB0p/mv6xM3ShD5aci42vOwcjof8slG8Kpo3Q7+1WTv7DaRHKWRgLPqrFDt+4GtuFAtA== + lilconfig@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52" From c5aeb5fbf04183b4dadac594f6bd74fdc182e741 Mon Sep 17 00:00:00 2001 From: surinkwon Date: Fri, 2 Aug 2024 16:57:38 +0900 Subject: [PATCH 3/8] =?UTF-8?q?feat:=20=EC=97=90=ED=94=BD,=20=EC=8A=A4?= =?UTF-8?q?=ED=86=A0=EB=A6=AC,=20=ED=83=9C=EC=8A=A4=ED=81=AC=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EC=8B=9C=20=EB=9E=AD=ED=81=AC=20=EA=B0=92=EB=8F=84?= =?UTF-8?q?=20=ED=95=A8=EA=BB=98=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/backlog/EpicDropdown.tsx | 9 +++++- .../src/components/backlog/StoryBlock.tsx | 6 +++- .../components/backlog/StoryCreateForm.tsx | 29 ++++++++++++++----- .../components/backlog/TaskCreateBlock.tsx | 8 +++-- .../src/components/backlog/TaskCreateForm.tsx | 11 ++++++- .../hooks/pages/backlog/useEpicEmitEvent.ts | 1 + .../hooks/pages/backlog/useStoryEmitEvent.ts | 1 + .../hooks/pages/backlog/useTaskEmitEvent.ts | 7 +++-- frontend/src/types/DTO/backlogDTO.ts | 3 ++ frontend/src/types/common/backlog.ts | 2 ++ 10 files changed, 62 insertions(+), 15 deletions(-) diff --git a/frontend/src/components/backlog/EpicDropdown.tsx b/frontend/src/components/backlog/EpicDropdown.tsx index cb896553..fa46f742 100644 --- a/frontend/src/components/backlog/EpicDropdown.tsx +++ b/frontend/src/components/backlog/EpicDropdown.tsx @@ -13,6 +13,7 @@ import { BacklogSocketEpicAction, } from "../../types/common/backlog"; import EpicDropdownOption from "./EpicDropdownOption"; +import { LexoRank } from "lexorank"; interface EpicDropdownProps { selectedEpic?: EpicCategoryDTO; @@ -53,8 +54,14 @@ const EpicDropdown = ({ return; } + const rankValue = epicList.length + ? LexoRank.parse(epicList[epicList.length - 1].rankValue) + .genNext() + .toString() + : LexoRank.middle().toString(); + setValue(""); - emitEpicCreateEvent({ name: value, color: epicColor }); + emitEpicCreateEvent({ name: value, color: epicColor, rankValue }); } }; diff --git a/frontend/src/components/backlog/StoryBlock.tsx b/frontend/src/components/backlog/StoryBlock.tsx index 53c2a609..dac9b47e 100644 --- a/frontend/src/components/backlog/StoryBlock.tsx +++ b/frontend/src/components/backlog/StoryBlock.tsx @@ -31,6 +31,7 @@ interface StoryBlockProps { taskExist: boolean; epicList: EpicCategoryDTO[]; finished?: boolean; + lastTaskRankValue?: string; } const StoryBlock = ({ @@ -43,6 +44,7 @@ const StoryBlock = ({ taskExist, epicList, finished = false, + lastTaskRankValue, children, }: StoryBlockProps) => { const { socket }: { socket: Socket } = useOutletContext(); @@ -279,7 +281,9 @@ const StoryBlock = ({ {children} - {!finished && } + {!finished && ( + + )} )} diff --git a/frontend/src/components/backlog/StoryCreateForm.tsx b/frontend/src/components/backlog/StoryCreateForm.tsx index b7958db8..7bc41101 100644 --- a/frontend/src/components/backlog/StoryCreateForm.tsx +++ b/frontend/src/components/backlog/StoryCreateForm.tsx @@ -9,33 +9,42 @@ import { useOutletContext } from "react-router-dom"; import EpicDropdown from "./EpicDropdown"; import { EpicCategoryDTO } from "../../types/DTO/backlogDTO"; import useDropdownState from "../../hooks/common/dropdown/useDropdownState"; +import { LexoRank } from "lexorank"; interface StoryCreateFormProps { onCloseClick: () => void; epicList: EpicCategoryDTO[]; + lastStoryRankValue?: string; } -const StoryCreateForm = ({ onCloseClick, epicList }: StoryCreateFormProps) => { +const StoryCreateForm = ({ + onCloseClick, + epicList, + lastStoryRankValue, +}: StoryCreateFormProps) => { const { socket }: { socket: Socket } = useOutletContext(); - const [{ title, point, epicId, status }, setStoryFormData] = + const [{ title, point, epicId, status, rankValue }, setStoryFormData] = useState({ title: "", point: undefined, status: "시작전", epicId: undefined, + rankValue: lastStoryRankValue + ? LexoRank.parse(lastStoryRankValue).genNext().toString() + : LexoRank.middle().toString(), }); const { open, handleClose, handleOpen, dropdownRef } = useDropdownState(); const { emitStoryCreateEvent } = useStoryEmitEvent(socket); const handleTitleChange = ({ target }: ChangeEvent) => { const { value } = target; - setStoryFormData({ title: value, point, epicId, status }); + setStoryFormData({ title: value, point, epicId, status, rankValue }); }; const handlePointChange = ({ target }: ChangeEvent) => { const { value } = target; const newPoint = value === "" ? undefined : Number(value); - setStoryFormData({ title, point: newPoint, epicId, status }); + setStoryFormData({ title, point: newPoint, epicId, status, rankValue }); }; const handleSubmit = (event: FormEvent) => { @@ -71,12 +80,18 @@ const StoryCreateForm = ({ onCloseClick, epicList }: StoryCreateFormProps) => { return; } - emitStoryCreateEvent({ title, status, epicId, point }); + emitStoryCreateEvent({ title, status, epicId, point, rankValue }); onCloseClick(); }; const handleEpicChange = (selectedEpicId: number | undefined) => { - setStoryFormData({ title, status, point, epicId: selectedEpicId }); + setStoryFormData({ + title, + status, + point, + epicId: selectedEpicId, + rankValue, + }); handleClose(); }; @@ -93,7 +108,7 @@ const StoryCreateForm = ({ onCloseClick, epicList }: StoryCreateFormProps) => { useEffect(() => { if (!epicList.filter(({ id }) => id === epicId).length) { - setStoryFormData({ title, point, status, epicId: undefined }); + setStoryFormData({ title, point, status, epicId: undefined, rankValue }); } }, [epicList]); diff --git a/frontend/src/components/backlog/TaskCreateBlock.tsx b/frontend/src/components/backlog/TaskCreateBlock.tsx index ca9f658a..ebc25f26 100644 --- a/frontend/src/components/backlog/TaskCreateBlock.tsx +++ b/frontend/src/components/backlog/TaskCreateBlock.tsx @@ -4,15 +4,19 @@ import TaskCreateForm from "./TaskCreateForm"; interface TaskCreateBlockProps { storyId: number; + lastTaskRankValue?: string; } -const TaskCreateBlock = ({ storyId }: TaskCreateBlockProps) => { +const TaskCreateBlock = ({ + storyId, + lastTaskRankValue, +}: TaskCreateBlockProps) => { const { showDetail, handleShowDetail } = useShowDetail(); return ( <> {showDetail ? ( handleShowDetail(false)} /> ) : ( diff --git a/frontend/src/components/backlog/TaskCreateForm.tsx b/frontend/src/components/backlog/TaskCreateForm.tsx index e0b4e7cd..98112b31 100644 --- a/frontend/src/components/backlog/TaskCreateForm.tsx +++ b/frontend/src/components/backlog/TaskCreateForm.tsx @@ -5,13 +5,19 @@ import Check from "../../assets/icons/check.svg?react"; import Closed from "../../assets/icons/closed.svg?react"; import useTaskEmitEvent from "../../hooks/pages/backlog/useTaskEmitEvent"; import { TaskForm } from "../../types/common/backlog"; +import { LexoRank } from "lexorank"; interface TaskCreateFormProps { onCloseClick: () => void; storyId: number; + lastTaskRankValue?: string; } -const TaskCreateForm = ({ onCloseClick, storyId }: TaskCreateFormProps) => { +const TaskCreateForm = ({ + onCloseClick, + storyId, + lastTaskRankValue, +}: TaskCreateFormProps) => { const [taskFormData, setTaskFormData] = useState({ title: "", expectedTime: null, @@ -19,6 +25,9 @@ const TaskCreateForm = ({ onCloseClick, storyId }: TaskCreateFormProps) => { status: "시작전", assignedMemberId: null, storyId, + rankValue: lastTaskRankValue + ? LexoRank.parse(lastTaskRankValue).genNext().toString() + : LexoRank.middle().toString(), }); const { socket }: { socket: Socket } = useOutletContext(); const { emitTaskCreateEvent } = useTaskEmitEvent(socket); diff --git a/frontend/src/hooks/pages/backlog/useEpicEmitEvent.ts b/frontend/src/hooks/pages/backlog/useEpicEmitEvent.ts index 1df54f92..ffba7783 100644 --- a/frontend/src/hooks/pages/backlog/useEpicEmitEvent.ts +++ b/frontend/src/hooks/pages/backlog/useEpicEmitEvent.ts @@ -5,6 +5,7 @@ const useEpicEmitEvent = (socket: Socket) => { const emitEpicCreateEvent = (content: { name: string; color: BacklogCategoryColor; + rankValue: string; }) => { socket.emit("epic", { action: "create", content }); }; diff --git a/frontend/src/hooks/pages/backlog/useStoryEmitEvent.ts b/frontend/src/hooks/pages/backlog/useStoryEmitEvent.ts index c93ff396..76112f68 100644 --- a/frontend/src/hooks/pages/backlog/useStoryEmitEvent.ts +++ b/frontend/src/hooks/pages/backlog/useStoryEmitEvent.ts @@ -17,6 +17,7 @@ const useStoryEmitEvent = (socket: Socket) => { status?: BacklogStatusType; epicId?: number; point?: number; + rankValue?: string; }) => { socket.emit("story", { action: "update", content }); }; diff --git a/frontend/src/hooks/pages/backlog/useTaskEmitEvent.ts b/frontend/src/hooks/pages/backlog/useTaskEmitEvent.ts index 42576493..17b1ff63 100644 --- a/frontend/src/hooks/pages/backlog/useTaskEmitEvent.ts +++ b/frontend/src/hooks/pages/backlog/useTaskEmitEvent.ts @@ -15,13 +15,14 @@ const useTaskEmitEvent = (socket: Socket) => { assignedMemberId?: number; storyId?: number; status?: BacklogStatusType; + rankValue?: string; }) => { socket.emit("task", { action: "update", content }); }; - const emitTaskDeleteEvent = (content: {id: number}) => { - socket.emit("task", {action: "delete", content}) - } + const emitTaskDeleteEvent = (content: { id: number }) => { + socket.emit("task", { action: "delete", content }); + }; return { emitTaskCreateEvent, emitTaskUpdateEvent, emitTaskDeleteEvent }; }; diff --git a/frontend/src/types/DTO/backlogDTO.ts b/frontend/src/types/DTO/backlogDTO.ts index 78fb1c94..9f21ab79 100644 --- a/frontend/src/types/DTO/backlogDTO.ts +++ b/frontend/src/types/DTO/backlogDTO.ts @@ -18,6 +18,7 @@ export interface TaskDTO { status: BacklogStatusType; assignedMemberId: number | null; storyId: number; + rankValue: string; } export interface StoryDTO { @@ -27,12 +28,14 @@ export interface StoryDTO { status: BacklogStatusType; taskList: TaskDTO[]; epicId: number; + rankValue: string; } export interface EpicCategoryDTO { id: number; name: string; color: EpicColor; + rankValue: string; } export interface EpicDTO extends EpicCategoryDTO { diff --git a/frontend/src/types/common/backlog.ts b/frontend/src/types/common/backlog.ts index c6401cb0..581c6646 100644 --- a/frontend/src/types/common/backlog.ts +++ b/frontend/src/types/common/backlog.ts @@ -28,6 +28,7 @@ export interface StoryForm { title: string; point: number | undefined; status: "시작전"; + rankValue: string; } export interface TaskForm { @@ -37,6 +38,7 @@ export interface TaskForm { actualTime: number | null | ""; status: "시작전"; assignedMemberId: null; + rankValue: string; } export enum BacklogSocketDomain { From 28f33d680ccb597aafe09a6afb32f8a4d27bd1b7 Mon Sep 17 00:00:00 2001 From: surinkwon Date: Fri, 2 Aug 2024 17:58:26 +0900 Subject: [PATCH 4/8] =?UTF-8?q?feat:=20=EC=8A=A4=ED=86=A0=EB=A6=AC=20?= =?UTF-8?q?=EB=93=9C=EB=9E=98=EA=B7=B8=20=EC=95=A4=20=EB=93=9C=EB=A1=AD=20?= =?UTF-8?q?API=20=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/pages/backlog/UnfinishedStoryPage.tsx | 107 +++++++++++++----- frontend/src/utils/getDragElementIndex.ts | 24 ++++ 2 files changed, 105 insertions(+), 26 deletions(-) create mode 100644 frontend/src/utils/getDragElementIndex.ts diff --git a/frontend/src/pages/backlog/UnfinishedStoryPage.tsx b/frontend/src/pages/backlog/UnfinishedStoryPage.tsx index 7fb35d26..18eaa31e 100644 --- a/frontend/src/pages/backlog/UnfinishedStoryPage.tsx +++ b/frontend/src/pages/backlog/UnfinishedStoryPage.tsx @@ -1,4 +1,6 @@ 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"; @@ -7,21 +9,40 @@ 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 getDragElementIndex from "../../utils/getDragElementIndex"; const UnfinishedStoryPage = () => { - const { backlog }: { backlog: BacklogDTO } = useOutletContext(); + const { socket, backlog }: { socket: Socket; backlog: BacklogDTO } = + useOutletContext(); const { showDetail, handleShowDetail } = useShowDetail(); - const [beforeElementIndex, setBeforeElementIndex] = useState(); + const [storyElementIndex, setStoryElementIndex] = useState(); const storyComponentRefList = useRef([]); const draggingComponentIndexRef = useRef(); const storyList = useMemo( - () => changeEpicListToStoryList(backlog.epicList), + () => + changeEpicListToStoryList(backlog.epicList).sort((storyA, storyB) => { + if (storyA.rankValue < storyB.rankValue) { + return -1; + } + if (storyA.rankValue > storyB.rankValue) { + return 1; + } + return 0; + }), [backlog.epicList] ); const epicCategoryList = useMemo( - () => backlog.epicList.map(({ id, name, color }) => ({ id, name, color })), + () => + backlog.epicList.map(({ id, name, color, rankValue }) => ({ + id, + name, + color, + rankValue, + })), [backlog.epicList] ); + const { emitStoryUpdateEvent } = useStoryEmitEvent(socket); const setStoryComponentRef = (index: number) => (element: HTMLDivElement) => { storyComponentRefList.current[index] = element; @@ -29,8 +50,13 @@ const UnfinishedStoryPage = () => { const handleDragOver = (event: DragEvent) => { event.preventDefault(); - const index = getDragBeforeElement(event.clientY); - setBeforeElementIndex(index); + const index = getDragElementIndex( + storyComponentRefList.current, + draggingComponentIndexRef.current, + event.clientY + ); + + setStoryElementIndex(index); }; const handleDragStart = (index: number) => { @@ -39,28 +65,39 @@ const UnfinishedStoryPage = () => { const handleDragEnd = (event: DragEvent) => { event.stopPropagation(); + let rankValue; + + if (storyElementIndex === draggingComponentIndexRef.current) { + draggingComponentIndexRef.current = undefined; + setStoryElementIndex(undefined); + return; + } + + if (storyElementIndex === 0) { + const firstStoryRank = storyList[0].rankValue; + rankValue = LexoRank.parse(firstStoryRank).genPrev().toString(); + } else if (storyElementIndex === storyList.length) { + const lastStoryRank = storyList[storyList.length - 1].rankValue; + rankValue = LexoRank.parse(lastStoryRank).genNext().toString(); + } else { + const prevStoryRank = LexoRank.parse( + storyList[(storyElementIndex as number) - 1].rankValue + ); + const nextStoryRank = LexoRank.parse( + storyList[storyElementIndex as number].rankValue + ); + rankValue = prevStoryRank.between(nextStoryRank).toString(); + } + + emitStoryUpdateEvent({ + id: storyList[draggingComponentIndexRef.current as number].id, + rankValue, + }); + draggingComponentIndexRef.current = undefined; - setBeforeElementIndex(undefined); + setStoryElementIndex(undefined); }; - function getDragBeforeElement(y: number) { - return storyComponentRefList.current.reduce( - (closest, child, index) => { - const box = child.getBoundingClientRect(); - const offset = y - box.top - box.height / 2; - if (offset < 0 && offset > closest.offset) { - return { offset, index }; - } else { - return closest; - } - }, - { - offset: Number.NEGATIVE_INFINITY, - index: draggingComponentIndexRef.current, - } - ).index; - } - return (
@@ -84,7 +121,7 @@ const UnfinishedStoryPage = () => { >
{ progress={progress} taskExist={taskList.length > 0} epicList={epicCategoryList} + lastTaskRankValue={ + taskList.length + ? taskList[taskList.length - 1].rankValue + : undefined + } > {...taskList.map((task) => )} @@ -100,11 +142,24 @@ const UnfinishedStoryPage = () => { ); } )} +
{showDetail ? ( handleShowDetail(false)} + lastStoryRankValue={ + storyList.length + ? storyList[storyList.length - 1].rankValue + : undefined + } /> ) : ( handleShowDetail(true)} /> diff --git a/frontend/src/utils/getDragElementIndex.ts b/frontend/src/utils/getDragElementIndex.ts new file mode 100644 index 00000000..7f85be27 --- /dev/null +++ b/frontend/src/utils/getDragElementIndex.ts @@ -0,0 +1,24 @@ +const getDragElementIndex = ( + list: HTMLDivElement[], + initialIndex: number | undefined, + y: number +) => + list.reduce( + (closest, child, index) => { + const box = child.getBoundingClientRect(); + const offset = y - box.top - box.height / 2; + console.log(offset); + + if (offset < 0 && offset > closest.offset) { + return { offset, index }; + } else { + return closest; + } + }, + { + offset: Number.NEGATIVE_INFINITY, + index: initialIndex, + } + ).index; + +export default getDragElementIndex; From b818c5b510023718627a182c524918592d737ba8 Mon Sep 17 00:00:00 2001 From: surinkwon Date: Fri, 2 Aug 2024 20:14:59 +0900 Subject: [PATCH 5/8] =?UTF-8?q?feat:=20=EC=97=90=ED=94=BD=EB=B3=84=20?= =?UTF-8?q?=EB=B0=B1=EB=A1=9C=EA=B7=B8=20=ED=8E=98=EC=9D=B4=EC=A7=80=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/AppRouter.tsx | 3 +- frontend/src/components/backlog/EpicBlock.tsx | 50 ++++++++++++ .../src/components/backlog/StoryBlock.tsx | 33 ++++---- .../components/backlog/StoryCreateForm.tsx | 51 +++++++----- frontend/src/pages/backlog/EpicPage.tsx | 79 +++++++++++++++++++ .../src/pages/backlog/FinishedStoryPage.tsx | 8 +- 6 files changed, 186 insertions(+), 38 deletions(-) create mode 100644 frontend/src/components/backlog/EpicBlock.tsx create mode 100644 frontend/src/pages/backlog/EpicPage.tsx diff --git a/frontend/src/AppRouter.tsx b/frontend/src/AppRouter.tsx index 70f815ad..66ed890f 100644 --- a/frontend/src/AppRouter.tsx +++ b/frontend/src/AppRouter.tsx @@ -22,6 +22,7 @@ import InvitePage from "./pages/invite/InvitePage"; import UnfinishedStoryPage from "./pages/backlog/UnfinishedStoryPage"; import BacklogPage from "./pages/backlog/BacklogPage"; import FinishedStoryPage from "./pages/backlog/FinishedStoryPage"; +import EpicPage from "./pages/backlog/EpicPage"; type RouteType = "PRIVATE" | "PUBLIC"; @@ -86,7 +87,7 @@ const router = createBrowserRouter([ }, { path: ROUTER_URL.BACKLOG.EPIC, - element:
backlog epic Page
, + element: , }, { path: ROUTER_URL.BACKLOG.COMPLETED, diff --git a/frontend/src/components/backlog/EpicBlock.tsx b/frontend/src/components/backlog/EpicBlock.tsx new file mode 100644 index 00000000..c671d5ce --- /dev/null +++ b/frontend/src/components/backlog/EpicBlock.tsx @@ -0,0 +1,50 @@ +import useShowDetail from "../../hooks/pages/backlog/useShowDetail"; +import ChevronDown from "../../assets/icons/chevron-down.svg?react"; +import ChevronRight from "../../assets/icons/chevron-right.svg?react"; +import CategoryChip from "./CategoryChip"; +import { EpicCategoryDTO } from "../../types/DTO/backlogDTO"; + +interface EpicBlockProps { + storyExist: boolean; + epic: EpicCategoryDTO; + children: React.ReactNode; +} + +const EpicBlock = ({ storyExist, epic, children }: EpicBlockProps) => { + const { showDetail, handleShowDetail } = useShowDetail(); + + return ( + <> +
+ +
+ +
+
+ {showDetail &&
{children}
} + + ); +}; + +export default EpicBlock; diff --git a/frontend/src/components/backlog/StoryBlock.tsx b/frontend/src/components/backlog/StoryBlock.tsx index dac9b47e..6bc9e0d7 100644 --- a/frontend/src/components/backlog/StoryBlock.tsx +++ b/frontend/src/components/backlog/StoryBlock.tsx @@ -29,7 +29,7 @@ interface StoryBlockProps { status: BacklogStatusType; children: React.ReactNode; taskExist: boolean; - epicList: EpicCategoryDTO[]; + epicList?: EpicCategoryDTO[]; finished?: boolean; lastTaskRankValue?: string; } @@ -172,21 +172,24 @@ const StoryBlock = ({ onContextMenu={(event) => event.preventDefault()} ref={blockRef} > -
- + {epicList && ( +
+ + + {epicUpdating && ( + + )} +
+ )} - {epicUpdating && ( - - )} -
handleTitleUpdatingOpen(true)} diff --git a/frontend/src/components/backlog/StoryCreateForm.tsx b/frontend/src/components/backlog/StoryCreateForm.tsx index 7bc41101..05e1cf07 100644 --- a/frontend/src/components/backlog/StoryCreateForm.tsx +++ b/frontend/src/components/backlog/StoryCreateForm.tsx @@ -14,12 +14,14 @@ import { LexoRank } from "lexorank"; interface StoryCreateFormProps { onCloseClick: () => void; epicList: EpicCategoryDTO[]; + epic?: EpicCategoryDTO; lastStoryRankValue?: string; } const StoryCreateForm = ({ onCloseClick, epicList, + epic, lastStoryRankValue, }: StoryCreateFormProps) => { const { socket }: { socket: Socket } = useOutletContext(); @@ -28,7 +30,7 @@ const StoryCreateForm = ({ title: "", point: undefined, status: "시작전", - epicId: undefined, + epicId: epic?.id, rankValue: lastStoryRankValue ? LexoRank.parse(lastStoryRankValue).genNext().toString() : LexoRank.middle().toString(), @@ -117,32 +119,39 @@ const StoryCreateForm = ({ className="flex items-center w-full py-1 border-t border-b" onSubmit={handleSubmit} > -
- {epicId && ( - - )} - {open && ( - - )} -
+ {!epic ? ( +
+ {epicId && ( + + )} + {open && ( + + )} +
+ ) : ( +
+ )} + -
+
{ + const { backlog }: { backlog: BacklogDTO } = useOutletContext(); + const { showDetail, handleShowDetail } = useShowDetail(); + const epicCategoryList = useMemo( + () => + backlog.epicList.map(({ id, name, color, rankValue }) => ({ + id, + name, + color, + rankValue, + })), + [backlog.epicList] + ); + + return ( +
+ {...backlog.epicList.map( + ({ id: epicId, name, color, rankValue, storyList }) => ( + 1} + epic={{ id: epicId, name, color, rankValue }} + > + {...storyList.map(({ id, title, point, status, taskList }) => { + const progress = taskList.length + ? Math.round( + (taskList.filter(({ status }) => status === "완료").length / + taskList.length) * + 100 + ) + : 0; + + return ( + 0} + lastTaskRankValue={ + taskList.length + ? taskList[taskList.length - 1].rankValue + : undefined + } + > + {...taskList.map((task) => )} + + ); + })} + {showDetail ? ( + handleShowDetail(false)} + lastStoryRankValue={ + storyList.length + ? storyList[storyList.length - 1].rankValue + : undefined + } + /> + ) : ( + handleShowDetail(true)} /> + )} + + ) + )} +
+ ); +}; + +export default EpicPage; diff --git a/frontend/src/pages/backlog/FinishedStoryPage.tsx b/frontend/src/pages/backlog/FinishedStoryPage.tsx index 420799a3..ec0b7650 100644 --- a/frontend/src/pages/backlog/FinishedStoryPage.tsx +++ b/frontend/src/pages/backlog/FinishedStoryPage.tsx @@ -15,7 +15,13 @@ const FinishedStoryPage = () => { [backlog.epicList] ); const epicCategoryList = useMemo( - () => backlog.epicList.map(({ id, name, color }) => ({ id, name, color })), + () => + backlog.epicList.map(({ id, name, color, rankValue }) => ({ + id, + name, + color, + rankValue, + })), [backlog.epicList] ); From ab30afc405427733d46c3c22fb29b64997881ed2 Mon Sep 17 00:00:00 2001 From: surinkwon Date: Sat, 3 Aug 2024 17:31:45 +0900 Subject: [PATCH 6/8] =?UTF-8?q?feat:=20=EC=97=90=ED=94=BD=EB=B3=84=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=EC=97=90=EC=84=9C=20=EC=97=90?= =?UTF-8?q?=ED=94=BD=EC=9D=84=20=EC=88=98=EC=A0=95,=20=EC=82=AD=EC=A0=9C,?= =?UTF-8?q?=20=EC=83=9D=EC=84=B1=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EB=93=9C=EB=A1=AD=EB=8B=A4=EC=9A=B4=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/backlog/EpicBlock.tsx | 34 +++++++++++++++++-- frontend/src/pages/backlog/EpicPage.tsx | 1 + 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/backlog/EpicBlock.tsx b/frontend/src/components/backlog/EpicBlock.tsx index c671d5ce..fe67b43a 100644 --- a/frontend/src/components/backlog/EpicBlock.tsx +++ b/frontend/src/components/backlog/EpicBlock.tsx @@ -3,15 +3,34 @@ import ChevronDown from "../../assets/icons/chevron-down.svg?react"; import ChevronRight from "../../assets/icons/chevron-right.svg?react"; import CategoryChip from "./CategoryChip"; import { EpicCategoryDTO } from "../../types/DTO/backlogDTO"; +import useDropdownState from "../../hooks/common/dropdown/useDropdownState"; +import EpicDropdown from "./EpicDropdown"; interface EpicBlockProps { storyExist: boolean; epic: EpicCategoryDTO; + epicList: EpicCategoryDTO[]; children: React.ReactNode; } -const EpicBlock = ({ storyExist, epic, children }: EpicBlockProps) => { +const EpicBlock = ({ + storyExist, + epic, + epicList, + children, +}: EpicBlockProps) => { const { showDetail, handleShowDetail } = useShowDetail(); + const { + open: epicUpdating, + handleOpen: handleEpicUpdateOpen, + dropdownRef: epicRef, + } = useDropdownState(); + + const handleEpicColumnClick = () => { + if (!epicUpdating) { + handleEpicUpdateOpen(); + } + }; return ( <> @@ -38,8 +57,19 @@ const EpicBlock = ({ storyExist, epic, children }: EpicBlockProps) => { /> )} -
+
+ {epicUpdating && ( + {}} + /> + )}
{showDetail &&
{children}
} diff --git a/frontend/src/pages/backlog/EpicPage.tsx b/frontend/src/pages/backlog/EpicPage.tsx index bef7450b..8845c87d 100644 --- a/frontend/src/pages/backlog/EpicPage.tsx +++ b/frontend/src/pages/backlog/EpicPage.tsx @@ -28,6 +28,7 @@ const EpicPage = () => { ({ id: epicId, name, color, rankValue, storyList }) => ( 1} + epicList={epicCategoryList} epic={{ id: epicId, name, color, rankValue }} > {...storyList.map(({ id, title, point, status, taskList }) => { From abc50537b4fc8f79e544454c60f4fd162b5f6e92 Mon Sep 17 00:00:00 2001 From: surinkwon Date: Sat, 3 Aug 2024 17:33:11 +0900 Subject: [PATCH 7/8] =?UTF-8?q?fix:=20=EC=97=90=ED=94=BD=EB=B3=84=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=EC=97=90=EC=84=9C=20=EC=97=90?= =?UTF-8?q?=ED=94=BD=20=ED=81=B4=EB=A6=AD=EC=8B=9C=20=ED=95=B4=EB=8B=B9=20?= =?UTF-8?q?=EC=97=90=ED=94=BD=EB=A7=8C=20=EC=88=98=EC=A0=95,=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/backlog/EpicBlock.tsx | 10 ++-------- frontend/src/pages/backlog/EpicPage.tsx | 1 - 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/frontend/src/components/backlog/EpicBlock.tsx b/frontend/src/components/backlog/EpicBlock.tsx index fe67b43a..413f0bbe 100644 --- a/frontend/src/components/backlog/EpicBlock.tsx +++ b/frontend/src/components/backlog/EpicBlock.tsx @@ -9,16 +9,10 @@ import EpicDropdown from "./EpicDropdown"; interface EpicBlockProps { storyExist: boolean; epic: EpicCategoryDTO; - epicList: EpicCategoryDTO[]; children: React.ReactNode; } -const EpicBlock = ({ - storyExist, - epic, - epicList, - children, -}: EpicBlockProps) => { +const EpicBlock = ({ storyExist, epic, children }: EpicBlockProps) => { const { showDetail, handleShowDetail } = useShowDetail(); const { open: epicUpdating, @@ -66,7 +60,7 @@ const EpicBlock = ({ {epicUpdating && ( {}} /> )} diff --git a/frontend/src/pages/backlog/EpicPage.tsx b/frontend/src/pages/backlog/EpicPage.tsx index 8845c87d..bef7450b 100644 --- a/frontend/src/pages/backlog/EpicPage.tsx +++ b/frontend/src/pages/backlog/EpicPage.tsx @@ -28,7 +28,6 @@ const EpicPage = () => { ({ id: epicId, name, color, rankValue, storyList }) => ( 1} - epicList={epicCategoryList} epic={{ id: epicId, name, color, rankValue }} > {...storyList.map(({ id, title, point, status, taskList }) => { From 833ea860ebcae3e77f50fe9f971acfd01ce37793 Mon Sep 17 00:00:00 2001 From: surinkwon Date: Sun, 4 Aug 2024 21:34:40 +0900 Subject: [PATCH 8/8] =?UTF-8?q?fix:=20=EC=97=90=ED=94=BD=20=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=EB=A5=BC=20=EC=8A=A4=ED=86=A0=EB=A6=AC=20?= =?UTF-8?q?=EB=A6=AC=EC=8A=A4=ED=8A=B8=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= =?UTF-8?q?=ED=95=A0=20=EB=95=8C=20rankValue=EB=A5=BC=20=EB=84=98=EA=B2=A8?= =?UTF-8?q?=EC=A3=BC=EC=A7=80=20=EC=95=8A=EC=95=84=20=EC=83=9D=EA=B8=B0?= =?UTF-8?q?=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/utils/changeEpicListToStoryList.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/utils/changeEpicListToStoryList.ts b/frontend/src/utils/changeEpicListToStoryList.ts index b9f0fa14..e222bb05 100644 --- a/frontend/src/utils/changeEpicListToStoryList.ts +++ b/frontend/src/utils/changeEpicListToStoryList.ts @@ -3,9 +3,9 @@ import { EpicDTO } from "../types/DTO/backlogDTO"; const changeEpicListToStoryList = (epicList: EpicDTO[]) => { const newStoryList: UnfinishedStory[] = []; - epicList.forEach(({ id, name, color, storyList }) => { + epicList.forEach(({ id, name, color, rankValue, storyList }) => { storyList.forEach((story) => { - const newStory = { ...story, epic: { id, name, color } }; + const newStory = { ...story, epic: { id, name, color, rankValue } }; if (!newStory.taskList) { newStory.taskList = []; }