Skip to content

Commit

Permalink
feat: 스토리 수정, 삭제 API 연결
Browse files Browse the repository at this point in the history
스토리 에픽, 타이틀, 포인트, 상태 수정 API 연결
스토리 삭제 API 연결

스토리 블록의 크기가 커서 리팩토링 필요
  • Loading branch information
surinkwon committed Jul 11, 2024
1 parent ce73ce9 commit 2a38f47
Show file tree
Hide file tree
Showing 7 changed files with 277 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const BacklogStatusDropdown = ({
const statusList: BacklogStatusType[] = ["시작전", "진행중", "완료"];

return (
<div className="rounded-md w-fit shadow-box">
<div className="absolute top-0 bg-white rounded-md w-fit shadow-box">
<ul>
{...statusList.map((status) => (
<li
Expand Down
209 changes: 198 additions & 11 deletions frontend/src/components/backlog/StoryBlock.tsx
Original file line number Diff line number Diff line change
@@ -1,41 +1,184 @@
import { Socket } from "socket.io-client";
import { useOutletContext } from "react-router-dom";
import useShowDetail from "../../hooks/pages/backlog/useShowDetail";
import { BacklogStatusType } from "../../types/DTO/backlogDTO";
import { BacklogStatusType, EpicCategoryDTO } from "../../types/DTO/backlogDTO";
import BacklogStatusChip from "./BacklogStatusChip";
import CategoryChip from "./CategoryChip";
import TaskCreateButton from "./TaskCreateButton";
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 useBacklogInputChange from "../../hooks/pages/backlog/useBacklogInputChange";
import { MouseEvent } from "react";
import { MOUSE_KEY } from "../../constants/event";
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";

interface StoryBlockProps {
epic: string;
id: number;
epic: EpicCategoryDTO;
title: string;
point: number | null;
progress: number;
status: BacklogStatusType;
children: React.ReactNode;
taskExist: boolean;
epicList: EpicCategoryDTO[];
}

const StoryBlock = ({
id,
epic,
title,
point,
progress,
status,
taskExist,
epicList,
children,
}: StoryBlockProps) => {
const { socket }: { socket: Socket } = useOutletContext();
const { showDetail, handleShowDetail } = useShowDetail();
const {
updating: titleUpdating,
handleUpdating: handleTitleUpdatingOpen,
inputContainerRef: titleRef,
inputElementRef: titleInputRef,
} = useBacklogInputChange(updateTitle);
const {
updating: pointUpdating,
handleUpdating: handlePointUpdatingOpen,
inputContainerRef: pointRef,
inputElementRef: pointInputRef,
} = useBacklogInputChange(updatePoint);
const {
open: statusUpdating,
handleOpen: handleStatusUpdateOpen,
dropdownRef: statusRef,
} = useDropdownState();
const {
open: epicUpdating,
handleOpen: handleEpicUpdateOpen,
handleClose: handleEpicUpdateClose,
dropdownRef: epicRef,
} = useDropdownState();
const {
open: deleteMenuOpen,
handleOpen: handleDeleteMenuOpen,

dropdownRef: blockRef,
} = useDropdownState();
const { emitStoryUpdateEvent, emitStoryDeleteEvent } =
useStoryEmitEvent(socket);
const { open, close } = useModal();

function updateTitle<T>(data: T) {
if (!data || data === title) {
return;
}

if ((data as string).length > 100) {
alert("스토리 제목은 100자 이하여야 합니다.");
return;
}

emitStoryUpdateEvent({ id, title: data as string });
}
function updatePoint<T>(data: T) {
if ((!data && data !== 0) || data === point) {
return;
}

if ((data as number) < 0 || (data as number) > 100) {
alert("스토리 포인트는 0이상 100이하여야 합니다.");
return;
}

emitStoryUpdateEvent({ id, point: Number(data) });
}

function updateStatus(data: BacklogStatusType) {
if (data === status) {
return;
}
emitStoryUpdateEvent({ id, status: data as BacklogStatusType });
}

function updateEpic(data: number | undefined) {
if (data === epic.id) {
return;
}

emitStoryUpdateEvent({ id, epicId: data });
handleEpicUpdateClose();
}

const handleRightButtonClick = (event: MouseEvent) => {
if (event.button === MOUSE_KEY.RIGHT) {
handleDeleteMenuOpen();
}
};

const handleEpicColumnClick = () => {
if (!epicUpdating) {
handleEpicUpdateOpen();
}
};

const handleStoryDelete = () => {
emitStoryDeleteEvent({ id });
close();
};

const handleDeleteButtonClick = () => {
open(
<ConfirmModal
title="스토리 삭제"
body="해당 스토리와 연결된 모든 태스크가 삭제됩니다."
confirmText="삭제"
cancelText="취소"
confirmColor="#E33535"
cancelColor="#C6C6C6"
onCancelButtonClick={close}
onConfirmButtonClick={handleStoryDelete}
/>
);
};

return (
<>
<div className="flex items-center py-1 border-t border-b">
<div className="w-[5rem] mr-5">
<CategoryChip content={epic} bgColor="green" />
<div
className="flex items-center py-1 border-t border-b"
onMouseUp={handleRightButtonClick}
onContextMenu={(event) => event.preventDefault()}
ref={blockRef}
>
<div
className="w-[5rem] mr-5 hover:cursor-pointer"
onClick={handleEpicColumnClick}
ref={epicRef}
>
<CategoryChip content={epic.name} bgColor={epic.color} />

{epicUpdating && (
<EpicDropdown
selectedEpic={epic}
epicList={epicList}
onEpicChange={updateEpic}
/>
)}
</div>
<div className="flex items-center gap-1 w-[40.9rem] mr-4">
<div
className="flex items-center gap-1 w-[40.9rem] mr-4 hover:cursor-pointer"
onClick={() => handleTitleUpdatingOpen(true)}
ref={titleRef}
>
<button
className="flex items-center justify-center w-5 h-5 rounded-md hover:bg-dark-gray hover:bg-opacity-20"
type="button"
Expand All @@ -55,18 +198,62 @@ const StoryBlock = ({
/>
)}
</button>
<p>{title}</p>
{titleUpdating ? (
<input
className={`w-full rounded-sm focus:outline-none bg-gray-200 hover:cursor-pointer`}
type="text"
ref={titleInputRef}
defaultValue={title}
/>
) : (
<span className="w-full hover:cursor-pointer">{title}</span>
)}
</div>
<div className="w-[4rem] mr-[2.76rem] text-right">
<p className="">{point} POINT</p>
<div
className="flex items-center gap-1 w-[4rem] mr-[2.76rem] text-right hover:cursor-pointer"
onClick={() => handlePointUpdatingOpen(true)}
ref={pointRef}
>
{pointUpdating ? (
<input
className={`w-fit min-w-[1rem] max-w-[3.5rem] no-arrows text-right focus:outline-none rounded-sm bg-gray-200 hover:cursor-pointer`}
type="number"
ref={pointInputRef}
defaultValue={point !== 0 && !point ? 0 : point}
/>
) : (
<span>{point}</span>
)}

<span> POINT</span>
</div>
<div className="w-[4rem] mr-[2.76rem] text-right">
<span>{progress}%</span>
</div>
<div className="w-[6.25rem]">
<BacklogStatusChip status={status} />
<div
className="w-[6.25rem] hover:cursor-pointer relative"
onClick={handleStatusUpdateOpen}
>
<div ref={statusRef}>
<BacklogStatusChip status={status} />
</div>
{statusUpdating && (
<BacklogStatusDropdown onOptionClick={updateStatus} />
)}
</div>
</div>
{deleteMenuOpen && (
<div className="absolute px-2 py-1 bg-white rounded-md shadow-box">
<button
className="flex items-center w-full gap-3"
type="button"
onClick={handleDeleteButtonClick}
>
<TrashCan width={20} height={20} fill="red" />
<span>삭제</span>
</button>
</div>
)}
{showDetail && (
<TaskContainer>
<TaskHeader />
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/constants/event.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const MOUSE_KEY = {
LEFT: 0,
RIGHT: 2,
};
40 changes: 40 additions & 0 deletions frontend/src/hooks/pages/backlog/useBacklogInputChange.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { useEffect, useRef, useState } from "react";

const useBacklogInputChange = (update: <T>(data: T) => void) => {
const [updating, setUpdating] = useState<boolean>(false);
const inputContainerRef = useRef<HTMLDivElement | null>(null);
const inputElementRef = useRef<HTMLInputElement | null>(null);

const handleUpdating = (updating: boolean) => {
setUpdating(updating);
};

const handleOutsideClick = ({ target }: MouseEvent) => {
if (
inputContainerRef.current &&
!inputContainerRef.current.contains(target as Node)
) {
if (!updating) {
return;
}

if (inputElementRef.current) {
update(inputElementRef.current.value);
}

setUpdating(false);
}
};

useEffect(() => {
window.addEventListener("mouseup", handleOutsideClick);

return () => {
window.removeEventListener("mouseup", handleOutsideClick);
};
}, [updating]);

return { updating, inputContainerRef, inputElementRef, handleUpdating };
};

export default useBacklogInputChange;
28 changes: 28 additions & 0 deletions frontend/src/hooks/pages/backlog/useBacklogSocket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,34 @@ const useBacklogSocket = (socket: Socket) => {
return { epicList: newEpicList };
});
break;
case BacklogSocketStoryAction.UPDATE:
setBacklog((prevBacklog) => {
const newEpicList = prevBacklog.epicList.map((epic) => {
const newStoryList = epic.storyList.map((story) => {
if (story.id === content.id) {
return { ...story, ...content };
}
return story;
});
return { ...epic, storyList: newStoryList };
});

return { epicList: newEpicList };
});

break;
case BacklogSocketStoryAction.DELETE:
setBacklog((prevBacklog) => {
const newEpicList = prevBacklog.epicList.map((epic) => {
const newStoryList = epic.storyList.filter(
({ id }) => id !== content.id
);
return { ...epic, storyList: newStoryList };
});

return { epicList: newEpicList };
});
break;
}
};

Expand Down
7 changes: 4 additions & 3 deletions frontend/src/pages/backlog/UnfinishedStoryPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,13 @@ const UnfinishedStoryPage = () => {
return (
<div className="flex flex-col items-center gap-4">
<div className="w-full border-b">
{...storyList.map(({ epic, title, point, status, taskList }) => (
{...storyList.map(({ id, epic, title, point, status, taskList }) => (
<StoryBlock
{...{ title, point, status }}
epic={epic.name}
{...{ id, title, point, status }}
epic={epic}
progress={2}
taskExist={taskList.length > 0}
epicList={epicCategoryList}
>
{...taskList.map((task) => <TaskBlock {...task} />)}
</StoryBlock>
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/types/common/backlog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ export type BacklogCategoryColor =
| "purple"
| "gray";

export type BacklogInputField = "title" | "point";

export interface UnfinishedStory extends StoryDTO {
epic: EpicCategoryDTO;
}
Expand Down

0 comments on commit 2a38f47

Please sign in to comment.